summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components
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
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')
-rw-r--r--mobile/android/android-components/components/browser/domains/README.md67
-rw-r--r--mobile/android/android-components/components/browser/domains/build.gradle40
-rw-r--r--mobile/android/android-components/components/browser/domains/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/br50
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/ca49
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/de50
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/fr50
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/gb49
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/global444
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/hk50
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/id50
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/pl50
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/ru50
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/sg49
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/tw50
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/assets/domains/us49
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/CustomDomains.kt68
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domain.kt39
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/DomainAutoCompleteProvider.kt151
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domains.kt84
-rw-r--r--mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/autocomplete/Providers.kt110
-rw-r--r--mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/BaseDomainAutocompleteProviderTest.kt140
-rw-r--r--mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt81
-rw-r--r--mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainAutoCompleteProviderTest.kt116
-rw-r--r--mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainTest.kt39
-rw-r--r--mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainsTest.kt30
-rw-r--r--mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/ProvidersTest.kt85
-rw-r--r--mobile/android/android-components/components/browser/domains/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/README.md41
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/build.gradle193
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/docs/metrics.md8
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/geckoview.fml.yaml24
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/metrics.yaml30
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/androidTest/java/mozilla/components/browser/engine/gecko/fetch/geckoview/GeckoViewFetchTestCases.kt126
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt1502
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt1880
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionState.kt73
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineView.kt249
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoResult.kt76
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorage.kt146
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/NestedGeckoView.kt258
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegate.kt45
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegate.kt35
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegate.kt43
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt110
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt20
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorage.kt128
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepository.kt75
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt42
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt32
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoChoice.kt31
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoContentPermissions.kt25
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Login.kt43
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicy.kt82
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchClient.kt139
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/LocaleSettingUpdater.kt56
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/SettingUpdater.kt45
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegate.kt53
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediaquery/PreferredColorScheme.kt22
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionController.kt56
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegate.kt125
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt185
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt461
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt84
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegate.kt66
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt911
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegate.kt21
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegate.kt70
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegate.kt35
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegate.kt79
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslationUtils.kt63
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactory.kt154
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt449
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionException.kt72
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegate.kt39
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegate.kt73
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandler.kt31
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequest.kt23
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/experiment/NimbusExperimentDelegate.kt93
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionStateTest.kt61
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt4874
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt3673
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineViewTest.kt299
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt86
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorageTest.kt274
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoWebExtensionExceptionTest.kt116
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/NestedGeckoViewTest.kt580
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegateTest.kt75
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegateTest.kt62
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegateTest.kt28
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorageTest.kt161
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepositoryTest.kt74
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicyKtTest.kt81
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchUnitTestCases.kt351
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/integration/SettingUpdaterTest.kt69
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegateTest.kt116
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionControllerTest.kt49
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegateTest.kt219
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequestTest.kt242
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt740
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegateTest.kt81
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt2136
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegateTest.kt40
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegateTest.kt92
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegateTest.kt40
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegateTest.kt103
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactoryTest.kt129
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt644
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/MockWebExtension.kt115
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegateTest.kt117
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/MockWebNotification.kt37
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegateTest.kt154
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandlerTest.kt40
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequestTest.kt20
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/experiment/NimbusExperimentDelegateTest.kt68
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/test/ReflectionUtils.kt26
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/browser/engine-gecko/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/browser/engine-system/README.md58
-rw-r--r--mobile/android/android-components/components/browser/engine-system/build.gradle54
-rw-r--r--mobile/android/android-components/components/browser/engine-system/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/androidTest/java/mozilla/components/browser/engine/system/VersionTest.kt28
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/androidTest/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/NestedWebView.kt170
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngine.kt152
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSession.kt636
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSessionState.kt95
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineView.kt835
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/ReversibleString.kt98
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Safelist.kt176
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Trie.kt114
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/UrlMatcher.kt323
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.kt41
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/window/SystemWindowRequest.kt52
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_blocklist.json11046
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_safelist.json12347
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-am/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-an/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ann/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ar/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ast/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-az/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-azb/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ban/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-be/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-bg/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-bn/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-br/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-bs/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ca/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-cak/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ceb/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ckb/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-co/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-cs/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-cy/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-da/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-de/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-dsb/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-el/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rCA/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rGB/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-eo/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rAR/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rCL/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rES/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rMX/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-es/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-et/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-eu/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-fa/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ff/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-fi/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-fr/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-fur/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-fy-rNL/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ga-rIE/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-gd/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-gl/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-gn/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-gu-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-hi-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-hil/strings.xml5
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-hr/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-hsb/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-hu/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-hy-rAM/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ia/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-in/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-is/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-it/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-iw/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ja/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ka/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-kaa/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-kab/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-kk/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-kmr/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-kn/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ko/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-kw/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-lij/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-lo/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-lt/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-mix/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ml/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-mr/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-my/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-nb-rNO/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ne-rNP/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-nl/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-nn-rNO/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-nv/strings.xml5
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-oc/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-or/strings.xml5
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rPK/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-pl/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ppl/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rBR/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rPT/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-rm/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ro/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ru/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-sat/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-sc/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-si/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-sk/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-skr/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-sl/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-sq/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-sr/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-su/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-sv-rSE/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-szl/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ta/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-te/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-tg/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-th/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-tl/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-tok/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-tr/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-trs/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-tt/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-tzm/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ug/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-uk/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-ur/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-uz/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-vec/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-vi/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-yo/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rCN/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rTW/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/main/res/values/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/NestedWebViewTest.kt165
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionStateTest.kt138
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt1238
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineTest.kt105
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt1654
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/ReversibleStringTest.kt122
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/SafelistTest.kt126
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/TrieTest.kt46
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/UrlMatcherTest.kt296
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequestTest.kt99
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/window/SystemWindowRequestTest.kt76
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/browser/engine-system/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/browser/errorpages/README.md91
-rw-r--r--mobile/android/android-components/components/browser/errorpages/build.gradle42
-rw-r--r--mobile/android/android-components/components/browser/errorpages/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/assets/errorPageScripts.js122
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/assets/error_page_js.html58
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/assets/error_style.css165
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/java/mozilla/components/browser/errorpages/ErrorPages.kt249
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-am/strings.xml327
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-an/strings.xml247
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ann/strings.xml95
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ar/strings.xml223
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ast/strings.xml269
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-az/strings.xml121
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-azb/strings.xml98
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ban/strings.xml65
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-be/strings.xml262
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-bg/strings.xml206
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-bn/strings.xml135
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-br/strings.xml324
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-bs/strings.xml255
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ca/strings.xml311
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-cak/strings.xml274
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ceb/strings.xml293
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ckb/strings.xml142
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-co/strings.xml304
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-cs/strings.xml298
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-cy/strings.xml301
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-da/strings.xml309
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-de/strings.xml299
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-dsb/strings.xml303
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-el/strings.xml319
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rCA/strings.xml298
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rGB/strings.xml298
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-eo/strings.xml260
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rAR/strings.xml290
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rCL/strings.xml322
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rES/strings.xml303
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rMX/strings.xml252
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-es/strings.xml303
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-et/strings.xml259
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-eu/strings.xml308
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-fa/strings.xml260
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ff/strings.xml179
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-fi/strings.xml293
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-fr/strings.xml291
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-fur/strings.xml313
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-fy-rNL/strings.xml462
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ga-rIE/strings.xml284
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-gd/strings.xml263
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-gl/strings.xml308
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-gn/strings.xml276
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-gu-rIN/strings.xml163
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-hi-rIN/strings.xml221
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-hil/strings.xml87
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-hr/strings.xml267
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-hsb/strings.xml296
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-hu/strings.xml274
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-hy-rAM/strings.xml269
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ia/strings.xml330
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-in/strings.xml328
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-is/strings.xml284
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-it/strings.xml290
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-iw/strings.xml296
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ja/strings.xml302
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ka/strings.xml271
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-kaa/strings.xml319
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-kab/strings.xml322
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-kk/strings.xml264
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-kmr/strings.xml330
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-kn/strings.xml215
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ko/strings.xml290
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-kw/strings.xml124
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-lij/strings.xml250
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-lo/strings.xml321
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-lt/strings.xml275
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-mix/strings.xml138
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ml/strings.xml218
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-mr/strings.xml265
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-my/strings.xml254
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-nb-rNO/strings.xml326
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ne-rNP/strings.xml304
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-nl/strings.xml396
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-nn-rNO/strings.xml332
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-nv/strings.xml12
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-oc/strings.xml272
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-or/strings.xml133
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rIN/strings.xml330
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rPK/strings.xml50
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-pl/strings.xml309
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ppl/strings.xml115
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rBR/strings.xml327
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rPT/strings.xml280
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-rm/strings.xml263
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ro/strings.xml241
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ru/strings.xml304
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-sat/strings.xml304
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-sc/strings.xml164
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-si/strings.xml327
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-sk/strings.xml323
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-skr/strings.xml312
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-sl/strings.xml324
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-sq/strings.xml289
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-sr/strings.xml275
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-su/strings.xml279
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-sv-rSE/strings.xml314
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ta/strings.xml282
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-te/strings.xml258
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-tg/strings.xml330
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-th/strings.xml259
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-tl/strings.xml297
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-tok/strings.xml83
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-tr/strings.xml243
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-trs/strings.xml264
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-tt/strings.xml232
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-tzm/strings.xml25
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ug/strings.xml330
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-uk/strings.xml310
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-ur/strings.xml204
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-uz/strings.xml289
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-vec/strings.xml288
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-vi/strings.xml310
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-yo/strings.xml316
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rCN/strings.xml307
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rTW/strings.xml284
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/main/res/values/strings.xml307
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/test/java/mozilla/components/browser/errorpages/ErrorPagesTest.kt107
-rw-r--r--mobile/android/android-components/components/browser/errorpages/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/browser/icons/.gitignore1
-rw-r--r--mobile/android/android-components/components/browser/icons/README.md19
-rw-r--r--mobile/android/android-components/components/browser/icons/build.gradle92
-rw-r--r--mobile/android/android-components/components/browser/icons/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt70
-rw-r--r--mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt129
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/icons.js81
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.template.json22
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/assets/mozac.browser.icons/icons-top200.json1056
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt476
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/Icon.kt52
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequest.kt140
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderScope.kt59
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderState.kt35
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/Loader.kt50
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ICOIconDecoder.kt42
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/SvgIconDecoder.kt108
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ico/IconDirectoryEntry.kt315
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessage.kt118
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt53
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/WebAppManifest.kt49
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/DefaultIconGenerator.kt106
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/IconGenerator.kt18
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DataUriIconLoader.kt36
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DiskIconLoader.kt26
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/HttpIconLoader.kt116
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconLoader.kt34
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/MemoryIconLoader.kt27
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoader.kt43
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/pipeline/IconResourceComparator.kt75
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/DiskIconPreparer.kt32
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/IconPreprarer.kt16
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/MemoryIconPreparer.kt29
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparer.kt89
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessor.kt76
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ColorProcessor.kt37
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/DiskIconProcessor.kt52
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/IconProcessor.kt24
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/MemoryIconProcessor.kt42
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ResizingProcessor.kt67
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconDiskCache.kt185
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconMemoryCache.kt62
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/Utils.kt64
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/res/values/colors.xml18
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/res/values/dimens.xml13
-rw-r--r--mobile/android/android-components/components/browser/icons/src/main/res/values/tags.xml7
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt339
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt238
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/SvgIconDecoderTest.kt156
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt223
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageKtTest.kt62
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt66
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DataUriIconLoaderTest.kt93
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DiskIconLoaderTest.kt60
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/FailureCacheTest.kt51
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/HttpIconLoaderTest.kt273
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/MemoryIconLoaderTest.kt62
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.kt318
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/pipeline/IconResourceComparatorTest.kt354
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/DiskIconPreparerTest.kt69
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/MemoryIconPreparerTest.kt69
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparerTest.kt110
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessorTest.kt89
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ColorProcessorTest.kt53
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/DiskIconProcessorTest.kt117
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/MemoryIconProcessorTest.kt85
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ResizingProcessorTest.kt124
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconDiskCacheTest.kt106
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconMemoryCacheTest.kt62
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/bmp/test.bmpbin0 -> 30122 bytes
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/gif/cat.gifbin0 -> 844849 bytes
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/ico/golem_favicon.icobin0 -> 40648 bytes
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/ico/microsoft_favicon.icobin0 -> 17174 bytes
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/ico/nvidia_favicon.icobin0 -> 25214 bytes
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/jpg/tonys.jpgbin0 -> 83782 bytes
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/misc/test.txt1
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker3
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/png/mozac.pngbin0 -> 406 bytes
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/browser/icons/src/test/resources/webp/test.webpbin0 -> 2010 bytes
-rw-r--r--mobile/android/android-components/components/browser/menu/README.md152
-rw-r--r--mobile/android/android-components/components/browser/menu/build.gradle57
-rw-r--r--mobile/android/android-components/components/browser/menu/lint.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/AndroidManifest.xml6
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenu.kt320
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuAdapter.kt68
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuBuilder.kt29
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuHighlight.kt109
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuItem.kt64
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPlacement.kt64
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPositioning.kt143
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenu.kt141
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilder.kt166
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/BrowserMenuItem.kt35
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/View.kt16
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/facts/BrowserMenuFacts.kt45
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItem.kt66
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BackPressMenuItem.kt72
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCategory.kt81
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCheckbox.kt33
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButton.kt72
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuDivider.kt30
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItem.kt212
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitch.kt99
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitch.kt59
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageText.kt111
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButton.kt111
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbar.kt212
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuSwitch.kt33
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/CustomTooltip.kt154
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/ParentBrowserMenuItem.kt105
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItem.kt109
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItem.kt84
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageText.kt133
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItem.kt168
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionPlaceholderMenuItem.kt36
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerView.kt86
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/ExpandableLayout.kt436
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/MenuButton.kt211
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManager.kt74
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManager.kt73
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyItemLayoutManager.kt483
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_bottom.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_top.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_bottom.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_top.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_exit.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_browser_menu_notification_icon.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_indicator.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_notification.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/drawable/rounded_corner.xml9
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu.xml25
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_button.xml39
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_category.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_item.xml92
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_switch.xml60
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_checkbox.xml16
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_divider.xml8
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_switch.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text.xml32
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text_checkbox_button.xml69
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_parent_menu.xml48
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_simple.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_switch.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_toolbar.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_web_extension.xml57
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_tooltip_layout.xml27
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-am/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-an/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ar/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ast/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-az/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-azb/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ban/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-be/strings.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-bg/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-bn/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-br/strings.xml17
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-bs/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ca/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-cak/strings.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ceb/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ckb/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-co/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-cs/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-cy/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-da/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-de/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-dsb/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-el/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-en-rCA/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-en-rGB/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-eo/strings.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-es-rAR/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-es-rCL/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-es-rES/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-es-rMX/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-es/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-et/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-eu/strings.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-fa/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ff/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-fi/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-fr/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-fur/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-fy-rNL/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-gd/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-gl/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-gn/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-gu-rIN/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-hi-rIN/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-hil/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-hr/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-hsb/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-hu/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-hy-rAM/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ia/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-in/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-is/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-it/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-iw/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ja/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ka/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-kaa/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-kab/strings.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-kk/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-kmr/strings.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-kn/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ko/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-kw/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ldrtl/dimens.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-lij/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-lo/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-lt/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-mix/strings.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-mr/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-my/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-nb-rNO/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ne-rNP/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-nl/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-nn-rNO/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-oc/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-or/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rIN/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rPK/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-pl/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rBR/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rPT/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-rm/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ro/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ru/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-sat/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-sc/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-si/strings.xml17
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-sk/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-skr/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-sl/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-sq/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-sr/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-su/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-sv-rSE/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-szl/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ta/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-te/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-tg/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-th/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-tl/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-tok/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-tr/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-trs/strings.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-tt/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-tzm/strings.xml5
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ug/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-uk/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-ur/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-uz/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-vec/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-vi/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-yo/strings.xml11
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rCN/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rTW/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values/colors.xml8
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values/dimens.xml80
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values/strings.xml24
-rw-r--r--mobile/android/android-components/components/browser/menu/src/main/res/values/style.xml106
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuAdapterTest.kt210
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuBuilderTest.kt44
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuHighlightTest.kt49
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuPositioningTest.kt163
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuTest.kt496
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilderTest.kt510
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt525
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/ext/BrowserMenuItemTest.kt113
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItemTest.kt90
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCategoryTest.kt183
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCheckboxTest.kt38
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButtonTest.kt190
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuDividerTest.kt47
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItemTest.kt334
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitchTest.kt124
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitchTest.kt60
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButtonTest.kt204
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextTest.kt131
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbarTest.kt439
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuSwitchTest.kt38
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/ParentBrowserMenuItemTest.kt150
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItemTest.kt152
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItemTest.kt122
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageTextTest.kt87
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt355
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerViewTest.kt226
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/ExpandableLayoutTest.kt822
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemLayoutManager.kt32
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemsAdapter.kt23
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/MenuButtonTest.kt172
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManagerTest.kt189
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManagerTest.kt155
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyItemsLinearLayoutManagerTest.kt631
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker3
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/browser/menu/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/browser/menu2/README.md51
-rw-r--r--mobile/android/android-components/components/browser/menu2/build.gradle50
-rw-r--r--mobile/android/android-components/components/browser/menu2/lint.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt182
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt81
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt47
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt25
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt33
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt80
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt53
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt55
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt60
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt55
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt224
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt51
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt71
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt62
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt261
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt95
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt118
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt95
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml39
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml36
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml18
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml8
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml31
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml5
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml8
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml48
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml88
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt132
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt99
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt61
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt24
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt75
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt103
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt130
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt117
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt177
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt86
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt68
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt856
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt143
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt120
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt92
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker3
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/browser/session-storage/README.md19
-rw-r--r--mobile/android/android-components/components/browser/session-storage/build.gradle74
-rw-r--r--mobile/android/android-components/components/browser/session-storage/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/androidTest/assets/index.html8
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/FullRestoreTest.kt113
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/RestoringBrowsingSessionsTest.kt310
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt247
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorage.kt66
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt18
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt128
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt258
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt164
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt47
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt384
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt84
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt405
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt441
-rw-r--r--mobile/android/android-components/components/browser/session-storage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/browser/state/README.md23
-rw-r--r--mobile/android/android-components/components/browser/state/build.gradle82
-rw-r--r--mobile/android/android-components/components/browser/state/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/state/src/androidTest/java/mozilla/components/browser/state/helper/OnDeviceTargetTest.kt177
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/ActionWithTab.kt32
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/AwesomeBarAction.kt30
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt1838
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineMiddleware.kt61
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt491
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CrashMiddleware.kt47
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddleware.kt134
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddleware.kt224
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddleware.kt45
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/LinkingMiddleware.kt137
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt145
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SuspendMiddleware.kt60
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddleware.kt75
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt845
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddleware.kt92
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddleware.kt78
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt19
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt15
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/helper/Target.kt96
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/AwesomeBarStateReducer.kt30
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt161
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContainerReducer.kt38
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt385
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CookieBannerStateReducer.kt17
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducer.kt28
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt32
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt42
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DebugReducer.kt24
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DownloadStateReducer.kt43
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt110
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducer.kt17
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/HistoryMetadataReducer.kt50
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtils.kt18
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LastAccessReducer.kt45
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LocaleStateReducer.kt21
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/MediaSessionReducer.kt142
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ReaderStateReducer.kt59
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/RecentlyClosedReducer.kt35
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SearchReducer.kt206
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducer.kt21
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt23
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabGroupReducer.kt169
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt358
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TrackingProtectionStateReducer.kt45
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt446
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/UndoReducer.kt46
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/WebExtensionReducer.kt117
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/RegionState.kt25
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt75
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/selector/Selectors.kt187
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AppIntentState.kt18
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AwesomeBarState.kt19
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt59
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContainerState.kt56
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt109
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabConfig.kt107
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt98
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt39
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LastMediaAccessState.kt29
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LoadRequestState.kt18
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/MediaSessionState.kt40
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ReaderState.kt30
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt94
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SecurityInfoState.kt19
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt201
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabPartition.kt70
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt150
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TrackingProtectionState.kt24
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt29
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsState.kt43
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt24
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/WebExtensionState.kt37
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt97
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/FindResultState.kt14
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/HistoryState.kt20
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt52
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/ShareInternetResourceState.kt25
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/extension/WebExtensionPromptRequest.kt80
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/recover/RecoverableTab.kt119
-rw-r--r--mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/store/BrowserStore.kt49
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/AwesomeBarActionTest.kt111
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContainerActionTest.kt113
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt894
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CookieBannerActionTest.kt49
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CustomTabListActionTest.kt92
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DebugActionTest.kt34
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DownloadActionTest.kt132
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt140
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/HistoryMetadataActionTest.kt67
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LastAccessActionTest.kt32
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LocaleActionTest.kt32
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/MediaSessionActionTest.kt304
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ReaderActionTest.kt154
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/SearchActionTest.kt409
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt319
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt1477
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TrackingProtectionActionTest.kt171
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt903
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/UpdateProductUrlStateActionTest.kt77
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/WebExtensionActionTest.kt420
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt76
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt1832
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt148
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt213
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt813
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddlewareTest.kt44
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt246
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt155
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt155
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt246
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt945
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt257
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddlewareTest.kt135
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/helper/TargetTest.kt138
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/BrowserStateReducerKtTest.kt242
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducerTest.kt59
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/DebugReducerTest.kt33
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducerTest.kt41
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtilsTest.kt26
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LastAccessReducerTest.kt128
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LocaleStateReducerTest.kt41
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducerTest.kt60
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/selector/SelectorsKtTest.kt306
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/LanguageSettingTest.kt85
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt248
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TabPartitionTest.kt65
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationEngineStateTest.kt123
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationSupportTest.kt141
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/content/PermissionHighlightsStateTest.kt78
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt201
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt95
-rw-r--r--mobile/android/android-components/components/browser/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/README.md22
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/build.gradle64
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Connection.kt127
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProvider.kt36
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorage.kt299
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt409
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.kt41
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesStorage.kt228
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt154
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageExtensions.kt44
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageMaintenanceWorker.kt48
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Types.kt241
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProviderTest.kt32
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt239
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt1312
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorkerTest.kt93
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesStorageTest.kt109
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt174
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/bookmarks-v23.dbbin0 -> 270336 bytes
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/empty-v0.dbbin0 -> 4096 bytes
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/history-v34.dbbin0 -> 360448 bytes
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/pinnedSites-v39.dbbin0 -> 253952 bytes
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v38.dbbin0 -> 335873 bytes
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.dbbin0 -> 598016 bytes
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-shmbin0 -> 32768 bytes
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-wal0
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/withHistory-v39.dbbin0 -> 303104 bytes
-rw-r--r--mobile/android/android-components/components/browser/storage-sync/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/browser/tabstray/README.md19
-rw-r--r--mobile/android/android-components/components/browser/tabstray/build.gradle55
-rw-r--r--mobile/android/android-components/components/browser/tabstray/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt17
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabTouchCallback.kt53
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabViewHolder.kt152
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt105
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt35
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTrayStyling.kt31
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailView.kt33
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/res/layout/mozac_browser_tabstray_item.xml68
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/res/values/ids.xml8
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/main/res/values/mozac_browser_tabstray_strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/DefaultTabViewHolderTest.kt223
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabTouchCallbackTest.kt73
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabViewHolderTest.kt52
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt161
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailViewTest.kt81
-rw-r--r--mobile/android/android-components/components/browser/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/README.md81
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/build.gradle63
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/BrowserThumbnails.kt86
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsMiddleware.kt72
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt71
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorage.kt133
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt130
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/main/res/values/dimens.xml8
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/main/res/values/tags.xml8
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt139
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsMiddlewareTest.kt199
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt93
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt142
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt148
-rw-r--r--mobile/android/android-components/components/browser/thumbnails/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/browser/toolbar/README.md37
-rw-r--r--mobile/android/android-components/components/browser/toolbar/build.gradle56
-rw-r--r--mobile/android/android-components/components/browser/toolbar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt691
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt711
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbarView.kt51
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/HighlightView.kt91
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/MenuButton.kt106
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/OriginView.kt198
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/SiteSecurityIconView.kt48
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt135
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt415
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/facts/ToolbarFacts.kt60
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionContainer.kt134
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionWrapper.kt16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_browser_toolbar_icons_vertical_separator.xml9
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_dot_notification.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_site_security.xml11
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_off_for_a_site.xml12
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_no_trackers_blocked.xml12
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_trackers_blocked.xml12
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml175
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_edittoolbar.xml116
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-am/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-an/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ar/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ast/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-az/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-azb/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ban/strings.xml8
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-be/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-bg/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-bn/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-br/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-bs/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ca/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-cak/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ceb/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ckb/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-co/strings.xml19
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-cs/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-cy/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-da/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-de/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-dsb/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-el/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rCA/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rGB/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-eo/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rAR/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rCL/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rES/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rMX/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-es/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-et/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-eu/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-fa/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ff/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-fi/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-fr/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-fur/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-fy-rNL/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ga-rIE/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-gd/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-gl/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-gn/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-gu-rIN/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-hi-rIN/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-hil/strings.xml8
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-hr/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-hsb/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-hu/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-hy-rAM/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ia/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-in/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-is/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-it/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-iw/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ja/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ka/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-kaa/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-kab/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-kk/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-kmr/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-kn/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ko/strings.xml19
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ldrtl/dimens.xml8
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-lij/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-lo/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-lt/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-mix/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ml/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-mr/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-my/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-nb-rNO/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ne-rNP/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-nl/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-nn-rNO/strings.xml19
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-oc/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-or/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rIN/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rPK/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-pl/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rBR/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rPT/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-rm/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ro/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ru/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-sat/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-sc/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-si/strings.xml19
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-sk/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-skr/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-sl/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-sq/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-sr/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-su/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-sv-rSE/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-szl/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ta/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-te/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-tg/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-th/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-tl/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-tok/strings.xml14
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-tr/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-trs/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-tt/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-tzm/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ug/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-uk/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-ur/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-uz/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-vec/strings.xml16
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-vi/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-yo/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml18
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values/attrs_browser_toolbar.xml33
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values/dimens.xml31
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values/ids.xml8
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/main/res/values/strings.xml21
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt350
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt1044
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt824
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/HighlightViewTest.kt67
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/MenuButtonTest.kt156
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconViewTest.kt43
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt290
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/internal/ActionContainerTest.kt99
-rw-r--r--mobile/android/android-components/components/browser/toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/README.md19
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/build.gradle68
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBar.kt121
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarColors.kt18
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarDefaults.kt39
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarFacts.kt44
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarOrientation.kt13
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/Suggestion.kt184
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionFetcher.kt160
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionGroup.kt35
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/Suggestions.kt168
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ar/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ban/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ceb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hi-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-mr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-my/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ne-rNP/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-or/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-szl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ta/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tok/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ur/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-yo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/test/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionFetcherTest.kt69
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/test/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionsTest.kt173
-rw-r--r--mobile/android/android-components/components/compose/awesomebar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/README.md19
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/build.gradle54
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserDisplayToolbar.kt62
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserEditToolbar.kt93
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserToolbar.kt77
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ar/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ban/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ceb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hi-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hil/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-mix/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-mr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-my/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ne-rNP/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-szl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ta/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-te/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tok/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tzm/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ur/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-vec/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-yo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/compose/browser-toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/compose/cfr/README.md49
-rw-r--r--mobile/android/android-components/components/compose/cfr/build.gradle63
-rw-r--r--mobile/android/android-components/components/compose/cfr/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt195
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt186
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt537
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt272
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt65
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt19
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt561
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt136
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.kt32
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/compose/engine/README.md19
-rw-r--r--mobile/android/android-components/components/compose/engine/build.gradle61
-rw-r--r--mobile/android/android-components/components/compose/engine/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/compose/engine/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/compose/engine/src/main/java/mozilla/components/compose/engine/WebContent.kt62
-rw-r--r--mobile/android/android-components/components/compose/engine/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/compose/engine/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/compose/tabstray/README.md19
-rw-r--r--mobile/android/android-components/components/compose/tabstray/build.gradle58
-rw-r--r--mobile/android/android-components/components/compose/tabstray/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/Tab.kt97
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/TabCounterButton.kt79
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/TabList.kt77
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/drawable/mozac_tabcounter_background.xml15
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-am/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ar/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ast/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-azb/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-be/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-bg/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-br/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-bs/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ca/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-cak/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ceb/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ckb/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-co/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-cs/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-cy/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-da/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-de/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-dsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-el/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-en-rCA/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-en-rGB/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-eo/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rAR/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rCL/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rES/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rMX/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-es/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-et/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-eu/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-fa/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-fi/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-fr/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-fur/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-fy-rNL/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-gd/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-gl/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-gn/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-hi-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-hr/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-hsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-hu/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-hy-rAM/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ia/strings.xml8
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-in/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-is/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-it/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-iw/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ja/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ka/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-kaa/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-kab/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-kk/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-kmr/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ko/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-lo/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-lt/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-mr/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-my/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-nb-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ne-rNP/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-nl/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-nn-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-oc/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-pa-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-pa-rPK/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-pl/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-pt-rBR/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-pt-rPT/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-rm/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ro/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ru/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-sat/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-sc/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-si/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-sk/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-skr/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-sl/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-sq/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-sr/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-su/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-sv-rSE/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-szl/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ta/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-tg/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-th/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-tl/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-tr/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-trs/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-tt/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ug/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-uk/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-ur/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-uz/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-vec/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-vi/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-yo/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-zh-rCN/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values-zh-rTW/strings.xml7
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/main/res/values/strings.xml10
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/compose/tabstray/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/concept/awesomebar/README.md47
-rw-r--r--mobile/android/android-components/components/concept/awesomebar/build.gradle33
-rw-r--r--mobile/android/android-components/components/concept/awesomebar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/awesomebar/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/awesomebar/src/main/java/mozilla/components/concept/awesomebar/AwesomeBar.kt222
-rw-r--r--mobile/android/android-components/components/concept/base/README.md21
-rw-r--r--mobile/android/android-components/components/concept/base/build.gradle46
-rw-r--r--mobile/android/android-components/components/concept/base/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/base/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/Breadcrumb.kt134
-rw-r--r--mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/CrashReporting.kt23
-rw-r--r--mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/RustCrashReport.kt19
-rw-r--r--mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/images/ImageLoader.kt32
-rw-r--r--mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/images/ImageRequest.kt28
-rw-r--r--mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/memory/MemoryConsumer.kt26
-rw-r--r--mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/profiler/Profiler.kt155
-rw-r--r--mobile/android/android-components/components/concept/base/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/concept/base/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/concept/engine/README.md45
-rw-r--r--mobile/android/android-components/components/concept/engine/build.gradle52
-rw-r--r--mobile/android/android-components/components/concept/engine/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/CancellableOperation.kt31
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/DataCleanable.kt26
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt282
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt1103
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSessionState.kt51
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineView.kt201
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/HitResult.kt52
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/InputResultDetail.kt378
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/Settings.kt325
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/activity/ActivityDelegate.kt22
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/activity/OrientationDelegate.kt28
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/Tracker.kt20
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackerLog.kt23
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackingProtectionException.kt16
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackingProtectionExceptionStorage.kt57
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/cookiehandling/CookieBannersStorage.kt68
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/history/HistoryItem.kt15
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/history/HistoryTrackingDelegate.kt47
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/Size.kt59
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifest.kt253
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifestParser.kt238
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/ShareTargetParser.kt129
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/WebAppManifestIconParser.kt86
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/media/RecordingDevice.kt32
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/mediaquery/PreferredColorScheme.kt17
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/mediasession/MediaSession.kt193
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt158
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissions.kt96
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissionsStorage.kt82
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/Choice.kt63
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt445
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/ShareData.kt24
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/request/RequestInterceptor.kt107
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/search/SearchRequest.kt10
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/selection/SelectionActionDelegate.kt47
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/serviceworker/ServiceWorkerDelegate.kt25
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductAnalysis.kt51
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductAnalysisStatus.kt16
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductRecommendation.kt32
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/DetectedLanguages.kt20
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/Language.kt16
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/LanguageModel.kt22
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/LanguageSetting.kt125
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/ModelManagementOptions.kt19
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/ModelOperation.kt20
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/OperationLevel.kt26
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationDownloadSize.kt27
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationEngineState.kt46
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationError.kt175
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationOperation.kt48
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationOptions.kt15
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPageSettingOperation.kt32
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPageSettings.kt26
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPair.kt16
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationSupport.kt60
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationsRuntime.kt215
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/utils/EngineVersion.kt115
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/Action.kt54
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/InstallationMethod.kt20
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt677
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.kt177
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionRuntime.kt220
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotification.kt44
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotificationDelegate.kt25
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webpush/WebPush.kt80
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webpush/WebPushDelegate.kt28
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/window/WindowRequest.kt44
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/identitycredential/Account.kt25
-rw-r--r--mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/identitycredential/Provider.kt24
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineSessionTest.kt1099
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineTest.kt140
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineViewTest.kt84
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/HitResultTest.kt25
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/InputResultDetailTest.kt474
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/SettingsTest.kt227
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/SizeTest.kt45
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/WebAppManifestParserTest.kt603
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/mediasession/MediaSessionTest.kt230
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionRequestTest.kt89
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionTest.kt32
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/ChoiceTest.kt37
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt366
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/ShareDataTest.kt40
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/request/RequestInterceptorTest.kt37
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/utils/EngineVersionTest.kt186
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/ActionTest.kt58
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt105
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webpush/WebPushSubscriptionTest.kt112
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/example_google.json21
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/example_mdn.json37
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/invalid_json.json3
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/invalid_missing_name.json3
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal.json4
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal_share_target.json13
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal_short_name.json4
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/purpose_array.json23
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/spec_typical.json51
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/squoosh.json32
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/twitter_mobile.json1
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/manifests/unusual.json35
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/concept/engine/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/concept/fetch/README.md166
-rw-r--r--mobile/android/android-components/components/concept/fetch/build.gradle44
-rw-r--r--mobile/android/android-components/components/concept/fetch/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Client.kt98
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Headers.kt168
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Request.kt190
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Response.kt160
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/interceptor/Interceptor.kt93
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ClientTest.kt36
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/HeadersTest.kt240
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/RequestTest.kt191
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ResponseTest.kt258
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/interceptor/InterceptorTest.kt132
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/concept/fetch/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/concept/menu/build.gradle38
-rw-r--r--mobile/android/android-components/components/concept/menu/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuButton.kt47
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt53
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuStyle.kt44
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Orientation.kt39
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Side.kt20
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/ContainerStyle.kt16
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuCandidate.kt123
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuEffect.kt46
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuIcon.kt97
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/SmallMenuCandidate.kt22
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/TextStyle.kt46
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/ext/MenuCandidate.kt63
-rw-r--r--mobile/android/android-components/components/concept/menu/src/test/java/mozilla/components/concept/menu/ext/MenuCandidateTest.kt179
-rw-r--r--mobile/android/android-components/components/concept/push/README.md23
-rw-r--r--mobile/android/android-components/components/concept/push/build.gradle38
-rw-r--r--mobile/android/android-components/components/concept/push/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushProcessor.kt87
-rw-r--r--mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushService.kt41
-rw-r--r--mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/exceptions/SubscriptionException.kt17
-rw-r--r--mobile/android/android-components/components/concept/push/src/test/java/mozilla/components/concept/push/PushErrorTest.kt32
-rw-r--r--mobile/android/android-components/components/concept/push/src/test/java/mozilla/components/concept/push/PushProcessorTest.kt55
-rw-r--r--mobile/android/android-components/components/concept/push/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/concept/storage/README.md28
-rw-r--r--mobile/android/android-components/components/concept/storage/build.gradle41
-rw-r--r--mobile/android/android-components/components/concept/storage/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/BookmarksStorage.kt182
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/Cancellable.kt42
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt503
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/HistoryMetadataStorage.kt193
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/HistoryStorage.kt237
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/KeyManager.kt108
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/KeyProvider.kt58
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt277
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/Storage.kt20
-rw-r--r--mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceRegistry.kt27
-rw-r--r--mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/AddressTest.kt92
-rw-r--r--mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/BookmarkNodeTest.kt119
-rw-r--r--mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/CreditCardEntryTest.kt94
-rw-r--r--mobile/android/android-components/components/concept/sync/README.md26
-rw-r--r--mobile/android/android-components/components/concept/sync/build.gradle37
-rw-r--r--mobile/android/android-components/components/concept/sync/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt65
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt175
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt358
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt53
-rw-r--r--mobile/android/android-components/components/concept/tabstray/README.md19
-rw-r--r--mobile/android/android-components/components/concept/tabstray/build.gradle33
-rw-r--r--mobile/android/android-components/components/concept/tabstray/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/tabstray/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/Tab.kt42
-rw-r--r--mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/Tabs.kt21
-rw-r--r--mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/TabsTray.kt44
-rw-r--r--mobile/android/android-components/components/concept/toolbar/README.md19
-rw-r--r--mobile/android/android-components/components/concept/toolbar/build.gradle39
-rw-r--r--mobile/android/android-components/components/concept/toolbar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteDelegate.kt24
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteProvider.kt36
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteResult.kt22
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/ScrollableToolbar.kt34
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt563
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionButtonTest.kt76
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionImageTest.kt84
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionSpaceTest.kt47
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionToggleButtonTest.kt213
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/README.md64
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/build.gradle61
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/FxaPushSupportFeature.kt392
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt65
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabUseCases.kt193
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/PushScopeProperty.kt43
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/ScopeProperty.kt18
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/ext/String.kt34
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AccountObserverTest.kt167
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AutoPushObserverTest.kt134
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ConstellationObserverTest.kt185
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/EventsObserverTest.kt48
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/FxaPushSupportFeatureTest.kt83
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabFeatureKtTest.kt67
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabUseCasesTest.kt331
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/VerificationDelegateTest.kt140
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ext/StringKtTest.kt44
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/accounts/.gitignore1
-rw-r--r--mobile/android/android-components/components/feature/accounts/README.md18
-rw-r--r--mobile/android/android-components/components/feature/accounts/build.gradle58
-rw-r--r--mobile/android/android-components/components/feature/accounts/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/background.js22
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/fxawebchannel.js25
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/manifest.template.json33
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeature.kt119
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FxaWebChannelFeature.kt407
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt304
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FxaWebChannelFeatureTest.kt830
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/accounts/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/addons/README.md19
-rw-r--r--mobile/android/android-components/components/feature/addons/build.gradle89
-rw-r--r--mobile/android/android-components/components/feature/addons/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/addons/schemas/mozilla.components.feature.addons.update.db.UpdateAttemptsDatabase/1.json58
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/AndroidManifest.xml11
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/Addon.kt540
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/AddonManager.kt528
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/AddonsProvider.kt28
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/amo/AMOAddonsProvider.kt489
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidate.kt54
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/menu/WebExtensionNestedMenuCandidate.kt148
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/migration/SupportedAddonsChecker.kt144
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt54
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonFilePicker.kt90
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragment.kt254
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonPermissionsAdapter.kt73
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonsManagerAdapter.kt548
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterDelegate.kt63
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/CustomViewHolder.kt65
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/Extensions.kt143
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/PermissionsDialogFragment.kt281
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/RequiredPermissionsAdapter.kt52
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapter.kt112
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterDelegate.kt24
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/AddonUpdater.kt677
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/GlobalAddonDependencyProvider.kt43
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptDao.kt25
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptEntity.kt81
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptsDatabase.kt34
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/worker/Extensions.kt20
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_footer_section_item.xml15
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_fragment_dialog_addon_installed.xml84
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_fragment_dialog_addon_permissions.xml93
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_header_section_item.xml52
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_item.xml156
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_permission_item.xml28
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_permissions_required_item.xml28
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_section_item.xml30
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_section_unsupported_section_item.xml60
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_unsupported_item.xml51
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-am/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-an/strings.xml207
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ar/strings.xml215
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ast/strings.xml215
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-az/strings.xml159
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-azb/strings.xml267
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-be/strings.xml271
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-bg/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-bn/strings.xml211
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-br/strings.xml277
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-bs/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ca/strings.xml259
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-cak/strings.xml269
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ceb/strings.xml211
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ckb/strings.xml193
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-co/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-cs/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-cy/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-da/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-de/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-dsb/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-el/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-en-rCA/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-en-rGB/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-eo/strings.xml269
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-es-rAR/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-es-rCL/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-es-rES/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-es-rMX/strings.xml257
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-es/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-et/strings.xml237
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-eu/strings.xml269
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-fa/strings.xml215
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ff/strings.xml171
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-fi/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-fr/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-fur/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-fy-rNL/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ga-rIE/strings.xml145
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-gd/strings.xml215
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-gl/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-gn/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-gu-rIN/strings.xml179
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-hi-rIN/strings.xml193
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-hil/strings.xml39
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-hr/strings.xml255
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-hsb/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-hu/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-hy-rAM/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ia/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-in/strings.xml270
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-is/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-it/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-iw/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ja/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ka/strings.xml257
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-kaa/strings.xml215
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-kab/strings.xml267
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-kk/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-kmr/strings.xml269
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-kn/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ko/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-lij/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-lo/strings.xml245
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-lt/strings.xml213
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-mix/strings.xml55
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ml/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-mr/strings.xml196
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-my/strings.xml211
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-nb-rNO/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ne-rNP/strings.xml211
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-nl/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-nn-rNO/strings.xml283
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-oc/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-or/strings.xml113
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-pa-rIN/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-pa-rPK/strings.xml257
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-pl/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-pt-rBR/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-pt-rPT/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-rm/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ro/strings.xml205
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ru/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-sat/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-sc/strings.xml245
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-si/strings.xml275
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-sk/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-skr/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-sl/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-sq/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-sr/strings.xml255
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-su/strings.xml271
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-sv-rSE/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-szl/strings.xml215
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ta/strings.xml211
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-te/strings.xml211
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-tg/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-th/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-tl/strings.xml213
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-tr/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-trs/strings.xml223
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-tt/strings.xml209
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-tzm/strings.xml55
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ug/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-uk/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-ur/strings.xml209
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-uz/strings.xml213
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-vec/strings.xml216
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-vi/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-yo/strings.xml213
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-zh-rCN/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values-zh-rTW/strings.xml297
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values/colors.xml11
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values/dimens.xml9
-rw-r--r--mobile/android/android-components/components/feature/addons/src/main/res/values/strings.xml300
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt1001
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/AddonTest.kt514
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/amo/AMOAddonsProviderTest.kt684
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidateTest.kt126
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionNestedMenuCandidateTest.kt182
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/DefaultSupportedAddonCheckerTest.kt113
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/SupportedAddonsWorkerTest.kt106
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonDialogFragmentTest.kt71
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonFilePickerTest.kt151
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragmentTest.kt201
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterTest.kt856
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsPermissionsAdapterTest.kt49
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/ExtensionsTest.kt162
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/PermissionsDialogFragmentTest.kt248
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterTest.kt123
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/AddonUpdaterWorkerTest.kt225
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/DefaultAddonUpdaterTest.kt430
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/GlobalAddonDependencyProviderTest.kt44
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/NotificationHandlerServiceTest.kt71
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/UpdateAttemptStorageTest.kt72
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/UpdateStatusStorageTest.kt61
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/db/UpdateAttemptEntityTest.kt115
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/worker/ExtensionsTest.kt40
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_localized_single_result.json231
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_multiple_results.json689
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_single_result.json364
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/collection.json367
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/collection_with_empty_values.json263
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/localized_collection.json224
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/png/mozac.pngbin0 -> 406 bytes
-rw-r--r--mobile/android/android-components/components/feature/addons/src/test/resources/robolectric.properties1
-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
-rw-r--r--mobile/android/android-components/components/feature/autofill/README.md19
-rw-r--r--mobile/android/android-components/components/feature/autofill/build.gradle60
-rw-r--r--mobile/android/android-components/components/feature/autofill/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/AndroidManifest.xml11
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AbstractAutofillService.kt83
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AutofillConfiguration.kt45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AutofillUseCases.kt83
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/Authenticator.kt61
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/BiometricAuthenticator.kt81
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/DeviceCredentialAuthenticator.kt51
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/facts/AutofillFacts.kt106
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/handler/FillRequestHandler.kt106
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/lock/AutofillLock.kt48
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/preference/AutofillPreference.kt59
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/DatasetBuilder.kt18
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/LoginDatasetBuilder.kt206
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/SearchDatasetBuilder.kt76
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/AuthFillResponseBuilder.kt125
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/FillResponseBuilder.kt18
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/LoginFillResponseBuilder.kt60
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructure.kt110
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructureBuilder.kt229
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/RawStructure.kt36
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ViewNodeNavigator.kt186
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillConfirmActivity.kt139
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillSearchActivity.kt144
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillUnlockActivity.kt125
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/search/LoginViewHolder.kt30
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/search/LoginsAdapter.kt47
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/verify/CredentialAccessVerifier.kt50
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_login.xml23
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_preference.xml11
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_search.xml24
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-am/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ar/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ast/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-azb/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-be/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-bg/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-br/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-bs/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ca/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-cak/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ceb/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ckb/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-co/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-cs/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-cy/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-da/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-de/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-dsb/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-el/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-en-rCA/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-en-rGB/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-eo/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rAR/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rCL/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rES/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rMX/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-es/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-et/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-eu/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-fa/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ff/strings.xml17
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-fi/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-fr/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-fur/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-fy-rNL/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-gd/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-gl/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-gn/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-hi-rIN/strings.xml38
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-hil/strings.xml17
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-hr/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-hsb/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-hu/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-hy-rAM/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ia/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-in/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-is/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-it/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-iw/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ja/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ka/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-kaa/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-kab/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-kk/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-kmr/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ko/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-lo/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-lt/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-mix/strings.xml21
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-my/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-nb-rNO/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ne-rNP/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-nl/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-nn-rNO/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-oc/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-or/strings.xml25
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-pa-rIN/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-pa-rPK/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-pl/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-pt-rBR/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-pt-rPT/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-rm/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ro/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ru/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-sat/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-sc/strings.xml37
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-si/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-sk/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-skr/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-sl/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-sq/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-sr/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-su/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-sv-rSE/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ta/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-te/strings.xml25
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-tg/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-th/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-tl/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-tok/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-tr/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-trs/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-tt/strings.xml37
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-tzm/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ug/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-uk/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-ur/strings.xml37
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-uz/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-vec/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-vi/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-yo/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-zh-rCN/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values-zh-rTW/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/main/res/values/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/AutofillUseCasesTest.kt192
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/handler/FillRequestHandlerTest.kt235
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/structure/ParsedStructureTest.kt52
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/DOMNavigator.kt133
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/MockStructure.kt32
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_expensify.xml46
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_facebook.xml14
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_facebook_lite.xml59
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_messenger_lite.xml29
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_twitter.xml51
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/browser_fenix_amazon.co.uk.xml68
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/browser_webview_gmail.xml15
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/autofill/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/README.md19
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/build.gradle59
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/AwesomeBarFeature.kt257
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/facts/AwesomeBarFacts.kt92
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProvider.kt133
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProvider.kt128
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProvider.kt191
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProvider.kt127
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProvider.kt158
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchActionProvider.kt59
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProvider.kt77
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProvider.kt285
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchTermSuggestionsProvider.kt136
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SessionAutocompleteProvider.kt55
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProvider.kt98
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-am/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-an/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ar/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ast/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-az/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-azb/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ban/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-be/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bg/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bn/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-br/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bs/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ca/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cak/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ceb/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ckb/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-co/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cs/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cy/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-da/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-de/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-dsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-el/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-en-rCA/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-en-rGB/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-eo/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rAR/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rCL/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rES/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rMX/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-et/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-eu/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fa/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ff/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fi/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fr/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fur/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fy-rNL/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ga-rIE/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gd/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gl/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gn/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gu-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hi-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hr/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hu/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hy-rAM/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ia/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-in/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-is/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-it/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-iw/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ja/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ka/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kaa/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kab/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kk/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kmr/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kn/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ko/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lij/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lo/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lt/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-mix/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ml/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-mr/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-my/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nb-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ne-rNP/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nl/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nn-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-oc/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-or/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pa-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pa-rPK/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pl/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pt-rBR/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pt-rPT/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-rm/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ro/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ru/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sat/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sc/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-si/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sk/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-skr/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sl/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sq/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sr/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-su/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sv-rSE/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ta/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-te/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tg/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-th/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tl/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tr/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-trs/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tt/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ug/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-uk/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ur/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-uz/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-vec/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-vi/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-yo/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-zh-rCN/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values-zh-rTW/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/main/res/values/strings.xml13
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/AwesomeBarFeatureTest.kt305
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/facts/AwesomeBarFactsTest.kt79
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProviderTest.kt335
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProviderTest.kt245
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProviderTest.kt378
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProviderTest.kt322
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProviderTest.kt396
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchActionProviderTest.kt46
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProviderTest.kt122
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProviderTest.kt667
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchTermSuggestionsProviderTest.kt334
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionAutocompleteProviderTest.kt82
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProviderTest.kt449
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/awesomebar/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/containers/README.md19
-rw-r--r--mobile/android/android-components/components/feature/containers/build.gradle80
-rw-r--r--mobile/android/android-components/components/feature/containers/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/containers/schemas/mozilla.components.feature.containers.db.ContainerDatabase/1.json52
-rw-r--r--mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt107
-rw-r--r--mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt92
-rw-r--r--mobile/android/android-components/components/feature/containers/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerMiddleware.kt106
-rw-r--r--mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerStorage.kt72
-rw-r--r--mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDao.kt36
-rw-r--r--mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDatabase.kt63
-rw-r--r--mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerEntity.kt49
-rw-r--r--mobile/android/android-components/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt115
-rw-r--r--mobile/android/android-components/components/feature/containers/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/containers/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/README.md92
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/build.gradle56
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt689
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuFeature.kt140
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuFragment.kt173
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuUseCases.kt85
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/DefaultSelectionActionDelegate.kt119
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ext/DefaultSelectionActionDelegate.kt32
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/facts/ContextMenuFacts.kt58
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_dialog.xml39
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_item.xml18
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_title.xml23
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-am/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-an/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ar/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ast/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-az/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-azb/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ban/strings.xml21
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-be/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bg/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bn/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-br/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bs/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ca/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cak/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ceb/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ckb/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-co/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cs/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cy/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-da/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-de/strings.xml62
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-dsb/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-el/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-en-rCA/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-en-rGB/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-eo/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rAR/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rCL/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rES/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rMX/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-et/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-eu/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fa/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ff/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fi/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fr/strings.xml59
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fur/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fy-rNL/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ga-rIE/strings.xml26
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gd/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gl/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gn/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gu-rIN/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hi-rIN/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hil/strings.xml9
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hr/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hsb/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hu/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hy-rAM/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ia/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-in/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-is/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-it/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-iw/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ja/strings.xml62
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ka/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kaa/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kab/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kk/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kmr/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kn/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ko/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lij/strings.xml35
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lo/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lt/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-mix/strings.xml17
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ml/strings.xml35
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-mr/strings.xml49
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-my/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nb-rNO/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ne-rNP/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nl/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nn-rNO/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-oc/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-or/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pa-rIN/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pa-rPK/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pl/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pt-rBR/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pt-rPT/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-rm/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ro/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ru/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sat/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sc/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-si/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sk/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-skr/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sl/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sq/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sr/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-su/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sv-rSE/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ta/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-te/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tg/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-th/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tl/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tr/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-trs/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tt/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tzm/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ug/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-uk/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ur/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-uz/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-vec/strings.xml49
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-vi/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-yo/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-zh-rCN/strings.xml62
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values-zh-rTW/strings.xml62
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values/colors.xml8
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/main/res/values/style.xml14
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt2063
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFeatureTest.kt403
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFragmentTest.kt347
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/DefaultSelectionActionDelegateTest.kt267
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/contextmenu/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/customtabs/README.md30
-rw-r--r--mobile/android/android-components/components/feature/customtabs/build.gradle64
-rw-r--r--mobile/android/android-components/components/feature/customtabs/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/AbstractCustomTabsService.kt135
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt280
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt74
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabWindowFeature.kt107
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsFacts.kt37
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeature.kt425
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserver.kt50
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeature.kt67
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidates.kt32
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsAction.kt40
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceState.kt68
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducer.kt28
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStore.kt15
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/verify/OriginVerifier.kt76
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-an/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ar/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-az/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ban/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-bn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ceb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ff/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ga-rIE/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-gu-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-hi-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-kn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-lij/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ml/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-mr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-my/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ne-rNP/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-or/strings.xml4
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ro/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ta/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-te/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-tl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-tok/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-tzm/strings.xml4
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-ur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-vec/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-yo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values/dimens.xml7
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/main/res/values/strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt130
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt416
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt175
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabWindowFeatureTest.kt164
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt1582
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt113
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt82
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidatesTest.kt77
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducerTest.kt174
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt85
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker3
-rw-r--r--mobile/android/android-components/components/feature/customtabs/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/downloads/README.md183
-rw-r--r--mobile/android/android-components/components/feature/downloads/build.gradle90
-rw-r--r--mobile/android/android-components/components/feature/downloads/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/1.json76
-rw-r--r--mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/2.json82
-rw-r--r--mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/3.json76
-rw-r--r--mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/4.json76
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt281
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt114
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/AndroidManifest.xml39
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt1120
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt93
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadMiddleware.kt186
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt366
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadStorage.kt98
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt499
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsUseCases.kt92
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragment.kt237
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadDao.kt41
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadEntity.kt86
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadsDatabase.kt91
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragment.kt74
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/Context.kt56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt52
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/facts/DownloadsFacts.kt43
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/AndroidDownloadManager.kt165
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/DownloadManager.kt48
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/FetchDownloadManager.kt137
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/provider/FileProvider.kt18
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeature.kt98
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeature.kt97
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/TemporaryDownloadFeature.kt156
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialog.kt136
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt219
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderApp.kt31
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt72
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download.xml15
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim0.xml15
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim1.xml19
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim2.xml19
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim3.xml19
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim4.xml19
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim5.xml19
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_complete.xml14
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_failed.xml8
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_ongoing_download.xml13
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_app_list_item.xml44
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml78
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloader_chooser_prompt.xml75
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloads_prompt.xml92
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-am/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-an/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ar/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ast/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-az/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-azb/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-be/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-bg/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-bn/strings.xml44
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-br/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-bs/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ca/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-cak/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ceb/strings.xml54
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ckb/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-co/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-cs/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-cy/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-da/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-de/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-dsb/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-el/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rCA/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rGB/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-eo/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rAR/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rCL/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rES/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rMX/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-es/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-et/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-eu/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-fa/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ff/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-fi/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-fr/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-fur/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-fy-rNL/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ga-rIE/strings.xml40
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-gd/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-gl/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-gn/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-gu-rIN/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-hi-rIN/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-hil/strings.xml18
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-hr/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-hsb/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-hu/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-hy-rAM/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ia/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-in/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-is/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-it/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-iw/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ja/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ka/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-kaa/strings.xml61
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-kab/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-kk/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-kmr/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-kn/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ko/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-lij/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-lo/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-lt/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-mix/strings.xml19
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ml/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-mr/strings.xml47
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-my/strings.xml49
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-nb-rNO/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ne-rNP/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-nl/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-nn-rNO/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-oc/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-or/strings.xml28
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rIN/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rPK/strings.xml59
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-pl/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rBR/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rPT/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-rm/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ro/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ru/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-sat/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-sc/strings.xml55
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-si/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-sk/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-skr/strings.xml60
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-sl/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-sq/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-sr/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-su/strings.xml59
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-sv-rSE/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ta/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-te/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-tg/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-th/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-tl/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-tr/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-trs/strings.xml59
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-tt/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-tzm/strings.xml31
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ug/strings.xml59
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-uk/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-ur/strings.xml49
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-uz/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-vec/strings.xml39
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-vi/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-yo/strings.xml56
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rCN/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rTW/strings.xml58
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values/colors.xml7
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/values/strings.xml59
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/main/res/xml/feature_downloads_file_paths.xml10
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt2155
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadCancelDialogFragmentTest.kt150
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadDialogFragmentTest.kt65
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt607
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt414
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadStorageTest.kt36
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadUseCasesTest.kt51
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt1300
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragmentTest.kt151
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/db/DownloadEntityTest.kt94
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragmentTest.kt67
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ext/DownloadStateKtTest.kt48
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/AndroidDownloadManagerTest.kt212
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/FetchDownloadManagerTest.kt322
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeatureTest.kt340
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeatureTest.kt301
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialogTest.kt130
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapterTest.kt48
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/downloads/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/findinpage/README.md84
-rw-r--r--mobile/android/android-components/components/feature/findinpage/build.gradle48
-rw-r--r--mobile/android/android-components/components/feature/findinpage/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/FindInPageFeature.kt75
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/facts/FindInPageFacts.kt46
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/internal/FindInPageInteractor.kt75
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/internal/FindInPagePresenter.kt57
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/view/FindInPageBar.kt260
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/view/FindInPageView.kt54
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/layout/mozac_feature_findinpage_view.xml83
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-am/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-an/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ar/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ast/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-az/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-azb/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-be/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-bg/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-bn/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-br/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-bs/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ca/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-cak/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ceb/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ckb/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-co/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-cs/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-cy/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-da/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-de/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-dsb/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-el/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-en-rCA/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-en-rGB/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-eo/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rAR/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rCL/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rES/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rMX/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-es/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-et/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-eu/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-fa/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ff/strings.xml21
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-fi/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-fr/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-fur/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-fy-rNL/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ga-rIE/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-gd/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-gl/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-gn/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-gu-rIN/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-hi-rIN/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-hr/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-hsb/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-hu/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-hy-rAM/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ia/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-in/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-is/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-it/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-iw/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ja/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ka/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-kaa/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-kab/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-kk/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-kmr/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-kn/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ko/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-lij/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-lo/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-lt/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-mix/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ml/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-mr/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-my/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-nb-rNO/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ne-rNP/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-nl/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-nn-rNO/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-oc/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-or/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-pa-rIN/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-pa-rPK/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-pl/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-pt-rBR/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-pt-rPT/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-rm/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ro/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ru/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-sat/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-sc/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-si/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-sk/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-skr/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-sl/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-sq/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-sr/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-su/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-sv-rSE/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ta/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-te/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-tg/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-th/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-tl/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-tr/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-trs/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-tt/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-tzm/strings.xml21
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ug/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-uk/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-ur/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-uz/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-vec/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-vi/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-yo/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-zh-rCN/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values-zh-rTW/strings.xml24
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values/attrs.xml15
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values/dimens.xml12
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values/strings.xml27
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/main/res/values/style.xml12
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/FindInPageFeatureTest.kt130
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPageInteractorTest.kt211
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPagePresenterTest.kt117
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/view/FindInPageBarTest.kt170
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/findinpage/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/README.md49
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/build.gradle87
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/fxsuggest.fml.yaml36
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionScheduler.kt68
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionWorker.kt40
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestStorage.kt119
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestSuggestionProvider.kt185
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/GlobalFxSuggestDependencyProvider.kt30
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/facts/FxSuggestFacts.kt89
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/facts/FxSuggestFactsMiddleware.kt84
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestFactsMiddlewareTest.kt914
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestFactsTest.kt191
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionSchedulerTest.kt80
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionWorkerTest.kt61
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestSuggestionProviderTest.kt394
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/fxsuggest/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/intent/README.md19
-rw-r--r--mobile/android/android-components/components/feature/intent/build.gradle46
-rw-r--r--mobile/android/android-components/components/feature/intent/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/intent/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/ext/IntentExtensions.kt63
-rw-r--r--mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/IntentProcessor.kt20
-rw-r--r--mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/TabIntentProcessor.kt121
-rw-r--r--mobile/android/android-components/components/feature/intent/src/test/java/mozilla/components/feature/intent/ext/IntentExtensionsTest.kt83
-rw-r--r--mobile/android/android-components/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt408
-rw-r--r--mobile/android/android-components/components/feature/intent/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/intent/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/logins/README.md19
-rw-r--r--mobile/android/android-components/components/feature/logins/build.gradle73
-rw-r--r--mobile/android/android-components/components/feature/logins/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json40
-rw-r--r--mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt139
-rw-r--r--mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt78
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt20
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt85
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt30
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt43
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt36
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt22
-rw-r--r--mobile/android/android-components/components/feature/logins/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/media/README.md67
-rw-r--r--mobile/android/android-components/components/feature/media/build.gradle62
-rw-r--r--mobile/android/android-components/components/feature/media/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/AndroidManifest.xml12
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/MediaSessionFeature.kt108
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/ext/MediaSessionState.kt47
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/ext/SessionState.kt49
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/facts/MediaFacts.kt50
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocus.kt116
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusController.kt13
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusControllerV21.kt28
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusControllerV26.kt39
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeature.kt99
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/middleware/LastMediaAccessMiddleware.kt38
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/middleware/RecordingDevicesMiddleware.kt253
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/notification/MediaNotification.kt155
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/notification/MediaNotificationChannel.kt45
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/AbstractMediaSessionService.kt103
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionDelegate.kt32
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionServiceDelegate.kt298
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/session/MediaSessionCallback.kt28
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_action_pause.xml13
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_action_play.xml13
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_paused.xml13
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_playing.xml13
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-am/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-an/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ar/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ast/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-az/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-azb/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-be/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-bg/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-bn/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-br/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-bs/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ca/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-cak/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ceb/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ckb/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-co/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-cs/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-cy/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-da/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-de/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-dsb/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-el/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-en-rCA/strings.xml40
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-en-rGB/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-eo/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-es-rAR/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-es-rCL/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-es-rES/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-es-rMX/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-es/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-et/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-eu/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-fa/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ff/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-fi/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-fr/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-fur/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-fy-rNL/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ga-rIE/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-gd/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-gl/strings.xml40
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-gn/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-gu-rIN/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-hi-rIN/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-hil/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-hr/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-hsb/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-hu/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-hy-rAM/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ia/strings.xml44
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-in/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-is/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-it/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-iw/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ja/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ka/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-kaa/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-kab/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-kk/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-kmr/strings.xml40
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-kn/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ko/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-lij/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-lo/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-lt/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-mix/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ml/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-mr/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-my/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-nb-rNO/strings.xml40
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ne-rNP/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-nl/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-nn-rNO/strings.xml38
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-oc/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-or/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-pa-rIN/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-pa-rPK/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-pl/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-pt-rBR/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-pt-rPT/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-rm/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ro/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ru/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-sat/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-sc/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-si/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-sk/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-skr/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-sl/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-sq/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-sr/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-su/strings.xml37
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-sv-rSE/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ta/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-te/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-tg/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-th/strings.xml40
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-tl/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-tr/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-trs/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-tt/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-tzm/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ug/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-uk/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-ur/strings.xml23
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-uz/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-vec/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-vi/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-yo/strings.xml22
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-zh-rCN/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values-zh-rTW/strings.xml41
-rw-r--r--mobile/android/android-components/components/feature/media/src/main/res/values/strings.xml43
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/MediaSessionFeatureTest.kt358
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/ext/SessionStateKtTest.kt188
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/focus/AudioFocusTest.kt363
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeatureTest.kt479
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/middleware/LastMediaAccessMiddlewareTest.kt287
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/middleware/RecordingDevicesMiddlewareTest.kt138
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/notification/MediaNotificationTest.kt208
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/AbstractMediaSessionServiceTest.kt100
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaServiceBinderTest.kt30
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaSessionServiceDelegateTest.kt581
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/media/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/privatemode/README.md19
-rw-r--r--mobile/android/android-components/components/feature/privatemode/build.gradle46
-rw-r--r--mobile/android/android-components/components/feature/privatemode/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/AndroidManifest.xml8
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/feature/SecureWindowFeature.kt58
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationService.kt261
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeature.kt49
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-an/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ar/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-az/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ban/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-bn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ceb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-gu-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-hi-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-kn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-mr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-my/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ne-rNP/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ro/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ta/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-te/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-tl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-ur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-yo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/feature/SecureWindowFeatureTest.kt90
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationServiceTest.kt195
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeatureTest.kt133
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/privatemode/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/prompts/README.md76
-rw-r--r--mobile/android/android-components/components/feature/prompts/build.gradle79
-rw-r--r--mobile/android/android-components/components/feature/prompts/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/androidTest/java/mozilla/components/feature/prompts/file/OnDeviceFilePickerTest.kt155
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/AndroidManifest.xml25
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptContainer.kt63
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt1220
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptMiddleware.kt59
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt68
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt33
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt89
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt151
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/concept/PasswordPromptView.kt42
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/concept/SelectablePromptView.kt49
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardDelegate.kt34
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt48
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt119
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragment.kt210
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBar.kt152
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapter.kt39
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AbstractPromptTextDialogFragment.kt72
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AlertDialogFragment.kt80
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragment.kt189
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AutofillEditText.kt31
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/BasicColorAdapter.kt131
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ChoiceAdapter.kt217
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragment.kt145
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragment.kt164
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragment.kt84
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/FullScreenNotificationDialog.kt70
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/LoginDialogFacts.kt45
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragment.kt99
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptAbuserDetector.kt64
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt98
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragment.kt428
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragment.kt130
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragment.kt342
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/Calendar.kt65
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/EditText.kt25
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/PromptRequest.kt36
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/AddressAutofillDialogFacts.kt76
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFacts.kt100
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/LoginAutofillDialogFacts.kt60
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/PromptFacts.kt61
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FilePicker.kt247
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FileUploadsDirCleaner.kt86
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerMiddleware.kt40
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/MimeType.kt187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/DialogColors.kt57
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/IdentityCredentialItem.kt106
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/PrivacyPolicyDialogFragment.kt130
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectAccountDialog.kt172
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectAccountDialogFragment.kt118
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectProviderDialog.kt144
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectProviderDialogFragment.kt103
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/previews/DialogPreviewMaterialTheme.kt24
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/previews/LightDarkPreview.kt16
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/BasicLoginAdapter.kt67
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginDelegate.kt27
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginExceptions.kt22
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginPicker.kt80
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginSelectBar.kt140
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/StrongPasswordPromptViewListener.kt92
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordBar.kt110
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordDelegate.kt19
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/provider/FileProvider.kt18
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/share/ShareDelegate.kt51
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/LoginPanelTextInputLayout.kt55
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/MonthAndYearPicker.kt178
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/TimePrecisionPicker.kt231
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/color/button_state_list.xml8
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/drawable-hdpi/color_picker_row_bg.9.pngbin0 -> 151 bytes
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/drawable-mdpi/color_picker_row_bg.9.pngbin0 -> 135 bytes
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/drawable-xhdpi/color_picker_row_bg.9.pngbin0 -> 169 bytes
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/drawable-xxhdpi/color_picker_row_bg.9.pngbin0 -> 199 bytes
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/drawable/color_picker_checkmark.xml11
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/drawable/mozac_ic_password_reveal_two_state.xml8
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/login_selection_list_item.xml50
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_choice_dialogs.xml10
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_choice_group_item.xml7
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_login_multiselect_view.xml86
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_menu_choice_item.xml15
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_menu_separator_choice_item.xml8
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_multiple_choice_item.xml23
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_promps_widget_month_picker.xml40
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_auth_prompt.xml54
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_save_credit_card_prompt.xml140
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_save_login_prompt.xml144
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_simple_text.xml14
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_with_check_box.xml47
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_list_item.xml31
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_select_prompt.xml84
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_color_item.xml13
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_color_picker_dialogs.xml12
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_credit_card_list_item.xml59
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_credit_card_select_prompt.xml84
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_date_time_picker.xml26
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_time_picker.xml66
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_single_choice_item.xml22
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_suggest_strong_password_view.xml52
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_text_prompt.xml45
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-am/strings.xml192
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-an/strings.xml91
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ar/strings.xml136
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ast/strings.xml128
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-az/strings.xml99
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-azb/strings.xml192
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ban/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-be/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-bg/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-bn/strings.xml96
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-br/strings.xml176
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-bs/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ca/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-cak/strings.xml189
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ceb/strings.xml110
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ckb/strings.xml93
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-co/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-cs/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-cy/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-da/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-de/strings.xml199
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-dsb/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-el/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-en-rCA/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-en-rGB/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-eo/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rAR/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rCL/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rES/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rMX/strings.xml144
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-es/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-et/strings.xml180
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-eu/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-fa/strings.xml138
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ff/strings.xml79
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-fi/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-fr/strings.xml196
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-fur/strings.xml190
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-fy-rNL/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ga-rIE/strings.xml65
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-gd/strings.xml129
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-gl/strings.xml189
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-gn/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-gu-rIN/strings.xml81
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-hi-rIN/strings.xml108
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-hil/strings.xml39
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-hr/strings.xml144
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-hsb/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-hu/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-hy-rAM/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ia/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-in/strings.xml138
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-is/strings.xml189
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-it/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-iw/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ja/strings.xml199
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ka/strings.xml144
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-kaa/strings.xml134
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-kab/strings.xml175
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-kk/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-kmr/strings.xml170
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-kn/strings.xml83
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ko/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-lij/strings.xml81
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-lo/strings.xml146
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-lt/strings.xml130
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-mix/strings.xml18
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ml/strings.xml81
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-mr/strings.xml98
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-my/strings.xml110
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-nb-rNO/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ne-rNP/strings.xml111
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-nl/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-nn-rNO/strings.xml174
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-oc/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-or/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-pa-rIN/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-pa-rPK/strings.xml156
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-pl/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-pt-rBR/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-pt-rPT/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-rm/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ro/strings.xml95
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ru/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-sat/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-sc/strings.xml174
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-si/strings.xml189
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-sk/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-skr/strings.xml190
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-sl/strings.xml183
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-sq/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-sr/strings.xml144
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-su/strings.xml151
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-sv-rSE/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ta/strings.xml99
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-te/strings.xml100
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-tg/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-th/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-tl/strings.xml123
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-tok/strings.xml78
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-tr/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-trs/strings.xml142
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-tt/strings.xml128
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-tzm/strings.xml53
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ug/strings.xml190
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-uk/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-ur/strings.xml110
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-uz/strings.xml129
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-vec/strings.xml65
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-vi/strings.xml187
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-yo/strings.xml129
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-zam/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-zh-rCN/strings.xml199
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values-zh-rTW/strings.xml199
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values/attrs.xml23
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values/colors.xml20
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values/ids.xml7
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values/quarantined_strings.xml9
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values/strings-no-translatable.xml25
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values/strings.xml188
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/values/styles.xml18
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/main/res/xml/feature_prompts_file_paths.xml7
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptContainerTest.kt76
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt2850
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptMiddlewareTest.kt111
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt82
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt145
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt141
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt71
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt190
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragmentTest.kt315
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt146
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt49
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AlertDialogFragmentTest.kt147
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragmentTest.kt196
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt692
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragmentTest.kt159
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragmentTest.kt126
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragmentTest.kt252
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragmentTest.kt132
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragmentTest.kt153
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt284
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/ext/EditTextTest.kt44
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/ext/PromptRequestTest.kt55
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/AddressAutofillDialogFactsTest.kt94
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFactsTest.kt147
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FilePickerTest.kt405
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerMiddlewareTest.kt59
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerTest.kt87
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/MimeTypeTest.kt392
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/BasicLoginAdapterTest.kt64
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginPickerTest.kt142
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginSelectBarTest.kt104
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/StrongPasswordPromptViewListenerTest.kt143
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordBarTest.kt61
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/share/DefaultShareDelegateTest.kt70
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/MonthAndYearPickerTest.kt196
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/TimePrecisionPickerTest.kt106
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/prompts/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/push/README.md154
-rw-r--r--mobile/android/android-components/components/feature/push/assets/autopush-sequence-diagram.pngbin0 -> 327615 bytes
-rw-r--r--mobile/android/android-components/components/feature/push/build.gradle48
-rw-r--r--mobile/android/android-components/components/feature/push/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/push/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/push/src/main/java/mozilla/components/feature/push/AutoPushFeature.kt412
-rw-r--r--mobile/android/android-components/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureTest.kt489
-rw-r--r--mobile/android/android-components/components/feature/push/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/push/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/pwa/README.md85
-rw-r--r--mobile/android/android-components/components/feature/pwa/build.gradle89
-rw-r--r--mobile/android/android-components/components/feature/pwa/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/0.json51
-rw-r--r--mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/1.json73
-rw-r--r--mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/2.json73
-rw-r--r--mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/3.json87
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/androidTest/java/mozilla/components/feature/pwa/db/ManifestDatabaseMigrationTest.kt113
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/AndroidManifest.xml22
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ManifestStorage.kt159
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ProgressiveWebAppFacts.kt50
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppInterceptor.kt62
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppLauncherActivity.kt105
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppShortcutManager.kt287
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppUseCases.kt88
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestConverter.kt26
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDao.kt61
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDatabase.kt70
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestEntity.kt50
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Activity.kt26
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Bundle.kt31
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/CustomTabState.kt22
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Intent.kt48
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/SessionState.kt28
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Uri.kt25
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/WebAppManifest.kt98
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeature.kt94
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/SiteControlsBuilder.kt130
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppActivityFeature.kt54
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppContentFeature.kt30
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeature.kt117
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeature.kt198
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt96
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt95
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_pwa.xml13
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_refresh.xml13
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-am/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-an/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ar/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ast/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-az/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-azb/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ban/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-be/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-bg/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-bn/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-br/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-bs/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ca/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-cak/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ceb/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ckb/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-co/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-cs/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-cy/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-da/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-de/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-dsb/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-el/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rCA/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rGB/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-eo/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rAR/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rCL/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rES/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rMX/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-et/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-eu/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fa/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fi/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fur/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fy-rNL/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-gd/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-gl/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-gn/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-gu-rIN/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hi-rIN/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hil/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hsb/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hu/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hy-rAM/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ia/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-in/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-is/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-it/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-iw/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ja/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ka/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kaa/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kab/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kk/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kmr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kn/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ko/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-lo/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-lt/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-mix/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-mr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-my/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-nb-rNO/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ne-rNP/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-nl/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-nn-rNO/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-oc/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-or/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rIN/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rPK/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pl/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rBR/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rPT/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-rm/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ro/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ru/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sat/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sc/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-si/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sk/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-skr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sl/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sq/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-su/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sv-rSE/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ta/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-te/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tg/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-th/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tl/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-trs/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tt/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tzm/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ug/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-uk/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ur/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-uz/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-vi/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-yo/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rCN/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rTW/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values/strings.xml17
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values/styles.xml16
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt305
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppInterceptorTest.kt102
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppLauncherActivityTest.kt113
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt355
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt149
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/ActivityKtTest.kt89
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/CustomTabStateKtTest.kt59
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/SessionStateKtTest.kt169
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/UriKtTest.kt56
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/WebAppManifestKtTest.kt172
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt255
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppActivityFeatureTest.kt95
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppContentFeatureTest.kt53
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeatureTest.kt343
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeatureTest.kt147
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt97
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt154
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/qr/README.md42
-rw-r--r--mobile/android/android-components/components/feature/qr/build.gradle46
-rw-r--r--mobile/android/android-components/components/feature/qr/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFeature.kt145
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFragment.kt786
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/AutoFitTextureView.kt71
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/CustomViewFinder.kt298
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-hdpi/qr_cam_focus.webpbin0 -> 148 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-ldpi/qr_cam_focus.webpbin0 -> 100 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-mdpi/qr_cam_focus.webpbin0 -> 104 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-xhdpi/qr_cam_focus.webpbin0 -> 140 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxhdpi/qr_cam_focus.webpbin0 -> 202 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxxhdpi/qr_cam_focus.webpbin0 -> 140 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/layout/fragment_layout.xml35
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-am/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-an/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ar/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ast/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-az/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-azb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ban/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-be/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-bg/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-bn/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-br/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-bs/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ca/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-cak/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ceb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ckb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-co/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-cs/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-cy/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-da/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-de/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-dsb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-el/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-en-rCA/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-en-rGB/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-eo/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es-rAR/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es-rCL/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es-rES/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es-rMX/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-et/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-eu/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fa/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fi/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fur/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fy-rNL/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-gd/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-gl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-gn/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-gu-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hi-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hil/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hsb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hu/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hy-rAM/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ia/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-in/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-is/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-it/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-iw/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ja/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ka/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kaa/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kab/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kk/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kmr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kn/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ko/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-lo/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-lt/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-mix/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-mr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-my/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-nb-rNO/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ne-rNP/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-nl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-nn-rNO/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-oc/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rPK/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rBR/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rPT/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-rm/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ro/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ru/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sat/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sc/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-si/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sk/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-skr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sq/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-su/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sv-rSE/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ta/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-te/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tg/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-th/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tok/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-trs/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tt/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ug/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-uk/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ur/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-uz/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-vi/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-yo/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rCN/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rTW/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values/colors.xml8
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values/strings.xml13
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFeatureTest.kt269
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFragmentTest.kt786
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/AutoFitTextureViewTest.kt82
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/CustomViewFinderTest.kt102
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/readerview/.gitignore1
-rw-r--r--mobile/android/android-components/components/feature/readerview/README.md49
-rw-r--r--mobile/android/android-components/components/feature/readerview/build.gradle61
-rw-r--r--mobile/android/android-components/components/feature/readerview/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/manifest.template.json30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/JSDOMParser-0.4.2.js1196
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/readability-0.4.2.js2283
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/readability-readerable-0.4.2.js108
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview-background.js22
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview-content.js75
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.css319
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.html17
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.js366
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewFeature.kt382
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewMiddleware.kt134
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewConfig.kt108
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewControlsInteractor.kt51
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewControlsPresenter.kt43
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/view/ReaderViewControlsBar.kt174
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/view/ReaderViewControlsView.kt63
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/drawable/fontsize_controls_text_selector.xml11
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_dark_selector.xml24
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_light_selector.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_sepia_selector.xml24
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_selected_text_selector.xml11
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/layout/mozac_feature_readerview_view.xml155
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-am/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-an/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ar/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ast/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-az/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-azb/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-be/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-bg/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-bn/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-br/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-bs/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ca/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-cak/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ceb/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ckb/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-co/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-cs/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-cy/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-da/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-de/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-dsb/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-el/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-en-rCA/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-en-rGB/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-eo/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rAR/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rCL/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rES/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rMX/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-es/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-et/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-eu/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-fa/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ff/strings.xml18
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-fi/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-fr/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-fur/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-fy-rNL/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ga-rIE/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-gd/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-gl/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-gn/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-gu-rIN/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-hi-rIN/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-hil/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-hr/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-hsb/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-hu/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-hy-rAM/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ia/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-in/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-is/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-it/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-iw/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ja/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ka/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-kaa/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-kab/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-kk/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-kmr/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-kn/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ko/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-lij/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-lo/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-lt/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ml/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-mr/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-my/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-nb-rNO/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ne-rNP/strings.xml31
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-nl/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-nn-rNO/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-oc/strings.xml31
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-or/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-pa-rIN/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-pa-rPK/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-pl/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-pt-rBR/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-pt-rPT/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-rm/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ro/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ru/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-sat/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-sc/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-si/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-sk/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-skr/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-sl/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-sq/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-sr/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-su/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-sv-rSE/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ta/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-te/strings.xml31
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-tg/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-th/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-tl/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-tok/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-tr/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-trs/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-tt/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-tzm/strings.xml13
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ug/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-uk/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-ur/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-uz/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-vec/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-vi/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-yo/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-zh-rCN/strings.xml29
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values-zh-rTW/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values/attrs.xml26
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values/colors.xml15
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values/mozac_feature_readerview_strings.xml11
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/main/res/values/strings.xml35
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewFeatureTest.kt608
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewMiddlewareTest.kt187
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewConfigTest.kt108
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewControlsInteractorTest.kt90
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewControlsPresenterTest.kt61
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/view/ReaderViewControlsBarTest.kt204
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/ext/context.kt18
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker3
-rw-r--r--mobile/android/android-components/components/feature/readerview/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/README.md19
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/build.gradle81
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/schemas/mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase/1.json52
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/androidTest/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageOnDeviceTest.kt103
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddleware.kt144
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorage.kt136
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabDao.kt43
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabEntity.kt52
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabsDatabase.kt36
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddlewareTest.kt383
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabDaoTest.kt136
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageTest.kt355
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/recentlyclosed/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/search/.gitignore1
-rw-r--r--mobile/android/android-components/components/feature/search/README.md19
-rw-r--r--mobile/android/android-components/components/feature/search/build.gradle80
-rw-r--r--mobile/android/android-components/components/feature/search/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/adsTelemetry.js82
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/manifest.template.json219
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/manifest.template.json220
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/searchTelemetry.js61
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/search/list.json923
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/search/search_telemetry_v2.json657
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-au.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-ca.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-co-uk.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-de.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-es.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-fr.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-in.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-it.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-jp.xml31
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-nl.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-se.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazondotcom.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/azerdict.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/baidu.xml25
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/bing.xml24
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ceneje.xml15
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/coccoc.xml21
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/daum-kr.xml21
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ddg.xml23
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-at.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-au.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-befr.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ca.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ch.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-co-uk.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-de.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-es.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-fr.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ie.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-it.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-nl.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-pl.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ecosia.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/faclair-beag.xml13
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-b-1-m.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-b-m.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-com-nocodes.xml14
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/gulesider-mobile-NO.xml15
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/leo_ende_de.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mapy-cz.xml14
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-ar.xml14
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-cl.xml14
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-mx.xml14
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/odpiralni.xml15
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/pazaruvaj.xml14
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/prisjakt-sv-SE.xml14
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/qwant.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/rakuten.xml16
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/reddit.xml11
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/salidzinilv.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/seznam-cz.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/vatera.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-NN.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-NO.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-an.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ar.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-as.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ast.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-az.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-be.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bg.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bn.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-br.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bs.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ca.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-cy.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-cz.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-da.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-de.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-dsb.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-el.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-eo.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-es.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-et.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-eu.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fa.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fi.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fr.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fy-NL.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ga-IE.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gd.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gl.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gn.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gu.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-he.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hi.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hr.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hsb.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hu.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hy-AM.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ia.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-id.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-is.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-it.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ja.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ka.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kab.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kk.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-km.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kn.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lij.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lo.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lt.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ltg.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lv.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ml.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-mr.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ms.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-my.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ne.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-nl.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-oc.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-or.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pa.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pl.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pt.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-rm.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ro.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ru.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sk.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sl.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sq.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sr.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sv-SE.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ta.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-te.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-th.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-tr.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-uk.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ur.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-uz.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-vi.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-wo.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-zh-CN.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-zh-TW.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-kn.xml18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-oc.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-or.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-ta.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-te.xml17
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yahoo-jp-auctions.xml16
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yahoo-jp.xml16
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-en.xml22
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-ru.xml21
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-tr.xml22
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex.by.xml22
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex.xml21
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/youtube.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/BrowserStoreSearchAdapter.kt41
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchAdapter.kt21
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchFeature.kt52
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt343
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/BrowserStore.kt36
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt148
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/internal/SearchUrlBuilder.kt93
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddleware.kt77
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/SearchMiddleware.kt397
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/region/RegionManager.kt126
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/region/RegionMiddleware.kt72
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorage.kt278
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/CustomSearchEnginesStorage.kt66
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchEngineReader.kt243
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchEngineWriter.kt115
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchMetadataStorage.kt103
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/suggestions/Parser.kt79
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/suggestions/SearchSuggestionClient.kt101
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetry.kt132
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ExtensionInfo.kt18
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderCookie.kt24
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderModel.kt80
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SerpTelemetryRepository.kt170
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/TrackKeyInfo.kt38
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/Utils.kt116
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetry.kt124
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetry.kt86
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/widget/AppSearchWidgetProvider.kt312
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivity.kt134
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/drawable/mozac_rounded_search_widget_background.xml9
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_extra_small_v1.xml20
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_extra_small_v2.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_large.xml45
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_medium.xml45
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_small.xml27
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_small_no_mic.xml19
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-am/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ar/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ast/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-azb/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-be/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-bg/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-br/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-bs/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ca/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-cak/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ckb/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-co/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-cs/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-cy/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-da/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-de/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-dsb/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-el/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-en-rCA/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-en-rGB/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-eo/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-es-rAR/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-es-rCL/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-es-rES/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-es-rMX/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-es/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-et/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-eu/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-fa/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ff/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-fi/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-fr/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-fur/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-fy-rNL/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-gd/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-gl/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-gn/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-hr/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-hsb/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-hu/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-hy-rAM/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ia/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-in/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-is/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-it/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-iw/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ja/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ka/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-kaa/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-kab/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-kk/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-kmr/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ko/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-lo/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-lt/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-my/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-nb-rNO/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-night/colors.xml10
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-nl/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-nn-rNO/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-oc/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-pa-rIN/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-pa-rPK/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-pl/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-pt-rBR/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-pt-rPT/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-rm/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ru/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-sat/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-sc/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-si/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-sk/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-skr/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-sl/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-sq/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-sr/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-su/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-sv-rSE/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ta/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-te/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-tg/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-th/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-tl/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-tr/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-trs/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-tt/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-ug/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-uk/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-uz/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-vi/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-yo/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-zh-rCN/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values-zh-rTW/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values/colors.xml9
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values/dimens.xml7
-rw-r--r--mobile/android/android-components/components/feature/search/src/main/res/values/strings.xml16
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/BrowserStoreSeachAdapterTest.kt97
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/SearchFeatureTest.kt134
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt663
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/ext/BrowserStoreKtTest.kt117
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/ext/SearchEngineKtTest.kt196
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddlewareTest.kt126
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/SearchMiddlewareTest.kt1921
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionManagerTest.kt146
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionMiddlewareTest.kt153
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorageTest.kt196
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/CustomSearchEngineStorageTest.kt105
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/ParseSearchPluginsTest.kt106
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/SearchEngineReaderTest.kt84
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/SearchEngineWriterTest.kt212
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/ParserTest.kt149
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/SearchSuggestionClientTest.kt110
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetryTest.kt187
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SearchProviderModelTest.kt40
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SerpTelemetryRepositoryTest.kt92
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetryTest.kt271
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetryTest.kt467
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/AppSearchWidgetProviderTest.kt159
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivityExtendedForTests.kt24
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivityTest.kt134
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/search/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/serviceworker/README.md30
-rw-r--r--mobile/android/android-components/components/feature/serviceworker/build.gradle42
-rw-r--r--mobile/android/android-components/components/feature/serviceworker/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/serviceworker/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/serviceworker/src/main/java/mozilla/components/feature/serviceworker/ServiceWorkerSupport.kt52
-rw-r--r--mobile/android/android-components/components/feature/serviceworker/src/test/java/mozilla/components/feature/serviceworker/ServiceWorkerSupportTest.kt54
-rw-r--r--mobile/android/android-components/components/feature/serviceworker/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/serviceworker/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/session/README.md67
-rw-r--r--mobile/android/android-components/components/feature/session/build.gradle56
-rw-r--r--mobile/android/android-components/components/feature/session/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/CoordinateScrollingFeature.kt72
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/FullScreenFeature.kt97
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/HistoryDelegate.kt40
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/PictureInPictureFeature.kt89
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/ScreenOrientationFeature.kt37
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SessionFeature.kt67
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SessionUseCases.kt544
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SettingsUseCases.kt56
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SwipeRefreshFeature.kt92
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/TrackingProtectionUseCases.kt194
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/engine/EngineViewPresenter.kt92
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/LastAccessMiddleware.kt78
-rw-r--r--mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/undo/UndoMiddleware.kt153
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/CoordinateScrollingFeatureTest.kt89
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/FullScreenFeatureTest.kt465
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/HistoryDelegateTest.kt191
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/PictureInPictureFeatureTest.kt317
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/ScreenOrientationFeatureTest.kt56
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt401
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt519
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SettingsUseCasesTest.kt73
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SwipeRefreshFeatureTest.kt130
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/TrackingProtectionUseCasesTest.kt266
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/LastAccessMiddlewareTest.kt319
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/undo/UndoMiddlewareTest.kt294
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/session/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/share/README.md19
-rw-r--r--mobile/android/android-components/components/feature/share/build.gradle73
-rw-r--r--mobile/android/android-components/components/feature/share/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/1.json40
-rw-r--r--mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/2.json40
-rw-r--r--mobile/android/android-components/components/feature/share/src/androidTest/java/mozilla/components/feature/share/RecentAppsDaoTest.kt105
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentApp.kt24
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentAppsStorage.kt60
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/adapter/RecentAppAdapter.kt30
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppEntity.kt22
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDao.kt73
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDatabase.kt60
-rw-r--r--mobile/android/android-components/components/feature/share/src/test/java/mozilla/components/feature/share/RecentAppStorageTest.kt76
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/README.md61
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/build.gradle88
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/1.json81
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/2.json75
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/3.json88
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/4.json94
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/5.json94
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/6.json94
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/7.json94
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/8.json100
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/OnDeviceSitePermissionsStorageTest.kt230
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDaoTest.kt112
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorage.kt194
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt258
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsFacts.kt65
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsFeature.kt1057
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsRules.kt126
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDao.kt40
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDatabase.kt189
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsEntity.kt89
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/layout/mozac_site_permissions_prompt.xml127
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-am/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-an/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ar/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ast/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-az/strings.xml36
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-azb/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-be/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bg/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bn/strings.xml38
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-br/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bs/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ca/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cak/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ceb/strings.xml38
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ckb/strings.xml38
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-co/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cs/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cy/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-da/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-de/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-dsb/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-el/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-en-rCA/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-en-rGB/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-eo/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rAR/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rCL/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rES/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rMX/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-et/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-eu/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fa/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ff/strings.xml36
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fi/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fr/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fur/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fy-rNL/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ga-rIE/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gd/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gl/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gn/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gu-rIN/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hi-rIN/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hil/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hr/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hsb/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hu/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hy-rAM/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ia/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-in/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-is/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-it/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-iw/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ja/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ka/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kaa/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kab/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kk/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kmr/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kn/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ko/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lij/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lo/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lt/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ml/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-mr/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-my/strings.xml38
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nb-rNO/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ne-rNP/strings.xml46
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nl/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nn-rNO/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-oc/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-or/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pa-rIN/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pa-rPK/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pl/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pt-rBR/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pt-rPT/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-rm/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ro/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ru/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sat/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sc/strings.xml45
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-si/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sk/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-skr/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sl/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sq/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sr/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-su/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sv-rSE/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ta/strings.xml36
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-te/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tg/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-th/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tl/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tr/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-trs/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tt/strings.xml42
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tzm/strings.xml30
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ug/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-uk/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ur/strings.xml38
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-uz/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-vec/strings.xml34
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-vi/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-yo/strings.xml48
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-zh-rCN/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-zh-rTW/strings.xml57
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/main/res/values/strings.xml51
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorageTest.kt251
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragmentTest.kt481
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFactsTest.kt116
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFeatureTest.kt1450
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsRulesTest.kt199
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsTest.kt88
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/db/SitePermissionEntityTest.kt86
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/db/StatusConverterTest.kt72
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/sitepermissions/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/README.md36
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/build.gradle64
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/ClientTabPair.kt17
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsAutocompleteProvider.kt51
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsFeature.kt74
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProvider.kt92
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/DefaultController.kt68
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/SyncedTabsController.kt30
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/ext/SyncedTabsStorage.kt43
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/facts/SyncedTabsFacts.kt44
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractor.kt32
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/interactor/SyncedTabsInteractor.kt19
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenter.kt142
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/SyncedTabsPresenter.kt20
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsProvider.kt18
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorage.kt119
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/view/SyncedTabsView.kt89
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsAutocompleteProviderKtTest.kt68
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsFeatureTest.kt55
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProviderTest.kt88
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/controller/DefaultControllerTest.kt149
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/ext/SyncedTabsStorageKtTest.kt65
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/facts/SyncedTabsFactsTest.kt29
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/helper/SyncedTabsProvider.kt83
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractorTest.kt107
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenterTest.kt256
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorageTest.kt392
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/syncedtabs/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/README.md19
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/build.gradle87
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/schemas/mozilla.components.feature.tab.collections.db.TabCollectionDatabase/1.json122
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/TabCollectionStorageTest.kt493
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/db/TabCollectionDaoTest.kt158
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/db/TabDaoTest.kt115
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/Tab.kt42
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/TabCollection.kt57
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/TabCollectionStorage.kt170
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/adapter/TabAdapter.kt47
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/adapter/TabCollectionAdapter.kt77
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionDao.kt63
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionDatabase.kt36
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionEntity.kt28
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionWithTabs.kt19
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabDao.kt21
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabEntity.kt63
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/ext/TabsUseCases.kt86
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/test/java/mozilla/components/feature/tab/collections/ext/TabsUseCasesKtTest.kt94
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/tab-collections/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/tabs/README.md21
-rw-r--r--mobile/android/android-components/components/feature/tabs/build.gradle67
-rw-r--r--mobile/android/android-components/components/feature/tabs/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/CustomTabsUseCases.kt135
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt547
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/WindowFeature.kt77
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/BrowserState.kt38
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/TabSessionState.kt29
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/Tab.kt38
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabList.kt12
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/Tabs.kt19
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt61
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt56
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/toolbar/TabCounterToolbarButton.kt115
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/toolbar/TabsToolbarFeature.kt45
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-an/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ar/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-az/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ban/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-bn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ceb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ff/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ga-rIE/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-gu-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-hi-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-hil/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-kn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-lij/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ml/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-mr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-my/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ne-rNP/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ro/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ta/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-te/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-tl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-tzm/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-ur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-vec/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-yo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/CustomTabsUseCasesTest.kt76
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt632
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/WindowFeatureTest.kt119
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/ext/TabSessionStateTest.kt56
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt114
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt359
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/toolbar/TabCounterToolbarButtonTest.kt268
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/toolbar/TabsToolbarFeatureTest.kt113
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/tabs/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/toolbar/README.md19
-rw-r--r--mobile/android/android-components/components/feature/toolbar/build.gradle57
-rw-r--r--mobile/android/android-components/components/feature/toolbar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ContainerToolbarAction.kt89
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ContainerToolbarFeature.kt77
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeature.kt109
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarBehaviorController.kt79
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarFeature.kt102
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarInteractor.kt36
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarPresenter.kt114
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/WebExtensionToolbarAction.kt98
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeature.kt170
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/internal/URLRenderer.kt126
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/res/layout/mozac_feature_toolbar_container_action_layout.xml20
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/res/layout/mozac_feature_toolbar_web_extension_action_layout.xml29
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/main/res/values/colors.xml15
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarActionTest.kt67
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarFeatureTest.kt97
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt596
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarBehaviorControllerTest.kt200
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarFeatureTest.kt100
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt164
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt548
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeatureTest.kt432
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarTest.kt179
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt106
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/toolbar/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/top-sites/README.md19
-rw-r--r--mobile/android/android-components/components/feature/top-sites/build.gradle82
-rw-r--r--mobile/android/android-components/components/feature/top-sites/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/1.json52
-rw-r--r--mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/2.json58
-rw-r--r--mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/3.json58
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/OnDevicePinnedSitesStorageTest.kt313
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/db/PinnedSiteDaoTest.kt118
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt140
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/PinnedSiteStorage.kt104
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSite.kt90
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesConfig.kt50
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesFeature.kt38
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesProvider.kt20
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesStorage.kt65
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesUseCases.kt67
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteDao.kt48
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteEntity.kt59
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/TopSiteDatabase.kt107
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopFrecentSiteInfo.kt21
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopSite.kt34
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/facts/TopSitesFacts.kt31
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/presenter/DefaultTopSitesPresenter.kt58
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/presenter/TopSitesPresenter.kt17
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/view/TopSitesView.kt17
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt1169
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/PinnedSitesStorageTest.kt135
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesFeatureTest.kt34
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesUseCasesTest.kt65
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/ext/TopSiteTest.kt108
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/fact/TopSitesFactsTest.kt44
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/presenter/DefaultTopSitesPresenterTest.kt37
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/top-sites/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/webauthn/README.md59
-rw-r--r--mobile/android/android-components/components/feature/webauthn/build.gradle36
-rw-r--r--mobile/android/android-components/components/feature/webauthn/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/webauthn/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/webauthn/src/main/java/mozilla/components/feature/webauthn/WebAuthnFeature.kt66
-rw-r--r--mobile/android/android-components/components/feature/webauthn/src/test/java/mozilla/components/feature/webauthn/WebAuthnFeatureTest.kt92
-rw-r--r--mobile/android/android-components/components/feature/webauthn/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/webauthn/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/README.md39
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/build.gradle42
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/main/AndroidManifest.xml6
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/.eslintrc.js76
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/background.js93
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/experimentalAPIs/tabExtras.js53
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/experimentalAPIs/tabExtras.json37
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/icons/lightbulb.svg6
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/manifest.json50
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/main/java/mozilla/components/feature/webcompat/reporter/WebCompatReporterFeature.kt65
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/test/java/mozilla/components/feature/webcompat/reporter/WebCompatReporterFeatureTest.kt115
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/webcompat-reporter/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/webcompat/README.md32
-rw-r--r--mobile/android/android-components/components/feature/webcompat/build.gradle42
-rw-r--r--mobile/android/android-components/components/feature/webcompat/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/AboutCompat.sys.mjs35
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.css187
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.html51
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.js285
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPage.js42
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPage.json6
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPageProcessScript.js34
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/injections.js1061
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/shims.js889
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/ua_overrides.js1298
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefs.js53
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefs.json94
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefsChild.js29
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/appConstants.js28
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/appConstants.json15
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/matchPatterns.js30
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/matchPatterns.json29
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/systemManufacturer.js23
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/systemManufacturer.json20
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.js216
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.json102
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug0000000-testbed-css-injection.css7
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css15
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1610344-directv.com.co-hide-unsupported-message.css17
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css17
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1651917-teletrader.com.body-transform-origin.css18
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1653075-livescience.com-scrollbar-width.css17
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1654907-reactine.ca-hide-unsupported.css16
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1694470-myvidster.com-content-not-shown.css15
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1707795-office365-sheets-overscroll-disable.css12
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1712833-buskocchi.desuca.co.jp-fix-map-height.css13
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1741234-patient.alphalabs.ca-height-fix.css13
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1765947-veniceincoming.com-left-fix.css13
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1770962-coldwellbankerhomes.com-image-height.css18
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1774490-rainews.it-gallery-fix.css13
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1784141-aveeno.com-acuvue.com-unsupported.css17
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1784199-entrata-platform-unsupported.css18
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1819678-nppes.cms.hhs.gov-unsupported-banner.css15
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1829949-tomshardware.com-scrollbar-width.css18
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830747-babbel.com-page-height.css17
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830752-afisha.ru-slider-pointer-events.css27
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830761-91mobiles.com-content-height.css18
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830796-copyleaks.com-hide-unsupported.css13
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830810-interceramic.com-hide-unsupported.css13
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830813-page.onstove.com-hide-unsupported.css18
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1836103-autostar-novoross.ru-make-map-taller.css13
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1836105-cnn.com-fix-blank-pages-when-printing.css19
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848711-vio.com-page-height.css16
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848713-cleanrider.com-slider.css20
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848849-theaa.com-printing-mode-fix.css19
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1849019-axa-assistance.pl-datepicker-fix.css19
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1849388-kucharkaprodceru.cz-scroll-fix.css17
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1868345-tvmovie.de-scroll-fix.css17
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1877346-offerup.com-infinite-scroll-fix.css20
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1884842-foodora.cz-height-fix.css25
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug0000000-testbed-js-injection.js15
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1448747-fastclick-shim.js35
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js33
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js38
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js52
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js32
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1722955-frontgate.com-ua-override.js21
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1724868-news.yahoo.co.jp-ua-override.js29
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1739489-draftjs-beforeinput.js116
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js35
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1774005-installtrigger-shim.js26
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1799968-www.samsung.com-appVersion-linux-fix.js31
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1799980-healow.com-infinite-loop-fix.js37
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1818818-fastclick-legacy-shim.js24
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819450-cmbchina.com-ua-change.js29
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819476-axisbank.com-webkitSpeechRecognition-shim.js26
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819678-free4talk.com-window-chrome-shim.js25
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1830776-blueshieldca.com-unsupported.js24
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1831007-nintendo-window-OnetrustActiveGroups.js27
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1836157-thai-masszazs-niceScroll-disable.js23
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1842437-www.youtube.com-performance-now-precision.js39
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1849058-nicochannel.jp-picture-in-picture-shim.js26
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js27
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1855071-www.meteoam.it.js46
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1859617-installtrigger-removal-shim.js26
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1864564-esri-transfrom-names-shim.js46
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/about_compat_broker.js141
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/custom_functions.js109
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/injections.js272
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/intervention_helpers.js233
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/messaging_helper.js36
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/module_shim.js24
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/requestStorageAccess_helper.js30
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/shim_messaging_helper.js65
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/shims.js1110
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/ua_helpers.js99
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/ua_overrides.js210
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/manifest.json160
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/run.js45
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/addthis-angular.js16
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adform.js30
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adnexus-ast.js210
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adnexus-prebid.js68
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adsafeprotected-ima.js19
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/apstag.js73
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/blogger.js53
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/bloggerAccount.js68
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/bmauth.js21
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/branch.js84
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/chartbeat.js18
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/crave-ca.js56
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/criteo.js64
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/cxense.js593
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/doubleverify.js36
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/eluminate.js95
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/empty-script.js5
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/empty-shim.txt0
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/everest.js171
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/facebook-sdk.js554
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/facebook.svg3
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/fastclick.js75
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/firebase.js95
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ads.js77
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-and-tag-manager.js187
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js13
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-legacy.js137
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ima.js620
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-page-ad.js17
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-publisher-tags.js509
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-safeframe.html29
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/history.js54
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iam.js39
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iaspet.js45
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/instagram.js55
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/kinja.js44
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/live-test-shim.js82
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/maxmind-geoip.js69
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/microsoftLogin.js31
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/microsoftVirtualAssistant.js46
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/moat.js46
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-1.js87
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-2.js85
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-3.js7
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/nielsen.js111
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/optimizely.js205
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/play.svg7
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/rambler-authenticator.js84
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/rich-relevance.js288
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/salesforce.js47
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/spotify-embed.js133
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/tracking-pixel.pngbin0 -> 70 bytes
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/tsn-ca.js57
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vast2.xml12
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vast3.xml12
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vidible.js424
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vmad.xml12
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/webtrends.js46
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/main/java/mozilla/components/feature/webcompat/WebCompatFeature.kt34
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/test/java/mozilla/components/feature/webcompat/WebCompatFeatureTest.kt42
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/webcompat/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/README.md19
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/build.gradle46
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/NativeNotificationBridge.kt107
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationFeature.kt122
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationIntentProcessor.kt36
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-an/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ar/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-az/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ban/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ceb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ff/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gu-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hi-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kn/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-mr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-my/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ne-rNP/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ro/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ta/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-te/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tl/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tok/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tzm/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ur/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-yo/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt140
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt220
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationIntentProcessorTest.kt51
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/webnotifications/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/auth/build.gradle38
-rw-r--r--mobile/android/android-components/components/lib/auth/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/auth/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt29
-rw-r--r--mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.kt78
-rw-r--r--mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt51
-rw-r--r--mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.kt91
-rw-r--r--mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt50
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/build.gradle45
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/main/AndroidManifest.xml15
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt201
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.kt41
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt36
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt276
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt46
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt35
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/crash/README.md239
-rw-r--r--mobile/android/android-components/components/lib/crash/build.gradle99
-rw-r--r--mobile/android/android-components/components/lib/crash/images/crash-dialog.pngbin0 -> 20052 bytes
-rw-r--r--mobile/android/android-components/components/lib/crash/images/crash-in-app.pngbin0 -> 6574 bytes
-rw-r--r--mobile/android/android-components/components/lib/crash/metrics.yaml154
-rw-r--r--mobile/android/android-components/components/lib/crash/pings.yaml28
-rw-r--r--mobile/android/android-components/components/lib/crash/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json84
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml49
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt175
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt376
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.kt78
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt40
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt54
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt22
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt52
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt76
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt60
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt121
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt48
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt158
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt54
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt27
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt312
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt566
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt93
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt66
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt36
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt163
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt65
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml14
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml23
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml81
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml40
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml11
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml35
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml30
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml25
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml13
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml36
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml12
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml39
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml39
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml9
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml24
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml44
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml13
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt192
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt931
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt105
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt74
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.kt34
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt122
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt98
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.kt175
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt263
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt464
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt693
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt169
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt142
-rwxr-xr-xmobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile1
-rwxr-xr-xmobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile1
-rwxr-xr-xmobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile31
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/README.md19
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/build.gradle39
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt314
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt17
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt231
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt151
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt137
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt190
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt215
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md25
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/build.gradle40
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt195
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.kt40
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/README.md25
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/build.gradle43
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt149
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.kt27
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/jexl/README.md236
-rw-r--r--mobile/android/android-components/components/lib/jexl/build.gradle34
-rw-r--r--mobile/android/android-components/components/lib/jexl/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt102
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt230
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt86
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.kt158
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.kt51
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt32
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt141
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt223
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt85
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt32
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt252
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt230
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt307
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt112
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt53
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt375
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.kt65
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt483
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt506
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt170
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/README.md64
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/build.gradle49
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/assets/publicsuffixesbin0 -> 107497 bytes
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt138
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt158
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt50
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt122
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt482
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/README.md59
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/build.gradle42
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt103
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt115
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/state/README.md69
-rw-r--r--mobile/android/android-components/components/lib/state/build.gradle69
-rw-r--r--mobile/android/android-components/components/lib/state/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt182
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml8
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt14
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt23
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt53
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt10
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt13
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt10
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt187
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt147
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt105
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt265
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt39
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt44
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt67
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt58
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt33
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt311
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt301
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt572
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt89
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt98
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/contile/README.md20
-rw-r--r--mobile/android/android-components/components/service/contile/build.gradle45
-rw-r--r--mobile/android/android-components/components/service/contile/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/service/contile/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesProvider.kt307
-rw-r--r--mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdater.kt81
-rw-r--r--mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorker.kt34
-rw-r--r--mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUseCases.kt58
-rw-r--r--mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt335
-rw-r--r--mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt97
-rw-r--r--mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt69
-rw-r--r--mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt48
-rw-r--r--mobile/android/android-components/components/service/contile/src/test/resources/contile/contile.json24
-rw-r--r--mobile/android/android-components/components/service/contile/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/service/contile/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/README.md40
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/build.gradle42
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinder.kt128
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/AssetDescriptor.kt41
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Constants.kt10
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Relation.kt29
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/RelationChecker.kt21
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Statement.kt25
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/StatementListFetcher.kt16
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/CheckAssetLinksResponse.kt25
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApi.kt120
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/ListStatementsResponse.kt56
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/ext/Client.kt20
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/ext/Response.kt20
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/local/StatementApi.kt143
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/local/StatementRelationChecker.kt35
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinderTest.kt170
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApiTest.kt229
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementApiTest.kt356
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementRelationCheckerTest.kt89
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/README.md317
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/build.gradle66
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/proguard-rules-consumer.pro1
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/proguard-rules.pro25
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/AndroidManifest.xml5
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt267
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Config.kt90
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Exceptions.kt107
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt258
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt286
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceSettingsCache.kt66
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/SyncAuthInfoCache.kt58
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/SyncFacts.kt40
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt251
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Utils.kt153
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/AppServicesStateMachineChecker.kt224
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt938
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/GlobalAccountManager.kt79
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt216
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/SyncEnginesStorage.kt57
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/ext/FxaAccountManager.kt18
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt28
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt63
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt28
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt138
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/SyncManager.kt234
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/Types.kt71
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/WorkManagerSyncManager.kt585
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/AccountStorageTest.kt166
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt1646
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt498
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceSettingsCacheTest.kt69
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/SyncAuthInfoCacheTest.kt51
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/UtilsKtTest.kt391
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/GlobalAccountManagerTest.kt55
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt134
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/SyncEnginesStorageTest.kt47
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/ext/FxaAccountManagerKtTest.kt36
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt190
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/TypesTest.kt77
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/WorkManagerSyncManagerTest.kt97
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/glean/README.md21
-rw-r--r--mobile/android/android-components/components/service/glean/build.gradle58
-rw-r--r--mobile/android/android-components/components/service/glean/gradle.properties0
-rw-r--r--mobile/android/android-components/components/service/glean/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt145
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt50
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt109
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt119
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/ErrorType.kt10
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt31
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt7
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanFromJavaTest.java68
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt44
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt340
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/location/.gitignore1
-rw-r--r--mobile/android/android-components/components/service/location/README.md19
-rw-r--r--mobile/android/android-components/components/service/location/build.gradle44
-rw-r--r--mobile/android/android-components/components/service/location/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/service/location/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/LocationService.kt57
-rw-r--r--mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/MozillaLocationService.kt187
-rw-r--r--mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt23
-rw-r--r--mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt399
-rw-r--r--mobile/android/android-components/components/service/location/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/service/location/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/nimbus/.gitignore1
-rw-r--r--mobile/android/android-components/components/service/nimbus/README.md352
-rw-r--r--mobile/android/android-components/components/service/nimbus/build.gradle122
-rw-r--r--mobile/android/android-components/components/service/nimbus/messaging.fml.yaml194
-rw-r--r--mobile/android/android-components/components/service/nimbus/metrics.yaml110
-rw-r--r--mobile/android/android-components/components/service/nimbus/proguard-rules-consumer.pro4
-rw-r--r--mobile/android/android-components/components/service/nimbus/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/Nimbus.kt78
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/NimbusBuilder.kt54
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/NimbusUtils.kt20
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/JexlAttributeProvider.kt21
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/Message.kt82
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageMetadataStorage.kt26
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageSurfaceId.kt10
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt130
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerInterface.kt73
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorage.kt416
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorage.kt96
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchAdapter.kt98
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchItemViewHolder.kt34
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchesAdapterDelegate.kt19
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentAdapter.kt49
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentItemViewHolder.kt30
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentsAdapterDelegate.kt19
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_branch_item.xml58
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiment_details.xml16
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiment_item.xml57
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiments.xml28
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ar/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ban/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ceb/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-hi-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-kn/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-my/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ne-rNP/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-te/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-tl/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-ur/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-yo/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/NimbusTest.kt47
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerTest.kt276
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorageTest.kt927
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorageTest.kt139
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/ui/NimbusBranchItemViewHolderTest.kt92
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/ui/NimbusExperimentItemViewHolderTest.kt61
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/service/nimbus/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/pocket/.gitignore2
-rw-r--r--mobile/android/android-components/components/service/pocket/README.md44
-rw-r--r--mobile/android/android-components/components/service/pocket/build.gradle77
-rw-r--r--mobile/android/android-components/components/service/pocket/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/1.json70
-rw-r--r--mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/2.json120
-rw-r--r--mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/3.json194
-rw-r--r--mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/4.json204
-rw-r--r--mobile/android/android-components/components/service/pocket/src/androidTest/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabaseTest.kt723
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt69
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/Logger.kt12
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt68
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt172
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStory.kt99
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/ConceptFetch.kt30
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt99
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt50
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt69
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt186
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt60
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt67
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt178
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt91
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt27
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocEntity.kt41
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntity.kt45
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt73
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepository.kt47
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketStoriesUseCases.kt112
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketApiStory.kt28
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpoint.kt49
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpointRaw.kt52
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketJSONParser.kt78
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketResponse.kt42
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDao.kt46
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt185
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketStoryEntity.kt31
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorker.kt36
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/PocketStoriesRefreshScheduler.kt64
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshPocketWorker.kt36
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshSpocsWorker.kt36
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/SpocsRefreshScheduler.kt94
-rw-r--r--mobile/android/android-components/components/service/pocket/src/main/res/values/ids.xml10
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt50
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt62
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt229
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoryTest.kt100
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/ConceptFetchKtTest.kt80
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt114
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt135
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/Assert.kt83
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/MockResponses.kt30
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt171
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt93
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt314
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt327
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt161
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt200
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt17
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt29
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt513
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepositoryTest.kt75
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketStoriesUseCasesTest.kt197
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketApiStoryTest.kt17
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointRawTest.kt115
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointTest.kt97
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketJSONParserTest.kt182
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketResponseTest.kt56
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDaoTest.kt387
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketStoryEntityTest.kt23
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorkerTest.kt65
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/PocketStoriesRefreshSchedulerTest.kt103
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshPocketWorkerTest.kt64
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshSpocsWorkerTest.kt64
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/SpocsRefreshSchedulerTest.kt161
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/sponsored_stories_response.json98
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json44
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story.json8
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_imageUrl_response.json12
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_title_response.json12
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_url_response.json12
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json12
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/README.md19
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/build.gradle49
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt209
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCrypto.kt111
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt51
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/Errors.kt27
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt107
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/Types.kt92
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt407
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCryptoTest.kt147
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegateTest.kt134
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegateTest.kt243
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/service/sync-autofill/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/service/sync-logins/README.md76
-rw-r--r--mobile/android/android-components/components/service/sync-logins/build.gradle53
-rw-r--r--mobile/android/android-components/components/service/sync-logins/proguard-rules-consumer.pro1
-rw-r--r--mobile/android/android-components/components/service/sync-logins/proguard-rules.pro25
-rw-r--r--mobile/android/android-components/components/service/sync-logins/src/main/AndroidManifest.xml5
-rw-r--r--mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/DefaultLoginValidationDelegate.kt46
-rw-r--r--mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt69
-rw-r--r--mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/LoginsCrypto.kt129
-rw-r--r--mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt280
-rw-r--r--mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/Types.kt87
-rw-r--r--mobile/android/android-components/components/service/sync-logins/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/android-test/README.md19
-rw-r--r--mobile/android/android-components/components/support/android-test/build.gradle46
-rw-r--r--mobile/android/android-components/components/support/android-test/lint.xml13
-rw-r--r--mobile/android/android-components/components/support/android-test/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/Matchers.kt26
-rw-r--r--mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/espresso/ViewInteraction.kt54
-rw-r--r--mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/espresso/matcher/ViewMatchers.kt43
-rw-r--r--mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/rules/WebserverRule.kt65
-rw-r--r--mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/MatchersTest.kt27
-rw-r--r--mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/espresso/matcher/ViewMatchersKtTest.kt37
-rw-r--r--mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/helpers/Assert.kt18
-rw-r--r--mobile/android/android-components/components/support/base/README.md113
-rw-r--r--mobile/android/android-components/components/support/base/build.gradle126
-rw-r--r--mobile/android/android-components/components/support/base/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/support/base/src/main/AndroidManifest.xml6
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/Build.kt34
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/Clock.kt71
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NotificationsDelegate.kt178
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/Padding.kt23
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/coroutines/Dispatchers.kt33
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/NotificationManagerCompat.kt60
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/Throwable.kt88
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Action.kt97
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Fact.kt29
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/FactProcessor.kt20
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Facts.kt34
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/processor/CollectionProcessor.kt54
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/processor/LogFactProcessor.kt21
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ActivityResultHandler.kt22
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/LifecycleAwareFeature.kt32
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/PermissionsFeature.kt51
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/UserInteractionHandler.kt28
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapper.kt224
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ids/SharedIds.kt112
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ids/SharedIdsHelper.kt61
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/Log.kt93
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/logger/Logger.kt104
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/AndroidLogSink.kt43
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/LogSink.kt28
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/TestModeLogSink.kt25
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/Consumable.kt198
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/Observable.kt154
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/ObserverRegistry.kt257
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/LazyComponent.kt80
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/NamedThreadFactory.kt26
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/SharedPreferencesCache.kt77
-rw-r--r--mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/worker/Frequency.kt18
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-am/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ar/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ast/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-azb/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ban/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-be/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-bg/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-bn/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-br/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-bs/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ca/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-cak/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ceb/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ckb/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-co/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-cs/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-cy/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-da/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-de/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-dsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-el/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-en-rCA/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-en-rGB/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-eo/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-es-rAR/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-es-rCL/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-es-rES/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-es-rMX/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-es/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-et/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-eu/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-fa/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-fi/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-fr/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-fur/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-fy-rNL/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-gd/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-gl/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-gn/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-hi-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-hr/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-hsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-hu/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-hy-rAM/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ia/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-in/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-is/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-it/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-iw/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ja/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ka/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-kaa/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-kab/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-kk/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-kmr/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-kn/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ko/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-lo/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-lt/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-my/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-nb-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ne-rNP/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-nl/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-nn-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-oc/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-pa-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-pa-rPK/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-pl/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-pt-rBR/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-pt-rPT/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-rm/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ro/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ru/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-sat/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-sc/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-si/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-sk/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-skr/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-sl/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-sq/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-sr/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-su/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-sv-rSE/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-te/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-tg/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-th/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-tl/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-tr/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-trs/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-tt/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-tzm/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ug/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-uk/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-ur/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-uz/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-vi/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-yo/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-zh-rCN/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values-zh-rTW/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values/mozac_support_base_strings.xml7
-rw-r--r--mobile/android/android-components/components/support/base/src/main/res/values/strings.xml10
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/BuildTest.kt22
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/android/PaddingTest.kt22
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/ext/NotificationManagerCompatTest.kt113
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/ext/ThrowableTest.kt70
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactProcessorTest.kt36
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactTest.kt65
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactsTest.kt63
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/processor/LogFactProcessorTest.kt31
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapperTest.kt474
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/LogTest.kt91
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/fake/FakeLogSink.kt24
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/logger/LoggerTest.kt179
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/sink/AndroidLogSinkTest.kt183
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/notification/SharedIdsHelperTest.kt71
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/notification/SharedIdsTest.kt55
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/observer/ConsumableTest.kt506
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/observer/ObserverRegistryTest.kt658
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/LazyComponentTest.kt72
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/NamedThreadFactoryTest.kt52
-rw-r--r--mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/SharedPreferencesCacheTest.kt60
-rw-r--r--mobile/android/android-components/components/support/base/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/support/base/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/images/README.md19
-rw-r--r--mobile/android/android-components/components/support/images/build.gradle70
-rw-r--r--mobile/android/android-components/components/support/images/proguard-rules-consumer.pro1
-rw-r--r--mobile/android/android-components/components/support/images/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/support/images/src/androidTest/java/mozilla/components/support/images/decoder/OnDeviceAndroidImageDecoderTest.kt114
-rw-r--r--mobile/android/android-components/components/support/images/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/CancelOnDetach.kt18
-rw-r--r--mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/DesiredSize.kt24
-rw-r--r--mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoader.kt174
-rw-r--r--mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoaderScope.kt85
-rw-r--r--mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoaderState.kt29
-rw-r--r--mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/decoder/AndroidImageDecoder.kt88
-rw-r--r--mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/decoder/ImageDecoder.kt36
-rw-r--r--mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/CancelOnDetachTest.kt35
-rw-r--r--mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/DesiredSizeTest.kt21
-rw-r--r--mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/decoder/AndroidImageDecoderTest.kt259
-rw-r--r--mobile/android/android-components/components/support/images/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker3
-rw-r--r--mobile/android/android-components/components/support/images/src/test/resources/png/mozac.pngbin0 -> 406 bytes
-rw-r--r--mobile/android/android-components/components/support/images/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/ktx/README.md19
-rw-r--r--mobile/android/android-components/components/support/ktx/build.gradle64
-rw-r--r--mobile/android/android-components/components/support/ktx/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml11
-rw-r--r--mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt12
-rw-r--r--mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt31
-rw-r--r--mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt57
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml11
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt13
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt344
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt75
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt193
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt23
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt20
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt102
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt23
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt80
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt194
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt49
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt57
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt98
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt38
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt20
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt28
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt12
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt36
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt47
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt95
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt19
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt41
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt187
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt46
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt33
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt26
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt107
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt14
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt26
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt442
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt40
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt98
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt104
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml15
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt26
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt55
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt304
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt115
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt229
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt49
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt78
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt74
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt78
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt243
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt137
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt131
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt66
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt64
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt45
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt21
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt27
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt72
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt143
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt57
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt98
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt221
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt135
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt142
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt40
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt29
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt70
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt595
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt38
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt90
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt72
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt94
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt51
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/license/README.md63
-rw-r--r--mobile/android/android-components/components/support/license/build.gradle41
-rw-r--r--mobile/android/android-components/components/support/license/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/support/license/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/license/src/main/java/mozilla/components/support/license/LibrariesListFragment.kt129
-rw-r--r--mobile/android/android-components/components/support/license/src/main/res/layout/fragment_libraries_list.xml18
-rw-r--r--mobile/android/android-components/components/support/license/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/locale/README.md19
-rw-r--r--mobile/android/android-components/components/support/locale/build.gradle48
-rw-r--r--mobile/android/android-components/components/support/locale/gradle.properties0
-rw-r--r--mobile/android/android-components/components/support/locale/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/support/locale/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/ActivityContextWrapper.kt43
-rw-r--r--mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt57
-rw-r--r--mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt55
-rw-r--r--mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt25
-rw-r--r--mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt145
-rw-r--r--mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleMiddleware.kt64
-rw-r--r--mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleUseCases.kt53
-rw-r--r--mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/ActivityContextWrapperTest.kt47
-rw-r--r--mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleAwareAppCompatActivityTest.kt35
-rw-r--r--mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt153
-rw-r--r--mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleMiddlewareTest.kt99
-rw-r--r--mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleUseCasesTest.kt41
-rw-r--r--mobile/android/android-components/components/support/locale/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/support/locale/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/remotesettings/README.md18
-rw-r--r--mobile/android/android-components/components/support/remotesettings/build.gradle59
-rw-r--r--mobile/android/android-components/components/support/remotesettings/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/support/remotesettings/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/remotesettings/src/main/java/mozilla/components/support/remotesettings/RemoteSettingsClient.kt265
-rw-r--r--mobile/android/android-components/components/support/remotesettings/src/test/java/mozilla/components/support/remotesettings/RemoteSettingsClientTest.kt230
-rw-r--r--mobile/android/android-components/components/support/remotesettings/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/support/remotesettings/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/rusterrors/README.md5
-rw-r--r--mobile/android/android-components/components/support/rusterrors/build.gradle39
-rw-r--r--mobile/android/android-components/components/support/rusterrors/proguard-rules-consumer.pro1
-rw-r--r--mobile/android/android-components/components/support/rusterrors/proguard-rules.pro25
-rw-r--r--mobile/android/android-components/components/support/rusterrors/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/rusterrors/src/main/java/mozilla/components/support/rusterrors/RustErrors.kt39
-rw-r--r--mobile/android/android-components/components/support/rusthttp/README.md7
-rw-r--r--mobile/android/android-components/components/support/rusthttp/build.gradle42
-rw-r--r--mobile/android/android-components/components/support/rusthttp/proguard-rules-consumer.pro1
-rw-r--r--mobile/android/android-components/components/support/rusthttp/proguard-rules.pro25
-rw-r--r--mobile/android/android-components/components/support/rusthttp/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/rusthttp/src/main/java/mozilla/components/support/rusthttp/RustHttpConfig.kt38
-rw-r--r--mobile/android/android-components/components/support/rustlog/README.md0
-rw-r--r--mobile/android/android-components/components/support/rustlog/build.gradle50
-rw-r--r--mobile/android/android-components/components/support/rustlog/proguard-rules-consumer.pro1
-rw-r--r--mobile/android/android-components/components/support/rustlog/proguard-rules.pro25
-rw-r--r--mobile/android/android-components/components/support/rustlog/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/rustlog/src/main/java/mozilla/components/support/rustlog/RustLog.kt91
-rw-r--r--mobile/android/android-components/components/support/rustlog/src/test/java/mozilla/components/support/rustlog/RustLogTest.kt71
-rw-r--r--mobile/android/android-components/components/support/rustlog/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/test-appservices/README.md19
-rw-r--r--mobile/android/android-components/components/support/test-appservices/build.gradle37
-rw-r--r--mobile/android/android-components/components/support/test-appservices/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/test-fakes/README.md19
-rw-r--r--mobile/android/android-components/components/support/test-fakes/build.gradle38
-rw-r--r--mobile/android/android-components/components/support/test-fakes/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/android/FakeContext.kt279
-rw-r--r--mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/android/FakeSharedPreferences.kt89
-rw-r--r--mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngine.kt71
-rw-r--r--mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngineSessionState.kt24
-rw-r--r--mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngineView.kt33
-rw-r--r--mobile/android/android-components/components/support/test-libstate/README.md19
-rw-r--r--mobile/android/android-components/components/support/test-libstate/build.gradle45
-rw-r--r--mobile/android/android-components/components/support/test-libstate/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/test-libstate/src/main/java/mozilla/components/support/test/libstate/ext/Store.kt26
-rw-r--r--mobile/android/android-components/components/support/test-libstate/src/main/java/mozilla/components/support/test/middleware/CaptureActionsMiddleware.kt88
-rw-r--r--mobile/android/android-components/components/support/test-libstate/src/test/java/mozilla/components/support/test/libstate/ext/StoreTest.kt43
-rw-r--r--mobile/android/android-components/components/support/test/README.md19
-rw-r--r--mobile/android/android-components/components/support/test/build.gradle58
-rw-r--r--mobile/android/android-components/components/support/test/lint.xml13
-rw-r--r--mobile/android/android-components/components/support/test/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Expect.kt22
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/KArgumentCaptor.kt44
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Matchers.kt60
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Mock.kt54
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ThrowProperty.kt19
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt19
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Job.kt15
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/KProperty.kt29
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/fakes/FakeClock.kt19
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/file/Resources.kt19
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Extensions.kt14
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Fragments.kt32
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Permissions.kt21
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/shadow/PixelCopyShadow.kt41
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/Helpers.kt57
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/MainCoroutineRule.kt56
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/PermissionsTest.kt28
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/ThrowPropertyTest.kt24
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/file/ResourcesTest.kt17
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/ExtensionsTest.kt23
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/FragmentsTest.kt30
-rw-r--r--mobile/android/android-components/components/support/test/src/test/resources/example_file.txt1
-rw-r--r--mobile/android/android-components/components/support/test/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/utils/README.md19
-rw-r--r--mobile/android/android-components/components/support/utils/build.gradle47
-rw-r--r--mobile/android/android-components/components/support/utils/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/AndroidManifest.xml14
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/ktx/util/URLStringUtils.kt167
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BootUtils.kt63
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/Browsers.kt431
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BrowsersCache.kt52
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ColorUtils.kt45
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/CreditCardUtils.kt307
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DomainMatcher.kt108
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DownloadUtils.kt390
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DrawableUtils.kt43
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ManufacturerCodes.kt32
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/PendingIntentUtils.kt28
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/Performance.kt26
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/RunWhenReadyQueue.kt65
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeBundle.kt57
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeIntent.kt117
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeUrl.kt39
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/StatusBarUtils.kt30
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/StorageUtils.kt46
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ThreadUtils.kt60
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/TimePicker.kt32
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/WebURLFinder.kt162
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Bitmap.kt31
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Bundle.kt66
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Context.kt89
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Fragment.kt26
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Intent.kt47
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/PackageManager.kt68
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Pair.kt21
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Service.kt26
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/String.kt13
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/WindowInsetsCompat.kt38
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/intents.kt33
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_amex.xml4
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_diners.xml80
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_discover.xml41
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_jcb.xml121
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_mastercard.xml38
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_mir.xml30
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_unionpay.xml143
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_visa.xml28
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_icon_credit_card_generic.xml19
-rw-r--r--mobile/android/android-components/components/support/utils/src/main/res/values/arrays.xml10
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BootUtilsTest.kt85
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BrowsersCacheTest.kt152
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BrowsersTest.kt380
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BundleTest.kt116
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ColorUtilsTest.kt55
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/CreditCardUtilsTest.kt107
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/DomainMatcherTest.kt100
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/DownloadUtilsTest.kt189
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ManufacturerCodesTest.kt114
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/PendingIntentUtilsTest.kt28
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/RunWhenReadyQueueTest.kt71
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeBundleTest.kt60
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeIntentTest.kt254
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeUrlTest.kt68
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/StorageUtilsTest.kt57
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/URLStringUtilsTest.kt297
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/WebURLFinderTest.kt233
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/BitmapTest.kt122
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/PairKtTest.kt31
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/StringTest.kt19
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/WindowInsetsCompatTest.kt81
-rw-r--r--mobile/android/android-components/components/support/utils/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/support/webextensions/README.md37
-rw-r--r--mobile/android/android-components/components/support/webextensions/build.gradle55
-rw-r--r--mobile/android/android-components/components/support/webextensions/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/ExtensionsProcessDisabledPromptObserver.kt52
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionController.kt179
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionPopupObserver.kt48
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt535
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/facts/WebExtensionFacts.kt51
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionControllerTest.kt238
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionPopupObserverTest.kt55
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt1077
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/support/webextensions/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/tooling/detekt/README.md29
-rw-r--r--mobile/android/android-components/components/tooling/detekt/build.gradle28
-rw-r--r--mobile/android/android-components/components/tooling/detekt/src/main/kotlin/mozilla/components/tooling/detekt/MozillaRuleSetProvider.kt24
-rw-r--r--mobile/android/android-components/components/tooling/detekt/src/main/kotlin/mozilla/components/tooling/detekt/ProjectLicenseRule.kt52
-rw-r--r--mobile/android/android-components/components/tooling/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider1
-rw-r--r--mobile/android/android-components/components/tooling/detekt/src/test/kotlin/ProjectLicenseRuleTest.kt63
-rw-r--r--mobile/android/android-components/components/tooling/fetch-tests/README.md11
-rw-r--r--mobile/android/android-components/components/tooling/fetch-tests/build.gradle38
-rw-r--r--mobile/android/android-components/components/tooling/fetch-tests/lint.xml10
-rw-r--r--mobile/android/android-components/components/tooling/fetch-tests/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/tooling/fetch-tests/src/main/java/mozilla/components/tooling/fetch/tests/FetchTestCases.kt546
-rw-r--r--mobile/android/android-components/components/tooling/lint/README.md11
-rw-r--r--mobile/android/android-components/components/tooling/lint/build.gradle42
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/AndroidSrcXmlDetector.kt82
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ConceptFetchDetector.kt249
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/FactCollectDetector.kt103
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetector.kt81
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintIssueRegistry.kt25
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintLogChecks.kt61
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/NotificationManagerChecks.kt84
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetector.kt97
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/AndroidSrcXmlDetectorTest.kt68
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ConceptFetchDetectorTest.kt277
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/FactCollectDetectorTest.kt220
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetectorTest.kt69
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/LintLogChecksTest.kt56
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetectorTest.kt68
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/README.md19
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/build.gradle40
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/src/main/java/mozilla/components/ui/autocomplete/InlineAutocompleteEditText.kt905
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/src/main/res/values/attrs.xml9
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/src/test/java/mozilla/components/ui/autocomplete/InlineAutocompleteEditTextTest.kt585
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/ui/autocomplete/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/ui/colors/README.md19
-rw-r--r--mobile/android/android-components/components/ui/colors/build.gradle40
-rw-r--r--mobile/android/android-components/components/ui/colors/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/ui/colors/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/ui/colors/src/main/java/mozilla/components/ui/colors/PhotonColors.kt178
-rw-r--r--mobile/android/android-components/components/ui/colors/src/main/res/values/photon_colors.xml219
-rw-r--r--mobile/android/android-components/components/ui/fonts/README.md19
-rw-r--r--mobile/android/android-components/components/ui/fonts/build.gradle31
-rw-r--r--mobile/android/android-components/components/ui/fonts/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/ui/fonts/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/ui/fonts/src/main/res/values/roboto_fonts.xml18
-rw-r--r--mobile/android/android-components/components/ui/icons/README.md19
-rw-r--r--mobile/android/android-components/components/ui/icons/build.gradle31
-rw-r--r--mobile/android/android-components/components/ui/icons/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_asleep.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_confused.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_eye_roll.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_hourglass.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_inspect.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_lock.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_no_internet.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_question_file.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_shred_file.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_surprised.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_unplugged.svg4
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_add_to_homescreen_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_app_menu_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_app_menu_space_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_down_left_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_up_left_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_up_right_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_arrow_clockwise_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_slash_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_circle_24.xml18
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_circle_fill_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_info_circle_fill_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_warning_circle_fill_24.xml22
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_back_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_20.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_24.xml14
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_badge_fill_20.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_fill_20.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_fill_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_slash_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_tray_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_tray_fill_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_briefcase.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_broken_lock.xml21
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_camera_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_camera_slash_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cart.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_checkmark_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_20.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_8.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_left_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_right_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_up_20.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_up_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chill.xml19
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_circle.xml9
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_clipboard_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_collection_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_competitiveness_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies_slash_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_copy_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_credit_card_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_critical_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_critical_fill_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_20.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_fill_20.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_fill_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cryptominer_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_data_clearance_24.xml14
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_debug_drawer_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_delete_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_desktop_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_desktop_send_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_mobile_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_dollar.xml19
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_download_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_dropdown_arrow.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_edit_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_ellipsis_horizontal_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_ellipsis_vertical_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_experiments_24.xml28
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_cog_24.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_fill_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_external_link_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_eye_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_eye_slash_24.xml24
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fence.xml13
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fingerprinter_24.xml18
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_folder_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_folder_add_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_font.xml14
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_food.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_forward_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fruit.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_gift.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_globe_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_grid.xml13
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_grid_add_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_help_circle_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_help_circle_fill_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_history_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_home_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_image_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_image_slash_24.xml24
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_information_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_information_fill_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lightbulb_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_link_24.xml18
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_location_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_location_slash_24.xml18
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_20.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_slash_20.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_slash_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_warning_20.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_warning_24.xml18
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_login_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_chrome_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_firefox_24.xml14
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_safari_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_microphone_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_microphone_slash_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_more_grid_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_mozilla.xml7
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_night_mode_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_dot_badge_fill_20.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_slash_24.xml24
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_open_in.xml7
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_packaging_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_page_zoom_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_page_zoom_fill_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_passkey_24.xml18
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pause_badge_fill_16.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_permissions_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pet.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_badge_fill_16.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_fill_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_filled.xml13
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_slash_24.xml20
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_slash_fill_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_play_badge_fill_16.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_plugin_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_plus_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_preferences.xml14
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_price_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_print_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_24.xml14
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_20.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_24.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_48.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_stroke_20.xml22
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_qr_code_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_quality_24.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reader_view_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reader_view_fill_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reading_list_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reading_list_add_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reorder.xml7
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_rocket.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_rocket_filled.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_save_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_save_file_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_search_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_select_all.xml13
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_settings_24.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_share_android_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_share_apple_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shield_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shield_slash_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shipping_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shopping_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_social_tracker_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sparkle_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_fill_20.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_half_fill_20.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_one_half_fill_20.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_stop.xml13
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_storage_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_storage_slash_24.xml24
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sync_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sync_tabs_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab.xml13
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_badge_fill_20.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_new.xml13
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_number_24.xml17
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_tray_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_themes_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tool_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_translate_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tree.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_update_circle_24.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_vacation.xml26
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_warning_24.xml18
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_warning_fill_24.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_web_extension_default_icon.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_whats_new_24.xml16
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/values/attrs.xml12
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/values/colors.xml15
-rw-r--r--mobile/android/android-components/components/ui/icons/src/main/res/values/mozac_ui_icons_strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/.gitignore1
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/README.md37
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/build.gradle50
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/AndroidManifest.xml8
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/java/mozilla/components/ui/tabcounter/TabCounter.kt348
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/java/mozilla/components/ui/tabcounter/TabCounterMenu.kt101
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_bar.xml11
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_box.xml15
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_round_rectangle_ripple.xml13
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/layout/mozac_ui_tabcounter_layout.xml50
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-am/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ar/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ast/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-azb/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-be/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bg/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bn/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-br/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bs/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ca/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cak/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ceb/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ckb/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-co/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cs/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cy/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-da/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-de/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-dsb/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-el/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-en-rCA/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-en-rGB/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-eo/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rAR/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rCL/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rES/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rMX/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-et/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-eu/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fa/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ff/strings.xml7
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fi/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fr/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fur/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fy-rNL/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gd/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gl/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gn/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hi-rIN/strings.xml15
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hr/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hsb/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hu/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hy-rAM/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ia/strings.xml18
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-in/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-is/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-it/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-iw/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ja/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ka/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kaa/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kab/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kk/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kmr/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ko/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-lo/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-lt/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-my/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nb-rNO/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ne-rNP/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nl/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nn-rNO/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-oc/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-or/strings.xml11
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pa-rIN/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pa-rPK/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pl/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pt-rBR/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pt-rPT/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-rm/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ro/strings.xml11
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ru/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sat/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sc/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-si/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sk/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-skr/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sl/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sq/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sr/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-su/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sv-rSE/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-szl/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-te/strings.xml13
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tg/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-th/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tl/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tr/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-trs/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tt/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tzm/strings.xml9
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ug/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-uk/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ur/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-uz/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-vi/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-yo/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-zh-rCN/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values-zh-rTW/strings.xml17
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values/attrs.xml11
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values/colors.xml9
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values/dimens.xml8
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/main/res/values/strings.xml20
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/test/java/mozilla/components/ui/tabcounter/TabCounterMenuTest.kt53
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/test/java/mozilla/components/ui/tabcounter/TabCounterTest.kt70
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/ui/tabcounter/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/ui/widgets/README.md19
-rw-r--r--mobile/android/android-components/components/ui/widgets/build.gradle51
-rw-r--r--mobile/android/android-components/components/ui/widgets/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/Extentions.kt37
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/SnackbarDelegate.kt55
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/VerticalSwipeRefreshLayout.kt216
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/WidgetSiteItemView.kt106
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetector.kt192
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewClippingBehavior.kt90
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.kt237
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslationStrategy.kt189
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslator.kt81
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/drawable/mozac_widget_favicon_background.xml17
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/drawable/rounded_button_background.xml18
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/layout/mozac_widget_site_item.xml77
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ar/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ro/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values/attrs.xml19
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values/colors.xml10
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values/dimens.xml14
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/main/res/values/styles.xml63
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/TestUtils.kt73
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/VerticalSwipeRefreshLayoutTest.kt430
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/WidgetSiteItemViewTest.kt93
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetectorTest.kt231
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewClippingBehaviorTest.kt221
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorTest.kt575
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/TestUtils.kt62
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslationStrategyTest.kt712
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslatorTest.kt113
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/ui/widgets/src/test/resources/robolectric.properties1
7031 files changed, 517011 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/browser/domains/README.md b/mobile/android/android-components/components/browser/domains/README.md
new file mode 100644
index 0000000000..b965609bc9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/README.md
@@ -0,0 +1,67 @@
+# [Android Components](../../../README.md) > Browser > Domains
+
+This component provides APIs for managing localized and customizable domain lists (see [Domains](#domains) and [CustomDomains](#customdomains)). It also contains auto-complete functionality for these lists (see [DomainAutoCompleteProvider](#domainautocompleteprovider)) which can be used in conjuction with our [UI autocomplete component](../../ui/autocomplete/README.md).
+
+## Usage
+
+### 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:browser-domains:{latest-version}"
+```
+
+### Domains
+
+The `Domains` object is used to load the built-in localized domain lists which are shipped as part of this component. These lists are grouped by country and can be found [in our repository](src/main/assets/domains).
+
+```Kotlin
+// Load the domain lists for all countries in the default locale (fallback is US)
+val domains = Domains.load(context)
+```
+
+### CustomDomains
+
+The `CustomDomains` object can be used to manage a custom domain list which will be stored in `SharedPreferences`.
+
+```Kotlin
+// Load the custom domain list
+val domains = CustomDomains.load(context)
+
+// Save custom domains
+CustomDomains.save(context, listOf("mozilla.org", "getpocket.com"))
+
+// Remove custom domains
+CustomDomains.remove(context, listOf("nolongerexists.org"))
+```
+
+### DomainAutoCompleteProvider
+
+The class provides auto-complete functionality for both `Domains` and `CustomDomains`.
+
+```Kotlin
+// Initialize the provider
+val provider = DomainAutocompleteProvider()
+provider.initialize(
+ context,
+ useShippedDomains = true,
+ useCustomDomains = true,
+ loadDomainsFromDisk = true
+)
+```
+
+Note that when `loadDomainsFromDisk` is set to true there is no need to manually call `load` on either `Domains` or `CustomDomains`.
+
+```Kotlin
+// Autocomplete domain lists
+val result = provider.autocomplete("moz")
+```
+
+The result will contain the autocompleted text (`result.text`), the URL (`result.url`), and the source of the match (`result.source`), which is either `DEFAULT_LIST` if a result was found in the shipped domain list or `CUSTOM_LIST` otherwise. The custom domain list takes precendece over the built-in shipped domain list and the API will only return the first match.
+
+## 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/browser/domains/build.gradle b/mobile/android/android-components/components/browser/domains/build.gradle
new file mode 100644
index 0000000000..2c0b6b62b5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/build.gradle
@@ -0,0 +1,40 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.browser.domains'
+}
+
+dependencies {
+ implementation project(':concept-toolbar')
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/browser/domains/proguard-rules.pro b/mobile/android/android-components/components/browser/domains/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/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/browser/domains/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/domains/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/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/browser/domains/src/main/assets/domains/br b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/br
new file mode 100644
index 0000000000..3a7cd23b12
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/br
@@ -0,0 +1,50 @@
+google.com.br
+youtube.com
+google.com
+facebook.com
+globo.com
+uol.com.br
+blastingnews.com
+live.com
+mercadolivre.com.br
+yahoo.com
+blogspot.com.br
+wikipedia.org
+whatsapp.com
+netflix.com
+olx.com.br
+instagram.com
+msn.com
+metropoles.com
+fatosdesconhecidos.com.br
+twitter.com
+caixa.gov.br
+uptodown.com
+aliexpress.com
+curapelanatureza.com.br
+wordpress.com
+abril.com.br
+americanas.com.br
+correios.com.br
+reclameaqui.com.br
+bet365.com
+onclkds.com
+bol.uol.com.br
+techtudo.com.br
+fazenda.gov.br
+microsoft.com
+folha.uol.com.br
+linkedin.com
+tumblr.com
+sp.gov.br
+reddit.com
+bb.com.br
+pinterest.com
+itau.com.br
+letras.mus.br
+otvfoco.com.br
+vagalume.com.br
+portalinteressante.com
+myappolicious.com.br
+thewhizmarketing.com
+twitch.tv
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/ca b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/ca
new file mode 100644
index 0000000000..ea63155b7c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/ca
@@ -0,0 +1,49 @@
+google.com
+youtube.com
+facebook.com
+reddit.com
+amazon.com
+wikipedia.org
+yahoo.com
+twitter.com
+netflix.com
+ebay.com
+imgur.com
+linkedin.com
+instagram.com
+diply.com
+craigslist.org
+live.com
+office.com
+twitch.tv
+tumblr.com
+pinterest.com
+espn.com
+cnn.com
+bing.com
+wikia.com
+chase.com
+imdb.com
+nytimes.com
+paypal.com
+blogspot.com
+apple.com
+yelp.com
+stackoverflow.com
+bankofamerica.com
+wordpress.com
+github.com
+microsoft.com
+wellsfargo.com
+zillow.com
+salesforce.com
+msn.com
+walmart.com
+weather.com
+dropbox.com
+buzzfeed.com
+intuit.com
+washingtonpost.com
+soundcloud.com
+huffingtonpost.com
+indeed.com \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/de b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/de
new file mode 100644
index 0000000000..8bd37b7606
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/de
@@ -0,0 +1,50 @@
+google.de
+youtube.com
+google.com
+facebook.com
+amazon.de
+ebay.de
+wikipedia.org
+ebay-kleinanzeigen.de
+web.de
+yahoo.com
+ok.ru
+gmx.net
+reddit.com
+vk.com
+t-online.de
+twitter.com
+spiegel.de
+mail.ru
+instagram.com
+live.com
+chip.de
+bild.de
+paypal.com
+bing.com
+twitch.tv
+whatsapp.com
+yandex.ru
+gutefrage.net
+mobile.de
+google.ru
+blogspot.de
+tumblr.com
+bs.to
+focus.de
+linkedin.com
+netflix.com
+wordpress.com
+imgur.com
+postbank.de
+welt.de
+streamcloud.eu
+microsoft.com
+immobilienscout24.de
+msn.com
+dict.cc
+otto.de
+xing.com
+amazon.com
+heise.de
+github.com \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/fr b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/fr
new file mode 100644
index 0000000000..d582942113
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/fr
@@ -0,0 +1,50 @@
+google.fr
+youtube.com
+google.com
+facebook.com
+wikipedia.org
+amazon.fr
+leboncoin.fr
+yahoo.com
+live.com
+twitter.com
+orange.fr
+free.fr
+linkedin.com
+lemonde.fr
+instagram.com
+reddit.com
+lefigaro.fr
+ebay.fr
+cdiscount.com
+jeuxvideo.com
+zone-telechargement.ws
+labanquepostale.fr
+blogspot.fr
+allocine.fr
+msn.com
+commentcamarche.net
+pole-emploi.fr
+vk.com
+sfr.fr
+lequipe.fr
+twitch.tv
+francetvinfo.fr
+20minutes.fr
+pinterest.com
+netflix.com
+programme-tv.net
+credit-agricole.fr
+linternaute.com
+github.com
+wordpress.com
+caf.fr
+aliexpress.com
+dailymotion.com
+tumblr.com
+t411.ai
+stackoverflow.com
+microsoft.com
+meteofrance.com
+onclkds.com
+bfmtv.com \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/gb b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/gb
new file mode 100644
index 0000000000..57713ed98e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/gb
@@ -0,0 +1,49 @@
+google.co.uk
+youtube.com
+google.com
+facebook.com
+reddit.com
+bbc.co.uk
+amazon.co.uk
+wikipedia.org
+ebay.co.uk
+twitter.com
+live.com
+yahoo.com
+instagram.com
+diply.com
+linkedin.com
+imgur.com
+netflix.com
+theguardian.com
+dailymail.co.uk
+twitch.tv
+imdb.com
+paypal.com
+office.com
+tumblr.com
+www.gov.uk
+wikia.com
+givemesport.com
+amazon.com
+bing.com
+wordpress.com
+telegraph.co.uk
+rightmove.co.uk
+pinterest.com
+gumtree.com
+msn.com
+microsoft.com
+stackoverflow.com
+booking.com
+vk.com
+tripadvisor.co.uk
+lloydsbank.co.uk
+apple.com
+service.gov.uk
+onclkds.com
+github.com
+independent.co.uk
+bt.com
+vice.com
+hsbc.co.uk \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/global b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/global
new file mode 100644
index 0000000000..e60734e30a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/global
@@ -0,0 +1,444 @@
+google.com
+facebook.com
+amazon.com
+youtube.com
+yahoo.com
+ebay.com
+wikipedia.org
+twitter.com
+reddit.com
+go.com
+craigslist.org
+live.com
+netflix.com
+pinterest.com
+bing.com
+linkedin.com
+imgur.com
+espn.go.com
+walmart.com
+tumblr.com
+target.com
+paypal.com
+cnn.com
+chase.com
+instagram.com
+bestbuy.com
+blogspot.com
+nytimes.com
+msn.com
+imdb.com
+apple.com
+bankofamerica.com
+diply.com
+huffingtonpost.com
+yelp.com
+wellsfargo.com
+etsy.com
+weather.com
+wordpress.com
+buzzfeed.com
+zillow.com
+kohls.com
+aol.com
+homedepot.com
+foxnews.com
+microsoft.com
+comcast.net
+wikia.com
+groupon.com
+macys.com
+washingtonpost.com
+outbrain.com
+xfinity.com
+usps.com
+hulu.com
+americanexpress.com
+slickdeals.net
+pandora.com
+office.com
+cnet.com
+indeed.com
+capitalone.com
+nfl.com
+ups.com
+ask.com
+verizonwireless.com
+newegg.com
+usatoday.com
+forbes.com
+dailymail.co.uk
+dropbox.com
+att.com
+costco.com
+gfycat.com
+lowes.com
+gap.com
+about.com
+tripadvisor.com
+fedex.com
+baidu.com
+vice.com
+nordstrom.com
+adobe.com
+bbc.com
+twitch.tv
+allrecipes.com
+retailmenot.com
+stackoverflow.com
+citi.com
+sears.com
+jcpenney.com
+webmd.com
+nih.gov
+answers.com
+foodnetwork.com
+discovercard.com
+cbssports.com
+overstock.com
+businessinsider.com
+office365.com
+theguardian.com
+staples.com
+bleacherreport.com
+verizon.com
+github.com
+wayfair.com
+salesforce.com
+zulily.com
+wsj.com
+flickr.com
+goodreads.com
+realtor.com
+nbcnews.com
+ebates.com
+ancestry.com
+wunderground.com
+instructure.com
+people.com
+stackexchange.com
+drudgereport.com
+fidelity.com
+southwest.com
+deviantart.com
+thesaurus.com
+intuit.com
+woot.com
+pch.com
+soundcloud.com
+force.com
+samsclub.com
+ign.com
+qvc.com
+npr.org
+patch.com
+dell.com
+accuweather.com
+vimeo.com
+expedia.com
+trulia.com
+ca.gov
+swagbucks.com
+spotify.com
+bedbathandbeyond.com
+nypost.com
+aliexpress.com
+blackboard.com
+ticketmaster.com
+ikea.com
+feedly.com
+usaa.com
+tmz.com
+quora.com
+lifehacker.com
+kayak.com
+reference.com
+zappos.com
+gizmodo.com
+slate.com
+faithtap.com
+adp.com
+abcnews.go.com
+sephora.com
+cbs.com
+latimes.com
+shutterfly.com
+t-mobile.com
+littlethings.com
+glassdoor.com
+bloomberg.com
+cbsnews.com
+wikihow.com
+walgreens.com
+usbank.com
+blogger.com
+weebly.com
+gamestop.com
+food.com
+time.com
+kickstarter.com
+okcupid.com
+aa.com
+weather.gov
+nametests.com
+fandango.com
+engadget.com
+steamcommunity.com
+thekitchn.com
+nba.com
+mashable.com
+hp.com
+gamefaqs.com
+delta.com
+coupons.com
+eonline.com
+surveymonkey.com
+kmart.com
+barnesandnoble.com
+meetup.com
+bhphotovideo.com
+fanduel.com
+quizlet.com
+nydailynews.com
+sbnation.com
+nbcsports.com
+bbc.co.uk
+ew.com
+nike.com
+rottentomatoes.com
+steampowered.com
+reuters.com
+qq.com
+today.com
+mapquest.com
+audible.com
+priceline.com
+whitepages.com
+united.com
+myfitnesspal.com
+icloud.com
+forever21.com
+theatlantic.com
+microsoftstore.com
+theverge.com
+gawker.com
+houzz.com
+mayoclinic.org
+rei.com
+sfgate.com
+lifebuzz.com
+discover.com
+pnc.com
+pof.com
+iflscience.com
+popsugar.com
+creditkarma.com
+telegraph.co.uk
+airbnb.com
+buzzlie.com
+cnbc.com
+deadspin.com
+sina.com.cn
+legacy.com
+thedailybeast.com
+samsung.com
+nextdoor.com
+evite.com
+shopify.com
+yellowpages.com
+pcmag.com
+redfin.com
+weibo.com
+alibaba.com
+cabelas.com
+battle.net
+foxsports.com
+taobao.com
+eventbrite.com
+victoriassecret.com
+theblaze.com
+dealnews.com
+cbslocal.com
+cvs.com
+dailymotion.com
+ecollege.com
+gofundme.com
+fitbit.com
+instructables.com
+godaddy.com
+babycenter.com
+squarespace.com
+llbean.com
+dickssportinggoods.com
+6pm.com
+myway.com
+hsn.com
+wired.com
+officedepot.com
+ozztube.com
+usmagazine.com
+match.com
+cracked.com
+evernote.com
+box.com
+starbucks.com
+kbb.com
+mlb.com
+marriott.com
+si.com
+jezebel.com
+pbs.org
+consumerreports.org
+roblox.com
+urbandictionary.com
+kotaku.com
+xbox.com
+marketwatch.com
+refinery29.com
+wikimedia.org
+tvguide.com
+politico.com
+barclaycardus.com
+abc.go.com
+mint.com
+topix.com
+theblackfriday.com
+aarp.org
+hotnewhiphop.com
+yourdailydish.com
+sprint.com
+vox.com
+cafemom.com
+nbc.com
+dailykos.com
+azlyrics.com
+autotrader.com
+hilton.com
+irs.gov
+monster.com
+mailchimp.com
+webex.com
+landsend.com
+wix.com
+usnews.com
+jcrew.com
+jet.com
+capitalone360.com
+sharepoint.com
+schwab.com
+ulta.com
+vistaprint.com
+rollingstone.com
+biblegateway.com
+gamespot.com
+io9.com
+opentable.com
+hm.com
+duckduckgo.com
+chron.com
+photobucket.com
+shareasale.com
+directv.com
+avg.com
+oracle.com
+hotels.com
+timewarnercable.com
+chicagotribune.com
+ehow.com
+primewire.ag
+abs-cbnnews.com
+salon.com
+greatergood.com
+epicurious.com
+fool.com
+patheos.com
+custhelp.com
+purdue.edu
+tickld.com
+frys.com
+indiatimes.com
+amazon.co.uk
+zendesk.com
+tigerdirect.com
+stubhub.com
+healthcare.gov
+archive.org
+qualtrics.com
+ravelry.com
+cars.com
+redbox.com
+jalopnik.com
+speedtest.net
+harvard.edu
+slideshare.net
+kinja.com
+nesn.com
+michaels.com
+mit.edu
+bodybuilding.com
+edmunds.com
+nhl.com
+zergnet.com
+techcrunch.com
+pogo.com
+mozilla.org
+naver.com
+giphy.com
+bankrate.com
+msnbc.com
+digitaltrends.com
+fanfiction.net
+skype.com
+disney.go.com
+norton.com
+androidcentral.com
+tomshardware.com
+thefreedictionary.com
+liveleak.com
+247sports.com
+merriam-webster.com
+wnd.com
+earthlink.net
+independent.co.uk
+drugs.com
+rotoworld.com
+nationalgeographic.com
+ae.com
+noaa.gov
+arstechnica.com
+thinkgeek.com
+stanford.edu
+bizjournals.com
+hootsuite.com
+genius.com
+goodhousekeeping.com
+vanguard.com
+ny.gov
+citibankonline.com
+booking.com
+mic.com
+orbitz.com
+dominos.com
+medium.com
+wow.com
+urbanoutfitters.com
+douban.com
+timeanddate.com
+draftkings.com
+livestrong.com
+livingsocial.com
+cox.net
+theonion.com
+marthastewart.com
+comenity.net
+worldlifestyle.com
+disney.com
+realsimple.com
+vrbo.com
+playstation.com
+potterybarn.com
+zazzle.com
+ksl.com
+tdbank.com
+sourceforge.net
+careerbuilder.com
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/hk b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/hk
new file mode 100644
index 0000000000..73513d54d0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/hk
@@ -0,0 +1,50 @@
+google.com.hk
+youtube.com
+google.com
+facebook.com
+yahoo.com
+discuss.com.hk
+aastocks.com
+wikipedia.org
+baidu.com
+taobao.com
+pixnet.net
+bastillepost.com
+nextmedia.com
+whatsapp.com
+instagram.com
+price.com.hk
+ettoday.net
+qq.com
+hsbc.com.hk
+tmall.com
+live.com
+hkgolden.com
+reddit.com
+beautyexchange.com.hk
+etnet.com.hk
+on.cc
+amazon.com
+twitter.com
+uwants.com
+presslogic.com
+unwire.hk
+gamer.com.tw
+hangseng.com
+hk01.com
+twitch.tv
+linkedin.com
+teepr.com
+hkjc.com
+apple.com
+bomb01.com
+sina.com.cn
+weibo.com
+dcfever.com
+thestandnews.com
+office.com
+openrice.com
+tumblr.com
+tvb.com
+alipay.com
+stackoverflow.com \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/id b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/id
new file mode 100644
index 0000000000..6d59c97022
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/id
@@ -0,0 +1,50 @@
+google.com
+google.co.id
+youtube.com
+detik.com
+tribunnews.com
+facebook.com
+yahoo.com
+tokopedia.com
+liputan6.com
+kompas.com
+bukalapak.com
+kaskus.co.id
+kapanlagi.com
+wordpress.com
+merdeka.com
+okezone.com
+elevenia.co.id
+lazada.co.id
+uzone.id
+bintang.com
+brilio.net
+popads.net
+instagram.com
+bola.net
+wikipedia.org
+blogspot.com
+onclkds.com
+dream.co.id
+viva.co.id
+alodokter.com
+tempo.co
+suara.com
+wowkeren.com
+idntimes.com
+bola.com
+sindonews.com
+republika.co.id
+kompasiana.com
+vemale.com
+blanja.com
+cnnindonesia.com
+olx.co.id
+lk21.org
+popcash.net
+blibli.com
+poptm.com
+nonton.movie
+indexmovie.me
+adexchangeprediction.com
+subscene.com \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/pl b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/pl
new file mode 100644
index 0000000000..bef0c8cf34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/pl
@@ -0,0 +1,50 @@
+google.pl
+youtube.com
+facebook.com
+google.com
+allegro.pl
+onet.pl
+wp.pl
+wikipedia.org
+olx.pl
+vk.com
+interia.pl
+wykop.pl
+gazeta.pl
+filmweb.pl
+instagram.com
+wiocha.pl
+cda.pl
+aliexpress.com
+otomoto.pl
+mbank.pl
+reddit.com
+ceneo.pl
+tvn24.pl
+twitter.com
+gumtree.pl
+blogspot.com
+kwejk.pl
+wyborcza.pl
+joemonster.org
+stackoverflow.com
+twitch.tv
+o2.pl
+ipko.pl
+steamcommunity.com
+github.com
+chomikuj.pl
+centrum24.pl
+linkedin.com
+money.pl
+librus.pl
+demotywatory.pl
+sport.pl
+microsoft.com
+zalukaj.com
+wikia.com
+jbzdy.pl
+imgur.com
+flashscore.pl
+gry-online.pl
+pudelek.pl \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/ru b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/ru
new file mode 100644
index 0000000000..26eefe027d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/ru
@@ -0,0 +1,50 @@
+vk.com
+google.ru
+yandex.ru
+youtube.com
+mail.ru
+ok.ru
+google.com
+avito.ru
+aliexpress.com
+wikipedia.org
+instagram.com
+sberbank.ru
+gismeteo.ru
+rambler.ru
+kinogo.club
+kinopoisk.ru
+drom.ru
+facebook.com
+pikabu.ru
+drive2.ru
+rutracker.org
+twitch.tv
+rbc.ru
+hh.ru
+gosuslugi.ru
+lenta.ru
+pochta.ru
+wildberries.ru
+wikia.com
+4pda.ru
+fb.ru
+seasonvar.ru
+kp.ru
+znanija.com
+ucoz.ru
+narod.ru
+mts.ru
+infourok.ru
+ebay.com
+ozon.ru
+worldoftanks.ru
+mos.ru
+vesti.ru
+nnmclub.to
+microsoft.com
+rp5.ru
+2gis.ru
+consultant.ru
+fotostrana.ru
+dnevnik.ru \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/sg b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/sg
new file mode 100644
index 0000000000..470a1840c8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/sg
@@ -0,0 +1,49 @@
+google.com.sg
+youtube.com
+google.com
+facebook.com
+yahoo.com
+wikipedia.org
+reddit.com
+blogspot.sg
+live.com
+instagram.com
+qoo10.sg
+whatsapp.com
+linkedin.com
+dbs.com.sg
+amazon.com
+twitter.com
+wordpress.com
+onclkds.com
+office.com
+allsingaporestuff.com
+baidu.com
+lazada.sg
+straitstimes.com
+singpass.gov.sg
+google.co.id
+taobao.com
+tumblr.com
+gomovies.to
+wikia.com
+hardwarezone.com.sg
+nus.edu.sg
+msn.com
+microsoft.com
+carousell.com
+kissanime.ru
+ocbc.com
+stackoverflow.com
+ntu.edu.sg
+thepiratebay.org
+aliexpress.com
+imgur.com
+dropbox.com
+apple.com
+channelnewsasia.com
+imdb.com
+twitch.tv
+abs-cbn.com
+jobstreet.com.sg
+uob.com.sg \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/tw b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/tw
new file mode 100644
index 0000000000..5651311cac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/tw
@@ -0,0 +1,50 @@
+google.com.tw
+pixnet.net
+youtube.com
+facebook.com
+ettoday.net
+google.com
+yahoo.com
+ltn.com.tw
+nownews.com
+setn.com
+momoshop.com.tw
+wikipedia.org
+ck101.com
+ptt.cc
+tvbs.com.tw
+104.com.tw
+gamer.com.tw
+appledaily.com.tw
+pchome.com.tw
+ruten.com.tw
+ctitv.com.tw
+teepr.com
+life.tw
+blogspot.tw
+dcard.tw
+baidu.com
+udn.com
+mobile01.com
+eyny.com
+epochtimes.com
+qoolquiz.com
+bomb01.com
+talk.tw
+ipetgroup.com
+storm.mg
+123kubo.com
+cmoney.tw
+taobao.com
+twitch.tv
+instagram.com
+xuite.net
+sina.com.tw
+1111.com.tw
+businessweekly.com.tw
+elle.com.tw
+twitter.com
+books.com.tw
+591.com.tw
+everydayhealth.com.tw
+techbang.com \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/assets/domains/us b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/us
new file mode 100644
index 0000000000..ea63155b7c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/assets/domains/us
@@ -0,0 +1,49 @@
+google.com
+youtube.com
+facebook.com
+reddit.com
+amazon.com
+wikipedia.org
+yahoo.com
+twitter.com
+netflix.com
+ebay.com
+imgur.com
+linkedin.com
+instagram.com
+diply.com
+craigslist.org
+live.com
+office.com
+twitch.tv
+tumblr.com
+pinterest.com
+espn.com
+cnn.com
+bing.com
+wikia.com
+chase.com
+imdb.com
+nytimes.com
+paypal.com
+blogspot.com
+apple.com
+yelp.com
+stackoverflow.com
+bankofamerica.com
+wordpress.com
+github.com
+microsoft.com
+wellsfargo.com
+zillow.com
+salesforce.com
+msn.com
+walmart.com
+weather.com
+dropbox.com
+buzzfeed.com
+intuit.com
+washingtonpost.com
+soundcloud.com
+huffingtonpost.com
+indeed.com \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/CustomDomains.kt b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/CustomDomains.kt
new file mode 100644
index 0000000000..a93ace8871
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/CustomDomains.kt
@@ -0,0 +1,68 @@
+/* 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.browser.domains
+
+import android.content.Context
+import android.content.SharedPreferences
+
+/**
+ * Contains functionality to manage custom domains for auto-completion.
+ */
+object CustomDomains {
+ private const val PREFERENCE_NAME = "custom_autocomplete"
+ private const val KEY_DOMAINS = "custom_domains"
+ private const val SEPARATOR = "@<;>@"
+
+ /**
+ * Loads the previously added/saved custom domains from preferences.
+ *
+ * @param context the application context
+ * @return list of custom domains
+ */
+ fun load(context: Context): List<String> =
+ preferences(context).getString(KEY_DOMAINS, "")!!
+ .split(SEPARATOR)
+ .filter { !it.isEmpty() }
+
+ /**
+ * Saves the provided domains to preferences.
+ *
+ * @param context the application context
+ * @param domains list of domains
+ */
+ fun save(context: Context, domains: List<String>) {
+ preferences(context)
+ .edit()
+ .putString(KEY_DOMAINS, domains.joinToString(separator = SEPARATOR))
+ .apply()
+ }
+
+ /**
+ * Adds the provided domain to preferences.
+ *
+ * @param context the application context
+ * @param domain the domain to add
+ */
+ fun add(context: Context, domain: String) {
+ val domains = mutableListOf<String>()
+ domains.addAll(load(context))
+ domains.add(domain)
+
+ save(context, domains)
+ }
+
+ /**
+ * Removes the provided domain from preferences.
+ *
+ * @param context the application context
+ * @param domains the domain to remove
+ */
+ fun remove(context: Context, domains: List<String>) {
+ save(context, load(context) - domains)
+ }
+
+ private fun preferences(context: Context): SharedPreferences =
+ context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domain.kt b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domain.kt
new file mode 100644
index 0000000000..4d570328e4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domain.kt
@@ -0,0 +1,39 @@
+/* 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.browser.domains
+
+/**
+ * Class intended for internal use which encapsulates meta data about a domain.
+ */
+data class Domain(val protocol: String, val hasWww: Boolean, val host: String) {
+ internal val url: String
+ get() = "$protocol${if (hasWww) "www." else "" }$host"
+
+ companion object {
+ private const val PROTOCOL_INDEX = 1
+ private const val WWW_INDEX = 2
+ private const val HOST_INDEX = 3
+
+ private const val DEFAULT_PROTOCOL = "http://"
+
+ private val urlMatcher = Regex("""(https?://)?(www.)?(.+)?""")
+
+ fun create(url: String): Domain {
+ val result = urlMatcher.find(url)
+
+ return result?.let {
+ val protocol = it.groups[PROTOCOL_INDEX]?.value ?: DEFAULT_PROTOCOL
+ val hasWww = it.groups[WWW_INDEX]?.value == "www."
+ val host = it.groups[HOST_INDEX]?.value ?: throw IllegalStateException()
+
+ return Domain(protocol, hasWww, host)
+ } ?: throw IllegalStateException()
+ }
+ }
+}
+
+internal fun Iterable<String>.into(): List<Domain> {
+ return this.map { Domain.create(it) }
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/DomainAutoCompleteProvider.kt b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/DomainAutoCompleteProvider.kt
new file mode 100644
index 0000000000..1e66582660
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/DomainAutoCompleteProvider.kt
@@ -0,0 +1,151 @@
+/* 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.browser.domains
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import java.util.Locale
+
+/**
+ * Provides autocomplete functionality for domains, based on a provided list
+ * of assets (see [Domains]) and/or a custom domain list managed by
+ * [CustomDomains].
+ */
+// FIXME delete this https://github.com/mozilla-mobile/android-components/issues/1358
+@Deprecated(
+ "Use `ShippedDomainsProvider` or `CustomDomainsProvider`",
+ ReplaceWith(
+ "ShippedDomainsProvider()/CustomDomainsProvider()",
+ "mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider",
+ "mozilla.components.browser.domains.autocomplete.CustomDomainsProvider",
+ ),
+)
+class DomainAutoCompleteProvider {
+
+ object AutocompleteSource {
+ const val DEFAULT_LIST = "default"
+ const val CUSTOM_LIST = "custom"
+ }
+
+ /**
+ * Represents a result of auto-completion.
+ *
+ * @property text the result text starting with the raw search text as passed
+ * to [autocomplete] followed by the completion text (e.g. moz => mozilla.org)
+ * @property url the complete url (containing the protocol) as provided
+ * when the domain was saved. (e.g. https://mozilla.org)
+ * @property source the source identifier of the autocomplete source
+ * @property size total number of available autocomplete domains
+ * in this source
+ */
+ data class Result(val text: String, val url: String, val source: String, val size: Int)
+
+ // We compute these on worker threads; make sure results are immediately visible on the UI thread.
+ @Volatile
+ internal var customDomains = emptyList<Domain>()
+
+ @Volatile
+ internal var shippedDomains = emptyList<Domain>()
+ private var useCustomDomains = false
+ private var useShippedDomains = true
+
+ /**
+ * Computes an autocomplete suggestion for the given text, and invokes the
+ * provided callback, passing the result.
+ *
+ * @param rawText text to be auto completed
+ * @return the result of auto-completion. If no match is found an empty
+ * result object is returned.
+ */
+ @Suppress("ReturnCount")
+ fun autocomplete(rawText: String): Result {
+ if (useCustomDomains) {
+ val result = tryToAutocomplete(rawText, customDomains, AutocompleteSource.CUSTOM_LIST)
+ if (result != null) {
+ return result
+ }
+ }
+
+ if (useShippedDomains) {
+ val result = tryToAutocomplete(rawText, shippedDomains, AutocompleteSource.DEFAULT_LIST)
+ if (result != null) {
+ return result
+ }
+ }
+
+ return Result("", "", "", 0)
+ }
+
+ /**
+ * Initializes this provider instance by making sure the shipped and/or custom
+ * domains are loaded.
+ *
+ * @param context the application context
+ * @param useShippedDomains true (default) if the domains provided by this
+ * module should be used, otherwise false.
+ * @param useCustomDomains true if the custom domains provided by
+ * [CustomDomains] should be used, otherwise false (default).
+ * @param loadDomainsFromDisk true (default) if domains should be loaded,
+ * otherwise false. This parameter is for testing purposes only.
+ */
+ fun initialize(
+ context: Context,
+ useShippedDomains: Boolean = true,
+ useCustomDomains: Boolean = false,
+ loadDomainsFromDisk: Boolean = true,
+ ) {
+ this.useCustomDomains = useCustomDomains
+ this.useShippedDomains = useShippedDomains
+
+ if (!loadDomainsFromDisk) {
+ return
+ }
+
+ if (!useCustomDomains && !useShippedDomains) {
+ return
+ }
+
+ CoroutineScope(Dispatchers.IO).launch {
+ if (useCustomDomains) {
+ customDomains = async { CustomDomains.load(context).into() }.await()
+ }
+ if (useShippedDomains) {
+ shippedDomains = async { Domains.load(context).into() }.await()
+ }
+ }
+ }
+
+ @Suppress("ReturnCount")
+ private fun tryToAutocomplete(rawText: String, domains: List<Domain>, source: String): Result? {
+ // Search terms are all lowercase already, we just need to lowercase the search text
+ val searchText = rawText.lowercase(Locale.US)
+
+ domains.forEach {
+ val wwwDomain = "www.${it.host}"
+ if (wwwDomain.startsWith(searchText)) {
+ return Result(getResultText(rawText, wwwDomain), it.url, source, domains.size)
+ }
+
+ if (it.host.startsWith(searchText)) {
+ return Result(getResultText(rawText, it.host), it.url, source, domains.size)
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Our autocomplete list is all lower case, however the search text might
+ * be mixed case. Our autocomplete EditText code does more string comparison,
+ * which fails if the suggestion doesn't exactly match searchText (ie.
+ * if casing differs). It's simplest to just build a suggestion
+ * that exactly matches the search text - which is what this method is for:
+ */
+ private fun getResultText(rawSearchText: String, autocomplete: String) =
+ rawSearchText + autocomplete.substring(rawSearchText.length)
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domains.kt b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domains.kt
new file mode 100644
index 0000000000..2d2bf89925
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domains.kt
@@ -0,0 +1,84 @@
+/* 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.browser.domains
+
+import android.content.Context
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES
+import android.os.LocaleList
+import android.text.TextUtils
+import java.io.IOException
+import java.util.Locale
+
+/**
+ * Contains functionality to access domain lists shipped as part of this
+ * module's assets.
+ */
+object Domains {
+
+ /**
+ * Loads the domains applicable to the app's locale, plus the domains
+ * in the 'global' list.
+ *
+ * @param context the application context
+ * @return list of domains
+ */
+ fun load(context: Context): List<String> {
+ return load(context, getCountriesInDefaultLocaleList())
+ }
+
+ internal fun load(context: Context, countries: Set<String>): List<String> {
+ val domains = LinkedHashSet<String>()
+ val availableLists = getAvailableDomainLists(context)
+
+ // First initialize the country specific lists following the default locale order
+ countries
+ .filter { availableLists.contains(it) }
+ .forEach { loadDomainsForLanguage(context, domains, it) }
+
+ // And then add domains from the global list
+ loadDomainsForLanguage(context, domains, "global")
+
+ return domains.toList()
+ }
+
+ private fun getAvailableDomainLists(context: Context): Set<String> {
+ val availableDomains = LinkedHashSet<String>()
+ val assetManager = context.assets
+ val domains = try {
+ assetManager.list("domains") ?: emptyArray<String>()
+ } catch (e: IOException) {
+ emptyArray<String>()
+ }
+ availableDomains.addAll(domains)
+ return availableDomains
+ }
+
+ private fun loadDomainsForLanguage(context: Context, domains: MutableSet<String>, country: String) {
+ val assetManager = context.assets
+ val languageDomains = try {
+ assetManager.open("domains/$country").bufferedReader().readLines()
+ } catch (e: IOException) {
+ emptyList<String>()
+ }
+ domains.addAll(languageDomains)
+ }
+
+ private fun getCountriesInDefaultLocaleList(): Set<String> {
+ val countries = java.util.LinkedHashSet<String>()
+ val addIfNotEmpty = { c: String -> if (!TextUtils.isEmpty(c)) countries.add(c.lowercase(Locale.US)) }
+
+ if (SDK_INT >= VERSION_CODES.N) {
+ val list = LocaleList.getDefault()
+ for (i in 0 until list.size()) {
+ addIfNotEmpty(list.get(i).country)
+ }
+ } else {
+ addIfNotEmpty(Locale.getDefault().country)
+ }
+
+ return countries
+ }
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/autocomplete/Providers.kt b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/autocomplete/Providers.kt
new file mode 100644
index 0000000000..fc4f63d08a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/autocomplete/Providers.kt
@@ -0,0 +1,110 @@
+/* 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.browser.domains.autocomplete
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import mozilla.components.browser.domains.CustomDomains
+import mozilla.components.browser.domains.Domain
+import mozilla.components.browser.domains.Domains
+import mozilla.components.browser.domains.into
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.concept.toolbar.AutocompleteResult
+import java.util.Locale
+
+enum class DomainList(val listName: String) {
+ DEFAULT("default"),
+ CUSTOM("custom"),
+}
+
+/**
+ * Provides autocomplete functionality for domains based on provided list of assets (see [Domains]).
+ */
+class ShippedDomainsProvider(override val autocompletePriority: Int = 0) :
+ BaseDomainAutocompleteProvider(DomainList.DEFAULT, Domains.asLoader())
+
+/**
+ * Provides autocomplete functionality for domains based on a list managed by [CustomDomains].
+ */
+class CustomDomainsProvider(override val autocompletePriority: Int = 0) :
+ BaseDomainAutocompleteProvider(DomainList.CUSTOM, CustomDomains.asLoader())
+
+typealias DomainsLoader = (Context) -> List<Domain>
+
+private fun Domains.asLoader(): DomainsLoader = { context: Context -> load(context).into() }
+private fun CustomDomains.asLoader(): DomainsLoader = { context: Context -> load(context).into() }
+
+/**
+ * Provides common autocomplete functionality powered by domain lists.
+ *
+ * @param list source of domains
+ * @param domainsLoader provider for all available domains
+ */
+open class BaseDomainAutocompleteProvider(
+ private val list: DomainList,
+ private val domainsLoader: DomainsLoader,
+ override val autocompletePriority: Int = 0,
+) : AutocompleteProvider, CoroutineScope by CoroutineScope(Dispatchers.IO) {
+
+ // We compute 'domains' on the worker thread; make sure it's immediately visible on the UI thread.
+ @Volatile
+ var domains: List<Domain> = emptyList()
+
+ fun initialize(context: Context) {
+ launch {
+ domains = async { domainsLoader(context) }.await()
+ }
+ }
+
+ /**
+ * Computes an autocomplete suggestion for the given text, and invokes the
+ * provided callback, passing the result.
+ *
+ * @param query text to be auto completed
+ * @return the result of auto-completion, or null if no match is found.
+ */
+ override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
+ // Search terms are all lowercase already, we just need to lowercase the search text
+ val searchText = query.lowercase(Locale.US)
+
+ domains.forEach {
+ val wwwDomain = "www.${it.host}"
+ if (wwwDomain.startsWith(searchText)) {
+ return AutocompleteResult(
+ input = searchText,
+ text = getResultText(query, wwwDomain),
+ url = it.url,
+ source = list.listName,
+ totalItems = domains.size,
+ )
+ }
+
+ if (it.host.startsWith(searchText)) {
+ return AutocompleteResult(
+ input = searchText,
+ text = getResultText(query, it.host),
+ url = it.url,
+ source = list.listName,
+ totalItems = domains.size,
+ )
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Our autocomplete list is all lower case, however the search text might
+ * be mixed case. Our autocomplete EditText code does more string comparison,
+ * which fails if the suggestion doesn't exactly match searchText (ie.
+ * if casing differs). It's simplest to just build a suggestion
+ * that exactly matches the search text - which is what this method is for:
+ */
+ private fun getResultText(rawSearchText: String, autocomplete: String) =
+ rawSearchText + autocomplete.substring(rawSearchText.length)
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/BaseDomainAutocompleteProviderTest.kt b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/BaseDomainAutocompleteProviderTest.kt
new file mode 100644
index 0000000000..841047684c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/BaseDomainAutocompleteProviderTest.kt
@@ -0,0 +1,140 @@
+/* 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.browser.domains
+
+import android.content.Context
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
+import mozilla.components.browser.domains.autocomplete.DomainList
+import mozilla.components.browser.domains.autocomplete.DomainsLoader
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class BaseDomainAutocompleteProviderTest {
+
+ @Test
+ fun `empty provider with DEFAULT list returns nothing`() {
+ val provider = createAndInitProvider(testContext, DomainList.DEFAULT) {
+ emptyList()
+ }
+
+ assertNoCompletion(provider, "m")
+ assertNoCompletion(provider, "mo")
+ assertNoCompletion(provider, "moz")
+ assertNoCompletion(provider, "g")
+ assertNoCompletion(provider, "go")
+ assertNoCompletion(provider, "goo")
+ assertNoCompletion(provider, "w")
+ assertNoCompletion(provider, "www")
+ }
+
+ @Test
+ fun `empty provider with CUSTOM list returns nothing`() {
+ val provider = createAndInitProvider(testContext, DomainList.CUSTOM) {
+ emptyList()
+ }
+
+ assertNoCompletion(provider, "m")
+ assertNoCompletion(provider, "mo")
+ assertNoCompletion(provider, "moz")
+ assertNoCompletion(provider, "g")
+ assertNoCompletion(provider, "go")
+ assertNoCompletion(provider, "goo")
+ assertNoCompletion(provider, "w")
+ assertNoCompletion(provider, "www")
+ }
+
+ @Test
+ fun `non-empty provider with DEFAULT list returns completion`() {
+ val domains = listOf("mozilla.org", "google.com", "facebook.com").into()
+ val list = DomainList.DEFAULT
+ val domainsCount = domains.size
+
+ val provider = createAndInitProvider(testContext, list) { domains }
+ shadowOf(getMainLooper()).idle()
+
+ assertCompletion(provider, list, domainsCount, "m", "m", "mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "moz", "moz", "mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "www", "www", "www.mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "www.face", "www.face", "www.facebook.com", "http://facebook.com")
+ assertCompletion(provider, list, domainsCount, "M", "m", "Mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "MOZ", "moz", "MOZilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "www.GOO", "www.goo", "www.GOOgle.com", "http://google.com")
+ assertCompletion(provider, list, domainsCount, "WWW.GOOGLE.", "www.google.", "WWW.GOOGLE.com", "http://google.com")
+ assertCompletion(provider, list, domainsCount, "www.facebook.com", "www.facebook.com", "www.facebook.com", "http://facebook.com")
+ assertCompletion(provider, list, domainsCount, "facebook.com", "facebook.com", "facebook.com", "http://facebook.com")
+
+ assertNoCompletion(provider, "wwww")
+ assertNoCompletion(provider, "yahoo")
+ }
+
+ @Test
+ fun `non-empty provider with CUSTOM list returns completion`() {
+ val domains = listOf("mozilla.org", "google.com", "facebook.com").into()
+ val list = DomainList.CUSTOM
+ val domainsCount = domains.size
+
+ val provider = createAndInitProvider(testContext, list) { domains }
+ shadowOf(getMainLooper()).idle()
+
+ assertCompletion(provider, list, domainsCount, "m", "m", "mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "moz", "moz", "mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "www", "www", "www.mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "www.face", "www.face", "www.facebook.com", "http://facebook.com")
+ assertCompletion(provider, list, domainsCount, "M", "m", "Mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "MOZ", "moz", "MOZilla.org", "http://mozilla.org")
+ assertCompletion(provider, list, domainsCount, "www.GOO", "www.goo", "www.GOOgle.com", "http://google.com")
+ assertCompletion(provider, list, domainsCount, "WWW.GOOGLE.", "www.google.", "WWW.GOOGLE.com", "http://google.com")
+ assertCompletion(provider, list, domainsCount, "www.facebook.com", "www.facebook.com", "www.facebook.com", "http://facebook.com")
+ assertCompletion(provider, list, domainsCount, "facebook.com", "facebook.com", "facebook.com", "http://facebook.com")
+
+ assertNoCompletion(provider, "wwww")
+ assertNoCompletion(provider, "yahoo")
+ }
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+private fun assertCompletion(
+ provider: AutocompleteProvider,
+ domainSource: DomainList,
+ sourceSize: Int,
+ input: String,
+ expectedInput: String,
+ completion: String,
+ expectedUrl: String,
+) = runTest {
+ val result = provider.getAutocompleteSuggestion(input)!!
+
+ assertTrue("Autocompletion shouldn't be empty", result.text.isNotEmpty())
+
+ assertEquals("Autocompletion input", expectedInput, result.input)
+ assertEquals("Autocompletion completion", completion, result.text)
+ assertEquals("Autocompletion source list", domainSource.listName, result.source)
+ assertEquals("Autocompletion url", expectedUrl, result.url)
+ assertEquals("Autocompletion source list size", sourceSize, result.totalItems)
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+private fun assertNoCompletion(provider: AutocompleteProvider, input: String) = runTest {
+ val result = provider.getAutocompleteSuggestion(input)
+
+ assertNull("Result should be null", result)
+}
+
+private fun createAndInitProvider(context: Context, list: DomainList, loader: DomainsLoader): AutocompleteProvider =
+ object : BaseDomainAutocompleteProvider(list, loader) {
+ override val coroutineContext = super.coroutineContext + Dispatchers.Main
+ }.apply { initialize(context) }
diff --git a/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt
new file mode 100644
index 0000000000..488506b4ae
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt
@@ -0,0 +1,81 @@
+/* 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.browser.domains
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CustomDomainsTest {
+
+ @Before
+ fun setUp() {
+ testContext.getSharedPreferences("custom_autocomplete", Context.MODE_PRIVATE)
+ .edit()
+ .clear()
+ .apply()
+ }
+
+ @ExperimentalCoroutinesApi
+ @Test
+ fun customListIsEmptyByDefault() {
+ val domains = CustomDomains.load(testContext)
+
+ assertEquals(0, domains.size)
+ }
+
+ @Test
+ fun saveAndRemoveDomains() {
+ CustomDomains.save(
+ testContext,
+ listOf(
+ "mozilla.org",
+ "example.org",
+ "example.com",
+ ),
+ )
+
+ var domains = CustomDomains.load(testContext)
+ assertEquals(3, domains.size)
+
+ CustomDomains.remove(testContext, listOf("example.org", "example.com"))
+ domains = CustomDomains.load(testContext)
+ assertEquals(1, domains.size)
+ assertEquals("mozilla.org", domains.elementAt(0))
+ }
+
+ @Test
+ fun addAndLoadDomains() {
+ CustomDomains.add(testContext, "mozilla.org")
+ val domains = CustomDomains.load(testContext)
+ assertEquals(1, domains.size)
+ assertEquals("mozilla.org", domains.elementAt(0))
+ }
+
+ @Test
+ fun saveAndLoadDomains() {
+ CustomDomains.save(
+ testContext,
+ listOf(
+ "mozilla.org",
+ "example.org",
+ "example.com",
+ ),
+ )
+
+ val domains = CustomDomains.load(testContext)
+
+ assertEquals(3, domains.size)
+ assertEquals("mozilla.org", domains.elementAt(0))
+ assertEquals("example.org", domains.elementAt(1))
+ assertEquals("example.com", domains.elementAt(2))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainAutoCompleteProviderTest.kt b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainAutoCompleteProviderTest.kt
new file mode 100644
index 0000000000..f7c05bdb8e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainAutoCompleteProviderTest.kt
@@ -0,0 +1,116 @@
+/* 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/. */
+
+@file:Suppress("DEPRECATION")
+
+package mozilla.components.browser.domains
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.domains.DomainAutoCompleteProvider.AutocompleteSource.CUSTOM_LIST
+import mozilla.components.browser.domains.DomainAutoCompleteProvider.AutocompleteSource.DEFAULT_LIST
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * While [DomainAutoCompleteProvider] exists (even if it's deprecated) we need to test it.
+ */
+@RunWith(AndroidJUnit4::class)
+class DomainAutoCompleteProviderTest {
+
+ @Test
+ fun autocompletionWithShippedDomains() {
+ val provider = DomainAutoCompleteProvider().also {
+ it.initialize(
+ testContext,
+ useShippedDomains = true,
+ useCustomDomains = false,
+ loadDomainsFromDisk = false,
+ )
+
+ it.shippedDomains = listOf("mozilla.org", "google.com", "facebook.com").into()
+ it.customDomains = emptyList()
+ }
+
+ val size = provider.shippedDomains.size
+
+ assertCompletion(provider, "m", DEFAULT_LIST, size, "mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, "www", DEFAULT_LIST, size, "www.mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, "www.face", DEFAULT_LIST, size, "www.facebook.com", "http://facebook.com")
+ assertCompletion(provider, "MOZ", DEFAULT_LIST, size, "MOZilla.org", "http://mozilla.org")
+ assertCompletion(provider, "www.GOO", DEFAULT_LIST, size, "www.GOOgle.com", "http://google.com")
+ assertCompletion(provider, "WWW.GOOGLE.", DEFAULT_LIST, size, "WWW.GOOGLE.com", "http://google.com")
+ assertCompletion(provider, "www.facebook.com", DEFAULT_LIST, size, "www.facebook.com", "http://facebook.com")
+ assertCompletion(provider, "facebook.com", DEFAULT_LIST, size, "facebook.com", "http://facebook.com")
+
+ assertNoCompletion(provider, "wwww")
+ assertNoCompletion(provider, "yahoo")
+ }
+
+ @Test
+ fun autocompletionWithCustomDomains() {
+ val domains = listOf("facebook.com", "google.com", "mozilla.org")
+ val customDomains = listOf("gap.com", "www.fanfiction.com", "https://mobile.de")
+
+ val provider = DomainAutoCompleteProvider().also {
+ it.initialize(
+ testContext,
+ useShippedDomains = true,
+ useCustomDomains = true,
+ loadDomainsFromDisk = false,
+ )
+ it.shippedDomains = domains.into()
+ it.customDomains = customDomains.into()
+ }
+
+ assertCompletion(provider, "f", CUSTOM_LIST, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
+ assertCompletion(provider, "fa", CUSTOM_LIST, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
+ assertCompletion(provider, "fac", DEFAULT_LIST, domains.size, "facebook.com", "http://facebook.com")
+
+ assertCompletion(provider, "g", CUSTOM_LIST, customDomains.size, "gap.com", "http://gap.com")
+ assertCompletion(provider, "go", DEFAULT_LIST, domains.size, "google.com", "http://google.com")
+ assertCompletion(provider, "ga", CUSTOM_LIST, customDomains.size, "gap.com", "http://gap.com")
+
+ assertCompletion(provider, "m", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
+ assertCompletion(provider, "mo", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
+ assertCompletion(provider, "mob", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
+ assertCompletion(provider, "moz", DEFAULT_LIST, domains.size, "mozilla.org", "http://mozilla.org")
+ }
+
+ @Test
+ fun autocompletionWithoutDomains() {
+ val filter = DomainAutoCompleteProvider()
+ assertNoCompletion(filter, "mozilla")
+ }
+
+ private fun assertCompletion(
+ provider: DomainAutoCompleteProvider,
+ text: String,
+ domainSource: String,
+ sourceSize: Int,
+ completion: String,
+ expectedUrl: String,
+ ) {
+ val result = provider.autocomplete(text)
+
+ assertFalse(result.text.isEmpty())
+
+ assertEquals(completion, result.text)
+ assertEquals(domainSource, result.source)
+ assertEquals(expectedUrl, result.url)
+ assertEquals(sourceSize, result.size)
+ }
+
+ private fun assertNoCompletion(provider: DomainAutoCompleteProvider, text: String) {
+ val result = provider.autocomplete(text)
+
+ assertTrue(result.text.isEmpty())
+ assertTrue(result.url.isEmpty())
+ assertTrue(result.source.isEmpty())
+ assertEquals(0, result.size)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainTest.kt b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainTest.kt
new file mode 100644
index 0000000000..c1af392552
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainTest.kt
@@ -0,0 +1,39 @@
+/* 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.browser.domains
+
+import org.junit.Assert
+import org.junit.Test
+
+class DomainTest {
+ @Test
+ fun domainCreation() {
+ val firstItem = Domain.create("https://mozilla.com")
+
+ Assert.assertTrue(firstItem.protocol == "https://")
+ Assert.assertFalse(firstItem.hasWww)
+ Assert.assertTrue(firstItem.host == "mozilla.com")
+
+ val secondItem = Domain.create("www.mozilla.com")
+
+ Assert.assertTrue(secondItem.protocol == "http://")
+ Assert.assertTrue(secondItem.hasWww)
+ Assert.assertTrue(secondItem.host == "mozilla.com")
+ }
+
+ @Test
+ fun domainCanCreateUrl() {
+ val firstItem = Domain.create("https://mozilla.com")
+ Assert.assertEquals("https://mozilla.com", firstItem.url)
+
+ val secondItem = Domain.create("www.mozilla.com")
+ Assert.assertEquals("http://www.mozilla.com", secondItem.url)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun domainCreationWithBadURLThrowsException() {
+ Domain.create("http://www.")
+ }
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainsTest.kt b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainsTest.kt
new file mode 100644
index 0000000000..13782446d5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainsTest.kt
@@ -0,0 +1,30 @@
+/* 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.browser.domains
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DomainsTest {
+
+ @Test
+ fun loadDomains() {
+ val domains = Domains.load(testContext, setOf("us"))
+ Assert.assertFalse(domains.isEmpty())
+ Assert.assertTrue(domains.contains("reddit.com"))
+ }
+
+ @Test
+ fun loadDomainsWithDefaultCountries() {
+ val domains = Domains.load(testContext)
+ Assert.assertFalse(domains.isEmpty())
+ // From the global list
+ Assert.assertTrue(domains.contains("mozilla.org"))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/ProvidersTest.kt b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/ProvidersTest.kt
new file mode 100644
index 0000000000..7be689e67f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/ProvidersTest.kt
@@ -0,0 +1,85 @@
+/* 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.browser.domains
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
+import mozilla.components.browser.domains.autocomplete.CustomDomainsProvider
+import mozilla.components.browser.domains.autocomplete.DomainList
+import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class ProvidersTest {
+ @Test
+ fun autocompletionWithShippedDomains() {
+ val provider = ShippedDomainsProvider()
+ provider.domains = listOf("mozilla.org", "google.com", "facebook.com").into()
+
+ val size = provider.domains.size
+
+ assertCompletion(provider, "m", DomainList.DEFAULT, size, "mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, "www", DomainList.DEFAULT, size, "www.mozilla.org", "http://mozilla.org")
+ assertCompletion(provider, "www.face", DomainList.DEFAULT, size, "www.facebook.com", "http://facebook.com")
+ assertCompletion(provider, "MOZ", DomainList.DEFAULT, size, "MOZilla.org", "http://mozilla.org")
+ assertCompletion(provider, "www.GOO", DomainList.DEFAULT, size, "www.GOOgle.com", "http://google.com")
+ assertCompletion(provider, "WWW.GOOGLE.", DomainList.DEFAULT, size, "WWW.GOOGLE.com", "http://google.com")
+ assertCompletion(provider, "www.facebook.com", DomainList.DEFAULT, size, "www.facebook.com", "http://facebook.com")
+ assertCompletion(provider, "facebook.com", DomainList.DEFAULT, size, "facebook.com", "http://facebook.com")
+
+ assertNoCompletion(provider, "wwww")
+ assertNoCompletion(provider, "yahoo")
+ }
+
+ @Test
+ fun autocompletionWithCustomDomains() {
+ val customDomains = listOf("gap.com", "www.fanfiction.com", "https://mobile.de")
+
+ val provider = CustomDomainsProvider()
+ provider.domains = customDomains.into()
+
+ assertCompletion(provider, "f", DomainList.CUSTOM, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
+ assertCompletion(provider, "fa", DomainList.CUSTOM, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
+
+ assertCompletion(provider, "g", DomainList.CUSTOM, customDomains.size, "gap.com", "http://gap.com")
+ assertCompletion(provider, "ga", DomainList.CUSTOM, customDomains.size, "gap.com", "http://gap.com")
+
+ assertCompletion(provider, "m", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
+ assertCompletion(provider, "mo", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
+ assertCompletion(provider, "mob", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
+ }
+
+ @Test
+ fun autocompletionWithoutDomains() {
+ val filter = CustomDomainsProvider()
+ assertNoCompletion(filter, "mozilla")
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private fun assertCompletion(
+ provider: BaseDomainAutocompleteProvider,
+ text: String,
+ domainSource: DomainList,
+ sourceSize: Int,
+ completion: String,
+ expectedUrl: String,
+ ) = runTest {
+ val result = provider.getAutocompleteSuggestion(text)!!
+ assertFalse(result.text.isEmpty())
+
+ assertEquals(completion, result.text)
+ assertEquals(domainSource.listName, result.source)
+ assertEquals(expectedUrl, result.url)
+ assertEquals(sourceSize, result.totalItems)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private fun assertNoCompletion(provider: BaseDomainAutocompleteProvider, text: String) = runTest {
+ assertNull(provider.getAutocompleteSuggestion(text))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/domains/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/domains/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/domains/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/browser/engine-gecko/README.md b/mobile/android/android-components/components/browser/engine-gecko/README.md
new file mode 100644
index 0000000000..2071f57f5e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/README.md
@@ -0,0 +1,41 @@
+# [Android Components](../../../README.md) > Browser > Engine-Gecko
+
+[*Engine*](../../concept/engine/README.md) implementation based on [GeckoView](https://wiki.mozilla.org/Mobile/GeckoView).
+
+## Usage
+
+### 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:browser-engine-gecko:{latest-version}"
+```
+
+### Integration with the Glean SDK
+
+#### Before using this component
+Products sending telemetry and using this component *must request* a data-review following [this process](https://wiki.mozilla.org/Firefox/Data_Collection).
+
+The [Glean SDK](../../../components/service/glean/README.md) can be used to collect [Gecko Telemetry](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/index.html).
+Applications using both this component and the Glean SDK should setup the Gecko Telemetry delegate
+as shown below:
+
+```Kotlin
+ val builder = GeckoRuntimeSettings.Builder()
+ val runtimeSettings = builder
+ .telemetryDelegate(GeckoGleanAdapter()) // Sets up the delegate!
+ .build()
+ // Create the Gecko runtime.
+ GeckoRuntime.create(context, runtimeSettings)
+```
+
+#### Adding new metrics
+
+New Gecko metrics can be added as described [in the Firefox Telemetry docs](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/start/adding-a-new-probe.html).
+
+## 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/browser/engine-gecko/build.gradle b/mobile/android/android-components/components/browser/engine-gecko/build.gradle
new file mode 100644
index 0000000000..4e5c2fda65
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/build.gradle
@@ -0,0 +1,193 @@
+/* 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/. */
+
+buildscript {
+ repositories {
+ gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
+ maven {
+ url repository
+ if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
+ allowInsecureProtocol = true
+ }
+ }
+ }
+ }
+
+ dependencies {
+ classpath "${ApplicationServicesConfig.groupId}:tooling-nimbus-gradle:${ApplicationServicesConfig.version}"
+ classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}"
+ }
+}
+
+plugins {
+ id "com.jetbrains.python.envs" version "$python_envs_plugin"
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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']
+ }
+ }
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ namespace 'mozilla.components.browser.engine.gecko'
+}
+
+// Set configuration for the Glean parser to extract metrics.yaml
+// file from AAR dependencies of this project rather than look
+// for it into the project directory.
+ext.allowMetricsFromAAR = true
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':concept-fetch')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+ implementation(project(':service-nimbus')) {
+ exclude group: 'org.mozilla.telemetry', module: 'glean-native'
+ }
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ if (findProject(":geckoview") != null) {
+ api project(':geckoview')
+ } else {
+ api getGeckoViewDependency()
+ }
+
+ implementation ComponentsDependencies.androidx_paging
+ implementation ComponentsDependencies.androidx_data_store_preferences
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation project(':support-test')
+ testImplementation project(':tooling-fetch-tests')
+
+ // We only compile against Glean. It's up to the app to add those dependencies
+ // if it wants to collect GeckoView telemetry through the Glean SDK.
+ compileOnly project(":service-glean")
+ testImplementation project(":service-glean")
+ testImplementation ComponentsDependencies.androidx_work_testing
+
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation project(':tooling-fetch-tests')
+}
+
+apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
+nimbus {
+ // The path to the Nimbus feature manifest file
+ manifestFile = "geckoview.fml.yaml"
+
+ channels = [
+ debug: "debug",
+ release: "release",
+ ]
+
+ // This is an optional value, and updates the plugin to use a copy of application
+ // services. The path should be relative to the root project directory.
+ // *NOTE*: This example will not work for all projects, but should work for Fenix, Focus, and Android Components
+ applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir')
+ ? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null
+}
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
+
+// Non-official versions are like "61.0a1", where "a1" is the milestone.
+// This simply strips that off, leaving "61.0" in this example.
+def getAppVersionWithoutMilestone() {
+ return gradle.mozconfig.substs.MOZ_APP_VERSION.replaceFirst(/a[0-9]/, "")
+}
+
+// Mimic Python: open(os.path.join(buildconfig.topobjdir, 'buildid.h')).readline().split()[2]
+def getBuildId() {
+ if (System.env.MOZ_BUILD_DATE) {
+ if (System.env.MOZ_BUILD_DATE.length() == 14) {
+ return System.env.MOZ_BUILD_DATE
+ }
+ logger.warn("Ignoring invalid MOZ_BUILD_DATE: ${System.env.MOZ_BUILD_DATE}")
+ }
+ return file("${gradle.mozconfig.topobjdir}/buildid.h").getText('utf-8').split()[2]
+}
+
+def getVersionNumber() {
+ def appVersion = getAppVersionWithoutMilestone()
+ def parts = appVersion.split('\\.')
+ def version = parts[0] + "." + parts[1] + "." + getBuildId()
+
+ if (!gradle.mozconfig.substs.MOZILLA_OFFICIAL && !gradle.mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
+ // Use -SNAPSHOT versions locally to enable the local GeckoView substitution flow.
+ version += "-SNAPSHOT"
+ }
+
+ return version
+}
+
+def getArtifactSuffix() {
+ def suffix = ""
+
+ // Release artifacts don't specify the channel, for the sake of simplicity.
+ if (gradle.mozconfig.substs.MOZ_UPDATE_CHANNEL != 'release') {
+ suffix += "-${gradle.mozconfig.substs.MOZ_UPDATE_CHANNEL}"
+ }
+
+ return suffix
+}
+
+def getArtifactId() {
+ def id = "geckoview" + getArtifactSuffix()
+
+ if (!gradle.mozconfig.substs.MOZ_ANDROID_GECKOVIEW_LITE) {
+ id += "-omni"
+ }
+
+ if (gradle.mozconfig.substs.MOZILLA_OFFICIAL && !gradle.mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
+ // In automation, per-architecture artifacts identify
+ // the architecture; multi-architecture artifacts don't.
+ // When building locally, we produce a "skinny AAR" with
+ // one target architecture masquerading as a "fat AAR"
+ // to enable Gradle composite builds to substitute this
+ // project into consumers easily.
+ id += "-${gradle.mozconfig.substs.ANDROID_CPU_ARCH}"
+ }
+
+ return id
+}
+
+def getGeckoViewDependency() {
+ // on try, relax geckoview version pin to allow for --use-existing-task
+ if ('https://hg.mozilla.org/try' == System.env.GECKO_HEAD_REPOSITORY) {
+ rootProject.logger.lifecycle("Getting geckoview on try: ${getArtifactId()}:+")
+ return "org.mozilla.geckoview:${getArtifactId()}:+"
+ }
+ rootProject.logger.lifecycle("Getting geckoview: ${getArtifactId()}:${getVersionNumber()}")
+ return "org.mozilla.geckoview:${getArtifactId()}:${getVersionNumber()}"
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/docs/metrics.md b/mobile/android/android-components/components/browser/engine-gecko/docs/metrics.md
new file mode 100644
index 0000000000..203d71a2b3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/docs/metrics.md
@@ -0,0 +1,8 @@
+# Metrics definitions have moved
+
+Metrics definitions for projects using `engine-gecko` moved to the Glean Dictionary.
+
+For Firefox for Android those definitions can be found at:
+[https://dictionary.telemetry.mozilla.org/apps/fenix](https://dictionary.telemetry.mozilla.org/apps/fenix)
+
+This file is kept only for historical reference.
diff --git a/mobile/android/android-components/components/browser/engine-gecko/geckoview.fml.yaml b/mobile/android/android-components/components/browser/engine-gecko/geckoview.fml.yaml
new file mode 100644
index 0000000000..bf6ccecd88
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/geckoview.fml.yaml
@@ -0,0 +1,24 @@
+# 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/.
+about:
+ description: GeckoView features configurable via Nimbus
+ android:
+ package: mozilla.components.browser.engine.gecko
+ class: .GeckoNimbus
+channels:
+ - debug
+ - release
+features:
+ pdfjs:
+ description: "PDF.js features"
+ variables:
+ download-button:
+ description: "Download button"
+ type: Boolean
+ default: true
+
+ open-in-app-button:
+ description: "Open in app button"
+ type: Boolean
+ default: true
diff --git a/mobile/android/android-components/components/browser/engine-gecko/metrics.yaml b/mobile/android/android-components/components/browser/engine-gecko/metrics.yaml
new file mode 100644
index 0000000000..4775ce544d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/metrics.yaml
@@ -0,0 +1,30 @@
+# 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/.
+
+# IMPORTANT NOTE: this file is here only as a safety measure, to make
+# sure the correct code is generated even though the GeckoView AAR file
+# reports an empty metrics.yaml file. The metric in this file is currently
+# disabled and not supposed to collect any data.
+---
+
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+
+test.glean.geckoview:
+ streaming:
+ type: timing_distribution
+ gecko_datapoint: TELEMETRY_TEST_STREAMING
+ disabled: true
+ description: |
+ A test-only, disabled metric. This is required to guarantee
+ that a `GleanGeckoHistogramMapping` is always generated, even
+ though the GeckoView AAR exports no metric. Please note that
+ the data-review field below contains no review, since this
+ metric is disabled and not allowed to collect any data.
+ bugs:
+ - https://bugzilla.mozilla.org/1566374
+ data_reviews:
+ - https://bugzilla.mozilla.org/1566374
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
diff --git a/mobile/android/android-components/components/browser/engine-gecko/proguard-rules.pro b/mobile/android/android-components/components/browser/engine-gecko/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/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/browser/engine-gecko/src/androidTest/java/mozilla/components/browser/engine/gecko/fetch/geckoview/GeckoViewFetchTestCases.kt b/mobile/android/android-components/components/browser/engine-gecko/src/androidTest/java/mozilla/components/browser/engine/gecko/fetch/geckoview/GeckoViewFetchTestCases.kt
new file mode 100644
index 0000000000..38e0a1586f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/androidTest/java/mozilla/components/browser/engine/gecko/fetch/geckoview/GeckoViewFetchTestCases.kt
@@ -0,0 +1,126 @@
+/* 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.browser.engine.gecko.fetch.geckoview
+
+import androidx.test.annotation.UiThreadTest
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.MediumTest
+import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
+import mozilla.components.concept.fetch.Client
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@MediumTest
+class GeckoViewFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTestCases() {
+ override fun createNewClient(): Client = GeckoViewFetchClient(ApplicationProvider.getApplicationContext())
+
+ @Test
+ @UiThreadTest
+ fun clientInstance() {
+ assertTrue(createNewClient() is GeckoViewFetchClient)
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithGzippedBody() {
+ super.get200WithGzippedBody()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200OverridingDefaultHeaders() {
+ super.get200OverridingDefaultHeaders()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithDuplicatedCacheControlRequestHeaders() {
+ super.get200WithDuplicatedCacheControlRequestHeaders()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithDuplicatedCacheControlResponseHeaders() {
+ super.get200WithDuplicatedCacheControlResponseHeaders()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithHeaders() {
+ super.get200WithHeaders()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithReadTimeout() {
+ super.get200WithReadTimeout()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithStringBody() {
+ super.get200WithStringBody()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get302FollowRedirects() {
+ super.get302FollowRedirects()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get302FollowRedirectsDisabled() {
+ super.get302FollowRedirectsDisabled()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get404WithBody() {
+ super.get404WithBody()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun post200WithBody() {
+ super.post200WithBody()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun put201FileUpload() {
+ super.put201FileUpload()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithCookiePolicy() {
+ super.get200WithCookiePolicy()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithContentTypeCharset() {
+ super.get200WithContentTypeCharset()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun get200WithCacheControl() {
+ super.get200WithCacheControl()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun getThrowsIOExceptionWhenHostNotReachable() {
+ super.getThrowsIOExceptionWhenHostNotReachable()
+ }
+
+ @Test
+ @UiThreadTest
+ override fun getDataUri() {
+ super.getDataUri()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/engine-gecko/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/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/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt
new file mode 100644
index 0000000000..92e6074a61
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt
@@ -0,0 +1,1502 @@
+/* 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.browser.engine.gecko
+
+import android.content.Context
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.util.JsonReader
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.engine.gecko.activity.GeckoActivityDelegate
+import mozilla.components.browser.engine.gecko.activity.GeckoScreenOrientationDelegate
+import mozilla.components.browser.engine.gecko.ext.getAntiTrackingPolicy
+import mozilla.components.browser.engine.gecko.ext.getEtpLevel
+import mozilla.components.browser.engine.gecko.ext.getStrictSocialTrackingProtection
+import mozilla.components.browser.engine.gecko.integration.LocaleSettingUpdater
+import mozilla.components.browser.engine.gecko.mediaquery.from
+import mozilla.components.browser.engine.gecko.mediaquery.toGeckoValue
+import mozilla.components.browser.engine.gecko.profiler.Profiler
+import mozilla.components.browser.engine.gecko.serviceworker.GeckoServiceWorkerDelegate
+import mozilla.components.browser.engine.gecko.translate.GeckoTranslationUtils.intoTranslationError
+import mozilla.components.browser.engine.gecko.util.SpeculativeSessionFactory
+import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtensionException
+import mozilla.components.browser.engine.gecko.webnotifications.GeckoWebNotificationDelegate
+import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushDelegate
+import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushHandler
+import mozilla.components.concept.engine.CancellableOperation
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode
+import mozilla.components.concept.engine.EngineSession.SafeBrowsingPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.activity.ActivityDelegate
+import mozilla.components.concept.engine.activity.OrientationDelegate
+import mozilla.components.concept.engine.content.blocking.TrackerLog
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
+import mozilla.components.concept.engine.translate.Language
+import mozilla.components.concept.engine.translate.LanguageModel
+import mozilla.components.concept.engine.translate.LanguageSetting
+import mozilla.components.concept.engine.translate.ModelManagementOptions
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationSupport
+import mozilla.components.concept.engine.translate.TranslationsRuntime
+import mozilla.components.concept.engine.utils.EngineVersion
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.engine.webextension.ActionHandler
+import mozilla.components.concept.engine.webextension.EnableSource
+import mozilla.components.concept.engine.webextension.InstallationMethod
+import mozilla.components.concept.engine.webextension.TabHandler
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionDelegate
+import mozilla.components.concept.engine.webextension.WebExtensionInstallException
+import mozilla.components.concept.engine.webextension.WebExtensionRuntime
+import mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
+import mozilla.components.concept.engine.webpush.WebPushDelegate
+import mozilla.components.concept.engine.webpush.WebPushHandler
+import mozilla.components.support.ktx.kotlin.isResourceUrl
+import mozilla.components.support.utils.ThreadUtils
+import org.json.JSONObject
+import org.mozilla.geckoview.AllowOrDeny
+import org.mozilla.geckoview.ContentBlocking
+import org.mozilla.geckoview.ContentBlockingController
+import org.mozilla.geckoview.ContentBlockingController.Event
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoRuntimeSettings
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoWebExecutor
+import org.mozilla.geckoview.TranslationsController
+import org.mozilla.geckoview.WebExtensionController
+import org.mozilla.geckoview.WebNotification
+import java.lang.ref.WeakReference
+
+/**
+ * Gecko-based implementation of Engine interface.
+ */
+@Suppress("LargeClass", "TooManyFunctions")
+class GeckoEngine(
+ context: Context,
+ private val defaultSettings: Settings? = null,
+ private val runtime: GeckoRuntime = GeckoRuntime.getDefault(context),
+ executorProvider: () -> GeckoWebExecutor = { GeckoWebExecutor(runtime) },
+ override val trackingProtectionExceptionStore: TrackingProtectionExceptionStorage =
+ GeckoTrackingProtectionExceptionStorage(runtime),
+) : Engine, WebExtensionRuntime, TranslationsRuntime {
+ private val executor by lazy { executorProvider.invoke() }
+ private val localeUpdater = LocaleSettingUpdater(context, runtime)
+
+ @VisibleForTesting internal val speculativeConnectionFactory = SpeculativeSessionFactory()
+ private var webExtensionDelegate: WebExtensionDelegate? = null
+ private val webExtensionActionHandler = object : ActionHandler {
+ override fun onBrowserAction(extension: WebExtension, session: EngineSession?, action: Action) {
+ webExtensionDelegate?.onBrowserActionDefined(extension, action)
+ }
+
+ override fun onPageAction(extension: WebExtension, session: EngineSession?, action: Action) {
+ webExtensionDelegate?.onPageActionDefined(extension, action)
+ }
+
+ override fun onToggleActionPopup(extension: WebExtension, action: Action): EngineSession? {
+ return webExtensionDelegate?.onToggleActionPopup(
+ extension,
+ GeckoEngineSession(
+ runtime,
+ defaultSettings = defaultSettings,
+ ),
+ action,
+ )
+ }
+ }
+ private val webExtensionTabHandler = object : TabHandler {
+ override fun onNewTab(webExtension: WebExtension, engineSession: EngineSession, active: Boolean, url: String) {
+ webExtensionDelegate?.onNewTab(webExtension, engineSession, active, url)
+ }
+ }
+
+ private var webPushHandler: WebPushHandler? = null
+
+ init {
+ runtime.delegate = GeckoRuntime.Delegate {
+ // On shutdown: The runtime is shutting down (possibly because of an unrecoverable error state). We crash
+ // the app here for two reasons:
+ // - We want to know about those unsolicited shutdowns and fix those issues.
+ // - We can't recover easily from this situation. Just continuing will leave us with an engine that
+ // doesn't do anything anymore.
+ @Suppress("TooGenericExceptionThrown")
+ throw RuntimeException("GeckoRuntime is shutting down")
+ }
+ }
+
+ /**
+ * Fetch a list of trackers logged for a given [session] .
+ *
+ * @param session the session where the trackers were logged.
+ * @param onSuccess callback invoked if the data was fetched successfully.
+ * @param onError (optional) callback invoked if fetching the data caused an exception.
+ */
+ override fun getTrackersLog(
+ session: EngineSession,
+ onSuccess: (List<TrackerLog>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ val geckoSession = (session as GeckoEngineSession).geckoSession
+ runtime.contentBlockingController.getLog(geckoSession).then(
+ { contentLogList ->
+ val list = contentLogList ?: emptyList()
+ val logs = list.map { logEntry ->
+ logEntry.toTrackerLog()
+ }.filterNot {
+ !it.cookiesHasBeenBlocked &&
+ it.blockedCategories.isEmpty() &&
+ it.loadedCategories.isEmpty()
+ }
+
+ onSuccess(logs)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * Creates a new Gecko-based EngineView.
+ */
+ override fun createView(context: Context, attrs: AttributeSet?): EngineView {
+ return GeckoEngineView(context, attrs).apply {
+ setColorScheme(settings.preferredColorScheme)
+ }
+ }
+
+ /**
+ * See [Engine.createSession].
+ */
+ override fun createSession(private: Boolean, contextId: String?): EngineSession {
+ ThreadUtils.assertOnUiThread()
+ val speculativeSession = speculativeConnectionFactory.get(private, contextId)
+ return speculativeSession ?: GeckoEngineSession(runtime, private, defaultSettings, contextId)
+ }
+
+ /**
+ * See [Engine.createSessionState].
+ */
+ override fun createSessionState(json: JSONObject): EngineSessionState {
+ return GeckoEngineSessionState.fromJSON(json)
+ }
+
+ /**
+ * See [Engine.createSessionStateFrom].
+ */
+ override fun createSessionStateFrom(reader: JsonReader): EngineSessionState {
+ return GeckoEngineSessionState.from(reader)
+ }
+
+ /**
+ * See [Engine.speculativeCreateSession].
+ */
+ override fun speculativeCreateSession(private: Boolean, contextId: String?) {
+ ThreadUtils.assertOnUiThread()
+ speculativeConnectionFactory.create(runtime, private, contextId, defaultSettings)
+ }
+
+ /**
+ * See [Engine.clearSpeculativeSession].
+ */
+ override fun clearSpeculativeSession() {
+ speculativeConnectionFactory.clear()
+ }
+
+ /**
+ * Opens a speculative connection to the host of [url].
+ *
+ * This is useful if an app thinks it may be making a request to that host in the near future. If no request
+ * is made, the connection will be cleaned up after an unspecified.
+ */
+ override fun speculativeConnect(url: String) {
+ executor.speculativeConnect(url)
+ }
+
+ /**
+ * See [Engine.installBuiltInWebExtension].
+ */
+ override fun installBuiltInWebExtension(
+ id: String,
+ url: String,
+ onSuccess: ((WebExtension) -> Unit),
+ onError: ((Throwable) -> Unit),
+ ): CancellableOperation {
+ require(url.isResourceUrl()) { "url should be a resource url" }
+
+ val geckoResult = runtime.webExtensionController.ensureBuiltIn(url, id).apply {
+ then(
+ {
+ onExtensionInstalled(it!!, onSuccess)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(GeckoWebExtensionException.createWebExtensionException(throwable))
+ GeckoResult<Void>()
+ },
+ )
+ }
+ return geckoResult.asCancellableOperation()
+ }
+
+ /**
+ * See [Engine.installWebExtension].
+ */
+ override fun installWebExtension(
+ url: String,
+ installationMethod: InstallationMethod?,
+ onSuccess: ((WebExtension) -> Unit),
+ onError: ((Throwable) -> Unit),
+ ): CancellableOperation {
+ require(!url.isResourceUrl()) { "url shouldn't be a resource url" }
+
+ val geckoResult = runtime.webExtensionController.install(
+ url,
+ installationMethod?.toGeckoInstallationMethod(),
+ ).apply {
+ then(
+ {
+ onExtensionInstalled(it!!, onSuccess)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(GeckoWebExtensionException.createWebExtensionException(throwable))
+ GeckoResult<Void>()
+ },
+ )
+ }
+ return geckoResult.asCancellableOperation()
+ }
+
+ /**
+ * See [Engine.uninstallWebExtension].
+ */
+ override fun uninstallWebExtension(
+ ext: WebExtension,
+ onSuccess: () -> Unit,
+ onError: (String, Throwable) -> Unit,
+ ) {
+ runtime.webExtensionController.uninstall((ext as GeckoWebExtension).nativeExtension).then(
+ {
+ onSuccess()
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(ext.id, throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.updateWebExtension].
+ */
+ override fun updateWebExtension(
+ extension: WebExtension,
+ onSuccess: (WebExtension?) -> Unit,
+ onError: (String, Throwable) -> Unit,
+ ) {
+ runtime.webExtensionController.update((extension as GeckoWebExtension).nativeExtension).then(
+ { geckoExtension ->
+ val updatedExtension = if (geckoExtension != null) {
+ GeckoWebExtension(geckoExtension, runtime).also {
+ it.registerActionHandler(webExtensionActionHandler)
+ it.registerTabHandler(webExtensionTabHandler, defaultSettings)
+ }
+ } else {
+ null
+ }
+ onSuccess(updatedExtension)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(extension.id, GeckoWebExtensionException(throwable))
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.registerWebExtensionDelegate].
+ */
+ @Suppress("Deprecation")
+ override fun registerWebExtensionDelegate(
+ webExtensionDelegate: WebExtensionDelegate,
+ ) {
+ this.webExtensionDelegate = webExtensionDelegate
+
+ val promptDelegate = object : WebExtensionController.PromptDelegate {
+ override fun onInstallPrompt(ext: org.mozilla.geckoview.WebExtension): GeckoResult<AllowOrDeny> {
+ val extension = GeckoWebExtension(ext, runtime)
+ val result = GeckoResult<AllowOrDeny>()
+
+ webExtensionDelegate.onInstallPermissionRequest(extension) { allow ->
+ if (allow) result.complete(AllowOrDeny.ALLOW) else result.complete(AllowOrDeny.DENY)
+ }
+
+ return result
+ }
+
+ override fun onUpdatePrompt(
+ current: org.mozilla.geckoview.WebExtension,
+ updated: org.mozilla.geckoview.WebExtension,
+ newPermissions: Array<out String>,
+ newOrigins: Array<out String>,
+ ): GeckoResult<AllowOrDeny>? {
+ val result = GeckoResult<AllowOrDeny>()
+ webExtensionDelegate.onUpdatePermissionRequest(
+ GeckoWebExtension(current, runtime),
+ GeckoWebExtension(updated, runtime),
+ newPermissions.toList() + newOrigins.toList(),
+ ) { allow ->
+ if (allow) result.complete(AllowOrDeny.ALLOW) else result.complete(AllowOrDeny.DENY)
+ }
+ return result
+ }
+
+ override fun onOptionalPrompt(
+ extension: org.mozilla.geckoview.WebExtension,
+ permissions: Array<out String>,
+ origins: Array<out String>,
+ ): GeckoResult<AllowOrDeny>? {
+ val result = GeckoResult<AllowOrDeny>()
+ webExtensionDelegate.onOptionalPermissionsRequest(
+ GeckoWebExtension(extension, runtime),
+ permissions.toList() + origins.toList(),
+ ) { allow ->
+ if (allow) result.complete(AllowOrDeny.ALLOW) else result.complete(AllowOrDeny.DENY)
+ }
+ return result
+ }
+ }
+
+ val debuggerDelegate = object : WebExtensionController.DebuggerDelegate {
+ override fun onExtensionListUpdated() {
+ webExtensionDelegate.onExtensionListUpdated()
+ }
+ }
+
+ val addonManagerDelegate = object : WebExtensionController.AddonManagerDelegate {
+ override fun onDisabled(extension: org.mozilla.geckoview.WebExtension) {
+ webExtensionDelegate.onDisabled(GeckoWebExtension(extension, runtime))
+ }
+
+ override fun onEnabled(extension: org.mozilla.geckoview.WebExtension) {
+ webExtensionDelegate.onEnabled(GeckoWebExtension(extension, runtime))
+ }
+
+ override fun onReady(extension: org.mozilla.geckoview.WebExtension) {
+ webExtensionDelegate.onReady(GeckoWebExtension(extension, runtime))
+ }
+
+ override fun onUninstalled(extension: org.mozilla.geckoview.WebExtension) {
+ webExtensionDelegate.onUninstalled(GeckoWebExtension(extension, runtime))
+ }
+
+ override fun onInstalled(extension: org.mozilla.geckoview.WebExtension) {
+ val installedExtension = GeckoWebExtension(extension, runtime)
+ webExtensionDelegate.onInstalled(installedExtension)
+ installedExtension.registerActionHandler(webExtensionActionHandler)
+ installedExtension.registerTabHandler(webExtensionTabHandler, defaultSettings)
+ }
+
+ override fun onInstallationFailed(
+ extension: org.mozilla.geckoview.WebExtension?,
+ installException: org.mozilla.geckoview.WebExtension.InstallException,
+ ) {
+ val exception =
+ GeckoWebExtensionException.createWebExtensionException(installException)
+ webExtensionDelegate.onInstallationFailedRequest(
+ extension.toSafeWebExtension(),
+ exception as WebExtensionInstallException,
+ )
+ }
+ }
+
+ val extensionProcessDelegate = object : WebExtensionController.ExtensionProcessDelegate {
+ override fun onDisabledProcessSpawning() {
+ webExtensionDelegate.onDisabledExtensionProcessSpawning()
+ }
+ }
+
+ runtime.webExtensionController.setPromptDelegate(promptDelegate)
+ runtime.webExtensionController.setDebuggerDelegate(debuggerDelegate)
+ runtime.webExtensionController.setAddonManagerDelegate(addonManagerDelegate)
+ runtime.webExtensionController.setExtensionProcessDelegate(extensionProcessDelegate)
+ }
+
+ /**
+ * See [Engine.listInstalledWebExtensions].
+ */
+ override fun listInstalledWebExtensions(onSuccess: (List<WebExtension>) -> Unit, onError: (Throwable) -> Unit) {
+ runtime.webExtensionController.list().then(
+ {
+ val extensions = it?.map {
+ extension ->
+ GeckoWebExtension(extension, runtime)
+ } ?: emptyList()
+
+ extensions.forEach { extension ->
+ extension.registerActionHandler(webExtensionActionHandler)
+ extension.registerTabHandler(webExtensionTabHandler, defaultSettings)
+ }
+
+ onSuccess(extensions)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.enableWebExtension].
+ */
+ override fun enableWebExtension(
+ extension: WebExtension,
+ source: EnableSource,
+ onSuccess: (WebExtension) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ runtime.webExtensionController.enable((extension as GeckoWebExtension).nativeExtension, source.id).then(
+ {
+ val enabledExtension = GeckoWebExtension(it!!, runtime)
+ onSuccess(enabledExtension)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.addOptionalPermissions].
+ */
+ override fun addOptionalPermissions(
+ extensionId: String,
+ permissions: List<String>,
+ origins: List<String>,
+ onSuccess: (WebExtension) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ if (permissions.isEmpty() && origins.isEmpty()) {
+ onError(IllegalStateException("Either permissions or origins must not be empty"))
+ return
+ }
+
+ runtime.webExtensionController.addOptionalPermissions(
+ extensionId,
+ permissions.toTypedArray(),
+ origins.toTypedArray(),
+ ).then(
+ {
+ val enabledExtension = GeckoWebExtension(it!!, runtime)
+ onSuccess(enabledExtension)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.removeOptionalPermissions].
+ */
+ override fun removeOptionalPermissions(
+ extensionId: String,
+ permissions: List<String>,
+ origins: List<String>,
+ onSuccess: (WebExtension) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ if (permissions.isEmpty() && origins.isEmpty()) {
+ onError(IllegalStateException("Either permissions or origins must not be empty"))
+ return
+ }
+
+ runtime.webExtensionController.removeOptionalPermissions(
+ extensionId,
+ permissions.toTypedArray(),
+ origins.toTypedArray(),
+ ).then(
+ {
+ val enabledExtension = GeckoWebExtension(it!!, runtime)
+ onSuccess(enabledExtension)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.disableWebExtension].
+ */
+ override fun disableWebExtension(
+ extension: WebExtension,
+ source: EnableSource,
+ onSuccess: (WebExtension) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ runtime.webExtensionController.disable((extension as GeckoWebExtension).nativeExtension, source.id).then(
+ {
+ val disabledExtension = GeckoWebExtension(it!!, runtime)
+ onSuccess(disabledExtension)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.setAllowedInPrivateBrowsing].
+ */
+ override fun setAllowedInPrivateBrowsing(
+ extension: WebExtension,
+ allowed: Boolean,
+ onSuccess: (WebExtension) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ runtime.webExtensionController.setAllowedInPrivateBrowsing(
+ (extension as GeckoWebExtension).nativeExtension,
+ allowed,
+ ).then(
+ { geckoExtension ->
+ if (geckoExtension == null) {
+ onError(
+ Exception(
+ "Gecko extension was not returned after trying to" +
+ " setAllowedInPrivateBrowsing with value $allowed",
+ ),
+ )
+ } else {
+ val ext = GeckoWebExtension(geckoExtension, runtime)
+ webExtensionDelegate?.onAllowedInPrivateBrowsingChanged(ext)
+ onSuccess(ext)
+ }
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.enableExtensionProcessSpawning].
+ */
+ override fun enableExtensionProcessSpawning() {
+ runtime.webExtensionController.enableExtensionProcessSpawning()
+ }
+
+ /**
+ * See [Engine.disableExtensionProcessSpawning].
+ */
+ override fun disableExtensionProcessSpawning() {
+ runtime.webExtensionController.disableExtensionProcessSpawning()
+ }
+
+ /**
+ * See [Engine.registerWebNotificationDelegate].
+ */
+ override fun registerWebNotificationDelegate(
+ webNotificationDelegate: WebNotificationDelegate,
+ ) {
+ runtime.webNotificationDelegate = GeckoWebNotificationDelegate(webNotificationDelegate)
+ }
+
+ /**
+ * See [Engine.registerWebPushDelegate].
+ */
+ override fun registerWebPushDelegate(
+ webPushDelegate: WebPushDelegate,
+ ): WebPushHandler {
+ runtime.webPushController.setDelegate(GeckoWebPushDelegate(webPushDelegate))
+
+ if (webPushHandler == null) {
+ webPushHandler = GeckoWebPushHandler(runtime)
+ }
+
+ return requireNotNull(webPushHandler)
+ }
+
+ /**
+ * See [Engine.registerActivityDelegate].
+ */
+ override fun registerActivityDelegate(
+ activityDelegate: ActivityDelegate,
+ ) {
+ /**
+ * Having the activity delegate on the engine can cause issues with resolving multiple requests to the delegate
+ * from different sessions. Ideally, this should be moved to the [EngineView].
+ *
+ * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1672195
+ *
+ * Attaching the delegate to the Gecko [Engine] implicitly assumes we have WebAuthn support. When a feature
+ * implements the ActivityDelegate today, we need to make sure that it has full support for WebAuthn. This
+ * needs to be fixed in GeckoView.
+ *
+ * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1671988
+ */
+ runtime.activityDelegate = GeckoActivityDelegate(WeakReference(activityDelegate))
+ }
+
+ /**
+ * See [Engine.unregisterActivityDelegate].
+ */
+ override fun unregisterActivityDelegate() {
+ runtime.activityDelegate = null
+ }
+
+ /**
+ * See [Engine.registerScreenOrientationDelegate].
+ */
+ override fun registerScreenOrientationDelegate(
+ delegate: OrientationDelegate,
+ ) {
+ runtime.orientationController.delegate = GeckoScreenOrientationDelegate(delegate)
+ }
+
+ /**
+ * See [Engine.unregisterScreenOrientationDelegate].
+ */
+ override fun unregisterScreenOrientationDelegate() {
+ runtime.orientationController.delegate = null
+ }
+
+ override fun registerServiceWorkerDelegate(serviceWorkerDelegate: ServiceWorkerDelegate) {
+ runtime.serviceWorkerDelegate = GeckoServiceWorkerDelegate(
+ delegate = serviceWorkerDelegate,
+ runtime = runtime,
+ engineSettings = defaultSettings,
+ )
+ }
+
+ override fun unregisterServiceWorkerDelegate() {
+ runtime.serviceWorkerDelegate = null
+ }
+
+ override fun handleWebNotificationClick(webNotification: Parcelable) {
+ (webNotification as? WebNotification)?.click()
+ }
+
+ /**
+ * See [Engine.clearData].
+ */
+ override fun clearData(
+ data: Engine.BrowsingData,
+ host: String?,
+ onSuccess: () -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ val flags = data.types.toLong()
+ if (host != null) {
+ runtime.storageController.clearDataFromBaseDomain(host, flags)
+ } else {
+ runtime.storageController.clearData(flags)
+ }.then(
+ {
+ onSuccess()
+ GeckoResult<Void>()
+ },
+ {
+ throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.isTranslationsEngineSupported].
+ */
+ override fun isTranslationsEngineSupported(
+ onSuccess: (Boolean) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.isTranslationsEngineSupported().then(
+ {
+ if (it != null) {
+ onSuccess(it)
+ } else {
+ onError(TranslationError.UnexpectedNull())
+ }
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.getTranslationsPairDownloadSize].
+ */
+ override fun getTranslationsPairDownloadSize(
+ fromLanguage: String,
+ toLanguage: String,
+ onSuccess: (Long) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.checkPairDownloadSize(fromLanguage, toLanguage).then(
+ {
+ if (it != null) {
+ onSuccess(it)
+ } else {
+ onError(TranslationError.UnexpectedNull())
+ }
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.getTranslationsModelDownloadStates].
+ */
+ override fun getTranslationsModelDownloadStates(
+ onSuccess: (List<LanguageModel>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.listModelDownloadStates().then(
+ {
+ if (it != null) {
+ var listOfModels = mutableListOf<LanguageModel>()
+ for (each in it) {
+ var language = each.language?.let {
+ language ->
+ Language(language.code, each.language?.localizedDisplayName)
+ }
+ var model = LanguageModel(language, each.isDownloaded, each.size)
+ listOfModels.add(model)
+ }
+ onSuccess(listOfModels)
+ } else {
+ onError(TranslationError.UnexpectedNull())
+ }
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.getSupportedTranslationLanguages].
+ */
+ override fun getSupportedTranslationLanguages(
+ onSuccess: (TranslationSupport) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.listSupportedLanguages().then(
+ {
+ if (it != null) {
+ val listOfFromLanguages = mutableListOf<Language>()
+ val listOfToLanguages = mutableListOf<Language>()
+
+ if (it.fromLanguages != null) {
+ for (each in it.fromLanguages!!) {
+ listOfFromLanguages.add(Language(each.code, each.localizedDisplayName))
+ }
+ }
+
+ if (it.toLanguages != null) {
+ for (each in it.toLanguages!!) {
+ listOfToLanguages.add(Language(each.code, each.localizedDisplayName))
+ }
+ }
+
+ onSuccess(TranslationSupport(listOfFromLanguages, listOfToLanguages))
+ } else {
+ onError(TranslationError.UnexpectedNull())
+ }
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.manageTranslationsLanguageModel].
+ */
+ override fun manageTranslationsLanguageModel(
+ options: ModelManagementOptions,
+ onSuccess: () -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ val geckoOptions =
+ TranslationsController.RuntimeTranslation.ModelManagementOptions.Builder()
+ .operation(options.operation.toString())
+ .operationLevel(options.operationLevel.toString())
+
+ options.languageToManage?.let { geckoOptions.languageToManage(it) }
+
+ TranslationsController.RuntimeTranslation.manageLanguageModel(geckoOptions.build()).then(
+ {
+ onSuccess()
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.getUserPreferredLanguages].
+ */
+ override fun getUserPreferredLanguages(
+ onSuccess: (List<String>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.preferredLanguages().then(
+ {
+ if (it != null) {
+ onSuccess(it)
+ } else {
+ onError(TranslationError.UnexpectedNull())
+ }
+
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.getTranslationsOfferPopup].
+ */
+ override fun getTranslationsOfferPopup(): Boolean {
+ return runtime.settings.translationsOfferPopup
+ }
+
+ /**
+ * See [Engine.setTranslationsOfferPopup].
+ */
+ override fun setTranslationsOfferPopup(offer: Boolean) {
+ runtime.settings.translationsOfferPopup = offer
+ }
+
+ /**
+ * See [Engine.getLanguageSetting].
+ */
+ override fun getLanguageSetting(
+ languageCode: String,
+ onSuccess: (LanguageSetting) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.getLanguageSetting(languageCode).then(
+ {
+ if (it != null) {
+ try {
+ onSuccess(LanguageSetting.fromValue(it))
+ } catch (e: IllegalArgumentException) {
+ onError(e.intoTranslationError())
+ }
+ } else {
+ onError(TranslationError.UnexpectedNull())
+ }
+
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.setLanguageSetting].
+ */
+ override fun setLanguageSetting(
+ languageCode: String,
+ languageSetting: LanguageSetting,
+ onSuccess: () -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.setLanguageSettings(languageCode, languageSetting.toString()).then(
+ {
+ onSuccess()
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.getLanguageSettings].
+ */
+ override fun getLanguageSettings(
+ onSuccess: (Map<String, LanguageSetting>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.getLanguageSettings().then(
+ {
+ if (it != null) {
+ try {
+ val result = mutableMapOf<String, LanguageSetting>()
+ it.forEach { item ->
+ result[item.key] = LanguageSetting.fromValue(item.value)
+ }
+ onSuccess(result)
+ } catch (e: IllegalArgumentException) {
+ onError(e.intoTranslationError())
+ }
+ } else {
+ onError(TranslationError.UnexpectedNull())
+ }
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.getNeverTranslateSiteList].
+ */
+ override fun getNeverTranslateSiteList(
+ onSuccess: (List<String>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.getNeverTranslateSiteList().then(
+ {
+ if (it != null) {
+ try {
+ onSuccess(it)
+ } catch (e: IllegalArgumentException) {
+ onError(e.intoTranslationError())
+ }
+ } else {
+ onError(TranslationError.UnexpectedNull())
+ }
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.setNeverTranslateSpecifiedSite].
+ */
+ override fun setNeverTranslateSpecifiedSite(
+ origin: String,
+ setting: Boolean,
+ onSuccess: () -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ TranslationsController.RuntimeTranslation.setNeverTranslateSpecifiedSite(setting, origin).then(
+ {
+ onSuccess()
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable.intoTranslationError())
+ GeckoResult<Void>()
+ },
+ )
+ }
+
+ /**
+ * See [Engine.profiler].
+ */
+ override val profiler: Profiler? = Profiler(runtime)
+
+ override fun name(): String = "Gecko"
+
+ override val version: EngineVersion = EngineVersion.parse(
+ org.mozilla.geckoview.BuildConfig.MOZILLA_VERSION,
+ org.mozilla.geckoview.BuildConfig.MOZ_UPDATE_CHANNEL,
+ ) ?: throw IllegalStateException("Could not determine engine version")
+
+ /**
+ * See [Engine.settings]
+ */
+ override val settings: Settings = object : Settings() {
+ override var javascriptEnabled: Boolean
+ get() = runtime.settings.javaScriptEnabled
+ set(value) { runtime.settings.javaScriptEnabled = value }
+
+ override var webFontsEnabled: Boolean
+ get() = runtime.settings.webFontsEnabled
+ set(value) { runtime.settings.webFontsEnabled = value }
+
+ override var automaticFontSizeAdjustment: Boolean
+ get() = runtime.settings.automaticFontSizeAdjustment
+ set(value) { runtime.settings.automaticFontSizeAdjustment = value }
+
+ override var automaticLanguageAdjustment: Boolean
+ get() = localeUpdater.enabled
+ set(value) {
+ localeUpdater.enabled = value
+ defaultSettings?.automaticLanguageAdjustment = value
+ }
+
+ override var safeBrowsingPolicy: Array<SafeBrowsingPolicy> =
+ arrayOf(SafeBrowsingPolicy.RECOMMENDED)
+ set(value) {
+ val policy = value.sumOf { it.id }
+ runtime.settings.contentBlocking.setSafeBrowsing(policy)
+ field = value
+ }
+
+ override var trackingProtectionPolicy: TrackingProtectionPolicy? = null
+ set(value) {
+ value?.let { policy ->
+ with(runtime.settings.contentBlocking) {
+ if (enhancedTrackingProtectionLevel != value.getEtpLevel()) {
+ enhancedTrackingProtectionLevel = value.getEtpLevel()
+ }
+
+ if (strictSocialTrackingProtection != value.getStrictSocialTrackingProtection()) {
+ strictSocialTrackingProtection = policy.getStrictSocialTrackingProtection()
+ }
+
+ if (antiTrackingCategories != value.getAntiTrackingPolicy()) {
+ setAntiTracking(policy.getAntiTrackingPolicy())
+ }
+
+ if (cookieBehavior != value.cookiePolicy.id) {
+ cookieBehavior = value.cookiePolicy.id
+ }
+
+ if (cookieBehaviorPrivateMode != value.cookiePolicyPrivateMode.id) {
+ cookieBehaviorPrivateMode = value.cookiePolicyPrivateMode.id
+ }
+
+ if (cookiePurging != value.cookiePurging) {
+ setCookiePurging(value.cookiePurging)
+ }
+ }
+
+ defaultSettings?.trackingProtectionPolicy = value
+ field = value
+ }
+ }
+
+ override var cookieBannerHandlingMode: CookieBannerHandlingMode = CookieBannerHandlingMode.DISABLED
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.cookieBannerMode != value.mode) {
+ this.cookieBannerMode = value.mode
+ }
+ }
+ field = value
+ }
+
+ override var cookieBannerHandlingModePrivateBrowsing: CookieBannerHandlingMode =
+ CookieBannerHandlingMode.REJECT_ALL
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.cookieBannerModePrivateBrowsing != value.mode) {
+ this.cookieBannerModePrivateBrowsing = value.mode
+ }
+ }
+ field = value
+ }
+
+ override var emailTrackerBlockingPrivateBrowsing: Boolean = false
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.emailTrackerBlockingPrivateBrowsingEnabled != value) {
+ this.setEmailTrackerBlockingPrivateBrowsing(value)
+ }
+ }
+ field = value
+ }
+
+ override var cookieBannerHandlingDetectOnlyMode: Boolean = false
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.cookieBannerDetectOnlyMode != value) {
+ this.cookieBannerDetectOnlyMode = value
+ }
+ }
+ field = value
+ }
+
+ override var cookieBannerHandlingGlobalRules: Boolean = false
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.cookieBannerGlobalRulesEnabled != value) {
+ this.cookieBannerGlobalRulesEnabled = value
+ }
+ }
+ field = value
+ }
+
+ override var cookieBannerHandlingGlobalRulesSubFrames: Boolean = false
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.cookieBannerGlobalRulesSubFramesEnabled != value) {
+ this.cookieBannerGlobalRulesSubFramesEnabled = value
+ }
+ }
+ field = value
+ }
+
+ override var queryParameterStripping: Boolean = false
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.queryParameterStrippingEnabled != value) {
+ this.queryParameterStrippingEnabled = value
+ }
+ }
+ field = value
+ }
+
+ override var queryParameterStrippingPrivateBrowsing: Boolean = false
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.queryParameterStrippingPrivateBrowsingEnabled != value) {
+ this.queryParameterStrippingPrivateBrowsingEnabled = value
+ }
+ }
+ field = value
+ }
+
+ @Suppress("SpreadOperator")
+ override var queryParameterStrippingAllowList: String = ""
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.queryParameterStrippingAllowList.joinToString() != value) {
+ this.setQueryParameterStrippingAllowList(
+ *value.split(",")
+ .toTypedArray(),
+ )
+ }
+ }
+ field = value
+ }
+
+ @Suppress("SpreadOperator")
+ override var queryParameterStrippingStripList: String = ""
+ set(value) {
+ with(runtime.settings.contentBlocking) {
+ if (this.queryParameterStrippingStripList.joinToString() != value) {
+ this.setQueryParameterStrippingStripList(
+ *value.split(",").toTypedArray(),
+ )
+ }
+ }
+ field = value
+ }
+
+ override var remoteDebuggingEnabled: Boolean
+ get() = runtime.settings.remoteDebuggingEnabled
+ set(value) { runtime.settings.remoteDebuggingEnabled = value }
+
+ override var historyTrackingDelegate: HistoryTrackingDelegate?
+ get() = defaultSettings?.historyTrackingDelegate
+ set(value) { defaultSettings?.historyTrackingDelegate = value }
+
+ override var testingModeEnabled: Boolean
+ get() = defaultSettings?.testingModeEnabled ?: false
+ set(value) { defaultSettings?.testingModeEnabled = value }
+
+ override var userAgentString: String?
+ get() = defaultSettings?.userAgentString ?: GeckoSession.getDefaultUserAgent()
+ set(value) { defaultSettings?.userAgentString = value }
+
+ override var preferredColorScheme: PreferredColorScheme
+ get() = PreferredColorScheme.from(runtime.settings.preferredColorScheme)
+ set(value) { runtime.settings.preferredColorScheme = value.toGeckoValue() }
+
+ override var suspendMediaWhenInactive: Boolean
+ get() = defaultSettings?.suspendMediaWhenInactive ?: false
+ set(value) { defaultSettings?.suspendMediaWhenInactive = value }
+
+ override var clearColor: Int?
+ get() = defaultSettings?.clearColor
+ set(value) { defaultSettings?.clearColor = value }
+
+ override var fontInflationEnabled: Boolean?
+ get() = runtime.settings.fontInflationEnabled
+ set(value) {
+ // automaticFontSizeAdjustment is set to true by default, which
+ // will cause an exception if fontInflationEnabled is set
+ // (to either true or false). We therefore need to be able to
+ // set our built-in default value to null so that the exception
+ // is only thrown if an app is configured incorrectly but not
+ // if it uses default values.
+ value?.let {
+ runtime.settings.fontInflationEnabled = it
+ }
+ }
+
+ override var fontSizeFactor: Float?
+ get() = runtime.settings.fontSizeFactor
+ set(value) {
+ // automaticFontSizeAdjustment is set to true by default, which
+ // will cause an exception if fontSizeFactor is set as well.
+ // We therefore need to be able to set our built-in default value
+ // to null so that the exception is only thrown if an app is
+ // configured incorrectly but not if it uses default values.
+ value?.let {
+ runtime.settings.fontSizeFactor = it
+ }
+ }
+
+ override var loginAutofillEnabled: Boolean
+ get() = runtime.settings.loginAutofillEnabled
+ set(value) { runtime.settings.loginAutofillEnabled = value }
+
+ override var forceUserScalableContent: Boolean
+ get() = runtime.settings.forceUserScalableEnabled
+ set(value) { runtime.settings.forceUserScalableEnabled = value }
+
+ override var enterpriseRootsEnabled: Boolean
+ get() = runtime.settings.enterpriseRootsEnabled
+ set(value) { runtime.settings.enterpriseRootsEnabled = value }
+
+ override var httpsOnlyMode: Engine.HttpsOnlyMode
+ get() = when (runtime.settings.allowInsecureConnections) {
+ GeckoRuntimeSettings.ALLOW_ALL -> Engine.HttpsOnlyMode.DISABLED
+ GeckoRuntimeSettings.HTTPS_ONLY_PRIVATE -> Engine.HttpsOnlyMode.ENABLED_PRIVATE_ONLY
+ GeckoRuntimeSettings.HTTPS_ONLY -> Engine.HttpsOnlyMode.ENABLED
+ else -> throw java.lang.IllegalStateException("Unknown HTTPS-Only mode returned by GeckoView")
+ }
+ set(value) {
+ runtime.settings.allowInsecureConnections = when (value) {
+ Engine.HttpsOnlyMode.DISABLED -> GeckoRuntimeSettings.ALLOW_ALL
+ Engine.HttpsOnlyMode.ENABLED_PRIVATE_ONLY -> GeckoRuntimeSettings.HTTPS_ONLY_PRIVATE
+ Engine.HttpsOnlyMode.ENABLED -> GeckoRuntimeSettings.HTTPS_ONLY
+ }
+ }
+ override var globalPrivacyControlEnabled: Boolean
+ get() = runtime.settings.globalPrivacyControl
+ set(value) { runtime.settings.setGlobalPrivacyControl(value) }
+ }.apply {
+ defaultSettings?.let {
+ this.javascriptEnabled = it.javascriptEnabled
+ this.webFontsEnabled = it.webFontsEnabled
+ this.automaticFontSizeAdjustment = it.automaticFontSizeAdjustment
+ this.automaticLanguageAdjustment = it.automaticLanguageAdjustment
+ this.trackingProtectionPolicy = it.trackingProtectionPolicy
+ this.safeBrowsingPolicy = arrayOf(SafeBrowsingPolicy.RECOMMENDED)
+ this.remoteDebuggingEnabled = it.remoteDebuggingEnabled
+ this.testingModeEnabled = it.testingModeEnabled
+ this.userAgentString = it.userAgentString
+ this.preferredColorScheme = it.preferredColorScheme
+ this.fontInflationEnabled = it.fontInflationEnabled
+ this.fontSizeFactor = it.fontSizeFactor
+ this.forceUserScalableContent = it.forceUserScalableContent
+ this.clearColor = it.clearColor
+ this.loginAutofillEnabled = it.loginAutofillEnabled
+ this.enterpriseRootsEnabled = it.enterpriseRootsEnabled
+ this.httpsOnlyMode = it.httpsOnlyMode
+ this.cookieBannerHandlingMode = it.cookieBannerHandlingMode
+ this.cookieBannerHandlingModePrivateBrowsing = it.cookieBannerHandlingModePrivateBrowsing
+ this.cookieBannerHandlingDetectOnlyMode = it.cookieBannerHandlingDetectOnlyMode
+ this.cookieBannerHandlingGlobalRules = it.cookieBannerHandlingGlobalRules
+ this.cookieBannerHandlingGlobalRulesSubFrames = it.cookieBannerHandlingGlobalRulesSubFrames
+ this.globalPrivacyControlEnabled = it.globalPrivacyControlEnabled
+ this.emailTrackerBlockingPrivateBrowsing = it.emailTrackerBlockingPrivateBrowsing
+ }
+ }
+
+ @Suppress("ComplexMethod")
+ internal fun ContentBlockingController.LogEntry.BlockingData.getLoadedCategory(): TrackingCategory {
+ val socialTrackingProtectionEnabled = settings.trackingProtectionPolicy?.strictSocialTrackingProtection
+ ?: false
+
+ return when (category) {
+ Event.LOADED_FINGERPRINTING_CONTENT -> TrackingCategory.FINGERPRINTING
+ Event.LOADED_CRYPTOMINING_CONTENT -> TrackingCategory.CRYPTOMINING
+ Event.LOADED_SOCIALTRACKING_CONTENT -> {
+ if (socialTrackingProtectionEnabled) TrackingCategory.MOZILLA_SOCIAL else TrackingCategory.NONE
+ }
+ Event.COOKIES_LOADED_SOCIALTRACKER -> {
+ if (!socialTrackingProtectionEnabled) TrackingCategory.MOZILLA_SOCIAL else TrackingCategory.NONE
+ }
+ Event.LOADED_LEVEL_1_TRACKING_CONTENT -> TrackingCategory.SCRIPTS_AND_SUB_RESOURCES
+ Event.LOADED_LEVEL_2_TRACKING_CONTENT -> {
+ // We are making sure that we are only showing trackers that our settings are
+ // taking into consideration.
+ val isContentListActive =
+ settings.trackingProtectionPolicy?.contains(TrackingCategory.CONTENT)
+ ?: false
+ val isStrictLevelActive =
+ runtime.settings
+ .contentBlocking
+ .getEnhancedTrackingProtectionLevel() == ContentBlocking.EtpLevel.STRICT
+
+ if (isStrictLevelActive && isContentListActive) {
+ TrackingCategory.SCRIPTS_AND_SUB_RESOURCES
+ } else {
+ TrackingCategory.NONE
+ }
+ }
+ else -> TrackingCategory.NONE
+ }
+ }
+
+ private fun isCategoryActive(category: TrackingCategory) = settings.trackingProtectionPolicy?.contains(category)
+ ?: false
+
+ /**
+ * Mimics the behavior for categorizing trackers from desktop, they should be kept in sync,
+ * as differences will result in improper categorization for trackers.
+ * https://dxr.mozilla.org/mozilla-central/source/browser/base/content/browser-siteProtections.js
+ */
+ internal fun ContentBlockingController.LogEntry.toTrackerLog(): TrackerLog {
+ val cookiesHasBeenBlocked = this.blockingData.any { it.hasBlockedCookies() }
+ val blockedCategories = blockingData.map { it.getBlockedCategory() }
+ .filterNot { it == TrackingCategory.NONE }
+ .distinct()
+ val loadedCategories = blockingData.map { it.getLoadedCategory() }
+ .filterNot { it == TrackingCategory.NONE }
+ .distinct()
+
+ /**
+ * When a resource is shimmed we'll received a [REPLACED_TRACKING_CONTENT] event with
+ * the quantity [BlockingData.count] of categories that were shimmed, but it doesn't
+ * specify which ones, it only tells us how many. For example:
+ * {
+ * "category": REPLACED_TRACKING_CONTENT,
+ * "count": 2
+ * }
+ *
+ * This indicates that there are 2 categories that were shimmed, as a result
+ * we have to infer based on the categories that are active vs the amount of
+ * shimmed categories, for example:
+ *
+ * "blockData": [
+ * {
+ * "category": LOADED_LEVEL_1_TRACKING_CONTENT,
+ * "count": 1
+ * },
+ * {
+ * "category": LOADED_SOCIALTRACKING_CONTENT,
+ * "count": 1
+ * },
+ * {
+ * "category": REPLACED_TRACKING_CONTENT,
+ * "count": 2
+ * }
+ * ]
+ * This indicates that categories [LOADED_LEVEL_1_TRACKING_CONTENT] and
+ * [LOADED_SOCIALTRACKING_CONTENT] were loaded but shimmed and we should display them
+ * as blocked instead of loaded.
+ */
+ val shimmedCount = blockingData.find {
+ it.category == Event.REPLACED_TRACKING_CONTENT
+ }?.count ?: 0
+
+ // If we find blocked categories that are loaded it means they were shimmed.
+ val shimmedCategories = loadedCategories.filter { isCategoryActive(it) }
+ .take(shimmedCount)
+
+ // We have to remove the categories that are shimmed from the loaded list and
+ // put them back in the blocked list.
+ return TrackerLog(
+ url = origin,
+ loadedCategories = loadedCategories.filterNot { it in shimmedCategories },
+ blockedCategories = (blockedCategories + shimmedCategories).distinct(),
+ cookiesHasBeenBlocked = cookiesHasBeenBlocked,
+ unBlockedBySmartBlock = this.blockingData.any { it.unBlockedBySmartBlock() },
+ )
+ }
+
+ internal fun org.mozilla.geckoview.WebExtension?.toSafeWebExtension(): GeckoWebExtension? {
+ return if (this != null) {
+ GeckoWebExtension(
+ this,
+ runtime,
+ )
+ } else {
+ null
+ }
+ }
+
+ private fun onExtensionInstalled(
+ ext: org.mozilla.geckoview.WebExtension,
+ onSuccess: ((WebExtension) -> Unit),
+ ) {
+ val installedExtension = GeckoWebExtension(ext, runtime)
+ webExtensionDelegate?.onInstalled(installedExtension)
+ installedExtension.registerActionHandler(webExtensionActionHandler)
+ installedExtension.registerTabHandler(webExtensionTabHandler, defaultSettings)
+ onSuccess(installedExtension)
+ }
+}
+
+internal fun ContentBlockingController.LogEntry.BlockingData.hasBlockedCookies(): Boolean {
+ return category == Event.COOKIES_BLOCKED_BY_PERMISSION ||
+ category == Event.COOKIES_BLOCKED_TRACKER ||
+ category == Event.COOKIES_BLOCKED_ALL ||
+ category == Event.COOKIES_PARTITIONED_FOREIGN ||
+ category == Event.COOKIES_BLOCKED_FOREIGN ||
+ category == Event.COOKIES_BLOCKED_SOCIALTRACKER
+}
+
+internal fun ContentBlockingController.LogEntry.BlockingData.unBlockedBySmartBlock(): Boolean {
+ return category == Event.ALLOWED_TRACKING_CONTENT
+}
+
+internal fun ContentBlockingController.LogEntry.BlockingData.getBlockedCategory(): TrackingCategory {
+ return when (category) {
+ Event.BLOCKED_FINGERPRINTING_CONTENT -> TrackingCategory.FINGERPRINTING
+ Event.BLOCKED_CRYPTOMINING_CONTENT -> TrackingCategory.CRYPTOMINING
+ Event.BLOCKED_SOCIALTRACKING_CONTENT, Event.COOKIES_BLOCKED_SOCIALTRACKER -> TrackingCategory.MOZILLA_SOCIAL
+ Event.BLOCKED_TRACKING_CONTENT -> TrackingCategory.SCRIPTS_AND_SUB_RESOURCES
+ else -> TrackingCategory.NONE
+ }
+}
+
+internal fun InstallationMethod.toGeckoInstallationMethod(): String? {
+ return when (this) {
+ InstallationMethod.MANAGER -> WebExtensionController.INSTALLATION_METHOD_MANAGER
+ InstallationMethod.FROM_FILE -> WebExtensionController.INSTALLATION_METHOD_FROM_FILE
+ else -> null
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
new file mode 100644
index 0000000000..e6907c6dde
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
@@ -0,0 +1,1880 @@
+/* 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.browser.engine.gecko
+
+import android.annotation.SuppressLint
+import android.net.Uri
+import android.os.Build
+import android.view.WindowManager
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.engine.gecko.ext.isExcludedForTrackingProtection
+import mozilla.components.browser.engine.gecko.fetch.toResponse
+import mozilla.components.browser.engine.gecko.media.GeckoMediaDelegate
+import mozilla.components.browser.engine.gecko.mediasession.GeckoMediaSessionDelegate
+import mozilla.components.browser.engine.gecko.permission.GeckoPermissionRequest
+import mozilla.components.browser.engine.gecko.prompt.GeckoPromptDelegate
+import mozilla.components.browser.engine.gecko.translate.GeckoTranslateSessionDelegate
+import mozilla.components.browser.engine.gecko.translate.GeckoTranslationUtils.intoTranslationError
+import mozilla.components.browser.engine.gecko.window.GeckoWindowRequest
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.ALLOW_ADDITIONAL_HEADERS
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.ALLOW_JAVASCRIPT_URL
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.history.HistoryItem
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifestParser
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.concept.engine.request.RequestInterceptor.InterceptionResponse
+import mozilla.components.concept.engine.shopping.Highlight
+import mozilla.components.concept.engine.shopping.ProductAnalysis
+import mozilla.components.concept.engine.shopping.ProductAnalysisStatus
+import mozilla.components.concept.engine.shopping.ProductRecommendation
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.translate.TranslationOptions
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_DISPOSITION
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_LENGTH
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.storage.PageVisit
+import mozilla.components.concept.storage.RedirectSource
+import mozilla.components.concept.storage.VisitType
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.isEmail
+import mozilla.components.support.ktx.kotlin.isExtensionUrl
+import mozilla.components.support.ktx.kotlin.isGeoLocation
+import mozilla.components.support.ktx.kotlin.isPhone
+import mozilla.components.support.ktx.kotlin.sanitizeFileName
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+import mozilla.components.support.utils.DownloadUtils
+import mozilla.components.support.utils.DownloadUtils.RESPONSE_CODE_SUCCESS
+import mozilla.components.support.utils.DownloadUtils.makePdfContentDisposition
+import org.json.JSONObject
+import org.mozilla.geckoview.AllowOrDeny
+import org.mozilla.geckoview.ContentBlocking
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoSession.NavigationDelegate
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
+import org.mozilla.geckoview.GeckoSession.Recommendation
+import org.mozilla.geckoview.GeckoSessionSettings
+import org.mozilla.geckoview.WebRequestError
+import org.mozilla.geckoview.WebResponse
+import java.util.Locale
+import kotlin.coroutines.CoroutineContext
+import org.mozilla.geckoview.TranslationsController.SessionTranslation as GeckoViewTranslateSession
+
+/**
+ * Gecko-based EngineSession implementation.
+ */
+@Suppress("TooManyFunctions", "LargeClass")
+class GeckoEngineSession(
+ private val runtime: GeckoRuntime,
+ private val privateMode: Boolean = false,
+ private val defaultSettings: Settings? = null,
+ contextId: String? = null,
+ private val geckoSessionProvider: () -> GeckoSession = {
+ val settings = GeckoSessionSettings.Builder()
+ .usePrivateMode(privateMode)
+ .contextId(contextId)
+ .build()
+ GeckoSession(settings)
+ },
+ private val context: CoroutineContext = Dispatchers.IO,
+ openGeckoSession: Boolean = true,
+) : CoroutineScope, EngineSession() {
+
+ // This logger is temporary and parsed by FNPRMS for performance measurements. It can be
+ // removed once FNPRMS is replaced: https://github.com/mozilla-mobile/android-components/issues/8662
+ // It mimics GeckoView debug log statements, hence the unintuitive tag and messages.
+ private val fnprmsLogger = Logger("GeckoSession")
+
+ private val logger = Logger("GeckoEngineSession")
+
+ internal lateinit var geckoSession: GeckoSession
+ internal var currentUrl: String? = null
+ internal var currentTitle: String? = null
+ internal var lastLoadRequestUri: String? = null
+ internal var pageLoadingUrl: String? = null
+ internal var appRedirectUrl: String? = null
+ internal var scrollY: Int = 0
+
+ // The Gecko site permissions for the loaded site.
+ internal var geckoPermissions: List<ContentPermission> = emptyList()
+
+ internal var job: Job = Job()
+ private var canGoBack: Boolean = false
+ private var canGoForward: Boolean = false
+
+ /**
+ * See [EngineSession.settings]
+ */
+ override val settings: Settings = object : Settings() {
+ override var requestInterceptor: RequestInterceptor? = null
+ override var historyTrackingDelegate: HistoryTrackingDelegate? = null
+ override var userAgentString: String?
+ get() = geckoSession.settings.userAgentOverride
+ set(value) {
+ geckoSession.settings.userAgentOverride = value
+ }
+ override var suspendMediaWhenInactive: Boolean
+ get() = geckoSession.settings.suspendMediaWhenInactive
+ set(value) {
+ geckoSession.settings.suspendMediaWhenInactive = value
+ }
+ }
+
+ internal var initialLoad = true
+
+ override val coroutineContext: CoroutineContext
+ get() = context + job
+
+ init {
+ createGeckoSession(shouldOpen = openGeckoSession)
+ }
+
+ /**
+ * Represents a request to load a [url].
+ *
+ * @param url the url to load.
+ * @param parent the parent (referring) [EngineSession] i.e. the session that
+ * triggered creating this one.
+ * @param flags the [LoadUrlFlags] to use when loading the provided url.
+ * @param additionalHeaders the extra headers to use when loading the provided url.
+ **/
+ data class LoadRequest(
+ val url: String,
+ val parent: EngineSession?,
+ val flags: LoadUrlFlags,
+ val additionalHeaders: Map<String, String>?,
+ )
+
+ @VisibleForTesting
+ internal var initialLoadRequest: LoadRequest? = null
+
+ /**
+ * See [EngineSession.loadUrl]
+ */
+ override fun loadUrl(
+ url: String,
+ parent: EngineSession?,
+ flags: LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {
+ notifyObservers { onLoadUrl() }
+
+ val scheme = Uri.parse(url).normalizeScheme().scheme
+ if (BLOCKED_SCHEMES.contains(scheme) && !shouldLoadJSSchemes(scheme, flags)) {
+ logger.error("URL scheme not allowed. Aborting load.")
+ return
+ }
+
+ if (initialLoad) {
+ initialLoadRequest = LoadRequest(url, parent, flags, additionalHeaders)
+ }
+
+ val loader = GeckoSession.Loader()
+ .uri(url)
+ .flags(flags.getGeckoFlags())
+
+ if (additionalHeaders != null) {
+ val headerFilter = if (flags.contains(ALLOW_ADDITIONAL_HEADERS)) {
+ GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE
+ } else {
+ GeckoSession.HEADER_FILTER_CORS_SAFELISTED
+ }
+ loader.additionalHeaders(additionalHeaders)
+ .headerFilter(headerFilter)
+ }
+
+ if (parent != null) {
+ loader.referrer((parent as GeckoEngineSession).geckoSession)
+ }
+
+ geckoSession.load(loader)
+ Fact(
+ Component.BROWSER_ENGINE_GECKO,
+ Action.IMPLEMENTATION_DETAIL,
+ "GeckoSession.load",
+ ).collect()
+ }
+
+ private fun shouldLoadJSSchemes(
+ scheme: String?,
+ flags: LoadUrlFlags,
+ ) = scheme?.startsWith(JS_SCHEME) == true && flags.contains(ALLOW_JAVASCRIPT_URL)
+
+ /**
+ * See [EngineSession.loadData]
+ */
+ override fun loadData(data: String, mimeType: String, encoding: String) {
+ when (encoding) {
+ "base64" -> geckoSession.load(GeckoSession.Loader().data(data.toByteArray(), mimeType))
+ else -> geckoSession.load(GeckoSession.Loader().data(data, mimeType))
+ }
+ notifyObservers { onLoadData() }
+ }
+
+ /**
+ * See [EngineSession.requestPdfToDownload]
+ */
+ override fun requestPdfToDownload() {
+ geckoSession.saveAsPdf().then(
+ { inputStream ->
+ if (inputStream == null) {
+ logger.error("No input stream available for Save to PDF.")
+ return@then GeckoResult<Void>()
+ }
+
+ val url = this.currentUrl ?: ""
+ val contentType = "application/pdf"
+ val disposition = currentTitle?.let { makePdfContentDisposition(it) }
+ // A successful status code suffices because the PDF is generated on device.
+ val responseStatus = RESPONSE_CODE_SUCCESS
+ // We do not know the size at this point; send 0 so consumers do not display it.
+ val contentLength = 0L
+ // NB: If the title is an empty string, there is a chance the PDF will not have a name.
+ // See https://github.com/mozilla-mobile/android-components/issues/12276
+ val fileName = DownloadUtils.guessFileName(
+ disposition,
+ destinationDirectory = null,
+ url = url,
+ mimeType = contentType,
+ )
+
+ val response = Response(
+ url = url,
+ status = responseStatus,
+ headers = MutableHeaders(),
+ body = Response.Body(inputStream),
+ )
+
+ notifyObservers {
+ onExternalResource(
+ url = url,
+ contentLength = contentLength,
+ contentType = contentType,
+ fileName = fileName,
+ response = response,
+ isPrivate = privateMode,
+ )
+ }
+
+ notifyObservers {
+ onSaveToPdfComplete()
+ }
+
+ GeckoResult()
+ },
+ { throwable ->
+ // Log the error. There is nothing we can do otherwise.
+ logger.error("Save to PDF failed.", throwable)
+ notifyObservers {
+ onSaveToPdfException(throwable)
+ }
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.requestPrintContent]
+ */
+ override fun requestPrintContent() {
+ geckoSession.didPrintPageContent().then(
+ { finishedPrinting ->
+ if (finishedPrinting == true) {
+ notifyObservers {
+ onPrintFinish()
+ }
+ }
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ logger.error("Printing failed.", throwable)
+ notifyObservers {
+ onPrintException(true, throwable)
+ }
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.stopLoading]
+ */
+ override fun stopLoading() {
+ geckoSession.stop()
+ }
+
+ /**
+ * See [EngineSession.reload]
+ */
+ override fun reload(flags: LoadUrlFlags) {
+ initialLoadRequest?.let {
+ // We have a pending initial load request, which means we never
+ // successfully loaded a page. Calling reload now would just reload
+ // about:blank. To prevent that we trigger the initial load again.
+ loadUrl(it.url, it.parent, it.flags, it.additionalHeaders)
+ } ?: geckoSession.reload(flags.getGeckoFlags())
+ }
+
+ /**
+ * See [EngineSession.goBack]
+ */
+ override fun goBack(userInteraction: Boolean) {
+ geckoSession.goBack(userInteraction)
+ if (canGoBack) {
+ notifyObservers { onNavigateBack() }
+ }
+ }
+
+ /**
+ * See [EngineSession.goForward]
+ */
+ override fun goForward(userInteraction: Boolean) {
+ geckoSession.goForward(userInteraction)
+ if (canGoForward) {
+ notifyObservers { onNavigateForward() }
+ }
+ }
+
+ /**
+ * See [EngineSession.goToHistoryIndex]
+ */
+ override fun goToHistoryIndex(index: Int) {
+ geckoSession.gotoHistoryIndex(index)
+ notifyObservers { onGotoHistoryIndex() }
+ }
+
+ /**
+ * See [EngineSession.restoreState]
+ */
+ override fun restoreState(state: EngineSessionState): Boolean {
+ if (state !is GeckoEngineSessionState) {
+ throw IllegalStateException("Can only restore from GeckoEngineSessionState")
+ }
+ // Also checking if SessionState is empty as a workaround for:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1687523
+ if (state.actualState.isNullOrEmpty()) {
+ return false
+ }
+
+ geckoSession.restoreState(state.actualState)
+ return true
+ }
+
+ /**
+ * See [EngineSession.updateTrackingProtection]
+ */
+ override fun updateTrackingProtection(policy: TrackingProtectionPolicy) {
+ updateContentBlocking(policy)
+ val enabled = policy != TrackingProtectionPolicy.none()
+ etpEnabled = enabled
+ notifyObservers {
+ onTrackerBlockingEnabledChange(this, enabled)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun updateContentBlocking(policy: TrackingProtectionPolicy) {
+ /**
+ * As described on https://bugzilla.mozilla.org/show_bug.cgi?id=1579264,useTrackingProtection
+ * is a misleading setting. When is set to true is blocking content (scripts/sub-resources).
+ * Instead of just turn on/off tracking protection. Until, this issue is fixed consumers need
+ * a way to indicate, if they want to block content or not, this is why we use
+ * [TrackingProtectionPolicy.TrackingCategory.SCRIPTS_AND_SUB_RESOURCES].
+ */
+ val shouldBlockContent =
+ policy.contains(TrackingProtectionPolicy.TrackingCategory.SCRIPTS_AND_SUB_RESOURCES)
+
+ val enabledInBrowsingMode = if (privateMode) {
+ policy.useForPrivateSessions
+ } else {
+ policy.useForRegularSessions
+ }
+ geckoSession.settings.useTrackingProtection = enabledInBrowsingMode && shouldBlockContent
+ }
+
+ // This is a temporary solution to address
+ // https://github.com/mozilla-mobile/android-components/issues/8431
+ // until we eventually delete [EngineObserver] then this will not be needed.
+ @VisibleForTesting
+ internal var etpEnabled: Boolean? = null
+
+ override fun register(observer: Observer) {
+ super.register(observer)
+ etpEnabled?.let { enabled ->
+ onTrackerBlockingEnabledChange(observer, enabled)
+ }
+ }
+
+ private fun onTrackerBlockingEnabledChange(observer: Observer, enabled: Boolean) {
+ // We now register engine observers in a middleware using a dedicated
+ // store thread. Since this notification can be delayed until an observer
+ // is registered we switch to the main scope to make sure we're not notifying
+ // on the store thread.
+ MainScope().launch {
+ observer.onTrackerBlockingEnabledChange(enabled)
+ }
+ }
+
+ /**
+ * Indicates if this [EngineSession] should be ignored the tracking protection policies.
+ * @return if this [EngineSession] is in
+ * the exception list, true if it is in, otherwise false.
+ */
+ internal fun isIgnoredForTrackingProtection(): Boolean {
+ return geckoPermissions.any { it.isExcludedForTrackingProtection }
+ }
+
+ /**
+ * See [EngineSession.settings]
+ */
+ override fun toggleDesktopMode(enable: Boolean, reload: Boolean) {
+ val currentMode = geckoSession.settings.userAgentMode
+ val currentViewPortMode = geckoSession.settings.viewportMode
+ var overrideUrl: String? = null
+
+ val newMode = if (enable) {
+ GeckoSessionSettings.USER_AGENT_MODE_DESKTOP
+ } else {
+ GeckoSessionSettings.USER_AGENT_MODE_MOBILE
+ }
+
+ val newViewportMode = if (enable) {
+ overrideUrl = currentUrl?.let { checkForMobileSite(it) }
+ GeckoSessionSettings.VIEWPORT_MODE_DESKTOP
+ } else {
+ GeckoSessionSettings.VIEWPORT_MODE_MOBILE
+ }
+
+ if (newMode != currentMode || newViewportMode != currentViewPortMode) {
+ geckoSession.settings.userAgentMode = newMode
+ geckoSession.settings.viewportMode = newViewportMode
+ notifyObservers { onDesktopModeChange(enable) }
+ }
+
+ if (reload) {
+ if (overrideUrl == null) {
+ this.reload()
+ } else {
+ loadUrl(overrideUrl, flags = LoadUrlFlags.select(LoadUrlFlags.LOAD_FLAGS_REPLACE_HISTORY))
+ }
+ }
+ }
+
+ /**
+ * See [EngineSession.hasCookieBannerRuleForSession]
+ */
+ override fun hasCookieBannerRuleForSession(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.hasCookieBannerRuleForBrowsingContextTree().then(
+ { response ->
+ if (response == null) {
+ logger.error(
+ "Invalid value: unable to get response from hasCookieBannerRuleForBrowsingContextTree.",
+ )
+ onException(
+ java.lang.IllegalStateException(
+ "Invalid value: unable to get response from hasCookieBannerRuleForBrowsingContextTree.",
+ ),
+ )
+ return@then GeckoResult()
+ }
+ onResult(response)
+ GeckoResult<Boolean>()
+ },
+ { throwable ->
+ logger.error("Checking for cookie banner rule failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * Checks and returns a non-mobile version of the url.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun checkForMobileSite(url: String): String? {
+ var overrideUrl: String? = null
+ val mPrefix = "m."
+ val mobilePrefix = "mobile."
+
+ val uri = Uri.parse(url)
+ val authority = uri.authority?.lowercase(Locale.ROOT) ?: return null
+
+ val foundPrefix = when {
+ authority.startsWith(mPrefix) -> mPrefix
+ authority.startsWith(mobilePrefix) -> mobilePrefix
+ else -> null
+ }
+
+ foundPrefix?.let {
+ val mobileUri = Uri.parse(url).buildUpon().authority(authority.substring(it.length))
+ overrideUrl = mobileUri.toString()
+ }
+
+ return overrideUrl
+ }
+
+ /**
+ * See [EngineSession.findAll]
+ */
+ override fun findAll(text: String) {
+ notifyObservers { onFind(text) }
+ geckoSession.finder.find(text, 0).then { result: GeckoSession.FinderResult? ->
+ result?.let {
+ val activeMatchOrdinal = if (it.current > 0) it.current - 1 else it.current
+ notifyObservers { onFindResult(activeMatchOrdinal, it.total, true) }
+ }
+ GeckoResult<Void>()
+ }
+ }
+
+ /**
+ * See [EngineSession.findNext]
+ */
+ @SuppressLint("WrongConstant") // FinderFindFlags annotation doesn't include a 0 value.
+ override fun findNext(forward: Boolean) {
+ val findFlags = if (forward) 0 else GeckoSession.FINDER_FIND_BACKWARDS
+ geckoSession.finder.find(null, findFlags).then { result: GeckoSession.FinderResult? ->
+ result?.let {
+ val activeMatchOrdinal = if (it.current > 0) it.current - 1 else it.current
+ notifyObservers { onFindResult(activeMatchOrdinal, it.total, true) }
+ }
+ GeckoResult<Void>()
+ }
+ }
+
+ /**
+ * See [EngineSession.clearFindMatches]
+ */
+ override fun clearFindMatches() {
+ geckoSession.finder.clear()
+ }
+
+ /**
+ * See [EngineSession.exitFullScreenMode]
+ */
+ override fun exitFullScreenMode() {
+ geckoSession.exitFullScreen()
+ }
+
+ /**
+ * See [EngineSession.markActiveForWebExtensions].
+ */
+ override fun markActiveForWebExtensions(active: Boolean) {
+ runtime.webExtensionController.setTabActive(geckoSession, active)
+ }
+
+ /**
+ * See [EngineSession.updateSessionPriority].
+ */
+ override fun updateSessionPriority(priority: SessionPriority) {
+ geckoSession.setPriorityHint(priority.id)
+ }
+
+ /**
+ * See [EngineSession.setDisplayMode].
+ */
+ override fun setDisplayMode(displayMode: WebAppManifest.DisplayMode) {
+ geckoSession.settings.displayMode = when (displayMode) {
+ WebAppManifest.DisplayMode.MINIMAL_UI -> GeckoSessionSettings.DISPLAY_MODE_MINIMAL_UI
+ WebAppManifest.DisplayMode.FULLSCREEN -> GeckoSessionSettings.DISPLAY_MODE_FULLSCREEN
+ WebAppManifest.DisplayMode.STANDALONE -> GeckoSessionSettings.DISPLAY_MODE_STANDALONE
+ else -> GeckoSessionSettings.DISPLAY_MODE_BROWSER
+ }
+ }
+
+ /**
+ * See [EngineSession.checkForFormData].
+ */
+ override fun checkForFormData() {
+ geckoSession.containsFormData().then(
+ { result ->
+ if (result == null) {
+ logger.error("No result from GeckoView containsFormData.")
+ return@then GeckoResult<Boolean>()
+ }
+ notifyObservers { onCheckForFormData(result) }
+ GeckoResult<Boolean>()
+ },
+ { throwable ->
+ notifyObservers {
+ onCheckForFormDataException(throwable)
+ }
+ GeckoResult<Boolean>()
+ },
+ )
+ }
+
+ /**
+ * Checks if a PDF viewer is being used on the current page or not via GeckoView session.
+ */
+ override fun checkForPdfViewer(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.isPdfJs.then(
+ { response ->
+ if (response == null) {
+ logger.error(
+ "Invalid value: No result from GeckoView if a PDF viewer is used.",
+ )
+ onException(
+ IllegalStateException(
+ "Invalid value: No result from GeckoView if a PDF viewer is used.",
+ ),
+ )
+ return@then GeckoResult()
+ }
+ onResult(response)
+ GeckoResult<Boolean>()
+ },
+ { throwable ->
+ logger.error("Checking for PDF viewer failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.requestProductRecommendations]
+ */
+ override fun requestProductRecommendations(
+ url: String,
+ onResult: (List<ProductRecommendation>) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.requestRecommendations(url).then(
+ { response: List<Recommendation>? ->
+ if (response == null) {
+ logger.error("Invalid value: unable to get analysis result from Gecko Engine.")
+ onException(
+ java.lang.IllegalStateException(
+ "Invalid value: unable to get analysis result from Gecko Engine.",
+ ),
+ )
+ return@then GeckoResult()
+ }
+
+ val productRecommendations = response.map { it: Recommendation ->
+ ProductRecommendation(
+ url = it.url,
+ analysisUrl = it.analysisUrl,
+ adjustedRating = it.adjustedRating,
+ sponsored = it.sponsored,
+ imageUrl = it.imageUrl,
+ aid = it.aid,
+ name = it.name,
+ grade = it.grade,
+ price = it.price,
+ currency = it.currency,
+ )
+ }
+ onResult(productRecommendations)
+ GeckoResult<ProductRecommendation>()
+ },
+ { throwable ->
+ logger.error("Requesting product analysis failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.requestProductAnalysis]
+ */
+ @Suppress("ComplexCondition")
+ override fun requestProductAnalysis(
+ url: String,
+ onResult: (ProductAnalysis) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.requestAnalysis(url).then(
+ { response ->
+ if (response == null) {
+ logger.error(
+ "Invalid value: unable to get analysis result from Gecko Engine.",
+ )
+ onException(
+ java.lang.IllegalStateException(
+ "Invalid value: unable to get analysis result from Gecko Engine.",
+ ),
+ )
+ return@then GeckoResult()
+ }
+
+ val highlights = if (
+ response.highlights?.quality == null &&
+ response.highlights?.price == null &&
+ response.highlights?.shipping == null &&
+ response.highlights?.appearance == null &&
+ response.highlights?.competitiveness == null
+ ) {
+ null
+ } else {
+ Highlight(
+ response.highlights?.quality?.toList(),
+ response.highlights?.price?.toList(),
+ response.highlights?.shipping?.toList(),
+ response.highlights?.appearance?.toList(),
+ response.highlights?.competitiveness?.toList(),
+ )
+ }
+
+ val analysisResult = ProductAnalysis(
+ productId = response.productId,
+ analysisURL = response.analysisURL,
+ grade = response.grade,
+ adjustedRating = response.adjustedRating,
+ needsAnalysis = response.needsAnalysis,
+ pageNotSupported = response.pageNotSupported,
+ notEnoughReviews = response.notEnoughReviews,
+ lastAnalysisTime = response.lastAnalysisTime,
+ deletedProductReported = response.deletedProductReported,
+ deletedProduct = response.deletedProduct,
+ highlights = highlights,
+ )
+
+ onResult(analysisResult)
+ GeckoResult<ProductAnalysis>()
+ },
+ { throwable ->
+ logger.error("Requesting product analysis failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.reanalyzeProduct]
+ */
+ override fun reanalyzeProduct(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.requestCreateAnalysis(url).then(
+ { response ->
+ val errorMessage = "Invalid value: unable to reanalyze product from Gecko Engine."
+ if (response == null) {
+ logger.error(errorMessage)
+ onException(
+ java.lang.IllegalStateException(errorMessage),
+ )
+ return@then GeckoResult()
+ }
+ onResult(response)
+ GeckoResult<String>()
+ },
+ { throwable ->
+ logger.error("Request to reanalyze product failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.requestAnalysisStatus]
+ */
+ override fun requestAnalysisStatus(
+ url: String,
+ onResult: (ProductAnalysisStatus) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.requestAnalysisStatus(url).then(
+ { response ->
+ val errorMessage = "Invalid value: unable to request analysis status from Gecko Engine."
+ if (response == null) {
+ logger.error(errorMessage)
+ onException(
+ java.lang.IllegalStateException(errorMessage),
+ )
+ return@then GeckoResult()
+ }
+ val analysisStatusResult = ProductAnalysisStatus(
+ status = response.status,
+ progress = response.progress,
+ )
+ onResult(analysisStatusResult)
+ GeckoResult<ProductAnalysisStatus>()
+ },
+ { throwable ->
+ logger.error("Request for product analysis status failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.sendClickAttributionEvent]
+ */
+ override fun sendClickAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.sendClickAttributionEvent(aid).then(
+ { response ->
+ val errorMessage = "Invalid value: unable to send click attribution event through Gecko Engine."
+ if (response == null) {
+ logger.error(errorMessage)
+ onException(
+ java.lang.IllegalStateException(errorMessage),
+ )
+ return@then GeckoResult()
+ }
+ onResult(response)
+ GeckoResult<Boolean>()
+ },
+ { throwable ->
+ logger.error("Sending click attribution event failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.sendImpressionAttributionEvent]
+ */
+ override fun sendImpressionAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.sendImpressionAttributionEvent(aid).then(
+ { response ->
+ val errorMessage = "Invalid value: unable to send impression attribution event through Gecko Engine."
+ if (response == null) {
+ logger.error(errorMessage)
+ onException(
+ java.lang.IllegalStateException(errorMessage),
+ )
+ return@then GeckoResult()
+ }
+ onResult(response)
+ GeckoResult<Boolean>()
+ },
+ { throwable ->
+ logger.error("Sending impression attribution event failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.sendPlacementAttributionEvent]
+ */
+ override fun sendPlacementAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.sendPlacementAttributionEvent(aid).then(
+ { response ->
+ val errorMessage = "Invalid value: unable to send placement attribution event through Gecko Engine."
+ if (response == null) {
+ logger.error(errorMessage)
+ onException(
+ java.lang.IllegalStateException(errorMessage),
+ )
+ return@then GeckoResult()
+ }
+ onResult(response)
+ GeckoResult<Boolean>()
+ },
+ { throwable ->
+ logger.error("Sending placement attribution event failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.reportBackInStock]
+ */
+ override fun reportBackInStock(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ geckoSession.reportBackInStock(url).then(
+ { response ->
+ val errorMessage = "Invalid value: unable to report back in stock from Gecko Engine."
+ if (response == null) {
+ logger.error(errorMessage)
+ onException(
+ java.lang.IllegalStateException(errorMessage),
+ )
+ return@then GeckoResult()
+ }
+ onResult(response)
+ GeckoResult<String>()
+ },
+ { throwable ->
+ logger.error("Request for reporting back in stock failed.", throwable)
+ onException(throwable)
+ GeckoResult()
+ },
+ )
+ }
+
+ /**
+ * See [EngineSession.requestTranslate]
+ */
+ override fun requestTranslate(
+ fromLanguage: String,
+ toLanguage: String,
+ options: TranslationOptions?,
+ ) {
+ if (geckoSession.sessionTranslation == null) {
+ notifyObservers {
+ onTranslateException(
+ TranslationOperation.TRANSLATE,
+ TranslationError.MissingSessionCoordinator(),
+ )
+ }
+ return
+ }
+
+ var geckoOptions: GeckoViewTranslateSession.TranslationOptions? = null
+ if (options != null) {
+ geckoOptions =
+ GeckoViewTranslateSession.TranslationOptions.Builder()
+ .downloadModel(options.downloadModel).build()
+ }
+
+ geckoSession.sessionTranslation!!.translate(fromLanguage, toLanguage, geckoOptions).then({
+ notifyObservers {
+ onTranslateComplete(TranslationOperation.TRANSLATE)
+ }
+ GeckoResult<Void>()
+ }, {
+ throwable ->
+ logger.error("Request for translation failed: ", throwable)
+ notifyObservers {
+ onTranslateException(
+ TranslationOperation.TRANSLATE,
+ throwable.intoTranslationError(),
+ )
+ }
+ GeckoResult()
+ })
+ }
+
+ /**
+ * See [EngineSession.requestTranslationRestore]
+ */
+ override fun requestTranslationRestore() {
+ if (geckoSession.sessionTranslation == null) {
+ notifyObservers {
+ onTranslateException(
+ TranslationOperation.RESTORE,
+ TranslationError.MissingSessionCoordinator(),
+ )
+ }
+ return
+ }
+
+ geckoSession.sessionTranslation!!.restoreOriginalPage().then({
+ notifyObservers {
+ onTranslateComplete(TranslationOperation.RESTORE)
+ }
+ GeckoResult<Void>()
+ }, {
+ throwable ->
+ logger.error("Request for translation failed: ", throwable)
+ notifyObservers {
+ onTranslateException(TranslationOperation.RESTORE, throwable.intoTranslationError())
+ }
+ GeckoResult()
+ })
+ }
+
+ /**
+ * See [EngineSession.getNeverTranslateSiteSetting]
+ */
+ override fun getNeverTranslateSiteSetting(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ if (geckoSession.sessionTranslation == null) {
+ onException(TranslationError.MissingSessionCoordinator())
+ return
+ }
+
+ geckoSession.sessionTranslation!!.neverTranslateSiteSetting.then({
+ response ->
+ if (response == null) {
+ logger.error("Did not receive a site setting response.")
+ onException(
+ TranslationError.UnexpectedNull(),
+ )
+ return@then GeckoResult()
+ }
+ onResult(response)
+ GeckoResult<Boolean>()
+ }, {
+ throwable ->
+ logger.error("Request for site translation preference failed: ", throwable)
+ onException(throwable.intoTranslationError())
+ GeckoResult()
+ })
+ }
+
+ /**
+ * See [EngineSession.setNeverTranslateSiteSetting]
+ */
+ override fun setNeverTranslateSiteSetting(
+ setting: Boolean,
+ onResult: () -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ if (geckoSession.sessionTranslation == null) {
+ onException(TranslationError.MissingSessionCoordinator())
+ return
+ }
+
+ geckoSession.sessionTranslation!!.setNeverTranslateSiteSetting(setting).then({
+ onResult()
+ GeckoResult<Boolean>()
+ }, {
+ throwable ->
+ logger.error("Request for setting site translation preference failed: ", throwable)
+ onException(throwable.intoTranslationError())
+ GeckoResult()
+ })
+ }
+
+ /**
+ * Purges the history for the session (back and forward history).
+ */
+ override fun purgeHistory() {
+ geckoSession.purgeHistory()
+ }
+
+ /**
+ * See [EngineSession.close].
+ */
+ override fun close() {
+ super.close()
+ job.cancel()
+ geckoSession.close()
+ }
+
+ override fun getBlockedSchemes(): List<String> {
+ return BLOCKED_SCHEMES
+ }
+
+ /**
+ * NavigationDelegate implementation for forwarding callbacks to observers of the session.
+ */
+ @Suppress("ComplexMethod")
+ private fun createNavigationDelegate() = object : GeckoSession.NavigationDelegate {
+ override fun onLocationChange(
+ session: GeckoSession,
+ url: String?,
+ geckoPermissions: List<ContentPermission>,
+ hasUserGesture: Boolean,
+ ) {
+ this@GeckoEngineSession.geckoPermissions = geckoPermissions
+ if (url == null) {
+ return // ¯\_(ツ)_/¯
+ }
+
+ // Ignore initial loads of about:blank, see:
+ // https://github.com/mozilla-mobile/android-components/issues/403
+ // https://github.com/mozilla-mobile/android-components/issues/6832
+ if (initialLoad && url == ABOUT_BLANK) {
+ return
+ }
+
+ appRedirectUrl?.let {
+ if (url == appRedirectUrl) {
+ goBack(false)
+ return
+ }
+ }
+
+ currentUrl = url
+ initialLoad = false
+ initialLoadRequest = null
+
+ notifyObservers {
+ onExcludedOnTrackingProtectionChange(isIgnoredForTrackingProtection())
+ }
+ // Re-set the status of cookie banner handling when the user navigates to another site.
+ notifyObservers {
+ onCookieBannerChange(CookieBannerHandlingStatus.NO_DETECTED)
+ }
+ // Reset the status of current page being product or not when user navigates away.
+ notifyObservers { onProductUrlChange(false) }
+ notifyObservers { onLocationChange(url, hasUserGesture) }
+ }
+
+ override fun onLoadRequest(
+ session: GeckoSession,
+ request: NavigationDelegate.LoadRequest,
+ ): GeckoResult<AllowOrDeny> {
+ // The process switch involved when loading extension pages will
+ // trigger an initial load of about:blank which we want to
+ // avoid:
+ // https://github.com/mozilla-mobile/android-components/issues/6832
+ // https://github.com/mozilla-mobile/android-components/issues/403
+ if (currentUrl?.isExtensionUrl() != request.uri.isExtensionUrl()) {
+ initialLoad = true
+ }
+
+ return when {
+ maybeInterceptRequest(request, false) != null ->
+ GeckoResult.fromValue(AllowOrDeny.DENY)
+ request.target == NavigationDelegate.TARGET_WINDOW_NEW ->
+ GeckoResult.fromValue(AllowOrDeny.ALLOW)
+ else -> {
+ notifyObservers {
+ onLoadRequest(
+ url = request.uri,
+ triggeredByRedirect = request.isRedirect,
+ triggeredByWebContent = request.hasUserGesture,
+ )
+ }
+
+ GeckoResult.fromValue(AllowOrDeny.ALLOW)
+ }
+ }
+ }
+
+ override fun onSubframeLoadRequest(
+ session: GeckoSession,
+ request: NavigationDelegate.LoadRequest,
+ ): GeckoResult<AllowOrDeny> {
+ if (request.target == NavigationDelegate.TARGET_WINDOW_NEW) {
+ return GeckoResult.fromValue(AllowOrDeny.ALLOW)
+ }
+
+ return if (maybeInterceptRequest(request, true) != null) {
+ GeckoResult.fromValue(AllowOrDeny.DENY)
+ } else {
+ // Not notifying session observer because of performance concern and currently there
+ // is no use case.
+ GeckoResult.fromValue(AllowOrDeny.ALLOW)
+ }
+ }
+
+ override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
+ notifyObservers { onNavigationStateChange(canGoForward = canGoForward) }
+ this@GeckoEngineSession.canGoForward = canGoForward
+ }
+
+ override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
+ notifyObservers { onNavigationStateChange(canGoBack = canGoBack) }
+ this@GeckoEngineSession.canGoBack = canGoBack
+ }
+
+ override fun onNewSession(
+ session: GeckoSession,
+ uri: String,
+ ): GeckoResult<GeckoSession> {
+ val newEngineSession =
+ GeckoEngineSession(runtime, privateMode, defaultSettings, openGeckoSession = false)
+ notifyObservers {
+ onWindowRequest(GeckoWindowRequest(uri, newEngineSession))
+ }
+ return GeckoResult.fromValue(newEngineSession.geckoSession)
+ }
+
+ override fun onLoadError(
+ session: GeckoSession,
+ uri: String?,
+ error: WebRequestError,
+ ): GeckoResult<String> {
+ val response = settings.requestInterceptor?.onErrorRequest(
+ this@GeckoEngineSession,
+ geckoErrorToErrorType(error.code),
+ uri,
+ )
+ return GeckoResult.fromValue(response?.uri)
+ }
+
+ private fun maybeInterceptRequest(
+ request: NavigationDelegate.LoadRequest,
+ isSubframeRequest: Boolean,
+ ): InterceptionResponse? {
+ if (request.hasUserGesture) {
+ lastLoadRequestUri = ""
+ }
+
+ val interceptor = settings.requestInterceptor
+ val interceptionResponse = if (
+ interceptor != null && (!request.isDirectNavigation || interceptor.interceptsAppInitiatedRequests())
+ ) {
+ val engineSession = this@GeckoEngineSession
+ val isSameDomain =
+ engineSession.currentUrl?.tryGetHostFromUrl() == request.uri.tryGetHostFromUrl()
+ interceptor.onLoadRequest(
+ engineSession,
+ request.uri,
+ lastLoadRequestUri,
+ request.hasUserGesture,
+ isSameDomain,
+ request.isRedirect,
+ request.isDirectNavigation,
+ isSubframeRequest,
+ )?.apply {
+ when (this) {
+ is InterceptionResponse.Content -> loadData(data, mimeType, encoding)
+ is InterceptionResponse.Url -> loadUrl(
+ url = url,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ )
+ is InterceptionResponse.AppIntent -> {
+ appRedirectUrl = lastLoadRequestUri
+ notifyObservers {
+ onLaunchIntentRequest(url = url, appIntent = appIntent)
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+ } else {
+ null
+ }
+
+ if (interceptionResponse !is InterceptionResponse.AppIntent) {
+ appRedirectUrl = ""
+ }
+
+ lastLoadRequestUri = request.uri
+ return interceptionResponse
+ }
+ }
+
+ /**
+ * ProgressDelegate implementation for forwarding callbacks to observers of the session.
+ */
+ private fun createProgressDelegate() = object : GeckoSession.ProgressDelegate {
+ override fun onProgressChange(session: GeckoSession, progress: Int) {
+ notifyObservers { onProgress(progress) }
+ }
+
+ override fun onSecurityChange(
+ session: GeckoSession,
+ securityInfo: GeckoSession.ProgressDelegate.SecurityInformation,
+ ) {
+ // Ignore initial load of about:blank (see https://github.com/mozilla-mobile/android-components/issues/403)
+ if (initialLoad && securityInfo.origin?.startsWith(MOZ_NULL_PRINCIPAL) == true) {
+ return
+ }
+
+ notifyObservers {
+ // TODO provide full certificate info: https://github.com/mozilla-mobile/android-components/issues/5557
+ onSecurityChange(
+ securityInfo.isSecure,
+ securityInfo.host,
+ securityInfo.getIssuerName(),
+ )
+ }
+ }
+
+ override fun onPageStart(session: GeckoSession, url: String) {
+ // This log statement is temporary and parsed by FNPRMS for performance measurements. It can be
+ // removed once FNPRMS is replaced: https://github.com/mozilla-mobile/android-components/issues/8662
+ fnprmsLogger.info("handleMessage GeckoView:PageStart uri=") // uri intentionally blank
+
+ pageLoadingUrl = url
+
+ // Ignore initial load of about:blank (see https://github.com/mozilla-mobile/android-components/issues/403)
+ if (initialLoad && url == ABOUT_BLANK) {
+ return
+ }
+
+ notifyObservers {
+ onProgress(PROGRESS_START)
+ onLoadingStateChange(true)
+ }
+ }
+
+ override fun onPageStop(session: GeckoSession, success: Boolean) {
+ // This log statement is temporary and parsed by FNPRMS for performance measurements. It can be
+ // removed once FNPRMS is replaced: https://github.com/mozilla-mobile/android-components/issues/8662
+ fnprmsLogger.info("handleMessage GeckoView:PageStop uri=null") // uri intentionally hard-coded to null
+ // by the time we reach here, any new request will come from web content.
+ // If it comes from the chrome, loadUrl(url) or loadData(string) will set it to
+ // false.
+
+ // Ignore initial load of about:blank (see https://github.com/mozilla-mobile/android-components/issues/403)
+ if (initialLoad && pageLoadingUrl == ABOUT_BLANK) {
+ return
+ }
+
+ notifyObservers {
+ onProgress(PROGRESS_STOP)
+ onLoadingStateChange(false)
+ }
+ }
+
+ override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
+ notifyObservers {
+ onStateUpdated(GeckoEngineSessionState(sessionState))
+ }
+ }
+ }
+
+ @Suppress("ComplexMethod")
+ internal fun createHistoryDelegate() = object : GeckoSession.HistoryDelegate {
+ @SuppressWarnings("ReturnCount")
+ override fun onVisited(
+ session: GeckoSession,
+ url: String,
+ lastVisitedURL: String?,
+ flags: Int,
+ ): GeckoResult<Boolean>? {
+ // Don't track:
+ // - private visits
+ // - error pages
+ // - non-top level visits (i.e. iframes).
+ if (privateMode ||
+ (flags and GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL) == 0 ||
+ (flags and GeckoSession.HistoryDelegate.VISIT_UNRECOVERABLE_ERROR) != 0
+ ) {
+ return GeckoResult.fromValue(false)
+ }
+
+ appRedirectUrl?.let {
+ if (url == appRedirectUrl) {
+ return GeckoResult.fromValue(false)
+ }
+ }
+
+ val delegate = settings.historyTrackingDelegate ?: return GeckoResult.fromValue(false)
+
+ // Check if the delegate wants this type of url.
+ if (!delegate.shouldStoreUri(url)) {
+ return GeckoResult.fromValue(false)
+ }
+
+ val isReload = lastVisitedURL?.let { it == url } ?: false
+
+ // Note the difference between `VISIT_REDIRECT_PERMANENT`,
+ // `VISIT_REDIRECT_TEMPORARY`, `VISIT_REDIRECT_SOURCE`, and
+ // `VISIT_REDIRECT_SOURCE_PERMANENT`.
+ //
+ // The former two indicate if the visited page is the *target*
+ // of a redirect; that is, another page redirected to it.
+ //
+ // The latter two indicate if the visited page is the *source*
+ // of a redirect: it's redirecting to another page, because the
+ // server returned an HTTP 3xy status code.
+ //
+ // So, we mark the **source** redirects as actual redirects, while treating **target**
+ // redirects as normal visits.
+ val visitType = when {
+ isReload -> VisitType.RELOAD
+ flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE_PERMANENT != 0 ->
+ VisitType.REDIRECT_PERMANENT
+ flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE != 0 ->
+ VisitType.REDIRECT_TEMPORARY
+ else -> VisitType.LINK
+ }
+ val redirectSource = when {
+ flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE_PERMANENT != 0 ->
+ RedirectSource.PERMANENT
+ flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE != 0 ->
+ RedirectSource.TEMPORARY
+ else -> null
+ }
+
+ return launchGeckoResult {
+ delegate.onVisited(url, PageVisit(visitType, redirectSource))
+ true
+ }
+ }
+
+ override fun getVisited(
+ session: GeckoSession,
+ urls: Array<out String>,
+ ): GeckoResult<BooleanArray>? {
+ if (privateMode) {
+ return GeckoResult.fromValue(null)
+ }
+
+ val delegate = settings.historyTrackingDelegate ?: return GeckoResult.fromValue(null)
+
+ return launchGeckoResult {
+ val visits = delegate.getVisited(urls.toList())
+ visits.toBooleanArray()
+ }
+ }
+
+ override fun onHistoryStateChange(
+ session: GeckoSession,
+ historyList: GeckoSession.HistoryDelegate.HistoryList,
+ ) {
+ val items = historyList.map {
+ // title is sometimes null despite the @NotNull annotation
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1660286
+ val title: String? = it.title
+ HistoryItem(
+ title = title ?: it.uri,
+ uri = it.uri,
+ )
+ }
+ notifyObservers { onHistoryStateChanged(items, historyList.currentIndex) }
+ }
+ }
+
+ @Suppress("ComplexMethod", "NestedBlockDepth")
+ internal fun createContentDelegate() = object : GeckoSession.ContentDelegate {
+ override fun onCookieBannerDetected(session: GeckoSession) {
+ notifyObservers { onCookieBannerChange(CookieBannerHandlingStatus.DETECTED) }
+ }
+
+ override fun onCookieBannerHandled(session: GeckoSession) {
+ notifyObservers { onCookieBannerChange(CookieBannerHandlingStatus.HANDLED) }
+ }
+
+ override fun onProductUrl(session: GeckoSession) {
+ notifyObservers { onProductUrlChange(true) }
+ }
+
+ override fun onFirstComposite(session: GeckoSession) = Unit
+
+ override fun onFirstContentfulPaint(session: GeckoSession) {
+ notifyObservers { onFirstContentfulPaint() }
+ }
+
+ override fun onPaintStatusReset(session: GeckoSession) {
+ notifyObservers { onPaintStatusReset() }
+ }
+
+ override fun onContextMenu(
+ session: GeckoSession,
+ screenX: Int,
+ screenY: Int,
+ element: GeckoSession.ContentDelegate.ContextElement,
+ ) {
+ val hitResult = handleLongClick(element.srcUri, element.type, element.linkUri, element.title)
+ hitResult?.let {
+ notifyObservers { onLongPress(it) }
+ }
+ }
+
+ override fun onCrash(session: GeckoSession) {
+ notifyObservers { onCrash() }
+ }
+
+ override fun onKill(session: GeckoSession) {
+ notifyObservers {
+ onProcessKilled()
+ }
+ }
+
+ override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
+ notifyObservers { onFullScreenChange(fullScreen) }
+ }
+
+ override fun onExternalResponse(session: GeckoSession, webResponse: WebResponse) {
+ with(webResponse) {
+ val contentType = headers[CONTENT_TYPE]?.trim()
+ val contentLength = headers[CONTENT_LENGTH]?.trim()?.toLongOrNull()
+ val contentDisposition = headers[CONTENT_DISPOSITION]?.trim()
+ val url = uri
+ val fileName = DownloadUtils.guessFileName(
+ contentDisposition,
+ destinationDirectory = null,
+ url = url,
+ mimeType = contentType,
+ )
+ val response = webResponse.toResponse()
+ notifyObservers {
+ onExternalResource(
+ url = url,
+ contentLength = contentLength,
+ contentType = DownloadUtils.sanitizeMimeType(contentType),
+ fileName = fileName.sanitizeFileName(),
+ response = response,
+ isPrivate = privateMode,
+ openInApp = webResponse.requestExternalApp,
+ skipConfirmation = webResponse.skipConfirmation,
+ )
+ }
+ }
+ }
+
+ override fun onCloseRequest(session: GeckoSession) {
+ notifyObservers {
+ onWindowRequest(
+ GeckoWindowRequest(
+ engineSession = this@GeckoEngineSession,
+ type = WindowRequest.Type.CLOSE,
+ ),
+ )
+ }
+ }
+
+ override fun onTitleChange(session: GeckoSession, title: String?) {
+ if (appRedirectUrl.isNullOrEmpty()) {
+ if (!privateMode) {
+ currentUrl?.let { url ->
+ settings.historyTrackingDelegate?.let { delegate ->
+ if (delegate.shouldStoreUri(url)) {
+ // NB: There's no guarantee that the title change will be processed by the
+ // delegate before the session is closed (and the corresponding coroutine
+ // job is cancelled). Observers will always be notified of the title
+ // change though.
+ launch(coroutineContext) {
+ delegate.onTitleChanged(url, title ?: "")
+ }
+ }
+ }
+ }
+ }
+ this@GeckoEngineSession.currentTitle = title
+ notifyObservers { onTitleChange(title ?: "") }
+ }
+ }
+
+ override fun onPreviewImage(session: GeckoSession, previewImageUrl: String) {
+ if (!privateMode) {
+ currentUrl?.let { url ->
+ settings.historyTrackingDelegate?.let { delegate ->
+ if (delegate.shouldStoreUri(url)) {
+ launch(coroutineContext) {
+ delegate.onPreviewImageChange(url, previewImageUrl)
+ }
+ }
+ }
+ }
+ }
+ notifyObservers { onPreviewImageChange(previewImageUrl) }
+ }
+
+ override fun onFocusRequest(session: GeckoSession) = Unit
+
+ override fun onWebAppManifest(session: GeckoSession, manifest: JSONObject) {
+ val parsed = WebAppManifestParser().parse(manifest)
+ if (parsed is WebAppManifestParser.Result.Success) {
+ notifyObservers { onWebAppManifestLoaded(parsed.manifest) }
+ }
+ }
+
+ override fun onMetaViewportFitChange(session: GeckoSession, viewportFit: String) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ val layoutInDisplayCutoutMode = when (viewportFit) {
+ "cover" -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+ "contain" -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
+ else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ }
+
+ notifyObservers { onMetaViewportFitChanged(layoutInDisplayCutoutMode) }
+ }
+ }
+
+ override fun onShowDynamicToolbar(geckoSession: GeckoSession) {
+ notifyObservers { onShowDynamicToolbar() }
+ }
+ }
+
+ private fun createContentBlockingDelegate() = object : ContentBlocking.Delegate {
+ override fun onContentBlocked(session: GeckoSession, event: ContentBlocking.BlockEvent) {
+ notifyObservers {
+ onTrackerBlocked(event.toTracker())
+ }
+ }
+
+ override fun onContentLoaded(session: GeckoSession, event: ContentBlocking.BlockEvent) {
+ notifyObservers {
+ onTrackerLoaded(event.toTracker())
+ }
+ }
+ }
+
+ private fun ContentBlocking.BlockEvent.toTracker(): Tracker {
+ val blockedContentCategories = mutableListOf<TrackingProtectionPolicy.TrackingCategory>()
+
+ if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.AD)) {
+ blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.AD)
+ }
+
+ if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.ANALYTIC)) {
+ blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.ANALYTICS)
+ }
+
+ if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.SOCIAL)) {
+ blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.SOCIAL)
+ }
+
+ if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.FINGERPRINTING)) {
+ blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.FINGERPRINTING)
+ }
+
+ if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.CRYPTOMINING)) {
+ blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.CRYPTOMINING)
+ }
+
+ if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.CONTENT)) {
+ blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.CONTENT)
+ }
+
+ if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.TEST)) {
+ blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.TEST)
+ }
+
+ return Tracker(
+ url = uri,
+ trackingCategories = blockedContentCategories,
+ cookiePolicies = getCookiePolicies(),
+ )
+ }
+
+ private fun ContentBlocking.BlockEvent.getCookiePolicies(): List<TrackingProtectionPolicy.CookiePolicy> {
+ val cookiesPolicies = mutableListOf<TrackingProtectionPolicy.CookiePolicy>()
+
+ if (cookieBehaviorCategory == ContentBlocking.CookieBehavior.ACCEPT_ALL) {
+ cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_ALL)
+ }
+
+ if (cookieBehaviorCategory.contains(ContentBlocking.CookieBehavior.ACCEPT_FIRST_PARTY)) {
+ cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_ONLY_FIRST_PARTY)
+ }
+
+ if (cookieBehaviorCategory.contains(ContentBlocking.CookieBehavior.ACCEPT_NONE)) {
+ cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_NONE)
+ }
+
+ if (cookieBehaviorCategory.contains(ContentBlocking.CookieBehavior.ACCEPT_NON_TRACKERS)) {
+ cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_NON_TRACKERS)
+ }
+
+ if (cookieBehaviorCategory.contains(ContentBlocking.CookieBehavior.ACCEPT_VISITED)) {
+ cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_VISITED)
+ }
+
+ return cookiesPolicies
+ }
+
+ internal fun GeckoSession.ProgressDelegate.SecurityInformation.getIssuerName(): String? {
+ return certificate?.issuerDN?.name?.substringAfterLast("O=")?.substringBeforeLast(",C=")
+ }
+
+ private operator fun Int.contains(mask: Int): Boolean {
+ return (this and mask) != 0
+ }
+
+ private fun createPermissionDelegate() = object : GeckoSession.PermissionDelegate {
+ override fun onContentPermissionRequest(
+ session: GeckoSession,
+ geckoContentPermission: ContentPermission,
+ ): GeckoResult<Int> {
+ val geckoResult = GeckoResult<Int>()
+ val uri = geckoContentPermission.uri
+ val type = geckoContentPermission.permission
+ val request = GeckoPermissionRequest.Content(uri, type, geckoContentPermission, geckoResult)
+ notifyObservers { onContentPermissionRequest(request) }
+ return geckoResult
+ }
+
+ override fun onMediaPermissionRequest(
+ session: GeckoSession,
+ uri: String,
+ video: Array<out GeckoSession.PermissionDelegate.MediaSource>?,
+ audio: Array<out GeckoSession.PermissionDelegate.MediaSource>?,
+ callback: GeckoSession.PermissionDelegate.MediaCallback,
+ ) {
+ val request = GeckoPermissionRequest.Media(
+ uri,
+ video?.toList() ?: emptyList(),
+ audio?.toList() ?: emptyList(),
+ callback,
+ )
+ notifyObservers { onContentPermissionRequest(request) }
+ }
+
+ override fun onAndroidPermissionsRequest(
+ session: GeckoSession,
+ permissions: Array<out String>?,
+ callback: GeckoSession.PermissionDelegate.Callback,
+ ) {
+ val request = GeckoPermissionRequest.App(
+ permissions?.toList() ?: emptyList(),
+ callback,
+ )
+ notifyObservers { onAppPermissionRequest(request) }
+ }
+ }
+
+ private fun createScrollDelegate() = object : GeckoSession.ScrollDelegate {
+ override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
+ this@GeckoEngineSession.scrollY = scrollY
+ notifyObservers { onScrollChange(scrollX, scrollY) }
+ }
+ }
+
+ @Suppress("ComplexMethod")
+ fun handleLongClick(elementSrc: String?, elementType: Int, uri: String? = null, title: String? = null): HitResult? {
+ return when (elementType) {
+ GeckoSession.ContentDelegate.ContextElement.TYPE_AUDIO ->
+ elementSrc?.let {
+ HitResult.AUDIO(it, title)
+ }
+ GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO ->
+ elementSrc?.let {
+ HitResult.VIDEO(it, title)
+ }
+ GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE -> {
+ when {
+ elementSrc != null && uri != null ->
+ HitResult.IMAGE_SRC(elementSrc, uri)
+ elementSrc != null ->
+ HitResult.IMAGE(elementSrc, title)
+ else -> HitResult.UNKNOWN("")
+ }
+ }
+ GeckoSession.ContentDelegate.ContextElement.TYPE_NONE -> {
+ elementSrc?.let {
+ when {
+ it.isPhone() -> HitResult.PHONE(it)
+ it.isEmail() -> HitResult.EMAIL(it)
+ it.isGeoLocation() -> HitResult.GEO(it)
+ else -> HitResult.UNKNOWN(it)
+ }
+ } ?: uri?.let {
+ HitResult.UNKNOWN(it)
+ }
+ }
+ else -> HitResult.UNKNOWN("")
+ }
+ }
+
+ private fun createGeckoSession(shouldOpen: Boolean = true) {
+ this.geckoSession = geckoSessionProvider()
+
+ defaultSettings?.trackingProtectionPolicy?.let { updateTrackingProtection(it) }
+ defaultSettings?.requestInterceptor?.let { settings.requestInterceptor = it }
+ defaultSettings?.historyTrackingDelegate?.let { settings.historyTrackingDelegate = it }
+ defaultSettings?.testingModeEnabled?.let {
+ geckoSession.settings.fullAccessibilityTree = it
+ }
+ defaultSettings?.userAgentString?.let { geckoSession.settings.userAgentOverride = it }
+ defaultSettings?.suspendMediaWhenInactive?.let {
+ geckoSession.settings.suspendMediaWhenInactive = it
+ }
+ defaultSettings?.clearColor?.let { geckoSession.compositorController.clearColor = it }
+
+ if (shouldOpen) {
+ geckoSession.open(runtime)
+ }
+
+ geckoSession.navigationDelegate = createNavigationDelegate()
+ geckoSession.progressDelegate = createProgressDelegate()
+ geckoSession.contentDelegate = createContentDelegate()
+ geckoSession.contentBlockingDelegate = createContentBlockingDelegate()
+ geckoSession.permissionDelegate = createPermissionDelegate()
+ geckoSession.promptDelegate = GeckoPromptDelegate(this)
+ geckoSession.mediaDelegate = GeckoMediaDelegate(this)
+ geckoSession.historyDelegate = createHistoryDelegate()
+ geckoSession.mediaSessionDelegate = GeckoMediaSessionDelegate(this)
+ geckoSession.scrollDelegate = createScrollDelegate()
+ geckoSession.translationsSessionDelegate = GeckoTranslateSessionDelegate(this)
+ }
+
+ companion object {
+ internal const val PROGRESS_START = 25
+ internal const val PROGRESS_STOP = 100
+ internal const val MOZ_NULL_PRINCIPAL = "moz-nullprincipal:"
+ internal const val ABOUT_BLANK = "about:blank"
+ internal const val JS_SCHEME = "javascript"
+ internal val BLOCKED_SCHEMES =
+ listOf("file", "resource", JS_SCHEME) // See 1684761 and 1684947
+
+ /**
+ * Provides an ErrorType corresponding to the error code provided.
+ */
+ @Suppress("ComplexMethod")
+ internal fun geckoErrorToErrorType(errorCode: Int) =
+ when (errorCode) {
+ WebRequestError.ERROR_UNKNOWN -> ErrorType.UNKNOWN
+ WebRequestError.ERROR_SECURITY_SSL -> ErrorType.ERROR_SECURITY_SSL
+ WebRequestError.ERROR_SECURITY_BAD_CERT -> ErrorType.ERROR_SECURITY_BAD_CERT
+ WebRequestError.ERROR_NET_INTERRUPT -> ErrorType.ERROR_NET_INTERRUPT
+ WebRequestError.ERROR_NET_TIMEOUT -> ErrorType.ERROR_NET_TIMEOUT
+ WebRequestError.ERROR_CONNECTION_REFUSED -> ErrorType.ERROR_CONNECTION_REFUSED
+ WebRequestError.ERROR_UNKNOWN_SOCKET_TYPE -> ErrorType.ERROR_UNKNOWN_SOCKET_TYPE
+ WebRequestError.ERROR_REDIRECT_LOOP -> ErrorType.ERROR_REDIRECT_LOOP
+ WebRequestError.ERROR_OFFLINE -> ErrorType.ERROR_OFFLINE
+ WebRequestError.ERROR_PORT_BLOCKED -> ErrorType.ERROR_PORT_BLOCKED
+ WebRequestError.ERROR_NET_RESET -> ErrorType.ERROR_NET_RESET
+ WebRequestError.ERROR_UNSAFE_CONTENT_TYPE -> ErrorType.ERROR_UNSAFE_CONTENT_TYPE
+ WebRequestError.ERROR_CORRUPTED_CONTENT -> ErrorType.ERROR_CORRUPTED_CONTENT
+ WebRequestError.ERROR_CONTENT_CRASHED -> ErrorType.ERROR_CONTENT_CRASHED
+ WebRequestError.ERROR_INVALID_CONTENT_ENCODING -> ErrorType.ERROR_INVALID_CONTENT_ENCODING
+ WebRequestError.ERROR_UNKNOWN_HOST -> ErrorType.ERROR_UNKNOWN_HOST
+ WebRequestError.ERROR_MALFORMED_URI -> ErrorType.ERROR_MALFORMED_URI
+ WebRequestError.ERROR_UNKNOWN_PROTOCOL -> ErrorType.ERROR_UNKNOWN_PROTOCOL
+ WebRequestError.ERROR_FILE_NOT_FOUND -> ErrorType.ERROR_FILE_NOT_FOUND
+ WebRequestError.ERROR_FILE_ACCESS_DENIED -> ErrorType.ERROR_FILE_ACCESS_DENIED
+ WebRequestError.ERROR_PROXY_CONNECTION_REFUSED -> ErrorType.ERROR_PROXY_CONNECTION_REFUSED
+ WebRequestError.ERROR_UNKNOWN_PROXY_HOST -> ErrorType.ERROR_UNKNOWN_PROXY_HOST
+ WebRequestError.ERROR_SAFEBROWSING_MALWARE_URI -> ErrorType.ERROR_SAFEBROWSING_MALWARE_URI
+ WebRequestError.ERROR_SAFEBROWSING_UNWANTED_URI -> ErrorType.ERROR_SAFEBROWSING_UNWANTED_URI
+ WebRequestError.ERROR_SAFEBROWSING_HARMFUL_URI -> ErrorType.ERROR_SAFEBROWSING_HARMFUL_URI
+ WebRequestError.ERROR_SAFEBROWSING_PHISHING_URI -> ErrorType.ERROR_SAFEBROWSING_PHISHING_URI
+ WebRequestError.ERROR_HTTPS_ONLY -> ErrorType.ERROR_HTTPS_ONLY
+ WebRequestError.ERROR_BAD_HSTS_CERT -> ErrorType.ERROR_BAD_HSTS_CERT
+ else -> ErrorType.UNKNOWN
+ }
+ }
+}
+
+/**
+ * Provides all gecko flags ignoring flags that only exists on AC.
+ **/
+@VisibleForTesting
+internal fun EngineSession.LoadUrlFlags.getGeckoFlags(): Int {
+ var newValue = value
+
+ if (contains(ALLOW_ADDITIONAL_HEADERS)) {
+ newValue -= ALLOW_ADDITIONAL_HEADERS
+ }
+
+ if (contains(ALLOW_JAVASCRIPT_URL)) {
+ newValue -= ALLOW_JAVASCRIPT_URL
+ }
+
+ return newValue
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionState.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionState.kt
new file mode 100644
index 0000000000..e457ab2ac3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionState.kt
@@ -0,0 +1,73 @@
+/* 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.browser.engine.gecko
+
+import android.util.JsonReader
+import android.util.JsonWriter
+import mozilla.components.concept.engine.EngineSessionState
+import org.json.JSONException
+import org.json.JSONObject
+import org.mozilla.geckoview.GeckoSession
+import java.io.IOException
+
+private const val GECKO_STATE_KEY = "GECKO_STATE"
+
+class GeckoEngineSessionState internal constructor(
+ internal val actualState: GeckoSession.SessionState?,
+) : EngineSessionState {
+ override fun writeTo(writer: JsonWriter) {
+ with(writer) {
+ beginObject()
+
+ name(GECKO_STATE_KEY)
+ value(actualState.toString())
+
+ endObject()
+ flush()
+ }
+ }
+
+ companion object {
+ fun fromJSON(json: JSONObject): GeckoEngineSessionState = try {
+ val state = json.getString(GECKO_STATE_KEY)
+
+ GeckoEngineSessionState(
+ GeckoSession.SessionState.fromString(state),
+ )
+ } catch (e: JSONException) {
+ GeckoEngineSessionState(null)
+ }
+
+ /**
+ * Creates a [GeckoEngineSessionState] from the given [JsonReader].
+ */
+ fun from(reader: JsonReader): GeckoEngineSessionState = try {
+ reader.beginObject()
+
+ val rawState = if (reader.hasNext()) {
+ val key = reader.nextName()
+ if (key != GECKO_STATE_KEY) {
+ throw AssertionError("Unknown state key: $key")
+ }
+
+ reader.nextString()
+ } else {
+ null
+ }
+
+ reader.endObject()
+
+ GeckoEngineSessionState(
+ rawState?.let { GeckoSession.SessionState.fromString(it) },
+ )
+ } catch (e: IOException) {
+ GeckoEngineSessionState(null)
+ } catch (e: JSONException) {
+ // Internally GeckoView uses org.json and currently may throw JSONException in certain cases
+ // https://github.com/mozilla-mobile/android-components/issues/9332
+ GeckoEngineSessionState(null)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineView.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineView.kt
new file mode 100644
index 0000000000..d5d77b3073
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineView.kt
@@ -0,0 +1,249 @@
+/* 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.browser.engine.gecko
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import androidx.annotation.VisibleForTesting
+import androidx.core.view.ViewCompat
+import mozilla.components.browser.engine.gecko.activity.GeckoViewActivityContextDelegate
+import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import org.mozilla.geckoview.BasicSelectionActionDelegate
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoSession
+import java.lang.ref.WeakReference
+
+/**
+ * Gecko-based EngineView implementation.
+ */
+class GeckoEngineView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr), EngineView {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var geckoView = object : NestedGeckoView(context) {
+
+ override fun onAttachedToWindow() {
+ try {
+ super.onAttachedToWindow()
+ } catch (e: IllegalStateException) {
+ // This is to debug "display already acquired" crashes
+ val otherActivityClassName =
+ this.session?.accessibility?.view?.context?.javaClass?.simpleName
+ val otherActivityClassHashcode =
+ this.session?.accessibility?.view?.context?.hashCode()
+ val activityClassName = context.javaClass.simpleName
+ val activityClassHashCode = context.hashCode()
+ val msg = "ATTACH VIEW: Current activity: $activityClassName hashcode " +
+ "$activityClassHashCode Other activity: $otherActivityClassName " +
+ "hashcode $otherActivityClassHashcode"
+ throw IllegalStateException(msg, e)
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ // We are releasing the session before GeckoView gets detached from the window. Otherwise
+ // GeckoView will close the session automatically and we do not want that.
+ releaseSession()
+
+ super.onDetachedFromWindow()
+ }
+ }.apply {
+ // Explicitly mark this view as important for autofill. The default "auto" doesn't seem to trigger any
+ // autofill behavior for us here.
+ @Suppress("WrongConstant")
+ ViewCompat.setImportantForAutofill(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES)
+ }
+
+ internal fun setColorScheme(preferredColorScheme: PreferredColorScheme) {
+ var colorScheme = preferredColorScheme
+ if (preferredColorScheme == PreferredColorScheme.System) {
+ colorScheme =
+ if (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+ == Configuration.UI_MODE_NIGHT_YES
+ ) {
+ PreferredColorScheme.Dark
+ } else {
+ PreferredColorScheme.Light
+ }
+ }
+
+ if (colorScheme == PreferredColorScheme.Dark) {
+ geckoView.coverUntilFirstPaint(DARK_COVER)
+ } else {
+ geckoView.coverUntilFirstPaint(Color.WHITE)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var currentSession: GeckoEngineSession? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var currentSelection: BasicSelectionActionDelegate? = null
+
+ override var selectionActionDelegate: SelectionActionDelegate? = null
+
+ init {
+ addView(geckoView)
+
+ /**
+ * With the current design, we have a [NestedGeckoView] inside this
+ * [GeckoEngineView]. In our supported embedders, we wrap this with the
+ * AndroidX `SwipeRefreshLayout` to enable features like Pull-To-Refresh:
+ *
+ * ```
+ * SwipeRefreshLayout
+ * └── GeckoEngineView
+ * └── NestedGeckoView
+ * ```
+ *
+ * `SwipeRefreshLayout` only looks at the direct child to see if it has nested scrolling
+ * enabled. As we embed [NestedGeckoView] inside [GeckoEngineView], we change the hierarchy
+ * so that [NestedGeckoView] is no longer the direct child of `SwipeRefreshLayout`.
+ *
+ * To fix this we enable nested scrolling on the GeckoEngineView to emulate this
+ * information. This is required information for `View.requestDisallowInterceptTouchEvent`
+ * to work correctly in the [NestedGeckoView].
+ */
+ isNestedScrollingEnabled = true
+ }
+
+ /**
+ * Render the content of the given session.
+ */
+ @Synchronized
+ override fun render(session: EngineSession) {
+ val internalSession = session as GeckoEngineSession
+ currentSession = session
+
+ if (geckoView.session != internalSession.geckoSession) {
+ geckoView.session?.let {
+ // Release a previously assigned session. Otherwise GeckoView will close it
+ // automatically.
+ detachSelectionActionDelegate(it)
+ geckoView.releaseSession()
+ }
+
+ try {
+ geckoView.setSession(internalSession.geckoSession)
+ attachSelectionActionDelegate(internalSession.geckoSession)
+ } catch (e: IllegalStateException) {
+ // This is to debug "display already acquired" crashes
+ val otherActivityClassName =
+ internalSession.geckoSession.accessibility.view?.context?.javaClass?.simpleName
+ val otherActivityClassHashcode =
+ internalSession.geckoSession.accessibility.view?.context?.hashCode()
+ val activityClassName = context.javaClass.simpleName
+ val activityClassHashCode = context.hashCode()
+ val msg = "SET SESSION: Current activity: $activityClassName hashcode " +
+ "$activityClassHashCode Other activity: $otherActivityClassName " +
+ "hashcode $otherActivityClassHashcode"
+ throw IllegalStateException(msg, e)
+ }
+ }
+ }
+
+ private fun attachSelectionActionDelegate(session: GeckoSession) {
+ val delegate = GeckoSelectionActionDelegate.maybeCreate(context, selectionActionDelegate)
+ if (delegate != null) {
+ session.selectionActionDelegate = delegate
+ currentSelection = delegate
+ }
+ }
+
+ private fun detachSelectionActionDelegate(session: GeckoSession?) {
+ if (currentSelection != null) {
+ session?.selectionActionDelegate = null
+ currentSelection = null
+ }
+ }
+
+ @Synchronized
+ override fun release() {
+ detachSelectionActionDelegate(currentSession?.geckoSession)
+
+ currentSession = null
+
+ geckoView.releaseSession()
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+
+ release()
+ }
+
+ override fun canClearSelection() = !currentSelection?.selection?.text.isNullOrEmpty()
+
+ override fun canScrollVerticallyUp() = currentSession?.let { it.scrollY > 0 } != false
+
+ override fun canScrollVerticallyDown() =
+ true // waiting for this issue https://bugzilla.mozilla.org/show_bug.cgi?id=1507569
+
+ override fun getInputResultDetail() = geckoView.inputResultDetail
+
+ override fun setVerticalClipping(clippingHeight: Int) {
+ geckoView.setVerticalClipping(clippingHeight)
+ }
+
+ override fun setDynamicToolbarMaxHeight(height: Int) {
+ geckoView.setDynamicToolbarMaxHeight(height)
+ }
+
+ override fun setActivityContext(context: Context?) {
+ geckoView.activityContextDelegate = GeckoViewActivityContextDelegate(WeakReference(context))
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) {
+ try {
+ val geckoResult = geckoView.capturePixels()
+ geckoResult.then(
+ { bitmap ->
+ onFinish(bitmap)
+ GeckoResult<Void>()
+ },
+ {
+ onFinish(null)
+ GeckoResult<Void>()
+ },
+ )
+ } catch (e: Exception) {
+ // There's currently no reliable way for consumers of GeckoView to
+ // know whether or not the compositor is ready. So we have to add
+ // a catch-all here. In the future, GeckoView will invoke our error
+ // callback instead and this block can be removed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1645114
+ // https://github.com/mozilla-mobile/android-components/issues/6680
+ onFinish(null)
+ }
+ }
+
+ override fun clearSelection() {
+ currentSelection?.clearSelection()
+ }
+
+ override fun setVisibility(visibility: Int) {
+ // GeckoView doesn't react to onVisibilityChanged so we need to propagate ourselves for now:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1630775
+ // We do this to prevent the content from resizing when the view is not visible:
+ // https://github.com/mozilla-mobile/android-components/issues/6664
+ geckoView.visibility = visibility
+ super.setVisibility(visibility)
+ }
+
+ companion object {
+ internal const val DARK_COVER = 0xFF2A2A2E.toInt()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoResult.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoResult.kt
new file mode 100644
index 0000000000..86dc42cba2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoResult.kt
@@ -0,0 +1,76 @@
+/* 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.browser.engine.gecko
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.launch
+import mozilla.components.concept.engine.CancellableOperation
+import org.mozilla.geckoview.GeckoResult
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+/**
+ * Wait for a GeckoResult to be complete in a co-routine.
+ */
+suspend fun <T> GeckoResult<T>.await() = suspendCoroutine<T?> { continuation ->
+ then(
+ {
+ continuation.resume(it)
+ GeckoResult<Void>()
+ },
+ {
+ continuation.resumeWithException(it)
+ GeckoResult<Void>()
+ },
+ )
+}
+
+/**
+ * Converts a [GeckoResult] to a [CancellableOperation].
+ */
+fun <T> GeckoResult<T>.asCancellableOperation(): CancellableOperation {
+ val geckoResult = this
+ return object : CancellableOperation {
+ override fun cancel(): Deferred<Boolean> {
+ val result = CompletableDeferred<Boolean>()
+ geckoResult.cancel().then(
+ {
+ result.complete(it ?: false)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ result.completeExceptionally(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ return result
+ }
+ }
+}
+
+/**
+ * Create a GeckoResult from a co-routine.
+ */
+@Suppress("TooGenericExceptionCaught")
+fun <T> CoroutineScope.launchGeckoResult(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend CoroutineScope.() -> T,
+) = GeckoResult<T>().apply {
+ launch(context, start) {
+ try {
+ val value = block()
+ complete(value)
+ } catch (exception: Throwable) {
+ completeExceptionally(exception)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorage.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorage.kt
new file mode 100644
index 0000000000..62d8a42a97
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorage.kt
@@ -0,0 +1,146 @@
+/* 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.browser.engine.gecko
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import mozilla.components.browser.engine.gecko.content.blocking.GeckoTrackingProtectionException
+import mozilla.components.browser.engine.gecko.ext.geckoTrackingProtectionPermission
+import mozilla.components.browser.engine.gecko.ext.isExcludedForTrackingProtection
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
+import mozilla.components.support.ktx.kotlin.getOrigin
+import mozilla.components.support.ktx.kotlin.stripDefaultPort
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
+
+/**
+ * A [TrackingProtectionExceptionStorage] implementation to store tracking protection exceptions.
+ */
+internal class GeckoTrackingProtectionExceptionStorage(
+ private val runtime: GeckoRuntime,
+) : TrackingProtectionExceptionStorage {
+ internal var scope = CoroutineScope(Dispatchers.IO)
+
+ override fun contains(session: EngineSession, onResult: (Boolean) -> Unit) {
+ val url = (session as GeckoEngineSession).currentUrl
+ if (!url.isNullOrEmpty()) {
+ getPermissions(url) { permissions ->
+ val contains = permissions.isNotEmpty()
+ onResult(contains)
+ }
+ } else {
+ onResult(false)
+ }
+ }
+
+ override fun fetchAll(onResult: (List<TrackingProtectionException>) -> Unit) {
+ runtime.storageController.allPermissions.accept { permissions ->
+ val trackingExceptions = permissions.filterTrackingProtectionExceptions()
+ onResult(trackingExceptions.map { exceptions -> exceptions.toTrackingProtectionException() })
+ }
+ }
+
+ private fun List<ContentPermission>?.filterTrackingProtectionExceptions() =
+ this.orEmpty().filter { it.isExcludedForTrackingProtection }
+
+ private fun List<ContentPermission>?.filterTrackingProtectionExceptions(url: String) =
+ this.orEmpty()
+ .filter {
+ it.isExcludedForTrackingProtection && it.uri.getOrigin().orEmpty()
+ .stripDefaultPort() == url
+ }
+
+ override fun add(session: EngineSession, persistInPrivateMode: Boolean) {
+ val geckoEngineSession = (session as GeckoEngineSession)
+ if (persistInPrivateMode) {
+ addPersistentPrivateException(geckoEngineSession)
+ } else {
+ geckoEngineSession.geckoTrackingProtectionPermission?.let {
+ runtime.storageController.setPermission(it, VALUE_ALLOW)
+ }
+ }
+
+ geckoEngineSession.notifyObservers {
+ onExcludedOnTrackingProtectionChange(true)
+ }
+ }
+
+ internal fun addPersistentPrivateException(geckoEngineSession: GeckoEngineSession) {
+ val permission = geckoEngineSession.geckoTrackingProtectionPermission
+ permission?.let {
+ runtime.storageController.setPrivateBrowsingPermanentPermission(it, VALUE_ALLOW)
+ }
+ }
+
+ override fun remove(session: EngineSession) {
+ val geckoEngineSession = (session as GeckoEngineSession)
+ val url = geckoEngineSession.currentUrl ?: return
+ geckoEngineSession.notifyObservers {
+ onExcludedOnTrackingProtectionChange(false)
+ }
+ remove(url)
+ }
+
+ override fun remove(exception: TrackingProtectionException) {
+ if (exception is GeckoTrackingProtectionException) {
+ remove(exception.contentPermission)
+ } else {
+ remove(exception.url)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun remove(url: String) {
+ val storage = runtime.storageController
+ getPermissions(url) { permissions ->
+ permissions.forEach { geckoPermissions ->
+ storage.setPermission(geckoPermissions, VALUE_DENY)
+ }
+ }
+ }
+
+ // This is a workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1723280 gets addressed
+ private fun getPermissions(url: String, onFinish: (List<ContentPermission>) -> Unit) {
+ val localUrl = url.getOrigin().orEmpty().stripDefaultPort()
+ val storage = runtime.storageController
+ if (localUrl.isNotEmpty()) {
+ storage.allPermissions.accept { permissions ->
+ onFinish(permissions.filterTrackingProtectionExceptions(localUrl))
+ }
+ } else {
+ onFinish(emptyList())
+ }
+ }
+
+ @VisibleForTesting
+ internal fun remove(contentPermission: ContentPermission) {
+ runtime.storageController.setPermission(contentPermission, VALUE_DENY)
+ }
+
+ override fun removeAll(activeSessions: List<EngineSession>?, onRemove: () -> Unit) {
+ val storage = runtime.storageController
+ activeSessions?.forEach { engineSession ->
+ engineSession.notifyObservers {
+ onExcludedOnTrackingProtectionChange(false)
+ }
+ }
+ storage.allPermissions.accept { permissions ->
+ val trackingExceptions = permissions.filterTrackingProtectionExceptions()
+ trackingExceptions.forEach {
+ storage.setPermission(it, VALUE_DENY)
+ }
+ onRemove.invoke()
+ }
+ }
+}
+
+private fun ContentPermission.toTrackingProtectionException(): GeckoTrackingProtectionException {
+ return GeckoTrackingProtectionException(uri, privateMode, this)
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/NestedGeckoView.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/NestedGeckoView.kt
new file mode 100644
index 0000000000..717c3e8dca
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/NestedGeckoView.kt
@@ -0,0 +1,258 @@
+/* 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.browser.engine.gecko
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.MotionEvent
+import androidx.annotation.VisibleForTesting
+import androidx.core.view.NestedScrollingChild
+import androidx.core.view.NestedScrollingChildHelper
+import androidx.core.view.ViewCompat
+import mozilla.components.concept.engine.InputResultDetail
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoView
+import org.mozilla.geckoview.PanZoomController
+
+/**
+ * geckoView that supports nested scrolls (for using in a CoordinatorLayout).
+ *
+ * This code is a simplified version of the NestedScrollView implementation
+ * which can be found in the support library:
+ * [android.support.v4.widget.NestedScrollView]
+ *
+ * Based on:
+ * https://github.com/takahirom/webview-in-coordinatorlayout
+ */
+
+@Suppress("ClickableViewAccessibility")
+open class NestedGeckoView(context: Context) : GeckoView(context), NestedScrollingChild {
+ @VisibleForTesting
+ internal var lastY: Int = 0
+
+ @VisibleForTesting
+ internal val scrollOffset = IntArray(2)
+
+ private val scrollConsumed = IntArray(2)
+
+ private var gestureCanReachParent = true
+
+ private var initialDownY: Float = 0f
+
+ @VisibleForTesting
+ internal var nestedOffsetY: Int = 0
+
+ @VisibleForTesting
+ internal var childHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
+
+ /**
+ * How user's MotionEvent will be handled.
+ *
+ * @see InputResultDetail
+ */
+ internal var inputResultDetail = InputResultDetail.newInstance(true)
+
+ init {
+ isNestedScrollingEnabled = true
+ }
+
+ @Suppress("ComplexMethod")
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
+ val event = MotionEvent.obtain(ev)
+ val action = ev.actionMasked
+ val eventY = event.y.toInt()
+
+ when (action) {
+ MotionEvent.ACTION_MOVE -> {
+ val allowScroll = !shouldPinOnScreen() && inputResultDetail.isTouchHandledByBrowser()
+
+ var deltaY = lastY - eventY
+
+ if (allowScroll && dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
+ deltaY -= scrollConsumed[1]
+ event.offsetLocation(0f, (-scrollOffset[1]).toFloat())
+ nestedOffsetY += scrollOffset[1]
+ }
+
+ lastY = eventY - scrollOffset[1]
+
+ if (allowScroll && dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
+ lastY -= scrollOffset[1]
+ event.offsetLocation(0f, scrollOffset[1].toFloat())
+ nestedOffsetY += scrollOffset[1]
+ }
+
+ // If this event is the first touch move event, there are two possible cases
+ // where we still need to wait for the response for this first touch move event.
+ // a) we haven't yet received the response from GeckoView because of active touch
+ // event listeners etc.
+ // b) we have received the response for the touch down event that GeckoView
+ // consumed the event
+ // In the case of a) it's possible a touch move event listener does preventDefault()
+ // for this touch move event, then any subsequent touch events need to be directly
+ // routed to GeckoView rather than being intercepted.
+ // In the case of b) if GeckoView consumed this touch move event to scroll down the
+ // web content, any touch event interception should not be allowed since, for example
+ // SwipeRefreshLayout is supposed to trigger a refresh after the user started scroll
+ // down if the user restored the scroll position at the top.
+ val hasDragGestureStarted = event.y != initialDownY
+ if (gestureCanReachParent && hasDragGestureStarted) {
+ updateInputResult(event)
+ event.recycle()
+ return true
+ }
+ }
+
+ MotionEvent.ACTION_DOWN -> {
+ // A new gesture started. Ask GV if it can handle this.
+ parent?.requestDisallowInterceptTouchEvent(true)
+ updateInputResult(event)
+
+ nestedOffsetY = 0
+ lastY = eventY
+ initialDownY = event.y
+
+ // The event should be handled either by onTouchEvent,
+ // either by onTouchEventForResult, never by both.
+ // Early return if we sent it to updateInputResult(..) which calls onTouchEventForResult.
+ event.recycle()
+ return true
+ }
+
+ // We don't care about other touch events
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ // inputResultDetail needs to be reset here and not in the next ACTION_DOWN, because
+ // its value is used by other features that poll for the value via
+ // `EngineView.getInputResultDetail`. Not resetting this in ACTION_CANCEL/ACTION_UP
+ // would then mean we send stale information to those features from a previous
+ // gesture's result.
+ inputResultDetail = InputResultDetail.newInstance(true)
+ stopNestedScroll()
+
+ // Allow touch event interception here so that the next ACTION_DOWN event can be properly
+ // intercepted by the parent.
+ parent?.requestDisallowInterceptTouchEvent(false)
+ gestureCanReachParent = true
+ }
+ }
+
+ // Execute event handler from parent class in all cases
+ val eventHandled = callSuperOnTouchEvent(event)
+
+ // Recycle previously obtained event
+ event.recycle()
+
+ return eventHandled
+ }
+
+ @VisibleForTesting
+ internal fun callSuperOnTouchEvent(event: MotionEvent): Boolean {
+ return super.onTouchEvent(event)
+ }
+
+ @SuppressLint("WrongThread") // Lint complains startNestedScroll() needs to be called on the main thread
+ @VisibleForTesting
+ internal fun updateInputResult(event: MotionEvent) {
+ val eventAction = event.actionMasked
+ val eventY = event.y
+ superOnTouchEventForDetailResult(event)
+ .accept {
+ // Since the response from APZ is async, we could theoretically have a response
+ // which is out of time when we get the ACTION_MOVE events, and we do not want
+ // to forward this to the parent pre-emptively.
+ if (!gestureCanReachParent) {
+ return@accept
+ }
+
+ inputResultDetail = inputResultDetail.copy(
+ it?.handledResult(),
+ it?.scrollableDirections(),
+ it?.overscrollDirections(),
+ )
+
+ when (eventAction) {
+ MotionEvent.ACTION_DOWN -> {
+ // Gesture can reach the parent only if the content is already at the top
+ gestureCanReachParent = inputResultDetail.canOverscrollTop()
+
+ if (gestureCanReachParent && inputResultDetail.isTouchUnhandled()) {
+ // If the event wasn't used in GeckoView, allow touch event interception.
+ parent?.requestDisallowInterceptTouchEvent(false)
+ }
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ if (initialDownY < eventY) {
+ // In the case of scroll upwards gestures, allow touch event interception
+ // only if the event wasn't consumed by the web site. I.e. even if
+ // the event was consumed by the browser to scroll up the content.
+ if (!inputResultDetail.isTouchHandledByWebsite()) {
+ parent?.requestDisallowInterceptTouchEvent(false)
+ }
+ } else if (initialDownY > eventY) {
+ // Once after the content started scroll down, touch event interception
+ // is never allowed.
+ parent?.requestDisallowInterceptTouchEvent(true)
+ gestureCanReachParent = false
+ } else {
+ // Normally ACTION_MOVE should happen with moving the event position,
+ // but if it happened allow touch event interception just in case.
+ parent?.requestDisallowInterceptTouchEvent(false)
+ }
+ }
+ }
+
+ startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
+ }
+ }
+
+ @VisibleForTesting
+ internal open fun superOnTouchEventForDetailResult(
+ event: MotionEvent,
+ ): GeckoResult<PanZoomController.InputResultDetail> =
+ super.onTouchEventForDetailResult(event)
+
+ override fun setNestedScrollingEnabled(enabled: Boolean) {
+ childHelper.isNestedScrollingEnabled = enabled
+ }
+
+ override fun isNestedScrollingEnabled(): Boolean {
+ return childHelper.isNestedScrollingEnabled
+ }
+
+ override fun startNestedScroll(axes: Int): Boolean {
+ return childHelper.startNestedScroll(axes)
+ }
+
+ override fun stopNestedScroll() {
+ childHelper.stopNestedScroll()
+ }
+
+ override fun hasNestedScrollingParent(): Boolean {
+ return childHelper.hasNestedScrollingParent()
+ }
+
+ override fun dispatchNestedScroll(
+ dxConsumed: Int,
+ dyConsumed: Int,
+ dxUnconsumed: Int,
+ dyUnconsumed: Int,
+ offsetInWindow: IntArray?,
+ ): Boolean {
+ return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
+ }
+
+ override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
+ return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
+ }
+
+ override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
+ return childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
+ }
+
+ override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
+ return childHelper.dispatchNestedPreFling(velocityX, velocityY)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegate.kt
new file mode 100644
index 0000000000..fa967bbc83
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegate.kt
@@ -0,0 +1,45 @@
+/* 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.browser.engine.gecko.activity
+
+import android.app.PendingIntent
+import android.content.Intent
+import mozilla.components.concept.engine.activity.ActivityDelegate
+import mozilla.components.support.base.log.logger.Logger
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import java.lang.ref.WeakReference
+
+/**
+ * A wrapper for the [ActivityDelegate] to communicate with the Gecko-based delegate.
+ */
+internal class GeckoActivityDelegate(
+ private val delegateRef: WeakReference<ActivityDelegate>,
+) : GeckoRuntime.ActivityDelegate {
+
+ private val logger = Logger(GeckoActivityDelegate::javaClass.name)
+
+ override fun onStartActivityForResult(intent: PendingIntent): GeckoResult<Intent> {
+ val result: GeckoResult<Intent> = GeckoResult()
+ val delegate = delegateRef.get()
+
+ if (delegate == null) {
+ logger.warn("No activity delegate attached. Cannot request FIDO auth.")
+
+ result.completeExceptionally(RuntimeException("Activity for result failed; no delegate attached."))
+
+ return result
+ }
+
+ delegate.startIntentSenderForResult(intent.intentSender) { data ->
+ if (data != null) {
+ result.complete(data)
+ } else {
+ result.completeExceptionally(RuntimeException("Activity for result failed."))
+ }
+ }
+ return result
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegate.kt
new file mode 100644
index 0000000000..86d0e72f9f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegate.kt
@@ -0,0 +1,35 @@
+/* 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.browser.engine.gecko.activity
+
+import mozilla.components.concept.engine.activity.OrientationDelegate
+import org.mozilla.geckoview.AllowOrDeny
+import org.mozilla.geckoview.AllowOrDeny.ALLOW
+import org.mozilla.geckoview.AllowOrDeny.DENY
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.OrientationController
+
+/**
+ * Default [OrientationController.OrientationDelegate] implementation that delegates both the behavior
+ * and the returned value to a [OrientationDelegate].
+ */
+internal class GeckoScreenOrientationDelegate(
+ private val delegate: OrientationDelegate,
+) : OrientationController.OrientationDelegate {
+ override fun onOrientationLock(requestedOrientation: Int): GeckoResult<AllowOrDeny> {
+ val result = GeckoResult<AllowOrDeny>()
+
+ when (delegate.onOrientationLock(requestedOrientation)) {
+ true -> result.complete(ALLOW)
+ false -> result.complete(DENY)
+ }
+
+ return result
+ }
+
+ override fun onOrientationUnlock() {
+ delegate.onOrientationUnlock()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegate.kt
new file mode 100644
index 0000000000..41841687d9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegate.kt
@@ -0,0 +1,43 @@
+/* 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.browser.engine.gecko.activity
+
+import android.app.Activity
+import android.content.Context
+import mozilla.components.support.base.log.logger.Logger
+import org.mozilla.geckoview.GeckoView
+import java.lang.ref.WeakReference
+
+/**
+ * GeckoViewActivityContextDelegate provides an active Activity to GeckoView or null. Not to be confused
+ * with the runtime delegate of [GeckoActivityDelegate], which is tightly coupled to webauthn.
+ * See bug 1806191 for more information on delegate differences.
+ *
+ * @param contextRef A reference to an active Activity context or null for GeckoView to use.
+ */
+class GeckoViewActivityContextDelegate(
+ private val contextRef: WeakReference<Context?>,
+) : GeckoView.ActivityContextDelegate {
+ private val logger = Logger("GeckoViewActivityContextDelegate")
+ init {
+ if (contextRef.get() == null) {
+ logger.warn("Activity context is null.")
+ } else if (contextRef.get() !is Activity) {
+ logger.warn("A non-activity context was set.")
+ }
+ }
+
+ /**
+ * Used by GeckoView to get an Activity context for operations such as printing.
+ * @return An active Activity context or null.
+ */
+ override fun getActivityContext(): Context? {
+ val context = contextRef.get()
+ if ((context == null)) {
+ return null
+ }
+ return context
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt
new file mode 100644
index 0000000000..cb178b6292
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt
@@ -0,0 +1,110 @@
+/* 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.browser.engine.gecko.autofill
+
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
+import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
+import mozilla.components.browser.engine.gecko.ext.toLoginEntry
+import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginStorageDelegate
+import org.mozilla.geckoview.Autocomplete
+import org.mozilla.geckoview.GeckoResult
+
+/**
+ * Gecko credit card and login storage delegate that handles runtime storage requests. This allows
+ * the Gecko runtime to call the underlying storage to handle requests for fetching, saving and
+ * updating of autocomplete items in the storage.
+ *
+ * @param creditCardsAddressesStorageDelegate An instance of [CreditCardsAddressesStorageDelegate].
+ * Provides methods for retrieving [CreditCard]s from the underlying storage.
+ * @param loginStorageDelegate An instance of [LoginStorageDelegate].
+ * Provides read/write methods for the [Login] storage.
+ */
+class GeckoAutocompleteStorageDelegate(
+ private val creditCardsAddressesStorageDelegate: CreditCardsAddressesStorageDelegate,
+ private val loginStorageDelegate: LoginStorageDelegate,
+) : Autocomplete.StorageDelegate {
+
+ override fun onAddressFetch(): GeckoResult<Array<Autocomplete.Address>>? {
+ val result = GeckoResult<Array<Autocomplete.Address>>()
+
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(IO) {
+ val addresses = creditCardsAddressesStorageDelegate.onAddressesFetch()
+ .map { it.toAutocompleteAddress() }
+ .toTypedArray()
+
+ result.complete(addresses)
+ }
+
+ return result
+ }
+
+ override fun onCreditCardFetch(): GeckoResult<Array<Autocomplete.CreditCard>> {
+ val result = GeckoResult<Array<Autocomplete.CreditCard>>()
+
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(IO) {
+ val key = creditCardsAddressesStorageDelegate.getOrGenerateKey()
+
+ val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch()
+ .mapNotNull {
+ val plaintextCardNumber =
+ creditCardsAddressesStorageDelegate.decrypt(key, it.encryptedCardNumber)?.number
+
+ if (plaintextCardNumber == null) {
+ null
+ } else {
+ Autocomplete.CreditCard.Builder()
+ .guid(it.guid)
+ .name(it.billingName)
+ .number(plaintextCardNumber)
+ .expirationMonth(it.expiryMonth.toString())
+ .expirationYear(it.expiryYear.toString())
+ .build()
+ }
+ }
+ .toTypedArray()
+
+ result.complete(creditCards)
+ }
+
+ return result
+ }
+
+ override fun onCreditCardSave(creditCard: Autocomplete.CreditCard) {
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(IO) {
+ creditCardsAddressesStorageDelegate.onCreditCardSave(creditCard.toCreditCardEntry())
+ }
+ }
+
+ override fun onLoginSave(login: Autocomplete.LoginEntry) {
+ loginStorageDelegate.onLoginSave(login.toLoginEntry())
+ }
+
+ override fun onLoginFetch(domain: String): GeckoResult<Array<Autocomplete.LoginEntry>> {
+ val result = GeckoResult<Array<Autocomplete.LoginEntry>>()
+
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(IO) {
+ val storedLogins = loginStorageDelegate.onLoginFetch(domain)
+
+ val logins = storedLogins.await()
+ .map { it.toLoginEntry() }
+ .toTypedArray()
+
+ result.complete(logins)
+ }
+
+ return result
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt
new file mode 100644
index 0000000000..281f6e6968
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt
@@ -0,0 +1,20 @@
+/* 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.browser.engine.gecko.content.blocking
+
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
+
+/**
+ * Represents a site that will be ignored by the tracking protection policies.
+ * @property url The url of the site to be ignored.
+ * @property privateMode Indicates if this exception should persisted in private mode.
+ * @property contentPermission The associated gecko content permission of this exception.
+ */
+data class GeckoTrackingProtectionException(
+ override val url: String,
+ val privateMode: Boolean = false,
+ val contentPermission: ContentPermission,
+) : TrackingProtectionException
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorage.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorage.kt
new file mode 100644
index 0000000000..18aefed775
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorage.kt
@@ -0,0 +1,128 @@
+/* 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.browser.engine.gecko.cookiebanners
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.engine.gecko.await
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.DISABLED
+import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
+import mozilla.components.support.base.log.logger.Logger
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.StorageController
+
+/**
+ * A storage to store [CookieBannerHandlingMode] using GeckoView APIs.
+ */
+class GeckoCookieBannersStorage(
+ runtime: GeckoRuntime,
+ private val reportSiteDomainsRepository: ReportSiteDomainsRepository,
+) : CookieBannersStorage {
+
+ private val geckoStorage: StorageController = runtime.storageController
+ private val mainScope = CoroutineScope(Dispatchers.Main)
+
+ override suspend fun addException(
+ uri: String,
+ privateBrowsing: Boolean,
+ ) {
+ setGeckoException(uri, DISABLED, privateBrowsing)
+ }
+
+ override suspend fun isSiteDomainReported(siteDomain: String): Boolean {
+ return reportSiteDomainsRepository.isSiteDomainReported(siteDomain)
+ }
+
+ override suspend fun saveSiteDomain(siteDomain: String) {
+ reportSiteDomainsRepository.saveSiteDomain(siteDomain)
+ }
+
+ override suspend fun addPersistentExceptionInPrivateMode(uri: String) {
+ setPersistentPrivateGeckoException(uri, DISABLED)
+ }
+
+ override suspend fun findExceptionFor(
+ uri: String,
+ privateBrowsing: Boolean,
+ ): CookieBannerHandlingMode? {
+ return queryExceptionInGecko(uri, privateBrowsing)
+ }
+
+ override suspend fun hasException(uri: String, privateBrowsing: Boolean): Boolean? {
+ val result = findExceptionFor(uri, privateBrowsing)
+ return if (result != null) {
+ result == DISABLED
+ } else {
+ null
+ }
+ }
+
+ override suspend fun removeException(uri: String, privateBrowsing: Boolean) {
+ removeGeckoException(uri, privateBrowsing)
+ }
+
+ @VisibleForTesting
+ internal fun removeGeckoException(uri: String, privateBrowsing: Boolean) {
+ geckoStorage.removeCookieBannerModeForDomain(uri, privateBrowsing)
+ }
+
+ @VisibleForTesting
+ internal fun setGeckoException(
+ uri: String,
+ mode: CookieBannerHandlingMode,
+ privateBrowsing: Boolean,
+ ) {
+ geckoStorage.setCookieBannerModeForDomain(
+ uri,
+ mode.mode,
+ privateBrowsing,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun setPersistentPrivateGeckoException(
+ uri: String,
+ mode: CookieBannerHandlingMode,
+ ) {
+ geckoStorage.setCookieBannerModeAndPersistInPrivateBrowsingForDomain(
+ uri,
+ mode.mode,
+ )
+ }
+
+ @VisibleForTesting
+ @Suppress("TooGenericExceptionCaught")
+ internal suspend fun queryExceptionInGecko(
+ uri: String,
+ privateBrowsing: Boolean,
+ ): CookieBannerHandlingMode? {
+ return try {
+ withContext(mainScope.coroutineContext) {
+ geckoStorage.getCookieBannerModeForDomain(uri, privateBrowsing).await()
+ ?.toCookieBannerHandlingMode() ?: throw IllegalArgumentException(
+ "An error happened trying to find cookie banners mode for the " +
+ "uri $uri and private browsing mode $privateBrowsing",
+ )
+ }
+ } catch (e: Exception) {
+ // This normally happen on internal sites like about:config or ip sites.
+ val disabledErrors = listOf("NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS", "NS_ERROR_HOST_IS_IP_ADDRESS")
+ if (disabledErrors.any { (e.message ?: "").contains(it) }) {
+ Logger("GeckoCookieBannersStorage").error("Unable to query cookie banners exception", e)
+ null
+ } else {
+ throw e
+ }
+ }
+ }
+}
+
+@VisibleForTesting
+internal fun Int.toCookieBannerHandlingMode(): CookieBannerHandlingMode {
+ return CookieBannerHandlingMode.values().first { it.mode == this }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepository.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepository.kt
new file mode 100644
index 0000000000..9fbb9b3eda
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepository.kt
@@ -0,0 +1,75 @@
+/* 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.browser.engine.gecko.cookiebanners
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
+import androidx.datastore.preferences.core.stringPreferencesKey
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.engine.gecko.cookiebanners.ReportSiteDomainsRepository.PreferencesKeys.REPORT_SITE_DOMAINS
+import mozilla.components.support.base.log.logger.Logger
+import java.io.IOException
+
+/**
+ * A repository to save reported site domains with the datastore API.
+ */
+class ReportSiteDomainsRepository(
+ private val dataStore: DataStore<Preferences>,
+) {
+
+ companion object {
+ const val SEPARATOR = "@<;>@"
+ const val REPORT_SITE_DOMAINS_REPOSITORY_NAME = "report_site_domains_preferences"
+ const val PREFERENCE_KEY_NAME = "report_site_domains"
+ }
+
+ private object PreferencesKeys {
+ val REPORT_SITE_DOMAINS = stringPreferencesKey(PREFERENCE_KEY_NAME)
+ }
+
+ /**
+ * Check if the given site's domain url is saved locally.
+ * @param siteDomain the [siteDomain] that will be checked.
+ */
+ suspend fun isSiteDomainReported(siteDomain: String): Boolean {
+ return dataStore.data
+ .catch { exception ->
+ if (exception is IOException) {
+ Logger.error("Error reading preferences.", exception)
+ emit(emptyPreferences())
+ } else {
+ throw exception
+ }
+ }.map { preferences ->
+ val reportSiteDomainsString = preferences[REPORT_SITE_DOMAINS] ?: ""
+ val reportSiteDomainsList =
+ reportSiteDomainsString.split(SEPARATOR).filter { it.isNotEmpty() }
+ reportSiteDomainsList.contains(siteDomain)
+ }.first()
+ }
+
+ /**
+ * Save the given site's domain url in datastore to keep it persistent locally.
+ * This method gets called after the site domain was reported with Nimbus.
+ * @param siteDomain the [siteDomain] that will be saved.
+ */
+ suspend fun saveSiteDomain(siteDomain: String) {
+ dataStore.edit { preferences ->
+ val siteDomainsPreferences = preferences[REPORT_SITE_DOMAINS] ?: ""
+ val siteDomainsList = siteDomainsPreferences.split(SEPARATOR).filter { it.isNotEmpty() }
+ if (siteDomainsList.contains(siteDomain)) {
+ return@edit
+ }
+ val domains = mutableListOf<String>()
+ domains.addAll(siteDomainsList)
+ domains.add(siteDomain)
+ preferences[REPORT_SITE_DOMAINS] = domains.joinToString(SEPARATOR)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt
new file mode 100644
index 0000000000..2378326ed0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt
@@ -0,0 +1,42 @@
+/* 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.browser.engine.gecko.ext
+
+import mozilla.components.concept.storage.Address
+import org.mozilla.geckoview.Autocomplete
+
+/**
+ * Converts a GeckoView [Autocomplete.Address] to an Android Components [Address].
+ */
+fun Autocomplete.Address.toAddress() = Address(
+ guid = guid ?: "",
+ name = name,
+ organization = organization,
+ streetAddress = streetAddress,
+ addressLevel3 = addressLevel3,
+ addressLevel2 = addressLevel2,
+ addressLevel1 = addressLevel1,
+ postalCode = postalCode,
+ country = country,
+ tel = tel,
+ email = email,
+)
+
+/**
+ * Converts an Android Components [Address] to a GeckoView [Autocomplete.Address].
+ */
+fun Address.toAutocompleteAddress() = Autocomplete.Address.Builder()
+ .guid(guid)
+ .name(name)
+ .organization(organization)
+ .streetAddress(streetAddress)
+ .addressLevel3(addressLevel3)
+ .addressLevel2(addressLevel2)
+ .addressLevel1(addressLevel1)
+ .postalCode(postalCode)
+ .country(country)
+ .tel(tel)
+ .email(email)
+ .build()
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt
new file mode 100644
index 0000000000..79bb5fd091
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt
@@ -0,0 +1,32 @@
+/* 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.browser.engine.gecko.ext
+
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.support.utils.creditCardIIN
+import org.mozilla.geckoview.Autocomplete
+
+/**
+ * Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCardEntry].
+ */
+fun Autocomplete.CreditCard.toCreditCardEntry() = CreditCardEntry(
+ guid = guid,
+ name = name,
+ number = number,
+ expiryMonth = expirationMonth,
+ expiryYear = expirationYear,
+ cardType = number.creditCardIIN()?.creditCardIssuerNetwork?.name ?: "",
+)
+
+/**
+ * Converts an Android Components [CreditCardEntry] to a GeckoView [Autocomplete.CreditCard].
+ */
+fun CreditCardEntry.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder()
+ .guid(guid)
+ .name(name)
+ .number(number)
+ .expirationMonth(expiryMonth)
+ .expirationYear(expiryYear)
+ .build()
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoChoice.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoChoice.kt
new file mode 100644
index 0000000000..f988d37a16
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoChoice.kt
@@ -0,0 +1,31 @@
+/* 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.browser.engine.gecko.ext
+
+import mozilla.components.browser.engine.gecko.prompt.GeckoChoice
+import mozilla.components.concept.engine.prompt.Choice
+
+/**
+ * Converts a GeckoView [GeckoChoice] to an Android Components [Choice].
+ */
+private fun GeckoChoice.toChoice(): Choice {
+ val choiceChildren = items?.map { it.toChoice() }?.toTypedArray()
+ // On the GeckoView docs states that label is a @NonNull, but on run-time
+ // we are getting null values
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1771149
+ @Suppress("USELESS_ELVIS")
+ return Choice(id, !disabled, label ?: "", selected, separator, choiceChildren)
+}
+
+/**
+ * Convert an array of [GeckoChoice] to Choice array.
+ * @return array of Choice
+ */
+fun convertToChoices(
+ geckoChoices: Array<out GeckoChoice>,
+): Array<Choice> = geckoChoices.map { geckoChoice ->
+ val choice = geckoChoice.toChoice()
+ choice
+}.toTypedArray()
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoContentPermissions.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoContentPermissions.kt
new file mode 100644
index 0000000000..1e5ddcce35
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoContentPermissions.kt
@@ -0,0 +1,25 @@
+/* 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.browser.engine.gecko.ext
+
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
+
+/**
+ * Indicates if this Gecko permission is a tracking protection permission and it is excluded
+ * from the tracking protection policies.
+ */
+val ContentPermission.isExcludedForTrackingProtection: Boolean
+ get() = this.permission == PERMISSION_TRACKING &&
+ value == VALUE_ALLOW
+
+/**
+ * Provides the tracking protection permission for the given [GeckoEngineSession].
+ * This is available after every onLocationChange call.
+ */
+val GeckoEngineSession.geckoTrackingProtectionPermission: ContentPermission?
+ get() = this.geckoPermissions.find { it.permission == PERMISSION_TRACKING }
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Login.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Login.kt
new file mode 100644
index 0000000000..dbc6d1a308
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Login.kt
@@ -0,0 +1,43 @@
+/* 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.browser.engine.gecko.ext
+
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import org.mozilla.geckoview.Autocomplete
+
+/**
+ * Converts a GeckoView [Autocomplete.LoginEntry] to an Android Components [LoginEntry].
+ */
+fun Autocomplete.LoginEntry.toLoginEntry() = LoginEntry(
+ origin = origin,
+ formActionOrigin = formActionOrigin,
+ httpRealm = httpRealm,
+ username = username,
+ password = password,
+)
+
+/**
+ * Converts an Android Components [Login] to a GeckoView [Autocomplete.LoginEntry].
+ */
+fun Login.toLoginEntry() = Autocomplete.LoginEntry.Builder()
+ .guid(guid)
+ .origin(origin)
+ .formActionOrigin(formActionOrigin)
+ .httpRealm(httpRealm)
+ .username(username)
+ .password(password)
+ .build()
+
+/**
+ * Converts an Android Components [LoginEntry] to a GeckoView [Autocomplete.LoginEntry].
+ */
+fun LoginEntry.toLoginEntry() = Autocomplete.LoginEntry.Builder()
+ .origin(origin)
+ .formActionOrigin(formActionOrigin)
+ .httpRealm(httpRealm)
+ .username(username)
+ .password(password)
+ .build()
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicy.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicy.kt
new file mode 100644
index 0000000000..1775010197
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicy.kt
@@ -0,0 +1,82 @@
+/* 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.browser.engine.gecko.ext
+
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
+import org.mozilla.geckoview.ContentBlocking
+import org.mozilla.geckoview.GeckoRuntimeSettings
+
+/**
+ * Converts a [TrackingProtectionPolicy] into a GeckoView setting that can be used with [GeckoRuntimeSettings.Builder].
+ * Also contains the cookie banner handling settings for regular and private browsing.
+ */
+@Suppress("SpreadOperator")
+fun TrackingProtectionPolicy.toContentBlockingSetting(
+ safeBrowsingPolicy: Array<EngineSession.SafeBrowsingPolicy> = arrayOf(EngineSession.SafeBrowsingPolicy.RECOMMENDED),
+ cookieBannerHandlingMode: EngineSession.CookieBannerHandlingMode = EngineSession.CookieBannerHandlingMode.DISABLED,
+ cookieBannerHandlingModePrivateBrowsing: EngineSession.CookieBannerHandlingMode =
+ EngineSession.CookieBannerHandlingMode.REJECT_ALL,
+ cookieBannerHandlingDetectOnlyMode: Boolean = false,
+ cookieBannerGlobalRulesEnabled: Boolean = false,
+ cookieBannerGlobalRulesSubFramesEnabled: Boolean = false,
+ queryParameterStripping: Boolean = false,
+ queryParameterStrippingPrivateBrowsing: Boolean = false,
+ queryParameterStrippingAllowList: String = "",
+ queryParameterStrippingStripList: String = "",
+) = ContentBlocking.Settings.Builder().apply {
+ enhancedTrackingProtectionLevel(getEtpLevel())
+ antiTracking(getAntiTrackingPolicy())
+ cookieBehavior(cookiePolicy.id)
+ cookieBehaviorPrivateMode(cookiePolicyPrivateMode.id)
+ cookiePurging(cookiePurging)
+ safeBrowsing(safeBrowsingPolicy.sumOf { it.id })
+ strictSocialTrackingProtection(getStrictSocialTrackingProtection())
+ cookieBannerHandlingMode(cookieBannerHandlingMode.mode)
+ cookieBannerHandlingModePrivateBrowsing(cookieBannerHandlingModePrivateBrowsing.mode)
+ cookieBannerHandlingDetectOnlyMode(cookieBannerHandlingDetectOnlyMode)
+ cookieBannerGlobalRulesEnabled(cookieBannerGlobalRulesEnabled)
+ cookieBannerGlobalRulesSubFramesEnabled(cookieBannerGlobalRulesSubFramesEnabled)
+ queryParameterStrippingEnabled(queryParameterStripping)
+ queryParameterStrippingPrivateBrowsingEnabled(queryParameterStrippingPrivateBrowsing)
+ queryParameterStrippingAllowList(*queryParameterStrippingAllowList.split(",").toTypedArray())
+ queryParameterStrippingStripList(*queryParameterStrippingStripList.split(",").toTypedArray())
+}.build()
+
+/**
+ * Returns whether [TrackingCategory.STRICT] is enabled in the [TrackingProtectionPolicy].
+ */
+internal fun TrackingProtectionPolicy.getStrictSocialTrackingProtection(): Boolean {
+ return strictSocialTrackingProtection ?: trackingCategories.contains(TrackingCategory.STRICT)
+}
+
+/**
+ * Returns the [TrackingProtectionPolicy] categories as an Enhanced Tracking Protection level for GeckoView.
+ */
+internal fun TrackingProtectionPolicy.getEtpLevel(): Int {
+ return when {
+ trackingCategories.contains(TrackingCategory.NONE) -> ContentBlocking.EtpLevel.NONE
+ else -> ContentBlocking.EtpLevel.STRICT
+ }
+}
+
+/**
+ * Returns the [TrackingProtectionPolicy] as a tracking policy for GeckoView.
+ */
+internal fun TrackingProtectionPolicy.getAntiTrackingPolicy(): Int {
+ /**
+ * The [TrackingProtectionPolicy.TrackingCategory.SCRIPTS_AND_SUB_RESOURCES] is an
+ * artificial category, created with the sole purpose of going around this bug
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1579264, for this reason we have to
+ * remove its value from the valid anti tracking categories, when is present.
+ */
+ val total = trackingCategories.sumOf { it.id }
+ return if (contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES)) {
+ total - TrackingCategory.SCRIPTS_AND_SUB_RESOURCES.id
+ } else {
+ total
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchClient.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchClient.kt
new file mode 100644
index 0000000000..c28989752d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchClient.kt
@@ -0,0 +1,139 @@
+/* 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.browser.engine.gecko.fetch
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.Response.Companion.SUCCESS
+import mozilla.components.concept.fetch.isBlobUri
+import mozilla.components.concept.fetch.isDataUri
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoWebExecutor
+import org.mozilla.geckoview.WebRequest
+import org.mozilla.geckoview.WebRequest.CACHE_MODE_DEFAULT
+import org.mozilla.geckoview.WebRequest.CACHE_MODE_RELOAD
+import org.mozilla.geckoview.WebRequestError
+import org.mozilla.geckoview.WebResponse
+import java.io.IOException
+import java.net.SocketTimeoutException
+import java.nio.ByteBuffer
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+/**
+ * GeckoView ([GeckoWebExecutor]) based implementation of [Client].
+ */
+class GeckoViewFetchClient(
+ context: Context,
+ runtime: GeckoRuntime = GeckoRuntime.getDefault(context),
+ private val maxReadTimeOut: Pair<Long, TimeUnit> = Pair(MAX_READ_TIMEOUT_MINUTES, TimeUnit.MINUTES),
+) : Client() {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var executor: GeckoWebExecutor = GeckoWebExecutor(runtime)
+
+ @Throws(IOException::class)
+ override fun fetch(request: Request): Response {
+ if (request.isDataUri()) {
+ return fetchDataUri(request)
+ }
+
+ val webRequest = request.toWebRequest()
+
+ val readTimeOut = request.readTimeout ?: maxReadTimeOut
+ val readTimeOutMillis = readTimeOut.let { (timeout, unit) ->
+ unit.toMillis(timeout)
+ }
+
+ return try {
+ val webResponse = executor.fetch(webRequest, request.fetchFlags).poll(readTimeOutMillis)
+ webResponse?.toResponse() ?: throw IOException("Fetch failed with null response")
+ } catch (e: TimeoutException) {
+ throw SocketTimeoutException()
+ } catch (e: WebRequestError) {
+ throw IOException(e)
+ }
+ }
+
+ private val Request.fetchFlags: Int
+ get() {
+ var fetchFlags = 0
+ if (cookiePolicy == Request.CookiePolicy.OMIT) {
+ fetchFlags += GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS
+ }
+ if (private) {
+ fetchFlags += GeckoWebExecutor.FETCH_FLAGS_PRIVATE
+ }
+ if (redirect == Request.Redirect.MANUAL) {
+ fetchFlags += GeckoWebExecutor.FETCH_FLAGS_NO_REDIRECTS
+ }
+ return fetchFlags
+ }
+
+ companion object {
+ const val MAX_READ_TIMEOUT_MINUTES = 5L
+ }
+}
+
+private fun Request.toWebRequest(): WebRequest = WebRequest.Builder(url)
+ .method(method.name)
+ .addHeadersFrom(this)
+ .addBodyFrom(this)
+ .referrer(referrerUrl)
+ .cacheMode(if (useCaches) CACHE_MODE_DEFAULT else CACHE_MODE_RELOAD)
+ .beConservative(conservative)
+ .build()
+
+private fun WebRequest.Builder.addHeadersFrom(request: Request): WebRequest.Builder {
+ request.headers?.forEach { header ->
+ addHeader(header.name, header.value)
+ }
+
+ return this
+}
+
+private fun WebRequest.Builder.addBodyFrom(request: Request): WebRequest.Builder {
+ request.body?.let { body ->
+ body.useStream { inStream ->
+ val bytes = inStream.readBytes()
+ val buffer = ByteBuffer.allocateDirect(bytes.size)
+ buffer.put(bytes)
+ this.body(buffer)
+ }
+ }
+
+ return this
+}
+
+internal fun WebResponse.toResponse(): Response {
+ val isDataUri = uri.startsWith("data:")
+ val isBlobUri = uri.startsWith("blob:")
+ val headers = translateHeaders(this)
+ // We use the same API for blobs, data URLs and HTTP requests, but blobs won't receive a status code.
+ // If no exception is thrown we assume success.
+ val status = if (isBlobUri || isDataUri) SUCCESS else statusCode
+ return Response(
+ uri,
+ status,
+ headers,
+ body?.let {
+ Response.Body(it, headers["Content-Type"])
+ } ?: Response.Body.empty(),
+ )
+}
+
+private fun translateHeaders(webResponse: WebResponse): Headers {
+ val headers = MutableHeaders()
+ webResponse.headers.forEach { (k, v) ->
+ v.split(",").forEach { headers.append(k, it.trim()) }
+ }
+
+ return headers
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/LocaleSettingUpdater.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/LocaleSettingUpdater.kt
new file mode 100644
index 0000000000..dc0d0427a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/LocaleSettingUpdater.kt
@@ -0,0 +1,56 @@
+/* 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.browser.engine.gecko.integration
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.core.content.ContextCompat
+import mozilla.components.support.utils.ext.registerReceiverCompat
+import org.mozilla.geckoview.GeckoRuntime
+import androidx.core.os.LocaleListCompat as LocaleList
+
+/**
+ * Class to set the locales setting for geckoview, updating from the locale of the device.
+ */
+class LocaleSettingUpdater(
+ private val context: Context,
+ private val runtime: GeckoRuntime,
+) : SettingUpdater<Array<String>>() {
+
+ override var value: Array<String> = findValue()
+ set(value) {
+ runtime.settings.locales = value
+ field = value
+ }
+
+ private val localeChangedReceiver by lazy {
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ updateValue()
+ }
+ }
+ }
+
+ override fun registerForUpdates() {
+ context.registerReceiverCompat(
+ localeChangedReceiver,
+ IntentFilter(Intent.ACTION_LOCALE_CHANGED),
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+ }
+
+ override fun unregisterForUpdates() {
+ context.unregisterReceiver(localeChangedReceiver)
+ }
+
+ override fun findValue(): Array<String> {
+ val localeList = LocaleList.getAdjustedDefault()
+ return arrayOfNulls<Unit>(localeList.size())
+ .mapIndexedNotNull { i, _ -> localeList.get(i)?.toLanguageTag() }
+ .toTypedArray()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/SettingUpdater.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/SettingUpdater.kt
new file mode 100644
index 0000000000..af4d455a79
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/SettingUpdater.kt
@@ -0,0 +1,45 @@
+/* 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.browser.engine.gecko.integration
+
+abstract class SettingUpdater<T> {
+ /**
+ * Toggle the automatic tracking of a setting derived from the device state.
+ */
+ var enabled: Boolean = false
+ set(value) {
+ if (value) {
+ updateValue()
+ registerForUpdates()
+ } else {
+ unregisterForUpdates()
+ }
+ field = value
+ }
+
+ /**
+ * The setter for this property should change the GeckoView setting.
+ */
+ abstract var value: T
+
+ internal fun updateValue() {
+ value = findValue()
+ }
+
+ /**
+ * Register for updates from the device state. This is setting specific.
+ */
+ abstract fun registerForUpdates()
+
+ /**
+ * Unregister for updates from the device state.
+ */
+ abstract fun unregisterForUpdates()
+
+ /**
+ * Find the value of the setting from the device state. This is setting specific.
+ */
+ abstract fun findValue(): T
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegate.kt
new file mode 100644
index 0000000000..1d807dde02
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegate.kt
@@ -0,0 +1,53 @@
+/* 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.browser.engine.gecko.media
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.media.RecordingDevice
+import org.mozilla.geckoview.GeckoSession
+import java.security.InvalidParameterException
+import org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice as GeckoRecordingDevice
+
+/**
+ * Gecko-based GeckoMediaDelegate implementation.
+ */
+internal class GeckoMediaDelegate(private val geckoEngineSession: GeckoEngineSession) :
+ GeckoSession.MediaDelegate {
+
+ override fun onRecordingStatusChanged(
+ session: GeckoSession,
+ geckoDevices: Array<out GeckoRecordingDevice>,
+ ) {
+ val devices = geckoDevices.map { geckoRecording ->
+ val type = geckoRecording.toType()
+ val status = geckoRecording.toStatus()
+ RecordingDevice(type, status)
+ }
+ geckoEngineSession.notifyObservers { onRecordingStateChanged(devices) }
+ }
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun GeckoRecordingDevice.toType(): RecordingDevice.Type {
+ return when (type) {
+ GeckoRecordingDevice.Type.CAMERA -> RecordingDevice.Type.CAMERA
+ GeckoRecordingDevice.Type.MICROPHONE -> RecordingDevice.Type.MICROPHONE
+ else -> {
+ throw InvalidParameterException("Unexpected Gecko Media type $type status $status")
+ }
+ }
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun GeckoRecordingDevice.toStatus(): RecordingDevice.Status {
+ return when (status) {
+ GeckoRecordingDevice.Status.RECORDING -> RecordingDevice.Status.RECORDING
+ GeckoRecordingDevice.Status.INACTIVE -> RecordingDevice.Status.INACTIVE
+ else -> {
+ throw InvalidParameterException("Unexpected Gecko Media type $type status $status")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediaquery/PreferredColorScheme.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediaquery/PreferredColorScheme.kt
new file mode 100644
index 0000000000..bb62a71dc6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediaquery/PreferredColorScheme.kt
@@ -0,0 +1,22 @@
+/* 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.browser.engine.gecko.mediaquery
+
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import org.mozilla.geckoview.GeckoRuntimeSettings
+
+internal fun PreferredColorScheme.Companion.from(geckoValue: Int) =
+ when (geckoValue) {
+ GeckoRuntimeSettings.COLOR_SCHEME_DARK -> PreferredColorScheme.Dark
+ GeckoRuntimeSettings.COLOR_SCHEME_LIGHT -> PreferredColorScheme.Light
+ GeckoRuntimeSettings.COLOR_SCHEME_SYSTEM -> PreferredColorScheme.System
+ else -> PreferredColorScheme.System
+ }
+
+internal fun PreferredColorScheme.toGeckoValue() =
+ when (this) {
+ is PreferredColorScheme.Dark -> GeckoRuntimeSettings.COLOR_SCHEME_DARK
+ is PreferredColorScheme.Light -> GeckoRuntimeSettings.COLOR_SCHEME_LIGHT
+ is PreferredColorScheme.System -> GeckoRuntimeSettings.COLOR_SCHEME_SYSTEM
+ }
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionController.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionController.kt
new file mode 100644
index 0000000000..5c65652faf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionController.kt
@@ -0,0 +1,56 @@
+/* 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.browser.engine.gecko.mediasession
+
+import mozilla.components.concept.engine.mediasession.MediaSession
+import org.mozilla.geckoview.MediaSession as GeckoViewMediaSession
+
+/**
+ * [MediaSession.Controller] (`concept-engine`) implementation for GeckoView.
+ */
+internal class GeckoMediaSessionController(
+ private val mediaSession: GeckoViewMediaSession,
+) : MediaSession.Controller {
+
+ override fun pause() {
+ mediaSession.pause()
+ }
+
+ override fun stop() {
+ mediaSession.stop()
+ }
+
+ override fun play() {
+ mediaSession.play()
+ }
+
+ override fun seekTo(time: Double, fast: Boolean) {
+ mediaSession.seekTo(time, fast)
+ }
+
+ override fun seekForward() {
+ mediaSession.seekForward()
+ }
+
+ override fun seekBackward() {
+ mediaSession.seekBackward()
+ }
+
+ override fun nextTrack() {
+ mediaSession.nextTrack()
+ }
+
+ override fun previousTrack() {
+ mediaSession.previousTrack()
+ }
+
+ override fun skipAd() {
+ mediaSession.skipAd()
+ }
+
+ override fun muteAudio(mute: Boolean) {
+ mediaSession.muteAudio(mute)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegate.kt
new file mode 100644
index 0000000000..68c0a7cb5b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegate.kt
@@ -0,0 +1,125 @@
+/* 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.browser.engine.gecko.mediasession
+
+import android.graphics.Bitmap
+import kotlinx.coroutines.withTimeoutOrNull
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.browser.engine.gecko.await
+import mozilla.components.concept.engine.mediasession.MediaSession
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.Image.ImageProcessingException
+import org.mozilla.geckoview.MediaSession as GeckoViewMediaSession
+
+private const val ARTWORK_RETRIEVE_TIMEOUT = 1000L
+private const val ARTWORK_IMAGE_SIZE = 48
+
+internal class GeckoMediaSessionDelegate(
+ private val engineSession: GeckoEngineSession,
+) : GeckoViewMediaSession.Delegate {
+
+ override fun onActivated(geckoSession: GeckoSession, mediaSession: GeckoViewMediaSession) {
+ engineSession.notifyObservers {
+ onMediaActivated(GeckoMediaSessionController(mediaSession))
+ }
+ }
+
+ override fun onDeactivated(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
+ engineSession.notifyObservers {
+ onMediaDeactivated()
+ }
+ }
+
+ override fun onMetadata(
+ session: GeckoSession,
+ mediaSession: GeckoViewMediaSession,
+ metaData: GeckoViewMediaSession.Metadata,
+ ) {
+ val getArtwork: (suspend () -> Bitmap?)? = metaData.artwork?.let {
+ {
+ try {
+ withTimeoutOrNull(ARTWORK_RETRIEVE_TIMEOUT) {
+ it.getBitmap(ARTWORK_IMAGE_SIZE).await()
+ }
+ } catch (e: ImageProcessingException) {
+ null
+ }
+ }
+ }
+
+ engineSession.notifyObservers {
+ onMediaMetadataChanged(
+ MediaSession.Metadata(metaData.title, metaData.artist, metaData.album, getArtwork),
+ )
+ }
+ }
+
+ override fun onFeatures(
+ session: GeckoSession,
+ mediaSession: GeckoViewMediaSession,
+ features: Long,
+ ) {
+ engineSession.notifyObservers {
+ onMediaFeatureChanged(MediaSession.Feature(features))
+ }
+ }
+
+ override fun onPlay(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
+ engineSession.notifyObservers {
+ onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
+ }
+ }
+
+ override fun onPause(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
+ engineSession.notifyObservers {
+ onMediaPlaybackStateChanged(MediaSession.PlaybackState.PAUSED)
+ }
+ }
+
+ override fun onStop(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
+ engineSession.notifyObservers {
+ onMediaPlaybackStateChanged(MediaSession.PlaybackState.STOPPED)
+ }
+ }
+
+ override fun onPositionState(
+ session: GeckoSession,
+ mediaSession: GeckoViewMediaSession,
+ positionState: GeckoViewMediaSession.PositionState,
+ ) {
+ engineSession.notifyObservers {
+ onMediaPositionStateChanged(
+ MediaSession.PositionState(
+ positionState.duration,
+ positionState.position,
+ positionState.playbackRate,
+ ),
+ )
+ }
+ }
+
+ override fun onFullscreen(
+ session: GeckoSession,
+ mediaSession: GeckoViewMediaSession,
+ enabled: Boolean,
+ elementMetaData: GeckoViewMediaSession.ElementMetadata?,
+ ) {
+ val sessionElementMetaData =
+ elementMetaData?.let {
+ MediaSession.ElementMetadata(
+ elementMetaData.source,
+ elementMetaData.duration,
+ elementMetaData.width,
+ elementMetaData.height,
+ elementMetaData.audioTrackCount,
+ elementMetaData.videoTrackCount,
+ )
+ }
+
+ engineSession.notifyObservers {
+ onMediaFullscreenChanged(enabled, sessionElementMetaData)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt
new file mode 100644
index 0000000000..96c17ae7fa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt
@@ -0,0 +1,185 @@
+/* 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.browser.engine.gecko.permission
+
+import android.Manifest.permission.ACCESS_COARSE_LOCATION
+import android.Manifest.permission.ACCESS_FINE_LOCATION
+import android.Manifest.permission.CAMERA
+import android.Manifest.permission.RECORD_AUDIO
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.concept.engine.permission.PermissionRequest
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_AUDIOCAPTURE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_CAMERA
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_MICROPHONE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_OTHER
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_SCREEN
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS
+import java.util.UUID
+
+/**
+ * Gecko-based implementation of [PermissionRequest].
+ *
+ * @property permissions the list of requested permissions.
+ * @property callback the callback to grant/reject the requested permissions.
+ * @property id a unique identifier for the request.
+ */
+sealed class GeckoPermissionRequest constructor(
+ override val permissions: List<Permission>,
+ private val callback: PermissionDelegate.Callback? = null,
+ override val id: String = UUID.randomUUID().toString(),
+) : PermissionRequest {
+
+ /**
+ * Represents a gecko-based content permission request.
+ *
+ * @property uri the URI of the content requesting the permissions.
+ * @property type the type of the requested content permission (will be
+ * mapped to corresponding [Permission]).
+ * @property geckoPermission Indicates which gecko permissions is requested.
+ * @property geckoResult the gecko result that serves as a callback to grant/reject the requested permissions.
+ */
+ data class Content(
+ override val uri: String,
+ private val type: Int,
+ internal val geckoPermission: PermissionDelegate.ContentPermission,
+ internal val geckoResult: GeckoResult<Int>,
+ ) : GeckoPermissionRequest(
+ listOf(permissionsMap.getOrElse(type) { Permission.Generic("$type", "Gecko permission type = $type") }),
+ ) {
+ companion object {
+ val permissionsMap = mapOf(
+ PERMISSION_DESKTOP_NOTIFICATION to Permission.ContentNotification(),
+ PERMISSION_GEOLOCATION to Permission.ContentGeoLocation(),
+ PERMISSION_AUTOPLAY_AUDIBLE to Permission.ContentAutoPlayAudible(),
+ PERMISSION_AUTOPLAY_INAUDIBLE to Permission.ContentAutoPlayInaudible(),
+ PERMISSION_PERSISTENT_STORAGE to Permission.ContentPersistentStorage(),
+ PERMISSION_MEDIA_KEY_SYSTEM_ACCESS to Permission.ContentMediaKeySystemAccess(),
+ PERMISSION_STORAGE_ACCESS to Permission.ContentCrossOriginStorageAccess(),
+ )
+ }
+
+ @VisibleForTesting
+ internal var isCompleted = false
+
+ override fun grant(permissions: List<Permission>) {
+ if (!isCompleted) {
+ geckoResult.complete(VALUE_ALLOW)
+ }
+ isCompleted = true
+ }
+
+ override fun reject() {
+ if (!isCompleted) {
+ geckoResult.complete(VALUE_DENY)
+ }
+ isCompleted = true
+ }
+ }
+
+ /**
+ * Represents a gecko-based application permission request.
+ *
+ * @property uri the URI of the content requesting the permissions.
+ * @property nativePermissions the list of requested app permissions (will be
+ * mapped to corresponding [Permission]s).
+ * @property callback the callback to grant/reject the requested permissions.
+ */
+ data class App(
+ private val nativePermissions: List<String>,
+ private val callback: PermissionDelegate.Callback,
+ ) : GeckoPermissionRequest(
+ nativePermissions.map { permissionsMap.getOrElse(it) { Permission.Generic(it) } },
+ callback,
+ ) {
+ override val uri: String? = null
+
+ companion object {
+ val permissionsMap = mapOf(
+ ACCESS_COARSE_LOCATION to Permission.AppLocationCoarse(ACCESS_COARSE_LOCATION),
+ ACCESS_FINE_LOCATION to Permission.AppLocationFine(ACCESS_FINE_LOCATION),
+ CAMERA to Permission.AppCamera(CAMERA),
+ RECORD_AUDIO to Permission.AppAudio(RECORD_AUDIO),
+ )
+ }
+ }
+
+ /**
+ * Represents a gecko-based media permission request.
+ *
+ * @property uri the URI of the content requesting the permissions.
+ * @property videoSources the list of requested video sources (will be
+ * mapped to the corresponding [Permission]).
+ * @property audioSources the list of requested audio sources (will be
+ * mapped to corresponding [Permission]).
+ * @property callback the callback to grant/reject the requested permissions.
+ */
+ data class Media(
+ override val uri: String,
+ private val videoSources: List<MediaSource>,
+ private val audioSources: List<MediaSource>,
+ private val callback: PermissionDelegate.MediaCallback,
+ ) : GeckoPermissionRequest(
+ videoSources.map { mapPermission(it) } + audioSources.map { mapPermission(it) },
+ ) {
+ override fun grant(permissions: List<Permission>) {
+ val videos = permissions.mapNotNull { permission -> videoSources.find { it.id == permission.id } }
+ val audios = permissions.mapNotNull { permission -> audioSources.find { it.id == permission.id } }
+ callback.grant(videos.firstOrNull(), audios.firstOrNull())
+ }
+
+ override fun containsVideoAndAudioSources(): Boolean {
+ return videoSources.isNotEmpty() && audioSources.isNotEmpty()
+ }
+
+ override fun reject() {
+ callback.reject()
+ }
+
+ companion object {
+ fun mapPermission(mediaSource: MediaSource): Permission =
+ if (mediaSource.type == MediaSource.TYPE_AUDIO) {
+ mapAudioPermission(mediaSource)
+ } else {
+ mapVideoPermission(mediaSource)
+ }
+
+ @Suppress("SwitchIntDef")
+ private fun mapAudioPermission(mediaSource: MediaSource) = when (mediaSource.source) {
+ SOURCE_AUDIOCAPTURE -> Permission.ContentAudioCapture(mediaSource.id, mediaSource.name)
+ SOURCE_MICROPHONE -> Permission.ContentAudioMicrophone(mediaSource.id, mediaSource.name)
+ SOURCE_OTHER -> Permission.ContentAudioOther(mediaSource.id, mediaSource.name)
+ else -> Permission.Generic(mediaSource.id, mediaSource.name)
+ }
+
+ @Suppress("ComplexMethod", "SwitchIntDef")
+ private fun mapVideoPermission(mediaSource: MediaSource) = when (mediaSource.source) {
+ SOURCE_CAMERA -> Permission.ContentVideoCamera(mediaSource.id, mediaSource.name)
+ SOURCE_SCREEN -> Permission.ContentVideoScreen(mediaSource.id, mediaSource.name)
+ SOURCE_OTHER -> Permission.ContentVideoOther(mediaSource.id, mediaSource.name)
+ else -> Permission.Generic(mediaSource.id, mediaSource.name)
+ }
+ }
+ }
+
+ override fun grant(permissions: List<Permission>) {
+ callback?.grant()
+ }
+
+ override fun reject() {
+ callback?.reject()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt
new file mode 100644
index 0000000000..20d0c8ae8e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt
@@ -0,0 +1,461 @@
+/* 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.browser.engine.gecko.permission
+
+import androidx.annotation.VisibleForTesting
+import androidx.paging.DataSource
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.engine.gecko.await
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage
+import mozilla.components.support.ktx.kotlin.stripDefaultPort
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_PROMPT
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
+import org.mozilla.geckoview.StorageController
+import org.mozilla.geckoview.StorageController.ClearFlags
+
+/**
+ * A storage to save [SitePermissions] using GeckoView APIs.
+ */
+@Suppress("LargeClass")
+class GeckoSitePermissionsStorage(
+ runtime: GeckoRuntime,
+ private val onDiskStorage: SitePermissionsStorage,
+) : SitePermissionsStorage {
+
+ private val geckoStorage: StorageController = runtime.storageController
+ private val mainScope = CoroutineScope(Dispatchers.Main)
+
+ /*
+ * Temporary permissions are created when users doesn't
+ * check the 'Remember my decision checkbox'. At the moment,
+ * gecko view doesn't handle temporary permission,
+ * we have to store them in memory, and clear them manually,
+ * until we have an API for it see:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1710447
+ */
+ @VisibleForTesting
+ internal val geckoTemporaryPermissions = mutableListOf<ContentPermission>()
+
+ override suspend fun save(sitePermissions: SitePermissions, request: PermissionRequest?, private: Boolean) {
+ val geckoSavedPermissions = updateGeckoPermissionIfNeeded(sitePermissions, request, private)
+ onDiskStorage.save(geckoSavedPermissions, request, private)
+ }
+
+ override fun saveTemporary(request: PermissionRequest?) {
+ if (request is GeckoPermissionRequest.Content) {
+ geckoTemporaryPermissions.add(request.geckoPermission)
+ }
+ }
+
+ override fun clearTemporaryPermissions() {
+ geckoTemporaryPermissions.forEach {
+ geckoStorage.setPermission(it, VALUE_PROMPT)
+ }
+ geckoTemporaryPermissions.clear()
+ }
+
+ override suspend fun update(sitePermissions: SitePermissions, private: Boolean) {
+ val updatedPermission = updateGeckoPermissionIfNeeded(sitePermissions, private = private)
+ onDiskStorage.update(updatedPermission, private)
+ }
+
+ override suspend fun findSitePermissionsBy(
+ origin: String,
+ includeTemporary: Boolean,
+ private: Boolean,
+ ): SitePermissions? {
+ /**
+ * GeckoView ony persists [GeckoPermissionRequest.Content] other permissions like
+ * [GeckoPermissionRequest.Media], we have to store them ourselves.
+ * For this reason, we query both storage ([geckoStorage] and [onDiskStorage]) and
+ * merge both results into one [SitePermissions] object.
+ */
+ val onDiskPermission: SitePermissions? =
+ onDiskStorage.findSitePermissionsBy(origin, private = private)
+ val geckoPermissions = findGeckoContentPermissionBy(origin, includeTemporary, private).groupByType()
+
+ return mergePermissions(onDiskPermission, geckoPermissions)
+ }
+
+ override suspend fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissions> {
+ val geckoPermissionsByHost = findAllGeckoContentPermissions().groupByDomain()
+
+ return onDiskStorage.getSitePermissionsPaged().map { onDiskPermission ->
+ val geckoPermissions = geckoPermissionsByHost[onDiskPermission.origin].groupByType()
+ mergePermissions(onDiskPermission, geckoPermissions) ?: onDiskPermission
+ }
+ }
+
+ override suspend fun remove(sitePermissions: SitePermissions, private: Boolean) {
+ onDiskStorage.remove(sitePermissions, private)
+ removeGeckoContentPermissionBy(sitePermissions.origin, private)
+ }
+
+ override suspend fun removeAll() {
+ onDiskStorage.removeAll()
+ removeGeckoAllContentPermissions()
+ }
+
+ override suspend fun all(): List<SitePermissions> {
+ val onDiskPermissions: List<SitePermissions> = onDiskStorage.all()
+ val geckoPermissionsByHost = findAllGeckoContentPermissions().groupByDomain()
+
+ return onDiskPermissions.mapNotNull { onDiskPermission ->
+ val map = geckoPermissionsByHost[onDiskPermission.origin].groupByType()
+ mergePermissions(onDiskPermission, map)
+ }
+ }
+
+ @VisibleForTesting
+ internal suspend fun findAllGeckoContentPermissions(): List<ContentPermission>? {
+ return withContext(mainScope.coroutineContext) {
+ geckoStorage.allPermissions.await()
+ .filterNotTemporaryPermissions(geckoTemporaryPermissions)
+ }
+ }
+
+ /**
+ * Updates the [geckoStorage] if the provided [userSitePermissions]
+ * exists on the [geckoStorage] or it's provided as a part of the [permissionRequest]
+ * otherwise nothing is updated.
+ * @param userSitePermissions the values provided by the user to be updated.
+ * @param permissionRequest the [PermissionRequest] from the web content.
+ * @return An updated [SitePermissions] with default values, if they were updated
+ * on the [geckoStorage] otherwise the same [SitePermissions].
+ */
+ @VisibleForTesting
+ @Suppress("LongMethod")
+ internal suspend fun updateGeckoPermissionIfNeeded(
+ userSitePermissions: SitePermissions,
+ permissionRequest: PermissionRequest? = null,
+ private: Boolean,
+ ): SitePermissions {
+ var updatedPermission = userSitePermissions
+ val geckoPermissionsByType =
+ permissionRequest.extractGeckoPermissionsOrQueryTheStore(userSitePermissions.origin, private)
+
+ if (geckoPermissionsByType.isNotEmpty()) {
+ val geckoNotification = geckoPermissionsByType[PERMISSION_DESKTOP_NOTIFICATION]?.firstOrNull()
+ val geckoLocation = geckoPermissionsByType[PERMISSION_GEOLOCATION]?.firstOrNull()
+ val geckoMedia = geckoPermissionsByType[PERMISSION_MEDIA_KEY_SYSTEM_ACCESS]?.firstOrNull()
+ val geckoLocalStorage = geckoPermissionsByType[PERMISSION_PERSISTENT_STORAGE]?.firstOrNull()
+ val geckoCrossOriginStorageAccess = geckoPermissionsByType[PERMISSION_STORAGE_ACCESS]?.firstOrNull()
+ val geckoAudible = geckoPermissionsByType[PERMISSION_AUTOPLAY_AUDIBLE]?.firstOrNull()
+ val geckoInAudible = geckoPermissionsByType[PERMISSION_AUTOPLAY_INAUDIBLE]?.firstOrNull()
+
+ /*
+ * To avoid GeckoView caching previous request, we need to clear, previous data
+ * before updating. See: https://github.com/mozilla-mobile/android-components/issues/6322
+ */
+ clearGeckoCacheFor(updatedPermission.origin)
+
+ if (geckoNotification != null) {
+ removeTemporaryPermissionIfAny(geckoNotification)
+ geckoStorage.setPermission(
+ geckoNotification,
+ userSitePermissions.notification.toGeckoStatus(),
+ )
+ updatedPermission = updatedPermission.copy(notification = NO_DECISION)
+ }
+
+ if (geckoLocation != null) {
+ removeTemporaryPermissionIfAny(geckoLocation)
+ geckoStorage.setPermission(
+ geckoLocation,
+ userSitePermissions.location.toGeckoStatus(),
+ )
+ updatedPermission = updatedPermission.copy(location = NO_DECISION)
+ }
+
+ if (geckoMedia != null) {
+ removeTemporaryPermissionIfAny(geckoMedia)
+ geckoStorage.setPermission(
+ geckoMedia,
+ userSitePermissions.mediaKeySystemAccess.toGeckoStatus(),
+ )
+ updatedPermission = updatedPermission.copy(mediaKeySystemAccess = NO_DECISION)
+ }
+
+ if (geckoLocalStorage != null) {
+ removeTemporaryPermissionIfAny(geckoLocalStorage)
+ geckoStorage.setPermission(
+ geckoLocalStorage,
+ userSitePermissions.localStorage.toGeckoStatus(),
+ )
+ updatedPermission = updatedPermission.copy(localStorage = NO_DECISION)
+ }
+
+ if (geckoCrossOriginStorageAccess != null) {
+ removeTemporaryPermissionIfAny(geckoCrossOriginStorageAccess)
+ geckoStorage.setPermission(
+ geckoCrossOriginStorageAccess,
+ userSitePermissions.crossOriginStorageAccess.toGeckoStatus(),
+ )
+ updatedPermission = updatedPermission.copy(crossOriginStorageAccess = NO_DECISION)
+ }
+
+ if (geckoAudible != null) {
+ removeTemporaryPermissionIfAny(geckoAudible)
+ geckoStorage.setPermission(
+ geckoAudible,
+ userSitePermissions.autoplayAudible.toGeckoStatus(),
+ )
+ updatedPermission = updatedPermission.copy(autoplayAudible = AutoplayStatus.BLOCKED)
+ }
+
+ if (geckoInAudible != null) {
+ removeTemporaryPermissionIfAny(geckoInAudible)
+ geckoStorage.setPermission(
+ geckoInAudible,
+ userSitePermissions.autoplayInaudible.toGeckoStatus(),
+ )
+ updatedPermission =
+ updatedPermission.copy(autoplayInaudible = AutoplayStatus.BLOCKED)
+ }
+ }
+ return updatedPermission
+ }
+
+ /**
+ * Combines a permission that comes from our on disk storage with the gecko permissions,
+ * and combined both into a single a [SitePermissions].
+ * @param onDiskPermissions a permission from the on disk storage.
+ * @param geckoPermissionByType a list of all the gecko permissions mapped by permission type.
+ * @return a [SitePermissions] containing the values from the on disk and gecko permission.
+ */
+ @VisibleForTesting
+ @Suppress("ComplexMethod")
+ internal fun mergePermissions(
+ onDiskPermissions: SitePermissions?,
+ geckoPermissionByType: Map<Int, List<ContentPermission>>,
+ ): SitePermissions? {
+ var combinedPermissions = onDiskPermissions
+
+ if (geckoPermissionByType.isNotEmpty() && onDiskPermissions != null) {
+ val geckoNotification = geckoPermissionByType[PERMISSION_DESKTOP_NOTIFICATION]?.firstOrNull()
+ val geckoLocation = geckoPermissionByType[PERMISSION_GEOLOCATION]?.firstOrNull()
+ val geckoMedia = geckoPermissionByType[PERMISSION_MEDIA_KEY_SYSTEM_ACCESS]?.firstOrNull()
+ val geckoStorage = geckoPermissionByType[PERMISSION_PERSISTENT_STORAGE]?.firstOrNull()
+ // Currently we'll receive the "storage_access" permission for all iframes of the same parent
+ // so we need to ensure we are reporting the permission for the current iframe request.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1746436 for more details.
+ val geckoCrossOriginStorageAccess = geckoPermissionByType[PERMISSION_STORAGE_ACCESS]?.firstOrNull {
+ it.thirdPartyOrigin == onDiskPermissions.origin.stripDefaultPort()
+ }
+ val geckoAudible = geckoPermissionByType[PERMISSION_AUTOPLAY_AUDIBLE]?.firstOrNull()
+ val geckoInAudible = geckoPermissionByType[PERMISSION_AUTOPLAY_INAUDIBLE]?.firstOrNull()
+
+ /**
+ * We only consider permissions from geckoView, when the values default value
+ * has been changed otherwise we favor the values [onDiskPermissions].
+ */
+ if (geckoNotification != null && geckoNotification.value != VALUE_PROMPT) {
+ combinedPermissions = combinedPermissions?.copy(
+ notification = geckoNotification.value.toStatus(),
+ )
+ }
+
+ if (geckoLocation != null && geckoLocation.value != VALUE_PROMPT) {
+ combinedPermissions = combinedPermissions?.copy(
+ location = geckoLocation.value.toStatus(),
+ )
+ }
+
+ if (geckoMedia != null && geckoMedia.value != VALUE_PROMPT) {
+ combinedPermissions = combinedPermissions?.copy(
+ mediaKeySystemAccess = geckoMedia.value.toStatus(),
+ )
+ }
+
+ if (geckoStorage != null && geckoStorage.value != VALUE_PROMPT) {
+ combinedPermissions = combinedPermissions?.copy(
+ localStorage = geckoStorage.value.toStatus(),
+ )
+ }
+
+ if (geckoCrossOriginStorageAccess != null && geckoCrossOriginStorageAccess.value != VALUE_PROMPT) {
+ combinedPermissions = combinedPermissions?.copy(
+ crossOriginStorageAccess = geckoCrossOriginStorageAccess.value.toStatus(),
+ )
+ }
+
+ /**
+ * Autoplay permissions don't have initial values, so when the value is changed on
+ * the gecko storage we trust it.
+ */
+ if (geckoAudible != null && geckoAudible.value != VALUE_PROMPT) {
+ combinedPermissions = combinedPermissions?.copy(
+ autoplayAudible = geckoAudible.value.toAutoPlayStatus(),
+ )
+ }
+
+ if (geckoInAudible != null && geckoInAudible.value != VALUE_PROMPT) {
+ combinedPermissions = combinedPermissions?.copy(
+ autoplayInaudible = geckoInAudible.value.toAutoPlayStatus(),
+ )
+ }
+ }
+ return combinedPermissions
+ }
+
+ @VisibleForTesting
+ internal suspend fun findGeckoContentPermissionBy(
+ origin: String,
+ includeTemporary: Boolean = false,
+ private: Boolean,
+ ): List<ContentPermission>? {
+ return withContext(mainScope.coroutineContext) {
+ val geckoPermissions = geckoStorage.getPermissions(origin, private).await()
+ if (includeTemporary) {
+ geckoPermissions
+ } else {
+ geckoPermissions.filterNotTemporaryPermissions(geckoTemporaryPermissions)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal suspend fun clearGeckoCacheFor(origin: String) {
+ withContext(mainScope.coroutineContext) {
+ geckoStorage.clearDataFromHost(origin, ClearFlags.PERMISSIONS).await()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun clearAllPermissionsGeckoCache() {
+ geckoStorage.clearData(ClearFlags.PERMISSIONS)
+ }
+
+ @VisibleForTesting
+ internal fun removeTemporaryPermissionIfAny(permission: ContentPermission) {
+ if (geckoTemporaryPermissions.any { permission.areSame(it) }) {
+ geckoTemporaryPermissions.removeAll { permission.areSame(it) }
+ }
+ }
+
+ @VisibleForTesting
+ internal suspend fun removeGeckoContentPermissionBy(origin: String, private: Boolean) {
+ findGeckoContentPermissionBy(
+ origin = origin,
+ private = private,
+ )?.forEach { geckoPermissions ->
+ removeGeckoContentPermission(geckoPermissions)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun removeGeckoContentPermission(geckoPermissions: ContentPermission) {
+ val value = if (geckoPermissions.permission != PERMISSION_TRACKING) {
+ VALUE_PROMPT
+ } else {
+ VALUE_DENY
+ }
+ geckoStorage.setPermission(geckoPermissions, value)
+ removeTemporaryPermissionIfAny(geckoPermissions)
+ }
+
+ @VisibleForTesting
+ internal suspend fun removeGeckoAllContentPermissions() {
+ findAllGeckoContentPermissions()?.forEach { geckoPermissions ->
+ removeGeckoContentPermission(geckoPermissions)
+ }
+ clearAllPermissionsGeckoCache()
+ }
+
+ private suspend fun PermissionRequest?.extractGeckoPermissionsOrQueryTheStore(
+ origin: String,
+ private: Boolean,
+ ): Map<Int, List<ContentPermission>> {
+ return if (this is GeckoPermissionRequest.Content) {
+ mapOf(geckoPermission.permission to listOf(geckoPermission))
+ } else {
+ findGeckoContentPermissionBy(origin, includeTemporary = true, private).groupByType()
+ }
+ }
+}
+
+@VisibleForTesting
+internal fun List<ContentPermission>?.groupByDomain(): Map<String, List<ContentPermission>> {
+ return this?.groupBy {
+ it.uri.tryGetHostFromUrl()
+ }.orEmpty()
+}
+
+@VisibleForTesting
+internal fun List<ContentPermission>?.groupByType(): Map<Int, List<ContentPermission>> {
+ return this?.groupBy { it.permission }.orEmpty()
+}
+
+@VisibleForTesting
+internal fun List<ContentPermission>?.filterNotTemporaryPermissions(
+ temporaryPermissions: List<ContentPermission>,
+): List<ContentPermission>? {
+ return this?.filterNot { geckoPermission ->
+ temporaryPermissions.any { geckoPermission.areSame(it) }
+ }
+}
+
+@VisibleForTesting
+internal fun ContentPermission.areSame(other: ContentPermission) =
+ other.uri.tryGetHostFromUrl() == this.uri.tryGetHostFromUrl() &&
+ other.permission == this.permission && other.privateMode == this.privateMode
+
+@VisibleForTesting
+internal fun Int.toStatus(): Status {
+ return when (this) {
+ VALUE_PROMPT -> NO_DECISION
+ VALUE_DENY -> BLOCKED
+ VALUE_ALLOW -> ALLOWED
+ else -> BLOCKED
+ }
+}
+
+@VisibleForTesting
+internal fun Int.toAutoPlayStatus(): AutoplayStatus {
+ return when (this) {
+ VALUE_PROMPT, VALUE_DENY -> AutoplayStatus.BLOCKED
+ VALUE_ALLOW -> AutoplayStatus.ALLOWED
+ else -> AutoplayStatus.BLOCKED
+ }
+}
+
+@VisibleForTesting
+internal fun Status.toGeckoStatus(): Int {
+ return when (this) {
+ NO_DECISION -> VALUE_PROMPT
+ BLOCKED -> VALUE_DENY
+ ALLOWED -> VALUE_ALLOW
+ else -> VALUE_ALLOW
+ }
+}
+
+@VisibleForTesting
+internal fun AutoplayStatus.toGeckoStatus(): Int {
+ return when (this) {
+ AutoplayStatus.BLOCKED -> VALUE_DENY
+ AutoplayStatus.ALLOWED -> VALUE_ALLOW
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt
new file mode 100644
index 0000000000..b257565473
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt
@@ -0,0 +1,84 @@
+/* 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.browser.engine.gecko.profiler
+
+import mozilla.components.concept.base.profiler.Profiler
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+
+/**
+ * Gecko-based implementation of [Profiler], wrapping the
+ * ProfilerController object provided by GeckoView.
+ */
+class Profiler(
+ private val runtime: GeckoRuntime,
+) : Profiler {
+
+ /**
+ * See [Profiler.isProfilerActive].
+ */
+ override fun isProfilerActive(): Boolean {
+ return runtime.profilerController.isProfilerActive
+ }
+
+ /**
+ * See [Profiler.getProfilerTime].
+ */
+ override fun getProfilerTime(): Double? {
+ return runtime.profilerController.profilerTime
+ }
+
+ /**
+ * See [Profiler.addMarker].
+ */
+ override fun addMarker(markerName: String, startTime: Double?, endTime: Double?, text: String?) {
+ runtime.profilerController.addMarker(markerName, startTime, endTime, text)
+ }
+
+ /**
+ * See [Profiler.addMarker].
+ */
+ override fun addMarker(markerName: String, startTime: Double?, text: String?) {
+ runtime.profilerController.addMarker(markerName, startTime, text)
+ }
+
+ /**
+ * See [Profiler.addMarker].
+ */
+ override fun addMarker(markerName: String, startTime: Double?) {
+ runtime.profilerController.addMarker(markerName, startTime)
+ }
+
+ /**
+ * See [Profiler.addMarker].
+ */
+ override fun addMarker(markerName: String, text: String?) {
+ runtime.profilerController.addMarker(markerName, text)
+ }
+
+ /**
+ * See [Profiler.addMarker].
+ */
+ override fun addMarker(markerName: String) {
+ runtime.profilerController.addMarker(markerName)
+ }
+
+ override fun startProfiler(filters: Array<String>, features: Array<String>) {
+ runtime.profilerController.startProfiler(filters, features)
+ }
+
+ override fun stopProfiler(onSuccess: (ByteArray?) -> Unit, onError: (Throwable) -> Unit) {
+ runtime.profilerController.stopProfiler().then(
+ { profileResult ->
+ onSuccess(profileResult)
+ GeckoResult<Void>()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult<Void>()
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegate.kt
new file mode 100644
index 0000000000..b8353e753a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegate.kt
@@ -0,0 +1,66 @@
+/* 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.browser.engine.gecko.prompt
+
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.browser.engine.gecko.ext.convertToChoices
+import mozilla.components.concept.engine.prompt.PromptRequest
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.BasePrompt
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.ChoicePrompt
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptInstanceDelegate
+
+/**
+ * Implementation of [PromptInstanceDelegate] used to update a
+ * prompt request when onPromptUpdate is invoked.
+ *
+ * @param geckoSession [GeckoEngineSession] used to notify the engine observer
+ * with the onPromptUpdate callback.
+ * @param previousPrompt [PromptRequest] to be updated.
+ */
+internal class ChoicePromptDelegate(
+ private val geckoSession: GeckoEngineSession,
+ private var previousPrompt: PromptRequest,
+) : PromptInstanceDelegate {
+
+ override fun onPromptDismiss(prompt: BasePrompt) {
+ geckoSession.notifyObservers {
+ onPromptDismissed(previousPrompt)
+ }
+ }
+
+ override fun onPromptUpdate(prompt: BasePrompt) {
+ if (prompt is ChoicePrompt) {
+ val promptRequest = updatePromptChoices(prompt)
+ if (promptRequest != null) {
+ geckoSession.notifyObservers {
+ this.onPromptUpdate(previousPrompt.uid, promptRequest)
+ }
+ previousPrompt = promptRequest
+ }
+ }
+ }
+
+ /**
+ * Use the received prompt to create the updated [PromptRequest]
+ * @param updatedPrompt The [ChoicePrompt] with the updated choices.
+ */
+ private fun updatePromptChoices(updatedPrompt: ChoicePrompt): PromptRequest? {
+ return when (previousPrompt) {
+ is PromptRequest.MenuChoice -> {
+ (previousPrompt as PromptRequest.MenuChoice)
+ .copy(choices = convertToChoices(updatedPrompt.choices))
+ }
+ is PromptRequest.SingleChoice -> {
+ (previousPrompt as PromptRequest.SingleChoice)
+ .copy(choices = convertToChoices(updatedPrompt.choices))
+ }
+ is PromptRequest.MultipleChoice -> {
+ (previousPrompt as PromptRequest.MultipleChoice)
+ .copy(choices = convertToChoices(updatedPrompt.choices))
+ }
+ else -> null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt
new file mode 100644
index 0000000000..d4276e675a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt
@@ -0,0 +1,911 @@
+/* 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.browser.engine.gecko.prompt
+
+import android.content.Context
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.browser.engine.gecko.ext.convertToChoices
+import mozilla.components.browser.engine.gecko.ext.toAddress
+import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
+import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard
+import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
+import mozilla.components.browser.engine.gecko.ext.toLoginEntry
+import mozilla.components.concept.engine.prompt.Choice
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.prompt.PromptRequest.File.Companion.DEFAULT_UPLOADS_DIR_NAME
+import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
+import mozilla.components.concept.engine.prompt.ShareData
+import mozilla.components.concept.identitycredential.Account
+import mozilla.components.concept.identitycredential.Provider
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.support.ktx.android.net.toFileUri
+import mozilla.components.support.ktx.kotlin.toDate
+import mozilla.components.support.utils.TimePicker.shouldShowMillisecondsPicker
+import mozilla.components.support.utils.TimePicker.shouldShowSecondsPicker
+import org.mozilla.geckoview.AllowOrDeny
+import org.mozilla.geckoview.Autocomplete
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoSession.PromptDelegate
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.AutocompleteRequest
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.BeforeUnloadPrompt
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATE
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATETIME_LOCAL
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.MONTH
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.TIME
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEEK
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.AccountSelectorPrompt
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.PrivacyPolicyPrompt
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.ProviderSelectorPrompt
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptResponse
+import java.security.InvalidParameterException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+typealias GeckoAuthOptions = PromptDelegate.AuthPrompt.AuthOptions
+typealias GeckoChoice = PromptDelegate.ChoicePrompt.Choice
+typealias GECKO_AUTH_FLAGS = PromptDelegate.AuthPrompt.AuthOptions.Flags
+typealias GECKO_AUTH_LEVEL = PromptDelegate.AuthPrompt.AuthOptions.Level
+typealias GECKO_PROMPT_FILE_TYPE = PromptDelegate.FilePrompt.Type
+typealias GECKO_PROMPT_PROVIDER_SELECTOR = ProviderSelectorPrompt.Provider
+typealias GECKO_PROMPT_ACCOUNT_SELECTOR = AccountSelectorPrompt.Account
+typealias GECKO_PROMPT_ACCOUNT_SELECTOR_PROVIDER = AccountSelectorPrompt.Provider
+typealias GECKO_PROMPT_CHOICE_TYPE = PromptDelegate.ChoicePrompt.Type
+typealias GECKO_PROMPT_FILE_CAPTURE = PromptDelegate.FilePrompt.Capture
+typealias GECKO_PROMPT_SHARE_RESULT = PromptDelegate.SharePrompt.Result
+typealias AC_AUTH_LEVEL = PromptRequest.Authentication.Level
+typealias AC_AUTH_METHOD = PromptRequest.Authentication.Method
+typealias AC_FILE_FACING_MODE = PromptRequest.File.FacingMode
+
+/**
+ * Gecko-based PromptDelegate implementation.
+ */
+@Suppress("LargeClass")
+internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSession) :
+ PromptDelegate {
+ override fun onSelectIdentityCredentialProvider(
+ session: GeckoSession,
+ prompt: ProviderSelectorPrompt,
+ ): GeckoResult<PromptResponse> {
+ val geckoResult = GeckoResult<PromptResponse>()
+
+ val onConfirm: (Provider) -> Unit = { provider ->
+ if (!prompt.isComplete) {
+ geckoResult.complete(
+ prompt.confirm(
+ provider.id,
+ ),
+ )
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ prompt.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.IdentityCredential.SelectProvider(
+ providers = prompt.providers.map { it.toProvider() },
+ onConfirm = onConfirm,
+ onDismiss = onDismiss,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onSelectIdentityCredentialAccount(
+ session: GeckoSession,
+ prompt: AccountSelectorPrompt,
+ ): GeckoResult<PromptResponse> {
+ val geckoResult = GeckoResult<PromptResponse>()
+
+ val onConfirm: (Account) -> Unit = { account ->
+ if (!prompt.isComplete) {
+ geckoResult.complete(
+ prompt.confirm(
+ account.id,
+ ),
+ )
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ prompt.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.IdentityCredential.SelectAccount(
+ accounts = prompt.accounts.map { it.toAccount() },
+ provider = prompt.provider.let { it.toProvider() },
+ onConfirm = onConfirm,
+ onDismiss = onDismiss,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onShowPrivacyPolicyIdentityCredential(
+ session: GeckoSession,
+ prompt: PrivacyPolicyPrompt,
+ ): GeckoResult<PromptResponse> {
+ val geckoResult = GeckoResult<PromptResponse>()
+
+ val onConfirm: (Boolean) -> Unit = { confirmed ->
+ if (!prompt.isComplete) {
+ geckoResult.complete(
+ prompt.confirm(confirmed),
+ )
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ prompt.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.IdentityCredential.PrivacyPolicy(
+ privacyPolicyUrl = prompt.privacyPolicyUrl,
+ termsOfServiceUrl = prompt.termsOfServiceUrl,
+ providerDomain = prompt.providerDomain,
+ host = prompt.host,
+ icon = prompt.icon,
+ onConfirm = onConfirm,
+ onDismiss = onDismiss,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onCreditCardSave(
+ session: GeckoSession,
+ request: AutocompleteRequest<Autocomplete.CreditCardSaveOption>,
+ ): GeckoResult<PromptResponse> {
+ val geckoResult = GeckoResult<PromptResponse>()
+
+ val onConfirm: (CreditCardEntry) -> Unit = { creditCard ->
+ if (!request.isComplete) {
+ geckoResult.complete(
+ request.confirm(
+ Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard()),
+ ),
+ )
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ request.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.SaveCreditCard(
+ creditCard = request.options[0].value.toCreditCardEntry(),
+ onConfirm = onConfirm,
+ onDismiss = onDismiss,
+ ).also {
+ request.delegate = PromptInstanceDismissDelegate(
+ geckoEngineSession,
+ it,
+ )
+ },
+ )
+ }
+
+ return geckoResult
+ }
+
+ /**
+ * Handle a credit card selection prompt request. This is triggered by the user
+ * focusing on a credit card input field.
+ *
+ * @param session The [GeckoSession] that triggered the request.
+ * @param request The [AutocompleteRequest] containing the credit card selection request.
+ */
+ override fun onCreditCardSelect(
+ session: GeckoSession,
+ request: AutocompleteRequest<Autocomplete.CreditCardSelectOption>,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+
+ val onConfirm: (CreditCardEntry) -> Unit = { creditCard ->
+ if (!request.isComplete) {
+ geckoResult.complete(
+ request.confirm(
+ Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard()),
+ ),
+ )
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ request.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.SelectCreditCard(
+ creditCards = request.options.map { it.value.toCreditCardEntry() },
+ onDismiss = onDismiss,
+ onConfirm = onConfirm,
+ ),
+ )
+ }
+
+ return geckoResult
+ }
+
+ override fun onLoginSave(
+ session: GeckoSession,
+ prompt: AutocompleteRequest<Autocomplete.LoginSaveOption>,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val onConfirmSave: (LoginEntry) -> Unit = { entry ->
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(Autocomplete.LoginSelectOption(entry.toLoginEntry())))
+ }
+ }
+ val onDismiss: () -> Unit = {
+ prompt.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.SaveLoginPrompt(
+ hint = prompt.options[0].hint,
+ logins = prompt.options.map { it.value.toLoginEntry() },
+ onConfirm = onConfirmSave,
+ onDismiss = onDismiss,
+ ).also {
+ prompt.delegate = PromptInstanceDismissDelegate(
+ geckoEngineSession,
+ it,
+ )
+ },
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onLoginSelect(
+ session: GeckoSession,
+ prompt: AutocompleteRequest<Autocomplete.LoginSelectOption>,
+ ): GeckoResult<PromptResponse>? {
+ val promptOptions = prompt.options
+ val generatedPassword =
+ if (promptOptions.isNotEmpty() && promptOptions.first().hint == Autocomplete.SelectOption.Hint.GENERATED) {
+ promptOptions.first().value.password
+ } else {
+ null
+ }
+ val geckoResult = GeckoResult<PromptResponse>()
+ val onConfirmSelect: (Login) -> Unit = { login ->
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(Autocomplete.LoginSelectOption(login.toLoginEntry())))
+ }
+ }
+ val onDismiss: () -> Unit = {
+ prompt.dismissSafely(geckoResult)
+ }
+
+ // `guid` plus exactly one of `httpRealm` and `formSubmitURL` must be present to be a valid login entry.
+ val loginList = promptOptions.filter { option ->
+ option.value.guid != null && (option.value.formActionOrigin != null || option.value.httpRealm != null)
+ }.map { option ->
+ Login(
+ guid = option.value.guid!!,
+ origin = option.value.origin,
+ formActionOrigin = option.value.formActionOrigin,
+ httpRealm = option.value.httpRealm,
+ username = option.value.username,
+ password = option.value.password,
+ )
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.SelectLoginPrompt(
+ logins = loginList,
+ generatedPassword = generatedPassword,
+ onConfirm = onConfirmSelect,
+ onDismiss = onDismiss,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onChoicePrompt(
+ session: GeckoSession,
+ geckoPrompt: PromptDelegate.ChoicePrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val choices = convertToChoices(geckoPrompt.choices)
+
+ val onDismiss: () -> Unit = {
+ geckoPrompt.dismissSafely(geckoResult)
+ }
+
+ val onConfirmSingleChoice: (Choice) -> Unit = { selectedChoice ->
+ if (!geckoPrompt.isComplete) {
+ geckoResult.complete(geckoPrompt.confirm(selectedChoice.id))
+ }
+ }
+ val onConfirmMultipleSelection: (Array<Choice>) -> Unit = { selectedChoices ->
+ if (!geckoPrompt.isComplete) {
+ val ids = selectedChoices.toIdsArray()
+ geckoResult.complete(geckoPrompt.confirm(ids))
+ }
+ }
+
+ val promptRequest = when (geckoPrompt.type) {
+ GECKO_PROMPT_CHOICE_TYPE.SINGLE -> SingleChoice(
+ choices,
+ onConfirmSingleChoice,
+ onDismiss,
+ )
+ GECKO_PROMPT_CHOICE_TYPE.MENU -> MenuChoice(
+ choices,
+ onConfirmSingleChoice,
+ onDismiss,
+ )
+ GECKO_PROMPT_CHOICE_TYPE.MULTIPLE -> MultipleChoice(
+ choices,
+ onConfirmMultipleSelection,
+ onDismiss,
+ )
+ else -> throw InvalidParameterException("${geckoPrompt.type} is not a valid Gecko @Choice.ChoiceType")
+ }
+
+ geckoPrompt.delegate = ChoicePromptDelegate(
+ geckoEngineSession,
+ promptRequest,
+ )
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(promptRequest)
+ }
+
+ return geckoResult
+ }
+
+ override fun onAddressSelect(
+ session: GeckoSession,
+ request: AutocompleteRequest<Autocomplete.AddressSelectOption>,
+ ): GeckoResult<PromptResponse> {
+ val geckoResult = GeckoResult<PromptResponse>()
+
+ val onConfirm: (Address) -> Unit = { address ->
+ if (!request.isComplete) {
+ geckoResult.complete(
+ request.confirm(
+ Autocomplete.AddressSelectOption(address.toAutocompleteAddress()),
+ ),
+ )
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ request.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.SelectAddress(
+ addresses = request.options.map { it.value.toAddress() },
+ onConfirm = onConfirm,
+ onDismiss = onDismiss,
+ ),
+ )
+ }
+
+ return geckoResult
+ }
+
+ override fun onAlertPrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.AlertPrompt,
+ ): GeckoResult<PromptResponse> {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
+ val onConfirm: (Boolean) -> Unit = { _ -> onDismiss() }
+ val title = prompt.title ?: ""
+ val message = prompt.message ?: ""
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Alert(
+ title,
+ message,
+ false,
+ onConfirm,
+ onDismiss,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onFilePrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.FilePrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val isMultipleFilesSelection = prompt.type == GECKO_PROMPT_FILE_TYPE.MULTIPLE
+
+ val captureMode = when (prompt.capture) {
+ GECKO_PROMPT_FILE_CAPTURE.ANY -> AC_FILE_FACING_MODE.ANY
+ GECKO_PROMPT_FILE_CAPTURE.USER -> AC_FILE_FACING_MODE.FRONT_CAMERA
+ GECKO_PROMPT_FILE_CAPTURE.ENVIRONMENT -> AC_FILE_FACING_MODE.BACK_CAMERA
+ else -> AC_FILE_FACING_MODE.NONE
+ }
+
+ val onSelectMultiple: (Context, Array<Uri>) -> Unit = { context, uris ->
+ val filesUris = uris.map {
+ toFileUri(uri = it, context)
+ }.toTypedArray()
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(context, filesUris))
+ }
+ }
+
+ val onSelectSingle: (Context, Uri) -> Unit = { context, uri ->
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(context, toFileUri(uri, context)))
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ prompt.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.File(
+ prompt.mimeTypes ?: emptyArray(),
+ isMultipleFilesSelection,
+ captureMode,
+ onSelectSingle,
+ onSelectMultiple,
+ onDismiss,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ @Suppress("ComplexMethod")
+ override fun onDateTimePrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.DateTimePrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val onConfirm: (String) -> Unit = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(it))
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ prompt.dismissSafely(geckoResult)
+ }
+
+ val onClear: () -> Unit = {
+ onConfirm("")
+ }
+ val initialDateString = prompt.defaultValue ?: ""
+ val stepValue = with(prompt.stepValue) {
+ if (this?.toDoubleOrNull() == null) {
+ null
+ } else {
+ this
+ }
+ }
+
+ val format = when (prompt.type) {
+ DATE -> "yyyy-MM-dd"
+ MONTH -> "yyyy-MM"
+ WEEK -> "yyyy-'W'ww"
+ TIME -> {
+ if (shouldShowMillisecondsPicker(stepValue?.toFloat())) {
+ "HH:mm:ss.SSS"
+ } else if (shouldShowSecondsPicker(stepValue?.toFloat())) {
+ "HH:mm:ss"
+ } else {
+ "HH:mm"
+ }
+ }
+ DATETIME_LOCAL -> "yyyy-MM-dd'T'HH:mm"
+ else -> {
+ throw InvalidParameterException("${prompt.type} is not a valid DatetimeType")
+ }
+ }
+
+ notifyDatePromptRequest(
+ prompt.title ?: "",
+ initialDateString,
+ prompt.minValue,
+ prompt.maxValue,
+ stepValue,
+ onClear,
+ format,
+ onConfirm,
+ onDismiss,
+ )
+
+ return geckoResult
+ }
+
+ override fun onAuthPrompt(
+ session: GeckoSession,
+ geckoPrompt: PromptDelegate.AuthPrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val title = geckoPrompt.title ?: ""
+ val message = geckoPrompt.message ?: ""
+ val uri = geckoPrompt.authOptions.uri
+ val flags = geckoPrompt.authOptions.flags
+ val userName = geckoPrompt.authOptions.username ?: ""
+ val password = geckoPrompt.authOptions.password ?: ""
+ val method =
+ if (flags in GECKO_AUTH_FLAGS.HOST) AC_AUTH_METHOD.HOST else AC_AUTH_METHOD.PROXY
+ val level = geckoPrompt.authOptions.toACLevel()
+ val onlyShowPassword = flags in GECKO_AUTH_FLAGS.ONLY_PASSWORD
+ val previousFailed = flags in GECKO_AUTH_FLAGS.PREVIOUS_FAILED
+ val isCrossOrigin = flags in GECKO_AUTH_FLAGS.CROSS_ORIGIN_SUB_RESOURCE
+
+ val onConfirm: (String, String) -> Unit =
+ { user, pass ->
+ if (!geckoPrompt.isComplete) {
+ if (onlyShowPassword) {
+ geckoResult.complete(geckoPrompt.confirm(pass))
+ } else {
+ geckoResult.complete(geckoPrompt.confirm(user, pass))
+ }
+ }
+ }
+
+ val onDismiss: () -> Unit = { geckoPrompt.dismissSafely(geckoResult) }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Authentication(
+ uri,
+ title,
+ message,
+ userName,
+ password,
+ method,
+ level,
+ onlyShowPassword,
+ previousFailed,
+ isCrossOrigin,
+ onConfirm,
+ onDismiss,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onTextPrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.TextPrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val title = prompt.title ?: ""
+ val inputLabel = prompt.message ?: ""
+ val inputValue = prompt.defaultValue ?: ""
+ val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
+ val onConfirm: (Boolean, String) -> Unit = { _, valueInput ->
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(valueInput))
+ }
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.TextPrompt(
+ title,
+ inputLabel,
+ inputValue,
+ false,
+ onConfirm,
+ onDismiss,
+ ),
+ )
+ }
+
+ return geckoResult
+ }
+
+ override fun onColorPrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.ColorPrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val onConfirm: (String) -> Unit = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(it))
+ }
+ }
+ val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
+
+ val defaultColor = prompt.defaultValue ?: ""
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Color(defaultColor, onConfirm, onDismiss),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onPopupPrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.PopupPrompt,
+ ): GeckoResult<PromptResponse> {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val onAllow: () -> Unit = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(AllowOrDeny.ALLOW))
+ }
+ }
+ val onDeny: () -> Unit = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(AllowOrDeny.DENY))
+ }
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Popup(prompt.targetUri ?: "", onAllow, onDeny),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onBeforeUnloadPrompt(
+ session: GeckoSession,
+ geckoPrompt: BeforeUnloadPrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val title = geckoPrompt.title ?: ""
+ val onAllow: () -> Unit = {
+ if (!geckoPrompt.isComplete) {
+ geckoResult.complete(geckoPrompt.confirm(AllowOrDeny.ALLOW))
+ }
+ }
+ val onDeny: () -> Unit = {
+ if (!geckoPrompt.isComplete) {
+ geckoResult.complete(geckoPrompt.confirm(AllowOrDeny.DENY))
+ geckoEngineSession.notifyObservers { onBeforeUnloadPromptDenied() }
+ }
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(PromptRequest.BeforeUnload(title, onAllow, onDeny))
+ }
+
+ return geckoResult
+ }
+
+ override fun onSharePrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.SharePrompt,
+ ): GeckoResult<PromptResponse> {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val onSuccess = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(GECKO_PROMPT_SHARE_RESULT.SUCCESS))
+ }
+ }
+ val onFailure = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(GECKO_PROMPT_SHARE_RESULT.FAILURE))
+ }
+ }
+ val onDismiss = { prompt.dismissSafely(geckoResult) }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Share(
+ ShareData(
+ title = prompt.title,
+ text = prompt.text,
+ url = prompt.uri,
+ ),
+ onSuccess,
+ onFailure,
+ onDismiss,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onButtonPrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.ButtonPrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+ val title = prompt.title ?: ""
+ val message = prompt.message ?: ""
+
+ val onConfirmPositiveButton: (Boolean) -> Unit = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(PromptDelegate.ButtonPrompt.Type.POSITIVE))
+ }
+ }
+ val onConfirmNegativeButton: (Boolean) -> Unit = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(PromptDelegate.ButtonPrompt.Type.NEGATIVE))
+ }
+ }
+
+ val onDismiss: (Boolean) -> Unit = { prompt.dismissSafely(geckoResult) }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Confirm(
+ title,
+ message,
+ false,
+ "",
+ "",
+ "",
+ onConfirmPositiveButton,
+ onConfirmNegativeButton,
+ onDismiss,
+ ) {
+ onDismiss(false)
+ },
+ )
+ }
+ return geckoResult
+ }
+
+ override fun onRepostConfirmPrompt(
+ session: GeckoSession,
+ prompt: PromptDelegate.RepostConfirmPrompt,
+ ): GeckoResult<PromptResponse>? {
+ val geckoResult = GeckoResult<PromptResponse>()
+
+ val onConfirm: () -> Unit = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(AllowOrDeny.ALLOW))
+ }
+ }
+ val onCancel: () -> Unit = {
+ if (!prompt.isComplete) {
+ geckoResult.complete(prompt.confirm(AllowOrDeny.DENY))
+ geckoEngineSession.notifyObservers { onRepostPromptCancelled() }
+ }
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Repost(
+ onConfirm,
+ onCancel,
+ ),
+ )
+ }
+ return geckoResult
+ }
+
+ @Suppress("LongParameterList")
+ private fun notifyDatePromptRequest(
+ title: String,
+ initialDateString: String,
+ minDateString: String?,
+ maxDateString: String?,
+ stepValue: String?,
+ onClear: () -> Unit,
+ format: String,
+ onConfirm: (String) -> Unit,
+ onDismiss: () -> Unit,
+ ) {
+ val initialDate = initialDateString.toDate(format)
+ val minDate = if (minDateString.isNullOrEmpty()) null else minDateString.toDate()
+ val maxDate = if (maxDateString.isNullOrEmpty()) null else maxDateString.toDate()
+ val onSelect: (Date) -> Unit = {
+ val stringDate = it.toString(format)
+ onConfirm(stringDate)
+ }
+
+ val selectionType = when (format) {
+ "HH:mm", "HH:mm:ss", "HH:mm:ss.SSS" -> PromptRequest.TimeSelection.Type.TIME
+ "yyyy-MM" -> PromptRequest.TimeSelection.Type.MONTH
+ "yyyy-MM-dd'T'HH:mm" -> PromptRequest.TimeSelection.Type.DATE_AND_TIME
+ else -> PromptRequest.TimeSelection.Type.DATE
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.TimeSelection(
+ title,
+ initialDate,
+ minDate,
+ maxDate,
+ stepValue,
+ selectionType,
+ onSelect,
+ onClear,
+ onDismiss,
+ ),
+ )
+ }
+ }
+
+ private fun GeckoAuthOptions.toACLevel(): AC_AUTH_LEVEL {
+ return when (level) {
+ GECKO_AUTH_LEVEL.NONE -> AC_AUTH_LEVEL.NONE
+ GECKO_AUTH_LEVEL.PW_ENCRYPTED -> AC_AUTH_LEVEL.PASSWORD_ENCRYPTED
+ GECKO_AUTH_LEVEL.SECURE -> AC_AUTH_LEVEL.SECURED
+ else -> {
+ AC_AUTH_LEVEL.NONE
+ }
+ }
+ }
+
+ private operator fun Int.contains(mask: Int): Boolean {
+ return (this and mask) != 0
+ }
+
+ @VisibleForTesting
+ internal fun toFileUri(uri: Uri, context: Context): Uri {
+ return uri.toFileUri(context, dirToCopy = DEFAULT_UPLOADS_DIR_NAME)
+ }
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun Array<Choice>.toIdsArray(): Array<String> {
+ return this.map { it.id }.toTypedArray()
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun Date.toString(format: String): String {
+ val formatter = SimpleDateFormat(format, Locale.ROOT)
+ return formatter.format(this) ?: ""
+}
+
+/**
+ * Only dismiss if the prompt is not already dismissed.
+ */
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun PromptDelegate.BasePrompt.dismissSafely(geckoResult: GeckoResult<PromptResponse>) {
+ if (!this.isComplete) {
+ geckoResult.complete(dismiss())
+ }
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun GECKO_PROMPT_PROVIDER_SELECTOR.toProvider(): Provider {
+ return Provider(id, icon, name, domain)
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun GECKO_PROMPT_ACCOUNT_SELECTOR.toAccount(): Account {
+ return Account(id, email, name, icon)
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun GECKO_PROMPT_ACCOUNT_SELECTOR_PROVIDER.toProvider(): Provider {
+ return Provider(0, icon, name, domain)
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegate.kt
new file mode 100644
index 0000000000..1448b726c4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegate.kt
@@ -0,0 +1,21 @@
+/* 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.browser.engine.gecko.prompt
+
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.prompt.PromptRequest
+import org.mozilla.geckoview.GeckoSession
+
+internal class PromptInstanceDismissDelegate(
+ private val geckoSession: GeckoEngineSession,
+ private val promptRequest: PromptRequest,
+) : GeckoSession.PromptDelegate.PromptInstanceDelegate {
+
+ override fun onPromptDismiss(prompt: GeckoSession.PromptDelegate.BasePrompt) {
+ geckoSession.notifyObservers {
+ onPromptDismissed(promptRequest)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegate.kt
new file mode 100644
index 0000000000..ad840f1476
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegate.kt
@@ -0,0 +1,70 @@
+/* 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.browser.engine.gecko.selection
+
+import android.app.Activity
+import android.content.Context
+import android.view.MenuItem
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import org.mozilla.geckoview.BasicSelectionActionDelegate
+
+/**
+ * An adapter between the GV [BasicSelectionActionDelegate] and a generic [SelectionActionDelegate].
+ *
+ * @param customDelegate handles as much of this logic as possible.
+ */
+open class GeckoSelectionActionDelegate(
+ activity: Activity,
+ @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val customDelegate: SelectionActionDelegate,
+) : BasicSelectionActionDelegate(activity) {
+
+ companion object {
+ /**
+ * @returns a [GeckoSelectionActionDelegate] if [customDelegate] is non-null and [context]
+ * is an instance of [Activity]. Otherwise, returns null.
+ */
+ fun maybeCreate(context: Context, customDelegate: SelectionActionDelegate?): GeckoSelectionActionDelegate? {
+ return if (context is Activity && customDelegate != null) {
+ GeckoSelectionActionDelegate(context, customDelegate)
+ } else {
+ null
+ }
+ }
+ }
+
+ override fun getAllActions(): Array<String> {
+ return customDelegate.sortedActions(super.getAllActions() + customDelegate.getAllActions())
+ }
+
+ override fun isActionAvailable(id: String): Boolean {
+ val selectedText = mSelection?.text
+
+ val customActionIsAvailable = !selectedText.isNullOrEmpty() &&
+ customDelegate.isActionAvailable(id, selectedText)
+
+ return customActionIsAvailable ||
+ super.isActionAvailable(id)
+ }
+
+ override fun prepareAction(id: String, item: MenuItem) {
+ val title = customDelegate.getActionTitle(id)
+ ?: return super.prepareAction(id, item)
+
+ item.title = title
+ }
+
+ override fun performAction(id: String, item: MenuItem): Boolean {
+ /* Temporary, removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1694983 is fixed */
+ try {
+ val selectedText = mSelection?.text ?: return super.performAction(id, item)
+
+ return customDelegate.performAction(id, selectedText) || super.performAction(id, item)
+ } catch (e: SecurityException) {
+ return false
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegate.kt
new file mode 100644
index 0000000000..3bf0f24fff
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegate.kt
@@ -0,0 +1,35 @@
+/* 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.browser.engine.gecko.serviceworker
+
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession
+
+/**
+ * Default implementation for supporting Gecko service workers.
+ *
+ * @param delegate [ServiceWorkerDelegate] handling service workers requests.
+ * @param runtime [GeckoRuntime] current engine's runtime.
+ * @param engineSettings [Settings] default settings used when new [EngineSession]s are to be created.
+ */
+class GeckoServiceWorkerDelegate(
+ internal val delegate: ServiceWorkerDelegate,
+ internal val runtime: GeckoRuntime,
+ internal val engineSettings: Settings?,
+) : GeckoRuntime.ServiceWorkerDelegate {
+ override fun onOpenWindow(url: String): GeckoResult<GeckoSession> {
+ val newEngineSession = GeckoEngineSession(runtime, false, engineSettings, openGeckoSession = false)
+
+ return when (delegate.addNewTab(newEngineSession)) {
+ true -> GeckoResult.fromValue(newEngineSession.geckoSession)
+ false -> GeckoResult.fromValue(null)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegate.kt
new file mode 100644
index 0000000000..3266ba8538
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegate.kt
@@ -0,0 +1,79 @@
+/* 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.browser.engine.gecko.translate
+
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.translate.DetectedLanguages
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.concept.engine.translate.TranslationPair
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.TranslationsController
+
+internal class GeckoTranslateSessionDelegate(
+ private val engineSession: GeckoEngineSession,
+) : TranslationsController.SessionTranslation.Delegate {
+
+ /**
+ * This delegate function is triggered when requesting a translation on the page is likely.
+ *
+ * The criteria is that the page is in a different language than the user's known languages and
+ * that the page is translatable (a model is available).
+ *
+ * @param session The session that this delegate event corresponds to.
+ */
+ override fun onExpectedTranslate(session: GeckoSession) {
+ engineSession.notifyObservers {
+ onTranslateExpected()
+ }
+ }
+
+ /**
+ * This delegate function is triggered when the app should offer a translation.
+ *
+ * The criteria is that the translation is likely and it is the user's first visit to the host site.
+ *
+ * @param session The session that this delegate event corresponds to.
+ */
+ override fun onOfferTranslate(session: GeckoSession) {
+ engineSession.notifyObservers {
+ onTranslateOffer()
+ }
+ }
+
+ /**
+ * This delegate function is triggered when the state of the translation or translation options
+ * for the page has changed. State changes usually occur on navigation or if a translation
+ * action was requested, such as translating or restoring to the original page.
+ *
+ * This provides the translations engine state and information for the page.
+ *
+ * @param session The session that this delegate event corresponds to.
+ * @param state The reported translations state. Not to be confused
+ * with the browser translation state.
+ */
+ override fun onTranslationStateChange(
+ session: GeckoSession,
+ state: TranslationsController.SessionTranslation.TranslationState?,
+ ) {
+ val detectedLanguages = DetectedLanguages(
+ state?.detectedLanguages?.docLangTag,
+ state?.detectedLanguages?.isDocLangTagSupported,
+ state?.detectedLanguages?.userLangTag,
+ )
+ val pair = TranslationPair(
+ state?.requestedTranslationPair?.fromLanguage,
+ state?.requestedTranslationPair?.toLanguage,
+ )
+ val translationsState = TranslationEngineState(
+ detectedLanguages,
+ state?.error,
+ state?.isEngineReady,
+ pair,
+ )
+
+ engineSession.notifyObservers {
+ onTranslateStateChange(translationsState)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslationUtils.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslationUtils.kt
new file mode 100644
index 0000000000..c91992f355
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslationUtils.kt
@@ -0,0 +1,63 @@
+/* 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.browser.engine.gecko.translate
+
+import mozilla.components.concept.engine.translate.TranslationError
+import org.mozilla.geckoview.TranslationsController.TranslationsException
+
+/**
+ * Utility file for translations functions related to the Gecko implementation.
+ */
+object GeckoTranslationUtils {
+
+ /**
+ * Convenience method for mapping a [TranslationsException] to the Android Components defined
+ * error type of [TranslationError].
+ *
+ * Throwable is the engine throwable that occurred during translating. Ordinarily should be
+ * a [TranslationsException].
+ */
+ fun Throwable.intoTranslationError(): TranslationError {
+ return if (this is TranslationsException) {
+ when ((this).code) {
+ TranslationsException.ERROR_UNKNOWN ->
+ TranslationError.UnknownError(this)
+
+ TranslationsException.ERROR_ENGINE_NOT_SUPPORTED ->
+ TranslationError.EngineNotSupportedError(this)
+
+ TranslationsException.ERROR_COULD_NOT_TRANSLATE ->
+ TranslationError.CouldNotTranslateError(this)
+
+ TranslationsException.ERROR_COULD_NOT_RESTORE ->
+ TranslationError.CouldNotRestoreError(this)
+
+ TranslationsException.ERROR_COULD_NOT_LOAD_LANGUAGES ->
+ TranslationError.CouldNotLoadLanguagesError(this)
+
+ TranslationsException.ERROR_LANGUAGE_NOT_SUPPORTED ->
+ TranslationError.LanguageNotSupportedError(this)
+
+ TranslationsException.ERROR_MODEL_COULD_NOT_RETRIEVE ->
+ TranslationError.ModelCouldNotRetrieveError(this)
+
+ TranslationsException.ERROR_MODEL_COULD_NOT_DELETE ->
+ TranslationError.ModelCouldNotDeleteError(this)
+
+ TranslationsException.ERROR_MODEL_COULD_NOT_DOWNLOAD ->
+ TranslationError.ModelCouldNotDownloadError(this)
+
+ TranslationsException.ERROR_MODEL_LANGUAGE_REQUIRED ->
+ TranslationError.ModelLanguageRequiredError(this)
+
+ TranslationsException.ERROR_MODEL_DOWNLOAD_REQUIRED ->
+ TranslationError.ModelDownloadRequiredError(this)
+
+ else -> TranslationError.UnknownError(this)
+ }
+ } else {
+ TranslationError.UnknownError(this)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactory.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactory.kt
new file mode 100644
index 0000000000..91f0b1ece1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactory.kt
@@ -0,0 +1,154 @@
+/* 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.browser.engine.gecko.util
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.Settings
+import org.mozilla.geckoview.GeckoRuntime
+
+/**
+ * Helper factory for creating and maintaining a speculative [EngineSession].
+ */
+internal class SpeculativeSessionFactory {
+ @VisibleForTesting
+ internal var speculativeEngineSession: SpeculativeEngineSession? = null
+
+ /**
+ * Creates a speculative [EngineSession] using the provided [contextId] and [defaultSettings].
+ * Creates a private session if [private] is set to true.
+ *
+ * The speculative [EngineSession] is kept internally until explicitly needed and access via [get].
+ */
+ @Synchronized
+ fun create(
+ runtime: GeckoRuntime,
+ private: Boolean,
+ contextId: String?,
+ defaultSettings: Settings?,
+ ) {
+ if (speculativeEngineSession?.matches(private, contextId) == true) {
+ // We already have a speculative engine session for this configuration. Nothing to do here.
+ return
+ }
+
+ // Clear any potentially non-matching engine session
+ clear()
+
+ speculativeEngineSession = SpeculativeEngineSession.create(
+ this,
+ runtime,
+ private,
+ contextId,
+ defaultSettings,
+ )
+ }
+
+ /**
+ * Clears the internal speculative [EngineSession].
+ */
+ @Synchronized
+ fun clear() {
+ speculativeEngineSession?.cleanUp()
+ speculativeEngineSession = null
+ }
+
+ /**
+ * Returns and consumes a previously created [private] speculative [EngineSession] if it uses
+ * the same [contextId]. Returns `null` if no speculative [EngineSession] for that
+ * configuration is available.
+ */
+ @Synchronized
+ fun get(
+ private: Boolean,
+ contextId: String?,
+ ): GeckoEngineSession? {
+ val speculativeEngineSession = speculativeEngineSession ?: return null
+
+ return if (speculativeEngineSession.matches(private, contextId)) {
+ this.speculativeEngineSession = null
+ speculativeEngineSession.unwrap()
+ } else {
+ clear()
+ null
+ }
+ }
+
+ @VisibleForTesting
+ internal fun hasSpeculativeSession(): Boolean {
+ return speculativeEngineSession != null
+ }
+}
+
+/**
+ * Internal wrapper for [GeckoEngineSession] that takes care of registering and unregistering an
+ * observer for handling content process crashes/kills.
+ */
+internal class SpeculativeEngineSession constructor(
+ @get:VisibleForTesting internal val engineSession: GeckoEngineSession,
+ @get:VisibleForTesting internal val observer: SpeculativeSessionObserver,
+) {
+ /**
+ * Checks whether the [SpeculativeEngineSession] matches the given configuration.
+ */
+ fun matches(private: Boolean, contextId: String?): Boolean {
+ return engineSession.geckoSession.settings.usePrivateMode == private &&
+ engineSession.geckoSession.settings.contextId == contextId
+ }
+
+ /**
+ * Unwraps the internal [GeckoEngineSession].
+ *
+ * After calling [unwrap] the wrapper will no longer observe the [GeckoEngineSession] and further
+ * crash handling is left to the application.
+ */
+ fun unwrap(): GeckoEngineSession {
+ engineSession.unregister(observer)
+ return engineSession
+ }
+
+ /**
+ * Cleans up the internal state of this [SpeculativeEngineSession]. After calling this method
+ * his [SpeculativeEngineSession] cannot be used anymore.
+ */
+ fun cleanUp() {
+ engineSession.unregister(observer)
+ engineSession.close()
+ }
+
+ companion object {
+ fun create(
+ factory: SpeculativeSessionFactory,
+ runtime: GeckoRuntime,
+ private: Boolean,
+ contextId: String?,
+ defaultSettings: Settings?,
+ ): SpeculativeEngineSession {
+ val engineSession = GeckoEngineSession(runtime, private, defaultSettings, contextId)
+ val observer = SpeculativeSessionObserver(factory)
+ engineSession.register(observer)
+
+ return SpeculativeEngineSession(engineSession, observer)
+ }
+ }
+}
+
+/**
+ * [EngineSession.Observer] implementation that will notify the [SpeculativeSessionFactory] if an
+ * [GeckoEngineSession] can no longer be used after a crash.
+ */
+internal class SpeculativeSessionObserver(
+ private val factory: SpeculativeSessionFactory,
+
+) : EngineSession.Observer {
+ override fun onCrash() {
+ factory.clear()
+ }
+
+ override fun onProcessKilled() {
+ factory.clear()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt
new file mode 100644
index 0000000000..2c5eef52c4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt
@@ -0,0 +1,449 @@
+/* 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.browser.engine.gecko.webextension
+
+import android.graphics.Bitmap
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.browser.engine.gecko.await
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.engine.webextension.ActionHandler
+import mozilla.components.concept.engine.webextension.DisabledFlags
+import mozilla.components.concept.engine.webextension.Incognito
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Metadata
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.concept.engine.webextension.TabHandler
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.support.base.log.logger.Logger
+import org.json.JSONObject
+import org.mozilla.geckoview.AllowOrDeny
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.WebExtension as GeckoNativeWebExtension
+import org.mozilla.geckoview.WebExtension.Action as GeckoNativeWebExtensionAction
+
+/**
+ * Gecko-based implementation of [WebExtension], wrapping the native web
+ * extension object provided by GeckoView.
+ */
+class GeckoWebExtension(
+ val nativeExtension: GeckoNativeWebExtension,
+ val runtime: GeckoRuntime,
+) : WebExtension(nativeExtension.id, nativeExtension.location, true) {
+
+ private val connectedPorts: MutableMap<PortId, GeckoPort> = mutableMapOf()
+ private val logger = Logger("GeckoWebExtension")
+
+ /**
+ * Uniquely identifies a port using its name and the session it
+ * was opened for. Ports connected from background scripts will
+ * have a null session.
+ */
+ data class PortId(val name: String, val session: EngineSession? = null)
+
+ /**
+ * See [WebExtension.registerBackgroundMessageHandler].
+ */
+ override fun registerBackgroundMessageHandler(name: String, messageHandler: MessageHandler) {
+ val portDelegate = object : GeckoNativeWebExtension.PortDelegate {
+
+ override fun onPortMessage(message: Any, port: GeckoNativeWebExtension.Port) {
+ messageHandler.onPortMessage(message, GeckoPort(port))
+ }
+
+ override fun onDisconnect(port: GeckoNativeWebExtension.Port) {
+ val connectedPort = connectedPorts[PortId(name)]
+ if (connectedPort != null && connectedPort.nativePort == port) {
+ connectedPorts.remove(PortId(name))
+ messageHandler.onPortDisconnected(GeckoPort(port))
+ }
+ }
+ }
+
+ connectedPorts[PortId(name)]?.nativePort?.setDelegate(portDelegate)
+
+ val messageDelegate = object : GeckoNativeWebExtension.MessageDelegate {
+
+ override fun onConnect(port: GeckoNativeWebExtension.Port) {
+ port.setDelegate(portDelegate)
+ val geckoPort = GeckoPort(port)
+ connectedPorts[PortId(name)] = geckoPort
+ messageHandler.onPortConnected(geckoPort)
+ }
+
+ override fun onMessage(
+ // We don't use the same delegate instance for multiple apps so we don't need to verify the name.
+ name: String,
+ message: Any,
+ sender: GeckoNativeWebExtension.MessageSender,
+ ): GeckoResult<Any>? {
+ val response = messageHandler.onMessage(message, null)
+ return response?.let { GeckoResult.fromValue(it) }
+ }
+ }
+
+ nativeExtension.setMessageDelegate(messageDelegate, name)
+ }
+
+ /**
+ * See [WebExtension.registerContentMessageHandler].
+ */
+ override fun registerContentMessageHandler(session: EngineSession, name: String, messageHandler: MessageHandler) {
+ val portDelegate = object : GeckoNativeWebExtension.PortDelegate {
+
+ override fun onPortMessage(message: Any, port: GeckoNativeWebExtension.Port) {
+ messageHandler.onPortMessage(message, GeckoPort(port, session))
+ }
+
+ override fun onDisconnect(port: GeckoNativeWebExtension.Port) {
+ val connectedPort = connectedPorts[PortId(name, session)]
+ if (connectedPort != null && connectedPort.nativePort == port) {
+ connectedPorts.remove(PortId(name, session))
+ messageHandler.onPortDisconnected(connectedPort)
+ }
+ }
+ }
+
+ connectedPorts[PortId(name, session)]?.nativePort?.setDelegate(portDelegate)
+
+ val messageDelegate = object : GeckoNativeWebExtension.MessageDelegate {
+
+ override fun onConnect(port: GeckoNativeWebExtension.Port) {
+ port.setDelegate(portDelegate)
+ val geckoPort = GeckoPort(port, session)
+ connectedPorts[PortId(name, session)] = geckoPort
+ messageHandler.onPortConnected(geckoPort)
+ }
+
+ override fun onMessage(
+ // We don't use the same delegate instance for multiple apps so we don't need to verify the name.
+ name: String,
+ message: Any,
+ sender: GeckoNativeWebExtension.MessageSender,
+ ): GeckoResult<Any>? {
+ val response = messageHandler.onMessage(message, session)
+ return response?.let { GeckoResult.fromValue(it) }
+ }
+ }
+
+ val geckoSession = (session as GeckoEngineSession).geckoSession
+ geckoSession.webExtensionController.setMessageDelegate(nativeExtension, messageDelegate, name)
+ }
+
+ /**
+ * See [WebExtension.hasContentMessageHandler].
+ */
+ override fun hasContentMessageHandler(session: EngineSession, name: String): Boolean {
+ val geckoSession = (session as GeckoEngineSession).geckoSession
+ return geckoSession.webExtensionController.getMessageDelegate(nativeExtension, name) != null
+ }
+
+ /**
+ * See [WebExtension.getConnectedPort].
+ */
+ override fun getConnectedPort(name: String, session: EngineSession?): Port? {
+ return connectedPorts[PortId(name, session)]
+ }
+
+ /**
+ * See [WebExtension.disconnectPort].
+ */
+ override fun disconnectPort(name: String, session: EngineSession?) {
+ val portId = PortId(name, session)
+ val port = connectedPorts[portId]
+ port?.let {
+ it.disconnect()
+ connectedPorts.remove(portId)
+ }
+ }
+
+ /**
+ * See [WebExtension.registerActionHandler].
+ */
+ override fun registerActionHandler(actionHandler: ActionHandler) {
+ if (!supportActions) {
+ logger.error(
+ "Attempt to register default action handler but browser and page " +
+ "action support is turned off for this extension: $id",
+ )
+ return
+ }
+
+ val actionDelegate = object : GeckoNativeWebExtension.ActionDelegate {
+
+ override fun onBrowserAction(
+ ext: GeckoNativeWebExtension,
+ // Session will always be null here for the global default delegate
+ session: GeckoSession?,
+ action: GeckoNativeWebExtensionAction,
+ ) {
+ actionHandler.onBrowserAction(this@GeckoWebExtension, null, action.convert())
+ }
+
+ override fun onPageAction(
+ ext: GeckoNativeWebExtension,
+ // Session will always be null here for the global default delegate
+ session: GeckoSession?,
+ action: GeckoNativeWebExtensionAction,
+ ) {
+ actionHandler.onPageAction(this@GeckoWebExtension, null, action.convert())
+ }
+
+ override fun onTogglePopup(
+ ext: GeckoNativeWebExtension,
+ action: GeckoNativeWebExtensionAction,
+ ): GeckoResult<GeckoSession>? {
+ val session = actionHandler.onToggleActionPopup(this@GeckoWebExtension, action.convert())
+ return session?.let { GeckoResult.fromValue((session as GeckoEngineSession).geckoSession) }
+ }
+ }
+
+ nativeExtension.setActionDelegate(actionDelegate)
+ }
+
+ /**
+ * See [WebExtension.registerActionHandler].
+ */
+ override fun registerActionHandler(session: EngineSession, actionHandler: ActionHandler) {
+ if (!supportActions) {
+ logger.error(
+ "Attempt to register action handler on session but browser and page " +
+ "action support is turned off for this extension: $id",
+ )
+ return
+ }
+
+ val actionDelegate = object : GeckoNativeWebExtension.ActionDelegate {
+
+ override fun onBrowserAction(
+ ext: GeckoNativeWebExtension,
+ geckoSession: GeckoSession?,
+ action: GeckoNativeWebExtensionAction,
+ ) {
+ actionHandler.onBrowserAction(this@GeckoWebExtension, session, action.convert())
+ }
+
+ override fun onPageAction(
+ ext: GeckoNativeWebExtension,
+ geckoSession: GeckoSession?,
+ action: GeckoNativeWebExtensionAction,
+ ) {
+ actionHandler.onPageAction(this@GeckoWebExtension, session, action.convert())
+ }
+ }
+
+ val geckoSession = (session as GeckoEngineSession).geckoSession
+ geckoSession.webExtensionController.setActionDelegate(nativeExtension, actionDelegate)
+ }
+
+ /**
+ * See [WebExtension.hasActionHandler].
+ */
+ override fun hasActionHandler(session: EngineSession): Boolean {
+ val geckoSession = (session as GeckoEngineSession).geckoSession
+ return geckoSession.webExtensionController.getActionDelegate(nativeExtension) != null
+ }
+
+ /**
+ * See [WebExtension.registerTabHandler].
+ */
+ override fun registerTabHandler(tabHandler: TabHandler, defaultSettings: Settings?) {
+ val tabDelegate = object : GeckoNativeWebExtension.TabDelegate {
+
+ override fun onNewTab(
+ ext: GeckoNativeWebExtension,
+ tabDetails: GeckoNativeWebExtension.CreateTabDetails,
+ ): GeckoResult<GeckoSession>? {
+ val geckoEngineSession = GeckoEngineSession(
+ runtime,
+ defaultSettings = defaultSettings,
+ openGeckoSession = false,
+ )
+
+ tabHandler.onNewTab(
+ this@GeckoWebExtension,
+ geckoEngineSession,
+ tabDetails.active == true,
+ tabDetails.url ?: "",
+ )
+ return GeckoResult.fromValue(geckoEngineSession.geckoSession)
+ }
+
+ override fun onOpenOptionsPage(ext: GeckoNativeWebExtension) {
+ ext.metaData.optionsPageUrl?.let { optionsPageUrl ->
+ tabHandler.onNewTab(
+ this@GeckoWebExtension,
+ GeckoEngineSession(
+ runtime,
+ defaultSettings = defaultSettings,
+ ),
+ false,
+ optionsPageUrl,
+ )
+ }
+ }
+ }
+
+ nativeExtension.tabDelegate = tabDelegate
+ }
+
+ /**
+ * See [WebExtension.registerTabHandler].
+ */
+ override fun registerTabHandler(session: EngineSession, tabHandler: TabHandler) {
+ val tabDelegate = object : GeckoNativeWebExtension.SessionTabDelegate {
+
+ override fun onUpdateTab(
+ ext: GeckoNativeWebExtension,
+ geckoSession: GeckoSession,
+ tabDetails: GeckoNativeWebExtension.UpdateTabDetails,
+ ): GeckoResult<AllowOrDeny> {
+ return if (tabHandler.onUpdateTab(
+ this@GeckoWebExtension,
+ session,
+ tabDetails.active == true,
+ tabDetails.url,
+ )
+ ) {
+ GeckoResult.allow()
+ } else {
+ GeckoResult.deny()
+ }
+ }
+
+ override fun onCloseTab(
+ ext: GeckoNativeWebExtension?,
+ geckoSession: GeckoSession,
+ ): GeckoResult<AllowOrDeny> {
+ return if (ext != null) {
+ if (tabHandler.onCloseTab(this@GeckoWebExtension, session)) {
+ GeckoResult.allow()
+ } else {
+ GeckoResult.deny()
+ }
+ } else {
+ GeckoResult.deny()
+ }
+ }
+ }
+
+ val geckoSession = (session as GeckoEngineSession).geckoSession
+ geckoSession.webExtensionController.setTabDelegate(nativeExtension, tabDelegate)
+ }
+
+ /**
+ * See [WebExtension.hasTabHandler].
+ */
+ override fun hasTabHandler(session: EngineSession): Boolean {
+ val geckoSession = (session as GeckoEngineSession).geckoSession
+ return geckoSession.webExtensionController.getTabDelegate(nativeExtension) != null
+ }
+
+ /**
+ * See [WebExtension.getMetadata].
+ */
+ override fun getMetadata(): Metadata {
+ return nativeExtension.metaData.let {
+ Metadata(
+ name = it.name,
+ fullDescription = it.fullDescription,
+ downloadUrl = it.downloadUrl,
+ updateDate = it.updateDate,
+ averageRating = it.averageRating.toFloat(),
+ reviewCount = it.reviewCount,
+ description = it.description,
+ developerName = it.creatorName,
+ developerUrl = it.creatorUrl,
+ homepageUrl = it.homepageUrl,
+ creatorName = it.creatorName,
+ creatorUrl = it.creatorUrl,
+ reviewUrl = it.reviewUrl,
+ version = it.version,
+ permissions = it.promptPermissions.toList(),
+ optionalPermissions = it.optionalPermissions.toList(),
+ grantedOptionalPermissions = it.grantedOptionalPermissions.toList(),
+ grantedOptionalOrigins = it.grantedOptionalOrigins.toList(),
+ optionalOrigins = it.optionalOrigins.toList(),
+ // Origins is marked as @NonNull but may be null: https://bugzilla.mozilla.org/show_bug.cgi?id=1629957
+ hostPermissions = it.origins.orEmpty().toList(),
+ disabledFlags = DisabledFlags.select(it.disabledFlags),
+ optionsPageUrl = it.optionsPageUrl,
+ openOptionsPageInTab = it.openOptionsPageInTab,
+ baseUrl = it.baseUrl,
+ temporary = it.temporary,
+ detailUrl = it.amoListingUrl,
+ incognito = Incognito.fromString(it.incognito),
+ )
+ }
+ }
+
+ override fun isBuiltIn(): Boolean {
+ return nativeExtension.isBuiltIn
+ }
+
+ override fun isEnabled(): Boolean {
+ return nativeExtension.metaData.enabled
+ }
+
+ override fun isAllowedInPrivateBrowsing(): Boolean {
+ return isBuiltIn() || nativeExtension.metaData.allowedInPrivateBrowsing
+ }
+
+ override suspend fun loadIcon(size: Int): Bitmap? {
+ return getIcon(size).await()
+ }
+
+ @VisibleForTesting
+ internal fun getIcon(size: Int): GeckoResult<Bitmap> {
+ return nativeExtension.metaData.icon.getBitmap(size)
+ }
+}
+
+/**
+ * Gecko-based implementation of [Port], wrapping the native port provided by GeckoView.
+ */
+class GeckoPort(
+ internal val nativePort: GeckoNativeWebExtension.Port,
+ engineSession: EngineSession? = null,
+) : Port(engineSession) {
+
+ override fun postMessage(message: JSONObject) {
+ nativePort.postMessage(message)
+ }
+
+ override fun name(): String {
+ return nativePort.name
+ }
+
+ override fun senderUrl(): String {
+ return nativePort.sender.url
+ }
+
+ override fun disconnect() {
+ nativePort.disconnect()
+ }
+}
+
+private fun GeckoNativeWebExtensionAction.convert(): Action {
+ val loadIcon: (suspend (Int) -> Bitmap?)? = icon?.let {
+ { size -> icon?.getBitmap(size)?.await() }
+ }
+
+ val onClick = { click() }
+
+ return Action(
+ title,
+ enabled,
+ loadIcon,
+ badgeText,
+ badgeTextColor,
+ badgeBackgroundColor,
+ onClick,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionException.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionException.kt
new file mode 100644
index 0000000000..3874dd903b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionException.kt
@@ -0,0 +1,72 @@
+/* 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.browser.engine.gecko.webextension
+
+import mozilla.components.concept.engine.webextension.WebExtensionException
+import mozilla.components.concept.engine.webextension.WebExtensionInstallException
+import org.mozilla.geckoview.WebExtension.InstallException
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_BLOCKLISTED
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_CORRUPT_FILE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCOMPATIBLE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_NETWORK_FAILURE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_UNSUPPORTED_ADDON_TYPE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED
+
+/**
+ * An unexpected gecko exception that occurs when trying to perform an action on the extension like
+ * (but not exclusively) installing/uninstalling, removing or updating..
+ */
+class GeckoWebExtensionException(throwable: Throwable) : WebExtensionException(throwable) {
+ override val isRecoverable: Boolean = throwable is InstallException &&
+ throwable.code == ERROR_USER_CANCELED
+
+ companion object {
+ internal fun createWebExtensionException(throwable: Throwable): WebExtensionException {
+ if (throwable is InstallException) {
+ return when (throwable.code) {
+ ERROR_USER_CANCELED -> WebExtensionInstallException.UserCancelled(
+ extensionName = throwable.extensionName,
+ throwable,
+ )
+
+ ERROR_BLOCKLISTED -> WebExtensionInstallException.Blocklisted(
+ extensionName = throwable.extensionName,
+ throwable,
+ )
+
+ ERROR_CORRUPT_FILE -> WebExtensionInstallException.CorruptFile(
+ throwable = throwable,
+ )
+
+ ERROR_NETWORK_FAILURE -> WebExtensionInstallException.NetworkFailure(
+ throwable = throwable,
+ )
+
+ ERROR_SIGNEDSTATE_REQUIRED -> WebExtensionInstallException.NotSigned(
+ throwable = throwable,
+ )
+
+ ERROR_INCOMPATIBLE -> WebExtensionInstallException.Incompatible(
+ extensionName = throwable.extensionName,
+ throwable,
+ )
+
+ ERROR_UNSUPPORTED_ADDON_TYPE -> WebExtensionInstallException.UnsupportedAddonType(
+ extensionName = throwable.extensionName,
+ throwable,
+ )
+
+ else -> WebExtensionInstallException.Unknown(
+ extensionName = throwable.extensionName,
+ throwable,
+ )
+ }
+ }
+
+ return GeckoWebExtensionException(throwable)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegate.kt
new file mode 100644
index 0000000000..bf9cdb71b9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegate.kt
@@ -0,0 +1,39 @@
+/* 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.browser.engine.gecko.webnotifications
+
+import mozilla.components.concept.engine.webnotifications.WebNotification
+import mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
+import org.mozilla.geckoview.WebNotification as GeckoViewWebNotification
+import org.mozilla.geckoview.WebNotificationDelegate as GeckoViewWebNotificationDelegate
+
+internal class GeckoWebNotificationDelegate(
+ private val webNotificationDelegate: WebNotificationDelegate,
+) : GeckoViewWebNotificationDelegate {
+ override fun onShowNotification(webNotification: GeckoViewWebNotification) {
+ webNotificationDelegate.onShowNotification(webNotification.toWebNotification())
+ }
+
+ override fun onCloseNotification(webNotification: GeckoViewWebNotification) {
+ webNotificationDelegate.onCloseNotification(webNotification.toWebNotification())
+ }
+
+ private fun GeckoViewWebNotification.toWebNotification(): WebNotification {
+ return WebNotification(
+ title = title,
+ tag = tag,
+ body = text,
+ sourceUrl = source,
+ iconUrl = imageUrl,
+ direction = textDirection,
+ lang = lang,
+ requireInteraction = requireInteraction,
+ triggeredByWebExtension = source == null,
+ privateBrowsing = privateBrowsing,
+ engineNotification = this@toWebNotification,
+ silent = silent,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegate.kt
new file mode 100644
index 0000000000..528f798594
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegate.kt
@@ -0,0 +1,73 @@
+/* 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.browser.engine.gecko.webpush
+
+import mozilla.components.concept.engine.webpush.WebPushDelegate
+import mozilla.components.concept.engine.webpush.WebPushSubscription
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.WebPushDelegate as GeckoViewWebPushDelegate
+import org.mozilla.geckoview.WebPushSubscription as GeckoWebPushSubscription
+
+/**
+ * A wrapper for the [WebPushDelegate] to communicate with the Gecko-based delegate.
+ */
+internal class GeckoWebPushDelegate(private val delegate: WebPushDelegate) : GeckoViewWebPushDelegate {
+
+ /**
+ * See [GeckoViewWebPushDelegate.onGetSubscription].
+ */
+ override fun onGetSubscription(scope: String): GeckoResult<GeckoWebPushSubscription>? {
+ val result: GeckoResult<GeckoWebPushSubscription> = GeckoResult()
+
+ delegate.onGetSubscription(scope) { subscription ->
+ result.complete(subscription?.toGeckoSubscription())
+ }
+
+ return result
+ }
+
+ /**
+ * See [GeckoViewWebPushDelegate.onSubscribe].
+ */
+ override fun onSubscribe(scope: String, appServerKey: ByteArray?): GeckoResult<GeckoWebPushSubscription>? {
+ val result: GeckoResult<GeckoWebPushSubscription> = GeckoResult()
+
+ delegate.onSubscribe(scope, appServerKey) { subscription ->
+ result.complete(subscription?.toGeckoSubscription())
+ }
+
+ return result
+ }
+
+ /**
+ * See [GeckoViewWebPushDelegate.onUnsubscribe].
+ */
+ override fun onUnsubscribe(scope: String): GeckoResult<Void>? {
+ val result: GeckoResult<Void> = GeckoResult()
+
+ delegate.onUnsubscribe(scope) { success ->
+ if (success) {
+ result.complete(null)
+ } else {
+ result.completeExceptionally(WebPushException("Un-subscribing from subscription failed."))
+ }
+ }
+
+ return result
+ }
+}
+
+/**
+ * A helper extension to convert the subscription data class to the Gecko-based implementation.
+ */
+internal fun WebPushSubscription.toGeckoSubscription() = GeckoWebPushSubscription(
+ scope,
+ endpoint,
+ appServerKey,
+ publicKey,
+ authSecret,
+)
+
+internal class WebPushException(message: String) : IllegalStateException(message)
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandler.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandler.kt
new file mode 100644
index 0000000000..09982b3847
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandler.kt
@@ -0,0 +1,31 @@
+/* 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.browser.engine.gecko.webpush
+
+import mozilla.components.concept.engine.webpush.WebPushHandler
+import org.mozilla.geckoview.GeckoRuntime
+
+/**
+ * Gecko-based implementation of [WebPushHandler], wrapping the
+ * controller object provided by GeckoView.
+ */
+internal class GeckoWebPushHandler(
+ private val runtime: GeckoRuntime,
+) : WebPushHandler {
+
+ /**
+ * See [WebPushHandler].
+ */
+ override fun onPushMessage(scope: String, message: ByteArray?) {
+ runtime.webPushController.onPushEvent(scope, message)
+ }
+
+ /**
+ * See [WebPushHandler].
+ */
+ override fun onSubscriptionChanged(scope: String) {
+ runtime.webPushController.onSubscriptionChanged(scope)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequest.kt
new file mode 100644
index 0000000000..22b09817f6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequest.kt
@@ -0,0 +1,23 @@
+/* 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.browser.engine.gecko.window
+
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.window.WindowRequest
+
+/**
+ * Gecko-based implementation of [WindowRequest].
+ */
+class GeckoWindowRequest(
+ override val url: String = "",
+ private val engineSession: GeckoEngineSession,
+ override val type: WindowRequest.Type = WindowRequest.Type.OPEN,
+) : WindowRequest {
+
+ override fun prepare(): EngineSession {
+ return this.engineSession
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/experiment/NimbusExperimentDelegate.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/experiment/NimbusExperimentDelegate.kt
new file mode 100644
index 0000000000..a14031a874
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/experiment/NimbusExperimentDelegate.kt
@@ -0,0 +1,93 @@
+/* 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.experiment
+
+import mozilla.components.browser.engine.gecko.GeckoNimbus
+import mozilla.components.support.base.log.logger.Logger
+import org.json.JSONObject
+import org.mozilla.experiments.nimbus.internal.FeatureHolder
+import org.mozilla.geckoview.ExperimentDelegate
+import org.mozilla.geckoview.ExperimentDelegate.ExperimentException
+import org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_FEATURE_NOT_FOUND
+import org.mozilla.geckoview.GeckoResult
+
+/**
+ * Default Nimbus [ExperimentDelegate] implementation to communicate with mobile Gecko and GeckoView.
+ */
+class NimbusExperimentDelegate : ExperimentDelegate {
+
+ private val logger = Logger(NimbusExperimentDelegate::javaClass.name)
+
+ /**
+ * Retrieves experiment information on the feature for use in GeckoView.
+ *
+ * @param feature Nimbus feature to retrieve information about
+ * @return a [GeckoResult] with a JSON object containing experiment information or completes exceptionally.
+ */
+ override fun onGetExperimentFeature(feature: String): GeckoResult<JSONObject> {
+ val result = GeckoResult<JSONObject>()
+ val nimbusFeature = GeckoNimbus.getFeature(feature)
+ if (nimbusFeature != null) {
+ result.complete(nimbusFeature.toJSONObject())
+ } else {
+ logger.warn("Could not find Nimbus feature '$feature' to retrieve experiment information.")
+ result.completeExceptionally(ExperimentException(ERROR_FEATURE_NOT_FOUND))
+ }
+ return result
+ }
+
+ /**
+ * Records that an exposure event occurred with the feature.
+ *
+ * @param feature Nimbus feature to record information about
+ * @return a [GeckoResult] that completes if the feature was found and recorded or completes exceptionally.
+ */
+ override fun onRecordExposureEvent(feature: String): GeckoResult<Void> {
+ return recordWithFeature(feature) { it.recordExposure() }
+ }
+
+ /**
+ * Records that an exposure event occurred with the feature, in a given experiment.
+ * Note: See [onRecordExposureEvent] if no slug is known or needed
+ *
+ * @param feature Nimbus feature to record information about
+ * @param slug Nimbus experiment slug to record information about
+ * @return a [GeckoResult] that completes if the feature was found and recorded or completes exceptionally.
+ */
+ override fun onRecordExperimentExposureEvent(feature: String, slug: String): GeckoResult<Void> {
+ return recordWithFeature(feature) { it.recordExperimentExposure(slug) }
+ }
+
+ /**
+ * Records a malformed exposure event for the feature.
+ *
+ * @param feature Nimbus feature to record information about
+ * @param part an optional detail or part identifier for then event. May be an empty string.
+ * @return a [GeckoResult] that completes if the feature was found and recorded or completes exceptionally.
+ */
+ override fun onRecordMalformedConfigurationEvent(feature: String, part: String): GeckoResult<Void> {
+ return recordWithFeature(feature) { it.recordMalformedConfiguration(part) }
+ }
+
+ /**
+ * Convenience method to record experiment events and return the correct errors.
+ *
+ * @param featureId Nimbus feature to record information on
+ * @param closure Nimbus record function to use
+ * @return a [GeckoResult] that completes if successful or else with an exception
+ */
+ private fun recordWithFeature(featureId: String, closure: (FeatureHolder<*>) -> Unit): GeckoResult<Void> {
+ val result = GeckoResult<Void>()
+ val nimbusFeature = GeckoNimbus.getFeature(featureId)
+ if (nimbusFeature != null) {
+ closure(nimbusFeature)
+ result.complete(null)
+ } else {
+ logger.warn("Could not find Nimbus feature '$featureId' to record an exposure event.")
+ result.completeExceptionally(ExperimentException(ERROR_FEATURE_NOT_FOUND))
+ }
+ return result
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionStateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionStateTest.kt
new file mode 100644
index 0000000000..3b6f5eb427
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionStateTest.kt
@@ -0,0 +1,61 @@
+/* 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.browser.engine.gecko
+
+import android.util.JsonWriter
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mozilla.geckoview.GeckoSession
+import java.io.ByteArrayOutputStream
+
+@RunWith(AndroidJUnit4::class)
+class GeckoEngineSessionStateTest {
+
+ @Test
+ fun writeTo() {
+ val geckoState: GeckoSession.SessionState = mock()
+ doReturn("<state>").`when`(geckoState).toString()
+
+ val state = GeckoEngineSessionState(geckoState)
+
+ val stream = ByteArrayOutputStream()
+ val writer = JsonWriter(stream.writer())
+ state.writeTo(writer)
+ val json = JSONObject(stream.toString())
+
+ assertEquals(1, json.length())
+ assertTrue(json.has("GECKO_STATE"))
+ assertEquals("<state>", json.getString("GECKO_STATE"))
+ }
+
+ @Test
+ fun fromJSON() {
+ val json = JSONObject().apply {
+ put("GECKO_STATE", "{ 'foo': 'bar' }")
+ }
+
+ val state = GeckoEngineSessionState.fromJSON(json)
+
+ assertEquals("""{"foo":"bar"}""", state.actualState.toString())
+ }
+
+ @Test
+ fun `fromJSON with invalid JSON returns empty State`() {
+ val json = JSONObject().apply {
+ put("nothing", "helpful")
+ }
+
+ val state = GeckoEngineSessionState.fromJSON(json)
+
+ assertNull(state.actualState)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt
new file mode 100644
index 0000000000..87849440b6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt
@@ -0,0 +1,4874 @@
+/* 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.browser.engine.gecko
+
+import android.content.Intent
+import android.graphics.Color
+import android.os.Handler
+import android.os.Looper.getMainLooper
+import android.os.Message
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.engine.gecko.ext.geckoTrackingProtectionPermission
+import mozilla.components.browser.engine.gecko.ext.isExcludedForTrackingProtection
+import mozilla.components.browser.engine.gecko.permission.geckoContentPermission
+import mozilla.components.browser.engine.gecko.translate.GeckoTranslationUtils.intoTranslationError
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+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.concept.engine.EngineSession.SafeBrowsingPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.concept.engine.UnsupportedSettingException
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.history.HistoryItem
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.storage.PageVisit
+import mozilla.components.concept.storage.RedirectSource
+import mozilla.components.concept.storage.VisitType
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.expectException
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import mozilla.components.support.utils.DownloadUtils.RESPONSE_CODE_SUCCESS
+import mozilla.components.support.utils.ThreadUtils
+import mozilla.components.test.ReflectionUtils
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+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.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mozilla.geckoview.AllowOrDeny
+import org.mozilla.geckoview.ContentBlocking
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.TYPE_AUDIO
+import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE
+import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.TYPE_NONE
+import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO
+import org.mozilla.geckoview.GeckoSession.GeckoPrintException
+import org.mozilla.geckoview.GeckoSession.GeckoPrintException.ERROR_PRINT_SETTINGS_SERVICE_NOT_AVAILABLE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
+import org.mozilla.geckoview.GeckoSession.ProgressDelegate.SecurityInformation
+import org.mozilla.geckoview.GeckoSessionSettings
+import org.mozilla.geckoview.SessionFinder
+import org.mozilla.geckoview.TranslationsController
+import org.mozilla.geckoview.TranslationsController.TranslationsException
+import org.mozilla.geckoview.WebRequestError
+import org.mozilla.geckoview.WebRequestError.ERROR_CATEGORY_UNKNOWN
+import org.mozilla.geckoview.WebRequestError.ERROR_MALFORMED_URI
+import org.mozilla.geckoview.WebRequestError.ERROR_UNKNOWN
+import org.mozilla.geckoview.WebResponse
+import org.robolectric.Shadows.shadowOf
+import java.io.IOException
+import java.security.Principal
+import java.security.cert.X509Certificate
+
+typealias GeckoAntiTracking = ContentBlocking.AntiTracking
+typealias GeckoSafeBrowsing = ContentBlocking.SafeBrowsing
+typealias GeckoCookieBehavior = ContentBlocking.CookieBehavior
+
+private const val AID = "AID"
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class GeckoEngineSessionTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var runtime: GeckoRuntime
+ private lateinit var geckoSession: GeckoSession
+ private lateinit var geckoSessionProvider: () -> GeckoSession
+
+ private lateinit var navigationDelegate: ArgumentCaptor<GeckoSession.NavigationDelegate>
+ private lateinit var progressDelegate: ArgumentCaptor<GeckoSession.ProgressDelegate>
+ private lateinit var mediaDelegate: ArgumentCaptor<GeckoSession.MediaDelegate>
+ private lateinit var contentDelegate: ArgumentCaptor<GeckoSession.ContentDelegate>
+ private lateinit var permissionDelegate: ArgumentCaptor<GeckoSession.PermissionDelegate>
+ private lateinit var scrollDelegate: ArgumentCaptor<GeckoSession.ScrollDelegate>
+ private lateinit var contentBlockingDelegate: ArgumentCaptor<ContentBlocking.Delegate>
+ private lateinit var historyDelegate: ArgumentCaptor<GeckoSession.HistoryDelegate>
+
+ @Suppress("DEPRECATION")
+ // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/8514
+ @Before
+ fun setup() {
+ ThreadUtils.setHandlerForTest(
+ object : Handler() {
+ override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean {
+ val wrappedRunnable = Runnable {
+ try {
+ msg.callback?.run()
+ } catch (t: Throwable) {
+ // We ignore this in the test as the runnable could be calling
+ // a native method (disposeNative) which won't work in Robolectric
+ }
+ }
+ return super.sendMessageAtTime(Message.obtain(this, wrappedRunnable), uptimeMillis)
+ }
+ },
+ )
+
+ runtime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ navigationDelegate = ArgumentCaptor.forClass(GeckoSession.NavigationDelegate::class.java)
+ progressDelegate = ArgumentCaptor.forClass(GeckoSession.ProgressDelegate::class.java)
+ mediaDelegate = ArgumentCaptor.forClass(GeckoSession.MediaDelegate::class.java)
+ contentDelegate = ArgumentCaptor.forClass(GeckoSession.ContentDelegate::class.java)
+ permissionDelegate = ArgumentCaptor.forClass(GeckoSession.PermissionDelegate::class.java)
+ scrollDelegate = ArgumentCaptor.forClass(GeckoSession.ScrollDelegate::class.java)
+ contentBlockingDelegate = ArgumentCaptor.forClass(ContentBlocking.Delegate::class.java)
+ historyDelegate = ArgumentCaptor.forClass(GeckoSession.HistoryDelegate::class.java)
+
+ geckoSession = mockGeckoSession()
+ geckoSessionProvider = { geckoSession }
+ }
+
+ private fun captureDelegates() {
+ verify(geckoSession).navigationDelegate = navigationDelegate.capture()
+ verify(geckoSession).progressDelegate = progressDelegate.capture()
+ verify(geckoSession).contentDelegate = contentDelegate.capture()
+ verify(geckoSession).permissionDelegate = permissionDelegate.capture()
+ verify(geckoSession).scrollDelegate = scrollDelegate.capture()
+ verify(geckoSession).contentBlockingDelegate = contentBlockingDelegate.capture()
+ verify(geckoSession).historyDelegate = historyDelegate.capture()
+ verify(geckoSession).mediaDelegate = mediaDelegate.capture()
+ }
+
+ @Test
+ fun engineSessionInitialization() {
+ GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ verify(geckoSession).open(any())
+
+ captureDelegates()
+
+ assertNotNull(navigationDelegate.value)
+ assertNotNull(progressDelegate.value)
+ }
+
+ @Test
+ fun isIgnoredForTrackingProtection() {
+ val session = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ session.geckoPermissions =
+ listOf(geckoContentPermission(type = PERMISSION_TRACKING, value = VALUE_ALLOW))
+
+ var ignored = session.isIgnoredForTrackingProtection()
+
+ assertTrue(ignored)
+
+ session.geckoPermissions =
+ listOf(geckoContentPermission(type = PERMISSION_TRACKING, value = VALUE_DENY))
+
+ ignored = session.isIgnoredForTrackingProtection()
+
+ assertFalse(ignored)
+ }
+
+ @Test
+ fun `WHEN calling isExcludedForTrackingProtection THEN indicate if it is excluded for tracking protection`() {
+ val excludedPermission = geckoContentPermission(type = PERMISSION_TRACKING, value = VALUE_ALLOW)
+
+ assertTrue(excludedPermission.isExcludedForTrackingProtection)
+
+ val noExcludedPermission = geckoContentPermission(type = PERMISSION_TRACKING, value = VALUE_DENY)
+
+ assertFalse(noExcludedPermission.isExcludedForTrackingProtection)
+
+ val storagePermission = geckoContentPermission(type = PERMISSION_STORAGE_ACCESS, value = VALUE_DENY)
+
+ assertFalse(storagePermission.isExcludedForTrackingProtection)
+ }
+
+ @Test
+ fun `WHEN calling geckoTrackingProtectionPermission on a session THEN provide the gecko tracking protection permission`() {
+ val session = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+ val trackingProtectionPermission = geckoContentPermission(type = PERMISSION_TRACKING, value = VALUE_ALLOW)
+ val storagePermission = geckoContentPermission(type = PERMISSION_STORAGE_ACCESS, value = VALUE_DENY)
+
+ session.geckoPermissions = listOf(trackingProtectionPermission, storagePermission)
+
+ assertEquals(session.geckoTrackingProtectionPermission, trackingProtectionPermission)
+
+ session.geckoPermissions = listOf(storagePermission)
+
+ assertNull(session.geckoTrackingProtectionPermission)
+ }
+
+ @Test
+ fun progressDelegateNotifiesObservers() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ var observedProgress = 0
+ var observedLoadingState = false
+ var observedSecurityChange = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadingStateChange(loading: Boolean) { observedLoadingState = loading }
+ override fun onProgress(progress: Int) { observedProgress = progress }
+ override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
+ // We cannot assert on actual parameters as SecurityInfo's fields can't be set
+ // from the outside and its constructor isn't accessible either.
+ observedSecurityChange = true
+ }
+ },
+ )
+
+ captureDelegates()
+
+ progressDelegate.value.onPageStart(mock(), "http://mozilla.org")
+ assertEquals(GeckoEngineSession.PROGRESS_START, observedProgress)
+ assertEquals(true, observedLoadingState)
+
+ progressDelegate.value.onPageStop(mock(), true)
+ assertEquals(GeckoEngineSession.PROGRESS_STOP, observedProgress)
+ assertEquals(false, observedLoadingState)
+
+ // Stop will update the loading state and progress observers even when
+ // we haven't completed been successful.
+ progressDelegate.value.onPageStart(mock(), "http://mozilla.org")
+ assertEquals(GeckoEngineSession.PROGRESS_START, observedProgress)
+ assertEquals(true, observedLoadingState)
+
+ progressDelegate.value.onPageStop(mock(), false)
+ assertEquals(GeckoEngineSession.PROGRESS_STOP, observedProgress)
+ assertEquals(false, observedLoadingState)
+
+ val securityInfo = mock<SecurityInformation>()
+ progressDelegate.value.onSecurityChange(mock(), securityInfo)
+ assertTrue(observedSecurityChange)
+
+ observedSecurityChange = false
+
+ progressDelegate.value.onSecurityChange(mock(), mock())
+ assertTrue(observedSecurityChange)
+ }
+
+ @Test
+ fun navigationDelegateNotifiesObservers() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ var observedUrl = ""
+ var observedUserGesture = true
+ var observedCanGoBack = false
+ var observedCanGoForward = false
+ var cookieBanner = CookieBannerHandlingStatus.HANDLED
+ var displaysProduct = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLocationChange(url: String, hasUserGesture: Boolean) {
+ observedUrl = url
+ observedUserGesture = hasUserGesture
+ }
+ override fun onNavigationStateChange(canGoBack: Boolean?, canGoForward: Boolean?) {
+ canGoBack?.let { observedCanGoBack = canGoBack }
+ canGoForward?.let { observedCanGoForward = canGoForward }
+ }
+ override fun onCookieBannerChange(status: CookieBannerHandlingStatus) {
+ cookieBanner = status
+ }
+ override fun onProductUrlChange(isProductUrl: Boolean) {
+ displaysProduct = isProductUrl
+ }
+ },
+ )
+
+ captureDelegates()
+
+ navigationDelegate.value.onLocationChange(mock(), "http://mozilla.org", emptyList(), false)
+ assertEquals("http://mozilla.org", observedUrl)
+ assertEquals(false, observedUserGesture)
+ assertEquals(CookieBannerHandlingStatus.NO_DETECTED, cookieBanner)
+ // TO DO: add a positive test case after a test endpoint is implemented in desktop (Bug 1846341)
+ assertEquals(false, displaysProduct)
+
+ navigationDelegate.value.onCanGoBack(mock(), true)
+ assertEquals(true, observedCanGoBack)
+
+ navigationDelegate.value.onCanGoForward(mock(), true)
+ assertEquals(true, observedCanGoForward)
+ }
+
+ @Test
+ fun contentDelegateNotifiesObserverAboutDownloads() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = true,
+ )
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ val response = WebResponse.Builder("https://download.mozilla.org/image.png")
+ .addHeader(Headers.Names.CONTENT_TYPE, "image/png")
+ .addHeader(Headers.Names.CONTENT_LENGTH, "42")
+ .skipConfirmation(true)
+ .requestExternalApp(true)
+ .body(mock())
+ .build()
+
+ val captor = argumentCaptor<Response>()
+ captureDelegates()
+ contentDelegate.value.onExternalResponse(mock(), response)
+
+ verify(observer).onExternalResource(
+ url = eq("https://download.mozilla.org/image.png"),
+ fileName = eq("image.png"),
+ contentLength = eq(42),
+ contentType = eq("image/png"),
+ cookie = eq(null),
+ userAgent = eq(null),
+ isPrivate = eq(true),
+ skipConfirmation = eq(true),
+ openInApp = eq(true),
+ response = captor.capture(),
+ )
+
+ assertNotNull(captor.value)
+ }
+
+ @Test
+ fun contentDelegateNotifiesObserverAboutDownloadsWithMalformedContentLength() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = true,
+ )
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ val response = WebResponse.Builder("https://download.mozilla.org/image.png")
+ .addHeader(Headers.Names.CONTENT_TYPE, "image/png")
+ .addHeader(Headers.Names.CONTENT_LENGTH, "42,42")
+ .body(mock())
+ .build()
+
+ val captor = argumentCaptor<Response>()
+ captureDelegates()
+ contentDelegate.value.onExternalResponse(mock(), response)
+
+ verify(observer).onExternalResource(
+ url = eq("https://download.mozilla.org/image.png"),
+ fileName = eq("image.png"),
+ contentLength = eq(null),
+ contentType = eq("image/png"),
+ cookie = eq(null),
+ userAgent = eq(null),
+ isPrivate = eq(true),
+ skipConfirmation = eq(false),
+ openInApp = eq(false),
+ response = captor.capture(),
+ )
+
+ assertNotNull(captor.value)
+ }
+
+ @Test
+ fun contentDelegateNotifiesObserverAboutDownloadsWithEmptyContentLength() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = true,
+ )
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ val response = WebResponse.Builder("https://download.mozilla.org/image.png")
+ .addHeader(Headers.Names.CONTENT_TYPE, "image/png")
+ .addHeader(Headers.Names.CONTENT_LENGTH, "")
+ .body(mock())
+ .build()
+
+ val captor = argumentCaptor<Response>()
+ captureDelegates()
+ contentDelegate.value.onExternalResponse(mock(), response)
+
+ verify(observer).onExternalResource(
+ url = eq("https://download.mozilla.org/image.png"),
+ fileName = eq("image.png"),
+ contentLength = eq(null),
+ contentType = eq("image/png"),
+ cookie = eq(null),
+ userAgent = eq(null),
+ isPrivate = eq(true),
+ skipConfirmation = eq(false),
+ openInApp = eq(false),
+ response = captor.capture(),
+ )
+
+ assertNotNull(captor.value)
+ }
+
+ @Test
+ fun contentDelegateNotifiesObserverAboutWebAppManifest() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ val json = JSONObject().apply {
+ put("name", "Minimal")
+ put("start_url", "/")
+ }
+ val manifest = WebAppManifest(
+ name = "Minimal",
+ startUrl = "/",
+ )
+
+ captureDelegates()
+ contentDelegate.value.onWebAppManifest(mock(), json)
+
+ verify(observer).onWebAppManifestLoaded(manifest)
+ }
+
+ @Test
+ fun permissionDelegateNotifiesObservers() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ val observedContentPermissionRequests: MutableList<PermissionRequest> = mutableListOf()
+ val observedAppPermissionRequests: MutableList<PermissionRequest> = mutableListOf()
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onContentPermissionRequest(permissionRequest: PermissionRequest) {
+ observedContentPermissionRequests.add(permissionRequest)
+ }
+
+ override fun onAppPermissionRequest(permissionRequest: PermissionRequest) {
+ observedAppPermissionRequests.add(permissionRequest)
+ }
+ },
+ )
+
+ captureDelegates()
+
+ permissionDelegate.value.onContentPermissionRequest(
+ geckoSession,
+ geckoContentPermission("originContent", GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION),
+ )
+
+ permissionDelegate.value.onContentPermissionRequest(
+ geckoSession,
+ geckoContentPermission("", GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION),
+ )
+
+ permissionDelegate.value.onMediaPermissionRequest(
+ geckoSession,
+ "originMedia",
+ emptyArray(),
+ emptyArray(),
+ mock(),
+ )
+
+ permissionDelegate.value.onMediaPermissionRequest(
+ geckoSession,
+ "about:blank",
+ null,
+ null,
+ mock(),
+ )
+
+ permissionDelegate.value.onAndroidPermissionsRequest(
+ geckoSession,
+ emptyArray(),
+ mock(),
+ )
+
+ permissionDelegate.value.onAndroidPermissionsRequest(
+ geckoSession,
+ null,
+ mock(),
+ )
+
+ assertEquals(4, observedContentPermissionRequests.size)
+ assertEquals("originContent", observedContentPermissionRequests[0].uri)
+ assertEquals("", observedContentPermissionRequests[1].uri)
+ assertEquals("originMedia", observedContentPermissionRequests[2].uri)
+ assertEquals("about:blank", observedContentPermissionRequests[3].uri)
+ assertEquals(2, observedAppPermissionRequests.size)
+ }
+
+ @Test
+ fun scrollDelegateNotifiesObservers() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ val observedScrollChanges: MutableList<Pair<Int, Int>> = mutableListOf()
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onScrollChange(scrollX: Int, scrollY: Int) {
+ observedScrollChanges.add(Pair(scrollX, scrollY))
+ }
+ },
+ )
+
+ captureDelegates()
+
+ scrollDelegate.value.onScrollChanged(
+ geckoSession,
+ 1234,
+ 4321,
+ )
+
+ scrollDelegate.value.onScrollChanged(
+ geckoSession,
+ 2345,
+ 5432,
+ )
+
+ assertEquals(2, observedScrollChanges.size)
+ assertEquals(Pair(1234, 4321), observedScrollChanges[0])
+ assertEquals(Pair(2345, 5432), observedScrollChanges[1])
+ }
+
+ @Test
+ fun loadUrl() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val parentEngineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+
+ engineSession.loadUrl("http://mozilla.org")
+ verify(geckoSession).load(
+ GeckoSession.Loader().uri("http://mozilla.org"),
+ )
+
+ engineSession.loadUrl("http://www.mozilla.org", flags = LoadUrlFlags.select(LoadUrlFlags.EXTERNAL))
+ verify(geckoSession).load(
+ GeckoSession.Loader().uri("http://www.mozilla.org").flags(LoadUrlFlags.EXTERNAL),
+ )
+
+ engineSession.loadUrl("http://www.mozilla.org", parent = parentEngineSession)
+ verify(geckoSession).load(
+ GeckoSession.Loader().uri("http://www.mozilla.org").referrer(parentEngineSession.geckoSession),
+ )
+
+ val extraHeaders = mapOf("X-Extra-Header" to "true")
+ engineSession.loadUrl("http://www.mozilla.org", additionalHeaders = extraHeaders)
+ verify(geckoSession).load(
+ GeckoSession.Loader().uri("http://www.mozilla.org").additionalHeaders(extraHeaders)
+ .headerFilter(GeckoSession.HEADER_FILTER_CORS_SAFELISTED),
+ )
+
+ engineSession.loadUrl(
+ "http://www.mozilla.org",
+ flags = LoadUrlFlags.select(LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS),
+ additionalHeaders = extraHeaders,
+ )
+ verify(geckoSession).load(
+ GeckoSession.Loader().uri("http://www.mozilla.org").additionalHeaders(extraHeaders)
+ .headerFilter(GeckoSession.HEADER_FILTER_CORS_SAFELISTED),
+ )
+ }
+
+ @Test
+ fun `loadUrl doesn't load URLs with blocked schemes`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+
+ engineSession.loadUrl("file://test.txt")
+ engineSession.loadUrl("FILE://test.txt")
+ verify(geckoSession, never()).load(GeckoSession.Loader().uri("file://test.txt"))
+ verify(geckoSession, never()).load(GeckoSession.Loader().uri("FILE://test.txt"))
+
+ engineSession.loadUrl("resource://package/test.text")
+ engineSession.loadUrl("RESOURCE://package/test.text")
+ verify(geckoSession, never()).load(GeckoSession.Loader().uri("resource://package/test.text"))
+ verify(geckoSession, never()).load(GeckoSession.Loader().uri("RESOURCE://package/test.text"))
+ }
+
+ @Test
+ fun loadData() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ engineSession.loadData("<html><body>Hello!</body></html>")
+ verify(geckoSession).load(
+ GeckoSession.Loader().data("<html><body>Hello!</body></html>", "text/html"),
+ )
+
+ engineSession.loadData("Hello!", "text/plain", "UTF-8")
+ verify(geckoSession).load(
+ GeckoSession.Loader().data("Hello!", "text/plain"),
+ )
+
+ engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", "text/plain", "base64")
+ verify(geckoSession).load(
+ GeckoSession.Loader().data("ahr0cdovl21vemlsbgeub3jn==".toByteArray(), "text/plain"),
+ )
+
+ engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", encoding = "base64")
+ verify(geckoSession).load(
+ GeckoSession.Loader().data("ahr0cdovl21vemlsbgeub3jn==".toByteArray(), "text/html"),
+ )
+ }
+
+ @Test
+ fun `getGeckoFlags returns only gecko load flags`() {
+ val flags = LoadUrlFlags.select(LoadUrlFlags.all().getGeckoFlags())
+
+ assertFalse(flags.contains(LoadUrlFlags.NONE))
+ assertTrue(flags.contains(LoadUrlFlags.BYPASS_CACHE))
+ assertTrue(flags.contains(LoadUrlFlags.BYPASS_PROXY))
+ assertTrue(flags.contains(LoadUrlFlags.EXTERNAL))
+ assertTrue(flags.contains(LoadUrlFlags.ALLOW_POPUPS))
+ assertTrue(flags.contains(LoadUrlFlags.BYPASS_CLASSIFIER))
+ assertTrue(flags.contains(LoadUrlFlags.LOAD_FLAGS_FORCE_ALLOW_DATA_URI))
+ assertTrue(flags.contains(LoadUrlFlags.LOAD_FLAGS_REPLACE_HISTORY))
+ assertTrue(flags.contains(LoadUrlFlags.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE))
+ assertFalse(flags.contains(LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS))
+ assertFalse(flags.contains(LoadUrlFlags.ALLOW_JAVASCRIPT_URL))
+ }
+
+ @Test
+ fun loadDataBase64() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ engineSession.loadData("Hello!", "text/plain", "UTF-8")
+ verify(geckoSession).load(
+ GeckoSession.Loader().data("Hello!", "text/plain"),
+ )
+
+ engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", "text/plain", "base64")
+ verify(geckoSession).load(
+ GeckoSession.Loader().data("ahr0cdovl21vemlsbgeub3jn==".toByteArray(), "text/plain"),
+ )
+
+ engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", encoding = "base64")
+ verify(geckoSession).load(
+ GeckoSession.Loader().data("ahr0cdovl21vemlsbgeub3jn==".toByteArray(), "text/plain"),
+ )
+ }
+
+ @Test
+ fun stopLoading() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ engineSession.stopLoading()
+
+ verify(geckoSession).stop()
+ }
+
+ @Test
+ fun reload() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ engineSession.loadUrl("http://mozilla.org")
+
+ // Initial load is still in progress so reload should not be called.
+ // Instead we should have called loadUrl again to prevent reloading
+ // about:blank.
+ engineSession.reload()
+ verify(geckoSession, never()).reload(GeckoSession.LOAD_FLAGS_BYPASS_CACHE)
+ verify(geckoSession, times(2)).load(
+ GeckoSession.Loader().uri("http://mozilla.org"),
+ )
+
+ // Subsequent reloads should simply call reload on the gecko session.
+ engineSession.initialLoadRequest = null
+ engineSession.reload()
+ verify(geckoSession).reload(GeckoSession.LOAD_FLAGS_NONE)
+
+ engineSession.reload(flags = LoadUrlFlags.select(LoadUrlFlags.BYPASS_CACHE))
+ verify(geckoSession).reload(GeckoSession.LOAD_FLAGS_BYPASS_CACHE)
+ }
+
+ @Test
+ fun goBack() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ engineSession.goBack()
+
+ verify(geckoSession).goBack(true)
+ }
+
+ @Test
+ fun goForward() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ engineSession.goForward()
+
+ verify(geckoSession).goForward(true)
+ }
+
+ @Test
+ fun goToHistoryIndex() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ engineSession.goToHistoryIndex(0)
+
+ verify(geckoSession).gotoHistoryIndex(0)
+ }
+
+ @Test
+ fun restoreState() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ val actualState: GeckoSession.SessionState = mock()
+ val state = GeckoEngineSessionState(actualState)
+
+ assertTrue(engineSession.restoreState(state))
+ verify(geckoSession).restoreState(any())
+ }
+
+ @Test
+ fun `restoreState returns false for null state`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ val state = GeckoEngineSessionState(null)
+
+ assertFalse(engineSession.restoreState(state))
+ verify(geckoSession, never()).restoreState(any())
+ }
+
+ @Test
+ fun progressDelegateIgnoresInitialLoadOfAboutBlank() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ var observedSecurityChange = false
+ var progressObserved = false
+ var loadingStateChangeObserved = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
+ observedSecurityChange = true
+ }
+
+ override fun onProgress(progress: Int) {
+ progressObserved = true
+ }
+
+ override fun onLoadingStateChange(loading: Boolean) {
+ loadingStateChangeObserved = true
+ }
+ },
+ )
+
+ captureDelegates()
+
+ progressDelegate.value.onSecurityChange(
+ mock(),
+ MockSecurityInformation("moz-nullprincipal:{uuid}"),
+ )
+ assertFalse(observedSecurityChange)
+
+ progressDelegate.value.onSecurityChange(
+ mock(),
+ MockSecurityInformation("https://www.mozilla.org"),
+ )
+ assertTrue(observedSecurityChange)
+
+ progressDelegate.value.onPageStart(mock(), "about:blank")
+ assertFalse(progressObserved)
+ assertFalse(loadingStateChangeObserved)
+
+ progressDelegate.value.onPageStop(mock(), true)
+ assertFalse(progressObserved)
+ assertFalse(loadingStateChangeObserved)
+
+ progressDelegate.value.onPageStart(mock(), "https://www.mozilla.org")
+ assertTrue(progressObserved)
+ assertTrue(loadingStateChangeObserved)
+
+ progressDelegate.value.onPageStop(mock(), true)
+ assertTrue(progressObserved)
+ assertTrue(loadingStateChangeObserved)
+ }
+
+ @Test
+ fun navigationDelegateIgnoresInitialLoadOfAboutBlank() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ var observedUrl = ""
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLocationChange(url: String, hasUserGesture: Boolean) { observedUrl = url }
+ },
+ )
+
+ captureDelegates()
+
+ navigationDelegate.value.onLocationChange(mock(), "about:blank", emptyList(), false)
+ assertEquals("", observedUrl)
+
+ navigationDelegate.value.onLocationChange(mock(), "about:blank", emptyList(), false)
+ assertEquals("", observedUrl)
+
+ navigationDelegate.value.onLocationChange(mock(), "https://www.mozilla.org", emptyList(), false)
+ assertEquals("https://www.mozilla.org", observedUrl)
+
+ navigationDelegate.value.onLocationChange(mock(), "about:blank", emptyList(), false)
+ assertEquals("about:blank", observedUrl)
+ }
+
+ @Test
+ fun `onLoadRequest will reset initial load flag on process switch to ignore about blank loads`() {
+ val session = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+ captureDelegates()
+ assertTrue(session.initialLoad)
+
+ navigationDelegate.value.onLocationChange(mock(), "https://mozilla.org", emptyList(), false)
+ assertFalse(session.initialLoad)
+
+ navigationDelegate.value.onLoadRequest(mock(), mockLoadRequest("moz-extension://1234-test"))
+ assertTrue(session.initialLoad)
+
+ var observedUrl = ""
+ session.register(
+ object : EngineSession.Observer {
+ override fun onLocationChange(url: String, hasUserGesture: Boolean) { observedUrl = url }
+ },
+ )
+ navigationDelegate.value.onLocationChange(mock(), "about:blank", emptyList(), false)
+ assertEquals("", observedUrl)
+
+ navigationDelegate.value.onLocationChange(mock(), "https://www.mozilla.org", emptyList(), false)
+ assertEquals("https://www.mozilla.org", observedUrl)
+
+ navigationDelegate.value.onLocationChange(mock(), "about:blank", emptyList(), false)
+ assertEquals("about:blank", observedUrl)
+ }
+
+ @Test
+ fun `do not keep track of current url via onPageStart events`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ assertNull(engineSession.currentUrl)
+ progressDelegate.value.onPageStart(geckoSession, "https://www.mozilla.org")
+ assertNull(engineSession.currentUrl)
+
+ progressDelegate.value.onPageStart(geckoSession, "https://www.firefox.com")
+ assertNull(engineSession.currentUrl)
+ }
+
+ @Test
+ fun `keeps track of current url via onLocationChange events`() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+ val geckoResult = GeckoResult<Boolean?>()
+
+ captureDelegates()
+ geckoResult.complete(true)
+
+ assertNull(engineSession.currentUrl)
+ navigationDelegate.value.onLocationChange(geckoSession, "https://www.mozilla.org", emptyList(), false)
+ assertEquals("https://www.mozilla.org", engineSession.currentUrl)
+
+ navigationDelegate.value.onLocationChange(geckoSession, "https://www.firefox.com", emptyList(), false)
+ assertEquals("https://www.firefox.com", engineSession.currentUrl)
+ }
+
+ @Test
+ fun `WHEN onLocationChange is called THEN geckoPermissions is assigned`() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ captureDelegates()
+
+ navigationDelegate.value.onLocationChange(geckoSession, "https://www.mozilla.org", listOf(mock()), false)
+
+ assertTrue(engineSession.geckoPermissions.isNotEmpty())
+ }
+
+ @Test
+ fun `WHEN onLocationChange is called with null URL THEN geckoPermissions is assigned`() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ captureDelegates()
+
+ navigationDelegate.value.onLocationChange(geckoSession, null, listOf(mock()), false)
+
+ assertTrue(engineSession.geckoPermissions.isNotEmpty())
+ }
+
+ @Test
+ fun `notifies configured history delegate of title changes`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ // Nothing breaks if history delegate isn't configured.
+ contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+ whenever(historyTrackingDelegate.shouldStoreUri(eq("https://www.mozilla.com"))).thenReturn(true)
+
+ contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
+ verify(historyTrackingDelegate, never()).onTitleChanged(anyString(), anyString())
+
+ // This sets the currentUrl.
+ navigationDelegate.value.onLocationChange(geckoSession, "https://www.mozilla.com", emptyList(), false)
+
+ contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
+ verify(historyTrackingDelegate).onTitleChanged(eq("https://www.mozilla.com"), eq("Hello World!"))
+ verify(historyTrackingDelegate).shouldStoreUri(eq("https://www.mozilla.com"))
+ }
+
+ @Test
+ fun `does not notify configured history delegate of title changes for private sessions`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ privateMode = true,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ // Nothing breaks if history delegate isn't configured.
+ contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
+ verify(historyTrackingDelegate, never()).onTitleChanged(anyString(), anyString())
+ verify(observer).onTitleChange("Hello World!")
+
+ // This sets the currentUrl.
+ progressDelegate.value.onPageStart(geckoSession, "https://www.mozilla.com")
+
+ contentDelegate.value.onTitleChange(geckoSession, "Mozilla")
+ verify(historyTrackingDelegate, never()).onTitleChanged(anyString(), anyString())
+ verify(observer).onTitleChange("Mozilla")
+ }
+
+ @Test
+ fun `GIVEN an app initiated request WHEN the user swipe back or launches the browser THEN the tab should display the correct page`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+
+ captureDelegates()
+
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ var observedUrl = "https://www.google.com"
+ var observedTitle = "Google Search"
+ val emptyPageUrl = "https://example.com"
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLocationChange(url: String, hasUserGesture: Boolean) { observedUrl = url }
+ override fun onTitleChange(title: String) { observedTitle = title }
+ },
+ )
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+ engineSession.appRedirectUrl = emptyPageUrl
+
+ class MockHistoryList(
+ items: List<GeckoSession.HistoryDelegate.HistoryItem>,
+ private val currentIndex: Int,
+ ) : ArrayList<GeckoSession.HistoryDelegate.HistoryItem>(items), GeckoSession.HistoryDelegate.HistoryList {
+ override fun getCurrentIndex() = currentIndex
+ }
+
+ fun mockHistoryItem(title: String?, uri: String): GeckoSession.HistoryDelegate.HistoryItem {
+ val item = mock<GeckoSession.HistoryDelegate.HistoryItem>()
+ whenever(item.title).thenReturn(title)
+ whenever(item.uri).thenReturn(uri)
+ return item
+ }
+
+ historyDelegate.value.onHistoryStateChange(mock(), MockHistoryList(emptyList(), 0))
+
+ historyDelegate.value.onHistoryStateChange(
+ mock(),
+ MockHistoryList(
+ listOf(
+ mockHistoryItem("Google Search", observedUrl),
+ mockHistoryItem("Moved", emptyPageUrl),
+ ),
+ 1,
+ ),
+ )
+
+ navigationDelegate.value.onLocationChange(geckoSession, emptyPageUrl, emptyList(), false)
+ contentDelegate.value.onTitleChange(geckoSession, emptyPageUrl)
+
+ historyDelegate.value.onVisited(
+ geckoSession,
+ emptyPageUrl,
+ null,
+ 9,
+ )
+
+ verify(historyTrackingDelegate, never()).onVisited(eq(emptyPageUrl), any())
+ assertEquals("https://www.google.com", observedUrl)
+ assertEquals("Google Search", observedTitle)
+ }
+
+ @Test
+ fun `notifies configured history delegate of preview image URL changes`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+ val geckoResult = GeckoResult<Boolean?>()
+
+ captureDelegates()
+ geckoResult.complete(true)
+
+ val previewImageUrl = "https://test.com/og-image-url"
+
+ // Nothing breaks if history delegate isn't configured.
+ contentDelegate.value.onPreviewImage(geckoSession, previewImageUrl)
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+ whenever(historyTrackingDelegate.shouldStoreUri(eq("https://www.mozilla.com"))).thenReturn(true)
+
+ contentDelegate.value.onPreviewImage(geckoSession, previewImageUrl)
+ verify(historyTrackingDelegate, never()).onPreviewImageChange(anyString(), anyString())
+
+ // This sets the currentUrl.
+ navigationDelegate.value.onLocationChange(geckoSession, "https://www.mozilla.com", emptyList(), false)
+
+ contentDelegate.value.onPreviewImage(geckoSession, previewImageUrl)
+ verify(historyTrackingDelegate).onPreviewImageChange(eq("https://www.mozilla.com"), eq(previewImageUrl))
+ verify(historyTrackingDelegate).shouldStoreUri(eq("https://www.mozilla.com"))
+ }
+
+ @Test
+ fun `does not notify configured history delegate of preview image URL changes for private sessions`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ privateMode = true,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ // Nothing breaks if history delegate isn't configured.
+ contentDelegate.value.onPreviewImage(geckoSession, "https://test.com/og-image-url")
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ contentDelegate.value.onPreviewImage(geckoSession, "https://test.com/og-image-url")
+ verify(historyTrackingDelegate, never()).onPreviewImageChange(anyString(), anyString())
+ verify(observer).onPreviewImageChange("https://test.com/og-image-url")
+
+ // This sets the currentUrl.
+ progressDelegate.value.onPageStart(geckoSession, "https://www.mozilla.com")
+
+ contentDelegate.value.onPreviewImage(geckoSession, "https://test.com/og-image.jpg")
+ verify(historyTrackingDelegate, never()).onPreviewImageChange(anyString(), anyString())
+ verify(observer).onPreviewImageChange("https://test.com/og-image.jpg")
+ }
+
+ @Test
+ fun `does not notify configured history delegate for top-level visits to error pages`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+ whenever(historyTrackingDelegate.shouldStoreUri(any())).thenReturn(true)
+
+ historyDelegate.value.onVisited(
+ geckoSession,
+ "about:neterror",
+ null,
+ GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
+ or GeckoSession.HistoryDelegate.VISIT_UNRECOVERABLE_ERROR,
+ )
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate, never()).onVisited(anyString(), any())
+ }
+
+ @Test
+ fun `notifies configured history delegate of visits`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+ whenever(historyTrackingDelegate.shouldStoreUri("https://www.mozilla.com")).thenReturn(true)
+
+ historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com"), eq(PageVisit(VisitType.LINK)))
+ }
+
+ @Test
+ fun `notifies configured history delegate of reloads`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+ whenever(historyTrackingDelegate.shouldStoreUri("https://www.mozilla.com")).thenReturn(true)
+
+ historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", "https://www.mozilla.com", GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com"), eq(PageVisit(VisitType.RELOAD)))
+ }
+
+ @Test
+ fun `checks with the delegate before trying to record a visit`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+ whenever(historyTrackingDelegate.shouldStoreUri("https://www.mozilla.com/allowed")).thenReturn(true)
+ whenever(historyTrackingDelegate.shouldStoreUri("https://www.mozilla.com/not-allowed")).thenReturn(false)
+
+ historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com/allowed", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
+
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).shouldStoreUri("https://www.mozilla.com/allowed")
+ verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/allowed"), eq(PageVisit(VisitType.LINK)))
+
+ historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com/not-allowed", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
+
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).shouldStoreUri("https://www.mozilla.com/not-allowed")
+ verify(historyTrackingDelegate, never()).onVisited(eq("https://www.mozilla.com/not-allowed"), any())
+ }
+
+ @Test
+ fun `correctly processes redirect visit flags`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+ whenever(historyTrackingDelegate.shouldStoreUri(any())).thenReturn(true)
+
+ historyDelegate.value.onVisited(
+ geckoSession,
+ "https://www.mozilla.com/tempredirect",
+ null,
+ // bitwise 'or'
+ GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
+ or GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE,
+ )
+
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/tempredirect"), eq(PageVisit(VisitType.REDIRECT_TEMPORARY, RedirectSource.TEMPORARY)))
+
+ historyDelegate.value.onVisited(
+ geckoSession,
+ "https://www.mozilla.com/permredirect",
+ null,
+ GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
+ or GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE_PERMANENT,
+ )
+
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/permredirect"), eq(PageVisit(VisitType.REDIRECT_PERMANENT, RedirectSource.PERMANENT)))
+
+ // Visits below are targets of redirects, not redirects themselves.
+ // Check that they're mapped to "link".
+ historyDelegate.value.onVisited(
+ geckoSession,
+ "https://www.mozilla.com/targettemp",
+ null,
+ GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
+ or GeckoSession.HistoryDelegate.VISIT_REDIRECT_TEMPORARY,
+ )
+
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/targettemp"), eq(PageVisit(VisitType.LINK)))
+
+ historyDelegate.value.onVisited(
+ geckoSession,
+ "https://www.mozilla.com/targetperm",
+ null,
+ GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL
+ or GeckoSession.HistoryDelegate.VISIT_REDIRECT_PERMANENT,
+ )
+
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).onVisited(eq("https://www.mozilla.com/targetperm"), eq(PageVisit(VisitType.LINK)))
+ }
+
+ @Test
+ fun `does not notify configured history delegate of visits for private sessions`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ privateMode = true,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+
+ historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", "https://www.mozilla.com", GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate, never()).onVisited(anyString(), any())
+ }
+
+ @Test
+ fun `requests visited URLs from configured history delegate`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ // Nothing breaks if history delegate isn't configured.
+ historyDelegate.value.getVisited(geckoSession, arrayOf("https://www.mozilla.com", "https://www.mozilla.org"))
+ engineSession.job.children.forEach { it.join() }
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+
+ historyDelegate.value.getVisited(geckoSession, arrayOf("https://www.mozilla.com", "https://www.mozilla.org"))
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate).getVisited(eq(listOf("https://www.mozilla.com", "https://www.mozilla.org")))
+ }
+
+ @Test
+ fun `does not request visited URLs from configured history delegate in private sessions`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ privateMode = true,
+ )
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ captureDelegates()
+
+ engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
+
+ historyDelegate.value.getVisited(geckoSession, arrayOf("https://www.mozilla.com", "https://www.mozilla.org"))
+ engineSession.job.children.forEach { it.join() }
+ verify(historyTrackingDelegate, never()).getVisited(anyList())
+ }
+
+ @Test
+ fun `notifies configured history delegate of state changes`() = runTestOnMain {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ context = coroutineContext,
+ )
+ val observer = mock<EngineSession.Observer>()
+ engineSession.register(observer)
+
+ captureDelegates()
+
+ class MockHistoryList(
+ items: List<GeckoSession.HistoryDelegate.HistoryItem>,
+ private val currentIndex: Int,
+ ) : ArrayList<GeckoSession.HistoryDelegate.HistoryItem>(items), GeckoSession.HistoryDelegate.HistoryList {
+ override fun getCurrentIndex() = currentIndex
+ }
+
+ fun mockHistoryItem(title: String?, uri: String): GeckoSession.HistoryDelegate.HistoryItem {
+ val item = mock<GeckoSession.HistoryDelegate.HistoryItem>()
+ whenever(item.title).thenReturn(title)
+ whenever(item.uri).thenReturn(uri)
+ return item
+ }
+
+ historyDelegate.value.onHistoryStateChange(mock(), MockHistoryList(emptyList(), 0))
+ verify(observer).onHistoryStateChanged(emptyList(), 0)
+
+ historyDelegate.value.onHistoryStateChange(
+ mock(),
+ MockHistoryList(
+ listOf(
+ mockHistoryItem("Firefox", "https://firefox.com"),
+ mockHistoryItem("Mozilla", "http://mozilla.org"),
+ mockHistoryItem(null, "https://example.com"),
+ ),
+ 1,
+ ),
+ )
+ verify(observer).onHistoryStateChanged(
+ listOf(
+ HistoryItem("Firefox", "https://firefox.com"),
+ HistoryItem("Mozilla", "http://mozilla.org"),
+ HistoryItem("https://example.com", "https://example.com"),
+ ),
+ 1,
+ )
+ }
+
+ @Test
+ fun websiteTitleUpdates() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ captureDelegates()
+
+ contentDelegate.value.onTitleChange(geckoSession, "Hello World!")
+
+ verify(observer).onTitleChange("Hello World!")
+ }
+
+ @Test
+ fun `WHEN preview image URL changes THEN notify observers`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ captureDelegates()
+
+ val previewImageURL = "https://test.com/og-image-url"
+ contentDelegate.value.onPreviewImage(geckoSession, previewImageURL)
+
+ verify(observer).onPreviewImageChange(previewImageURL)
+ }
+
+ @Test
+ fun trackingProtectionDelegateNotifiesObservers() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ var trackerBlocked: Tracker? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onTrackerBlocked(tracker: Tracker) {
+ trackerBlocked = tracker
+ }
+ },
+ )
+
+ captureDelegates()
+ var geckoCategories = 0
+ geckoCategories = geckoCategories.or(GeckoAntiTracking.AD)
+ geckoCategories = geckoCategories.or(GeckoAntiTracking.ANALYTIC)
+ geckoCategories = geckoCategories.or(GeckoAntiTracking.SOCIAL)
+ geckoCategories = geckoCategories.or(GeckoAntiTracking.CRYPTOMINING)
+ geckoCategories = geckoCategories.or(GeckoAntiTracking.FINGERPRINTING)
+ geckoCategories = geckoCategories.or(GeckoAntiTracking.CONTENT)
+ geckoCategories = geckoCategories.or(GeckoAntiTracking.TEST)
+
+ contentBlockingDelegate.value.onContentBlocked(
+ geckoSession,
+ ContentBlocking.BlockEvent("tracker1", geckoCategories, 0, 0, false),
+ )
+
+ assertEquals("tracker1", trackerBlocked!!.url)
+
+ val expectedBlockedCategories = listOf(
+ TrackingCategory.AD,
+ TrackingCategory.ANALYTICS,
+ TrackingCategory.SOCIAL,
+ TrackingCategory.CRYPTOMINING,
+ TrackingCategory.FINGERPRINTING,
+ TrackingCategory.CONTENT,
+ TrackingCategory.TEST,
+ )
+
+ assertTrue(trackerBlocked!!.trackingCategories.containsAll(expectedBlockedCategories))
+
+ var trackerLoaded: Tracker? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onTrackerLoaded(tracker: Tracker) {
+ trackerLoaded = tracker
+ }
+ },
+ )
+
+ var geckoCookieCategories = 0
+ geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_ALL)
+ geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_VISITED)
+ geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_NON_TRACKERS)
+ geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_NONE)
+ geckoCookieCategories = geckoCookieCategories.or(GeckoCookieBehavior.ACCEPT_FIRST_PARTY)
+
+ contentBlockingDelegate.value.onContentLoaded(
+ geckoSession,
+ ContentBlocking.BlockEvent("tracker1", 0, 0, geckoCookieCategories, false),
+ )
+
+ val expectedCookieCategories = listOf(
+ CookiePolicy.ACCEPT_ONLY_FIRST_PARTY,
+ CookiePolicy.ACCEPT_NONE,
+ CookiePolicy.ACCEPT_VISITED,
+ CookiePolicy.ACCEPT_NON_TRACKERS,
+ )
+
+ assertEquals("tracker1", trackerLoaded!!.url)
+ assertTrue(trackerLoaded!!.cookiePolicies.containsAll(expectedCookieCategories))
+
+ contentBlockingDelegate.value.onContentLoaded(
+ geckoSession,
+ ContentBlocking.BlockEvent("tracker1", 0, 0, GeckoCookieBehavior.ACCEPT_ALL, false),
+ )
+
+ assertTrue(
+ trackerLoaded!!.cookiePolicies.containsAll(
+ listOf(
+ CookiePolicy.ACCEPT_ALL,
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN updateing tracking protection with a recommended policy THEN etpEnabled should be enabled`() {
+ whenever(runtime.settings).thenReturn(mock())
+ whenever(runtime.settings.contentBlocking).thenReturn(mock())
+
+ val session = spy(GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider))
+ var trackerBlockingObserved = false
+
+ session.register(
+ object : EngineSession.Observer {
+ override fun onTrackerBlockingEnabledChange(enabled: Boolean) {
+ trackerBlockingObserved = enabled
+ }
+ },
+ )
+
+ val policy = TrackingProtectionPolicy.recommended()
+ session.updateTrackingProtection(policy)
+ shadowOf(getMainLooper()).idle()
+
+ verify(session).updateContentBlocking(policy)
+ assertTrue(session.etpEnabled!!)
+ assertTrue(trackerBlockingObserved)
+ }
+
+ @Test
+ fun `WHEN calling updateTrackingProtection with a none policy THEN etpEnabled should be disabled`() {
+ whenever(runtime.settings).thenReturn(mock())
+ whenever(runtime.settings.contentBlocking).thenReturn(mock())
+
+ val session = spy(GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider))
+ var trackerBlockingObserved = false
+
+ session.register(
+ object : EngineSession.Observer {
+ override fun onTrackerBlockingEnabledChange(enabled: Boolean) {
+ trackerBlockingObserved = enabled
+ }
+ },
+ )
+
+ val policy = TrackingProtectionPolicy.none()
+ session.updateTrackingProtection(policy)
+
+ verify(session).updateContentBlocking(policy)
+ assertFalse(session.etpEnabled!!)
+ assertFalse(trackerBlockingObserved)
+ }
+
+ @Test
+ fun `WHEN updating the contentBlocking with a policy SCRIPTS_AND_SUB_RESOURCES useForPrivateSessions being in privateMode THEN useTrackingProtection should be true`() {
+ val geckoSetting = mock<GeckoSessionSettings>()
+ val geckoSession = mock<GeckoSession>()
+
+ val session = spy(
+ GeckoEngineSession(
+ runtime = runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = true,
+ ),
+ )
+
+ whenever(geckoSession.settings).thenReturn(geckoSetting)
+
+ session.geckoSession = geckoSession
+
+ val policy = TrackingProtectionPolicy.select(trackingCategories = arrayOf(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES)).forPrivateSessionsOnly()
+
+ session.updateContentBlocking(policy)
+
+ verify(geckoSetting).useTrackingProtection = true
+ }
+
+ @Test
+ fun `WHEN calling updateContentBlocking with a policy SCRIPTS_AND_SUB_RESOURCES useForRegularSessions being in privateMode THEN useTrackingProtection should be true`() {
+ val geckoSetting = mock<GeckoSessionSettings>()
+ val geckoSession = mock<GeckoSession>()
+
+ val session = spy(
+ GeckoEngineSession(
+ runtime = runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = false,
+ ),
+ )
+
+ whenever(geckoSession.settings).thenReturn(geckoSetting)
+
+ session.geckoSession = geckoSession
+
+ val policy = TrackingProtectionPolicy.select(trackingCategories = arrayOf(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES)).forRegularSessionsOnly()
+
+ session.updateContentBlocking(policy)
+
+ verify(geckoSetting).useTrackingProtection = true
+ }
+
+ @Test
+ fun `WHEN updating content blocking without a policy SCRIPTS_AND_SUB_RESOURCES for any browsing mode THEN useTrackingProtection should be false`() {
+ val geckoSetting = mock<GeckoSessionSettings>()
+ val geckoSession = mock<GeckoSession>()
+
+ var session = spy(
+ GeckoEngineSession(
+ runtime = runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = false,
+ ),
+ )
+
+ whenever(geckoSession.settings).thenReturn(geckoSetting)
+ session.geckoSession = geckoSession
+
+ val policy = TrackingProtectionPolicy.none()
+
+ session.updateContentBlocking(policy)
+
+ verify(geckoSetting).useTrackingProtection = false
+
+ session = spy(
+ GeckoEngineSession(
+ runtime = runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = true,
+ ),
+ )
+
+ whenever(geckoSession.settings).thenReturn(geckoSetting)
+ session.geckoSession = geckoSession
+
+ session.updateContentBlocking(policy)
+
+ verify(geckoSetting, times(2)).useTrackingProtection = false
+ }
+
+ @Test
+ fun `changes to updateTrackingProtection will be notified to all new observers`() {
+ whenever(runtime.settings).thenReturn(mock())
+ whenever(runtime.settings.contentBlocking).thenReturn(mock())
+ val session = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+ val observers = mutableListOf<EngineSession.Observer>()
+ val policy = TrackingProtectionPolicy.strict()
+
+ for (x in 1..5) {
+ observers.add(spy(object : EngineSession.Observer {}))
+ }
+
+ session.updateTrackingProtection(policy)
+
+ observers.forEach { session.register(it) }
+ shadowOf(getMainLooper()).idle()
+
+ observers.forEach {
+ verify(it).onTrackerBlockingEnabledChange(true)
+ }
+
+ observers.forEach { session.unregister(it) }
+ shadowOf(getMainLooper()).idle()
+
+ session.updateTrackingProtection(TrackingProtectionPolicy.none())
+
+ observers.forEach { session.register(it) }
+ shadowOf(getMainLooper()).idle()
+
+ observers.forEach {
+ verify(it).onTrackerBlockingEnabledChange(false)
+ }
+ }
+
+ @Test
+ fun safeBrowsingCategoriesAreAligned() {
+ assertEquals(GeckoSafeBrowsing.NONE, SafeBrowsingPolicy.NONE.id)
+ assertEquals(GeckoSafeBrowsing.MALWARE, SafeBrowsingPolicy.MALWARE.id)
+ assertEquals(GeckoSafeBrowsing.UNWANTED, SafeBrowsingPolicy.UNWANTED.id)
+ assertEquals(GeckoSafeBrowsing.HARMFUL, SafeBrowsingPolicy.HARMFUL.id)
+ assertEquals(GeckoSafeBrowsing.PHISHING, SafeBrowsingPolicy.PHISHING.id)
+ assertEquals(GeckoSafeBrowsing.DEFAULT, SafeBrowsingPolicy.RECOMMENDED.id)
+ }
+
+ @Test
+ fun trackingProtectionCategoriesAreAligned() {
+ assertEquals(GeckoAntiTracking.NONE, TrackingCategory.NONE.id)
+ assertEquals(GeckoAntiTracking.AD, TrackingCategory.AD.id)
+ assertEquals(GeckoAntiTracking.CONTENT, TrackingCategory.CONTENT.id)
+ assertEquals(GeckoAntiTracking.SOCIAL, TrackingCategory.SOCIAL.id)
+ assertEquals(GeckoAntiTracking.TEST, TrackingCategory.TEST.id)
+ assertEquals(GeckoAntiTracking.CRYPTOMINING, TrackingCategory.CRYPTOMINING.id)
+ assertEquals(GeckoAntiTracking.FINGERPRINTING, TrackingCategory.FINGERPRINTING.id)
+ assertEquals(GeckoAntiTracking.STP, TrackingCategory.MOZILLA_SOCIAL.id)
+ assertEquals(GeckoAntiTracking.EMAIL, TrackingCategory.EMAIL.id)
+
+ assertEquals(GeckoCookieBehavior.ACCEPT_ALL, CookiePolicy.ACCEPT_ALL.id)
+ assertEquals(
+ GeckoCookieBehavior.ACCEPT_NON_TRACKERS,
+ CookiePolicy.ACCEPT_NON_TRACKERS.id,
+ )
+ assertEquals(GeckoCookieBehavior.ACCEPT_NONE, CookiePolicy.ACCEPT_NONE.id)
+ assertEquals(
+ GeckoCookieBehavior.ACCEPT_FIRST_PARTY,
+ CookiePolicy.ACCEPT_ONLY_FIRST_PARTY.id,
+
+ )
+ assertEquals(GeckoCookieBehavior.ACCEPT_VISITED, CookiePolicy.ACCEPT_VISITED.id)
+ }
+
+ @Test
+ fun settingTestingMode() {
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = DefaultSettings(),
+ )
+ verify(geckoSession.settings).fullAccessibilityTree = false
+
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = DefaultSettings(testingModeEnabled = true),
+ )
+ verify(geckoSession.settings).fullAccessibilityTree = true
+ }
+
+ @Test
+ fun settingUserAgent() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+ engineSession.settings.userAgentString
+
+ verify(geckoSession.settings).userAgentOverride
+
+ engineSession.settings.userAgentString = "test-ua"
+
+ verify(geckoSession.settings).userAgentOverride = "test-ua"
+ }
+
+ @Test
+ fun settingUserAgentDefault() {
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = DefaultSettings(userAgentString = "test-ua"),
+ )
+
+ verify(geckoSession.settings).userAgentOverride = "test-ua"
+ }
+
+ @Test
+ fun settingSuspendMediaWhenInactive() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+ verify(geckoSession.settings, never()).suspendMediaWhenInactive = anyBoolean()
+
+ assertFalse(engineSession.settings.suspendMediaWhenInactive)
+ verify(geckoSession.settings).suspendMediaWhenInactive
+
+ engineSession.settings.suspendMediaWhenInactive = true
+ verify(geckoSession.settings).suspendMediaWhenInactive = true
+ }
+
+ @Test
+ fun settingSuspendMediaWhenInactiveDefault() {
+ GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+ verify(geckoSession.settings, never()).suspendMediaWhenInactive = anyBoolean()
+
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = DefaultSettings(),
+ )
+ verify(geckoSession.settings).suspendMediaWhenInactive = false
+
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = DefaultSettings(suspendMediaWhenInactive = true),
+ )
+ verify(geckoSession.settings).suspendMediaWhenInactive = true
+ }
+
+ @Test
+ fun settingClearColorDefault() {
+ whenever(geckoSession.compositorController).thenReturn(mock())
+
+ GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ verify(geckoSession.compositorController, never()).clearColor = anyInt()
+
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = DefaultSettings(),
+ )
+ verify(geckoSession.compositorController, never()).clearColor = anyInt()
+
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = DefaultSettings(clearColor = Color.BLUE),
+ )
+ verify(geckoSession.compositorController).clearColor = Color.BLUE
+ }
+
+ @Test
+ fun unsupportedSettings() {
+ val settings = GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ ).settings
+
+ expectException(UnsupportedSettingException::class) {
+ settings.javascriptEnabled = true
+ }
+
+ expectException(UnsupportedSettingException::class) {
+ settings.domStorageEnabled = false
+ }
+
+ expectException(UnsupportedSettingException::class) {
+ settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+ }
+ }
+
+ @Test
+ fun settingInterceptorToProvideAlternativeContent() {
+ var interceptorCalledWithUri: String? = null
+
+ val interceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ interceptorCalledWithUri = uri
+ return RequestInterceptor.InterceptionResponse.Content("<h1>Hello World</h1>")
+ }
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = interceptor)
+ GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider, defaultSettings = defaultSettings)
+ captureDelegates()
+
+ navigationDelegate.value.onLoadRequest(geckoSession, mockLoadRequest("sample:about"))
+
+ assertEquals("sample:about", interceptorCalledWithUri)
+ verify(geckoSession).load(
+ GeckoSession.Loader().data("<h1>Hello World</h1>", "text/html"),
+ )
+ }
+
+ @Test
+ fun settingInterceptorToProvideAlternativeUrl() {
+ var interceptorCalledWithUri: String? = null
+
+ val interceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ interceptorCalledWithUri = uri
+ return RequestInterceptor.InterceptionResponse.Url("https://mozilla.org")
+ }
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = interceptor)
+ GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider, defaultSettings = defaultSettings)
+ captureDelegates()
+
+ navigationDelegate.value.onLoadRequest(geckoSession, mockLoadRequest("sample:about", "trigger:uri"))
+
+ assertEquals("sample:about", interceptorCalledWithUri)
+ verify(geckoSession).load(
+ GeckoSession.Loader().uri("https://mozilla.org").flags(EXTERNAL + LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE),
+ )
+ }
+
+ @Test
+ fun settingInterceptorCanIgnoreAppInitiatedRequests() {
+ var interceptorCalled = false
+
+ val interceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = false
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ interceptorCalled = true
+ return RequestInterceptor.InterceptionResponse.Url("https://mozilla.org")
+ }
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = interceptor)
+ GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider, defaultSettings = defaultSettings)
+ captureDelegates()
+
+ navigationDelegate.value.onLoadRequest(geckoSession, mockLoadRequest("sample:about", isDirectNavigation = true))
+ assertFalse(interceptorCalled)
+
+ navigationDelegate.value.onLoadRequest(geckoSession, mockLoadRequest("sample:about", isDirectNavigation = false))
+ assertTrue(interceptorCalled)
+ }
+
+ @Test
+ fun onLoadRequestWithoutInterceptor() {
+ val defaultSettings = DefaultSettings()
+
+ GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = defaultSettings,
+ )
+
+ captureDelegates()
+
+ navigationDelegate.value.onLoadRequest(geckoSession, mockLoadRequest("sample:about"))
+
+ verify(geckoSession, never()).load(any())
+ }
+
+ @Test
+ fun onLoadRequestWithInterceptorThatDoesNotIntercept() {
+ var interceptorCalledWithUri: String? = null
+
+ val interceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ interceptorCalledWithUri = uri
+ return null
+ }
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = interceptor)
+
+ GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = defaultSettings,
+ )
+
+ captureDelegates()
+
+ navigationDelegate.value.onLoadRequest(geckoSession, mockLoadRequest("sample:about"))
+
+ assertEquals("sample:about", interceptorCalledWithUri!!)
+ verify(geckoSession, never()).load(any())
+ }
+
+ @Test
+ fun onLoadErrorCallsInterceptorWithNull() {
+ var interceptedUri: String? = null
+ val requestInterceptor: RequestInterceptor = mock()
+ var defaultSettings = DefaultSettings()
+ var engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = defaultSettings,
+ )
+
+ captureDelegates()
+
+ // Interceptor is not called when there is none attached.
+ var onLoadError = navigationDelegate.value.onLoadError(
+ geckoSession,
+ "",
+ WebRequestError(
+ ERROR_CATEGORY_UNKNOWN,
+ ERROR_UNKNOWN,
+ ),
+ )
+ verify(requestInterceptor, never()).onErrorRequest(engineSession, ErrorType.UNKNOWN, "")
+ onLoadError!!.then { value: String? ->
+ interceptedUri = value
+ GeckoResult.fromValue(null)
+ }
+ assertNull(interceptedUri)
+
+ // Interceptor is called correctly
+ defaultSettings = DefaultSettings(requestInterceptor = requestInterceptor)
+ geckoSession = mockGeckoSession()
+ engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = defaultSettings,
+ )
+
+ captureDelegates()
+
+ onLoadError = navigationDelegate.value.onLoadError(
+ geckoSession,
+ "",
+ WebRequestError(
+ ERROR_CATEGORY_UNKNOWN,
+ ERROR_UNKNOWN,
+ ),
+ )
+
+ verify(requestInterceptor).onErrorRequest(engineSession, ErrorType.UNKNOWN, "")
+ onLoadError!!.then { value: String? ->
+ interceptedUri = value
+ GeckoResult.fromValue(null)
+ }
+ assertNull(interceptedUri)
+ }
+
+ @Test
+ fun onLoadErrorCallsInterceptorWithErrorPage() {
+ val requestInterceptor: RequestInterceptor = object : RequestInterceptor {
+ override fun onErrorRequest(
+ session: EngineSession,
+ errorType: ErrorType,
+ uri: String?,
+ ): RequestInterceptor.ErrorResponse? =
+ RequestInterceptor.ErrorResponse("nonNullData")
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = requestInterceptor)
+ GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ defaultSettings = defaultSettings,
+ )
+
+ captureDelegates()
+
+ val onLoadError = navigationDelegate.value.onLoadError(
+ geckoSession,
+ "about:failed",
+ WebRequestError(
+ ERROR_CATEGORY_UNKNOWN,
+ ERROR_UNKNOWN,
+ ),
+ )
+
+ onLoadError!!.then { value: String? ->
+ GeckoResult.fromValue(value)
+ }
+ }
+
+ @Test
+ fun onLoadErrorCallsInterceptorWithInvalidUri() {
+ val requestInterceptor: RequestInterceptor = mock()
+ val defaultSettings = DefaultSettings(requestInterceptor = requestInterceptor)
+ val engineSession = GeckoEngineSession(runtime, defaultSettings = defaultSettings)
+
+ engineSession.geckoSession.navigationDelegate!!.onLoadError(
+ engineSession.geckoSession,
+ null,
+ WebRequestError(ERROR_MALFORMED_URI, ERROR_CATEGORY_UNKNOWN),
+ )
+ verify(requestInterceptor).onErrorRequest(engineSession, ErrorType.ERROR_MALFORMED_URI, null)
+ }
+
+ @Test
+ fun geckoErrorMappingToErrorType() {
+ assertEquals(
+ ErrorType.ERROR_SECURITY_SSL,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_SECURITY_SSL),
+ )
+ assertEquals(
+ ErrorType.ERROR_SECURITY_BAD_CERT,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_SECURITY_BAD_CERT),
+ )
+ assertEquals(
+ ErrorType.ERROR_NET_INTERRUPT,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_NET_INTERRUPT),
+ )
+ assertEquals(
+ ErrorType.ERROR_NET_TIMEOUT,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_NET_TIMEOUT),
+ )
+ assertEquals(
+ ErrorType.ERROR_NET_RESET,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_NET_RESET),
+ )
+ assertEquals(
+ ErrorType.ERROR_CONNECTION_REFUSED,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_CONNECTION_REFUSED),
+ )
+ assertEquals(
+ ErrorType.ERROR_UNKNOWN_SOCKET_TYPE,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_UNKNOWN_SOCKET_TYPE),
+ )
+ assertEquals(
+ ErrorType.ERROR_REDIRECT_LOOP,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_REDIRECT_LOOP),
+ )
+ assertEquals(
+ ErrorType.ERROR_OFFLINE,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_OFFLINE),
+ )
+ assertEquals(
+ ErrorType.ERROR_PORT_BLOCKED,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_PORT_BLOCKED),
+ )
+ assertEquals(
+ ErrorType.ERROR_UNSAFE_CONTENT_TYPE,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_UNSAFE_CONTENT_TYPE),
+ )
+ assertEquals(
+ ErrorType.ERROR_CORRUPTED_CONTENT,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_CORRUPTED_CONTENT),
+ )
+ assertEquals(
+ ErrorType.ERROR_CONTENT_CRASHED,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_CONTENT_CRASHED),
+ )
+ assertEquals(
+ ErrorType.ERROR_INVALID_CONTENT_ENCODING,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_INVALID_CONTENT_ENCODING),
+ )
+ assertEquals(
+ ErrorType.ERROR_UNKNOWN_HOST,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_UNKNOWN_HOST),
+ )
+ assertEquals(
+ ErrorType.ERROR_MALFORMED_URI,
+ GeckoEngineSession.geckoErrorToErrorType(ERROR_MALFORMED_URI),
+ )
+ assertEquals(
+ ErrorType.ERROR_UNKNOWN_PROTOCOL,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_UNKNOWN_PROTOCOL),
+ )
+ assertEquals(
+ ErrorType.ERROR_FILE_NOT_FOUND,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_FILE_NOT_FOUND),
+ )
+ assertEquals(
+ ErrorType.ERROR_FILE_ACCESS_DENIED,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_FILE_ACCESS_DENIED),
+ )
+ assertEquals(
+ ErrorType.ERROR_PROXY_CONNECTION_REFUSED,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_PROXY_CONNECTION_REFUSED),
+ )
+ assertEquals(
+ ErrorType.ERROR_UNKNOWN_PROXY_HOST,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_UNKNOWN_PROXY_HOST),
+ )
+ assertEquals(
+ ErrorType.ERROR_SAFEBROWSING_MALWARE_URI,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_SAFEBROWSING_MALWARE_URI),
+ )
+ assertEquals(
+ ErrorType.ERROR_SAFEBROWSING_HARMFUL_URI,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_SAFEBROWSING_HARMFUL_URI),
+ )
+ assertEquals(
+ ErrorType.ERROR_SAFEBROWSING_PHISHING_URI,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_SAFEBROWSING_PHISHING_URI),
+ )
+ assertEquals(
+ ErrorType.ERROR_SAFEBROWSING_UNWANTED_URI,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_SAFEBROWSING_UNWANTED_URI),
+ )
+ assertEquals(
+ ErrorType.UNKNOWN,
+ GeckoEngineSession.geckoErrorToErrorType(-500),
+ )
+ assertEquals(
+ ErrorType.ERROR_HTTPS_ONLY,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_HTTPS_ONLY),
+ )
+ assertEquals(
+ ErrorType.ERROR_BAD_HSTS_CERT,
+ GeckoEngineSession.geckoErrorToErrorType(WebRequestError.ERROR_BAD_HSTS_CERT),
+ )
+ }
+
+ @Test
+ fun defaultSettings() {
+ val runtime = mock<GeckoRuntime>()
+ whenever(runtime.settings).thenReturn(mock())
+
+ val defaultSettings =
+ DefaultSettings(trackingProtectionPolicy = TrackingProtectionPolicy.strict())
+
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = false,
+ defaultSettings = defaultSettings,
+ )
+
+ assertFalse(geckoSession.settings.usePrivateMode)
+ verify(geckoSession.settings).useTrackingProtection = true
+ }
+
+ @Test
+ fun `WHEN TrackingCategory do not includes content then useTrackingProtection must be set to false`() {
+ val defaultSettings =
+ DefaultSettings(trackingProtectionPolicy = TrackingProtectionPolicy.recommended())
+
+ GeckoEngineSession(
+ runtime,
+ geckoSessionProvider = geckoSessionProvider,
+ privateMode = false,
+ defaultSettings = defaultSettings,
+ )
+
+ verify(geckoSession.settings).useTrackingProtection = false
+ }
+
+ @Test
+ fun contentDelegate() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ val delegate = engineSession.createContentDelegate()
+
+ var observedChanged = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLongPress(hitResult: HitResult) {
+ observedChanged = true
+ }
+ },
+ )
+
+ class MockContextElement(
+ baseUri: String?,
+ linkUri: String?,
+ title: String?,
+ altText: String?,
+ typeStr: String,
+ srcUri: String?,
+ ) : GeckoSession.ContentDelegate.ContextElement(baseUri, linkUri, title, altText, typeStr, srcUri)
+
+ delegate.onContextMenu(
+ geckoSession,
+ 0,
+ 0,
+ MockContextElement(null, null, "title", "alt", "HTMLAudioElement", "file.mp3"),
+ )
+ assertTrue(observedChanged)
+
+ observedChanged = false
+ delegate.onContextMenu(
+ geckoSession,
+ 0,
+ 0,
+ MockContextElement(null, null, "title", "alt", "HTMLAudioElement", null),
+ )
+ assertFalse(observedChanged)
+
+ observedChanged = false
+ delegate.onContextMenu(
+ geckoSession,
+ 0,
+ 0,
+ MockContextElement(null, null, "title", "alt", "foobar", null),
+ )
+ assertFalse(observedChanged)
+ }
+
+ @Test
+ fun contentDelegateCookieBanner() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ val delegate = engineSession.createContentDelegate()
+
+ var cookieBannerStatus: CookieBannerHandlingStatus? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onCookieBannerChange(status: CookieBannerHandlingStatus) {
+ cookieBannerStatus = status
+ }
+ },
+ )
+
+ delegate.onCookieBannerDetected(geckoSession)
+
+ assertNotNull(cookieBannerStatus)
+ assertEquals(CookieBannerHandlingStatus.DETECTED, cookieBannerStatus)
+
+ cookieBannerStatus = null
+
+ delegate.onCookieBannerHandled(geckoSession)
+
+ assertNotNull(cookieBannerStatus)
+ assertEquals(CookieBannerHandlingStatus.HANDLED, cookieBannerStatus)
+ }
+
+ @Test
+ fun handleLongClick() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ var result = engineSession.handleLongClick("file.mp3", TYPE_AUDIO)
+ assertNotNull(result)
+ assertTrue(result is HitResult.AUDIO && result.src == "file.mp3")
+
+ result = engineSession.handleLongClick("file.mp4", TYPE_VIDEO)
+ assertNotNull(result)
+ assertTrue(result is HitResult.VIDEO && result.src == "file.mp4")
+
+ result = engineSession.handleLongClick("file.png", TYPE_IMAGE)
+ assertNotNull(result)
+ assertTrue(result is HitResult.IMAGE && result.src == "file.png")
+
+ result = engineSession.handleLongClick("file.png", TYPE_IMAGE, "https://mozilla.org")
+ assertNotNull(result)
+ assertTrue(result is HitResult.IMAGE_SRC && result.src == "file.png" && result.uri == "https://mozilla.org")
+
+ result = engineSession.handleLongClick(null, TYPE_IMAGE)
+ assertNotNull(result)
+ assertTrue(result is HitResult.UNKNOWN && result.src == "")
+
+ result = engineSession.handleLongClick("tel:+1234567890", TYPE_NONE)
+ assertNotNull(result)
+ assertTrue(result is HitResult.PHONE && result.src == "tel:+1234567890")
+
+ result = engineSession.handleLongClick("geo:1,-1", TYPE_NONE)
+ assertNotNull(result)
+ assertTrue(result is HitResult.GEO && result.src == "geo:1,-1")
+
+ result = engineSession.handleLongClick("mailto:asa@mozilla.com", TYPE_NONE)
+ assertNotNull(result)
+ assertTrue(result is HitResult.EMAIL && result.src == "mailto:asa@mozilla.com")
+
+ result = engineSession.handleLongClick(null, TYPE_NONE, "https://mozilla.org")
+ assertNotNull(result)
+ assertTrue(result is HitResult.UNKNOWN && result.src == "https://mozilla.org")
+
+ result = engineSession.handleLongClick("data://foobar", TYPE_NONE, "https://mozilla.org")
+ assertNotNull(result)
+ assertTrue(result is HitResult.UNKNOWN && result.src == "data://foobar")
+
+ result = engineSession.handleLongClick(null, TYPE_NONE, null)
+ assertNull(result)
+ }
+
+ @Test
+ fun setDesktopMode() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ var desktopModeToggled = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onDesktopModeChange(enabled: Boolean) {
+ desktopModeToggled = true
+ }
+ },
+ )
+ engineSession.toggleDesktopMode(true)
+ assertTrue(desktopModeToggled)
+
+ desktopModeToggled = false
+ whenever(geckoSession.settings.userAgentMode)
+ .thenReturn(GeckoSessionSettings.USER_AGENT_MODE_DESKTOP)
+ whenever(geckoSession.settings.viewportMode)
+ .thenReturn(GeckoSessionSettings.VIEWPORT_MODE_DESKTOP)
+
+ engineSession.toggleDesktopMode(true)
+ assertFalse(desktopModeToggled)
+
+ engineSession.toggleDesktopMode(true)
+ assertFalse(desktopModeToggled)
+
+ engineSession.toggleDesktopMode(false)
+ assertTrue(desktopModeToggled)
+ }
+
+ @Test
+ fun `toggleDesktopMode should reload a non-mobile url when set to desktop mode`() {
+ val mobileUrl = "https://m.example.com"
+ val nonMobileUrl = "https://example.com"
+ val engineSession = spy(GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider))
+ engineSession.currentUrl = mobileUrl
+
+ engineSession.toggleDesktopMode(true, reload = true)
+ verify(engineSession, atLeastOnce()).loadUrl(nonMobileUrl, null, LoadUrlFlags.select(LoadUrlFlags.LOAD_FLAGS_REPLACE_HISTORY), null)
+
+ engineSession.toggleDesktopMode(false, reload = true)
+ verify(engineSession, atLeastOnce()).reload()
+ }
+
+ @Test
+ fun `hasCookieBannerRuleForSession should call onSuccess callback for a valid GV response`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<Boolean>()
+ whenever(geckoSession.hasCookieBannerRuleForBrowsingContextTree()).thenReturn(ruleResult)
+
+ engineSession.hasCookieBannerRuleForSession(
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ ruleResult.complete(true)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `hasCookieBannerRuleForSession should call onError callback in case GV returns an exception`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<Boolean>()
+ whenever(geckoSession.hasCookieBannerRuleForBrowsingContextTree()).thenReturn(ruleResult)
+
+ engineSession.hasCookieBannerRuleForSession(
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ ruleResult.completeExceptionally(IOException())
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onExceptionCalled)
+ assertFalse(onResultCalled)
+ }
+
+ @Test
+ fun `hasCookieBannerRuleForSession should call onError callback in case GV returns a null`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<Boolean>()
+ whenever(geckoSession.hasCookieBannerRuleForBrowsingContextTree()).thenReturn(ruleResult)
+
+ engineSession.hasCookieBannerRuleForSession(
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ ruleResult.complete(null)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onExceptionCalled)
+ assertFalse(onResultCalled)
+ }
+
+ @Test
+ fun `checkForPdfViewer should correctly process a GV response`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<Boolean>()
+ whenever(geckoSession.isPdfJs).thenReturn(ruleResult)
+
+ engineSession.checkForPdfViewer(
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ ruleResult.complete(true)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session onProductUrlChange is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val delegate = engineSession.createContentDelegate()
+ var productUrlStatus = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onProductUrlChange(isProductUrl: Boolean) {
+ productUrlStatus = isProductUrl
+ }
+ },
+ )
+
+ delegate.onProductUrl(geckoSession)
+
+ assertTrue(productUrlStatus)
+ assertEquals(true, productUrlStatus)
+ }
+
+ @Test
+ fun `WHEN session requestProductAnalysis is successful with analysis object THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<GeckoSession.ReviewAnalysis>()
+ whenever(geckoSession.requestAnalysis("mozilla.com")).thenReturn(ruleResult)
+
+ engineSession.requestProductAnalysis(
+ "mozilla.com",
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ val productId = "banana"
+ val grade = "A"
+ val adjustedRating = 4.5
+ val lastAnalysisTime = 12345.toLong()
+ val analysisURL = "https://analysis.com"
+ val analysisObject = GeckoSession.ReviewAnalysis.Builder(productId)
+ .grade(grade)
+ .adjustedRating(adjustedRating)
+ .analysisUrl(analysisURL)
+ .needsAnalysis(true)
+ .pageNotSupported(false)
+ .notEnoughReviews(false)
+ .highlights(null)
+ .lastAnalysisTime(lastAnalysisTime)
+ .deletedProductReported(true)
+ .deletedProduct(true)
+ .build()
+
+ ruleResult.complete(analysisObject)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN requestProductAnalysis is not successful THEN onException callback for error is called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<GeckoSession.ReviewAnalysis>()
+ whenever(geckoSession.requestAnalysis("mozilla.com")).thenReturn(ruleResult)
+
+ engineSession.requestProductAnalysis(
+ "mozilla.com",
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ ruleResult.completeExceptionally(IOException())
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onResultCalled)
+ assertTrue(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session requestProductRecommendations is successful with empty list THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<List<GeckoSession.Recommendation>>()
+ whenever(geckoSession.requestRecommendations("mozilla.com")).thenReturn(ruleResult)
+
+ engineSession.requestProductRecommendations(
+ "mozilla.com",
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ ruleResult.complete(emptyList())
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session requestProductRecommendations is successful with Recommendation THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<List<GeckoSession.Recommendation>>()
+ whenever(geckoSession.requestRecommendations("mozilla.com")).thenReturn(ruleResult)
+
+ engineSession.requestProductRecommendations(
+ "mozilla.com",
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ val recommendationUrl = "https://recommendation.com"
+ val adjustedRating = 3.5
+ val imageUrl = "http://image.com"
+ val aid = "banana"
+ val name = "apple"
+ val grade = "C"
+ val price = "450"
+ val currency = "USD"
+
+ val recommendationObject = GeckoSession.Recommendation.Builder(recommendationUrl)
+ .adjustedRating(adjustedRating)
+ .sponsored(true)
+ .imageUrl(imageUrl)
+ .aid(aid)
+ .name(name)
+ .grade(grade)
+ .price(price)
+ .currency(currency)
+ .build()
+
+ ruleResult.complete(listOf(recommendationObject))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN requestProductRecommendations is not successful THEN onException callback for error is called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val ruleResult = GeckoResult<List<GeckoSession.Recommendation>>()
+ whenever(geckoSession.requestRecommendations("mozilla.com")).thenReturn(ruleResult)
+
+ engineSession.requestProductRecommendations(
+ "mozilla.com",
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ ruleResult.completeExceptionally(IOException())
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onResultCalled)
+ assertTrue(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session reanalyzeProduct is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val mUrl = "https://m.example.com"
+ val geckoResult = GeckoResult<String?>()
+ geckoResult.complete("COMPLETED")
+ whenever(geckoSession.requestCreateAnalysis(mUrl))
+ .thenReturn(geckoResult)
+
+ engineSession.reanalyzeProduct(
+ mUrl,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session requestAnalysisStatus is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val mUrl = "https://m.example.com"
+ val geckoResult = GeckoResult<GeckoSession.AnalysisStatusResponse>()
+
+ val status = "in_progress"
+ val progress = 90.9
+ val analysisObject = GeckoSession.AnalysisStatusResponse.Builder(status)
+ .progress(progress)
+ .build()
+
+ geckoResult.complete(analysisObject)
+ whenever(geckoSession.requestAnalysisStatus(mUrl))
+ .thenReturn(geckoResult)
+
+ engineSession.requestAnalysisStatus(
+ mUrl,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session sendClickAttributionEvent is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val geckoResult = GeckoResult<Boolean?>()
+ geckoResult.complete(true)
+ whenever(geckoSession.sendClickAttributionEvent(AID))
+ .thenReturn(geckoResult)
+
+ engineSession.sendClickAttributionEvent(
+ aid = AID,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session sendClickAttributionEvent is not successful THEN onException callback for error is called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val geckoResult = GeckoResult<Boolean?>()
+ whenever(geckoSession.sendClickAttributionEvent(AID))
+ .thenReturn(geckoResult)
+
+ engineSession.sendClickAttributionEvent(
+ aid = AID,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ geckoResult.completeExceptionally(IOException())
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onResultCalled)
+ assertTrue(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session sendImpressionAttributionEvent is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val geckoResult = GeckoResult<Boolean?>()
+ geckoResult.complete(true)
+ whenever(geckoSession.sendImpressionAttributionEvent(AID))
+ .thenReturn(geckoResult)
+
+ engineSession.sendImpressionAttributionEvent(
+ aid = AID,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session sendImpressionAttributionEvent is not successful THEN onException callback for error is called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val geckoResult = GeckoResult<Boolean?>()
+ whenever(geckoSession.sendImpressionAttributionEvent(AID))
+ .thenReturn(geckoResult)
+
+ engineSession.sendImpressionAttributionEvent(
+ aid = AID,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ geckoResult.completeExceptionally(IOException())
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onResultCalled)
+ assertTrue(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session sendPlacementAttributionEvent is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val geckoResult = GeckoResult<Boolean?>()
+ geckoResult.complete(true)
+ whenever(geckoSession.sendPlacementAttributionEvent(AID))
+ .thenReturn(geckoResult)
+
+ engineSession.sendPlacementAttributionEvent(
+ aid = AID,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session sendPlacementAttributionEvent is not successful THEN onException callback for error is called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ val geckoResult = GeckoResult<Boolean?>()
+ whenever(geckoSession.sendPlacementAttributionEvent(AID))
+ .thenReturn(geckoResult)
+
+ engineSession.sendPlacementAttributionEvent(
+ aid = AID,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ geckoResult.completeExceptionally(IOException())
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onResultCalled)
+ assertTrue(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session requestTranslate is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val mockedGeckoController: TranslationsController.SessionTranslation = mock()
+
+ val geckoResult = GeckoResult<Void>()
+ val fromLanguage = "en"
+ val toLanguage = "es"
+ val options = null
+
+ geckoResult.complete(null)
+ whenever(geckoSession.sessionTranslation).thenReturn(mockedGeckoController)
+ whenever(geckoSession.sessionTranslation!!.translate(fromLanguage, toLanguage, options)).thenReturn(geckoResult)
+
+ engineSession.register(object : EngineSession.Observer {
+ override fun onTranslateComplete(operation: TranslationOperation) {
+ assert(true) { "We should notify of a successful translation." }
+ }
+
+ override fun onTranslateException(
+ operation: TranslationOperation,
+ translationError: TranslationError,
+ ) {
+ assert(false) { "We should not notify of a failure." }
+ }
+ })
+
+ engineSession.requestTranslate(
+ fromLanguage = fromLanguage,
+ toLanguage = toLanguage,
+ options = options,
+ )
+
+ shadowOf(getMainLooper()).idle()
+ }
+
+ @Test
+ fun `WHEN session requestTranslationRestore is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val mockedGeckoController: TranslationsController.SessionTranslation = mock()
+
+ val geckoResult = GeckoResult<Void>()
+ geckoResult.complete(null)
+ whenever(geckoSession.sessionTranslation).thenReturn(mockedGeckoController)
+ whenever(geckoSession.sessionTranslation!!.restoreOriginalPage()).thenReturn(geckoResult)
+
+ engineSession.register(object : EngineSession.Observer {
+ override fun onTranslateComplete(operation: TranslationOperation) {
+ assert(true) { "We should notify of a successful translation." }
+ }
+ override fun onTranslateException(
+ operation: TranslationOperation,
+ translationError: TranslationError,
+ ) {
+ assert(false) { "We should not notify of a failure." }
+ }
+ })
+
+ engineSession.requestTranslationRestore()
+
+ shadowOf(getMainLooper()).idle()
+ }
+
+ @Test
+ fun `WHEN session requestTranslate is unsuccessful THEN notify of failure`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val mockedGeckoController: TranslationsController.SessionTranslation = mock()
+
+ val geckoResult = GeckoResult<Void>()
+ val fromLanguage = "en"
+ val toLanguage = "es"
+ val options = null
+
+ geckoResult.completeExceptionally(Exception())
+ whenever(geckoSession.sessionTranslation).thenReturn(mockedGeckoController)
+ whenever(geckoSession.sessionTranslation!!.translate(fromLanguage, toLanguage, options)).thenReturn(geckoResult)
+
+ engineSession.register(object : EngineSession.Observer {
+ override fun onTranslateComplete(operation: TranslationOperation) {
+ assert(false) { "We should not notify of a successful translation." }
+ }
+
+ override fun onTranslateException(
+ operation: TranslationOperation,
+ translationError: TranslationError,
+ ) {
+ assert(true) { "We should notify of a failure." }
+ }
+ })
+
+ engineSession.requestTranslate(
+ fromLanguage = fromLanguage,
+ toLanguage = toLanguage,
+ options = options,
+ )
+
+ shadowOf(getMainLooper()).idle()
+ }
+
+ @Test
+ fun `WHEN session requestTranslationRestore is unsuccessful THEN notify of failure`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val mockedGeckoController: TranslationsController.SessionTranslation = mock()
+
+ val geckoResult = GeckoResult<Void>()
+ geckoResult.completeExceptionally(Exception())
+ whenever(geckoSession.sessionTranslation).thenReturn(mockedGeckoController)
+ whenever(geckoSession.sessionTranslation!!.restoreOriginalPage()).thenReturn(geckoResult)
+
+ engineSession.register(object : EngineSession.Observer {
+ override fun onTranslateComplete(operation: TranslationOperation) {
+ assert(false) { "We should not notify of a successful translation." }
+ }
+ override fun onTranslateException(
+ operation: TranslationOperation,
+ translationError: TranslationError,
+ ) {
+ assert(true) { "We should notify of a failure." }
+ }
+ })
+
+ engineSession.requestTranslationRestore()
+
+ shadowOf(getMainLooper()).idle()
+ }
+
+ @Test
+ fun `WHEN session getNeverTranslateSiteSetting is successful THEN onResult should be called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val mockedGeckoController: TranslationsController.SessionTranslation = mock()
+
+ val geckoResult = GeckoResult<Boolean>()
+
+ whenever(geckoSession.sessionTranslation).thenReturn(mockedGeckoController)
+ whenever(geckoSession.sessionTranslation!!.neverTranslateSiteSetting).thenReturn(geckoResult)
+
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ engineSession.getNeverTranslateSiteSetting(
+ onResult = {
+ onResultCalled = true
+ assertTrue(it)
+ },
+ onException = { onExceptionCalled = true },
+ )
+
+ geckoResult.complete(true)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session getNeverTranslateSiteSetting has an error THEN onException should be called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val mockedGeckoController: TranslationsController.SessionTranslation = mock()
+
+ val geckoResult = GeckoResult<Boolean>()
+
+ whenever(geckoSession.sessionTranslation).thenReturn(mockedGeckoController)
+ whenever(geckoSession.sessionTranslation!!.neverTranslateSiteSetting).thenReturn(geckoResult)
+
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ engineSession.getNeverTranslateSiteSetting(
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onResultCalled)
+ assertTrue(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session setNeverTranslateSiteSetting is successful THEN onResult should be called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val mockedGeckoController: TranslationsController.SessionTranslation = mock()
+
+ val geckoResult = GeckoResult<Void>()
+
+ whenever(geckoSession.sessionTranslation).thenReturn(mockedGeckoController)
+ whenever(geckoSession.sessionTranslation!!.setNeverTranslateSiteSetting(any())).thenReturn(geckoResult)
+
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ engineSession.setNeverTranslateSiteSetting(
+ true,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ geckoResult.complete(null)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onResultCalled)
+ assertFalse(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN session setNeverTranslateSiteSetting has an error THEN onException should be called`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ val mockedGeckoController: TranslationsController.SessionTranslation = mock()
+
+ val geckoResult = GeckoResult<Void>()
+
+ whenever(geckoSession.sessionTranslation).thenReturn(mockedGeckoController)
+ whenever(geckoSession.sessionTranslation!!.setNeverTranslateSiteSetting(any())).thenReturn(geckoResult)
+
+ var onResultCalled = false
+ var onExceptionCalled = false
+
+ engineSession.setNeverTranslateSiteSetting(
+ true,
+ onResult = { onResultCalled = true },
+ onException = { onExceptionCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onResultCalled)
+ assertTrue(onExceptionCalled)
+ }
+
+ @Test
+ fun `WHEN mapping a Gecko TranslationsException THEN it maps as expected to a TranslationError`() {
+ // Specifically defined unknown error thrown by the translations engine
+ val geckoUnknownError = TranslationsException(TranslationsException.ERROR_UNKNOWN)
+ val unknownError = geckoUnknownError.intoTranslationError()
+ assertTrue(
+ unknownError is TranslationError.UnknownError,
+ )
+ assertEquals(
+ (unknownError as TranslationError.UnknownError).cause,
+ geckoUnknownError,
+ )
+ assertEquals(
+ (unknownError as Throwable).cause,
+ geckoUnknownError,
+ )
+ assertEquals(
+ unknownError.errorName,
+ "unknown",
+ )
+ assertEquals(
+ unknownError.displayError,
+ false,
+ )
+
+ // Something really unexpected was thrown
+ val unexpectedUnknownError = Exception("Something very unexpected")
+ val unexpectedUnknown = unexpectedUnknownError.intoTranslationError()
+ assertTrue(
+ unexpectedUnknown is
+ TranslationError.UnknownError,
+ )
+ assertEquals(
+ (unexpectedUnknown as TranslationError.UnknownError).cause,
+ unexpectedUnknownError,
+ )
+ assertEquals(
+ unexpectedUnknown.errorName,
+ "unknown",
+ )
+ assertEquals(
+ unexpectedUnknown.displayError,
+ false,
+ )
+
+ // For manual use as a guard for when the API returns a null value and it shouldn't be
+ // possible
+ val unexpectedNullError = TranslationError.UnexpectedNull()
+ assertEquals(
+ unexpectedNullError.errorName,
+ "unexpected-null",
+ )
+ assertEquals(
+ unexpectedNullError.displayError,
+ false,
+ )
+
+ // For manual use as a guard for when the engine is missing a session coordinator
+ val missingCoordinator = TranslationError.MissingSessionCoordinator()
+ assertEquals(
+ missingCoordinator.errorName,
+ "missing-session-coordinator",
+ )
+ assertEquals(
+ missingCoordinator.displayError,
+ false,
+ )
+
+ val notSupported =
+ TranslationsException(TranslationsException.ERROR_ENGINE_NOT_SUPPORTED).intoTranslationError()
+ assertTrue(
+ notSupported is
+ TranslationError.EngineNotSupportedError,
+ )
+ assertEquals(
+ notSupported.errorName,
+ "engine-not-supported",
+ )
+ assertEquals(
+ notSupported.displayError,
+ false,
+ )
+
+ val couldNotTranslate =
+ TranslationsException(TranslationsException.ERROR_COULD_NOT_TRANSLATE).intoTranslationError()
+ assertTrue(
+ couldNotTranslate is
+ TranslationError.CouldNotTranslateError,
+ )
+ assertEquals(
+ couldNotTranslate.errorName,
+ "could-not-translate",
+ )
+ assertEquals(
+ couldNotTranslate.displayError,
+ true,
+ )
+
+ val couldNotRestore =
+ TranslationsException(TranslationsException.ERROR_COULD_NOT_RESTORE).intoTranslationError()
+ assertTrue(
+ couldNotRestore is
+ TranslationError.CouldNotRestoreError,
+ )
+ assertEquals(
+ couldNotRestore.errorName,
+ "could-not-restore",
+ )
+ assertEquals(
+ couldNotRestore.displayError,
+ false,
+ )
+
+ val couldNotLoadLanguages =
+ TranslationsException(TranslationsException.ERROR_COULD_NOT_LOAD_LANGUAGES).intoTranslationError()
+ assertTrue(
+ couldNotLoadLanguages is
+ TranslationError.CouldNotLoadLanguagesError,
+ )
+ assertEquals(
+ couldNotLoadLanguages.errorName,
+ "could-not-load-languages",
+ )
+ assertEquals(
+ couldNotLoadLanguages.displayError,
+ true,
+ )
+
+ val languageNotSupported =
+ TranslationsException(TranslationsException.ERROR_LANGUAGE_NOT_SUPPORTED).intoTranslationError()
+ assertTrue(
+ languageNotSupported is
+ TranslationError.LanguageNotSupportedError,
+ )
+ assertEquals(
+ languageNotSupported.errorName,
+ "language-not-supported",
+ )
+ assertEquals(
+ languageNotSupported.displayError,
+ true,
+ )
+
+ val couldNotRetrieve =
+ TranslationsException(TranslationsException.ERROR_MODEL_COULD_NOT_RETRIEVE).intoTranslationError()
+ assertTrue(
+ couldNotRetrieve is
+ TranslationError.ModelCouldNotRetrieveError,
+ )
+ assertEquals(
+ couldNotRetrieve.errorName,
+ "model-could-not-retrieve",
+ )
+ assertEquals(
+ couldNotRetrieve.displayError,
+ false,
+ )
+
+ val couldNotDelete =
+ TranslationsException(TranslationsException.ERROR_MODEL_COULD_NOT_DELETE).intoTranslationError()
+ assertTrue(
+ couldNotDelete is
+ TranslationError.ModelCouldNotDeleteError,
+ )
+ assertEquals(
+ couldNotDelete.errorName,
+ "model-could-not-delete",
+ )
+ assertEquals(
+ couldNotDelete.displayError,
+ false,
+ )
+
+ val couldNotDownload =
+ TranslationsException(TranslationsException.ERROR_MODEL_COULD_NOT_DOWNLOAD).intoTranslationError()
+ assertTrue(
+ couldNotDownload is
+ TranslationError.ModelCouldNotDownloadError,
+ )
+ assertEquals(
+ couldNotDownload.errorName,
+ "model-could-not-download",
+ )
+ assertEquals(
+ couldNotDelete.displayError,
+ false,
+ )
+
+ val languageRequired =
+ TranslationsException(TranslationsException.ERROR_MODEL_LANGUAGE_REQUIRED).intoTranslationError()
+ assertTrue(
+ languageRequired is
+ TranslationError.ModelLanguageRequiredError,
+ )
+ assertEquals(
+ languageRequired.errorName,
+ "model-language-required",
+ )
+ assertEquals(
+ languageRequired.displayError,
+ false,
+ )
+
+ val downloadRequired =
+ TranslationsException(TranslationsException.ERROR_MODEL_DOWNLOAD_REQUIRED).intoTranslationError()
+ assertTrue(
+ downloadRequired is
+ TranslationError.ModelDownloadRequiredError,
+ )
+ assertEquals(
+ downloadRequired.errorName,
+ "model-download-required",
+ )
+ assertEquals(
+ downloadRequired.displayError,
+ false,
+ )
+ }
+
+ @Test
+ fun containsFormData() {
+ val engineSession = GeckoEngineSession(runtime = mock(), geckoSessionProvider = geckoSessionProvider)
+ var formData = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onCheckForFormData(containsFormData: Boolean) {
+ formData = true
+ }
+ },
+ )
+
+ whenever(geckoSession.containsFormData())
+ .thenReturn(GeckoResult.fromValue(null))
+ .thenReturn(GeckoResult.fromException(IllegalStateException()))
+ engineSession.checkForFormData()
+ assertEquals(false, formData)
+ }
+
+ @Test
+ fun checkForMobileSite() {
+ val mUrl = "https://m.example.com"
+ val mobileUrl = "https://mobile.example.com"
+ val nonAuthorityUrl = "mobile.example.com"
+ val unrecognizedMobilePrefixUrl = "https://phone.example.com"
+ val nonMobileUrl = "https://example.com"
+
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+
+ assertNull(engineSession.checkForMobileSite(nonAuthorityUrl))
+ assertNull(engineSession.checkForMobileSite(unrecognizedMobilePrefixUrl))
+ assertEquals(nonMobileUrl, engineSession.checkForMobileSite(mUrl))
+ assertEquals(nonMobileUrl, engineSession.checkForMobileSite(mobileUrl))
+ }
+
+ @Test
+ fun findAll() {
+ val finderResult = mock<GeckoSession.FinderResult>()
+ val sessionFinder = mock<SessionFinder>()
+ whenever(sessionFinder.find("mozilla", 0))
+ .thenReturn(GeckoResult.fromValue(finderResult))
+
+ whenever(geckoSession.finder).thenReturn(sessionFinder)
+
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ var findObserved: String? = null
+ var findResultObserved = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onFind(text: String) {
+ findObserved = text
+ }
+
+ override fun onFindResult(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean) {
+ assertEquals(0, activeMatchOrdinal)
+ assertEquals(0, numberOfMatches)
+ assertTrue(isDoneCounting)
+ findResultObserved = true
+ }
+ },
+ )
+
+ engineSession.findAll("mozilla")
+ shadowOf(getMainLooper()).idle()
+
+ assertEquals("mozilla", findObserved)
+ assertTrue(findResultObserved)
+ verify(sessionFinder).find("mozilla", 0)
+ }
+
+ @Test
+ fun findNext() {
+ val finderResult = mock<GeckoSession.FinderResult>()
+ val sessionFinder = mock<SessionFinder>()
+ whenever(sessionFinder.find(eq(null), anyInt()))
+ .thenReturn(GeckoResult.fromValue(finderResult))
+
+ whenever(geckoSession.finder).thenReturn(sessionFinder)
+
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ var findResultObserved = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onFindResult(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean) {
+ assertEquals(0, activeMatchOrdinal)
+ assertEquals(0, numberOfMatches)
+ assertTrue(isDoneCounting)
+ findResultObserved = true
+ }
+ },
+ )
+
+ engineSession.findNext(true)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(findResultObserved)
+ verify(sessionFinder).find(null, 0)
+
+ engineSession.findNext(false)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(findResultObserved)
+ verify(sessionFinder).find(null, GeckoSession.FINDER_FIND_BACKWARDS)
+ }
+
+ @Test
+ fun clearFindMatches() {
+ val finder = mock<SessionFinder>()
+ whenever(geckoSession.finder).thenReturn(finder)
+
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ engineSession.clearFindMatches()
+
+ verify(finder).clear()
+ }
+
+ @Test
+ fun exitFullScreenModeTriggersExitEvent() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ val observer: EngineSession.Observer = mock()
+
+ // Verify the event is triggered for exiting fullscreen mode and GeckoView is called.
+ engineSession.exitFullScreenMode()
+ verify(geckoSession).exitFullScreen()
+
+ // Verify the call to the observer.
+ engineSession.register(observer)
+
+ captureDelegates()
+
+ contentDelegate.value.onFullScreen(geckoSession, true)
+
+ verify(observer).onFullScreenChange(true)
+ }
+
+ @Test
+ fun exitFullscreenTrueHasNoInteraction() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ engineSession.exitFullScreenMode()
+ verify(geckoSession).exitFullScreen()
+ }
+
+ @Test
+ fun viewportFitChangeTranslateValuesCorrectly() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ val observer: EngineSession.Observer = mock()
+
+ // Verify the call to the observer.
+ engineSession.register(observer)
+ captureDelegates()
+
+ contentDelegate.value.onMetaViewportFitChange(geckoSession, "test")
+ verify(observer).onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT)
+ reset(observer)
+
+ contentDelegate.value.onMetaViewportFitChange(geckoSession, "auto")
+ verify(observer).onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT)
+ reset(observer)
+
+ contentDelegate.value.onMetaViewportFitChange(geckoSession, "cover")
+ verify(observer).onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
+ reset(observer)
+
+ contentDelegate.value.onMetaViewportFitChange(geckoSession, "contain")
+ verify(observer).onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER)
+ reset(observer)
+ }
+
+ @Test
+ fun onShowDynamicToolbarTriggersTheRightEvent() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ val observer: EngineSession.Observer = mock()
+
+ // Verify the call to the observer.
+ engineSession.register(observer)
+ captureDelegates()
+
+ contentDelegate.value.onShowDynamicToolbar(geckoSession)
+
+ verify(observer).onShowDynamicToolbar()
+ }
+
+ @Test
+ fun clearData() {
+ val engineSession = GeckoEngineSession(runtime, geckoSessionProvider = geckoSessionProvider)
+ val observer: EngineSession.Observer = mock()
+
+ engineSession.register(observer)
+
+ engineSession.clearData()
+
+ verifyNoInteractions(observer)
+ }
+
+ @Test
+ fun `Closing engine session should close underlying gecko session`() {
+ val geckoSession = mockGeckoSession()
+
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = { geckoSession })
+
+ engineSession.close()
+
+ verify(geckoSession).close()
+ }
+
+ @Test
+ fun `onLoadRequest will try to intercept new window load requests`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observedUrl: String? = null
+ var observedIntent: Intent? = null
+
+ var observedLoadUrl: String? = null
+ var observedTriggeredByRedirect: Boolean? = null
+ var observedTriggeredByWebContent: Boolean? = null
+
+ engineSession.settings.requestInterceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ return when (uri) {
+ "sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
+ else -> null
+ }
+ }
+ }
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLaunchIntentRequest(
+ url: String,
+ appIntent: Intent?,
+ ) {
+ observedUrl = url
+ observedIntent = appIntent
+ }
+
+ override fun onLoadRequest(url: String, triggeredByRedirect: Boolean, triggeredByWebContent: Boolean) {
+ observedLoadUrl = url
+ observedTriggeredByRedirect = triggeredByRedirect
+ observedTriggeredByWebContent = triggeredByWebContent
+ }
+ },
+ )
+
+ var result = navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest(
+ "sample:about",
+ null,
+ GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW,
+ triggeredByRedirect = true,
+ ),
+ )
+
+ assertEquals(result!!.poll(0), AllowOrDeny.DENY)
+ assertNotNull(observedIntent)
+ assertEquals("result", observedUrl)
+ assertNull(observedLoadUrl)
+ assertNull(observedTriggeredByRedirect)
+ assertNull(observedTriggeredByWebContent)
+
+ result = navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest(
+ "sample:about",
+ null,
+ GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW,
+ triggeredByRedirect = false,
+ ),
+ )
+
+ assertEquals(result!!.poll(0), AllowOrDeny.DENY)
+ assertNotNull(observedIntent)
+ assertEquals("result", observedUrl)
+ assertNull(observedLoadUrl)
+ assertNull(observedTriggeredByRedirect)
+ assertNull(observedTriggeredByWebContent)
+ }
+
+ @Test
+ fun `onLoadRequest allows new window requests if not intercepted`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observedUrl: String? = null
+ var observedIntent: Intent? = null
+
+ var observedLoadUrl: String? = null
+ var observedTriggeredByRedirect: Boolean? = null
+ var observedTriggeredByWebContent: Boolean? = null
+
+ engineSession.settings.requestInterceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ return when (uri) {
+ "sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
+ else -> null
+ }
+ }
+ }
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLaunchIntentRequest(
+ url: String,
+ appIntent: Intent?,
+ ) {
+ observedUrl = url
+ observedIntent = appIntent
+ }
+
+ override fun onLoadRequest(url: String, triggeredByRedirect: Boolean, triggeredByWebContent: Boolean) {
+ observedLoadUrl = url
+ observedTriggeredByRedirect = triggeredByRedirect
+ observedTriggeredByWebContent = triggeredByWebContent
+ }
+ },
+ )
+
+ var result = navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest(
+ "about:blank",
+ null,
+ GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW,
+ triggeredByRedirect = true,
+ ),
+ )
+
+ assertEquals(result!!.poll(0), AllowOrDeny.ALLOW)
+ assertNull(observedIntent)
+ assertNull(observedUrl)
+ assertNull(observedLoadUrl)
+ assertNull(observedTriggeredByRedirect)
+ assertNull(observedTriggeredByWebContent)
+
+ result = navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest(
+ "https://www.example.com",
+ null,
+ GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW,
+ triggeredByRedirect = true,
+ ),
+ )
+
+ assertEquals(result!!.poll(0), AllowOrDeny.ALLOW)
+ assertNull(observedIntent)
+ assertNull(observedUrl)
+ assertNull(observedLoadUrl)
+ assertNull(observedTriggeredByRedirect)
+ assertNull(observedTriggeredByWebContent)
+ }
+
+ @Test
+ fun `onLoadRequest not intercepted and not new window will notify observer`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observedLoadUrl: String? = null
+ var observedTriggeredByRedirect: Boolean? = null
+ var observedTriggeredByWebContent: Boolean? = null
+
+ engineSession.settings.requestInterceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ return when (uri) {
+ "sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
+ else -> null
+ }
+ }
+ }
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadRequest(url: String, triggeredByRedirect: Boolean, triggeredByWebContent: Boolean) {
+ observedLoadUrl = url
+ observedTriggeredByRedirect = triggeredByRedirect
+ observedTriggeredByWebContent = triggeredByWebContent
+ }
+ },
+ )
+
+ val result = navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest("https://www.example.com", null, triggeredByRedirect = true),
+ )
+
+ assertEquals(result!!.poll(0), AllowOrDeny.ALLOW)
+ assertEquals("https://www.example.com", observedLoadUrl)
+ assertEquals(true, observedTriggeredByRedirect)
+ assertEquals(false, observedTriggeredByWebContent)
+ }
+
+ @Test
+ fun `State provided through delegate will be returned from saveState`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ val state: GeckoSession.SessionState = mock()
+
+ var observedState: EngineSessionState? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onStateUpdated(state: EngineSessionState) {
+ observedState = state
+ }
+ },
+ )
+
+ progressDelegate.value.onSessionStateChange(mock(), state)
+
+ assertNotNull(observedState)
+ assertTrue(observedState is GeckoEngineSessionState)
+
+ val actualState = (observedState as GeckoEngineSessionState).actualState
+ assertEquals(state, actualState)
+ }
+
+ @Test
+ fun `onFirstContentfulPaint notifies observers`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observed = false
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onFirstContentfulPaint() {
+ observed = true
+ }
+ },
+ )
+
+ contentDelegate.value.onFirstContentfulPaint(mock())
+ assertTrue(observed)
+ }
+
+ @Test
+ fun `onPaintStatusReset notifies observers`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observed = false
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPaintStatusReset() {
+ observed = true
+ }
+ },
+ )
+
+ contentDelegate.value.onPaintStatusReset(mock())
+ assertTrue(observed)
+ }
+
+ @Test
+ fun `onCrash notifies observers about crash`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var crashedState = false
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onCrash() {
+ crashedState = true
+ }
+ },
+ )
+
+ contentDelegate.value.onCrash(mock())
+
+ assertEquals(true, crashedState)
+ }
+
+ @Test
+ fun `onLoadRequest will notify onLaunchIntent observers if request was intercepted with app intent`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observedUrl: String? = null
+ var observedIntent: Intent? = null
+
+ engineSession.settings.requestInterceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ return when (uri) {
+ "sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
+ else -> null
+ }
+ }
+ }
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLaunchIntentRequest(
+ url: String,
+ appIntent: Intent?,
+ ) {
+ observedUrl = url
+ observedIntent = appIntent
+ }
+ },
+ )
+
+ navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = true),
+ )
+
+ assertNotNull(observedIntent)
+ assertEquals("result", observedUrl)
+
+ navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = false),
+ )
+
+ assertNotNull(observedIntent)
+ assertEquals("result", observedUrl)
+ }
+
+ @Test
+ fun `onLoadRequest keep track of the last onLoadRequest uri correctly`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observedUrl: String? = null
+
+ engineSession.settings.requestInterceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ observedUrl = lastUri
+ return null
+ }
+ }
+
+ navigationDelegate.value.onLoadRequest(mock(), mockLoadRequest("test1"))
+ assertEquals(null, observedUrl)
+
+ navigationDelegate.value.onLoadRequest(mock(), mockLoadRequest("test2"))
+ assertEquals("test1", observedUrl)
+
+ navigationDelegate.value.onLoadRequest(mock(), mockLoadRequest("test3"))
+ assertEquals("test2", observedUrl)
+ }
+
+ @Test
+ fun `onSubframeLoadRequest will notify onLaunchIntent observers if request was intercepted with app intent`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observedUrl: String? = null
+ var observedIntent: Intent? = null
+ var observedIsSubframe = false
+
+ engineSession.settings.requestInterceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ observedIsSubframe = isSubframeRequest
+ return when (uri) {
+ "sample:about" -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), "result")
+ else -> null
+ }
+ }
+ }
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLaunchIntentRequest(
+ url: String,
+ appIntent: Intent?,
+ ) {
+ observedUrl = url
+ observedIntent = appIntent
+ }
+ },
+ )
+
+ navigationDelegate.value.onSubframeLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = true),
+ )
+
+ assertNotNull(observedIntent)
+ assertEquals("result", observedUrl)
+ assertEquals(true, observedIsSubframe)
+
+ navigationDelegate.value.onSubframeLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = false),
+ )
+
+ assertNotNull(observedIntent)
+ assertEquals("result", observedUrl)
+ assertEquals(true, observedIsSubframe)
+ }
+
+ @Test
+ fun `onLoadRequest will notify any observers if request was intercepted as url`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observedLaunchIntentUrl: String? = null
+ var observedLaunchIntent: Intent? = null
+ var observedOnLoadRequestUrl: String? = null
+ var observedTriggeredByRedirect: Boolean? = null
+ var observedTriggeredByWebContent: Boolean? = null
+
+ engineSession.settings.requestInterceptor = object : RequestInterceptor {
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ return when (uri) {
+ "sample:about" -> RequestInterceptor.InterceptionResponse.Url("result")
+ else -> null
+ }
+ }
+ }
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLaunchIntentRequest(
+ url: String,
+ appIntent: Intent?,
+ ) {
+ observedLaunchIntentUrl = url
+ observedLaunchIntent = appIntent
+ }
+
+ override fun onLoadRequest(
+ url: String,
+ triggeredByRedirect: Boolean,
+ triggeredByWebContent: Boolean,
+ ) {
+ observedOnLoadRequestUrl = url
+ observedTriggeredByRedirect = triggeredByRedirect
+ observedTriggeredByWebContent = triggeredByWebContent
+ }
+ },
+ )
+
+ navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = true),
+ )
+
+ assertNull(observedLaunchIntentUrl)
+ assertNull(observedLaunchIntent)
+ assertNull(observedTriggeredByRedirect)
+ assertNull(observedTriggeredByWebContent)
+ assertNull(observedOnLoadRequestUrl)
+
+ navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = false),
+ )
+
+ assertNull(observedLaunchIntentUrl)
+ assertNull(observedLaunchIntent)
+ assertNull(observedTriggeredByRedirect)
+ assertNull(observedTriggeredByWebContent)
+ assertNull(observedOnLoadRequestUrl)
+ }
+
+ @Test
+ fun `onLoadRequest will notify onLoadRequest observers if request was not intercepted`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observedLaunchIntentUrl: String? = null
+ var observedLaunchIntent: Intent? = null
+ var observedOnLoadRequestUrl: String? = null
+ var observedTriggeredByRedirect: Boolean? = null
+ var observedTriggeredByWebContent: Boolean? = null
+
+ engineSession.settings.requestInterceptor = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLaunchIntentRequest(
+ url: String,
+ appIntent: Intent?,
+ ) {
+ observedLaunchIntentUrl = url
+ observedLaunchIntent = appIntent
+ }
+
+ override fun onLoadRequest(
+ url: String,
+ triggeredByRedirect: Boolean,
+ triggeredByWebContent: Boolean,
+ ) {
+ observedOnLoadRequestUrl = url
+ observedTriggeredByRedirect = triggeredByRedirect
+ observedTriggeredByWebContent = triggeredByWebContent
+ }
+ },
+ )
+
+ navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = true),
+ )
+
+ assertNull(observedLaunchIntentUrl)
+ assertNull(observedLaunchIntent)
+ assertNotNull(observedTriggeredByRedirect)
+ assertTrue(observedTriggeredByRedirect!!)
+ assertNotNull(observedTriggeredByWebContent)
+ assertFalse(observedTriggeredByWebContent!!)
+ assertEquals("sample:about", observedOnLoadRequestUrl)
+
+ navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = false),
+ )
+
+ assertNull(observedLaunchIntentUrl)
+ assertNull(observedLaunchIntent)
+ assertNotNull(observedTriggeredByRedirect)
+ assertFalse(observedTriggeredByRedirect!!)
+ assertNotNull(observedTriggeredByWebContent)
+ assertFalse(observedTriggeredByWebContent!!)
+ assertEquals("sample:about", observedOnLoadRequestUrl)
+ }
+
+ @Test
+ fun `onLoadRequest will notify observers if the url is loaded from the user interacting with chrome`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ val fakeUrl = "https://example.com"
+ var observedUrl: String?
+ var observedTriggeredByWebContent: Boolean?
+
+ engineSession.settings.requestInterceptor = object : RequestInterceptor {
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ return when (uri) {
+ fakeUrl -> null
+ else -> RequestInterceptor.InterceptionResponse.AppIntent(mock(), fakeUrl)
+ }
+ }
+ }
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadRequest(
+ url: String,
+ triggeredByRedirect: Boolean,
+ triggeredByWebContent: Boolean,
+ ) {
+ observedTriggeredByWebContent = triggeredByWebContent
+ observedUrl = url
+ }
+ },
+ )
+
+ fun fakePageLoad(expectedTriggeredByWebContent: Boolean) {
+ observedTriggeredByWebContent = null
+ observedUrl = null
+ navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest(
+ fakeUrl,
+ triggeredByRedirect = true,
+ hasUserGesture = expectedTriggeredByWebContent,
+ ),
+ )
+ progressDelegate.value.onPageStop(mock(), true)
+ assertNotNull(observedTriggeredByWebContent)
+ assertEquals(expectedTriggeredByWebContent, observedTriggeredByWebContent!!)
+ assertNotNull(observedUrl)
+ assertEquals(fakeUrl, observedUrl)
+ }
+
+ // loadUrl(url: String)
+ engineSession.loadUrl(fakeUrl)
+ verify(geckoSession).load(
+ GeckoSession.Loader().uri(fakeUrl),
+ )
+ fakePageLoad(false)
+
+ // subsequent page loads _are_ from web content
+ fakePageLoad(true)
+
+ // loadData(data: String, mimeType: String, encoding: String)
+ val fakeData = "data://"
+ val fakeMimeType = ""
+ val fakeEncoding = ""
+ engineSession.loadData(data = fakeData, mimeType = fakeMimeType, encoding = fakeEncoding)
+ verify(geckoSession).load(
+ GeckoSession.Loader().data(fakeData, fakeMimeType),
+ )
+ fakePageLoad(false)
+
+ fakePageLoad(true)
+
+ // reload()
+ engineSession.initialLoadRequest = null
+ engineSession.reload()
+ verify(geckoSession).reload(GeckoSession.LOAD_FLAGS_NONE)
+ fakePageLoad(false)
+
+ fakePageLoad(true)
+
+ // goBack()
+ engineSession.goBack()
+ verify(geckoSession).goBack(true)
+ fakePageLoad(false)
+
+ fakePageLoad(true)
+
+ // goForward()
+ engineSession.goForward()
+ verify(geckoSession).goForward(true)
+ fakePageLoad(false)
+
+ fakePageLoad(true)
+
+ // toggleDesktopMode()
+ engineSession.toggleDesktopMode(false, reload = true)
+ // This is the second time in this test, so we actually want two invocations.
+ verify(geckoSession, times(2)).reload(GeckoSession.LOAD_FLAGS_NONE)
+ fakePageLoad(false)
+
+ fakePageLoad(true)
+
+ // goToHistoryIndex(index: Int)
+ engineSession.goToHistoryIndex(0)
+ verify(geckoSession).gotoHistoryIndex(0)
+ fakePageLoad(false)
+
+ fakePageLoad(true)
+ }
+
+ @Test
+ fun `onLoadRequest will return correct GeckoResult if no observer is available`() {
+ GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+ captureDelegates()
+
+ val geckoResult = navigationDelegate.value.onLoadRequest(
+ mock(),
+ mockLoadRequest("sample:about", triggeredByRedirect = true),
+ )
+
+ assertEquals(geckoResult!!, GeckoResult.fromValue(AllowOrDeny.ALLOW))
+ }
+
+ @Test
+ fun loadFlagsAreAligned() {
+ assertEquals(LoadUrlFlags.BYPASS_CACHE, GeckoSession.LOAD_FLAGS_BYPASS_CACHE)
+ assertEquals(LoadUrlFlags.BYPASS_PROXY, GeckoSession.LOAD_FLAGS_BYPASS_PROXY)
+ assertEquals(LoadUrlFlags.EXTERNAL, GeckoSession.LOAD_FLAGS_EXTERNAL)
+ assertEquals(LoadUrlFlags.ALLOW_POPUPS, GeckoSession.LOAD_FLAGS_ALLOW_POPUPS)
+ assertEquals(LoadUrlFlags.BYPASS_CLASSIFIER, GeckoSession.LOAD_FLAGS_BYPASS_CLASSIFIER)
+ assertEquals(LoadUrlFlags.LOAD_FLAGS_FORCE_ALLOW_DATA_URI, GeckoSession.LOAD_FLAGS_FORCE_ALLOW_DATA_URI)
+ assertEquals(LoadUrlFlags.LOAD_FLAGS_REPLACE_HISTORY, GeckoSession.LOAD_FLAGS_REPLACE_HISTORY)
+ }
+
+ @Test
+ fun `onKill will notify observers`() {
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ captureDelegates()
+
+ var observerNotified = false
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onProcessKilled() {
+ observerNotified = true
+ }
+ },
+ )
+
+ val mockedState: GeckoSession.SessionState = mock()
+ progressDelegate.value.onSessionStateChange(geckoSession, mockedState)
+
+ contentDelegate.value.onKill(geckoSession)
+
+ assertTrue(observerNotified)
+ }
+
+ @Test
+ fun `onNewSession creates window request`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+
+ captureDelegates()
+
+ var receivedWindowRequest: WindowRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onWindowRequest(windowRequest: WindowRequest) {
+ receivedWindowRequest = windowRequest
+ }
+ },
+ )
+
+ navigationDelegate.value.onNewSession(mock(), "mozilla.org")
+
+ assertNotNull(receivedWindowRequest)
+ assertEquals("mozilla.org", receivedWindowRequest!!.url)
+ assertEquals(WindowRequest.Type.OPEN, receivedWindowRequest!!.type)
+ }
+
+ @Test
+ fun `onCloseRequest creates window request`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+
+ captureDelegates()
+
+ var receivedWindowRequest: WindowRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onWindowRequest(windowRequest: WindowRequest) {
+ receivedWindowRequest = windowRequest
+ }
+ },
+ )
+
+ contentDelegate.value.onCloseRequest(geckoSession)
+
+ assertNotNull(receivedWindowRequest)
+ assertSame(engineSession, receivedWindowRequest!!.prepare())
+ assertEquals(WindowRequest.Type.CLOSE, receivedWindowRequest!!.type)
+ }
+
+ class MockSecurityInformation(
+ origin: String? = null,
+ certificate: X509Certificate? = null,
+ ) : SecurityInformation() {
+ init {
+ origin?.let {
+ ReflectionUtils.setField(this, "origin", origin)
+ }
+ certificate?.let {
+ ReflectionUtils.setField(this, "certificate", certificate)
+ }
+ }
+ }
+
+ @Test
+ fun `certificate issuer is parsed and provided onSecurityChange`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+
+ var observedIssuer: String? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
+ observedIssuer = issuer
+ }
+ },
+ )
+
+ captureDelegates()
+
+ val unparsedIssuerName = "Verified By: CN=Digicert SHA2 Extended Validation Server CA,OU=www.digicert.com,O=DigiCert Inc,C=US"
+ val parsedIssuerName = "DigiCert Inc"
+ val certificate: X509Certificate = mock()
+ val principal: Principal = mock()
+ whenever(principal.name).thenReturn(unparsedIssuerName)
+ whenever(certificate.issuerDN).thenReturn(principal)
+
+ val securityInformation = MockSecurityInformation(certificate = certificate)
+ progressDelegate.value.onSecurityChange(mock(), securityInformation)
+ assertEquals(parsedIssuerName, observedIssuer)
+ }
+
+ @Test
+ fun `certificate issuer is parsed and provided onSecurityChange with null arg`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+
+ var observedIssuer: String? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
+ observedIssuer = issuer
+ }
+ },
+ )
+
+ captureDelegates()
+
+ val unparsedIssuerName = null
+ val parsedIssuerName = null
+ val certificate: X509Certificate = mock()
+ val principal: Principal = mock()
+ whenever(principal.name).thenReturn(unparsedIssuerName)
+ whenever(certificate.issuerDN).thenReturn(principal)
+
+ val securityInformation = MockSecurityInformation(certificate = certificate)
+ progressDelegate.value.onSecurityChange(mock(), securityInformation)
+ assertEquals(parsedIssuerName, observedIssuer)
+ }
+
+ @Test
+ fun `pattern-breaking certificate issuer isnt parsed and returns original name `() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+
+ var observedIssuer: String? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
+ observedIssuer = issuer
+ }
+ },
+ )
+
+ captureDelegates()
+
+ val unparsedIssuerName = "pattern breaking cert"
+ val parsedIssuerName = "pattern breaking cert"
+ val certificate: X509Certificate = mock()
+ val principal: Principal = mock()
+ whenever(principal.name).thenReturn(unparsedIssuerName)
+ whenever(certificate.issuerDN).thenReturn(principal)
+
+ val securityInformation = MockSecurityInformation(certificate = certificate)
+ progressDelegate.value.onSecurityChange(mock(), securityInformation)
+ assertEquals(parsedIssuerName, observedIssuer)
+ }
+
+ @Test
+ fun `GIVEN canGoBack true WHEN goBack() is called THEN verify EngineObserver onNavigateBack() is triggered`() {
+ var observedOnNavigateBack = false
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onNavigateBack() {
+ observedOnNavigateBack = true
+ }
+ },
+ )
+
+ captureDelegates()
+ navigationDelegate.value.onCanGoBack(mock(), true)
+ engineSession.goBack()
+ assertTrue(observedOnNavigateBack)
+ }
+
+ @Test
+ fun `GIVEN canGoBack false WHEN goBack() is called THEN verify EngineObserver onNavigateBack() is not triggered`() {
+ var observedOnNavigateBack = false
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onNavigateBack() {
+ observedOnNavigateBack = true
+ }
+ },
+ )
+
+ captureDelegates()
+ navigationDelegate.value.onCanGoBack(mock(), false)
+ engineSession.goBack()
+ assertFalse(observedOnNavigateBack)
+ }
+
+ @Test
+ fun `GIVEN forward navigation is possible WHEN navigating forward THEN observers are notified`() {
+ var observedOnNavigateForward = false
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onNavigateForward() {
+ observedOnNavigateForward = true
+ }
+ },
+ )
+
+ captureDelegates()
+ navigationDelegate.value.onCanGoForward(mock(), true)
+ engineSession.goForward()
+ assertTrue(observedOnNavigateForward)
+ }
+
+ @Test
+ fun `GIVEN forward navigation is not possible WHEN navigating forward THEN forward navigation observers are not notified`() {
+ var observedOnNavigateForward = false
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onNavigateBack() {
+ observedOnNavigateForward = true
+ }
+ },
+ )
+
+ captureDelegates()
+ navigationDelegate.value.onCanGoForward(mock(), false)
+ engineSession.goForward()
+ assertFalse(observedOnNavigateForward)
+ }
+
+ @Test
+ fun `WHEN URL is loaded THEN URL load observer is notified`() {
+ var onLoadUrlTriggered = false
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadUrl() {
+ onLoadUrlTriggered = true
+ }
+ },
+ )
+
+ captureDelegates()
+ engineSession.loadUrl("http://mozilla.org")
+ assertTrue(onLoadUrlTriggered)
+ }
+
+ @Test
+ fun `WHEN data is loaded THEN data load observer is notified`() {
+ var onLoadDataTriggered = false
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadData() {
+ onLoadDataTriggered = true
+ }
+ },
+ )
+
+ captureDelegates()
+ engineSession.loadData("<html><body/></html>")
+ assertTrue(onLoadDataTriggered)
+ }
+
+ @Test
+ fun `WHEN navigating to history index THEN the observer is notified`() {
+ var onGotoHistoryIndexTriggered = false
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onGotoHistoryIndex() {
+ onGotoHistoryIndexTriggered = true
+ }
+ },
+ )
+
+ captureDelegates()
+ engineSession.goToHistoryIndex(0)
+ assertTrue(onGotoHistoryIndexTriggered)
+ }
+
+ @Test
+ fun `GIVEN a list of blocked schemes set WHEN getBlockedSchemes is called THEN it returns that list`() {
+ val engineSession = GeckoEngineSession(mock(), geckoSessionProvider = geckoSessionProvider)
+
+ assertSame(GeckoEngineSession.BLOCKED_SCHEMES, engineSession.getBlockedSchemes())
+ }
+
+ @Test
+ fun `WHEN requestPdfToDownload THEN notify observers`() {
+ val engineSession = GeckoEngineSession(
+ runtime = mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ ).apply {
+ currentUrl = "https://mozilla.org"
+ currentTitle = "Mozilla"
+ }
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onExternalResource(
+ url: String,
+ fileName: String?,
+ contentLength: Long?,
+ contentType: String?,
+ cookie: String?,
+ userAgent: String?,
+ isPrivate: Boolean,
+ skipConfirmation: Boolean,
+ openInApp: Boolean,
+ response: Response?,
+ ) {
+ assertEquals("PDF response is always a success.", RESPONSE_CODE_SUCCESS, response!!.status)
+ assertEquals("Length should always be zero.", 0L, contentLength)
+ assertEquals("Filename is based on title, when available.", "Mozilla.pdf", fileName)
+ assertEquals("Content type is always static.", "application/pdf", contentType)
+ }
+ },
+ )
+
+ whenever(geckoSession.saveAsPdf()).thenReturn(GeckoResult.fromValue(mock()))
+
+ engineSession.requestPdfToDownload()
+ shadowOf(getMainLooper()).idle()
+ }
+
+ @Test
+ fun `WHEN requestPdfToDownload cannot return a result THEN do nothing`() {
+ val engineSession = GeckoEngineSession(
+ runtime = mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onExternalResource(
+ url: String,
+ fileName: String?,
+ contentLength: Long?,
+ contentType: String?,
+ cookie: String?,
+ userAgent: String?,
+ isPrivate: Boolean,
+ skipConfirmation: Boolean,
+ openInApp: Boolean,
+ response: Response?,
+ ) {
+ assert(false) { "We should not notify observers." }
+ }
+ },
+ )
+
+ whenever(geckoSession.saveAsPdf())
+ .thenReturn(GeckoResult.fromValue(null))
+ .thenReturn(GeckoResult.fromException(IllegalStateException()))
+
+ // When input stream in the GeckoResult is null.
+ engineSession.requestPdfToDownload()
+ shadowOf(getMainLooper()).idle()
+
+ // When we receive an exception from the GeckoResult.
+ engineSession.requestPdfToDownload()
+ shadowOf(getMainLooper()).idle()
+ }
+
+ @Test
+ fun `setDisplayMode sets same display mode value`() {
+ val geckoSetting = mock<GeckoSessionSettings>()
+ val geckoSession = mock<GeckoSession>()
+
+ val engineSession = GeckoEngineSession(
+ mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+
+ whenever(geckoSession.settings).thenReturn(geckoSetting)
+
+ engineSession.geckoSession = geckoSession
+
+ engineSession.setDisplayMode(WebAppManifest.DisplayMode.FULLSCREEN)
+ verify(geckoSetting, atLeastOnce()).setDisplayMode(GeckoSessionSettings.DISPLAY_MODE_FULLSCREEN)
+
+ engineSession.setDisplayMode(WebAppManifest.DisplayMode.STANDALONE)
+ verify(geckoSetting, atLeastOnce()).setDisplayMode(GeckoSessionSettings.DISPLAY_MODE_STANDALONE)
+
+ engineSession.setDisplayMode(WebAppManifest.DisplayMode.MINIMAL_UI)
+ verify(geckoSetting, atLeastOnce()).setDisplayMode(GeckoSessionSettings.DISPLAY_MODE_MINIMAL_UI)
+
+ engineSession.setDisplayMode(WebAppManifest.DisplayMode.BROWSER)
+ verify(geckoSetting, atLeastOnce()).setDisplayMode(GeckoSessionSettings.DISPLAY_MODE_BROWSER)
+ }
+
+ fun `WHEN requestPrintContent is successful THEN notify of completion`() {
+ val engineSession = GeckoEngineSession(
+ runtime = mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ whenever(geckoSession.didPrintPageContent()).thenReturn(GeckoResult.fromValue(true))
+
+ engineSession.register(object : EngineSession.Observer {
+ override fun onPrintFinish() {
+ assert(true) { "We should notify of a successful print." }
+ }
+
+ override fun onPrintException(isPrint: Boolean, throwable: Throwable) {
+ assert(false) { "We should not notify of an exception." } }
+ })
+ engineSession.requestPrintContent()
+ shadowOf(getMainLooper()).idle()
+ }
+
+ @Test
+ fun `WHEN requestPrintContent has an exception THEN do nothing`() {
+ val engineSession = GeckoEngineSession(
+ runtime = mock(),
+ geckoSessionProvider = geckoSessionProvider,
+ )
+ class MockGeckoPrintException() : GeckoPrintException()
+ whenever(geckoSession.didPrintPageContent()).thenReturn(GeckoResult.fromException(MockGeckoPrintException()))
+
+ engineSession.register(object : EngineSession.Observer {
+ override fun onPrintFinish() {
+ assert(false) { "We should not notify of a successful print." }
+ }
+
+ override fun onPrintException(isPrint: Boolean, throwable: Throwable) {
+ assert(true) { "An exception should occur." }
+ assertEquals("A GeckoPrintException occurred.", ERROR_PRINT_SETTINGS_SERVICE_NOT_AVAILABLE, (throwable as GeckoPrintException).code)
+ }
+ })
+ engineSession.requestPrintContent()
+ shadowOf(getMainLooper()).idle()
+ }
+
+ private fun mockGeckoSession(): GeckoSession {
+ val session = mock<GeckoSession>()
+ whenever(session.settings).thenReturn(
+ mock(),
+ )
+ return session
+ }
+
+ private fun mockLoadRequest(
+ uri: String,
+ triggerUri: String? = null,
+ target: Int = 0,
+ triggeredByRedirect: Boolean = false,
+ hasUserGesture: Boolean = false,
+ isDirectNavigation: Boolean = false,
+ ): GeckoSession.NavigationDelegate.LoadRequest {
+ var flags = 0
+ if (triggeredByRedirect) {
+ flags = flags or 0x800000
+ }
+
+ val constructor = GeckoSession.NavigationDelegate.LoadRequest::class.java.getDeclaredConstructor(
+ String::class.java,
+ String::class.java,
+ Int::class.java,
+ Int::class.java,
+ Boolean::class.java,
+ Boolean::class.java,
+ )
+ constructor.isAccessible = true
+
+ return constructor.newInstance(uri, triggerUri, target, flags, hasUserGesture, isDirectNavigation)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt
new file mode 100644
index 0000000000..6a8ed3c330
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt
@@ -0,0 +1,3673 @@
+/* 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.browser.engine.gecko
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Color
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.ext.getAntiTrackingPolicy
+import mozilla.components.browser.engine.gecko.mediaquery.toGeckoValue
+import mozilla.components.browser.engine.gecko.serviceworker.GeckoServiceWorkerDelegate
+import mozilla.components.browser.engine.gecko.util.SpeculativeEngineSession
+import mozilla.components.browser.engine.gecko.util.SpeculativeSessionObserver
+import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtensionException
+import mozilla.components.browser.engine.gecko.webextension.mockNativeWebExtension
+import mozilla.components.browser.engine.gecko.webextension.mockNativeWebExtensionMetaData
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.SafeBrowsingPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
+import mozilla.components.concept.engine.UnsupportedSettingException
+import mozilla.components.concept.engine.content.blocking.TrackerLog
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
+import mozilla.components.concept.engine.translate.LanguageSetting
+import mozilla.components.concept.engine.translate.ModelManagementOptions
+import mozilla.components.concept.engine.translate.ModelOperation
+import mozilla.components.concept.engine.translate.OperationLevel
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.engine.webextension.InstallationMethod
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionDelegate
+import mozilla.components.concept.engine.webextension.WebExtensionException
+import mozilla.components.concept.engine.webextension.WebExtensionInstallException
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import mozilla.components.test.ReflectionUtils
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.ContentBlocking
+import org.mozilla.geckoview.ContentBlocking.CookieBehavior
+import org.mozilla.geckoview.ContentBlockingController
+import org.mozilla.geckoview.ContentBlockingController.Event
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoRuntimeSettings
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoWebExecutor
+import org.mozilla.geckoview.OrientationController
+import org.mozilla.geckoview.StorageController
+import org.mozilla.geckoview.TranslationsController
+import org.mozilla.geckoview.TranslationsController.Language
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.LanguageModel
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.checkPairDownloadSize
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.getLanguageSetting
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.getLanguageSettings
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.getNeverTranslateSiteList
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.isTranslationsEngineSupported
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.listModelDownloadStates
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.listSupportedLanguages
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.manageLanguageModel
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.preferredLanguages
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.setLanguageSettings
+import org.mozilla.geckoview.TranslationsController.RuntimeTranslation.setNeverTranslateSpecifiedSite
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_BLOCKLISTED
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_CORRUPT_FILE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_FILE_ACCESS
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCORRECT_HASH
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCORRECT_ID
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_NETWORK_FAILURE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_UNEXPECTED_ADDON_TYPE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED
+import org.mozilla.geckoview.WebExtensionController
+import org.mozilla.geckoview.WebNotification
+import org.mozilla.geckoview.WebPushController
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import java.io.IOException
+import org.mozilla.geckoview.WebExtension as GeckoWebExtension
+
+typealias GeckoInstallException = org.mozilla.geckoview.WebExtension.InstallException
+
+@RunWith(AndroidJUnit4::class)
+class GeckoEngineTest {
+
+ private lateinit var runtime: GeckoRuntime
+ private lateinit var context: Context
+
+ @Before
+ fun setup() {
+ runtime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ context = mock()
+ }
+
+ @Test
+ fun createView() {
+ assertTrue(
+ GeckoEngine(context, runtime = runtime).createView(
+ Robolectric.buildActivity(Activity::class.java).get(),
+ ) is GeckoEngineView,
+ )
+ }
+
+ @Test
+ fun createSession() {
+ val engine = GeckoEngine(context, runtime = runtime)
+ assertTrue(engine.createSession() is GeckoEngineSession)
+
+ // Create a private speculative session and consume it
+ engine.speculativeCreateSession(private = true)
+ assertTrue(engine.speculativeConnectionFactory.hasSpeculativeSession())
+ var privateSpeculativeSession = engine.speculativeConnectionFactory.speculativeEngineSession!!.engineSession
+ assertSame(privateSpeculativeSession, engine.createSession(private = true))
+ assertFalse(engine.speculativeConnectionFactory.hasSpeculativeSession())
+
+ // Create a regular speculative session and make sure it is not returned
+ // if a private session is requested instead.
+ engine.speculativeCreateSession(private = false)
+ assertTrue(engine.speculativeConnectionFactory.hasSpeculativeSession())
+ privateSpeculativeSession = engine.speculativeConnectionFactory.speculativeEngineSession!!.engineSession
+ assertNotSame(privateSpeculativeSession, engine.createSession(private = true))
+ // Make sure previous (never used) speculative session is now closed
+ assertFalse(privateSpeculativeSession.geckoSession.isOpen)
+ assertFalse(engine.speculativeConnectionFactory.hasSpeculativeSession())
+ }
+
+ @Test
+ fun speculativeCreateSession() {
+ val engine = GeckoEngine(context, runtime = runtime)
+ assertNull(engine.speculativeConnectionFactory.speculativeEngineSession)
+
+ // Create a private speculative session
+ engine.speculativeCreateSession(private = true)
+ assertNotNull(engine.speculativeConnectionFactory.speculativeEngineSession)
+ val privateSpeculativeSession = engine.speculativeConnectionFactory.speculativeEngineSession!!
+ assertTrue(privateSpeculativeSession.engineSession.geckoSession.settings.usePrivateMode)
+
+ // Creating another private speculative session should have no effect as
+ // session hasn't been "consumed".
+ engine.speculativeCreateSession(private = true)
+ assertSame(privateSpeculativeSession, engine.speculativeConnectionFactory.speculativeEngineSession)
+ assertTrue(privateSpeculativeSession.engineSession.geckoSession.settings.usePrivateMode)
+
+ // Creating a non-private speculative session should affect prepared session
+ engine.speculativeCreateSession(private = false)
+ assertNotSame(privateSpeculativeSession, engine.speculativeConnectionFactory.speculativeEngineSession)
+ // Make sure previous (never used) speculative session is now closed
+ assertFalse(privateSpeculativeSession.engineSession.geckoSession.isOpen)
+ val regularSpeculativeSession = engine.speculativeConnectionFactory.speculativeEngineSession!!
+ assertFalse(regularSpeculativeSession.engineSession.geckoSession.settings.usePrivateMode)
+ }
+
+ @Test
+ fun clearSpeculativeSession() {
+ val engine = GeckoEngine(context, runtime = runtime)
+ assertNull(engine.speculativeConnectionFactory.speculativeEngineSession)
+
+ val mockEngineSession: GeckoEngineSession = mock()
+ val mockEngineSessionObserver: SpeculativeSessionObserver = mock()
+ engine.speculativeConnectionFactory.speculativeEngineSession =
+ SpeculativeEngineSession(mockEngineSession, mockEngineSessionObserver)
+ engine.clearSpeculativeSession()
+
+ verify(mockEngineSession).unregister(mockEngineSessionObserver)
+ verify(mockEngineSession).close()
+ assertNull(engine.speculativeConnectionFactory.speculativeEngineSession)
+ }
+
+ @Test
+ fun `createSession with contextId`() {
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ // Create a speculative session with a context id and consume it
+ engine.speculativeCreateSession(private = false, contextId = "1")
+ assertNotNull(engine.speculativeConnectionFactory.speculativeEngineSession)
+ var newSpeculativeSession = engine.speculativeConnectionFactory.speculativeEngineSession!!.engineSession
+ assertSame(newSpeculativeSession, engine.createSession(private = false, contextId = "1"))
+ assertNull(engine.speculativeConnectionFactory.speculativeEngineSession)
+
+ // Create a regular speculative session and make sure it is not returned
+ // if a session with a context id is requested instead.
+ engine.speculativeCreateSession(private = false)
+ assertNotNull(engine.speculativeConnectionFactory.speculativeEngineSession)
+ newSpeculativeSession = engine.speculativeConnectionFactory.speculativeEngineSession!!.engineSession
+ assertNotSame(newSpeculativeSession, engine.createSession(private = false, contextId = "1"))
+ // Make sure previous (never used) speculative session is now closed
+ assertFalse(newSpeculativeSession.geckoSession.isOpen)
+ assertNull(engine.speculativeConnectionFactory.speculativeEngineSession)
+ }
+
+ @Test
+ fun name() {
+ assertEquals("Gecko", GeckoEngine(context, runtime = runtime).name())
+ }
+
+ @Test
+ fun settings() {
+ val defaultSettings = DefaultSettings()
+ val contentBlockingSettings = ContentBlocking.Settings.Builder().build()
+ val runtime = mock<GeckoRuntime>()
+ val runtimeSettings = mock<GeckoRuntimeSettings>()
+ whenever(runtimeSettings.javaScriptEnabled).thenReturn(true)
+ whenever(runtimeSettings.webFontsEnabled).thenReturn(true)
+ whenever(runtimeSettings.automaticFontSizeAdjustment).thenReturn(true)
+ whenever(runtimeSettings.fontInflationEnabled).thenReturn(true)
+ whenever(runtimeSettings.fontSizeFactor).thenReturn(1.0F)
+ whenever(runtimeSettings.forceUserScalableEnabled).thenReturn(false)
+ whenever(runtimeSettings.loginAutofillEnabled).thenReturn(false)
+ whenever(runtimeSettings.enterpriseRootsEnabled).thenReturn(false)
+ whenever(runtimeSettings.contentBlocking).thenReturn(contentBlockingSettings)
+ whenever(runtimeSettings.preferredColorScheme).thenReturn(GeckoRuntimeSettings.COLOR_SCHEME_SYSTEM)
+ whenever(runtime.settings).thenReturn(runtimeSettings)
+ val engine = GeckoEngine(context, runtime = runtime, defaultSettings = defaultSettings)
+
+ assertTrue(engine.settings.javascriptEnabled)
+ engine.settings.javascriptEnabled = false
+ verify(runtimeSettings).javaScriptEnabled = false
+
+ assertFalse(engine.settings.loginAutofillEnabled)
+ engine.settings.loginAutofillEnabled = true
+ verify(runtimeSettings).loginAutofillEnabled = true
+
+ assertFalse(engine.settings.enterpriseRootsEnabled)
+ engine.settings.enterpriseRootsEnabled = true
+ verify(runtimeSettings).enterpriseRootsEnabled = true
+
+ assertTrue(engine.settings.webFontsEnabled)
+ engine.settings.webFontsEnabled = false
+ verify(runtimeSettings).webFontsEnabled = false
+
+ assertTrue(engine.settings.automaticFontSizeAdjustment)
+ engine.settings.automaticFontSizeAdjustment = false
+ verify(runtimeSettings).automaticFontSizeAdjustment = false
+
+ assertTrue(engine.settings.fontInflationEnabled!!)
+ engine.settings.fontInflationEnabled = null
+ verify(runtimeSettings, never()).fontInflationEnabled = anyBoolean()
+ engine.settings.fontInflationEnabled = false
+ verify(runtimeSettings).fontInflationEnabled = false
+
+ assertEquals(1.0F, engine.settings.fontSizeFactor)
+ engine.settings.fontSizeFactor = null
+ verify(runtimeSettings, never()).fontSizeFactor = anyFloat()
+ engine.settings.fontSizeFactor = 2.0F
+ verify(runtimeSettings).fontSizeFactor = 2.0F
+
+ assertFalse(engine.settings.forceUserScalableContent)
+ engine.settings.forceUserScalableContent = true
+ verify(runtimeSettings).forceUserScalableEnabled = true
+
+ assertFalse(engine.settings.remoteDebuggingEnabled)
+ engine.settings.remoteDebuggingEnabled = true
+ verify(runtimeSettings).remoteDebuggingEnabled = true
+
+ assertFalse(engine.settings.testingModeEnabled)
+ engine.settings.testingModeEnabled = true
+ assertTrue(engine.settings.testingModeEnabled)
+
+ assertEquals(PreferredColorScheme.System, engine.settings.preferredColorScheme)
+ engine.settings.preferredColorScheme = PreferredColorScheme.Dark
+ verify(runtimeSettings).preferredColorScheme = PreferredColorScheme.Dark.toGeckoValue()
+
+ assertFalse(engine.settings.suspendMediaWhenInactive)
+ engine.settings.suspendMediaWhenInactive = true
+ assertEquals(true, engine.settings.suspendMediaWhenInactive)
+
+ assertNull(engine.settings.clearColor)
+ engine.settings.clearColor = Color.BLUE
+ assertEquals(Color.BLUE, engine.settings.clearColor)
+
+ // Specifying no ua-string default should result in GeckoView's default.
+ assertEquals(GeckoSession.getDefaultUserAgent(), engine.settings.userAgentString)
+ // It also should be possible to read and set a new default.
+ engine.settings.userAgentString = engine.settings.userAgentString + "-test"
+ assertEquals(GeckoSession.getDefaultUserAgent() + "-test", engine.settings.userAgentString)
+
+ assertEquals(null, engine.settings.trackingProtectionPolicy)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ val trackingStrictCategories = TrackingProtectionPolicy.strict().trackingCategories.sumOf { it.id }
+ val artificialCategory =
+ TrackingCategory.SCRIPTS_AND_SUB_RESOURCES.id
+ assertEquals(
+ trackingStrictCategories - artificialCategory,
+ contentBlockingSettings.antiTrackingCategories,
+ )
+
+ assertFalse(engine.settings.emailTrackerBlockingPrivateBrowsing)
+ engine.settings.emailTrackerBlockingPrivateBrowsing = true
+ assertTrue(engine.settings.emailTrackerBlockingPrivateBrowsing)
+
+ val safeStrictBrowsingCategories = SafeBrowsingPolicy.RECOMMENDED.id
+ assertEquals(safeStrictBrowsingCategories, contentBlockingSettings.safeBrowsingCategories)
+
+ engine.settings.safeBrowsingPolicy = arrayOf(SafeBrowsingPolicy.PHISHING)
+ assertEquals(SafeBrowsingPolicy.PHISHING.id, contentBlockingSettings.safeBrowsingCategories)
+
+ assertEquals(defaultSettings.trackingProtectionPolicy, TrackingProtectionPolicy.strict())
+ assertEquals(contentBlockingSettings.cookieBehavior, CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS.id)
+ assertEquals(
+ contentBlockingSettings.cookieBehaviorPrivateMode,
+ CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS.id,
+ )
+
+ assertEquals(contentBlockingSettings.cookieBannerMode, EngineSession.CookieBannerHandlingMode.DISABLED.mode)
+ assertEquals(contentBlockingSettings.cookieBannerModePrivateBrowsing, EngineSession.CookieBannerHandlingMode.DISABLED.mode)
+ assertEquals(contentBlockingSettings.cookieBannerDetectOnlyMode, engine.settings.cookieBannerHandlingDetectOnlyMode)
+ assertEquals(contentBlockingSettings.cookieBannerGlobalRulesEnabled, engine.settings.cookieBannerHandlingGlobalRules)
+ assertEquals(contentBlockingSettings.cookieBannerGlobalRulesSubFramesEnabled, engine.settings.cookieBannerHandlingGlobalRulesSubFrames)
+ assertEquals(contentBlockingSettings.queryParameterStrippingEnabled, engine.settings.queryParameterStripping)
+ assertEquals(contentBlockingSettings.queryParameterStrippingPrivateBrowsingEnabled, engine.settings.queryParameterStrippingPrivateBrowsing)
+ assertEquals(contentBlockingSettings.queryParameterStrippingAllowList[0], engine.settings.queryParameterStrippingAllowList)
+ assertEquals(contentBlockingSettings.queryParameterStrippingStripList[0], engine.settings.queryParameterStrippingStripList)
+
+ assertEquals(contentBlockingSettings.emailTrackerBlockingPrivateBrowsingEnabled, engine.settings.emailTrackerBlockingPrivateBrowsing)
+
+ try {
+ engine.settings.domStorageEnabled
+ fail("Expected UnsupportedOperationException")
+ } catch (e: UnsupportedSettingException) { }
+
+ try {
+ engine.settings.domStorageEnabled = false
+ fail("Expected UnsupportedOperationException")
+ } catch (e: UnsupportedSettingException) { }
+ }
+
+ @Test
+ fun `the SCRIPTS_AND_SUB_RESOURCES tracking protection category must not be passed to gecko view`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ val trackingStrictCategories = TrackingProtectionPolicy.strict().trackingCategories.sumOf { it.id }
+ val artificialCategory = TrackingCategory.SCRIPTS_AND_SUB_RESOURCES.id
+
+ assertEquals(
+ trackingStrictCategories - artificialCategory,
+ mockRuntime.settings.contentBlocking.antiTrackingCategories,
+ )
+
+ mockRuntime.settings.contentBlocking.setAntiTracking(0)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.select(
+ arrayOf(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES),
+ )
+
+ assertEquals(0, mockRuntime.settings.contentBlocking.antiTrackingCategories)
+ }
+
+ @Test
+ fun `WHEN a strict tracking protection policy is set THEN the strict social list must be activated`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(mock())
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ verify(mockRuntime.settings.contentBlocking).setStrictSocialTrackingProtection(true)
+ }
+
+ @Test
+ fun `WHEN a strict tracking protection policy is set THEN the setEnhancedTrackingProtectionLevel must be STRICT`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(mock())
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ verify(mockRuntime.settings.contentBlocking).setEnhancedTrackingProtectionLevel(
+ ContentBlocking.EtpLevel.STRICT,
+ )
+ }
+
+ @Test
+ fun `WHEN an HTTPS-Only mode is set THEN allowInsecureConnections is getting set on GeckoRuntime`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ whenever(mockRuntime.settings).thenReturn(mock())
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ reset(mockRuntime.settings)
+ engine.settings.httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED_PRIVATE_ONLY
+ verify(mockRuntime.settings).allowInsecureConnections = GeckoRuntimeSettings.HTTPS_ONLY_PRIVATE
+
+ reset(mockRuntime.settings)
+ engine.settings.httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
+ verify(mockRuntime.settings).allowInsecureConnections = GeckoRuntimeSettings.HTTPS_ONLY
+
+ reset(mockRuntime.settings)
+ engine.settings.httpsOnlyMode = Engine.HttpsOnlyMode.DISABLED
+ verify(mockRuntime.settings).allowInsecureConnections = GeckoRuntimeSettings.ALLOW_ALL
+ }
+
+ @Test
+ fun `setAntiTracking is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+ val policy = TrackingProtectionPolicy.recommended()
+
+ engine.settings.trackingProtectionPolicy = policy
+
+ verify(mockRuntime.settings.contentBlocking).setAntiTracking(
+ policy.getAntiTrackingPolicy(),
+ )
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = policy
+
+ verify(mockRuntime.settings.contentBlocking, never()).setAntiTracking(
+ policy.getAntiTrackingPolicy(),
+ )
+ }
+
+ @Test
+ fun `cookiePurging is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+ val policy = TrackingProtectionPolicy.recommended()
+
+ engine.settings.trackingProtectionPolicy = policy
+
+ verify(mockRuntime.settings.contentBlocking).setCookiePurging(policy.cookiePurging)
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = policy
+
+ verify(mockRuntime.settings.contentBlocking, never()).setCookiePurging(policy.cookiePurging)
+ }
+
+ @Test
+ fun `setCookieBehavior is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+ whenever(mockRuntime.settings.contentBlocking.cookieBehavior).thenReturn(CookieBehavior.ACCEPT_NONE)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+ val policy = TrackingProtectionPolicy.recommended()
+
+ engine.settings.trackingProtectionPolicy = policy
+
+ verify(mockRuntime.settings.contentBlocking).setCookieBehavior(
+ policy.cookiePolicy.id,
+ )
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = policy
+
+ verify(mockRuntime.settings.contentBlocking, never()).setCookieBehavior(
+ policy.cookiePolicy.id,
+ )
+ }
+
+ @Test
+ fun `setCookieBehavior private mode is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+ whenever(mockRuntime.settings.contentBlocking.cookieBehaviorPrivateMode).thenReturn(CookieBehavior.ACCEPT_NONE)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+ val policy = TrackingProtectionPolicy.recommended()
+
+ engine.settings.trackingProtectionPolicy = policy
+
+ verify(mockRuntime.settings.contentBlocking).setCookieBehaviorPrivateMode(
+ policy.cookiePolicy.id,
+ )
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = policy
+
+ verify(mockRuntime.settings.contentBlocking, never()).setCookieBehaviorPrivateMode(
+ policy.cookiePolicy.id,
+ )
+ }
+
+ @Test
+ fun `setCookieBannerMode is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+ val policy = EngineSession.CookieBannerHandlingMode.REJECT_ALL
+
+ engine.settings.cookieBannerHandlingMode = policy
+
+ verify(mockRuntime.settings.contentBlocking).setCookieBannerMode(policy.mode)
+
+ reset(settings)
+
+ engine.settings.cookieBannerHandlingMode = policy
+
+ verify(mockRuntime.settings.contentBlocking, never()).setCookieBannerMode(policy.mode)
+ }
+
+ @Test
+ fun `setCookieBannerModePrivateBrowsing is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+ val policy = EngineSession.CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL
+
+ engine.settings.cookieBannerHandlingModePrivateBrowsing = policy
+
+ verify(mockRuntime.settings.contentBlocking).setCookieBannerModePrivateBrowsing(policy.mode)
+
+ reset(settings)
+
+ engine.settings.cookieBannerHandlingModePrivateBrowsing = policy
+
+ verify(mockRuntime.settings.contentBlocking, never()).setCookieBannerModePrivateBrowsing(policy.mode)
+ }
+
+ @Test
+ fun `setCookieBannerHandlingDetectOnlyMode is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.cookieBannerHandlingDetectOnlyMode = true
+
+ verify(mockRuntime.settings.contentBlocking).setCookieBannerDetectOnlyMode(true)
+
+ reset(settings)
+
+ engine.settings.cookieBannerHandlingDetectOnlyMode = true
+
+ verify(mockRuntime.settings.contentBlocking, never()).setCookieBannerDetectOnlyMode(true)
+ }
+
+ @Test
+ fun `setCookieBannerHandlingGlobalRules is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.cookieBannerHandlingGlobalRules = true
+
+ verify(mockRuntime.settings.contentBlocking).setCookieBannerGlobalRulesEnabled(true)
+
+ reset(settings)
+
+ engine.settings.cookieBannerHandlingGlobalRules = true
+
+ verify(mockRuntime.settings.contentBlocking, never()).setCookieBannerGlobalRulesEnabled(true)
+ }
+
+ @Test
+ fun `setCookieBannerHandlingGlobalRulesSubFrames is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.cookieBannerHandlingGlobalRulesSubFrames = true
+
+ verify(mockRuntime.settings.contentBlocking).setCookieBannerGlobalRulesSubFramesEnabled(true)
+
+ reset(settings)
+
+ engine.settings.cookieBannerHandlingGlobalRulesSubFrames = true
+
+ verify(mockRuntime.settings.contentBlocking, never()).setCookieBannerGlobalRulesSubFramesEnabled(true)
+ }
+
+ @Test
+ fun `setQueryParameterStripping is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.queryParameterStripping = true
+
+ verify(mockRuntime.settings.contentBlocking).setQueryParameterStrippingEnabled(true)
+
+ reset(settings)
+
+ engine.settings.queryParameterStripping = true
+
+ verify(mockRuntime.settings.contentBlocking, never()).setQueryParameterStrippingEnabled(true)
+ }
+
+ @Test
+ fun `setQueryParameterStrippingPrivateBrowsingEnabled is only invoked when the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.queryParameterStrippingPrivateBrowsing = true
+
+ verify(mockRuntime.settings.contentBlocking).setQueryParameterStrippingPrivateBrowsingEnabled(true)
+
+ reset(settings)
+
+ engine.settings.queryParameterStrippingPrivateBrowsing = true
+
+ verify(mockRuntime.settings.contentBlocking, never()).setQueryParameterStrippingPrivateBrowsingEnabled(true)
+ }
+
+ @Test
+ fun `emailTrackerBlockingPrivateBrowsing is only invoked with the value is changed`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.emailTrackerBlockingPrivateBrowsing = true
+
+ verify(mockRuntime.settings.contentBlocking).setEmailTrackerBlockingPrivateBrowsing(true)
+
+ reset(settings)
+
+ engine.settings.emailTrackerBlockingPrivateBrowsing = true
+
+ verify(mockRuntime.settings.contentBlocking, never()).setEmailTrackerBlockingPrivateBrowsing(true)
+ }
+
+ @Test
+ fun `Cookie banner handling settings are aligned`() {
+ assertEquals(ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_DISABLED, EngineSession.CookieBannerHandlingMode.DISABLED.mode)
+ assertEquals(ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_REJECT, EngineSession.CookieBannerHandlingMode.REJECT_ALL.mode)
+ assertEquals(ContentBlocking.CookieBannerMode.COOKIE_BANNER_MODE_REJECT_OR_ACCEPT, EngineSession.CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL.mode)
+ }
+
+ @Test
+ fun `setEnhancedTrackingProtectionLevel MUST always be set to STRICT unless the tracking protection policy is none`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ val settings = spy(ContentBlocking.Settings.Builder().build())
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(settings)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.recommended()
+
+ verify(mockRuntime.settings.contentBlocking).setEnhancedTrackingProtectionLevel(
+ ContentBlocking.EtpLevel.STRICT,
+ )
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.recommended()
+
+ verify(mockRuntime.settings.contentBlocking, never()).setEnhancedTrackingProtectionLevel(
+ ContentBlocking.EtpLevel.STRICT,
+ )
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ verify(mockRuntime.settings.contentBlocking, never()).setEnhancedTrackingProtectionLevel(
+ ContentBlocking.EtpLevel.STRICT,
+ )
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.none()
+ verify(mockRuntime.settings.contentBlocking).setEnhancedTrackingProtectionLevel(
+ ContentBlocking.EtpLevel.NONE,
+ )
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.none()
+ verify(mockRuntime.settings.contentBlocking, never()).setEnhancedTrackingProtectionLevel(
+ ContentBlocking.EtpLevel.NONE,
+ )
+
+ reset(settings)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ verify(mockRuntime.settings.contentBlocking).setEnhancedTrackingProtectionLevel(
+ ContentBlocking.EtpLevel.STRICT,
+ )
+ }
+
+ @Test
+ fun `WHEN a non strict tracking protection policy is set THEN the strict social list must be disabled`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking.strictSocialTrackingProtection).thenReturn(true)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.recommended()
+
+ verify(mockRuntime.settings.contentBlocking).setStrictSocialTrackingProtection(false)
+ }
+
+ @Test
+ fun `WHEN strict social tracking protection is set to true THEN the strict social list must be activated`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(mock())
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.select(
+ strictSocialTrackingProtection = true,
+ )
+
+ verify(mockRuntime.settings.contentBlocking).setStrictSocialTrackingProtection(true)
+ }
+
+ @Test
+ fun `WHEN strict social tracking protection is set to false THEN the strict social list must be disabled`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ whenever(mockRuntime.settings).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking).thenReturn(mock())
+ whenever(mockRuntime.settings.contentBlocking.strictSocialTrackingProtection).thenReturn(true)
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.select(
+ strictSocialTrackingProtection = false,
+ )
+
+ verify(mockRuntime.settings.contentBlocking).setStrictSocialTrackingProtection(false)
+ }
+
+ @Test
+ fun defaultSettings() {
+ val runtime = mock<GeckoRuntime>()
+ val runtimeSettings = mock<GeckoRuntimeSettings>()
+ val contentBlockingSettings = ContentBlocking.Settings.Builder().build()
+ whenever(runtimeSettings.javaScriptEnabled).thenReturn(true)
+ whenever(runtime.settings).thenReturn(runtimeSettings)
+ whenever(runtimeSettings.contentBlocking).thenReturn(contentBlockingSettings)
+ whenever(runtimeSettings.fontInflationEnabled).thenReturn(true)
+
+ val engine = GeckoEngine(
+ context,
+ DefaultSettings(
+ trackingProtectionPolicy = TrackingProtectionPolicy.strict(),
+ javascriptEnabled = false,
+ webFontsEnabled = false,
+ automaticFontSizeAdjustment = false,
+ fontInflationEnabled = false,
+ fontSizeFactor = 2.0F,
+ remoteDebuggingEnabled = true,
+ testingModeEnabled = true,
+ userAgentString = "test-ua",
+ preferredColorScheme = PreferredColorScheme.Light,
+ suspendMediaWhenInactive = true,
+ forceUserScalableContent = false,
+ ),
+ runtime,
+ )
+
+ verify(runtimeSettings).javaScriptEnabled = false
+ verify(runtimeSettings).webFontsEnabled = false
+ verify(runtimeSettings).automaticFontSizeAdjustment = false
+ verify(runtimeSettings).fontInflationEnabled = false
+ verify(runtimeSettings).fontSizeFactor = 2.0F
+ verify(runtimeSettings).remoteDebuggingEnabled = true
+ verify(runtimeSettings).forceUserScalableEnabled = false
+
+ val trackingStrictCategories = TrackingProtectionPolicy.strict().trackingCategories.sumOf { it.id }
+ val artificialCategory =
+ TrackingCategory.SCRIPTS_AND_SUB_RESOURCES.id
+ assertEquals(
+ trackingStrictCategories - artificialCategory,
+ contentBlockingSettings.antiTrackingCategories,
+ )
+
+ assertEquals(SafeBrowsingPolicy.RECOMMENDED.id, contentBlockingSettings.safeBrowsingCategories)
+
+ assertEquals(CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS.id, contentBlockingSettings.cookieBehavior)
+ assertEquals(
+ CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS.id,
+ contentBlockingSettings.cookieBehaviorPrivateMode,
+ )
+ assertTrue(engine.settings.testingModeEnabled)
+ assertEquals("test-ua", engine.settings.userAgentString)
+ assertEquals(PreferredColorScheme.Light, engine.settings.preferredColorScheme)
+ assertTrue(engine.settings.suspendMediaWhenInactive)
+
+ engine.settings.safeBrowsingPolicy = arrayOf(SafeBrowsingPolicy.PHISHING)
+ engine.settings.trackingProtectionPolicy =
+ TrackingProtectionPolicy.select(
+ trackingCategories = arrayOf(TrackingCategory.AD),
+ cookiePolicy = CookiePolicy.ACCEPT_ONLY_FIRST_PARTY,
+ )
+
+ assertEquals(
+ TrackingCategory.AD.id,
+ contentBlockingSettings.antiTrackingCategories,
+ )
+
+ assertEquals(
+ SafeBrowsingPolicy.PHISHING.id,
+ contentBlockingSettings.safeBrowsingCategories,
+ )
+
+ assertEquals(
+ CookiePolicy.ACCEPT_ONLY_FIRST_PARTY.id,
+ contentBlockingSettings.cookieBehavior,
+ )
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.none()
+
+ assertEquals(CookiePolicy.ACCEPT_ALL.id, contentBlockingSettings.cookieBehavior)
+
+ assertEquals(EngineSession.CookieBannerHandlingMode.DISABLED.mode, contentBlockingSettings.cookieBannerMode)
+ assertEquals(EngineSession.CookieBannerHandlingMode.DISABLED.mode, contentBlockingSettings.cookieBannerModePrivateBrowsing)
+ }
+
+ @Test
+ fun `speculativeConnect forwards call to executor`() {
+ val executor: GeckoWebExecutor = mock()
+
+ val engine = GeckoEngine(context, runtime = runtime, executorProvider = { executor })
+
+ engine.speculativeConnect("https://www.mozilla.org")
+
+ verify(executor).speculativeConnect("https://www.mozilla.org")
+ }
+
+ @Test
+ fun `install built-in web extension successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "resource://android/assets/extensions/test"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onSuccessCalled = false
+ var onErrorCalled = false
+ val result = GeckoResult<GeckoWebExtension>()
+
+ whenever(extensionController.ensureBuiltIn(extUrl, extId)).thenReturn(result)
+ engine.installBuiltInWebExtension(
+ extId,
+ extUrl,
+ onSuccess = { onSuccessCalled = true },
+ onError = { _ -> onErrorCalled = true },
+ )
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ val extUrlCaptor = argumentCaptor<String>()
+ val extIdCaptor = argumentCaptor<String>()
+ verify(extensionController).ensureBuiltIn(extUrlCaptor.capture(), extIdCaptor.capture())
+ assertEquals(extUrl, extUrlCaptor.value)
+ assertEquals(extId, extIdCaptor.value)
+ assertTrue(onSuccessCalled)
+ assertFalse(onErrorCalled)
+ }
+
+ @Test
+ fun `add optional permissions to a web extension successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "resource://android/assets/extensions/test"
+ val permissions = listOf("permission1")
+ val origin = listOf("origin")
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onSuccessCalled = false
+ var onErrorCalled = false
+ val result = GeckoResult<GeckoWebExtension>()
+
+ whenever(
+ extensionController.addOptionalPermissions(
+ extId,
+ permissions.toTypedArray(),
+ origin.toTypedArray(),
+ ),
+ ).thenReturn(
+ result,
+ )
+ engine.addOptionalPermissions(
+ extId,
+ permissions,
+ origin,
+ onSuccess = { onSuccessCalled = true },
+ onError = { _ -> onErrorCalled = true },
+ )
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(extensionController).addOptionalPermissions(anyString(), any(), any())
+ assertTrue(onSuccessCalled)
+ assertFalse(onErrorCalled)
+ }
+
+ @Test
+ fun `addOptionalPermissions with empty permissions and origins with `() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onErrorCalled = false
+
+ engine.addOptionalPermissions(
+ extId,
+ emptyList(),
+ emptyList(),
+ onError = { _ -> onErrorCalled = true },
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onErrorCalled)
+ }
+
+ @Test
+ fun `remove optional permissions to a web extension successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "resource://android/assets/extensions/test"
+ val permissions = listOf("permission1")
+ val origin = listOf("origin")
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onSuccessCalled = false
+ var onErrorCalled = false
+ val result = GeckoResult<GeckoWebExtension>()
+
+ whenever(
+ extensionController.removeOptionalPermissions(
+ extId,
+ permissions.toTypedArray(),
+ origin.toTypedArray(),
+ ),
+ ).thenReturn(
+ result,
+ )
+ engine.removeOptionalPermissions(
+ extId,
+ permissions,
+ origin,
+ onSuccess = { onSuccessCalled = true },
+ onError = { _ -> onErrorCalled = true },
+ )
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(extensionController).removeOptionalPermissions(anyString(), any(), any())
+ assertTrue(onSuccessCalled)
+ assertFalse(onErrorCalled)
+ }
+
+ @Test
+ fun `removeOptionalPermissions with empty permissions and origins with `() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onErrorCalled = false
+
+ engine.removeOptionalPermissions(
+ extId,
+ emptyList(),
+ emptyList(),
+ onError = { _ -> onErrorCalled = true },
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onErrorCalled)
+ }
+
+ @Test
+ fun `install external web extension successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onSuccessCalled = false
+ var onErrorCalled = false
+ val result = GeckoResult<GeckoWebExtension>()
+
+ whenever(extensionController.install(any(), any())).thenReturn(result)
+ engine.installWebExtension(
+ extUrl,
+ onSuccess = { onSuccessCalled = true },
+ onError = { _ -> onErrorCalled = true },
+ )
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ val extCaptor = argumentCaptor<String>()
+ verify(extensionController).install(extCaptor.capture(), any())
+ assertEquals(extUrl, extCaptor.value)
+ assertTrue(onSuccessCalled)
+ assertFalse(onErrorCalled)
+ }
+
+ @Test
+ fun `install built-in web extension failure`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "resource://android/assets/extensions/test"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onErrorCalled = false
+ val expected = IOException()
+ val result = GeckoResult<GeckoWebExtension>()
+
+ var throwable: Throwable? = null
+ whenever(extensionController.ensureBuiltIn(extUrl, extId)).thenReturn(result)
+ engine.installBuiltInWebExtension(extId, extUrl) { e ->
+ onErrorCalled = true
+ throwable = e
+ }
+ result.completeExceptionally(expected)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onErrorCalled)
+ assertTrue(throwable is GeckoWebExtensionException)
+ }
+
+ @Test
+ fun `install external web extension failure`() {
+ val runtime = mock<GeckoRuntime>()
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onErrorCalled = false
+ val expected = IOException()
+ val result = GeckoResult<GeckoWebExtension>()
+
+ var throwable: Throwable? = null
+ whenever(extensionController.install(any(), any())).thenReturn(result)
+ engine.installWebExtension(extUrl) { e ->
+ onErrorCalled = true
+ throwable = e
+ }
+ result.completeExceptionally(expected)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onErrorCalled)
+ assertTrue(throwable is GeckoWebExtensionException)
+ }
+
+ @Test
+ fun `install web extension with installation method manager`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val result = GeckoResult<GeckoWebExtension>()
+
+ whenever(extensionController.install(any(), any())).thenReturn(result)
+
+ engine.installWebExtension(
+ extUrl,
+ InstallationMethod.MANAGER,
+ )
+
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ val methodCaptor = argumentCaptor<String>()
+
+ verify(extensionController).install(any(), methodCaptor.capture())
+
+ assertEquals(WebExtensionController.INSTALLATION_METHOD_MANAGER, methodCaptor.value)
+ }
+
+ @Test
+ fun `install web extension with installation method file`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val result = GeckoResult<GeckoWebExtension>()
+
+ whenever(extensionController.install(any(), any())).thenReturn(result)
+
+ engine.installWebExtension(
+ extUrl,
+ InstallationMethod.FROM_FILE,
+ )
+
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ val methodCaptor = argumentCaptor<String>()
+
+ verify(extensionController).install(any(), methodCaptor.capture())
+
+ assertEquals(WebExtensionController.INSTALLATION_METHOD_FROM_FILE, methodCaptor.value)
+ }
+
+ @Test
+ fun `install web extension with null installation method`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val result = GeckoResult<GeckoWebExtension>()
+
+ whenever(extensionController.install(any(), any())).thenReturn(result)
+
+ engine.installWebExtension(
+ extUrl,
+ null,
+ )
+
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ val methodCaptor = argumentCaptor<String>()
+
+ verify(extensionController).install(any(), methodCaptor.capture())
+
+ assertNull(methodCaptor.value)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `installWebExtension should throw when a resource URL is passed`() {
+ val engine = GeckoEngine(context, runtime = mock())
+ engine.installWebExtension("resource://android/assets/extensions/test")
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `installBuiltInWebExtension should throw when a non-resource URL is passed`() {
+ val engine = GeckoEngine(context, runtime = mock())
+ engine.installBuiltInWebExtension(id = "id", url = "https://addons.mozilla.org/1/some_web_ext.xpi")
+ }
+
+ @Test
+ fun `uninstall web extension successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val nativeExtension = mockNativeWebExtension("test-webext", "https://addons.mozilla.org/1/some_web_ext.xpi")
+ val ext = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ nativeExtension,
+ runtime,
+ )
+
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+ val result = GeckoResult<Void>()
+
+ whenever(extensionController.uninstall(any())).thenReturn(result)
+ engine.uninstallWebExtension(
+ ext,
+ onSuccess = { onSuccessCalled = true },
+ onError = { _, _ -> onErrorCalled = true },
+ )
+ result.complete(null)
+
+ shadowOf(getMainLooper()).idle()
+
+ val extCaptor = argumentCaptor<GeckoWebExtension>()
+ verify(extensionController).uninstall(extCaptor.capture())
+ assertSame(nativeExtension, extCaptor.value)
+ assertTrue(onSuccessCalled)
+ assertFalse(onErrorCalled)
+ }
+
+ @Test
+ fun `uninstall web extension failure`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val nativeExtension = mockNativeWebExtension(
+ "test-webext",
+ "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi",
+ )
+ val ext = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ nativeExtension,
+ runtime,
+ )
+
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ var onErrorCalled = false
+ val expected = IOException()
+ val result = GeckoResult<Void>()
+
+ var throwable: Throwable? = null
+ whenever(extensionController.uninstall(any())).thenReturn(result)
+ engine.uninstallWebExtension(ext) { _, e ->
+ onErrorCalled = true
+ throwable = e
+ }
+ result.completeExceptionally(expected)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onErrorCalled)
+ assertEquals(expected, throwable)
+ }
+
+ @Test
+ fun `web extension delegate handles installation of built-in extensions`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val extId = "test-webext"
+ val extUrl = "resource://android/assets/extensions/test"
+ val result = GeckoResult<GeckoWebExtension>()
+ whenever(webExtensionController.ensureBuiltIn(extUrl, extId)).thenReturn(result)
+ engine.installBuiltInWebExtension(extId, extUrl)
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ val extCaptor = argumentCaptor<WebExtension>()
+ verify(webExtensionsDelegate).onInstalled(extCaptor.capture())
+ assertEquals(extId, extCaptor.value.id)
+ assertEquals(extUrl, extCaptor.value.url)
+ }
+
+ @Test
+ fun `web extension delegate handles installation of external extensions`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val extId = "test-webext"
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/123/some_web_ext.xpi"
+ val result = GeckoResult<GeckoWebExtension>()
+ whenever(webExtensionController.install(any(), any())).thenReturn(result)
+ engine.installWebExtension(extUrl)
+ result.complete(mockNativeWebExtension(extId, extUrl))
+
+ shadowOf(getMainLooper()).idle()
+
+ val extCaptor = argumentCaptor<WebExtension>()
+ verify(webExtensionsDelegate).onInstalled(extCaptor.capture())
+ assertEquals(extId, extCaptor.value.id)
+ assertEquals(extUrl, extCaptor.value.url)
+ }
+
+ @Test
+ fun `GIVEN approved permissions prompt WHEN onInstallPermissionRequest THEN delegate is called with allow`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
+ verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
+
+ val result = geckoDelegateCaptor.value.onInstallPrompt(extension)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val onConfirmCaptor = argumentCaptor<((Boolean) -> Unit)>()
+
+ verify(webExtensionsDelegate).onInstallPermissionRequest(extensionCaptor.capture(), onConfirmCaptor.capture())
+
+ onConfirmCaptor.value(true)
+
+ assertEquals(GeckoResult.allow(), result)
+ }
+
+ @Test
+ fun `GIVEN denied permissions prompt WHEN onInstallPermissionRequest THEN delegate is called with deny`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
+ verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
+
+ val result = geckoDelegateCaptor.value.onInstallPrompt(extension)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val onConfirmCaptor = argumentCaptor<((Boolean) -> Unit)>()
+
+ verify(webExtensionsDelegate).onInstallPermissionRequest(extensionCaptor.capture(), onConfirmCaptor.capture())
+
+ onConfirmCaptor.value(false)
+
+ assertEquals(GeckoResult.deny(), result)
+ }
+
+ @Test
+ fun `web extension delegate handles update prompt`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val currentExtension = mockNativeWebExtension("test", "uri")
+ val updatedExtension = mockNativeWebExtension("testUpdated", "uri")
+ val updatedPermissions = arrayOf("p1", "p2")
+ val hostPermissions = arrayOf("p3", "p4")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
+ verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
+
+ val result = geckoDelegateCaptor.value.onUpdatePrompt(
+ currentExtension,
+ updatedExtension,
+ updatedPermissions,
+ hostPermissions,
+ )
+ assertNotNull(result)
+
+ val currentExtensionCaptor = argumentCaptor<WebExtension>()
+ val updatedExtensionCaptor = argumentCaptor<WebExtension>()
+ val onPermissionsGrantedCaptor = argumentCaptor<((Boolean) -> Unit)>()
+ verify(webExtensionsDelegate).onUpdatePermissionRequest(
+ currentExtensionCaptor.capture(),
+ updatedExtensionCaptor.capture(),
+ eq(updatedPermissions.toList() + hostPermissions.toList()),
+ onPermissionsGrantedCaptor.capture(),
+ )
+ val current =
+ currentExtensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(currentExtension, current.nativeExtension)
+ val updated =
+ updatedExtensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(updatedExtension, updated.nativeExtension)
+
+ onPermissionsGrantedCaptor.value.invoke(true)
+ assertEquals(GeckoResult.allow(), result)
+ }
+
+ @Test
+ fun `web extension delegate handles update prompt with empty host permissions`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val currentExtension = mockNativeWebExtension("test", "uri")
+ val updatedExtension = mockNativeWebExtension("testUpdated", "uri")
+ val updatedPermissions = arrayOf("p1", "p2")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
+ verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
+
+ val result = geckoDelegateCaptor.value.onUpdatePrompt(
+ currentExtension,
+ updatedExtension,
+ updatedPermissions,
+ emptyArray(),
+ )
+ assertNotNull(result)
+
+ val currentExtensionCaptor = argumentCaptor<WebExtension>()
+ val updatedExtensionCaptor = argumentCaptor<WebExtension>()
+ val onPermissionsGrantedCaptor = argumentCaptor<((Boolean) -> Unit)>()
+ verify(webExtensionsDelegate).onUpdatePermissionRequest(
+ currentExtensionCaptor.capture(),
+ updatedExtensionCaptor.capture(),
+ eq(updatedPermissions.toList()),
+ onPermissionsGrantedCaptor.capture(),
+ )
+ val current =
+ currentExtensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(currentExtension, current.nativeExtension)
+ val updated =
+ updatedExtensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(updatedExtension, updated.nativeExtension)
+
+ onPermissionsGrantedCaptor.value.invoke(true)
+ assertEquals(GeckoResult.allow(), result)
+ }
+
+ @Test
+ fun `web extension delegate handles optional permissions prompt - allow`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val permissions = arrayOf("p1", "p2")
+ val origins = arrayOf("p3", "p4")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
+ verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
+
+ val result = geckoDelegateCaptor.value.onOptionalPrompt(extension, permissions, origins)
+ assertNotNull(result)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val onPermissionsGrantedCaptor = argumentCaptor<((Boolean) -> Unit)>()
+ verify(webExtensionsDelegate).onOptionalPermissionsRequest(
+ extensionCaptor.capture(),
+ eq(permissions.toList() + origins.toList()),
+ onPermissionsGrantedCaptor.capture(),
+ )
+ val current = extensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(extension, current.nativeExtension)
+
+ onPermissionsGrantedCaptor.value.invoke(true)
+ assertEquals(GeckoResult.allow(), result)
+ }
+
+ @Test
+ fun `web extension delegate handles optional permissions prompt - deny`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val permissions = arrayOf("p1", "p2")
+ val origins = emptyArray<String>()
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.PromptDelegate>()
+ verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture()
+
+ val result = geckoDelegateCaptor.value.onOptionalPrompt(extension, permissions, origins)
+ assertNotNull(result)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val onPermissionsGrantedCaptor = argumentCaptor<((Boolean) -> Unit)>()
+ verify(webExtensionsDelegate).onOptionalPermissionsRequest(
+ extensionCaptor.capture(),
+ eq(permissions.toList() + origins.toList()),
+ onPermissionsGrantedCaptor.capture(),
+ )
+ val current = extensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(extension, current.nativeExtension)
+
+ onPermissionsGrantedCaptor.value.invoke(false)
+ assertEquals(GeckoResult.deny(), result)
+ }
+
+ @Test
+ fun `web extension delegate notified of browser actions from built-in extensions`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "resource://android/assets/extensions/test"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val result = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.ensureBuiltIn(extUrl, extId)).thenReturn(result)
+ engine.installBuiltInWebExtension(extId, extUrl)
+ val extension = mockNativeWebExtension(extId, extUrl)
+ result.complete(extension)
+
+ shadowOf(getMainLooper()).idle()
+
+ val actionDelegateCaptor = argumentCaptor<org.mozilla.geckoview.WebExtension.ActionDelegate>()
+ verify(extension).setActionDelegate(actionDelegateCaptor.capture())
+
+ val browserAction: org.mozilla.geckoview.WebExtension.Action = mock()
+ actionDelegateCaptor.value.onBrowserAction(extension, null, browserAction)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val actionCaptor = argumentCaptor<Action>()
+ verify(webExtensionsDelegate).onBrowserActionDefined(extensionCaptor.capture(), actionCaptor.capture())
+ assertEquals(extId, extensionCaptor.value.id)
+
+ actionCaptor.value.onClick()
+ verify(browserAction).click()
+ }
+
+ @Test
+ fun `web extension delegate notified of page actions from built-in extensions`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "resource://android/assets/extensions/test"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val result = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.ensureBuiltIn(extUrl, extId)).thenReturn(result)
+ engine.installBuiltInWebExtension(extId, extUrl)
+ val extension = mockNativeWebExtension(extId, extUrl)
+ result.complete(extension)
+
+ shadowOf(getMainLooper()).idle()
+
+ val actionDelegateCaptor = argumentCaptor<org.mozilla.geckoview.WebExtension.ActionDelegate>()
+ verify(extension).setActionDelegate(actionDelegateCaptor.capture())
+
+ val pageAction: org.mozilla.geckoview.WebExtension.Action = mock()
+ actionDelegateCaptor.value.onPageAction(extension, null, pageAction)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val actionCaptor = argumentCaptor<Action>()
+ verify(webExtensionsDelegate).onPageActionDefined(extensionCaptor.capture(), actionCaptor.capture())
+ assertEquals(extId, extensionCaptor.value.id)
+
+ actionCaptor.value.onClick()
+ verify(pageAction).click()
+ }
+
+ @Test
+ fun `web extension delegate notified when built-in extension wants to open tab`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "resource://android/assets/extensions/test"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val result = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.ensureBuiltIn(extUrl, extId)).thenReturn(result)
+ engine.installBuiltInWebExtension(extId, extUrl)
+ val extension = mockNativeWebExtension(extId, extUrl)
+ result.complete(extension)
+
+ shadowOf(getMainLooper()).idle()
+
+ val tabDelegateCaptor = argumentCaptor<org.mozilla.geckoview.WebExtension.TabDelegate>()
+ verify(extension).tabDelegate = tabDelegateCaptor.capture()
+
+ val createTabDetails: org.mozilla.geckoview.WebExtension.CreateTabDetails = mock()
+ tabDelegateCaptor.value.onNewTab(extension, createTabDetails)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""))
+ assertEquals(extId, extensionCaptor.value.id)
+ }
+
+ @Test
+ fun `web extension delegate notified of browser actions from external extensions`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val result = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.install(any(), any())).thenReturn(result)
+ engine.installWebExtension(extUrl)
+ val extension = mockNativeWebExtension(extId, extUrl)
+ result.complete(extension)
+
+ shadowOf(getMainLooper()).idle()
+
+ val actionDelegateCaptor = argumentCaptor<org.mozilla.geckoview.WebExtension.ActionDelegate>()
+ verify(extension).setActionDelegate(actionDelegateCaptor.capture())
+
+ val browserAction: org.mozilla.geckoview.WebExtension.Action = mock()
+ actionDelegateCaptor.value.onBrowserAction(extension, null, browserAction)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val actionCaptor = argumentCaptor<Action>()
+ verify(webExtensionsDelegate).onBrowserActionDefined(extensionCaptor.capture(), actionCaptor.capture())
+ assertEquals(extId, extensionCaptor.value.id)
+
+ actionCaptor.value.onClick()
+ verify(browserAction).click()
+ }
+
+ @Test
+ fun `web extension delegate notified of page actions from external extensions`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val result = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.install(any(), any())).thenReturn(result)
+ engine.installWebExtension(extUrl)
+ val extension = mockNativeWebExtension(extId, extUrl)
+ result.complete(extension)
+
+ shadowOf(getMainLooper()).idle()
+
+ val actionDelegateCaptor = argumentCaptor<org.mozilla.geckoview.WebExtension.ActionDelegate>()
+ verify(extension).setActionDelegate(actionDelegateCaptor.capture())
+
+ val pageAction: org.mozilla.geckoview.WebExtension.Action = mock()
+ actionDelegateCaptor.value.onPageAction(extension, null, pageAction)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val actionCaptor = argumentCaptor<Action>()
+ verify(webExtensionsDelegate).onPageActionDefined(extensionCaptor.capture(), actionCaptor.capture())
+ assertEquals(extId, extensionCaptor.value.id)
+
+ actionCaptor.value.onClick()
+ verify(pageAction).click()
+ }
+
+ @Test
+ fun `web extension delegate notified when external extension wants to open tab`() {
+ val runtime = mock<GeckoRuntime>()
+ val extId = "test-webext"
+ val extUrl = "https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi"
+
+ val extensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val result = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.install(any(), any())).thenReturn(result)
+ engine.installWebExtension(extUrl)
+ val extension = mockNativeWebExtension(extId, extUrl)
+ result.complete(extension)
+
+ shadowOf(getMainLooper()).idle()
+
+ val tabDelegateCaptor = argumentCaptor<org.mozilla.geckoview.WebExtension.TabDelegate>()
+ verify(extension).tabDelegate = tabDelegateCaptor.capture()
+
+ val createTabDetails: org.mozilla.geckoview.WebExtension.CreateTabDetails = mock()
+ tabDelegateCaptor.value.onNewTab(extension, createTabDetails)
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""))
+ assertEquals(extId, extensionCaptor.value.id)
+ }
+
+ @Test
+ fun `web extension delegate notified of extension list change`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val debuggerDelegateCaptor = argumentCaptor<WebExtensionController.DebuggerDelegate>()
+ verify(webExtensionController).setDebuggerDelegate(debuggerDelegateCaptor.capture())
+
+ debuggerDelegateCaptor.value.onExtensionListUpdated()
+ verify(webExtensionsDelegate).onExtensionListUpdated()
+ }
+
+ @Test
+ fun `web extension delegate notified of extension process spawning disabled`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val webExtensionDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionDelegate)
+
+ val extensionProcessDelegate = argumentCaptor<WebExtensionController.ExtensionProcessDelegate>()
+ verify(webExtensionController).setExtensionProcessDelegate(extensionProcessDelegate.capture())
+
+ extensionProcessDelegate.value.onDisabledProcessSpawning()
+ verify(webExtensionDelegate).onDisabledExtensionProcessSpawning()
+ }
+
+ @Test
+ fun `update web extension successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val updatedExtension = mockNativeWebExtension()
+ val updateExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.update(any())).thenReturn(updateExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ var onErrorCalled = false
+
+ engine.updateWebExtension(
+ extension,
+ onSuccess = { result = it },
+ onError = { _, _ -> onErrorCalled = true },
+ )
+ updateExtensionResult.complete(updatedExtension)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onErrorCalled)
+ assertNotNull(result)
+ }
+
+ @Test
+ fun `try to update a web extension without a new update available`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val updateExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.update(any())).thenReturn(updateExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ var onErrorCalled = false
+
+ engine.updateWebExtension(
+ extension,
+ onSuccess = { result = it },
+ onError = { _, _ -> onErrorCalled = true },
+ )
+ updateExtensionResult.complete(null)
+
+ assertFalse(onErrorCalled)
+ assertNull(result)
+ }
+
+ @Test
+ fun `update web extension failure`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val updateExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.update(any())).thenReturn(updateExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ val expected = IOException()
+ var throwable: Throwable? = null
+
+ engine.updateWebExtension(
+ extension,
+ onSuccess = { result = it },
+ onError = { _, e -> throwable = e },
+ )
+ updateExtensionResult.completeExceptionally(expected)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertSame(expected, throwable!!.cause)
+ assertNull(result)
+ }
+
+ @Test
+ fun `failures when updating MUST indicate if they are recoverable`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ val performUpdate: (GeckoInstallException) -> WebExtensionException = { exception ->
+ val updateExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.update(any())).thenReturn(updateExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+ var throwable: WebExtensionException? = null
+
+ engine.updateWebExtension(
+ extension,
+ onError = { _, e ->
+ throwable = e as WebExtensionException
+ },
+ )
+
+ updateExtensionResult.completeExceptionally(exception)
+
+ shadowOf(getMainLooper()).idle()
+
+ throwable!!
+ }
+
+ val unrecoverableExceptions = listOf(
+ mockGeckoInstallException(ERROR_NETWORK_FAILURE),
+ mockGeckoInstallException(ERROR_INCORRECT_HASH),
+ mockGeckoInstallException(ERROR_CORRUPT_FILE),
+ mockGeckoInstallException(ERROR_FILE_ACCESS),
+ mockGeckoInstallException(ERROR_SIGNEDSTATE_REQUIRED),
+ mockGeckoInstallException(ERROR_UNEXPECTED_ADDON_TYPE),
+ mockGeckoInstallException(ERROR_INCORRECT_ID),
+ mockGeckoInstallException(ERROR_POSTPONED),
+ )
+
+ unrecoverableExceptions.forEach { exception ->
+ assertFalse(performUpdate(exception).isRecoverable)
+ }
+
+ val recoverableExceptions = listOf(mockGeckoInstallException(ERROR_USER_CANCELED))
+
+ recoverableExceptions.forEach { exception ->
+ assertTrue(performUpdate(exception).isRecoverable)
+ }
+ }
+
+ @Test
+ fun `list web extensions successfully`() {
+ val installedExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(allowedInPrivateBrowsing = false),
+ )
+
+ val installedExtensions = listOf(installedExtension)
+ val installedExtensionResult = GeckoResult<List<GeckoWebExtension>>()
+
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+ whenever(extensionController.list()).thenReturn(installedExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(testContext, runtime = runtime)
+ var extensions: List<WebExtension>? = null
+ var onErrorCalled = false
+
+ engine.listInstalledWebExtensions(
+ onSuccess = { extensions = it },
+ onError = { onErrorCalled = true },
+ )
+ installedExtensionResult.complete(installedExtensions)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onErrorCalled)
+ assertNotNull(extensions)
+ }
+
+ @Test
+ fun `list web extensions failure`() {
+ val installedExtensionResult = GeckoResult<List<GeckoWebExtension>>()
+
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+ whenever(extensionController.list()).thenReturn(installedExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ var extensions: List<WebExtension>? = null
+ val expected = IOException()
+ var throwable: Throwable? = null
+
+ engine.listInstalledWebExtensions(
+ onSuccess = { extensions = it },
+ onError = { throwable = it },
+ )
+ installedExtensionResult.completeExceptionally(expected)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertSame(expected, throwable)
+ assertNull(extensions)
+ }
+
+ @Test
+ fun `enable web extension successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val enabledExtension = mockNativeWebExtension(id = "id", location = "uri")
+ val enableExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.enable(any(), anyInt())).thenReturn(enableExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ var result: WebExtension? = null
+ var onErrorCalled = false
+
+ engine.enableWebExtension(
+ extension,
+ onSuccess = { result = it },
+ onError = { onErrorCalled = true },
+ )
+ enableExtensionResult.complete(enabledExtension)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onErrorCalled)
+ assertNotNull(result)
+ }
+
+ @Test
+ fun `enable web extension failure`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val enableExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.enable(any(), anyInt())).thenReturn(enableExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ val expected = IOException()
+ var throwable: Throwable? = null
+
+ engine.enableWebExtension(
+ extension,
+ onSuccess = { result = it },
+ onError = { throwable = it },
+ )
+ enableExtensionResult.completeExceptionally(expected)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertSame(expected, throwable)
+ assertNull(result)
+ }
+
+ @Test
+ fun `disable web extension successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val disabledExtension = mockNativeWebExtension(id = "id", location = "uri")
+ val disableExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.disable(any(), anyInt())).thenReturn(disableExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ var onErrorCalled = false
+
+ engine.disableWebExtension(
+ extension,
+ onSuccess = { result = it },
+ onError = { onErrorCalled = true },
+ )
+ disableExtensionResult.complete(disabledExtension)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onErrorCalled)
+ assertNotNull(result)
+ }
+
+ @Test
+ fun `disable web extension failure`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val disableExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.disable(any(), anyInt())).thenReturn(disableExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ val expected = IOException()
+ var throwable: Throwable? = null
+
+ engine.disableWebExtension(
+ extension,
+ onSuccess = { result = it },
+ onError = { throwable = it },
+ )
+ disableExtensionResult.completeExceptionally(expected)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertSame(expected, throwable)
+ assertNull(result)
+ }
+
+ @Test
+ fun `set allowedInPrivateBrowsing successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val allowedInPrivateBrowsing = mockNativeWebExtension(id = "id", location = "uri")
+ val allowedInPrivateBrowsingExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.setAllowedInPrivateBrowsing(any(), anyBoolean())).thenReturn(allowedInPrivateBrowsingExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ var onErrorCalled = false
+
+ engine.setAllowedInPrivateBrowsing(
+ extension,
+ true,
+ onSuccess = { ext -> result = ext },
+ onError = { onErrorCalled = true },
+ )
+ allowedInPrivateBrowsingExtensionResult.complete(allowedInPrivateBrowsing)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onErrorCalled)
+ assertNotNull(result)
+ verify(webExtensionsDelegate).onAllowedInPrivateBrowsingChanged(result!!)
+ }
+
+ @Test
+ fun `set allowedInPrivateBrowsing failure`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val allowedInPrivateBrowsingExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.setAllowedInPrivateBrowsing(any(), anyBoolean())).thenReturn(allowedInPrivateBrowsingExtensionResult)
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ val expected = IOException()
+ var throwable: Throwable? = null
+
+ engine.setAllowedInPrivateBrowsing(
+ extension,
+ true,
+ onSuccess = { ext -> result = ext },
+ onError = { throwable = it },
+ )
+ allowedInPrivateBrowsingExtensionResult.completeExceptionally(expected)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertSame(expected, throwable)
+ assertNull(result)
+ verify(webExtensionsDelegate, never()).onAllowedInPrivateBrowsingChanged(any())
+ }
+
+ @Test
+ fun `GIVEN null native extension WHEN calling setAllowedInPrivateBrowsing THEN call onError`() {
+ val runtime = mock<GeckoRuntime>()
+ val extensionController: WebExtensionController = mock()
+
+ val allowedInPrivateBrowsingExtensionResult = GeckoResult<GeckoWebExtension>()
+ whenever(extensionController.setAllowedInPrivateBrowsing(any(), anyBoolean())).thenReturn(
+ allowedInPrivateBrowsingExtensionResult,
+ )
+ whenever(runtime.webExtensionController).thenReturn(extensionController)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
+ mockNativeWebExtension(),
+ runtime,
+ )
+ var result: WebExtension? = null
+ var throwable: Throwable? = null
+
+ engine.setAllowedInPrivateBrowsing(
+ extension,
+ true,
+ onSuccess = { ext -> result = ext },
+ onError = { throwable = it },
+ )
+ allowedInPrivateBrowsingExtensionResult.complete(null)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertNotNull(throwable)
+ assertNull(result)
+ verify(webExtensionsDelegate, never()).onAllowedInPrivateBrowsingChanged(any())
+ }
+
+ @Test(expected = RuntimeException::class)
+ fun `WHEN GeckoRuntime is shutting down THEN GeckoEngine throws runtime exception`() {
+ val runtime: GeckoRuntime = mock()
+
+ GeckoEngine(context, runtime = runtime)
+
+ val captor = argumentCaptor<GeckoRuntime.Delegate>()
+ verify(runtime).delegate = captor.capture()
+
+ assertNotNull(captor.value)
+
+ captor.value.onShutdown()
+ }
+
+ @Test
+ fun `clear browsing data for all hosts`() {
+ val runtime: GeckoRuntime = mock()
+ val storageController: StorageController = mock()
+
+ var onSuccessCalled = false
+
+ val result = GeckoResult<Void>()
+ whenever(runtime.storageController).thenReturn(storageController)
+ whenever(storageController.clearData(eq(Engine.BrowsingData.all().types.toLong()))).thenReturn(result)
+ result.complete(null)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.clearData(data = Engine.BrowsingData.all(), onSuccess = { onSuccessCalled = true })
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onSuccessCalled)
+ }
+
+ @Test
+ fun `error handler invoked when clearing browsing data for all hosts fails`() {
+ val runtime: GeckoRuntime = mock()
+ val storageController: StorageController = mock()
+
+ var throwable: Throwable? = null
+ var onErrorCalled = false
+
+ val exception = IOException()
+ val result = GeckoResult<Void>()
+ whenever(runtime.storageController).thenReturn(storageController)
+ whenever(storageController.clearData(eq(Engine.BrowsingData.all().types.toLong()))).thenReturn(result)
+ result.completeExceptionally(exception)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.clearData(
+ data = Engine.BrowsingData.all(),
+ onError = {
+ onErrorCalled = true
+ throwable = it
+ },
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onErrorCalled)
+ assertSame(exception, throwable)
+ }
+
+ @Test
+ fun `clear browsing data for specified host`() {
+ val runtime: GeckoRuntime = mock()
+ val storageController: StorageController = mock()
+
+ var onSuccessCalled = false
+
+ val result = GeckoResult<Void>()
+ whenever(runtime.storageController).thenReturn(storageController)
+ whenever(
+ storageController.clearDataFromBaseDomain(
+ eq("mozilla.org"),
+ eq(Engine.BrowsingData.all().types.toLong()),
+ ),
+ ).thenReturn(result)
+ result.complete(null)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.clearData(data = Engine.BrowsingData.all(), host = "mozilla.org", onSuccess = { onSuccessCalled = true })
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onSuccessCalled)
+ }
+
+ @Test
+ fun `error handler invoked when clearing browsing data for specified hosts fails`() {
+ val runtime: GeckoRuntime = mock()
+ val storageController: StorageController = mock()
+
+ var throwable: Throwable? = null
+ var onErrorCalled = false
+
+ val exception = IOException()
+ val result = GeckoResult<Void>()
+ whenever(runtime.storageController).thenReturn(storageController)
+ whenever(
+ storageController.clearDataFromBaseDomain(
+ eq("mozilla.org"),
+ eq(Engine.BrowsingData.all().types.toLong()),
+ ),
+ ).thenReturn(result)
+ result.completeExceptionally(exception)
+
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.clearData(
+ data = Engine.BrowsingData.all(),
+ host = "mozilla.org",
+ onError = {
+ onErrorCalled = true
+ throwable = it
+ },
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onErrorCalled)
+ assertSame(exception, throwable)
+ }
+
+ @Test
+ fun `test parsing engine version`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ val version = engine.version
+
+ println(version)
+
+ assertTrue(version.major >= 69)
+ assertTrue(version.isAtLeast(69, 0, 0))
+ }
+
+ @Test
+ fun `fetch trackers logged successfully`() {
+ val runtime = mock<GeckoRuntime>()
+ val engine = GeckoEngine(context, runtime = runtime)
+ var onSuccessCalled = false
+ var onErrorCalled = false
+ val mockSession = mock<GeckoEngineSession>()
+ val mockGeckoSetting = mock<GeckoRuntimeSettings>()
+ val mockGeckoContentBlockingSetting = mock<ContentBlocking.Settings>()
+ var trackersLog: List<TrackerLog>? = null
+
+ val mockContentBlockingController = mock<ContentBlockingController>()
+ var logEntriesResult = GeckoResult<List<ContentBlockingController.LogEntry>>()
+
+ whenever(runtime.settings).thenReturn(mockGeckoSetting)
+ whenever(mockGeckoSetting.contentBlocking).thenReturn(mockGeckoContentBlockingSetting)
+ whenever(mockGeckoContentBlockingSetting.enhancedTrackingProtectionLevel).thenReturn(
+ ContentBlocking.EtpLevel.STRICT,
+ )
+ whenever(runtime.contentBlockingController).thenReturn(mockContentBlockingController)
+ whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
+
+ engine.getTrackersLog(
+ mockSession,
+ onSuccess = {
+ trackersLog = it
+ onSuccessCalled = true
+ },
+ onError = { onErrorCalled = true },
+ )
+
+ logEntriesResult.complete(createDummyLogEntryList())
+
+ shadowOf(getMainLooper()).idle()
+
+ val trackerLog = trackersLog!!.first()
+ assertTrue(trackerLog.cookiesHasBeenBlocked)
+ assertEquals("www.tracker.com", trackerLog.url)
+ assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES))
+ assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.FINGERPRINTING))
+ assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.CRYPTOMINING))
+ assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
+ assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES))
+ assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.FINGERPRINTING))
+ assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.CRYPTOMINING))
+ assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
+ assertTrue(trackerLog.unBlockedBySmartBlock)
+
+ assertTrue(onSuccessCalled)
+ assertFalse(onErrorCalled)
+
+ logEntriesResult = GeckoResult()
+ whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
+ logEntriesResult.completeExceptionally(Exception())
+
+ engine.getTrackersLog(
+ mockSession,
+ onSuccess = {
+ trackersLog = it
+ onSuccessCalled = true
+ },
+ onError = { onErrorCalled = true },
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onErrorCalled)
+ }
+
+ @Test
+ fun `shimmed content MUST be categorized as blocked`() {
+ val runtime = mock<GeckoRuntime>()
+ val engine = spy(GeckoEngine(context, runtime = runtime))
+ val mockSession = mock<GeckoEngineSession>()
+ val mockGeckoSetting = mock<GeckoRuntimeSettings>()
+ val mockGeckoContentBlockingSetting = mock<ContentBlocking.Settings>()
+ var trackersLog: List<TrackerLog>? = null
+
+ val mockContentBlockingController = mock<ContentBlockingController>()
+ val logEntriesResult = GeckoResult<List<ContentBlockingController.LogEntry>>()
+
+ val engineSetting = DefaultSettings()
+ engineSetting.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ whenever(engine.settings).thenReturn(engineSetting)
+ whenever(runtime.settings).thenReturn(mockGeckoSetting)
+ whenever(mockGeckoSetting.contentBlocking).thenReturn(mockGeckoContentBlockingSetting)
+
+ whenever(runtime.contentBlockingController).thenReturn(mockContentBlockingController)
+ whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
+
+ engine.getTrackersLog(mockSession, onSuccess = { trackersLog = it })
+
+ logEntriesResult.complete(createShimmedEntryList())
+
+ shadowOf(getMainLooper()).idle()
+
+ val trackerLog = trackersLog!!.first()
+ assertEquals("www.tracker.com", trackerLog.url)
+ assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES))
+ assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
+ assertTrue(trackerLog.loadedCategories.isEmpty())
+ }
+
+ @Test
+ fun `fetch site with social trackers`() {
+ val runtime = mock<GeckoRuntime>()
+ val engine = GeckoEngine(context, runtime = runtime)
+ val mockSession = mock<GeckoEngineSession>()
+ val mockGeckoSetting = mock<GeckoRuntimeSettings>()
+ val mockGeckoContentBlockingSetting = mock<ContentBlocking.Settings>()
+ var trackersLog: List<TrackerLog>? = null
+
+ val mockContentBlockingController = mock<ContentBlockingController>()
+ var logEntriesResult = GeckoResult<List<ContentBlockingController.LogEntry>>()
+
+ whenever(runtime.settings).thenReturn(mockGeckoSetting)
+ whenever(mockGeckoSetting.contentBlocking).thenReturn(mockGeckoContentBlockingSetting)
+ whenever(runtime.contentBlockingController).thenReturn(mockContentBlockingController)
+ whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.recommended()
+
+ engine.getTrackersLog(mockSession, onSuccess = { trackersLog = it })
+ logEntriesResult.complete(createSocialTrackersLogEntryList())
+
+ shadowOf(getMainLooper()).idle()
+
+ var trackerLog = trackersLog!!.first()
+ assertTrue(trackerLog.cookiesHasBeenBlocked)
+ assertEquals("www.tracker.com", trackerLog.url)
+ assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
+
+ var trackerLog2 = trackersLog!![1]
+ assertFalse(trackerLog2.cookiesHasBeenBlocked)
+ assertEquals("www.tracker2.com", trackerLog2.url)
+ assertTrue(trackerLog2.loadedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ logEntriesResult = GeckoResult()
+ whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
+
+ engine.getTrackersLog(mockSession, onSuccess = { trackersLog = it })
+ logEntriesResult.complete(createSocialTrackersLogEntryList())
+
+ trackerLog = trackersLog!!.first()
+ assertTrue(trackerLog.cookiesHasBeenBlocked)
+ assertEquals("www.tracker.com", trackerLog.url)
+ assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
+
+ trackerLog2 = trackersLog!![1]
+ assertFalse(trackerLog2.cookiesHasBeenBlocked)
+ assertEquals("www.tracker2.com", trackerLog2.url)
+ assertTrue(trackerLog2.loadedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
+ }
+
+ @Test
+ fun `fetch trackers logged of the level 2 list`() {
+ val runtime = mock<GeckoRuntime>()
+ val engine = GeckoEngine(context, runtime = runtime)
+ val mockSession = mock<GeckoEngineSession>()
+ val mockGeckoSetting = mock<GeckoRuntimeSettings>()
+ val mockGeckoContentBlockingSetting = mock<ContentBlocking.Settings>()
+ var trackersLog: List<TrackerLog>? = null
+
+ val mockContentBlockingController = mock<ContentBlockingController>()
+ var logEntriesResult = GeckoResult<List<ContentBlockingController.LogEntry>>()
+
+ whenever(runtime.settings).thenReturn(mockGeckoSetting)
+ whenever(mockGeckoSetting.contentBlocking).thenReturn(mockGeckoContentBlockingSetting)
+ whenever(mockGeckoContentBlockingSetting.enhancedTrackingProtectionLevel).thenReturn(
+ ContentBlocking.EtpLevel.STRICT,
+ )
+ whenever(runtime.contentBlockingController).thenReturn(mockContentBlockingController)
+ whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
+
+ engine.settings.trackingProtectionPolicy = TrackingProtectionPolicy.select(
+ arrayOf(
+ TrackingCategory.STRICT,
+ TrackingCategory.CONTENT,
+ ),
+ )
+
+ logEntriesResult = GeckoResult()
+ whenever(runtime.contentBlockingController).thenReturn(mockContentBlockingController)
+ whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
+
+ engine.getTrackersLog(
+ mockSession,
+ onSuccess = {
+ trackersLog = it
+ },
+ onError = { },
+ )
+ logEntriesResult.complete(createDummyLogEntryList())
+
+ shadowOf(getMainLooper()).idle()
+
+ val trackerLog = trackersLog!![1]
+ assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES))
+ }
+
+ @Test
+ fun `registerWebNotificationDelegate sets delegate`() {
+ val runtime = mock<GeckoRuntime>()
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ engine.registerWebNotificationDelegate(mock())
+
+ verify(runtime).webNotificationDelegate = any()
+ }
+
+ @Test
+ fun `registerWebPushDelegate sets delegate and returns same handler`() {
+ val runtime = mock<GeckoRuntime>()
+ val controller: WebPushController = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ whenever(runtime.webPushController).thenReturn(controller)
+
+ val handler1 = engine.registerWebPushDelegate(mock())
+ val handler2 = engine.registerWebPushDelegate(mock())
+
+ verify(controller, times(2)).setDelegate(any())
+
+ assert(handler1 == handler2)
+ }
+
+ @Test
+ fun `registerActivityDelegate sets delegate`() {
+ val runtime = mock<GeckoRuntime>()
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ engine.registerActivityDelegate(mock())
+
+ verify(runtime).activityDelegate = any()
+ }
+
+ @Test
+ fun `unregisterActivityDelegate sets delegate to null`() {
+ val runtime = mock<GeckoRuntime>()
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ engine.registerActivityDelegate(mock())
+
+ verify(runtime).activityDelegate = any()
+
+ engine.unregisterActivityDelegate()
+
+ verify(runtime).activityDelegate = null
+ }
+
+ @Test
+ fun `registerScreenOrientationDelegate sets delegate`() {
+ val orientationController = mock<OrientationController>()
+ val runtime = mock<GeckoRuntime>()
+ doReturn(orientationController).`when`(runtime).orientationController
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ engine.registerScreenOrientationDelegate(mock())
+
+ verify(orientationController).delegate = any()
+ }
+
+ @Test
+ fun `unregisterScreenOrientationDelegate sets delegate to null`() {
+ val orientationController = mock<OrientationController>()
+ val runtime = mock<GeckoRuntime>()
+ doReturn(orientationController).`when`(runtime).orientationController
+ val engine = GeckoEngine(context, runtime = runtime)
+
+ engine.registerScreenOrientationDelegate(mock())
+ verify(orientationController).delegate = any()
+
+ engine.unregisterScreenOrientationDelegate()
+ verify(orientationController).delegate = null
+ }
+
+ @Test
+ fun `registerServiceWorkerDelegate sets delegate`() {
+ val delegate = mock<ServiceWorkerDelegate>()
+ val runtime = GeckoRuntime.getDefault(testContext)
+ val settings = DefaultSettings()
+ val engine = GeckoEngine(context, runtime = runtime, defaultSettings = settings)
+
+ engine.registerServiceWorkerDelegate(delegate)
+ val result = runtime.serviceWorkerDelegate as GeckoServiceWorkerDelegate
+
+ assertEquals(delegate, result.delegate)
+ assertEquals(runtime, result.runtime)
+ assertEquals(settings, result.engineSettings)
+ }
+
+ @Test
+ fun `unregisterServiceWorkerDelegate sets delegate to null`() {
+ val runtime = GeckoRuntime.getDefault(testContext)
+ val settings = DefaultSettings()
+ val engine = GeckoEngine(context, runtime = runtime, defaultSettings = settings)
+
+ engine.registerServiceWorkerDelegate(mock())
+ assertNotNull(runtime.serviceWorkerDelegate)
+
+ engine.unregisterServiceWorkerDelegate()
+ assertNull(runtime.serviceWorkerDelegate)
+ }
+
+ @Test
+ fun `handleWebNotificationClick calls click on the WebNotification`() {
+ val runtime = GeckoRuntime.getDefault(testContext)
+ val settings = DefaultSettings()
+ val engine = GeckoEngine(context, runtime = runtime, defaultSettings = settings)
+
+ // Check that having another argument doesn't cause any issues
+ engine.handleWebNotificationClick(runtime)
+
+ val notification: WebNotification = mock()
+ engine.handleWebNotificationClick(notification)
+ verify(notification).click()
+ }
+
+ @Test
+ fun `web extension delegate handles add-on onEnabled event`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.AddonManagerDelegate>()
+ verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture())
+
+ assertEquals(Unit, geckoDelegateCaptor.value.onEnabled(extension))
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ verify(webExtensionsDelegate).onEnabled(extensionCaptor.capture())
+ val capturedExtension =
+ extensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(extension, capturedExtension.nativeExtension)
+ }
+
+ @Test
+ fun `web extension delegate handles add-on onInstallationFailed event`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ val exception = mockGeckoInstallException(ERROR_BLOCKLISTED)
+
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.AddonManagerDelegate>()
+ verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture())
+
+ assertEquals(Unit, geckoDelegateCaptor.value.onInstallationFailed(extension, exception))
+
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ val exceptionCaptor = argumentCaptor<WebExtensionInstallException>()
+
+ verify(webExtensionsDelegate).onInstallationFailedRequest(
+ extensionCaptor.capture(),
+ exceptionCaptor.capture(),
+ )
+ val capturedExtension =
+ extensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(extension, capturedExtension.nativeExtension)
+
+ assertTrue(exceptionCaptor.value is WebExtensionInstallException.Blocklisted)
+ }
+
+ @Test
+ fun `web extension delegate handles add-on onDisabled event`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.AddonManagerDelegate>()
+ verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture())
+
+ assertEquals(Unit, geckoDelegateCaptor.value.onDisabled(extension))
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ verify(webExtensionsDelegate).onDisabled(extensionCaptor.capture())
+ val capturedExtension =
+ extensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(extension, capturedExtension.nativeExtension)
+ }
+
+ @Test
+ fun `web extension delegate handles add-on onUninstalled event`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.AddonManagerDelegate>()
+ verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture())
+
+ assertEquals(Unit, geckoDelegateCaptor.value.onUninstalled(extension))
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ verify(webExtensionsDelegate).onUninstalled(extensionCaptor.capture())
+ val capturedExtension =
+ extensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(extension, capturedExtension.nativeExtension)
+ }
+
+ @Test
+ fun `web extension delegate handles add-on onInstalled event`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val extension = mockNativeWebExtension("test", "uri")
+ val webExtensionsDelegate: WebExtensionDelegate = mock()
+ val engine = GeckoEngine(context, runtime = runtime)
+ engine.registerWebExtensionDelegate(webExtensionsDelegate)
+
+ val geckoDelegateCaptor = argumentCaptor<WebExtensionController.AddonManagerDelegate>()
+ verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture())
+
+ assertEquals(Unit, geckoDelegateCaptor.value.onInstalled(extension))
+ val extensionCaptor = argumentCaptor<WebExtension>()
+ verify(webExtensionsDelegate).onInstalled(extensionCaptor.capture())
+ val capturedExtension =
+ extensionCaptor.value as mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
+ assertEquals(extension, capturedExtension.nativeExtension)
+
+ // Make sure we called `registerActionHandler()` on the installed extension.
+ verify(extension).setActionDelegate(any())
+ // Make sure we called `registerTabHandler()` on the installed extension.
+ verify(extension).tabDelegate = any()
+ }
+
+ @Test
+ fun `WHEN isTranslationsEngineSupported is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Boolean>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Boolean>> { isTranslationsEngineSupported() }
+ .thenReturn(geckoResult)
+
+ engine.isTranslationsEngineSupported(
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(true)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully determine translation engine status." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN isTranslationsEngineSupported is called AND excepts THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Boolean>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Boolean>> { isTranslationsEngineSupported() }
+ .thenReturn(geckoResult)
+
+ engine.isTranslationsEngineSupported(
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not have successfully determine translation engine status." }
+ assert(onErrorCalled) { "Should have had an exception." }
+ }
+ }
+
+ @Test
+ fun `WHEN getTranslationsPairDownloadSize is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Long>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Long>> { checkPairDownloadSize(any(), any()) }
+ .thenReturn(geckoResult)
+
+ engine.getTranslationsPairDownloadSize(
+ fromLanguage = "es",
+ toLanguage = "en",
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(12345)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully determine pair size." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getTranslationsPairDownloadSize is called AND excepts THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Long>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Long>> { checkPairDownloadSize(any(), any()) }
+ .thenReturn(geckoResult)
+
+ engine.getTranslationsPairDownloadSize(
+ fromLanguage = "es",
+ toLanguage = "en",
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not have successfully determine pair size." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getTranslationsModelDownloadStates is called successfully THEN onSuccess is called AND the LanguageModel maps as expected`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ var code = "es"
+ var localizedDisplayName = "Spanish"
+ var isDownloaded = true
+ var size: Long = 1234
+ var geckoLanguage = TranslationsController.Language(code, localizedDisplayName)
+ var geckoLanguageModel = LanguageModel(geckoLanguage, isDownloaded, size)
+ var geckoResultValue: List<LanguageModel> = mutableListOf(geckoLanguageModel)
+ val geckoResult = GeckoResult<List<LanguageModel>>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<List<LanguageModel>>> { listModelDownloadStates() }
+ .thenReturn(geckoResult)
+
+ engine.getTranslationsModelDownloadStates(
+ onSuccess = {
+ onSuccessCalled = true
+ assertTrue(it[0].language!!.code == code)
+ assertTrue(it[0].language!!.localizedDisplayName == localizedDisplayName)
+ assertTrue(it[0].isDownloaded == isDownloaded)
+ assertTrue(it[0].size == size)
+ },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(geckoResultValue)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should have successfully listed model download state." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getTranslationsModelDownloadStates is called AND excepts THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<List<LanguageModel>>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<List<LanguageModel>>> { listModelDownloadStates() }
+ .thenReturn(geckoResult)
+
+ engine.getTranslationsModelDownloadStates(
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not have successfully listed model download state." }
+ assert(onErrorCalled) { "An error should have have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getSupportedTranslationLanguages is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<TranslationsController.RuntimeTranslation.TranslationSupport>()
+ val toLanguage = Language("de", "German")
+ val fromLanguage = Language("es", "Spanish")
+ val geckoResultValue = TranslationsController.RuntimeTranslation.TranslationSupport(listOf<Language>(fromLanguage), listOf<Language>(toLanguage))
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<TranslationsController.RuntimeTranslation.TranslationSupport>> { listSupportedLanguages() }
+ .thenReturn(geckoResult)
+
+ engine.getSupportedTranslationLanguages(
+ onSuccess = {
+ onSuccessCalled = true
+ assertTrue(it.fromLanguages!![0].code == fromLanguage.code)
+ assertTrue(it.toLanguages!![0].code == toLanguage.code)
+ },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(geckoResultValue)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Successfully retrieved list of supported languages." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getSupportedTranslationLanguages is called AND excepts THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<TranslationsController.RuntimeTranslation.TranslationSupport>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<TranslationsController.RuntimeTranslation.TranslationSupport>> { listSupportedLanguages() }
+ .thenReturn(geckoResult)
+
+ engine.getSupportedTranslationLanguages(
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not have retrieved list of supported languages." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN manageTranslationsLanguageModel is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ var options = ModelManagementOptions(null, ModelOperation.DOWNLOAD, OperationLevel.ALL)
+ val geckoResult = GeckoResult<Void>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Void>> { manageLanguageModel(any()) }
+ .thenReturn(geckoResult)
+
+ engine.manageTranslationsLanguageModel(
+ options = options,
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(null)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully manage language models." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN manageTranslationsLanguageModel is called AND excepts THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ var options = ModelManagementOptions(null, ModelOperation.DOWNLOAD, OperationLevel.ALL)
+ val geckoResult = GeckoResult<Void>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Void>> { manageLanguageModel(any()) }
+ .thenReturn(geckoResult)
+
+ engine.manageTranslationsLanguageModel(
+ options = options,
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not successfully manage language models." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getUserPreferredLanguages is called successfully THEN onSuccess is called `() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<List<String>>()
+ val geckoResultValue = listOf<String>("en", "es", "de")
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<List<String>>> { preferredLanguages() }
+ .thenReturn(geckoResult)
+
+ engine.getUserPreferredLanguages(
+ onSuccess = {
+ onSuccessCalled = true
+ assertTrue(it[0] == "en")
+ },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(geckoResultValue)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully list user languages." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getUserPreferredLanguages is called AND excepts THEN onError is called `() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<List<String>>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<List<String>>> { preferredLanguages() }
+ .thenReturn(geckoResult)
+
+ engine.getUserPreferredLanguages(
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not successfully list user languages." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getTranslationsOfferPopup is called successfully THEN a result is retrieved `() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+ val runtimeSettings = mock<GeckoRuntimeSettings>()
+
+ whenever(runtime.settings).thenReturn(runtimeSettings)
+ whenever(runtime.settings.translationsOfferPopup).thenReturn(true)
+
+ val result = engine.getTranslationsOfferPopup()
+ assert(result) { "Should successfully get a language setting." }
+ }
+
+ @Test
+ fun `WHEN getLanguageSetting is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<String>()
+ val geckoResultValue = "always"
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<String>> { getLanguageSetting(any()) }
+ .thenReturn(geckoResult)
+
+ engine.getLanguageSetting(
+ "es",
+ onSuccess = {
+ onSuccessCalled = true
+ assertTrue(it == LanguageSetting.ALWAYS)
+ },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(geckoResultValue)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully get a language setting." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getLanguageSetting is unsuccessful THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<String>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<String>> { getLanguageSetting(any()) }
+ .thenReturn(geckoResult)
+
+ engine.getLanguageSetting(
+ "es",
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not successfully get a language setting." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN setLanguageSetting is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Void>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Void>> { setLanguageSettings(any(), any()) }
+ .thenReturn(geckoResult)
+
+ engine.setLanguageSetting(
+ "es",
+ LanguageSetting.ALWAYS,
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(null)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully set a language setting." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN setLanguageSetting is unsuccessful THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Void>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Void>> { setLanguageSettings(any(), any()) }
+ .thenReturn(geckoResult)
+
+ engine.setLanguageSetting(
+ "es",
+ LanguageSetting.ALWAYS,
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not successfully set a language setting." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getLanguageSetting is unrecognized THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<String>()
+ val geckoResultValue = "NotAnExpectedResponse"
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<String>> { getLanguageSetting(any()) }
+ .thenReturn(geckoResult)
+
+ engine.getLanguageSetting(
+ "es",
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(geckoResultValue)
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not successfully get a language setting." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getLanguageSettings is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Map<String, String>>()
+ val geckoResultValue = mapOf("es" to "offer", "de" to "always", "fr" to "never")
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Map<String, String>>> { getLanguageSettings() }
+ .thenReturn(geckoResult)
+
+ engine.getLanguageSettings(
+ onSuccess = {
+ onSuccessCalled = true
+ assertTrue(it["es"] == LanguageSetting.OFFER)
+ assertTrue(it["de"] == LanguageSetting.ALWAYS)
+ assertTrue(it["fr"] == LanguageSetting.NEVER)
+ },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(geckoResultValue)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully list language settings." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getLanguageSettings is unsuccessful THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Map<String, String>>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Map<String, String>>> { getLanguageSettings() }
+ .thenReturn(geckoResult)
+
+ engine.getLanguageSettings(
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not successfully list language settings." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getNeverTranslateSiteList is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<List<String>>()
+ val geckoResultValue = listOf("www.mozilla.org")
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<List<String>>> { getNeverTranslateSiteList() }
+ .thenReturn(geckoResult)
+
+ engine.getNeverTranslateSiteList(
+ onSuccess = {
+ onSuccessCalled = true
+ assertTrue(it[0] == "www.mozilla.org")
+ },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(geckoResultValue)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully list of never translate websites." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN getNeverTranslateSiteList is unsuccessful THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<List<String>>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<List<String>>> { getNeverTranslateSiteList() }
+ .thenReturn(geckoResult)
+
+ engine.getNeverTranslateSiteList(
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not successfully list never translate sites." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN setNeverTranslateSpecifiedSite is called successfully THEN onSuccess is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<Void>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<Void>> { setNeverTranslateSpecifiedSite(any(), any()) }
+ .thenReturn(geckoResult)
+
+ engine.setNeverTranslateSpecifiedSite(
+ "www.mozilla.org",
+ true,
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.complete(null)
+ shadowOf(getMainLooper()).idle()
+
+ assert(onSuccessCalled) { "Should successfully complete when setting the never translate site." }
+ assert(!onErrorCalled) { "An error should not have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN setNeverTranslateSpecifiedSite is unsuccessful THEN onError is called`() {
+ val runtime: GeckoRuntime = mock()
+ val engine = GeckoEngine(testContext, runtime = runtime)
+
+ var onSuccessCalled = false
+ var onErrorCalled = false
+
+ val geckoResult = GeckoResult<List<String>>()
+
+ Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use {
+ mocked ->
+ mocked.`when`<GeckoResult<List<String>>> { setNeverTranslateSpecifiedSite(any(), any()) }
+ .thenReturn(geckoResult)
+
+ engine.setNeverTranslateSpecifiedSite(
+ "www.mozilla.org",
+ true,
+ onSuccess = { onSuccessCalled = true },
+ onError = { onErrorCalled = true },
+ )
+
+ geckoResult.completeExceptionally(Exception())
+ shadowOf(getMainLooper()).idle()
+
+ assert(!onSuccessCalled) { "Should not successfully complete when setting the never translate site." }
+ assert(onErrorCalled) { "An error should have occurred." }
+ }
+ }
+
+ @Test
+ fun `WHEN Global Privacy Control value is set THEN setGlobalPrivacyControl is getting called on GeckoRuntime`() {
+ val mockRuntime = mock<GeckoRuntime>()
+ whenever(mockRuntime.settings).thenReturn(mock())
+
+ val engine = GeckoEngine(testContext, runtime = mockRuntime)
+
+ reset(mockRuntime.settings)
+ engine.settings.globalPrivacyControlEnabled = true
+ verify(mockRuntime.settings).setGlobalPrivacyControl(true)
+
+ reset(mockRuntime.settings)
+ engine.settings.globalPrivacyControlEnabled = false
+ verify(mockRuntime.settings).setGlobalPrivacyControl(false)
+ }
+
+ private fun createSocialTrackersLogEntryList(): List<ContentBlockingController.LogEntry> {
+ val blockedLogEntry = object : ContentBlockingController.LogEntry() {}
+
+ ReflectionUtils.setField(blockedLogEntry, "origin", "www.tracker.com")
+ val blockedCookieSocialTracker = createBlockingData(Event.COOKIES_BLOCKED_SOCIALTRACKER)
+ val blockedSocialContent = createBlockingData(Event.BLOCKED_SOCIALTRACKING_CONTENT)
+
+ ReflectionUtils.setField(blockedLogEntry, "blockingData", listOf(blockedSocialContent, blockedCookieSocialTracker))
+
+ val loadedLogEntry = object : ContentBlockingController.LogEntry() {}
+ ReflectionUtils.setField(loadedLogEntry, "origin", "www.tracker2.com")
+
+ val loadedCookieSocialTracker = createBlockingData(Event.COOKIES_LOADED_SOCIALTRACKER)
+ val loadedSocialContent = createBlockingData(Event.LOADED_SOCIALTRACKING_CONTENT)
+
+ ReflectionUtils.setField(loadedLogEntry, "blockingData", listOf(loadedCookieSocialTracker, loadedSocialContent))
+
+ return listOf(blockedLogEntry, loadedLogEntry)
+ }
+
+ private fun createDummyLogEntryList(): List<ContentBlockingController.LogEntry> {
+ val addLogEntry = object : ContentBlockingController.LogEntry() {}
+
+ ReflectionUtils.setField(addLogEntry, "origin", "www.tracker.com")
+ val blockedCookiePermission = createBlockingData(Event.COOKIES_BLOCKED_BY_PERMISSION)
+ val loadedCookieSocialTracker = createBlockingData(Event.COOKIES_LOADED_SOCIALTRACKER)
+ val blockedCookieSocialTracker = createBlockingData(Event.COOKIES_BLOCKED_SOCIALTRACKER)
+
+ val blockedTrackingContent = createBlockingData(Event.BLOCKED_TRACKING_CONTENT)
+ val blockedFingerprintingContent = createBlockingData(Event.BLOCKED_FINGERPRINTING_CONTENT)
+ val blockedCyptominingContent = createBlockingData(Event.BLOCKED_CRYPTOMINING_CONTENT)
+ val blockedSocialContent = createBlockingData(Event.BLOCKED_SOCIALTRACKING_CONTENT)
+
+ val loadedTrackingLevel1Content = createBlockingData(Event.LOADED_LEVEL_1_TRACKING_CONTENT)
+ val loadedTrackingLevel2Content = createBlockingData(Event.LOADED_LEVEL_2_TRACKING_CONTENT)
+ val loadedFingerprintingContent = createBlockingData(Event.LOADED_FINGERPRINTING_CONTENT)
+ val loadedCyptominingContent = createBlockingData(Event.LOADED_CRYPTOMINING_CONTENT)
+ val loadedSocialContent = createBlockingData(Event.LOADED_SOCIALTRACKING_CONTENT)
+ val unBlockedBySmartBlock = createBlockingData(Event.ALLOWED_TRACKING_CONTENT)
+
+ val contentBlockingList = listOf(
+ blockedTrackingContent,
+ loadedTrackingLevel1Content,
+ loadedTrackingLevel2Content,
+ blockedFingerprintingContent,
+ loadedFingerprintingContent,
+ blockedCyptominingContent,
+ loadedCyptominingContent,
+ blockedCookiePermission,
+ blockedSocialContent,
+ loadedSocialContent,
+ loadedCookieSocialTracker,
+ blockedCookieSocialTracker,
+ unBlockedBySmartBlock,
+ )
+
+ val addLogSecondEntry = object : ContentBlockingController.LogEntry() {}
+ ReflectionUtils.setField(addLogSecondEntry, "origin", "www.tracker2.com")
+ val contentBlockingSecondEntryList = listOf(loadedTrackingLevel2Content)
+
+ ReflectionUtils.setField(addLogEntry, "blockingData", contentBlockingList)
+ ReflectionUtils.setField(addLogSecondEntry, "blockingData", contentBlockingSecondEntryList)
+
+ return listOf(addLogEntry, addLogSecondEntry)
+ }
+
+ private fun createShimmedEntryList(): List<ContentBlockingController.LogEntry> {
+ val addLogEntry = object : ContentBlockingController.LogEntry() {}
+
+ ReflectionUtils.setField(addLogEntry, "origin", "www.tracker.com")
+ val shimmedContent = createBlockingData(Event.REPLACED_TRACKING_CONTENT, 2)
+ val loadedTrackingLevel1Content = createBlockingData(Event.LOADED_LEVEL_1_TRACKING_CONTENT)
+ val loadedSocialContent = createBlockingData(Event.LOADED_SOCIALTRACKING_CONTENT)
+
+ val contentBlockingList = listOf(
+ loadedTrackingLevel1Content,
+ loadedSocialContent,
+ shimmedContent,
+ )
+
+ ReflectionUtils.setField(addLogEntry, "blockingData", contentBlockingList)
+
+ return listOf(addLogEntry)
+ }
+
+ private fun createBlockingData(category: Int, count: Int = 0): ContentBlockingController.LogEntry.BlockingData {
+ val blockingData = object : ContentBlockingController.LogEntry.BlockingData() {}
+ ReflectionUtils.setField(blockingData, "category", category)
+ ReflectionUtils.setField(blockingData, "count", count)
+ return blockingData
+ }
+
+ private fun mockGeckoInstallException(errorCode: Int): GeckoInstallException {
+ val exception = object : GeckoInstallException() {}
+ ReflectionUtils.setField(exception, "code", errorCode)
+ return exception
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineViewTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineViewTest.kt
new file mode 100644
index 0000000000..7056187e09
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineViewTest.kt
@@ -0,0 +1,299 @@
+/* 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.browser.engine.gecko
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.os.Looper.getMainLooper
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.GeckoEngineView.Companion.DARK_COVER
+import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import mozilla.components.test.ReflectionUtils
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoSession
+import org.robolectric.Robolectric.buildActivity
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class GeckoEngineViewTest {
+
+ private val context: Context
+ get() = buildActivity(Activity::class.java).get()
+
+ @Test
+ fun render() {
+ val engineView = GeckoEngineView(context)
+ val engineSession = mock<GeckoEngineSession>()
+ val geckoSession = mock<GeckoSession>()
+ val geckoView = mock<NestedGeckoView>()
+
+ whenever(engineSession.geckoSession).thenReturn(geckoSession)
+ engineView.geckoView = geckoView
+
+ engineView.render(engineSession)
+ verify(geckoView, times(1)).setSession(geckoSession)
+
+ whenever(geckoView.session).thenReturn(geckoSession)
+ engineView.render(engineSession)
+ verify(geckoView, times(1)).setSession(geckoSession)
+ }
+
+ @Test
+ fun captureThumbnail() {
+ val engineView = GeckoEngineView(context)
+ val mockGeckoView = mock<NestedGeckoView>()
+ var thumbnail: Bitmap? = null
+
+ var geckoResult = GeckoResult<Bitmap>()
+ whenever(mockGeckoView.capturePixels()).thenReturn(geckoResult)
+ engineView.geckoView = mockGeckoView
+
+ // Test GeckoResult resolves successfuly
+ engineView.captureThumbnail {
+ thumbnail = it
+ }
+ verify(mockGeckoView).capturePixels()
+ geckoResult.complete(mock())
+ shadowOf(getMainLooper()).idle()
+
+ assertNotNull(thumbnail)
+
+ geckoResult = GeckoResult()
+ whenever(mockGeckoView.capturePixels()).thenReturn(geckoResult)
+
+ // Test GeckoResult resolves in error
+ engineView.captureThumbnail {
+ thumbnail = it
+ }
+ geckoResult.completeExceptionally(mock())
+ shadowOf(getMainLooper()).idle()
+
+ assertNull(thumbnail)
+
+ // Test GeckoView throwing an exception
+ whenever(mockGeckoView.capturePixels()).thenThrow(IllegalStateException("Compositor not ready"))
+
+ thumbnail = mock()
+ engineView.captureThumbnail {
+ thumbnail = it
+ }
+ assertNull(thumbnail)
+ }
+
+ @Test
+ fun `clearSelection is forwarded to BasicSelectionAction instance`() {
+ val engineView = GeckoEngineView(context)
+ engineView.geckoView = mock()
+ engineView.currentSelection = mock()
+
+ engineView.clearSelection()
+
+ verify(engineView.currentSelection)?.clearSelection()
+ }
+
+ @Test
+ fun `setColorScheme uses preferred color scheme to set correct cover color`() {
+ val engineView = GeckoEngineView(context)
+
+ engineView.geckoView = mock()
+
+ var preferredColorScheme: PreferredColorScheme = PreferredColorScheme.Light
+
+ engineView.setColorScheme(preferredColorScheme)
+
+ verify(engineView.geckoView)?.coverUntilFirstPaint(Color.WHITE)
+
+ preferredColorScheme = PreferredColorScheme.Dark
+ engineView.setColorScheme(preferredColorScheme)
+ verify(engineView.geckoView)?.coverUntilFirstPaint(DARK_COVER)
+ }
+
+ @Test
+ fun `setVerticalClipping is forwarded to GeckoView instance`() {
+ val engineView = GeckoEngineView(context)
+ engineView.geckoView = mock()
+
+ engineView.setVerticalClipping(-42)
+
+ verify(engineView.geckoView).setVerticalClipping(-42)
+ }
+
+ @Test
+ fun `setDynamicToolbarMaxHeight is forwarded to GeckoView instance`() {
+ val engineView = GeckoEngineView(context)
+ engineView.geckoView = mock()
+
+ engineView.setDynamicToolbarMaxHeight(42)
+
+ verify(engineView.geckoView).setDynamicToolbarMaxHeight(42)
+ }
+
+ @Test
+ fun `release method releases session from GeckoView`() {
+ val engineView = GeckoEngineView(context)
+ val engineSession = mock<GeckoEngineSession>()
+ val geckoSession = mock<GeckoSession>()
+ val geckoView = mock<NestedGeckoView>()
+
+ whenever(engineSession.geckoSession).thenReturn(geckoSession)
+ engineView.geckoView = geckoView
+
+ engineView.render(engineSession)
+
+ verify(geckoView, never()).releaseSession()
+
+ engineView.release()
+
+ verify(geckoView).releaseSession()
+ }
+
+ @Test
+ fun `after rendering currentSelection should be a GeckoSelectionActionDelegate`() {
+ val engineView = GeckoEngineView(context).apply {
+ selectionActionDelegate = mock()
+ }
+ val engineSession = mock<GeckoEngineSession>()
+ val geckoSession = mock<GeckoSession>()
+ val geckoView = mock<NestedGeckoView>()
+
+ whenever(engineSession.geckoSession).thenReturn(geckoSession)
+ engineView.geckoView = geckoView
+
+ engineView.render(engineSession)
+
+ assertTrue(engineView.currentSelection is GeckoSelectionActionDelegate)
+ }
+
+ @Test
+ fun `will attach and detach selection action delegate when rendering and releasing`() {
+ val delegate: SelectionActionDelegate = mock()
+
+ val engineView = GeckoEngineView(context).apply {
+ selectionActionDelegate = delegate
+ }
+ val engineSession = mock<GeckoEngineSession>()
+ val geckoSession = mock<GeckoSession>()
+ val geckoView = mock<NestedGeckoView>()
+
+ whenever(engineSession.geckoSession).thenReturn(geckoSession)
+ engineView.geckoView = geckoView
+
+ engineView.render(engineSession)
+
+ val captor = argumentCaptor<GeckoSession.SelectionActionDelegate>()
+ verify(geckoSession).selectionActionDelegate = captor.capture()
+
+ assertTrue(captor.value is GeckoSelectionActionDelegate)
+ val capturedDelegate = captor.value as GeckoSelectionActionDelegate
+
+ assertEquals(delegate, capturedDelegate.customDelegate)
+
+ verify(geckoSession, never()).selectionActionDelegate = null
+
+ engineView.release()
+
+ verify(geckoSession).selectionActionDelegate = null
+ }
+
+ @Test
+ fun `will attach and detach selection action delegate when rendering new session`() {
+ val delegate: SelectionActionDelegate = mock()
+
+ val engineView = GeckoEngineView(context).apply {
+ selectionActionDelegate = delegate
+ }
+ val engineSession = mock<GeckoEngineSession>()
+ val geckoSession = mock<GeckoSession>()
+ val geckoView = mock<NestedGeckoView>()
+
+ whenever(engineSession.geckoSession).thenReturn(geckoSession)
+ engineView.geckoView = geckoView
+
+ engineView.render(engineSession)
+
+ val captor = argumentCaptor<GeckoSession.SelectionActionDelegate>()
+ verify(geckoSession).selectionActionDelegate = captor.capture()
+
+ assertTrue(captor.value is GeckoSelectionActionDelegate)
+ val capturedDelegate = captor.value as GeckoSelectionActionDelegate
+
+ assertEquals(delegate, capturedDelegate.customDelegate)
+
+ verify(geckoSession, never()).selectionActionDelegate = null
+
+ whenever(geckoView.session).thenReturn(geckoSession)
+
+ engineView.render(
+ mock<GeckoEngineSession>().apply {
+ whenever(this.geckoSession).thenReturn(mock())
+ },
+ )
+
+ verify(geckoSession).selectionActionDelegate = null
+ }
+
+ @Test
+ fun `setVisibility is propagated to gecko view`() {
+ val engineView = GeckoEngineView(context)
+ engineView.geckoView = mock()
+
+ engineView.visibility = View.GONE
+ verify(engineView.geckoView)?.visibility = View.GONE
+ }
+
+ @Test
+ fun `canClearSelection should return false for null selection, null and empty selection text`() {
+ val engineView = GeckoEngineView(context)
+ engineView.geckoView = mock()
+ engineView.currentSelection = mock()
+
+ // null selection returns false
+ whenever(engineView.currentSelection?.selection).thenReturn(null)
+ assertFalse(engineView.canClearSelection())
+
+ // selection with null text returns false
+ val selectionWthNullText: GeckoSession.SelectionActionDelegate.Selection = mock()
+ whenever(engineView.currentSelection?.selection).thenReturn(selectionWthNullText)
+ assertFalse(engineView.canClearSelection())
+
+ // selection with empty text returns false
+ val selectionWthEmptyText: GeckoSession.SelectionActionDelegate.Selection = mockSelection("")
+ whenever(engineView.currentSelection?.selection).thenReturn(selectionWthEmptyText)
+ assertFalse(engineView.canClearSelection())
+ }
+
+ @Test
+ fun `GIVEN a GeckoView WHEN EngineView returns the InputResultDetail THEN the value from the GeckoView is used`() {
+ val engineView = GeckoEngineView(context)
+ val geckoview = engineView.geckoView
+
+ assertSame(geckoview.inputResultDetail, engineView.getInputResultDetail())
+ }
+
+ private fun mockSelection(text: String): GeckoSession.SelectionActionDelegate.Selection {
+ val selection: GeckoSession.SelectionActionDelegate.Selection = mock()
+ ReflectionUtils.setField(selection, "text", text)
+ return selection
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt
new file mode 100644
index 0000000000..b8c0220046
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt
@@ -0,0 +1,86 @@
+/* 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.browser.engine.gecko
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+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.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.GeckoResult
+import org.robolectric.annotation.LooperMode
+
+@Suppress("DEPRECATION") // Suppress deprecation for LooperMode.Mode.LEGACY
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.LEGACY)
+class GeckoResultTest {
+
+ @Test
+ fun awaitWithResult() = runTest {
+ val result = GeckoResult.fromValue(42).await()
+ assertEquals(42, result)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun awaitWithException() = runTest {
+ GeckoResult.fromException<Unit>(IllegalStateException()).await()
+ }
+
+ @Test
+ fun fromResult() = runTest {
+ val result = launchGeckoResult { 42 }
+
+ result.then<Int> {
+ assertEquals(42, it)
+ GeckoResult.fromValue(null)
+ }.await()
+ }
+
+ @Test
+ fun fromException() = runTest {
+ val result = launchGeckoResult { throw IllegalStateException() }
+
+ result.then<Unit>(
+ {
+ assertTrue("Invalid branch", false)
+ GeckoResult.fromValue(null)
+ },
+ {
+ assertTrue(it is IllegalStateException)
+ GeckoResult.fromValue(null)
+ },
+ ).await()
+ }
+
+ @Test
+ fun asCancellableOperation() = runTest {
+ val geckoResult: GeckoResult<Int> = mock()
+ val op = geckoResult.asCancellableOperation()
+
+ whenever(geckoResult.cancel()).thenReturn(GeckoResult.fromValue(false))
+ assertFalse(op.cancel().await())
+
+ whenever(geckoResult.cancel()).thenReturn(GeckoResult.fromValue(null))
+ assertFalse(op.cancel().await())
+
+ whenever(geckoResult.cancel()).thenReturn(GeckoResult.fromValue(true))
+ assertTrue(op.cancel().await())
+
+ whenever(geckoResult.cancel()).thenReturn(GeckoResult.fromException(IllegalStateException()))
+ try {
+ op.cancel().await()
+ fail("Expected IllegalStateException")
+ } catch (e: IllegalStateException) {
+ // expected
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorageTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorageTest.kt
new file mode 100644
index 0000000000..146f9f137f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorageTest.kt
@@ -0,0 +1,274 @@
+/* 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.browser.engine.gecko
+
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import mozilla.components.browser.engine.gecko.content.blocking.GeckoTrackingProtectionException
+import mozilla.components.browser.engine.gecko.permission.geckoContentPermission
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
+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.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
+import org.mozilla.geckoview.StorageController
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class GeckoTrackingProtectionExceptionStorageTest {
+
+ private lateinit var runtime: GeckoRuntime
+
+ private lateinit var storage: GeckoTrackingProtectionExceptionStorage
+
+ @Before
+ fun setup() {
+ runtime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ storage = spy(GeckoTrackingProtectionExceptionStorage(runtime))
+ storage.scope = CoroutineScope(Dispatchers.Main)
+ }
+
+ @Test
+ fun `GIVEN a new exception WHEN adding THEN the exception is stored on the gecko storage`() {
+ val storageController = mock<StorageController>()
+ val mockGeckoSession = mock<GeckoSession>()
+ val session = spy(GeckoEngineSession(runtime, geckoSessionProvider = { mockGeckoSession }))
+
+ val geckoPermission = geckoContentPermission(type = PERMISSION_TRACKING, value = VALUE_ALLOW)
+ session.geckoPermissions = listOf(geckoPermission)
+
+ whenever(session.geckoSession).thenReturn(mockGeckoSession)
+ whenever(runtime.storageController).thenReturn(storageController)
+
+ var excludedOnTrackingProtection = false
+
+ session.register(
+ object : EngineSession.Observer {
+ override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) {
+ excludedOnTrackingProtection = excluded
+ }
+ },
+ )
+
+ storage.add(session)
+
+ verify(storageController).setPermission(geckoPermission, VALUE_ALLOW)
+ assertTrue(excludedOnTrackingProtection)
+ }
+
+ @Test
+ fun `GIVEN a persistInPrivateMode new exception WHEN adding THEN the exception is stored on the gecko storage`() {
+ val storageController = mock<StorageController>()
+ val mockGeckoSession = mock<GeckoSession>()
+ val session = spy(GeckoEngineSession(runtime, geckoSessionProvider = { mockGeckoSession }))
+
+ val geckoPermission = geckoContentPermission(type = PERMISSION_TRACKING, value = VALUE_ALLOW)
+ session.geckoPermissions = listOf(geckoPermission)
+
+ whenever(session.geckoSession).thenReturn(mockGeckoSession)
+ whenever(runtime.storageController).thenReturn(storageController)
+
+ var excludedOnTrackingProtection = false
+
+ session.register(
+ object : EngineSession.Observer {
+ override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) {
+ excludedOnTrackingProtection = excluded
+ }
+ },
+ )
+
+ storage.add(session, persistInPrivateMode = true)
+
+ verify(storageController).setPrivateBrowsingPermanentPermission(geckoPermission, VALUE_ALLOW)
+ assertTrue(excludedOnTrackingProtection)
+ }
+
+ @Test
+ fun `WHEN removing an exception by session THEN the session is removed of the exception list`() {
+ val mockGeckoSession = mock<GeckoSession>()
+ val session = spy(GeckoEngineSession(runtime, geckoSessionProvider = { mockGeckoSession }))
+
+ whenever(session.geckoSession).thenReturn(mockGeckoSession)
+ whenever(session.currentUrl).thenReturn("https://example.com/")
+ doNothing().`when`(storage).remove(anyString())
+
+ var excludedOnTrackingProtection = true
+
+ session.register(
+ object : EngineSession.Observer {
+ override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) {
+ excludedOnTrackingProtection = excluded
+ }
+ },
+ )
+
+ storage.remove(session)
+
+ verify(storage).remove(anyString())
+ assertFalse(excludedOnTrackingProtection)
+ }
+
+ @Test
+ fun `GIVEN TrackingProtectionException WHEN removing THEN remove the exception using with its contentPermission`() {
+ val geckoException = mock<GeckoTrackingProtectionException>()
+ val contentPermission = mock<ContentPermission>()
+
+ whenever(geckoException.contentPermission).thenReturn(contentPermission)
+ doNothing().`when`(storage).remove(contentPermission)
+
+ storage.remove(geckoException)
+ verify(storage).remove(geckoException.contentPermission)
+ }
+
+ @Test
+ fun `GIVEN URL WHEN removing THEN remove the exception using with its URL`() {
+ val exception = mock<TrackingProtectionException>()
+
+ whenever(exception.url).thenReturn("https://example.com/")
+ doNothing().`when`(storage).remove(anyString())
+
+ storage.remove(exception)
+ verify(storage).remove(anyString())
+ }
+
+ @Test
+ fun `WHEN removing an exception by contentPermission THEN remove it from the gecko storage`() {
+ val contentPermission = mock<ContentPermission>()
+ val storageController = mock<StorageController>()
+
+ whenever(runtime.storageController).thenReturn(storageController)
+
+ storage.remove(contentPermission)
+
+ verify(storageController).setPermission(contentPermission, VALUE_DENY)
+ }
+
+ @Test
+ fun `WHEN removing an exception by URL THEN try to find it in the gecko store and remove it`() {
+ val contentPermission =
+ geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW)
+ val storageController = mock<StorageController>()
+ val geckoResult = GeckoResult<List<ContentPermission>>()
+
+ whenever(runtime.storageController).thenReturn(storageController)
+ whenever(runtime.storageController.allPermissions).thenReturn(geckoResult)
+
+ storage.remove("https://example.com/")
+
+ geckoResult.complete(listOf(contentPermission))
+ shadowOf(getMainLooper()).idle()
+
+ verify(storageController).setPermission(contentPermission, VALUE_DENY)
+ }
+
+ @Test
+ fun `WHEN removing all exceptions THEN remove all the exceptions in the gecko store`() {
+ val mockGeckoSession = mock<GeckoSession>()
+ val session = GeckoEngineSession(runtime, geckoSessionProvider = { mockGeckoSession })
+
+ val contentPermission =
+ geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW)
+ val storageController = mock<StorageController>()
+ val geckoResult = GeckoResult<List<ContentPermission>>()
+ var excludedOnTrackingProtection = true
+
+ session.register(
+ object : EngineSession.Observer {
+ override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) {
+ excludedOnTrackingProtection = excluded
+ }
+ },
+ )
+
+ whenever(runtime.storageController).thenReturn(storageController)
+ whenever(runtime.storageController.allPermissions).thenReturn(geckoResult)
+
+ storage.removeAll(listOf(session))
+
+ geckoResult.complete(listOf(contentPermission))
+ shadowOf(getMainLooper()).idle()
+
+ verify(storageController).setPermission(contentPermission, VALUE_DENY)
+ assertFalse(excludedOnTrackingProtection)
+ }
+
+ @Test
+ fun `WHEN querying all exceptions THEN all the exceptions in the gecko store should be fetched`() {
+ val contentPermission =
+ geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW)
+ val storageController = mock<StorageController>()
+ val geckoResult = GeckoResult<List<ContentPermission>>()
+ var exceptionList: List<TrackingProtectionException>? = null
+
+ whenever(runtime.storageController).thenReturn(storageController)
+ whenever(runtime.storageController.allPermissions).thenReturn(geckoResult)
+
+ storage.fetchAll { exceptions ->
+ exceptionList = exceptions
+ }
+
+ geckoResult.complete(listOf(contentPermission))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(exceptionList!!.isNotEmpty())
+ val exception = exceptionList!!.first() as GeckoTrackingProtectionException
+
+ assertEquals("https://example.com/", exception.url)
+ assertEquals(contentPermission, exception.contentPermission)
+ }
+
+ @Test
+ fun `WHEN checking if exception is on the exception list THEN the exception is found in the storage`() {
+ val session = mock<GeckoEngineSession>()
+ val mockGeckoSession = mock<GeckoSession>()
+ var containsException = false
+ val contentPermission =
+ geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW)
+ val storageController = mock<StorageController>()
+ val geckoResult = GeckoResult<List<ContentPermission>>()
+
+ whenever(runtime.storageController).thenReturn(storageController)
+ whenever(runtime.storageController.allPermissions).thenReturn(geckoResult)
+
+ whenever(session.currentUrl).thenReturn("https://example.com/")
+ whenever(session.geckoSession).thenReturn(mockGeckoSession)
+
+ storage.contains(session) { contains ->
+ containsException = contains
+ }
+
+ geckoResult.complete(listOf(contentPermission))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(containsException)
+
+ whenever(session.currentUrl).thenReturn("")
+
+ storage.contains(session) { contains ->
+ containsException = contains
+ }
+
+ assertFalse(containsException)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoWebExtensionExceptionTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoWebExtensionExceptionTest.kt
new file mode 100644
index 0000000000..0af4abd95e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoWebExtensionExceptionTest.kt
@@ -0,0 +1,116 @@
+/* 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.browser.engine.gecko
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtensionException
+import mozilla.components.concept.engine.webextension.WebExtensionInstallException
+import mozilla.components.support.test.mock
+import mozilla.components.test.ReflectionUtils
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.WebExtension
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_BLOCKLISTED
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_CORRUPT_FILE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCOMPATIBLE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_NETWORK_FAILURE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_UNSUPPORTED_ADDON_TYPE
+import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED
+
+@RunWith(AndroidJUnit4::class)
+class GeckoWebExtensionExceptionTest {
+
+ @Test
+ fun `Handles an user cancelled exception`() {
+ val geckoException = mock<WebExtension.InstallException>()
+ ReflectionUtils.setField(geckoException, "code", ERROR_USER_CANCELED)
+ val webExtensionException =
+ GeckoWebExtensionException.createWebExtensionException(geckoException)
+
+ assertTrue(webExtensionException is WebExtensionInstallException.UserCancelled)
+ }
+
+ @Test
+ fun `Handles a generic exception`() {
+ val geckoException = Exception()
+ val webExtensionException =
+ GeckoWebExtensionException.createWebExtensionException(geckoException)
+
+ assertTrue(webExtensionException is GeckoWebExtensionException)
+ }
+
+ @Test
+ fun `Handles a blocklisted exception`() {
+ val geckoException = mock<WebExtension.InstallException>()
+ ReflectionUtils.setField(geckoException, "code", ERROR_BLOCKLISTED)
+ val webExtensionException =
+ GeckoWebExtensionException.createWebExtensionException(geckoException)
+
+ assertTrue(webExtensionException is WebExtensionInstallException.Blocklisted)
+ }
+
+ @Test
+ fun `Handles a CorruptFile exception`() {
+ val geckoException = mock<WebExtension.InstallException>()
+ ReflectionUtils.setField(geckoException, "code", ERROR_CORRUPT_FILE)
+ val webExtensionException =
+ GeckoWebExtensionException.createWebExtensionException(geckoException)
+
+ assertTrue(webExtensionException is WebExtensionInstallException.CorruptFile)
+ }
+
+ @Test
+ fun `Handles a NetworkFailure exception`() {
+ val geckoException = mock<WebExtension.InstallException>()
+ ReflectionUtils.setField(geckoException, "code", ERROR_NETWORK_FAILURE)
+ val webExtensionException =
+ GeckoWebExtensionException.createWebExtensionException(geckoException)
+
+ assertTrue(webExtensionException is WebExtensionInstallException.NetworkFailure)
+ }
+
+ @Test
+ fun `Handles an NotSigned exception`() {
+ val geckoException = mock<WebExtension.InstallException>()
+ ReflectionUtils.setField(
+ geckoException,
+ "code",
+ ERROR_SIGNEDSTATE_REQUIRED,
+ )
+ val webExtensionException =
+ GeckoWebExtensionException.createWebExtensionException(geckoException)
+
+ assertTrue(webExtensionException is WebExtensionInstallException.NotSigned)
+ }
+
+ @Test
+ fun `Handles an Incompatible exception`() {
+ val geckoException = mock<WebExtension.InstallException>()
+ ReflectionUtils.setField(
+ geckoException,
+ "code",
+ ERROR_INCOMPATIBLE,
+ )
+ val webExtensionException =
+ GeckoWebExtensionException.createWebExtensionException(geckoException)
+
+ assertTrue(webExtensionException is WebExtensionInstallException.Incompatible)
+ }
+
+ @Test
+ fun `Handles an UnsupportedAddonType exception`() {
+ val geckoException = mock<WebExtension.InstallException>()
+ ReflectionUtils.setField(
+ geckoException,
+ "code",
+ ERROR_UNSUPPORTED_ADDON_TYPE,
+ )
+ val webExtensionException = GeckoWebExtensionException.createWebExtensionException(geckoException)
+
+ assertTrue(webExtensionException is WebExtensionInstallException.UnsupportedAddonType)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/NestedGeckoViewTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/NestedGeckoViewTest.kt
new file mode 100644
index 0000000000..9e956c4566
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/NestedGeckoViewTest.kt
@@ -0,0 +1,580 @@
+/* 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.browser.engine.gecko
+
+import android.app.Activity
+import android.content.Context
+import android.os.Looper.getMainLooper
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import android.widget.FrameLayout
+import androidx.core.view.NestedScrollingChildHelper
+import androidx.core.view.ViewCompat.SCROLL_AXIS_VERTICAL
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.INPUT_HANDLING_UNKNOWN
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.mockMotionEvent
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.PanZoomController.INPUT_RESULT_HANDLED
+import org.mozilla.geckoview.PanZoomController.INPUT_RESULT_HANDLED_CONTENT
+import org.mozilla.geckoview.PanZoomController.InputResultDetail
+import org.mozilla.geckoview.PanZoomController.OVERSCROLL_FLAG_HORIZONTAL
+import org.mozilla.geckoview.PanZoomController.OVERSCROLL_FLAG_NONE
+import org.mozilla.geckoview.PanZoomController.OVERSCROLL_FLAG_VERTICAL
+import org.mozilla.geckoview.PanZoomController.SCROLLABLE_FLAG_BOTTOM
+import org.robolectric.Robolectric.buildActivity
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class NestedGeckoViewTest {
+
+ private val context: Context
+ get() = buildActivity(Activity::class.java).get()
+
+ @Test
+ fun `NestedGeckoView must delegate NestedScrollingChild implementation to childHelper`() {
+ val nestedWebView = NestedGeckoView(context)
+ val mockChildHelper: NestedScrollingChildHelper = mock()
+ nestedWebView.childHelper = mockChildHelper
+
+ doReturn(true).`when`(mockChildHelper).isNestedScrollingEnabled
+ doReturn(true).`when`(mockChildHelper).hasNestedScrollingParent()
+
+ nestedWebView.isNestedScrollingEnabled = true
+ verify(mockChildHelper).isNestedScrollingEnabled = true
+
+ assertTrue(nestedWebView.isNestedScrollingEnabled)
+ verify(mockChildHelper).isNestedScrollingEnabled
+
+ nestedWebView.startNestedScroll(1)
+ verify(mockChildHelper).startNestedScroll(1)
+
+ nestedWebView.stopNestedScroll()
+ verify(mockChildHelper).stopNestedScroll()
+
+ assertTrue(nestedWebView.hasNestedScrollingParent())
+ verify(mockChildHelper).hasNestedScrollingParent()
+
+ nestedWebView.dispatchNestedScroll(0, 0, 0, 0, null)
+ verify(mockChildHelper).dispatchNestedScroll(0, 0, 0, 0, null)
+
+ nestedWebView.dispatchNestedPreScroll(0, 0, null, null)
+ verify(mockChildHelper).dispatchNestedPreScroll(0, 0, null, null)
+
+ nestedWebView.dispatchNestedFling(0f, 0f, true)
+ verify(mockChildHelper).dispatchNestedFling(0f, 0f, true)
+
+ nestedWebView.dispatchNestedPreFling(0f, 0f)
+ verify(mockChildHelper).dispatchNestedPreFling(0f, 0f)
+ }
+
+ @Test
+ fun `verify onTouchEvent when ACTION_DOWN`() {
+ val nestedWebView = spy(NestedGeckoView(context))
+ val mockChildHelper: NestedScrollingChildHelper = mock()
+ val downEvent = mockMotionEvent(ACTION_DOWN)
+ val eventCaptor = argumentCaptor<MotionEvent>()
+ nestedWebView.childHelper = mockChildHelper
+
+ nestedWebView.onTouchEvent(downEvent)
+ shadowOf(getMainLooper()).idle()
+
+ // We pass a deep copy to `updateInputResult`.
+ // Can't easily check for equality, `eventTime` should be good enough.
+ verify(nestedWebView).updateInputResult(eventCaptor.capture())
+ assertEquals(downEvent.eventTime, eventCaptor.value.eventTime)
+ verify(mockChildHelper).startNestedScroll(SCROLL_AXIS_VERTICAL)
+ verify(nestedWebView, times(0)).callSuperOnTouchEvent(any())
+ }
+
+ @Test
+ fun `verify onTouchEvent when ACTION_MOVE`() {
+ val nestedWebView = spy(NestedGeckoView(context))
+ val mockChildHelper: NestedScrollingChildHelper = mock()
+ nestedWebView.childHelper = mockChildHelper
+ nestedWebView.inputResultDetail = nestedWebView.inputResultDetail.copy(INPUT_RESULT_HANDLED)
+ doReturn(true).`when`(nestedWebView).callSuperOnTouchEvent(any())
+
+ doReturn(true).`when`(mockChildHelper).dispatchNestedPreScroll(
+ anyInt(),
+ anyInt(),
+ any(),
+ any(),
+ )
+
+ nestedWebView.scrollOffset[0] = 1
+ nestedWebView.scrollOffset[1] = 2
+
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_DOWN, y = 0f))
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_MOVE, y = 5f))
+ assertEquals(2, nestedWebView.nestedOffsetY)
+ assertEquals(3, nestedWebView.lastY)
+
+ doReturn(true).`when`(mockChildHelper).dispatchNestedScroll(
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ any(),
+ )
+
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_MOVE, y = 10f))
+ assertEquals(6, nestedWebView.nestedOffsetY)
+ assertEquals(6, nestedWebView.lastY)
+
+ // onTouchEventForResult should be also called for ACTION_MOVE
+ verify(nestedWebView, times(3)).updateInputResult(any())
+ }
+
+ @Test
+ fun `verify onTouchEvent when ACTION_UP or ACTION_CANCEL`() {
+ val nestedWebView = spy(NestedGeckoView(context))
+ val initialInputResultDetail = nestedWebView.inputResultDetail.copy(INPUT_RESULT_HANDLED)
+ nestedWebView.inputResultDetail = initialInputResultDetail
+ val mockChildHelper: NestedScrollingChildHelper = mock()
+ nestedWebView.childHelper = mockChildHelper
+ doReturn(true).`when`(nestedWebView).callSuperOnTouchEvent(any())
+
+ assertEquals(INPUT_RESULT_HANDLED, nestedWebView.inputResultDetail.inputResult)
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_UP))
+ verify(mockChildHelper).stopNestedScroll()
+ // ACTION_UP should reset nestedWebView.inputResultDetail.
+ assertNotEquals(initialInputResultDetail, nestedWebView.inputResultDetail)
+ assertEquals(INPUT_HANDLING_UNKNOWN, nestedWebView.inputResultDetail.inputResult)
+
+ nestedWebView.inputResultDetail = initialInputResultDetail
+ assertEquals(INPUT_RESULT_HANDLED, nestedWebView.inputResultDetail.inputResult)
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_CANCEL))
+ verify(mockChildHelper, times(2)).stopNestedScroll()
+ // ACTION_CANCEL should reset nestedWebView.inputResultDetail.
+ assertNotEquals(initialInputResultDetail, nestedWebView.inputResultDetail)
+ assertEquals(INPUT_HANDLING_UNKNOWN, nestedWebView.inputResultDetail.inputResult)
+
+ // onTouchEventForResult should never be called for ACTION_UP or ACTION_CANCEL
+ verify(nestedWebView, times(0)).updateInputResult(any())
+ }
+
+ @Test
+ fun `requestDisallowInterceptTouchEvent doesn't pass touch events to parents until engineView responds`() {
+ var viewParentInterceptCounter = 0
+ val result: GeckoResult<InputResultDetail> = GeckoResult()
+ val nestedWebView = object : NestedGeckoView(context) {
+ init {
+ // We need to make the view a non-zero size so that the touch events hit it.
+ left = 0
+ top = 0
+ right = 5
+ bottom = 5
+ }
+
+ override fun superOnTouchEventForDetailResult(event: MotionEvent) = result
+ }
+ val viewParent = object : FrameLayout(context) {
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ viewParentInterceptCounter++
+ return super.onInterceptTouchEvent(ev)
+ }
+ }.apply {
+ addView(nestedWebView)
+ }
+
+ // Down action enables requestDisallowInterceptTouchEvent (and starts a gesture).
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_DOWN, y = 0f))
+
+ // `onInterceptTouchEvent` will be triggered the first time because it's the first pass.
+ assertEquals(1, viewParentInterceptCounter)
+
+ // Move action assert that onInterceptTouchEvent calls continue to be ignored.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 1f))
+
+ assertEquals(1, viewParentInterceptCounter)
+
+ // Simulate a `handled` response from the APZ GeckoEngineView API.
+ val inputResultMock = mock<InputResultDetail>().apply {
+ whenever(handledResult()).thenReturn(INPUT_RESULT_HANDLED)
+ whenever(scrollableDirections()).thenReturn(SCROLLABLE_FLAG_BOTTOM)
+ whenever(overscrollDirections()).thenReturn(OVERSCROLL_FLAG_VERTICAL)
+ }
+ result.complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ // Move action no longer ignores onInterceptTouchEvent calls.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 2f))
+
+ assertEquals(2, viewParentInterceptCounter)
+
+ // Complete the gesture by finishing with an up action.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_UP))
+
+ assertEquals(3, viewParentInterceptCounter)
+ }
+
+ @Test
+ fun `touch events are never intercepted once after scrolled down`() {
+ var viewParentInterceptCounter = 0
+ val result: GeckoResult<InputResultDetail> = GeckoResult()
+ val nestedWebView = object : NestedGeckoView(context) {
+ init {
+ // We need to make the view a non-zero size so that the touch events hit it.
+ left = 0
+ top = 0
+ right = 5
+ bottom = 5
+ }
+
+ override fun superOnTouchEventForDetailResult(event: MotionEvent): GeckoResult<InputResultDetail> = result
+ }
+
+ val viewParent = object : FrameLayout(context) {
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ viewParentInterceptCounter++
+ return super.onInterceptTouchEvent(ev)
+ }
+ }.apply {
+ addView(nestedWebView)
+ }
+
+ // Down action enables requestDisallowInterceptTouchEvent (and starts a gesture).
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_DOWN, y = 4f))
+
+ // `onInterceptTouchEvent` will be triggered the first time because it's the first pass.
+ assertEquals(1, viewParentInterceptCounter)
+
+ // Move action to scroll down assert that onInterceptTouchEvent calls continue to be ignored.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 3f))
+
+ assertEquals(1, viewParentInterceptCounter)
+
+ // Simulate a `handled` response from the APZ GeckoEngineView API.
+ val inputResultMock = mock<InputResultDetail>().apply {
+ whenever(handledResult()).thenReturn(INPUT_RESULT_HANDLED)
+ whenever(scrollableDirections()).thenReturn(SCROLLABLE_FLAG_BOTTOM)
+ whenever(overscrollDirections()).thenReturn(OVERSCROLL_FLAG_VERTICAL or OVERSCROLL_FLAG_HORIZONTAL)
+ }
+ result.complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ // Move action to scroll down further that onInterceptTouchEvent calls continue to be ignored.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 2f))
+
+ assertEquals(1, viewParentInterceptCounter)
+
+ // Complete the gesture by finishing with an up action.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_UP))
+
+ assertEquals(1, viewParentInterceptCounter)
+ }
+
+ @Suppress("UNUSED_CHANGED_VALUE")
+ @Test
+ fun `GIVEN page is not at its top touch events WHEN user pulls page up THEN parent doesn't intercept the gesture`() {
+ var viewParentInterceptCounter = 0
+ val geckoResults = mutableListOf<GeckoResult<InputResultDetail>>()
+ var resultCurrentIndex = 0
+ val nestedWebView = object : NestedGeckoView(context) {
+ init {
+ // We need to make the view a non-zero size so that the touch events hit it.
+ left = 0
+ top = 0
+ right = 5
+ bottom = 5
+ }
+
+ override fun superOnTouchEventForDetailResult(event: MotionEvent): GeckoResult<InputResultDetail> {
+ return GeckoResult<InputResultDetail>().also(geckoResults::add)
+ }
+ }
+
+ val viewParent = object : FrameLayout(context) {
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ viewParentInterceptCounter++
+ return super.onInterceptTouchEvent(ev)
+ }
+ }.apply {
+ addView(nestedWebView)
+ }
+
+ // Down action enables requestDisallowInterceptTouchEvent (and starts a gesture).
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_DOWN, y = 1f))
+
+ // `onInterceptTouchEvent` will be triggered the first time because it's the first pass.
+ assertEquals(1, viewParentInterceptCounter)
+
+ // Move action to scroll down assert that onInterceptTouchEvent calls continue to be ignored.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 2f))
+
+ // Simulate a `handled` response from the APZ GeckoEngineView API.
+ val inputResultMock = mock<InputResultDetail>().apply {
+ whenever(handledResult()).thenReturn(INPUT_RESULT_HANDLED)
+ whenever(scrollableDirections()).thenReturn(SCROLLABLE_FLAG_BOTTOM)
+ whenever(overscrollDirections()).thenReturn(OVERSCROLL_FLAG_NONE)
+ }
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ assertEquals(1, viewParentInterceptCounter)
+
+ // Move action to scroll down further that onInterceptTouchEvent calls continue to be ignored.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 3f))
+
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ assertEquals(1, viewParentInterceptCounter)
+
+ // Complete the gesture by finishing with an up action.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_UP))
+
+ assertEquals(1, viewParentInterceptCounter)
+ }
+
+ @Suppress("UNUSED_CHANGED_VALUE")
+ @Test
+ fun `verify parent don't intercept touch when gesture started with an downward scroll on a page`() {
+ var viewParentInterceptCounter = 0
+ val geckoResults = mutableListOf<GeckoResult<InputResultDetail>>()
+ var resultCurrentIndex = 0
+ var disallowInterceptTouchEventValue = false
+ val nestedWebView = object : NestedGeckoView(context) {
+ init {
+ // We need to make the view a non-zero size so that the touch events hit it.
+ left = 0
+ top = 0
+ right = 5
+ bottom = 5
+ }
+
+ override fun superOnTouchEventForDetailResult(event: MotionEvent): GeckoResult<InputResultDetail> {
+ return GeckoResult<InputResultDetail>().also(geckoResults::add)
+ }
+ }
+
+ val viewParent = object : FrameLayout(context) {
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ viewParentInterceptCounter++
+ return super.onInterceptTouchEvent(ev)
+ }
+
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
+ disallowInterceptTouchEventValue = disallowIntercept
+ super.requestDisallowInterceptTouchEvent(disallowIntercept)
+ }
+ }.apply {
+ addView(nestedWebView)
+ }
+
+ // Simulate a `handled` response from the APZ GeckoEngineView API.
+ val inputResultMock = generateOverscrollInputResultMock(INPUT_RESULT_HANDLED)
+
+ // Down action enables requestDisallowInterceptTouchEvent (and starts a gesture).
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_DOWN, y = 2f))
+
+ inputResultMock.hashCode()
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+
+ // `onInterceptTouchEvent` will be triggered the first time because it's the first pass.
+ assertEquals(1, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are still ignored by the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 2f))
+
+ // Make sure the size of results hasn't increased, meaning we don't pass the event to GeckoView to process
+ assertEquals(1, geckoResults.size)
+
+ // Make sure the parent couldn't intercept the touch event
+ assertEquals(1, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are still ignored by the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 3f))
+
+ // Make sure the size of results hasn't increased, meaning we don't pass the event to GeckoView to process
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ // Parent should now be allowed to intercept the next event, this one was not intercepted
+ assertEquals(1, viewParentInterceptCounter)
+ assertFalse(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are now reaching the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 4f))
+
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ assertEquals(2, viewParentInterceptCounter)
+ assertFalse(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls still reaching the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 4f))
+
+ geckoResults[resultCurrentIndex++].complete(generateOverscrollInputResultMock(INPUT_RESULT_HANDLED_CONTENT))
+ shadowOf(getMainLooper()).idle()
+
+ assertEquals(3, viewParentInterceptCounter)
+ assertFalse(disallowInterceptTouchEventValue)
+
+ // Move action to scroll downwards, assert that onInterceptTouchEvent calls don't reach the parent any more.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 1f))
+
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ assertEquals(4, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are still ignored by the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 4f))
+
+ assertEquals(5, resultCurrentIndex)
+ assertEquals(4, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are still ignored by the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 5f))
+
+ assertEquals(5, resultCurrentIndex)
+ assertEquals(4, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Complete the gesture by finishing with an up action.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_UP))
+ assertEquals(4, viewParentInterceptCounter)
+ }
+
+ @Suppress("UNUSED_CHANGED_VALUE")
+ @Test
+ fun `verify parent don't intercept touch when gesture started with an downward scroll on a page2`() {
+ var viewParentInterceptCounter = 0
+ val geckoResults = mutableListOf<GeckoResult<InputResultDetail>>()
+ var resultCurrentIndex = 0
+ var disallowInterceptTouchEventValue = false
+ val nestedWebView = object : NestedGeckoView(context) {
+ init {
+ // We need to make the view a non-zero size so that the touch events hit it.
+ left = 0
+ top = 0
+ right = 5
+ bottom = 5
+ }
+
+ override fun superOnTouchEventForDetailResult(event: MotionEvent): GeckoResult<InputResultDetail> {
+ return GeckoResult<InputResultDetail>().also(geckoResults::add)
+ }
+ }
+
+ val viewParent = object : FrameLayout(context) {
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ viewParentInterceptCounter++
+ return super.onInterceptTouchEvent(ev)
+ }
+
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
+ disallowInterceptTouchEventValue = disallowIntercept
+ super.requestDisallowInterceptTouchEvent(disallowIntercept)
+ }
+ }.apply {
+ addView(nestedWebView)
+ }
+
+ // Simulate a `handled` response from the APZ GeckoEngineView API.
+ val inputResultMock = mock<InputResultDetail>().apply {
+ whenever(handledResult()).thenReturn(INPUT_RESULT_HANDLED)
+ whenever(scrollableDirections()).thenReturn(SCROLLABLE_FLAG_BOTTOM)
+ whenever(overscrollDirections()).thenReturn(OVERSCROLL_FLAG_VERTICAL or OVERSCROLL_FLAG_HORIZONTAL)
+ }
+
+ // Down action enables requestDisallowInterceptTouchEvent (and starts a gesture).
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_DOWN, y = 2f))
+
+ inputResultMock.hashCode()
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+
+ // `onInterceptTouchEvent` will be triggered the first time because it's the first pass.
+ assertEquals(1, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are still ignored by the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 2f))
+
+ // Make sure the size of results hasn't increased, meaning we don't pass the event to GeckoView to process
+ assertEquals(1, geckoResults.size)
+
+ // Make sure the parent couldn't intercept the touch event
+ assertEquals(1, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are still ignored by the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 3f))
+
+ // Make sure the size of results hasn't increased, meaning we don't pass the event to GeckoView to process
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ // Parent should now be allowed to intercept the next event, this one was not intercepted
+ assertEquals(1, viewParentInterceptCounter)
+ assertFalse(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are now reaching the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 4f))
+
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ assertEquals(2, viewParentInterceptCounter)
+ assertFalse(disallowInterceptTouchEventValue)
+
+ // Move action to scroll downwards, assert that onInterceptTouchEvent calls don't reach the parent any more.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 1f))
+
+ geckoResults[resultCurrentIndex++].complete(inputResultMock)
+ shadowOf(getMainLooper()).idle()
+
+ assertEquals(3, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are still ignored by the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 4f))
+
+ assertEquals(4, resultCurrentIndex)
+ assertEquals(3, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Move action to scroll upwards, assert that onInterceptTouchEvent calls are still ignored by the parent.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_MOVE, y = 5f))
+
+ assertEquals(4, resultCurrentIndex)
+ assertEquals(3, viewParentInterceptCounter)
+ assertTrue(disallowInterceptTouchEventValue)
+
+ // Complete the gesture by finishing with an up action.
+ viewParent.dispatchTouchEvent(mockMotionEvent(ACTION_UP))
+ assertEquals(3, viewParentInterceptCounter)
+ }
+
+ private fun generateOverscrollInputResultMock(inputResult: Int) = mock<InputResultDetail>().apply {
+ whenever(handledResult()).thenReturn(inputResult)
+ whenever(scrollableDirections()).thenReturn(SCROLLABLE_FLAG_BOTTOM)
+ whenever(overscrollDirections()).thenReturn(OVERSCROLL_FLAG_VERTICAL or OVERSCROLL_FLAG_HORIZONTAL)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegateTest.kt
new file mode 100644
index 0000000000..461b0f4df5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegateTest.kt
@@ -0,0 +1,75 @@
+/* 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.browser.engine.gecko.activity
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.IntentSender
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.activity.ActivityDelegate
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mozilla.geckoview.GeckoResult
+import java.lang.ref.WeakReference
+
+@RunWith(AndroidJUnit4::class)
+class GeckoActivityDelegateTest {
+ lateinit var pendingIntent: PendingIntent
+
+ @Before
+ fun setup() {
+ pendingIntent = mock()
+ `when`(pendingIntent.intentSender).thenReturn(mock())
+ }
+
+ @Test
+ fun `onStartActivityForResult is completed successfully`() {
+ val delegate: ActivityDelegate = object : ActivityDelegate {
+ override fun startIntentSenderForResult(intent: IntentSender, onResult: (Intent?) -> Unit) {
+ onResult(mock())
+ }
+ }
+
+ val geckoActivityDelegate = GeckoActivityDelegate(WeakReference(delegate))
+ val result = geckoActivityDelegate.onStartActivityForResult(pendingIntent)
+
+ result.accept {
+ assertNotNull(it)
+ }
+ }
+
+ @Test
+ fun `onStartActivityForResult completes exceptionally on null response`() {
+ val delegate: ActivityDelegate = object : ActivityDelegate {
+ override fun startIntentSenderForResult(intent: IntentSender, onResult: (Intent?) -> Unit) {
+ onResult(null)
+ }
+ }
+
+ val geckoActivityDelegate = GeckoActivityDelegate(WeakReference(delegate))
+ val result = geckoActivityDelegate.onStartActivityForResult(pendingIntent)
+
+ result.exceptionally { throwable ->
+ assertEquals("Activity for result failed.", throwable.localizedMessage)
+ GeckoResult.fromValue(null)
+ }
+ }
+
+ @Test
+ fun `onStartActivityForResult completes exceptionally when there is no object attached to the weak reference`() {
+ val geckoActivityDelegate = GeckoActivityDelegate(WeakReference(null))
+ val result = geckoActivityDelegate.onStartActivityForResult(pendingIntent)
+
+ result.exceptionally { throwable ->
+ assertEquals("Activity for result failed; no delegate attached.", throwable.localizedMessage)
+ GeckoResult.fromValue(null)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegateTest.kt
new file mode 100644
index 0000000000..c519110d64
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegateTest.kt
@@ -0,0 +1,62 @@
+/* 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.browser.engine.gecko.activity
+
+import android.content.pm.ActivityInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.activity.OrientationDelegate
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.AllowOrDeny
+
+@RunWith(AndroidJUnit4::class)
+class GeckoScreenOrientationDelegateTest {
+ @Test
+ fun `GIVEN a delegate is set WHEN the orientation should be locked THEN call this on the delegate`() {
+ val activityDelegate = mock<OrientationDelegate>()
+ val geckoDelegate = GeckoScreenOrientationDelegate(activityDelegate)
+
+ geckoDelegate.onOrientationLock(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+
+ verify(activityDelegate).onOrientationLock(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+ }
+
+ @Test
+ fun `GIVEN a delegate is set WHEN the orientation should be locked THEN return ALLOW depending on the delegate response`() {
+ val activityDelegate = object : OrientationDelegate {
+ override fun onOrientationLock(requestedOrientation: Int) = true
+ }
+ val geckoDelegate = GeckoScreenOrientationDelegate(activityDelegate)
+
+ val result = geckoDelegate.onOrientationLock(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)
+
+ assertTrue(result.poll(1) == AllowOrDeny.ALLOW)
+ }
+
+ @Test
+ fun `GIVEN a delegate is set WHEN the orientation should be locked THEN return DENY depending on the delegate response`() {
+ val activityDelegate = object : OrientationDelegate {
+ override fun onOrientationLock(requestedOrientation: Int) = false
+ }
+ val geckoDelegate = GeckoScreenOrientationDelegate(activityDelegate)
+
+ val result = geckoDelegate.onOrientationLock(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)
+
+ assertTrue(result.poll(1) == AllowOrDeny.DENY)
+ }
+
+ @Test
+ fun `GIVEN a delegate is set WHEN the orientation should be unlocked THEN call this on the delegate`() {
+ val activityDelegate = mock<OrientationDelegate>()
+ val geckoDelegate = GeckoScreenOrientationDelegate(activityDelegate)
+
+ geckoDelegate.onOrientationUnlock()
+
+ verify(activityDelegate).onOrientationUnlock()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegateTest.kt
new file mode 100644
index 0000000000..4eeeea4460
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegateTest.kt
@@ -0,0 +1,28 @@
+/* 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.browser.engine.gecko.activity
+
+import android.app.Activity
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.lang.ref.WeakReference
+
+class GeckoViewActivityContextDelegateTest {
+
+ @Test
+ fun `getActivityContext returns the same activity as set on the delegate`() {
+ val mockActivity = mock<Activity>()
+ val activityContextDelegate = GeckoViewActivityContextDelegate(WeakReference(mockActivity))
+ assertTrue(mockActivity == activityContextDelegate.activityContext)
+ }
+
+ @Test
+ fun `getActivityContext returns null when the activity reference is null`() {
+ val activityContextDelegate = GeckoViewActivityContextDelegate(WeakReference(null))
+ assertNull(activityContextDelegate.activityContext)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorageTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorageTest.kt
new file mode 100644
index 0000000000..958553706b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorageTest.kt
@@ -0,0 +1,161 @@
+/* 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.browser.engine.gecko.cookiebanners
+
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.DISABLED
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.StorageController
+
+@ExperimentalCoroutinesApi
+class GeckoCookieBannersStorageTest {
+ private lateinit var runtime: GeckoRuntime
+ private lateinit var geckoStorage: GeckoCookieBannersStorage
+ private lateinit var storageController: StorageController
+ private lateinit var reportSiteDomainsRepository: ReportSiteDomainsRepository
+
+ @Before
+ fun setup() {
+ storageController = mock()
+ runtime = mock()
+ reportSiteDomainsRepository = mock()
+
+ whenever(runtime.storageController).thenReturn(storageController)
+
+ geckoStorage = spy(GeckoCookieBannersStorage(runtime, reportSiteDomainsRepository))
+ }
+
+ @Test
+ fun `GIVEN a cookie banner mode WHEN adding an exception THEN add an exception for the given uri and browsing mode`() =
+ runTest {
+ val uri = "https://www.mozilla.org"
+
+ doNothing().`when`(geckoStorage)
+ .setGeckoException(uri = uri, mode = DISABLED, privateBrowsing = false)
+
+ geckoStorage.addException(uri = uri, privateBrowsing = false)
+
+ verify(geckoStorage).setGeckoException(uri, DISABLED, false)
+ }
+
+ @Test
+ fun `GIVEN uri and browsing mode WHEN removing an exception THEN remove the exception`() =
+ runTest {
+ val uri = "https://www.mozilla.org"
+
+ doNothing().`when`(geckoStorage).removeGeckoException(uri, false)
+
+ geckoStorage.removeException(uri = uri, privateBrowsing = false)
+
+ verify(geckoStorage).removeGeckoException(uri, false)
+ }
+
+ @Test
+ fun `GIVEN uri and browsing mode WHEN querying an exception THEN return the matching exception`() =
+ runTest {
+ val uri = "https://www.mozilla.org"
+
+ doReturn(REJECT_OR_ACCEPT_ALL).`when`(geckoStorage)
+ .queryExceptionInGecko(uri = uri, privateBrowsing = false)
+
+ val result = geckoStorage.findExceptionFor(uri = uri, privateBrowsing = false)
+ assertEquals(REJECT_OR_ACCEPT_ALL, result)
+ }
+
+ @Test
+ fun `GIVEN error WHEN querying an exception THEN return null`() =
+ runTest {
+ val uri = "https://www.mozilla.org"
+
+ doReturn(null).`when`(geckoStorage)
+ .queryExceptionInGecko(uri = uri, privateBrowsing = false)
+
+ val result = geckoStorage.findExceptionFor(uri = uri, privateBrowsing = false)
+ assertNull(result)
+ }
+
+ @Test
+ fun `GIVEN uri and browsing mode WHEN checking for an exception THEN indicate if it has exceptions`() =
+ runTest {
+ val uri = "https://www.mozilla.org"
+
+ doReturn(REJECT_OR_ACCEPT_ALL).`when`(geckoStorage)
+ .queryExceptionInGecko(uri = uri, privateBrowsing = false)
+
+ var result = geckoStorage.hasException(uri = uri, privateBrowsing = false)
+
+ assertFalse(result!!)
+
+ Mockito.reset(geckoStorage)
+
+ doReturn(DISABLED).`when`(geckoStorage)
+ .queryExceptionInGecko(uri = uri, privateBrowsing = false)
+
+ result = geckoStorage.hasException(uri = uri, privateBrowsing = false)
+
+ assertTrue(result!!)
+ }
+
+ @Test
+ fun `GIVEN an error WHEN checking for an exception THEN indicate if that an error happened`() =
+ runTest {
+ val uri = "https://www.mozilla.org"
+
+ doReturn(null).`when`(geckoStorage)
+ .queryExceptionInGecko(uri = uri, privateBrowsing = false)
+
+ val result = geckoStorage.hasException(uri = uri, privateBrowsing = false)
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `GIVEN a cookie banner mode WHEN adding a persistent exception in private mode THEN add a persistent exception for the given uri in private browsing mode`() =
+ runTest {
+ val uri = "https://www.mozilla.org"
+
+ doNothing().`when`(geckoStorage)
+ .setPersistentPrivateGeckoException(uri = uri, mode = DISABLED)
+
+ geckoStorage.addPersistentExceptionInPrivateMode(uri = uri)
+
+ verify(geckoStorage).setPersistentPrivateGeckoException(uri, DISABLED)
+ }
+
+ @Test
+ fun `GIVEN site domain url WHEN checking if site domain is reported THEN the report site domain repository gets called`() =
+ runTest {
+ val reportSiteDomainUrl = "mozilla.org"
+
+ geckoStorage.isSiteDomainReported(reportSiteDomainUrl)
+
+ verify(reportSiteDomainsRepository).isSiteDomainReported(reportSiteDomainUrl)
+ }
+
+ @Test
+ fun `GIVEN site domain url WHEN saving a site domain THEN the save method from repository should get called`() =
+ runTest {
+ val reportSiteDomainUrl = "mozilla.org"
+
+ geckoStorage.saveSiteDomain(reportSiteDomainUrl)
+
+ verify(reportSiteDomainsRepository).saveSiteDomain(reportSiteDomainUrl)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepositoryTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepositoryTest.kt
new file mode 100644
index 0000000000..dbc809ef2c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepositoryTest.kt
@@ -0,0 +1,74 @@
+/* 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.browser.engine.gecko.cookiebanners
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.preferencesDataStoreFile
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class ReportSiteDomainsRepositoryTest {
+
+ companion object {
+ const val TEST_DATASTORE_NAME = "test_data_store"
+ }
+
+ private lateinit var testDataStore: DataStore<Preferences>
+
+ private lateinit var reportSiteDomainsRepository: ReportSiteDomainsRepository
+
+ @Before
+ fun setUp() {
+ testDataStore = PreferenceDataStoreFactory.create(
+ produceFile = { testContext.preferencesDataStoreFile(TEST_DATASTORE_NAME) },
+ )
+ reportSiteDomainsRepository = ReportSiteDomainsRepository(testDataStore)
+ }
+
+ @After
+ fun cleanUp() = runTest { testDataStore.edit { it.clear() } }
+
+ @Test
+ fun `GIVEN site domain url WHEN site domain url is not saved THEN is side domain reported return false`() =
+ runTest {
+ assertFalse(reportSiteDomainsRepository.isSiteDomainReported("mozilla.org"))
+ }
+
+ @Test
+ fun `GIVEN site domain url WHEN site domain url is saved THEN is side domain reported return true`() =
+ runTest {
+ val siteDomainReported = "mozilla.org"
+
+ reportSiteDomainsRepository.saveSiteDomain(siteDomainReported)
+
+ assertTrue(reportSiteDomainsRepository.isSiteDomainReported(siteDomainReported))
+ }
+
+ @Test
+ fun `GIVEN site domain urls WHEN site domain urls are saved THEN is side domain reported return true for each one`() =
+ runTest {
+ val mozillaSiteDomainReported = "mozilla.org"
+ val youtubeSiteDomainReported = "youtube.com"
+
+ reportSiteDomainsRepository.saveSiteDomain(mozillaSiteDomainReported)
+ reportSiteDomainsRepository.saveSiteDomain(youtubeSiteDomainReported)
+
+ assertTrue(reportSiteDomainsRepository.isSiteDomainReported(mozillaSiteDomainReported))
+ assertTrue(reportSiteDomainsRepository.isSiteDomainReported(youtubeSiteDomainReported))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicyKtTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicyKtTest.kt
new file mode 100644
index 0000000000..e29cfcb61d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicyKtTest.kt
@@ -0,0 +1,81 @@
+/* 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.browser.engine.gecko.ext
+
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mozilla.geckoview.ContentBlocking.EtpLevel
+
+class TrackingProtectionPolicyKtTest {
+
+ private val defaultSafeBrowsing = arrayOf(EngineSession.SafeBrowsingPolicy.RECOMMENDED)
+
+ @Test
+ fun `transform the policy to a GeckoView ContentBlockingSetting`() {
+ val policy = TrackingProtectionPolicy.recommended()
+ val setting = policy.toContentBlockingSetting()
+ val cookieBannerSetting = EngineSession.CookieBannerHandlingMode.REJECT_OR_ACCEPT_ALL
+ val cookieBannerSettingPrivateBrowsing = EngineSession.CookieBannerHandlingMode.DISABLED
+
+ assertEquals(policy.getEtpLevel(), setting.enhancedTrackingProtectionLevel)
+ assertEquals(policy.getAntiTrackingPolicy(), setting.antiTrackingCategories)
+ assertEquals(policy.cookiePolicy.id, setting.cookieBehavior)
+ assertEquals(policy.cookiePolicyPrivateMode.id, setting.cookieBehavior)
+ assertEquals(defaultSafeBrowsing.sumOf { it.id }, setting.safeBrowsingCategories)
+ assertEquals(setting.strictSocialTrackingProtection, policy.strictSocialTrackingProtection)
+ assertEquals(setting.cookiePurging, policy.cookiePurging)
+ assertEquals(EngineSession.CookieBannerHandlingMode.DISABLED.mode, setting.cookieBannerMode)
+ assertEquals(EngineSession.CookieBannerHandlingMode.REJECT_ALL.mode, setting.cookieBannerModePrivateBrowsing)
+ assertFalse(setting.cookieBannerDetectOnlyMode)
+ assertFalse(setting.queryParameterStrippingEnabled)
+ assertFalse(setting.queryParameterStrippingPrivateBrowsingEnabled)
+ assertEquals("", setting.queryParameterStrippingAllowList[0])
+ assertEquals("", setting.queryParameterStrippingStripList[0])
+
+ val policyWithSafeBrowsing =
+ TrackingProtectionPolicy.recommended().toContentBlockingSetting(
+ safeBrowsingPolicy = emptyArray(),
+ cookieBannerHandlingMode = cookieBannerSetting,
+ cookieBannerHandlingModePrivateBrowsing = cookieBannerSettingPrivateBrowsing,
+ cookieBannerHandlingDetectOnlyMode = true,
+ cookieBannerGlobalRulesEnabled = true,
+ cookieBannerGlobalRulesSubFramesEnabled = true,
+ queryParameterStripping = true,
+ queryParameterStrippingPrivateBrowsing = true,
+ queryParameterStrippingAllowList = "AllowList",
+ queryParameterStrippingStripList = "StripList",
+ )
+ assertEquals(0, policyWithSafeBrowsing.safeBrowsingCategories)
+ assertEquals(cookieBannerSetting.mode, policyWithSafeBrowsing.cookieBannerMode)
+ assertEquals(cookieBannerSettingPrivateBrowsing.mode, policyWithSafeBrowsing.cookieBannerModePrivateBrowsing)
+ assertTrue(policyWithSafeBrowsing.cookieBannerDetectOnlyMode)
+ assertTrue(policyWithSafeBrowsing.cookieBannerGlobalRulesEnabled)
+ assertTrue(policyWithSafeBrowsing.cookieBannerGlobalRulesSubFramesEnabled)
+ assertTrue(policyWithSafeBrowsing.queryParameterStrippingEnabled)
+ assertTrue(policyWithSafeBrowsing.queryParameterStrippingPrivateBrowsingEnabled)
+ assertEquals("AllowList", policyWithSafeBrowsing.queryParameterStrippingAllowList[0])
+ assertEquals("StripList", policyWithSafeBrowsing.queryParameterStrippingStripList[0])
+ }
+
+ @Test
+ fun `getEtpLevel is always Strict unless None`() {
+ assertEquals(EtpLevel.STRICT, TrackingProtectionPolicy.recommended().getEtpLevel())
+ assertEquals(EtpLevel.STRICT, TrackingProtectionPolicy.strict().getEtpLevel())
+ assertEquals(EtpLevel.NONE, TrackingProtectionPolicy.none().getEtpLevel())
+ }
+
+ @Test
+ fun `getStrictSocialTrackingProtection is true if category is STRICT`() {
+ val recommendedPolicy = TrackingProtectionPolicy.recommended()
+ val strictPolicy = TrackingProtectionPolicy.strict()
+
+ assertFalse(recommendedPolicy.toContentBlockingSetting().strictSocialTrackingProtection)
+ assertTrue(strictPolicy.toContentBlockingSetting().strictSocialTrackingProtection)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchUnitTestCases.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchUnitTestCases.kt
new file mode 100644
index 0000000000..3a889550ed
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchUnitTestCases.kt
@@ -0,0 +1,351 @@
+/* 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.browser.engine.gecko.fetch
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import mozilla.components.tooling.fetch.tests.FetchTestCases
+import okhttp3.Headers.Companion.toHeaders
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoWebExecutor
+import org.mozilla.geckoview.WebRequest
+import org.mozilla.geckoview.WebRequestError
+import org.mozilla.geckoview.WebResponse
+import java.io.IOException
+import java.nio.charset.Charset
+import java.util.concurrent.TimeoutException
+
+/**
+ * We can't run standard JVM unit tests for GWE. Therefore, we provide both
+ * instrumented tests as well as these unit tests which mock both requests
+ * and responses. While these tests guard our logic to map responses to our
+ * concept-fetch abstractions, they are not sufficient to guard the full
+ * functionality of [GeckoViewFetchClient]. That's why end-to-end tests are
+ * provided in instrumented tests.
+ */
+@RunWith(AndroidJUnit4::class)
+class GeckoViewFetchUnitTestCases : FetchTestCases() {
+
+ override fun createNewClient(): Client {
+ val client = GeckoViewFetchClient(testContext, mock())
+ geckoWebExecutor?.let { client.executor = it }
+ return client
+ }
+
+ override fun createWebServer(): MockWebServer {
+ return mockWebServer ?: super.createWebServer()
+ }
+
+ private var geckoWebExecutor: GeckoWebExecutor? = null
+ private var mockWebServer: MockWebServer? = null
+
+ @Before
+ fun setup() {
+ geckoWebExecutor = null
+ }
+
+ @Test
+ fun clientInstance() {
+ assertTrue(createNewClient() is GeckoViewFetchClient)
+ }
+
+ @Test
+ override fun get200WithDuplicatedCacheControlRequestHeaders() {
+ val headerMap = mapOf("Cache-Control" to "no-cache, no-store")
+ mockRequest(headerMap)
+ mockResponse(200)
+
+ super.get200WithDuplicatedCacheControlRequestHeaders()
+ }
+
+ @Test
+ override fun get200WithDuplicatedCacheControlResponseHeaders() {
+ val responseHeaderMap = mapOf(
+ "Cache-Control" to "no-cache, no-store",
+ "Content-Length" to "16",
+ )
+ mockResponse(200, responseHeaderMap)
+
+ super.get200WithDuplicatedCacheControlResponseHeaders()
+ }
+
+ @Test
+ override fun get200OverridingDefaultHeaders() {
+ val headerMap = mapOf(
+ "Accept" to "text/html",
+ "Accept-Encoding" to "deflate",
+ "User-Agent" to "SuperBrowser/1.0",
+ "Connection" to "close",
+ )
+ mockRequest(headerMap)
+ mockResponse(200)
+
+ super.get200OverridingDefaultHeaders()
+ }
+
+ @Test
+ override fun get200WithGzippedBody() {
+ val responseHeaderMap = mapOf("Content-Encoding" to "gzip")
+ mockRequest()
+ mockResponse(200, responseHeaderMap, "This is compressed")
+
+ super.get200WithGzippedBody()
+ }
+
+ @Test
+ override fun get200WithHeaders() {
+ val requestHeaders = mapOf(
+ "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Encoding" to "gzip, deflate",
+ "Accept-Language" to "en-US,en;q=0.5",
+ "Connection" to "keep-alive",
+ "User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0",
+ )
+ mockRequest(requestHeaders)
+ mockResponse(200)
+
+ super.get200WithHeaders()
+ }
+
+ @Test
+ override fun get200WithReadTimeout() {
+ mockRequest()
+ mockResponse(200)
+
+ val geckoResult = mock<GeckoResult<*>>()
+ whenever(geckoResult.poll(anyLong())).thenThrow(TimeoutException::class.java)
+ @Suppress("UNCHECKED_CAST")
+ whenever(geckoWebExecutor!!.fetch(any(), anyInt())).thenReturn(geckoResult as GeckoResult<WebResponse>)
+
+ super.get200WithReadTimeout()
+ }
+
+ @Test
+ override fun get200WithStringBody() {
+ mockRequest()
+ mockResponse(200, body = "Hello World")
+
+ super.get200WithStringBody()
+ }
+
+ @Test
+ override fun get302FollowRedirects() {
+ mockResponse(200)
+
+ val request = mock<Request>()
+ whenever(request.url).thenReturn("https://mozilla.org")
+ whenever(request.method).thenReturn(Request.Method.GET)
+ whenever(request.redirect).thenReturn(Request.Redirect.FOLLOW)
+ createNewClient().fetch(request)
+
+ verify(geckoWebExecutor)!!.fetch(any(), eq(GeckoWebExecutor.FETCH_FLAGS_NONE))
+ }
+
+ @Test
+ override fun get302FollowRedirectsDisabled() {
+ mockResponse(200)
+
+ val request = mock<Request>()
+ whenever(request.url).thenReturn("https://mozilla.org")
+ whenever(request.method).thenReturn(Request.Method.GET)
+ whenever(request.redirect).thenReturn(Request.Redirect.MANUAL)
+ createNewClient().fetch(request)
+
+ verify(geckoWebExecutor)!!.fetch(any(), eq(GeckoWebExecutor.FETCH_FLAGS_NO_REDIRECTS))
+ }
+
+ @Test
+ override fun get404WithBody() {
+ mockRequest()
+ mockResponse(404, body = "Error")
+ super.get404WithBody()
+ }
+
+ @Test
+ override fun post200WithBody() {
+ mockRequest(method = "POST", body = "Hello World")
+ mockResponse(200)
+ super.post200WithBody()
+ }
+
+ @Test
+ override fun put201FileUpload() {
+ mockRequest(method = "PUT", headerMap = mapOf("Content-Type" to "image/png"), body = "I am an image file!")
+ mockResponse(201, headerMap = mapOf("Location" to "/your-image.png"), body = "Thank you!")
+ super.put201FileUpload()
+ }
+
+ @Test(expected = IOException::class)
+ fun pollReturningNull() {
+ mockResponse(200)
+
+ val geckoResult = mock<GeckoResult<*>>()
+ whenever(geckoResult.poll(anyLong())).thenReturn(null)
+ @Suppress("UNCHECKED_CAST")
+ whenever(geckoWebExecutor!!.fetch(any(), anyInt())).thenReturn(geckoResult as GeckoResult<WebResponse>)
+
+ val request = mock<Request>()
+ whenever(request.url).thenReturn("https://mozilla.org")
+ whenever(request.method).thenReturn(Request.Method.GET)
+ createNewClient().fetch(request)
+ }
+
+ @Test
+ override fun get200WithCookiePolicy() {
+ mockResponse(200)
+
+ val request = mock<Request>()
+ whenever(request.url).thenReturn("https://mozilla.org")
+ whenever(request.method).thenReturn(Request.Method.GET)
+ whenever(request.cookiePolicy).thenReturn(Request.CookiePolicy.OMIT)
+ createNewClient().fetch(request)
+
+ verify(geckoWebExecutor)!!.fetch(any(), eq(GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS))
+ }
+
+ @Test
+ fun performPrivateRequest() {
+ mockResponse(200)
+
+ val request = mock<Request>()
+ whenever(request.url).thenReturn("https://mozilla.org")
+ whenever(request.method).thenReturn(Request.Method.GET)
+ whenever(request.private).thenReturn(true)
+ createNewClient().fetch(request)
+
+ verify(geckoWebExecutor)!!.fetch(any(), eq(GeckoWebExecutor.FETCH_FLAGS_PRIVATE))
+ }
+
+ @Test
+ override fun get200WithContentTypeCharset() {
+ val request = mock<Request>()
+ whenever(request.url).thenReturn("https://mozilla.org")
+ whenever(request.method).thenReturn(Request.Method.GET)
+
+ mockResponse(
+ 200,
+ headerMap = mapOf("Content-Type" to "text/html; charset=ISO-8859-1"),
+ body = "ÄäÖöÜü",
+ charset = Charsets.ISO_8859_1,
+ )
+
+ val response = createNewClient().fetch(request)
+ assertEquals("ÄäÖöÜü", response.body.string())
+ }
+
+ @Test
+ override fun get200WithCacheControl() {
+ mockResponse(200)
+
+ val request = mock<Request>()
+ whenever(request.url).thenReturn("https://mozilla.org")
+ whenever(request.method).thenReturn(Request.Method.GET)
+ whenever(request.useCaches).thenReturn(false)
+ createNewClient().fetch(request)
+
+ val captor = ArgumentCaptor.forClass(WebRequest::class.java)
+
+ verify(geckoWebExecutor)!!.fetch(captor.capture(), eq(GeckoWebExecutor.FETCH_FLAGS_NONE))
+ assertEquals(WebRequest.CACHE_MODE_RELOAD, captor.value.cacheMode)
+ }
+
+ @Test(expected = IOException::class)
+ override fun getThrowsIOExceptionWhenHostNotReachable() {
+ val executor = mock<GeckoWebExecutor>()
+ whenever(executor.fetch(any(), anyInt())).thenAnswer { throw WebRequestError(0, 0) }
+ geckoWebExecutor = executor
+
+ createNewClient().fetch(Request(""))
+ }
+
+ @Test
+ fun toResponseMustReturn200ForBlobUrls() {
+ val builder = WebResponse.Builder("blob:https://mdn.mozillademos.org/d518464c-5075-9046-aef2-9c313214ed53").statusCode(0).build()
+ assertEquals(Response.SUCCESS, builder.toResponse().status)
+ }
+
+ @Test
+ fun get200WithReferrerUrl() {
+ mockResponse(200)
+
+ val request = mock<Request>()
+ whenever(request.url).thenReturn("https://mozilla.org")
+ whenever(request.method).thenReturn(Request.Method.GET)
+ whenever(request.referrerUrl).thenReturn("https://mozilla.org")
+ createNewClient().fetch(request)
+
+ val captor = ArgumentCaptor.forClass(WebRequest::class.java)
+
+ verify(geckoWebExecutor)!!.fetch(captor.capture(), eq(GeckoWebExecutor.FETCH_FLAGS_NONE))
+ assertEquals("https://mozilla.org", captor.value.referrer)
+ }
+
+ @Test
+ fun toResponseMustReturn200ForDataUrls() {
+ val builder = WebResponse.Builder("data:,Hello%2C%20World!").statusCode(0).build()
+ assertEquals(Response.SUCCESS, builder.toResponse().status)
+ }
+
+ private fun mockRequest(headerMap: Map<String, String>? = null, body: String? = null, method: String = "GET") {
+ val server = mock<MockWebServer>()
+ whenever(server.url(any())).thenReturn(mock())
+ val request = mock<RecordedRequest>()
+ whenever(request.method).thenReturn(method)
+
+ headerMap?.let {
+ whenever(request.headers).thenReturn(headerMap.toHeaders())
+ whenever(request.getHeader(any())).thenAnswer { inv -> it[inv.getArgument(0)] }
+ }
+
+ body?.let {
+ val buffer = okio.Buffer()
+ buffer.write(body.toByteArray())
+ whenever(request.body).thenReturn(buffer)
+ }
+
+ whenever(server.takeRequest()).thenReturn(request)
+ mockWebServer = server
+ }
+
+ private fun mockResponse(
+ statusCode: Int,
+ headerMap: Map<String, String>? = null,
+ body: String? = null,
+ charset: Charset = Charsets.UTF_8,
+ ) {
+ val executor = mock<GeckoWebExecutor>()
+ val builder = WebResponse.Builder("").statusCode(statusCode)
+ headerMap?.let {
+ headerMap.forEach { (k, v) -> builder.addHeader(k, v) }
+ }
+
+ body?.let {
+ builder.body(it.byteInputStream(charset))
+ }
+
+ val response = builder.build()
+
+ whenever(executor.fetch(any(), anyInt())).thenReturn(GeckoResult.fromValue(response))
+ geckoWebExecutor = executor
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/integration/SettingUpdaterTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/integration/SettingUpdaterTest.kt
new file mode 100644
index 0000000000..c07729894e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/integration/SettingUpdaterTest.kt
@@ -0,0 +1,69 @@
+/* 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.browser.engine.gecko.integration
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SettingUpdaterTest {
+
+ @Test
+ fun `test updateValue`() {
+ val subject = DummySettingUpdater("current", "new")
+ assertEquals("current", subject.value)
+
+ subject.updateValue()
+ assertEquals("new", subject.value)
+ }
+
+ @Test
+ fun `test enabled updates value`() {
+ val subject = DummySettingUpdater("current", "new")
+ assertEquals("current", subject.value)
+
+ subject.enabled = true
+ assertEquals("new", subject.value)
+
+ // disabling doesn't update the value.
+ subject.nextValue = "disabled"
+ subject.enabled = false
+ assertEquals("new", subject.value)
+ }
+
+ @Test
+ fun `test registering and deregistering for updates`() {
+ val subject = DummySettingUpdater("current", "new")
+ assertFalse("Initialized not registering for updates", subject.registered)
+
+ subject.updateValue()
+ assertFalse("updateValue not registering for updates", subject.registered)
+
+ subject.enabled = true
+ assertTrue("enabled = true registering for updates", subject.registered)
+
+ subject.enabled = false
+ assertFalse("enabled = false deregistering for updates", subject.registered)
+ }
+}
+
+class DummySettingUpdater(
+ override var value: String = "",
+ var nextValue: String,
+) : SettingUpdater<String>() {
+
+ var registered = false
+
+ override fun registerForUpdates() {
+ registered = true
+ }
+
+ override fun unregisterForUpdates() {
+ registered = false
+ }
+
+ override fun findValue() = nextValue
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegateTest.kt
new file mode 100644
index 0000000000..71b9303ac9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegateTest.kt
@@ -0,0 +1,116 @@
+/* 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.browser.engine.gecko.media
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertTrue
+import junit.framework.TestCase.fail
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.media.RecordingDevice
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import mozilla.components.test.ReflectionUtils
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.GeckoRuntime
+import java.security.InvalidParameterException
+import org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice as GeckoRecordingDevice
+
+@RunWith(AndroidJUnit4::class)
+class GeckoMediaDelegateTest {
+ private lateinit var runtime: GeckoRuntime
+
+ @Before
+ fun setup() {
+ runtime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ }
+
+ @Test
+ fun `WHEN onRecordingStatusChanged is called THEN notify onRecordingStateChanged`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var onRecordingWasCalled = false
+ val geckoRecordingDevice = createGeckoRecordingDevice(
+ status = GeckoRecordingDevice.Status.RECORDING,
+ type = GeckoRecordingDevice.Type.CAMERA,
+ )
+ val gecko = GeckoMediaDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onRecordingStateChanged(devices: List<RecordingDevice>) {
+ onRecordingWasCalled = true
+ }
+ },
+ )
+
+ gecko.onRecordingStatusChanged(mock(), arrayOf(geckoRecordingDevice))
+
+ assertTrue(onRecordingWasCalled)
+ }
+
+ @Test
+ fun `GIVEN a GeckoRecordingDevice status WHEN calling toStatus THEN covert to the RecordingDevice status`() {
+ val geckoRecordingDevice = createGeckoRecordingDevice(
+ status = GeckoRecordingDevice.Status.RECORDING,
+ )
+ val geckoInactiveDevice = createGeckoRecordingDevice(
+ status = GeckoRecordingDevice.Status.INACTIVE,
+ )
+
+ assertEquals(RecordingDevice.Status.RECORDING, geckoRecordingDevice.toStatus())
+ assertEquals(RecordingDevice.Status.INACTIVE, geckoInactiveDevice.toStatus())
+ }
+
+ @Test
+ fun `GIVEN an invalid GeckoRecordingDevice status WHEN calling toStatus THEN throw an exception`() {
+ val geckoInvalidDevice = createGeckoRecordingDevice(
+ status = 12,
+ )
+ try {
+ geckoInvalidDevice.toStatus()
+ fail()
+ } catch (_: InvalidParameterException) {
+ }
+ }
+
+ @Test
+ fun `GIVEN a GeckoRecordingDevice type WHEN calling toType THEN covert to the RecordingDevice type`() {
+ val geckoCameraDevice = createGeckoRecordingDevice(
+ type = GeckoRecordingDevice.Type.CAMERA,
+ )
+ val geckoMicDevice = createGeckoRecordingDevice(
+ type = GeckoRecordingDevice.Type.MICROPHONE,
+ )
+
+ assertEquals(RecordingDevice.Type.CAMERA, geckoCameraDevice.toType())
+ assertEquals(RecordingDevice.Type.MICROPHONE, geckoMicDevice.toType())
+ }
+
+ @Test
+ fun `GIVEN an invalid GeckoRecordingDevice type WHEN calling toType THEN throw an exception`() {
+ val geckoInvalidDevice = createGeckoRecordingDevice(
+ type = 12,
+ )
+ try {
+ geckoInvalidDevice.toType()
+ fail()
+ } catch (_: InvalidParameterException) {
+ }
+ }
+
+ private fun createGeckoRecordingDevice(
+ status: Long = GeckoRecordingDevice.Status.RECORDING,
+ type: Long = GeckoRecordingDevice.Type.CAMERA,
+ ): GeckoRecordingDevice {
+ val device: GeckoRecordingDevice = mock()
+ ReflectionUtils.setField(device, "status", status)
+ ReflectionUtils.setField(device, "type", type)
+ return device
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionControllerTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionControllerTest.kt
new file mode 100644
index 0000000000..433e3e3af7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionControllerTest.kt
@@ -0,0 +1,49 @@
+/* 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.browser.engine.gecko.mediasession
+
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.MediaSession as GeckoViewMediaSession
+
+class GeckoMediaSessionControllerTest {
+ @Test
+ fun `GeckoMediaSessionController works correctly with GeckoView MediaSession`() {
+ val geckoViewMediaSession: GeckoViewMediaSession = mock()
+ val controller = GeckoMediaSessionController(geckoViewMediaSession)
+
+ controller.pause()
+ verify(geckoViewMediaSession, times(1)).pause()
+
+ controller.stop()
+ verify(geckoViewMediaSession, times(1)).stop()
+
+ controller.play()
+ verify(geckoViewMediaSession, times(1)).play()
+
+ controller.seekTo(123.0, true)
+ verify(geckoViewMediaSession, times(1)).seekTo(123.0, true)
+
+ controller.seekForward()
+ verify(geckoViewMediaSession, times(1)).seekForward()
+
+ controller.seekBackward()
+ verify(geckoViewMediaSession, times(1)).seekBackward()
+
+ controller.nextTrack()
+ verify(geckoViewMediaSession, times(1)).nextTrack()
+
+ controller.previousTrack()
+ verify(geckoViewMediaSession, times(1)).previousTrack()
+
+ controller.skipAd()
+ verify(geckoViewMediaSession, times(1)).skipAd()
+
+ controller.muteAudio(true)
+ verify(geckoViewMediaSession, times(1)).muteAudio(true)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegateTest.kt
new file mode 100644
index 0000000000..e68bfa9e9f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegateTest.kt
@@ -0,0 +1,219 @@
+/* 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.browser.engine.gecko.mediasession
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.mediasession.MediaSession
+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.assertNotNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.MediaSession as GeckoViewMediaSession
+
+@RunWith(AndroidJUnit4::class)
+class GeckoMediaSessionDelegateTest {
+ private lateinit var runtime: GeckoRuntime
+
+ @Before
+ fun setup() {
+ runtime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ }
+
+ @Test
+ fun `media session activated is forwarded to observer`() {
+ val engineSession = GeckoEngineSession(runtime)
+ val geckoViewMediaSession: GeckoViewMediaSession = mock()
+
+ var observedController: MediaSession.Controller? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onMediaActivated(mediaSessionController: MediaSession.Controller) {
+ observedController = mediaSessionController
+ }
+ },
+ )
+
+ engineSession.geckoSession.mediaSessionDelegate!!.onActivated(mock(), geckoViewMediaSession)
+
+ assertNotNull(observedController)
+ observedController!!.play()
+ verify(geckoViewMediaSession).play()
+ }
+
+ @Test
+ fun `media session deactivated is forwarded to observer`() {
+ val engineSession = GeckoEngineSession(runtime)
+ val geckoViewMediaSession: GeckoViewMediaSession = mock()
+
+ var observedActivated = true
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onMediaDeactivated() {
+ observedActivated = false
+ }
+ },
+ )
+
+ engineSession.geckoSession.mediaSessionDelegate!!.onDeactivated(mock(), geckoViewMediaSession)
+
+ assertFalse(observedActivated)
+ }
+
+ @Test
+ fun `media session metadata is forwarded to observer`() {
+ val engineSession = GeckoEngineSession(runtime)
+ val geckoViewMediaSession: GeckoViewMediaSession = mock()
+
+ var observedMetadata: MediaSession.Metadata? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onMediaMetadataChanged(
+ metadata: MediaSession.Metadata,
+ ) {
+ observedMetadata = metadata
+ }
+ },
+ )
+
+ val metadata: GeckoViewMediaSession.Metadata = mock()
+ engineSession.geckoSession.mediaSessionDelegate!!.onMetadata(mock(), geckoViewMediaSession, metadata)
+
+ assertNotNull(observedMetadata)
+ assertEquals(observedMetadata?.title, metadata.title)
+ assertEquals(observedMetadata?.artist, metadata.artist)
+ assertEquals(observedMetadata?.album, metadata.album)
+ assertEquals(observedMetadata?.getArtwork, metadata.artwork)
+ }
+
+ @Test
+ fun `media session feature is forwarded to observer`() {
+ val engineSession = GeckoEngineSession(runtime)
+ val geckoViewMediaSession: GeckoViewMediaSession = mock()
+
+ var observedFeature: MediaSession.Feature? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onMediaFeatureChanged(
+ features: MediaSession.Feature,
+ ) {
+ observedFeature = features
+ }
+ },
+ )
+
+ engineSession.geckoSession.mediaSessionDelegate!!.onFeatures(mock(), geckoViewMediaSession, 123)
+
+ assertNotNull(observedFeature)
+ assertEquals(observedFeature, MediaSession.Feature(123))
+ }
+
+ @Test
+ fun `media session play state is forwarded to observer`() {
+ val engineSession = GeckoEngineSession(runtime)
+ val geckoViewMediaSession: GeckoViewMediaSession = mock()
+
+ var observedPlaybackState: MediaSession.PlaybackState? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onMediaPlaybackStateChanged(
+ playbackState: MediaSession.PlaybackState,
+ ) {
+ observedPlaybackState = playbackState
+ }
+ },
+ )
+
+ engineSession.geckoSession.mediaSessionDelegate!!.onPlay(mock(), geckoViewMediaSession)
+
+ assertNotNull(observedPlaybackState)
+ assertEquals(observedPlaybackState, MediaSession.PlaybackState.PLAYING)
+
+ observedPlaybackState = null
+ engineSession.geckoSession.mediaSessionDelegate!!.onPause(mock(), geckoViewMediaSession)
+
+ assertNotNull(observedPlaybackState)
+ assertEquals(observedPlaybackState, MediaSession.PlaybackState.PAUSED)
+
+ observedPlaybackState = null
+ engineSession.geckoSession.mediaSessionDelegate!!.onStop(mock(), geckoViewMediaSession)
+
+ assertNotNull(observedPlaybackState)
+ assertEquals(observedPlaybackState, MediaSession.PlaybackState.STOPPED)
+ }
+
+ @Test
+ fun `media session position state is forwarded to observer`() {
+ val engineSession = GeckoEngineSession(runtime)
+ val geckoViewMediaSession: GeckoViewMediaSession = mock()
+
+ var observedPositionState: MediaSession.PositionState? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onMediaPositionStateChanged(
+ positionState: MediaSession.PositionState,
+ ) {
+ observedPositionState = positionState
+ }
+ },
+ )
+
+ val positionState: GeckoViewMediaSession.PositionState = mock()
+ engineSession.geckoSession.mediaSessionDelegate!!.onPositionState(mock(), geckoViewMediaSession, positionState)
+
+ assertNotNull(observedPositionState)
+ assertEquals(observedPositionState?.duration, positionState.duration)
+ assertEquals(observedPositionState?.position, positionState.position)
+ assertEquals(observedPositionState?.playbackRate, positionState.playbackRate)
+ }
+
+ @Test
+ fun `media session fullscreen state is forwarded to observer`() {
+ val engineSession = GeckoEngineSession(runtime)
+ val geckoViewMediaSession: GeckoViewMediaSession = mock()
+
+ var observedFullscreen: Boolean? = null
+ var observedElementMetadata: MediaSession.ElementMetadata? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onMediaFullscreenChanged(
+ fullscreen: Boolean,
+ elementMetadata: MediaSession.ElementMetadata?,
+ ) {
+ observedFullscreen = fullscreen
+ observedElementMetadata = elementMetadata
+ }
+ },
+ )
+
+ val elementMetadata: GeckoViewMediaSession.ElementMetadata = mock()
+ engineSession.geckoSession.mediaSessionDelegate!!.onFullscreen(mock(), geckoViewMediaSession, true, elementMetadata)
+
+ assertNotNull(observedFullscreen)
+ assertNotNull(observedElementMetadata)
+ assertEquals(observedFullscreen, true)
+ assertEquals(observedElementMetadata?.source, elementMetadata.source)
+ assertEquals(observedElementMetadata?.duration, elementMetadata.duration)
+ assertEquals(observedElementMetadata?.width, elementMetadata.width)
+ assertEquals(observedElementMetadata?.height, elementMetadata.height)
+ assertEquals(observedElementMetadata?.audioTrackCount, elementMetadata.audioTrackCount)
+ assertEquals(observedElementMetadata?.videoTrackCount, elementMetadata.videoTrackCount)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequestTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequestTest.kt
new file mode 100644
index 0000000000..2a458c9042
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequestTest.kt
@@ -0,0 +1,242 @@
+/* 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.browser.engine.gecko.permission
+
+import android.Manifest
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.support.test.mock
+import mozilla.components.test.ReflectionUtils
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
+
+class GeckoPermissionRequestTest {
+
+ @Test
+ fun `create content permission request`() {
+ val uri = "https://mozilla.org"
+
+ var request = GeckoPermissionRequest.Content(uri, PERMISSION_DESKTOP_NOTIFICATION, mock(), mock())
+ assertEquals(uri, request.uri)
+ assertEquals(listOf(Permission.ContentNotification()), request.permissions)
+
+ request = GeckoPermissionRequest.Content(uri, PERMISSION_GEOLOCATION, mock(), mock())
+ assertEquals(uri, request.uri)
+ assertEquals(listOf(Permission.ContentGeoLocation()), request.permissions)
+
+ request = GeckoPermissionRequest.Content(uri, GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE, mock(), mock())
+ assertEquals(uri, request.uri)
+ assertEquals(listOf(Permission.ContentAutoPlayAudible()), request.permissions)
+
+ request = GeckoPermissionRequest.Content(uri, GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE, mock(), mock())
+ assertEquals(uri, request.uri)
+ assertEquals(listOf(Permission.ContentAutoPlayInaudible()), request.permissions)
+
+ request = GeckoPermissionRequest.Content(uri, 1234, mock(), mock())
+ assertEquals(uri, request.uri)
+ assertEquals(listOf(Permission.Generic("1234", "Gecko permission type = 1234")), request.permissions)
+ }
+
+ @Test
+ fun `grant content permission request`() {
+ val uri = "https://mozilla.org"
+ val geckoResult = mock<GeckoResult<Int>>()
+
+ val request = GeckoPermissionRequest.Content(uri, PERMISSION_GEOLOCATION, mock(), geckoResult)
+
+ assertFalse(request.isCompleted)
+
+ request.grant()
+
+ verify(geckoResult).complete(VALUE_ALLOW)
+ assertTrue(request.isCompleted)
+ }
+
+ @Test
+ fun `reject content permission request`() {
+ val uri = "https://mozilla.org"
+ val geckoResult = mock<GeckoResult<Int>>()
+
+ val request = GeckoPermissionRequest.Content(uri, PERMISSION_GEOLOCATION, mock(), geckoResult)
+
+ assertFalse(request.isCompleted)
+
+ request.reject()
+ verify(geckoResult).complete(VALUE_DENY)
+ assertTrue(request.isCompleted)
+ }
+
+ @Test
+ fun `create app permission request`() {
+ val callback: GeckoSession.PermissionDelegate.Callback = mock()
+ val permissions = listOf(
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.CAMERA,
+ Manifest.permission.RECORD_AUDIO,
+ "unknown app permission",
+ )
+
+ val mappedPermissions = listOf(
+ Permission.AppLocationCoarse(Manifest.permission.ACCESS_COARSE_LOCATION),
+ Permission.AppLocationFine(Manifest.permission.ACCESS_FINE_LOCATION),
+ Permission.AppCamera(Manifest.permission.CAMERA),
+ Permission.AppAudio(Manifest.permission.RECORD_AUDIO),
+ Permission.Generic("unknown app permission"),
+ )
+
+ val request = GeckoPermissionRequest.App(permissions, callback)
+ assertEquals(mappedPermissions, request.permissions)
+ }
+
+ @Test
+ fun `grant app permission request`() {
+ val callback: GeckoSession.PermissionDelegate.Callback = mock()
+
+ val request = GeckoPermissionRequest.App(listOf(Manifest.permission.CAMERA), callback)
+ request.grant()
+ verify(callback).grant()
+ }
+
+ @Test
+ fun `reject app permission request`() {
+ val callback: GeckoSession.PermissionDelegate.Callback = mock()
+
+ val request = GeckoPermissionRequest.App(listOf(Manifest.permission.CAMERA), callback)
+ request.reject()
+ verify(callback).reject()
+ }
+
+ @Test
+ fun `create media permission request`() {
+ val callback: GeckoSession.PermissionDelegate.MediaCallback = mock()
+ val uri = "https://mozilla.org"
+
+ val audioMicrophone = MockMediaSource(
+ "audioMicrophone",
+ "audioMicrophone",
+ MediaSource.SOURCE_MICROPHONE,
+ MediaSource.TYPE_AUDIO,
+ )
+ val audioCapture = MockMediaSource(
+ "audioCapture",
+ "audioCapture",
+ MediaSource.SOURCE_AUDIOCAPTURE,
+ MediaSource.TYPE_AUDIO,
+ )
+ val audioOther = MockMediaSource(
+ "audioOther",
+ "audioOther",
+ MediaSource.SOURCE_OTHER,
+ MediaSource.TYPE_AUDIO,
+ )
+
+ val videoCamera = MockMediaSource(
+ "videoCamera",
+ "videoCamera",
+ MediaSource.SOURCE_CAMERA,
+ MediaSource.TYPE_VIDEO,
+ )
+ val videoScreen = MockMediaSource(
+ "videoScreen",
+ "videoScreen",
+ MediaSource.SOURCE_SCREEN,
+ MediaSource.TYPE_VIDEO,
+ )
+ val videoOther = MockMediaSource(
+ "videoOther",
+ "videoOther",
+ MediaSource.SOURCE_OTHER,
+ MediaSource.TYPE_VIDEO,
+ )
+
+ val audioSources = listOf(audioCapture, audioMicrophone, audioOther)
+ val videoSources = listOf(videoCamera, videoOther, videoScreen)
+
+ val mappedPermissions = listOf(
+ Permission.ContentVideoCamera("videoCamera", "videoCamera"),
+ Permission.ContentVideoScreen("videoScreen", "videoScreen"),
+ Permission.ContentVideoOther("videoOther", "videoOther"),
+ Permission.ContentAudioMicrophone("audioMicrophone", "audioMicrophone"),
+ Permission.ContentAudioCapture("audioCapture", "audioCapture"),
+ Permission.ContentAudioOther("audioOther", "audioOther"),
+ )
+
+ val request = GeckoPermissionRequest.Media(uri, videoSources, audioSources, callback)
+ assertEquals(uri, request.uri)
+ assertEquals(mappedPermissions.size, request.permissions.size)
+ assertTrue(request.permissions.containsAll(mappedPermissions))
+ }
+
+ @Test
+ fun `grant media permission request`() {
+ val callback: GeckoSession.PermissionDelegate.MediaCallback = mock()
+ val uri = "https://mozilla.org"
+
+ val audioMicrophone = MockMediaSource(
+ "audioMicrophone",
+ "audioMicrophone",
+ MediaSource.SOURCE_MICROPHONE,
+ MediaSource.TYPE_AUDIO,
+ )
+ val videoCamera = MockMediaSource(
+ "videoCamera",
+ "videoCamera",
+ MediaSource.SOURCE_CAMERA,
+ MediaSource.TYPE_VIDEO,
+ )
+
+ val audioSources = listOf(audioMicrophone)
+ val videoSources = listOf(videoCamera)
+
+ val request = GeckoPermissionRequest.Media(uri, videoSources, audioSources, callback)
+ request.grant(request.permissions)
+ verify(callback).grant(videoCamera, audioMicrophone)
+ }
+
+ @Test
+ fun `reject media permission request`() {
+ val callback: GeckoSession.PermissionDelegate.MediaCallback = mock()
+ val uri = "https://mozilla.org"
+
+ val audioMicrophone = MockMediaSource(
+ "audioMicrophone",
+ "audioMicrophone",
+ MediaSource.SOURCE_MICROPHONE,
+ MediaSource.TYPE_AUDIO,
+ )
+ val videoCamera = MockMediaSource(
+ "videoCamera",
+ "videoCamera",
+ MediaSource.SOURCE_CAMERA,
+ MediaSource.TYPE_VIDEO,
+ )
+
+ val audioSources = listOf(audioMicrophone)
+ val videoSources = listOf(videoCamera)
+
+ val request = GeckoPermissionRequest.Media(uri, videoSources, audioSources, callback)
+ request.reject()
+ verify(callback).reject()
+ }
+
+ class MockMediaSource(id: String, name: String, source: Int, type: Int) : MediaSource() {
+ init {
+ ReflectionUtils.setField(this, "id", id)
+ ReflectionUtils.setField(this, "name", name)
+ ReflectionUtils.setField(this, "source", source)
+ ReflectionUtils.setField(this, "type", type)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt
new file mode 100644
index 0000000000..cd996b96d7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt
@@ -0,0 +1,740 @@
+/* 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.browser.engine.gecko.permission
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import mozilla.components.test.ReflectionUtils
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_PROMPT
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS
+import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
+import org.mozilla.geckoview.StorageController
+import org.mozilla.geckoview.StorageController.ClearFlags
+
+@ExperimentalCoroutinesApi
+class GeckoSitePermissionsStorageTest {
+ private lateinit var runtime: GeckoRuntime
+ private lateinit var geckoStorage: GeckoSitePermissionsStorage
+ private lateinit var onDiskStorage: SitePermissionsStorage
+ private lateinit var storageController: StorageController
+
+ @Before
+ fun setup() {
+ storageController = mock()
+ runtime = mock()
+ onDiskStorage = mock()
+
+ whenever(runtime.storageController).thenReturn(storageController)
+
+ geckoStorage = spy(GeckoSitePermissionsStorage(runtime, onDiskStorage))
+ }
+
+ @Test
+ fun `GIVEN a location permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(location = ALLOWED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_GEOLOCATION)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_GEOLOCATION, geckoPermissions, mock())
+ val permissionsCaptor = argumentCaptor<SitePermissions>()
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ geckoStorage.save(sitePermissions, geckoRequest, false)
+
+ verify(onDiskStorage).save(permissionsCaptor.capture(), any(), anyBoolean())
+
+ assertEquals(NO_DECISION, permissionsCaptor.value.location)
+ verify(storageController).setPermission(geckoPermissions, VALUE_ALLOW)
+ }
+
+ @Test
+ fun `GIVEN a notification permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(notification = BLOCKED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_DESKTOP_NOTIFICATION)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_DESKTOP_NOTIFICATION, geckoPermissions, mock())
+ val permissionsCaptor = argumentCaptor<SitePermissions>()
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ geckoStorage.save(sitePermissions, geckoRequest, false)
+
+ verify(onDiskStorage).save(permissionsCaptor.capture(), any(), anyBoolean())
+
+ assertEquals(NO_DECISION, permissionsCaptor.value.notification)
+ verify(storageController).setPermission(geckoPermissions, VALUE_DENY)
+ }
+
+ @Test
+ fun `GIVEN a localStorage permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(localStorage = BLOCKED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_PERSISTENT_STORAGE)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_PERSISTENT_STORAGE, geckoPermissions, mock())
+ val permissionsCaptor = argumentCaptor<SitePermissions>()
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ geckoStorage.save(sitePermissions, geckoRequest, false)
+
+ verify(onDiskStorage).save(permissionsCaptor.capture(), any(), anyBoolean())
+
+ assertEquals(NO_DECISION, permissionsCaptor.value.localStorage)
+ verify(storageController).setPermission(geckoPermissions, VALUE_DENY)
+ }
+
+ @Test
+ fun `GIVEN a crossOriginStorageAccess permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(crossOriginStorageAccess = BLOCKED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_STORAGE_ACCESS)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_STORAGE_ACCESS, geckoPermissions, mock())
+ val permissionsCaptor = argumentCaptor<SitePermissions>()
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ geckoStorage.save(sitePermissions, geckoRequest, false)
+
+ verify(onDiskStorage).save(permissionsCaptor.capture(), any(), anyBoolean())
+
+ assertEquals(NO_DECISION, permissionsCaptor.value.crossOriginStorageAccess)
+ verify(storageController).setPermission(geckoPermissions, VALUE_DENY)
+ }
+
+ @Test
+ fun `GIVEN a mediaKeySystemAccess permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(mediaKeySystemAccess = ALLOWED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_MEDIA_KEY_SYSTEM_ACCESS)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, geckoPermissions, mock())
+ val permissionsCaptor = argumentCaptor<SitePermissions>()
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ geckoStorage.save(sitePermissions, geckoRequest, false)
+
+ verify(onDiskStorage).save(permissionsCaptor.capture(), any(), anyBoolean())
+
+ assertEquals(NO_DECISION, permissionsCaptor.value.mediaKeySystemAccess)
+ verify(storageController).setPermission(geckoPermissions, VALUE_ALLOW)
+ }
+
+ @Test
+ fun `GIVEN a autoplayInaudible permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(autoplayInaudible = AutoplayStatus.ALLOWED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_INAUDIBLE)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_INAUDIBLE, geckoPermissions, mock())
+ val permissionsCaptor = argumentCaptor<SitePermissions>()
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ geckoStorage.save(sitePermissions, geckoRequest, false)
+
+ verify(onDiskStorage).save(permissionsCaptor.capture(), any(), anyBoolean())
+
+ assertEquals(AutoplayStatus.BLOCKED, permissionsCaptor.value.autoplayInaudible)
+ verify(storageController).setPermission(geckoPermissions, VALUE_ALLOW)
+ }
+
+ @Test
+ fun `GIVEN a autoplayAudible permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(autoplayAudible = AutoplayStatus.ALLOWED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoPermissions, mock())
+ val permissionsCaptor = argumentCaptor<SitePermissions>()
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ geckoStorage.save(sitePermissions, geckoRequest, false)
+
+ verify(onDiskStorage).save(permissionsCaptor.capture(), any(), anyBoolean())
+
+ assertEquals(AutoplayStatus.BLOCKED, permissionsCaptor.value.autoplayAudible)
+ verify(storageController).setPermission(geckoPermissions, VALUE_ALLOW)
+ }
+
+ @Test
+ fun `WHEN saving a site permission THEN the permission is saved in the gecko storage and in disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(autoplayAudible = AutoplayStatus.ALLOWED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoPermissions, mock())
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ geckoStorage.save(sitePermissions, geckoRequest, false)
+
+ verify(onDiskStorage).save(
+ sitePermissions.copy(autoplayAudible = AutoplayStatus.BLOCKED),
+ geckoRequest,
+ false,
+ )
+ verify(storageController).setPermission(geckoPermissions, VALUE_ALLOW)
+ }
+
+ @Test
+ fun `GIVEN a temporary permission WHEN saving THEN the permission is saved in memory`() = runTest {
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoPermissions, mock())
+
+ geckoStorage.saveTemporary(geckoRequest)
+
+ assertTrue(geckoStorage.geckoTemporaryPermissions.contains(geckoPermissions))
+ }
+
+ @Test
+ fun `GIVEN media type temporary permission WHEN saving THEN the permission is NOT saved in memory`() = runTest {
+ val geckoRequest = GeckoPermissionRequest.Media("mozilla.org", emptyList(), emptyList(), mock())
+
+ assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty())
+
+ geckoStorage.saveTemporary(geckoRequest)
+
+ assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN multiple saved temporary permissions WHEN clearing all temporary permission THEN all permissions are cleared`() = runTest {
+ val geckoAutoPlayPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE)
+ val geckoPersistentStoragePermissions = geckoContentPermission("mozilla.org", PERMISSION_PERSISTENT_STORAGE)
+ val geckoStorageAccessPermissions = geckoContentPermission("mozilla.org", PERMISSION_STORAGE_ACCESS)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoAutoPlayPermissions, mock())
+
+ assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty())
+
+ geckoStorage.saveTemporary(geckoRequest)
+
+ assertEquals(1, geckoStorage.geckoTemporaryPermissions.size)
+
+ geckoStorage.saveTemporary(geckoRequest.copy(geckoPermission = geckoPersistentStoragePermissions))
+
+ assertEquals(2, geckoStorage.geckoTemporaryPermissions.size)
+
+ geckoStorage.saveTemporary(geckoRequest.copy(geckoPermission = geckoStorageAccessPermissions))
+
+ assertEquals(3, geckoStorage.geckoTemporaryPermissions.size)
+
+ geckoStorage.clearTemporaryPermissions()
+
+ verify(storageController).setPermission(geckoAutoPlayPermissions, VALUE_PROMPT)
+ verify(storageController).setPermission(geckoPersistentStoragePermissions, VALUE_PROMPT)
+ verify(storageController).setPermission(geckoStorageAccessPermissions, VALUE_PROMPT)
+
+ assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN a localStorage permission WHEN updating THEN the permission is updated in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(location = ALLOWED)
+ val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_GEOLOCATION)
+ val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoPermissions, mock())
+
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ val permission = geckoStorage.updateGeckoPermissionIfNeeded(
+ sitePermissions,
+ geckoRequest,
+ private = false,
+ )
+
+ assertEquals(NO_DECISION, permission.location)
+ verify(storageController).setPermission(geckoPermissions, VALUE_ALLOW)
+ }
+
+ @Test
+ fun `WHEN updating a permission THEN the permission is updated in the gecko storage and on the disk storage`() = runTest {
+ val sitePermissions = createNewSitePermission().copy(location = ALLOWED)
+
+ doReturn(sitePermissions).`when`(geckoStorage)
+ .updateGeckoPermissionIfNeeded(sitePermissions, private = true)
+
+ geckoStorage.update(sitePermissions, true)
+
+ verify(geckoStorage).updateGeckoPermissionIfNeeded(sitePermissions, private = true)
+ verify(onDiskStorage).update(sitePermissions, private = true)
+ }
+
+ @Test
+ fun `WHEN updating THEN the permission is updated in the gecko storage and set to the default value on the disk storage`() = runTest {
+ val sitePermissions = SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = ALLOWED,
+ location = ALLOWED,
+ notification = ALLOWED,
+ microphone = ALLOWED,
+ camera = ALLOWED,
+ bluetooth = ALLOWED,
+ mediaKeySystemAccess = ALLOWED,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ savedAt = 0,
+ )
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION),
+ geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS),
+ geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE),
+ geckoContentPermission(type = PERMISSION_STORAGE_ACCESS),
+ )
+
+ doReturn(geckoPermissions).`when`(geckoStorage)
+ .findGeckoContentPermissionBy(anyString(), anyBoolean(), anyBoolean())
+ doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin)
+
+ val permission = geckoStorage.updateGeckoPermissionIfNeeded(sitePermissions, null, false)
+
+ geckoPermissions.forEach {
+ verify(geckoStorage).removeTemporaryPermissionIfAny(it)
+ verify(storageController).setPermission(it, VALUE_ALLOW)
+ }
+
+ assertEquals(NO_DECISION, permission.location)
+ assertEquals(NO_DECISION, permission.notification)
+ assertEquals(NO_DECISION, permission.localStorage)
+ assertEquals(NO_DECISION, permission.crossOriginStorageAccess)
+ assertEquals(NO_DECISION, permission.mediaKeySystemAccess)
+ assertEquals(ALLOWED, permission.camera)
+ assertEquals(ALLOWED, permission.microphone)
+ assertEquals(AutoplayStatus.BLOCKED, permission.autoplayAudible)
+ assertEquals(AutoplayStatus.BLOCKED, permission.autoplayInaudible)
+ }
+
+ @Test
+ fun `WHEN querying the store by origin THEN the gecko and the on disk storage are queried and results are combined`() = runTest {
+ val sitePermissions = SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = ALLOWED,
+ location = ALLOWED,
+ notification = ALLOWED,
+ microphone = ALLOWED,
+ camera = ALLOWED,
+ bluetooth = ALLOWED,
+ mediaKeySystemAccess = ALLOWED,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ savedAt = 0,
+ )
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION, value = VALUE_ALLOW),
+ geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION, value = VALUE_ALLOW),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, value = VALUE_ALLOW),
+ geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE, value = VALUE_ALLOW),
+ geckoContentPermission(type = PERMISSION_STORAGE_ACCESS, value = VALUE_ALLOW),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE, value = VALUE_ALLOW),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE, value = VALUE_ALLOW),
+ )
+
+ doReturn(sitePermissions).`when`(onDiskStorage)
+ .findSitePermissionsBy(
+ origin = "mozilla.dev",
+ includeTemporary = false,
+ private = false,
+ )
+ doReturn(geckoPermissions).`when`(geckoStorage)
+ .findGeckoContentPermissionBy(
+ origin = "mozilla.dev",
+ includeTemporary = false,
+ private = false,
+ )
+
+ val foundPermissions = geckoStorage.findSitePermissionsBy(
+ origin = "mozilla.dev",
+ includeTemporary = false,
+ private = false,
+ )!!
+
+ assertEquals(ALLOWED, foundPermissions.location)
+ assertEquals(ALLOWED, foundPermissions.notification)
+ assertEquals(ALLOWED, foundPermissions.localStorage)
+ assertEquals(ALLOWED, foundPermissions.crossOriginStorageAccess)
+ assertEquals(ALLOWED, foundPermissions.mediaKeySystemAccess)
+ assertEquals(ALLOWED, foundPermissions.camera)
+ assertEquals(ALLOWED, foundPermissions.microphone)
+ assertEquals(AutoplayStatus.ALLOWED, foundPermissions.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, foundPermissions.autoplayInaudible)
+ }
+
+ @Test
+ fun `GIVEN a gecko and on disk permissions WHEN merging values THEN both should be combined into one`() = runTest {
+ val onDiskPermissions = SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = ALLOWED,
+ location = ALLOWED,
+ notification = ALLOWED,
+ microphone = ALLOWED,
+ camera = ALLOWED,
+ bluetooth = ALLOWED,
+ mediaKeySystemAccess = ALLOWED,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ savedAt = 0,
+ )
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_STORAGE_ACCESS, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE, value = VALUE_DENY),
+ ).groupByType()
+
+ val mergedPermissions = geckoStorage.mergePermissions(onDiskPermissions, geckoPermissions)!!
+
+ assertEquals(BLOCKED, mergedPermissions.location)
+ assertEquals(BLOCKED, mergedPermissions.notification)
+ assertEquals(BLOCKED, mergedPermissions.localStorage)
+ assertEquals(BLOCKED, mergedPermissions.crossOriginStorageAccess)
+ assertEquals(BLOCKED, mergedPermissions.mediaKeySystemAccess)
+ assertEquals(ALLOWED, mergedPermissions.camera)
+ assertEquals(ALLOWED, mergedPermissions.microphone)
+ assertEquals(AutoplayStatus.BLOCKED, mergedPermissions.autoplayAudible)
+ assertEquals(AutoplayStatus.BLOCKED, mergedPermissions.autoplayInaudible)
+ }
+
+ @Test
+ fun `GIVEN permissions that are not present on the gecko storage WHEN merging THEN favor the values on disk permissions`() = runTest {
+ val onDiskPermissions = SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = ALLOWED,
+ location = ALLOWED,
+ notification = ALLOWED,
+ microphone = ALLOWED,
+ camera = ALLOWED,
+ bluetooth = ALLOWED,
+ mediaKeySystemAccess = ALLOWED,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ savedAt = 0,
+ )
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION, value = VALUE_DENY),
+ ).groupByType()
+
+ val mergedPermissions = geckoStorage.mergePermissions(onDiskPermissions, geckoPermissions)!!
+
+ assertEquals(BLOCKED, mergedPermissions.location)
+ assertEquals(ALLOWED, mergedPermissions.notification)
+ assertEquals(ALLOWED, mergedPermissions.localStorage)
+ assertEquals(ALLOWED, mergedPermissions.crossOriginStorageAccess)
+ assertEquals(ALLOWED, mergedPermissions.mediaKeySystemAccess)
+ assertEquals(ALLOWED, mergedPermissions.camera)
+ assertEquals(ALLOWED, mergedPermissions.microphone)
+ assertEquals(AutoplayStatus.ALLOWED, mergedPermissions.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, mergedPermissions.autoplayInaudible)
+ }
+
+ @Test
+ fun `GIVEN different cross_origin_storage_access permissions WHEN mergePermissions is called THEN they are filtered by origin url`() {
+ val onDiskPermissions = SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = NO_DECISION,
+ location = ALLOWED,
+ notification = ALLOWED,
+ microphone = ALLOWED,
+ camera = ALLOWED,
+ bluetooth = ALLOWED,
+ mediaKeySystemAccess = ALLOWED,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ savedAt = 0,
+ )
+ val geckoPermission1 = geckoContentPermission(
+ type = PERMISSION_STORAGE_ACCESS,
+ value = VALUE_DENY,
+ thirdPartyOrigin = "mozilla.com",
+ )
+ val geckoPermission2 = geckoContentPermission(
+ type = PERMISSION_STORAGE_ACCESS,
+ value = VALUE_ALLOW,
+ thirdPartyOrigin = "mozilla.dev",
+ )
+ val geckoPermission3 = geckoContentPermission(
+ type = PERMISSION_STORAGE_ACCESS,
+ value = VALUE_PROMPT,
+ thirdPartyOrigin = "mozilla.org",
+ )
+
+ val mergedPermissions = geckoStorage.mergePermissions(
+ onDiskPermissions,
+ mapOf(PERMISSION_STORAGE_ACCESS to listOf(geckoPermission1, geckoPermission2, geckoPermission3)),
+ )
+
+ assertEquals(onDiskPermissions.copy(crossOriginStorageAccess = ALLOWED), mergedPermissions!!)
+ }
+
+ @Test
+ fun `WHEN removing a site permissions THEN permissions should be removed from the on disk and gecko storage`() = runTest {
+ val onDiskPermissions = createNewSitePermission()
+
+ doReturn(Unit).`when`(geckoStorage).removeGeckoContentPermissionBy(
+ origin = onDiskPermissions.origin,
+ private = false,
+ )
+
+ geckoStorage.remove(sitePermissions = onDiskPermissions, private = false)
+
+ verify(onDiskStorage).remove(sitePermissions = onDiskPermissions, private = false)
+ verify(geckoStorage).removeGeckoContentPermissionBy(
+ origin = onDiskPermissions.origin,
+ private = false,
+ )
+ }
+
+ @Test
+ fun `WHEN removing gecko permissions THEN permissions should be set to the default values in the gecko storage`() = runTest {
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION),
+ geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS),
+ geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE),
+ geckoContentPermission(type = PERMISSION_STORAGE_ACCESS),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE),
+ geckoContentPermission(type = PERMISSION_TRACKING),
+ )
+
+ doReturn(geckoPermissions).`when`(geckoStorage)
+ .findGeckoContentPermissionBy(anyString(), anyBoolean(), anyBoolean())
+
+ geckoStorage.removeGeckoContentPermissionBy(origin = "mozilla.dev", private = false)
+
+ geckoPermissions.forEach {
+ val value = if (it.permission != PERMISSION_TRACKING) {
+ VALUE_PROMPT
+ } else {
+ VALUE_DENY
+ }
+ verify(geckoStorage).removeTemporaryPermissionIfAny(it)
+ verify(storageController).setPermission(it, value)
+ }
+ }
+
+ @Test
+ fun `WHEN removing a temporary permissions THEN the permissions should be remove from memory`() = runTest {
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION),
+ geckoContentPermission(type = PERMISSION_GEOLOCATION),
+ geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS),
+ geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE),
+ geckoContentPermission(type = PERMISSION_STORAGE_ACCESS),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE),
+ geckoContentPermission(type = PERMISSION_TRACKING),
+ )
+
+ assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty())
+
+ geckoStorage.geckoTemporaryPermissions.addAll(geckoPermissions)
+
+ assertEquals(10, geckoStorage.geckoTemporaryPermissions.size)
+
+ geckoPermissions.forEach {
+ geckoStorage.removeTemporaryPermissionIfAny(it)
+ }
+
+ assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty())
+ }
+
+ @Test
+ fun `WHEN removing all THEN all permissions should be removed from the on disk and gecko storage`() = runTest {
+ doReturn(Unit).`when`(geckoStorage).removeGeckoAllContentPermissions()
+
+ geckoStorage.removeAll()
+
+ verify(onDiskStorage).removeAll()
+ verify(geckoStorage).removeGeckoAllContentPermissions()
+ }
+
+ @Test
+ fun `WHEN removing all gecko permissions THEN remove all permissions on gecko and clear the site permissions info`() = runTest {
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION),
+ geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS),
+ geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE),
+ geckoContentPermission(type = PERMISSION_STORAGE_ACCESS),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE),
+ geckoContentPermission(type = PERMISSION_TRACKING),
+ )
+
+ doReturn(geckoPermissions).`when`(geckoStorage).findAllGeckoContentPermissions()
+ doNothing().`when`(geckoStorage).removeGeckoContentPermission(any())
+
+ geckoStorage.removeGeckoAllContentPermissions()
+
+ geckoPermissions.forEach {
+ verify(geckoStorage).removeGeckoContentPermission(it)
+ }
+ verify(storageController).clearData(ClearFlags.PERMISSIONS)
+ }
+
+ @Test
+ fun `WHEN querying all permission THEN the gecko and the on disk storage are queried and results are combined`() = runTest {
+ val onDiskPermissions = SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = ALLOWED,
+ location = ALLOWED,
+ notification = ALLOWED,
+ microphone = ALLOWED,
+ camera = ALLOWED,
+ bluetooth = ALLOWED,
+ mediaKeySystemAccess = ALLOWED,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ savedAt = 0,
+ )
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_STORAGE_ACCESS, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE, value = VALUE_DENY),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE, value = VALUE_DENY),
+ )
+
+ doReturn(listOf(onDiskPermissions)).`when`(onDiskStorage).all()
+ doReturn(geckoPermissions).`when`(geckoStorage).findAllGeckoContentPermissions()
+
+ val foundPermissions = geckoStorage.all().first()
+
+ assertEquals(BLOCKED, foundPermissions.location)
+ assertEquals(BLOCKED, foundPermissions.notification)
+ assertEquals(BLOCKED, foundPermissions.localStorage)
+ assertEquals(BLOCKED, foundPermissions.crossOriginStorageAccess)
+ assertEquals(BLOCKED, foundPermissions.mediaKeySystemAccess)
+ assertEquals(ALLOWED, foundPermissions.camera)
+ assertEquals(ALLOWED, foundPermissions.microphone)
+ assertEquals(AutoplayStatus.BLOCKED, foundPermissions.autoplayAudible)
+ assertEquals(AutoplayStatus.BLOCKED, foundPermissions.autoplayInaudible)
+ }
+
+ @Test
+ fun `WHEN filtering temporary permissions THEN all temporary permissions should be removed`() = runTest {
+ val temporary = listOf(geckoContentPermission(type = PERMISSION_GEOLOCATION))
+
+ val geckoPermissions = listOf(
+ geckoContentPermission(type = PERMISSION_GEOLOCATION),
+ geckoContentPermission(type = PERMISSION_GEOLOCATION),
+ geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION),
+ geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS),
+ geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE),
+ geckoContentPermission(type = PERMISSION_STORAGE_ACCESS),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE),
+ geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE),
+ )
+
+ val filteredPermissions = geckoPermissions.filterNotTemporaryPermissions(temporary)!!
+
+ assertEquals(6, filteredPermissions.size)
+ assertFalse(filteredPermissions.any { it.permission == PERMISSION_GEOLOCATION })
+ }
+
+ @Test
+ fun `WHEN compering two gecko ContentPermissions THEN they are the same when host, mode and permissions are the same`() = runTest {
+ val location1 = geckoContentPermission(uri = "mozilla.dev", type = PERMISSION_GEOLOCATION)
+ val location2 = geckoContentPermission(uri = "mozilla.dev", type = PERMISSION_GEOLOCATION)
+ val notification = geckoContentPermission(uri = "mozilla.dev", type = PERMISSION_DESKTOP_NOTIFICATION)
+ val privateNotification = geckoContentPermission(uri = "mozilla.dev", type = PERMISSION_DESKTOP_NOTIFICATION, privateMode = true)
+
+ assertTrue(location1.areSame(location2))
+ assertFalse(notification.areSame(location1))
+ assertFalse(notification.areSame(privateNotification))
+ }
+
+ @Test
+ fun `WHEN converting from gecko status to sitePermissions status THEN they get converted to the equivalent one`() = runTest {
+ assertEquals(NO_DECISION, VALUE_PROMPT.toStatus())
+ assertEquals(BLOCKED, VALUE_DENY.toStatus())
+ assertEquals(ALLOWED, VALUE_ALLOW.toStatus())
+ }
+
+ @Test
+ fun `WHEN converting from gecko status to autoplay sitePermissions status THEN they get converted to the equivalent one`() = runTest {
+ assertEquals(AutoplayStatus.BLOCKED, VALUE_PROMPT.toAutoPlayStatus())
+ assertEquals(AutoplayStatus.BLOCKED, VALUE_DENY.toAutoPlayStatus())
+ assertEquals(AutoplayStatus.ALLOWED, VALUE_ALLOW.toAutoPlayStatus())
+ }
+
+ @Test
+ fun `WHEN converting a sitePermissions status to gecko status THEN they get converted to the equivalent one`() = runTest {
+ assertEquals(VALUE_PROMPT, NO_DECISION.toGeckoStatus())
+ assertEquals(VALUE_DENY, BLOCKED.toGeckoStatus())
+ assertEquals(VALUE_ALLOW, ALLOWED.toGeckoStatus())
+ }
+
+ @Test
+ fun `WHEN converting from autoplay sitePermissions to gecko status THEN they get converted to the equivalent one`() = runTest {
+ assertEquals(VALUE_DENY, AutoplayStatus.BLOCKED.toGeckoStatus())
+ assertEquals(VALUE_ALLOW, AutoplayStatus.ALLOWED.toGeckoStatus())
+ }
+
+ private fun createNewSitePermission(): SitePermissions {
+ return SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = BLOCKED,
+ location = BLOCKED,
+ notification = NO_DECISION,
+ microphone = NO_DECISION,
+ camera = NO_DECISION,
+ bluetooth = ALLOWED,
+ savedAt = 0,
+ )
+ }
+}
+
+internal fun geckoContentPermission(
+ uri: String = "mozilla.dev",
+ type: Int,
+ value: Int = VALUE_PROMPT,
+ thirdPartyOrigin: String = "mozilla.dev",
+ privateMode: Boolean = false,
+): ContentPermission {
+ val permission: ContentPermission = mock()
+ ReflectionUtils.setField(permission, "uri", uri)
+ ReflectionUtils.setField(permission, "thirdPartyOrigin", thirdPartyOrigin)
+ ReflectionUtils.setField(permission, "permission", type)
+ ReflectionUtils.setField(permission, "value", value)
+ ReflectionUtils.setField(permission, "privateMode", privateMode)
+ return permission
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegateTest.kt
new file mode 100644
index 0000000000..3f00852a1a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegateTest.kt
@@ -0,0 +1,81 @@
+/* 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.browser.engine.gecko.prompt
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.support.test.mock
+import mozilla.components.test.ReflectionUtils
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.GeckoSession
+
+@RunWith(AndroidJUnit4::class)
+class ChoicePromptDelegateTest {
+
+ @Test
+ fun `WHEN onPromptUpdate is called from GeckoView THEN notifyObservers is invoked with onPromptUpdate`() {
+ val mockSession = GeckoEngineSession(mock())
+ var isOnPromptUpdateCalled = false
+ var isOnConfirmCalled = false
+ var isOnDismissCalled = false
+ var observedPrompt: PromptRequest? = null
+ var observedUID: String? = null
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptUpdate(
+ previousPromptRequestUid: String,
+ promptRequest: PromptRequest,
+ ) {
+ observedPrompt = promptRequest
+ observedUID = previousPromptRequestUid
+ isOnPromptUpdateCalled = true
+ }
+ },
+ )
+ val prompt = PromptRequest.SingleChoice(
+ arrayOf(),
+ { isOnConfirmCalled = true },
+ { isOnDismissCalled = true },
+ )
+ val delegate = ChoicePromptDelegate(mockSession, prompt)
+ val updatedPrompt = mock<GeckoSession.PromptDelegate.ChoicePrompt>()
+ ReflectionUtils.setField(updatedPrompt, "choices", arrayOf<GeckoChoice>())
+
+ delegate.onPromptUpdate(updatedPrompt)
+
+ assertTrue(isOnPromptUpdateCalled)
+ assertEquals(prompt.uid, observedUID)
+ // Verify if the onConfirm and onDismiss callbacks were changed
+ (observedPrompt as PromptRequest.SingleChoice).onConfirm(mock())
+ (observedPrompt as PromptRequest.SingleChoice).onDismiss()
+ assertTrue(isOnDismissCalled)
+ assertTrue(isOnConfirmCalled)
+ }
+
+ @Test
+ fun `WHEN onPromptDismiss is called from GeckoView THEN notifyObservers is invoked with onPromptDismissed`() {
+ val mockSession = GeckoEngineSession(mock())
+ var isOnDismissCalled = false
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptDismissed(promptRequest: PromptRequest) {
+ super.onPromptDismissed(promptRequest)
+ isOnDismissCalled = true
+ }
+ },
+ )
+ val basePrompt: GeckoSession.PromptDelegate.ChoicePrompt = mock()
+ val prompt: PromptRequest.SingleChoice = mock()
+ val delegate = ChoicePromptDelegate(mockSession, prompt)
+
+ delegate.onPromptDismiss(basePrompt)
+
+ assertTrue(isOnDismissCalled)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt
new file mode 100644
index 0000000000..8c5c058055
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt
@@ -0,0 +1,2136 @@
+/* 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.browser.engine.gecko.prompt
+
+import android.net.Uri
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
+import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard
+import mozilla.components.browser.engine.gecko.ext.toLoginEntry
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.prompt.Choice
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.prompt.PromptRequest.IdentityCredential
+import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.support.ktx.kotlin.toDate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import mozilla.components.test.ReflectionUtils
+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.Mockito
+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.mozilla.geckoview.AllowOrDeny
+import org.mozilla.geckoview.Autocomplete
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATE
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATETIME_LOCAL
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.MONTH
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.TIME
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEEK
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.ANY
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.NONE
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt.Capture.USER
+import org.robolectric.Shadows.shadowOf
+import java.security.InvalidParameterException
+import java.util.Calendar
+import java.util.Calendar.YEAR
+import java.util.Date
+
+typealias GeckoChoice = GeckoSession.PromptDelegate.ChoicePrompt.Choice
+typealias GECKO_AUTH_LEVEL = GeckoSession.PromptDelegate.AuthPrompt.AuthOptions.Level
+typealias GECKO_PROMPT_CHOICE_TYPE = GeckoSession.PromptDelegate.ChoicePrompt.Type
+typealias GECKO_AUTH_FLAGS = GeckoSession.PromptDelegate.AuthPrompt.AuthOptions.Flags
+typealias GECKO_PROMPT_FILE_TYPE = GeckoSession.PromptDelegate.FilePrompt.Type
+typealias AC_AUTH_METHOD = PromptRequest.Authentication.Method
+typealias AC_AUTH_LEVEL = PromptRequest.Authentication.Level
+
+@RunWith(AndroidJUnit4::class)
+class GeckoPromptDelegateTest {
+
+ private lateinit var runtime: GeckoRuntime
+
+ @Before
+ fun setup() {
+ runtime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ }
+
+ @Test
+ fun `onChoicePrompt called with CHOICE_TYPE_SINGLE must provide a SingleChoice PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var promptRequestSingleChoice: PromptRequest = MultipleChoice(arrayOf(), {}, {})
+ var confirmWasCalled = false
+ val gecko = GeckoPromptDelegate(mockSession)
+ val geckoChoice = object : GeckoChoice() {}
+ val geckoPrompt = geckoChoicePrompt(
+ "title",
+ "message",
+ GECKO_PROMPT_CHOICE_TYPE.SINGLE,
+ arrayOf(geckoChoice),
+ )
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ promptRequestSingleChoice = promptRequest
+ }
+ },
+ )
+
+ val geckoResult = gecko.onChoicePrompt(mock(), geckoPrompt)
+
+ geckoResult!!.accept {
+ confirmWasCalled = true
+ }
+
+ assertTrue(promptRequestSingleChoice is SingleChoice)
+ val request = promptRequestSingleChoice as SingleChoice
+
+ request.onConfirm(request.choices.first())
+ shadowOf(getMainLooper()).idle()
+ assertTrue(confirmWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ confirmWasCalled = false
+ request.onConfirm(request.choices.first())
+ shadowOf(getMainLooper()).idle()
+ assertFalse(confirmWasCalled)
+ }
+
+ @Test
+ fun `onChoicePrompt called with CHOICE_TYPE_MULTIPLE must provide a MultipleChoice PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var promptRequestSingleChoice: PromptRequest = SingleChoice(arrayOf(), {}, {})
+ var confirmWasCalled = false
+ val gecko = GeckoPromptDelegate(mockSession)
+ val mockGeckoChoice = object : GeckoChoice() {}
+ val geckoPrompt = geckoChoicePrompt(
+ "title",
+ "message",
+ GECKO_PROMPT_CHOICE_TYPE.MULTIPLE,
+ arrayOf(mockGeckoChoice),
+ )
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ promptRequestSingleChoice = promptRequest
+ }
+ },
+ )
+
+ val geckoResult = gecko.onChoicePrompt(mock(), geckoPrompt)
+
+ geckoResult!!.accept {
+ confirmWasCalled = true
+ }
+
+ assertTrue(promptRequestSingleChoice is MultipleChoice)
+
+ (promptRequestSingleChoice as MultipleChoice).onConfirm(arrayOf())
+ shadowOf(getMainLooper()).idle()
+ assertTrue(confirmWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ confirmWasCalled = false
+ (promptRequestSingleChoice as MultipleChoice).onConfirm(arrayOf())
+ shadowOf(getMainLooper()).idle()
+ assertFalse(confirmWasCalled)
+ }
+
+ @Test
+ fun `onChoicePrompt called with CHOICE_TYPE_MENU must provide a MenuChoice PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var promptRequestSingleChoice: PromptRequest = PromptRequest.MenuChoice(arrayOf(), {}, {})
+ var confirmWasCalled = false
+ val gecko = GeckoPromptDelegate(mockSession)
+ val geckoChoice = object : GeckoChoice() {}
+ val geckoPrompt = geckoChoicePrompt(
+ "title",
+ "message",
+ GECKO_PROMPT_CHOICE_TYPE.MENU,
+ arrayOf(geckoChoice),
+ )
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ promptRequestSingleChoice = promptRequest
+ }
+ },
+ )
+
+ val geckoResult = gecko.onChoicePrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ confirmWasCalled = true
+ }
+
+ assertTrue(promptRequestSingleChoice is PromptRequest.MenuChoice)
+ val request = promptRequestSingleChoice as PromptRequest.MenuChoice
+
+ request.onConfirm(request.choices.first())
+ shadowOf(getMainLooper()).idle()
+ assertTrue(confirmWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ confirmWasCalled = false
+ request.onConfirm(request.choices.first())
+ shadowOf(getMainLooper()).idle()
+ assertFalse(confirmWasCalled)
+ }
+
+ @Test(expected = InvalidParameterException::class)
+ fun `calling onChoicePrompt with not valid Gecko ChoiceType will throw an exception`() {
+ val promptDelegate = GeckoPromptDelegate(mock())
+ val geckoPrompt = geckoChoicePrompt(
+ "title",
+ "message",
+ -1,
+ arrayOf(),
+ )
+ promptDelegate.onChoicePrompt(mock(), geckoPrompt)
+ }
+
+ @Test
+ fun `onAlertPrompt must provide an alert PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var alertRequest: PromptRequest? = null
+ var dismissWasCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ alertRequest = promptRequest
+ }
+ },
+ )
+
+ val geckoResult = promptDelegate.onAlertPrompt(mock(), geckoAlertPrompt())
+ geckoResult.accept {
+ dismissWasCalled = true
+ }
+ assertTrue(alertRequest is PromptRequest.Alert)
+
+ (alertRequest as PromptRequest.Alert).onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(dismissWasCalled)
+
+ assertEquals((alertRequest as PromptRequest.Alert).title, "title")
+ assertEquals((alertRequest as PromptRequest.Alert).message, "message")
+ }
+
+ @Test
+ fun `toIdsArray must convert an list of choices to array of id strings`() {
+ val choices = arrayOf(Choice(id = "0", label = ""), Choice(id = "1", label = ""))
+ val ids = choices.toIdsArray()
+ ids.forEachIndexed { index, item ->
+ assertEquals("$index", item)
+ }
+ }
+
+ @Test
+ fun `onDateTimePrompt called with DATETIME_TYPE_DATE must provide a date PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var dateRequest: PromptRequest? = null
+ var geckoPrompt = geckoDateTimePrompt("title", DATE, "", "", "")
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ dateRequest = promptRequest
+ }
+ },
+ )
+
+ promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+
+ assertTrue(dateRequest is PromptRequest.TimeSelection)
+ val date = Date()
+ (dateRequest as PromptRequest.TimeSelection).onConfirm(date)
+ verify(geckoPrompt, times(1)).confirm(eq(date.toString("yyyy-MM-dd")))
+ assertEquals((dateRequest as PromptRequest.TimeSelection).title, "title")
+
+ geckoPrompt = geckoDateTimePrompt("title", DATE, "", "", "")
+ promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+
+ (dateRequest as PromptRequest.TimeSelection).onClear()
+ verify(geckoPrompt, times(1)).confirm(eq(""))
+ }
+
+ @Test
+ fun `onDateTimePrompt DATETIME_TYPE_DATE with date parameters must format dates correctly`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var timeSelectionRequest: PromptRequest.TimeSelection? = null
+ val confirmCaptor = argumentCaptor<String>()
+
+ val geckoPrompt =
+ geckoDateTimePrompt(
+ title = "title",
+ type = DATE,
+ defaultValue = "2019-11-29",
+ minValue = "2019-11-28",
+ maxValue = "2019-11-30",
+ )
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ timeSelectionRequest = promptRequest as PromptRequest.TimeSelection
+ }
+ },
+ )
+
+ promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+
+ assertNotNull(timeSelectionRequest)
+ with(timeSelectionRequest!!) {
+ assertEquals(initialDate, "2019-11-29".toDate("yyyy-MM-dd"))
+ assertEquals(minimumDate, "2019-11-28".toDate("yyyy-MM-dd"))
+ assertEquals(maximumDate, "2019-11-30".toDate("yyyy-MM-dd"))
+ }
+ val selectedDate = "2019-11-28".toDate("yyyy-MM-dd")
+ (timeSelectionRequest as PromptRequest.TimeSelection).onConfirm(selectedDate)
+ verify(geckoPrompt).confirm(confirmCaptor.capture())
+ assertEquals(confirmCaptor.value.toDate("yyyy-MM-dd"), selectedDate)
+ assertEquals((timeSelectionRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test
+ fun `onDateTimePrompt called with DATETIME_TYPE_MONTH must provide a date PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var dateRequest: PromptRequest? = null
+ var confirmCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ dateRequest = promptRequest
+ }
+ },
+ )
+ val geckoPrompt = geckoDateTimePrompt(type = MONTH)
+
+ val geckoResult = promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ confirmCalled = true
+ }
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(dateRequest is PromptRequest.TimeSelection)
+ (dateRequest as PromptRequest.TimeSelection).onConfirm(Date())
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(confirmCalled)
+ assertEquals((dateRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test
+ fun `onDateTimePrompt DATETIME_TYPE_MONTH with date parameters must format dates correctly`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var timeSelectionRequest: PromptRequest.TimeSelection? = null
+ val confirmCaptor = argumentCaptor<String>()
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ timeSelectionRequest = promptRequest as PromptRequest.TimeSelection
+ }
+ },
+ )
+ val geckoPrompt = geckoDateTimePrompt(
+ title = "title",
+ type = MONTH,
+ defaultValue = "2019-11",
+ minValue = "2019-11",
+ maxValue = "2019-11",
+ )
+ promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+
+ assertNotNull(timeSelectionRequest)
+ with(timeSelectionRequest!!) {
+ assertEquals(initialDate, "2019-11".toDate("yyyy-MM"))
+ assertEquals(minimumDate, "2019-11".toDate("yyyy-MM"))
+ assertEquals(maximumDate, "2019-11".toDate("yyyy-MM"))
+ }
+ val selectedDate = "2019-11".toDate("yyyy-MM")
+ (timeSelectionRequest as PromptRequest.TimeSelection).onConfirm(selectedDate)
+ verify(geckoPrompt).confirm(confirmCaptor.capture())
+ assertEquals(confirmCaptor.value.toDate("yyyy-MM"), selectedDate)
+ assertEquals((timeSelectionRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test
+ fun `onDateTimePrompt called with DATETIME_TYPE_WEEK must provide a date PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var dateRequest: PromptRequest? = null
+ var confirmCalled = false
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ dateRequest = promptRequest
+ }
+ },
+ )
+ val geckoPrompt = geckoDateTimePrompt(type = WEEK)
+
+ val geckoResult = promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ confirmCalled = true
+ }
+
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(dateRequest is PromptRequest.TimeSelection)
+ (dateRequest as PromptRequest.TimeSelection).onConfirm(Date())
+ shadowOf(getMainLooper()).idle()
+ assertTrue(confirmCalled)
+ assertEquals((dateRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test
+ fun `onDateTimePrompt DATETIME_TYPE_WEEK with date parameters must format dates correctly`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var timeSelectionRequest: PromptRequest.TimeSelection? = null
+ val confirmCaptor = argumentCaptor<String>()
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ timeSelectionRequest = promptRequest as PromptRequest.TimeSelection
+ }
+ },
+ )
+
+ val geckoPrompt = geckoDateTimePrompt(
+ title = "title",
+ type = WEEK,
+ defaultValue = "2018-W18",
+ minValue = "2018-W18",
+ maxValue = "2018-W26",
+ )
+ promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+
+ assertNotNull(timeSelectionRequest)
+ with(timeSelectionRequest!!) {
+ assertEquals(initialDate, "2018-W18".toDate("yyyy-'W'ww"))
+ assertEquals(minimumDate, "2018-W18".toDate("yyyy-'W'ww"))
+ assertEquals(maximumDate, "2018-W26".toDate("yyyy-'W'ww"))
+ }
+ val selectedDate = "2018-W26".toDate("yyyy-'W'ww")
+ (timeSelectionRequest as PromptRequest.TimeSelection).onConfirm(selectedDate)
+ verify(geckoPrompt).confirm(confirmCaptor.capture())
+ assertEquals(confirmCaptor.value.toDate("yyyy-'W'ww"), selectedDate)
+ assertEquals((timeSelectionRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test
+ fun `onDateTimePrompt called with DATETIME_TYPE_TIME must provide a TimeSelection PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var dateRequest: PromptRequest? = null
+ var confirmCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ dateRequest = promptRequest
+ }
+ },
+ )
+ val geckoPrompt = geckoDateTimePrompt(type = TIME)
+
+ val geckoResult = promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ confirmCalled = true
+ }
+
+ assertTrue(dateRequest is PromptRequest.TimeSelection)
+ (dateRequest as PromptRequest.TimeSelection).onConfirm(Date())
+ shadowOf(getMainLooper()).idle()
+ assertTrue(confirmCalled)
+ assertEquals((dateRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test
+ fun `onDateTimePrompt DATETIME_TYPE_TIME with time parameters must format time correctly`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var timeSelectionRequest: PromptRequest.TimeSelection? = null
+ val confirmCaptor = argumentCaptor<String>()
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ timeSelectionRequest = promptRequest as PromptRequest.TimeSelection
+ }
+ },
+ )
+
+ val geckoPrompt = geckoDateTimePrompt(
+ title = "title",
+ type = TIME,
+ defaultValue = "17:00",
+ minValue = "9:00",
+ maxValue = "18:00",
+ )
+ promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+
+ assertNotNull(timeSelectionRequest)
+ with(timeSelectionRequest!!) {
+ assertEquals(initialDate, "17:00".toDate("HH:mm"))
+ assertEquals(minimumDate, "9:00".toDate("HH:mm"))
+ assertEquals(maximumDate, "18:00".toDate("HH:mm"))
+ }
+ val selectedDate = "17:00".toDate("HH:mm")
+ (timeSelectionRequest as PromptRequest.TimeSelection).onConfirm(selectedDate)
+ verify(geckoPrompt).confirm(confirmCaptor.capture())
+ assertEquals(confirmCaptor.value.toDate("HH:mm"), selectedDate)
+ assertEquals((timeSelectionRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test
+ fun `onDateTimePrompt DATETIME_TYPE_TIME with stepValue time parameter must format time correctly`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var timeSelectionRequest: PromptRequest.TimeSelection? = null
+ val confirmCaptor = argumentCaptor<String>()
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ timeSelectionRequest = promptRequest as PromptRequest.TimeSelection
+ }
+ },
+ )
+ val minutesGeckoPrompt = geckoDateTimePrompt(
+ type = TIME,
+ defaultValue = "17:00",
+ stepValue = "",
+ )
+ val secondsGeckoPrompt = geckoDateTimePrompt(
+ type = TIME,
+ defaultValue = "17:00:00",
+ stepValue = "1",
+ )
+ val millisecondsGeckoPrompt = geckoDateTimePrompt(
+ type = TIME,
+ defaultValue = "17:00:00.000",
+ stepValue = "0.1",
+ )
+
+ promptDelegate.onDateTimePrompt(mock(), minutesGeckoPrompt)
+
+ var selectedTime = "17:00"
+ assertNotNull(timeSelectionRequest)
+ (timeSelectionRequest as PromptRequest.TimeSelection).onConfirm(selectedTime.toDate("HH:mm"))
+ verify(minutesGeckoPrompt).confirm(confirmCaptor.capture())
+ assertEquals(selectedTime, confirmCaptor.value)
+
+ promptDelegate.onDateTimePrompt(mock(), secondsGeckoPrompt)
+
+ selectedTime = "17:00:25"
+ assertNotNull(timeSelectionRequest)
+ (timeSelectionRequest as PromptRequest.TimeSelection).onConfirm(selectedTime.toDate("HH:mm:ss"))
+ verify(secondsGeckoPrompt).confirm(confirmCaptor.capture())
+ assertEquals(selectedTime, confirmCaptor.value)
+
+ promptDelegate.onDateTimePrompt(mock(), millisecondsGeckoPrompt)
+
+ selectedTime = "17:00:20.100"
+ assertNotNull(timeSelectionRequest)
+ (timeSelectionRequest as PromptRequest.TimeSelection).onConfirm(selectedTime.toDate("HH:mm:ss.SSS"))
+ verify(millisecondsGeckoPrompt).confirm(confirmCaptor.capture())
+ assertEquals(selectedTime, confirmCaptor.value)
+ }
+
+ @Test
+ fun `WHEN DateTimePrompt request with invalid stepValue parameter is triggered THEN stepValue is passed as null`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var timeSelectionRequest: PromptRequest.TimeSelection? = null
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ timeSelectionRequest = promptRequest as PromptRequest.TimeSelection
+ }
+ },
+ )
+ val geckoPrompt = geckoDateTimePrompt(
+ type = TIME,
+ defaultValue = "17:00",
+ stepValue = "Time",
+ )
+
+ promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+
+ assertNotNull(timeSelectionRequest)
+ assertEquals(PromptRequest.TimeSelection.Type.TIME, timeSelectionRequest?.type)
+ assertNull(timeSelectionRequest?.stepValue)
+ }
+
+ @Test
+ fun `onDateTimePrompt called with DATETIME_TYPE_DATETIME_LOCAL must provide a TimeSelection PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var dateRequest: PromptRequest? = null
+ var confirmCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ dateRequest = promptRequest
+ }
+ },
+ )
+ val geckoResult =
+ promptDelegate.onDateTimePrompt(mock(), geckoDateTimePrompt(type = DATETIME_LOCAL))
+ geckoResult!!.accept {
+ confirmCalled = true
+ }
+
+ assertTrue(dateRequest is PromptRequest.TimeSelection)
+ (dateRequest as PromptRequest.TimeSelection).onConfirm(Date())
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(confirmCalled)
+ assertEquals((dateRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test
+ fun `onDateTimePrompt DATETIME_TYPE_DATETIME_LOCAL with date parameters must format time correctly`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var timeSelectionRequest: PromptRequest.TimeSelection? = null
+ val confirmCaptor = argumentCaptor<String>()
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ timeSelectionRequest = promptRequest as PromptRequest.TimeSelection
+ }
+ },
+ )
+ val geckoPrompt = geckoDateTimePrompt(
+ title = "title",
+ type = DATETIME_LOCAL,
+ defaultValue = "2018-06-12T19:30",
+ minValue = "2018-06-07T00:00",
+ maxValue = "2018-06-14T00:00",
+ )
+ promptDelegate.onDateTimePrompt(mock(), geckoPrompt)
+
+ assertNotNull(timeSelectionRequest)
+ with(timeSelectionRequest!!) {
+ assertEquals(initialDate, "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm"))
+ assertEquals(minimumDate, "2018-06-07T00:00".toDate("yyyy-MM-dd'T'HH:mm"))
+ assertEquals(maximumDate, "2018-06-14T00:00".toDate("yyyy-MM-dd'T'HH:mm"))
+ }
+ val selectedDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm")
+ (timeSelectionRequest as PromptRequest.TimeSelection).onConfirm(selectedDate)
+ verify(geckoPrompt).confirm(confirmCaptor.capture())
+ assertEquals(confirmCaptor.value.toDate("yyyy-MM-dd'T'HH:mm"), selectedDate)
+ assertEquals((timeSelectionRequest as PromptRequest.TimeSelection).title, "title")
+ }
+
+ @Test(expected = InvalidParameterException::class)
+ fun `Calling onDateTimePrompt with invalid DatetimeType will throw an exception`() {
+ val promptDelegate = GeckoPromptDelegate(mock())
+ promptDelegate.onDateTimePrompt(
+ mock(),
+ geckoDateTimePrompt(
+ type = 13223,
+ defaultValue = "17:00",
+ minValue = "9:00",
+ maxValue = "18:00",
+ ),
+ )
+ }
+
+ @Test
+ fun `date to string`() {
+ val date = Date()
+
+ var dateString = date.toString()
+ assertNotNull(dateString.isEmpty())
+
+ dateString = date.toString("yyyy")
+ val calendar = Calendar.getInstance()
+ calendar.time = date
+ val year = calendar[YEAR].toString()
+ assertEquals(dateString, year)
+ }
+
+ @Test
+ fun `Calling onFilePrompt must provide a FilePicker PromptRequest`() {
+ val context = spy(testContext)
+ val contentResolver = spy(context.contentResolver)
+ val mockSession = GeckoEngineSession(runtime)
+ var onSingleFileSelectedWasCalled = false
+ var onMultipleFilesSelectedWasCalled = false
+ var onDismissWasCalled = false
+ val mockUri: Uri = mock()
+
+ doReturn(contentResolver).`when`(context).contentResolver
+
+ var filePickerRequest: PromptRequest.File = mock()
+
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ // Prevent the file from being copied
+ doReturn(mockUri).`when`(promptDelegate).toFileUri(any(), any())
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ filePickerRequest = promptRequest as PromptRequest.File
+ }
+ },
+ )
+ var geckoPrompt = geckoFilePrompt(type = GECKO_PROMPT_FILE_TYPE.SINGLE, capture = NONE)
+
+ var geckoResult = promptDelegate.onFilePrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onSingleFileSelectedWasCalled = true
+ }
+
+ filePickerRequest.onSingleFileSelected(context, mockUri)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onSingleFileSelectedWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onSingleFileSelectedWasCalled = false
+ filePickerRequest.onSingleFileSelected(context, mockUri)
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onSingleFileSelectedWasCalled)
+
+ geckoPrompt = geckoFilePrompt(type = GECKO_PROMPT_FILE_TYPE.MULTIPLE, capture = ANY)
+ geckoResult = promptDelegate.onFilePrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onMultipleFilesSelectedWasCalled = true
+ }
+
+ filePickerRequest.onMultipleFilesSelected(context, arrayOf(mockUri))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onMultipleFilesSelectedWasCalled)
+
+ geckoPrompt = geckoFilePrompt(type = GECKO_PROMPT_FILE_TYPE.SINGLE, capture = NONE)
+ geckoResult = promptDelegate.onFilePrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onDismissWasCalled = true
+ }
+
+ filePickerRequest.onDismiss()
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onDismissWasCalled)
+
+ assertTrue(filePickerRequest.mimeTypes.isEmpty())
+ assertFalse(filePickerRequest.isMultipleFilesSelection)
+ assertEquals(PromptRequest.File.FacingMode.NONE, filePickerRequest.captureMode)
+
+ promptDelegate.onFilePrompt(
+ mock(),
+ geckoFilePrompt(type = GECKO_PROMPT_FILE_TYPE.MULTIPLE, capture = USER),
+ )
+
+ assertTrue(filePickerRequest.isMultipleFilesSelection)
+ assertEquals(
+ PromptRequest.File.FacingMode.FRONT_CAMERA,
+ filePickerRequest.captureMode,
+ )
+ }
+
+ @Test
+ fun `Calling onLoginSave must provide an SaveLoginPrompt PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var onLoginSaved = false
+ var onDismissWasCalled = false
+
+ var loginSaveRequest: PromptRequest.SaveLoginPrompt = mock()
+
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ loginSaveRequest = promptRequest as PromptRequest.SaveLoginPrompt
+ }
+ },
+ )
+
+ val entry = createLoginEntry()
+ val saveOption = Autocomplete.LoginSaveOption(entry.toLoginEntry())
+
+ var geckoResult =
+ promptDelegate.onLoginSave(mock(), geckoLoginSavePrompt(arrayOf(saveOption)))
+
+ geckoResult!!.accept {
+ onDismissWasCalled = true
+ }
+
+ loginSaveRequest.onDismiss()
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onDismissWasCalled)
+
+ val geckoPrompt = geckoLoginSavePrompt(arrayOf(saveOption))
+ geckoResult = promptDelegate.onLoginSave(mock(), geckoPrompt)
+
+ geckoResult!!.accept {
+ onLoginSaved = true
+ }
+
+ loginSaveRequest.onConfirm(entry)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onLoginSaved)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onLoginSaved = false
+
+ loginSaveRequest.onConfirm(entry)
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onLoginSaved)
+ }
+
+ @Test
+ fun `Calling onLoginSave must set a PromptInstanceDismissDelegate`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var loginSaveRequest: PromptRequest.SaveLoginPrompt = mock()
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ loginSaveRequest = promptRequest as PromptRequest.SaveLoginPrompt
+ }
+ },
+ )
+ val login = createLogin()
+ val saveOption = Autocomplete.LoginSaveOption(login.toLoginEntry())
+ val saveLoginPrompt = geckoLoginSavePrompt(arrayOf(saveOption))
+
+ promptDelegate.onLoginSave(mock(), saveLoginPrompt)
+
+ assertNotNull(loginSaveRequest)
+ assertNotNull(saveLoginPrompt.delegate)
+ }
+
+ @Test
+ fun `Calling onLoginSelect must provide an SelectLoginPrompt PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var onLoginSelected = false
+ var onDismissWasCalled = false
+
+ var loginSelectRequest: PromptRequest.SelectLoginPrompt = mock()
+
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ loginSelectRequest = promptRequest as PromptRequest.SelectLoginPrompt
+ }
+ },
+ )
+
+ val login = createLogin()
+ val loginSelectOption = Autocomplete.LoginSelectOption(login.toLoginEntry())
+
+ val secondLogin = createLogin(username = "username2")
+ val secondLoginSelectOption = Autocomplete.LoginSelectOption(secondLogin.toLoginEntry())
+
+ var geckoResult =
+ promptDelegate.onLoginSelect(
+ mock(),
+ geckoLoginSelectPrompt(arrayOf(loginSelectOption, secondLoginSelectOption)),
+ )
+
+ geckoResult!!.accept {
+ onDismissWasCalled = true
+ }
+
+ loginSelectRequest.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+
+ val geckoPrompt = geckoLoginSelectPrompt(arrayOf(loginSelectOption, secondLoginSelectOption))
+ geckoResult = promptDelegate.onLoginSelect(
+ mock(),
+ geckoPrompt,
+ )
+
+ geckoResult!!.accept {
+ onLoginSelected = true
+ }
+
+ loginSelectRequest.onConfirm(login)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onLoginSelected)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onLoginSelected = false
+ loginSelectRequest.onConfirm(login)
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onLoginSelected)
+ }
+
+ fun createLogin(
+ guid: String = "id",
+ password: String = "password",
+ username: String = "username",
+ origin: String = "https://www.origin.com",
+ httpRealm: String = "httpRealm",
+ formActionOrigin: String = "https://www.origin.com",
+ usernameField: String = "usernameField",
+ passwordField: String = "passwordField",
+ ) = Login(
+ guid = guid,
+ origin = origin,
+ password = password,
+ username = username,
+ httpRealm = httpRealm,
+ formActionOrigin = formActionOrigin,
+ usernameField = usernameField,
+ passwordField = passwordField,
+ )
+
+ fun createLoginEntry(
+ password: String = "password",
+ username: String = "username",
+ origin: String = "https://www.origin.com",
+ httpRealm: String = "httpRealm",
+ formActionOrigin: String = "https://www.origin.com",
+ usernameField: String = "usernameField",
+ passwordField: String = "passwordField",
+ ) = LoginEntry(
+ origin = origin,
+ password = password,
+ username = username,
+ httpRealm = httpRealm,
+ formActionOrigin = formActionOrigin,
+ usernameField = usernameField,
+ passwordField = passwordField,
+ )
+
+ @Test
+ fun `Calling onCreditCardSave must provide an SaveCreditCard PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var onCreditCardSaved = false
+ var onDismissWasCalled = false
+
+ var saveCreditCardPrompt: PromptRequest.SaveCreditCard = mock()
+
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ saveCreditCardPrompt = promptRequest as PromptRequest.SaveCreditCard
+ }
+ },
+ )
+
+ val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+ val creditCardSaveOption =
+ Autocomplete.CreditCardSaveOption(creditCard.toAutocompleteCreditCard())
+
+ var geckoResult = promptDelegate.onCreditCardSave(
+ mock(),
+ geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption)),
+ )
+
+ geckoResult.accept {
+ onDismissWasCalled = true
+ }
+
+ saveCreditCardPrompt.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+
+ val geckoPrompt = geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption))
+ geckoResult = promptDelegate.onCreditCardSave(mock(), geckoPrompt)
+
+ geckoResult.accept {
+ onCreditCardSaved = true
+ }
+
+ saveCreditCardPrompt.onConfirm(creditCard)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onCreditCardSaved)
+
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+ onCreditCardSaved = false
+ saveCreditCardPrompt.onConfirm(creditCard)
+
+ assertFalse(onCreditCardSaved)
+ }
+
+ @Test
+ fun `Calling onCreditSave must set a PromptInstanceDismissDelegate`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var saveCreditCardPrompt: PromptRequest.SaveCreditCard = mock()
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ saveCreditCardPrompt = promptRequest as PromptRequest.SaveCreditCard
+ }
+ },
+ )
+
+ val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+ val creditCardSaveOption =
+ Autocomplete.CreditCardSaveOption(creditCard.toAutocompleteCreditCard())
+ val geckoPrompt = geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption))
+
+ promptDelegate.onCreditCardSave(mock(), geckoPrompt)
+
+ assertNotNull(saveCreditCardPrompt)
+ assertNotNull(geckoPrompt.delegate)
+ }
+
+ @Test
+ fun `Calling onCreditCardSelect must provide as CreditCardSelectOption PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var onConfirmWasCalled = false
+ var onDismissWasCalled = false
+
+ var selectCreditCardPrompt: PromptRequest.SelectCreditCard = mock()
+
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ selectCreditCardPrompt = promptRequest as PromptRequest.SelectCreditCard
+ }
+ },
+ )
+
+ val creditCard1 = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+ val creditCardSelectOption1 =
+ Autocomplete.CreditCardSelectOption(creditCard1.toAutocompleteCreditCard())
+
+ val creditCard2 = CreditCardEntry(
+ guid = "2",
+ name = "Orange Pineapple",
+ number = "4111111111115555",
+ expiryMonth = "1",
+ expiryYear = "2040",
+ cardType = "amex",
+ )
+ val creditCardSelectOption2 =
+ Autocomplete.CreditCardSelectOption(creditCard2.toAutocompleteCreditCard())
+
+ var geckoResult = promptDelegate.onCreditCardSelect(
+ mock(),
+ geckoSelectCreditCardPrompt(arrayOf(creditCardSelectOption1, creditCardSelectOption2)),
+ )
+
+ geckoResult!!.accept {
+ onDismissWasCalled = true
+ }
+
+ selectCreditCardPrompt.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+
+ val geckoPrompt =
+ geckoSelectCreditCardPrompt(arrayOf(creditCardSelectOption1, creditCardSelectOption2))
+ geckoResult = promptDelegate.onCreditCardSelect(mock(), geckoPrompt)
+
+ geckoResult!!.accept {
+ onConfirmWasCalled = true
+ }
+
+ selectCreditCardPrompt.onConfirm(creditCard1)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onConfirmWasCalled)
+
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+ onConfirmWasCalled = false
+ selectCreditCardPrompt.onConfirm(creditCard1)
+
+ assertFalse(onConfirmWasCalled)
+ }
+
+ @Test
+ fun `Calling onAuthPrompt must provide an Authentication PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var authRequest: PromptRequest.Authentication = mock()
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ authRequest = promptRequest as PromptRequest.Authentication
+ }
+ },
+ )
+
+ var geckoPrompt = geckoAuthPrompt(authOptions = mock())
+ promptDelegate.onAuthPrompt(mock(), geckoPrompt)
+
+ authRequest.onConfirm("", "")
+ verify(geckoPrompt, times(1)).confirm(eq(""), eq(""))
+
+ geckoPrompt = geckoAuthPrompt(authOptions = mock())
+ promptDelegate.onAuthPrompt(mock(), geckoPrompt)
+ authRequest.onDismiss()
+ verify(geckoPrompt, times(1)).dismiss()
+
+ val authOptions = geckoAuthOptions()
+ ReflectionUtils.setField(authOptions, "level", GECKO_AUTH_LEVEL.SECURE)
+
+ var flags = 0
+ flags = flags.or(GECKO_AUTH_FLAGS.ONLY_PASSWORD)
+ flags = flags.or(GECKO_AUTH_FLAGS.PREVIOUS_FAILED)
+ flags = flags.or(GECKO_AUTH_FLAGS.CROSS_ORIGIN_SUB_RESOURCE)
+ flags = flags.or(GECKO_AUTH_FLAGS.HOST)
+ ReflectionUtils.setField(authOptions, "flags", flags)
+
+ geckoPrompt = geckoAuthPrompt(authOptions = authOptions)
+ promptDelegate.onAuthPrompt(mock(), geckoPrompt)
+
+ authRequest.onConfirm("", "")
+
+ with(authRequest) {
+ assertTrue(onlyShowPassword)
+ assertTrue(previousFailed)
+ assertTrue(isCrossOrigin)
+
+ assertEquals(method, AC_AUTH_METHOD.HOST)
+ assertEquals(level, AC_AUTH_LEVEL.SECURED)
+
+ verify(geckoPrompt, never()).confirm(eq(""), eq(""))
+ verify(geckoPrompt, times(1)).confirm(eq(""))
+ }
+
+ ReflectionUtils.setField(authOptions, "level", GECKO_AUTH_LEVEL.PW_ENCRYPTED)
+
+ promptDelegate.onAuthPrompt(mock(), geckoAuthPrompt(authOptions = authOptions))
+
+ assertEquals(authRequest.level, AC_AUTH_LEVEL.PASSWORD_ENCRYPTED)
+
+ ReflectionUtils.setField(authOptions, "level", -2423)
+
+ promptDelegate.onAuthPrompt(mock(), geckoAuthPrompt(authOptions = authOptions))
+
+ assertEquals(authRequest.level, AC_AUTH_LEVEL.NONE)
+ }
+
+ @Test
+ fun `WHEN onSelectIdentityCredentialProvider is called THEN SelectProvider prompt request must be provided with the correct callbacks`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var selectProviderRequest: IdentityCredential.SelectProvider = mock()
+ var onConfirmWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ selectProviderRequest = promptRequest as IdentityCredential.SelectProvider
+ }
+ },
+ )
+
+ val geckoProvider = GECKO_PROMPT_PROVIDER_SELECTOR(0, "name", "icon", "domain")
+ val acProvider = geckoProvider.toProvider()
+ val geckoPrompt = geckoProviderSelectorPrompt(listOf(geckoProvider))
+ var geckoResult = promptDelegate.onSelectIdentityCredentialProvider(mock(), geckoPrompt)
+
+ geckoResult.accept {
+ onConfirmWasCalled = true
+ }
+
+ with(selectProviderRequest) {
+ // Verifying we are parsing the providers correctly.
+ assertEquals(acProvider, this.providers.first())
+
+ onConfirm(acProvider)
+
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onConfirmWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ // Just making sure we are not completing the geckoResult twice.
+ onConfirmWasCalled = false
+ onConfirm(acProvider)
+ shadowOf(getMainLooper()).idle()
+ assertFalse(onConfirmWasCalled)
+ }
+
+ // Verifying we are handling the dismiss correctly.
+ geckoResult = promptDelegate.onSelectIdentityCredentialProvider(mock(), geckoProviderSelectorPrompt(listOf(geckoProvider)))
+ geckoResult.accept {
+ onDismissWasCalled = true
+ }
+
+ selectProviderRequest.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `WHEN onSelectIdentityCredentialAccount is called THEN SelectAccount prompt request must be provided with the correct callbacks`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var selectAccountRequest: IdentityCredential.SelectAccount = mock()
+ var onConfirmWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ selectAccountRequest = promptRequest as IdentityCredential.SelectAccount
+ }
+ },
+ )
+
+ val geckoAccount = GECKO_PROMPT_ACCOUNT_SELECTOR(0, "foo@mozilla.org", "foo", "icon")
+ val provider = GECKO_PROMPT_ACCOUNT_SELECTOR_PROVIDER("name", "domain", "favicon")
+ val acAccount = geckoAccount.toAccount()
+ val geckoPrompt = geckoAccountSelectorPrompt(listOf(geckoAccount), provider)
+ var geckoResult = promptDelegate.onSelectIdentityCredentialAccount(mock(), geckoPrompt)
+
+ geckoResult.accept {
+ onConfirmWasCalled = true
+ }
+
+ with(selectAccountRequest) {
+ // Verifying we are parsing the providers correctly.
+ assertEquals(acAccount, this.accounts.first())
+
+ onConfirm(acAccount)
+
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onConfirmWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ // Just making sure we are not completing the geckoResult twice.
+ onConfirmWasCalled = false
+ onConfirm(acAccount)
+ shadowOf(getMainLooper()).idle()
+ assertFalse(onConfirmWasCalled)
+ }
+
+ // Verifying we are handling the dismiss correctly.
+ geckoResult = promptDelegate.onSelectIdentityCredentialAccount(mock(), geckoAccountSelectorPrompt(listOf(geckoAccount), provider))
+ geckoResult.accept {
+ onDismissWasCalled = true
+ }
+
+ selectAccountRequest.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `WHEN onShowPrivacyPolicyIdentityCredential is called THEN the PrivacyPolicy prompt request must be provided with the correct callbacks`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var privacyPolicyRequest: IdentityCredential.PrivacyPolicy = mock()
+ var onConfirmWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ privacyPolicyRequest = promptRequest as IdentityCredential.PrivacyPolicy
+ }
+ },
+ )
+
+ val geckoPrompt = geckoPrivacyPolicyPrompt()
+ var geckoResult = promptDelegate.onShowPrivacyPolicyIdentityCredential(mock(), geckoPrompt)
+
+ geckoResult.accept {
+ onConfirmWasCalled = true
+ }
+
+ with(privacyPolicyRequest) {
+ // Verifying we are parsing the providers correctly.
+ assertEquals(privacyPolicyUrl, "privacyPolicyUrl")
+ assertEquals(termsOfServiceUrl, "termsOfServiceUrl")
+ assertEquals(providerDomain, "providerDomain")
+ assertEquals(host, "host")
+ assertEquals(icon, "icon")
+
+ onConfirm(true)
+
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onConfirmWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ // Just making sure we are not completing the geckoResult twice.
+ onConfirmWasCalled = false
+ onConfirm(true)
+ shadowOf(getMainLooper()).idle()
+ assertFalse(onConfirmWasCalled)
+ }
+
+ // Verifying we are handling the dismiss correctly.
+ geckoResult = promptDelegate.onShowPrivacyPolicyIdentityCredential(mock(), geckoPrivacyPolicyPrompt())
+ geckoResult.accept {
+ onDismissWasCalled = true
+ }
+
+ privacyPolicyRequest.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `Calling onColorPrompt must provide a Color PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var colorRequest: PromptRequest.Color = mock()
+ var onConfirmWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ colorRequest = promptRequest as PromptRequest.Color
+ }
+ },
+ )
+
+ val geckoPrompt = geckoColorPrompt(defaultValue = "#e66465")
+ var geckoResult = promptDelegate.onColorPrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onConfirmWasCalled = true
+ }
+
+ with(colorRequest) {
+ assertEquals(defaultColor, "#e66465")
+ onConfirm("#f6b73c")
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onConfirmWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onConfirmWasCalled = false
+ onConfirm("#f6b73c")
+ shadowOf(getMainLooper()).idle()
+ assertFalse(onConfirmWasCalled)
+ }
+
+ geckoResult = promptDelegate.onColorPrompt(mock(), geckoColorPrompt())
+ geckoResult!!.accept {
+ onDismissWasCalled = true
+ }
+
+ colorRequest.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+
+ with(colorRequest) {
+ assertEquals(defaultColor, "defaultValue")
+ }
+ }
+
+ @Test
+ fun `onTextPrompt must provide an TextPrompt PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var request: PromptRequest.TextPrompt = mock()
+ var dismissWasCalled = false
+ var confirmWasCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.TextPrompt
+ }
+ },
+ )
+
+ var geckoResult = promptDelegate.onTextPrompt(mock(), geckoTextPrompt())
+ geckoResult!!.accept {
+ dismissWasCalled = true
+ }
+
+ with(request) {
+ assertEquals(title, "title")
+ assertEquals(inputLabel, "message")
+ assertEquals(inputValue, "defaultValue")
+
+ onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(dismissWasCalled)
+ }
+
+ val geckoPrompt = geckoTextPrompt()
+ geckoResult = promptDelegate.onTextPrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ confirmWasCalled = true
+ }
+
+ request.onConfirm(true, "newInput")
+ shadowOf(getMainLooper()).idle()
+ assertTrue(confirmWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ confirmWasCalled = false
+ request.onConfirm(true, "newInput")
+ shadowOf(getMainLooper()).idle()
+ assertFalse(confirmWasCalled)
+ }
+
+ @Test
+ fun `onPopupRequest must provide a Popup PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var request: PromptRequest.Popup? = null
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.Popup
+ }
+ },
+ )
+
+ var geckoPrompt = geckoPopupPrompt(targetUri = "www.popuptest.com/")
+ promptDelegate.onPopupPrompt(mock(), geckoPrompt)
+
+ with(request!!) {
+ assertEquals(targetUri, "www.popuptest.com/")
+
+ onAllow()
+ verify(geckoPrompt, times(1)).confirm(eq(AllowOrDeny.ALLOW))
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onAllow()
+ verify(geckoPrompt, times(1)).confirm(eq(AllowOrDeny.ALLOW))
+ }
+
+ geckoPrompt = geckoPopupPrompt()
+ promptDelegate.onPopupPrompt(mock(), geckoPrompt)
+
+ request!!.onDeny()
+ verify(geckoPrompt, times(1)).confirm(eq(AllowOrDeny.DENY))
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ request!!.onDeny()
+ verify(geckoPrompt, times(1)).confirm(eq(AllowOrDeny.DENY))
+ }
+
+ @Test
+ fun `onBeforeUnloadPrompt must provide a BeforeUnload PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var request: PromptRequest.BeforeUnload? = null
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.BeforeUnload
+ }
+ },
+ )
+
+ var geckoPrompt = geckoBeforeUnloadPrompt()
+ promptDelegate.onBeforeUnloadPrompt(mock(), geckoPrompt)
+ assertEquals(request!!.title, "")
+
+ request!!.onLeave()
+ verify(geckoPrompt, times(1)).confirm(eq(AllowOrDeny.ALLOW))
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ request!!.onLeave()
+ verify(geckoPrompt, times(1)).confirm(eq(AllowOrDeny.ALLOW))
+
+ geckoPrompt = geckoBeforeUnloadPrompt()
+ promptDelegate.onBeforeUnloadPrompt(mock(), geckoPrompt)
+
+ request!!.onStay()
+ verify(geckoPrompt, times(1)).confirm(eq(AllowOrDeny.DENY))
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ request!!.onStay()
+ verify(geckoPrompt, times(1)).confirm(eq(AllowOrDeny.DENY))
+ }
+
+ @Test
+ fun `onBeforeUnloadPrompt will inform listeners when if navigation is cancelled`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var onBeforeUnloadPromptCancelledCalled = false
+ var request: PromptRequest.BeforeUnload = mock()
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.BeforeUnload
+ }
+
+ override fun onBeforeUnloadPromptDenied() {
+ onBeforeUnloadPromptCancelledCalled = true
+ }
+ },
+ )
+ val prompt = geckoBeforeUnloadPrompt()
+ doReturn(false).`when`(prompt).isComplete
+
+ GeckoPromptDelegate(mockSession).onBeforeUnloadPrompt(mock(), prompt)
+ request.onStay()
+
+ assertTrue(onBeforeUnloadPromptCancelledCalled)
+ }
+
+ @Test
+ fun `onSharePrompt must provide a Share PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var request: PromptRequest.Share? = null
+ var onSuccessWasCalled = false
+ var onFailureWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.Share
+ }
+ },
+ )
+
+ var geckoPrompt = geckoSharePrompt()
+ var geckoResult = promptDelegate.onSharePrompt(mock(), geckoPrompt)
+ geckoResult.accept {
+ onSuccessWasCalled = true
+ }
+
+ with(request!!) {
+ assertEquals(data.title, "title")
+ assertEquals(data.text, "text")
+ assertEquals(data.url, "https://example.com")
+
+ onSuccess()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onSuccessWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onSuccessWasCalled = false
+ onSuccess()
+ shadowOf(getMainLooper()).idle()
+ assertFalse(onSuccessWasCalled)
+ }
+
+ geckoPrompt = geckoSharePrompt()
+ geckoResult = promptDelegate.onSharePrompt(mock(), geckoPrompt)
+ geckoResult.accept {
+ onFailureWasCalled = true
+ }
+
+ request!!.onFailure()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onFailureWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onFailureWasCalled = false
+ request!!.onFailure()
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onFailureWasCalled)
+
+ geckoPrompt = geckoSharePrompt()
+ geckoResult = promptDelegate.onSharePrompt(mock(), geckoPrompt)
+ geckoResult.accept {
+ onDismissWasCalled = true
+ }
+
+ request!!.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `onButtonPrompt must provide a Confirm PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var request: PromptRequest.Confirm = mock()
+ var onPositiveButtonWasCalled = false
+ var onNegativeButtonWasCalled = false
+ var onNeutralButtonWasCalled = false
+ var dismissWasCalled = false
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.Confirm
+ }
+ },
+ )
+
+ var geckoPrompt = geckoButtonPrompt()
+ var geckoResult = promptDelegate.onButtonPrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onPositiveButtonWasCalled = true
+ }
+
+ with(request) {
+ assertNotNull(request)
+ assertEquals(title, "title")
+ assertEquals(message, "message")
+
+ onConfirmPositiveButton(false)
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onPositiveButtonWasCalled)
+
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+ onPositiveButtonWasCalled = false
+ onConfirmPositiveButton(false)
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onPositiveButtonWasCalled)
+ }
+
+ geckoPrompt = geckoButtonPrompt()
+ geckoResult = promptDelegate.onButtonPrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onNeutralButtonWasCalled = true
+ }
+
+ request.onConfirmNeutralButton(false)
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onNeutralButtonWasCalled)
+
+ geckoPrompt = geckoButtonPrompt()
+ geckoResult = promptDelegate.onButtonPrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onNegativeButtonWasCalled = true
+ }
+
+ request.onConfirmNegativeButton(false)
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onNegativeButtonWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onNegativeButtonWasCalled = false
+ request.onConfirmNegativeButton(false)
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onNegativeButtonWasCalled)
+
+ geckoResult = promptDelegate.onButtonPrompt(mock(), geckoButtonPrompt())
+ geckoResult!!.accept {
+ dismissWasCalled = true
+ }
+
+ request.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(dismissWasCalled)
+ }
+
+ @Test
+ fun `onRepostConfirmPrompt must provide a Repost PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var request: PromptRequest.Repost = mock()
+ var onPositiveButtonWasCalled = false
+ var onNegativeButtonWasCalled = false
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.Repost
+ }
+ },
+ )
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+
+ var geckoPrompt = geckoRepostPrompt()
+ var geckoResult = promptDelegate.onRepostConfirmPrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onPositiveButtonWasCalled = true
+ }
+ request.onConfirm()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onPositiveButtonWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onPositiveButtonWasCalled = false
+ request.onConfirm()
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onPositiveButtonWasCalled)
+
+ geckoPrompt = geckoRepostPrompt()
+ geckoResult = promptDelegate.onRepostConfirmPrompt(mock(), geckoPrompt)
+ geckoResult!!.accept {
+ onNegativeButtonWasCalled = true
+ }
+ request.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onNegativeButtonWasCalled)
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+
+ onNegativeButtonWasCalled = false
+ request.onDismiss()
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(onNegativeButtonWasCalled)
+ }
+
+ @Test
+ fun `onRepostConfirmPrompt will not be able to complete multiple times`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var request: PromptRequest.Repost = mock()
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.Repost
+ }
+ },
+ )
+
+ val promptDelegate = GeckoPromptDelegate(mockSession)
+
+ var prompt = geckoRepostPrompt()
+ promptDelegate.onRepostConfirmPrompt(mock(), prompt)
+ doReturn(false).`when`(prompt).isComplete
+ request.onConfirm()
+ verify(prompt).confirm(any())
+
+ prompt = mock()
+ promptDelegate.onRepostConfirmPrompt(mock(), prompt)
+ doReturn(true).`when`(prompt).isComplete
+ request.onConfirm()
+ verify(prompt, never()).confirm(any())
+
+ prompt = mock()
+ promptDelegate.onRepostConfirmPrompt(mock(), prompt)
+ doReturn(false).`when`(prompt).isComplete
+ request.onDismiss()
+ verify(prompt).confirm(any())
+
+ prompt = mock()
+ promptDelegate.onRepostConfirmPrompt(mock(), prompt)
+ doReturn(true).`when`(prompt).isComplete
+ request.onDismiss()
+ verify(prompt, never()).confirm(any())
+ }
+
+ @Test
+ fun `onRepostConfirmPrompt will inform listeners when it is being dismissed`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var onRepostPromptCancelledCalled = false
+ var request: PromptRequest.Repost = mock()
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest as PromptRequest.Repost
+ }
+
+ override fun onRepostPromptCancelled() {
+ onRepostPromptCancelledCalled = true
+ }
+ },
+ )
+ val prompt = geckoRepostPrompt()
+ doReturn(false).`when`(prompt).isComplete
+
+ GeckoPromptDelegate(mockSession).onRepostConfirmPrompt(mock(), prompt)
+ request.onDismiss()
+
+ assertTrue(onRepostPromptCancelledCalled)
+ }
+
+ @Test
+ fun `dismissSafely only dismiss if the prompt is NOT already dismissed`() {
+ val prompt = geckoAlertPrompt()
+ val geckoResult = mock<GeckoResult<GeckoSession.PromptDelegate.PromptResponse>>()
+
+ doReturn(false).`when`(prompt).isComplete
+
+ prompt.dismissSafely(geckoResult)
+
+ verify(geckoResult).complete(any())
+ }
+
+ @Test
+ fun `dismissSafely do nothing if the prompt is already dismissed`() {
+ val prompt = geckoAlertPrompt()
+ val geckoResult = mock<GeckoResult<GeckoSession.PromptDelegate.PromptResponse>>()
+
+ doReturn(true).`when`(prompt).isComplete
+
+ prompt.dismissSafely(geckoResult)
+
+ verify(geckoResult, never()).complete(any())
+ }
+
+ @Test
+ fun `WHEN onAddressSelect is called THEN SelectAddress prompt request must be provided with the correct callbacks`() {
+ val mockSession = GeckoEngineSession(runtime)
+
+ var isOnConfirmCalled = false
+ var isOnDismissCalled = false
+
+ var selectAddressPrompt: PromptRequest.SelectAddress = mock()
+
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ // Capture the SelectAddress prompt request
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ selectAddressPrompt = promptRequest as PromptRequest.SelectAddress
+ }
+ },
+ )
+
+ val address = Address(
+ guid = "1",
+ name = "Firefox",
+ organization = "-",
+ streetAddress = "street",
+ addressLevel3 = "address3",
+ addressLevel2 = "address2",
+ addressLevel1 = "address1",
+ postalCode = "1",
+ country = "Country",
+ tel = "1",
+ email = "@",
+ )
+ val addressSelectOption =
+ Autocomplete.AddressSelectOption(address.toAutocompleteAddress())
+
+ var geckoPrompt =
+ geckoSelectAddressPrompt(arrayOf(addressSelectOption))
+
+ var geckoResult = promptDelegate.onAddressSelect(
+ mock(),
+ geckoPrompt,
+ )
+
+ // Verify that the onDismiss callback was called
+ geckoResult.accept {
+ isOnDismissCalled = true
+ }
+
+ selectAddressPrompt.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(isOnDismissCalled)
+
+ // Verify that the onConfirm callback was called
+ geckoPrompt =
+ geckoSelectAddressPrompt(arrayOf(addressSelectOption))
+
+ geckoResult = promptDelegate.onAddressSelect(
+ mock(),
+ geckoPrompt,
+ )
+
+ geckoResult.accept {
+ isOnConfirmCalled = true
+ }
+
+ selectAddressPrompt.onConfirm(selectAddressPrompt.addresses.first())
+ shadowOf(getMainLooper()).idle()
+ assertTrue(isOnConfirmCalled)
+
+ // Verify that when the prompt request is already completed and onConfirm callback is called,
+ // then onConfirm callback is not executed
+ isOnConfirmCalled = false
+ geckoPrompt =
+ geckoSelectAddressPrompt(arrayOf(addressSelectOption), true)
+
+ geckoResult = promptDelegate.onAddressSelect(
+ mock(),
+ geckoPrompt,
+ )
+
+ geckoResult.accept {
+ isOnConfirmCalled = true
+ }
+
+ selectAddressPrompt.onConfirm(selectAddressPrompt.addresses.first())
+ shadowOf(getMainLooper()).idle()
+ assertFalse(isOnConfirmCalled)
+ }
+
+ private fun geckoChoicePrompt(
+ title: String,
+ message: String,
+ type: Int,
+ choices: Array<out GeckoChoice>,
+ ): GeckoSession.PromptDelegate.ChoicePrompt {
+ val prompt: GeckoSession.PromptDelegate.ChoicePrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "type", type)
+ ReflectionUtils.setField(prompt, "message", message)
+ ReflectionUtils.setField(prompt, "choices", choices)
+ return prompt
+ }
+
+ private fun geckoAlertPrompt(
+ title: String = "title",
+ message: String = "message",
+ ): GeckoSession.PromptDelegate.AlertPrompt {
+ val prompt: GeckoSession.PromptDelegate.AlertPrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "message", message)
+ return prompt
+ }
+
+ private fun geckoDateTimePrompt(
+ title: String = "title",
+ type: Int,
+ defaultValue: String = "",
+ minValue: String = "",
+ maxValue: String = "",
+ stepValue: String = "",
+ ): GeckoSession.PromptDelegate.DateTimePrompt {
+ val prompt: GeckoSession.PromptDelegate.DateTimePrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "type", type)
+ ReflectionUtils.setField(prompt, "defaultValue", defaultValue)
+ ReflectionUtils.setField(prompt, "minValue", minValue)
+ ReflectionUtils.setField(prompt, "maxValue", maxValue)
+ ReflectionUtils.setField(prompt, "stepValue", stepValue)
+ return prompt
+ }
+
+ private fun geckoFilePrompt(
+ title: String = "title",
+ type: Int,
+ capture: Int = 0,
+ mimeTypes: Array<out String> = emptyArray(),
+ ): GeckoSession.PromptDelegate.FilePrompt {
+ val prompt: GeckoSession.PromptDelegate.FilePrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "type", type)
+ ReflectionUtils.setField(prompt, "capture", capture)
+ ReflectionUtils.setField(prompt, "mimeTypes", mimeTypes)
+ return prompt
+ }
+
+ private fun geckoAuthPrompt(
+ title: String = "title",
+ message: String = "message",
+ authOptions: GeckoSession.PromptDelegate.AuthPrompt.AuthOptions,
+ ): GeckoSession.PromptDelegate.AuthPrompt {
+ val prompt: GeckoSession.PromptDelegate.AuthPrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "message", message)
+ ReflectionUtils.setField(prompt, "authOptions", authOptions)
+ return prompt
+ }
+
+ private fun geckoColorPrompt(
+ title: String = "title",
+ defaultValue: String = "defaultValue",
+ ): GeckoSession.PromptDelegate.ColorPrompt {
+ val prompt: GeckoSession.PromptDelegate.ColorPrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "defaultValue", defaultValue)
+ return prompt
+ }
+
+ private fun geckoProviderSelectorPrompt(
+ providers: List<GECKO_PROMPT_PROVIDER_SELECTOR> = emptyList(),
+ ): GeckoSession.PromptDelegate.IdentityCredential.ProviderSelectorPrompt {
+ val prompt: GeckoSession.PromptDelegate.IdentityCredential.ProviderSelectorPrompt = mock()
+ ReflectionUtils.setField(prompt, "providers", providers.toTypedArray())
+ return prompt
+ }
+
+ private fun geckoAccountSelectorPrompt(
+ accounts: List<GECKO_PROMPT_ACCOUNT_SELECTOR> = emptyList(),
+ provider: GECKO_PROMPT_ACCOUNT_SELECTOR_PROVIDER,
+ ): GeckoSession.PromptDelegate.IdentityCredential.AccountSelectorPrompt {
+ val prompt: GeckoSession.PromptDelegate.IdentityCredential.AccountSelectorPrompt = mock()
+ ReflectionUtils.setField(prompt, "accounts", accounts.toTypedArray())
+ ReflectionUtils.setField(prompt, "provider", provider)
+ return prompt
+ }
+
+ private fun geckoPrivacyPolicyPrompt(): GeckoSession.PromptDelegate.IdentityCredential.PrivacyPolicyPrompt {
+ val prompt: GeckoSession.PromptDelegate.IdentityCredential.PrivacyPolicyPrompt = mock()
+ ReflectionUtils.setField(prompt, "privacyPolicyUrl", "privacyPolicyUrl")
+ ReflectionUtils.setField(prompt, "termsOfServiceUrl", "termsOfServiceUrl")
+ ReflectionUtils.setField(prompt, "providerDomain", "providerDomain")
+ ReflectionUtils.setField(prompt, "host", "host")
+ ReflectionUtils.setField(prompt, "icon", "icon")
+ return prompt
+ }
+ private fun geckoTextPrompt(
+ title: String = "title",
+ message: String = "message",
+ defaultValue: String = "defaultValue",
+ ): GeckoSession.PromptDelegate.TextPrompt {
+ val prompt: GeckoSession.PromptDelegate.TextPrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "message", message)
+ ReflectionUtils.setField(prompt, "defaultValue", defaultValue)
+ return prompt
+ }
+
+ private fun geckoPopupPrompt(
+ targetUri: String = "targetUri",
+ ): GeckoSession.PromptDelegate.PopupPrompt {
+ val prompt: GeckoSession.PromptDelegate.PopupPrompt = mock()
+ ReflectionUtils.setField(prompt, "targetUri", targetUri)
+ return prompt
+ }
+
+ private fun geckoBeforeUnloadPrompt(): GeckoSession.PromptDelegate.BeforeUnloadPrompt {
+ return mock()
+ }
+
+ private fun geckoSharePrompt(
+ title: String? = "title",
+ text: String? = "text",
+ url: String? = "https://example.com",
+ ): GeckoSession.PromptDelegate.SharePrompt {
+ val prompt: GeckoSession.PromptDelegate.SharePrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "text", text)
+ ReflectionUtils.setField(prompt, "uri", url)
+ return prompt
+ }
+
+ private fun geckoButtonPrompt(
+ title: String = "title",
+ message: String = "message",
+ ): GeckoSession.PromptDelegate.ButtonPrompt {
+ val prompt: GeckoSession.PromptDelegate.ButtonPrompt = mock()
+ ReflectionUtils.setField(prompt, "title", title)
+ ReflectionUtils.setField(prompt, "message", message)
+ return prompt
+ }
+
+ private fun geckoLoginSelectPrompt(
+ loginArray: Array<Autocomplete.LoginSelectOption>,
+ ): GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSelectOption> {
+ val prompt: GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSelectOption> = mock()
+ ReflectionUtils.setField(prompt, "options", loginArray)
+ return prompt
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun geckoLoginSavePrompt(
+ login: Array<Autocomplete.LoginSaveOption>,
+ ): GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSaveOption> {
+ val prompt = Mockito.mock(
+ GeckoSession.PromptDelegate.AutocompleteRequest::class.java,
+ Mockito.RETURNS_DEEP_STUBS, // for testing prompt.delegate
+ ) as GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSaveOption>
+
+ ReflectionUtils.setField(prompt, "options", login)
+ return prompt
+ }
+
+ private fun geckoAuthOptions(): GeckoSession.PromptDelegate.AuthPrompt.AuthOptions {
+ return mock()
+ }
+
+ private fun geckoRepostPrompt(): GeckoSession.PromptDelegate.RepostConfirmPrompt {
+ return mock()
+ }
+
+ private fun geckoSelectCreditCardPrompt(
+ creditCards: Array<Autocomplete.CreditCardSelectOption>,
+ ): GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.CreditCardSelectOption> {
+ val prompt: GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.CreditCardSelectOption> =
+ mock()
+ ReflectionUtils.setField(prompt, "options", creditCards)
+ return prompt
+ }
+
+ private fun geckoSelectAddressPrompt(
+ addresses: Array<Autocomplete.AddressSelectOption>,
+ isComplete: Boolean = false,
+ ): GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.AddressSelectOption> {
+ val prompt: GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.AddressSelectOption> =
+ mock()
+ whenever(prompt.isComplete).thenReturn(isComplete)
+ ReflectionUtils.setField(prompt, "options", addresses)
+ return prompt
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun geckoCreditCardSavePrompt(
+ creditCard: Array<Autocomplete.CreditCardSaveOption>,
+ ): GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.CreditCardSaveOption> {
+ val prompt = Mockito.mock(
+ GeckoSession.PromptDelegate.AutocompleteRequest::class.java,
+ Mockito.RETURNS_DEEP_STUBS, // for testing prompt.delegate
+ ) as GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.CreditCardSaveOption>
+
+ ReflectionUtils.setField(prompt, "options", creditCard)
+ return prompt
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegateTest.kt
new file mode 100644
index 0000000000..6ae5f87862
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegateTest.kt
@@ -0,0 +1,40 @@
+/* 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.browser.engine.gecko.prompt
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.Autocomplete
+import org.mozilla.geckoview.GeckoSession
+
+@RunWith(AndroidJUnit4::class)
+class PromptInstanceDismissDelegateTest {
+
+ @Test
+ fun `GIVEN delegate with promptRequest WHEN onPromptDismiss called from geckoview THEN notifyObservers the prompt is dismissed`() {
+ val mockSession = GeckoEngineSession(mock())
+ var onDismissWasCalled = false
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptDismissed(promptRequest: PromptRequest) {
+ super.onPromptDismissed(promptRequest)
+ onDismissWasCalled = true
+ }
+ },
+ )
+ val basePrompt: GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSaveOption> = mock()
+ val prompt: PromptRequest.SingleChoice = mock()
+ val delegate = PromptInstanceDismissDelegate(mockSession, prompt)
+
+ delegate.onPromptDismiss(basePrompt)
+
+ assertTrue(onDismissWasCalled)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegateTest.kt
new file mode 100644
index 0000000000..8e784cacc1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegateTest.kt
@@ -0,0 +1,92 @@
+/* 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.browser.engine.gecko.selection
+
+import android.app.Activity
+import android.app.Application
+import android.app.Service
+import android.view.MenuItem
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import mozilla.components.support.test.mock
+import org.junit.Assert
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class GeckoSelectionActionDelegateTest {
+
+ @Test
+ fun `maybe create with non-activity context should return null`() {
+ val customDelegate = mock<SelectionActionDelegate>()
+
+ assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Application>(), customDelegate))
+ assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Service>(), customDelegate))
+ }
+
+ @Test
+ fun `maybe create with null delegate context should return null`() {
+ assertNull(GeckoSelectionActionDelegate.maybeCreate(mock<Activity>(), null))
+ }
+
+ @Test
+ fun `maybe create with expected inputs should return non-null`() {
+ assertNotNull(GeckoSelectionActionDelegate.maybeCreate(mock<Activity>(), mock()))
+ }
+
+ @Test
+ fun `getAllActions should contain all actions from the custom delegate`() {
+ val customActions = arrayOf("1", "2", "3")
+ val customDelegate = object : SelectionActionDelegate {
+ override fun getAllActions(): Array<String> = customActions
+ override fun isActionAvailable(id: String, selectedText: String): Boolean = false
+ override fun getActionTitle(id: String): CharSequence? = ""
+ override fun performAction(id: String, selectedText: String): Boolean = false
+ override fun sortedActions(actions: Array<String>): Array<String> {
+ return actions
+ }
+ }
+
+ val geckoDelegate = TestGeckoSelectionActionDelegate(mock(), customDelegate)
+
+ val actualActions = geckoDelegate.allActions
+
+ customActions.forEach {
+ Assert.assertTrue(actualActions.contains(it))
+ }
+ }
+
+ @Test
+ fun `WHEN perform action triggers a security exception THEN false is returned`() {
+ val customActions = arrayOf("1", "2", "3")
+ val customDelegate = object : SelectionActionDelegate {
+ override fun getAllActions(): Array<String> = customActions
+ override fun isActionAvailable(id: String, selectedText: String): Boolean = false
+ override fun getActionTitle(id: String): CharSequence? = ""
+ override fun performAction(id: String, selectedText: String): Boolean {
+ throw SecurityException("test")
+ }
+ override fun sortedActions(actions: Array<String>): Array<String> {
+ return actions
+ }
+ }
+
+ val geckoDelegate = TestGeckoSelectionActionDelegate(mock(), customDelegate)
+ assertFalse(geckoDelegate.performAction("test", mock()))
+ }
+}
+
+/**
+ * Test object that overrides visibility for [getAllActions]
+ */
+class TestGeckoSelectionActionDelegate(
+ activity: Activity,
+ customDelegate: SelectionActionDelegate,
+) : GeckoSelectionActionDelegate(activity, customDelegate) {
+ public override fun getAllActions() = super.getAllActions()
+ public override fun performAction(id: String, item: MenuItem): Boolean {
+ return super.performAction(id, item)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegateTest.kt
new file mode 100644
index 0000000000..8e4cbf2983
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegateTest.kt
@@ -0,0 +1,40 @@
+/* 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.browser.engine.gecko.serviceworker
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+
+@RunWith(AndroidJUnit4::class)
+class GeckoServiceWorkerDelegateTest() {
+ @Test
+ fun `GIVEN a delegate to add tabs WHEN it added a new tab for the request to open a new window THEN return a the new closed session`() {
+ val delegate = mock<ServiceWorkerDelegate>()
+ doReturn(true).`when`(delegate).addNewTab(any())
+ val geckoDelegate = GeckoServiceWorkerDelegate(delegate, mock(), mock())
+
+ val result = geckoDelegate.onOpenWindow("").poll(1)
+
+ assertFalse(result!!.isOpen)
+ }
+
+ @Test
+ fun `GIVEN a delegate to add tabs WHEN it disn't add a new tab for the request to open a new window THEN return null`() {
+ val delegate = mock<ServiceWorkerDelegate>()
+ doReturn(false).`when`(delegate).addNewTab(any())
+ val geckoDelegate = GeckoServiceWorkerDelegate(delegate, mock(), mock())
+
+ val result = geckoDelegate.onOpenWindow("").poll(1)
+
+ assertNull(result)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegateTest.kt
new file mode 100644
index 0000000000..65a1c7d8f9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegateTest.kt
@@ -0,0 +1,103 @@
+/* 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.browser.engine.gecko.translate
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import junit.framework.TestCase.assertTrue
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.TranslationsController
+
+@RunWith(AndroidJUnit4::class)
+class GeckoTranslateSessionDelegateTest {
+ private lateinit var runtime: GeckoRuntime
+ private lateinit var mockSession: GeckoEngineSession
+
+ @Before
+ fun setup() {
+ runtime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ mockSession = GeckoEngineSession(runtime)
+ }
+
+ @Test
+ fun `WHEN onExpectedTranslate is called THEN notify onTranslateExpected`() {
+ var onTranslateExpectedWasCalled = false
+ val gecko = GeckoTranslateSessionDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onTranslateExpected() {
+ onTranslateExpectedWasCalled = true
+ }
+ },
+ )
+
+ gecko.onExpectedTranslate(mock())
+
+ assertTrue(onTranslateExpectedWasCalled)
+ }
+
+ @Test
+ fun `WHEN onOfferTranslate is called THEN notify onTranslateOffer`() {
+ var onTranslateOfferWasCalled = false
+ val gecko = GeckoTranslateSessionDelegate(mockSession)
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onTranslateOffer() {
+ onTranslateOfferWasCalled = true
+ }
+ },
+ )
+
+ gecko.onOfferTranslate(mock())
+
+ assertTrue(onTranslateOfferWasCalled)
+ }
+
+ @Test
+ fun `WHEN onTranslationStateChange is called THEN notify onTranslateStateChange AND ensure mapped values are correct`() {
+ var onTranslateStateChangeWasCalled = false
+ val gecko = GeckoTranslateSessionDelegate(mockSession)
+
+ // Mock state parameters to check Gecko to AC mapping is correctly occurring
+ var userLangTag = "en"
+ var isDocLangTagSupported = true
+ var docLangTag = "es"
+ var fromLanguage = "de"
+ var toLanguage = "bg"
+ var error = "Error!"
+ var isEngineReady = false
+
+ mockSession.register(
+ object : EngineSession.Observer {
+ override fun onTranslateStateChange(state: TranslationEngineState) {
+ onTranslateStateChangeWasCalled = true
+ assertTrue(state.detectedLanguages?.userPreferredLangTag == userLangTag)
+ assertTrue(state.detectedLanguages?.supportedDocumentLang == isDocLangTagSupported)
+ assertTrue(state.detectedLanguages?.documentLangTag == docLangTag)
+ assertTrue(state.requestedTranslationPair?.fromLanguage == fromLanguage)
+ assertTrue(state.requestedTranslationPair?.toLanguage == toLanguage)
+ assertTrue(state.error == error)
+ assertTrue(state.isEngineReady == isEngineReady)
+ }
+ },
+ )
+
+ // Mock states
+ var mockDetectedLanguages = TranslationsController.SessionTranslation.DetectedLanguages(userLangTag, isDocLangTagSupported, docLangTag)
+ var mockTranslationsPair = TranslationsController.SessionTranslation.TranslationPair(fromLanguage, toLanguage)
+ var mockGeckoState = TranslationsController.SessionTranslation.TranslationState(mockTranslationsPair, error, mockDetectedLanguages, isEngineReady)
+ gecko.onTranslationStateChange(mock(), mockGeckoState)
+
+ assertTrue(onTranslateStateChangeWasCalled)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactoryTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactoryTest.kt
new file mode 100644
index 0000000000..e660a3761f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactoryTest.kt
@@ -0,0 +1,129 @@
+/* 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.browser.engine.gecko.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.GeckoRuntime
+
+@RunWith(AndroidJUnit4::class)
+class SpeculativeSessionFactoryTest {
+
+ private lateinit var runtime: GeckoRuntime
+
+ @Before
+ fun setup() {
+ runtime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ }
+
+ @Test
+ fun `create does nothing if matching speculative session already exists`() {
+ val factory = SpeculativeSessionFactory()
+ assertNull(factory.speculativeEngineSession)
+
+ factory.create(runtime = runtime, private = true, contextId = null, defaultSettings = mock())
+ val speculativeSession = factory.speculativeEngineSession
+ assertNotNull(speculativeSession)
+
+ factory.create(runtime = runtime, private = true, contextId = null, defaultSettings = mock())
+ assertSame(speculativeSession, factory.speculativeEngineSession)
+ }
+
+ @Test
+ fun `create clears previous non-matching speculative session`() {
+ val factory = SpeculativeSessionFactory()
+ assertNull(factory.speculativeEngineSession)
+
+ factory.create(runtime = runtime, private = true, contextId = null, defaultSettings = mock())
+ val speculativeSession = factory.speculativeEngineSession
+ assertNotNull(speculativeSession)
+
+ factory.create(runtime = runtime, private = false, contextId = null, defaultSettings = mock())
+ assertNotSame(speculativeSession, factory.speculativeEngineSession)
+ assertFalse(speculativeSession!!.engineSession.geckoSession.isOpen)
+ assertFalse(speculativeSession.engineSession.isObserved())
+ }
+
+ @Test
+ fun `get consumes matching speculative session`() {
+ val factory = SpeculativeSessionFactory()
+ assertFalse(factory.hasSpeculativeSession())
+
+ factory.create(runtime = runtime, private = true, contextId = null, defaultSettings = mock())
+ assertTrue(factory.hasSpeculativeSession())
+
+ val speculativeSession = factory.get(private = true, contextId = null)
+ assertNotNull(speculativeSession)
+ assertFalse(speculativeSession!!.isObserved())
+
+ assertFalse(factory.hasSpeculativeSession())
+ assertNull(factory.speculativeEngineSession)
+ }
+
+ @Test
+ fun `get clears previous non-matching speculative session`() {
+ val factory = SpeculativeSessionFactory()
+ assertNull(factory.speculativeEngineSession)
+
+ factory.create(runtime = runtime, private = true, contextId = null, defaultSettings = mock())
+ val speculativeSession = factory.speculativeEngineSession
+ assertNotNull(speculativeSession)
+
+ assertNull(factory.get(private = true, contextId = "test"))
+ assertFalse(speculativeSession!!.engineSession.geckoSession.isOpen)
+ assertFalse(speculativeSession.engineSession.isObserved())
+ }
+
+ @Test
+ fun `clears speculative session on crash`() {
+ val factory = SpeculativeSessionFactory()
+ factory.create(runtime = runtime, private = true, contextId = null, defaultSettings = mock())
+ assertTrue(factory.hasSpeculativeSession())
+ val speculativeSession = factory.speculativeEngineSession
+
+ factory.speculativeEngineSession?.engineSession?.notifyObservers { onCrash() }
+ assertFalse(factory.hasSpeculativeSession())
+ assertFalse(speculativeSession!!.engineSession.geckoSession.isOpen)
+ assertFalse(speculativeSession.engineSession.isObserved())
+ }
+
+ @Test
+ fun `clears speculative session when process is killed`() {
+ val factory = SpeculativeSessionFactory()
+ factory.create(runtime = runtime, private = true, contextId = null, defaultSettings = mock())
+ assertTrue(factory.hasSpeculativeSession())
+ val speculativeSession = factory.speculativeEngineSession
+
+ factory.speculativeEngineSession?.engineSession?.notifyObservers { onProcessKilled() }
+ assertFalse(factory.hasSpeculativeSession())
+ assertFalse(speculativeSession!!.engineSession.geckoSession.isOpen)
+ assertFalse(speculativeSession.engineSession.isObserved())
+ }
+
+ @Test
+ fun `clear unregisters observer and closes session`() {
+ val factory = SpeculativeSessionFactory()
+ factory.create(runtime = runtime, private = true, contextId = null, defaultSettings = mock())
+ assertTrue(factory.hasSpeculativeSession())
+ val speculativeSession = factory.speculativeEngineSession
+ assertTrue(speculativeSession!!.engineSession.isObserved())
+
+ factory.clear()
+ assertFalse(factory.hasSpeculativeSession())
+ assertFalse(speculativeSession.engineSession.geckoSession.isOpen)
+ assertFalse(speculativeSession.engineSession.isObserved())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt
new file mode 100644
index 0000000000..dd53da92bb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt
@@ -0,0 +1,644 @@
+/* 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.browser.engine.gecko.webextension
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.engine.webextension.ActionHandler
+import mozilla.components.concept.engine.webextension.DisabledFlags
+import mozilla.components.concept.engine.webextension.Incognito
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.concept.engine.webextension.TabHandler
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.Image
+import org.mozilla.geckoview.WebExtension
+import org.mozilla.geckoview.WebExtensionController
+
+@RunWith(AndroidJUnit4::class)
+class GeckoWebExtensionTest {
+
+ @Test
+ fun `register background message handler`() {
+ val runtime: GeckoRuntime = mock()
+ val nativeGeckoWebExt: WebExtension = mockNativeWebExtension()
+ val messageHandler: MessageHandler = mock()
+ val updatedMessageHandler: MessageHandler = mock()
+ val messageDelegateCaptor = argumentCaptor<WebExtension.MessageDelegate>()
+ val portCaptor = argumentCaptor<Port>()
+ val portDelegateCaptor = argumentCaptor<WebExtension.PortDelegate>()
+
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+
+ extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
+ verify(nativeGeckoWebExt).setMessageDelegate(messageDelegateCaptor.capture(), eq("mozacTest"))
+
+ // Verify messages are forwarded to message handler
+ val message: Any = mock()
+ val sender: WebExtension.MessageSender = mock()
+ whenever(messageHandler.onMessage(eq(message), eq(null))).thenReturn("result")
+ assertNotNull(messageDelegateCaptor.value.onMessage("mozacTest", message, sender))
+ verify(messageHandler).onMessage(eq(message), eq(null))
+
+ whenever(messageHandler.onMessage(eq(message), eq(null))).thenReturn(null)
+ assertNull(messageDelegateCaptor.value.onMessage("mozacTest", message, sender))
+ verify(messageHandler, times(2)).onMessage(eq(message), eq(null))
+
+ // Verify port is connected and forwarded to message handler
+ val port: WebExtension.Port = mock()
+ messageDelegateCaptor.value.onConnect(port)
+ verify(messageHandler).onPortConnected(portCaptor.capture())
+ assertSame(port, (portCaptor.value as GeckoPort).nativePort)
+ assertNotNull(extension.getConnectedPort("mozacTest"))
+ assertSame(port, (extension.getConnectedPort("mozacTest") as GeckoPort).nativePort)
+
+ // Verify port messages are forwarded to message handler
+ verify(port).setDelegate(portDelegateCaptor.capture())
+ val portDelegate = portDelegateCaptor.value
+ val portMessage: JSONObject = mock()
+ portDelegate.onPortMessage(portMessage, port)
+ verify(messageHandler).onPortMessage(eq(portMessage), portCaptor.capture())
+ assertSame(port, (portCaptor.value as GeckoPort).nativePort)
+
+ // Verify content message handler can be updated and receive messages
+ extension.registerBackgroundMessageHandler("mozacTest", updatedMessageHandler)
+ verify(port, times(2)).setDelegate(portDelegateCaptor.capture())
+ portDelegateCaptor.value.onPortMessage(portMessage, port)
+ verify(updatedMessageHandler).onPortMessage(eq(portMessage), portCaptor.capture())
+
+ // Verify disconnected port is forwarded to message handler if connected
+ portDelegate.onDisconnect(mock())
+ verify(messageHandler, never()).onPortDisconnected(portCaptor.capture())
+
+ portDelegate.onDisconnect(port)
+ verify(messageHandler).onPortDisconnected(portCaptor.capture())
+ assertSame(port, (portCaptor.value as GeckoPort).nativePort)
+ assertNull(extension.getConnectedPort("mozacTest"))
+ }
+
+ @Test
+ fun `register content message handler`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionSessionController: WebExtension.SessionController = mock()
+ val nativeGeckoWebExt: WebExtension = mockNativeWebExtension()
+ val messageHandler: MessageHandler = mock()
+ val updatedMessageHandler: MessageHandler = mock()
+ val session: GeckoEngineSession = mock()
+ val geckoSession: GeckoSession = mock()
+ val messageDelegateCaptor = argumentCaptor<WebExtension.MessageDelegate>()
+ val portCaptor = argumentCaptor<Port>()
+ val portDelegateCaptor = argumentCaptor<WebExtension.PortDelegate>()
+
+ whenever(geckoSession.webExtensionController).thenReturn(webExtensionSessionController)
+ whenever(session.geckoSession).thenReturn(geckoSession)
+
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ assertFalse(extension.hasContentMessageHandler(session, "mozacTest"))
+ extension.registerContentMessageHandler(session, "mozacTest", messageHandler)
+ verify(webExtensionSessionController).setMessageDelegate(eq(nativeGeckoWebExt), messageDelegateCaptor.capture(), eq("mozacTest"))
+
+ // Verify messages are forwarded to message handler and return value passed on
+ val message: Any = mock()
+ val sender: WebExtension.MessageSender = mock()
+ whenever(messageHandler.onMessage(eq(message), eq(session))).thenReturn("result")
+ assertNotNull(messageDelegateCaptor.value.onMessage("mozacTest", message, sender))
+ verify(messageHandler).onMessage(eq(message), eq(session))
+
+ whenever(messageHandler.onMessage(eq(message), eq(session))).thenReturn(null)
+ assertNull(messageDelegateCaptor.value.onMessage("mozacTest", message, sender))
+ verify(messageHandler, times(2)).onMessage(eq(message), eq(session))
+
+ // Verify port is connected and forwarded to message handler
+ val port: WebExtension.Port = mock()
+ messageDelegateCaptor.value.onConnect(port)
+ verify(messageHandler).onPortConnected(portCaptor.capture())
+ assertSame(port, (portCaptor.value as GeckoPort).nativePort)
+ assertSame(session, (portCaptor.value as GeckoPort).engineSession)
+ assertNotNull(extension.getConnectedPort("mozacTest", session))
+ assertSame(port, (extension.getConnectedPort("mozacTest", session) as GeckoPort).nativePort)
+
+ // Verify port messages are forwarded to message handler
+ verify(port).setDelegate(portDelegateCaptor.capture())
+ val portDelegate = portDelegateCaptor.value
+ val portMessage: JSONObject = mock()
+ portDelegate.onPortMessage(portMessage, port)
+ verify(messageHandler).onPortMessage(eq(portMessage), portCaptor.capture())
+ assertSame(port, (portCaptor.value as GeckoPort).nativePort)
+ assertSame(session, (portCaptor.value as GeckoPort).engineSession)
+
+ // Verify content message handler can be updated and receive messages
+ extension.registerContentMessageHandler(session, "mozacTest", updatedMessageHandler)
+ verify(port, times(2)).setDelegate(portDelegateCaptor.capture())
+ portDelegateCaptor.value.onPortMessage(portMessage, port)
+ verify(updatedMessageHandler).onPortMessage(eq(portMessage), portCaptor.capture())
+
+ // Verify disconnected port is forwarded to message handler if connected
+ portDelegate.onDisconnect(mock())
+ verify(messageHandler, never()).onPortDisconnected(portCaptor.capture())
+
+ portDelegate.onDisconnect(port)
+ verify(messageHandler).onPortDisconnected(portCaptor.capture())
+ assertSame(port, (portCaptor.value as GeckoPort).nativePort)
+ assertSame(session, (portCaptor.value as GeckoPort).engineSession)
+ assertNull(extension.getConnectedPort("mozacTest", session))
+ }
+
+ @Test
+ fun `disconnect port from content script`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionSessionController: WebExtension.SessionController = mock()
+ val nativeGeckoWebExt: WebExtension = mockNativeWebExtension()
+ val messageHandler: MessageHandler = mock()
+ val session: GeckoEngineSession = mock()
+ val geckoSession: GeckoSession = mock()
+ val messageDelegateCaptor = argumentCaptor<WebExtension.MessageDelegate>()
+
+ whenever(geckoSession.webExtensionController).thenReturn(webExtensionSessionController)
+ whenever(session.geckoSession).thenReturn(geckoSession)
+
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ extension.registerContentMessageHandler(session, "mozacTest", messageHandler)
+ verify(webExtensionSessionController).setMessageDelegate(eq(nativeGeckoWebExt), messageDelegateCaptor.capture(), eq("mozacTest"))
+
+ // Connect port
+ val port: WebExtension.Port = mock()
+ messageDelegateCaptor.value.onConnect(port)
+ assertNotNull(extension.getConnectedPort("mozacTest", session))
+
+ // Disconnect port
+ extension.disconnectPort("mozacTest", session)
+ verify(port).disconnect()
+ assertNull(extension.getConnectedPort("mozacTest", session))
+ }
+
+ @Test
+ fun `disconnect port from background script`() {
+ val runtime: GeckoRuntime = mock()
+ val nativeGeckoWebExt: WebExtension = mockNativeWebExtension()
+ val messageHandler: MessageHandler = mock()
+ val messageDelegateCaptor = argumentCaptor<WebExtension.MessageDelegate>()
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
+
+ verify(nativeGeckoWebExt).setMessageDelegate(messageDelegateCaptor.capture(), eq("mozacTest"))
+
+ // Connect port
+ val port: WebExtension.Port = mock()
+ messageDelegateCaptor.value.onConnect(port)
+ assertNotNull(extension.getConnectedPort("mozacTest"))
+
+ // Disconnect port
+ extension.disconnectPort("mozacTest")
+ verify(port).disconnect()
+ assertNull(extension.getConnectedPort("mozacTest"))
+ }
+
+ @Test
+ fun `register global default action handler`() {
+ val runtime: GeckoRuntime = mock()
+ val nativeGeckoWebExt: WebExtension = mockNativeWebExtension()
+ val actionHandler: ActionHandler = mock()
+ val actionDelegateCaptor = argumentCaptor<WebExtension.ActionDelegate>()
+ val browserActionCaptor = argumentCaptor<Action>()
+ val pageActionCaptor = argumentCaptor<Action>()
+ val nativeBrowserAction: WebExtension.Action = mock()
+ val nativePageAction: WebExtension.Action = mock()
+
+ // Create extension and register global default action handler
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ extension.registerActionHandler(actionHandler)
+ verify(nativeGeckoWebExt).setActionDelegate(actionDelegateCaptor.capture())
+
+ // Verify that browser actions are forwarded to the handler
+ actionDelegateCaptor.value.onBrowserAction(nativeGeckoWebExt, null, nativeBrowserAction)
+ verify(actionHandler).onBrowserAction(eq(extension), eq(null), browserActionCaptor.capture())
+
+ // Verify that page actions are forwarded to the handler
+ actionDelegateCaptor.value.onPageAction(nativeGeckoWebExt, null, nativePageAction)
+ verify(actionHandler).onPageAction(eq(extension), eq(null), pageActionCaptor.capture())
+
+ // Verify that toggle popup is forwarded to the handler
+ actionDelegateCaptor.value.onTogglePopup(nativeGeckoWebExt, nativeBrowserAction)
+ verify(actionHandler).onToggleActionPopup(eq(extension), any())
+
+ // We don't have access to the native WebExtension.Action fields and
+ // can't mock them either, but we can verify that we've linked
+ // the actions by simulating a click.
+ browserActionCaptor.value.onClick()
+ verify(nativeBrowserAction).click()
+ pageActionCaptor.value.onClick()
+ verify(nativePageAction).click()
+ }
+
+ @Test
+ fun `register session-specific action handler`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionSessionController: WebExtension.SessionController = mock()
+ val session: GeckoEngineSession = mock()
+ val geckoSession: GeckoSession = mock()
+ whenever(geckoSession.webExtensionController).thenReturn(webExtensionSessionController)
+ whenever(session.geckoSession).thenReturn(geckoSession)
+
+ val nativeGeckoWebExt: WebExtension = mockNativeWebExtension()
+ val actionHandler: ActionHandler = mock()
+ val actionDelegateCaptor = argumentCaptor<WebExtension.ActionDelegate>()
+ val browserActionCaptor = argumentCaptor<Action>()
+ val pageActionCaptor = argumentCaptor<Action>()
+ val nativeBrowserAction: WebExtension.Action = mock()
+ val nativePageAction: WebExtension.Action = mock()
+
+ // Create extension and register action handler for session
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ extension.registerActionHandler(session, actionHandler)
+ verify(webExtensionSessionController).setActionDelegate(eq(nativeGeckoWebExt), actionDelegateCaptor.capture())
+
+ whenever(webExtensionSessionController.getActionDelegate(nativeGeckoWebExt)).thenReturn(actionDelegateCaptor.value)
+ assertTrue(extension.hasActionHandler(session))
+
+ // Verify that browser actions are forwarded to the handler
+ actionDelegateCaptor.value.onBrowserAction(nativeGeckoWebExt, null, nativeBrowserAction)
+ verify(actionHandler).onBrowserAction(eq(extension), eq(session), browserActionCaptor.capture())
+
+ // Verify that page actions are forwarded to the handler
+ actionDelegateCaptor.value.onPageAction(nativeGeckoWebExt, null, nativePageAction)
+ verify(actionHandler).onPageAction(eq(extension), eq(session), pageActionCaptor.capture())
+
+ // We don't have access to the native WebExtension.Action fields and
+ // can't mock them either, but we can verify that we've linked
+ // the actions by simulating a click.
+ browserActionCaptor.value.onClick()
+ verify(nativeBrowserAction).click()
+ pageActionCaptor.value.onClick()
+ verify(nativePageAction).click()
+ }
+
+ @Test
+ fun `register global tab handler`() {
+ val runtime: GeckoRuntime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ whenever(runtime.webExtensionController).thenReturn(mock())
+ val tabHandler: TabHandler = mock()
+ val tabDelegateCaptor = argumentCaptor<WebExtension.TabDelegate>()
+ val engineSessionCaptor = argumentCaptor<GeckoEngineSession>()
+
+ val nativeGeckoWebExt: WebExtension =
+ mockNativeWebExtension(id = "id", location = "uri", metaData = mockNativeWebExtensionMetaData())
+
+ // Create extension and register global tab handler
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ val defaultSettings: DefaultSettings = mock()
+
+ extension.registerTabHandler(tabHandler, defaultSettings)
+ verify(nativeGeckoWebExt).tabDelegate = tabDelegateCaptor.capture()
+
+ // Verify that tab methods are forwarded to the handler
+ val tabDetails = mockCreateTabDetails(active = true, url = "url")
+ tabDelegateCaptor.value.onNewTab(nativeGeckoWebExt, tabDetails)
+ verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"))
+ assertNotNull(engineSessionCaptor.value)
+
+ tabDelegateCaptor.value.onOpenOptionsPage(nativeGeckoWebExt)
+ verify(tabHandler, never()).onNewTab(eq(extension), any(), eq(false), eq("http://options-page.moz"))
+
+ val nativeGeckoWebExtWithMetadata =
+ mockNativeWebExtension(id = "id", location = "uri", metaData = mockNativeWebExtensionMetaData())
+ tabDelegateCaptor.value.onOpenOptionsPage(nativeGeckoWebExtWithMetadata)
+ verify(tabHandler, never()).onNewTab(eq(extension), any(), eq(false), eq("http://options-page.moz"))
+
+ val nativeGeckoWebExtWithOptionsPageUrl = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(optionsPageUrl = "http://options-page.moz"),
+ )
+ tabDelegateCaptor.value.onOpenOptionsPage(nativeGeckoWebExtWithOptionsPageUrl)
+ verify(tabHandler).onNewTab(eq(extension), any(), eq(false), eq("http://options-page.moz"))
+ }
+
+ @Test
+ fun `register session-specific tab handler`() {
+ val runtime: GeckoRuntime = mock()
+ whenever(runtime.webExtensionController).thenReturn(mock())
+ val webExtensionSessionController: WebExtension.SessionController = mock()
+ val session: GeckoEngineSession = mock()
+ val geckoSession: GeckoSession = mock()
+ whenever(geckoSession.webExtensionController).thenReturn(webExtensionSessionController)
+ whenever(session.geckoSession).thenReturn(geckoSession)
+
+ val tabHandler: TabHandler = mock()
+ val tabDelegateCaptor = argumentCaptor<WebExtension.SessionTabDelegate>()
+
+ val nativeGeckoWebExt: WebExtension = mockNativeWebExtension()
+ // Create extension and register tab handler for session
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ extension.registerTabHandler(session, tabHandler)
+ verify(webExtensionSessionController).setTabDelegate(eq(nativeGeckoWebExt), tabDelegateCaptor.capture())
+
+ assertFalse(extension.hasTabHandler(session))
+ whenever(webExtensionSessionController.getTabDelegate(nativeGeckoWebExt)).thenReturn(tabDelegateCaptor.value)
+ assertTrue(extension.hasTabHandler(session))
+
+ // Verify that tab methods are forwarded to the handler
+ val tabDetails = mockUpdateTabDetails(active = true)
+ tabDelegateCaptor.value.onUpdateTab(nativeGeckoWebExt, mock(), tabDetails)
+ verify(tabHandler).onUpdateTab(eq(extension), eq(session), eq(true), eq(null))
+
+ tabDelegateCaptor.value.onCloseTab(nativeGeckoWebExt, mock())
+ verify(tabHandler).onCloseTab(eq(extension), eq(session))
+ }
+
+ @Test
+ fun `all metadata fields are mapped correctly`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val nativeWebExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(
+ origins = arrayOf("o1", "o2"),
+ description = "desc",
+ version = "1.0",
+ creatorName = "developer1",
+ creatorUrl = "https://developer1.dev",
+ homepageUrl = "https://mozilla.org",
+ name = "myextension",
+ optionsPageUrl = "http://options-page.moz",
+ baseUrl = "moz-extension://123c5c5b-cd03-4bea-b23f-ac0b9ab40257/",
+ openOptionsPageInTab = false,
+ disabledFlags = DisabledFlags.USER,
+ temporary = true,
+ permissions = arrayOf("p1", "p2"),
+ optionalPermissions = arrayOf("clipboardRead"),
+ grantedOptionalPermissions = arrayOf("clipboardRead"),
+ optionalOrigins = arrayOf("*://*.example.com/*", "*://opt-host-perm.example.com/*"),
+ grantedOptionalOrigins = arrayOf("*://*.example.com/*"),
+ fullDescription = "fullDescription",
+ downloadUrl = "downloadUrl",
+ reviewUrl = "reviewUrl",
+ updateDate = "updateDate",
+ reviewCount = 2,
+ averageRating = 2.2,
+ incognito = "split",
+ ),
+ )
+ val extensionWithMetadata = GeckoWebExtension(nativeWebExtension, runtime)
+ val metadata = extensionWithMetadata.getMetadata()
+ assertNotNull(metadata)
+
+ assertEquals("1.0", metadata.version)
+ assertEquals(listOf("clipboardRead"), metadata.optionalPermissions)
+ assertEquals(listOf("clipboardRead"), metadata.grantedOptionalPermissions)
+ assertEquals(listOf("*://*.example.com/*", "*://opt-host-perm.example.com/*"), metadata.optionalOrigins)
+ assertEquals(listOf("*://*.example.com/*"), metadata.grantedOptionalOrigins)
+ assertEquals(listOf("p1", "p2"), metadata.permissions)
+ assertEquals(listOf("o1", "o2"), metadata.hostPermissions)
+ assertEquals("desc", metadata.description)
+ assertEquals("developer1", metadata.developerName)
+ assertEquals("https://developer1.dev", metadata.developerUrl)
+ assertEquals("https://mozilla.org", metadata.homepageUrl)
+ assertEquals("myextension", metadata.name)
+ assertEquals("http://options-page.moz", metadata.optionsPageUrl)
+ assertEquals("moz-extension://123c5c5b-cd03-4bea-b23f-ac0b9ab40257/", metadata.baseUrl)
+ assertEquals("fullDescription", metadata.fullDescription)
+ assertEquals("downloadUrl", metadata.downloadUrl)
+ assertEquals("reviewUrl", metadata.reviewUrl)
+ assertEquals("updateDate", metadata.updateDate)
+ assertEquals(2, metadata.reviewCount)
+ assertEquals(2.2f, metadata.averageRating)
+ assertFalse(metadata.openOptionsPageInTab)
+ assertTrue(metadata.temporary)
+ assertTrue(metadata.disabledFlags.contains(DisabledFlags.USER))
+ assertFalse(metadata.disabledFlags.contains(DisabledFlags.BLOCKLIST))
+ assertFalse(metadata.disabledFlags.contains(DisabledFlags.APP_SUPPORT))
+ assertEquals(Incognito.SPLIT, metadata.incognito)
+ }
+
+ @Test
+ fun `nullable metadata fields`() {
+ val runtime: GeckoRuntime = mock()
+ val webExtensionController: WebExtensionController = mock()
+ whenever(runtime.webExtensionController).thenReturn(webExtensionController)
+
+ val nativeWebExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(
+ version = "1.0",
+ baseUrl = "moz-extension://123c5c5b-cd03-4bea-b23f-ac0b9ab40257/",
+ disabledFlags = DisabledFlags.USER,
+ permissions = arrayOf("p1", "p2"),
+ incognito = null,
+ ),
+ )
+ val extensionWithMetadata = GeckoWebExtension(nativeWebExtension, runtime)
+ val metadata = extensionWithMetadata.getMetadata()
+ assertNotNull(metadata)
+ assertEquals("1.0", metadata.version)
+ assertEquals(0.0f, metadata.averageRating)
+ assertEquals(0, metadata.reviewCount)
+ assertEquals(listOf("p1", "p2"), metadata.permissions)
+ assertEquals(emptyList<String>(), metadata.hostPermissions)
+ assertEquals("moz-extension://123c5c5b-cd03-4bea-b23f-ac0b9ab40257/", metadata.baseUrl)
+ assertNull(metadata.description)
+ assertNull(metadata.developerName)
+ assertNull(metadata.developerUrl)
+ assertNull(metadata.homepageUrl)
+ assertNull(metadata.name)
+ assertNull(metadata.optionsPageUrl)
+ assertNull(metadata.fullDescription)
+ assertNull(metadata.reviewUrl)
+ assertNull(metadata.updateDate)
+ assertNull(metadata.downloadUrl)
+ assertEquals(Incognito.SPANNING, metadata.incognito)
+ }
+
+ @Test
+ fun `isBuiltIn depends on native state`() {
+ val runtime: GeckoRuntime = mock()
+
+ val builtInExtension = GeckoWebExtension(
+ mockNativeWebExtension(id = "id", location = "uri", isBuiltIn = true),
+ runtime,
+ )
+ assertTrue(builtInExtension.isBuiltIn())
+
+ val externalExtension = GeckoWebExtension(
+ mockNativeWebExtension(id = "id", location = "uri", isBuiltIn = false),
+ runtime,
+ )
+ assertFalse(externalExtension.isBuiltIn())
+ }
+
+ @Test
+ fun `isEnabled depends on native state and defaults to true if state unknown`() {
+ val runtime: GeckoRuntime = mock()
+ whenever(runtime.webExtensionController).thenReturn(mock())
+
+ val nativeEnabledWebExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(
+ enabled = true,
+ ),
+ )
+ val enabledWebExtension = GeckoWebExtension(nativeEnabledWebExtension, runtime)
+ assertTrue(enabledWebExtension.isEnabled())
+
+ val nativeDisabledWebExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(
+ enabled = false,
+ ),
+ )
+ val disabledWebExtension = GeckoWebExtension(nativeDisabledWebExtension, runtime)
+ assertFalse(disabledWebExtension.isEnabled())
+ }
+
+ @Test
+ fun `isAllowedInPrivateBrowsing depends on native state and defaults to false if state unknown`() {
+ val runtime: GeckoRuntime = mock()
+ whenever(runtime.webExtensionController).thenReturn(mock())
+
+ val nativeBuiltInExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ isBuiltIn = true,
+ metaData = mockNativeWebExtensionMetaData(
+ allowedInPrivateBrowsing = false,
+ ),
+ )
+ val builtInExtension = GeckoWebExtension(nativeBuiltInExtension, runtime)
+ assertTrue(builtInExtension.isAllowedInPrivateBrowsing())
+
+ val nativeWebExtensionWithPrivateBrowsing = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(
+ allowedInPrivateBrowsing = true,
+ ),
+ )
+ val webExtensionWithPrivateBrowsing = GeckoWebExtension(nativeWebExtensionWithPrivateBrowsing, runtime)
+ assertTrue(webExtensionWithPrivateBrowsing.isAllowedInPrivateBrowsing())
+
+ val nativeWebExtensionWithoutPrivateBrowsing = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(
+ allowedInPrivateBrowsing = false,
+ ),
+ )
+ val webExtensionWithoutPrivateBrowsing = GeckoWebExtension(nativeWebExtensionWithoutPrivateBrowsing, runtime)
+ assertFalse(webExtensionWithoutPrivateBrowsing.isAllowedInPrivateBrowsing())
+ }
+
+ @Test
+ fun `loadIcon tries to load icon from metadata`() {
+ val runtime: GeckoRuntime = mock()
+ whenever(runtime.webExtensionController).thenReturn(mock())
+
+ val iconMock: Image = mock()
+ whenever(iconMock.getBitmap(48)).thenReturn(mock())
+ val nativeWebExtensionWithIcon = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(icon = iconMock),
+ )
+
+ val webExtensionWithIcon = GeckoWebExtension(nativeWebExtensionWithIcon, runtime)
+ webExtensionWithIcon.getIcon(48)
+ verify(iconMock).getBitmap(48)
+ }
+
+ @Test
+ fun `incognito set to spanning`() {
+ val runtime: GeckoRuntime = mock()
+ val nativeWebExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(version = "1", incognito = "spanning"),
+ )
+ val extensionWithMetadata = GeckoWebExtension(nativeWebExtension, runtime)
+
+ val metadata = extensionWithMetadata.getMetadata()
+ assertNotNull(metadata)
+ assertEquals(Incognito.SPANNING, metadata.incognito)
+ }
+
+ @Test
+ fun `incognito set to not_allowed`() {
+ val runtime: GeckoRuntime = mock()
+ val nativeWebExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(version = "1", incognito = "not_allowed"),
+ )
+ val extensionWithMetadata = GeckoWebExtension(nativeWebExtension, runtime)
+
+ val metadata = extensionWithMetadata.getMetadata()
+ assertNotNull(metadata)
+ assertEquals(Incognito.NOT_ALLOWED, metadata.incognito)
+ }
+
+ @Test
+ fun `incognito set to split`() {
+ val runtime: GeckoRuntime = mock()
+ val nativeWebExtension = mockNativeWebExtension(
+ id = "id",
+ location = "uri",
+ metaData = mockNativeWebExtensionMetaData(version = "1", incognito = "split"),
+ )
+ val extensionWithMetadata = GeckoWebExtension(nativeWebExtension, runtime)
+
+ val metadata = extensionWithMetadata.getMetadata()
+ assertNotNull(metadata)
+ assertEquals(Incognito.SPLIT, metadata.incognito)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/MockWebExtension.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/MockWebExtension.kt
new file mode 100644
index 0000000000..c90be13bc3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/MockWebExtension.kt
@@ -0,0 +1,115 @@
+/* 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.browser.engine.gecko.webextension
+
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.test.ReflectionUtils
+import org.mockito.Mockito.doNothing
+import org.mozilla.geckoview.Image
+import org.mozilla.geckoview.WebExtension
+
+fun mockNativeWebExtension(
+ id: String = "id",
+ location: String = "uri",
+ flags: Int = 0,
+ isBuiltIn: Boolean = false,
+ metaData: WebExtension.MetaData? = null,
+): WebExtension {
+ val extension: WebExtension = mock()
+ ReflectionUtils.setField(extension, "id", id)
+ ReflectionUtils.setField(extension, "location", location)
+ ReflectionUtils.setField(extension, "flags", flags)
+ ReflectionUtils.setField(extension, "isBuiltIn", isBuiltIn)
+ ReflectionUtils.setField(extension, "metaData", metaData)
+
+ doNothing().`when`(extension).setActionDelegate(any())
+ return extension
+}
+
+fun mockNativeWebExtensionMetaData(
+ icon: Image = mock(),
+ permissions: Array<String> = emptyArray(),
+ optionalPermissions: Array<String> = emptyArray(),
+ grantedOptionalPermissions: Array<String> = emptyArray(),
+ grantedOptionalOrigins: Array<String> = emptyArray(),
+ optionalOrigins: Array<String> = emptyArray(),
+ origins: Array<String> = emptyArray(),
+ name: String? = null,
+ description: String? = null,
+ version: String? = null,
+ creatorName: String? = null,
+ creatorUrl: String? = null,
+ homepageUrl: String? = null,
+ optionsPageUrl: String? = null,
+ openOptionsPageInTab: Boolean = false,
+ isRecommended: Boolean = false,
+ blocklistState: Int = 0,
+ signedState: Int = 0,
+ disabledFlags: Int = 0,
+ baseUrl: String = "",
+ allowedInPrivateBrowsing: Boolean = false,
+ enabled: Boolean = false,
+ temporary: Boolean = false,
+ fullDescription: String? = null,
+ downloadUrl: String? = null,
+ reviewUrl: String? = null,
+ updateDate: String? = null,
+ reviewCount: Int = 0,
+ averageRating: Double = 0.0,
+ incognito: String? = "spanning",
+): WebExtension.MetaData {
+ val metadata: WebExtension.MetaData = mock()
+ ReflectionUtils.setField(metadata, "icon", icon)
+ ReflectionUtils.setField(metadata, "promptPermissions", permissions)
+ ReflectionUtils.setField(metadata, "optionalPermissions", optionalPermissions)
+ ReflectionUtils.setField(metadata, "grantedOptionalPermissions", grantedOptionalPermissions)
+ ReflectionUtils.setField(metadata, "optionalOrigins", optionalOrigins)
+ ReflectionUtils.setField(metadata, "grantedOptionalOrigins", grantedOptionalOrigins)
+ ReflectionUtils.setField(metadata, "origins", origins)
+ ReflectionUtils.setField(metadata, "name", name)
+ ReflectionUtils.setField(metadata, "description", description)
+ ReflectionUtils.setField(metadata, "version", version)
+ ReflectionUtils.setField(metadata, "creatorName", creatorName)
+ ReflectionUtils.setField(metadata, "creatorUrl", creatorUrl)
+ ReflectionUtils.setField(metadata, "homepageUrl", homepageUrl)
+ ReflectionUtils.setField(metadata, "optionsPageUrl", optionsPageUrl)
+ ReflectionUtils.setField(metadata, "openOptionsPageInTab", openOptionsPageInTab)
+ ReflectionUtils.setField(metadata, "isRecommended", isRecommended)
+ ReflectionUtils.setField(metadata, "blocklistState", blocklistState)
+ ReflectionUtils.setField(metadata, "signedState", signedState)
+ ReflectionUtils.setField(metadata, "disabledFlags", disabledFlags)
+ ReflectionUtils.setField(metadata, "baseUrl", baseUrl)
+ ReflectionUtils.setField(metadata, "allowedInPrivateBrowsing", allowedInPrivateBrowsing)
+ ReflectionUtils.setField(metadata, "enabled", enabled)
+ ReflectionUtils.setField(metadata, "temporary", temporary)
+ ReflectionUtils.setField(metadata, "fullDescription", fullDescription)
+ ReflectionUtils.setField(metadata, "downloadUrl", downloadUrl)
+ ReflectionUtils.setField(metadata, "reviewUrl", reviewUrl)
+ ReflectionUtils.setField(metadata, "updateDate", updateDate)
+ ReflectionUtils.setField(metadata, "reviewCount", reviewCount)
+ ReflectionUtils.setField(metadata, "averageRating", averageRating)
+ ReflectionUtils.setField(metadata, "averageRating", averageRating)
+ ReflectionUtils.setField(metadata, "incognito", incognito)
+ return metadata
+}
+
+fun mockCreateTabDetails(
+ active: Boolean,
+ url: String,
+): WebExtension.CreateTabDetails {
+ val createTabDetails: WebExtension.CreateTabDetails = mock()
+ ReflectionUtils.setField(createTabDetails, "active", active)
+ ReflectionUtils.setField(createTabDetails, "url", url)
+ return createTabDetails
+}
+
+fun mockUpdateTabDetails(
+ active: Boolean,
+): WebExtension.UpdateTabDetails {
+ val updateTabDetails: WebExtension.UpdateTabDetails = mock()
+ ReflectionUtils.setField(updateTabDetails, "active", active)
+ return updateTabDetails
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegateTest.kt
new file mode 100644
index 0000000000..6984a4cf03
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegateTest.kt
@@ -0,0 +1,117 @@
+/* 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.browser.engine.gecko.webnotifications
+
+import mozilla.components.concept.engine.webnotifications.WebNotification
+import mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.WebNotification as GeckoViewWebNotification
+
+class GeckoWebNotificationDelegateTest {
+
+ @Test
+ fun `onShowNotification is forwarded to delegate`() {
+ val webNotificationDelegate: WebNotificationDelegate = mock()
+ val geckoViewWebNotification: GeckoViewWebNotification = mockWebNotification(
+ title = "title",
+ tag = "tag",
+ text = "text",
+ imageUrl = "imageUrl",
+ textDirection = "textDirection",
+ lang = "lang",
+ requireInteraction = true,
+ source = "source",
+ privateBrowsing = true,
+ )
+ val geckoWebNotificationDelegate = GeckoWebNotificationDelegate(webNotificationDelegate)
+
+ val notificationCaptor = argumentCaptor<WebNotification>()
+ geckoWebNotificationDelegate.onShowNotification(geckoViewWebNotification)
+ verify(webNotificationDelegate).onShowNotification(notificationCaptor.capture())
+
+ val notification = notificationCaptor.value
+ assertEquals(notification.title, geckoViewWebNotification.title)
+ assertEquals(notification.tag, geckoViewWebNotification.tag)
+ assertEquals(notification.body, geckoViewWebNotification.text)
+ assertEquals(notification.sourceUrl, geckoViewWebNotification.source)
+ assertEquals(notification.iconUrl, geckoViewWebNotification.imageUrl)
+ assertEquals(notification.direction, geckoViewWebNotification.textDirection)
+ assertEquals(notification.lang, geckoViewWebNotification.lang)
+ assertEquals(notification.requireInteraction, geckoViewWebNotification.requireInteraction)
+ assertFalse(notification.triggeredByWebExtension)
+ assertTrue(notification.privateBrowsing)
+ }
+
+ @Test
+ fun `onCloseNotification is forwarded to delegate`() {
+ val webNotificationDelegate: WebNotificationDelegate = mock()
+ val geckoViewWebNotification: GeckoViewWebNotification = mockWebNotification(
+ title = "title",
+ tag = "tag",
+ text = "text",
+ imageUrl = "imageUrl",
+ textDirection = "textDirection",
+ lang = "lang",
+ requireInteraction = true,
+ source = "source",
+ privateBrowsing = false,
+ )
+ val geckoWebNotificationDelegate = GeckoWebNotificationDelegate(webNotificationDelegate)
+
+ val notificationCaptor = argumentCaptor<WebNotification>()
+ geckoWebNotificationDelegate.onCloseNotification(geckoViewWebNotification)
+ verify(webNotificationDelegate).onCloseNotification(notificationCaptor.capture())
+
+ val notification = notificationCaptor.value
+ assertEquals(notification.title, geckoViewWebNotification.title)
+ assertEquals(notification.tag, geckoViewWebNotification.tag)
+ assertEquals(notification.body, geckoViewWebNotification.text)
+ assertEquals(notification.sourceUrl, geckoViewWebNotification.source)
+ assertEquals(notification.iconUrl, geckoViewWebNotification.imageUrl)
+ assertEquals(notification.direction, geckoViewWebNotification.textDirection)
+ assertEquals(notification.lang, geckoViewWebNotification.lang)
+ assertEquals(notification.requireInteraction, geckoViewWebNotification.requireInteraction)
+ assertEquals(notification.privateBrowsing, geckoViewWebNotification.privateBrowsing)
+ }
+
+ @Test
+ fun `notification without a source are from web extensions`() {
+ val webNotificationDelegate: WebNotificationDelegate = mock()
+ val geckoViewWebNotification: GeckoViewWebNotification = mockWebNotification(
+ title = "title",
+ tag = "tag",
+ text = "text",
+ imageUrl = "imageUrl",
+ textDirection = "textDirection",
+ lang = "lang",
+ requireInteraction = true,
+ source = null,
+ privateBrowsing = true,
+ )
+ val geckoWebNotificationDelegate = GeckoWebNotificationDelegate(webNotificationDelegate)
+
+ val notificationCaptor = argumentCaptor<WebNotification>()
+ geckoWebNotificationDelegate.onShowNotification(geckoViewWebNotification)
+ verify(webNotificationDelegate).onShowNotification(notificationCaptor.capture())
+
+ val notification = notificationCaptor.value
+ assertEquals(notification.title, geckoViewWebNotification.title)
+ assertEquals(notification.tag, geckoViewWebNotification.tag)
+ assertEquals(notification.body, geckoViewWebNotification.text)
+ assertEquals(notification.sourceUrl, geckoViewWebNotification.source)
+ assertEquals(notification.iconUrl, geckoViewWebNotification.imageUrl)
+ assertEquals(notification.direction, geckoViewWebNotification.textDirection)
+ assertEquals(notification.lang, geckoViewWebNotification.lang)
+ assertEquals(notification.requireInteraction, geckoViewWebNotification.requireInteraction)
+ assertTrue(notification.triggeredByWebExtension)
+ assertTrue(notification.privateBrowsing)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/MockWebNotification.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/MockWebNotification.kt
new file mode 100644
index 0000000000..247bf220b2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/MockWebNotification.kt
@@ -0,0 +1,37 @@
+/* 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.browser.engine.gecko.webnotifications
+
+import mozilla.components.support.test.mock
+import mozilla.components.test.ReflectionUtils
+import org.mozilla.geckoview.WebNotification
+
+fun mockWebNotification(
+ tag: String,
+ requireInteraction: Boolean,
+ vibrate: IntArray = IntArray(0),
+ title: String? = null,
+ text: String? = null,
+ imageUrl: String? = null,
+ textDirection: String? = null,
+ lang: String? = null,
+ source: String? = null,
+ silent: Boolean = false,
+ privateBrowsing: Boolean = false,
+): WebNotification {
+ val webNotification: WebNotification = mock()
+ ReflectionUtils.setField(webNotification, "title", title)
+ ReflectionUtils.setField(webNotification, "tag", tag)
+ ReflectionUtils.setField(webNotification, "text", text)
+ ReflectionUtils.setField(webNotification, "imageUrl", imageUrl)
+ ReflectionUtils.setField(webNotification, "textDirection", textDirection)
+ ReflectionUtils.setField(webNotification, "lang", lang)
+ ReflectionUtils.setField(webNotification, "requireInteraction", requireInteraction)
+ ReflectionUtils.setField(webNotification, "source", source)
+ ReflectionUtils.setField(webNotification, "silent", silent)
+ ReflectionUtils.setField(webNotification, "vibrate", vibrate)
+ ReflectionUtils.setField(webNotification, "privateBrowsing", privateBrowsing)
+ return webNotification
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegateTest.kt
new file mode 100644
index 0000000000..db50ceb559
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegateTest.kt
@@ -0,0 +1,154 @@
+/* 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.browser.engine.gecko.webpush
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.webpush.WebPushDelegate
+import mozilla.components.concept.engine.webpush.WebPushSubscription
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoResult
+
+@RunWith(AndroidJUnit4::class)
+class GeckoWebPushDelegateTest {
+
+ @Test
+ fun `delegate is always invoked`() {
+ val delegate: WebPushDelegate = mock()
+ val geckoDelegate = GeckoWebPushDelegate(delegate)
+
+ geckoDelegate.onGetSubscription("test")
+
+ verify(delegate).onGetSubscription(eq("test"), any())
+
+ geckoDelegate.onSubscribe("test", null)
+
+ verify(delegate).onSubscribe(eq("test"), isNull(), any())
+
+ geckoDelegate.onSubscribe("test", "key".toByteArray())
+
+ verify(delegate).onSubscribe(eq("test"), eq("key".toByteArray()), any())
+
+ geckoDelegate.onUnsubscribe("test")
+
+ verify(delegate).onUnsubscribe(eq("test"), any())
+ }
+
+ @Test
+ fun `onGetSubscription result is completed`() {
+ var subscription: WebPushSubscription? = WebPushSubscription(
+ "test",
+ "https://example.com",
+ null,
+ ByteArray(65),
+ ByteArray(16),
+ )
+ val delegate: WebPushDelegate = object : WebPushDelegate {
+ override fun onGetSubscription(
+ scope: String,
+ onSubscription: (WebPushSubscription?) -> Unit,
+ ) {
+ onSubscription(subscription)
+ }
+ }
+
+ val geckoDelegate = GeckoWebPushDelegate(delegate)
+ val result = geckoDelegate.onGetSubscription("test")
+
+ result?.accept { sub ->
+ assert(sub!!.scope == subscription!!.scope)
+ }
+
+ subscription = null
+
+ val nullResult = geckoDelegate.onGetSubscription("test")
+
+ nullResult?.accept { sub ->
+ assertNull(sub)
+ }
+ }
+
+ @Test
+ fun `onSubscribe result is completed`() {
+ var subscription: WebPushSubscription? = WebPushSubscription(
+ "test",
+ "https://example.com",
+ null,
+ ByteArray(65),
+ ByteArray(16),
+ )
+ val delegate: WebPushDelegate = object : WebPushDelegate {
+ override fun onSubscribe(
+ scope: String,
+ serverKey: ByteArray?,
+ onSubscribe: (WebPushSubscription?) -> Unit,
+ ) {
+ onSubscribe(subscription)
+ }
+ }
+
+ val geckoDelegate = GeckoWebPushDelegate(delegate)
+ val result = geckoDelegate.onSubscribe("test", null)
+
+ result?.accept { sub ->
+ assert(sub!!.scope == subscription!!.scope)
+ assertNull(sub.appServerKey)
+ }
+
+ subscription = null
+
+ val nullResult = geckoDelegate.onSubscribe("test", null)
+ nullResult?.accept { sub ->
+ assertNull(sub)
+ }
+ }
+
+ @Test
+ fun `onUnsubscribe result is completed successfully`() {
+ val delegate: WebPushDelegate = object : WebPushDelegate {
+ override fun onUnsubscribe(
+ scope: String,
+ onUnsubscribe: (Boolean) -> Unit,
+ ) {
+ onUnsubscribe(true)
+ }
+ }
+
+ val geckoDelegate = GeckoWebPushDelegate(delegate)
+ val result = geckoDelegate.onUnsubscribe("test")
+
+ result?.accept { sub ->
+ assertNull(sub)
+ }
+ }
+
+ @Test
+ fun `onUnsubscribe result receives throwable when unsuccessful`() {
+ val delegate: WebPushDelegate = object : WebPushDelegate {
+ override fun onUnsubscribe(
+ scope: String,
+ onUnsubscribe: (Boolean) -> Unit,
+ ) {
+ onUnsubscribe(false)
+ }
+ }
+
+ val geckoDelegate = GeckoWebPushDelegate(delegate)
+
+ val result = geckoDelegate.onUnsubscribe("test")
+
+ result?.exceptionally<Void> { throwable ->
+ assertTrue(throwable.localizedMessage == "Un-subscribing from subscription failed.")
+ GeckoResult.fromValue(null)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandlerTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandlerTest.kt
new file mode 100644
index 0000000000..a59ee61c1e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandlerTest.kt
@@ -0,0 +1,40 @@
+/* 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.browser.engine.gecko.webpush
+
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.WebPushController
+
+class GeckoWebPushHandlerTest {
+
+ lateinit var runtime: GeckoRuntime
+ lateinit var controller: WebPushController
+
+ @Before
+ fun setup() {
+ controller = mock()
+ runtime = mock()
+ `when`(runtime.webPushController).thenReturn(controller)
+ }
+
+ @Test
+ fun `runtime controller is invoked`() {
+ val handler = GeckoWebPushHandler(runtime)
+
+ handler.onPushMessage("", null)
+ verify(controller).onPushEvent(any(), isNull())
+
+ handler.onSubscriptionChanged("test")
+ verify(controller).onSubscriptionChanged(eq("test"))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequestTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequestTest.kt
new file mode 100644
index 0000000000..ec4009a1f1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequestTest.kt
@@ -0,0 +1,20 @@
+/* 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.browser.engine.gecko.window
+
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class GeckoWindowRequestTest {
+
+ @Test
+ fun testPrepare() {
+ val engineSession: GeckoEngineSession = mock()
+ val windowRequest = GeckoWindowRequest("mozilla.org", engineSession)
+ assertEquals(engineSession, windowRequest.prepare())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/experiment/NimbusExperimentDelegateTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/experiment/NimbusExperimentDelegateTest.kt
new file mode 100644
index 0000000000..13a2236243
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/experiment/NimbusExperimentDelegateTest.kt
@@ -0,0 +1,68 @@
+/* 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.browser.experiment
+
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.experiment.NimbusExperimentDelegate
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.ExperimentDelegate
+import org.mozilla.geckoview.ExperimentDelegate.ExperimentException
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoRuntimeSettings
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class NimbusExperimentDelegateTest {
+
+ @Test
+ fun `WHEN an experiment delegate can be set on the runtime THEN the same delegate can be returned`() {
+ val runtime = mock<GeckoRuntime>()
+ val mockExperimentDelegate = mock<ExperimentDelegate>()
+ val mockGeckoSetting = mock<GeckoRuntimeSettings>()
+ whenever(runtime.settings).thenReturn(mockGeckoSetting)
+ whenever(mockGeckoSetting.experimentDelegate).thenReturn(mockExperimentDelegate)
+ assertThat("Can set and retrieve experiment delegate.", runtime.settings.experimentDelegate, equalTo(mockExperimentDelegate))
+ }
+
+ @Test
+ fun `WHEN the Nimbus experiment delegate is used AND the feature does not exist THEN the delegate responds with exceptions`() {
+ val nimbusExperimentDelegate = NimbusExperimentDelegate()
+ nimbusExperimentDelegate.onGetExperimentFeature("test-no-op")
+ .accept { assertTrue("Should not have completed.", false) }
+ .exceptionally { e ->
+ assertTrue("Should have completed exceptionally.", (e as ExperimentException).code == ExperimentException.ERROR_FEATURE_NOT_FOUND)
+ GeckoResult.fromValue(null)
+ }
+
+ nimbusExperimentDelegate.onRecordExposureEvent("test-no-op")
+ .accept { assertTrue("Should not have completed.", false) }
+ .exceptionally { e ->
+ assertTrue("Should have completed exceptionally.", (e as ExperimentException).code == ExperimentException.ERROR_FEATURE_NOT_FOUND)
+ GeckoResult.fromValue(null)
+ }
+ nimbusExperimentDelegate.onRecordExperimentExposureEvent("test-no-op", "test-no-op")
+ .accept { assertTrue("Should not have completed.", false) }
+ .exceptionally { e ->
+ assertTrue("Should have completed exceptionally.", (e as ExperimentException).code == ExperimentException.ERROR_FEATURE_NOT_FOUND)
+ GeckoResult.fromValue(null)
+ }
+ nimbusExperimentDelegate.onRecordMalformedConfigurationEvent("test-no-op", "test")
+ .accept { assertTrue("Should not have completed.", false) }
+ .exceptionally { e ->
+ assertTrue("Should have completed exceptionally.", (e as ExperimentException).code == ExperimentException.ERROR_FEATURE_NOT_FOUND)
+ GeckoResult.fromValue(null)
+ }
+
+ shadowOf(getMainLooper()).idle()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/test/ReflectionUtils.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/test/ReflectionUtils.kt
new file mode 100644
index 0000000000..f49849fbd4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/test/ReflectionUtils.kt
@@ -0,0 +1,26 @@
+/* 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.test
+
+import java.security.AccessController
+import java.security.PrivilegedExceptionAction
+
+object ReflectionUtils {
+ fun <T : Any> setField(instance: T, fieldName: String, value: Any?) {
+ val mapField = AccessController.doPrivileged(
+ PrivilegedExceptionAction {
+ try {
+ val field = instance::class.java.getField(fieldName)
+ field.isAccessible = true
+ return@PrivilegedExceptionAction field
+ } catch (e: ReflectiveOperationException) {
+ throw Error(e)
+ }
+ },
+ )
+
+ mapField.set(instance, value)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/engine-gecko/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/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/browser/engine-gecko/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/engine-gecko/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/browser/engine-system/README.md b/mobile/android/android-components/components/browser/engine-system/README.md
new file mode 100644
index 0000000000..e49a486156
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/README.md
@@ -0,0 +1,58 @@
+# [Android Components](../../../README.md) > Browser > Engine-System
+
+[*Engine*](../../concept/engine/README.md) implementation based on the system's WebView.
+
+## Usage
+
+See [concept-engine](../../concept/engine/README.md) for a documentation of the abstract engine API this component implements.
+
+### 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:browser-engine-system:{latest-version}"
+```
+
+### Initializing
+
+It is recommended t create only one `SystemEngine` instance per app.
+
+```Kotlin
+// Create default settings (optional) and enable tracking protection for all future sessions.
+val defaultSettings = DefaultSettings().apply {
+ trackingProtectionPolicy = EngineSession.TrackingProtectionPolicy.all()
+}
+
+// Create an engine instance to be used by other components.
+val engine = SystemEngine(context, defaultSettings)
+```
+
+### Integration
+
+Usually it is not needed to interact with the `Engine` component directly. The [browser-session](../session/README.md) component will take care of making the state accessible and link a `Session` to an `EngineSession` internally. The [feature-session](../../feature/session/README.md) component will provide "use cases" to perform actions like loading URLs and takes care of rendering the selected `Session` on an `EngineView`.
+
+### View
+
+`SystemEngineView` is the Gecko-based implementation of `EngineView` in order to render web content.
+
+```XML
+<mozilla.components.browser.engine.system.SystemEngineView
+ android:id="@+id/engineView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+```
+
+`SystemEngineView ` can render any `SystemEngineSession` using the `render()` method.
+
+```Kotlin
+val engineSession = engine.createSession()
+val engineView = view.findViewById<SystemEngineView>(R.id.engineView)
+engineView.render(engineSession)
+```
+
+## 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/browser/engine-system/build.gradle b/mobile/android/android-components/components/browser/engine-system/build.gradle
new file mode 100644
index 0000000000..37ac0daa86
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/build.gradle
@@ -0,0 +1,54 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.browser.engine.system'
+}
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+
+}
+
+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/browser/engine-system/proguard-rules.pro b/mobile/android/android-components/components/browser/engine-system/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/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/browser/engine-system/src/androidTest/java/mozilla/components/browser/engine/system/VersionTest.kt b/mobile/android/android-components/components/browser/engine-system/src/androidTest/java/mozilla/components/browser/engine/system/VersionTest.kt
new file mode 100644
index 0000000000..dca1cead0c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/androidTest/java/mozilla/components/browser/engine/system/VersionTest.kt
@@ -0,0 +1,28 @@
+/* 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.browser.engine.system
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class VersionTest {
+ @Test
+ fun testParsingOfActualWebViewVersion() {
+ runBlocking(Dispatchers.Main) {
+ val context: Context = ApplicationProvider.getApplicationContext()
+ val engine = SystemEngine(context)
+ val version = engine.version
+
+ assertTrue(version.major > 60)
+
+ // 60.0.3112 was released 2017-07-31.
+ assertTrue(version.isAtLeast(60, 0, 3113))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/androidTest/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/engine-system/src/androidTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/androidTest/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/browser/engine-system/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/engine-system/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/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/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/NestedWebView.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/NestedWebView.kt
new file mode 100644
index 0000000000..74b3479b11
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/NestedWebView.kt
@@ -0,0 +1,170 @@
+/* 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.browser.engine.system
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import android.view.MotionEvent.obtain
+import android.webkit.WebView
+import androidx.annotation.VisibleForTesting
+import androidx.core.view.NestedScrollingChild
+import androidx.core.view.NestedScrollingChildHelper
+import androidx.core.view.ViewCompat
+import mozilla.components.concept.engine.INPUT_HANDLED
+import mozilla.components.concept.engine.INPUT_UNHANDLED
+import mozilla.components.concept.engine.InputResultDetail
+
+/**
+ * WebView that supports nested scrolls (for using in a CoordinatorLayout).
+ *
+ * This code is a simplified version of the NestedScrollView implementation
+ * which can be found in the support library:
+ * [android.support.v4.widget.NestedScrollView]
+ *
+ * Based on:
+ * https://github.com/takahirom/webview-in-coordinatorlayout
+ */
+class NestedWebView(context: Context) : WebView(context), NestedScrollingChild {
+
+ @VisibleForTesting
+ internal var lastY: Int = 0
+
+ @VisibleForTesting
+ internal val scrollOffset = IntArray(2)
+
+ private val scrollConsumed = IntArray(2)
+
+ @VisibleForTesting
+ internal var nestedOffsetY: Int = 0
+
+ @VisibleForTesting
+ internal var childHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
+
+ /**
+ * How user's MotionEvent will be handled.
+ *
+ * @see InputResultDetail
+ */
+ internal var inputResultDetail = InputResultDetail.newInstance()
+
+ init {
+ isNestedScrollingEnabled = true
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
+ val event = obtain(ev)
+ val action = ev.actionMasked
+
+ if (action == ACTION_DOWN) {
+ nestedOffsetY = 0
+ }
+
+ val eventY = event.y.toInt()
+ event.offsetLocation(0f, nestedOffsetY.toFloat())
+
+ when (action) {
+ ACTION_MOVE -> {
+ var deltaY = lastY - eventY
+
+ if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
+ deltaY -= scrollConsumed[1]
+ event.offsetLocation(0f, (-scrollOffset[1]).toFloat())
+ nestedOffsetY += scrollOffset[1]
+ }
+
+ lastY = eventY - scrollOffset[1]
+
+ if (dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
+ lastY -= scrollOffset[1]
+ event.offsetLocation(0f, scrollOffset[1].toFloat())
+ nestedOffsetY += scrollOffset[1]
+ }
+ }
+
+ ACTION_DOWN -> {
+ lastY = eventY
+ startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
+ }
+
+ // We don't care about other touch events
+ ACTION_UP, ACTION_CANCEL -> stopNestedScroll()
+ }
+
+ // Execute event handler from parent class in all cases
+ val eventHandled = callSuperOnTouchEvent(event)
+ updateInputResult(eventHandled)
+
+ // Recycle previously obtained event
+ event.recycle()
+
+ return eventHandled
+ }
+
+ @VisibleForTesting
+ internal fun callSuperOnTouchEvent(event: MotionEvent): Boolean {
+ return super.onTouchEvent(event)
+ }
+
+ // NestedScrollingChild
+
+ override fun setNestedScrollingEnabled(enabled: Boolean) {
+ childHelper.isNestedScrollingEnabled = enabled
+ }
+
+ override fun isNestedScrollingEnabled(): Boolean {
+ return childHelper.isNestedScrollingEnabled
+ }
+
+ override fun startNestedScroll(axes: Int): Boolean {
+ return childHelper.startNestedScroll(axes)
+ }
+
+ override fun stopNestedScroll() {
+ childHelper.stopNestedScroll()
+ }
+
+ override fun hasNestedScrollingParent(): Boolean {
+ return childHelper.hasNestedScrollingParent()
+ }
+
+ override fun dispatchNestedScroll(
+ dxConsumed: Int,
+ dyConsumed: Int,
+ dxUnconsumed: Int,
+ dyUnconsumed: Int,
+ offsetInWindow: IntArray?,
+ ): Boolean {
+ return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
+ }
+
+ override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
+ return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
+ }
+
+ override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
+ return childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
+ }
+
+ override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
+ return childHelper.dispatchNestedPreFling(velocityX, velocityY)
+ }
+
+ @VisibleForTesting
+ internal fun updateInputResult(eventHandled: Boolean) {
+ inputResultDetail = inputResultDetail.copy(
+ if (eventHandled) {
+ INPUT_HANDLED
+ } else {
+ INPUT_UNHANDLED
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngine.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngine.kt
new file mode 100644
index 0000000000..0117b53014
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngine.kt
@@ -0,0 +1,152 @@
+/* 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.browser.engine.system
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.JsonReader
+import android.webkit.WebSettings
+import android.webkit.WebView
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.base.profiler.Profiler
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.engine.utils.EngineVersion
+import org.json.JSONObject
+import java.lang.IllegalStateException
+
+/**
+ * WebView-based implementation of the Engine interface.
+ */
+class SystemEngine(
+ private val context: Context,
+ private val defaultSettings: Settings = DefaultSettings(),
+) : Engine {
+ init {
+ initDefaultUserAgent(context)
+ }
+
+ /**
+ * Creates a new WebView-based EngineView implementation.
+ */
+ override fun createView(context: Context, attrs: AttributeSet?): EngineView {
+ return SystemEngineView(context, attrs)
+ }
+
+ /**
+ * Creates a new WebView-based EngineSession implementation.
+ */
+ override fun createSession(private: Boolean, contextId: String?): EngineSession {
+ if (private) {
+ // TODO Implement private browsing: https://github.com/mozilla-mobile/android-components/issues/649
+ throw UnsupportedOperationException("Private browsing is not supported in ${this::class.java.simpleName}")
+ } else if (contextId != null) {
+ throw UnsupportedOperationException(
+ "Contextual identities are not supported in ${this::class.java.simpleName}",
+ )
+ }
+
+ return SystemEngineSession(context, defaultSettings)
+ }
+
+ /**
+ * Opens a speculative connection to the host of [url].
+ *
+ * Note: This implementation is a no-op.
+ */
+ override fun speculativeConnect(url: String) = Unit
+
+ /**
+ * See [Engine.profiler].
+ */
+ override val profiler: Profiler? = null
+
+ /**
+ * See [Engine.name]
+ */
+ override fun name(): String = "System"
+
+ @Suppress("TooGenericExceptionCaught")
+ override val version: EngineVersion
+ get() {
+ val userAgent = WebSettings.getDefaultUserAgent(context)
+ val version = try {
+ "Chrome/([^ ]+)".toRegex().find(userAgent)?.groups?.get(1)?.value
+ ?: throw IllegalStateException("Could not get version from user agent: $userAgent")
+ } catch (e: IllegalStateException) {
+ throw IllegalStateException("Could not get version from user agent: $userAgent")
+ } catch (e: IndexOutOfBoundsException) {
+ throw IllegalStateException("Could not get version from user agent: $userAgent")
+ }
+
+ return EngineVersion.parse(version)
+ ?: throw IllegalStateException("Could not determine engine version: $version")
+ }
+
+ override fun createSessionState(json: JSONObject): EngineSessionState {
+ return SystemEngineSessionState.fromJSON(json)
+ }
+
+ override fun createSessionStateFrom(reader: JsonReader): EngineSessionState {
+ return SystemEngineSessionState.from(reader)
+ }
+
+ /**
+ * See [Engine.settings]
+ */
+ override val settings: Settings = object : Settings() {
+ private var internalRemoteDebuggingEnabled = false
+ override var remoteDebuggingEnabled: Boolean
+ get() = internalRemoteDebuggingEnabled
+ set(value) {
+ WebView.setWebContentsDebuggingEnabled(value)
+ internalRemoteDebuggingEnabled = value
+ }
+
+ override var userAgentString: String?
+ get() = defaultSettings.userAgentString
+ set(value) {
+ defaultSettings.userAgentString = value
+ }
+
+ override var trackingProtectionPolicy: TrackingProtectionPolicy?
+ get() = defaultSettings.trackingProtectionPolicy
+ set(value) {
+ defaultSettings.trackingProtectionPolicy = value
+ }
+
+ override var historyTrackingDelegate: HistoryTrackingDelegate?
+ get() = defaultSettings.historyTrackingDelegate
+ set(value) {
+ defaultSettings.historyTrackingDelegate = value
+ }
+ }.apply {
+ this.remoteDebuggingEnabled = defaultSettings.remoteDebuggingEnabled
+ this.trackingProtectionPolicy = defaultSettings.trackingProtectionPolicy
+ if (defaultSettings.userAgentString == null) {
+ defaultSettings.userAgentString = defaultUserAgent
+ }
+ }
+
+ companion object {
+ // In Robolectric tests we can't call WebSettings.getDefaultUserAgent(context)
+ // as this would result in a NPE. So, we expose this field to circumvent the call.
+ @VisibleForTesting
+ var defaultUserAgent: String? = null
+
+ private fun initDefaultUserAgent(context: Context): String {
+ if (defaultUserAgent == null) {
+ defaultUserAgent = WebSettings.getDefaultUserAgent(context)
+ }
+ return defaultUserAgent as String
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSession.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSession.kt
new file mode 100644
index 0000000000..aaf2ec1634
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSession.kt
@@ -0,0 +1,636 @@
+/* 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.browser.engine.system
+
+import android.content.Context
+import android.webkit.CookieManager
+import android.webkit.WebChromeClient
+import android.webkit.WebSettings
+import android.webkit.WebSettings.LOAD_NO_CACHE
+import android.webkit.WebStorage
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.webkit.WebViewDatabase
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.Engine.BrowsingData
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.concept.engine.shopping.ProductAnalysis
+import mozilla.components.concept.engine.shopping.ProductAnalysisStatus
+import mozilla.components.concept.engine.shopping.ProductRecommendation
+import mozilla.components.concept.engine.translate.TranslationOptions
+import kotlin.reflect.KProperty
+
+internal val xRequestHeader = mapOf(
+ // For every request WebView sends a "X-requested-with" header with the package name of the
+ // application. We can't really prevent that but we can at least send an empty value.
+ // Unfortunately the additional headers will not be propagated to subsequent requests
+ // (e.g. redirects). See issue #696.
+ "X-Requested-With" to "",
+)
+
+/**
+ * WebView-based EngineSession implementation.
+ */
+@Suppress("LargeClass", "TooManyFunctions")
+class SystemEngineSession(
+ context: Context,
+ private val defaultSettings: Settings? = null,
+) : EngineSession() {
+ private val resources = context.resources
+
+ @Volatile internal lateinit var internalSettings: Settings
+
+ @Volatile internal var historyTrackingDelegate: HistoryTrackingDelegate? = null
+
+ @Volatile internal var trackingProtectionPolicy: TrackingProtectionPolicy? = null
+
+ @Volatile internal var webFontsEnabled = true
+
+ @Volatile internal var currentUrl = ""
+
+ @Volatile internal var useWideViewPort: Boolean? = null // See [toggleDesktopMode]
+
+ @Volatile internal var fullScreenCallback: WebChromeClient.CustomViewCallback? = null
+
+ // This is public for FFTV which needs access to the WebView instance. We can mark it internal once
+ // https://github.com/mozilla-mobile/android-components/issues/1616 is resolved.
+ @Volatile var webView: WebView = NestedWebView(context)
+ set(value) {
+ field = value
+ initSettings()
+ }
+
+ init {
+ initSettings()
+ }
+
+ /**
+ * See [EngineSession.loadUrl]. Note that [LoadUrlFlags] are ignored in this engine
+ * implementation.
+ */
+ override fun loadUrl(
+ url: String,
+ parent: EngineSession?,
+ flags: LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {
+ notifyObservers { onLoadUrl() }
+
+ val headers =
+ if (additionalHeaders == null) {
+ xRequestHeader
+ } else {
+ xRequestHeader + additionalHeaders
+ }
+
+ if (!url.isEmpty()) {
+ currentUrl = url
+ webView.loadUrl(url, headers)
+ }
+ }
+
+ /**
+ * See [EngineSession.loadData]
+ */
+ override fun loadData(data: String, mimeType: String, encoding: String) {
+ webView.loadData(data, mimeType, encoding)
+ notifyObservers { onLoadData() }
+ }
+
+ override fun requestPdfToDownload() {
+ throw UnsupportedOperationException("PDF support is not available in this engine")
+ }
+
+ override fun requestPrintContent() {
+ throw UnsupportedOperationException("Print support is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.stopLoading]
+ */
+ override fun stopLoading() {
+ webView.stopLoading()
+ }
+
+ /**
+ * See [EngineSession.reload]
+ * @param flags currently not supported in `SystemEngineSession`.
+ */
+ override fun reload(flags: LoadUrlFlags) {
+ webView.reload()
+ }
+
+ /**
+ * See [EngineSession.goBack]
+ */
+ override fun goBack(userInteraction: Boolean) {
+ webView.goBack()
+ if (webView.canGoBack()) {
+ notifyObservers { onNavigateBack() }
+ }
+ }
+
+ /**
+ * See [EngineSession.goForward]
+ */
+ override fun goForward(userInteraction: Boolean) {
+ webView.goForward()
+ if (webView.canGoForward()) {
+ notifyObservers { onNavigateForward() }
+ }
+ }
+
+ /**
+ * See [EngineSession.goToHistoryIndex]
+ */
+ override fun goToHistoryIndex(index: Int) {
+ val historyList = webView.copyBackForwardList()
+ webView.goBackOrForward(index - historyList.currentIndex)
+ notifyObservers { onGotoHistoryIndex() }
+ }
+
+ /**
+ * See [EngineSession.restoreState]
+ */
+ override fun restoreState(state: EngineSessionState): Boolean {
+ if (state !is SystemEngineSessionState) {
+ throw IllegalArgumentException("Can only restore from SystemEngineSessionState")
+ }
+
+ return state.bundle?.let { webView.restoreState(it) } != null
+ }
+
+ /**
+ * See [EngineSession.updateTrackingProtection]
+ */
+ override fun updateTrackingProtection(policy: TrackingProtectionPolicy) {
+ // Make sure Url matcher is preloaded now that tracking protection is enabled
+ CoroutineScope(Dispatchers.IO).launch {
+ SystemEngineView.getOrCreateUrlMatcher(resources, policy)
+ }
+
+ // TODO check if policy should be applied for this session type
+ // (regular|private) once we support private browsing in system engine:
+ // https://github.com/mozilla-mobile/android-components/issues/649
+ trackingProtectionPolicy = policy
+ notifyObservers { onTrackerBlockingEnabledChange(true) }
+ }
+
+ @VisibleForTesting
+ internal fun disableTrackingProtection() {
+ trackingProtectionPolicy = null
+ notifyObservers { onTrackerBlockingEnabledChange(false) }
+ }
+
+ /**
+ * See [EngineSession.close]
+ */
+ override fun close() {
+ super.close()
+ // The WebView instance must remain useable for the duration of this session.
+ // We can only destroy it once we're sure this session will not be used
+ // again which is why destroy happens here are not part of regular (activity)
+ // lifecycle event.
+ webView.destroy()
+ }
+
+ /**
+ * See [EngineSession.clearData]
+ */
+ @Suppress("TooGenericExceptionCaught")
+ override fun clearData(data: BrowsingData, host: String?, onSuccess: () -> Unit, onError: (Throwable) -> Unit) {
+ webView.apply {
+ try {
+ if (data.contains(BrowsingData.DOM_STORAGES)) {
+ webStorage().deleteAllData()
+ }
+ if (data.contains(BrowsingData.IMAGE_CACHE) || data.contains(BrowsingData.NETWORK_CACHE)) {
+ clearCache(true)
+ }
+ if (data.contains(BrowsingData.COOKIES)) {
+ CookieManager.getInstance().removeAllCookies(null)
+ }
+ if (data.contains(BrowsingData.AUTH_SESSIONS)) {
+ webViewDatabase(context).clearHttpAuthUsernamePassword()
+ }
+ if (data.contains(BrowsingData.ALL)) {
+ clearSslPreferences()
+ clearFormData()
+ clearMatches()
+ clearHistory()
+ }
+ onSuccess()
+ } catch (e: Throwable) {
+ onError(e)
+ }
+ }
+ }
+
+ /**
+ * See [EngineSession.findAll]
+ */
+ override fun findAll(text: String) {
+ notifyObservers { onFind(text) }
+ webView.findAllAsync(text)
+ }
+
+ /**
+ * See [EngineSession.findNext]
+ */
+ override fun findNext(forward: Boolean) {
+ webView.findNext(forward)
+ }
+
+ /**
+ * See [EngineSession.clearFindMatches]
+ */
+ override fun clearFindMatches() {
+ webView.clearMatches()
+ }
+
+ /**
+ * Clears the internal back/forward list.
+ */
+ override fun purgeHistory() {
+ webView.clearHistory()
+ }
+
+ /**
+ * See [EngineSession.settings]
+ */
+ override val settings: Settings
+ get() = internalSettings
+
+ class WebSetting<T>(private val get: () -> T, private val set: (T) -> Unit) {
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T = get()
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = set(value)
+ }
+
+ @VisibleForTesting
+ internal fun initSettings() {
+ webView.settings.apply {
+ // Explicitly set global defaults.
+
+ cacheMode = LOAD_NO_CACHE
+ databaseEnabled = false
+
+ setDeprecatedWebSettings(this)
+
+ // We currently don't implement the callback to support turning this on.
+ setGeolocationEnabled(false)
+
+ // webViewSettings built-in zoom controls are the only supported ones,
+ // so they should be turned on but hidden.
+ builtInZoomControls = true
+ displayZoomControls = false
+
+ initSettings(webView, this)
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun setDeprecatedWebSettings(webSettings: WebSettings) {
+ // Since API26 an autofill platform feature is used instead of WebView's form data. This
+ // has no effect. Form data is supported on pre-26 API versions.
+ webSettings.saveFormData = false
+ // Deprecated in API18.
+ webSettings.savePassword = false
+ }
+
+ private fun setUseWideViewPort(settings: WebSettings, useWideViewPort: Boolean?) {
+ this.useWideViewPort = useWideViewPort
+ useWideViewPort?.let { settings.useWideViewPort = it }
+ }
+
+ private fun initSettings(webView: WebView, s: WebSettings) {
+ internalSettings = object : Settings() {
+ override var javascriptEnabled by WebSetting(s::getJavaScriptEnabled, s::setJavaScriptEnabled)
+ override var domStorageEnabled by WebSetting(s::getDomStorageEnabled, s::setDomStorageEnabled)
+ override var allowFileAccess by WebSetting(s::getAllowFileAccess, s::setAllowFileAccess)
+ override var allowContentAccess by WebSetting(s::getAllowContentAccess, s::setAllowContentAccess)
+ override var userAgentString by WebSetting(s::getUserAgentString, s::setUserAgentString)
+ override var displayZoomControls by WebSetting(s::getDisplayZoomControls, s::setDisplayZoomControls)
+ override var loadWithOverviewMode by WebSetting(s::getLoadWithOverviewMode, s::setLoadWithOverviewMode)
+ override var useWideViewPort: Boolean?
+ get() = this@SystemEngineSession.useWideViewPort
+ set(value) = setUseWideViewPort(s, value)
+ override var supportMultipleWindows by WebSetting(s::supportMultipleWindows, s::setSupportMultipleWindows)
+
+ @Suppress("DEPRECATION")
+ // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/8513
+ override var allowFileAccessFromFileURLs by WebSetting(
+ s::getAllowFileAccessFromFileURLs,
+ s::setAllowFileAccessFromFileURLs,
+ )
+
+ @Suppress("DEPRECATION")
+ // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/8514
+ override var allowUniversalAccessFromFileURLs by WebSetting(
+ s::getAllowUniversalAccessFromFileURLs,
+ s::setAllowUniversalAccessFromFileURLs,
+ )
+
+ override var mediaPlaybackRequiresUserGesture by WebSetting(
+ s::getMediaPlaybackRequiresUserGesture,
+ s::setMediaPlaybackRequiresUserGesture,
+ )
+ override var javaScriptCanOpenWindowsAutomatically by WebSetting(
+ s::getJavaScriptCanOpenWindowsAutomatically,
+ s::setJavaScriptCanOpenWindowsAutomatically,
+ )
+
+ override var verticalScrollBarEnabled
+ get() = webView.isVerticalScrollBarEnabled
+ set(value) { webView.isVerticalScrollBarEnabled = value }
+
+ override var horizontalScrollBarEnabled
+ get() = webView.isHorizontalScrollBarEnabled
+ set(value) { webView.isHorizontalScrollBarEnabled = value }
+
+ override var webFontsEnabled
+ get() = this@SystemEngineSession.webFontsEnabled
+ set(value) { this@SystemEngineSession.webFontsEnabled = value }
+
+ override var trackingProtectionPolicy: TrackingProtectionPolicy?
+ get() = this@SystemEngineSession.trackingProtectionPolicy
+ set(value) = value?.let { updateTrackingProtection(it) } ?: disableTrackingProtection()
+
+ override var historyTrackingDelegate: HistoryTrackingDelegate?
+ get() = this@SystemEngineSession.historyTrackingDelegate
+ set(value) { this@SystemEngineSession.historyTrackingDelegate = value }
+
+ override var requestInterceptor: RequestInterceptor? = null
+ }.apply {
+ defaultSettings?.let {
+ javascriptEnabled = it.javascriptEnabled
+ domStorageEnabled = it.domStorageEnabled
+ webFontsEnabled = it.webFontsEnabled
+ displayZoomControls = it.displayZoomControls
+ loadWithOverviewMode = it.loadWithOverviewMode
+ useWideViewPort = it.useWideViewPort
+ trackingProtectionPolicy = it.trackingProtectionPolicy
+ historyTrackingDelegate = it.historyTrackingDelegate
+ requestInterceptor = it.requestInterceptor
+ mediaPlaybackRequiresUserGesture = it.mediaPlaybackRequiresUserGesture
+ javaScriptCanOpenWindowsAutomatically = it.javaScriptCanOpenWindowsAutomatically
+ allowFileAccess = it.allowFileAccess
+ allowContentAccess = it.allowContentAccess
+ allowUniversalAccessFromFileURLs = it.allowUniversalAccessFromFileURLs
+ allowFileAccessFromFileURLs = it.allowFileAccessFromFileURLs
+ verticalScrollBarEnabled = it.verticalScrollBarEnabled
+ horizontalScrollBarEnabled = it.horizontalScrollBarEnabled
+ userAgentString = it.userAgentString
+ supportMultipleWindows = it.supportMultipleWindows
+ }
+ }
+ }
+
+ /**
+ * See [EngineSession.toggleDesktopMode]
+ *
+ * Precondition:
+ * If settings.useWideViewPort = true, then webSettings.useWideViewPort is always on
+ * If settings.useWideViewPort = false or null, then webSettings.useWideViewPort can be on/off
+ */
+ override fun toggleDesktopMode(enable: Boolean, reload: Boolean) {
+ val webSettings = webView.settings
+ webSettings.userAgentString = toggleDesktopUA(webSettings.userAgentString, enable)
+ webSettings.useWideViewPort = if (settings.useWideViewPort == true) true else enable
+
+ notifyObservers { onDesktopModeChange(enable) }
+
+ if (reload) {
+ webView.reload()
+ }
+ }
+
+ /**
+ * Checks for if PDF Viewer is used.
+ */
+ override fun checkForPdfViewer(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Checking for PDF viewer is not available in this engine")
+ }
+
+ /**
+ * /**
+ * See [EngineSession.requestProductRecommendations]
+ */
+ */
+ override fun requestProductRecommendations(
+ url: String,
+ onResult: (List<ProductRecommendation>) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Analysis of product reviews for shopping is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.requestProductAnalysis]
+ */
+ override fun requestProductAnalysis(
+ url: String,
+ onResult: (ProductAnalysis) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Analysis of product reviews for shopping is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.reanalyzeProduct]
+ */
+ override fun reanalyzeProduct(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Reanalyzing product reviews for shopping is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.requestAnalysisStatus]
+ */
+ override fun requestAnalysisStatus(
+ url: String,
+ onResult: (ProductAnalysisStatus) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Requesting product analysis status is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.sendClickAttributionEvent]
+ */
+ override fun sendClickAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Sending click attribution event is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.sendImpressionAttributionEvent]
+ */
+ override fun sendImpressionAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Sending impression attribution event is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.sendPlacementAttributionEvent]
+ */
+ override fun sendPlacementAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Sending placement attribution event is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.reportBackInStock]
+ */
+ override fun reportBackInStock(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Reporting back in stock is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.requestTranslate]
+ */
+ override fun requestTranslate(
+ fromLanguage: String,
+ toLanguage: String,
+ options: TranslationOptions?,
+ ) {
+ throw UnsupportedOperationException("Translate support is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.requestTranslationRestore]
+ */
+ override fun requestTranslationRestore() {
+ throw UnsupportedOperationException("Translate restore support is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.getNeverTranslateSiteSetting]
+ */
+ override fun getNeverTranslateSiteSetting(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Getting the site's translate setting is not available in this engine.")
+ }
+
+ /**
+ * See [EngineSession.setNeverTranslateSiteSetting]
+ */
+ override fun setNeverTranslateSiteSetting(
+ setting: Boolean,
+ onResult: () -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Setting the site's translate setting is not available in this engine")
+ }
+
+ override fun hasCookieBannerRuleForSession(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {
+ throw UnsupportedOperationException("Cookie Banner handling is not available in this engine")
+ }
+
+ /**
+ * See [EngineSession.exitFullScreenMode]
+ */
+ override fun exitFullScreenMode() {
+ fullScreenCallback?.onCustomViewHidden()
+ }
+
+ internal fun toggleDesktopUA(userAgent: String, requestDesktop: Boolean): String {
+ return if (requestDesktop) {
+ userAgent.replace("Mobile", "eliboM").replace("Android", "diordnA")
+ } else {
+ userAgent.replace("eliboM", "Mobile").replace("diordnA", "Android")
+ }
+ }
+
+ internal fun webStorage(): WebStorage = WebStorage.getInstance()
+
+ internal fun webViewDatabase(context: Context) = WebViewDatabase.getInstance(context)
+
+ /**
+ * Helper method to notify observers from other classes in this package. This is needed as
+ * almost everything is implemented by WebView and its listeners. There is no actual concept of
+ * a session when using WebView.
+ */
+ internal fun internalNotifyObservers(block: Observer.() -> Unit) {
+ super.notifyObservers(block)
+ }
+
+ companion object {
+ /**
+ * Provides an ErrorType corresponding to the error code provided.
+ *
+ * Chromium's mapping (internal error code, to Android WebView error code) is described at:
+ * https://goo.gl/vspwct (ErrorCodeConversionHelper.java)
+ */
+ internal fun webViewErrorToErrorType(errorCode: Int) =
+ when (errorCode) {
+ WebViewClient.ERROR_UNKNOWN -> ErrorType.UNKNOWN
+
+ // This is probably the most commonly shown error. If there's no network, we inevitably
+ // show this.
+ WebViewClient.ERROR_HOST_LOOKUP -> ErrorType.ERROR_UNKNOWN_HOST
+
+ WebViewClient.ERROR_CONNECT -> ErrorType.ERROR_CONNECTION_REFUSED
+
+ // It's unclear what this actually means - it's not well documented. Based on looking at
+ // ErrorCodeConversionHelper this could happen if networking is disabled during load, in which
+ // case the generic error is good enough:
+ WebViewClient.ERROR_IO -> ErrorType.ERROR_CONNECTION_REFUSED
+
+ WebViewClient.ERROR_TIMEOUT -> ErrorType.ERROR_NET_TIMEOUT
+
+ WebViewClient.ERROR_REDIRECT_LOOP -> ErrorType.ERROR_REDIRECT_LOOP
+
+ WebViewClient.ERROR_UNSUPPORTED_SCHEME -> ErrorType.ERROR_UNKNOWN_PROTOCOL
+
+ WebViewClient.ERROR_FAILED_SSL_HANDSHAKE -> ErrorType.ERROR_SECURITY_SSL
+
+ WebViewClient.ERROR_BAD_URL -> ErrorType.ERROR_MALFORMED_URI
+
+ // Seems to be an indication of OOM, insufficient resources, or too many queued DNS queries
+ WebViewClient.ERROR_TOO_MANY_REQUESTS -> ErrorType.UNKNOWN
+
+ WebViewClient.ERROR_FILE_NOT_FOUND -> ErrorType.ERROR_FILE_NOT_FOUND
+
+ // There's no mapping for the following errors yet. At the time this library was
+ // extracted from Focus we didn't use any of those errors.
+ // WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME
+ // WebViewClient.ERROR_AUTHENTICATION
+ // WebViewClient.ERROR_FILE
+ else -> ErrorType.UNKNOWN
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSessionState.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSessionState.kt
new file mode 100644
index 0000000000..198fc9a303
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSessionState.kt
@@ -0,0 +1,95 @@
+/* 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.browser.engine.system
+
+import android.os.Bundle
+import android.util.JsonReader
+import android.util.JsonToken
+import android.util.JsonWriter
+import mozilla.components.concept.engine.EngineSessionState
+import org.json.JSONObject
+
+class SystemEngineSessionState(
+ internal val bundle: Bundle?,
+) : EngineSessionState {
+ override fun writeTo(writer: JsonWriter) {
+ writer.beginObject()
+
+ bundle?.keySet()?.forEach { key ->
+ when (
+ @Suppress("DEPRECATION")
+ val value = bundle[key]
+ ) {
+ is Number -> writer.name(key).value(value)
+ is String -> writer.name(key).value(value)
+ is Boolean -> writer.name(key).value(value)
+ }
+ }
+
+ writer.endObject()
+ writer.flush()
+ }
+
+ companion object {
+ fun fromJSON(json: JSONObject): SystemEngineSessionState {
+ return SystemEngineSessionState(json.toBundle())
+ }
+
+ /**
+ * Creates a [SystemEngineSessionState] from the given [JsonReader].
+ */
+ fun from(reader: JsonReader): SystemEngineSessionState {
+ return SystemEngineSessionState(reader.toBundle())
+ }
+ }
+}
+
+private fun JsonReader.toBundle(): Bundle {
+ beginObject()
+
+ val bundle = Bundle()
+
+ while (peek() != JsonToken.END_OBJECT) {
+ val name = nextName()
+
+ when (peek()) {
+ JsonToken.NULL -> nextNull()
+ JsonToken.BOOLEAN -> bundle.putBoolean(name, nextBoolean())
+ JsonToken.STRING -> bundle.putString(name, nextString())
+ JsonToken.NUMBER -> bundle.putDouble(name, nextDouble())
+ JsonToken.BEGIN_OBJECT -> bundle.putBundle(name, toBundle())
+ else -> skipValue()
+ }
+ }
+
+ endObject()
+
+ return bundle
+}
+
+private fun JSONObject.toBundle(): Bundle {
+ val bundle = Bundle()
+
+ keys().forEach { key ->
+ val value = get(key)
+ bundle.put(key, value)
+ }
+
+ return bundle
+}
+
+private fun Bundle.put(key: String, value: Any) {
+ when (value) {
+ is Int -> putInt(key, value)
+ is Double -> putDouble(key, value)
+ is Long -> putLong(key, value)
+ is Float -> putFloat(key, value)
+ is Char -> putChar(key, value)
+ is Short -> putShort(key, value)
+ is Byte -> putByte(key, value)
+ is String -> putString(key, value)
+ is Boolean -> putBoolean(key, value)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineView.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineView.kt
new file mode 100644
index 0000000000..d0a5a6af8e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineView.kt
@@ -0,0 +1,835 @@
+/* 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.browser.engine.system
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.net.Uri
+import android.net.http.SslError
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Handler
+import android.os.Message
+import android.util.AttributeSet
+import android.view.PixelCopy
+import android.view.View
+import android.webkit.CookieManager
+import android.webkit.DownloadListener
+import android.webkit.HttpAuthHandler
+import android.webkit.JsPromptResult
+import android.webkit.JsResult
+import android.webkit.PermissionRequest
+import android.webkit.SslErrorHandler
+import android.webkit.ValueCallback
+import android.webkit.WebChromeClient
+import android.webkit.WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
+import android.webkit.WebView
+import android.webkit.WebView.HitTestResult.EMAIL_TYPE
+import android.webkit.WebView.HitTestResult.GEO_TYPE
+import android.webkit.WebView.HitTestResult.IMAGE_TYPE
+import android.webkit.WebView.HitTestResult.PHONE_TYPE
+import android.webkit.WebView.HitTestResult.SRC_ANCHOR_TYPE
+import android.webkit.WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
+import android.webkit.WebViewClient
+import android.widget.FrameLayout
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.core.net.toUri
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.engine.system.matcher.UrlMatcher
+import mozilla.components.browser.engine.system.permission.SystemPermissionRequest
+import mozilla.components.browser.engine.system.window.SystemWindowRequest
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.concept.engine.InputResultDetail
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.request.RequestInterceptor.InterceptionResponse
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.concept.storage.PageVisit
+import mozilla.components.concept.storage.VisitType
+import mozilla.components.support.ktx.android.view.getRectWithViewLocation
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+import mozilla.components.support.utils.DownloadUtils
+
+/**
+ * WebView-based implementation of EngineView.
+ */
+@Suppress("TooManyFunctions")
+class SystemEngineView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr), EngineView, View.OnLongClickListener {
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var session: SystemEngineSession? = null
+
+ override var selectionActionDelegate: SelectionActionDelegate? = null
+
+ /**
+ * Render the content of the given session.
+ */
+ override fun render(session: EngineSession) {
+ removeAllViews()
+
+ this.session = session as SystemEngineSession
+ (session.webView.parent as? SystemEngineView)?.removeView(session.webView)
+ addView(initWebView(session.webView))
+ }
+
+ override fun release() {
+ this.session = null
+
+ removeAllViews()
+ }
+
+ override fun onLongClick(view: View?): Boolean {
+ val result = session?.webView?.hitTestResult
+ return result?.let { handleLongClick(result.type, result.extra ?: "") } ?: false
+ }
+
+ override fun onPause() {
+ session?.apply {
+ webView.onPause()
+ webView.pauseTimers()
+ }
+ }
+
+ override fun onResume() {
+ session?.apply {
+ webView.onResume()
+ webView.resumeTimers()
+ }
+ }
+
+ override fun onDestroy() {
+ session?.apply {
+ // The WebView instance is long-lived, as it's referenced in the
+ // engine session. We can't destroy it here since the session
+ // might be used with a different engine view instance later.
+
+ // Further, when this engine view gets destroyed, we need to
+ // remove/detach the WebView so that engine view's activity context
+ // can properly be destroyed and gc'ed. The WebView instances are
+ // created with the context provided to the engine (application
+ // context) and reference their parent (this engine view). Since
+ // we're keeping the engine session (and their WebView) instances
+ // in the SessionManager until closed we'd otherwise prevent
+ // this engine view and its context from getting gc'ed.
+ (webView.parent as? SystemEngineView)?.removeView(webView)
+ }
+ }
+
+ internal fun initWebView(webView: WebView): WebView {
+ webView.tag = "mozac_system_engine_webview"
+ webView.webViewClient = createWebViewClient()
+ webView.webChromeClient = createWebChromeClient()
+ webView.setDownloadListener(createDownloadListener())
+ webView.setFindListener(createFindListener())
+ return webView
+ }
+
+ @Suppress("ComplexMethod", "NestedBlockDepth")
+ private fun createWebViewClient() = object : WebViewClient() {
+ override fun doUpdateVisitedHistory(view: WebView, url: String, isReload: Boolean) {
+ // TODO private browsing not supported for SystemEngine
+ // https://github.com/mozilla-mobile/android-components/issues/649
+ // Check if the delegate wants this type of url.
+ val delegate = session?.settings?.historyTrackingDelegate ?: return
+
+ if (!delegate.shouldStoreUri(url)) {
+ return
+ }
+
+ val visitType = when (isReload) {
+ true -> VisitType.RELOAD
+ false -> VisitType.LINK
+ }
+
+ runBlocking {
+ session?.settings?.historyTrackingDelegate?.onVisited(url, PageVisit(visitType))
+ }
+ }
+
+ override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
+ url?.let {
+ session?.currentUrl = url
+ session?.internalNotifyObservers {
+ onLoadingStateChange(true)
+ onLocationChange(it, false)
+ onNavigationStateChange(view.canGoBack(), view.canGoForward())
+ }
+ }
+ }
+
+ override fun onPageFinished(view: WebView?, url: String?) {
+ url?.let {
+ val cert = view?.certificate
+ session?.internalNotifyObservers {
+ onLocationChange(it, false)
+ onLoadingStateChange(false)
+ onSecurityChange(
+ secure = cert != null,
+ host = cert?.let { Uri.parse(url).host },
+ issuer = cert?.issuedBy?.oName,
+ )
+ }
+ }
+ }
+
+ @Suppress("ReturnCount", "NestedBlockDepth", "LongMethod")
+ override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
+ if (session?.webFontsEnabled == false && UrlMatcher.isWebFont(request.url)) {
+ return WebResourceResponse(null, null, null)
+ }
+
+ session?.trackingProtectionPolicy?.let {
+ val resourceUri = request.url
+ val scheme = resourceUri.scheme
+ val path = resourceUri.path
+
+ if (!request.isForMainFrame && scheme != "http" && scheme != "https") {
+ // Block any malformed non-http(s) URIs. WebView will already ignore things like market: URLs,
+ // but not in all cases (malformed market: URIs, such as market:://... will still end up here).
+ // (Note: data: URIs are automatically handled by WebView, and won't end up here either.)
+ // file:// URIs are disabled separately by setting WebSettings.setAllowFileAccess()
+ return WebResourceResponse(null, null, null)
+ }
+
+ // WebView always requests a favicon, even though it won't be used anywhere. This check
+ // isn't able to block all favicons (some of them will be loaded using <link rel="shortcut icon">
+ // with a custom URL which we can't match or detect), but reduces the amount of unnecessary
+ // favicon loading that's performed.
+ if (path != null && path.endsWith("/favicon.ico")) {
+ return WebResourceResponse(null, null, null)
+ }
+
+ val (matches, stringCategory) = getOrCreateUrlMatcher(resources, it).matches(
+ resourceUri,
+ Uri.parse(session?.currentUrl),
+ )
+
+ if (!request.isForMainFrame && matches) {
+ session?.internalNotifyObservers {
+ val matchedCategories = stringCategory.toTrackingProtectionCategories()
+ onTrackerBlocked(
+ Tracker(
+ resourceUri.toString(),
+ matchedCategories,
+ ),
+ )
+ }
+ return WebResourceResponse(null, null, null)
+ }
+ }
+
+ val isRedirect = if (SDK_INT >= Build.VERSION_CODES.N) {
+ request.isRedirect
+ } else {
+ false
+ }
+
+ session?.let { session ->
+ session.settings.requestInterceptor?.let { interceptor ->
+ interceptor.onLoadRequest(
+ session,
+ request.url.toString(),
+ session.currentUrl,
+ request.hasGesture(),
+ session.currentUrl.tryGetHostFromUrl() == request.url.host,
+ isRedirect,
+ false,
+ request.isForMainFrame,
+ )?.apply {
+ return when (this) {
+ is InterceptionResponse.Content ->
+ WebResourceResponse(mimeType, encoding, data.byteInputStream())
+ is InterceptionResponse.Url -> {
+ view.post { view.loadUrl(url) }
+ super.shouldInterceptRequest(view, request)
+ }
+ is InterceptionResponse.AppIntent -> {
+ if (request.isForMainFrame) {
+ session.notifyObservers {
+ onLaunchIntentRequest(url = url, appIntent = appIntent)
+ }
+ }
+
+ super.shouldInterceptRequest(view, request)
+ }
+
+ is InterceptionResponse.Deny -> super.shouldInterceptRequest(view, request)
+ }
+ }
+ }
+ }
+
+ if (request.isForMainFrame) {
+ session?.let {
+ it.notifyObservers {
+ onLoadRequest(request.url.toString(), request.hasGesture(), true)
+ }
+ }
+ }
+
+ return super.shouldInterceptRequest(view, request)
+ }
+
+ override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
+ handler.cancel()
+ session?.let { session ->
+ session.settings.requestInterceptor?.onErrorRequest(
+ session,
+ ErrorType.ERROR_SECURITY_SSL,
+ error.url,
+ )?.apply {
+ view.loadUrl(this.uri)
+ }
+ }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onReceivedError(view: WebView, errorCode: Int, description: String?, failingUrl: String?) {
+ session?.let { session ->
+ val errorType = SystemEngineSession.webViewErrorToErrorType(errorCode)
+ session.settings.requestInterceptor?.onErrorRequest(
+ session,
+ errorType,
+ failingUrl,
+ )?.apply {
+ view.loadUrl(this.uri)
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
+ session?.let { session ->
+ if (!request.isForMainFrame) {
+ return
+ }
+ val errorType = SystemEngineSession.webViewErrorToErrorType(error.errorCode)
+ session.settings.requestInterceptor?.onErrorRequest(
+ session,
+ errorType,
+ request.url.toString(),
+ )?.apply {
+ view.loadUrl(this.uri)
+ }
+ }
+ }
+
+ override fun onReceivedHttpAuthRequest(view: WebView, handler: HttpAuthHandler, host: String, realm: String) {
+ val session = session ?: return handler.cancel()
+
+ val formattedUrl = session.currentUrl.toUri().let { uri ->
+ "${uri.scheme ?: "http"}://${uri.host ?: host}"
+ }
+
+ // Trim obnoxiously long realms.
+ val trimmedRealm = if (realm.length > MAX_REALM_LENGTH) {
+ realm.substring(0, MAX_REALM_LENGTH) + "\u2026"
+ } else {
+ realm
+ }
+
+ val message = if (trimmedRealm.isEmpty()) {
+ context.getString(R.string.mozac_browser_engine_system_auth_no_realm_message, formattedUrl)
+ } else {
+ context.getString(R.string.mozac_browser_engine_system_auth_message, trimmedRealm, formattedUrl)
+ }
+
+ val credentials = view.getAuthCredentials(host, realm)
+ val userName = credentials.first
+ val password = credentials.second
+
+ session.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Authentication(
+ formattedUrl,
+ "",
+ message,
+ userName,
+ password,
+ PromptRequest.Authentication.Method.HOST,
+ PromptRequest.Authentication.Level.NONE,
+ onConfirm = { user, pass -> handler.proceed(user, pass) },
+ onDismiss = { handler.cancel() },
+ ),
+ )
+ }
+ }
+ }
+
+ @Suppress("ComplexMethod")
+ private fun createWebChromeClient() = object : WebChromeClient() {
+ override fun getVisitedHistory(callback: ValueCallback<Array<String>>) {
+ // TODO private browsing not supported for SystemEngine
+ // https://github.com/mozilla-mobile/android-components/issues/649
+ session?.settings?.historyTrackingDelegate?.let {
+ runBlocking {
+ callback.onReceiveValue(it.getVisited().toTypedArray())
+ }
+ }
+ }
+
+ override fun onProgressChanged(view: WebView?, newProgress: Int) {
+ session?.internalNotifyObservers { onProgress(newProgress) }
+ }
+
+ override fun onReceivedTitle(view: WebView, title: String?) {
+ val titleOrEmpty = title ?: ""
+ // TODO private browsing not supported for SystemEngine
+ // https://github.com/mozilla-mobile/android-components/issues/649
+ session?.currentUrl?.takeIf { it.isNotEmpty() }?.let { url ->
+ session?.settings?.historyTrackingDelegate?.let { delegate ->
+ runBlocking {
+ delegate.onTitleChanged(url, titleOrEmpty)
+ }
+ }
+ }
+ session?.internalNotifyObservers {
+ onTitleChange(titleOrEmpty)
+ onNavigationStateChange(view.canGoBack(), view.canGoForward())
+ }
+ }
+
+ override fun onShowCustomView(view: View, callback: CustomViewCallback) {
+ addFullScreenView(view, callback)
+ session?.internalNotifyObservers { onFullScreenChange(true) }
+ }
+
+ override fun onHideCustomView() {
+ removeFullScreenView()
+ session?.internalNotifyObservers { onFullScreenChange(false) }
+ }
+
+ override fun onPermissionRequestCanceled(request: PermissionRequest) {
+ session?.internalNotifyObservers { onCancelContentPermissionRequest(SystemPermissionRequest(request)) }
+ }
+
+ override fun onPermissionRequest(request: PermissionRequest) {
+ session?.internalNotifyObservers { onContentPermissionRequest(SystemPermissionRequest(request)) }
+ }
+
+ override fun onJsAlert(view: WebView, url: String?, message: String?, result: JsResult): Boolean {
+ val session = session ?: return applyDefaultJsDialogBehavior(result)
+
+ // When an alert is triggered from a iframe, url is equals to about:blank, using currentUrl as a fallback.
+ val safeUrl = if (url.isNullOrBlank()) {
+ session.currentUrl
+ } else {
+ if (url.contains("about")) session.currentUrl else url
+ }
+
+ val title = context.getString(R.string.mozac_browser_engine_system_alert_title, safeUrl)
+
+ val onDismiss: () -> Unit = {
+ result.cancel()
+ }
+
+ val onConfirm: (Boolean) -> Unit = { _ -> result.confirm() }
+
+ session.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Alert(
+ title,
+ message ?: "",
+ false,
+ onConfirm,
+ onDismiss,
+ ),
+ )
+ }
+ return true
+ }
+
+ override fun onJsPrompt(
+ view: WebView?,
+ url: String?,
+ message: String?,
+ defaultValue: String?,
+ result: JsPromptResult,
+ ): Boolean {
+ val session = session ?: return applyDefaultJsDialogBehavior(result)
+
+ val title = context.getString(R.string.mozac_browser_engine_system_alert_title, url ?: session.currentUrl)
+
+ val onDismiss: () -> Unit = {
+ result.cancel()
+ }
+
+ val onConfirm: (Boolean, String) -> Unit = { _, valueInput ->
+ result.confirm(valueInput)
+ }
+
+ session.notifyObservers {
+ onPromptRequest(
+ PromptRequest.TextPrompt(
+ title,
+ message ?: "",
+ defaultValue ?: "",
+ false,
+ onConfirm,
+ onDismiss,
+ ),
+ )
+ }
+ return true
+ }
+
+ override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult): Boolean {
+ val session = session ?: return applyDefaultJsDialogBehavior(result)
+ val title = context.getString(R.string.mozac_browser_engine_system_alert_title, url ?: session.currentUrl)
+
+ val onDismiss: () -> Unit = {
+ result.cancel()
+ }
+
+ val onConfirmPositiveButton: (Boolean) -> Unit = { _ ->
+ result.confirm()
+ }
+
+ val onConfirmNegativeButton: (Boolean) -> Unit = { _ ->
+ result.cancel()
+ }
+
+ session.notifyObservers {
+ onPromptRequest(
+ PromptRequest.Confirm(
+ title,
+ message ?: "",
+ false,
+ "",
+ "",
+ "",
+ onConfirmPositiveButton,
+ onConfirmNegativeButton,
+ {},
+ onDismiss,
+ ),
+ )
+ }
+ return true
+ }
+
+ override fun onShowFileChooser(
+ webView: WebView?,
+ filePathCallback: ValueCallback<Array<Uri>>?,
+ fileChooserParams: FileChooserParams?,
+ ): Boolean {
+ var mimeTypes = fileChooserParams?.acceptTypes ?: arrayOf()
+
+ if (mimeTypes.isNotEmpty() && mimeTypes.first().isNullOrEmpty()) {
+ mimeTypes = arrayOf()
+ }
+
+ val isMultipleFilesSelection = fileChooserParams?.mode == MODE_OPEN_MULTIPLE
+
+ val captureMode = if (fileChooserParams?.isCaptureEnabled == true) {
+ PromptRequest.File.FacingMode.ANY
+ } else {
+ PromptRequest.File.FacingMode.NONE
+ }
+
+ val onSelectMultiple: (Context, Array<Uri>) -> Unit = { _, uris ->
+ filePathCallback?.onReceiveValue(uris)
+ }
+
+ val onSelectSingle: (Context, Uri) -> Unit = { _, uri ->
+ filePathCallback?.onReceiveValue(arrayOf(uri))
+ }
+
+ val onDismiss: () -> Unit = {
+ filePathCallback?.onReceiveValue(null)
+ }
+
+ session?.notifyObservers {
+ onPromptRequest(
+ PromptRequest.File(
+ mimeTypes,
+ isMultipleFilesSelection,
+ captureMode,
+ onSelectSingle,
+ onSelectMultiple,
+ onDismiss,
+ ),
+ )
+ }
+
+ return true
+ }
+
+ override fun onCreateWindow(
+ view: WebView,
+ isDialog: Boolean,
+ isUserGesture: Boolean,
+ resultMsg: Message?,
+ ): Boolean {
+ session?.internalNotifyObservers {
+ val newEngineSession = SystemEngineSession(context, session?.settings)
+ onWindowRequest(
+ SystemWindowRequest(
+ view,
+ newEngineSession,
+ NestedWebView(context),
+ isDialog,
+ isUserGesture,
+ resultMsg,
+ ),
+ )
+ }
+ return true
+ }
+
+ override fun onCloseWindow(window: WebView) {
+ session?.internalNotifyObservers {
+ onWindowRequest(SystemWindowRequest(window, type = WindowRequest.Type.CLOSE))
+ }
+ }
+ }
+
+ internal fun createDownloadListener(): DownloadListener {
+ return DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
+ session?.internalNotifyObservers {
+ val fileName = DownloadUtils.guessFileName(contentDisposition, null, url, mimetype)
+ val cookie = CookieManager.getInstance().getCookie(url)
+ onExternalResource(url, fileName, contentLength, mimetype, cookie, userAgent)
+ }
+ }
+ }
+
+ internal fun createFindListener(): WebView.FindListener {
+ return WebView.FindListener { activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean ->
+ session?.internalNotifyObservers {
+ onFindResult(activeMatchOrdinal, numberOfMatches, isDoneCounting)
+ }
+ }
+ }
+
+ internal fun handleLongClick(type: Int, extra: String): Boolean {
+ val result: HitResult? = when (type) {
+ EMAIL_TYPE -> {
+ HitResult.EMAIL(extra)
+ }
+ GEO_TYPE -> {
+ HitResult.GEO(extra)
+ }
+ PHONE_TYPE -> {
+ HitResult.PHONE(extra)
+ }
+ IMAGE_TYPE -> {
+ HitResult.IMAGE(extra)
+ }
+ SRC_ANCHOR_TYPE -> {
+ HitResult.UNKNOWN(extra)
+ }
+ SRC_IMAGE_ANCHOR_TYPE -> {
+ // HitTestResult.getExtra() contains only the image URL, and not the link
+ // URL. Internally, WebView's HitTestData contains both, but they only
+ // make it available via requestFocusNodeHref...
+ val message = Message()
+ message.target = ImageHandler(session)
+ session?.webView?.requestFocusNodeHref(message)
+ null
+ }
+ else -> null
+ }
+ result?.let {
+ session?.internalNotifyObservers { onLongPress(it) }
+ return true
+ }
+ return false
+ }
+
+ internal fun addFullScreenView(view: View, callback: WebChromeClient.CustomViewCallback) {
+ val webView = findViewWithTag<WebView>("mozac_system_engine_webview")
+ val layoutParams = FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ )
+ webView?.apply { this.visibility = View.INVISIBLE }
+
+ session?.fullScreenCallback = callback
+
+ view.tag = "mozac_system_engine_fullscreen"
+ addView(view, layoutParams)
+ }
+
+ internal fun removeFullScreenView() {
+ val view = findViewWithTag<View>("mozac_system_engine_fullscreen")
+ val webView = findViewWithTag<WebView>("mozac_system_engine_webview")
+ view?.let {
+ webView?.apply { this.visibility = View.VISIBLE }
+ removeView(view)
+ }
+ }
+
+ // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/8514
+ @Suppress("DEPRECATION")
+ class ImageHandler(val session: SystemEngineSession?) : Handler() {
+ override fun handleMessage(msg: Message) {
+ val url = msg.data.getString("url")
+ val src = msg.data.getString("src")
+
+ if (url == null || src == null) {
+ throw IllegalStateException("WebView did not supply url or src for image link")
+ }
+
+ session?.internalNotifyObservers { onLongPress(HitResult.IMAGE_SRC(src, url)) }
+ }
+ }
+
+ override fun setVerticalClipping(clippingHeight: Int) {
+ // no-op
+ }
+
+ override fun setDynamicToolbarMaxHeight(height: Int) {
+ // no-op
+ }
+
+ override fun setActivityContext(context: Context?) {
+ // no-op
+ }
+
+ override fun canScrollVerticallyUp() = session?.webView?.canScrollVertically(-1) ?: false
+
+ override fun canScrollVerticallyDown() = session?.webView?.canScrollVertically(1) ?: false
+
+ override fun getInputResultDetail(): InputResultDetail {
+ return (session?.webView as? NestedWebView)?.inputResultDetail
+ ?: InputResultDetail.newInstance()
+ }
+
+ override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) {
+ val webView = session?.webView
+ if (webView == null) {
+ onFinish(null)
+ return
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ createThumbnailUsingDrawingView(webView, onFinish)
+ } else {
+ createThumbnailUsingPixelCopy(webView, onFinish)
+ }
+ }
+
+ override fun clearSelection() {
+ // no-op
+ }
+
+ private fun createThumbnailUsingDrawingView(view: View, onFinish: (Bitmap?) -> Unit) {
+ val outBitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(outBitmap)
+ view.draw(canvas)
+ onFinish(outBitmap)
+ }
+
+ @TargetApi(Build.VERSION_CODES.O)
+ private fun createThumbnailUsingPixelCopy(view: View, onFinish: (Bitmap?) -> Unit) {
+ val out = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
+ val viewRect = view.getRectWithViewLocation()
+ val window = (context as Activity).window
+
+ PixelCopy.request(
+ window,
+ viewRect,
+ out,
+ { copyResult ->
+ val result = if (copyResult == PixelCopy.SUCCESS) out else null
+ onFinish(result)
+ },
+ handler,
+ )
+ }
+
+ private fun applyDefaultJsDialogBehavior(result: JsResult?): Boolean {
+ result?.cancel()
+ return true
+ }
+
+ @Suppress("Deprecation")
+ private fun WebView.getAuthCredentials(host: String, realm: String): Pair<String, String> {
+ val credentials = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ session?.webViewDatabase(context)?.getHttpAuthUsernamePassword(host, realm)
+ } else {
+ this.getHttpAuthUsernamePassword(host, realm)
+ }
+
+ var credentialsPair = "" to ""
+
+ if (!credentials.isNullOrEmpty() && credentials.size == 2) {
+ val user = credentials[0] ?: ""
+ val pass = credentials[1] ?: ""
+
+ credentialsPair = user to pass
+ }
+ return credentialsPair
+ }
+
+ companion object {
+
+ // Maximum number of successive dialogs before we prompt users to disable dialogs.
+ internal const val MAX_SUCCESSIVE_DIALOG_COUNT: Int = 2
+
+ // Minimum time required between dialogs in seconds before enabling the stop dialog.
+ internal const val MAX_SUCCESSIVE_DIALOG_SECONDS_LIMIT: Int = 3
+
+ // Maximum realm length to be shown in authentication dialog.
+ internal const val MAX_REALM_LENGTH: Int = 50
+
+ // Number of milliseconds in 1 second.
+ internal const val SECOND_MS: Int = 1000
+
+ @Volatile
+ internal var URL_MATCHER: UrlMatcher? = null
+
+ private val urlMatcherCategoryMap = mapOf(
+ UrlMatcher.ADVERTISING to TrackingProtectionPolicy.TrackingCategory.AD,
+ UrlMatcher.ANALYTICS to TrackingProtectionPolicy.TrackingCategory.ANALYTICS,
+ UrlMatcher.CONTENT to TrackingProtectionPolicy.TrackingCategory.CONTENT,
+ UrlMatcher.SOCIAL to TrackingProtectionPolicy.TrackingCategory.SOCIAL,
+ UrlMatcher.CRYPTOMINING to TrackingProtectionPolicy.TrackingCategory.CRYPTOMINING,
+ UrlMatcher.FINGERPRINTING to TrackingProtectionPolicy.TrackingCategory.FINGERPRINTING,
+ )
+
+ private fun String?.toTrackingProtectionCategories(): List<TrackingProtectionPolicy.TrackingCategory> {
+ val category = urlMatcherCategoryMap[this]
+ return if (category != null) {
+ listOf(category)
+ } else {
+ emptyList()
+ }
+ }
+
+ @Synchronized
+ internal fun getOrCreateUrlMatcher(resources: Resources, policy: TrackingProtectionPolicy): UrlMatcher {
+ val categories = urlMatcherCategoryMap.filterValues { policy.contains(it) }.keys
+
+ URL_MATCHER?.setCategoriesEnabled(categories) ?: run {
+ URL_MATCHER = UrlMatcher.createMatcher(
+ resources,
+ R.raw.domain_blocklist,
+ R.raw.domain_safelist,
+ categories,
+ )
+ }
+
+ return URL_MATCHER as UrlMatcher
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/ReversibleString.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/ReversibleString.kt
new file mode 100644
index 0000000000..5625fda9f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/ReversibleString.kt
@@ -0,0 +1,98 @@
+/* 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.browser.engine.system.matcher
+
+/**
+ * A String wrapper utility that allows for efficient string reversal. We
+ * regularly need to reverse strings. The standard way of doing this in Java
+ * would be to copy the string to reverse (e.g. using StringBuffer.reverse()).
+ * This seems wasteful when we only read our Strings character by character,
+ * in which case can just transpose positions as needed.
+ */
+abstract class ReversibleString private constructor(
+ protected val string: String,
+ protected val offsetStart: Int,
+ protected val offsetEnd: Int,
+) {
+ abstract val isReversed: Boolean
+ abstract fun charAt(position: Int): Char
+ abstract fun substring(startIndex: Int): ReversibleString
+
+ init {
+ if (offsetStart > offsetEnd || offsetStart < 0 || offsetEnd < 0) {
+ throw StringIndexOutOfBoundsException("Cannot create negative-length String")
+ }
+ }
+
+ /**
+ * Returns the length of this string.
+ */
+ fun length(): Int = offsetEnd - offsetStart
+
+ /**
+ * Reverses this string.
+ */
+ fun reverse(): ReversibleString =
+ if (isReversed) {
+ ForwardString(string, offsetStart, offsetEnd)
+ } else {
+ ReverseString(string, offsetStart, offsetEnd)
+ }
+
+ private class ForwardString(
+ string: String,
+ offsetStart: Int,
+ offsetEnd: Int,
+ ) : ReversibleString(string, offsetStart, offsetEnd) {
+ override val isReversed: Boolean = false
+
+ override fun charAt(position: Int): Char {
+ if (position > length()) {
+ throw StringIndexOutOfBoundsException()
+ }
+ return string[position + offsetStart]
+ }
+
+ override fun substring(startIndex: Int): ReversibleString {
+ return ForwardString(string, offsetStart + startIndex, offsetEnd)
+ }
+ }
+
+ private class ReverseString(
+ string: String,
+ offsetStart: Int,
+ offsetEnd: Int,
+ ) : ReversibleString(string, offsetStart, offsetEnd) {
+ override val isReversed: Boolean = true
+
+ override fun charAt(position: Int): Char {
+ if (position > length()) {
+ throw StringIndexOutOfBoundsException()
+ }
+ return string[length() - 1 - position + offsetStart]
+ }
+
+ override fun substring(startIndex: Int): ReversibleString {
+ return ReverseString(string, offsetStart, offsetEnd - startIndex)
+ }
+ }
+
+ companion object {
+ /**
+ * Create a [ReversibleString] for the provided [String].
+ */
+ fun create(string: String): ReversibleString {
+ return ForwardString(string, 0, string.length)
+ }
+ }
+}
+
+fun String.reversible(): ReversibleString {
+ return ReversibleString.create(this)
+}
+
+fun String.reverse(): ReversibleString {
+ return this.reversible().reverse()
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Safelist.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Safelist.kt
new file mode 100644
index 0000000000..d75d9966ec
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Safelist.kt
@@ -0,0 +1,176 @@
+/* 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.browser.engine.system.matcher
+
+import android.net.Uri
+import android.text.TextUtils
+import android.util.JsonReader
+import java.util.ArrayList
+
+/**
+ * Stores safe-listed URIs for individual hosts.
+ */
+internal class Safelist {
+ private val rootNode: SafelistTrie = SafelistTrie.createRootNode()
+
+ /**
+ * Adds the provided safelist for the provided host.
+ *
+ * @param host the reversed host URI ("foo.com".reverse())
+ * @param safelist a [Trie] representing the safe-listed URIs
+ */
+ fun put(host: ReversibleString, safelist: Trie) {
+ rootNode.putSafelist(host, safelist)
+ }
+
+ /**
+ * Checks if the given resource is safe-listed for the given host.
+ *
+ * @param host the host URI as string ("foo.com")
+ * @param host the resources URI as string ("bar.com")
+ */
+ fun contains(host: String, resource: String): Boolean {
+ return contains(Uri.parse(host), Uri.parse(resource))
+ }
+
+ /**
+ * Checks if the given resource is safe-listed for the given host.
+ *
+ * @param hostUri the host URI
+ * @param resource the resources URI
+ */
+ fun contains(hostUri: Uri, resource: Uri): Boolean {
+ return if (TextUtils.isEmpty(hostUri.host) || TextUtils.isEmpty(resource.host) || hostUri.scheme == "data") {
+ false
+ } else if (resource.scheme?.isPermittedResourceProtocol() == true &&
+ hostUri.scheme?.isSupportedProtocol() == true
+ ) {
+ contains(hostUri.host!!.reverse(), resource.host!!.reverse(), rootNode)
+ } else {
+ false
+ }
+ }
+
+ private fun contains(site: ReversibleString, resource: ReversibleString, revHostTrie: Trie): Boolean {
+ val next = revHostTrie.children.get(site.charAt(0).code) as? SafelistTrie ?: return false
+
+ if (next.safelist?.findNode(resource) != null) {
+ return true
+ }
+
+ return if (site.length() == 1) false else contains(site.substring(1), resource, next)
+ }
+
+ /**
+ * Check if this String is a valid resource protocol.
+ */
+ private fun String.isPermittedResourceProtocol(): Boolean {
+ return this.startsWith("http") ||
+ this.startsWith("https") ||
+ this.startsWith("file") ||
+ this.startsWith("data") ||
+ this.startsWith("javascript") ||
+ this.startsWith("about")
+ }
+
+ /**
+ * Check if this String is a supported protocol.
+ */
+ private fun String.isSupportedProtocol(): Boolean {
+ return this.isPermittedResourceProtocol() || this.startsWith("error")
+ }
+
+ companion object {
+ /**
+ * Parses json for safe-listed URIs.
+ *
+ * @param reader a JsonReader
+ * @return the safe list.
+ */
+ @Suppress("NestedBlockDepth")
+ fun fromJson(reader: JsonReader): Safelist {
+ val safelist = Safelist()
+ reader.beginObject()
+
+ while (reader.hasNext()) {
+ reader.skipValue()
+ reader.beginObject()
+
+ val safelistTrie = Trie.createRootNode()
+ val propertyList = ArrayList<String>()
+ while (reader.hasNext()) {
+ val itemName = reader.nextName()
+ if (itemName == "properties") {
+ reader.beginArray()
+ while (reader.hasNext()) {
+ propertyList.add(reader.nextString())
+ }
+ reader.endArray()
+ } else if (itemName == "resources") {
+ reader.beginArray()
+ while (reader.hasNext()) {
+ safelistTrie.put(reader.nextString().reverse())
+ }
+ reader.endArray()
+ }
+ }
+ propertyList.forEach { safelist.put(it.reverse(), safelistTrie) }
+ reader.endObject()
+ }
+ reader.endObject()
+ return safelist
+ }
+ }
+}
+
+/**
+ * A [Trie] implementation which stores a safe list (another [Trie]).
+ */
+internal class SafelistTrie private constructor(character: Char, parent: SafelistTrie?) : Trie(character, parent) {
+ var safelist: Trie? = null
+
+ override fun createNode(character: Char, parent: Trie): Trie {
+ return SafelistTrie(character, parent as SafelistTrie)
+ }
+
+ /**
+ * Adds new nodes (recursively) for all chars in the provided string and stores
+ * the provide safelist Trie.
+ *
+ * @param string the string for which a node should be added.
+ * @param safelist the safelist to store.
+ * @return the newly created node or the existing one.
+ */
+ fun putSafelist(string: String, safelist: Trie) {
+ this.putSafelist(string.reversible(), safelist)
+ }
+
+ /**
+ * Adds new nodes (recursively) for all chars in the provided string and stores
+ * the provide safelist Trie.
+ *
+ * @param string the string for which a node should be added.
+ * @param safelist the safelist to store.
+ * @return the newly created node or the existing one.
+ */
+ fun putSafelist(string: ReversibleString, safelist: Trie) {
+ val node = super.put(string) as SafelistTrie
+
+ if (node.safelist != null) {
+ throw IllegalStateException("Safelist already set for node $string")
+ }
+
+ node.safelist = safelist
+ }
+
+ companion object {
+ /**
+ * Creates a new root node.
+ */
+ fun createRootNode(): SafelistTrie {
+ return SafelistTrie(Character.MIN_VALUE, null)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Trie.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Trie.kt
new file mode 100644
index 0000000000..2a46ba1381
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Trie.kt
@@ -0,0 +1,114 @@
+/* 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.browser.engine.system.matcher
+
+import android.util.SparseArray
+
+/**
+ * Simple implementation of a Trie, used for indexing URLs.
+ */
+open class Trie constructor(character: Char, parent: Trie?) {
+ val children = SparseArray<Trie>()
+ private var terminator = false
+
+ init {
+ parent?.children?.put(character.code, this)
+ }
+
+ /**
+ * Finds the node corresponding to the provided string.
+ *
+ * @param string the string to search.
+ * @return the corresponding node if found, otherwise null.
+ */
+ fun findNode(string: String): Trie? {
+ return this.findNode(string.reversible())
+ }
+
+ /**
+ * Finds the node corresponding to the provided string.
+ *
+ * @param string the string to search.
+ * @return the corresponding node if found, otherwise null.
+ */
+ fun findNode(string: ReversibleString): Trie? {
+ var match: Trie? = null
+ if (terminator && (string.length() == 0 || string.charAt(0) == '.')) {
+ // Match found and we're at a domain boundary. This is important, because
+ // we don't want to return on partial domain matches. If the trie node is bar.com,
+ // and the search string is foo-bar.com, we shouldn't match, but
+ // foo.bar.com should match.)
+ match = this
+ } else if (string.length() != 0) {
+ val next = children.get(string.charAt(0).code)
+ match = next?.findNode(string.substring(1))
+ }
+ return match
+ }
+
+ /**
+ * Adds new nodes (recursively) for all chars in the provided string.
+ *
+ * @param string the string for which a node should be added.
+ * @return the newly created node or the existing one.
+ */
+ fun put(string: String): Trie {
+ return this.put(string.reversible())
+ }
+
+ /**
+ * Adds new nodes (recursively) for all chars in the provided string.
+ *
+ * @param string the string for which a node should be added.
+ * @return the newly created node or the existing one.
+ */
+ fun put(string: ReversibleString): Trie {
+ if (string.length() == 0) {
+ terminator = true
+ return this
+ }
+
+ val character = string.charAt(0)
+ val child = put(character)
+ return child.put(string.substring(1))
+ }
+
+ /**
+ * Adds a new node for the provided character if none exists.
+ *
+ * @param character the character for which a node should be added.
+ * @return the newly created node or the existing one.
+ */
+ fun put(character: Char): Trie {
+ val existingChild = children.get(character.code)
+
+ if (existingChild != null) {
+ return existingChild
+ }
+
+ val newChild = createNode(character, this)
+ children.put(character.code, newChild)
+ return newChild
+ }
+
+ /**
+ * Creates a new node for the provided character and parent node.
+ *
+ * @param character the character this node represents
+ * @param parent the parent of this node
+ */
+ open fun createNode(character: Char, parent: Trie): Trie {
+ return Trie(character, parent)
+ }
+
+ companion object {
+ /**
+ * Creates a new root node.
+ */
+ fun createRootNode(): Trie {
+ return Trie(Character.MIN_VALUE, null)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/UrlMatcher.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/UrlMatcher.kt
new file mode 100644
index 0000000000..b496cca756
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/UrlMatcher.kt
@@ -0,0 +1,323 @@
+/* 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.browser.engine.system.matcher
+
+import android.content.Context
+import android.content.res.Resources
+import android.net.Uri
+import android.util.JsonReader
+import androidx.annotation.RawRes
+import java.io.InputStreamReader
+import java.io.Reader
+import java.nio.charset.StandardCharsets.UTF_8
+import java.util.LinkedList
+
+/**
+ * Provides functionality to process categorized URL block/safe lists and match
+ * URLs against these lists.
+ */
+class UrlMatcher {
+ private val categories: MutableMap<String, Trie>
+ internal val enabledCategories = HashSet<String>()
+
+ private val safelist: Safelist?
+ private val previouslyMatched = HashSet<String>()
+ private val previouslyUnmatched = HashSet<String>()
+
+ constructor(patterns: Array<String>) {
+ categories = HashMap()
+ safelist = null
+
+ val defaultCategory = Trie.createRootNode()
+ patterns.forEach { defaultCategory.put(it.reverse()) }
+ categories[DEFAULT] = defaultCategory
+ enabledCategories.add(DEFAULT)
+ }
+
+ internal constructor(
+ enabledCategories: Set<String>,
+ supportedCategories: Set<String>,
+ categoryMap: MutableMap<String, Trie>,
+ safelist: Safelist? = null,
+ ) {
+ this.safelist = safelist
+ this.categories = categoryMap
+
+ for ((key) in categoryMap) {
+ if (!supportedCategories.contains(key)) {
+ throw IllegalArgumentException("$key categoryMap contains undeclared category")
+ }
+ }
+
+ enabledCategories.forEach { setCategoryEnabled(it, true) }
+ }
+
+ /**
+ * Enables the provided categories.
+ *
+ * @param categories set of categories to enable.
+ */
+ fun setCategoriesEnabled(categories: Set<String>) {
+ if (enabledCategories != categories) {
+ enabledCategories.removeAll { it != DEFAULT }
+ categories.forEach { setCategoryEnabled(it, true) }
+ }
+ }
+
+ internal fun setCategoryEnabled(category: String, enabled: Boolean) {
+ if (enabled) {
+ if (enabledCategories.contains(category)) {
+ return
+ } else {
+ enabledCategories.add(category)
+ previouslyUnmatched.clear()
+ }
+ } else {
+ if (!enabledCategories.contains(category)) {
+ return
+ } else {
+ enabledCategories.remove(category)
+ previouslyMatched.clear()
+ }
+ }
+ }
+
+ /**
+ * Checks if the given page URI is blocklisted for the given resource URI.
+ * Returns true if the site (page URI) is allowed to access
+ * the resource URI, otherwise false.
+ *
+ * @param resourceURI URI of a resource to be loaded by the page
+ * @param pageURI URI of the page
+ * @return a [Pair] of <Boolean, String?> the first indicates, if the URI matches and the second
+ * indicates the category of the match if available otherwise null.
+ */
+ fun matches(resourceURI: String, pageURI: String): Pair<Boolean, String?> {
+ return matches(Uri.parse(resourceURI), Uri.parse(pageURI))
+ }
+
+ /**
+ * Checks if the given page URI is blocklisted for the given resource URI.
+ * Returns true if the site (page URI) is allowed to access
+ * the resource URI, otherwise false.
+ *
+ * @param resourceURI URI of a resource to be loaded by the page
+ * @param pageURI URI of the page
+ * @return a [Pair] of <Boolean, String?> the first indicates, if the URI matches and the second
+ * indicates the category of the match if available otherwise null.
+ */
+ @Suppress("ReturnCount", "ComplexMethod")
+ fun matches(resourceURI: Uri, pageURI: Uri): Pair<Boolean, String?> {
+ val resourceURLString = resourceURI.toString()
+ val resourceHost = resourceURI.host
+ val pageHost = pageURI.host
+ val notMatchesFound = false to null
+
+ if (previouslyUnmatched.contains(resourceURLString)) {
+ return notMatchesFound
+ }
+
+ if (safelist?.contains(pageURI, resourceURI) == true) {
+ return notMatchesFound
+ }
+
+ if (pageHost != null && pageHost == resourceHost) {
+ return notMatchesFound
+ }
+
+ if (previouslyMatched.contains(resourceURLString)) {
+ return true to null
+ }
+
+ if (resourceHost == null) {
+ return notMatchesFound
+ }
+
+ for ((key, value) in categories) {
+ if (enabledCategories.contains(key) && value.findNode(resourceHost.reverse()) != null) {
+ previouslyMatched.add(resourceURLString)
+ return true to key
+ }
+ }
+
+ previouslyUnmatched.add(resourceURLString)
+ return notMatchesFound
+ }
+
+ companion object {
+ const val ADVERTISING = "Advertising"
+ const val ANALYTICS = "Analytics"
+ const val CONTENT = "Content"
+ const val SOCIAL = "Social"
+ const val DEFAULT = "default"
+ const val CRYPTOMINING = "Cryptomining"
+ const val FINGERPRINTING = "Fingerprinting"
+
+ private val ignoredCategories = setOf("Legacy Disconnect", "Legacy Content")
+ private val webfontExtensions = arrayOf(".woff2", ".woff", ".eot", ".ttf", ".otf")
+ private val supportedCategories = setOf(
+ ADVERTISING,
+ ANALYTICS,
+ SOCIAL,
+ CONTENT,
+ CRYPTOMINING,
+ FINGERPRINTING,
+ )
+
+ /**
+ * Creates a new matcher instance for the provided URL lists.
+ *
+ * @deprecated Pass resources directly
+ * @param blocklistFile resource ID to a JSON file containing the block list
+ * @param safelistFile resource ID to a JSON file containing the safe list
+ */
+ fun createMatcher(
+ context: Context,
+ @RawRes blocklistFile: Int,
+ @RawRes safelistFile: Int,
+ enabledCategories: Set<String> = supportedCategories,
+ ): UrlMatcher =
+ createMatcher(context.resources, blocklistFile, safelistFile, enabledCategories)
+
+ /**
+ * Creates a new matcher instance for the provided URL lists.
+ *
+ * @param blocklistFile resource ID to a JSON file containing the block list
+ * @param safelistFile resource ID to a JSON file containing the safe list
+ */
+ fun createMatcher(
+ resources: Resources,
+ @RawRes blocklistFile: Int,
+ @RawRes safelistFile: Int,
+ enabledCategories: Set<String> = supportedCategories,
+ ): UrlMatcher {
+ val blocklistReader = InputStreamReader(resources.openRawResource(blocklistFile), UTF_8)
+ val safelistReader = InputStreamReader(resources.openRawResource(safelistFile), UTF_8)
+ return createMatcher(blocklistReader, safelistReader, enabledCategories)
+ }
+
+ /**
+ * Creates a new matcher instance for the provided URL lists.
+ *
+ * @param block reader containing the block list
+ * @param safe resource ID to a JSON file containing the safe list
+ */
+ fun createMatcher(
+ block: Reader,
+ safe: Reader,
+ enabledCategories: Set<String> = supportedCategories,
+ ): UrlMatcher {
+ val categoryMap = HashMap<String, Trie>()
+
+ JsonReader(block).use {
+ jsonReader ->
+ loadCategories(jsonReader, categoryMap)
+ }
+
+ var safelist: Safelist?
+ JsonReader(safe).use { jsonReader -> safelist = Safelist.fromJson(jsonReader) }
+ return UrlMatcher(enabledCategories, supportedCategories, categoryMap, safelist)
+ }
+
+ /**
+ * Checks if the given URI points to a Web font.
+ * @param uri the URI to check.
+ *
+ * @return true if the URI is a Web font, otherwise false.
+ */
+ fun isWebFont(uri: Uri): Boolean {
+ val path = uri.path ?: return false
+ return webfontExtensions.find { path.endsWith(it) } != null
+ }
+
+ private fun loadCategories(
+ reader: JsonReader,
+ categoryMap: MutableMap<String, Trie>,
+ override: Boolean = false,
+ ): Map<String, Trie> {
+ reader.beginObject()
+
+ while (reader.hasNext()) {
+ val name = reader.nextName()
+ if (name == "categories") {
+ extractCategories(reader, categoryMap, override)
+ } else {
+ reader.skipValue()
+ }
+ }
+
+ reader.endObject()
+ return categoryMap
+ }
+
+ @Suppress("ThrowsCount", "ComplexMethod", "NestedBlockDepth")
+ private fun extractCategories(reader: JsonReader, categoryMap: MutableMap<String, Trie>, override: Boolean) {
+ reader.beginObject()
+
+ val socialOverrides = LinkedList<String>()
+ while (reader.hasNext()) {
+ val categoryName = reader.nextName()
+ when {
+ ignoredCategories.contains(categoryName) -> reader.skipValue()
+ else -> {
+ val categoryTrie: Trie?
+ if (!override) {
+ if (categoryMap.containsKey(categoryName)) {
+ throw IllegalStateException("Cannot insert already loaded category")
+ }
+ categoryTrie = Trie.createRootNode()
+ categoryMap[categoryName] = categoryTrie
+ } else {
+ categoryTrie = categoryMap[categoryName]
+ if (categoryTrie == null) {
+ throw IllegalStateException("Cannot add override items to nonexistent category")
+ }
+ }
+ extractCategory(reader) { url, _ -> categoryTrie.put(url.reverse()) }
+ }
+ }
+ }
+
+ val socialTrie = categoryMap[SOCIAL]
+ if (socialTrie == null && !override) {
+ throw IllegalStateException("Expected social list to exist")
+ }
+ socialOverrides.forEach { socialTrie?.put(it.reverse()) }
+ reader.endObject()
+ }
+
+ private fun extractCategory(reader: JsonReader, callback: (String, String) -> Unit) {
+ reader.beginArray()
+ while (reader.hasNext()) {
+ extractSite(reader, callback)
+ }
+ reader.endArray()
+ }
+
+ private fun extractSite(reader: JsonReader, callback: (String, String) -> Unit) {
+ reader.beginObject()
+ val siteOwner = reader.nextName()
+
+ reader.beginObject()
+ while (reader.hasNext()) {
+ reader.skipValue()
+ val nextToken = reader.peek()
+ if (nextToken.name == "STRING") {
+ // Sometimes there's a "dnt" entry, with unspecified purpose.
+ reader.skipValue()
+ } else {
+ reader.beginArray()
+ while (reader.hasNext()) {
+ callback(reader.nextString(), siteOwner)
+ }
+ reader.endArray()
+ }
+ }
+ reader.endObject()
+
+ reader.endObject()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.kt
new file mode 100644
index 0000000000..0579b359e1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.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.browser.engine.system.permission
+
+import android.webkit.PermissionRequest.RESOURCE_AUDIO_CAPTURE
+import android.webkit.PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID
+import android.webkit.PermissionRequest.RESOURCE_VIDEO_CAPTURE
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.concept.engine.permission.PermissionRequest
+
+/**
+ * WebView-based implementation of [PermissionRequest].
+ *
+ * @property nativeRequest the underlying WebView permission request.
+ */
+class SystemPermissionRequest(private val nativeRequest: android.webkit.PermissionRequest) : PermissionRequest {
+ override val uri: String = nativeRequest.origin.toString()
+ override val id: String = java.util.UUID.randomUUID().toString()
+
+ override val permissions = nativeRequest.resources.map { resource ->
+ permissionsMap.getOrElse(resource) { Permission.Generic(resource) }
+ }
+
+ override fun grant(permissions: List<Permission>) {
+ nativeRequest.grant(permissions.map { it.id }.toTypedArray())
+ }
+
+ override fun reject() {
+ nativeRequest.deny()
+ }
+
+ companion object {
+ val permissionsMap = mapOf(
+ RESOURCE_AUDIO_CAPTURE to Permission.ContentAudioCapture(RESOURCE_AUDIO_CAPTURE),
+ RESOURCE_VIDEO_CAPTURE to Permission.ContentVideoCapture(RESOURCE_VIDEO_CAPTURE),
+ RESOURCE_PROTECTED_MEDIA_ID to Permission.ContentProtectedMediaId(RESOURCE_PROTECTED_MEDIA_ID),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/window/SystemWindowRequest.kt b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/window/SystemWindowRequest.kt
new file mode 100644
index 0000000000..38fd33284b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/window/SystemWindowRequest.kt
@@ -0,0 +1,52 @@
+/* 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.browser.engine.system.window
+
+import android.os.Message
+import android.webkit.WebView
+import mozilla.components.browser.engine.system.SystemEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.window.WindowRequest
+
+/**
+ * WebView-based implementation of [WindowRequest].
+ *
+ * @property webView the WebView from which the request originated.
+ * @property newWebView the WebView to use for opening a new window, may be null for close requests.
+ * @property newEngineSession the new [EngineSession] to handle this request.
+ * @property openAsDialog whether or not the window should be opened as a dialog, defaults to false.
+ * @property triggeredByUser whether or not the request was triggered by the user, defaults to false.
+ * @property resultMsg the message to send to the new WebView, may be null.
+ */
+class SystemWindowRequest(
+ private val webView: WebView,
+ private val newEngineSession: EngineSession? = null,
+ private val newWebView: WebView? = null,
+ val openAsDialog: Boolean = false,
+ val triggeredByUser: Boolean = false,
+ private val resultMsg: Message? = null,
+ override val type: WindowRequest.Type = WindowRequest.Type.OPEN,
+) : WindowRequest {
+
+ override val url: String = ""
+
+ override fun prepare(): EngineSession {
+ requireNotNull(newEngineSession)
+
+ newWebView?.let {
+ (newEngineSession as SystemEngineSession).webView = it
+ }
+ return newEngineSession
+ }
+
+ override fun start() {
+ val message = resultMsg
+ val transport = message?.obj as? WebView.WebViewTransport
+ transport?.let {
+ it.webView = newWebView
+ message.sendToTarget()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_blocklist.json b/mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_blocklist.json
new file mode 100644
index 0000000000..845571cfa0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_blocklist.json
@@ -0,0 +1,11046 @@
+{
+ "license": "Copyright 2010-2019 Disconnect, Inc. / This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. / This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. / You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.",
+ "categories": {
+ "Advertising": [
+ {
+ "2leep.com": {
+ "http://2leep.com/": [
+ "2leep.com"
+ ]
+ }
+ },
+ {
+ "33Across": {
+ "http://33across.com/": [
+ "33across.com"
+ ]
+ }
+ },
+ {
+ "365Media": {
+ "http://365media.com/": [
+ "365media.com"
+ ]
+ }
+ },
+ {
+ "4INFO": {
+ "http://www.4info.com/": [
+ "4info.com",
+ "adhaven.com"
+ ]
+ }
+ },
+ {
+ "4mads": {
+ "http://4mads.com/": [
+ "4mads.com"
+ ]
+ }
+ },
+ {
+ "Abax Interactive": {
+ "http://abaxinteractive.com/": [
+ "abaxinteractive.com"
+ ]
+ }
+ },
+ {
+ "Accelia": {
+ "http://www.accelia.net/": [
+ "accelia.net",
+ "durasite.net"
+ ]
+ }
+ },
+ {
+ "Accordant Media": {
+ "http://www.accordantmedia.com/": [
+ "accordantmedia.com"
+ ]
+ }
+ },
+ {
+ "Acquisio": {
+ "http://www.acquisio.com/": [
+ "acquisio.com",
+ "clickequations.net"
+ ]
+ }
+ },
+ {
+ "Actisens": {
+ "http://www.actisens.com/": [
+ "actisens.com",
+ "gestionpub.com"
+ ]
+ }
+ },
+ {
+ "ActiveConversion": {
+ "http://www.activeconversion.com/": [
+ "activeconversion.com",
+ "activemeter.com"
+ ]
+ }
+ },
+ {
+ "Act-On": {
+ "http://www.act-on.com/": [
+ "act-on.com",
+ "actonsoftware.com"
+ ]
+ }
+ },
+ {
+ "Acuity": {
+ "http://www.acuity.com/": [
+ "acuity.com",
+ "acuityads.com",
+ "acuityplatform.com"
+ ]
+ }
+ },
+ {
+ "AD2ONE": {
+ "http://www.ad2onegroup.com/": [
+ "ad2onegroup.com"
+ ]
+ }
+ },
+ {
+ "Ad4Game": {
+ "http://ad4game.com/": [
+ "ad4game.com"
+ ]
+ }
+ },
+ {
+ "ad6media": {
+ "http://www.ad6media.fr/": [
+ "ad6media.fr"
+ ]
+ }
+ },
+ {
+ "Adabra": {
+ "https://www.adabra.com/": [
+ "adabra.com"
+ ]
+ }
+ },
+ {
+ "Adality": {
+ "http://adality.de/": [
+ "adality.de",
+ "adrtx.net"
+ ]
+ }
+ },
+ {
+ "AdaptiveAds": {
+ "http://www.adaptiveads.com/": [
+ "adaptiveads.com"
+ ]
+ }
+ },
+ {
+ "Adaptly": {
+ "http://adaptly.com/": [
+ "adaptly.com"
+ ]
+ }
+ },
+ {
+ "Adara Media": {
+ "http://www.adaramedia.com/": [
+ "adaramedia.com",
+ "opinmind.com",
+ "yieldoptimizer.com"
+ ]
+ }
+ },
+ {
+ "Adatus": {
+ "http://www.adatus.com/": [
+ "adatus.com"
+ ]
+ }
+ },
+ {
+ "Adbot": {
+ "https://adbot.tw/": [
+ "adbot.tw"
+ ]
+ }
+ },
+ {
+ "Adbrain": {
+ "http://www.adbrain.com/": [
+ "adbrain.com",
+ "adbrn.com"
+ ]
+ }
+ },
+ {
+ "adBrite": {
+ "http://www.adbrite.com/": [
+ "adbrite.com"
+ ]
+ }
+ },
+ {
+ "Adbroker.de": {
+ "http://adbroker.de/": [
+ "adbroker.de"
+ ]
+ }
+ },
+ {
+ "Adchemy": {
+ "http://www.adchemy.com/": [
+ "adchemy.com"
+ ]
+ }
+ },
+ {
+ "AdCirrus": {
+ "http://adcirrus.com/": [
+ "adcirrus.com"
+ ]
+ }
+ },
+ {
+ "Ad Decisive": {
+ "http://www.addecisive.com/": [
+ "a2dfp.net",
+ "addecisive.com"
+ ]
+ }
+ },
+ {
+ "addGloo": {
+ "http://www.addgloo.com/": [
+ "addgloo.com"
+ ]
+ }
+ },
+ {
+ "Addvantage Media": {
+ "http://www.addvantagemedia.com/": [
+ "addvantagemedia.com"
+ ]
+ }
+ },
+ {
+ "Ad Dynamo": {
+ "http://www.addynamo.com/": [
+ "addynamo.com",
+ "addynamo.net"
+ ]
+ }
+ },
+ {
+ "Adelphic": {
+ "https://adelphic.com/": [
+ "adelphic.com",
+ "ipredictive.com"
+ ]
+ }
+ },
+ {
+ "AdEngage": {
+ "http://adengage.com/": [
+ "adengage.com"
+ ]
+ }
+ },
+ {
+ "AD Europe": {
+ "http://www.adeurope.com/": [
+ "adeurope.com"
+ ]
+ }
+ },
+ {
+ "AdExtent": {
+ "http://www.adextent.com/": [
+ "adextent.com"
+ ]
+ }
+ },
+ {
+ "AdF.ly": {
+ "http://adf.ly/": [
+ "adf.ly"
+ ]
+ }
+ },
+ {
+ "Adfonic": {
+ "http://adfonic.com/": [
+ "adfonic.com"
+ ]
+ }
+ },
+ {
+ "Adforge": {
+ "http://adforgeinc.com/": [
+ "adforgeinc.com"
+ ]
+ }
+ },
+ {
+ "Adform": {
+ "http://www.adform.com/": [
+ "adform.com",
+ "adform.net",
+ "adformdsp.net"
+ ]
+ }
+ },
+ {
+ "AdFox": {
+ "http://adfox.ru/": [
+ "adfox.ru"
+ ]
+ }
+ },
+ {
+ "AdFrontiers": {
+ "http://www.adfrontiers.com/": [
+ "adfrontiers.com"
+ ]
+ }
+ },
+ {
+ "Adfunky": {
+ "http://www.adfunky.com/": [
+ "adfunky.com",
+ "adfunkyserver.com"
+ ]
+ }
+ },
+ {
+ "Adfusion": {
+ "http://www.adfusion.com/": [
+ "adfusion.com"
+ ]
+ }
+ },
+ {
+ "AdGainerSolutions": {
+ "http://adgainersolutions.com/adgainer/": [
+ "adgainersolutions.com"
+ ]
+ }
+ },
+ {
+ "AdGent Digital": {
+ "http://www.adgentdigital.com/": [
+ "adgentdigital.com",
+ "shorttailmedia.com"
+ ]
+ }
+ },
+ {
+ "AdGibbon": {
+ "http://www.adgibbon.com/": [
+ "adgibbon.com"
+ ]
+ }
+ },
+ {
+ "Adglare": {
+ "https://www.adglare.com/": [
+ "adglare.com",
+ "adglare.net"
+ ]
+ }
+ },
+ {
+ "adhood": {
+ "http://www.adhood.com/": [
+ "adhood.com"
+ ]
+ }
+ },
+ {
+ "Adiant": {
+ "http://www.adiant.com/": [
+ "adblade.com",
+ "adiant.com"
+ ]
+ }
+ },
+ {
+ "AdInsight": {
+ "http://www.adinsight.com/": [
+ "adinsight.com",
+ "adinsight.eu"
+ ]
+ }
+ },
+ {
+ "AdIQuity": {
+ "http://adiquity.com/": [
+ "adiquity.com"
+ ]
+ }
+ },
+ {
+ "ADITION": {
+ "http://www.adition.com/": [
+ "adition.com"
+ ]
+ }
+ },
+ {
+ "AdJug": {
+ "http://www.adjug.com/": [
+ "adjug.com"
+ ]
+ }
+ },
+ {
+ "AdJuggler": {
+ "http://www.adjuggler.com/": [
+ "adjuggler.com",
+ "adjuggler.net"
+ ]
+ }
+ },
+ {
+ "Adjust": {
+ "https://adjust.com": [
+ "adjust.com"
+ ]
+ }
+ },
+ {
+ "AdKeeper": {
+ "http://www.adkeeper.com/": [
+ "adkeeper.com",
+ "akncdn.com"
+ ]
+ }
+ },
+ {
+ "AdKernel": {
+ "http://adkernel.com": [
+ "adkernel.com"
+ ]
+ }
+ },
+ {
+ "Ad Knife": {
+ "http://static.adknife.com/": [
+ "adknife.com"
+ ]
+ }
+ },
+ {
+ "Adknowledge": {
+ "http://www.adknowledge.com/": [
+ "adknowledge.com",
+ "adparlor.com",
+ "bidsystem.com",
+ "cubics.com",
+ "lookery.com"
+ ]
+ }
+ },
+ {
+ "AdLantis": {
+ "http://www.adlantis.jp/": [
+ "adimg.net",
+ "adlantis.jp"
+ ]
+ }
+ },
+ {
+ "AdLeave": {
+ "http://www.adleave.com/": [
+ "adleave.com"
+ ]
+ }
+ },
+ {
+ "Adlibrium": {
+ "http://www.adlibrium.com/": [
+ "adlibrium.com"
+ ]
+ }
+ },
+ {
+ "Adlucent": {
+ "http://adlucent.com": [
+ "adlucent.com"
+ ]
+ }
+ },
+ {
+ "Ad Magnet": {
+ "http://www.admagnet.com/": [
+ "admagnet.com",
+ "admagnet.net"
+ ]
+ }
+ },
+ {
+ "Admarketplace": {
+ "http://www.admarketplace.com/": [
+ "admarketplace.com",
+ "admarketplace.net",
+ "ampxchange.com"
+ ]
+ }
+ },
+ {
+ "AdMarvel": {
+ "http://www.admarvel.com/": [
+ "admarvel.com"
+ ]
+ }
+ },
+ {
+ "AdMatrix": {
+ "http://www.admatrix.jp/": [
+ "admatrix.jp"
+ ]
+ }
+ },
+ {
+ "AdMaven": {
+ "https://ad-maven.com/": [
+ "ad-maven.com",
+ "agreensdistra.info",
+ "boudja.com",
+ "rensovetors.info",
+ "wrethicap.info"
+ ]
+ }
+ },
+ {
+ "AdMaximizer Network": {
+ "http://admaximizer.com/": [
+ "admaximizer.com"
+ ]
+ }
+ },
+ {
+ "AdMedia": {
+ "http://www.admedia.com/": [
+ "admedia.com"
+ ]
+ }
+ },
+ {
+ "Admeta": {
+ "http://www.admeta.com/": [
+ "admeta.com",
+ "atemda.com"
+ ]
+ }
+ },
+ {
+ "Admicro": {
+ "http://www.admicro.vn/": [
+ "admicro.vn",
+ "vcmedia.vn"
+ ]
+ }
+ },
+ {
+ "Admixer": {
+ "https://admixer.co.kr/main": [
+ "admixer.co.kr"
+ ]
+ }
+ },
+ {
+ "Admized": {
+ "http://www.admized.com/": [
+ "admized.com"
+ ]
+ }
+ },
+ {
+ "Admobile": {
+ "http://admobile.com/": [
+ "admobile.com"
+ ]
+ }
+ },
+ {
+ "Admotion": {
+ "http://www.admotion.com/": [
+ "admotion.com",
+ "nspmotion.com"
+ ]
+ }
+ },
+ {
+ "Adnetik": {
+ "http://adnetik.com/": [
+ "adnetik.com",
+ "wtp101.com"
+ ]
+ }
+ },
+ {
+ "AdNetwork.net": {
+ "http://www.adnetwork.net/": [
+ "adnetwork.net"
+ ]
+ }
+ },
+ {
+ "Adnium": {
+ "https://adnium.com": [
+ "adnium.com"
+ ]
+ }
+ },
+ {
+ "adnologies": {
+ "http://www.adnologies.com/": [
+ "adnologies.com",
+ "heias.com"
+ ]
+ }
+ },
+ {
+ "Adobe": {
+ "http://www.adobe.com/": [
+ "2o7.net",
+ "auditude.com",
+ "demdex.com",
+ "demdex.net",
+ "dmtracker.com",
+ "efrontier.com",
+ "everestads.net",
+ "everestjs.net",
+ "everesttech.net",
+ "hitbox.com",
+ "omniture.com",
+ "omtrdc.net",
+ "touchclarity.com"
+ ]
+ }
+ },
+ {
+ "AdOcean": {
+ "http://www.adocean-global.com/": [
+ "adocean-global.com",
+ "adocean.pl"
+ ]
+ }
+ },
+ {
+ "Adometry": {
+ "http://www.adometry.com/": [
+ "adometry.com",
+ "dmtry.com"
+ ]
+ }
+ },
+ {
+ "Adomik": {
+ "http://www.adomik.com/": [
+ "adomik.com"
+ ]
+ }
+ },
+ {
+ "AdOnion": {
+ "http://www.adonion.com/": [
+ "adonion.com"
+ ]
+ }
+ },
+ {
+ "Adorika": {
+ "http://www.clickotmedia.com/": [
+ "clickotmedia.com"
+ ]
+ }
+ },
+ {
+ "Adotmob": {
+ "https://adotmob.com/": [
+ "adotmob.com"
+ ]
+ }
+ },
+ {
+ "ADP Dealer Services": {
+ "http://www.adpdealerservices.com/": [
+ "admission.net",
+ "adpdealerservices.com",
+ "cobalt.com"
+ ]
+ }
+ },
+ {
+ "ad pepper media": {
+ "http://www.adpepper.us/": [
+ "adpepper.com",
+ "adpepper.us"
+ ]
+ }
+ },
+ {
+ "AdPerfect": {
+ "http://www.adperfect.com/": [
+ "adperfect.com"
+ ]
+ }
+ },
+ {
+ "Adperium": {
+ "http://www.adperium.com/": [
+ "adperium.com"
+ ]
+ }
+ },
+ {
+ "Adpersia": {
+ "http://www.adpersia.com/": [
+ "adpersia.com"
+ ]
+ }
+ },
+ {
+ "adPrecision": {
+ "http://adprecision.net/": [
+ "adprs.net",
+ "aprecision.net"
+ ]
+ }
+ },
+ {
+ "AdPredictive": {
+ "http://www.adpredictive.com/": [
+ "adpredictive.com"
+ ]
+ }
+ },
+ {
+ "AdReactor": {
+ "http://www.adreactor.com/": [
+ "adreactor.com"
+ ]
+ }
+ },
+ {
+ "AdReady": {
+ "http://www.adready.com/": [
+ "adready.com",
+ "adreadytractions.com"
+ ]
+ }
+ },
+ {
+ "AdRevolution": {
+ "http://adrevolution.com/": [
+ "adrevolution.com"
+ ]
+ }
+ },
+ {
+ "AdRiver": {
+ "http://adriver.ru/": [
+ "adriver.ru"
+ ]
+ }
+ },
+ {
+ "adrolays": {
+ "http://adrolays.com/": [
+ "adrolays.com",
+ "adrolays.de"
+ ]
+ }
+ },
+ {
+ "AdRoll": {
+ "http://www.adroll.com/": [
+ "adroll.com"
+ ]
+ }
+ },
+ {
+ "adscale": {
+ "http://www.adscale.de/": [
+ "adscale.de"
+ ]
+ }
+ },
+ {
+ "Adscience": {
+ "https://www.adscience.nl/": [
+ "adscience.nl"
+ ]
+ }
+ },
+ {
+ "AdServerPub": {
+ "http://www.adserverpub.com/": [
+ "adserverpub.com"
+ ]
+ }
+ },
+ {
+ "AdShuffle": {
+ "http://www.adshuffle.com/": [
+ "adshuffle.com"
+ ]
+ }
+ },
+ {
+ "AdSide": {
+ "http://www.adside.com/": [
+ "adside.com",
+ "doclix.com"
+ ]
+ }
+ },
+ {
+ "AdSpeed": {
+ "http://www.adspeed.com/": [
+ "adspeed.com",
+ "adspeed.net"
+ ]
+ }
+ },
+ {
+ "Adsperity": {
+ "https://www.adsperity.com/": [
+ "adsperity.com"
+ ]
+ }
+ },
+ {
+ "AdSpirit": {
+ "http://www.adspirit.de/": [
+ "adspirit.com",
+ "adspirit.de",
+ "adspirit.net"
+ ]
+ }
+ },
+ {
+ "Adsrevenue.net": {
+ "http://adsrevenue.net/": [
+ "adsrevenue.net"
+ ]
+ }
+ },
+ {
+ "AdStir": {
+ "https://en.ad-stir.com/": [
+ "ad-stir.com"
+ ]
+ }
+ },
+ {
+ "AdsTours": {
+ "http://www.adstours.com/": [
+ "adstours.com",
+ "clickintext.net"
+ ]
+ }
+ },
+ {
+ "Adsty": {
+ "http://adsty.com/": [
+ "adsty.com",
+ "adx1.com"
+ ]
+ }
+ },
+ {
+ "Adsupply": {
+ "http://www.adsupply.com/": [
+ "4dsply.com",
+ "adsupply.com"
+ ]
+ }
+ },
+ {
+ "Adswizz": {
+ "http://adswizz.com": [
+ "adswizz.com"
+ ]
+ }
+ },
+ {
+ "ADTECH": {
+ "http://www.adtech.com/": [
+ "adtech.com",
+ "adtech.de",
+ "adtechus.com"
+ ]
+ }
+ },
+ {
+ "Adtegrity.com": {
+ "http://www.adtegrity.com/": [
+ "adtegrity.com",
+ "adtegrity.net"
+ ]
+ }
+ },
+ {
+ "ADTELLIGENCE": {
+ "http://www.adtelligence.de/": [
+ "adtelligence.de"
+ ]
+ }
+ },
+ {
+ "Adthink": {
+ "https://adthink.com/": [
+ "adthink.com",
+ "audienceinsights.net"
+ ]
+ }
+ },
+ {
+ "AdTiger": {
+ "http://www.adtiger.de/": [
+ "adtiger.de"
+ ]
+ }
+ },
+ {
+ "AdTruth": {
+ "http://adtruth.com/": [
+ "adtruth.com"
+ ]
+ }
+ },
+ {
+ "Adult AdWorld": {
+ "http://adultadworld.com/": [
+ "adultadworld.com"
+ ]
+ }
+ },
+ {
+ "Adultmoda": {
+ "http://www.adultmoda.com/": [
+ "adultmoda.com"
+ ]
+ }
+ },
+ {
+ "Adventive": {
+ "http://adventive.com/": [
+ "adventive.com"
+ ]
+ }
+ },
+ {
+ "Adverline": {
+ "http://www.adverline.com/": [
+ "adnext.fr",
+ "adverline.com"
+ ]
+ }
+ },
+ {
+ "Adversal.com": {
+ "http://www.adversal.com/": [
+ "adv-adserver.com",
+ "adversal.com"
+ ]
+ }
+ },
+ {
+ "Adverticum": {
+ "http://www.adverticum.com/": [
+ "adsmart.com",
+ "adverticum.com",
+ "adverticum.net"
+ ]
+ }
+ },
+ {
+ "Advertise.com": {
+ "http://www.advertise.com/": [
+ "advertise.com"
+ ]
+ }
+ },
+ {
+ "AdvertiseSpace": {
+ "http://www.advertisespace.com/": [
+ "advertisespace.com"
+ ]
+ }
+ },
+ {
+ "Advert Stream": {
+ "http://www.advertstream.com/": [
+ "advertstream.com"
+ ]
+ }
+ },
+ {
+ "Advisor Media": {
+ "http://advisormedia.cz/": [
+ "advisormedia.cz"
+ ]
+ }
+ },
+ {
+ "Adworx": {
+ "http://adworx.at/": [
+ "adworx.at",
+ "adworx.be",
+ "adworx.nl"
+ ]
+ }
+ },
+ {
+ "AdXpansion": {
+ "http://www.adxpansion.com/": [
+ "adxpansion.com"
+ ]
+ }
+ },
+ {
+ "Adxvalue": {
+ "http://adxvalue.com/": [
+ "adxvalue.com",
+ "adxvalue.de"
+ ]
+ }
+ },
+ {
+ "adyard": {
+ "http://adyard.de/": [
+ "adyard.de"
+ ]
+ }
+ },
+ {
+ "AdYield": {
+ "http://www.adyield.com/": [
+ "adxyield.com",
+ "adyield.com"
+ ]
+ }
+ },
+ {
+ "AdYouLike": {
+ "https://www.adyoulike.com/": [
+ "adyoulike.com",
+ "omnitagjs.com",
+ "pulpix.com"
+ ]
+ }
+ },
+ {
+ "ADZ": {
+ "http://www.adzcentral.com/": [
+ "adzcentral.com"
+ ]
+ }
+ },
+ {
+ "Adzerk": {
+ "http://www.adzerk.com/": [
+ "adzerk.com",
+ "adzerk.net"
+ ]
+ }
+ },
+ {
+ "adzly": {
+ "http://www.adzly.com/": [
+ "adzly.com"
+ ]
+ }
+ },
+ {
+ "Aegis Group": {
+ "http://www.aemedia.com/": [
+ "aemedia.com",
+ "bluestreak.com"
+ ]
+ }
+ },
+ {
+ "AERIFY MEDIA": {
+ "http://aerifymedia.com/": [
+ "aerifymedia.com",
+ "anonymous-media.com"
+ ]
+ }
+ },
+ {
+ "Affectv": {
+ "http://affectv.co.uk/": [
+ "affectv.co.uk"
+ ]
+ }
+ },
+ {
+ "affilinet": {
+ "http://www.affili.net/": [
+ "affili.net",
+ "affilinet-inside.de",
+ "banner-rotation.com",
+ "successfultogether.co.uk"
+ ]
+ }
+ },
+ {
+ "Affine": {
+ "http://www.affine.tv/": [
+ "affine.tv",
+ "affinesystems.com"
+ ]
+ }
+ },
+ {
+ "Affinity": {
+ "http://www.affinity.com/": [
+ "affinity.com"
+ ]
+ }
+ },
+ {
+ "AfterDownload": {
+ "http://www.afterdownload.com/": [
+ "afdads.com",
+ "afterdownload.com"
+ ]
+ }
+ },
+ {
+ "Aim4Media": {
+ "http://aim4media.com/": [
+ "aim4media.com"
+ ]
+ }
+ },
+ {
+ "Airpush": {
+ "http://www.airpush.com/": [
+ "airpush.com"
+ ]
+ }
+ },
+ {
+ "AK": {
+ "http://www.aggregateknowledge.com/": [
+ "aggregateknowledge.com",
+ "agkn.com"
+ ]
+ }
+ },
+ {
+ "Akamai": {
+ "http://www.akamai.com/": [
+ "imiclk.com"
+ ]
+ }
+ },
+ {
+ "Albacross": {
+ "https://albacross.com": [
+ "albacross.com"
+ ]
+ }
+ },
+ {
+ "AllStarMediaGroup": {
+ "http://allstarmediagroup.com/": [
+ "allstarmediagroup.com"
+ ]
+ }
+ },
+ {
+ "Aloodo": {
+ "https://aloodo.com/": [
+ "aloodo.com"
+ ]
+ }
+ },
+ {
+ "AlterGeo": {
+ "http://altergeo.ru/": [
+ "altergeo.ru"
+ ]
+ }
+ },
+ {
+ "Amazon.com": {
+ "http://www.amazon.com/": [
+ "amazon-adsystem.com",
+ "amazon.ca",
+ "amazon.co.jp",
+ "amazon.co.uk",
+ "amazon.de",
+ "amazon.es",
+ "amazon.fr",
+ "amazon.it",
+ "assoc-amazon.com"
+ ]
+ }
+ },
+ {
+ "Ambient Digital": {
+ "http://ambientdigital.com.vn/": [
+ "adnetwork.vn",
+ "ambientdigital.com.vn"
+ ]
+ }
+ },
+ {
+ "Amobee": {
+ "http://amobee.com/": [
+ "adconion.com",
+ "amgdgt.com",
+ "amobee.com",
+ "euroclick.com",
+ "smartclip.com",
+ "turn.com"
+ ]
+ }
+ },
+ {
+ "AndBeyond": {
+ "http://andbeyond.media/": [
+ "andbeyond.media"
+ ]
+ }
+ },
+ {
+ "Answers.com": {
+ "http://www.answers.com/": [
+ "dsply.com"
+ ]
+ }
+ },
+ {
+ "AOL": {
+ "http://www.aol.com/": [
+ "adsonar.com",
+ "adtechjp.com",
+ "advertising.com",
+ "aolcloud.net",
+ "atwola.com",
+ "leadback.com",
+ "tacoda.net",
+ "vidible.tv"
+ ]
+ }
+ },
+ {
+ "AppCast": {
+ "https://appcast.io/": [
+ "appcast.io"
+ ]
+ }
+ },
+ {
+ "Appenda": {
+ "http://www.appenda.com/": [
+ "appenda.com"
+ ]
+ }
+ },
+ {
+ "AppFlood": {
+ "http://appflood.com/": [
+ "appflood.com"
+ ]
+ }
+ },
+ {
+ "Appier": {
+ "http://appier.com/": [
+ "appier.com"
+ ]
+ }
+ },
+ {
+ "Applifier": {
+ "http://www.applifier.com/": [
+ "applifier.com"
+ ]
+ }
+ },
+ {
+ "Applovin": {
+ "http://www.applovin.com/": [
+ "applovin.com"
+ ]
+ }
+ },
+ {
+ "AppNexus": {
+ "http://www.appnexus.com/": [
+ "adlantic.nl",
+ "adnxs.com",
+ "adrdgt.com",
+ "alenty.com",
+ "appnexus.com"
+ ]
+ }
+ },
+ {
+ "AppsFlyer": {
+ "http://appsflyer.com/": [
+ "appsflyer.com"
+ ]
+ }
+ },
+ {
+ "appssavvy": {
+ "http://appssavvy.com/": [
+ "appssavvy.com"
+ ]
+ }
+ },
+ {
+ "Arkwrights Homebrew": {
+ "http://www.arkwrightshomebrew.com/": [
+ "arkwrightshomebrew.com",
+ "ctasnet.com"
+ ]
+ }
+ },
+ {
+ "AT Internet": {
+ "http://www.atinternet.com/": [
+ "hit-parade.com"
+ ]
+ }
+ },
+ {
+ "ATN": {
+ "http://affiliatetracking.com/": [
+ "affiliatetracking.com"
+ ]
+ }
+ },
+ {
+ "Atoomic.com": {
+ "http://www.atoomic.com/": [
+ "atoomic.com"
+ ]
+ }
+ },
+ {
+ "Atrinsic": {
+ "http://atrinsic.com/": [
+ "atrinsic.com"
+ ]
+ }
+ },
+ {
+ "AT&T": {
+ "http://www.att.com/": [
+ "att.com",
+ "yp.com"
+ ]
+ }
+ },
+ {
+ "Audience2Media": {
+ "http://www.audience2media.com/": [
+ "audience2media.com"
+ ]
+ }
+ },
+ {
+ "Audience Ad Network": {
+ "http://audienceadnetwork.com/": [
+ "audienceadnetwork.com"
+ ]
+ }
+ },
+ {
+ "AudienceScience": {
+ "http://www.audiencescience.com/": [
+ "audiencescience.com",
+ "revsci.net",
+ "targetingmarketplace.com",
+ "wunderloop.net"
+ ]
+ }
+ },
+ {
+ "Augme": {
+ "http://www.augme.com/": [
+ "augme.com",
+ "hipcricket.com"
+ ]
+ }
+ },
+ {
+ "Augur": {
+ "http://www.augur.io/": [
+ "augur.io"
+ ]
+ }
+ },
+ {
+ "AUTOCENTRE.UA": {
+ "http://www.autocentre.ua/": [
+ "am.ua",
+ "autocentre.ua"
+ ]
+ }
+ },
+ {
+ "Automattic": {
+ "http://automattic.com/": [
+ "pubmine.com"
+ ]
+ }
+ },
+ {
+ "Avalanchers": {
+ "http://www.avalanchers.com/": [
+ "avalanchers.com"
+ ]
+ }
+ },
+ {
+ "AvantLink": {
+ "http://www.avantlink.com/": [
+ "avantlink.com"
+ ]
+ }
+ },
+ {
+ "Avocet": {
+ "https://avocet.io/": [
+ "avocet.io"
+ ]
+ }
+ },
+ {
+ "Avsads": {
+ "http://avsads.com/": [
+ "avsads.com"
+ ]
+ }
+ },
+ {
+ "AWeber": {
+ "http://www.aweber.com/": [
+ "aweber.com"
+ ]
+ }
+ },
+ {
+ "Awin": {
+ "http://www.awin.com/": [
+ "digitalwindow.com",
+ "dwin1.com",
+ "perfiliate.com"
+ ]
+ }
+ },
+ {
+ "Azet": {
+ "http://mediaimpact.sk/": [
+ "azetklik.sk",
+ "rsz.sk"
+ ]
+ }
+ },
+ {
+ "BackBeat Media": {
+ "http://www.backbeatmedia.com/": [
+ "backbeatmedia.com"
+ ]
+ }
+ },
+ {
+ "Bannerconnect": {
+ "http://www.bannerconnect.net/": [
+ "bannerconnect.net"
+ ]
+ }
+ },
+ {
+ "Barilliance": {
+ "http://www.barilliance.com/": [
+ "barilliance.com"
+ ]
+ }
+ },
+ {
+ "BaronsNetworks": {
+ "http://baronsoffers.com/": [
+ "baronsoffers.com"
+ ]
+ }
+ },
+ {
+ "Batanga Network": {
+ "http://www.batanganetwork.com/": [
+ "batanga.com",
+ "batanganetwork.com"
+ ]
+ }
+ },
+ {
+ "BeachFront": {
+ "http://beachfront.com/": [
+ "beachfront.com"
+ ]
+ }
+ },
+ {
+ "Beanstock Media": {
+ "http://www.beanstockmedia.com/": [
+ "beanstockmedia.com"
+ ]
+ }
+ },
+ {
+ "beencounter": {
+ "http://www.beencounter.com/": [
+ "beencounter.com"
+ ]
+ }
+ },
+ {
+ "Begun": {
+ "http://www.begun.ru/": [
+ "begun.ru"
+ ]
+ }
+ },
+ {
+ "belboon": {
+ "http://www.belboon.com/": [
+ "adbutler.de",
+ "belboon.com"
+ ]
+ }
+ },
+ {
+ "Betgenius": {
+ "http://www.betgenius.com/": [
+ "betgenius.com",
+ "connextra.com"
+ ]
+ }
+ },
+ {
+ "BetweenDigital": {
+ "http://betweendigital.com": [
+ "betweendigital.com"
+ ]
+ }
+ },
+ {
+ "Bidfluence": {
+ "https://www.bidfluence.com/": [
+ "bidfluence.com"
+ ]
+ }
+ },
+ {
+ "Bidr": {
+ "http://bidr.io": [
+ "bidr.io"
+ ]
+ }
+ },
+ {
+ "BidSwitch": {
+ "https://www.bidswitch.com/": [
+ "bidswitch.net",
+ "mfadsrvr.com"
+ ]
+ }
+ },
+ {
+ "Bidtellect": {
+ "https://www.bidtellect.com/": [
+ "bidtellect.com",
+ "bttrack.com"
+ ]
+ }
+ },
+ {
+ "BidVertiser": {
+ "http://www.bidvertiser.com/": [
+ "bidvertiser.com"
+ ]
+ }
+ },
+ {
+ "BigClick": {
+ "http://bigclick.me/": [
+ "bgclck.me",
+ "xcvgdf.party"
+ ]
+ }
+ },
+ {
+ "bigmirnet": {
+ "http://www.bigmir.net/": [
+ "bigmir.net"
+ ]
+ }
+ },
+ {
+ "BinLayer": {
+ "http://binlayer.com/": [
+ "binlayer.com"
+ ]
+ }
+ },
+ {
+ "Bitcoin Plus": {
+ "http://www.bitcoinplus.com/": [
+ "bitcoinplus.com"
+ ]
+ }
+ },
+ {
+ "BitMedia": {
+ "https://bitmedia.io/": [
+ "bitmedia.io"
+ ]
+ }
+ },
+ {
+ "BittAds": {
+ "http://www.bittads.com/": [
+ "bittads.com"
+ ]
+ }
+ },
+ {
+ "Bizo": {
+ "http://www.bizo.com/": [
+ "bizo.com",
+ "bizographics.com"
+ ]
+ }
+ },
+ {
+ "Black Label Ads": {
+ "http://www.blacklabelads.com/": [
+ "blacklabelads.com"
+ ]
+ }
+ },
+ {
+ "BlogCatalog": {
+ "http://www.blogcatalog.com/": [
+ "blogcatalog.com"
+ ]
+ }
+ },
+ {
+ "BlogFrog": {
+ "http://theblogfrog.com/": [
+ "theblogfrog.com"
+ ]
+ }
+ },
+ {
+ "BlogHer": {
+ "http://www.blogher.com/": [
+ "blogher.com",
+ "blogherads.com"
+ ]
+ }
+ },
+ {
+ "BlogRollr": {
+ "http://blogrollr.com/": [
+ "blogrollr.com"
+ ]
+ }
+ },
+ {
+ "BLOOM Digital Platforms": {
+ "http://bloom-hq.com/": [
+ "adgear.com",
+ "adgrx.com",
+ "bloom-hq.com"
+ ]
+ }
+ },
+ {
+ "BlueKai": {
+ "http://www.bluekai.com/": [
+ "bkrtx.com",
+ "bluekai.com",
+ "tracksimple.com"
+ ]
+ }
+ },
+ {
+ "Blu Trumpet": {
+ "http://www.blutrumpet.com/": [
+ "blutrumpet.com"
+ ]
+ }
+ },
+ {
+ "Boo-Box": {
+ "http://boo-box.com/": [
+ "boo-box.com"
+ ]
+ }
+ },
+ {
+ "BoostBox": {
+ "https://www.boostbox.com.br/": [
+ "boostbox.com.br"
+ ]
+ }
+ },
+ {
+ "Bouncex": {
+ "https://www.bouncex.com/": [
+ "bounceexchange.com",
+ "bouncex.com",
+ "bouncex.net"
+ ]
+ }
+ },
+ {
+ "Brainient": {
+ "http://brainient.com/": [
+ "brainient.com"
+ ]
+ }
+ },
+ {
+ "Brand Affinity Technologies": {
+ "http://www.brandaffinity.net/": [
+ "brandaffinity.net"
+ ]
+ }
+ },
+ {
+ "Brandcrumb": {
+ "http://www.brandcrumb.com": [
+ "brandcrumb.com"
+ ]
+ }
+ },
+ {
+ "Brand.net": {
+ "http://www.brand.net/": [
+ "brand.net"
+ ]
+ }
+ },
+ {
+ "Brandscreen": {
+ "http://www.brandscreen.com/": [
+ "brandscreen.com",
+ "rtbidder.net"
+ ]
+ }
+ },
+ {
+ "BreakTime": {
+ "https://www.breaktime.com.tw/": [
+ "breaktime.com.tw"
+ ]
+ }
+ },
+ {
+ "BrightRoll": {
+ "http://www.brightroll.com/": [
+ "brightroll.com",
+ "btrll.com"
+ ]
+ }
+ },
+ {
+ "BrightTag": {
+ "http://www.brighttag.com/": [
+ "brighttag.com",
+ "btstatic.com",
+ "thebrighttag.com"
+ ]
+ }
+ },
+ {
+ "Brilig": {
+ "http://www.brilig.com/": [
+ "brilig.com"
+ ]
+ }
+ },
+ {
+ "BuckSense": {
+ "http://www.bucksense.com": [
+ "bucksense.com"
+ ]
+ }
+ },
+ {
+ "Burstly": {
+ "http://www.burstly.com/": [
+ "burstly.com"
+ ]
+ }
+ },
+ {
+ "Burst Media": {
+ "http://www.burstmedia.com/": [
+ "burstbeacon.com",
+ "burstdirectads.com",
+ "burstmedia.com",
+ "burstnet.com",
+ "giantrealm.com"
+ ]
+ }
+ },
+ {
+ "BusinessOnline": {
+ "http://www.businessol.com/": [
+ "businessol.com"
+ ]
+ }
+ },
+ {
+ "Button": {
+ "https://www.usebutton.com": [
+ "usebutton.com"
+ ]
+ }
+ },
+ {
+ "BuySellAds": {
+ "http://buysellads.com/": [
+ "beaconads.com",
+ "buysellads.com"
+ ]
+ }
+ },
+ {
+ "Buysight": {
+ "http://www.buysight.com/": [
+ "buysight.com",
+ "permuto.com",
+ "pulsemgr.com"
+ ]
+ }
+ },
+ {
+ "BuzzParadise": {
+ "http://www.buzzparadise.com/": [
+ "buzzparadise.com"
+ ]
+ }
+ },
+ {
+ "BV! MEDIA": {
+ "http://www.bvmedia.ca/": [
+ "bvmedia.ca",
+ "networldmedia.com",
+ "networldmedia.net"
+ ]
+ }
+ },
+ {
+ "c1exchange": {
+ "https://c1exchange.com/": [
+ "c1exchange.com"
+ ]
+ }
+ },
+ {
+ "C3 Metrics": {
+ "http://c3metrics.com/": [
+ "attributionmodel.com",
+ "c3metrics.com",
+ "c3tag.com"
+ ]
+ }
+ },
+ {
+ "Cadreon": {
+ "http://www.cadreon.com/": [
+ "cadreon.com"
+ ]
+ }
+ },
+ {
+ "CampaignGrid": {
+ "http://www.campaigngrid.com/": [
+ "campaigngrid.com"
+ ]
+ }
+ },
+ {
+ "CAPITALDATA": {
+ "http://www.capitaldata.fr/": [
+ "capitaldata.fr"
+ ]
+ }
+ },
+ {
+ "Carambola": {
+ "https://www.carambola.com/": [
+ "carambo.la"
+ ]
+ }
+ },
+ {
+ "Caraytech": {
+ "http://www.caraytech.com.ar/": [
+ "caraytech.com.ar",
+ "e-planning.net"
+ ]
+ }
+ },
+ {
+ "Cart.ro": {
+ "http://www.cart.ro/": [
+ "cart.ro",
+ "statistics.ro"
+ ]
+ }
+ },
+ {
+ "CartsGuru": {
+ "https://carts.guru/": [
+ "carts.guru"
+ ]
+ }
+ },
+ {
+ "Casale Media": {
+ "http://www.casalemedia.com/": [
+ "casalemedia.com",
+ "medianet.com"
+ ]
+ }
+ },
+ {
+ "CBproADS": {
+ "http://www.cbproads.com/": [
+ "cbproads.com"
+ ]
+ }
+ },
+ {
+ "Cedato": {
+ "https://www.cedato.com/": [
+ "cedato.com"
+ ]
+ }
+ },
+ {
+ "Chango": {
+ "http://www.chango.com/": [
+ "chango.ca",
+ "chango.com"
+ ]
+ }
+ },
+ {
+ "ChannelAdvisor": {
+ "http://www.channeladvisor.com/": [
+ "channeladvisor.com",
+ "searchmarketing.com"
+ ]
+ }
+ },
+ {
+ "Channel Intelligence": {
+ "http://www.channelintelligence.com/": [
+ "channelintelligence.com"
+ ]
+ }
+ },
+ {
+ "Chartboost": {
+ "https://www.chartboost.com/": [
+ "chartboost.com"
+ ]
+ }
+ },
+ {
+ "CheckM8": {
+ "http://www.checkm8.com/": [
+ "checkm8.com"
+ ]
+ }
+ },
+ {
+ "Chitika": {
+ "http://chitika.com/": [
+ "chitika.com",
+ "chitika.net"
+ ]
+ }
+ },
+ {
+ "ChoiceStream": {
+ "http://www.choicestream.com/": [
+ "choicestream.com"
+ ]
+ }
+ },
+ {
+ "ClearLink": {
+ "https://www.clearlink.com/": [
+ "clearlink.com"
+ ]
+ }
+ },
+ {
+ "ClearSaleing": {
+ "http://www.clearsaleing.com/": [
+ "clearsaleing.com",
+ "csdata1.com",
+ "csdata2.com",
+ "csdata3.com"
+ ]
+ }
+ },
+ {
+ "Clearsearch Media": {
+ "http://www.clearsearchmedia.com/": [
+ "clearsearchmedia.com",
+ "csm-secure.com"
+ ]
+ }
+ },
+ {
+ "ClearSight Interactive": {
+ "http://www.clearsightinteractive.com/": [
+ "clearsightinteractive.com",
+ "csi-tracking.com"
+ ]
+ }
+ },
+ {
+ "ClickAider": {
+ "http://clickaider.com/": [
+ "clickaider.com"
+ ]
+ }
+ },
+ {
+ "Clickayab": {
+ "http://www.clickyab.com": [
+ "clickyab.com"
+ ]
+ }
+ },
+ {
+ "Clickbooth": {
+ "http://www.clickbooth.com/": [
+ "adtoll.com",
+ "clickbooth.com"
+ ]
+ }
+ },
+ {
+ "ClickDimensions": {
+ "http://www.clickdimensions.com/": [
+ "clickdimensions.com"
+ ]
+ }
+ },
+ {
+ "ClickDistrict": {
+ "http://www.clickdistrict.com/": [
+ "clickdistrict.com",
+ "creative-serving.com"
+ ]
+ }
+ },
+ {
+ "ClickFrog": {
+ "https://clickfrog.ru/": [
+ "bashirian.biz",
+ "buckridge.link",
+ "clickfrog.ru",
+ "franecki.net",
+ "quitzon.net",
+ "reichelcormier.bid",
+ "wisokykulas.bid"
+ ]
+ }
+ },
+ {
+ "ClickFuel": {
+ "http://clickfuel.com/": [
+ "conversiondashboard.com"
+ ]
+ }
+ },
+ {
+ "ClickInc": {
+ "http://www.clickinc.com/": [
+ "clickinc.com"
+ ]
+ }
+ },
+ {
+ "Clicksor": {
+ "http://www.clicksor.com/": [
+ "clicksor.com",
+ "clicksor.net"
+ ]
+ }
+ },
+ {
+ "Clickwinks": {
+ "http://www.clickwinks.com/": [
+ "clickwinks.com"
+ ]
+ }
+ },
+ {
+ "ClicManager": {
+ "http://www.clicmanager.fr/": [
+ "clicmanager.fr"
+ ]
+ }
+ },
+ {
+ "Clixtell": {
+ "https://www.clixtell.com/": [
+ "clixtell.com"
+ ]
+ }
+ },
+ {
+ "Clove Network": {
+ "http://www.clovenetwork.com/": [
+ "clovenetwork.com"
+ ]
+ }
+ },
+ {
+ "Cognitive Match": {
+ "http://www.cognitivematch.com/": [
+ "cmads.com.tw",
+ "cmadsasia.com",
+ "cmadseu.com",
+ "cmmeglobal.com",
+ "cognitivematch.com"
+ ]
+ }
+ },
+ {
+ "Collective": {
+ "http://collective.com/": [
+ "collective-media.net",
+ "collective.com",
+ "oggifinogi.com",
+ "tumri.com",
+ "tumri.net",
+ "yt1187.net"
+ ]
+ }
+ },
+ {
+ "Commission Junction": {
+ "http://www.cj.com/": [
+ "apmebf.com",
+ "awltovhc.com",
+ "cj.com",
+ "ftjcfx.com",
+ "kcdwa.com",
+ "qksz.com",
+ "qksz.net",
+ "tqlkg.com",
+ "yceml.net"
+ ]
+ }
+ },
+ {
+ "Communicator Corp": {
+ "http://www.communicatorcorp.com/": [
+ "communicatorcorp.com"
+ ]
+ }
+ },
+ {
+ "Compass Labs": {
+ "http://compasslabs.com/": [
+ "compasslabs.com"
+ ]
+ }
+ },
+ {
+ "Complex Media": {
+ "http://www.complexmedianetwork.com/": [
+ "complex.com",
+ "complexmedianetwork.com"
+ ]
+ }
+ },
+ {
+ "comScore": {
+ "http://www.comscore.com/": [
+ "adxpose.com",
+ "proxilinks.com",
+ "proximic.com",
+ "proximic.net"
+ ]
+ }
+ },
+ {
+ "Connatix.com": {
+ "https://connatix.com/": [
+ "connatix.com"
+ ]
+ }
+ },
+ {
+ "Connexity": {
+ "http://www.connexity.com/": [
+ "pricegrabber.com"
+ ]
+ }
+ },
+ {
+ "Consilium Media": {
+ "http://www.consiliummedia.com/": [
+ "consiliummedia.com"
+ ]
+ }
+ },
+ {
+ "Consumable": {
+ "http://consumable.com/": [
+ "consumable.com"
+ ]
+ }
+ },
+ {
+ "CONTAXE": {
+ "http://www.contaxe.com/": [
+ "contaxe.com"
+ ]
+ }
+ },
+ {
+ "ContentABC": {
+ "http://contentabc.com/": [
+ "contentabc.com"
+ ]
+ }
+ },
+ {
+ "CONTEXTin": {
+ "http://www.contextin.com/": [
+ "admailtiser.com",
+ "contextin.com"
+ ]
+ }
+ },
+ {
+ "ContextuAds": {
+ "http://www.contextuads.com/": [
+ "agencytradingdesk.net",
+ "contextuads.com"
+ ]
+ }
+ },
+ {
+ "CONTEXTWEB": {
+ "http://www.contextweb.com/": [
+ "contextweb.com"
+ ]
+ }
+ },
+ {
+ "ConvergeDirect": {
+ "http://www.convergedirect.com/": [
+ "convergedirect.com",
+ "convergetrack.com"
+ ]
+ }
+ },
+ {
+ "ConversantMedia": {
+ "http://conversantmedia.com": [
+ "adserver.com",
+ "conversantmedia.com",
+ "dotomi.com",
+ "dtmpub.com",
+ "emjcd.com",
+ "fastclick.com",
+ "fastclick.net",
+ "greystripe.com",
+ "lduhtrp.net",
+ "mediaplex.com",
+ "valueclick.com",
+ "valueclick.net",
+ "valueclickmedia.com"
+ ]
+ }
+ },
+ {
+ "ConversionRuler": {
+ "http://www.conversionruler.com/": [
+ "conversionruler.com"
+ ]
+ }
+ },
+ {
+ "Conversive": {
+ "http://www.conversive.nl/": [
+ "conversive.nl"
+ ]
+ }
+ },
+ {
+ "CoreMotives": {
+ "http://coremotives.com/": [
+ "coremotives.com"
+ ]
+ }
+ },
+ {
+ "Cox Digital Solutions": {
+ "http://www.coxdigitalsolutions.com/": [
+ "adify.com",
+ "afy11.net",
+ "coxdigitalsolutions.com"
+ ]
+ }
+ },
+ {
+ "CPMStar": {
+ "http://www.cpmstar.com/": [
+ "cpmstar.com"
+ ]
+ }
+ },
+ {
+ "CPX Interactive": {
+ "http://www.cpxinteractive.com/": [
+ "adreadypixels.com",
+ "cpxadroit.com",
+ "cpxinteractive.com"
+ ]
+ }
+ },
+ {
+ "Creafi": {
+ "http://www.creafi.com/": [
+ "creafi.com"
+ ]
+ }
+ },
+ {
+ "Crimtan": {
+ "http://www.crimtan.com/": [
+ "crimtan.com"
+ ]
+ }
+ },
+ {
+ "Crisp Media": {
+ "http://www.crispmedia.com/": [
+ "crispmedia.com"
+ ]
+ }
+ },
+ {
+ "Criteo": {
+ "http://www.criteo.com/": [
+ "criteo.com",
+ "criteo.net",
+ "hlserve.com",
+ "hooklogic.com",
+ "storetail.io"
+ ]
+ }
+ },
+ {
+ "Cross Pixel": {
+ "http://crosspixel.net/": [
+ "crosspixel.net",
+ "crosspixelmedia.com",
+ "crsspxl.com"
+ ]
+ }
+ },
+ {
+ "cXense": {
+ "http://www.cxense.com/": [
+ "cxense.com",
+ "emediate.biz",
+ "emediate.com",
+ "emediate.dk",
+ "emediate.eu"
+ ]
+ }
+ },
+ {
+ "Cyberplex": {
+ "http://www.cyberplex.com/": [
+ "cyberplex.com"
+ ]
+ }
+ },
+ {
+ "Dada": {
+ "http://dada.pro/": [
+ "dada.pro",
+ "simply.com"
+ ]
+ }
+ },
+ {
+ "Datalogix": {
+ "http://www.datalogix.com/": [
+ "nexac.com",
+ "nextaction.net"
+ ]
+ }
+ },
+ {
+ "DataXu": {
+ "http://www.dataxu.com/": [
+ "dataxu.com",
+ "dataxu.net",
+ "mexad.com",
+ "w55c.net"
+ ]
+ }
+ },
+ {
+ "Datonics": {
+ "http://datonics.com/": [
+ "datonics.com",
+ "pro-market.net"
+ ]
+ }
+ },
+ {
+ "Datran Media": {
+ "http://www.datranmedia.com/": [
+ "datranmedia.com",
+ "displaymarketplace.com"
+ ]
+ }
+ },
+ {
+ "Datvantage": {
+ "http://datvantage.com/": [
+ "datvantage.com"
+ ]
+ }
+ },
+ {
+ "DC Storm": {
+ "http://www.dc-storm.com/": [
+ "dc-storm.com",
+ "stormiq.com"
+ ]
+ }
+ },
+ {
+ "Dedicated Media": {
+ "http://www.dedicatedmedia.com/": [
+ "dedicatedmedia.com",
+ "dedicatednetworks.com"
+ ]
+ }
+ },
+ {
+ "Delivr": {
+ "http://delivr.com/": [
+ "delivr.com",
+ "percentmobile.com"
+ ]
+ }
+ },
+ {
+ "Delta Projects": {
+ "http://www.deltaprojects.se/": [
+ "adaction.se",
+ "de17a.com",
+ "deltaprojects.se"
+ ]
+ }
+ },
+ {
+ "Demand Media": {
+ "http://www.demandmedia.com/": [
+ "demandmedia.com",
+ "indieclick.com"
+ ]
+ }
+ },
+ {
+ "Deutsche Post DHL": {
+ "http://www.dp-dhl.com/": [
+ "adcloud.com",
+ "adcloud.net",
+ "dp-dhl.com"
+ ]
+ }
+ },
+ {
+ "Developer Media": {
+ "http://developermedia.com/": [
+ "developermedia.com",
+ "lqcdn.com"
+ ]
+ }
+ },
+ {
+ "DG": {
+ "http://www.dgit.com/": [
+ "dgit.com",
+ "eyeblaster.com",
+ "eyewonder.com",
+ "mdadx.com",
+ "serving-sys.com",
+ "unicast.com"
+ ]
+ }
+ },
+ {
+ "dianomi": {
+ "http://www.dianomi.com/": [
+ "dianomi.com"
+ ]
+ }
+ },
+ {
+ "Didit": {
+ "http://www.didit.com/": [
+ "did-it.com",
+ "didit.com"
+ ]
+ }
+ },
+ {
+ "DigitalAdConsortium": {
+ "https://www.dac.co.jp/": [
+ "impact-ad.jp"
+ ]
+ }
+ },
+ {
+ "Digital River": {
+ "http://www.digitalriver.com/": [
+ "digitalriver.com",
+ "keywordmax.com",
+ "netflame.cc"
+ ]
+ }
+ },
+ {
+ "Digital Target": {
+ "http://digitaltarget.ru": [
+ "digitaltarget.ru"
+ ]
+ }
+ },
+ {
+ "Digitize": {
+ "http://www.digitize.ie/": [
+ "digitize.ie"
+ ]
+ }
+ },
+ {
+ "DirectAdvert": {
+ "http://www.directadvert.ru/": [
+ "directadvert.ru"
+ ]
+ }
+ },
+ {
+ "Direct Response Group": {
+ "http://www.directresponsegroup.com/": [
+ "directresponsegroup.com",
+ "ppctracking.net"
+ ]
+ }
+ },
+ {
+ "Directtrack": {
+ "http://directtrack.com/": [
+ "directtrack.com"
+ ]
+ }
+ },
+ {
+ "Disqus": {
+ "http://disqus.com/": [
+ "disqusads.com"
+ ]
+ }
+ },
+ {
+ "DistrictM": {
+ "https://districtm.net": [
+ "districtm.io"
+ ]
+ }
+ },
+ {
+ "dmpxs": {
+ "http://bob.dmpxs.com": [
+ "dmpxs.com"
+ ]
+ }
+ },
+ {
+ "DoublePimp": {
+ "http://doublepimp.com/": [
+ "doublepimp.com"
+ ]
+ }
+ },
+ {
+ "DoublePositive": {
+ "http://www.doublepositive.com/": [
+ "bid-tag.com",
+ "doublepositive.com"
+ ]
+ }
+ },
+ {
+ "Drawbridge": {
+ "http://drawbrid.ge/": [
+ "adsymptotic.com",
+ "drawbrid.ge"
+ ]
+ }
+ },
+ {
+ "DS-IQ": {
+ "http://www.ds-iq.com/": [
+ "ds-iq.com"
+ ]
+ }
+ },
+ {
+ "DSNR Group": {
+ "http://www.dsnrmg.com/": [
+ "dsnrgroup.com",
+ "dsnrmg.com",
+ "traffiliate.com",
+ "z5x.com",
+ "z5x.net"
+ ]
+ }
+ },
+ {
+ "DynAdmic": {
+ "https://dynadmic.com/": [
+ "dynadmic.com",
+ "dyntrk.com"
+ ]
+ }
+ },
+ {
+ "DynamicOxygen": {
+ "http://www.dynamicoxygen.com/": [
+ "dynamicoxygen.com",
+ "exitjunction.com"
+ ]
+ }
+ },
+ {
+ "DynamicYield": {
+ "https://www.dynamicyield.com/": [
+ "px-eu.dynamicyield.com",
+ "px.dynamicyield.com"
+ ]
+ }
+ },
+ {
+ "Earnify": {
+ "http://earnify.com/": [
+ "earnify.com"
+ ]
+ }
+ },
+ {
+ "eBay": {
+ "http://www.ebay.com/": [
+ "ebay.com"
+ ]
+ }
+ },
+ {
+ "Effective Measure": {
+ "http://www.effectivemeasure.com/": [
+ "effectivemeasure.com",
+ "effectivemeasure.net"
+ ]
+ }
+ },
+ {
+ "ekolay": {
+ "http://www.ekolay.net/": [
+ "e-kolay.net",
+ "ekolay.net"
+ ]
+ }
+ },
+ {
+ "Eleavers": {
+ "http://eleavers.com/": [
+ "eleavers.com"
+ ]
+ }
+ },
+ {
+ "Emego": {
+ "http://www.usemax.de/": [
+ "usemax.de"
+ ]
+ }
+ },
+ {
+ "Emerse": {
+ "https://www.emerse.com": [
+ "emerse.com"
+ ]
+ }
+ },
+ {
+ "EMX": {
+ "https://emxdigital.com/": [
+ "brealtime.com",
+ "clearstream.tv",
+ "emxdgt.com",
+ "emxdigital.com"
+ ]
+ }
+ },
+ {
+ "Enecto": {
+ "http://www.enecto.com/": [
+ "enecto.com"
+ ]
+ }
+ },
+ {
+ "engage:BDR": {
+ "http://engagebdr.com/": [
+ "bnmla.com",
+ "engagebdr.com"
+ ]
+ }
+ },
+ {
+ "Engago Technology": {
+ "http://www.engago.com/": [
+ "appmetrx.com",
+ "engago.com"
+ ]
+ }
+ },
+ {
+ "Engine Network": {
+ "http://enginenetwork.com/": [
+ "enginenetwork.com"
+ ]
+ }
+ },
+ {
+ "Ensighten": {
+ "http://www.ensighten.com/": [
+ "ensighten.com"
+ ]
+ }
+ },
+ {
+ "Entireweb": {
+ "http://www.entireweb.com/": [
+ "entireweb.com"
+ ]
+ }
+ },
+ {
+ "Epic Media Group": {
+ "http://www.theepicmediagroup.com/": [
+ "epicadvertising.com",
+ "epicmarketplace.com",
+ "epicmobileads.com",
+ "theepicmediagroup.com",
+ "trafficmp.com"
+ ]
+ }
+ },
+ {
+ "Epsilon": {
+ "http://www.epsilon.com/": [
+ "epsilon.com"
+ ]
+ }
+ },
+ {
+ "EQ Ads": {
+ "http://www.eqads.com/": [
+ "eqads.com"
+ ]
+ }
+ },
+ {
+ "EroAdvertising": {
+ "http://www.ero-advertising.com/": [
+ "ero-advertising.com"
+ ]
+ }
+ },
+ {
+ "Etarget": {
+ "http://etargetnet.com/": [
+ "etarget.eu",
+ "etargetnet.com"
+ ]
+ }
+ },
+ {
+ "Etineria": {
+ "http://www.etineria.com/": [
+ "adwitserver.com",
+ "etineria.com"
+ ]
+ }
+ },
+ {
+ "eTrigue": {
+ "http://www.etrigue.com/": [
+ "etrigue.com"
+ ]
+ }
+ },
+ {
+ "Evergage": {
+ "http://www.evergage.com": [
+ "mybuys.com",
+ "veruta.com"
+ ]
+ }
+ },
+ {
+ "Everyday Health": {
+ "http://www.everydayhealth.com/": [
+ "everydayhealth.com",
+ "waterfrontmedia.com"
+ ]
+ }
+ },
+ {
+ "Evisions Marketing": {
+ "http://www.evisionsmarketing.com/": [
+ "engineseeker.com",
+ "evisionsmarketing.com"
+ ]
+ }
+ },
+ {
+ "Evolve": {
+ "http://www.evolvemediacorp.com/": [
+ "evolvemediacorp.com",
+ "evolvemediametrics.com",
+ "gorillanation.com"
+ ]
+ }
+ },
+ {
+ "eWayDirect": {
+ "http://www.ewaydirect.com/": [
+ "ewaydirect.com",
+ "ixs1.net"
+ ]
+ }
+ },
+ {
+ "ewebse": {
+ "http://ewebse.com/": [
+ "777seo.com",
+ "ewebse.com"
+ ]
+ }
+ },
+ {
+ "excitad": {
+ "http://excitad.com/": [
+ "excitad.com"
+ ]
+ }
+ },
+ {
+ "eXelate": {
+ "http://exelate.com/": [
+ "exelate.com",
+ "exelator.com"
+ ]
+ }
+ },
+ {
+ "ExoClick": {
+ "http://www.exoclick.com/": [
+ "exoclick.com"
+ ]
+ }
+ },
+ {
+ "Exosrv": {
+ "http://main.exosrv.com/": [
+ "exosrv.com"
+ ]
+ }
+ },
+ {
+ "Experian": {
+ "http://www.experian.com/": [
+ "audienceiq.com",
+ "experian.com"
+ ]
+ }
+ },
+ {
+ "expo-MAX": {
+ "http://expo-max.com/": [
+ "expo-max.com"
+ ]
+ }
+ },
+ {
+ "Exponential Interactive": {
+ "http://www.exponential.com/": [
+ "adotube.com",
+ "exponential.com",
+ "fulltango.com",
+ "tribalfusion.com"
+ ]
+ }
+ },
+ {
+ "Extension Factory": {
+ "http://www.extensionfactory.com/": [
+ "extensionfactory.com"
+ ]
+ }
+ },
+ {
+ "EXTENSIONS.RU": {
+ "http://extensions.ru/": [
+ "extensions.ru"
+ ]
+ }
+ },
+ {
+ "Eyeconomy": {
+ "http://www.eyeconomy.co.uk/": [
+ "eyeconomy.co.uk",
+ "eyeconomy.com",
+ "sublimemedia.net"
+ ]
+ }
+ },
+ {
+ "EyeNewton": {
+ "http://eyenewton.ru/": [
+ "eyenewton.ru"
+ ]
+ }
+ },
+ {
+ "eyeReturn Marketing": {
+ "http://www.eyereturnmarketing.com/": [
+ "eyereturn.com",
+ "eyereturnmarketing.com"
+ ]
+ }
+ },
+ {
+ "Eyeviewdigital": {
+ "http://www.eyeviewdigital.com/": [
+ "eyeviewdigital.com"
+ ]
+ }
+ },
+ {
+ "Facebook": {
+ "http://www.facebook.com/": [
+ "atlassolutions.com"
+ ]
+ }
+ },
+ {
+ "Facilitate Digital": {
+ "http://www.facilitatedigital.com/": [
+ "adsfac.eu",
+ "adsfac.info",
+ "adsfac.net",
+ "adsfac.sg",
+ "adsfac.us",
+ "facilitatedigital.com"
+ ]
+ }
+ },
+ {
+ "Fairfax Media": {
+ "http://www.fxj.com.au/": [
+ "fairfax.com.au",
+ "fxj.com.au"
+ ]
+ }
+ },
+ {
+ "faithadnet": {
+ "http://www.faithadnet.com/": [
+ "faithadnet.com"
+ ]
+ }
+ },
+ {
+ "Fanplayr": {
+ "https://fanplayr.com/": [
+ "fanplayr.com"
+ ]
+ }
+ },
+ {
+ "Fathom": {
+ "http://www.fathomdelivers.com/": [
+ "fathomdelivers.com",
+ "fathomseo.com"
+ ]
+ }
+ },
+ {
+ "Federated Media": {
+ "http://www.federatedmedia.net/": [
+ "federatedmedia.net",
+ "fmpub.net",
+ "lijit.com"
+ ]
+ }
+ },
+ {
+ "FetchBack": {
+ "http://www.fetchback.com/": [
+ "fetchback.com"
+ ]
+ }
+ },
+ {
+ "Fiksu": {
+ "http://www.fiksu.com/": [
+ "fiksu.com"
+ ]
+ }
+ },
+ {
+ "FinancialContent": {
+ "http://www.financialcontent.com/": [
+ "financialcontent.com"
+ ]
+ }
+ },
+ {
+ "Fizz-Buzz Media": {
+ "http://www.fizzbuzzmedia.com/": [
+ "fizzbuzzmedia.com",
+ "fizzbuzzmedia.net"
+ ]
+ }
+ },
+ {
+ "Flashtalking": {
+ "http://www.flashtalking.com/": [
+ "flashtalking.com"
+ ]
+ }
+ },
+ {
+ "Flite": {
+ "http://www.flite.com/": [
+ "flite.com",
+ "widgetserver.com"
+ ]
+ }
+ },
+ {
+ "Fluct": {
+ "https://corp.fluct.jp/": [
+ "adingo.jp",
+ "fluct.jp"
+ ]
+ }
+ },
+ {
+ "Flytxt": {
+ "http://www.flytxt.com/": [
+ "flytxt.com"
+ ]
+ }
+ },
+ {
+ "Forbes": {
+ "http://www.forbes.com/": [
+ "brandsideplatform.com",
+ "forbes.com"
+ ]
+ }
+ },
+ {
+ "Fox One Stop Media": {
+ "http://www.foxonestop.com/": [
+ "fimserve.com",
+ "foxnetworks.com",
+ "foxonestop.com",
+ "mobsmith.com",
+ "myads.com",
+ "othersonline.com"
+ ]
+ }
+ },
+ {
+ "FreakOut": {
+ "http://fout.jp/": [
+ "fout.jp"
+ ]
+ }
+ },
+ {
+ "Freedom Communications": {
+ "http://www.freedom.com/": [
+ "freedom.com"
+ ]
+ }
+ },
+ {
+ "FreeWheel": {
+ "http://www.freewheel.tv/": [
+ "stickyadstv.com"
+ ]
+ }
+ },
+ {
+ "FriendFinder Networks": {
+ "http://ffn.com/": [
+ "adultfriendfinder.com",
+ "ffn.com",
+ "pop6.com"
+ ]
+ }
+ },
+ {
+ "Friends2Follow": {
+ "https://friends2follow.com/": [
+ "tracking.friends2follow.com"
+ ]
+ }
+ },
+ {
+ "Frog Sex": {
+ "http://www.frogsex.com/": [
+ "double-check.com",
+ "frogsex.com"
+ ]
+ }
+ },
+ {
+ "FuelX": {
+ "https://fuelx.com/": [
+ "fuel451.com",
+ "fuelx.com"
+ ]
+ }
+ },
+ {
+ "Future Ads": {
+ "https://www.futureads.com/": [
+ "futureads.com",
+ "resultlinks.com"
+ ]
+ }
+ },
+ {
+ "Fyber": {
+ "https://www.fyber.com/": [
+ "fyber.com"
+ ]
+ }
+ },
+ {
+ "Game Advertising Online": {
+ "http://www.game-advertising-online.com/": [
+ "game-advertising-online.com"
+ ]
+ }
+ },
+ {
+ "Games2win": {
+ "http://www.games2win.com/": [
+ "games2win.com",
+ "inviziads.com"
+ ]
+ }
+ },
+ {
+ "Gamned": {
+ "http://www.gamned.com/": [
+ "gamned.com"
+ ]
+ }
+ },
+ {
+ "Gannett": {
+ "http://www.gannett.com/": [
+ "gannett.com",
+ "pointroll.com"
+ ]
+ }
+ },
+ {
+ "GB-World": {
+ "http://www.gb-world.net/": [
+ "gb-world.net"
+ ]
+ }
+ },
+ {
+ "Gemius": {
+ "http://www.gemius.com/": [
+ "gemius.com",
+ "gemius.pl"
+ ]
+ }
+ },
+ {
+ "Genesis Media": {
+ "http://www.genesismedia.com/": [
+ "genesismedia.com",
+ "genesismediaus.com"
+ ]
+ }
+ },
+ {
+ "GENIEE": {
+ "https://geniee.co.jp/": [
+ "geniee.co.jp",
+ "gssprt.jp"
+ ]
+ }
+ },
+ {
+ "GENIE GROUP": {
+ "http://www.geniegroupltd.co.uk/": [
+ "geniegroupltd.co.uk"
+ ]
+ }
+ },
+ {
+ "GeoAds": {
+ "http://www.geoads.com/": [
+ "geoads.com"
+ ]
+ }
+ },
+ {
+ "GetGlue": {
+ "http://getglue.com/": [
+ "getglue.com",
+ "smrtlnks.com"
+ ]
+ }
+ },
+ {
+ "GetIntent": {
+ "http://getintent.com/": [
+ "adhigh.net",
+ "getintent.com"
+ ]
+ }
+ },
+ {
+ "GISMAds": {
+ "http://www.gismads.jp/": [
+ "gismads.jp"
+ ]
+ }
+ },
+ {
+ "Glam Media": {
+ "http://www.glammedia.com/": [
+ "glam.com",
+ "glammedia.com"
+ ]
+ }
+ },
+ {
+ "Gleam": {
+ "https://gleam.io/": [
+ "fraudjs.io",
+ "gleam.io"
+ ]
+ }
+ },
+ {
+ "Globe7": {
+ "http://www.globe7.com/": [
+ "globe7.com"
+ ]
+ }
+ },
+ {
+ "GoDataFeed": {
+ "http://godatafeed.com/": [
+ "godatafeed.com"
+ ]
+ }
+ },
+ {
+ "Goldbach": {
+ "http://www.goldbachgroup.com/": [
+ "goldbach.com",
+ "goldbachgroup.com"
+ ]
+ }
+ },
+ {
+ "GoldSpot Media": {
+ "http://www.goldspotmedia.com/": [
+ "goldspotmedia.com"
+ ]
+ }
+ },
+ {
+ "Google": {
+ "http://www.google.com/": [
+ "2mdn.net",
+ "admeld.com",
+ "admob.com",
+ "adservice.google.ca",
+ "adservice.google.com",
+ "adwords.google.com",
+ "cc-dt.com",
+ "destinationurl.com",
+ "doubleclick.net",
+ "googleadservices.com",
+ "googlesyndication.com",
+ "googletagservices.com",
+ "invitemedia.com",
+ "smtad.net",
+ "teracent.com",
+ "teracent.net",
+ "ytsa.net"
+ ]
+ }
+ },
+ {
+ "Grapeshot": {
+ "http://www.grapeshot.co.uk/": [
+ "grapeshot.co.uk"
+ ]
+ }
+ },
+ {
+ "Graphnium": {
+ "https://www.graphinium.com/": [
+ "crm4d.com"
+ ]
+ }
+ },
+ {
+ "Grocery Shopping Network": {
+ "http://www.groceryshopping.net/": [
+ "groceryshopping.net"
+ ]
+ }
+ },
+ {
+ "GroovinAds": {
+ "http://www.groovinads.com/": [
+ "groovinads.com"
+ ]
+ }
+ },
+ {
+ "Gruner + Jahr": {
+ "http://www.guj.de/": [
+ "guj.de",
+ "ligatus.com"
+ ]
+ }
+ },
+ {
+ "GumGum": {
+ "http://gumgum.com/": [
+ "gumgum.com"
+ ]
+ }
+ },
+ {
+ "Gunggo": {
+ "http://www.gunggo.com/": [
+ "gunggo.com"
+ ]
+ }
+ },
+ {
+ "Hands Mobile": {
+ "http://www.hands.com.br/": [
+ "hands.com.br"
+ ]
+ }
+ },
+ {
+ "Harrenmedia": {
+ "http://www.harrenmedia.com/": [
+ "harrenmedia.com",
+ "harrenmedianetwork.com"
+ ]
+ }
+ },
+ {
+ "HealthPricer": {
+ "http://www.healthpricer.com/": [
+ "adacado.com",
+ "healthpricer.com"
+ ]
+ }
+ },
+ {
+ "Hearst": {
+ "http://www.hearst.com/": [
+ "hearst.com",
+ "ic-live.com",
+ "iclive.com",
+ "icrossing.com",
+ "sptag.com",
+ "sptag1.com",
+ "sptag2.com",
+ "sptag3.com"
+ ]
+ }
+ },
+ {
+ "HilltopAds": {
+ "https://hilltopads.com/": [
+ "hilltopads.com",
+ "hilltopads.net",
+ "shoporielder.pro"
+ ]
+ }
+ },
+ {
+ "Hi-media": {
+ "http://www.hi-media.com/": [
+ "comclick.com",
+ "hi-media.com"
+ ]
+ }
+ },
+ {
+ "Horyzon Media": {
+ "http://www.horyzon-media.com/": [
+ "horyzon-media.com"
+ ]
+ }
+ },
+ {
+ "HotMart": {
+ "https://www.hotmart.com/en/": [
+ "hotmart.com"
+ ]
+ }
+ },
+ {
+ "HOTWords": {
+ "http://www.hotwords.com/": [
+ "hotwords.com",
+ "hotwords.es"
+ ]
+ }
+ },
+ {
+ "HP": {
+ "http://www.hp.com/": [
+ "hp.com",
+ "optimost.com"
+ ]
+ }
+ },
+ {
+ "Httpool": {
+ "http://www.httpool.com/": [
+ "httpool.com"
+ ]
+ }
+ },
+ {
+ "HUNT Mobile Ads": {
+ "http://www.huntmads.com/": [
+ "huntmads.com"
+ ]
+ }
+ },
+ {
+ "Hurra.com": {
+ "http://www.hurra.com/": [
+ "hurra.com"
+ ]
+ }
+ },
+ {
+ "IAB": {
+ "https://iabtechlab.com/": [
+ "digitru.st"
+ ]
+ }
+ },
+ {
+ "IAC": {
+ "http://www.iac.com/": [
+ "iac.com",
+ "iacadvertising.com"
+ ]
+ }
+ },
+ {
+ "iBehavior": {
+ "http://www.i-behavior.com/": [
+ "i-behavior.com",
+ "ib-ibi.com"
+ ]
+ }
+ },
+ {
+ "IBM": {
+ "http://www.ibm.com/": [
+ "unica.com"
+ ]
+ }
+ },
+ {
+ "ID5": {
+ "http://id5.io/": [
+ "id5-sync.com"
+ ]
+ }
+ },
+ {
+ "IDG": {
+ "http://www.idg.com/": [
+ "idg.com",
+ "idgtechnetwork.com"
+ ]
+ }
+ },
+ {
+ "iEntry": {
+ "http://www.ientry.com/": [
+ "600z.com",
+ "ientry.com"
+ ]
+ }
+ },
+ {
+ "IgnitAd": {
+ "http://www.ignitad.com/": [
+ "ignitad.com"
+ ]
+ }
+ },
+ {
+ "IgnitionOne": {
+ "http://www.ignitionone.com/": [
+ "ignitionone.com",
+ "ignitionone.net",
+ "searchignite.com"
+ ]
+ }
+ },
+ {
+ "Improve Digital": {
+ "www.improvedigital.com/": [
+ "360yield.com",
+ "improvedigital.com"
+ ]
+ }
+ },
+ {
+ "Inadco": {
+ "http://www.inadco.com/": [
+ "anadcoads.com",
+ "inadco.com",
+ "inadcoads.com"
+ ]
+ }
+ },
+ {
+ "IndexExchange": {
+ "https://www.indexexchange.com": [
+ "indexexchange.com"
+ ]
+ }
+ },
+ {
+ "Infectious Media": {
+ "http://www.infectiousmedia.com/": [
+ "impressiondesk.com",
+ "infectiousmedia.com"
+ ]
+ }
+ },
+ {
+ "Inflection Point Media": {
+ "http://www.inflectionpointmedia.com/": [
+ "inflectionpointmedia.com"
+ ]
+ }
+ },
+ {
+ "Infogroup": {
+ "http://www.infogroup.com/": [
+ "infogroup.com"
+ ]
+ }
+ },
+ {
+ "Infolinks": {
+ "http://www.infolinks.com/": [
+ "infolinks.com"
+ ]
+ }
+ },
+ {
+ "Infra-Ad": {
+ "http://www.infra-ad.com/": [
+ "infra-ad.com"
+ ]
+ }
+ },
+ {
+ "InMobi": {
+ "http://www.inmobi.com/": [
+ "aerserv.com",
+ "inmobi.com",
+ "sproutinc.com"
+ ]
+ }
+ },
+ {
+ "inneractive": {
+ "http://inner-active.com/": [
+ "inner-active.com"
+ ]
+ }
+ },
+ {
+ "Innity": {
+ "http://innity.com/": [
+ "innity.com"
+ ]
+ }
+ },
+ {
+ "InsightExpress": {
+ "http://www.insightexpress.com/": [
+ "insightexpress.com",
+ "insightexpressai.com"
+ ]
+ }
+ },
+ {
+ "InSkin Media": {
+ "http://inskinmedia.com/": [
+ "inskinmedia.com"
+ ]
+ }
+ },
+ {
+ "Instinctive": {
+ "https://instinctive.io/": [
+ "instinctive.io",
+ "instinctiveads.com"
+ ]
+ }
+ },
+ {
+ "Integral Ad Science": {
+ "https://integralads.com/": [
+ "adsafemedia.com",
+ "adsafeprotected.com",
+ "iasds01.com",
+ "integralads.com"
+ ]
+ }
+ },
+ {
+ "Intent Media": {
+ "http://www.intentmedia.com/": [
+ "intentmedia.com",
+ "intentmedia.net"
+ ]
+ }
+ },
+ {
+ "Intergi": {
+ "http://intergi.com/": [
+ "intergi.com"
+ ]
+ }
+ },
+ {
+ "Intermarkets": {
+ "http://www.intermarkets.net/": [
+ "intermarkets.net"
+ ]
+ }
+ },
+ {
+ "Intermundo Media": {
+ "http://intermundomedia.com/": [
+ "intermundomedia.com"
+ ]
+ }
+ },
+ {
+ "Internet Brands": {
+ "http://www.internetbrands.com/": [
+ "ibpxl.com",
+ "internetbrands.com"
+ ]
+ }
+ },
+ {
+ "Interpolls": {
+ "http://www.interpolls.com/": [
+ "interpolls.com"
+ ]
+ }
+ },
+ {
+ "Inuvo": {
+ "http://inuvo.com/": [
+ "inuvo.com"
+ ]
+ }
+ },
+ {
+ "InvestingChannel": {
+ "http://investingchannel.com/": [
+ "investingchannel.com"
+ ]
+ }
+ },
+ {
+ "IponWeb": {
+ "https://www.iponweb.com/": [
+ "iponweb.com",
+ "iponweb.net"
+ ]
+ }
+ },
+ {
+ "iPROM": {
+ "http://www.iprom.si/": [
+ "centraliprom.com",
+ "iprom.net",
+ "iprom.si",
+ "mediaiprom.com"
+ ]
+ }
+ },
+ {
+ "iPromote": {
+ "http://www.ipromote.com/": [
+ "ipromote.com"
+ ]
+ }
+ },
+ {
+ "iProspect": {
+ "http://www.iprospect.com/": [
+ "clickmanage.com",
+ "iprospect.com"
+ ]
+ }
+ },
+ {
+ "ISI Technologies": {
+ "http://digbro.com/": [
+ "adversalservers.com",
+ "digbro.com"
+ ]
+ }
+ },
+ {
+ "ismatlab.com": {
+ "http://ismatlab.com": [
+ "ismatlab.com"
+ ]
+ }
+ },
+ {
+ "I.UA": {
+ "http://www.i.ua/": [
+ "i.ua"
+ ]
+ }
+ },
+ {
+ "Jaroop": {
+ "http://www.jaroop.com/": [
+ "jaroop.com"
+ ]
+ }
+ },
+ {
+ "JasperLabs": {
+ "http://www.jasperlabs.com/": [
+ "jasperlabs.com"
+ ]
+ }
+ },
+ {
+ "Jemm": {
+ "http://jemmgroup.com/": [
+ "jemmgroup.com"
+ ]
+ }
+ },
+ {
+ "Jink": {
+ "http://www.jink.de/": [
+ "jink.de",
+ "jinkads.com"
+ ]
+ }
+ },
+ {
+ "Jirbo": {
+ "http://jirbo.com/": [
+ "adcolony.com",
+ "jirbo.com"
+ ]
+ }
+ },
+ {
+ "Jivox": {
+ "http://www.jivox.com/": [
+ "jivox.com"
+ ]
+ }
+ },
+ {
+ "JobThread": {
+ "http://www.jobthread.com/": [
+ "jobthread.com"
+ ]
+ }
+ },
+ {
+ "JuicyAds": {
+ "http://www.juicyads.com/": [
+ "juicyads.com"
+ ]
+ }
+ },
+ {
+ "Jumptap": {
+ "http://www.jumptap.com/": [
+ "jumptap.com"
+ ]
+ }
+ },
+ {
+ "justuno": {
+ "https://www.justuno.com/": [
+ "justuno.com"
+ ]
+ }
+ },
+ {
+ "Kargo": {
+ "https://kargo.com/": [
+ "kargo.com"
+ ]
+ }
+ },
+ {
+ "Kenshoo": {
+ "http://www.kenshoo.com/": [
+ "kenshoo.com",
+ "xg4ken.com"
+ ]
+ }
+ },
+ {
+ "Keyade": {
+ "http://www.keyade.com/": [
+ "keyade.com"
+ ]
+ }
+ },
+ {
+ "Keywee": {
+ "https://keywee.co": [
+ "keywee.co"
+ ]
+ }
+ },
+ {
+ "KissMyAds": {
+ "http://kissmyads.com/": [
+ "kissmyads.com"
+ ]
+ }
+ },
+ {
+ "Kitara Media": {
+ "http://www.kitaramedia.com/": [
+ "103092804.com",
+ "kitaramedia.com"
+ ]
+ }
+ },
+ {
+ "KIT digital": {
+ "http://kitd.com/": [
+ "keewurd.com",
+ "kitd.com",
+ "peerset.com"
+ ]
+ }
+ },
+ {
+ "Kokteyl": {
+ "http://www.kokteyl.com/": [
+ "admost.com",
+ "kokteyl.com"
+ ]
+ }
+ },
+ {
+ "Komli": {
+ "http://www.komli.com/": [
+ "komli.com"
+ ]
+ }
+ },
+ {
+ "Kontera": {
+ "http://www.kontera.com/": [
+ "kontera.com"
+ ]
+ }
+ },
+ {
+ "Korrelate": {
+ "http://korrelate.com/": [
+ "adsummos.com",
+ "adsummos.net",
+ "korrelate.com"
+ ]
+ }
+ },
+ {
+ "Krux": {
+ "http://www.krux.com/": [
+ "krux.com",
+ "kruxdigital.com",
+ "krxd.net"
+ ]
+ }
+ },
+ {
+ "Lakana": {
+ "http://www.lakana.com/": [
+ "ibsys.com",
+ "lakana.com"
+ ]
+ }
+ },
+ {
+ "Layer-Ad.org": {
+ "http://layer-ad.org/": [
+ "layer-ad.org"
+ ]
+ }
+ },
+ {
+ "Layer Ads": {
+ "http://layer-ads.net/": [
+ "layer-ads.net"
+ ]
+ }
+ },
+ {
+ "LeadBolt": {
+ "http://www.leadbolt.com/": [
+ "leadbolt.com"
+ ]
+ }
+ },
+ {
+ "LeadFormix": {
+ "http://www.leadformix.com/": [
+ "leadforce1.com",
+ "leadformix.com"
+ ]
+ }
+ },
+ {
+ "LeanPlum": {
+ "https://www.leanplum.com/": [
+ "leanplum.com"
+ ]
+ }
+ },
+ {
+ "Legolas Media": {
+ "http://www.legolas-media.com/": [
+ "legolas-media.com"
+ ]
+ }
+ },
+ {
+ "Levexis": {
+ "http://www.levexis.com/": [
+ "levexis.com"
+ ]
+ }
+ },
+ {
+ "Lexos Media": {
+ "http://www.lexosmedia.com/": [
+ "adbull.com",
+ "lexosmedia.com"
+ ]
+ }
+ },
+ {
+ "LifeStreet": {
+ "http://lifestreetmedia.com/": [
+ "lfstmedia.com",
+ "lifestreetmedia.com"
+ ]
+ }
+ },
+ {
+ "LinkConnector": {
+ "http://www.linkconnector.com/": [
+ "linkconnector.com"
+ ]
+ }
+ },
+ {
+ "LinkShare": {
+ "http://www.linkshare.com/": [
+ "linkshare.com",
+ "linksynergy.com"
+ ]
+ }
+ },
+ {
+ "Linkz": {
+ "http://www.linkz.net/": [
+ "linkz.net"
+ ]
+ }
+ },
+ {
+ "Listrak": {
+ "http://www.listrak.com/": [
+ "listrak.com",
+ "listrakbi.com"
+ ]
+ }
+ },
+ {
+ "LiveIntent": {
+ "http://www.liveintent.com/": [
+ "liadm.com",
+ "liveintent.com"
+ ]
+ }
+ },
+ {
+ "LiveInternet": {
+ "http://www.liveinternet.ru": [
+ "liveinternet.ru",
+ "yadro.ru"
+ ]
+ }
+ },
+ {
+ "LiveRamp": {
+ "https://liveramp.com/": [
+ "liveramp.com",
+ "tvpixel.com"
+ ]
+ }
+ },
+ {
+ "LKQD": {
+ "http://lkqd.com": [
+ "lkqd.com",
+ "lkqd.net"
+ ]
+ }
+ },
+ {
+ "Local Yokel Media": {
+ "http://www.localyokelmedia.com/": [
+ "localyokelmedia.com"
+ ]
+ }
+ },
+ {
+ "Localytics": {
+ "https://www.localytics.com/": [
+ "localytics.com"
+ ]
+ }
+ },
+ {
+ "LockerDome": {
+ "https://lockerdome.com/": [
+ "lockerdome.com"
+ ]
+ }
+ },
+ {
+ "Longboard Media": {
+ "http://longboardmedia.com/": [
+ "longboardmedia.com"
+ ]
+ }
+ },
+ {
+ "Loomia": {
+ "http://www.loomia.com/": [
+ "loomia.com"
+ ]
+ }
+ },
+ {
+ "LoopFuse": {
+ "https://www.loopfuse.net/": [
+ "lfov.net",
+ "loopfuse.net"
+ ]
+ }
+ },
+ {
+ "LoopMe": {
+ "https://loopme.com/": [
+ "loopme.com"
+ ]
+ }
+ },
+ {
+ "LotLinx": {
+ "https://www.lotlinx.com": [
+ "lotlinx.com"
+ ]
+ }
+ },
+ {
+ "Lower My Bills": {
+ "http://lowermybills.com": [
+ "lowermybills.com"
+ ]
+ }
+ },
+ {
+ "lptracker": {
+ "https://lptracker.io/": [
+ "lptracker.io"
+ ]
+ }
+ },
+ {
+ "LucidMedia": {
+ "http://www.lucidmedia.com/": [
+ "lucidmedia.com"
+ ]
+ }
+ },
+ {
+ "m6d": {
+ "http://m6d.com/": [
+ "m6d.com",
+ "media6degrees.com"
+ ]
+ }
+ },
+ {
+ "Madhouse": {
+ "http://www.madhouse.cn/": [
+ "madhouse.cn"
+ ]
+ }
+ },
+ {
+ "Madison Logic": {
+ "http://www.madisonlogic.com/": [
+ "dinclinx.com",
+ "madisonlogic.com"
+ ]
+ }
+ },
+ {
+ "madvertise": {
+ "http://madvertise.com/": [
+ "madvertise.com"
+ ]
+ }
+ },
+ {
+ "Magnetic": {
+ "http://www.magnetic.com/": [
+ "domdex.com",
+ "domdex.net",
+ "magnetic.com",
+ "qjex.net"
+ ]
+ }
+ },
+ {
+ "Magnify360": {
+ "http://www.magnify360.com/": [
+ "dialogmgr.com",
+ "magnify360.com"
+ ]
+ }
+ },
+ {
+ "MailChimp": {
+ "http://mailchimp.com/": [
+ "campaign-archive1.com",
+ "list-manage.com",
+ "mailchimp.com"
+ ]
+ }
+ },
+ {
+ "Manifest": {
+ "http://www.manifest.ru/": [
+ "bannerbank.ru",
+ "manifest.ru"
+ ]
+ }
+ },
+ {
+ "Marchex": {
+ "http://www.marchex.com/": [
+ "industrybrains.com",
+ "marchex.com"
+ ]
+ }
+ },
+ {
+ "Marimedia": {
+ "http://www.marimedia.net/": [
+ "marimedia.net"
+ ]
+ }
+ },
+ {
+ "MarketGid": {
+ "http://www.marketgid.com/": [
+ "dt00.net",
+ "dt07.net",
+ "marketgid.com"
+ ]
+ }
+ },
+ {
+ "Marketo": {
+ "http://www.marketo.com/": [
+ "marketo.com",
+ "marketo.net"
+ ]
+ }
+ },
+ {
+ "Martini Media": {
+ "http://martinimedianetwork.com/": [
+ "martiniadnetwork.com",
+ "martinimedianetwork.com"
+ ]
+ }
+ },
+ {
+ "mashero": {
+ "http://www.mashero.com/": [
+ "mashero.com"
+ ]
+ }
+ },
+ {
+ "Match.com": {
+ "http://www.match.com/": [
+ "chemistry.com",
+ "match.com",
+ "meetic-partners.com"
+ ]
+ }
+ },
+ {
+ "Matomy": {
+ "http://www.matomy.com/": [
+ "adnetinteractive.com",
+ "adsmarket.com",
+ "matomy.com",
+ "matomymarket.com",
+ "matomymedia.com",
+ "mediawhiz.com",
+ "optimatic.com",
+ "xtendmedia.com"
+ ]
+ }
+ },
+ {
+ "MaxBounty": {
+ "http://www.maxbounty.com/": [
+ "maxbounty.com",
+ "mb01.com"
+ ]
+ }
+ },
+ {
+ "MaxPoint": {
+ "http://maxpointinteractive.com/": [
+ "maxpointinteractive.com",
+ "maxusglobal.com",
+ "mxptint.net"
+ ]
+ }
+ },
+ {
+ "MdotM": {
+ "http://mdotm.com/": [
+ "mdotm.com"
+ ]
+ }
+ },
+ {
+ "MediaBrix": {
+ "http://www.mediabrix.com/": [
+ "mediabrix.com"
+ ]
+ }
+ },
+ {
+ "MediaCom": {
+ "http://www.mediacom.com/": [
+ "mediacom.com"
+ ]
+ }
+ },
+ {
+ "mediaFORGE": {
+ "http://www.mediaforge.com/": [
+ "mediaforge.com"
+ ]
+ }
+ },
+ {
+ "Medialets": {
+ "http://www.medialets.com/": [
+ "medialets.com"
+ ]
+ }
+ },
+ {
+ "MediaMath": {
+ "http://www.mediamath.com/": [
+ "adroitinteractive.com",
+ "designbloxlive.com",
+ "mathtag.com",
+ "mediamath.com"
+ ]
+ }
+ },
+ {
+ "media.net": {
+ "http://www.media.net/": [
+ "media.net"
+ ]
+ }
+ },
+ {
+ "Mediaocean": {
+ "http://www.mediaocean.com/": [
+ "adbuyer.com",
+ "mediaocean.com"
+ ]
+ }
+ },
+ {
+ "MediaShakers": {
+ "http://www.mediashakers.com/": [
+ "media-servers.net",
+ "mediashakers.com"
+ ]
+ }
+ },
+ {
+ "MediaTrust": {
+ "http://www.mediatrust.com/": [
+ "mediatrust.com"
+ ]
+ }
+ },
+ {
+ "Medicx Media Solutions": {
+ "http://www.medicxmedia.com/": [
+ "medicxmedia.com"
+ ]
+ }
+ },
+ {
+ "MegaIndex": {
+ "http://www.megaindex.ru/": [
+ "megaindex.ru"
+ ]
+ }
+ },
+ {
+ "Mercent": {
+ "http://www.mercent.com/": [
+ "mercent.com"
+ ]
+ }
+ },
+ {
+ "MerchantAdvantage": {
+ "http://www.merchantadvantage.com/": [
+ "merchantadvantage.com"
+ ]
+ }
+ },
+ {
+ "Merchenta": {
+ "http://www.merchenta.com/": [
+ "merchenta.com"
+ ]
+ }
+ },
+ {
+ "Merkle": {
+ "https://www.merkleinc.com/": [
+ "rimmkaufman.com",
+ "rkdms.com"
+ ]
+ }
+ },
+ {
+ "Meta Network": {
+ "http://www.metanetwork.com/": [
+ "metanetwork.com"
+ ]
+ }
+ },
+ {
+ "Meteor": {
+ "http://www.meteorsolutions.com/": [
+ "meteorsolutions.com"
+ ]
+ }
+ },
+ {
+ "MetrixLab": {
+ "https://www.metrixlab.com": [
+ "adoftheyear.com",
+ "crm-metrix.com",
+ "customerconversio.com",
+ "metrixlab.com",
+ "opinionbar.com"
+ ]
+ }
+ },
+ {
+ "MicroAd": {
+ "http://www.microad.jp/": [
+ "microad.jp"
+ ]
+ }
+ },
+ {
+ "Microsoft": {
+ "http://www.microsoft.com/": [
+ "adbureau.net",
+ "adecn.com",
+ "aquantive.com",
+ "msads.net",
+ "netconversions.com",
+ "roiservice.com"
+ ]
+ }
+ },
+ {
+ "Millennial Media": {
+ "http://www.millennialmedia.com/": [
+ "decktrade.com",
+ "millennialmedia.com",
+ "mydas.mobi"
+ ]
+ }
+ },
+ {
+ "Mindset Media": {
+ "http://www.mindset-media.com/": [
+ "mindset-media.com",
+ "mmismm.com"
+ ]
+ }
+ },
+ {
+ "Mirando": {
+ "http://www.mirando.de/": [
+ "mirando.de"
+ ]
+ }
+ },
+ {
+ "Mixpo": {
+ "http://www.mixpo.com/": [
+ "mixpo.com"
+ ]
+ }
+ },
+ {
+ "Moat": {
+ "http://www.moat.com/": [
+ "moat.com",
+ "moatads.com"
+ ]
+ }
+ },
+ {
+ "MobFox": {
+ "http://www.mobfox.com/": [
+ "mobfox.com"
+ ]
+ }
+ },
+ {
+ "Mobials": {
+ "http://mobials.com": [
+ "mobials.com"
+ ]
+ }
+ },
+ {
+ "MobileAdTrading": {
+ "https://mobileadtrading.com/": [
+ "mobileadtrading.com"
+ ]
+ }
+ },
+ {
+ "Mobile Meteor": {
+ "http://mobilemeteor.com/": [
+ "mobilemeteor.com",
+ "showmeinn.com"
+ ]
+ }
+ },
+ {
+ "Mobile Storm": {
+ "http://mobilestorm.com/": [
+ "mobilestorm.com"
+ ]
+ }
+ },
+ {
+ "MobVision": {
+ "http://www.mobvision.com/": [
+ "admoda.com",
+ "mobvision.com"
+ ]
+ }
+ },
+ {
+ "Mocean Mobile": {
+ "http://www.moceanmobile.com/": [
+ "moceanmobile.com"
+ ]
+ }
+ },
+ {
+ "Mochila": {
+ "http://www.mochila.com/": [
+ "mochila.com"
+ ]
+ }
+ },
+ {
+ "Mojiva": {
+ "http://www.mojiva.com/": [
+ "mojiva.com"
+ ]
+ }
+ },
+ {
+ "Monetate": {
+ "http://monetate.com/": [
+ "monetate.com",
+ "monetate.net"
+ ]
+ }
+ },
+ {
+ "MONETIZEdigital": {
+ "https://www.cpalead.com/": [
+ "cpalead.com"
+ ]
+ }
+ },
+ {
+ "Monetize More": {
+ "http://monetizemore.com/": [
+ "monetizemore.com"
+ ]
+ }
+ },
+ {
+ "Monoloop": {
+ "http://www.monoloop.com/": [
+ "monoloop.com"
+ ]
+ }
+ },
+ {
+ "Monster": {
+ "http://www.monster.com/": [
+ "monster.com"
+ ]
+ }
+ },
+ {
+ "Moolah Media": {
+ "http://www.moolahmedia.com/": [
+ "moolah-media.com",
+ "moolahmedia.com"
+ ]
+ }
+ },
+ {
+ "MoPub": {
+ "http://www.mopub.com/": [
+ "mopub.com"
+ ]
+ }
+ },
+ {
+ "MovieLush.com": {
+ "https://www.movielush.com/": [
+ "affbuzzads.com",
+ "movielush.com"
+ ]
+ }
+ },
+ {
+ "Multiple Stream Media": {
+ "http://www.multiplestreammktg.com/": [
+ "adclickmedia.com",
+ "multiplestreammktg.com"
+ ]
+ }
+ },
+ {
+ "MUNDO Media": {
+ "http://www.mundomedia.com/": [
+ "mundomedia.com",
+ "silver-path.com"
+ ]
+ }
+ },
+ {
+ "MyCounter": {
+ "http://mycounter.com.ua/": [
+ "mycounter.com.ua"
+ ]
+ }
+ },
+ {
+ "MyPressPlus": {
+ "http://www.mypressplus.com/": [
+ "mypressplus.com",
+ "ppjol.net"
+ ]
+ }
+ },
+ {
+ "myThings": {
+ "http://www.mythings.com/": [
+ "mythings.com",
+ "mythingsmedia.com"
+ ]
+ }
+ },
+ {
+ "MyWebGrocer": {
+ "http://www.mywebgrocer.com/": [
+ "mywebgrocer.com"
+ ]
+ }
+ },
+ {
+ "Nanigans": {
+ "http://www.nanigans.com/": [
+ "nanigans.com"
+ ]
+ }
+ },
+ {
+ "NativeAds": {
+ "https://nativeads.com/": [
+ "nativeads.com"
+ ]
+ }
+ },
+ {
+ "Nativo": {
+ "http://www.nativo.net/": [
+ "postrelease.com"
+ ]
+ }
+ },
+ {
+ "Navegg": {
+ "http://www.navegg.com/": [
+ "navdmp.com",
+ "navegg.com"
+ ]
+ }
+ },
+ {
+ "NetAffiliation": {
+ "http://www.netaffiliation.com/": [
+ "netaffiliation.com"
+ ]
+ }
+ },
+ {
+ "NetBina": {
+ "http://www.netbina.com/": [
+ "netbina.com"
+ ]
+ }
+ },
+ {
+ "NetElixir": {
+ "http://www.netelixir.com/": [
+ "adelixir.com",
+ "netelixir.com"
+ ]
+ }
+ },
+ {
+ "Netmining": {
+ "http://www.netmining.com/": [
+ "netmining.com",
+ "netmng.com"
+ ]
+ }
+ },
+ {
+ "Net-Results": {
+ "http://www.net-results.com/": [
+ "cdnma.com",
+ "net-results.com",
+ "nr7.us"
+ ]
+ }
+ },
+ {
+ "NetSeer": {
+ "http://www.netseer.com/": [
+ "netseer.com"
+ ]
+ }
+ },
+ {
+ "NetShelter": {
+ "http://netshelter.com/": [
+ "netshelter.com",
+ "netshelter.net"
+ ]
+ }
+ },
+ {
+ "Neustar": {
+ "http://www.neustar.biz/": [
+ "adadvisor.net",
+ "neustar.biz"
+ ]
+ }
+ },
+ {
+ "newtention": {
+ "http://newtention.de/": [
+ "newtention.de",
+ "newtention.net",
+ "newtentionassets.net"
+ ]
+ }
+ },
+ {
+ "Nexage": {
+ "http://nexage.com/": [
+ "nexage.com"
+ ]
+ }
+ },
+ {
+ "Nextag": {
+ "http://www.nextag.com/": [
+ "nextag.com"
+ ]
+ }
+ },
+ {
+ "NextPerformance": {
+ "http://www.nextperformance.com/": [
+ "nextperformance.com",
+ "nxtck.com"
+ ]
+ }
+ },
+ {
+ "Nielsen": {
+ "http://www.nielsen.com/": [
+ "imrworldwide.com",
+ "imrworldwide.net"
+ ]
+ }
+ },
+ {
+ "Ninua": {
+ "http://www.ninua.com/": [
+ "networkedblogs.com",
+ "ninua.com"
+ ]
+ }
+ },
+ {
+ "Nokta": {
+ "http://www.noktamedya.com/": [
+ "noktamedya.com",
+ "virgul.com"
+ ]
+ }
+ },
+ {
+ "NowSpots": {
+ "http://nowspots.com/": [
+ "nowspots.com"
+ ]
+ }
+ },
+ {
+ "nrelate": {
+ "http://nrelate.com/": [
+ "nrelate.com"
+ ]
+ }
+ },
+ {
+ "Nuffnang": {
+ "http://www.nuffnang.com.my/": [
+ "nuffnang.com",
+ "nuffnang.com.my"
+ ]
+ }
+ },
+ {
+ "nugg.ad": {
+ "http://www.nugg.ad/": [
+ "nugg.ad",
+ "nuggad.net"
+ ]
+ }
+ },
+ {
+ "Ohana Media": {
+ "http://www.ohana-media.com/": [
+ "adohana.com",
+ "ohana-media.com",
+ "ohanaqb.com"
+ ]
+ }
+ },
+ {
+ "Omnicom Group": {
+ "http://www.omnicomgroup.com/": [
+ "accuenmedia.com",
+ "omnicomgroup.com",
+ "p-td.com"
+ ]
+ }
+ },
+ {
+ "onAd": {
+ "http://www.onad.eu/": [
+ "onad.eu"
+ ]
+ }
+ },
+ {
+ "Onclusive": {
+ "https://onclusive.com/": [
+ "airpr.com"
+ ]
+ }
+ },
+ {
+ "OneAd": {
+ "https://www.onead.com.tw/": [
+ "guoshipartners.com",
+ "onevision.com.tw"
+ ]
+ }
+ },
+ {
+ "One iota": {
+ "http://www.itsoneiota.com/": [
+ "itsoneiota.com",
+ "oneiota.co.uk"
+ ]
+ }
+ },
+ {
+ "Oneupweb": {
+ "http://www.oneupweb.com/": [
+ "oneupweb.com",
+ "sodoit.com"
+ ]
+ }
+ },
+ {
+ "OnlineMetrix": {
+ "http://h.online-metrix.net": [
+ "online-metrix.net"
+ ]
+ }
+ },
+ {
+ "Open New Media": {
+ "http://www.onm.de/": [
+ "onm.de"
+ ]
+ }
+ },
+ {
+ "OpenX": {
+ "http://openx.com/": [
+ "liftdna.com",
+ "openx.com",
+ "openx.net",
+ "openx.org",
+ "openxenterprise.com",
+ "servedbyopenx.com"
+ ]
+ }
+ },
+ {
+ "Opera": {
+ "http://www.opera.com/": [
+ "mobiletheory.com",
+ "opera.com",
+ "operamediaworks.com",
+ "operasoftware.com"
+ ]
+ }
+ },
+ {
+ "OPT": {
+ "http://www.opt.ne.jp/": [
+ "advg.jp",
+ "opt.ne.jp",
+ "p-advg.com"
+ ]
+ }
+ },
+ {
+ "Optify": {
+ "http://www.optify.net/": [
+ "optify.net"
+ ]
+ }
+ },
+ {
+ "Optimal": {
+ "http://optim.al/": [
+ "cpmadvisors.com",
+ "cpmatic.com",
+ "nprove.com",
+ "optim.al",
+ "orbengine.com",
+ "xa.net"
+ ]
+ }
+ },
+ {
+ "OptimumResponse": {
+ "http://www.optimumresponse.com/": [
+ "optimumresponse.com"
+ ]
+ }
+ },
+ {
+ "OptinMonster": {
+ "https://optinmonster.com/": [
+ "optinmonster.com",
+ "optnmstr.com"
+ ]
+ }
+ },
+ {
+ "OptMD": {
+ "http://optmd.com/": [
+ "optmd.com"
+ ]
+ }
+ },
+ {
+ "Oracle": {
+ "http://www.oracle.com/": [
+ "estara.com"
+ ]
+ }
+ },
+ {
+ "OrangeSoda": {
+ "http://www.orangesoda.com/": [
+ "orangesoda.com",
+ "otracking.com"
+ ]
+ }
+ },
+ {
+ "Outbrain": {
+ "http://www.outbrain.com/": [
+ "outbrain.com",
+ "sphere.com",
+ "visualrevenue.com"
+ ]
+ }
+ },
+ {
+ "Out There Media": {
+ "http://www.out-there-media.com/": [
+ "out-there-media.com"
+ ]
+ }
+ },
+ {
+ "Oversee.net": {
+ "http://www.oversee.net/": [
+ "dsnextgen.com",
+ "oversee.net"
+ ]
+ }
+ },
+ {
+ "OwnerIQ": {
+ "http://www.owneriq.com/": [
+ "owneriq.com",
+ "owneriq.net"
+ ]
+ }
+ },
+ {
+ "OxaMedia": {
+ "http://www.oxamedia.com/": [
+ "adconnexa.com",
+ "adsbwm.com",
+ "oxamedia.com"
+ ]
+ }
+ },
+ {
+ "PageFair": {
+ "https://pagefair.com/": [
+ "pagefair.com",
+ "pagefair.net"
+ ]
+ }
+ },
+ {
+ "Paid-To-Promote.net": {
+ "http://www.paid-to-promote.net/": [
+ "paid-to-promote.net"
+ ]
+ }
+ },
+ {
+ "Pardot": {
+ "http://www.pardot.com/": [
+ "pardot.com"
+ ]
+ }
+ },
+ {
+ "PayHit": {
+ "http://www.payhit.com/": [
+ "payhit.com"
+ ]
+ }
+ },
+ {
+ "Paypopup.com": {
+ "http://www.paypopup.com/": [
+ "lzjl.com",
+ "paypopup.com"
+ ]
+ }
+ },
+ {
+ "PebblePost": {
+ "https://www.pebblepost.com/": [
+ "pbbl.co"
+ ]
+ }
+ },
+ {
+ "Peer39": {
+ "http://www.peer39.com/": [
+ "peer39.com",
+ "peer39.net"
+ ]
+ }
+ },
+ {
+ "PeerFly": {
+ "http://peerfly.com/": [
+ "peerfly.com"
+ ]
+ }
+ },
+ {
+ "Performancing": {
+ "http://performancing.com/": [
+ "performancing.com"
+ ]
+ }
+ },
+ {
+ "PerimeterX": {
+ "https://www.perimeterx.com": [
+ "perimeterx.net"
+ ]
+ }
+ },
+ {
+ "Pheedo": {
+ "http://site.pheedo.com/": [
+ "pheedo.com"
+ ]
+ }
+ },
+ {
+ "Pictela": {
+ "http://www.pictela.com/": [
+ "pictela.com",
+ "pictela.net"
+ ]
+ }
+ },
+ {
+ "PinPoll": {
+ "https://pinpoll.com/": [
+ "pinpoll.com"
+ ]
+ }
+ },
+ {
+ "Pixel.sg": {
+ "http://www.pixel.sg/": [
+ "pixel.sg"
+ ]
+ }
+ },
+ {
+ "Piximedia": {
+ "http://www.piximedia.com/": [
+ "piximedia.com"
+ ]
+ }
+ },
+ {
+ "Pixlee": {
+ "https://www.pixlee.com/": [
+ "pixlee.com"
+ ]
+ }
+ },
+ {
+ "PLATFORM ONE": {
+ "http://www.platform-one.co.jp/": [
+ "platform-one.co.jp"
+ ]
+ }
+ },
+ {
+ "plista": {
+ "http://www.plista.com/": [
+ "plista.com"
+ ]
+ }
+ },
+ {
+ "PocketCents": {
+ "http://pocketcents.com/": [
+ "pocketcents.com"
+ ]
+ }
+ },
+ {
+ "Polar Mobile": {
+ "http://polarmobile.com": [
+ "mediavoice.com",
+ "polarmobile.com"
+ ]
+ }
+ },
+ {
+ "Politads": {
+ "http://politads.com/": [
+ "politads.com"
+ ]
+ }
+ },
+ {
+ "Polymorph": {
+ "http://getpolymorph.com/": [
+ "adsnative.com",
+ "getpolymorph.com"
+ ]
+ }
+ },
+ {
+ "Pontiflex": {
+ "http://www.pontiflex.com/": [
+ "pontiflex.com"
+ ]
+ }
+ },
+ {
+ "PopAds": {
+ "https://www.popads.net/": [
+ "popads.net",
+ "popadscdn.net"
+ ]
+ }
+ },
+ {
+ "PopRule": {
+ "http://poprule.com/": [
+ "gocampaignlive.com",
+ "poprule.com"
+ ]
+ }
+ },
+ {
+ "Popunder.ru": {
+ "http://popunder.ru/": [
+ "popunder.ru"
+ ]
+ }
+ },
+ {
+ "Po.st": {
+ "http://www.po.st/": [
+ "po.st"
+ ]
+ }
+ },
+ {
+ "Powerlinks": {
+ "https://www.powerlinks.com/": [
+ "powerlinks.com"
+ ]
+ }
+ },
+ {
+ "PPCProtect": {
+ "https://ppcprotect.com": [
+ "ppcprotect.com"
+ ]
+ }
+ },
+ {
+ "PrecisionClick": {
+ "http://www.precisionclick.com/": [
+ "precisionclick.com"
+ ]
+ }
+ },
+ {
+ "PredictAd": {
+ "http://www.predictad.com/": [
+ "predictad.com"
+ ]
+ }
+ },
+ {
+ "Pressflex": {
+ "http://www.pressflex.com/": [
+ "blogads.com",
+ "pressflex.com"
+ ]
+ }
+ },
+ {
+ "Prime Visibility": {
+ "http://www.primevisibility.com/": [
+ "adcde.com",
+ "addlvr.com",
+ "adonnetwork.com",
+ "adonnetwork.net",
+ "adtrgt.com",
+ "bannertgt.com",
+ "cptgt.com",
+ "cpvfeed.com",
+ "cpvtgt.com",
+ "dashboardad.net",
+ "popcde.com",
+ "primevisibility.com",
+ "sdfje.com",
+ "urtbk.com"
+ ]
+ }
+ },
+ {
+ "Primis": {
+ "https://www.primis.tech": [
+ "sekindo.com"
+ ]
+ }
+ },
+ {
+ "PrismApp": {
+ "https://www.prismapp.io/": [
+ "prismapp.io"
+ ]
+ }
+ },
+ {
+ "Proclivity": {
+ "http://www.proclivitymedia.com/": [
+ "proclivitymedia.com",
+ "proclivitysystems.com",
+ "pswec.com"
+ ]
+ }
+ },
+ {
+ "Project Wonderful": {
+ "http://www.projectwonderful.com/": [
+ "projectwonderful.com"
+ ]
+ }
+ },
+ {
+ "PrometheusIntelligenceTechnology": {
+ "https://prometheusintelligencetechnology.com/": [
+ "prometheusintelligencetechnology.com"
+ ]
+ }
+ },
+ {
+ "Propeller Ads": {
+ "http://propellerads.com/": [
+ "propellerads.com"
+ ]
+ }
+ },
+ {
+ "Prosperent": {
+ "http://prosperent.com/": [
+ "prosperent.com"
+ ]
+ }
+ },
+ {
+ "Protected Media": {
+ "http://www.protected.media/": [
+ "ad-score.com",
+ "protected.media"
+ ]
+ }
+ },
+ {
+ "Provers": {
+ "http://provers.pro": [
+ "provers.pro"
+ ]
+ }
+ },
+ {
+ "Psonstrentie": {
+ "http://psonstrentie.info": [
+ "psonstrentie.info"
+ ]
+ }
+ },
+ {
+ "Public-Idées": {
+ "http://www.publicidees.com/": [
+ "publicidees.com"
+ ]
+ }
+ },
+ {
+ "Publishers Clearing House": {
+ "http://www.pch.com/": [
+ "pch.com"
+ ]
+ }
+ },
+ {
+ "PubMatic": {
+ "http://www.pubmatic.com/": [
+ "pubmatic.com",
+ "revinet.com"
+ ]
+ }
+ },
+ {
+ "PulsePoint": {
+ "https://www.pulsepoint.com/": [
+ "pulsepoint.com"
+ ]
+ }
+ },
+ {
+ "quadrantOne": {
+ "http://www.quadrantone.com/": [
+ "quadrantone.com"
+ ]
+ }
+ },
+ {
+ "Quake Marketing": {
+ "http://quakemarketing.com/": [
+ "quakemarketing.com"
+ ]
+ }
+ },
+ {
+ "Quantcast": {
+ "http://www.quantcast.com/": [
+ "quantcast.com",
+ "quantcount.com",
+ "quantserve.com"
+ ]
+ }
+ },
+ {
+ "QuantumAdvertising": {
+ "http://quantum-advertising.com": [
+ "quantum-advertising.com"
+ ]
+ }
+ },
+ {
+ "QuinStreet": {
+ "http://quinstreet.com/": [
+ "qnsr.com",
+ "qsstats.com",
+ "quinstreet.com"
+ ]
+ }
+ },
+ {
+ "QUISMA": {
+ "https://quisma.com/": [
+ "iaded.com",
+ "quisma.com",
+ "quismatch.com",
+ "xaded.com",
+ "xmladed.com"
+ ]
+ }
+ },
+ {
+ "Radial": {
+ "https://www.radial.com": [
+ "gsicommerce.com",
+ "gsimedia.net"
+ ]
+ }
+ },
+ {
+ "Radiate Media": {
+ "http://www.radiatemedia.com/": [
+ "matchbin.com",
+ "radiatemedia.com"
+ ]
+ }
+ },
+ {
+ "RadiumOne": {
+ "http://www.radiumone.com/": [
+ "gwallet.com",
+ "radiumone.com"
+ ]
+ }
+ },
+ {
+ "Radius Marketing": {
+ "http://www.radiusmarketing.com/": [
+ "radiusmarketing.com"
+ ]
+ }
+ },
+ {
+ "Rambler": {
+ "http://www.rambler.ru/": [
+ "rambler.ru"
+ ]
+ }
+ },
+ {
+ "Rapleaf": {
+ "http://www.rapleaf.com/": [
+ "rapleaf.com",
+ "rlcdn.com"
+ ]
+ }
+ },
+ {
+ "ReachLocal": {
+ "http://www.reachlocal.com/": [
+ "reachlocal.com",
+ "rlcdn.net"
+ ]
+ }
+ },
+ {
+ "React2Media": {
+ "http://www.react2media.com/": [
+ "react2media.com"
+ ]
+ }
+ },
+ {
+ "Redux Media": {
+ "http://reduxmedia.com/": [
+ "reduxmedia.com"
+ ]
+ }
+ },
+ {
+ "Rekko": {
+ "http://rekko.com/": [
+ "convertglobal.com",
+ "rekko.com"
+ ]
+ }
+ },
+ {
+ "Reklamport": {
+ "http://www.reklamport.com/": [
+ "reklamport.com"
+ ]
+ }
+ },
+ {
+ "Reklam Store": {
+ "http://reklamstore.com/": [
+ "reklamstore.com"
+ ]
+ }
+ },
+ {
+ "Reklamz": {
+ "http://www.reklamz.com/": [
+ "reklamz.com"
+ ]
+ }
+ },
+ {
+ "Relevad": {
+ "http://www.relevad.com/": [
+ "relestar.com",
+ "relevad.com"
+ ]
+ }
+ },
+ {
+ "Renegade Internet": {
+ "http://www.renegadeinternet.com/": [
+ "advertserve.com",
+ "renegadeinternet.com"
+ ]
+ }
+ },
+ {
+ "Reporo": {
+ "http://www.reporo.com/": [
+ "buzzcity.com"
+ ]
+ }
+ },
+ {
+ "ResolutionMedia": {
+ "https://nonstoppartner.net/": [
+ "nonstoppartner.net"
+ ]
+ }
+ },
+ {
+ "Resolution Media": {
+ "http://resolutionmedia.com/": [
+ "resolutionmedia.com"
+ ]
+ }
+ },
+ {
+ "Resonate": {
+ "http://www.resonateinsights.com/": [
+ "reson8.com",
+ "resonateinsights.com",
+ "resonatenetworks.com"
+ ]
+ }
+ },
+ {
+ "Responsys": {
+ "http://www.responsys.com/": [
+ "responsys.com"
+ ]
+ }
+ },
+ {
+ "ReTargeter": {
+ "http://www.retargeter.com/": [
+ "retargeter.com"
+ ]
+ }
+ },
+ {
+ "Retirement Living": {
+ "www.retirement-living.com/": [
+ "blvdstatus.com",
+ "retirement-living.com"
+ ]
+ }
+ },
+ {
+ "RevContent": {
+ "http://revcontent.com/": [
+ "revcontent.com"
+ ]
+ }
+ },
+ {
+ "RevenueMax": {
+ "http://revenuemax.de/": [
+ "revenuemax.de"
+ ]
+ }
+ },
+ {
+ "Rhythm": {
+ "http://rhythmnewmedia.com/": [
+ "1rx.io",
+ "rhythmnewmedia.com",
+ "rhythmone.com",
+ "rhythmxchange.com",
+ "rnmd.net"
+ ]
+ }
+ },
+ {
+ "RichAudience": {
+ "https://richaudience.com/": [
+ "richaudience.com"
+ ]
+ }
+ },
+ {
+ "RichRelevance": {
+ "http://www.richrelevance.com/": [
+ "richrelevance.com"
+ ]
+ }
+ },
+ {
+ "RightAction": {
+ "http://rightaction.com/": [
+ "rightaction.com"
+ ]
+ }
+ },
+ {
+ "RMBN": {
+ "http://rmbn.net/": [
+ "rmbn.net",
+ "rmbn.ru"
+ ]
+ }
+ },
+ {
+ "RMM": {
+ "http://www.rmmonline.com/": [
+ "rmmonline.com"
+ ]
+ }
+ },
+ {
+ "Rocket Fuel": {
+ "http://rocketfuel.com/": [
+ "rfihub.com",
+ "rfihub.net",
+ "rocketfuel.com",
+ "ru4.com",
+ "xplusone.com"
+ ]
+ }
+ },
+ {
+ "Rovion": {
+ "http://www.rovion.com/": [
+ "rovion.com"
+ ]
+ }
+ },
+ {
+ "rtk": {
+ "http://rtk.io/": [
+ "rtk.io"
+ ]
+ }
+ },
+ {
+ "RubiconProject": {
+ "http://rubiconproject.com/": [
+ "adsbyisocket.com",
+ "isocket.com",
+ "rubiconproject.com"
+ ]
+ }
+ },
+ {
+ "RunAds": {
+ "http://www.runads.com/": [
+ "runads.com",
+ "rundsp.com"
+ ]
+ }
+ },
+ {
+ "RuTarget": {
+ "http://www.rutarget.ru/": [
+ "rutarget.ru"
+ ]
+ }
+ },
+ {
+ "Sabavision": {
+ "http://www.sabavision.com": [
+ "sabavision.com"
+ ]
+ }
+ },
+ {
+ "Sabre": {
+ "http://www.sabre.com/": [
+ "reztrack.com",
+ "sabre.com",
+ "sabrehospitality.com"
+ ]
+ }
+ },
+ {
+ "Salesforce.com": {
+ "http://www.salesforce.com/": [
+ "salesforce.com"
+ ]
+ }
+ },
+ {
+ "Samurai Factory": {
+ "http://www.samurai-factory.jp/": [
+ "samurai-factory.jp",
+ "shinobi.jp"
+ ]
+ }
+ },
+ {
+ "SAP": {
+ "https://www.sap.com": [
+ "seewhy.com"
+ ]
+ }
+ },
+ {
+ "Sapient": {
+ "http://www.sapient.com/": [
+ "bridgetrack.com",
+ "sapient.com"
+ ]
+ }
+ },
+ {
+ "SAS": {
+ "http://www.sas.com/": [
+ "aimatch.com",
+ "sas.com"
+ ]
+ }
+ },
+ {
+ "Scandinavian AdNetworks": {
+ "http://www.scandinavianadnetworks.com/": [
+ "scandinavianadnetworks.com"
+ ]
+ }
+ },
+ {
+ "Scribol": {
+ "http://scribol.com/": [
+ "scribol.com"
+ ]
+ }
+ },
+ {
+ "SearchForce": {
+ "http://www.searchforce.com/": [
+ "searchforce.com",
+ "searchforce.net"
+ ]
+ }
+ },
+ {
+ "Seevast": {
+ "http://www.seevast.com/": [
+ "kanoodle.com",
+ "pulse360.com",
+ "seevast.com",
+ "syndigonetworks.com"
+ ]
+ }
+ },
+ {
+ "Selectable Media": {
+ "http://selectablemedia.com/": [
+ "nabbr.com",
+ "selectablemedia.com"
+ ]
+ }
+ },
+ {
+ "Semantiqo": {
+ "http://semantiqo.com/": [
+ "semantiqo.com"
+ ]
+ }
+ },
+ {
+ "Semasio": {
+ "http://www.semasio.com/": [
+ "semasio.com",
+ "semasio.net"
+ ]
+ }
+ },
+ {
+ "SevenAds": {
+ "http://www.sevenads.net/": [
+ "sevenads.net"
+ ]
+ }
+ },
+ {
+ "SexInYourCity": {
+ "http://www.sexinyourcity.com/": [
+ "sexinyourcity.com"
+ ]
+ }
+ },
+ {
+ "ShaftTraffic": {
+ "https://shafttraffic.com": [
+ "libertystmedia.com"
+ ]
+ }
+ },
+ {
+ "ShareASale": {
+ "http://www.shareasale.com/": [
+ "shareasale.com"
+ ]
+ }
+ },
+ {
+ "Sharethrough": {
+ "http://sharethrough.com/": [
+ "sharethrough.com"
+ ]
+ }
+ },
+ {
+ "Shopzilla": {
+ "http://www.shopzilla.com/": [
+ "shopzilla.com"
+ ]
+ }
+ },
+ {
+ "Shortest": {
+ "http://shorte.st/": [
+ "shorte.st"
+ ]
+ }
+ },
+ {
+ "Silverpop": {
+ "http://www.silverpop.com/": [
+ "mkt51.net",
+ "pages05.net",
+ "silverpop.com",
+ "vtrenz.net"
+ ]
+ }
+ },
+ {
+ "Simpli.fi": {
+ "http://www.simpli.fi/": [
+ "simpli.fi"
+ ]
+ }
+ },
+ {
+ "SiteScout": {
+ "http://www.sitescout.com/": [
+ "sitescout.com"
+ ]
+ }
+ },
+ {
+ "Skimlinks": {
+ "http://skimlinks.com/": [
+ "skimlinks.com",
+ "skimresources.com"
+ ]
+ }
+ },
+ {
+ "Skupe Net": {
+ "http://www.skupenet.com/": [
+ "adcentriconline.com",
+ "skupenet.com"
+ ]
+ }
+ },
+ {
+ "Smaato": {
+ "http://www.smaato.com/": [
+ "smaato.com"
+ ]
+ }
+ },
+ {
+ "SmartAdServer": {
+ "http://smartadserver.com/": [
+ "smartadserver.com"
+ ]
+ }
+ },
+ {
+ "SmartyAds": {
+ "https://smartyads.com/": [
+ "smartyads.com"
+ ]
+ }
+ },
+ {
+ "Smiley Media": {
+ "http://www.smileymedia.com/": [
+ "smileymedia.com"
+ ]
+ }
+ },
+ {
+ "Smowtion": {
+ "http://smowtion.com/": [
+ "smowtion.com"
+ ]
+ }
+ },
+ {
+ "Snap": {
+ "http://www.snap.com/": [
+ "snap.com"
+ ]
+ }
+ },
+ {
+ "SocialChorus": {
+ "http://www.socialchorus.com/": [
+ "halogenmediagroup.com",
+ "halogennetwork.com",
+ "socialchorus.com"
+ ]
+ }
+ },
+ {
+ "SocialInterface": {
+ "http://socialinterface.com/": [
+ "ratevoice.com",
+ "socialinterface.com"
+ ]
+ }
+ },
+ {
+ "SocialTwist": {
+ "http://tellafriend.socialtwist.com/": [
+ "socialtwist.com"
+ ]
+ }
+ },
+ {
+ "sociomantic labs": {
+ "http://www.sociomantic.com/": [
+ "sociomantic.com"
+ ]
+ }
+ },
+ {
+ "Socital": {
+ "https://www.socital.com": [
+ "socital.com"
+ ]
+ }
+ },
+ {
+ "Sojern": {
+ "https://www.sojern.com": [
+ "sojern.com"
+ ]
+ }
+ },
+ {
+ "SomoAudience": {
+ "https://somoaudience.com/": [
+ "somoaudience.com"
+ ]
+ }
+ },
+ {
+ "Sonobi": {
+ "http://sonobi.com/": [
+ "sonobi.com"
+ ]
+ }
+ },
+ {
+ "sophus3": {
+ "http://www.sophus3.com/": [
+ "sophus3.co.uk",
+ "sophus3.com"
+ ]
+ }
+ },
+ {
+ "Sortable": {
+ "https://www.sortable.com/": [
+ "deployads.com"
+ ]
+ }
+ },
+ {
+ "Sovrn": {
+ "https://www.sovrn.com/": [
+ "sovrn.com"
+ ]
+ }
+ },
+ {
+ "Space Chimp Media": {
+ "http://spacechimpmedia.com/": [
+ "spacechimpmedia.com"
+ ]
+ }
+ },
+ {
+ "Sparklit": {
+ "http://www.sparklit.com/": [
+ "adbutler.com",
+ "sparklit.com"
+ ]
+ }
+ },
+ {
+ "Spark Studios": {
+ "http://www.sparkstudios.com/": [
+ "sparkstudios.com"
+ ]
+ }
+ },
+ {
+ "Specific Media": {
+ "http://www.specificmedia.com/": [
+ "adviva.co.uk",
+ "adviva.net",
+ "sitemeter.com",
+ "specificclick.net",
+ "specificmedia.co.uk",
+ "specificmedia.com"
+ ]
+ }
+ },
+ {
+ "Spectate": {
+ "http://spectate.com/": [
+ "spectate.com"
+ ]
+ }
+ },
+ {
+ "Sponge": {
+ "http://spongegroup.com/": [
+ "spongegroup.com"
+ ]
+ }
+ },
+ {
+ "Spongecell": {
+ "http://www.spongecell.com/": [
+ "spongecell.com"
+ ]
+ }
+ },
+ {
+ "SponsorAds": {
+ "http://www.sponsorads.de/": [
+ "sponsorads.de"
+ ]
+ }
+ },
+ {
+ "Spot200": {
+ "http://spot200.com/": [
+ "spot200.com"
+ ]
+ }
+ },
+ {
+ "SpotX": {
+ "https://www.spotx.tv": [
+ "spotx.tv"
+ ]
+ }
+ },
+ {
+ "SpotXchange": {
+ "http://www.spotxchange.com/": [
+ "spotxchange.com"
+ ]
+ }
+ },
+ {
+ "SpringServe": {
+ "https://springserve.com/": [
+ "springserve.com"
+ ]
+ }
+ },
+ {
+ "StackAdapt": {
+ "https://www.stackadapt.com/": [
+ "stackadapt.com"
+ ]
+ }
+ },
+ {
+ "StarGames": {
+ "https://www.stargames.net/": [
+ "stargamesaffiliate.com"
+ ]
+ }
+ },
+ {
+ "SteelHouse": {
+ "http://www.steelhouse.com/": [
+ "steelhouse.com",
+ "steelhousemedia.com"
+ ]
+ }
+ },
+ {
+ "Storygize": {
+ "http://www.storygize.com/": [
+ "storygize.com",
+ "storygize.net"
+ ]
+ }
+ },
+ {
+ "Streamray": {
+ "http://streamray.com/": [
+ "cams.com",
+ "streamray.com"
+ ]
+ }
+ },
+ {
+ "StrikeAd": {
+ "http://www.strikead.com/": [
+ "strikead.com"
+ ]
+ }
+ },
+ {
+ "StrongMail": {
+ "http://www.strongmail.com/": [
+ "popularmedia.com"
+ ]
+ }
+ },
+ {
+ "Struq": {
+ "http://struq.com/": [
+ "struq.com"
+ ]
+ }
+ },
+ {
+ "Sublime Skinz": {
+ "http://sublime.xyz/": [
+ "ayads.co",
+ "sublime.xyz"
+ ]
+ }
+ },
+ {
+ "Suite 66": {
+ "http://www.suite66.com/": [
+ "suite66.com"
+ ]
+ }
+ },
+ {
+ "Summit": {
+ "http://www.summit.co.uk/": [
+ "summitmedia.co.uk"
+ ]
+ }
+ },
+ {
+ "Superfish": {
+ "http://www.superfish.com/": [
+ "superfish.com"
+ ]
+ }
+ },
+ {
+ "SupersonicAds": {
+ "http://www.supersonicads.com/": [
+ "supersonicads.com"
+ ]
+ }
+ },
+ {
+ "Survata": {
+ "https://www.survata.com/": [
+ "survata.com"
+ ]
+ }
+ },
+ {
+ "Switch": {
+ "http://www.switchconcepts.com/": [
+ "ethicalads.net",
+ "switchadhub.com",
+ "switchconcepts.co.uk",
+ "switchconcepts.com"
+ ]
+ }
+ },
+ {
+ "Swoop": {
+ "http://swoop.com/": [
+ "swoop.com"
+ ]
+ }
+ },
+ {
+ "SymphonyAM": {
+ "http://www.factortg.com/": [
+ "factortg.com"
+ ]
+ }
+ },
+ {
+ "Syncapse": {
+ "http://www.syncapse.com/": [
+ "clickable.net",
+ "syncapse.com"
+ ]
+ }
+ },
+ {
+ "Syrup Ad": {
+ "http://adotsolution.com/": [
+ "adotsolution.com"
+ ]
+ }
+ },
+ {
+ "Taboola": {
+ "https://www.taboola.com/": [
+ "perfectmarket.com",
+ "taboola.com"
+ ]
+ }
+ },
+ {
+ "Tailsweep": {
+ "http://www.tailsweep.com/": [
+ "tailsweep.com"
+ ]
+ }
+ },
+ {
+ "Taleria": {
+ "https://outstream.telaria.com/": [
+ "freeskreen.com"
+ ]
+ }
+ },
+ {
+ "Tapad": {
+ "http://www.tapad.com/": [
+ "tapad.com"
+ ]
+ }
+ },
+ {
+ "Tapgage": {
+ "http://www.tapgage.com/": [
+ "bizmey.com",
+ "tapgage.com"
+ ]
+ }
+ },
+ {
+ "TapIt!": {
+ "http://tapit.com/": [
+ "tapit.com"
+ ]
+ }
+ },
+ {
+ "Tap.me": {
+ "http://tap.me/": [
+ "tap.me"
+ ]
+ }
+ },
+ {
+ "Targetix": {
+ "http://targetix.net/": [
+ "targetix.net"
+ ]
+ }
+ },
+ {
+ "Tatto Media": {
+ "http://tattomedia.com/": [
+ "quicknoodles.com",
+ "tattomedia.com"
+ ]
+ }
+ },
+ {
+ "Teadma": {
+ "http://www.teadma.com/": [
+ "teadma.com"
+ ]
+ }
+ },
+ {
+ "Teads.tv": {
+ "http://teads.tv/": [
+ "ebuzzing.com",
+ "teads.tv"
+ ]
+ }
+ },
+ {
+ "Technorati": {
+ "http://technorati.com/": [
+ "technorati.com",
+ "technoratimedia.com"
+ ]
+ }
+ },
+ {
+ "TellApart": {
+ "http://tellapart.com/": [
+ "tellapart.com",
+ "tellapt.com"
+ ]
+ }
+ },
+ {
+ "Telstra": {
+ "http://www.telstra.com.au/": [
+ "sensis.com.au",
+ "sensisdata.com.au",
+ "sensisdigitalmedia.com.au",
+ "telstra.com.au"
+ ]
+ }
+ },
+ {
+ "Terra": {
+ "http://www.terra.com.br/": [
+ "eztargetmedia.com",
+ "terra.com.br"
+ ]
+ }
+ },
+ {
+ "The Numa Group": {
+ "http://www.thenumagroup.com/": [
+ "hittail.com",
+ "thenumagroup.com"
+ ]
+ }
+ },
+ {
+ "The Search Agency": {
+ "http://www.thesearchagency.com/": [
+ "thesearchagency.com",
+ "thesearchagency.net"
+ ]
+ }
+ },
+ {
+ "The Trade Desk": {
+ "http://thetradedesk.com/": [
+ "adsrvr.org",
+ "thetradedesk.com"
+ ]
+ }
+ },
+ {
+ "Think Realtime": {
+ "http://www.thinkrealtime.com/": [
+ "echosearch.com",
+ "esm1.net",
+ "thinkrealtime.com"
+ ]
+ }
+ },
+ {
+ "Tinder": {
+ "http://tinder.com/": [
+ "carbonads.com",
+ "tinder.com"
+ ]
+ }
+ },
+ {
+ "TiqIQ": {
+ "http://www.tiqiq.com/": [
+ "tiqiq.com"
+ ]
+ }
+ },
+ {
+ "Tisoomi": {
+ "http://www.tisoomi.com/": [
+ "adternal.com",
+ "tisoomi.com"
+ ]
+ }
+ },
+ {
+ "TLVMedia": {
+ "http://tlvmedia.com/": [
+ "tlvmedia.com"
+ ]
+ }
+ },
+ {
+ "Todacell": {
+ "http://www.todacell.com/": [
+ "todacell.com"
+ ]
+ }
+ },
+ {
+ "ToneFuse": {
+ "http://tonefuse.com/": [
+ "tonefuse.com"
+ ]
+ }
+ },
+ {
+ "ToneMedia": {
+ "http://tonemedia.com/": [
+ "clickfuse.com",
+ "tonemedia.com"
+ ]
+ }
+ },
+ {
+ "TouchCommerce": {
+ "http://www.touchcommerce.com/": [
+ "inq.com",
+ "touchcommerce.com"
+ ]
+ }
+ },
+ {
+ "TrackingSoft": {
+ "http://trackingsoft.com/": [
+ "trackingsoft.com"
+ ]
+ }
+ },
+ {
+ "Tradedoubler": {
+ "http://www.tradedoubler.com/": [
+ "tradedoubler.com"
+ ]
+ }
+ },
+ {
+ "TradeTracker": {
+ "http://www.tradetracker.com/": [
+ "tradetracker.com",
+ "tradetracker.net"
+ ]
+ }
+ },
+ {
+ "TrafficHaus": {
+ "http://www.traffichaus.com/": [
+ "traffichaus.com",
+ "traffichouse.com"
+ ]
+ }
+ },
+ {
+ "TrafficRevenue": {
+ "http://www.trafficrevenue.net/": [
+ "trafficrevenue.net"
+ ]
+ }
+ },
+ {
+ "Traffiq": {
+ "http://www.traffiq.com/": [
+ "traffiq.com"
+ ]
+ }
+ },
+ {
+ "Trafmag": {
+ "http://trafmag.com/": [
+ "trafmag.com"
+ ]
+ }
+ },
+ {
+ "Traverse": {
+ "http://www.traversedata.com/": [
+ "traversedlp.com"
+ ]
+ }
+ },
+ {
+ "Travora Media": {
+ "http://www.travoramedia.com/": [
+ "traveladnetwork.com",
+ "traveladvertising.com",
+ "travoramedia.com"
+ ]
+ }
+ },
+ {
+ "Tremor Video": {
+ "http://www.tremorvideo.com/": [
+ "scanscout.com",
+ "tmnetads.com",
+ "tremorhub.com",
+ "tremormedia.com",
+ "tremorvideo.com"
+ ]
+ }
+ },
+ {
+ "Triggit": {
+ "http://triggit.com/": [
+ "triggit.com"
+ ]
+ }
+ },
+ {
+ "TripleLift": {
+ "http://triplelift.com/": [
+ "3lift.com",
+ "triplelift.com"
+ ]
+ }
+ },
+ {
+ "TruEffect": {
+ "http://www.trueffect.com/": [
+ "adlegend.com",
+ "trueffect.com"
+ ]
+ }
+ },
+ {
+ "TrustX": {
+ "https://trustx.org/": [
+ "trustx.org"
+ ]
+ }
+ },
+ {
+ "TubeMogul": {
+ "http://www.tubemogul.com/": [
+ "tmogul.com",
+ "tubemogul.com"
+ ]
+ }
+ },
+ {
+ "Twelvefold": {
+ "http://www.twelvefold.com/": [
+ "buzzlogic.com",
+ "twelvefold.com"
+ ]
+ }
+ },
+ {
+ "Twitter": {
+ "https://twitter.com/": [
+ "ads-twitter.com"
+ ]
+ }
+ },
+ {
+ "Twyn Group": {
+ "http://www.twyn.com/": [
+ "twyn-group.com",
+ "twyn.com"
+ ]
+ }
+ },
+ {
+ "Tyroo": {
+ "http://www.tyroo.com/": [
+ "tyroo.com"
+ ]
+ }
+ },
+ {
+ "ucfunnel": {
+ "https://www.ucfunnel.com/": [
+ "aralego.com",
+ "ucfunnel.com"
+ ]
+ }
+ },
+ {
+ "uCoz": {
+ "http://www.ucoz.com/": [
+ "ucoz.ae",
+ "ucoz.br",
+ "ucoz.com",
+ "ucoz.du",
+ "ucoz.fr",
+ "ucoz.net",
+ "ucoz.ru"
+ ]
+ }
+ },
+ {
+ "Unanimis": {
+ "http://www.unanimis.co.uk/": [
+ "unanimis.co.uk"
+ ]
+ }
+ },
+ {
+ "Underdog Media": {
+ "http://www.underdogmedia.com/": [
+ "udmserve.net",
+ "underdogmedia.com"
+ ]
+ }
+ },
+ {
+ "Undertone": {
+ "http://www.undertone.com/": [
+ "undertone.com",
+ "undertonenetworks.com",
+ "undertonevideo.com"
+ ]
+ }
+ },
+ {
+ "UniQlick": {
+ "http://www.uniqlick.com/": [
+ "51network.com",
+ "uniqlick.com",
+ "wanmo.com"
+ ]
+ }
+ },
+ {
+ "Unruly": {
+ "https://unruly.co/": [
+ "unrulymedia.com"
+ ]
+ }
+ },
+ {
+ "Upland": {
+ "https://uplandsoftware.com/": [
+ "leadlander.com",
+ "trackalyzer.com"
+ ]
+ }
+ },
+ {
+ "up-value": {
+ "http://www.up-value.de/": [
+ "up-value.de"
+ ]
+ }
+ },
+ {
+ "Value Ad": {
+ "http://valuead.com/": [
+ "valuead.com"
+ ]
+ }
+ },
+ {
+ "Various": {
+ "http://www.various.com/": [
+ "amigos.com",
+ "getiton.com",
+ "medley.com",
+ "nostringsattached.com",
+ "various.com"
+ ]
+ }
+ },
+ {
+ "Vdopia": {
+ "http://www.vdopia.com/": [
+ "ivdopia.com",
+ "vdopia.com"
+ ]
+ }
+ },
+ {
+ "Veeseo": {
+ "http://veeseo.com": [
+ "veeseo.com"
+ ]
+ }
+ },
+ {
+ "Velocity Media": {
+ "http://adsvelocity.com/": [
+ "adsvelocity.com"
+ ]
+ }
+ },
+ {
+ "Velti": {
+ "http://www.velti.com/": [
+ "mobclix.com",
+ "velti.com"
+ ]
+ }
+ },
+ {
+ "Vemba": {
+ "https://www.vemba.com/": [
+ "vemba.com"
+ ]
+ }
+ },
+ {
+ "Venatus Media": {
+ "http://venatusmedia.com": [
+ "venatusmedia.com"
+ ]
+ }
+ },
+ {
+ "Vendemore": {
+ "https://vendemore.com/": [
+ "vendemore.com"
+ ]
+ }
+ },
+ {
+ "Vendio": {
+ "http://www.vendio.com/": [
+ "singlefeed.com",
+ "vendio.com"
+ ]
+ }
+ },
+ {
+ "Veoxa": {
+ "http://www.veoxa.com/": [
+ "veoxa.com"
+ ]
+ }
+ },
+ {
+ "Veremedia": {
+ "http://www.veremedia.com/": [
+ "veremedia.com"
+ ]
+ }
+ },
+ {
+ "VerticalHealth": {
+ "https://www.verticalhealth.com/": [
+ "verticalhealth.net"
+ ]
+ }
+ },
+ {
+ "VerticalResponse": {
+ "http://www.verticalresponse.com/": [
+ "verticalresponse.com",
+ "vresp.com"
+ ]
+ }
+ },
+ {
+ "Vibrant Media": {
+ "http://www.vibrantmedia.com/": [
+ "intellitxt.com",
+ "picadmedia.com",
+ "vibrantmedia.com"
+ ]
+ }
+ },
+ {
+ "VideoIntelligence": {
+ "https://www.vi.ai/": [
+ "vi.ai"
+ ]
+ }
+ },
+ {
+ "VigLink": {
+ "http://www.viglink.com/": [
+ "viglink.com"
+ ]
+ }
+ },
+ {
+ "VisibleBrands": {
+ "http://www.visbrands.com/": [
+ "visbrands.com"
+ ]
+ }
+ },
+ {
+ "Visible Measures": {
+ "http://www.visiblemeasures.com/": [
+ "viewablemedia.net",
+ "visiblemeasures.com"
+ ]
+ }
+ },
+ {
+ "VisualDNA": {
+ "http://www.visualdna.com/": [
+ "vdna-assets.com",
+ "visualdna-stats.com",
+ "visualdna.com"
+ ]
+ }
+ },
+ {
+ "Vizu": {
+ "http://www.vizu.com/": [
+ "vizu.com"
+ ]
+ }
+ },
+ {
+ "Vizury": {
+ "http://www.vizury.com/": [
+ "vizury.com"
+ ]
+ }
+ },
+ {
+ "Vserv": {
+ "http://www.vserv.com/": [
+ "vserv.com",
+ "vserv.mobi"
+ ]
+ }
+ },
+ {
+ "Vuble": {
+ "https://vuble.tv/us/": [
+ "mediabong.com"
+ ]
+ }
+ },
+ {
+ "Wahoha": {
+ "http://wahoha.com/": [
+ "contentwidgets.net",
+ "wahoha.com"
+ ]
+ }
+ },
+ {
+ "Wayfair": {
+ "https://www.wayfair.com/": [
+ "wayfair.com"
+ ]
+ }
+ },
+ {
+ "WebAds": {
+ "http://www.webads.co.uk/": [
+ "webads.co.uk"
+ ]
+ }
+ },
+ {
+ "Web.com": {
+ "http://www.web.com/": [
+ "feedperfect.com",
+ "web.com"
+ ]
+ }
+ },
+ {
+ "WebGozar.com": {
+ "http://www.webgozar.com/": [
+ "webgozar.com",
+ "webgozar.ir"
+ ]
+ }
+ },
+ {
+ "Webmecanik": {
+ "https://www.webmecanik.com/": [
+ "webmecanik.com"
+ ]
+ }
+ },
+ {
+ "WebMetro": {
+ "http://www.webmetro.com/": [
+ "dsmmadvantage.com",
+ "webmetro.com"
+ ]
+ }
+ },
+ {
+ "Weborama": {
+ "http://weborama.com/": [
+ "weborama.com",
+ "weborama.fr"
+ ]
+ }
+ },
+ {
+ "Webtraffic": {
+ "http://www.webtraffic.se/": [
+ "webtraffic.no",
+ "webtraffic.se"
+ ]
+ }
+ },
+ {
+ "WideOrbit": {
+ "https://www.wideorbit.com/": [
+ "dep-x.com"
+ ]
+ }
+ },
+ {
+ "WiredMinds": {
+ "http://www.wiredminds.com/": [
+ "wiredminds.com",
+ "wiredminds.de"
+ ]
+ }
+ },
+ {
+ "Wishabi": {
+ "http://wishabi.com": [
+ "wishabi.com",
+ "wishabi.net"
+ ]
+ }
+ },
+ {
+ "WordStream": {
+ "http://www.wordstream.com/": [
+ "wordstream.com"
+ ]
+ }
+ },
+ {
+ "WPP": {
+ "http://www.wpp.com/": [
+ "247realmedia.com",
+ "accelerator-media.com",
+ "acceleratorusa.com",
+ "decdna.net",
+ "decideinteractive.com",
+ "gmads.net",
+ "groupm.com",
+ "kantarmedia.com",
+ "mecglobal.com",
+ "mindshare.nl",
+ "mookie1.com",
+ "pm14.com",
+ "realmedia.com",
+ "targ.ad",
+ "themig.com",
+ "wpp.com",
+ "xaxis.com"
+ ]
+ }
+ },
+ {
+ "xAd": {
+ "http://www.xad.com/": [
+ "xad.com"
+ ]
+ }
+ },
+ {
+ "Xertive Media": {
+ "http://www.xertivemedia.com/": [
+ "admanager-xertive.com",
+ "xertivemedia.com"
+ ]
+ }
+ },
+ {
+ "xplosion interactive": {
+ "http://www.xplosion.de/": [
+ "xplosion.de"
+ ]
+ }
+ },
+ {
+ "Xrost DS": {
+ "http://www.adplan-ds.com/": [
+ "adplan-ds.com"
+ ]
+ }
+ },
+ {
+ "Yabuka": {
+ "http://www.yabuka.com/": [
+ "yabuka.com"
+ ]
+ }
+ },
+ {
+ "Yahoo!": {
+ "http://www.yahoo.com/": [
+ "adinterax.com",
+ "adrevolver.com",
+ "ads.yahoo.com",
+ "adserver.yahoo.com",
+ "advertising.yahoo.com",
+ "bluelithium.com",
+ "dapper.net",
+ "flurry.com",
+ "interclick.com",
+ "marketingsolutions.yahoo.com",
+ "overture.com",
+ "rightmedia.com",
+ "rmxads.com",
+ "secure-adserver.com",
+ "thewheelof.com",
+ "yieldmanager.com",
+ "yieldmanager.net",
+ "yldmgrimg.net"
+ ]
+ }
+ },
+ {
+ "Yandex": {
+ "http://www.yandex.com/": [
+ "adfox.yandex.ru",
+ "an.yandex.ru",
+ "awaps.yandex.ru",
+ "mc.yandex.ru",
+ "moikrug.ru",
+ "web-visor.com",
+ "yandex.ru/clck/click",
+ "yandex.ru/clck/counter",
+ "yandex.ru/cycounter",
+ "yandex.ru/portal/set/any",
+ "yandex.ru/set/s/rsya-tag-users/data"
+ ]
+ }
+ },
+ {
+ "Ybrant Digital": {
+ "http://www.ybrantdigital.com/": [
+ "addynamix.com",
+ "adserverplus.com",
+ "oridian.com",
+ "ybrantdigital.com"
+ ]
+ }
+ },
+ {
+ "YD": {
+ "http://www.ydworld.com/": [
+ "ydworld.com",
+ "yieldivision.com"
+ ]
+ }
+ },
+ {
+ "YellowHammer": {
+ "http://www.yhmg.com/": [
+ "attracto.com",
+ "clickhype.com",
+ "yellowhammermg.com",
+ "yhmg.com"
+ ]
+ }
+ },
+ {
+ "Yes Ads": {
+ "http://yesads.com/": [
+ "yesads.com"
+ ]
+ }
+ },
+ {
+ "YieldAds": {
+ "http://yieldads.com/": [
+ "yieldads.com"
+ ]
+ }
+ },
+ {
+ "YieldBids": {
+ "http://ybx.io/": [
+ "ybx.io"
+ ]
+ }
+ },
+ {
+ "YieldBot": {
+ "http://yieldbot.com/": [
+ "yldbt.com"
+ ]
+ }
+ },
+ {
+ "YieldBuild": {
+ "http://yieldbuild.com/": [
+ "yieldbuild.com"
+ ]
+ }
+ },
+ {
+ "Yieldify": {
+ "https://www.yieldify.com/": [
+ "yieldify.com"
+ ]
+ }
+ },
+ {
+ "Yieldlab": {
+ "http://www.yieldlab.de/": [
+ "yieldlab.de",
+ "yieldlab.net"
+ ]
+ }
+ },
+ {
+ "Yieldmo": {
+ "https://yieldmo.com": [
+ "yieldmo.com"
+ ]
+ }
+ },
+ {
+ "YieldNexus": {
+ "https://www.yieldnexus.com/": [
+ "ynxs.io"
+ ]
+ }
+ },
+ {
+ "YOC": {
+ "http://group.yoc.com/": [
+ "yoc-performance.com",
+ "yoc.com"
+ ]
+ }
+ },
+ {
+ "Yoggrt": {
+ "http://www.yoggrt.com/": [
+ "yoggrt.com"
+ ]
+ }
+ },
+ {
+ "youknowbest": {
+ "http://www.youknowbest.com/": [
+ "youknowbest.com"
+ ]
+ }
+ },
+ {
+ "YuMe": {
+ "http://www.yume.com/": [
+ "yume.com",
+ "yumenetworks.com"
+ ]
+ }
+ },
+ {
+ "ZafulAffiliate": {
+ "https://affiliate.zaful.com/": [
+ "affasi.com",
+ "gw-ec.com",
+ "zaful.com"
+ ]
+ }
+ },
+ {
+ "Zango": {
+ "http://www.zango.com/": [
+ "metricsdirect.com",
+ "zango.com"
+ ]
+ }
+ },
+ {
+ "zanox": {
+ "http://www.zanox.com/": [
+ "buy.at",
+ "zanox-affiliate.de",
+ "zanox.com"
+ ]
+ }
+ },
+ {
+ "zapunited": {
+ "http://www.zapunited.com/": [
+ "zaparena.com",
+ "zapunited.com"
+ ]
+ }
+ },
+ {
+ "ZEDO": {
+ "http://www.zedo.com/": [
+ "zedo.com",
+ "zincx.com"
+ ]
+ }
+ },
+ {
+ "Zefir": {
+ "https://ze-fir.com/": [
+ "ze-fir.com"
+ ]
+ }
+ },
+ {
+ "Zemanta": {
+ "http://www.zemanta.com/": [
+ "zemanta.com"
+ ]
+ }
+ },
+ {
+ "ZestAd": {
+ "http://www.zestad.com/": [
+ "zestad.com"
+ ]
+ }
+ },
+ {
+ "Zeta Email Solutions": {
+ "http://www.zetaemailsolutions.com/": [
+ "insightgrit.com",
+ "zetaemailsolutions.com"
+ ]
+ }
+ },
+ {
+ "Zumobi": {
+ "http://www.zumobi.com/": [
+ "zumobi.com"
+ ]
+ }
+ },
+ {
+ "ZypMedia": {
+ "http://www.zypmedia.com/": [
+ "extend.tv",
+ "zypmedia.com"
+ ]
+ }
+ }
+ ],
+ "Content": [
+ {
+ "33Across": {
+ "http://33across.com/": [
+ "tynt.com"
+ ]
+ }
+ },
+ {
+ "ActivEngage": {
+ "http://www.activengage.com/": [
+ "activengage.com"
+ ]
+ }
+ },
+ {
+ "Adap.tv": {
+ "http://adap.tv/": [
+ "adap.tv"
+ ]
+ }
+ },
+ {
+ "Adobe": {
+ "http://www.adobe.com/": [
+ "adobe.com",
+ "fyre.co",
+ "livefyre.com",
+ "typekit.com"
+ ]
+ }
+ },
+ {
+ "Akamai": {
+ "http://www.akamai.com/": [
+ "abmr.net",
+ "akamai.com",
+ "edgesuite.net"
+ ]
+ }
+ },
+ {
+ "AKQA": {
+ "http://www.akqa.com/": [
+ "akqa.com",
+ "srtk.net"
+ ]
+ }
+ },
+ {
+ "Amazon.com": {
+ "http://www.amazon.com/": [
+ "alexa.com",
+ "amazon.com",
+ "cloudfront.net"
+ ]
+ }
+ },
+ {
+ "AOL": {
+ "http://www.aol.com/": [
+ "5min.com",
+ "aim.com",
+ "aol.com",
+ "aolanswers.com",
+ "aolcdn.com",
+ "aoltechguru.com",
+ "autoblog.com",
+ "cambio.com",
+ "dailyfinance.com",
+ "editions.com",
+ "engadget.com",
+ "games.com",
+ "homesessive.com",
+ "huffingtonpost.com",
+ "joystiq.com",
+ "kitchendaily.com",
+ "makers.com",
+ "mandatory.com",
+ "mapquest.com",
+ "moviefone.com",
+ "noisecreep.com",
+ "patch.com",
+ "pawnation.com",
+ "shortcuts.com",
+ "shoutcast.com",
+ "spinner.com",
+ "stylelist.com",
+ "stylemepretty.com",
+ "surphace.com",
+ "techcrunch.com",
+ "theboombox.com",
+ "theboot.com",
+ "tuaw.com",
+ "userplane.com",
+ "winamp.com"
+ ]
+ }
+ },
+ {
+ "Automattic": {
+ "http://automattic.com/": [
+ "automattic.com",
+ "gravatar.com",
+ "intensedebate.com"
+ ]
+ }
+ },
+ {
+ "Baynote": {
+ "http://www.baynote.com/": [
+ "baynote.com",
+ "baynote.net"
+ ]
+ }
+ },
+ {
+ "Bazaarvoice": {
+ "http://www.bazaarvoice.com/": [
+ "bazaarvoice.com"
+ ]
+ }
+ },
+ {
+ "BigDoor": {
+ "http://www.bigdoor.com/": [
+ "bigdoor.com",
+ "onetruefan.com"
+ ]
+ }
+ },
+ {
+ "Brightcove": {
+ "http://www.brightcove.com/": [
+ "brightcove.com"
+ ]
+ }
+ },
+ {
+ "Browser-Update.org": {
+ "www.browser-update.org/": [
+ "browser-update.org"
+ ]
+ }
+ },
+ {
+ "BTBuckets": {
+ "http://btbuckets.com/": [
+ "btbuckets.com"
+ ]
+ }
+ },
+ {
+ "Buffer": {
+ "http://bufferapp.com/": [
+ "bufferapp.com"
+ ]
+ }
+ },
+ {
+ "Bunchball": {
+ "http://www.bunchball.com/": [
+ "bunchball.com"
+ ]
+ }
+ },
+ {
+ "buySAFE": {
+ "http://www.buysafe.com/": [
+ "buysafe.com"
+ ]
+ }
+ },
+ {
+ "BuzzFeed": {
+ "http://www.buzzfeed.com/": [
+ "buzzfed.com",
+ "buzzfeed.com"
+ ]
+ }
+ },
+ {
+ "Cbox": {
+ "http://www.cbox.ws/": [
+ "cbox.ws"
+ ]
+ }
+ },
+ {
+ "CBS Interactive": {
+ "http://www.cbsinteractive.com/": [
+ "cbsinteractive.com",
+ "com.com"
+ ]
+ }
+ },
+ {
+ "Cedexis": {
+ "http://www.cedexis.com/": [
+ "cedexis.com",
+ "cedexis.net"
+ ]
+ }
+ },
+ {
+ "Certona": {
+ "http://www.certona.com/": [
+ "certona.com",
+ "res-x.com"
+ ]
+ }
+ },
+ {
+ "ClipSyndicate": {
+ "http://www.clipsyndicate.com/": [
+ "clipsyndicate.com"
+ ]
+ }
+ },
+ {
+ "Collarity": {
+ "http://www.collarity.com/": [
+ "collarity.com"
+ ]
+ }
+ },
+ {
+ "Conduit": {
+ "http://www.conduit.com/": [
+ "conduit-banners.com",
+ "conduit-services.com",
+ "conduit.com",
+ "wibiya.com"
+ ]
+ }
+ },
+ {
+ "Congoo": {
+ "http://www.congoo.com/": [
+ "congoo.com"
+ ]
+ }
+ },
+ {
+ "Contact At Once!": {
+ "http://www.contactatonce.com/": [
+ "contactatonce.com"
+ ]
+ }
+ },
+ {
+ "Conviva": {
+ "http://www.conviva.com/": [
+ "conviva.com"
+ ]
+ }
+ },
+ {
+ "DailyMe": {
+ "http://dailyme.com/": [
+ "dailyme.com",
+ "newstogram.com"
+ ]
+ }
+ },
+ {
+ "DataSift": {
+ "http://datasift.com/": [
+ "datasift.com",
+ "tweetmeme.com"
+ ]
+ }
+ },
+ {
+ "Disqus": {
+ "http://disqus.com/": [
+ "disqus.com"
+ ]
+ }
+ },
+ {
+ "Echo": {
+ "http://aboutecho.com/": [
+ "aboutecho.com",
+ "haloscan.com",
+ "js-kit.com"
+ ]
+ }
+ },
+ {
+ "Facebook": {
+ "http://www.facebook.com/": [
+ "fbcdn.net",
+ "instagram.com",
+ "messenger.com"
+ ]
+ }
+ },
+ {
+ "Flattr": {
+ "http://flattr.com/": [
+ "flattr.com"
+ ]
+ }
+ },
+ {
+ "FreeWheel": {
+ "http://www.freewheel.tv/": [
+ "freewheel.tv",
+ "fwmrm.net"
+ ]
+ }
+ },
+ {
+ "Genius.com": {
+ "http://www.genius.com/": [
+ "genius.com"
+ ]
+ }
+ },
+ {
+ "Get Satisfaction": {
+ "https://getsatisfaction.com/": [
+ "getsatisfaction.com"
+ ]
+ }
+ },
+ {
+ "Gigya": {
+ "http://www.gigya.com/": [
+ "gigcount.com",
+ "gigya.com"
+ ]
+ }
+ },
+ {
+ "Global Takeoff": {
+ "http://www.globaltakeoff.com/": [
+ "globaltakeoff.com",
+ "globaltakeoff.net"
+ ]
+ }
+ },
+ {
+ "GoGrid": {
+ "http://www.gogrid.com/": [
+ "formalyzer.com",
+ "gogrid.com",
+ "komli.net"
+ ]
+ }
+ },
+ {
+ "Google": {
+ "http://www.google.com/": [
+ "accounts.google.com",
+ "apis.google.com",
+ "appengine.google.com",
+ "apture.com",
+ "blogger.com",
+ "books.google.com",
+ "checkout.google.com",
+ "chrome.google.com",
+ "code.google.com",
+ "codesearch.google.com",
+ "docs.google.com",
+ "drive.google.com",
+ "earth.google.com",
+ "encrypted.google.com",
+ "feedburner.com",
+ "feedburner.google.com",
+ "feedproxy.google.com",
+ "finance.google.com",
+ "ggpht.com",
+ "gmodules.com",
+ "google-melange.com",
+ "google.ad",
+ "google.ae",
+ "google.al",
+ "google.am",
+ "google.as",
+ "google.at",
+ "google.az",
+ "google.ba",
+ "google.be",
+ "google.bf",
+ "google.bg",
+ "google.bi",
+ "google.bj",
+ "google.bs",
+ "google.bt",
+ "google.by",
+ "google.ca",
+ "google.cat",
+ "google.cd",
+ "google.cf",
+ "google.cg",
+ "google.ch",
+ "google.ci",
+ "google.cl",
+ "google.cm",
+ "google.cn",
+ "google.co.ao",
+ "google.co.bw",
+ "google.co.ck",
+ "google.co.cr",
+ "google.co.id",
+ "google.co.il",
+ "google.co.in",
+ "google.co.jp",
+ "google.co.ke",
+ "google.co.kr",
+ "google.co.ls",
+ "google.co.ma",
+ "google.co.mz",
+ "google.co.nz",
+ "google.co.th",
+ "google.co.tz",
+ "google.co.ug",
+ "google.co.uk",
+ "google.co.uz",
+ "google.co.ve",
+ "google.co.vi",
+ "google.co.za",
+ "google.co.zm",
+ "google.co.zw",
+ "google.com",
+ "google.com.af",
+ "google.com.ag",
+ "google.com.ai",
+ "google.com.ar",
+ "google.com.au",
+ "google.com.bd",
+ "google.com.bh",
+ "google.com.bn",
+ "google.com.bo",
+ "google.com.br",
+ "google.com.bz",
+ "google.com.co",
+ "google.com.cu",
+ "google.com.cy",
+ "google.com.do",
+ "google.com.ec",
+ "google.com.eg",
+ "google.com.et",
+ "google.com.fj",
+ "google.com.gh",
+ "google.com.gi",
+ "google.com.gt",
+ "google.com.hk",
+ "google.com.jm",
+ "google.com.kh",
+ "google.com.kw",
+ "google.com.lb",
+ "google.com.ly",
+ "google.com.mm",
+ "google.com.mt",
+ "google.com.mx",
+ "google.com.my",
+ "google.com.na",
+ "google.com.nf",
+ "google.com.ng",
+ "google.com.ni",
+ "google.com.np",
+ "google.com.om",
+ "google.com.pa",
+ "google.com.pe",
+ "google.com.pg",
+ "google.com.ph",
+ "google.com.pk",
+ "google.com.pr",
+ "google.com.py",
+ "google.com.qa",
+ "google.com.sa",
+ "google.com.sb",
+ "google.com.sg",
+ "google.com.sl",
+ "google.com.sv",
+ "google.com.tj",
+ "google.com.tr",
+ "google.com.tw",
+ "google.com.ua",
+ "google.com.uy",
+ "google.com.vc",
+ "google.com.vn",
+ "google.cv",
+ "google.cz",
+ "google.de",
+ "google.dj",
+ "google.dk",
+ "google.dm",
+ "google.dz",
+ "google.ee",
+ "google.es",
+ "google.fi",
+ "google.fm",
+ "google.fr",
+ "google.ga",
+ "google.ge",
+ "google.gg",
+ "google.gl",
+ "google.gm",
+ "google.gp",
+ "google.gr",
+ "google.gy",
+ "google.hn",
+ "google.hr",
+ "google.ht",
+ "google.hu",
+ "google.ie",
+ "google.im",
+ "google.iq",
+ "google.is",
+ "google.it",
+ "google.je",
+ "google.jo",
+ "google.kg",
+ "google.ki",
+ "google.kz",
+ "google.la",
+ "google.li",
+ "google.lk",
+ "google.lt",
+ "google.lu",
+ "google.lv",
+ "google.md",
+ "google.me",
+ "google.mg",
+ "google.mk",
+ "google.ml",
+ "google.mn",
+ "google.ms",
+ "google.mu",
+ "google.mv",
+ "google.mw",
+ "google.ne",
+ "google.nl",
+ "google.no",
+ "google.nr",
+ "google.nu",
+ "google.pl",
+ "google.pn",
+ "google.ps",
+ "google.pt",
+ "google.ro",
+ "google.rs",
+ "google.ru",
+ "google.rw",
+ "google.sc",
+ "google.se",
+ "google.sh",
+ "google.si",
+ "google.sk",
+ "google.sm",
+ "google.sn",
+ "google.so",
+ "google.st",
+ "google.td",
+ "google.tg",
+ "google.tk",
+ "google.tl",
+ "google.tm",
+ "google.tn",
+ "google.to",
+ "google.tt",
+ "google.vg",
+ "google.vu",
+ "google.ws",
+ "googleapis.com",
+ "googleartproject.com",
+ "googleusercontent.com",
+ "groups.google.com",
+ "gstatic.com",
+ "health.google.com",
+ "images.google.com",
+ "investor.google.com",
+ "knol.google.com",
+ "maps.google.com",
+ "music.google.com",
+ "news.google.com",
+ "panoramio.com",
+ "picasa.google.com",
+ "picasaweb.google.com",
+ "play.google.com",
+ "postini.com",
+ "recaptcha.net",
+ "script.google.com",
+ "shopping.google.com",
+ "sites.google.com",
+ "sketchup.google.com",
+ "support.google.com",
+ "talk.google.com",
+ "talkgadget.google.com",
+ "toolbar.google.com",
+ "translate.google.com",
+ "trends.google.com",
+ "video.google.com",
+ "videos.google.com",
+ "wallet.google.com",
+ "youtube.com"
+ ]
+ }
+ },
+ {
+ "Gravity": {
+ "http://www.gravity.com/": [
+ "gravity.com",
+ "grvcdn.com"
+ ]
+ }
+ },
+ {
+ "Heyzap": {
+ "http://www.heyzap.com/": [
+ "heyzap.com"
+ ]
+ }
+ },
+ {
+ "HubSpot": {
+ "http://www.hubspot.com/": [
+ "hubspot.com"
+ ]
+ }
+ },
+ {
+ "IBM": {
+ "http://www.ibm.com/": [
+ "xtify.com"
+ ]
+ }
+ },
+ {
+ "iovation": {
+ "http://www.iovation.com/": [
+ "iesnare.com",
+ "iovation.com"
+ ]
+ }
+ },
+ {
+ "Kaltura": {
+ "http://corp.kaltura.com/": [
+ "kaltura.com"
+ ]
+ }
+ },
+ {
+ "kikin": {
+ "http://www.kikin.com/": [
+ "kikin.com"
+ ]
+ }
+ },
+ {
+ "Limelight Networks": {
+ "http://www.limelight.com/": [
+ "clickability.com",
+ "limelight.com",
+ "llnwd.net"
+ ]
+ }
+ },
+ {
+ "LivePerson": {
+ "http://www.liveperson.net/": [
+ "liveperson.net"
+ ]
+ }
+ },
+ {
+ "LiveRail": {
+ "http://liverail.com/": [
+ "liverail.com"
+ ]
+ }
+ },
+ {
+ "LongTail Video": {
+ "http://www.longtailvideo.com/": [
+ "longtailvideo.com",
+ "ltassrv.com"
+ ]
+ }
+ },
+ {
+ "Markit": {
+ "http://www.markit.com/": [
+ "markit.com",
+ "wsod.com"
+ ]
+ }
+ },
+ {
+ "MashLogic": {
+ "http://www.mashlogic.com/": [
+ "mashlogic.com"
+ ]
+ }
+ },
+ {
+ "McAfee": {
+ "http://www.mcafee.com/": [
+ "mcafee.com",
+ "scanalert.com"
+ ]
+ }
+ },
+ {
+ "Microsoft": {
+ "http://www.microsoft.com/": [
+ "bing.com",
+ "gamesforwindows.com",
+ "getgamesmart.com",
+ "healthvault.com",
+ "ieaddons.com",
+ "iegallery.com",
+ "live.com",
+ "microsoft.com",
+ "microsoftalumni.com",
+ "microsoftalumni.org",
+ "microsoftstore.com",
+ "msn.com",
+ "msndirect.com",
+ "office.com",
+ "officelive.com",
+ "outlook.com",
+ "s-msn.com",
+ "skype.com",
+ "windowsphone.com",
+ "worldwidetelescope.org",
+ "xbox.com",
+ "zune.com",
+ "zune.net"
+ ]
+ }
+ },
+ {
+ "NDN": {
+ "http://www.newsinc.com/": [
+ "newsinc.com"
+ ]
+ }
+ },
+ {
+ "Oberon Media": {
+ "http://www.oberon-media.com/": [
+ "blaze.com",
+ "oberon-media.com"
+ ]
+ }
+ },
+ {
+ "Ooyala": {
+ "http://www.ooyala.com/": [
+ "oo4.com",
+ "ooyala.com"
+ ]
+ }
+ },
+ {
+ "Oracle": {
+ "http://www.oracle.com/": [
+ "atgsvcs.com",
+ "instantservice.com",
+ "istrack.com",
+ "oracle.com"
+ ]
+ }
+ },
+ {
+ "Peerius": {
+ "http://www.peerius.com/": [
+ "peerius.com"
+ ]
+ }
+ },
+ {
+ "Pinterest": {
+ "http://pinterest.com/": [
+ "pinimg.com",
+ "pinterest.com"
+ ]
+ }
+ },
+ {
+ "PunchTab": {
+ "http://www.punchtab.com/": [
+ "punchtab.com"
+ ]
+ }
+ },
+ {
+ "RIM": {
+ "http://www.rim.com/": [
+ "rim.com",
+ "scoreloop.com"
+ ]
+ }
+ },
+ {
+ "Salesforce.com": {
+ "http://www.salesforce.com/": [
+ "salesforceliveagent.com"
+ ]
+ }
+ },
+ {
+ "SAY": {
+ "http://saymedia.com/": [
+ "saymedia.com",
+ "typepad.com",
+ "videoegg.com"
+ ]
+ }
+ },
+ {
+ "ScribeFire": {
+ "http://www.scribefire.com/": [
+ "scribefire.com"
+ ]
+ }
+ },
+ {
+ "Six Apart": {
+ "http://www.sixapart.com/": [
+ "sixapart.com"
+ ]
+ }
+ },
+ {
+ "Skribit": {
+ "http://skribit.com/": [
+ "skribit.com"
+ ]
+ }
+ },
+ {
+ "SnapEngage": {
+ "http://www.snapengage.com/": [
+ "snapengage.com"
+ ]
+ }
+ },
+ {
+ "Spring Metrics": {
+ "http://www.springmetrics.com/": [
+ "springmetrics.com"
+ ]
+ }
+ },
+ {
+ "Synacor": {
+ "http://www.synacor.com/": [
+ "synacor.com"
+ ]
+ }
+ },
+ {
+ "ThingLink": {
+ "http://www.thinglink.com/": [
+ "thinglink.com"
+ ]
+ }
+ },
+ {
+ "Thismoment": {
+ "http://www.thismoment.com/": [
+ "thismoment.com"
+ ]
+ }
+ },
+ {
+ "Thummit": {
+ "http://www.thummit.com/": [
+ "thummit.com"
+ ]
+ }
+ },
+ {
+ "Topsy": {
+ "http://topsy.com/": [
+ "topsy.com"
+ ]
+ }
+ },
+ {
+ "TraceMyIP.org": {
+ "http://www.tracemyip.org/": [
+ "tracemyip.org"
+ ]
+ }
+ },
+ {
+ "Trackset": {
+ "http://www.trackset.com/": [
+ "trackset.com"
+ ]
+ }
+ },
+ {
+ "Trovus": {
+ "http://www.trovus.co.uk/": [
+ "trovus.co.uk"
+ ]
+ }
+ },
+ {
+ "Trumba": {
+ "http://www.trumba.com/": [
+ "trumba.com"
+ ]
+ }
+ },
+ {
+ "TRUSTe": {
+ "http://www.truste.com/": [
+ "truste.com"
+ ]
+ }
+ },
+ {
+ "TurnTo": {
+ "http://www.turntonetworks.com/": [
+ "turnto.com",
+ "turntonetworks.com"
+ ]
+ }
+ },
+ {
+ "Tweetboard": {
+ "http://tweetboard.com/": [
+ "tweetboard.com"
+ ]
+ }
+ },
+ {
+ "Twitter Counter": {
+ "http://twittercounter.com/": [
+ "twittercounter.com"
+ ]
+ }
+ },
+ {
+ "UberMedia": {
+ "http://ubermedia.com/": [
+ "tweetup.com",
+ "ubermedia.com"
+ ]
+ }
+ },
+ {
+ "UberTags": {
+ "http://ubertags.com/": [
+ "ubertags.com"
+ ]
+ }
+ },
+ {
+ "Unbounce": {
+ "http://unbounce.com/": [
+ "unbounce.com"
+ ]
+ }
+ },
+ {
+ "Uptrends": {
+ "http://www.uptrends.com/": [
+ "uptrends.com"
+ ]
+ }
+ },
+ {
+ "Usability Sciences": {
+ "http://www.usabilitysciences.com/": [
+ "usabilitysciences.com",
+ "webiqonline.com"
+ ]
+ }
+ },
+ {
+ "UserVoice": {
+ "http://www.uservoice.com/": [
+ "uservoice.com"
+ ]
+ }
+ },
+ {
+ "Vertical Acuity": {
+ "http://www.verticalacuity.com/": [
+ "verticalacuity.com"
+ ]
+ }
+ },
+ {
+ "VG WORT": {
+ "http://www.vgwort.de/": [
+ "vgwort.de"
+ ]
+ }
+ },
+ {
+ "Videology": {
+ "http://www.videologygroup.com/": [
+ "tidaltv.com",
+ "videologygroup.com"
+ ]
+ }
+ },
+ {
+ "Viewbix": {
+ "http://www.viewbix.com/": [
+ "qoof.com",
+ "viewbix.com"
+ ]
+ }
+ },
+ {
+ "Vimeo": {
+ "http://vimeo.com/": [
+ "vimeo.com",
+ "vimeocdn.com"
+ ]
+ }
+ },
+ {
+ "VINDICO": {
+ "http://vindicogroup.com/": [
+ "vindicogroup.com",
+ "vindicosuite.com"
+ ]
+ }
+ },
+ {
+ "Voice2Page": {
+ "http://www.voice2page.com/": [
+ "voice2page.com"
+ ]
+ }
+ },
+ {
+ "WebsiteAlive": {
+ "http://www.websitealive.com/": [
+ "websitealive.com",
+ "websitealive0.com",
+ "websitealive1.com",
+ "websitealive2.com",
+ "websitealive3.com",
+ "websitealive4.com",
+ "websitealive5.com",
+ "websitealive6.com",
+ "websitealive7.com",
+ "websitealive8.com",
+ "websitealive9.com"
+ ]
+ }
+ },
+ {
+ "Yahoo!": {
+ "http://www.yahoo.com/": [
+ "answers.yahoo.com",
+ "apps.yahoo.com",
+ "autos.yahoo.com",
+ "biz.yahoo.com",
+ "developer.yahoo.com",
+ "everything.yahoo.com",
+ "finance.yahoo.com",
+ "flickr.com",
+ "games.yahoo.com",
+ "groups.yahoo.com",
+ "help.yahoo.com",
+ "hotjobs.yahoo.com",
+ "info.yahoo.com",
+ "local.yahoo.com",
+ "luminate.com",
+ "messages.yahoo.com",
+ "movies.yahoo.com",
+ "msg.yahoo.com",
+ "news.yahoo.com",
+ "omg.yahoo.com",
+ "pipes.yahoo.com",
+ "pixazza.com",
+ "realestate.yahoo.com",
+ "search.yahoo.com",
+ "shine.yahoo.com",
+ "smallbusiness.yahoo.com",
+ "sports.yahoo.com",
+ "staticflickr.com",
+ "suggestions.yahoo.com",
+ "travel.yahoo.com",
+ "tumblr.com",
+ "upcoming.yahoo.com",
+ "webhosting.yahoo.com",
+ "widgets.yahoo.com",
+ "www.yahoo.com",
+ "yahooapis.com",
+ "yahoofs.com",
+ "yimg.com",
+ "ypolicyblog.com",
+ "yuilibrary.com",
+ "zenfs.com"
+ ]
+ }
+ },
+ {
+ "Yandex": {
+ "http://www.yandex.com/": [
+ "kinopoisk.ru",
+ "yandex.by",
+ "yandex.com",
+ "yandex.com.tr",
+ "yandex.ru",
+ "yandex.st",
+ "yandex.ua"
+ ]
+ }
+ },
+ {
+ "Zendesk": {
+ "http://www.zendesk.com/": [
+ "zendesk.com"
+ ]
+ }
+ },
+ {
+ "Zopim": {
+ "https://www.zopim.com/": [
+ "zopim.com"
+ ]
+ }
+ }
+ ],
+ "Analytics": [
+ {
+ "63 Squares": {
+ "http://63squares.com/": [
+ "63squares.com",
+ "i-stats.com"
+ ]
+ }
+ },
+ {
+ "Acxiom": {
+ "http://www.acxiom.com/": [
+ "acxiom.com",
+ "acxiomapac.com",
+ "mm7.net",
+ "pippio.com"
+ ]
+ }
+ },
+ {
+ "AddFreeStats": {
+ "http://www.addfreestats.com/": [
+ "3dstats.com",
+ "addfreestats.com"
+ ]
+ }
+ },
+ {
+ "Adloox": {
+ "http://www.adloox.com/": [
+ "adloox.com",
+ "adlooxtracking.com"
+ ]
+ }
+ },
+ {
+ "Adventori": {
+ "https://adventori.com": [
+ "adventori.com"
+ ]
+ }
+ },
+ {
+ "AIData": {
+ "http://www.aidata.me/": [
+ "advombat.ru",
+ "aidata.me"
+ ]
+ }
+ },
+ {
+ "AivaLabs": {
+ "https://aivalabs.com": [
+ "aivalabs.com"
+ ]
+ }
+ },
+ {
+ "Akamai": {
+ "http://www.akamai.com/": [
+ "go-mpulse.net"
+ ]
+ }
+ },
+ {
+ "Amadesa": {
+ "http://www.amadesa.com/": [
+ "amadesa.com"
+ ]
+ }
+ },
+ {
+ "Amazing Counters": {
+ "http://amazingcounters.com/": [
+ "amazingcounters.com"
+ ]
+ }
+ },
+ {
+ "Amazon.com": {
+ "http://www.amazon.com/": [
+ "alexametrics.com"
+ ]
+ }
+ },
+ {
+ "Amplitude": {
+ "https://amplitude.com/": [
+ "amplitude.com"
+ ]
+ }
+ },
+ {
+ "anormal-media.de": {
+ "http://anormal-media.de/": [
+ "anormal-media.de",
+ "anormal-tracker.de"
+ ]
+ }
+ },
+ {
+ "AT Internet": {
+ "http://www.atinternet.com/": [
+ "at-o.net",
+ "atinternet.com",
+ "xiti.com"
+ ]
+ }
+ },
+ {
+ "Attracta": {
+ "https://www.attracta.com/": [
+ "attracta.com"
+ ]
+ }
+ },
+ {
+ "Automattic": {
+ "http://automattic.com/": [
+ "polldaddy.com"
+ ]
+ }
+ },
+ {
+ "AvantLink": {
+ "http://www.avantlink.com/": [
+ "avmws.com"
+ ]
+ }
+ },
+ {
+ "Awio": {
+ "http://www.awio.com/": [
+ "awio.com",
+ "w3counter.com",
+ "w3roi.com"
+ ]
+ }
+ },
+ {
+ "Belstat": {
+ "http://www.belstat.com/": [
+ "belstat.be",
+ "belstat.com",
+ "belstat.de",
+ "belstat.fr",
+ "belstat.nl"
+ ]
+ }
+ },
+ {
+ "BetssonPalantir": {
+ "https://betssonpalantir.com/": [
+ "betssonpalantir.com"
+ ]
+ }
+ },
+ {
+ "BlogCounter.com": {
+ "http://www.blogcounter.de/": [
+ "blogcounter.de"
+ ]
+ }
+ },
+ {
+ "BloomReach": {
+ "http://www.bloomreach.com/": [
+ "p.brsrvr.com"
+ ]
+ }
+ },
+ {
+ "BlueCava": {
+ "http://www.bluecava.com/": [
+ "bluecava.com"
+ ]
+ }
+ },
+ {
+ "Bluemetrix": {
+ "http://www.bluemetrix.com/": [
+ "bluemetrix.com",
+ "bmmetrix.com"
+ ]
+ }
+ },
+ {
+ "Bombora": {
+ "https://bombora.com/": [
+ "ml314.com"
+ ]
+ }
+ },
+ {
+ "Branch": {
+ "https://branch.io/": [
+ "branch.io"
+ ]
+ }
+ },
+ {
+ "Branica": {
+ "http://www.branica.com/": [
+ "branica.com"
+ ]
+ }
+ },
+ {
+ "BrightEdge": {
+ "http://www.brightedge.com/": [
+ "b0e8.com",
+ "brightedge.com"
+ ]
+ }
+ },
+ {
+ "Bubblestat": {
+ "http://www.bubblestat.com/": [
+ "bubblestat.com"
+ ]
+ }
+ },
+ {
+ "Cardlytics": {
+ "http://www.cardlytics.com/": [
+ "cardlytics.com"
+ ]
+ }
+ },
+ {
+ "Chartbeat": {
+ "http://chartbeat.com/": [
+ "chartbeat.com",
+ "chartbeat.net"
+ ]
+ }
+ },
+ {
+ "Clickdensity": {
+ "http://www.clickdensity.com/": [
+ "clickdensity.com"
+ ]
+ }
+ },
+ {
+ "ClickGuard": {
+ "https://www.clickguard.com/": [
+ "clickguard.com"
+ ]
+ }
+ },
+ {
+ "ClickTale": {
+ "http://www.clicktale.com/": [
+ "clicktale.com",
+ "clicktale.net",
+ "pantherssl.com"
+ ],
+ "session-replay": "true"
+ }
+ },
+ {
+ "ClixMetrix": {
+ "http://www.clixmetrix.com/": [
+ "clixmetrix.com"
+ ]
+ }
+ },
+ {
+ "Clixpy": {
+ "http://clixpy.com/": [
+ "clixpy.com"
+ ]
+ }
+ },
+ {
+ "ClustrMaps": {
+ "http://www.clustrmaps.com/": [
+ "clustrmaps.com"
+ ]
+ }
+ },
+ {
+ "CNZZ": {
+ "http://www.cnzz.com/": [
+ "cnzz.com"
+ ]
+ }
+ },
+ {
+ "Compuware": {
+ "http://www.compuware.com/": [
+ "axf8.net",
+ "compuware.com",
+ "gomez.com"
+ ]
+ }
+ },
+ {
+ "comScore": {
+ "http://www.comscore.com/": [
+ "certifica.com",
+ "comscore.com",
+ "mdotlabs.com",
+ "scorecardresearch.com",
+ "sitestat.com",
+ "voicefive.com"
+ ]
+ }
+ },
+ {
+ "Connexity": {
+ "http://www.connexity.com/": [
+ "connexity.com",
+ "connexity.net"
+ ]
+ }
+ },
+ {
+ "Convert Insights": {
+ "http://www.convert.com/": [
+ "convert.com",
+ "reedge.com"
+ ]
+ }
+ },
+ {
+ "Convertro": {
+ "http://www.convertro.com/": [
+ "convertro.com"
+ ]
+ }
+ },
+ {
+ "Crazy Egg": {
+ "http://www.crazyegg.com/": [
+ "cetrk.com",
+ "crazyegg.com"
+ ]
+ }
+ },
+ {
+ "Crowd Science": {
+ "http://crowdscience.com/": [
+ "crowdscience.com"
+ ]
+ }
+ },
+ {
+ "Cya2": {
+ "http://cya2.net/": [
+ "cya2.net"
+ ]
+ }
+ },
+ {
+ "Dataium": {
+ "http://www.dataium.com/": [
+ "collserve.com",
+ "dataium.com"
+ ]
+ }
+ },
+ {
+ "Deep Intent": {
+ "https://www.deepintent.com/": [
+ "deepintent.com"
+ ]
+ }
+ },
+ {
+ "Demandbase": {
+ "http://www.demandbase.com/": [
+ "company-target.com",
+ "demandbase.com"
+ ]
+ }
+ },
+ {
+ "DirectCORP": {
+ "http://www.directcorp.de/": [
+ "ipcounter.de"
+ ]
+ }
+ },
+ {
+ "DistilNetworks": {
+ "https://www.distilnetworks.com/": [
+ "distiltag.com"
+ ]
+ }
+ },
+ {
+ "DoubleVerify": {
+ "http://www.doubleverify.com/": [
+ "doubleverify.com"
+ ]
+ }
+ },
+ {
+ "dwstat.com": {
+ "http://www.dwstat.cn/": [
+ "dwstat.cn"
+ ]
+ }
+ },
+ {
+ "ECSAnalytics": {
+ "https://www.theecsinc.com/": [
+ "ecsanalytics.com"
+ ]
+ }
+ },
+ {
+ "EFF": {
+ "https://www.eff.org/": [
+ "do-not-tracker.org",
+ "eviltracker.net",
+ "trackersimulator.org"
+ ]
+ }
+ },
+ {
+ "eProof.com": {
+ "http://www.eproof.com/": [
+ "eproof.com"
+ ]
+ }
+ },
+ {
+ "etracker": {
+ "http://www.etracker.com/": [
+ "etracker.com",
+ "etracker.de",
+ "sedotracker.com",
+ "sedotracker.de"
+ ]
+ }
+ },
+ {
+ "Eulerian Technologies": {
+ "http://www.eulerian.com/": [
+ "eulerian.com",
+ "eulerian.net"
+ ]
+ }
+ },
+ {
+ "eXTReMe digital": {
+ "http://extremetracking.com/": [
+ "extreme-dm.com",
+ "extremetracking.com"
+ ]
+ }
+ },
+ {
+ "Eyeota": {
+ "http://eyeota.net/": [
+ "eyeota.net"
+ ]
+ }
+ },
+ {
+ "Feedjit": {
+ "http://feedjit.com/": [
+ "feedjit.com"
+ ]
+ }
+ },
+ {
+ "Flashtalking": {
+ "http://www.flashtalking.com/": [
+ "encoremetrics.com",
+ "sitecompass.com"
+ ]
+ }
+ },
+ {
+ "Footprint": {
+ "http://www.footprintlive.com/": [
+ "footprintlive.com"
+ ]
+ }
+ },
+ {
+ "Free Online Users": {
+ "http://www.freeonlineusers.com/": [
+ "freeonlineusers.com"
+ ]
+ }
+ },
+ {
+ "Free-PageRank.com": {
+ "http://www.free-pagerank.com/": [
+ "free-pagerank.com"
+ ]
+ }
+ },
+ {
+ "Friends2Follow": {
+ "https://friends2follow.com/": [
+ "antifraudjs.friends2follow.com"
+ ]
+ }
+ },
+ {
+ "Fullstory": {
+ "https://www.fullstory.com/": [
+ "fullstory.com"
+ ],
+ "session-replay": "true"
+ }
+ },
+ {
+ "GetSiteControl": {
+ "https://getsitecontrol.com/": [
+ "getsitecontrol.com"
+ ]
+ }
+ },
+ {
+ "GfK Group": {
+ "http://www.gfk.com/": [
+ "daphnecm.com",
+ "gfk.com",
+ "gfkdaphne.com"
+ ]
+ }
+ },
+ {
+ "GitHub": {
+ "https://github.com/": [
+ "gaug.es"
+ ]
+ }
+ },
+ {
+ "Go Daddy": {
+ "http://www.godaddy.com/": [
+ "godaddy.com",
+ "trafficfacts.com"
+ ]
+ }
+ },
+ {
+ "Google": {
+ "http://www.google.com/": [
+ "google-analytics.com",
+ "postrank.com"
+ ]
+ }
+ },
+ {
+ "GoSquared": {
+ "https://www.gosquared.com/": [
+ "gosquared.com"
+ ]
+ }
+ },
+ {
+ "GoStats": {
+ "http://gostats.com/": [
+ "gostats.com"
+ ]
+ }
+ },
+ {
+ "GrapheneMedia": {
+ "http://graphenemedia.in/": [
+ "graphenedigitalanalytics.in"
+ ]
+ }
+ },
+ {
+ "GTop": {
+ "http://www.gtop.ro/": [
+ "gtop.ro",
+ "gtopstats.com"
+ ]
+ }
+ },
+ {
+ "Hearst": {
+ "http://www.hearst.com/": [
+ "raasnet.com",
+ "redaril.com"
+ ]
+ }
+ },
+ {
+ "Histats": {
+ "http://www.histats.com/": [
+ "histats.com"
+ ]
+ }
+ },
+ {
+ "HitsLink": {
+ "http://www.hitslink.com/": [
+ "hitslink.com"
+ ]
+ }
+ },
+ {
+ "Hit Sniffer": {
+ "http://www.hitsniffer.com/": [
+ "hitsniffer.com"
+ ]
+ }
+ },
+ {
+ "Hotjar": {
+ "https://www.hotjar.com": [
+ "hotjar.com"
+ ]
+ }
+ },
+ {
+ "HubSpot": {
+ "http://www.hubspot.com/": [
+ "hs-analytics.net"
+ ]
+ }
+ },
+ {
+ "IBM": {
+ "http://www.ibm.com/": [
+ "cmcore.com",
+ "coremetrics.com",
+ "ibm.com"
+ ]
+ }
+ },
+ {
+ "InboundWriter": {
+ "http://www.inboundwriter.com/": [
+ "enquisite.com",
+ "inboundwriter.com"
+ ]
+ }
+ },
+ {
+ "Infernotions": {
+ "https://infernotions.com/": [
+ "infernotions.com"
+ ]
+ }
+ },
+ {
+ "INFOnline": {
+ "https://www.infonline.de/": [
+ "infonline.de",
+ "ioam.de",
+ "ivwbox.de"
+ ]
+ }
+ },
+ {
+ "InfoStars": {
+ "http://infostars.ru/": [
+ "hotlog.ru",
+ "infostars.ru"
+ ]
+ }
+ },
+ {
+ "Inspectlet": {
+ "http://www.inspectlet.com/": [
+ "inspectlet.com"
+ ]
+ }
+ },
+ {
+ "IntelligenceFocus": {
+ "http://www.intelligencefocus.com/": [
+ "domodomain.com",
+ "intelligencefocus.com"
+ ]
+ }
+ },
+ {
+ "iPerceptions": {
+ "http://www.iperceptions.com/": [
+ "iperceptions.com"
+ ]
+ }
+ },
+ {
+ "IslayTech": {
+ "http://islay.tech": [
+ "islay.tech"
+ ]
+ }
+ },
+ {
+ "ItIsATracker": {
+ "https://itisatracker.com/": [
+ "itisatracker.com"
+ ],
+ "dnt": "eff"
+ }
+ },
+ {
+ "KeyMetric": {
+ "http://www.keymetric.net/": [
+ "keymetric.net"
+ ]
+ }
+ },
+ {
+ "KISSmetrics": {
+ "http://kissmetrics.com/": [
+ "kissmetrics.com"
+ ]
+ }
+ },
+ {
+ "Kitcode": {
+ "http://src.kitcode.net/": [
+ "src.kitcode.net"
+ ]
+ }
+ },
+ {
+ "LeadForensics": {
+ "https://www.leadforensics.com": [
+ "leadforensics.com"
+ ]
+ }
+ },
+ {
+ "LineZing": {
+ "http://www.linezing.com/": [
+ "linezing.com"
+ ]
+ }
+ },
+ {
+ "LivePerson": {
+ "http://www.liveperson.net/": [
+ "liveperson.com",
+ "nuconomy.com"
+ ]
+ }
+ },
+ {
+ "Logdy": {
+ "http://logdy.com/": [
+ "logdy.com"
+ ]
+ }
+ },
+ {
+ "Lotame": {
+ "http://www.lotame.com/": [
+ "crwdcntrl.net",
+ "lotame.com"
+ ]
+ }
+ },
+ {
+ "LuckyOrange": {
+ "https://www.luckyorange.com": [
+ "luckyorange.com",
+ "luckyorange.net"
+ ],
+ "session-replay": "true"
+ }
+ },
+ {
+ "Lynchpin": {
+ "http://www.lynchpin.com/": [
+ "lynchpin.com",
+ "lypn.com"
+ ]
+ }
+ },
+ {
+ "Lyris": {
+ "http://www.lyris.com/": [
+ "clicktracks.com",
+ "lyris.com"
+ ]
+ }
+ },
+ {
+ "Lytiks": {
+ "http://www.lytiks.com/": [
+ "lytiks.com"
+ ]
+ }
+ },
+ {
+ "MarkMonitor": {
+ "https://www.markmonitor.com": [
+ "9c9media.ca",
+ "markmonitor.com"
+ ]
+ }
+ },
+ {
+ "Marktest": {
+ "http://www.marktest.com/": [
+ "marktest.com",
+ "marktest.pt"
+ ]
+ }
+ },
+ {
+ "MaxMind": {
+ "https://www.maxmind.com/en/home": [
+ "maxmind.com",
+ "mmapiws.com"
+ ]
+ }
+ },
+ {
+ "Médiamétrie-eStat": {
+ "http://www.mediametrie-estat.com/": [
+ "estat.com",
+ "mediametrie-estat.com"
+ ]
+ }
+ },
+ {
+ "Merkle": {
+ "https://www.merkleinc.com/": [
+ "merkleinc.com",
+ "rkdms.com"
+ ]
+ }
+ },
+ {
+ "Mixpanel": {
+ "https://mixpanel.com/": [
+ "mixpanel.com",
+ "mxpnl.com"
+ ]
+ }
+ },
+ {
+ "Mongoose Metrics": {
+ "http://www.mongoosemetrics.com/": [
+ "mongoosemetrics.com"
+ ]
+ }
+ },
+ {
+ "Monitus": {
+ "http://www.monitus.net/": [
+ "monitus.net"
+ ]
+ }
+ },
+ {
+ "motigo": {
+ "http://motigo.com/": [
+ "motigo.com",
+ "nedstatbasic.net"
+ ]
+ }
+ },
+ {
+ "Mouseflow": {
+ "http://mouseflow.com/": [
+ "mouseflow.com"
+ ]
+ }
+ },
+ {
+ "MyPagerank.Net": {
+ "http://www.mypagerank.net/": [
+ "mypagerank.net"
+ ]
+ }
+ },
+ {
+ "Mystighty": {
+ "http://mystighty.info/": [
+ "mystighty.info",
+ "sweeterge.info"
+ ]
+ }
+ },
+ {
+ "Narrative": {
+ "http://narrative.io/2/": [
+ "narrative.io"
+ ]
+ }
+ },
+ {
+ "Net Applications": {
+ "http://www.netapplications.com/": [
+ "hitsprocessor.com",
+ "netapplications.com"
+ ]
+ }
+ },
+ {
+ "New Relic": {
+ "http://newrelic.com/": [
+ "newrelic.com",
+ "nr-data.net"
+ ]
+ }
+ },
+ {
+ "NewsRight": {
+ "http://www.newsright.com/": [
+ "apnewsregistry.com"
+ ]
+ }
+ },
+ {
+ "NextSTAT": {
+ "http://www.nextstat.com/": [
+ "nextstat.com"
+ ]
+ }
+ },
+ {
+ "Nielsen": {
+ "http://www.nielsen.com/": [
+ "glanceguide.com",
+ "nielsen.com"
+ ]
+ }
+ },
+ {
+ "NuDataSecurity": {
+ "https://nudatasecurity.com/": [
+ "nudatasecurity.com"
+ ]
+ }
+ },
+ {
+ "nurago": {
+ "http://www.nurago.com/": [
+ "nurago.com",
+ "nurago.de",
+ "sensic.net"
+ ]
+ }
+ },
+ {
+ "Observer": {
+ "http://observerapp.com/": [
+ "observerapp.com"
+ ]
+ }
+ },
+ {
+ "OnAudience": {
+ "http://www.onaudience.com/": [
+ "behavioralengine.com",
+ "onaudience.com"
+ ]
+ }
+ },
+ {
+ "OneStat": {
+ "http://www.onestat.com/": [
+ "onestat.com"
+ ]
+ }
+ },
+ {
+ "Openstat": {
+ "https://www.openstat.ru/": [
+ "openstat.ru",
+ "spylog.com"
+ ]
+ }
+ },
+ {
+ "Opentracker": {
+ "http://www.opentracker.net/": [
+ "opentracker.net"
+ ]
+ }
+ },
+ {
+ "Opolen": {
+ "https://opolen.com.br": [
+ "opolen.com.br"
+ ]
+ }
+ },
+ {
+ "Optimizely": {
+ "https://www.optimizely.com/": [
+ "optimizely.com"
+ ]
+ }
+ },
+ {
+ "Oracle": {
+ "http://www.oracle.com/": [
+ "eloqua.com",
+ "maxymiser.com"
+ ]
+ }
+ },
+ {
+ "ÖWA": {
+ "http://www.oewa.at/": [
+ "oewa.at",
+ "oewabox.at"
+ ]
+ }
+ },
+ {
+ "Parse.ly": {
+ "http://parsely.com/": [
+ "parsely.com"
+ ]
+ }
+ },
+ {
+ "PersianStat.com": {
+ "http://www.persianstat.com/": [
+ "persianstat.com"
+ ]
+ }
+ },
+ {
+ "Phonalytics": {
+ "http://www.phonalytics.com/": [
+ "phonalytics.com"
+ ]
+ }
+ },
+ {
+ "phpMyVisites": {
+ "http://www.phpmyvisites.us/": [
+ "phpmyvisites.us"
+ ]
+ }
+ },
+ {
+ "Piwik": {
+ "http://piwik.org/": [
+ "piwik.org"
+ ]
+ }
+ },
+ {
+ "PixAnalytics": {
+ "https://pixanalytics.com/": [
+ "pixanalytics.com"
+ ]
+ }
+ },
+ {
+ "Poool": {
+ "http://poool.fr/": [
+ "poool.fr"
+ ]
+ }
+ },
+ {
+ "Pronunciator": {
+ "http://www.pronunciator.com/": [
+ "pronunciator.com",
+ "visitorville.com"
+ ]
+ }
+ },
+ {
+ "Qualaroo": {
+ "http://qualaroo.com/": [
+ "kissinsights.com",
+ "qualaroo.com"
+ ]
+ }
+ },
+ {
+ "QuinStreet": {
+ "http://quinstreet.com/": [
+ "thecounter.com"
+ ]
+ }
+ },
+ {
+ "Quintelligence": {
+ "http://www.quintelligence.com/": [
+ "quintelligence.com"
+ ]
+ }
+ },
+ {
+ "RadarURL": {
+ "http://radarurl.com/": [
+ "radarurl.com"
+ ]
+ }
+ },
+ {
+ "Research Now": {
+ "http://www.researchnow.com/": [
+ "researchnow.com",
+ "valuedopinions.co.uk"
+ ]
+ }
+ },
+ {
+ "Retail Automata": {
+ "https://retailautomata.com": [
+ "retailautomata.com"
+ ]
+ }
+ },
+ {
+ "Revtracks": {
+ "http://revtrax.com/": [
+ "revtrax.com"
+ ]
+ }
+ },
+ {
+ "Ringier": {
+ "http://ringier.cz/": [
+ "ringier.cz"
+ ]
+ }
+ },
+ {
+ "Rollick": {
+ "https://gorollick.com": [
+ "rollick.io"
+ ]
+ }
+ },
+ {
+ "Roxr": {
+ "http://roxr.net/": [
+ "getclicky.com",
+ "roxr.net",
+ "staticstuff.net"
+ ]
+ }
+ },
+ {
+ "Safecount": {
+ "http://www.safecount.net/": [
+ "dl-rms.com",
+ "dlqm.net",
+ "questionmarket.com",
+ "safecount.net"
+ ]
+ }
+ },
+ {
+ "SageMetrics": {
+ "http://www.sagemetrics.com/": [
+ "sageanalyst.net",
+ "sagemetrics.com"
+ ]
+ }
+ },
+ {
+ "Salesintelligence": {
+ "https://salesintelligence.pl/": [
+ "plugin.management"
+ ]
+ }
+ },
+ {
+ "SeeVolution": {
+ "https://www.seevolution.com/": [
+ "seevolution.com",
+ "svlu.net"
+ ]
+ }
+ },
+ {
+ "Segment.io": {
+ "https://segment.io/": [
+ "segment.io"
+ ]
+ }
+ },
+ {
+ "SendPulse": {
+ "https://sendpulse.com/": [
+ "sendpulse.com"
+ ]
+ }
+ },
+ {
+ "SessionCam": {
+ "https://sessioncam.com/": [
+ "sessioncam.com"
+ ],
+ "session-replay": "true"
+ }
+ },
+ {
+ "ShinyStat": {
+ "http://www.shinystat.com/": [
+ "shinystat.com"
+ ]
+ }
+ },
+ {
+ "Smartlook": {
+ "https://www.smartlook.com/": [
+ "smartlook.com"
+ ],
+ "session-replay": "true"
+ }
+ },
+ {
+ "Snoobi": {
+ "http://www.snoobi.com/": [
+ "snoobi.com"
+ ]
+ }
+ },
+ {
+ "Sourcepoint": {
+ "https://www.sourcepoint.com/": [
+ "summerhamster.com"
+ ]
+ }
+ },
+ {
+ "Sputnik.ru": {
+ "http://sputnik.ru": [
+ "sputnik.ru"
+ ]
+ }
+ },
+ {
+ "StackTrack": {
+ "http://stat-track.com": [
+ "stat-track.com"
+ ]
+ }
+ },
+ {
+ "stat4u": {
+ "http://stat.4u.pl/": [
+ "4u.pl"
+ ]
+ }
+ },
+ {
+ "StatCounter": {
+ "http://statcounter.com/": [
+ "statcounter.com"
+ ]
+ }
+ },
+ {
+ "Statisfy": {
+ "http://statisfy.net": [
+ "statisfy.net"
+ ]
+ }
+ },
+ {
+ "STATSIT": {
+ "http://www.statsit.com/": [
+ "statsit.com"
+ ]
+ }
+ },
+ {
+ "Storeland": {
+ "https://storeland.ru/": [
+ "storeland.ru"
+ ]
+ }
+ },
+ {
+ "Stratigent": {
+ "http://www.stratigent.com/": [
+ "stratigent.com"
+ ]
+ }
+ },
+ {
+ "Tealium": {
+ "https://tealium.com": [
+ "tealiumiq.com"
+ ]
+ }
+ },
+ {
+ "TechSolutions": {
+ "https://www.techsolutions.com.tw/": [
+ "techsolutions.com.tw"
+ ]
+ }
+ },
+ {
+ "TENSQUARE": {
+ "http://www.tensquare.com/": [
+ "tensquare.com"
+ ]
+ }
+ },
+ {
+ "The Heron Partnership": {
+ "http://www.heronpartners.com.au/": [
+ "heronpartners.com.au",
+ "marinsm.com"
+ ]
+ }
+ },
+ {
+ "TNS": {
+ "http://www.tnsglobal.com/": [
+ "sesamestats.com",
+ "statistik-gallup.net",
+ "tns-counter.ru",
+ "tns-cs.net",
+ "tnsglobal.com"
+ ]
+ }
+ },
+ {
+ "TrackingSoft": {
+ "http://trackingsoft.com/": [
+ "roia.biz",
+ "trackingsoft.com"
+ ]
+ }
+ },
+ {
+ "TrafficScore": {
+ "https://trafficscore.com/": [
+ "trafficscore.com"
+ ]
+ }
+ },
+ {
+ "Twitter": {
+ "https://twitter.com/": [
+ "crashlytics.com",
+ "tweetdeck.com"
+ ]
+ }
+ },
+ {
+ "Umbel": {
+ "https://www.umbel.com/": [
+ "umbel.com"
+ ]
+ }
+ },
+ {
+ "User Local": {
+ "http://nakanohito.jp/": [
+ "nakanohito.jp"
+ ]
+ }
+ },
+ {
+ "V12 Data": {
+ "https://www.v12data.com/": [
+ "v12data.com",
+ "v12group.com"
+ ]
+ }
+ },
+ {
+ "Vertster": {
+ "http://www.vertster.com/": [
+ "vertster.com"
+ ]
+ }
+ },
+ {
+ "VisiStat": {
+ "http://www.visistat.com/": [
+ "sa-as.com",
+ "visistat.com"
+ ]
+ }
+ },
+ {
+ "Visit Streamer": {
+ "http://www.visitstreamer.com/": [
+ "visitstreamer.com"
+ ]
+ }
+ },
+ {
+ "vistrac": {
+ "http://vistrac.com/": [
+ "vistrac.com"
+ ]
+ }
+ },
+ {
+ "ViziSense": {
+ "http://www.vizisense.com/": [
+ "vizisense.com",
+ "vizisense.net"
+ ]
+ }
+ },
+ {
+ "Webclicktracker": {
+ "http://www.webclicktracker.com/": [
+ "webclicktracker.com"
+ ]
+ }
+ },
+ {
+ "Web Stats": {
+ "http://www.onlinewebstats.com/": [
+ "onlinewebstats.com"
+ ]
+ }
+ },
+ {
+ "Web Tracking Services": {
+ "http://www.webtrackingservices.com/": [
+ "web-stat.com",
+ "webtrackingservices.com"
+ ]
+ }
+ },
+ {
+ "Web Traxs": {
+ "http://www.webtraxs.com/": [
+ "webtraxs.com"
+ ]
+ }
+ },
+ {
+ "Webtrekk": {
+ "http://www.webtrekk.com/": [
+ "webtrekk.com",
+ "webtrekk.net"
+ ]
+ }
+ },
+ {
+ "Webtrends": {
+ "http://webtrends.com/": [
+ "reinvigorate.net",
+ "webtrends.com",
+ "webtrendslive.com"
+ ]
+ }
+ },
+ {
+ "White Ops": {
+ "https://www.whiteops.com/": [
+ "adzmath.com",
+ "whiteops.com"
+ ]
+ }
+ },
+ {
+ "whos.amung.us": {
+ "http://whos.amung.us/": [
+ "amung.us"
+ ]
+ }
+ },
+ {
+ "Wingify": {
+ "http://wingify.com/": [
+ "visualwebsiteoptimizer.com",
+ "wingify.com"
+ ]
+ }
+ },
+ {
+ "Woopra": {
+ "http://www.woopra.com/": [
+ "woopra-ns.com",
+ "woopra.com"
+ ]
+ }
+ },
+ {
+ "WOW Analytics": {
+ "http://www.wowanalytics.co.uk/": [
+ "wowanalytics.co.uk"
+ ]
+ }
+ },
+ {
+ "WPP": {
+ "http://www.wpp.com/": [
+ "compete.com"
+ ]
+ }
+ },
+ {
+ "Wysistat": {
+ "http://www.wysistat.com/": [
+ "wysistat.com"
+ ]
+ }
+ },
+ {
+ "Yahoo!": {
+ "http://www.yahoo.com/": [
+ "analytics.yahoo.com"
+ ]
+ }
+ },
+ {
+ "YellowTracker": {
+ "http://www.yellowtracker.com/": [
+ "yellowtracker.com"
+ ]
+ }
+ },
+ {
+ "YSance": {
+ "https://www.ysance.com/data-services/fr/home/": [
+ "y-track.com"
+ ]
+ }
+ }
+ ],
+ "Fingerprinting": [
+ {
+ "Adabra": {
+ "https://www.adabra.com/": [
+ "adabra.com"
+ ]
+ }
+ },
+ {
+ "Adbot": {
+ "https://adbot.tw/": [
+ "adbot.tw"
+ ]
+ }
+ },
+ {
+ "AdGainerSolutions": {
+ "http://adgainersolutions.com/adgainer/": [
+ "adgainersolutions.com"
+ ]
+ }
+ },
+ {
+ "AdMaven": {
+ "https://ad-maven.com/": [
+ "ad-maven.com",
+ "agreensdistra.info",
+ "boudja.com",
+ "rensovetors.info",
+ "wrethicap.info"
+ ]
+ }
+ },
+ {
+ "Admicro": {
+ "http://www.admicro.vn/": [
+ "admicro.vn",
+ "vcmedia.vn"
+ ]
+ }
+ },
+ {
+ "Adnium": {
+ "https://adnium.com": [
+ "adnium.com",
+ "montwam.top"
+ ]
+ }
+ },
+ {
+ "AdScore": {
+ "http://www.adscoremarketing.com/": [
+ "adsco.re"
+ ]
+ }
+ },
+ {
+ "AdYouLike": {
+ "https://www.adyoulike.com/": [
+ "pulpix.com"
+ ]
+ }
+ },
+ {
+ "AivaLabs": {
+ "https://aivalabs.com": [
+ "aivalabs.com"
+ ]
+ }
+ },
+ {
+ "Albacross": {
+ "https://albacross.com": [
+ "albacross.com"
+ ]
+ }
+ },
+ {
+ "AppCast": {
+ "https://appcast.io/": [
+ "appcast.io"
+ ]
+ }
+ },
+ {
+ "AuditedMedia": {
+ "https://auditedmedia.com/": [
+ "aamapi.com",
+ "aamsitecertifier.com",
+ "auditedmedia.com"
+ ]
+ }
+ },
+ {
+ "Augur": {
+ "http://www.augur.io/": [
+ "augur.io"
+ ]
+ }
+ },
+ {
+ "Azet": {
+ "http://mediaimpact.sk/": [
+ "azetklik.sk",
+ "rsz.sk"
+ ]
+ }
+ },
+ {
+ "BetssonPalantir": {
+ "https://betssonpalantir.com/": [
+ "betssonpalantir.com"
+ ]
+ }
+ },
+ {
+ "BigClick": {
+ "http://bigclick.me/": [
+ "bgclck.me",
+ "xcvgdf.party"
+ ]
+ }
+ },
+ {
+ "BitMedia": {
+ "https://bitmedia.io/": [
+ "bitmedia.io"
+ ]
+ }
+ },
+ {
+ "BlueCava": {
+ "http://www.bluecava.com/": [
+ "bluecava.com"
+ ]
+ }
+ },
+ {
+ "BoostBox": {
+ "https://www.boostbox.com.br/": [
+ "boostbox.com.br"
+ ]
+ }
+ },
+ {
+ "Brandcrumb": {
+ "http://www.brandcrumb.com": [
+ "brandcrumb.com"
+ ]
+ }
+ },
+ {
+ "BreakTime": {
+ "https://www.breaktime.com.tw/": [
+ "breaktime.com.tw"
+ ]
+ }
+ },
+ {
+ "BrightEdge": {
+ "http://www.brightedge.com/": [
+ "b0e8.com"
+ ]
+ }
+ },
+ {
+ "C3 Metrics": {
+ "http://c3metrics.com/": [
+ "attributionmodel.com",
+ "c3metrics.com",
+ "c3tag.com"
+ ]
+ }
+ },
+ {
+ "CallSource": {
+ "https://www.callsource.com/": [
+ "leadtrackingdata.com"
+ ]
+ }
+ },
+ {
+ "CartsGuru": {
+ "https://carts.guru/": [
+ "carts.guru"
+ ]
+ }
+ },
+ {
+ "ClearLink": {
+ "https://www.clearlink.com/": [
+ "clearlink.com"
+ ]
+ }
+ },
+ {
+ "Clickayab": {
+ "http://www.clickyab.com": [
+ "clickyab.com"
+ ]
+ }
+ },
+ {
+ "ClickFrog": {
+ "https://clickfrog.ru/": [
+ "bashirian.biz",
+ "buckridge.link",
+ "franecki.net",
+ "quitzon.net",
+ "reichelcormier.bid",
+ "wisokykulas.bid"
+ ]
+ }
+ },
+ {
+ "ClickGuard": {
+ "https://www.clickguard.com/": [
+ "clickguard.com"
+ ]
+ }
+ },
+ {
+ "Clixtell": {
+ "https://www.clixtell.com/": [
+ "clixtell.com"
+ ]
+ }
+ },
+ {
+ "Consumable": {
+ "http://consumable.com/": [
+ "consumable.com"
+ ]
+ }
+ },
+ {
+ "dmpxs": {
+ "http://bob.dmpxs.com": [
+ "dmpxs.com"
+ ]
+ }
+ },
+ {
+ "ECSAnalytics": {
+ "https://www.theecsinc.com/": [
+ "ecsanalytics.com"
+ ]
+ }
+ },
+ {
+ "EroAdvertising": {
+ "http://www.ero-advertising.com/": [
+ "ero-advertising.com"
+ ]
+ }
+ },
+ {
+ "eyeReturn Marketing": {
+ "http://www.eyereturnmarketing.com/": [
+ "eyereturn.com",
+ "eyereturnmarketing.com"
+ ]
+ }
+ },
+ {
+ "Fanplayr": {
+ "https://fanplayr.com/": [
+ "fanplayr.com"
+ ]
+ }
+ },
+ {
+ "Foresee": {
+ "https://www.foresee.com": [
+ "answerscloud.com",
+ "foresee.com"
+ ]
+ }
+ },
+ {
+ "Friends2Follow": {
+ "https://friends2follow.com/": [
+ "antifraudjs.friends2follow.com"
+ ]
+ }
+ },
+ {
+ "FuelX": {
+ "https://fuelx.com/": [
+ "fuel451.com",
+ "fuelx.com"
+ ]
+ }
+ },
+ {
+ "Gleam": {
+ "https://gleam.io/": [
+ "fraudjs.io"
+ ]
+ }
+ },
+ {
+ "GrapheneMedia": {
+ "http://graphenemedia.in/": [
+ "graphenedigitalanalytics.in"
+ ]
+ }
+ },
+ {
+ "Gruner + Jahr": {
+ "http://www.guj.de/": [
+ "ligatus.com"
+ ]
+ }
+ },
+ {
+ "HilltopAds": {
+ "https://hilltopads.com/": [
+ "hilltopads.net",
+ "shoporielder.pro"
+ ]
+ }
+ },
+ {
+ "HotelChamp": {
+ "https://www.hotelchamp.com": [
+ "hotelchamp.com"
+ ]
+ }
+ },
+ {
+ "iMedia": {
+ "http://www.imedia.cz": [
+ "imedia.cz"
+ ]
+ }
+ },
+ {
+ "IslayTech": {
+ "http://islay.tech": [
+ "islay.tech"
+ ]
+ }
+ },
+ {
+ "ismatlab.com": {
+ "http://ismatlab.com": [
+ "ismatlab.com"
+ ]
+ }
+ },
+ {
+ "Itch": {
+ "https://itch.io/": [
+ "itch.io"
+ ]
+ }
+ },
+ {
+ "justuno": {
+ "https://www.justuno.com/": [
+ "justuno.com"
+ ]
+ }
+ },
+ {
+ "Konduto": {
+ "http://konduto.com": [
+ "k-analytix.com",
+ "konduto.com"
+ ]
+ }
+ },
+ {
+ "LeadsHub": {
+ "https://ztsrv.com/": [
+ "ztsrv.com"
+ ]
+ }
+ },
+ {
+ "lptracker": {
+ "https://lptracker.io/": [
+ "lptracker.io"
+ ]
+ }
+ },
+ {
+ "MaxMind": {
+ "https://www.maxmind.com/en/home": [
+ "maxmind.com",
+ "mmapiws.com"
+ ]
+ }
+ },
+ {
+ "Mercadopago": {
+ "https://www.mercadopago.com/": [
+ "mercadopago.com"
+ ]
+ }
+ },
+ {
+ "Mobials": {
+ "http://mobials.com": [
+ "mobials.com"
+ ]
+ }
+ },
+ {
+ "Mystighty": {
+ "http://mystighty.info/": [
+ "mystighty.info",
+ "sweeterge.info"
+ ]
+ }
+ },
+ {
+ "Negishim": {
+ "http://www.negishim.org": [
+ "negishim.org"
+ ]
+ }
+ },
+ {
+ "NuDataSecurity": {
+ "https://nudatasecurity.com/": [
+ "nudatasecurity.com"
+ ]
+ }
+ },
+ {
+ "OneAd": {
+ "https://www.onead.com.tw/": [
+ "guoshipartners.com",
+ "onevision.com.tw"
+ ]
+ }
+ },
+ {
+ "OnlineMetrix": {
+ "http://h.online-metrix.net": [
+ "online-metrix.net"
+ ]
+ }
+ },
+ {
+ "Opolen": {
+ "https://opolen.com.br": [
+ "opolen.com.br"
+ ]
+ }
+ },
+ {
+ "PaymentsMB": {
+ "https://paymentsmb.com": [
+ "paymentsmb.com"
+ ]
+ }
+ },
+ {
+ "Paypal": {
+ "https://www.paypal.com": [
+ "simility.com"
+ ]
+ }
+ },
+ {
+ "PerimeterX": {
+ "https://www.perimeterx.com": [
+ "perimeterx.net"
+ ]
+ }
+ },
+ {
+ "PixAnalytics": {
+ "https://pixanalytics.com/": [
+ "pixanalytics.com"
+ ]
+ }
+ },
+ {
+ "Pixlee": {
+ "https://www.pixlee.com/": [
+ "pixlee.com"
+ ]
+ }
+ },
+ {
+ "Poool": {
+ "http://poool.fr/": [
+ "poool.fr"
+ ]
+ }
+ },
+ {
+ "PPCProtect": {
+ "https://ppcprotect.com": [
+ "ppcprotect.com"
+ ]
+ }
+ },
+ {
+ "PrismApp": {
+ "https://www.prismapp.io/": [
+ "prismapp.io"
+ ]
+ }
+ },
+ {
+ "PrometheusIntelligenceTechnology": {
+ "https://prometheusintelligencetechnology.com/": [
+ "prometheusintelligencetechnology.com"
+ ]
+ }
+ },
+ {
+ "Provers": {
+ "http://provers.pro": [
+ "provers.pro"
+ ]
+ }
+ },
+ {
+ "Psonstrentie": {
+ "http://psonstrentie.info": [
+ "psonstrentie.info"
+ ]
+ }
+ },
+ {
+ "Rollick": {
+ "https://gorollick.com": [
+ "rollick.io"
+ ]
+ }
+ },
+ {
+ "SAP": {
+ "https://www.sap.com": [
+ "seewhy.com"
+ ]
+ }
+ },
+ {
+ "Selectable Media": {
+ "http://selectablemedia.com/": [
+ "nabbr.com",
+ "selectablemedia.com"
+ ]
+ }
+ },
+ {
+ "Semantiqo": {
+ "http://semantiqo.com/": [
+ "semantiqo.com"
+ ]
+ }
+ },
+ {
+ "SendPulse": {
+ "https://sendpulse.com/": [
+ "sendpulse.com"
+ ]
+ }
+ },
+ {
+ "ShaftTraffic": {
+ "https://shafttraffic.com": [
+ "libertystmedia.com"
+ ]
+ }
+ },
+ {
+ "Shortest": {
+ "http://shorte.st/": [
+ "shorte.st"
+ ]
+ }
+ },
+ {
+ "SiftScience": {
+ "https://sift.com/": [
+ "siftscience.com"
+ ]
+ }
+ },
+ {
+ "Signifyd": {
+ "https://www.signifyd.com/": [
+ "signifyd.com"
+ ]
+ }
+ },
+ {
+ "Smi": {
+ "http://24smi.net": [
+ "24smi.net"
+ ]
+ }
+ },
+ {
+ "Socital": {
+ "https://www.socital.com": [
+ "socital.com"
+ ]
+ }
+ },
+ {
+ "Storeland": {
+ "https://storeland.ru/": [
+ "storeland.ru"
+ ]
+ }
+ },
+ {
+ "Stripe": {
+ "https://stripe.com": [
+ "stripe.network"
+ ]
+ }
+ },
+ {
+ "TechSolutions": {
+ "https://www.techsolutions.com.tw/": [
+ "techsolutions.com.tw"
+ ]
+ }
+ },
+ {
+ "tongdun.cn": {
+ "https://www.tongdun.cn/?lan=EN": [
+ "fraudmetrix.cn",
+ "tongdun.net"
+ ]
+ }
+ },
+ {
+ "Upland": {
+ "https://uplandsoftware.com/": [
+ "leadlander.com",
+ "sf14g.com"
+ ]
+ }
+ },
+ {
+ "Vendemore": {
+ "https://vendemore.com/": [
+ "vendemore.com"
+ ]
+ }
+ },
+ {
+ "VerticalHealth": {
+ "https://www.verticalhealth.com/": [
+ "verticalhealth.net"
+ ]
+ }
+ },
+ {
+ "Webmecanik": {
+ "https://www.webmecanik.com/": [
+ "webmecanik.com"
+ ]
+ }
+ },
+ {
+ "WideOrbit": {
+ "https://www.wideorbit.com/": [
+ "dep-x.com"
+ ]
+ }
+ },
+ {
+ "YSance": {
+ "https://www.ysance.com/data-services/fr/home/": [
+ "y-track.com"
+ ]
+ }
+ },
+ {
+ "ZafulAffiliate": {
+ "https://affiliate.zaful.com/": [
+ "affasi.com",
+ "gw-ec.com",
+ "zaful.com"
+ ]
+ }
+ },
+ {
+ "Zefir": {
+ "https://ze-fir.com/": [
+ "ze-fir.com"
+ ]
+ }
+ }
+ ],
+ "Social": [
+ {
+ "AddThis": {
+ "http://www.addthis.com/": [
+ "addthis.com",
+ "addthiscdn.com",
+ "addthisedge.com",
+ "clearspring.com",
+ "connectedads.net",
+ "xgraph.com",
+ "xgraph.net"
+ ]
+ }
+ },
+ {
+ "Causes": {
+ "http://www.causes.com/": [
+ "causes.com"
+ ]
+ }
+ },
+ {
+ "Digg": {
+ "http://digg.com/": [
+ "digg.com"
+ ]
+ }
+ },
+ {
+ "Facebook": {
+ "http://www.facebook.com/": [
+ "apps.fbsbx.com",
+ "atdmt.com",
+ "facebook.com",
+ "facebook.de",
+ "facebook.fr",
+ "facebook.net",
+ "fb.com",
+ "fbsbx.com",
+ "friendfeed.com"
+ ]
+ }
+ },
+ {
+ "Google": {
+ "http://www.google.com/": [
+ "developers.google.com",
+ "gmail.com",
+ "googlemail.com",
+ "inbox.google.com",
+ "mail.google.com",
+ "orkut.com",
+ "plus.google.com",
+ "plusone.google.com",
+ "smartlock.google.com",
+ "voice.google.com",
+ "wave.google.com"
+ ]
+ }
+ },
+ {
+ "LinkedIn": {
+ "http://www.linkedin.com/": [
+ "licdn.com",
+ "linkedin.com"
+ ]
+ }
+ },
+ {
+ "Lockerz": {
+ "http://lockerz.com/": [
+ "lockerz.com"
+ ]
+ }
+ },
+ {
+ "Mail.Ru": {
+ "http://mail.ru/": [
+ "list.ru",
+ "mail.ru"
+ ]
+ }
+ },
+ {
+ "Meebo": {
+ "https://www.meebo.com/": [
+ "meebo.com",
+ "meebocdn.net"
+ ]
+ }
+ },
+ {
+ "Papaya": {
+ "http://papayamobile.com/": [
+ "papayamobile.com"
+ ]
+ }
+ },
+ {
+ "reddit": {
+ "http://www.reddit.com/": [
+ "reddit.com"
+ ]
+ }
+ },
+ {
+ "Shareaholic": {
+ "http://www.shareaholic.com/": [
+ "shareaholic.com"
+ ]
+ }
+ },
+ {
+ "ShareThis": {
+ "http://sharethis.com/": [
+ "sharethis.com"
+ ]
+ }
+ },
+ {
+ "StumbleUpon": {
+ "http://www.stumbleupon.com/": [
+ "stumble-upon.com",
+ "stumbleupon.com"
+ ]
+ }
+ },
+ {
+ "Twitter": {
+ "https://twitter.com/": [
+ "twimg.com",
+ "twitter.com",
+ "twitter.jp"
+ ]
+ }
+ },
+ {
+ "VKontakte": {
+ "http://vk.com/": [
+ "userapi.com",
+ "vk.com",
+ "vkontakte.ru"
+ ]
+ }
+ },
+ {
+ "Yahoo!": {
+ "http://www.yahoo.com/": [
+ "address.yahoo.com",
+ "alerts.yahoo.com",
+ "avatars.yahoo.com",
+ "buzz.yahoo.com",
+ "calendar.yahoo.com",
+ "edit.yahoo.com",
+ "legalredirect.yahoo.com",
+ "login.yahoo.com",
+ "mail.yahoo.com",
+ "my.yahoo.com",
+ "mybloglog.com",
+ "notepad.yahoo.com",
+ "pulse.yahoo.com",
+ "rocketmail.com",
+ "webmessenger.yahoo.com",
+ "ymail.com"
+ ]
+ }
+ }
+ ],
+ "Cryptomining": [
+ {
+ "a.js": {
+ "http://zymerget.bid": [
+ "alflying.date",
+ "alflying.win",
+ "anybest.site",
+ "flightsy.bid",
+ "flightsy.win",
+ "flightzy.bid",
+ "flightzy.date",
+ "flightzy.win",
+ "zymerget.bid",
+ "zymerget.faith"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "CashBeet": {
+ "http://cashbeet.com": [
+ "cashbeet.com",
+ "serv1swork.com"
+ ]
+ }
+ },
+ {
+ "CoinHive": {
+ "https://coinhive.com": [
+ "ad-miner.com",
+ "authedmine.com",
+ "bmst.pw",
+ "cnhv.co",
+ "coin-hive.com",
+ "coinhive.com",
+ "wsservices.org"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "CoinPot": {
+ "http://coinpot.co": [
+ "coinpot.co"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "CryptoLoot": {
+ "https://crypto-loot.com": [
+ "cryptaloot.pro",
+ "crypto-loot.com",
+ "cryptolootminer.com",
+ "flashx.pw",
+ "gitgrub.pro",
+ "reauthenticator.com",
+ "statdynamic.com",
+ "webmine.pro"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "CryptoWebMiner": {
+ "https://www.crypto-webminer.com": [
+ "bitcoin-pay.eu",
+ "crypto-webminer.com",
+ "ethpocket.de",
+ "ethtrader.de"
+ ]
+ }
+ },
+ {
+ "Gridcash": {
+ "https://www.gridcash.net/": [
+ "adless.io",
+ "gridcash.net"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "JSE": {
+ "http://jsecoin.com": [
+ "freecontent.bid",
+ "freecontent.date",
+ "freecontent.stream",
+ "hashing.win",
+ "hostingcloud.racing",
+ "hostingcloud.science",
+ "jsecoin.com"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "MinerAlt": {
+ "http://mineralt.io": [
+ "1q2w3.website",
+ "analytics.blue",
+ "aster18cdn.nl",
+ "belicimo.pw",
+ "besstahete.info",
+ "dinorslick.icu",
+ "feesocrald.com",
+ "gramombird.com",
+ "istlandoll.com",
+ "mepirtedic.com",
+ "mineralt.io",
+ "pampopholf.com",
+ "tercabilis.info",
+ "tulip18.com",
+ "vidzi.tv",
+ "yololike.space"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "Minescripts": {
+ "http://minescripts.info": [
+ "minescripts.info",
+ "sslverify.info"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "MineXMR": {
+ "http://minexmr.stream": [
+ "minexmr.stream"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "NeroHut": {
+ "https://nerohut.com": [
+ "nerohut.com",
+ "nhsrv.cf"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "Service4refresh": {
+ "https://service4refresh.info": [
+ "service4refresh.info"
+ ]
+ }
+ },
+ {
+ "SpareChange": {
+ "http://sparechange.io": [
+ "sparechange.io"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "SwiftMining": {
+ "https://swiftmining.win/": [
+ "swiftmining.win"
+ ]
+ }
+ },
+ {
+ "Webmine": {
+ "https://webmine.cz/": [
+ "authedwebmine.cz",
+ "webmine.cz"
+ ]
+ }
+ },
+ {
+ "WebminePool": {
+ "http://webminepool.com": [
+ "webminepool.com"
+ ],
+ "performance": "true"
+ }
+ },
+ {
+ "Webmining": {
+ "https://webmining.co/": [
+ "webmining.co"
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_safelist.json b/mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_safelist.json
new file mode 100644
index 0000000000..a2c80176b6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_safelist.json
@@ -0,0 +1,12347 @@
+{
+ "2leep.com": {
+ "properties": [
+ "2leep.com"
+ ],
+ "resources": [
+ "2leep.com"
+ ]
+ },
+ "33Across": {
+ "properties": [
+ "33across.com",
+ "tynt.com"
+ ],
+ "resources": [
+ "33across.com",
+ "tynt.com"
+ ]
+ },
+ "365Media": {
+ "properties": [
+ "aggregateintelligence.com"
+ ],
+ "resources": [
+ "365media.com",
+ "aggregateintelligence.com"
+ ]
+ },
+ "4INFO": {
+ "properties": [
+ "4info.com",
+ "adhaven.com"
+ ],
+ "resources": [
+ "4info.com",
+ "adhaven.com"
+ ]
+ },
+ "4mads": {
+ "properties": [
+ "4mads.com"
+ ],
+ "resources": [
+ "4mads.com"
+ ]
+ },
+ "63 Squares": {
+ "properties": [
+ "63labs.com"
+ ],
+ "resources": [
+ "63labs.com",
+ "63squares.com",
+ "i-stats.com"
+ ]
+ },
+ "Abax Interactive": {
+ "properties": [
+ "abaxinteractive.com"
+ ],
+ "resources": [
+ "abaxinteractive.com"
+ ]
+ },
+ "Accelia": {
+ "properties": [
+ "accelia.net",
+ "durasite.net"
+ ],
+ "resources": [
+ "accelia.net",
+ "durasite.net"
+ ]
+ },
+ "Accordant Media": {
+ "properties": [
+ "accordantmedia.com"
+ ],
+ "resources": [
+ "accordantmedia.com"
+ ]
+ },
+ "Acquisio": {
+ "properties": [
+ "acquisio.com",
+ "clickequations.net"
+ ],
+ "resources": [
+ "acquisio.com",
+ "clickequations.net"
+ ]
+ },
+ "Actisens": {
+ "properties": [
+ "actisens.com",
+ "gestionpub.com"
+ ],
+ "resources": [
+ "actisens.com",
+ "gestionpub.com"
+ ]
+ },
+ "ActiveConversion": {
+ "properties": [
+ "activeconversion.com",
+ "activemeter.com"
+ ],
+ "resources": [
+ "activeconversion.com",
+ "activemeter.com"
+ ]
+ },
+ "ActivEngage": {
+ "properties": [
+ "activengage.com"
+ ],
+ "resources": [
+ "activengage.com"
+ ]
+ },
+ "Act-On": {
+ "properties": [
+ "act-on.com",
+ "actonsoftware.com"
+ ],
+ "resources": [
+ "act-on.com",
+ "actonsoftware.com"
+ ]
+ },
+ "Acuity": {
+ "properties": [
+ "acuity.com",
+ "acuityads.com",
+ "acuityplatform.com"
+ ],
+ "resources": [
+ "acuity.com",
+ "acuityads.com",
+ "acuityplatform.com"
+ ]
+ },
+ "Acxiom": {
+ "properties": [
+ "acxiom.com",
+ "mm7.net"
+ ],
+ "resources": [
+ "acxiom.com",
+ "acxiomapac.com",
+ "mm7.net",
+ "pippio.com"
+ ]
+ },
+ "AD2ONE": {
+ "properties": [
+ "ad2onegroup.com"
+ ],
+ "resources": [
+ "ad2onegroup.com"
+ ]
+ },
+ "Ad4Game": {
+ "properties": [
+ "ad4game.com"
+ ],
+ "resources": [
+ "ad4game.com"
+ ]
+ },
+ "ad6media": {
+ "properties": [
+ "ad6media.fr"
+ ],
+ "resources": [
+ "ad6media.fr"
+ ]
+ },
+ "Adabra": {
+ "properties": [
+ "adabra.com"
+ ],
+ "resources": [
+ "adabra.com"
+ ]
+ },
+ "Adality": {
+ "properties": [
+ "adality.de"
+ ],
+ "resources": [
+ "adality.de",
+ "adrtx.net"
+ ]
+ },
+ "AdaptiveAds": {
+ "properties": [
+ "adaptiveads.com"
+ ],
+ "resources": [
+ "adaptiveads.com"
+ ]
+ },
+ "Adaptly": {
+ "properties": [
+ "adaptly.com"
+ ],
+ "resources": [
+ "adaptly.com"
+ ]
+ },
+ "Adap.tv": {
+ "properties": [
+ "adap.tv"
+ ],
+ "resources": [
+ "adap.tv"
+ ]
+ },
+ "Adara Media": {
+ "properties": [
+ "adaramedia.com",
+ "opinmind.com",
+ "yieldoptimizer.com"
+ ],
+ "resources": [
+ "adaramedia.com",
+ "opinmind.com",
+ "yieldoptimizer.com"
+ ]
+ },
+ "Adatus": {
+ "properties": [
+ "adatus.com"
+ ],
+ "resources": [
+ "adatus.com"
+ ]
+ },
+ "Adbot": {
+ "properties": [
+ "adbot.tw"
+ ],
+ "resources": [
+ "adbot.tw"
+ ]
+ },
+ "Adbrain": {
+ "properties": [
+ "adbrain.com"
+ ],
+ "resources": [
+ "adbrain.com",
+ "adbrn.com"
+ ]
+ },
+ "adBrite": {
+ "properties": [
+ "adbrite.com"
+ ],
+ "resources": [
+ "adbrite.com"
+ ]
+ },
+ "Adbroker.de": {
+ "properties": [
+ "adbroker.de"
+ ],
+ "resources": [
+ "adbroker.de"
+ ]
+ },
+ "Adchemy": {
+ "properties": [
+ "adchemy.com"
+ ],
+ "resources": [
+ "adchemy.com"
+ ]
+ },
+ "AdCirrus": {
+ "properties": [
+ "adcirrus.com"
+ ],
+ "resources": [
+ "adcirrus.com"
+ ]
+ },
+ "Ad Decisive": {
+ "properties": [
+ "a2dfp.net",
+ "addecisive.com"
+ ],
+ "resources": [
+ "a2dfp.net",
+ "addecisive.com"
+ ]
+ },
+ "AddFreeStats": {
+ "properties": [
+ "3dstats.com",
+ "addfreestats.com"
+ ],
+ "resources": [
+ "3dstats.com",
+ "addfreestats.com"
+ ]
+ },
+ "addGloo": {
+ "properties": [
+ "addgloo.com"
+ ],
+ "resources": [
+ "addgloo.com"
+ ]
+ },
+ "AddThis": {
+ "properties": [
+ "addthis.com"
+ ],
+ "resources": [
+ "addthis.com",
+ "addthiscdn.com",
+ "addthisedge.com",
+ "clearspring.com",
+ "connectedads.net",
+ "xgraph.com",
+ "xgraph.net"
+ ]
+ },
+ "Addvantage Media": {
+ "properties": [
+ "addvantagemedia.com"
+ ],
+ "resources": [
+ "addvantagemedia.com"
+ ]
+ },
+ "Ad Dynamo": {
+ "properties": [
+ "addynamo.com"
+ ],
+ "resources": [
+ "addynamo.com",
+ "addynamo.net"
+ ]
+ },
+ "Adelphic": {
+ "properties": [
+ "adelphic.com"
+ ],
+ "resources": [
+ "adelphic.com",
+ "ipredictive.com"
+ ]
+ },
+ "AdEngage": {
+ "properties": [
+ "adengage.com"
+ ],
+ "resources": [
+ "adengage.com"
+ ]
+ },
+ "AD Europe": {
+ "properties": [
+ "adeurope.com"
+ ],
+ "resources": [
+ "adeurope.com"
+ ]
+ },
+ "AdExtent": {
+ "properties": [
+ "adextent.com"
+ ],
+ "resources": [
+ "adextent.com"
+ ]
+ },
+ "AdF.ly": {
+ "properties": [
+ "adf.ly"
+ ],
+ "resources": [
+ "adf.ly"
+ ]
+ },
+ "Adfonic": {
+ "properties": [
+ "adfonic.com"
+ ],
+ "resources": [
+ "adfonic.com"
+ ]
+ },
+ "Adforge": {
+ "properties": [
+ "adforgeinc.com"
+ ],
+ "resources": [
+ "adforgeinc.com"
+ ]
+ },
+ "Adform": {
+ "properties": [
+ "adform.com"
+ ],
+ "resources": [
+ "adform.com",
+ "adform.net",
+ "adformdsp.net"
+ ]
+ },
+ "AdFox": {
+ "properties": [
+ "adfox.ru"
+ ],
+ "resources": [
+ "adfox.ru"
+ ]
+ },
+ "AdFrontiers": {
+ "properties": [
+ "adfrontiers.com"
+ ],
+ "resources": [
+ "adfrontiers.com"
+ ]
+ },
+ "Adfunky": {
+ "properties": [
+ "adfunky.com",
+ "adfunkyserver.com"
+ ],
+ "resources": [
+ "adfunky.com",
+ "adfunkyserver.com"
+ ]
+ },
+ "Adfusion": {
+ "properties": [
+ "adfusion.com"
+ ],
+ "resources": [
+ "adfusion.com"
+ ]
+ },
+ "AdGainerSolutions": {
+ "properties": [
+ "adgainersolutions.com"
+ ],
+ "resources": [
+ "adgainersolutions.com"
+ ]
+ },
+ "AdGent Digital": {
+ "properties": [
+ "adgentdigital.com"
+ ],
+ "resources": [
+ "adgentdigital.com",
+ "shorttailmedia.com"
+ ]
+ },
+ "AdGibbon": {
+ "properties": [
+ "adgibbon.com"
+ ],
+ "resources": [
+ "adgibbon.com"
+ ]
+ },
+ "Adglare": {
+ "properties": [
+ "adglare.com"
+ ],
+ "resources": [
+ "adglare.com",
+ "adglare.net"
+ ]
+ },
+ "adhood": {
+ "properties": [
+ "adhood.com"
+ ],
+ "resources": [
+ "adhood.com"
+ ]
+ },
+ "Adiant": {
+ "properties": [
+ "adblade.com",
+ "adiant.com"
+ ],
+ "resources": [
+ "adblade.com",
+ "adiant.com"
+ ]
+ },
+ "AdInsight": {
+ "properties": [
+ "responsetap.com"
+ ],
+ "resources": [
+ "adinsight.com",
+ "adinsight.eu",
+ "responsetap.com"
+ ]
+ },
+ "AdIQuity": {
+ "properties": [
+ "adiquity.com"
+ ],
+ "resources": [
+ "adiquity.com"
+ ]
+ },
+ "ADITION": {
+ "properties": [
+ "adition.com"
+ ],
+ "resources": [
+ "adition.com"
+ ]
+ },
+ "AdJug": {
+ "properties": [
+ "adjug.com"
+ ],
+ "resources": [
+ "adjug.com"
+ ]
+ },
+ "AdJuggler": {
+ "properties": [
+ "adjuggler.com",
+ "adjuggler.net"
+ ],
+ "resources": [
+ "adjuggler.com",
+ "adjuggler.net"
+ ]
+ },
+ "Adjust": {
+ "properties": [
+ "adjust.com"
+ ],
+ "resources": [
+ "adjust.com"
+ ]
+ },
+ "AdKeeper": {
+ "properties": [
+ "keep.com"
+ ],
+ "resources": [
+ "adkeeper.com",
+ "akncdn.com",
+ "keep.com"
+ ]
+ },
+ "AdKernel": {
+ "properties": [
+ "adkernel.com"
+ ],
+ "resources": [
+ "adkernel.com"
+ ]
+ },
+ "Ad Knife": {
+ "properties": [
+ "adknife.com"
+ ],
+ "resources": [
+ "adknife.com"
+ ]
+ },
+ "Adknowledge": {
+ "properties": [
+ "adknowledge.com",
+ "adparlor.com",
+ "bidsystem.com",
+ "cubics.com",
+ "lookery.com"
+ ],
+ "resources": [
+ "adknowledge.com",
+ "adparlor.com",
+ "bidsystem.com",
+ "cubics.com",
+ "lookery.com"
+ ]
+ },
+ "AdLantis": {
+ "properties": [
+ "adimg.net",
+ "adlantis.jp",
+ "www.adlantis.jp"
+ ],
+ "resources": [
+ "adimg.net",
+ "adlantis.jp",
+ "www.adlantis.jp"
+ ]
+ },
+ "AdLeave": {
+ "properties": [
+ "adleave.com"
+ ],
+ "resources": [
+ "adleave.com"
+ ]
+ },
+ "Adlibrium": {
+ "properties": [
+ "adlibrium.com"
+ ],
+ "resources": [
+ "adlibrium.com"
+ ]
+ },
+ "Adloox": {
+ "properties": [
+ "adloox.com"
+ ],
+ "resources": [
+ "adloox.com",
+ "adlooxtracking.com"
+ ]
+ },
+ "Adlucent": {
+ "properties": [
+ "adlucent.com"
+ ],
+ "resources": [
+ "adlucent.com"
+ ]
+ },
+ "Ad Magnet": {
+ "properties": [
+ "admagnet.com",
+ "admagnet.net"
+ ],
+ "resources": [
+ "admagnet.com",
+ "admagnet.net"
+ ]
+ },
+ "Admarketplace": {
+ "properties": [
+ "admarketplace.com"
+ ],
+ "resources": [
+ "admarketplace.com",
+ "admarketplace.net",
+ "ampxchange.com"
+ ]
+ },
+ "AdMarvel": {
+ "properties": [
+ "admarvel.com"
+ ],
+ "resources": [
+ "admarvel.com"
+ ]
+ },
+ "AdMatrix": {
+ "properties": [
+ "admatrix.jp"
+ ],
+ "resources": [
+ "admatrix.jp"
+ ]
+ },
+ "AdMaven": {
+ "properties": [
+ "ad-maven.com"
+ ],
+ "resources": [
+ "ad-maven.com",
+ "agreensdistra.info",
+ "boudja.com",
+ "rensovetors.info",
+ "wrethicap.info"
+ ]
+ },
+ "AdMaximizer Network": {
+ "properties": [
+ "admaximizer.com"
+ ],
+ "resources": [
+ "admaximizer.com"
+ ]
+ },
+ "AdMedia": {
+ "properties": [
+ "admedia.com"
+ ],
+ "resources": [
+ "admedia.com"
+ ]
+ },
+ "Admeta": {
+ "properties": [
+ "admeta.com",
+ "atemda.com"
+ ],
+ "resources": [
+ "admeta.com",
+ "atemda.com"
+ ]
+ },
+ "Admicro": {
+ "properties": [
+ "admicro.vn"
+ ],
+ "resources": [
+ "admicro.vn",
+ "vcmedia.vn"
+ ]
+ },
+ "Admixer": {
+ "properties": [
+ "admixer.co.kr"
+ ],
+ "resources": [
+ "admixer.co.kr"
+ ]
+ },
+ "Admized": {
+ "properties": [
+ "admized.com"
+ ],
+ "resources": [
+ "admized.com"
+ ]
+ },
+ "Admobile": {
+ "properties": [
+ "admobile.com"
+ ],
+ "resources": [
+ "admobile.com"
+ ]
+ },
+ "Admotion": {
+ "properties": [
+ "admotion.com"
+ ],
+ "resources": [
+ "admotion.com",
+ "nspmotion.com"
+ ]
+ },
+ "Adnetik": {
+ "properties": [
+ "wtp101.com"
+ ],
+ "resources": [
+ "adnetik.com",
+ "wtp101.com"
+ ]
+ },
+ "AdNetwork.net": {
+ "properties": [
+ "adnetwork.net"
+ ],
+ "resources": [
+ "adnetwork.net"
+ ]
+ },
+ "Adnium": {
+ "properties": [
+ "adnium.com"
+ ],
+ "resources": [
+ "adnium.com",
+ "montwam.top"
+ ]
+ },
+ "adnologies": {
+ "properties": [
+ "adnologies.com",
+ "heias.com"
+ ],
+ "resources": [
+ "adnologies.com",
+ "heias.com"
+ ]
+ },
+ "Adobe": {
+ "properties": [
+ "adobe.com",
+ "livefyre.com",
+ "typekit.com"
+ ],
+ "resources": [
+ "2o7.net",
+ "adobe.com",
+ "auditude.com",
+ "demdex.com",
+ "demdex.net",
+ "dmtracker.com",
+ "efrontier.com",
+ "everestads.net",
+ "everestjs.net",
+ "everesttech.net",
+ "fyre.co",
+ "hitbox.com",
+ "livefyre.com",
+ "omniture.com",
+ "omtrdc.net",
+ "touchclarity.com",
+ "typekit.com"
+ ]
+ },
+ "AdOcean": {
+ "properties": [
+ "adocean-global.com",
+ "adocean.pl"
+ ],
+ "resources": [
+ "adocean-global.com",
+ "adocean.pl"
+ ]
+ },
+ "Adometry": {
+ "properties": [
+ "adometry.com"
+ ],
+ "resources": [
+ "adometry.com",
+ "dmtry.com"
+ ]
+ },
+ "Adomik": {
+ "properties": [
+ "adomik.com"
+ ],
+ "resources": [
+ "adomik.com"
+ ]
+ },
+ "AdOnion": {
+ "properties": [
+ "adonion.com"
+ ],
+ "resources": [
+ "adonion.com"
+ ]
+ },
+ "Adorika": {
+ "properties": [
+ "clickotmedia.com"
+ ],
+ "resources": [
+ "clickotmedia.com"
+ ]
+ },
+ "Adotmob": {
+ "properties": [
+ "adotmob.com"
+ ],
+ "resources": [
+ "adotmob.com"
+ ]
+ },
+ "ADP Dealer Services": {
+ "properties": [
+ "cdkglobal.com"
+ ],
+ "resources": [
+ "admission.net",
+ "adpdealerservices.com",
+ "cdkglobal.com",
+ "cobalt.com"
+ ]
+ },
+ "ad pepper media": {
+ "properties": [
+ "adpepper.com",
+ "adpepper.us"
+ ],
+ "resources": [
+ "adpepper.com",
+ "adpepper.us"
+ ]
+ },
+ "AdPerfect": {
+ "properties": [
+ "adperfect.com"
+ ],
+ "resources": [
+ "adperfect.com"
+ ]
+ },
+ "Adperium": {
+ "properties": [
+ "adperium.com"
+ ],
+ "resources": [
+ "adperium.com"
+ ]
+ },
+ "Adpersia": {
+ "properties": [
+ "adpersia.com"
+ ],
+ "resources": [
+ "adpersia.com"
+ ]
+ },
+ "adPrecision": {
+ "properties": [
+ "adprecision.net",
+ "adprs.net"
+ ],
+ "resources": [
+ "adprecision.net",
+ "adprs.net",
+ "aprecision.net"
+ ]
+ },
+ "AdPredictive": {
+ "properties": [
+ "adpredictive.com"
+ ],
+ "resources": [
+ "adpredictive.com"
+ ]
+ },
+ "AdReactor": {
+ "properties": [
+ "adreactor.com"
+ ],
+ "resources": [
+ "adreactor.com"
+ ]
+ },
+ "AdReady": {
+ "properties": [
+ "digitalremedy.com"
+ ],
+ "resources": [
+ "adready.com",
+ "adreadytractions.com",
+ "digitalremedy"
+ ]
+ },
+ "AdRevolution": {
+ "properties": [
+ "adrevolution.com"
+ ],
+ "resources": [
+ "adrevolution.com"
+ ]
+ },
+ "AdRiver": {
+ "properties": [
+ "adriver.ru"
+ ],
+ "resources": [
+ "adriver.ru"
+ ]
+ },
+ "adrolays": {
+ "properties": [
+ "contactimpact.de"
+ ],
+ "resources": [
+ "adrolays.com",
+ "adrolays.de",
+ "contactimpact.de"
+ ]
+ },
+ "AdRoll": {
+ "properties": [
+ "adroll.com"
+ ],
+ "resources": [
+ "adroll.com"
+ ]
+ },
+ "adscale": {
+ "properties": [
+ "stroeer.de"
+ ],
+ "resources": [
+ "adscale.de",
+ "stroeer.de"
+ ]
+ },
+ "Adscience": {
+ "properties": [
+ "adscience.nl"
+ ],
+ "resources": [
+ "adscience.nl"
+ ]
+ },
+ "AdScore": {
+ "properties": [
+ "adscoremarketing.com"
+ ],
+ "resources": [
+ "adsco.re"
+ ]
+ },
+ "AdServerPub": {
+ "properties": [
+ "adserverpub.com"
+ ],
+ "resources": [
+ "adserverpub.com"
+ ]
+ },
+ "AdShuffle": {
+ "properties": [
+ "adshuffle.com"
+ ],
+ "resources": [
+ "adshuffle.com"
+ ]
+ },
+ "AdSide": {
+ "properties": [
+ "adside.com",
+ "doclix.com"
+ ],
+ "resources": [
+ "adside.com",
+ "doclix.com"
+ ]
+ },
+ "AdSpeed": {
+ "properties": [
+ "adspeed.com",
+ "adspeed.net"
+ ],
+ "resources": [
+ "adspeed.com",
+ "adspeed.net"
+ ]
+ },
+ "Adsperity": {
+ "properties": [
+ "adsperity.com"
+ ],
+ "resources": [
+ "adsperity.com"
+ ]
+ },
+ "AdSpirit": {
+ "properties": [
+ "adspirit.com",
+ "adspirit.de",
+ "adspirit.net"
+ ],
+ "resources": [
+ "adspirit.com",
+ "adspirit.de",
+ "adspirit.net"
+ ]
+ },
+ "Adsrevenue.net": {
+ "properties": [
+ "adsrevenue.net"
+ ],
+ "resources": [
+ "adsrevenue.net"
+ ]
+ },
+ "AdStir": {
+ "properties": [
+ "ad-stir.com"
+ ],
+ "resources": [
+ "ad-stir.com"
+ ]
+ },
+ "AdsTours": {
+ "properties": [
+ "adstours.com",
+ "clickintext.net"
+ ],
+ "resources": [
+ "adstours.com",
+ "clickintext.net"
+ ]
+ },
+ "Adsty": {
+ "properties": [
+ "adsty.com",
+ "adx1.com"
+ ],
+ "resources": [
+ "adsty.com",
+ "adx1.com"
+ ]
+ },
+ "Adsupply": {
+ "properties": [
+ "4dsply.com",
+ "adsupply.com"
+ ],
+ "resources": [
+ "4dsply.com",
+ "adsupply.com"
+ ]
+ },
+ "Adswizz": {
+ "properties": [
+ "adswizz.com"
+ ],
+ "resources": [
+ "adswizz.com"
+ ]
+ },
+ "ADTECH": {
+ "properties": [
+ "adtech.com",
+ "adtech.de",
+ "adtechus.com"
+ ],
+ "resources": [
+ "adtech.com",
+ "adtech.de",
+ "adtechus.com"
+ ]
+ },
+ "Adtegrity.com": {
+ "properties": [
+ "adtegrity.com",
+ "adtegrity.net"
+ ],
+ "resources": [
+ "adtegrity.com",
+ "adtegrity.net"
+ ]
+ },
+ "ADTELLIGENCE": {
+ "properties": [
+ "adtelligence.de"
+ ],
+ "resources": [
+ "adtelligence.de"
+ ]
+ },
+ "Adthink": {
+ "properties": [
+ "adthink.com"
+ ],
+ "resources": [
+ "adthink.com",
+ "audienceinsights.net"
+ ]
+ },
+ "AdTiger": {
+ "properties": [
+ "adtiger.de"
+ ],
+ "resources": [
+ "adtiger.de"
+ ]
+ },
+ "AdTruth": {
+ "properties": [
+ "adtruth.com"
+ ],
+ "resources": [
+ "adtruth.com"
+ ]
+ },
+ "Adult AdWorld": {
+ "properties": [
+ "adultadworld.com"
+ ],
+ "resources": [
+ "adultadworld.com"
+ ]
+ },
+ "Adultmoda": {
+ "properties": [
+ "adultmoda.com"
+ ],
+ "resources": [
+ "adultmoda.com"
+ ]
+ },
+ "Adventive": {
+ "properties": [
+ "adventive.com"
+ ],
+ "resources": [
+ "adventive.com"
+ ]
+ },
+ "Adventori": {
+ "properties": [
+ "adventori.com"
+ ],
+ "resources": [
+ "adventori.com"
+ ]
+ },
+ "Adverline": {
+ "properties": [
+ "adnext.fr",
+ "adverline.com"
+ ],
+ "resources": [
+ "adnext.fr",
+ "adverline.com"
+ ]
+ },
+ "Adversal.com": {
+ "properties": [
+ "adv-adserver.com",
+ "adversal.com"
+ ],
+ "resources": [
+ "adv-adserver.com",
+ "adversal.com"
+ ]
+ },
+ "Adverticum": {
+ "properties": [
+ "adsmart.com",
+ "adverticum.com",
+ "adverticum.net"
+ ],
+ "resources": [
+ "adsmart.com",
+ "adverticum.com",
+ "adverticum.net"
+ ]
+ },
+ "Advertise.com": {
+ "properties": [
+ "advertise.com"
+ ],
+ "resources": [
+ "advertise.com"
+ ]
+ },
+ "AdvertiseSpace": {
+ "properties": [
+ "advertisespace.com"
+ ],
+ "resources": [
+ "advertisespace.com"
+ ]
+ },
+ "Advert Stream": {
+ "properties": [
+ "advertstream.com"
+ ],
+ "resources": [
+ "advertstream.com"
+ ]
+ },
+ "Advisor Media": {
+ "properties": [
+ "advisormedia.cz"
+ ],
+ "resources": [
+ "advisormedia.cz"
+ ]
+ },
+ "Adworx": {
+ "properties": [
+ "adworx.at",
+ "adworx.be",
+ "adworx.nl"
+ ],
+ "resources": [
+ "adworx.at",
+ "adworx.be",
+ "adworx.nl"
+ ]
+ },
+ "AdXpansion": {
+ "properties": [
+ "adxpansion.com"
+ ],
+ "resources": [
+ "adxpansion.com"
+ ]
+ },
+ "Adxvalue": {
+ "properties": [
+ "adxvalue.com",
+ "adxvalue.de"
+ ],
+ "resources": [
+ "adxvalue.com",
+ "adxvalue.de"
+ ]
+ },
+ "adyard": {
+ "properties": [
+ "adyard.de"
+ ],
+ "resources": [
+ "adyard.de"
+ ]
+ },
+ "AdYield": {
+ "properties": [
+ "adyield.com"
+ ],
+ "resources": [
+ "adxyield.com",
+ "adyield.com"
+ ]
+ },
+ "AdYouLike": {
+ "properties": [
+ "adyoulike.com"
+ ],
+ "resources": [
+ "adyoulike.com",
+ "omnitagjs.com",
+ "pulpix.com"
+ ]
+ },
+ "ADZ": {
+ "properties": [
+ "adzcentral.com"
+ ],
+ "resources": [
+ "adzcentral.com"
+ ]
+ },
+ "Adzerk": {
+ "properties": [
+ "adzerk.com"
+ ],
+ "resources": [
+ "adzerk.com",
+ "adzerk.net"
+ ]
+ },
+ "adzly": {
+ "properties": [
+ "adzly.com"
+ ],
+ "resources": [
+ "adzly.com"
+ ]
+ },
+ "Aegis Group": {
+ "properties": [
+ "aemedia.com",
+ "bluestreak.com",
+ "dentsuaegisnetwork.com"
+ ],
+ "resources": [
+ "aemedia.com",
+ "bluestreak.com",
+ "dentsuaegisnetwork.com"
+ ]
+ },
+ "AERIFY MEDIA": {
+ "properties": [
+ "aerifymedia.com",
+ "anonymous-media.com"
+ ],
+ "resources": [
+ "aerifymedia.com",
+ "anonymous-media.com"
+ ]
+ },
+ "Affectv": {
+ "properties": [
+ "affectv.co.uk"
+ ],
+ "resources": [
+ "affectv.co.uk"
+ ]
+ },
+ "affilinet": {
+ "properties": [
+ "affili.net",
+ "affilinet-inside.de"
+ ],
+ "resources": [
+ "affili.net",
+ "affilinet-inside.de",
+ "banner-rotation.com",
+ "successfultogether.co.uk"
+ ]
+ },
+ "Affine": {
+ "properties": [
+ "affine.tv",
+ "affinesystems.com"
+ ],
+ "resources": [
+ "affine.tv",
+ "affinesystems.com"
+ ]
+ },
+ "Affinity": {
+ "properties": [
+ "affinity.com"
+ ],
+ "resources": [
+ "affinity.com"
+ ]
+ },
+ "AfterDownload": {
+ "properties": [
+ "afdads.com",
+ "afterdownload.com"
+ ],
+ "resources": [
+ "afdads.com",
+ "afterdownload.com"
+ ]
+ },
+ "AIData": {
+ "properties": [
+ "advombat.ru",
+ "aidata.me"
+ ],
+ "resources": [
+ "advombat.ru",
+ "aidata.me"
+ ]
+ },
+ "Aim4Media": {
+ "properties": [
+ "aim4media.com"
+ ],
+ "resources": [
+ "aim4media.com"
+ ]
+ },
+ "Airpush": {
+ "properties": [
+ "airpush.com"
+ ],
+ "resources": [
+ "airpush.com"
+ ]
+ },
+ "AivaLabs": {
+ "properties": [
+ "aivalabs.com"
+ ],
+ "resources": [
+ "aivalabs.com"
+ ]
+ },
+ "a.js": {
+ "properties": [
+ "alflying.date",
+ "alflying.win",
+ "anybest.site",
+ "flightsy.bid",
+ "flightsy.win",
+ "flightzy.bid",
+ "flightzy.date",
+ "flightzy.win",
+ "zymerget.bid",
+ "zymerget.faith"
+ ],
+ "resources": [
+ "alflying.date",
+ "alflying.win",
+ "anybest.site",
+ "flightsy.bid",
+ "flightsy.win",
+ "flightzy.bid",
+ "flightzy.date",
+ "flightzy.win",
+ "zymerget.bid",
+ "zymerget.faith"
+ ]
+ },
+ "AK": {
+ "properties": [
+ "aggregateknowledge.com",
+ "agkn.com"
+ ],
+ "resources": [
+ "aggregateknowledge.com",
+ "agkn.com"
+ ]
+ },
+ "Akamai": {
+ "properties": [
+ "akamai.com"
+ ],
+ "resources": [
+ "abmr.net",
+ "akamai.com",
+ "edgesuite.net",
+ "go-mpulse.net",
+ "imiclk.com"
+ ]
+ },
+ "AKQA": {
+ "properties": [
+ "akqa.com"
+ ],
+ "resources": [
+ "akqa.com",
+ "srtk.net"
+ ]
+ },
+ "Albacross": {
+ "properties": [
+ "albacross.com"
+ ],
+ "resources": [
+ "albacross.com"
+ ]
+ },
+ "AllStarMediaGroup": {
+ "properties": [
+ "allstarmediagroup.com"
+ ],
+ "resources": [
+ "allstarmediagroup.com"
+ ]
+ },
+ "Aloodo": {
+ "properties": [
+ "aloodo.com"
+ ],
+ "resources": [
+ "aloodo.com"
+ ]
+ },
+ "AlterGeo": {
+ "properties": [
+ "altergeo.ru"
+ ],
+ "resources": [
+ "altergeo.ru"
+ ]
+ },
+ "Amadesa": {
+ "properties": [
+ "amadesa.com"
+ ],
+ "resources": [
+ "amadesa.com"
+ ]
+ },
+ "Amazing Counters": {
+ "properties": [
+ "amazingcounters.com"
+ ],
+ "resources": [
+ "amazingcounters.com"
+ ]
+ },
+ "Amazon.com": {
+ "properties": [
+ "6pm.com",
+ "abebooks.co.uk",
+ "abebooks.com",
+ "abebooks.de",
+ "abebooks.fr",
+ "abebooks.it",
+ "acx.com",
+ "alexa.com",
+ "amazon.ae",
+ "amazon.ca",
+ "amazon.cn",
+ "amazon.co.jp",
+ "amazon.co.uk",
+ "amazon.com",
+ "amazon.com.au",
+ "amazon.com.br",
+ "amazon.com.mx",
+ "amazon.com.sg",
+ "amazon.com.tr",
+ "amazon.de",
+ "amazon.es",
+ "amazon.fr",
+ "amazon.in",
+ "amazon.it",
+ "amazon.nl",
+ "amazon.sa",
+ "amazonaws.com",
+ "amazoninspire.com",
+ "assoc-amazon.com",
+ "audible.co.jp",
+ "audible.co.uk",
+ "audible.com",
+ "audible.de",
+ "audible.fr",
+ "audible.in",
+ "audible.it",
+ "bookdepository.com",
+ "boxofficemojo.com",
+ "brilliancepublishing.com",
+ "comixology.com",
+ "createspace.com",
+ "dpreview.com",
+ "dpreview.in",
+ "eastdane.com",
+ "fabric.com",
+ "goodreads.com",
+ "iberlibro.com",
+ "imdb.com",
+ "imdb.de",
+ "junglee.com",
+ "look.com",
+ "pillpack.com",
+ "shopbop.com",
+ "souq.com",
+ "twitch.com",
+ "twitch.tv",
+ "wholefoodsmarket.com",
+ "withoutabox.com",
+ "woot.com",
+ "yoyo.com",
+ "zappos.com",
+ "zvab.com"
+ ],
+ "resources": [
+ "alexa.com",
+ "alexametrics.com",
+ "amazon-adsystem.com",
+ "amazon.ca",
+ "amazon.co.jp",
+ "amazon.co.uk",
+ "amazon.com",
+ "amazon.de",
+ "amazon.es",
+ "amazon.fr",
+ "amazon.it",
+ "amazonaws.com",
+ "assoc-amazon.com",
+ "cloudfront.net",
+ "ssl-images-amazon.com"
+ ]
+ },
+ "Ambient Digital": {
+ "properties": [
+ "adnetwork.vn",
+ "ambientdigital.com.vn"
+ ],
+ "resources": [
+ "adnetwork.vn",
+ "ambientdigital.com.vn"
+ ]
+ },
+ "Amobee": {
+ "properties": [
+ "amobee.com",
+ "smartclip.com"
+ ],
+ "resources": [
+ "adconion.com",
+ "amgdgt.com",
+ "amobee.com",
+ "euroclick.com",
+ "smartclip.com",
+ "turn.com"
+ ]
+ },
+ "Amplitude": {
+ "properties": [
+ "amplitude.com"
+ ],
+ "resources": [
+ "amplitude.com"
+ ]
+ },
+ "AndBeyond": {
+ "properties": [
+ "andbeyond.media"
+ ],
+ "resources": [
+ "andbeyond.media"
+ ]
+ },
+ "anormal-media.de": {
+ "properties": [
+ "anormal-media.de",
+ "primawebtools.de"
+ ],
+ "resources": [
+ "anormal-media.de",
+ "anormal-tracker.de",
+ "primawebtools.de"
+ ]
+ },
+ "Answers.com": {
+ "properties": [
+ "answers.com",
+ "dsply.com"
+ ],
+ "resources": [
+ "dsply.com"
+ ]
+ },
+ "AOL": {
+ "properties": [
+ "5min.com",
+ "adsonar.com",
+ "advertising.com",
+ "aim.com",
+ "aol.com",
+ "aolcdn.com",
+ "aoltechguru.com",
+ "atwola.com",
+ "autoblog.com",
+ "cambio.com",
+ "dailyfinance.com",
+ "editions.com",
+ "engadget.com",
+ "games.com",
+ "homesessive.com",
+ "huffingtonpost.com",
+ "leadback.com",
+ "makers.com",
+ "mandatory.com",
+ "mapquest.com",
+ "moviefone.com",
+ "noisecreep.com",
+ "patch.com",
+ "pawnation.com",
+ "shortcuts.com",
+ "shoutcast.com",
+ "spinner.com",
+ "stylelist.com",
+ "stylemepretty.com",
+ "surphace.com",
+ "tacoda.net",
+ "techcrunch.com",
+ "theboombox.com",
+ "theboot.com",
+ "userplane.com",
+ "winamp.com"
+ ],
+ "resources": [
+ "5min.com",
+ "adsonar.com",
+ "adtechjp.com",
+ "advertising.com",
+ "aim.com",
+ "aol.com",
+ "aolcdn.com",
+ "aolcloud.net",
+ "atwola.com",
+ "editions.com",
+ "leadback.com",
+ "mapquest.com",
+ "patch.com",
+ "shortcuts.com",
+ "shoutcast.com",
+ "spinner.com",
+ "surphace.com",
+ "tacoda.net",
+ "userplane.com",
+ "vidible.tv",
+ "winamp.com"
+ ]
+ },
+ "AppCast": {
+ "properties": [
+ "appcast.io"
+ ],
+ "resources": [
+ "appcast.io"
+ ]
+ },
+ "Appenda": {
+ "properties": [
+ "appenda.com"
+ ],
+ "resources": [
+ "appenda.com"
+ ]
+ },
+ "AppFlood": {
+ "properties": [
+ "appflood.com"
+ ],
+ "resources": [
+ "appflood.com"
+ ]
+ },
+ "Appier": {
+ "properties": [
+ "appier.com"
+ ],
+ "resources": [
+ "appier.com"
+ ]
+ },
+ "Applifier": {
+ "properties": [
+ "applifier.com"
+ ],
+ "resources": [
+ "applifier.com"
+ ]
+ },
+ "Applovin": {
+ "properties": [
+ "applovin.com"
+ ],
+ "resources": [
+ "applovin.com"
+ ]
+ },
+ "AppNexus": {
+ "properties": [
+ "adlantic.nl",
+ "adnxs.com",
+ "adrdgt.com",
+ "appnexus.com"
+ ],
+ "resources": [
+ "adlantic.nl",
+ "adnxs.com",
+ "adrdgt.com",
+ "appnexus.com"
+ ]
+ },
+ "AppsFlyer": {
+ "properties": [
+ "appsflyer.com"
+ ],
+ "resources": [
+ "appsflyer.com"
+ ]
+ },
+ "appssavvy": {
+ "properties": [
+ "appssavvy.com"
+ ],
+ "resources": [
+ "appssavvy.com"
+ ]
+ },
+ "Arkwrights Homebrew": {
+ "properties": [
+ "whiskyandwines.com"
+ ],
+ "resources": [
+ "arkwrightshomebrew.com",
+ "ctasnet.com",
+ "whiskyandwines.com"
+ ]
+ },
+ "AT Internet": {
+ "properties": [
+ "atinternet.com",
+ "xiti.com"
+ ],
+ "resources": [
+ "at-o.net",
+ "atinternet.com",
+ "hit-parade.com",
+ "xiti.com"
+ ]
+ },
+ "ATN": {
+ "properties": [
+ "affiliatetracking.com"
+ ],
+ "resources": [
+ "affiliatetracking.com"
+ ]
+ },
+ "Atoomic.com": {
+ "properties": [
+ "atoomic.com"
+ ],
+ "resources": [
+ "atoomic.com"
+ ]
+ },
+ "Atrinsic": {
+ "properties": [
+ "atrinsic.com"
+ ],
+ "resources": [
+ "atrinsic.com"
+ ]
+ },
+ "AT&T": {
+ "properties": [
+ "att.com",
+ "yp.com"
+ ],
+ "resources": [
+ "att.com",
+ "yp.com"
+ ]
+ },
+ "Attracta": {
+ "properties": [
+ "attracta.com"
+ ],
+ "resources": [
+ "attracta.com"
+ ]
+ },
+ "Audience2Media": {
+ "properties": [
+ "audience2media.com"
+ ],
+ "resources": [
+ "audience2media.com"
+ ]
+ },
+ "Audience Ad Network": {
+ "properties": [
+ "audienceadnetwork.com"
+ ],
+ "resources": [
+ "audienceadnetwork.com"
+ ]
+ },
+ "AudienceScience": {
+ "properties": [
+ "audiencescience.com"
+ ],
+ "resources": [
+ "audiencescience.com",
+ "revsci.net",
+ "targetingmarketplace.com",
+ "wunderloop.net"
+ ]
+ },
+ "AuditedMedia": {
+ "properties": [
+ "auditedmedia.com"
+ ],
+ "resources": [
+ "aamapi.com",
+ "aamsitecertifier.com",
+ "auditedmedia.com"
+ ]
+ },
+ "Augme": {
+ "properties": [
+ "hipcricket.com"
+ ],
+ "resources": [
+ "augme.com",
+ "hipcricket.com"
+ ]
+ },
+ "Augur": {
+ "properties": [
+ "augur.io"
+ ],
+ "resources": [
+ "augur.io"
+ ]
+ },
+ "AUTOCENTRE.UA": {
+ "properties": [
+ "am.ua",
+ "autocentre.ua"
+ ],
+ "resources": [
+ "am.ua",
+ "autocentre.ua"
+ ]
+ },
+ "Automattic": {
+ "properties": [
+ "automattic.com",
+ "gravatar.com",
+ "intensedebate.com",
+ "polldaddy.com"
+ ],
+ "resources": [
+ "automattic.com",
+ "gravatar.com",
+ "intensedebate.com",
+ "polldaddy.com",
+ "pubmine.com"
+ ]
+ },
+ "Avalanchers": {
+ "properties": [
+ "avalanchers.com"
+ ],
+ "resources": [
+ "avalanchers.com"
+ ]
+ },
+ "AvantLink": {
+ "properties": [
+ "avantlink.com",
+ "avantmetrics.com"
+ ],
+ "resources": [
+ "avantlink.com",
+ "avmws.com"
+ ]
+ },
+ "Avocet": {
+ "properties": [
+ "avocet.io"
+ ],
+ "resources": [
+ "avocet.io"
+ ]
+ },
+ "Avsads": {
+ "properties": [
+ "avsads.com"
+ ],
+ "resources": [
+ "avsads.com"
+ ]
+ },
+ "AWeber": {
+ "properties": [
+ "aweber.com"
+ ],
+ "resources": [
+ "aweber.com"
+ ]
+ },
+ "Awin": {
+ "properties": [
+ "awin.com"
+ ],
+ "resources": [
+ "awin.com",
+ "digitalwindow.com",
+ "dwin1.com",
+ "perfiliate.com"
+ ]
+ },
+ "Awio": {
+ "properties": [
+ "awio.com",
+ "w3counter.com"
+ ],
+ "resources": [
+ "awio.com",
+ "w3counter.com",
+ "w3roi.com"
+ ]
+ },
+ "Azet": {
+ "properties": [
+ "azet.sk",
+ "mediaimpact.sk"
+ ],
+ "resources": [
+ "azet.sk",
+ "azetklik.sk",
+ "mediaimpact.sk",
+ "rsz.sk"
+ ]
+ },
+ "BackBeat Media": {
+ "properties": [
+ "backbeatmedia.com"
+ ],
+ "resources": [
+ "backbeatmedia.com"
+ ]
+ },
+ "Bannerconnect": {
+ "properties": [
+ "bannerconnect.net"
+ ],
+ "resources": [
+ "bannerconnect.net"
+ ]
+ },
+ "Barilliance": {
+ "properties": [
+ "barilliance.com"
+ ],
+ "resources": [
+ "barilliance.com"
+ ]
+ },
+ "BaronsNetworks": {
+ "properties": [
+ "baronsoffers.com"
+ ],
+ "resources": [
+ "baronsoffers.com"
+ ]
+ },
+ "Batanga Network": {
+ "properties": [
+ "batanga.com",
+ "corp.vix.com",
+ "vix.com"
+ ],
+ "resources": [
+ "batanga.com",
+ "batanganetwork.com",
+ "vix.com"
+ ]
+ },
+ "Baynote": {
+ "properties": [
+ "baynote.com"
+ ],
+ "resources": [
+ "baynote.com",
+ "baynote.net"
+ ]
+ },
+ "Bazaarvoice": {
+ "properties": [
+ "bazaarvoice.com"
+ ],
+ "resources": [
+ "bazaarvoice.com"
+ ]
+ },
+ "BeachFront": {
+ "properties": [
+ "beachfront.com"
+ ],
+ "resources": [
+ "beachfront.com"
+ ]
+ },
+ "Beanstock Media": {
+ "properties": [
+ "beanstockmedia.com"
+ ],
+ "resources": [
+ "beanstockmedia.com"
+ ]
+ },
+ "beencounter": {
+ "properties": [
+ "beencounter.com"
+ ],
+ "resources": [
+ "beencounter.com"
+ ]
+ },
+ "Begun": {
+ "properties": [
+ "begun.ru"
+ ],
+ "resources": [
+ "begun.ru"
+ ]
+ },
+ "belboon": {
+ "properties": [
+ "belboon.com"
+ ],
+ "resources": [
+ "adbutler.de",
+ "belboon.com"
+ ]
+ },
+ "Belstat": {
+ "properties": [
+ "belstat.be",
+ "belstat.com",
+ "belstat.de",
+ "belstat.fr",
+ "belstat.nl"
+ ],
+ "resources": [
+ "belstat.be",
+ "belstat.com",
+ "belstat.de",
+ "belstat.fr",
+ "belstat.nl"
+ ]
+ },
+ "Betgenius": {
+ "properties": [
+ "betgenius.com",
+ "connextra.com"
+ ],
+ "resources": [
+ "betgenius.com",
+ "connextra.com"
+ ]
+ },
+ "BetssonPalantir": {
+ "properties": [
+ "betssonpalantir.com"
+ ],
+ "resources": [
+ "betssonpalantir.com"
+ ]
+ },
+ "BetweenDigital": {
+ "properties": [
+ "betweendigital.com"
+ ],
+ "resources": [
+ "betweendigital.com"
+ ]
+ },
+ "Bidfluence": {
+ "properties": [
+ "bidfluence.com"
+ ],
+ "resources": [
+ "bidfluence.com"
+ ]
+ },
+ "Bidr": {
+ "properties": [
+ "bidr.io"
+ ],
+ "resources": [
+ "bidr.io"
+ ]
+ },
+ "BidSwitch": {
+ "properties": [
+ "bidswitch.com"
+ ],
+ "resources": [
+ "bidswitch.net",
+ "mfadsrvr.com"
+ ]
+ },
+ "Bidtellect": {
+ "properties": [
+ "bidtellect.com",
+ "bttrack.com"
+ ],
+ "resources": [
+ "bidtellect.com",
+ "bttrack.com"
+ ]
+ },
+ "BidVertiser": {
+ "properties": [
+ "bidvertiser.com"
+ ],
+ "resources": [
+ "bidvertiser.com"
+ ]
+ },
+ "BigClick": {
+ "properties": [
+ "bigclick.me"
+ ],
+ "resources": [
+ "bgclck.me",
+ "xcvgdf.party"
+ ]
+ },
+ "BigDoor": {
+ "properties": [
+ "bigdoor.com"
+ ],
+ "resources": [
+ "bigdoor.com",
+ "onetruefan.com"
+ ]
+ },
+ "bigmirnet": {
+ "properties": [
+ "bigmir.net"
+ ],
+ "resources": [
+ "bigmir.net"
+ ]
+ },
+ "BinLayer": {
+ "properties": [
+ "binlayer.com"
+ ],
+ "resources": [
+ "binlayer.com"
+ ]
+ },
+ "Bitcoin Plus": {
+ "properties": [
+ "bitcoinplus.com"
+ ],
+ "resources": [
+ "bitcoinplus.com"
+ ]
+ },
+ "BitMedia": {
+ "properties": [
+ "bitmedia.io"
+ ],
+ "resources": [
+ "bitmedia.io"
+ ]
+ },
+ "BittAds": {
+ "properties": [
+ "bittads.com"
+ ],
+ "resources": [
+ "bittads.com"
+ ]
+ },
+ "Bizo": {
+ "properties": [
+ "bizo.com",
+ "bizographics.com"
+ ],
+ "resources": [
+ "bizo.com",
+ "bizographics.com"
+ ]
+ },
+ "Black Label Ads": {
+ "properties": [
+ "blacklabelads.com"
+ ],
+ "resources": [
+ "blacklabelads.com"
+ ]
+ },
+ "BlogCatalog": {
+ "properties": [
+ "blogcatalog.com"
+ ],
+ "resources": [
+ "blogcatalog.com"
+ ]
+ },
+ "BlogCounter.com": {
+ "properties": [
+ "blogcounter.de"
+ ],
+ "resources": [
+ "blogcounter.de"
+ ]
+ },
+ "BlogFrog": {
+ "properties": [
+ "theblogfrog.com"
+ ],
+ "resources": [
+ "theblogfrog.com"
+ ]
+ },
+ "BlogHer": {
+ "properties": [
+ "blogher.com",
+ "blogherads.com"
+ ],
+ "resources": [
+ "blogher.com",
+ "blogherads.com"
+ ]
+ },
+ "BlogRollr": {
+ "properties": [
+ "blogrollr.com"
+ ],
+ "resources": [
+ "blogrollr.com"
+ ]
+ },
+ "BLOOM Digital Platforms": {
+ "properties": [
+ "adgear.com",
+ "bloom-hq.com"
+ ],
+ "resources": [
+ "adgear.com",
+ "adgrx.com",
+ "bloom-hq.com"
+ ]
+ },
+ "BloomReach": {
+ "properties": [
+ "bloomreach.com",
+ "brcdn.com"
+ ],
+ "resources": [
+ "bloomreach.com",
+ "brcdn.com",
+ "brsrvr.com"
+ ]
+ },
+ "BlueCava": {
+ "properties": [
+ "bluecava.com"
+ ],
+ "resources": [
+ "bluecava.com"
+ ]
+ },
+ "BlueKai": {
+ "properties": [
+ "bluekai.com",
+ "tracksimple.com"
+ ],
+ "resources": [
+ "bkrtx.com",
+ "bluekai.com",
+ "tracksimple.com"
+ ]
+ },
+ "Bluemetrix": {
+ "properties": [
+ "bluemetrix.com",
+ "bmmetrix.com"
+ ],
+ "resources": [
+ "bluemetrix.com",
+ "bmmetrix.com"
+ ]
+ },
+ "Blu Trumpet": {
+ "properties": [
+ "blutrumpet.com"
+ ],
+ "resources": [
+ "blutrumpet.com"
+ ]
+ },
+ "Bombora": {
+ "properties": [
+ "bombora.com"
+ ],
+ "resources": [
+ "ml314.com"
+ ]
+ },
+ "Boo-Box": {
+ "properties": [
+ "boo-box.com"
+ ],
+ "resources": [
+ "boo-box.com"
+ ]
+ },
+ "BoostBox": {
+ "properties": [
+ "boostbox.com.br"
+ ],
+ "resources": [
+ "boostbox.com.br"
+ ]
+ },
+ "Bouncex": {
+ "properties": [
+ "bouncex.com"
+ ],
+ "resources": [
+ "bounceexchange.com",
+ "bouncex.com",
+ "bouncex.net"
+ ]
+ },
+ "Brainient": {
+ "properties": [
+ "brainient.com"
+ ],
+ "resources": [
+ "brainient.com"
+ ]
+ },
+ "Branch": {
+ "properties": [
+ "branch.io"
+ ],
+ "resources": [
+ "branch.io"
+ ]
+ },
+ "Brand Affinity Technologies": {
+ "properties": [
+ "brandaffinity.net"
+ ],
+ "resources": [
+ "brandaffinity.net"
+ ]
+ },
+ "Brandcrumb": {
+ "properties": [
+ "brandcrumb.com"
+ ],
+ "resources": [
+ "brandcrumb.com"
+ ]
+ },
+ "Brand.net": {
+ "properties": [
+ "brand.net"
+ ],
+ "resources": [
+ "brand.net"
+ ]
+ },
+ "Brandscreen": {
+ "properties": [
+ "brandscreen.com",
+ "rtbidder.net"
+ ],
+ "resources": [
+ "brandscreen.com",
+ "rtbidder.net"
+ ]
+ },
+ "Branica": {
+ "properties": [
+ "branica.com"
+ ],
+ "resources": [
+ "branica.com"
+ ]
+ },
+ "BreakTime": {
+ "properties": [
+ "breaktime.com.tw"
+ ],
+ "resources": [
+ "breaktime.com.tw"
+ ]
+ },
+ "Brightcove": {
+ "properties": [
+ "brightcove.com"
+ ],
+ "resources": [
+ "brightcove.com"
+ ]
+ },
+ "BrightEdge": {
+ "properties": [
+ "brightedge.com"
+ ],
+ "resources": [
+ "b0e8.com",
+ "brightedge.com"
+ ]
+ },
+ "BrightRoll": {
+ "properties": [
+ "brightroll.com"
+ ],
+ "resources": [
+ "brightroll.com",
+ "btrll.com"
+ ]
+ },
+ "BrightTag": {
+ "properties": [
+ "brighttag.com",
+ "btstatic.com",
+ "thebrighttag.com"
+ ],
+ "resources": [
+ "brighttag.com",
+ "btstatic.com",
+ "thebrighttag.com"
+ ]
+ },
+ "Brilig": {
+ "properties": [
+ "brilig.com"
+ ],
+ "resources": [
+ "brilig.com"
+ ]
+ },
+ "Browser-Update.org": {
+ "properties": [
+ "browser-update.org"
+ ],
+ "resources": [
+ "browser-update.org"
+ ]
+ },
+ "BTBuckets": {
+ "properties": [
+ "btbuckets.com"
+ ],
+ "resources": [
+ "btbuckets.com"
+ ]
+ },
+ "Bubblestat": {
+ "properties": [
+ "bubblestat.com"
+ ],
+ "resources": [
+ "bubblestat.com"
+ ]
+ },
+ "BuckSense": {
+ "properties": [
+ "bucksense.com"
+ ],
+ "resources": [
+ "bucksense.com"
+ ]
+ },
+ "Buffer": {
+ "properties": [
+ "bufferapp.com"
+ ],
+ "resources": [
+ "bufferapp.com"
+ ]
+ },
+ "Bunchball": {
+ "properties": [
+ "bunchball.com"
+ ],
+ "resources": [
+ "bunchball.com"
+ ]
+ },
+ "Burstly": {
+ "properties": [
+ "burstly.com"
+ ],
+ "resources": [
+ "burstly.com"
+ ]
+ },
+ "Burst Media": {
+ "properties": [
+ "burstbeacon.com",
+ "burstdirectads.com",
+ "burstmedia.com",
+ "burstnet.com",
+ "giantrealm.com"
+ ],
+ "resources": [
+ "burstbeacon.com",
+ "burstdirectads.com",
+ "burstmedia.com",
+ "burstnet.com",
+ "giantrealm.com"
+ ]
+ },
+ "BusinessOnline": {
+ "properties": [
+ "businessol.com"
+ ],
+ "resources": [
+ "businessol.com"
+ ]
+ },
+ "Button": {
+ "properties": [
+ "usebutton.com"
+ ],
+ "resources": [
+ "usebutton.com"
+ ]
+ },
+ "buySAFE": {
+ "properties": [
+ "buysafe.com"
+ ],
+ "resources": [
+ "buysafe.com"
+ ]
+ },
+ "BuySellAds": {
+ "properties": [
+ "beaconads.com",
+ "buysellads.com"
+ ],
+ "resources": [
+ "beaconads.com",
+ "buysellads.com"
+ ]
+ },
+ "Buysight": {
+ "properties": [
+ "buysight.com",
+ "permuto.com",
+ "pulsemgr.com"
+ ],
+ "resources": [
+ "buysight.com",
+ "permuto.com",
+ "pulsemgr.com"
+ ]
+ },
+ "BuzzFeed": {
+ "properties": [
+ "buzzfeed.com"
+ ],
+ "resources": [
+ "buzzfed.com",
+ "buzzfeed.com"
+ ]
+ },
+ "BuzzParadise": {
+ "properties": [
+ "buzzparadise.com"
+ ],
+ "resources": [
+ "buzzparadise.com"
+ ]
+ },
+ "BV! MEDIA": {
+ "properties": [
+ "branchez-vous.com",
+ "bvmedia.ca"
+ ],
+ "resources": [
+ "branchez-vous.com",
+ "bvmedia.ca",
+ "networldmedia.com",
+ "networldmedia.net"
+ ]
+ },
+ "c1exchange": {
+ "properties": [
+ "c1exchange.com"
+ ],
+ "resources": [
+ "c1exchange.com"
+ ]
+ },
+ "C3 Metrics": {
+ "properties": [
+ "attributionmodel.com",
+ "c3metrics.com",
+ "c3tag.com"
+ ],
+ "resources": [
+ "attributionmodel.com",
+ "c3metrics.com",
+ "c3tag.com"
+ ]
+ },
+ "Cadreon": {
+ "properties": [
+ "cadreon.com"
+ ],
+ "resources": [
+ "cadreon.com"
+ ]
+ },
+ "CallSource": {
+ "properties": [
+ "callsource.com"
+ ],
+ "resources": [
+ "leadtrackingdata.com"
+ ]
+ },
+ "CampaignGrid": {
+ "properties": [
+ "campaigngrid.com"
+ ],
+ "resources": [
+ "campaigngrid.com"
+ ]
+ },
+ "CAPITALDATA": {
+ "properties": [
+ "capitaldata.fr"
+ ],
+ "resources": [
+ "capitaldata.fr"
+ ]
+ },
+ "Carambola": {
+ "properties": [
+ "carambola.com"
+ ],
+ "resources": [
+ "carambo.la"
+ ]
+ },
+ "Caraytech": {
+ "properties": [
+ "caraytech.com.ar",
+ "e-planning.net",
+ "www.caraytech.com.ar"
+ ],
+ "resources": [
+ "caraytech.com.ar",
+ "e-planning.net",
+ "www.caraytech.com.ar"
+ ]
+ },
+ "Cardlytics": {
+ "properties": [
+ "cardlytics.com"
+ ],
+ "resources": [
+ "cardlytics.com"
+ ]
+ },
+ "Cart.ro": {
+ "properties": [
+ "cart.ro"
+ ],
+ "resources": [
+ "cart.ro",
+ "statistics.ro"
+ ]
+ },
+ "CartsGuru": {
+ "properties": [
+ "carts.guru"
+ ],
+ "resources": [
+ "carts.guru"
+ ]
+ },
+ "Casale Media": {
+ "properties": [
+ "casalemedia.com",
+ "medianet.com"
+ ],
+ "resources": [
+ "casalemedia.com",
+ "medianet.com"
+ ]
+ },
+ "CashBeet": {
+ "properties": [
+ "cashbeet.com"
+ ],
+ "resources": [
+ "cashbeet.com",
+ "serv1swork.com"
+ ]
+ },
+ "Causes": {
+ "properties": [
+ "causes.com"
+ ],
+ "resources": [
+ "causes.com"
+ ]
+ },
+ "Cbox": {
+ "properties": [
+ "cbox.ws"
+ ],
+ "resources": [
+ "cbox.ws"
+ ]
+ },
+ "CBproADS": {
+ "properties": [
+ "cbproads.com"
+ ],
+ "resources": [
+ "cbproads.com"
+ ]
+ },
+ "CBS Interactive": {
+ "properties": [
+ "cbsinteractive.com",
+ "com.com"
+ ],
+ "resources": [
+ "cbsinteractive.com",
+ "com.com"
+ ]
+ },
+ "Cedato": {
+ "properties": [
+ "cedato.com"
+ ],
+ "resources": [
+ "cedato.com"
+ ]
+ },
+ "Cedexis": {
+ "properties": [
+ "cedexis.com"
+ ],
+ "resources": [
+ "cedexis.com",
+ "cedexis.net"
+ ]
+ },
+ "Certona": {
+ "properties": [
+ "certona.com",
+ "res-x.com"
+ ],
+ "resources": [
+ "certona.com",
+ "res-x.com"
+ ]
+ },
+ "Chango": {
+ "properties": [
+ "chango.ca",
+ "chango.com"
+ ],
+ "resources": [
+ "chango.ca",
+ "chango.com"
+ ]
+ },
+ "ChannelAdvisor": {
+ "properties": [
+ "channeladvisor.com",
+ "searchmarketing.com"
+ ],
+ "resources": [
+ "channeladvisor.com",
+ "searchmarketing.com"
+ ]
+ },
+ "Channel Intelligence": {
+ "properties": [
+ "channelintelligence.com"
+ ],
+ "resources": [
+ "channelintelligence.com"
+ ]
+ },
+ "Chartbeat": {
+ "properties": [
+ "chartbeat.com",
+ "chartbeat.net"
+ ],
+ "resources": [
+ "chartbeat.com",
+ "chartbeat.net"
+ ]
+ },
+ "Chartboost": {
+ "properties": [
+ "chartboost.com"
+ ],
+ "resources": [
+ "chartboost.com"
+ ]
+ },
+ "CheckM8": {
+ "properties": [
+ "checkm8.com"
+ ],
+ "resources": [
+ "checkm8.com"
+ ]
+ },
+ "Chitika": {
+ "properties": [
+ "chitika.com"
+ ],
+ "resources": [
+ "chitika.com",
+ "chitika.net"
+ ]
+ },
+ "ChoiceStream": {
+ "properties": [
+ "choicestream.com"
+ ],
+ "resources": [
+ "choicestream.com"
+ ]
+ },
+ "ClearLink": {
+ "properties": [
+ "clearlink.com"
+ ],
+ "resources": [
+ "clearlink.com"
+ ]
+ },
+ "ClearSaleing": {
+ "properties": [
+ "clearsaleing.com"
+ ],
+ "resources": [
+ "clearsaleing.com",
+ "csdata1.com",
+ "csdata2.com",
+ "csdata3.com"
+ ]
+ },
+ "Clearsearch Media": {
+ "properties": [
+ "pathinteractive.com"
+ ],
+ "resources": [
+ "clearsearchmedia.com",
+ "csm-secure.com",
+ "pathinteractive.com"
+ ]
+ },
+ "ClearSight Interactive": {
+ "properties": [
+ "clearsightinteractive.com",
+ "csi-tracking.com"
+ ],
+ "resources": [
+ "clearsightinteractive.com",
+ "csi-tracking.com"
+ ]
+ },
+ "ClickAider": {
+ "properties": [
+ "clickaider.com"
+ ],
+ "resources": [
+ "clickaider.com"
+ ]
+ },
+ "Clickayab": {
+ "properties": [
+ "clickyab.com"
+ ],
+ "resources": [
+ "clickyab.com"
+ ]
+ },
+ "Clickbooth": {
+ "properties": [
+ "clickbooth.com"
+ ],
+ "resources": [
+ "adtoll.com",
+ "clickbooth.com"
+ ]
+ },
+ "Clickdensity": {
+ "properties": [
+ "clickdensity.com"
+ ],
+ "resources": [
+ "clickdensity.com"
+ ]
+ },
+ "ClickDimensions": {
+ "properties": [
+ "clickdimensions.com"
+ ],
+ "resources": [
+ "clickdimensions.com"
+ ]
+ },
+ "ClickDistrict": {
+ "properties": [
+ "clickdistrict.com",
+ "creative-serving.com"
+ ],
+ "resources": [
+ "clickdistrict.com",
+ "creative-serving.com"
+ ]
+ },
+ "ClickFrog": {
+ "properties": [
+ "clickfrog.ru"
+ ],
+ "resources": [
+ "bashirian.biz",
+ "buckridge.link",
+ "clickfrog.ru",
+ "franecki.net",
+ "quitzon.net",
+ "reichelcormier.bid",
+ "wisokykulas.bid"
+ ]
+ },
+ "ClickFuel": {
+ "properties": [
+ "clickfuel.com",
+ "myconversionlab.com"
+ ],
+ "resources": [
+ "clickfuel.com",
+ "conversiondashboard.com",
+ "myconversionlab.com"
+ ]
+ },
+ "ClickGuard": {
+ "properties": [
+ "clickguard.com"
+ ],
+ "resources": [
+ "clickguard.com"
+ ]
+ },
+ "ClickInc": {
+ "properties": [
+ "clickinc.com"
+ ],
+ "resources": [
+ "clickinc.com"
+ ]
+ },
+ "Clicksor": {
+ "properties": [
+ "clicksor.com",
+ "clicksor.net"
+ ],
+ "resources": [
+ "clicksor.com",
+ "clicksor.net"
+ ]
+ },
+ "ClickTale": {
+ "properties": [
+ "clicktale.com"
+ ],
+ "resources": [
+ "clicktale.com",
+ "clicktale.net",
+ "pantherssl.com"
+ ]
+ },
+ "Clickwinks": {
+ "properties": [
+ "clickwinks.com"
+ ],
+ "resources": [
+ "clickwinks.com"
+ ]
+ },
+ "ClicManager": {
+ "properties": [
+ "clicmanager.fr"
+ ],
+ "resources": [
+ "clicmanager.fr"
+ ]
+ },
+ "ClipSyndicate": {
+ "properties": [
+ "clipsyndicate.com"
+ ],
+ "resources": [
+ "clipsyndicate.com"
+ ]
+ },
+ "ClixMetrix": {
+ "properties": [
+ "clixmetrix.com"
+ ],
+ "resources": [
+ "clixmetrix.com"
+ ]
+ },
+ "Clixpy": {
+ "properties": [
+ "clixpy.com"
+ ],
+ "resources": [
+ "clixpy.com"
+ ]
+ },
+ "Clixtell": {
+ "properties": [
+ "clixtell.com"
+ ],
+ "resources": [
+ "clixtell.com"
+ ]
+ },
+ "Clove Network": {
+ "properties": [
+ "clovenetwork.com"
+ ],
+ "resources": [
+ "clovenetwork.com"
+ ]
+ },
+ "ClustrMaps": {
+ "properties": [
+ "clustrmaps.com"
+ ],
+ "resources": [
+ "clustrmaps.com"
+ ]
+ },
+ "CNZZ": {
+ "properties": [
+ "cnzz.com"
+ ],
+ "resources": [
+ "cnzz.com"
+ ]
+ },
+ "Cognitive Match": {
+ "properties": [
+ "cmads.com.tw",
+ "cmadsasia.com",
+ "cmadseu.com",
+ "cmmeglobal.com",
+ "cognitivematch.com"
+ ],
+ "resources": [
+ "cmads.com.tw",
+ "cmadsasia.com",
+ "cmadseu.com",
+ "cmmeglobal.com",
+ "cognitivematch.com"
+ ]
+ },
+ "CoinHive": {
+ "properties": [
+ "authedmine.com",
+ "coinhive.com"
+ ],
+ "resources": [
+ "ad-miner.com",
+ "authedmine.com",
+ "bmst.pw",
+ "cnhv.co",
+ "coin-hive.com",
+ "coinhive.com",
+ "wsservices.org"
+ ]
+ },
+ "CoinPot": {
+ "properties": [
+ "coinpot.co"
+ ],
+ "resources": [
+ "coinpot.co"
+ ]
+ },
+ "Collarity": {
+ "properties": [
+ "collarity.com"
+ ],
+ "resources": [
+ "collarity.com"
+ ]
+ },
+ "Collective": {
+ "properties": [
+ "collective.com"
+ ],
+ "resources": [
+ "collective-media.net",
+ "collective.com",
+ "oggifinogi.com",
+ "tumri.com",
+ "tumri.net",
+ "yt1187.net"
+ ]
+ },
+ "Commission Junction": {
+ "properties": [
+ "cj.com"
+ ],
+ "resources": [
+ "apmebf.com",
+ "awltovhc.com",
+ "cj.com",
+ "ftjcfx.com",
+ "kcdwa.com",
+ "qksz.com",
+ "qksz.net",
+ "tqlkg.com",
+ "yceml.net"
+ ]
+ },
+ "Communicator Corp": {
+ "properties": [
+ "communicatorcorp.com"
+ ],
+ "resources": [
+ "communicatorcorp.com"
+ ]
+ },
+ "Compass Labs": {
+ "properties": [
+ "compasslabs.com"
+ ],
+ "resources": [
+ "compasslabs.com"
+ ]
+ },
+ "Complex Media": {
+ "properties": [
+ "collider.com",
+ "complex.com",
+ "complexmedianetwork.com",
+ "firstwefeast.com",
+ "pigeonsandplanes.com",
+ "solecollector.com",
+ "theridechannel.com"
+ ],
+ "resources": [
+ "complex.com",
+ "complexmedianetwork.com"
+ ]
+ },
+ "Compuware": {
+ "properties": [
+ "axf8.net",
+ "compuware.com",
+ "dynatrace.com"
+ ],
+ "resources": [
+ "axf8.net",
+ "compuware.com",
+ "dynatrace.com",
+ "gomez.com"
+ ]
+ },
+ "comScore": {
+ "properties": [
+ "adxpose.com",
+ "comscore.com",
+ "scorecardresearch.com",
+ "sitestat.com",
+ "voicefive.com"
+ ],
+ "resources": [
+ "adxpose.com",
+ "certifica.com",
+ "comscore.com",
+ "mdotlabs.com",
+ "proxilinks.com",
+ "proximic.com",
+ "proximic.net",
+ "scorecardresearch.com",
+ "sitestat.com",
+ "voicefive.com"
+ ]
+ },
+ "Conduit": {
+ "properties": [
+ "conduit-banners.com",
+ "conduit.com"
+ ],
+ "resources": [
+ "conduit-banners.com",
+ "conduit-services.com",
+ "conduit.com",
+ "wibiya.com"
+ ]
+ },
+ "Congoo": {
+ "properties": [
+ "congoo.com"
+ ],
+ "resources": [
+ "congoo.com"
+ ]
+ },
+ "Connatix.com": {
+ "properties": [
+ "connatix.com"
+ ],
+ "resources": [
+ "connatix.com"
+ ]
+ },
+ "Connexity": {
+ "properties": [
+ "connexity.com",
+ "pricegrabber.com"
+ ],
+ "resources": [
+ "connexity.com",
+ "connexity.net",
+ "pricegrabber.com"
+ ]
+ },
+ "Consilium Media": {
+ "properties": [
+ "consiliummedia.com"
+ ],
+ "resources": [
+ "consiliummedia.com"
+ ]
+ },
+ "Consumable": {
+ "properties": [
+ "consumable.com"
+ ],
+ "resources": [
+ "consumable.com"
+ ]
+ },
+ "Contact At Once!": {
+ "properties": [
+ "contactatonce.com"
+ ],
+ "resources": [
+ "contactatonce.com"
+ ]
+ },
+ "CONTAXE": {
+ "properties": [
+ "contaxe.com"
+ ],
+ "resources": [
+ "contaxe.com"
+ ]
+ },
+ "ContentABC": {
+ "properties": [
+ "contentabc.com"
+ ],
+ "resources": [
+ "contentabc.com"
+ ]
+ },
+ "CONTEXTin": {
+ "properties": [
+ "admailtiser.com",
+ "contextin.com"
+ ],
+ "resources": [
+ "admailtiser.com",
+ "contextin.com"
+ ]
+ },
+ "ContextuAds": {
+ "properties": [
+ "agencytradingdesk.net",
+ "contextuads.com"
+ ],
+ "resources": [
+ "agencytradingdesk.net",
+ "contextuads.com"
+ ]
+ },
+ "CONTEXTWEB": {
+ "properties": [
+ "contextweb.com"
+ ],
+ "resources": [
+ "contextweb.com"
+ ]
+ },
+ "ConvergeDirect": {
+ "properties": [
+ "convergedirect.com",
+ "convergetrack.com"
+ ],
+ "resources": [
+ "convergedirect.com",
+ "convergetrack.com"
+ ]
+ },
+ "ConversantMedia": {
+ "properties": [
+ "conversantmedia.com"
+ ],
+ "resources": [
+ "adserver.com",
+ "conversantmedia.com",
+ "dotomi.com",
+ "dtmpub.com",
+ "emjcd.com",
+ "fastclick.com",
+ "fastclick.net",
+ "greystripe.com",
+ "lduhtrp.net",
+ "mediaplex.com",
+ "valueclick.com",
+ "valueclick.net",
+ "valueclickmedia.com"
+ ]
+ },
+ "ConversionRuler": {
+ "properties": [
+ "conversionruler.com"
+ ],
+ "resources": [
+ "conversionruler.com"
+ ]
+ },
+ "Conversive": {
+ "properties": [
+ "conversive.nl"
+ ],
+ "resources": [
+ "conversive.nl"
+ ]
+ },
+ "Convert Insights": {
+ "properties": [
+ "convert.com",
+ "reedge.com"
+ ],
+ "resources": [
+ "convert.com",
+ "reedge.com"
+ ]
+ },
+ "Convertro": {
+ "properties": [
+ "convertro.com"
+ ],
+ "resources": [
+ "convertro.com"
+ ]
+ },
+ "Conviva": {
+ "properties": [
+ "conviva.com"
+ ],
+ "resources": [
+ "conviva.com"
+ ]
+ },
+ "CoreMotives": {
+ "properties": [
+ "coremotives.com"
+ ],
+ "resources": [
+ "coremotives.com"
+ ]
+ },
+ "Cox Digital Solutions": {
+ "properties": [
+ "adify.com",
+ "coxdigitalsolutions.com",
+ "novomotus.com"
+ ],
+ "resources": [
+ "adify.com",
+ "afy11.net",
+ "coxdigitalsolutions.com",
+ "novomotus.com"
+ ]
+ },
+ "CPMStar": {
+ "properties": [
+ "cpmstar.com"
+ ],
+ "resources": [
+ "cpmstar.com"
+ ]
+ },
+ "CPX Interactive": {
+ "properties": [
+ "cpxadroit.com"
+ ],
+ "resources": [
+ "adreadypixels.com",
+ "cpxadroit.com",
+ "cpxinteractive.com"
+ ]
+ },
+ "Crazy Egg": {
+ "properties": [
+ "cetrk.com",
+ "crazyegg.com"
+ ],
+ "resources": [
+ "cetrk.com",
+ "crazyegg.com"
+ ]
+ },
+ "Creafi": {
+ "properties": [
+ "creafi.com"
+ ],
+ "resources": [
+ "creafi.com"
+ ]
+ },
+ "Crimtan": {
+ "properties": [
+ "crimtan.com"
+ ],
+ "resources": [
+ "crimtan.com"
+ ]
+ },
+ "Crisp Media": {
+ "properties": [
+ "crispmedia.com"
+ ],
+ "resources": [
+ "crispmedia.com"
+ ]
+ },
+ "Criteo": {
+ "properties": [
+ "criteo.com",
+ "criteo.net"
+ ],
+ "resources": [
+ "criteo.com",
+ "criteo.net",
+ "hlserve.com",
+ "hooklogic.com",
+ "storetail.io"
+ ]
+ },
+ "Cross Pixel": {
+ "properties": [
+ "crosspixel.net"
+ ],
+ "resources": [
+ "crosspixel.net",
+ "crosspixelmedia.com",
+ "crsspxl.com"
+ ]
+ },
+ "Crowd Science": {
+ "properties": [
+ "crowdscience.com"
+ ],
+ "resources": [
+ "crowdscience.com"
+ ]
+ },
+ "CryptoLoot": {
+ "properties": [
+ "crypto-loot.com"
+ ],
+ "resources": [
+ "cryptaloot.pro",
+ "crypto-loot.com",
+ "cryptolootminer.com",
+ "flashx.pw",
+ "gitgrub.pro",
+ "reauthenticator.com",
+ "statdynamic.com",
+ "webmine.pro"
+ ]
+ },
+ "CryptoWebMiner": {
+ "properties": [
+ "crypto-webminer.com"
+ ],
+ "resources": [
+ "bitcoin-pay.eu",
+ "crypto-webminer.com",
+ "ethpocket.de",
+ "ethtrader.de"
+ ]
+ },
+ "cXense": {
+ "properties": [
+ "cxense.com"
+ ],
+ "resources": [
+ "cxense.com",
+ "emediate.biz",
+ "emediate.com",
+ "emediate.dk",
+ "emediate.eu"
+ ]
+ },
+ "Cya2": {
+ "properties": [
+ "cya2.net"
+ ],
+ "resources": [
+ "cya2.net"
+ ]
+ },
+ "Cyberplex": {
+ "properties": [
+ "cyberplex.com"
+ ],
+ "resources": [
+ "cyberplex.com"
+ ]
+ },
+ "Dada": {
+ "properties": [
+ "dada.eu",
+ "dada.pro",
+ "simply.com"
+ ],
+ "resources": [
+ "dada.eu",
+ "dada.pro",
+ "simply.com"
+ ]
+ },
+ "DailyMe": {
+ "properties": [
+ "dailyme.com",
+ "newstogram.com"
+ ],
+ "resources": [
+ "dailyme.com",
+ "newstogram.com"
+ ]
+ },
+ "Dataium": {
+ "properties": [
+ "collserve.com",
+ "ihs.com"
+ ],
+ "resources": [
+ "collserve.com",
+ "dataium.com",
+ "ihs.com"
+ ]
+ },
+ "Datalogix": {
+ "properties": [
+ "datalogix.com",
+ "nexac.com"
+ ],
+ "resources": [
+ "datalogix.com",
+ "nexac.com",
+ "nextaction.net"
+ ]
+ },
+ "DataSift": {
+ "properties": [
+ "datasift.com",
+ "tweetmeme.com"
+ ],
+ "resources": [
+ "datasift.com",
+ "tweetmeme.com"
+ ]
+ },
+ "DataXu": {
+ "properties": [
+ "dataxu.com",
+ "mexad.com",
+ "w55c.net"
+ ],
+ "resources": [
+ "dataxu.com",
+ "dataxu.net",
+ "mexad.com",
+ "w55c.net"
+ ]
+ },
+ "Datonics": {
+ "properties": [
+ "datonics.com"
+ ],
+ "resources": [
+ "datonics.com",
+ "pro-market.net"
+ ]
+ },
+ "Datran Media": {
+ "properties": [
+ "datranmedia.com",
+ "displaymarketplace.com"
+ ],
+ "resources": [
+ "datranmedia.com",
+ "displaymarketplace.com"
+ ]
+ },
+ "Datvantage": {
+ "properties": [
+ "datvantage.com"
+ ],
+ "resources": [
+ "datvantage.com"
+ ]
+ },
+ "DC Storm": {
+ "properties": [
+ "dc-storm.com",
+ "stormiq.com"
+ ],
+ "resources": [
+ "dc-storm.com",
+ "stormiq.com"
+ ]
+ },
+ "Dedicated Media": {
+ "properties": [
+ "dedicatedmedia.com",
+ "dedicatednetworks.com"
+ ],
+ "resources": [
+ "dedicatedmedia.com",
+ "dedicatednetworks.com"
+ ]
+ },
+ "Deep Intent": {
+ "properties": [
+ "deepintent.com"
+ ],
+ "resources": [
+ "deepintent.com"
+ ]
+ },
+ "Delivr": {
+ "properties": [
+ "delivr.com"
+ ],
+ "resources": [
+ "delivr.com",
+ "percentmobile.com"
+ ]
+ },
+ "Delta Projects": {
+ "properties": [
+ "deltaprojects.com"
+ ],
+ "resources": [
+ "adaction.se",
+ "de17a.com",
+ "deltaprojects.com",
+ "deltaprojects.se"
+ ]
+ },
+ "Demandbase": {
+ "properties": [
+ "demandbase.com"
+ ],
+ "resources": [
+ "company-target.com",
+ "demandbase.com"
+ ]
+ },
+ "Demand Media": {
+ "properties": [
+ "leafgroup.com"
+ ],
+ "resources": [
+ "demandmedia.com",
+ "indieclick.com"
+ ]
+ },
+ "Deutsche Post DHL": {
+ "properties": [
+ "dpdhl.com"
+ ],
+ "resources": [
+ "adcloud.com",
+ "adcloud.net",
+ "dp-dhl.com",
+ "dpdhl.com"
+ ]
+ },
+ "Developer Media": {
+ "properties": [
+ "developermedia.com"
+ ],
+ "resources": [
+ "developermedia.com",
+ "lqcdn.com"
+ ]
+ },
+ "DG": {
+ "properties": [
+ "dgit.com",
+ "sizmek.com"
+ ],
+ "resources": [
+ "dgit.com",
+ "eyeblaster.com",
+ "eyewonder.com",
+ "mdadx.com",
+ "serving-sys.com",
+ "unicast.com"
+ ]
+ },
+ "dianomi": {
+ "properties": [
+ "dianomi.com"
+ ],
+ "resources": [
+ "dianomi.com"
+ ]
+ },
+ "Didit": {
+ "properties": [
+ "didit.com"
+ ],
+ "resources": [
+ "did-it.com",
+ "didit.com"
+ ]
+ },
+ "Digg": {
+ "properties": [
+ "digg.com"
+ ],
+ "resources": [
+ "digg.com"
+ ]
+ },
+ "DigitalAdConsortium": {
+ "properties": [
+ "dac.co.jp"
+ ],
+ "resources": [
+ "impact-ad.jp"
+ ]
+ },
+ "Digital River": {
+ "properties": [
+ "digitalriver.com",
+ "keywordmax.com",
+ "netflame.cc"
+ ],
+ "resources": [
+ "digitalriver.com",
+ "keywordmax.com",
+ "netflame.cc"
+ ]
+ },
+ "Digital Target": {
+ "properties": [
+ "digitaltarget.ru"
+ ],
+ "resources": [
+ "digitaltarget.ru"
+ ]
+ },
+ "Digitize": {
+ "properties": [
+ "digitize.ie"
+ ],
+ "resources": [
+ "digitize.ie"
+ ]
+ },
+ "DirectAdvert": {
+ "properties": [
+ "directadvert.ru"
+ ],
+ "resources": [
+ "directadvert.ru"
+ ]
+ },
+ "DirectCORP": {
+ "properties": [
+ "directcorp.de",
+ "ipcounter.de"
+ ],
+ "resources": [
+ "directcorp.de",
+ "ipcounter.de"
+ ]
+ },
+ "Direct Response Group": {
+ "properties": [
+ "directresponsegroup.com"
+ ],
+ "resources": [
+ "directresponsegroup.com",
+ "ppctracking.net"
+ ]
+ },
+ "Directtrack": {
+ "properties": [
+ "directtrack.com"
+ ],
+ "resources": [
+ "directtrack.com"
+ ]
+ },
+ "Disqus": {
+ "properties": [
+ "disqus.com",
+ "disqusads.com"
+ ],
+ "resources": [
+ "disqus.com",
+ "disqusads.com"
+ ]
+ },
+ "DistilNetworks": {
+ "properties": [
+ "distilnetworks.com"
+ ],
+ "resources": [
+ "distilnetworks.com",
+ "distiltag.com"
+ ]
+ },
+ "DistrictM": {
+ "properties": [
+ "districtm.net"
+ ],
+ "resources": [
+ "districtm.io"
+ ]
+ },
+ "dmpxs": {
+ "properties": [
+ "dmpxs.com"
+ ],
+ "resources": [
+ "dmpxs.com"
+ ]
+ },
+ "DoublePimp": {
+ "properties": [
+ "doublepimp.com"
+ ],
+ "resources": [
+ "doublepimp.com"
+ ]
+ },
+ "DoublePositive": {
+ "properties": [
+ "doublepositive.com"
+ ],
+ "resources": [
+ "bid-tag.com",
+ "doublepositive.com"
+ ]
+ },
+ "DoubleVerify": {
+ "properties": [
+ "doubleverify.com"
+ ],
+ "resources": [
+ "doubleverify.com"
+ ]
+ },
+ "Drawbridge": {
+ "properties": [
+ "drawbridge.com"
+ ],
+ "resources": [
+ "adsymptotic.com",
+ "drawbrid.ge",
+ "drawbridge.com"
+ ]
+ },
+ "DS-IQ": {
+ "properties": [
+ "ds-iq.com"
+ ],
+ "resources": [
+ "ds-iq.com"
+ ]
+ },
+ "DSNR Group": {
+ "properties": [
+ "dsnrgroup.com",
+ "dsnrmg.com",
+ "traffiliate.com",
+ "z5x.net"
+ ],
+ "resources": [
+ "dsnrgroup.com",
+ "dsnrmg.com",
+ "traffiliate.com",
+ "z5x.com",
+ "z5x.net"
+ ]
+ },
+ "dwstat.com": {
+ "properties": [
+ "dwstat.cn"
+ ],
+ "resources": [
+ "dwstat.cn"
+ ]
+ },
+ "DynAdmic": {
+ "properties": [
+ "dynadmic.com"
+ ],
+ "resources": [
+ "dynadmic.com",
+ "dyntrk.com"
+ ]
+ },
+ "DynamicOxygen": {
+ "properties": [
+ "dynamicoxygen.com",
+ "exitjunction.com"
+ ],
+ "resources": [
+ "dynamicoxygen.com",
+ "exitjunction.com"
+ ]
+ },
+ "DynamicYield": {
+ "properties": [
+ "dynamicyield.com"
+ ],
+ "resources": [
+ "dynamicyield.com"
+ ]
+ },
+ "Earnify": {
+ "properties": [
+ "earnify.com"
+ ],
+ "resources": [
+ "earnify.com"
+ ]
+ },
+ "eBay": {
+ "properties": [
+ "ebay.at",
+ "ebay.ba",
+ "ebay.be",
+ "ebay.ca",
+ "ebay.ch",
+ "ebay.cn",
+ "ebay.co.jp",
+ "ebay.co.kr",
+ "ebay.co.uk",
+ "ebay.com",
+ "ebay.com.au",
+ "ebay.com.hk",
+ "ebay.com.my",
+ "ebay.com.ph",
+ "ebay.com.sg",
+ "ebay.com.tw",
+ "ebay.de",
+ "ebay.es",
+ "ebay.fr",
+ "ebay.ie",
+ "ebay.in",
+ "ebay.it",
+ "ebay.nl",
+ "ebay.pl"
+ ],
+ "resources": [
+ "ebay.com"
+ ]
+ },
+ "Echo": {
+ "properties": [
+ "aboutecho.com",
+ "haloscan.com",
+ "js-kit.com"
+ ],
+ "resources": [
+ "aboutecho.com",
+ "haloscan.com",
+ "js-kit.com"
+ ]
+ },
+ "ECSAnalytics": {
+ "properties": [
+ "ecsanalytics.com",
+ "theecsinc.com"
+ ],
+ "resources": [
+ "ecsanalytics.com"
+ ]
+ },
+ "EFF": {
+ "properties": [
+ "do-not-tracker.org",
+ "eff.org",
+ "eviltracker.net",
+ "trackersimulator.org"
+ ],
+ "resources": [
+ "do-not-tracker.org",
+ "eff.org",
+ "eviltracker.net",
+ "trackersimulator.org"
+ ]
+ },
+ "Effective Measure": {
+ "properties": [
+ "effectivemeasure.com",
+ "effectivemeasure.net"
+ ],
+ "resources": [
+ "effectivemeasure.com",
+ "effectivemeasure.net"
+ ]
+ },
+ "ekolay": {
+ "properties": [
+ "hurriyet.com.tr"
+ ],
+ "resources": [
+ "e-kolay.net",
+ "ekolay.net",
+ "hurriyet.com.tr"
+ ]
+ },
+ "Eleavers": {
+ "properties": [
+ "eleavers.com"
+ ],
+ "resources": [
+ "eleavers.com"
+ ]
+ },
+ "Emego": {
+ "properties": [
+ "usemax.de"
+ ],
+ "resources": [
+ "usemax.de"
+ ]
+ },
+ "Emerse": {
+ "properties": [
+ "emerse.com"
+ ],
+ "resources": [
+ "emerse.com"
+ ]
+ },
+ "EMX": {
+ "properties": [
+ "emxdigital.com"
+ ],
+ "resources": [
+ "brealtime.com",
+ "clearstream.tv",
+ "emxdgt.com",
+ "emxdigital.com"
+ ]
+ },
+ "Enecto": {
+ "properties": [
+ "enecto.com"
+ ],
+ "resources": [
+ "enecto.com"
+ ]
+ },
+ "engage:BDR": {
+ "properties": [
+ "engagebdr.com"
+ ],
+ "resources": [
+ "bnmla.com",
+ "engagebdr.com"
+ ]
+ },
+ "Engago Technology": {
+ "properties": [
+ "engago.com"
+ ],
+ "resources": [
+ "appmetrx.com",
+ "engago.com"
+ ]
+ },
+ "Engine Network": {
+ "properties": [
+ "enginenetwork.com"
+ ],
+ "resources": [
+ "enginenetwork.com"
+ ]
+ },
+ "Ensighten": {
+ "properties": [
+ "ensighten.com"
+ ],
+ "resources": [
+ "ensighten.com"
+ ]
+ },
+ "Entireweb": {
+ "properties": [
+ "entireweb.com"
+ ],
+ "resources": [
+ "entireweb.com"
+ ]
+ },
+ "Epic Media Group": {
+ "properties": [
+ "epicadvertising.com",
+ "epicmarketplace.com",
+ "theepicmediagroup.com"
+ ],
+ "resources": [
+ "epicadvertising.com",
+ "epicmarketplace.com",
+ "epicmobileads.com",
+ "theepicmediagroup.com",
+ "trafficmp.com"
+ ]
+ },
+ "eProof.com": {
+ "properties": [
+ "eproof.com"
+ ],
+ "resources": [
+ "eproof.com"
+ ]
+ },
+ "Epsilon": {
+ "properties": [
+ "epsilon.com"
+ ],
+ "resources": [
+ "epsilon.com"
+ ]
+ },
+ "EQ Ads": {
+ "properties": [
+ "eqads.com"
+ ],
+ "resources": [
+ "eqads.com"
+ ]
+ },
+ "EroAdvertising": {
+ "properties": [
+ "ero-advertising.com"
+ ],
+ "resources": [
+ "ero-advertising.com"
+ ]
+ },
+ "Etarget": {
+ "properties": [
+ "etarget.net",
+ "etargetnet.com"
+ ],
+ "resources": [
+ "etarget.net",
+ "etargetnet.com"
+ ]
+ },
+ "Etineria": {
+ "properties": [
+ "adwitserver.com",
+ "etineria.com"
+ ],
+ "resources": [
+ "adwitserver.com",
+ "etineria.com"
+ ]
+ },
+ "etracker": {
+ "properties": [
+ "etracker.com",
+ "etracker.de"
+ ],
+ "resources": [
+ "etracker.com",
+ "etracker.de",
+ "sedotracker.com",
+ "sedotracker.de"
+ ]
+ },
+ "eTrigue": {
+ "properties": [
+ "etrigue.com"
+ ],
+ "resources": [
+ "etrigue.com"
+ ]
+ },
+ "Eulerian Technologies": {
+ "properties": [
+ "eulerian.com"
+ ],
+ "resources": [
+ "eulerian.com",
+ "eulerian.net"
+ ]
+ },
+ "Evergage": {
+ "properties": [
+ "evergage.com"
+ ],
+ "resources": [
+ "mybuys.com",
+ "veruta.com"
+ ]
+ },
+ "Everyday Health": {
+ "properties": [
+ "everydayhealth.com",
+ "waterfrontmedia.com"
+ ],
+ "resources": [
+ "everydayhealth.com",
+ "waterfrontmedia.com"
+ ]
+ },
+ "Evisions Marketing": {
+ "properties": [
+ "engineseeker.com",
+ "evisionsmarketing.com"
+ ],
+ "resources": [
+ "engineseeker.com",
+ "evisionsmarketing.com"
+ ]
+ },
+ "Evolve": {
+ "properties": [
+ "evolvemediacorp.com",
+ "gorillanation.com"
+ ],
+ "resources": [
+ "evolvemediacorp.com",
+ "evolvemediametrics.com",
+ "gorillanation.com"
+ ]
+ },
+ "eWayDirect": {
+ "properties": [
+ "ewaydirect.com"
+ ],
+ "resources": [
+ "ewaydirect.com",
+ "ixs1.net"
+ ]
+ },
+ "ewebse": {
+ "properties": [
+ "777seo.com",
+ "ewebse.com"
+ ],
+ "resources": [
+ "777seo.com",
+ "ewebse.com"
+ ]
+ },
+ "excitad": {
+ "properties": [
+ "excitad.com"
+ ],
+ "resources": [
+ "excitad.com"
+ ]
+ },
+ "eXelate": {
+ "properties": [
+ "exelate.com"
+ ],
+ "resources": [
+ "exelate.com",
+ "exelator.com"
+ ]
+ },
+ "ExoClick": {
+ "properties": [
+ "exoclick.com"
+ ],
+ "resources": [
+ "exoclick.com"
+ ]
+ },
+ "Exosrv": {
+ "properties": [
+ "exosrv.com"
+ ],
+ "resources": [
+ "exosrv.com"
+ ]
+ },
+ "Experian": {
+ "properties": [
+ "experian.com"
+ ],
+ "resources": [
+ "audienceiq.com",
+ "experian.com"
+ ]
+ },
+ "expo-MAX": {
+ "properties": [
+ "expo-max.com"
+ ],
+ "resources": [
+ "expo-max.com"
+ ]
+ },
+ "Exponential Interactive": {
+ "properties": [
+ "exponential.com",
+ "fulltango.com"
+ ],
+ "resources": [
+ "adotube.com",
+ "exponential.com",
+ "fulltango.com",
+ "tribalfusion.com"
+ ]
+ },
+ "Extension Factory": {
+ "properties": [
+ "extensionfactory.com"
+ ],
+ "resources": [
+ "extensionfactory.com"
+ ]
+ },
+ "EXTENSIONS.RU": {
+ "properties": [
+ "extensions.ru"
+ ],
+ "resources": [
+ "extensions.ru"
+ ]
+ },
+ "eXTReMe digital": {
+ "properties": [
+ "extremetracking.com"
+ ],
+ "resources": [
+ "extreme-dm.com",
+ "extremetracking.com"
+ ]
+ },
+ "Eyeconomy": {
+ "properties": [
+ "eyeconomy.co.uk"
+ ],
+ "resources": [
+ "eyeconomy.co.uk",
+ "eyeconomy.com",
+ "sublimemedia.net",
+ "www.eyeconomy.co.uk"
+ ]
+ },
+ "EyeNewton": {
+ "properties": [
+ "eyenewton.ru"
+ ],
+ "resources": [
+ "eyenewton.ru"
+ ]
+ },
+ "Eyeota": {
+ "properties": [
+ "eyeota.net"
+ ],
+ "resources": [
+ "eyeota.net"
+ ]
+ },
+ "eyeReturn Marketing": {
+ "properties": [
+ "eyereturnmarketing.com"
+ ],
+ "resources": [
+ "eyereturn.com",
+ "eyereturnmarketing.com"
+ ]
+ },
+ "Eyeviewdigital": {
+ "properties": [
+ "eyeviewdigital.com"
+ ],
+ "resources": [
+ "eyeviewads.com",
+ "eyeviewdigital.com"
+ ]
+ },
+ "Facebook": {
+ "properties": [
+ "atlassolutions.com",
+ "facebook.com",
+ "facebook.de",
+ "facebook.fr",
+ "facebook.net",
+ "fb.com",
+ "fb.me",
+ "fbcdn.net",
+ "friendfeed.com",
+ "instagram.com",
+ "internalfb.com",
+ "messenger.com",
+ "oculus.com",
+ "whatsapp.com",
+ "workplace.com"
+ ],
+ "resources": [
+ "apps.fbsbx.com",
+ "atdmt.com",
+ "atlassolutions.com",
+ "facebook.com",
+ "facebook.de",
+ "facebook.fr",
+ "facebook.net",
+ "fb.com",
+ "fb.me",
+ "fbcdn.net",
+ "fbsbx.com",
+ "friendfeed.com",
+ "instagram.com",
+ "messenger.com"
+ ]
+ },
+ "Facilitate Digital": {
+ "properties": [
+ "adsfac.eu",
+ "adsfac.net",
+ "adsfac.us",
+ "facilitatedigital.com"
+ ],
+ "resources": [
+ "adsfac.eu",
+ "adsfac.info",
+ "adsfac.net",
+ "adsfac.sg",
+ "adsfac.us",
+ "facilitatedigital.com"
+ ]
+ },
+ "Fairfax Media": {
+ "properties": [
+ "fairfax.com.au",
+ "fxj.com.au",
+ "www.fxj.com.au"
+ ],
+ "resources": [
+ "fairfax.com.au",
+ "fxj.com.au",
+ "www.fxj.com.au"
+ ]
+ },
+ "faithadnet": {
+ "properties": [
+ "faithadnet.com"
+ ],
+ "resources": [
+ "faithadnet.com"
+ ]
+ },
+ "Fanplayr": {
+ "properties": [
+ "fanplayr.com"
+ ],
+ "resources": [
+ "fanplayr.com"
+ ]
+ },
+ "Fathom": {
+ "properties": [
+ "fathomdelivers.com",
+ "fathomseo.com"
+ ],
+ "resources": [
+ "fathomdelivers.com",
+ "fathomseo.com"
+ ]
+ },
+ "Federated Media": {
+ "properties": [
+ "hyfn.com",
+ "lijit.com"
+ ],
+ "resources": [
+ "federatedmedia.net",
+ "fmpub.net",
+ "hyfn.com",
+ "lijit.com"
+ ]
+ },
+ "Feedjit": {
+ "properties": [
+ "feedjit.com"
+ ],
+ "resources": [
+ "feedjit.com"
+ ]
+ },
+ "FetchBack": {
+ "properties": [
+ "fetchback.com"
+ ],
+ "resources": [
+ "fetchback.com"
+ ]
+ },
+ "Fiksu": {
+ "properties": [
+ "fiksu.com"
+ ],
+ "resources": [
+ "fiksu.com"
+ ]
+ },
+ "FinancialContent": {
+ "properties": [
+ "financialcontent.com"
+ ],
+ "resources": [
+ "financialcontent.com"
+ ]
+ },
+ "Fizz-Buzz Media": {
+ "properties": [
+ "fizzbuzzmedia.com",
+ "fizzbuzzmedia.net"
+ ],
+ "resources": [
+ "fizzbuzzmedia.com",
+ "fizzbuzzmedia.net"
+ ]
+ },
+ "Flashtalking": {
+ "properties": [
+ "flashtalking.com"
+ ],
+ "resources": [
+ "encoremetrics.com",
+ "flashtalking.com",
+ "sitecompass.com"
+ ]
+ },
+ "Flattr": {
+ "properties": [
+ "flattr.com"
+ ],
+ "resources": [
+ "flattr.com"
+ ]
+ },
+ "Flite": {
+ "properties": [
+ "flite.com",
+ "widgetserver.com"
+ ],
+ "resources": [
+ "flite.com",
+ "widgetserver.com"
+ ]
+ },
+ "Fluct": {
+ "properties": [
+ "adingo.jp",
+ "fluct.jp"
+ ],
+ "resources": [
+ "adingo.jp",
+ "fluct.jp"
+ ]
+ },
+ "Flytxt": {
+ "properties": [
+ "flytxt.com"
+ ],
+ "resources": [
+ "flytxt.com"
+ ]
+ },
+ "Footprint": {
+ "properties": [
+ "footprintlive.com"
+ ],
+ "resources": [
+ "footprintlive.com"
+ ]
+ },
+ "Forbes": {
+ "properties": [
+ "brandsideplatform.com",
+ "forbes.com"
+ ],
+ "resources": [
+ "brandsideplatform.com",
+ "forbes.com"
+ ]
+ },
+ "Foresee": {
+ "properties": [
+ "foresee.com"
+ ],
+ "resources": [
+ "answerscloud.com"
+ ]
+ },
+ "Fox One Stop Media": {
+ "properties": [
+ "fimserve.com",
+ "foxnetworks.com",
+ "foxonestop.com",
+ "mobsmith.com",
+ "myads.com",
+ "othersonline.com"
+ ],
+ "resources": [
+ "fimserve.com",
+ "foxnetworks.com",
+ "foxonestop.com",
+ "mobsmith.com",
+ "myads.com",
+ "othersonline.com"
+ ]
+ },
+ "FreakOut": {
+ "properties": [
+ "fout.jp"
+ ],
+ "resources": [
+ "fout.jp"
+ ]
+ },
+ "Freedom Communications": {
+ "properties": [
+ "freedom.com"
+ ],
+ "resources": [
+ "freedom.com"
+ ]
+ },
+ "Free Online Users": {
+ "properties": [
+ "freeonlineusers.com"
+ ],
+ "resources": [
+ "freeonlineusers.com"
+ ]
+ },
+ "Free-PageRank.com": {
+ "properties": [
+ "free-pagerank.com"
+ ],
+ "resources": [
+ "free-pagerank.com"
+ ]
+ },
+ "FreeWheel": {
+ "properties": [
+ "freewheel.tv",
+ "fwmrm.net"
+ ],
+ "resources": [
+ "freewheel.tv",
+ "fwmrm.net",
+ "stickyadstv.com"
+ ]
+ },
+ "FriendFinder Networks": {
+ "properties": [
+ "adultfriendfinder.com",
+ "ffn.com",
+ "pop6.com"
+ ],
+ "resources": [
+ "adultfriendfinder.com",
+ "ffn.com",
+ "pop6.com"
+ ]
+ },
+ "Friends2Follow": {
+ "properties": [
+ "friends2follow.com"
+ ],
+ "resources": [
+ "friends2follow.com"
+ ]
+ },
+ "Frog Sex": {
+ "properties": [
+ "double-check.com",
+ "frogsex.com"
+ ],
+ "resources": [
+ "double-check.com",
+ "frogsex.com"
+ ]
+ },
+ "FuelX": {
+ "properties": [
+ "fuelx.com"
+ ],
+ "resources": [
+ "fuel451.com"
+ ]
+ },
+ "Fullstory": {
+ "properties": [
+ "fullstory.com"
+ ],
+ "resources": [
+ "fullstory.com"
+ ]
+ },
+ "Future Ads": {
+ "properties": [
+ "futureads.com",
+ "resultlinks.com"
+ ],
+ "resources": [
+ "futureads.com",
+ "resultlinks.com"
+ ]
+ },
+ "Fyber": {
+ "properties": [
+ "fyber.com"
+ ],
+ "resources": [
+ "fyber.com"
+ ]
+ },
+ "Game Advertising Online": {
+ "properties": [
+ "game-advertising-online.com"
+ ],
+ "resources": [
+ "game-advertising-online.com"
+ ]
+ },
+ "Games2win": {
+ "properties": [
+ "games2win.com",
+ "inviziads.com"
+ ],
+ "resources": [
+ "games2win.com",
+ "inviziads.com"
+ ]
+ },
+ "Gamned": {
+ "properties": [
+ "gamned.com"
+ ],
+ "resources": [
+ "gamned.com"
+ ]
+ },
+ "Gannett": {
+ "properties": [
+ "gannett.com",
+ "pointroll.com"
+ ],
+ "resources": [
+ "gannett.com",
+ "pointroll.com"
+ ]
+ },
+ "GB-World": {
+ "properties": [
+ "gb-world.net"
+ ],
+ "resources": [
+ "gb-world.net"
+ ]
+ },
+ "Gemius": {
+ "properties": [
+ "gemius.com",
+ "gemius.pl"
+ ],
+ "resources": [
+ "gemius.com",
+ "gemius.pl"
+ ]
+ },
+ "Genesis Media": {
+ "properties": [
+ "genesismedia.com"
+ ],
+ "resources": [
+ "genesismedia.com",
+ "genesismediaus.com"
+ ]
+ },
+ "GENIEE": {
+ "properties": [
+ "geniee.co.jp"
+ ],
+ "resources": [
+ "geniee.co.jp",
+ "gssprt.jp"
+ ]
+ },
+ "GENIE GROUP": {
+ "properties": [
+ "geniegroupltd.co.uk",
+ "www.geniegroupltd.co.uk"
+ ],
+ "resources": [
+ "geniegroupltd.co.uk",
+ "www.geniegroupltd.co.uk"
+ ]
+ },
+ "Genius.com": {
+ "properties": [
+ "genius.com",
+ "rsvpgenius.com"
+ ],
+ "resources": [
+ "genius.com",
+ "rsvpgenius.com"
+ ]
+ },
+ "GeoAds": {
+ "properties": [
+ "geoads.com"
+ ],
+ "resources": [
+ "geoads.com"
+ ]
+ },
+ "GetGlue": {
+ "properties": [
+ "elfie.com",
+ "smrtlnks.com"
+ ],
+ "resources": [
+ "getglue.com",
+ "smrtlnks.com"
+ ]
+ },
+ "GetIntent": {
+ "properties": [
+ "adhigh.net",
+ "getintent.com"
+ ],
+ "resources": [
+ "adhigh.net",
+ "getintent.com"
+ ]
+ },
+ "Get Satisfaction": {
+ "properties": [
+ "getsatisfaction.com"
+ ],
+ "resources": [
+ "getsatisfaction.com"
+ ]
+ },
+ "GetSiteControl": {
+ "properties": [
+ "getsitecontrol.com"
+ ],
+ "resources": [
+ "getsitecontrol.com"
+ ]
+ },
+ "GfK Group": {
+ "properties": [
+ "gfk.com"
+ ],
+ "resources": [
+ "daphnecm.com",
+ "gfk.com",
+ "gfkdaphne.com"
+ ]
+ },
+ "Gigya": {
+ "properties": [
+ "gigya.com"
+ ],
+ "resources": [
+ "gigcount.com",
+ "gigya.com"
+ ]
+ },
+ "GISMAds": {
+ "properties": [
+ "gismads.jp"
+ ],
+ "resources": [
+ "gismads.jp"
+ ]
+ },
+ "GitHub": {
+ "properties": [
+ "gaug.es",
+ "github.com"
+ ],
+ "resources": [
+ "gaug.es",
+ "github.com"
+ ]
+ },
+ "Glam Media": {
+ "properties": [
+ "glam.com",
+ "glammedia.com"
+ ],
+ "resources": [
+ "glam.com",
+ "glammedia.com"
+ ]
+ },
+ "Gleam": {
+ "properties": [
+ "gleam.io"
+ ],
+ "resources": [
+ "fraudjs.io",
+ "gleam.io"
+ ]
+ },
+ "Global Takeoff": {
+ "properties": [
+ "globaltakeoff.com",
+ "globaltakeoff.net"
+ ],
+ "resources": [
+ "globaltakeoff.com",
+ "globaltakeoff.net"
+ ]
+ },
+ "Globe7": {
+ "properties": [
+ "globe7.com"
+ ],
+ "resources": [
+ "globe7.com"
+ ]
+ },
+ "Go Daddy": {
+ "properties": [
+ "godaddy.com",
+ "trafficfacts.com"
+ ],
+ "resources": [
+ "godaddy.com",
+ "trafficfacts.com"
+ ]
+ },
+ "GoDataFeed": {
+ "properties": [
+ "godatafeed.com"
+ ],
+ "resources": [
+ "godatafeed.com"
+ ]
+ },
+ "GoGrid": {
+ "properties": [
+ "datapipe.com",
+ "formalyzer.com"
+ ],
+ "resources": [
+ "datapipe.com",
+ "formalyzer.com",
+ "gogrid.com",
+ "komli.net"
+ ]
+ },
+ "Goldbach": {
+ "properties": [
+ "goldbachgroup.com"
+ ],
+ "resources": [
+ "goldbach.com",
+ "goldbachgroup.com"
+ ]
+ },
+ "GoldSpot Media": {
+ "properties": [
+ "goldspotmedia.com"
+ ],
+ "resources": [
+ "goldspotmedia.com"
+ ]
+ },
+ "Google": {
+ "properties": [
+ "abc.xyz",
+ "admeld.com",
+ "blogger.com",
+ "blogspot.com",
+ "crashlytics.com",
+ "google-melange.com",
+ "google.ac",
+ "google.ad",
+ "google.ae",
+ "google.al",
+ "google.am",
+ "google.as",
+ "google.at",
+ "google.az",
+ "google.ba",
+ "google.be",
+ "google.bf",
+ "google.bg",
+ "google.bi",
+ "google.bj",
+ "google.bs",
+ "google.bt",
+ "google.by",
+ "google.ca",
+ "google.cat",
+ "google.cd",
+ "google.cf",
+ "google.cg",
+ "google.ch",
+ "google.ci",
+ "google.cl",
+ "google.cm",
+ "google.cn",
+ "google.co.ao",
+ "google.co.bw",
+ "google.co.ck",
+ "google.co.cr",
+ "google.co.id",
+ "google.co.il",
+ "google.co.in",
+ "google.co.jp",
+ "google.co.ke",
+ "google.co.kr",
+ "google.co.ls",
+ "google.co.ma",
+ "google.co.mz",
+ "google.co.nz",
+ "google.co.th",
+ "google.co.tz",
+ "google.co.ug",
+ "google.co.uk",
+ "google.co.uz",
+ "google.co.ve",
+ "google.co.vi",
+ "google.co.za",
+ "google.co.zm",
+ "google.co.zw",
+ "google.com",
+ "google.com.af",
+ "google.com.ag",
+ "google.com.ai",
+ "google.com.ar",
+ "google.com.au",
+ "google.com.bd",
+ "google.com.bh",
+ "google.com.bn",
+ "google.com.bo",
+ "google.com.br",
+ "google.com.bz",
+ "google.com.co",
+ "google.com.cu",
+ "google.com.cy",
+ "google.com.do",
+ "google.com.ec",
+ "google.com.eg",
+ "google.com.et",
+ "google.com.fj",
+ "google.com.gh",
+ "google.com.gi",
+ "google.com.gt",
+ "google.com.hk",
+ "google.com.jm",
+ "google.com.kh",
+ "google.com.kw",
+ "google.com.lb",
+ "google.com.ly",
+ "google.com.mm",
+ "google.com.mt",
+ "google.com.mx",
+ "google.com.my",
+ "google.com.na",
+ "google.com.nf",
+ "google.com.ng",
+ "google.com.ni",
+ "google.com.np",
+ "google.com.om",
+ "google.com.pa",
+ "google.com.pe",
+ "google.com.pg",
+ "google.com.ph",
+ "google.com.pk",
+ "google.com.pr",
+ "google.com.py",
+ "google.com.qa",
+ "google.com.sa",
+ "google.com.sb",
+ "google.com.sg",
+ "google.com.sl",
+ "google.com.sv",
+ "google.com.tj",
+ "google.com.tr",
+ "google.com.tw",
+ "google.com.ua",
+ "google.com.uy",
+ "google.com.vc",
+ "google.com.vn",
+ "google.cv",
+ "google.cz",
+ "google.de",
+ "google.dj",
+ "google.dk",
+ "google.dm",
+ "google.dz",
+ "google.ee",
+ "google.es",
+ "google.fi",
+ "google.fm",
+ "google.fr",
+ "google.ga",
+ "google.ge",
+ "google.gg",
+ "google.gl",
+ "google.gm",
+ "google.gp",
+ "google.gr",
+ "google.gy",
+ "google.hn",
+ "google.hr",
+ "google.ht",
+ "google.hu",
+ "google.ie",
+ "google.im",
+ "google.iq",
+ "google.is",
+ "google.it",
+ "google.je",
+ "google.jo",
+ "google.kg",
+ "google.ki",
+ "google.kz",
+ "google.la",
+ "google.li",
+ "google.lk",
+ "google.lt",
+ "google.lu",
+ "google.lv",
+ "google.md",
+ "google.me",
+ "google.mg",
+ "google.mk",
+ "google.ml",
+ "google.mn",
+ "google.ms",
+ "google.mu",
+ "google.mv",
+ "google.mw",
+ "google.ne",
+ "google.nl",
+ "google.no",
+ "google.nr",
+ "google.nu",
+ "google.pl",
+ "google.pn",
+ "google.ps",
+ "google.pt",
+ "google.ro",
+ "google.rs",
+ "google.ru",
+ "google.rw",
+ "google.sc",
+ "google.se",
+ "google.sh",
+ "google.si",
+ "google.sk",
+ "google.sm",
+ "google.sn",
+ "google.so",
+ "google.st",
+ "google.td",
+ "google.tg",
+ "google.tk",
+ "google.tl",
+ "google.tm",
+ "google.tn",
+ "google.to",
+ "google.tt",
+ "google.vg",
+ "google.vu",
+ "google.ws",
+ "googlesource.com",
+ "ingress.com",
+ "nest.com",
+ "panoramio.com",
+ "pinpoint-dot-chromeperf.appspot.com",
+ "youtube.com"
+ ],
+ "resources": [
+ "2mdn.net",
+ "admeld.com",
+ "admob.com",
+ "apture.com",
+ "blogger.com",
+ "cc-dt.com",
+ "crashlytics.com",
+ "destinationurl.com",
+ "doubleclick.net",
+ "ggpht.com",
+ "gmail.com",
+ "gmodules.com",
+ "google-analytics.com",
+ "google.ac",
+ "google.ad",
+ "google.ae",
+ "google.al",
+ "google.am",
+ "google.as",
+ "google.at",
+ "google.az",
+ "google.ba",
+ "google.be",
+ "google.bf",
+ "google.bg",
+ "google.bi",
+ "google.bj",
+ "google.bs",
+ "google.bt",
+ "google.by",
+ "google.ca",
+ "google.cat",
+ "google.cc",
+ "google.cd",
+ "google.cf",
+ "google.cg",
+ "google.ch",
+ "google.ci",
+ "google.cl",
+ "google.cm",
+ "google.cn",
+ "google.co.ao",
+ "google.co.bw",
+ "google.co.ck",
+ "google.co.cr",
+ "google.co.id",
+ "google.co.il",
+ "google.co.in",
+ "google.co.jp",
+ "google.co.ke",
+ "google.co.kr",
+ "google.co.ls",
+ "google.co.ma",
+ "google.co.mz",
+ "google.co.nz",
+ "google.co.th",
+ "google.co.tz",
+ "google.co.ug",
+ "google.co.uk",
+ "google.co.uz",
+ "google.co.ve",
+ "google.co.vi",
+ "google.co.za",
+ "google.co.zm",
+ "google.co.zw",
+ "google.com",
+ "google.com.af",
+ "google.com.ag",
+ "google.com.ai",
+ "google.com.ar",
+ "google.com.au",
+ "google.com.bd",
+ "google.com.bh",
+ "google.com.bn",
+ "google.com.bo",
+ "google.com.br",
+ "google.com.bz",
+ "google.com.co",
+ "google.com.cu",
+ "google.com.cy",
+ "google.com.do",
+ "google.com.ec",
+ "google.com.eg",
+ "google.com.et",
+ "google.com.fj",
+ "google.com.gh",
+ "google.com.gi",
+ "google.com.gt",
+ "google.com.hk",
+ "google.com.jm",
+ "google.com.kh",
+ "google.com.kw",
+ "google.com.lb",
+ "google.com.lc",
+ "google.com.ly",
+ "google.com.mm",
+ "google.com.mt",
+ "google.com.mx",
+ "google.com.my",
+ "google.com.na",
+ "google.com.nf",
+ "google.com.ng",
+ "google.com.ni",
+ "google.com.np",
+ "google.com.om",
+ "google.com.pa",
+ "google.com.pe",
+ "google.com.pg",
+ "google.com.ph",
+ "google.com.pk",
+ "google.com.pr",
+ "google.com.py",
+ "google.com.qa",
+ "google.com.sa",
+ "google.com.sb",
+ "google.com.sg",
+ "google.com.sl",
+ "google.com.sv",
+ "google.com.tj",
+ "google.com.tn",
+ "google.com.tr",
+ "google.com.tw",
+ "google.com.ua",
+ "google.com.uy",
+ "google.com.vc",
+ "google.com.vn",
+ "google.cv",
+ "google.cz",
+ "google.de",
+ "google.dj",
+ "google.dk",
+ "google.dm",
+ "google.dz",
+ "google.ee",
+ "google.es",
+ "google.fi",
+ "google.fm",
+ "google.fr",
+ "google.ga",
+ "google.ge",
+ "google.gf",
+ "google.gg",
+ "google.gl",
+ "google.gm",
+ "google.gp",
+ "google.gr",
+ "google.gy",
+ "google.hn",
+ "google.hr",
+ "google.ht",
+ "google.hu",
+ "google.ie",
+ "google.im",
+ "google.io",
+ "google.iq",
+ "google.is",
+ "google.it",
+ "google.je",
+ "google.jo",
+ "google.kg",
+ "google.ki",
+ "google.kz",
+ "google.la",
+ "google.li",
+ "google.lk",
+ "google.lt",
+ "google.lu",
+ "google.lv",
+ "google.md",
+ "google.me",
+ "google.mg",
+ "google.mk",
+ "google.ml",
+ "google.mn",
+ "google.ms",
+ "google.mu",
+ "google.mv",
+ "google.mw",
+ "google.ne",
+ "google.nl",
+ "google.no",
+ "google.nr",
+ "google.nu",
+ "google.pl",
+ "google.pn",
+ "google.ps",
+ "google.pt",
+ "google.ro",
+ "google.rs",
+ "google.ru",
+ "google.rw",
+ "google.sc",
+ "google.se",
+ "google.sh",
+ "google.si",
+ "google.sk",
+ "google.sm",
+ "google.sn",
+ "google.so",
+ "google.st",
+ "google.td",
+ "google.tg",
+ "google.tk",
+ "google.tl",
+ "google.tm",
+ "google.tn",
+ "google.to",
+ "google.tt",
+ "google.vg",
+ "google.vu",
+ "google.ws",
+ "googleadservices.com",
+ "googleapis.com",
+ "googlemail.com",
+ "googlesyndication.com",
+ "googletagservices.com",
+ "googleusercontent.com",
+ "googlevideo.com",
+ "gstatic.com",
+ "invitemedia.com",
+ "postrank.com",
+ "recaptcha.net",
+ "smtad.net",
+ "youtube.com"
+ ]
+ },
+ "GoSquared": {
+ "properties": [
+ "gosquared.com"
+ ],
+ "resources": [
+ "gosquared.com"
+ ]
+ },
+ "GoStats": {
+ "properties": [
+ "gostats.com"
+ ],
+ "resources": [
+ "gostats.com"
+ ]
+ },
+ "Grapeshot": {
+ "properties": [
+ "grapeshot.co.uk",
+ "www.grapeshot.co.uk"
+ ],
+ "resources": [
+ "grapeshot.co.uk",
+ "www.grapeshot.co.uk"
+ ]
+ },
+ "GrapheneMedia": {
+ "properties": [
+ "graphenemedia.in"
+ ],
+ "resources": [
+ "graphenedigitalanalytics.in"
+ ]
+ },
+ "Graphnium": {
+ "properties": [
+ "graphinium.com"
+ ],
+ "resources": [
+ "crm4d.com"
+ ]
+ },
+ "Gravity": {
+ "properties": [
+ "gravity.com",
+ "grvcdn.com"
+ ],
+ "resources": [
+ "gravity.com",
+ "grvcdn.com"
+ ]
+ },
+ "Gridcash": {
+ "properties": [
+ "adless.io",
+ "gridcash.net"
+ ],
+ "resources": [
+ "adless.io",
+ "gridcash.net"
+ ]
+ },
+ "Grocery Shopping Network": {
+ "properties": [
+ "groceryshopping.net"
+ ],
+ "resources": [
+ "groceryshopping.net"
+ ]
+ },
+ "GroovinAds": {
+ "properties": [
+ "groovinads.com"
+ ],
+ "resources": [
+ "groovinads.com"
+ ]
+ },
+ "Gruner + Jahr": {
+ "properties": [
+ "guj.de",
+ "ligatus.com"
+ ],
+ "resources": [
+ "guj.de",
+ "ligatus.com"
+ ]
+ },
+ "GTop": {
+ "properties": [
+ "arenaweb.ro"
+ ],
+ "resources": [
+ "arenaweb.ro",
+ "gtop.ro",
+ "gtopstats.com"
+ ]
+ },
+ "GumGum": {
+ "properties": [
+ "gumgum.com"
+ ],
+ "resources": [
+ "gumgum.com"
+ ]
+ },
+ "Gunggo": {
+ "properties": [
+ "gunggo.com"
+ ],
+ "resources": [
+ "gunggo.com"
+ ]
+ },
+ "Hands Mobile": {
+ "properties": [
+ "hands.com.br",
+ "www.hands.com.br"
+ ],
+ "resources": [
+ "hands.com.br",
+ "www.hands.com.br"
+ ]
+ },
+ "Harrenmedia": {
+ "properties": [
+ "harrenmedia.com",
+ "harrenmedianetwork.com"
+ ],
+ "resources": [
+ "harrenmedia.com",
+ "harrenmedianetwork.com"
+ ]
+ },
+ "HealthPricer": {
+ "properties": [
+ "adacado.com",
+ "healthpricer.com"
+ ],
+ "resources": [
+ "adacado.com",
+ "healthpricer.com"
+ ]
+ },
+ "Hearst": {
+ "properties": [
+ "hearst.com",
+ "ic-live.com",
+ "iclive.com",
+ "icrossing.com",
+ "raasnet.com"
+ ],
+ "resources": [
+ "hearst.com",
+ "ic-live.com",
+ "iclive.com",
+ "icrossing.com",
+ "raasnet.com",
+ "redaril.com",
+ "sptag.com",
+ "sptag1.com",
+ "sptag2.com",
+ "sptag3.com"
+ ]
+ },
+ "Heyzap": {
+ "properties": [
+ "heyzap.com"
+ ],
+ "resources": [
+ "heyzap.com"
+ ]
+ },
+ "HilltopAds": {
+ "properties": [
+ "hilltopads.com"
+ ],
+ "resources": [
+ "hilltopads.com",
+ "hilltopads.net",
+ "shoporielder.pro"
+ ]
+ },
+ "Hi-media": {
+ "properties": [
+ "himediagroup.com"
+ ],
+ "resources": [
+ "comclick.com",
+ "hi-media.com",
+ "himediagroup.com"
+ ]
+ },
+ "Histats": {
+ "properties": [
+ "histats.com"
+ ],
+ "resources": [
+ "histats.com"
+ ]
+ },
+ "HitsLink": {
+ "properties": [
+ "hitslink.com"
+ ],
+ "resources": [
+ "hitslink.com"
+ ]
+ },
+ "Hit Sniffer": {
+ "properties": [
+ "hitsniffer.com"
+ ],
+ "resources": [
+ "hitsniffer.com"
+ ]
+ },
+ "Horyzon Media": {
+ "properties": [
+ "horyzon-media.com"
+ ],
+ "resources": [
+ "horyzon-media.com"
+ ]
+ },
+ "HotelChamp": {
+ "properties": [
+ "hotelchamp.com"
+ ],
+ "resources": [
+ "hotelchamp.com"
+ ]
+ },
+ "Hotjar": {
+ "properties": [
+ "hotjar.com"
+ ],
+ "resources": [
+ "hotjar.com"
+ ]
+ },
+ "HotMart": {
+ "properties": [
+ "hotmart.com"
+ ],
+ "resources": [
+ "hotmart.com"
+ ]
+ },
+ "HOTWords": {
+ "properties": [
+ "hotwords.com",
+ "hotwords.es"
+ ],
+ "resources": [
+ "hotwords.com",
+ "hotwords.es"
+ ]
+ },
+ "HP": {
+ "properties": [
+ "hp.com",
+ "opentext.com",
+ "optimost.com"
+ ],
+ "resources": [
+ "hp.com",
+ "optimost.com"
+ ]
+ },
+ "Httpool": {
+ "properties": [
+ "httpool.com"
+ ],
+ "resources": [
+ "httpool.com"
+ ]
+ },
+ "HubSpot": {
+ "properties": [
+ "hubspot.com"
+ ],
+ "resources": [
+ "hs-analytics.net",
+ "hubspot.com"
+ ]
+ },
+ "HUNT Mobile Ads": {
+ "properties": [
+ "huntmads.com"
+ ],
+ "resources": [
+ "huntmads.com"
+ ]
+ },
+ "Hurra.com": {
+ "properties": [
+ "hurra.com"
+ ],
+ "resources": [
+ "hurra.com"
+ ]
+ },
+ "IAB": {
+ "properties": [
+ "digitru.st",
+ "iabtechlab.com"
+ ],
+ "resources": [
+ "digitru.st"
+ ]
+ },
+ "IAC": {
+ "properties": [
+ "iac.com",
+ "iacadvertising.com"
+ ],
+ "resources": [
+ "iac.com",
+ "iacadvertising.com"
+ ]
+ },
+ "iBehavior": {
+ "properties": [
+ "i-behavior.com",
+ "ib-ibi.com"
+ ],
+ "resources": [
+ "i-behavior.com",
+ "ib-ibi.com"
+ ]
+ },
+ "IBM": {
+ "properties": [
+ "ibm.com",
+ "multicloud-ibm.com"
+ ],
+ "resources": [
+ "cmcore.com",
+ "coremetrics.com",
+ "ibm.com",
+ "unica.com",
+ "xtify.com"
+ ]
+ },
+ "ID5": {
+ "properties": [
+ "id5.io"
+ ],
+ "resources": [
+ "id5-sync.com"
+ ]
+ },
+ "IDG": {
+ "properties": [
+ "idg.com",
+ "idgtechnetwork.com"
+ ],
+ "resources": [
+ "idg.com",
+ "idgtechnetwork.com"
+ ]
+ },
+ "iEntry": {
+ "properties": [
+ "600z.com",
+ "ientry.com"
+ ],
+ "resources": [
+ "600z.com",
+ "ientry.com"
+ ]
+ },
+ "IgnitAd": {
+ "properties": [
+ "ignitad.com"
+ ],
+ "resources": [
+ "ignitad.com"
+ ]
+ },
+ "IgnitionOne": {
+ "properties": [
+ "ignitionone.com",
+ "ignitionone.net",
+ "searchignite.com"
+ ],
+ "resources": [
+ "ignitionone.com",
+ "ignitionone.net",
+ "searchignite.com"
+ ]
+ },
+ "iMedia": {
+ "properties": [
+ "imedia.cz"
+ ],
+ "resources": [
+ "imedia.cz"
+ ]
+ },
+ "Improve Digital": {
+ "properties": [
+ "360yield.com",
+ "improvedigital.com"
+ ],
+ "resources": [
+ "360yield.com",
+ "improvedigital.com"
+ ]
+ },
+ "Inadco": {
+ "properties": [
+ "inadco.com"
+ ],
+ "resources": [
+ "anadcoads.com",
+ "inadco.com",
+ "inadcoads.com"
+ ]
+ },
+ "InboundWriter": {
+ "properties": [
+ "enquisite.com",
+ "inboundwriter.com"
+ ],
+ "resources": [
+ "enquisite.com",
+ "inboundwriter.com"
+ ]
+ },
+ "IndexExchange": {
+ "properties": [
+ "indexexchange.com"
+ ],
+ "resources": [
+ "indexexchange.com"
+ ]
+ },
+ "Infectious Media": {
+ "properties": [
+ "infectiousmedia.com"
+ ],
+ "resources": [
+ "impressiondesk.com",
+ "infectiousmedia.com"
+ ]
+ },
+ "Infernotions": {
+ "properties": [
+ "infernotions.com"
+ ],
+ "resources": [
+ "infernotions.com"
+ ]
+ },
+ "Inflection Point Media": {
+ "properties": [
+ "inflectionpointmedia.com"
+ ],
+ "resources": [
+ "inflectionpointmedia.com"
+ ]
+ },
+ "Infogroup": {
+ "properties": [
+ "infogroup.com"
+ ],
+ "resources": [
+ "infogroup.com"
+ ]
+ },
+ "Infolinks": {
+ "properties": [
+ "infolinks.com"
+ ],
+ "resources": [
+ "infolinks.com"
+ ]
+ },
+ "INFOnline": {
+ "properties": [
+ "infonline.de"
+ ],
+ "resources": [
+ "infonline.de",
+ "ioam.de",
+ "ivwbox.de"
+ ]
+ },
+ "InfoStars": {
+ "properties": [
+ "hotlog.ru",
+ "infostars.ru"
+ ],
+ "resources": [
+ "hotlog.ru",
+ "infostars.ru"
+ ]
+ },
+ "Infra-Ad": {
+ "properties": [
+ "infra-ad.com"
+ ],
+ "resources": [
+ "infra-ad.com"
+ ]
+ },
+ "InMobi": {
+ "properties": [
+ "aerserv.com",
+ "inmobi.com",
+ "sproutinc.com"
+ ],
+ "resources": [
+ "aerserv.com",
+ "inmobi.com",
+ "sproutinc.com"
+ ]
+ },
+ "inneractive": {
+ "properties": [
+ "inner-active.com"
+ ],
+ "resources": [
+ "inner-active.com"
+ ]
+ },
+ "Innity": {
+ "properties": [
+ "innity.com"
+ ],
+ "resources": [
+ "innity.com"
+ ]
+ },
+ "InsightExpress": {
+ "properties": [
+ "insightexpress.com"
+ ],
+ "resources": [
+ "insightexpress.com",
+ "insightexpressai.com"
+ ]
+ },
+ "InSkin Media": {
+ "properties": [
+ "inskinmedia.com"
+ ],
+ "resources": [
+ "inskinmedia.com"
+ ]
+ },
+ "Inspectlet": {
+ "properties": [
+ "inspectlet.com"
+ ],
+ "resources": [
+ "inspectlet.com"
+ ]
+ },
+ "Instinctive": {
+ "properties": [
+ "instinctive.io"
+ ],
+ "resources": [
+ "instinctive.io",
+ "instinctiveads.com"
+ ]
+ },
+ "Integral Ad Science": {
+ "properties": [
+ "integralads.com"
+ ],
+ "resources": [
+ "adsafemedia.com",
+ "adsafeprotected.com",
+ "iasds01.com",
+ "integralads.com"
+ ]
+ },
+ "IntelligenceFocus": {
+ "properties": [
+ "intelligencefocus.com",
+ "leadchampion.com"
+ ],
+ "resources": [
+ "domodomain.com",
+ "intelligencefocus.com",
+ "leadchampion.com"
+ ]
+ },
+ "Intent Media": {
+ "properties": [
+ "intentmedia.com"
+ ],
+ "resources": [
+ "intentmedia.com",
+ "intentmedia.net"
+ ]
+ },
+ "Intergi": {
+ "properties": [
+ "intergi.com"
+ ],
+ "resources": [
+ "intergi.com"
+ ]
+ },
+ "Intermarkets": {
+ "properties": [
+ "intermarkets.net"
+ ],
+ "resources": [
+ "intermarkets.net"
+ ]
+ },
+ "Intermundo Media": {
+ "properties": [
+ "intermundomedia.com"
+ ],
+ "resources": [
+ "intermundomedia.com"
+ ]
+ },
+ "Internet Brands": {
+ "properties": [
+ "ibpxl.com",
+ "internetbrands.com"
+ ],
+ "resources": [
+ "ibpxl.com",
+ "internetbrands.com"
+ ]
+ },
+ "Interpolls": {
+ "properties": [
+ "interpolls.com"
+ ],
+ "resources": [
+ "interpolls.com"
+ ]
+ },
+ "Inuvo": {
+ "properties": [
+ "inuvo.com"
+ ],
+ "resources": [
+ "inuvo.com"
+ ]
+ },
+ "InvestingChannel": {
+ "properties": [
+ "investingchannel.com"
+ ],
+ "resources": [
+ "investingchannel.com"
+ ]
+ },
+ "iovation": {
+ "properties": [
+ "iovation.com"
+ ],
+ "resources": [
+ "iesnare.com",
+ "iovation.com"
+ ]
+ },
+ "iPerceptions": {
+ "properties": [
+ "iperceptions.com"
+ ],
+ "resources": [
+ "iperceptions.com"
+ ]
+ },
+ "IponWeb": {
+ "properties": [
+ "iponweb.com"
+ ],
+ "resources": [
+ "iponweb.com",
+ "iponweb.net"
+ ]
+ },
+ "iPROM": {
+ "properties": [
+ "centraliprom.com",
+ "iprom.net",
+ "iprom.si",
+ "mediaiprom.com"
+ ],
+ "resources": [
+ "centraliprom.com",
+ "iprom.net",
+ "iprom.si",
+ "mediaiprom.com"
+ ]
+ },
+ "iPromote": {
+ "properties": [
+ "ipromote.com"
+ ],
+ "resources": [
+ "ipromote.com"
+ ]
+ },
+ "iProspect": {
+ "properties": [
+ "iprospect.com"
+ ],
+ "resources": [
+ "clickmanage.com",
+ "iprospect.com"
+ ]
+ },
+ "ISI Technologies": {
+ "properties": [
+ "adversalservers.com",
+ "digbro.com"
+ ],
+ "resources": [
+ "adversalservers.com",
+ "digbro.com"
+ ]
+ },
+ "IslayTech": {
+ "properties": [
+ "islay.tech"
+ ],
+ "resources": [
+ "islay.tech"
+ ]
+ },
+ "ismatlab.com": {
+ "properties": [
+ "ismatlab.com"
+ ],
+ "resources": [
+ "ismatlab.com"
+ ]
+ },
+ "Itch": {
+ "properties": [
+ "itch.io"
+ ],
+ "resources": [
+ "itch.io"
+ ]
+ },
+ "ItIsATracker": {
+ "properties": [
+ "itisatracker.com"
+ ],
+ "resources": [
+ "itisatracker.com"
+ ]
+ },
+ "I.UA": {
+ "properties": [
+ "i.ua"
+ ],
+ "resources": [
+ "i.ua"
+ ]
+ },
+ "Jaroop": {
+ "properties": [
+ "jaroop.com"
+ ],
+ "resources": [
+ "jaroop.com"
+ ]
+ },
+ "JasperLabs": {
+ "properties": [
+ "jasperlabs.com"
+ ],
+ "resources": [
+ "jasperlabs.com"
+ ]
+ },
+ "Jemm": {
+ "properties": [
+ "jemmgroup.com"
+ ],
+ "resources": [
+ "jemmgroup.com"
+ ]
+ },
+ "Jink": {
+ "properties": [
+ "jink.de",
+ "jinkads.com"
+ ],
+ "resources": [
+ "jink.de",
+ "jinkads.com"
+ ]
+ },
+ "Jirbo": {
+ "properties": [
+ "adcolony.com"
+ ],
+ "resources": [
+ "adcolony.com",
+ "jirbo.com"
+ ]
+ },
+ "Jivox": {
+ "properties": [
+ "jivox.com"
+ ],
+ "resources": [
+ "jivox.com"
+ ]
+ },
+ "JobThread": {
+ "properties": [
+ "jobthread.com"
+ ],
+ "resources": [
+ "jobthread.com"
+ ]
+ },
+ "JSE": {
+ "properties": [
+ "jsecoin.com"
+ ],
+ "resources": [
+ "freecontent.bid",
+ "freecontent.date",
+ "freecontent.stream",
+ "hashing.win",
+ "hostingcloud.racing",
+ "hostingcloud.science",
+ "jsecoin.com"
+ ]
+ },
+ "JuicyAds": {
+ "properties": [
+ "juicyads.com"
+ ],
+ "resources": [
+ "juicyads.com"
+ ]
+ },
+ "Jumptap": {
+ "properties": [
+ "jumptap.com"
+ ],
+ "resources": [
+ "jumptap.com"
+ ]
+ },
+ "justuno": {
+ "properties": [
+ "justuno.com"
+ ],
+ "resources": [
+ "justuno.com"
+ ]
+ },
+ "Kaltura": {
+ "properties": [
+ "kaltura.com"
+ ],
+ "resources": [
+ "kaltura.com"
+ ]
+ },
+ "Kargo": {
+ "properties": [
+ "kargo.com"
+ ],
+ "resources": [
+ "kargo.com"
+ ]
+ },
+ "Kenshoo": {
+ "properties": [
+ "kenshoo.com",
+ "xg4ken.com"
+ ],
+ "resources": [
+ "kenshoo.com",
+ "xg4ken.com"
+ ]
+ },
+ "Keyade": {
+ "properties": [
+ "keyade.com"
+ ],
+ "resources": [
+ "keyade.com"
+ ]
+ },
+ "KeyMetric": {
+ "properties": [
+ "keymetric.net"
+ ],
+ "resources": [
+ "keymetric.net"
+ ]
+ },
+ "Keywee": {
+ "properties": [
+ "keywee.co"
+ ],
+ "resources": [
+ "keywee.co"
+ ]
+ },
+ "kikin": {
+ "properties": [
+ "kikin.com"
+ ],
+ "resources": [
+ "kikin.com"
+ ]
+ },
+ "KISSmetrics": {
+ "properties": [
+ "kissmetrics.com"
+ ],
+ "resources": [
+ "kissmetrics.com"
+ ]
+ },
+ "KissMyAds": {
+ "properties": [
+ "kissmyads.com"
+ ],
+ "resources": [
+ "kissmyads.com"
+ ]
+ },
+ "Kitara Media": {
+ "properties": [
+ "103092804.com",
+ "kitaramedia.com"
+ ],
+ "resources": [
+ "103092804.com",
+ "kitaramedia.com"
+ ]
+ },
+ "Kitcode": {
+ "properties": [
+ "kitcode.net"
+ ],
+ "resources": [
+ "kitcode.net"
+ ]
+ },
+ "KIT digital": {
+ "properties": [
+ "kitd.com"
+ ],
+ "resources": [
+ "keewurd.com",
+ "kitd.com",
+ "peerset.com"
+ ]
+ },
+ "Kokteyl": {
+ "properties": [
+ "admost.com",
+ "kokteyl.com"
+ ],
+ "resources": [
+ "admost.com",
+ "kokteyl.com"
+ ]
+ },
+ "Komli": {
+ "properties": [
+ "komli.com"
+ ],
+ "resources": [
+ "komli.com"
+ ]
+ },
+ "Konduto": {
+ "properties": [
+ "konduto.com"
+ ],
+ "resources": [
+ "k-analytix.com",
+ "konduto.com"
+ ]
+ },
+ "Kontera": {
+ "properties": [
+ "kontera.com"
+ ],
+ "resources": [
+ "kontera.com"
+ ]
+ },
+ "Korrelate": {
+ "properties": [
+ "korrelate.com"
+ ],
+ "resources": [
+ "adsummos.com",
+ "adsummos.net",
+ "korrelate.com"
+ ]
+ },
+ "Krux": {
+ "properties": [
+ "krux.com",
+ "kruxdigital.com"
+ ],
+ "resources": [
+ "krux.com",
+ "kruxdigital.com",
+ "krxd.net"
+ ]
+ },
+ "Lakana": {
+ "properties": [
+ "lakana.com"
+ ],
+ "resources": [
+ "ibsys.com",
+ "lakana.com"
+ ]
+ },
+ "Layer-Ad.org": {
+ "properties": [
+ "layer-ad.org"
+ ],
+ "resources": [
+ "layer-ad.org"
+ ]
+ },
+ "Layer Ads": {
+ "properties": [
+ "layer-ads.net"
+ ],
+ "resources": [
+ "layer-ads.net"
+ ]
+ },
+ "LeadBolt": {
+ "properties": [
+ "leadbolt.com"
+ ],
+ "resources": [
+ "leadbolt.com"
+ ]
+ },
+ "LeadForensics": {
+ "properties": [
+ "leadforensics.com"
+ ],
+ "resources": [
+ "leadforensics.com"
+ ]
+ },
+ "LeadFormix": {
+ "properties": [
+ "calliduscloud.com",
+ "leadforce1.com",
+ "leadformix.com"
+ ],
+ "resources": [
+ "calliduscloud.com",
+ "leadforce1.com",
+ "leadformix.com"
+ ]
+ },
+ "LeadsHub": {
+ "properties": [
+ "ztsrv.com"
+ ],
+ "resources": [
+ "ztsrv.com"
+ ]
+ },
+ "LeanPlum": {
+ "properties": [
+ "leanplum.com"
+ ],
+ "resources": [
+ "leanplum.com"
+ ]
+ },
+ "Legolas Media": {
+ "properties": [
+ "legolas-media.com"
+ ],
+ "resources": [
+ "legolas-media.com"
+ ]
+ },
+ "Levexis": {
+ "properties": [
+ "levexis.com"
+ ],
+ "resources": [
+ "levexis.com"
+ ]
+ },
+ "Lexos Media": {
+ "properties": [
+ "adbull.com",
+ "lexosmedia.com"
+ ],
+ "resources": [
+ "adbull.com",
+ "lexosmedia.com"
+ ]
+ },
+ "LifeStreet": {
+ "properties": [
+ "lfstmedia.com",
+ "lifestreetmedia.com"
+ ],
+ "resources": [
+ "lfstmedia.com",
+ "lifestreetmedia.com"
+ ]
+ },
+ "Limelight Networks": {
+ "properties": [
+ "limelight.com"
+ ],
+ "resources": [
+ "clickability.com",
+ "limelight.com",
+ "llnwd.net"
+ ]
+ },
+ "LineZing": {
+ "properties": [
+ "linezing.com"
+ ],
+ "resources": [
+ "linezing.com"
+ ]
+ },
+ "LinkConnector": {
+ "properties": [
+ "linkconnector.com"
+ ],
+ "resources": [
+ "linkconnector.com"
+ ]
+ },
+ "LinkedIn": {
+ "properties": [
+ "linkedin.com"
+ ],
+ "resources": [
+ "licdn.com",
+ "linkedin.com"
+ ]
+ },
+ "LinkShare": {
+ "properties": [
+ "rakutenmarketing.com"
+ ],
+ "resources": [
+ "linkshare.com",
+ "linksynergy.com",
+ "rakutenmarketing.com"
+ ]
+ },
+ "Linkz": {
+ "properties": [
+ "linkz.net"
+ ],
+ "resources": [
+ "linkz.net"
+ ]
+ },
+ "Listrak": {
+ "properties": [
+ "listrak.com",
+ "listrakbi.com"
+ ],
+ "resources": [
+ "listrak.com",
+ "listrakbi.com"
+ ]
+ },
+ "LiveIntent": {
+ "properties": [
+ "liveintent.com"
+ ],
+ "resources": [
+ "liadm.com",
+ "liveintent.com"
+ ]
+ },
+ "LiveInternet": {
+ "properties": [
+ "liveinternet.ru",
+ "yadro.ru"
+ ],
+ "resources": [
+ "liveinternet.ru",
+ "yadro.ru"
+ ]
+ },
+ "LivePerson": {
+ "properties": [
+ "liveperson.com"
+ ],
+ "resources": [
+ "liveperson.com",
+ "liveperson.net",
+ "nuconomy.com"
+ ]
+ },
+ "LiveRail": {
+ "properties": [
+ "liverail.com"
+ ],
+ "resources": [
+ "liverail.com"
+ ]
+ },
+ "LiveRamp": {
+ "properties": [
+ "liveramp.com"
+ ],
+ "resources": [
+ "liveramp.com",
+ "tvpixel.com"
+ ]
+ },
+ "LKQD": {
+ "properties": [
+ "lkqd.com",
+ "lkqd.net"
+ ],
+ "resources": [
+ "lkqd.com",
+ "lkqd.net"
+ ]
+ },
+ "Local Yokel Media": {
+ "properties": [
+ "localyokelmedia.com"
+ ],
+ "resources": [
+ "localyokelmedia.com"
+ ]
+ },
+ "Localytics": {
+ "properties": [
+ "localytics.com"
+ ],
+ "resources": [
+ "localytics.com"
+ ]
+ },
+ "LockerDome": {
+ "properties": [
+ "lockerdome.com"
+ ],
+ "resources": [
+ "lockerdome.com"
+ ]
+ },
+ "Lockerz": {
+ "properties": [
+ "lockerz.com"
+ ],
+ "resources": [
+ "lockerz.com"
+ ]
+ },
+ "Logdy": {
+ "properties": [
+ "logdy.com"
+ ],
+ "resources": [
+ "logdy.com"
+ ]
+ },
+ "Longboard Media": {
+ "properties": [
+ "longboardmedia.com"
+ ],
+ "resources": [
+ "longboardmedia.com"
+ ]
+ },
+ "LongTail Video": {
+ "properties": [
+ "jwplayer.com"
+ ],
+ "resources": [
+ "longtailvideo.com",
+ "ltassrv.com"
+ ]
+ },
+ "Loomia": {
+ "properties": [
+ "loomia.com"
+ ],
+ "resources": [
+ "loomia.com"
+ ]
+ },
+ "LoopFuse": {
+ "properties": [
+ "lfov.net",
+ "loopfuse.net"
+ ],
+ "resources": [
+ "lfov.net",
+ "loopfuse.net"
+ ]
+ },
+ "LoopMe": {
+ "properties": [
+ "loopme.com"
+ ],
+ "resources": [
+ "loopme.com"
+ ]
+ },
+ "Lotame": {
+ "properties": [
+ "crwdcntrl.net",
+ "lotame.com"
+ ],
+ "resources": [
+ "crwdcntrl.net",
+ "lotame.com"
+ ]
+ },
+ "LotLinx": {
+ "properties": [
+ "lotlinx.com"
+ ],
+ "resources": [
+ "lotlinx.com"
+ ]
+ },
+ "Lower My Bills": {
+ "properties": [
+ "lowermybills.com"
+ ],
+ "resources": [
+ "lowermybills.com"
+ ]
+ },
+ "lptracker": {
+ "properties": [
+ "lptracker.io"
+ ],
+ "resources": [
+ "lptracker.io"
+ ]
+ },
+ "LucidMedia": {
+ "properties": [
+ "lucidmedia.com"
+ ],
+ "resources": [
+ "lucidmedia.com"
+ ]
+ },
+ "LuckyOrange": {
+ "properties": [
+ "luckyorange.com"
+ ],
+ "resources": [
+ "luckyorange.com",
+ "luckyorange.net"
+ ]
+ },
+ "Lynchpin": {
+ "properties": [
+ "lynchpin.com"
+ ],
+ "resources": [
+ "lynchpin.com",
+ "lypn.com"
+ ]
+ },
+ "Lyris": {
+ "properties": [
+ "aurea.com"
+ ],
+ "resources": [
+ "aurea.com",
+ "clicktracks.com",
+ "lyris.com"
+ ]
+ },
+ "Lytiks": {
+ "properties": [
+ "lytiks.com"
+ ],
+ "resources": [
+ "lytiks.com"
+ ]
+ },
+ "m6d": {
+ "properties": [
+ "dstillery.com"
+ ],
+ "resources": [
+ "dstillery.com",
+ "m6d.com",
+ "media6degrees.com"
+ ]
+ },
+ "Madhouse": {
+ "properties": [
+ "madhouse.cn"
+ ],
+ "resources": [
+ "madhouse.cn"
+ ]
+ },
+ "Madison Logic": {
+ "properties": [
+ "dinclinx.com",
+ "madisonlogic.com"
+ ],
+ "resources": [
+ "dinclinx.com",
+ "madisonlogic.com"
+ ]
+ },
+ "madvertise": {
+ "properties": [
+ "madvertise.com"
+ ],
+ "resources": [
+ "madvertise.com"
+ ]
+ },
+ "Magnetic": {
+ "properties": [
+ "domdex.net",
+ "magnetic.com"
+ ],
+ "resources": [
+ "domdex.com",
+ "domdex.net",
+ "magnetic.com",
+ "qjex.net"
+ ]
+ },
+ "Magnify360": {
+ "properties": [
+ "dialogmgr.com",
+ "magnify360.com"
+ ],
+ "resources": [
+ "dialogmgr.com",
+ "magnify360.com"
+ ]
+ },
+ "MailChimp": {
+ "properties": [
+ "campaign-archive1.com",
+ "mailchi.mp",
+ "mailchimp.com"
+ ],
+ "resources": [
+ "campaign-archive1.com",
+ "list-manage.com",
+ "mailchi.mp",
+ "mailchimp.com"
+ ]
+ },
+ "Mail.Ru": {
+ "properties": [
+ "list.ru",
+ "mail.ru"
+ ],
+ "resources": [
+ "list.ru",
+ "mail.ru"
+ ]
+ },
+ "Manifest": {
+ "properties": [
+ "bannerbank.ru",
+ "manifest.ru"
+ ],
+ "resources": [
+ "bannerbank.ru",
+ "manifest.ru"
+ ]
+ },
+ "Marchex": {
+ "properties": [
+ "industrybrains.com",
+ "marchex.com"
+ ],
+ "resources": [
+ "industrybrains.com",
+ "marchex.com"
+ ]
+ },
+ "Marimedia": {
+ "properties": [
+ "marimedia.net"
+ ],
+ "resources": [
+ "marimedia.net"
+ ]
+ },
+ "MarketGid": {
+ "properties": [
+ "dt00.net",
+ "dt07.net",
+ "marketgid.com"
+ ],
+ "resources": [
+ "dt00.net",
+ "dt07.net",
+ "marketgid.com"
+ ]
+ },
+ "Marketo": {
+ "properties": [
+ "marketo.com"
+ ],
+ "resources": [
+ "marketo.com",
+ "marketo.net"
+ ]
+ },
+ "Markit": {
+ "properties": [
+ "markit.com",
+ "wsod.com"
+ ],
+ "resources": [
+ "markit.com",
+ "wsod.com"
+ ]
+ },
+ "MarkMonitor": {
+ "properties": [
+ "9c9media.ca",
+ "markmonitor.com"
+ ],
+ "resources": [
+ "9c9media.ca",
+ "markmonitor.com"
+ ]
+ },
+ "Marktest": {
+ "properties": [
+ "marktest.com",
+ "marktest.pt"
+ ],
+ "resources": [
+ "marktest.com",
+ "marktest.pt"
+ ]
+ },
+ "Martini Media": {
+ "properties": [
+ "martiniadnetwork.com"
+ ],
+ "resources": [
+ "martiniadnetwork.com",
+ "martinimedianetwork.com"
+ ]
+ },
+ "mashero": {
+ "properties": [
+ "mashero.com"
+ ],
+ "resources": [
+ "mashero.com"
+ ]
+ },
+ "MashLogic": {
+ "properties": [
+ "mashlogic.com"
+ ],
+ "resources": [
+ "mashlogic.com"
+ ]
+ },
+ "Match.com": {
+ "properties": [
+ "chemistry.com",
+ "match.com"
+ ],
+ "resources": [
+ "chemistry.com",
+ "match.com",
+ "meetic-partners.com"
+ ]
+ },
+ "Matomy": {
+ "properties": [
+ "matomy.com"
+ ],
+ "resources": [
+ "adnetinteractive.com",
+ "adsmarket.com",
+ "matomy.com",
+ "matomymarket.com",
+ "matomymedia.com",
+ "mediawhiz.com",
+ "optimatic.com",
+ "xtendmedia.com"
+ ]
+ },
+ "MaxBounty": {
+ "properties": [
+ "maxbounty.com",
+ "mb01.com"
+ ],
+ "resources": [
+ "maxbounty.com",
+ "mb01.com"
+ ]
+ },
+ "MaxMind": {
+ "properties": [
+ "maxmind.com"
+ ],
+ "resources": [
+ "maxmind.com",
+ "mmapiws.com"
+ ]
+ },
+ "MaxPoint": {
+ "properties": [
+ "maxpointinteractive.com",
+ "maxusglobal.com",
+ "mxptint.net"
+ ],
+ "resources": [
+ "maxpointinteractive.com",
+ "maxusglobal.com",
+ "mxptint.net"
+ ]
+ },
+ "McAfee": {
+ "properties": [
+ "mcafee.com",
+ "mcafeesecure.com"
+ ],
+ "resources": [
+ "mcafee.com",
+ "mcafeesecure.com",
+ "scanalert.com"
+ ]
+ },
+ "MdotM": {
+ "properties": [
+ "mdotm.com"
+ ],
+ "resources": [
+ "mdotm.com"
+ ]
+ },
+ "MediaBrix": {
+ "properties": [
+ "mediabrix.com"
+ ],
+ "resources": [
+ "mediabrix.com"
+ ]
+ },
+ "MediaCom": {
+ "properties": [
+ "mediacom.com"
+ ],
+ "resources": [
+ "mediacom.com"
+ ]
+ },
+ "mediaFORGE": {
+ "properties": [
+ "mediaforge.com"
+ ],
+ "resources": [
+ "mediaforge.com"
+ ]
+ },
+ "Medialets": {
+ "properties": [
+ "medialets.com"
+ ],
+ "resources": [
+ "medialets.com"
+ ]
+ },
+ "MediaMath": {
+ "properties": [
+ "mediamath.com"
+ ],
+ "resources": [
+ "adroitinteractive.com",
+ "designbloxlive.com",
+ "mathtag.com",
+ "mediamath.com"
+ ]
+ },
+ "Médiamétrie-eStat": {
+ "properties": [
+ "mediametrie-estat.com"
+ ],
+ "resources": [
+ "estat.com",
+ "mediametrie-estat.com"
+ ]
+ },
+ "media.net": {
+ "properties": [
+ "media.net"
+ ],
+ "resources": [
+ "media.net"
+ ]
+ },
+ "Mediaocean": {
+ "properties": [
+ "adbuyer.com",
+ "mediaocean.com"
+ ],
+ "resources": [
+ "adbuyer.com",
+ "mediaocean.com"
+ ]
+ },
+ "MediaShakers": {
+ "properties": [
+ "media-servers.net",
+ "mediashakers.com"
+ ],
+ "resources": [
+ "media-servers.net",
+ "mediashakers.com"
+ ]
+ },
+ "MediaTrust": {
+ "properties": [
+ "mediatrust.com"
+ ],
+ "resources": [
+ "mediatrust.com"
+ ]
+ },
+ "Medicx Media Solutions": {
+ "properties": [
+ "medicxmedia.com"
+ ],
+ "resources": [
+ "medicxmedia.com"
+ ]
+ },
+ "Meebo": {
+ "properties": [
+ "meebo.com"
+ ],
+ "resources": [
+ "meebo.com",
+ "meebocdn.net"
+ ]
+ },
+ "MegaIndex": {
+ "properties": [
+ "megaindex.ru"
+ ],
+ "resources": [
+ "megaindex.ru"
+ ]
+ },
+ "Mercadopago": {
+ "properties": [
+ "mercadolibre.cl",
+ "mercadolibre.co.cr",
+ "mercadolibre.com",
+ "mercadolibre.com.ar",
+ "mercadolibre.com.bo",
+ "mercadolibre.com.co",
+ "mercadolibre.com.do",
+ "mercadolibre.com.ec",
+ "mercadolibre.com.gt",
+ "mercadolibre.com.hn",
+ "mercadolibre.com.mx",
+ "mercadolibre.com.ni",
+ "mercadolibre.com.pa",
+ "mercadolibre.com.pe",
+ "mercadolibre.com.py",
+ "mercadolibre.com.sv",
+ "mercadolibre.com.uy",
+ "mercadolibre.com.ve",
+ "mercadolivre.com.br",
+ "mercadopago.com"
+ ],
+ "resources": [
+ "mercadopago.com"
+ ]
+ },
+ "Mercent": {
+ "properties": [
+ "mercent.com"
+ ],
+ "resources": [
+ "mercent.com"
+ ]
+ },
+ "MerchantAdvantage": {
+ "properties": [
+ "merchantadvantage.com"
+ ],
+ "resources": [
+ "merchantadvantage.com"
+ ]
+ },
+ "Merchenta": {
+ "properties": [
+ "merchenta.com"
+ ],
+ "resources": [
+ "merchenta.com"
+ ]
+ },
+ "Merkle": {
+ "properties": [
+ "merkleinc.com",
+ "rkdms.com"
+ ],
+ "resources": [
+ "merkleinc.com",
+ "rimmkaufman.com",
+ "rkdms.com"
+ ]
+ },
+ "Meta Network": {
+ "properties": [
+ "metanetwork.com"
+ ],
+ "resources": [
+ "metanetwork.com"
+ ]
+ },
+ "Meteor": {
+ "properties": [
+ "meteorsolutions.com"
+ ],
+ "resources": [
+ "meteorsolutions.com"
+ ]
+ },
+ "MetrixLab": {
+ "properties": [
+ "crm-metrix.com",
+ "customerconversio.com",
+ "metrixlab.com",
+ "opinionbar.com"
+ ],
+ "resources": [
+ "adoftheyear.com",
+ "crm-metrix.com",
+ "customerconversio.com",
+ "metrixlab.com",
+ "opinionbar.com"
+ ]
+ },
+ "MicroAd": {
+ "properties": [
+ "microad.jp",
+ "www.microad.jp"
+ ],
+ "resources": [
+ "microad.jp",
+ "www.microad.jp"
+ ]
+ },
+ "Microsoft": {
+ "properties": [
+ "acompli.net",
+ "aka.ms",
+ "azure.com",
+ "azure.net",
+ "azurerms.com",
+ "bing.com",
+ "cloudappsecurity.com",
+ "gamesforwindows.com",
+ "getgamesmart.com",
+ "gfx.ms",
+ "healthvault.com",
+ "hockeyapp.net",
+ "ieaddons.com",
+ "iegallery.com",
+ "live.com",
+ "microsoft.com",
+ "microsoftalumni.com",
+ "microsoftalumni.org",
+ "microsoftazuread-sso.com",
+ "microsoftedgeinsiders.com",
+ "microsoftonline-p.com",
+ "microsoftonline-p.net",
+ "microsoftonline.com",
+ "microsoftstore.com",
+ "microsoftstream.com",
+ "msappproxy.net",
+ "msft.net",
+ "msftidentity.com",
+ "msidentity.com",
+ "msn.com",
+ "o365weve.com",
+ "oaspapps.com",
+ "office.com",
+ "office365.com",
+ "officelive.com",
+ "onedrive.com",
+ "onenote.com",
+ "outlook.com",
+ "outlookmobile.com",
+ "phonefactor.net",
+ "s-msn.com",
+ "sfx.ms",
+ "sharepoint.com",
+ "skype.com",
+ "skypeforbusiness.com",
+ "staffhub.ms",
+ "sway-extensions.com",
+ "sway.com",
+ "trafficmanager.net",
+ "virtualearth.net",
+ "visualstudio.com",
+ "windows.net",
+ "windowsazure.com",
+ "windowsphone.com",
+ "worldwidetelescope.org",
+ "wunderlist.com",
+ "xbox.com",
+ "yammer.com"
+ ],
+ "resources": [
+ "aadrm.com",
+ "adbureau.net",
+ "adecn.com",
+ "aquantive.com",
+ "aspnetcdn.com",
+ "assets-yammer.com",
+ "azure.com",
+ "azureedge.net",
+ "bing.com",
+ "cloudapp.net",
+ "gamesforwindows.com",
+ "getgamesmart.com",
+ "gfx.ms",
+ "healthvault.com",
+ "live.com",
+ "microsoft.com",
+ "microsoftazuread-sso.com",
+ "microsoftonline-p.com",
+ "microsoftonline-p.net",
+ "microsoftonline.com",
+ "microsoftstore.com",
+ "msads.net",
+ "msauthimages.net",
+ "msecnd.net",
+ "msedge.net",
+ "msndirect.com",
+ "msocdn.com",
+ "netconversions.com",
+ "oaspapps.com",
+ "office.com",
+ "office.net",
+ "officelive.com",
+ "onenote.net",
+ "onestore.ms",
+ "onmicrosoft.com",
+ "outlook.com",
+ "roiservice.com",
+ "s-msn.com",
+ "sfbassets.com",
+ "sharepoint.com",
+ "skype.com",
+ "skypeassets.com",
+ "sway-cdn.com",
+ "sway-extensions.com",
+ "windows.net",
+ "windowsazure.com",
+ "yammerusercontent.com"
+ ]
+ },
+ "Millennial Media": {
+ "properties": [
+ "decktrade.com",
+ "millennialmedia.com",
+ "mydas.mobi"
+ ],
+ "resources": [
+ "decktrade.com",
+ "millennialmedia.com",
+ "mydas.mobi"
+ ]
+ },
+ "Mindset Media": {
+ "properties": [
+ "mindset-media.com"
+ ],
+ "resources": [
+ "mindset-media.com",
+ "mmismm.com"
+ ]
+ },
+ "MinerAlt": {
+ "properties": [
+ "mineralt.io",
+ "vidzi.nu",
+ "vidzi.tv"
+ ],
+ "resources": [
+ "1q2w3.website",
+ "analytics.blue",
+ "aster18cdn.nl",
+ "belicimo.pw",
+ "besstahete.info",
+ "dinorslick.icu",
+ "feesocrald.com",
+ "gramombird.com",
+ "istlandoll.com",
+ "mepirtedic.com",
+ "mineralt.io",
+ "pampopholf.com",
+ "tercabilis.info",
+ "tulip18.com",
+ "vidzi.tv",
+ "yololike.space"
+ ]
+ },
+ "Minescripts": {
+ "properties": [
+ "minescripts.info"
+ ],
+ "resources": [
+ "minescripts.info",
+ "sslverify.info"
+ ]
+ },
+ "MineXMR": {
+ "properties": [
+ "minexmr.stream"
+ ],
+ "resources": [
+ "minexmr.stream"
+ ]
+ },
+ "Mirando": {
+ "properties": [
+ "mirando.de"
+ ],
+ "resources": [
+ "mirando.de"
+ ]
+ },
+ "Mixpanel": {
+ "properties": [
+ "mixpanel.com"
+ ],
+ "resources": [
+ "mixpanel.com",
+ "mxpnl.com"
+ ]
+ },
+ "Mixpo": {
+ "properties": [
+ "mixpo.com"
+ ],
+ "resources": [
+ "mixpo.com"
+ ]
+ },
+ "Moat": {
+ "properties": [
+ "moat.com",
+ "moatads.com"
+ ],
+ "resources": [
+ "moat.com",
+ "moatads.com"
+ ]
+ },
+ "MobFox": {
+ "properties": [
+ "mobfox.com"
+ ],
+ "resources": [
+ "mobfox.com"
+ ]
+ },
+ "Mobials": {
+ "properties": [
+ "mobials.com"
+ ],
+ "resources": [
+ "mobials.com"
+ ]
+ },
+ "MobileAdTrading": {
+ "properties": [
+ "mobileadtrading.com"
+ ],
+ "resources": [
+ "mobileadtrading.com"
+ ]
+ },
+ "Mobile Meteor": {
+ "properties": [
+ "mobilemeteor.com"
+ ],
+ "resources": [
+ "mobilemeteor.com",
+ "showmeinn.com"
+ ]
+ },
+ "Mobile Storm": {
+ "properties": [
+ "mobilestorm.com"
+ ],
+ "resources": [
+ "mobilestorm.com"
+ ]
+ },
+ "MobVision": {
+ "properties": [
+ "admoda.com"
+ ],
+ "resources": [
+ "admoda.com",
+ "mobvision.com"
+ ]
+ },
+ "Mocean Mobile": {
+ "properties": [
+ "moceanmobile.com"
+ ],
+ "resources": [
+ "moceanmobile.com"
+ ]
+ },
+ "Mochila": {
+ "properties": [
+ "mochila.com"
+ ],
+ "resources": [
+ "mochila.com"
+ ]
+ },
+ "Mojiva": {
+ "properties": [
+ "mojiva.com"
+ ],
+ "resources": [
+ "mojiva.com"
+ ]
+ },
+ "Monetate": {
+ "properties": [
+ "monetate.com",
+ "monetate.net"
+ ],
+ "resources": [
+ "monetate.com",
+ "monetate.net"
+ ]
+ },
+ "MONETIZEdigital": {
+ "properties": [
+ "cpalead.com"
+ ],
+ "resources": [
+ "cpalead.com"
+ ]
+ },
+ "Monetize More": {
+ "properties": [
+ "monetizemore.com"
+ ],
+ "resources": [
+ "monetizemore.com"
+ ]
+ },
+ "Mongoose Metrics": {
+ "properties": [
+ "mongoosemetrics.com"
+ ],
+ "resources": [
+ "mongoosemetrics.com"
+ ]
+ },
+ "Monitus": {
+ "properties": [
+ "monitus.net"
+ ],
+ "resources": [
+ "monitus.net"
+ ]
+ },
+ "Monoloop": {
+ "properties": [
+ "monoloop.com"
+ ],
+ "resources": [
+ "monoloop.com"
+ ]
+ },
+ "Monster": {
+ "properties": [
+ "monster.com"
+ ],
+ "resources": [
+ "monster.com"
+ ]
+ },
+ "Moolah Media": {
+ "properties": [
+ "moolah-media.com",
+ "moolahmedia.com"
+ ],
+ "resources": [
+ "moolah-media.com",
+ "moolahmedia.com"
+ ]
+ },
+ "MoPub": {
+ "properties": [
+ "mopub.com"
+ ],
+ "resources": [
+ "mopub.com"
+ ]
+ },
+ "motigo": {
+ "properties": [
+ "motigo.com"
+ ],
+ "resources": [
+ "motigo.com",
+ "nedstatbasic.net"
+ ]
+ },
+ "Mouseflow": {
+ "properties": [
+ "mouseflow.com"
+ ],
+ "resources": [
+ "mouseflow.com"
+ ]
+ },
+ "MovieLush.com": {
+ "properties": [
+ "affbuzzads.com",
+ "movielush.com"
+ ],
+ "resources": [
+ "affbuzzads.com",
+ "movielush.com"
+ ]
+ },
+ "Multiple Stream Media": {
+ "properties": [
+ "adclickmedia.com",
+ "multiplestreammktg.com"
+ ],
+ "resources": [
+ "adclickmedia.com",
+ "multiplestreammktg.com"
+ ]
+ },
+ "MUNDO Media": {
+ "properties": [
+ "mundomedia.com",
+ "silver-path.com"
+ ],
+ "resources": [
+ "mundomedia.com",
+ "silver-path.com"
+ ]
+ },
+ "MyCounter": {
+ "properties": [
+ "mycounter.com.ua"
+ ],
+ "resources": [
+ "mycounter.com.ua"
+ ]
+ },
+ "MyPagerank.Net": {
+ "properties": [
+ "mypagerank.net"
+ ],
+ "resources": [
+ "mypagerank.net"
+ ]
+ },
+ "MyPressPlus": {
+ "properties": [
+ "mypressplus.com",
+ "ppjol.net"
+ ],
+ "resources": [
+ "mypressplus.com",
+ "ppjol.net"
+ ]
+ },
+ "Mystighty": {
+ "properties": [
+ "mystighty.info"
+ ],
+ "resources": [
+ "mystighty.info",
+ "sweeterge.info"
+ ]
+ },
+ "myThings": {
+ "properties": [
+ "mythings.com",
+ "mythingsmedia.com"
+ ],
+ "resources": [
+ "mythings.com",
+ "mythingsmedia.com"
+ ]
+ },
+ "MyWebGrocer": {
+ "properties": [
+ "mywebgrocer.com"
+ ],
+ "resources": [
+ "mywebgrocer.com"
+ ]
+ },
+ "Nanigans": {
+ "properties": [
+ "nanigans.com"
+ ],
+ "resources": [
+ "nanigans.com"
+ ]
+ },
+ "Narrative": {
+ "properties": [
+ "narrative.io"
+ ],
+ "resources": [
+ "narrative.io"
+ ]
+ },
+ "NativeAds": {
+ "properties": [
+ "nativeads.com"
+ ],
+ "resources": [
+ "nativeads.com"
+ ]
+ },
+ "Nativo": {
+ "properties": [
+ "nativo.com",
+ "postrelease.com"
+ ],
+ "resources": [
+ "nativo.com",
+ "postrelease.com"
+ ]
+ },
+ "Navegg": {
+ "properties": [
+ "navdmp.com",
+ "navegg.com"
+ ],
+ "resources": [
+ "navdmp.com",
+ "navegg.com"
+ ]
+ },
+ "NDN": {
+ "properties": [
+ "newsinc.com"
+ ],
+ "resources": [
+ "newsinc.com"
+ ]
+ },
+ "Negishim": {
+ "properties": [
+ "negishim.org"
+ ],
+ "resources": [
+ "negishim.org"
+ ]
+ },
+ "NeroHut": {
+ "properties": [
+ "nerohut.com"
+ ],
+ "resources": [
+ "nerohut.com",
+ "nhsrv.cf"
+ ]
+ },
+ "NetAffiliation": {
+ "properties": [
+ "netaffiliation.com"
+ ],
+ "resources": [
+ "netaffiliation.com"
+ ]
+ },
+ "Net Applications": {
+ "properties": [
+ "netapplications.com"
+ ],
+ "resources": [
+ "hitsprocessor.com",
+ "netapplications.com"
+ ]
+ },
+ "NetBina": {
+ "properties": [
+ "netbina.com"
+ ],
+ "resources": [
+ "netbina.com"
+ ]
+ },
+ "NetElixir": {
+ "properties": [
+ "adelixir.com",
+ "netelixir.com"
+ ],
+ "resources": [
+ "adelixir.com",
+ "netelixir.com"
+ ]
+ },
+ "Netmining": {
+ "properties": [
+ "netmining.com",
+ "netmng.com"
+ ],
+ "resources": [
+ "netmining.com",
+ "netmng.com"
+ ]
+ },
+ "Net-Results": {
+ "properties": [
+ "net-results.com",
+ "nr7.us"
+ ],
+ "resources": [
+ "cdnma.com",
+ "net-results.com",
+ "nr7.us"
+ ]
+ },
+ "NetSeer": {
+ "properties": [
+ "netseer.com"
+ ],
+ "resources": [
+ "netseer.com"
+ ]
+ },
+ "NetShelter": {
+ "properties": [
+ "ziffdavistech.com"
+ ],
+ "resources": [
+ "netshelter.com",
+ "netshelter.net",
+ "ziffdavistech.com"
+ ]
+ },
+ "Neustar": {
+ "properties": [
+ "adadvisor.net",
+ "home.neustar",
+ "neustar.biz"
+ ],
+ "resources": [
+ "adadvisor.net",
+ "neustar.biz"
+ ]
+ },
+ "New Relic": {
+ "properties": [
+ "newrelic.com"
+ ],
+ "resources": [
+ "newrelic.com",
+ "nr-data.net"
+ ]
+ },
+ "NewsRight": {
+ "properties": [
+ "apnewsregistry.com",
+ "newsright.com"
+ ],
+ "resources": [
+ "apnewsregistry.com",
+ "newsright.com"
+ ]
+ },
+ "newtention": {
+ "properties": [
+ "newtention.de",
+ "newtention.net",
+ "newtentionassets.net"
+ ],
+ "resources": [
+ "newtention.de",
+ "newtention.net",
+ "newtentionassets.net"
+ ]
+ },
+ "Nexage": {
+ "properties": [
+ "nexage.com"
+ ],
+ "resources": [
+ "nexage.com"
+ ]
+ },
+ "Nextag": {
+ "properties": [
+ "nextag.com"
+ ],
+ "resources": [
+ "nextag.com"
+ ]
+ },
+ "NextPerformance": {
+ "properties": [
+ "nextperf.com",
+ "nextperformance.com",
+ "nxtck.com"
+ ],
+ "resources": [
+ "nextperf.com",
+ "nextperformance.com",
+ "nxtck.com"
+ ]
+ },
+ "NextSTAT": {
+ "properties": [
+ "nextstat.com"
+ ],
+ "resources": [
+ "nextstat.com"
+ ]
+ },
+ "Nielsen": {
+ "properties": [
+ "glanceguide.com",
+ "imrworldwide.com",
+ "imrworldwide.net",
+ "nielsen.com"
+ ],
+ "resources": [
+ "glanceguide.com",
+ "imrworldwide.com",
+ "imrworldwide.net",
+ "nielsen.com"
+ ]
+ },
+ "Ninua": {
+ "properties": [
+ "networkedblogs.com",
+ "ninua.com"
+ ],
+ "resources": [
+ "networkedblogs.com",
+ "ninua.com"
+ ]
+ },
+ "Nokta": {
+ "properties": [
+ "noktamedya.com",
+ "virgul.com"
+ ],
+ "resources": [
+ "noktamedya.com",
+ "virgul.com"
+ ]
+ },
+ "NowSpots": {
+ "properties": [
+ "nowspots.com"
+ ],
+ "resources": [
+ "nowspots.com"
+ ]
+ },
+ "nrelate": {
+ "properties": [
+ "nrelate.com"
+ ],
+ "resources": [
+ "nrelate.com"
+ ]
+ },
+ "NuDataSecurity": {
+ "properties": [
+ "nudatasecurity.com"
+ ],
+ "resources": [
+ "nudatasecurity.com"
+ ]
+ },
+ "Nuffnang": {
+ "properties": [
+ "nuffnang.com",
+ "nuffnang.com.my",
+ "www.nuffnang.com.my"
+ ],
+ "resources": [
+ "nuffnang.com",
+ "nuffnang.com.my",
+ "www.nuffnang.com.my"
+ ]
+ },
+ "nugg.ad": {
+ "properties": [
+ "nugg.ad"
+ ],
+ "resources": [
+ "nugg.ad",
+ "nuggad.net"
+ ]
+ },
+ "nurago": {
+ "properties": [
+ "sensic.net"
+ ],
+ "resources": [
+ "nurago.com",
+ "nurago.de",
+ "sensic.net"
+ ]
+ },
+ "Oberon Media": {
+ "properties": [
+ "iwin.com"
+ ],
+ "resources": [
+ "blaze.com",
+ "iwin.com",
+ "oberon-media.com"
+ ]
+ },
+ "Observer": {
+ "properties": [
+ "observerapp.com"
+ ],
+ "resources": [
+ "observerapp.com"
+ ]
+ },
+ "Ohana Media": {
+ "properties": [
+ "adohana.com",
+ "ohana-media.com",
+ "ohanaqb.com"
+ ],
+ "resources": [
+ "adohana.com",
+ "ohana-media.com",
+ "ohanaqb.com"
+ ]
+ },
+ "Omnicom Group": {
+ "properties": [
+ "accuenmedia.com",
+ "omnicomgroup.com"
+ ],
+ "resources": [
+ "accuenmedia.com",
+ "omnicomgroup.com",
+ "p-td.com"
+ ]
+ },
+ "onAd": {
+ "properties": [
+ "onad.eu"
+ ],
+ "resources": [
+ "onad.eu"
+ ]
+ },
+ "OnAudience": {
+ "properties": [
+ "behavioralengine.com",
+ "onaudience.com"
+ ],
+ "resources": [
+ "behavioralengine.com",
+ "onaudience.com"
+ ]
+ },
+ "Onclusive": {
+ "properties": [
+ "onclusive.com"
+ ],
+ "resources": [
+ "airpr.com"
+ ]
+ },
+ "OneAd": {
+ "properties": [
+ "onead.com.tw"
+ ],
+ "resources": [
+ "guoshipartners.com",
+ "onevision.com.tw"
+ ]
+ },
+ "One iota": {
+ "properties": [
+ "itsoneiota.com",
+ "oneiota.co.uk"
+ ],
+ "resources": [
+ "itsoneiota.com",
+ "oneiota.co.uk"
+ ]
+ },
+ "OneStat": {
+ "properties": [
+ "onestat.com"
+ ],
+ "resources": [
+ "onestat.com"
+ ]
+ },
+ "Oneupweb": {
+ "properties": [
+ "oneupweb.com",
+ "sodoit.com"
+ ],
+ "resources": [
+ "oneupweb.com",
+ "sodoit.com"
+ ]
+ },
+ "OnlineMetrix": {
+ "properties": [
+ "online-metrix.net"
+ ],
+ "resources": [
+ "online-metrix.net"
+ ]
+ },
+ "Ooyala": {
+ "properties": [
+ "ooyala.com"
+ ],
+ "resources": [
+ "oo4.com",
+ "ooyala.com"
+ ]
+ },
+ "Open New Media": {
+ "properties": [
+ "onm.de"
+ ],
+ "resources": [
+ "onm.de"
+ ]
+ },
+ "Openstat": {
+ "properties": [
+ "openstat.com"
+ ],
+ "resources": [
+ "openstat.com",
+ "openstat.ru",
+ "spylog.com"
+ ]
+ },
+ "Opentracker": {
+ "properties": [
+ "opentracker.net"
+ ],
+ "resources": [
+ "opentracker.net"
+ ]
+ },
+ "OpenX": {
+ "properties": [
+ "openx.com",
+ "openx.net"
+ ],
+ "resources": [
+ "liftdna.com",
+ "openx.com",
+ "openx.net",
+ "openx.org",
+ "openxenterprise.com",
+ "servedbyopenx.com"
+ ]
+ },
+ "Opera": {
+ "properties": [
+ "opera.com"
+ ],
+ "resources": [
+ "mobiletheory.com",
+ "opera.com"
+ ]
+ },
+ "Opolen": {
+ "properties": [
+ "opolen.com.br"
+ ],
+ "resources": [
+ "opolen.com.br"
+ ]
+ },
+ "OPT": {
+ "properties": [
+ "www.opt.ne.jp"
+ ],
+ "resources": [
+ "advg.jp",
+ "opt.ne.jp",
+ "p-advg.com",
+ "www.opt.ne.jp"
+ ]
+ },
+ "Optify": {
+ "properties": [
+ "optify.net"
+ ],
+ "resources": [
+ "optify.net"
+ ]
+ },
+ "Optimal": {
+ "properties": [
+ "bn.co"
+ ],
+ "resources": [
+ "cpmadvisors.com",
+ "cpmatic.com",
+ "nprove.com",
+ "optim.al",
+ "orbengine.com",
+ "xa.net"
+ ]
+ },
+ "Optimizely": {
+ "properties": [
+ "optimizely.com"
+ ],
+ "resources": [
+ "optimizely.com"
+ ]
+ },
+ "OptimumResponse": {
+ "properties": [
+ "optimumresponse.com"
+ ],
+ "resources": [
+ "optimumresponse.com"
+ ]
+ },
+ "OptinMonster": {
+ "properties": [
+ "optinmonster.com",
+ "optnmstr.com"
+ ],
+ "resources": [
+ "optinmonster.com",
+ "optnmstr.com"
+ ]
+ },
+ "OptMD": {
+ "properties": [
+ "optmd.com"
+ ],
+ "resources": [
+ "optmd.com"
+ ]
+ },
+ "Oracle": {
+ "properties": [
+ "oracle.com"
+ ],
+ "resources": [
+ "atgsvcs.com",
+ "eloqua.com",
+ "estara.com",
+ "instantservice.com",
+ "istrack.com",
+ "maxymiser.com",
+ "oracle.com"
+ ]
+ },
+ "OrangeSoda": {
+ "properties": [
+ "orangesoda.com",
+ "otracking.com"
+ ],
+ "resources": [
+ "orangesoda.com",
+ "otracking.com"
+ ]
+ },
+ "Outbrain": {
+ "properties": [
+ "outbrain.com",
+ "sphere.com"
+ ],
+ "resources": [
+ "outbrain.com",
+ "sphere.com",
+ "visualrevenue.com"
+ ]
+ },
+ "Out There Media": {
+ "properties": [
+ "out-there-media.com"
+ ],
+ "resources": [
+ "out-there-media.com"
+ ]
+ },
+ "Oversee.net": {
+ "properties": [
+ "dsnextgen.com",
+ "oversee.net"
+ ],
+ "resources": [
+ "dsnextgen.com",
+ "oversee.net"
+ ]
+ },
+ "ÖWA": {
+ "properties": [
+ "oewa.at"
+ ],
+ "resources": [
+ "oewa.at",
+ "oewabox.at"
+ ]
+ },
+ "OwnerIQ": {
+ "properties": [
+ "owneriq.com",
+ "owneriq.net"
+ ],
+ "resources": [
+ "owneriq.com",
+ "owneriq.net"
+ ]
+ },
+ "OxaMedia": {
+ "properties": [
+ "oxamedia.com"
+ ],
+ "resources": [
+ "adconnexa.com",
+ "adsbwm.com",
+ "oxamedia.com"
+ ]
+ },
+ "PageFair": {
+ "properties": [
+ "pagefair.com",
+ "pagefair.net"
+ ],
+ "resources": [
+ "pagefair.com",
+ "pagefair.net"
+ ]
+ },
+ "Paid-To-Promote.net": {
+ "properties": [
+ "paid-to-promote.net"
+ ],
+ "resources": [
+ "paid-to-promote.net"
+ ]
+ },
+ "Papaya": {
+ "properties": [
+ "papayamobile.com"
+ ],
+ "resources": [
+ "papayamobile.com"
+ ]
+ },
+ "Pardot": {
+ "properties": [
+ "pardot.com"
+ ],
+ "resources": [
+ "pardot.com"
+ ]
+ },
+ "Parse.ly": {
+ "properties": [
+ "parsely.com"
+ ],
+ "resources": [
+ "parsely.com"
+ ]
+ },
+ "PayHit": {
+ "properties": [
+ "payhit.com"
+ ],
+ "resources": [
+ "payhit.com"
+ ]
+ },
+ "PaymentsMB": {
+ "properties": [
+ "paymentsmb.com"
+ ],
+ "resources": [
+ "paymentsmb.com"
+ ]
+ },
+ "Paypal": {
+ "properties": [
+ "paypal.com",
+ "simility.com"
+ ],
+ "resources": [
+ "paypal.com",
+ "simility.com"
+ ]
+ },
+ "Paypopup.com": {
+ "properties": [
+ "paypopup.com"
+ ],
+ "resources": [
+ "lzjl.com",
+ "paypopup.com"
+ ]
+ },
+ "PebblePost": {
+ "properties": [
+ "pebblepost.com"
+ ],
+ "resources": [
+ "pbbl.co"
+ ]
+ },
+ "Peer39": {
+ "properties": [
+ "peer39.com",
+ "peer39.net"
+ ],
+ "resources": [
+ "peer39.com",
+ "peer39.net"
+ ]
+ },
+ "PeerFly": {
+ "properties": [
+ "peerfly.com"
+ ],
+ "resources": [
+ "peerfly.com"
+ ]
+ },
+ "Peerius": {
+ "properties": [
+ "peerius.com"
+ ],
+ "resources": [
+ "peerius.com"
+ ]
+ },
+ "Performancing": {
+ "properties": [
+ "performancing.com"
+ ],
+ "resources": [
+ "performancing.com"
+ ]
+ },
+ "PerimeterX": {
+ "properties": [
+ "perimeterx.com"
+ ],
+ "resources": [
+ "perimeterx.com"
+ ]
+ },
+ "PersianStat.com": {
+ "properties": [
+ "persianstat.com"
+ ],
+ "resources": [
+ "persianstat.com"
+ ]
+ },
+ "Pheedo": {
+ "properties": [
+ "pheedo.com"
+ ],
+ "resources": [
+ "pheedo.com"
+ ]
+ },
+ "Phonalytics": {
+ "properties": [
+ "phonalytics.com"
+ ],
+ "resources": [
+ "phonalytics.com"
+ ]
+ },
+ "phpMyVisites": {
+ "properties": [
+ "phpmyvisites.us"
+ ],
+ "resources": [
+ "phpmyvisites.us"
+ ]
+ },
+ "Pictela": {
+ "properties": [
+ "pictela.com",
+ "pictela.net"
+ ],
+ "resources": [
+ "pictela.com",
+ "pictela.net"
+ ]
+ },
+ "PinPoll": {
+ "properties": [
+ "pinpoll.com"
+ ],
+ "resources": [
+ "pinpoll.com"
+ ]
+ },
+ "Pinterest": {
+ "properties": [
+ "pinterest.at",
+ "pinterest.ca",
+ "pinterest.ch",
+ "pinterest.cl",
+ "pinterest.co.kr",
+ "pinterest.co.uk",
+ "pinterest.com",
+ "pinterest.com.au",
+ "pinterest.com.mx",
+ "pinterest.de",
+ "pinterest.dk",
+ "pinterest.es",
+ "pinterest.fr",
+ "pinterest.ie",
+ "pinterest.jp",
+ "pinterest.nz",
+ "pinterest.pt",
+ "pinterest.se"
+ ],
+ "resources": [
+ "pinimg.com",
+ "pinterest.com"
+ ]
+ },
+ "Piwik": {
+ "properties": [
+ "piwik.org"
+ ],
+ "resources": [
+ "piwik.org"
+ ]
+ },
+ "PixAnalytics": {
+ "properties": [
+ "pixanalytics.com"
+ ],
+ "resources": [
+ "pixanalytics.com"
+ ]
+ },
+ "Pixel.sg": {
+ "properties": [
+ "pixel.sg"
+ ],
+ "resources": [
+ "pixel.sg"
+ ]
+ },
+ "Piximedia": {
+ "properties": [
+ "piximedia.com"
+ ],
+ "resources": [
+ "piximedia.com"
+ ]
+ },
+ "Pixlee": {
+ "properties": [
+ "pixlee.com"
+ ],
+ "resources": [
+ "pixlee.com"
+ ]
+ },
+ "PLATFORM ONE": {
+ "properties": [
+ "platform-one.co.jp",
+ "www.platform-one.co.jp"
+ ],
+ "resources": [
+ "platform-one.co.jp",
+ "www.platform-one.co.jp"
+ ]
+ },
+ "plista": {
+ "properties": [
+ "plista.com"
+ ],
+ "resources": [
+ "plista.com"
+ ]
+ },
+ "PocketCents": {
+ "properties": [
+ "pocketcents.com"
+ ],
+ "resources": [
+ "pocketcents.com"
+ ]
+ },
+ "Polar Mobile": {
+ "properties": [
+ "mediavoice.com"
+ ],
+ "resources": [
+ "mediavoice.com",
+ "polarmobile.com"
+ ]
+ },
+ "Politads": {
+ "properties": [
+ "politads.com"
+ ],
+ "resources": [
+ "politads.com"
+ ]
+ },
+ "Polymorph": {
+ "properties": [
+ "getpolymorph.com"
+ ],
+ "resources": [
+ "adsnative.com",
+ "getpolymorph.com"
+ ]
+ },
+ "Pontiflex": {
+ "properties": [
+ "pontiflex.com"
+ ],
+ "resources": [
+ "pontiflex.com"
+ ]
+ },
+ "Poool": {
+ "properties": [
+ "poool.fr"
+ ],
+ "resources": [
+ "poool.fr"
+ ]
+ },
+ "PopAds": {
+ "properties": [
+ "popads.net"
+ ],
+ "resources": [
+ "popads.net",
+ "popadscdn.net"
+ ]
+ },
+ "PopRule": {
+ "properties": [
+ "gocampaignlive.com",
+ "poprule.com"
+ ],
+ "resources": [
+ "gocampaignlive.com",
+ "poprule.com"
+ ]
+ },
+ "Popunder.ru": {
+ "properties": [
+ "popunder.ru"
+ ],
+ "resources": [
+ "popunder.ru"
+ ]
+ },
+ "Po.st": {
+ "properties": [
+ "po.st"
+ ],
+ "resources": [
+ "po.st"
+ ]
+ },
+ "Powerlinks": {
+ "properties": [
+ "powerlinks.com"
+ ],
+ "resources": [
+ "powerlinks.com"
+ ]
+ },
+ "PPCProtect": {
+ "properties": [
+ "ppcprotect.com"
+ ],
+ "resources": [
+ "ppcprotect.com"
+ ]
+ },
+ "PrecisionClick": {
+ "properties": [
+ "precisionclick.com"
+ ],
+ "resources": [
+ "precisionclick.com"
+ ]
+ },
+ "PredictAd": {
+ "properties": [
+ "predictad.com"
+ ],
+ "resources": [
+ "predictad.com"
+ ]
+ },
+ "Pressflex": {
+ "properties": [
+ "blogads.com",
+ "pressflex.com"
+ ],
+ "resources": [
+ "blogads.com",
+ "pressflex.com"
+ ]
+ },
+ "Prime Visibility": {
+ "properties": [
+ "primevisibility.com"
+ ],
+ "resources": [
+ "adcde.com",
+ "addlvr.com",
+ "adonnetwork.com",
+ "adonnetwork.net",
+ "adtrgt.com",
+ "bannertgt.com",
+ "cptgt.com",
+ "cpvfeed.com",
+ "cpvtgt.com",
+ "dashboardad.net",
+ "popcde.com",
+ "primevisibility.com",
+ "sdfje.com",
+ "urtbk.com"
+ ]
+ },
+ "Primis": {
+ "properties": [
+ "primis.tech"
+ ],
+ "resources": [
+ "sekindo.com"
+ ]
+ },
+ "PrismApp": {
+ "properties": [
+ "prismapp.io"
+ ],
+ "resources": [
+ "prismapp.io"
+ ]
+ },
+ "Proclivity": {
+ "properties": [
+ "proclivitysystems.com",
+ "pswec.com"
+ ],
+ "resources": [
+ "proclivitymedia.com",
+ "proclivitysystems.com",
+ "pswec.com"
+ ]
+ },
+ "Project Wonderful": {
+ "properties": [
+ "projectwonderful.com"
+ ],
+ "resources": [
+ "projectwonderful.com"
+ ]
+ },
+ "PrometheusIntelligenceTechnology": {
+ "properties": [
+ "prometheusintelligencetechnology.com"
+ ],
+ "resources": [
+ "prometheusintelligencetechnology.com"
+ ]
+ },
+ "Pronunciator": {
+ "properties": [
+ "pronunciator.com",
+ "visitorville.com"
+ ],
+ "resources": [
+ "pronunciator.com",
+ "visitorville.com"
+ ]
+ },
+ "Propeller Ads": {
+ "properties": [
+ "propellerads.com"
+ ],
+ "resources": [
+ "propellerads.com"
+ ]
+ },
+ "Prosperent": {
+ "properties": [
+ "prosperent.com"
+ ],
+ "resources": [
+ "prosperent.com"
+ ]
+ },
+ "Protected Media": {
+ "properties": [
+ "ad-score.com",
+ "protected.media"
+ ],
+ "resources": [
+ "ad-score.com",
+ "protected.media"
+ ]
+ },
+ "Provers": {
+ "properties": [
+ "provers.pro"
+ ],
+ "resources": [
+ "provers.pro"
+ ]
+ },
+ "Psonstrentie": {
+ "properties": [
+ "psonstrentie.info"
+ ],
+ "resources": [
+ "psonstrentie.info"
+ ]
+ },
+ "Public-Idées": {
+ "properties": [
+ "publicidees.com"
+ ],
+ "resources": [
+ "publicidees.com"
+ ]
+ },
+ "Publishers Clearing House": {
+ "properties": [
+ "pch.com"
+ ],
+ "resources": [
+ "pch.com"
+ ]
+ },
+ "PubMatic": {
+ "properties": [
+ "pubmatic.com"
+ ],
+ "resources": [
+ "pubmatic.com",
+ "revinet.com"
+ ]
+ },
+ "PulsePoint": {
+ "properties": [
+ "pulsepoint.com"
+ ],
+ "resources": [
+ "pulsepoint.com"
+ ]
+ },
+ "PunchTab": {
+ "properties": [
+ "punchtab.com"
+ ],
+ "resources": [
+ "punchtab.com"
+ ]
+ },
+ "quadrantOne": {
+ "properties": [
+ "quadrantone.com"
+ ],
+ "resources": [
+ "quadrantone.com"
+ ]
+ },
+ "Quake Marketing": {
+ "properties": [
+ "quakemarketing.com"
+ ],
+ "resources": [
+ "quakemarketing.com"
+ ]
+ },
+ "Qualaroo": {
+ "properties": [
+ "qualaroo.com"
+ ],
+ "resources": [
+ "kissinsights.com",
+ "qualaroo.com"
+ ]
+ },
+ "Quantcast": {
+ "properties": [
+ "quantcast.com",
+ "quantserve.com"
+ ],
+ "resources": [
+ "quantcast.com",
+ "quantserve.com"
+ ]
+ },
+ "QuantumAdvertising": {
+ "properties": [
+ "quantum-advertising.com"
+ ],
+ "resources": [
+ "quantum-advertising.com"
+ ]
+ },
+ "QuinStreet": {
+ "properties": [
+ "quinstreet.com",
+ "thecounter.com"
+ ],
+ "resources": [
+ "qnsr.com",
+ "qsstats.com",
+ "quinstreet.com",
+ "thecounter.com"
+ ]
+ },
+ "Quintelligence": {
+ "properties": [
+ "quintelligence.com"
+ ],
+ "resources": [
+ "quintelligence.com"
+ ]
+ },
+ "QUISMA": {
+ "properties": [
+ "quisma.com"
+ ],
+ "resources": [
+ "iaded.com",
+ "quisma.com",
+ "quismatch.com",
+ "xaded.com",
+ "xmladed.com"
+ ]
+ },
+ "RadarURL": {
+ "properties": [
+ "radarurl.com"
+ ],
+ "resources": [
+ "radarurl.com"
+ ]
+ },
+ "Radial": {
+ "properties": [
+ "radial.com"
+ ],
+ "resources": [
+ "gsicommerce.com",
+ "gsimedia.net"
+ ]
+ },
+ "Radiate Media": {
+ "properties": [
+ "gtnetwork.com.au",
+ "solesolution.com"
+ ],
+ "resources": [
+ "gtnetwork.com.au",
+ "matchbin.com",
+ "radiatemedia.com",
+ "solesolution.com"
+ ]
+ },
+ "RadiumOne": {
+ "properties": [
+ "radiumone.com"
+ ],
+ "resources": [
+ "gwallet.com",
+ "radiumone.com"
+ ]
+ },
+ "Radius Marketing": {
+ "properties": [
+ "radiusmarketing.com"
+ ],
+ "resources": [
+ "radiusmarketing.com"
+ ]
+ },
+ "Rambler": {
+ "properties": [
+ "rambler.ru"
+ ],
+ "resources": [
+ "rambler.ru"
+ ]
+ },
+ "Rapleaf": {
+ "properties": [
+ "rapleaf.com",
+ "rlcdn.com"
+ ],
+ "resources": [
+ "rapleaf.com",
+ "rlcdn.com"
+ ]
+ },
+ "ReachLocal": {
+ "properties": [
+ "reachlocal.com",
+ "rlcdn.net"
+ ],
+ "resources": [
+ "reachlocal.com",
+ "rlcdn.net"
+ ]
+ },
+ "React2Media": {
+ "properties": [
+ "react2media.com"
+ ],
+ "resources": [
+ "react2media.com"
+ ]
+ },
+ "reddit": {
+ "properties": [
+ "reddit.com"
+ ],
+ "resources": [
+ "reddit.com"
+ ]
+ },
+ "Redux Media": {
+ "properties": [
+ "reduxmedia.com"
+ ],
+ "resources": [
+ "reduxmedia.com"
+ ]
+ },
+ "Rekko": {
+ "properties": [
+ "convertglobal.com",
+ "rekko.com"
+ ],
+ "resources": [
+ "convertglobal.com",
+ "rekko.com"
+ ]
+ },
+ "Reklamport": {
+ "properties": [
+ "reklamport.com"
+ ],
+ "resources": [
+ "reklamport.com"
+ ]
+ },
+ "Reklam Store": {
+ "properties": [
+ "reklamstore.com"
+ ],
+ "resources": [
+ "reklamstore.com"
+ ]
+ },
+ "Reklamz": {
+ "properties": [
+ "reklamz.com"
+ ],
+ "resources": [
+ "reklamz.com"
+ ]
+ },
+ "Relevad": {
+ "properties": [
+ "relestar.com",
+ "relevad.com"
+ ],
+ "resources": [
+ "relestar.com",
+ "relevad.com"
+ ]
+ },
+ "Renegade Internet": {
+ "properties": [
+ "advertserve.com",
+ "renegadeinternet.com"
+ ],
+ "resources": [
+ "advertserve.com",
+ "renegadeinternet.com"
+ ]
+ },
+ "Reporo": {
+ "properties": [
+ "reporo.com"
+ ],
+ "resources": [
+ "buzzcity.com"
+ ]
+ },
+ "Research Now": {
+ "properties": [
+ "researchnow.com",
+ "valuedopinions.co.uk"
+ ],
+ "resources": [
+ "researchnow.com",
+ "valuedopinions.co.uk"
+ ]
+ },
+ "ResolutionMedia": {
+ "properties": [
+ "nonstoppartner.net"
+ ],
+ "resources": [
+ "nonstoppartner.net"
+ ]
+ },
+ "Resolution Media": {
+ "properties": [
+ "resolutionmedia.com"
+ ],
+ "resources": [
+ "resolutionmedia.com"
+ ]
+ },
+ "Resonate": {
+ "properties": [
+ "resonateinsights.com",
+ "resonatenetworks.com"
+ ],
+ "resources": [
+ "reson8.com",
+ "resonateinsights.com",
+ "resonatenetworks.com"
+ ]
+ },
+ "Responsys": {
+ "properties": [
+ "responsys.com"
+ ],
+ "resources": [
+ "responsys.com"
+ ]
+ },
+ "Retail Automata": {
+ "properties": [
+ "retailautomata.com"
+ ],
+ "resources": [
+ "retailautomata.com"
+ ]
+ },
+ "ReTargeter": {
+ "properties": [
+ "retargeter.com"
+ ],
+ "resources": [
+ "retargeter.com"
+ ]
+ },
+ "Retirement Living": {
+ "properties": [
+ "blvdstatus.com",
+ "retirement-living.com"
+ ],
+ "resources": [
+ "blvdstatus.com",
+ "retirement-living.com"
+ ]
+ },
+ "RevContent": {
+ "properties": [
+ "revcontent.com"
+ ],
+ "resources": [
+ "revcontent.com"
+ ]
+ },
+ "RevenueMax": {
+ "properties": [
+ "revenuemax.de"
+ ],
+ "resources": [
+ "revenuemax.de"
+ ]
+ },
+ "Revtracks": {
+ "properties": [
+ "revtrax.com"
+ ],
+ "resources": [
+ "revtrax.com"
+ ]
+ },
+ "Rhythm": {
+ "properties": [
+ "rhythmone.com"
+ ],
+ "resources": [
+ "1rx.io",
+ "rhythmnewmedia.com",
+ "rhythmone.com",
+ "rhythmxchange.com",
+ "rnmd.net"
+ ]
+ },
+ "RichAudience": {
+ "properties": [
+ "richaudience.com"
+ ],
+ "resources": [
+ "richaudience.com"
+ ]
+ },
+ "RichRelevance": {
+ "properties": [
+ "richrelevance.com"
+ ],
+ "resources": [
+ "richrelevance.com"
+ ]
+ },
+ "RightAction": {
+ "properties": [
+ "rightaction.com"
+ ],
+ "resources": [
+ "rightaction.com"
+ ]
+ },
+ "RIM": {
+ "properties": [
+ "global.blackberry.com",
+ "laptopverge.com"
+ ],
+ "resources": [
+ "global.blackberry.com",
+ "laptopverge.com",
+ "rim.com",
+ "scoreloop.com"
+ ]
+ },
+ "Ringier": {
+ "properties": [
+ "ringier.cz"
+ ],
+ "resources": [
+ "ringier.cz"
+ ]
+ },
+ "RMBN": {
+ "properties": [
+ "traforet.com"
+ ],
+ "resources": [
+ "rmbn.net",
+ "rmbn.ru",
+ "traforet.com"
+ ]
+ },
+ "RMM": {
+ "properties": [
+ "rmmonline.com"
+ ],
+ "resources": [
+ "rmmonline.com"
+ ]
+ },
+ "Rocket Fuel": {
+ "properties": [
+ "rfihub.com",
+ "rfihub.net",
+ "rocketfuel.com"
+ ],
+ "resources": [
+ "rfihub.com",
+ "rfihub.net",
+ "rocketfuel.com",
+ "ru4.com",
+ "xplusone.com"
+ ]
+ },
+ "Rollick": {
+ "properties": [
+ "gorollick.com"
+ ],
+ "resources": [
+ "rollick.io"
+ ]
+ },
+ "Rovion": {
+ "properties": [
+ "rovion.com"
+ ],
+ "resources": [
+ "rovion.com"
+ ]
+ },
+ "Roxr": {
+ "properties": [
+ "clicky.com",
+ "roxr.net"
+ ],
+ "resources": [
+ "clicky.com",
+ "getclicky.com",
+ "roxr.net",
+ "staticstuff.net"
+ ]
+ },
+ "rtk": {
+ "properties": [
+ "rtk.io"
+ ],
+ "resources": [
+ "rtk.io"
+ ]
+ },
+ "RubiconProject": {
+ "properties": [
+ "rubiconproject.com"
+ ],
+ "resources": [
+ "adsbyisocket.com",
+ "isocket.com",
+ "rubiconproject.com"
+ ]
+ },
+ "RunAds": {
+ "properties": [
+ "runads.com"
+ ],
+ "resources": [
+ "runads.com",
+ "rundsp.com"
+ ]
+ },
+ "RuTarget": {
+ "properties": [
+ "rutarget.ru"
+ ],
+ "resources": [
+ "rutarget.ru"
+ ]
+ },
+ "Sabavision": {
+ "properties": [
+ "sabavision.com"
+ ],
+ "resources": [
+ "sabavision.com"
+ ]
+ },
+ "Sabre": {
+ "properties": [
+ "reztrack.com",
+ "sabre.com",
+ "sabrehospitality.com"
+ ],
+ "resources": [
+ "reztrack.com",
+ "sabre.com",
+ "sabrehospitality.com"
+ ]
+ },
+ "Safecount": {
+ "properties": [
+ "safecount.net"
+ ],
+ "resources": [
+ "dl-rms.com",
+ "dlqm.net",
+ "questionmarket.com",
+ "safecount.net"
+ ]
+ },
+ "SageMetrics": {
+ "properties": [
+ "sagemetrics.com"
+ ],
+ "resources": [
+ "sageanalyst.net",
+ "sagemetrics.com"
+ ]
+ },
+ "Salesforce.com": {
+ "properties": [
+ "force.com",
+ "salesforce.com",
+ "trailblazer.me"
+ ],
+ "resources": [
+ "documentforce.com",
+ "force.com",
+ "forcesslreports.com",
+ "forceusercontent.com",
+ "lightning.com",
+ "salesforce-communities.com",
+ "salesforce-hub.com",
+ "salesforce.com",
+ "salesforceliveagent.com",
+ "trailblazer.me",
+ "visualforce.com"
+ ]
+ },
+ "Salesintelligence": {
+ "properties": [
+ "salesintelligence.pl"
+ ],
+ "resources": [
+ "plugin.management"
+ ]
+ },
+ "Samurai Factory": {
+ "properties": [
+ "samurai-factory.jp",
+ "shinobi.jp"
+ ],
+ "resources": [
+ "samurai-factory.jp",
+ "shinobi.jp"
+ ]
+ },
+ "SAP": {
+ "properties": [
+ "sap.com"
+ ],
+ "resources": [
+ "sap.com",
+ "seewhy.com"
+ ]
+ },
+ "Sapient": {
+ "properties": [
+ "bridgetrack.com",
+ "sapient.com"
+ ],
+ "resources": [
+ "bridgetrack.com",
+ "sapient.com"
+ ]
+ },
+ "SAS": {
+ "properties": [
+ "aimatch.com",
+ "sas.com"
+ ],
+ "resources": [
+ "aimatch.com",
+ "sas.com"
+ ]
+ },
+ "SAY": {
+ "properties": [
+ "saymedia.com",
+ "typepad.com",
+ "videoegg.com"
+ ],
+ "resources": [
+ "saymedia.com",
+ "typepad.com",
+ "videoegg.com"
+ ]
+ },
+ "Scandinavian AdNetworks": {
+ "properties": [
+ "scandinavianadnetworks.com"
+ ],
+ "resources": [
+ "scandinavianadnetworks.com"
+ ]
+ },
+ "ScribeFire": {
+ "properties": [
+ "scribefire.com"
+ ],
+ "resources": [
+ "scribefire.com"
+ ]
+ },
+ "Scribol": {
+ "properties": [
+ "scribol.com"
+ ],
+ "resources": [
+ "scribol.com"
+ ]
+ },
+ "SearchForce": {
+ "properties": [
+ "searchforce.com",
+ "searchforce.net"
+ ],
+ "resources": [
+ "searchforce.com",
+ "searchforce.net"
+ ]
+ },
+ "Seevast": {
+ "properties": [
+ "kanoodle.com"
+ ],
+ "resources": [
+ "kanoodle.com",
+ "pulse360.com",
+ "seevast.com",
+ "syndigonetworks.com"
+ ]
+ },
+ "SeeVolution": {
+ "properties": [
+ "seevolution.com",
+ "svlu.net"
+ ],
+ "resources": [
+ "seevolution.com",
+ "svlu.net"
+ ]
+ },
+ "Segment.io": {
+ "properties": [
+ "segment.io"
+ ],
+ "resources": [
+ "segment.io"
+ ]
+ },
+ "Selectable Media": {
+ "properties": [
+ "selectablemedia.com"
+ ],
+ "resources": [
+ "nabbr.com",
+ "selectablemedia.com"
+ ]
+ },
+ "Semantiqo": {
+ "properties": [
+ "semantiqo.com"
+ ],
+ "resources": [
+ "semantiqo.com"
+ ]
+ },
+ "Semasio": {
+ "properties": [
+ "semasio.com"
+ ],
+ "resources": [
+ "semasio.com",
+ "semasio.net"
+ ]
+ },
+ "SendPulse": {
+ "properties": [
+ "sendpulse.com"
+ ],
+ "resources": [
+ "sendpulse.com"
+ ]
+ },
+ "Service4refresh": {
+ "properties": [
+ "service4refresh.info"
+ ],
+ "resources": [
+ "service4refresh.info"
+ ]
+ },
+ "SessionCam": {
+ "properties": [
+ "sessioncam.com"
+ ],
+ "resources": [
+ "sessioncam.com"
+ ]
+ },
+ "SevenAds": {
+ "properties": [
+ "sevenads.net"
+ ],
+ "resources": [
+ "sevenads.net"
+ ]
+ },
+ "SexInYourCity": {
+ "properties": [
+ "sexinyourcity.com"
+ ],
+ "resources": [
+ "sexinyourcity.com"
+ ]
+ },
+ "ShaftTraffic": {
+ "properties": [
+ "shafttraffic.com"
+ ],
+ "resources": [
+ "libertystmedia.com",
+ "shafttraffic.com"
+ ]
+ },
+ "Shareaholic": {
+ "properties": [
+ "shareaholic.com"
+ ],
+ "resources": [
+ "shareaholic.com"
+ ]
+ },
+ "ShareASale": {
+ "properties": [
+ "shareasale.com"
+ ],
+ "resources": [
+ "shareasale.com"
+ ]
+ },
+ "ShareThis": {
+ "properties": [
+ "sharethis.com"
+ ],
+ "resources": [
+ "sharethis.com"
+ ]
+ },
+ "Sharethrough": {
+ "properties": [
+ "sharethrough.com"
+ ],
+ "resources": [
+ "sharethrough.com"
+ ]
+ },
+ "ShinyStat": {
+ "properties": [
+ "shinystat.com"
+ ],
+ "resources": [
+ "shinystat.com"
+ ]
+ },
+ "Shopzilla": {
+ "properties": [
+ "shopzilla.com"
+ ],
+ "resources": [
+ "shopzilla.com"
+ ]
+ },
+ "Shortest": {
+ "properties": [
+ "shorte.st"
+ ],
+ "resources": [
+ "shorte.st"
+ ]
+ },
+ "SiftScience": {
+ "properties": [
+ "sift.com"
+ ],
+ "resources": [
+ "siftscience.com"
+ ]
+ },
+ "Signifyd": {
+ "properties": [
+ "signifyd.com"
+ ],
+ "resources": [
+ "signifyd.com"
+ ]
+ },
+ "Silverpop": {
+ "properties": [
+ "mkt51.net",
+ "silverpop.com"
+ ],
+ "resources": [
+ "mkt51.net",
+ "pages05.net",
+ "silverpop.com",
+ "vtrenz.net"
+ ]
+ },
+ "Simpli.fi": {
+ "properties": [
+ "simpli.fi"
+ ],
+ "resources": [
+ "simpli.fi"
+ ]
+ },
+ "SiteScout": {
+ "properties": [
+ "sitescout.com"
+ ],
+ "resources": [
+ "sitescout.com"
+ ]
+ },
+ "Six Apart": {
+ "properties": [
+ "movabletype.com",
+ "sixapart.com"
+ ],
+ "resources": [
+ "movabletype.com",
+ "sixapart.com"
+ ]
+ },
+ "Skimlinks": {
+ "properties": [
+ "skimlinks.com",
+ "skimresources.com"
+ ],
+ "resources": [
+ "skimlinks.com",
+ "skimresources.com"
+ ]
+ },
+ "Skribit": {
+ "properties": [
+ "paulstamatiou.com"
+ ],
+ "resources": [
+ "paulstamatiou.com",
+ "skribit.com"
+ ]
+ },
+ "Skupe Net": {
+ "properties": [
+ "adcentriconline.com",
+ "skupenet.com"
+ ],
+ "resources": [
+ "adcentriconline.com",
+ "skupenet.com"
+ ]
+ },
+ "Smaato": {
+ "properties": [
+ "smaato.com"
+ ],
+ "resources": [
+ "smaato.com"
+ ]
+ },
+ "SmartAdServer": {
+ "properties": [
+ "smartadserver.com"
+ ],
+ "resources": [
+ "smartadserver.com"
+ ]
+ },
+ "Smartlook": {
+ "properties": [
+ "smartlook.com"
+ ],
+ "resources": [
+ "smartlook.com"
+ ]
+ },
+ "SmartyAds": {
+ "properties": [
+ "smartyads.com"
+ ],
+ "resources": [
+ "smartyads.com"
+ ]
+ },
+ "Smi": {
+ "properties": [
+ "24smi.net"
+ ],
+ "resources": [
+ "24smi.net"
+ ]
+ },
+ "Smiley Media": {
+ "properties": [
+ "smileymedia.com"
+ ],
+ "resources": [
+ "smileymedia.com"
+ ]
+ },
+ "Smowtion": {
+ "properties": [
+ "smowtion.com"
+ ],
+ "resources": [
+ "smowtion.com"
+ ]
+ },
+ "Snap": {
+ "properties": [
+ "snap.com"
+ ],
+ "resources": [
+ "snap.com"
+ ]
+ },
+ "SnapEngage": {
+ "properties": [
+ "snapengage.com"
+ ],
+ "resources": [
+ "snapengage.com"
+ ]
+ },
+ "Snoobi": {
+ "properties": [
+ "snoobi.fi"
+ ],
+ "resources": [
+ "snoobi.com",
+ "snoobi.fi"
+ ]
+ },
+ "SocialChorus": {
+ "properties": [
+ "socialchorus.com"
+ ],
+ "resources": [
+ "halogenmediagroup.com",
+ "halogennetwork.com",
+ "socialchorus.com"
+ ]
+ },
+ "SocialInterface": {
+ "properties": [
+ "socialinterface.com"
+ ],
+ "resources": [
+ "ratevoice.com",
+ "socialinterface.com"
+ ]
+ },
+ "SocialTwist": {
+ "properties": [
+ "socialtwist.com"
+ ],
+ "resources": [
+ "socialtwist.com"
+ ]
+ },
+ "sociomantic labs": {
+ "properties": [
+ "sociomantic.com"
+ ],
+ "resources": [
+ "sociomantic.com"
+ ]
+ },
+ "Socital": {
+ "properties": [
+ "socital.com"
+ ],
+ "resources": [
+ "socital.com"
+ ]
+ },
+ "Sojern": {
+ "properties": [
+ "sojern.com"
+ ],
+ "resources": [
+ "sojern.com"
+ ]
+ },
+ "SomoAudience": {
+ "properties": [
+ "somoaudience.com"
+ ],
+ "resources": [
+ "somoaudience.com"
+ ]
+ },
+ "Sonobi": {
+ "properties": [
+ "sonobi.com"
+ ],
+ "resources": [
+ "sonobi.com"
+ ]
+ },
+ "sophus3": {
+ "properties": [
+ "sophus3.com"
+ ],
+ "resources": [
+ "sophus3.co.uk",
+ "sophus3.com"
+ ]
+ },
+ "Sortable": {
+ "properties": [
+ "sortable.com"
+ ],
+ "resources": [
+ "deployads.com"
+ ]
+ },
+ "Sourcepoint": {
+ "properties": [
+ "sourcepoint.com"
+ ],
+ "resources": [
+ "summerhamster.com"
+ ]
+ },
+ "Sovrn": {
+ "properties": [
+ "sovrn.com"
+ ],
+ "resources": [
+ "sovrn.com"
+ ]
+ },
+ "Space Chimp Media": {
+ "properties": [
+ "spacechimpmedia.com"
+ ],
+ "resources": [
+ "spacechimpmedia.com"
+ ]
+ },
+ "SpareChange": {
+ "properties": [
+ "sparechange.io"
+ ],
+ "resources": [
+ "sparechange.io"
+ ]
+ },
+ "Sparklit": {
+ "properties": [
+ "adbutler.com",
+ "sparklit.com"
+ ],
+ "resources": [
+ "adbutler.com",
+ "sparklit.com"
+ ]
+ },
+ "Spark Studios": {
+ "properties": [
+ "sparkstudios.com"
+ ],
+ "resources": [
+ "sparkstudios.com"
+ ]
+ },
+ "Specific Media": {
+ "properties": [
+ "sitemeter.com",
+ "specificmedia.com"
+ ],
+ "resources": [
+ "adviva.co.uk",
+ "adviva.net",
+ "sitemeter.com",
+ "specificclick.net",
+ "specificmedia.com"
+ ]
+ },
+ "Spectate": {
+ "properties": [
+ "spectate.com"
+ ],
+ "resources": [
+ "spectate.com"
+ ]
+ },
+ "Sponge": {
+ "properties": [
+ "spongegroup.com"
+ ],
+ "resources": [
+ "spongegroup.com"
+ ]
+ },
+ "Spongecell": {
+ "properties": [
+ "spongecell.com"
+ ],
+ "resources": [
+ "spongecell.com"
+ ]
+ },
+ "SponsorAds": {
+ "properties": [
+ "sponsorads.de"
+ ],
+ "resources": [
+ "sponsorads.de"
+ ]
+ },
+ "Spot200": {
+ "properties": [
+ "spot200.com"
+ ],
+ "resources": [
+ "spot200.com"
+ ]
+ },
+ "SpotX": {
+ "properties": [
+ "spotx.tv"
+ ],
+ "resources": [
+ "spotx.tv"
+ ]
+ },
+ "SpotXchange": {
+ "properties": [
+ "spotxchange.com"
+ ],
+ "resources": [
+ "spotxcdn.com",
+ "spotxchange.com"
+ ]
+ },
+ "Spring Metrics": {
+ "properties": [
+ "springmetrics.com"
+ ],
+ "resources": [
+ "springmetrics.com"
+ ]
+ },
+ "SpringServe": {
+ "properties": [
+ "springserve.com"
+ ],
+ "resources": [
+ "springserve.com"
+ ]
+ },
+ "Sputnik.ru": {
+ "properties": [
+ "sputnik.ru"
+ ],
+ "resources": [
+ "sputnik.ru"
+ ]
+ },
+ "StackAdapt": {
+ "properties": [
+ "stackadapt.com"
+ ],
+ "resources": [
+ "stackadapt.com"
+ ]
+ },
+ "StackTrack": {
+ "properties": [
+ "stat-track.com"
+ ],
+ "resources": [
+ "stat-track.com"
+ ]
+ },
+ "StarGames": {
+ "properties": [
+ "stargames.net",
+ "stargamesaffiliate.com"
+ ],
+ "resources": [
+ "stargames.net",
+ "stargamesaffiliate.com"
+ ]
+ },
+ "stat4u": {
+ "properties": [
+ "4u.pl"
+ ],
+ "resources": [
+ "4u.pl"
+ ]
+ },
+ "StatCounter": {
+ "properties": [
+ "statcounter.com"
+ ],
+ "resources": [
+ "statcounter.com"
+ ]
+ },
+ "Statisfy": {
+ "properties": [
+ "statisfy.net"
+ ],
+ "resources": [
+ "statisfy.net"
+ ]
+ },
+ "STATSIT": {
+ "properties": [
+ "statsit.com"
+ ],
+ "resources": [
+ "statsit.com"
+ ]
+ },
+ "SteelHouse": {
+ "properties": [
+ "steelhouse.com",
+ "steelhousemedia.com"
+ ],
+ "resources": [
+ "steelhouse.com",
+ "steelhousemedia.com"
+ ]
+ },
+ "Storeland": {
+ "properties": [
+ "storeland.ru"
+ ],
+ "resources": [
+ "storeland.ru"
+ ]
+ },
+ "Storygize": {
+ "properties": [
+ "storygize.com"
+ ],
+ "resources": [
+ "storygize.com",
+ "storygize.net"
+ ]
+ },
+ "Stratigent": {
+ "properties": [
+ "stratigent.com"
+ ],
+ "resources": [
+ "stratigent.com"
+ ]
+ },
+ "Streamray": {
+ "properties": [
+ "cams.com",
+ "streamray.com"
+ ],
+ "resources": [
+ "cams.com",
+ "streamray.com"
+ ]
+ },
+ "StrikeAd": {
+ "properties": [
+ "strikead.com"
+ ],
+ "resources": [
+ "strikead.com"
+ ]
+ },
+ "Stripe": {
+ "properties": [
+ "stripe.com"
+ ],
+ "resources": [
+ "stripe.network"
+ ]
+ },
+ "StrongMail": {
+ "properties": [
+ "strongmail.com"
+ ],
+ "resources": [
+ "popularmedia.com",
+ "strongmail.com"
+ ]
+ },
+ "Struq": {
+ "properties": [
+ "struq.com"
+ ],
+ "resources": [
+ "struq.com"
+ ]
+ },
+ "StumbleUpon": {
+ "properties": [
+ "stumbleupon.com"
+ ],
+ "resources": [
+ "stumble-upon.com",
+ "stumbleupon.com"
+ ]
+ },
+ "Sublime Skinz": {
+ "properties": [
+ "sublime.xyz"
+ ],
+ "resources": [
+ "ayads.co",
+ "sublime.xyz"
+ ]
+ },
+ "Suite 66": {
+ "properties": [
+ "suite66.com"
+ ],
+ "resources": [
+ "suite66.com"
+ ]
+ },
+ "Summit": {
+ "properties": [
+ "summitmedia.co.uk",
+ "www.summit.co.uk"
+ ],
+ "resources": [
+ "summitmedia.co.uk",
+ "www.summit.co.uk"
+ ]
+ },
+ "Superfish": {
+ "properties": [
+ "superfish.com"
+ ],
+ "resources": [
+ "superfish.com"
+ ]
+ },
+ "SupersonicAds": {
+ "properties": [
+ "supersonicads.com"
+ ],
+ "resources": [
+ "supersonicads.com"
+ ]
+ },
+ "Survata": {
+ "properties": [
+ "survata.com"
+ ],
+ "resources": [
+ "survata.com"
+ ]
+ },
+ "SwiftMining": {
+ "properties": [
+ "swiftmining.win"
+ ],
+ "resources": [
+ "swiftmining.win"
+ ]
+ },
+ "Switch": {
+ "properties": [
+ "switchadhub.com",
+ "switchconcepts.com"
+ ],
+ "resources": [
+ "switchadhub.com",
+ "switchads.com",
+ "switchconcepts.co.uk",
+ "switchconcepts.com"
+ ]
+ },
+ "Swoop": {
+ "properties": [
+ "swoop.com"
+ ],
+ "resources": [
+ "swoop.com"
+ ]
+ },
+ "SymphonyAM": {
+ "properties": [
+ "factortg.com"
+ ],
+ "resources": [
+ "factortg.com"
+ ]
+ },
+ "Synacor": {
+ "properties": [
+ "synacor.com"
+ ],
+ "resources": [
+ "synacor.com"
+ ]
+ },
+ "Syncapse": {
+ "properties": [
+ "clickable.net",
+ "syncapse.com"
+ ],
+ "resources": [
+ "clickable.net",
+ "syncapse.com"
+ ]
+ },
+ "Syrup Ad": {
+ "properties": [
+ "adotsolution.com"
+ ],
+ "resources": [
+ "adotsolution.com"
+ ]
+ },
+ "Taboola": {
+ "properties": [
+ "taboola.com"
+ ],
+ "resources": [
+ "perfectmarket.com",
+ "taboola.com"
+ ]
+ },
+ "Tailsweep": {
+ "properties": [
+ "tailsweep.com"
+ ],
+ "resources": [
+ "tailsweep.com"
+ ]
+ },
+ "Taleria": {
+ "properties": [
+ "telaria.com"
+ ],
+ "resources": [
+ "freeskreen.com"
+ ]
+ },
+ "Tapad": {
+ "properties": [
+ "tapad.com"
+ ],
+ "resources": [
+ "tapad.com"
+ ]
+ },
+ "Tapgage": {
+ "properties": [
+ "bizmey.com",
+ "tapgage.com"
+ ],
+ "resources": [
+ "bizmey.com",
+ "tapgage.com"
+ ]
+ },
+ "TapIt!": {
+ "properties": [
+ "tapit.com"
+ ],
+ "resources": [
+ "tapit.com"
+ ]
+ },
+ "Tap.me": {
+ "properties": [
+ "tap.me"
+ ],
+ "resources": [
+ "tap.me"
+ ]
+ },
+ "Targetix": {
+ "properties": [
+ "targetix.net"
+ ],
+ "resources": [
+ "targetix.net"
+ ]
+ },
+ "Tatto Media": {
+ "properties": [
+ "tattomedia.com"
+ ],
+ "resources": [
+ "quicknoodles.com",
+ "tattomedia.com"
+ ]
+ },
+ "Teadma": {
+ "properties": [
+ "teadma.com"
+ ],
+ "resources": [
+ "teadma.com"
+ ]
+ },
+ "Teads.tv": {
+ "properties": [
+ "teads.tv"
+ ],
+ "resources": [
+ "teads.tv"
+ ]
+ },
+ "Tealium": {
+ "properties": [
+ "tealium.com"
+ ],
+ "resources": [
+ "tealiumiq.com"
+ ]
+ },
+ "Technorati": {
+ "properties": [
+ "technorati.com"
+ ],
+ "resources": [
+ "technorati.com",
+ "technoratimedia.com"
+ ]
+ },
+ "TechSolutions": {
+ "properties": [
+ "techsolutions.com.tw"
+ ],
+ "resources": [
+ "techsolutions.com.tw"
+ ]
+ },
+ "TellApart": {
+ "properties": [
+ "tellapart.com",
+ "tellapt.com"
+ ],
+ "resources": [
+ "tellapart.com",
+ "tellapt.com"
+ ]
+ },
+ "Telstra": {
+ "properties": [
+ "sensis.com.au",
+ "sensisdata.com.au",
+ "telstra.com.au"
+ ],
+ "resources": [
+ "sensis.com.au",
+ "sensisdata.com.au",
+ "sensisdigitalmedia.com.au",
+ "telstra.com.au"
+ ]
+ },
+ "TENSQUARE": {
+ "properties": [
+ "tensquare.com"
+ ],
+ "resources": [
+ "tensquare.com"
+ ]
+ },
+ "Terra": {
+ "properties": [
+ "eztargetmedia.com",
+ "terra.com.br",
+ "www.terra.com.br"
+ ],
+ "resources": [
+ "eztargetmedia.com",
+ "terra.com.br",
+ "www.terra.com.br"
+ ]
+ },
+ "The Heron Partnership": {
+ "properties": [
+ "marinsm.com"
+ ],
+ "resources": [
+ "heronpartners.com.au",
+ "marinsm.com",
+ "marinsoftware.com"
+ ]
+ },
+ "The Numa Group": {
+ "properties": [
+ "hittail.com",
+ "thenumagroup.com"
+ ],
+ "resources": [
+ "hittail.com",
+ "thenumagroup.com"
+ ]
+ },
+ "The Search Agency": {
+ "properties": [
+ "thesearchagency.com"
+ ],
+ "resources": [
+ "thesearchagency.com",
+ "thesearchagency.net"
+ ]
+ },
+ "The Trade Desk": {
+ "properties": [
+ "thetradedesk.com"
+ ],
+ "resources": [
+ "adsrvr.org",
+ "thetradedesk.com"
+ ]
+ },
+ "ThingLink": {
+ "properties": [
+ "thinglink.com"
+ ],
+ "resources": [
+ "thinglink.com"
+ ]
+ },
+ "Think Realtime": {
+ "properties": [
+ "echosearch.com",
+ "thinkrealtime.com"
+ ],
+ "resources": [
+ "echosearch.com",
+ "esm1.net",
+ "thinkrealtime.com"
+ ]
+ },
+ "Thismoment": {
+ "properties": [
+ "thismoment.com"
+ ],
+ "resources": [
+ "thismoment.com"
+ ]
+ },
+ "Thummit": {
+ "properties": [
+ "thummit.com"
+ ],
+ "resources": [
+ "thummit.com"
+ ]
+ },
+ "Tinder": {
+ "properties": [
+ "carbonads.com",
+ "tinder.com"
+ ],
+ "resources": [
+ "carbonads.com",
+ "tinder.com"
+ ]
+ },
+ "TiqIQ": {
+ "properties": [
+ "tiqiq.com"
+ ],
+ "resources": [
+ "tiqiq.com"
+ ]
+ },
+ "Tisoomi": {
+ "properties": [
+ "adternal.com",
+ "tisoomi.com"
+ ],
+ "resources": [
+ "adternal.com",
+ "tisoomi.com"
+ ]
+ },
+ "TLVMedia": {
+ "properties": [
+ "tlvmedia.com"
+ ],
+ "resources": [
+ "tlvmedia.com"
+ ]
+ },
+ "TNS": {
+ "properties": [
+ "statistik-gallup.net",
+ "tns-counter.ru",
+ "tns-cs.net",
+ "tnsglobal.com"
+ ],
+ "resources": [
+ "sesamestats.com",
+ "statistik-gallup.net",
+ "tns-counter.ru",
+ "tns-cs.net",
+ "tnsglobal.com"
+ ]
+ },
+ "Todacell": {
+ "properties": [
+ "todacell.com"
+ ],
+ "resources": [
+ "todacell.com"
+ ]
+ },
+ "ToneFuse": {
+ "properties": [
+ "tonefuse.com"
+ ],
+ "resources": [
+ "tonefuse.com"
+ ]
+ },
+ "ToneMedia": {
+ "properties": [
+ "clickfuse.com"
+ ],
+ "resources": [
+ "clickfuse.com",
+ "tonemedia.com"
+ ]
+ },
+ "tongdun.cn": {
+ "properties": [
+ "tongdun.cn"
+ ],
+ "resources": [
+ "fraudmetrix.cn",
+ "tongdun.net"
+ ]
+ },
+ "Topsy": {
+ "properties": [
+ "topsy.com"
+ ],
+ "resources": [
+ "topsy.com"
+ ]
+ },
+ "TouchCommerce": {
+ "properties": [
+ "nuance.com"
+ ],
+ "resources": [
+ "inq.com",
+ "nuance.com",
+ "touchcommerce.com"
+ ]
+ },
+ "TraceMyIP.org": {
+ "properties": [
+ "tracemyip.org"
+ ],
+ "resources": [
+ "tracemyip.org"
+ ]
+ },
+ "TrackingSoft": {
+ "properties": [
+ "roia.biz",
+ "trackingsoft.com"
+ ],
+ "resources": [
+ "roia.biz",
+ "trackingsoft.com"
+ ]
+ },
+ "Trackset": {
+ "properties": [
+ "trackset.com"
+ ],
+ "resources": [
+ "trackset.com"
+ ]
+ },
+ "Tradedoubler": {
+ "properties": [
+ "tradedoubler.com"
+ ],
+ "resources": [
+ "tradedoubler.com"
+ ]
+ },
+ "TradeTracker": {
+ "properties": [
+ "tradetracker.com"
+ ],
+ "resources": [
+ "tradetracker.com",
+ "tradetracker.net"
+ ]
+ },
+ "TrafficHaus": {
+ "properties": [
+ "traffichaus.com",
+ "traffichouse.com"
+ ],
+ "resources": [
+ "traffichaus.com",
+ "traffichouse.com"
+ ]
+ },
+ "TrafficRevenue": {
+ "properties": [
+ "trafficrevenue.net"
+ ],
+ "resources": [
+ "trafficrevenue.net"
+ ]
+ },
+ "TrafficScore": {
+ "properties": [
+ "trafficscore.com"
+ ],
+ "resources": [
+ "trafficscore.com"
+ ]
+ },
+ "Traffiq": {
+ "properties": [
+ "traffiq.com"
+ ],
+ "resources": [
+ "traffiq.com"
+ ]
+ },
+ "Trafmag": {
+ "properties": [
+ "trafmag.com"
+ ],
+ "resources": [
+ "trafmag.com"
+ ]
+ },
+ "Traverse": {
+ "properties": [
+ "traversedata.com"
+ ],
+ "resources": [
+ "traversedlp.com"
+ ]
+ },
+ "Travora Media": {
+ "properties": [
+ "travoramedia.com"
+ ],
+ "resources": [
+ "traveladnetwork.com",
+ "traveladvertising.com",
+ "travoramedia.com"
+ ]
+ },
+ "Tremor Video": {
+ "properties": [
+ "tremorvideo.com"
+ ],
+ "resources": [
+ "scanscout.com",
+ "tmnetads.com",
+ "tremorhub.com",
+ "tremormedia.com",
+ "tremorvideo.com"
+ ]
+ },
+ "Triggit": {
+ "properties": [
+ "triggit.com"
+ ],
+ "resources": [
+ "triggit.com"
+ ]
+ },
+ "TripleLift": {
+ "properties": [
+ "triplelift.com"
+ ],
+ "resources": [
+ "3lift.com",
+ "triplelift.com"
+ ]
+ },
+ "Trovus": {
+ "properties": [
+ "trovus.co.uk",
+ "www.trovus.co.uk"
+ ],
+ "resources": [
+ "trovus.co.uk",
+ "www.trovus.co.uk"
+ ]
+ },
+ "TruEffect": {
+ "properties": [
+ "adlegend.com",
+ "trueffect.com"
+ ],
+ "resources": [
+ "adlegend.com",
+ "trueffect.com"
+ ]
+ },
+ "Trumba": {
+ "properties": [
+ "trumba.com"
+ ],
+ "resources": [
+ "trumba.com"
+ ]
+ },
+ "TRUSTe": {
+ "properties": [
+ "truste.com"
+ ],
+ "resources": [
+ "truste.com"
+ ]
+ },
+ "TrustX": {
+ "properties": [
+ "trustx.org"
+ ],
+ "resources": [
+ "trustx.org"
+ ]
+ },
+ "TubeMogul": {
+ "properties": [
+ "tmogul.com",
+ "tubemogul.com"
+ ],
+ "resources": [
+ "tmogul.com",
+ "tubemogul.com"
+ ]
+ },
+ "TurnTo": {
+ "properties": [
+ "turntonetworks.com"
+ ],
+ "resources": [
+ "turnto.com",
+ "turntonetworks.com"
+ ]
+ },
+ "Tweetboard": {
+ "properties": [
+ "tweetboard.com"
+ ],
+ "resources": [
+ "tweetboard.com"
+ ]
+ },
+ "Twelvefold": {
+ "properties": [
+ "buzzlogic.com",
+ "twelvefold.com"
+ ],
+ "resources": [
+ "buzzlogic.com",
+ "twelvefold.com"
+ ]
+ },
+ "Twitter": {
+ "properties": [
+ "digits.com",
+ "fabric.io",
+ "tweetdeck.com",
+ "twitter.com",
+ "twitter.jp"
+ ],
+ "resources": [
+ "ads-twitter.com",
+ "fabric.io",
+ "tweetdeck.com",
+ "twimg.com",
+ "twitter.com",
+ "twitter.jp"
+ ]
+ },
+ "Twitter Counter": {
+ "properties": [
+ "twittercounter.com"
+ ],
+ "resources": [
+ "twittercounter.com"
+ ]
+ },
+ "Twyn Group": {
+ "properties": [
+ "twyn-group.com",
+ "twyn.com"
+ ],
+ "resources": [
+ "twyn-group.com",
+ "twyn.com"
+ ]
+ },
+ "Tyroo": {
+ "properties": [
+ "tyroo.com"
+ ],
+ "resources": [
+ "tyroo.com"
+ ]
+ },
+ "UberMedia": {
+ "properties": [
+ "tweetup.com",
+ "ubermedia.com"
+ ],
+ "resources": [
+ "tweetup.com",
+ "ubermedia.com"
+ ]
+ },
+ "UberTags": {
+ "properties": [
+ "ubertags.com"
+ ],
+ "resources": [
+ "ubertags.com"
+ ]
+ },
+ "ucfunnel": {
+ "properties": [
+ "ucfunnel.com"
+ ],
+ "resources": [
+ "aralego.com",
+ "ucfunnel.com"
+ ]
+ },
+ "uCoz": {
+ "properties": [
+ "ucoz.ae",
+ "ucoz.com",
+ "ucoz.fr",
+ "ucoz.net",
+ "ucoz.ru"
+ ],
+ "resources": [
+ "ucoz.ae",
+ "ucoz.br",
+ "ucoz.com",
+ "ucoz.du",
+ "ucoz.fr",
+ "ucoz.net",
+ "ucoz.ru"
+ ]
+ },
+ "Umbel": {
+ "properties": [
+ "umbel.com"
+ ],
+ "resources": [
+ "umbel.com"
+ ]
+ },
+ "Unanimis": {
+ "properties": [
+ "unanimis.co.uk",
+ "www.unanimis.co.uk"
+ ],
+ "resources": [
+ "unanimis.co.uk",
+ "www.unanimis.co.uk"
+ ]
+ },
+ "Unbounce": {
+ "properties": [
+ "unbounce.com"
+ ],
+ "resources": [
+ "unbounce.com"
+ ]
+ },
+ "Underdog Media": {
+ "properties": [
+ "udmserve.net",
+ "underdogmedia.com"
+ ],
+ "resources": [
+ "udmserve.net",
+ "underdogmedia.com"
+ ]
+ },
+ "Undertone": {
+ "properties": [
+ "undertone.com",
+ "undertonevideo.com"
+ ],
+ "resources": [
+ "undertone.com",
+ "undertonenetworks.com",
+ "undertonevideo.com"
+ ]
+ },
+ "UniQlick": {
+ "properties": [
+ "51network.com",
+ "uniqlick.com",
+ "wanmo.com"
+ ],
+ "resources": [
+ "51network.com",
+ "uniqlick.com",
+ "wanmo.com"
+ ]
+ },
+ "Unruly": {
+ "properties": [
+ "unruly.co"
+ ],
+ "resources": [
+ "unrulymedia.com"
+ ]
+ },
+ "Upland": {
+ "properties": [
+ "uplandsoftware.com"
+ ],
+ "resources": [
+ "leadlander.com",
+ "sf14g.com",
+ "trackalyzer.com",
+ "uplandsoftware.com"
+ ]
+ },
+ "Uptrends": {
+ "properties": [
+ "uptrends.com"
+ ],
+ "resources": [
+ "uptrends.com"
+ ]
+ },
+ "up-value": {
+ "properties": [
+ "up-value.de"
+ ],
+ "resources": [
+ "up-value.de"
+ ]
+ },
+ "Usability Sciences": {
+ "properties": [
+ "usabilitysciences.com"
+ ],
+ "resources": [
+ "usabilitysciences.com",
+ "webiqonline.com"
+ ]
+ },
+ "User Local": {
+ "properties": [
+ "nakanohito.jp"
+ ],
+ "resources": [
+ "nakanohito.jp"
+ ]
+ },
+ "UserVoice": {
+ "properties": [
+ "uservoice.com"
+ ],
+ "resources": [
+ "uservoice.com"
+ ]
+ },
+ "V12 Data": {
+ "properties": [
+ "v12group.com"
+ ],
+ "resources": [
+ "v12data.com",
+ "v12group.com"
+ ]
+ },
+ "Value Ad": {
+ "properties": [
+ "valuead.com"
+ ],
+ "resources": [
+ "valuead.com"
+ ]
+ },
+ "Various": {
+ "properties": [
+ "amigos.com",
+ "getiton.com",
+ "medley.com",
+ "nostringsattached.com",
+ "various.com"
+ ],
+ "resources": [
+ "amigos.com",
+ "getiton.com",
+ "medley.com",
+ "nostringsattached.com",
+ "various.com"
+ ]
+ },
+ "Vdopia": {
+ "properties": [
+ "ivdopia.com",
+ "vdopia.com"
+ ],
+ "resources": [
+ "ivdopia.com",
+ "vdopia.com"
+ ]
+ },
+ "Veeseo": {
+ "properties": [
+ "veeseo.com"
+ ],
+ "resources": [
+ "veeseo.com"
+ ]
+ },
+ "Velocity Media": {
+ "properties": [
+ "adsvelocity.com"
+ ],
+ "resources": [
+ "adsvelocity.com"
+ ]
+ },
+ "Velti": {
+ "properties": [
+ "mobclix.com",
+ "velti.com"
+ ],
+ "resources": [
+ "mobclix.com",
+ "velti.com"
+ ]
+ },
+ "Vemba": {
+ "properties": [
+ "vemba.com"
+ ],
+ "resources": [
+ "vemba.com"
+ ]
+ },
+ "Venatus Media": {
+ "properties": [
+ "venatusmedia.com"
+ ],
+ "resources": [
+ "venatusmedia.com"
+ ]
+ },
+ "Vendemore": {
+ "properties": [
+ "vendemore.com"
+ ],
+ "resources": [
+ "vendemore.com"
+ ]
+ },
+ "Vendio": {
+ "properties": [
+ "singlefeed.com",
+ "vendio.com"
+ ],
+ "resources": [
+ "singlefeed.com",
+ "vendio.com"
+ ]
+ },
+ "Veoxa": {
+ "properties": [
+ "veoxa.com"
+ ],
+ "resources": [
+ "veoxa.com"
+ ]
+ },
+ "Veremedia": {
+ "properties": [
+ "veremedia.com"
+ ],
+ "resources": [
+ "veremedia.com"
+ ]
+ },
+ "Vertical Acuity": {
+ "properties": [
+ "verticalacuity.com"
+ ],
+ "resources": [
+ "verticalacuity.com"
+ ]
+ },
+ "VerticalHealth": {
+ "properties": [
+ "verticalhealth.com"
+ ],
+ "resources": [
+ "verticalhealth.net"
+ ]
+ },
+ "VerticalResponse": {
+ "properties": [
+ "verticalresponse.com",
+ "vresp.com"
+ ],
+ "resources": [
+ "verticalresponse.com",
+ "vresp.com"
+ ]
+ },
+ "Vertster": {
+ "properties": [
+ "vertster.com"
+ ],
+ "resources": [
+ "vertster.com"
+ ]
+ },
+ "VG WORT": {
+ "properties": [
+ "vgwort.de"
+ ],
+ "resources": [
+ "vgwort.de"
+ ]
+ },
+ "Vibrant Media": {
+ "properties": [
+ "vibrantmedia.com"
+ ],
+ "resources": [
+ "intellitxt.com",
+ "picadmedia.com",
+ "vibrantmedia.com"
+ ]
+ },
+ "VideoIntelligence": {
+ "properties": [
+ "vi.ai"
+ ],
+ "resources": [
+ "vi.ai"
+ ]
+ },
+ "Videology": {
+ "properties": [
+ "tidaltv.com",
+ "videologygroup.com"
+ ],
+ "resources": [
+ "tidaltv.com",
+ "videologygroup.com"
+ ]
+ },
+ "Viewbix": {
+ "properties": [
+ "qoof.com",
+ "viewbix.com"
+ ],
+ "resources": [
+ "qoof.com",
+ "viewbix.com"
+ ]
+ },
+ "VigLink": {
+ "properties": [
+ "viglink.com"
+ ],
+ "resources": [
+ "viglink.com"
+ ]
+ },
+ "Vimeo": {
+ "properties": [
+ "vimeo.com",
+ "vimeocdn.com"
+ ],
+ "resources": [
+ "vimeo.com",
+ "vimeocdn.com"
+ ]
+ },
+ "VINDICO": {
+ "properties": [
+ "vindicogroup.com",
+ "vindicosuite.com"
+ ],
+ "resources": [
+ "vindicogroup.com",
+ "vindicosuite.com"
+ ]
+ },
+ "VisibleBrands": {
+ "properties": [
+ "visbrands.com"
+ ],
+ "resources": [
+ "visbrands.com"
+ ]
+ },
+ "Visible Measures": {
+ "properties": [
+ "visiblemeasures.com"
+ ],
+ "resources": [
+ "viewablemedia.net",
+ "visiblemeasures.com"
+ ]
+ },
+ "VisiStat": {
+ "properties": [
+ "id.kickfire.com",
+ "sa-as.com"
+ ],
+ "resources": [
+ "d.kickfire.com",
+ "sa-as.com",
+ "visistat.com"
+ ]
+ },
+ "Visit Streamer": {
+ "properties": [
+ "visitstreamer.com"
+ ],
+ "resources": [
+ "visitstreamer.com"
+ ]
+ },
+ "vistrac": {
+ "properties": [
+ "vistrac.com"
+ ],
+ "resources": [
+ "vistrac.com"
+ ]
+ },
+ "VisualDNA": {
+ "properties": [
+ "vdna-assets.com",
+ "visualdna-stats.com",
+ "visualdna.com"
+ ],
+ "resources": [
+ "vdna-assets.com",
+ "visualdna-stats.com",
+ "visualdna.com"
+ ]
+ },
+ "ViziSense": {
+ "properties": [
+ "vizisense.com",
+ "vizisense.net"
+ ],
+ "resources": [
+ "vizisense.com",
+ "vizisense.net"
+ ]
+ },
+ "Vizu": {
+ "properties": [
+ "vizu.com"
+ ],
+ "resources": [
+ "vizu.com"
+ ]
+ },
+ "Vizury": {
+ "properties": [
+ "vizury.com"
+ ],
+ "resources": [
+ "vizury.com"
+ ]
+ },
+ "VKontakte": {
+ "properties": [
+ "vk.com"
+ ],
+ "resources": [
+ "userapi.com",
+ "vk.com",
+ "vkontakte.ru"
+ ]
+ },
+ "Voice2Page": {
+ "properties": [
+ "voice2page.com"
+ ],
+ "resources": [
+ "voice2page.com"
+ ]
+ },
+ "Vserv": {
+ "properties": [
+ "vserv.com",
+ "vserv.mobi"
+ ],
+ "resources": [
+ "vserv.com",
+ "vserv.mobi"
+ ]
+ },
+ "Vuble": {
+ "properties": [
+ "vuble.tv"
+ ],
+ "resources": [
+ "mediabong.com"
+ ]
+ },
+ "Wahoha": {
+ "properties": [
+ "contentwidgets.net",
+ "wahoha.com"
+ ],
+ "resources": [
+ "contentwidgets.net",
+ "wahoha.com"
+ ]
+ },
+ "Wayfair": {
+ "properties": [
+ "wayfair.com"
+ ],
+ "resources": [
+ "wayfair.com"
+ ]
+ },
+ "WebAds": {
+ "properties": [
+ "webads.co.uk",
+ "www.webads.co.uk"
+ ],
+ "resources": [
+ "webads.co.uk",
+ "www.webads.co.uk"
+ ]
+ },
+ "Webclicktracker": {
+ "properties": [
+ "webclicktracker.com"
+ ],
+ "resources": [
+ "webclicktracker.com"
+ ]
+ },
+ "Web.com": {
+ "properties": [
+ "feedperfect.com",
+ "web.com"
+ ],
+ "resources": [
+ "feedperfect.com",
+ "web.com"
+ ]
+ },
+ "WebGozar.com": {
+ "properties": [
+ "webgozar.com",
+ "webgozar.ir"
+ ],
+ "resources": [
+ "webgozar.com",
+ "webgozar.ir"
+ ]
+ },
+ "Webmecanik": {
+ "properties": [
+ "webmecanik.com"
+ ],
+ "resources": [
+ "webmecanik.com"
+ ]
+ },
+ "WebMetro": {
+ "properties": [
+ "dsmmadvantage.com",
+ "revanadigital.com"
+ ],
+ "resources": [
+ "dsmmadvantage.com",
+ "revanadigital.com",
+ "webmetro.com"
+ ]
+ },
+ "Webmine": {
+ "properties": [
+ "webmine.cz"
+ ],
+ "resources": [
+ "authedwebmine.cz",
+ "webmine.cz"
+ ]
+ },
+ "WebminePool": {
+ "properties": [
+ "webminepool.com"
+ ],
+ "resources": [
+ "webminepool.com"
+ ]
+ },
+ "Webmining": {
+ "properties": [
+ "webmining.co"
+ ],
+ "resources": [
+ "webmining.co"
+ ]
+ },
+ "Weborama": {
+ "properties": [
+ "weborama.com"
+ ],
+ "resources": [
+ "weborama.com",
+ "weborama.fr"
+ ]
+ },
+ "WebsiteAlive": {
+ "properties": [
+ "websitealive.com",
+ "websitealive0.com",
+ "websitealive1.com",
+ "websitealive2.com",
+ "websitealive3.com",
+ "websitealive4.com",
+ "websitealive5.com",
+ "websitealive6.com",
+ "websitealive7.com",
+ "websitealive8.com",
+ "websitealive9.com"
+ ],
+ "resources": [
+ "websitealive.com"
+ ]
+ },
+ "Web Stats": {
+ "properties": [
+ "onlinewebstats.com"
+ ],
+ "resources": [
+ "onlinewebstats.com"
+ ]
+ },
+ "Web Tracking Services": {
+ "properties": [
+ "web-stat.com",
+ "webtrackingservices.com"
+ ],
+ "resources": [
+ "web-stat.com",
+ "webtrackingservices.com"
+ ]
+ },
+ "Webtraffic": {
+ "properties": [
+ "webtraffic.no",
+ "webtraffic.se"
+ ],
+ "resources": [
+ "webtraffic.no",
+ "webtraffic.se"
+ ]
+ },
+ "Web Traxs": {
+ "properties": [
+ "webtraxs.com"
+ ],
+ "resources": [
+ "webtraxs.com"
+ ]
+ },
+ "Webtrekk": {
+ "properties": [
+ "webtrekk.com",
+ "webtrekk.net"
+ ],
+ "resources": [
+ "webtrekk.com",
+ "webtrekk.net"
+ ]
+ },
+ "Webtrends": {
+ "properties": [
+ "webtrends.com"
+ ],
+ "resources": [
+ "reinvigorate.net",
+ "webtrends.com",
+ "webtrendslive.com"
+ ]
+ },
+ "White Ops": {
+ "properties": [
+ "adzmath.com",
+ "whiteops.com"
+ ],
+ "resources": [
+ "adzmath.com",
+ "whiteops.com"
+ ]
+ },
+ "whos.amung.us": {
+ "properties": [
+ "amung.us"
+ ],
+ "resources": [
+ "amung.us"
+ ]
+ },
+ "WideOrbit": {
+ "properties": [
+ "wideorbit.com"
+ ],
+ "resources": [
+ "dep-x.com"
+ ]
+ },
+ "Wingify": {
+ "properties": [
+ "vwo.com",
+ "wingify.com"
+ ],
+ "resources": [
+ "visualwebsiteoptimizer.com",
+ "vwo.com",
+ "wingify.com"
+ ]
+ },
+ "WiredMinds": {
+ "properties": [
+ "wiredminds.de"
+ ],
+ "resources": [
+ "wiredminds.com",
+ "wiredminds.de"
+ ]
+ },
+ "Wishabi": {
+ "properties": [
+ "wishabi.com",
+ "wishabi.net"
+ ],
+ "resources": [
+ "flipp.com",
+ "wishabi.com",
+ "wishabi.net"
+ ]
+ },
+ "Woopra": {
+ "properties": [
+ "woopra-ns.com",
+ "woopra.com"
+ ],
+ "resources": [
+ "woopra-ns.com",
+ "woopra.com"
+ ]
+ },
+ "WordStream": {
+ "properties": [
+ "wordstream.com"
+ ],
+ "resources": [
+ "wordstream.com"
+ ]
+ },
+ "WOW Analytics": {
+ "properties": [
+ "wowanalytics.co.uk"
+ ],
+ "resources": [
+ "wowanalytics.co.uk"
+ ]
+ },
+ "WPP": {
+ "properties": [
+ "compete.com",
+ "decdna.net",
+ "groupm.com",
+ "kantarmedia.com",
+ "mecglobal.com",
+ "mindshareworld.com",
+ "themig.com",
+ "wpp.com",
+ "xaxis.com"
+ ],
+ "resources": [
+ "247realmedia.com",
+ "accelerator-media.com",
+ "acceleratorusa.com",
+ "compete.com",
+ "decdna.net",
+ "decideinteractive.com",
+ "gmads.net",
+ "groupm.com",
+ "kantarmedia.com",
+ "mecglobal.com",
+ "mindshare.nl",
+ "mindshareworld.com",
+ "mookie1.com",
+ "pm14.com",
+ "realmedia.com",
+ "targ.ad",
+ "themig.com",
+ "wpp.com",
+ "xaxis.com"
+ ]
+ },
+ "Wysistat": {
+ "properties": [
+ "wysistat.net"
+ ],
+ "resources": [
+ "wysistat.com",
+ "wysistat.net"
+ ]
+ },
+ "xAd": {
+ "properties": [
+ "xad.com"
+ ],
+ "resources": [
+ "xad.com"
+ ]
+ },
+ "Xertive Media": {
+ "properties": [
+ "xertivemedia.com"
+ ],
+ "resources": [
+ "admanager-xertive.com",
+ "xertivemedia.com"
+ ]
+ },
+ "xplosion interactive": {
+ "properties": [
+ "xplosion.de"
+ ],
+ "resources": [
+ "xplosion.de"
+ ]
+ },
+ "Xrost DS": {
+ "properties": [
+ "adplan-ds.com"
+ ],
+ "resources": [
+ "adplan-ds.com"
+ ]
+ },
+ "Yabuka": {
+ "properties": [
+ "yabuka.com"
+ ],
+ "resources": [
+ "yabuka.com"
+ ]
+ },
+ "Yahoo!": {
+ "properties": [
+ "flickr.com",
+ "flurry.com",
+ "tumblr.com",
+ "yahoo.co.jp",
+ "yahoo.com",
+ "yahoostudios.com",
+ "yuilibrary.com"
+ ],
+ "resources": [
+ "adinterax.com",
+ "adrevolver.com",
+ "bluelithium.com",
+ "dapper.net",
+ "flickr.com",
+ "flurry.com",
+ "interclick.com",
+ "luminate.com",
+ "mybloglog.com",
+ "overture.com",
+ "pixazza.com",
+ "rightmedia.com",
+ "rmxads.com",
+ "rocketmail.com",
+ "secure-adserver.com",
+ "staticflickr.com",
+ "tumblr.com",
+ "yahoo.co.jp",
+ "yahoo.com",
+ "yahooapis.com",
+ "yahooapis.jp",
+ "yahoofs.com",
+ "yieldmanager.com",
+ "yieldmanager.net",
+ "yimg.com",
+ "yimg.jp",
+ "yldmgrimg.net",
+ "ymail.com",
+ "yuilibrary.com",
+ "zenfs.com"
+ ]
+ },
+ "Yandex": {
+ "properties": [
+ "kinopoisk.ru",
+ "moikrug.ru",
+ "yadi.sk",
+ "yandex.by",
+ "yandex.com",
+ "yandex.com.tr",
+ "yandex.ru",
+ "yandex.st",
+ "yandex.ua"
+ ],
+ "resources": [
+ "api-maps.yandex.ru",
+ "moikrug.ru",
+ "web-visor.com",
+ "yandex.by",
+ "yandex.com",
+ "yandex.com.tr",
+ "yandex.ru",
+ "yandex.st",
+ "yandex.ua"
+ ]
+ },
+ "Ybrant Digital": {
+ "properties": [
+ "addynamix.com",
+ "brightcom.com",
+ "luj.sdsjweb.com"
+ ],
+ "resources": [
+ "addynamix.com",
+ "adserverplus.com",
+ "brightcom.com",
+ "oridian.com",
+ "ybrantdigital.com"
+ ]
+ },
+ "YD": {
+ "properties": [
+ "ydworld.com",
+ "yieldivision.com"
+ ],
+ "resources": [
+ "ydworld.com",
+ "yieldivision.com"
+ ]
+ },
+ "YellowHammer": {
+ "properties": [
+ "yhmg.com"
+ ],
+ "resources": [
+ "attracto.com",
+ "clickhype.com",
+ "yellowhammermg.com",
+ "yhmg.com"
+ ]
+ },
+ "YellowTracker": {
+ "properties": [
+ "yellowtracker.com"
+ ],
+ "resources": [
+ "yellowtracker.com"
+ ]
+ },
+ "Yes Ads": {
+ "properties": [
+ "yesads.com"
+ ],
+ "resources": [
+ "yesads.com"
+ ]
+ },
+ "YieldAds": {
+ "properties": [
+ "yieldads.com"
+ ],
+ "resources": [
+ "yieldads.com"
+ ]
+ },
+ "YieldBids": {
+ "properties": [
+ "ybx.io"
+ ],
+ "resources": [
+ "ybx.io"
+ ]
+ },
+ "YieldBot": {
+ "properties": [
+ "yieldbot.com"
+ ],
+ "resources": [
+ "yldbt.com"
+ ]
+ },
+ "YieldBuild": {
+ "properties": [
+ "yieldbuild.com"
+ ],
+ "resources": [
+ "yieldbuild.com"
+ ]
+ },
+ "Yieldify": {
+ "properties": [
+ "yieldify.com"
+ ],
+ "resources": [
+ "yieldify.com"
+ ]
+ },
+ "Yieldlab": {
+ "properties": [
+ "yieldlab.de",
+ "yieldlab.net"
+ ],
+ "resources": [
+ "yieldlab.de",
+ "yieldlab.net"
+ ]
+ },
+ "Yieldmo": {
+ "properties": [
+ "yieldmo.com"
+ ],
+ "resources": [
+ "yieldmo.com"
+ ]
+ },
+ "YieldNexus": {
+ "properties": [
+ "ynxs.io"
+ ],
+ "resources": [
+ "ynxs.io"
+ ]
+ },
+ "YOC": {
+ "properties": [
+ "yoc.com"
+ ],
+ "resources": [
+ "yoc.com"
+ ]
+ },
+ "Yoggrt": {
+ "properties": [
+ "yoggrt.com"
+ ],
+ "resources": [
+ "yoggrt.com"
+ ]
+ },
+ "youknowbest": {
+ "properties": [
+ "youknowbest.com"
+ ],
+ "resources": [
+ "youknowbest.com"
+ ]
+ },
+ "YSance": {
+ "properties": [
+ "ysance.com"
+ ],
+ "resources": [
+ "y-track.com"
+ ]
+ },
+ "YuMe": {
+ "properties": [
+ "yume.com",
+ "yumenetworks.com"
+ ],
+ "resources": [
+ "yume.com",
+ "yumenetworks.com"
+ ]
+ },
+ "ZafulAffiliate": {
+ "properties": [
+ "zaful.com"
+ ],
+ "resources": [
+ "zaful.com"
+ ]
+ },
+ "Zango": {
+ "properties": [
+ "metricsdirect.com",
+ "zango.com"
+ ],
+ "resources": [
+ "metricsdirect.com",
+ "zango.com"
+ ]
+ },
+ "zanox": {
+ "properties": [
+ "buy.at",
+ "zanox-affiliate.de",
+ "zanox.com"
+ ],
+ "resources": [
+ "buy.at",
+ "zanox-affiliate.de",
+ "zanox.com"
+ ]
+ },
+ "zapunited": {
+ "properties": [
+ "zaparena.com",
+ "zapunited.com"
+ ],
+ "resources": [
+ "zaparena.com",
+ "zapunited.com"
+ ]
+ },
+ "ZEDO": {
+ "properties": [
+ "zedo.com",
+ "zincx.com"
+ ],
+ "resources": [
+ "zedo.com",
+ "zincx.com"
+ ]
+ },
+ "Zefir": {
+ "properties": [
+ "ze-fir.com"
+ ],
+ "resources": [
+ "ze-fir.com"
+ ]
+ },
+ "Zemanta": {
+ "properties": [
+ "zemanta.com"
+ ],
+ "resources": [
+ "zemanta.com"
+ ]
+ },
+ "Zendesk": {
+ "properties": [
+ "zendesk.com"
+ ],
+ "resources": [
+ "zendesk.com"
+ ]
+ },
+ "ZestAd": {
+ "properties": [
+ "zestad.com"
+ ],
+ "resources": [
+ "zestad.com"
+ ]
+ },
+ "Zeta Email Solutions": {
+ "properties": [
+ "insightgrit.com",
+ "zetaemailsolutions.com"
+ ],
+ "resources": [
+ "insightgrit.com",
+ "zetaemailsolutions.com"
+ ]
+ },
+ "Zopim": {
+ "properties": [
+ "zopim.com"
+ ],
+ "resources": [
+ "zopim.com"
+ ]
+ },
+ "Zumobi": {
+ "properties": [
+ "zumobi.com"
+ ],
+ "resources": [
+ "zumobi.com"
+ ]
+ },
+ "ZypMedia": {
+ "properties": [
+ "zypmedia.com"
+ ],
+ "resources": [
+ "extend.tv",
+ "zypmedia.com"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..e52c54b1a8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-am/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">በ%1$s ላይ ያለው ገጽ እንዲህ ይላል፡-</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s የእርስዎን የተጠቃሚ ስም እና የይለፍ ቃል እየጠየቀ ነው። ድረ-ገፁ እንዲህ ይላል፡- &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s የእርስዎን የተጠቃሚ ስም እና የይለፍ ቃል እየጠየቀ ነው።</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..676854b02c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-an/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La pachina en %1$s diz:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s te ye pedindo lo tuyo nombre d’usuario y clau. Lo puesto diz: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s te ye pedindo lo tuyo nombre d’usuario y clau.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ann/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ann/strings.xml
new file mode 100644
index 0000000000..96cb729e73
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ann/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Akpọk òkup me %1$s ìbe:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ìkido erieen̄ òsikwaan̄ kwun̄ mè ikọ-atafia. Akpatan̄ ya ìbe: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ìkido erieen̄ òsikwaan̄ kwun̄ mè ikọ-atafia.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..7b36637be6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ar/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">تقول الصفحة في %1$s:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">يطلب الوسيط %2$s اسم مستخدم و كلمة سر. يقول الموقع: ”%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">يطلب %1$s اسم المستخدم و كلمة السر.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..5cf994aa8e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ast/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La páxina de «%1$s» diz:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">«%2$s» solicita un nome d\'usuariu y una contraseña. El sitiu diz «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">«%1$s» solicita un nome d\'usuariu y una contraseña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..0f445744de
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-az/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s səhifəsi deyir ki:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s istifadəçi adı və parolunuzu istəyir. Sayt deyir ki: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s istifadəçi adı və parolunuzu istəyir.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..2c64a4e2ba
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-azb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s یارپاغیندا دئییلیر:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s قوللانیجی آدینیزی و رمزینیزی ایستییر. سایت‌دا دئییلیر: ”%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s قوللانیجی آدینیزی و رمزینیزی ایستییر.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..ed2eb2520d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ban/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Kaca ring %1$s mapikobet:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ngidih aran sang anganggé miwah kruna sandi Ragané. Situs nyuratang: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ngidih aran sang anganggé miwah sandi Ragané.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..d29e4e6dfa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-be/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Старонка на %1$s паведамляе:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s запытвае імя карыстальніка і пароль. Сайт паведамляе: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s запытвае імя карыстальніка і пароль.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..e8b1b76299
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bg/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Страницата на %1$s казва:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s пита за вашето потребителско име и парола. Сайтът казва: „%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s пита за вашето потребителско име и парола.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..7cc8bde2c2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bn/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s পাতায় বলা হয়েছে:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s আপনার ব্যবহারকারী নাম এবং পাসওয়ার্ডের জন্য অনুরোধ করছে। সাইটটি বলছে: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s আপনার ব্যবহারকারী নাম এবং পাসওয়ার্ড অনুরোধ করছে।</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..882b2fbab0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-br/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Ar bajenn war %1$s a lâr:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">Emañ ar proksi %2$s ocʼh azgoulenn un anv arveriad hag ur ger-tremen. Emañ al lecʼhienn o lavarout: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">Emañ ar proksi %1$s ocʼh azgoulenn un anv arveriad hag ur ger-tremen.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..3d65bfb52b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-bs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Stranica pri %1$s kaže:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s zahtijeva korisničko ime i lozinku. Stranica vraća odgovor: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s zahtijeva tvoje korisničko ime i lozinku.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..488d0cf419
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ca/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La pàgina a %1$s diu:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s sol·licita el vostre nom d’usuari i contrasenya. El lloc diu: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s sol·licita el vostre nom d’usuari i contrasenya.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..2054981f69
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cak/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Ri ruxaq %1$s nub\'ij:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s nuk\'utuj rub\'i\' winäq chuqa\' jun ewan tzij. Ri ruxaq k\'amaya\'l nub\'ij: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s nuk\'utuj ri rub\'i\' ataqoya\'l chuqa\' ewan atzij.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..33ffd2b3d7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Ang page sa %1$s ingon:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s nangayo sa imong username ug password. Ang site ingon: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s nangayo sa imong username ug password.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..baa85b12a8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">پەڕە لە %1$s دەڵێت:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s داوای ناوی بەکارهێنەر و وشەی تێپەڕبوون دەکات. ماڵپەڕەکە دەڵێت: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s داوای ناوی بەکارهێنەر و ووشەی تێپەڕبوون دەکات.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..26e36461c0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-co/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Annunziamentu di a pagina %1$s :find in page</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">U situ %2$s richiede u vostru nome d’utilizatore è a vostra parolla d’intesa. U site indica : « %1$s »</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">U situ %1$s richiede u vostru nome d’utilizatore è a vostra parolla d’intesa.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..bc5b5608f0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Sdělení stránky %1$s:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s požaduje vaše uživatelské jméno a heslo. Sdělení serveru: „%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s požaduje vaše uživatelské jméno a heslo.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..0ab3645f16
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-cy/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Mae tudalen yn %1$s yn dweud:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">Mae %2$s yn gofyn am enw defnyddiwr a chyfrinair. Mae’r wefan yn dweud: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">Mae %1$s yn gofyn am eich enw defnyddiwr a chyfrinair.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..2e5451caef
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-da/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Siden på %1$s siger:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s beder om dit brugernavn og din adgangskode. Webstedet siger: &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s beder om dit brugernavn og din adgangskode.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..0fcf258a76
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-de/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Die Seite mit der Adresse %1$s meldet:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s verlangt einen Benutzernamen und ein Passwort. Ausgabe der Website: „%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s verlangt einen Benutzernamen und ein Passwort.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..187703d7c9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Bok na %1$s groni:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s pomina wužywarske mě a gronidło. Sedło groni: &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s pomina wašo wužywarske mě a gronidło.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..7376515b82
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-el/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Η σελίδα στο %1$s δηλώνει:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">Το %2$s ζητά όνομα χρήστη και κωδικό πρόσβασής. Ο ιστότοπος δηλώνει: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">Το %1$s ζητά το όνομα χρήστη και τον κωδικό πρόσβασής σας.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..b70ef57248
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">The page at %1$s says:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s is requesting your username and password. The site says: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s is requesting your username and password.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..b70ef57248
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">The page at %1$s says:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s is requesting your username and password. The site says: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s is requesting your username and password.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..aaeb343bab
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-eo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La paĝo ĉe %1$s diras:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s petas vian nomon de uzanto kaj pasvorton. La retejo diras &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s petas vian nomon de uzanto kaj pasvorton.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..8f0dde846d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La página en %1$s dice:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s está pidiendo tu nombre de usuario y contraseña. El sitio dice: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s te está pidiendo tu nombre de usuario y contraseña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..f679763411
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La página en %1$s dice:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s está solicitando tu usuario y contraseña. El sitio dice: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s está solicitando tu usuario y contraseña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..a9ccba99c5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La página en %1$s dice:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s te está pidiendo tu nombre de usuario y contraseña. El sitio dice: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s te está pidiendo tu nombre de usuario y contraseña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..6c3bd9f23c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La página en %1$s dice:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s está solicitando tu nombre de usuario y contraseña. El sitio dice: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s está solicitando tu nombre de usuario y contraseña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..a9ccba99c5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-es/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La página en %1$s dice:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s te está pidiendo tu nombre de usuario y contraseña. El sitio dice: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s te está pidiendo tu nombre de usuario y contraseña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..2efd14bd0d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-et/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Veebileht aadressil %1$s ütleb:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">Sait aadressil %2$s nõuab kasutajanime ja parooli. Teade saidilt: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s nõuab kasutajanime ja parooli.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..23cf8239d3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-eu/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s helbideko orriak hau dio:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s guneak erabiltzaile-izen eta pasahitza eskatzen ditu. Guneak hau dio: &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s guneak erabiltzaile-izen eta pasahitza eskatzen ditu.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..ce4b29bd0e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fa/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">صفحهٔ %1$s می‌گوید:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s درخواست نام کاربری و گذرواژهٔ شما را دارد. این پایگاه می‌گوید: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s درخواست نام کاربری و گذرواژهٔ شما را دارد.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..206ab42aa6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ff/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Hello wonngo to %1$s wiyi:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ena naamnii innde kuutoro maa e finnde. Lowre ndee wiyi: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ena naamnii innde kuutoro maa e finnde.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..af33a323c4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fi/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Sivu osoitteessa %1$s sanoo:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s pyytää käyttäjätunnusta ja salasanaa. Sivusto sanoo: ”%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s pyytää käyttäjätunnusta ja salasanaa.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..d4f313637b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Annonce de la page %1$s :</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">Le site %2$s demande votre nom d’utilisateur et votre mot de passe. Le site indique : « %1$s »</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s demande votre nom d’utilisateur et votre mot de passe.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..96aef64c14
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fur/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La pagjine su %1$s e dîs:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s al domande il to non utent e la password. Il sît al dîs: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s al domande il to non utent e la password.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..3350940938
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">De side op %1$s meldt:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s freget om jo brûkersnamme en wachtwurd. De website meldt: ‘%1$s’</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s freget om jo brûkersnamme en wachtwurd.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..5ff5ac4408
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Deir an leathanach ag %1$s:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">Tá %2$s ag iarraidh ainm úsáideora agus focal faire uait. Deir an suíomh: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">Tá %1$s ag iarraidh ainm úsáideora agus focal faire uait.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..00304632ce
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gd/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Tha an duilleag aig %1$s ag ràdh:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">Tha am progsaidh %2$s ag iarraidh ainm-cleachdaiche is facal-faire. Tha an làrach ag ràdh: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">Tha %1$s ag iarraidh an ainm-chleachdaiche is an fhacail-fhaire agad.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..2f63376db4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">A páxina en %1$s di:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s solicita o seu nome de usuario e o contrasinal. O sitio di: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s solicita o seu nome de usuario e o contrasinal.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..0445a456da
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gn/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Pe kuatiarogue %1$s pegua he’i:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ojerure poruhára réra ha avei ñe’ẽñemi. Pe tenda he’i: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ojerure nde poruhára réra ha ñe’ẽñemi.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..73ee5f15b2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s આગળનું પાનું આમ કહે છે:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s તમારું વપરાશકર્તા નામ અને પાસવર્ડની વિનંતી કરી રહ્યું છે. આ સાઇટ કહે છે: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s તમારા વપરાશકર્તા નામ અને પાસવર્ડની વિનંતી કરી રહ્યું છે.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..99d77afaaa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s पर मौजूद पृष्ठ का कहना है:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s आपके उपयोगकर्ता नाम और पासवर्ड का अनुरोध कर रहा है। साइट का कहना है: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s आपके उपयोगकर्ता नाम और पासवर्ड का अनुरोध कर रहा है।</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..fc19280ac9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hil/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Ang pahina sa %1$s nagasiling:</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..3d65bfb52b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Stranica pri %1$s kaže:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s zahtijeva korisničko ime i lozinku. Stranica vraća odgovor: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s zahtijeva tvoje korisničko ime i lozinku.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..9886d5a970
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Strona na %1$s praji:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s žada sej waše wužiwarske mjeno a hesło. Sydło praji: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s sej waše wužiwarske mjeno a hesło žada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..61f9314eb1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hu/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Az oldal a(z) %1$s helyen azt mondja:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">A(z) %2$s felhasználónevet és jelszót kér. A webhely üzenete: „%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">A(z) %1$s felhasználónevet és jelszót kér.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..3f5ca0b268
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title"> %1$s-ից էջը հաղորդում է`</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s-ը պահանջում է օգտվողի անուն և գաղտնաբառ: Կայքը հաղորդում է` &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s-ը հարցնում է օգտվողի Ձեր անունը և գաղտնաբառը</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..6b1d1e8d3b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ia/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Le pagina sur %1$s dice:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s requesta tu nomine de usator e contrasigno. Le sito dice: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s requesta tu nomine de usator e contrasigno.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..589964cfea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-in/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Laman dari %1$s menjelaskan:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s meminta nama pengguna dan sandi anda. Situs ini berkata: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s meminta nama pengguna dan sandi anda.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..afc6c1bccc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-is/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Síðan %1$s segir:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s er að biðja um notandanafnið þitt og lykilorð. Tilkynningin frá vefsvæðinu er: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s er að biðja um notandanafnið þitt og lykilorð.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..2bb6759963
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-it/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La pagina sul server %1$s riporta:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s richiede un nome utente e una password. Il sito riporta: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s richiede un nome utente e una password.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..c07e1eb199
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-iw/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">הדף %1$s אומר:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">השרת %2$s מבקש את שם המשתמש והססמה שלך. מהאתר נמסר: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">השרת %1$s מבקש את שם המשתמש והססמה שלך.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..f04da64533
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ja/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s のページから:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s がユーザー名とパスワードを要求しています。サイトからのメッセージ: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s がユーザー名とパスワードを要求しています。</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..b428118f78
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ka/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">გვერდი %1$s გამცნობთ:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ითხოვს მომხმარებლის სახელსა და პაროლს. საიტი გამცნობთ: „%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ითხოვს მომხმარებლის სახელსა და პაროლს.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..bca1aa650f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s betinde aytılıwınsha:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s paydalanıwshı atın hám parolin sorap atır. Sayt aytıwınsha: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s paydalanıwshı atın hám parolin sorap atır.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..92b5287629
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kab/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Asebter deg %1$s yeqqar-d:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s yessutur isem-ik n useqdac akked wawal-ik uffir. Asmel yaqqar: &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s yessutur isem-ik n useqdac akked wawal-ik uffir.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..8776c5afc5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s адресіндегі бет хабарлайды:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s сайты сіздің пайдаланушы атын мен паролін сұрайды. Сайт айтады: &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s сіздің пайдаланушы атын және паролін сұрап тұр.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..7009b2f611
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Rûpela %1$s’ê dibêje:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s navê bikarhêner û pêborîna te dixwaze. Malper dibêje: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s navê bikarhêner û pêborîna te dixwaze.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..f787624f46
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kn/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s ನಲ್ಲಿರುವ ಪುಟವು ಹೀಗೆ ಹೇಳುತ್ತದೆ:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ಎಂಬ ಪ್ರಾಕ್ಸಿಯು ಒಂದು ಬಳಕೆದಾರ ಪದ ಹಾಗು ಗುಪ್ತಪದಕ್ಕಾಗಿ ಮನವಿ ಸಲ್ಲಿಸಿದೆ. ತಾಣವು ಹೀಗೆ ಹೇಳುತ್ತದೆ: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ನಿಮ್ಮ ಬಳಕೆದಾರನ ಹೆಸರು ಮತ್ತು ಪ್ರವೇಶ ಪದ ಕೇಳುತ್ತಿದೆ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..a7fa3f43ba
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ko/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s 페이지의 메세지:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s가 사용자 이름과 비밀번호를 요청하고 있습니다. 사이트 메시지: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s가 사용자 이름과 비밀번호를 요청하고 있습니다.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kw/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kw/strings.xml
new file mode 100644
index 0000000000..533de3e828
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-kw/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">An folen orth %1$s a lever:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s a bys a\'th hanow devnydhyer ha ger tremena. An wiasva a lever: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s a bys a\'th hanow devnydhyer ha ger tremena.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..60d8ae087c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lij/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">A pagina in %1$s a dixe:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s o domanda o teu nomme utente e paròlla segreta. O scito o dixe: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s o domanda o teu nomme utente e paròlla segreta.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..f6703c74e8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">ຫນ້າເວັບທີ່ %1$s ລະບຸວ່າ:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ຕ້ອງການຊື່ຜູ້ໃຊ້ ແລະ ລະຫັດຜ່ານຂອງທ່ານ. ເວັບໄຊທລະບຸວ່າ: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ຕ້ອງການຊື່ຜູ້ໃຊ້ ແລະ ລະຫັດຜ່ານຂອງທ່ານ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..f17c191cdc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-lt/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Tinklalapis %1$s praneša:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s reikalauja jūsų vardo ir slaptažodžio. Svetainės pranešimas: „%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s reikalauja jūsų vardo iš slaptažodžio.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..372b02ea26
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-mix/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Página nu %1$s katyi:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s tsiki sivi tsi tu un se e. Ndaka tu in: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s tsiki sivi tsi tu un se e.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..0728f4572a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ml/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s ൽ ഉള്ള പേജ് പറയുന്നത്:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ന് നിങ്ങളുടെ ഉപയോക്തൃനാമവും രഹസ്യവാക്കും ആവശ്യമാണ് . സൈറ്റ് പറയുന്നത് : “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s നിങ്ങളുടെ ഉപയോക്തൃനാമവും രഹസ്യവാക്കും ആവശ്യപെടുന്നു.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..c7cc6930a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-mr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s वरील पृष्ठ म्हणते:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s वापरकर्तानाव व पासवर्डसाठी विनंती करत आहे. स्थळ असे म्हणते: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s आपल्या वापरकर्तानाव आणि पासवर्डसाठी विनंती करत आहे.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..9d9a02f29c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-my/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s ရှိစာမျက်နှာက -</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s သည်သင်၏သုံးစွဲသူအမည်နှင့်စကားဝှက်ကိုတောင်းနေသည်။ ဆိုဒ်တွင်“%1$s” ဆိုထားသည်။</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s သည်သင်၏သုံးစွဲသူအမည်နှင့်စကားဝှက်ကိုတောင်းနေသည်။</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..ac3004ffc6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Nettsiden på %1$s sier:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ber om brukernavn og passord. Nettstedet sier: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s krever brukernavn og passord.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..56135b2712
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title"> %1$s मा रहेको पृष्ठले भन्छ:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message"> %2$s ले प्रयोगकर्ता नाम र पासवर्ड अनुरोध गरिरहेको छ। साइट भन्छ: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message"> %1$s ले तपाईँको प्रयोगकर्ता नाम र पासवर्ड अनुरोध गरेको छ।</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..71b2ddf0b2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">De pagina op %1$s zegt:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s vraagt om uw gebruikersnaam en wachtwoord. De website zegt: ‘%1$s’</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s vraagt om uw gebruikersnaam en wachtwoord.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..7763857a1f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Nettsida på %1$s seier:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ber om brukarnamn og passord. Nettstaden seier: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s krev brukarnamn og passord.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nv/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nv/strings.xml
new file mode 100644
index 0000000000..e305ee9534
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-nv/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Níłch’i naaltsoos -gi ’ání %1$s:</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..cdf223c0f7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-oc/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Anóncia de la pagina %1$s :</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s demanda un nom d’utilizaire e un senhal. Lo site indica : « %1$s »</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s demanda vòstres nom d’utilizaire e senhal.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..e93ac76cf2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-or/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s ରେ ଥିବା ପୃଷ୍ଠା କହେ:</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..8a6d8fac81
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s ਉੱਤੇ ਸਫ਼ਾ ਦਰਸਾਉਂਦਾ ਹੈ:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ਤੁਹਾਡੇ ਵਰਤੋਂਕਾਰ-ਨਾਂ ਅਤੇ ਪਾਸਵਰਡ ਦੀ ਮੰਗ ਕਰ ਰਹੀ ਹੈ। ਸਾਈਟ ਕਹਿੰਦੀ ਹੈ: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ਤੁਹਾਡੇ ਵਰਤੋਂਕਾਰ-ਨਾਂ ਅਤੇ ਪਾਸਵਰਡ ਦੀ ਮੰਗ ਕਰ ਰਹੀ ਹੈ। </string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..acabc7da8e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">پتے %1$s دے صفحے توں سنیہا:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s تہاڈے ورتنوالے دا ناں تے پاس‌ورڈ دی منگ کر رہی اے۔ ایتھوں سنیہا اے – ”%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s تہاڈے ورتنوالے دا ناں تے پاس‌ورڈ دی منگ کر رہی اے۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..8a242ccbd4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Komunikat ze strony %1$s:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s prosi o podanie nazwy użytkownika i hasła. Komunikat witryny: „%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s prosi o podanie nazwy użytkownika i hasła.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ppl/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ppl/strings.xml
new file mode 100644
index 0000000000..8ed3d80af8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ppl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Ne iswat tik %1$s ina:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s metztajtanilia muusuariojtukay wan muichtakataketzalis. Ne sitioj ina: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s metztajtanilia muusuariojtukay wan muichtakataketzalis.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..0378158e27
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">A página em %1$s diz:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s está solicitando seu nome de usuário e senha. O site diz: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s está solicitando seu nome de usuário e senha.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..abac1094bb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">A página %1$s diz:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s está a solicitar o seu nome de utilizador e a palavra-passe. O site diz: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s está a solicitar o seu nome de utilizador e a palavra-passe.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..af664e1e22
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-rm/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">La pagina cun l\'adressa %1$s di:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s dumonda tes num d\'utilisader e pled-clav. La pagina di: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s dumonda tes num d\'utilisader e pled-clav.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..1dfac786f0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ro/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Pagina de la %1$s spune:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s solicită numele de utilizator și parola. Site-ul spune: „%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s solicită numele de utilizator și parola.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..04b5fe508a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ru/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Страница на %1$s сообщает:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s запрашивает имя пользователя и пароль. Сайт сообщает: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s запрашивает имя пользователя и пароль.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..e16abb8a6d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sat/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s ᱴᱷᱮᱱ ᱢᱮᱱᱟᱜ ᱥᱟᱦᱴᱟ ᱢᱮᱱᱮᱜᱼᱟᱭ:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ᱫᱚ ᱟᱢᱟᱜ ᱭᱩᱡᱟᱹᱨᱱᱮᱢ ᱟᱨ ᱯᱟᱥᱥᱣᱟᱹᱨᱰ ᱠᱷᱚᱡ ᱠᱟᱱᱟᱭ ᱾ ᱥᱟᱭᱴ ᱮ ᱢᱮᱱᱮᱜᱼᱟᱭ: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ᱫᱚ ᱟᱢᱟᱜ ᱭᱩᱡᱟᱹᱨᱱᱮᱢ ᱟᱨ ᱯᱟᱥᱣᱟᱹᱨᱰ ᱠᱷᱚᱡ ᱠᱟᱱᱟᱭ ᱾</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..8088687053
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sc/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Sa pàgina in %1$s narat:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s rechedet su nòmine tuo e sa crae. Su situ narat: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s rechedet su nòmine tuo e sa crae.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..2cdf4bed5e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-si/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s හි පිටුව මෙසේ පවසයි:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ඔබගේ පරිශීලක නාමය සහ මුරපදය ඉල්ලා සිටියි. අඩවිය පවසන්නේ: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ඔබගේ පරිශීලක නාමය සහ මුරපදය ඉල්ලා සිටියි.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..df5bf93d16
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Oznámenie stránky %1$s:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s požaduje zadanie vášho používateľského mena a hesla. Oznámenie stránky: „%1$s“</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s požaduje zadanie vášho používateľského mena a hesla.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..f5b21bf699
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-skr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s تے ورقہ آہدے:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ورتݨ ناں تے پاسورڈ دی ارداس کریندا پئے۔ سائٹ آہدی ہے: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s تہاݙے ورتݨ ناں تے پاسورڈ دی ارداس کریندا پئے۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..8e839b6f40
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Stran na %1$s sporoča:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s zahteva uporabniško ime in geslo. Sporočilo strani: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s zahteva uporabniško ime in geslo.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..ba788eb520
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sq/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Faqja te %1$s thotë:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s po kërkon emrin tuaj të përdoruesit dhe fjalëkalimin. Sajti thotë: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s po kërkon emrin tuaj të përdoruesit dhe fjalëkalimin.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..f79607c7c3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Порука са странице %1$s гласи:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s захтева ваше корисничко име и лозинку. Страница поручује: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s захтева ваше корисничко име и лозинку.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..f3256f67c6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-su/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Kaca di %1$s nyebutkeun:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ménta sandiasma jeung kecap konci anjeun. Situsna nyebutkeun: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ménta sandiasma jeung kecap konci anjeun.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..f95ae52bd7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Sidan på %1$s säger:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s efterfrågar ditt användarnamn och lösenord. Webbplatsen säger: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s efterfrågar ditt användarnamn och lösenord.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..8d445f4a01
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-szl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Strōna %1$s dowo znać:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s prosi ô twoje miano używocza i hasło. Kōmunikat strōny: „%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s prosi ô twoje miano używocza i hasło.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..a9878f95c9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ta/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">பக்கம் %1$s இல் சொல்வது:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s உங்கள் பயனர்பெயர் மற்றும் கடவுச்சொல்லைக் கோருகிறது. தளம் கூறுகிறது: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s உங்கள் பயனர்பெயர் மற்றும் கடவுச்சொல்லைக் கோருகிறது.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..387402a8ba
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-te/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s వద్ద పేజీ అంటోంది:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s మీ వాడుకరి పేరును, సంకేతపదాన్ని అడుగుతోంది. సైటు ఇలా అంటుోంది: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s మీ వాడుకరి పేరును, సంకేతపదాన్ని అడుగుతోంది.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..4d5d623a07
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tg/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Саҳифа дар %1$s хабар медиҳад:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s номи корбар ва ниҳонвожаи шуморо дархост мекунад. Сомона хабар медиҳад: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s номи корбар ва ниҳонвожаи шуморо дархост мекунад.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..96dfa57c15
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-th/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">หน้าที่ %1$s ระบุว่า:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s กำลังขอชื่อผู้ใช้และรหัสผ่านของคุณ ไซต์ระบุว่า: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s กำลังขอชื่อผู้ใช้และรหัสผ่านของคุณ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..520af991f2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Ang pahina sa %1$s ay nagsasabi ng:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ay humihingi ng iyong username at password. Ang sabi ng site ay: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">Hinihingi ng %1$s ang iyong username at password.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..3c3cc21d77
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tok/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">lipu %1$s li toki:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s li wile e nimi sina e nimi open sina. lipu li toki e ni: &quot;%1$s&quot;</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s li wile e nimi sina e nimi open sina.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..e5011323c2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s sayfası diyor ki:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s kullanıcı adı ve parolanızı istiyor. Site diyor ki: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s kullanıcı adı ve parolanızı istiyor.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..93075f89dd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-trs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Pajinâ nū riña %1$s taj:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s nachin\' man si yuguît ngà da\'nga\' huì arâj sunt. Sa tāj sitiô nan huin: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s nachin\' man si yuguît ngà da\'nga\' huì arâj sunt.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..8abc7cdffc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tt/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s сәхифәсеннән хәбәр:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s кулланучы исемен һәм серсүзен сорый. Сайт хәбәре: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s кулланучы исемен һәм серсүзен сорый.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..6400a304ee
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Da tettini tasna g %1$s:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">Issutur-ak %2$s ism d tguri n uzray-nnek. Da ittini usit: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">Issutur-ak %1$s isem d tguri n uzray-nnek.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..67daade49d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ug/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">بەت %1$s تە دېيىلگىنى:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ئىشلەتكۈچى ئىسمى بىلەن ئىمنى تەلەپ قىلىۋاتىدۇ. تور بېكەتتە دېيىلگىنى: «%1$s»</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ئىشلەتكۈچى ئىسمى بىلەن ئىمنى تەلەپ قىلىۋاتىدۇ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..876a66cc03
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-uk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Сторінка на %1$s повідомляє:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s запитує ваше ім’я користувача і пароль. Повідомлення сайту: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s запитує ім’я користувача і пароль.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..4af1973108
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-ur/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s پر صفحہ کہتا ہے:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s صارف کا نام اور پاسورڈ کی درخواست کر رہا ہے۔ سائٹ کہتی ہے: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s صارف کا نام اور پاسورڈ کی درخواست کر رہا ہے۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..0e3c13ca2c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-uz/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s sahifasi xabari:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s login va parolingizni soʻrayapti. Sayt xabari: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s login va parolingizni soʻrayapti.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..9f85621706
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-vec/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Ƚa pàgina so’l server %1$s ƚa riporta:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s el dimanda on nòme utente e na password. El sito el riporta: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s dimanda on nòme utente e na password.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..573499c584
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-vi/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Trang %1$s cho biết:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s yêu cầu tên người dùng và mật khẩu của bạn. Trang web thông báo: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s yêu cầu tên người dùng và mật khẩu của bạn.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..5676053cd2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-yo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">Ojú-ìwé %1$s sọ pé:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s ń béèrè orúkọ ìṣàmúlò àti kóòdù rẹ. Ìkànnì náà sọ pé: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s ń béère orúkọ ìwọlé àti pásíwọọ̀dù </string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..52a4d15cfb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">域名为 %1$s 的页面提示:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s 要求您输入用户名和密码。该网站提示:“%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s 要求您输入用户名和密码。</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..3c2573af95
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">%1$s 這一頁說:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the realm, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s 要求您輸入帳號密碼。此網站說:「%1$s」</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s 要求您輸入帳號與密碼。</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/main/res/values/strings.xml b/mobile/android/android-components/components/browser/engine-system/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..7e80343142
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+<?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>
+ <!-- Text for the title of an alert dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_alert_title">The page at %1$s says:</string>
+ <!-- Text for the message of an auth dialog displayed by a web page.
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_message">%2$s is requesting your username and password. The site says: “%1$s”</string>
+ <!-- Text for the message of an auth dialog displayed by a web page. %1$s will be replaced with the URL of the current page (displaying the dialog). -->
+ <string name="mozac_browser_engine_system_auth_no_realm_message">%1$s is requesting your username and password.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/NestedWebViewTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/NestedWebViewTest.kt
new file mode 100644
index 0000000000..e822084cb3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/NestedWebViewTest.kt
@@ -0,0 +1,165 @@
+/* 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.browser.engine.system
+
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import androidx.core.view.NestedScrollingChildHelper
+import androidx.core.view.ViewCompat.SCROLL_AXIS_VERTICAL
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.mockMotionEvent
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class NestedWebViewTest {
+
+ @Test
+ fun `NestedWebView must delegate NestedScrollingChild implementation to childHelper`() {
+ val nestedWebView = NestedWebView(testContext)
+ val mockChildHelper: NestedScrollingChildHelper = mock()
+ nestedWebView.childHelper = mockChildHelper
+
+ doReturn(true).`when`(mockChildHelper).isNestedScrollingEnabled
+ doReturn(true).`when`(mockChildHelper).hasNestedScrollingParent()
+
+ nestedWebView.isNestedScrollingEnabled = true
+ verify(mockChildHelper).isNestedScrollingEnabled = true
+
+ assertTrue(nestedWebView.isNestedScrollingEnabled)
+ verify(mockChildHelper).isNestedScrollingEnabled
+
+ nestedWebView.startNestedScroll(1)
+ verify(mockChildHelper).startNestedScroll(1)
+
+ nestedWebView.stopNestedScroll()
+ verify(mockChildHelper).stopNestedScroll()
+
+ assertTrue(nestedWebView.hasNestedScrollingParent())
+ verify(mockChildHelper).hasNestedScrollingParent()
+
+ nestedWebView.dispatchNestedScroll(0, 0, 0, 0, null)
+ verify(mockChildHelper).dispatchNestedScroll(0, 0, 0, 0, null)
+
+ nestedWebView.dispatchNestedPreScroll(0, 0, null, null)
+ verify(mockChildHelper).dispatchNestedPreScroll(0, 0, null, null)
+
+ nestedWebView.dispatchNestedFling(0f, 0f, true)
+ verify(mockChildHelper).dispatchNestedFling(0f, 0f, true)
+
+ nestedWebView.dispatchNestedPreFling(0f, 0f)
+ verify(mockChildHelper).dispatchNestedPreFling(0f, 0f)
+ }
+
+ @Test
+ fun `verify onTouchEvent when ACTION_DOWN`() {
+ val nestedWebView = NestedWebView(testContext)
+ val mockChildHelper: NestedScrollingChildHelper = mock()
+ nestedWebView.childHelper = mockChildHelper
+
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_DOWN))
+ verify(mockChildHelper).startNestedScroll(SCROLL_AXIS_VERTICAL)
+ }
+
+ @Test
+ fun `verify onTouchEvent when ACTION_MOVE`() {
+ val nestedWebView = NestedWebView(testContext)
+ val mockChildHelper: NestedScrollingChildHelper = mock()
+ nestedWebView.childHelper = mockChildHelper
+
+ doReturn(true).`when`(mockChildHelper).dispatchNestedPreScroll(
+ anyInt(),
+ anyInt(),
+ any(),
+ any(),
+ )
+
+ nestedWebView.scrollOffset[0] = 1
+ nestedWebView.scrollOffset[1] = 2
+
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_MOVE, y = 10f))
+ assertEquals(nestedWebView.nestedOffsetY, 2)
+ assertEquals(nestedWebView.lastY, 8)
+
+ doReturn(true).`when`(mockChildHelper).dispatchNestedScroll(
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ any(),
+ )
+
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_MOVE, y = 10f))
+ assertEquals(nestedWebView.nestedOffsetY, 6)
+ assertEquals(nestedWebView.lastY, 6)
+ }
+
+ @Test
+ fun `verify onTouchEvent when ACTION_UP or ACTION_CANCEL`() {
+ val nestedWebView = NestedWebView(testContext)
+ val mockChildHelper: NestedScrollingChildHelper = mock()
+ nestedWebView.childHelper = mockChildHelper
+
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_UP))
+ verify(mockChildHelper).stopNestedScroll()
+
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_CANCEL))
+ verify(mockChildHelper, times(2)).stopNestedScroll()
+ }
+
+ @Test
+ fun `GIVEN NestedWebView WHEN a new instance is created THEN a properly configured InputResultDetail is created`() {
+ val nestedWebView = NestedWebView(testContext)
+
+ assertTrue(nestedWebView.inputResultDetail.isTouchHandlingUnknown())
+ assertFalse(nestedWebView.inputResultDetail.canScrollToLeft())
+ assertFalse(nestedWebView.inputResultDetail.canScrollToTop())
+ assertFalse(nestedWebView.inputResultDetail.canScrollToRight())
+ assertFalse(nestedWebView.inputResultDetail.canScrollToBottom())
+ assertFalse(nestedWebView.inputResultDetail.canOverscrollLeft())
+ assertFalse(nestedWebView.inputResultDetail.canOverscrollTop())
+ assertFalse(nestedWebView.inputResultDetail.canOverscrollRight())
+ assertFalse(nestedWebView.inputResultDetail.canOverscrollBottom())
+ }
+
+ @Test
+ fun `GIVEN NestedWebView WHEN onTouchEvent is called THEN updateInputResult is called with the result of whether the touch is handled or not`() {
+ val nestedWebView = spy(NestedWebView(testContext))
+
+ doReturn(true).`when`(nestedWebView).callSuperOnTouchEvent(any())
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_DOWN))
+ verify(nestedWebView).updateInputResult(true)
+
+ doReturn(false).`when`(nestedWebView).callSuperOnTouchEvent(any())
+ nestedWebView.onTouchEvent(mockMotionEvent(ACTION_DOWN))
+ verify(nestedWebView).updateInputResult(false)
+ }
+
+ @Test
+ fun `GIVEN an instance of InputResultDetail WHEN updateInputResult called THEN it sets whether the touch was handled`() {
+ val nestedWebView = NestedWebView(testContext)
+
+ assertTrue(nestedWebView.inputResultDetail.isTouchHandlingUnknown())
+
+ nestedWebView.updateInputResult(true)
+ assertTrue(nestedWebView.inputResultDetail.isTouchHandledByBrowser())
+
+ nestedWebView.updateInputResult(false)
+ assertTrue(nestedWebView.inputResultDetail.isTouchUnhandled())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionStateTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionStateTest.kt
new file mode 100644
index 0000000000..a3aafae50e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionStateTest.kt
@@ -0,0 +1,138 @@
+/* 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.browser.engine.system
+
+import android.os.Bundle
+import android.util.JsonReader
+import android.util.JsonWriter
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.ByteArrayOutputStream
+
+@RunWith(AndroidJUnit4::class)
+class SystemEngineSessionStateTest {
+ @Test
+ fun fromJSON() {
+ val json = JSONObject().apply {
+ put("k0", "v0")
+ put("k1", 1)
+ put("k2", true)
+ put("k3", 5.0)
+ put("k4", 1.0f)
+ put("k5", JSONArray(listOf(1, 2, 3)))
+ }
+
+ val state = SystemEngineSessionState.fromJSON(json)
+ val bundle = state.bundle!!
+
+ assertEquals(5, bundle.size())
+
+ assertTrue(bundle.containsKey("k0"))
+ assertTrue(bundle.containsKey("k1"))
+ assertTrue(bundle.containsKey("k2"))
+ assertTrue(bundle.containsKey("k3"))
+ assertTrue(bundle.containsKey("k4"))
+
+ assertEquals("v0", bundle.getString("k0"))
+ assertEquals(1, bundle.getInt("k1"))
+ assertEquals(true, bundle.getBoolean("k2"))
+ assertEquals(5.0, bundle.getDouble("k3"), 0.0)
+ assertEquals(1.0f, bundle.getFloat("k4"))
+ }
+
+ @Test
+ fun writeToAndFromJSON() {
+ val state = SystemEngineSessionState(
+ Bundle().apply {
+ putString("k0", "v0")
+ putInt("k1", 1)
+ putBoolean("k2", true)
+ putStringArrayList("k3", ArrayList<String>(listOf("Hello", "World")))
+ putDouble("k4", 5.0)
+ putFloat("k5", 1.0f)
+ putFloat("k6", 42.25f)
+ putDouble("k7", 23.23)
+ },
+ )
+
+ val outputStream = ByteArrayOutputStream()
+ state.writeTo(JsonWriter(outputStream.writer()))
+
+ val bundle = SystemEngineSessionState.fromJSON(
+ JSONObject(outputStream.toString()),
+ ).bundle
+
+ assertNotNull(bundle!!)
+
+ assertEquals(7, bundle.size())
+
+ assertTrue(bundle.containsKey("k0"))
+ assertTrue(bundle.containsKey("k1"))
+ assertTrue(bundle.containsKey("k2"))
+ assertFalse(bundle.containsKey("k3"))
+ assertTrue(bundle.containsKey("k4"))
+ assertTrue(bundle.containsKey("k5"))
+ assertTrue(bundle.containsKey("k6"))
+ assertTrue(bundle.containsKey("k7"))
+
+ assertEquals("v0", bundle.getString("k0"))
+ assertEquals(1, bundle.getInt("k1"))
+ assertEquals(true, bundle.getBoolean("k2"))
+ assertEquals(5.0, bundle.getDouble("k4"), 0.0)
+ assertEquals(1.0, bundle.getDouble("k5"), 0.0)
+ assertEquals(42.25, bundle.getDouble("k6"), 0.0)
+ assertEquals(23.23, bundle.getDouble("k7"), 0.0)
+ }
+
+ @Test
+ fun writeToAndReadFrom() {
+ val state = SystemEngineSessionState(
+ Bundle().apply {
+ putString("k0", "v0")
+ putInt("k1", 1)
+ putBoolean("k2", true)
+ putStringArrayList("k3", ArrayList<String>(listOf("Hello", "World")))
+ putDouble("k4", 5.0)
+ putFloat("k5", 1.0f)
+ putFloat("k6", 42.25f)
+ putDouble("k7", 23.23)
+ },
+ )
+
+ val outputStream = ByteArrayOutputStream()
+ state.writeTo(JsonWriter(outputStream.writer()))
+
+ val reader = JsonReader(outputStream.toString().reader())
+ val bundle = SystemEngineSessionState.from(reader).bundle
+
+ assertNotNull(bundle!!)
+
+ assertEquals(7, bundle.size())
+
+ assertTrue(bundle.containsKey("k0"))
+ assertTrue(bundle.containsKey("k1"))
+ assertTrue(bundle.containsKey("k2"))
+ assertFalse(bundle.containsKey("k3"))
+ assertTrue(bundle.containsKey("k4"))
+ assertTrue(bundle.containsKey("k5"))
+ assertTrue(bundle.containsKey("k6"))
+ assertTrue(bundle.containsKey("k7"))
+
+ assertEquals("v0", bundle.getString("k0"))
+ assertEquals(1.0, bundle.getDouble("k1"), 0.0) // We only see token "number", so we have to read a double and can't know that this was an int.
+ assertEquals(true, bundle.getBoolean("k2"))
+ assertEquals(5.0, bundle.getDouble("k4"), 0.0)
+ assertEquals(1.0, bundle.getDouble("k5"), 0.0)
+ assertEquals(42.25, bundle.getDouble("k6"), 0.0)
+ assertEquals(23.23, bundle.getDouble("k7"), 0.0)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt
new file mode 100644
index 0000000000..4f56d9c467
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt
@@ -0,0 +1,1238 @@
+/* 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.browser.engine.system
+
+import android.content.Context
+import android.net.Uri
+import android.os.Bundle
+import android.webkit.WebChromeClient
+import android.webkit.WebResourceRequest
+import android.webkit.WebSettings
+import android.webkit.WebStorage
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.webkit.WebViewDatabase
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.engine.system.matcher.UrlMatcher
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.Engine.BrowsingData
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.LooperMode
+import java.lang.reflect.Modifier
+import org.mockito.ArgumentMatchers.any as mockitoAny
+
+@Suppress("DEPRECATION") // Suppress deprecation for LooperMode.Mode.LEGACY
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.LEGACY)
+class SystemEngineSessionTest {
+
+ @Test
+ fun webChromeClientNotifiesObservers() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ var observedProgress = 0
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onProgress(progress: Int) { observedProgress = progress }
+ },
+ )
+
+ engineSession.webView.webChromeClient!!.onProgressChanged(null, 100)
+ assertEquals(100, observedProgress)
+ }
+
+ @Test
+ fun loadUrl() {
+ var loadedUrl: String? = null
+ var loadHeaders: Map<String, String>? = null
+
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = spy(
+ object : WebView(testContext) {
+ override fun loadUrl(url: String, additionalHttpHeaders: MutableMap<String, String>) {
+ loadedUrl = url
+ loadHeaders = additionalHttpHeaders
+ }
+ },
+ )
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.webView = webView
+
+ engineSession.loadUrl("")
+ verify(webView, never()).loadUrl(anyString())
+
+ engineSession.loadUrl("http://mozilla.org")
+ verify(webView).loadUrl(eq("http://mozilla.org"), any())
+
+ assertEquals("http://mozilla.org", loadedUrl)
+
+ assertNotNull(loadHeaders)
+ assertEquals(1, loadHeaders!!.size)
+ assertTrue(loadHeaders!!.containsKey("X-Requested-With"))
+ assertEquals("", loadHeaders!!["X-Requested-With"])
+
+ val extraHeaders = mapOf("X-Extra-Header" to "true")
+ engineSession.loadUrl("http://mozilla.org", additionalHeaders = extraHeaders)
+ assertNotNull(loadHeaders)
+ assertEquals(2, loadHeaders!!.size)
+ assertTrue(loadHeaders!!.containsKey("X-Extra-Header"))
+ assertEquals("true", loadHeaders!!["X-Extra-Header"])
+ }
+
+ @Test
+ fun `WHEN URL is loaded THEN URL load observer is notified`() {
+ var onLoadUrlTriggered = false
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadUrl() {
+ onLoadUrlTriggered = true
+ }
+ },
+ )
+ engineSession.webView = webView
+
+ engineSession.loadUrl("http://mozilla.org")
+
+ assertTrue(onLoadUrlTriggered)
+ }
+
+ @Test
+ fun loadData() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.loadData("<html><body>Hello!</body></html>")
+ verify(webView, never()).loadData(anyString(), eq("text/html"), eq("UTF-8"))
+
+ engineSession.webView = webView
+
+ engineSession.loadData("<html><body>Hello!</body></html>")
+ verify(webView).loadData(eq("<html><body>Hello!</body></html>"), eq("text/html"), eq("UTF-8"))
+
+ engineSession.loadData("Hello!", "text/plain", "UTF-8")
+ verify(webView).loadData(eq("Hello!"), eq("text/plain"), eq("UTF-8"))
+
+ engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", "text/plain", "base64")
+ verify(webView).loadData(eq("ahr0cdovl21vemlsbgeub3jn=="), eq("text/plain"), eq("base64"))
+ }
+
+ @Test
+ fun `WHEN data is loaded THEN data load observer is notified`() {
+ var onLoadDataTriggered = false
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadData() {
+ onLoadDataTriggered = true
+ }
+ },
+ )
+ engineSession.webView = webView
+
+ engineSession.loadData("<html><body/></html>")
+
+ assertTrue(onLoadDataTriggered)
+ }
+
+ @Test
+ fun stopLoading() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.stopLoading()
+ verify(webView, never()).stopLoading()
+
+ engineSession.webView = webView
+
+ engineSession.stopLoading()
+ verify(webView).stopLoading()
+ }
+
+ @Test
+ fun reload() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ engineSession.reload()
+ verify(webView, never()).reload()
+
+ engineSession.webView = webView
+
+ engineSession.reload()
+ verify(webView).reload()
+ }
+
+ @Test
+ fun goBack() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.goBack()
+ verify(webView, never()).goBack()
+
+ engineSession.webView = webView
+
+ engineSession.goBack()
+ verify(webView).goBack()
+ }
+
+ @Test
+ fun goForward() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.goForward()
+ verify(webView, never()).goForward()
+
+ engineSession.webView = webView
+
+ engineSession.goForward()
+ verify(webView).goForward()
+ }
+
+ @Test
+ fun `GIVEN forward navigation is possible WHEN navigating forward THEN forward navigation observer is notified`() {
+ var observedOnNavigateForward = false
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ whenever(webView.canGoForward()).thenReturn(true)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onNavigateForward() {
+ observedOnNavigateForward = true
+ }
+ },
+ )
+ engineSession.webView = webView
+
+ engineSession.goForward()
+
+ assertTrue(observedOnNavigateForward)
+ }
+
+ @Test
+ fun `GIVEN forward navigation is not possible WHEN navigating forward THEN forward navigation observer is not notified`() {
+ var observedOnNavigateForward = false
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ whenever(webView.canGoForward()).thenReturn(false)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onNavigateForward() {
+ observedOnNavigateForward = true
+ }
+ },
+ )
+ engineSession.webView = webView
+
+ engineSession.goForward()
+
+ assertFalse(observedOnNavigateForward)
+ }
+
+ @Test
+ fun goToHistoryIndex() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ whenever(webView.copyBackForwardList()).thenReturn(mock())
+ engineSession.goToHistoryIndex(0)
+ verify(webView, never()).goBackOrForward(0)
+
+ engineSession.webView = webView
+
+ engineSession.goToHistoryIndex(0)
+ verify(webView).goBackOrForward(0)
+ }
+
+ @Test
+ fun `WHEN navigating to history index THEN the observer is notified`() {
+ var onGotoHistoryIndexTriggered = false
+ val engineSession = spy(SystemEngineSession(testContext))
+ val settings = mock<WebSettings>()
+ val webView = mock<WebView>() {
+ whenever(this.settings).thenReturn(settings)
+ whenever(copyBackForwardList()).thenReturn(mock())
+ }
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onGotoHistoryIndex() {
+ onGotoHistoryIndexTriggered = true
+ }
+ },
+ )
+ engineSession.webView = webView
+
+ engineSession.goToHistoryIndex(0)
+
+ assertTrue(onGotoHistoryIndexTriggered)
+ }
+
+ @Test
+ fun restoreState() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = spy(WebView(testContext))
+
+ try {
+ engineSession.restoreState(mock())
+ fail("Expected IllegalArgumentException")
+ } catch (e: IllegalArgumentException) {}
+ assertFalse(engineSession.restoreState(SystemEngineSessionState(Bundle())))
+ verify(webView, never()).restoreState(mockitoAny(Bundle::class.java))
+
+ engineSession.webView = webView
+ engineSession.webView.loadUrl("http://example.com")
+
+ // update the WebView's history async.
+ shadowOf(webView).pushEntryToHistory("http://example.com")
+
+ val bundle = Bundle()
+ webView.saveState(bundle)
+ val state = SystemEngineSessionState(bundle)
+
+ assertTrue(engineSession.restoreState(state))
+ verify(webView).restoreState(bundle)
+ }
+
+ @ExperimentalCoroutinesApi
+ @Test
+ fun enableTrackingProtection() = runTest {
+ SystemEngineView.URL_MATCHER = UrlMatcher(arrayOf(""))
+
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+
+ whenever(webView.settings).thenReturn(settings)
+ whenever(webView.context).thenReturn(testContext)
+
+ engineSession.webView = webView
+
+ var enabledObserved: Boolean? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onTrackerBlockingEnabledChange(enabled: Boolean) {
+ enabledObserved = enabled
+ }
+ },
+ )
+
+ assertNull(engineSession.trackingProtectionPolicy)
+ engineSession.updateTrackingProtection()
+ assertEquals(
+ EngineSession.TrackingProtectionPolicy.strict(),
+ engineSession.trackingProtectionPolicy,
+ )
+ assertNotNull(enabledObserved)
+ assertTrue(enabledObserved as Boolean)
+ }
+
+ @Test
+ fun disableTrackingProtection() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ var enabledObserved: Boolean? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onTrackerBlockingEnabledChange(enabled: Boolean) {
+ enabledObserved = enabled
+ }
+ },
+ )
+
+ engineSession.trackingProtectionPolicy = EngineSession.TrackingProtectionPolicy.strict()
+
+ engineSession.disableTrackingProtection()
+ assertNull(engineSession.trackingProtectionPolicy)
+ assertNotNull(enabledObserved)
+ assertFalse(enabledObserved as Boolean)
+ }
+
+ @Test
+ fun initSettings() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ assertNotNull(engineSession.internalSettings)
+
+ val webViewSettings = mock<WebSettings>()
+ whenever(webViewSettings.displayZoomControls).thenReturn(true)
+ whenever(webViewSettings.allowContentAccess).thenReturn(true)
+ whenever(webViewSettings.allowFileAccess).thenReturn(true)
+ whenever(webViewSettings.mediaPlaybackRequiresUserGesture).thenReturn(true)
+ whenever(webViewSettings.supportMultipleWindows()).thenReturn(false)
+
+ val webView = mock<WebView>()
+ whenever(webView.context).thenReturn(testContext)
+ whenever(webView.settings).thenReturn(webViewSettings)
+ whenever(webView.isVerticalScrollBarEnabled).thenReturn(true)
+ whenever(webView.isHorizontalScrollBarEnabled).thenReturn(true)
+ engineSession.webView = webView
+
+ assertFalse(engineSession.settings.javascriptEnabled)
+ engineSession.settings.javascriptEnabled = true
+ verify(webViewSettings).javaScriptEnabled = true
+
+ assertFalse(engineSession.settings.domStorageEnabled)
+ engineSession.settings.domStorageEnabled = true
+ verify(webViewSettings).domStorageEnabled = true
+
+ assertNull(engineSession.settings.userAgentString)
+ engineSession.settings.userAgentString = "userAgent"
+ verify(webViewSettings).userAgentString = "userAgent"
+
+ assertTrue(engineSession.settings.mediaPlaybackRequiresUserGesture)
+ engineSession.settings.mediaPlaybackRequiresUserGesture = false
+ verify(webViewSettings).mediaPlaybackRequiresUserGesture = false
+
+ assertFalse(engineSession.settings.javaScriptCanOpenWindowsAutomatically)
+ engineSession.settings.javaScriptCanOpenWindowsAutomatically = true
+ verify(webViewSettings).javaScriptCanOpenWindowsAutomatically = true
+
+ assertTrue(engineSession.settings.displayZoomControls)
+ engineSession.settings.javaScriptCanOpenWindowsAutomatically = false
+ verify(webViewSettings).javaScriptCanOpenWindowsAutomatically = false
+
+ assertFalse(engineSession.settings.loadWithOverviewMode)
+ engineSession.settings.loadWithOverviewMode = true
+ verify(webViewSettings).loadWithOverviewMode = true
+
+ assertNull(engineSession.settings.useWideViewPort)
+ engineSession.settings.useWideViewPort = false
+ verify(webViewSettings).useWideViewPort = false
+
+ assertTrue(engineSession.settings.allowContentAccess)
+ engineSession.settings.allowContentAccess = false
+ verify(webViewSettings).allowContentAccess = false
+
+ assertTrue(engineSession.settings.allowFileAccess)
+ engineSession.settings.allowFileAccess = false
+ verify(webViewSettings).allowFileAccess = false
+
+ assertFalse(engineSession.settings.allowUniversalAccessFromFileURLs)
+ engineSession.settings.allowUniversalAccessFromFileURLs = true
+ verify(webViewSettings).allowUniversalAccessFromFileURLs = true
+
+ assertFalse(engineSession.settings.allowFileAccessFromFileURLs)
+ engineSession.settings.allowFileAccessFromFileURLs = true
+ verify(webViewSettings).allowFileAccessFromFileURLs = true
+
+ assertTrue(engineSession.settings.verticalScrollBarEnabled)
+ engineSession.settings.verticalScrollBarEnabled = false
+ verify(webView).isVerticalScrollBarEnabled = false
+
+ assertTrue(engineSession.settings.horizontalScrollBarEnabled)
+ engineSession.settings.horizontalScrollBarEnabled = false
+ verify(webView).isHorizontalScrollBarEnabled = false
+
+ assertFalse(engineSession.settings.supportMultipleWindows)
+ engineSession.settings.supportMultipleWindows = true
+ verify(webViewSettings).setSupportMultipleWindows(true)
+
+ assertTrue(engineSession.webFontsEnabled)
+ assertTrue(engineSession.settings.webFontsEnabled)
+ engineSession.settings.webFontsEnabled = false
+ assertFalse(engineSession.webFontsEnabled)
+ assertFalse(engineSession.settings.webFontsEnabled)
+
+ assertNull(engineSession.settings.trackingProtectionPolicy)
+ engineSession.settings.trackingProtectionPolicy =
+ EngineSession.TrackingProtectionPolicy.strict()
+ verify(engineSession).updateTrackingProtection(EngineSession.TrackingProtectionPolicy.strict())
+
+ engineSession.settings.trackingProtectionPolicy = null
+ verify(engineSession).disableTrackingProtection()
+
+ verify(webViewSettings).cacheMode = WebSettings.LOAD_NO_CACHE
+ verify(webViewSettings).setGeolocationEnabled(false)
+ verify(webViewSettings).databaseEnabled = false
+ verify(webViewSettings).savePassword = false
+ verify(webViewSettings).saveFormData = false
+ verify(webViewSettings).builtInZoomControls = true
+ verify(webViewSettings).displayZoomControls = false
+ }
+
+ @Test
+ fun withProvidedDefaultSettings() {
+ val defaultSettings = DefaultSettings(
+ javascriptEnabled = false,
+ domStorageEnabled = false,
+ webFontsEnabled = false,
+ trackingProtectionPolicy = EngineSession.TrackingProtectionPolicy.strict(),
+ userAgentString = "userAgent",
+ mediaPlaybackRequiresUserGesture = false,
+ javaScriptCanOpenWindowsAutomatically = true,
+ displayZoomControls = true,
+ loadWithOverviewMode = true,
+ useWideViewPort = true,
+ supportMultipleWindows = true,
+ )
+ val engineSession = spy(SystemEngineSession(testContext, defaultSettings))
+
+ val webView = mock<WebView>()
+ whenever(webView.context).thenReturn(testContext)
+
+ val webViewSettings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(webViewSettings)
+
+ engineSession.webView = webView
+
+ verify(webViewSettings).domStorageEnabled = false
+ verify(webViewSettings).javaScriptEnabled = false
+ verify(webViewSettings).userAgentString = "userAgent"
+ verify(webViewSettings).mediaPlaybackRequiresUserGesture = false
+ verify(webViewSettings).javaScriptCanOpenWindowsAutomatically = true
+ verify(webViewSettings).displayZoomControls = true
+ verify(webViewSettings).loadWithOverviewMode = true
+ verify(webViewSettings).useWideViewPort = true
+ verify(webViewSettings).setSupportMultipleWindows(true)
+ verify(engineSession).updateTrackingProtection(EngineSession.TrackingProtectionPolicy.strict())
+ assertFalse(engineSession.webFontsEnabled)
+ }
+
+ @Test
+ fun sharedFieldsAreVolatile() {
+ val internalSettings = SystemEngineSession::class.java.getDeclaredField("internalSettings")
+ val webFontsEnabledField = SystemEngineSession::class.java.getDeclaredField("webFontsEnabled")
+ val trackingProtectionField = SystemEngineSession::class.java.getDeclaredField("trackingProtectionPolicy")
+ val historyTrackingDelegate = SystemEngineSession::class.java.getDeclaredField("historyTrackingDelegate")
+ val fullScreenCallback = SystemEngineSession::class.java.getDeclaredField("fullScreenCallback")
+ val currentUrl = SystemEngineSession::class.java.getDeclaredField("currentUrl")
+ val webView = SystemEngineSession::class.java.getDeclaredField("webView")
+
+ assertTrue(Modifier.isVolatile(internalSettings.modifiers))
+ assertTrue(Modifier.isVolatile(webFontsEnabledField.modifiers))
+ assertTrue(Modifier.isVolatile(trackingProtectionField.modifiers))
+ assertTrue(Modifier.isVolatile(historyTrackingDelegate.modifiers))
+ assertTrue(Modifier.isVolatile(fullScreenCallback.modifiers))
+ assertTrue(Modifier.isVolatile(currentUrl.modifiers))
+ assertTrue(Modifier.isVolatile(webView.modifiers))
+ }
+
+ @Test
+ fun settingInterceptorToProvideAlternativeContent() {
+ var interceptorCalledWithUri: String? = null
+
+ val interceptor = object : RequestInterceptor {
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ interceptorCalledWithUri = uri
+ return RequestInterceptor.InterceptionResponse.Content("<h1>Hello World</h1>")
+ }
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = interceptor)
+
+ val engineSession = SystemEngineSession(testContext, defaultSettings)
+ engineSession.webView = spy(engineSession.webView)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val request: WebResourceRequest = mock()
+ doReturn(Uri.parse("sample:about")).`when`(request).url
+
+ val response = engineSession.webView.webViewClient.shouldInterceptRequest(
+ engineSession.webView,
+ request,
+ )
+
+ assertEquals("sample:about", interceptorCalledWithUri)
+
+ assertNotNull(response)
+
+ assertEquals("<h1>Hello World</h1>", response!!.data.bufferedReader().use { it.readText() })
+ assertEquals("text/html", response.mimeType)
+ assertEquals("UTF-8", response.encoding)
+ }
+
+ @Test
+ fun `shouldInterceptRequest notifies observers if request was not intercepted`() {
+ val url = "sample:about"
+ val request: WebResourceRequest = mock()
+ doReturn(true).`when`(request).isForMainFrame
+ doReturn(true).`when`(request).hasGesture()
+ doReturn(Uri.parse(url)).`when`(request).url
+
+ val engineSession = SystemEngineSession(testContext)
+ engineSession.webView = spy(engineSession.webView)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ engineSession.webView.webViewClient.shouldInterceptRequest(engineSession.webView, request)
+
+ verify(observer).onLoadRequest(anyString(), eq(true), eq(true))
+
+ val redirect: WebResourceRequest = mock()
+ doReturn(true).`when`(redirect).isForMainFrame
+ doReturn(false).`when`(redirect).hasGesture()
+ doReturn(Uri.parse("sample:about")).`when`(redirect).url
+
+ engineSession.webView.webViewClient.shouldInterceptRequest(engineSession.webView, redirect)
+
+ verify(observer).onLoadRequest(anyString(), eq(true), eq(true))
+ }
+
+ @Test
+ fun `shouldInterceptRequest does not notify observers if request was intercepted`() {
+ val request: WebResourceRequest = mock()
+ doReturn(true).`when`(request).isForMainFrame
+ doReturn(true).`when`(request).hasGesture()
+ doReturn(Uri.parse("sample:about")).`when`(request).url
+
+ val interceptor = object : RequestInterceptor {
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ return RequestInterceptor.InterceptionResponse.Content("<h1>Hello World</h1>")
+ }
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = interceptor)
+
+ val engineSession = SystemEngineSession(testContext, defaultSettings)
+ engineSession.webView = spy(engineSession.webView)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ engineSession.webView.webViewClient.shouldInterceptRequest(
+ engineSession.webView,
+ request,
+ )
+
+ verify(observer, never()).onLoadRequest(anyString(), anyBoolean(), anyBoolean())
+ }
+
+ @Test
+ fun settingInterceptorToProvideAlternativeUrl() {
+ var interceptorCalledWithUri: String? = null
+
+ val interceptor = object : RequestInterceptor {
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ interceptorCalledWithUri = uri
+ return RequestInterceptor.InterceptionResponse.Url("https://mozilla.org")
+ }
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = interceptor)
+
+ val engineSession = SystemEngineSession(testContext, defaultSettings)
+ engineSession.webView = spy(engineSession.webView)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val request: WebResourceRequest = mock()
+ doReturn(Uri.parse("sample:about")).`when`(request).url
+
+ val response = engineSession.webView.webViewClient.shouldInterceptRequest(
+ engineSession.webView,
+ request,
+ )
+
+ assertNull(response)
+ assertEquals("sample:about", interceptorCalledWithUri)
+ assertEquals("https://mozilla.org", engineSession.webView.url)
+ }
+
+ @Test
+ fun onLoadRequestWithoutInterceptor() {
+ val defaultSettings = DefaultSettings()
+
+ val engineSession = SystemEngineSession(testContext, defaultSettings)
+ engineSession.webView = spy(engineSession.webView)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val request: WebResourceRequest = mock()
+ doReturn(Uri.parse("sample:about")).`when`(request).url
+
+ val response = engineSession.webView.webViewClient.shouldInterceptRequest(
+ engineSession.webView,
+ request,
+ )
+
+ assertNull(response)
+ }
+
+ @Test
+ fun onLoadRequestWithInterceptorThatDoesNotIntercept() {
+ var interceptorCalledWithUri: String? = null
+
+ val interceptor = object : RequestInterceptor {
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ interceptorCalledWithUri = uri
+ return null
+ }
+ }
+
+ val defaultSettings = DefaultSettings(requestInterceptor = interceptor)
+
+ val engineSession = SystemEngineSession(testContext, defaultSettings)
+ engineSession.webView = spy(engineSession.webView)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val request: WebResourceRequest = mock()
+ doReturn(Uri.parse("sample:about")).`when`(request).url
+
+ val response = engineSession.webView.webViewClient.shouldInterceptRequest(
+ engineSession.webView,
+ request,
+ )
+
+ assertEquals("sample:about", interceptorCalledWithUri)
+ assertNull(response)
+ }
+
+ @Test
+ fun webViewErrorMappingToErrorType() {
+ assertEquals(
+ ErrorType.ERROR_UNKNOWN_HOST,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_HOST_LOOKUP),
+ )
+ assertEquals(
+ ErrorType.ERROR_CONNECTION_REFUSED,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_CONNECT),
+ )
+ assertEquals(
+ ErrorType.ERROR_CONNECTION_REFUSED,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_IO),
+ )
+ assertEquals(
+ ErrorType.ERROR_NET_TIMEOUT,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_TIMEOUT),
+ )
+ assertEquals(
+ ErrorType.ERROR_REDIRECT_LOOP,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_REDIRECT_LOOP),
+ )
+ assertEquals(
+ ErrorType.ERROR_UNKNOWN_PROTOCOL,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_UNSUPPORTED_SCHEME),
+ )
+ assertEquals(
+ ErrorType.ERROR_SECURITY_SSL,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE),
+ )
+ assertEquals(
+ ErrorType.ERROR_MALFORMED_URI,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_BAD_URL),
+ )
+ assertEquals(
+ ErrorType.UNKNOWN,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_TOO_MANY_REQUESTS),
+ )
+ assertEquals(
+ ErrorType.ERROR_FILE_NOT_FOUND,
+ SystemEngineSession.webViewErrorToErrorType(WebViewClient.ERROR_FILE_NOT_FOUND),
+ )
+ assertEquals(
+ ErrorType.UNKNOWN,
+ SystemEngineSession.webViewErrorToErrorType(-500),
+ )
+ }
+
+ @Test
+ fun desktopMode() {
+ val userAgentMobile = "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36 Mobile Safari/537.36"
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val webViewSettings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(webViewSettings)
+
+ var desktopMode = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onDesktopModeChange(enabled: Boolean) {
+ desktopMode = enabled
+ }
+ },
+ )
+
+ engineSession.webView = webView
+ whenever(webView.settings).thenReturn(webViewSettings)
+ whenever(webViewSettings.userAgentString).thenReturn(userAgentMobile)
+
+ engineSession.toggleDesktopMode(true)
+ verify(webViewSettings).useWideViewPort = true
+ verify(engineSession).toggleDesktopUA(userAgentMobile, true)
+ assertTrue(desktopMode)
+
+ engineSession.toggleDesktopMode(true)
+ verify(webView, never()).reload()
+
+ engineSession.toggleDesktopMode(true, true)
+ verify(webView).reload()
+ }
+
+ @Test
+ fun desktopModeWithProvidedTrueWideViewPort() {
+ val userAgentMobile = "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36 Mobile Safari/537.36"
+ val defaultSettings = DefaultSettings(useWideViewPort = true)
+ val engineSession = spy(SystemEngineSession(testContext, defaultSettings))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ val webViewSettings = mock<WebSettings>()
+ var desktopMode = false
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onDesktopModeChange(enabled: Boolean) {
+ desktopMode = enabled
+ }
+ },
+ )
+
+ engineSession.webView = webView
+ whenever(webView.settings).thenReturn(webViewSettings)
+ whenever(webViewSettings.userAgentString).thenReturn(userAgentMobile)
+
+ engineSession.toggleDesktopMode(true)
+ verify(webViewSettings).useWideViewPort = true
+ verify(engineSession).toggleDesktopUA(userAgentMobile, true)
+ assertTrue(desktopMode)
+ }
+
+ @Test
+ fun desktopModeWithProvidedFalseWideViewPort() {
+ val userAgentMobile = "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36 Mobile Safari/537.36"
+ val defaultSettings = DefaultSettings(useWideViewPort = false)
+ val engineSession = spy(SystemEngineSession(testContext, defaultSettings))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ val webViewSettings = mock<WebSettings>()
+ var desktopMode = false
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onDesktopModeChange(enabled: Boolean) {
+ desktopMode = enabled
+ }
+ },
+ )
+
+ engineSession.webView = webView
+ whenever(webView.settings).thenReturn(webViewSettings)
+ whenever(webViewSettings.userAgentString).thenReturn(userAgentMobile)
+
+ engineSession.toggleDesktopMode(true)
+ verify(webViewSettings).useWideViewPort = true
+ verify(engineSession).toggleDesktopUA(userAgentMobile, true)
+ assertTrue(desktopMode)
+
+ engineSession.toggleDesktopMode(false)
+ verify(webViewSettings).useWideViewPort = false
+ verify(engineSession).toggleDesktopUA(userAgentMobile, false)
+ assertFalse(desktopMode)
+ }
+
+ @Test
+ fun desktopModeToggleTrueWithNoProvidedDefault() {
+ val userAgentMobile = "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36 Mobile Safari/537.36"
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+
+ val webViewSettings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(webViewSettings)
+ whenever(webViewSettings.userAgentString).thenReturn(userAgentMobile)
+
+ var desktopMode = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onDesktopModeChange(enabled: Boolean) {
+ desktopMode = enabled
+ }
+ },
+ )
+
+ engineSession.webView = webView
+ whenever(webView.settings).thenReturn(webViewSettings)
+ whenever(webViewSettings.userAgentString).thenReturn(userAgentMobile)
+
+ engineSession.toggleDesktopMode(true)
+ verify(webViewSettings).useWideViewPort = true
+ verify(engineSession).toggleDesktopUA(userAgentMobile, true)
+ assertTrue(desktopMode)
+ }
+
+ @Test
+ fun desktopModeToggleFalseWithNoProvidedDefault() {
+ val userAgentMobile = "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36 Mobile Safari/537.36"
+ val engineSession = spy(SystemEngineSession(testContext))
+
+ val webView = mock<WebView>()
+
+ val webViewSettings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(webViewSettings)
+ whenever(webViewSettings.userAgentString).thenReturn(userAgentMobile)
+
+ var desktopMode = false
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onDesktopModeChange(enabled: Boolean) {
+ desktopMode = enabled
+ }
+ },
+ )
+
+ engineSession.webView = webView
+ whenever(webView.settings).thenReturn(webViewSettings)
+ whenever(webViewSettings.userAgentString).thenReturn(userAgentMobile)
+
+ engineSession.toggleDesktopMode(false)
+ verify(webViewSettings).useWideViewPort = false
+ verify(engineSession).toggleDesktopUA(userAgentMobile, false)
+ assertFalse(desktopMode)
+ }
+
+ @Test
+ fun desktopModeUA() {
+ val userAgentMobile = "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36 Mobile Safari/537.36"
+ val userAgentDesktop = "Mozilla/5.0 (Linux; diordnA 9) AppleWebKit/537.36 eliboM Safari/537.36"
+ val engineSession = spy(SystemEngineSession(testContext))
+
+ assertEquals(engineSession.toggleDesktopUA(userAgentMobile, false), userAgentMobile)
+ assertEquals(engineSession.toggleDesktopUA(userAgentMobile, true), userAgentDesktop)
+ }
+
+ @Test
+ fun findAll() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ engineSession.findAll("mozilla")
+ verify(webView, never()).findAllAsync(anyString())
+
+ engineSession.webView = webView
+ var findObserved: String? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onFind(text: String) {
+ findObserved = text
+ }
+ },
+ )
+ engineSession.findAll("mozilla")
+ verify(webView).findAllAsync("mozilla")
+ assertEquals("mozilla", findObserved)
+ }
+
+ @Test
+ fun findNext() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.findNext(true)
+ verify(webView, never()).findNext(mockitoAny(Boolean::class.java))
+
+ engineSession.webView = webView
+ engineSession.findNext(true)
+ verify(webView).findNext(true)
+ }
+
+ @Test
+ fun clearFindMatches() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.clearFindMatches()
+ verify(webView, never()).clearMatches()
+
+ engineSession.webView = webView
+ engineSession.clearFindMatches()
+ verify(webView).clearMatches()
+ }
+
+ @Test
+ fun clearDataMakingExpectedCalls() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ val webStorage: WebStorage = mock()
+ val webViewDatabase: WebViewDatabase = mock()
+ val context: Context = testContext
+
+ doReturn(webStorage).`when`(engineSession).webStorage()
+ doReturn(webViewDatabase).`when`(engineSession).webViewDatabase(context)
+ whenever(webView.context).thenReturn(context)
+ engineSession.webView = webView
+
+ // clear all data by default
+ engineSession.clearData()
+ verify(webView).clearFormData()
+ verify(webView).clearHistory()
+ verify(webView).clearMatches()
+ verify(webView).clearSslPreferences()
+ verify(webView).clearCache(true)
+ verify(webStorage).deleteAllData()
+ verify(webViewDatabase).clearHttpAuthUsernamePassword()
+
+ // clear storages
+ engineSession.clearData(BrowsingData.select(BrowsingData.DOM_STORAGES))
+ verify(webStorage, times(2)).deleteAllData()
+ verify(webView, times(1)).clearCache(true)
+ verify(webView, times(1)).clearFormData()
+ verify(webView, times(1)).clearMatches()
+ verify(webView, times(1)).clearHistory()
+ verify(webView, times(1)).clearSslPreferences()
+ verify(webViewDatabase, times(1)).clearHttpAuthUsernamePassword()
+
+ // clear auth info
+ engineSession.clearData(BrowsingData.select(BrowsingData.AUTH_SESSIONS))
+ verify(webViewDatabase, times(2)).clearHttpAuthUsernamePassword()
+ verify(webStorage, times(2)).deleteAllData()
+ verify(webView, times(1)).clearCache(true)
+ verify(webView, times(1)).clearFormData()
+ verify(webView, times(1)).clearMatches()
+ verify(webView, times(1)).clearHistory()
+ verify(webView, times(1)).clearSslPreferences()
+
+ // clear cookies
+ engineSession.clearData(BrowsingData.select(BrowsingData.COOKIES))
+ verify(webViewDatabase, times(2)).clearHttpAuthUsernamePassword()
+ verify(webStorage, times(2)).deleteAllData()
+ verify(webView, times(1)).clearCache(true)
+ verify(webView, times(1)).clearFormData()
+ verify(webView, times(1)).clearMatches()
+ verify(webView, times(1)).clearHistory()
+ verify(webView, times(1)).clearSslPreferences()
+
+ // clear image cache
+ engineSession.clearData(BrowsingData.select(BrowsingData.IMAGE_CACHE))
+ verify(webView, times(2)).clearCache(true)
+ verify(webViewDatabase, times(2)).clearHttpAuthUsernamePassword()
+ verify(webStorage, times(2)).deleteAllData()
+ verify(webView, times(1)).clearFormData()
+ verify(webView, times(1)).clearMatches()
+ verify(webView, times(1)).clearHistory()
+ verify(webView, times(1)).clearSslPreferences()
+
+ // clear network cache
+ engineSession.clearData(BrowsingData.select(BrowsingData.NETWORK_CACHE))
+ verify(webView, times(3)).clearCache(true)
+ verify(webViewDatabase, times(2)).clearHttpAuthUsernamePassword()
+ verify(webStorage, times(2)).deleteAllData()
+ verify(webView, times(1)).clearFormData()
+ verify(webView, times(1)).clearMatches()
+ verify(webView, times(1)).clearHistory()
+ verify(webView, times(1)).clearSslPreferences()
+
+ // clear all caches
+ engineSession.clearData(BrowsingData.allCaches())
+ verify(webView, times(4)).clearCache(true)
+ verify(webViewDatabase, times(2)).clearHttpAuthUsernamePassword()
+ verify(webStorage, times(2)).deleteAllData()
+ verify(webView, times(1)).clearFormData()
+ verify(webView, times(1)).clearMatches()
+ verify(webView, times(1)).clearHistory()
+ verify(webView, times(1)).clearSslPreferences()
+ }
+
+ @Test
+ fun clearDataInvokesSuccessCallback() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ val webStorage: WebStorage = mock()
+ val webViewDatabase: WebViewDatabase = mock()
+ val context: Context = testContext
+ var onSuccessCalled = false
+
+ doReturn(webStorage).`when`(engineSession).webStorage()
+ doReturn(webViewDatabase).`when`(engineSession).webViewDatabase(context)
+ whenever(webView.context).thenReturn(context)
+ engineSession.webView = webView
+
+ engineSession.clearData(onSuccess = { onSuccessCalled = true })
+ assertTrue(onSuccessCalled)
+ }
+
+ @Test
+ fun clearDataInvokesErrorCallback() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ val webViewDatabase: WebViewDatabase = mock()
+ val context: Context = testContext
+ var onErrorCalled = false
+
+ val exception = RuntimeException()
+ doThrow(exception).`when`(engineSession).webStorage()
+ doReturn(webViewDatabase).`when`(engineSession).webViewDatabase(context)
+ whenever(webView.context).thenReturn(context)
+ engineSession.webView = webView
+
+ engineSession.clearData(
+ onError = {
+ onErrorCalled = true
+ assertSame(it, exception)
+ },
+ )
+ assertTrue(onErrorCalled)
+ }
+
+ @Test
+ fun testExitFullscreenModeWithWebViewAndCallBack() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ val customViewCallback = mock<WebChromeClient.CustomViewCallback>()
+
+ engineView.render(engineSession)
+ engineSession.exitFullScreenMode()
+ verify(customViewCallback, never()).onCustomViewHidden()
+
+ engineSession.fullScreenCallback = customViewCallback
+ engineSession.exitFullScreenMode()
+ verify(customViewCallback).onCustomViewHidden()
+ }
+
+ @Test
+ fun closeDestroysWebView() {
+ val engineSession = SystemEngineSession(testContext)
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+ engineSession.webView = webView
+
+ engineSession.close()
+ verify(webView).destroy()
+ }
+
+ @Test
+ fun `purgeHistory delegates to clearHistory`() {
+ val engineSession = SystemEngineSession(testContext)
+
+ val webView: WebView = mock()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.webView = webView
+
+ engineSession.purgeHistory()
+ verify(webView).clearHistory()
+ }
+
+ @Test
+ fun `GIVEN webView_canGoBack() true WHEN goBack() is called THEN verify EngineObserver onNavigateBack() is triggered`() {
+ var observedOnNavigateBack = false
+
+ val engineSession = SystemEngineSession(testContext)
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.webView = webView
+ Mockito.`when`(webView.canGoBack()).thenReturn(true)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onNavigateBack() {
+ observedOnNavigateBack = true
+ }
+ },
+ )
+
+ engineSession.goBack()
+ assertTrue(observedOnNavigateBack)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineTest.kt
new file mode 100644
index 0000000000..596157111b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineTest.kt
@@ -0,0 +1,105 @@
+/* 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.browser.engine.system
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.UnsupportedSettingException
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SystemEngineTest {
+
+ @Before
+ fun setup() {
+ // This is setting a internal field just for testing purposes as
+ // WebSettings.getDefaultUserAgent isn't mocked by Roboelectric
+ SystemEngine.defaultUserAgent = "test-ua-string"
+ }
+
+ @Test
+ fun createView() {
+ val engine = SystemEngine(testContext)
+ assertTrue(engine.createView(testContext) is SystemEngineView)
+ }
+
+ @Test
+ fun createSession() {
+ val engine = SystemEngine(testContext)
+ assertTrue(engine.createSession() is SystemEngineSession)
+
+ try {
+ engine.createSession(true)
+ // Private browsing not yet supported
+ fail("Expected UnsupportedOperationException")
+ } catch (e: UnsupportedOperationException) { }
+
+ try {
+ engine.createSession(false, "1")
+ // Contextual identities not yet supported
+ fail("Expected UnsupportedOperationException")
+ } catch (e: UnsupportedOperationException) { }
+ }
+
+ @Test
+ fun name() {
+ val engine = SystemEngine(testContext)
+ assertEquals("System", engine.name())
+ }
+
+ @Test
+ fun settings() {
+ val engine = SystemEngine(
+ testContext,
+ DefaultSettings(
+ remoteDebuggingEnabled = true,
+ trackingProtectionPolicy = EngineSession.TrackingProtectionPolicy.strict(),
+ ),
+ )
+
+ assertTrue(engine.settings.remoteDebuggingEnabled)
+ engine.settings.remoteDebuggingEnabled = false
+ assertFalse(engine.settings.remoteDebuggingEnabled)
+
+ assertEquals(
+ engine.settings.trackingProtectionPolicy,
+ EngineSession.TrackingProtectionPolicy.strict(),
+ )
+ engine.settings.trackingProtectionPolicy = EngineSession.TrackingProtectionPolicy.none()
+ assertEquals(engine.settings.trackingProtectionPolicy, EngineSession.TrackingProtectionPolicy.none())
+
+ // Specifying no ua-string default should result in WebView default
+ // It should be possible to read and set a new default
+ assertEquals("test-ua-string", engine.settings.userAgentString)
+ engine.settings.userAgentString = engine.settings.userAgentString + "-test"
+ assertEquals("test-ua-string-test", engine.settings.userAgentString)
+
+ // It should be possible to specify a custom ua-string default
+ assertEquals("foo", SystemEngine(testContext, DefaultSettings(userAgentString = "foo")).settings.userAgentString)
+ }
+
+ // This feature will be covered on this issue
+ // https://github.com/mozilla-mobile/android-components/issues/4206
+ @Test(expected = UnsupportedSettingException::class)
+ fun safeBrowsingIsNotSupportedYet() {
+ val engine = SystemEngine(
+ testContext,
+ DefaultSettings(
+ remoteDebuggingEnabled = true,
+ trackingProtectionPolicy = EngineSession.TrackingProtectionPolicy.strict(),
+ ),
+ )
+
+ engine.settings.safeBrowsingPolicy
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt
new file mode 100644
index 0000000000..95cfbd2253
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt
@@ -0,0 +1,1654 @@
+/* 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.browser.engine.system
+
+import android.app.Activity
+import android.graphics.Bitmap
+import android.net.Uri
+import android.net.http.SslCertificate
+import android.net.http.SslError
+import android.os.Build
+import android.os.Bundle
+import android.os.Message
+import android.view.PixelCopy
+import android.view.View
+import android.webkit.HttpAuthHandler
+import android.webkit.JsPromptResult
+import android.webkit.JsResult
+import android.webkit.SslErrorHandler
+import android.webkit.ValueCallback
+import android.webkit.WebChromeClient
+import android.webkit.WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebView.HitTestResult
+import android.webkit.WebViewClient
+import android.webkit.WebViewDatabase
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.engine.system.matcher.UrlMatcher
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.concept.engine.InputResultDetail
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.storage.PageVisit
+import mozilla.components.concept.storage.VisitType
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.shadow.PixelCopyShadow
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.robolectric.Robolectric
+import org.robolectric.annotation.Config
+import java.io.StringReader
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class SystemEngineViewTest {
+
+ @Test
+ fun `EngineView initialization`() {
+ val engineView = SystemEngineView(testContext)
+ val webView = WebView(testContext)
+
+ engineView.initWebView(webView)
+ assertNotNull(webView.webChromeClient)
+ assertNotNull(webView.webViewClient)
+ }
+
+ @Test
+ fun `EngineView renders WebView`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+
+ engineView.render(engineSession)
+ assertEquals(engineSession.webView, engineView.getChildAt(0))
+ }
+
+ @Test
+ fun `WebViewClient notifies observers`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ var observedUrl = ""
+ var observedUserGesture = true
+ var observedLoadingState = false
+ var observedSecurityChange: Triple<Boolean, String?, String?> = Triple(false, null, null)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadingStateChange(loading: Boolean) { observedLoadingState = loading }
+ override fun onLocationChange(url: String, hasUserGesture: Boolean) {
+ observedUrl = url
+ observedUserGesture = hasUserGesture
+ }
+ override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
+ observedSecurityChange = Triple(secure, host, issuer)
+ }
+ },
+ )
+
+ engineSession.webView.webViewClient.onPageStarted(mock(), "https://wiki.mozilla.org/", null)
+ assertEquals(true, observedLoadingState)
+ assertEquals(observedUrl, "https://wiki.mozilla.org/")
+
+ observedLoadingState = true
+ engineSession.webView.webViewClient.onPageFinished(null, "http://mozilla.org")
+ assertEquals("http://mozilla.org", observedUrl)
+ assertEquals(false, observedUserGesture)
+ assertFalse(observedLoadingState)
+ assertEquals(Triple(false, null, null), observedSecurityChange)
+
+ val view = mock<WebView>()
+ engineSession.webView.webViewClient.onPageFinished(view, "http://mozilla.org")
+ assertEquals(Triple(false, null, null), observedSecurityChange)
+
+ val certificate = mock<SslCertificate>()
+ val dName = mock<SslCertificate.DName>()
+ doReturn("testCA").`when`(dName).oName
+ doReturn(certificate).`when`(view).certificate
+ engineSession.webView.webViewClient.onPageFinished(view, "http://mozilla.org")
+
+ doReturn("testCA").`when`(dName).oName
+ doReturn(dName).`when`(certificate).issuedBy
+ doReturn(certificate).`when`(view).certificate
+ engineSession.webView.webViewClient.onPageFinished(view, "http://mozilla.org")
+ assertEquals(Triple(true, "mozilla.org", "testCA"), observedSecurityChange)
+ }
+
+ @Test
+ fun `HitResult type handling`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var hitTestResult: HitResult = HitResult.UNKNOWN("")
+ engineView.render(engineSession)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLongPress(hitResult: HitResult) {
+ hitTestResult = hitResult
+ }
+ },
+ )
+
+ engineView.handleLongClick(HitTestResult.EMAIL_TYPE, "mailto:asa@mozilla.com")
+ assertTrue(hitTestResult is HitResult.EMAIL)
+ assertEquals("mailto:asa@mozilla.com", hitTestResult.src)
+
+ engineView.handleLongClick(HitTestResult.GEO_TYPE, "geo:1,-1")
+ assertTrue(hitTestResult is HitResult.GEO)
+ assertEquals("geo:1,-1", hitTestResult.src)
+
+ engineView.handleLongClick(HitTestResult.PHONE_TYPE, "tel:+123456789")
+ assertTrue(hitTestResult is HitResult.PHONE)
+ assertEquals("tel:+123456789", hitTestResult.src)
+
+ engineView.handleLongClick(HitTestResult.IMAGE_TYPE, "image.png")
+ assertTrue(hitTestResult is HitResult.IMAGE)
+ assertEquals("image.png", hitTestResult.src)
+
+ engineView.handleLongClick(HitTestResult.SRC_ANCHOR_TYPE, "https://mozilla.org")
+ assertTrue(hitTestResult is HitResult.UNKNOWN)
+ assertEquals("https://mozilla.org", hitTestResult.src)
+
+ var result = engineView.handleLongClick(HitTestResult.SRC_IMAGE_ANCHOR_TYPE, "image.png")
+ assertFalse(result) // Intentional for image links; see ImageHandler tests.
+
+ result = engineView.handleLongClick(HitTestResult.EDIT_TEXT_TYPE, "https://mozilla.org")
+ assertFalse(result)
+ }
+
+ @Test
+ fun `ImageHandler notifies observers`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ val handler = SystemEngineView.ImageHandler(engineSession)
+ val message = mock<Message>()
+ val bundle = mock<Bundle>()
+ var observerNotified = false
+
+ whenever(message.data).thenReturn(bundle)
+ whenever(message.data.getString("url")).thenReturn("https://mozilla.org")
+ whenever(message.data.getString("src")).thenReturn("file.png")
+
+ engineView.render(engineSession)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLongPress(hitResult: HitResult) {
+ observerNotified = true
+ }
+ },
+ )
+
+ handler.handleMessage(message)
+ assertTrue(observerNotified)
+
+ observerNotified = false
+ val nullHandler = SystemEngineView.ImageHandler(null)
+ nullHandler.handleMessage(message)
+ assertFalse(observerNotified)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `null image src`() {
+ val engineSession = SystemEngineSession(testContext)
+ val handler = SystemEngineView.ImageHandler(engineSession)
+ val message = mock<Message>()
+ val bundle = mock<Bundle>()
+
+ whenever(message.data).thenReturn(bundle)
+ whenever(message.data.getString("url")).thenReturn("https://mozilla.org")
+
+ handler.handleMessage(message)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `null image url`() {
+ val engineSession = SystemEngineSession(testContext)
+ val handler = SystemEngineView.ImageHandler(engineSession)
+ val message = mock<Message>()
+ val bundle = mock<Bundle>()
+
+ whenever(message.data).thenReturn(bundle)
+ whenever(message.data.getString("src")).thenReturn("file.png")
+
+ handler.handleMessage(message)
+ }
+
+ @Test
+ fun `WebChromeClient notifies observers`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ var observedProgress = 0
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onProgress(progress: Int) { observedProgress = progress }
+ },
+ )
+
+ engineSession.webView.webChromeClient!!.onProgressChanged(null, 100)
+ assertEquals(100, observedProgress)
+ }
+
+ @Test
+ fun `SystemEngineView updates current session url via onPageStart events`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ assertEquals("", engineSession.currentUrl)
+ engineSession.webView.webViewClient.onPageStarted(engineSession.webView, "https://www.mozilla.org/", null)
+ assertEquals("https://www.mozilla.org/", engineSession.currentUrl)
+
+ engineSession.webView.webViewClient.onPageStarted(engineSession.webView, "https://www.firefox.com/", null)
+ assertEquals("https://www.firefox.com/", engineSession.currentUrl)
+ }
+
+ @Test
+ fun `WebView client notifies navigation state changes`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+
+ val observer: EngineSession.Observer = mock()
+ val webView: WebView = mock()
+ whenever(webView.canGoBack()).thenReturn(true)
+ whenever(webView.canGoForward()).thenReturn(true)
+
+ engineSession.register(observer)
+ verify(observer, never()).onNavigationStateChange(true, true)
+
+ engineView.render(engineSession)
+ engineSession.webView.webViewClient.onPageStarted(webView, "https://www.mozilla.org/", null)
+ verify(observer).onNavigationStateChange(true, true)
+ }
+
+ @Test
+ fun `WebView client notifies configured history delegate of url visits`() = runTest {
+ val engineSession = SystemEngineSession(testContext)
+
+ val engineView = SystemEngineView(testContext)
+ val webView: WebView = mock()
+ val historyDelegate: HistoryTrackingDelegate = mock()
+
+ engineView.render(engineSession)
+
+ // Nothing breaks if delegate isn't set.
+ engineSession.webView.webViewClient.doUpdateVisitedHistory(webView, "https://www.mozilla.com", false)
+
+ engineSession.settings.historyTrackingDelegate = historyDelegate
+ whenever(historyDelegate.shouldStoreUri(any())).thenReturn(true)
+
+ engineSession.webView.webViewClient.doUpdateVisitedHistory(webView, "https://www.mozilla.com", false)
+ verify(historyDelegate).onVisited(eq("https://www.mozilla.com"), eq(PageVisit(VisitType.LINK)))
+
+ engineSession.webView.webViewClient.doUpdateVisitedHistory(webView, "https://www.mozilla.com", true)
+ verify(historyDelegate).onVisited(eq("https://www.mozilla.com"), eq(PageVisit(VisitType.RELOAD)))
+ }
+
+ @Test
+ fun `WebView client checks with the delegate if the URI visit should be recorded`() = runTest {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ val webView: WebView = mock()
+ engineView.render(engineSession)
+
+ val historyDelegate: HistoryTrackingDelegate = mock()
+ engineSession.settings.historyTrackingDelegate = historyDelegate
+
+ whenever(historyDelegate.shouldStoreUri("https://www.mozilla.com")).thenReturn(true)
+
+ // Verify that engine session asked delegate if uri should be stored.
+ engineSession.webView.webViewClient.doUpdateVisitedHistory(webView, "https://www.mozilla.com", false)
+ verify(historyDelegate).onVisited(eq("https://www.mozilla.com"), eq(PageVisit(VisitType.LINK)))
+ verify(historyDelegate).shouldStoreUri("https://www.mozilla.com")
+
+ // Verify that engine won't try to store a uri that delegate doesn't want.
+ engineSession.webView.webViewClient.doUpdateVisitedHistory(webView, "https://www.mozilla.com/not-allowed", false)
+ verify(historyDelegate, never()).onVisited(eq("https://www.mozilla.com/not-allowed"), any())
+ verify(historyDelegate).shouldStoreUri("https://www.mozilla.com/not-allowed")
+ Unit
+ }
+
+ @Test
+ fun `WebView client requests history from configured history delegate`() = runTest {
+ val engineSession = SystemEngineSession(testContext)
+
+ val engineView = SystemEngineView(testContext)
+ val historyDelegate = object : HistoryTrackingDelegate {
+ override suspend fun onVisited(uri: String, visit: PageVisit) {
+ fail()
+ }
+
+ override fun shouldStoreUri(uri: String): Boolean {
+ return true
+ }
+
+ override suspend fun onTitleChanged(uri: String, title: String) {
+ fail()
+ }
+
+ override suspend fun onPreviewImageChange(uri: String, previewImageUrl: String) {
+ fail()
+ }
+
+ override suspend fun getVisited(uris: List<String>): List<Boolean> {
+ fail()
+ return emptyList()
+ }
+
+ override suspend fun getVisited(): List<String> {
+ return listOf("https://www.mozilla.com")
+ }
+ }
+
+ engineView.render(engineSession)
+
+ // Nothing breaks if delegate isn't set.
+ engineSession.webView.webChromeClient!!.getVisitedHistory(mock())
+
+ engineSession.settings.historyTrackingDelegate = historyDelegate
+
+ val historyValueCallback: ValueCallback<Array<String>> = mock()
+ engineSession.webView.webChromeClient!!.getVisitedHistory(historyValueCallback)
+ verify(historyValueCallback).onReceiveValue(arrayOf("https://www.mozilla.com"))
+ }
+
+ @Test
+ fun `WebView client notifies configured history delegate of title changes`() = runTest {
+ val engineSession = SystemEngineSession(testContext)
+
+ val engineView = SystemEngineView(testContext)
+ val webView: WebView = mock()
+ val historyDelegate: HistoryTrackingDelegate = mock()
+
+ engineView.render(engineSession)
+
+ // Nothing breaks if delegate isn't set.
+ engineSession.webView.webChromeClient!!.onReceivedTitle(webView, "New title!")
+
+ // We can now set the delegate. Were it set before the render call,
+ // it'll get overwritten during settings initialization.
+ engineSession.settings.historyTrackingDelegate = historyDelegate
+
+ // Delegate not notified if, somehow, there's no currentUrl present in the view.
+ engineSession.webView.webChromeClient!!.onReceivedTitle(webView, "New title!")
+ verify(historyDelegate, never()).onTitleChanged(eq(""), eq("New title!"))
+
+ // This sets the currentUrl.
+ engineSession.webView.webViewClient.onPageStarted(webView, "https://www.mozilla.org/", null)
+
+ engineSession.webView.webChromeClient!!.onReceivedTitle(webView, "New title!")
+ verify(historyDelegate).onTitleChanged(eq("https://www.mozilla.org/"), eq("New title!"))
+
+ reset(historyDelegate)
+
+ // Empty title when none provided
+ engineSession.webView.webChromeClient!!.onReceivedTitle(webView, null)
+ verify(historyDelegate).onTitleChanged(eq("https://www.mozilla.org/"), eq(""))
+ }
+
+ @Test
+ fun `WebView client notifies observers about title changes`() {
+ val engineSession = SystemEngineSession(testContext)
+
+ val engineView = SystemEngineView(testContext)
+ val observer: EngineSession.Observer = mock()
+ val webView: WebView = mock()
+ whenever(webView.canGoBack()).thenReturn(true)
+ whenever(webView.canGoForward()).thenReturn(true)
+
+ engineSession.register(observer)
+ engineView.render(engineSession)
+ engineSession.webView.webChromeClient!!.onReceivedTitle(webView, "Hello World!")
+ verify(observer).onTitleChange(eq("Hello World!"))
+ verify(observer).onNavigationStateChange(true, true)
+
+ reset(observer)
+
+ // Empty title when none provided.
+ engineSession.webView.webChromeClient!!.onReceivedTitle(webView, null)
+ verify(observer).onTitleChange(eq(""))
+ verify(observer).onNavigationStateChange(true, true)
+ }
+
+ @Test
+ fun `download listener notifies observers`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ var observerNotified = false
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onExternalResource(
+ url: String,
+ fileName: String?,
+ contentLength: Long?,
+ contentType: String?,
+ cookie: String?,
+ userAgent: String?,
+ isPrivate: Boolean,
+ skipConfirmation: Boolean,
+ openInApp: Boolean,
+ response: Response?,
+ ) {
+ assertEquals("https://download.mozilla.org", url)
+ assertEquals("image.png", fileName)
+ assertEquals(1337L, contentLength)
+ assertNull(cookie)
+ assertEquals("Components/1.0", userAgent)
+
+ observerNotified = true
+ }
+ },
+ )
+
+ val listener = engineView.createDownloadListener()
+ listener.onDownloadStart(
+ "https://download.mozilla.org",
+ "Components/1.0",
+ "attachment; filename=\"image.png\"",
+ "image/png",
+ 1337,
+ )
+
+ assertTrue(observerNotified)
+ }
+
+ @Test
+ fun `WebView client tracking protection`() {
+ SystemEngineView.URL_MATCHER = UrlMatcher(arrayOf("blocked.random"))
+
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val webViewClient = engineSession.webView.webViewClient
+ val invalidRequest = mock<WebResourceRequest>()
+ whenever(invalidRequest.isForMainFrame).thenReturn(false)
+ whenever(invalidRequest.url).thenReturn(Uri.parse("market://foo.bar/"))
+
+ var response = webViewClient.shouldInterceptRequest(engineSession.webView, invalidRequest)
+ assertNull(response)
+
+ engineSession.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+ response = webViewClient.shouldInterceptRequest(engineSession.webView, invalidRequest)
+ assertNotNull(response)
+ assertNull(response!!.data)
+ assertNull(response.encoding)
+ assertNull(response.mimeType)
+
+ val faviconRequest = mock<WebResourceRequest>()
+ whenever(faviconRequest.isForMainFrame).thenReturn(false)
+ whenever(faviconRequest.url).thenReturn(Uri.parse("http://foo/favicon.ico"))
+ response = webViewClient.shouldInterceptRequest(engineSession.webView, faviconRequest)
+ assertNotNull(response)
+ assertNull(response!!.data)
+ assertNull(response.encoding)
+ assertNull(response.mimeType)
+
+ val blockedRequest = mock<WebResourceRequest>()
+ whenever(blockedRequest.isForMainFrame).thenReturn(false)
+ whenever(blockedRequest.url).thenReturn(Uri.parse("http://blocked.random"))
+
+ var trackerBlocked: Tracker? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onTrackerBlocked(tracker: Tracker) {
+ trackerBlocked = tracker
+ }
+ },
+ )
+
+ response = webViewClient.shouldInterceptRequest(engineSession.webView, blockedRequest)
+ assertNotNull(response)
+ assertNull(response!!.data)
+ assertNull(response.encoding)
+ assertNull(response.mimeType)
+ assertTrue(trackerBlocked!!.trackingCategories.isEmpty())
+ }
+
+ @Test
+ fun `blocked trackers are reported with correct categories`() {
+ val BLOCK_LIST = """{
+ "license": "test-license",
+ "categories": {
+ "Advertising": [
+ {
+ "AdTest1": {
+ "http://www.adtest1.com/": [
+ "adtest1.com"
+ ]
+ }
+ }
+ ],
+ "Analytics": [
+ {
+ "AnalyticsTest": {
+ "http://analyticsTest1.com/": [
+ "analyticsTest1.com"
+ ]
+ }
+ }
+ ],
+ "Content": [
+ {
+ "ContentTest1": {
+ "http://contenttest1.com/": [
+ "contenttest1.com"
+ ]
+ }
+ }
+ ],
+ "Social": [
+ {
+ "SocialTest1": {
+ "http://www.socialtest1.com/": [
+ "socialtest1.com"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ """
+ SystemEngineView.URL_MATCHER = UrlMatcher.createMatcher(
+ StringReader(BLOCK_LIST),
+ StringReader("{}"),
+ )
+
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var trackerBlocked: Tracker? = null
+
+ engineView.render(engineSession)
+ val webViewClient = engineSession.webView.webViewClient
+
+ engineSession.trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onTrackerBlocked(tracker: Tracker) {
+ trackerBlocked = tracker
+ }
+ },
+ )
+
+ val blockedRequest = mock<WebResourceRequest>()
+ whenever(blockedRequest.isForMainFrame).thenReturn(false)
+
+ whenever(blockedRequest.url).thenReturn(Uri.parse("http://www.adtest1.com/"))
+ webViewClient.shouldInterceptRequest(engineSession.webView, blockedRequest)
+
+ assertTrue(trackerBlocked!!.trackingCategories.first() == TrackingCategory.AD)
+
+ whenever(blockedRequest.url).thenReturn(Uri.parse("http://analyticsTest1.com/"))
+ webViewClient.shouldInterceptRequest(engineSession.webView, blockedRequest)
+
+ assertTrue(trackerBlocked!!.trackingCategories.first() == TrackingCategory.ANALYTICS)
+
+ whenever(blockedRequest.url).thenReturn(Uri.parse("http://www.socialtest1.com/"))
+ webViewClient.shouldInterceptRequest(engineSession.webView, blockedRequest)
+
+ assertTrue(trackerBlocked!!.trackingCategories.first() == TrackingCategory.SOCIAL)
+
+ SystemEngineView.URL_MATCHER = null
+ }
+
+ @Test
+ @Suppress("Deprecation")
+ fun `WebViewClient calls interceptor from deprecated onReceivedError API`() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+ doNothing().`when`(engineSession).initSettings()
+
+ val requestInterceptor: RequestInterceptor = mock()
+ val webViewClient = engineSession.webView.webViewClient
+
+ // No session or interceptor attached.
+ webViewClient.onReceivedError(
+ engineSession.webView,
+ WebViewClient.ERROR_UNKNOWN,
+ null,
+ "http://failed.random",
+ )
+ verifyNoInteractions(requestInterceptor)
+
+ // Session attached, but not interceptor.
+ engineView.render(engineSession)
+ webViewClient.onReceivedError(
+ engineSession.webView,
+ WebViewClient.ERROR_UNKNOWN,
+ null,
+ "http://failed.random",
+ )
+ verifyNoInteractions(requestInterceptor)
+
+ // Session and interceptor.
+ engineSession.settings.requestInterceptor = requestInterceptor
+ webViewClient.onReceivedError(
+ engineSession.webView,
+ WebViewClient.ERROR_UNKNOWN,
+ null,
+ "http://failed.random",
+ )
+ verify(requestInterceptor).onErrorRequest(engineSession, ErrorType.UNKNOWN, "http://failed.random")
+
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.webView = webView
+ val errorResponse = RequestInterceptor.ErrorResponse("about:fail")
+ webViewClient.onReceivedError(
+ engineSession.webView,
+ WebViewClient.ERROR_UNKNOWN,
+ null,
+ "http://failed.random",
+ )
+ verify(webView, never()).loadUrl(ArgumentMatchers.anyString())
+
+ whenever(requestInterceptor.onErrorRequest(engineSession, ErrorType.UNKNOWN, "http://failed.random"))
+ .thenReturn(errorResponse)
+ webViewClient.onReceivedError(
+ engineSession.webView,
+ WebViewClient.ERROR_UNKNOWN,
+ null,
+ "http://failed.random",
+ )
+ verify(webView).loadUrl("about:fail")
+
+ val errorResponse2 = RequestInterceptor.ErrorResponse("about:fail2")
+ webViewClient.onReceivedError(
+ engineSession.webView,
+ WebViewClient.ERROR_UNKNOWN,
+ null,
+ "http://failed.random",
+ )
+ verify(webView, never()).loadUrl("about:fail2")
+
+ whenever(requestInterceptor.onErrorRequest(engineSession, ErrorType.UNKNOWN, "http://failed.random"))
+ .thenReturn(errorResponse2)
+ webViewClient.onReceivedError(
+ engineSession.webView,
+ WebViewClient.ERROR_UNKNOWN,
+ null,
+ "http://failed.random",
+ )
+ verify(webView).loadUrl("about:fail2")
+ }
+
+ @Test
+ fun `WebViewClient calls interceptor from new onReceivedError API`() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+ doNothing().`when`(engineSession).initSettings()
+
+ val requestInterceptor: RequestInterceptor = mock()
+ val webViewClient = engineSession.webView.webViewClient
+ val webRequest: WebResourceRequest = mock()
+ val webError: WebResourceError = mock()
+ val url: Uri = mock()
+
+ webViewClient.onReceivedError(engineSession.webView, webRequest, webError)
+ verifyNoInteractions(requestInterceptor)
+
+ engineView.render(engineSession)
+ webViewClient.onReceivedError(engineSession.webView, webRequest, webError)
+ verifyNoInteractions(requestInterceptor)
+
+ whenever(webError.errorCode).thenReturn(WebViewClient.ERROR_UNKNOWN)
+ whenever(webRequest.url).thenReturn(url)
+ whenever(url.toString()).thenReturn("http://failed.random")
+ engineSession.settings.requestInterceptor = requestInterceptor
+ webViewClient.onReceivedError(engineSession.webView, webRequest, webError)
+ verify(requestInterceptor, never()).onErrorRequest(engineSession, ErrorType.UNKNOWN, "http://failed.random")
+
+ whenever(webRequest.isForMainFrame).thenReturn(true)
+ webViewClient.onReceivedError(engineSession.webView, webRequest, webError)
+ verify(requestInterceptor).onErrorRequest(engineSession, ErrorType.UNKNOWN, "http://failed.random")
+
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.webView = webView
+ val errorResponse = RequestInterceptor.ErrorResponse("about:fail")
+ webViewClient.onReceivedError(engineSession.webView, webRequest, webError)
+ verify(webView, never()).loadUrl(ArgumentMatchers.anyString())
+
+ whenever(requestInterceptor.onErrorRequest(engineSession, ErrorType.UNKNOWN, "http://failed.random"))
+ .thenReturn(errorResponse)
+ webViewClient.onReceivedError(engineSession.webView, webRequest, webError)
+ verify(webView).loadUrl("about:fail")
+
+ val errorResponse2 = RequestInterceptor.ErrorResponse("about:fail2")
+ webViewClient.onReceivedError(engineSession.webView, webRequest, webError)
+ verify(webView, never()).loadUrl("about:fail2")
+
+ whenever(requestInterceptor.onErrorRequest(engineSession, ErrorType.UNKNOWN, "http://failed.random"))
+ .thenReturn(errorResponse2)
+ webViewClient.onReceivedError(engineSession.webView, webRequest, webError)
+ verify(webView).loadUrl("about:fail2")
+ }
+
+ @Test
+ fun `WebViewClient calls interceptor when onReceivedSslError`() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+ doNothing().`when`(engineSession).initSettings()
+
+ val requestInterceptor: RequestInterceptor = mock()
+ val webViewClient = engineSession.webView.webViewClient
+ val handler: SslErrorHandler = mock()
+ val error: SslError = mock()
+
+ webViewClient.onReceivedSslError(engineSession.webView, handler, error)
+ verifyNoInteractions(requestInterceptor)
+
+ engineView.render(engineSession)
+ webViewClient.onReceivedSslError(engineSession.webView, handler, error)
+ verifyNoInteractions(requestInterceptor)
+
+ whenever(error.primaryError).thenReturn(SslError.SSL_EXPIRED)
+ whenever(error.url).thenReturn("http://failed.random")
+ engineSession.settings.requestInterceptor = requestInterceptor
+ webViewClient.onReceivedSslError(engineSession.webView, handler, error)
+ verify(requestInterceptor).onErrorRequest(engineSession, ErrorType.ERROR_SECURITY_SSL, "http://failed.random")
+ verify(handler, times(3)).cancel()
+
+ val webView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(webView.settings).thenReturn(settings)
+
+ engineSession.webView = webView
+ val errorResponse = RequestInterceptor.ErrorResponse("about:fail")
+ webViewClient.onReceivedSslError(engineSession.webView, handler, error)
+ verify(webView, never()).loadUrl(ArgumentMatchers.anyString())
+
+ whenever(
+ requestInterceptor.onErrorRequest(
+ engineSession,
+ ErrorType.ERROR_SECURITY_SSL,
+ "http://failed.random",
+ ),
+ ).thenReturn(errorResponse)
+ webViewClient.onReceivedSslError(engineSession.webView, handler, error)
+ verify(webView).loadUrl("about:fail")
+
+ val errorResponse2 = RequestInterceptor.ErrorResponse("about:fail2")
+ webViewClient.onReceivedSslError(engineSession.webView, handler, error)
+ verify(webView, never()).loadUrl("about:fail2")
+
+ whenever(requestInterceptor.onErrorRequest(engineSession, ErrorType.ERROR_SECURITY_SSL, "http://failed.random"))
+ .thenReturn(errorResponse2)
+ webViewClient.onReceivedSslError(engineSession.webView, handler, error)
+ verify(webView).loadUrl("about:fail2")
+
+ whenever(requestInterceptor.onErrorRequest(engineSession, ErrorType.ERROR_SECURITY_SSL, "http://failed.random"))
+ .thenReturn(RequestInterceptor.ErrorResponse("http://failed.random"))
+ webViewClient.onReceivedSslError(engineSession.webView, handler, error)
+ verify(webView).loadUrl("http://failed.random")
+ }
+
+ @Test
+ fun `WebViewClient blocks WebFonts`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val webViewClient = engineSession.webView.webViewClient
+ val webFontRequest = mock<WebResourceRequest>()
+ whenever(webFontRequest.url).thenReturn(Uri.parse("/fonts/test.woff"))
+ assertNull(webViewClient.shouldInterceptRequest(engineSession.webView, webFontRequest))
+
+ engineView.render(engineSession)
+ assertNull(webViewClient.shouldInterceptRequest(engineSession.webView, webFontRequest))
+
+ engineSession.settings.webFontsEnabled = false
+
+ val request = mock<WebResourceRequest>()
+ whenever(request.url).thenReturn(Uri.parse("http://mozilla.org"))
+ assertNull(webViewClient.shouldInterceptRequest(engineSession.webView, request))
+
+ val response = webViewClient.shouldInterceptRequest(engineSession.webView, webFontRequest)
+ assertNotNull(response)
+ assertNull(response!!.data)
+ assertNull(response.encoding)
+ assertNull(response.mimeType)
+ }
+
+ @Test
+ fun `FindListener notifies observers`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ var observerNotified = false
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onFindResult(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean) {
+ assertEquals(0, activeMatchOrdinal)
+ assertEquals(1, numberOfMatches)
+ assertTrue(isDoneCounting)
+ observerNotified = true
+ }
+ },
+ )
+
+ val listener = engineView.createFindListener()
+ listener.onFindResultReceived(0, 1, true)
+ assertTrue(observerNotified)
+ }
+
+ @Test
+ fun `lifecycle methods are invoked`() {
+ val mockWebView = mock<WebView>()
+ val settings = mock<WebSettings>()
+ whenever(mockWebView.settings).thenReturn(settings)
+
+ val engineSession1 = SystemEngineSession(testContext)
+ val engineSession2 = SystemEngineSession(testContext)
+
+ val engineView = SystemEngineView(testContext)
+ engineView.onPause()
+ engineView.onResume()
+ engineView.onDestroy()
+
+ engineSession1.webView = mockWebView
+ engineView.render(engineSession1)
+ engineView.onDestroy()
+
+ engineView.render(engineSession2)
+ assertNotNull(engineSession2.webView.parent)
+
+ engineView.onDestroy()
+ assertNull(engineSession2.webView.parent)
+
+ engineView.render(engineSession1)
+ engineView.onPause()
+ verify(mockWebView, times(1)).onPause()
+ verify(mockWebView, times(1)).pauseTimers()
+
+ engineView.onResume()
+ verify(mockWebView, times(1)).onResume()
+ verify(mockWebView, times(1)).resumeTimers()
+
+ engineView.onDestroy()
+ }
+
+ @Test
+ fun `showCustomView notifies fullscreen mode observers and execs callback`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val observer: EngineSession.Observer = mock()
+ engineSession.register(observer)
+
+ val view = mock<View>()
+ val customViewCallback = mock<WebChromeClient.CustomViewCallback>()
+ engineSession.webView.webChromeClient!!.onShowCustomView(view, customViewCallback)
+
+ verify(observer).onFullScreenChange(true)
+ }
+
+ @Test
+ fun `addFullScreenView execs callback and removeView`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val view = View(ApplicationProvider.getApplicationContext())
+ val customViewCallback = mock<WebChromeClient.CustomViewCallback>()
+
+ assertNull(engineSession.fullScreenCallback)
+
+ engineSession.webView.webChromeClient!!.onShowCustomView(view, customViewCallback)
+
+ assertNotNull(engineSession.fullScreenCallback)
+ assertEquals(customViewCallback, engineSession.fullScreenCallback)
+ assertEquals("mozac_system_engine_fullscreen", view.tag)
+
+ engineSession.webView.webChromeClient!!.onHideCustomView()
+ assertEquals(View.VISIBLE, engineSession.webView.visibility)
+ }
+
+ @Test
+ fun `addFullScreenView with no matching webView`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val view = View(ApplicationProvider.getApplicationContext())
+ val customViewCallback = mock<WebChromeClient.CustomViewCallback>()
+
+ engineSession.webView.tag = "not_webview"
+ engineSession.webView.webChromeClient!!.onShowCustomView(view, customViewCallback)
+
+ assertNotEquals(View.INVISIBLE, engineSession.webView.visibility)
+ }
+
+ @Test
+ fun `removeFullScreenView with no matching views`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ val view = View(ApplicationProvider.getApplicationContext())
+ val customViewCallback = mock<WebChromeClient.CustomViewCallback>()
+
+ // When the fullscreen view isn't available
+ engineSession.webView.webChromeClient!!.onShowCustomView(view, customViewCallback)
+ engineView.findViewWithTag<View>("mozac_system_engine_fullscreen").tag = "not_fullscreen"
+
+ engineSession.webView.webChromeClient!!.onHideCustomView()
+
+ assertNotNull(engineSession.fullScreenCallback)
+ verify(engineSession.fullScreenCallback, never())?.onCustomViewHidden()
+ assertEquals(View.INVISIBLE, engineSession.webView.visibility)
+
+ // When fullscreen view is available, but WebView isn't.
+ engineView.findViewWithTag<View>("not_fullscreen").tag = "mozac_system_engine_fullscreen"
+ engineSession.webView.tag = "not_webView"
+
+ engineSession.webView.webChromeClient!!.onHideCustomView()
+
+ assertEquals(View.INVISIBLE, engineSession.webView.visibility)
+ }
+
+ @Test
+ fun `fullscreenCallback is null`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ engineSession.webView.webChromeClient!!.onHideCustomView()
+ assertNull(engineSession.fullScreenCallback)
+ }
+
+ @Test
+ fun `onPageFinished handles invalid URL`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ var observedUrl = ""
+ var observedLoadingState = true
+ var observedSecurityChange: Triple<Boolean, String?, String?> = Triple(false, null, null)
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onLoadingStateChange(loading: Boolean) { observedLoadingState = loading }
+ override fun onLocationChange(url: String, hasUserGesture: Boolean) { observedUrl = url }
+ override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
+ observedSecurityChange = Triple(secure, host, issuer)
+ }
+ },
+ )
+
+ // We need a certificate to trigger parsing the potentially invalid URL for
+ // the host parameter in onSecurityChange
+ val view = mock<WebView>()
+ val certificate = mock<SslCertificate>()
+ val dName = mock<SslCertificate.DName>()
+ doReturn("testCA").`when`(dName).oName
+ doReturn(dName).`when`(certificate).issuedBy
+ doReturn(certificate).`when`(view).certificate
+
+ engineSession.webView.webViewClient.onPageFinished(view, "invalid:")
+ assertEquals("invalid:", observedUrl)
+ assertFalse(observedLoadingState)
+ assertEquals(Triple(true, null, "testCA"), observedSecurityChange)
+ }
+
+ @Test
+ fun `URL matcher categories can be changed`() {
+ SystemEngineView.URL_MATCHER = null
+ val resources = testContext.resources
+
+ var urlMatcher = SystemEngineView.getOrCreateUrlMatcher(
+ resources,
+ TrackingProtectionPolicy.select(
+ arrayOf(
+ TrackingCategory.AD,
+ TrackingCategory.ANALYTICS,
+ ),
+ ),
+ )
+ assertEquals(setOf(UrlMatcher.ADVERTISING, UrlMatcher.ANALYTICS), urlMatcher.enabledCategories)
+
+ urlMatcher = SystemEngineView.getOrCreateUrlMatcher(
+ resources,
+ TrackingProtectionPolicy.select(
+ arrayOf(
+ TrackingCategory.AD,
+ TrackingCategory.SOCIAL,
+ ),
+ ),
+ )
+ assertEquals(setOf(UrlMatcher.ADVERTISING, UrlMatcher.SOCIAL), urlMatcher.enabledCategories)
+ }
+
+ @Test
+ fun `URL matcher supports compounded categories`() {
+ val recommendedPolicy = TrackingProtectionPolicy.recommended()
+ val strictPolicy = TrackingProtectionPolicy.strict()
+ val resources = testContext.resources
+ val recommendedCategories = setOf(
+ UrlMatcher.ADVERTISING,
+ UrlMatcher.ANALYTICS,
+ UrlMatcher.SOCIAL,
+ UrlMatcher.FINGERPRINTING,
+ UrlMatcher.CRYPTOMINING,
+ )
+ val strictCategories = setOf(
+ UrlMatcher.ADVERTISING,
+ UrlMatcher.ANALYTICS,
+ UrlMatcher.SOCIAL,
+ UrlMatcher.FINGERPRINTING,
+ UrlMatcher.CRYPTOMINING,
+ )
+
+ var urlMatcher = SystemEngineView.getOrCreateUrlMatcher(resources, recommendedPolicy)
+
+ assertEquals(recommendedCategories, urlMatcher.enabledCategories)
+
+ urlMatcher = SystemEngineView.getOrCreateUrlMatcher(resources, strictPolicy)
+
+ assertEquals(strictCategories, urlMatcher.enabledCategories)
+ }
+
+ @Test
+ fun `permission requests are forwarded to observers`() {
+ val permissionRequest: android.webkit.PermissionRequest = mock()
+ whenever(permissionRequest.resources).thenReturn(emptyArray())
+ whenever(permissionRequest.origin).thenReturn(Uri.parse("https://mozilla.org"))
+
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ var observedPermissionRequest: PermissionRequest? = null
+ var cancelledPermissionRequest: PermissionRequest? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onContentPermissionRequest(permissionRequest: PermissionRequest) {
+ observedPermissionRequest = permissionRequest
+ }
+
+ override fun onCancelContentPermissionRequest(permissionRequest: PermissionRequest) {
+ cancelledPermissionRequest = permissionRequest
+ }
+ },
+ )
+
+ engineSession.webView.webChromeClient!!.onPermissionRequest(permissionRequest)
+ assertNotNull(observedPermissionRequest)
+
+ engineSession.webView.webChromeClient!!.onPermissionRequestCanceled(permissionRequest)
+ assertNotNull(cancelledPermissionRequest)
+ }
+
+ @Test
+ fun `window requests are forwarded to observers`() {
+ val permissionRequest: android.webkit.PermissionRequest = mock()
+ whenever(permissionRequest.resources).thenReturn(emptyArray())
+ whenever(permissionRequest.origin).thenReturn(Uri.parse("https://mozilla.org"))
+
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ engineView.render(engineSession)
+
+ var createWindowRequest: WindowRequest? = null
+ var closeWindowRequest: WindowRequest? = null
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onWindowRequest(windowRequest: WindowRequest) {
+ if (windowRequest.type == WindowRequest.Type.OPEN) {
+ createWindowRequest = windowRequest
+ } else {
+ closeWindowRequest = windowRequest
+ }
+ }
+ },
+ )
+
+ engineSession.webView.webChromeClient!!.onCreateWindow(mock(), false, false, null)
+ assertNotNull(createWindowRequest)
+ assertNull(closeWindowRequest)
+
+ engineSession.webView.webChromeClient!!.onCloseWindow(mock())
+ assertNotNull(closeWindowRequest)
+ }
+
+ @Test
+ fun `Calling onShowFileChooser must provide a FilePicker PromptRequest`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var onSingleFileSelectedWasCalled = false
+ var onMultipleFilesSelectedWasCalled = false
+ var onDismissWasCalled = false
+ var request: PromptRequest? = null
+
+ val callback = ValueCallback<Array<Uri>> {
+ if (it == null) {
+ onDismissWasCalled = true
+ } else {
+ if (it.size == 1) {
+ onSingleFileSelectedWasCalled = true
+ } else {
+ onMultipleFilesSelectedWasCalled = true
+ }
+ }
+ }
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+
+ engineView.render(engineSession)
+
+ val mockFileChooserParams = mock<WebChromeClient.FileChooserParams>()
+
+ doReturn(MODE_OPEN_MULTIPLE).`when`(mockFileChooserParams).mode
+
+ engineSession.webView.webChromeClient!!.onShowFileChooser(null, callback, mockFileChooserParams)
+
+ val filePickerRequest = request as PromptRequest.File
+ assertTrue(request is PromptRequest.File)
+
+ filePickerRequest.onSingleFileSelected(mock(), mock())
+ assertTrue(onSingleFileSelectedWasCalled)
+
+ filePickerRequest.onMultipleFilesSelected(mock(), arrayOf(mock(), mock()))
+ assertTrue(onMultipleFilesSelectedWasCalled)
+
+ filePickerRequest.onDismiss()
+ assertTrue(onDismissWasCalled)
+
+ assertTrue(filePickerRequest.mimeTypes.isEmpty())
+ assertTrue(filePickerRequest.isMultipleFilesSelection)
+
+ doReturn(arrayOf("")).`when`(mockFileChooserParams).acceptTypes
+ engineSession.webView.webChromeClient!!.onShowFileChooser(null, callback, mockFileChooserParams)
+ assertTrue(filePickerRequest.mimeTypes.isEmpty())
+ }
+
+ @Test
+ fun `canScrollVerticallyDown can be called without session`() {
+ val engineView = SystemEngineView(testContext)
+ assertFalse(engineView.canScrollVerticallyDown())
+
+ engineView.render(SystemEngineSession(testContext))
+ assertFalse(engineView.canScrollVerticallyDown())
+ }
+
+ @Test
+ fun `onLongClick can be called without session`() {
+ val engineView = SystemEngineView(testContext)
+ assertFalse(engineView.onLongClick(null))
+
+ engineView.render(SystemEngineSession(testContext))
+ assertFalse(engineView.onLongClick(null))
+ }
+
+ @Test
+ fun `Calling onJsAlert must provide an Alert PromptRequest`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+
+ engineView.render(engineSession)
+
+ val mockJSResult = mock<JsResult>()
+
+ engineSession.webView.webChromeClient!!.onJsAlert(mock(), "http://www.mozilla.org", "message", mockJSResult)
+
+ val alertRequest = request as PromptRequest.Alert
+ assertTrue(request is PromptRequest.Alert)
+
+ assertTrue(alertRequest.title.contains("mozilla.org"))
+ assertEquals(alertRequest.message, "message")
+
+ alertRequest.onConfirm(true)
+ verify(mockJSResult).confirm()
+
+ alertRequest.onDismiss()
+ verify(mockJSResult).cancel()
+ }
+
+ @Test
+ fun `calling onJsPrompt must provide a TextPrompt PromptRequest`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+
+ engineView.render(engineSession)
+
+ val mockJSPromptResult = mock<JsPromptResult>()
+
+ engineSession.webView.webChromeClient!!.onJsPrompt(
+ mock(),
+ "http://www.mozilla.org",
+ "message",
+ "defaultValue",
+ mockJSPromptResult,
+ )
+
+ val textPromptRequest = request as PromptRequest.TextPrompt
+ assertTrue(request is PromptRequest.TextPrompt)
+
+ assertTrue(textPromptRequest.title.contains("mozilla.org"))
+ assertEquals(textPromptRequest.hasShownManyDialogs, false)
+ assertEquals(textPromptRequest.inputLabel, "message")
+ assertEquals(textPromptRequest.inputValue, "defaultValue")
+
+ textPromptRequest.onConfirm(true, "value")
+ verify(mockJSPromptResult).confirm("value")
+
+ textPromptRequest.onDismiss()
+ verify(mockJSPromptResult).cancel()
+
+ textPromptRequest.onConfirm(true, "value")
+ }
+
+ @Test
+ fun `calling onJsPrompt with a null session must not provide a TextPrompt PromptRequest`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+
+ engineView.render(engineSession)
+
+ val mockJSPromptResult = mock<JsPromptResult>()
+ engineView.session = null
+
+ val wasTheDialogHandled = engineSession.webView.webChromeClient!!.onJsPrompt(
+ mock(),
+ "http://www.mozilla.org",
+ "message",
+ "defaultValue",
+ mockJSPromptResult,
+ )
+
+ assertTrue(wasTheDialogHandled)
+ assertNull(request)
+ verify(mockJSPromptResult).cancel()
+ }
+
+ @Test
+ fun `calling onJsConfirm must provide a Confirm PromptRequest`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+
+ engineView.render(engineSession)
+
+ val mockJSPromptResult = mock<JsResult>()
+
+ engineSession.webView.webChromeClient!!.onJsConfirm(
+ mock(),
+ "http://www.mozilla.org",
+ "message",
+ mockJSPromptResult,
+ )
+
+ val confirmPromptRequest = request as PromptRequest.Confirm
+ assertTrue(request is PromptRequest.Confirm)
+
+ assertTrue(confirmPromptRequest.title.contains("mozilla.org"))
+ assertEquals(confirmPromptRequest.hasShownManyDialogs, false)
+ assertEquals(confirmPromptRequest.message, "message")
+
+ confirmPromptRequest.onConfirmPositiveButton(true)
+ verify(mockJSPromptResult).confirm()
+
+ confirmPromptRequest.onDismiss()
+ verify(mockJSPromptResult).cancel()
+
+ confirmPromptRequest.onConfirmNegativeButton(true)
+ verify(mockJSPromptResult, times(2)).cancel()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun captureThumbnailOnPreO() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val engineView = SystemEngineView(activity)
+ val webView = mock<WebView>()
+
+ whenever(webView.width).thenReturn(100)
+ whenever(webView.height).thenReturn(200)
+
+ engineView.session = mock()
+
+ whenever(engineView.session!!.webView).thenReturn(webView)
+
+ var thumbnail: Bitmap? = null
+
+ engineView.captureThumbnail {
+ thumbnail = it
+ }
+ verify(webView).draw(any())
+ assertNotNull(thumbnail)
+
+ engineView.session = null
+ engineView.captureThumbnail {
+ thumbnail = it
+ }
+
+ assertNull(thumbnail)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O], shadows = [PixelCopyShadow::class])
+ fun captureThumbnailOnPostO() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val engineView = SystemEngineView(activity)
+ val webView = mock<WebView>()
+ whenever(webView.width).thenReturn(100)
+ whenever(webView.height).thenReturn(200)
+
+ var thumbnail: Bitmap? = null
+
+ engineView.session = null
+ engineView.captureThumbnail {
+ thumbnail = it
+ }
+ assertNull(thumbnail)
+
+ engineView.session = mock()
+ whenever(engineView.session!!.webView).thenReturn(webView)
+
+ PixelCopyShadow.copyResult = PixelCopy.ERROR_UNKNOWN
+ engineView.captureThumbnail {
+ thumbnail = it
+ }
+ assertNull(thumbnail)
+
+ PixelCopyShadow.copyResult = PixelCopy.SUCCESS
+ engineView.captureThumbnail {
+ thumbnail = it
+ }
+ assertNotNull(thumbnail)
+ }
+
+ @Test
+ fun `calling onReceivedHttpAuthRequest must provide an Authentication PromptRequest`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+ engineView.render(engineSession)
+
+ val authHandler = mock<HttpAuthHandler>()
+ val host = "mozilla.org"
+ val realm = "realm"
+
+ engineSession.webView.webViewClient.onReceivedHttpAuthRequest(engineSession.webView, authHandler, host, realm)
+
+ val authRequest = request as PromptRequest.Authentication
+ assertTrue(request is PromptRequest.Authentication)
+
+ assertEquals(authRequest.title, "")
+
+ authRequest.onConfirm("u", "p")
+ verify(authHandler).proceed("u", "p")
+
+ authRequest.onDismiss()
+ verify(authHandler).cancel()
+ }
+
+ @Test
+ fun `calling onReceivedHttpAuthRequest with a null session must not provide an Authentication PromptRequest`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+ engineView.render(engineSession)
+
+ val authHandler = mock<HttpAuthHandler>()
+ engineView.session = null
+
+ engineSession.webView.webViewClient.onReceivedHttpAuthRequest(mock(), authHandler, "mozilla.org", "realm")
+
+ assertNull(request)
+ verify(authHandler).cancel()
+ }
+
+ @Test
+ fun `onReceivedHttpAuthRequest correctly handles realm`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+ engineView.render(engineSession)
+
+ val webView = engineSession.webView
+ val authHandler = mock<HttpAuthHandler>()
+ val host = "mozilla.org"
+
+ val longRealm = "Login with a user name of httpwatch and a different password each time"
+ webView.webViewClient.onReceivedHttpAuthRequest(webView, authHandler, host, longRealm)
+ assertTrue((request as PromptRequest.Authentication).message.endsWith("differen…”"))
+
+ val emptyRealm = ""
+ webView.webViewClient.onReceivedHttpAuthRequest(webView, authHandler, host, emptyRealm)
+ val noRealmMessageTail = testContext.getString(R.string.mozac_browser_engine_system_auth_no_realm_message).let {
+ it.substring(it.length - 10)
+ }
+ assertTrue((request as PromptRequest.Authentication).message.endsWith(noRealmMessageTail))
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ @Suppress("Deprecation")
+ fun `onReceivedHttpAuthRequest takes credentials from WebView`() {
+ val engineSession = SystemEngineSession(testContext)
+ val engineView = SystemEngineView(testContext)
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+
+ engineSession.webView = spy(engineSession.webView)
+ engineView.render(engineSession)
+
+ // use captor as getWebViewClient() is available only from Oreo
+ // and this test runs on N to not use WebViewDatabase
+ val captor = argumentCaptor<WebViewClient>()
+ verify(engineSession.webView).webViewClient = captor.capture()
+ val webViewClient = captor.value
+
+ val host = "mozilla.org"
+ val realm = "realm"
+ val userName = "user123"
+ val password = "pass@123"
+
+ val validCredentials = arrayOf(userName, password)
+ whenever(engineSession.webView.getHttpAuthUsernamePassword(host, realm)).thenReturn(validCredentials)
+ webViewClient.onReceivedHttpAuthRequest(engineSession.webView, mock(), host, realm)
+ assertEquals((request as PromptRequest.Authentication).userName, userName)
+ assertEquals((request as PromptRequest.Authentication).password, password)
+
+ val nullCredentials = null
+ whenever(engineSession.webView.getHttpAuthUsernamePassword(host, realm)).thenReturn(nullCredentials)
+ webViewClient.onReceivedHttpAuthRequest(engineSession.webView, mock(), host, realm)
+ assertEquals((request as PromptRequest.Authentication).userName, "")
+ assertEquals((request as PromptRequest.Authentication).password, "")
+
+ val credentialsWithNulls = arrayOf<String?>(null, null)
+ whenever(engineSession.webView.getHttpAuthUsernamePassword(host, realm)).thenReturn(credentialsWithNulls)
+ webViewClient.onReceivedHttpAuthRequest(engineSession.webView, mock(), host, realm)
+ assertEquals((request as PromptRequest.Authentication).userName, "")
+ assertEquals((request as PromptRequest.Authentication).password, "")
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O])
+ fun `onReceivedHttpAuthRequest uses WebViewDatabase on Oreo+`() {
+ val engineSession = spy(SystemEngineSession(testContext))
+ val engineView = SystemEngineView(testContext)
+ var request: PromptRequest? = null
+
+ engineSession.register(
+ object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ request = promptRequest
+ }
+ },
+ )
+ engineView.render(engineSession)
+
+ val host = "mozilla.org"
+ val realm = "realm"
+ val userName = "userFromDB"
+ val password = "pass@123FromDB"
+ val webViewDatabase = mock<WebViewDatabase>()
+ whenever(webViewDatabase.getHttpAuthUsernamePassword(host, realm)).thenReturn(arrayOf(userName, password))
+ whenever(engineSession.webViewDatabase(testContext)).thenReturn(webViewDatabase)
+
+ engineSession.webView.webViewClient.onReceivedHttpAuthRequest(engineSession.webView, mock(), host, realm)
+
+ val authRequest = request as PromptRequest.Authentication
+ assertEquals(authRequest.userName, userName)
+ assertEquals(authRequest.password, password)
+ }
+
+ @Test
+ fun `GIVEN SystemEngineView WHEN getInputResultDetail is called THEN it returns the instance from webView`() {
+ val engineView = SystemEngineView(testContext)
+ val engineSession = SystemEngineSession(testContext)
+ val webView = spy(NestedWebView(testContext))
+ engineSession.webView = webView
+ engineView.render(engineSession)
+ val inputResult = InputResultDetail.newInstance()
+ doReturn(inputResult).`when`(webView).inputResultDetail
+
+ assertSame(inputResult, engineView.getInputResultDetail())
+ }
+
+ @Test
+ fun `GIVEN SystemEngineView WHEN getInputResultDetail is called THEN it returns a new default instance if not available from webView`() {
+ val engineView = spy(SystemEngineView(testContext))
+
+ val result = engineView.getInputResultDetail()
+
+ assertNotNull(result)
+ assertTrue(result.isTouchHandlingUnknown())
+ assertFalse(result.canScrollToLeft())
+ assertFalse(result.canScrollToTop())
+ assertFalse(result.canScrollToRight())
+ assertFalse(result.canScrollToBottom())
+ assertFalse(result.canOverscrollLeft())
+ assertFalse(result.canOverscrollTop())
+ assertFalse(result.canOverscrollRight())
+ assertFalse(result.canOverscrollBottom())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/ReversibleStringTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/ReversibleStringTest.kt
new file mode 100644
index 0000000000..e7c417be45
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/ReversibleStringTest.kt
@@ -0,0 +1,122 @@
+/* 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.browser.engine.system.matcher
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ReversibleStringTest {
+
+ @Test(expected = StringIndexOutOfBoundsException::class)
+ @Throws(StringIndexOutOfBoundsException::class)
+ fun outOfBounds() {
+ val fullStringRaw = "a"
+ val fullString = ReversibleString.create(fullStringRaw)
+ fullString.charAt(1)
+ }
+
+ @Test(expected = StringIndexOutOfBoundsException::class)
+ @Throws(StringIndexOutOfBoundsException::class)
+ fun outOfBoundsAfterSubstring() {
+ val fullStringRaw = "abcd"
+ val fullString = ReversibleString.create(fullStringRaw)
+
+ val substring = fullString.substring(3)
+ substring.charAt(1)
+ }
+
+ @Test(expected = StringIndexOutOfBoundsException::class)
+ @Throws(StringIndexOutOfBoundsException::class)
+ fun outOfBoundsSubstring() {
+ val fullStringRaw = "abcd"
+ val fullString = ReversibleString.create(fullStringRaw)
+ fullString.substring(5)
+ }
+
+ @Test(expected = StringIndexOutOfBoundsException::class)
+ @Throws(StringIndexOutOfBoundsException::class)
+ fun outOfBoundsSubstringNegative() {
+ val fullStringRaw = "abcd"
+ val fullString = ReversibleString.create(fullStringRaw)
+ fullString.substring(-1)
+ }
+
+ @Test(expected = StringIndexOutOfBoundsException::class)
+ @Throws(StringIndexOutOfBoundsException::class)
+ fun outOfBoundsAfterSubstringEmpty() {
+ val fullStringRaw = "abcd"
+ val fullString = ReversibleString.create(fullStringRaw)
+
+ val substring = fullString.substring(4)
+ substring.charAt(0)
+ }
+
+ @Test
+ fun substringLength() {
+ val fullStringRaw = "a"
+ val fullString = ReversibleString.create(fullStringRaw)
+
+ assertEquals("Length must match input string length", fullStringRaw.length, fullString.length())
+
+ val sameString = fullString.substring(0)
+ assertEquals("substring(0) should equal input String", fullStringRaw.length, sameString.length())
+ assertEquals("substring(0) should equal input String", fullStringRaw[0], sameString.charAt(0))
+
+ val emptyString = fullString.substring(1)
+ assertEquals("Empty substring should be empty", 0, emptyString.length())
+ }
+
+ @Test
+ fun forwardString() {
+ val fullStringRaw = "abcd"
+ val fullString = ReversibleString.create(fullStringRaw)
+
+ assertEquals("Length must match input string length", fullStringRaw.length, fullString.length())
+
+ for (i in 0 until fullStringRaw.length) {
+ assertEquals("Character doesn't match input string character", fullStringRaw[i], fullString.charAt(i))
+ }
+
+ val substringRaw = fullStringRaw.substring(2)
+ val substring = fullString.substring(2)
+
+ for (i in 0 until substringRaw.length) {
+ assertEquals("Character doesn't match input string character", substringRaw[i], substring.charAt(i))
+ }
+ }
+
+ @Test
+ fun reverseString() {
+ val fullUnreversedStringRaw = "abcd"
+ val fullStringRaw = StringBuffer(fullUnreversedStringRaw).reverse().toString()
+ val fullString = ReversibleString.create(fullUnreversedStringRaw).reverse()
+
+ assertEquals("Length must match input string length", fullStringRaw.length, fullString.length())
+
+ for (i in 0 until fullStringRaw.length) {
+ assertEquals("Character doesn't match input string character", fullStringRaw[i], fullString.charAt(i))
+ }
+
+ val substringRaw = fullStringRaw.substring(2)
+ val substring = fullString.substring(2)
+
+ for (i in 0 until substringRaw.length) {
+ assertEquals("Character doesn't match input string character", substringRaw[i], substring.charAt(i))
+ }
+ }
+
+ @Test
+ fun reverseReversedString() {
+ val fullUnreversedStringRaw = "abcd"
+ val fullStringRaw = StringBuffer(fullUnreversedStringRaw).toString()
+ val fullString = ReversibleString.create(fullUnreversedStringRaw).reverse().reverse()
+
+ assertEquals("Length must match input string length", fullStringRaw.length, fullString.length())
+
+ for (i in 0 until fullStringRaw.length) {
+ assertEquals("Character doesn't match input string character", fullStringRaw[i], fullString.charAt(i))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/SafelistTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/SafelistTest.kt
new file mode 100644
index 0000000000..0fdba4f16f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/SafelistTest.kt
@@ -0,0 +1,126 @@
+/* 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.browser.engine.system.matcher
+
+import android.util.JsonReader
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.StringReader
+
+@RunWith(AndroidJUnit4::class)
+class SafelistTest {
+
+ /**
+ * Test setup:
+ * mozilla.org: allow foo.com
+ * foo.mozilla.org: additionally allow bar.com
+ *
+ * Test:
+ * mozilla.org can only use foo.com, but foo.mozilla.org can use both foo.com and bar.com
+ */
+ @Test
+ fun safelist() {
+ val mozillaOrg = "mozilla.org"
+ val fooMozillaOrg = "foo.mozilla.org"
+ val fooCom = "foo.com"
+ val barCom = "bar.com"
+
+ val fooComTrie = Trie.createRootNode()
+ fooComTrie.put(fooCom.reverse())
+
+ val barComTrie = Trie.createRootNode()
+ barComTrie.put(barCom.reverse())
+
+ val safelist = Safelist()
+ safelist.put(mozillaOrg.reverse(), fooComTrie)
+ safelist.put(fooMozillaOrg.reverse(), barComTrie)
+
+ assertTrue(safelist.contains("http://$mozillaOrg", "http://$fooCom"))
+ assertFalse(safelist.contains("http://$mozillaOrg", "http://$barCom"))
+ assertTrue(safelist.contains("http://hello.$mozillaOrg", "http://$fooCom"))
+ assertFalse(safelist.contains("http://hello.$mozillaOrg", "http://$barCom"))
+ assertTrue(safelist.contains("http://$mozillaOrg/somewhere", "http://$fooCom/somewhereElse/bla/bla"))
+ assertFalse(safelist.contains("http://$mozillaOrg/another/page.html?u=a", "http://$barCom/hello"))
+
+ assertTrue(safelist.contains("http://$fooMozillaOrg", "http://$fooCom"))
+ assertTrue(safelist.contains("http://$fooMozillaOrg", "http://$barCom"))
+ assertTrue(safelist.contains("http://hello.$fooMozillaOrg", "http://$fooCom"))
+ assertTrue(safelist.contains("http://hello.$fooMozillaOrg", "http://$barCom"))
+ assertTrue(safelist.contains("http://$fooMozillaOrg/somewhere", "http://$fooCom/somewhereElse/bla/bla"))
+ assertTrue(safelist.contains("http://$fooMozillaOrg/another/page.html?u=a", "http://$barCom/hello"))
+
+ // Test some invalid inputs
+ assertFalse(safelist.contains("http://$barCom", "http://$barCom"))
+ assertFalse(safelist.contains("http://$barCom", "http://$mozillaOrg"))
+
+ // Check we don't safelist resources for data:
+ assertFalse(safelist.contains("data:text/html;stuff", "http://$fooCom/somewhereElse/bla/bla"))
+ }
+
+ @Test
+ fun safelistTrie() {
+ val safelist = Trie.createRootNode()
+ safelist.put("abc")
+
+ val trie = SafelistTrie.createRootNode()
+ trie.putSafelist("def", safelist)
+ Assert.assertNull(trie.findNode("abc"))
+
+ val foundSafelist = trie.findNode("def") as SafelistTrie
+ Assert.assertNotNull(foundSafelist)
+ Assert.assertNotNull(foundSafelist.safelist?.findNode("abc"))
+
+ try {
+ trie.putSafelist("def", safelist)
+ fail("Expected IllegalStateException")
+ } catch (e: IllegalStateException) { }
+ }
+
+ val SAFE_LIST_JSON = """{
+ "Host1": {
+ "properties": [
+ "host1.com",
+ "host1.de"
+ ],
+ "resources": [
+ "host1ads.com",
+ "host1ads.de"
+ ]
+ },
+ "Host2": {
+ "properties": [
+ "host2.com",
+ "host2.de"
+ ],
+ "resources": [
+ "host2ads.com",
+ "host2ads.de"
+ ]
+ }
+ }"""
+
+ @Test
+ fun fromJson() {
+ val safelist = Safelist.fromJson(JsonReader(StringReader(SAFE_LIST_JSON)))
+
+ assertTrue(safelist.contains("http://host1.com", "http://host1ads.com"))
+ assertTrue(safelist.contains("https://host1.com", "https://host1ads.de"))
+ assertTrue(safelist.contains("javascript://host1.de", "javascript://host1ads.com"))
+ assertTrue(safelist.contains("file://host1.de", "file://host1ads.de"))
+
+ assertTrue(safelist.contains("http://host2.com", "http://host2ads.com"))
+ assertTrue(safelist.contains("about://host2.com", "about://host2ads.de"))
+ assertTrue(safelist.contains("http://host2.de", "http://host2ads.com"))
+ assertTrue(safelist.contains("http://host2.de", "http://host2ads.de"))
+
+ assertFalse(safelist.contains("data://host2.de", "data://host2ads.de"))
+ assertFalse(safelist.contains("foo://host2.de", "foo://host2ads.de"))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/TrieTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/TrieTest.kt
new file mode 100644
index 0000000000..38b47761a5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/TrieTest.kt
@@ -0,0 +1,46 @@
+/* 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.browser.engine.system.matcher
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TrieTest {
+
+ @Test
+ fun findNode() {
+ val trie = Trie.createRootNode()
+
+ assertNull(trie.findNode("hello"))
+ val putNode = trie.put("hello")
+ val foundNode = trie.findNode("hello")
+ assertNotNull(putNode)
+ assertNotNull(foundNode)
+ assertEquals(putNode, foundNode)
+
+ // Substring matching doesn't happen (except for subdomains)
+ assertNull(trie.findNode("hell"))
+ assertNull(trie.findNode("hellop"))
+
+ // Ensure both old and new overlapping strings can still be found
+ trie.put("hellohello")
+ assertNotNull(trie.findNode("hello"))
+ assertNotNull(trie.findNode("hellohello"))
+ assertNull(trie.findNode("hell"))
+ assertNull(trie.findNode("hellop"))
+
+ // Domain and subdomain can be found
+ trie.put("foo.com".reverse())
+ assertNotNull(trie.findNode("foo.com".reverse()))
+ assertNotNull(trie.findNode("bar.foo.com".reverse()))
+ assertNull(trie.findNode("bar-foo.com".reverse()))
+ assertNull(trie.findNode("oo.com".reverse()))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/UrlMatcherTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/UrlMatcherTest.kt
new file mode 100644
index 0000000000..e01572e130
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/UrlMatcherTest.kt
@@ -0,0 +1,296 @@
+/* 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.browser.engine.system.matcher
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.system.matcher.UrlMatcher.Companion.ADVERTISING
+import mozilla.components.browser.engine.system.matcher.UrlMatcher.Companion.ANALYTICS
+import mozilla.components.browser.engine.system.matcher.UrlMatcher.Companion.CONTENT
+import mozilla.components.browser.engine.system.matcher.UrlMatcher.Companion.SOCIAL
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.io.StringReader
+import java.util.HashMap
+
+@RunWith(AndroidJUnit4::class)
+class UrlMatcherTest {
+
+ @Test
+ fun basicMatching() {
+ val matcher = UrlMatcher(arrayOf("bcd.random"))
+
+ assertTrue(matcher.matches("http://bcd.random/something", "http://mozilla.org").first)
+ assertTrue(matcher.matches("http://bcd.random", "http://mozilla.org").first)
+ assertTrue(matcher.matches("http://www.bcd.random", "http://mozilla.org").first)
+ assertTrue(matcher.matches("http://www.bcd.random/something", "http://mozilla.org").first)
+ assertTrue(matcher.matches("http://foobar.bcd.random", "http://mozilla.org").first)
+ assertTrue(matcher.matches("http://foobar.bcd.random/something", "http://mozilla.org").first)
+
+ assertFalse(matcher.matches("http://other.random", "http://mozilla.org").first)
+ assertFalse(matcher.matches("http://other.random/something", "http://mozilla.org").first)
+ assertFalse(matcher.matches("http://www.other.random", "http://mozilla.org").first)
+ assertFalse(matcher.matches("http://www.other.random/something", "http://mozilla.org").first)
+ assertFalse(matcher.matches("http://bcd.specific", "http://mozilla.org").first)
+ assertFalse(matcher.matches("http://bcd.specific/something", "http://mozilla.org").first)
+ assertFalse(matcher.matches("http://www.bcd.specific", "http://mozilla.org").first)
+ assertFalse(matcher.matches("http://www.bcd.specific/something", "http://mozilla.org").first)
+
+ assertFalse(matcher.matches("http://mozilla.org/resource", "data:text/html;stuff here").first)
+ assertTrue(matcher.matches("http://bcd.random/resource", "data:text/html;stuff here").first)
+ }
+
+ /**
+ * Tests that category enabling/disabling works correctly. We test this by creating
+ * 4 categories, each with only one domain. We then iterate over all permutations of categories,
+ * and test that only the expected domains are actually blocked.
+ */
+ @Test
+ fun enableDisableCategories() {
+ val categories = HashMap<String, Trie>()
+ val suppportedCategories = mutableSetOf<String>()
+ val enabledCategories = mutableSetOf<String>()
+ val categoryCount = 4
+
+ for (i in 0 until categoryCount) {
+ val trie = Trie.createRootNode()
+ trie.put("category$i.com".reverse())
+
+ val categoryName = "category$i"
+ categories[categoryName] = trie
+ enabledCategories.add(categoryName)
+ suppportedCategories.add(categoryName)
+ }
+
+ val matcher = UrlMatcher(suppportedCategories, enabledCategories, categories)
+
+ // We can test every permutation by iterating over every value of a 4-bit integer (each bit
+ // indicates whether a given category is enabled or disabled).
+ // N categories -> N bits == (2^N - 1) == '1111...'
+ // 4 categories -> 4 bits == 15 == 2^N-1 = '1111'
+ val allEnabledPattern = (1 shl categoryCount) - 1
+ for (categoryPattern in 0..allEnabledPattern) {
+ // Ensure all the correct categories enabled
+ for (currentCategory in 0 until categoryCount) {
+ val currentBit = 1 shl currentCategory
+ val enabled = currentBit and categoryPattern == currentBit
+ matcher.setCategoryEnabled("category$currentCategory", enabled)
+
+ // Make sure our category enabling code actually sets the correct
+ // values for a few known combinations (i.e. we're doing a test within the test)
+ if (categoryPattern == 0) {
+ assertFalse("All categories should be disabled for categorypattern==0", enabled)
+ } else if (categoryPattern == allEnabledPattern) {
+ assertTrue("All categories should be enabled for categorypattern=='111....'", enabled)
+ } else if (categoryPattern == Integer.parseInt("1100", 2)) {
+ if (currentCategory < 2) {
+ assertFalse("Categories 0/1 expected to be disabled", enabled)
+ } else {
+ assertTrue("Categories >= 2 expected to be enabled", enabled)
+ }
+ }
+ }
+
+ for (currentCategory in 0 until categoryCount) {
+ val currentBit = 1 shl currentCategory
+ val enabled = currentBit and categoryPattern == currentBit
+ val url = "http://category$currentCategory.com"
+ assertEquals(
+ "Incorrect category matched for combo=$categoryPattern url=$url",
+ enabled,
+ matcher.matches(url, "http://www.mozilla.org").first,
+ )
+ }
+ }
+ }
+
+ val BLOCK_LIST = """{
+ "license": "test-license",
+ "categories": {
+ "Advertising": [
+ {
+ "AdTest1": {
+ "http://www.adtest1.com/": [
+ "adtest1.com",
+ "adtest1.de"
+ ]
+ }
+ },
+ {
+ "AdTest2": {
+ "http://www.adtest2.com/": [
+ "adtest2.com"
+ ]
+ }
+ }
+ ],
+ "Analytics": [
+ {
+ "AnalyticsTest": {
+ "http://analyticsTest1.com/": [
+ "analyticsTest1.com",
+ "analyticsTest1.de"
+ ]
+ }
+ }
+ ],
+ "Content": [
+ {
+ "ContentTest1": {
+ "http://contenttest1.com/": [
+ "contenttest1.com"
+ ]
+ }
+ }
+ ],
+ "Social": [
+ {
+ "SocialTest1": {
+ "http://www.socialtest1.com/": [
+ "socialtest1.com",
+ "socialtest1.de"
+ ]
+ }
+ }
+ ],
+ "Legacy Disconnect": [
+ {
+ "Ignored1": {
+ "http://www.ignored1.com/": [
+ "ignored.de"
+ ]
+ }
+ }
+ ],
+ "Legacy Content": [
+ {
+ "Ignored2": {
+ "http://www.ignored2.com/": [
+ "ignored.de"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ """
+
+ val SAFE_LIST = """{
+ "SocialTest1": {
+ "properties": [
+ "www.socialtest1.com"
+ ],
+ "resources": [
+ "socialtest1.de"
+ ]
+ }
+ }"""
+
+ @Test
+ fun createMatcher() {
+ val matcher = UrlMatcher.createMatcher(
+ StringReader(BLOCK_LIST),
+ StringReader(SAFE_LIST),
+ )
+
+ // Check returns correct category
+ val (matchesAds, categoryAds) = matcher.matches("http://adtest1.com", "http://www.adtest1.com")
+
+ assertTrue(matchesAds)
+ assertEquals(categoryAds, ADVERTISING)
+
+ val (matchesAds2, categoryAd2) = matcher.matches("http://adtest1.de", "http://www.adtest1.com")
+
+ assertTrue(matchesAds2)
+ assertEquals(categoryAd2, ADVERTISING)
+
+ val (matchesSocial, categorySocial) = matcher.matches(
+ "http://socialtest1.com/",
+ "http://www.socialtest1.com/",
+ )
+
+ assertTrue(matchesSocial)
+ assertEquals(categorySocial, SOCIAL)
+
+ val (matchesContent, categoryContent) = matcher.matches(
+ "http://contenttest1.com/",
+ "http://www.contenttest1.com/",
+ )
+
+ assertTrue(matchesContent)
+ assertEquals(categoryContent, CONTENT)
+
+ val (matchesAnalytics, categoryAnalytics) = matcher.matches(
+ "http://analyticsTest1.com/",
+ "http://www.analyticsTest1.com/",
+ )
+
+ assertTrue(matchesAnalytics)
+ assertEquals(categoryAnalytics, ANALYTICS)
+
+ // Check that safe list worked
+ assertTrue(matcher.matches("http://socialtest1.com", "http://www.socialtest1.com").first)
+ assertFalse(matcher.matches("http://socialtest1.de", "http://www.socialtest1.com").first)
+
+ // Check ignored categories
+ assertFalse(matcher.matches("http://ignored1.de", "http://www.ignored1.com").first)
+ assertFalse(matcher.matches("http://ignored2.de", "http://www.ignored2.com").first)
+ }
+
+ @Test
+ fun isWebFont() {
+ assertFalse(UrlMatcher.isWebFont(mock()))
+ assertFalse(UrlMatcher.isWebFont(Uri.parse("mozilla.org")))
+ assertTrue(UrlMatcher.isWebFont(Uri.parse("/fonts/test.woff2")))
+ assertTrue(UrlMatcher.isWebFont(Uri.parse("/fonts/test.woff")))
+ assertTrue(UrlMatcher.isWebFont(Uri.parse("/fonts/test.eot")))
+ assertTrue(UrlMatcher.isWebFont(Uri.parse("/fonts/test.ttf")))
+ assertTrue(UrlMatcher.isWebFont(Uri.parse("/fonts/test.otf")))
+ }
+
+ @Test
+ fun setCategoriesEnabled() {
+ val matcher = spy(
+ UrlMatcher.createMatcher(
+ StringReader(BLOCK_LIST),
+ StringReader(SAFE_LIST),
+ setOf("Advertising", "Analytics"),
+ ),
+ )
+
+ matcher.setCategoriesEnabled(setOf("Advertising", "Analytics"))
+ verify(matcher, never()).setCategoryEnabled(any(), anyBoolean())
+
+ matcher.setCategoriesEnabled(setOf("Advertising", "Analytics", "Content"))
+ verify(matcher).setCategoryEnabled("Advertising", true)
+ verify(matcher).setCategoryEnabled("Analytics", true)
+ verify(matcher).setCategoryEnabled("Content", true)
+ }
+
+ @Test
+ fun webFontsNotBlockedByDefault() {
+ val matcher = UrlMatcher.createMatcher(
+ StringReader(BLOCK_LIST),
+ StringReader(SAFE_LIST),
+ setOf(UrlMatcher.ADVERTISING, UrlMatcher.ANALYTICS, UrlMatcher.SOCIAL, UrlMatcher.CONTENT),
+ )
+
+ assertFalse(
+ matcher.matches(
+ "http://mozilla.org/fonts/test.woff2",
+ "http://mozilla.org",
+ ).first,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequestTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequestTest.kt
new file mode 100644
index 0000000000..bed30887bb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequestTest.kt
@@ -0,0 +1,99 @@
+/* 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.browser.engine.system.permission
+
+import android.net.Uri
+import android.webkit.PermissionRequest
+import android.webkit.PermissionRequest.RESOURCE_AUDIO_CAPTURE
+import android.webkit.PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID
+import android.webkit.PermissionRequest.RESOURCE_VIDEO_CAPTURE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SystemPermissionRequestTest {
+
+ @Test
+ fun `uri is equal to native request origin`() {
+ val nativeRequest: PermissionRequest = mock()
+ whenever(nativeRequest.origin).thenReturn(Uri.parse("https://mozilla.org"))
+ whenever(nativeRequest.resources).thenReturn(emptyArray())
+ val request = SystemPermissionRequest(nativeRequest)
+ assertEquals(request.uri, "https://mozilla.org")
+ }
+
+ @Test
+ fun `resources are correctly mapped to permissions`() {
+ val nativeRequest: PermissionRequest = mock()
+ whenever(nativeRequest.origin).thenReturn(Uri.parse("https://mozilla.org"))
+ whenever(nativeRequest.resources).thenReturn(
+ arrayOf(
+ RESOURCE_AUDIO_CAPTURE,
+ RESOURCE_VIDEO_CAPTURE,
+ RESOURCE_PROTECTED_MEDIA_ID,
+ ),
+ )
+
+ val expected = listOf(
+ Permission.ContentAudioCapture(RESOURCE_AUDIO_CAPTURE),
+ Permission.ContentVideoCapture(RESOURCE_VIDEO_CAPTURE),
+ Permission.ContentProtectedMediaId(RESOURCE_PROTECTED_MEDIA_ID),
+ )
+ val request = SystemPermissionRequest(nativeRequest)
+ assertEquals(expected, request.permissions)
+ }
+
+ @Test
+ fun `reject denies native request`() {
+ val nativeRequest: PermissionRequest = mock()
+ whenever(nativeRequest.origin).thenReturn(Uri.parse("https://mozilla.org"))
+ whenever(nativeRequest.resources).thenReturn(emptyArray())
+
+ val request = SystemPermissionRequest(nativeRequest)
+ request.reject()
+ verify(nativeRequest).deny()
+ }
+
+ @Test
+ fun `grant permission to all native request resources`() {
+ val resources = arrayOf(
+ RESOURCE_AUDIO_CAPTURE,
+ RESOURCE_VIDEO_CAPTURE,
+ RESOURCE_PROTECTED_MEDIA_ID,
+ )
+
+ val nativeRequest: PermissionRequest = mock()
+ whenever(nativeRequest.origin).thenReturn(Uri.parse("https://mozilla.org"))
+ whenever(nativeRequest.resources).thenReturn(resources)
+
+ val request = SystemPermissionRequest(nativeRequest)
+ request.grant()
+ verify(nativeRequest).grant(eq(resources))
+ }
+
+ @Test
+ fun `grant permission to selected native request resources`() {
+ val resources = arrayOf(
+ RESOURCE_AUDIO_CAPTURE,
+ RESOURCE_VIDEO_CAPTURE,
+ RESOURCE_PROTECTED_MEDIA_ID,
+ )
+
+ val nativeRequest: PermissionRequest = mock()
+ whenever(nativeRequest.origin).thenReturn(Uri.parse("https://mozilla.org"))
+ whenever(nativeRequest.resources).thenReturn(resources)
+
+ val request = SystemPermissionRequest(nativeRequest)
+ request.grant(listOf(Permission.ContentAudioCapture(RESOURCE_AUDIO_CAPTURE)))
+ verify(nativeRequest).grant(eq(arrayOf(RESOURCE_AUDIO_CAPTURE)))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/window/SystemWindowRequestTest.kt b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/window/SystemWindowRequestTest.kt
new file mode 100644
index 0000000000..37efa78ace
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/window/SystemWindowRequestTest.kt
@@ -0,0 +1,76 @@
+/* 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.browser.engine.system.window
+
+import android.os.Message
+import android.webkit.WebSettings
+import android.webkit.WebView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.system.SystemEngineSession
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SystemWindowRequestTest {
+
+ @Test
+ fun `init request`() {
+ val curWebView = mock<WebView>()
+ val newWebView = mock<WebView>()
+ val newEngineSession = mock<SystemEngineSession>()
+ val request = SystemWindowRequest(curWebView, newEngineSession, newWebView, true, true)
+
+ assertTrue(request.openAsDialog)
+ assertTrue(request.triggeredByUser)
+ assertEquals("", request.url)
+ }
+
+ @Test
+ fun `prepare sets webview on engine session`() {
+ val curWebView = mock<WebView>()
+ val newWebView = mock<WebView>()
+ val settings = mock<WebSettings>()
+
+ whenever(curWebView.settings).thenReturn(settings)
+ whenever(newWebView.settings).thenReturn(settings)
+
+ val newEngineSession = SystemEngineSession(testContext)
+ val request = SystemWindowRequest(curWebView, newEngineSession, newWebView)
+
+ val engineSession = request.prepare() as SystemEngineSession
+ assertSame(newWebView, engineSession.webView)
+ }
+
+ @Test
+ fun `start sends message to target`() {
+ val curWebView = mock<WebView>()
+ val newWebView = mock<WebView>()
+ val resultMsg = mock<Message>()
+ val newEngineSession = mock<SystemEngineSession>()
+
+ SystemWindowRequest(curWebView, newEngineSession, newWebView, false, false).start()
+ verify(resultMsg, never()).sendToTarget()
+
+ SystemWindowRequest(curWebView, newEngineSession, newWebView, false, false, resultMsg).start()
+ verify(resultMsg, never()).sendToTarget()
+
+ resultMsg.obj = ""
+ SystemWindowRequest(curWebView, newEngineSession, newWebView, false, false, resultMsg).start()
+ verify(resultMsg, never()).sendToTarget()
+
+ resultMsg.obj = mock<WebView.WebViewTransport>()
+ SystemWindowRequest(curWebView, newEngineSession, newWebView, false, false, resultMsg).start()
+ verify(resultMsg, times(1)).sendToTarget()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/engine-system/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/engine-system/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/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/browser/engine-system/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/engine-system/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/engine-system/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/browser/errorpages/README.md b/mobile/android/android-components/components/browser/errorpages/README.md
new file mode 100644
index 0000000000..607819c39b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/README.md
@@ -0,0 +1,91 @@
+# [Android Components](../../../README.md) > Browser > Errorpages
+
+Responsive browser error pages for Android apps.
+
+## Usage
+
+### 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:browser-errorpages:{latest-version}"
+```
+### Quick Start
+
+If you have an `ErrorType` already at hand, and you want to generate an error page for it with the default template:
+
+```kotlin
+val errorType: ErrorType = ErrorType.Unknown
+ErrorPages.createErrorPage(context, errorType)
+
+// OR
+
+ErrorPages.createErrorPage(context, errorType, R.raw.custom_html, R.raw.custom_css)
+```
+
+If you want to use your own custom HTML template, make sure that you have the following attributes within percentage values (`%`) added to your document so that they can be populated by the engine:
+- `pageTitle` - Title of the page.
+- `css` - The location of where to place the CSS contents in the document.
+- `messageShort` - A one line description of the error message
+Gecko and System engines map their respective error codes to the `ErrorType` values.
+- `messageLong` - A more detailed message about the error that was seen.
+- `button` - Button text that can be clicked on. This is commonly used to reload the page.
+
+For example, here is an HTML error page that will have only a title, short message and some CSS:
+
+```html
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta name="viewport" content="width=device-width; user-scalable=false;" />
+ <title>%pageTitle%</title>
+ <style>%css%</style>
+ </head>
+ <body>
+ <div>
+ <h1 class="errorTitleText">%messageShort%</h1>
+ </div>
+ </body>
+</html>
+```
+
+Error Pages are also mostly used along with the `RequestInterceptor`, which can be added to an Engine's Settings:
+
+```kotlin
+val settings = DefaultSettings(
+ requestInterceptor = RequestInterceptor {
+ override fun onErrorRequest(
+ session: EngineSession,
+ errorType: ErrorType,
+ uri: String?
+ ): RequestInterceptor.ErrorResponse? =
+ RequestInterceptor.ErrorResponse(ErrorPages.createErrorPage(context, errorType))
+ }
+)
+GeckoEngine(settings)
+```
+
+See the `ErrorType` enum for the full list of supported error types.
+
+### Engine Support
+
+If you want to add support for another engine, you need to support the `RequestInterceptor` and have it invoked with an `ErrorType` based on the `EngineSession`'s' error code for that request:
+
+```kotlin
+class CustomEngineSession(val interceptor: RequestInterceptor) : EngineSession {
+ override onError(errorCode: Int, uri: String) {
+ val errorType = when (errorCode) {
+ 1..5 -> ErrorType.ERROR_SECURITY_SSL
+ else -> ErrorType.ERROR_OFFLINE
+ }
+ interceptor.onErrorRequest(session, errorType, uri)
+ }
+}
+```
+
+## 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/browser/errorpages/build.gradle b/mobile/android/android-components/components/browser/errorpages/build.gradle
new file mode 100644
index 0000000000..f6da5e7bd1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/build.gradle
@@ -0,0 +1,42 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.browser.errorpages'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_annotation
+
+ implementation project(':support-ktx')
+
+ implementation project(':ui-icons')
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/browser/errorpages/proguard-rules.pro b/mobile/android/android-components/components/browser/errorpages/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/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/browser/errorpages/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/errorpages/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/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/browser/errorpages/src/main/assets/errorPageScripts.js b/mobile/android/android-components/components/browser/errorpages/src/main/assets/errorPageScripts.js
new file mode 100644
index 0000000000..7836e30ac0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/assets/errorPageScripts.js
@@ -0,0 +1,122 @@
+/* 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/. */
+
+/**
+ * Handles the parsing of the ErrorPages URI and then passes them to injectValues
+ */
+function parseQuery(queryString) {
+ if (queryString[0] === '?') {
+ queryString = queryString.substr(1);
+ }
+ const query = Object.fromEntries(new URLSearchParams(queryString).entries());
+ injectValues(query)
+ updateShowSSL(query)
+ updateShowHSTS(query)
+};
+
+/**
+ * Updates the HTML elements based on the queryMap
+ */
+function injectValues(queryMap) {
+ const tryAgainButton = document.getElementById('errorTryAgain')
+ const continueHttpButton = document.getElementById("continueHttp")
+
+
+ // Go through each element and inject the values
+ document.title = queryMap.title
+ tryAgainButton.innerHTML = queryMap.button
+ continueHttpButton.innerHTML = queryMap.continueHttpButton
+ document.getElementById('errorTitleText').innerHTML = queryMap.title
+ document.getElementById('errorShortDesc').innerHTML = queryMap.description
+ document.getElementById('advancedButton').innerHTML = queryMap.badCertAdvanced
+ document.getElementById('badCertTechnicalInfo').innerHTML = queryMap.badCertTechInfo
+ document.getElementById('advancedPanelBackButton').innerHTML = queryMap.badCertGoBack
+ document.getElementById('advancedPanelAcceptButton').innerHTML = queryMap.badCertAcceptTemporary
+ document.getElementById('advancedPanelAcceptButton').s = queryMap.badCertAcceptTemporary
+
+ // If no image is passed in, remove the element so as not to leave an empty iframe
+ const errorImage = document.getElementById('errorImage');
+ if (!queryMap.image) {
+ errorImage.remove();
+ } else {
+ errorImage.src = "resource://android/assets/" + queryMap.image;
+ }
+
+ if (queryMap.showContinueHttp === "true") {
+ // On the "HTTPS-Only" error page "Try again" doesn't make sense since reloading the page
+ // will just show an error page again.
+ tryAgainButton.style.display = 'none';
+ } else {
+ continueHttpButton.style.display = 'none';
+ }
+}
+
+var advancedVisible = false;
+
+/**
+ * Used to show or hide the "advanced" button based on the validity of the SSL certificate
+ */
+function updateShowSSL(queryMap) {
+ /** @type {'true' | 'false'} */
+ const showSSL = queryMap.showSSL;
+ if (typeof document.addCertException === "undefined") {
+ document.getElementById('advancedButton').style.display='none';
+ } else {
+ if (showSSL === 'true') {
+ document.getElementById('advancedButton').style.display='block';
+ } else {
+ document.getElementById('advancedButton').style.display='none';
+ }
+ }
+}
+
+/**
+ * Used to show or hide the "accept" button based for the HSTS error page
+ */
+function updateShowHSTS(queryMap) {
+ const showHSTS = queryMap.showHSTS;
+ if (showHSTS === 'true') {
+ document.getElementById('advancedButton').style.display='block';
+ document.getElementById('advancedPanelAcceptButton').style.display='none';
+ }
+}
+
+/**
+ * Used to display information about the SSL certificate in `error_pages.html`
+ */
+function toggleAdvanced() {
+ if (advancedVisible) {
+ document.getElementById('badCertAdvancedPanel').style.display='none';
+ } else {
+ document.getElementById('badCertAdvancedPanel').style.display='block';
+ }
+ advancedVisible = !advancedVisible;
+}
+
+/**
+ * Used to bypass an SSL pages in `error_pages.html`
+ */
+async function acceptAndContinue(temporary) {
+ try {
+ await document.addCertException(temporary);
+ location.reload();
+ } catch (error) {
+ console.error("Unexpected error: " + error)
+ }
+}
+
+document.addEventListener('DOMContentLoaded', function () {
+ if (window.history.length == 1) {
+ document.getElementById('advancedPanelBackButton').style.display = 'none';
+ } else {
+ document.getElementById('advancedPanelBackButton').addEventListener('click', () => window.history.back());
+ }
+
+ document.getElementById('errorTryAgain').addEventListener('click', () => window.location.reload());
+ document.getElementById('advancedButton').addEventListener('click', toggleAdvanced);
+ document.getElementById('advancedPanelAcceptButton').addEventListener('click', () => acceptAndContinue(true));
+ document.getElementById('continueHttp').addEventListener('click', () => document.reloadWithHttpsOnlyException());
+});
+
+parseQuery(document.documentURI);
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/assets/error_page_js.html b/mobile/android/android-components/components/browser/errorpages/src/main/assets/error_page_js.html
new file mode 100644
index 0000000000..397e237303
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/assets/error_page_js.html
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html>
+<!-- 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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width; user-scalable=false;" />
+ <meta http-equiv="Content-Security-Policy" content="default-src resource:; object-src 'none'" />
+ <link rel="stylesheet" type="text/css" href="error_style.css">
+ </head>
+
+ <body id="errorPage" dir="auto">
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer">
+
+ <!-- Error Image -->
+ <iframe id="errorImage" src="" frameborder="0"></iframe>
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 id="errorTitleText"></h1>
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+ <div id="errorShortDesc"></div>
+ </div>
+
+ <!-- Retry Button -->
+ <button id="errorTryAgain"></button>
+
+ <!-- Advanced Button -->
+ <button id="advancedButton" class="buttonSecondary"></button>
+
+ <!-- "Continue to HTTP site" Button (For HTTPS-Only error page only) -->
+ <button id="continueHttp" class="buttonSecondary"></button>
+
+ <div id="advancedPanelContainer">
+ <div id="badCertAdvancedPanel" class="advanced-panel">
+ <p id="badCertTechnicalInfo"></p>
+ <div id="advancedPanelBackButtonContainer" class="advancedPanelButtonContainer">
+ <button id="advancedPanelBackButton"></button>
+ </div>
+ <div id="advancedPanelAcceptButtonContainer" class="advancedPanelButtonContainer">
+ <button id="advancedPanelAcceptButton" class="buttonSecondary"></button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </body>
+
+ <!-- Each consumer that uses a unique HTML error page must implement a parsing script-->
+ <script src="./errorPageScripts.js"></script>
+</html>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/assets/error_style.css b/mobile/android/android-components/components/browser/errorpages/src/main/assets/error_style.css
new file mode 100644
index 0000000000..85436f6035
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/assets/error_style.css
@@ -0,0 +1,165 @@
+/* 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/. */
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ --moz-vertical-spacing: 10px;
+ --moz-background-height: 32px;
+}
+
+body {
+ background-size: 64px var(--moz-background-height);
+ /* background-size: 64px 32px; */
+ background-repeat: repeat-x;
+
+ background-color: #363B40;
+ color: #FFFFFF;
+ padding: 0 20px;
+
+ font-weight: 300;
+ font-size: 13px;
+ -moz-text-size-adjust: none;
+ font-family: sans-serif;
+}
+
+ul {
+ /* Shove the list indicator so that its left aligned, but use outside so that text
+ * doesn't don't wrap the text around it */
+ padding: 0 1em;
+ margin: 0;
+ list-style: round outside none;
+}
+
+#errorShortDesc,
+li:not(:last-of-type) {
+ /* Margins between the li and buttons below it won't be collapsed. Remove the bottom margin here. */
+ margin: var(--moz-vertical-spacing) 0;
+}
+
+h1 {
+ margin: 0;
+ /* Since this has an underline, use padding for vertical spacing rather than margin */
+ padding: var(--moz-vertical-spacing) 0;
+ font-weight: 300;
+ border-bottom: 1px solid #e0e2e5;
+}
+
+h2 {
+ font-size: small;
+ padding: 0;
+ margin: var(--moz-vertical-spacing) 0;
+}
+
+p {
+ margin: var(--moz-vertical-spacing) 0;
+}
+
+button {
+ /* Force buttons to display: block here to try and enfoce collapsing margins */
+ display: block;
+ width: 100%;
+ border: none;
+ padding: 1rem;
+ font-family: sans-serif;
+ background-color: #00A4DC;
+ color: #FFFFFF;
+ font-weight: 300;
+ border-radius: 2px;
+ background-image: none;
+ margin: var(--moz-vertical-spacing) 0 0;
+}
+
+.buttonSecondary{
+ /* Force buttons to display: block here to try and enforce collapsing margins */
+ display: block;
+ width: 100%;
+ border: none;
+ padding: 1rem;
+ font-family: sans-serif;
+ background-color: rgba(249, 249, 250, 0.1);
+ color: #FFFFFF;
+ font-weight: 300;
+ border-radius: 2px;
+ background-image: none;
+ margin: var(--moz-vertical-spacing) 0 0;
+}
+
+#errorPageContainer {
+ /* If the page is greater than 550px center the content.
+ * This number should be kept in sync with the media query for tablets below */
+ max-width: 550px;
+ margin: 0 auto;
+ transform: translateY(var(--moz-background-height));
+ padding-bottom: var(--moz-vertical-spacing);
+
+ min-height: calc(100% - var(--moz-background-height) - var(--moz-vertical-spacing));
+ display: flex;
+ flex-direction: column;
+}
+
+/* On large screen devices (hopefully a 7+ inch tablet, we already center content (see #errorPageContainer above).
+ Apply tablet specific styles here */
+@media (min-width: 550px) {
+ button {
+ min-width: 160px;
+ width: auto;
+ }
+
+ /* If the tablet is tall as well, add some padding to make content feel a bit more centered */
+ @media (min-height: 550px) {
+ #errorPageContainer {
+ padding-top: 64px;
+ min-height: calc(100% - 64px);
+ }
+ }
+}
+
+.advancedPanelButtonContainer {
+ background-color: rgba(128, 128, 147, 0.1);
+ display: flex;
+ justify-content: center;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+#advancedPanelBackButtonContainer {
+ padding-bottom: 0;
+}
+
+#advancedPanelContainer {
+ width: 100%;
+ left: 0;
+}
+
+.advanced-panel {
+ display: none;
+ background-color: #202023;
+ border: 1px solid rgba(249, 249, 250, 0.2);
+ margin: 48px auto;
+ min-width: 13em;
+ max-width: 52em;
+}
+
+.button-container {
+ display: flex;
+ flex-flow: row;
+}
+
+#badCertTechnicalInfo {
+ margin: 0em 1em 1em;
+ overflow: auto;
+ white-space: pre-line;
+}
+
+#advancedButton {
+ display: none;
+}
+
+#badCertAdvancedPanel {
+ display: none;
+}
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/java/mozilla/components/browser/errorpages/ErrorPages.kt b/mobile/android/android-components/components/browser/errorpages/src/main/java/mozilla/components/browser/errorpages/ErrorPages.kt
new file mode 100644
index 0000000000..3101050a07
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/java/mozilla/components/browser/errorpages/ErrorPages.kt
@@ -0,0 +1,249 @@
+/* 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.browser.errorpages
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.annotation.StringRes
+import mozilla.components.support.ktx.android.content.appName
+import mozilla.components.support.ktx.kotlin.urlEncode
+import mozilla.components.ui.icons.R as iconsR
+
+object ErrorPages {
+
+ private const val HTML_RESOURCE_FILE = "error_page_js.html"
+
+ /**
+ * Provides an encoded URL for an error page. Supports displaying images
+ *
+ * @param titleOverride A function that can return an error page title for an error type. If not
+ * provided or if `null` is returned from the function then the default page title for this
+ * error type, provided by this component, will be used.
+ * @param descriptionOverride A function that can return an error page description text for an
+ * error type. If not provided or if `null` is returned from the function then the default
+ * description text for this error type, provided by this component, will be used.
+ */
+ @SuppressLint("StringFormatInvalid")
+ fun createUrlEncodedErrorPage(
+ context: Context,
+ errorType: ErrorType,
+ uri: String? = null,
+ htmlResource: String = HTML_RESOURCE_FILE,
+ titleOverride: (ErrorType) -> String? = { null },
+ descriptionOverride: (ErrorType) -> String? = { null },
+ ): String {
+ val title = titleOverride(errorType) ?: context.getString(errorType.titleRes)
+ val button = context.getString(errorType.refreshButtonRes)
+ val description = descriptionOverride(errorType) ?: context.getString(errorType.messageRes, uri)
+ val imageName = if (errorType.imageNameRes != null) context.getString(errorType.imageNameRes) + ".svg" else ""
+ val continueHttpButton = context.getString(R.string.mozac_browser_errorpages_httpsonly_button)
+ val badCertAdvanced = context.getString(R.string.mozac_browser_errorpages_security_bad_cert_advanced)
+ val badCertTechInfo = when (errorType) {
+ ErrorType.ERROR_SECURITY_BAD_CERT ->
+ context.getString(
+ R.string.mozac_browser_errorpages_security_bad_cert_techInfo,
+ context.appName,
+ uri.toString(),
+ )
+ ErrorType.ERROR_BAD_HSTS_CERT -> context.getString(
+ R.string.mozac_browser_errorpages_security_bad_hsts_cert_techInfo2,
+ uri.toString().trim('/'),
+ context.appName,
+ )
+ else -> ""
+ }
+
+ val badCertGoBack = context.getString(R.string.mozac_browser_errorpages_security_bad_cert_back)
+ val badCertAcceptTemporary = context.getString(
+ R.string.mozac_browser_errorpages_security_bad_cert_accept_temporary,
+ )
+
+ val showSSLAdvanced: String = when (errorType) {
+ ErrorType.ERROR_SECURITY_BAD_CERT -> true
+ else -> false
+ }.toString()
+
+ val showHSTSAdvanced: String = when (errorType) {
+ ErrorType.ERROR_BAD_HSTS_CERT -> true
+ else -> false
+ }.toString()
+
+ val showContinueHttp: String = (errorType == ErrorType.ERROR_HTTPS_ONLY).toString()
+
+ /**
+ * Warning: When updating these params you WILL cause breaking changes that are undetected
+ * by consumers. Update the README accordingly.
+ */
+ var urlEncodedErrorPage = "resource://android/assets/$htmlResource?" +
+ "&title=${title.urlEncode()}" +
+ "&button=${button.urlEncode()}" +
+ "&description=${description.urlEncode()}" +
+ "&image=${imageName.urlEncode()}" +
+ "&showSSL=${showSSLAdvanced.urlEncode()}" +
+ "&showHSTS=${showHSTSAdvanced.urlEncode()}" +
+ "&badCertAdvanced=${badCertAdvanced.urlEncode()}" +
+ "&badCertTechInfo=${badCertTechInfo.urlEncode()}" +
+ "&badCertGoBack=${badCertGoBack.urlEncode()}" +
+ "&badCertAcceptTemporary=${badCertAcceptTemporary.urlEncode()}" +
+ "&showContinueHttp=${showContinueHttp.urlEncode()}" +
+ "&continueHttpButton=${continueHttpButton.urlEncode()}"
+
+ urlEncodedErrorPage = urlEncodedErrorPage
+ .replace("<ul>".urlEncode(), "<ul role=\"presentation\">".urlEncode())
+ return urlEncodedErrorPage
+ }
+}
+
+/**
+ * Enum containing all supported error types that we can display an error page for.
+ */
+enum class ErrorType(
+ @StringRes val titleRes: Int,
+ @StringRes val messageRes: Int,
+ @StringRes val refreshButtonRes: Int = R.string.mozac_browser_errorpages_page_refresh,
+ @StringRes val imageNameRes: Int? = null,
+) {
+ UNKNOWN(
+ R.string.mozac_browser_errorpages_generic_title,
+ R.string.mozac_browser_errorpages_generic_message,
+ ),
+ ERROR_SECURITY_SSL(
+ R.string.mozac_browser_errorpages_security_ssl_title,
+ R.string.mozac_browser_errorpages_security_ssl_message,
+ imageNameRes = iconsR.string.mozac_error_lock,
+ ),
+ ERROR_SECURITY_BAD_CERT(
+ R.string.mozac_browser_errorpages_security_bad_cert_title,
+ R.string.mozac_browser_errorpages_security_bad_cert_message,
+ imageNameRes = iconsR.string.mozac_error_lock,
+ ),
+ ERROR_NET_INTERRUPT(
+ R.string.mozac_browser_errorpages_net_interrupt_title,
+ R.string.mozac_browser_errorpages_net_interrupt_message,
+ imageNameRes = iconsR.string.mozac_error_eye_roll,
+ ),
+ ERROR_NET_TIMEOUT(
+ R.string.mozac_browser_errorpages_net_timeout_title,
+ R.string.mozac_browser_errorpages_net_timeout_message,
+ imageNameRes = iconsR.string.mozac_error_asleep,
+ ),
+ ERROR_CONNECTION_REFUSED(
+ R.string.mozac_browser_errorpages_connection_failure_title,
+ R.string.mozac_browser_errorpages_connection_failure_message,
+ imageNameRes = iconsR.string.mozac_error_confused,
+ ),
+ ERROR_UNKNOWN_SOCKET_TYPE(
+ R.string.mozac_browser_errorpages_unknown_socket_type_title,
+ R.string.mozac_browser_errorpages_unknown_socket_type_message,
+ imageNameRes = iconsR.string.mozac_error_confused,
+ ),
+ ERROR_REDIRECT_LOOP(
+ R.string.mozac_browser_errorpages_redirect_loop_title,
+ R.string.mozac_browser_errorpages_redirect_loop_message,
+ imageNameRes = iconsR.string.mozac_error_surprised,
+ ),
+ ERROR_OFFLINE(
+ R.string.mozac_browser_errorpages_offline_title,
+ R.string.mozac_browser_errorpages_offline_message,
+ imageNameRes = iconsR.string.mozac_error_no_internet,
+ ),
+ ERROR_PORT_BLOCKED(
+ R.string.mozac_browser_errorpages_port_blocked_title,
+ R.string.mozac_browser_errorpages_port_blocked_message,
+ imageNameRes = iconsR.string.mozac_error_lock,
+ ),
+ ERROR_NET_RESET(
+ R.string.mozac_browser_errorpages_net_reset_title,
+ R.string.mozac_browser_errorpages_net_reset_message,
+ imageNameRes = iconsR.string.mozac_error_unplugged,
+ ),
+ ERROR_UNSAFE_CONTENT_TYPE(
+ R.string.mozac_browser_errorpages_unsafe_content_type_title,
+ R.string.mozac_browser_errorpages_unsafe_content_type_message,
+ imageNameRes = iconsR.string.mozac_error_inspect,
+ ),
+ ERROR_CORRUPTED_CONTENT(
+ R.string.mozac_browser_errorpages_corrupted_content_title,
+ R.string.mozac_browser_errorpages_corrupted_content_message,
+ imageNameRes = iconsR.string.mozac_error_shred_file,
+ ),
+ ERROR_CONTENT_CRASHED(
+ R.string.mozac_browser_errorpages_content_crashed_title,
+ R.string.mozac_browser_errorpages_content_crashed_message,
+ imageNameRes = iconsR.string.mozac_error_surprised,
+ ),
+ ERROR_INVALID_CONTENT_ENCODING(
+ R.string.mozac_browser_errorpages_invalid_content_encoding_title,
+ R.string.mozac_browser_errorpages_invalid_content_encoding_message,
+ imageNameRes = iconsR.string.mozac_error_surprised,
+ ),
+ ERROR_UNKNOWN_HOST(
+ R.string.mozac_browser_errorpages_unknown_host_title,
+ R.string.mozac_browser_errorpages_unknown_host_message,
+ imageNameRes = iconsR.string.mozac_error_confused,
+ ),
+ ERROR_NO_INTERNET(
+ R.string.mozac_browser_errorpages_no_internet_title,
+ R.string.mozac_browser_errorpages_no_internet_message,
+ R.string.mozac_browser_errorpages_no_internet_refresh_button,
+ imageNameRes = iconsR.string.mozac_error_no_internet,
+ ),
+ ERROR_MALFORMED_URI(
+ R.string.mozac_browser_errorpages_malformed_uri_title,
+ R.string.mozac_browser_errorpages_malformed_uri_message,
+ imageNameRes = iconsR.string.mozac_error_confused,
+ ),
+ ERROR_UNKNOWN_PROTOCOL(
+ R.string.mozac_browser_errorpages_unknown_protocol_title,
+ R.string.mozac_browser_errorpages_unknown_protocol_message,
+ imageNameRes = iconsR.string.mozac_error_confused,
+ ),
+ ERROR_FILE_NOT_FOUND(
+ R.string.mozac_browser_errorpages_file_not_found_title,
+ R.string.mozac_browser_errorpages_file_not_found_message,
+ imageNameRes = iconsR.string.mozac_error_confused,
+ ),
+ ERROR_FILE_ACCESS_DENIED(
+ R.string.mozac_browser_errorpages_file_access_denied_title,
+ R.string.mozac_browser_errorpages_file_access_denied_message,
+ imageNameRes = iconsR.string.mozac_error_question_file,
+ ),
+ ERROR_PROXY_CONNECTION_REFUSED(
+ R.string.mozac_browser_errorpages_proxy_connection_refused_title,
+ R.string.mozac_browser_errorpages_proxy_connection_refused_message,
+ imageNameRes = iconsR.string.mozac_error_confused,
+ ),
+ ERROR_UNKNOWN_PROXY_HOST(
+ R.string.mozac_browser_errorpages_unknown_proxy_host_title,
+ R.string.mozac_browser_errorpages_unknown_proxy_host_message,
+ imageNameRes = iconsR.string.mozac_error_unplugged,
+ ),
+ ERROR_SAFEBROWSING_MALWARE_URI(
+ R.string.mozac_browser_errorpages_safe_browsing_malware_uri_title,
+ R.string.mozac_browser_errorpages_safe_browsing_malware_uri_message,
+ ),
+ ERROR_SAFEBROWSING_UNWANTED_URI(
+ R.string.mozac_browser_errorpages_safe_browsing_unwanted_uri_title,
+ R.string.mozac_browser_errorpages_safe_browsing_unwanted_uri_message,
+ ),
+ ERROR_SAFEBROWSING_HARMFUL_URI(
+ R.string.mozac_browser_errorpages_safe_harmful_uri_title,
+ R.string.mozac_browser_errorpages_safe_harmful_uri_message,
+ ),
+ ERROR_SAFEBROWSING_PHISHING_URI(
+ R.string.mozac_browser_errorpages_safe_phishing_uri_title,
+ R.string.mozac_browser_errorpages_safe_phishing_uri_message,
+ ),
+ ERROR_HTTPS_ONLY(
+ R.string.mozac_browser_errorpages_httpsonly_title,
+ R.string.mozac_browser_errorpages_httpsonly_message,
+ imageNameRes = iconsR.string.mozac_error_lock,
+ ),
+ ERROR_BAD_HSTS_CERT(
+ R.string.mozac_browser_errorpages_security_bad_hsts_cert_title,
+ R.string.mozac_browser_errorpages_security_bad_hsts_cert_message,
+ imageNameRes = iconsR.string.mozac_error_lock,
+ ),
+}
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..d3b4e3344c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-am/strings.xml
@@ -0,0 +1,327 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">እንደገና ሞክር</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ጥያቄን ማጠናቀቅ አይቻልም</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>ስለዚህ ችግር ወይም ስህተት ተጨማሪ መረጃ በአሁኑ ጊዜ አይገኝም።</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">ደህንነቱ የተጠበቀ ግንኙነት አልተሳካም</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>ሊያዩት የሞከሩት ገጽ ሊታይ አይችልም ምክንያቱም የሚያቀርበው ውሂብ ትክክለኛነት ሊረጋገጥ አልቻለም።</li>
+ <li>እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">ደህንነቱ የተጠበቀ ግንኙነት አልተሳካም</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>ይህ በአገልጋዩ ውቅር ላይ ያጋጠመ ችግር ሊሆን ይችላል ወይም ደግሞ አገልጋዩን ለመምሰል የሚሞክር ሰው ሊሆን ይችላል።</li>
+ <li>ከዚህ አገልጋይ ጋር ከዚህ ቀደም በተሳካ ሁኔታ ከተገናኙ ስህተቱ ጊዜያዊ ሊሆን ይችላል እና ቆይተው እንደገና መሞከር ይችላሉ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">የላቀ…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>አንድ ሰው ድረ-ገፁን ለማስመሰል እየሞከረ ሊሆን ይችላል እና እርስዎ መቀጠል የለብዎትም።</label>
+ <br><br>
+ <label>ድረ-ገፆች ማንነታቸውን በእውቅና ማረጋገጫዎች ያረጋግጣሉ። %1$s <b>%2$s</b>ን አያምንም ምክንያቱም የእውቅና ማረጋገጫ ሰጪው ስለማይታወቅ፣ የእውቅና ማረጋገጫው በራሱ የተፈረመ ነው ወይም አገልጋዩ ትክክለኛ መካከለኛ የእውቅና ማረጋገጫዎችን እየላከ አይደለም።</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ተመለስ (የሚመከር)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">አደጋውን ይቀበሉ እና ይቀጥሉ</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">ይህ ድረ-ገፅ ደህንነቱ የተጠበቀ ግንኙነት ይፈልጋል።</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>ለማየት የሞከሩት ገጽ ሊታይ አይችልም ምክንያቱም ይህ ድረ-ገጽ ደህንነቱ የተጠበቀ ግንኙነት ስለሚያስፈልገው።</li>
+ <li>ጉዳዩ ከድረ-ገፁ ሊሆን ይችላል፣ እና እሱን ለመፍታት እርስዎ ምንም ማድረግ አይችሉም።</li>
+ <li>ስለ ችግሩ የድረ-ገፁን አስተዳዳሪ ማሳወቅ ይችላሉ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">የላቀ…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> HTTP Strict Transport Security (HSTS) የሚባል የደህንነት ፖሊሲ አለው፣ ይህ ማለት <b>%2$s</b> ደህንነቱ በተጠበቀ ሁኔታ ብቻ ነው መገናኘት የሚችለው። ይህንን ድረ-ገጽ ለመጎብኘት የተለየ ፈቃድ ማከል አይችሉም። </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">ወደኋላ ተመለስ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">ግንኙነቱ ተቋርጧል</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>አሳሹ በተሳካ ሁኔታ ተገናኝቷል፣ ነገር ግን መረጃ በሚተላለፍበት ጊዜ ግንኙነቱ ተቋርጧል። እባክዎ እንደገና ይሞክሩ።</p>
+ <ul>
+ <li>ድረ-ገጹ ለጊዜው የማይገኝ ወይም በጣም ስራ የበዛበት ሊሆን ይችላል። ከጥቂት ጊዜ በኋላ እንደገና ይሞክሩ።</li>
+ <li>ምንም ገጾችን መጫን ካልቻሉ የመሣሪያዎን ውሂብ ወይም የWi-Fi ግንኙነት ያረጋግጡ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">ግንኙነቱ ጊዜው አልፎበታል</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>የተጠየቀው ድረ-ገጽ ለግንኙነት ጥያቄ ምላሽ አልሰጠም እና አሳሹ ምላሽ መጠበቅ አቁሟል።</p>
+ <ul>
+ <li>አገልጋዩ ከፍተኛ ፍላጎት ወይም ጊዜያዊ መቋረጥ እያጋጠመው ሊሆን ይችላል? ቆይተው እንደገና ይሞክሩ።</li>
+ <li>ሌሎች ድረ-ገጾችን ማሰስ አይችሉም? የመሳሪያውን የአውታረ መረብ ግንኙነት ይፈትሹ።</li>
+ <li>የእርስዎ መሣሪያ ወይም አውታረ መረብ በፋየርዎል ወይም በፕሮክሲ የተጠበቀ ነው? ትክክል ያልሆኑ ቅንብሮች በድር አሰሳ ላይ ጣልቃ ሊገቡ ይችላሉ።</li>
+ <li>አሁንም ችግር አለ? ለእርዳታ የአውታረ መረብ አስተዳዳሪዎን ወይም የበይነመረብ አቅራቢዎን ያማክሩ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">መገናኘት አልተቻለም</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>ጣቢያው ለጊዜው የማይገኝ ወይም በጣም ስራ የበዛበት ሊሆን ይችላል። ከጥቂት ጊዜ በኋላ እንደገና ይሞክሩ።</li>
+ <li>ምንም ገጾችን መጫን ካልቻሉ የመሣሪያዎን ውሂብ ወይም የWi-Fi ግንኙነት ያረጋግጡ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">ከሰርቨር ያልተጠበቀ ምላሽ</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>ድረ-ገፁ ለኔትወርክ ጥያቄው ባልተጠበቀ መልኩ ምላሽ ሰጥቷል እና አሳሹ መቀጠል አልቻለም።</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ገጹ በትክክል እየተዘዋወረ አይደለም</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>አሳሹ የተጠየቀውን ንጥል ሰርስሮ ለማውጣት መሞከሩን አቁሟል። ድረ-ገጹ ጥያቄውን በማያጠናቅቅ መንገድ እየመራው ነው።</p>
+ <ul>
+ <li>በዚህ ድረ-ገጽ የሚፈለጉ ኩኪዎችን አሰናክለዋል ወይም አግደዋል?</li>
+ <li>የድረ-ገጹን ኩኪዎች መቀበል ችግሩን ካልፈታው፣ ምናልባት የእርስዎ መሣሪያ ሳይሆን የአገልጋይ ውቅር ችግር ነው።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ከመስመር ውጭ ሁነታ</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>አሳሹ የሚሰራው ከመስመር ውጭ በሆነው ሁነታ ነው እና ከተጠየቀው ንጥል ጋር መገናኘት አይችልም።</p>
+ <ul>
+ <li>መሣሪያው ከገባሪ አውታረ መረብ ጋር የተገናኘ ነው?</li>
+ <li>ወደ የመስመር ላይ ሁነታ ለመቀየር እና ገጹን እንደገና ለመጫን “እንደገና ሞክር”ን ይጫኑ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">ለደህንነት ሲባል ወደብ ተገድቧል</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>የተጠየቀው አድራሻ ወደብ (ምሣ. <q>mozilla.org:80</q> በmozilla.org ላይ ወደብ 80) በተለምዶ ከድር አሰሳ ውጭ ለ<em>ሌላ</em> ጥቅም ላይ ይውላል። ለእርስዎ ጥበቃ እና ደህንነት አሳሹ ጥያቄውን ሰርዟል።</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">ግንኙነቱ ዳግም ተጀምሯል</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>በግንኙነት ላይ ሲደራደሩ የአውታረ መረቡ ማገናኛ ተቋርጧል። እባክዎ እንደገና ይሞክሩ።</p>
+ <ul>
+ <li>ድረ-ገጹ ለጊዜው የማይገኝ ወይም በጣም ስራ የበዛበት ሊሆን ይችላል። ከጥቂት ጊዜ በኋላ እንደገና ይሞክሩ።</li>
+ <li>ምንም ገጾችን መጫን ካልቻሉ የመሣሪያዎን ውሂብ ወይም የWi-Fi ግንኙነት ያረጋግጡ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">ደህንነቱ ያልተጠበቀ የፋይል አይነት</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">የተበላሸ የይዘት ስህተት</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>ለማየት እየሞከሩት ያለው ገጽ ሊታይ አይችልም ምክንያቱም በመረጃ ስርጭቱ ላይ ስህተት ስለተገኘ።</p>
+ <ul>
+ <li>እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">ይዘት ተበላሽቷል</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>ለማየት እየሞከሩት ያለው ገጽ ሊታይ አይችልም ምክንያቱም በመረጃ ስርጭቱ ላይ ስህተት ስለተገኘ።</p>
+ <ul>
+ <li>እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">የይዘት መቀየር ስህተት</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>ለማየት እየሞከሩት ያለው ገጽ ልክ ያልሆነ ወይም የማይደገፍ የማመቅ ዘዴ ስለሚጠቀም ሊታይ አይችልም።</p>
+ <ul>
+ <li>እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">አድራሻ አልተገኘም</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>አሳሹ ለተጠቀሰው አድራሻ አስተናጋጅ አገልጋይ ማግኘት አልቻለም።</p>
+ <ul>
+ <li>ለትየባ ስህተቶች አድራሻውን ያረጋግጡ ለምሣሌ
+ <strong>ww</strong>.example.com
+ በ<strong>www</strong>.example.com ምትክ።</li>
+ <li>ምንም ገጾችን መጫን ካልቻሉ የመሣሪያዎን ውሂብ ወይም የWi-Fi ግንኙነት ያረጋግጡ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ምንም የበይነመረብ ግንኙነት የለም</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">የአውታረ መረብ ግንኙነትዎን ይፈትሹ ወይም ገጹን ከጥቂት ጊዜ በኋላ እንደገና ለመጫን ይሞክሩ።</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">እንደገና ጫን</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ልክ ያልሆነ አድራሻ</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>የቀረበው አድራሻ በሚታወቅ ቅርጸት አይደለም። እባክዎ ለስህተቶች የአካባቢ አሞሌን ያረጋግጡ እና እንደገና ይሞክሩ።</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">አድራሻው ልክ አይደለም</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>የድር አድራሻዎች ብዙውን ጊዜ እንደ <strong>http://www.example.com/</strong></li> ይጻፋሉ
+ <li>እዝባሮችን እየተጠቀሙ መሆንዎን ያረጋግጡ (ማለትም <strong>/</strong>)።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">ያልታወቀ ፕሮቶኮል</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>አድራሻው ፕሮቶኮልን ይገልጻል (ለምሳሌ <q>wxyz://</q>) አሳሹ ይህን ስለማያቀው በትክክል ከድረ-ገጹ ጋር መገናኘት አይችልም።</p>
+ <ul>
+ <li>መልቲሚዲያ ወይም ሌሎች የጽሑፍ ያልሆኑ አገልግሎቶችን ለማግኘት እየሞከሩ ነው? ለተጨማሪ መስፈርቶች ድረ-ገጹን ይፈትሹ።</li>
+ <li>አንዳንድ ፕሮቶኮሎች በአሳሹ ከመታወቃቸው በፊት የሶስተኛ ወገን ሶፍትዌር ወይም ቅጥያዎችን ሊፈልጉ ይችላሉ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ሰነዱ አልተገኘም</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>ንጥሉ ሊሰየም፣ ሊወገድ ወይም ወደ ሌላ ቦታ ሊዛወር ይችል ነበር?</li>
+ <li>በአድራሻው ውስጥ የፊደል ግድፈት፣ አጣጣል ወይም ሌላ የአጻጻፍ ስህተት አለ?</li>
+ <li>ለተጠየቀው ንጥል በቂ የመዳረሻ ፍቃድ አለዎት?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">የፋይሉ መዳረሻ ተከልክሏል</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>ተወግዷል፣ ተንቀሳቅሷል ወይም የፋይል ፍቃዶች መዳረሻን እየከለከሉ ሊሆን ይችላል።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ተኪ አገልጋይ ግንኙነትን ውድቅ አድርጓል</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>አሳሹ ተኪ አገልጋይ እንዲጠቀም ተዋቅሯል፣ ነገር ግን ተኪው ግንኙነትን ውድቅ አደረገ።</p>
+ <ul>
+ <li>የአሳሹ ተኪ ውቅር ትክክል ነው? ቅንብሮቹን ይፈትሹ እና እንደገና ይሞክሩ።</li>
+ <li>የተኪ አገልግሎቱ ከዚህ አውታረ መረብ ጋር መገናኘትን ይፈቅዳል?</li>
+ <li>አሁንም ችግር አለ? ለእርዳታ የአውታረ መረብ አስተዳዳሪዎን ወይም የበይነመረብ አቅራቢዎን ያማክሩ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ተኪ አገልጋይ አልተገኘም</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>አሳሹ ተኪ አገልጋይ እንዲጠቀም ተዋቅሯል፣ ነገር ግን ተኪው ሊገኝ አልቻለም።</p>
+ <ul>
+ <li>የአሳሹ ተኪ ውቅር ትክክል ነው? ቅንብሮቹን ይፈትሹ እና እንደገና ይሞክሩ።</li>
+ <li>መሣሪያው ከገባሪ አውታረ መረብ ጋር የተገናኘ ነው?</li>
+ <li>አሁንም ችግር አለ? ለእርዳታ የአውታረ መረብ አስተዳዳሪዎን ወይም የበይነመረብ አቅራቢዎን ያማክሩ።</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">ጎጂ ድረ-ገጽ ችግር</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>በ%1$s ያለው ድረ-ገፅ የጥቃት ድረ-ገፅ ተብሎ ሪፖርት ተደርጓል እና በእርስዎ የደህንነት ምርጫዎች መሠረት ታግዷል።</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">የማይፈለግ የድረ-ገፅ ጉዳይ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>በ%1$s ያለው ድረ-ገጽ ያልተፈለገ ሶፍትዌር እንደሚያቀርብ ሪፖርት ተደርጓል እና በእርስዎ የደህንነት ምርጫዎች መሰረት ታግዷል።</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ጎጂ የድረ-ገፅ ጉዳይ</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>በ%1$s ያለው ድረ-ገፅ ጎጂ ሊሆን የሚችል ድረ-ገፅ ተብሎ ሪፖርት ተደርጓል እና በእርስዎ የደህንነት ምርጫዎች መሠረት ታግዷል።</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">አታላይ የድረ-ገፅ ጉዳይ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>ይህ በ%1$s ላይ ያለው ድረ-ገጽ እንደ አታላይ ድረ-ገጽ ሪፖርት ተደርጓል እና በእርስዎ የደህንነት ምርጫዎች መሠረት ታግዷል።</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">ደህንነቱ የተጠበቀ ድረ-ገፅ የለም</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[ለተሻሻለ ደህንነት HTTPS-only ሁነታን አንቅተዋል፣ እና የ<em>%1$s</em> HTTPS ስሪት የለም።]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">ወደ HTTP ድረ-ገፅ ይቀጥሉ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..be8980c19f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-an/strings.xml
@@ -0,0 +1,247 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Tornar a intentar-lo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">No se puede completar la petición</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Actualment no i hai información adicional disponible pa este problema u error.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Connexión segura fallida</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>La pachina que yes intentando veyer no se puede amostrar perque no s’ha puesto verificar l’autenticidat d’os datos recibius.</li>
+ <li>Contacta con os propietarios d’o puesto web pa informar-les d’este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Connexión segura fallida</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Esto podría deber-se a un problema con a configuración d’o servidor, u podría estar belún intentando fer-se pasar per lo servidor.</li>
+ <li>Si ya t’hebas connectau dinantes a este servidor, la error podría estar temporal, y podrás tornar a intentar-lo mas tarde.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Abanzadas…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Belún podría estar intentando fer-se pasar per lo puesto y no habrías de continar. </label>
+ <br><br>
+ <label>Los puestos web preban la suya identidat per medio de certificaus. %1$s no confía en <b>%2$s</b> perque l’emisor d’o certificau ye desconoixiu, lo certificau ye autofirmado u lo servidor no ninvia los certificaus intermedios correctos.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Tornar enta zaga (recomendau)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Acceptar lo risgo y continar</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La connexión ha estau interrumpida</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Lo navegador s’ha connectau con exito, pero la connexión s’ha interrumpiu mientres se transferiba la información. Torna a intentar-lo.</p>
+ <ul>
+ <li>Lo puesto podría no estar disponible temporalment u estar masiau ocupau. Torna a intentar-lo en bells menutos.</li>
+ <li>Si no puetz cargar garra pachina, revisa la connexión wifi u de datos d’o tuyo dispositivo mobil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">S’ha pasau lo tiempo d’espera d’a connexión</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Lo puesto solicitau no respondió a una petición de connexión y lo navegador ha deixau d’asperar una respuesta.</p>
+ <ul>
+ <li>Podría estar experimentando lo servidor una alta demanda u un corte temporal? Torna a intentar-lo mas tarde.</li>
+ <li>No puetz navegar per atros puestos? Compreba la connexión de ret de l’equipo.</li>
+ <li>Lo tuyo ret u equipo ye protechiu per un firewall u un proxy? Una configuración incorrecta puede interferir con a navegación web.</li>
+ <li>Encara tiens problemas? Consulta con l’administrador de ret u furnidor d’Internet pa obtener asistencia tecnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">No se puede connectar</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Lo puesto podría no estar disponible temporalment u estar masiau ocupau. Torna a intentar-lo en bell minuto.</li>
+ <li>Si no puetz cargar garra pachina, revisa la connexión wifi u de datos d’o dispositivo mobil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respuesta inasperada d’o servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Lo puesto respondió a la solicitut de ret d’una forma inesperada y lo navegador no puede continar.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La pachina no ye reendrezando adequadament</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Lo navegador s’ha aturau mirando de recuperar l’elemento solicitau. Lo puesto ye reendrezando la solicitut d’una forma que nunca se va a completar.</p>
+ <ul>
+ <li>Tiens desactivadas u blocadas las cookies que ameneste este puesto?</li>
+ <li>Si acceptar las cookies d’o puesto no resuelte lo problema, ye probable que sía un problema de configuración d’o servidor y no de l’equipo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo sin connexión</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Lo navegador ye operando en modo sin connexión y no puede connectar-se con l’elemento solicitau.</p>
+ <ul>
+ <li>Ye connectau l’equipo a un ret activo?</li>
+ <li>Preta "Tornar a intentar-lo" pa pasar a lo modo con connexión y recargar la pachina.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Puerto restrinchiu per razons de seguridat</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>L’adreza solicitada especificaba un puerto (p. eix., <q>mozilla.org:80</q> pa lo puerto 80 de mozilla.org) que gosa usar-se pa propositos <em>distintos</em> a navegar per Internet. Lo navegador ha cancelau la solicitut pa la tuya protección y seguridat.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La connexión ha estau reiniciada</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Lo vinclo con o ret s’ha interrumpiu mientres se negociaba una connexión. Torna a intentar-lo.</p>
+ <ul>
+ <li>Lo puesto podría no estar disponible temporalment u estar masiau ocupau. Torna a intentar-lo en bell minuto.</li>
+ <li>Si no puetz cargar garra pachina, revisa la connexión wifi u de datos d’o dispositivo mobil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de fichero no seguro</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Mete-te en contacto con os propietarios d’o puesto web pa informar-les d’este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de conteniu malmeso</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>La pachina que yes intentando veyer no puede amostrar-se perque se detectó una error en a transmisión d’os datos.</p>
+ <ul>
+ <li>Mete-te en contacto con os propietarios d’o puesto web pa informar-les d’este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Conteniu blocau</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+<p>La pachina que yes intentando veyer no puede amostrar-se perque se detectó una error en a transmisión d’os datos.</p>
+ <ul>
+ <li>Mete-te en contacto con os propietarios d’o puesto web pa informar-les d’este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de codificación de conteniu</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>La pachina que yes mirando de veyer no puede amostrar-se perque usa una forma no valida u no admitida de compresión.</p>
+ <ul>
+ <li>Mete-te en contacto con os propietarios d’o puesto web pa informar-les d’este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">No se trobó l’adreza</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Lo navegador no podió trobar lo servidor pa l’adreza proporcionada.</p>
+ <ul>
+ <li>Compreba que l’adreza no contienga errors, per eixemplo, <strong>ww</strong>.eixemplo.com en cuenta de <strong>www</strong>.eixemplo.com.</li>
+ <li>Si no puetz cargar garra pachina, revisa la connexión wifi u de datos d’o dispositivo mobil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">No i hai connexión a Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifica la tuya connexión de ret u intenta tornar a cargar la pachina en uns momentos.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">L’adreza no ye valida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>L’adreza proporcionada no ye en un formato reconoixiu. Compreba si i hai errors en a barra d’adrezas y torna a intentar-lo.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">L’adreza no ye valida</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Las adrezas web gosan escribir-se asinas: <strong>http://www.ejemplo.com/</strong></li>
+ <li>Asegura-te d’estar usando barras inclinadas enta adebant (p. eix., <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo desconoixiu</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>L’adreza especifica un protocolo (p. eix., <q>wxyz://</q>) que lo navegador no reconoixe, asinas que lo navegador no puede connectar-se correctament con o puesto.</p>
+ <ul>
+ <li>Yes mirando d’acceder a conteniu multimedia u atros servicios que no son de texto? Compreba los requisitos adicionals d’o puesto.</li>
+ <li>Qualques protocolos pueden amenester software u plugins de tercers antes que lo navegador pueda reconoixer-los.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">No s’ha trobau lo fichero</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Ye posible que l’elemento s’haiga renombrau, eliminau u cambiau de rota?</li>
+ <li>I hai bella error d’ortografía, d’uso de mayusclas u de qualsequier atro tipo en l’adreza?</li>
+ <li>Tiens privilechios d’acceso suficients pa l’elemento solicitau?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">L’acceso a lo fichero ha estau denegau</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Puede haber-se eliminau u moviu, u los suyos permisos de fichero pueden estar impedindo l’acceso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Lo servidor proxy refusó la connexión</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Lo navegador ye configurau pa usar un servidor proxy, pero lo proxy refusó la connexión.</p>
+ <ul>
+ <li>Ye correcta la configuración de proxy d’o navegador? Compreba la configuración y torna a intentar-lo.</li>
+ <li>Permite lo servicio proxy connexions dende este ret?</li>
+ <li>Encara tiens problemas? Consulta con l’administrador de ret u furnidor d’Internet pa obtener asistencia tecnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">No se trobó lo servidor proxy</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Lo navegador ye configurau pa usar un servidor proxy, pero no se podió trobar lo servidor proxy.</p>
+ <ul>
+ <li>Ye correcta la configuración de proxy d’o navegador? Compreba la configuración y torna a intentar-lo.</li>
+ <li>Ye connectau l’equipo a un ret activo?</li>
+ <li>Encara tiens problemas? Consulta con l’administrador de ret u furnidor d’Internet pa obtener asistencia tecnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de puesto de malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Lo puesto en %1$s s’ha identificau como un puesto atacant y s’ha blocau, seguindo las tuyas preferencias de seguridat.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de puesto no deseyau</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Lo puesto en %1$s s’ha identificau como un puesto que ofreix software no deseyau y s’ha blocau, seguindo las tuyas preferencias de seguridat.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de puesto nocivo</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Lo puesto en %1$s s’ha identificau como un puesto potencialment nocivo y s’ha blocau, seguindo las tuyas preferencias de seguridat.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de puesto enganyoso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Lo puesto en %1$s s’ha identificau como un puesto potencialment nocivo y s’ha blocau, seguindo las tuyas preferencias de seguridat</p>]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ann/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ann/strings.xml
new file mode 100644
index 0000000000..8cc20b9499
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ann/strings.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Kpọk Sa</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Kpekọt Ìrọ Mbeek Ìsan̄a</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Kpunu ofifi etip òfolek ufialek mè ìre ǹlilọ yi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Ìkakọt ìtibi ìnin̄ me utelelek</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Akpọk okisa lek ijeen̄The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.</li>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Eyi môkọt ire ufialek mèlek onineen̄ òbeme-etip. Môkọt si ire enw okisa lek ifafiaan̄ irọ kubọk ọmọ ore achubọk òbeme-etip ya.</li>
+ <li>Ire owuulek irak itibi itet me lek òbeme-etip yi me mgbọ òraraka, ufialek yi ìbokup mgbidim mgbọ gaalek; owu môkọt igwu ikom inisa me mgbidim mgbọ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Òdọdọk…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Gwu kom (Mîrọ inye)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Chieek Unan ya mè Fo isi</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Mîkput ntibi-ntet ya</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Òwọlọ-etip îtibi itet inwọn, ire, mîgbugbana ntibi-ntet ya mgbọ îkiria etip.Soso kpọk sa.</p>
+ <ul>
+ <li>Môkọt ire akpatan̄ yaìkakup mgbọ keyi mè ìre ìkirọ owuwa ikwaan̄. Kpọk sa me mgbidim mgbọ.</li>
+ <li>Ire òkakọt ìchili akpọk geege, kpọ data okup me okwukwut kwun̄ mè ìyaka ire ntibi-ntet Wi-Fi kwun̄.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Okike mgbọ esun̄be inyi ntibi-ntet yi îraka</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Ere akpatan̄ ya edobe ìkanyi ifọọk ntibi-ntet eweekbe, òwọlọ-etip si îta ikukup iban ifọọk.</p>
+ <ul>
+ <li>Ìre môkọt ire owuwa iweweek okisi lek òbeme-etip? Kpọk sa ofifi mgbọ.</li>
+ <li>Ìre òkakọt ìwọlọ ere akpatan̄ yi? Kpọ ntibi-ntet eyi okwukwut kwun̄.</li>
+ <li>Ìre firewall sà ìre proxy okibem okwukwut kwun̄? Onineen̄ eyi ìkatatge môkọt igbugbana iwọwọlọ olik etip.</li>
+ <li>Owu gwa okikpọk ikaan̄ ufialek? Chichini ogwu àdìmin njin-etip kwun̄.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Ìkakọt ìtibi ìtet</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Môkọt ire ere akpatan̄ ìkakup me mgbidim mgbọ mè ìyaka ire ìkirọ owuwa ikwaan̄. Kpọk sa lek me mgbidim mgbọ.</li>
+ <li>Ire òkakọt ìchili akpọk geege, kpọ data òkup me okwukwut kwun̄ mè ìyaka ire Wi-Fi kwun̄.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Ifọọk eyi kpekpọ chieen̄ ònan̄a me òbeme-etip</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Ere akpatan̄ ya ìfọọk me oniin̄ kpekpọ chieen̄, eya orọ òwọlọ-etip ìkayaka ìkọt ìje ìfo isi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Akpọk ya ìkagwu ìsi ìtat</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Òwọlọ-etip îtele isasa lek me ibọbọkọ inu ya edobe. Ere akpatan̄ ya ìkigwu mbeek ya me otu oniin̄ kporọbe isan̄a.</p>
+ <ul>
+ <li>Ìre oniin̄ sà ìre ogban cookies eyi akpatan̄ yi okidobe?</li>
+ <li>Ire ichechieek cookies ìkarọ ufialek yi ita, môkọt ire onineen̄ òbeme-etip, ìkare okwukwut kwun̄.</li>
+ </ul>
+ ]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..a2595ef4b4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ar/strings.xml
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">أعِد المحاولة</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">تعذّر إكمال الطلب</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>لا يوجد حاليا المزيد من المعلومات حول هذه المشكلة أو الخطأ.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">فشل الاتصال الآمن</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul> <li>لا يمكن عرض الصفحة التي تحاول زيارتها لعدم إمكانية الاستيثاق من البيانات المستقبلة.</li> <li>من فضلك اتصل بمالكي الموقع لإعلامهم بهذه المشكلة.</li> </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">فشل الاتصال الآمن</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul> <li>قد يكون هذا بسبب مشكلة في إعدادات الخادوم، أو أنّ أحدًا يحاول انتحال هوية الخادوم</li> <li>إن كنت قد اتصلتَ بهذا الموقع بنجاح في الماضي، قد يكون هذا الخطأ مؤقتًا، لذا أعِد المحاولة في وقت لاحق.</li> </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">متقدم…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>هناك احتمال بمحاولة أحد الأشخاص انتحال هوية هذا الموقع، وبذلك عليك عدم المواصلة.</label>
+<br><br>
+<label>تثبت المواقع على الوِب هويّتها باستعمال الشهادات. لا يثق %1$s بالموقع <b>%2$s</b> لأن مُصدر شهادته غير معلوم، أو أن الشهادة موقعة ذاتيا، أو أن الخادوم لم يرسل الشهادات الوسيطة كما ينبغي.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">عُد للخلف (يُنصح به)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">أقبلُ المخاطرة فتابِع</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">يتطلب هذا الموقع اتصالاً آمنًا.</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">متقدم…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ يستخدم الموقع <label><b>%1$s</b> سياسة النقل الصارمة (HSTS)، ما يعني بأن <b>%2$s</b> لا يستطيع الاتصال به إلا بأمان. لا يمكنك إضافة استثناء لزيارة هذا الموقع.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">عُد للخلف</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">قُوطِع الاتصال</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>اتصل المتصفح بنجاح، لكن قطع الاتصال أثناء نقل البيانات. من فضلك أعد المحاولة.</p>
+<ul>
+<li>قد يكون الموقع متوقفًا مؤقتًا أو مشغولا جدًا. حاول ثانية بعد عدّة دقائق.</li>
+<li>إن لم تكن تستطيع تحميل أي صفحة، تحقق من اتصال المحمول بشبكة البيانات أو الشبكة اللاسلكية.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">انتهت مهلة الاتصال</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>لم يستجب الموقع المطلوب لطلب الاتصال، وتوقف المتصفح عن انتظار رده.</p>
+<ul>
+<li>ربما يواجه الخادوم طلبا متزايدا أو يعاني من انقطاع مؤقت؟ أعد المحاولة فيما بعد.</li>
+<li>ألا تستطيع تصفح المواقع الأخرى؟ راجع اتصال الجهاز بالشبكة.</li>
+<li>جهازك أو شبكتك محمية بجدار ناري أو وسيط؟ الإعدادات الخطأ قد تتعارض مع تصفح الوِب.</li>
+<li>أمازلت تواجه مشاكل؟ راجع مدير الشبكة أو مزود الخدمة للمساعدة</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">تعذّر الاتصال</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+<li>قد يكون الموقع غير متاح مؤقتًا أو مشغولًا جدًا. حاول مجددًا بعد قليل.</li>
+<li>إن لم تكن تستطيع تحميل أي صفحة، تحقق من اتصال المحمول بشبكة البيانات أو الشبكة اللاسلكية.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">استجابة غير متوقعة من الخادوم</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>استجاب الموقع لطلب الشبكة بطريقة غير متوقعة ولا يستطيع المتصفح المواصلة.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">لا تعيد الصفحة التوجيه بشكل سليم</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>توقف المتصفح عن محاولة جلب العنصر المطلوب. يعيد الموقع توجيه الطلب بصورة لن تتم أبدا.</p><ul><li>هل عطّلت أو حجبتَ الكعكات من هذا الموقع؟</li><li>إذا كان قبول كعكات هذا الموقع لا يحل مشكلتك، فهذه على الأرجح مشكلة في إعدادات الخادوم وليس حاسوبك.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">وضع بدون اتصال</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>المتصفح يعمل في وضع اللا اتصال ولا يستطيع تحميل العنصر المطلوب.</p>
+ <ul>
+ <li>هل الجهاز متّصل بشبكة نشِطة؟</li>
+ <li>انقر ”حاول مجددًا“ للتبديل إلى وضع الاتصال وإعادة تحميل الصفحة.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">المنفذ ممنوع لأسباب الأمان</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>حدَّد العنوان المطلوب منفذا (مثال <q>mozilla.org:80</q> من أجل المنفذ 80 على mozilla.org) يُستخدم عادة لأسباب <em>غير</em> تصفّح الوِب. ألغى المتصفّح الطّلب لحمايتك وأمنك.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">صُفِّر الاتصال</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>قُوطع الاتصال الشبكي أثناء التفاوض على الاتصال. من فضلك أعد المحاولة.</p>
+<ul>
+<li>قد يكون الموقع متوقفًا مؤقتًا أو مشغولا جدًا. حاول ثانية بعد عدّة دقائق.</li>
+<li>إذا كنت غير قادر على تحميل أي صفحة، افحص اتصال الحاسوب بالشبكة.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">نوع ملف غير آمن</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>من فضلك اتصل بمالكي الموقع لإعلامهم بهذه المشكلة.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">خطأ محتوى فاسد</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>تعذر عرض الصفحة التي تريد مشاهدتها بسبب خطأ أثناء نقل البيانات.</p><ul><li>الرجاء التواصل مع مالك الموقع لإبلاغه بهذه المشكلة.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">انهار المحتوى</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>تعذر عرض الصفحة التي تريد مشاهدتها بسبب خطأ أثناء نقل البيانات.</p><ul><li>الرجاء التواصل مع مالك الموقع لإبلاغه بهذه المشكلة.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">خطأ في ترميز المحتوى</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>الصفحة التي تحاول فتحها لا يمكن عرضها لأنها تستخدم صيغة ضغط غير سليمة أو غير مدعومة.</p><ul><li>الرجاء الاتصال أصحاب الموقع لإخبارهم بهذه المشكلة. </li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">العنوان غير موجود</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>لم يجد المتصفح الخادوم المستضيف للعنوان المعطى.</p>
+<ul>
+<li>هل قمت بخطأ في كتابة النطاق؟ (مثل <q><strong>ww</strong>.example.org</q> بدلا من <q><strong>www</strong>.example.org</q>)</li>
+<li>أمتأكد من وجود عنوان النطاق هذا؟ ربما قد يكون انتهى تسجيله.</li>
+<li>إن لم تكن تستطيع تحميل أي صفحة، تحقق من اتصال المحمول بشبكة البيانات أو الشبكة اللاسلكية.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">لا يوجد اتصال بالإنترنت</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">افحص اتصال الشبكة أو جرّب إعادة تحميل الصفحة بعد قليل.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">أعِد التحميل</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">عنوان غير صحيح</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>تنسيق العنوان المُعطى غير معروف. من فضلك راجع شريط العنوان بحثا عن أخطاء ثم أعد المحاولة.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">العنوان غير صالح</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>تُكتب عناوين الوِب عادة على الشكل الآتي <strong>‪http://www.example.com/‬</strong>‏</li>
+ <li>تأكد أنك تستخدم الشرطة المائلة إلى اليمين (أي <strong>/</strong>).</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">بروتوكول مجهول</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>يحدد العنوان بروتوكولا (مثلا: <q>wxyz://</q>) لا يتعرفه المتصفح، لذا لا يستطيع المتصفح الاتصال بالموقع بشكل سليم.</p><ul><li>هل تحاول الوصول لمحتوى متعدد الوسائط أو خدمة غير نصية؟ راجع الموقع لمتطلبات إضافية.</li><li>قد تحتاج بعض البروتوكولات لبرامج إضافية أو ملحقات ليستطيع المتصفح التعرف عليها.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">الملف غير موجود</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul><li>ربما حُذف العنصر أو تغيّر اسمه أو مكانه؟</li><li>هل هناك خطأ إملائي أو مطبعي في العنوان؟</li><li>هل لديك صلاحيات كافية للنفاذ إلى العنصر المطلوب؟</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">مُنِع الوصول للملف</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul> <li>قد يكون حُذف أو نُقل أو أن صلاحيّات الملف تمنع الوصول إليه.</li> </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">رفض الخادوم الوسيط الاتصال</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>ضُبط المتصفّح ليستخدم خادوما وسيطا، لكن الوسيط رفض الاتصال.</p><ul><li>هل إعدادات الوسيط سليمة؟ تأكد من الإعدادات وأعد المحاولة.</li><li>هل يسمح الخادوم الوسيط بالاتصالات من هذه الشبكة؟</li><li>أما زلت تواجه المشاكل؟ راجع مدير الشبكة أو مزود الخدمة للمساعدة.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">لم يُعثر على خادوم وسيط</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>ضُبط المتصفّح ليستخدم خادوما وسيطا، لكن لم يوجد الوسيط.</p><ul><li>هل إعدادات الوسيط سليمة؟ تأكد من الإعدادات وأعد المحاولة.</li><li>هل الجهاز متصل بشبكة نشطة؟</li><li>أما زلت تواجه المشاكل؟ راجع مدير الشبكة أو مزود الخدمة للمساعدة.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">مشكلة موقع يحتوي برمجيات خبيثة</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>أُبلِغ عن أن الموقع %1$s موقع هجمات و حُجب بناء على تفضيلات الأمن.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">مشكلة موقع غير مرغوب فيه</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>أُبلِغ عن أن الموقع %1$s يقدم برمجيات غير مرغوب فيها و حُجب بناء على تفضيلات الأمن.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">مشكلة موقع ضار</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>أُبلِغ عن أن الموقع %1$s موقع خطر محتمل و حُجِبَ بناء على تفضيلات الأمن.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">مشكلة موقع مخادع</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>أُبلِغ عن أن الموقع %1$s موقع مخادع و حُجِبَ بناء على تفضيلات الأمن.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">خاصية الموقع الآمن غير متاحة</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[فعّلت وضع ”HTTPS فقط“ لتحصل على أفضل مستوى من الحماية، ولكن لا توجد نسخة HTTPS من الموقع <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">واصِل نحو نسخة HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..06e8ccabcf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ast/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Retentar</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Nun se pue completar la solicitú</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>La información adicional tocante a esti problema o error nun ta disponible.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">La conexón segura falló</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Nun se pue amosar la páxina que tentes de ver porque nun se pudo verificar l\'autenticidá de los datos recibíos.</li>
+ <li>Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">La conexón segura falló</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Esto podría ser un problema cola configuración del sirvidor o que daquién tea tentando de suplantalu.</li>
+ <li>Si nel pasáu te conectesti correutamente al sirvidor, ye posible que l\'error seya temporal polo qu\'anueva la páxina dempués.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Opciones avanzaes…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Daquién podría tar tentando de suplantar el sitiu ya nun habríes siguir.</label>
+ <br><br>
+ <label>Los sitios web demuestren la so identidá per certificaos. %1$s nun s\'enfota en <b>%2$s</b> porque\'l so emisor de certificaos ye desconocíu, el certificáu ta autorobláu o\'l sirvidor nun ta unviando los certificaos intermedios correutos.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Dir p\'atrás (aconséyase)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceptar el riesgu ya siguir</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Esti sitiu web rique una conexón segura.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Nun se pue amosar la páxina que tentes de ver porque esti sitiu web rique una conexón segura.</li>
+ <li>Ye mui probable que\'l problema seya del sitiu web ya nun puedas facer nada pa igualu.</li>
+ <li>Pues avisar a l\'alministración del sitiu web pa informar del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Opciones avanzaes…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> tien una política de seguranza llamada HSTS (HTTP Strict Transport Security), lo que significa que <b>%2$s</b> namás se pue conectar con seguranza al sitiu. Nun pues amestar nenguna esceición pa visitar esti sitiu. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Dir p\'atrás</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Torgóse la conexón</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>El restolador conectóse correutamente mas la conexón torgóse mentanto se tresfería información. Volvi tentalo.</p>
+ <ul>
+ <li>Seique\'l sitiu ta temporalmente non disponible o perocupáu. Volvi probar nun momentu.</li>
+ <li>Si nun yes a cargar nenguna páxina, comprueba la conexón Wi-Fi o móvil del preséu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Escosó\'l tiempu d\'espera de la conexón</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>El sitiu solicitáu nun respondió a una solicitú de conexón ya\'l restolador dexó d\'esperar una rempuesta.</p>
+ <ul>
+ <li>¿Pue ser que\'l sirvidor tea sufriendo una demanda alta o una cayida temporal? Volvi tentalo dempués.</li>
+ <li>¿Nun yes a restolar per otros sitios? Comprueba la conexón del preséu a la rede.</li>
+ <li>¿El preséu ta protexíu por un tornafuéu o proxy? Una configuración incorreuta pue afeutar al restolar de la web.</li>
+ <li>¿Sigues teniendo problemes? Consulta al to alministrador de redes o fornidor d\'internet pa consiguir más asistencia.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Nun ye posible conectase</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Seique\'l sitiu tea temporalmente non disponible o perocupáu. Volvi tentalo nun momentu.</li>
+ <li>Si nun yes a cargar nenguna páxina, comprueba la conexón Wi-Fi o móvil del preséu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Rempuesta inesperada del sirvidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>El sitiu respondió d\'una forma inesperada a la solicitú de rede ya\'l restolador nun pue siguir.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La páxina nun ta redirixendo afayadizamente</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>El restolador dexó de tentar de recuperar l\'elementu solicitáu. El sitiu ta redirixendo la solicitú d\'una forma qu\'enxamás nun va completase.</p>
+ <ul>
+ <li>¿Desactivesti o bloquiesti les cookies riquíes por esti sitiu?</li>
+ <li>Si l\'aceutación de les cookies del sitiu nun resuelve\'l problema, ye probable que seya un problema de la configuración del sirvidor y non del preséu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mou ensin conexón</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>El restolador ta trabayando nel mou ensin conexón y nun pue conectase al elementu solicitáu.</p>
+ <ul>
+ <li>¿El preséu ta conectáu a una rede activa?</li>
+ <li>Primi «Retentar» pa cambiar pal mou conexón y volver cargar la páxina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Torgóse un puertu por seguranza</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>La direición solicitada especificaba un puertu (por exemplu, <q>softastur.org:80</q> pal puertu 80 en softastur.org) que davezu tien otru propósitu <em>distintu</em> al de restolar la web. El restolador encaboxó la solicitú pa la to proteición y seguranza.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Reanicióse la conexón</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Torgóse l\'enllaz a la rede mentanto se negociaba una conexón. Volvi probar.</p>
+ <ul>
+ <li>Seique\'l sitiu tea temporalmente non disponible o perocupáu. Volvi tentalo nun momentu.</li>
+ <li>Si nun yes a cargar nenguna páxina, comprueba la conexón Wi-Fi o móvil del preséu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">El tipu de ficheru ye inseguru</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de conteníu toyíu</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Nun se pue amosar la páxina que tentes de ver porque se detectó un error na tresmisión de los datos.</p>
+ <ul>
+ <li>Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Cascó\'l conteníu</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Nun se pue amosar la páxina que tentes de ver porque se detectó un error na tresmisión de los datos.</p>
+ <ul>
+ <li>Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de la codificación del conteníu</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Nun se pue amosar la páxina que tentes de ver porque usa un tipu de compresión que nun ye válidu o compatible.</p>
+ <ul>
+ <li>Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Nun s\'atopó la direición</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>El restolador nun pudo atopar l\'agospiador de la direición apurrida.</p>
+ <ul>
+ <li>Comprueba que nun s\'introduxeren fallos al teclexar la direición:
+ <strong>ww</strong>.softastur.org en cuentes de
+ <strong>www</strong>.softastur.org.</li>
+ <li>Si nun yes a cargar nenguna páxina, comprueba la conexón Wi-Fi o móvil del preséu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nun hai conexón a internet</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Comprueba la conexón a la rede o tenta de recargar la páxina nun momentu.</string>
+
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">La direición nun ye válida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>La direición apurrida nun ta nun formatu reconocíu. Comprueba si hai fallos na barra de direiciones y volvi a tentalo.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">La direición nun ye válida</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Les direiciones web suelen escribise asina <strong>https://www.softastur.org/</strong></li>
+ <li>Asegúrate de que tas usando barres inclinaes a la derecha (ye dicir, <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Desconozse\'l protocolu</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>La direición especifica un protocolu (por exemplu, <q>wxyz://</q>) que\'l restolador nun reconoz, polo que nun se pue conectar afayadizamente al sitiu.</p>
+ <ul>
+ <li>¿Tas tentando d\'acceder a servicios multimedia o que nun son de testu? Comprueba\'l sitiu pa ver más requirimientos.</li>
+ <li>Dalgunos protocolos riquen software o plugins de terceros pa que\'l restolador pueda reconocelos.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Nun s\'atopó\'l ficheru</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>¿Pue ser que l\'elementu se renomare, quitare o moviere?</li>
+ <li>¿La direición tien dalguna falta u otru error tipográficu?</li>
+ <li>¿Tienes abondos permisos p\'acceder al elementu solicitáu?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Negóse l\'accesu al ficheru</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Seique se desaniciare, moviere o los permisos del ficheru eviten l\'accesu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">El sirvidor del proxy refugó la conexón</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>El restolador ta configuráu pa usar un sirvidor proxy mas esti últimu refugó la conexón.</p>
+ <ul>
+ <li>¿La configuración del proxy del restolador ye correuta? Comprueba los axustes y volvi tentalo.</li>
+ <li>¿El serviciu del proxy permite les conexones dende esta rede?</li>
+ <li>¿Sigues teniendo problemes? Consulta al to alministrador de redes o fornidor d\'internet pa más asistencia.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Nun s\'atopó\'l sirvidor del proxy</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>El restolador ta configuráu pa usar un sirvidor proxy mas nun se pudo atopar.</p>
+ <ul>
+ <li>¿La configuración del proxy del restolador ye correuta? Comprueba los axustes y volvi tentalo.</li>
+ <li>¿El preséu ta conectáu a una rede activa?</li>
+ <li>¿Sigues teniendo problemes? Consulta al to alministrador de redes o fornidor d\'internet pa más asistencia.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de sitiu con malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Informóse que %1$s ye un sitiu atacador y bloquióse según les tos preferencies de seguranza.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sitiu indeseable</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Informóse que\'l sitiu de %1$s ta sirviendo software indeseable y bloquióse según les tos preferencies de seguranza.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sitiu peligrosu</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Informóse que\'l sitiu de %1$s ye potencialmente peligrosu y bloquióse según les tos preferencies de seguranza.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de sitiu engañosu</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Informóse de que la páxina web de %1$s ye engañosa y bloquióse según les tos preferencies de seguranza.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">El sitiu seguru nun ta disponible</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Activesti\'l mou de namás HTTPS p\'ameyorar la seguranza, y la versión HTTPS de <em>%1$s</em> nun ta disponible.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Siguir col sitiu HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..a4a6d1154f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-az/strings.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Təkrar Yoxla</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">İstək yerinə yetirilə bilmir</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Bu xəta və problem haqqında ətraflı məlumat mövcud deyil</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Təhlükəsiz bağlantı qurula bilmədi</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Görmək istədiyiniz səhifə alınan məlumatlar təsdiqlənə bilmədi deyə göstərilə bilmir.</li>
+ <li>Lütfən sayt sahibləri ilə əlaqə saxlayın və bu problemdən xəbərdar edin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Təhlükəsiz bağlantı qurula bilmədi</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Bu, serverin tənzimləməsi ilə bağlı bir problemdən ya da başqa birinin sizi səhv serverə yönləndirməsindən qaynaqlana bilər.</li>
+ <li>Əvvəllər bu serverə uğurla bağlana bilirdinizsə problem müvəqqəti ola bilər və daha sonra təkrar yoxlaya bilərsiniz.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Təkmilləşmiş…</string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Geri get (Məsləhətlidir)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Riski qəbul et və davam et</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Bağlantı kəsildi</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Şəbəkə gözləmə müddəti bitdi</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Açmaq istədiyiniz sayt cavab vermədiyi üçün gözləmə ləğv edildi.</p>
+ <ul>
+ <li>Server çox yüklənib və ya müvəqqəti olaraq bir problemlə qarşılaşmış ola bilər. Daha sonra təkrar yoxlayın.</li>
+ <li>Digər səhifələrdə açılmaya bilər. Əgər elədirsə bağlantınızı yoxlayın.</li>
+ <li>Cihazınız ya da şəbəkəniz təhlükəsizlik divarı ilə qorunmuş ola bilər. Düzgün nizamlanmamış seçimlər internətə qoşulmağınıza problem yarada bilər.</li>
+ <li>Bütün yolları yoxlamağınıza baxmayaraq hələ də qoşula bilmirsinizsə, internet provayderinizlə və ya bağlantı idarəçinizlə əlaqə saxlayın.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Əlaqə cəhdi uğursuz oldu</string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Serverdən gözlənilməz cavab</string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Səhifə düzgün yönləndirilmir</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Oflayn rejim</string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Təhlükəsizlik səbəblərindən port məhdudlaşdırılıb</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Əlaqə sıfırlandı</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Etibarsız fayl növü</string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Zədəli məzmun xətası</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Məzmun çökdü</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Məzmun kodlama səhvi</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Ünvan tapılmadı</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">İnternet bağlantısı yoxdur</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Şəbəkə əlaqənizi yoxlayın və ya səhifəni az sonra təkrar yeniləməyi yoxlayın.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Yenilə</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Xətalı ünvan</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Daxil etdiyiniz ünvan bilinən formata uyğun deyil. Lütfən ünvan sətrinə baxıb mümkün səhvləri düzəltdikdən sonra yenidən yoxlayın.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Ünvan səhvdir</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Naməlum protokol</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fayl tapılmadı</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Faylın işlədilməsinə icazə verilmədi</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy server bağlantını rədd etdi</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy server tapılmadı</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Ziyanverici sayt problemi</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">İstənməyən sayt problemi</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Zərərli sayt problemi</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Aldadıcı sayt problemi</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..158dbeb60f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-azb/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">یئنی‌دن چالیش</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ایستک قورتارا بیلمیر</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>بو موشکول و خطا ایله ایلگیلی آرتیقراق بیلگی هله یوخدور.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">گوونلی باغلانتی اوغورسوز اولدو</string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">گوونلی باغلانتی اوغورسوز اولدو</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">قاباغجیل</string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">قاییت(توصیه اولونور)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">ریسکی قبول ائلییرک دوام ائدین</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">بو سایت گوونلی باغلانتی ایسته ییر.</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">قاباغجیل…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">دالیا گئت</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">باغلانتی کسیلدی</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">باغلانتی‌نین زامان قیسیتی قوتولدی</string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">باغلانانمیر</string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">سروردن گودولمه‌ین جواب گلدی</string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">صفحه دوزگون یؤنلندیریلمیر</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">آفلاین حالت</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">باغلانتی یئنی‌دن قورولدی</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">گوونسیز سند تیپی</string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">کورلانمیش ایچریک خطاسی</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">ایچریک سیندی</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">ایچریک کدلاما خطاسی</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">آدرس تاپیلمادی</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">اینترنت باغلانتی‌سی یوخدور</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">شبکه باغلانتیزی یوخلایین و یا آز سونرا صفحه‌نی یئنیله‌مه‌یی دئنه‌یین.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">یئنیله</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">گئچرسیز آدرس</string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">آدرس گئچرلی دئییل</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">تانینمایان پروتکل</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ایستنمیه‌ن سایت سورونو</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ضررلی سایت سورونو</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">آلداتان سایت سورونو</string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">گوونلی سایت موجود دئییل</string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP سایتینا دوام ائت</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..3a6a772252
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ban/strings.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Coba malih</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Pinunas Ragané Ten Prasida Puput</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Informasi tambahan indik pikobet puniki utawi galat sané mangkin durung kasedia.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Konéksi Aman Gagal</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Kaca sané ragané coba cingak tan prasida katampilang mawinan kaaslian data sané katerima tan prasida kavérifikasi.</li>
+ <li>Durus hubungin sané ngadruénang situs wéb antuk nganikain indik pikobet puniki.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Konéksi Aman Gagal</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Puniki minab pikobet sareng setélan peladén, utawi minab wénten sané nyoba nuutin peladén.</li>
+ <li>Pinaka ragané sampun maasil mahubung nuju peladén puniki sadurungné, galat minab abaan nyané ajebos, miwah ragané prasida nyoba malih nyanan.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Lanturan…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Minab wénten sané nyoba nyamar dados situs miwah ragané tan dados ngalanturang.</label>
+ <br><br>
+ <label>Situs wéb ngabuktiyang identitas dané saking sértifikat. %1$s tan kaparcaya <b>%2$s</b> krana pawedar sértifikat nyané nénten kauningin, sértifikat katanda tanganin ngaraga, utawi peladén nénten ngirimang sértifikat parantara sané patut.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Mawali (Kanikayang)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Tampi Résiko miwah Lanturang</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Soroh Berkas Tan Aman</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Tanpa konéksi internét</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Muat malih</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protokol Tan Kauningin</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Berkas Tan Katemu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Aksés nuju berkas tan kalugra</string>
+
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Lanturang ka Situs HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..6bdee61552
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-be/strings.xml
@@ -0,0 +1,262 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Паспрабаваць зноў</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Немагчыма скончыць запыт</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ · <p>Дадатковыя звесткі пра гэтую праблему або памылку зараз недаступны.</p>
+ ·]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Няўдача бяспечнага злучэння</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Старонка, якую вы спрабуеце адкрыць, не можа быць паказана, бо сапраўднасць атрыманых звестак нельга пацвердзіць.</li>
+ <li>Калі ласка, паведаміце ўладальніку сайта пра гэтую праблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Няўдача бяспечнага злучэння</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Гэта можа быць праблемай налад сервера ці, магчыма,
+хтосьці спрабуе прыкінуцца гэтым серверам</li>
+ <li>Калі вы паспяхова злучаліся з гэтым серверам раней, памылка можа
+быць часовай, таму вы можаце паспрабаваць зноў пазней.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Дадаткова…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Хтось можа спрабаваць падмяніць гэты вэб-сайт. Вам лепш не працягваць.</label>
+ <br><br>
+ <label>Вэб-сайты пацвярджаюць сваю ідэнтычнасць з дапамогаю сертыфікатаў. %1$s не давярае <b>%2$s</b>, таму што выдавец яго сертыфіката нявызначаны, сертыфікат самападпісаны, або сервер не дае спраўных прамежкавых сертыфікатаў.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Вярнуцца (рэкамендуецца)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Прыняць рызыку і працягнуць</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Гэты вэб-сайт патрабуе бяспечнага злучэння.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Старонку, якую вы спрабуеце прагледзець, немагчыма паказаць, бо для гэтага вэб-сайта патрабуецца бяспечнае злучэнне.</li>
+ <li>Праблема, хутчэй за ўсё, звязана з вэб-сайтам, і вы нічога не можаце зрабіць, каб яе вырашыць.</li>
+ <li>Вы можаце паведаміць адміністратарам вэб-сайта аб праблеме.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Дадаткова…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> мае палітыку бяспекі, што называецца HTTP Strict Transport Security (HSTS), і гэта азначае, што <b>%2$s</b> можа звязвацца з ім толькі абароненым злучэннем. Вы не можаце дадаць выключэнне для наведвання гэтага сайта. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Вярнуцца</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Злучэнне перарвана</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Браўзер паспяхова падлучыўся, але злучэнне было перарвана падчас перадачы інфармацыі. Паспрабуйце, калі ласка, зноў.</p>
+ <ul>
+ <li>Магчыма, сайт часова недаступны ці перагружаны запытамі. Пачакайце некаторы час і паспрабуйце зноў.</li>
+ <li>Калі вы не можаце загрузіць ніводную старонку, праверце злучэнне вашай прылады з мабільнай або Wi-Fi сеткай.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Час чакання злучэння выйшаў</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Запатрабаваны сайт не адказаў на запыт злучэння і браўзер скончыў чакаць адказ.</p>
+ <ul>
+ <li>Ці не можа сервер быць перагружаным або часова спыненым? Паспрабуйце зноў пазней.</li>
+ <li>Вы не здольны аглядаць іншыя сайты? Праверце злучэнне прылады з сеткай.</li>
+ <li>Ваша прылада альбо сетка абараняецца фаерволам або проксі? Няправільныя налады могуць замінаць агляданню ў сеціве.</li>
+ <li>Дагэтуль маеце праблемы? Парайцеся з адмістратарам сеткі або дастаўшчыком паслугаў Інтэрнэту.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Нельга злучыцца</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Магчыма, сайт часова недаступны ці перагружаны запытамі. Пачакайце некаторы час і паспрабуйце зноў.</li>
+ <li>Калі вы не можаце загрузіць ніводную старонку, праверце злучэнне вашай прылады з мабільнай або Wi-Fi сеткай.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Нечаканы адказ сервера</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Сайт адказаў на сеткавы запыт нечаканым спосабам, таму браўзер не можа працягваць.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Старонка няправільна перанакіроўваецца</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Браўзер спыніў спробы атрымаць запатрабаваную адзінку. Сайт перанакіроўвае запыты да сябе спосабам, які ніколі не будзе завершаны.</p>
+ <ul>
+ <li>Магчыма вы забаранілі або блакавалі кукі, якія патрэбны гэтаму сайту?</li>
+ <li>Калі разблакаванне кук сайта не вырашае праблему, дык гэта хутчэй праблема налад сервера, а не вашай прылады.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Пазасеткавы рэжым</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Браўзер працуе ў пазасеткавым рэжыме і не можа злучыцца з запатрабаванай адзінкай.</p>
+ <ul>
+ <li>Прылада злучана з дзейнай сеткай?</li>
+ <li>Націсніце “Паспрабаваць зноў”, каб пераключыцца ў сеткавы рэжым і абнавіць старонку.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Порт абмежаваны дзеля бяспекі</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Запатрабаваны адрас прызначае порт (напрыклад, <q>mozilla.org:80</q> – порт 80 на mozilla.org) , які звычайна ўжываецца ў <em>іншых</em> мэтах, а не для аглядання ў Сеціве. Браўзер скасаваў гэты запыт дзеля вашай аховы і бяспекі.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Злучэнне скінута</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Сеткавае злучэнне абарвана падчас яго наладжвання. Паспрабуйце, калі ласка, зноў.</p>
+ <ul>
+ <li>Магчыма, сайт часова недаступны ці перагружаны запытамі. Пачакайце некаторы час і паспрабуйце зноў.</li>
+ <li>Калі вы не можаце загрузіць ніводную старонку, праверце злучэнне вашай прылады з мабільнай або Wi-Fi сеткай.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Небяспечны тып файла</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Паведамце, калі ласка, уладальнікам вэб-сайта пра гэтую праблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Памылка пашкоджанага змесціва</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Старонка, якую вы спрабуеце адкрыць, не можа быць паказана, бо выяўлена памылка перадачы дадзеных.</p>
+ <ul>
+ <li>Калі ласка, паведаміце ўладальніку сайта пра гэтую праблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Крах змесціва</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Старонка, якую вы спрабуеце адкрыць, не можа быць паказана, бо выяўлена памылка перадачы дадзеных.</p>
+ <ul>
+ <li>Калі ласка, паведаміце ўладальніку сайта пра гэтую праблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Памылка кадавання змесціва</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Старонку, якую вы спрабуеце пабачыць, немагчыма паказаць, бо яна выкарыстоўвае недапушчальную або непадтрымальную форму сціскання.</p>
+ <ul>
+ <li>Паведамце, калі ласка, уладальнікам вэб-сайта пра гэтую праблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Адрас не знойдзены</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Браўзер не змог знайсці хост-сервер па ўказаным адрасе.</p>
+ <ul>
+ <li>Праверце адрас на памылкі ўводу такія як
+ <strong>ww</strong>.example.com замест
+ <strong>www</strong>.example.com.</li>
+ <li>Калі вы не можаце загрузіць ніводную старонку, праверце злучэнне вашай прылады з мабільнай або Wi-Fi сеткай.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Няма інтэрнэт-злучэння</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Праверце падлучэнне да сеткі або паспрабуйце перазагрузіць старонку праз некаторы час.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Перачытаць</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Несапраўдны адрас</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Фармат дадзенага адраса не апазнаны. Праверце, калі ласка, ці няма памылак на паліцы месцазнаходжання і паспрабуйце зноў.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Несапраўдны адрас</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Адрас Сеціва звычайна пішацца, як <strong>http://www.example.com/</strong></li>
+ <li>Упэўніцеся, што вы ўжываеце простыя косыя рыскі (г.зн. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Невядомы пратакол</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Адрас вызначае пратакол (напрыклад: <q>wxyz://</q>), які не распазнаецца браўзерам, таму браўзер не можа злучыцца з сайтам належным чынам.</p>
+ <ul>
+ <li>Вы спрабуеце даступіцца да мультымедыйных або нетэкставых службаў? Праверце, ці існуюць на сайце дадатковыя патрабаванні.</li>
+ <li>Асобныя пратаколы могуць патрабаваць пабочныя праграмы або плагіны, каб браўзер мог распазнаваць іх.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Файл не знойдзены</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Ці не была адзінка перайменавана, выдалена або перамешчана?</li>
+ <li>Можа існуе нейкая памылка ў адрасе, як прапушчаная/лішняя літара або вялікая літара замест малой, ці інакшая?</li>
+ <li>Вы маеце дастатковыя дазволы для доступу да запатрабаванай адзінкі?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Доступ да файла забаронены</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Магчыма, што ён быў выдалены або перамешчаны, або дазволы на файл не даюць атрымаць да яго доступ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Проксі-сервер адмовіўся злучацца</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Браўзер наладжаны карыстацца проксі-серверам, але проксі адхіліў злучэнне.</p>
+ <ul>
+ <li>Ці налады проксі браўзера правільныя? Праверце налады і паспрабуйце зноў.</li>
+ <li>Ці дазваляе проксі-сервер падлучэнні з гэтай сеткі?</li>
+ <li>Дагэтуль маеце праблемы? Парайцеся з адміністратарам сеткі або пастаўшчыком паслуг Інтэрнэту.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Проксі-сервер не знойдзены</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Браўзер наладжаны карыстацца проксі-серверам, але проксі не знойдзены.</p>
+ <ul>
+ <li>Ці налады проксі браўзера правільныя? Праверце налады і паспрабуйце зноў.</li>
+ <li>Ці падключана прылада да дзейнай сеткі?</li>
+ <li>Дагэтуль маеце праблемы? Парайцеся з адміністратарам сеткі або пастаўшчыком паслуг Інтэрнэту.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Шкоднасны сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Сайт %1$s вядомы як нападнік і заблакаваны згодна з вашымі наладамі бяспекі.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Непажаданы сайт</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Сайт па адрасе %1$s вядомы як пляцоўка для непажаданых праграм, заблакаваны ў адпаведнасці з вашымі наладамі бяспекі.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Шкоднасны сайт</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Сайт па адрасе %1$s вядомы як патэнцыйна шкодны, заблакаваны ў адпаведнасці з вашымі наладамі бяспекі.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Падроблены сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Старонка сеціва па адрасе %1$s вядома як падманлівы сайт, заблакавана ў адпаведнасці з вашымі наладамі бяспекі.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Бяспечны сайт недаступны</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Вы ўключылі рэжым "толькі HTTPS" для лепшай бяспекі, а HTTPS-версія для <em>%1$s</em> недаступная.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Перайсці на HTTP-сайт</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..eab4b874d8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bg/strings.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Повторен опит</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Заявката не може да бъде завършена</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Допълнителна информация за този проблем или грешка в момента не е налична.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Неуспешно установяване на шифрована връзка</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul> <li>Страницата не може да бъде показана, защото достоверността на получените данни не може да бъде проверена.</li> <li>Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.</li> </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Неуспешно установяване на шифрована връзка</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul> <li>Това е или проблем с настройките на сървъра, или някой се опитва се представи за него.</li> <li>Ако и преди сте се свързвали успешно с него, има вероятност грешката да е временна и може да опитате по-късно.</li> </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Разширени…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Някой може би се опитва да подмени истинската страница и по-добре да не продължавате.</label>
+<br><br>
+<label>Уеб страниците доказват своята самоличност чрез сертификати. %1$s не се доверява на <b>%2$s</b>, защото издателят на сертификата е неизвестен, сертификатът е самоподписан или сървърът не изпраща верните посреднически сертификати.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Връщане назад (препоръчително)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Продължаване въпреки риска</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Страницата изисква защитена връзка.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Страницата не може да бъде показана, защото изисква защитена връзка.</li>
+ <li>Проблемът най-вероятно е в страницата и нищо не може да направите.</li>
+ <li>Може да уведомите администратора на страницата за него.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Разширени…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label> <b>%1$s</b> използва политика за сигурност наречена HTTP Strict Transport Security (HSTS), което означава, че <b>%2$s</b> може да използва само сигурни връзки. Не може да добавяте изключение при посещение на тази страница.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Назад</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Връзката е прекъсната</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Четецът осъществи връзка, но тя е прекъсната по време на прехвърляне на информация. Опитайте отново.</p>
+<ul>
+ <li>Страницата може да е временно недостъпна или натоварена. Опитайте отново след малко.</li>
+ <li>Ако и други страници не се отварят - проверете връзката за данни или Wi-Fi.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Времето за изчакване на връзка изтече</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Страницата не отговаря на заявките за свързване и четецът спря да чака отговор.</p><ul><li>Дали сървърът не е претоварен, или изключен? Опитайте отново по-късно.</li><li>Можете ли да разглеждате други страници? Проверете връзката си с интернет.</li><li>Устройството намира ли се зад защитна стена, или мрежов посредник? Неправилните настройки могат да попречат на разглеждането.</li><li>Ако все още има проблем, обърнете се за помощ към мрежовия администратор или доставчика си на интернет.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Не е установена връзка</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Страницата може да е временно недостъпна или натоварена. Опитайте отново след малко.</li>
+ <li>Ако и други страници не се отварят - проверете връзката за данни или Wi-Fi.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Неочакван отговор от сървъра</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Страницата отговори на мрежово запитване по неочакван начин и четецът не може да продължи.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Страницата не пренасочва правилно</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Четецът спря опитите за зареждане. Страницата препраща заявката по начин, който никога не завършва.</p><ul><li>Да не би да сте изключили или спрели бисквитките на страницата?</li><li>Ако разрешаването на бисквитките не разреши проблема, вероятно става въпрос за грешка в настройките на сървъра, а не във вашето устройство.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Работа извън мрежата</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Четецът е настроен да работи в този режим и затова не може да се свърже.</p><ul><li>Работи ли мрежата към която е свързано устройството?</li><li>Изберете „Повторен опит“ за свързване към мрежата и презареждане на страницата.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Достъп до порта ограничен от съображения за сигурност</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Търсеният адрес съдържа порт (напр., <q>mozilla.org:80</q> за порт 80 на mozilla.org), който обикновено се използва за цели, <em>различни</em> от разглеждане. От съображения за сигурност мрежовият четец прекъсна заявката.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Връзката е прекъсната</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Връзка е прекъсната по време на свързване. Моля, опитайте отново.</p>
+<ul>
+ <li>Страницата може да е временно недостъпна или натоварена. Опитайте отново след малко.</li>
+ <li>Ако и други страници не се отварят - проверете връзката за данни или Wi-Fi.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Опасен вид файл</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul> <li>Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.</li> </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Грешка поради повредено съдържание</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Страницата не може да бъде показана поради грешка при прехвърляне на данните.</p><ul><li>Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Съдържанието се срина</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Страницата не може да бъде показана поради грешка при прехвърляне на данните.</p><ul><li>Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Грешка в кодировката на съдържанието</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Страницата не може да бъде показана, защото използва невалиден или неподдържан вид компресия.</p><ul><li>Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Адресът не е намерен</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Четецът не намира сървър на този адрес.</p>
+<ul>
+ <li>Проверете го за грешки при въвеждане, например <strong>ww</strong>.example.com вместо <strong>www</strong>.example.com.</li>
+ <li>Ако и други страници не се отварят - проверете връзката за данни или Wi-Fi.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Няма връзка с интернет</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Проверете мрежовата си свързаност или опитайте да презаредите страницата след малко.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Презареждане</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Недействителен адрес</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Адресът е в неразпознаваема форма. Моля, проверете адресната лента за грешки и опитайте отново.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Адресът е недействителен</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Адресите обикновено изглеждат така <strong>http://www.example.com/</strong></li>
+ <li>Уверете се, че използвате прави наклонени черти (т.е. <strong>/</strong>).</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Неизвестен протокол</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Адресът използва протокол (напр. <q>wxyz://</q>), който мрежовият четец не разпознава, така че не може да се установи връзка със страницата.</p><ul><li>Опитвате ли да се свържете с мултимедиен ресурс или друга нетекстова услуга? Проверете страницата за допълнителни изисквания.</li><li>Някои протоколи изискват софтуер или приставка от трета страна.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Файлът не е намерен</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Дали ресурсът не е преименуван, премахнат, или преместен?</li>
+ <li>Има ли грешка в правописа, регистъра на буквите, или друга техническа грешка?</li>
+ <li>Имате ли права за достъп до желания ресурс?</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Отказан достъп до файла</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Може да е премахнат, преместен или правата му да ограничават достъпа.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Мрежовият посредник отказа свързване</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Четецът е настроен да използва мрежов посредник, но той отказва връзката.</p><ul><li>Правилно ли е настроен? Проверете настройките и опитайте отново.</li><li>Мрежовият посредник разрешава ли свързване от вашата мрежа?</li><li>Ако все още има проблем, се обърнете за помощ към мрежовия администратор или своя доставчик на интернет.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Мрежовият посредник не е намерен</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Четецът е настроен да използва мрежов посредник, но той не може да бъде намерен.</p><ul><li>Правилно ли е настроен? Проверете настройките и опитайте отново.</li><li>Устройството свързано ли е към работеща мрежа?</li><li>Ако все още има проблем, се обърнете за помощ към мрежовия администратор или своя доставчик на интернет.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Враждебна страница</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Страницата %1$s е докладвана като враждебна и е блокирана спрямо вашите настройки за безопасност.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Нежелана страница</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Страницата %1$s е докладвана за сервиране на нежелан софтуер и е блокирана спрямо вашите настройки за безопасност.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Зловредна страница</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Страницата %1$s е докладвана като потенциално зловредна и е блокирана спрямо вашите настройки за безопасност.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Измамническа страница</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Страницата %1$s е докладвана като измамническа и е блокирана спрямо вашите настройки за безопасност.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Сигурната версия на сайта не е налична</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Включили сте режим „само HTTPS“ за подобрена сигурност, но версия на <em>%1$s</em> през HTTPS не е налична.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Продължаване с HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..63fdb3e5a7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bn/strings.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">আবার চেষ্টা করুন</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">অনুরোধ পূরণ করা সম্ভব নয়</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>এই সমস্যা বা ত্রুটি সম্পর্কে অতিরিক্ত তথ্য বর্তমানে নেই।</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">নিরাপদ সংযোগ স্থাপনে ব্যর্থ হয়েছে</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>আপনি যে পাতাটি দেখার চেষ্টা করছেন সেটি প্রদর্শিত হবে না কারণ প্রাপ্ত ডাটার সত্যতা যাচাই করা যায়নি।</li>
+ <li>এই সমস্যা সম্পর্কে তাদের অবহিত করতে ওয়েবসাইটের মালিকদের সাথে যোগাযোগ করুন।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">নিরাপদ সংযোগ স্থাপনে ব্যর্থ হয়েছে</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>সম্ভবত সার্ভারের কনফিগারেশনে সমস্যা হয়েছে অথবা কোনো ব্যক্তি এই সার্ভারের পরিচয় জাল করার চেষ্টা করছে।</li>
+ <li>আপনি যদি আগে এই সার্ভারের সাথে সফলভাবে সংযোগ স্থাপন করে থাকেন, তাহলে সম্ভবত কোনো অস্থায়ী কারণে এই সমস্যা দেখা দিয়েছে এবং কিছুক্ষণ পর আবার চেষ্টা করুন।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">উন্নতপর্যায়ের…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label> কেউ সাইটটি ছদ্মবেশ তৈরি করার চেষ্টা করছেন এবং আপনার চালিয়ে যাওয়া উচিত নয় </label>
+ <br> <br>
+ <label> ওয়েবসাইটগুলি সার্টিফিকেটের মাধ্যমে তাদের পরিচয় প্রমাণ করে। %1$s <b>%2$s</b> কে বিশ্বাস করে না কারণ এটির সার্টিফিকেটের জারিকারী অজানা, সার্টিফিকেটটি স্ব-স্বাক্ষরিত, অথবা সার্ভার সঠিক মধ্যবর্তী সার্টিফিকেট প্রেরণ করছে না </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ফিরে যান (প্রস্তাবিত)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">ঝুঁকি নিন এবং চালিয়ে যান</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">সংযোগ বিঘ্নিত হয়েছে</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">সংযোগের সময়সীমা উত্তীর্ণ হয়ে গেছে</string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">সংযোগ স্থাপন করতে ব্যর্থ</string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">সার্ভার থেকে অপ্রত্যাশিত প্রতিক্রিয়া দেখাচ্ছে</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>সাইটটি একটি নেটওয়ার্ক অনুরোধের অপ্রত্যাশিত উত্তর দিয়েছে যার ফলে ব্রাউজার এটি চালিয়ে যেতে পারছে না।</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">পাতাটি সঠিকভাবে পুনঃনির্দেশনা দিচ্ছে না</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">অফলাইন মোড</string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">নিরাপত্তাজনিত কারণে পোর্টটি সীমাবদ্ধ করা হয়েছে</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">সংযোগটি পুনস্থাপিত করা হয়েছে</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">অনিরাপদ শ্রেণীর ফাইল</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>অনুগ্রহ করে এই সমস্যা সম্পর্কে ওয়েব সাইট নির্মাতাদের অবগত করুন।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">ক্ষতিগ্রস্ত কন্টেন্টের ত্রুটি</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">কন্টেন্ট ক্র্যাশ করেছে</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">কন্টেন্টের এনকোডিং-এ ত্রুটি</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ঠিকানা পাওয়া যায়নি</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">কোনো ইন্টারনেট সংযোগ নেই</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">আপনার নেটওয়ার্কের সংযোগটি পরীক্ষা করুন বা কিছুক্ষণ পর পাতাটি আবার লোড করার চেষ্টা করুন।</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">পুনরায় লোড করুন</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">অকার্যকর ঠিকানা</string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ঠিকানাটি কার্যকর নয়</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">অপরিচিত প্রোটোকল</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ফাইল পাওয়া যায়নি</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ফাইলে প্রবেশাধিকার প্রত্যাখ্যাত হয়েছে</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">প্রক্সি সার্ভার সংযোগ প্রত্যাখ্যান করেছে</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">প্রক্সি সার্ভার পাওয়া যায়নি</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">ম্যালওয়্যার সাইটের সমস্যা</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">অযাচিত সাইট সমস্যা</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ক্ষতিকারক সাইটের সমস্যা</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">বিভ্রান্তিকর সাইটের সমস্যা</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..db13724f52
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-br/strings.xml
@@ -0,0 +1,324 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Klask en-dro</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Nʼhaller ket echuiñ an azgoulenn-mañ</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Titouroù ouzhpenn diwar-benn ar gudenn-mañ pe ar fazi-mañ nʼint ket hegerz evit poent.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Cʼhwitadenn war ar cʼhennaskañ diarvar</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Nʼhall ket bezañ diskouezet ar bajennad emaocʼh o klask gwelout rak nʼhall ket bezañ gwiriet dilested ar roadennoù bet degemeret.</li>
+ <li>Kit e darempred gant percʼhenned al lecʼhienn evit kas keloù dezho a-zivout ar gudenn-mañ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Cʼhwitadenn war ar cʼhennaskañ diarvar</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Marteze e vefe ur gudenn gant kefluniadur an dafariad, pe unan bennak o klask dreveziñ an dafariad.</li>
+ <li>Mard ocʼh bet kennasket ouzh an dafariad gant berzh a-raok na vefe eus ur fazi padennek moarvat, klaskit diwezhatocʼh.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Kempleshocʼh…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Gallout a rafe bezañ unan bennak o klask en em lakaat e plas al lecʼhienn ha ne rankfecʼh ket kendercʼhel ganti.</label>
+ <br><br>
+ <label>Dre testenioù e vez prouet o fivelezh gant al lecʼhiennoù. %1$s nʼeus ket fiziañs e <b>%2$s</b> dre ma nʼeo ket anavezet pourchaser an testeni, emsinet eo an testeni pe ne vez ket kaset an testenioù etre gant an dafariad.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Distreiñ (Erbedet)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Asantiñ ar riskl ha kendercʼhel</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ur c’hennask diogel a zo goulennet evit al lec’hienn-mañ.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>N’haller ket skrammañ ar bajenn a glaskit gwelet dre ma vez goulennet ur c’hennask diogel gant al lec’hienn.</li>
+ <li>Ar gudenn a zo gant al lec’hienn moarvat, ha n’eus netra a c’hallit ober.</li>
+ <li>Gallout a rit kemenn ardoer al lec’hienn ez eus ur gudenn.</li>
+ </ul>
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Kempleshoc’h…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> a zo gantañ ur politikerezh surentez anvet HTTP Strict Transport Security (HSTS), pezh a dalv ne c’hall <b>%2$s</b> kevreañ outañ nemet en un doare sur. Ne c’hallit ket ouzhpennañ un nemedenn evit gweladenniñ al lec’hienn-mañ. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Distreiñ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Harzet eo bet treuzkas ar roadennoù</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Kennasket eo ar merdeer gant berzh, met harzhet eo bet ar c’hennask pa oa o treuzkas titouroù. Klaskit en-dro mar plij.</p>
+ <ul>
+ <li>Gallout a rafe al lec’hienn-mañ bezañ sac’het pe ac’hubet. Klaskit en-dro benn nebeut.</li>
+ <li>Ma n’hoc’h ket gouest da gargañ pajenn ebet, gwiriekait ho kennask Wi-Fi pe roadennoù ho trevnad.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Diamzeret eo ar cʼhennask</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Nʼhe deus ket respontet al lecʼhienn azgoulennet dʼan azgoulenn kennaskañ ha ne cʼhortoz ket mui ar merdeer evit ar respont.</p>
+ <ul>
+ <li>Marteze eo soulgarget pe sacʼhet an dafariad? Klaskit diwezhatocʼh.</li>
+ <li>Nʼocʼh ket evit gweladenniñ lecʼhiennoù all? Gwiriañ ar cʼhennaskañ ouzh ar rouedad.</li>
+ <li>Ha gwarezet eo hocʼh urzhiataer gant un tanvoger pe ur proksi? Arventennoù fall a cʼhallfe gwallemellout gant ho merdeiñ.</li>
+ <li>Trubuilhoù cʼhoazh? Kit e darempred gant ardoer ho reizhiad pe ho pourchaser internet evit kaout skoazell.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Nʼhaller ket kennaskañ</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Al lecʼhienn a cʼhallfe bezañ dihegerz pe acʼhubet betek re e vefe evit poent. Klaskit adarre bremaik.</li>
+ <li>Ma nʼhocʼh ket evit kargañ pajennad ebet, gwiriit kennask ar roadennoù pe Wi-Fi ho trevnad.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respont dicʼhortoz a-berzh an dafariad</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Al lecʼhienn he deus respontet gant un doare dicʼhortoz da azgoulenn ar rouedad ha nʼeo ket evit kendercʼhel ganti ar merdeer.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Adheñchañ ar bajenn nʼeo ket dereat</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Ne glask ket mui ar merdeer kavout an ergorenn azgoulennet. Emañ al lecʼhienn ocʼh adheñchañ an azgoulenn ken na vo biken echuet.</p>
+ <ul>
+ <li>Ha diweredekaet pe cʼhennet hocʼh eus an toupinoù azgoulennet gant al lecʼhienn-mañ?</li>
+ <li>Ma nʼeo ket diskoulmet ar gudenn ur wech bet degemeret an toupinoù ez eus ur gudenn gant kefluniadur an dafariad ha nʼeus ket gant hocʼh urzhiataer.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mod ezlinenn</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Emañ ar merdeer o labourat gant e vod ezlinenn ha nʼhall ket kennaskañ ouzh an ergorenn bet goulennet.</p>
+ <ul>
+ <li>Ha kennasket eo an trevnad ouzh ur rouedad oberiant?</li>
+ <li>Pouezit war "Klask en-dro" evit trecʼhaoliñ etrezek ar mod enlinenn hag adkargañ ar bajennad.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Dindan strishadurioù emañ ar porzh-mañ</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Gant ar chomlecʼh goulennet ez eus bet erspizet ur porzh (d.l.e. <q>mozilla.org:80</q> evit ar porzh 80 war mozilla.org) arveret dre voaz evit palioù <em>all</em> eget ar merdeiñ war internet. Dilezet eo bet an azgoulenn gant ar merdeer evit ho kwarez hag ho tiogelroez.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ehanet eo bet ar cʼhennaskañ</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Harzet eo bet al liamm rouedad en ur glask krouiñ ur cʼhennask. Klaskit en-dro mar plij.</p>
+ <ul>
+ <li>Gallout a rafe al lecʼhienn-mañ bezañ sacʼhet pe acʼhubet. Klaskit en-dro benn nebeut.</li>
+ <li>Ma nʼhocʼh ket gouest da gargañ pajenn ebet, gwiriekait ho kennask Wi-Fi pe roadennoù.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Rizh restr arvarus</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Kit e darempred gant percʼhennerien al lecʼhienn evit kas keloù dezho a-zivout ar gudenn-mañ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Fazi a-fet endalcʼhad bet kontronet</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Ar bajenn emaocʼh o klask gweladenniñ nʼhall ket bezañ skrammet rak degouezhet ez eus bet ur fazi e-pad treuzkas ar roadennoù.</p>
+ <ul>
+ <li>Mar plij, kit e darempred gant percʼhenned al lecʼhienn a-benn kelaouiñ anezho eus ar gudenn-mañ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Sacʼhet eo an endalcʼhad</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Ar bajenn emaocʼh o klask gweladenniñ nʼhall ket bezañ skrammet rak degouezhet ez eus bet ur fazi e-pad treuzkas ar roadennoù.</p>
+ <ul>
+ <li>Mar plij, kit e darempred gant percʼhenned al lecʼhienn a-benn kelaouiñ anezho eus ar gudenn-mañ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Fazi enrinegañ an endalcʼhad</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Ar bajennad emaocʼh o klask gwelout nʼhall ket bezañ diskouezet rak un doare koazhañ anskor pe didalvoudek zo arveret ganti.</p>
+ <ul>
+ <li>Kit e darempred gant percʼhenned al lecʼhienn evit kas keloù dezho a-zivout ar gudenn-mañ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Nʼeo ket bet kavet ar chomlecʼh</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Nʼeo ket ar merdeer evit kavout an dafariad ostiz evit ar chomlecʼh roet.</p>
+ <ul>
+ <li>Gwiriekait ar chomlecʼh evit fazioù biziata evel
+ <strong>ww</strong>.skouer.bzh e plas
+ <strong>www</strong>.skouer.bzh.</li>
+ <li>Ma nʼhocʼh ket gouest da gargañ pajennoù, gwiriekait kennask roadennoù pe Wi-Fi ho trevnad.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Kennask internet ebet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Gwiriekait ho kennask ouzh ar rouedad pe klaskit adkargañ ar bajenn a-benn nebeud.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Adkargañ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Nʼeo ket talvoudek ar chomlecʼh</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Nʼeo ket anavezet mentrezh ar chomlecʼh pourvezet. Gwiriit barrenn al lecʼhiadur evit kavout ur fazi ha klaskit en-dro.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Nʼeo ket talvoudek ar chomlecʼh</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Peurvuiañ e vez skrivet ar chomlecʼhioù web evel-mañ <strong>http://www.example.com/</strong></li>
+ <li>Gwiriekait e vez implijet barennoù stouet ganeocʼh (d.l.e <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Komenad dianav</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Ur cʼhomenad (d.l.e. <q>wxyz://</q>) nad eo ket anavezet gant ar merdeer zo erspizet gant ar chomlecʼh, neuze nʼeo ket ar merdeer evit kennaskañ mat ouzh al lecʼhienn.</p>
+ <ul>
+ <li>Hag emaocʼh o klask tizhout liesvedia pe gwazerezhioù andestenn? Gwiriit war al lecʼhienn mar bez ezhomm paramantadurioù all.</li>
+ <li>Goulennet e vez meziantoù ouzhpenn pe enlugelladoù gant komenadoù zo a-raok ma vo gouest ar merdeer dʼo anavezout.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Restr dianav</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Gwiriañ ha dilecʼhiet eo bet, adanvet pe dilamet an ergorenn?</li>
+ <li>Gwiriañ anv ar chomlecʼh rak marteze ez eus fazioù pennlizherennoù pe fazioù skrivañ all?</li>
+ <li>Hag an aotreoù hocʼh eus evit haeziñ an ergorenn goulennet?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Nacʼhet eo bet haeziñ dʼar restr</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Gallout a ra bezañ dilamet, dilecʼhiet, pe nʼeus ket ar gwirioù a-zere evit an haeziñ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Dafariad ar proksi en deus nacʼhet ar cʼhennaskañ</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Kefluniet eo ar merdeer evit ober gant un dafariad proksi, met nacʼhet eo bet ar cʼhennaskañ gant ar proksi.</p>
+ <ul>
+ <li>Ha kefluniet mat eo proksi ar merdeer? Gwiriit an arventennoù ha klaskit en-dro.</li>
+ <li>Ha gwazerezh ar proksi a aotre kennaskadurioù diouzh ar rouedad-mañ?</li>
+ <li>Trubuilhoù cʼhoazh? Kit e darempred gant ardoer ar rouedad pe ho pourchaser internet a-benn kaout skoazell.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Nʼeo ket bet kavet an dafariad proksi</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Kefluniet eo ar merdeer evit ober gant un dafariad proksi, met nʼeo ket bet kavet ar proksi.</p>
+ <ul>
+ <li>Ha kefluniet mat eo proksi ar merdeer? Gwiriit an arventennoù ha klaskit en-dro.</li>
+ <li>Ha kennasket eo an trevnad ouzh ur rouedad oberiant?</li>
+ <li>Trubuilhoù cʼhoazh? Kit e darempred gant ardoer ar rouedad pe ho pourchaser internet a-benn kaout skoazell.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Kudenn lecʼhienn dagus</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Al lecʼhienn e %1$s zo bet marilhet evel ul lecʼhienn dagus ha harzet eo bet gant ho kwellvezioù diogelroez.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Kudenn lecʼhienn dicʼhoantaet</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Al lecʼhienn e %1$s zo bet marilhet evel ul lecʼhienn o kinnig meziantoù dicʼhoantaet ha harzet eo bet gant ho kwellvezioù diogelroez.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Fazi lecʼhienn noazus</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Al lecʼhienn e %1$s zo bet marilhet evel ul lecʼhienn a cʼhell bezañ noazus ha harzet eo bet gant ho kwellvezioù diogelroez.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Fazi lecʼhienn douellus</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Al lecʼhienn e %1$s zo bet marilhet evel ul lecʼhienn douellus ha harzet eo bet gant ho kwellvezioù diogelroez.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Lec’hienn diogel dihegerz</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Gweredekaet ho peus ar mod HTTPS-hepken evit muioc’h a ziogelroez, ha n’eus ket a stumm HTTPS eus <em>%1$s</em> .]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Kenderc’hel etrezek al lec’hienn HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..e149d06ded
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-bs/strings.xml
@@ -0,0 +1,255 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Pokušaj ponovo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Nije moguće dovršiti zahtjev</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Dodatne informacije o ovom problemu ili grešci trenutno nisu dostupne.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Sigurna veza nije uspjela</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Stranicu kojoj pokušavaš pristupiti nije moguće prikazati jer nije moguće provjeriti autentičnost primljenih podataka.</li>
+ <li>Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Sigurna veza nije uspjela</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Možda se radi o problemu s postavkama na serveru ili možda neko pokušava oponašati ovaj server.</li>
+ <li>Ako si se na ovaj server u prošlosti bez problema spajao/la, moguće je da se radi o privremenoj grešci, stoga pokušaj ponovo kasnije.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Napredno…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Netko možda pokušava lažno predstavljati web stranicu, stoga je bolje da ne nastaviš.</label>
+        <br>
+        <label> Web stranice dokazuju svoj identitet pomoću certifikata. %1$s ne vjeruje stranici na <b>%2$s</b>, jer je izdavač certifikata nepoznat, certifikat je samopotpisan ili server ne šalje ispravne međucertifikate. </label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Idi nazad (preporučeno)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Prihvati rizik i nastavi</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ova web stranica zahtijeva sigurnu vezu.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Stranica koju pokušavate pogledati se ne može prikazati jer ova web stranica zahtijeva sigurnu vezu.</li>
+ <li>Problem je najvjerovatnije na web stranici i ne možete ništa učiniti da ga riješite.</li>
+ <li>Možete obavijestiti administratora web stranice o problemu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Napredno…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ima sigurnosnu politiku koja se zove HTTP Strict Transport Security (HSTS), što znači da se <b>%2$s</b> može samo sigurno povezati na njega. Ne možete dodati izuzetak da posjetite ovu stranicu. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Idi nazad</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Veza je prekinuta</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Browser se uspješno povezao, ali veza je prekinuta tokom prenosa informacija. Pokušaj ponovo.</p>
+      <ul>
+        <li>Stranica je možda privremeno nedostupna ili prezauzeta. Pokušaj ponovo za nekoliko trenutaka.</li>
+        <li>Ako ne možeš učitati nijednu stranicu, provjeri podatke tvog uređaja ili Wi-Fi vezu.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Vezi je isteklo vrijeme</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Zatražena stranica nije odgovorila na zahtjev i browser je prestao čekati na odgovor.</p>
+ <ul>
+ <li>Možda je server opterećen velikom količinom zahtjeva ili je privremeno ostao bez napajanja? Pokušaj ponovo kasnije.</li>
+ <li>Možeš li pregledavati ostale stranice? Provjeri mrežnu vezu svog računara.</li>
+ <li>Jesu li tvoje računalo ili mreža zaštićeni vatrozidom ili proxyjem? Neispravne postavke mogu prouzročiti probleme prilikom pregledavanja weba.</li>
+ <li>Ukoliko još uvijek imaš probleme, za pomoć se obrati administratoru mreže ili serveru internetske usluge.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Povezivanje nije moguće</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Stranica je možda privremeno nedostupna ili preopterećena. Pokušaj ponovo malo kasnije.</li>
+ <li>Ako ne možeš učitati niti jednu stranicu, provjeri podatke svog uređaja ili Wi-Fi vezu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Neočekivani odgovor od servera</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Stranica je na mrežni zahtjev odgovorila na neočekivani način, zbog čega preglednik ne može nastaviti.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Stranica ne preusmjerava ispravno</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Preglednik je prestao dohvaćati zatražene stranice. Stranica preusmjerava zahtjev na takav način, da se on nikada ne može ispuniti.</p>
+ <ul>
+ <li>Jesu li kolačići za ovu stranicu deaktivirani ili blokirani?</li>
+ <li>Ako prihvaćanje kolačića stranice ne riješi problem, vrlo vjerojatno se radi o problemu s konfiguracijom poslužitelja, a ne tvog računala.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Izvanmrežni način rada</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Browser je u izvanmrežnom načinu rada i ne može se spojiti na traženu stavku.</p>
+ <ul>
+ <li>Je li uređaj spojen na aktivnu mrežu?</li>
+ <li>Klikni na „Pokušaj ponovo” za prebacivanje na mrežni način rada i ponovo učitaj stranicu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Priključak je iz sigurnosnih razloga ograničen</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Zatražena adresa ima definiran priključak (npr. <q>mozilla.org:80</q> za priključak 80 na mozilla.org) koji se inače koristi za <em>druge</em> radnje, a ne za pregledavanje weba. Browser je prekinuo zahtjev radi tvoje zaštite i sigurnosti.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Veza je resetovana</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Mrežna veza je prekinuta tijekom povezivanja. Pokušaj ponovo.</p>
+      <ul>
+        <li>Stranica je možda privremeno nedostupna ili prezauzeta. Pokušaj ponovo za nekoliko trenutaka.</li>
+        <li>Ako ne možeš učitati nijednu stranicu, provjeri podatke tvog uređaja ili Wi-Fi vezu.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Nesigurna vrsta datoteke</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Greška oštećenog sadržaja</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Stranicu kojoj pokušavaš pristupiti nije moguće prikazati zbog greške u prijenosu podataka.</p>
+ <ul>
+ <li>Obavijesti vlasnike web stranice o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Greška u sadržaju</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Stranicu kojoj pokušavaš pristupiti nije moguće prikazati zbog greške u prenosu podataka.</p>
+ <ul>
+ <li>Obavijesti vlasnike web stranice o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Greška u enkodiranju sadržaja</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Stranica koju pokušavaš vidjeti ne može biti prikazana jer koristi neispravni ili nepodržani oblik komprimiranja.</p>
+ <ul>
+ <li>Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresa nije pronađena</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Browser nije mogao pronaći server domaćina za navedenu adresu.</p>
+      <ul>
+        <li>Pazi da nemaš greške u tipkanju, kao što su
+          <strong>ww</strong>.primjer.ba umjesto
+          <strong>www</strong>.primjer.ba.</li>
+ <li>Ako ne možeš učitati nijednu stranicu, provjeri podatke svog uređaja ili Wi-Fi vezu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nema internet konekcije</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Provjeri mrežnu vezu ili pokušaj ponovo učitati stranicu za nekoliko trenutaka.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Učitaj ponovo</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Neispravna adresa</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Navedena adresa nije u poznatrom formatu. Molimo provjerite lokaciju sa greškama i pokušajte ponovo.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adresa je nevažeća</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Adrese web stranice se obično pišu u formatu poput <strong>http://www.example.com/</strong></li>
+ <li>Pazi na način pisanja kose crte (tj. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Nepoznat protokol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Adresa navodi protokol (npr., <q>wxyz://</q>) koji browser ne prepoznaje, pa se browser ne može pravilno povezati na stranicu.</p>
+ <ul>
+ <li>Da li pokušavate pristupiti multimediji ili drugim netekstualnim servisima? Provjerite stranicu za dodatne zahtjeve.</li>
+ <li>Neki protokoli mogu zahtijevati softver trećeg lica ili plugine prije nego ih browser može prepoznati.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fajl nije pronađen</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Da li je stavka preimenovana, uklonjena ili premještena?</li>
+ <li>Da li postoji pravopisna ili neka druga greška u adresi?</li>
+ <li>Da li imate potrebne dozvole za pristup zatraženoj stavci?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Pristup fajlu je odbijen</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Možda je uklonjen, premješten ili vam dozvole za fajl onemogućuju pristup.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy server je odbio povezivanje</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Browser je konfigurisan da koristi proxy server, ali je proxy odbio povezivanje.</p>
+ <ul>
+ <li>Da li je proxy konfiguracija browsera ispravna? Provjerite postavke i pokušajte ponovo.</li>
+ <li>Da li proxy usluga dozvoljava povezivanje sa ove mreže?</li>
+ <li>I dalje imate problem? Posavjetujte se sa vašim mrežnim administratorom ili Internet provajderom.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy server nije pronađen</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Browser je konfigurisan da koristi proxy server, ali je proxy odbio povezivanje.</p>
+ <ul>
+ <li>Da li je proxy konfiguracija browsera ispravna? Provjerite postavke i pokušajte ponovo.</li>
+ <li>Da li je uređan povezan na aktivnu mrežu?</li>
+ <li>I dalje imate problem? Posavjetujte se sa vašim mrežnim administratorom ili Internet provajderom.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problem sa malware stranicom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Stranica %1$s je prijavljena kao napadačka i blokirana je na osnovu vaših sigurnosnih postavki.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problem sa neželjenom stranicom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Stranica %1$s je prijavljena da servira neželjeni softver i blokirana je na osnovu vaših sigurnosnih postavki.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problem sa štetnom stranicom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Stranica %1$s je prijavljena kao potencijalno štetna i blokirana je na osnovu vaših sigurnosnih postavki.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problem sa obmanjujućom stranicom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Stranica %1$s je prijavljena kao obmanjujuća i blokirana je na osnovu vaših sigurnosnih postavki.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sigurna stranica nije dostupna</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Omogućili ste način rada samo za HTTPS radi poboljšane sigurnosti, a HTTPS verzija za <em>%1$s</em> nije dostupna.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Nastavite na HTTP stranicu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..b52dfb31b7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ca/strings.xml
@@ -0,0 +1,311 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Torna-ho a provar</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">No es pot completar la sol·licitud</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>No hi ha informació disponible actualment sobre aquest problema o error.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Ha fallat la connexió segura</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>La pàgina que esteu intentant veure no es pot mostrar perquè no s’ha pogut verificar l’autenticitat de les dades rebudes.</li>
+ <li>Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Ha fallat la connexió segura</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Això podria ser un problema amb la configuració del servidor, o bé podria ser que algú estigués intentant fer-se passar pel servidor.</li>
+ <li>Si us hi heu connectat sense cap problema alguna altra vegada, l’error podria ser temporal i podeu tornar-ho a provar més tard.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avançat…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>És possible que algú estigui intentant suplantar el lloc i no hauríeu de continuar.</label>
+ <br><br>
+ <label>Els llocs web demostren la seva identitat mitjançant certificats. El %1$s no confia en <b>%2$s</b> perquè l’emissor del seu certificat és desconegut, el certificat està signat per ell mateix o el servidor no envia els certificats intermedis correctes.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Vés enrere (recomanat)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Accepto el risc i vull continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Aquest lloc web requereix una connexió segura.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La pàgina que esteu intentant veure no es pot mostrar perquè aquest lloc requereix una connexió segura.</li>
+ <li>Molt probablement, l’error és del lloc web i no hi podeu fer res per resoldre’l.</li>
+ <li>Podeu notificar el problema a l’administrador del lloc web.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avançat…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label><b>%1$s</b> té una política de seguretat anomenada «HTTP Strict Transport Security» (Seguretat estricta de transport HTTP, o HSTS), que vol dir que el <b>%2$s</b> només pot connectar-s’hi de forma segura. No podeu afegir cap excepció per visitar aquest lloc.</label>
+
+]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Vés enrere</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">S’ha interromput la connexió</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>El navegador s’ha connectat correctament, però la connexió s’ha interromput mentre es transferia informació. Torneu-ho a provar.</p>
+ <ul>
+ <li>El lloc web podria estar temporalment no disponible o massa ocupat. Torneu-ho a provar d’aquí a uns moments.</li>
+ <li>Si no podeu carregar cap pàgina, comproveu la connexió de dades o Wi-Fi del vostre dispositiu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">S’ha esgotat el temps d’espera de la connexió</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>El lloc sol·licitat no ha respost a la sol·licitud de connexió i el navegador ha deixat d’esperar-ne una resposta.</p>
+ <ul>
+ <li>Pot ser que el servidor estigui experimentant una alta demanda o una suspensió temporal? Torneu-ho a provar més tard.</li>
+ <li>No podeu navegar per altres llocs? Comproveu la connexió del vostre navegador a la xarxa.</li>
+ <li>L’ordinador està protegit amb un tallafoc o servidor intermediari (proxy)? Si hi ha cap paràmetre incorrecte, podria afectar la navegació web.</li>
+ <li>Encara teniu problemes? Consulteu el vostre administrador de xarxes o demaneu assistència al vostre proveïdor d’Internet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">No s’ha pogut connectar</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>El lloc web podria estar temporalment no disponible o massa ocupat. Torneu-ho a provar d’aquí a uns moments.</li>
+ <li>Si no podeu carregar cap pàgina, comproveu la connexió de dades o Wi-Fi del vostre dispositiu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Resposta inesperada del servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>El lloc ha respost a la sol·licitud de la xarxa d’una manera inesperada i el navegador no podrà continuar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La pàgina no està redirigint correctament</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>El navegador ha deixat de provar d’obtenir l’element sol·licitat. El lloc redirigeix la sol·licitud d’una manera que mai es podrà completar.</p>
+ <ul>
+ <li>Heu inhabilitat o blocat les galetes que necessita el lloc?</li>
+ <li>Si en acceptar les galetes del lloc no es resol el problema, probablement es tracta d’una incidència amb la configuració del servidor i no del vostre ordinador.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mode fora de línia</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>El navegador està treballant fora de línia i no es pot connectar a l’element sol·licitat.</p>
+ <ul>
+ <li>L’ordinador està connectat a una xarxa activa?</li>
+ <li>Premeu «Torna-ho a provar» per canviar al mode en línia i tornar a carregar la pàgina.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">El port s’ha restringit per motius de seguretat</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>L’adreça sol·licitada especifica un port (per exemple, <q>mozilla.org:80</q> per al port 80 a mozilla.org) que normalment s’utilitza per a propòsits <em>diferents</em> de la navegació web. El navegador ha cancel·lat la sol·licitud per garantir la vostra protecció i seguretat.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">S’ha reiniciat la connexió</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>S’ha interromput l’enllaç a la xarxa mentre es negociava una connexió. Torneu-ho a provar.</p>
+ <ul>
+ <li>El lloc web podria estar temporalment no disponible o massa ocupat. Torneu-ho a provar d’aquí a uns moments.</li>
+ <li>Si no podeu carregar cap pàgina, comproveu la connexió de dades o Wi-Fi del vostre dispositiu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipus de fitxer insegur</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de contingut malmès</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>La pàgina que esteu intentant veure no es pot mostrar perquè s’ha produït un error en la transmissió de les dades.</p>
+ <ul>
+ <li>Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">El contingut ha fallat</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>La pàgina que esteu intentant veure no es pot mostrar perquè s’ha produït un error en la transmissió de les dades.</p>
+ <ul>
+ <li>Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de codificació del contingut</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>La pàgina que esteu intentant veure no es pot mostrar perquè utilitza una forma de compressió no vàlida o incompatible.</p>
+ <ul>
+ <li>Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">No s’ha trobat l’adreça</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>El navegador no ha pogut trobar l’ordinador central per a l’adreça proporcionada.</p>
+ <ul>
+ <li>Comproveu que no s’hagin introduït errors en teclejar l’adreça, p. ex.
+ <strong>ww</strong>.example.com en lloc de
+ <strong>www</strong>.example.com.</li>
+ <li>Si no podeu carregar cap pàgina, comproveu la connexió de dades o Wi-Fi del vostre dispositiu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">No hi ha connexió a Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Comproveu la vostra connexió a la xarxa o proveu de tornar a carregar la pàgina d’aquí a uns moments.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Torna a carregar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">L’adreça no és vàlida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>L’adreça proporcionada no té un format reconegut. Comproveu si a la barra d’ubicació hi ha cap error i torneu-ho a provar.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">L’adreça no és vàlida</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Les adreces web normalment s’escriuen així: <strong>http://www.example.com/</strong></li>
+ <li>Assegureu-vos que utilitzeu les barres inclinades (<strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocol desconegut</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>L’adreça especifica un protocol (p. ex. <q>wxyz://</q>) que el navegador no reconeix, així doncs, no es pot connectar correctament al lloc.</p>
+ <ul>
+ <li>Esteu provant d’accedir a contingut multimèdia o bé a altres serveis que no són text? Comproveu els requisits addicionals del lloc.</li>
+ <li>Alguns protocols poden requerir programari o connectors de tercers per tal que el navegador els pugui reconèixer.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">No s’ha trobat el fitxer</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Pot ser que l’element hagi canviat de nom, s’hagi esborrat o canviat d’ubicació?</li>
+ <li>Hi ha cap error ortogràfic, de majúscules/minúscules, o bé tipogràfic a l’adreça?</li>
+ <li>Teniu suficients permisos d’accés per accedir a l’element sol·licitat?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">S’ha denegat l’accés al fitxer</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Pot ser que s’hagi eliminat, que s’hagi traslladat o que els permisos del fitxer n’impedeixin l’accés.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">S’ha rebutjat la connexió al servidor intermediari</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>S’ha configurat el navegador perquè utilitzi un servidor intermediari, però s’ha rebutjat la connexió.</p>
+ <ul>
+ <li>És correcta la configuració del servidor intermediari? Comproveu els seus paràmetres i torneu-ho a provar.</li>
+ <li>Esteu segur que el servidor intermediari permet connexions des d’aquesta xarxa?</li>
+ <li>Encara teniu problemes? Demaneu ajuda al vostre administrador de xarxa o al vostre proveïdor d’Internet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">No s’ha trobat el servidor intermediari</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>S’ha configurat el navegador perquè utilitzi un servidor intermediari, però aquest no s’ha pogut trobar.</p>
+ <ul>
+ <li>És correcta la configuració del servidor intermediari? Comproveu els seus paràmetres i torneu-ho a provar.</li>
+ <li>L’ordinador està connectat a una xarxa activa?</li>
+ <li>Encara teniu problemes? Demaneu ajuda al vostre administrador de xarxa o al vostre proveïdor d’Internet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de lloc amb programari maliciós</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>S’ha informat que %1$s és un lloc atacant i s’ha blocat d’acord amb les vostres preferències de seguretat.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de lloc amb programari indesitjable</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>S’ha informat que el lloc %1$s conté programari indesitjable i s’ha blocat d’acord amb les vostres preferències de seguretat.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de lloc maliciós</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>S’ha informat que %1$s és un lloc potencialment maliciós i s’ha blocat d’acord amb les vostres preferències de seguretat.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de lloc enganyós</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>S’ha informat que aquesta pàgina web de %1$s és enganyosa i s’ha blocat d’acord amb les vostres preferències de seguretat.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Lloc segur no disponible</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Heu activat el «mode només HTTPS» per a millorar la seguretat i no hi ha disponible la versió HTTPS de <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Ves al lloc HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..ac6665469a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cak/strings.xml
@@ -0,0 +1,274 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Titojtob\'ëx chik</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Man xtikïr ta xutz\'aqatisaj ri k\'utunïk</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Majun na\'oj k\'o chi rij re k\'ayewal o sachoj re\'.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Xq\'ate\' ri Ütz Okem</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Man tikirel ta nik\'ut pe ri ruxaq natojtob\'ej najäq ruma man tikirel ta nijikib\'äx nitz\'et chi e qitzij ri taq tzij.</li>
+ <li>Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Xqate\' ri Ütz Okem</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Rik\'in jub\'a\' jun ruk\'ayewal ri runuk\'ulem ruk\'u\'x samaj, o chuqa\' k\'o ri nik\'exo ri ruk\'u\'x samaj.</li>
+ <li>We at jikïl chi atokinäq chik ütz pa re ruk\'u\'x samaj re\', ri sachoj xa xe jun ti mej ruma ri\' tikirel natojtob\'ej chik pa jun ch\'utiramaj.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Taq Q\'axinäq…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Rik\'in jub\'a\' k\'o noxk\'ob\'en ri ruxaq ruma ri\' man ta chik nasamajij qa.</label>
+ <br><br>
+ <label>Ri ajk\'amaya\'l ruxaq nikik\'üt ri kib\'anikil rik\'in iqitzijob\'al. %1$s man rukuqub\'an ta ruk\'u\'x pa <b>%2$s</b> ruma man etaman ta ruwäch ri xya\'o ri iqitzijob\'al, ri iqitzijob\'al ruyon juch\'un o ri ruk\'u\'x samaj man yerutäq ta ri nik\'aj ütz taq iqitizjob\'al.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Titzolïx (Chilab\'en)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Tak\'ulu\' ri K\'ayewal chuqa\' Tinsamajij el</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Re ruxaq k\'amaya\'l nrajo\' jun jikïl okem.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Ri ruxaq natojtob\'ej natz\'ët man tz\'etel ta ruma chi ri ruxaq k\'amaya\'l nrajo\' jun jikïl okem.</li>
+ <li>Rik\'in jub\'a\' ri k\'ayewal xa ruma ri ruxaq ajk\'amaya\'l, majun yatikïr nab\'än chuwäch richin nasöl.</li>
+ <li>Yatikïr naya\' rutzijol chi re ri runuk\'samajel ruxaq k\'amaya\'l chi rij ri k\'ayewal.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Q\'axinäq…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b>k\'o jun runa\'ojil jikomal b\'ina\'am HTTP Jikïl Ruk\'waxik Jikomal (HSTS), ri nuq\'ajuj chi ri <b>%2$s</b> xa xe nitikïr nok pa ütz jikomal. Man yatikïr ta natz\'aqatisaj jun man relik ta richin natz\'ët re ruxaq re\'. </label>
+
+]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Titzolin</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Xq\'at ri okem</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Ütz xok ri okik\'amaya\'l, xa xe chi xnaq ri okem toq niq\'axäx ri etamab\'äl. Tatojtob\'ej chik.</p>
+ <ul>
+ <li>Rik\'in jub\'a\' man wachel ta ri ruxaq wakami o yalan rusamaj. Tatojb\'ej pa jun chik ti ramaj.</li>
+ <li>We majun ruxaq nisamäj, tanik\'oj ri Wi-Fi Okem o taq rutzij ri awokisab\'al oyonib\'äl.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Xxik\'o ruq\'ijul ri okem</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Ri ruxaq k’amaya’l xk’utüx, man xunimaj ta tzij richin nok chuqa’ ri okik’amaya’l xtane’ chi royob’exik jun tzolin tzij.</p>
+ <ul>
+ <li>¿La nik’ulwachitäj chi e k’ïy yekanon richin o xqupïx jun ch’utiramaj ri ruk’u’x samaj? Tatojtob’ej chik pa jun ch’utiramaj.</li>
+ <li>¿La man nitikïr ta nok pa juley chik ruxaq k’amaya’l? Tanik’oj ri rokem pa k’amab’ey awokisab’al.</li>
+ <li>¿La nichajïx ri awokisab’al rik’in jun proxi o jun firewall? Jun itzel runuk’ulem nitikïr nuq’ät ri okem pa k’amaya’l.</li>
+ <li>¿La k’a k’o na ak’ayewal? Tak’ulb’ej ri runuk’unel ak’amab’ey o ri ya’öl ak’amaya’l richin yato’ chi rusamajixik.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Man nitikïr ta nok</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Ri ruxaq k\'amaya\'l rik\'in jub\'a\' man okel ta wakami o yalan tajin nokisäx. Tatojtob\'ej chik pa jun ti mej</li>
+ <li>We man yatikïr ta nasamajij jujun taq ruxaq k\'amaya\'l, tanik\'oj ri rutzij oyonib\'äl okisaxel o ri Wi-Fi awokem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Man oyob\'en ta re rutzijol nuya\' pe ri ruk\'u\'x samaj</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Ri okik\'amaya\'l man nitikïr ta chik nusamajib\'ej ri tzolin rutzij ruxaq k\'amaya\'l.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Man ütz ta niq\'axan ri ruxaq k\'amaya\'l</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Ri okik\'amaya\'l xtane\' nutojtob\'ej nuköl ri ch\'akulal xk\'utüx. Ri ruxaq k\'amaya\'l nuq\'axan chik ri taqowuj, rik\'in jun rub\'eyal ri man xtik\'is ta rutz\'aqatisaxik.</p>
+ <ul>
+ <li>¿La e chupül o eq\'aton ri taq kaxlanwey yek\'atzin chi re re ruxaq k\'amaya\'l re\'?</li>
+ <li>Man nusöl ta ri k\'ayewal we ye\'awajo\' ri taq kaxlanwey, rik\'in jub\'a\' jun k\'ayewal richin runuk\'ulem ri ruk\'u\'x samaj, man k\'a ruk\'ayewal ta ri awokisab\'al.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Pa Rub\'eyal Majun Okem</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Ri okik\'amaya\'l nisamäj pa rub\'eyal majun okem pa k\'amaya\'l, ruma ri\' toq man tikirel ta nok pa ri ch\'akulal nik\'utüx chi re.</p>
+ <ul>
+ <li>¿La okinäq ri okisab\'äl pa jun tzijïl k\'amab\'ey?</li>
+ <li>Tapitz\'a\' “Titojtob\'ëx chik” richin tiq\'ax pa rub\'eyal okem pa k\'amaya\'l chuqa\' tisamajib\'ëx chik ri ruxaq k\amaya\'l.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Q\'aton ri b\'ey ruma ri nichajïx</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Ri ochochib\'äl nik\'utüx xuya\' retal jun b\'ey (achi\'el <q>mozilla.org:80</q> richin ri b\'ey 80 richin mozilla.org) okisan kichin <em>jalajöj</em> okem pa k\'amaya\'l. Ri okik\'amaya\'l xuq\'ät ri taqowuj ruma ri ato\'ik chuqa\' achajixïk.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Xtikirisäx chik ri okem</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Xnaq ri ruk\'amab\'ey ximonel toq nitzijöx jun okem . Tatojtob\'ej chik.</p>
+ <ul>
+ <li>Rik\'in jub\'a\' man wachel ta ri ruxaq wakami o yalan rusamaj. Tatojb\'ej pa jun chik ti ramaj.</li>
+ <li>We majun ruxaq nisamäj, tanik\'oj ri Wi-Fi Okem o taq rutzij ri awokisab\'al oyonib\'äl.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Itzel Ruwäch Chi Yakb\'äl</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Rusachoj Itzelan Rupam</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Ri ruxaq wuj natojtob\'ej natz\'ët man tikirel ta nik\'ut pe ruma xilitäj jun sachoj toq yeq\'alajisäx pe ri taq tzij.</p>
+ <ul>
+ <li>Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Q\'aton rupam</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Ri ruxaq wuj natojtob\'ej natz\'ët man tikirel ta nik\'ut pe ruma xilitäj jun sachoj toq yeq\'alajisäx pe ri taq tzij.</p>
+ <ul>
+ <li>Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Rusachoj Rucholajil Rupam</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Ri ruxaq nawajo\' natzu\', man tikirel ta nik\'ut pe, ruma nrokisaj jun ruwäch chi jitz\'oj, ri man ütz ta chuqa\' man nik\'ul ta.</p>
+ <ul>
+ <li>Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Man Xilitäj Ta Ri Ochochib\'äl</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Man xril ta ri ruk\'u\'x samaj ochochib\'äl ri okik\'amaya\'l.</p>
+ <ul>
+ <li>Tanik\'oj chi majun sachoj ruk\'wan ri ochochib\'äl achi\'el
+ <strong>ww</strong>.example.com pa ruk\'exel ri
+ <strong>www</strong>.example.com.</li>
+ <li>We man yatikïr ta nasamajib\'ej jun ruxaq, tanik\'oj ri Wi-Fi awokem o rutzij awokisab\'al.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Majun Okem pa K\'amaya\'l</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Tanik\'oj ri awokem pa k\'amab\'ey o tatojtob\'ej nasamajij chik ri ruxaq pa jun ti mej.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Tisamajïx chik</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Man Okel Ta Ri Ochochib\'äl</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Man etaman ta ruwäch ri rub\'anikil ri ochochib\'äl xtz\'ib\'äx. Ke\'anik\'oj ri taq sachoj pa ri kikajtz\'ik ochochib\'äl, k\'a ri\' tatojtob\'ej chik.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Man nuxïm ta ri\' ri ochochib\'äl</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Rochochib\'al Ajk\'amaya\'l achi\'el jantape\' nitz\'ib\'äx achi\'el <strong>http://www.example.com/</strong></li>
+ <li>Tajikib\'a\' chi ye\'awokisaj ri q\'eq\'el taq juch\' (achi\'el <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Man Etaman Ta Ruwäch Ri Rub\'eyal Samaj</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Ri ochochib\'äl nuya\' retal jun rub\'eyal samaj (achi\'el <q>wxyz://</q>) man netamäx ta ruwäch ruma ri okik\'amaya\'l, ruma ri man nitikïr ta nok pa rub\'eyal pa ri ruxaq k\'amaya\'l.</p>
+ <ul>
+ <li>¿La natojtob\'ej yatok chupam jun k\'ïy k\'oxom o jun chik chi samajib\'äl? Tanik\'oj we k\'o chi k\'o taq rutz\'aqat rajowaxik k\'o chi k\'o chupam.</li>
+ <li>Jujun taq rub\'eyal samaj rik\'in jub\'a\' yekajo\' taq kema\' o taq tz\'aqat kichin aj rox winaqi\' richin ütz yesamäj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Man Xilitäj Ta ri Yakb\'äl</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>¿Rik\'in jub\'a\' chi nisik\'ïx chik, xyujtäj o xk\'ex rub\'ey ri ch\'akulal?</li>
+ <li>¿La k\'o jun sachoj pa ruwi\' rutz\'ib\'axik, nimatz\'ib\' o jun chik chi sachoj toq xtz\'ib\'äx ri ochochib\'äl?</li>
+ <li>¿La kiya\'on aq\'ij richin yatok chupam ri ch\'akulal xk\'utüx?</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Xq\'at rutz\'etik ri yakb\'äl</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Rik\'in jub\'a\' xyuj el, xsilöx el o ri niya\'on q\'ij chi ke ri yakb\'äl niq\'ato rutz\'etik.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Xxutüx ri okem pa k\'amaya\'l ruma ri ruproxi ruk\'u\'x samaj</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Nuk\'un ri okik\'amaya\'l richin nrokisaj jun proxi ruk\'u\'x samaj, xa xe chi xtzolïx pe ri okem.</p>
+ <ul>
+ <li>¿La ütz ri runuk\'ulem ruproxi okik\'amaya\'l? Tanik\'oj ri runuk\'ulem chuqa\' tatojtob\'ej chik.</li>
+ <li>¿Nuya\' q\'ij ri proxi ruk\'u\'x samaj taq okem pa re k\'amab\'ey re\'?</li>
+ <li>¿La k\'a k\'o taq k\'ayewal? Tak\'ulub\'ej ri runuk\'samajel k\'amab\'ey o ya\'öl k\'amaya\'l richin ato\'ik pa ruwi\' rusamajixik.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Man Xilitäj Ta Ri Proxi Ruk\'u\'x Samaj</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Nuk\'un ri okik\'amaya\'l richin nrokisaj jun proxi ruk\'u\'x samaj, xa xe chi man xilitäj ta ri proxi ruk\'u\'x samaj.</p>
+ <ul>
+ <li>¿La ütz ri runuk\'ulem ruproxi okik\'amaya\'l? Tanik\'oj ri runuk\'ulem chuqa\' tatojtob\'ej chik.</li>
+ <li>¿La tzijïl ri okisab\'äl pa jun tzijïl k\'amab\'ey?</li>
+ <li>¿La k\'a k\'o taq k\'ayewal? Tak\'ulub\'ej ri runuk\'samajel k\'amab\'ey o ya\'öl k\'amaya\'l richin ato\'ik pa ruwi\' rusamajixik.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Ruk\'ayewal ruxaq rik\'in Malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Re ruxaq k\'amaya\'l pa %1$s xya\' rutzijol chi niqeleb\'en, ruma ri\' toq xq\'at ruma ach\'ojin achajinik.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Man oyob\'en ta reruk\'ayewal ruxaq re\'</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Re ruxaq k\'amaya\'l re\' %1$s xya\' rutzijol chi nik\'atzin chi ke ri itzel taq kema\', ruma ri\' xq\'eleb\'ëx richin ri rajowaxik chajinïk.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Ruk\'ayewal tz\'ilanel ruxaq</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Re ruxaq k\'amaya\'l pa %1$s xya\' rutzijol chi niqeleb\'en, ruma ri\' toq xq\'at ruma ach\'ojin achajinik.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Ruk\'ayewal q\'olonel ruxaq</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Re ruxaq k\'amaya\'l pa %1$s xya\' rutzijol chi niq\'eleb\'en, ruma ri\' toq xq\'at ruma ach\'ojin chajinïk.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Majun jikïl ruxaq</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Xatzïj ri HTTPS-Only Rub\'anikil richin nutziläx ri jikomal chuqa\' jun HTTPS ruwäch <em>%1$s</em> man wachel ta.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Tik\'oje\' na pa HTTP Ruxaq</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..e354e473a6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,293 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Usba</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Dili makumpleto ang Request</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ · <p>Wala pa\'y dugang detalye para ani nga problema o error.</p>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Secure Connection Pakyas</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ · <ul>
+ · <li>Ang page nga imong buot lantawon dili mapakita kay ang authenticity sa nadawat nga data dili ma-verify.</li>
+ · <li>Palihug pahibal-a ang tag-iya sa website mahitungod ani nga problema.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Secure Connection Pakyas</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ · <ul>
+ · <li>Basin problema ni sa server configuration, o basin naa\'y gusto mo-impersonate ani nga server.</li>
+ · <li>Kung naka-konek ka ani nga server sa niagi, basin temporary ra ni nga error, sulayi lang ug balik unya.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Advanced…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ · <label>Basin naay ga-impersonate sa site ug dili ka dapat mopadayon.</label>
+ · <br><br>
+ · <label>Ang mga website nagapaila-ila pinaagi sa mga certificate. Ang %1$s wala\'y salig sa <b>%2$s</b> kay dili ilado ang iyang certificate issuer, ang certificate kay self-signed, o ang server kay wala nagtubag sa insakto nga mga intermediate certificate.</label>
+ · ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Balik (Rekomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Dawata ang Risgo ug Padayon</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Advanced…</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Ang koneksyon naputol</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ · <p>Nakakonek ang browser, pero naputol ang koneksyon kamulong balhin sa impormasyon. Palihog sulayi usab.</p>
+ · <ul>
+ · <li>Basin ang site dili available kadyot o busy lang. Sulay balik karon taudtaod.</li>
+ · <li>Kung wala pa gihapon mogawas nga page, tan-awa imong device data o Wi-Fi connection.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ang koneksyon ni-time out.</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ · <p>Ang gi-request nga site wala nitubag sa request ug niundang na ang browser ug paghulat sa tubag.</p>
+ · <ul>
+ · <li>Basin nakasinati ang server ug taas nga demand o kasamtangang pagkapalong? Sulayi balik unya.</li>
+ · <li>DIli ba ka maka-browse ug uban site? Tan-awa ang device network connection.</li>
+ · <li>Aduna ba\'y firewall o proxy ang imong device o network? Ang sayop nga setting makaputol sa Web browsing.</li>
+ · <li>Problema pa gihapon? Tawagi imong network administrator o Internet provider para sa dugang tabang.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Dili ka-konek</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ · <ul>
+ · <li>Ang site dili ma-open o busy kaayo. Sulayi balik taudtaod.</li>
+ · <li>Kung wala pa gihapon mogawas nga page, tan-awa imong device data o koneksyon sa Wi-Fi.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Wala damha nga tubag sa server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ · <p>Ang site nitubag sa network request sa wala damha nga pamaagi ug ang browser dili na makapadayon.</p>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Ang page dili ka-redirect ug tarong</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ · <p>Ni-undang ang browser ug kuha sa mga gi-request nga mga item. Ang site ga-redirect sa pamaagi nga dili makumpleto.</p>
+ · <ul>
+ · <li>Imo ba gi-hunongan o gi-block ang mga cookie nga kinahanglan ani nga site?</li>
+ · <li>Kung ang pagdawat sa mga cookie sa site wala makasulbad sa problema, basin problema kini sa server configuration ug dili sa imong device.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline Mode</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ · <p>Ang browser naka-offline mode karon ug dili makakuha sa gipangayo nga item.</p>
+ · <ul>
+ · <li>Ang device konektado ba sa active nga network?</li>
+ · <li>Pinduta ang “Sulayi Balik” para maka-online mode ug ablihi balik ang page.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port gidid-an tungod sa seguridad</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ · <p>Ang girequest nga address duna\'y port (e.g., <q>mozilla.org:80</q> para port 80 sa mozilla.org) gigamit para lang sa <em>ubang tuyo</em> dili Web browsing. Gi-undang sa browser ang request alang sa imong proteksyon ug seguridad.</p>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Na-reset ang koneksyon</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ · <p>Naputol ang network link samtang gasulay ug konek. Palihog sulay balik.</p>
+ · <ul>
+ · <li>Ang site dili ma-open o busy kaayo. Sulayi balik taudtaod.</li>
+ · <li>Kung wala pa gihapon mogawas nga page, tan-awa imong device data o koneksyon sa Wi-Fi.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Delikado nga File Type</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ · <ul>
+ · <li>Palihog ug kontak sa mga tag-iya sa website aron ipahibalo ang problema.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Guba nga Content Error</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ · <p>Ang page nga gisulayan ug pakita dili makita tungod kay naa\'y error nga nabantayan kamulong transmission.</p>
+ · <ul>
+ · <li>Palihog ug kontak sa mga tag-iya sa website aron ipahibalo ang problema.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Daot nga Content</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ · <p>Ang page nga gisulayan ug pakita dili makita tungod kay naa\'y error nga nabantayan kamulong transmission.</p>
+ · <ul>
+ · <li>Palihog ug kontak sa mga tag-iya sa website aron ipahibalo ang problema.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Content Encoding Error</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ · <p>Ang page nga gisulayan ug pakita dili makita tungod kay gagamit kini ug sayop nga porma sa compression.</p>
+ · <ul>
+ · <li>Palihog ug kontak sa mga tag-iya sa website aron ipahibalo ang problema.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Wala\'y Address</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ · <p>Dili makita sa browser ang host server sa gihatag nga address.</p>
+ · <ul>
+ · <li>Susiha ang address kung naa\'y sayup sa pag-type sama sa
+ · <strong>ww</strong>.example.com imbis
+ · <strong>www</strong>.example.com.</li>
+ · <li>Kung wala pa gihapon mogawas nga page, tan-awa imong device data o koneksyon sa Wi-Fi.</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Wala\'y koneksyon sa internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Tan-awa imong koneksyon sa network o sulayi ug reload ang page taudtaod.</string>
+
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Reload</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Sayop ang Address</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ · <p>Ang gihatag nga address sayop ang format. Palihog ug tan-aw sa location bar kung naa\'y sayop ug sulay balik.</p>
+ · ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Sayop ang address</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ · <ul>
+ · <li>Ang kasagaran sa mga Web address gisulat sama sa <strong>http://www.example.com/</strong></li>
+ · <li>Siguruha nga nigamit ka ug mga forward slash (i.e. <strong>/</strong>).</li>
+ · </ul>
+ · ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Di inila nga Protocol</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Ang address nagbungat ug protocol (eg <q>wxyz://</q>) nga wa ilha sa browser, mao nga ang browser dili maka-konek ug tarong sa site.</p>
+ <ul>
+ <li>Nagsulay ba ka ug access ug multimedia o laing non-text nga mga service? i-Check ang site alang sa dugang requirement.</li>
+ <li>Ubang mga protocol nag-require ug third-party software o mga plugin aron makaila ang browser kanila.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Wala Makita ang File</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Basin ang item kay na-rename, na-remove, o nabalhin?</li>
+ <li>Naa ba\'y spelling, capitalization, o ubang sayop sa pag-sulat sa address?</li>
+ <li>Naa ba ka\'y igo nga access permission sa gi-request nga item?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Access sa File gi-deny</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+
+ <ul>
+ <li>Basin kini na-remove, nabalhin, o ang mga file permission nagpugong sa access.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Ang Proxy Server Balibad sa Koneksyon</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Ang browser na-configure nga mogamit ug proxy server, pero ang proxy gabalibad ug koneksyon.</p>
+ <ul>
+ <li>Sakto ba ang proxy configuration sa browser? Susiha ang mga setting ug sulay balik.</li>
+ <li>Ang proxy service gadawat ba ug koneksyon gikan ani nga network?</li>
+ <li>Aduna pa\'y problema? Konsulta sa inyong network administrator o internet service provider ug tabang.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy Server Wa Makita</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Ang browser na-configure nga mogamit ug proxy server, pero ang proxy dili makita.</p>
+ <ul>
+ <li>Sakto ba ang proxy configuration sa browser? Susiha ang mga setting ug sulay balik.</li>
+ <li>Ang proxy service gadawat ba ug koneksyon gikan ani nga network?</li>
+ <li>Aduna pa\'y problema? Konsulta sa inyong network administrator o internet service provider ug tabang.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Malware site issue</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Ang site sa %1$s nareport nga usa ka-attack site busa na-block base sa imong mga security preference.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Inayran nga site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Ang site sa %1$s nareport nga nagsilbi ug kadudahang software busa kini na-block base sa imong mga security preference.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Daotan nga site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Ang site sa %1$s nareport nga gidudahang makadaot nga site busa na-block kini base sa imong mga security preference.</p>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Makailad nga site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Kini nga webpage sa %1$s nareport nga mailarong site busa kini nablock base sa imong mga security preference.</p>
+ ]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..7bbef143d0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">دووبارە هەوڵ بدەرەوە</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ناتوانرێت داواکاری تەواو بکرێت</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>زانیاری زیاتر دەربارەی ئەم کێشەیە یان ئەم هەڵەیە ئێستا بەردەست نیە.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">پەیوەندی پارێزراو سەرکەوتوو نەبوو</string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">پەیوەندی پارێزراو سەرکەوتوو نەبوو</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">پێشکەوتوو…</string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">بڕۆ دواوە (پێشنیارکراوە)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">مەترسیەکە وەردەگرم و بەردەوام بە</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">پەیوەندی جیگیر نیە</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">پەیوەندی بەسەرچوو</string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">نەتوانرا پەیوەندی بکا</string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">وەڵامێکی چاوەڕواننەکراو لە ڕاژەوە</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>ماڵپەڕ بە شێوەیەکی نائاسایی وەڵامی دایەوە بۆیە وێبگەڕ بەردەوام نابێت.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">پەڕە بەشێوەیەکی ڕاست دووبارە ناردنەوە ئەنجام نادات</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">دۆخی دەرهێڵ</string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">دەرەچە ڕێگەپێنەدراوە لەبەر هۆکاری پاراستن</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">پەیوەندی نوێکرایەوە</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">جۆری پەڕگە پارێزراو نیە</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[ <ul>
+ <li>تکایە پەیوەندی بکە بە بەرپرسی ماڵپەڕەکەوە بۆ ئاگادادارکردنەوەی لەم کێشەیە.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">هەڵەی ناوەڕۆکی تێکشکاو</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>ئەو پەڕەیەیی کە دەتەوێت بیبینیت ناتوانرێت پیشانبدرێت لەبەرئەوەی گواستنەوەی زانیاری تێدا بە دیدەکرێت.</p>
+ <ul>
+ <li>تکایە پەیوەندی بکە بە خاوەنی ماڵپەڕەکەوە و کێشەکەیان پێ ڕابگەیەنە.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">ناوەڕۆک تێکشکا</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>ئەو پەڕەیەیی کە دەتەوێت بیبینیت ناتوانرێت پیشانبدرێت لەبەرئەوەی گواستنەوەی زانیاری تێدا بە دیدەکرێت.</p>
+ <ul>
+ <li>تکایە پەیوەندی بکە بە خاوەنی ماڵپەڕەکەوە و کێشەکەیان پێ ڕابگەیەنە.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">هەڵەی کۆدکردنی ناوەڕۆک</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>ئەو پەڕەیەیی کە دەتەوێت بیبینیت ناتوانرێت پیشانبدرێت لەبەرئەوەی فۆرمێکی هەڵە یان پشتگیر نەکراو بەکرادێنێت بۆ پەستاندن .</p>
+ <ul>
+ <li>تکایە پەیوەندی بکە بە خاوەنی ماڵپەڕەکەوە و کێشەکەیان پێ ڕابگەیەنە.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ناونیشان نەدۆزرایەوە</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">پەیوەندی ئینتەرنێت نیە</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">تکایە دڵنیابە ئینتەرنێتەکەت کار دەکات یاخوود پەڕە دووبارە نوێبکەرەوە.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">بارکردنەوە</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ناونیشان ڕاست نیە</string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ناونیشان گونجاو نیە</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>ناونیشانی ماڵپەڕ بەم شێوەیە دەنووسرێت <strong>http://www.example.com/</strong></li>
+ <li>دڵنیابە کە لارەهێڵی ڕاست بەکاردێنیت (بۆ نموونە <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">پرۆتۆکۆڵی نەناسراو</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">پەڕگە نەدۆزرایەوە</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">چوونەناوی پەڕگە ڕەتکرایەوە</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ڕاژەخوازی پرۆکسی نەدۆزرایەوە</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">گرفتی ماڵپەڕی داوانامەی خراپ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">گرفتی ماڵپەڕی نەویستراو</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">گرفتی ماڵپەڕی بەزیان</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">گرفتی ماڵپەڕی فێڵاوی</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..3006d727f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-co/strings.xml
@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Ripruvà</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Ùn si pò compie a richiesta</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Nisuna infurmazione ùn hè dispunibule à st’ora apprupositu di stu penseru o di stu sbagliu.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Fiascu di a cunnessione assicurizata</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>A pagina chì vò circate à fighjà ùn pò micca esse affissata, perchè a sputichezza di i dati ricevuti ùn pò micca esse verificata.</li>
+ <li>Ci vuleria à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Fiascu di a cunnessione assicurizata</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Stu penseru pò vene di a cunfigurazione di u servitore, osinnò ghjè qualchissia forse chì prova d’impatrunissi di u servitore.</li>
+ <li>S’è vo avete dighjà riesciutu à cunnettevi à stu servitore, u sbagliu pò esse timpurariu, è pudete pruvà torna da quì à pocu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Espertu…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Qualchissia puderia pruvà d’impatrunissi di u situ web è seria più sicuru d’ùn micca cuntinuà.</label>
+ <br><br>
+ <label>I siti web stabiliscenu a so identità via certificati. %1$s ùn hà fidanza in <b>%2$s</b> perchè l’emettore di u so certificatu hè scunnisciutu, u certificatu hè firmatu daperellu, o ancu u servitore ùn manda micca certificati intermediarii curretti.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Ritornu (ricumandatu)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Accettà u risicu è cuntinuà</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Stu situ web richiede una cunnessione sicura.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>A pagina chì vò circate à fighjà ùn pò micca esse affissata perchè u situ web richiede una cunnessione sicura.</li>
+ <li>Sicuramente, stu prublema hè cagiunatu da u situ web, è ùn pudete fà nunda per què.</li>
+ <li>Ma pudete cuntattà l’amministratore di stu situ web per infurmallu di stu prublema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Espertu…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> impiega una strategia di sicurità chjamata HTTP Strict Transport Security (HSTS), vole si dì chì <b>%2$s</b> pò solu ci cunnettesi di manera assicurizata. Ùn pudete micca aghjunghje un’eccezzione per visità stu situ. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Ritornu</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">A cunnessione hè stata interrotta</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>U navigatore s’hè cunnessu bè, ma a cunnessione hè stata interrotta durante u trasferimentu d’infurmazioni. Ci vole à pruvà torna.</p>
+ <ul>
+ <li>Forse u site hè timpurariamente indispunibule o sopraccarcu. Pruvate torna da quì à pocu.</li>
+ <li>S’ella ùn hè mancu pussibule di navigà nant’à un situ, verificate a cunnessione di dati o Wi-Fi di u vostru apparechju.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">U cumportu d’attesa hè statu ecciditu</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>U navigatore hà troppu aspettatu durante a cunnessione à u situ è hà fermatu d’aspettà una risposta.</p>
+ <ul>
+ <li>Forse u servitore tratta dumande numerose o hè timpurariamente in panna ? Pruvate torna da quì à pocu.</li>
+ <li>Ùn pudete micca navigà nant’à d’altri siti ? Verificate a cunnessione à a reta di u vostru apparechju.</li>
+ <li>U vostru apparechju o a vostra reta hè prutettu da un parafocu o un proxy ? Gattivi parametri ponu interferisce cù a navigazione nant’à u web.</li>
+ <li>Avete sempre penseri ? Cuntattate u vostru amministratore di a reta o u vostru furnidore d’accessu à internet per ottene un aiutu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Impussibule di cunnettesi</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Forse u site hè timpurariamente indispunibule o sopraccarcu. Pruvate torna da quì à pocu.</li>
+ <li>S’ella ùn hè mancu pussibule di navigà nant’à un situ, verificate a cunnessione di dati o Wi-Fi di u vostru apparechju.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Risposta inespettata da u servitore</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>U situ hà rispostu à a dumanda di a reta da una manera inaspettata è u navigatore ùn pò micca cuntinuà.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">A pagina ùn hè micca ridirettata currettamente</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>U navigatore hà piantatu d’aspettà una risposta da u situ. U situ hà creatu una ridirezzione d’una manera ch’ella ùn puderà mai riesce.</p>
+ <ul>
+ <li>Avete disattivatu o bluccatu i canistrelli richiesti da stu situ ?</li>
+ <li>S’è u penseru ùn hè currettu cù l’accunsentu di i canistrelli, forse ci serà una cunfigurazione gattiva di u servitore è micca di u vostru apparechju.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modu fora di cunnessione</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>U navigatore funziuneghja in u modu fora di cunnessione è ùn si pò cunnette à l’indirizzu indicatu.</p>
+ <ul>
+ <li>L’apparechju hè cunnessu à a reta ?</li>
+ <li>Sciglite « Pruvà torna » per cambià ver di u modu in linea è ricaricà a pagina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Portu ristrintu per raghjone di sicurità</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>L’indirizzu richiesta indicheghja un portu (i.e. : <q>mozilla.org:80</q> per u portu 80 nant’à mozilla.org) chì hè di solitu impiegatu per d’<em>altri</em> scopi chè a navigazione nant’à u Web. U navigatore hà abbandunatu a richiesta per a vostra prutezzione è a vostra sicurità.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">A cunnessione hè stata rilanciata</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>U liame cù a reta hè statu interrottu durante a neguziazione di a cunnessione. Ci vole à pruvà torna.</p>
+ <ul>
+ <li>Forse u site hè timpurariamente indispunibule o sopraccarcu. Pruvate torna da quì à pocu.</li>
+ <li>S’ella ùn hè mancu pussibule di navigà nant’à un situ, verificate a cunnessione di dati o Wi-Fi di u vostru apparechju.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipu di schedariu micca sicuru</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Ci vole à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Sbagliu di cuntenutu alteratu</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>A pagina chì vò circate à fighjà ùn pò micca esse affissata, perchè un sbagliu in a trasmissione di dati hè statu scupertu.</p>
+ <ul>
+ <li>Ci vole à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">U cuntenutu s’hè lampatu</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>A pagina chì vò circate à fighjà ùn pò micca esse affissata, perchè un sbagliu in a trasmissione di dati hè statu scupertu.</p>
+ <ul>
+ <li>Ci vole à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Sbagliu di cudificazione di cuntenutu</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>A pagina chì vò circate à fighjà ùn pò micca esse affissata, perchè ella impiega una forma di cumpressione scunnisciuta o micca accettata.</p>
+ <ul>
+ <li>Ci vole à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">L’indirizzu ùn esiste micca</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>U navigatore ùn pò micca truvà u servitore ospite per l’indirizzu indicatu.</p>
+ <ul>
+ <li>Verificate a sintassa di l’indirizzu per circà i sbaglii cum’è
+ <strong>ww</strong>.esempiu.com invece di
+ <strong>www</strong>.esempiu.com.</li>
+ <li>S’ella ùn hè mancu pussibule di navigà nant’à un situ, verificate a cunnessione di dati o Wi-Fi di u vostru apparechju.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Alcuna cunnessione internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verificate a vostra cunnessione internet o pruvate di ricaricà a pagina da quì à pocu.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Attualizà</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Indirizzu inaccettevule</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>L’indirizzu pruvistu ùn hè micca in un furmatu ricunnisciutu. Ci vole à verificà ch’ellu ùn ci hè micca sbagliu in a barra d’indirizzu è pruvà torna.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">L’indirizzu ùn hè accettevule</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>A sintassa di l’indirizzi hè di solitu <strong>http://www.esempiu.com/</strong></li>
+ <li>Assicuratevi d’impiegà barre sbieche (i.e. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocollu scunnisciutu</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>L’indirizzu indicheghja un protocollu (i.e. : <q>wxyz://</q>) scunnisciutu da u navigatore chì ùn riesce micca à cunnettesi currettamente à u situ.</p>
+ <ul>
+ <li>>Pruvate d’accede à cuntenutu multimedia o à d’altri servizii non-testu ? Verificate nant’à u situ i prugrammi chì sò richiesti.</li>
+ <li>Certi protocolli ponu richiede un terzu prugramma o moduli d’estensione per chì u navigatore possi ricunnoscelli.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">U schedariu ùn esiste micca</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Forse l’elementu hè statu rinuminatu, cacciatu o dispiazzatu ?</li>
+ <li>Ci hè un sbagliu d’ortugrafia, di maiuscula o un altru sbagliu di tipografia ?</li>
+ <li>Avete abbastanza permessi d’accessu per st’elementu ?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">L’accessu à u schedariu hè statu ricusatu</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Forse u schedariu hè statu cacciatu, dispiazzatu, o i so permessi ùn permettenu d’accedeci.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">A cunnessione hè stata ricusata da u servitore proxy</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>U navigatore hè cunfiguratu per impiegà un servitore proxy, ma quessu hà ricusatu a cunnessione.</p>
+ <ul>
+ <li>Hè curretta a cunfigurazione proxy di u navigatore ? Verificate e preferenze è pruvate torna.</li>
+ <li>U serviziu proxy permette e cunnessioni da sta reta ?</li>
+ <li>Avete sempre penseru ? Cuntattate u vostru amministratore di a reta o u vostru furnidore d’accessu à internet per ottene un aiutu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">U servitore proxy ùn si trova micca</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>U navigatore hè cunfiguratu per impiegà un servitore proxy, ma quessu ùn si trova micca.</p>
+ <ul>
+ <li>Hè curretta a cunfigurazione proxy di u navigatore ? Verificate e preferenze è pruvate torna.</li>
+ <li>Hè cunnessu à una reta attiva, l’apparechju ?</li>
+ <li>Avete sempre penseru ? Cuntattate u vostru amministratore di a reta o u vostru furnidore d’accessu à internet per ottene un aiutu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Prublema di prugramma animosu</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>U situ web à l’indirizzu %1$s hè statu signalatu cum’è una surghjente d’affronti è hè statu bluccatu secondu à e vostre preferenze di sicurità.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Prublema di situ indesiderevule</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>U situ web à l’indirizzu %1$s hè statu signalatu cum’è cuntenendu prugrammi indesiderevule è hè statu bluccatu secondu à e vostre preferenze di sicurità.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Prublema di situ periculosu</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>U situ web à l’indirizzu %1$s hè statu signalatu cum’è putenzialmente periculosu è hè statu bluccatu secondu à e vostre preferenze di sicurità.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Prublema di situ ingannatore</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>U situ web à l’indirizzu %1$s hè statu signalatu cum’è essendu ingannatore è hè statu bluccatu secondu à e vostre preferenze di sicurità.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">U situ sicurizatu ùn hè micca dispunibule</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Avete attivatu u modu « HTTPS solu » per una sicurità rinfurzata, ma ùn ci hè alcuna versione HTTPS di <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Cuntinuà nant’à u situ HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..f8641d820a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cs/strings.xml
@@ -0,0 +1,298 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Zkusit znovu</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Nepodařilo se dokončit požadavek</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Další informace o této chybě nejsou bohužel dostupné.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Chyba zabezpečeného spojení</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Požadovanou stránku nelze zobrazit, protože nelze ověřit autenticitu přijatých dat.</li>
+ <li>Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Chyba zabezpečeného spojení</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Tato chyba může být způsobena chybnou konfigurací serveru nebo někým,
+kdo se snaží vydávat za server.</li>
+ <li>Pokud jste se k tomuto serveru už v minulosti úspěšně připojili, je možná chyba jenom dočasná, a můžete to zkusit znovu později.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Rozšířené…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Někdo se může snažit vydávat za zmiňovaný server a proto byste neměli v připojení pokračovat.</label>
+ <br><br>
+ <label>Webové stránky prokazují svou totožnost pomocí certifikátů. %1$s nemůže server <b>%2$s</b> ověřit, protože vydavatel zaslaného certifikátu je neznámý, certifikát je podepsaný sám sebou nebo server neposílá správné mezilehlé certifikáty.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Zpátky (doporučeno)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Beru na vědomí a chci pokračovat</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Tento web vyžaduje zabezpečené připojení.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Stránka, kterou se snažíte načíst, nemůže být zobrazena, protože vyžaduje zabezpečené připojení.</li>
+ <li>Příčina tohoto problému je pravděpodobně na straně serveru a vy ji bohužel nemůžete odstranit.</li>
+ <li>O problému můžete informovat správce webu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Rozšířené…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[Server <label> <b>%1$s</b> má nastaveno bezpečnostní pravidlo HTTP Strict Transport Security (HSTS), které od aplikace <b>%2$s</b> vyžaduje používání pouze zabezpečeného spojení. Pro připojení k této stránce nelze udělit výjimku.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Zpátky</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Spojení bylo přerušeno</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Podařilo se připojit k serveru, ale spojení bylo v průběhu přenosu přerušeno. Opakujte akci.</p>
+ <ul>
+ <li>Stránka může být dočasně nedostupná nebo zaneprázdněná. Zkuste to znovu za pár okamžiků.</li>
+ <li>Pokud nemůžete načíst žádnou stránku, zkontrolujte svá mobilní data nebo Wi-Fi připojení.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Vypršel čas spojení</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Požadovaný server neodpověděl na požadavek o připojení a prohlížeč ukončil čekání na tuto odpověď.</p>
+ <ul>
+ <li>Server může být velmi vytížen. Opakujte akci později.</li>
+ <li>Funguje načítání ostatních webových stránek? Zkontrolujte síťové připojení vašeho zařízení.</li>
+ <li>Připojuje se vaše zařízení k síti skrze firewall nebo proxy server? Nesprávné nastavení může načítání stránek ovlivnit.</li>
+ <li>Pokud problém přetrvává, poraďte se se správcem vaší sítě, nebo poskytovatelem připojení k internetu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Chyba spojení</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Stránka může být dočasně nedostupná nebo zaneprázdněná. Zkuste to znovu za pár okamžiků.</li>
+ <li>Pokud nemůžete načíst žádnou stránku, zkontrolujte svá mobilní data nebo Wi-Fi připojení.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Neplatná odpověď serveru</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Server odpověděl na požadavek neočekávaným způsobem a prohlížeč tak nemohl pokračovat.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Smyčka při přesměrování</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Prohlížeč ukončil spojení, protože server přesměrovává požadavky na tuto adresu sám na sebe, a to takovým způsobem, který zabraňuje jejich dokončení.</p>
+ <ul>
+ <li>Je možné, že stránka vyžaduje ukládání cookies, které máte zakázané nebo je pro tento server blokujete.</li>
+ <li>Většinou se ale jedná o problém konfigurace serveru a <em>není</em> to tak problém vašeho zařízení.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Režim offline</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Prohlížeč je teď v režimu offline a k požadované položce se nelze připojit.</p>
+ <ul>
+ <li>Je zařízení připojeno k funkční síti?</li>
+ <li>Pro přechod do režimu online a opětovné načtení stránky klepněte na tlačítko „Zkusit znovu“.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Omezení přístupu na port</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>V požadované adrese (URL) byl zadán port (např. <q>mozilla.org:80</q> pro port 80 na serveru mozilla.org), který se obvykle používá pro <em>jiné</em> internetové služby než je prohlížení webových stránek. Prohlížeč zrušil požadavek z důvodů vaší ochrany.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Spojení přerušeno</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Spojení bylo v průběhu otevírání komunikačního kanálu se serverem neočekávaně přerušeno. Opakujte akci.</p>
+ <ul>
+ <li>Server je dočasně nedostupný. Zkuste to prosím znovu za chvíli.</li>
+ <li>Pokud nemůžete načíst žádnou stránku, zkontrolujte svá mobilní data nebo Wi-Fi připojení.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Nebezpečný typ souboru</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Chyba v obsahu stránky</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Požadovanou stránku nelze zobrazit, protože při přenosu dat došlo k chybě.</p>
+ <ul>
+ <li>Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Pád obsahu</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Požadovanou stránku nelze zobrazit, protože při přenosu dat došlo k chybě.</p>
+ <ul>
+ <li>Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Chyba znakové sady obsahu</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Požadovanou stránku nelze zobrazit, protože používá neplatný či nepodporovaný způsob komprese dat.</p>
+ <ul>
+ <li>Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresa nenalezena</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>URL adresa neodpovídá známému serveru a nelze ji načíst.</p>
+ <ul>
+ <li>Zkontrolujte, že je adresa napsaná správně a neobsahuje chyby jako
+ <strong>ww</strong>.example.com místo <strong>www</strong>.example.com.</li>
+ <li>Pokud se vám nezobrazují ani ostatní stránky, zkontrolujte své připojení přes mobilní data nebo Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Připojení k internetu není dostupné</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Zkontrolujte připojení k síti nebo zkuste stránku za chvilku znovu načíst.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Znovu načíst</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Neplatná adresa</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Adresa (URL) není platná a nelze ji načíst. Zkontrolujte prosím, že je adresa napsána správně.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Neplatná adresa</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Webové adresy jsou obvykle psány jako <strong>http://www.example.com/</strong></li>
+ <li>Ujistěte se, že používáte běžná lomítka (tj. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Neznámý protokol</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adresu (URL) určuje protokol (např. <q>wxyz://</q>), který nebyl prohlížečem rozpoznán, a proto se k ní nemůže korektně připojit.</p>
+ <ul>
+ <li>Zkoušíte přistupovat k multimédiím či jiné netextové službě? Podívejte se, jaké další věci stránka vyžaduje.</li>
+ <li>Některé protokoly mohou vyžadovat software třetích stran nebo zásuvné moduly dříve, než je prohlížeč může rozpoznat.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Soubor nenalezen</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Je možné, že byl smazán, přejmenován nebo přesunut.</li>
+ <li>Zkontrolujte prosím, že je adresa napsána správně, a to včetně velikosti písmen.</li>
+ <li>Jste-li autorem tohoto souboru, ověřte, že daný soubor na serveru existuje a že má příslušná práva na zobrazení.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Přístup k souboru byl odepřen</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Možná byl smazán, přesunut nebo jeho oprávnění zabraňují přístupu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy server odmítl spojení</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Prohlížeč je nastaven, aby používal proxy server, který odmítá spojení.</p>
+ <ul>
+ <li>Zkontrolujte v prohlížeči nastavení proxy serveru a akci opakujte.</li>
+ <li>Je možné, že proxy server nepovoluje připojení z vaší sítě.</li>
+ <li>Pokud problém přetrvává, poraďte se se správcem vaší sítě, nebo poskytovatelem připojení k internetu.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy server nenalezen</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Prohlížeč je nastaven, aby používal proxy server, který nelze nalézt.</p>
+ <ul>
+ <li>Zkontrolujte v prohlížeči nastavení proxy serveru a akci opakujte.</li>
+ <li>Zkontrolujte síťové připojení vašeho zařízení.</li>
+ <li>Pokud problém přetrvává, poraďte se se správcem vaší sítě, nebo poskytovatelem připojení k internetu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problém se škodlivým softwarem</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Stránka %1$s byla nahlášena jako útočná a byla zablokována na základě vašeho bezpečnostního nastavení.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problém s nežádoucí webovou stránkou</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Stránka %1$s byla nahlášena jako stránka s nežádoucím softwarem a byla zablokována na základě vašeho bezpečnostního nastavení.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problém se škodlivou stránkou</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Stránka %1$s byla nahlášena jako útočná a byla zablokována na základě vašeho bezpečnostního nastavení.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problém s klamavou stránkou</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Tato webová stránka na serveru %1$s byla nahlášena jako klamavá a byla zablokována na základě vašeho bezpečnostního nastavení.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Nelze navázat zabezpečené spojení</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Máte zapnutý režim „pouze HTTPS“ zajišťující vyšší míru zabezpečení, ale zabezpečená verze webu <em>%1$s</em> není dostupná.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Pokračovat přes nezabezpečené spojení</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..f0a28d1e10
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-cy/strings.xml
@@ -0,0 +1,301 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Ceisiwch Eto</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Methu Cyflawni’r Cais</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Nid oes gwybodaeth ar gael am y gwall neu broblem yma ar hyn o bryd.</p>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Methodd y Cysylltiad Diogel</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Nid oes modd dangos y dudalen rydych yn ceisio ei darllen am nad oes modd dilysu’r data rydych wedi ei dderbyn.</li>
+ <li>Cysylltwch â pherchnogion y wefan i’w hysbysu o\'r anhawster.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Methodd y Cysylltiad Diogel</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Gall hwn fod yn anhawster gyda ffurfweddiad y gweinydd neu gall fod yn rhywun sy\'n ceisio dynwared y gweinydd.</li>
+ <li>Os ydych wedi cysylltu’n llwyddiannus gyda’r gweinydd yn y gorffennol, efallai mai gwall dros dro ydyw ac i geisio eto.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Uwch…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+        <label>Gallai rhywun fod yn ceisio dynwared y wefan hon a dylech chi ddim mynd ymhellach.</label>
+        <br><br>
+        <label>Mae gwefannau yn profi eu hunaniaeth trwy dystysgrifau. Nid yw %1$s yn ymddiried yn <b>%2$s</b> oherwydd nad yw cyhoeddwr ei dystysgrif yn hysbys, mae’r dystysgrif wedi’i hunan-lofnodi, neu nid yw’r gweinydd yn anfon y tystysgrifau canolradd cywir.</label>
+    ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Ewch Nôl (Argymell)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Derbyn y Perygl a Pharhau</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Mae angen cysylltiad diogel ar y wefan hon.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Nid oes modd dangos y dudalen rydych yn ceisio ei gweld oherwydd bod angen cysylltiad diogel ar y wefan hon.</li>
+ <li>Mae’r broblem yn fwyaf tebygol gyda’r wefan, a does dim byd y gallwch chi ei wneud i’w ddatrys.</li>
+ <li>Gallwch roi gwybod i weinyddwr y wefan am y broblem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Uwch…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ Mae gan <label> <b>%1$s</b> bolisi diogelwch o’r enw HTTP Strict Transport Security (HSTS), sy’n golygu mai dim ond yn ddiogel y gall <b>%2$s</b> gysylltu ag ef. Ni allwch ychwanegu eithriad i ymweld â’r wefan hon. </label>
+]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Ewch Nôl</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Cafodd y cysylltiad ei darfu</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Cysylltodd y porwr yn llwyddiannus, ond amharwyd ar y cysylltiad wrth drosglwyddo gwybodaeth. Rhowch gynnig arall arni.</p>
+      <ul>
+        <li>Gallai’r wefan fod ar gael dros dro neu’n rhy brysur. Rhowch gynnig arall arni mewn ychydig eiliadau.</li>
+        <li>Os na allwch lwytho unrhyw dudalennau, gwiriwch ddata neu gysylltiad Wi-Fi eich dyfais.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Mae cyfnod y cyswllt wedi dod i ben</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Nid yw’r wefan wedi ymateb i gais am gyswllt ac mae’r porwr wedi peidio aros am ateb.</p>
+ <ul>
+ <li>Gall fod y gweinydd yn profi galw sylweddol neu ataliad dros dro? Ceisiwch eto.</li>
+ <li>A ydych yn methu pori i wefannau eraill? Gwiriwch gysylltiad rhwydwaith y ddyfais.</li>
+ <li>A yw eich dyfais wedi ei ddiogelu gan fur cadarn neu ddirprwy? Gall osodiadau anghywir effeithio ar bori\'r we.</li>
+ <li>Yn dal yn cael problemau? Cysylltwch â’ch gweinyddwr rhwydwaith neu ddarparwr rhyngrwyd am gymorth.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Methu cysylltu</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Mae’n bosib fod y wefan yn rhy brysur neu ddim ar gael dros dro. Ceisiwch eto cyn bo hir.</li>
+ <li>Os nad oes modd i chi lwytho unrhyw dudalennau, gwiriwch gysylltiad data neu Wi-Fi eich dyfais.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Ymateb annisgwyl gan y gweinydd</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Ymatebodd y wefan i’r cais rhwydwaith mewn ffordd annisgwyl ac nid yw’r porwr yn gallu parhau.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Nid yw’r dudalen yn ailgyfeirio’n iawn</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Mae’r porwr wedi rhoi’r gorau i geisio adfer yr eitem y gofynnwyd amdani. Mae’r wefan yn ailgyfeirio’r cais mewn ffordd na fydd byth yn ei gwblhau.</p>
+      <ul>
+        <li>A ydych wedi analluogi neu rwystro cwcis sy’n ofynnol gan y wefan hon?</li>
+        <li>Os nad yw derbyn cwcis y wefan yn datrys y broblem, mae’n debygol mai mater ffurfweddiad y gweinydd ydyw ac nid eich dyfais chi.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modd All-lein</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Mae’r porwr yn gweithredu yn ei fodd all-lein ac nid yw’n gallu cysylltu â’r eitem y gofynnwyd amdani.</p>
+      <ul>
+        <li>A yw’r ddyfais wedi’i chysylltu â rhwydwaith gweithredol?</li>
+        <li>Pwyswch “Ceisiwch Eto” i newid i’r modd ar-lein ac ail-lwytho’r dudalen</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porth wedi’i gyfyngu am resymau diogelwch</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Gofynnodd y cyfeiriad am borth (e.e., <q>mozilla.org:80</q> sef, porth 80 ar mozilla.org) sy’n cael ei ddefnyddio am resymau <em>heblaw</em> pori’r We. Mae’r porwr wedi diddymu eich cais er eich diogelwch.</p>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Cafodd y cysylltiad ei ailosod</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+      <p>Amharwyd ar y cysylltiad rhwydwaith wrth negodi cysylltiad. Rhowch gynnig arall arni.</p>
+      <ul>
+        <li>Gallai’r wefan fod ar gael dros dro neu’n rhy brysur. Rhowch gynnig arall arni mewn ychydig eiliadau.</li>
+        <li>Os na allwch lwytho unrhyw dudalennau, gwiriwch ddata neu gysylltiad Wi-Fi eich dyfais.</li>
+      </ul>
+    ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Math Anniogel o Ffeil</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Cysylltwch â pherchnogion y wefan i’w hysbysu o’r anhawster.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Gwall Cynnwys Llygredig</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Nid oes modd dangos y dudalen rydych yn ceisio ei gweld yn sgil canfod gwall trosglwyddo data.</p>
+ <ul>
+ <li>Cysylltwch â pherchnogion y wefan i’w hysbysu o’r anhawster.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Chwalodd y cynnwys</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Nid oes modd dangos y dudalen rydych yn ceisio ei gweld yn sgil canfod gwall trosglwyddo data.</p>
+ <ul>
+ <li>Cysylltwch â pherchnogion y wefan i’w hysbysu o’r anhawster.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Gwall Amgodio Cynnwys</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Nid oes modd dangos y dudalen hon am ei bod yn defnyddio ffurf o gywasgiad sy’n anhysbys neu sydd ddim yn cael ei gynnal.</p>
+ <ul>
+ <li>Cysylltwch â pherchnogion y wefan i’w hysbysu o’r anhawster.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Heb Ganfod Cyfeiriad</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+      <p>Nid yw’r porwr yn gallu dod o hyd i’r gweinydd cynnal ar gyfer y cyfeiriad hwn.</p>
+      <ul>
+        <li>Gwiriwch y cyfeiriad am wallau teipio fel
+          <strong>ww</strong>.example.com yn lle
+          <strong>www</strong>.example.com.</li>
+        <li> Os na allwch lwytho unrhyw dudalennau, gwiriwch ddata neu gysylltiad Wi-Fi eich dyfais</li>
+      </ul>
+    ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Dim cysylltiad rhyngrwyd</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Gwiriwch eich cysylltiad rhwydwaith neu ceisiwch ail-lwytho’r dudalen ymhen ychydig eiliadau.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Ail-lwytho</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Cyfeiriad Annilys</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Nid yw’r cyfeiriad yma mewn fformat adnabyddus. Gwiriwch y bar lleoliad a cheisiwch eto.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Nid yw’r cyfeiriad yn ddilys</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Fel rheol mae cyfeiriadau Gwe’n cael eu hysgrifennu fel <strong>http://www.example.com/</strong></li>
+ <li>Gwnewch yn siŵr eich bod yn defnyddio blaen slaes (h.y. <strong>/</strong>).</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocol Anhysbys</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Mae’r cyfeiriad yn pennu protocol (e.e. <q>wxyz://</q>) nad yw’r porwr yn ei adnabod, felly nid yw’r porwr yn gallu cysylltu’n iawn â’r wefan.</p>
+ <ul>
+ <li>Ydych chi’n ceisio cael mynediad at wasanaethau aml-gyfrwng neu destun yn unig? Gwiriwch y wefan am yr anghenion ychwanegol hyn.</li>
+ <li>Mae rhai protocolau angen meddalwedd trydydd parti neu ategion cyn bod modd i’r porwr eu hadnabod.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Heb Ganfod Ffeil</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Efallai bod yr eitem wedi cael ei hailenwi, ei thynnu neu ei symud?</li>
+ <li>Oes yna wall sillafu, llythrennu neu wall teipio arall yn y cyfeiriad?</li>
+ <li>A oes gennych hawl digonol i gael mynediad at yr eitem hon?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Mae mynediad i’r ffeil wedi ei wrthod</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Gall ei fod wedi ei dynnu, symud neu fod caniatâd ffeiliau yn rhwystro mynediad.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Gwrthododd y Gweinydd Dirprwyol y Cysylltiad</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Mae’r porwr wedi ei ffurfweddu i ddefnyddio gweinydd dirprwy, ond mae’r dirprwy wedi gwrthod cysylltiad.</p>
+ <ul>
+ <li>A yw ffurfweddiad dirprwy’r porwr yn gywir? Gwiriwch y gosodiadau a cheisio eto.</li>
+ <li>A yw’r gwasanaeth dirprwy yn caniatáu cysylltiadau o’r rhwydwaith?</li>
+ <li>Dal yn cael problemau? Cysylltwch â’ch gweinyddwr rhwydwaith neu ddarparwr rhyngrwyd am gymorth.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Heb Ganfod y Gweinydd Dirprwyol</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Mae’r porwr wedi ei ffurfweddu i ddefnyddio gweinydd dirprwy, ond nid oes modd canfod y dirprwy.</p>
+ <ul>
+ <li>A yw ffurfweddiad dirprwy’r porwr yn gywir? Gwiriwch y gosodiadau a cheisio eto.</li>
+ <li>A yw’r ddyfais wedi ei chysylltu i rwydwaith weithredol?</li>
+ <li>Dal yn cael problemau? Cysylltwch â’ch gweinyddwr rhwydwaith neu ddarparwr rhyngrwyd am gymorth.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Mater gwefan drwgwar</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Mae gwefan %1$s yn hysbys fel gwefan ymosod ac wedi cael ei rwystro ar sail eich dewisiadau diogelwch.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Mater gwefan diangen</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Mae gwefan %1$s yn hysbys fel gwefan sy’n cyflwyno meddalwedd diangen ac wedi cael ei rwystro ar sail eich dewisiadau diogelwch.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Mater gwefan niweidiol</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Mae gwefan %1$s yn hysbys fel gwefan a allai fod yn niweidiol ac wedi cael ei rwystro ar sail eich dewisiadau diogelwch.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Mater gwefan twyllodrus</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Mae gwefan %1$s yn hysbys fel gwefan twyllodrus ac wedi cael ei rwystro ar sail eich dewisiadau diogelwch.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Gwefan Ddiogel Ddim ar Gael</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Rydych wedi galluogi’r Modd HTTPS-yn-Unig ar gyfer gwell diogelwch, ac nid oes fersiwn HTTPS o <em>%1$s</em> ar gael.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Ymlaen i’r Wefan HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..f3d2965e5e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-da/strings.xml
@@ -0,0 +1,309 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Prøv igen</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Kan ikke færdiggøre forespørgsel</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Yderligere information om dette problem eller denne fejl er ikke tilgængelig lige nu.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Sikker forbindelse mislykkedes</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Siden kunne ikke vises, da autenticiteten af de modtagne data ikke kunne bekræftes.</li>
+ <li>Kontakt ejerne af webstedet omkring dette problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Sikker forbindelse mislykkedes</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Dette kan være et problem med serverens konfiguration, eller det kan betyde, at nogen forsøger at give sig ud for at være serveren.</li>
+ <li>Hvis du har kunnet tilgå serveren tidligere kan dette problem være midlertidigt og du anbefales at prøve igen senere.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanceret…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Nogen kan have lavet en falsk version af webstedet, og du bør ikke fortsætte.</label>
+ <br><br>
+ <label>Websteder bekræfter deres identitet ved hjælp af sikkerhedscertifikater. %1$s stoler ikke på <b>%2$s</b>, fordi udstederen af webstedets certifikat er ukendt, fordi certifikatet er underskrevet af indehaveren selv, eller fordi serveren ikke sender de korrekte mellemliggende certifikater.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Gå tilbage (anbefalet)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Accepter risikoen og fortsæt</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Dette websted kræver en sikker forbindelse.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Siden kan ikke vises, da webstedet kræver en sikker forbindelse.</li>
+ <li>Problemet skyldes højst sandsynligt webstedet, og du kan ikke selv løse problemet.</li>
+ <li>Du kan prøve at kontakte webstedets administrator for at gøre opmærksom på problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanceret…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> bruger en sikkerhedspolitik kaldet HTTP Strict Transport Security (HSTS), hvilket betyder, at <b>%2$s</b> kun kan oprette en sikker forbindelse til webstedet. Du kan ikke tilføje en undtagelse for at besøge webstedet. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Gå tilbage</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Forbindelsen blev afbrudt</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Browseren oprettede forbindelsen korrekt, men den blev afbrudt under overførsel af data. Prøv igen.</p>
+ <ul>
+ <li>Webstedet kan være midlertidigt utilgængelig eller optaget. Prøv igen senere.</li>
+ <li>Hvis du ikke kan indlæse nogen sider, skal du kontrollere din enheds data- eller wi-fi-forbindelse.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Forbindelsens tidsfrist udløb</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Det anmodede websted svarede ikke på en anmodning om forbindelse, og browseren er stoppet med at vente på et svar.</p>
+ <ul>
+ <li>Kan serveren opleve stor efterspørgsel eller være midlertidigt nede? Prøv igen senere.</li>
+ <li>Er du ikke i stand til at besøge andre websteder? Kontrollér enhedens netværksforbindelse.</li>
+ <li>Er din enhed eller dit netværk beskyttet af en firewall eller en proxy? Forkerte indstillinger kan forstyrre din webbrowsing.</li>
+ <li>Har du stadig problemer? Kontakt din netværksadministrator eller din internetudbyder for at få hjælp.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Kan ikke oprette forbindelse</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Webstedet kan være midlertidigt utilgængeligt eller travlt optaget. Prøv igen senere.</li>
+ <li>Hvis du ikke kan indlæse nogen websider overhovedet, kontrollér da din enheds data- eller wi-fi-forbindelse.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Uventet svar fra server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Webstedet svarede på netværksforespørgslen på en uventet måde, og browseren kan ikke fortsætte.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Denne side viderestiller ikke forespørgslen korrekt</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Browseren har afbrudt forespørgslen, fordi webstedet viderestiller forespørgslen på den måde, der forhindrer den i nogensinde at blive færdig.</p>
+ <ul>
+ <li>Har du deaktiveret eller blokeret cookies for webstedet?</li>
+ <li>Hvis det ikke hjælper at aktivere cookies for webstedet, så skyldes problemet sandsynligvis en fejl på webserveren og ikke din enhed.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline-tilstand</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Browseren er sat i offline-tilstand og kan ikke oprette den anmodede forbindelse.</p>
+ <ul>
+ <li>Er enheden forbundet til et aktivt netværk?</li>
+ <li>Tryk på "Prøv igen" for at skifte til online-tilstand og genindlæse siden.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port begrænset af sikkerhedshensyn</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Den angivne adresse specificerede en port (fx <q>mozilla.org:80</q> for port 80 på mozilla.org), der normalt anvendes til <em>andre</em> formål end visning af websider. Browseren har afbrudt forespørgslen af sikkerhedshensyn.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Forbindelsen blev nulstillet</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Netværkslinket blev afbrudt under forhandlinger om en forbindelse. Prøv igen.</p>
+ <ul>
+ <li>Webstedet kan være midlertidig utilgængeligt eller have for travlt. Prøv igen senere.</li>
+ <li>Hvis du ikke kan indlæse nogen sider overhovedet, så skal du kontrolle din enheds data eller wi-fi-forbindelse.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Usikker filtype</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Kontakt webstedets ejere omkring dette problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Fejlbehæftet indhold</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Siden, du forsøger at se, kan ikke vises, da der er fundet en fejl i overførslen af data.</p>
+ <ul>
+ <li>Kontakt webstedets ejere omkring dette problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Indholdet kunne ikke vises</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Siden, du forsøger at se, kan ikke vises, da der er fundet en fejl i overførslen af data.</p>
+ <ul>
+ <li>Kontakt webstedets ejere omkring dette problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Fejl i indholdskodning</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Siden kunne ikke vises, da den bruger en ugyldig eller ikke-understøttet form for komprimering.</p>
+ <ul>
+ <li>Kontakt webstedets ejere omkring dette problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adressen blev ikke fundet</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Browseren kunne ikke finde host-serveren til den angivne adresse.</p>
+ <ul>
+ <li>Kontroller adressen for tastefejl som fx
+ <strong>ww</strong>.eksempel.dk i stedet for
+ <strong>www</strong>.eksempel.dk.</li>
+ <li>Hvis du ikke kan indlæse nogen sider overhovedet, skal du kontrollere din enheds data- eller wi-fi-forbindelse.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ingen internetforbindelse</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Kontrollér din internetforbindelse, eller prøv at indlæse siden igen om et øjeblik.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Genindlæs</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Ugyldig adresse</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Adressen er ikke angivet i et genkendt format. Undersøg, om der er fejl i adressen, og prøv igen.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adressen er ikke gyldig</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Webadresser skrives normalt som <strong>http://www.eksempel.dk/</strong></li>
+ <li>Kontrollér at du ikke benytter divisionstegn (dvs. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Ukendt protokol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adressen starter med en protokol (fx <q>wxyz://</q>), der ikke genkendes af browseren. Det betyder, at browseren ikke kan oprette en korrekt forbindelse til webstedet.</p>
+ <ul>
+ <li>Prøver du at få adgang til multimedia eller andet indholder, der ikke er tekst? Kontrollér, om webstedet stiller særlige krav.</li>
+ <li>Nogle protokoller kan kræve, at du installere tredjeparts-software eller -plugins, før browseren kan genkende protokollen.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fil ikke fundet</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Er elementet blive omdøbt, fjernet eller flyttet?</li>
+ <li>Er der en stavefejl, fejl med store/små bogstaver eller andre typografiske fejl i adressen?</li>
+ <li>Har du de rette adgangstilladelser til det anmodede element?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Adgang til filen blev nægtet</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Den kan være blevet slettet, flyttet eller tilladelserne for filen kan forhindre adgang.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxyserveren afviste forbindelse</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Browseren er indstillet til at anvende en proxyserver, men proxyserveren afviste forbindelsen.</p>
+ <ul>
+ <li>Er browserens proxy-indstillinger korrekte? Kontrollér indstillingerne og prøv igen.</li>
+ <li>Tillader proxy-tjenesten forbindelser fra dette netværk?</li>
+ <li>Har du stadig problemer? Kontakt din netværksadministrator eller din internetudbyder for at få hjælp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxyserver blev ikke fundet</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Browseren er indstillet til at anvende en proxyserver, men serveren kunne ikke findes.</p>
+ <ul>
+ <li>Er browserens proxy-indstillinger korrekte? Kontrollér indstillingerne og prøv igen.</li>
+ <li>Er enheden forbundet til et aktivt netværk?</li>
+ <li>Har du stadig problemer? Kontakt din netværksadministrator eller din internetudbyder for at få hjælp.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problem med malware-websted</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Webstedet %1$s er blevet anmeldt som et angrebswebsted og er blevet blokeret som følge af dine sikkerhedsindstillinger.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problem med uønsket software</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Webstedet %1$s er blevet anmeldt for at tilbyde uønsket software og er blevet blokeret som følge af dine sikkerhedsindstillinger.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problem med skadeligt websted</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Webstedet %1$s er blevet anmeldt som et potentielt skadeligt websted og er blevet blokeret som følge af dine sikkerhedsindstillinger.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problem med vildledende websted</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Webstedet %1$s er blevet anmeldt som et vildledende websted og er blevet blokeret som følge af dine sikkerhedsindstillinger .</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sikkert websted er ikke tilgængeligt</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Du har aktiveret tilstanden kun-HTTPS for at øge sikkerheden, og en HTTPS-version af <em>%1$s</em> er ikke tilgængelig.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Fortsæt til HTTP-websted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..91ece303fa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-de/strings.xml
@@ -0,0 +1,299 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Erneut versuchen</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Anfrage kann nicht abgeschlossen werden</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Weitere Informationen zu diesem Problem oder Fehler sind momentan nicht verfügbar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Sichere Verbindung fehlgeschlagen</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Die Webseite kann nicht angezeigt werden, da die Authentizität der erhaltenen Daten nicht verifiziert werden konnte.</li>
+ <li>Kontaktieren Sie bitte den Inhaber der Website, um ihn über dieses Problem zu informieren.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Sichere Verbindung fehlgeschlagen</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Dies könnte ein Problem mit der Konfiguration des Servers sein oder jemand, der vorgibt, der Server zu sein.</li>
+ <li>Falls Sie früher bereits erfolgreich eine Verbindung zu dem Server aufgebaut haben, könnte dies ein vorübergehender Fehler sein. Versuchen Sie es in diesem Fall später noch einmal.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Erweitert…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Jemand könnte versuchen, sich als diese Website auszugeben, daher sollten Sie nicht fortfahren.</label>
+ <br><br>
+ <label>Websites weisen ihre Identität mittels Zertifikaten nach. %1$s vertraut <b>%2$s</b> nicht, weil der Aussteller des Zertifikats unbekannt ist, das Zertifikat selbstsigniert ist oder der Server nicht die korrekten Zwischenzertifikate sendet.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Zurück (empfohlen)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Risiko akzeptieren und fortfahren</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Diese Website erfordert eine sichere Verbindung.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Die Seite, die Sie aufrufen möchten, kann nicht angezeigt werden, da für diese Website eine sichere Verbindung erforderlich ist.</li>
+ <li>Das Problem liegt höchstwahrscheinlich bei der Website, und Sie können nichts tun, um es zu lösen.</li>
+ <li>Sie können den Administrator der Website über das Problem informieren.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Erweitert…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> hat eine Sicherheitsrichtlinie namens HTTP Strict Transport Security (HSTS), was bedeutet, dass sich <b>%2$s</b> nur über eine sichere Verbindung mit der Website verbinden kann. Daher kann keine Ausnahme für die Website hinzugefügt werden.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Zurück</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Fehler: Datenübertragung unterbrochen</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Der Browser hat die Verbindung erfolgreich hergestellt, diese ist jedoch während der Datenübertragung abgebrochen. Bitte versuchen Sie es erneut.</p>
+ <ul>
+ <li>Möglicherweise ist die Website vorübergehend nicht verfügbar oder zu beschäftigt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</li>
+ <li>Wenn Sie auch keine andere Website aufrufen können, überprüfen Sie bitte die Daten- oder WLAN-Verbindung.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Fehler: Netzwerk-Zeitüberschreitung</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Die angeforderte Website hat auf eine Verbindungsanfrage nicht reagiert und der Browser wartet inzwischen nicht mehr auf eine Antwort.</p>
+ <ul>
+ <li>Ist der Server eventuell zu stark ausgelastet oder vorübergehend ausgefallen? Versuchen Sie es später erneut.</li>
+ <li>Können Sie auch keine anderen Websites aufrufen? Prüfen Sie die Netzwerkverbindung des Computers.</li>
+ <li>Wird Ihr Computer oder Ihr Netzwerk von einer Firewall oder einem Proxy geschützt? Durch falsche Einstellungen kann das Surfen im Internet behindert werden.</li>
+ <li>Treten immer noch Probleme auf? Bitten Sie Ihren Netzwerkadministrator oder Internetanbieter um Hilfestellung.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Fehler: Verbindung fehlgeschlagen</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Möglicherweise ist die Website vorübergehend nicht verfügbar oder zu beschäftigt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</li>
+ <li>Wenn Sie auch keine andere Website aufrufen können, überprüfen Sie bitte die Daten- oder WLAN-Verbindung.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Fehler: Unerwartete Antwort</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Die aufgerufene Website hat in einer unerwarteten Art geantwortet, sodass die Verbindung nicht fortgesetzt werden kann.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Fehler: Umleitungsfehler</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Der Browser versucht nicht mehr, das angeforderte Element zu öffnen. Die aufgerufene Website leitet die Anfrage so um, dass sie nie beendet werden kann.</p>
+ <ul>
+ <li>Haben Sie Cookies, die von dieser Website benötigt werden, deaktiviert oder blockiert?</li>
+ <li> Falls das Akzeptieren von Cookies das Problem nicht behebt, handelt es sich vermutlich um eine Fehlkonfiguration des Servers und nicht um einen Fehler Ihres Computers.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline-Modus</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Der Browser arbeitet im Offline-Modus und kann daher keine Verbindung mit dem angefragten Element aufbauen.</p>
+ <ul>
+ <li>Ist der Computer mit einem aktiven Netzwerk verbunden?</li>
+ <li>Wählen Sie „Erneut versuchen“, um in den Online-Modus zu wechseln und die Seite erneut zu laden.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port aus Sicherheitsgründen gesperrt</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Die angeforderte Adresse hat einen Port (z. B. <q>mozilla.org:80</q> für Port 80 auf mozilla.org) übergeben, der normalerweise für <em>andere</em> Zwecke als für das Surfen im Internet verwendet wird. Der Browser hat die Anforderung zu Ihrem Schutz und Ihrer Sicherheit abgebrochen.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Fehler: Verbindung unterbrochen</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Die Netzwerkverbindung wurde während des Verbindungsaufbaus unterbrochen. Bitte versuchen Sie es erneut.</p>
+ <ul>
+ <li>Möglicherweise ist die Website vorübergehend nicht verfügbar oder zu beschäftigt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</li>
+ <li>Wenn Sie auch keine andere Website aufrufen können, überprüfen Sie bitte die Daten- oder WLAN-Verbindung.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Unsicherer Dateityp</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Kontaktieren Sie bitte die Betreiber der Website, um sie über dieses Problem zu informieren.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Fehler: Beschädigte Inhalte</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Die Seite, die Sie zu öffnen versuchen, kann nicht angezeigt werden, da ein Fehler in der Datenübertragung festgestellt wurde.</p>
+ <ul>
+ <li>Bitte kontaktieren Sie die Betreiber der Website, um sie über dieses Problem zu informieren.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Inhalt abgestürzt</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Die Seite, die Sie zu öffnen versuchen, kann nicht angezeigt werden, da ein Fehler in der Datenübertragung festgestellt wurde.</p>
+ <ul>
+ <li>Bitte kontaktieren Sie die Betreiber der Website, um sie über dieses Problem zu informieren.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Content-Encoding-Fehler</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Die aufgerufene Seite kann nicht angezeigt werden, da sie eine ungültige oder nicht unterstützte Form der Kompression verwendet.</p>
+ <ul>
+ <li>Bitte kontaktieren Sie die Website-Betreiber, um sie über dieses Problem zu informieren.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresse nicht gefunden</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Der Browser konnte den Host-Server für die angegebene Adresse nicht finden.</p>
+ <ul>
+ <li>Bitte überprüfen Sie die Adresse auf Tippfehler, wie <strong>ww</strong>.example.com statt <strong>www</strong>.example.com.</li>
+ <li>Wenn Sie auch keine andere Website aufrufen können, überprüfen Sie bitte die Daten- oder WLAN-Verbindung.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Keine Internetverbindung</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Überprüfen Sie Ihre Netzwerkverbindung oder versuchen Sie in wenigen Augenblicken, die Seite neu zu laden.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Neu laden</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Ungültige Adresse</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Das Format der angegebenen Adresse wurde nicht erkannt. Bitte überprüfen Sie die Adressleiste auf Fehler und versuchen Sie es erneut.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Fehler: Ungültige Adresse</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Web-Adressen sehen gewöhnlich folgendermaßen aus: <strong>http://www.example.com/</strong></li>
+ <li>Bitte stellen Sie sicher, dass Sie nicht den umgekehrten, sondern den einfachen Schrägstrich (<strong>/</strong>) verwenden.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Unbekanntes Protokoll</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>In der Adresse wird ein Protokoll angegeben (z. B. <q>wxyz://</q>), das der Browser nicht erkennt und ihn daran hindert, eine funktionierende Verbindung zu der Website herzustellen.</p>
+ <ul>
+ <li>Versuchen Sie, auf Multimedia-Inhalte oder andere nicht textorientierte Dienste zuzugreifen? Prüfen Sie, ob für die Website zusätzliche Anforderungen gelten.</li>
+ <li>Manche Protokolle benötigen unter Umständen Drittanbieter-Software oder Plugins, damit sie von Browsern erkannt werden.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Datei nicht gefunden</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Könnte der Eintrag umbenannt, gelöscht oder verschoben worden sein?</li>
+ <li>Enthält die Adresse einen Rechtschreib-, Groß-/Kleinschreibungs- oder anderen Schreibfehler?</li>
+ <li>Haben Sie ausreichende Zugriffsrechte für den angeforderten Eintrag?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Zugriff auf die Datei wurde verweigert</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Sie wurde möglicherweise entfernt, verschoben, oder fehlende Dateiberechtigungen könnten den Zugriff verhindern.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy-Server verweigert die Verbindung</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Der Browser ist für die Verwendung eines Proxy-Servers konfiguriert, doch der Proxy verweigert die Verbindung.</p>
+ <ul>
+ <li>Stimmt die Proxy-Konfiguration des Browsers? Überprüfen Sie die Einstellungen und versuchen Sie es erneut.</li>
+ <li>Lässt der Proxy-Dienst es zu, dass aus diesem Netzwerk heraus Verbindungen hergestellt werden?</li>
+ <li>Bestehen die Probleme weiterhin? Bitten Sie Ihren Netzwerkadministrator oder Internetanbieter um Hilfestellung.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy-Server nicht gefunden</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Der Browser ist für die Verwendung eines Proxy-Servers konfiguriert, doch der Proxy konnte nicht gefunden werden.</p>
+ <ul>
+ <li>Stimmt die Proxy-Konfiguration des Browsers? Überprüfen Sie die Einstellungen und versuchen Sie es erneut.</li>
+ <li>Ist der Computer mit einem aktiven Netzwerk verbunden?</li>
+ <li>Bestehen die Probleme weiterhin? Bitten Sie Ihren Netzwerkadministrator oder Internetanbieter um Hilfestellung.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Malware-Website blockiert</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Die Website auf %1$s wurde als attackierende Website gemeldet und aufgrund Ihrer Sicherheitseinstellungen blockiert.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Unerwünschte Website blockiert</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Die Website auf %1$s wurde als Lieferant von unerwünschter Software gemeldet und aufgrund Ihrer Sicherheitseinstellungen blockiert.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Gefährdende Website blockiert</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Die Website auf %1$s wurde als potenziell gefährdende Seite gemeldet und aufgrund Ihrer Sicherheitseinstellungen blockiert.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Betrügerische Website blockiert</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Die Website auf %1$s wurde als betrügerische Website gemeldet und aufgrund Ihrer Sicherheitseinstellungen blockiert.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sichere Website nicht verfügbar</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Sie haben den Nur-HTTPS-Modus für erhöhte Sicherheit aktiviert und es ist keine HTTPS-Version von <em>%1$s</em> verfügbar.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Weiter zur HTTP-Website</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..988e76c10e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,303 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Hyšći raz wopytaś</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Napšašowanje njedajo se dokóńcyś</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Pśidatne informacije wó toś tom problemje abo zmólce njestoje tuchylu k dispoziciji.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Zwisk njejo se raźił</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Bok, kótaryž cośo se woglědaś, njedajo se pokazaś, dokulaž awtentiskosć dostanych datow njedajo se pśeglědaś.</li>
+ <li>Pšosym stajśo se z wobsejźarjami websedła do zwiska, aby jich wó toś tym problemje informěrował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Zwisk njejo se raźił</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>To by mógło problem z konfiguraciju serwera byś, abo by mógło byś, až něchten wopytujo serwer imitěrowaś.</li>
+ <li>Jolic sćo ze serwerom w zachadnosći wuspěšnje zwězany był, by mógła zmólka snaź nachylna byś a móžośo pózdźej hyšći raz wopytaś.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Rozšyrjone…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Něchten mógał wopytaś, sedło za swójo wudaś, togodla njeměł wy z tym pókšacowaś.</label>
+ <br><br>
+ <label>Websedła swóju identitu pśez certifikaty dopokazuju. %1$s <b>%2$s</b> njedowěri, dokulaž jogo certifikatowy wudawaŕ jo njeznaty, certifkat jo sebjesigněrowany abo serwer korektne mjazycertifikaty njesćelo.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Slědk (dopórucony)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Riziko akceptěrowaś a pókšacowaś</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Toś to websedło se wěsty zwisk pomina.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Bok, kótaryž se cośo woglědaś, njedajo se pokazaś, dokulaž toś to websedło se wěsty zwisk pomina.</li>
+ <li>Problem se nejskerjej pśez websedło zawinujo a togodla njedajo nic, což móžośo cyniś, aby jen rozwězał.</li>
+ <li>Móžośo administratoroju websedła problem k wěsći daś.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Rozšyrjony…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ma wěstotne pšawidło z mjenim HTTP Strict Transport Security (HSTS), kótarež wóznamjenijo, až <b>%2$s</b> móžo se jano wěsće zwězaś. Njamóžśo wuwześe pśidaś, aby se k toś tomu sedłoju woglědał. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Slědk</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Zwisk jo se pśetergnuł</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Wobglědowak jo se wuspěšnje zwězał, ale zwisk jo se pśetergnuł, mjaztym až su se informacije pśenosowali. Pšosym wopytajśo hyšći raz.</p>
+ <ul>
+ <li>Sedło njejo snaź nachylu k dispoziji abo jo pśeśěžone. Wopytajśo za někotare wokognuśa hyšći raz.</li>
+ <li>Jolic njamóžośo boki zacytaś, pśeglědajśo daty swójogo rěda abo WLAN-zwisk.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Zwisk jo cas pśekšocył</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Pominane sedło njejo na zwiskowe napšašowanje wótegroniło a wobglědowak jo pśestał na wótegrono cakaś.</p>
+ <ul>
+ <li>Móžo byś, až serwer jo pśeśěžony abo ma nachylne mólenje? Wopytajśo pózdźej hyšći raz.</li>
+ <li>Njamóžośo druge sedła pśeglědowaś? Pśespytajśo seśowy zwisk rěda.</li>
+ <li>Šćita se waš rěd abo waša seś z wognjoweju murju abo proksy? Njekorektne nastajenja mógu webowemu pśeglědowanjeju zajźowaś.</li>
+ <li>Maśo hyšći śěže? Konsultěrujśo swójogo seśowego administratora abo internetnego póbitowarja za pódpěru.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Zwisk njejo móžny</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Sedło njestoj snaź nachylu k dispoziciji abo jo pśeśěžone. Wopytajśo za mało wokognuśow hyšći raz.</li>
+ <li>Jolic njamóžośo boki zacytaś, pśeglědajśo datowy abo WLAN-zwisk swójogo rěda.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Njewótčakane wótegrono ze serwera</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Sedło jo na napšašowanje seśi na njewócakowany nałog wótegroniło a wobglědowak njamóžo pókšacowaś.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Bok pšawje njepósrědnja</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Wobglědowak jo pśestał, pominany objekt wótwołowaś. Sedło pósrědnja napšašowanje na nałog, kótaryž se njekóńcy.</p>
+ <ul>
+ <li>Sćo cookieje znjemóžnił abo zablokěrował, kótarež su trěbne za tós to sedło?</li>
+ <li>Jolic akceptěrowanje sedłowych cookiejow njerozwězujo ten problem, jo to nejskerjej problem serwereje konfiguracije a nic wašogo rěda.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline-modus</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Wobglědowak źěła offline a njamóžo z pominanym objektom zwězaś.</p>
+ <ul>
+ <li>Jo rěd z aktiwneju seśu zwězany?</li>
+ <li>Klikniśo na „Hyšći raz“, aby do online-modusa pśejšeł a bok znowego zacytał.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port z pśicynow wěstoty wobgranicowany</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Pominana adresa jo port pódała (na pś. <q>mozilla.org:80</q> za port 80 na mozilla.org), kótaryž wužywa se normalnje za <em>druge</em> zaměry nježli webpśeglědowanje. Wobglědowak jo napšašowanje za waš šćit a wěstotu pśetergnuł.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Zwisk jo se slědk stajił</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Syśowy zwisk jo se pśetergnuł, mjaztym až sćo wopytał, zwisk nawězaś. Pšosym wopytajśo hyšći raz.</p>
+ <ul>
+ <li>Sedło njejo snaź nachylu k dispoziji abo jo pśeśěžone. Wopytajśo za někotare wokognuśa hyšći raz.</li>
+ <li>Jolic njamóžośo boki zacytaś, pśeglědajśo daty swójogo rěda abo WLAN-zwisk.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Njewěsty datajowy typ</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Pšosym stejśo se z wobsejźarjami websedła do zwiska, aby je wó toś tom problemje informěrował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Zmólka - wobškóźone wopśimjeśe</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Bok, kótaryž cośo se woglědaś, njedajo se pokazaś, dokulaž jo se zmólka pśi pśenosowanju datow namakała.</p>
+ <ul>
+ <li>Pšosym stajśo se z wobsejźarjami websedła do zwiska, aby je wó toś tom problemje informěrował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Wopśimjeśe jo se wowaliło</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Bok, kótaryž cośo se woglědaś, njedajo se pokazaś, dokulaž jo se zmólka pśi pśenosowanju datow namakała.</p>
+ <ul>
+ <li>Pšosym stajśo se z wobsejźarjami websedła do zwiska, aby je wó toś tom problemje informěrował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Zmólka pśi koděrowanju wopśimjeśa</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Bok, kótaryž cośo se woglědaś, njedajo se pokazaś, dokulaž wužywa njepłaśiwu abo njepódpěranu formu kompresije.</p>
+ <ul>
+ <li>Pšosym stajśo se z wobsejźarjami websedła do zwiska, aby je wó toś tom problemje informěrował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresa njejo se namakała</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Wobglědowak njejo mógał hostowy serwer za pódanu adresu namakaś.</p>
+ <ul>
+ <li>Pśeglědajśo adresu za pisańskimi zmólkami ako
+ <strong>ww</strong>.example.com město
+ <strong>www</strong>.example.com.</li>
+ <li>Jolic njamóžośo boki zacytaś, pśeglědajśo daty swójogo rěda abo WLAN-zwisk.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Žeden internetny zwisk</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Pśeglědajśo swój seśowy zwisk abo zacytajśo bok za mało wokognuśow znowego.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Znowego zacytaś</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Njepłaśiwa adresa</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Pódana adresa njejo w pśipóznatem formaśe. Pšosym pśeglědajśo adresowe pólo za zmólkami a wopytajśo hyšći raz.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adresa njejo płaśiwa</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Webadrese se zwětšego ako <strong>http://www.example.com/</strong> pišu.</li>
+ <li>Zawěsććo, až wužywaśo doprědka schylone nakósne smužki (t. gr. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Njeznaty protokol</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adresa pódawa protokol (na pś. <q>wxyz://</q>), kótaryž wobglědowak njepśipóznawa, tak až wobglědowak njamóžo pšawje ze sedłom zwězaś.</p>
+ <ul>
+ <li>Wopytujośo na multimedia abo druge njetekstowe słužby pśistup maś? Pśespytajśo sedło za wósebnymi pótrěbnosćami.</li>
+ <li>Někotare protokole trjebaju programy tśeśich abo tykace, nježli až wobglědowak móžo je pśipóznaś.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Dataja njejo se namakała</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Jo móžno, až objekt jo se pśemjenił, wótpórał abo pśesunuł?</li>
+ <li>Jo zmólka pšawopisa, wjelikopisanja, abo hynakša typografiska zmólka w adresy?</li>
+ <li>Maśo dosěgajuce pśistupne pšawa za pominany objekt?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Pśistup na dataju jo se wótpokazał</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Snaź jo se wótpórała, pśesunuła, abo datajowe pšawa zajźuju pśistupoju.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proksyjowy serwer jo zwisk wótpokazał</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Wobglědowak jo konfigurěrowany, aby serwer proksy wužywał, ale proksy jo zwisk wótpokazał.</p>
+ <ul>
+ <li>Jo konfiguracija proksy wobglědowaka korektna? Pśeglědajśo nastajenja a wopytajśo hyšći raz.</li>
+ <li>Dowólujo proksyjowa słužba zwiski z toś teje seśi?</li>
+ <li>Maśo hyšći śěže? Konsultěrujśo swójogo seśowego administratora abo internetnego póbitowarja za pódpěru.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proksyjowy serwer njenamakany</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Wobglědowak jo se konfigurěrował, aby se wužywa proksyjowy serwer, ale proksy njedajo se namakaś.</p>
+ <ul>
+ <li>Jo konfiguracija proksyja wobglědowaka korektna? Pśeglědajśo nastajenja a wopytajśo hyšći raz.</li>
+ <li>Jo rěd z aktiwneju seśu zwězany?</li>
+ <li>Maśo hyšći śěže? Stajśo ze swójim seśowym administratorom abo internetny póbitowarjom za pódpěru do zwiska.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problem ze sedłom ze škódneju softwaru</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Sedło na %1$s jo se ako napadujuce sedło k wěsći dało a jo se na zakłaźe wašych wěstotnych nastajenjow zablokěrowało.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problem z njewitanym sedłom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Sedło na %1$s jo se ako sedło k wěsći dało, kótarež póbitujo njewitanu softwaru a jo se na zakłaźe wašych wěstotnych nastajenjow zablokěrowało.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problem z wobgrozujucym sedłom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Sedło na %1$s jo se ako potencielnje wobgrozujuce sedło k wěsći dało a jo se na zakłaźe wašych wěstotnych nastajenjow zablokěrowało.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problem z wobšudnym sedłom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Toś ten bok na %1$s jo se ako wobšudne sedło k wěsći dało a jo se na zakłaźe wašych wěstotnych nastajenjow zablokěrowało.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Wěste sedło njejo k dispoziciji</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Sćo zmóžnił modus Jano-HTTPS za pólěpšonu wěstotu a HTTPS-wersija <em>%1$s</em> njejo k dispoziciji.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Dalej k HTTP-sedłoju</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..d7fbaf6627
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-el/strings.xml
@@ -0,0 +1,319 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Δοκιμή ξανά</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Αδυναμία ολοκλήρωσης αιτήματος</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Αυτή τη στιγμή, δεν διατίθενται επιπρόσθετες πληροφορίες σχετικά με αυτό το πρόβλημα ή σφάλμα.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Αποτυχία ασφαλούς σύνδεσης</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Η σελίδα που προσπαθείτε να προβάλετε δεν μπορεί να εμφανιστεί, επειδή δεν ήταν δυνατή η επαλήθευση της αυθεντικότητας των ληφθέντων δεδομένων.</li>
+ <li>Παρακαλούμε επικοινωνήστε με τους ιδιοκτήτες της ιστοσελίδας για να τους ενημερώσετε για αυτό το πρόβλημα.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Αποτυχία ασφαλούς σύνδεσης</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Ενδέχεται να υπάρχει πρόβλημα με τη διαμόρφωση του διακομιστή, ή κάποιος προσπαθεί να μιμηθεί το διακομιστή.</li>
+ <li>Αν έχετε συνδεθεί με επιτυχία σε αυτό το διακομιστή στο παρελθόν, το σφάλμα ίσως είναι προσωρινό και μπορείτε να δοκιμάσετε ξανά αργότερα.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Σύνθετα…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Κάποιος ίσως προσπαθεί να μιμηθεί την ιστοσελίδα και δεν πρέπει να συνεχίσετε.</label>
+ <br><br>
+ <label>Οι ιστοσελίδες αποδεικνύουν την ταυτότητά τους με τα πιστοποιητικά. Το %1$s δεν εμπιστεύεται το <b>%2$s</b>, επειδή ο εκδότης του πιστοποιητικού του είναι άγνωστος, το πιστοποιητικό είναι αυτοϋπογεγραμμένο, ή ο διακομιστής δεν στέλνει τα σωστά ενδιάμεσα πιστοποιητικά.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Επιστροφή (Προτείνεται)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Αποδοχή κινδύνου και συνέχεια</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Αυτός ο ιστότοπος απαιτεί ασφαλή σύνδεση.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Δεν είναι δυνατή η εμφάνιση της σελίδας που προσπαθείτε να προβάλετε, επειδή αυτός ο ιστότοπος απαιτεί ασφαλή σύνδεση.</li>
+ <li>Το ζήτημα οφείλεται κατά πάσα πιθανότητα στον ιστότοπο και δεν μπορείτε να κάνετε τίποτα για να το επιλύσετε.</li>
+ <li>Μπορείτε να ειδοποιήστε τον διαχειριστή του ιστοτόπου σχετικά με το πρόβλημα.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Σύνθετα…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> Το <b>%1$s</b> διαθέτει μια πολιτική ασφαλείας, η οποία ονομάζεται «HTTP Strict Transport Security (HSTS)», που σημαίνει ότι το <b>%2$s</b> μπορεί να συνδεθεί μόνο με ασφάλεια σε αυτό. Δεν μπορείτε να προσθέσετε εξαίρεση για να επισκεφθείτε τον ιστότοπο αυτό. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Επιστροφή</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Η σύνδεση διακόπηκε</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Το πρόγραμμα περιήγησης συνδέθηκε με επιτυχία, αλλά η σύνδεση διακόπηκε κατά τη μεταφορά πληροφοριών. Παρακαλούμε προσπαθήστε ξανά.</p>
+ <ul>
+ <li>Η σελίδα μπορεί να είναι προσωρινά μη διαθέσιμη ή πολύ απασχολημένη. Δοκιμάστε ξανά σε λίγα λεπτά.</li>
+ <li>Aν δεν μπορείτε να φορτώσετε καμία σελίδα, ελέγξτε τα δεδομένα ή τη σύνδεση Wi-Fi της συσκευής σας.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Το χρονικό όριο της σύνδεσης έληξε</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Η ζητούμενη ιστοσελίδα δεν αποκρίθηκε στο αίτημα σύνδεσης και το πρόγραμμα περιήγησης σταμάτησε να περιμένει απάντηση.</p>
+ <ul>
+ <li>Μήπως ο διακομιστής αντιμετωπίζει υψηλή ζήτηση ή προσωρινή διακοπή λειτουργίας; Δοκιμάστε ξανά αργότερα.</li>
+ <li>Δεν μπορείτε να περιηγηθείτε σε άλλες ιστοσελίδες; Ελέγξτε
+ τη σύνδεση δικτύου της συσκευής σας.</li>
+ <li>Προστατεύεται η συσκευή ή το δίκτυό σας από τείχος προστασίας ή διακομιστή μεσολάβησης; Οι εσφαλμένες ρυθμίσεις μπορούν να επηρεάσουν την περιήγηση.</li>
+ <li>Έχετε ακόμη πρόβλημα; Συμβουλευτείτε το διαχειριστή δικτύου ή τον πάροχό σας για βοήθεια.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Αδυναμία σύνδεσης</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Η σελίδα ενδέχεται να είναι προσωρινά μη διαθέσιμη ή πολύ απασχολημένη. Δοκιμάστε ξανά σε λίγο.</li>
+ <li>Αν δεν μπορείτε να φορτώσετε καμία σελίδα, ελέγξτε τη σύνδεση δεδομένων ή Wi-Fi σας.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Απρόσμενη απάντηση από το διακομιστή</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Ο ιστότοπος απάντησε με απρόσμενο τρόπο στο αίτημα δικτύου και το πρόγραμμα περιήγησης δεν μπορεί να συνεχίσει.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Η σελίδα δεν ανακατευθύνει σωστά</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Το πρόγραμμα περιήγησης έχει σταματήσει την προσπάθεια ανάκτησης του ζητούμενου στοιχείου. Η ιστοσελίδα ανακατευθύνει το αίτημα με τρόπο που δεν θα ολοκληρωθεί.</p>
+ <ul>
+ <li>Έχετε απενεργοποιήσει ή αποκλείσει τα cookie που απαιτούνται από την ιστοσελίδα;</li>
+ <li>Αν η αποδοχή των cookie δεν επιλύει το πρόβλημα, μάλλον οφείλεται σε ζήτημα ρύθμισης του διακομιστή και όχι στη συσκευή σας.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Λειτουργία εκτός σύνδεσης</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Το πρόγραμμα περιήγησης είναι σε λειτουργεία εκτός σύνδεσης και δεν μπορεί να συνδεθεί στο ζητούμενο στοιχείο.</p>
+ <ul>
+ <li>Είναι συνδεδεμένη η συσκευή σε ενεργό δίκτυο;</li>
+ <li>Πατήστε “Δοκιμή ξανά” για σύνδεση στο διαδίκτυο και ανανέωση της σελίδας.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Περιορισμός θύρας για λόγους ασφαλείας</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Η ζητούμενη διεύθυνση καθόρισε θύρα (π.χ., <q>mozilla.org:80</q> για τη θύρα 80 στο mozilla.org) που <em>δεν</em> χρησιμοποιείται συνήθως για περιήγηση στο διαδίκτυο. Το πρόγραμμα περιήγησης ακύρωσε το αίτημα για να σας προστατεύσει.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Έγινε επαναφορά της σύνδεσης</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Ο σύνδεσμος δικτύου διακόπηκε κατά τη διαδικασία σύνδεσης. Παρακαλούμε δοκιμάστε ξανά.</p>
+ <ul>
+ <li>Η ιστοσελίδα ίσως είναι προσωρινά μη διαθέσιμη ή πολύ απασχολημένη. Δοκιμάστε ξανά σε λίγο.</li>
+ <li>Αν δεν μπορείτε να φορτώσετε καμία σελίδα, ελέγξτε τη σύνδεση δεδομένων ή Wi-Fi της συσκευής σας.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Μη ασφαλής τύπος αρχείου</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Παρακαλώ επικοινωνήστε με τους ιδιοκτήτες του ιστοτόπου για να τους ενημερώσετε σχετικά με αυτό το πρόβλημα.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Σφάλμα κατεστραμμένου περιεχομένου</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Η σελίδα που προσπαθείτε να δείτε δεν μπορείτε να εμφανιστεί λόγω σφάλματος κατά τη μεταγωγή δεδομένων.</p>
+ <ul>
+ <li>Παρακαλούμε ενημερώστε τους ιδιοκτήτες της ιστοσελίδας για αυτό το πρόβλημα.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Το περιεχόμενο καταστράφηκε</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Η σελίδα που προσπαθείτε να δείτε δεν μπορείτε να εμφανιστεί λόγω σφάλματος κατά τη μεταγωγή δεδομένων.</p>
+ <ul>
+ <li>Παρακαλούμε ενημερώστε τους ιδιοκτήτες της ιστοσελίδας για αυτό το πρόβλημα.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Σφάλμα κωδικοποίησης περιεχομένου</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Η σελίδα που προσπαθείτε να δείτε δεν μπορεί να εμφανιστεί επειδή δεν χρησιμοποιεί έγκυρη ή υποστηριζόμενη μορφή συμπίεσης.</p>
+ <ul>
+ <li>Παρακαλώ επικοινωνήστε με τους ιδιοκτήτες του ιστοτόπου για να τους ενημερώσετε σχετικά με αυτό το πρόβλημα.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Η διεύθυνση δεν βρέθηκε</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Το πρόγραμμα περιήγησης δεν βρήκε τον κεντρικό διακομιστή για την παρεχόμενη διεύθυνση.</p>
+ <ul>
+ <li>Ελέγξτε τη διεύθυνση για τυχόν λάθη, όπως
+ <strong>ww</strong>.example.com αντί για
+ <strong>www</strong>.example.com.</li>
+ <li>Αν δεν μπορείτε να φορτώσετε καμία σελίδα, ελέγξτε τα δεδομένα ή τη σύνδεση Wi-Fi της συσκευής σας.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Δεν υπάρχει σύνδεση στο διαδίκτυο</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Ελέγξτε τη σύνδεση δικτύου σας ή δοκιμάστε να φορτώσετε εκ νέου τη σελίδα σε λίγα λεπτά.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Ανανέωση</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Μη έγκυρη διεύθυνση</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Η διεύθυνση δεν είναι σε αναγνωρισμένη μορφή. Παρακαλούμε ελέγξτε τη γραμμή διευθύνσεων για λάθη και δοκιμάστε ξανά.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Η διεύθυνση δεν είναι έγκυρη</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Οι διευθύνσεις ιστού γράφονται συνήθως ως εξής: <strong>http://www.example.com/</strong></li>
+ <li>Βεβαιωθείτε ότι χρησιμοποιείτε τις σωστές καθέτους (δηλ. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Άγνωστο πρωτόκολλο</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Η διεύθυνση καθορίζει ένα πρωτόκολλο (π.χ. «wxyz://») που δεν αναγνωρίζει το πρόγραμμα περιήγησης, επομένως δεν είναι δυνατή η σωστή σύνδεση στον ιστότοπο.</p>
+ <ul>
+ <li>Προσπαθείτε να προσπελάσετε πολυμέσα ή άλλες υπηρεσίες χωρίς κείμενο; Ελέγξτε τον ιστότοπο για επιπλέον απαιτήσεις.</li>
+ <li>Ορισμένα πρωτόκολλα ενδέχεται να απαιτούν λογισμικό ή αρθρώματα τρίτων πριν μπορέσουν να αναγνωριστούν από το πρόγραμμα περιήγησης.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Το αρχείο δεν βρέθηκε</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Μήπως το στοιχείο έχει μετονομαστεί, αφαιρεθεί, ή μετακινηθεί;</li>
+ <li>Υπάρχουν σφάλματα ορθογραφίας, κεφαλαίων, ή άλλα τυπογραφικά λάθη στη διεύθυνση;</li>
+ <li>Έχετε επαρκή δικαιώματα για πρόσβαση στο ζητούμενο στοιχείο;</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Άρνηση πρόσβασης στο αρχείο</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Ίσως έχει αφαιρεθεί, μετακινηθεί ή τα δικαιώματα αρχείου εμποδίζουν την πρόσβαση.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Άρνηση σύνδεσης διαμεσολαβητή</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Το πρόγραμμα περιήγησης έχει ρυθμιστεί ώστε να χρησιμοποιεί διακομιστή μεσολάβησης, αλλά ο διακομιστής μεσολάβησης αρνήθηκε τη σύνδεση.</p>
+ <ul>
+ <li>Έχει ρυθμιστεί σωστά ο διακομιστής μεσολάβησης; Ελέγξτε τις ρυθμίσεις και δοκιμάστε ξανά.</li>
+ <li>Επιτρέπει ο διακομιστής μεσολάβησης συνδέσεις από αυτό το δίκτυο;</li>
+ <li>Έχετε ακόμα πρόβλημα; Συμβουλευτείτε το διαχειριστή δικτύου ή τον πάροχό σας για βοήθεια.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Δεν βρέθηκε διακομιστής μεσολάβησης</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Το πρόγραμμα περιήγησης έχει ρυθμιστεί ώστε να χρησιμοποιεί διακομιστή μεσολάβησης, αλλά δεν βρέθηκε διακομιστής μεσολάβησης.</p>
+ <ul>
+ <li>Έχει ρυθμιστεί σωστά ο διακομιστής μεσολάβησης; Ελέγξτε τις ρυθμίσεις και δοκιμάστε ξανά.</li>
+ <li>Είναι συνδεδεμένη η συσκευή σε ενεργό δίκτυο;</li>
+ <li>Έχετε ακόμη πρόβλημα; Συμβουλευτείτε το διαχειριστή δικτύου σας ή τον πάροχό σας για βοήθεια.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Ζήτημα κακόβουλου ιστοτόπου</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Ο ιστότοπος στο %1$s έχει αναφερθεί ως ιστότοπος επιθέσεων και έχει αποκλειστεί βάσει των προτιμήσεων ασφαλείας σας.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Ζήτημα ανεπιθύμητου ιστοτόπου</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Ο ιστότοπος στο %1$s έχει αναφερθεί για διανομή ανεπιθύμητου λογισμικού και έχει αποκλειστεί βάσει των προτιμήσεων ασφαλείας σας.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Ζήτημα επιβλαβούς ιστοτόπου</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Ο ιστότοπος στο %1$s έχει αναφερθεί ως δυνητικά επιβλαβής και έχει αποκλειστεί βάσει των προτιμήσεων ασφαλείας σας.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Ζήτημα παραπλανητικού ιστοτόπου</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Η ιστοσελίδα στο %1$s έχει αναφερθεί ως παραπλανητικός ιστότοπος και έχει αποκλειστεί βάσει των προτιμήσεων ασφαλείας σας.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Δεν διατίθεται ασφαλής ιστότοπος</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Έχετε ενεργοποιήσει τη λειτουργία «Μόνο HTTPS» για ενισχυμένη ασφάλεια, αλλά δεν διατίθεται ασφαλής έκδοση HTTPS του <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Συνέχεια σε ιστότοπο HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..7424a2197c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,298 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Try Again</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Cannot Complete Request</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Additional information about this problem or error is currently unavailable.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Secure Connection Failed</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.</li>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Secure Connection Failed</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>This could be a problem with the server’s configuration, or it could be someone trying to impersonate the server.</li>
+ <li>If you have connected to this server successfully in the past, the error may be temporary, and you can try again later.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Advanced…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Someone could be trying to impersonate the site and you should not continue.</label>
+ <br><br>
+ <label>Websites prove their identity via certificates. %1$s does not trust <b>%2$s</b> because its certificate issuer is unknown, the certificate is self-signed, or the server is not sending the correct intermediate certificates.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Go Back (Recommended)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Accept the Risk and Continue</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">This website requires a secure connection.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>The page you are trying to view cannot be shown because this website requires a secure connection.</li>
+ <li>The issue is most likely with the website, and there is nothing you can do to resolve it.</li>
+ <li>You can notify the website’s administrator about the problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Advanced…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label><b>%1$s</b> has a security policy called HTTP Strict Transport Security (HSTS), which means that <b>%2$s</b> can only connect to it securely. You can’t add an exception to visit this site.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Go Back</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">The connection was interrupted</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>The browser connected successfully, but the connection was interrupted while transferring information. Please try again.</p>
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">The connection has timed out</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>The requested site did not respond to a connection request and the browser has stopped waiting for a reply.</p>
+ <ul>
+ <li>Could the server be experiencing high demand or a temporary outage? Try again later.</li>
+ <li>Are you unable to browse other sites? Check the device’s network connection.</li>
+ <li>Is your device or network protected by a firewall or proxy? Incorrect settings can interfere with Web browsing.</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Unable to connect</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Unexpected response from server</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>The site responded to the network request in an unexpected way and the browser cannot continue.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">The page isn’t redirecting properly</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>The browser has stopped trying to retrieve the requested item. The site is redirecting the request in a way that will never complete.</p>
+ <ul>
+ <li>Have you disabled or blocked cookies required by this site?</li>
+ <li>If accepting the site’s cookies does not resolve the problem, it is likely a server configuration issue and not your device.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline Mode</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>The browser is operating in its offline mode and cannot connect to the requested item.</p>
+ <ul>
+ <li>Is the device connected to an active network?</li>
+ <li>Press “Try Again” to switch to online mode and reload the page.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port restricted for security reasons</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>The requested address specified a port (e.g., <q>mozilla.org:80</q> for port 80 on mozilla.org) normally used for purposes <em>other</em> than Web browsing. The browser has cancelled the request for your protection and security.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">The connection was reset</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>The network link was interrupted while negotiating a connection. Please try again.</p>
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Unsafe File Type</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Corrupted Content Error</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Content crashed</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Content Encoding Error</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Address Not Found</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>The browser could not find the host server for the provided address.</p>
+ <ul>
+ <li>Check the address for typing errors such as
+ <strong>ww</strong>.example.com instead of
+ <strong>www</strong>.example.com.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">No internet connection</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Check your network connection or try reloading the page in a few moments.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Reload</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Invalid Address</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>The provided address is not in a recognized format. Please check the location bar for mistakes and try again.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">The address isn’t valid</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Web addresses are usually written like <strong>http://www.example.com/</strong></li>
+ <li>Make sure that you’re using forward slashes (i.e. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Unknown Protocol</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>The address specifies a protocol (e.g., <q>wxyz://</q>) the browser does not recognize, so the browser cannot properly connect to the site.</p>
+ <ul>
+ <li>Are you trying to access multimedia or other non-text services? Check the site for extra requirements.</li>
+ <li>Some protocols may require third-party software or plugins before the browser can recognize them.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">File Not Found</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Could the item have been renamed, removed, or relocated?</li>
+ <li>Is there a spelling, capitalization, or other typographical error in the address?</li>
+ <li>Do you have sufficient access permissions to the requested item?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Access to the file was denied</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>It may have been removed, moved, or file permissions may be preventing access.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy Server Refused Connection</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>The browser is configured to use a proxy server, but the proxy refused a connection.</p>
+ <ul>
+ <li>Is the browser’s proxy configuration correct? Check the settings and try again.</li>
+ <li>Does the proxy service allow connections from this network?</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy Server Not Found</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>The browser is configured to use a proxy server, but the proxy could not be found.</p>
+ <ul>
+ <li>Is the browser’s proxy configuration correct? Check the settings and try again.</li>
+ <li>Is the device connected to an active network?</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Malware site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as an attack site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Unwanted site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as serving unwanted software and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Harmful site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as a potentially harmful site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Deceptive site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>This web page at %1$s has been reported as a deceptive site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Secure Site Not Available</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[You’ve enabled HTTPS-Only Mode for enhanced security, and a HTTPS version of <em>%1$s</em> is not available.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continue to HTTP Site</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..8f9a48362f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,298 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Try Again</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Cannot Complete Request</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Additional information about this problem or error is currently unavailable.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Secure Connection Failed</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.</li>
+ <li>Please contact the web site owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Secure Connection Failed</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>This could be a problem with the server’s configuration, or it could be someone trying to impersonate the server.</li>
+ <li>If you have connected to this server successfully in the past, the error may be temporary, and you can try again later.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Advanced…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Someone could be trying to impersonate the site and you should not continue.</label>
+ <br><br>
+ <label>Web sites prove their identity via certificates. %1$s does not trust <b>%2$s</b> because its certificate issuer is unknown, the certificate is self-signed, or the server is not sending the correct intermediate certificates.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Go Back (Recommended)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Accept the Risk and Continue</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">This web site requires a secure connection.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>The page you are trying to view cannot be shown because this web site requires a secure connection.</li>
+ <li>The issue is most likely with the web site, and there is nothing you can do to resolve it.</li>
+ <li>You can notify the web site’s administrator about the problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Advanced…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> has a security policy called HTTP Strict Transport Security (HSTS), which means that <b>%2$s</b> can only connect to it securely. You can’t add an exception to visit this site. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Go Back</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">The connection was interrupted</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>The browser connected successfully, but the connection was interrupted while transferring information. Please try again.</p>
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">The connection has timed out</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>The requested site did not respond to a connection request and the browser has stopped waiting for a reply.</p>
+ <ul>
+ <li>Could the server be experiencing high demand or a temporary outage? Try again later.</li>
+ <li>Are you unable to browse other sites? Check the device’s network connection.</li>
+ <li>Is your device or network protected by a firewall or proxy? Incorrect settings can interfere with Web browsing.</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Unable to connect</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Unexpected response from server</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>The site responded to the network request in an unexpected way and the browser cannot continue.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">The page isn’t redirecting properly</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>The browser has stopped trying to retrieve the requested item. The site is redirecting the request in a way that will never complete.</p>
+ <ul>
+ <li>Have you disabled or blocked cookies required by this site?</li>
+ <li>If accepting the site’s cookies does not resolve the problem, it is likely a server configuration issue and not your device.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline Mode</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>The browser is operating in its offline mode and cannot connect to the requested item.</p>
+ <ul>
+ <li>Is the device connected to an active network?</li>
+ <li>Press “Try Again” to switch to online mode and reload the page.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port restricted for security reasons</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>The requested address specified a port (e.g., <q>mozilla.org:80</q> for port 80 on mozilla.org) normally used for purposes <em>other</em> than Web browsing. The browser has cancelled the request for your protection and security.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">The connection was reset</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>The network link was interrupted while negotiating a connection. Please try again.</p>
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Unsafe File Type</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Please contact the web site owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Corrupted Content Error</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p>
+ <ul>
+ <li>Please contact the web site owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Content crashed</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p>
+ <ul>
+ <li>Please contact the web site owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Content Encoding Error</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.</p>
+ <ul>
+ <li>Please contact the web site owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Address Not Found</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>The browser could not find the host server for the provided address.</p>
+ <ul>
+ <li>Check the address for typing errors such as
+ <strong>ww</strong>.example.com instead of
+ <strong>www</strong>.example.com.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">No internet connection</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Check your network connection or try reloading the page in a few moments.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Reload</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Invalid Address</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>The provided address is not in a recognised format. Please check the location bar for mistakes and try again.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">The address isn’t valid</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Web addresses are usually written like <strong>http://www.example.com/</strong></li>
+ <li>Make sure that you’re using forward slashes (i.e. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Unknown Protocol</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>The address specifies a protocol (e.g., <q>wxyz://</q>) the browser does not recognise, so the browser cannot properly connect to the site.</p>
+ <ul>
+ <li>Are you trying to access multimedia or other non-text services? Check the site for extra requirements.</li>
+ <li>Some protocols may require third-party software or plugins before the browser can recognise them.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">File Not Found</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Could the item have been renamed, removed, or relocated?</li>
+ <li>Is there a spelling, capitalisation, or other typographical error in the address?</li>
+ <li>Do you have sufficient access permissions to the requested item?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Access to the file was denied</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>It may have been removed, moved, or file permissions may be preventing access.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy Server Refused Connection</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>The browser is configured to use a proxy server, but the proxy refused a connection.</p>
+ <ul>
+ <li>Is the browser’s proxy configuration correct? Check the settings and try again.</li>
+ <li>Does the proxy service allow connections from this network?</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy Server Not Found</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>The browser is configured to use a proxy server, but the proxy could not be found.</p>
+ <ul>
+ <li>Is the browser’s proxy configuration correct? Check the settings and try again.</li>
+ <li>Is the device connected to an active network?</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Malware site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as an attack site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Unwanted site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as serving unwanted software and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Harmful site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as a potentially harmful site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Deceptive site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>This web page at %1$s has been reported as a deceptive site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Secure Site Not Available</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[You’ve enabled HTTPS-Only Mode for enhanced security, and a HTTPS version of <em>%1$s</em> is not available.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continue to HTTP Site</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..c8e46c7765
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-eo/strings.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Klopodi denove</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Ne eblas kompletigi la peton</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Ne disponelbas pli da informo pri tiu ĉi problemo aŭ eraro.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Malsukcesa sekura konekto</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>La paĝo, kiun vi klopodas vidi, ne povas esti montrita ĉar ne eblis kontroli la aŭtentikecon de la ricevitaj datumoj.</li>
+ <li>Bonvolu kontakti la posedantojn de la retejo por raporti al ili tiun ĉi problemon.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Malsukcesa sekura konekto</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+<li>Tio povus esti problemo en la agordo de la servilo, aŭ iu kiu klopodas preni la identecon de la servilo.</li>
+<li>Se vi iam jam sukcese konektiĝis al tiu ĉi servilo,
+la eraro povas esti tempa kaj vi povos klopodi denove poste.</li>
+</ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Spertula…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Iu eble klopodas ŝajnigi la retejon, vi ne devus daŭrigi.</label>
+ <br><br>
+ <label>Retejoj pruvas sian identecon per atestiloj. %1$s ne fidas <b>%2$s</b> ĉar la eldoninto de la atestilo ne estas konata, la atestilo estas memsubskribita aŭ la servilo ne sendas la ĝustajn interajn atestilojn.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Reen (rekomendita)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Akcepti la riskon kaj daŭrigi</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Tiu ĉi retejo postulas sekuran konekton.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La paĝo, kiun vi volas vidi, ne povas esti montrita ĉar la retejo postulas sekuran konekton.</li>
+ <li>La problemo plej verŝajne okazas en la retejo, kaj vi nenion povas fari por solvi ĝin.</li>
+ <li>Vi povas tamen sciigi la administranton de la retejo pri la problemo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Spertula…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> havas sekurecan politikon, kiun oni nomas HTTP Strict Transport Security (HSTS), kiu signifas ke <b>%2$s</b> nur povas konektiĝi per sekura konekto. Vi ne povas aldoni escepton por viziti la retejon.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Iri reen</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La konekto estis interrompita</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>La retumilo sukcese konektiĝis, sed la konekto estis interrompita dum transmeto de informoj. Bonvolu provi denove.</p>
+ <ul>
+ <li>Tiu ĉi retejo povus esti provizore ne atingebla aŭ tro okupata. Klopodu denove post kelkaj momentoj.</li>
+ <li>Se vi ne kapablas ŝargi iun ajn paĝon, kontrolu la aparatan datuman aŭ sendratan (Wi-Fi) konekton.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Limtempo por konekto atingita</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>La petita retejo ne respondis la konektan peton kaj la retumilo ĉesis atendi respondon.</p>
+ <ul>
+ <li>Ĉu eble eble la servilo estas tro okupata aŭ provizore malaktiva? Reprovu poste.</li>
+ <li>Ĉu ankaŭ aliajn retejojn vi ne povas viziti? Kontrolu la aparatan retaliron.</li>
+ <li>Ĉu via komputilo aŭ reto estas protektataj de retbarilo aŭ retperanto? Malĝustaj agordoj povas malhelpi retumon.</li>
+ <li>Ĉu ankoraŭ estas problemoj? Petu helpon al via reta administranto aŭ retprovizanto.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Ne eblas konektiĝi</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>La retejo povus esti nuntempe ne alirebla aŭ tro okupata. Bonvolu reprovi post iom da tempo.</li>
+ <li>Se vi ne povas viziti iun ajn paĝon, kontrolu la datuman aŭ sendratan (Wi-Fi) konekton de via poŝaparato.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Neatendita respondo el servilo</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>La retejo respondis la retpeton en maniero neatendita kaj la retumilo ne povas daŭrigi.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La paĝo ne redirektiĝas bone</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>La retumilo finis la klopodon akiri la petitan elementon. La retejo redirektas la peton tiamaniere ke ĝi neniam estos finita.</p>
+ <ul>
+ <li>Ĉu vi malpermesis aŭ blokis kuketojn kiuj estas postulataj de tiu ĉi retejo?</li>
+ <li>Se la akcepto de kuketoj el tiu ĉi retejo ne solvas la problemon, tre verŝajne temas pri malĝusta agordo en la servilo kaj ne en via aparato.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Malkonektita reĝimo</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>La retumilo funkcias nun en malkonektita reĝimo kaj ne povas konektiĝi al la petita elemento.</p>
+ <ul>
+ <li>Ĉu la aparato estas konektita al aktiva reto?</li>
+ <li>Bonvolu premi “Klopodi denove” por iri al konektita reĝimo kaj reŝargi la paĝon.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Aliro al pordo limigita pro sekurecaj kialoj</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>La petita retadreso enhavas pordon (e.g. <q>mozilla.org:80</q> por pordo 80 en mozilla.org) kiu normale ne estas uzata por retumado sed por <em>aliaj</em> celoj. La retlegilo, celante vian sekurecon kaj protekton, nuligis vian peton.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La konekto esti rekomencita</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>La retaliro estis interrompita dum la konekto. Bonvolu klopodi denove.</p>
+ <ul>
+ <li>La retejo povus esti provizore ne disponebla aŭ tro okupata. Klopodu denove post kelkaj momentoj.</li>
+ <li>Se vi ne kapablas ŝargi iun ajn paĝon, kontrolu la aparatan datuman aŭ sendratan (Wi-Fi) konekton.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Nesekura tipo de dosiero</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Bonvolu kontakti la retejajn posedantojn por raporti al ili tiun ĉi problemon.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Eraro pro difektita enhavo</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>La paĝo, kiun vi klopodas vidi, ne povas esti montrita ĉar okazis eraro dum la transmeto de datumoj.</p><ul><li>Bonvolu kontakti la posedantojn de la retejo por raporti al ili tiun ĉi problemon.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">La enhavo paneis</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>La paĝo, kiun vi klopodas vidi, ne povas esti montrita ĉar okazis eraro dum la transmeto de datumoj.</p><ul><li>Bonvolu kontakti la posedantojn de la retejo por raporti al ili tiun ĉi problemon.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Eraro de enkodigo de enhavo</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>La paĝo, kiun vi klopodas vidi, ne povas esti montrita ĉar ĝi uzas nevalidan aŭ nesubtenatan kompaktigon.</p>
+ <ul>
+<li>Bonvolu kontakti la posedantojn de la retejo por raporti al ili tiun ĉi problemon.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adreso ne trovita</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>La retumilo ne povis trovi la servilon asociata al la adreso indikita.</p>
+ <ul>
+ <li>Kontrolu la adreson por vidi ĉu estas tajperaro, ekzemple
+ <strong>ww</strong>.example.com anstataŭ
+ <strong>www</strong>.example.com.</li>
+ <li>Se vi ne kapablas ŝargi iun ajn paĝon, kontrolu la aparatan datuman aŭ sendratan (Wi-Fi) konekton.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Sen retaliro</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Kontrolu vian retaliron kaj klopodu reŝargi la paĝon post kelkaj momentoj.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Reŝargi</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Nevalida adreso</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>La provizita adreso havas formaton nerekoneblan. Bonvolu kontroli ĉu en la adresa strio io estas mistajpita kaj klopodu denove.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">La adreso ne estas valida</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Retadresoj estas kutime ĉi tiel skribitaj <strong>http://www.example.com/</strong></li>
+ <li>Estu certa uzi la ĝustajn oblikvajn strekojn (i.e. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Nekonata protokolo</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>La adreso specifas protokolon (ekzemple <q>wxyz://</q>) ne rekonata de la retumilo, kiu ne povas do taŭge konektiĝi al la retejo.</p>
+ <ul>
+ <li>Ĉu vi klopodas aliri aŭdvidaĵon aŭ alian servon ne tekstan? Kontrolu la retejon por scii ĉu estas apartaj postuloj.</li>
+ <li>Kelkaj protokoloj povas postuli apartajn programojn aŭ kromprogramojn antaŭ ol la retumilo povos rekoni ilin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Dosiero ne trovita</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Ĉu eble la elemento estis renomita, forigita aŭ translokita?</li>
+ <li>Ĉu estas literuma, majuskliga aŭ alia tajperaro en la adreso?</li>
+ <li>Ĉu vi havas sufiĉajn rajtojn aliri la petitan elementon?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Rifuzita aliro al dosiero</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Eble ĝi estis forigita, aŭ movita, aŭ la permesoj dosieraj evitas aliron al ĝi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Rifuzita konekto al retperanto</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>La retumilaj agordoj indikas uzon de retperanto, sed la retperanto rifuzis la konekton.</p>
+ <ul>
+ <li>Ĉu la retumilaj agordoj pri retperanto estas ĝustaj? Kontrolu la agordojn kaj klopodu denove.</li>
+ <li>Ĉu la retperanta servo akceptas konektojn el tiu ĉi retejo?</li>
+ <li>Ĉu ankoraŭ estas problemoj? Kontaktu vian retan administraton aŭ retprovizanton por ricevi helpon.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Retperanto ne trovita</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>La retumilaj agordoj indikas uzon de retperanto, sed la retperanto ne estis trovita.</p>
+ <ul>
+ <li>Ĉu la retumilaj agordoj pri retperanto estas ĝustaj? Kontrolu la agordojn kaj klopodu denove.</li>
+ <li>Ĉu la aparato estas konektita al aktiva reto?</li>
+ <li>Ĉu ankoraŭ estas problemoj? Kontaktu vian retan administraton aŭ retprovizanton por ricevi helpon.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problemo kun retejo kiu enhavas malicajn programojn</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>La retejo ĉe %1$s estis denuncita kiel ataka retejo, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problemo kun programtruda retejo</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>La retejo ĉe %1$s estis denuncita kiel programtruda, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problemo kun danĝera retejo</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>La retejo ĉe %1$s estis denuncita kiel eble danĝera retejo, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problemo kun trompa retejo</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Tiu ĉi paĝo ĉe %1$s estis denuncita kiel trompa retejo, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sekura retejo ne disponebla</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Vi ŝaltis la HTTPS-nuran reĝimon por plibonigita sekureco, kaj versio HTTPS de <em>%1$s</em> ne estas disponebla.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Daŭrigi al retejo HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..4baa9149df
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,290 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Probar de nuevo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">No se puede completar el pedido</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>En este momento no hay información adicional disponible para este problema o error.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Falló la conexión segura</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>La página que estás intentando ver no se puede mostrar porque no se pudo verificar la autenticidad de los datos recibidos.</li>
+ <li>Por favor contactate con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Falló la conexión segura</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+<li>Esto podría ser un problema con la configuración del servidor o podría ser alguien tratando de hacerse pasar por el servidor.</li>
+<li>Si te conectaste sin problemas a este servidor en el pasado, el error puede ser temporal y podés probar de nuevo más tarde.</li>
+</ul>
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzadas…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+        <label>Alguien podría estar intentando imitar el sitio y no deberías continuar. </label>
+        <br><br>
+        <label>Los sitios web prueban su identidad mediante certificados. %1$s no confía en <b>%2$s</b> porque el emisor del certificado es desconocido, el certificado está autofirmado o el servidor no envía los certificados intermedios correctos.</label>
+    ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Retroceder (recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceptar el riesgo y continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Este sitio web requiere una conexión segura.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La página que intentás ver no se puede mostrar porque este sitio web requiere una conexión segura.</li>
+ <li>Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que puedas hacer para resolverlo.</li>
+ <li>Podés avisarle al administrador del sitio web sobre el problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanzadas…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), lo que significa que <b>%2$s</b> solo puede conectarse de forma segura. No podés añadir una excepción para visitar este sitio.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Retroceder</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Se interrumpió la conexión </string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería la información. Volvé a probar.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Volvé a probar en unos minutos.</li>
+ <li>Si no podés cargar ninguna página, revisa la conexión wifi o de datos de tu dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">La conexión tardó demasiado tiempo</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>El sitio solicitado no respondió a una pedido de conexión y el navegador dejó de esperar una respuesta.</p>
+ <ul>
+ <li>¿El servidor podría estar experimentando una alta demanda o un corte temporal? Volvé a probar más tarde.</li>
+ <li>¿No podés navegar por otros sitios? Verificá la conexión de red del dispositivo.</li>
+ <li>¿Tu red o dispositivo está protegido por un firewall o un proxy? Una configuración incorrecta puede interferir con la navegación web.</li>
+ <li>¿Todavía tenés problemas? Consultá con el administrador de la red o el proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">No se puede conectar</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>El sitio puede estar temporariamente inaccesible o demasiado ocupado. Intentá nuevamente en un rato.</li>
+ <li>Si no podés cargar ninguna página, verificá la conexión de datos o Wi-Fi de tu dispositivo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respuesta inesperada del servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>El sitio respondió al pedido de la red de una forma inesperada y el navegador no puede continuar.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La página no se redirecciona correctamente</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>El navegador dejó de tratar de transferir el ítem solicitado. El sitio está redirigiendo el pedido en una manera que nunca se completará.</p>
+ <ul>
+ <li>¿Deshabilitaste o bloqueaste cookies requeridas por este sitio?</li>
+ <li> Si aceptar las cookies del sitio no resuelve el problema, seguramente es un problema de configuración del servidor y no de tu dispositivo.</li>
+
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo sin conexión</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>El navegador está funcionando en el modo sin conexión y no puede conectarse al ítem solicitado.</p><ul><li>¿El dispositivo está conectado a una red activa?</li><li>Presioná “Intentar de nuevo” para volver al modo con conexión y recargar la página.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Restricción del puerto por razones de seguridad</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>La dirección solicitada especificaba un puerto (p. ej., <q>mozilla.org:80</q> para el puerto 80 de mozilla.org) que suele usarse para propósitos <em>distintos</em> a navegar por Internet. El navegador canceló la solicitud para tu protección y seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Se restableció la conexión</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería la información. Volvé a probar.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Volvé a probar en unos minutos.</li>
+ <li>Si no podés cargar ninguna página, revisá la conexión wifi o de datos de tu dispositivo móvil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de archivo no seguro</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Contactate con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de contenido corrupto</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>La página que estás tratando de ver no puede ser mostrada porque se detectó un error en la transmisión de datos.</p>
+ <ul>
+ <li>Contactá a los dueños del sitio web para informarles sobre este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">El contenido falló</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>La página que estás tratando de ver no puede ser mostrada porque se detectó un error en la transmisión de datos.</p>
+ <ul>
+ <li>Contactá a los dueños del sitio web para informarles sobre este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de codificación de contenido</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>La página que estás tratando de ver no puede mostrarse porque usa una forma de compresión inválida o no soportada.</p>
+ <ul>
+ <li>Contactá a los dueños del sitio web para informarles sobre este problema.</li>
+
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">No se encontró la dirección</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>El navegador no pudo encontrar el servidor de la dirección provista.</p>
+ <ul>
+ <li>Verificá si la dirección no tiene errores de tipeo como
+ <strong>ww</strong>.example.com en lugar de
+ <strong>www</strong>.example.com.</li>
+ <li>Si no podés cargar ninguna página, verificá la conexión de datos o Wi-Fi de tu dispositivo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Sin conexión a Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verificá tu conexión a la red o intentá volver a cargar la página en un ratito.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">La dirección no es válida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>La dirección provista no tiene un formato reconocible. Mirá si no hay errores en la barra de direcciones e intentá nuevamente.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">La dirección no es válida</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Usualmente las direcciones web se escriben como <strong>http://www.example.com/</strong></li>
+ <li>Asegurate de estar usando las barras correctas (ej: <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo desconocido</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>La dirección especifica un protocolo (ej: <q>wxyz://</q>) que el navegador no reconoce, así que no puede conectarse adecuadamente al sitio.</p>
+ <ul>
+ <li>¿Estás tratando de acceder a multimedia o a otros servicios que no son de texto? Verifiá el sitio para requerimientos extra.</li>
+ <li>Algunos protocolos pueden requerir software de terceros o plugins antes que el navegador pueda reconocerlos.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">No se encontró el archivo</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>¿Puede ser que el ítem haya sido renombrado, removido o reubicado?</li>
+ <li>¿Hay un error de ortografía, mayúsculas o algún error tipográfico en la dirección?</li>
+ <li>¿Tenés suficientes permisos de acceso al ítem solicitado?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Se denegó el acceso al archivo</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Puede haber sido eliminado, movido o los permisos del archivo pueden evitar el acceso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">El servidor proxy rechazó la conexión</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.</p>
+<ul>
+ <li>¿Es correcta la configuración del proxy? Verificá la configuración y volvé a intentarlo.</li>
+ <li>¿El servidor proxy permite conexiones desde esta red?</li>
+ <li>¿Aún tenés problemas? Consultá con el administrador de tu red o el proveedor de internet.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">No se encontró el servidor proxy</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>El navegador está configurado para usar un servidor proxy, pero el proxy no se encontró el servidor.</p><ul><li>¿La configuración del proxy del navegador es correcta? Verificá la configuración y probá de nuevo.</li><li>¿El dispositivo está conectado a una red activa?</li><li>¿Todavía tenés problemas? Consultá con tu administrador de red o tu proveedor de Internet para recibir asistencia.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema del sitio malicioso</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Se informó el sitio %1$s como un sitio de ataque y se bloqueó basado en tus preferencias de seguridad.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sitio no deseado</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Se informó el sitio %1$s por instalar software no deseado y se bloqueó basado en tus preferencias de seguridad.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sitio dañino</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Se informó el sitio %1$s como potencialmente dañino y se bloqueó basado en tus preferencias de seguridad.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de sitio engañoso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Se informó la página web %1$s como un sitio engañoso y se bloqueó basado en tus preferencias de seguridad.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">No hay sitios seguros disponibles</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Habilitaste el modo solo HTTPS para aumentar la seguridad y una versión HTTPS de <em> %1$s </em> no está disponible.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar al sitio HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..8014572f23
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,322 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Volver a intentarlo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">No se puede completar solicitud</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Actualmente no hay información adicional disponible para este problema o error.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Falló la conexión segura</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>La página que intentas ver no puede ser mostrada porque la autenticidad de los datos recibidos no pudo ser verificada.</li>
+ <li>Por favor, contacta a los dueños del sitio para avisarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Falló la conexión segura</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Esto podría ser un problema con la configuración del servidor, o que alguien esté intentando suplantar al servidor.</li>
+ <li>Si te pudiste conectar con éxito en el pasado, el error puede ser temporal, y puedes volver a intentarlo en un rato.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzado…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Alguien podría estar intentando hacerse pasar por el sitio y no debieras continuar.</label>
+ <br><br>
+ <label>Los sitios prueban su identidad a través de certificados. %1$s no confñia en <b>%2$s</b> porque el emisor de su certificado es desconocido, el certificado fue auto-firmado o el servidor no está enviando los certificados intermediarios correctos.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Retroceder (recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceptar el riesgo y continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Este sitio web requiere una conexión segura.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La página que estás intentando ver no puede ser mostrada porque este sitio web requiere una conexión segura.</li>
+ <li>Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que se puedas hacer para resolverlo.</li>
+ <li>Puedes notificar al administrador del sitio web acerca de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanzado…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), lo que se traduce en que <b>%2$s</b> solo puede conectarse de forma segura. No puedes añadir una excepción para visitar este sitio.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Retroceder</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La conexión fue interrumpida</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>El navegador se conectó exitosamente, pero la conexión fue interrumpida mientras se transfería información. Por favor, vuelve a intentarlo.</p>
+ <ul>
+ <li>El sitio podría estar temporalmente no disponible o muy ocupado. Vuelve a intentarlo en un rato.</li>
+ <li>Si no puedes cargar ninguna página, revisa los datos de tu dispositivo o conexión Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">La conexión ha caducado</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.</p>
+ <ul>
+ <li>¿Podría estar experimentando el servidor alta demanda o un corte temporal? Vuelve a intentarlo en un rato.</li>
+ <li>¿No puedes navegar por otros sitios? Comprueba la conexión de red del computador.</li>
+ <li>¿Tu computador está protegido por un proxy o un firewall? Una configuración incorrecta puede interferir con la navegación.</li>
+ <li>¿Todavía con problemas? Consulta con tu administrador de red o proveedor de Internet para asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">No se pudo conectar</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>El sitio podría estar temporalmente no disponible o demasiado ocupado. Vuelve a intentarlo en un rato.</li>
+ <li>Si no puedes cargar ninguna página, comprueba el servicio de datos de tu dispositivo o la conexión Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respuesta inesperada del servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>El sitio respondió a la solicitud de red en una forma inesperada y el navegador no puede continuar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La página no está redirigiendo adecuadamente</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>El navegador se ha detenido intentando recuperar el elemento solicitado. El sitio está redirigiendo la solicitud de una forma que nunca se va a completar.</p>
+ <ul>
+ <li>¿Tiene desactivadas o bloqueadas las cookies requeridas por este sitio?</li>
+ <li>Si aceptar las cookies del sitio no resuelve el problema, probablemente es un problema de configuración del servidor y no de su computador.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo sin conexión</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>El navegador está actualmente funcionando en el modo sin conexión a la red y no puede conectarse al ítem solicitado.</p>
+ <ul>
+ <li>¿Está la computadora conectada a una red activa?</li>
+ <li>Presiona “Volver a intentarlo” para volver al modo con conexión y recargar la página.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Puerto restringido por razones de seguridad</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>La dirección solicitada especificaba un puerto (ej. <q>mozilla.org:80</q> para el puerto 80 de mozilla.org) usado normalmente para propósitos <em>distintos</em> a navegar por la web. El navegador ha cancelado la solicitud por su protección y seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La conexión fue reiniciada</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>El enlace de red fue interrumpido mientras se negociaba una conexión. Por favor, vuelve a intentarlo.</p>
+ <ul>
+ <li>El sitio podría estar temporalmente no disponible o muy ocupado. Vuelve a intentarlo en un rato.</li>
+ <li>Si no puedes cargar ninguna página, revisa los datos de tu dispositivo o conexión Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de archivo inseguro</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Por favor, contacta a los dueños del sitio para avisarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de contenido corrupto</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>La página que estás intentando ver no puede ser mostrada por que se detectó un error en la transmisión de datos.</p>
+ <ul>
+ <li>Por favor, contacta a los dueños del sitio para avisarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Fallo del contenido</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>La página que estás intentando ver no puede ser mostrada por que se detectó un error en la transmisión de datos.</p>
+ <ul>
+ <li>Por favor, contacta a los dueños del sitio para avisarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de codificación de contenido</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>La página que estás intentando ver no puede ser mostrada porque utiliza un formato de compresión no válido o no admitido.</p>
+ <ul>
+ <li>Por favor, contacta a los dueños del sitio para avisarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Dirección no encontrada</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>El navegador no pudo encontrar el servidor de la dirección proporcionada.</p>
+ <ul>
+ <li>Revisa la dirección por errores de tipeo como
+ <strong>ww</strong>. example.com en lugar de
+ <strong>www</strong>.example.com</li>
+ <li>Si no puedes cargar ninguna página, revisa los datos de tu dispositivo o conexión Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Sin conexión a internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Revisa tu conexión de red o vuelve a intentar cargar la página en un rato.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Dirección inválida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>La dirección proporcionada no está en un formato reconocido. Por favor, comprueba errores en la barra de direcciones y vuelve a intentarlo.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">La dirección no es válida</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Las direcciones Web usualmente son escritas como <strong>http://www.example.com/</strong></li>
+ <li>Asegúrate de que estás usando barras oblicuas (por ejemplo <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo desconocido</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>La dirección especifica un protocolo (ej. <q>wxyz://</q>) que el navegador no reconoce, por esto el navegador no puede conectar adecuadamente al sitio.</p>
+ <ul>
+ <li>¿Está intentando acceder a servicios multimedia u otros que no sean de texto? Revise el sitio por requerimientos extra.</li>
+ <li>Algunos protocolos pueden requerir software de terceros o complementos antes de que el navegador pueda reconocerlos.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Archivo no encontrado</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>¿Es posible que el elemento haya sido renombrado, eliminado o cambiado de ubicación?</li>
+ <li>¿Hay algún error de ortografía, mayúsculas o cualquier otro error al escribir la dirección?</li>
+ <li>¿Tienes privilegios de acceso suficientes para el elemento solicitado?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">El acceso al archivo fue denegado</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Puede haber sido removido o movido, o puede que los permisos del archivo prevengan el acceso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">El servidor proxy rechazó la conexión</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.</p>
+ <ul>
+ <li>¿Es correcta la configuración del proxy del navegador? Comprueba la configuración y vuelve a intentarlo.</li>
+ <li>¿Permite el servicio proxy conexiones desde esta red?</li>
+ <li>¿Todavía con problemas? Consulta con tu administrador de red o proveedor de Internet para asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Servidor Proxy no encontrado</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>El navegador está configurado para usar un servidor proxy, pero no se pudo encontrar el servidor proxy.</p>
+ <ul>
+ <li>¿Es correcta la configuración del proxy del navegador? Comprueba la configuración y vuelve a intentarlo.</li>
+ <li>¿El computador está conectado a una red activa?</li>
+ <li>¿Todavía con problemas? Consulta con tu administrador de red o proveedor de Internet para asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de sitio malicioso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>El sitio en %1$s ha sido reportado como un sitio atacante y ha sido bloqueado en base a tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sitio no deseado</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>El sitio en %1$s ha sido reportado por entregar software indeseado y ha sido bloqueado en base a tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sitio peligroso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>El sitio en %1$s ha sido reportado como un sitio potencialmente peligroso y ha sido bloqueado en base a tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de sitio fraudulento</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>El sitio en %1$s ha sido reportado como un sitio fraudulento y ha sido bloqueado en base a tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sitio seguro no disponible</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Has activado el modo de solo HTTPS para una seguridad mejorada, y una versión HTTPS de <em>%1$s</em> no se encuentra disponible.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar al sitio HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..9a5339426c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,303 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Volver a intentarlo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">No se puede completar la petición</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Actualmente no hay información adicional disponible para este problema o error.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Conexión segura fallida</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>La página que estás intentando ver no se puede mostrar porque no se ha podido verificar la autenticidad de los datos recibidos.</li>
+ <li>Contacta con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Conexión segura fallida</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Esto podría deberse a un problema con la configuración del servidor, o podría ser alguien intentando hacerse pasar por el servidor.</li>
+ <li>Si ya te habías conectado antes a este servidor, el error podría ser temporal, y podrás volver a intentarlo más tarde.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzadas…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+        <label>Alguien podría estar intentando hacerse pasar por el sitio y no deberías continuar. </label>
+        <br><br>
+        <label>Los sitios web prueban su identidad mediante certificados. %1$s no confía en <b>%2$s</b> porque el emisor del certificado es desconocido, el certificado está autofirmado o el servidor no envía los certificados intermedios correctos.</label>
+    ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Retroceder (recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceptar el riesgo y continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Este sitio web requiere una conexión segura.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La página que estás intentando ver no se puede mostrar porque este sitio web requiere una conexión segura.</li>
+ <li>Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que se pueda hacer para resolverlo.</li>
+ <li>Puedes notificar al administrador del sitio web sobre este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanzadas…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label><b>%1$s</b> tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), que significa que <b>%2$s</b> solo puede conectarse a él de forma segura. No puedes añadir una excepción para visitar este sitio.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Retroceder</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La conexión ha sido interrumpida</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería la información. Vuelve a intentarlo.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o de datos de tu dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">La conexión ha caducado</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.</p>
+ <ul>
+ <li>¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelve a intentarlo más tarde.</li>
+ <li>¿No puedes navegar por otros sitios? Comprueba la conexión de red del equipo.</li>
+ <li>¿Tu red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta puede interferir con la navegación web.</li>
+ <li>¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">No se puede conectar</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respuesta inesperada del servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>El sitio respondió a la solicitud de red de una forma inesperada y el navegador no puede continuar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La página no está redirigiendo adecuadamente</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>El navegador se ha detenido intentando recuperar el elemento solicitado. El sitio está redirigiendo la solicitud de una forma que nunca se va a completar.</p>
+ <ul>
+ <li>¿Tienes desactivadas o bloqueadas las cookies que necesita este sitio?</li>
+ <li>Si aceptar las cookies del sitio no resuelve el problema, es probable que sea un problema de configuración del servidor y no del equipo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo sin conexión</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>El navegador está operando en modo sin conexión y no puede conectarse con el elemento solicitado.</p>
+ <ul>
+ <li>¿Está conectado el equipo a una red activa?</li>
+ <li>Pulsa "Volver a intentarlo" para pasar al modo con conexión y recargar la página.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Puerto restringido por razones de seguridad</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>La dirección solicitada especificaba un puerto (p. ej., <q>mozilla.org:80</q> para el puerto 80 de mozilla.org) que suele usarse para propósitos <em>distintos</em> a navegar por Internet. El navegador ha cancelado la solicitud para tu protección y seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La conexión ha sido reiniciada</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>El enlace con la red se interrumpió mientras se negociaba una conexión. Vuelve a intentarlo.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de archivo no seguro</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de contenido dañado</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.</p>
+ <ul>
+ <li>Ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Contenido bloqueado</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.</p>
+ <ul>
+ <li>Ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de codificación de contenido</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>La página que estás intentando ver no puede mostrarse porque usa una forma no válida o no admitida de compresión.</p>
+ <ul>
+ <li>Ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">No se encontró la dirección</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>El navegador no pudo encontrar el servidor para la dirección proporcionada.</p>
+ <ul>
+ <li>Comprueba que la dirección no contenga errores, por ejemplo, <strong>ww</strong>.ejemplo.com en lugar de <strong>www</strong>.ejemplo.com.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">No hay conexión a Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifica tu conexión de red o intenta volver a cargar la página en unos momentos.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">La dirección no es válida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>La dirección proporcionada no está en un formato reconocido. Comprueba si hay errores en la barra de direcciones y vuelve a intentarlo.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">La dirección no es válida</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Las direcciones web suelen escribirse así: <strong>http://www.ejemplo.com/</strong></li>
+ <li>Asegúrate de estar usando barras inclinadas hacia adelante (p. ej., <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo desconocido</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>La dirección especifica un protocolo (p. ej., <q>wxyz://</q>) que el navegador no reconoce, así que el navegador no puede conectarse correctamente con el sitio.</p>
+ <ul>
+ <li>¿Estás intentando acceder a contenido multimedia u otros servicios que no son de texto? Comprueba los requisitos adicionales del sitio.</li>
+ <li>Algunos protocolos pueden necesitar software o plugins de terceros antes de que el navegador pueda reconocerlos.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Archivo no encontrado</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>¿Es posible que el elemento se haya renombrado, eliminado o cambiado de ruta?</li>
+ <li>¿Hay algún error de ortografía, de uso de mayúsculas o de cualquier otro tipo en la dirección?</li>
+ <li>¿Tienes privilegios de acceso suficientes para el elemento solicitado?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">El acceso al archivo ha sido denegado</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Puede haberse eliminado o movido, o sus permisos de archivo pueden estar impidiendo el acceso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">El servidor proxy rechazó la conexión</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.</p>
+ <ul>
+ <li>¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.</li>
+ <li>¿Permite el servicio proxy conexiones desde esta red?</li>
+ <li>¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">No se encontró el servidor proxy</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>El navegador está configurado para usar un servidor proxy, pero no se pudo encontrar el servidor proxy.</p>
+ <ul>
+ <li>¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.</li>
+ <li>¿Está conectado el equipo a una red activa?</li>
+ <li>¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de sitio de malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>El sitio en %1$s se ha identificado como un sitio atacante y se ha bloqueado, siguiendo tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sitio no deseado</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>El sitio en %1$s se ha identificado como un sitio que ofrece software no deseado y se ha bloqueado, siguiendo tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sitio dañino</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>El sitio en %1$s se ha identificado como un sitio potencialmente dañino y se ha bloqueado, siguiendo tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de sitio engañoso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>La página web en %1$s se ha identificado como un sitio engañoso y se ha bloqueado, siguiendo tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sitio seguro no disponible</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Has activado el modo solo HTTPS para mejorar la seguridad pero no está disponible una versión HTTPS de <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar al sitio HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..2c078a7d47
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,252 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Intenta de nuevo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">No se puede completar la solicitud</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Información adicional acerca de este problema o error no está actualmente disponible.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">La conexión segura ha fallado</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>La página que estás intentando ver no se puede mostrar porque no se ha podido verificar la autenticidad de los datos recibidos.</li>
+ <li>Por favor, ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">La conexión segura ha fallado</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Puede haber un problema en la configuración del servidor, o bien alguien podría estar intentando suplantar el servidor.</li>
+ <li>Si ya te has conectado a este servidor con éxito, el error puede ser temporal, por lo que puedes intentar nuevamente en breve.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzado…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Alguien podría estar intentando hacerse pasar por el sitio y no deberías continuar. </label>
+        <br><br>
+        <label>Los sitios web prueban su identidad mediante certificados. %1$s no confía en <b>%2$s</b> porque el emisor del certificado es desconocido, el certificado está autofirmado o el servidor no envía los certificados intermedios correctos.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Regresar (Recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceptar el riesgo y continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Este sitio web requiere una conexión segura.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La página que estás tratando de ver no se puede mostrar porque este sitio web requiere una conexión segura.</li>
+ <li>Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que se pueda hacer para resolverlo.</li>
+ <li>Puedes notificar al administrador del sitio web sobre este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Opciones avanzadas…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), lo que significa que <b>%2$s</b> solo puede conectarse de forma segura. No puedes agregar una excepción para visitar este sitio. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Regresar</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La conexión fue interrumpida</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería información. Por favor, intenta de nuevo.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o los datos de tu dispositivo móvil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">La conexión ha expirado</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.</p>
+ <ul>
+ <li>¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelve a intentarlo más tarde.</li>
+ <li>¿No puedes navegar por otros sitios? Comprueba la conexión de red del equipo.</li>
+ <li>¿Tu red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta puede interferir con la navegación web.</li>
+ <li>¿Todavía con problemas? Consulta con su administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">No se puede conectar</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o los datos de tu dispositivo móvil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respuesta inesperada del servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>El sitio ha respondido a la solicitud de red de una forma inesperada y el navegador no puede continuar.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La página no está redirigiendo adecuadamente</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>El navegador se ha detenido intentando recuperar el elemento solicitado. El sitio está redirigiendo la solicitud de una forma que nunca se va a completar.</p>
+ <ul>
+ <li>¿Tienes desactivadas o bloqueadas las cookies requeridas por este sitio?</li>
+ <li>Si aceptar las cookies del sitio no resuelve el problema, es probable que sea un problema de configuración del servidor y no de tu equipo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo sin conexión</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>El navegador está operando en modo sin conexión y no puede conectarse con el elemento solicitado.</p>
+ <ul>
+ <li>¿Estás conectado el equipo a una red activa?</li>
+ <li>Presiona "Volver a intentarlo" para pasar al modo con conexión y recargar la página.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Puerto restringido por razones de seguridad</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>La dirección solicitada especificaba un puerto (p. ej., <q>mozilla.org:80</q> para el puerto 80 de mozilla.org) que suele usarse para propósitos <em>distintos</em> a navegar por Internet. El navegador ha cancelado la solicitud para tu protección y seguridad.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La conexión se ha reiniciado</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>El enlace con la red se interrumpió mientras se negociaba una conexión. Por favor, intenta de nuevo.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o los datos de tu dispositivo móvil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de archivo inseguro</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Por favor, comunícate con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de contenido dañado</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.</p>
+ <ul>
+ <li>Por favor, comunícate con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Contenido bloqueado</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de datos.</p>
+ <ul>
+ <li>Por favor, comunícate con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de codificación de contenido</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>La página que estás intentando ver no puede mostrarse porque usa una forma no válida o no admitida de comprensión.</p>
+ <ul>
+ <li>Por favor, comunícate con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Dirección no encontrada</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>El navegador no pudo encontrar el servidor para dirección proporcionada.</p>
+ <ul>
+ <li>Verifica que la dirección no contenga error, por ejemplo:
+ <strong>ww</strong>.ejemplo.com en lugar de
+ <strong>www</strong>.ejemplo.com.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o los datos de tu dispositivo móvil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">No hay conexión a internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifica tu conexión de red o intenta volver a cargar la página en unos momentos.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Dirección inválida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>La dirección proporcionada no está en un formato reconocido. Comprueba si hay errores en la barra de direcciones y vuelve a intentarlo.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">La dirección no es válida</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Las direcciones web suelen escribirse así: <strong>http://www.ejemplo.com/</strong></li>
+ <li>Asegúrate de estar usando barras inclinadas hacia adelante (p. ej., <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo desconocido</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>La dirección especifica un protocolo (p. ej., <q>wxyz://</q>) que el navegador no reconoce, así que el navegador no puede conectarse correctamente con el sitio.</p>
+ <ul>
+ <li>¿Estás intentando acceder a contenido multimedia u otros servicios que no son de texto? Comprueba los requisitos adicionales del sitio.</li>
+ <li>Algunos protocolos pueden necesitar software o plugins de terceros antes de que el navegador pueda reconocerlos.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Archivo no encontrado</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>¿Es posible que el elemento se haya renombrado, eliminado o cambiado de ruta?</li>
+ <li>¿Hay algún error de ortografía, de uso de mayúsculas o de cualquier otro tipo en la dirección?</li>
+ <li>¿Tienes privilegios de acceso suficientes para el elemento solicitado?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">El acceso al archivo fue denegado</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Puede haberse eliminado o movido, o los permisos del archivo pueden estar impidiendo el acceso.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">El servidor proxy rechazó la conexión</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.</p>
+ <ul>
+ <li>¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.</li>
+ <li>¿Permite el servicio proxy conexiones desde esta red?</li>
+ <li>¿Todavía con problemas? Consulta con tu administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Servidor proxy no encontrado</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>El navegador está configurado para usar un servidor proxy, pero no se pudo encontrar el servidor proxy.</p>
+ <ul>
+ <li>¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.</li>
+ <li>¿Está conectado el equipo a una red activa?</li>
+ <li>¿Todavía con problemas? Consulta con su administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de sitio malicioso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>El sitio en %1$s se ha identificado como un sitio atacante y se ha bloqueado de acuerdo con tus preferencias de seguridad.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sitio no deseado</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>El sitio en %1$s se ha identificado como un sitio que ofrece software no deseado y se ha bloqueado de acuerdo con tus preferencias de seguridad.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sitio dañino</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>El sitio en %1$s se ha identificado como un sitio potencialmente dañino y se ha bloqueado de acuerdo con tus preferencias de seguridad.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de sitio engañoso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>La página web en %1$s se ha identificado como un sitio engañoso y se ha bloqueado de acuerdo con tus preferencias de seguridad.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sitio seguro no disponible</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Has habilitado el modo solo HTTPS para mejorar la seguridad pero no está disponible una versión HTTPS de <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar al sitio HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..9a5339426c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-es/strings.xml
@@ -0,0 +1,303 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Volver a intentarlo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">No se puede completar la petición</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Actualmente no hay información adicional disponible para este problema o error.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Conexión segura fallida</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>La página que estás intentando ver no se puede mostrar porque no se ha podido verificar la autenticidad de los datos recibidos.</li>
+ <li>Contacta con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Conexión segura fallida</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Esto podría deberse a un problema con la configuración del servidor, o podría ser alguien intentando hacerse pasar por el servidor.</li>
+ <li>Si ya te habías conectado antes a este servidor, el error podría ser temporal, y podrás volver a intentarlo más tarde.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzadas…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+        <label>Alguien podría estar intentando hacerse pasar por el sitio y no deberías continuar. </label>
+        <br><br>
+        <label>Los sitios web prueban su identidad mediante certificados. %1$s no confía en <b>%2$s</b> porque el emisor del certificado es desconocido, el certificado está autofirmado o el servidor no envía los certificados intermedios correctos.</label>
+    ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Retroceder (recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceptar el riesgo y continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Este sitio web requiere una conexión segura.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La página que estás intentando ver no se puede mostrar porque este sitio web requiere una conexión segura.</li>
+ <li>Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que se pueda hacer para resolverlo.</li>
+ <li>Puedes notificar al administrador del sitio web sobre este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanzadas…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label><b>%1$s</b> tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), que significa que <b>%2$s</b> solo puede conectarse a él de forma segura. No puedes añadir una excepción para visitar este sitio.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Retroceder</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La conexión ha sido interrumpida</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería la información. Vuelve a intentarlo.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o de datos de tu dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">La conexión ha caducado</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.</p>
+ <ul>
+ <li>¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelve a intentarlo más tarde.</li>
+ <li>¿No puedes navegar por otros sitios? Comprueba la conexión de red del equipo.</li>
+ <li>¿Tu red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta puede interferir con la navegación web.</li>
+ <li>¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">No se puede conectar</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respuesta inesperada del servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>El sitio respondió a la solicitud de red de una forma inesperada y el navegador no puede continuar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La página no está redirigiendo adecuadamente</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>El navegador se ha detenido intentando recuperar el elemento solicitado. El sitio está redirigiendo la solicitud de una forma que nunca se va a completar.</p>
+ <ul>
+ <li>¿Tienes desactivadas o bloqueadas las cookies que necesita este sitio?</li>
+ <li>Si aceptar las cookies del sitio no resuelve el problema, es probable que sea un problema de configuración del servidor y no del equipo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo sin conexión</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>El navegador está operando en modo sin conexión y no puede conectarse con el elemento solicitado.</p>
+ <ul>
+ <li>¿Está conectado el equipo a una red activa?</li>
+ <li>Pulsa "Volver a intentarlo" para pasar al modo con conexión y recargar la página.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Puerto restringido por razones de seguridad</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>La dirección solicitada especificaba un puerto (p. ej., <q>mozilla.org:80</q> para el puerto 80 de mozilla.org) que suele usarse para propósitos <em>distintos</em> a navegar por Internet. El navegador ha cancelado la solicitud para tu protección y seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La conexión ha sido reiniciada</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>El enlace con la red se interrumpió mientras se negociaba una conexión. Vuelve a intentarlo.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de archivo no seguro</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de contenido dañado</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.</p>
+ <ul>
+ <li>Ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Contenido bloqueado</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.</p>
+ <ul>
+ <li>Ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de codificación de contenido</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>La página que estás intentando ver no puede mostrarse porque usa una forma no válida o no admitida de compresión.</p>
+ <ul>
+ <li>Ponte en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">No se encontró la dirección</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>El navegador no pudo encontrar el servidor para la dirección proporcionada.</p>
+ <ul>
+ <li>Comprueba que la dirección no contenga errores, por ejemplo, <strong>ww</strong>.ejemplo.com en lugar de <strong>www</strong>.ejemplo.com.</li>
+ <li>Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">No hay conexión a Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifica tu conexión de red o intenta volver a cargar la página en unos momentos.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">La dirección no es válida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>La dirección proporcionada no está en un formato reconocido. Comprueba si hay errores en la barra de direcciones y vuelve a intentarlo.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">La dirección no es válida</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Las direcciones web suelen escribirse así: <strong>http://www.ejemplo.com/</strong></li>
+ <li>Asegúrate de estar usando barras inclinadas hacia adelante (p. ej., <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo desconocido</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>La dirección especifica un protocolo (p. ej., <q>wxyz://</q>) que el navegador no reconoce, así que el navegador no puede conectarse correctamente con el sitio.</p>
+ <ul>
+ <li>¿Estás intentando acceder a contenido multimedia u otros servicios que no son de texto? Comprueba los requisitos adicionales del sitio.</li>
+ <li>Algunos protocolos pueden necesitar software o plugins de terceros antes de que el navegador pueda reconocerlos.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Archivo no encontrado</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>¿Es posible que el elemento se haya renombrado, eliminado o cambiado de ruta?</li>
+ <li>¿Hay algún error de ortografía, de uso de mayúsculas o de cualquier otro tipo en la dirección?</li>
+ <li>¿Tienes privilegios de acceso suficientes para el elemento solicitado?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">El acceso al archivo ha sido denegado</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Puede haberse eliminado o movido, o sus permisos de archivo pueden estar impidiendo el acceso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">El servidor proxy rechazó la conexión</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.</p>
+ <ul>
+ <li>¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.</li>
+ <li>¿Permite el servicio proxy conexiones desde esta red?</li>
+ <li>¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">No se encontró el servidor proxy</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>El navegador está configurado para usar un servidor proxy, pero no se pudo encontrar el servidor proxy.</p>
+ <ul>
+ <li>¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.</li>
+ <li>¿Está conectado el equipo a una red activa?</li>
+ <li>¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de sitio de malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>El sitio en %1$s se ha identificado como un sitio atacante y se ha bloqueado, siguiendo tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sitio no deseado</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>El sitio en %1$s se ha identificado como un sitio que ofrece software no deseado y se ha bloqueado, siguiendo tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sitio dañino</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>El sitio en %1$s se ha identificado como un sitio potencialmente dañino y se ha bloqueado, siguiendo tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de sitio engañoso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>La página web en %1$s se ha identificado como un sitio engañoso y se ha bloqueado, siguiendo tus preferencias de seguridad.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sitio seguro no disponible</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Has activado el modo solo HTTPS para mejorar la seguridad pero no está disponible una versión HTTPS de <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar al sitio HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..50c02b28b1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-et/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Proovi uuesti</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Päringut pole võimalik lõpetada</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Selle probleemi kohta pole lisainformatsiooni.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Turvalise ühenduse viga</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Soovitud veebilehte pole võimalik näidata, kuna saadud andmete autentsust polnud võimalik kontrollida.</li>
+ <li>Palun võta veebilehe omanikuga ühendust ja teavita teda probleemist.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Turvalise ühenduse viga</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Tegemist võib olla serveri seadistuste probleemiga või siis üritab keegi antud
+serverina välja paista.</li>
+ <li>Kui sa oled varem selle serveriga edukalt ühendunud, siis võib olla tegemist ajutise veaga
+ja sa võid hiljem uuesti proovida.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Edasijõudnuile…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Keegi võib üritada selle saidina välja paista ja sa ei peaks jätkama.</label>
+ <br><br>
+ <label>Saidid tõestavad oma identiteeti sertide abil. %1$s ei usalda saiti <b>%2$s</b>, kuna selle serdi väljaandja on tundmatu, sert on allkirjastatud selle omaniku poolt või server ei edasta korrektseid vaheserte.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Mine tagasi (soovitatav)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Nõustu riskiga ja jätka</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">See sait nõuab turvalist ühendust.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Lehte, mida soovid vaadata, pole võimalik kuvada, sest see sait nõuab turvalist ühendust.</li>
+ <li>Probleem on suure tõenäosusega saidi poolel ja sa ei saa selle lahendamiseks midagi teha.</li>
+ <li>Sa võid sellest probleemist teavitada saidi administraatorit.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Edasijõudnuile…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Mine tagasi</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Ühendus katkes</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Veebilehitsejal õnnestus edukalt ühenduda, kuid ühendust segati keset andmete edastamist. Palun proovi uuesti.</p>
+ <ul>
+ <li>Veebileht võib olla ajutiselt kättesaamatu või liialt hõivatud. Proovi mõne aja pärast uuesti.</li>
+ <li>Kui sa ei saa avada ühtegi lehte, siis kontrolli oma seadme andmeside või Wi-Fi ühendust.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ühendus aegus</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Soovitud leht ei vastanud ühendusele ja veebilehitseja lõpetas vastuse ootamise.</p>
+ <ul>
+ <li>Kas serveril võib olla probleeme suure koormusega või on tegu ajutise veaga? Proovi hiljem uuesti.</li>
+ <li>Kas ka teiste lehtede vaatamine ei õnnestu? Kontrolli seadme võrguühendust.</li>
+ <li>Kas sinu seade või võrk on kaitstud tulemüüriga? Vigased tulemüüri sätted võivad segada veebilehitsemist.</li>
+ <li>Endiselt probleemid? Konsulteeri oma võrguadministraatori või interneti teenusepakkujaga.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Viga ühendumisel</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Veebileht võib olla ajutiselt kättesaamatu või liialt hõivatud. Proovi mõne hetke pärast uuesti.</li>
+ <li>Kui sa ei saa avada ühtegi lehte, siis kontrolli oma seadme andmeside või Wi-Fi ühendust.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Ootamatu vastus serverilt</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Veebisait andis päringule ootamatu vastuse ja veebilehitsejal pole võimalik jätkata.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Veebileht pole korralikult ümber suunatud</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Veebilehitseja lõpetas katsed objekti laadida. Veebileht suunab päringu edasi viisil, kuidas see kunagi ei õnnestu.</p>
+ <ul>
+ <li>Kas oled keelanud või blokkinud selle saidi küpsised?</li>
+ <li>Kui küpsiste lubamine ei lahenda antud probleemi, siis on tõenäoliselt tegemist probleemiga serveris, mitte sinu seadmes.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Võrguta režiim</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Brauser on võrguta režiimis ega saa soovitud aadressiga ühenduda.</p>
+ <ul>
+ <li>Kas seade on ühendatud töötavasse võrku?</li>
+ <li>Vajuta “Proovi uuesti”, et lülituda võrgurežiimi ning laadida leht uuesti.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port on turvakaalutlustel keelatud</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Soovitud aadress määratleb pordi (nt <q>mozilla.org:80</q> ehk pordi 80 aadressil mozilla.org), mida muidu kasutatakse <em>muul</em> otstarbel kui veebilehitsemine. Veebilehitseja katkestas päringu sinu turvalisuse ja julgeoleku huvides.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ühendus katkestati</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Ühenduse loomisel segati võrguliiklust. Palun proovi uuesti.</p>
+ <ul>
+ <li>Veebileht võib olla ajutiselt kättesaamatu või liialt hõivatud. Proovi mõne aja pärast uuesti.</li>
+ <li>Kui sa ei saa avada ühtegi lehte, siis kontrolli oma seadme andmeside või Wi-Fi ühendust.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Ohtlik faili tüüp</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Palun võta ühendust saidi omanikuga, et informeerida teda antud probleemist.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Vigane sisu</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Andmete edastamisel esinenud vea tõttu pole soovitud lehte võimalik kuvada.</p>
+ <ul>
+ <li>Palun võta ühendust saidi omanikega, et teavitada neid sellest probleemist.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Sisu kuvamisel esines viga</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Andmete edastamisel esinenud vea tõttu pole soovitud lehte võimalik kuvada.</p>
+ <ul>
+ <li>Palun võta ühendust saidi omanikega, et teavitada neid sellest probleemist.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Sisu kodeeringu viga</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Lehte, mida soovid vaadata, pole võimalik kuvada, kuna see kasutab vigast või mittetoetatud pakkimise vormingut.</p>
+ <ul>
+ <li>Palun võta ühendust veebilehe omanikuga, et informeerida teda antud probleemist.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Aadressi ei leitud</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Veebilehitseja ei leidnud sellele aadressile vastavat serverit.</p>
+ <ul>
+ <li>Kontrolli, kas aadressis pole sisestusvigu, näiteks
+ <strong>ww</strong>.example.com
+ <strong>www</strong>.example.com.</li> asemel)
+ <li>Kui sa ei saa avada ühtegi lehte, siis kontrolli oma seadme andmeside või Wi-Fi ühendust.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Puudub internetiühendus</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Kontrolli oma internetiühendust või proovi lehte mõne aja pärast uuesti laadida.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Laadi uuesti</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Vigane aadress</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Sisestatud aadress ei ole tunnustatud formaadis. Palun kontrolli aadressi korrektsust ja proovi uuesti.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Aadress pole korrektne</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Veebiaadressid on tavaliselt kirjutatud kujul <strong>http://www.example.com/</strong></li>
+ <li>Kontrolli üle, et kasutad ikka õigetpidi kaldkriipse (näiteks <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Tundmatu protokoll</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Soovitud aadress määratleb protokolli (nt <q>wxyz://</q>), mida veebilehitseja ei tunne, seega pole veebilehitsejal võimalik korralikult selle saidiga ühenduda.</p>
+ <ul>
+ <li>Kas üritad avada multimeedia- või muud teenust, mis pole teksti kujul? Kontrolli veebilehe lisanõudeid.</li>
+ <li>Mõned protokollid võivad nõuda kolmanda osapoole tarkvara või pluginaid, enne kui veebilehitseja suudab nad tuvastada.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Faili ei leitud</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Kas soovitud objekt võib olla ümber nimetatud, kustutatud või mujale paigutatud?</li>
+ <li>Kas aadressis võib olla sisestusviga või probleeme suurtähtedega?</li>
+ <li>Kas sul on objektile ligipääsemiseks piisavalt õigusi?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Ligipääs failile keelati</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Fail võib olla kustutatud, mujale liigutatud või on sellele seatud ligipääsu piiravad õigused.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Puhverserver keeldus ühendusest</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Brauser on häälestatud kasutama puhverserverit, aga puhverserver keeldub ühendusest.</p>
+ <ul>
+ <li>Kas brauseri puhverserveri sätted on korrektsed? Kontrolli sätteid ja proovi uuesti.</li>
+ <li>Kas puhverserver lubab ühendusi sellest võrgust?</li>
+ <li>Endiselt probleemid? Konsulteeri oma võrguadministraatori või interneti teenusepakkujaga.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Puhverserverit ei leitud</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Brauser on häälestatud kasutama puhverserverit, aga puhverserverit ei leitud.</p>
+ <ul>
+ <li>Kas brauseri puhverserveri sätted on korrektsed? Kontrolli sätteid ja proovi uuesti.</li>
+ <li>Kas seade on ühendatud töötavasse võrku?</li>
+ <li>Endiselt probleemid? Konsulteeri oma võrguadministraatori või interneti teenusepakkujaga.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Pahavara saidi probleem</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Aadressil %1$s asuv veebileht on teadete kohaselt ründav leht ja see blokiti vastavalt sinu turvasätetele.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Soovimatu saidi probleem</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Aadressil %1$s asuv veebileht levitab teadete kohaselt soovimatut tarkvara ja see blokiti vastavalt sinu turvasätetele.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Kahjuliku saidi probleem</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Aadressil %1$s asuv veebileht on teadete kohaselt potentsiaalselt kahjulik ja see blokiti vastavalt sinu turvasätetele.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Veebivõltsingu probleem</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Aadressil %1$s asuv veebileht on teadete kohaselt veebivõltsingut sisaldav leht ja see blokiti vastavalt sinu turvasätetele.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Turvaline sait pole saadaval</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Oled turvalisuse suurendamiseks lubanud ainult HTTPS-režiimi ja saidil <em>%1$s</em> puudub HTTPSi tugi.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Mine HTTP saidile</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..dc02cf8587
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-eu/strings.xml
@@ -0,0 +1,308 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Saiatu berriro</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Ezin da eskaera osatu</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Momentu honetan ez dago arazo edo errore honen inguruko argibide gehiago..</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Konexio seguruak huts egin du</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Ikusten saiatzen ari zaren orria ezin da erakutsi jasotako datuen egiazkotasuna ezin delako egiaztatu.</li>
+ <li>Mesedez jarri harremanetan webgunearen jabeekin arazoaren berri emateko.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Konexio seguruak huts egin du</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Hau zerbitzariaren konfigurazio arazo bat izan zitekeen, edo norbait zerbitzariaren nortasuna bidegabe bereganatzen saiatzen ibiltzea.</li>
+ <li>Zerbitzari honetara arazorik gabe konektatu bazara iraganean, errorea unekoa izan daiteke eta geroago saia zaitezke.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Aurreratua…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Baten bat gunea ordezten saiatzen egon liteke eta ez zenuke jarraitu behar.</label>
+ <br><br>
+ <label>Webguneek bere identitatea ziurtagirien bidez frogatzen dute. %1$sek ez du <b>%2$s</b> fidagarritzat jotzen ziurtagiriaren jaulkitzailea ezezaguna delako, ziurtagiria guneak berak sinatutakoa delako, edo zerbitzariak ez dituelako tarteko ziurtagiriak egoki bidaltzen.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Itzuli (gomendatua)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Onartu arriskua eta jarraitu</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Webgune honek konexio segurua eskatzen du.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Ikusten saiatzen ari zaren orria ezin da erakutsi webguneak konexio segurua eskatzen duelako.</li>
+ <li>Arazoa ziurrenik webgunearena da eta ezin duzu ezer egin hau konpontzeko.</li>
+ <li>Webgunearen kudeatzaileari arazoaren berri eman diezaiokezu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Aurreratua…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> webguneak HTTP Strict Transport Security (HSTS) izeneko segurtasun-politika dauka eta beraz <b>%2$s</b> aplikazioa modu seguruan konekta daiteke soilik. Ezin duzu gune hau bisitatzeko salbuespenik gehitu. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Itzuli</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Konexioa eten egin da</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Nabigatzaileak konexioa ondo sortu du, baina datuak jasotzen ari zela transferentzia eten egin da. Mesedez, saiatu berriro.</p>
+ <ul>
+ <li>Gunea une batez desgaituta edo oso lanpetuta egon daiteke. Saiatu berriro minutu batzuen buruan.</li>
+ <li>Ezin baduzu beste orririk kargatu, egiaztatu zure gailuaren datu- edo WiFi-konexioa.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Konexioaren denbora-muga gainditu da</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Eskatutako guneak ez du eskaera erantzun eta nabigatzaileak itxaroteari utzi dio.</p>
+ <ul>
+ <li>Zerbitzariak lan karga handia duelako gerta daiteke. Saiatu geroago.</li>
+ <li>Gauza bera gertatzen zaizu beste guneekin? Egiaztatu zure gailuaren sareko konexioa.</li>
+ <li>Zure gailua edo sarea suebaki edo proxy baten bitartez babestuta dago? Gaizki konfiguratutako ezarpenek web nabigazioa oztopa dezakete.</li>
+ <li>Arazoak oraindik? Jarri harremanetan zure sarearen kudeatzailearekin edo Internet hornitzailearekin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Ezin da konektatu</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Gunea une batez desgaituta edo oso lanpetuta egon daiteke. Saiatu berriro minutu batzuen buruan.</li>
+ <li>Ezin baduzu beste orririk kargatu, egiaztatu zure gailuaren datu- edo WiFi-konexioa.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Ustekabeko erantzuna zerbitzaritik</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Guneak emandako erantzuna ustekabekoa izan da eta nabigatzaileak ezin du aurrera jarraitu.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Orriak ez du birbideraketa ondo egiten</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Nabigatzaileak eskatutako elementua berreskuratzen saiatzeari utzi dio. Gunea eskaera modu okerrean birbideratzen ari da.</p>
+ <ul>
+ <li>Gune honetarako cookieak blokeatu edo ezgaitu dituzu?</li>
+ <li>Guneak bidalitako cookieak onartu eta gero arazoa konpontzen ez bada, zerbitzariaren konfigurazioaren arazoa izan daiteke eta ez zure gailuarena.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Lineaz kanpoko modua</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Nabigatzailea lineaz kanpo dago eta ezin du eskatutako elementuarekin konektatu.</p>
+ <ul>
+ <li>Gailua sare aktibo batera konektatuta dago?</li>
+ <li>Sakatu "Saiatu berriro" botoia nabigatzailea linean jarri eta orria berritzeko.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Segurtasun neurriengatik ataka galarazita dago</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Eskatutako helbideak zehaztutako ataka (adib. <q>mozilla.org:80</q> mozilla.org guneko 80. atakarentzako) web nabigazioa ez den <em>bestelako</em> helburuetarako erabili ohi da. Nabigatzaileak eskaera bertan behera utzi du zure babes eta segurtasunerako.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Konexioa berrezarri egin da</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Sareko lotura eten egin da konexioa negoziatzerakoan. Mesedez saiatu berriro.</p>
+ <ul>
+ <li>Gunea une batez desgaituta edo oso lanpetuta egon daiteke. Saiatu berriro minutu batzuen buruan.</li>
+ <li>Ezin baduzu beste orririk kargatu, egiaztatu zure gailuaren datu- edo WiFi-konexioa.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Fitxategi mota ez-segurua</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Jarri harremanetan webgunearen jabeekin arazo honen berri emateko.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Hondatutako edukien errorea</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Ikusten saiatzen ari zaren orria ezin da erakutsi errore bat detektatu delako datu-transmisioan.</p>
+ <ul>
+ <li>Jarri harremanetan gunearen arduradunarekin arazo honen berri emateko.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Edukiak huts egin du</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Ikusten saiatzen ari zaren orria ezin da erakutsi errore bat detektatu delako datu-transmisioan.</p>
+ <ul>
+ <li>Jarri harremanetan gunearen arduradunarekin arazo honen berri emateko.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Edukien kodeketa-errorea</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Ikusten saiatzen ari zaren orria ezin da ikusi baliogabeko edo onartzen ez den konpresio mota bat erabiltzen baitu.</p>
+ <ul>
+ <li>Jarri harremanetan gunearen arduradunarekin arazo honen berri emateko.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Helbidea ez da aurkitu</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Nabigatzaileak ezin du ostalariko zerbitzaria aurkitu emandako helbidean.</p>
+ <ul>
+ <li>Helbidea ondo begiratu mota honetako erroreak ekiditeko: <strong>ww</strong>.adibidea.eus <strong>www</strong>.adibidea.eus-en ordez.</li>
+ <li>Ezin baduzu inolako orririk kargatu, begiratu zure gailuaren datu- edo WiFi-konexioa.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Interneterako konexiorik ez</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Egiaztatu zure sareko konexioa edo saiatu orria berritzen geroago.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Berritu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Helbide baliogabea</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Idatzitako helbidearen formatua ez da ulertzen. Egiaztatu helbide-barran ea akatsik dagoen eta saiatu berriro.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Helbidea ez da baliozkoa</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Web helbideak normalean <strong>http://www.adibidea.eus/</strong> formatukoak dira</li>
+ <li>Ziurtatu aurrerako barrak erabiltzen dituzula (hau da, <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protokolo ezezaguna</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Helbideak zehaztutako protokoloa (adib. <q>wxyz://</q>) ez du ezagutzen nabigatzaileak, beraz ezin da behar bezala konektatu gunera.</p>
+ <ul>
+ <li>Multimedia edo testua ez den bestelako zerbitzuren bat atzitzen saiatzen ari zara? Egiaztatu guneak aparteko beharrik duen.</li>
+ <li>Zenbait protokolok hirugarrenen softwarea edo pluginak behar dituzte nabigatzaileak ezagutu ahal ditzan.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fitxategia ez da aurkitu</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Elementuaren izena aldatua, elementua bera ezabatuta edo lekuz aldatua egon daiteke?</li>
+ <li>Helbidea oker idatzi duzu?</li>
+ <li>Baduzu eskatutako elementua jasotzeko baimenik?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Fitxategi-atzipena ukatu egin da</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Kendua edo lekuz aldatua egon liteke, edo fitxategi-baimenek sarrera eragotz lezakete.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy-zerbitzariak konexioa ukatu egin du</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Nabigatzailea proxy-zerbitzari bat erabiltzeko konfiguratuta dago, baina proxy-zerbitzariak konexioa ukatu egin du.</p>
+ <ul>
+ <li>Proxy-zerbitzariaren ezarpenak ondo ezarrita daude? Egiaztatu ezarpenak eta saiatu berriro.</li>
+ <li>Proxy-zerbitzariak sare honetatik bideratutako konexioak baimenduta daude?</li>
+ <li>Arazoak oraindik? Jarri harremanetan zure sarearen kudeatzailearekin edo Internet hornitzailearekin.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy-zerbitzaria ez da aurkitu</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Nabigatzailea proxy-zerbitzari bat erabiltzeko konfiguratuta dago, baina proxy-zerbitzaria ez da aurkitu.</p>
+ <ul>
+ <li>Proxy-zerbitzariaren ezarpenak ondo ezarrita daude? Egiaztatu ezarpenak eta saiatu berriro.</li>
+ <li>Gailua sare batera konektatua dago?</li>
+ <li>Arazoak oraindik? Jarri harremanetan zure sarearen kudeatzailearekin edo Internet hornitzailearekin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Malware gunea</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>%1$s gunea gune erasotzaile bezala salatua dago eta blokeatu egin da zure segurtasun-ezarpenetan oinarrituta.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Nahi ez den gunea</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s gunea nahi ez den softwarearen zerbitzari bezala salatua dago eta blokeatu egin da zure segurtasun-ezarpenetan oinarrituta.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Gune arriskutsua</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s gunea balizko gune arriskutsu bezala salatua dago eta blokeatu egin da zure segurtasun-ezarpenetan oinarrituta.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Gune iruzurtia</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s gune iruzurti gisa dago salatuta eta blokeatu egin da zure segurtasun-ezarpenetan oinarrituta.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Gune segurua ez dago erabilgarri</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Segurtasun hobetua izateko HTTPS-Only modua gaitu duzu eta <em>%1$s</em> webgunearen HTTPS bertsioa ez dago erabilgarri.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Jarraitu HTTP gunera</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..6acacfab86
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fa/strings.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">تلاش دوباره</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">امکان تکمیل درخواست وجود ندارد</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>در حال حاضر اطلاعات بیشتری در مورد این ایراد یا خطا در دسترس نیست.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">برقراری پیوند ایمن شکست خورد</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>صفحه‌ای که تلاش می‌کنید از آن بازدید کنید قابل نمایش نیست، زیرا امکان تأیید اعتبار داده‌های دریافتی از آن وجود ندارد.</li>
+ <li>لطفاً با صاحبان این وبگاه تماس بگیرید و آن‌ها را در جریان این مشکل قرار دهید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">برپاسازی پیوند ایمن شکست خورد</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>علت می‌تواند اشکالی در پیکربندی کارساز، یا تلاش فردی برای وانمود کردن خود به جای این کارساز باشد.</li>
+ <li>اگر در گذشته با موفقیت به این کارساز متصل شده‌اید، امکان دارد این اشکال موقتی باشد، و می‌توانید بعداً دوباره تلاش کنید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">پیشرفته…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>شخصی می‌تواند در تلاش برای جعل هویت پایگاه باشد و شما نباید ادامه دهید.</label>
+ <br><br>
+ <label>وبگاه‌ها هویت خود را از طریق گواهینامه‌ها اثبات می‌کنند. %1$s به <b>%2$s</b> اعتماد ندارد، زیرا صادرکننده گواهینامهٔ آن ناشناخته است، گواهینامه خودامضا است یا کارساز در حال ارسال گواهینامه‌های واسط صحیح نیست.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">بازگشت (پیشنهاد می‌شود)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">پذیرش خطر و ادامه</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">این وبگاه به اتصالی ایمن نیاز دارد.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>صفحه‌ای را که می‌خواهید مشاهده کنید نمیتواند نمایش داده شود زیرا این وبگاه به اتصال ایمن نیاز دارد.</li>
+ <li>مشکل به احتمال زیاد مربوط به وبگاه است و کاری برای حل آن نمی توانید انجام دهید.</li>
+ <li>می‌توانید مشکل را به مدیر وبگاه اطلاع دهید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">پیش‌رفته…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> یک خط‌مشی امنیتی به نام HTTP Strict Transport Security (HSTS) دارد، به این معنی که <b>%2$s</b> فقط می‌تواند به صورت ایمن به آن متصل شود. شما نمی‌توانید استثنایی برای بازدید از این پایگاه اضافه کنید. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">بازگشت</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">اتّصال قطع شد</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>مرورگر با موفقیت متّصل شد، اما هنگام انتقال اطلاعات، اتّصال قطع شد. لطفاً دوباره تلاش کنید.</p>
+ <ul>
+ <li>سایت ممکن است موقتاً در دسترس نباشد یا خیلی شلوغ باشد. چند لحظه بعد دوباره تلاش کنید.</li>
+ <li>اگر قادر به بارگزاری هیچ صفحه‌ای نیستید، اتّصال دادهٔ دستگاه یا وای‌فای خود را بررسی کنید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">مهلت اتّصال تمام شد</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>پایگاه درخواست شده به تقاضای اتّصال مرورگر پاسخ نداد و مرورگر انتظار برای پاسخ را متوقف کرد.</p>
+ <ul>
+ <li>آیا ممکن است کارساز دچار تقاضای بیش از حد شده باشد یا موقتاً دارای مشکلی باشد؟ لطفاً بعداً دوباره تلاش کنید.</li>
+ <li>آیا قادر به مرور پایگاه‌های دیگر نیز نیستید؟ اتّصال شبکهٔ افزاره‌تان را بررسی کنید.</li>
+ <li>آیا افزاره یا شبکهٔ شما توسط دیوار آتش یا پیشکار محافظت می‌شود؟ تنظیمات نادرست آن مانع از مرور وب می‌شود.</li>
+ <li>هنوز هم مشکل دارید؟ برای دریافت کمک با مدیر شبکه یا فراهم‌کنندهٔ اینترنت خود مشورت کنید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">قادر به برقراری اتصال نیست</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>ممکن است پایگاه موقتاً در دسترس نباشد یا خیلی شلوغ باشد. چند لحظهٔ دیگر دوباره تلاش کنید.</li>
+ <li>اگر نمی‌توانید هیچ صفحه‌ای را باز کنید، اتّصال دادهٔ افزاره یا وای‌فای خود را بررسی کنید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">جواب غیرمنتظره از کارساز</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>این سایت به شیوهٔ غیر منتظره‌ای به درخواست شبکه پاسخ داد و مرورگر قادر به ادامه نیست.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">این صفحه درست تغییر مسیر نمی‌دهد</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>مرورگر تلاش برای دریافت مورد درخواستی را پایان داده است. وبگاه به صورتی درخواست را تغییر مسیر می‌دهد که این کار هیچ‌گاه به پایان نخواهد رسید.</p>
+ <ul>
+ <li>آیا کلوچک‌های احتمالی مورد نیاز این وبگاه را غیرفعال ساخته‌اید؟</li>
+ <li>اگر پذیرفتن کلوچک‌های وبگاه مشکل را حل نکرد، به احتمال زیاد این مشکلی در تنظیمات کارساز است و به رایانهٔ شما مربوط نیست.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">حالت برون‌خط</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>مرورگر در حالت برون‌خط قرار دارد و نمی‌تواند به مورد درخواستی وصل شود.</p>
+ <ul>
+ <li> آیا رایانه به یک شبکهٔ فعال متصل است؟</li><li>دکمهٔ «تلاش دوباره» را فشار دهید تا به حالت برخط رفته و صفحه را دوباره بار کنید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">درگاه به دلایل امنیتی محدود شده است</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>نشانی درخواست مشخصا(به عنوان مثال<q>mozilla.org:80</q>برای درگاه ۸۰ بر روی mozilla.org) ازدرگاهی استفاده می کندکه در حالت عادی به عنوان کاربردی <em>به غیر</em> از وبگردی استفاده می شود.مرورگر برای حفاظت و امنیت شما این درخواست را لغوکرد.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">اتصال از نو برقرار شد</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p> مرورگر با موفقیت متصل شد ، اما هنگام انتقال اطلاعات ، اتصال قطع شد. لطفا دوباره امتحان کنید. </p>
+      <ul>
+        <li> سایت ممکن است موقتاً در دسترس نباشد یا خیلی شلوغ باشد. در چند لحظه دوباره امتحان کنید. </li>
+        <li> اگر قادر به بارگیری هیچ صفحه ای نیستید ، اطلاعات دستگاه یا اتصال Wi-Fi خود را بررسی کنید. </li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">نوع پرونده ناامن است</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul> <li>لطفاً با صاحبان پایگاه وب تماس بگیرید تا آنها را در جریان این مشکل قرار دهید.</li> </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">خطای خرابی محتوا</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>صفحه‌ای که تلاش می‌کنید از آن بازدید کنید قابل نمایش نیست، زیرا خطایی در هنگام انتقال اطلاعات رُخ داده است.</p><ul><li>لطفاً با صاحبان این پایگاه اینترنتی تماس بگیرید و آنها را در جریان این مشکل قرار دهید.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">محتوا فروپاشید</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>صفحه‌ای که تلاش می‌کنید از آن بازدید کنید قابل نمایش نیست، زیرا خطایی در هنگام انتقال اطلاعات رخ داده است.</p>
+ <ul>
+ <li>لطفاً با صاحبان این وبگاه تماس بگیرید و آن‌ها را در جریان این مشکل قرار دهید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">خطای کدگذاری محتوا</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>قادر به نمایش صفحه‌ای که درخواست بازدید از آن را کرده‌اید نیست، زیرا از گونه‌ای ناشناخته یا پشتیبانی نشده از فشرده‌سازی استفاده می‌کند.</p><ul><li>لطفاً با صاحبان این پایگاه اینترنتی تماس برقرار کنید و آنها را در جریان این مشکل قرار دهید.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">نشانی پیدا نشد</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>مرورگر نتوانست کارساز میزبان را برای نشانی ارائه شده پیدا کند.</p>
+ <ul>
+ <li>نشانی را برای غلط‌های املایی بررسی کنید، مانند:
+ <strong>ww</strong>.example.com به جای
+ <strong>www</strong>.example.com.</li>
+ <li>اگر قادر به بارگیری هیچ صفحه‌ای نیستید، دادهٔ افزاره یا اتصال وای‌فای خود را بررسی کنید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">اتصال اینترنت در دسترس نیست</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">اتصال شبکهٔ خود را بررسی کنید یا پس از چند لحظه، بار کردن دوبارهٔ صفحه را بیازمایید.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">بار کردن دوباره</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">نشانی نامعتبر</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>نشانی فراهم‌شده دارای قالب معتبری نیست. لطفاً از عدم وجود اشتباه در نوار مکان اطمینان حاصل کنید و دوباره تلاش نمایید.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">نشانی معتبر نیست</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+<li>نشانی‌های وب معمولاً به این صورت نوشته می‌شوند <strong>http://www.example.com/</strong></li>
+<li>مطمئن شوید که از ممیز درست استفاده می‌کنید (یعنی <strong>/</strong>).</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">نشانی قابل فهم نبود</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>نشانی قراردادی مثلا (e.g. <q>wxyz://</q>)‏ را مشخص کرده است که مرورگر قادر به شناسایی آن نیست، بنابراین نمی‌تواند به شیوه صحیح به وب‌گاه متصل شود.</p><ul><li>آیا در حال تلاش برای دسترسی به خدمات چندرسانه‌ای یا غیر متنی هستید؟ این وب‌گاه را برای یافتن ملزومات اضافی جست‌وجو کنید.</li><li>برخی قراردادها ممکن است برای شناسایی نیاز به نرم‌افزار یا افزایه های دیگری جهت شناسایی داشته باشند.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">پرونده پیدا نشد</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul><li>آیا امکان دارد پروندهٔ مورد نظر تغییر نام یا محل داده باشد یا حذف شده باشد؟</li><li>آیا اشکال املائی، بزرگی یا کوچکی حروف یا دیگر اشکالات نوشتاری در نشانی وجود دارد؟</li><li>آیا مجوزهای دسترسی کافی برای دست‌یابی به این نشانی را دارید؟</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">دسترسی به این پرونده رد می‌شود</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul><li>ممکن است حذف،‌منتقل شده باشد یا مجوز‌های آن از دسترسی جلوگیری‌ می‌کند.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">کارساز پیشکار از اتصال خودداری می‌کند</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>مرورگر برای استفاده از یک کارساز پیشکار پیکربندی شده است، ولی پیشکار از اتصال خودداری می‌کند.</p>
+ <ul>
+ <li>آیا تنظیمات کارساز پیشکار مرورگر صحیح است؟ تنظیمات را کنترل کنید و دوباره تلاش نمایید.</li>
+ <li>آیا پیشکار اجازهٔ اتصال از این شبکه را می‌دهد؟</li>
+ <li>هنوز هم مشکل دارید؟ برای دریافت کمک با مدیر شبکه یا فراهم‌کنندهٔ اینترنت خود مشورت کنید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">کارساز پیشکار پیدا نشد</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>مرورگر برای استفاده از یک کارساز پیشکار پیکربندی شده است، ولی پیشکار پیدا نشد.</p>
+ <ul>
+ <li>آیا تنظیمات کارساز پیشکار مرورگر صحیح است؟ تنظیمات را کنترل کنید و دوباره تلاش نمایید.</li>
+ <li>آیا افزاره به شبکه‌ای فعال متصل است؟</li>
+ <li>هنوز هم مشکل دارید؟ برای دریافت کمک با مدیر شبکه یا فراهم‌کنندهٔ اینترنت خود مشورت کنید.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">مشکل پایگاه بدافزار</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>پایگاه%1$s به عنوان یک وب‌گاه تهاجمی گزارش شده و بر اساس ترجیحات امنیتی شما مسدود شده است.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">مشکل پایگاه ناخواسته</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>پایگاه %1$s به عنوان سایتی که اقدام به ارائه نرم‌افزارهای ناخواسته می‌کند گزارش شده و بر اساس ترجیحات امنیتی شما مسدود شده است.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">مشکل پایگاه زیان‌آور</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>پایگاه%1$s به عنوان یک وب‌گاه تهاجمی گزارش شده و بر اساس ترجیحات امنیتی شما مسدود شده است.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">مشکل پایگاه گمراه‌کننده</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>صفحه %1$s به عنوان یک پایگاه وب گمراه‌کننده گزارش شده و بر اساس ترجیحات امنیتی شما مسدود شده است.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">پایگاه ایمن در دسترس نیست</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[حالت فقط-HTTPS برای امنیت بیش‌تر فعال شده است و نگارشی HTTPS از <em>%1$s</em> در دسترس نیست.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">ادامه به این پایگاه اینترنتی به وسیله HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..218ad7aa19
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ff/strings.xml
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Fuɗɗito</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Horiima Timminde Ɗaɓɓitannde</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Humpito ɓeydorɗo baɗte ɗee caɗeele walla ndee juumre heɓotaako oo sahaa.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Ceŋagol Kisnangol Woorii</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+  <li>Hello ngo etoto-ɗaa naatnde ngoo waawaa hollireede sabu goongɗingol keɓe keɓaaɗe ɗee waawaa ƴeewteede.</li>
+ <li>Tiiɗno jokkondir e jeyɓe lowre geese ndee ngam humpitde-ɓe caɗeele ɗee.</li> 
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Ceŋagol Kisnangol Woorii</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Ɗum ena waawi ummaade e teeltol sarworde ndee, walla ena waawi wonde won etotooɗo ñemmbitaade sarworde ndee.</li>
+ <li>So tawii a meeɗii seŋaade e ndee sarworde e ko ɓenni, maa taw juumre ndee juutataa, ngati aɗa waawi etaade so ɓooyii.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Ceeɓtore…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Neɗɗo ina waawi wonde e etaade wujjude hefinirde lowre ndee etee a fotaani jokkude.</label>
+ <br><br>
+ <label>Lowe geese ndallinirta keɓtinirɗe mum en ko e seedanteeji. %1$shoolaaki <b>%2$s</b> sabu dokkoowo-mo seedamfaagu nguu anndaaka, seedamfaagu nguu ko siifngu hoore mum, walla sarworde ndee neldaani seedanteeji hakkundeeji moƴƴi.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Rutto (Ena wasiyaa)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Jaɓ tanaa oo njokkaa</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Nde lowre ena laaɓndii ceŋol kisnangol.</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Ceŋagol ngol taƴii</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Wanngorde ndee seŋii haa moƴƴi, kono ceŋogol ngol taƴii saanga baylugol humpito. Tiiɗno, eto goɗngol.</p>
+ <ul>
+ <li>Lowre ndee ina waasde heɓaade e mudda walla ko nde haljunde no feewi. Eto kadi ɗo e yeeso seeɗa.</li>
+ <li>O a horiima loowde hay hello wooto, ƴeewto keɓe kaɓirgal maa walla ceŋogol WI-FI.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ceŋagol ngol honaama waktu</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Lowre ɗaɓɓitaande ndee jaabaaki ɗaɓɓitaande ceŋol etee wanngorde ndee dartiima fadde jaabtol.</p>
+ <ul>
+ <li>Mbar sarworde ndee wonaa heewraande walla ena taƴi oo sahaa? Fuɗɗito so ɓooyii.</li>
+ <li>A horiima naatde e lowe goɗɗe? Ƴeewto ceŋol laylaytol masiŋel ngel. </li>
+ <li>Mbar ordinateer maa suuraaki caggal ɓalal-jaynge walla proxy? Teelɗe ɗe peewaani ena mbaawi haɗde peeragol e geese.</li>
+ <li>Haa jonni ena waɗi caɗeele? Jokkondir e jiiloowo laylaytol maa walla jeeyoowo ceŋol maa ngam ɗaɓɓude ballal.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Horiima seŋaade</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>dee lowre heɓotaako oo sahaa walla ena halji. Eto naatde e mayre ɗoo e yeeso seeɗa.</li>
+ <li>So a horiima loowde kelle fof, ƴeewto keɓe kaɓirgol maa walla seŋorde maa Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Jaabawol sarworde ngol tijjaaka</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Lowre ndee jaabtiima ɗaɓɓitaande laylaytol ndol e mabydi kaawniindi etee wanngorde ndee waawaa jokkude.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Ngoo hello wonaani e yiiltude no feewiri</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Wanngorde ndee dartinii aaftaade temre ɗaɓɓitaande ndee. Lowre ndee ɓenninii ɗaɓɓitaande ndee e mbaydi ndi jogoraani timmude.</p>
+ <ul>
+ <li>Mbar a daaƴaani walla pali-ɗaa kukiije cokalaaɗe e ndee lowre?</li>
+ <li>So jaɓde kukiije lowre ndee ñawndaani caɗeele ɗee, Ena wona tawwa ko teeltol sarworde ndee kono wonaa kaɓirgal maa.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mbaydi Ceŋtol</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Wanngorde ndee woni ko e mbayka ceŋtol etee waawaa seŋaade e temre ɗaɓɓitaande ndee.</p>
+ <ul>
+ <li>Mbar kaɓirgal maa ena seŋii e laylaytol caasngol?</li>
+ <li>Ñoƴƴu “eto kadi” ngam artirde a mbayka ceŋol, kesɗitinaa hello ngoo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ceŋagol ngol fuɗɗitaama</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Ndee Fiilde Hoolnaaki</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Tiiɗno jokkondir e jeyɓe lowre ndee ngam humpitde-ɓe ɗee caɗeele.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Juumre loowdi pirndi</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Hello ngo etot-ɗaa naatnde ngoo waawaa hollireede sabu juumre waɗii e baanjitagol keɓe ɗee.</p>
+ <ul>
+ <li>Tiiɗno jokkondir e jeyɓe lowre geese ndee ngam humpitde-ɓe caɗeele ɗee.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Loowdi ndii hookii</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Hello ngo etot-ɗaa naatnde ngoo waawaa hollireede sabu juumre waɗii e baanjitagol keɓe ɗee.</p>
+ <ul>
+ <li>Tiiɗno jokkondir e jeyɓe lowre geese ndee ngam humpitde-ɓe caɗeele ɗee.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Ñiiɓirde Yiytaaka</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Alaa ceŋol enternet</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Yeewto ceŋogol laylaytol maa walla eto loowtugol hello ngoo ko ɓooyaani.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Loowtu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Ñiiɓirde Moƴƴaani</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Ñiiɓirde hokkaande ndee wonaani e mbaydi keftinaandi. Tiiɗno ƴeewto palal ñiiɓiɗe ndee ngam pergitte, puɗɗito-ɗaa.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Ñiiɓirde ndee moƴƴaani</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul> <li>Ñiiɓirɗe geese keewi winndireede ko <strong>http://www.yeru.com/</strong></li> <li>Ƴeewto no feewi so a huutoriima laase yeeso (k.n. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fiilde Yiytaaka</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Ballagol fiilde salaama</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Maa taw ko nde momtaande, dirtinaande, walla jamire fiilde ena kala ballagol.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Sawrorde Proxy Saliima Ceŋagol</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Sarworde Proxy Yiytaaka</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Ndee lowre to %1$s jaŋtaama wonde ko lowre njangu etee ko ko daaƴaa e cuɓoraaɗe kisal maa.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Caɗe lowre ɗe njiɗaaka</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Ndee lowre to %1$s jaŋtaama wonde sarwat topirɗe gañaaɗe etee koko daaƴaa e cuɓoraaɗe kisal maa.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Caɗe lowre bonnde</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Ndee lowre to %1$s jaŋtaama wonde ko lowre waawnde bonnude etee koko daaƴaa e cuɓoraaɗe kisal maa.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Caɗe lowre fuuntoore</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Ndee lowre to %1$s jaŋtaama wonde ko lowre fuuntoore etee koko daaƴaa e cuɓoraaɗe kisal maa.</p>]]></string>
+
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Jooku to Lowre HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..974f28a0af
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fi/strings.xml
@@ -0,0 +1,293 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Yritä uudelleen</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Pyyntöä ei voi suorittaa</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Lisätietoja tästä ongelmasta tai virheestä ei ole juuri nyt saatavilla.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Suojattu yhteys epäonnistui</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Avattavaa sivua ei voida näyttää, koska vastaanotetun datan alkuperää ei kyetty varmentamaan.</li>
+ <li>Ilmoitathan ongelmasta sivuston omistajalle.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Suojattu yhteys epäonnistui</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+<li>Ongelma voi johtua palvelimen asetuksista tai jonkin toisen palvelimen vilpillisestä yrityksestä tekeytyä palvelimeksi.</li>
+<li>Jos yhteyden muodostuminen palvelimeen on aiemmin onnistunut, vika voi olla väliaikainen. Yritä tällöin myöhemmin uudestaan.</li>
+</ul>
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Lisäasetukset…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Jokin toinen osapuoli saattaa tekeytyä sivustoksi. Ei ole suositeltavaa jatkaa sivustolle.</label>
+<br><br>
+<label>Sivustot todistavat identiteettinsä varmenteella. %1$s ei luota sivustoon <b>%2$s</b>, koska sen käyttämän varmenteen myöntäjä on tuntematon, varmenne on itse allekirjoitettu tai palvelin ei lähetä oikeita välivarmenteita.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Palaa (suositellaan)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Ota riski ja jatka</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Tämä sivusto vaatii suojatun yhteyden.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Sivua, jota yrität katsella, ei voida näyttää, koska tämä verkkosivusto vaatii suojatun yhteyden.</li>
+ <li>Ongelma johtuu todennäköisesti verkkosivustosta, etkä voi tehdä mitään ongelman ratkaisemiseksi.</li>
+ <li>Voit ilmoittaa ongelmasta verkkosivuston ylläpitäjälle.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Lisäasetukset…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> noudattaa tietoturvakäytäntöä nimeltä HTTP Strict Transport Security (HSTS), mikä tarkoittaa, että <b>%2$s</b> voi muodostaa vain suojatun yhteyden. Tälle sivustolle siirtymistä varten ei voi lisätä poikkeusta. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Palaa takaisin</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Yhteys keskeytettiin</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Selain muodosti yhteyden, mutta yhteys katkesi siirrettäessä tietoa. Yritä uudestaan.</p>
+ <ul>
+ <li>Sivusto voi olla väliaikaisesti saavuttamattomissa tai kovan rasituksen alaisena. Yritä hetken kuluttua uudestaan.</li>
+ <li>Jos mitkään sivustot eivät toimi, tarkista laitteen data- tai Wi-Fi-yhteys.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Yhteys aikakatkaistiin</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Palvelin ei vastannut yhteyspyyntöön, ja selain lopetti vastauksen odottamisen.</p><ul><li>Palvelin voi olla kovan rasituksen alainen tai väliaikaisesti huollettavana. Yritä myöhemmin uudestaan.</li><li>Jos muutkaan sivustot eivät toimi, tarkista laitteen verkkoasetukset.</li><li>Onko laite tai verkko suojattu palomuurilla tai käytetäänkö välityspalvelinta? Virheelliset asetukset voivat haitata selaamista.</li><li>Jos ongelmat jatkuvat, ota yhteyttä verkon ylläpitoon tai verkkoyhteyden palveluntarjoajaan.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Yhdistäminen ei onnistu</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Sivusto saattaa olla väliaikaisesti pois käytöstä tai ruuhkautunut. Yritä uudelleen hetken kuluttua.</li>
+ <li>Jos et voi avata mitään sivuja, tarkista laitteen data- ja wifi-yhteyksien tila.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Odottamaton vastaus palvelimelta</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Sivusto vastasi verkkopyyntöön odottamattomalla tavalla, eikä selain voi jatkaa.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Sivu ei uudelleenohjaa oikein</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Selain on lopettanut pyydetyn kohteen lataamisen. Palvelin uudelleenohjaa yhteyspyyntöjä loputtomasti.</p>
+ <ul>
+ <li>Onko kaikki tai sivuston tarvitsemat evästeet estetty?</li>
+ <li>Jos sivuston evästeiden salliminen ei korjaa ongelmaa, vika on luultavasti palvelimen asetuksissa, eikä tässä laitteessa.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Yhteydetön tila</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Selain on verkkoyhteydettömässä tilassa eikä voi muodostaa yhteyttä pyydettyyn kohteeseen.</p>
+ <ul>
+ <li>Onko laitteen verkkoyhteys toiminnassa?</li>
+ <li>Aseta selain yhteystilaan ja yritä uudelleen.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Portin käyttöä rajoitettu tietoturvasyistä</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Pyydetty osoite sisältää käytettävän portin (esim. <q>mozilla.org:80</q> ottaa yhteyden mozilla.orgin porttiin 80), jota tavallisesti käytetään <em>muuhun</em> kuin verkkosivujen selaamiseen. Selain on perunut verkkopyynnön turvallisuussyistä.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Yhteys keskeytyi</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Verkkoyhteys katkesi muodostettaessa yhteyttä. Yritä uudestaan.</p>
+ <ul>
+ <li>Sivusto voi olla väliaikaisesti saavuttamattomissa tai kovan rasituksen alaisena. Yritä hetken kuluttua uudestaan.</li>
+ <li>Jos mitkään sivustot eivät toimi, tarkista laitteen data- tai Wi-Fi-yhteys.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Vaarallinen tiedostotyyppi</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Ole yhteydessä sivuston omistajaan ja ilmoita kohtaamastasi ongelmasta.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Vioittuneen sisällön virhe</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Avattavaa sivua ei voida näyttää, koska tiedonsiirrossa tapahtui virhe.</p>
+ <ul>
+ <li>Ilmoitathan ongelmasta sivuston omistajalle.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Sisältö kaatui</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Avattavaa sivua ei voida näyttää, koska tiedonsiirrossa tapahtui virhe.</p>
+ <ul>
+ <li>Ilmoitathan ongelmasta sivuston omistajalle.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Sisällön koodausvirhe</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Avattavaa sivua ei voida näyttää, koska se käyttää virheellistä tai ei-tuettua pakkaustapaa.</p>
+ <ul>
+ <li>Ilmoitathan ongelmasta sivuston omistajalle.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Osoitetta ei löytynyt</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Selain ei löytänyt osoitteessa annettua palvelinta.</p>
+ <ul>
+ <li>Tarkista osoite kirjoitusvirheiden varalta, esimerkiksi
+ <strong>ww</strong>.mozilla.org oikean muodon
+ <strong>www</strong>.mozilla.org sijaan.</li>
+ <li>Jos muutkaan sivustot eivät toimi, tarkista laitteen data- tai Wi-Fi-yhteys.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ei internetyhteyttä</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Tarkista verkkoyhteys tai yritä päivittää sivu hetken kuluttua.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Päivitä</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Virheellinen osoite</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Annettu osoite ei ole tunnistettavassa muodossa. Tarkista osoitepalkin sisältö virheiden varalta ja yritä uudelleen.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Osoite ei ole kelvollinen</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Verkko-osoitteet ovat yleensä muodossa <strong>http://www.example.com/</strong></li>
+ <li>Varmista että käytät vinoviivoja (esim. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Tuntematon yhteyskäytäntö</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Selain ei tunnistanut osoitteessa käytettyä yhteyskäytäntöä (esim. <q>wxyz://</q>), minkä takia yhteyttä palvelimeen ei voida muodostaa.</p>
+ <ul>
+ <li>Jos yhteyttä muodostetaan multimediaa tai jotain muuta kuin tekstiä tarjoavaan palveluun, tarkista palvelimen lisävaatimukset asiakasohjelmille.</li>
+ <li>Toimiakseen selaimessa jotkin yhteyskäytännöt vaativat kolmannen osapuolen tekemän ohjelman tai liitännäisen.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Tiedostoa ei löydy</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Tiedosto voi olla poistettu, siirretty tai nimetty uudelleen.</li>
+ <li>Onko tiedoston nimi ja sijainti kirjoitettu virheettömästi ja oikealla kirjainkoolla?</li>
+ <li>Onko käyttäjällä lukuoikeudet tiedostoon?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Pääsy tiedostoon estettiin</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Se saattaa olla poistettu, siirretty tai tiedoston oikeudet estävät sen käytön.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Välityspalvelin kieltäytyi yhteydestä</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Selain on asetettu käyttämään välityspalvelinta, mutta välityspalvelin ei hyväksynyt yhteyttä.</p>
+ <ul>
+ <li>Ovatko selaimen välityspalvelinasetukset oikeat? Tarkista asetukset ja yritä uudelleen.</li>
+ <li>Tulisiko välityspalvelimen hyväksyä yhteydet tästä verkkoyhteydestä?</li>
+ <li>Jos ongelmat jatkuvat, ota yhteyttä verkon ylläpitoon tai verkkoyhteyden palveluntarjoajaan.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Välityspalvelinta ei löytynyt</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Selain on asetettu käyttämään välityspalvelinta, mutta siihen ei saatu yhteyttä.</p>
+ <ul>
+ <li>Ovatko selaimen välityspalvelinasetukset oikeat? Tarkista asetukset ja yritä uudelleen.</li>
+ <li>Onko tietokoneen verkkoyhteys toimintakykyinen?</li>
+ <li>Jos ongelmat jatkuvat, ota yhteyttä verkon ylläpitoon tai verkkoyhteyden palveluntarjoajaan.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Haittaohjelmia sisältävä sivusto</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Sivu osoitteessa %1$s on ilmoitettu hyökkäyssivustoksi, ja on siksi estetty pohjautuen asettamiisi tietoturva-asetuksiin.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Ei-toivottua sisältöä tarjoava sivusto</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Sivun osoitteessa %1$s on ilmoitettu tarjoavan ei-haluttuja ohjelmistoja, ja on siksi estetty pohjautuen asettamiisi tietoturva-asetuksiin.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Vahingollinen sivusto</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Sivu osoitteessa %1$s on ilmoitettu mahdollisesti haitalliseksi sivustoksi, ja on siksi estetty pohjautuen asettamiisi tietoturva-asetuksiin.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Harhaanjohtava sivusto</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Sivu osoitteessa %1$s on ilmoitettu harhaanjohtavaksi sivustoksi, ja on siksi estetty pohjautuen asettamiisi tietoturva-asetuksiin.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Suojattu sivusto ei saatavilla</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Olet ottanut käyttöön Vain HTTPS -tilan turvallisuuden parantamiseksi, mutta HTTPS-versiota sivustosta <em>%1$s</em> ei ole käytettävissä.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Jatka HTTP-sivustolle</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..40f3da7e11
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fr/strings.xml
@@ -0,0 +1,291 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Réessayer</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">La requête ne peut aboutir</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Aucune autre information disponible concernant le problème ou l’erreur.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Échec de la connexion sécurisée</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>La page que vous essayez de consulter ne peut pas être affichée, car l’authenticité des données reçues ne peut être vérifiée.</li>
+ <li>Veuillez contacter les propriétaires du site web pour les informer de ce problème.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Échec de la connexion sécurisée</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Ceci peut être dû à un problème de configuration du serveur ou à une personne essayant d’usurper l’identité du serveur.</li>
+ <li>Si vous avez déjà pu vous connecter à ce serveur, l’erreur est peut-être temporaire et vous pouvez essayer à nouveau plus tard.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avancé…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label> Quelqu’un pourrait essayer de se faire passer pour le site et vous ne devriez pas continuer.</label>
+<br><br>
+<label>Les sites web prouvent leur identité via des certificats. %1$s ne fait pas confiance à <b>%2$s</b> car l’émetteur de son certificat est inconnu, le certificat est auto-signé ou le serveur n’envoie pas les bons certificats intermédiaires.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Retour (recommandé)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Accepter le risque et poursuivre</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ce site nécessite une connexion sécurisée.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La page que vous essayez d’atteindre ne peut pas être affichée, car ce site web nécessite une connexion sécurisée.</li>
+ <li>Ce problème vient probablement du site web et il n’y a rien que vous puissiez faire pour le résoudre.</li>
+ <li>Vous pouvez informer l’administrateur du site web du problème.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avancé…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label> <b>%1$s</b> a recours à une stratégie de sécurité HTTP Strict Transport Security (HSTS), ce qui signifie que <b>%2$s</b> doit impérativement établir une connexion sécurisée pour y accéder. Vous ne pouvez pas ajouter d’exception pour accéder à ce site.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Retour</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La connexion a été interrompue</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Le navigateur s’est correctement connecté, mais la connexion a été interrompue pendant le transfert d’informations. Veuillez réessayer.</p>
+ <ul>
+ <li>Le site est peut-être temporairement indisponible ou surchargé. Réessayez plus tard.</li>
+ <li> Si vous n’arrivez à naviguer sur aucun site, vérifiez la connexion données ou Wi-Fi de votre appareil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Le délai d’attente a été dépassé</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Le navigateur a attendu trop longtemps lors de la connexion au site et a arrêté d’attendre une réponse.</p>
+ <ul>
+ <li>Le serveur est peut-être en surcharge ou est temporairement en panne ? Réessayez plus tard.</li>
+ <li>D’autres sites sont aussi inaccessibles ? Vérifiez la connexion au réseau de votre appareil.</li>
+ <li>Votre appareil ou votre réseau est-il protégé par un pare-feu ou un proxy ? Des paramètres incorrects peuvent interférer avec la navigation sur le Web.</li>
+ <li>Vous avez toujours des problèmes ? Consultez votre administrateur réseau ou votre fournisseur d’accès à Internet pour obtenir de l’aide.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">La connexion a échoué</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Le site est peut-être temporairement indisponible ou surchargé. Réessayez plus tard.</li>
+ <li> Si vous n’arrivez à naviguer sur aucun site, vérifiez la connexion données ou Wi-Fi de votre appareil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Réponse inattendue du serveur</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Le site a répondu à la requête réseau d’une façon inattendue et le navigateur ne peut continuer.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La page n’est pas redirigée correctement</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Le navigateur a arrêté d’attendre une réponse du site. Le site crée une redirection de telle sorte que la requête ne peut jamais aboutir.</p>
+ <ul>
+ <li>Avez-vous désactivé ou bloqué les cookies nécessaires pour ce site ?</li>
+ <li> Si le problème n’est pas résolu en acceptant les cookies de ce site, il s’agit probablement d’un problème de configuration du serveur et non de votre appareil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mode hors connexion</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Le navigateur est en mode hors connexion et ne peut pas se connecter à l’adresse indiquée.</p>
+ <ul>
+ <li>L’appareil est-il connecté au réseau ?</li>
+ <li>Cliquez sur le bouton « Réessayer » pour revenir en mode connecté et recharger la page.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port restreint pour des raisons de sécurité</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>L’adresse demandée indique un port (p. ex. <q>mozilla.org:80</q> pour le port 80 sur mozilla.org) qui est normalement utilisé pour d’<em>autres</em> usages que la navigation sur le Web. Le navigateur a annulé la requête pour votre protection et votre sécurité.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La connexion a été réinitialisée</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>La liaison au réseau a été interrompue pendant la négociation d’une connexion. Veuillez réessayer.</p>
+ <ul>
+ <li>Le site est peut-être temporairement indisponible ou surchargé. Réessayez plus tard.</li>
+ <li> Si vous n’arrivez à naviguer sur aucun site, vérifiez la connexion données ou Wi-Fi de votre appareil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Type de fichier non sûr</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Veuillez contacter les propriétaires du site web pour les informer de ce problème.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Erreur due à un contenu corrompu</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>La page que vous essayez de consulter ne peut pas être affichée, car une erreur dans la transmission de données a été détectée.</p>
+ <ul>
+ <li>Veuillez contacter les propriétaires du site web pour les informer de ce problème.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Le contenu a planté</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>La page que vous essayez de consulter ne peut pas être affichée, car une erreur dans la transmission de données a été détectée.</p>
+ <ul>
+ <li>Veuillez contacter les propriétaires du site web pour les informer de ce problème.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Erreur d’encodage de contenu</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>La page que vous essayez de consulter ne peut être affichée car elle utilise un type de compression invalide ou non pris en charge.</p>
+ <ul>
+ <li>Veuillez contacter les propriétaires du site web pour les informer de ce problème.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresse introuvable</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Le navigateur n’a pas pu trouver le serveur hôte pour l’adresse indiquée.</p>
+ <ul>
+ <li>Vérifiez la syntaxe de l’adresse (saisie de <strong>ww</strong>.example.com au lieu de <strong>www</strong>.example.com par exemple) ;</li>
+ <li>Si vous n’arrivez à naviguer sur aucun site, vérifiez la connexion données ou Wi-Fi de votre appareil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Aucune connexion internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Vérifiez votre connexion réseau ou essayez d’actualiser la page dans quelques instants.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Actualiser</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Adresse invalide</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>L’adresse fournie n’est pas dans un format reconnu. Veuillez vérifier qu’il n’y a pas d’erreur dans la barre d’adresse et réessayez.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">L’adresse n’est pas valide</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>La syntaxe des adresses web est généralement <strong>http://www.example.com/</strong>.</li>
+ <li>Assurez-vous de bien utiliser des barres obliques (c.-à-d. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Le protocole n’a pas été reconnu</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>L’adresse indique un protocole (p. ex. <q>wxyz://</q>) inconnu du navigateur qui ne peut donc pas se connecter correctement au site.</p>
+ <ul>
+ <li>Essayez-vous d’accéder à du contenu multimédia ou d’autres services non texte ? Vérifiez les prérequis logiciels du site.</li>
+ <li>Certains protocoles peuvent nécessiter un logiciel tiers ou des plugins pour que le navigateur puisse les reconnaître.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fichier introuvable</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Le fichier a peut-être été renommé, supprimé ou déplacé ?</li>
+ <li>Y a-t-il une erreur d’orthographe, de majuscule ou une autre erreur typographique dans l’adresse ?</li>
+ <li>Avez-vous des permissions d’accès suffisantes pour ce fichier ?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">L’accès au fichier a été refusé</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Il a peut-être été supprimé, déplacé ou les permissions associées au fichier ne permettent pas d’y accéder.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">La connexion a été refusée par le serveur proxy</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Le navigateur est configuré pour utiliser un serveur proxy mais le proxy a refusé la connexion. </p>
+ <ul>
+ <li>La configuration proxy du navigateur est-elle correcte ? Vérifiez les paramètres et réessayez.</li>
+ <li>Le service proxy autorise-t-il les connexions à partir de ce réseau ?</li>
+ <li>Vous avez toujours des problèmes ? Consultez votre administrateur réseau ou votre fournisseur d’accès à Internet pour obtenir de l’aide.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Le serveur proxy est introuvable</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Le navigateur est configuré pour utiliser un serveur proxy mais le proxy est introuvable. </p>
+ <ul>
+ <li>La configuration proxy du navigateur est-elle correcte ? Vérifiez les paramètres et réessayez.</li>
+ <li>L’appareil est-il connecté au réseau ?</li>
+ <li>Vous avez toujours des problèmes ? Consultez votre administrateur réseau ou votre fournisseur d’accès à Internet pour obtenir de l’aide.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problème de logiciel malveillant</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Le site web à l’adresse %1$s a été signalé comme une source d’attaques et a été bloqué suivant vos préférences de sécurité.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problème de site indésirable</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Le site web à l’adresse %1$s a été signalé comme comportant des logiciels indésirables et a été bloqué suivant vos préférences de sécurité.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problème de site dangereux</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Le site web à l’adresse %1$s a été signalé comme potentiellement dangereux et a été bloqué suivant vos préférences de sécurité.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problème de site trompeur</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Le site web à l’adresse %1$s a été signalé comme étant trompeur et a été bloqué suivant vos préférences de sécurité.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Site sécurisé non disponible</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Vous avez activé le mode « HTTPS uniquement » pour une sécurité renforcée, mais il n’existe aucune version HTTPS du site <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuer vers le site HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..898afdf079
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fur/strings.xml
@@ -0,0 +1,313 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Torne prove</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Impussibil completâ la richieste</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Pal moment no son disponibilis altris informazions su chest probleme o erôr.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Conession sigure falide</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Nol è pussibil visualizâ la pagjine che tu stâs cirint di viodi parcè che nol è stât pussibil verificâ la autenticitât dai dâts ricevûts.</li>
+ <li>Par plasê contate i proprietaris dal sît web par informâju di chest probleme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Conession sigure falide</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Chest al podarès jessi un probleme cu la configurazion dal servidôr, opûr al podarès jessi che cualchidun al stedi cirint di impersonâ il servidôr.</li>
+ <li>Se in passât la conession a chest servidôr e leve, al podarès stâi che l’erôr al sedi temporani e che tu puedis provâ di gnûf plui tart.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzadis…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Cualchidun al podarès cirî di sostituîsi al sît origjinâl e o disconseìn di continuâ.</label>
+ <br><br>
+ <label>I sîts web a garantissin la lôr identitât midiant certificâts. %1$s nol considere atendibil il sît <b>%2$s</b> parcè che l’emitent dal so certificât nol è cognossût, il certificât al è auto-firmât opûr il servidôr nol à mandât i certificâts intermedis previodûts.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Torne indaûr (conseât)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Acete il risi e continue</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Chest sît web al domande une conession sigure.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Nol è pussibil mostrâ la pagjine che tu stâs cirint di visualizâ parcè che chest sît web al domande une conession sigure.</li>
+ <li>Al è probabil che il probleme al stedi tal sît web e duncje nol è nuie che tu podedis fâ par risolvilu.</li>
+ <li>Tu puedis però notificâ il probleme al aministradôr dal sît web.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanzadis…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> al à une politiche di sigurece clamade HTTP Strict Transport Security (HSTS), che al significhe che <b>%2$s</b> al pues conetisi dome in maniere sigure e no tu puedis zontâ une ecezion par visitâ chest sît. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Torne indaûr</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La conession e je stade interote</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Il navigadôr si è conetût cun sucès, ma la conession e je stade interote dilunc il trasferiment des informazions. Torne prove.</p>
+ <ul>
+ <li>Al è pussibil che il sît nol sedi disponibil in mût temporani o che al sedi masse ocupât. Torne prove chi di pôc.</li>
+ <li>Se no tu rivis a cjariâ nissune pagjine, controle i dâts dal dispositîf o la conession Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">La conession e je lade fûr timp massim</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Il sît domandât nol à rispuindût a une richieste di conession e il navigadôr al à fermât di spietâ une rispueste.</p>
+ <ul>
+ <li>Isal pussibil che il servidôr al vedi une richieste masse elevade o une interuzion temporanie? Torne prove plui indenant.</li>
+ <li>Rivistu a visitâ altris sîts? Controle la conession di rêt dal to dispositîf.</li>
+ <li>Sono il to dispositîf o la rêt protets di un firewall o un proxy? Lis impostazions sbaliadis a puedin interferî cu la navigazion sul web.</li>
+ <li>Âstu ancjemò problemis? Consulte l’aministradôr de rêt o il furnidôr di acès a internet par vê assistence.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Impussibil conetisi</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Al è pussibil che il sît nol sedi disponibil in maniere temporanie opûr masse ocupât. Torne prove chi di pôc.</li>
+ <li>Se no tu rivis a cjariâ nissune pagjine, controle i dâts dal to dispositîf o la conession Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Rispueste inspietade dal servidôr</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Ilsît al à rispuindût ae richieste di rêt intune maniere inspietade e il navigadôr nol pues continuâ.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Il gnûf-dirotament di cheste pagjine nol funzione ben</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Il navigadôr al à fermât di cirî di recuperâ l’element domandât. Il sît al sta tornant a indreçâ la richieste intune maniere che no vignarà mai completade.</p>
+ <ul>
+ <li>Âstu disabilitât o blocât i cookies necessaris a chest sît?</li>
+ <li>Se acetant i cookies dal sît il probleme al reste, al è probabil che al sedi un probleme di configurazion dal servidôr e no dal to dispositîf.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modalitât fûr rêt</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Il navigadôr al sta lavorant in modalitât fûr rêt e nol pues conetisi al element domandât.</p>
+ <ul>
+ <li>Il dispositîf isal conetût a une rêt ative?</li>
+ <li>Frache “Torne prove” par passâ ae modalitât in rêt e tornâ a cjariâ la pagjine.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Puarte limitade par resons di sigurece</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>La direzion domandade e à specificât une puarte (p.e. <q>mozilla.org:80</q> pe puarte 80 su mozilla.org) che di solit e ven doprade par <em>altris</em> finalitâts rispiet a chês di navigazion sul Web. Il navigadôr al à anulade la richieste pe tô protezion e sigurece.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La conession e je stade anulade</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Il colegament ae rêt al è stât interot dilunc la negoziazion di une conession. Torne prove.</p>
+ <ul>
+ <li>Al è pussibil che il sît nol sedi disponibil in maniere temporanie opûr masse ocupât. Torne prove chi di pôc.</li>
+ <li>Se no tu rivis a cjariâ nissune pagjine, controle i dâts dal dispositîf o la conession Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Gjenar di file no sigûr</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Contate il proprietari dal sît web par informâlu di chest probleme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Erôr di contignût comprometût</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Nol è pussibil mostrâ la pagjine che tu stâs cirint di visualizâ par vie che al è stât rilevât un erôr te trasmission dai dâts.</p>
+ <ul>
+ <li>Contate il proprietari dal sît web par informâlu di chest probleme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Contignût colassât</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Nol è pussibil mostrâ la pagjine che tu stâs cirint di visualizâ par vie che al è stât rilevât un erôr te trasmission dai dâts.</p>
+ <ul>
+ <li>Contate il proprietari dal sît web par informâlu di chest probleme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Erôr te codifiche dal contignût</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Nol è pussibil mostrâ la pagjine che tu stâs cirint di visualizâ parcè che no dopre un formât di compression valit o supuartât.</p>
+ <ul>
+ <li>Contate il proprietari dal sît web par informâlu di chest probleme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Direzion no cjatade</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Il navigadôr nol rive a cjatâ il servidôr host pe direzion indicade.</p>
+ <ul>
+ <li>Controle la direzion par erôrs di scriture come
+ <strong>ww</strong>.esempli.com al puest di
+ <strong>www</strong>.esempli.com.</li>
+ <li>Se no tu rivis a cjariâ nissune pagjine, controle i dâts dal to dispositîf o la conession Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nissune conession a internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifiche la tô conession di rêt o prove chi di pôc a tornâ a cjariâ la pagjine.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Torne cjame</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Direzion no valide</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>La direzion indicade no je intun formât ricognossût. Controle la sbare de direzion par cjatâ erôr e torne prove.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">La direzion no je valide</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Lis direzions dal web di solit a son scritis te forme <strong>http://www.esempli.com/</strong></li>
+ <li>Controle di vê doprât lis sbaris justis (vâl a dî <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocol no cognossût</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>La direzion e specifiche un protocol (p.e. <q>wxyz://</q>) che il navigadôr nol ricognòs, duncje il navigadôr nol pues conetisi al sît in maniere juste.</p>
+ <ul>
+ <li>Stâstu cirint di acedi a servizis multimediâi o altris servizis che no son testuâi? Controle sul sît i recuisîts necessaris.</li>
+ <li>Cualchi protocol al podarès domandâ software di tiercis parts o plugins par che il navigadôr ju podedi ricognossi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">File no cjatât</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Isal pussibil che l’element al vedi cambiât non, al sedi stât eliminât o spostât?</li>
+ <li>Isal un erôr di ortografie o scriture te direzion?</li>
+ <li>Âstu permès suficients par acedi al element domandât?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Acès al file dineât</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Al podarès jessi stât eliminât, spostât o i permès sul file a podaressin impedî l’acès.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Il servidôr proxy al à refudât la conession</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Il navigadôr al è configurât in mût di doprâ un servidôr proxy, ma il proxy al à refudât une conession.</p>
+ <ul>
+ <li>Ise juste la configurazions proxy dal navigadôr? Controle lis impostazions e torne prove.</li>
+ <li>Il servizi proxy permetial lis conessions di cheste rêt?</li>
+ <li>Âstu ancjemò problemis? Consulte l’aministradôr di rêt o il furnidôr di acès a internet pe assistence.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Impussibil contatâ il servidôr proxy</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Il navigadôr al è configurât in mût di doprâ un servidôr proxy, ma nol è stât pussibil cjatâ il proxy.</p>
+ <ul>
+ <li>La configurazion proxy dal navigadôr ise juste? Controle lis impostazions e torne prove.</li>
+ <li>Il dispositîf isal colegât a une rêt ative?</li>
+ <li>Âstu ancjemò problemis? Consulte il to aministradôr di rêt o il furnidôr di acès internet pe assistence.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Probleme di sît cun malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Il sît web %1$s al è stât segnalât tant che sît malevul e al è stât blocât daûr des tôs preferencis di sigurece.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Probleme di sît malvolût</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Il sît web %1$s al è stât segnalât tant che sît che al conten software malvolût e al è stât blocât daûr des tôs preferencis di sigurece.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Probleme di sît pericolôs</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Il sît web %1$s al è stât segnalât tant che sît potenzialmentri pericolôs e al è stât blocât daûr des tôs preferencis di sigurece.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Probleme di sît ingjanôs</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Il sît web %1$s al è stât segnalât tant che sît ingjanôs e al è stât blocât daûr des tôs preferencis di sigurece.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Version sigure dal sît no disponibile</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Tu âs ativade la modalitât dome HTTPS par vê une sigurece miorade, ma no je disponibile un version HTTPS di <em>%1$s</em> .]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continue sul sît HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..a24c7d673a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,462 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Opnij probearje</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Kin oanfraach net foltôgje</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+
+ <p>Ekstra ynformaasje oer dit probleem of dizze flater is op dit stuit net beskikber.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Befeilige ferbining mislearre</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>De side dy’t jo besjen wolle kin net toand wurde, omdat de echtheid fan de ûntfongen gegevens net ferifiearre wurde kin.</li>
+
+ <li>Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.</li>
+
+ </ul>
+
+
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Befeilige ferbining mislearre</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+
+ <ul>
+
+ <li>Dit kin in probleem mei de serverkonfiguraasje wêze, of ien probearret de server as in oar foar te dwaan.</li>
+
+ <li>As jo earder al mei sukses ferbining mei dizze server hân hawwe, kin de flater tydlik wêze en kinne jo it letter nochris probearje.</li>
+
+ </ul>
+
+
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avansearre…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+        <label>Ien kin probearje de side nei te meitsjen en jo moatte net trochgean.</label>
+        <br><br>
+        <label>Websites bewize harren identiteit troch sertifikaten. %1$s fertrout <b>%2$s</b> net, omdat syn sertifikaatútjouwer ûnbekend is, it sertifikaat selsûndertekene is, of de server stjoert net de krekte tuskensertifikaten.</label>
+    ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Tebekgean (Oanrekommandearre)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">It risiko oanfurdigje en trochgean</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Dizze webside fereasket in befeilige ferbining.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>De side dy’t jo besykje te besjen kin net werjûn wurde, omdat dizze webside in befeilige ferbining fereasket.</li>
+ <li>It probleem leit nei alle gedachten by de website, en jo kinne neat dwaan om it op te lossen.</li>
+ <li>Jo kinne de behearder fan de website ynformearje oer it probleem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avansearre…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> hat in befeiligingsbelied mei de namme HTTP Strict Transport Security (HSTS), wat betsjut dat <b>%2$s</b> allinnich in befeilige ferbining dêrmei meitsje kin. Jo kinne gjin útsûndering tafoegje om dizze website te besykjen. </label>
+
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Tebek</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">De ferbining waard ferbrutsen</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+
+ <p>De browser is suksesfol ferbûn, mar de ferbining waard ferbrutsen by it oerdragen fan ynformaasje. Probearje it opnij.</p>
+
+ <ul>
+
+ <li>De website kin tydlik net beskikber of te drok wêze. Probearje it oer in pear mominten opnij.
+</li>
+
+ <li>As jo gjin siden lade kinne, kontrolearje dan de gegevens of de wifi-ferbining fan jo apparaat.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">De wachttiid foar de ferbining is ferstrutsen</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+
+ <p>De opfrege website hat net op in ferbiningsfersyk antwurde en de browser wachtet net mear op in antwurd.</p>
+
+ <ul>
+
+ <li>Miskien ûnderfynt de server in hege fraach of in tydlike stroomûnderbrekking? Probearje it letter opnij.</li>
+
+ <li>Kinne jo gjin oare websites besykje? Kontrolearje de netwurkferbining fan it apparaat.</li>
+
+ <li>Wurdt jo apparaat of netwurk beskerme troch in firewall of proxy? Ferkearde ynstellingen kinne in goede wurking wylst it webbrowsen tsjin gean.</li>
+
+ <li>Hawwe jo noch hieltyd problemen? Freegje jo netwurkbehearder of ynternetprovider foar assistinsje.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Kin gjin ferbining meitsje</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+
+ <ul>
+
+ <li>Miskien is de website tydlik net beskikber of oerbelêste. Probearje it oer inkelde mominten opnij.</li>
+
+ <li>As jo gjin inkelde side lade kinne, kontrolearje dan de gegevens- of wifi-ferbining fan jo apparaat.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Unferwacht antwurd fan de server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+
+ <p>De website antwurde op in ûnferwachte manier op de netwurkoanfraach en de browser kin net trochgean.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">De side ferwiist net op in krekte wize troch</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+
+ <p>De browser is stoppe mei te probearjen it opfrege item op te heljen. It website ferwiist de oanfraach troch op in manier dy’t nea dien wêze sil.</p>
+
+ <ul>
+
+ <li>Hawwe jo cookies dy’t nedich binne foar dizze website útskeakele of blokkearre?</li>
+
+ <li>As it akseptearjen fan cookies fan dizze website it probleem net oplost is it wierskynlik in serverkonfiguraasjeprobleem en net jo apparaat.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offlinemodus</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+
+ <p>De browser wurket op dit stuit sûnder ferbining en kin net ferbine mei it frege ûnderdiel.</p>
+
+ <ul>
+
+ <li>Is it apparaat ferbûn mei in aktyf netwurk?</li>
+
+ <li>Klik op ‘Opnij probearje’ om nei onlinemodus te gean en laad de side opnij.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Poarte beheind om feilichheidsredenen</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+
+ <p>It opfrege adres spesifisearret in poarte (byg. <q>mozilla.org:80</q> foar poarte 80 op mozilla.org) dy’t normaal sprutsen foar <em>oare</em> doeleinen as websneupjen brûkt wurdt. De browser hat it fersyk foar jo beskerming en feilichheid annulearre.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">De ferbining waard opnij inisjalisearre</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+
+ <p>De netwurkkeppeling waard ûnderbrutsen wylst it ûnderhanneljen oer in ferbining. Probearje it opnij.</p>
+
+ <ul>
+
+ <li>De website is mooglik tydlik net beskikber of te drok. Probearje it oer in oantal eagenblikken opnij.</li>
+
+ <li>As jo gjin siden lade kinne, kontrolearje dan de gegevens- of wifi-ferbining fan jo apparaat.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Unfeilich bestânstype</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+
+ <ul>
+
+ <li>Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.</li>
+
+ </ul>
+
+
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Skansearre-ynhâldsflater</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+
+ <p>De side dy’t jo besjen wolle kin net werjûn wurde, omdat der in flater yn de gegevensoerdracht detektearre is.</p>
+
+ <ul>
+
+ <li>Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Ynhâld ferûngelokke</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+
+ <p>De side dy’t jo besjen wolle kin net werjûn wurde, omdat der in flater yn de gegevensoerdracht detektearre is.</p>
+
+ <ul>
+
+ <li>Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Ynhâldkodearringsflater</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+
+ <p>De side dy’t jo probearje te besjen kin net werjûn wurde, omdat it gebrûk makket fan in ûnjildige of net stipe foarm fan kompresje.</p>
+
+ <ul>
+
+ <li>Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adres net fûn</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+
+ <p>De browser koe de hostserver foar it opjûne adres net fine.</p>
+
+ <ul>
+ <li>Kontrolearje it adres op typeflaters, lykas
+
+ <strong>ww</strong>.example.com yn stee fan
+
+ <strong>www</strong>.example.com.</li>
+
+ <li>As jo gjin siden lade kinne, kontrolearje dan de gegevens- of wifi-ferbining fan jo apparaat.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Gjin ynternetferbining</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Kontrolearje jo netwurkferbining of probearje de side oer in amerijke opnij te laden.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Opnij lade</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Unjildich adres</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+
+ <p>It opjûne adres hat gjin werkenbere yndieling. Kontrolearje de lokaasjebalke op flaters en probearje it opnij.</p>
+
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">It adres is net jildich</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+
+ <ul>
+
+ <li>Webadressen wurde trochgeans skreaun as <strong>http://www.example.com/</strong></li>
+
+ <li>Let der op dat jo foarweartse slashes brûke (d.y. <strong>/</strong>).</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Unbekend protokol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+
+ <p>It adres spesifisearret in protokol (byg. <q>wxyz://</q>) dat de browser net werkend, wêrtroch de browser net op in krekte manier mei de website ferbine kin.</p>
+
+ <ul>
+
+ <li>Probearje jo tagong te krijen ta multimedia- of oare net-tekstservices? Kontrolearje de website op ekstra nedichheden.</li>
+
+ <li>Guon protokollen kinne programmatuer of ynstekkers fan tredden fereaskje foardat de browser se werkenne kin.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Bestân net fûn</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+
+ <ul>
+
+ <li>Kin it item omneamd, fuortsmiten of ferpleatst wêze?</li>
+
+ <li>Stiet der in stavering-, haadletter- of oare typografyske flater yn it adres?</li>
+
+ <li>Hawwe jo genôch tagongsrjochten foar it opfrege item?</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Tagong ta it bestân is wegere</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+
+ <ul>
+
+ <li>It kin fuortsmiten wêze, ferpleatst, of bestânsmachtigingen kinne tagong tsjingean.</li>
+
+
+ </ul>
+
+
+
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxyserver wegere de ferbining</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+
+ <p>De browser is konfigurearre om in proxyserver te brûken, mar de proxy wegere in ferbining.</p>
+
+ <ul>
+
+ <li>Is de proxykonfiguraasje fan de browser in oarder? Kontrolearje de ynstellingen en probearje it opnij.</li>
+
+ <li>Stiet de proxyservice ferbiningen fan dit netwurk ta?</li>
+
+ <li>Hawwe jo noch hieltyd problemen? Freegje jo netwurkbehearder of ynternetprovider foar assistinsje.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxyserver net fûn</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+
+ <p>De browser is konfigurearre om in proxyserver te brûken, mar de proxy koe net fûn wurde.</p>
+
+ <ul>
+
+ <li>Is de proxykonfiguraasje fan de browser yn oarder? Kontrolearje de ynstellingen en probearje it opnij.</li>
+
+ <li>Is it apparaat ferbûn mei in aktyf netwurk?</li>
+
+ <li>Hawwe jo noch hieltyd problemen? Freegje jo netwurkbehearder of ynternetprovider foar assistinsje.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Probleem mei malware op website</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+
+ <p>De website op %1$s is rapportearre as in fertochte side en is blokkearre op basis fan jo befeiligingsfoarkarren.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Probleem mei net winske website</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+
+ <p>De website op %1$s is rapportearre as in website dy’t net-winske software oanbiedt en is blokkearre op basis fan jo befeiligingsfoarkarren.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Probleem mei skealike website</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+
+ <p>De website op %1$s is rapportearre as in fertochte side en is blokkearre op basis fan jo befeiligingsfoarkarren.</p>
+
+
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Probleem mei misliedende website</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+
+ <p>De website op %1$s is rapportearre as in misliedende website en is blokkearre op basis fan jo befeiligingsfoarkarren.</p>
+
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Befeilige website net beskikber</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Jo hawwe de Allinnich-HTTPS-modus foar ferbettere befeiliging ynskeakele, en in HTTPS-ferzje fan <em>%1$s</em> is net beskikber.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Trochgean nei HTTP-website</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..4549a9b958
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Bain Triail Eile As</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Ní féidir an t-iarratas a chríochnú</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Níl eolas breise faoin bhfadhb nó faoin earráid seo ar fáil faoi láthair.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Níorbh Fhéidir Ceangal Slán a Bhunú</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Ní féidir an leathanach atá tú ag iarraidh amharc air a thaispeáint toisc nach féidir fírinne na sonraí a fuarthas a fhíordheimhniú.</li>
+ <li>Téigh i dteagmháil le húinéirí an tsuímh Ghréasáin leis an fhadhb seo a chur in iúl dóibh.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Níorbh Fhéidir Ceangal Slán a Bhunú</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>B\'fhéidir gur fadhb i gcumraíocht an fhreastalaí atá ann, nó b\'fhéidir go bhfuil duine éigin ag déanamh aithrise ar an bhfreastalaí.</li>
+ <li>Má éiríonn leat ceangal leis an bhfreastalaí seo de ghnáth, b\'fhéidir gur fadhb shealadach í agus gur féidir leat iarracht a dhéanamh níos déanaí.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Ardroghanna…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Seans go bhfuil duine éigin ag dul i mbréagriocht ar an suíomh, agus níor chóir duit leanúint ar aghaidh.</label>
+ <br><br>
+ <label>Úsáideann suímh Ghréasáin teastais mar chruthúnas aitheantais. Níl muinín ag %1$s as <b>%2$s</b> toisc nach bhfuil eisitheoir an teastais aitheanta, go bhfuil an teastas féinsínithe, nó nach bhfuil an freastalaí ag soláthar na teastais idirmheánacha chearta.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Amach As Seo (Molta)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Tuigim an Baol. Ar Aghaidh Linn!</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Idirbhriseadh an ceangal</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>D\'éirigh leis an mbrabhsálaí ceangal a bhunú, ach briseadh isteach air agus na sonraí á n-aistriú. Bain triail eile as.</p>
+ <ul>
+ <li>Seans go bhfuil fadhb shealadach leis an suíomh, go bhfuil sé róghnóthach faoi láthair. Bain triail eile as i gceann nóiméid.</li>
+ <li>Mura bhfuil tú in ann aon leathanach a lódáil, déan seiceáil ar do cheangal sonraí nó Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ceangal imithe thar am</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Níor fhreagair an suíomh iarrtha iarratas ceangail agus tá an brabhsálaí éirithe as bheith ag fanacht ar fhreagra.</p>
+ <ul>
+ <li>An féidir go bhfuil éileamh mór nó fadhb shealadach ar an bhfreastalaí? Triail arís é níos déanaí.</li>
+ <li>Mura bhfuil tú in ann suímh eile a bhrabhsáil, déan seiceáil ar cheangal líonra do ríomhaire.</li>
+ <li>An bhfuil do ríomhaire nó do líonra á chosaint ag seachfhreastalaí nó balla dóiteáin? D\'fhéadfadh socruithe mícheart cur isteach ar bhrabhsáil.</li>
+ <li>An bhfuil fadhbanna agat fós? Téigh i dteagmháil le do riarthóir líonra nó le do sholáthraí Idirlín le haghaidh cabhrach.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Ní féidir ceangal a bhunú</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>D\'fhéadfadh nach bhfuil fáil ar an suíomh nó go bhfuil sé róghnóthach faoi láthair. Bain triail eile as ar ball beag.</li>
+ <li>Mura bhfuil tú in ann leathanach ar bith a lódáil, cinntigh ceangal sonraí nó Wi-Fi do ghléis.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Freagra gan súil leis ón bhfreastalaí</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Ní rabhthas ag súil leis an bhfreagra ar an iarratas líonra a bhfuarthas ón suíomh agus ní féidir leis an mbrabhsálaí leanúint.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Níl an leathanach ag atreorú i gceart</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Tá an brabhsálaí éirithe as bheith ag iarraidh an mhír iarrtha a fháil. Tá an suíomh ag atreorú an iarratais i slí nach gcríochnóidh choíche.</p>
+ <ul>
+ <li>An bhfuil fianáin atá riachtanach don suíomh seo díchumasaithe nó coiscthe agat?</li>
+ <li>Mura bhfuil an fhadhb réitithe tar éis duit glacadh le fianáin an tsuímh, gach seans nach le do ríomhaire a bhaineann an cheist ach le cumraíocht an fhreastalaí.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mód As Líne</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Tá an brabhsálaí sa mhód as líne agus ní féidir leis ceangal leis an mhír iarrtha.</p>
+ <ul>
+ <li>An bhfuil an ríomhaire ceangailte le líonra beo?</li>
+ <li>Brúigh “Bain Triail Eile As” chun dul ar líne agus an leathanach a lódáil arís.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Tá an port srianta de bharr cúrsaí slándála</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Shonraigh an seoladh iarrtha port (m.sh. <q>mozilla.org:80</q> le haghaidh poirt 80 ar mozilla.org) a úsáidtear de ghnáth le haghaidh cúiseanna <em>seachas</em> brabhsáil. Chealaigh an brabhsálaí d\'iarratas ar mhaithe le do shlándáil.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Athshocraíodh an ceangal</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Briseadh an nasc líonra agus ceangal á bhunú. Bain triail eile as.</p>
+ <ul>
+ <li>Seans go bhfuil fadhb shealadach leis an suíomh, nó go bhfuil sé róghnóthach faoi láthair. Bain triail eile as i gceann nóiméid.</li>
+ <li>Mura bhfuil tú in ann aon leathanach a lódáil, déan seiceáil ar do cheangal sonraí nó Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Cineál Comhaid Baolach</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Téigh i dteagmháil le húinéirí an tsuímh ghréasáin leis an bhfadhb a chur in iúl dóibh.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Earráid: Ábhar Truaillithe</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>An leathanach atá tú ag iarraidh a amharc, ní féidir é a thaispeáint toisc gur tharla earráid agus na sonraí á seoladh.</p>
+ <ul>
+ <li>Téigh i dteagmháil le húinéir an tsuímh agus inis dóibh faoin fhadhb seo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Ábhar tuairteála</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>An leathanach atá tú ag iarraidh a amharc, ní féidir é a thaispeáint toisc gur tharla earráid agus na sonraí á seoladh.</p>
+ <ul>
+ <li>Téigh i dteagmháil le húinéir an tsuímh agus inis dóibh faoin fhadhb seo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Earráid: Ionchódú Ábhair</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>An leathanach atá tú ag iarraidh a amharc, ní féidir é a thaispeáint, agus comhbhrú ann nach bhfuil bailí nó nach bhfuil tacaíocht leis.</p>
+ <ul>
+ <li>Téigh i dteagmháil le húinéirí an tsuímh leis an bhfadhb a chur in iúl dóibh.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Seoladh Gan Aimsiú</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Níorbh fhéidir leis an mbrabhsálaí óstach don seoladh iarrtha a aimsiú.</p>
+ <ul>
+ <li>Deimhnigh nach bhfuil botún clóscríofa sa seoladh, ar nós
+ <strong>ww</strong>.example.com in áit
+ <strong>www</strong>.example.com.</li>
+ <li>Mura bhfuil tú in ann aon leathanach a lódáil, déan seiceáil ar do cheangal sonraí nó Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Gan ceangal Idirlín</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Déan seiceáil ar do cheangal líonra, nó bain triail as an leathanach a athlódáil i gceann nóiméid.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Athlódáil</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Seoladh Neamhbhailí</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Ní aithnítear formáid an tseolta sonraithe. Féach an bhfuil botúin i mbarra na suíomhanna agus bain triail eile as.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Níl an seoladh bailí</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>De ghnáth scríobhtar seoltaí Gréasáin mar seo <strong>http://www.example.com/</strong></li>
+ <li>Bí cinnte gur tulslaiseanna atá agat (.i. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Prótacal Anaithnid</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Sonraíonn an seoladh prótacal (m.sh. <q>wxyz://</q>) nach n-aithníonn an brabhsálaí agus ní féidir leis an mbrabhsálaí ceangal leis an suíomh i gceart dá bharr.</p>
+ <ul>
+ <li>An bhfuil tú ag iarraidh seirbhísí ilmheáin, nó seirbhísí eile nach téacs iad, a rochtain? Féach an bhfuil riachtanais bhreise ag an suíomh.</li>
+ <li>D\'fhéadfadh bogearraí nó forlíontáin ó sholáthraithe eile bheith ag teastáil ó phrótacail áirithe sula mbeidh an brabhsálaí in ann iad a aithint.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Comhad Gan Aimsiú</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>An féidir go bhfuil an mhír athainmnithe, bainte, nó bogtha?</li>
+ <li>An bhfuil botún litrithe, nó ceannlitir mhícheart, nó botún eile den sórt sin, sa seoladh?</li>
+ <li>An bhfuil na ceadanna rochtana cuí agat don mhír iarrtha?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Diúltaíodh rochtain ar an gcomhad</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Seans nach bhfuil an comhad ann a thuilleadh, nó b\'fhéidir nach bhfuil cead agat é a rochtain.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Dhiúltaigh an Seachfhreastalaí leis an gCeangal</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Tá an brabhsálaí cumraithe le seachfhreastalaí a úsáid, ach dhiúltaigh an seachfhreastalaí le ceangal.</p>
+ <ul>
+ <li>An bhfuil cumraíocht seachfhreastalaí cheart ar an mbrabhsálaí? Déan seiceáil ar na socruithe agus triail arís é.</li>
+ <li>An dtugann an tseirbhís seachfhreastalaí cead do cheangail ón líonra seo?</li>
+ <li>An bhfuil fadhbanna agat fós? Téigh i dteagmháil le do riarthóir líonra nó le do sholáthraí Idirlín le haghaidh cabhrach.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Seachfhreastalaí Gan Aimsiú</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Tá an brabhsálaí socruithe chun seachfhreastalaí a úsáid, ach níorbh fhéidir an seachfhreastalaí a aimsiú.</p>
+ <ul>
+ <li>An bhfuil cumraíocht an tseachfhreastalaí ceart? Déan seiceáil ar na socruithe agus triail arís é.</li>
+ <li>An bhfuil an ríomhaire ceangailte le líonra beo?</li>
+ <li>An bhfuil fadhbanna agat fós? Téigh i dteagmháil le do riarthóir líonra nó le do sholáthraí Idirlín le haghaidh cabhrach.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Suíomh le bogearraí mailíseacha</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Tuairiscíodh gur suíomh ionsaithe é an suíomh %1$s agus tá cosc curtha air de bharr do roghanna slándála.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Suíomh ar a bhfuil bogearraí gan iarraidh</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Tuairiscíodh go bhfuil bogearraí gan iarraidh ar an suíomh %1$s agus tá cosc curtha air de bharr do roghanna slándála.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Suíomh díobhálach</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Tuairiscíodh gur suíomh díobhálach é an suíomh %1$s agus tá cosc curtha air de bharr do roghanna slándála.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Suíomh cealgach</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Tuairiscíodh gur suíomh cealgach é an suíomh %1$s agus cuireadh cosc air de bharr do roghanna slándála.</p>
+ ]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..11e1f9a4b6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gd/strings.xml
@@ -0,0 +1,263 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Feuch ris a-rithist</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Cha ghabh an t-iarrtas a choileanadh</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Chan eil fiosrachadh a bharrachd ann mun duilgheadas no mun mhearachd seo an-dràsta.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Dh’fhàillig an ceangal tèarainte</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Cha ghabh an duilleag a tha thu airson faicinn a shealltainn a chionn ’s nach gabh dearbhachd an dàta a fhuaradh a dhearbhadh.</li>
+ <li>Cuir fios gun fheadhainn aig a bheil an làrach-lìn gu bheil a leithid de dhuilgheadas ann.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Dh’fhàillig an ceangal tèarainte</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Dh’fhaodadh gur e duilgheadas le rèiteachadh an fhrithealaiche a tha ann no gu bheil cuideigin a’ feuchainn ri gabhail orra gur iadsan am frithealaiche.</li>
+ <li>Ma chaidh agad air ceangal a dhèanamh ris an fhrithealaiche seo roimhe, ’s mathaid nach mair a’ mhearachd agus feuch ris a-rithist an ceann tamaill.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Adhartach…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Dh’fhaoidte gu bheul cuideigin a’ leigeil orra gur iadsan an làrach sin agus cha bu chòir dhut leantainn air adhart.</label>
+ <br><br>
+ <label>Dearbhaidh làraichean-lìn cò iad slighe theisteanasan. Chan eil earbsa aig %1$s ann an <b>%2$s</b> a chionn ’s nach aithne dhuinn foillsichear an teisteanais, tha an teisteanas air a fèin-soidhneadh no chan eil am frithealaichte a’ cur nan teisteanasan eadar-mheadhanach ceart.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Air ais (Mholamaid seo)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Tuigidh mi an cunnart, air adhart leam</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Feumaidh an làrach-lìn seo ceangal tèarainte.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Chan urrainn dhuinn an duilleag seo a shealltainn dhut a chionn ’s gum feum an làrach-lìn seo ceangal tèarainte.</li>
+ <li>Mar is trice, ’s ann aig an làrach-lìn a bhios an duilgheadas agus chan eil dad ann as urrainn dhut-sa a dhèanamh airson a chur ceart.</li>
+ <li>Ach is urrainn dhut innse do rianaire na làraich-lìn gu bheil an duilgheadas seo ann.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Adhartach…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> Tha poileasaidh tèarainteachd aig <b>%1$s</b> air a bheil HTTP Strict Transport Security (HSTS), agus is ciall dha sin nach urrainn dha <b>%2$s</b> ach ceangal tèarainte a dhèanamh. Chan urrainn dhut eisgeachd a chur ris a thadhal air an làrach seo.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Air ais</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Bhris rudeigin a-steach air a’ cheangal</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Rinn am brabhsair ceangal ach bhris an ceangal nuair a bha fiosrachadh ga chur. Feuch ris a-rithist</p>
+ <ul>
+ <li>Dh’fhaoidte nach eil an làrach ri fhaighinn an-dràsta fhèin no gu bheil e trang. Feuch ris a-rithist ann an tiotag.</li>
+ <li>Mur urrainn dhut duilleag sam bith a luchdadh, thoir sùil air a’ cheangal dàta no WiFi aig an uidheam agad.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Dh’fhalbh an ùine air a’ cheangal</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Cha do fhreagair an làrach a dh’iarr thu ris an iarrtas cheangail is sguir am brabhsair dhen a bhith a’ feitheamh ri freagairt.</p>
+ <ul>
+ <li>Saoil a bheil fèill mhòr air an fhrithealaiche an-dràsta fhèin no gu bheil e sìos rè seal? Feuch ris a-rithist an ceann greis.</li>
+ <li>Mura faic thu làraichean eile, cuir sùil air ceangal a’ choimpiutair agad ris an lìonra.</li>
+ <li>A bheil an coimpiutair agad ga dhìon le cachaileith-theine no progsaidh? Faodadh roghainnean cearra cur a-steach air seòladh an lìn.</li>
+ <li>A bheil duilgheadas agad fhathast? Bruidhinn ri rianaire an lìonraidh agad no ris an fhrithealaiche-lìn airson cobhair.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Cha ghabh ceangal a dhèanamh ris</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Dh’fhaodadh nach eil an làrach seo ri faighinn rè seal no gu bheil e ro thrang. Feuch ris a-rithist an ceann greis.</li>
+ <li>Mur urrainn dhut duilleag sam bith a ruigsinn, cuir sùil air ceangal dàta no WiFi an uidheim agad.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Freagairt ris nach robh dùil on fhrithealaiche</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Fhreagair an làrach ri iarrtas an lìonraidh air dòigh ris nach robh dùil ’s chan urrainn dhan bhrabhsair leantainn air adhart.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Chan eil an duilleag ag ath-stiùireadh mar bu chòir</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Sguir am brabhsair dhen a bhith a’ feuchainn an rud a dh’iarr thu fhaighinn. Tha an làrach ag ath-stiùireadh an iarrtais air dòigh nach tèid a choileanadh gu bràth.</p>
+ <ul>
+ <li>An do chuir briosgaidean à comas o chionn goirid a dh’fheumas an làrach seo no an do bhac thu iad?</li>
+ <li>Mura dèid a’ chùis a rèiteachadh ’s tu a’ gabhail ri briosgaidean na làraich seo, tha coltas gur e adhbhar rèiteachadh an fhrithealaiche a tha ag adhbharachadh seo ’s chan e an t-uidheam agad.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modh far loidhne</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Tha am brabhsair agad sa mhodh far loidhne ’s chan urrainn dha ceangal ris an rud a dh’iarr thu.</p>
+ <ul>
+ <li>A bheil an t-uidheam seo co-cheangailte ri lìonra beò?</li>
+ <li>Brùth “Feuch ris a-rithist” gus a dhol air loidhne is luchdaich an duilleag a-rithist an uairsin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Tha am port seo cuingichte air sgàth adhbharan tèarainteachd</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Shònraich an seòladh a chaidh iarraidh port (m.e. <q>mozilla.org:80</q> airson port 80 air mozilla.org) a bhios ga chleachdadh a chum adhbharan eile <em>seach</em> seòladh an lìn. Chuir am brabhsair casg air an iarrtas airson do dhìon is do thèarainteachd.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Chaidh an ceangal ath-shuidheachadh</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Bhris rudeigin a-steach air a’ cheangal ris an lìonra fhad ’s a bha sinn a’ rèiteachadh ceangail. Feuch ris a-rithist.</p>
+ <ul>
+ <li>Dh’fhaoidte nach eil an làrach ri fhaighinn an-dràsta fhèin no gu bheil e trang. Feuch ris a-rithist ann an tiotag.</li>
+ <li>Mur urrainn dhut duilleag sam bith a luchdadh, thoir sùil air a’ cheangal dàta no WiFi aig an uidheam agad.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Faidhle de sheòrsa neo-thèarainte</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Nach leig thu fios air an duilgheadas seo gun fheadhainn aig a bheil an làrach-lìn?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Mearachd air sgàth susbaint thruaillte</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Cha ghabh an duilleag a dh’iarr thu a shealltainn a chionn ’s gun deach mearachd a lorg ann an tar-chur an dàta.</p>
+ <ul>
+ <li>Nach leig thu fios do sheilbheadairean na làraich-lìn mun duilgheadas seo?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Thuislich an t-susbaint</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Cha ghabh an duilleag a dh’iarr thu a shealltainn a chionn ’s gun deach mearachd a lorg ann an tar-chur an dàta.</p>
+ <ul>
+ <li>Nach leig thu fios do sheilbheadairean na làraich-lìn mun duilgheadas seo?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Mearachd le còdachadh na susbaint</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Cha ghabh an duilleag a tha thu airson faicinn a shealltainn a chionn ’s gu bheil e a’ cleachdadh dùmhlachadh mì-dhligheach no feadhainn nach eil taic ann dha.</p>
+ <ul>
+ <li>Leig fios gu muinntir na làraich mun duilgheadas seo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Cha deach an seòladh a lorg</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Cha b’ urrainn dhan bhrabhsair frithealaichte an òstair a lorg airson an t-seòlaidh a chaidh a thoirt seachad.</p>
+ <ul>
+ <li>Thoir sùil air an t-seòladh is dèan cinnteach nach eil mearachdan cumanta ann mar
+ <strong>ww</strong>.example.com an àite
+ <strong>www</strong>.example.com.</li>
+ <li>Mur urrainn dhut duilleag sam bith a luchdadh, thoir sùil air a’ cheangal dàta no WiFi aig an uidheam agad.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Chan eil ceangal ris an eadar-lìon</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Thoir sùil air a’ cheangal agad ris an lìonra no feuch is ath-luchdaich an duilleag ann an tiotan no dhà.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Ath-luchdaich</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Seòladh mì-dhligheach</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Tha an seòladh a chuir thu ann am fòrmat neo-aithnichte. Dèan cinnteach nach eil mearachdan sa bhàr suidheachaidh agus feuch ris a-rithist.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Chan e seòladh dligheach a tha ann</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Bidh seòlaidhean-lìn coltach ris an fhear seo a ghnàth: <strong>http://www.example.com/</strong></li>
+ <li>Dèan cinnteach gu bheil thu a’ cleachdadh slaisichean (.i. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Pròtacal neo-aithnichte</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Tha an seòladh a’ sònrachadh pròtacal (m.e. <q>wxyz://</q>) nach eil am brabhsair ag aithneachadh ’s chan urrainn dhan bhrabhsair ceangal ris an làrach mar bu chòir.</p>
+ <ul>
+ <li>A bheil thu a’ feuchainn ri inntrigeadh fhaighinn gu seirbheisean ioma-mheadhanach no fheadhainn neo-theacsach eile? Cuir sùil air an làrach ’s faigh a-mach a bheil riatanasan sònraichte ann.</li>
+ <li>Feumaidh cuid dhe na pròtacalan bathar-bog de threas pàrtaidh no plugain mus aithnich am brabhsair iad.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Cha deach am faidhle a lorg</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Saoil an deach ainm eile a chur air an fhaidhle, no gun deach a thoirt air falbh no a ghluasad?</li>
+ <li>A bheil mearachd litreachaidh (a’ gabhail a-steach litrichean mòra/beaga) san t-seòladh?</li>
+ <li>A bheil cead iomchaidh agad gus an rud iarraidh?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Chaidh inntrigeadh dhan fhaidhle a dhiùltadh</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Dh’fhaoidte gun deach a thoirt air falbh no a ghluasad no gu bheil bacadh air inntrigeadh an cois ceadan an fhaidhle.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Dhiùlt am frithealaiche progsaidh an ceangal</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Tha am brabhsair air a rèiteachadh gus frithealaiche progsaidh a chleachdadh ach dhiùlt a’ phrogsaidh ceangal.</p>
+ <ul>
+ <li>A bheil rèiteachadh progsaidh a’ bhrabhsair ceart? Cuir sùil air na roghainnean is feuch ris a-rithist.</li>
+ <li>A bheil an t-seirbheis progsaidh seo a’ toirt cead do cheanglaichean on lìonra seo?</li>
+ <li>Duilgheadasan agad fhathast? Leig fios gu rianaire an lìonraidh agad no gun fhrithealaiche-lìn agad.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Cha deach frithealaiche progsaidh a lorg</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Tha am brabhsair air a rèiteachadh gus frithealaiche progsaidh a chleachdadh ach cha deach progsaidh a lorg.</p>
+ <ul>
+ <li>A bheil rèiteachadh progsaidh a’ bhrabhsair ceart? Cuir sùil air na roghainnean is feuch ris a-rithist.</li>
+ <li>A bheil an t-uidheam co-cheangailte ri lìonra beò?</li>
+ <li>Duilgheadasan agad fhathast? Leig fios gu rianaire an lìonraidh agad no gun fhrithealaiche-lìn agad airson cuideachadh.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Duilgheadas le bathar-bog droch-rùnach air an làrach</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Chaidh aithris gur e làrach ionnsaighe a tha san làrach %1$s is chaidh bacadh a chur air a-rèir do roghainnean tèarainteachd.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Duilgheadas le bathar-bog gun iarrtas</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Chaidh aithris gur e làrach a sgaoileas bathar-bog gun iarrtas a tha san làrach %1$s is chaidh bacadh a chur air a-rèir do roghainnean tèarainteachd.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Duilgheadas le làrach cunnartach</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Chaidh aithris gur e làrach cunnartach a tha san làrach %1$s is chaidh bacadh a chur air a-rèir do roghainnean tèarainteachd.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Duilgheadas le làrach foille</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Chaidh aithris gur e làrach foille a tha san duilleag-lìn seo air %1$s is chaidh bacadh a chur air a-rèir do roghainnean tèarainteachd.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Chan eil làrach thèarainte ri fhaighinn</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Chuir thu am modh HTTPS a-mhàin an comas a chùm barrachd tèarainteachd ach chan eil tionndadh HTTPS dhe <em>%1$s</em> ri fhaighinn.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Lean air adhart gun làrach-lìn HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..bf9a961099
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gl/strings.xml
@@ -0,0 +1,308 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Tentar de novo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Non é posíbel rematar a solicitude</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Non hai información adicional dispoñíbel neste momento acerca deste problema ou erro.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Fallou a conexión segura</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+<ul>
+ <li>Non é posíbel amosar a páxina que está a tentar visualizar porque non foi posíbel comprobar a autenticidade dos datos recibidos.</li>
+ <li>Contacte cos propietarios do sitio web para informalos deste problema.</li>
+</ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Fallou a conexión segura</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Isto podería ser un problema coa configuración do servidor ou pode ser alguén que está tentando facerse pasar polo servidor.</li>
+ <li>Se xa puido conectarse a este servidor de forma correcta, pode ser que o erro sexa temporal e pode tentalo máis tarde.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzadas…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Alguén podería estar intentando suplantar o sitio e non debería continuar. </label>
+ <br> <br>
+ <label> Os sitios web demostran a súa identidade mediante certificados. %1$s non confía en <b>%2$s</b> porque o seu emisor de certificados é descoñecido, o certificado está asinado por si mesmo, ou o servidor non está a enviar os certificados intermedios correctos.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Retroceder (recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceptar o risco e continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Este sitio web require unha conexión segura.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>A páxina que está tentando ver non se pode mostrar porque este sitio web require unha conexión segura.</li>
+ <li>O máis probable é que o problema estea relacionado co sitio web e non pode facer nada para resolvelo.</li>
+ <li>Pode notificarlle o problema ao administrador do sitio web.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanzadas…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ten unha política de seguranza chamada HTTP Strict Transport Security (HSTS), o que significa que <b>%2$s</b> só se pode conectar a ela de forma segura. Non pode engadir unha excepción para visitar este sitio. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Retroceder</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Interrompeuse a conexión</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>O navegador conectouse correctamente, pero a conexión foi interrompida ao transferir información. Ténteo de novo.</p>
+ <ul>
+ <li>O sitio podería estar dispoñible temporalmente ou estar demasiado ocupado. Téntao de novo nuns instantes.</li>
+ <li>Se non pode cargar ningunha páxina, comprobe os datos do dispositivo ou a conexión wifi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">A conexión esgotou o tempo</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>O navegador esgotou o tempo de espera de resposta porque o sitio solicitado non respondeu ao pedido de conexión.</p>
+ <ul>
+ <li>É posíbel que o servidor tivese un alto número de visitas ou estivese fóra de servizo? Tente de novo máis tarde.</li>
+ <li>Non consegue navegar por outros sitios? Verifique a conexión de rede do computador.</li>
+ <li>Están o computador ou a rede protexidos por un firewall ou por proxy? Unha configuración incorrecta pode interferir na exploración web.</li>
+ <li>Continúa a ter problemas? Consulte coa adminstración da rede ou co fornecedor da Internet para obter soporte técnico.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Non foi posíbel conectarse</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>O sitio podería estar non dispoñíbel temporalmente ou estar demasiado saturado. Tente acceder de novo nuns minutos.</li>
+ <li>Se non consegue cargar algunhas páxinas, comprobe a conexión de datos ou Wi-Fi do seu dispositivo móbil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Resposta inesperada do servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>O sitio respondeu de maneira inesperada á solicitude da rede e o navegador non pode continuar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">A páxina non está a redirixir correctamente</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>O navegador deixou de tentar recuperar o elemento solicitado. O sitio está a redireccionar o pedido dunha forma que nunca concluirá.</p>
+ <ul>
+ <li>Desactivou ou bloqueou cookies necesarias para este sitio?</li>
+ <li>Se aceptar as cookies do sitio non soluciona o problema, é probábel que se deba a un
+erro da configuración do servidor e non do computador.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo sen conexión</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>O navegador está a funcionar en modo sen conexión e non é posíbel conectar ao elemento solicitado.</p>
+ <ul>
+ <li>Está o seu dispositivo conectado a unha rede activa?</li>
+ <li>Prema en «Tentar de novo» para desactivar o modo sen conexión e recargar a páxina.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porto restrinxido por motivos de seguranza</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>O enderezo solicitado especificou un porto (por exemplo, <q>mozilla.org:80</q> para o porto 80 de mozilla.org) que normalmente <em>non</em> se utiliza para a exploración web. O explorador cancelou a súa solicitude para súa protección e seguranza.</p>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Reiniciouse a conexión</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Interrompeuse a ligazón de rede mentres se negociaba unha conexión. Ténteo de novo.</p>
+ <ul>
+ <li>O sitio podería estar dispoñible temporalmente ou estar demasiado ocupado. Ténteo de novo nuns instantes.</li>
+ <li>Se non pode cargar ningunha páxina, comprobe os datos do dispositivo ou a conexión wifi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de ficheiro inseguro</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Contacte cos propietarios do sitio web para informalos deste problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Erro de contido danado</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Non é posíbel amosar a páxina á que está a tentar acceder porque se detectou un erro na transmisión de datos.</p>
+ <ul>
+ <li>Contacte cos propietarios do sitio web para informalos deste problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">O contido estragouse</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Non é posíbel amosar a páxina a que está a tentar acceder porque se detectou un erro na transmisión de datos.</p>
+ <ul>
+ <li>Contacte cos propietarios do sitio web para informalos deste problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Erro de codificación do contido</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Non é posíbel amosar a páxina a que está a tentar acceder porque utiliza un formulario de compresión non compatíbel.</p>
+ <ul>
+ <li>Contacte cos propietarios do sitio web para informalos deste problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Non se atopou o enderezo</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>O navegador non atopou o servidor host para a dirección indicada.</p>
+ <ul>
+ <li>Comprobe o enderezo para atopar erros de escritura como
+ <strong>ww </strong>.exemplo.gal en vez de
+ <strong>www </strong>.exemplo.gal.</li>
+ <li>Se non pode cargar ningunha páxina, comprobe os datos do dispositivo ou a conexión wifi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Non hai conexión á internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Comprobe a súa conexión de rede ou tente volver a cargar a páxina nuns instantes.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Enderezo incorrecto</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Non se recoñece o formato do enderezo fornecido. Busque erros na barra de localización e tente de novo.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">O enderezo é incorrecto</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Normalmente, os enderezos web escríbense como <strong>http://www.exemplo.gal/</strong></li>
+ <li>Asegúrese de que está a usar barras inclinadas cara adiante (isto é <strong>/</strong>).</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo descoñecido</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>O enderezo especifica un protocolo que o navegador non recoñece (p.ex., <q>wxyz://</q>) e, por tanto, non consegue conectar correctamente co sitio.</p>
+ <ul>
+ <li>Está a tentar acceder a algún tipo de servizo multimedia ou outros servizos non textuais? Busque no sitio requisitos adicionais.</li>
+ <li>Algúns protocolos poden requirir software ou engadidos de terceiros para que o explorador consiga recoñecelos.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Non se atopou o ficheiro</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>É posíbel que o elemento fose renomeado, eliminado ou movido?</li>
+ <li>Hai algún erro ortográfico, de maiúsculas ou tipográfico no enderezo?</li>
+ <li>Ten vostede os permisos necesarios para acceder ao elemento solicitado?</li>
+</ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Denegouse o acceso ao ficheiro</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+<ul>
+ <li>Pode que fose retirado, movido ou os permisos do ficheiro impiden o acceso.</li>
+</ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">O servidor proxy rexeitou a conexión</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>O navegador está configurado para utilizar un servidor proxy mais o proxy rexeitou unha conexión.</p>
+ <ul>
+ <li>É correcta a configuración proxy do navegador? Comprobe a configuración e tente de novo.</li>
+ <li>Permite o servizo proxy conexións con esta rede?</li>
+ <li>Continúa a ter problemas? Consulte coa administración da rede ou co fornecedor da Internet para obter soporte técnico.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Non se atopou o servidor proxy</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>O navegador está configurado para utilizar un servidor proxy mais non foi posíbel
+atopalo.</p>
+ <ul>
+ <li>É correcta a configuración proxy do navegador? Comprobe a configuración e tente de novo.</li>
+ <li>Está o dispositivo conectado a unha rede activa?</li>
+ <li>Continúa a ter problemas? Consulte coa administración da rede ou co fornecedor da Internet para obter soporte técnico.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de sitio con software malicioso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>O sitio %1$s ten sido denunciado como sitio atacante e foi bloqueado segundo as súas preferencias de seguranza.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sitio non desexado</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>O sitio %1$s ten sido denunciado como sitio que serve software non desexado e foi bloqueado segundo as súas preferencias de seguranza.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sitio prexudicial</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>O sitio %1$s ten sido denunciado como sitio potencialmente pernicioso e foi bloqueado segundo as súas preferencias de seguranza.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de sitio enganoso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Esta páxina web en %1$s ten sido denunciada sitio enganoso e foi bloqueado segundo as súas preferencias de seguranza.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sitio seguro non dispoñíbel</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Vostede activou o modo con só HTTPS para mellorar a seguranza mais non hai unha versión con HTTPS de <em>%1$s</em> dispoñíbel.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar ao sitio con HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..78ab4bc0e3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gn/strings.xml
@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Eha’ãjey</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Ndaikatúi ojejapopa mba’ejerure</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Ko’ag̃aite ndaipóri marandu juapypyre ko apañuãi térã jejavýpe g̃uarã.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Jeikekatu ojavy</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Ko kuatiarogue ehechaseva hína ndaikatúi ojekuaa ndojehechajeykuaái rupi mba’ekuaarã moneĩmby og̃uahẽramóva.</li>
+ <li>Eñe’ẽ umi ñanduti renda jára ndive emombe’u hag̃ua ko apañuãi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Jeikekatu ojavy</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Kóva ikatu hína oiko apañuãi mohendahavusu ñemboheko rupive, térã oĩ omohendagueséva ko mohendahavusúpe.</li>
+ <li>Eike memeramovoi mohendahavusúpe ko mohendavusu rovake, ko jejavy ikatu sapy’aguávante, ha eha’ãkuaajey uperire.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Jehopyre…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Oĩvaicha omyendagueséva ko tendápe ha anive emba’apo.</label>
+ <br><br>
+ <label>Umi ñanduti renda oha’ã heraite mboajepyre rupive. %1$s ndojeroviái <b>%2$s</b> rehe pe mboajepyre me’ẽha mavave ndoikuaái, pe mboajepyre heraguapy ijehegui térã pe mohendahavusu ndoguerahaukái mboajepyrekuéra mbytegua iporãva.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Guevi (jeroviaháva)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Emoneĩ mbyaikuaa ha eku’ejey</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ko ñanduti renda oikotevẽ jeikekatu.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+<ul>
+<li>Pe kuatiarogue rehechaséva ndaikatúi ojehechauka ko ñanduti renda oikotevẽgui peteĩ jeikekatu.</li>
+<li>Ikatu pe apañuãi ojoajuvehína ñanduti renda rehe ha ndaipóri mba’eve ikatúva ojejapo oñemyatyrõ hag̃ua.</li>
+<li>Emomarandukuaa ñanduti renda ñangarekohárape ko apañuãi rehegua.</li>
+</ul>
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Opanungáva…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+<label> <b>%1$s</b> oguereko peteĩ ñe’ẽñemi ñeñangarekorã hérava HTTP Strict Transport Security (HSTS), he’iséva <b>%2$s</b> ikatuha oñembojoaju hekopete añoite. Ndaikatúi remoĩ peteĩ oĩ’ỹva reike hag̃ua ko tendápe. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Guevijey</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Pe jeike ojejokóma</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Pe kundahára oike hekopete, hákatu osẽ ñandutígui ombahasakuévo marandu. Eha’ãjey.</p>
+ <ul>
+ <li>Pe tenda ikatu ndoikói sapy’ami térã ojeporueterei. Eha’ãjey ag̃amieve.</li>
+ <li>Ndaikatúiramo emyanyhẽ kuatiarogue, ehecha oikópa nde wifi térã mba’ekuaarã ne pumbyry pegua.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Pe jeike ndoikovéima</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Tenda ejeruréva nombohovái pe jerure jeike rehegua ha kundahára noha’ãrõvéima ñembohovái.</p>
+ <ul>
+ <li>¿Ikatu mohendahavusu ojeporueterei térã peteĩ ñekytĩ ndahi’aréitava? Eha’ãjey ag̃amieve.</li>
+ <li>Ndaikatúi eikundaha ambue tenda rupi? Ehechajey ne mohendaha oĩpa ñandutípe.</li>
+ <li>Ne ñanduti térã ne mohendaha omo’ã chupe firewall térã proxy? Peteĩ ñemboheko oiko’ỹva ikatu omboykese ne ñeikundaha ñandutípe.</li>
+ <li>Oĩ gueteri apañuãi? Eporandu ne ñanduti ñangarekohára térã ne ñanduti me’ẽhárape nepytyvõ hag̃ua.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Ndaikatúi eike</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Tenda ikatu ndoikói sapy’ami térã ojeporueterei. Eha’ãjey ag̃ave.</li>
+ <li>Ndaikatúiramo emyanyhẽ mavave kuatiarogue, ehechajey oĩpa wifi térã mba’ekuaarã ne mba’e’oka oku’éva rehegua.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Mbohovái eha’ãrõ’ỹva mohendahavusúgui</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Pe tenda ombohovái ñanduti mba’ejerure oñeha’ãrõ’ỹre ha pe kundahára ndaikatuvéima oku’ejey.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Ko kuatiarogue ndoguerahajeýi hekoitépe</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Kundahára opytáma orujeysehápe mba’eporu jerurepyre. Pe tenda oguerahajeýta nde jerurepyre ysaja araka’eve noĩmbamo’ãiva rehegua.</p>
+ <ul>
+ <li>Erekópa mboguehápe térã jokohápe kookie oikotevẽva ko tenda.</li>
+ <li>Emboajérõ jepe kookie tenda pegua nomyatyrõi apañuãi, ikatuhína apañuãi mohendahavusu ñemboheko ha ndaha’éi ne mohendaha rehegua.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Jeikekatu’ỹre</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Pe kundaha omba’apo jeikekatu’ỹre ha ndaikatúi oike pe mba’eporu jerurepyrépe.</p>
+ <ul>
+ <li>¿Oñembojuaju mohendaha ñanduti oikóva rehe?</li>
+ <li>Ejopy “Ha’ãjey” ehasa hag̃ua jeikekatúpe ha emonyhẽjey kuatiarogue.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Mbojuajuhaite oñemomichĩva tekorosãrã</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Pe kundaharape ejeruréva omoha’eño mbojuajuhaite (e.g. <q>mozilla.org:80</q> mbojuajuhaite 80 mozilla.org-pe) ojeporúva ojeike hag̃ua <em>ambuéva</em> eikundaha ñandutípe. Pe kundahára ojokóma jerurepyre ne ñemo’ã ha rekorosãrã.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Pe jeike oñepyrũjeýma</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Pe juajuha ñandutigua ndoikói oñeñe’ẽ aja jeikerã rehe. Eha’ãjey ag̃ave.</p>
+ <ul>
+ <li>Pe tenda ikatu ndoikói hína sapy’ami térã ojeporueterei. Eha’ãjey ag̃amieve.</li>
+ <li>Ndaikatúiramo emyanyhẽ mavave kuatiarogue, ehecha oikópa nde Wi-Fi térã mba’ekuaarã ne pumbyry pegua.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Marandurenda hekorosã’ỹva</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Eñe’ẽ ñanduti renda jára ndive ha emomarandu chupekuéra ko apañuãi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Tetepy imarãpyréva jejavy</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Pe kuatiarogue ehechasevahína ndaikatúi ojechauka oiko rupi jejavy mba’ekuaarã ohasakuévo.</p>
+ <ul>
+ <li>Eñe’ẽ umi ñanduti renda jára ndine ha emomarandu chupekuéra ko apañuãi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Tetepy jokopyre</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Pe kuatiarogue ehechasevahína ndaikatúi ojechauka oiko rupi jejavy mba’ekuaarã ohasakuévo.</p>
+ <ul>
+ <li>Eñe’ẽ umi ñanduti renda jára ndine ha emomarandu chupekuéra ko apañuãi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Tetepy mbopapapy jejavy</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Ko kuatiarogue eñeha’ãva ehecha ndojehechaukakuaái oiporu rupi ñeikũmby oiko’ỹvaa térã ipu’aka’ỹva.</p>
+ <ul>
+ <li>Eñe’ẽ ñanduti renda jára ndive emombe’u hag̃ua chupekuéra ko apañuãi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Ndojejuhúi kundaharape</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Kundahára ndojuhúi mohendahavusu pe kundaharapépe g̃uarã.</p>
+ <ul>
+ <li>Ehechajey kundaharapépa ndojavýi, techapyrã,
+ <strong>ww</strong>.ejemplo.com.
+ <strong>www</strong>.ejemplo.com. rendaguépe.</li>
+ <li>Ndaikatúiramo emyanyhẽ mavave kuatiarogue, ehecha oikópa Wi-Fi térã pumbyry mba’ekuaarã.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ndaikatúi eike ñandutípe</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Ehechajey ne ñanduti juajuha térã emyanyhẽjey pe kuatiarogue ag̃amieve.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Myanyhẽjey</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Kundaharape ndoikói</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Kundaharape me’ẽmbyre noĩri peteĩ ysaja ojekuaávape. Ehechajey oĩpa jejavy kundaharape rendápe ha eha’ãjey.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Pe kundaharape ndoikói</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Ñanduti kundaharape ojehai jepi kóicha: <strong>http://www.example.com/</strong></li>
+ <li>Ema’ẽjey eiporu porãpa barra ojero’áva tenonde gotyo (i.e. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Taperekoite ojekuaa’ỹva</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Pe kundaharape omoha’eño peteĩ mba’ete (techapyrã <q>wxyz://</q>) kundahára nomoneĩri, péva rupi pe kundahára ndaikatúi oikekatu tendápe.</p>
+ <ul>
+ <li>Ndépa eikese jeikeha hekoetáva térã ambue mba’epytyvõrã ndaha’éiva moñe’ẽrã. Ehechajey tekotevẽva tenda pegua.</li>
+ <li>Heta protocolo oikotevẽkuaa software térã mboguerã’i mbohapyháva pe kundahára oikuaa hag̃ua.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Marandurenda ndojejuhúiva</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>¿Ikatu pe mba’eporu oñemboherajey, omboguévo térã omoambuévo tape?</li>
+ <li>¿Oĩ jejavy jehaípe, taiguasu jeporúpe térã oimeraẽva ambuechagua kundaharapépe?</li>
+ <li>¿Ereko jeikekuaa oikoitéva pe mba’eporu jerurepyrépe?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Marandurendápe jeike noñemoneĩri</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul> <li>Ikatúmakuri oñemboguete térã oñemongu’e, térã marandurenda ñemoneĩ omboyke pe jeike.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Mohendahavusu proxy ombotove jeike</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Ko kundahára oñemboheko oiporu hag̃ua mohendahavusu proxy, hákatu pe proxy omboyke jeike.</p>
+ <ul>
+ <li>Oĩporãpa proxy ñemboheko kundahárape. Ehechajey ñemboheko ha eha’ãjey.</li>
+ <li>¿Omoneĩpa proxy mba’eporu jeike ko ñanduti guive?</li>
+ <li>Oguereko gueteri apañuãi? Eporandu ñanduti ñangarekoha térã ñanduti me’ẽhárape pytyvõrã aporekoguáva rehe.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Ndojejuhúi mohendahavusu proxy</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Ko kundahára oñemboheko oiporu hag̃ua mohendahavusu proxy, hákatu ndojejuhúi mohendahavusu proxy?</p>
+ <ul>
+ <li>Oĩporã proxy kundahára ñemboheko? Ehechajey ñemboheko ha eha’ãjey.</li>
+ <li>¿Pe mohendaha ojoajuhína ñanduti oikóvare?</li>
+ <li>Oguereko gueteri apañuãi? Eporandu ñanduti ñangarekoha térã ñanduti me’ẽhárape pytyvõrã aporekoguávare.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Apañuãi malware rendápe</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Tenda %1$s-pe ojehechakuaa ha’eha tenda ivaíva ha upévare ojejokóma, ohechakuaávo ne nekorosã erohoryvéva.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Apañuãi tenda eipota’ỹvape</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Pe tenda %1$s pegua oje’e hese ha’eha software ivaíva ha upévare ojejokóma, ohapypuehóvo eguerohoryvéva tekorosarã.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Apañuãi tenda imarãvape</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Tenda %1$s ojehechakuaa ha’eha peteĩ tenda ivaikuaáva ha upévare ojejoko, emongu’évo erohoryvéva rekorosãrã.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Apañuãi tenda ijapúvape</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Ko ñanduti renda %1$s ojehechakuaa ha’eha tenda ivaíva ha upévare ojejokóma, ohechakuaávo ne nekorosã erohoryvéva.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Ndaipóri tenda hekorosãva</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Emyandyhína HTTPS ayvu añoitéva hekorosãve hag̃ua ha ndaipóri HTTPS rehegua <em>%1$s</em> reiporukuaáva.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Epyta ko HTTP rendápe</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..5840e3f630
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ફરીથી પ્રયત્ન કરો</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">વિનંતી પૂર્ણ કરી શકાતી નથી</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>આ સમસ્યા અથવા ભૂલ વિશેની અતિરિક્ત માહિતી હાલમાં ઉપલબ્ધ નથી.</p>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">સુરક્ષિત જોડાણ નિષ્ફળ થયું</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>તમે જે પૃષ્ઠને જોવા માટે પ્રયત્ન કરી રહ્યા છો તે બતાવી શકાતું નથી કારણ કે પ્રાપ્ત ડેટાની પ્રામાણિકતા ચકાસી શકાઈ નથી.</li>
+ <li>કૃપા કરીને વેબસાઇટ માલિકોને આ સમસ્યા વિશે જાણ કરવા માટે સંપર્ક કરો.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">સુરક્ષિત જોડાણ નિષ્ફળ થયું</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>આ સર્વરના ગોઠવણીમાં સમસ્યા હોઈ શકે છે, અથવા કોઈ હોઈ શકે છે કે જે સર્વરની impersonate કરવાનો પ્રયાસ કરે છે.</li>
+ <li>જો તમે આ સર્વર સાથે ભૂતકાળમાં સફળતાપૂર્વક કનેક્ટ કર્યું છે, તો ભૂલ અસ્થાયી હોઈ શકે છે, અને તમે પછીથી ફરી પ્રયાસ કરી શકો છો.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">અદ્યતન…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>કોઈક સાઇટની નકલ કરવાની કોશિશ કરી શકે છે અને તમારે ચાલુ રાખવું જોઈએ નહીં.</label>
+ <br><br>
+ <label>વેબસાઇટ્સ પ્રમાણપત્રો દ્વારા તેમની ઓળખ સાબિત કરે છે. %1$s <b>%2$s</b>પર વિશ્વાસ કરતો નથી કારણ કે તેનું પ્રમાણપત્ર આપનાર અજ્ઞાત છે, પ્રમાણપત્ર સ્વ-સહી થયેલ છે, અથવા સર્વર યોગ્ય મધ્યવર્તી પ્રમાણપત્રો મોકલી રહ્યું નથી.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">પાછા જાઓ (ભલામણ કરેલ)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">જોખમ સ્વીકારો અને ચાલુ રાખો</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">કનેક્શન વિક્ષેપિત થયું હતું</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>બ્રાઉઝર સફળતાપૂર્વક કનેક્ટ થયું, પરંતુ માહિતી સ્થાનાંતરણ કરતી વખતે કનેક્શન અવરોધિત થયું. મેહરબાની કરી ને ફરી થી પ્રયાસ કરો.</p>
+ <ul>
+ <li>સાઇટ અસ્થાયી રૂપે અનુપલબ્ધ અથવા ખૂબ વ્યસ્ત હોઈ શકે છે. થોડીવારમાં ફરી પ્રયાસ કરો.</li>
+ <li>જો તમે કોઈપણ પૃષ્ઠોને લોડ કરવામાં અસમર્થ છો, તો તમારા ઉપકરણનો ડેટા અથવા Wi-Fi કનેક્શન તપાસો.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">જોડાણ નો સમય સમાપ્ત થઇ ગયો છે</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>વિનંતી કરેલી સાઇટએ કનેક્શન વિનંતીનો જવાબ આપ્યો નથી અને બ્રાઉઝરે જવાબની રાહ જોવી બંધ કરી દીધી છે.</p>
+ <ul>
+ <li>શું સર્વર વધુ માંગ અથવા અસ્થાયી આઉટેજ અનુભવી શકે છે? પછીથી ફરી પ્રયાસ કરો.</li>
+ <li>શું તમે અન્ય સાઇટ્સ બ્રાઉઝ કરવામાં અસમર્થ છો? કમ્પ્યુટરનું નેટવર્ક કનેક્શન તપાસો.</li>
+ <li>શું તમારું કમ્પ્યુટર અથવા નેટવર્ક firewall અથવા પ્રોક્સી દ્વારા સુરક્ષિત છે? ખોટી સેટિંગ્સ વેબ બ્રાઉઝિંગમાં દખલ કરી શકે છે.</li>
+ <li>હજી મુશ્કેલી આવી રહી છે? સહાય માટે તમારા નેટવર્ક એડમિનિસ્ટ્રેટર અથવા ઇન્ટરનેટ પ્રદાતાની સલાહ લો.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">કનેક્ટ કરવામાં અસમર્થ</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>સાઇટ અસ્થાયી રૂપે અનુપલબ્ધ અથવા ખૂબ વ્યસ્ત હોઈ શકે છે. થોડીક ક્ષણોમાં ફરી પ્રયાસ કરો.</li>
+ <li>જો તમે કોઈપણ પૃષ્ઠોને લોડ કરવામાં અસમર્થ છો, તો તમારા મોબાઇલ ઉપકરણનો ડેટા અથવા Wi-Fi કનેક્શન તપાસો.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">સર્વર તરફથી અનપેક્ષિત પ્રતિસાદ</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>સાઇટએ નેટવર્ક વિનંતીનો અનપેક્ષિત રીતે જવાબ આપ્યો અને બ્રાઉઝર ચાલુ રાખી શકશે નહીં.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">પૃષ્ઠ યોગ્ય રીતે રીડાયરેક્ટ થઈ રહ્યું નથી</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ઓફલાઈન મોડ</string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">સુરક્ષા કારણોસર પોર્ટ પ્રતિબંધિત છે</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">કનેક્શન ફરીથી સેટ થયું હતું</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">અસુરક્ષિત ફાઈલ પ્રકાર</string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">બગડેલ સમાવિષ્ટોની ક્ષતિ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">સામગ્રી ક્રેશ થઈ ગઈ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">સમાવિષ્ટ સંગ્રહપદ્ધતિ ભૂલ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">સરનામું મળ્યું નથી</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ઇન્ટરનેટ કનેક્શન નથી</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">તમારું નેટવર્ક કનેક્શન તપાસો અથવા થોડી ક્ષણોમાં પૃષ્ઠને ફરીથી લોડ કરવાનો પ્રયાસ કરો.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ફરીથી લોડ કરો</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">અમાન્ય સરનામું</string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">સરનામું માન્ય નથી</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">અજ્ઞાત પ્રોટોકોલ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ફાઈલ મળી નહિં</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ફાઈલ માં પ્રવેશ નકારવામાં આવ્યો હતો</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">પ્રોક્સી સર્વરે જોડાણ તોડી નાંખ્યું</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">પ્રોક્સી સર્વર મળ્યું નથી</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">માલવેર સાઇટ ઇશ્યૂ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s પરની સાઇટ એટેક સાઇટ તરીકેની જાણ કરવામાં આવી છે અને તમારી સુરક્ષા પસંદગીઓના આધારે અવરોધિત કરવામાં આવી છે.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">અનિચ્છનીય સાઇટ ઇશ્યૂ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s પરની સાઇટને અનિચ્છનીય સૉફ્ટવેરની સેવા તરીકે જાણ કરવામાં આવી છે અને તમારી સુરક્ષા પસંદગીઓનાં આધારે તેને અવરોધિત કરવામાં આવી છે.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">નુકસાનકારક સાઇટનો ઇશ્યૂ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s પરની સાઇટને સંભવિત હાનિકારક સાઇટ તરીકે જાણ કરવામાં આવી છે અને તમારી સુરક્ષા પસંદગીઓના આધારે અવરોધિત કરવામાં આવી છે.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">ભ્રામક સાઇટ ઇશ્યૂ</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$s પરના આ વેબ પૃષ્ઠને ભ્રામક સાઇટ તરીકે જાણ કરવામાં આવ્યું છે અને તમારી સુરક્ષા પસંદગીઓના આધારે અવરોધિત કરવામાં આવ્યું છે.</p>]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..4aad7f4800
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">पुनः प्रयास करें</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">अनुरोध पूरा नहीं किया जा सका</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>इस समस्या या त्रुटी के बारे में अतिरिक्त जानकारी फिलहाल उपलब्ध नहीं है।</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">सुरक्षित संपर्क विफ़ल</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>जिस पृष्ठ को आप देखने की कोशिश कर रहे हैं उसे दिखाया नहीं जा सकता क्योंकि प्राप्त डेटा के प्रमाणिकता की सत्यापन नहीं की जा सकी</li>
+ <li>कृपया इस समस्या की जानकारी देने के लिए वेबसाइट प्रदाता से संपर्क करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">सुरक्षित संपर्क विफ़ल</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>यह सर्वर विन्यास से जुड़ी समस्या हो सकती है, या यह कोई ऐसा हो सकता है जो सर्वर की नकल बनाने की कोशिश कर रहा हो।</li>
+ <li>यदि पहले कभी आप इस सर्वर से सफलतापूर्वक जुड़े हों, तो त्रुटी कुछ समय की हो सकती है, और बाद में आप पुनः प्रयास कर सकते हैं।</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">विस्तृत…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>शायद कोई व्यक्ति साइट की नकल करने की कोशिश कर रहा हो और आपको आगे नहीं बढ़ना चाहिए।</label>
+ <br><br>
+ <label>वेबसाइट अपनी पहचान प्रमाण-पत्रों द्वारा साबित करते हैं। <b>%2$s</b> पर %1$s विश्वास नहीं करता क्योंकि इसका प्रमाणपत्र प्रदाता अज्ञात है, प्रमाणपत्र स्वतः हस्ताक्षरित है या सर्वर सही मध्यवर्ती प्रमाणपत्र नहीं भेज रहा है।</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">पीछे जाएं (सलाह)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">जोखिम स्वीकारें और आगे बढ़ें</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">संपर्क में बाधा उत्पन्न हुई</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>ब्राउज़र सफलतापूर्वक जुड़ चुका है, पर जानकारी स्थानांतरित करते समय संपर्क में कोई बाधा उत्पन्न हुई। कृपया पुनः प्रयास करें।</p>
+ <ul>
+ <li>यह साइट अस्थाई रूप से अनुपलब्ध या अत्यंत व्यस्त हो सकता है। कुछ समय बाद पुनः प्रयास करें।</li>
+ <li>यदि आप कोई पृष्ठ लोड नहीं कर पा रहे हैं, तो अपने उपकरण की डेटा या वाई-फाई कनेक्शन की जांच करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">संपर्क समय समाप्त</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>अनुरोधित साइट ने संपर्क की अनुरोध का जवाब नहीं दिया और ब्राउज़र ने जवाब की प्रतीक्षा करना बंद कर दिया है।</p>
+ <ul>
+ <li>हो सकता है कि सर्वर काफी अनुरोध या अस्थाई निलंबन झेल रहा हो? कृपया बाद में पुनः प्रयास करें।</li>
+ <li>क्या आप अन्य साइट भी ब्राउज़ नहीं कर पा रहे? कृपया उपकरण के नेटवर्क कनेक्शन की जांच करें।</li>
+ <li>क्या आपका उपकरण या नेटवर्क किसी फ़ायरवॉल या प्रॉक्सी द्वारा सुरक्षित है? गलत सेटिंग से वेब ब्राउज़िंग में बाधा आ सकती है।</li>
+ <li>अभी भी समस्या हो रही है? सहायता के लिए अपने नेटवर्क व्यवस्थापक या इंटरनेट प्रदाता से संपर्क करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">संपर्क करने में असमर्थ</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>यह साइट अस्थाई रूप से अनुपलब्ध या अत्यंत व्यस्त हो सकता है। कुछ समय बाद पुनः प्रयास करें।</li>
+ <li>यदि आप कोई भी पृष्ठ लोड करने में असमर्थ हैं, तो अपने मोबाइल उपकरण के डेटा या वाई-फाई कनेक्शन की जांच करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">सर्वर द्वारा अप्रत्याशित प्रतिक्रिया प्राप्त हुआ</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>इस साइट ने नेटवर्क अनुरोध की प्रतिक्रिया एक अप्रत्याशित रूप से दी है, जिस कारण ब्राउज़र आगे नहीं बढ़ सकता।</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">यह पृष्ठ सही से पुनर्निर्देशित नहीं हो पा रहा</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>ब्राउज़र ने अनुरोध की गई चीज़ को पाने का प्रयास बंद कर दिया है। यह साइट अनुरोध को इस प्रकार पुनर्निर्देशित कर रहा है कि वह कभी पूरा नहीं होगा।</p>
+ <ul>
+ <li>क्या आपने साइट के लिए आवश्यक कूकीज़ को अक्षम या अवरुद्ध किया है?</li>
+ <li>यदि साइट की कूकीज़ स्वीकारने से भी यह समस्या ठीक नहीं होती, तो संभवतः यह सर्वर विन्यास की समस्या है ना कि आपके उपकरण की।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ऑफलाइन मोड</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>ब्राउज़र अपने ऑफलाइन मोड में चल रहा है और अनुरिधित चीज़ से नहीं जुड़ सकता।</p>
+ <ul>
+ <li>क्या उपकरण किसी सक्रीय नेटवर्क से जुड़ा हुआ है?</li>
+ <li>ऑनलाइन मोड में जाने के लिए “पुनः प्रयास करें” दबाएं या पृष्ठ को पुनः लोड करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">सुरक्षा कारणों से पोर्ट प्रतिबंधित है</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p> अनुरोधित पते ने एक पोर्ट (उदाहरण के लिए, <q> mozilla.org:80 </ q> पोर्ट 80 के लिए mozilla.org पर) निर्दिष्ट किया गया है, जो सामान्य रूप से वेब ब्राउजिंग के अलावा <em> अन्य </ em> उद्देश्यों के लिए उपयोग किया जाता है। ब्राउज़र ने आपकी सुरक्षा और सुरक्षा के लिए अनुरोध को रद्द कर दिया है।</ p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">कनेक्शन रिसेट किया गया था</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p> कनैक्शन स्थापित करते समय नेटवर्क लिंक बाधित हो गयी थी। कृपया पुनः प्रयास करें। </ p>
+ <Ul>
+
+ <li> साइट अस्थायी रूप से अनुपलब्ध या बहुत व्यस्त हो सकती है। कुछ समय बाद पुनः प्रयास करें। </ li> 
+ <li> यदि आप कोई पेज लोड नहीं कर पा रहे हैं, तो अपने डिवाइस का डेटा या वाई-फाई कनेक्शन जांचें। </ li>
+ </ Ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">असुरक्षित फाइल प्रकार</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>कृपया वेबसाइट मालिक से उन्हें इस समस्या के बारे में बताने के लिए संपर्क करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">अनुपयोगी सामग्री त्रुटि</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>पेज जिसे आप देखने की कोशिश कर रहे हैं नहीं दिखाया जा सकता है क्योंकि डेटा संचारण में त्रुटि पाया गया।</p>
+ <ul>
+ <li>कृपया वेबसाइट मालिक को इस समस्या के बारे में सूचित करने के लिए संपर्क करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">सामग्री क्रैश</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>पेज जिसे आप देखने की कोशिश कर रहे हैं नहीं दिखाया जा सकता है क्योंकि डेटा संचारण में त्रुटि पाया गया।</p>
+ <ul>
+ <li>कृपया वेबसाइट मालिक को इस समस्या के बारे में सूचित करने के लिए संपर्क करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">सामग्री एंकोडिंग त्रुटि</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>पेज जिसे आप देखने की कोशिश कर रहे हैं नहीं दिखाया जा सकता है क्योंकि यह संकुचन का अवैध या असमर्थित प्रारूप का प्रयोग करता है।</p>
+ <ul>
+ <li>कृपया वेबसाइट मालिक को इस समस्या के बारे में सूचित करने के लिए संपर्क करें।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">पता नहीं मिला</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">इंटरनेट कनेक्शन नही है</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">अपना नेटवर्क कनेक्शन जांचें या पेज कुछ ही क्षणों में पुनः लोड करने का प्रयास करें।</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">फिर से लोड करें</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">अमान्य पता</string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">पता मान्य नहीं है</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>वेब पता प्रायः इस तरह लिखा जाता है <strong>http://www.example.com/</strong></li>
+ <li>सुनिश्चित करें कि आप फॉरवर्ड स्लैश का प्रयोग कर रहे हैं (यानी <strong>/</strong>)।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">अज्ञात प्रोटोकॉल</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">फ़ाइल नहीं मिला</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">फ़ाइल के ऐक्सेस को रोक दिया गया</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>इसे हटाया, खिसकाया जा सकता है या फाइल अनुमति पहुँच प्रतिबाधित कर सकती हैं।</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">प्रॉक्सी सर्वर का कनेक्शन अस्वीकार किया गया</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">प्रॉक्सी सर्वर नहीं मिला</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">मैलवेयर साइट समस्या</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s पर साइट को एक हमला साइट के रूप में रिपोट किया गया है और आपकी सुरक्षा प्राथमिकताओं के आधार पर ब्लॉक किया गया हैं।</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">अवांछित साइट समस्या</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s पर साइट को एक अवांछित सॉफ्टवेयर की सेवा के रूप में रिपोट किया गया है और आपकी सुरक्षा प्राथमिकताओं के आधार पर ब्लॉक किया गया हैं।</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">हानिकारक साइट समस्या</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s पर साइट एक संभावित हानिकारक साइट के रूप में रिपोर्ट किया गया है और आपकी सुरक्षा प्राथमिकताओं के आधार पर ब्लॉक किया गया हैं।</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">धोखादायक साइट समस्या</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$s पर यह वेबपेज एक धोखादायक साइट के रूप में रिपोट किया गया है और आपकी सुरक्षा प्राथमिकताओं के आधार पर ब्लॉक किया गया हैं।</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">सुरक्षित साइट उपलब्ध नहीं है</string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP साइट को जारी रखें</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..b5410ea715
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hil/strings.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Tilawan Liwat</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Wala na kompleto nga bilin</string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Ang gindangat nga sugpon napalsu</string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Ang gindangat nga sugpon napalsu</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Nag-abanse…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Pagabalikan (Ginarekomendar)</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Nag-abanse…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Balikan</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">May nagsablag sa koneksyon</string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Indi makaangot</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline Mode</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Na-reset ang koneksyon</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Unsafe File Type</string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Corrupted Content Error</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Ang kaundan nag-crash</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Indi Makita ang Address</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Wala koneksyon sa internet</string>
+
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Mag-reload</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Sala nga Address</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Unknown Protocol</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Indi makita ang File</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy Server Refused Connection</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Indi makita ang Proxy Server</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Isyu sang malware site</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Hinali nga isyu sa site</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Delekado na isyu sa site</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Dulot nga kabutigan sang site</string>
+
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Ang sugpon sa HTTP Site</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..892e37aac3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hr/strings.xml
@@ -0,0 +1,267 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Pokušaj ponovo</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Nije moguće dovršiti zahtjev</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Dodatne informacije o ovom problemu ili grešci trenutno nisu dostupne.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Sigurna veza nije uspjela</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Stranicu kojoj pokušavaš pristupiti nije moguće prikazati jer nije moguće provjeriti autentičnost primljenih podataka.</li>
+ <li>Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Sigurna veza nije uspjela</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Možda se radi o problemu s postavkama na poslužitelju ili možda netko pokušava oponašati ovaj poslužitelj.</li>
+ <li>Ako u prošlosti nije bilo problema sa spajanjem na ovaj poslužitelj, moguće je da se radi o privremenoj grešci, stoga pokušaj ponovo kasnije.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Napredno …</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Netko možda pokušava lažno predstavljati web-stranicu, stoga je bolje da ne nastaviš.</label>
+        <br>
+        <label> Web-stranice dokazuju svoj identitet pomoću certifikata. %1$s ne vjeruje stranici na <b>%2$s</b>, jer je izdavač certifikata nepoznat, certifikat je samopotpisan ili poslužitelj ne šalje ispravne međucertifikate. </label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Idi natrag (preporučeno)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Prihvati rizik i nastavi</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ova web stranica zahtjeva sigurnu vezu.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Stranica koju pokušavate vidjeti se ne može prikazati zato što zahtjeva sigurnu vezu.</li>
+ <li>Problemu je najvjerojatnije uzrok web stranica i ne možete ništa napraviti što bi ga riješilo.</li>
+ <li>Možete obavijestiti administratora web stranice o problemu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Napredno…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> provodi sigurnosnu politiku pod nazivom HTTP Strict Transport Security (HSTS), koja znači da se <b>%2$s</b> može na istu povezati samo sigurno. Ne možete dodati iznimku za posjete ovoj web stranici. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Idi natrag</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Veza je prekinuta</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Preglednik se uspješno povezao, ali veza je prekinuta tijekom prijenosa informacija. Pokušaj ponovo.</p>
+      <ul>
+        <li>Stranica je možda privremeno nedostupna ili prezauzeta. Pokušaj ponovo za nekoliko trenutaka.</li>
+        <li>Ako ne možeš učitati nijednu stranicu, provjeri podatke tvog uređaja ili Wi-Fi vezu.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Vezi je isteklo vrijeme</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Zatražena stranica nije odgovorila na zahtjev i preglednik je prestao čekati na odgovor.</p>
+ <ul>
+ <li>Možda je poslužitelj opterećen velikom količinom zahtjeva ili je privremeno ostao bez napajanja? Pokušaj ponovo kasnije.</li>
+ <li>Možeš li pregledavati ostale stranice? Provjeri mrežnu vezu svog računala.</li>
+ <li>Jesu li tvoje računalo ili mreža zaštićeni vatrozidom ili proxyjem? Neispravne postavke mogu prouzročiti probleme prilikom pregledavanja weba.</li>
+ <li>Ukoliko još uvijek imaš probleme, za pomoć se obrati administratoru mreže ili pružatelju internetske usluge.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Povezivanje nije moguće</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Stranica je možda privremeno nedostupna ili preopterećena. Pokušaj ponovno malo kasnije.</li>
+ <li>Ako ne možeš učitati niti jednu stranicu, provjeri podatke svog uređaja ili Wi-Fi vezu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Neočekivani odgovor od poslužitelja</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Stranica je na mrežni zahtjev odgovorila na neočekivani način, zbog čega preglednik ne može nastaviti.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Stranica ne preusmjerava ispravno</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Preglednik je prestao dohvaćati zatražene stranice. Stranica preusmjerava zahtjev na takav način, da se on nikada ne može ispuniti.</p>
+ <ul>
+ <li>Jesu li kolačići za ovu stranicu deaktivirani ili blokirani?</li>
+ <li>Ako prihvaćanje kolačića stranice ne riješi problem, vrlo vjerojatno se radi o problemu s konfiguracijom poslužitelja, a ne tvog računala.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Izvanmrežni način rada</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Preglednik je u izvanmrežnom načinu rada i ne može se spojiti na traženu stavku.</p>
+ <ul>
+ <li>Je li uređaj spojen na aktivnu mrežu?</li>
+ <li>Klikni na „Pokušaj ponovo” za prebacivanje na mrežni način rada i ponovo učitaj stranicu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Priključak je iz sigurnosnih razloga ograničen</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Zatražena adresa ima definiran priključak (npr. <q>mozilla.org:80</q> za priključak 80 na mozilla.org) koji se inače koristi za <em>druge</em> radnje, a ne za pregledavanje weba. Preglednik je prekinuo zahtjev radi tvoje zaštite i sigurnosti.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Veza je prekinuta</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Mrežna veza je prekinuta tijekom povezivanja. Pokušaj ponovo.</p>
+      <ul>
+        <li>Stranica je možda privremeno nedostupna ili prezauzeta. Pokušaj ponovo za nekoliko trenutaka.</li>
+        <li>Ako ne možeš učitati nijednu stranicu, provjeri podatke tvog uređaja ili Wi-Fi vezu.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Nesigurna vrsta datoteke</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Greška oštećenog sadržaja</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Stranicu kojoj pokušavaš pristupiti nije moguće prikazati zbog greške u prijenosu podataka.</p>
+ <ul>
+ <li>Obavijesti vlasnike web stranice o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Greška u sadržaju</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Stranicu kojoj pokušavaš pristupiti nije moguće prikazati zbog greške u prijenosu podataka.</p>
+ <ul>
+ <li>Obavijesti vlasnike web stranice o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Greška u kodiranju sadržaja</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Stranica koju pokušavaš vidjeti ne može biti prikazana jer koristi neispravni ili nepodržani oblik komprimiranja.</p>
+ <ul>
+ <li>Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresa nije pronađena</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Preglednik nije mogao pronaći poslužitelja domaćina za navedenu adresu.</p>
+      <ul>
+        <li>Pazi da nemaš greške u tipkanju, kao što su
+          <strong>ww</strong>.primjer.hr umjesto
+          <strong>www</strong>.primjer.hr.</li>
+ <li>Ako ne možeš učitati nijednu stranicu, provjeri podatke svog uređaja ili Wi-Fi vezu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ne postoji veza s internetom</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Provjeri mrežnu vezu ili pokušaj ponovo učitati stranicu za nekoliko trenutaka.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Učitaj ponovo</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Neispravna adresa</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Upisana adresa nije u prepoznatljivom obliku. Provjeri postoji li greška u unosu u adresnoj traci.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adresa nije ispravna</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Adrese web stranica se obično pišu u formatu poput <strong>http://www.example.com/</strong></li>
+ <li>Pazi na način pisanja kose crte (tj. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Nepoznat protokol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Adresa definira protkol (npr. <q>xyz://</q>) kojeg preglednik ne poznaje ili se ne može povezati sa zatraženom stranicom.</p>
+ <ul>
+ <li>Pokušavaš li pristupiti multimedijskim ili drugim netekstualnim uslugama? Provjeri ima li stranica dodatnih zahtjeva.</li>
+ <li>Neki protokoli zahtijevaju program ili priključke treće strane, kako bi ih preglednik mogao prepoznati.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Datoteka nije pronađena</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Možda je uklonjena, premještena ili joj je ime promijenjeno?</li>
+ <li>Postoji li pravopisna ili nekakva tipografska greška?</li>
+ <li>Imaš li dovoljna prava za pristup traženoj datoteci?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Pristup datoteci je odbijen</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Možda je uklonjena, premještena ili dozvole za datoteku spriječavaju pristup.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy poslužitelj odbija povezivanje</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Preglednik je postavljen da koristi proxy poslužitelj, ali proxy poslužitelj odbija povezivanje.</p>
+ <ul>
+ <li>Jesu li proxy postavke ispravne? Provjeri postavke i pokušaj ponovo.</li>
+ <li>Dozvoljava li proxy usluga povezivanje iz ove mreže?</li>
+ <li>Ukoliko još uvijek imaš probleme, za pomoć se obrati administratoru mreže ili pružatelju internetske usluge.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy poslužitelj nije pronađen</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Preglednik je postavljen da koristi proxy poslužitelj, ali ga nije moguće naći.</p>
+ <ul>
+ <li>Jesu li proxy postavke ispravne? Provjeri postavke i pokušaj ponovo.</li>
+ <li>Je li uređaj povezan na aktivnu mrežu?</li>
+ <li>Ukoliko još uvijek imaš probleme, za pomoć se obrati administratoru mreže ili pružatelju internet usluge.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Stranica sa zlonamjernim softverom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Stranica na %1$s je prijavljena kao zloćudna stranica i blokirana je na temelju tvojih sigurnosnih postavki.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Neželjena stranica</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Stranica na %1$s je prijavljena zbog posluživanja nepoželjnog softvera, te je blokirana na temelju tvojih sigurnosnih postavki.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Štetna stranica</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Web stranica %1$s je prijavljena kao potencijalno zlonamjerna stranica i blokirana je na temelju tvojih sigurnosnih postavki.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problem s obmanjujućom stranicom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Web stranica %1$s je prijavljena kao obmanjujuća stranica i blokirana je na temelju tvojih sigurnosnih postavki.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sigurna stranica nije dostupna</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Omogućen je "samo HTTPS" način rada za poboljšanu sigurnost, a HTTPS inačica stranice <em>%1$s</em> nije dostupna.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Nastavi na stranicu preko HTTP-a</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..31364ea46f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,296 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Hišće raz spytać</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Naprašowanje njeda so dokónčić</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[ <p>Přidatne informacije wo tutym problemje abo zmylku tuchwilu k dispoziciji njesteja.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Wěsty zwisk njeje so poradźił</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[ <ul>
+ <li>Strona, kotruž chceće sej wobhladać, njeda so pokazać, dokelž awtentiskosć přijatych datow njeda so přepruwować.</li>
+ <li>Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Wěsty zwisk njeje so poradźił</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>To móhło problem z konfiguraciju serwera być, abo móhło być, zo něchtó pospytuje serwer imitować.</li>
+ <li>Jeli sće ze serwerom w zańdźenosći wuspěšnje zwjazany był, móhł zmylk snano nachwilny być a móžeće pozdźišo hišće raz spytać.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Rozšěrjene…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Něchtó móhł spytać, sydło za swoje wudać, tohodla njeměł wy z tym pokročować.</label>
+ <br><br>
+ <label>Websydła swoju identitu přez certifikaty dopokazuja. %1$s <b>%2$s</b> njedowěrja, dokelž jeho certifikatowy wudawar je njeznaty, certifkat je samsignowany abo serwer korektne mjezycertifikaty njesćele.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Wróćo (doporučeny)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Riziko akceptować a pokročować</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Tute websydło sej wěsty zwisk žada.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Strona, kotruž sej chceće wobhladać, njeda so pokazać, dokelž tute websydło sej wěsty zwisk žada.</li>
+ <li>Problem so najskerje přez websydło zawinuje a tohodla ničo njeje, štož móžeće činić, zo byšće jón rozrisał.</li>
+ <li>Móžeće administratorej websydła problem zdźělić.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Rozšěrjeny…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ma wěstotne prawidło z mjenom HTTP Strict Transport Security (HSTS), kotrež woznamjenja, zo <b>%2$s</b> móže so jenož wěsće zwjazać. Njemóžeće wuwzaće přidać, zo byšće tute sydło wopytał. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Wróćo</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Zwisk je so přetorhnył</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Wobhladowak je so wuspěšnje zwjazał, ale zwisk je so přetorhnył, mjeztym zo so informacije přenošowachu. Prošu spytajće hišće raz.</p>
+ <ul>
+ <li>Sydło snano nachwilu k dispoziji njeje abo je přećežene. Spytajće za někotre wokomiki hišće raz.</li>
+ <li>Jeli njemóžeće strony začitać, přepruwujće daty swojeho grata abo WLAN-zwisk.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Zwisk je čas překročił</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Požadane sydło na zwiskowe naprašowanje njewotmołwi a wobhladowak přesta na wotmołwu čakać.</p>
+ <ul>
+ <li>Móže być, zo serwer je přećeženy abo ma nachwilne mylenje? Spytajće pozdźišo hišće raz.</li>
+ <li>Njemóžeće druhe sydła přehladować? Přepruwujće syćowy zwisk grata.</li>
+ <li>Škita so waš grat abo waša syć z wohnjowej murju abo proksy? Njekorektne nastajenja móža webpřehladowanju wadźić.</li>
+ <li>Maće hišće ćeže? Skonsultujće swojeho syćoweho administratora abo internetneho poskićowarja za podpěru.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Zwisk móžny njeje</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Sydło njesteji snano nachwilu k dispoziciji abo je přećežene. Spytajće za mało wokomikow hišće raz.</li>
+ <li>Jeli njemóžeće strony začitać, přepruwujće daty abo WLAN-zwisk wašeho mobilneho grata.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Njewočakowana wotmołwa ze serwera</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Sydło wotmołwi na naprašowanje syće na njewočakowane wašnje a wobhladowak njemóže pokročować.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Strona njeprawje posrědkuje</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Wobhladowak přesta požadany objekt wotwołować. Sydło sposrědkuje naprašowanje na wašnje, kotrež so njekónči.</p>
+ <ul>
+ <li>Sće placki znjemóžnił abo zablokował, kotrež su trěbne za tute sysdło?</li>
+ <li>Jeli akceptowanje plackow tutoho sydła problem njerozrisa, je to najskerje problem konfiguracije serwera a nic wašeho grata.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline-modus</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Wobhladowak offline dźěła a njemóže z požadanym objektom zwjazać.</p>
+ <ul>
+ <li>Je grat z aktiwnej syću zwjazany?</li>
+ <li>Klikńće na “Hišće raz”, zo byšće do online-modusa přešoł a stronu znowa začitał.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port z přičinow wěstoty wobmjezowany</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Požadana adresa poda port (na př. <q>mozilla.org:80</q> za port 80 on mozilla.org), kotryž so normalnje za <em>hinaše</em> zaměry hač webpřehladowanje wužiwa. Wobhladowak je naprašowanje za waš škit a wěstotu přetorhnył.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Zwisk bu wróćo stajeny</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Syćowy zwisk je so přetorhnył, mjeztym zo sće spytał, zwisk nawjazać. Prošu spytajće hišće raz.</p>
+ <ul>
+ <li>Sydło snano nachwilu k dispoziji njeje abo je přećežene. Spytajće za někotre wokomiki hišće raz.</li>
+ <li>Jeli njemóžeće strony začitać, přepruwujće daty swojeho grata abo WLAN-zwisk.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Njewěsty datajowy typ</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Zmylk - wobškodźeny wobsah</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Strona, kotruž chceće sej wobhladać, njeda so pokazać, dokelž je so zmylk při přenošowanju datow namakał.</p>
+ <ul>
+ <li>Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Wobsah jo spadnył</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Strona, kotruž chceće sej wobhladać, njeda so pokazać, dokelž je so zmylk při přenošowanju datow namakał.</p>
+ <ul>
+ <li>Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Zmylk při kodowanju wobsaha</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Strona, kotruž chcéce sej wobhladać, njeda so pokazać, dokelž njepłaćiwu abo njepodpěrowanu formu kompresije wužiwa.</p>
+ <ul>
+ <li>Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresa njenamakana</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Wobhladowak njemóžeše hostowy serwer za podatu adresu namakać.</p>
+ <ul>
+ <li>Přepruwujće adresu za pisanskimi zmylkami kaž
+ <strong>ww</strong>.example.com město
+ <strong>www</strong>.example.com.</li>
+ <li>Jeli njemóžeće strony začitać, přepruwujće daty swojeho grata abo WLAN-zwisk.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Žadyn internetny zwisk</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Přepruwujće swój syćowy zwisk abo začitajće stronu za mało wokomikow znowa.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Znowa začitać</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Njepłaćiwa adresa</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Podata adresa w připóznatym formaće njeje. Prošu přepruwujće adresowu lajstu za zmylkami a spytajće hišće raz.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adresa płaćiwa njeje</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+<ul>
+ <li>Webadresy so z wašnjom kaž <strong>http://www.example.com/</strong> pisaja.</li>
+ <li>Zawěsćće, zo wužiwaće doprědka nachilene nakósne smužki (t.j. <strong>/</strong>).</li>
+</ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Njeznaty protokol</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adresa podawa protokol (na př. <q>wxyz://</q>), kotryž wobhladowak njepřipóznawa, tak zo wobhladowak njemóže prawje ze sydłom zwjazać.</p>
+ <ul>
+ <li>Pospytujeće na multimedia abo druhe njetekstowe słužby přistup měć? Přepruwujće sydło za wosebitymi potrěbnosćemi.</li>
+ <li>Někotre protokole trjebaja programy třećich abo tykače, prjedy hač wobhladowak móže je připóznać.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Dataja njeje so namakała</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Je móžno, zo bu objekt přemjenowany, wotstronjeny abo přesunjeny?</li>
+ <li>Je zmylk prawopisa, wulkopisanja, abo hinaši typografiski zmylk w adresy?</li>
+ <li>Maće dosahace přistupne prawa za požadany objekt?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Přistup k dataji je so wotpokazał</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Snano je so wotstroniła, přesunyła, abo datajowe prawa zadźěwaju přistupej.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proksyserwer je zwisk wotpokazał</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Wobhladowak bu konfigurowany, zo by so proksyserwer wužiwa, ale proksy zwisk wotpokaza.</p>
+ <ul>
+ <li>Je konfiguracija proksy wobhladowaka korektna? Přepruwujće nastajenja a spytajće hišće raz.</li>
+ <li>Dowola słužba proksy zwiski z tuteje syće?</li>
+ <li>Maće hišće ćeže? Skonsultujće swojeho syćoweho administratora abo internetneho poskićowarja za podpěru.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proksyserwer njenamakany</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Wobhladowak bu konfigurowany, zo by so proksyserwer wužiwa, ale proksy njeda so namakać.</p>
+ <ul>
+ <li>Je konfiguracija proksyja wobhladowaka korektna? Přepruwujće nastajenja a spytajće hišće raz.</li>
+ <li>Je grat z aktiwnej syću zwjazany?</li>
+ <li>Maće hišće ćeže? Stajće so ze swojim syćowym administratorom abo internetnym poskićowarjom za podpěru do zwiska.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problem ze sydłom ze škódnej softwaru</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Sydło %1$s bu jako nadpadowace sydło zdźělene a bu na zakładźe wašich wěstotnych nastajenjow zablokowane.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problem z njewitanym sydłom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Sydło na %1$s bu jako sydło zdźělene, kotrež njewitanu software poskića a bu na zakładźe wašich wěstotnych nastajenjow zablokowane.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problem z wohrožacym sydłom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Sydło %1$s bu jako potencielnje wohrožace sydło zdźělene a bu na zakładźe wašich wěstotnych nastajenjow zablokowane.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problem z wobšudnym sydłom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Tuta webstrona na %1$s bu jako wobšudne sydło zdźělena a bu na zakładźe wašich wěstotnych nastajenjow zablokowana.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Wěste sydło k dispoziciji njeje</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Sće modus Jenož-HTTPS za polěpšenu wěstotu zmóžnił a HTTPS-wersija <em>%1$s</em> k dispoziciji njeje.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Dale k HTTP-sydłu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..19e610d67b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hu/strings.xml
@@ -0,0 +1,274 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Próbálja újra</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">A kérés nem teljesíthető</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Jelenleg nem áll rendelkezésre további információ a problémáról vagy hibáról.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">A biztonságos kapcsolat sikertelen</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul><li>A megtekinteni kívánt oldal nem jeleníthető meg, mert a kapott adatok hitelessége nem ellenőrizhető.</li> <li>Lépjen kapcsolatba a webhely üzemeltetőjével, és értesítse a problémáról.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">A biztonságos kapcsolat sikertelen</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul><li>Ezt okozhatja a kiszolgáló nem megfelelő beállítása, de az is lehet, hogy valaki megpróbál a kiszolgáló nevében fellépni.</li><li>Ha korábban már sikeresen kapcsolódott ehhez a kiszolgálóhoz, akkor lehet, hogy a hiba csak ideiglenes, és később újra próbálkozhat.</li></ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Speciális…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Lehet, hogy valaki megpróbálja megszemélyesíteni az oldalt, így nem kellene folytatnia.</label>
+ <br><br>
+ <label>A weboldalak tanúsítványok segítségével bizonyítják a személyazonosságukat. A %1$s nem bízik a(z) <b>%2$s</b> oldalban, mert a tanúsítvány kibocsátója ismeretlen, a tanúsítvány önaláírt, vagy a kiszolgáló nem küldi el a helyes közbülső tanúsítványokat.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Visszalépés (ajánlott)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Kockázat elfogadása és továbblépés</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ez a webhely biztonságos kapcsolatot igényel.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>A megtekintendő oldal nem jeleníthető meg, mert ez a webhely biztonságos kapcsolatot igényel.</li>
+ <li>A probléma nagy valószínűséggel a webhelyen van, és nem tud mit tenni a megoldása érdekében.</li>
+ <li>A problémáról értesítheti a webhely rendszergazdáját.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Speciális…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label>A(z) <b>%1$s</b> oldal a HTTP Strict Transport Security (HSTS) nevű biztonsági házirendet használja, amely azt jelenti, hogy a(z) <b>%2$s</b> csak biztonságosan kapcsolódhat hozzá. Nem adhat hozzá kivételt, hogy felkeresse ezt az oldalt. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Ugrás vissza</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">A kapcsolat megszakadt</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>A böngésző sikeresen kapcsolódott, de a kapcsolat megszakadt az adatok átvitelekor. Próbálja újra.</p>
+ <ul>
+ <li>Az oldal ideiglenes nem érhető el, vagy túl elfoglalt. Próbálja újra néhány pillanat múlva.</li>
+ <li>Ha egyetlen oldalt sem tud betölteni, akkor ellenőrizze az eszköz adat- vagy Wi-Fi kapcsolatát.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">A kapcsolat időtúllépés miatt megszakadt</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>A kért webhely nem válaszolt a kapcsolatkezdeményezésre, és a böngésző beszüntette a várakozást a válaszra.</p><ul><li>Lehet, hogy a kiszolgáló túl sok lekérést kap, vagy ideiglenesen üzemen kívül van? Próbálja újra később.</li><li>Más webhelyeket sem képes elérni? Ellenőrizze a számítógép hálózati kapcsolatát.</li><li>Lehetséges, hogy tűzfal vagy proxy mögött van a számítógépe vagy a helyi hálózata? A helytelen beállítások zavarhatják a webböngészést.</li><li>Továbbra is fennáll a probléma? Kérjen segítséget a rendszergazdától vagy az internetszolgáltatójától.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">A kapcsolódás sikertelen</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>A webhely ideiglenesen nem érhető el, vagy túl elfoglalt. Próbálja újra néhány pillanat múlva.</li>
+ <li>Ha nem tölt be egyetlen oldal sem, akkor ellenőrizze a mobileszköz adat- vagy Wi-Fi kapcsolatát.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Váratlan válasz a kiszolgálótól</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>A webhely a hálózati kérésre váratlan módon válaszolt, ezért a böngésző nem tudja folytatni a párbeszédet.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Az oldal nem megfelelően van átirányítva</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>A böngésző megszakította a kért elem lekérésére irányuló kísérleteket. A webhely oly módon irányította át a kérést, hogy az soha nem teljesülhet.</p>
+ <ul>
+ <li>Letiltotta a webhely által megkövetelt sütiket?</li>
+ <li>Ha a webhely sütijeinek elfogadása nem oldja meg a problémát, valószínűleg a kiszolgáló beállításában van a hiba, nem az Ön számítógépében.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Kapcsolat nélküli mód</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>A böngésző kapcsolat nélküli módban van, ezért nem tud csatlakozni a kért elemhez.</p>
+ <ul>
+ <li>Csatlakoztatva van a számítógép a hálózathoz?</li>
+ <li>Nyomja meg a „Próbálja újra” gombot az online módba váltáshoz és az oldal újratöltéséhez.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">A port biztonsági okok miatt tiltva van</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>A kért cím olyan portot adott meg (pl. <q>mozilla.org:80</q> a mozilla.org 80-as portjához), amelyet általában <em>nem</em> szokás webböngészés céljaira használni. A böngésző nem engedélyezi ezt a lekérést az Ön védelme és biztonsága érdekében.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">A kapcsolat alaphelyzetbe állt</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>A hálózati kapcsolat megszakadt a kapcsolat újratárgyalásakor. Próbálja újra.</p>
+ <ul>
+ <li>Az oldal ideiglenes nem érhető el, vagy túl elfoglalt. Próbálja újra néhány pillanat múlva.</li>
+ <li>Ha egyetlen oldalt sem tud betölteni, akkor ellenőrizze az eszköz adat- vagy Wi-Fi kapcsolatát.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Nem biztonságos fájltípus</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul><li>Értesítse a webhely tulajdonosait erről a problémáról.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Sérült tartalom hiba</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>A megtekinteni kívánt oldal nem jeleníthető meg, mert az adatátvitel közben hiba történt.</p><ul><li>Lépjen kapcsolatba a webhely üzemeltetőjével, és értesítse a problémáról.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">A tartalom összeomlott</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>A megtekinteni kívánt oldal nem jeleníthető meg, mert az adatátvitel közben hiba történt.</p><ul><li>Lépjen kapcsolatba a webhely üzemeltetőjével, és értesítse a problémáról.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Tartalomkódolási hiba</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>A megtekinteni kívánt oldal nem jeleníthető meg, mert érvénytelen vagy nem támogatott tömörítési formátumot használ.</p>
+ <ul>
+ <li>Lépjen kapcsolatba a webhely üzemeltetőjével, és értesítse a problémáról.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Cím nem található</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+
+ <p>A böngésző nem találta a megadott címhez tartozó gazdakiszolgálót.</p>
+ <ul>
+ <li>Ellenőrizze, hogy nincsenek elírások a címben, például
+ <strong>ww</strong>.example.com a
+ <strong>www</strong>.example.com helyett.</li>
+ <li>Ha egyetlen oldalt sem tud betölteni, akkor ellenőrizze az eszköz adat- vagy Wi-Fi kapcsolatát.</li>
+ </ul>
+
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nincs internetkapcsolat</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Ellenőrizze a hálózati kapcsolatot, vagy próbálja meg újratölteni az oldalt néhány pillanat múlva.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Újratöltés</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Érvénytelen cím</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>A megadott cím formátuma nem felismerhető. Ellenőrizze a címsorba beírtakat, javítsa a hibákat, és próbálja újra.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">A cím érvénytelen</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul><li>A webcímek általában úgy néznek ki, mint a <strong>http://www.example.com/</strong></li><li>Győződjön meg róla, hogy perjeleket használ (tehát <strong>/</strong>).</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Ismeretlen protokoll</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>A cím meghatározott egy protokollt (például <q>wxyz://</q>), amelyet a böngésző nem ismer fel, ezért nem képes megfelelően csatlakozni a webhelyhez.</p>
+ <ul>
+ <li>Multimédiás vagy más, nem szöveges szolgáltatást szeretne elérni? Nézze meg a webhelyet, milyen további követelményeket támaszt.</li>
+ <li>Bizonyos protokollok harmadik féltől származó szoftverek vagy bővítmények meglétét követelik meg, a böngésző csak ezek megléte esetén ismeri fel az adott protokollt.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">A fájl nem található</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Lehet, hogy a fájlt átnevezték, eltávolították vagy áthelyezték?</li>
+ <li>Nincs elgépelés, kis- és nagybetű eltérés stb. a címben?</li>
+ <li>Rendelkezik a megfelelő hozzáférési jogokkal a kért fájlhoz?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">A fájl elérése megtagadva</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Lehet hogy törölve lett, át lett helyezve, vagy a fájljogosultságok megakadályozzák a hozzáférést.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">A proxykiszolgáló visszautasította a kapcsolatot</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>A böngésző proxykiszolgáló használatára van beállítva, de a proxy visszautasította a kapcsolatot.</p>
+ <ul>
+ <li>Jók a böngésző proxybeállításai? Ellenőrizze a beállításokat, és próbálja újra.</li>
+ <li>A proxyszolgáltatás engedélyezi a kapcsolatokat erről a hálózatról?</li>
+ <li>Továbbra is fennáll a probléma? Kérjen segítséget a rendszergazdától vagy az internetszolgáltatójától.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxykiszolgáló nem található</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>A böngésző proxykiszolgáló használatára van beállítva, de a proxy nem található.</p>
+ <ul>
+ <li>Jók a böngésző proxybeállításai? Ellenőrizze a beállításokat, és próbálja újra.</li>
+ <li>Csatlakoztatva van a számítógép a hálózathoz?</li>
+ <li>Továbbra is fennáll a probléma? Kérjen segítséget a rendszergazdától vagy az internetszolgáltatójától.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Kártékony szoftvert terjesztő webhely probléma</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>A(z) %1$s címen működő webhelyről bejelentés érkezett, hogy támadó webhely, ezért a biztonsági beállítások alapján a böngésző a hozzáférést nem engedélyezi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Nem kívánt szoftvert terjesztő webhely probléma</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>A(z) %1$s címen működő webhelyről bejelentés érkezett, hogy nem kívánatos szoftvereket szolgál ki, ezért a biztonsági beállítások alapján a böngésző a hozzáférést nem engedélyezi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Káros webhely probléma</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>A(z) %1$s címen működő webhelyről bejelentés érkezett, hogy ártalmas webhely lehet, ezért a biztonsági beállítások alapján a böngésző a hozzáférést nem engedélyezi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Félrevezető oldal probléma</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>A(z) %1$s címen működő webhelyről bejelentés érkezett, hogy félrevezető webhely, ezért a biztonsági beállítások alapján a böngésző a hozzáférést nem engedélyezi.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Biztonságos webhely nem érhető el</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Engedélyezte a Csak-HTTPS módot a fokozott biztonság érdekében, és a <em>%1$s</em> HTTPS verziója nem érhető el.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Tovább a HTTP oldalra</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..05992822ac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Կրկին փորձել</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Անհնար է հարցումն ավարտել</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Այս խնդրի կամ սխալի մասին հավելյալ տեղեկություններ այժմ անմատչելի է:</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Անվտանգ կապակցումը ձախողվեց</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Այն էջը, որը փորձում եք դիտել, կարող է չերևալ, որովհետև ստացված տվյալների իսկությունը չի կարող ստուգվել:</li>
+ <li>Խնդրում ենք կապնվել կայքի սեփականատերերի հետ՝ նրանց տեղյակ պահելու այս խնդրի մասին:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Անվտանգ կապակցումը ձախողվեց</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Այս խնդիրը կարող է կապված լինել սպասարկիչի կարգավորման հետ կամ, հնարավոր է, որ որևէ մեկը փորձում է որպես սպասարկիչ հանդես գալ:</li>
+ <li>Եթե դուք այս սպասարկիչին նախկինում հաջողությամբ եք կապակցվել, ապա սխալը կարող է ժամանակավոր բնույթ կրել և կարող եք կրկին փորձել ավելի ուշ:</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Ընդլայնված…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Ինչ-որ մեկը կարող է փորձել գաղտնալսել կայքը, և դուք չպետք է շարունակեք: </label>
+        <br> <br>
+        <label> Կայքերը ապացուցում են իրենց ինքնությունը հավաստագրերի միջոցով: %1$sը չի վստահում <b>%2$s</b>-ին, քանի որ դրա վկայագրի թողարկողը անհայտ է, վկայագիրը ինքնագիր է ստորագրված, կամ սպասարկիչը չի ուղարկում ճիշտ միջանկյալ վկայականներ:</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Վերադառնալ (առաջարկվում է)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Ընդունել վտանգը և շարունակել</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Այս կայքը պահանջում է անվտանգ կապ:</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Էջը, որ փորձում եք դիտել, հնարավոր չէ ցուցադրել, քանի որ կայքը պահանջում է անվտանգ կապակցում:</li>
+ <li>Ամենայն հավանականությամբ՝ խնդիրը կայքի հետ է և դուք ոչինչ չեք կարող անել այն ուղղելու համար:</li>
+ <li>Խնդրի մասին կարող եք ծանուցել կայքի վարիչին:</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Լրացուցիչ…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b>-ը ունի անվտանգության քաղաքականություն, որը կոչվում է HTTP Strict Transport Security (HSTS), ինչը նշանակում է, որ <b>%2$s</b>-ը կարող է դրան միմիայն անվտանգ կապակցվել: Դուք չեք կարող բացառություն ավելացնել տվյալ էջն այցելելու համար: </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Հետ գնալ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Կապը խզվեց</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p> Դիտարկիչը հաջողությամբ կապակցվեց, բայց տեղեկատվությունը փոխանցելիս կապն ընդհատվեց: Խնդրում ենք կրկին փորձել: </p>
+      <ul>
+        <li> Կայքը կարող է ժամանակավորապես անհասանելի կամ շատ զբաղված լինել: Կրկին փորձեք մի քանի րոպեից: </li>
+        <li> Եթե չեք կարողանում բեռնել որևէ էջ, ստուգեք ձեր սարքի տվյալները կամ Wi-Fi կապը: </li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Կապը ժամանակասպառվեց</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Հարցվող կայքը չպատասխանեց կապի հարցմանը և ծրագիրը դադարեցրեց պատասխան սպասելուն</p>
+ <ul>
+ <li>Հնարավո՞ր է կայքը խիստ ծանրաբեռնված է կամ ժամանակավորապես չի գործում: Փորձեք քիչ ուշ:</li>
+ <li>Այլ կայքեր ևս չի՞ լինում բացել: Ստուգեք ցանցային կապակցումը:</li>
+ <li>Արդյոք ձեր համակարգիչը կամ ցանցը պաշտպանվա՞ծ են Firewall­-ով կամ պրոքսիով: Դրա սխալ կազմաձևումը կարող է խանգարել վեբի զննմանը:</li>
+ <li>Եթե դեռ դժվարություններ ունեք, ապա օգնության համար դիմեք ցանցային վարիչին կամ համացանցի սպասարկողին:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Հնարավոր չէ միանալ</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Կայքը ժամանակավոր անհասանելի կամ զբաղված է: Փորձեք մի փոքր ավելի ուշ:</li>
+ <li>Եթե չկարողանաք բեռնել որևէ էջ՝ ստուգեք ձեր սարքի տվյալները կամ Wi-Fi կապակցումը:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Անսպասելի պատասխան սպասարկիչից</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Կայքը ցանցային հարցմանը պատասխանեց անսպասելի ձևով, ուստի դիտարկիչը չի կարող շարունակել:</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Էջը ճիշտ չէ վերահասցեավորվել</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Դիտարկիչը դադարեցրեց պահանջվող միույթը ստանալու փորձը: Կայքն այնպես է վերահասցեավորում հարցումը, որ այն երբեք չի ավարտվի:</p>
+ <ul>
+ <li>Արդյո՞ք այս հանգույցի կողմից պահանջվող cookie-ները արգելափակել եք</li>
+ <li>Եթե կայքի cookie-ները ընդունելը խնդիրը չլուծի, ապա ուրեմն, ամենայն հավանականությամբ, դա սպասարկչի կազմաձևման խնդիր է և ոչ թե Ձեր սարքի:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Անցանց կերպ</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Դիտարկիչը աշխատում է անցանց կերպում և չի կարող կապակցվել հարցվող միույթին:</p>
+ <ul>
+ <li>Համակարգիչը կապակցվա՞ծ է ակտիվ ցանցի:</li>
+ <li>Սեղմեք “Կրկին փորձել”՝ առցանց անցնելու և էջը կրկին բեռնելու համար:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Պորտը սահմանափակված է անվտանգության նկատառումներից ելնելով</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Հարցվող հասցեն հատկորոշել է պորտ (օրինակ՝ <q>mozilla.org:80</q>՝ 80 պորտի համար mozilla.org-ում), որը սովորաբար օգտագործվում է <em>այլ</em> նպատակների համար և ոչ վեբի դիտարկման: Ձեր անվտանգության և պաշտպանության նկատառումներով դիտարկիչը չեղարկել է հարցումը:</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Կապը վերակայվեց</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Դիտարկիչի հղումը ընդհատվել է կապակցման ընթացքում: Կրկին փորձեք:</p>
+ <ul>
+ <li>Հնարավոր է՝ կայքը ժամանակավորապես անմատչելի կամ զբաղված է: Փոքր ինչ հետո կրկին փորձեք:</li>
+ <li>Եթե հնարավոր չէ բեռնել որևէ էջ, ապա ստուգեք ձեր սարքի տվյալները կամ Wi-Fi կապակցումը:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Ոչ անվտանգ ֆայլի տեսակ</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Խնդրեմ կապնվեք այս կայքի սեփականատերերի հետ՝ այս խնդրի մասին տեղյակ պահելու համար:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Բովանդակությունը վնասված է</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Տվյալ էջը չի կարող ցուցադրվել, քանի որ տեղի է ունեցնել տվյալների փոխանցման սխալ։</p>
+ <ul>
+ <li>Կապնվեք կայքի սեփականատերերի հետ՝ խնդրի մասին հաղորդելու համար։</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Բովանդակությունը խափանվեց</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Ընթացիկ էջը չի կարող ցուցադրվել, քանի որ տեղի է ունեցնել փոխանցման սխալ։</p>
+ <ul>
+ <li>Կապնվեք վեբ կայքի հեղինակների հետ՝ նրանց այս մասին հաղորդելու համար։</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Բովանդակության գաղտնագրման սխալ</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Էջը, որը դուք փորձում էք դիտել, չի կարող ցուցադրվել, քանի որ այն օգտագործում է սխալ կամ չսպասարկվող խտացման ձև:</p>
+ <ul>
+ <li>Կապնվեք կայքի սեփականատերերի հետ և տեղյակ պահեք նրանց խնդրի մասին:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Հասցեն չի գտնվել</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p> Դիտարկիչը չկարողացավ գտնել տրամադրված հասցեի հոսթի սպասարկիչը:</p>
+      <ul>
+        <li> Ստուգեք հասցեն՝ մուտքագրելու սխալների համար, ինչպիսիք են
+          <strong> ww </strong>.example.com
+          <strong> www </strong>.example.com-ի փոխարեն</li>
+        <li>Եթե որևէ էջ չի բեռնվում, ապա ստուգեք ձեր սարքի տվյալները կամ Wi-Fi կապակցումը:</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Չկա կապակցում համացանցին</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Ստուգեք ձեր ցանցային կապակցումը կամ փորձեք կրկին բեռնել էջը մի քանի վայրկյանից:</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Կրկին բեռնել</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Անվավեր Հասցե</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Ներկայացված հասցեն ճանաչված ձևաչափով չէ: Խնդրում ենք ստուգել տեղադրության գոտին՝ արդյոք սխալ կա, և կրկին փորձել:</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Հասցեն վավեր չէ</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Վեբ հասցեները սովորաբար գրվում են հետևյալ կերպ` <strong>http://www.example.com/</strong></li>
+ <li>Համոզվեք, որ օգտագործում եք հակադարձ սլեշներ (այսինքն` <strong>/</strong>):</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Անհայտ աշխատակարգ</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Հասցեն հատկորոշում է հաղորդակարգ (օրինակ՝ <q>wxyz://</q>), որը դիտարկիչը չի կարողանում ճանաչել, այդ իսկ պատճառով դիտարկիչը ի վիճակի չէ պատշաճ կապակցվել կայքի հետ:</p>
+ <ul>
+ <li>Փորձո՞ւմ եք մուլտիմեդիա կամ այլ ոչ տեքստային ծառայություններ մատչել: Ստուգե՛ք կայքի հավելյալ պահանջները:</li>
+ <li>Որոշ հաղորդակարգեր պահանջում են երրորդ կողմից տրամադրված ծրագրաշար կամ օժանդակ ծրագրեր, որպեսզի դիտարկիչը կարողանա դրանք ճանաչել:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Ֆայլը չի գտնվել</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Կարո՞ղ է նշված միույթը վերանվանվել կամ տեղափոխվել է կամ վերատեղաբաշխվել է:</li>
+ <li>Արդյո՞ք ուղղագրական, մեծատառ/փոքրատառ կամ այլ տպելասխալ չկա հասցեում:</li>
+ <li>Արդյո՞ք մատչելու բավարար թույլտվություն ունեք հարցվող միությի նկատմամբ:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Հասանելիությունը ֆայլին մերժվեց</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Հնարավոր է՝ այն հեռացվել է, տեղափոխվել կամ ֆայլի թույլտվությունները կանխել են մատչումը:</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Փոխանորդ սպասարկիչը մերժեց կապը</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Դիտարկիչը կազմաձևված է օգտագործելու վստահված պրոքսի սպասարկիչ, բայց պրոքսին մերժել է կապակցումը:</p>
+ <ul>
+        <li>Դիտարկիչի պրոքսի կազմաձևումը ճի՞շտ է: Ստուգեք կարգավորումները և կրկին փորձեք:</li>
+        <li>Արդյո՞ք պրոքսի ծառայությունը թույլ է տալիս կապակցումներ այս ցանցից:</li>
+        <li> Դեռ խնդիրներ ունե՞ք: Օգնության համար խորհրդակցեք ձեր ցանցի վարիչի կամ ինտերնետի մատակարարի հետ: </li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Փոխանորդ սպասարկիչը չգտնվեց</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Դիտարկիչը կազմաձևված է օգտագործելու վստահված պրոքսի սպասարկիչ, բայց պրոքսին մերժել է կապակցումը:</p>
+ <ul>
+        <li>Դիտարկիչի պրոքսի կազմաձևումը ճի՞շտ է: Ստուգեք կարգավորումները և կրկին փորձեք:</li>
+        <li>Սարքը կապակցված է ակտիվ ցանցի:</li>
+        <li> Դեռ խնդիրներ ունե՞ք: Օգնության համար խորհրդակցեք ձեր ցանցի վարիչի կամ ինտերնետի մատակարարի հետ: </li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Չարամիտ կայքի թողարկում</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s կայքը հաղորդվել է որպես հարձակվող կայք և արգելափակված է ըստ ձեր անվտանգության նախապատվությունների:</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Կայքի անցանկալի խնդիր</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s կայքը հաղորդվել է որպես անցանկալի ծրագրաշար սպասարկող և արգելափակված է ըստ ձեր անվտանգության նախապատվությունների:</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Վնասակար կայքի խնդիր</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s կայքը հաղորդվել է որպես հավանական վնասակար կայք և արգելափակված է ըստ ձեր անվտանգության նախապատվությունների:</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Խաբուսիկ կայքի խնդիր</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p> %1$s վեբ էջը հաղորդվել է որպես խաբուսիկ կայք և արգելափակված է ըստ ձեր անվտանգության նախապատվությունների:</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Անվտանգ կայքը հասանելի չէ</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Դուք ակտիվացրել եք միայն HTTPS ռեժիմը ուժեղացված անվտանգության համար, և <em>%1$s</em>-ի HTTPS տարբերակը հասանելի չէ:]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Շարունակել HTTP կայքում</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..6320dc055e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ia/strings.xml
@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Retentar</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Impossibile completar le requesta</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Nulle altere information sur iste problema o error es disponibile actualmente.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Connexion secur fallite</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Le pagina que tu tenta vider non pote esser monstrate perque le authenticitate del datos recipite non poteva esser verificate.</li>
+ <li>Contacta le proprietarios del sito web pro informar les de iste problema.</li>
+ </ul>
+
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Connexion secur fallite</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Isto pote esser un problema con le configuration del servitor, o pote esser que un persona tenta de usurpar le identitate del servitor.</li>
+ <li>Si tu te ha connectite a iste servitor con successo in le passato, le error pote esser temporari, e tu pote tentar de novo plus tarde.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avantiate…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Es possibile que alcuno tenta usurpar le identitate del sito. Tu non deberea continuar.</label>
+ <br><br>
+ <label>Sitos web prova lor identitate via certificatos. %1$s non es digne de fide <b>%2$s</b> perque su emissor de certificatos es incognite, le certificato es auto-signate o le servitor non invia le correcte certificatos intermedie.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Retroceder (Recommendate)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Acceptar le risco e continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Le sito web require un connexion secur.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Le pagina que tu tenta de vider non pote monstrar se perque iste sito web require un connexion secur.</li>
+ <li>Le problema es probabilemente con le sito web, e tu nihil pote facer pro resolver lo.</li>
+ <li>Avisa le administrator del sito web re le problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avantiate…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ha un politica de securitate appellate HTTP Strict Transport Security (HSTS), le qual significa que <b>%2$s</b> solo pote connecter se a illo de maniera secur. Tu non pote adder un exception pro visitar iste sito. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Retornar</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Le connexion ha essite interrumpite</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Le navigator se ha connectite con successo, ma le connexion ha essite interrumpite durante le transferimento de informationes. Per favor tenta lo de novo.</p>
+ <ul>
+ <li>Le sito pote esser temporarimente indisponibile o troppo occupate. Retenta in alcun momentos.</li>
+ <li>Si tu non succede a cargar alcun pagina, verifica le connexion de datos o Wi-Fi de tu apparato.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Le connexion ha expirate</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Le sito requestate non ha respondite a un requesta de connexion e le navigator ha cessate de attender un responsa.</p>
+ <ul>
+ <li>Poterea le servitor esser in supercarga o temporarimente foras de servicio? Retenta plus tarde.</li>
+ <li>Impossibile acceder a altere sitos? Verifica le connexion de rete del apparato.</li>
+ <li>Es tu apparato o rete protegite per un firewall o proxy? Configurationes incorrecte pote interferer con le navigation del web.</li>
+ <li>Ancora difficultates? Consulta tu administrator de rete o fornitor de Internet pro assistentia.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Connexion impossibile</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Le sito pote esser temporarimente indisponibile o troppo occupate. Retenta in alcun momentos.</li>
+ <li>Si tu non pote cargar alcun pagina, verifica le connexion de datos o Wi-Fi de tu apparato.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Responsa inexpectate del servitor</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Le sito respondeva al requesta de rete in un maniera impreviste e le navigator non pote continuar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Le pagina non redirige correctemente</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Le navigator ha cessate de tentar recuperar le elemento requestate. Le sito redirige le requesta in un maniera que non se completara jammais.</p>
+ <ul>
+ <li>Ha tu disactivate o blocate cookies necessari pro iste sito?</li>
+ <li>Si acceptar le cookies del sito non resolve le problema, se tracta probabilemente de un problema de configuration del servitor e non de tu apparato.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo disconnectite</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Le navigator opera in su modo sin connexion e non pote connecter se al elemento requestate.</p>
+ <ul>
+ <li>Es le apparato connectite a un rete active?</li>
+ <li>Pulsa “Retentar” pro passar al modo in linea e recargar le pagina.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porta limitate pro rationes de securitate</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+· <p>Le adresse requestate specificava un porta (p.ex. <q>mozilla.org:80</q> pro porta 80 sur mozilla.org) normalmente usate pro <em>altere</em> scopos que le navigation del web. Le navigator ha cancellate le requesta pro tu protection e securitate.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Le connexion ha essite interrumpite</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Le ligation al rete ha essite interrumpite durante le negotiation de un connexion. Per favor tenta lo de novo.</p>
+ <ul>
+ <li>Le sito pote esser temporarimente indisponibile o troppo occupate. Retenta post alcun momentos.</li>
+ <li>Si tu non pote cargar alcun pagina, verifica le connexion de datos o Wi-Fi de tu apparato.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Typo de file non secur</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+<ul>
+ <li>Contacta le proprietarios del sito web pro informar les de iste problema.</li>
+</ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error de contento corrumpite</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Le pagina que tu tenta vider non pote esser monstrate perque un error in le transmission de datos ha essite detegite.</p>
+ <ul>
+ <li>Contacta le proprietarios del sito web pro informar les de iste problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Contento collabite</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Le pagina que tu tenta vider non pote esser monstrate perque un error in le transmission de datos ha essite detegite.</p>
+ <ul>
+ <li>Contacta le proprietarios del sito web pro informar les de iste problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error de codification del contento</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Le pagina que tu tenta vider non pote esser monstrate perque illo usa un forma de compression non valide o non supportate.</p>
+ <ul>
+ <li>Contacta le proprietarios del sito web pro informar les de iste problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresse non trovate</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Le navigator non pote trovar le servitor hospite al adresse fornite.</p>
+ <ul>
+ <li>Verifica le adresse pro errores de scriptura como
+ <strong>ww</strong>.example.com in loco de
+ <strong>www</strong>.example.com.</li>
+ <li>Si tu non pote cargar alcun pagina, verifica le connexion de datos o Wi-Fi de tu apparato.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nulle connexion a Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifica tu connexion de rete o tenta recargar le pagina post alcun momentos.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Adresse non valide</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Le adresse fornite non es in un formato recognoscite. Verifica si il ha errores in le barra de adresse e tenta lo de novo.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Le adresse non es valide</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Adresses web se scribe normalmente in forma <strong>http://www.example.com/</strong></li>
+ <li>Assecura te de usar barras oblique non inverse (i.e. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocollo incognite</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Le adresse specifica un protocollo (p.ex. <q>wxyz://</q>) que le navigator non recognosce, dunque le navigator non pote connecter te correctemente al sito.</p>
+ <ul>
+ <li>Tenta tu acceder a files multimedial o altere servicios non textual? Verifica si le sito ha requisitos additional.</li>
+ <li>Alcun protocollos pote requirer software o plug-ins de tertios ante que le navigator pote recognoscer los.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">File non trovate</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Poterea le objecto haber essite renominate, removite o displaciate?</li>
+ <li>Ha il un error de orthographia, de majusculas o un altere error typographic in le adresse?</li>
+ <li>Ha tu permissiones de accesso sufficiente pro le objecto requestate?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Le accesso al file ha essite refusate</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Illo pote haber essite removite o displaciate, o le permissiones del file pote impedir le accesso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Connexion refusate per le servitor proxy</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Le navigator es configurate pro usar un servitor proxy, ma le proxy refusava un connexion.</p>
+ <ul>
+ <li>Es le configuration del proxy del navigator correcte? Verifica le parametros e retenta.</li>
+ <li>Al le servicio proxy permitte al connexiones de iste rete?</li>
+ <li>Ancora ha tu difficultates? Consulta tu administrator de rete o fornitor de Internet pro assistentia.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Servitor proxy non trovate</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Le navigator es configurate pro usar un servitor proxy, ma le proxy non pote esser trovate.</p>
+ <ul>
+ <li>Es le configuration del proxy del navigator correcte? Verifica le parametros e retenta.</li>
+ <li>Es le apparato connectite a un rete active?</li>
+ <li>Ancora ha tu difficultates? Consulta tu administrator de rete o fornitor de Internet pro assistentia.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de sito malware</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Le sito a %1$s ha essite signalate como sito attaccante e ha essite blocate in base a tu preferentias de securitate.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sito indesirate</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Le sito a %1$s ha essite signalate como servitor de software indesirate e ha essite blocate in base a tu preferentias de securitate.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sito malefic</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Le sito a %1$s ha essite signalate como potentialmente malefic e ha essite blocate in base a tu preferentias de securitate.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de sito fraudulente</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Iste pagina web a %1$s ha essite signalate como sito fraudulente e ha essite blocate in base a tu preferentias de securitate.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sito secur non disponibile</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Tu ha activate le modo solo HTTPS pro melior securitate, ma non es disponibile un version HTTPS de <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar al sito HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..831962e4c1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-in/strings.xml
@@ -0,0 +1,328 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Coba Lagi</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Tidak Dapat Menyelesaikan Permintaan</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Informasi tambahan terkait masalah atau kesalahan ini sedang tidak tersedia.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Sambungan Aman Gagal</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Laman yang Anda ingin lihat tidak dapat ditampilkan karena otentikasi data yang diterima tidak dapat diverifikasi</li>
+ <li>Mohon hubungi pemilik situs web untuk mengabarkan mereka tentang masalah ini.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Sambungan Aman Gagal</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Mungkin terjadi masalah dengan konfigurasi server, atau bisa saja seseorang berusaha menyamar menjadi server.</li>
+ <li>Jika Anda pernah tersambung dengan baik, kesalahan ini mungkin hanya sementara dan Anda dapat mencoba lagi nanti.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Tingkat Lanjut…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Seseorang mungkin berusaha menyamar sebagai situs dan sebaiknya Anda tidak melanjutkan.</label>
+ <br><br>
+ <label>Situs web membuktikan identitasnya menggunakan sertifikat. %1$s tidak mempercayai <b>%2$s</b> karena penerbit sertifikat tidak diketahui, sertifikat ditandatangani sendiri, atau server tidak mengirimkan sertifikat perantara yang benar.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Kembali (Disarankan)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Terima Risikonya dan Lanjutkan</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Situs web ini membutuhkan sambungan aman.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Halaman yang Anda coba lihat tidak dapat ditampilkan karena situs web ini memerlukan sambungan aman</li>
+ <li>Kemungkinan besar, masalahnya terletak pada situs web, dan tidak ada yang dapat Anda lakukan untuk menyelesaikannya.</li>
+ <li>Anda dapat memberi tahu admin situs web tentang masalah ini.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Tingkat lanjut…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> memiliki kebijakan keaman yang disebut HTTP Strict Transport Security (HSTS), yang berarti <b>%2$s</b> hanya dapat terhubung dengan aman. Anda tidak dapat menambahkan pengecualian untuk mengunjungi situs ini. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Kembali</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Sambungan terputus</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Peramban terhubung dengan sukses, namun koneksi terganggu saat mentransfer informasi. Silakan coba lagi.</p>
+ <ul>
+ <li>Situs ini mungkin sementara tidak sedia atau sedang sibuk. Coba lagi dalam beberapa saat.</li>
+ <li>Jika Anda tidak dapat memuat laman apapun, periksa koneksi data atau Wi-Fi pada perangkat Anda.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Tenggang waktu tersambung habis</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Situs yang diminta tidak menjawab permintaan sambungan dan peramban berhenti menunggu jawaban</p>
+ <ul>
+ <li>Mungkinkan server sedang dalam keadaan sibuk atau mati sementara? Coba lagi nanti.</li>
+ <li>Apakah Anda tidak dapat mengakses situs lainnya? Periksa koneksi jaringan komputer Anda.</li>
+ <li>Apakah jaringan atau komputer Anda dilindungi firewall atau proxy? Pengaturan yang salah dapat mengganggu penjelajahan Web.</li>
+ <li>Masih bermasalah? Tanyakan pada adminstrator jaringan Anda atau Penyedia Jasa Layanan Internet Anda.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Tidak dapat tersambung</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Situs mungkin tidak tersedia untuk sementara atau terlalu sibuk . Cobalah beberapa saat lagi.</li>
+ <li>Jika Anda tidak dapat memuat halaman apa pun, periksa data peranti atau koneksi Wi-Fi Anda.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respon tidak terduga dari server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Situs menanggapi permintaan jaringan dengan cara yang tak terduga dan peramban tidak dapat melanjutkan.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Laman tidak teralihkan dengan benar</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Peramban telah berhenti untuk menerima item yang diminta. Situs tersebut mengalihkan permintaan seakan-akan tidak pernah selesai</p>
+ <ul>
+ <li>Apakah Anda menonaktifkan atau memblokir kuki yang diperlukan oleh situs ini?</li>
+ <li>Jika menerima kuki situs tidak menyelesaikan masalah, nampaknya ada masalah konfigurasi server dan bukan masalah pada komputer Anda.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mode Luring</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Peramban beroperasi dalam mode luring dan tidak dapat terhubung pada item yang diminta.</p>
+ <ul>
+ <li>Apakah komputer terhubung pada jaringan aktif?</li>
+ <li>Tekan “Coba Lagi” untuk berpindah ke mode daring dan memuat ulang laman.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port dibatasi untuk alasan keamanan</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Alamat yang diminta menspesifikasikan sebuah port (contoh: <q>mozilla.org:80</q> untuk port 80 pada mozilla.org) biasanya digunakan untuk keperluan <em>selain</em> dari penjelajahan Web. Peramban telah membatalkan permintaan untuk perlindungan dan keamanan Anda.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Sambungan diputus</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Tautan jaringan terganggu saat menegosiasikan koneksi. Silakan coba lagi.</p>
+ <ul>
+ <li>Situs bisa saja sedang tidak tersedia sementara atau terlalu sibuk. Coba lagi dalam beberapa saat.</li>
+ <li>Jika Anda tidak dapat memuat laman apapun, cek koneksi data atau Wi-Fi perangkat Anda.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Jenis Berkas Tidak Aman</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Mohon hubungi pemilik situs web untuk mengabarkan masalah ini pada mereka.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Galat Konten Rusak</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+
+ <p>Laman yang akan dibuka tidak dapat ditampilkan karena ada terdeteksi galat pada pengiriman data</p>
+ <ul>
+ <li>Silakan hubungi pemilik situs web mengenai masalah ini.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Konten macet</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Laman yang Anda coba lihat tidak dapat ditampilkan karena terdapat gangguan transmisi data.</p>
+ <ul>
+ <li>Silakan hubungi pengguna situs Web untuk mengabarkan masalah ini kepada mereka.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Kesalahan Pengodean Isi (Content Encoding)</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Laman yang Anda coba lihat tidak dapat ditampilkan karena menggunakan bentuk kompresi yang tidak valid atau tidak didukung.</p>
+ <ul>
+ <li>Silakan hubungi pengguna situs Web untuk mengabarkan masalah ini kepada mereka.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Alamat Tidak Ditemukan</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Peramban tidak dapat menemukan host server untuk alamat yang disediakan.</p>
+ <ul>
+ <li>Periksa alamat untuk kesalahan pengetikan seperti
+ <strong>ww</strong>.contoh.com alih-alih
+ <strong>www</strong>.contoh.com.</li>
+ <li>Jika Anda tidak dapat memuat laman apapun, periksa koneksi data atau Wi-Fi perangkat Anda.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Tidak ada sambungan Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Periksa koneksi jaringan Anda atau coba muat ulang halaman dalam beberapa saat.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Muat ulang</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Alamat Tidak Valid</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Laman yang diberikan tidak dalam format yang dikenali. Periksa ulang bilah lokasi kemudian coba kembali.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Alamat tidak valid</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Alamat Web biasanya ditulis seperti <strong>http://www.example.com/</strong></li>
+ <li>Pastikan Anda menggunakan garis miring depan (contoh: <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protokol Tidak Dikenal</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Alamat menspesifikasi protokol (contoh: <q>wxyz://</q>) yang tidak dikenali peramban, sehingga peramban tidak dapat menghubungi situs dengan baik.</p>
+ <ul>
+ <li>Apakah Anda ingin mengakses layanan multimedia atau non-teks lainnya? Periksa situs untuk persyaratan tambahan.</li>
+ <li>Beberapa protokol mungkin memerlukan perangkat lunak atau plugin pihak ketiga sebelum peramban dapat mengenalinya.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Berkas Tidak Ditemukan</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Apakah objek telah diganti namanya, dibuang, atau dipindahkan?</li>
+ <li>Apa tidak ada kesalahan ejaan, huruf besar, atau <em>kesaalhan keitk</em> lainnya pada penulisan alamat?</li>
+ <li>Apakah Anda memiliki hak akses untuk mengakses objek yang diminta?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Akses ke berkas ditolak</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Mungkin sudah dihapus, dipindahkan, atau hak akses yang ada mencegah akses terhadap berkas.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Server Proxy Menolak Sambungan</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+
+ <p>Program peramban diatur untuk menggunakan server proksi, tetapi server proksi menolak sambungan.</p>
+ <ul>
+ <li>Apakah pengaturan proksi peramban sudah benar? Periksa lagi pengaturan tersebut dan coba lagi.</li>
+ <li>Apakah layanan proksi mengizinkan sambungan dari jaringan komputer ini?</li>
+ <li>Masih bermasalah? Tanyakan pada administrator jaringan Anda atau Penyedia Jasa Layanan Internet (Internet Service Provider) untuk mendapatkan bantuan.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Server Proksi Tidak Ditemukan</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Program peramban diatur untuk menggunakan server proxy, tetapi server proxy tidak bisa ditemukan.</p>
+ <ul>
+ <li>Apakah pengaturan proxy program peramban sudah benar? Periksa lagi pengaturan tersebut dan coba lagi.</li>
+ <li>Apakah komputer tersambung pada jaringan yang berfungsi?</li>
+ <li>Masih bermasalah? Tanyakan pada administrator jaringan Anda atau Penyedia Jasa Layanan Internet (Internet Service Provider).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Masalah situs malware</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Situs di %1$s telah dilaporkan sebagai situs penyerang dan telah diblokir sesuai dengan pengaturan keamanan Anda.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Masalah situs yang tidak diinginkan</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Situs di %1$s telah dilaporkan melayani perangkat lunak yang tidak diinginkan dan telah diblokir sesuai dengan pengaturan keamanan Anda.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Masalah situs berbahaya</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Situs di %1$s telah dilaporkan sebagai situs yang berpotensi membahayakan dan telah diblokir sesuai dengan pengaturan keamanan Anda.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Masalah situs tipuan</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Laman web di %1$s telah dilaporkan sebagai situs tipuan dan telah diblokir sesuai dengan pengaturan keamanan Anda.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Situs Aman Tidak Tersedia</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Anda telah mengaktifkan Mode Hanya HTTPS untuk keamanan yang ditingkatkan tetapi versi HTTPS <em>%1$s</em> tidak tersedia.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Lanjutkan ke Situs HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..a0b48ded36
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-is/strings.xml
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Reyndu aftur</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Get ekki klárað beiðni</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Viðbótarupplýsingar um þetta vandamál eru ekki til staðar.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Ekki tókst að koma á öruggri tengingu</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Ekki er hægt að sýna síðuna vegna þess að ekki var hægt að auðkenna gögnin.</li>
+ <li>Hafið samband við vefstjóra vefsvæðisins til að láta hann vita af þessu vandamáli.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Ekki tókst að koma á öruggri tengingu</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Það gæti verið vandamál með uppsetningu netþjónsins eða það gæti einhver verið að reyna að þykjast vera þessi netþjónn.</li>
+ <li>Ef þú hefur náð að tengjast þessum netþjón áður þá gæti villan verið tímabundin og þú gætir reynt aftur síðar.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Frekari stillingar…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Einhver gæti verið að reyna að þykjast vera þetta vefsvæði og þú ættir ekki að halda áfram.</label>
+ <br><br>
+ <label>Vefsvæði sanna auðkenni sitt með skírteini. %1$s treysti ekki <b>%2$s</b> vegna þess að útgefandi skírteinisins er óþekktur, skírteinið er gefið út af sjálfu sér eða þjóninn er ekki að senda rétt milli skírteini.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Til baka (mælt með)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Samþykkja áhættuna og halda áfram</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Þetta vefsvæði krefst öruggrar tengingar.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Síðan sem þú ert að reyna að skoða er ekki hægt að sýna vegna þess að þetta vefsvæði krefst öruggrar tengingar.</li>
+ <li>Vandamálið tengist líklegast vefsvæðinu sjálfu og það er ekkert sem þú getur gert til að leysa það.</li>
+ <li>Þú getur látið stjórnanda vefsvæðisins vita um vandamálið.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Ítarlegt…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ · <label> <b>%1$s</b> er með öryggisstefnu sem kallast HTTP Strict Transport Security (HSTS), sem þýðir að <b>%2$s</b> getur aðeins tengst því á öruggan hátt. Þú getur ekki bætt við undantekningu til að heimsækja þetta vefsvæði.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Fara til baka</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Tenging slitnaði</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Vafrinn náði að tengjast en tengingin rofnaði vði flutning upplýsinga. Endilega reyndu aftur.</p>
+ <ul>
+ <li>Vefsvæðið gæti verið ótiltækt tímabundið eða of undir of miklu álagi. Reyndu aftur eftir smá tíma.</li>
+ <li>Athugaðu gagnatengingu tækisins eða þráðulausu tenginguna ef þú getur ekki hlaðið síður.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Tengingin svaraði ekki tímanlega</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Umbeðið vefsvæði svaraði ekki tengingu og vafrinn hætti að bíða eftir svari.</p>
+ <ul>
+ <li>Gæti verið að svæðið sé undir of miklu álagi eða sé niðri tímabundið? Reyndu aftur seinna.</li>
+ <li>Getur þú ekki náð sambandi við önnur vefsvæði? Athugaðu nettenginu tækisins.</li>
+ <li>Er tækið þitt varið af eldvegg eða milliþjóni? Rangar stilingar gætu haft áhrif á vafra.</li>
+ <li>Er vandamálið enn til staðar? Hafðu samband við kerfistjóra eða netaðila eftir aðstoð.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Get ekki tengst</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Svæðið gæti verið ótiltækt tímabundið eða of upptekið til að svara. Reyndu aftur eftir smá tíma.</li>
+ <li>Skoðaðu gagnatenginguna eða þráðlausutenginguna á tækinu þínu ef þú nærð ekki að hlaða inn neinum síðum.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Rangt svar frá netþjóni</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Vefsvæðið svaraði netbeiðni á óvæntan hátt þannig að vafrinn getur ekki haldið áfram.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Síðan er ekki að endurbeina rétt</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Vafrinn hefur hætt að reyna að ná í umbeðinn hlut. Vefsvæðið er að endursenda beiðnina á þann hátt að því mun aldrei ljúka.</p>
+ <ul>
+ <li>Hefurðu lokað á eða gert vefkökur óvirkar frá þessu vefsvæð?</li>
+ <li>Ef þú velur að taka á móti vefkökum frá þessu vefsvæði og það lagar ekki vandamálið, er netþjónninn sjálfur líklega rangt stilltur en ekki tölvan þín.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Ónettengdur hamur</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Vafrinn er að vinna án nettengingar og getur ekki tengst við umbeðna síðu.</p>
+ <ul>
+ <li>Er tölvan tengd við virkt net?</li>
+ <li>Smelltu á “Reyna aftur” til að tengjast netingu og endurnýja síðuna.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Vegna öryggis er aðgangur að þessari gátt ekki leyfður</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Umbeðið veffang bað um ákveðna gátt (t.d. <q>mozilla.org:80</q> fyrir gátt 80 á mozilla.org) sem venjulega er notað fyrir eitthvað <em>annað</em> en að vafra. Vafrinn hefur lokað á beiðnina þér til verndar.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Tenging slitnaði</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Vafrinn náði að tengjast en tengingin rofnaði við flutning upplýsinga. Endilega reyndu aftur.</p>
+ <ul>
+ <li>Vefsvæðið gæti verið ótiltækt tímabundið eða of undir of miklu álagi. Reyndu aftur eftir smá tíma.</li>
+ <li>Athugaðu gagnatengingu tækisins eða þráðulausu tenginguna ef þú getur ekki hlaðið neinar síður.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Óörugg skráartegund</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Hafðu samband við vefstjóra vefsvæðisins og láttu hann vita af þessu vandamáli.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Villa vegna skemmdra gagna</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Ekki er hægt að sýna síðuna vegna villu í gagnasendingu.</p>
+ <ul>
+ <li>Hafðu samband við vefsíðueiganda til að láta hann vita af vandamálinu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Innihald olli hruni</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Ekki er hægt að sýna síðuna vegna villu í gagnasendingu.</p>
+ <ul>
+ <li>Hafðu samband við vefsíðueiganda til að láta hann vita af vandamálinu.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Kóðunarvilla</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Ekki er hægt að sýna síðuna því hún notar ógilda eða óstudda þjöppun.</p>
+ <ul>
+ <li>Hafðu samband við vefstjóra vefsvæðisins og láttu hann vita af þessu vandamáli.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Veffang fannst ekki</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Vafrinn gat ekki fundið hýsingarþjóninn fyrir þetta vistfang.</p>
+ <ul>
+ <li>Athugaðu hvort það séu villu í vistfanginu, eins og t.d.
+ <strong>ww</strong>.example.com í staðinn fyrir
+ <strong>www</strong>.example.com.</li>
+ <li>Ef þú getur ekki hlaðið neinum vefsvæðum, athugaðu þá gagnatenginu eða þráðulausu tenginu tækis þíns.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Engin internet tenging</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Athugaðu nettenginguna þína eða reyndu að endurglæða síðuna eftir nokkrar mínútur.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Endurglæða</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Veffang ekki gilt</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Innslegið veffang er ekki á viðurkenndu sniði. Skoðaðu veffangið í stikunni og reyndu aftur.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Veffang ekki gilt</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Vistföng eru vanalega rituð á þennan hátt <strong>http://www.example.com/</strong></li>
+ <li>Gangtu úr skugga um að þú sért að nota rétt skástrik (s.s. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Óþekkt samskiptaregla</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Umbeðið vistfang (t.d., <q>wxyz://</q>) skilgreinir samskiptareglu sem vafrinn kannast ekki við, þannig að vafrinn getur ekki tengst við vefsvæðið.</p>
+ <ul>
+ <li>Ertu að reyna að tengjast myndefni eða annarskonar gagnaþjónustum? Athugaðu á vefsvæðinu hvort þú þurfir fleiri hluti.</li>
+ <li>Sumar samskiptareglur þarfnast forrita eða viðbóta frá þriðja aðila áður en vafrinn getur þekkt þær.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Skrá fannst ekki</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Gæti verið að hluturinn hafi verið endurnefndur, fjarlægður eða færður til?</li>
+ <li>Er stafsetningarvilla, hástafaritun, eða annarskonar stafsetningarvilla í vistfanginu?</li>
+ <li>Hefur þú nægar aðgangsheimildir?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Aðgangur að skránni ekki leyfður</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Vera má að skráin hafi verið fjarlægð, færð til eða réttindarleyfi komi í veg fyrir aðgengi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Milliþjónn neitar tengingum</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Vafrinn er stilltur til að nota milliþjóna, en milliþjónninn hafnaði tengingu.</p>
+ <ul>
+ <li>Eru vafra stillingar milliþjónsins réttar? Athugaðu stillingarnar og reyndu aftur.</li>
+ <li>Leyfir milliþjónn tengingar frá þínu neti?</li>
+ <li>Hafðu samband við kerfisstjóra eða netaðila ef vandamálin eru enn til staðar.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Fann ekki milliþjón</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Vafrinn er stilltur til að nota milliþjóna, en milliþjónn fannst ekki.</p>
+ <ul>
+ <li>Eru vafra stillingar milliþjóns rétt stilltar? Athugaðu stillingarnar og reyndu aftur.</li>
+ <li>Er tækið tengt virku neti?</li>
+ <li>Hafðu samband við kerfisstjóra eða netaðila ef vandamálin eru enn til staðar.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Vandamál með spilliforrit</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Vefsvæðið %1$s hefur verið tilkynnt sem árásarsvæði og aðgangur hefur verið hindraður vegna öryggisstillinga.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Vandamál með óæskilegt vefsvæði</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Tilkynnt hefur verið að vefsvæðið %1$s sé að deila óæskilegum hugbúnaði og hefur aðgangur að því verið lokaður vegna öryggisstillinga.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Villa vegna skaðlegs vefsvæðis</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Tilkynnt hefur verið að vefsvæðið %1$s sé að deila óæskilegum hugbúnaði og hefur aðgangur að því verið lokaður vegna öryggisstillinga..</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Villa vegna svindlsvæðis</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Vefsvæðið %1$s hefur verið tilkynnt sem svindlsvæði og aðgangur hefur verið hindraður vegna öryggisstillinga.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Öruggt vefsvæði ekki tiltækt</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Þú hefur virkjað Einungis-HTTPS-ham til að auka öryggi en HTTPS-útgáfa af <em>%1$s</em> er ekki tiltæk.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Halda áfram á HTTP-vefsvæði</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..20ba1f388c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-it/strings.xml
@@ -0,0 +1,290 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Riprovare</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Impossibile completare la richiesta</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Attualmente non sono disponibili informazioni aggiuntive relative a questo problema o errore.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Connessione sicura non riuscita</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>La pagina che si sta cercando di visualizzare non può essere mostrata in quanto non è possibile verificare l’autenticità dei dati ricevuti.</li>
+ <li> Contattare il responsabile del sito web per informarlo del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Connessione sicura non riuscita</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Potrebbe trattarsi di un problema nella configurazione del server oppure di un tentativo da parte di qualcuno di sostituirsi al server stesso.</li>
+ <li> Se è stato possibile connettersi a questo server in passato, il problema potrebbe essere solo temporaneo. Si consiglia di riprovare in seguito.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzate…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Potrebbe trattarsi di un tentativo di sostituirsi al sito originale. È sconsigliato proseguire.</label>
+ <br><br>
+ <label>I siti web garantiscono la propria identità attraverso certificati. %1$s non considera il sito <b>%2$s</b> attendibile perché l’autorità emittente del certificato è sconosciuta, il certificato è autofirmato oppure il server non ha inviato i certificati intermedi previsti.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Torna indietro (consigliato)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Accetta il rischio e continua</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Questo sito web richiede una connessione sicura.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[<ul>
+ <li>La pagina che si sta cercando di visualizzare non può essere mostrata in quanto questo sito web richiede una connessione sicura.</li>
+ <li>L’errore è probabilmente causato dal sito web e non può essere risolto.</li>
+ <li>È possibile segnalare il problema al gestore del sito web.</li>
+</ul>
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanzate…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label><b>%1$s</b> utilizza un criterio di sicurezza chiamato HTTP Strict Transport Security (HSTS). Questo significa che <b>%2$s</b> può connettersi solo in modo sicuro e non è possibile aggiungere un’eccezione per visitare questo sito.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Torna indietro</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La connessione è stata interrotta</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Il browser è stato connesso correttamente, ma la connessione è stata interrotta durante il trasferimento delle informazioni. Riprovare. </p>
+ <ul>
+ <li> Il sito potrebbe essere temporaneamente non disponibile o troppo occupato. Riprovare tra qualche istante. </li>
+ <li> Se non si riesce a caricare alcuna pagina, controllare i dati del dispositivo o la connessione Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Tempo per la connessione esaurito</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Il sito richiesto non ha risposto a una richiesta di connessione e il browser ha smesso di attendere una risposta.</p>
+ <ul>
+ <li>È possibile che il server sia soggetto a un’elevata richiesta o un’interruzione temporanea. Riprovare più tardi.</li>
+ <li>Si riesce a navigare su altri siti? Controllare la connessione di rete del dispositivo. </li>
+ <li> Il dispositivo o la rete sono protetti da un firewall o un proxy? Impostazioni errate possono interferire con la navigazione sul web. </li>
+ <li> Si riscontrano ancora problemi? Consultare l’amministratore di rete o il provider Internet per ricevere assistenza. </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Connessione non riuscita</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li> Il sito potrebbe essere temporaneamente non disponibile o troppo occupato. Riprovare tra qualche istante. </li>
+ <li> Se non si riesce a caricare alcuna pagina, controllare i dati del dispositivo o la connessione Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Risposta inattesa del server</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Il sito ha risposto alla richiesta di rete in modo imprevisto, quindi il browser non può continuare.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La pagina non reindirizza in modo corretto</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p> Il browser ha smesso di cercare di recuperare l’elemento richiesto. Il sito sta reindirizzando la richiesta in un modo che non sarà mai completato. </p>
+ <ul>
+ <li> Sono stati disabilitati o bloccati i cookie richiesti da questo sito? </li>
+ <li> Se accettando i cookie del sito non risolve il problema, è probabile che si tratti di un problema di configurazione del server e non del dispositivo. </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Non in linea</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Il browser si trova in modalità non in linea e non è possibile connettersi con l’elemento richiesto.</p>
+ <ul>
+ <li>Il dispositivo è collegato a una rete attiva?</li>
+ <li>Selezionare “Riprova” per passare alla modalità in linea e ricaricare la pagina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porta bloccata per motivi di sicurezza</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p> L’indirizzo richiesto specificava una porta (per es. <q> Mozilla.org:80</q> per la porta 80 su mozilla.org) normalmente utilizzata per scopi <em>diversi</em> dalla navigazione sul web. Il browser ha annullato la richiesta per garantire la protezione e la sicurezza dell’utente. </p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La connessione è stata annullata</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Il collegamento di rete è stato interrotto durante la negoziazione di una connessione. Riprovare. </p>
+ <ul>
+ <li> Il sito potrebbe essere temporaneamente non disponibile o troppo occupato. Riprovare tra qualche istante. </li>
+ <li> Se non si riesce a caricare alcuna pagina, controllare i dati del dispositivo o la connessione Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo di file non sicuro</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Contattare il proprietario del sito web per informarlo del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Errore: contenuto danneggiato</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>La pagina che si sta cercando di visualizzare non può essere mostrata a causa di un errore di trasmissione dati.</p>
+ <ul>
+ <li>Contattare l’amministratore del sito per segnalare il problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Arresto anomalo del contenuto</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>La pagina che si sta cercando di visualizzare non può essere mostrata a causa di un errore di trasmissione dati.</p>
+ <ul>
+ <li>Contattare l’amministratore del sito per segnalare il problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Errore di codifica del contenuto</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>La pagina che si sta cercando di visualizzare non può essere mostrata poiché fa uso di una forma di compressione non valida o non supportata.</p>
+ <ul>
+ <li>Contattare il proprietario del sito web per informarlo del problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Indirizzo non trovato</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Il browser non è riuscito a trovare il server host per l’indirizzo fornito.</p>
+ <ul>
+ <li>Verificare se l’indirizzo contiene errori di battitura del tipo
+ <strong>ww</strong>.example.com invece di
+ <strong>www</strong>.example.com</li>
+ <li>Se non è possibile caricare alcuna pagina, controllare la connessione dati o Wi-Fi del dispositivo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nessuna connessione a Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifica la tua connessione di rete o prova a ricaricare la pagina tra qualche istante.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Ricarica</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Indirizzo non valido</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p> L’indirizzo fornito non è in un formato riconosciuto. Controllare la barra degli indirizzi per individuare eventuali errori e riprovare. </p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">L’indirizzo non è valido</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Gli indirizzi internet normalmente si scrivono nella forma <strong>http://www.example.com/</strong></li>
+ <li>Verificare se si stanno utilizzando le barre corrette (ad esempio <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocollo sconosciuto</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>L’indirizzo richiede un protocollo (ad es. <q>wxyz://</q>) che il browser non riconosce, quindi non può collegarsi correttamente al sito.</p>
+<ul>
+ <li>Si sta accedendo a servizi multimediali o non testuali? Verificare sul sito i requisiti necessari.</li>
+ <li>Alcuni protocolli richiedono software esterni o plugin affinché il browser li possa riconoscere.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">File non trovato</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>L’oggetto potrebbe essere stato rinominato, rimosso o spostato.</li>
+ <li>Potrebbe esserci un errore di ortografia nell’indirizzo.</li>
+ <li>Si possiedono i permessi per accedere all’oggetto specificato?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Accesso al file non consentito</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Il file potrebbe essere stato spostato o cancellato oppure i permessi sul file potrebbero impedirne l’accesso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Connessione rifiutata dal server proxy</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p> Il browser è configurato per utilizzare un server proxy, ma il proxy ha rifiutato una connessione. </p>
+ <ul>
+ <li> La configurazione proxy del browser è corretta? Controllare le impostazioni e riprovare. </li>
+ <li> Il servizio proxy consente le connessioni da questa rete? </li>
+ <li> Si riscontrano ancora problemi? Consultare l’amministratore di rete o il provider Internet per ricevere assistenza. </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Impossibile contattare il server proxy</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p> Il browser è configurato per utilizzare un server proxy, ma il proxy non è stato rilevato. </p>
+ <ul>
+ <li> La configurazione proxy del browser è corretta? Controllare le impostazioni e riprovare. </li>
+ <li> Il dispositivo è collegato a una rete funzionante? </li>
+ <li> Si riscontrano ancora problemi? Consultare l’amministratore di rete o il provider Internet per ricevere assistenza. </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema di sito con malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Il sito web %1$s è stato segnalato come sito web malevolo ed è stato bloccato sulla base delle impostazioni di sicurezza.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema di sito non desiderato</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Il sito web %1$s è stato segnalato come un sito contenente software indesiderato ed è stato bloccato sulla base delle impostazioni di sicurezza.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema di sito pericoloso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Il sito web %1$s è stato segnalato come sito web potenzialmente pericoloso ed è stato bloccato sulla base delle impostazioni di sicurezza.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema con sito ingannevole</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Il sito web %1$s è stato segnalato come sito ingannevole ed è stato bloccato sulla base delle impostazioni di sicurezza.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Versione sicura del sito non disponibile</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[È stata attivata la modalità solo HTTPS per una maggiore sicurezza ma non è disponibile una versione HTTPS di <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Prosegui sul sito HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..186c0b6280
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-iw/strings.xml
@@ -0,0 +1,296 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ניסיון חוזר</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">לא ניתן להשלים את הבקשה</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+<p>מידע נוסף על בעיה או שגיאה זו אינו זמין כעת.</p>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">חיבור מאובטח נכשל</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>לא ניתן להציג את הדף המבוקש מכיוון שאין אפשרות לאמת את אמינות הנתונים שהתקבלו.</li>
+ <li>נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">חיבור מאובטח נכשל</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>יתכן שמדובר בבעיה בתצורת השרת או שגורם כלשהו מנסה להתחזות לשרת.</li>
+ <li>אם התחברת לשרת זה בהצלחה בעבר, ייתכן שהשגיאה זמנית, ומומלץ לנסות שוב מאוחר יותר.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">מתקדם…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>יכול להיות שמישהו מנסה להתחזות לאתר ועדיף שלא להמשיך.</label>
+ <br><br>
+ <label>אתרים מוכיחים את מהימנותם באמצעות אישורים. ל־%1$s אין אמון ב־<b>%2$s</b> כיוון שמנפיק האישור אינו מוכר, האישור נחתם עצמאית, או שהשרת לא שולח את אישורי הביניים הנכונים.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">חזרה (מומלץ)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">קבלת הסיכון והמשך</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">אתר זה דורש חיבור מאובטח.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>העמוד בו הינך מנסה לצפות אינו ניתן להצגה מכיוון שאתר זה דורש חיבור מאובטח.</li>
+ <li>כנראה שהבעיה היא באתר, ואין שום דבר שבאפשרותך לעשות כדי לפתור זאת.</li>
+ <li>ניתן להודיע למנהל האתר על הבעיה.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">מתקדם…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label> ל־<b>%1$s</b> יש מדיניות אבטחה בשם אבטחת תעבורה מחמירה של HTTP ‏(HSTS), כלומר <b>%2$s</b> יכול להתחבר לאתר באופן מאובטח בלבד. לא ניתן להוסיף חריגה כדי לבקר באתר זה.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">חזרה אחורה</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">החיבור הופסק</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>הדפדפן התחבר בהצלחה אך החיבור הופסק במהלך העברת המידע. נא לנסות שוב.</p>
+ <ul>
+ <li>יכול להיות שהאתר אינו זמין או עמוס מדי. יש לנסות שוב בעוד מספר רגעים.</li>
+ <li>אם אף דף אינו נטען, יש לוודא שחיבור הנתונים הסלולריים או רשת ה־Wi-Fi תקין.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">תם הזמן המוקצב לחיבור</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>האתר המבוקש לא הגיב לבקשת התחברות והדפדפן הפסיק להמתין לתגובה.</p>
+ <ul>
+ <li>האם ייתכן שהשרת חווה עומסים גבוהים או הפסקה זמנית? נא לנסות שוב מאוחר יותר.</li>
+ <li>האם אין ביכולתך לגלוש באתרים אחרים? נא לבדוק את הגדרות החיבור לרשת של המחשב.</li>
+ <li>האם המחשב או הרשת שלך מוגנים על־ידי חומת אש או שרת מתווך? הגדרות לא נכונות עלולות להפריע לגלישה באינטרנט.</li>
+ <li>עדיין נתקל בבעיות? היוועץ במנהל הרשת או בספק האינטרנט שלך לקבלת עזרה.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">כישלון בהתחברות</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>יכול להיות שהאתר אינו זמין או עמוס מדי. יש לנסות שוב בעוד מספר רגעים.</li>
+ <li>אם אף דף אינו נטען, יש לוודא שחיבור הנתונים הסלולריים או רשת ה־Wi-Fi תקין.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">תגובה לא צפויה מהשרת</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>האתר הגיב לבקשה מהרשת באופן בלתי צפוי, והדפדפן אינו יכול להמשיך.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">הדף מבצע העברה לא תקינה</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>הדפדפן הפסיק לנסות למצוא את הפריט המבוקש. האתר מכוון מחדש את הבקשה בצורה שלעולם לא תושלם.</p>
+ <ul>
+ <li>האם ביטלת או חסמת עוגיות שנדרשות על־ידי אתר זה?</li>
+ <li>אם קבלת עוגיות מאתר זה אינה פותרת את הבעיה, סביר שמדובר בתקלה בהגדרות השרת ולא במכשיר שלך.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">מצב לא־מקוון</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>הדפדפן פועל כעת במצב לא־מקוון ואינו יכול להתחבר לפריט המבוקש.</p>
+ <ul>
+ <li>האם המכשיר מחובר לרשת פעילה?</li>
+ <li>יש ללחוץ על ״ניסיון חוזר״ כדי לעבור למצב מקוון ולטעון מחדש את הדף.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">השער נחסם מסיבות אבטחה</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>הכתובת המבוקשת ציינה שער (לדוגמה: <q>mozilla.org:80</q> עבור שער 80 ב־mozilla.org) המיועד בדרך כלל לשימוש <em>אחר</em> מאשר גלישה באינטרנט. הדפדפן ביטל את הבקשה עבור ההגנה והאבטחה שלך.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">החיבור הופסק</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>קישור הרשת נקטע במהלך המשא והמתן על החיבור. נא לנסות שוב.</p>
+ <ul>
+ <li>יכול להיות שהאתר אינו זמין או עמוס מדי. יש לנסות שוב בעוד מספר רגעים.</li>
+ <li>אם אף דף אינו נטען, יש לוודא שחיבור הנתונים הסלולריים או רשת ה־Wi-Fi תקין.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">סוג קובץ מסוכן</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">שגיאת תוכן פגום</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>לא ניתן להציג את הדף המבוקש מכיוון שאותרה שגיאה בתעבורת הנתונים.</p>
+ <ul>
+ <li>נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">התוכן קרס</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>לא ניתן להציג את הדף המבוקש מכיוון שאותרה שגיאה בתעבורת הנתונים.</p>
+ <ul>
+ <li>נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">שגיאה בקידוד תוכן</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>לא ניתן להציג את הדף המבוקש מכיוון שהוא משתמש בסוג דחיסה שאינו חוקי או שאינו נתמך.</p>
+ <ul>
+ <li>נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">הכתובת לא נמצאה</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>הדפדפן לא הצליח למצוא את השרת המארח לכתובת שסופקה.</p>
+ <ul>
+ <li>יש לבדוק שהכתובת אינה מכילה שגיאות כגון
+ <strong>ww</strong>.example.com במקום
+ <strong>www</strong>.example.com.</li>
+ <li>אם אף דף אינו נטען, יש לוודא שחיבור הנתונים הסלולריים או רשת ה־Wi-Fi תקין.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">אין חיבור לרשת</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">נא לבדוק את החיבור שלך לרשת או לנסות לטעון את הדף מחדש בעוד מספר רגעים.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">טעינה מחדש</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">כתובת לא חוקית</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>הכתובת שסופקה אינה בתבנית מזוהה. נא לבדוק את שורת המיקום עבור שגיאות ולנסות שוב.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">כתובת לא חוקית</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>כתובות אינטרנט לרוב נכתבות בצורה דומה לזו: <strong>http://www.example.com/</strong></li>
+ <li>יש לוודא כי נעשה שימוש בלוכסנים קדמיים (כלומר <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">פרוטוקול לא מוכר</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>הכתובת מציינת פרוטוקול (לדוגמה: <q>wxyz://</q>) שהדפדפן אינו מזהה, ולכן הדפדפן אינו יכול להתחבר לאתר כראוי.</p>
+ <ul>
+ <li>האם הינך מנסה לגשת לשירות מולטימדיה או לשירות אחר שאינו מבוסס טקסט? נא לבדוק אם קיימות לאתר דרישות נוספות.</li>
+ <li>פרוטוקולים מסויימים עשויים לדרוש שימוש ביישומי צד־שלישי או בתוספים חיצוניים לפני שהדפדפן יוכל לזהות אותם.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">קובץ לא נמצא</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>האם ייתכן שהפריט הוסר, הועבר, או שינה שם?</li>
+ <li>האם ישנה שגיאת איות, שגיאת רישיות, או שגיאות טופוגרפיות אחרות בכתובת?</li>
+ <li>האם יש לך הרשאות גישה מספיקות אל הפריט המבוקש?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">הגישה לקובץ נדחתה</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>ייתכן שהקובץ הוסר, הועבר או שההרשאות מונעות את הגישה אליו.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">השרת המתווך דחה את ההתחברות</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>הדפדפן מוגדר להשתמש בשרת מתווך, אולם השרת המתווך דחה את ההתחברות.</p>
+ <ul>
+ <li>האם הגדרות השרת המתווך נכונות? נא לבדוק אותן ולנסות שוב.</li>
+ <li>האם שירות התיווך מאפשר חיבורים מהרשת הזאת?</li>
+ <li>עדיין יש תקלות? כדאי להיוועץ בהנהלת הרשת או בספקית האינטרנט לקבלת סיוע.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">שרת מתווך לא נמצא</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>הדפדפן מוגדר להשתמש בשרת מתווך, אולם השרת המתווך דחה את ההתחברות.</p>
+ <ul>
+ <li>האם הגדרות השרת המתווך נכונות? נא לבדוק אותן ולנסות שוב.</li>
+ <li>האם המכשיר מחובר לרשת פעילה?</li>
+ <li>עדיין יש תקלות? כדאי להיוועץ בהנהלת הרשת או בספקית האינטרנט לקבלת סיוע.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">בעיה עם אתר זדוני</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>האתר בכתובת %1$s דווח כאתר תקיפה ונחסם בהתאם להעדפות האבטחה שלך.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">בעיה עם אתר בלתי רצוי</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>האתר בכתובת %1$s דווח כאתר המגיש תוכנה בלתי רצויה ונחסם בהתאם להעדפות האבטחה שלך.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">בעיה עם אתר מזיק</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>האתר בכתובת %1$s דווח כאתר שככל הנראה מזיק ונחסם בהתאם להעדפות האבטחה שלך.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">בעיה עם אתר מטעה</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>הדף בכתובת %1$s דווח כאתר מטעה ונחסם בהתאם להעדפות האבטחה שלך.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">אתר מאובטח אינו זמין</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[הפעלת את מצב ״HTTPS בלבד״ לטובת אבטחה משופרת, אבל גרסת HTTPS של <em>%1$s</em> אינה זמינה.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">המשך לאתר בגרסת HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..6db6f35bfe
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ja/strings.xml
@@ -0,0 +1,302 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">再試行</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">リクエストを正常に完了できませんでした</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>現在のところこの問題やエラーについての詳細情報はありません。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">安全な接続ができませんでした</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>受信したデータの真正性を検証できなかったため、このページは表示できませんでした。</li>
+ <li>ウェブサイトの所有者に連絡を取り、この問題を報告してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">安全な接続ができませんでした</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>サーバーの設定に問題があるか、誰かが正規のサーバーになりすましている可能性があります。</li>
+ <li>以前は正常に接続できていた場合、この問題は恐らく一時的なものですので、後で再度試してみてください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">詳細設定…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>誰かがサイトになりすまそうとしている可能性があります。この先へ進んではいけません。</label>
+ <br><br>
+ <label>ウェブサイトは証明書によって身元が証明されます。証明書の発行者が不明か、証明書が自己署名されているか、サーバーが正しい中間証明書を送信していないため、%1$s は <b>%2$s</b> を信頼できません。</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">戻る (推奨)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">危険性を承知の上で使用する</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">このウェブサイトには安全な接続が必要です。</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[<ul>
+ <li>このウェブサイトには安全な接続が必要なため、閲覧しようとしているページを表示できません。</li>
+ <li>この問題はウェブサイト側に原因がある可能性が高く、解決するためにあなたにできることはありません。</li>
+ <li>この問題について、ウェブサイトの管理者に知らせてください。</li>
+</ul>
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">詳細情報...</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> には HTTP Strict Transport Security (HSTS) と呼ばれるセキュリティポリシーが設定されており、<b>%2$s</b> は安全な接続でしか通信できません。そのため、このサイトを例外に追加することはできません。</label>
+]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">戻る</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">接続が中断されました</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>サイトに正常に接続できましたが、データの転送中にネットワーク接続が切断されました。再度試してください。</p>
+ <ul>
+ <li>このサイトが一時的に利用できなくなっていたり、サーバーの負荷が高すぎて接続できなくなっている可能性があります。しばらくしてから再度試してください。</li>
+ <li>他のサイトも表示できない場合、端末のデータ接続や Wi-Fi 接続を確認してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">接続がタイムアウトしました</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>接続リクエストに対してリクエスト先サーバーが応答を返さなかったため、接続を中止しました。</p>
+ <ul>
+ <li>サーバーに負荷が集中したり、一時的に停止している可能性があります。しばらく後で再度試してください。</li>
+ <li>他のサイトも表示できない場合、コンピューターのネットワーク接続を確認してください。</li>
+ <li>ファイアウォールやプロキシでネットワークが保護されている場合、その設定に問題があると正常に表示できなくなることがあります。</li>
+ <li>問題が繰り返される場合、ネットワーク管理者またはインターネットプロバイダーに問い合わせてください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">正常に接続できませんでした</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>このサイトが一時的に利用できなくなっていたり、サーバーの負荷が高すぎて接続できなくなっている可能性があります。しばらくしてから再度試してください。</li>
+ <li>他のサイトも表示できない場合、端末のデータ接続や Wi-Fi 接続を確認してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">サーバーの応答が不正です</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>ネットワークリクエストに対するサイトの応答が正しくなかったため、接続を中断しました。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ページの自動転送設定が正しくありません</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>リクエストされたリソースの取得を中止しました。このサイトではリクエストの自動転送がループしています。</p>
+ <ul>
+ <li>このサイトで要求されている Cookie を無効化またはブロックしていないか確認してください。</li>
+ <li>サイトによる Cookie の使用を許可しても解決しない場合、これはご利用のコンピューターではなくサーバーの設定に問題があると思われます。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">オフラインモードです</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>ブラウザーは現在オフラインモードで動作しており、リクエスト対象に接続できません。</p>
+ <ul>
+ <li>コンピューターが有効なネットワークに接続されているか確認してください。</li>
+ <li>“再試行” ボタンを押してブラウザーをオンラインモードに切り替え、ページを再読み込みしてください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">セキュリティ上の理由によりポートの使用が制限されています</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>リクエストされたアドレスのポート (例えば mozilla.jp のポート 80 であれば <q>mozilla.jp:80</q>) は普通ウェブサイトの表示<em>以外の</em>目的で使用されます。ユーザーの保護とセキュリティのため、リクエストは中止されました。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">接続がリセットされました</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>ネットワーク接続の確立中にリンクが切れました。再度試してください</p>
+ <ul>
+ <li>このサイトが一時的に利用できなくなっていたり、サーバーの負荷が高すぎて接続できなくなっている可能性があります。しばらくしてから再度試してください。</li>
+ <li>他のサイトも表示できない場合、端末のデータ接続や Wi-Fi 接続を確認してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">安全でないファイルタイプ</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>ウェブサイトの所有者に連絡を取り、この問題を報告してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">コンテンツデータ破損エラー</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>このページは、データの伝送中にエラーが検出されたため表示できません。</p>
+ <ul>
+ <li>ウェブサイトの所有者に連絡を取り、この問題を報告してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">コンテンツデータのクラッシュ</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>このページは、データの伝送中にエラーが検出されたため表示できません。</p>
+ <ul>
+ <li>ウェブサイトの所有者に連絡を取り、この問題を報告してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">内容符号化 (Content-Encoding) に問題があります</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>不正または不明な形式で圧縮されているため、ページを表示できません。</p>
+ <ul>
+ <li>ウェブサイトの所有者に連絡を取り、この問題を報告してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">アドレスが見つかりません</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>指定されたアドレスのホストサーバーが見つかりませんでした。</p>
+ <ul>
+ <li><strong>www</strong>.example.com を間違えて
+ <strong>ww</strong>.example.com と入力するなど、アドレスを間違って入力していないか確認してください。
+ </li>
+ <li>他のサイトも表示できない場合、端末のデータ接続や Wi-Fi 接続を確認してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">インターネットに接続されていません</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ネットワーク接続を確認するか、しばらくしてからページを再読み込みしてください。</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">再読み込み</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">アドレスが無効です</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>指定されたアドレスの書式が正しくありません。ミスがないかロケーションバーを確認してください。</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">アドレスの書式が正しくありません</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>ウェブのアドレスは通常 <strong>http://www.example.com/</strong></li> のようなものになります。
+ <li>スラッシュ (<strong>/</strong>) </li>が使われているか確認してください。
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">プロトコルが不明です</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>指定されたプロトコル (例えば <q>wxyz://</q>) を認識できないため、サイトに正常に接続できませんでした。</p>
+ <ul>
+ <li>マルチメディアファイルなど非テキストデータにアクセスしようとしている場合、サイトによる特別な動作要件がないか確認してください。</li>
+ <li>一部のプロトコルを使用するにはサードパーティのソフトウェアやプラグインが必要になります。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ファイルが見つかりません</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>ファイルの名前が変更、削除、または移動されている可能性があります。</li>
+ <li>アドレスに入力ミス、大文字/小文字の違い、その他の間違いがないか確認してください。</li>
+ <li>リソースへのアクセス権限があるか確認してください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ファイルへのアクセスが拒否されました</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>ファイルが削除または移動されているかファイルの許可属性によりアクセスが拒否された可能性があります。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">プロキシサーバーが接続を拒否しました</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>プロキシサーバーを使用する設定になっていますが、プロキシサーバーは接続を拒否しました。</p>
+ <ul>
+ <li>ブラウザーのプロキシ設定が正しいか確認してください。</li>
+ <li>このネットワークからプロキシサービスへの接続が許可されているか確認してください。</li>
+ <li>問題が繰り返される場合、ネットワーク管理者またはインターネットプロバイダーに問い合わせてください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">プロキシサーバーが見つかりませんでした</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>プロキシサーバーを使用する設定になっていますが、プロキシサーバーが見つかりませんでした。</p>
+ <ul>
+ <li>ブラウザーのプロキシ設定が正しいか確認してください。</li>
+ <li>コンピューターが有効なネットワークに接続されているか確認してください。</li>
+ <li>問題が繰り返される場合、ネットワーク管理者またはインターネットプロバイダーに問い合わせてください。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">マルウェアサイトの問題</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>%1$s のウェブサイトは攻撃サイトとして報告されており、セキュリティ設定に従いブロックされています。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">望ましくないサイトの問題</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s のウェブサイトは望ましくないソフトウェアを配布しているサイトとして報告されており、セキュリティ設定に従いブロックされています。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">有害なサイトの問題</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s のウェブサイトは有害サイトの可能性があると報告されており、セキュリティ設定に従いブロックされています。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">詐欺サイトの問題</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s のウェブページは詐欺サイトとして報告されており、セキュリティ設定に従いブロックされています。</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">安全なサイトが利用できません</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[セキュリティを強化する HTTPS-Only モードは有効ですが、<em>%1$s</em> の HTTPS バージョンは利用できません。]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP サイトを開く</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..fb267df963
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ka/strings.xml
@@ -0,0 +1,271 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ხელახლა ცდა</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">მოთხოვნის შესრულება ვერ მოხერხდა</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>დამატებითი მონაცემები, ამ ხარვეზის ან შეცდომის შესახებ ამჟამად მიუწვდომელია.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">უსაფრთხო დაკავშირება ვერ მოხერხდა</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>თქვენ მიერ მოთხოვნილი გვერდის ნახვა შეუძლებელია, ვინაიდან მიღებული მონაცემების ნამდვილობა ვერ დასტურდება.</li>
+ <li>გთხოვთ, დაუკავშირდეთ ვებსაიტის მფლობელებს და შეატყობინოთ ეს პრობლემა.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">უსაფრთხო დაკავშირება ვერ მოხერხდა</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>ამას შესაძლოა, საიტის გაუმართაობა ან სხვა საიტად გასაღების მცდელობა იწვევდეს.</li>
+ <li>თუ აღნიშნულ საიტს მანამდე წარმატებით უკავშირდებოდით, ხარვეზი შეიძლება დროებითია და მოგვიანებით შეგიძლიათ, კვლავ სცადოთ.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">დამატებით…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>შესაძლოა, ვიღაც ამ საიტის სხვა საიტად გასაღებას ცდილობდეს და ჯობია, აღარ განაგრძოთ.</label>
+ <br><br>
+ <label>ვებსაიტები საკუთარ ნამდვილობას სერტიფიკატებით ადასტურებს. %1$s არ ენდობა <b>%2$s</b>-ს, რადგან მისი უსაფრთხოების სერტიფიკატის გამომცემი უცნობია, შეიძლება თავადვე აქვთ ხელმოწერილი ან სერვერი სათანადოდ არ აგზავნის შუალედურ სერტიფიკატებს.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">უკან დაბრუნება (სასურველია)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">საფრთხის გაცნობიერება და გაგრძელება</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">ვებსაიტი ითხოვს დაცულ კავშირს.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>გვერდი, რომლის ნახვაც გსურთ, ვერ გამოჩნდება, ვინაიდან საჭიროებს დაცულ კავშირს.</li>
+ <li>ეს ხარვეზი უმეტესად დაკავშირებულია თავად ვებსაიტთან და თქვენ ვერ მოახერხებთ მის გამოსწორებას.</li>
+ <li>მხოლოდ შგიძლიათ აცნობოთ საიტის მფლობელებს ამ ხარვეზის შესახებ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">დამატებით…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> იყენებს უსაფრთხოების დებულებას სახელწოდებით HTTP Strict Transport Security (HSTS), ეს კი ნიშნავს, რომ <b>%2$s</b> მას მხოლოდ უსაფრთხო შეერთებით შეიძლება დაუკავშირდეს. გამონაკლისს ვერ დაამატებთ ამ საიტისთვის. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">უკან დაბრუნება</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">კავშირი გაწყდა</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>დაკავშირება წარმატებული იყო, მაგრამ მოულოდნელად გაწყდა მონაცემთა გადმოტანისას. გთხოვთ, კვლავ სცადოთ.</p>
+<ul>
+ <li>საიტი დროებით მიუწვდომელი ან გადატვირთულია. სცადეთ ხელახლა ცოტა ხანში.</li>
+ <li>თუ სხვა გვერდების გახსნასაც ვერ ახერხებთ, შეამოწმეთ მოწყობილობის ფიჭური ან WiFi-კავშირი.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">კავშირის დრო ამოიწურა</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>საიტი კავშირის მოთხოვნას არ პასუხობს და ბრაუზერმა შეწყვიტა პასუხის ლოდინი.</p>
+ <ul>
+ <li>შესაძლოა სერვერი გადატვირთული ან დროებით გათიშული იყოს? სცადეთ მოგვიანებით.</li>
+ <li>ვერც სხვა საიტებს ხსნით? შეამოწმეთ მოწყობილობის ქსელთან კავშირი.</li>
+ <li>თქვენი მოწყობილობა ან ქსელი ფარით ან პროქსითაა დაცული? გაუმართავი პარამეტრები შესაძლოა აფერხებდეს ინტერნეტკავშირს.</li>
+ <li>კვლავ ხარვეზებია? მაშინ, დახმარებისთვის მიმართეთ თქვენი ქსელის მმართველს ან ინტერნეტმომსახურების მომწოდებელს.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">დაკავშირება ვერ ხერხდება</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>საიტი დროებით მიუწვდომელი ან გადატვირთულია. სცადეთ ხელახლა ცოტა ხანში.</li>
+ <li>თუ სხვა გვერდების გახსნასაც ვერ ახერხებთ, შეამოწმეთ მოწყობილობის ფიჭური ან WiFi-კავშირი.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">გაუთვალისწინებელი პასუხი სერვერიდან</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>საიტის პასუხი მოთხოვნაზე გაურკვეველი სახისაა და ბრაუზერი ვერ ახერხებს მის დამუშავებას.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ეს გვერდი არამართებულად გადამისამართდა</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>ბრაუზერმა შეწყვიტა მოთხოვნილი გვერდის ჩატვირთვის მცდელობა. საიტის გადამისამართება მოთხოვნის დასრულების შესაძლებლობას გამორიცხავს.</p>
+ <ul>
+ <li>ამორთული ან შეზღუდული ხომ არ გაქვთ ფუნთუშები ამ საიტიდან?</li>
+ <li>თუ საიტის ფუნთუშების დაშვებამაც არ გამოასწორა, მაშინ ხარვეზი საიტის გაუმართაობას უკავშირდება და არა თქვენს მოწყობილობას.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">კავშირგარეშე რეჟიმი</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>ბრაუზერი მუშაობს კავშირგარეშედ და მოთხოვნილ მისამართს ვერ დაუკავშირდება.</p>
+ <ul>
+ <li>ჩართულია მოწყობილობა მოქმედ ქსელში?</li>
+ <li>გამოიყენეთ „სცადეთ ხელახლა“ ღილაკი კავშირზე დასაბრუნებლად და გვერდის ხელახლა ჩასატვირთად.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">პორტი შეზღუდულია უსაფრთხოების მიზნით</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>მოთხოვნილ მისამართში მითითებულია პორტი (მაგ., <q>mozilla.org:80</q> ანუ პორტი 80 mozilla.org საიტზე) რომელიც, ჩვეულებრივ <em>სხვა მიზნით</em> გამოიყენება ხოლმე და არა ვებგვერდების მოსანახულებლად. თქვენი უსაფრთხოებისთვის ბრაუზერმა ეს მოთხოვნა გააუქმა.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">კავშირი განულდა</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>ქსელური ბმა გაწყდა კავშირის შეთანხმებისას. გთხოვთ კვლავ სცადოთ.</p>
+ <ul>
+ <li>საიტი დროებით მიუწვდომელი ან გადატვირთულია. სცადეთ ხელახლა ცოტა ხანში.</li>
+ <li>თუ სხვა გვერდების გახსნასაც ვერ ახერხებთ, შეამოწმეთ მოწყობილობის ფიჭური ან WiFi-კავშირი.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">სახიფათო სახის ფაილი</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">დაზიანებული შიგთავსის შეცდომა</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო</p>
+ <ul>
+ <li>გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">შიგთავსი დაზიანდა</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო</p>
+ <ul>
+ <li>გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">შიგთავსის დაშიფვრის შეცდომა</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>გვერდის ჩვენება, რომლის ნახვაც გსურთ, შეუძლებელია, რადგან იყენებს შეკუმშვის გაუგებარ ან არამართებულ საშუალებას.</p>
+ <ul>
+ <li>გთხოვთ, დაუკავშირდეთ ვებსაიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">მისამართი ვერ მოიძებნა</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>მითითებულ მისამართზე, ბრაუზერმა სერვერი ვერ მოიძია.</p>
+ <ul>
+ <li>შეცდომა ხომ არ დაუშვით აკრეფისას, მაგ.
+ <strong>ww</strong>.example.com კი არა, უნდა იყოს
+ <strong>www</strong>.example.com.</li>
+ <li>თუ სხვა გვერდების გახსნასაც ვერ ახერხებთ, შეამოწმეთ მოწყობილობის ფიჭური ან WiFi-კავშირი.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ინტერნეტთან კავშირი არაა</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">შეამოწმეთ ქსელთან თქვენი კავშირი ან სცადეთ გვერდის ხელახლა გახსნა რამდენიმე წუთში.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ხელახლა გახსნა</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">არამართებული მისამართი</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>მითითებული მისამართი უცნობი სახისაა. გთხოვთ, გადაამოწმოთ აკრეფის შეცდომები და სცადოთ ხელახლა.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">მისამართი უმართებულოა</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>ვებ მისამართი ჩვეულებრივ ამგვარად მიეთითება <strong>http://www.example.com/</strong></li>
+ <li>გადაამოწმეთ, რომ წინ გადახრილ ხაზს იყენებთ (მაგ. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">უცნობი ოქმი</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>მისამართში გამოყენებული ოქმი (მაგ., <q>wxyz://</q>) ბრაუზერმა ვერ ამოიცნო და შესაბამისად, საიტს მართებულად ვერ დაუკავშირდა.</p>
+ <ul>
+ <li>მულტიმედიურ, ან სხვა არატექსტური შიგთავსის მქონე საიტთან დაკავშირებას ცდილობთ? გადაამოწმეთ საიტის დამატებითი მოთხოვნები.</li>
+ <li>ზოგი ოქმი, ბრაუზერთან სამუშაოდ, შეიძლება სხვა პროგრამას, ან მოდულებს საჭიროებდეს.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ფაილი ვერ მოიძებნა</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>იქნებ გადარქმეული, წაშლილი ან გადაადგილებულია?</li>
+ <li>ხომ არაა მართლწერის შეცდომა, მთავრული ან სხვა სახის მცდარბეჭდილი მისამართში?</li>
+ <li>გაქვთ შესაბამისი ნებართვა მოთხოვნილ ფაილთან წვდომისთვის?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ფაილთან წვდომა უარყოფილია</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>შესაძლოა წაშლილია, გადატანილია ან ფაილთან წვდომის უფლებები შეზღუდულია.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">პროქსი-სერვერმა კავშირი უარყო</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>ბრაუზერი გამართულია პროქსი-სერვერის გამოსაყენებლად, მაგრამ პროქსი-სერვერმა კავშირი უარყო.</p>
+ <ul>
+ <li>ბრაუზერის პროქსი, სწორადაა გამართული? შეამოწმეთ პარამეტრები და სცადეთ ხელახლა.</li>
+ <li>იძლევა პროქსი-სერვერი ამ ქსელიდან დაკავშირების უფლებას?</li>
+ <li>კვლავ ხარვეზებია? მაშინ, დახმარებისთვის მიმართეთ თქვენი ქსელის მმართველს ან ინტერნეტმომსახურების მომწოდებელს.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">პროქსი-სერვერი ვერ მოიძებნა</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>ბრაუზერი გამართულია პროქსი-სერვერის გამოსაყენებლად, მაგრამ პროქსი-სერვერი ვერ მოიძებნა.</p>
+ <ul>
+ <li>ბრაუზერის პროქსი სწორადაა გამართული? შეამოწმეთ პარამეტრები და სცადეთ ხელახლა.</li>
+ <li>მოწყობილობა მოქმედ ქსელთანაა მიერთებული?</li>
+ <li>კვლავ ხარვეზებია? მაშინ, დახმარებისთვის მიმართეთ თქვენი ქსელის მმართველს ან ინტერნეტმომსახურების მომწოდებელს.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">მავნე საიტი</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>ვებგვერდი %1$s მიჩნეულია მოიერიშე საიტად და შეზღუდულია უსაფრთხოების მიზნით.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">არასასურველი საიტი</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>ვებგვერდი %1$s შემჩნეულია არასასურველი პროგრამების შემოთავაზებაში და შეზღუდულია უსაფრთხოების მიზნით</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">სახიფათო საიტი</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>ვებგვერდი %1$s მიჩნეულია მავნე საიტად და შეზღუდულია უსაფრთხოების მიზნით.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">თაღლითური საიტი</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>ვებგვერდი %1$s მიჩნეულია თაღლითურ საიტად და შეზღუდულია უსაფრთხოების მიზნით.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">უსაფრთხო საიტი მიუწვდომელია</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[თქვენ ჩართული გაქვთ მხოლოდ-HTTPS-რეჟიმი გაუმჯობესებული უსაფრთხოებისთვის, მაგრამ <em>%1$s</em> არაა წარმოდგენილი დაცული HTTPS-ვერსიით.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">გადასვლა HTTP-საიტზე</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..311d0b2043
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,319 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Qayta urınıp kóriń</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Sorawıńızdı qanaatlandırıw múmkinshiligi joq</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Bul mashqala hám qáte haqqında qosımsha maǵlıwmat házirshe joq.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Qáwipsiz qosılıw ámelge aspadı</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Siz kórmekshi bolǵan betti kórsetip bolmaydı, sebebi alınǵan maǵlıwmatlardıń isenimli ekenligin tekseriw ámelge aspadı.</li>
+ <li>Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Qáwipsiz jalǵanıw ámelge aspadı</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Buǵan nadurıs server sazlawları sebep bolıwı múmkin yamasa birew sizge kerek bolǵan serverdi basqasına almastırıwǵa háreket qılmaqta.</li>
+ <li>Eger siz aldın bul serverge tabıslı jalǵanǵan bolsańız, bul qáte waqtınsha bolıwı múmkin hám keyinirek qayta urınıp kóriń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Qosımsha…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Birew sizge kerek bolǵan sayttı basqasına almastırıwǵa háreket qılıp atırǵan bolıwı múmkin, sonlıqtan siz dawam etpegenińiz maqul.</label>
+ <br><br>
+ <label>Veb-saytlar óziniń jeke maǵlıwmatların sertifikat arqalı tastıyıqlaydı. %1$s <b>%2$s</b>ǵa isenbeydi sebebi onıń sertifikat beriwshisi belgisiz, sertifikatqa sayttıń ózi arqalı qol qoyılǵan yamasa onıń serveri aradaǵı durıs sertifikatlardı jibermey atır.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Artqa qaytıw (Usınıs etiledi)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Qáwipti qabıllaw hám dawam etiw</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Bul sayt qáwipsiz jalǵanıwdı talap etedi.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Siz kórmekshi bolǵan betti kórsetip bolmaydı, sebebi bul sayt qáwipsiz jalǵanıwdı talap etedi.</li>
+ <li>Bul mashqala sayttıń ózi menen baylanıslı bolıwı múmkin hám siz bul jaǵdayda heshnárse isley almaysız.</li>
+ <li>Siz bul mashqala haqqında sayttıń administratorın xabarlandırıwıńız múmkin.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Qosımsha…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> HTTP qatań transport qáwipsizligi (HSTS) dep atalǵan qáwipsizlik siyasatına iye, yaǵnıy <b>%2$s</b> oǵan tek ǵana qáwipsiz jalǵanıwı múmkin. Bul saytqa kiriw ushın esaptan tısqarılardı qosa almaysız.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Artqa qaytıw</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Jalǵanıw úzildi</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Brauzer tabıslı jalǵandı, biraq maǵlıwmatlardı jetkeriw waqtında baylanısta úzilis boldı. Ótinish, qayta urınıp kóriń.</p>
+ <ul>
+ <li>Sayt waqtınsha islemey qalıwı yamasa júdá bánt bolıwı múmkin. Bir neshe minuttan soń, qayta urınıp kóriń.</li>
+ <li>Eger siz birde-bir betti júkley almasańız, qurılmańız mobil internetin yamasa Wi-Fi jalǵanıwın tekseriń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Jalǵanıw waqtı tawsıldı</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Sayt jalǵanıw sorawına juwap bermedi hám brauzer kútiwdi toqtattı</p>
+ <ul>
+ <li>Sayttıń serveri waqtınsha islemey qalıwı yamasa júdá bánt bolıwı múmkin. Bir neshe minuttan soń, qayta urınıp kóriń.</li>
+ <li>Eger de siz basqa saytlardı asha almay atırǵan bolsańız, qurılmańızdıń tarmaq penen baylanısın tekseriń.</li>
+ <li>Eger de siziń qurılmańız yamasa jergilikli tarmaǵıńız qáwipsizlik diywalı yamasa isenimli server menen qorǵalǵan bolsa, olardıń sazlawların tekseriń. Sebebi nadurıs sazlawlar saytlardı kóriwge tosqınlıq etiwi múmkin.</li>
+ <li>Mashqala ele saplastırılmadı ma? Járdem ushın tarmaq administratorı yamasa internet provayderińiz benen másláhátlesiń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Jalǵanıw ámelge aspadı</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Sayt waqıtsha islemey qalǵan yamasa júdá bánt bolıwı múmkin. Keyinirek jáne urınıp kórıń.</li>
+ <li>Eger siz birde-bir betti júkley almasańız, qurılmańız mobil internetin yamasa Wi-Fi jalǵanıwın tekseriń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Serverdan kútilmegen juwap</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Sayt tarmaq sorawına kútilgen kóriniste juwap berdi hám brauzer óz jumısın dawam ete almaydı.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Bet durıs jóneltirilmey atır</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Brauzer betti júklew háreketin toqtattı, sebebi sayt sorawdı heshqashan orınlanbaytuǵınday etip jóneltirip atır.</p>
+ <ul>
+ <li>Siz bul sayttıń islewi ushın kerek bolǵan cookiedi óshirip yamasa bloklap qoyǵan bolıwıńız múmkin.</li>
+ <li>Eger de cookiedi qosqannan keyin de mashqala saplastırılmasa, onda mashqala siziń qurılmańızdan emes, al serverdiń sazlawlarınan bolıwı múmkin.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Oflayn rejim</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Brauzer oflayn rejimde islemekte hám soralǵan sayt penen baylanıs ornata almaydı.</p>
+ <ul>
+ <li>Qurılmańız islep turǵan tarmaqqa jalǵanǵanına isenim kámil etiń.</li>
+ <li>Online rejimǵe qaytıw hám betti jańalaw ushın «Qayta urınıw» túymesine basıń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port qáwipsizlik sebepli sheklengen</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Soralǵan mánzil ushın ádette veb-saytlar menen islew ushın <em>qollanılmaytuǵın</em> port kórsetilgen (mısalı: <q>mozilla.org:80</q> - bul mozilla.orgdaǵı 80-port). Qáwipsizligińiz ushın brauzer bul sorawdı biykar etdi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Jalǵanıw qayta ornatıldı</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Sayt penen jalǵanıwdı ornatıw waqtında baylanıs úzildi.
+Ótinish, qayta urınıp kóriń.</p>
+ <ul>
+ <li>Sayt waqtınsha islemey qalıwı yamasa júdá bánt bolıwı múmkin. Bir neshe minuttan soń, qayta urınıp kóriń.</li>
+ <li>Eger siz birde-bir betti júkley almasańız, qurılmańız mobil internetin yamasa Wi-Fi jalǵanıwın tekseriń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Qáwipli fayl túri</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Buzılǵan kontent qátesi</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Siz kórmekshi bolǵan betti kórsetip bolmaydı, sebebi maǵlıwmatlardı jetkerip beriwde qátelik anıqlandı.</p>
+ <ul>
+ <li>Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Kontent buzıldı</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Siz kórmekshi bolǵan betti kórsetip bolmaydı, sebebi maǵlıwmatlardı jetkerip beriwde qátelik anıqlandı.</p>
+ <ul>
+ <li>Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Kontentti sıǵıw qátesi</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Siz kóriwge háreket etip atırǵan betti kórsetip bolmaydı, sebebi ol sıǵıwdıń nadurıs yamasa qollap-quwatlamaytuǵın usılınan paydalanadı.</p>
+ <ul>
+ <li>Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Mánzil tawılmadı</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Brauzer kórsetilgen mánzil boyınsha serverdi taba almadı.</p>
+ <ul>
+ <li>Mánzilde qátelik joqlıǵın tekserıń, Mısalı:
+ <strong>www</strong>.example.com ornına
+ <strong>ww</strong>.example.com.</li>
+ <li>Eger siz birde-bir betti júkley almasańız, qurılmańız mobil internetin yamasa Wi-Fi jalǵanıwın tekseriń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Internet penen baylanıs joq</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Tarmaq jalǵanǵanın tekseriń yamasa bir neshe minutlardan keyin qayta júklewge urınıp kóriń.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Qayta júklew</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Jaramsız mánzil</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Kórsetilgen mánzildıń formatın anıqlaw múmkin bolmay atır. Mánzildıń qátesiz kiritilgenin tekserıń hám qayta urınıp kórıń.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Mánzil jaramsız</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Sayt mánzilleri ádette usınday kóriniste jazıladi <strong>http://www.example.com/</strong></li>
+ <li>Qıya sızıqtan (/) paydalanıp atırǵanıńızǵa isenim kámil etıń (Mısalı: <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Belgisiz Protokol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Mánzil (<q>wxyz://</q> uqsaǵan) belgisiz protokoldı óz ishine aladı, sonıń ushın brauzer sayt penen durıs baylanıs ornata almaydı.</p>
+ <ul>
+ <li>Eger de siz multimedia yamasa basqa da tekst emes xızmetlerdi ashıwǵa urınıp atırǵan bolsańız, sayttıń ayırım talapların tekseriń.</li>
+ <li>Brauzer anıqlawı ushın ayırım protokollar sırtqı baǵdarlamalardı yamasa plaginlerdi ornatıwdı talap etiwi múmkin.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fayl tawılmadı</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Fayl ózgertilgen, óshirilgen yamasa kóshirilgen bolıwı múmkin.</li>
+ <li>Mánzildi kiritgende qátelik ketpegenin tekseriń.</li>
+ <li>Soralǵan fayldı kóriwge jeterli dárejede huqıqqa iyeligińizge isenim kámil etiń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Faylǵa kiriw biykar etilgen</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Fayl óshirilgen, kóshirilgen yamasa faylǵa ruqsatlar kóriwge tosqınlıq etip atırǵan bolıwı múmkin.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proksi server qosılıwdı biykarladı</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Brauzer isenimli serverden paydalanıwǵa sazlanǵan, biraq isenimli server baylanıstı biykarladı.</p>
+ <ul>
+ <li>Brauzerdiń isenimli serveri sazlawların tekseriń hám qayta urınıp kóriń.</li>
+ <li>Isenimli server bul tarmaqtan baylanısıwǵa ruqsat bere me?</li>
+ <li>Mashqala ele saplastırılmadı ma? Járdem ushın tarmaq administratorı yamasa internet provayderińiz benen másláhátlesiń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proksi server tawılmadı</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Brauzer isenimli serverden paydalanıwǵa sazlanǵan, biraq isenimli server tabılmadı.</p>
+ <ul>
+ <li>Brauzerdiń isenimli serveri sazlawların tekseriń hám qayta urınıp kóriń.</li>
+ <li>Qurılmańız islep turǵan tarmaqqa jalǵanǵanına isenim kámil etiń.</li>
+ <li>Mashqala ele saplastırılmadı ma? Járdem ushın tarmaq administratorı yamasa internet provayderińiz benen másláhátlesiń.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Zıyanlı sayt mashqalası</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s saytı paydalanıwshılardıń qurılmalarına hújim islew ushın qollanılatuǵınlıǵı haqqında maǵlıwmat bar hám siziń qawipsizlik sazlawlarıńız tiykarında bloklanǵan.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Kútilmegen sayt mashqalası</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s saytı unamsız baǵdarlamalardı tarqatıw ushın paydalanılǵanı haqqında maǵlıwmat bar hám siziń qáwipsizlik sazlawlarıńız tiykarında bloklanǵan.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Zıyanlı sayt mashqalası</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s saytı itimallı zıyanlı ekenligi haqqında maǵlıwmat bar hám siziń qawipsizlik sazlawlarıńız tiykarında bloklanǵan.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Aldamshı sayt mashqalası</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$sdaǵı bul veb bet jalǵan ekenligi haqqında maǵlıwmat bar hám siziń qáwipsizlik sazlawlarıńız tiykarında bloklanǵan.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Qáwipsiz sayt joq</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Siz qáwipsizlikti kúsheytiw ushın tek HTTPS rejimin qostıńız, biraq ta <em>%1$s</em> saytında HTTPS versiyası qollap-quwatlanbaydı.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP saytında dawam etiw</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..f875b47c8d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kab/strings.xml
@@ -0,0 +1,322 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Ɛreḍ tikkelt-nniḍen</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Tuttra ur tezmir ad temmed</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Ulac talɣut-nniḍen ɣef wugur-a neɣ tuccḍa-a ulac-itt akka tura.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Tuqqna taɣelsant ur teddi ara</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Asebter-agi i tebɣiḍ ad twaliḍ ur yizmir ara ad d-yettwasken, acku ur nemzir ara ad nessenqed tilawt n yisefka d-ittwaremsen</li>
+ <li>Ma ulac aɣilif nermes imawlan n usmel web ɣef wugur-agi.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Tuqqna taɣelsant ur teddi ara</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Ayagi yezmer ad yili d ugur n twila n uqeddac neɣ d albaɛḍ i yeɛarḍen ad yakwer tamagit n uqeddac-agi.</li>
+ <li>Ma yella teqqneḍ yakan ɣer uqeddac-agi, tuccḍa tezmer ahat ad tili d taskudant, u tzemreḍ ad tɛerḍeḍ i tikelt nniḍen ticki.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Talqayt…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Illa ahat amdan i yebɣan ad yakker tamagit n usmel, ur ilaq ara ad tkemleḍ.</label>
+<br><br>
+<label>Ismal ttaken-d ttbut ɣef tmagit-nsen s lmendad n tisirag. %1$s ur yumin ara <b>%2$s</b> acku tasiragt d tarussint, tasiragt tesêa azmul-is, neɣ aqeddac ur d-yuzin ara tisragt timeɣta.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Uɣal ɣer deffir (Yelha)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Qbel ugur u kemmel</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Asmel-a web yesra tuqqna taɣelsant.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Asebter i tettaɛraḍeḍ ad t-twaliḍ ur yezmir ara ad d-yettwaskan acku asmel-a yesra tuqqna taɣelsant.</li>
+ <li>Ugur ad yili akked usmel-a web, ur yelli wayen ara t-tgeḍ i wakken ad yefru.</li>
+ <li>You can notify the website’s administrator about the problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Talqayt…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ɣur-s tasertit n tɣellist HTTP Strict Transport Security (HSTS), ay-agi yemmal-d d akken <b>%2$s</b> izmer kan ad iqqen ɣur-s s tɣellist. Ur tezmireḍ ara ad ternuḍ tasureft akken ad twaliḍ asmel-agi.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Uɣal ɣer deffir</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Tuqqna teḥbes</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Iminig yeqqen akken iwata, maca tuqqna teḥbes deg usiweḍ n telɣut.Ma ulac aɣilif ɛreḍ tikkelt-nniḍen.</p>
+ <ul>
+ <li>Asmel yezmer yeḥbes kra n wakud neɣ ahat iɛebba taɛekkemt. Ɛreḍ tikkelt-nniden ticki.</li>
+ <li> Ma yella ur tezmireḍ ara ad tinigeḍ deg yismal meṛṛa, sefqed tuqqna-ik n yisefka neɣ Wi-Fi n yibenk-ik.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Tanzagt n uraǧu tezri</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Asmel i tessutreḍ ur d-yerri ara tiririt i usuter n tuqqna, ihi iminig iseḥbes araǧu n tririt.</p>
+ <ul>
+ <li>Ahat aqeddac iɛebba ayen ur yezmir neɣ yeḥbes kra n wakud?Ɛreḍ tikkelt-nniḍen ticki.</li>
+ <li>Ur tezmireḍ ara ad tinigeḍ deg yismal-nniḍen? Sefqed tuqqna n uẓeṭṭa n uselkim-ik.</li>
+ <li>Aselkim-ik yettummesten s uɣrab n tmes neɣ apṛuksi? Yir iɣewwaṛen zemren ad sḥebsen tunigin deg web.</li>
+ <li>Mazal uguren? Suter anedbal-ik n uẓeṭṭa neɣ asaǧǧaw-ik nternet ɣef wugar n tallelt.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Igguma ad iqqen</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Ahat asmel ulac-it akka tura neγ iɛebba kra. Ɛreḍ tikkelt-nniḍen ticki.</li>
+ <li>Ma yella ur tezmireḍ ara ad tinigeḍ ɣer usmel, senqed tuqqna-ik n yisefka neɣ Wi-Fi n yibenk-ik.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Aqeddac yerra-d yir tiririt ur nettwaṛǧu ara</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Asmel yerra-d i tririt n uẓeṭṭa s tarrayt n nettwarǧi ara, ihi iminig ur yezmir ara ad ikemmel.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Asebter ur yettuwelleh ara akken iwata</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Iminig-ik yeḥbes araǧu n tririt n usmel. Asmel iwehha tuttra s tarrayt ara tt-yeǧǧen ur tettaweḍ ara.</p>
+ <ul>
+ <li>Ahat tsenseḍ neɣ tesweḥleḍ inagan n tuqqna i ilaqen deg usmel-agi?</li>
+ <li> Ma yella ugur ur yefri ara ticki tremdeḍ inagan n tuqqna, ahat ugur ad yili deg twila n uqeddac mačči deg uselkim-ik.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Askar war tuqqna</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Iminig iteddu deg uskar war tuqqna ihi ur yezmir ara ad yeqqen ɣer tansa i d-yettwammlen.</p>
+ <ul>
+ <li>Aselkim-ik iqqen ɣer uẓeṭṭa urmid?</li>
+ <li>Sit ɣef "Ɛreḍ tikkelt-nniḍen" akken ad tuɣaleḍ ɣer uskar usrid sakin sali-d asebter-inek tikkelt-nniḍen.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Tabburt tezzem ɣef sebba n tɣellist</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Tansa d-ittusutren temmal-d tabburt (amedya, <q> mozilla.org:80</q> i tebburt 80 ɣef mozilla.org) i yettuseqdacen deg<em>umnaḍ</em>-nniḍen war tunigin deg web. Iminig isefsex tuttra n ummesten-ik d tɣellist-ik.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Tuqqna tettuwennez</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Asiweḍ n telɣut ɣer uẓeṭṭa yeḥbes deg uɛraḍ n tuqqna. Ma ulac aɣilif ɛreḍ tikkelt-nniḍen.</p>
+ <ul>
+ <li>Asmel yezmer yeḥbes kra n wakud neɣ ahat iɛebba taɛekkemt. Ɛreḍ tikkelt-nniden ticki.</li>
+ <li> Ma yella ur tezmireḍ ara ad tinigeḍ deg yismal meṛṛa, sefqed tuqqna-ik n yisefka neɣ Wi-Fi n yibenk-ik.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tawsit n ufaylu mačči d taɣelsant</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Ma ulac aɣilif, nermes imawlan n usmel web ɣef wugur-agi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Tuccḍa d yekkan seg ugbur yerẓen</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Asebter-agi i tebɣiḍ ad twaliḍ ur yizmir ara ad ittwasken, acku tella tuccḍa di tuzzna n isefka.</p>
+ <ul>
+ <li>Ma ulac aɣilif nermes imawlan n usmel web ɣef ugur-agi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Yeɣli ugbur</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Asebter-agi i tebɣiḍ ad twaliḍ ur yizmir ara ad ittwasken, acku tella tuccḍa di tuzzna n isefka.</p>
+ <ul>
+ <li>Ma ulac aɣilif nermes imawlan n usmel web ɣef ugur-agi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Tuccḍa n usettengel n ugbur</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Asebter-agi i tebɣiḍ ad twaliḍ ur yizmir ara ad ittwasken, acku iseqdac tawsit usekkussem tarameɣtut neɣ ur yettusefraken ara.</p>
+ <ul>
+ <li>Ma ulac aɣilif nermes imawlan n usmel web ɣef ugur-agi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Ulac tansa</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Iminig ur izmir ara ad d-yaf aqeddac asenneftaɣ i tansa i d-ttunefken.</p>
+ <ul>
+ <li>Senqed tansa ɣef tuccḍiwin n tira am
+ <strong>ww</strong>.amedya.com deg umḍiq n
+ <strong>www</strong>.amedya.com.</li>
+ <li>Ma tzemreḍ ad tessaliḍ yal asebter, wali isefka n yibenk-ik neɣ tuqqna Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ulac tuqqna Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Senqed tuqqna n uẓeṭṭa-ik neɣ ɛreḍ asali n usebter deg kra n wakud.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Smiren</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Tansa tarameɣtut</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Tansa i d-tmuddeḍ ur tella ara deg umasal ittwassnen. Ma ulac aɣilif senqed afeggag n tansa akken ad tesseɣtiḍ tuccḍiwin sakin ɛreḍ tikkelt-nniḍen.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Tansa-yagi d tarameɣtut</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Tansiwin web ttwarunt am <strong>http://www.example.com/</strong></li>
+ <li>Wali ma yella tseqdaceḍ amgal islacen (i.e. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Aneggaf arussin</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Tansa temmaled aneggaf (amedya <q>wxyz://</q>) arussin n iminig, ihi iminig ur yezmir ara ad iqqen ɣer usmel.</p>
+ <ul>
+ <li>Tettaɛraḍeḍ ad tkecmeḍ ɣer tanfiwin n wallalen n teywalt neɣ araḍris? Senqed asmel ma yesra ayen nniḍen.</li>
+ <li>Kra n ineggafen sran aseɣẓan neɣ azegrir wis kraḍ send ad ten-yissin iminig.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Ulac afaylu</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Ahat afaylu ittusnifel, ittwakkes neɣ ittusenkez?</li>
+ <li>Llant tuccḍiwin n teɣdira, isekkilen imeqqṛanen, neɣ tuccḍiwin nniḍen?</li>
+ <li>Ɣur-k tasiregt ara iqadden ɣef ufaylu agi?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Anekcum ɣer ufaylu yegdel</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Ahat yettwakkes, yettusenkez, neɣ tisirag uggint anekcum.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Tuqqna yugi-tt uqeddac apṛuksi</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Iminig-inek ittuswel akken ad iseqdec aqeddac apṛuksi, acu kan apṛuksi yuggi tuqqna.</p>
+ <ul>
+ <li>Tawila n uminig inek d tameɣtut? Senqed iɣewwaṛen u ɛreḍ tikelt nniḍen.</li>
+ <li>Aseɣẓan agrawan isirig tuqqniwin seg uẓeṭṭa-yagi?</li>
+ <li>Zgan ɣur-k wuguren? Nermes anedbal-ik n unagraw neɣ aseǧǧaw-ik n Internet i tallalt.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Ulac aqeddac apṛuksi</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Iminig-inek ittuswel akken ad iseqdec aqeddac apṛuksi, acukan apṛksi ulac-it.</p>
+ <ul>
+ <li>Tawila n uminig-inek d tameɣtut? Senqed iɣewwaṛen sakin ɛreḍ tikkelt-nniḍen.</li>
+ <li>Aselkim-inek iqqen ɣer uẓeṭṭa urmid?</li>
+ <li>Zgan ɣur-k wuguren? Nermes anedbal-ik n unagraw neɣ aseǧǧaw-ik n Internet i tallelt.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Ugur n yir asmel</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+
+<p>Asmel web illan deg tansa %1$s ittwammel d akken d asmel n uẓḍam. Ihi ittusewḥel akken llan ismenyifen-inek n tɣellist.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Ugur n usmel ur nerǧi ara</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Asmel web illan deg tansa %1$s ittwammel d akken d asmel n uẓḍam. Ihi ittusewḥel akken llan ismenyifen-ik n tɣellist.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Ugur n usmel aqesḥan</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Asmel web %1$s ittwammel d akken d asmel diri-t i teγlist. Ihi ittusewḥel akken llan ismenyifen-ik n tɣellist.</p>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Ugur n usmel n ukellex</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Asmel web illan deg tansa %1$s ittwammel d akken d asmel n ukellex. Ihi ittusewḥel akken llan ismenyifen-inek n tɣellist.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Ulac taɣellist n usmel</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Tremdeḍ askar HTTPS-Only i tɣellist ifazen, yerna ulac lqem n HTTPS n <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Kemmel ɣer usmel HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..12506be43c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kk/strings.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Қайталап көру</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Сұранымды аяқтау мүмкін емес</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Бұл қате жөнінде қосымша ақпарат қазір қолжетерсіз.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Қорғалған байланысты орнату сәтсіз аяқталды</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Сіз сұраған парақ көрсетілмейді, өйткені алынған мәліметтер шынайылығын тексеру мүмкін емес.</li>
+ <li>Сайт иесіне осы мәселе жөнінде хабарлаңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Қорғалған байланысты орнату сәтсіз аяқталды</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Бұл сервердің қатесі болуы мүмкін, немесе біреу сізге керек серверді басқасымен ауыстырғысы келеді.</li>
+ <li>Осыған дейін осы серверге сәтті қосылған болсаңыз, осы қате уақытша болуы мүмкін. Біраздан кейін қайталап көріңіз.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Кеңейтілген…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Біреу бұл сайтты еліктеу талабын жасап жатқаны мүмкін, сондықтан жалғастырмауыңыз керек.</label>
+ <br><br>
+ <label>Веб-сайттар өз шынайылығын сертификаттар арқылы көрсетеді. %1$s қолданбасы <b>%2$s</b> адресіне сенбейді, өйткені сертификат шығарушысы белгісіз, сертификат қолтаңбасы өздігінен қойылған, немесе сервер аралық сертификаттарды жібермеген.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Артқа оралу (ұсынылады)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Тәуекелді қабылдап, жалғастыру</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Бұл веб-сайт қауіпсіз байланысты талап етеді.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Сіз көргіңіз келетін бетті көрсету мүмкін емес, себебі бұл веб-сайт қауіпсіз байланысты талап етеді.</li>
+ <li>Мәселе веб-сайтта болуы мүмкін және оны шешу үшін ештеңе істей алмайсыз.</li>
+ <li>Веб-сайттың әкімшісіне мәселе туралы хабарлауға болады.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Қосымша…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> сайтының HTTP Strict Transport Security (HSTS) деп аталатын қауіпсіздік саясаты бар, бұл дегеніміз, <b>%2$s</b> оған тек қауіпсіз түрде байланыса алады. Бұл веб-сайт үшін ережеден тыс жағдайды қоса алмайсыз.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Артқа</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Байланыс үзілген</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Браузер сәтті байланысқан, бірақ, байланыс ақпаратты тасымалдау кезінде үзілген. Қайталап көріңіз.</p>
+ <ul>
+ <li>Сайт уақытша қолжетімсіз немесе сұранымдарға толы шығар. Біраздан кейін қайталап көріңіз.</li>
+ <li>Егер бірде-бір парақ жүктелмесе, құрылғының деректер немесе Wi-Fi байланысын тексеріңіз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Байланысты күту уақыты аяқталды</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Көрсетілген сайтпен байланыс орнату сұранымына жауап бермеді және браузердің күту уақыты бітті.</p>
+ <ul>
+ <li>Сайт сервері тым жүктелген немесе уақытша жұмыстан тыс болуы мүмкін бе? Біраз уақыт күтіп, қайталап көріңіз.</li>
+ <li>Егер сіз басқа да сайттарды аша алмасаңыз, компьютеріңіздің желімен байланысын тексеріңіз.</li>
+ <li>Егер құрылғыңыз желіаралық экран немесе прокси-сервермен қорғалса, олардың баптауларын тексеріңіз.</li>
+ <li>Егер басқа да мәселелер пайда болса, жүйелік администраторыңызға не Интернет-провайдеріңізге хабарласыңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Байланысты орнату мүмкін емес</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Сайт уақытша қолжетімсіз, немесе сұранымдарға толы шығар. Кейінірек қайталап көріңіз.</li>
+ <li>Бірде-бір сайт ашылмаса, мобильді құрылғыңыздың деректер не Wi-Fi байланысын тексеріңіз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Сервердің жауабы күтпеген түрде</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Сайт сұранымға күтпеген түрде жауап берді, браузер өз жұмысын жалғастыра алмайды.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Парақтағы қайта бағдарлау дұрыс емес</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Браузер парақтың жүктелуін тоқтатты, өйткені сайт сұранымды ешқашан аяқталмайтындықтай бағдарлайтыны анықталды.</p>
+ <ul>
+ <li>Бұл сайт сұраған cookies өшірген немесе бұғаттаған жоқсыз ба?</li>
+ <li>Егер сайттан cookies қабылдау мүмкіндігін қосу арқылы мәселе шешілмесе, онда бұл қате серверден кеткен шығар.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Дербес жұмыс режимі</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Браузер қазір дербес жұмыс істеу режимінде, сол үшін сұранған нәрсеге байланыс орната алмайды.</p>
+ <ul>
+ <li>Бұл құрылғы белсенді желіге қосулы тұр ма?</li>
+ <li>Онлайн режиміне ауысып, парақты жаңарту үшін, "Қайталап көру" басыңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Порт қауіпсіздік мақсатында шектелген</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Сұранған байланыс үшін көрсетілген порт (мысалы: <q>mozilla.org:80</q> бұл mozilla.org сайтындағы 80 порт) әдетте веб-сайттармен байланысу үшін <em>қолданылмайды</em>. Қауіпсіздік мақсатында браузер бұл байланысты үзді.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Байланыс үзілген</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Байланысты орнату кезінде желілік байланыс үзілді. Қайталап көріңіз.</p>
+ <ul>
+ <li>Сайт уақытша қолжетімсіз немесе сұранымдарға толы шығар. Біраздан кейін қайталап көріңіз.</li>
+ <li>Егер бірде-бір парақ жүктелмесе, құрылғының деректер немесе Wi-Fi байланысын тексеріңіз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Қауіпсіз емес файл түрі</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Веб сайт иелеріне осы мәселе жөнінде хабарлаңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Зақымдалған құрама қатесі</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Сіз қараймын деген парақ көрсетілмейді, өйткені мәліметтер тасымалданған кезде қате анықталды.</p>
+ <ul>
+ <li>Веб сайт иелеріне осы мәселе жөнінде хабарлаңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Құрамасы бұзылды</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Сіз қараймын деген парақ көрсетілмейді, өйткені мәліметтер тасымалданған кезде қате анықталды.</p>
+ <ul>
+ <li>Веб сайт иелеріне осы мәселе жөнінде хабарлаңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Құраманы декодтау кезінде қате кетті</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Сіз сұраған бет көрсетілмейді, өйткені оның сығуы қате немесе браузер оны қолдамайды.</p>
+ <ul>
+ <li>Веб-сайттың иесімен осы мәселе жайында хабарласыңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Адрес табылмады</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Браузер көрсетілген адрес үшін хост атын таба алмады.</p>
+ <ul>
+ <li>Адресті теру қателеріне тексеріңіз, мысалы
+ <strong>www</strong>.example.com орнына
+ <strong>ww</strong>.example.com.</li>
+ <li>Бірде-бір парақ жүктелмесе, құрылғының деректер немесе Wi-Fi байланысын тексеріңіз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Интернетпен байланыс жоқ</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Желілік байланысты тексеріңіз немесе біраздан кейін бетті қайта жүктеп көріңіз.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Қайта жүктеу</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Адрес пішімі қате</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Енгізілген адрестің пішімі қате. Оның енгізілуін тексеріп, қайта көріңіз.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Адрес пішімі қате</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Веб адрестері әдетте келесідей жазылады - <strong>http://www.example.com/</strong></li>
+ <li>Қалыпты слэштер қолданатыңызға көз жеткізіңіз (мыс. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Белгісіз хаттама</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Көрсетілген адрес браузерге белгсіз хаттамадан (<q>wxyz://</q> сияқты) басталады, сондықтан браузер сайтпен байланыс орната алмайды.</p>
+ <ul>
+ <li>Егер сіз мультимедиа немесе басқа да мәтіндік емес сервистері бар сайтпен байланыс орнатсаңыз, сайттың бағдарламалық қамтамаға қойылатын талаптарын тексеріңіз.</li>
+ <li>Браузер кейбір хаттамаларды қолдана алу үшін сыртқы бағдарламалық қамтаманы немесе плагиндерді орнату керек.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Файл табылмады</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Ол нәрсенің аты ауысқан, ол өшірілген не орны ауысқан болуы мүмкін бе?</li>
+ <li>Адрес ішінде айтылу не жазылу қателері не басқа қателер жоқ па?</li>
+ <li>Сұранған нәрсеге сізде керек рұқсаттар бар ма?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Файлға қатынау құқығы жоқ</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Ол өшірілген, жылжытылған немесе файл рұқсаттары қатынауға жол бермеуі мүмкін.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Прокси-сервер сұранымды үзген</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Браузер прокси-сервер қолдануға бапталған, бірақ прокси-сервер байланыс орнатуға рұқсат бермейді.</p>
+ <ul>
+ <li>Браузердегі прокси-сервер баптаулары дұрыс па? Оларды тексеріп, қайталап көріңіз.</li>
+ <li>Прокси-сервер осы желіден байланыс орнатуға рұқсат бере ме?</li>
+ <li>Әлі де мәселелер бар ма? Жүйелік администраторыңызбен, не Интернет-провайдеріңізбен хабарласыңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Прокси-сервер табылмады</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Браузер прокси-серверді қолдануға бапталған, бірақ прокси-серверді табу мүмкін емес.</p>
+ <ul>
+ <li>Браузердің прокси-сервер баптаулары дұрыс па? Оларды тексеріп, қайталап көріңіз.</li>
+ <li>Құрылғы белсенді желіге байланысып тұр ма?</li>
+ <li>Әлі де мәселелер бар ма? Жүйелік администраторыңызбен, не Интернет-провайдеріңізбен хабарласыңыз.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Зиянкес бағдарлама сайты</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s сайты пайдаланушыларға шабуыл жасау үшін қолданылытыны туралы ақпарат бар, сондықтан қауіпсіздік баптауларыңызға сәйкес ол бұғатталды.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Зиянкес бағдарлама сайты</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s сайты ұнамсыз бағдарламалық қамтаманы тарататыны туралы ақпарат бар, сондықтан қауіпсіздік баптауларыңызға сәйкес ол бұғатталды.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Зиянкес сайт мәселесі</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s сайты зиянкес болу мүмкіншілігі туралы ақпарат бар, сондықтан қауіпсіздік баптауларыңызға сәйкес ол бұғатталды..</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Жалған сайт мәселесі</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$s сайты жалған сайт екені туралы ақпарат бар, сондықтан қауіпсіздік баптауларыңызға сәйкес ол бұғатталды.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Қауіпсіз сайт қолжетімді емес</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Сіз жақсартылған қауіпсіздік үшін тек-HTTPS режимін іске қосқансыз, бірақ, <em>%1$s</em> адресінің HTTPS нұсқасы қолжетімсіз.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP сайтына өту</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..4a32b60456
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Dîsa biceribîne</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Daxwaz nehate temamkirin</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Derbarê vê çewtiyê an jî pirsgirêkê de agahiya ekstra niha ne mewcûd e.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Girêdana ewle pêk nehat</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Rûpela ku tu hewl didî wê bibînî, nayê nîşandan ji ber ku rastbûna daneyên ku hatine stendin nayê piştrastkirin.</li>
+ <li>Ji kerema xwe bi xwediyên malperê re têkeve têkiliyê û hayê wan ji vê pirsgirêkê çêke.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Girêdana ewle pêk nehat</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Dibe ku ev problemeke têkildarî eyarên pêşkêşkarê be yan jî tiştekî din be ku dike pêşkêşkarê teqlîd bike.</li>
+ <li>Heke tu berê bi vê pêşkêşkarê re bi serkeftî hatibî girêdan, dibe ku çewtî demkî be û tu dikarî paşê dîsa biceribînî.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Pêşketî…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Dibe ku hinek pêşkêşkarê teqlîd dikin û divê tu dewam nekî. </label>
+ <br> <br>
+ <label>Malper nasnameyên xwe bi sertîfîkayan îsbat dikin. Ji ber ku belavkara wê ya sertîfîkayê nayê nasîn, %1$s baweriyê bi <b>%2$s</b>’ê nayîne. Yan sertîfîka bi xwe hatiye îmzekirin yan jî pêşkêşkar sertîfîkayên navberê yên rast naşîne. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Vegere (tê pêşniyarkirin)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Rîskê qebûl bike û bidomîne</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Pêdiviya vê malperê bi peywendiyeke ewle heye.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Malpera tu hewlê didî ku vekî nayê nîşandan, lewre pêdiviya wê bi peywendiyeke ewle heye..</li>
+ <li>Ev bi piranî têkildarî malperê ye û ji bo çareserkirinê tiştek ji destê te nayê.</li>
+ <li>Tu dikarî rêveberiya malperê derbarê problemê de agahdar bikî.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Pêşketî…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> rêgezeke ewlekariyê ya bi navê Ewlekariya Jidandî ya Guheztinê ya HTTPê (HSTS) heye û loma <b>%2$s</b> dikare tenê di rewşeke ewle de bi malperê re têkiliyê dayne. Tu nikarî zêdekariyan bidiyê û têkeviyê.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Vegere</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Girêdan qut bû</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Gerokê bi serfirazî girêdan çêkir, lê di şandina agahiyan de girêdan qut bû. Tika ye careke din biceribîne.</p>
+ <ul>
+ <li>Dibe ku malper demekê ne berdest be yan mijûl be. Piştî çend xulekan dîsa biceribîne. </li>
+ <li>Heke tu nikarî ti rûpelan bar bikî, girêdana daneyî û Wi-Fi’ya cîhaza xwe kontrol bike.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Girêdan derdem bû</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Malpera ku hat xwestin bersiva daxwaza pêwendiye neda û gerokê jî li bendemayîna ji bo bersivê rawestand. </p>
+ <ul>
+ <li>Gelo dibe ku ji pêşkêşkarê re gelek daxwaz hene yan jî qutbûneke demkî hebe. </li>
+ <li>Gelo tu nikarî malperên din jî vekî? Girêdana înternetê ya cîhazê xwe kontrol bike.</li>
+ <li>Cîhaz an jî tora te ji hêla dîwarê ewlehiyê an proxyê ve tê parastin? Eyarên çewt dikarin lêgerîna webê asteng bikin.</li>
+ <li>Hê jî pirsgirêk heye? Ji bo alîkariyê bi rêveberê xwe yê torê an jî peydakera înternetê re bişêwirin. </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Nehate girêdan</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Dibe ku malper bi awayekî demkî ne berdest be yan jî pir mijûl be. Piştî çend xulekan dîsa biceribîne</li>
+ <li>Heke tu nikarî rûpelên din jî bar bikî, girêdana daneyî yan jî Wi-Fi’ya cîhaza xwe kontrol bike.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Bersiva nebende ji serverê</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p> Malperê bi awayekî nebende bersiva daxwaza torê da û gerok nikare berdewam bike. </p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Rûpel bi awayekî rast nayê berhêlkirin</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Gerokê dest ji hewla vegerandina hêmana daxwazkirî berda. Malper daxwazê bi rengekî ku ew ê ti carî temam nebe, dibersivîne</p>
+ <ul>
+ <li>Te çerezên ku ji hêla vê malperê ve hatine xwestin neçalak an jî asteng kirine? </li>
+ <li>Heke qebûlkirina çerezên malperê pirsgirêkê çareser neke, nexwe pirsgirêk ji sazkariyên pêşkêşkarê ye û ne ji cîhaza te ye.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Moda derhêl</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Gerok di moda xwe ya negirêdayî de dixebite û nikare xwe bigihîne hêmana daxwazkirî.</p>
+ <ul>
+ <li>Gelo amûr bi toreke çalak ve girêdayî ye?</li>
+ <li>Li ser "Dîsa Biceribîne"yê bitikîne ku derbasî moda girêdayî bibî û rûpelê bar bikî.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port ji ber sedemên ewlehiyê hatiye sînordarkirin</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Navnîşana hatî xwestin porteke diyarkirî ye (mînak, <q>mozilla.org:80</q> ji bo potra 80 li ser mozilla.orgê) ku di rewşa asayî de ji bilî gera webê ji bo armancên <em>din</em> tê bikaranîn. Gerokê daxwaz ji bo parastin û ewlekariya te betal kir..</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Girêdan hate resetkirin</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Girêdana torê di dema bazariya girêdanê de qut bû. Tika ye dîsa biceribîne.</p>
+ <ul>
+ <li>Dibe ku malper demkî nayê bikaranîn yan jî gelekî mijêl e. Piştî çend xulekan dîsa biceribîne.</li>
+ <li>Heke ti malperek venabe, girêdana înterneta cîhaza xwe yan jî girêdana Wi-Fiyê kontrol bike.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Cureya dosyeyê ya neewle</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Tika ye bi xwediyên malperê re peywendiyê dayne ku wan derbarê vê problemê de agahdar bikî.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Çewtiya Naveroka Xerabe</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Ji ber ku di guhestina daneyan de çewtiyek hatiye tespîtkirin, rûpela dixwazî vekî nayê nîşandan.</p>
+ <ul>
+ <li>Tika ye bi xwediyên malperê re peywendiyê çêke û wan agahdar bike.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Naverok têk çû</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Ji ber ku di guhestina daneyan de çewtiyek hatiye tespîtkirin, rûpela dixwazî vekî nayê nîşandan.</p>
+ <ul>
+ <li>Tika ye bi xwediyên malperê re peywendiyê çêke û wan agahdar bike.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Çewtiya kodkirina naverokê</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Ji ber ku rûpela dikî vekî şêweyeke jidandinê ya nederbasdar yan jî nayê destekkirin bi kar tîne, rûpela dixwazî vekî nayê nîşandan.</p>
+ <ul>
+ <li>Tika ye bi xwediyên malperê re peywendiyê çêke û wan agahdar bike.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Navnîşan nehate dîtin</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Gerokê nekarî ji bo navnîşana hatî xwestin pêşkêşkata host peyda bike.</p>
+ <ul>
+ <li>Navnîşanê ji bo şaşiyên nivîsandinê kontrol bike: wekî
+ <strong>ww</strong>.example.com ji dêvla
+ <strong>www</strong>.example.com.</li>
+ <li>Heke tu nkarî ti rûpelan bar bikî, înterneta amûra xwe yan jî girêdana Wi-Fiyê kontrol bike.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Girêdana înternetê tune</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Girêdana înterneta xwe kontrol bike an jî bîstek din rûpelê ji nû ve bar bike.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Ji nû ve bar bike</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Navnîşana nederbasdar</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Navnîşana hatî nivîsandin ne di formata naskirî de ye. Tika ye darikê cîgehê ji bo şaşiyan kontrol bike û dîsa biceribîne.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Navnîşan ne derbasdar e</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Malperên înternetê bi piranî wisa tên nivîsandin: <strong>http://www.example.com/</strong></li>
+ <li>Bala xwe bidê ka te xêza paldayî nivîsandiye: <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protokola Nenas</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Navnîşan protokolekê destnîşan dike (mînak, <q>wxyz://</q>) ku gerok wê nas nake, loma gerok nikare bi asayî bi malperê ve bê girîdan.</p>
+ <ul>
+ <li>Tu dixwazî xwe bigihînî multîmedyayekê yan jî xizmeteke din ya ne nivîskî? Malperê ji bo pêdiviyên zêdek kontrol bike.</li>
+ <li>Dibe ku ji bo hin protokolan beriya gerok wan nas bike, nermalav yan jî pêvekên partiya sêyemîn bivên.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Rûpel nehate dîtin</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Dibe ku pel hatibe guheztin, jêbirin yan jî navê wê hatibe guhertin?</li>
+ <li>Dibe ku navnîşan çewt hatibe nivîsandin?</li>
+ <li>Dibe ku ji bo xwe bigihînî vê hêmanê destûrs te tune be?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Gihîna li dosyeyê hate redkirin</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Dibe ku hatibe jêbirin, ciyê wê hatibe guhertin yan jî ji ber destûrên pelê nikarî xwe bigihîniyê.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy Serverê girêdan red kir</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Gerok ji bo bikaranîna pêşkêşkara proksy hatiye eyarkirin, lê proksyê daxwaza têkiliyê red kir.</p>
+ <ul>
+ <li>DIbe ku eyarên proksyê ne rast hatibin çêkirin? Eyaran kontrol bike û dîsa biceribîne.</li>
+ <li>DIbe ku ev proksy destûrê nade têkiliyên di ser wê torê re?</li>
+ <li>Heke hê jî problem hebin, serî li rêveberiya torê yan jî dabînkera xizmeta înternetê bide.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy server nehate dîtin</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Gerok li gorî bikarînana rajekara proxyê hatiye vesazkirin, lê rajekara proxyê nehat dîtin</p>
+ <ul>
+ <li>Vesazkirina proxyê rast e? Sazkariyan kontrol bike û dîsa biceribîne.</li>
+ <li>Cîhaza te bi toreke çalak ve girêdayî ye?</li>
+ <li>Hîn jî pirsgirêk dewam dike? Ji bo alîkariyê bi rêvebirê tora xwe an jî bi dabînkera înternetê re bişêwire.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Pirsgirêka malpera nebaş (malware)</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Malpera %1$sê wekî malpereke erîşkar hate ragihandin û ew li gorî tercîhên te yên ewlehiyê hate astengkirin.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Pirsgirêka malpera nexwestî</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Malpera %1$sê wekî nermalava ku nayê xwestin hate ragihandin û ew li gorî tercîhên te yên ewlehiyê hate astengkirin.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Pirsgirêka malpera ziyandar</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Malpera %1$sê wekî malpereke ziyandar hate ragihandin û li gorî tercîhên te yên ewlehiyê malper hate astengkirin.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Pirsgirêka malpera xapîner</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Malpera webê ya %1$sê wekî malpereke xapînok hate ragihandin û ev malper li gorî tercîhên te yên ewlehiyê hate astengkirin.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Malpera ewle ne berdest e</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Te ji bo zêdetirina ewlekariyê HTTPS-Tenê vekiriye lê versiyoneke ewle ya HTTPSê ya malpera <em>%1$s</em>ê ne berdest e.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Ji malpera HTTP dewam bike</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..ec9877ece0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kn/strings.xml
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ಮತ್ತೆ ಪ್ರಯತ್ನಿಸು</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ಮನವಿಯನ್ನು ಪೂರ್ಣಗೊಳಿಸಲಾಗಿಲ್ಲ</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>ಈ ತೊಂದರೆ ಅಥವ ದೋಷಕ್ಕಾಗಿನ ಹೆಚ್ಚುವರಿ ಮಾಹಿತಿ ಪ್ರಸ್ತುತ ಲಭ್ಯವಿಲ್ಲ.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">ಸುರಕ್ಷಿತ ಸಂಪರ್ಕವು ವಿಫಲಗೊಂಡಿದೆ</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>ನೀವು ನೋಡಲು ಪ್ರಯತ್ನಿಸುತ್ತಿರುವ ಪುಟವನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ ಪಡೆಯಲಾದ ಮಾಹಿತಿಯ ವಿಶ್ವಾಸಾರ್ಹತೆಯನ್ನು ಪರಿಶೀಲಿಸಲಾಗಿಲ್ಲ.</li>
+ <li>ದಯವಿಟ್ಟು ಈ ತೊಂದರೆಯ ಬಗ್ಗೆ ಜಾಲತಾಣದ ಮಾಲಿಕರಿಗೆ ತಿಳಿಸಿ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">ಸುರಕ್ಷಿತ ಸಂಪರ್ಕವು ವಿಫಲಗೊಂಡಿದೆ</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>ಇದು ಸರ್ವರ್ ಸಂರಚನೆಯಲ್ಲಿನ ಒಂದು ತೊಂದರೆ ಇರಬಹುದು, ಅಥವ ಯಾರಾದರೂ ಸರ್ವರ್‍‍ನಂತೆ ವರ್ತಿಸಲು ಪ್ರಯತ್ನಿಸುತ್ತಿರಬಹುದು.</li>
+ <li>ಈ ಮೊದಲು ನೀವು ಸರ್ವರ್‍‍ಗೆ ಯಶಸ್ವಿಯಾಗಿ ಸಂಪರ್ಕ ಹೊಂದಲು ಸಾಧ್ಯವಾಗಿದ್ದಲ್ಲಿ, ಈಗಿನ ದೋಷವು ತಾತ್ಕಾಲಿಕವಾಗಿರಬಹುದು, ಹಾಗು ನೀವು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ಮುಂದುವರೆದ…</string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ಹಿಂತಿರುಗಿ (ಶಿಫಾರಸು ಮಾಡಲಾಗಿದೆ)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">ಅಪಾಯವನ್ನು ಸ್ವೀಕರಿಸಿ ಮತ್ತು ಮುಂದುವರಿಸಿ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">ಸಂಪರ್ಕಕ್ಕೆ ಅಡಚಣೆಯಾಗಿದೆ</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">ಸಂಪರ್ಕದ ಕಾಲಾವಧಿ ಮುಗಿದಿದೆ</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>ಮನವಿ ಸಲ್ಲಿಸಲಾದ ತಾಣವು ಒಂದು ಸಂಪರ್ಕ ಮನವಿಗೆ ಪ್ರತಿಸ್ಪಂದಿಸುತ್ತಿಲ್ಲ ಹಾಗು ವೀಕ್ಷಕವು ಒಂದು ಪ್ರತ್ಯುತ್ತರಕ್ಕೆ ಕಾಯುವುದನ್ನು ನಿಲ್ಲಿಸಿದೆ.</p>
+ <ul>
+ <li>ಪರಿಚಾರಕವು ಅತಿಯಾದ ಬೇಡಿಕೆಗೆ ಒಳಪಟ್ಟಿರಬಹುದೆ ಅಥವ ಒಂದು ತಾತ್ಕಾಲಿಕ ಸಂಪರ್ಕ ಕಡಿತಕ್ಕೆ ಒಳಗಾಗಿರಬಹುದೆ? ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.</li>
+ <li>ನೀವು ಬೇರೆ ತಾಣಗಳನ್ನು ವೀಕ್ಷಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲವೆ? ಗಣಕದ ಜಾಲ ಸಂಪರ್ಕವನ್ನು ಪರೀಕ್ಷಿಸಿ.</li>
+ <li>ನಿಮ್ಮ ಗಣಕ ಅಥವ ಜಾಲ ಸಂಪರ್ಕವು ಫೈರ್ವಾಲ್ ಅಥವ ಪ್ರಾಕ್ಸಿಯಿಂದ ಸಂರಕ್ಷಿತಗೊಂಡಿದೆಯೆ? ಸರಿಯಲ್ಲದ ಸಿದ್ಧತೆಗಳು ಜಾಲ ವೀಕ್ಷಣೆಯಲ್ಲಿ ಹಸ್ತಕ್ಷೇಪ ಮಾಡಬಹುದು.</li>
+ <li>ಇನ್ನೂ ಸಹ ತೊಂದರೆ ಇದೆಯೆ?ನೆರವಿಗಾಗಿ ನಿಮ್ಮ ಗಣಕ ವ್ಯವಸ್ಥಾಪಕರನ್ನು ಅಥವ ಜಾಲ ಸಂಪರ್ಕ ಒದಗಿಸುವವರನ್ನು ಸಂಪರ್ಕಿಸಿ</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">ಸಂಪರ್ಕ ಹೊಂದಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>ಈ ತಾಣವು ತಾತ್ಕಾಲಿಕವಾಗಿ ಲಭ್ಯವಿಲ್ಲದಿರಬಹುದು ಅಥವಾ ಕಾರ್ಯದ ಒತ್ತಡದಲ್ಲಿರಬಹುದು. ಕೆಲವು ಕ್ಷಣಗಳ ನಂತರ ಮತ್ತೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸಿ.</li>
+ <li>ಯಾವುದೇ ಪುಟವೂ ತೆರೆಯದಿದ್ದಲ್ಲಿ, ನಿಮ್ಮ ಮೊಬೈಲಿನ ಡೇಟಾ ಅಥವಾ ವೈ-ಫೈ ಪರಿಶೀಲಿಸಿ. </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">ಪರಿಚಾರಕದಿಂದ ಅನಿರೀಕ್ಷಿತ ಪ್ರತಿಕ್ರಿಯೆ</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>ತಾಣವು ಒಂದು ಅನಿರೀಕ್ಷಿತ ರೀತಿಯಲ್ಲಿ ಜಾಲ ಮನವಿಗಳಿಗೆ ಪ್ರತ್ಯುತ್ತರಿಸಿದೆ ಹಾಗು ವೀಕ್ಷಕವು ಮುಂದುವರೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ಪುಟವು ಸರಿಯಾಗಿ ಮರಳಿ ನಿರ್ದೇಶನಗೊಳ್ಳುತ್ತಿಲ್ಲ</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>ವೀಕ್ಷಕವು ಮನವಿ ಸಲ್ಲಿಸಿದ ವಿಷಯವನ್ನು ಮರಳಿ ಪಡೆಯುವುದನ್ನು ನಿಲ್ಲಿಸಿದೆ. ತಾಣವು ಎಂದೆಂದಿಗೂ ಮುಗಿಯದ ರೀತಿಯಲ್ಲಿ ಮನವಿಯನ್ನು ಮರಳಿ ನಿರ್ದೇಶಿಸುತ್ತಿದೆ.</p>
+ <ul>
+ <li>ಈ ತಾಣಕ್ಕೆ ಅಗತ್ಯವಿರುವ ಕುಕಿಗಳನ್ನು ನೀವು ಅಶಕ್ತ ಅಥವ ನಿರ್ಬಂಧಿಸಿದ್ದೀರೆ?</li>
+ <li>ಈ ತಾಣದ ಕುಕಿಗಳನ್ನು ಅನುಮತಿಸಿದರೂ ಸಹ ತೊಂದರೆ ಪರಿಹಾರವಾಗದಿದ್ದರೆ, ಅದು ಒಂದು ಪರಿಚಾರಕದ ಸಂರಚನೆಗೆ ಸಂಬಂಧಿತ ವಿಷಯವಾಗಿದ್ದು ನಿಮ್ಮ ಗಣಕಕ್ಕೆ ಸಂಬಂಧ ಪಟ್ಟಿದ್ದಲ್ಲಎಂದರ್ಥ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ಆಫ್‍ಲೈನ್ ವಿಧಾನ</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>ಜಾಲವೀಕ್ಷಕವು ತನ್ನ ಆಫ್‌ಲೈನ್ ಕ್ರಮದಲ್ಲಿದೆ ಮತ್ತು ಮನವಿ ಮಾಡಲಾದ ಅಂಶದೊಂದಿಗೆ ಸಂಪರ್ಕವನ್ನು ಸಾಧಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ.</p>
+ <ul>
+ <li>ಇದು ಗಣಕವು ಸಕ್ರಿಯ ಜಾಲಬಂಧದೊಂದಿಗೆ ಸಂಪರ್ಕತಗೊಂಡಿದೆಯೆ?</li>
+ <li>ಆನ್‌ಲೈನ್ ಕ್ರಮಕ್ಕೆ ಬದಲಾಯಿಸಿ ನಂತರ ಪುಟವನ್ನು ಮರಳಿ ಲೋಡ್ ಮಾಡಲು “ಇನ್ನೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸು” ಅನ್ನು ಒತ್ತಿ.</li>
+ </ul>]]></string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>ಮನವಿ ಸಲ್ಲಿಸಲಾದ ವಿಳಾಸವು ಒಂದು ಸಂಪರ್ಕ ಸ್ಥಾನವನ್ನು (e.g. <q>mozilla.org:80</q> ಪೋರ್ಟ್ 80 ಕ್ಕೆ mozilla.org ನಲ್ಲಿ) ಸೂಚಿಸಿದೆ ಸಾಮಾನ್ಯವಾಗಿ ಜಾಲ ವೀಕ್ಷಣೆಯಲ್ಲದೆ <em>ಇತರೆ</em> ಕಾರ್ಯಗಳಿಗೂ ಬಳಸಲ್ಪಡುತ್ತದೆ. ವೀಕ್ಷಕವು ನಿಮ್ಮ ಸುರಕ್ಷತೆ ಹಾಗು ಸಂರಕ್ಷಣೆಯ ಸಲುವಾಗಿ ಈ ಮನವಿಯನ್ನು ರದ್ದು ಮಾಡಿದೆ.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">ಸಂಪರ್ಕವು ಮರಳಿ ಹೊಂದಿಸಲ್ಪಟ್ಟಿತು</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">ಅಸುರಕ್ಷಿತ ಕಡತದ ಬಗೆ</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>ದಯವಿಟ್ಟು ಈ ತೊಂದರೆಯನ್ನು ತಿಳಿಸಲು ಜಾಲತಾಣದ ಮಾಲಿಕರನ್ನು ಸಂಪರ್ಕಿಸಿ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">ವಿಷಯ ಹಾಳಾದ ದೋಷ</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>ನೀವು ನೋಡಲು ಬಯಸುವ ಪುಟವನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ ಮಾಹಿತಿ ವರ್ಗಾವಣೆಯಲ್ಲಿ ಒಂದು ದೋಷವು ಕಂಡು ಬಂದಿದೆ.</p>
+ <ul>
+ <li>ಈ ತೊಂದರೆಯನ್ನು ವರದಿ ಮಾಡಲು ಜಾಲತಾಣದ ಮಾಲಿಕರನ್ನು ಸಂಪರ್ಕಿಸಿ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">ವಿಷಯ ಕ್ರ್ಯಾಶ್ ಆಗಿದೆ</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>ನೀವು ನೋಡಲು ಬಯಸುವ ಪುಟವನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ ಮಾಹಿತಿ ವರ್ಗಾವಣೆಯಲ್ಲಿ ಒಂದು ದೋಷವು ಕಂಡು ಬಂದಿದೆ.</p>
+ <ul>
+ <li>ಈ ತೊಂದರೆಯನ್ನು ವರದಿ ಮಾಡಲು ಜಾಲತಾಣದ ಮಾಲಿಕರನ್ನು ಸಂಪರ್ಕಿಸಿ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">ವಿಷಯದ ಎನ್ಕೋಡಿಂಗ್‌ ದೋಷ</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>ನೀವು ನೋಡಲು ಬಯಸುತ್ತಿರುವ ಪುಟವನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ ಅದು ಅಮಾನ್ಯವಾದ ಅಥವ ಬೆಂಬಲವಿಲ್ಲದ ಬಗೆಯ ಒಂದು ಸಂಕುಚನ(ಕಂಪ್ರೆಶನ್) ಅನ್ನು ಬಳಸುತ್ತದೆ.</p>
+ <ul>
+ <li>ದಯವಿಟ್ಟು ಈ ತೊಂದರೆಯನ್ನು ತಿಳಿಸಲು ಜಾಲತಾಣದ ಮಾಲಿಕರನ್ನು ಸಂಪರ್ಕಿಸಿ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ವಿಳಾಸವು ಕಂಡುಬಂದಿಲ್ಲ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕ ಇಲ್ಲ</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ನಿಮ್ಮ ನೆಟ್‌ವರ್ಕ್ ಸಂಪರ್ಕವನ್ನು ಪರಿಶೀಲಿಸಿ ಅಥವಾ ಕೆಲವು ಕ್ಷಣಗಳಲ್ಲಿ ಪುಟವನ್ನು ಮರುಲೋಡ್ ಮಾಡಲು ಪ್ರಯತ್ನಿಸಿ.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ಪುನಃ ಲೋಡ್ ಮಾಡು</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ಅಸಿಂಧುವಾದ ವಿಳಾಸ</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>ಒದಗಿಸಲಾದ ವಿಳಾಸವು ಒಂದು ಗುರುತಿಸಬಲ್ಲ ಮಾದರಿಯಲ್ಲಿ ಇಲ್ಲ. ದಯವಿಟ್ಟು ತಪ್ಪುಗಳಿಗಾಗಿ ಸ್ಥಳ ಪಟ್ಟಿಯಲ್ಲಿ ನೋಡಿ ನಂತರ ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ವಿಳಾಸವು ಸಮಂಜಸವಾಗಿಲ್ಲ</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>ಜಾಲ ವಿಳಾಸವು ಸಾಮಾನ್ಯವಾಗಿ <strong>http://www.example.com/</strong> ರೀತಿಯಲ್ಲಿ ಬರೆಯಲ್ಪಡುತ್ತದೆ</li>
+ <li>ಮುಮ್ಮುಖವಾಗಿರುವ ಅಡ್ಡಗೆರಗಳನ್ನು ನೀವು ಬಳಸಿದ್ದೀರ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ (ಉದಾ. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">ಅಜ್ಞಾತ ಪ್ರೊಟೊಕಾಲ್</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>ವಿಳಾಸ ಸೂಚಿಸುವ ಒಂದು ಪ್ರೊಟೊಕಾಲನ್ನು (e.g. <q>wxyz://</q>) ವೀಕ್ಷಕವು ಗುರುತಿಸಲಾಗಿಲ್ಲ, ಆದ್ದರಿಂದ ವೀಕ್ಷಕವು ತಾಣಕ್ಕೆ ಸರಿಯಾಗಿ ಸಂಪರ್ಕ ಹೊಂದಲಾಗುತ್ತಿಲ್ಲ.</p>
+ <ul>
+ <li>ನೀವು ಮಲ್ಟಿಮೀಡಿಯಾ ಅಥವ ಪಠ್ಯವಲ್ಲದ ಇತರೆ ಸೇವೆಗಳನ್ನು ನಿಲುಕಿಸಿಕೊಳ್ಳಲು ಪ್ರಯತ್ನಿಸುತ್ತಿದ್ದೀರಾ?? ತಾಣದ ಹೆಚ್ಚುವರಿ ಅಗತ್ಯತೆಗಳನ್ನು ಪರೀಕ್ಷಿಸಿ.</li>
+ <li>ಕೆಲವೊಂದು ಪ್ರೋಟೊಕಾಲ್‍ಗಳನ್ನು ವೀಕ್ಷಕವು ಗುರುತಿಸಲು ಕೆಲವೊಂದು ಮೂರನೆ ಪಕ್ಷದ(third party) ತಂತ್ರಾಂಶ ಅಥವ ಪ್ಲಗ್‍ಇನ್‍ಗಳ ಅಗತ್ಯವಿರುತ್ತದೆ.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ಕಡತವು ಕಂಡು ಬಂದಿಲ್ಲ</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>ಅದರ ಹೆಸರು ಬದಲಾಯಿಸರಬಹುದು, ತೆಗೆದುಹಾಕಲ್ಪಟ್ಟಿರಬಹುದು, ಅಥವ ಸ್ಥಳಾಂತರಗೊಂಡಿರಬಹುದು</li>
+ <li>ವಿಳಾಸದಲ್ಲಿ ಕಾಗುಣಿತ, ಕ್ಯಾಪಿಟಲೈಸೇಶನ್, ಅಥವ ಬೆರಳಚ್ಚುದೋಷ ಇದ್ದಿರಬಹುದೆ?</li>
+ <li>ನೀವು ಮನವಿ ಸಲ್ಲಿಸಿದ ವಿಷಯವನ್ನು ನಿಲುಕಿಸಿಕೊಳ್ಳಲು ನಿಮಗೆ ಸಾಕಷ್ಟು ಅನುಮತಿ ಇದೆಯೆ?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ಕಡತ ಪ್ರವೇಶವನ್ನು ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>ಅದನ್ನು ತೆಗೆದುಹಾಕಿರಬಹುದು, ಜರುಗಿಸಿರಬಹುದು, ಅಥವಾ ಕಡತದ ಅನುಮತಿಗಳು ಪ್ರವೇಶವನ್ನು ತಡೆಹಿಡಿಯುತ್ತಿರವಬಹುದು.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ಪ್ರಾಕ್ಸಿ ಪರಿಚಾರಕವು ಸಂಪರ್ಕವನ್ನು ನಿರಾಕರಿಸಿದೆ</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>ವೀಕ್ಷಕವು ಒಂದು ಪ್ರಾಕ್ಸಿ ಪರಿಚಾರಕವನ್ನು ಬಳಸಲು ಸಂರಚಿತಗೊಂಡಿದೆ, ಆದರೆ ಪ್ರಾಕ್ಸಿಯು ಒಂದು ಸಂಪರ್ಕವನ್ನು ನಿರಾಕರಿಸಿದೆ.</p>
+ <ul>
+ <li>ವೀಕ್ಷಕದ ಪ್ರಾಕ್ಸಿ ಸಂರಚನೆಯು ಸರಿಯಾಗಿದೆಯೆ? ಸಂರಚನೆಯನ್ನು ಪರೀಕ್ಷಿಸಿ ಹಾಗು ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.</li>
+ <li>ಪ್ರಾಕ್ಸಿ ಸೇವೆಯು ಈ ಜಾಲದಿಂದ ಸಂಪರ್ಕವನ್ನು ಅನುಮತಿಸುತ್ತದೆಯೆ?</li>
+ <li>ಇನ್ನೂ ಸಹ ತೊಂದರೆ ಇದೆಯೆ?ನೆರವಿಗಾಗಿ ನಿಮ್ಮ ಗಣಕ ವ್ಯವಸ್ಥಾಪಕರನ್ನು ಅಥವ ಜಾಲ ಸಂಪರ್ಕ ಒದಗಿಸುವವರನ್ನು ಸಂಪರ್ಕಿಸಿ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ಪ್ರಾಕ್ಸಿ ಪರಿಚಾರಕವು ಕಂಡು ಬಂದಿಲ್ಲ</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>ವೀಕ್ಷಕವು ಒಂದು ಪ್ರಾಕ್ಸಿ ಪರಿಚಾರಕವನ್ನು ಬಳಸಲು ಸಂರಚಿತಗೊಂಡಿದೆ, ಆದರೆ ಪ್ರಾಕ್ಸಿಯು ಒಂದು ಸಂಪರ್ಕವನ್ನು ನಿರಾಕರಿಸಿದೆ.</p>
+ <ul>
+ <li>ವೀಕ್ಷಕದ ಪ್ರಾಕ್ಸಿ ಸಂರಚನೆಯು ಸರಿಯಾಗಿದೆಯೆ? ಸಂರಚನೆಯನ್ನು ಪರೀಕ್ಷಿಸಿ ಹಾಗು ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.</li>
+ <li>ಪ್ರಾಕ್ಸಿ ಸೇವೆಯು ಈ ಜಾಲದಿಂದ ಸಂಪರ್ಕವನ್ನು ಅನುಮತಿಸುತ್ತದೆಯೆ?</li>
+ <li>ಇನ್ನೂ ಸಹ ತೊಂದರೆ ಇದೆಯೆ?ನೆರವಿಗಾಗಿ ನಿಮ್ಮ ಗಣಕ ವ್ಯವಸ್ಥಾಪಕರನ್ನು ಅಥವ ಜಾಲ ಸಂಪರ್ಕ ಒದಗಿಸುವವರನ್ನು ಸಂಪರ್ಕಿಸಿ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">ಮಾಲ್ವೇರ್ ಸೈಟ್ ಸಮಸ್ಯೆ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s ಒಂದು ಧಾಳಿಕಾರಕ ತಾಣವೆಂದು ವರದಿ ಮಾಡಲ್ಪಟ್ಟಿದೆ ಹಾಗು ನಿಮ್ಮ ಸುರಕ್ಷತಾ ಆದ್ಯತೆಗಳಿಗನುಸಾರವಾಗಿಅದು ನಿರ್ಬಂಧಿಸಲ್ಪಟ್ಟಿದೆ.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ಅನಗತ್ಯ ಸೈಟ್ ಸಮಸ್ಯೆ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s ಅನವಶ್ಯ ತಂತ್ರಾಂಶಗಳನ್ನು ಒದಗಿಸುವ ತಾಣವೆಂದು ವರದಿ ಮಾಡಲ್ಪಟ್ಟಿದೆ ಹಾಗು ನಿಮ್ಮ ಸುರಕ್ಷತಾ ಆದ್ಯತೆಗಳಿಗನುಸಾರವಾಗಿ ಅದು ನಿರ್ಬಂಧಿಸಲ್ಪಟ್ಟಿದೆ.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ಹಾನಿಕಾರಕ ಸೈಟ್ ಸಮಸ್ಯೆ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s ಒಂದು ಧಾಳಿಕಾರಕ ತಾಣವೆಂದು ವರದಿ ಮಾಡಲ್ಪಟ್ಟಿದೆ ಹಾಗು ನಿಮ್ಮ ಸುರಕ್ಷತಾ ಆದ್ಯತೆಗಳಿಗನುಸಾರವಾಗಿಅದು ನಿರ್ಬಂಧಿಸಲ್ಪಟ್ಟಿದೆ.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">ವಂಚಕ ತಾಣ ವರದಿ ಮಾಡಿ</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$s ಒಂದು ಧಾಳಿಕಾರಕ ತಾಣವೆಂದು ವರದಿ ಮಾಡಲ್ಪಟ್ಟಿದೆ ಹಾಗು ನಿಮ್ಮ ಸುರಕ್ಷತಾ ಆದ್ಯತೆಗಳಿಗನುಸಾರವಾಗಿಅದು ನಿರ್ಬಂಧಿಸಲ್ಪಟ್ಟಿದೆ.</p>]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..6c8c5ee5c9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ko/strings.xml
@@ -0,0 +1,290 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">다시 시도</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">요청을 완료할 수 없습니다</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>현재 이 문제 또는 오류에 대한 추가적 정보가 없습니다.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">보안 연결 실패</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>현재 보시려는 페이지는 수신된 데이터의 진위를 확인할 수 없기 때문에 보여줄 수 없습니다.</li>
+ <li>웹 사이트 소유자에게 이 문제를 알리십시오.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">보안 연결 실패</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>이는 서버의 설정에 문제가 있거나, 누군가가 서버를 위장하려 할 때 발생할 수 있습니다.</li>
+ <li>이전에는 서버에 연결할 때 아무 문제도 없었다면 일시적인 오류일 수 있으므로 나중에 다시 시도해보십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">고급…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>누군가 사이트를 가장하려고 할 수 있으므로 계속해서는 안됩니다.</label>
+ <br><br>
+ <label>웹 사이트는 인증서를 통해 신원을 증명합니다. 인증서 발급자를 알 수 없거나 인증서가 자체 서명되었거나 서버가 올바른 중간 인증서를 보내지 않고 있기 때문에 %1$s이(가) <b>%2$s</b>을(를) 신뢰하지 않습니다.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">뒤로 (권장)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">위험을 감수하고 계속</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">이 웹사이트는 보안 연결이 필요합니다.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>이 웹 사이트는 보안 연결이 필요하므로, 페이지를 표시할 수 없습니다.</li>
+ <li>이러한 문제는 대부분 웹 사이트와 관련이 있고 사용자가 할 수 있는 일은 없습니다.</li>
+ <li>웹 사이트의 관리자에게 문제에 대해 알려주실 수 있습니다.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">고급…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> 사이트는 HTTP Strict Transport Security (HSTS)라는 보안 정책을 가지고 있어서 <b>%2$s</b>가 보안 연결만 할 수 있습니다. 이 사이트를 방문하기 위해 예외를 추가 할 수 없습니다. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">뒤로</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">연결이 끊어짐</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>브라우저가 성공적으로 연결되었지만 정보 전송 중에 연결이 끊어졌습니다. 다시 시도해보십시오.</p>
+ <ul>
+ <li>서버가 일시적으로 사용할 수 없거나 사용자가 너무 많은 상태일 수 있습니다. 잠시 후 다시 시도해보십시오.</li>
+ <li>페이지가 전혀 로딩되지 않는다면 기기의 데이터 또는 와이파이 연결 상태를 확인해보십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">연결 시간 초과</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>요청하신 사이트가 연결 요청에 응답하지 않아 브라우저가 응답 대기를 중단했습니다.</p>
+ <ul>
+ <li>서버의 사용량이 많거나, 일시적 가동 중지일 수 있습니다. 나중에 다시 시도해 보십시오.</li>
+ <li>다른 사이트도 열리지 않습니까? 컴퓨터의 네트워크 연결을 점검해 보십시오.</li>
+ <li>사용자의 컴퓨터 또는 네트워크가 방화벽 또는 프록시에 의해 보호되고 있습니까? 설정이 잘못되면 웹 탐색에 방해가 될 수 있습니다.</li>
+ <li>여전히 문제가 있습니까? 네트워크 관리자 또는 인터넷 서비스 제공자에게 지원을 요청하세요.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">연결할 수 없음</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>서버가 일시적으로 사용할 수 없거나 사용자가 너무 많은 상태일 수 있습니다. 잠시 후 다시 시도해보십시오.</li>
+ <li>페이지가 전혀 로딩되지 않는다면 기기의 데이터 또는 와이파이 연결 상태를 확인해보십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">서버에서 예기치 않은 응답</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>네트워크 요청에 대해 사이트가 예기치 않은 방식으로 반응했기 때문에 브라우저가 계속할 수 없습니다.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">페이지 리다이렉션 오류</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>브라우저가 요청하신 항목을 불러오려는 시도를 중단했습니다. 사이트가 해당 요청을 결코 끝날 수 없는 방식으로 리다이렉트하고 있습니다.</p>
+ <ul>
+ <li>이 사이트가 요구하는 쿠키를 비활성화하거나 차단했습니까?</li>
+ <li>사이트의 쿠키를 승인해도 문제가 해결되지 않는다면 컴퓨터의 문제가 아니라 서버 설정 문제일 가능성이 큽니다.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">오프라인 모드</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>브라우저가 오프라인 모드로 작동하고 있고 요청하신 항목에 연결할 수 없습니다.</p>
+ <ul>
+ <li>활성화된 상태의 네트워크에 컴퓨터가 연결되어 있습니까?</li>
+ <li>‘다시 시도하기’를 눌러 온라인 모드로 전환한 다음 페이지를 다시 로딩하십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">보안상 이유로 제한된 포트</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>요청하신 주소는 일반적으로 웹 탐색 <em>이외</em>의 목적으로 사용되는 포트를 특정하고 있습니다(예: mozilla.org에서 포트 80에 대한 <q>mozilla.org:80</q>). 안전과 보안을 위해 브라우저가 해당 요청을 취소했습니다.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">연결이 초기화되었습니다</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>연결 시도 중에 네트워크 연결이 끊어졌습니다. 다시 시도해보십시오.</p>
+ <ul>
+ <li>서버가 일시적으로 사용할 수 없거나 사용자가 너무 많은 상태일 수 있습니다. 잠시 후 다시 시도해보십시오.</li>
+ <li>어느 페이지도 로딩되지 않는다면 기기의 데이터 또는 와이파이 연결 상태를 확인해보십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">안전하지 않은 파일 형식</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>웹 사이트 소유자에게 이 문제를 알리십시오.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">손상된 콘텐츠 오류</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>현재 보시려는 페이지는 데이터 전송 오류가 탐지되었기 때문에 보여줄 수 없습니다.</p>
+ <ul>
+ <li>웹 사이트 소유자에게 이 문제를 알리십시오.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">콘텐츠 충돌됨</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>현재 보시려는 페이지는 데이터 전송 오류가 탐지되었기 때문에 보여줄 수 없습니다.</p>
+ <ul>
+ <li>웹 사이트 소유자에게 이 문제를 알리십시오.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">콘텐츠 인코딩 오류</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>현재 보시려는 페이지는 유효하지 않거나 지원되지 않는 압축 형식을 사용하고 있기 때문에 보여드릴 수 없습니다.</p>
+ <ul>
+ <li>웹 사이트 소유자에게 이 문제를 알리십시오.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">서버를 찾을 수 없음</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>브라우저가 제시된 주소에 대한 호스트 서버를 찾지 못했습니다.</p>
+ <ul>
+ <li><strong>www</strong>.example.com이 아니라 <strong>ww</strong>.example.com이라고 쓰는 등의 오타가 있지 않은지 확인해보십시오.</li>
+ <li>어느 페이지도 로딩되지 않는다면 기기의 데이터 또는 와이파이 연결 상태를 확인해보십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">인터넷 연결 안 됨</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">네트워크 연결을 확인하시거나 잠시 후 페이지를 다시 로드하세요.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">새로 고침</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">잘못된 주소</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>제시된 주소는 인정된 형식이 아닙니다. 주소 표시줄에 오류가 있지 않은지 확인한 다음 다시 시도해보십시오.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">유효하지 않은 주소입니다</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>웹 주소는 일반적으로 다음과 같이 씁니다:<strong>http://www.example.com/</strong></li>
+ <li>반드시 슬래시(<strong>/</strong>)를 사용하십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">알 수 없는 프로토콜</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>이 주소는 브라우저가 인식하지 못하는 프로토콜(예: <q>wxyz://</q>)을 특정하고 있기 때문에 브라우저가 사이트와 연결할 수 없습니다.</p>
+ <ul>
+ <li>멀티미디어 또는 기타 비문자 서비스에 접근하려는 것입니까? 사이트에서 추가적 요건을 확인해보십시오.</li>
+ <li>어떤 프로토콜은 브라우저가 인식할 수 없는 제3자 소프트웨어 또는 플러그인을 요할 수 있습니다.</li>
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">파일을 찾을 수 없음</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>해당 항목이 명칭이 바뀌거나, 제거되거나, 이동되지 않았습니까?</li>
+ <li>주소에 철자, 대소문자 구분 또는 기타 오타가 있지 않습니까?</li>
+ <li>요청하신 항목에 대한 충분한 접근권을 가지고 있습니까?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">파일 접근이 거부되었습니다</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>파일이 제거 또는 이동되었거나, 파일 권한 문제로 접근이 금지될 수 있습니다.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">프록시 서버가 연결을 거부했습니다</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>브라우저가 프록시 서버를 사용하도록 설정되어 있지만 프록시가 연결을 거부했습니다.</p>
+ <ul>
+ <li>브라우저의 프록시 설정이 올바르게 되어 있습니까? 설정을 확인한 다음 다시 시도해보십시오.</li>
+ <li>프록시 서비스가 이 네트워크를 통한 연결을 허용합니까?</li>
+ <li>여전히 문제가 있습니까? 네트워크 관리자 또는 인터넷 서비스 제공자에게 지원을 요청하십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">프록시 서버를 찾을 수 없습니다</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>브라우저가 프록시 서버를 사용하도록 설정되어 있지만 프록시를 찾을 수 없습니다.</p>
+ <ul>
+ <li>브라우저의 프록시 설정이 올바르게 되어 있습니까? 설정을 확인한 다음 다시 시도해보십시오.</li>
+ <li>컴퓨터가 활성화된 네트워크에 연결되어 있습니까?</li>
+ <li>여전히 문제가 있습니까? 네트워크 관리자 또는 인터넷 서비스 제공자에게 지원을 요청하십시오.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">악성 코드 사이트 문제</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>%1$s의 사이트는 공격 사이트로 보고되었으며, 사용자의 보안 설정에 따라 차단되었습니다.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">원하지 않은 사이트 문제</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s의 사이트는 원하지 않은 소프트웨어를 제공하는 것으로 보고되었으며, 사용자의 보안 설정에 따라 차단되었습니다.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">유해 사이트 문제</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s의 사이트는 잠재적 유해 사이트로 보고되었으며, 사용자의 보안 설정에 따라 차단되었습니다.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">사기 사이트 문제</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s의 이 웹페이지는 사기 사이트로 보고되었으며, 사용자의 보안 설정에 따라 차단되었습니다.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">보안 사이트를 사용할 수 없음</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[보안 강화를 위해 HTTPS 전용 모드를 사용하도록 설정했으며 <em>%1$s</em>의 HTTPS 버전을 사용할 수 없습니다.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP 사이트로 계속</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kw/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kw/strings.xml
new file mode 100644
index 0000000000..4b8e2fb078
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-kw/strings.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Assaya arta</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Ny Yllir Kowlwul Govyn</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Nyns yw kavadow kedhlow ynwedhek war an kudyn po gwall ma.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Fyllis Junyans Servyer</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Ny yllir diskwedhes dhis an folen may fynn\'ta drehedhes drefen na allas gwirhe gwiryonder an deur resevys.</li>
+ <li>Mar pleg kestav orth perghenogyon an wiasva dhe dherivas orta an kudyn ma.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Junyans Diogel a Fyllis</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Martesen yth yw hemma kudyn gans kefurvyans an servyer, po y hyll bos nebonan owth assaya omwul an servyer.</li>
+ <li>Mar junsys yn sewen orth an servyer ma kyns, y hyll bos anparghus an gwall ha ty a yll assaya arta diwettha.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avonsys…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Possybyl yw bos nebonan owth assaya omwul an wiva ha gwell vydh sevel orth pesya.</label>
+ <br><br>
+ <label>Gwiasvaow a brev aga honanieth dre destskrifow. Ny fydh %1$s orth <b>%2$s</b> rag bos ankoth dyller hy thestskrif, bos honan-sinys, po nag usi an servyer ow tanvon an testksrifow kres ewn.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Mos War-gamm (Avisys)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Degemeres an Peryl ha Pesya</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Res yw junyans diogel dhe\'n wiasva ma.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Ny yllir diskwedhes an folen mayth esowgh owth assaya mires drefen bos res junyans diogel dhe\'n wiasva ma.</li>
+ <li>Dres lycklod yma an kudyn gans an wiasva, ha ny yllydh gul travyth dh\'y ewna.</li>
+ <li>Ty a yll gwarnya menystrer an wiasva a\'n kudyn.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avonsys…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ Yma polisi diogeledh dhe <label><b>%1$s</b> HTTP Strict Transport Security (HSTS) y hanow, ha rag henna ny yll <b>%2$s</b> mes junya orti yn tiogel. Ny yll\'ta keworra namm dhe vysytya orth an wiasva ma.</label>
+ ]]></string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Mos War-gamm</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Goderrys veu an junyans</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>An beurell a junyas yn sewen, mes goderrys veu an junyans hag ow treuskorra kedhlow. Mar pleg assay arta.</p>
+ <ul>
+ <li>Possybyl yw bos ankavadow yn anparghus po re vysi.Assay arta yn tro.</li>
+ <li>Mar ny yll\'ta karga py folennow pynag, check kedhlow po junyans Diwi dha dhevis.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Termyn an junyans re dhiwedhas</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Ny worthebis an wiasva a vynnsys orth govyn junyans hag an beurell a worfennas gortos gorthyp.</p>
+ <ul>
+ <li>A alsa bos an servyer yn-dann veur dhemond po torr servis? Assay arta diwettha.</li>
+ <li>A ny yll\'ta peuri gwiasvaow erel? Check junyans rosweyth an devis.</li>
+ <li>Yw difresys dha rosweyth gans tanfos po kanasek? Y hyll dewisyow kamm mellya orth peuri an Wi.</li>
+ <li>Trobel hwath? Kussul orth menystrer dha rosweyth po dha brovier kesrosweyth rag gweres.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Ny allas junya</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+<ul>
+ <li>An wiasva a alsa bos ankavadow rag tro po re vysi. Assay arta wosa pols.</li>
+ <li>Mar ny yll\'ta karga folen vyth, check deur po junyans Diwi dha dhevis.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Gorthyp anwaytys a\'n servyer</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>An wiasva a worthebis orth an govyn rosweyth yn fordh anwaytys ha nyns yw possybyl an beurell dhe besya.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Nyns usi an folen ow tasprennya yn ewn</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>An beurell a hedhas assaya hwilas an daklen a veu bysys. Yma an wiva ow tasprennya an govyn ma na yll nevra bos kowlwrys.</p>
+ <ul>
+ <li>A dhisweythressys po lettya an pastiow yw res dhe\'n wiva ma?</li>
+ <li>Mar ny weres degemeres pastiow an wiva owth ewna an kudyn, possybyl yw bos kudyn kefurvyans an servyer a-der dha dhevis tejy.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modh Dhywarlinen</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..ee6e980218
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lij/strings.xml
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Preuva torna</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">No riescio a finî a domanda</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>No gh\'é son atre informaçioin in sciô problema.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Conescion segua no riescia</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>A pagina che ti veu vedde a no peu ese mostrâ perché no l\'é poscibile verificâ l’aotenticitæ di dæti riçevui.</li>
+ <li>Ciamma o responsabile do scito web pe informalo do problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Conescion segua no riescia</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Sto chi o peu ese un problema co-e inpostaçion do server ò peu ascì ese quarchedun che o preuva a fâ finta de ese o server.</li>
+ <li>Se ti ê conesso sensa problemi a sto server into passou, aloa l\'erô o peu esê tenporaneo e ti peu provâ torna dòppo.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avansæ…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Porieva ese che quardun o preuva a fâ finta d\'ese o scito e sarieiva megio se no ti væ avanti.</label>
+ <br><br>
+ <label>I sciti web garantiscian a pròpia identitæ con di certificati. %1$s o no considera o scito <b>%2$s</b> fidou perché l\’autoritæ ch\'a l\'à emisso o certificato a l\'é sconosciua, o certificato o l\'é firmato pe conto seu opûre o server o no à mandou i certificati intermedi che servan.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Vanni inderê (Racomandou)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Acetta o reizego e vanni</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avansæ…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Vanni inderê</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">A conescion a l\'é stæta scancelâ</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>O navegatô o s\'é conesso ben, ma a conescion a l\'é è stæta interotta into trasferimento de informaçioin. Preuva torna. </p>
+ <ul>
+ <li> O scito o porieiva ese tenporaneamente no disponibile ò tròppo indafarou. Preuva torna tra quarche momento. </li>
+ <li> Se no ti riesci a caregâ nisciunn-a pagina, contròlla i dæti do dispoxitivo ò a conescion Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">A conescion a gh\'à misso tròppo tenpo</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>O scito che t\'æ domandou o no risponde e o navegatô o no l\'aspetâ ciù a risposta.</p>
+ <ul>
+ <li>Fòscia o server o gh\'à \'n\'erta domanda ò \'n problema tenporaneo? Preuva torna dòppo.</li>
+ <li>No ti riesci a navegâ inti atri sciti? Contròlla a conescion.</li>
+ <li>O teu computer ò a teu conescion en protezui da \'n firewall ò proxy? Se e teu inpostaçioin son sbaliæ peuan interferî co-a navegaçion.</li>
+ <li>i gh\'æ torna di problemi? Ciamma o teu aministratô da ræ ò fornitô de serviççi internet pe ascistensa.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">No riescio a conetime</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>O scito o porieiva ese tenporaneamente inacesibile ò tròppo traficou. Preuva torna tra quarche momento.</li>
+ <li>Se no ti riesci a caregâ nisciunn-a pagina, preuva a controlâ a conescion do teu computer.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Risposta sbaliâ da-o server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>O scito risponde in \'n mòddo ch\'o no capiscio e-o navegatô o no peu continoâ.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">A pagina no redireçionn-a ben</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>O navegatô o no preuvâ ciù a riçeive l\'ògetto domandou. O scito redireçionn-a sensa fin.</p>
+ <ul>
+ <li>T\'æ dizabilitou ò blocou i cookie domandæ da sto scito?</li>
+ <li>Se ti acetti i cookie do scito e no ti rizòlvi o problema, aloa porieivan ese e inpostaçioin do server che no van ben e no quelle do teu dispoxitivo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Feua linia</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>O navegatô o l\'é into mòddo feua linia e o no peu colegase a l\'ògetto çernuo.</p>
+ <ul>
+ <li>O dispoxitivo o l\'é conesso a \'na ræ ativa?</li>
+ <li>Sciacca “Preuva torna” pe pasâ a-o mòddo in linia e caregâ torna a pagina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Acesso a-a pòrta dizabilitou pe raxoin de seguessa</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>A pòrta domandâ (ez. <q>mozilla.org:80</q> pa-a pòrta 80 in sciô mozilla.org) l\'é uzâ pe <em>atri</em> fin che no seggian a navegaçion. O navegatô l\'à scancelou a domanda pe a teu proteçion e seguessa.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Conescion scancelâ</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>O navegatô o s\'é conesso ben, ma a conescion a l\'é è stæta interotta into trasferimento de informaçioin. Preuva torna. </p>
+ <ul>
+ <li> O scito o porieiva ese tenporaneamente no disponibile ò tròppo indafarou. Preuva torna tra quarche momento. </li>
+ <li> Se no ti riesci a caregâ nisciunn-a pagina, contròlla i dæti do dispoxitivo ò a conescion Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de file no seguo</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Pe piaxei, contatta i padroin do scito pe faghe savei de questo problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Erô contegnuo andæto a mâ</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>A pagina che ti veu vedde a no peu ese mostrâ perché gh\'é \'n erô inta trasmiscion di dæti.</p>
+ <ul>
+ <li>Pe piaxei, ciamma o responsabile do scito pe informalo do problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Contegnuo ciantou</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>A pagina che ti veu vedde a no peu ese mostrâ perché gh\'é \'n erô inta trasmiscion di dæti.</p>
+ <ul>
+ <li>Pe piaxei, ciamma o responsabile do scito pe informalo do problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Erô còdifica do contegnuo</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>A pagina che t\'eu vedde a no se peu mostrâ perché a gh\'à \'na comprescion ch\'a no l\'é soportâ.</p>
+ <ul>
+ <li>Pe piaxei, contatta i padroin do scito pe faghe savei de questo problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Indirisso no trovou</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>O navegatô o no riesce a trovâ o server host pe l\'indirisso fornio.</p>
+ <ul>
+ <li>Contròlla che no ghe segge di eroî comme:
+ <strong>ww</strong>.ezenpio.com in cangio de
+ <strong>www</strong>.ezenpio.com.</li>
+ <li>Se ti no riesci a caregâ nisciunn-a pagina, contròla a conescion dæti o Wi-Fi do teu dispoxitivo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Niscinn-a conescion Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ontròlla a tue conescion ò preuva a caregâ a pagina fra quarche momento.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recarega</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Indirisso no valido</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>L\'indirisso che ti me dæto o no va ben. Pe piaxei contròlla che no ghe segge di eroî inta bara di indirissi e preuva torna.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">L\'indirisso o no l\'é valido</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Generalmente i indirissi web en scriti coscì <strong>http://www.ezenpio.com/</strong></li>
+ <li>Amia ben se tu deuvi e bare giuste (prezenpio <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocòllo sconosciuo</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>L\’indirisso o domanda \'n protocòllo (prez. <q>wxyz://</q>) ch\'o navegatô o no conosce, quindi o no peu colegase a-o scito.</p>
+ <ul>
+ <li>Ti ê derê a acede a serviççi moltimediali ò no de testo? Verificâ in sciô scito i requixiti necesai</li>
+ <li>Quarche protocòllo o domanda software esterni ò plugin pe poei ese riconosciui da o navegatô.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">File no trovou</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Peu miga ese che l\'ògetto o segge stæto rinominou, scancelou ò mesciou?</li>
+ <li>Gh\'é quarche erô de òrtografia inte l\'indirisso?</li>
+ <li>Ti ghe l\'æ abasta permissi pe acede a l\'ògetto?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Acesso a-o file negou</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>O peu ese stæto scancelou,mesciou, ò i permissi de acesso a-i file peuan proibine l\'acesso.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Conescion refuâ do-u server proxy</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>O navegatô o l\'é inpostou pe uzâ un server proxy ma o proxy o refua e conescioin.</p>
+ <ul>
+ <li>E inpostaçioin proxy do navegatô en giuste? Contròlla e preuva torna.</li>
+ <li>O proxy permette e conescioin da sta ræ?</li>
+ <li>Ti gh\'æ torna di problemi? Ciamma o teu aministratô da ræ ò fornitô de serviççi internet pe ascistensa.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">No treuvo o proxy</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>O navegatô o l\'é inpostou pe uzâ un server proxy ma o proxy o refua e conescioin.</p>
+ <ul>
+ <li>E inpostaçioin proxy do navegatô en giuste? Contròlla e preuva torna.</li>
+ <li>Ti ê conesso a \'na ræ ativa?</li>
+ <li>Ti gh\'æ torna di problemi? Ciamma o teu aministratô da ræ ò fornitô de serviççi internet pe ascistensa.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de scito con malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>O scito %1$s o l\'é stæto segnalou comme un scito de atacco e o l\'é stæto blocou da-e teu preferense de seguessa.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de scito no no deziderou</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>O scito web %1$s o l\'é segnalou comme \'n scito con do software indeziderou e o l\'é blocou da-e inpostaçioin de seguessa.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Scito pericolozo</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>O scito %1$s o l\'é stæto segnalou comme un scito potensialmente pericolozo e o l\'é stæto blocou da-e teu preferense de seguessa.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Scito inganevole</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>A pagina %1$s a l\'é segnalâ comme \'n scito inganevole a l\'é stæta blocâ da-e teu preferense de seguessa.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Verscion segua do scito no disponibile</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..98a01e372c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lo/strings.xml
@@ -0,0 +1,321 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ລອງ​ອີກ​ຄັ້ງ</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ບໍ່ສາມາດຕອບສະຫນອງຕາມຄຳຮ້ອງຂໍໄດ້</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>ຂໍ້ມູນເພີ່ມເຕີ່ມກ່ຽວກັບບັນຫານີ້ແມ່ນຫຍັງບໍ່ທັນມີເທື່ອໃນຕອນນີ້.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">ການເຊື່ອມຕໍ່ທີປອດໄພຫລົ້ມເຫລວ</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>ບໍ່ສາມາດສະແດງໜ້າທີ່ທ່ານກຳລັງພະຍາຍາມເຂົ້າໄປເບິ່ງໃຫ້ເຫັນໄດ້ເພາະວ່າຄວາມຖືກຕ້ອງຂອງຂໍ້ມູນທີ່ໄດ້ຮັບບໍ່ສາມາດຢັ້ງຢືນໄດ້.</li>
+ <li>ກະລຸນາຕິດຕໍ່ຫາເຈົ້າຂອງເວັບໄຊທເພື່ອແຈ້ງໃຫ້ພວກເຂົາຮູ້ກ່ຽວກັບບັນຫານີ້.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">ການເຊື່ອມຕໍ່ທີປອດໄພຫລົ້ມເຫລວ</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>ປັນຫານີ້ອາດຈະເກີດມາຈາກການຕັ້ງຄ່າຂອງເຊີບເວີ ຫລື ອາດຈະມີຄົນພະຍາຍາມປອມແປງເຊີບເວີ. </li>
+ <li>ຖ້າທ່ານຫາກເຄີຍເຊື່ອມຕໍ່ກັບເຊີບເວີນີ້ໄດ້ມາກ່ອນ, ຂໍ້ຜິດພາດນີ້ອາດຈະເກີດຂື້ນຊົ່ວຄາວ ແລະ ທ່ານສາມາດລອງໃໝ່ອີກຄັ້ງໃນພາຍຫຼັງ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ຂັ້ນສູງ…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>ມີບາງຄົນອາດຈະກຳລັງປອມແປງເວັບໄຊທ ແລະ ທ່ານບໍ່ຄວນຈະເຂົ້າໄປ.</label>
+ <br><br>
+ <label>ເວັບໄຊທຈະຢັ້ງຢືນຕົວຕົນຜ່ານທາງໃບຮັບຮອງ. %1$s ບໍ່ຫນ້າເຊື່ອຖື <b>%2$s</b> ເພາະວ່າຜູ້ອອກໃບຮັບຮອງຂອງພວກເຂົາແມ່ນບໍເປັນທີ່ຮູ້ຈັກ, ໃບຮັບຮອງແມ່ນສ້າງຂື້ນມາເອງ ຫລື ເຊີເວີບໍ່ໄດ້ສົ່ງໃບຮັບຮອງທີ່ຖືກຕ້ອງມາໃຫ້.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ກັບຄືນ (ແນະນຳ)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">ຍອມຮັບຄວາມສ່ຽງ ແລະ ດຳເນີນການຕໍ່</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">ເວັບໄຊທນີ້ຕ້ອງການການເຊື່ອມຕໍ່ທີ່ປອດໄພ.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>ໜ້າ​ທີ່​ເຈົ້າ​ພະ​ຍາ​ຍາມ​ເບິ່ງ​ບໍ່​ສາ​ມາດ​ສະ​ແດງ​ໄດ້​ເນື່ອງ​ຈາກ​ວ່າ​ເວັບ​ໄຊ​ທ​໌​ນີ້​ຕ້ອງ​ການ​ການ​ເຊື່ອມ​ຕໍ່​ທີ່​ປອດ​ໄພ.</li>
+ <li>ບັນຫາແມ່ນເປັນໄປໄດ້ຫຼາຍທີ່ສຸດກັບເວັບໄຊທ໌, ແລະບໍ່ມີຫຍັງທີ່ເຈົ້າສາມາດແກ້ໄຂມັນໄດ້.</li>
+ <li>ທ່ານ​ສາ​ມາດ​ແຈ້ງ​ໃຫ້​ຜູ້​ຄວບ​ຄຸມ​ຂອງ​ເວັບ​ໄຊ​ທ​໌​ກ່ຽວ​ກັບ​ບັນ​ຫາ​ໄດ້.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">ຂັ້ນສູງ…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ມີນະໂຍບາຍຄວາມປອດໄພທີ່ເອີ້ນວ່າ HTTP Strict Transport Security (HSTS), ຊຶ່ງຫມາຍຄວາມວ່າ <b>%2$s</b> ສາມາດເຊື່ອມຕໍ່ກັບມັນໄດ້ຢ່າງປອດໄພເທົ່ານັ້ນ. ທ່ານບໍ່ສາມາດເພີ່ມຂໍ້ຍົກເວັ້ນເພື່ອເຂົ້າເບິ່ງເວັບໄຊນີ້ໄດ້.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">ກັບຄືນ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">ການເຊື່ອມຕໍ່ຖືກລົບກວນ</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>ບຣາວເຊີໄດ້ສຳເລັດການເຊື່ອຕໍ່ແລ້ວ, ແຕ່ວ່າການເຊື່ອມຕໍ່ໄດ້ຖືກຕັດລະຫວ່າງການສົ່ງຂໍ້ມູນ. ກະລຸນາລອງຫໃ່ອີກຄັ້ງຫນຶ່ງ.</p>
+ <ul>
+ <li>ເວັບໄຊທອາດຈະໃຊ້ງານບໍ່ໄດ້ຊົ່ວຄາວ ຫລື ອາດຈະມີຄົນເຂົ້າຫລາຍເກີນໄປ. ລອງໃຫມ່ອີກຄັ້ງໃນອີກສອງສາມນາທີຂ້າງຫນ້າ.</li>
+ <li>ຖ້າຫາກວ່າທ່ານບໍ່ສາມາດໂຫລດຫນ້າເວັບໃດໆໄດ້ເລີຍໃຫ້ທ່ານກວດປະລິມານການນຳໃຊ້ຂໍ້ມູນ ຫລື ການເຊື່ອມຕໍ່ກັບ Wi-Fi ໃນອຸປະກອນຂອງທ່ານຄືນໃຫມ່.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">ການເຊື່ອມຕໍ່ໄດ້ຫມົດເວລາແລ້ວ</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>ເວັບໄຊທທີ່ທ່ານຮ້ອງຂໍບໍ່ສາມາດດຳເນີນການໄດ້ ແລະ ບຣາວເຊີໄດ້ຍຸດການລໍຖ້າຄຳຮ້ອງຂໍດັ່ງກ່າວແລ້ວ.</p>
+ <ul>
+ <li>ອາດຈະເປັນນຳເຊີເວີອາດປະສົບກັບບັນຫາທີ່ມີຜູ້ເຂົ້ານຳໃຊ້ສຸງ ຫລື ໄຟຟ້າດັບຊົ່ວຄາວ. ໃຫ້ລອງເຂົ້າໃຫມ່ອີກຄັ້ງໃນພາຍຫລັງ.</li>
+ <li>ທ່ານສາມາດເຂົ້າເວັບໄຊທອື່ນໄດ້ບໍ່? ໃຫ້ກວດເບິງການເຊື່ອມຕໍ່ເນັດເວີກຂອງຄອມພິວເຕີຄືນ.</li>
+ <li>ເນັດເວີກຂອງທ່ານມີໄຟວໍ ຫລື ພັອກຊີຄັ້ນໄວ້ບໍ່? ການຕັ້ງຄ່າທີ່ຜິດພາດອາດຈະເຮັດໃຫ້ລົບກວນການເຂົ້າເວັບໄດ້.</li>
+ <li>ຫຍັງຄົງມີບັນຫາຢູ່ບໍ່? ໃຫ້ປຶກສາກັບຜູ້ຄຸ້ມຄອງລະບົບເນັດເວີກຂອງທ່ານ ຫລື ຜູ້ໃຫ້ບໍລິການອິນເຕີເນັດສຳລັບການຊ່ວຍເຫລືອ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">ບໍ່​ສາ​ມາດ​ທີ່​ຈະ​ເຊື່ອມ​ຕໍ່</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>ເວັບໄຊທອາດຈະໃຊ້ງານບໍ່ໄດ້ຊົ່ວຄາວ ຫລື ອາດຈະມີຄົນເຂົ້າຫລາຍເກີນໄປ. ລອງໃຫມ່ອີກຄັ້ງໃນອີກສອງສາມນາທີຂ້າງຫນ້າ.</li>
+ <li>ຖ້າຫາກວ່າທ່ານບໍ່ສາມາດໂຫລດຫນ້າເວັບໃດໆໄດ້ເລີຍໃຫ້ທ່ານກວດປະລິມານການນຳໃຊ້ຂໍ້ມູນ ຫລື ການເຊື່ອມຕໍ່ກັບ Wi-Fi ໃນອຸປະກອນຂອງທ່ານຄືນໃຫມ່.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">ການຕອບສະຫນອງທີ່ບໍ່ຄາດຄິດຈາກເຊີເວີ</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>ເວັບໄຊທຕອບສະຫນອງຕໍຄຳຮ້ອງຂໍຂອງເນັດເວີກດ້ວຍວິທີທີ່ບໍຖືກຕ້ອງ ແລະ ບຣາວເຊີບໍ່ສາມາດເຂົ້າໄປຕໍໄດ້.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ຫນ້າເວັບນີ້ມີການປ່ຽນເສັ້ນທາງທີ່ບໍ່ຖືກຕ້ອງ</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>ບຣາວເຊີໄດ້ຢຸດພະຍາຍາມໃນການຮ້ອງຂໍລາຍການ. ເວັບໄຊທໄດ້ປ່ຽນເສັ້ນທາງຄຳຮ້ອງຂໍໄປໃນທາງທີ່ບໍມີການສີ້ນສຸດ. </p>
+ <ul>
+ <li>ທ່ານໄດ້ປິດ ຫລື ບັອກຄຸກກີ້ທີ່ຕ້ອງການໂດຍເວັບໄຊທນີ້ບໍ?</li>
+ <li>ຖ້າວ່າການຍອມຮັບຄຸກກີ້ຂອງເວັບໄຊທບໍ່ໄດ້ແກ້ໄຂບັນຫາ ມັນອາດຈະເປັນນຳບັນຫາໃນການຕັ້ງຄ່າເຊີເວີ ແລະ ບໍ່ໄດ້ເປັນນຳຄອມພິວເຕີຂອງທ່ານ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ໂໝດອັອບໄລ</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>ບຣາວເຊີກຳລັງເຮັດວຽກໃນໂຫມດອັອບລາຍ ແລະ ບໍ່ສາມາດເຊື່ອມຕໍ່ໄປຫາລາຍການທີ່ຮ້ອງຂໍໄດ້.</p>
+ <ul>
+ <li>ຄອມພິວເຕີໄດ້ເຊື່ອມຕໍ່ກັບເນັດເວີກຢູ່ບໍ?</li>
+ <li>ກົດ “ລອງໃຫມ່ອີກຄັ້ງ” ເພື່ອປ່ຽນໄປໂຫມດອອນລາຍ ແລະ ໂຫລດຫນ້າໃຫມ່ອີກຄັ້ງ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">ພອດຖືກຈຳກັດດ້ວຍເຫດຜົນດ້ານຄວາມປອດໄພ</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>ທີ່ຢູ່ນີ້ຮ້ອງຂໍພອດທີ່ລະບຸຊັດເຈນ (ເຊັ່ນ: <q>mozilla.org:80</q> ສຳລັບພອດ 80 ໃນ mozilla.org) ປົກກະຕິແລ້ວໃຊ້ສຳລັບ <em>ອື່ນໆ</em> ຫຼາຍກ່ອນການທ່ອງເວັບ. ບຣາວເຊີໄດ້ຍົກເລີກຄຳຮ້ອງຂໍດັ່ງກ່າວເພື່ອປົກປ້ອງທ່ານ ແລະ ຄວາມປອດໄພ.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">ການເຊື່ອມຕໍ່ຖືກຕັດ</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>ເນັດເວີກລີ້ງໄດ້ຮັບການຂັດຈັງຫວະໃນຂະນະທີ່ແລກປ່ຽນຂໍ້ມູນການເຊື່ອມຕໍ. ກະລຸນາລອງໃຫມ່ອີກຄັ້ງ.</p>
+ <ul>
+ <li>ເວັບໄຊທອາດຈະບໍ່ສາມາດໃຊ້ງານໄດ້ຊົ່ວຄາວ ຫລື ເຂົ້າບໍ່ໄດ້. ໃຫ້ລອງເຂົ້າໃຫມ່ໃນອີກ 2-3 ນາທີ.</li>
+ <li>ຖ້າຫາກວ່າທ່ານຍັງບໍສາມາດໂຫລດຫນ້າເວັບໄດ້ ໃຫ້ລອງກວດເບິງອິນເຕີເນັດ ຫລື ການເຊື່ອມຕໍ Wi-Fi ໃນອຸປະກອນຂອງທ່ານ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">ປະເພດເອກະສານທີ່ບໍ່ປອດໄພ</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>ກະລູນາຕິດຕໍ່ຫາເຈົ້າຂອງເວັບໄຊທເພື່ອແຈ້ງບັນຫານີ້ໃຫ້ພວກເຂົາຮູ້.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">ຂໍ້ຜິດພາດເນື້ອຫາທີ່ເສີຍຫາຍ</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>ຫນ້າເວັບທີ່ທ່ານກຳລັງເຂົ້າໄປເບິງບໍ່ສາມາດສະແດງໄດ້ເພາະວ່າມັນເກີດມີຂໍ້ຜິດພາດໃນການສົ່ງຂໍ້ມູນ.</p>
+ <ul>
+ <li>ກະລຸນາແຈ້ງບັນຫານີ້ໃຫ້ເຈົ້າຂອງເວັບໄຊທຮູ້.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">ເນື້ອຫາຂັດຂ້ອງ</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>ບໍ່ສາມາດສະແດງຫນ້າເວັບທີ່ທ່ານກຳລັງພະຍາຍາມເຂົ້າໄປເບິງໄດ້ເນື່ອງຈາກກວດພົບຂໍ້ຜິດພາດໃນການສົ່ງຂໍ້ມູນ.</p>
+ <ul>
+ <li>ກະລຸນາຕິດຕໍ່ຫາເຈົ້າຂອງເວັບໄຊທເພື່ອແຈ້ງບັນຫານີ້.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">ຂໍ້ຜິດພາດການເຂົ້າລະຫັດເນື້ອຫາ</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>ຫນ້າເວັບທີ່ທ່ານກຳລັງເຂົ້າໄປເບິງບໍ່ສາມາດສະແດງໄດ້ເພາະວ່າມັນໃຊ້ຮູບແບບການບີບອັດຂໍ້ມູນທີ່ບໍ່ຖືກຕ້ອງ ຫລື ບໍ່ໄດ້ຮັບການຊັບພອດ.</p>
+ <ul>
+ <li>ກະລຸນາແຈ້ງບັນຫານີ້ໃຫ້ເຈົ້າຂອງເວັບໄຊທຮູ້.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ບໍ່ພົບທີ່ຢູ່</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>ບຣາວເຊີອາດຈະບໍ່ສາມາດຊອກຫາທີ່ຢູ່ຂອງໂຮສທີ່ລະບຸມາ.</p>
+ <ul>
+ <li>ກວດຄືນເບິງວ່າຂຽນຊື່ທີ່ຢູ່ຖືກຕ້ອງແລ້ວບໍເຊັ່ນວ່າ:
+ <strong>ww</strong>.example.com ແທນທີ່ຈະເປັນ
+ <strong>www</strong>.example.com.</li>
+ <li>ຖ້າຫາກວ່າທ່ານຍັງບໍສາມາດໂຫລດຫນ້າເວັບໄດ້ ໃຫ້ລອງກວດເບິງອິນເຕີເນັດ ຫລື ການເຊື່ອມຕໍ Wi-Fi ໃນອຸປະກອນຂອງທ່ານ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ບໍ່ມີການເຊື່ອມຕໍ່ກັບອິນເຕີເນັດ</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ກວດເບິງເນັດເວີກຂອງທ່ານຄືນ ຫລື ລອງໂຫລດຫນ້າເວັບຄືນໃຫມ່ໃນອີກ 2-3 ນາທີ.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ໂຫລດຄືນໃຫມ່</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ທີ່ຢູ່ບໍ່ຖືກຕ້ອງ</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>ຮູບແບບຂອງທີ່ຢູ່ທີໃຫ້ມາແມ່ນບໍ່ຖືກຕ້ອງ. ກະລຸນາກວດເບິງທີ່ຢູ່ໃນແຖບທີ່ຕັ້ງເພື່ອຊອກຫາຂໍ້ຜິດພາດ ແລະ ລອງໃຫມ່ອີກຄັ້ງ.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ທີ່ຢູ່ບໍ່ຖືກຕ້ອງ</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>ທີ່ຢູ່ຂອງເວັບມັກຈະຂຽນເປັນ <strong>http://www.example.com/</strong></li>
+ <li>ກວດເບິງໃຫ້ຫມັ້ນໃຈວ່າທ່ານໄດ້ໃຊ້ເຄືອງຫມາຍສະແລັດຖືກຕ້ອງແລ້ວ ( <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">ໂປຼໂຕຄໍທີ່ບໍ່ຮູ້ຈັກ</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>ທີ່ຢູ່ລະບຸໂປຣໂຕຄໍ (ເຊັ່ນ: <q>wxyz://</q>) ບຣາວເຊີບໍ່ຮັບຮູ້, ສະນັ້ນ ບຣາວເຊີບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເວັບໄຊທ໌ໄດ້ຢ່າງຖືກຕ້ອງ.</p>
+ <ul>
+ <li>ທ່ານກຳລັງພະຍາຍາມເຂົ້າເຖິງມັນຕິມີເດຍ ຫຼືບໍລິການອື່ນໆທີ່ບໍ່ແມ່ນຂໍ້ຄວາມບໍ? ກວດເບິ່ງເວັບໄຊສຳລັບຄວາມຕ້ອງການເພີ່ມເຕີມ.</li>
+ <li>ບາງໂປຣໂຕຄໍອາດຈະຕ້ອງການຊອບແວພາກສ່ວນທີສາມ ຫຼື plugins ກ່ອນທີ່ຕົວທ່ອງເວັບສາມາດຮັບຮູ້ພວກມັນໄດ້.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ບໍ່ພົບໄຟລ໌ນີ້</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>ລາຍການດັ່ງກ່າວຖືກປ່ຽນຊື່, ລຶບອອກ ຫຼື ຍ້າຍອອກບໍ?</li>
+ <li>ມີການສະກົດຄໍາ, ຕົວພິມໃຫຍ່, ຫຼືການພິມຜິດອື່ນໆຢູ່ໃນນີ້ບໍ?</li>
+ <li>ສິດອະນຸຍາດຂອງທ່ານພຽງພໍກັບລາຍການທີ່ຮ້ອງຂໍບໍ?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ການເຂົ້າເຖິງໄຟລ໌ຂໍ້ມູນໄດ້ຖືກປະຕິເສດ</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul> <li>ມັນອາດຈະຖືກລຶບອອກໄປແລ້ວ, ຍ້າຍໄປໄວ້ຢູ່ບ່ອນອື່ນແລ້ວ ຫລື ທ່ານອາດບໍ່ມີສິດເຂົ້າເຖິງຟາຍຂໍ້ມູນນີ້.</li> </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ພັອກຊີເຊີເວີໄດ້ປະຕິເສດການເຊື່ອມຕໍ່</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>ຕົວ​ທ່ອງ​ເວັບ​ໄດ້​ຖືກ​ຕັ້ງ​ຄ່າ​ໃຫ້​ໃຊ້​ເຊີບ​ເວີ​ພຣັອກຊີ, ແຕ່​ພຣັອກຊີ​ໄດ້​ປະ​ຕິ​ເສດ​ການ​ເຊື່ອມ​ຕໍ່.</p>
+ <ul>
+ <li>ການກຳນົດຄ່າພຣັອກຊີຂອງບຣາວເຊີຖືກຕ້ອງບໍ? ກວດເບິ່ງການຕັ້ງຄ່າແລ້ວລອງໃໝ່ອີກຄັ້ງ.</li>
+ <li>ບໍລິການພຣັອກຊີອະນຸຍາດການເຊື່ອມຕໍ່ຈາກເຄືອຂ່າຍນີ້ບໍ?</li>
+ <li>ຍັງມີບັນຫາຢູ່ບໍ? ປຶກສາຜູ້ເບິ່ງແຍງລະບົບເຄືອຂ່າຍ ຫຼືຜູ້ໃຫ້ບໍລິການອິນເຕີເນັດຂອງທ່ານເພື່ອຂໍຄວາມຊ່ວຍເຫຼືອ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ບໍ່ພົບພລັອກຊີເຊີເວີ</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>ຕົວ​ທ່ອງ​ເວັບ​ໄດ້​ຖືກ​ຕັ້ງ​ຄ່າ​ໃຫ້​ໃຊ້​ເຊີບ​ເວີ​ພຣັອກ​ຊີ, ແຕ່​ບໍ່​ສາ​ມາດ​ຊອກ​ຫາ​ຕົວ​ແທນ​ໄດ້.</p>
+ <ul>
+ <li>ການກຳນົດຄ່າພຣັອກຊີຂອງບຣາວເຊີຖືກຕ້ອງບໍ? ກວດເບິ່ງການຕັ້ງຄ່າແລ້ວລອງໃໝ່ອີກຄັ້ງ.</li>
+ <li>ອຸປະກອນເຊື່ອມຕໍ່ກັບເຄືອຂ່າຍທີ່ໃຊ້ຢູ່ບໍ?</li>
+ <li>ຍັງມີບັນຫາຢູ່ບໍ? ປຶກສາຜູ້ເບິ່ງແຍງລະບົບເຄືອຂ່າຍ ຫຼືຜູ້ໃຫ້ບໍລິການອິນເຕີເນັດຂອງທ່ານເພື່ອຂໍຄວາມຊ່ວຍເຫຼືອ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">ປັນຫາເວັບໄຊທ Malware</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>ເວັບໄຊທທີ່ %1$s ໄດ້ຖືກລາຍງານວ່າເປັນເວັບອັນຕະລາຍ ແລະ ໄດ້ຖືກບັອກໂດຍອີງຈາກການຕັ້ງຄ່າຄວາມປອດໄພຂອງທ່ານ.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ບັນຫາໃນເວັບໄຊທທີ່ບໍ່ເພິງປະສົງ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>ເວັບໄຊທທີ່ %1$s ໄດ້ຖືກລາຍງານວ່າເປັນເວັບທີ່ໃຫ້ບໍລິການຊັອບແວທີ່ອັນຕະລາຍ ແລະ ໄດ້ຖືກບັອກໂດຍອີງຈາກການຕັ້ງຄ່າຄວາມປອດໄພຂອງທ່ານ.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ເວບໄຊທທີ່ເປັນອັນຕະລາຍ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>ເວັບໄຊທທີ່ %1$s ໄດ້ຖືກລາຍງານວ່າເປັນເວັບທີ່ອາດຈະເປັນອັນຕະລາຍ ແລະ ໄດ້ຖືກບັອກໂດຍອີງຈາກການຕັ້ງຄ່າຄວາມປອດໄພຂອງທ່ານ.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">ບັນຫາເວັບໄຊທປອມແປງ</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>ຫນ້າເວັບນີ້ທີ່ %1$s ໄດ້ຖືກລາຍງານວ່າເປັນເວັບປອມແປງ ແລະ ໄດ້ຖືກບັອກໂດຍອີງຈາກການຕັ້ງຄ່າຄວາມປອດໄພຂອງທ່ານ.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">ເວັບໄຊທທີ່ປອດໄພຍັງບໍ່ພ້ອມໃຫ້ໃຊ້ງານ</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[ທ່ານ​ໄດ້​ເປີດ​ໃຊ້​ງານໂຫມດ HTTPS-Only ເພື່ອ​ຄວາມ​ປອດ​ໄພ​ທີ່​ຫຼາຍຂື້ນ ແລະ HTTPS ເວີ​ຊັນ <em>%1$s</em> ຍັງບໍພ້ອມໃຫ້ໃຊ້ງານ.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">ສືບຕໍ່ໄປຫາເວັບໄຊທທີ່ນຳໃຊ້ HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..998dcd440b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-lt/strings.xml
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Bandyti dar kartą</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Nepavyko atlikti užklausos</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Šiuo metu apie šią problemą ar klaidą daugiau informacijos nėra.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Saugaus ryšio užmegzti nepavyko</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Svetainė, kurią bandote atverti, negali būti įkelta, nes nepavyko patikrinti gaunamų duomenų autentiškumo.</li>
+ <li>Prašome informuoti svetainės kūrėjus apie šią problemą.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Saugaus ryšio užmegzti nepavyko</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Tai gali būti serverio konfigūracijos problema, arba kažkieno bandymas apsimesti serveriu.</li>
+ <li>Jei anksčiau esate sėkmingai prisijungę prie šio serverio, ši klaida gali būti laikina, tad pabandykite vėliau.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Papildomai…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Kažkas gali bandyti apsimesti svetaine, tad patartume jos neatverti.</label>
+ <br><br>
+ <label>Svetainės patvirtina savo tapatumą pateikdamos liudijimus. „%1$s“ nepasitikti <b>%2$s</b>, nes liudijimą išdavusi įstaiga nėra žinoma, liudijimas yra pasirašytas pačių svetainės kūrėjų, arba serveris neperduoda teisingų tarpinių liudijimų.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Grįžti (rekomenduojama)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Priimti riziką ir tęsti</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Šiai svetainei reikalingas saugus ryšys.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Tinklalapis, kurį bandote peržiūrėti, negali būti parodytas, nes šiai svetainei reikalingas saugus ryšys.</li>
+ <li>Greičiausiai problema kyla dėl svetainės, ir jūs nieko negalite padaryti, kad ją išspręstumėte.</li>
+ <li>Apie problemą galite pranešti svetainės prižiūrėtojui.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Papildomai…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> turi saugos nuostatą, vadinamą „HTTP Strict Transport Security“ (HSTS), ir reiškia, kad <b>„%2$s“</b> gali jungtis tik saugiu ryšiu. Jūs negalite sukurti išimties, kad aplankytumėte šią svetainę. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Eiti atgal</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Ryšys buvo nutrauktas</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Naršyklė sėkmingai užmezgė ryšį, tačiau prisijungimas buvo nutrauktas perduodant duomenis. Bandykite dar kartą.</p>
+ <ul>
+ <li>Svetainė gali būti laikinai nepasiekiama arba turi daug lankytojų. Pabandykite dar kartą šiek tiek vėliau.</li>
+ <li>Jeigu negalite įkelti jokio tinklalapio, patikrinkite savo įrenginio mobilųjį arba belaidį ryšį.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ryšiui skirtas laikas baigėsi</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Svetainė neatsakė į bandymą užmegzti ryšį, o naršyklės laukimo limitas baigėsi.</p>
+ <ul>
+ <li>Svetainė gali turėti didelį lankytojų srautą, arba laikinų nesklandumų. Prašome bandyti vėliau.</li>
+ <li>Jei nepavyksta įkelti ir kitų tinklalapių, patikrinkite įrenginio ryšį su tinklu.</li>
+ <li>Jei jūsų įrenginys ar tinklas yra apsaugotas užkarda, arba jungiamasi per įgaliotąjį serverį, tai įsitikinkite, kad parinktos tinkamos nuostatos.</li>
+ <li>Jei nesklandumo pašalinti nepavyksta, kreipkitės į tinklo administratorių ar interneto paslaugų teikėją.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Nepavyko užmegzti ryšio</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Svetainė gali būti laikinai nepasiekiama. Pabandykite šiek tiek vėliau.</li>
+ <li>Jeigu nepavyksta įkelti jokio tinklalapio, patikrinkite savo įrenginio duomenų arba belaidį ryšį.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Netikėtas serverio atsakas</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Svetainė netikėtu būdu atsakė į tinklo užklausą, tad naršyklė negali tęsti darbo.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Netinkamas tinklalapio peradresavimas</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Naršyklė nutraukė ryšį, kadangi svetainė cikliškai peradresuoja užklausas sau pačiai.</p>
+ <ul>
+ <li>Ši klaida galėjo įvykti dėl to, kad naršyklė nepriima svetainės slapukų.</li>
+ <li>Jei leidus priimti svetainės slapukus nesklandumas išlieka, tai jo priežastis gali būti ne jūsų kompiuteryje, o serverio sąrankoje.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Dirbama neprisijungus prie tinklo</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Naršyklė yra atjungta nuo tinklo ir negali užmegzti ryšio su užklaustu objektu.</p>
+ <ul>
+ <li>Ar jūsų įrenginys prijungtas prie veikiančio tinklo?</li>
+ <li>Spustelėkite „Bandyti dar kartą“, kad būtų prisijungta prie tinklo ir pabandyta įkelti tinklalapį iš naujo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Saugumo tikslais apribota prieiga prie šio prievado</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Adreso, prie kurio bandoma prisijungti, nurodytas numeris prievado (pvz., <q>mozilla.org:80</q> – mozilla.org su prievadu 80), paprastai naudojamo ne naršymui saityne, o <em>kitiems</em> tikslams. Kad užtikrintų jūsų saugumą, naršyklė atmetė šią užklausą.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ryšys nutrūko</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Prisijungimas buvo nutrauktas bandant užmegzti ryšį. Bandykite dar kartą.</p>
+ <ul>
+ <li>Svetainė gali būti laikinai nepasiekiama, arba turi daug lankytojų. Pabandykite dar kartą šiek tiek vėliau.</li>
+ <li>Jeigu negalite įkelti jokio tinklalapio, patikrinkite savo įrenginio mobilųjį arba belaidį ryšį.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Nesaugus failo tipas</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Praneškite apie šią problemą svetainės prižiūrėtojams.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Klaida: duomenys pažeisti</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Tinklalapio, kurį bandote atverti, parodyti negalima, nes perduodant duomenis įvyko klaida.</p>
+ <ul>
+ <li>Praneškite apie šią problemą svetainės prižiūrėtojams.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Turinio problema</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Tinklalapio, kurį bandote atverti, parodyti negalima, nes perduodant duomenis įvyko klaida,</p>
+ <ul>
+ <li>Praneškite apie šią problemą svetainės prižiūrėtojams.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Kodavimo klaida</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Tinklalapis, kurį bandote atverti, negali būti parodytas, nes naudoja nežinomą arba nepalaikomą suspaudimo būdą.</p>
+ <ul>
+ <li>Praneškite apie šią problemą svetainės prižiūrėtojams.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Nerastas serveris</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Naršyklei nepavyko rasti serverio nurodytu adresu.</p>
+ <ul>
+ <li>Patikrinkite, ar rinkdami adresą nepadarėte klaidų, pavyzdžiui,
+ <strong>ww</strong>.example.com vietoje
+ <strong>www</strong>.example.com.</li>
+ <li>Jei nepavyksta įkelti ir kitų tinklalapių, patikrinkite savo įrenginio duomenų ar belaidį ryšį.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nėra ryšio su internetu</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Patikrinkite savo tinklo ryšį, arba pabandykite įkelti tinklalapį iš naujo.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Įkelti iš naujo</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Neteisingas adresas</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Neteisingas adreso formatas. Patikrinkite adresą adreso lauke ir bandykite dar kartą.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Neteisingas adresas</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Svetainių adresai dažniausiai rašomi taip: <strong>http://www.example.com/</strong></li>
+ <li>Įsitikinkite, kad naudojate pasviruosius brūkšnius (į priekį): <strong>/</strong>.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Nežinomas protokolas</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Adrese nurodytas protokolas (pvz., <q>wxyz://</q>) naršyklei nežinomas, todėl svetainės įkėlimas nutrauktas.</p>
+ <ul>
+ <li>Jei bandote prisijungti prie įvairialypės terpės išteklių, patikrinkite, ar nėra tam specialių reikalavimų.</li>
+ <li>Kai kurių protokolų atvėrimui gali būti reikalinga papildoma programinė įranga arba naršyklės papildiniai.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Nerastas failas</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Patikrinkite, ar failas nebuvo pervadintas, perkeltas, ar pašalintas.</li>
+ <li>Patikrinkite, ar failo pavadinime nėra rinkimo klaidų, pvz., didžiosios raidės pakeistos mažosiomis.</li>
+ <li>Patikrinkite, ar turite leidimą šį failą atverti.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Priėjimas prie failo uždraustas</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Jis galėjo būti pašalintas, perkeltas, arba priėjimą riboja failo leidimai.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Įgaliotasis serveris atmetė ryšį</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Naršyklės nuostatose nurodytas įgaliotasis serveris, bet jis atmetė ryšį.</p>
+ <ul>
+ <li>Patikrinkite įgaliotojo serverio nuostatas ir bandykite vėl.</li>
+ <li>Įsitikinkite, ar įgaliotasis serveris leidžia prisijungimus iš jūsų tinklo.</li>
+ <li>Jei nesklandumo pašalinti nepavyksta, kreipkitės į tinklo prižiūrėtoją ar interneto paslaugų teikėją.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Nerastas įgaliotasis serveris</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Naršyklės nuostatose nurodytas įgaliotasis serveris, tačiau jo nepavyko rasti.</p>
+ <ul>
+ <li>Patikrinkite įgaliotojo serverio nuostatas ir bandykite vėl.</li>
+ <li>Įsitikinkite, ar įrenginys yra prijungtas prie veikiančio tinklo.</li>
+ <li>Jei nesklandumo pašalinti nepavyksta, kreipkitės į tinklo prižiūrėtoją ar interneto paslaugų teikėją.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema dėl kenkėjiškos svetainės</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Yra pranešta, jog svetainė adresu %1$s yra kenkėjiška, tad ji užblokuota remiantis jūsų saugumo nuostatomis.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema dėl nepageidaujamos svetainės</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Yra pranešta, jog svetainė adresu %1$s yra kenkėjiška, tad ji užblokuota remiantis jūsų saugumo nuostatomis.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema dėl žalingos svetainės</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Yra pranešta, jog svetainė adresu %1$s yra kenkėjiška, tad ji užblokuota remiantis jūsų saugumo nuostatomis.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema dėl apgaulingos svetainės</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Yra pranešta, jog svetainė adresu %1$s yra apgaulinga, tad ji užblokuota remiantis jūsų saugumo nuostatomis.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Saugi svetainė nepasiekiama</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Įjungėte tik HTTPS veikseną išplėstam saugumui, o <em>%1$s</em> HTTPS versija nėra pasiekiama.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Eiti į HTTP svetainę</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..c22fdad0a3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-mix/strings.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Ki’tsàa tuku</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Ma ku tsinuña</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Koo tu"un tsa la vaa yo.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Mani^ku tyiitai^ña</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Kue ku snai página kunu kuntyeu tsiniñu kuntyeu la ntyau.</li>
+ <li>Katu"un nu sto"o web takua na kuntyii ntyi vas yee.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Conexión segura fallida</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Ntyityí</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+        <label>Alguien podría estar intentando imitar el sitio y no deberías continuar. </label>
+        <br><br>
+        <label>Los sitios web prueban su identidad mediante certificados. %1$s no confía en <b>%2$s</b> porque el emisor del certificado es desconocido, el certificado está autofirmado o el servidor no envía los certificados intermedios correctos.</label>
+    ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Ntyiko
+(recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Vaa kitsa</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Stini ñu´u iin conexión vaa.</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Satà</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Ma ku kitsau</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>El navegador se conectó exitosamente, pero se interrumpió la conexión mientras se transfería la información. Vuelva a intentarlo.</p>
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelva a intentarlo en unos minutos.</li>
+ <li>Si no puede cargar ninguna página, revise la conexión wifi o de datos de su dispositivo móvil.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Koo ña kunu kuntyeu</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.</p>
+ <ul>
+ <li>¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelva a intentarlo más tarde.</li>
+ <li>¿No puede navegar por otros sitios? Compruebe la conexión de red del equipo.</li>
+ <li>¿Su red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta podría interferir con la navegación web.</li>
+ <li>¿Aun con problemas? Consulte con su administrador de red o proveedor de Internet para obtener asistencia técnica.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Kue ku tyitaiña</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelva a intentarlo en unos minutos.</li>
+ <li>Si no puede cargar ninguna página, revise la conexión wifi o de datos de su dispositivo móvil.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Respuesta inesperada del servidor, y el navegador no puede continuar</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>El sitio respondió a la solicitud de red de una forma inesperada y el navegador no puede continuar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo koo conexión</string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Puerto restringido, por razones de seguridad</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ndu tsaa conexión</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tutu vaá</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>La página que está intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.</p>
+ <ul>
+ <li>Póngase en contacto con los propietarios del sitio web para informarles de este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Maku kuntyeu </string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Kue vaa yee la kunu kuntyeu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Kue ni ndanii dirección</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ma ku kivu nu Internet</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Kitsa tuku</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Va^a dirección</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Kue vaa yee ña ntyau </string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Kòo tutu ndukuku</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Kue ku kunaku tutu ma ku kuntyeu ña</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Pàgina vaa</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..b0434e46ef
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ml/strings.xml
@@ -0,0 +1,218 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">വീണ്ടും ശ്രമിക്കുക</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">അഭ്യർത്ഥന പൂർത്തിയാക്കാൻ കഴിയില്ല</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>ഈ പിശകിനെയോ അല്ലെങ്കില്‍ പ്രശ്നത്തെയോ സംബന്ധിച്ചുള്ള കൂടുതല്‍ വിവരങ്ങള്‍ നിലവില്‍ ലഭ്യമല്ല.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">സുരക്ഷിതമായ കണക്ഷന്‍ പരാജയപ്പെട്ടിരിക്കുന്നു</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>ലഭ്യമായ ഡേറ്റയുടെ ആധികാരികത ഉറപ്പാക്കുവാന്‍ സാധ്യമാകാത്തതിനാൽ, നിങ്ങള്‍ കാണുവാന്‍ ശ്രമിക്കുന്ന താള്‍ ഇപ്പോള്‍ ലഭ്യമല്ല.</li>
+ <li>ഇതു് സംബന്ധിച്ചുള്ള വിവരങ്ങള്‍ ദയവായി വെബ്‍സൈറ്റ് ഉടമസ്ഥരെ അറിയിക്കുക.</li>
+  </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">സുരക്ഷിതമായ കണക്ഷന്‍ പരാജയപ്പെട്ടു</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>ഇത്തിന്റെ കാരണം സെര്‍വറിന്റെ ക്രമീകരണത്തിലുള്ള പിശകോ അല്ലെങ്കില്‍ ആരെങ്കിലും ഈ സെര്‍വറാണെന്നു തെറ്റിദ്ധരിപ്പിയ്ക്കാന്‍ ശ്രമിയ്ക്കുന്നതോ ആണ്.</li>
+<li>നിങ്ങള്‍ ഇതിനു് മുമ്പു് ഈ സെര്‍വറിലേക്കു് വിജയകരമായി കണക്ട് ചെയ്തിട്ടുണ്ടെങ്കില്‍, ഇതു് വെറും ഒരു താല്‍ക്കാലിക പ്രശ്നമാവാം, അതുകൊണ്ടു് അല്പസമയം കഴിഞ്ഞ് ശ്രമിക്കുക.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">സങ്കീർണ്ണമായവ…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>ആരോ ആൾമാറാട്ടം നടത്താൻ ശ്രമിക്കുന്നതിനാൽ നിങ്ങൾ പോജിൽ തുടരരുത്.</label>
+ <br><br>
+ <label>വെബ്‌സൈറ്റുകൾ സർട്ടിഫിക്കറ്റുകൾ വഴി അവരുടെ ഐഡന്റിറ്റി തെളിയിക്കുന്നു. %1$s നെ<b>%2$s</b> വിശ്വസിയ്ക്കുന്നില്ല, കാരണം അതിന്റെ സർ‌ട്ടിഫിക്കറ്റ് നൽ‌കുന്നയാൾ‌ ഒന്നുകിൽ അജ്ഞാതനാണ് അല്ലെങ്കിൽ സർ‌ട്ടിഫിക്കറ്റ് സ്വയം ഒപ്പിട്ടതാണ്, അതുമല്ലെങ്കിൽ‌ സെർ‌വർ‌ ശരിയായ ഇന്റർ‌മീഡിയറ്റ് സർ‌ട്ടിഫിക്കറ്റുകൾ‌ അയയ്‌ക്കുന്നില്ല.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">തിരികെ പോകുക (ശുപാർശചെയ്യുന്നു)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">അപകടസാധ്യത സ്വീകരിച്ച് തുടരുക</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">കണക്ഷൻ തടസ്സപ്പെട്ടു</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p> ബ്രൗസർ വിജയകരമായി കണക്റ്റുചെയ്തു, പക്ഷേ വിവരങ്ങൾ കൈമാറുമ്പോൾ കണക്ഷൻ തടസ്സപ്പെട്ടു. ദയവായി വീണ്ടും ശ്രമിക്കുക.</p>
+ <ul>
+ <li>സൈറ്റ് താൽ‌ക്കാലികമായി ലഭ്യമല്ല അല്ലെങ്കിൽ‌ വളരെ തിരക്കിലാണ്. അല്പ സമയത്തിന് ശേഷം വീണ്ടും ശ്രമിക്കുക.</li>
+ <li>നിങ്ങൾക്ക് പേജുകളൊന്നും ലോഡുചെയ്യാൻ കഴിയുന്നില്ലെങ്കിൽ നിങ്ങളുടെ ഉപകരണത്തിന്റെ ഡാറ്റയോ വൈഫൈ കണക്ഷനോ പരിശോധിക്കുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">കണക്ഷന്റെ സമയം കഴിഞ്ഞിരിക്കുന്നു</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>ആവശ്യപ്പെട്ട സൈറ്റ് കണക്ഷനുള്ള മറുപടി നല്‍കിയിട്ടില്ല, ബ്രൌസര്‍ മറുപടിയ്ക്കായി കാത്തിരിക്കുന്നത് നിർത്തിയിരിയ്ക്കുന്നു.</p>
+ <ul>
+ <li>െസര്‍വര്‍ തിരക്കിലാണോ അതോ താല്‍ക്കാലികമായി പ്രവര്‍ത്തനരഹിതമാണോ? കുറച്ച് കഴിഞ്ഞ് ശ്രമിക്കൂ.</li>
+ <li>നിങ്ങള്‍ക്ക് മറ്റ് സൈറ്റുകള്‍ തിരയുവാൻ സാധിയ്ക്കുന്നുണ്ടോ? കംപ്യൂട്ടറിന്റെ നെറ്റ്‌വര്‍ക്ക്കണക്ഷന്‍ പരിശോധിയ്ക്കൂ.</li>
+ <li>നിങ്ങളുടെ കംപ്യൂട്ടര്‍ അല്ലെങ്കില്‍ നെറ്റ്‌വര്‍ക്ക് ഫയര്‍വോളോ പ്രോക്സിയോ ഉപയോഗിച്ച് സുരക്ഷിതമാക്കിയിട്ടുണ്ടോ? തെറ്റായ ക്രമീകരണങ്ങള്‍ തിരച്ചിലിന് തടസ്സമുണ്ടാക്കുന്നതാണ് .</li>
+ <li>എന്നിട്ടും പ്രശ്നം ഉണ്ടോ? എങ്കില്‍ നെറ്റ്‌വര്‍ക്ക്അഡ്മിനിസ്ട്രേറ്റര്‍ അല്ലെങ്കില്‍ ഇന്റര്‍നെറ്റ് പ്രൊവൈഡറുമായി ബന്ധപ്പെടുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">ബന്ധിപ്പിക്കാൻ കഴിയില്ല</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>ഈ സൈറ്റ് തിരക്കിലാണ് അല്ലെങ്കിൽ താത്കാലമായി ലഭ്യമല്ല. അല്പ സമയത്തിനു് ശേഷം വീണ്ടും ശ്രമിയ്ക്കൂ.</li>
+ <li>ഒരു പേജും ലഭ്യമാക്കാനാവുന്നില്ലെങ്കില്‍, ഉപകരണത്തിന്റെ ഡേറ്റാ അല്ലെങ്കില്‍ വൈഫൈ കണക്ഷന്‍ പരിശോധിയ്ക്കുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">സെര്‍വറില്‍ നിന്നും പ്രതീക്ഷിക്കാത്ത പ്രതികരണം</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>നെറ്റ്‍വർക്കിൽ നിന്നും അപ്രതീക്ഷിതമായ പ്രതികരണമുണ്ടായി, അതിനാല്‍ ബ്രൗസറിനു് മുമ്പോട്ട് തുടരുവാന്‍ സാധ്യമാകുന്നില്ല.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ഈ പേജ് ശരിയായി റീഡയറക്‌ട് ചെയ്യുന്നില്ല</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>ആവശ്യപ്പെട്ട താള്‍ ലഭ്യമാക്കുന്നതിനുള്ള ശ്രമം ബ്രൌസര്‍ നിര്‍ത്തിയിരിക്കുന്നു. താള്‍ റീഡയറക്‌ട് ചെയ്യുന്നത് ഒരിക്കലും അവസാനിക്കുന്നില്ല.</p>
+ <ul>
+ <li>ഈ താളിന് ആവശ്യമുള്ള കൂക്കികള്‍ നിങ്ങള്‍ നിര്‍ജ്ജീവമാക്കുകയോ ഇല്ലാതാക്കുകയോ ചെയ്തിട്ടുണ്ടോ?</li>
+ <li>സൈറ്റിനുള്ള കുക്കികള്‍ അനുവദിച്ചിട്ടും പ്രശ്നം പരിഹരിക്കപ്പെട്ടില്ലയെങ്കിൽ അത് സെര്‍വറിന്റെ സജ്ജീകരണത്തിലുള്ള പിശക് ആകാം</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ഓഫ്‌ലൈൻ മോഡ്</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>ബ്രൌസര്‍ ഓഫ്‌ലൈന്‍ മോഡില്‍ പ്രവര്‍ത്തിയ്ക്കുന്നതിനാല്‍ ആവശ്യപ്പെട്ട താളിലേയ്ക്കു് കണക്ട് ചെയ്യുവാന്‍ സാധിയ്ക്കന്നില്ല.</p>
+ <ul>
+ <li>കമ്പ്യൂട്ടര്‍ സജീവമായൊരു നെറ്റ്‌വര്‍ക്കിലേയ്ക്ക് കണക്ട് ചെയ്തിട്ടുണ്ടോ?</li>
+ <li>വീണ്ടും ശ്രമിയ്ക്കുന്നതിനായി “വീണ്ടും ശ്രമിയ്ക്കുക” അമര്‍ത്തുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">സുരക്ഷാ കാരണങ്ങളാല്‍ പോര്‍ട്ടിലേക്കു് പ്രവേശനം നിഷേധിച്ചിരിയ്ക്കുന്നു</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>വിലാസത്തിൽ ആവശ്യപ്പെട്ട പോര്‍ട്ട് (ഉദാ mozilla.org-ലുള്ള പോര്‍ട്ട് 80-ക്കായി <q>mozilla.org:80</q>) , വെബ് ബ്രൗസിങ് ഒഴികെ<em>മറ്റ്</em> ആവശ്യങ്ങള്‍ക്കായി സാധാരണ ഉപയോഗിക്കുന്നു. നിങ്ങളുടെ സുരക്ഷയ്ക്കായി ഈ ആവശ്യം ബ്രൗസര്‍ റദ്ദാക്കിയിരിക്കുന്നു.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">കണക്ഷൻ പുനഃസജ്ജമാക്കിയതാണ്</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>കണക്ഷൻ നെഗോഷ്യേറ്റ് ചെയ്യുമ്പോൾ നെറ്റ്‌വർക്ക് തടസ്സപ്പെട്ടു. ദയവായി വീണ്ടും ശ്രമിക്കുക.</p>
+ <ul>
+        <li>സൈറ്റ് താൽ‌ക്കാലികമായി ലഭ്യമല്ല അല്ലെങ്കിൽ‌ തിരക്കിലാണ്. അല്പം കഴിഞ്ഞ് വീണ്ടും ശ്രമിക്കുക.</li>
+        <li>നിങ്ങൾക്ക് താളുകളൊന്നും ലോഡുചെയ്യാൻ കഴിയുന്നില്ലെങ്കിൽ നിങ്ങളുടെ ഉപകരണത്തിന്റെ ഡാറ്റയോ വൈഫൈ കണക്ഷനോ പരിശോധിക്കുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">സുരക്ഷിതമല്ലാത്ത ഫയല്‍</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>ദയവായി വെബ് സൈറ്റ് ഉടമകളെ ഈ പ്രശ്നം അറിയിയ്ക്കുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">ഉള്ളടക്കത്തിന്റെ തകരാറ് മൂലമുള്ള പിശക്</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>ഡേറ്റാ അയയ്ക്കുന്നതിലെ പിശകു് കാരണം നിങ്ങള്‍ കാണുവാന്‍ ശ്രമിയ്ക്കുന്ന പേജ് ലഭ്യമല്ല.</p>
+ <ul>
+ <li>ഈ പ്രശ്നത്തെപ്പറ്റി ദയവായി വെബ്സൈറ്റിന്റെ ഉടമസ്ഥരെ അറിയിയ്ക്കുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">ഉള്ളടക്കം തകർന്നു</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>ഡേറ്റാ അയയ്ക്കുന്നതിലുള്ള പിശകു് കാരണം നിങ്ങള്‍ കാണുവാന്‍ ശ്രമിയ്ക്കുന്ന പേജ് ലഭ്യമല്ല.</p>
+ <ul>
+ <li>ഈ പ്രശ്നത്തെപ്പറ്റി ദയവായി വെബ്‍സൈറ്റ് ഉടമസ്ഥരെ അറിയിയ്ക്കുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">ഉള്ളടക്കം എന്‍കോഡ് ചെയ്യുന്നതില്‍ പിശകു് സംഭവിച്ചു</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>നിങ്ങള്‍ ഇപ്പോള്‍ കാണുവാന്‍ ശ്രമിക്കുന്ന പോജ് ലഭ്യമല്ല. കാരണം അതു് തെറ്റായ ആല്ലെങ്കില്‍ പിന്തുണ ലഭ്യമല്ലാത്ത രീതിയിലുള്ള കംപ്രഷന്‍ ഉപയോഗിക്കുന്നു.</p>
+ <ul>
+ <li>ഇതു് സംബന്ധിച്ചുള്ള വിവരങ്ങള്‍ ദയവായി വെബ്‍സൈറ്റ് ഉടമസ്ഥരെ അറിയിക്കുക.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">വിലാസം ലഭ്യമായില്ല</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p> നൽകിയ വിലാസത്തിന്റെ ഹോസ്റ്റ് സെർവർ കണ്ടെത്താൻ ബ്രൗസറിന് കഴിഞ്ഞില്ല. </ p>
+      <ul>
+        <li>താഴെ കാണിച്ചിട്ടുള്ളത് പോലുള്ള പിശകുകൾ വിലാസം നൽകുന്നതിൽ പറ്റിയിട്ടുണ്ടോയെന്ന് പരിശോധിക്കുക
+          <strong> www </strong> .example.com എന്നതിന് പകരം
+          <strong> ww </strong> .example.com. </li>
+        <li> താങ്കൾക്ക് പേജുകളൊന്നും ലോഡുചെയ്യാൻ കഴിയുന്നില്ലെങ്കിൽ, താങ്കളുടെ ഉപകരണത്തിന്റെ ഡാറ്റയോ വൈഫൈ കണക്ഷനോ പരിശോധിക്കുക.</ Li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ഇന്റർനെറ്റ് കണക്ഷൻ ലഭ്യമല്ല</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">നിങ്ങളുടെ നെറ്റ്‌വർക്ക് കണക്ഷൻ പരിശോധിക്കുക അല്ലെങ്കിൽ കുറച്ച് നിമിഷങ്ങൾക്ക് ശേഷം പേജ് വീണ്ടും ലഭ്യമാക്കാൻ ശ്രമിക്കുക.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">വീണ്ടും ലഭ്യമാക്കുക</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">അസാധുവായ വിലാസം</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>നല്‍കിയിരിക്കുന്ന വിലാസം പരിചിതമായ രീതിയിലുള്ളതല്ല. തെറ്റുകള്‍ക്കായി ലോക്കേഷന്‍ ബാര്‍ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ഈ വിലാസം സാധുവല്ല</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>വെബ് വിലാസങ്ങള്‍ സാധാരണ <strong>http://www.example.com/</strong> എന്ന രീതിയിലാണ് എഴുതുന്നത്:</li>
+ <li>മുന്നോട്ടുള്ള സ്ലാഷ് ആണുപയോഗിക്കുന്നത് എന്നുറപ്പാക്കുക (അതായത്, <strong>/</strong>).</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">അപരിചിതമായ പ്രോട്ടോക്കോള്‍</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>വിലാസം നൽകിയ പ്രോട്ടോകോൾ (ഉദാ. <q>wxyz://</q>) ബ്രൗസറിനു് തിരിച്ചറിയുവാന്‍ സാധിക്കുന്നില്ല, ആയതിനാൽ ബ്രൗസറിന് സൈറ്റിലേക്ക് ശരിയായി ബന്ധിപ്പിക്കാനാവുന്നില്ല</p><ul><li>നിങ്ങള്‍ മള്‍ട്ടിമീഡിയയോ അതോ മറ്റേതെങ്കിലും ടെക്സ്റ്റല്ലാത്ത സേവനങ്ങളോ ലഭ്യമാക്കാനാണോ ശ്രമിക്കുന്നത്? കൂടുതല്‍ ആവശ്യങ്ങൾ എന്തെല്ലാം എന്നറിയുന്നതിനായി സൈറ്റ് പരിശോധിക്കുക.</li><li>ചില പ്രോട്ടോക്കോളുകള്‍ക്ക്, അവയെ ബ്രൗസറിന് തിരിച്ചറിയാനാവുന്നതിന് തേര്‍ഡ്-പാര്‍ട്ടി സോഫ്റ്റ്‌വെയര്‍ അല്ലെങ്കില്‍ പ്ലഗിനുകൾ ആവശ്യമുണ്ടാകാം.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ഫയൽ കണ്ടെത്താനായില്ല</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul><li>വസ്തുവിന്റെ പേര് മാറ്റുകയോ, അതിനെ നീക്കം ചെയ്യുകയോ സ്ഥലം മാറ്റുകയോ ചെയ്തിട്ടുണ്ടാകുമോ?</li><li>അക്ഷരത്തെറ്റോ മറ്റെന്തെങ്കിലും തെറ്റുകളോ വിലാസത്തില്‍ ഉണ്ടോ?</li><li>ആവശ്യപ്പെട്ട വസ്തുവിലേക്ക് ലഭിക്കുന്നതിനുള്ള മതിയായ അനുവാദം നിങ്ങള്‍ക്കുണ്ടോ?</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ഫയലിലേക്കുള്ള പ്രവേശനം നിഷേധിച്ചു</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul><li>ഇത് നീക്കംചെയ്തിരിക്കാം, സ്ഥലം മാറ്റിയിരിക്കാം, അല്ലെങ്കിൽ ഫയൽ അനുമതികൾ പ്രവേശനം തടയുന്നുണ്ടാവാം</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">പ്രോക്സി സര്‍വര്‍ ബന്ധം നിഷേധിച്ചിരിക്കുന്നു</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>ഒരു പ്രോക്സി സര്‍വര്‍ ഉപയോഗിക്കുന്നതിനായി ബ്രൗസറിനെ ക്രമീകരിച്ചിരിക്കുന്നു, പക്ഷേ പ്രോക്സി ബന്ധം നിഷേധിച്ചിരിക്കുന്നു.</p><ul><li>ബ്രൗസറിന്റെ പ്രോക്സി ക്രമീകരണം ശരിയാണോ? അവ പരിശോധിച്ച ശേഷം വീണ്ടും ശ്രമിക്കുക.</li><li>ഈ ശൃംഘലയിൽ നിന്നുള്ള ബന്ധങ്ങൾ പ്രോക്സി സേവനം അനുവദിക്കുന്നുണ്ടോ?</li><li>ഇപ്പോഴും പ്രശ്നം ഉണ്ടോ? എങ്കില്‍ ശൃംഘല കാര്യനിർവാഹകർ അല്ലെങ്കില്‍ ഇന്റര്‍നെറ്റ് ദേതാവുമായി ബന്ധപ്പെടുക.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">പ്രോക്സി സെർവർ കണ്ടെത്താനായില്ല</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>ഒരു പ്രോക്സി സര്‍വര്‍ ഉപയോഗിക്കുന്നതിനായി ബ്രൗസറിനെ ക്രമീകരിച്ചിരിക്കുന്നു, പക്ഷേ പ്രോക്സി കണ്ടെത്താനായില്ല.</p><ul><li>ബ്രൗസറിന്റെ പ്രോക്സി ക്രമീകരണം ശരിയാണോ? അവ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക.</li><li>കംപ്യൂട്ടര്‍ സജീവമായ ഒരു ശൃംഘലയിലാണോ?</li><li>ഇപ്പോഴും പ്രശ്നം ഉണ്ടോ? എങ്കില്‍ ശൃംഘല കാര്യനിർവാഹകർ അല്ലെങ്കില്‍ ഇന്റര്‍നെറ്റ് ദേതാവുമായി ബന്ധപ്പെടുക.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">മാൽവെയർ സൈറ്റ് പ്രശ്നം</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s എന്നിടത്തുള്ള സൈറ്റ് ഒരു ആക്രമണ സൈറ്റായി രേഖപ്പെടുത്തിയിട്ടുള്ളതിനാൽ താങ്കളുടെ സുരക്ഷാ ക്രമീകരണങ്ങളനുസരിച്ച് തടയപ്പെട്ടിരിക്കുന്നു.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">അനാവശ്യ സൈറ്റ് പ്രശ്നം</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s എന്നിടത്തുള്ള സൈറ്റ് ഒരു അനാവശ്യ സോഫ്റ്റ്‌വെയറുകൾ വ്യാപിപ്പിക്കുന്നു എന്ന് രേഖപ്പെടുത്തിയിട്ടുള്ളതിനാൽ താങ്കളുടെ സുരക്ഷാ ക്രമീകരണങ്ങളനുസരിച്ച് തടയപ്പെട്ടിരിക്കുന്നു.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ദോഷകരമായ സൈറ്റ് പ്രശ്നം</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s എന്നിടത്തുള്ള സൈറ്റ് ഒരു അപകടകരമായ സൈറ്റാണ് എന്ന് രേഖപ്പെടുത്തിയിട്ടുള്ളതിനാൽ താങ്കളുടെ സുരക്ഷാ ക്രമീകരണങ്ങളനുസരിച്ച് തടയപ്പെട്ടിരിക്കുന്നു.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">തെറ്റിദ്ധരിപ്പിക്കുന്ന സൈറ്റ്</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$s എന്നിടത്തുള്ള സൈറ്റ് ഒരു തെറ്റിദ്ധരിപ്പിക്കുന്ന സൈറ്റാണ് എന്ന് രേഖപ്പെടുത്തിയിട്ടുള്ളതിനാൽ താങ്കളുടെ സുരക്ഷാ ക്രമീകരണങ്ങളനുസരിച്ച് തടയപ്പെട്ടിരിക്കുന്നു.</p>]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..39188c5077
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-mr/strings.xml
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">पुन्हा प्रयत्न करा</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">विनंती पूर्ण करू शकत नाही</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>या समस्येबद्दल किंवा त्रुटीबद्दल अतिरिक्त माहिती सध्या उपलब्ध नाही.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">सुरक्षित जोडणी अयशस्वी</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>प्राप्त माहितीची सत्यता तपासता न आल्यामुळे आपणास इच्छित पृष्ठ पाहता येणार नाही.</li>
+ <li>कृपया संकेतस्थळाच्या मालकाला या अडचणी विषयी अवगत करा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">सुरक्षित जोडणी अयशस्वी</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>ही सर्व्हर च्या संयोजनातील अडचण असू शकते किंवा कोणीतरी या सर्व्हर सारखे रूप घेऊन फसवत असू शकते.</li>
+ <li>आपण या सर्व्हरशी पूर्वी यशस्वीरित्या जुळवणी स्थापीत केली असल्यास, त्रुटी तात्पूर्ती असू शकते, व आपण पुन्हा प्रयत्न करू शकता.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">प्रगत…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>कोणीतरी साईटचे प्रतिरूपण करत असावे आपण पुढे जाऊ नये.</label>
+ <br><br>
+ <label>वेबसाईट प्रमाणपत्राद्वारे आपली ओळख सिद्ध करतात. <b>%2$s</b> ला %1$s विश्वासार्ह मानत नाही कारण याचा प्रमाणपत्रदाता अज्ञात आहे, प्रमाणपत्र स्व-स्वाक्षरीकृत आहे किंवा सर्व्हर योग्य मध्यस्थ प्रमाणपत्र पाठवत नाही.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">मागे जा (शिफारसीय)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">जोखीम स्वीकारा आणि पुढे चला</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">जोडणी मध्ये अडथळा</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>ब्राउझरची जोडणी यशस्वी झाली, पण माहिती पाठवत असताना खंडीत झाली. कृपया पुन्हा प्रयत्न करा.</p>
+ <ul>
+ <li>हे संकेतस्थळ अतिव्यस्त किंवा तात्पुरते बंद असू शकते. काही क्षणात पुन्हा प्रयत्न करा.</li>
+
+ <li>आपण जर कोणतेही पृष्ठ लोड करू शकत नसाल तर आपल्या उपकरणाची डेटा किंवा वायफाय जोडणी तपासा</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">जोडणी कालबाह्य झाली</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>विनंती केलेल्या साईटने जोडणी विनंतीला प्रतिसाद दिला नाही आणि ब्राऊझरने उत्तराची प्रतीक्षा करणे थांबवले आहे.</p>
+
+ <ul>
+ <li>कदाचित सर्व्हर प्रचंड मागणी किंवा तात्पुरता व्यत्यय अनुभवत असेल का?</li>
+
+ <li>इतर साईटही दिसत नाहीत?आपल्या डिव्हाईसची नेटवर्क जोडणी तपासून पहा.</li>
+
+ <li>आपले डिव्हाईस किंवा नेटवर्क फायरवॉल किंवा प्रॉक्सी ने सुरक्षित आहे का?चुकीच्या सेटिंग इंटरनेट ब्राऊझिंगला व्यत्यय निर्माण करू शकतात.</li>
+
+ <li>अजूनही समस्या आहेच?आपल्या नेटवर्क व्यवस्थापक किंवा इंटरनेट प्रदात्यांकडून मदत घ्या.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">जोडणी होऊ शकत नाही</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>हे संकेतस्थळ तात्पुरते उपलब्ध नसेल किंवा अतिशय व्यस्त असेल. काही क्षणात पुन्हा प्रयत्न करा.</li>
+
+ <li>जर आपण कोणतीही संकेतस्थळे उघडू शकत नसाल तर आपल्या उपकरणाची वायफाय किंवा डेटा जोडणी तपासून पहा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">सर्व्हरकडून अनपेक्षित प्रतिसाद</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>साईटने नेटवर्क विनंतीला अनपेक्षितरित्या उत्तर दिले व ब्राउझर पुढे जाऊ शकत नाही.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">पृष्ठ योग्यरित्या पुनर्निर्देशित होत नाही</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>विनंती केलेली गोष्ट मिळवणे ब्राऊझरने थांबवले आहे. कधीही पूर्ण होणार नाही अशा पद्धतीने साईट ने विनंती वळवली आहे.</p>
+ <ul>
+ <li>या साईटला हवे असलेल्या कुकीज आपण निष्क्रिय किंवा अवरोधित केल्या आहेत का?</li>
+
+ <li>जर साईट कुकीज स्वीकारण्याची समस्या सुटली नाही तर ही कदाचित सर्व्हरची समस्या आहे आपल्या डिव्हाईसची नाही.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ऑफलाइन मोड</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>ब्राउझर आपल्या ऑफलाईन मोड मध्ये कार्यरत आहे व विनंती केलेल्या गोष्टीला जोडू शकत नाही.</p>
+ <ul>
+
+ <li>डिव्हाईस सक्रिय नेटवर्कला जोडलेले आहे का?</li>
+
+ <li>ऑनलाईन मोड वर जाण्यासाठी “पुन्हा प्रयत्न करा” दाबा आणि पृष्ठ पुन्हा लोड करा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">सुरक्षा कारणास्तव पोर्ट प्रतिबंधित</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>विनंती केलेल्या पत्त्याने सहसा वेब ब्राऊझिंग सोडून <em>इतर</em> कारणासाठी वापरले जाणारे पोर्ट नमूद केले आहे (उदा. <q>mozilla.org:80</q> mozilla.org वर पोर्ट क्र. 80). आपल्या संरक्षणार्थ ब्राउझर ने विनंती रद्द केली आहे</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">जोडणी पुनःप्रस्थापित करण्यात आली</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>जोडणीची देवाणघेवाण करताना नेटवर्क लिंक खंडित झाली. कृपया पुन्हा प्रयत्न करा.</p>
+ <ul>
+ <li>हे संकेतस्थळ अतिव्यस्त किंवा तात्पुरते बंद असू शकते. काही क्षणात पुन्हा प्रयत्न करा.</li>
+
+ <li>आपण जर कोणतेही पृष्ठ लोड करू शकत नसाल तर आपल्या उपकरणाची डेटा किंवा वायफाय जोडणी तपासा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">असुरक्षीत फाइल प्रकार</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>या समस्येबद्दल माहिती देण्यासाठी कृपया संकेतस्थळाच्या मालकाशी संपर्क करा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">दुषीत मजकूर त्रुटी</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>डाटा स्थानांतरनवेळी त्रुटी आढळल्याने आपण पाहू इच्छित पृष्ठ दाखवणे अशक्य आहे.</p>
+ <ul>
+
+ <li>या अडचणीविषयी माहिती पुरवण्याकरीता, कृपया संकेतस्थळाच्या मालकांशी संपर्क करा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">मजकूर भग्न झाला</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>डाटा स्थानांतरनवेळी त्रुटी आढळल्याने आपण पाहू इच्छित पृष्ठ दाखवणे अशक्य आहे.</p>
+ <ul>
+
+ <li>या अडचणीविषयी माहिती पुरवण्याकरीता, कृपया संकेतस्थळाच्या मालकांशी संपर्क करा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">मजकूर एन्कोडींग त्रुटी</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>पृष्ठ अवैध किंवा असमर्थीत संकुचन प्रकार वापरत असल्यामुळे इच्छित पृष्ठ पाहता येणार नाही.</p>
+ <ul>
+
+ <li>कृपया संकेतस्थळाच्या मालकाला या अडचणी विषयी अगत करा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">पत्ता आढळला नाही</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>पुरवलेल्या पत्त्याचा होस्ट सर्व्हर ब्राउझरला सापडला नाही.</p>
+ <ul>
+ <li>अशा टाइपिंग चुकांसाठी पत्ता तपासा
+ <strong>www</strong>.example.com
+ च्याऐवजी <strong>ww</strong>.example.com</li>
+ <li>आपण कोणतीही पृष्ठे लोड करू शकत नसल्यास आपल्या डिव्हाइसचा डेटा किंवा वायफाय कनेक्शन तपासा.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">इंटरनेट जोडणी नाही</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">आपली नेटवर्क जोडणी तपासा किंवा काही क्षणात पृष्ठ परत लोड करण्याचा प्रयत्न करा.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">पुन्हा लोड करा</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">अवैध पत्ता</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>पुरवलेला पत्ता ओळखण्याजोग्या स्वरूपात नाही. कृपया चुकांसाठी पत्ता पट्टी तपासा आणि पुन्हा प्रयत्न करा.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">पत्ता वैध नाही</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>वेब पत्ते सहसा असे लिहितात <strong>http://www.example.com/</strong></li>
+
+ <li>आपण पुढील छेद वापरताय याची खात्री करा (उदा. <strong>य</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">अज्ञात प्रोटोकॉल</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>पत्त्यामध्ये ब्राउझर ओळखत नाही असा प्रोटोकॉल (<q>wxyz://</q>) नमूद केला आहे, म्हणून ब्राउझर साईटबरोबर जोडणी करू शकत नाही.</p>
+ <ul>
+
+ <li>आपण मल्टिमीडिया किंवा अन्य लिखित मजकुरेतर सेवा वापरू इच्छिता का?अतिरिक्त आवश्यकतांकरिता साईट तपासा.</li>
+
+ <li>काही प्रोटोकॉल ब्राउझर ने ओळखण्यासाठी तृतीयपक्षीय सॉफ्टवेअर किंवा प्लग-इन आवश्यक असू शकते.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">फाइल आढळली नाही</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>ह्या घटकाचे कदाचित नाव बदलले असेल, काढून टाकला किंवा हलवला असू शकतो का?</li>
+
+ <li>पत्त्यामध्ये अक्षरलेखन,मोठी लिपी किंवा इतर लेखन त्रुटी असू शकते का?</li>
+
+ <li>विनंती केलेल्या पत्त्यावरचा घटक वापरायची आपल्याला परवानगी आहे का?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">फाइल वापर नाकारण्यात आला</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>ते कदाचित काढून टाकले गेले, हलविले, किंवा त्यास फाइल परवानग्या प्रवेश प्रतिबंधित करत असतील.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">प्रॉक्सी सर्व्हरने जोडणी नकारली</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>ब्राउझरची संरचना प्रॉक्सी सर्व्हर वापरण्यासाठी केली आहे, पण प्रॉक्सीने जोडणी नकारली.</p>
+ <ul>
+ <li>ब्राउझरची प्रॉक्सी संरचना अचूक आहे का? सेटिंग तपासा व पुन्हा प्रयत्न करा.</li>
+ <li>प्रॉक्सी सेवा या नेटवर्ककडून जोडणीला अनुमती देते का?</li>
+ <li>अजूनही समस्या आहे? मदतीसाठी आपल्या नेटवर्क प्रशासकाचा किंवा इंटरनेट प्रदात्याचा सल्ला घ्या.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">प्रॉक्सी सर्व्हर आढळला नाही</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>ब्राउझरची संरचना प्रॉक्सी सर्व्हर वापरण्यासाठी केली आहे, पण प्रॉक्सी सापडला नाही.</p>
+ <ul>
+ <li>ब्राउझरची प्रॉक्सी संरचना अचूक आहे का? सेटिंग तपासा व पुन्हा प्रयत्न करा.</li>
+ <li>उपकरण सक्रिय नेटवर्कशी जोडले आहे का?</li>
+ <li>अजूनही समस्या आहे? मदतीसाठी आपल्या नेटवर्क प्रशासकाचा किंवा इंटरनेट प्रदात्याचा सल्ला घ्या.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">मालवेअर साइट समस्या</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$sयेथील साईट घातक म्हणून घोषित केली आहे आणि आपल्या सुरक्षितता प्राधान्यक्रमानुसार अवरोधित केली आहे.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">अनिष्ट साईट समस्या</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s येथील साईट निरिच्छ सॉफ्टवेअर देणारी म्हणून घोषित केली आहे आणि आपल्या सुरक्षितता प्राधान्यक्रमानुसार अवरोधित केली आहे.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">घातक साईट समस्या</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s वरील संकेतस्थळ धोकादायक म्हणून घोषीत केले गेले आहे व आपल्या सुरक्षा प्राधान्यक्रम आधारावर रोखले गेले आहे.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">भ्रामक साइट समस्या</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$s वरील संकेतस्थळ फसवे म्हणून घोषीत केले गेले आहे व सुरक्षा प्राधान्यक्रम कारणास्तव रोखले गेले आहे.</p>]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..fd919189ac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-my/strings.xml
@@ -0,0 +1,254 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ထပ်ကြိုးစားပါ</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">တောင်းဆိုချက်ကို ပြီးဆုံးသည်အထိ မဆောင်ရွက်နိုင်ပါ</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p> ဒီပြဿနာအတွက်နောက်ထပ်အချက်အလက်များကို လက်ရှိမရရှိနိုင်သေးပါ။</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">လုံခြုံသော ချိတ်ဆက်မှု မအောင်မြင်ခဲ့ပါ</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>သင်ကြည့်ရန်ကြိုးစားနေသော စာမျက်နှာကို မဖော်ပြနိုင်ပါ။ အဘယ့်ကြောင့်ဆိုသော် လက်ခံရရှိထားသော အချက်အလက်၏ ထောက်ခံချက်ကို အတည်မပြုနိုင်သောကြောင့် ဖြစ်သည်။</li>
+ <li>ဒီပြဿနာကို အသိပေးရန် ကျေးဇူးပြု၍ ဝဘ်ဆိုဒ်ပိုင်ရှင်ထံသို့ ဆက်သွယ်ပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">လုံခြုံသော ချိတ်ဆက်မှု မအောင်မြင်ခဲ့ပါ</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>ယခုပြဿနာသည် ဆာဗာ၏ အပြင်အဆင်ကြောင့်ဖြစ်နိုင်သည် သို့မဟုတ် တစ်ယောက်ယောက်က ဆာဗာကဲ့သို့ အယောင်ဆောင်နေခြင်းကြောင့် ဖြစ်နေနိုင်သည်။</li>
+ <li>သင်သည် ယခုဆာဗာကို အရင်က ချိတ်ဆက်ဖူးပါက ယခုပြဿနာသည် ယာယီဖြစ်နိုင်သည်၊ ထို့ပြင် နောက်မှ ပြန်ဖွင့်ကြည့်နိုင်သည်။</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">အဆင့်မြင့်…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>တစ်စုံတစ်ယောက်သည် ဝဘ်ဆိုက်ကဲ့သို့ အယောင်ဆောင်နေခြင်း ဖြစ်နေနိုင်ပြီး သင် ရှေ့ဆက်မလုပ်ဆောင်သင့်ပါ။</label>
+ <br><br>
+ <label>ဝဘ်ဆိုက်များသည် သူတို့၏အထောက်အထားကို လက်မှတ်ဖြင့် သက်သေပြသည်။ %1$s သည် <b>%2$s</b> ကို မယုံကြည်ပါ၊ အဘယ့်ကြောင့်ဆိုသော် ၄င်း၏ လက်မှတ်ထုတ်ပေးသူကို မသိရသောကြောင့်၊ ကိုယ်တိုင်လုပ်လက်မှတ် ဖြစ်သောကြောင့်၊ သို့မဟုတ် ဆာဗာက မှန်ကန်သော ပေါင်းကူးလက်မှတ်များ မပို့ပေးသောကြောင့် ဖြစ်သည်။</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ပြန်သွားပါ (အကြံပြုချက်အရ)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">အန္တရာယ်ကိုလက်ခံပြီးဆက်လုပ်ပါ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">ချိတ်ဆက်မှု ပြတ်တောက်သွားခဲ့သည်</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>ဘရောင်ဇာသည် အောင်မြင်စွာ ချိတ်ဆက်နိုင်ခဲ့သော်လည်း အချက်အလက်လွှဲပြောင်းနေစဉ် ချိတ်ဆက်မှု ပြတ်တောက်ခဲ့သည်။ ကျေးဇူးပြု၍ ထပ်မံ ဆောင်ရွက်ကြည့်ပါ။</p>
+ <ul>
+ <li>ဝဘ်ဆိုက်ကို ယာယီ ကြည့်ရှု၍ မရနိုင်ပါ သို့မဟုတ် ၄င်းသည် အလုပ်များနေသောကြောင့် ဖြစ်နိုင်သည်။ ခဏကြာလျှင် ထပ်ကြိုးစားကြည့်ပါ။</li>
+ <li>သင်သည် မည်သည့်ဝဘ်ဆိုက်မျှ မကြည့်ရှုနိုင်ပါက သင်၏ ကိရိယာ၏ ဒေတာ သို့မဟုတ် ဝိုင်ဖိုင်ချိတ်ဆက်မှုကို စစ်ဆေးပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">ချိတ်ဆက်မှုသည် သတ်မှတ်ချိန် ကျော်လွန်ခဲ့သည်</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>ချိတ်ဆက်မှုတောင်းဆိုချက်ကို ဝဘ်ဆိုက်က မတုံ့ပြန်နိုင်ပါ၊ ပြီးနောက် ဘရောင်ဇာသည် တုံ့ပြန်မှုစောင့်ဆိုင်းနေခြင်းမှ ရပ်ဆိုင်းသွားသည်။</p>
+ <ul>
+ <li>ဆာဗာသည် ဝန်မနိုင်သောပြဿနာ ကြုံတွေ့နေခြင်း သို့မဟုတ် ယာယီပြတ်တောက်မှု ဖြစ်နိုင်ပါသလား။ နောက်မှ ပြန်ကြိုးစားကြည့်ပါ။</li>
+ <li>အခြားဝဘ်ဆိုက်များကို ကြည့်ရှု၍ မရနိုင် ဖြစ်နေပါသလား။ ကိရိယာ၏ ကွန်ယက်ချိတ်ဆက်မှုကို စစ်ဆေးပါ။</li>
+ <li>သင်၏ ကိရိယာ သို့မဟုတ် ကွန်ယက်ကို မီးနံရံ သို့မဟုတ် ကြားခံဆာဗာက ကာကွယ်ထားပါသလား။ အပြင်အဆင်အမှားများသည်လည်း ဝဘ်ကြည့်ရှုခြင်းကို နှောက်ယှက်နိုင်သည်။</li>
+ <li>ပြဿနာရှိနေဆဲ ဖြစ်နေပါသလား။ သင်၏ ကွန်ယက်ထိန်းချုပ်သူ သို့မဟုတ် အင်တာနက်ဝန်ဆောင်မှုပေးသူကို အကူအညီတောင်းပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">မချိတ်ဆက်နိုင်ပါ</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>ယခုဝဘ်ဆိုက်ကို ယာယီ အသုံးမပြုနိုင်ပါ သို့မဟုတ် အသုံးပြုသူ များနေသောကြောင့် ဖြစ်နိုင်သည်။ ခဏကြာလျှင် ပြန်ကြိုးစားပါ။</li>
+ <li>အကယ်၍ သင်သည် မည်သည့်ဝဘ်ဆိုက်မျှ မဖွင့်နိုင်ပါက သင့်မိုဘိုင်းကိရိယာ၏ ဒေတာ သို့မဟုတ် ဝိုင်ဖိုင် ချိတ်ဆက်မှုကို စစ်ဆေးပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">ဆာဗာမှမမျှော်လင့်သောတုံ့ပြန်မှု</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>ဝဘ်ဆိုက်သည် ကွန်ယက်တောင်းဆိုမှုကို မမျှော်မှန်းထားသောနည်းလမ်းဖြင့် တုန့်ပြန်ခဲ့ပြီး ဘရောင်ဇာသည် ဆက်လက်မဆောင်ရွက်နိုင်ပါ။</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ယခုစာမျက်နှာသည် ကောင်းမွန်စွာ လမ်းညွှန်မပေးနိုင်ပါ</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>ဘရောင်ဇာသည် တောင်းဆိုထားသည့်အရာကို ရယူရန် ကြိုးစားနေစဉ် ရပ်ဆိုင်းသွားသည်။ ဝဘ်ဆိုက်သည် တောင်းဆိုချက်ကို ပြီးစီးအောင် ဆောင်ရွက်နိုင်မည်မဟုတ်သောပုံစံဖြင့် ပြန်ညွှန်းနေသည်။</p>
+ <ul>
+ <li>ယခုဝဘ်ဆိုက်က လိုအပ်သောကွတ်ကီးများကို ပိတ် သို့မဟုတ် တားဆီးထားပါသလား။</li>
+ <li>ဝဘ်ဆိုက်၏ ကွတ်ကီးကို လက်ခံထားသော်လည်း ပြဿနာ မပြေလည်သေးပါက ၄င်းသည် ဆာဗာ၏ အပြင်အဆင်အမှား ဖြစ်နိုင်သည်၊ သင်၏ ကိရိယာကြောင့် မဟုတ်လောက်ပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">အော့ဖ်လိုင်း mode</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>ဘရောင်ဇာသည် ကွန်ယက်မဲ့သုံးပုံစံတွင် ဖြစ်နေပြီး တောင်းဆိုထားသည့်အရာကို မချိက်ဆက်နိုင်ပါ။</p>
+ <ul>
+ <li>ယခုကိရိယာသည် ကွန်ယက်ကို ချိက်ဆက်ထားပါသလား။</li>
+ <li>ကွန်ယက်သုံးပုံစံကို ပြောင်းရန် “ထပ်မံကြိုးစားကြည့်ပါ” ကို နှိပ်ပြီး စာမျက်နှာကို ပြန်ခေါ်ပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">လုံခြုံရေး ကိစ္စများကြောင့်တားမြစ်ထားတဲ့ Port</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>တောင်းဆိုထားသော လိပ်စာသည် ပုံမှန်အားဖြင့် ဝက်ဆိုက်ကြည့်ရှုရန် မဟုတ်သည့် အပေါက် ( ဥပမာ <q>mozilla.org:80</q> တွင် 80 သည် အပေါက်နံပတ်ဖြစ်သည်) တစ်ခုအား ညွှန်ထားသည် ။ သင့်အား ကာကွယ်ရန် ဘ‌ယောက်ဆာမှ တောင်းဆိုမှုအား မလုပ်တော့ပါ ။</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">ဆက်သွယ်မှုကို ပြန်လည်သတ်မှတ်သည်</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>စတင်ခေါ်ဆိုနေစဥ် နက်ဝက် ချိတ်ဆက်မှု ပြတ်တောက်သွားသည်။ ကျေးဇူးပြုပြီး ထပ်ကြိုးစားပါ။</p>
+ <ul>
+ <li>ဝက်ဆိုက် အလုပ်အရမ်းများနေတာလည်း ဖြစ်နိုင်သည် ။ တစ်ခဏနေ မှ ထပ်ကြိုးစားသင့်သည်။ </li>
+ <li>သင် အခြား ဝက်စာမျက်နှာများပါဖွင့် မရပါက သင့် စက်၏ အချက်အလက် ချိတ်ဆက်မှု သို့ ဝိုင်ဖိုင် အား စစ်ပါ</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">မလုံခြုံသောဖိုင်အမျိုးအစား</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li> ကျေးဇူးပြု၍ ယခုပြဿနာကို အသိပေးရန် ဝဘ်ဆိုက်ပိုင်ရှင်ကို ဆက်သွယ်ပါ။ </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">အကြောင်းအရာ မစုံလင်သော အမှား</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>သင် တောင်းဆိုထားသော စာမျက်နှာအား ရယူရာတွင် အခက်အခဲတခု ဖြစ်ပွား နေသဖြင့် ဖွင့်ပြမပေးနိုင်ပါ </p>
+ <ul>
+ <li>ကျေးဇူးပြုပြီး ဝက်ဆိုက် ပိုင်ရှင်အား ယခု ပြသာနာနှင့်ပတ်သတ်ပြီး ဆက်သွယ် အကြောင်းကြားပေးပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">အကြောင်းအရာပျက်နေတယ်</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>သင်တောင်းဆိုထားသောစာမျက်နှာအား ရယူရာတွင်အခက်အခဲဖြစ်နေသဖြင့် ဖွင့်ပြမပေးနိုင်ပါ။</p>
+ <ul>
+ <li>ကျေးဇူးပြုပြီး ဝက်ဆိုက် ပိုင်ရှင်အား ယခု ပြသာနာနှင့်ပတ်သတ်ပြီး အကြောင်းကြားပေးပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">အကြောင်းအရာ အန်ကုဒ်ဒင်း အမှား</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>သင်တောင်းဆိုထားသောစာမျက်နှာအား ရယူရာတွင်အခက်အခဲဖြစ်နေသဖြင့် ဖွင့်ပြမပေးနိုင်ပါ။</p>
+ <ul>
+ <li>ကျေးဇူးပြုပြီး ဝက်ဆိုက် ပိုင်ရှင်အား ယခု ပြသာနာနှင့်ပတ်သတ်ပြီး အကြောင်းကြားပေးပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">လိပ်စာမတွေ့ပါ</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>ဘရောက်ဇာသည်ပေးထားသောလိပ်စာအတွက် host server ကိုရှာမရပါ။ </p>
+ <ul>
+ <li>ဥပမာ လိပ်စာအတွက်စာရိုက်အမှားများကိုစစ်ဆေးပါ။
+ <strong>ww</strong>.example.com instead of
+ <strong>www</strong>.example.com.</li>
+ <li>စာမျက်နှာတစ်ခုမျှဖွင့်မရပါက ဝိုင်ဖိုင်ချိတ်ဆက်မှုကိုစစ်ပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">အင်တာနက်ချိတ်ဆက်မှု မရှိပါ</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">သင်၏ကွန်ယက်ချိတ်ဆက်မှုကိုစစ်ဆေးပါ (သို့) အချိန်တိုအတွင်းစာမျက်နှာကိုပြန်ဖွင့်ရန်ကြိုးစားပါ။</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ပြန်တင်ပါ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">မမှန်ကန်သောလိပ်စာ</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>ပေးထားသောလိပ်စာသည် သတ်မှတ်ထားသော ပုံစံမျိုးဖြင့်မဟုတ်ပါ။ ကျေးဇူးပြုပြီး ရှာဖွေရေးဘားကို စစ်ဆေးပြီး ထပ်မံ ဆောင်ရွက်ကြည့်ပါ။</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">လိပ်စာမမှန်ကန်ပါ</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>ဝဘ်လိပ်စာများကိုအများအားဖြင့်အောက်ပါအတိုင်းရေးကြသည်။<strong>http://www.example.com/</strong></li>
+ <li>မျဉ်းစောင်းလေးပါသည်ကိုဂရုပြုပါ။ (နမူနာ<strong>/</strong>)</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">မသိသောပရိုတိုကော</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>လိပ်စာက protocol ကိုသတ်မှတ်သည် (ဥပမာ -<q>wxyz://</q>) ဘရောက်ဇာမှ မသိသော‌ကြောင့် ဘရောက်ဇာမှ ဤဆိုက်ကိုမချိတ်ဆက်နိုင်ပါ။</p>
+ <ul>
+ <li>သင် multimedia သို့မဟုတ်အခြားစာမဟုတ်သောဝန်ဆောင်မှုများကိုရယူရန်ကြိုးစားနေပါသလား။ အပိုလိုအပ်ချက်များအတွက် site ကိုစစ်ဆေးပါ။ </li>
+ <li>browser သည်၎င်းတို့ကိုအသိမပြုမီ အချို့သော protocol များသည် third-party software သို့မဟုတ် plugins လိုအပ်လိမ့်မည်။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ဖိုင်ရှာမတွေ့ပါ</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>ယခုအရာသည် အမည်ပြောင်း၊ ဖျက်၊ နေရာရွှေ့ခံထားရခြင်း ဖြစ်နိုင်ပါသလား။</li>
+ <li>လိပ်စာရေးရာတွင် သတ်ပုံ၊ စာလုံးအကြီးအသေး၊ စာလုံးပေါင်းမှားနေခြင်း ရှိနေပါသလား။</li>
+ <li>တောင်းဆိုထားသော အရာအတွက် သင့်တွင် လုံလောက်သော ခွင့်ပြုချက် ရှိပါသလား။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ဖိုင်အသုံးပြုခြင်းကို တားမြစ်ထားသည်</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>၄င်းကို ဖျက်ထား၊ ရွှေ့ထား သို့မဟုတ် ဖိုင်အသုံးပြုခွင့်က တားဆီးနေခြင်း ဖြစ်နိုင်သည်။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ကြားခံဆာဗာသည် ချိတ်ဆက်မှုကို ငြင်းဆန်ထားသည်</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>ကြားခံဆာဗာသုံးရန် ဘရောင်ဇာကို ပြင်ဆင်ထားသော်လည်း ကြားခံဆာဗာက ချိတ်ဆက်မှုကို ငြင်းဆိုနေသည်။</p>
+ <ul>
+ <li>ဘရောင်ဇာ၏ ကြားခံဆာဗာအပြင်အဆင် မှားနေသလား။ အပြင်အဆင်ကို စစ်ဆေးပြီး နောက်တစ်ကြိမ်ထပ်ကြိုးစားကြည့်ပါ။</li>
+ <li>ကြားခံဆာဗာဝန်ဆောင်မှုသည် ယခုကွန်ယက်မှ ချိိတ်ဆက်မှုများကို ခွင့်ပြုနေပါသလား။</li>
+ <li>ပြဿနာရှိနေဆဲ ဖြစ်နေပါသလား။ သင်၏ ကွန်ယက်ထိန်းချုပ်သူ သို့မဟုတ် အင်တာနက်ဝန်ဆောင်မှုပေးသူကို အကူအညီတောင်းပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ကြားခံဆာဗာကို မတွေ့ရပါ</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>ဤဘရောင်ဇာသည် ပရောက်ဆီသုံးရန်ပြုပြင်ထားသော်လည်း ပရောက်ဆီကို ရှာမတွေ့ပါ။</p>
+ <ul>
+ <li>ပရောက်ဆီ၏ အပြင်အဆင်တွေမှန်ပါသလား။ အပြင်အဆင်များကိုစစ်ဆေးပြီးထပ်မံဆောင်ရွက်ကြည့်ပါ။</li>
+ <li>ကိရိယာသည် ကွန်ယက်သို့ ချိတ်ဆက်ထားပါသလား</li>
+ <li>ပြဿနာရှိနေဆဲ ဖြစ်နေပါသလား။ သင်၏ ကွန်ယက်ထိန်းချုပ်သူ သို့မဟုတ် အင်တာနက်ဝန်ဆောင်မှုပေးသူကို အကူအညီတောင်းပါ။</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Malware site ပြဿနာ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s တွင် ရှိသော ယခုဝဘ်ဆိုက်သည် တိုက်ခိုက်နိုင်ဖွယ်ရှိသောစာမျက်နှာဟု သတင်းပေးခြင်းခံရပြီး သင်၏လုံခြုံရေးအပြင်အဆင်များအရ ပိတ်ပင်ထားသည်။</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">မလိုလားအပ်သောဆိုက်ပြဿနာ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s ရှိ ဆိုက်သည် မလိုအပ်သည့် ဆော့ဖ်ဝဲများ ဖြန့်သည်ဟု တိုင်ကြားခံထားရသည်။ ထို့ကြောင့် သင့် လုံခြုံရေးအပြင်အဆင်များအရ ထိုဆိုက်ကို ပိတ်ပင်တားဆီးထားသည်။</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">အန္တရာယ်ရှိသော site ပြဿနာ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s ရှိ ဝဘ်ဆိုက်သည် အန္တရာယ်ရှိနိုင်သော ဝဘ်ဆိုက်ဖြစ်ကြောင်း သတင်းရထားပြီး လုံခြုံရေးအစီအမံများအရ ၎င်းကို ပိတ်ပင်ထားသည်။</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">လှည့်စားသော site ကိုပြဿနာ</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$s တွင် ရှိသော ယခုဝဘ်ဆိုက်သည် တိုက်ခိုက်နိုင်ဖွယ်ရှိသောစာမျက်နှာဟု သတင်းပေးခြင်းခံရပြီး သင်၏လုံခြုံရေးအပြင်အဆင်များအရ ပိတ်ပင်ထားသည်။</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">လုံခြုံသောဝဘ်ချိတ်ဆက်မှုမရနိုင်ပါ</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[သင်သည် ပိုမိုကောင်းမွန်သောလုံခြုံမှုအတွက် HTTPS သီးသန့်ပုံစံအားဖွင့်ထားပြီး <em>%1$s</em> ၏ HTTPS ပုံစံသည် မရရှိနိုင်ပါ။]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP ဝဘ်သို့ဆက်သွားမည်</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..85063395d3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,326 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Prøv igjen</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Klarte ikke fullføre forspørselen</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Mer informasjon om denne feilen er ikke tilgjengelig.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Sikker tilkobling mislyktes</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Siden du forsøker åpne kan ikke vises fordi det ikke kunne bekreftes at overført data er autentisk.</li>
+ <li>Kontakt nettstedseieren og informer om problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Sikker tilkobling mislyktes</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Dette kan være på grunn av et problem med serverens innstillinger, eller det kan være at noen forsøker å forfalske tilkoblingen til serveren.</li>
+ <li>Dersom du tidligere har koblet til serveren er det mulig at feilen er midlertidig, og du kan prøve igjen senere.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avansert…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Noen kan prøve å etterligne nettstedet, og du bør ikke fortsette.</label>
+ <br><br>
+ <label>Nettsteder beviser sin identitet via sertifikater. %1$s stoler ikke på <b>%2$s</b> fordi sertifikatutstederen er ukjent, sertifikatet er selv-signert, eller fordi serveren ikke sender de rette mellomsertifikatene.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Gå tilbake (Anbefalt)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Godta risikoen og fortsett</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Dette nettstedet krever en sikker tilkobling.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Siden du prøver å vise kan ikke vises fordi dette nettstedet krever en sikker tilkobling.</li>
+ <li>Problemet er mest sannsynlig med nettstedet, og det er ingenting du kan gjøre for å løse det.</li>
+ <li>Du kan varsle nettstedets administrator om problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avansert …</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> har en sikkerhetspolicy kalt HTTP Strict Transport Security (HSTS), som betyr at <b>%2$s</b> bare kan koble til den sikkert. Du kan ikke legge til et unntak for å besøke dette nettstedet. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Gå tilbake</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Tilkoblingen ble avbrutt</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Nettleseren ble tilkoblet, men tilkoblingen ble avbrutt under overføring av informasjon. Prøv igjen.</p>
+ <ul>
+ <li>Siden kan være midlertidig utilgjengelig, eller opptatt. Prøv igjen om en stund.</li>
+ <li>Hvis du ikke klarer å laste noen sider, kontroller data- eller Wi-Fi-forbindelsen til enheten din.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Tilkoblingen fikk tidsavbrudd</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Det forespurte nettstedet svarte ikke på en tilkoblingsforespørsel, og nettleseren har sluttet å vente på svar.</p>
+ <ul>
+ <li>Kan det hende at nettstedet har unormalt høy belastning akkurat nå, eller er midlertidig utilgjengelig? Prøv igjen senere.</li>
+ <li>Klarer du å koble til andre nettsted? Kontroller at datamaskinens nettverkstilkobling virker.</li>
+ <li>Er datamaskinen din beskyttet av en brannmur eller proxy? Feilaktige innstillinger kan gjøre det umulig å få tilgang til Internett.</li>
+ <li>Har du fortsatt problemer? Kontakt systemansvarlig eller Internett-tilbyderen for mer hjelp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Kan ikke koble til</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Siden kan være midlertidig utilgjengelig, eller opptatt. Prøv igjen om en stund.</li>
+ <li>Hvis du ikke klarer å laste noen sider, kontroller data- eller Wi-Fi-forbindelsen til enheten din.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Uventet svar fra server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Nettstedet svarte på en forespørsel på en uventet måte, og nettleseren kan ikke fortsette.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Nettsiden videresender ikke ordentlig</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Nettleseren har sluttet å forsøke å laste det ønskede elementet. Nettsiden omdirigerer forespørselen på en måte slik at den aldri vil fullføre.</p>
+ <ul>
+ <li>Har du slått av eller blokkert infokapsler som er påkrevd av dette nettstedet?</li>
+ <li>Dersom det ikke hjelper å akseptere nettstedets infokapsler, kan det være et problem med nettstedets innstillinger, og problemet gjelder da ikke datamaskinen din.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Frakoblet modus</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Nettleseren er i frakoblet modus, og kan ikke koble til serveren.</p>
+ <ul>
+ <li>Er datamaskinen koblet til et aktivt nettverk?</li>
+ <li>Trykk «Prøv igjen» for å bytte til tilkoblet modus og laste siden på nytt.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porten er adgangsbegrenset av sikkerhetsårsaker</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Forespørselsadressen oppgav en port (f.eks. <q>mozilla.org:80</q> for port 80 på mozilla.org) som normalt brukes til et <em>annet</em> formål enn nettlesing. Nettleseren har avbrutt forespørselen av sikkerhetsårsaker.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Tilkoblingen ble avbrutt</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Nettverkskoblingen ble avbrutt under forhandlinger om en tilkobling. Prøv igjen.</p>
+ <ul>
+ <li>Siden kan være midlertidig utilgjengelig, eller opptatt. Prøv igjen om en stund.</li>
+ <li>Hvis du ikke klarer å laste noen sider, kontroller data- eller Wi-Fi-forbindelsen til enheten din.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Usikker filtype</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Kontakt eieren av nettstedet og informer dem om dette problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Ødelagt innholdsfeil</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Siden du forsøker å vise kan ikke åpnes på grunn av en feil i dataoverføringen.</p>
+ <ul>
+ <li>Kontakt nettstedseierne og informer dem om dette problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Innhold krasjet</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Siden du forsøker å vise kan ikke åpnes på grunn av en feil i dataoverføringen.</p>
+ <ul>
+ <li>Kontakt nettstedseierne og informer dem om dette problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Feil med tegnkoding</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Nettsiden du forsøker å åpne kan ikke vises fordi den bruker en ukjent eller ugyldig komprimeringsmetode.</p>
+ <ul>
+ <li>Kontakt den ansvarlige for nettstedet og informer dem om problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adressen ikke funnet</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Nettleseren kunne ikke finne serveren for den oppgitte adressen.</p>
+ <ul>
+ <li>Kontroller adressen for skrivefeil som f.eks
+ <strong>ww</strong>.example.com i stedet for
+ <strong>www</strong>.example.com.</li>
+ <li>Hvis du ikke kan laste noen sider, kan du sjekke enhetens data- eller Wi-Fi-tilkobling.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ingen internettforbindelse</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Sjekk nettverkstilkoblingen din, eller prøv å laste siden på nytt om en stund.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Last på nytt</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Ugyldig adresse</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Den oppgitte adressen er ikke i et gjenkjennbart format. Se i adresselinjen om det er skrivefeil, og prøv igjen.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adressen er ugyldig</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Nettadresser skrives vanligvis som <strong>http://www.example.com/</strong></li>
+ <li>Pass på at du bruker framover-skråstrek (dvs. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Ukjent protokoll</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adressen oppgir en protokoll (f.eks. <q>wxyz://</q>) som nettleseren ikke forstår, slik at den ikke kan koble ordentlig til serveren.</p>
+ <ul>
+ <li>Prøver du å få tilgang til multimedia eller annen ikke-tekstlig kilde? Sjekk om nettstedet oppgir at du trenger andre programmer i tillegg.</li>
+ <li>Noen protokoller krever at tredjeparts programvare eller programtillegg er tilgjengelig, før nettleseren kan gjenkjenne dem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fant ikke filen</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Kan filen ha endret navn, blitt fjernet, eller kanskje endret plassering?</li>
+ <li>Er navnet skrevet korrekt, er stor bokstav liten bokstav byttet om, eller er det andre typografiske feil i adressen?</li>
+ <li>Har du tilstrekkelige tilgangsprivilegier for å åpne filen?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Tilgang til filen ble nektet</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Den kan ha blitt fjernet, flyttet eller filrettighetene forhindrer tilgang.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy avviste tilkoblingen</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Nettleseren er innstilt på å bruke en proxy, men proxy avviste tilkoblingen.</p>
+ <ul>
+ <li>Er nettleserens proxyinnstillinger korrekte? Kontroller innstillingene og prøv igjen.</li>
+ <li>Tillater proxyen tilkoblinger fra dette nettverket?</li>
+ <li>Har du fortsatt problem? Kontakt nettverksansvarlig eller Internett-tilbyderen din for mer hjelp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy server ikke funnet</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Nettleseren er innstilt på å bruke en proxy, men nettleseren klarte ikke å finne proxyen.</p>
+ <ul>
+ <li>Er nettleserens proxyinnstillinger korrekte? Kontroller innstillingene og prøv igjen.</li>
+ <li>Er datamaskinen tilkoblet et aktivt nettverk?</li>
+ <li>Har du fortsatt problem? Kontakt nettverksansvarlig eller Internett-tilbyder for mer hjelp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problemer med skadelig kode</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Nettstedet på %1$s er rapportert som et angrepsnettsted, og er blokkert på grunnlag av sikkerhetsinnstillingene dine.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problemer med uønsket programvarenettsted</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Nettstedet på %1$s er rapportert som at det leverer uønsket programvare, og er blokkert basert på sikkerhetsinnstillingene dine.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problemer med angrepsnettsted</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Nettstedet på %1$s er rapportert som et angrepsnettsted, og er blokkert på grunnlag av sikkerhetsinnstillingene dine.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problemer med villedende nettsted</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Denne nettsiden på %1$s er rapportert som et villedende nettsted, og er blokkert basert på sikkerhetsinnstillingene dine.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sikkert nettsted er ikke tilgjengelig</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Du har aktivert kun-HTTPS-modus for forbedret sikkerhet, og en HTTPS-versjon av <em>%1$s</em> er ikke tilgjengelig.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Fortsett til HTTP-nettstedet</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..08d7e0a728
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">पुनः प्रयास गर्नुहोस्</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">अनुरोध पूर्ण गर्न सकिएन</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[ <p> यस समस्या वा त्रुटिको बारेमा अतिरिक्त जानकारी हाल उपलब्ध छैन। </p> ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">सुरक्षित जडान असफल भयो</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>तपाईंले हेर्न खोज्नु भएको पृष्ठ देखाउन सकिँदैन किनभने प्राप्त डाटाको प्रमाणिकता प्रमाणित गर्न सकिएन।</li>
+ <li>कृपया वेबसाइट मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउनको लागि सम्पर्क गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">सुरक्षित जडान असफल भयो</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>यो सर्भरको कन्फिगरेसनमा समस्या हुन सक्छ, वा कसैले सर्भरको प्रतिरूपण गर्न खोजेको हुन सक्छ।</li>
+ <li>यदि तपाईंले विगतमा यो सर्भरमा सफलतापूर्वक जडान गर्नुभएको छ भने, त्रुटि अस्थायी हुन सक्छ, र तपाईंले केही बेर पछि पुन: प्रयास गर्न सक्नुहुन्छ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">उन्नत…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>कसैले साइटको प्रतिरूपण गर्ने प्रयास गरिरहेको हुन सक्छ र तपाईंले जारी राख्नु हुँदैन।</label>
+ <br><br>
+ <label>वेबसाइटहरूले प्रमाणपत्रहरू मार्फत आफ्नो पहिचान प्रमाणित गर्छन्। %1$s ले <b>%2$s</b> लाई विश्वास गर्दैन किनभने, यसको प्रमाणपत्र जारीकर्ता अज्ञात छ, प्रमाणपत्र स्व-हस्ताक्षरित छ, वा सर्भरले सही मध्यवर्ती प्रमाणपत्रहरू पठाउँदैन।</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">पछाडि जानुहोस् (सिफारिस गरिएको)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">जोखिम स्वीकार्नुहोस् र जारी राख्नुहोस्</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">जडान अवरूद्ध भयो</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>ब्राउजर सफलतापूर्वक जडान भयो, तर जानकारी स्थानान्तरण गर्दा जडान अवरुद्ध भयो। कृपया पुन: प्रयास गर्नुहोस्।</p>
+ <ul>
+ <li>साइट अस्थायी रूपमा अनुपलब्ध वा धेरै व्यस्त हुन सक्छ। केही क्षणमा पुन: प्रयास गर्नुहोस्।</li>
+ <li>यदि तपाईं कुनै पनि पृष्ठहरू लोड गर्न असमर्थ हुनुहुन्छ भने, आफ्नो उपकरणको डाटा वा Wi-Fi जडान जाँच गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">जडान अवधि सकियो</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>अनुरोध गरिएको साइटले जडान अनुरोधको जवाफ दिएन र ब्राउजरले जवाफको प्रतिक्षा गर्न छोडेको छ।</p>
+ <ul>
+ <li>के सर्भरले उच्च माग वा अस्थायी आउटेजको अनुभव गरिरहेको हुन सक्छ? पछि पुन: प्रयास गर्नुहोस्।</li>
+ <li>के तपाइँ अन्य साइटहरू ब्राउज गर्न असमर्थ हुनुहुन्छ? उपकरणको नेटवर्क जडान जाँच गर्नुहोस्।</li>
+ <li>तपाईँको उपकरण वा नेटवर्क फायरवाल वा प्रोक्सी द्वारा सुरक्षित छ? गलत सेटिङहरूले वेब ब्राउजिङमा हस्तक्षेप गर्न सक्छ।</li>
+ <li>अझै पनि समस्या भइरहेको छ? सहायताको लागि आफ्नो नेटवर्क प्रशासक वा इन्टरनेट प्रदायकसँग परामर्श गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">जडान हुन सकेन</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>साइट अस्थायी रूपमा अनुपलब्ध वा धेरै व्यस्त हुन सक्छ। केही क्षणमा पुन: प्रयास गर्नुहोस्।</li>
+ <li>यदि तपाईं कुनै पनि पृष्ठहरू लोड गर्न असमर्थ हुनुहुन्छ भने, आफ्नो उपकरणको डाटा वा Wi-Fi जडान जाँच गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">सर्भरबाट अप्रत्याशित प्रतिक्रिया</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>साइटले अप्रत्याशित तरिकाले नेटवर्क अनुरोधलाई प्रतिक्रिया दियो र ब्राउजरले कार्य जारी राख्न सक्दैन।</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">पृष्ठ राम्रोसँग पुनः निर्देशित भइरहेको छैन</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>ब्राउजरले अनुरोध गरिएको वस्तु पुन: प्राप्त गर्ने प्रयास रोकेको छ। साइटले अनुरोधलाई कहिल्यै पूरा नहुने तरिकामा पुनः निर्देशित गर्दैछ।</p>
+ <ul>
+ <li>के तपाईंले यस साइटलाई आवश्यक पर्ने कुकीजहरू असक्षम वा अवरुद्ध गर्नुभएको छ?</li>
+ <li>यदि साइटको कुकीजहरू स्वीकार गर्दा समस्या समाधान हुँदैन, यो सम्भवतः सर्भर कन्फिगरेसन समस्या हो र तपाईंको उपकरण होइन।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">अफलाइन मोड</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>ब्राउजरले अफलाइन मोडमा काम गरिरहेको छ र अनुरोध गरिएको वस्तुमा जडान हुन सक्दैन।</p>
+ <ul>
+ <li>के तपाइँको यन्त्र सक्रिय नेटवर्कमा जोडिएको छ?</li>
+ <li>अनलाइन मोडमा स्विच गर्न र पृष्ठ पुन: लोड गर्न "पुनः प्रयास गर्नुहोस्" थिच्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">सुरक्षा कारणहरुका लागि पोर्ट प्रतिबन्धित छ</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>अनुरोध गरिएको ठेगानाले पोर्ट निर्दिष्ट गरेको छ (उदाहरणका लागि, mozilla.org मा पोर्ट 80 को लागि <q>mozilla.org:80</q>) सामान्यतया वेब ब्राउजिङ्ग भन्दा <em>अन्य</em> उद्देश्यका लागि प्रयोग गरिन्छ। ब्राउजरले तपाईंको सुरक्षा र सुरक्षाको लागि अनुरोध रद्द गरेको छ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">जडान रिसेट गरिएको थियो</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>एक जडान कुराकानी गर्दा नेटवर्क लिङ्क अवरोध भयो। कृपया पुन: प्रयास गर्नुहोस्।</p>
+ <ul>
+ <li>साइट अस्थायी रूपमा अनुपलब्ध वा धेरै व्यस्त हुन सक्छ। केही क्षणमा पुन: प्रयास गर्नुहोस्।</li>
+ <li>यदि तपाईं कुनै पनि पृष्ठहरू लोड गर्न असमर्थ हुनुहुन्छ भने, आफ्नो उपकरणको डाटा वा Wi-Fi जडान जाँच गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">असुरक्षित फाइल प्रकार</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>कृपया वेबसाइटका मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउन सम्पर्क गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">भ्रष्ट सामग्री त्रुटि</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>तपाईले हेर्न खोज्नु भएको पृष्ठ देखाउन सकिँदैन किनभने डाटा प्रसारणमा त्रुटि पत्ता लाग्यो।</p>
+ <ul>
+ <li>कृपया वेबसाइटका मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउन सम्पर्क गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">सामग्री क्रास भयो</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>तपाईले हेर्न खोज्नु भएको पृष्ठ देखाउन सकिँदैन किनभने, डाटा प्रसारणमा त्रुटि पत्ता लाग्यो।</p>
+ <ul>
+ <li>कृपया वेबसाइटका मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउन सम्पर्क गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">सामग्री इन्कोडिङ्ग त्रुटि</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>तपाईले हेर्न खोज्नु भएको पृष्ठ देखाउन सकिँदैन किनभने, यसले कम्प्रेसनको अमान्य वा असमर्थित रूप प्रयोग गर्दछ।</p>
+ <ul>
+ <li>कृपया वेबसाइट मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउन सम्पर्क गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ठेगाना फेला परेन</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>ब्राउजरले प्रदान गरिएको ठेगानाको लागि होस्ट सर्भर फेला पार्न सकेन।</p>
+ <ul>
+ <li>टाइपिङ्ग त्रुटिहरूको लागि ठेगाना जाँच गर्नुहोस् जस्तै
+ <strong>ww</strong>.example.com को सट्टा
+ <strong>www</strong>.example.com।</li>
+ <li>यदि तपाईं कुनै पनि पृष्ठहरू लोड गर्न असमर्थ हुनुहुन्छ भने, आफ्नो उपकरणको डाटा वा Wi-Fi जडान जाँच गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">इन्टरनेट जडान छैन</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">तपाईंको नेटवर्क जडान जाँच गर्नुहोस् वा केही क्षणमा पृष्ठ पुन: लोड गर्न प्रयास गर्नुहोस् ।</string>
+
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">पुनः लोड गर्नुहोस्</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">अवैध ठेगाना</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>प्रदान गरिएको ठेगाना मान्यता प्राप्त ढाँचामा छैन। कृपया, गल्तीहरूको लागि स्थान पट्टी जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्।</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">यो ठेगाना मान्य छैन</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>वेब ठेगानाहरू सामान्यतया लेखिएका हुन्छन् <strong>http://www.example.com/</strong></li>
+ <li>तपाईंले फर्वार्ड स्ल्यासहरू प्रयोग गरिरहनुभएको छ भन्ने कुरा सुनिश्चित गर्नुहोस् (जस्तै <strong>/</strong>)।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">अज्ञात प्रोटोकल</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>ठेगानाले एउटा प्रोटोकल निर्दिष्ट गर्दछ (जस्तै, <q>wxyz://</q>) ब्राउजरले चिन्न सक्दैन, त्यसैले ब्राउजरले साइटमा राम्रोसँग जडान गर्न सक्दैन।</p>
+ <ul>
+ <li>के तपाइँले मल्टिमिडिया वा अन्य गैर-पाठ सेवाहरू पहुँच गर्न प्रयास गर्दै हुनुहुन्छ? अतिरिक्त आवश्यकताहरूको लागि साइट जाँच गर्नुहोस्।</li>
+ <li>केही प्रोटोकलहरूलाई ब्राउजरले पहिचान गर्न सक्नु अघि तेस्रो-पक्ष सफ्टवेयर वा प्लगइनहरू आवश्यक हुन सक्छ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">फाइल फेला परेन</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>वस्तुको पुन: नामाकरण, हटाइएको वा स्थानान्तरण गर्न सकिन्छ?</li>
+ <li>ठेगानामा हिज्जे, वा अन्य टाइपोग्राफिकल त्रुटि छ?</li>
+ <li>के तपाइँसँग अनुरोध गरिएको वस्तुमा पर्याप्त पहुँच अनुमतिहरू छन्?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">फाइलसम्मको पहुँच अस्वीकृत भयो</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>यो हटाइएको, सारिएको हुन सक्छ, वा फाइल अनुमतिहरूले पहुँच रोक्न सक्छ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">प्रोक्सी सर्भरले जडान अस्वीकार गर्यो</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>ब्राउजरलाई प्रोक्सी सर्भर प्रयोग गर्न कन्फिगर गरिएको छ, तर प्रोक्सीले जडान अस्वीकार गर्यो।</p>
+ <ul>
+ <li>के ब्राउजरको प्रोक्सी कन्फिगरेसन सही छ? सेटिङ्गहरू जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्।</li>
+ <li>के प्रोक्सी सेवाले यस नेटवर्कबाट जडानहरूलाई अनुमति दिन्छ?</li>
+ <li>अझै पनि समस्या भइरहेको छ? सहायताको लागि आफ्नो नेटवर्क प्रशासक वा इन्टरनेट प्रदायकसँग परामर्श गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">प्रोक्सी सर्भर फेला परेन</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>ब्राउजरलाई प्रोक्सी सर्भर प्रयोग गर्न कन्फिगर गरिएको छ, तर प्रोक्सी फेला पार्न सकिएन।</p>
+ <ul>
+ <li>के ब्राउजरको प्रोक्सी कन्फिगरेसन सही छ? सेटिङ्गहरू जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्।</li>
+ <li>के यन्त्र सक्रिय नेटवर्कमा जोडिएको छ?</li>
+ <li>अझै पनि समस्या भइरहेको छ? सहायताको लागि आफ्नो नेटवर्क प्रशासक वा इन्टरनेट प्रदायकसँग परामर्श गर्नुहोस्।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">मालवेयर साइट मामिला</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>%1$s मा रहेको साइटलाई आक्रमण साइटको रूपमा प्रतिबेदन गरिएको छ र तपाईंको सुरक्षा प्राथमिकताहरूको आधारमा ब्लक गरिएको छ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">नरुचाइएको साइट मामिला</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s मा रहेको साइटले नचाहिने सफ्टवेयर सेवा गरिरहेको रिपोर्ट गरिएको छ र तपाईंको सुरक्षा प्राथमिकताहरूको आधारमा ब्लक गरिएको छ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">हानिकारक साइट मामिला</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s मा रहेको साइटलाई सम्भावित रूपमा हानिकारक साइटको रूपमा रिपोर्ट गरिएको छ र तपाईंको सुरक्षा प्राथमिकताहरूको आधारमा ब्लक गरिएको छ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">भ्रामक साइट मामिला</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s मा रहेको यो वेब पृष्ठलाई भ्रामक साइटको रूपमा रिपोर्ट गरिएको छ र तपाईंको सुरक्षा प्राथमिकताहरूको आधारमा ब्लक गरिएको छ।</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">सुरक्षित साइट उपलब्ध छैन</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[तपाईंले परिष्कृत सुरक्षाको लागि HTTPS-मात्र मोड सक्षम गर्नुभएको छ, र <em>%1$s</em> को HTTPS संस्करण उपलब्ध छैन।]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP साइटमा जारी राख्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..a2a869356d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nl/strings.xml
@@ -0,0 +1,396 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Opnieuw proberen</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Kan aanvraag niet voltooien</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Extra informatie over dit probleem of deze fout is momenteel niet beschikbaar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Beveiligde verbinding mislukt</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>De pagina die u wilt bekijken kan niet worden weergegeven, omdat de echtheid van de ontvangen gegevens niet kon worden geverifieerd.</li>
+ <li>Neem contact op met de website-eigenaars om ze over dit probleem te informeren.</li>
+ </ul>
+
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Beveiligde verbinding mislukt</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Dit kan een probleem met de serverconfiguratie zijn, of iemand probeert de server na te bootsen.</li>
+ <li>Als u in het verleden met succes verbinding met deze server hebt gemaakt, kan de fout van tijdelijke aard zijn en kunt u het later nogmaals proberen.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Geavanceerd…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Iemand kan proberen de website na te doen en u moet niet verdergaan.</label>
+ <br><br>
+ <label>Websites tonen hun identiteit aan middels certificaten. %1$s vertrouwt <b>%2$s</b> niet, omdat de certificaatuitgever onbekend is, het certificaat zelfondertekend is, of de server niet de juiste intermediaire certificaten verzendt.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Teruggaan (Aanbevolen)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Het risico aanvaarden en doorgaan</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Deze website vereist een beveiligde verbinding.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>De pagina die probeert te bekijken kan niet worden getoond, omdat deze website een beveiligde verbinding vereist.</li>
+ <li>Het issue ligt waarschijnlijk bij de website en er is niets dat u kunt doen om het op te lossen.</li>
+ <li>U kunt de beheerder van de website op de hoogte stellen van het probleem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Geavanceerd…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> heeft een beveiligingsbeleid genaamd HTTP Strict Transport Security (HSTS), wat betekent dat <b>%2$s</b> er alleen beveiligd mee kan verbinden. U kunt geen uitzondering toevoegen om deze website te bezoeken. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Terug</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">De verbinding werd onderbroken</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>De browser is succesvol verbonden, maar de verbinding is verbroken tijdens het overbrengen van informatie. Probeer het opnieuw.</p>
+ <ul>
+ <li>De website kan tijdelijk niet beschikbaar of te druk zijn. Probeer het over een paar seconden opnieuw.</li>
+ <li>Als u geen pagina’s kunt laden, controleer dan de gegevens of Wi-Fi-verbinding van uw apparaat.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">De wachttijd voor de verbinding is verstreken</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>De opgevraagde website heeft niet op een verbindingsaanvraag geantwoord, en de browser wacht niet meer op een antwoord.</p>
+ <ul>
+ <li>Misschien wordt de server zwaar belast of is deze tijdelijk onbereikbaar? Probeer het later opnieuw.</li>
+ <li>Kunt u geen andere websites bezoeken? Controleer de netwerkverbinding van de computer.</li>
+ <li>Wordt uw computer of netwerk beschermd door een firewall of proxy? Onjuiste instellingen kunnen een goede werking tijdens het webbrowsen verstoren.</li>
+ <li>Hebt u nog steeds problemen? Raadpleeg uw netwerkbeheerder of internetprovider voor assistentie.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Kan geen verbinding maken</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Misschien is de website tijdelijk niet beschikbaar of overbelast. Probeer het over enkele ogenblikken opnieuw.</li>
+ <li>Als u geen enkele pagina kunt laden, controleer dan de gegevens- of wifi-verbinding van uw apparaat.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Onverwacht antwoord van server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>De website antwoordde op een onverwachte manier op de netwerkaanvraag, en de browser kan niet doorgaan.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">De pagina verwijst niet op een juiste manier door</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>De browser is gestopt met pogen het opgevraagde item op te halen. De website verwijst de aanvraag door op een manier die nooit zal worden voltooid.</p>
+ <ul>
+ <li>Hebt u cookies die nodig zijn voor deze website uitgeschakeld of geblokkeerd?</li>
+ <li>Als het accepteren van cookies van deze website het probleem niet oplost, is dit waarschijnlijk een probleem met de serverconfiguratie en niet met uw computer.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offlinemodus</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>De browser werkt momenteel in offlinemodus en kan geen verbinding maken met het opgevraagde item.</p>
+ <ul>
+ <li>Is de computer verbonden met een actief netwerk?</li>
+ <li>Klik op ‘Opnieuw proberen’ om naar de onlinemodus over te schakelen en de pagina opnieuw te laden.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Poort beperkt om veiligheidsredenen</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Het opgevraagde adres specificeert een poort (bv. <q>mozilla.org:80</q> voor poort 80 op mozilla.org) die normaal gesproken voor <em>andere</em> doeleinden dan webbrowsen wordt gebruikt. De browser heeft de aanvraag voor uw bescherming en veiligheid geannuleerd.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">De verbinding werd geherinitialiseerd</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>De netwerkkoppeling werd onderbroken tijdens het onderhandelen over een verbinding. Probeer het opnieuw.</p>
+ <ul>
+ <li>De website is mogelijk tijdelijk niet beschikbaar of te druk. Probeer het over een aantal ogenblikken opnieuw.</li>
+ <li>Als u geen pagina’s kunt laden, controleer dan de gegevens- of Wi-Fi-verbinding van uw apparaat.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Onveilig bestandstype</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Neem contact op met de website-eigenaars om ze over dit probleem te informeren.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Beschadigde-inhoudsfout</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+
+ <p>De pagina die u wilt bekijken kan niet worden weergegeven, omdat er een fout in de gegevensoverdracht is gedetecteerd.</p>
+
+ <ul>
+
+ <li>Neem contact op met de website-eigenaars om ze over dit probleem te informeren.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Inhoud gecrasht</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+
+ <p>De pagina die u wilt bekijken kan niet worden weergegeven, omdat er een fout in de gegevensoverdracht is gedetecteerd.</p>
+
+ <ul>
+
+ <li>Neem contact op met de website-eigenaars om ze over dit probleem te informeren.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Inhoudcoderingsfout</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+
+ <p>De pagina die u wilt bekijken kan niet worden weergegeven, omdat deze gebruikmaakt van een ongeldige of niet-ondersteunde vorm van compressie.</p>
+
+ <ul>
+
+ <li>Neem contact op met de website-eigenaars om ze over dit probleem te informeren.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adres niet gevonden</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+
+ <p>De browser kon de hostserver voor het opgegeven adres niet vinden.</p>
+
+ <ul>
+ <li>Controleer het adres op typefouten, zoals
+
+ <strong>ww</strong>.example.com in plaats van
+
+ <strong>www</strong>.example.com.</li>
+
+ <li>Als u geen pagina’s kunt laden, controleer dan de gegevens- of wifi-verbinding van uw apparaat.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Geen internetverbinding</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Controleer uw netwerkverbinding of probeer de pagina over enkele ogenblikken opnieuw te laden.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Opnieuw laden</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Ongeldig adres</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+
+ <p>Het opgegeven adres heeft geen herkenbare indeling. Controleer de locatiebalk op fouten en probeer het opnieuw.</p>
+
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Het adres is niet geldig</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+
+ <ul>
+
+ <li>Webadressen worden doorgaans geschreven als <strong>http://www.example.com/</strong></li>
+
+ <li>Let erop dat u voorwaartse slashes gebruikt (d.i. <strong>/</strong>).</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Onbekend protocol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+
+ <p>Het adres specificeert een protocol (bv. <q>wxyz://</q>) dat de browser niet herkent, waardoor de browser niet op een juiste manier met de website kan verbinden.</p>
+
+ <ul>
+
+ <li>Probeert u toegang te krijgen tot multimedia- of andere niet-tekstservices? Controleer de website op extra benodigdheden.</li>
+
+ <li>Sommige protocollen kunnen software of plug-ins van derden vereisen voordat de browser ze kan herkennen.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Bestand niet gevonden</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+
+ <ul>
+
+ <li>Kan het item zijn hernoemd, verwijderd of verplaatst?</li>
+
+ <li>Staat er een spel-, hoofdletter- of andere typografische fout in het adres?</li>
+
+ <li>Hebt u voldoende toegangsrechten voor het opgevraagde item?</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Toegang tot het bestand is geweigerd</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+
+ <ul>
+
+ <li>Het kan zijn verwijderd, verplaatst, of bestandsmachtigingen kunnen toegang verhinderen.</li>
+
+ </ul>
+
+
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxyserver weigerde verbinding</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+
+ <p>De browser is geconfigureerd om een proxyserver te gebruiken, maar de proxy weigerde een verbinding.</p>
+
+ <ul>
+
+ <li>Is de proxyconfiguratie van de browser in orde? Controleer de instellingen en probeer het opnieuw.</li>
+
+ <li>Staat de proxyservice verbindingen van dit netwerk toe?</li>
+
+ <li>Hebt u nog steeds problemen? Raadpleeg uw netwerkbeheerder of internetprovider voor assistentie.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxyserver niet gevonden</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+
+ <p>De browser is geconfigureerd om een proxyserver te gebruiken, maar de proxy kon niet worden gevonden.</p>
+
+ <ul>
+
+ <li>Is de proxyconfiguratie van de browser in orde? Controleer de instellingen en probeer het opnieuw.</li>
+
+ <li>Is de computer verbonden met een actief netwerk?</li>
+
+ <li>Hebt u nog steeds problemen? Raadpleeg uw netwerkbeheerder of internetprovider voor assistentie.</li>
+
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Probleem met malware op website</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+
+ <p>De website op %1$s is gerapporteerd als een aanvalsite en is geblokkeerd op basis van uw beveiligingsvoorkeuren.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Probleem met ongewenste website</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+
+ <p>De website op %1$s is gerapporteerd als een website die ongewenste software aanbiedt en is geblokkeerd op basis van uw beveiligingsvoorkeuren.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Probleem met schadelijke website</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+
+ <p>De website op %1$s is gerapporteerd als een mogelijk schadelijke website en is geblokkeerd op basis van uw beveiligingsvoorkeuren.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Probleem met misleidende website</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+
+ <p>De website op %1$s is gerapporteerd als een misleidende website en is geblokkeerd op basis van uw beveiligingsvoorkeuren.</p>
+
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Beveiligde website niet beschikbaar</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[U hebt de Alleen-HTTPS-modus voor verbeterde beveiliging ingeschakeld, en een HTTPS-versie van <em>%1$s</em> is niet beschikbaar.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Doorgaan naar HTTP-website</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..351072f5a7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,332 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Prøv igjen</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Klarte ikkje å fullføre førespurnaden</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Meir informasjon om denne feilen er ikkje tilgjengeleg.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Sikker tilkopling feila</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Sida du prøver å opne kan ikkje visast fordi det ikkje kunne stadfestast at overførte data er autentiske.</li>
+ <li>Kontakt nettstadeigaren og informer om problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Sikker tilkopling feila</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Dette kan vere på grunn av eit problem med innstillingane til serveren, eller det kan vere at nokon prøver å forfalske tilkoplinga til serveren.</li>
+ <li>Dersom du tidlegare har kopla til serveren kan det vere at feilen er kortvarig, og du kan prøve igjen seinare.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avansert…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Nokon prøver å etterlikne nettsida, og du bør ikkje fortsetje.</label>
+ <br><br>
+ <label>Nettstadar beviser identiteten sin via sertifikat. %1$s stolar ikkje på <b>%2$s</b> fordi sertifikatutskrivaren er ukjend, sertifikatet er sjølvsignert, eller fordi serveren ikkje sender dei rette mellomsertifikata.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Gå tilbake (Tilrådd)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Godta risikoen og fortset</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Denne nettstaden krev ei trygg tilkopling.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Sida du prøver å vise kan ikkje visast fordi denne nettstaden krev ei trygg tilkopling.</li>
+ <li>Problemet er mest sannsynleg på nettstaden, og det er ingenting du kan gjere for å løyse det.</li>
+ <li>Du kan varsle administrator for nettstaden om problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avansert…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> har ein sikkerheitspolicy kalt HTTP Strict Transport Security (HSTS), som tyder at <b>%2$s</b> berre kan kople til den sikkert. Du kan ikkje leggje til eit unntak for å besøkje dette nenne nettstaden. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Gå tilbake</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Tilkoplinga vart avbroten</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Nettlesaren vart tilkopla, men tilkoplinga vart avbroten under overføring av informasjon. Prøv igjen.</p>
+ <ul>
+ <li>Sida kan vere kortvarig utilgjengeleg, eller opptatt. Prøv igjen om ei stund.</li>
+ <li>Viss du ikkje klarer å laste sider, kontroller data- eller Wi-Fi-sambandet til eininga di.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Tilkoplinga fekk tidsavbrot</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Nettstaden du spurde etter svarte ikkje på ein tilkoplingsførespurnad og nettlesaren har slutta å vente på svar.</p>
+ <ul>
+ <li>Kan det hende at nettstaden har unormal høg belastning akkurat no, eller er mellombels utilgjengeleg? Prøv igjen seinare.</li>
+ <li>Klarer du å kople til andre nettstadar? Kontroller at nettverkstilkoplinga til datamaskina verkar.</li>
+ <li>Er datamaskina di verna av ein brannmur eller proxy? Feil-innstillinger kan hindre tilgang til nettet.</li>
+ <li>Har du framleis problem? Kontakt systemansvarleg eller Internett-tilbydar for meir hjelp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Kan ikkje kople til</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Sida kan vere mellombels utilgjengeleg, eller opptatt. Prøv igjen om ei stund.</li>
+ <li>Viss du ikkje klarer å laste nokre sider, kontroller data- eller Wi-Fi-sambandet til eininga di.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Uventa svar frå server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Nettstaden svarte på ein førespurnad på ein uventa måte, og nettlesaren kan ikkje fortsetje.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Nettsida vidaresender ikkje skikkeleg</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Nettlesaren har slutta å prøve å laste det ønskte elementet. Nettstaden omdirigerer førespurnaden på ein måte slik at den aldri vil fullføre.</p>
+ <ul>
+ <li>Har du slått av eller blokkert infokapslar som er påkravd av denne nettstaden?</li>
+ <li>Dersom det ikkje hjelper å godta infokapslane til nettstaden, kan det vere eit problem med innstillingane til nettstaden, og ikkje noko problem på datamaskina di.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Fråkopla-modus</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Nettlesaren er i fråkopla modus, og kan ikkje kople til serveren.</p>
+ <ul>
+ <li>Er datamaskina kopla til eit aktivt nettverk?</li>
+ <li>Trykk «Prøv igjen» for å byte til tilkopla modus og laste sida på nytt.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porten har sikkerheitsrestriksjonar</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Den førespurde adressa spesifiserte ein port (t.d. <q>mozilla.org:80</q> for port 80 på mozilla.org) som normalt vert brukt til eit <em>anna</em> føremål enn nettlesing. Nettlesaren braut av førespurnaden av sikkerheitsårsaker.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Tilkoplinga vart avbroten</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Nettverkskoplinga vart avbroten under forhandlingar om ei tilkopling. Prøv igjen.</p>
+ <ul>
+ <li>Sida kan vere mellombels utilgjengeleg, eller opptatt. Prøv igjen om ei stund.</li>
+ <li>Viss du ikkje klarer å laste nokre sider, kontroller data- eller Wi-Fi-sambandet til eininga di.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Usikker filtype</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Kontakt eigaren av nettstaden og informer dei om dette problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Øydelagd innhaldsfeil</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Sida du prøver å vise kan ikkje opnast på grunn av ein feil i dataoverføringa.</p>
+ <ul>
+ <li>Kontakt nettstadeigarane og informer dei om dette problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Innhaldet krasja</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Sida du prøver å vise kan ikkje opnast på grunn av ein feil i dataoverføringa.</p>
+ <ul>
+ <li>Kontakt nettstadeigarane og informer dei om dette problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Feil med teiknkoding</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Nettsida du prøver å opne kan ikkje visast fordi ho brukar ein ukjend eller ugyldig komprimeringsmetode.</p>
+ <ul>
+ <li>Kontakt den ansvarlege for nettstaden og informer dei om problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Fann ikkje adressa</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Nettlesaren fann ikkje serveren for den oppgitte adressa.</p>
+ <ul>
+ <li>Kontroller adressa for skrivefeil som t.d.
+ <strong>ww</strong>.example.com i staden for
+ <strong>www</strong>.example.com.</li>
+ <li>Viss du ikkje kan laste nokre sider, kan du sjekke data- eller Wi-Fi-tilkoplinga til eininga.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Inga internetttilkopling</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Sjekk nettverkstilkoplinga di, eller prøv å laste sida på nytt om ei stund.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Oppdater</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Ugyldig adresse</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Oppgjeven adresse er ikkje i eit attkjennande format. Sjå i adresselinja om det er skrivefeil, og prøv igjen.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adressa er ugyldig</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Nettadresser skriv ein på følgjande måte: <strong>http://www.example.com/</strong></li>
+ <li>Pass på at du brukar framover-skråstrek (dvs. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Ukjend protokoll</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adressa spesifiserer ein protokoll (f.eks. <q>wxyz://</q>) som nettlesaren ikkje forstår, slik at han ikkje kan kople seg skikkeleg til serveren.</p>
+ <ul>
+ <li>Prøver du å få tilgang til multimedia eller anna ikkje-tekstleg kjelde? Sjekk om nettstaden krev andre program i tillegg.</li>
+ <li>Nokre protokollar krev at tredjeparts programvare eller programtillegg er tilgjengelege før nettlesaren kan gjenkjenne dei.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fann ikkje fila</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Kan fila ha endra namn, blitt fjerna, eller kanskje endra plassering?</li>
+ <li>Er namnet skrive korrekt, er stor og liten bokstav bytt om, eller er det andre typografiske feil i adressa?</li>
+ <li>Har du tilstrekkelege tilgangsprivilegium for å opne fila?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Tilgang til fila vart nekta</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Den kan ha blitt fjerna, flytta eller filrettane hindrar tilgang.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy avviste tilkoplinga</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Nettlesaren er innstilt på å bruke ein proxy, men proxyen avviste tilkoplinga.</p>
+ <ul>
+ <li>Er proxyinnstillingane til nettlesaren korrekte? Kontroller innstillingane og prøv igjen.</li>
+ <li>Tillèt proxyen tilkoplingar frå dette nettverket?</li>
+ <li>Har du framleis problem? Kontakt nettverksansvarleg eller Internett-tilbydaren din for meir hjelp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Fann ikkje proxy-serveren</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Nettlesaren er innstilt på å bruke ein proxy, men nettlesaren klarte ikkje å finne proxyen.</p>
+ <ul>
+ <li>Er proxyinnstillingane til nettlesaren korrekte? Kontroller innstillingane og prøv igjen.</li>
+ <li>Er datamaskina tilkopla eit aktivt nettverk?</li>
+ <li>Har du framleis problem? Kontakt nettverksansvarleg eller Internett-tilbydar for meir hjelp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problem med skadeleg kode</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Nettstaden på %1$s er rapportert som ein angrepsnettstad, og er blokkert på grunnlag av sikkerheitsinnstillingane dine.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problem med uønskt programvarenettstad</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Nettstaden på %1$s er rapportert som at det leverer uønskt programvare, og er blokkert basert på sikkerheitsinnstillingane dine.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problem med angrepsnettstad</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Nettstaden på %1$s er rapportert som ein angrepsnettstad, og er blokkert på grunnlag av sikkerheitsinnstillingane dine.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problemer med villeiande nettstad</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Denne nettsida på %1$s er rapportert som ein villeiande nettstad, og er blokkert basert på sikkerheitsinnstillingane dine.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Trygg nettstad ikkje tilgjengeleg</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Du har aktivert modusen berre-HTTPS for å auke sikkerheita, og ein HTTPS-versjon av <em>%1$s</em> er ikkje tilgjengeleg.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Fortset til HTTP-nettstaden</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nv/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nv/strings.xml
new file mode 100644
index 0000000000..892802db49
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-nv/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Bínáánítááh</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Wókeed bóhodoolníłígíí doo bíighah da. </string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Díí ʼachʼįʼ nahwiisʼnááʼígíí hazhóʼó baa haneʼígíí kʼad doo hólǫ́ǫ da.</p>]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..8c2139431b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-oc/strings.xml
@@ -0,0 +1,272 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Tornar ensajar</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Impossible de completar la requèsta</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>I a pas cap d‘informacion disponibla actualament tocant aqueste problèma o error.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Fracàs de la connexion securizada</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>La pagina qu’ensajatz de consultar pòt pas èsser afichada perque l’autenticitat de las donadas recebudas pòt pas èsser verificada.</li>
+ <li>Contactatz los proprietaris del sit Web per los n’assabentar.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">La connexion segura a pas capitat</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Aquò pòt èsser degut a un problèma de configuracion del servidor o a una persona qu’ensaja d’usurpar l’identitat del servidor.</li>
+ <li>Se ja sètz connectat(ada) amb succès a aqueste servidor, benlèu l’error es temporària e podètz ensajar tornamai pus tard.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avançat…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Qualqu’un poiriá usurpar l’identitat del site, deuriatz pas contunhar.</label>
+ <br><br>
+ <label>Los sites web pròvan lor identitat via de certificats. %1$s se fisa pas de <b>%2$s</b> perque lo seu emissor de certificats es desconegut, lo certificat es auto-signat, o lo servidor envia pas los certificats intermediari corrèctes.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Tornar (recomandat)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Acceptar lo risc e contunhar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Aqueste site web requerís una connexion segura.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La pagina qu’ensajatz de visualizar se pòt pas afichar perque aqueste site requerís una connexion segura.</li>
+ <li>Lo problèma ven probablament del site web e i a pas res que poscatz far per lo resòlver;</li>
+ <li>Podètz senhalar lo problèma als administrators del site web.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avançat…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+<label> <b>%1$s</b> a una estrategia de seguretat HTTP Strict Transport Security (HSTS), valent a dire que <b>%2$s</b> pòt sonque s’i connectar amb una connexion securizada. Podètz pas apondre d’excepcion per consultar aqueste site. </label>
+]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Tornar</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La connexion es estada interrompuda</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Lo navegador s’es corrèctament connectat, mas la connexion es estada interrompuda pendent lo transferiment d’informacions. Tornatz ensajar.</p>
+ <ul>
+ <li>Lo site es benlèu pas accessible o subrecargat. Tornatz ensajar dins un momenton</li>
+ <li>Se capitatz pas de navegar sus cap de site, verificatz la connexion de donadas del periferic o la connexion Wi-Fi</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Relambi d’espèra passat</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Lo navegador a esperat tròp longtemps al moment de la connexion al site e a arrestat d’esperar una responsa.</p>
+ <ul>
+ <li>Benlèu que lo servidor es en suscarga o es temporàriament en pana ? Ensajatz mai tard.</li>
+ <li>D’autres sites son tanben inaccessibles ? Verificatz la connexion a la ret de vòstre periferic.</li>
+ <li>Vòstre periferic o vòstra ret es protegida per un parafuòc o un proxy ? De paramètres incorrèctes pòdon interferir amb la navegacion sus lo Web.</li>
+ <li>Avètz totjorn de problèmas ? Consultatz l’administrator de la ret o vòstre provesidor d’accès a Internet per obténer d’ajuda.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Connexion impossibla</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Aqueste site pòt èsser indisponible pel moment o subrecargat. Tornatz ensajar dins una estona.</li>
+ <li>S’es impossible de telecargar quitament una pagina, verificatz las donadas de connexion o Wifi de vòstre periferic mobil</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Responsa inesperada del servidor</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Lo site a respondut a la requèsta de la ret d’una faiçon inesperada e lo navegador pòt pas contunhar.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Redireccion de pagina incorrècta</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Lo navegador a quitat d’esperar una responsa del site. Lo site crèa una redireccion d’un biais que fa qu’acabarà pas jamai.</p>
+ <ul>
+ <li>Avètz desactivat o blocat los cookies necessaris per aqueste site ?</li>
+ <li>Se lo problèma es pas resolgut en acceptant los cookies d’aqueste site, s’agís probablament d’un problèma de configuracion del servidor e non pas de vòstre aparelh.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mòde fòra connexion</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Lo navegador es en mòde fòra connexion e se pòt pas connectar a l’element indicat.</p>
+ <ul>
+ <li>L’ordenador es connectat a la ret ?</li>
+ <li>Clicatz sul boton « Ensajar tornarmai » per tornar en mòde connectat e recargar la pagina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Pòrt restrench per de rasons de seguretat.</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>L’adreça demandada indica un pòrt (per ex.<q>mozilla.org:80</q> pel pòrt 80 sus mozilla.org) qu’es normalament utilizat per d’<em>autres</em> usatges que la navegacion sul Web. Lo navegador a anullat la requèsta per vòstra proteccion e vòstra seguretat.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La connexion es estada reïnicializada</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Lo ligam ret es estat copat pendent la negociacion d’una connexion. Tornatz ensajar.</p>
+ <ul>
+ <li>Lo site es benlèu pas accessible o subrecargat. Tornatz ensajar dins un momenton</li>
+ <li>Se capitatz pas de navegar sus cap de site, verificatz la connexion de donadas del periferic o la connexion Wi-Fi</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipe de fichièr pas segur</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Contactatz lo webmèstre del site per l’assabentar d’aqueste problèma.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Error deguda a un contengut corromput</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>La pagina qu’ensajatz de veire pòt pas èsser afichada perque una error dins la transmission de donadas es estada detectada.</p>
+ <ul>
+ <li>Contactatz los proprietaris del site Web per los assabentar d’aqueste problèma.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Lo contengut a plantat</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>La pagina qu’ensajatz de veire pòt pas èsser afichada perque una error dins la transmission de donadas es estada detectada.</p>
+ <ul>
+ <li>Contactatz los proprietaris del site Web per los assabentar d’aqueste problèma.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error d’encodatge de contengut</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>La pagina qu’ensajatz de veire pòt pas èsser afichada perque utiliza un tipe de compression invalid o pas pres en carga.</p>
+ <ul>
+ <li>Contactatz lo webmèstre del site Web per l’assabentar d’aqueste problèma.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Pagina pas trobada</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Lo navegador a pas pogut trobar lo servidor òste per l’adreça fornida.</p>
+ <ul>
+ <li>Verificatz l’adreça se per cas i a una deca coma
+ <strong>ww</strong>.exemple.com allòc de
+ <strong>www</strong>.exemple.com.</li>
+ <li>Se podètz pas cargar cap de pagina, verificatz la connexion de donada del periferic o Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Cap de connexion Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verificatz vòstra connexion ret o ensajatz de recargar la pagina d’aquí un moment.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Tornar cargar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Adreça invalida</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>L’adreça fornida es pas dins un format reconegut. Mercés de verificar qu’i aja pas cap d’error a la barra d’adreça e tornatz ensajar.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">L’adreça es pas valida</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Las adreças web normalament son aital <strong>http://www.exemple.com/</strong></li>
+ <li>Asseguratz-vos qu’utilizetz las barras inclinadas(<strong>/</strong>).</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocòl desconegut</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>L’adreça indica un protocòl (per ex. <q>wxyz://</q>) desconegut del navegador que doncas se pòt pas connectar corrèctament al site.</p>
+ <ul>
+ <li>Ensajatz d’accedir a de contengut multimèdia o d’autres servicis que son pas de tèxte ? Verificatz los prerequesits logicials del site.</li>
+ <li>D’unes protocòls pòdon necessitar un logiciel tèrç o de plugins per que lo navegador los pòsca reconéisser.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fichièr introbable</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Lo fichièr es benlèu estat renomenat, suprimit o desplaçat ?</li>
+ <li>I a una deca de majuscula, d’accent o una autra error ?</li>
+ <li>Avètz las permissions d’accès que cal per aqueste fichièr ?</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">L’accès al fichièr es estat refusat</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Benlèu es estat suprimit, bolegat o las permissions del fichièr n’empacharián l’accès.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Lo servidor mandatari a refusat la connexion</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Lo navegador es configurat per utilizar un servidor mandatari mas lo proxy a refusat la connexion.</p>
+ <ul>
+ <li>La configuracion proxy del navegador es corrècta ? Verificatz los paramètres e tornatz ensajar.</li>
+ <li>Lo servici proxy autoriza las connexions a partir d’aquesta ret ?</li>
+ <li>Avètz encara de problèmas ? Consultatz vòstre administrator de ret o vòstre provesidor d’accès a Internet per obténer d’ajuda.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Impossible de trobar lo servidor mandatari</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Lo navegador es configurat per utilizar un servidor mandatari mas lo proxy es introbable.</p>
+ <ul>
+ <li>La configuracion proxy del navegador es corrècta ? Verificatz los paramètres e tornatz ensajar.</li>
+ <li>Lo servici proxy autoriza las connexions a partir d’aquesta ret ?</li>
+ <li>Avètz encara de problèmas ? Consultatz vòstre administrator de ret o vòstre provesidor d’accès a Internet per obténer d’ajuda.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problèma de logicial malvolent</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Lo site a l’adreça %1$s es estat senhalat coma comportant de logicials indesirables e es estat blocat segon vòstras preferéncias de seguretat.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problèma de site indesirable</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Lo site a l’adreça %1$s es estat senhalat coma comportant de logicials indesirables e es estat blocat segon vòstras preferéncias de seguretat.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problèma de site perilhós</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Lo site a %1$s es estat senhalat coma un site possiblament malfasent e es estat blocat segon vòstras preferéncias de seguretat.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problèma de site enganaire</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Lo site a l’adreça %1$s es estat senhalat coma un site enganaire e es estat blocat segon vòstras preferéncias de seguretat.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Site securizat non disponible</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Avètz activat lo mòde HTTPS solament e cap de version HTTPS de <em>%1$s</em> es pas disponibla.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Contunhar cap al site HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..fd355fb97d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-or/strings.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ଅନୁରୋଧକୁ ସମ୍ପୂର୍ଣ୍ଣ କରିହେବ ନାହିଁ</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>ଏହି ସମସ୍ୟା ବିଷୟରେ ଅତିରିକ୍ତ ସୂଚନା କିମ୍ବା ତ୍ରୁଟି ବର୍ତ୍ତମାନ ଉପଲବ୍ଧ ନାହିଁ।</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">ସୁରକ୍ଷିତ ସଂଯୋଗ ବିଫଳ ହେଲା</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul> <li>ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବା ପୃଷ୍ଠାକୁ ଦର୍ଶାଯାଇ ପାରିବ ନାହିଁ କାରଣ ଗ୍ରହଣ କରାଯାଇଥିବା ତଥ୍ୟର ବୈଧିକରଣକୁ ଯାଞ୍ଚ କରାଯାଇନାହିଁ।</li> <li>ଏହି ସମସ୍ୟା ବିଷୟରେ ଅଗଚର କରାଇବା ପାଇଁ ଦୟାକରି ୱେବସାଇଟ ମାଲିକଙ୍କ ସହିତ ସମ୍ପର୍କ କରନ୍ତୁ।</li> </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">ସୁରକ୍ଷିତ ସଂଯୋଗ ବିଫଳ ହେଲା</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul> <li>ଏହା ହୁଏତଃ ସର୍ଭର ସଂରଚନା ସମସ୍ୟା ହୋଇପାରେ, କିମ୍ବା ଏହି ସର୍ଭରର କେହି ପରିଚୟ ଚୋରି କରିଥାଇପାରେ।</li> <li>ଯଦି ଆପଣ ଅତୀତରେ ଏହି ସର୍ଭର ସହିତ ସଫଳତାର ସହିତ ସଂଯୁକ୍ତ କରିଛନ୍ତି, ତେବେ ଏହି ତ୍ରୁଟିଟି ଅସ୍ଥାୟୀ ହୋଇପାରେ, ଏବଂ ଆପଣ ପରେ ପୁଣିଥରେ ଚେଷ୍ଟା କରିପାରିବେ।</li> </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ଉନ୍ନତ…</string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ପଛକୁ ଫେରି ଯାଆନ୍ତୁ (ପରାମର୍ଶିତ)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">ଆଶଙ୍କାକୁ ସ୍ୱୀକାର କରନ୍ତୁ ଏବଂ ଆଗାନ୍ତୁ</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">ଉନ୍ନତ…</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">ସଂଯୋଗଟି ବାଧାପ୍ରାପ୍ତ ହୋଇଛି</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">ସଂଯୋଗ ସମୟ ସମାପ୍ତ ହୋଇଯାଇଛି</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>ଅନୁରୋଧ କରାଯାଇଥିବା ସାଇଟଟି ସଂଯୋଗ ପାଇଁ ଅନୁରୋଧକୁ ଉତ୍ତର ଦେଉ ନାହିଁ ଏବଂ ବ୍ରାଉଜର ଉତ୍ତରକୁ ଆଉ ଅପେକ୍ଷା କରୁ ନାହିଁ।</p><ul><li>ସର୍ଭରଟି ଅଧିକ ଭାରାକ୍ରାନ୍ତ ହୋଇଥାଇପାରେ ଅଥବା ଅସ୍ଥାୟୀ ଭାବରେ ଖରାପ ଥାଇପାରେ? ପରେ ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ।</li><li>ଆପଣ ଅନ୍ୟ ସାଇଟଗୁଡ଼ିକୁ ବ୍ରାଉଜ କରିବାରେ ଅକ୍ଷମ କି? କମ୍ପୁଟରର ନେଟୱର୍କ ସଂଯୋଗକୁ ଯାଞ୍ଚ କରନ୍ତୁ।</li><li>ଆପଣଙ୍କର କମ୍ପୁଟର କିମ୍ବା ନେଟୱର୍କ ଫାୟାରୱାଲ କିମ୍ବା ପ୍ରକ୍ସି ଦ୍ୱାରା ପ୍ରତିରୋଧିତ କି? ଭୁଲ ସଂରଚନା ୱେବ ବ୍ରାଉଜିଙ୍ଗରେ ବାଧା ସୃଷ୍ଟି କରିପାରେ।</li><li>ଏପର୍ଯ୍ୟନ୍ତ ସମସ୍ୟା ଅଛି କି? ସହାୟତା ପାଇଁ ଆପଣଙ୍କର ନେଟୱର୍କ ପ୍ରଶାସକଙ୍କ କିମ୍ବା ଇଣ୍ଟରନେଟ ପ୍ରଦାନକାରୀ ସହିତ ଯୋଗାଯୋଗ କରନ୍ତୁ।</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">ସଂଯୋଗ କରିବାରେ ଅସମର୍ଥ</string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">ସର୍ଭରରୁ ଅପ୍ରତ୍ୟାଶିତ ଉତ୍ତର</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>ସାଇଟ ନେଟୱର୍କ ଅନୁରୋଧକୁ ଅପ୍ରତ୍ୟାଶିତ ଭାବରେ ଉତ୍ତର ଦେଇଥାଏ ଏବଂ ବ୍ରାଉଜର ଅଗ୍ରସର ହୋଇନପାରେ।</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ଏହି ପୃଷ୍ଠାଟି ସଠିକ ଭାବରେ ପୁନଃପ୍ରେରଣ କରିପାରୁନାହିଁ</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>ବ୍ରାଉଜର ଅନୁରୋଧ କରାଯାଇଥିବା ବସ୍ତୁକୁ କାଢ଼ିବା ବନ୍ଦ କରିଛି। ସାଇଟ ସେହି ଅନୁରୋଧକୁ କଦାପି ସମ୍ପୂର୍ଣ୍ଣ ହେଉନଥିବା ଉପାୟରେ ଦିଗ ପରିବର୍ତ୍ତନ କରୁଅଛି।</p><ul><li>ଆପଣ ଏହି ସାଇଟ ଦ୍ୱାରା ଆବଶ୍ୟକ କୁକିଗୁଡ଼ିକୁ ନିଷ୍କ୍ରିୟ କରିଛନ୍ତି ଅଥବା ଅଟକ ରଖିଛନ୍ତି?</li><li>ଯଦି ସାଇଟର କୁକିଗୁଡ଼ିକୁ ଗ୍ରହଣ କରିବା ଦ୍ୱାରା ତାହା ସମସ୍ୟାର ସମାଧାନ କରିନଥାଏ, ତେବେ ଏହା ହୁଏତଃ ଆପଣଙ୍କ କମ୍ପୁଟର ପରିବର୍ତ୍ତେ ସର୍ଭର ସଂରଚନା ସମସ୍ୟା ହୋଇପାରେ।</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ଅଫଲାଇନ ଅବସ୍ଥା</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>ବ୍ରାଉଜର ଅଫଲାଇନ ଧାରାରେ ଚାଲୁଅଛି ଏବଂ ଅନୁରୋଧ କରିଥିବା ବସ୍ତୁ ସହିତ ସଂଯୋଗ କରିପାରିବେ ନାହିଁ।</p><ul><li>କମ୍ପୁଟରଟି ସକ୍ରିୟ ନେଟୱର୍କ ସହିତ ସଂଯୁକ୍ତ ହୋଇଛି କି ନାହିଁ?</li><li>&quot; କୁ ଦବାନ୍ତୁ ଏବଂ ଅନଲାଇନ ଧାରାକୁ ପରିବର୍ତ୍ତନ କରିବା ପାଇଁ ଏବଂ ପୃଷ୍ଠାକୁ ପୁନର୍ଧାରଣ କରିବା ପାଇଁ &quot; କୁ ପୁଣିଥରେ ଚେଷ୍ଟାକରନ୍ତୁ।</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">ସୁରକ୍ଷା କାରଣ ହେତୁ ପୋର୍ଟକୁ ଅଟକାଯାଇଛି</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>ଅନୁରୋଧ କରାଯାଇଥିବା ଠିକଣା ଗୋଟିଏ ପୋର୍ଟକୁ ଉଲ୍ଲେଖ କରିଥାଏ (ଯେପରିକି <q>mozilla.org:80</q> mozilla.org ରେ ପୋର୍ଟ 80 ପାଇଁ) ସାଧାରଣତଃ ୱେବ ବ୍ରାଉଜିଙ୍ଗ ବ୍ୟତୀତ <em>ଅନ୍ୟାନ୍ୟ</em> କାର୍ଯ୍ୟ ପାଇଁ ବ୍ୟବହୃତ ହୋଇଥାଏ। ବ୍ରାଉଜର ଆପଣଙ୍କର ପ୍ରତିରକ୍ଷା ଏବଂ ସୁରକ୍ଷା ପାଇଁ ଅନୁରୋଧକୁ ବାତିଲ କରିଛି।</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">ସଂଯୋଗ ପୁନର୍ବିନ୍ୟାସ ହୋଇଥିଲା</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">ଅସୁରକ୍ଷିତ ଫାଇଲ ପ୍ରକାର</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>ଦୟାକରି ଏହି ସମସ୍ୟା ବିଷୟରେ ସୂଚନା ଦେବାକୁ ୱେବସାଇଟ୍ ମାଲିକଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">ତ୍ରୁଟିଯୁକ୍ତ ବିଷୟବସ୍ତୁ ତ୍ରୁଟି</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବା ପୃଷ୍ଠାକୁ ଦର୍ଶାଯାଇ ପାରିବ ନାହିଁ କାରଣ ତଥ୍ୟ ପରିବହନରେ ଗୋଟିଏ ତ୍ରୁଟି ଦେଖା ଦେଇଛି।</p><ul><li>ଏହି ସମସ୍ୟା ବିଷୟରେ ଅଗଚର କରାଇବା ପାଇଁ ଦୟାକରି ୱେବସାଇଟ ମାଲିକଙ୍କ ସହିତ ସମ୍ପର୍କ କରନ୍ତୁ।</li></ul>]]></string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବା ପୃଷ୍ଠାକୁ ଦର୍ଶାଯାଇ ପାରିବ ନାହିଁ କାରଣ ତଥ୍ୟ ପରିବହନରେ ଗୋଟିଏ ତ୍ରୁଟି ଦେଖା ଦେଇଛି।</p><ul><li>ଏହି ସମସ୍ୟା ବିଷୟରେ ଅଗଚର କରାଇବା ପାଇଁ ଦୟାକରି ୱେବସାଇଟ ମାଲିକଙ୍କ ସହିତ ସମ୍ପର୍କ କରନ୍ତୁ।</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">ବିଷୟବସ୍ତୁ ସାଙ୍କେତିକରଣ ତ୍ରୁଟି</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>ଆପଣ ଦେଖିବାକୁ ଚେଷ୍ଟାକରୁଥିବା ପୃଷ୍ଠାକୁ ଦର୍ଶାଯାଇପାରିବ ନାହିଁ କାରଣ ଏହା ଗୋଟିଏ ଅବୈଧ କିମ୍ବା ଅସମର୍ଥିତ ସଙ୍କୋଚନକୁ ବ୍ୟବହାର କରିଥାଏ।</p><ul><li>ଏହି ସମସ୍ୟା ବିଷୟରେ ସୂଚନା ଦେବା ପାଇଁ ଦୟାକରି ୱେବସାଇଟ ମାଲିକଙ୍କ ସହିତ ଯୋଗାଯୋଗ କରନ୍ତୁ।</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ଠିକଣା ମିଳୁନାହିଁ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ଇଣ୍ଟରନେଟ୍ ସଂଯୋଗ ନାହିଁ</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ଆପଣଙ୍କର ନେଟୱର୍କ ସଂଯୋଗ ଦେଖନ୍ତୁ କିମ୍ବା ପୃଷ୍ଠାଟିକୁ ଆଉ କିଛି କ୍ଷଣ ପରେ ପୁନର୍ଧାରଣ କରନ୍ତୁ</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ପୁନର୍ଧାରଣ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ଅବୈଧ ଠିକଣା</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>ଦିଆଯାଇଥିବା ଠିକଣାଟି ପରିଚିତ ଅବସ୍ଥାରେ ନାହିଁ। ଭୁଲ ପାଇଁ ଦୟାକରି ଅବସ୍ଥିତି ପଟିକୁ ଯାଞ୍ଚ କରନ୍ତୁ ଏବଂ ପୁଣି ଚେଷ୍ଟାକରନ୍ତୁ।</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ଏହି ଠିକଣାଟି ବୈଧ ନୁହେଁ</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul> <li>ୱେବ ଠିକଣାଗୁଡ଼ିକ ସାଧାରଣତଃ ଏହି ପ୍ରକାରେ ଲେଖାଯାଇଥାଏ <strong>http://www.example.com/</strong></li> <li>ନିଶ୍ଚିତ କରନ୍ତୁ ଯେ ଆପଣ ତୀର୍ଯକ ରେଖା ବ୍ୟବହାର କରୁଛନ୍ତି (i.e. <strong>/</strong>).</li> </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">ଅଜଣା ପ୍ରୋଟୋକଲ</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>ଏହି ଠିକଣାଟି ଏକ ପ୍ରଟୋକଲକୁ ଉଲ୍ଲେଖ କରିଥାଏ (ଯେପରିକି <q>wxyz://</q>) ବ୍ରାଉଜର ଟିହ୍ନିପାରୁ ନାହିଁ, ତେଣୁ ବ୍ରାଉଜର ସେହି ସାଇଟ ସହିତ ସଠିକ ଭାବରେ ସଂଯୋଗ ସ୍ଥାପନ କରିପାରିବ ନାହିଁ।</p><ul><li>ଆପଣ ମଲଟିମେଡିଆ କିମ୍ବା ଅନ୍ୟାନ୍ୟ ପାଠ୍ୟ-ବିହୀନ ସର୍ଭରଗୁଡ଼ିକ ପାଇଁ ଚେଷ୍ଟା କରୁଛନ୍ତି କି? ଅଧିକ ଆବଶ୍ୟକତା ପାଇଁ ସାଇଟକୁ ଯାଞ୍ଚ କରନ୍ତୁ।</li><li>କିଛି ପ୍ରଟୋକଲଗୁଡ଼ିକ ବ୍ରାଉଜର ସେମାନଙ୍କୁ ଚିହ୍ନିବା ପୂର୍ବରୁ ହୁଏତଃ ତୃତୀୟ ପକ୍ଷ ସଫ୍ଟୱେର କିମ୍ବା ପ୍ଲଗଇନଗୁଡ଼ିକୁ ଆବଶ୍ୟକ କରିପାରନ୍ତି।</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ଫାଇଲ ମିଳୁନାହିଁ</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul><li>ବସ୍ତୁଗୁଡ଼ିକୁ ପୁନଃ ନାମକରଣ, ଅପସାରଣ, କିମ୍ବା ସ୍ଥାନାନ୍ତରଣ କରିହେବ କି?</li><li>ଏହି ଠିକଣାରେ ବନାନ ତ୍ରୁଟି, ପୁଞ୍ଜିକରଣ, କିମ୍ବା ଅନ୍ୟାନ୍ୟ ଛପାଯୋଗ୍ୟ ତ୍ରୁଟି ଅଛି କି?</li><li>ଅନୁରୋଧ କରାଯାଇଥିବା ବସ୍ତୁ ପାଇଁ ଯଥେଷ୍ଟ ଅଭିଗମ୍ୟ ଅନୁମତି ଅଛି କି?</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ଫାଇଲକୁ ପ୍ରବେଶକୁ ବାରଣ କରାଯାଇଥିଲା</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ପ୍ରକ୍ସି ସର୍ଭର ସଂଯୋଗ ପାଇଁ ମନା କରିଲା</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>ବ୍ରାଉଜରଟି ଏକ ପ୍ରକ୍ସି ସର୍ଭର ବ୍ୟବହାର ପାଇଁ ବିନ୍ୟାସିତ ହୋଇଛି, କିନ୍ତୁ ସେହି ପ୍ରକ୍ସି ସଂଯୋଗ ପାଇଁ ବାରଣ କରିଛି।</p><ul><li>ବ୍ରାଉଜରର ପ୍ରକ୍ସି ସଂରଚନା ସଠିକ ଅଛି କି? ସଂରଚନାକୁ ଯାଞ୍ଚ କରନ୍ତୁ ଏବଂ ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ।</li><li>ପ୍ରକ୍ସି ସର୍ଭିସ ଏହି ନେଟୱର୍କରୁ ସଂଯୋଗକୁ ଅନୁମତି ଦେଇଥାଏ କି?</li><li>ଏପର୍ଯ୍ୟନ୍ତ ସମସ୍ୟା ଅଛି କି? ସହାୟତା ପାଇଁ ଆପଣଙ୍କର ନେଟୱର୍କ ପ୍ରଶାସକଙ୍କ କିମ୍ବା ଇଣ୍ଟରନେଟ ପ୍ରଦାନକାରୀ ସହିତ ଯୋଗାଯୋଗ କରନ୍ତୁ।</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ପ୍ରକ୍ସି ସର୍ଭର ମିଳୁନାହିଁ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">ମାଲୱେୟାର ୱେବସାଇଟ ସମସ୍ୟା</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ଅନାବଶ୍ୟକ ୱେବସାଇଟ ସମସ୍ୟା</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ହାନିକାରକ ସାଇଟ ସମସ୍ୟା</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..745f2d5bb0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ਬੇਨਤੀ ਪੂਰੀ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>ਇਸ ਸਮੱਸਿਆ ਬਾਰੇ ਵਧੀਕ ਜਾਣਕਾਰੀ ਜਾਂ ਗਲਤੀ ਇਸ ਵੇਲੇ ਉਪਲਬੱਧ ਨਹੀਂ ਹੈ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">ਸੁਰੱਖਿਅਤ ਕੁਨੈਕਸ਼ਨ ਫੇਲ੍ਹ ਹੋਇਆ</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>ਜਿਸ ਸਫ਼ੇ ਨੂੰ ਤੁਸੀਂ ਵੇਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ, ਉਹ ਨਹੀਂ ਦਿਖਾਇਆ ਜਾ ਸਕਦਾ ਕਿਉਂਕਿ ਮਿਲੇ ਡਾਟੇ ਦੀ ਪ੍ਰਮਾਣਿਕਤਾ ਦੀ ਪੁਸ਼ਟੀ ਨਹੀਂ ਹੋ ਸਕੀ।</li>
+ <li>ਇਸ ਸਮੱਸਿਆ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦੇਣ ਲਈ ਵੈਬਸਾਈਟ ਮਾਲਕਾਂ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">ਸੁਰੱਖਿਅਤ ਕੁਨੈਕਸ਼ਨ ਫੇਲ੍ਹ ਹੋਇਆ</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>ਇਹ ਸਰਵਰ ਦੀ ਸੰਰਚਨਾ ਕਰਕੇ ਸਮੱਸਿਆ ਹੋ ਸਕਦੀ ਹੈ ਜਾਂ ਕੋਈ ਸਰਵਰ ਦੀ ਨਕਲ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਿਹਾ ਹੈ।</li>
+ <li>ਜੇ ਤੁਸੀਂ ਪਹਿਲਾਂ ਵੀ ਇਸ ਸਰਵਰ ਨਾਲ ਠੀਕ ਤਰ੍ਹਾਂ ਕਨੈਕਟ ਹੁੰਦੇ ਰਹੇ ਹੋ ਤਾਂ ਗਲਤੀ ਆਰਜ਼ੀ ਹੋ ਸਕਦਾ ਹੈ ਅਤੇ ਤੁਸੀਂ ਬਾਅਦ ‘ਚ ਕੋਸ਼ਿਸ਼ ਕਰ ਸਕਦੇ ਹੋ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ਤਕਨੀਕੀ…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>ਕੋਈ ਸਾਈਟ ਦੀ ਨਕਲ ਕਰਦਾ ਹੋ ਸਕਦਾ ਹੈ ਅਤੇ ਤੁਹਾਨੂੰ ਜਾਰੀ ਨਹੀਂ ਰੱਖਣਾ ਚਾਹੀਦਾ ਹੈ।</label>
+ <br><br>
+ <label>ਵੈੱਬਸਾਈਟਾਂ ਆਪਣੀ ਪਛਾਣ ਸਰਟੀਫਿਕੇਟ ਰਾਹੀਂ ਸਿੱਧ ਕਰਦੀਆਂ ਹਨ। %1$s <b>%2$s</b> ਉੱਤੇ ਭਰੋਸਾ ਨਹੀਂ ਕਰਦਾ ਹੈ, ਕਿਉਂਕਿ ਇਹ ਸਰਟੀਫਿਕੇਟ ਨੂੰ ਜਾਰੀ ਕਰਨ ਵਾਲਾ ਅਣਪਛਾਤਾ ਹੈ, ਸਰਟੀਫਿਕੇਟ ਖੁਦ-ਦਸਤਖਤੀ ਹੈ ਜਾਂ ਸਰਵਰ ਠੀਕ ਵਿਚਕਾਰਲੇ ਸਰਟੀਫਿਕੇਟ ਨਹੀਂ ਭੇਜ ਰਿਹਾ ਹੈ।</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ਪਿੱਛੇ ਜਾਓ (ਸਿਫਾਰਸ਼ੀ)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">ਖ਼ਤਰਾ ਮੰਨੋ ਤੇ ਜਾਰੀ ਰੱਖੋ</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">ਇਸ ਵੈੱਬਸਾਈਟ ਲਈ ਸੁਰੱਖਿਅਤ ਕਨੈਕਸ਼ਨ ਚਾਹੀਦਾ ਹੈ।</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>ਤੁਸੀਂ ਜਿਸ ਸਫ਼ੇ ਨੂੰ ਖੋਲ੍ਹਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ, ਉਸ ਇਸ ਵੈੱਬਸਾਈਟ ਵਲੋਂ ਸੁਰੱਖਿਅਤ ਕਨੈਕਸ਼ਨ ਜ਼ਰੂਰੀ ਹੋਣ ਕਰਕੇ ਦਿਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ ਹੈ।</li>
+ <li>ਮਸਲਾ ਅਕਸਰ ਵੈੱਬਸਾਈਟ ਨਾਲ ਹੈ ਅਤੇ ਇਸ ਨੂੰ ਠੀਕ ਕਰਨ ਲਈ ਤੁਸੀਂ ਕੁਝ ਵੀ ਨਹੀਂ ਕਰ ਸਕਦੇ ਹੋ।</li>
+ <li>ਤੁਸੀਂ ਸਮੱਸਿਆ ਬਾਰੇ ਵੈੱਬਸਾਈਟ ਦੇ ਪਰਸ਼ਾਸਕ ਨੂੰ ਜਾਣਕਾਰੀ ਦੇ ਸਕਦੇ ਹੋ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">…ਤਕਨੀਕੀ</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ਦੀ ਸੁਰੱਖਿਆ ਪਾਲਸੀ ਹੈ, ਜਿਸ ਨੂੰ HTTP ਸਟਰਿਕ ਟਰਾਂਸਪੋਰਟ ਸਕਿਉਰਟੀ (HSTS) ਕਹਿੰਦੇ ਹਨ, ਜਿਸ ਦਾ ਅਰਥ ਹੈ ਕਿ <b>%2$s</b> ਨੂੰ ਸਿਰਫ਼ ਸੁਰੱਖਿਅਤ ਢੰਗ ਨਾਲ ਹੀ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ। ਤੁਸੀਂ ਇਸ ਸਾਈਟ ਨੂੰ ਛੋਟ ਨਹੀਂ ਦਿੱਤੀ ਜਾ ਸਕਦੀ ਹੈ।</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">ਪਿੱਛੇ ਜਾਓ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">ਕਨੈਕਸ਼ਨ ‘ਚ ਰੁਕਾਵਟ ਆਈ ਸੀ</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>ਬਰਾਊਜ਼ਰ ਕਾਮਯਾਬੀ ਨਾਲ ਕਨੈਕਟ ਹੋ ਗਿਆ, ਪਰ ਜਾਣਕਾਰੀ ਤਬਦੀਲ ਕਰਨ ਦੇ ਦੌਰਾਨ ਕਨੈਕਸ਼ਨ ‘ਚ ਰੁਕਾਵਟ ਆਈ ਸੀ। ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।</p>
+ <ul>
+ <li>ਸਾਈਟ ਆਰਜ਼ੀ ਤੌਰ ‘ਤੇ ਅਣ-ਉਪਲਬਧ ਹੋ ਸਕਦੀ ਹੈ ਜਾਂ ਜ਼ਿਆਦਾ ਰੁਝੀ ਹੋ ਸਕਦੀ ਹੈ। ਕੁਝ ਪਲ਼ਾਂ ‘ਚ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।</li>
+ <li>ਜੇ ਤੁਸੀਂ ਕੋਈ ਵੀ ਸਫ਼ਾ ਲੋਡ ਨਹੀਂ ਕਰ ਸਕਦੇ ਹੋ ਤਾਂ ਆਪਣੇ ਡਿਵਾਈਸ ਦੇ ਡਾਟੇ ਜਾਂ Wi-Fi ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">ਕਨੈਕਸ਼ਨ ਲਈ ਸਮਾਂ ਸਮਾਪਤ ਹੈ</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>ਮੰਗ ਕੀਤੀ ਸਾਇਟ ਨੇ ਸੰਪਰਕ ਦੀ ਬੇਨਤੀ ਦਾ ਜਵਾਬ ਨਹੀਂ ਦਿੱਤਾ ਅਤੇ ਬਰਾਊਜ਼ਰ ਨੇ ਜਵਾਬ ਲਈ ਉਡੀਕ ਕਰਨੀ ਛੱਡ ਦਿੱਤੀ ਹੈ।</p>
+ <ul>
+ <li>ਕੀ ਸਰਵਰ ਦੀ ਮੰਗ ਬਹੁਤ ਵੱਧ ਹੋ ਗਈ ਹੋ ਸਕਦੀ ਹੈ ਜਾਂ ਆਰਜ਼ੀ ਤੌਰ ‘ਤੇ ਸਮੱਸਿਆ ਹੋ ਸਕਦੀ ਹੈ? ਬਾਅਦ ‘ਚ ਕੋਸ਼ਿਸ਼ ਕਰੋ।</li>
+ <li>ਕੀ ਤੁਸੀਂ ਹੋਰ ਸਾਈਟਾਂ ਨੂੰ ਬਰਾਊਜ਼ ਕਰ ਸਕਦੇ ਹੋ? ਕੰਪਿਊਟਰ ਦੇ ਨੈੱਟਵਰਕ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।</li>
+ <li>ਕੀ ਤੁਹਾਡਾ ਕੰਪਿਊਟਰ ਜਾਂ ਨੈੱਟਵਰਕ ਫਾਇਰਵਾਲ ਜਾਂ ਪਰਾਕਸੀ ਰਾਹੀਂ ਸੁਰੱਖਿਅਤ ਹੈ? ਗਲਤ ਸੈਟਿੰਗਾਂ ਵੈੱਬ ਬਰਾਊਜ਼ ਕਰਨ ਦੇ ਰਾਹ ਵਿੱਚ ਅੜਿੱਕਾ ਪਾ ਸਕਦੀਆਂ ਹਨ।</li>
+ <li>ਹਾਲੇ ਵੀ ਮੁਸ਼ਕਲ ਆ ਰਹੀ ਹੈ? ਸਹਾਇਤਾ ਲਈ ਆਪਣੇ ਨੈੱਟਵਰਕ ਪਰਸ਼ਾਸ਼ਕ ਜਾਂ ਇੰਟਰਨੈੱਟ ਦੇਣ ਵਾਲੇ ਨਾਲ ਸੰਪਰਕ ਕਰੋ</li>
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">ਕਨੈਕਟ ਕਰਨ ਲਈ ਅਸਮਰੱਥ ਹੈ</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>ਸਾਈਟ ਆਰਜ਼ੀ ਰੂਪ ਵਿੱਚ ਬੰਦ ਹੋ ਸਕਦੀ ਹੈ ਜਾਂ ਬਹੁਤ ਰੁੱਝੀ ਹੋ ਸਕਦੀ ਹੈ। ਕੁੱਝ ਕੁ ਪਲਾਂ ਵਿੱਚ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।</li>
+ <li>ਜੇ ਤੁਸੀਂ ਕੋਈ ਵੀ ਵਰਕਾ ਲੋਡ ਨਹੀਂ ਕਰ ਸਕਦੇ ਹੋ ਤਾਂ ਆਪਣੇ ਡਿਵਾਈਸ ਦੇ ਡਾਟੇ ਜਾਂ Wi-Fi ਕੁਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ ਜੀ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">ਸਰਵਰ ਤੋਂ ਅਣਚਿਤਵਿਆ ਜਵਾਬ ਮਿਲਿਆ</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>ਸਾਇਟ ਨੈੱਟਵਰਕ ਬੇਨਤੀ ਨੂੰ ਇੱਕ ਅਣਜਾਣੇ ਢੰਗ ਨਾਲ ਜਵਾਬ ਦੇ ਰਹੀ ਹੈ ਅਤੇ ਬਰਾਊਜ਼ਰ ਜਾਰੀ ਨਹੀਂ ਰੱਖ ਸਕਦਾ ਹੈ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ਸਫ਼ੇ ਲਈ ਠੀਕ ਤਰ੍ਹਾਂ ਮੁੜ-ਦਿਸ਼ਾ ਪਰਿਵਰਤਨ ਨਹੀਂ ਕੀਤਾ ਗਿਆ</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>ਬਰਾਊਜਰ ਨੇ ਮੰਗ ਕੀਤੀ ਚੀਜ਼ ਪ੍ਰਾਪਤ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਨੂੰ ਰੋਕ ਦਿੱਤਾ ਹੈ। ਸਾਇਟ ਬੇਨਤੀ ਨੂੰ ਇਸ ਢੰਗ ਨਾਲ ਦਿਸ਼ਾ ਪਰਿਵਰਤਨ ਕਰ ਰਹੀ ਹੈ ਜੋ ਕਿ ਕਦੇ ਪੂਰੀ ਨਹੀਂ ਹੋਵੇਗੀ।</p>
+ <ul>
+ <li>ਕੀ ਤੁਸੀਂ ਇਸ ਸਾਈਟ ਲਈ ਚਾਹੀਦੇ ਕੂਕੀਜ਼ ਅਸਮਰੱਥ ਕੀਤੇ ਜਾਂ ਪਾਬੰਦੀ ਲਗਾਏ ਹਨ?</li>
+ <li>ਜੇ ਸਾਈਟ ਦੇ ਕੂਕੀਜ਼ ਨੂੰ ਮਨਜ਼ੂਰ ਕਰਨ ਨਾਲ ਸਮੱਸਿਆ ਹੱਲ ਨਹੀਂ ਹੁੰਦੀ ਹੈ ਤਾਂ ਇਹ ਸਰਵਰ ਦੀ ਸੰਰਚਨਾ ਨਾਲ ਮਸਲਾ ਹੋ ਸਕਦਾ, ਨਾ ਕਿ ਤੁਹਾਡੇ ਕੰਪਿਊਟਰ ਨਾਲ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ਆਫ਼ਲਾਈਨ ਢੰਗ</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>ਬਰਾਊਜ਼ਰ ਆਫ਼ਲਾਈਨ ਢੰਗ ਵਿੱਚ ਕੰਮ ਕਰ ਰਿਹਾ ਹੈ ਅਤੇ ਮੰਗ ਕੀਤੀ ਚੀਜ਼ ਨਾਲ ਕਨੈਕਟ ਨਹੀਂ ਕਰ ਸਕਦਾ ਹੈ।</p>
+ <ul>
+ <li>ਕੀ ਕੰਪਿਊਟਰ ਸਰਗਰਮ ਨੈੱਟਵਰਕ ਨਾਲ ਕਨੈਕਟ ਹੈ?</li>
+ <li>ਆਨਲਾਈਨ ਢੰਗ ‘ਚ ਜਾਣ ਅਤੇ ਸਫ਼ੇ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰਨ ਲਈ “ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ” ਨੂੰ ਦਬਾਓ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">ਸੁਰੱਖਿਆ ਕਾਰਨਾਂ ਕਰਕੇ ਪੋਰਟ ਉੱਤੇ ਪਾਬੰਦੀ ਲਗਾਈ</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>ਮੰਗੇ ਗਏ ਸਿਰਨਾਵੇਂ ਨੇ ਪੋਰਟ ਦਿੱਤੀ ਹੈ, (ਜਿਵੇਂ ਕਿ <q>mozilla.org:80</q> mozilla.org ਲਈ 809) ਜੋ ਕਿ ਆਮ ਕਰਕੇ ਵਰਤੀ ਜਾਂਦੀ ਹੈ, <em>ਹੋਰ</em> ਵੈਬ ਝਲਕੀ ਤੋਂ ਬਿਨਾਂ। ਬਰਾਊਜ਼ਰ ਨੇ ਤੁਹਾਡੀ ਬਚਾਅ ਅਤੇ ਸੁਰੱਖਿਆ ਲਈ ਬੇਨਤੀ ਨੂੰ ਰੱਦ ਕਰ ਦਿੱਤਾ ਹੈ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">ਕਨੈਕਸ਼ਨ ਮੁੜ-ਸੈੱਟ ਕੀਤਾ ਗਿਆ ਸੀ</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>ਸੰਪਰਕ ਦੇ ਸਮਝੌਤੇ ਵੇਲੇ ਨੈੱਟਵਰਕ ਲਿੰਕ ਵਿੱਚ ਰੁਕਾਵਟ ਆਈ ਸੀ। ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।</p>
+ <ul>
+ <li>ਸਾਈਟ ਆਰਜ਼ੀ ਤੌਰ ‘ਤੇ ਉਪਲਬਧ ਨਹੀਂ ਹੋ ਸਕਦੀ ਜਾਂ ਬਹੁਤ ਰੁੱਝੀ ਹੋ ਸਕਦੀ ਹੈ। ਕੁੱਝ ਪਲ਼ਾਂ ਵਿੱਚ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।</li>
+ <li>ਜੇ ਤੁਸੀਂ ਕੋਈ ਵੀ ਵਰਕਾ ਲੋਡ ਨਹੀਂ ਕਰ ਸਕਦੇ ਹੋ ਤਾਂ ਆਪਣੇ ਡਿਵਾਈਸ ਦੇ ਡਾਟੇ ਜਾਂ Wi-Fi ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">ਅਸੁਰੱਖਿਅਤ ਫਾਈਲ ਕਿਸਮ</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>ਇਸ ਸਮੱਸਿਆ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦੇਣ ਲਈ ਵੈੱਬਸਾਈਟ ਦੇ ਮਾਲਕਾਂ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">ਖਰਾਬ ਹੋਈ ਦੀ ਸਮੱਗਰੀ ਗਲਤੀ</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>ਸਫ਼ਾ, ਜੋ ਤੁਸੀਂ ਵੇਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਨੂੰ ਵੇਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ ਹੈ, ਕਿਉਂਕਿ ਡਾਟਾ ਲੈਣ-ਦੇਣ ਵਿੱਚ ਗਲਤੀ ਖੋਜੀ ਗਈ ਹੈ।</p>
+ <ul>
+ <li>ਇਹ ਸਮੱਸਿਆ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦੇਣ ਲਈ ਵੈੱਬਸਾਈਟ ਮਾਲਕਾਂ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">ਸਮੱਗਰੀ ਕਰੈਸ਼ ਹੋਈ</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>ਜਿਸ ਵਰਕੇ ਨੂੰ ਤੁਸੀਂ ਵੇਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ ਉਹ ਵੇਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ ਹੈ, ਕਿਉਂਕਿ ਡਾਟਾ ਲੈਣ-ਦੇਣ ਵਿੱਚ ਗਲਤੀ ਖੋਜੀ ਗਈ ਹੈ।</p>
+ <ul>
+ <li>ਇਹ ਸਮੱਸਿਆ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦੇਣ ਲਈ ਵੈੱਬਸਾਈਟ ਮਾਲਕਾਂ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">ਸਮੱਗਰੀ ਇੰਕੋਡਿੰਗ ਗਲਤੀ</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>ਸਫ਼ਾ, ਜਿਸ ਨੂੰ ਤੁਸੀਂ ਵੇਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ, ਵੇਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ, ਕਿਉਂਕਿ ਇਹ ਗਲਤ ਜਾਂ ਗ਼ੈਰ-ਸਹਾਇਕ ਕੰਪਰੈਸ਼ਨ ਫਾਰਮ ਵਰਤਦਾ ਹੈ।</p>
+ <ul>
+ <li>ਇਹ ਸਮੱਸਿਆ ਬਾਰੇ ਵੈੱਬ ਸਾਇਟ ਦੇ ਓਨਰ (ਮਾਲਕ) ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ਸਿਰਨਾਵਾਂ ਨਹੀਂ ਲੱਭਿਆ</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>ਬਰਾਊਜ਼ਰ ਦਿੱਤੇ ਸਿਰਨਾਵਾਂ ਲਈ ਹੋਸਟ ਸਰਵਰ ਲੱਭ ਨਹੀਂ ਸਕਿਆ ਸੀ।</p>
+ <ul>
+ <li>ਲਿਖਣ ਦੀਆਂ ਗਲਤੀਆਂ ਲਈ ਸਿਰਨਾਵੇਂ ਦੀ ਜਾਂਚ ਕਰੋ ਜਿਵੇਂ ਕਿ
+ <strong>www</strong>.example.com ਦੀ ਬਜਾਏ
+ <strong>ww</strong>.example.com ਲਿਖਣਾ</li>
+ <li>ਜੇ ਤੁਸੀਂ ਕੋਈ ਵੀ ਸਫ਼ਾ ਲੋਡ ਕਰਨ ਲਈ ਅਸਮਰੱਥ ਹੋ ਤਾਂ ਆਪਣੇ ਡਿਵਾਈਸ ਦੇ ਡਾਟੇ ਜਾਂ Wi-Fi ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ਕੋਈ ਇੰਟਰਨੈੱਟ ਕਨੈਕਸ਼ਨ ਨਹੀਂ ਹੈ</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ਆਪਣੇ ਨੈੱਟਵਰਕ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ ਜਾਂ ਕੁਝ ਪਲ਼ਾਂ ਵਿੱਚ ਸਫ਼ੇ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ।</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ਮੁੜ-ਲੋਡ ਕਰੋ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ਅਢੁੱਕਵਾਂ ਸਿਰਨਾਵਾਂ</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>ਦਿੱਤਾ ਗਿਆ ਪਤਾ ਪਰਵਾਨਤ ਬਣਤਰ ਨਾਲ ਮੇਲ ਨਹੀਂ ਖਾਂਦਾ। ਗਲਤੀਆਂ ਲਈ ਟਿਕਾਣਾ ਪੱਟੀ ਦੀ ਜਾਂਚ ਕਰੋ ਤੇ ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ਸਿਰਨਾਵਾਂ ਵਾਜਬ ਨਹੀਂ ਹੈ</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>ਵੈੱਬ ਸਿਰਨਾਵੇਂ ਨੂੰ ਆਮ ਤੌਰ <strong>http://www.example.com/</strong> ਵਜੋਂ ਲਿਖਿਆ ਜਾਂਦਾ ਹੈ</li>
+ <li>ਧਿਆਨ ਦਿਓ ਕਿ ਤੁਸੀਂ ਫਾਰਵਰਡ ਸਲੈਸ਼ (ਜਿਵੇਂ <strong>/</strong>) ਹੀ ਵਰਤ ਰਹੇ ਹੋ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">ਅਣਪਛਾਤਾ ਪਰੋਟੋਕਾਲ</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>ਸਿਰਨਾਵਾਂ ਪਰੋਟੋਕਾਲ (ਜਿਵੇਂ ਕਿ <q>wxyz://</q>) ਦਿੰਦਾ ਹੈ, ਜਿਸ ਦੀ ਪਛਾਣ ਬਰਾਊਜ਼ਰ ਨਹੀਂ ਕਰ ਸਕਦਾ ਹੈ, ਇਸਕਰਕੇ ਬਰਾਊਜ਼ਰ ਸਾਈਟ ਨਾਲ ਠੀਕ ਤਰ੍ਹਾਂ ਕਨੈਕਟ ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ।</p>
+ <ul>
+ <li>ਕੀ ਤੁਸੀਂ ਮਲਟੀਮੀਡਿਆ ਜਾਂ ਹੋਰ ਗ਼ੈਰ-ਲਿਖਤ ਸੇਵਾ ਨੂੰ ਵਰਤਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ? ਸਾਈਟਾਂ ਦੀ ਵਾਧੂ ਲੋੜਾਂ ਨੂੰ ਚੈੱਕ ਕਰੋ</li>
+ <li>ਕੁਝ ਪਰੋਟੋਕਾਲ ਲਈ ਸੁਤੰਤਰ ਧਿਰ ਸਾਫਟਵੇਅਰ ਜਾਂ ਪਲੱਗਇਨ ਚਾਹੀਦੇ ਹੋ ਸਕਦੇ ਹਨ ਤਾਂ ਕਿ ਬਰਾਊਜ਼ਰ ਉਹਨਾਂ ਦੀ ਪਛਾਣ ਕਰ ਸਕੇ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ਫਾਈਲ ਨਹੀਂ ਲੱਭੀ</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>ਆਈਟਮ ਦਾ ਨਾਂ ਬਦਲਿਆ ਗਿਆ ਹੋ ਸਕਦਾ ਹੈ, ਹਟਾਇਆ ਗਿਆ ਹੋ ਸਕਦਾ ਹੈ ਜਾਂ ਹੋਰ ਥਾਂ ਭੇਜੀ ਗਈ ਹੋ ਸਕਦੀ ਹੈ?</li>
+ <li>ਕੋਈ ਸ਼ਬਦ ਗਲਤ ਲਿਖੇ ਗਏ ਹਨ, ਅੱਖਰ ਵੱਡੇ ਛੋਟੋ ਹੋ ਗਏ ਹਨ ਜਾਂ ਸਿਰਨਾਵੇਂ ਵਿੱਚ ਕੋਈ ਅੱਖਰ ਦੀ ਗਲਤੀ ਹੈ?</li>
+ <li>ਕੀ ਤੁਹਾਡੇ ਕੋਲ ਮੰਗੀ ਗਈ ਆਈਟਮ ਦੀ ਵਰਤੋਂ ਲਈ ਢੁੱਕਵੇਂ ਅਧਿਕਾਰ ਹਨ?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ਫਾਈਲ ਲਈ ਪਹੁੰਚ ਤੋਂ ਨਾਂਹ ਕੀਤੀ</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>ਇਸਨੂੰ ਹਟਾਇਆ, ਕਿਤੇ ਹੋਰ ਭੇਜਿਆ ਗਿਆ ਹੋ ਸਕਦਾ ਹੈ ਜਾਂ ਫਾਈਲ ਮੰਨਜੂਰੀਆਂ ਇਸ ਤੱਕ ਪਹੁੰਚ ਹੋਣ ਤੋ ਰੋਕ ਰਹੀਆਂ ਹੋਣਗੀਆਂ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ਪਰਾਕਸੀ ਸਰਵਰ ਨੇ ਕਨੈਕਸ਼ਨ ਤੋਂ ਇਨਕਾਰ ਕੀਤਾ</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>ਬਰਾਊਜ਼ਰ ਨੂੰ ਪਰਾਕਸੀ ਸਰਵਰ ਵਰਤਣ ਲਈ ਸੰਰਚਿਤ ਕੀਤਾ ਗਿਆ ਹੈ, ਪਰ ਪਰਾਕਸੀ ਸਰਵਰ ਨੇ ਕਨੈਕਸ਼ਨ ਤੋਂ ਇਨਕਾਰ ਕਰ ਦਿੱਤਾ ਹੈ।</p>
+ <ul>
+ <li>ਕੀ ਬਰਾਊਜ਼ਰ ਦੀ ਪਰਾਕਸੀ ਸੰਰਚਨਾ ਠੀਕ ਹੈ? ਸੈਟਿੰਗਾਂ ਚੈੱਕ ਕਰਕੇ ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।</li>
+ <li>ਕੀ ਪਰਾਕਸੀ ਸੇਵਾ ਇਸ ਨੈੱਟਵਰਕ ਤੋਂ ਕੁਨੈਕਸ਼ਨ ਮਨਜ਼ੂਰ ਕਰਦੀ ਹੈ?</li>
+ <li>ਹਾਲੇ ਵੀ ਸਮੱਸਿਆ ਹੈ? ਮੱਦਦ ਲਈ ਆਪਣੇ ਨੈੱਟਵਰਕ ਪਰਸ਼ਾਸ਼ਕ ਜਾਂ ਇੰਟਰਨੈੱਟ ਪਰੋਵਾਇਡਰ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ਪਰਾਕਸੀ ਸਰਵਰ ਨਹੀਂ ਲੱਭਿਆ</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>ਬਰਾਊਜ਼ਰ ਨੂੰ ਪਰਾਕਸੀ ਸਰਵਰ ਵਰਤਣ ਲਈ ਸੰਰਚਿਤ ਕੀਤਾ ਗਿਆ ਹੈ, ਪਰ ਪਰਾਕਸੀ ਲੱਭਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ।</p>
+ <ul>
+ <li>ਕੀ ਬਰਾਊਜ਼ਰ ਦੀ ਪਰਾਕਸੀ ਸੰਰਚਨਾ ਠੀਕ ਹੈ? ਸੈਟਿੰਗਾਂ ਚੈੱਕ ਕਰਕੇ ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।</li>
+ <li>ਕੀ ਕੰਪਿਊਟਰ ਸਰਗਰਮ ਨੈੱਟਵਰਕ ਨਾਲ ਕਨੈਕਟ ਹੈ?</li>
+ <li>ਹਾਲੇ ਵੀ ਸਮੱਸਿਆ ਹੈ? ਮਦਦ ਲਈ ਆਪਣੇ ਨੈੱਟਵਰਕ ਪਰਸ਼ਾਸ਼ਕ ਜਾਂ ਇੰਟਰਨੈੱਟ ਪਰੋਵਾਇਡਰ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">ਮਾਲਵੇਅਰ ਸਾਈਟ ਮਸਲਾ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+      <p>%1$s ਤੇ ਸਾਈਟ ਨੂੰ ਇੱਕ ਅਟੈਕ ਸਾਈਟ ਵਜੋਂ ਦੱਸਿਆ ਗਿਆ ਹੈ ਅਤੇ ਤੁਹਾਡੀ ਸੁਰੱਖਿਆ ਤਰਜੀਹਾਂ ਦੇ ਅਧਾਰ ਤੇ ਬਲੌਕ ਕੀਤਾ ਗਿਆ ਹੈ।</p>
+    ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ਬੇਲੋੜੀ ਸਾਈਟ ਮਸਲਾ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s ਤੋਂ ਸਾਈਟ ਨੂੰ ਬੇਲੋੜੇ ਸਾਫਟਵੇਅਰ ਵੰਡਣ ਵਾਲੇ ਵਜੋਂ ਗਰਦਾਨਿਆ ਗਿਆ ਹੈ ਅਤੇ ਤੁਹਾਡੀਆਂ ਸੁਰੱਖਿਆ ਪਸੰਦਾਂ ਦੇ ਮੁਤਾਬਕ ਪਾਬੰਦੀ ਲਗਾਈ ਹੈ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ਨੁਕਸਾਨਦੇਹ ਸਾਈਟ ਮਸਲਾ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s ਉਤਲੀ ਸਾਈਟ ਦੀ ਸੰਭਾਵਿਤ ਹਮਲਾਵਰ ਸਾਈਟ ਵਜੋਂ ਇਤਲਾਹ ਹੋਈ ਹੈ ਅਤੇ ਤੁਹਾਡੀਆਂ ਸੁਰੱਖਿਆ ਤਰਜੀਹਾਂ ਦੇ ਮੁਤਾਬਕ ਇਸ ਉੱਤੇ ਪਾਬੰਦੀ ਲਗਾਈ ਹੈ।</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">ਭਰਮਪੂਰਨ ਸਾਈਟ ਮਸਲਾ</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s ਤੋਂ ਇਸ ਵੈੱਬ ਸਫ਼ੇ ਨੂੰ ਭਰਮਪੂਰਕ ਸਾਈਟ ਵਜੋਂ ਇਤਲਾਹ ਦਿੱਤੀ ਗਈ ਹੈ ਅਤੇ ਤੁਹਾਡੀਆਂ ਸੁਰੱਖਿਆ ਪਸੰਦਾਂ ਦੇ ਮੁਤਾਬਕ ਪਾਬੰਦੀ ਲਗਾਈ ਹੈ।</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">ਸੁਰੱਖਿਅਤ ਸਾਈਟ ਮੌਜੂਦ ਨਹੀਂ ਹੈ</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[ਤੁਸੀਂ ਵਧਾਈ ਸੁਰੱਖਿਆ ਵਾਸਤੇ ਸਿਰਫ਼-HTTPS ਢੰਗ ਸਮਰੱਥ ਕੀਤਾ ਹੋਇਆ ਹੈ, ਅਤੇ <em>%1$s</em> ਦਾ HTTPS ਵਰਜ਼ਨ ਮੌਜੂਦ ਨਹੀਂ ਹੈ।]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP ਸਾਈਟ ਨਾਲ ਜਾਰੀ ਰੱਖੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..fa2c85734f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">فیر کرو</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">بنیتی پوری نہیں کیتی جا سکدی</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>ایس مسئلے بارے ودھیک جاݨکاری یا غلطی ایس ویلے اپلبدھ نہیں اے۔</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">سرکھیت جوڑن توں غلطی ہو گئی اے</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>جیس صفحے نوں تسیں ویکھݨ دی کوشش کر رہے او، اوہ نہیں دکھایا جا سکدا کیوں‌کہ ملے ڈیٹے دی پرماݨکتا دی پشٹی نہیں ہو سکی۔</li>
+ <li>ایس سمسیا بارے جاݨکاری دیݨ لئی سائٹ مالکاں نال دسیو۔</li>
+ </ul>
+
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">سرکھیت جوڑن توں غلطی ہو گئی اے</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>ایہہ سرور دی سنرچنا کرکے سمسیا ہو سکدی اے یا کوئی سرور دی نکل کرن دی کوشش کر رہا اے۔</li>
+ <li>جے تسیں پہلاں وی ایس سرور نال ٹھیک طرحاں جوڑن ہندے رہے او تاں غلطی آرزی ہو سکدا اے تے تسیں بعد چ کوشش کر سکدے او۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">اضافی…</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">پتہ نہیں لبھیاں</string>
+
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">پھر لوڈ کرو</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">غلط پتہ</string>
+
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">غیر سرکھیت دی سائٹ نوں فیر وی جاؤ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..6dfdee71f2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pl/strings.xml
@@ -0,0 +1,309 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Spróbuj ponownie</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Wystąpił błąd</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Dodatkowe informacje o tym problemie lub błędzie nie są obecnie dostępne.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Bezpieczne połączenie się nie powiodło</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Otwierana strona nie może zostać wyświetlona, ponieważ nie udało się potwierdzić autentyczności otrzymanych danych.</li>
+ <li>Proszę poinformować właścicieli witryny o tym problemie.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Bezpieczne połączenie się nie powiodło</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Może to być problem konfiguracji serwera lub próba podania się za ten serwer przez podmiot nieuprawniony.</li>
+ <li>Jeśli łączono się wcześniej z tym serwerem, błąd może być tymczasowy i należy spróbować ponownie później.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Zaawansowane…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Ktoś może próbować podszywać się pod tę witrynę. Odradzamy kontynuowanie.</label>
+ <br><br>
+ <label>Witryny potwierdzają swoją tożsamość poprzez certyfikaty. %1$s nie ufa certyfikatowi witryny <b>%2$s</b>, ponieważ jego wystawca jest nieznany, jest samopodpisany lub serwer nie przesyła właściwych certyfikatów pośrednich.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Wróć do poprzedniej strony (zalecane)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Akceptuję ryzyko, kontynuuj</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Witryna wymaga zabezpieczonego połączenia.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Otwierana strona nie może zostać wyświetlona, ponieważ witryna wymaga zabezpieczonego połączenia.</li>
+ <li>Problem leży prawdopodobnie po stronie witryny i nie masz możliwości jego rozwiązania.</li>
+ <li>Możesz powiadomić administratora witryny o problemie.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Zaawansowane…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> Witryna „<b>%1$s</b>” określa poprzez HSTS (HTTP Strict Transport Security), że <b>%2$s</b> ma się z nią łączyć jedynie w sposób zabezpieczony. Dodanie wyjątku w celu odwiedzenia tej witryny jest niemożliwe. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Wróć do poprzedniej strony</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Przerwane połączenie</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Przeglądarka nawiązała połączenie, ale zostało ono przerwane podczas przesyłania informacji.</p>
+ <ul>
+ <li>Witryna może być tymczasowo niedostępna lub przeciążona. Spróbuj ponownie za pewien czas.</li>
+ <li>Jeśli nie możesz otworzyć żadnej strony, sprawdź swoje połączenie sieciowe.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Przekroczono czas oczekiwania</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Witryna przez dłuższy czas nie odpowiedziała na żądanie połączenia i przeglądarka przestała czekać na odpowiedź.</p>
+ <ul>
+ <li>Witryna może być tymczasowo niedostępna lub przeciążona. Spróbuj ponownie za pewien czas.</li>
+ <li>Jeśli nie możesz otworzyć żadnej strony, sprawdź swoje połączenie sieciowe.</li>
+ <li>Jeśli to urządzenie jest chronione przez zaporę sieciową lub serwer proxy, sprawdź, czy ten program jest uprawniony do łączenia się z Internetem.</li>
+ <li>Jeśli nadal występują problemy, skonsultuj się z administratorem sieci lub dostawcą usług internetowych.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Nie można połączyć</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Strona może być tymczasowo niedostępna lub przeciążona. Spróbuj ponownie za chwilę.</li>
+ <li>Jeśli nie możesz otworzyć żadnej strony, sprawdź swoje połączenie z Internetem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Nieoczekiwana odpowiedź serwera</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Witryna odpowiedziała w sposób nieoczekiwany i przeglądarka nie może kontynuować.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Nieprawidłowe przekierowanie strony</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Przeglądarka przerwała próby pobrania żądanego elementu. Witryna przekierowuje żądanie w sposób, który uniemożliwia jego dokończenie.</p>
+ <ul>
+ <li>Czy ciasteczka zostały wyłączone lub zablokowane dla tej witryny?</li>
+ <li>Jeśli włączenie obsługi ciasteczek dla tej witryny nie rozwiązuje problemu, najprawdopodobniej jest to problem w konfiguracji serwera, a nie oprogramowania na urządzeniu użytkownika.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Tryb offline</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Przeglądarka pracuje w trybie offline i nie może pobrać żądanego elementu.</p>
+ <ul>
+ <li>Czy urządzenie podłączone jest do działającej sieci?</li>
+ <li>Naciśnij „Spróbuj ponownie”, aby przejść do trybu online i ponownie wczytać stronę.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Zastrzeżony adres</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Żądany adres zawiera numer portu (np. w adresie <q>mozilla.org:80</q> liczba 80 to port na serwerze mozilla.org), który zazwyczaj <em>nie jest</em> wykorzystywany do przeglądania witryn WWW. Przeglądarka anulowała to żądanie ze względów bezpieczeństwa.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Przerwane połączenie</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Połączenie sieciowe zostało przerwane podczas negocjacji.</p>
+ <ul>
+ <li>Witryna może być tymczasowo niedostępna lub przeciążona. Spróbuj ponownie za pewien czas.</li>
+ <li>Jeśli nie możesz otworzyć żadnej strony, sprawdź swoje połączenie sieciowe.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Niebezpieczny typ pliku</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Proszę poinformować właścicieli witryny o tym problemie.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Błąd: treść uszkodzona</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Otwierana strona nie może zostać wyświetlona, ponieważ wykryto błąd w transmisji danych.</p>
+ <ul>
+ <li>Proszę poinformować właścicieli witryny o tym problemie.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Zawartość uległa awarii</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Otwierana strona nie może zostać wyświetlona, ponieważ wykryto błąd w transmisji danych.</p>
+ <ul>
+ <li>Proszę poinformować właścicieli witryny o tym problemie.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Błąd kodowania zawartości</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Otwierana strona nie może zostać wyświetlona, ponieważ używa nieprawidłowych lub nieobsługiwanych metod kompresji.</p>
+ <ul>
+ <li>Proszę poinformować właścicieli witryny o tym problemie.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Nie odnaleziono adresu</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Przeglądarka nie mogła odnaleźć adresu serwera dla podanego adresu.</p>
+ <ul>
+ <li>Upewnij się, że wprowadzony adres nie zawiera takich literówek, jak
+ <strong>ww</strong>.example.com zamiast
+ <strong>www</strong>.example.com.</li>
+ <li>Jeśli nie możesz otworzyć żadnej strony, sprawdź połączenie sieciowe.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Brak połączenia z Internetem</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Sprawdź połączenie z Internetem lub spróbuj za chwilę ponownie wczytać stronę.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Wczytaj ponownie</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Nieprawidłowy adres</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Podano adres w nierozpoznawalnym formacie. Sprawdź, czy w pasku adresu nie ma błędów, a następnie spróbuj ponownie.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adres jest nieprawidłowy</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Adresy internetowe są zwykle postaci <strong>http://www.example.com/</strong></li>
+ <li>Upewnij się, że adres zawiera prawidłowe ukośniki (tzn. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Nieznany protokół</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adres zawiera protokół (np. <q>wxyz://</q>), który nie jest rozpoznawany przez przeglądarkę. Nie może więc ona poprawnie połączyć się z daną witryną.</p>
+ <ul>
+ <li>Czy próbowano korzystać z multimediów lub z innych usług nieopartych na tekście? Sprawdź, czy witryna nie ma dodatkowych wymagań.</li>
+ <li>Obsługa niektórych protokołów może wymagać oprogramowania lub wtyczek dostarczonych przez zewnętrznych producentów.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Nie odnaleziono pliku</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Możliwe, że element ten został usunięty, przeniesiony lub zmieniono mu nazwę.</li>
+ <li>Sprawdź, czy w podanym adresie nie ma błędu w pisowni, w tym wielkości liter, ani innych błędów typograficznych.</li>
+ <li>Upewnij się, czy masz odpowiednie uprawnienia do przeglądania żądanej strony.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Odmowa dostępu do pliku</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Plik mógł zostać usunięty, przeniesiony lub jego uprawnienia uniemożliwiają dostęp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Serwer proxy odrzucił połączenie</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Przeglądarka została skonfigurowana tak, aby używać serwera proxy, który jednak odrzucił połączenie.</p>
+ <ul>
+ <li>Czy konfiguracja serwerów proxy w przeglądarce jest prawidłowa? Sprawdź ustawienia i spróbuj ponownie.</li>
+ <li>Upewnij się, że serwer proxy dopuszcza połączenia z tej sieci.</li>
+ <li>Jeśli nadal występują problemy, skonsultuj się z administratorem sieci lub dostawcą usług internetowych.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Nie odnaleziono serwera proxy</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Przeglądarka została skonfigurowana tak, aby używać serwera proxy, ale serwer proxy nie może zostać odnaleziony.</p>
+ <ul>
+ <li>Czy konfiguracja serwerów proxy w przeglądarce jest prawidłowa? Sprawdź ustawienia i spróbuj ponownie.</li>
+ <li>Upewnij się, że urządzenie jest podłączone do działającej sieci.</li>
+ <li>Jeśli nadal występują problemy, skonsultuj się z administratorem sieci lub dostawcą usług internetowych.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Witryna ze złośliwym oprogramowaniem</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Witryna „%1$s” została zgłoszona jako stanowiąca zagrożenie i została zablokowana zgodnie z ustawieniami bezpieczeństwa.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Witryna z niechcianym oprogramowaniem</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Witryna „%1$s” została zgłoszona jako rozprowadzająca niechciane oprogramowanie i została zablokowana zgodnie z ustawieniami bezpieczeństwa.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Potencjalnie szkodliwa witryna</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Witryna „%1$s” została zgłoszona jako potencjalnie szkodliwa i została zablokowana zgodnie z ustawieniami bezpieczeństwa.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Podejrzana witryna</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Witryna „%1$s” została zgłoszona jako przypadek oszustwa i została zablokowana zgodnie z ustawieniami bezpieczeństwa.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Zabezpieczona witryna jest niedostępna</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[W celu zwiększenia bezpieczeństwa włączony jest tryb używania wyłącznie protokołu HTTPS, a wersja HTTPS witryny <em>%1$s</em> nie jest dostępna.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Otwórz witrynę przez HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ppl/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ppl/strings.xml
new file mode 100644
index 0000000000..34cb6932aa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ppl/strings.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Shikejeku uksenpa</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Tesu welik muajshitia ne tajtanilis</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Sanuk tesu nemi ukse informacion ipanpa ini uwijkayut u tajtakul.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Tesu welik muchiwa se musalulis seguruj</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Ne iswat taja tikneki tikita tesu weli muneshtia ika tesu weli tikneltia asu ijkia ne itukey ne datos tikwijtiwit .</li>
+ <li>Shiknutza ne itejtekuyu ne sitioj matapan pal tiknawatilia ini uijkayu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Tesu welik muchiwa se musalulis seguruj</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Ini anka se uijkayu iwan ne iconfiguración ne servidor, u anka se akaj kiejekua tashijshikua mukwepa ne servidor.</li>
+ <li>Asu ikman taja timusalujtuk yek itech ini servidor, ne tatakul yu ishtuna chupi, wan tiweli taejekua nemanha.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Chayawtuk…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Se akaj kiejekua tashijshikua mukwepa ne sitioj wan te nemi pal tipanu.</label>
+ <br><br>
+ <label>Ne tzawalsitioj ina ka yajasan ipanpa certificados. %1$s tesu kineltuka <b>%2$s</b> ika ne kimaka certificados tesu muishmati, ne certificadoj kiwawasu itukey isel, u ne servidor tesu kititania ne yejyek certificados intermedioj.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Shimukwepa (tanawatilis)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Nikwi ne riesgoj wan nipanu</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ini tzawalsitioj kineki se tasalulis seguroj.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Ne iswat tikneki tikita tesu weli muneshtia ika ini sitioj matapan kineki se tasalulis seguruj.</li>
+ <li>Ini uijkayu anka iwan ne sitioj matapan, wan tesu tiweli tikchiwa te tatka pal tikishtia.</li>
+ <li>Tiweli tiknawatilia ne administrador pal ini sitioj ini uijkayu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Chayawtuk…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Shimukwepa</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Ne tasalulis kutunik</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ne tasalulis tamik</string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Tesu welik musalua</string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Ne servidor tesu nankilia ken muchiya.</string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Ne iswat tesu yek tatijtitania senpa.</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modoj kupintuk</string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Tesu weli kalaki tik ne puertoj ipanpa ne tajpiyalis.</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ne tasalulis mupewaltijtuk senpa.</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Ne tipoj archivoj tesajsay</string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Tatakul kalijtik palantuk</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Ne tay nemi kalijtik kichiwki tajtakul</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Tajtakul pal ne itasenputzulis ne tay nemi kalijtik.</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Ne dirección tesu muajsik</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Te musalujtuk tech Internet</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Takimiltia senpa</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Tesu muajsituk ne archivoj</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..868c286373
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,327 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Tentar novamente</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Não foi possível concluir a solicitação</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Informações adicionais sobre este problema ou erro não estão disponíveis no momento.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Falha na conexão segura</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>A página que você está tentando ver não pode ser exibida porque a autenticidade dos dados recebidos não pôde ser comprovada.</li>
+ <li>Entre em contato com os responsáveis pelo site para informar este problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Falha na conexão segura</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Pode ser um problema com a configuração do servidor, ou alguém tentando se passar por ele.</li>
+ <li>Caso já tenha conseguido se conectar com este servidor antes, talvez o erro seja temporário e você pode tentar novamente mais tarde.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avançado…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Alguém pode estar tentando se passar pelo site e você não deve continuar.</label>
+ <br><br>
+ <label>Os sites comprovam a própria identidade por meio de certificados. O %1$s não confia em <b>%2$s</b> porque o emissor do certificado é desconhecido, o certificado é autoassinado, ou o servidor não está enviando os certificados intermediários corretos.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Voltar (recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceitar o risco e continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Este site requer uma conexão segura.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>A página que você está tentando ver não pode ser exibida porque este site requer uma conexão segura.</li>
+ <li>O problema provavelmente está no site e não há nada que você possa fazer para resolver.</li>
+ <li>Você pode notificar o administrador do site sobre o problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avançado…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> tem uma diretiva de segurança chamada HTTP Strict Transport Security (HSTS), que significa que o <b>%2$s</b> só pode se conectar a ele com segurança. Você não pode adicionar uma exceção para visitar este site. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Voltar</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">A conexão foi interrompida</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>O navegador se conectou com sucesso, mas a conexão foi interrompida durante a transferência de informações. Tente novamente.</p>
+ <ul>
+ <li>O site pode estar temporariamente indisponível ou ocupado demais. Tente novamente daqui a pouco.</li>
+ <li>Se não estiver conseguindo carregar nenhuma página, verifique a conexão de dados móveis ou WiFi do seu dispositivo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Atingiu o limite de tempo da conexão</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>O site solicitado não respondeu a um pedido de conexão e o navegador deixou de esperar uma resposta.</p>
+ <ul>
+ <li>O servidor pode estar sobrecarregado ou temporariamente indisponível? Tente novamente mais tarde.</li>
+ <li>Não consegue navegar para outros sites? Verifique a conexão de rede do computador.</li>
+ <li>Seu computador ou a rede são protegidos por um firewall ou proxy? Configurações incorretas podem interferir na navegação.</li>
+ <li>O problema persiste? Peça ajuda ao seu administrador de rede ou ao suporte do provedor de internet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Não foi possível conectar</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>O site pode estar temporariamente indisponível ou ocupado demais. Tente novamente daqui a pouco.</li>
+ <li>Se não estiver conseguindo carregar nenhuma página, verifique a conexão de dados móveis ou WiFi do seu dispositivo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Resposta não esperada do servidor</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>O site respondeu à solicitação de rede de forma não esperada e o navegador não pode continuar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">A página não está redirecionando corretamente</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>O navegador deixou de tentar obter o item solicitado. O site está redirecionando o pedido de uma forma que nunca será concluído.</p>
+ <ul>
+ <li>Você desativou ou bloqueou cookies exigidos por este site?</li>
+ <li>Se aceitar cookies do site não resolver, é provavelmente um problema de configuração do servidor, não no seu computador.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo offline</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>O navegador está operando em modo offline e não consegue se conectar com o item solicitado.</p>
+ <ul>
+ <li>O computador está conectado a uma rede ativa?</li>
+ <li>Pressione “Tentar novamente” para sair do modo offline e recarregar a página.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porta restringida por motivos de segurança</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>O endereço solicitado especifica uma porta (por exemplo, <q>mozilla.org:80</q> para a porta 80 em mozilla.org) normalmente usada para propósitos <em>diferentes</em> da navegação na web. O navegador cancelou a solicitação para sua proteção e segurança.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">A conexão foi reiniciada</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>O link de rede foi interrompido ao negociar uma conexão. Tente novamente.</p>
+ <ul>
+ <li>O site pode estar temporariamente indisponível ou ocupado demais. Tente novamente daqui a pouco.</li>
+ <li>Se não estiver conseguindo carregar nenhuma página, verifique a conexão de dados móveis ou WiFi do seu dispositivo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de arquivo não seguro</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Entre em contato com os responsáveis pelo site para informar o problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Erro de conteúdo corrompido</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>A página que você está tentando ver não pode ser exibida porque foi detectado um erro na transmissão de dados.</p>
+ <ul>
+ <li>Entre em contato com os responsáveis pelo site para informar o problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Conteúdo travado</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>A página que você está tentando ver não pode ser exibida porque foi detectado um erro na transmissão de dados.</p>
+ <ul>
+ <li>Entre em contato com os responsáveis pelo site para informar o problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Erro na codificação do conteúdo</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>A página que você está tentando ver não pode ser exibida porque ela usa uma forma de compressão inválida ou não suportada.</p>
+ <ul>
+ <li>Entre em contato com os responsáveis pelo site para informar o problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Endereço não encontrado</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>O navegador não conseguiu encontrar o servidor de hospedagem do endereço fornecido.</p>
+ <ul>
+ <li>Verifique se há erros de digitação no endereço, como
+ <strong>ww</strong>.example.com em vez de
+ <strong>www</strong>.example.com.</li>
+ <li>Se não estiver conseguindo carregar nenhuma página, verifique a conexão de dados móveis ou WiFi do seu dispositivo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Sem conexão com a internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifique sua conexão de rede ou tente recarregar a página daqui a pouco.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recarregar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Endereço inválido</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>O endereço fornecido não está em um formato conhecido. Verifique se há erros na barra de endereços e tente novamente.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">O endereço não é válido</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Endereços web geralmente são escritos como <strong>http://www.example.com/</strong></li>
+ <li>Verifique se está usando barras comuns (ou seja, <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo desconhecido</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>O endereço especifica um protocolo (por exemplo, <q>wxyz://</q>) que o navegador não reconhece, assim ele não consegue se conectar corretamente com o site.</p>
+ <ul>
+ <li>Você está tentando acessar conteúdo multimídia ou outro serviço que não seja de texto? Verifique se o site exige requisitos adicionais.</li>
+ <li>Alguns protocolos podem necessitar de softwares ou plugins de terceiros para que o navegador possa reconhecer.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Arquivo não encontrado</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>O item pode ter sido renomeado, removido ou realocado?</li>
+ <li>Há algum erro de grafia, maiúsculas/minúsculas, ou outro erro tipográfico no endereço?</li>
+ <li>Você tem suficiente permissão de acesso ao item solicitado?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">O acesso ao arquivo foi negado</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Ele pode ter sido removido, movido, ou as permissões do arquivo podem estar impedindo o acesso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Servidor proxy recusou a conexão</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>O navegador está configurado para usar um servidor proxy, mas o proxy recusou uma conexão.</p>
+ <ul>
+ <li>A configuração de proxy do navegador está correta? Verifique a configuração e tente novamente.</li>
+ <li>O serviço proxy permite conexões a partir desta rede?</li>
+ <li>O problema persiste? Peça ajuda ao administrador de rede ou ao suporte do provedor de internet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Servidor proxy não encontrado</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>O navegador está configurado para usar um servidor proxy, mas o proxy não foi encontrado.</p>
+ <ul>
+ <li>A configuração de proxy do navegador está correta? Verifique a configuração e tente novamente.</li>
+ <li>O computador está conectado a uma rede ativa?</li>
+ <li>O problema persiste? Peça ajuda ao administrador de rede ou ao suporte do provedor de internet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de site com malware</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>O site em %1$s foi denunciado como foco de ataques e foi bloqueado com base nas suas preferências de segurança.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de site indesejado</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>O site em %1$s foi denunciado por servir software indesejado e foi bloqueado com base nas suas preferências de segurança.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de site prejudicial</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>O site em %1$s foi denunciado como potencialmente prejudicial e foi bloqueado com base nas suas preferências de segurança.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de site enganoso</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Esta página web em %1$s foi denunciada como um site enganoso e foi bloqueada com base nas suas preferências de segurança.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Site seguro não disponível</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Você ativou o modo somente HTTPS para maior segurança, mas uma versão HTTPS de <em>%1$s</em> não está disponível.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar para a versão HTTP do site</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..aaecfccb97
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,280 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Tentar novamente</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Não é possível concluir o pedido</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Neste momento, não está disponível informação adicional sobre este problema ou erro.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">A ligação segura falhou</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>A página que está a tentar ver não pode ser mostrada porque não foi possível verificar a autenticidade dos dados recebidos.</li>
+ <li>Por favor, reporte o problema aos proprietários do site.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">A ligação segura falhou</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Isto pode dever-se a um problema de configuração do servidor, ou pode ser alguém a tentar usurpar a identidade do servidor.</li>
+ <li>Se conseguiu ligar-se com sucesso a este servidor no passado, o erro poderá ser temporário e poderá tentar novamente mais tarde.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avançado…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Alguém pode estar a tentar usurpar a identidade do site e não deve continuar.</label>
+ <br><br>
+ <label>Os sites comprovam a sua identidade via certificados. O %1$s não confia em <b>%2$s</b> porque o emissor do certificado é desconhecido, o certificado é auto-assinado ou o servidor não está a enviar os certificados intermédios corretos.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Retroceder (recomendado)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceitar o risco e continuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Este website precisa de uma ligação segura.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>A página que está a tentar visualizar não pode ser exibida porque este website precisa de uma ligação segura.</li>
+ <li>O problema provavelmente está no website e não há nada que possa fazer para resolvê-lo.</li>
+ <li>Pode notificar o administrador do website sobre o problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avançado…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label> <b>%1$s</b> tem uma política de segurança chamada HTTP Strict Transport Security (HSTS), que significa que o <b>%2$s</b> apenas pode ligar-se com este protocolo. Não é possível adicionar uma exceção para este site.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Retroceder</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">A ligação foi interrompida</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>O navegador estabeleceu a ligação com sucesso, mas a ligação foi interrompida enquanto transferia informação. Por favor, tente novamente.</p>
+ <ul>
+ <li>O site pode estar temporariamente indisponível ou demasiado ocupado. Tente novamente dentro de alguns instantes.</li>
+ <li>Se não consegue carregar quaisquer páginas, confirme a ligação de dados ou de rede sem fios.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">A ligação expirou</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>O site solicitado não respondeu ao pedido de ligação e o navegador deixou de esperar por uma resposta.</p>
+ <ul>
+ <li>O servidor poderá estar sobrecarregado ou temporariamente indisponível? Tente novamente mais tarde.</li>
+ <li>Não consegue navegar noutros sites? Verifique a ligação do computador à rede.</li>
+ <li>O seu computador ou rede estão protegidos por uma firewall ou proxy? Definições incorretas podem interferir com a navegação na Internet.</li>
+ <li>Continua a ter problemas? Contacte o seu administrador de rede ou fornecedor de Internet para apoio.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Não é possível ligar</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>O site pode estar temporariamente indisponível ou demasiado ocupado. Volte a tentar dentro de alguns momentos.</li>
+ <li>Se não conseguir carregar nenhuma página, verifique a ligação de dados ou de rede sem fios do dispositivo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Resposta inesperada do servidor</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>O site respondeu ao pedido de rede de uma forma inesperada e o navegador não pode continuar.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">A página não está a redirecionar corretamente</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>O navegador deixou de tentar obter o item solicitado. O site está a redirecionar o pedido de modo a que este nunca seja concluído.</p>
+ <ul>
+ <li>Desativou ou bloqueou cookies que são necessárias para este site?</li>
+ <li>Se aceitar os cookies do site não resolver o problema, é provável que seja um problema de configuração do servidor e não do seu computador.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modo desligado</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>O navegador está a operar no modo desligado e não consegue estabelecer ligação ao objeto solicitado.</p>
+ <ul>
+ <li>O computador está ligado a uma rede ativa?</li>
+ <li>Pressione "Tentar novamente" para mudar para o modo ligado e recarregar a página.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porta restringida por questões de segurança</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>O endereço solicitado especifica uma porta (por exemplo, <q>mozilla.org:80</q> para a porta 80 em mozilla.org) normalmente utilizada para <em>outros</em> fins para além de navegação na Internet. Para sua proteção e segurança o navegador cancelou o pedido.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">A ligação foi reposta</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>A ligação de rede foi interrompida ao negociar uma ligação. Por favor, tente novamente.</p>
+ <ul>
+ <li>O site pode estar temporariamente indisponível ou demasiado ocupado. Tente novamente dentro de alguns instantes.</li>
+ <li>Se não consegue carregar quaisquer páginas, confirme a ligação de dados ou de rede sem fios do seu dispositivo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de ficheiro inseguro</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Por favor, contacte os proprietários do site para os informar deste problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Erro de conteúdo corrompido</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>A página que está a tentar ver não pode ser apresentada porque foi detetado um erro na transmissão de dados.</p>
+ <ul>
+ <li>Por favor, contacte os proprietários do site para os informar deste problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">O conteúdo falhou</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>A página que está a tentar ver não pode ser apresentada porque foi detetado um erro na transmissão de dados.</p>
+ <ul>
+ <li>Por favor, contacte os proprietários do site para os informar deste problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Erro de codificação de conteúdo</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>A página que está a tentar ver não pode ser apresentada porque utiliza uma forma de compressão inválida ou não suportada.</p>
+ <ul>
+ <li>Por favor, contacte os proprietários do site para os informar deste problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Endereço não encontrado</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>O navegador não conseguiu encontrar o servidor anfitrião para o endereço fornecido.</p>
+ <ul>
+ <li>Verifique se o endereço não tem erros de escrita, tais como <strong>ww</strong>.example.com em vez de
+ <strong>www</strong>.example.com.</li>
+ <li>Se não consegue carregar quaisquer páginas, verifique a ligação de dados ou de rede sem fios do seu dispositivo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Sem ligação à Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifique a sua ligação de rede ou tente recarregar a página dentro de alguns momentos.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recarregar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Endereço inválido</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>O endereço fornecido não está num formato reconhecível. Por favor, verifique se a barra de endereço tem erros e tente novamente.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">O endereço não é válido</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Os endereços de Internet são habitualmente escritos como <strong>http://www.example.com/</strong></li>
+ <li>Verifique se está a utilizar barras (por exemplo, <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocolo desconhecido</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>O endereço especifica um protocolo (por exemplo, <q>wxyz://</q>) que o navegador não reconhece, motivo pelo qual o navegador não consegue ligar-se corretamente ao site.</p>
+ <ul>
+ <li>Está a tentar aceder a recursos multimédia ou a outros serviços não baseados em texto? Verifique os requisitos adicionais do site.</li>
+ <li>Alguns protocolos podem necessitar de software ou de plugins de terceiros para que o navegador os possa reconhecer.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Ficheiro não encontrado</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>O item pode ter sido renomeado, removido, ou movido?</li>
+ <li>Existe algum erro ortográfico, de maiúsculas/minúsculas ou outro erro tipográfico no endereço?</li>
+ <li>Tem permissões de acesso suficientes ao item solicitado?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">O acesso ao ficheiro foi negado</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Pode ter sido removido, movido ou as permissões do ficheiro podem estar a impedir o acesso.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">O servidor proxy recusou a ligação</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>O navegador está configurado para utilizar um servidor proxy, mas este recusou a ligação.</p>
+ <ul>
+ <li>A configuração de proxy do navegador está correta? Verifique as definições e tente novamente.</li>
+ <li>O serviço de proxy permite ligações a partir desta rede?</li>
+ <li>Continua a ter problemas? Consulte o seu administrador de rede ou fornecedor de Internet para apoio.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Servidor proxy não encontrado</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>O navegador está configurado para utilizar um servidor proxy, mas este não foi encontrado.</p>
+ <ul>
+ <li>A configuração de proxy do navegador está correta? Verifique as definições e tente novamente.</li>
+ <li>O computador está ligado a uma rede ativa?</li>
+ <li>Continua a ter problemas? Consulte o seu administrador de rede ou fornecedor de Internet para apoio.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de software malicioso no site</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>O site em %1$s foi reportado como sendo um site de ataque e foi bloqueado com base nas suas preferências de segurança.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de site indesejado</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>O site em %1$s foi reportado como sendo um site que fornece software não-solicitado e foi bloqueado com base nas suas preferências de segurança.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de site nocivo</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>O site em %1$s foi reportado como sendo um site potencialmente nocivo e foi bloqueado com base nas suas preferências de segurança.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de site decetivo</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Esta página de Internet em %1$s foi reportada como sendo um site decetivo e foi bloqueada com base nas suas preferências de segurança.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Site seguro não disponível</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Ativou o modo Apenas HTTPS para uma maior segurança e não está disponível uma versão HTTPS de <em> %1$s </em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continuar para o site HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..f907c2acf0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-rm/strings.xml
@@ -0,0 +1,263 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Empruvar anc ina giada</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Impussibel d\'exequir la dumonda</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[ <p>Ulteriuras infurmaziuns davart quest problem u questa errur n\'èn per il mument betg disponiblas.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">La connexiun segira n\'è betg reussida</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>La pagina dumandada na po betg vegnir mussada, perquai ch\'i na va betg da verifitgar l\'autenticitad da las datas.</li>
+ <li>Contactescha per plaschair ils administraturs da la website per als infurmar davart quest problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">La connexiun segirada n\'è betg reussida</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Quai pudess esser in problem cun la configuraziun dal server u ch\'insatgi vul sa dar per quest server.</li>
+ <li>Sche ti has connectà cun success cun quest server en il passà, sa tracti eventualmain mo d\'ina errur temporara e ti pos empruvar pli tard anc ina giada.</li>
+</ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avanzà…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Eventualmain emprova in\'autra website da sa dar per la website giavischada. I vegn recumandà da betg cuntinuar.</label>
+ <br><br>
+ <label>Websites cumprovan lur identitad cun certificats. %1$s na sa fida betg da <b>%2$s</b> perquai che l\'emettur dal certificat n\'è betg enconuschent, perquai ch\'il certificat è auto-signà u perquai ch\'il server na trametta betg ils certificats intermediars corrects.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Turnar (recumandà)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Acceptar la ristga e cuntinuar</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Questa website pretenda ina connexiun segirada.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>La pagina che ti emprovas da visitar na po betg vegnir mussada perquai che questa website pretenda ina connexiun segirada.</li>
+ <li>I sa tracta probablamain dad in problem da la website e ti na pos far nagut per al schliar.</li>
+ <li>Ti pos dentant infurmar l\'administratur da la website davart il problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avanzà…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ha ina directiva da segirezza che sa numna HTTP Strict Transport Security (HSTS). Quai munta che <b>%2$s</b> po mo connectar a moda segirada cun la website. I n\'è betg pussaivel dad agiuntar ina excepziun per visitar questa website. </label>
+]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Turnar</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">La connexiun è interrutta</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Il navigatur ha pudì stabilir ina connexiun, ma ella è interrutta durant la transmissiun da datas. Emprova per plaschair anc ina giada.</p>
+ <ul>
+ <li>Eventualmain n\'è la website temporarmain betg cuntanschibla u ch\'ella è surchargiada. Emprova pli tard anc ina giada.</li>
+ <li>Sche ti na pos era betg chargiar autras paginas, controllescha per plaschair la connexiun da datas u da WLAN da tes apparat.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Surpassà il temp per la connexiun</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>La website dumandada n\'ha betg respundì ad ina emprova da connectar ed il navigatur ha chalà da spetgar ina resposta.</p>
+ <ul>
+ <li>Eventualmain è il server surchargià u temporarmain ord funcziun? Emprova pli tard anc ina giada.</li>
+ <li>Na pos ti era betg chargiar autras websites? Controllescha la configuraziun da la connexiun cun la rait da tes computer.</li>
+ <li>Èn tia rait u tes computer protegids dad ina firewall u dad in proxy? Parameters incorrects pon disturbar la navigaziun en il web.</li>
+ <li>Na funcziuneschi anc adina betg? Contactescha l\'administratur da la rait u il provider e dumonda agid.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Connexiun betg reussida</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Eventualmain n\'è la website temporarmain betg cuntanschibla u ch\'ella è surchargiada. Emprova pli tard anc ina giada.</li>
+ <li>Sche ti na pos era betg chargiar autras paginas, controllescha la connexiun da datas u da WLAN da tes apparat.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Resposta nunspetgada dal server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>La website dumandada ha respundì a moda nunspetgada. Perquai na po la connexiun betg vegnir mantegnida.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">La pagina na renviescha betg a moda correcta</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Il navigatur ha chalà dad empruvar da retschaiver l\'element dumandà. La website dumandada sviescha la dumonda uschia ch\'ella na po mai vegnir terminada.</p>
+ <ul>
+ <li>Has ti bloccà u deactivà cookies indispensabels per questa website?</li>
+ <li>Per cas ch\'i na gida betg d\'acceptar ils cookies da la website, sa tracti probablamain dad in problem cun la configuraziun dal server e betg dad in problem da tes apparat.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modus offline</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Il navigatur è en il modus offline e na po perquai betg connectar cun l\'element dumandà.</p>
+ <ul>
+ <li>È l\'apparat collià cun ina rait activa?</li>
+ <li>Clicca sin «Empruvar anc ina giada» per midar en il modus online e rechargiar la pagina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Il port è bloccà per motivs da segirezza</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>L\'adressa dumandada ha inditgà in port (per exempel <q>mozilla.org:80</q> per il port 80 sin mozilla.org) che na vegn <em>normalmain betg</em> utilisà per navigar en il web. Il navigatur ha annullà la dumonda per ta proteger.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">La connexiun è interrutta</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>La connexiun a la rait è vegnida interrutta durant stabilir ina connexiun. Emprova p.pl. anc ina giada.</p>
+ <ul>
+ <li>Eventualmain n\'è la website temporarmain betg cuntanschibla u ch\'ella è surchargiada. Emprova pli tard anc ina giada.</li>
+ <li>Sche ti na pos era betg chargiar autras paginas, controllescha per plaschair la connexiun da datas u da WLAN da tes apparat.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tip da datoteca malsegir</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Contactescha per plaschair ils possessurs da la website per als infurmar davart quest problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Errur: Cuntegn donnegià</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Impussibel da visualisar la pagina che ti vuls chargiar perquai ch\'ina errur è capitada en la transmissiun da datas.</p>
+ <ul>
+ <li>Contactescha per plaschair ils possessurs da la website per als infurmar davart quest problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Il cuntegn è collabà</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Impussibel da visualisar la pagina che ti vuls chargiar perquai ch\'ina errur è capitada en la transmissiun da datas.</p>
+ <ul>
+ <li>Contactescha per plaschair ils possessurs da la website per als infurmar davart quest problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Cuntegns cun codaziun nunvalida</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Impussibel da visualisar la pagina che ti vuls chargiar perquai ch\'ella utilisescha ina furma da cumpressiun nunvalida u betg cumpatibla.</p>
+ <ul>
+ <li>Contactescha per plaschair ils possessurs da la website per als infurmar davart quest problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Impussibel da chattar l\'adressa</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Impussibel da chattar il server da host da l\'adressa dumandada.</p>
+ <ul>
+ <li>Controllescha sche l\'adressa cuntegna sbagls da scriver sco
+ <strong>ww</strong>.example.com empè da
+ <strong>www</strong>.example.com</li>
+ <li>Sch\'i na va era betg da chargiar autras paginas, controllescha per plaschair la connexiun da datas u da WLAN da tes apparat.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nagina connexiun cun l\'internet</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Controllescha tia connexiun cun la rait u emprova da chargiar danovamain la pagina en in mument.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Rechargiar</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Adressa nunvalida</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>L\'adressa inditgada ha in format nunenconuschent. Controllescha per plaschair sche l\'adressa cuntegna sbagls ed emprova anc ina giada.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">L\'adressa è nunvalida</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Adressas d\'internet vegnan normalmain scrittas uschia:
+ <strong>http://www.example.com/</strong></li>
+ <li>Controllescha che ti has utilisà stritgs diagonals che mussan enavant (q.v.d.
+ <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocol nunenconuschent</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>L\'adressa inditgescha in protocol (per exempel <q>wxyz://</q>) ch\'il navigatur n\'enconuscha betg. Perquai na po il navigatur betg connectar correctamain cun la website.</p>
+ <ul>
+ <li>Emprovas ti dad acceder a multimedia u auters servetschs betg textuals? Controllescha sche la website pretenda premissas spezialas.</li>
+ <li>Tscherts protocols dovran software da terzs u plug-ins per ch\'il navigatur als reconuschia.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Betg chattà la datoteca</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Forsa è l\'element vegnì renumnà, stizzà u spustà? </li>
+ <li>Cuntegna l\'adressa in sbagl ortografic, in sbagl da scripziun grond e pitschen u in auter sbagl da scriver? </li>
+ <li>Has ti ils dretgs d\'access necessaris per chargiar l\'element dumandà?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Refusà l\'access a la datoteca</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Forsa è ella stizzada, spustada u che ti n\'es betg autorisà per l\'access.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Il proxy server ha refusà la connexiun</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Il navigatur è configurà per l\'utilisaziun dad in proxy server, ma il proxy ha refusà ina connexiun.</p>
+ <ul>
+ <li>È la configuraziun dal proxy correcta? Controllescha ils parameters ed emprova anc ina giada.</li>
+ <li>Permetta il servetsch da proxy connexiuns or da questa rait?</li>
+ <li>Na funcziuneschi anc adina betg? Contactescha l\'administratur da la rait u il provider e dumonda agid.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Impussibel da chattar il proxy server</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Il navigatur è configurà per l\'utilisaziun dad in proxy server, ma i n\'è betg reussì da chattar il proxy.</p>
+ <ul>
+ <li>È la configuraziun dal proxy correcta? Controllescha ils parameters ed emprova anc ina giada.</li>
+ <li>È l\'apparat connectà ad ina rait activa?</li>
+ <li>Na funcziuneschi anc adina betg? Contactescha l\'administratur da la rait u il provider e dumonda agid.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problem cun ina pagina da malware</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>La pagina %1$s è vegnida annunziada sco pagina che attatga. Ella vegn bloccada sin fundament da tias preferenzas da segirezza.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problem cun ina pagina nungiavischada</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>La pagina %1$s è annunziada sco pagina che distribuescha software nungiavischada. Ella vegn bloccada sin fundament da tias preferenzas da segirezza.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problem cun ina pagina privlusa</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>La pagina %1$s è vegnida annunziada sco pagina potenzialmain privlusa. Ella vegn bloccada sin fundament da tias preferenzas da segirezza.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problem cun ina pagina che engiona</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>La pagina %1$s è annunziada sco pagina che engiona. Ella vegn bloccada sin fundament da tias preferenzas da segirezza.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Website segirada betg disponibla</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Ti has activà il modus «mo HTTPS» per augmentar la segirezza ed ina versiun HTTPS da <em>%1$s</em> na stat betg a disposiziun.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Cuntinuar cun la website HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..c8b2853dda
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ro/strings.xml
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Încearcă din nou</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Cererea nu poate fi finalizată</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Nu sunt disponibile momentan informații suplimentare despre această problemă sau eroare.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Conexiunea securizată a eșuat</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+        <li>Pagina pe care încerci să o vizualizezi nu poate fi afișată, deoarece autenticitatea datelor primite nu a putut fi verificată.</li>
+        <li>Te rugăm să contactezi proprietarii site-ului web pentru a-i informa despre această problemă.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Conexiunea securizată a eșuat</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+<li>Ar putea fi din cauza unei probleme în configurația serverului sau ar putea să fie cineva care încearcă să uzurpe identitatea serverului.</li>
+<li>Dacă te-ai conectat cu succes în trecut la acest server, eroarea poate fi temporară și poți încerca din nou mai târziu.</li>
+</ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avansat…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Cineva ar putea încerca să uzurpe site-ul și ar fi bine să nu mergi mai departe.</label>
+ <br><br>
+        <label>Site-urile web își dovedesc identitatea prin certificate. %1$s nu are încredere în <b>%2$s</b> deoarece emitentul certificatului este necunoscut, certificatul este autosemnat sau serverul nu trimite certificatele intermediare corecte.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Înapoi (recomandat)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Acceptă riscul și continuă</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Conexiunea a fost întreruptă</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Browserul s-a conectat cu succes, dar conexiunea a fost întreruptă în timpul transferului de informații. Încearcă din nou.</p>
+      <ul>
+        <li>Site-ul ar putea fi temporar indisponibil sau prea ocupat. Încearcă din nou în câteva momente.</li>
+        <li>Dacă nu poți încărca nicio pagină, verifică datele dispozitivului tău sau conexiunea Wi-Fi.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Conexiunea a expirat</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p> Site-ul solicitat nu a răspuns la o solicitare de conectare și browserul a încetat să aștepte un răspuns. </p>
+ <ul>
+        <li> S-ar putea ca serverul să aibă o cerere ridicată sau o întrerupere temporară? Încearcă din nou mai târziu. </li>
+        <li> Nu poți naviga pe alte site-uri? Verifică-ți conexiunea la rețea. </li>
+        <li> Dispozitivul tău sau rețeaua ta este protejat(ă) de un firewall sau proxy? Setările incorecte pot interfera cu navigarea pe Web. </li>
+        <li> Mai ai probleme? Consultă administratorul de rețea sau furnizorul de internet pentru asistență. </li>
+  </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Conectare eșuată</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+        <li>Site-ul ar putea fi temporar indisponibil sau prea ocupat. Încearcă din nou în câteva momente.</li>
+        <li>Dacă nu poți încărca nicio pagină, verifică datele dispozitivului sau conexiunea Wi-Fi.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Răspuns neașteptat de la server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Site-ul a răspuns într-un mod neașteptat solicitării rețelei și browserul nu poate continua.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Pagina nu se redirecționează corect</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Browserul a încetat să încerce să recupereze articolul solicitat. Site-ul redirecționează solicitarea într-un mod în care nu va fi niciodată finalizat. </p>
+      <ul>
+        <li>Ai dezactivat sau ai blocat cookie-urile solicitate de acest site?</li>
+        <li>Dacă acceptarea cookie-urilor site-ului nu rezolvă problema, este probabil o problemă de configurare a serverului și nu pe calculatorul tău.</li>
+      </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mod offline</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Browserul este în modul offline și nu se poate conecta la articolul solicitat.</p>
+ <ul>
+ <li>Dispozitivul este conectat la o rețea activă?</li>
+ <li>Apasă „Încearcă din nou” pentru trecerea pe modul online și reîncărcarea paginii.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port restricționat din motive de securitate</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Adresa solicitată a specificat un port (de ex. <q>mozilla.org:80</q> pentru portul 80 oe mozilla.org) folosit în mod normal în <em>alte</em> decât navigarea Web. Browserul a anulat cererea de protecție și securitate.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Conexiunea a fost resetată</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Legătura la rețea a fost întreruptă în timpul negocierii unei conexiuni. Încearcă din nou.</p>
+ <ul>
+ <li>S-ar putea ca site-ul să fie temporar indisponibil sau prea ocupat. Încearcă din nou în câteva momente.</li>
+ <li>Dacă nu poți încărca nicio pagină, verifică datele dispozitivului sau conexiunea Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tip de fișier nesigur</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Te rugăm să contactezi proprietarii site-ului web ca să-i informezi despre această problemă.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Eroare de conținut corupt</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Pagina pe care încerci să o vizualizezi nu poate fi afișată deoarece s-a depistat o eroare în transmisia de date.</p>
+ <ul>
+ <li>Te rugăm să contactezi proprietarii site-ului să-i informezi despre problemă.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Conținut blocat</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Pagina pe care încerci să o vizualizezi nu poate fi afișată deoarece s-a depistat o eroare în transmisia de date.</p>
+ <ul>
+ <li>Te rugăm să contactezi proprietarii site-ului să-i informezi despre problemă.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Eroare de codificare a conținutului</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Pagina pe care încerci să o vezi nu poate fi afișată pentru că folosește o formă de compresie nevalidă sau nesuportată.</p>
+ <ul>
+ <li>Te rugăm să contactezi proprietarii site-ului web pentru a-i informa despre această problemă.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Nu s-a găsit adresa</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Browserul nu a găsit serverul-gazdă pentru adresa furnizată.</p>
+ <ul>
+ <li>Verifică adresa pentru erori de tastare precum
+ <strong>ww</strong>.exemplu.com în loc de
+ <strong>www</strong>.exemplu.com.</li>
+ <li>Dacă nu poți încărca nicio pagină, verifică datele dispozitivului sau conexiunea Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Lipsă conexiune la internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifică-ți conexiunea la rețea sau încearcă să reîncarci pagina în câteva momente.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Reîncarcă</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Adresă nevalidă</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Adresa furnizată nu este într-un format recunoscut. Verifică să nu fie greșeli în bara de adrese și încearcă din nou</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adresa nu este validă</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Adresele web sunt scrise în mod normal așa: <strong>http://www.example.com/</strong></li>
+ <li>Asigură-te că folosești bare înclinate la dreapta (adică <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocol necunoscut</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Adresa specifică un protocol (de ex. <q>wxyz://</q>) pe care browserul nu îl recunoaște și, prin urmare, nu se poate conecta corect la site.</p>
+ <ul>
+ <li>Încerci să accesezi conținut multimedia sau alte servicii non-text? Verifică site-ul pentru cerințe suplimentare.</li>
+ <li>Este posibil ca unele protocoale să necesite softuri de la terți sau pluginuri înainte ca browserul să le poată recunoaște.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fișierul nu a fost găsit</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Se poate ca articolul să fi fost redenumit, șters sau mutat?</li>
+ <li>Este vreo eroare de ortografie, majuscule/minuscule sau altă eroare tipografică în adresă?</li>
+ <li>Ai suficiente drepturi de acces la articolul solicitat?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Accesul la fișier a fost refuzat</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Este posibil să fi fost șters, mutat sau poate că permisiunile fișierului împiedică accesul.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Serverul proxy a refuzat conexiunea</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Browserul este configurat să folosească un server proxy, însă proxyul a refuzat o conexiune.</p>
+ <ul>
+ <li>Configurația proxy a browserului este corectă? Verifică setările și încearcă din nou.</li>
+ <li>Serviciul proxy permite conexiuni din această rețea?</li>
+ <li>Problemele persistă? Contactează administratorul de rețea sau furnizorul de servicii internet pentru asistență.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Serverul proxy nu a fost găsit</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Browserul este configurat să folosească un server proxy, însă proxyul nu a putut fi găsit.</p>
+ <ul>
+ <li>Configurația proxy a browserului este corectă? Verifică setările și încearcă din nou.</li>
+ <li>Dispozitivul este conectat la o rețea activă?</li>
+ <li>Problemele persistă? Contactează administratorul de rețea sau furnizorul de servicii internet pentru asistență.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problemă de soft rău intenționat pe acest site</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Site-ul de la %1$s a fost raportat ca sursă de atacuri și a fost blocat în baza preferințelor tale de securitate.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problemă de site nedorit</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Site-ul de la%1$s a fost raportat ca purtător de softuri nedorite și a fost blocat pe baza preferințelor tale de securitate.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problemă de site dăunător</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Site-ul de la %1$s a fost raportat ca site potențial dăunător și a fost blocat pe baza preferințelor tale de securitate.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problemă de site înșelător</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Această pagină web de la %1$s a fost raportată ca site înșelător și a fost blocată pe baza preferințelor tale de securitate.</p>]]></string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..d3c8a6c767
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ru/strings.xml
@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Попробовать снова</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Не удалось выполнить запрос</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Дополнительные сведения об этой проблеме или ошибке сейчас недоступны.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Не удалось установить безопасное соединение</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как достоверность полученных данных не может быть проверена.</li>
+ <li>Пожалуйста, свяжитесь с владельцами сайта и сообщите им об этой проблеме.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Не удалось установить безопасное соединение</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Это может быть вызвано неправильной настройкой сервера, или, возможно, кто-то пытается подменить нужный вам сервер другим.</li>
+ <li>Если раньше вы успешно соединялись с этим сервером, то ошибка может быть временной. В таком случае попробуйте снова позже.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Дополнительно…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Возможно, кто-то пытается подменить нужный вам сайт другим, поэтому вам лучше не продолжать.</label>
+ <br><br>
+ <label>Веб-сайты подтверждают свою подлинность с помощью сертификатов. %1$s не может доверять <b>%2$s</b>, потому что издатель его сертификата неизвестен, сертификат подписан самим сайтом или его сервер не отправляет правильные промежуточные сертификаты.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Назад (желательно)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Принять риск и продолжить</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Этот сайт требует использования безопасного соединения.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как этот веб-сайт требует использования безопасного соединения.</li>
+ <li>Скорее всего, проблема связана с веб-сайтом, и вы никак не можете её решить.</li>
+ <li>Вы можете уведомить администратора веб-сайта об этой проблеме.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Дополнительно…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> использует политику безопасности под названием Форсированное безопасное соединение HTTP (HSTS), согласно которой <b>%2$s</b> может соединяться с ним только с помощью безопасного соединения. Вы не можете добавить исключение для посещения этого сайта. </label>
+
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Назад</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Соединение было прервано</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Браузер успешно установил соединение, но оно было прервано во время передачи информации. Пожалуйста, попробуйте снова.</p>
+ <ul>
+ <li>Возможно, сайт временно недоступен или перегружен запросами. Попробуйте снова через некоторое время.</li>
+ <li>Если вам не удаётся загрузить ни одну страницу, проверьте соединение своего устройства с мобильной или Wi-Fi сетью.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Время ожидания соединения истекло</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Сайт не отвечал на запрос соединения, и браузер прекратил ожидание.</p>
+ <ul>
+ <li>Возможно, сервер сайта перегружен или временно недоступен. Попробуйте снова позже.</li>
+ <li>Если вам не удаётся открыть и другие сайты, проверьте соединение своего устройства с сетью.</li>
+ <li>Если ваше устройство или локальная сеть защищены межсетевым экраном или прокси-сервером, проверьте их, так как неправильные настройки могут препятствовать просмотру веб-сайтов.</li>
+ <li>Проблема не исчезла? Обратитесь к своему системному администратору или Интернет-провайдеру за помощью.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Попытка соединения не удалась</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Возможно, сайт временно недоступен или перегружен запросами. Попробуйте снова через некоторое время.</li>
+ <li>Если вам не удаётся загрузить ни одну страницу, проверьте соединение своего устройства с мобильной или Wi-Fi сетью.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Неизвестный/неопознанный ответ сервера</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Сайт ответил на запрос неожиданным образом и браузер не может обработать его ответ.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Циклическое перенаправление на странице</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Браузер прекратил попытки загрузки страницы, так как сайт перенаправляет запрос таким образом, что его выполнение никогда не завершится.</p>
+ <ul>
+ <li>Возможно, вы отключили или заблокировали куки, необходимые для работы этого сайта.</li>
+ <li>Если разрешение кук сайта не решило проблему, то, скорее всего, она связана с настройками сервера, а не с вашим устройством.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Автономный режим</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Браузер работает в автономном режиме и не может установить соединение с запрашиваемым сайтом.</p>
+ <ul>
+ <li>Проверьте, что устройство подключено к работающей сети.</li>
+ <li>Нажмите «Попробовать снова», чтобы выйти из автономного режима и перезагрузить страницу.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">В целях безопасности обращение к порту было заблокировано</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Для запрошенного адреса указан порт (например, <q>mozilla.org:80</q> — это порт 80 на mozilla.org), который обычно <em>не используется</em> для работы с веб-сайтами. В целях безопасности браузер отменил этот запрос.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Соединение было сброшено</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Связь с сайтом была оборвана на этапе установки соединения. Пожалуйста, попробуйте снова.</p>
+ <ul>
+ <li>Возможно, сайт временно недоступен или перегружен запросами. Попробуйте снова через некоторое время.</li>
+ <li>Если вам не удаётся загрузить ни одну страницу, проверьте соединение своего устройства с мобильной или Wi-Fi сетью.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Небезопасный тип файла</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Пожалуйста, свяжитесь с владельцами веб-сайта и сообщите им об этой проблеме.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Загружаемое содержимое повреждено</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как при передаче данных была обнаружена ошибка.</p>
+ <ul>
+ <li>Пожалуйста, свяжитесь с владельцами веб-сайта и сообщите им об этой проблеме.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Загружаемое содержимое повреждено</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как при передаче данных была обнаружена ошибка.</p>
+ <ul>
+ <li>Пожалуйста, свяжитесь с владельцами веб-сайта и сообщите им об этой проблеме.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Ошибка сжатия содержимого</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как использует неправильный или неподдерживаемый способ сжатия данных.</p>
+ <ul>
+ <li>Пожалуйста, свяжитесь с владельцами веб-сайта и сообщите им об этой проблеме.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Адрес не найден</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Браузеру не удалось найти сервер по указанному адресу.</p>
+ <ul>
+ <li>Проверьте, нет ли в адресе опечаток, например,
+ <strong>ww</strong>.example.com вместо
+ <strong>www</strong>.example.com.</li>
+ <li>Если вам не удаётся загрузить ни одну страницу, проверьте соединение своего устройства с мобильной или Wi-Fi сетью.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Нет соединения с Интернетом</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Проверьте своё соединение с сетью или попробуйте перезагрузить страницу через некоторое время.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Перезагрузить</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Неправильный формат адреса</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Не удаётся распознать формат указанного адреса. Проверьте адрес на наличие ошибок и попробуйте снова.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Неправильный формат адреса</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Веб-адреса обычно имеют вид <strong>http://www.example.com/</strong></li>
+ <li>Убедитесь, что вы используете обычную косую черту (т.е. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Неизвестный протокол</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Адрес содержит неизвестный браузеру протокол (такой как <q>wxyz://</q>), поэтому браузер не может правильно установить соединение с сайтом.</p>
+ <ul>
+ <li>Если вы пытаетесь открыть мультимедиа или другие нетекстовые сервисы, проверьте сайт на наличие особых требований.</li>
+ <li>Некоторые протоколы могут требовать установки стороннего программного обеспечения или плагинов для того, чтобы браузер мог их распознать.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Файл не найден</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Возможно, файл был переименован, удалён или перемещён.</li>
+ <li>Проверьте, не допустили ли вы ошибку при вводе адреса.</li>
+ <li>Убедитесь, что у вас достаточно прав для просмотра запрашиваемого файла.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Отказано в доступе к файлу</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Возможно, файл был удалён, перемещён или у вас недостаточно прав для его просмотра.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Прокси-сервер отказал в установке соединения</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>В браузере настроено использование прокси-сервера, но прокси-сервер отказал в установке соединения.</p>
+ <ul>
+ <li>Проверьте настройки прокси браузера и попробуйте снова.</li>
+ <li>Проверьте, разрешает ли служба прокси соединения из этой сети.</li>
+ <li>Проблема не исчезла? Обратитесь к своему системному администратору или Интернет-провайдеру за помощью.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Прокси-сервер не найден</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>В браузере настроено использование прокси-сервера, но прокси-сервер не удалось найти.</p>
+ <ul>
+ <li>Проверьте настройки прокси браузера и попробуйте снова.</li>
+ <li>Проверьте, подключено ли ваше устройство к активной сети.</li>
+ <li>Проблема не исчезла? Обратитесь к своему системному администратору или Интернет-провайдеру за помощью.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Вредоносный сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Есть информация, что сайт %1$s используется для атак на устройства пользователей, поэтому он был заблокирован в соответствии с вашими настройками защиты.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Нежелательный сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Есть информация о том, что сайт %1$s используется для распространения нежелательного программного обеспечения, поэтому он был заблокирован в соответствии с вашими настройками защиты.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Опасный сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Есть информация о том, что сайт %1$s потенциально опасен, поэтому он был заблокирован в соответствии с вашими настройками защиты.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Поддельный сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Есть информация о том, что эта веб-страница на %1$s — поддельная, поэтому она была заблокирована в соответствии с вашими настройками защиты.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Безопасная версия сайта недоступна</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Вы включили режим «Только HTTPS» для повышения безопасности, однако HTTPS-версия сайта <em>%1$s</em> недоступна.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Перейти на HTTP-версию</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..a4feeb2af6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sat/strings.xml
@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ᱫᱚᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ᱱᱮᱦᱚᱨ ᱵᱟᱝ ᱯᱩᱨᱟᱹᱣ ᱞᱮᱱᱟ</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>ᱟᱨᱦᱚᱸ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱛ ᱵᱟᱰᱟᱭ ᱞᱟᱹᱜᱤᱫ ᱟᱨ ᱵᱟᱝ ᱵᱷᱩᱞ ᱵᱟᱵᱚᱛ ᱫᱚ ᱵᱟᱹᱱᱩᱜᱼᱟ ᱾</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">ᱡᱟᱯᱛᱤ ᱡᱚᱱᱚᱲᱟᱹᱣ ᱰᱤᱜᱟᱹᱣ</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>ᱚᱠᱟ ᱥᱟᱦᱴᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ ᱠᱷᱚᱡᱚᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱸᱡ ᱚᱱᱟ ᱫᱚ ᱰᱟᱴᱟ ᱨᱮᱭᱟᱜ ᱚᱛᱷᱮᱱᱴᱤᱥᱤᱴᱭ ᱫᱚ ᱵᱟᱝ ᱯᱩᱥᱴᱟᱹᱣ ᱫᱟᱲᱮᱞᱟᱱᱟ ᱾</li>
+ <li>ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱫᱽ ᱠᱷᱚᱵᱚᱨ ᱮᱢ ᱢᱮ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">ᱡᱟᱯᱛᱤ ᱡᱚᱱᱚᱲᱟᱣ ᱦᱩᱲᱟᱹᱜ</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>ᱱᱚᱶᱟ ᱥᱟᱨᱵᱷᱟᱨ ᱠᱚᱱᱯᱷᱤᱜᱽᱨᱮᱥᱚᱱ ᱥᱟᱶ ᱮᱴᱠᱮᱴᱚᱬᱮ ᱦᱩᱭ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ, ᱥᱮ ᱱᱚᱶᱟ ᱡᱟᱦᱟᱸᱭ ᱦᱚᱛᱮᱛᱮ ᱤᱢᱯᱚᱨᱥᱳᱱᱮᱴ ᱨᱤᱠᱟᱹ ᱦᱩᱭ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱾ </li><li> ᱡᱩᱫᱤ ᱟᱢ ᱞᱟᱦᱟ ᱛᱮ ᱥᱟᱨᱵᱷᱟᱨ ᱥᱟᱶ ᱡᱚᱲᱟᱣ ᱢᱮᱱᱟᱜ ᱢᱮᱭᱟ, ᱤᱨᱳᱨ ᱛᱟᱦᱮᱸ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ, ᱟᱨ ᱛᱟᱭᱚᱢ ᱛᱮᱢ ᱨᱤᱠᱟᱹ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱾</li></ul>
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ᱞᱟᱦᱟᱱᱛᱤ…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label> ᱤᱢᱯᱚᱨᱥᱳᱱᱮᱴ ᱥᱟᱭᱤᱴ ᱡᱟᱦᱟᱸᱭ ᱠᱚ ᱨᱤᱠᱟᱹ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱟᱨ ᱟᱢ ᱫᱚ ᱪᱟᱞᱟᱣ ᱛᱮ ᱦᱩᱭ ᱟᱢᱟ ᱾ </label>
+<br><br>
+<label>ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱟᱡᱟᱜ ᱩᱯᱨᱩᱢ ᱛᱮ ᱥᱚᱫᱚᱨ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱾ %1$s ᱯᱟᱹᱛᱭᱟᱹᱣ ᱵᱟᱹᱱᱩᱜ <b>%2$s</b>ᱪᱮᱫᱟᱜ ᱥᱮ ᱱᱚᱶᱟ ᱨᱮᱱᱟᱜ ᱥᱟᱨᱴᱤᱯᱷᱤᱠᱮᱴ ᱪᱟᱞᱩ ᱵᱟᱰᱟᱭ ᱵᱟᱹᱱᱩᱜ-ᱟ, ᱥᱟᱨᱴᱤᱯᱷᱤᱠᱮᱴ ᱥᱮᱞᱯᱷ ᱥᱟᱭᱤᱱ ᱜᱮᱭᱟ, ᱥᱮ ᱥᱟᱨᱵᱷᱟᱨ ᱥᱟᱨᱴᱤᱯᱷᱤᱠᱮᱴ ᱵᱟᱝ ᱥᱮᱱ ᱫᱟᱲᱮᱭᱟᱜ ᱱᱤᱦᱟᱹᱛᱤ </label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ᱛᱟᱭᱚᱢ ᱥᱮᱱᱚᱜ ᱢᱮ (ᱠᱷᱚᱡᱚᱜ ᱜᱮᱭᱟ)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">ᱡᱤᱢᱟ ᱦᱟᱛᱟᱣ ᱢᱮ ᱟᱨ ᱥᱮᱱᱚᱜ ᱢᱮ</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">ᱱᱚᱶᱟ ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱵᱟᱹᱱᱩᱜᱼᱟ ᱾</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>ᱟᱢ ᱧᱮᱞ ᱥᱮᱱᱟ ᱥᱟᱦᱴᱟ ᱫᱚ ᱵᱟᱝ ᱧᱮᱞ ᱫᱟᱲᱮᱭᱟᱜᱼᱟᱢ ᱪᱮᱫᱟᱜ ᱥᱮ ᱱᱚᱶᱟ ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱡᱩᱲᱟᱹᱣ ᱫᱚᱨᱠᱟᱨ ᱠᱟᱱᱟ ᱾</li>
+ <li>ᱱᱚᱶᱟ ᱫᱚ ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱨᱮᱭᱟᱜ ᱵᱤᱥᱚᱭ ᱛᱮ ᱠᱟᱱᱟ, ᱱᱚᱶᱟ ᱵᱟᱵᱚᱛ ᱥᱩᱡᱷᱟᱹᱣ ᱫᱚ ᱟᱢ ᱯᱟᱦᱴᱟ ᱠᱷᱚᱱ ᱪᱮᱫ ᱥᱩᱡᱷᱟᱹᱣ ᱠᱟᱹᱢᱤ ᱵᱟᱭ ᱮᱢᱟ ᱾</li>
+ <li>ᱱᱚᱶᱟ ᱰᱤᱜᱟᱹᱣ ᱫᱚ ᱟᱢ ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱮᱰᱢᱤᱱᱥᱴᱨᱮᱴᱚᱨ ᱠᱷᱚᱵᱚᱨ ᱫᱟᱲᱮ ᱠᱚᱣᱟᱢ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">ᱞᱟᱦᱟᱱᱛᱤ…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> ᱴᱷᱚᱱ ᱢᱤᱫ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱱᱤᱛᱤ ᱡᱟᱦᱟᱸ ᱫᱚ HTTP ᱥᱴᱨᱤᱠᱴ ᱴᱨᱟᱱᱥᱯᱚᱴ ᱠᱚ ᱥᱮᱠᱭᱚᱨᱤᱴᱤ (HSTS) ᱠᱚ ᱢᱮᱛᱮᱜ ᱠᱟᱱᱟ, ᱡᱟᱦᱟᱸ ᱢᱮᱱᱮᱛ ᱫᱚ <b>%2$s</b> ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱡᱩᱲᱟᱹᱣᱜᱼᱟ ᱾ ᱱᱚᱶᱟ ᱥᱟᱭᱤᱴ ᱛᱮ ᱪᱟᱞᱟᱜ ᱞᱟᱹᱜᱤᱫ ᱪᱮᱫ ᱱᱤᱥᱮᱫᱷ ᱞᱟᱹᱠᱛᱤ ᱵᱟᱝ ᱠᱟᱱᱟ ᱾ </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">ᱛᱟᱭᱚᱢ ᱥᱮᱫ ᱪᱟᱞᱟᱣ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">ᱡᱚᱲᱟᱣ ᱵᱟᱝ ᱧᱮᱞ ᱞᱮᱱᱟ</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>ᱵᱽᱨᱟᱣᱡᱟᱨ ᱡᱚᱲᱟᱣ ᱞᱟᱦᱟ ᱮᱱᱟ, ᱢᱮᱱᱠᱷᱟᱱ ᱡᱚᱲᱟᱣ ᱵᱟᱝ ᱧᱮᱞ ᱮᱱᱟ ᱡᱚᱠᱷᱚᱱ ᱠᱷᱚᱵᱚᱨ ᱵᱷᱮᱡᱟᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱱᱟ ᱾ ᱫᱟᱭᱟ ᱠᱟᱛᱮ ᱫᱚᱦᱲᱟ ᱨᱤᱠᱟᱹᱭ ᱢᱮ </p><ul><li>ᱥᱟᱭᱤᱴ ᱵᱟᱝ ᱧᱟᱢ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱥᱮ ᱠᱟᱹᱢᱤ ᱨᱮ ᱛᱟᱦᱮᱱᱟ ᱾ ᱠᱤᱪᱷᱩ ᱜᱷᱟᱹᱲᱤᱡ ᱛᱟᱭᱚᱢ ᱨᱤᱠᱟᱹᱭ ᱢᱮ ᱾ </li><li> ᱡᱩᱫᱤ ᱥᱟᱦᱴᱟ ᱵᱟᱝ ᱮᱢ ᱞᱟᱫᱮ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ, ᱟᱢᱟᱜ ᱰᱤᱵᱷᱟᱭᱥ ᱨᱮᱱᱟᱜ ᱰᱟᱴᱟ ᱥᱮ ᱣᱟᱭ-ᱯᱷᱟᱭ ᱡᱚᱲᱟᱣ ᱧᱮᱞ ᱞᱮᱜᱟᱭ ᱢᱮ ᱾ </li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">ᱡᱚᱯᱚᱲᱟᱣ ᱚᱠᱛᱚ ᱪᱟᱵᱟ ᱮᱱᱟ</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>ᱱᱮᱦᱚᱨ ᱟᱠᱟᱱ ᱥᱟᱭᱤᱴ ᱫᱚ ᱡᱩᱲᱟᱹᱣ ᱵᱟᱭ ᱟᱸᱡᱚᱢ ᱮᱫᱟᱭ ᱟᱨ ᱵᱷᱮᱡᱟ ᱨᱩᱣᱟᱹᱲ ᱮ ᱵᱚᱸᱫᱚ ᱠᱮᱜᱼᱟᱭ ᱾</p>
+ <ul>
+ <li>ᱥᱟᱹᱨᱣᱟᱹᱨ ᱦᱟᱤ ᱰᱤᱢᱟᱸᱰ ᱟᱨ ᱵᱟᱝ ᱵᱟᱛᱷᱟᱱᱤᱛ ᱟᱣᱴᱨᱮᱡ ᱞᱟᱹᱤᱫ ᱦᱩᱭ ᱠᱚᱜᱼᱟ? ᱛᱟᱭᱚᱢ ᱛᱮ ᱪᱮᱥᱴᱟᱭ ᱢᱮ ᱾</li>
+ <li>ᱟᱢ ᱪᱮᱫ ᱚᱞᱜᱟ ᱥᱟᱭᱤ ᱵᱟᱧ ᱵᱟᱨᱩᱡ ᱫᱟᱲᱟᱭᱟᱜ ᱠᱟᱱᱟᱢ? ᱥᱟᱫᱷᱚᱱ ᱨᱮᱭᱟᱜ ᱱᱮᱴᱣᱟᱨᱠ ᱡᱩᱲᱟᱹᱣ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱾</li>
+ <li>ᱪᱮᱫ ᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱫᱚ ᱯᱷᱟᱭᱟᱨᱣᱟᱞ ᱟᱨ ᱵᱟᱝ ᱯᱨᱳᱠᱥᱭ ᱱᱮᱣᱴᱣᱟᱨᱠ ᱯᱨᱚᱴᱮᱠᱴᱮᱰ ᱜᱮᱭᱟ? ᱵᱷᱩᱞ ᱥᱟᱡᱟᱣ ᱠᱚ ᱠᱟᱹᱜᱤᱫ ᱣᱮᱵ ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱨᱮ ᱵᱟᱫᱷᱟ ᱮᱢ ᱫᱟᱲᱮᱜᱼᱟᱭ ᱾</li>
+ <li>ᱱᱤᱛ ᱦᱟᱹᱵᱤᱡ ᱥᱸᱝᱠᱚᱴ ᱨᱮ?ᱜᱚᱲᱚ ᱞᱟᱹᱜᱤᱫ ᱟᱢᱤᱡ ᱱᱮᱴᱣᱟᱨᱠ ᱟᱰᱢᱤᱱᱤᱥᱴᱨᱮᱴᱚᱨ ᱟᱨ ᱵᱟᱝ ᱤᱱᱴᱚᱨᱱᱮᱴ ᱯᱨᱚᱣᱟᱭᱰᱟᱹᱨ ᱥᱟᱞᱟᱜ ᱡᱩᱜᱟᱡᱩᱜ ᱢᱮ</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">ᱵᱟᱝ ᱡᱩᱲᱟᱹᱣ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>ᱥᱟᱭᱤᱴ ᱵᱮᱛᱷᱟᱱᱤᱠ ᱛᱟᱦᱮᱸ ᱠᱚᱜᱼᱟ ᱟᱨ ᱵᱟᱝ ᱡᱟᱹᱛᱤ ᱵᱤᱡᱭ ᱛᱟᱦᱮᱸ ᱠᱚᱜᱼᱟ ᱾ ᱠᱤᱪᱷᱤ ᱥᱚᱢᱚᱭ ᱛᱟᱭᱚᱢ ᱛᱮ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ ᱾</li>
+ <li>ᱟᱢ ᱡᱚᱫᱤ ᱥᱟᱦᱴᱟ ᱵᱟᱝ ᱞᱚᱰ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟᱢ, ᱥᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱨᱮᱭᱟᱜ ᱰᱟᱴᱟ ᱟᱨ ᱵᱟᱝ Wi-Fi ᱡᱩᱲᱟᱹᱣ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">ᱥᱟᱹᱨᱣᱟᱨ ᱠᱷᱚᱱ ᱵᱟᱝ ᱟᱸᱥ ᱨᱚᱲ ᱨᱩᱣᱟᱹᱲ</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>ᱥᱟᱭᱤᱴ ᱫᱚ ᱱᱮᱴᱣᱟᱨᱠ ᱱᱮᱦᱚᱨ ᱴᱷᱤᱠ ᱥᱮ ᱵᱟᱭ ᱟᱸᱡᱚᱢ ᱞᱮᱫᱟᱭ ᱟᱨ ᱵᱨᱟᱣᱡᱚᱨ ᱵᱟᱝ ᱞᱟᱦᱟ ᱫᱟᱲᱮᱟᱜᱼᱟᱭ ᱾</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ᱥᱟᱦᱴᱟ ᱵᱮᱥ ᱞᱮᱠᱟᱛᱮ ᱵᱟᱭ ᱪᱟᱞᱟᱣ ᱨᱩᱣᱟᱹᱲ ᱠᱟᱱᱟ</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱫᱚ ᱱᱮᱦᱚᱨ ᱨᱤᱴᱨᱭᱤᱵᱷ ᱞᱟᱹᱜᱤᱫ ᱪᱮᱥᱴᱟ ᱵᱚᱸᱫᱚᱭ ᱠᱮᱜᱼᱟᱭ ᱾ ᱥᱟᱭᱴ ᱱᱚᱶᱟ ᱦᱤᱥᱟᱵ ᱨᱮ ᱨᱤᱰᱟᱭᱨᱮᱠᱼᱴ ᱮᱜᱼᱟᱭ ᱡᱮ ᱱᱮᱦᱚᱨ ᱛᱤᱥᱚᱜ ᱵᱟᱭ ᱯᱩᱨᱟᱹᱣᱜᱼᱟ ᱾</p>
+ <ul>
+ <li>ᱟᱢ ᱪᱮᱫ ᱠᱩᱩᱠᱤᱡ ᱠᱚ ᱮᱢᱟᱱ ᱵᱚᱸᱫᱚ ᱟᱠᱟᱫᱼᱟᱢ ᱟᱨ ᱵᱟᱝ ᱵᱞᱚᱠ ᱟᱠᱟᱫᱼᱟᱢ ᱚᱠᱟ ᱫᱚ ᱥᱟᱭᱤᱴ ᱛᱟᱭ ᱫᱚᱨᱠᱟᱨ?</li>
+ <li>ᱡᱚᱫᱤ ᱠᱩᱩᱠᱤᱡ ᱤᱫᱤ ᱠᱟᱛᱮᱫ ᱦᱚᱸ ᱫᱤᱜᱫᱷᱟᱹ ᱵᱟᱝ ᱴᱷᱤᱠ ᱚᱜᱼᱟ, ᱮᱱᱰᱮᱠᱷᱟᱱ ᱱᱚᱶᱟ ᱫᱚ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱠᱚᱱᱯᱷᱤᱜᱭᱩᱨᱮᱥᱚᱱ ᱨᱮ ᱵᱷᱩᱞ ᱛᱟᱦᱮᱸ ᱠᱚᱜᱼᱟ ᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱨᱮ ᱫᱚ ᱵᱟᱝᱟ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ᱚᱯᱷᱞᱟᱭᱤᱱ ᱢᱳᱰ</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>ᱵᱨᱟᱣᱡᱚᱨ ᱫᱚ ᱚᱯᱷᱞᱟᱭᱤᱱ ᱢᱳᱰ ᱨᱮ ᱠᱷᱩᱞᱟᱹᱜ ᱠᱟᱱᱟ ᱟᱨ ᱱᱮᱦᱚᱨ ᱟᱠᱟᱱ ᱡᱤᱱᱤᱥ ᱥᱟᱞᱟᱜ ᱵᱟᱝ ᱡᱩᱰᱟᱹᱣᱼᱜ ᱠᱟᱱᱟ ᱾</p>
+ <ul>
+ <li>ᱪᱮᱫ ᱱᱚᱶᱟ ᱥᱟᱫᱷᱱ ᱫᱚ ᱮᱠᱴᱤᱵᱷ ᱱᱮᱴᱣᱟᱨᱠ ᱥᱟᱞᱟᱜ ᱡᱩᱲᱟᱹᱣ ᱠᱟᱱᱟ ?</li>
+ <li>ᱫᱚᱭᱟᱠᱟᱛᱮ ᱚᱱᱞᱟᱭᱤᱱ ᱢᱳᱰ ᱟᱨ ᱥᱟᱦᱴᱟ ᱨᱤᱞᱚᱰ ᱞᱟᱹᱜᱤᱫ "ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟ " ᱛᱮ ᱴᱤᱯᱟᱹᱣ ᱯᱮ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">ᱡᱟᱹᱯᱛᱤ ᱠᱟᱨᱚᱬ ᱞᱟᱹᱜᱤᱫ ᱟᱠᱚᱴ ᱛᱤᱸᱜᱩ ᱴᱷᱟᱸᱣ</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>ᱱᱮᱸᱦᱚᱨᱟᱠᱟᱱ ᱴᱷᱤᱠᱬᱟᱹ ᱯᱚᱨᱴ ᱮ ᱩᱫᱩᱜᱮᱟᱭ(ᱡᱮᱢᱚᱱ, <q>mozilla.org:80</q> ᱯᱚᱨᱴ 80 ᱞᱟᱹᱜᱤᱫ mozilla.org ᱨᱮ) ᱚᱫᱷᱤᱠᱟᱸᱥ ᱵᱮᱵᱷᱟᱨᱟᱠᱚ <em>ᱚᱞᱟᱜᱟ ᱦᱤᱹᱥᱟᱵ ᱛᱮ</em> ᱣᱮᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱪᱷᱚᱰᱟ ᱾ ᱵᱨᱟᱣᱡᱟᱹᱨ ᱫᱚ ᱟᱢᱟᱜ ᱠᱷᱟᱹᱛᱤᱨ ᱟᱨ ᱥᱨᱩᱠᱷᱭᱟᱹ ᱞᱟᱹᱜᱤᱫ ᱱᱮᱦᱚᱨ ᱵᱟᱛᱤᱞ ᱠᱮᱜᱼᱟᱭ ᱾</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">ᱡᱚᱱᱚᱲᱟᱹᱣ ᱫᱚ ᱨᱤᱥᱮᱴ ᱮᱱᱟ</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>ᱡᱩᱲᱟᱹᱣ ᱱᱮᱜᱚᱥᱤᱭᱮᱴ ᱡᱚᱠᱷᱟᱜ ᱱᱮᱴᱣᱟᱨᱠ ᱞᱤᱝᱠ ᱠᱚᱴᱟᱣᱮᱱᱟ ᱾ ᱫᱟᱭᱟ ᱠᱟᱛᱮ ᱫᱚᱦᱲᱟ ᱨᱤᱠᱟᱹᱭ ᱢᱮ </p><ul><li>ᱥᱟᱭᱤᱴ ᱵᱟᱝ ᱧᱟᱢ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱥᱮ ᱠᱟᱹᱢᱤ ᱨᱮ ᱛᱟᱦᱮᱱᱟ ᱾ ᱠᱤᱪᱷᱩ ᱜᱷᱟᱹᱲᱤᱡ ᱛᱟᱭᱚᱢ ᱨᱤᱠᱟᱹᱭ ᱢᱮ ᱾ </li><li> ᱡᱩᱫᱤ ᱥᱟᱦᱴᱟ ᱵᱟᱝ ᱮᱢ ᱞᱟᱫᱮ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ, ᱟᱢᱟᱜ ᱰᱤᱵᱷᱟᱭᱥ ᱨᱮᱱᱟᱜ ᱰᱟᱴᱟ ᱥᱮ ᱣᱟᱭ-ᱯᱷᱟᱭ ᱡᱚᱲᱟᱣ ᱧᱮᱞ ᱞᱮᱜᱟᱭ ᱢᱮ ᱾ </li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">ᱨᱮᱫ ᱯᱨᱚᱠᱟᱨ ᱵᱟᱝᱨᱟᱠᱷᱭᱟᱼᱟᱜ ᱠᱟᱱᱟ</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱤᱥᱚᱭ ᱨᱮ ᱠᱷᱚᱵᱚᱨ ᱮᱢᱟ ᱠᱚᱢ</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">ᱨᱟᱹᱯᱩᱫ ᱡᱤᱱᱤᱥ ᱦᱩᱲᱟᱹᱜ</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[ <p>ᱚᱠᱟ ᱥᱟᱦᱴᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ ᱠᱷᱚᱡᱚᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱸᱡ ᱚᱱᱟ ᱫᱚ ᱰᱟᱴᱟ ᱴᱨᱟᱱᱥᱢᱤᱥᱥᱚᱱ ᱨᱮ ᱵᱷᱩᱞ ᱞᱟᱹᱜᱤᱫ ᱵᱟᱝ ᱫᱮᱠᱷᱟᱣᱞᱮᱱᱟ᱾</p>
+ <ul>
+ <li>ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱫ ᱠᱷᱚᱵᱚᱨ ᱮᱢ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">ᱡᱤᱱᱤᱥ ᱠᱨᱟᱥ</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>ᱚᱠᱟ ᱥᱟᱦᱴᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ ᱠᱷᱚᱡᱚᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱸᱡ ᱚᱱᱟ ᱫᱚ ᱰᱟᱴᱟ ᱴᱨᱟᱱᱥᱢᱤᱥᱥᱚᱱ ᱨᱮ ᱵᱷᱩᱞ ᱞᱟᱹᱜᱤᱫ ᱵᱟᱞᱮ ᱫᱮᱠᱷᱟᱣ ᱫᱟᱲᱮᱭᱟᱞᱮᱢ ᱠᱟᱱᱟ ᱾</p>
+ <ul>
+ <li>ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱫ ᱠᱷᱚᱵᱚᱨ ᱮᱢ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">ᱡᱤᱱᱤᱥ ᱮᱱᱠᱳᱰᱤᱸᱝ ᱵᱷᱩᱞ</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>ᱚᱠᱟ ᱥᱟᱦᱴᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ ᱠᱷᱚᱡᱚᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱸᱡ ᱮᱢ ᱚᱱᱟ ᱫᱚ ᱤᱱᱣᱮᱞᱤᱰ ᱟᱨ ᱵᱟᱝ ᱵᱟᱭ ᱥᱟᱯᱯᱚᱴ ᱮᱫ ᱠᱚᱢᱯᱨᱮᱥᱥᱚᱱ ᱯᱷᱚᱨᱢ ᱨᱮ ᱢᱮᱱᱟᱜ ᱠᱷᱟᱹᱛᱤᱨ ᱵᱟᱝ ᱫᱮᱠᱷᱟᱣᱜ ᱠᱟᱱᱟ ᱾</p>
+ <ul>
+ <li>ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱫ ᱠᱷᱚᱵᱚᱨ ᱮᱢ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ᱴᱷᱤᱠᱬᱟᱹ ᱵᱟᱝ ᱧᱟᱞ ᱞᱟᱱᱟ</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>ᱮᱢ ᱟᱠᱟᱱ ᱴᱷᱤᱠᱬᱟᱹ ᱨᱮ ᱵᱨᱟᱣᱡᱚᱨ ᱦᱚᱥᱴ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱵᱟᱭ ᱧᱟᱢ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟᱭ ᱾</p>
+ <ul>
+ <li>ᱵᱷᱩᱞ ᱴᱭᱯᱤᱝ ᱞᱟᱹᱜᱤᱫ ᱴᱷᱤᱠᱬᱟᱹ ᱠᱚ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱡᱮᱢᱚᱱ
+ <strong>ww</strong>.example.com ᱵᱚᱫᱚᱞ ᱛᱮ
+ <strong>www</strong>.example.com.</li>
+ <li>ᱟᱢ ᱡᱩᱫᱤ ᱥᱟᱦᱴᱟ ᱵᱟᱝ ᱞᱚᱰ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟᱢ, ᱢᱮᱱᱠᱷᱟᱱ ᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱨᱮᱭᱟᱜ ᱰᱟᱴᱟ ᱟᱨ ᱵᱟᱝ Wi-Fi ᱡᱩᱲᱟᱹᱣ ᱧᱮᱞ ᱵᱤᱲᱟᱹᱣ ᱢᱮ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ᱤᱱᱴᱟᱹᱨᱱᱮᱴ ᱡᱚᱱᱚᱲᱟᱣ ᱵᱟᱹᱱᱩᱜ-ᱟ</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ᱱᱮᱴᱣᱟᱨᱠ ᱡᱩᱲᱟᱹᱣ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱟᱨ ᱵᱟᱝ ᱠᱤᱪᱷᱤ ᱥᱚᱢᱚᱭ ᱛᱟᱭᱚᱢ ᱛᱮ ᱥᱟᱦᱴᱟ ᱫᱩᱦᱲᱟᱹ ᱨᱤᱞᱚᱰ ᱢᱮ ᱾</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ᱫᱚᱦᱲᱟ ᱞᱟᱫᱤ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ᱵᱷᱩᱞ ᱴᱷᱤᱠᱬᱟᱹ</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>ᱮᱢ ᱟᱠᱟᱱ ᱴᱷᱤᱠᱬᱟᱹ ᱫᱚ ᱵᱟᱝ ᱵᱟᱰᱟᱭ ᱯᱷᱚᱨᱢᱟᱴ ᱨᱮ ᱢᱮᱱᱟᱜᱼᱟ ᱾ ᱫᱚᱭᱟᱠᱟᱛᱮ ᱡᱟᱭᱜᱟ ᱵᱟᱨ ᱨᱮ ᱵᱷᱩᱞ ᱠᱚ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱟᱨ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ ᱾</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ᱴᱷᱤᱠᱬᱟᱹ ᱵᱟᱭ ᱴᱷᱤᱠ ᱟ</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>ᱣᱮᱵ ᱴᱷᱤᱠᱬᱟᱹ ᱫᱚ<strong>http://www.example.com/</strong>ᱠᱚ ᱚᱞᱟ</li>
+ <li>ᱯᱷᱚᱨᱣᱟᱰ ᱥᱞᱟᱥ ᱮᱢ ᱟᱞᱚ ᱦᱤᱲᱤᱧᱟᱢ (ᱡᱮᱢᱚᱱ <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">ᱵᱟᱝ ᱵᱟᱰᱟᱭ ᱯᱚᱨᱴᱟᱞ</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>ᱴᱷᱤᱠᱬᱟᱹ ᱫᱚ ᱯᱨᱚᱴᱚᱠᱚᱞ ᱮ ᱫᱮᱠᱷᱟᱣᱮᱫᱟᱭ (ᱡᱮᱢᱚᱱ, <q>wxyz://</q>) ᱵᱨᱟᱣᱡᱟᱹᱨ ᱵᱟᱝ ᱪᱤᱱᱦᱟᱹᱣ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ, ᱵᱟᱨᱣᱡᱟᱹᱨ ᱥᱟᱭᱤᱴ ᱨᱮ ᱵᱟᱝ ᱡᱩᱲᱟᱣᱼ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ ᱾</p>
+ <ul>
+ <li>ᱟᱢ ᱪᱮᱫ ᱢᱟᱹᱞᱴᱤᱢᱤᱰᱤᱭᱟ ᱟᱨ ᱵᱟᱝ ᱵᱟᱝᱼᱚᱞ ᱥᱟᱹᱨᱣᱤᱥ ᱫᱚᱨᱠᱟᱨ ᱥᱮ? ᱡᱟᱹᱥᱛᱤ ᱡᱤᱱᱤᱥ ᱞᱟᱹᱜᱤᱫ ᱥᱟᱭᱤᱴ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱾</li>
+ <li>ᱠᱤᱪᱷᱤ ᱯᱚᱨᱴᱟᱞ ᱫᱚ ᱛᱷᱟᱰ ᱯᱟᱨᱴᱭ ᱥᱚᱯᱷᱴᱣᱮᱨ ᱟᱨ ᱵᱟᱝ ᱯᱞᱟᱹᱜᱤᱱᱥ ᱫᱚᱨᱠᱟᱨ ᱚᱠᱟ ᱫᱚ ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱪᱤᱷᱟᱣ ᱨᱮ ᱜᱚᱲᱚᱣᱟᱭ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ᱨᱮᱫ ᱵᱟᱝ ᱧᱟᱢ ᱞᱮᱱᱟ</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[•<ul>•<li>ᱪᱮᱫ ᱟᱭᱴᱚᱢ ᱫᱚᱦᱲᱟ ᱛᱮ ᱧᱩᱛᱩᱢ ᱟᱹᱛᱩ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ, ᱚᱪᱚᱫ, ᱥᱮ ᱫᱚᱦᱲᱟ ᱛᱷᱟᱯᱚᱱᱚᱜ-ᱟ?</li>•<li>ᱪᱮᱫ ᱮᱰᱨᱮᱥ ᱨᱮ ᱟᱹᱲ ᱠᱮᱯᱤᱴᱭᱞᱤᱡᱮᱥᱚᱱ, ᱥᱮ ᱮᱴᱟᱜ ᱴᱟᱭᱯᱳᱜᱽᱨᱟᱯᱷᱤᱠᱟᱞ ᱨᱮ ᱦᱩᱲᱟᱹᱜ ᱢᱮᱱᱟᱜ-ᱟ?</li>•<li> ᱪᱮᱫ ᱟᱢ ᱴᱷᱮᱱ ᱫᱟᱣ ᱮᱢ ᱞᱟᱹᱜᱤᱫ ᱥᱟᱳᱷᱤᱥᱤᱭᱚᱱᱴ ᱯᱚᱨᱢᱤᱥᱚᱱ ᱢᱮᱱᱟᱜ-ᱟ? </li>•</ul>•]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ᱨᱮᱫ ᱨᱮ ᱫᱚᱠᱷᱚᱞ ᱵᱟᱹᱰ</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>ᱱᱚᱶᱟ ᱫᱚ ᱚᱰᱚᱠ, ᱩᱪᱟᱹᱲ, ᱟᱨ ᱵᱟᱝ ᱨᱮᱫ ᱨᱮᱭᱟᱜ ᱟᱹᱱᱩᱢᱟᱫᱛᱤ ᱠᱚ ᱠᱷᱟᱹᱛᱤᱨ ᱦᱩᱭ ᱠᱚᱜᱼᱟ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ᱮᱲᱮ ᱥᱟᱹᱨᱚᱣᱟᱹᱨ ᱡᱚᱱᱟᱲᱟᱣ ᱵᱟᱭ ᱦᱮᱸ ᱞᱮᱫᱟ</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>ᱱᱚᱶᱟ ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱫᱚ ᱯᱨᱚᱠᱥᱭ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱞᱟᱹᱤᱫ ᱜᱮ ᱴᱷᱤᱠ ᱟᱠᱟᱱᱟ, ᱦᱮᱞᱮ ᱯᱨᱚᱠᱥᱭ ᱫᱚ ᱵᱟᱭ ᱡᱩᱰᱟᱹᱣ ᱞᱮᱱᱟᱭ ᱾</p>
+ <ul>
+ <li>ᱵᱨᱟᱣᱡᱟᱹᱨ ᱯᱨᱚᱠᱥᱭ ᱠᱚᱱᱯᱷᱤᱜᱭᱩᱨᱮᱥᱚᱱ ᱴᱷᱤᱠ ᱜᱮᱭᱟ ᱥᱮ? ᱥᱟᱡᱟᱣ ᱠᱚ ᱧᱮᱞ ᱢᱮ ᱟᱨ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ</li>
+ <li>ᱪᱮᱫ ᱯᱨᱚᱠᱥᱭ ᱥᱟᱹᱨᱣᱤᱥ ᱱᱚᱶᱟ ᱱᱮᱴᱣᱟᱨᱠ ᱥᱟᱞᱟᱜ ᱡᱩᱲᱟᱹᱣ ᱟᱱᱩᱢᱟᱹᱛᱭ ᱮᱢᱚ ᱟᱭ ?</li>
+ <li>ᱱᱤᱛ ᱦᱚᱸ ᱵᱟᱭ ᱴᱷᱤᱠ ᱟ? ᱜᱚᱲᱚ ᱞᱟᱹᱜᱤᱫ ᱟᱢᱟᱜ ᱱᱮᱴᱣᱟᱨᱠ ᱮᱰᱢᱤᱱᱤᱥᱴᱨᱮᱴᱚᱨ ᱟᱨ ᱵᱟᱝ ᱤᱱᱴᱚᱨᱱᱮᱴ ᱯᱨᱚᱣᱟᱭᱰᱟᱹᱨ ᱥᱟᱞᱟᱜ ᱠᱟᱛᱷᱟᱜ ᱢᱮ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ᱮᱲᱮ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱵᱟᱝ ᱧᱟᱢ ᱞᱮᱱᱟ</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>ᱱᱚᱶᱟ ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱫᱚ ᱯᱨᱚᱠᱥᱭ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱞᱟᱹᱤᱫ ᱜᱮ ᱴᱷᱤᱠ ᱟᱠᱟᱱᱟ, ᱦᱮᱞᱮ ᱯᱨᱚᱠᱥᱭ ᱫᱚ ᱵᱟᱭ ᱧᱟᱢ ᱞᱟᱱᱟ ᱾</p>
+ <ul>
+ <li>ᱵᱨᱟᱣᱡᱟᱹᱨ ᱯᱨᱚᱠᱥᱭ ᱠᱚᱱᱯᱷᱤᱜᱭᱩᱨᱮᱥᱚᱱ ᱴᱷᱤᱠ ᱜᱮᱭᱟ ᱥᱮ? ᱥᱟᱡᱟᱣ ᱠᱚ ᱧᱮᱞ ᱢᱮ ᱟᱨ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ</li>
+ <li>ᱪᱮᱫ ᱯᱨᱚᱠᱥᱭ ᱥᱟᱹᱨᱣᱤᱥ ᱱᱚᱶᱟ ᱱᱮᱴᱣᱟᱨᱠ ᱥᱟᱞᱟᱜ ᱡᱩᱲᱟᱹᱣ ᱟᱱᱩᱢᱟᱹᱛᱭ ᱮᱢᱚ ᱟᱭ ?</li>
+ <li>ᱱᱤᱛ ᱦᱚᱸ ᱵᱟᱭ ᱴᱷᱤᱠ ᱟ? ᱜᱚᱲᱚ ᱞᱟᱹᱜᱤᱫ ᱟᱢᱟᱜ ᱱᱮᱴᱣᱟᱨᱠ ᱮᱰᱢᱤᱱᱤᱥᱴᱨᱮᱴᱚᱨ ᱟᱨ ᱵᱟᱝ ᱤᱱᱴᱚᱨᱱᱮᱴ ᱯᱨᱚᱣᱟᱭᱰᱟᱹᱨ ᱥᱟᱞᱟᱜ ᱠᱟᱛᱷᱟᱜ ᱢᱮ ᱾</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">ᱢᱟᱞᱣᱮᱨ ᱥᱟᱭᱤᱴ ᱤᱥᱥᱩ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p> %1$s ᱨᱮ ᱢᱮᱱᱟᱜ ᱟᱠᱟᱱᱟ ᱥᱟᱭᱴ ᱫᱚ ᱟᱴᱴᱟᱠ ᱥᱟᱭᱤᱴ ᱞᱮᱠᱷᱟ ᱛᱮ ᱠᱷᱚᱵᱚᱨ ᱟᱠᱟᱱᱟ ᱟᱨ ᱟᱢᱟᱜ ᱥᱤᱠᱭᱚᱨᱭᱴᱤ ᱞᱟᱫᱜᱤᱫ ᱚᱱᱟ ᱫᱚ ᱵᱚᱸᱫᱚ ᱟᱠᱟᱱᱟ ᱾</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ᱵᱮᱠᱷᱟᱛᱤᱨ ᱥᱟᱭᱤᱴ ᱤᱥᱥᱩ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s ᱨᱮ ᱢᱮᱱᱟᱜ ᱥᱟᱭᱤᱴ ᱫᱚ ᱵᱮᱠᱟᱨ ᱥᱚᱯᱷᱴᱣᱮᱨ ᱠᱚ ᱵᱟᱛᱚᱣ ᱟᱨ ᱟᱢᱟᱜ ᱥᱮᱠᱭᱚᱨᱟᱹᱴᱭ ᱯᱨᱤᱯᱷᱮᱨᱮᱱᱥ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱵᱞᱚᱠ ᱟᱠᱟᱱᱟ ᱾</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ᱵᱚᱛᱚᱨ ᱥᱟᱭᱤᱴ ᱤᱢ ᱪᱟᱞ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s ᱨᱮ ᱢᱮᱱᱟᱜ ᱥᱟᱭᱤᱴ ᱫᱚ ᱠᱷᱟᱹᱛᱨᱟ ᱜᱮᱭᱟ ᱟᱨ ᱟᱢᱟᱜ ᱥᱮᱠᱭᱚᱨᱟᱹᱴᱭ ᱯᱨᱤᱯᱷᱮᱨᱮᱱᱥ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱵᱞᱚᱠ ᱟᱠᱟᱱᱟ ᱾</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">ᱮᱲᱮ ᱥᱟᱭᱤᱴ ᱵᱟᱵᱚᱛ</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s ᱨᱮ ᱢᱮᱱᱟᱜ ᱥᱟᱭᱤᱴ ᱫᱚ ᱠᱷᱟᱹᱛᱨᱟ ᱟᱨ ᱮᱲᱮ ᱜᱮᱭᱟ ᱢᱮᱱᱛᱮ ᱠᱚ ᱠᱷᱚᱵᱚᱨ ᱟᱠᱟᱫᱼᱟ ᱟᱨ ᱟᱢᱟᱜ ᱥᱮᱠᱭᱚᱨᱟᱹᱴᱭ ᱯᱨᱤᱯᱷᱮᱨᱮᱱᱥ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱵᱞᱚᱠ ᱟᱠᱟᱱᱟ ᱾</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">ᱨᱩᱠᱷᱤᱭᱟᱹ ᱥᱟᱭᱤᱴ ᱫᱚ ᱵᱟᱹᱱᱩᱜ ᱟᱹᱱᱤᱡ</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[ᱨᱩᱠᱷᱤᱭᱟᱹ ᱵᱚᱰᱷᱟᱣ ᱞᱟᱹᱜᱤᱫ ᱟᱢ HTTPS-ᱠᱷᱟᱹᱞᱤ ᱢᱳᱰ ᱮᱢ ᱪᱷᱚ ᱟᱠᱟᱫᱟᱢ, ᱟᱨ HTTPS ᱵᱷᱟᱹᱨᱥᱚᱱ ᱨᱮᱭᱟᱜ <em>%1$s</em> ᱫᱚ ᱵᱟᱹᱱᱩᱜᱟᱹᱱᱤᱡ ᱾]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP ᱥᱟᱭᱤᱴ ᱛᱮ ᱞᱟᱦᱟᱜ ᱢᱮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..91eaa8369e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sc/strings.xml
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Torra a proare</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Impossìbile cumpletare sa rechesta</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Immoe non ddoe at a disponimentu àteras informatziones subra de custu problema o faddina.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Faddina in sa connessione segura</string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Faddina in sa connessione segura</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avantzadu…</string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">A coa (cussigiadu)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Atzeta s’arriscu e sighi</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Custu situ rechedet una connessione segura.</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avantzadu…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">A coa</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Connessione interrùmpida</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Tempus pro sa connessione iscadidu</string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Impossìbile connètere</string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Risposta imprevista de su serbidore</string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Sa pàgina no est torrende a indiritzare in manera curreta</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Modalidade foras de lìnia</string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Ghenna restrinta pro resones de seguresa</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Connessione torrada a aviare</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Genia de archìviu non segura</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Informa s’amministratzione de su situ web de custu problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Faddina de cuntenutu corrotu</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Sa pàgina chi ses proende a abèrrere non podet èssere ammustrada, ca est istada agatada una faddina in sa trasmissione de is datos.</p>
+ <ul>
+ <li>Cuntata is meres de su situ pro informare de custu problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Cuntenutu faddidu</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Sa pàgina chi ses proende a abèrrere non podet èssere ammustrada, ca est istada agatada una faddina in sa trasmissione de is datos.</p>
+ <ul>
+ <li>Cuntata is meres de su situ pro informare de custu problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Faddina de codìfica de su cuntenutu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Indiritzu no agatadu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nissuna connessione de internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Controlla sa cunfiguratzione de rete o torra a carrigare sa pàgina luego.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Torra a carrigare</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Indiritzu non vàlidu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">S’indiritzu no est vàlidu</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Is indiritzos web in generale s’iscrient in custa manera: <strong>http://www.esempru.com/</strong></li>
+ <li>Assegura·ti chi ses impreende is barras curretas (<strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocollu disconnotu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Archìviu no agatadu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Atzessu denegadu a s’archìviu</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Podet dare chi siat istadu cantzelladu o mòvidu, o chi is permissos de archìviu bi siant blochende s’atzessu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Connessione cun su serbidore intermediàriu refudada</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Impossìbile agatare su serbidore intermediàriu</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Su situ in s’indiritzu %1$s est istadu sinnaladu comente situ web malitziosu e est istadu blocadu de acordu cun is preferèntzias de seguresa tuas.</p>
+ ]]></string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Ant sinnaladu chi su situ in s’indiritzu %1$s cuntenet programmas non disigiados, e est istadu blocadu de acordu cun is preferèntzias de seguresa tuas.</p>
+ ]]></string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Su situ in s’indiritzu %1$s est istadu sinnaladu comente perigulosu e est istadu blocadu de acordu cun is preferèntzias de seguresa tuas.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema de situ ingannosu</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Sa pàgina web in s’indiritzu %1$s est istada sinnalada comente ingannosa e est istada blocada de acordu cun is preferèntzias de seguresa tuas.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Situ seguru non disponìbile</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[As ativadu sa modalidade HTTPS ebbia pro megiorare sa seguresa, ma no esistet sa versione HTTPS de su situ <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Sighi in su situ HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..a60c5503b6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-si/strings.xml
@@ -0,0 +1,327 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">නැවත</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ඉල්ලීම නිම කළ නොහැකිය</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>දැනට මෙම ගැටලුව හෝ දෝෂය පිළිබඳව අතිරේක තොරතුරු නැත.</p>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">ආරක්‍ෂිත සම්බන්‍ධතාවය බිඳ වැටුණි</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+<ul>
+ <li>ලැබුණු දත්ත සත්‍යාපනයට නොහැකි නිසා ඔබ දැකීමට උත්සාහ කරන පිටුව පෙන්වීමට නොහැකිය.</li>
+ <li>මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.</li>
+</ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">ආරක්‍ෂිත සම්බන්‍ධතාවය බිඳ වැටුණි</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>මෙය සේවාදායකයේ වින්‍යාසයෙහි ගැටලුවක් විය හැකිය, හෝ යමෙක් සේවාදායකය ලෙස වංචනිකව පෙනී සිටීමට තැත් කරනවා විය හැකිය.</li>
+ <li>ඔබ කලින් මෙම සේවාදායකයට සාර්ථකව සම්බන්‍ධ වී ඇත්නම්, මෙය තාවකාලික දෝෂයක් විය හැකිය, පසුව උත්සාහ කරන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">වැඩිදුර…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>යමෙක් වංචනිකව මෙම අඩවිය හසුරුවීමට තැත් කරන හෙයින් ඔබ තවදුරටත් ඉදිරියට නොයා යුතුය.</label>
+ <br><br>
+ <label>සහතික හරහා අඩවි ඔවුන්ගේ අනන්‍යතාවය ඔප්පු කරයි. <b>%2$s</b> හි සහතිකයේ නිකුත්කරු නොදන්නා නිසා %1$s එය විශ්වාස නොකරයි, සහතිකය ස්වයං-අත්සන් කර හෝ සේවාදායකය නිවැරදි අතරමැදි සහතිකය එවන්නේ නැත.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ආපසු යන්න (නිර්දේශිතයි)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">අවදානම පිළිගෙන ඉදිරියට</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">අඩවියට ආරක්‍ෂිත සම්බන්ධතාවයක් අවශ්‍යයි.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>ඔබ දැකීමට උත්සාහ කරන අඩවියට ආරක්‍ෂිත සම්බන්ධතාවයක් අවශ්‍ය නිසා පෙන්වීමට නොහැකිය.</li>
+ <li>බොහෝ විට අඩවියෙහි ගැටලුවක් නිසා, එය විසඳීමට ඔබට කළ හැකි කිසිවක් නැත.</li>
+ <li>මෙම ගැටලුව පිළිබඳව අඩවියේ පරිපාලකයා වෙත දැනුම් දීමට හැකිය.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">වැඩිදුර…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> හි HTTP දැඩි පරිවහන ආරක්‍ෂාව (HSTS) නම් ආරක්‍ෂණ ප්‍රතිපත්තියක් ඇත, එනම් <b>%2$s</b> මගින් එයට සම්බන්ධ වීමට හැකි වන්නේ ආරක්‍ෂිතව පමණි. ඔබට මෙම අඩවියට ගොඩවැදීමට හැරදැමීමක් එක් කිරීමට නොහැකිය. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">ආපසු යන්න</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">සම්බන්ධතාවයට බාධා විය</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>අතිරික්සුව සාර්ථකව සම්බන්ධ විය, නමුත් තොරතුරු මාරු කිරීමේ දී සම්බන්ධතාවයට බාධා විය. නැවත උත්සාහ කරන්න.</p>
+ <ul>
+ <li>අඩවිය තාවකාලිකව නොතිබේ හෝ ඉතා කාර්ය බහුලයි. මොහොතකින් නැවත බලන්න.</li>
+ <li>කිසිදු පිටුවක් පූරණය නොවේ නම්, ඔබගේ උපාංගයේ දත්ත හෝ වයි-ෆයි සම්බන්ධතාවය පරීක්‍ෂා කරන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">සම්බන්ධතාවය ඉකුත් වී ඇත</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>ඉල්ලන ලද අඩවිය සම්බන්ධතා ඉල්ලීමකට ප්‍රතිචාර නොදැක්වූ අතර අතිරික්සුව පිළිතුරක් අපේක්‍ෂාවෙන් සිටීම නතර කර ඇත.</p>
+ <ul>
+ <li>සේවාදායකය වෙත ඉහළ ඉල්ලුමක් හෝ තාවකාලික ඇණහිටීමක් විය හැකිද? පසුව උත්සාහ කරන්න.</li>
+ <li>ඔබට වෙනත් අඩවි පිරික්සීමට නොහැකිද? පරිගණකයේ ජාල සම්බන්ධතාව පරීක්‍ෂා කරන්න.</li>
+ <li>ඔබගේ පරිගණකය හෝ ජාලය ගිනි පවුරකින් හෝ ප්‍රතියුක්තයකින් ආරක්‍ෂා කර තිබේද? වැරදි සැකසුම් නිසා මෙවැනි දෑ සිදු වීමට හැකිය.</li>
+ <li>තවමත් ගැටළු තිබේද? සහාය සඳහා ඔබගේ ජාලයේ පරිපාලක හෝ අන්තර්ජාල සැපයුම්කරු අමතන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">සම්බන්ධ වීමට නොහැකිය</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>අඩවිය තාවකාලිකව නොතිබේ හෝ කාර්ය බහුලයි. මොහොතකින් උත්සාහ කරන්න.</li>
+ <li>කිසිදු පිටුවක් පූරණය නොවේ නම්, උපාංගයේ දත්ත හෝ වයි-ෆයි සම්බන්ධතාවය පරීක්‍ෂා කරන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">සේවාදායකයෙන් අනපේක්‍ෂිත ප්‍රතිචාරයකි</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>ජාල ඉල්ලීමට අනපේක්‍ෂිත අයුරින් අඩවිය ප්‍රතිචාර දැක්වූ අතර අතිරික්සුවට ඉදිරියට යාමට නොහැකිය.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">පිටුව නිසි අයුරින් හරවා යවන්නේ නැත</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>අතිරික්සුව ඉල්ලූ අථකය ලබා ගැනීමට උත්සාහ කිරීම නවතා ඇත. අඩවිය ඉල්ලීම කිසිම විටෙක නිම නොවන අයුරින් යළි හරවා යවයි.</p>
+ <ul>
+ <li>මෙම අඩවියට වුවමනා දත්තකඩ අබල හෝ අවහිර කර තිබේ ද?</li>
+ <li>අඩවියෙහි දත්තකඩ පිළිගැනීමෙන් ගැටලුව නොවිසඳෙයි නම්, එය ඔබගේ උපාංගයේ නොව සේවාදායකයේ වින්‍යාසයෙහි වරදක් විය හැකිය.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">මාර්ගඅපගත ප්‍රකාරය</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>අතිරික්සුව එහි මාර්ගඅපගත ප්‍රකාරයෙන් ක්‍රියාත්මක වන නිසා ඉල්ලූ අථකයට සම්බන්ධ වීමට නොහැකිය.</p>
+ <ul>
+ <li>උපාංගය සක්‍රිය ජාලයකට සම්බන්ධිත ද?</li>
+ <li>මාර්ගගත ප්‍රකාරයට මාරු වීමට සහ පිටුව නැවත පූරණයට “යළි උත්සාහ කරන්න” ඔබන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">ආරක්‍ෂණ හේතුන් මත තොට සීමා කර ඇත.</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>ඉල්ලන ලද ලිපිනය වියමන පිරික්සීමට සාමාන්‍යයෙන් <em>භාවිතා නොකරන</em> තොටක් (උදා. mozilla.org හි තොට 80 සඳහා <q>mozilla.org:80</q>) භාවිතා කරයි. ඔබගේ රැකවරණය හා ආරක්‍ෂාව සඳහා අතිරික්සුව එම ඉල්ලීම අවලංගු කරන ලදි.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">සම්බන්ධතාවය යළි සැකසිණි</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>සම්බන්ධතාවයක් කතිකා වන අතරතුර ජාල සබැඳියට බාධා විය. යළි උත්සාහ කරන්න.</p>
+ <ul>
+ <li>අඩවිය තාවකාලිකව නොතිබේ හෝ ඉතා කාර්ය බහුලයි. මොහොතකින් නැවත බලන්න.</li>
+ <li>කිසිදු පිටුවක් පූරණය නොවේ නම්, ඔබගේ උපාංගයේ දත්ත හෝ වයි-ෆයි සම්බන්ධතාවය පරීක්‍ෂා කරන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">අනාරක්‍ෂිත ගොනු වර්ගයකි</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+<ul>
+ <li>කරුණාකර මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.</li>
+</ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">හානි වූ අන්තර්ගත දෝෂයකි</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>දත්ත සම්ප්‍රේෂණ දෝෂයක් අනාවරණය වූ නිසා ඔබ දැකීමට උත්සාහ කරන පිටුව පෙන්වීමට නොහැකිය.</p>
+ <ul>
+ <li>මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">අන්තර්ගතය බිඳ වැටුණි</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>දත්ත සම්ප්‍රේෂණ දෝෂයක් අනාවරණය වූ නිසා ඔබ දැකීමට උත්සාහ කරන පිටුව පෙන්වීමට නොහැකිය.</p>
+ <ul>
+ <li>මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">අන්තර්ගත ආකේතන දෝෂයකි</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>ඔබ දැකීමට උත්සාහ කරන පිටුව වලංගු නොවන හෝ සහාය නොදක්වන හැකිළුම් අන්දමක් භාවිතා කරන නිසා පෙන්වීමට නොහැකිය.</p>
+ <ul>
+ <li>මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.</li></ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ලිපිනය හමු නොවුණි</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>අතිරික්සුවට ලබා දුන් ලිපිනයෙහි සත්කාරක සේවාදායකය සොයා ගැනීමට නොහැකිය.</p>
+ <ul>
+ <li>මෙවැනි ලිවීමේ දෝෂ තිබේදැයි බලන්න
+ <strong>www</strong>.example.com වෙනුවට
+ <strong>ww</strong>.example.com.</li>
+ <li>කිසිදු පිටුවක් පූරණය නොවේ නම්, ඔබගේ උපාංගයේ දත්ත හෝ වයි-ෆයි සම්බන්ධතාවය පරීක්‍ෂා කරන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">අන්තර්ජාල සම්බන්ධතාවයක් නැත</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ඔබගේ ජාලයේ සම්බන්ධතාවය පරීක්‍ෂා කරන්න හෝ මොහොතකින් පිටුව යළි පූරණය කර බලන්න.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">යළි පූරණය</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">සාවද්‍ය ලිපියකි</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>ලබා දුන් ලිපිනය පිළිගත් ආකෘතියක නැත. වැරදි සඳහා ස්ථාන තීරුව පරීක්‍ෂා කර නැවත උත්සාහ කරන්න.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ලිපිනය වලංගු නොවේ</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>ලිපින සාමාන්‍යයෙන් ලියන්නේ <strong>http://www.example.com/</strong> ලෙසය</li>
+ <li>ඔබ ඉදිරි ඇල ඉරි භාවිතා කරන බවට වග බලා ගන්න (එනම් <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">නොදන්නා කෙටුම්පතකි</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>අතිරික්සුව හඳුනා නොගන්නා කෙටුම්පතක් ලිපිනයෙන් නිරූපනය කෙරේ (උදා: <q>wxyz://</q>), එබැවින් අඩවියට නිසි ලෙස සම්බන්ධ වීමට නොහැකිය.</p>
+ <ul>
+ <li>ඔබ බහුමාධ්‍ය හෝ වෙනත් අකුරු නොවන සේවා වෙත ප්‍රවේශ වීමට උත්සාහ කරනවාද? අමතර අවශ්‍යතා සඳහා අඩවිය පරීක්‍ෂා කරන්න.</li>
+ <li>සමහර කෙටුම්පත් සඳහා ඒවා අතිරික්සුවකින් හඳුනා ගැනීමට තෙවන පාර්ශ්ව මෘදුකාංග හෝ පේනු අවශ්‍ය විය හැකිය.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ගොනුව හමු නොවුණි</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>අථකය යළි නම් කර, ඉවත් කර, හෝ අන් තැනකට ගෙන ගොස් ද?</li>
+ <li>ලිපිනයෙහි අකුරු වින්‍යාසය, හැඩයේ හෝ වෙනත් මුද්‍රණ දෝෂයක් තිබේද?</li>
+ <li>ඔබ ඉල්ලන ලද අථකයට ප්‍රමාණවත් ප්‍රවේශ වීමේ අවසර තිබේද?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ගොනුවට ප්‍රවේශය ප්‍රතික්‍ෂේප විය</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>එය ඉවත් කර, ගෙන ගොස්, හෝ ගොනුවට ප්‍රවේශ වීමේ අවසර නැත.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ප්‍රතියුක්ත සේවාදායකය සබැඳුම ප්‍රතික්‍ෂේප කෙරිණි</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>ප්‍රතියුක්ත සේවාදායකයක් භාවිතයට අතිරික්සුව වින්‍යාසගත කර ඇත, නමුත් ප්‍රතියුක්තය සම්බන්ධතාවයක් ප්‍රතික්‍ෂේප කෙරිණි.</p><ul>
+ <li>අතිරික්සුවෙහි ප්‍රතියුක්ත වින්‍යාසය නිවැරදි ද? සැකසුම් පරීක්‍ෂා කර යළි උත්සාහ කරන්න.</li>
+ <li>ප්‍රතියුක්ත සේවාව මෙම ජාලයෙහි සම්බන්ධතා සඳහා ඉඩ දෙන්නේද?</li>
+ <li>තවමත් ගැටළු තිබේද? සහාය සඳහා ඔබගේ ජාලයේ පරිපාලක හෝ අන්තර්ජාල සැපයුම්කරු අමතන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ප්‍රතියුක්ත සේවාදායකය හමු නොවුණි</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>ප්‍රතියුක්ත සේවාදායකයක් භාවිතයට අතිරික්සුව වින්‍යාසගත කර ඇත, නමුත් ප්‍රතියුක්තය හමු නොවිණි.</p><ul>
+ <li>අතිරික්සුවෙහි ප්‍රතියුක්ත වින්‍යාසය නිවැරදි ද? සැකසුම් පරීක්‍ෂා කර යළි උත්සාහ කරන්න.</li>
+ <li>උපාංගය සක්‍රිය සම්බන්ධතාවයක් වෙත සම්බන්ධිත ද?</li>
+ <li>තවමත් ගැටළු තිබේද? සහාය සඳහා ඔබගේ ජාලයේ පරිපාලක හෝ අන්තර්ජාල සැපයුම්කරු අමතන්න.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">අනිෂ්ට අඩවියක ගැටලුවකි</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>ප්‍රහාරක අඩවියක් ලෙස %1$s වාර්තා කර ඇත. ඔබගේ ආරක්‍ෂණ අභිප්‍රේත මත පදනම්ව අවහිර කර ඇත.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">අනවශ්‍ය අඩවියක ගැටලුවකි</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>අනවශ්‍ය මෘදුකාංග අඩවියක් ලෙස %1$s වාර්තා කර ඇත. ඔබගේ ආරක්‍ෂණ අභිප්‍රේත මත පදනම්ව අවහිර කර ඇත.</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">හානිකර අඩවියක ගැටලුවකි</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s අඩවිය හානිකර යැයි වාර්තා කර ඇත. ඔබගේ ආරක්‍ෂණ අභිප්‍රේත මත පදනම්ව අවහිර කර ඇත.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">කූට අඩවියක ගැටලුවකි</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>කූට පිටුවක් ලෙස %1$s වාර්තා කර ඇත. ඔබගේ ආරක්‍ෂණ අභිප්‍රේත මත පදනම්ව අවහිර කර ඇත.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">ආරක්‍ෂිත අඩවිය නැත</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[ඉහළ ආරක්‍ෂාවක් සඳහා ඔබ HTTPS-පමණි ප්‍රකාරය සබල කර තිබෙන අතර, <em>%1$s</em> හි HTTPS අනුවාදයක් නැත.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP අඩවියට යන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..bc692e3ced
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sk/strings.xml
@@ -0,0 +1,323 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Skúsiť znova</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Nepodarilo sa dokončiť požiadavku</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Dodatočná informácia o tomto probléme alebo chybe je momentálne nedostupná.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Zabezpečené pripojenie zlyhalo</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Stránku nemožno zobraziť, pretože pravosť prijímaných údajov sa nedá overiť.</li>
+ <li>Obráťte sa na vlastníkov stránky a informujte ich o tomto probléme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Zabezpečené pripojenie zlyhalo</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Toto môže byť problém s konfiguráciou servera alebo sa ho niekto snaží napodobniť.</li>
+ <li>Ak ste sa k tomuto serveru úspešne pripojili v minulosti, chyba môže byť dočasná a pokus by ste mali opakovať neskôr.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Pokročilé…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Niekto sa môže vydávať za daný server a preto by ste nemali pokračovať.</label>
+ <br><br>
+ <label>Webové stránky preukazujú svoju identitu pomocou certifikátov. Aplikácia %1$s nemôže stránku <b>%2$s</b>, pretože vydavateľ daného certifikátu je neznámy, certifikát je podpísaný vlastným podpisom alebo server neposiela správne sprostredkujúce certifikáty.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Prejsť naspäť (odporúča sa)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Rozumiem riziku a chcem pokračovať</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Táto webová stránka vyžaduje zabezpečené pripojenie.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Stránka, ktorú sa pokúšate zobraziť, sa nedá zobraziť, pretože táto webová lokalita vyžaduje zabezpečené pripojenie.</li>
+ <li>Problém je s najväčšou pravdepodobnosťou na webových stránkach a nemôžete ho nijako vyriešiť.</li>
+ <li>O probléme môžete informovať správcu webových stránok.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Pokročilé…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> má bezpečnostnú politiku s názvom HTTP Strict Transport Security (HSTS), čo znamená, že <b>%2$s</b> sa k nemu môže pripojiť iba zabezpečene. Na návštevu tohto webu nemôžete pridať výnimku.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Prejsť naspäť</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Pripojenie bolo prerušené</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Prehliadač sa úspešne pripojil k serveru, ale spojenie bolo v priebehu prenosu údajov prerušené. Prosím, skúste to znova.</p>
+ <ul>
+ <li>Stránka môže byť dočasne nedostupná alebo zaneprázdnená. Skúste to znova neskôr.</li>
+ <li>Ak sa vám nedarí načítať žiadnu stránku, skontrolujte pripojenie svojho zariadenia na internet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Čas pripojenia vypršal</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Vypršal čas pri pokuse o pripojenie na zadaný server.</p>
+ <ul>
+ <li>Server môže byť preťažený, alebo preťaženie siete spôsobuje prílišné omeškanie dát.</li>
+ <li>Ak sa dá predpokladať, že server je preťažený, skúste pred ďalším pokusom o pripojenie chvíľu počkať.</li>
+ <li>Ak sú vaše zariadenie alebo sieť chránené pomocou firewallu alebo servera proxy, skontrolujte ich nastavenia.</li>
+ <li>Pokiaľ budú problémy pretrvávať, kontaktujte svojho správcu siete alebo poskytovateľa pripojenia.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Nie je možné sa pripojiť</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Stránka môže byť dočasne nedostupná alebo zaneprázdnená. Svoj pokus opakujte neskôr.</li>
+ <li>Ak sa nedá načítať žiadna stránka, skontrolujte pripojenie svojho zariadenia k internetu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Neočakávaná odpoveď servera</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Server odpovedal na sieťovú požiadavku nečakaným spôsobom a prehliadač nedokáže pokračovať.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Stránku sa nepodarilo správne presmerovať</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Prehliadač prerušil spojenie, pretože server smeruje požiadavku sám na seba v nekonečnej slučke.</p>
+ <ul>
+ <li>Toto nastáva, ak stránka vyžaduje cookies a vy ste cookies nepovolili alebo sú pre túto stránku blokované.</li>
+ <li>Toto je častý problém s nastavením servera a nie vášho zariadenia.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Režim offline</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Prehliadač je momentálne v režime offline a nemôže sa pripojiť na požadovaný server.</p>
+ <ul>
+ <li>Je zariadenie pripojené k aktívnej sieti?</li>
+ <li>Kliknutím na tlačidlo „Skúsiť znova“ prejdete do režimu online a opätovne načítate obsah stránky.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Bezpečnostné obmedzenie prístupu na port</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Zadaná adresa (URL) špecifikovala port (napr. <q>mozilla.org:80</q> je port 80 na mozilla.org), ktorý je normálne určený na <em>iné</em> služby ako prehliadanie webu. Prehliadač požiadavku kvôli vašej ochrane a bezpečnosti neakceptoval.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Výpadok pripojenia</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Spojenie bolo v priebehu otvárania komunikačného kanála so serverom nečakane prerušené. Skúste to znova</p>
+ <ul>
+ <li>Stránka môže byť dočasne nedostupná alebo zaneprázdnená. Svoj pokus opakujte neskôr.</li>
+ <li>Ak sa nedá načítať žiadna stránka, skontrolujte pripojenie svojho zariadenia k internetu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Nebezpečný typ obsahu</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Prosím, obráťte sa na vlastníkov stránky a informujte ich o tomto probléme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Poškodený obsah stránky</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Táto stránka nemohla byť zobrazená kvôli chybe pri prenose údajov.</p>
+ <ul>
+ <li>Kontaktujte prevádzkovateľa webovej stránky a informujte ho o tomto probléme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Obsah zlyhal</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Táto stránka nemohla byť zobrazená kvôli chybe pri prenose údajov.</p>
+ <ul>
+ <li>Kontaktujte prevádzkovateľa webovej stránky a informujte ho o probléme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Chyba kódovania obsahu</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Stránka nemôže byť zobrazená, pretože používa neplatné alebo nepodporované formátovanie.</p>
+ <ul>
+ <li>Prosím, kontaktujte majiteľov stránky a informujte ich o tomto probléme.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adresa sa nenašla</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>URL adresa nezodpovedá žiadnemu serveru a nie je možné ju načítať.</p>
+ <ul>
+ <li>Skontrolujte, či ste adresu napísali správne (napr. že neobsahuje
+ <strong>ww</strong>.example.com namiesto
+ <strong>www</strong>.example.com).</li>
+ <li>Ak nemôžete načítať žiadnu stránku, skontrolujte pripojenie svojho zariadenia k internetu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Žiadne pripojenie k internetu</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Skontrolujte pripojenie k sieti alebo skúste načítať stránku o chvíľu.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Obnoviť</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Neplatná adresa</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Adresa (URL) nie je platná a nemožno ju načítať. Prosím, skontrolujte text v paneli s adresou a skúste to znova.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adresa nie je platná</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Webové adresy sú zvyčajne zadávané v tvare <strong>http://www.example.com/</strong></li>
+ <li>Skontrolujte, či používate správne lomky (t.j. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Neznámy protokol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adresa sa začína protokolom (napr. <q>wxyz://</q>), ktorý prehliadač nepozná, takže sa nemôže správne pripojiť k serveru.</p>
+ <ul>
+ <li>Pokúšate sa získať prístup k multimediálnym alebo iným netextovým službám? Na stránke budú zrejme uvedené ďalšie inštrukcie.</li>
+ <li>Niektoré protokoly môžu vyžadovať softvér alebo doplnky tretích strán, aby ich prehliadač mohol rozpoznať.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Súbor nebol nájdený</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Je možné, že bol odstránený, premenovaný alebo premiestnený?</li>
+ <li>Skontrolujte, či má adresa správny formát a či neobsahuje chyby.</li>
+ <li>Máte príslušné povolenia na zobrazenie daného súboru?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Prístup k súboru bol zamietnutý</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Mohol byť odstránený, premiestnený alebo vám v prístupe bránia oprávnenia.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy server odmietol spojenie</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Prehliadač je nakonfigurovaný na používanie proxy servera, no tento odmietol pripojenie.</p>
+ <ul>
+ <li>Skontrolujte nastavenia proxy servera v prehliadači a skúste to znova.</li>
+ <li>Má služba proxy povolený prístup k sieti?</li>
+ <li>Ak problémy pretrvávajú, kontaktujte, prosím, svojho správcu siete alebo poskytovateľa internetu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy server nenájdený</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Prehliadač je nakonfigurovaný na používanie proxy servera, no tento odmietol pripojenie.</p>
+ <ul>
+ <li>Skontrolujte nastavenia proxy servera v prehliadači a skúste to znova.</li>
+ <li>Je zariadenie pripojené k aktívnej sieti?</li>
+ <li>Ak problémy pretrvávajú, kontaktujte, prosím, svojho správcu siete alebo poskytovateľa internetu.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Stránka so škodlivým softvérom</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Stránka %1$s bola označená ako nebezpečná a na základe nastavení zabezpečenia bola zablokovaná.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Nežiadúca webová stránka</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Stránka %1$s bola označená ako ponúkajúca nevyžiadaný softvér a na základe nastavení zabezpečenia bola zablokovaná.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Škodlivá stránka</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Stránka %1$s bola označená ako nebezpečná a na základe nastavení zabezpečenia bola zablokovaná.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Podvodná stránka</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Stránka %1$s bola označená ako podvodná a na základe nastavení zabezpečenia bola zablokovaná.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Zabezpečená verzia stránky nie je k dispozícii</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Webové stránky prehliadate v režime "Len HTTPS", ale zabezpečená verzia stránky <em>%1$s</em> nie je k dispozícii.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Pokračovať na nezabezpečenú stránku</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..66896048e8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-skr/strings.xml
@@ -0,0 +1,312 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ولدا کوشش کرو</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ارداس پوری کائنی کر سڳدا</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>ایں مسئلے بارے وادھوں معلومات فی الحال دستیاب کائنی۔</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">قابل بھروسہ کنکشن ناکام تھی ڳیا</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو موصول ڈیٹا دی اصلیت دی تصدیق کائنی تھی سڳی۔</li>
+ <li>ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">قابل بھروسہ کنکشن ناکام تھی ڳیا</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>ایہ سرور کانفیگریشن دا کوئی مسئلہ تھی سڳدا ہے یا تھی سڳدے جو کوئی سرور کوں نقلی بݨاوݨ دی کوشش کریندا پیا ہے۔</li>
+ <li>جے تساں ماضی وچ ایں سرور نال جڑے ہو تاں تھی سڳدا ہے جو خرابی کجھ وقت کیتے ہووے تے تساں کجھ دیر بعد وت کوشش کر سڳدے ہو۔</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ودھایا۔۔۔</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>تھی سڳدا ہے جو کوئی سائٹ دی نقل بݨیندا پیا ہے تے تساں جاری نہ رکھو۔</label>
+ <br><br>
+ <label>ویب سائٹساں سرٹیفکیٹ دے ذریعہ آپݨی سُن٘ڄاݨ ثابت کریندیاں ہن۔ %1$s، ایں <b>%2$s</b> تے بھروسہ کائنی کریندا کیوں جو سرٹیفکیٹ جاری کرݨ آلا نامعلوم ہے، سرٹیفکیٹ تے خود آپ دستخط کیتے ہوئے ہن یا سرور صحیح درمیانی سرٹیفیکیٹ کائنی پٹھیندا پیا۔</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">واپس ون٘ڄو(سفارش کیتی ویندی ہے)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">خطرے کوں قبول کرو تے جاری رکھو</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">ایں ویب سائٹ کوں محفوظ کنکشن دی لوڑ ہے۔</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>جہڑا ورقہ تساں ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیوں جو ایں ویب سائٹ کوں محفوظ کنکشن دی لوڑ ہے۔</li>
+ <li>ایہ مسئلہ غالباً ویب سائٹ وی وجہ نال ہے۔ ایندے حل کیتے تہاݙے کول کجھ کائنی۔</li>
+ <li>تساں ایں مسئلے بارے ویب سائٹ ایڈمن کوں ݙسا سڳدے ہو۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">ودھایا۔۔۔</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> دی ہک حفاظتی پالیسی ہے جیکوں ایچ ٹی ٹی پی سخت ٹرانسپورٹ حفاظت(HSTS) آہدے ہن، جیندے مطلب ایہ ہے جو <b>%2$s</b> صرف ایندے نال حفاظت نال کنکٹ کر سڳدے۔ تساں ایں سائٹ تے ون٘ڄݨ کیتے کوئی استثناء شامل نہوے کر سڳدے۔</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">واپس ون٘ڄو</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">کنکشن خراب تھی ڳیا ہائی۔</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+<p>براؤزر کامیابی نال کنکٹ تھی ڳیا ہے، پر معلومات دی منتقلی دے دوران کنکشن وچ رکاوٹ پیدا تھئی۔ براہ کرم، ولدا کوشش کرو۔</p>
+ <ul>
+ <li>سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد وت کوشش کرو۔</li>
+ <li>جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے ، تاں آپݨاں ڈیوائس ڈیٹا یا وائی-فائی کنیکشن دی پڑتال کرو۔ </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">کنکشن ٹائم آوٹ تھی ڳیا ہے</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>ارداس کیتی ڳئی سائٹ نے کنکشن دی ارداس دا جواب کائنی ݙتا تے براؤزر نے جواب دی تانگھ کرݨ بند کر ݙتی ہے۔</p>
+ <ul>
+ <li>تھی سڳدے جو سرور زیادہ مانگ یا عارضی وقفے دا سامھݨا کریندا پیا ہووے؟ ولدا بعد وچ کوشش کرو۔</li>
+ <li> پھلا تساں ݙوجھی سائٹ تے وی براؤز نہوے کر سڳدے پئے؟ ڈیوائس دے نیٹ ورک کنکشن دی جانچ کرو۔</li>
+ <li>بھلا تہاݙی ڈیوائس یا نیٹ ورک کہیں فائروال یا پراکسی دے ذریعے محفوظ ہے؟غلط ترتیباں ویب براؤز کرݨ وچ مداخلت کر سڳدی ہے۔</li>
+ <li>ہݨ وی مسئلہ درپیش ہے؟ مدد کیتے آپݨے نیٹ ورک کے نگران یا انٹرنیٹ مہیا کار نال رابطہ کرو۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">جڑݨ وچ ناکام ریہا</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>سائٹ عارضی طور تے غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد ولدا کوشش کرو۔</li>
+ <li>جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے، تاں آپݨی ڈیوائس دا ڈیٹا یا وائی-فائی کنکشن دی جانچ کرو۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">سرور ولوں غیر متوقع جواب</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>سائٹ نے نیٹ ورک ارداس دا غیر متوقع طریقے نال جواب ݙتا تے براؤزر جاری کائنی رہ سڳدا۔</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">ورقہ ٹھیک طرح ری ڈائریکٹ کائنی تھیندا پیا</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>براؤزر نے ارداس کیتے ڳئے آئٹم کوں واگزار کرواوݨ دی کوشش روک ݙتی ہے۔ سائٹ ایں ارداس کوں ایں طرحاں ٻئے پاسے بھیڄیندا پیا ہے جہڑی کݙاہیں وی مکمل کائناں تھیسی۔</p>
+ <ul>
+ <li>بھلا تساں سائٹ کیتے ضروری کوکیاں کوں معذور یا بلاک کر ݙتے؟</li>
+ <li>اگر سائٹ دیاں کوکیاں کوں قبول کرݨ نال وی مسئلہ ٹھیک نئیں تھیندا، تاں ممکن ہے جو ایہ سرور کنفگریشن دا کوئی مسئلہ ہووے نا کہ تہاݙی ڈیوائس دا۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">آف لائن موڈ</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>براؤزر آپݨے آف لائن موڈ وچ کم کریندا پئے تے ارداس تھئے آئٹم چیز نال کائنی جڑ سڳدا۔
+</p>
+ <ul>
+ <li>بھلا ڈیوائس فعال نیٹ ورک نال کنکٹ تھیا ہویا ہے؟</li>
+ <li>آن لائن موڈ وچ گھن آوݨ کیتے “ولدا کوشش کرو” دباؤ تے ورقے کوں ولدا لوڈ کرو۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">حفاظتی وجوہات کیتے پورٹ محدود کر ݙتا ڳیا ہے</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>ارداس کیتا پتہ ہک خاص پورٹ (مثلاً، mozilla.org تے پورٹ 80 کیتے <q>mozilla.org:80</q>)
+ کوں مخصوص کریندا ہے جہڑا عام طور تے ویب براؤز کرݨ دے علاوہ <em>کجھ ٻئے</em> مقاصد کیتے ورتیندے۔ تہاݙی حفاظت تے سلامتی کیتے براؤزر نے ارداس کوں منسوخ کر ݙتا ہے۔</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">کنکشن ریسٹ تھی ڳیا</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+<p>کنکشن قائم کرݨ دے دوران نیٹ ورک لنک وچ رکاوٹ پیدا تھئی۔ براہ کرم، ولدا کوشش کرو۔</p>
+ <ul>
+ <li>سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد وت کوشش کرو۔</li>
+ <li>جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے ، تاں آپݨاں ڈیوائس ڈیٹا یا وائی-فائی کنیکشن دی پڑتال کرو۔ </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">غیر محفوظ فائل قسم</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>ویب سائٹ دے مالکاں کوں ایہ مسلے ݙسݨ کیتے رابطہ کرو۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">خراب مواد نقص</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ڈیٹا ترسیل وچ ہک خرابی دا سراغ لڳے۔</p>
+ <ul>
+ <li>ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">مواد تباہ تھی ڳیا</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ڈیٹا ترسیل وچ ہک خرابی دا سراغ لڳے۔</p>
+ <ul>
+ <li>ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">مواد اینکوڈ کرݨ وچ خرابی</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ایہ ہک غلط یا بغیر سہارا تھئی کمپریشن ورتیندے۔</p>
+ <ul>
+ <li>ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">پتہ کائنی لبھا</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>ݙتے ہوئے پتے کیتے براؤزر ہوسٹ سرور کائنی لبھ سڳا۔</p>
+ <ul>
+ <li>پتے وچ ایں طرحاں دیاں ٹائپنگ غلطیاں دی پڑتال کرو
+ <strong>ww</strong>.example.com پر لکھݨا ہائی
+ <strong>www</strong>.example.com.</li>
+ <li>جے تساں کہیں وی ورقے کوں لوڈ نہوے کر سڳدے تاں ڈیوائس ڈیٹا یا وائی فائی کنکشن دی پڑتال کرو</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">کوئی انٹرنیٹ کنکشن کائنی</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">اپݨے نیٹ ورک کنکشن دی پڑتال کرو یا کجھ لمحے بعد ولدا کوشش کرو۔</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">ولدا لوڈ کرو</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">غلط پتہ</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>مہیا کیتا ڳیا پتہ سُن٘ڄاپُو فارمیٹ وچ کائنی۔ سوہݨا، غلطیاں کیتے لوکیشن پٹی دی پڑتال کرو تے ولدا کوشش کرو۔</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">پتہ ٹھیک کائنی</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>ویب پتے عمومی طور تے این٘ویں <strong>http://www.example.com/</strong> لکھے ویندے ہن </li>
+ <li>یقینی بݨاؤ جو تساں فارورڈ سلیشیز (جین٘ویں <strong>/</strong>) ورتندے پئے ہو۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">نامعلوم پروٹوکول</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>پتے وچ پروٹوکول (مثال دے طور تے <q>wxyz://</q>) جیکوں براؤز کائنی سُن٘ڄݨیندا، تہوں تاں براؤزر سائٹ کوں ٹھیک طرحاں کنکٹ کائنی کر سڳدا۔</p>
+ <ul>
+ <li>بھلا تساں ملٹی میڈیا تے ٻیاں غیرعبارت خدمتاں تے رسائی دی کوشش کریندے پئے ہو؟ ٻیاں ضروریات کیتے سائٹ دی پڑتال کرو۔</li>
+ <li>براؤزر دے سُن٘ڄاݨݨ کنوں پہلے کجھ پروٹوکولاں کوں تریجھی پارٹی سافٹ ویئر یا پلگ اناں دی لوڑ ہوندی ہے۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">فائل کائنی لبھی</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>بھلا آئٹم دا ناں وٹایا ون٘ڄ سڳدا ہائی، ہٹا ون٘ڄ سڳدا ہائی، یا ٻئی جاء تے منتقل کیتا ون٘ڄ سڳدا ہائی؟</li>
+ <li>بھلا پتے وچ ہجیاں، وݙے حروف یا ٹائپ دی کوئی ٻئی غلطی ہے؟</li>
+ <li>بھلا تہاݙے کول مطلوبہ آئٹم تائیں رسائی دی کافی اجازت ہے؟</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">فائل تائیں رسائی مسترد کر ݙتی ڳئی ہائی</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>ایہ ہٹا یا ٹور ݙتا ڳیا ہوسی یا فائل اجازتاں رسائی کائناں تھیوݨ ݙیندیاں پیاں ہوسن</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">پراکسی سرور کنکشن دا انکار کر ݙتے</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>براؤزر پراکسی سرر ورتݨ کیتے کنفیگر تھیا ہویا ہے، پر پراکسی نے کنکشن دا انکار کر ݙتے۔</p>
+ <ul>
+ <li>بھلا براؤزر دی پراکسی کنفیگریشن ٹھیک ہے؟ ترتیباں دی پڑتال کرو تے ولدا کوشش کرو۔</li>
+ <li>بھلا پراکسی سروس ایں نیٹ ورک کنوں اجازت ݙیندی ہے؟</li>
+ <li>اڄݨ وی مشکل وچ ہو؟ مدد کیتے نیٹ ورک ایڈمن یا انٹرنیٹ فراہم کرݨ آلے نال مشورہ کرو۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">پراکسی سرور کائنی لبھا</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>براؤزر پراکسی سرر ورتݨ کیتے کنفیگر تھیا ہویا ہے، پر پراکسی کائنی لبھ سڳی۔</p>
+ <ul>
+ <li>بھلا براؤزر دی پراکسی کنفیگریشن ٹھیک ہے؟ ترتیباں دی پڑتال کرو تے ولدا کوشش کرو۔</li>
+ <li>بھلا ڈیوائس فعال نیٹ ورک نال کنکٹ تھئی ہوئی ہے؟</li>
+ <li>اڄݨ وی مشکل وچ ہو؟ مدد کیتے نیٹ ورک ایڈمن یا انٹرنیٹ فراہم کرݨ آلے نال مشورہ کرو۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">مالویئر سائٹ مسئلہ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>%1$s تے موجود سائٹ کوں حملہ سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ناپسندیدہ سائٹ مسئلہ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s تے موجود سائٹ کوں ناپسندیدہ سافٹ ویئر خدمت دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">نقصان دہ سائٹ مسئلہ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s تے موجود سائٹ کوں ممکنہ نقصان دہ سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">فریبی سائٹ مسئلہ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s تے موجود ویب ورقے کوں فریبی سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">محفوظ سائٹ دستیاب کائنی</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[وادھوں حفاظت کیتے اساں ایچ ٹی ٹی پی ایس ــ صرف موڈ فعال کر ݙتے، تے <em>%1$s</em> دا ایچ ٹی ٹی پی ایس ورشن دستیاب کائنی۔]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">ایچ ٹی ٹی پی سائٹ تے جاری رکھو</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..c352228f3e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sl/strings.xml
@@ -0,0 +1,324 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Poskusi znova</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Zahteve ni mogoče izpolniti</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Trenutno ni na voljo dodatnih informacij o tej težavi ali napaki.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Varna povezava ni uspela</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Strani, ki si jo želite ogledati, ni mogoče prikazati, ker ni mogoče preveriti pristnosti sprejetih podatkov.</li>
+ <li>O napaki obvestite lastnike spletnega mesta.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Varna povezava ni uspela</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Težava se lahko nahaja v nastavitvah strežnika ali pa ga kdo poskuša oponašati.</li>
+ <li>Če ste se v preteklosti že uspešno povezali na ta strežnik, je napaka morda samo začasna, zato lahko ponovno poskusite kasneje.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Napredno …</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Nekdo lahko poskuša oponašati to spletno mesto, zato nadaljevanje ni priporočeno.</label>
+ <br><br>
+ <label>Spletna mesta svojo istovetnost dokazujejo z digitalnimi potrdili. %1$s ne zaupa spletnemu mestu <b>%2$s</b>, ker izdajatelj njegovega digitalnega potrdila ni znan, ker je potrdilo samopodpisano ali pa strežnik ne pošilja pravih vmesnih digitalnih potrdil.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Pojdi nazaj (priporočeno)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Sprejmi tveganje in nadaljuj</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">To spletno mesto zahteva varno povezavo.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Strani, ki si jo želite ogledati, ni mogoče prikazati, ker to spletno mesto zahteva varno povezavo.</li>
+ <li>Težava je najverjetneje na spletnem mestu, zato je sami ne morete odpraviti.</li>
+ <li>O težavi lahko tudi obvestite skrbnika spletnega mesta.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Napredno …</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> uporablja varnostni pravilnik, imenovan HTTP Strict Transport Security (HSTS), kar pomeni, da se lahko <b>%2$s</b> nanj poveže zgolj varno. Za obisk tega spletnega mesta ne morete dodati izjeme. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Nazaj</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Povezava je bila prekinjena</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Brskalnik se je uspešno povezal, vendar se je povezava prekinila med prenosom podatkov. Poskusite znova.</p>
+ <ul>
+ <li>Spletno mesto je morda začasno nedosegljivo ali preobremenjeno. Poskusite znova
+ nekoliko pozneje.</li>
+ <li>Če ne morete naložiti nobenega spletnega mesta, preverite nastavitve podatkovne povezave ali Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Povezava je potekla</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Zahtevana stran ni odgovorila na zahtevo po povezavi, brskalnik pa je prenehal čakati na odgovor.</p>
+ <ul>
+ <li>Če mislite, da bi strežnik lahko bil zelo obremenjen ali trenutno nedostopen, poskusite znova nekoliko kasneje.</li>
+ <li>Če se ne morete povezati na nobeno spletno mesto, preverite povezavo svoje naprave do interneta.</li>
+ <li>Če uporabljate posrednika ali požarni zid, se prepričajte, da so vaše nastavitve pravilne.</li>
+ <li>Če težava ne izgine, se posvetujte s svojim skrbnikom sistema ali s ponudnikom internetnih storitev.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Povezave ni mogoče vzpostaviti</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Spletno mesto je morda začasno nedosegljivo ali preobremenjeno. Poskusite znova
+ nekoliko pozneje.</li>
+ <li>Če ne morete naložiti nobene strani, preverite nastavitve podatkovne povezave ali Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Nepričakovan odgovor strežnika</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Spletno mesto je na mrežno zahtevo odgovorilo na nepričakovan način in brskalnik ne more nadaljevati.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Stran se ne preusmerja pravilno</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Brskalnik je prenehal s prenašanjem zahtevane strani. Spletno mesto preusmerja zahtevek tako, da se krog ne bi nikoli zaključil.</p>
+ <ul>
+ <li>Ste onemogočili ali prepovedali piškotke, ki bi jih to spletno mesto potrebovalo?</li>
+ <li>Če sprejetje piškotkov tega spletnega mesta ne odpravi težave, je to skoraj gotovo napaka nastavitev strežnika in nima zveze z vašo napravo.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Brez povezave</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Brskalnik trenutno deluje v načinu brez povezave in se zato ne more povezati na zahtevano stran.</p>
+ <ul>
+ <li>Je naprava povezana v delujoče omrežje?</li>
+ <li>Pritisnite “Poskusi znova” za preklop v povezan način in ponovno nalaganje strani.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Vrata nesprejemljiva iz varnostnih razlogov</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Zahtevan naslov določa, katera vrata naj se uporabijo (npr. <q>mozilla.org:80</q> pomeni vrata 80 na strežniku mozilla.org) za namene, ki <em>niso</em> brskanje po spletu. Brskalnik je preprečil to zahtevo zaradi varnostnih razlogov.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Povezava je bila ponastavljena</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Povezava s stranjo je bila nepričakovano prekinjena med pogajanjem za povezavo. Poskusite znova.</p>
+ <ul>
+ <li>Spletno mesto je morda začasno nedosegljivo ali preobremenjeno. Poskusite znova
+ nekoliko pozneje.</li>
+ <li>Če ne morete naložiti nobene strani, preverite nastavitve podatkovne povezave ali Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Nevarna vrsta datoteke</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>O napaki obvestite lastnike spletnega mesta.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Napaka zaradi pokvarjene vsebine</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Strani, ki si jo želite ogledati, ni mogoče prikazati, ker je bila zaznana napaka pri prenosu podatkov.</p>
+ <ul>
+ <li>O napaki obvestite lastnike spletnega mesta.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Vsebina se je sesula</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Strani, ki si jo želite ogledati, ni mogoče prikazati, ker je bila zaznana napaka pri prenosu podatkov.</p>
+ <ul>
+ <li>O napaki obvestite lastnike spletnega mesta.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Napaka pri kodiranju vsebine</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Strani, ki si jo želite ogledati, ni mogoče prikazati, ker uporablja neveljavno ali nepodprto obliko stiskanja.</p>
+ <ul>
+ <li>O napaki obvestite lastnike spletnega mesta.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Naslova ni mogoče najti</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Strani ni bilo mogoče najti.</p>
+ <ul>
+ <li>Morda ste se zmotili pri črkovanju naslova, npr.
+ <strong>ww</strong>.example.com namesto
+ <strong>www</strong>.example.com.</li>
+ <li>Če ne morete naložiti nobene strani, preverite nastavitve podatkovne povezave ali Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ni internetne povezave</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Preverite omrežno povezavo ali poskusite znova naložiti stran.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Ponovno naloži</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Neveljaven naslov</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Vneseni naslov ni v prepoznani obliki. Preverite, da v vrstici z naslovom ni napak, in poskusite znova.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Neveljaven naslov</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Spletni naslovi so običajno napisani v obliki <strong>http://www.example.com/</strong></li>
+ <li>Prepričajte se, da uporabljate poševnice naprej (tj. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Neznan protokol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Naslov navaja protokol (npr. <q>wxyz://</q>), ki ga brskalnik ne pozna, zato se ne more pravilno povezati s spletnim mestom.</p>
+ <ul>
+ <li>Želite uporabiti večpredstavnost ali drugo nebesedilno storitev? Preverite morebitne dodatne programske zahteve spletnega mesta.</li>
+ <li>Nekateri protokoli zahtevajo programsko opremo drugih izdelovalcev ali dodatke, preden jih lahko brskalnik uporablja.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Datoteke ni mogoče najti</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Morda je bila želena datoteka preimenovana, izbrisana ali prestavljena.</li>
+ <li>Je njeno ime napačno črkovano, napisano z nepravilnimi velikimi in malimi črkami ali kako drugače narobe napisano?</li>
+ <li>Imate ustrezna dovoljenja za dostop do želene datoteke?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Dostop do datoteke je bil zavrnjen</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Morda je bila odstranjena, premaknjena ali pa dovoljenja datoteke preprečujejo dostop.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Posrednik zavrnil povezavo</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Povezava z internetom je nastavljena prek posrednika, ki pa odklanja povezavo.</p>
+ <ul>
+ <li>Je nastavitev posrednika ustrezna? Preverite nastavitve in poskusite ponovno.</li>
+ <li>Ali ta posrednik dovoli povezave s te mreže?</li>
+ <li>Če težava ne izgine, se posvetujte s svojim skrbnikom sistema ali s ponudnikom internetnih storitev.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Posrednika ni mogoče najti</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Povezava z internetom je nastavljena prek posrednika, ki ga ni mogoče najti.</p>
+ <ul>
+ <li>Je nastavitev posrednika ustrezna? Preverite nastavitve in poskusite ponovno.</li>
+ <li>Je naprava povezana v delujoče omrežje?</li>
+ <li>Če težava ne izgine, se posvetujte s svojim skrbnikom sistema ali s ponudnikom internetnih storitev.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Težava zlonamerne strani</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Spletno mesto %1$s je bilo prijavljeno kot napadalno in je zaradi vaših varnostnih nastavitev zavrnjeno.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Težava neželene strani</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Spletno mesto %1$s je bilo prijavljeno, da razširja neželeno programsko opremo, in je zaradi vaših varnostnih nastavitev zavrnjeno.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Težava škodljive strani</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Spletno mesto %1$s je bilo prijavljeno kot morebitno škodljivo in je bilo zavrnjeno na podlagi vaših varnostnih nastavitev.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Težava zavajajoče strani</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Spletna stran na naslovu %1$s je bila prijavljena kot zavajajoča in je bila zavrnjena na podlagi vaših varnostnih nastavitev.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Varno spletno mesto ni na voljo</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Omogočili ste način "samo HTTPS" za izboljšano varnost in varna različica HTTPS strani <em>%1$s</em> ni na voljo.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Nadaljuj na stran HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..cbd5911762
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sq/strings.xml
@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Riprovoni</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Kërkesa Nuk Plotësohet Dot</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Hëpërhë s’ka hollësi shtesë të gatshme rreth këtij problemi apo gabimi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Dështoi Lidhja e Sigurt</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[ <ul>
+ <li>Faqja që po provoni të shihni s’shfaqet dot, ngaqë mirëfilltësia e të dhënave të marra s’u vërtetua dot.</li>
+ <li>Ju lutemi, lidhuni me të zotët e sajtit, për t’u bërë të ditur këtë problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Dështoi Lidhja e Sigurt</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[ <ul>
+ <li>Mund të jetë problem i formësimit të shërbyesit, ose mundet që dikush të jetë duke imituar shërbyesin.</li>
+ <li>Nëse në të kaluarën jeni lidhur me sukses te ky shërbyes, gabimi mund të jetë i përkohshëm dhe mund të riprovonit më vonë.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Të mëtejshme…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[ <label>Dikush mund të jetë duke provuar të imitojë sajtin dhe s’duhet të vazhdoni.</label>
+ <br><br>
+ <label>Sajtet dëshmojnë identitetin e tyre përmes dëshmish. %1$s nuk i zë besë <b>%2$s</b>, ngaqë lëshuesi i dëshmisë së tij është i panjohur, dëshmia është e vetënënshkruar, ose shërbyesi s’po dërgon dëshmi të ndërmjetme të sakta.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Kthehuni Mbrapsht (E këshilluar)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Pranoni Rrezikun dhe Vazhdoni</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ky sajt lyp një lidhje të siguruar.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Faqja që po rrekeni të shihni, s’mund të shfaqet, ngaqë ky sajt lyp një lidhje të siguruar.</li>
+ <li>Gjasat janë që problemi të jetë me sajtin dhe s’ka gjë që mund ta bëni për ta zgjidhur.</li>
+ <li>Mund të njoftoni përgjegjësin e sajtit për problemin.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Të mëtejshme…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label><b>%1$s</b> përmban një rregull sigurie të quajtur HTTP Strict Transport Security (HSTS), që do të thotë se <b>%2$s</b>-i mund të lidhet me të vetëm nën mënyrë të sigurt. S’shtoni dot një përjashtim që të vizitoni këtë sajt. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Kthehu Mbrapsht</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Lidhja u ndërpre</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[ <p>Shfletuesi u lidh me sukses, por lidhja u ndërpre teksa shpërnguleshin të dhëna. Ju lutemi, riprovoni.</p>
+ <ul>
+ <li>Sajti mund të jetë përkohësisht jashtë funksionimi, ose shumë i zënë. Riprovoni pas pak çastesh.</li>
+ <li>Nëse s’jeni në gjendje të ngarkoni çfarëdo faqe, kontrolloni lidhjen me rrjetin celular, ose Wi-Fi të pajisjes tuaj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Lidhjes i mbaroi koha</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Sajti i kërkuar s’iu përgjigj kërkesës për lidhje dhe shfletuesi reshti së prituri për përgjigje.</p>
+ <ul>
+ <li>Mundet që shërbyesi është duke u përballur me kërkesë të madhe, ose mundet të jetë përkohësisht jashtë loje? Riprovoni më vonë.</li>
+ <li>S’jeni në gjendje të shfletoni sajte të tjerë? Kontrolloni lidhjen në rrjet të pajisjes.</li>
+ <li>A është pajisja, ose rrjeti juaj i mbrojtur me një <em>firewall</em> apo ndërmjetës? Rregullime të pasakta mund të prekin shfletimin në Web.</li>
+ <li>Keni ende vështirësi? Për ndihmë, lidhuni me administruesin e rrjetit tuaj, ose me furnizuesin e Internetit.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">S’arrin të lidhet</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[ <ul>
+ <li>Sajti mund të jetë përkohësisht jashtë funksionimit ose shumë i zënë. Riprovoni pas pak çastesh.</li>
+ <li>Nëse s’jeni në gjendje të ngarkoni çfarëdo faqe, kontrolloni lidhjen me rrjetin celular, ose Wi-Fi të pajisjes tuaj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Përgjigje e papritur prej shërbyesit</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[ <p>Sajti iu përgjigj kërkesës së rrjetit në një mënyrë të papritur dhe shfletuesi s’vazhdon dot.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Faqja s’është ridrejtuar si duhet</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Shfletuesi ka reshtur së provuari të marrë objektin e kërkuar. Sajti po e ridrejton kërkesën në një mënyrë që s’do të plotësohet kurrë.</p>
+ <ul>
+ <li>I keni çaktivizuar apo bllokuar cookie-t e domosdoshme për këtë sajt?</li>
+ <li>Nëse pranimi i cookie-ve s’e zgjidh problemin, ka të ngjarë të jetë çështje formësimi shërbyesi dhe jo e pajisjes tuaj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mënyrë Jo Në Linjë</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Shfletuesi po punon nën mënyrën jashtë linje dhe s’lidhet dot te objekti i kërkuar.</p>
+ <ul>
+ <li>A është pajisja e lidhur te një rrjet aktiv?</li>
+ <li>Shtypni “Riprovoni” që të kalohet nën mënyrën në linjë dhe të ringarkohet faqja.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Portë e kufizuar për arsye sigurie</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[ <p>Adresa e kërkuar donte një portë (p.sh., <q>mozilla.org:80</q> për portën 80 te mozilla.org) normalisht e përdorur për qëllime <em>të tjera</em> dhe jo për shfletim Web. Shfletuesi e anuloi kërkesën me qëllim mbrojtjen dhe sigurinë tuaj.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Lidhja u rivendos</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Ndërlidhja me rrjetin u ndërpre teksa negociohej një lidhje. Ju lutemi, riprovoni.</p>
+ <ul>
+ <li>Sajti mund të jetë përkohësisht jashtë funksionimi, ose shumë i zënë. Riprovoni pas pak çastesh.</li>
+ <li>Nëse s’jeni në gjendje të ngarkoni çfarëdo faqe, kontrolloni lidhjen me rrjetin celular, ose Wi-Fi të pajisjes tuaj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Lloj Jo i Parrezik Kartelash</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Ju lutemi, lidhuni me të zotët e sajtit, për t’u bërë të ditur këtë problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Gabim nga Lëndë e Dëmtuar</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[ <p>Faqja që po rrekeni të shihni, s’mund të shfaqet, ngaqë u pikas një gabim në transmetimin e të dhënave.</p>
+ <ul>
+ <li>Ju lutemi, lidhuni me të zotët e sajtit që t’u njoftoni këtë problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Vithisje lënde</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[ <p>Faqja që po rrekeni të shihni, s’mund të shfaqet, ngaqë u pikas një gabim në transmetimin e të dhënave.</p>
+ <ul>
+ <li>Ju lutemi, lidhuni me të zotët e sajtit që t’u njoftoni këtë problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Gabim Kodimi Lënde</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[ <p>Faqja që po rrekeni të shihni s’shfaqet dot, sepse përdor një formë ngjeshjeje të mangët, ose të pambuluar.</p>
+ <ul>
+ <li>Ju lutemi, lidhuni me të zotët e sajtit për t’u njoftuar këtë problem.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">S’u Gjet Adresë</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Shfletuesi s’gjeti dot shërbyesin strehë për adresën e dhënë.</p>
+ <ul>
+ <li>Kontrolloni adresën për gabime shkrimi, bie fjala,
+ <strong>ww</strong>.example.com në vend të
+ <strong>www</strong>.example.com.</li>
+ <li>Nëse s’jeni në gjendje të ngarkoni çfarëdo faqe, kontrolloni lidhjen e pajisjes tuaj me rrjetin celular, ose atë Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Pa lidhje internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Kontrolloni lidhjen tuaj në rrjet, ose provoni të ringarkoni faqen pas pak çastesh.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Ringarkoje</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Adresë e Pavlefshme</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Adresa e dhënë nuk është sipas ndonjë formati të pranuar. Ju lutemi, kontrolloni për gabime te shtylla e vendeve dhe riprovoni.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adresa s’është e vlefshme</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Adresat Web zakonisht shkruhen si <strong>http://www.example.com/</strong></li>
+ <li>Sigurohuni se po përdorni pjerrake djathtas (d.m.th., <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protokoll i Panjohur</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adresa tregon një protokoll (p.sh., <q>wxyz://</q>) të cilin shfletuesi s’e njeh, kështu që s’mund të lidhet si duhet me këtë sajt.</p>
+ <ul>
+ <li>Mos po përpiqeni të hapni shërbime multimedia apo shërbim tjetër jo tekst? Shihni te sajti për domosdoshmëri ekstra.</li>
+ <li>Disa protokolle mund të lypin software ose shtojca prej palësh të treta, përpara se shfletuesi të mund t’i njohë.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">S’u Gjet Kartelë</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Mundet që objekti të jetë riemërtuar, hequr, ose shpërngulur gjetkë?</li>
+ <li>A ka ndonjë gabim shkrimi apo gabim tjetër tipografik te adresa?</li>
+ <li>A keni leje të mjafta hyrjeje për te objekti i kërkuar?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Hyrja te kartela u mohua</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Mund të jetë hequr, lëvizur, ose hyrjen e pengojnë lejet mbi kartelën.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Shërbyesi Ndërmjetës Hodhi Poshtë Kërkesën për Lidhje</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Shfletuesi është formësuar të përdorë shërbyes ndërmjetës, por ndërmjetësi s’pranoi lidhje.</p>
+ <ul>
+ <li>Është i saktë formësimi i shfletuesit për ndërmjetësin? Kontrolloni rregullimet dhe riprovoni.</li>
+ <li>A lejon shërbimi ndërmjetës lidhje prej këtij rrjeti?</li>
+ <li>Keni ende vështirësi? Për ndihmë, lidhuni me administruesin e rrjetit tuaj, ose me furnizuesin e Internetit.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">S’u Gjet Shërbyes Ndërmjetës</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Shfletuesi është formësuar të përdorë një shërbyes ndërmjetës, por ndërmjetësi s’u gjet dot.</p>
+ <ul>
+ <li>Është i saktë formësimi i shfletuesit për ndërmjetësin? Kontrolloni rregullimet dhe riprovoni.</li>
+ <li>A është e lidhur pajisja te një rrjet aktiv?</li>
+ <li>Keni ende vështirësi? Për ndihmë, lidhuni me administruesin e rrjetit tuaj, ose me furnizuesin e Internetit.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problem sajti malware-i</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Sajti te %1$s është raportuar si sajt sulmesh dhe është bllokuar bazuar në parapëlqimet tuaja për sigurinë.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problem sajti të padëshiruar</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Sajti te %1$s është raportuar se shërben software të padëshiruar dhe është bllokuar bazuar në parapëlqimet tuaja për sigurinë.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problem sajti të dëmshëm</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Sajti te %1$s është raportuar si sajt potencialisht i dëmshëm dhe është bllokuar bazuar në parapëlqimet tuaja për sigurinë.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problem sajti të rremë</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Kjo faqe web te %1$s është raportuar si sajt i rremë dhe është bllokuar bazuar në parapëlqimet tuaja për sigurinë.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">S’ka Sajt të Sigurt</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Keni aktivizuar Mënyrën Vetëm-HTTPS, për siguri të thelluar dhe s’ka version HTTPS për <em>%1$s</em>.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Vazhdo te Sajti HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..cb5da3cade
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sr/strings.xml
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Покушајте поново</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Не могу довршити захтев</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Додатне информације о овом проблему или грешци тренутно нису доступне.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Безбедна веза није успостављена</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Страница коју желите погледати не може се приказати јер се не може проверити веродостојност примљених података.</li>
+ <li>Контактирајте власнике веб странице и обавестите их о овом проблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Безбедна веза није успостављена</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Ова грешка је можда последица погрешно конфигурисаног сервера или неко покушава да се лажно представља за сервер.</li>
+ <li>Ако је веза са сервером у прошлости била успешна, проблем може бити привремен. Покушајте поново касније.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Напредно…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Неко покушава да се лажно представља за страницу и не препоручује се да наставите.</label>
+ <br><br>
+ <label>Веб странице потврђују свој идентитет путем сертификата. %1$s не верује <b>%2$s</b> јер ауторитет сертификата који користи је непознат, сертификат је самопотписан или сервер не шаље исправне посредничке сертификате.</label>]]></string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Врати се назад (препоручено)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Прихватите ризик и наставите</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Овај веб сајт захтева сигурну везу.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Страница коју покушавате отворити не може бити приказана јер она захтева безбедну везу.</li>
+ <li>Проблем се вероватно налази на самој страници и не можете га решити самостално.</li>
+ <li>Можете обавестити администратора веб странице о овом проблему.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Напредно…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> проводи сигурносну политику под називом HTTP Strict Transport Security (HSTS), што значи да <b>%2$s</b> може на исту повезати само сигурно. Не можете да додате изузетак за посете овој страници. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Иди назад</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Веза је прекинута</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Прегледач се успешно повезао, али је веза прекинута током преноса података. Покушајте поново касније.</p>
+ <ul>
+ <li>Страница је можда привремено недоступна или презаузета. Покушајте поново за неколико тренутака.</li>
+ <li>Ако не можете да учитате ниједну страницу, проверите мобилни пренос података или Wi-Fi везу.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Веза је истекла</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Захтевана страница није одговорила на захтев за повезивање и прегледач је зауставио чекање на одговор.</p>
+ <ul>
+ <li>Можда је сервер оптерећен великом количином захтева или је привремено остао без напајања? Покушајте поново касније.</li>
+ <li>Можете ли прегледати друге странице? Проверите мрежну везу вашег рачунара.</li>
+ <li>Да ли је ваш уређај или мрежа заштићена заштитним зидом или проксијем? Неисправна подешавања могу ометати ваше прегледање.</li>
+ <li>Још увек имате проблема? За помоћ се обратите администратору мреже или интернет добављачу.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Веза није успела</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Страница може бити привремено недоступна или заузета. Покушајте поново за неколико тренутака.</li>
+ <li>Ако не можете да учитате ниједну страницу, проверите мобилни пренос података или Wi-Fi везу.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Неочекивани одговор сервера</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Страница је неочекивано одговорила на захтев и прегледач не може да настави.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Страница се не преусмерава исправно</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Прегледач је престао да покушава да успостави тражену везу. Страница преусмерава упит на начин који никада неће бити довршен.</p>
+ <ul>
+ <li>Да ли сте онемогућили или блокирали колачиће који су потребни за ову страницу?</li>
+ <li>Ако прихватање колачића са ове странице не решава проблем, онда је проблем вероватно са конфигурацијом сервера, а не са вашим уређајем.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Офлајн режим</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Прегледач је у офлајн режиму и не може успоставити тражену везу.</p>
+ <ul>
+ <li>Да ли је уређај повезан на активну мрежу?</li>
+ <li>Притисните “Покушај поново” да бисте прешли на онлајн режим и поново учитали страницу.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Порт је ограничен из безбедносних разлога</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Тражена адреса је одредила порт (нпр. <q>mozilla.org:80</q> за порт 80 на mozilla.org) који се, поред за прегледање, обично користи у <em>друге</em> интернет сврхе. Прегледач је прекинуо упит из безбедносних разлога.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Веза је ресетована</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Мрежна веза прекинута је током преговора о вези. Покушајте поново.</p>
+ <ul>
+ <li>Страница може бити привремено недоступна или превише заузета. Покушајте поново за пар тренутака.</li>
+ <li>Ако не можете да учитате ниједну страницу, проверите мобилни пренос података или Wi-Fi везу.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Несигуран тип датотеке</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Обратите се власницима веб странице и обавестите их о овом проблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Грешка оштећеног садржаја</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Ова страница се не може приказати јер је дошло до грешке приликом преноса података.</p>
+ <ul>
+ <li>Обратите се власницима веб странице и обавестите их о овом проблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Садржај се срушио</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Затражена страница се не може приказати јер је дошло до грешке током преноса података.</p>
+ <ul>
+ <li>Обратите се власницима веб странице и обавестите их о овом проблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Грешка у кодирању садржаја</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Затражена страница се не може приказати јер користи неисправан или неподржан вид компресије.</p>
+ <ul>
+ <li>Обратите се власницима веб странице и обавестите их о овом проблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Адреса није пронађена</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Прегледач није могао пронаћи сервер за дату адресу.</p>
+ <ul>
+ <li>Пазите да немате грешке у куцању као што је
+ <strong>ww</strong>.example.com уместо
+ <strong>www</strong>.example.com.</li>
+ <li>Ако не можете да учитате ниједну страницу, проверите мобилни пренос података или Wi-Fi везу.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Нема интернет везе</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Проверите мрежну везу или пробајте поново учитати страницу за неколико тренутака.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Поново учитај</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Адреса није исправна</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Наведена адреса није у препознатљивом формату. Проверите да ли има грешака у адресној траци и покушајте поново.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Адреса није исправна</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Адресе веб страница се обично пишу као <strong>http://www.example.com/</strong></li>
+ <li>Проверите да ли користите косу црту (тј. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Непознат протокол</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Адреса означава протокол (нпр. <q>wxyz://</q>) који прегледач не препознаје и који не може повезати са траженом страницом.</p>
+ <ul>
+ <li>Да ли покушавате да приступите мултимедијалном или неким другим нетекстуланим услугама? Проверите има ли страница додатних захтева.</li>
+ <li>Неки протоколи захтевају софтвер или прикључке треће стране, како би их прегледач могао препознати.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Датотека није нађена</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Да није можда преименована, уклоњена или премештена?</li>
+ <li>Постоји ли у адреси везе правописна грешка или грешка при куцању?</li>
+ <li>Имате ли довољна овлашћења за приступ траженој ставци?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Приступ датотеци је одбијен</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Можда је уклоњена, премештена или су овлашћења датотеке таква да спречавају приступ.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Прокси сервер је одбио везу</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Прегледач је подешен да користи прокси сервер, али је он одбио везу.</p>
+ <ul>
+ <li>Да ли је прокси конфигурација прегледача исправна? Проверите подешавања и покушајте поново.</li>
+ <li>Да ли прокси услуга дозвољава повезивања из ове мреже?</li>
+ <li>Још увек имате проблема? За помоћ се обратите мрежном администратору или интернет добављачу.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Не могу да пронађем прокси сервер</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Прегледач је подешен да користи прокси сервер, али он није пронађен.</p>
+ <ul>
+ <li>Да ли је прокси конфигурација прегледача исправна? Проверите подешавања и покушајте поново.</li>
+ <li>Да ли је уређај повезан на активну мрежу?</li>
+ <li>Још увек имате проблема? За помоћ се обратите мрежном администратору или интернет добављачу.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Проблем са злонамерним софтвером</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Страница на %1$s је пријављена као злонамерна и блокирана је на основу ваших сигурносних подешавања.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Проблем са непожељном страницом</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Страница на %1$s је пријављена због послуживања непожељног софтвера и блокирана је на основу ваших сигурносних подешавања.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Проблем са штетном страницом</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Страница на %1$s је пријављена као потенцијално штетна и блокирана је на основу ваших сигурносних подешавања.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Проблем са обманљивом страницом</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Страница на %1$s је пријављена као обманљива и блокирана је на основу ваших сигурносних подешавања.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Безбедна страница није доступна</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Омогућили сте HTTPS режим за побољшану сигурност а HTTPS верзија за <em>%1$s</em> није доступна.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Настави на HTTP сајт</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..2620051760
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-su/strings.xml
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Pecakan Deui</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Rekés Gagal</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Can aya iber lianna ngeunaan ieu masalah atawa galat.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Sambungan Aman Gagal</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Kaca anu rék ditempo teu bisa ditémbongkeun alatan oténtisitas data anu katarima teu bisa dipuguhkeun.</li>
+ <li>Mangga kontak anu boga raramatloka pikeun ngiberan ieu masalah.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Sambungan Aman Gagal</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Sigana alatan masalah dina konpigurasi server, atawa aya nu nyobaan or it could be someone trying to impersonate the server.</li>
+ <li>If you have connected to this server successfully in the past, the error may be temporary, and you can try again later.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Leuwih lengkep…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Batur bisa nyobaan malsukeun lokana, mending ulah diteruskeun.</label>
+ <br><br>
+ <label>Raramatloka ngabuktikeun idéntitasna maké sértipikat. %1$s teu percaya ka <b>%2$s</b> alatan anu ngaluarkeun sértipikatna teu dipikawanoh, sértipikatna ditéken sorangan, atawa serperna teu ngirim sértipikat panengah anu bener.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Balik (Disarankeun)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Tampa Risiko jeung Teruskeun</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ieu raramatloka butuh sambungan anu aman.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Kaca anu rék dibuka teu bisa ditémbongkeun kusabab ieu raramatloka butuh sambungan anu aman.</li>
+ <li>Masalahna sigana di raramatlokana, jadi anjeun teu bisa kumaha.</li>
+ <li>Anjeun bisa ngiberan kuncén raramatlokana ngeunaan ieu masalah.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Leuwih lengkep…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> miboga kawijakan kaamanan anu katelah HTTP Strict Transport Security (HSTS), anu hartina <b>%2$s</b> ngan bisa nyambung ka dinya ku cara anu aman. Anjeun teu bisa ngiwalkeun pikeun muka ieu loka. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Balik Deui</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Sambunganana kapegat</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Panyungsi laksana nyambung, tapi sambunganana kapegat sabot nransferkeun émbaran. Mangga cobaan deui.</p>
+ <ul>
+ <li>Lokana sigana keur pareum samentawis atawa sibuk pisan. Cobaan sakeudeung deui.</li>
+ <li>Lamun anjeun teu bisa muka loka hiji hiji acan, pariksa sambungan data atawa Wi-Fina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Sambunganana béakeun waktu</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Loka anu dipénta henteu ngaréspon kana paménta sambungan sareng panyungsi parantos lirén ngantosan balesan.</p>
+ <ul>
+ <li>Bisa pangladén ngalaman paménta anu luhur atanapi pamiayaan samentawis? Cobian deui engké.</li>
+ <li>Naha anjeun teu tiasa ngotéktak situs sanés? Parios sambungan jaringan alat na.</li>
+ <li>Naha alat anjeun atanapi jaringan ditangtayungan ku firewall atanapi proksi? Setélan anu salah tiasa ngaganggu keur nyungsi Web.</li>
+ <li>Masih kénéh gangguan? Taroskeun ka admin jaringan anjeun atanapi panyadia Internét pikeun bantuan.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Teu bisa nyambung</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Lokana sigana keur teu bisa dibuka atawa keur riweuh. Pecakan sakeudeung deui.</li>
+ <li>Lamun teu bisa ngamuat kacana hiji hiji acan, pariksa data ponsél atawa sambungan Wi-Fi anjeun.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Tanggapan anu teu kaduga ti serper</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Loka éta ngabales panyambungan jaringan ku cara anu teu kaduga sareng panyungsi teu tiasa diteruskeun.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Kacana teu bener kaalihkeunna</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Pamaluruh eureun nyobaan mulut hal anu direkéskeun. Lokana ngarahkeun rekésna nepi ka moal anggeus-anggeus.</p>
+ <ul>
+ <li>Anjeun numpurkeun atawa meungpeuk réréméh anu dipénta ku ieu loka?</li>
+ <li>Lamun teu bisa bérés ku nampa réréméh loka, sigana masalahna dina konpigurasi serper sarta lain tina piranti anjeun.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Mode Oplén</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Panyungsi keur leumpang dina mode luring na jeung teu tiasa nyambung ka barang anu dipénta.</p>
+ <ul>
+ <li>Ieu parangkat nyambung ka jaringan aktip heunteu?</li>
+ <li>Pencét "Cobi deui" pikeun ngalih ka modeu daring sareng ngamuat deui ieu kaca.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port diwates pikeun kaamanan</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Alamat anu dipénta nyebutkeun port (contona <q>mozilla.org:80</q> pikeun port 80 di mozilla.org) galibna dipaké <em>lain</em> pikeun nyungsi Raramat. Panyungsi geus ngabolaykeun paménta pikeun kaamanan anjeun.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Sambunganana dirését</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Tutumbu jaringan kaganggu bari parundingan sambungan. Cobaan deui.</p>
+ <ul>
+ <li>Loka éta samentawis henteu kapayun atanapi sibuk teuing. Cobian deui dina sababaraha waktos.</li>
+ <li>Upami anjeun teu tiasa muka kaca, pariksa data paranti atanapi sambungan Wi-Fi anjeun.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipe Berkas Teu Aman</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Mangga kontak anu boga raramatloka pikeun ngiberkeun ieu masalah.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Galat Kontén Ruksak</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Kaca anu rék ditempo teu bisa ditémbongkeun alatan kadetéksi aya galat dina transmisi data.</p>
+ <ul>
+ <li>Mangga kontak anu miboga raramatloka pikeun ngiberan ieu masalah.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Kontén ruksak</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Kaca anu rék ditempo teu bisa ditémbongkeun alatan kadetéksi aya galat dina transmisi data.</p>
+ <ul>
+ <li>Mangga kontak anu miboga raramatloka pikeun ngiberan ieu masalah.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Galat pengkodean kontén</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Kaca anu rék ditempo teu bisa ditémbongkeun alatan maké bentuk komprési anu teu sah atawa teu didukung.</p>
+ <ul>
+ <li>Mangga kontak anu miboga raramatloka pikeun ngiberan ieu masalah.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Alamat Teu Kapanggih</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Panyungsi teu bisa manggihan serper host pikeun alamat nu disadiakeun.</p>
+ <ul>
+ <li>Pariksa alamatna bisi aya salah ketik kawas
+ <strong>ww</strong>.conto.com batan
+ <strong>www</strong>.conto.com.</li>
+ <li>Lamun teu hiji hiji acan anu bisa dibuka, pariksa data perangkat atawa sambungan Wi-Fina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Euweuh sambungan internét</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Pariksa sambungan jaringan anjeun atawa cobaan muat deui kacana engké.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Muat ulang</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Alamat Teu Bener</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Alamat anu diasupkeun formatna teu dpikawanoh. Pariksa palang lokasi bisi aya nu salah, laju cobaan deui.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Alamatna teu bener</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Alamat raramat ilaharna ditulis siga <strong>http://www.conto.com/</strong></li>
+ <li>Pastikeun anjeunn maké gurat condong maju (i.e. <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protokol Teu Dipikawanoh</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Alamatna méré protocol (e.g., <q>wxyz://</q>) anu teu dipikawanoh ku pamaluruh, ku kituna pamaluruhna teu bisa nyambung kalawan bener ka lokana.</p>
+ <ul>
+ <li>Badé muka multimédia atawa layanan non-téks lianna? Pariksa lokana pikeun kalengkepanana.</li>
+ <li>Sababaraha protokol butuh sopwér atawa plugin pihak katilu sangkan bisa dipikawanoh ku pamaluruhna.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Berkas Teu Kapanggih</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Boa itemna geus ganti ngaran, dipiceun, atawa dipindahkeun?</li>
+ <li>Aya salah ketik, kapitalisasi, atawa galat tipograpik dina alamatna?</li>
+ <li>Naha anjeun boga idin aksés nu cukup kana item anu dipundut?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Aksés ka berkas ditolak</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Bisa jadi kulantaran geus dipupus, dipindahkeun, atawa ayana idin berkas anu matak nyaram aksés.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Server proxy Nolak Sambungan</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Pamaluruhna disetél maké serper proksi, tapi proksina nolak sambungan.</p>
+ <ul>
+ <li>Konfigurasi proksi pamaluruhna geus bener? Pariksa setélanana sarta cobaan deui.</li>
+ <li>Layanan proksina ngidinan sambungan ti ieu jaringan?</li>
+ <li>Can bener kénéh? Cobi pundut bantuan ka administrator jaringan atawa panyadia internétna.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Server Proksi Teu Kapanggih</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Pamaluruhna disetél maké serper proksi, tapi proksina teu kapanggih.</p>
+ <ul>
+ <li>Konpigurasi proksi pamaluruhna geus bener? Pariksa setélanana sarta cobaan deui.</li>
+ <li>Perangkatna nyambung ka jaringan aktip?</li>
+ <li>Can bener kénéh? Cobi pundut bantuan ka administrator jaringan atawa panyadia internétna.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Isu loka malwér</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Lokana di %1$s dilaporkeun salaku loka gangas sarta geus dipeungpeuk dumasar kana préperénsi kaamanan anjeun.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Isu loka teu dipiharep</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Lokana di %1$s dilaporkeun nginangan sopwér anu teu dipiharep sarta geus dipeungpeuk dumasar kana préperénsi kaamanan anjeun.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Isu loka pibahyaeun</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Lokana di %1$s dilaporkeun salaku loka pibahyaeun sarta geus dipeungpeuk dumasar kana préperénsi kaamanan anjeun.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Masalah loka nu nipu</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Kaca web ieu di %1$s dilaporkeun salaku loka nu nipu jeung tos diblokir dumasar kana pilihan kahoyong kaamanan.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Situs Aman Teu Sayaga</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Anjeun ngahurungkeun Mode Ukur-HTTPS pikeun kaamanan lanjutan, anapon pérsi HTTPS <em>%1$s</em> teu sayaga.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Teruskeun ka Situs HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..9a0bf69388
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,314 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Försök igen</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Begäran kan inte slutföras</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Ytterligare information om detta problem eller fel är för närvarande inte tillgänglig.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Säker anslutning misslyckades</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Sidan du försöker visa kan inte visas eftersom autenticiteten för de mottagna uppgifterna inte kunde verifieras.</li>
+ <li>Kontakta webbplatsägarna för att informera dem om detta problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Säker anslutning misslyckades</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Detta kan vara ett problem med serverns konfiguration eller det kan vara någon som försöker efterapa servern.</li>
+ <li>Om du har anslutit till denna server framgångsrikt kan felet vara tillfälligt och du kan försöka igen senare.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avancerat…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Någon kan försöka efterlikna webbplatsen och du bör inte fortsätta.</label>
+ <br><br>
+ <label>Webbplatser bevisar sin identitet via certifikat. %1$s litar inte på <b>%2$s</b> eftersom certifikatutgivaren är okänd, certifikatet är självsignerat eller servern skickar inte rätt mellanliggande certifikat.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Gå tillbaka (rekommenderas)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Acceptera risken och fortsätt</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Denna webbplats kräver en säker anslutning.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Sidan du försöker visa kan inte visas eftersom den här webbplatsen kräver en säker anslutning.</li>
+ <li>Problemet ligger troligen på webbplatsen och det finns inget du kan göra för att lösa det.</li>
+ <li>Du kan meddela webbplatsens administratör om problemet.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Avancerat…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> har en säkerhetspolicy som kallas HTTP Strict Transport Security (HSTS), vilket innebär att <b>%2$s</b> bara kan ansluta till den på ett säkert sätt. Du kan inte lägga till ett undantag för att besöka den här webbplatsen.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Gå tillbaka</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Anslutningen avbröts</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Webbläsaren anslöt framgångsrikt, men anslutningen avbröts vid överföring av information. Försök igen.</p>
+ <ul>
+ <li>Webbplatsen kan vara tillfälligt otillgänglig eller för upptagen. Försök igen om några minuter.</li>
+ <li>Om du inte kan ladda några sidor kan du kontrollera enhetens data eller Wi-Fi-anslutning.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Anslutningens tidsgräns överskreds </string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Den begärda webbplatsen svarade inte på en anslutningsbegäran och webbläsaren har slutat vänta på ett svar.</p>
+ <ul>
+ <li>Kan servern ha hög efterfrågan eller tillfälligt avbrott? Försök igen senare.</li>
+ <li>Kan du inte surfa på andra webbplatser? Kontrollera enhetens nätverksanslutning.</li>
+ <li>Är din enhet eller nätverk skyddad av en brandvägg eller proxy? Felaktiga inställningar kan påverka surfningen.</li>
+ <li>Har du fortfarande problem? Kontakta nätverksadministratören eller internetleverantören för hjälp.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Kan inte ansluta</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Webbplatsen kan vara tillfälligt otillgänglig eller för upptagen. Försök igen om några minuter.</li>
+ <li>Om du inte kan ladda några sidor kan du kontrollera enhetens data eller Wi-Fi-anslutning.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Oväntat svar från servern</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Webbplatsen svarade på nätverksbegäran på ett oväntat sätt och webbläsaren kan inte fortsätta.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Sidan dirigeras om felaktigt</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Webbläsaren har slutat försöka hämta det begärda objektet. Webbplatsen omdirigerar begäran på ett sätt som aldrig kommer att slutföras.</p>
+ <ul>
+ <li>Har du inaktiverat eller blockerat kakor som krävs av denna webbplats?</li>
+ <li>Om att acceptera webbplatsens kakor inte löser problemet, är det troligtvis ett konfigurationsproblem i servern och inte din enhet.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offlineläge</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Webbläsaren arbetar i sitt offlineläge och kan inte ansluta till det begärda objektet.</p>
+ <ul>
+ <li>Är datorn ansluten till ett aktivt nätverk?</li>
+ <li> Tryck på "Försök igen" för att växla till online-läge och ladda om sidan igen.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porten har säkerhetsrestriktioner</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Den begärda adressen specificerade en port (t.ex. <q>mozilla.org:80</q> för port 80 på mozilla.org) som normalt används för <em>andra</em> syften än surfning. Webbläsaren har avbrutit begäran för skydd och säkerhet.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Anslutningen återställdes</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Nätverkslänken avbröts under förhandlingar om en anslutning. Försök igen.</p>
+ <ul>
+ <li>Webbplatsen kan vara tillfälligt otillgänglig eller för upptagen. Försök igen om några minuter.</li>
+ <li>Om du inte kan ladda några sidor kan du kontrollera enhetens data eller Wi-Fi-anslutning.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Osäker filtyp</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Kontakta webbplatsägarna för att informera dem om detta problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Korrupt innehållsfel</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Sidan du försöker visa kan inte visas eftersom ett fel i dataöverföringen upptäcktes.</p>
+ <ul>
+ <li>Kontakta webbplatsägarna för att informera dem om detta problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Innehållet kraschade</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Sidan du försöker visa kan inte visas eftersom ett fel i dataöverföringen upptäcktes.</p>
+ <ul>
+ <li>Kontakta webbplatsägarna för att informera dem om detta problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Kodningsfel av innehållet</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Sidan du försöker visa kan inte visas eftersom den använder en ogiltig eller icke-stödd form av komprimering.</p>
+ <ul>
+ <li>Kontakta webbplatsägarna för att informera dem om detta problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adressen hittades inte</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Webbläsaren kunde inte hitta värdservern för den angivna adressen.</p>
+ <ul>
+ <li>Kontrollera adressen för skrivfel som t.ex.
+ <strong>ww</strong>.example.com istället för
+ <strong>www</strong>.example.com.</li>
+ <li>Om du inte kan ladda några sidor kan du kontrollera enhetens data eller Wi-Fi-anslutning.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Ingen internetanslutning</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Kontrollera din nätverksanslutning eller försök ladda om sidan om en stund.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Ladda om</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Ogiltig adress</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Den angivna adressen är inte i ett erkänt format. Kontrollera adressfältet för fel och försök igen.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adressen är inte giltig</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Webbadresser är vanligtvis skrivna som <strong> http://www.example.com/</strong></li>
+ <li>Se till att du använder framåtriktade snedstreck (dvs. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Okänt protokoll</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Adressen anger ett protokoll (t.ex. <q>wxyz://</q>) webbläsaren inte känner igen, så webbläsaren kan inte ansluta till webbplatsen korrekt. </p>
+ <ul>
+ <li>Försöker du få tillgång till multimedia eller andra icke-texttjänster? Kontrollera webbplatsen för extra krav.</li>
+ <li>Vissa protokoll kan kräva programvara eller plugins från tredje part innan webbläsaren kan känna igen dem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Filen hittades inte</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Kan objektet ha bytt namn, tagits bort eller flyttat?</li>
+ <li>Finns det stavfel, stor bokstav eller annat typografiskt fel i adressen?</li>
+ <li>Har du tillräckliga åtkomstbehörigheter till den begärda objektet?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Åtkomst till filen nekades</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Den kan ha tagits bort, flyttats eller filbehörigheter kan förhindra åtkomst.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxyservern avslog anslutningen</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Webbläsaren är konfigurerad för att använda en proxyserver, men proxyn vägrade en anslutning.</p>
+ <ul>
+ <li>Är webbläsarens proxykonfiguration korrekt? Kontrollera inställningarna och försök igen.</li>
+ <li>Tillåter proxytjänsten anslutningar från det här nätverket?</li>
+ <li>Har du fortfarande problem? Kontakta nätverksadministratören eller internetleverantören för hjälp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxyservern hittades inte</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Webbläsaren är konfigurerad för att använda en proxyserver, men proxyn kunde inte hittas.</p>
+ <ul>
+ <li>Är webbläsarens proxykonfiguration korrekt? Kontrollera inställningarna och försök igen.</li>
+ <li>Är datorn ansluten till ett aktivt nätverk?</li>
+ <li>Har du fortfarande problem? Kontakta nätverksadministratören eller internetleverantören för hjälp.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problem med skadlig kod</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Webbplatsen %1$s har rapporterats som en attackplats och har blockerats baserat på dina säkerhetsinställningar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problem med oönskad webbplats</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Webbplatsen %1$s har rapporterats betjäna oönskad programvara och har blockerats baserat på dina säkerhetsinställningar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problem med skadlig webbplats</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Webbplatsen %1$s har rapporterats som en potentiellt skadlig webbplats och har blockerats baserat på dina säkerhetsinställningar.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problem med vilseledande webbplats</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Denna webbsida på %1$s har rapporterats som en vilseledande webbplats och har blockerats baserat på dina säkerhetsinställningar.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Säker webbplats finns inte tillgänglig</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Du har aktiverat endast HTTPS-läge för förbättrad säkerhet och en HTTPS-version av <em>%1$s</em> finns inte tillgänglig.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Fortsätt till HTTP-webbplats</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..56502063db
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ta/strings.xml
@@ -0,0 +1,282 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">மீண்டும் முயற்சி செய்</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">கோரிக்கையை முடிக்க முடியவில்லை</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>இச்சிக்கல் அல்லது பிழைகுறித்து கூடுதல் விவரங்கள் தற்போது இல்லை.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">பாதுகாப்பான இணைப்பு முறிந்தது</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>பெற்ற தரவின் நம்பகத்தன்மையை சரிபார்க்க முடியவில்லை, ஏனெனில் நீங்கள் காண முயற்சிக்கும் பக்கம் பார்க்க முடியாது.</li>
+ <li>இந்தப் பிரச்சனைகுறித்து இணையதளத்தில் உரிமையாளர்களை தொடர்பு கொள்க.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">பாதுகாப்பான இணைப்பு முறிந்தது</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>இந்தச் சேவையகத்தின் கட்டமைப்பில் சிக்கல் இருக்க வேண்டும், அல்லது அதில் யாராவது ஆள்மாறாட்டம் செய்ய முயற்சி செய்திருப்பார்கள்.</li>
+ <li>நீங்கள் இதற்கு முன்பு வெற்றிகரமாக இந்தச் சேவையகத்துடன் இணைந்து இருந்தால், இந்தப் பிழை தற்காலிகமானதாக இருக்கும், மேலும் நீங்கள் பிறகு மீண்டும் முயற்சி செய்யலாம்.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">மேம்பட்டது…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>தளத்தை ஆள்மாறாட்டம் செய்ய யாரோ ஒருவர் முயற்சிக்கலாம் எனவே நீங்கள் தொடர வேண்டாம்.</label>
+ <br><br>
+ <label>வலைத்தளங்கள் சான்றிதழ்கள் வழியாகத் தங்கள் அடையாளத்தை நிரூபிக்கின்றன. அறியப்படா சான்றிதழ் வழங்குநர், சான்றிதழ் சுய ஒப்பமிடல், அல்லது சேவையகம் சரியான இடைநிலைச் சான்றிதழ்களை அனுப்பாதது போன்ற காரணங்களால் %1$s <b>%2$s</b> வலைத்தளத்தை நம்பவில்லை.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">பின் செல்க (பரிந்துரைக்கப்படுகிறது)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">இடரை ஏற்றுத் தொடருங்கள்</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">இந்த இணையத் தளத்திற்குப் பாதுகாப்பான இணைப்பு தேவை.</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">மேம்பட்டது…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">பின் செல்க</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">இணைப்பில் தடங்கல் உள்ளது</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>உலாவி வெற்றிகரமாக இணைக்கப்பட்டது, ஆனால் தகவலை மாற்றும்போது இணைப்பு தடைப்பட்டது. மீண்டும் முயற்சிக்கவும்.</p>
+ <ul>
+ <li>இந்தத் தளம் தற்காலிகமாகக் கிடைக்கவில்லை அல்லது மிகவும் பிஸியாக இருக்கலாம். சிறிது நேரத்திற்க்கு பின் மீண்டும் முயற்சிக்கவும்.</li>
+ <li>உங்களால் எந்தப் பக்கங்களையும் ஏற்ற முடியவில்லை என்றால், உங்கள் சாதனத்தின் தரவு அல்லது வைஃபை இணைப்பைச் சரிபார்க்கவும்.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">இணைப்பு நேரம் முடிந்தது</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>கோரிய தளம் வேண்டுகோளுக்கு பதிலளிக்கவில்லை மேலும் உலாவி காத்திருத்தலை நிறுத்தியது.</p><ul><li>வழங்கி பற்றாக்குறை அல்லது தற்காலிக செயலிழப்பில் இருக்கலாம்? பின்னர் முயற்சிக்க.</li><li>பிற தளங்களில் உலாவ முடியவில்லையா? கணினி இணைப்பை சோதிக்க.</li><li>உங்கள் கணினி பிணையம் தீயரண் அல்லது பதிலாளால் பாதுகாக்கப்பட்டுள்ளதா? தவறான அமைப்புகள் வலை உலாவலில் இடைபுகலாம்.</li><li>இன்னும் சிக்கல் உள்ளதா? உதவிக்கு பிணைய நிர்வாகி அல்லது இணைய வழங்குநரை ஆலோசிக்கவும்.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">இணைக்க இயலவில்லை</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>இத்தளம் தற்சமயம் கிடைக்காமலோ சேவகன் மிகவும் மும்முரமாகவோ இருக்கலாம். சிறிது நேரம் கழித்து முயற்சிக்கவும்.</li>
+ <li>எந்த வலைதளத்தையும் அணுக முடியாவிட்டால், கைபேசியில் அல்லது அருகலை இணையம் உள்ளதா என உறுதிசெய்யவும்.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">சேவையகத்திலிருந்து எதிர்பாராத பதில் வந்துள்ளது</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>இந்த வலைத்தளம் வலைப்பின்னலின் கோரிக்கைக்கு எதிர்பாரா வழியில் பதிலளித்தது எனவே உலாவியால் தொடர இயலாது.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">பக்கம் ஒழுங்காகத் திருப்பிவிடவில்லை</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>உலாவி கோரப்பட்ட உருப்படியைப் பெறுவதற்கான முயற்சியை நிறுத்தியது. தளம் நிறைவடையாத வழியில் கோரிக்கையைத் திருப்பி விடுகிறது.</p>
+ <ul>
+ <li>நீங்கள் இத்தளத்திற்குத் தேவையான நினைவிகளைச் செயல்நீக்கி அல்லது முடக்கியுள்ளீர்களா?</li>
+ <li>தளத்தின் நினைவிகளை ஏற்பது பிரச்சினையைச் சரிசெய்யவில்லையெனில், இது சேவையகக் கட்டமைப்புப் பிழையாக இருக்க வாய்ப்புள்ளது, உங்கள் சாதனத்தில் பிரச்சனையில்லை.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">இணையமற்ற முறை</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>உலாவி இணையமிலா முறைமையில் செயல்படுகிறது கோரிய உருப்படியுடன் இணைக்க இயலாது.</p>
+ <ul>
+ <li>சாதனம் செயல்படும் இணைய வலைப்பின்னலுடன் இணைக்கப்பட்டுள்ளதா?</li>
+ <li>பக்கத்தை மீளேற்றி இணைய முறைமைக்கு மாற “மீண்டும் முயற்சிக்க” என்பதை அழுத்துங்கள்.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">பாதுகாப்புக் காரணங்களுக்காக முனையம் தடைசெய்யப்பட்டுள்ளது</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>கோரப்பட்ட முகவரியானது பொதுவாக வலை உலாவல் <em>அல்லாத</em> பிற நோக்கங்களுக்காகப் பயன்படுத்தப்படும் முனையத்தைக் (எ.கா., <q>mozilla.org:80</q> mozilla.org இன் முனையம் 80 காக) குறிப்பிட்டது. உங்கள் பாதுகாப்பிற்காக உலாவி கோரிக்கையை இரத்து செய்தது.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">இணைப்பு மீட்டமைக்கப்பட்டது</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>இணைக்க முயற்சிக்கும்போது வலைப்பின்னல் இணைப்பு தடைபட்டது. மீண்டும் முயற்சியுங்கள்.</p>
+ <ul>
+ <li>தளம் தற்காலிகமாகக் கிடைக்கவில்லை அல்லது பளுவான வேலையில் உள்ளது. சிறிது நேரத்திற்குப் பின் முயற்சியுங்கள்.</li>
+ <li>உங்கள் எந்தப் பக்கத்தையும் ஏற்ற முடியவில்லையெனில், உங்கள் சாதனத்தின் தரவு அல்லது அருகலை இணைப்பைச் சரிபாருங்கள்.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">பாதுகாப்பற்ற கோப்பு வகை</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>இணைய உரிமையாளர்களைத் தொடர்பு கொண்டு இந்தப் பிரச்சனைகுறித்து தெரிவிக்கவும்.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">சிதைந்த உள்ளடக்கப் பிழை</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>தரவுப் பரிமாற்றத்தில் ஒரு பிழை கண்டறியப்பட்டுள்ளதால், நீங்கள் காண முயற்சிக்கும் பக்கத்தைக் காண்பிக்க முடியாது.</p>
+ <ul>
+ <li>இணைய உரிமையாளர்களைத் தொடர்பு கொண்டு இந்தப் பிரச்சனைகுறித்து தெரிவிக்கவும்.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">உள்ளடக்கம் செயலிழந்தது</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>தரவுப் பரிமாற்றத்தில் ஒரு பிழை கண்டறியப்பட்டுள்ளதால், நீங்கள் காண முயற்சிக்கும் பக்கத்தைக் காண்பிக்க முடியாது.</p>
+ <ul>
+ <li>இணைய உரிமையாளர்களைத் தொடர்பு கொண்டு இந்தப் பிரச்சனைகுறித்து தெரிவிக்கவும்.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">உள்ளடக்க குறிமுறை பிழை</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>நீங்கள் காண முயற்சிக்கும் பக்கம் பார்க்க முடியாது ஏனெனில் அதைச் சுருக்க ஒரு தவறான அல்லது ஆதரவற்ற வடிவம் பயன்படுத்துகிறது.</p>
+ <ul>
+ <li>இணைய உரிமையாளர்களைத் தொடர்பு கொண்டு இந்தப் பிரச்சனைகுறித்து தெரிவிக்கவும்.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">முகவரி கிடைக்கவில்லை</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>உலாவியால் கொடுக்கப்பட்ட முகவரியின் வழங்கல் சேவையகத்தைக் கண்டறிய இயலவில்லை.</p>
+ <ul>
+ <li>முகவரியில் தட்டச்சுப் பிழைகள் உள்ளனவா என சரிபாருங்கள் எ.கா
+ <strong>ww</strong>.example.com என்பதற்குப் பதிலாக
+ <strong>www</strong>.example.com.</li>
+ <li>உங்கள் எந்தப் பக்கங்களையும் ஏற்ற இயலவில்லையெனில், உங்கள் சாதனத்தின் தரவு அல்லது அருகலை இணைப்பைச் சரிபாருங்கள்.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">இணையத் தொடர்பு இல்லை</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">உங்கள் இணைய இணைப்பைச் சரிபாருங்கள் அல்லது சிறிதுநேரத்திற்குப் பின் பக்கத்தை மறுஏற்ற முயற்சியுங்கள்.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">மீளேற்று</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">செல்லாத முகவரி</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>வழங்கிய முகவரி அடையாளங்காணத்தக்க வடிவில் இல்லை. இருப்பிடப்பட்டியைப் பார்த்து ஏதேனும் பிழைகள் உள்ளதா எனச் சரிபார்த்துவிட்டு மீண்டும் முயற்சிக்கவும்.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">இந்த முகவரி தவறானது</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>இணைய முகவரி <strong>http://www.example.com/</strong> போன்று இருக்கும்</li>
+ <li>நீங்கள் / குறியைப் பயன்படுத்தினீர்களா எனப் பார்க்கவும் (i.e. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">நெறிமுறை தெரியவில்லை</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>முகவரி உலாவியால் புரிந்துகொள்ள முடியாத நெறிமுறையைக் (எ.கா, <q>wxyz://</q>) குறிப்பிடுகிறது, எனவே உலாவியால் தளத்துடன் சரியாக இணைக்க முடியவில்லை.</p>
+ <ul>
+ <li>நீங்கள் பல்லூடகம் அல்லது பிற உரையல்லாச் சேவைகளை அணுக முயற்சிக்கிறீர்களா? கூடுதல் தேவைகளுள்ளதா எனத் தளத்தைப் பாருங்கள்.</li>
+ <li>உலாவியால் அடையாளம் காணப்படுவதற்குச் சில நெறிமுறைகளுக்கு மூன்றாம் தரப்பு மென்பொருள் அல்லது செருகுநிரல்கள் தேவைப்படலாம்.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">கோப்பைக் காணவில்லை</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>உருப்படி பெயர்மாற்ற, நீக்க, இடம்மாற்றப்பட்டிருக்கலாமா?</li>
+ <li>முகவரியில் உச்சரிப்பு, எழுத்துப்பிழை, அல்லது பிற தட்டச்சுப் பிழைகள் உள்ளனவா?</li>
+ <li>கோரப்பட்ட உருப்படிக்கான போதுமான அணுகல் அனுமதிகள் உங்களிடம் உள்ளனவா?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">கோப்பு அணுகல் மறுக்கப்பட்டது</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>கோப்பு நீக்கப்பட்டிருக்கலாம், நகர்த்தப்பட்டிருக்கலாம் அல்லது அனுமதி மறுக்கப்பட்டிருக்கலாம்.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">பதிலாளி சேவையகம் இணைப்பை மறுத்துவிட்டது</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>உலாவி பதிலாள் சேவையகத்தைப் பயன்படுத்துமாறு அமைவாக்கம் செய்யப்பட்டுள்ளது, ஆனால் பதிலாள் இணைப்பை மறுத்தது.</p>
+ <ul>
+ <li>உலாவியின் பதிலாள் அமைவுகள் சரியாக உள்ளனவா? அமைவுகளைச் சரிபார்த்து மீண்டும் முயற்சியுங்கள்.</li>
+ <li>பதிலாள் சேவைகள் இந்த வலைப்பின்னலிலிருந்து வரும் இணைப்புகளை அனுமதிக்கின்றனவா?</li>
+ <li>இன்னும் சிக்கல் உள்ளதா? உதவிக்கு உங்கள் வலைப்பின்னல் நிர்வாகி அல்லது இணைய சேவை வழங்குநரைத் தொடர்பு கொள்ளுங்கள்.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">பதிலாளி சேவையகம் கிடைக்கவில்லை</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>உலாவி பதிலாள் சேவையகத்தைப் பயன்படுத்துமாறு அமைவாக்கம் செய்யப்பட்டுள்ளது, ஆனால் பதிலாளைக் கண்டுபிடிக்க இயலவில்லை.</p>
+ <ul>
+ <li>உலாவியின் பதிலாள் அமைவுகள் சரியாக உள்ளனவா? அமைவுகளைச் சரிபார்த்து மீண்டும் முயற்சியுங்கள்.</li>
+ <li>இச்சாதனம் செயல்பாட்டிலுள்ள வலைப்பின்னலுடன் இணைக்கப்பட்டுள்ளதா?</li>
+ <li>இன்னும் சிக்கல் உள்ளதா? உதவிக்கு உங்கள் வலைப்பின்னல் நிர்வாகி அல்லது இணைய சேவை வழங்குநரைத் தொடர்பு கொள்ளுங்கள்.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">தீம்பொருள் தள சிக்கல்</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+      <p>%1$s இல் உள்ள தளம் தாக்குதல் தளமாக புகாரளிக்கப்பட்டுள்ளது மற்றும் உங்கள் பாதுகாப்பு விருப்பங்களின் அடிப்படையில் தடுக்கப்பட்டுள்ளது.</p>
+    ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">தேவையற்ற தள சிக்கல்</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+      <p>%1$s இல் உள்ள தளம் தேவையற்ற மென்பொருளை வழங்குவதாக அறிவிக்கப்பட்டுள்ளது மற்றும் உங்கள் பாதுகாப்பு விருப்பங்களின் அடிப்படையில் தடுக்கப்பட்டுள்ளது.</p>
+    ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">தீங்கு விளைவிக்கும் தள சிக்கல்</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+      <p>%1$s இல் உள்ள தளம் தீங்கு விளைவிக்கும் தளமாக புகாரளிக்கப்பட்டுள்ளது மற்றும் உங்கள் பாதுகாப்பு விருப்பங்களின் அடிப்படையில் தடுக்கப்பட்டுள்ளது.</p>
+    ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">ஏமாற்றும் தள சிக்கல்</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+      <p>%1$s இல் உள்ள இந்த வலைப்பக்கம் ஒரு ஏமாற்றும் தளமாக புகாரளிக்கப்பட்டுள்ளது மற்றும் உங்கள் பாதுகாப்பு விருப்பங்களின் அடிப்படையில் தடுக்கப்பட்டுள்ளது.</p>
+    ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">பாதுகாப்பான தளம் கிடைக்கப்பெறவில்லை</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[மேம்படுத்தப்பட்ட பாதுகாப்பிற்காக நீங்கள் HTTPS-மட்டும் பயன்முறையை இயக்கியுள்ளீர்கள், ஆனால் <em>%1$s</em> இன் HTTPS பதிப்பு கிடைக்கவில்லை.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP தளத்தில் தொடரவும்</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..29300458e0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-te/strings.xml
@@ -0,0 +1,258 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">తిరిగి ప్రయత్నించు</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">అభ్యర్థనను పూర్తి చేయలేము</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>ఈ సమస్యకు లేదా దోషానికి సంబంధించిన అదనపు సమాచారం ప్రస్తుతం అందుబాటులో లేదు</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">సురక్షిత అనుసంధానం విఫలమైంది</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>మీరు చూడటానికి ప్రయత్నిస్తున్న పేజీని చూపించలేము ఎందుకంటే అందుకున్న డేటా ప్రామాణికతను తనిఖీ చేయలేకపోయాం.</li>
+ <li>దయచేసి ఈ వెబ్‌సైట్ యజమానులను సంప్రదించి వారికి ఈ సమస్య గురించి తెలియజేయండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">సురక్షిత అనుసంధానం విఫలమైంది</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>ఇది స్వరరు స్వరూపణంలో సమస్య కావచ్చు, లేదా ఎవరో సర్వరును అనుకరించడానికి ప్రయత్నిస్తూండవచ్చు.</li>
+ <li>మీరు ఇంతకుముందు ఈ సర్వరుకి విజయవంతంగా అనుసంధానమైతే, ఈ తప్పిదం తాత్కాలికం కావచ్చు, మీరు కాసేపు ఆగి మళ్ళీ ప్రయత్నించవచ్చు.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ఉన్నతం…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>ఎవరో ఈ సైటును అనుకరించడానికి ప్రయత్నిస్తూండవచ్చు, మీరు ముందుకెళ్ళకూడదు.</label>
+ <br><br>
+ <label>వెబ్‌సైట్లు తమ గుర్తింపును ధృవపత్రాల ద్వారా నిరూపిస్తాయి. <b>%2$s</b>ను %1$s నమ్మడంలేదు, ఎందుకంటే వారి ధృవపత్రాన్ని జారీ చేసినది గుర్తుతెలియనివారు, ఆ ధృవవత్రం స్వయం-జారీ చేయబడింది, లేదా సర్వరు సరైన మధ్యవర్తి ధృవపత్రాలను పంపించడం లేదు.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">వెనక్కి వెళ్ళండి (సిఫారసు చేయబడింది)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">నష్టభయాన్ని అంగీకరించి ముందుకు కొనసాగండి</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">ఈ వెబ్‌సైటుకి సురక్షిత అనుసంధానం కావాలి.</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">ఉన్నతం…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">వెనుకకు వెళ్ళు</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">అనుసంధానానికి అంతరాయం కలిగింది</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>విహారిణి విజయవంతంగా అనుసంధానమయ్యింది, కానీ సమాచారం బదిలీ అవుతున్నప్పుడు అనుసంధానానికి అంతరాయం కలిగింది. దయచేసి మళ్ళీ ప్రయత్నించండి.</p>
+ <ul>
+ <li>సైటు తాత్కాలికంగా అందుబాటులో లేకపోవచ్చు లేదా చాలా ఒత్తిడిలో ఉండొచ్చు. కొంతసేపు ఆగి మళ్ళీ ప్రయత్నించండి.</li>
+ <li>మీకు వేరే పేజీలు కూడా తెరుచుకోకపోతూంటే, మీ పరికరంలో డేటా లేక వై-ఫై అనుసంధానాన్ని సరిచూసుకోండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">అనుసంధానానికి కాలం చెల్లిపోయింది</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>మీరు అడిగిన సైటు అనుసంధాన అభ్యర్థనకు స్పందించలేదు, విహారిణి ఇక స్పందన కోసం చూడటం ఆపివేసింది.</p>
+ <ul>
+ <li>సర్వరు అధిక ఒత్తిడిలో ఉందేమో లేక తాత్కాలికంగా పనిచేయడం లేదేమో? కాసేపటి తర్వాత మళ్ళీ పయత్నించండి.</li>
+ <li>మీకు వేరే సైట్లు కూడా తెరుచుకోవడం లేదా? మీ పరికరపు అనుసంధానాన్ని సరిచూసుకోండి.</li>
+ <li>మీ పరికరం ఫైర్‌వాల్ లేదా ప్రాక్సీ ద్వారా సంరక్షితమై ఉందా? తప్పుడు అమరికలు మీ జాల విహరణకు అడ్డుపడుతూండవచ్చు.</li>
+ <li>ఇంకా సమస్య ఉందా? సహాయం కొరకు మీ నెట్‌వర్క్ నిర్వాహకులను గానీ లేక అంతర్జాల సేవాదారుని గానీ సంప్రదించండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">అనుసంధానం సాధ్యం కావడంలేదు</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>సైటు తాత్కాలికంగా అందుబాటులో లేదు లేక చాలా ఒత్తిలో ఉండవచ్చు. దయచేసి కాపేపు ఆగి మళ్ళీ ప్రయత్నించండి.</li>
+ <li>మీకు మరే ఇతర పేజీలు తెరుచుకోకపోతూంటే, మీ పరికరపు డేటా లేక వై-ఫై అనుసంధానాన్ని సరిచూసుకోండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">సర్వరు నుండి అనుకోని స్పందన</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>నెట్‌వర్క్ అభ్యర్థనకు సైటు అనుకోని విధంగా స్పందించింది, కనుక విహారిణి ముందుకు వెళ్ళలేదు.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">పేజీ సరిగా దారిమళ్ళించడం లేదు</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>అడిగిన అంశం తేవడాన్ని విహారిణి ఆపివేసింది. అభ్యర్థనను ఎప్పటికీ పూర్తికాని విధంగా ఈ సైటు దారిమళ్ళిస్తోంది.</p>
+ <ul>
+ <li>ఈ సైటుకు కావలసిన కుకీలను మీరు అచేతనించడం లేక నిరోధించడం గానీ చేసారా?</li>
+ <li>ఈ సైటు కుకీలను అనుమతించడం సమస్యను పరిష్కరించకపోతే, అది బహుశా సర్వరు స్వరూపణంలో సమస్య కావచ్చు, మీ పరికరంలో కాకపోవచ్చు.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">ఆఫ్‌లైన్ రీతి</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>విహారిణి ఆఫ్‌లైన్ రీతితో పనిచేస్తుంది, అడిగిన అంశానికి అనుసంధానం కాలేకున్నది.</p>
+ <ul>
+ <li>ఈ పరికరం క్రియాశీల నెట్‌వర్కుకి అనుసంధానమై ఉందా?</li>
+ <li>ఆన్‌లైన్ రీతికి మారి పేజీని మళ్ళీ లోడుచేయడానికి “మళ్ళీ ప్రయత్నించు” నొక్కండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">భద్రతా కారణాల దృష్ట్యా పోర్టు నియంత్రించబడింది</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>అడిగిన చిరునామా సామాన్యంగా జాల విహారణ కోసం కాకుండా <em>ఇతర</em> ఉద్దేశాల కోసం వాడే ఒక పోర్టును పేర్కొన్నది (ఉదా॥ mozilla.orgలో పోర్టు 80 కొరకు<q>mozilla.org:80</q>). మీ సంరక్షణ, భద్రతలకై విహారిణి ఈ అభ్యర్థనను రద్దుచేసింది.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">అనుసంధానానికి అంతరాయం కలిగింది</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>అనుసంధానానికి ప్రయత్నిస్తూన్నప్పుడు నెట్‌వర్క్ లంకెకు అంతరాయం కలిగింది. దయచేసి మళ్ళీ ప్రయత్నించండి.</p>
+ <ul>
+ <li>ఆ సైటు తాత్కాలికంగా అందుబాటులో లేకపోవచ్చు లేక చాలా ఒత్తిడిలో ఉండివుండొచ్చు. కాసేపు ఆగి మళ్ళీ ప్రయత్నించండి.</li>
+ <li>మీకు మరే ఇతర పేజీలు కూడా తెరుచుకోకపోతూంటే, మీ పరికరపు డేటా లేక వై-ఫై అనుసంధానాన్ని సరిచూసుకోండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">సురక్షితం కాని ఫైలు రకం</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>దయచేసి వెబ్‌సైటు యజమానులను సంప్రదించి వారిగి ఈ సమస్యను తెలియజేయండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">పాడయిన విషయపు తప్పిదం</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>మీరు చూడడానికి ప్రయత్నిస్తున్న పేజీని చూపించలేము ఎందుకంటే డేటా బదలాయింపులో తప్పిదం కనుగొనబడింది.</p>
+ <ul>
+ <li>దయచేసి వెబ్‌సైటు యజమానులను సంప్రదించి వారికి ఈ సమస్య గురించి తెలియజేయండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">విషయం దెబ్బ తిన్నది</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>మీరు చూడడానికి ప్రయత్నిస్తున్న పేజీని చూపించలేము ఎందుకంటే డేటా బదలాయింపులో తప్పిదం కనుగొనబడింది.</p>
+ <ul>
+ <li>దయచేసి వెబ్‌సైటు యజమానులను సంప్రదించి వారికి ఈ సమస్య గురించి తెలియజేయండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">విషయపు ఎన్‌కోడింగ్ తప్పిదం</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>మీరు చూడడానికి ప్రయత్నిస్తున్న పేజీని చూపించలేము ఎందుకంటే అది చెల్లని లేక తోడ్పాటు లేని కంప్రెషన్ పద్ధతిని ఉపయోగిస్తోంది.</p>
+ <ul>
+ <li>దయచేసి వెబ్‌సైటు యజమానులను సంప్రదించి వారికి ఈ సమస్య గురించి తెలియజేయండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">చిరునామా కనపడలేదు</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>ఇచ్చిన చిరునామా కొరకు హోస్టు సర్వరును విహారిణి కనుగొనలేకపోయింది.</p>
+ <ul>
+ <li>చిరునామాలో
+ <strong>www</strong>.example.com బదులుగా
+ <strong>ww</strong>.example.com అని టైపు చేసారేమో
+ సరిచూసుకోండి.</li>
+ <li>మీరు మరే ఇతర పేజీలను తెరవలేకపోతూంటే, మీ పరికరపు డేటా లేక వై-ఫై అనుసంధానాన్ని సరిచూసుకోండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">అంతర్జాల అనుసంధానం లేదు</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">మీ నెట్‌వర్క్ అనుసంధానాన్ని సరిచూసుకోండి లేక కొన్ని క్షణాల తర్వాత ఈ పేజీని మళ్ళీ తెరవండి.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">మళ్ళీ లోడుచేయి</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">చెల్లని చిరునామా</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>ఇచ్చిన చిరునామా గుర్తించగలిగే ఆకృతిలో లేదు. దయచేసి చిరునామా పట్టీలో తప్పులను సరిచేసి అప్పుడు ప్రయత్నించండి.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ఈ చిరునామా చెల్లదు</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>జాల చిరునామాలను మాములుగా ఇలా వ్రాస్తారు <strong>http://www.example.com/</strong></li>
+ <li>మీరు ఫార్వార్డు స్లాషులనే (అంటే <strong>/</strong>) వాడేలా చూసుకోండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">తెలియని ప్రొటోకాల్</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>ఆ చిరునామా విహారిణి గుర్తించలేని ప్రొటోకాలును పేర్కొంది (ఉదా॥ <q>wxyz://</q>), కాబట్టి విహారిణి ఆ సైటుకి సరిగా అనుసంధానం కాజాలదు.</p>
+ <ul>
+ <li>మీరు మల్టీమీడియా లేదా పాఠ్యేతర సేవలను పొందడానికి ప్రయత్నిస్తున్నారా? అదనపు ఆవశ్యకాల కోసం సైటులో చూడండి.</li>
+ <li>కొన్ని ప్రొటోకాల్లను ముడో-పక్ష సాఫ్ట్‌వేరు లేక ప్లగిన్లు కావలసిరావచ్చు, ఆ తర్వాత వాటిని విహారిణి గుర్తించగలదు.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ఫైలు కనబడలేదు</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>పేరుమార్చబడి ఉండొచ్చు, తొలగింబడి ఉండొచ్చు, లేదా వేరే చోటికి తరలించబడి ఉండచ్చేమో?</li>
+ <li>చిరునామాలో స్పెల్లింగు, క్యాపిటలైజేషన్ లేదా ఇతర పాఠ్య సంబంధిత పొరపాట్లు ఏమైనా ఉన్నాయా?</li>
+ <li>అభ్యర్థించిన అంశాన్ని చూడడానికి మీకు తగిన అనుమతులు ఉన్నాయా?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ఫైలుకి ప్రాప్యత నిరాకరించబడింది</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>ఇది తీసివేయబడి ఉండచ్చు, తరలించవేయబడి ఉండచ్చు, లేదా ఫైలు అనుమతులు దాన్ని చూడడాన్ని నిరోధిస్తూండవచ్చు.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ప్రాక్రీ సర్వరు అనుసంధానాన్ని తిరస్కరించింది</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>ఈ విహారిణి ప్రాక్సీ సర్వరును వాడేలా స్వరూపించబడింది, కానీ ప్రాక్సీ అనుసంధానాన్ని నిరాకరించింది.</p>
+ <ul>
+ <li>విహారిణి ప్రాక్సీ స్వరూపణం సరియేనా? అమరికలను సరిచూసి అప్పుడు ప్రయత్నించండి.</li>
+ <li>ప్రాక్సీ సర్వరు ఈ నెట్‌వర్కు నుండి అనుసంధానాలను అనుమతిస్తుందా?</li>
+ <li>ఇంకా సమస్య ఉందా? సహాయం కొరకు మీ నెట్‌వర్క్ నిర్వాహకులను గానీ లేదా అంతర్జాల సేవాదారుని గానీ సంప్రదించండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ప్రాక్సీ సర్వరు కనబడలేదు</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>ఈ విహారిణి ప్రాక్సీ సర్వరును వాడేలా స్వరూపించబడింది, కానీ ప్రాక్సీ కనబడటం లేదు.</p>
+ <ul>
+ <li>విహారిణి ప్రాక్సీ స్వరూపణం సరియేనా? అమరికలను సరిచూసి అప్పుడు ప్రయత్నించండి.</li>
+ <li>ఈ పరికరం క్రియాశీలమైన నెట్‌వర్కుకు అనుసంధానమై ఉందా?</li>
+ <li>ఇంకా సమస్య ఉందా? సహాయం కొరకు మీ నెట్‌వర్క్ నిర్వాహకులను గానీ లేదా అంతర్జాల సేవాదారుని గానీ సంప్రదించండి.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">మాల్‌వేర్ సైటు సమస్య</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>%1$s వద్దగల సైటు దాడిచేసే సైటుగా నివేదించబడింది, మీ భద్రత అభిరుచుల మేరకు ఆ సైటు నిరోధించబడింది.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">అవాంఛిత సైటు సమస్య</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>%1$s వద్దగల సైటు అవాంఛిత సాఫ్ట్‌వేరును అందించేదిగా నివేదించబడింది. మీ భద్రత అభిరుచుల మేరకు ఈ సైటు నిరోధించబడింది.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">హానికరమైన సైటు సమస్య</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s వద్దగల సైటు హానికరంకాగల సైటుగా నివేదించబడింది, మీ భద్రత అభిరుచుల మేరకు ఆ సైటు నిరోధించబడింది.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">మోసపూరితమైన సైటు సమస్య</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>%1$s వద్ద గల వెబ్ పేజీ మోసపూరితమైన సైటుగా నివేదించబడి ఉంది, మీ భద్రతా అభిరుచుల మేరకు ఆ సైటు నిరోధించబడింది.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">సురక్షితమైన సైటు అందుబాటులో లేదు</string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP సైటుకి కొనసాగు</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..2cd3a9898f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tg/strings.xml
@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Аз нав кӯшиш кардан</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Дархостро ба анҷом расонида наметавонад</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Маълумоти иловагӣ дар бораи ин мушкилӣ ё хато ҳоло дастнорас аст.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Пайвасти бехатар иҷро нашуд</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Браузер саҳифаеро, ки шумо дидан мехоҳед, нишон дода наметавонад, зеро эътиборнокии маълумоти гирифташуда наметавонад тасдиқ карда шавад.</li>
+ <li>Лутфан, бо соҳибони сомона дар тамос шавед ва ба онҳо дар бораи ин мушкилӣ хабар диҳед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Пайвасти бехатар иҷро нашуд</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Ин метавонад дар танзимоти сервер мушкилӣ дошта бошад ё касе кӯшиш мекунад, ки серверро тақаллуб намояд.</li>
+ <li>Агар шумо қаблан ба ин сервер бо муваффақият пайваст шуда бошед, эҳтимол ин хато муваққатӣ аст ва шумо метавонед баъдтар амалро аз нав кӯшиш намоед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Иловагӣ…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Касе метавонад тақаллуб кардани сомонаро кӯшиш намояд ва шумо бояд идома надиҳед.</label>
+ <br><br>
+ <label>Сомонаҳо айнияти худро тавассути гувоҳномаҳо исбот мекунанд. %1$s ба <b>%2$s</b> эътимод надорад, зеро барорандаи гувоҳнома номаълум аст, гувоҳнома ба таври худ имзо гузоштааст ё сервер гувоҳномаҳои фосилавии дурустро намефиристад.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Бозгашт (тавсия дода мешавад)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Таваккалро қабул кунед ва идома диҳед</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ин сомона пайвасти бехатареро талаб мекунад.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Саҳифае, ки шумо мехоҳед аз назар гузаронед, намоиш дода намешавад, зеро ки ин сомона пайвасти бехатареро талаб мекунад.</li>
+ <li>Чунин менамояд, ки сомона дорои мушкилие мебошад, ки шумо онро мустақилона ҳал карда наметавонед.</li>
+ <li>Шумо метавонед дар бораи ин мушкилӣ ба маъмури сомона хабар диҳед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Иловагӣ…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> дорои сиёсати амниятӣ бо номи «Амнияти интиқоли қатъии HTTP» (HSTS) мебошад, ва ин маънои онро дорад, ки <b>%2$s</b> метавонад ба он танҳо тавассути алоқаи бехатар пайваст шавад. Шумо барои ворид шудан ба ин сомона истисноро илова карда наметавонед. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Бозгашт</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Пайвастшавӣ қатъ карда шуд</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Браузер бо муваффақият пайваст шуд, аммо ҳангоми интиқоли иттилоот пайвастшавӣ қатъ карда шуд. Лутфан, аз нав кӯшиш кунед.</p>
+ <ul>
+ <li>Сомона метавонад муваққатан дастнорас бошад ё бо дархостҳои зиёд хеле машғул бошад. Пас аз чанд лаҳза аз нав кӯшиш кунед.</li>
+ <li>Агар шумо ягон саҳифаро бор карда натавонед, пайвасти мобилии дастгоҳ ё пайвасти Wi-Fi -ро санҷед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Вақти пайвастшавӣ ба анҷом расид</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Сомонаи дархостшуда ба дархости пайвастшавӣ посух надод ва браузер мунтазири посухро қатъ кард.</p>
+ <ul>
+ <li>Эҳтимол сервер бо дархостҳои зиёд машғул аст ё ин ки муваққатан дастнорас аст? Баъдтар аз нав кӯшиш кунед.</li>
+ <li>Шумо метавонед, ки сомонаҳои дигарро бинед? Пайвасти шабакавии дастгоҳро санҷед.</li>
+ <li>Дастгоҳ ё шабакаи шумо бо девори оташ ё прокси муҳофизат шудааст? Танзимоти нодуруст метавонад ба дидани сомона халал расонад.</li>
+ <li>Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат намоед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Пайваст ғайриимкон аст</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Сомона метавонад муваққатан дастнорас бошад ё бо дархостҳои зиёд хеле машғул бошад. Пас аз чанд лаҳза аз нав кӯшиш кунед.</li>
+ <li>Агар шумо ягон саҳифаро бор карда натавонед, пайвасти мобилии дастгоҳ ё пайвасти Wi-Fi -ро санҷед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Ҷавоби ногаҳон аз сервер</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Сомона ба дархости шабака ба таври ғайричашмдошт посух дод ва браузер наметавонад идома диҳад.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Саҳифа ба таври дуруст равона карда намешавад</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p> Браузер кӯшиши ҷустуҷӯи маводи дархостшударо қатъ кард. Сомона дархостро тавре равона мекунад, ки раванд ҳеҷ гоҳ ба анҷом намерасад. </p>
+ <ul>
+ <li> Шумо кукиҳоеро, ки ин сомона талаб мекунад, ғайрифаъол кардед ё бастед? </li>
+ <li> Агар қабули кукиҳои сомона мушкилиро ҳал накунад, ин эҳтимолан мушкилии танзимоти сервер мебошад, на дастгоҳи шумо. </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Реҷаи офлайн</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Браузер дар реҷаи офлайн қарор дорад ва бо сомонаи дархостшуда пайваст шуда наметавонад.</p>
+ <ul>
+ <li>Оё дастгоҳ ба шабакаи фаъол пайваст аст?</li>
+ <li>Барои гузаштан ба реҷаи онлайн ва аз нав бор кардани саҳифа, тугмаи "Аз нав кӯшиш кардан"-ро пахш кунед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Порт бо сабабҳои амниятӣ маҳдуд карда шудааст</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Нишонии дархостшуда портеро муайян кард (масалан, <q>mozilla.org:80</q> барои порти 80 дар mozilla.org), ки одатан барои мақсадҳои <em>ба ғайр аз</em> тамошокунии сомонаҳо истифода мешавад. Браузер барои муҳофизат ва амнияти шумо дархостро бекор кард.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Пайвастшавӣ аз нав танзим карда шуд</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Алоқаи шабақа ҳангоми танзими пайвастшавӣ қатъ карда шуд. Лутфан, аз нав кӯшиш кунед.</p>
+ <ul>
+ <li>Сомона метавонад муваққатан дастнорас бошад ё бо дархостҳои зиёд хеле машғул бошад. Пас аз чанд лаҳза аз нав кӯшиш кунед.</li>
+ <li>Агар шумо ягон саҳифаро бор карда натавонед, пайвасти мобилии дастгоҳ ё пайвасти Wi-Fi -ро санҷед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Навъи файли беэътимод</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Лутфан, бо соҳибони сомона дар тамос шавед ва дар бораи ин мушкилӣ ба онҳо хабар диҳед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Хатои муҳтавои вайроншуда</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Браузер саҳифаеро, ки шумо дидан мехоҳед, нишон дода наметавонад, зеро дар интиқоли маълумот хато муайян карда шуд.</p>
+ <ul>
+ <li>Лутфан, бо соҳибони сомона дар тамос шавед ва ба онҳо дар бораи ин мушкилӣ хабар диҳед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Муҳтавои вайрон</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Браузер саҳифаеро, ки шумо дидан мехоҳед, нишон дода наметавонад, зеро дар интиқоли маълумот хато муайян карда шуд.</p>
+ <ul>
+ <li>Лутфан, бо соҳибони сомона дар тамос шавед ва ба онҳо дар бораи ин мушкилӣ хабар диҳед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Хатои рамзгузории муҳтаво</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Браузер саҳифаеро, ки шумо дидан мехоҳед, нишон дода наметавонад, зеро он аз шакли фишурдасозии дастгиринашаванда ё беэътибор истифода мебарад.</p>
+ <ul>
+ <li>Лутфан, бо соҳибони сомона дар тамос шавед ва ба онҳо дар бораи ин мушкилӣ хабар диҳед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Нишонӣ ёфт нашуд</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Браузер барои нишонии пешниҳодшуда мизбони серверро ёфт накард.</p>
+ <ul>
+ <li>Нишониро барои хатоҳои зерин санҷед:
+ <strong>ww</strong>.example.com ба ҷойи
+ <strong>www</strong>.example.com.</li>
+ <li>Агар шумо ягон саҳифаро бор карда натавонед, пайвасти мобилии дастгоҳ ё пайвасти Wi-Fi -ро санҷед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Пайвасти интернет нест</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Пайвасти Интернетро санҷед ё саҳифаро пас аз чанд лаҳза аз нав бор кунед.</string>
+
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Аз нав бор кардан</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Нишонии нодуруст</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Нишонии пешниҳодшуда дар шакли эътирофшуда намебошад. Лутфан, навори нишониро барои хатоҳо санҷед ва аз нав кӯшиш намоед.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Нишонӣ дуруст нест</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Нишониҳои сомона одатан дар ин шакл навишта мешаванд: <strong>http://www.example.com/</strong></li>
+ <li>Мутмаин шавед, ки шумо хатҳои каҷро истифода мебаред (масалан, <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Протоколи номаълум</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Нишонӣ протоколеро нишон медиҳад, ки браузер эътироф намекунад (масалан, <q> wxyz: // </q>). Аз ин рӯ браузер ба сомона дуруст пайваст шуда наметавонад. </p>
+ <ul>
+ <li> Оё шумо кӯшиши дастрас кардани мултимедия ё дигар хидматҳои ғайриматни доред? Талаботи иловагиро аз сомона санҷед. </li>
+ <li> Баъзеи протоколҳо метавонанд нармафзор ё плагинҳои тарафи сеюмро пеш аз шинохтани браузер талаб кунанд. </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Файл ёфт нашуд</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li> Эҳтимол аст, ки номи мавод иваз карда шуд, ё ин ки мавод тоза карда шуд, ё ба ҷойи дигар гузошта шуд? </li>
+ <li> Дар нишонӣ ягон хатои имлоӣ, ҳуруфчинӣ ё дигар хатои типографӣ вуҷуд дорад? </li>
+ <li> Шумо ба маводи дархостшуда иҷозати дастрасии кофӣ доред? </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Дастрасӣ ба файл манъ карда шуд</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Эҳтимол, он тоза карда шудааст, ба ҷойи дигар интиқол дода шудааст ё иҷозатҳои файл дастрасиро манъ мекунанд.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Сервери прокси пайвастро рад кард</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Браузер барои истифодаи сервери прокси танзим карда шудааст, аммо прокси аз пайвастшавӣ даст кашид.</p>
+ <ul>
+ <li>Танзимоти прокси дар браузер дуруст аст? Танзимотро санҷед ва аз нав кӯшиш кунед.</li>
+ <li>Хидмати прокси барои пайвастшавӣ аз ин шабака иҷозат медиҳад?</li>
+ <li>Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат кунед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Сервери прокси ёфт нашуд</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Браузер барои истифодаи сервери прокси танзим карда шудааст, аммо прокси ёфт нашуд.</p>
+ <ul>
+ <li>Танзимоти прокси дар браузер дуруст аст? Танзимотро санҷед ва аз нав кӯшиш кунед.</li>
+ <li>Дастгоҳ ба шабакаи фаъол пайваст аст?</li>
+ <li>Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат кунед.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Сомона бо нармафзори зарарнок</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Сомона дар %1$s ҳамчун cомонаи ҳамлакунанда гузориш дода шудааст ва дар асоси бартариҳои амниятии шумо баста шудааст.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Сомона бо муҳтавои номатлуб</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Сомона дар %1$s ҳамчун cомонаи паҳнкунандаи нармафзори зараровар гузориш дода шудааст ва дар асоси бартариҳои амниятии шумо баста шудааст.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Сомона бо муҳтавои зараровар</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Сомона дар %1$s ҳамчун cомона бо қобилияти зарароварии пинҳонӣ гузориш дода шудааст ва дар асоси бартариҳои амниятии шумо баста шудааст.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Сомона бо муҳтавои қалбакӣ</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Ин саҳифаи сомона дар %1$s ҳамчун сомонаи қалбакӣ гузориш дода шудааст ва дар асоси бартариҳои амниятии шумо баста шудааст.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Сомонаи бехатар дастрас нест</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Шумо барои амнияти такмилёфта реҷаи «Танҳо-HTTPS»-ро фаъол кардед, аммо версияи HTTPS барои сомонаи <em>%1$s</em> дастрас нест.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Ба сомонаи HTTP идома диҳед</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..f508da6f5d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-th/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">ลองอีกครั้ง</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">ไม่สามารถทำคำขอให้เสร็จสมบูรณ์</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>ไม่มีข้อมูลเพิ่มเติมเกี่ยวกับปัญหาหรือข้อผิดพลาดนี้ในปัจจุบัน</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">การเชื่อมต่อปลอดภัยล้มเหลว</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>หน้าที่คุณกำลังพยายามจะดูไม่สามารถแสดงได้เนื่องจากไม่สามารถยืนยันความถูกต้องของข้อมูลที่ได้รับ</li>
+ <li>โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">การเชื่อมต่อปลอดภัยล้มเหลว</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>สิ่งนี้อาจเป็นปัญหาจากการกำหนดค่าของเซิร์ฟเวอร์ หรืออาจมีใครบางคนกำลังพยายามปลอมแปลงเซิร์ฟเวอร์</li>
+ <li>หากคุณเคยเชื่อมต่อกับเซิร์ฟเวอร์นี้สำเร็จในอดีต ข้อผิดพลาดอาจเกิดขึ้นเพียงชั่วคราว และคุณสามารถลองอีกครั้งในภายหลัง</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ขั้นสูง…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>อาจมีใครบางคนกำลังพยายามเลียนแบบไซต์และคุณไม่ควรดำเนินการต่อ</label>
+ <br><br>
+ <label>เว็บไซต์พิสูจน์ข้อมูลประจำตัวของตัวเองผ่านใบรับรอง %1$s ไม่เชื่อถือ <b>%2$s</b> เนื่องจากไม่ทราบผู้ออกใบรับรอง, ใบรับรองถูกลงชื่อด้วยตนเอง, หรือเซิร์ฟเวอร์ไม่ส่งใบรับรองระดับกลางที่ถูกต้อง</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">ย้อนกลับ (แนะนำ)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">ยอมรับความเสี่ยงและดำเนินการต่อ</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">เว็บไซต์นี้ต้องการการเชื่อมต่อที่ปลอดภัย</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>ไม่สามารถแสดงหน้าที่คุณต้องการเข้าชมได้ เนื่องจากเว็บไซต์นี้บังคับใช้การเชื่อมต่อที่ปลอดภัย</li>
+ <li>โดยส่วนใหญ่แล้ว ปัญหานี้จะเกิดขึ้นที่ฝั่งเว็บไซต์ และคุณไม่สามารถแก้ปัญหาที่ฝั่งของคุณได้</li>
+ <li>คุณสามารถแจ้งผู้ดูแลเว็บไซต์ให้ทราบถึงปัญหานี้</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">ขั้นสูง…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> มีนโยบายการรักษาความปลอดภัยที่เรียกว่า HTTP Strict Transport Security (HSTS) ซึ่งหมายความว่า <b>%2$s</b> สามารถทำการเชื่อมต่อได้อย่างปลอดภัยเท่านั้น คุณไม่สามารถเพิ่มข้อยกเว้นเพื่อเยี่ยมชมไซต์นี้ได้ </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">ย้อนกลับ</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">การเชื่อมต่อถูกขัดจังหวะ</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>เบราว์เซอร์ทำการเชื่อมต่อสำเร็จแล้ว แต่การเชื่อมต่อถูกขัดจังหวะขณะถ่ายโอนข้อมูล โปรดลองอีกครั้ง</p>
+ <ul>
+ <li>ไซต์อาจไม่พร้อมใช้งานชั่วคราวหรือยุ่งเกินไป ลองอีกครั้งในอีกสักครู่</li>
+ <li>หากคุณไม่สามารถโหลดหน้าใด ๆ ได้ ให้ตรวจสอบข้อมูลของอุปกรณ์ของคุณหรือการเชื่อมต่อ Wi-Fi</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">การเชื่อมต่อหมดเวลา</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>ไซต์ที่ร้องขอไม่ตอบสนองต่อคำร้องขอการเชื่อมต่อและเบราว์เซอร์ได้หยุดการรอสำหรับการตอบกลับ</p>
+ <ul>
+ <li>เซิร์ฟเวอร์อาจประสบกับความต้องการที่สูงหรือดับไปชั่วคราว? ลองอีกครั้งในภายหลัง</li>
+ <li>คุณไม่สามารถเรียกดูไซต์อื่น ๆ ได้? ตรวจสอบการเชื่อมต่อเครือข่ายของคอมพิวเตอร์</li>
+ <li>คอมพิวเตอร์หรือเครือข่ายของคุณถูกปกป้องด้วยไฟร์วอลล์หรือพร็อกซี? การตั้งค่าที่ไม่ถูกต้องสามารถรบกวนการท่องเว็บได้</li>
+ <li>ยังคงมีปัญหา? ติดต่อผู้ดูแลเครือข่ายหรือผู้ให้บริการอินเทอร์เน็ตของคุณเพื่อขอรับความช่วยเหลือ</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">ไม่สามารถเชื่อมต่อ</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>ไซต์อาจไม่พร้อมใช้งานชั่วคราวหรือกำลังทำงานหนักเกินไป ลองอีกครั้งในอีกสักครู่</li>
+ <li>หากคุณไม่สามารถโหลดหน้าใด ๆ ได้ ตรวจสอบการเชื่อมต่อข้อมูลหรือ Wi-Fi ของอุปกรณ์ของคุณ</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">การตอบสนองที่ไม่คาดคิดจากเซิร์ฟเวอร์</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>ไซต์ตอบสนองต่อคำร้องขอเครือข่ายด้วยวิธีที่ไม่คาดคิดและเบราว์เซอร์ไม่สามารถดำเนินการต่อได้</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">หน้าไม่ได้เปลี่ยนเส้นทางอย่างถูกต้อง</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>เบราว์เซอร์ได้หยุดความพยายามในการเรียกรายการที่ร้องขอ ไซต์กำลังเปลี่ยนเส้นทางคำร้องขอในทางที่ไม่มีวันเสร็จสมบูรณ์</p>
+ <ul>
+ <li>คุณได้ปิดใช้งานหรือปิดกั้นคุกกี้ที่ต้องการโดยไซต์นี้หรือไม่?</li>
+ <li>หากการยอมรับคุกกี้ของไซต์ไม่แก้ปัญหา มันอาจเป็นปัญหาจากการกำหนดค่าเซิร์ฟเวอร์ ไม่ใช่คอมพิวเตอร์ของคุณ</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">โหมดออฟไลน์</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>เบราว์เซอร์กำลังทำงานในโหมดออฟไลน์และไม่สามารถเชื่อมต่อไปยังรายการที่ร้องขอ</p>
+ <ul>
+ <li>คอมพิวเตอร์เชื่อมต่อกับเครือข่ายที่ใช้งานได้หรือไม่?</li>
+ <li>กด “ลองอีกครั้ง” เพื่อสลับไปยังโหมดออนไลน์และโหลดหน้าใหม่</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">พอร์ตถูกจำกัดด้วยเหตุผลด้านความปลอดภัย</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>ที่อยู่ที่ร้องขอระบุพอร์ต (เช่น <q>mozilla.org:80</q> สำหรับพอร์ต 80 บน mozilla.org) ซึ่งโดยปกติจะใช้เพื่อวัตถุประสงค์ <em>อื่น</em> ที่ไม่ใช่การเรียกดูเว็บ เบราว์เซอร์ได้ยกเลิกคำร้องขอดังกล่าวเพื่อปกป้องคุณและเพื่อความปลอดภัย</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">การเชื่อมต่อถูกตัด</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>การเชื่อมโยงเครือข่ายถูกขัดจังหวะขณะแลกเปลี่ยนข้อมูลการเชื่อมต่อ โปรดลองอีกครั้ง</p>
+ <ul>
+ <li>ไซต์อาจไม่พร้อมใช้งานชั่วคราวหรือยุ่งเกินไป ลองอีกครั้งในอีกสักครู่</li>
+ <li>หากคุณไม่สามารถโหลดหน้าใด ๆ ได้ ให้ตรวจสอบข้อมูลของอุปกรณ์ของคุณหรือการเชื่อมต่อ Wi-Fi</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">ชนิดไฟล์ที่ไม่ปลอดภัย</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">ข้อผิดพลาดเนื้อหาเสียหาย</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>ไม่สามารถแสดงหน้าที่คุณกำลังพยายามจะดูเนื่องจากตรวจพบข้อผิดพลาดในการส่งผ่านข้อมูล</p>
+ <ul>
+ <li>โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">เนื้อหาขัดข้อง</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>ไม่สามารถแสดงหน้าที่คุณกำลังพยายามจะดูเนื่องจากตรวจพบข้อผิดพลาดในการส่งผ่านข้อมูล</p>
+ <ul>
+ <li>โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">ข้อผิดพลาดการเข้ารหัสเนื้อหา</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>ไม่สามารถแสดงหน้าที่คุณกำลังพยายามจะดูเนื่องจากหน้าใช้รูปแบบการบีบอัดที่ไม่ถูกต้องหรือไม่รองรับ</p>
+ <ul>
+ <li>โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ไม่พบที่อยู่</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>เบราว์เซอร์ไม่พบเซิร์ฟเวอร์โฮสต์สำหรับที่อยู่ที่ระบุ</p>
+ <ul>
+ <li>ตรวจสอบข้อผิดพลาดในการพิมพ์ที่อยู่ เช่น
+ <strong>ww</strong>.example.com แทนที่จะเป็น
+ <strong>www</strong>.example.com</li>
+ <li>หากคุณไม่สามารถโหลดหน้าใด ๆ ได้ ให้ตรวจสอบข้อมูลของอุปกรณ์หรือการเชื่อมต่อ Wi-Fi ของคุณ</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">ไม่มีการเชื่อมต่ออินเทอร์เน็ต</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">ตรวจสอบการเชื่อมต่อเครือข่ายของคุณหรือลองโหลดหน้านี้อีกครั้งในอีกสักครู่</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">เรียกใหม่</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ที่อยู่ไม่ถูกต้อง</string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>ที่อยู่ที่ให้มาไม่อยู่ในรูปแบบที่รู้จัก โปรดตรวจสอบที่อยู่ในแถบที่ตั้งเพื่อหาข้อผิดพลาดและลองใหม่อีกครั้ง</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ที่อยู่ไม่ถูกต้อง</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>ที่อยู่เว็บมักจะเขียนเป็น <strong>http://www.example.com/</strong></li>
+ <li>ตรวจสอบให้แน่ใจว่าคุณใช้เครื่องหมายทับไปข้างหน้า (กล่าวคือ <strong>/</strong>)</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">โปรโตคอลที่ไม่รู้จัก</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>ที่อยู่ระบุโปรโตคอล (เช่น <q>wxyz://</q>) ที่เบราว์เซอร์ไม่รู้จัก เบราว์เซอร์จึงไม่สามารถเชื่อมต่อไปยังไซต์ได้อย่างเหมาะสม</p>
+ <ul>
+ <li>คุณกำลังพยายามเข้าถึงมัลติมีเดียหรือบริการอื่น ๆ ที่ไม่ใช่ตัวอักษร? ตรวจสอบไซต์สำหรับข้อกำหนดพิเศษ</li>
+ <li>บางโปรโตคอลอาจต้องการซอฟต์แวร์หรือปลั๊กอินจากบุคคลที่สามก่อนที่เบราว์เซอร์จะสามารถรู้จัก</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ไม่พบไฟล์</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>รายการดังกล่าวอาจถูกเปลี่ยนชื่อ เอาออก หรือย้ายตำแหน่งที่ตั้งแล้วหรือไม่?</li>
+ <li>ที่อยู่สะกดผิด พิมพ์ตัวพิมพ์ใหญ่-เล็กไม่ตรง หรือตกหล่นหรือไม่?</li>
+ <li>คุณมีสิทธิอนุญาตการเข้าถึงรายการที่ร้องขอเพียงพอหรือไม่?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">การเข้าถึงไฟล์ถูกปฏิเสธ</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>ไฟล์อาจถูกเอาออก ย้าย หรือสิทธิอนุญาตของไฟล์อาจป้องกันการเข้าถึง</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">เซิร์ฟเวอร์พร็อกซีปฏิเสธการเชื่อมต่อ</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>เบราว์เซอร์ถูกกำหนดค่าให้ใช้เซิร์ฟเวอร์พร็อกซี</p>
+ <ul>
+ <li>การกำหนดค่าพร็อกซีของเบราว์เซอร์ถูกต้องหรือไม่? ตรวจสอบการตั้งค่าและลองอีกครั้ง</li>
+ <li>บริการพร็อกซีอนุญาตการเชื่อมต่อจากเครือข่ายนี้หรือไม่?</li>
+ <li>ยังคงมีปัญหา? ติดต่อผู้ดูแลเครือข่ายหรือผู้ให้บริการอินเทอร์เน็ตของคุณเพื่อขอรับความช่วยเหลือ</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ไม่พบเซิร์ฟเวอร์พร็อกซี</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>เบราว์เซอร์ถูกกำหนดค่าให้ใช้เซิร์ฟเวอร์พร็อกซี แต่พร็อกซีไม่สามารถหาพบ</p>
+ <ul>
+ <li>การกำหนดค่าพร็อกซีของเบราว์เซอร์ถูกต้องหรือไม่? ตรวจสอบการตั้งค่าและลองอีกครั้ง</li>
+ <li>คอมพิวเตอร์เชื่อมต่อกับเครือข่ายที่ใช้งานได้หรือไม่?</li>
+ <li>ยังคงมีปัญหา? ติดต่อผู้ดูแลเครือข่ายหรือผู้ให้บริการอินเทอร์เน็ตของคุณเพื่อขอรับความช่วยเหลือ</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">ปัญหาไซต์มัลแวร์</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>ไซต์ที่ %1$s ถูกรายงานว่าเป็นไซต์รุกรานและถูกปิดกั้นตามค่ากำหนดความปลอดภัยของคุณ</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ปัญหาไซต์ที่ไม่พึงประสงค์</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>ไซต์ที่ %1$s ถูกรายงานว่าแจกจ่ายซอฟต์แวร์ไม่พึงประสงค์และถูกปิดกั้นตามค่ากำหนดความปลอดภัยของคุณ</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">ปัญหาไซต์ที่เป็นอันตราย</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>ไซต์ที่ %1$s ถูกรายงานว่าเป็นไซต์ที่อาจเป็นอันตรายและถูกปิดกั้นตามค่ากำหนดความปลอดภัยของคุณ</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">ปัญหาไซต์หลอกลวง</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>หน้าเว็บนี้ที่ %1$s ถูกรายงานว่าเป็นไซต์หลอกลวงและถูกปิดกั้นตามค่ากำหนดความปลอดภัยของคุณ</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">ไซต์ที่ปลอดภัยไม่พร้อมใช้งาน</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[คุณได้เปิดใช้งานโหมด HTTPS-Only สำหรับความปลอดภัยที่มากขึ้น และรุ่น HTTPS ของ <em>%1$s</em> ไม่พร้อมใช้งาน]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">ดำเนินการต่อไปยังไซต์ HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..cacb270764
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tl/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Subukan Uli</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Hindi Makumpleto ang Kahilingan</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Karagdagang impormasyon tungkol sa problema o error na ito ay hindi puwede sa kasalukuyan.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Nabigo ang Ligtas na Koneksyon</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Hindi maipakita ang pahinang sinusubukan mong buksan dahil hindi masigurong katiwa-tiwala ang natanggap na data.</li>
+ <li>Mangyaring ipagbigay-alam ang problemang ito sa mga may-ari ng website.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Nabigo ang Ligtas na Koneksyon</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Ang problemang ito’y maaaring may kinalaman sa server configuration, o maaaring mayroong nagpapanggap bilang server na ito.</li>
+ <li>Kung nakakonekta ka na sa server na ito dati, maaaring pansamantala lang itong problema, at pwede mo uli subukan mamaya.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Advanced…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Maaaring mayroong may nagpapanggap bilang site na ito at hindi ka dapat magpatuloy.</label>
+ <br><br>
+ <label>Pinatutunayan ng mga website ang kanilang pagkakakilanlan gamit ang mga sertipiko. Hindi pinagkakatiwalaan ng %1$s ang <b>%2$s</b> dahil hindi kilala ang certificate issuer nito, self-signed ang sertipiko, o hindi ipinapadala ng server ang mga tamang intermediate certificate.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Bumalik (Inirerekomenda)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Tanggapin ang Panganib at Magpatuloy</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ang Website na ito ay na ngangailangan ng sekyur na koneksyon</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Pumaroon…</string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Bumalik</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Ang koneksyon ay naantala</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Tagumpay na nakakonekta ang browser, pero nagambala ang koneksyon habang nagpapadala ng impormasyon. Pakisubukan uli.</p>
+ <ul>
+ <li>Maaaring maraming ginagawa o pansamantalang hindi maabot ang site. Subukan uli sa loob ng ilang sandali.</li>
+ <li>Kung wala ka talagang mabuksang mga pahina, pakisuri ang iyong device data o koneksyon sa Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ang koneksyon ay nag-time out</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Ang site na hiningi ay hindi nagbigay-tugon sa isang connection request at tumigil nang maghintay sa reply ang browser.</p>
+ <ul>
+ <li>Baka naman maraming kumokonekta sa server o kaya pansamantalang pagtigil? Subukan uli mamaya.</li>
+ <li>Nakakapag-browse ka ba ng ibang mga site? Tingnan din ang network connection ng device.</li>
+ <li>Ang device mo ba o network ay protektado ng firewall o proxy? Nakakahambalang sa Web browsing ang mga maling setting.</li>
+ <li>Nagkakaproblema pa rin? Sumangguni sa iyong network administrator o Internet provider para makahingi ng tulong.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Hindi makakonekta</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Ang site ay maaaring pansamantalang hindi magagamit o masyadong abala. Subukan muli sa ilang sandali.</li>
+ <li>Kung hindi ka talaga makapag-load ng mga pahina, suriin ang iyong device data o koneksyon sa Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Hindi inaasahang tugon mula sa server</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Nagbigay-tugon ang site sa network request sa di-inaasahang paraan at hindi na maaaring magpatuloy ang browser.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Ang pahina ay hindi nagdidirekta nang maayos</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline Mode</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>The browser is operating in its offline mode and cannot connect to the requested item.</p>
+ <ul>
+ <li>Is the device connected to an active network?</li>
+ <li>Press “Try Again” to switch to online mode and reload the page.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Hinarang ang Port dahil sa mga kadahilanang pangseguridad</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ang koneksyon ay na-reset</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>The network link was interrupted while negotiating a connection. Please try again.</p>
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Hindi Ligtas na File Type</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Corrupted Content Error</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Bumagsak ang nilalaman</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Error sa Pag-encode ng Nilalaman</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Hindi Natagpuan ang Address</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Hindi matagpuan ng browser ang host server para sa nabanggit na address.</p>
+ <ul>
+ <li>Suriin ang address para sa mga maling pagkakatype gaya ng
+ <strong>ww</strong>.example.com sa halip na
+ <strong>www</strong>.example.com</li>
+ <li>Kung hindi ka talaga makapag-load ng kahit anong pahina, tingnan mo ang iyong device data o koneksyon sa Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Walang koneksyon sa Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Suriin ang iyong koneksyon sa network o subukang i-reload ang pahina sa ilang sandali.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Mag-reload</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Di-wastong Address</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Ang nabanggit na address ay nasa di-kilalang format. Pakisuri ang location bar sa mga pagkakamali at subukan uli.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Ang address ay hindi wasto</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Web addresses are usually written like <strong>http://www.example.com/</strong></li>
+ <li>Make sure that you’re using forward slashes (i.e. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Hindi kilalang Protocol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>The address specifies a protocol (e.g., <q>wxyz://</q>) the browser does not recognize, so the browser cannot properly connect to the site.</p>
+ <ul>
+ <li>Are you trying to access multimedia or other non-text services? Check the site for extra requirements.</li>
+ <li>Some protocols may require third-party software or plugins before the browser can recognize them.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Hindi Natagpuan ang File</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Could the item have been renamed, removed, or relocated?</li>
+ <li>Is there a spelling, capitalization, or other typographical error in the address?</li>
+ <li>Do you have sufficient access permissions to the requested item?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Pinigilan ang pag-access sa file</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Maaaring ito ay tinanggal, inilipat, o may mga permiso sa file na pumipigil sa access.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Tumanggi sa Koneksyon ng Proxy Server</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Ang browser ay naka-configure na gumamit ng proxy server, pero tumangging magkonekta ang proxy.</p>
+ <ul>
+ <li>Tama ba ang proxy configuration ng browser? Tingnan ang mga setting at subukan uli.</li>
+ <li>Pinapayagan ba ng proxy service ang mga koneksyon mula sa network na ito?</li>
+ <li>Nagkakaproblema pa rin? Sumangguni sa iyong network administrator o Internet provider para sa karagdagang tulong.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Hindi Matagpuan ang Proxy Server</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>The browser is configured to use a proxy server, but the proxy could not be found.</p>
+ <ul>
+ <li>Is the browser’s proxy configuration correct? Check the settings and try again.</li>
+ <li>Is the device connected to an active network?</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Isyu ng site sa malware</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as an attack site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Hindi ginustong isyu ng site</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as serving unwanted software and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Mapanganib na isyu sa site</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as a potentially harmful site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">May mapanlinlang na site</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>This web page at %1$s has been reported as a deceptive site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Tumuloy sa HTTP Site</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..4246855590
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tok/strings.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">o sin</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">mi ken ala pini e kama jo</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+<p>sona namako pi pakala ni li lon ala.</p>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">linja len li pakala</string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">linja len li pakala</string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">namako…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>ken la jan pi lipu ni li lon ala, li wile ike e sina kepeken nasin len. ni la o tawa ala.</label>
+ <br><br>
+ <label>lipu li pana e sona ona kepeken lipu lawa. <b>lipu %2$s</b> li pona ala tawa ilo %1$s tan ni: lipu lawa ona li tan seme? mi sona ala.</label>
+]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">o tawa pona (sina pona tan ni)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">o awen tawa lipu</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">linja li pakala</string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">tenpo mute la linja li pakala</string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">mi ken ala tawa lipu</string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">lipu li pana e pakala. nasin la mi sona ala</string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">lipu li toki e ni: o tawa ni. taso tawa ni li pakala</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">nasin pi linja ala</string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">lipu pi nasin ike</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+<ul>
+<li>o toki e pakala tawa jan lawa pi lipu ni.</li>
+</ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">linja li lon ala</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">o lukin e linja sina. o sin e lipu lon tenpo kama.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">o sin</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">nimi nasin pakala</string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">nimi nasin li pakala.</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">lipu li lon ala</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">sina ken ala lukin e lipu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">lipu li wile ike e sina</string>
+
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">o tawa lipu pi len ala</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..3a4970f3d5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tr/strings.xml
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Tekrar dene</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">İstek tamamlanamadı</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Bu sorun veya hata hakkında ek bilgi mevcut değil.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Güvenli bağlantı kurulamadı</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul> <li>Görüntülemeye çalıştığınız sayfa, alınan verilerin yetkinliği doğrulanamadığı için gösterilemiyor.</li> <li>Lütfen site yöneticisi ile irtibata geçip durumu bildirin.</li> </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Güvenli bağlantı kurulamadı</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul> <li>Sunucunun yapılandırmasıyla ilgili bir sorun olabilir veya birisi sunucuyu taklit etmeye çalışıyor olabilir.</li> <li>Daha önce bu sunucuya sorunsuz bağlandıysanız sorun geçici olabilir ve daha sonra yeniden bağlanmayı deneyebilirsiniz.</li> </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Gelişmiş…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Birisi bu siteyi taklit etmeye çalışıyor olabilir. Devam etmemenizi öneririz.</label>
+ <br><br>
+ <label>Web siteleri kimliklerini kanıtlamak için sertifikaları kullanlır. %1$s <b>%2$s</b> sitesine güvenmiyor. Sitenin sertifikasını düzenleyen kuruluş tanınmıyor olabilir, sertifika kendi kendine imzalanmış olabilir veya sunucu doğu ara sertifikaları göndermiyor olabilir.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Geri dön (Önerilir)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Riski kabul ederek devam et</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Bu web sitesi güvenli bir bağlantı gerektiriyor.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Bu web sitesi güvenli bir bağlantı gerektirdiği için, görüntülemeye çalıştığınızı sayfa gösterilemiyor.</li>
+ <li>Sorun büyük ihtimalle siteden kaynaklanıyor ve sorunu çözmek için sizin yapabileceğiniz bir şey yok.</li>
+ <li>Sorun hakkında site yöneticisini bilgilendirmeyi deneyebilirsiniz.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Gelişmiş…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label><b>%1$s</b> HTTP Sıkı Aktarım Güvenliği (HSTS) denilen bir güvenlik ilkesi uyguluyor. Bu nedenle <b>%2$s</b> bu siteye yalnızca güvenli bir şekilde bağlanabilir. Bu siteye ayrıcalık tanıyarak siteyi ziyaret edemezsiniz.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Geri dön</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Bağlantı kesintiye uğradı</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+      <p>Tarayıcı başarıyla bağlandı ama bilgi aktarılırken bağlantı kesildi. Lütfen tekrar deneyin.</p>
+      <ul>
+        <li>Site geçici olarak kullanılamıyor veya çok meşgul olabilir. Birkaç dakika sonra tekrar deneyin.</li>
+        <li>Hiçbir sayfa açılmıyorsa cihazınızın mobil internet veya Wi-Fi bağlantısını kontrol edin.</li>
+      </ul>
+    ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Bağlantı zaman aşımına uğradı</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>İstenen site bağlantı isteğine yanıt vermeyince tarayıcı da beklemeyi bıraktı.</p><ul><li>Sunucu, yoğun talep veya geçici bir kesinti nedeniyle hizmet veremiyor olabilir. Daha sonra yeniden deneyin.</li><li>Diğer siteler de mi açılmıyor? Öyleyse cihazınızın ağ bağlantısını kontrol edin.</li><li>Cihazınız veya ağınız bir güvenlik duvarıyla veya vekil sunucuyla korunuyorsa hatalı ayarlar internete erişmenizi engelleyebilir.</li><li>Her yolu denemenize rağmen hâlâ bağlantı kuramıyorsanız ağ yöneticinize veya internet servis sağlayıcınıza başvurun.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Bağlanılamadı</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Site geçici olarak kapalı veya çok meşgul olabilir. Birkaç dakika sonra yeniden deneyin.</li>
+ <li>Hiçbir sayfa açılmıyorsa cihazınızın mobil internet veya Wi-Fi bağlantısını kontrol edin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Sunucudan beklenmeyen yanıt</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Site, ağ isteğine beklenmeyen bir biçimde karşılık verdi. Tarayıcı bu işlemi sürdüremiyor.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Sayfa doğru bir şekilde yönlendirilmiyor</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Tarayıcı istenen öğeye ulaşmayı denemeyi bıraktı. Site tarayıcının isteğine hiçbir zaman sona ermeyecek bir yönlendirme döngüsü ile yanıt veriyor.</p><ul><li>Site tarafından bırakılmak istenen çerezleri engellemiş ya da etkisizleştirmiş olabilirsiniz.</li><li>Çerezleri kabul etmek sorununuzu çözmeye yetmiyorsa sorun büyük olasılıkla cihazınızdan değil, sunucu yapılandırmasından kaynaklanıyordur.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Çevrimdışı kip</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Tarayıcı şu an çevrimdışı kipte çalışıyor ve istenen öğeye bağlanamaz.</p><ul><li>Cihazınız etkin bir ağa bağlı mı?</li><li>Çevrimiçi kipe geçerek sayfayı tazelemek için “Yeniden dene” düğmesine tıklayın.</li></ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Güvenlik nedeniyle bağlantı noktası kısıtlanmış</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>İstenen adres normalde web gezintisi <em>dışında</em> amaçlar için kullanılan bir bağlantı noktası (örn. mozilla.org üzerinde 80. port için <q>mozilla.org:80</q>) içeriyor. Tarayıcı sizi korumak ve güvenliğini sağlamak amacıyla isteği iptal etti.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Bağlantı sıfırlandı</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+      <p>Bağlantı anlaşması yapılırken ağ bağlantısı kesildi. Lütfen tekrar deneyin.</p>
+      <ul>
+        <li>Site geçici olarak kullanılamıyor veya çok meşgul olabilir. Birkaç dakika sonra tekrar deneyin.</li>
+        <li>Hiçbir sayfa açılmıyorsa cihazınızın mobil internet veya Wi-Fi bağlantısını kontrol edin.</li>
+      </ul>
+    ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Güvensiz dosya türü</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+<ul>
+ <li>Site sahipleriyle iletişim kurarak bu sorunu onlara bildirmeyi düşünebilirsiniz.</li>
+</ul>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Hasarlı İçerik Hatası</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Veri aktarımında bir hata tespit edildiği için bakmak istediğiniz sayfa gösterilemiyor.</p><ul><li>Site sahipleriyle iletişim kurup bu sorunu onlara bildirmeyi düşünebilirsiniz.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">İçerik çöktü</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Veri aktarımında bir hata tespit edildiği için bakmak istediğiniz sayfa gösterilemiyor.</p><ul><li>Site sahipleriyle iletişim kurup bu sorunu onlara bildirmeyi düşünebilirsiniz.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">İçerik kodlama hatası</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+      <p>Görüntülemeye çalıştığınız sayfa geçersiz veya desteklenmeyen bir sıkıştırma biçimi kullandığından dolayı gösterilemiyor.</p>
+      <ul>
+        <li>Bu sorunu bildirmek için lütfen web sitesi sahipleriyle iletişime geçin.</li>
+      </ul>
+    ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Adres bulunamadı</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[ <p>Tarayıcı girilen adresin sunucusunu bulamadı.</p>
+ <ul>
+ <li>Adreste olabilecek yazım hatalarını kontrol edin.
+ Örneğin <strong>www</strong>.example.com yerine
+ <strong>ww</strong>.example.com yazmış olabilir misiniz?</li>
+ <li>Hiçbir sayfa açılmıyorsa cihazınızın mobil internet veya Wi-Fi bağlantısını kontrol edin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">İnternet bağlantısı yok</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Ağ bağlantınızı kontrol edin veya birkaç dakika sonra sayfayı tazelemeyi deneyin.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Tazele</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Geçersiz adres</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Girilen adres tanınan bir biçimde değil. Konum çubuğuna bakıp olası hataları giderdikten sonra yeniden deneyin.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Adres geçersiz</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Web adresleri genelde <strong>http://www.example.com/</strong> biçiminde yazılır.</li>
+ <li>Adreste sağa yatık bölü işareti (<strong>/</strong>) bulunduğundan emin olun.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Bilinmeyen protokol</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Girilen adres tarayıcı tarafından tanınmayan bir iletişim kuralına (ör. <q>abcd://</q>) işaret ettiğinden tarayıcı siteye düzgünce bağlanamıyor.</p><ul><li>Çoklu ortam barındıran veya metin içermeyen bir hizmete bağlanmak istiyor olabilirsiniz.</li><li>Bazı iletişim kurallarının tarayıcı tarafından tanınabilmesi için üçüncü kişilerce geliştirilen yazılımlara ya da eklentilere ihtiyaç duyulabilir.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Dosya bulunamadı</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul><li>Dosya taşınmış, silinmiş ya da adı değiştirilmiş olabilir.</li><li>Adreste yazım hatası yapılmış olabilir.</li><li>İstenen öğeye erişmek için gerekli izniniz olmayabilir.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Dosyaya erişim reddedildi</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+<ul>
+ <li>Silinmiş, taşınmış veya dosya izinleri nedeniyle erişilemiyor olabilir.</li>
+</ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Vekil sunucu bağlantıyı reddetti</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Tarayıcı vekil sunucu kullanmak üzere yapılandırılmış, ancak vekil sunucu bağlantı isteğini geri çevirdi.</p><ul><li>Vekil sunucu ayarları düzgün yapılmamış olabilir. Ayarlarınızı gözden geçirip yeniden deneyin.</li><li>Belirtilen vekil sunucu ağınız üzerinden kurulan bağlantılara izin vermiyor olabilir.</li><li>Hâlâ sorun yaşıyorsanız ağ yöneticinize ya da internet servis sağlayıcınıza başvurun.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Vekil sunucu bulunamadı</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Tarayıcı vekil sunucu kullanmak üzere yapılandırılmış, ancak vekil sunucu bulunamadı.</p><ul><li>Vekil sunucu ayarları düzgün yapılmamış olabilir. Ayarlarınızı gözden geçirip yeniden deneyin.</li><li>Cihazınız etkin bir ağ bağlantısına sahip olmayabilir.</li><li>Hâlâ sorun yaşıyorsanız ağ yöneticinize ya da internet servis sağlayıcınıza başvurun.</li></ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Kötü amaçlı yazılım sitesi sorunu</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>%1$s konumundaki sitenin saldırı sitesi olduğu bildirildi ve güvenlik tercihlerinize dayanılarak site engellendi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">İstenmeyen site sorunu</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s konumundaki sitenin istenmeyen yazılım dağıttığı bildirildi ve güvenlik tercihlerinize dayanılarak site engellendi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Zararlı site sorunu</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>%1$s konumundaki sitenin potansiyel olarak zararlı site olduğu bildirildi ve güvenlik tercihlerinize dayanılarak site engellendi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Aldatıcı site sorunu</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p> %1$s web sayfasının aldatıcı bir sayfa olduğu ihbar edilmiştir. Sayfa, güvenlik tercihlerinize dayanılarak engellendi.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Güvenli site mevcut değil</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Güvenliğinizi artırmak için Yalnızca HTTPS modunu açtınız ve <em>%1$s</em> sitesinin güvenli bir HTTPS sürümü mevcut değil.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP siteye devam et</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..898dd59297
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-trs/strings.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">A\'ngô ñû</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Na\'ue gārayinaj sa achín nì\'iát</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Nitāj ni\'īn nùhuin saj gire\' ma.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Gire\' koneksiûn dugumî sò\'</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>\'Ngō paginâ ni\'iājt nan nī na\'ue nāyi\'nïn dadin\' na\'ue nāni\'in sà\' aga\' nan nej datô nahuin ra\'ât.</li>
+ <li>Gi\'iaj \'ngō sunnūj nī nanà\'uì\'t ahuin si si\'iaj huin sitiô nan nī gatāj nan\'ānj si gunînt nùhuin saj \'ia dànanj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Gire\' koneksiûn dugumî sò\'</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Nagi\'iaj chìj sa ruhuât nādūnāt riña servidor, asi huā \'ngō sa ânej ruhuâ natū riña sa huin ruhuât na\'nïnt.</li>
+ <li>Sisī ngà\' gatûj ñùt riña servidor nan nī nitāj nùj gi\'iaj chìj nī ga\'ue sisī man si giyichin\' akuanj, yakāj da\'nga\' gātūt nanâ doj a.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Sa tàj ñā doj…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>Huā sa huin ruhuâ dīga\'ñūn\'unj sò\' sisī huê sitiô nan huin nī da\'uît nānikàj rūkût.</label>
+ <br><br>
+ <label>Ngà sertifikâdo nadigân nej sitio web man\'an nej man. %1$snu yumân ruhuâ aga\' nan ni\'in <b>%2$s</b>dadin\' na\'ue nani\'in ahuin si giri si sērtifikadoj, huā a\'nan\' sertifikado asi nu ga\'nïnj hue\'ê servidor sertifikâdo.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Nānikàj ne\' rūkùu (sa sà\'a huin)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Gārāyino\' si ahī hua man nī gun\' ne\' ñāan</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ni’ñānj sitiô nan ‘ngō koneksiûn dūgumîn ñù’.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[<ul>
+<li>Nā’hue nāyi’nïn pajinâ ruhuât ni’hiājt dadin’ ni’ñānj web nan ‘ngō kōneksiûn hue’ê.</li>
+<li>Ahui a’nan’ man si gūruhuaj dadin’ pajinân nan ‘hiaj nī nitāj dàj gaj gī’hiát da’ gānahuin man.</li>
+<li>Gā’hue gātāj nan’ānjt riña administrador sitio nan rayi’î sa ahui a’nan’ nan.</li></ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Sa huāj ñā doj…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label> <b>%1$s</b> nīka ‘ngō chrēj dugumîn ñùn’ gū’nàj HTTP Strict Transport Security (HSTS), ô’ nī gāta ruhuâj sisī <b>%2$s</b> gā’hue gātūt riñanj ngâ sa huā arán doj. Si ga’hue nādunāt sa huā riña sitiô nan si ruhuât gātūt. </label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Nānīkāj ne\’ Rūkùu</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Giyichin\' ngè kōneksiûn</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>Nayi\'nïnj hue\'ê riña sā nana\'uî\'t, sanī gire\' ngè koneksiûn ngà hìaj nayî\'nïn. Gī\'iaj \'ngō sunūnj un, ginù huin ñû.</p>
+ <ul>
+ <li>Ga\'ue sisī ûta hua chrūj riña sitio asi nitāj si \'iaj sun man akuan\' nïn. Ginù huin ñû yichrá \'ngō nâ nùkuaj.</li>
+ <li>Sisī na\'ue nāyi\'nïn riña à\'ngō pâjina, ni’iāj sisī huā hue\'ê internet riña si āgâ\'t.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ganahuij diû ana\'uìjt</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>Nu ga\'ue nāyi\'nïn riña sitiô gachrûnt yi\'ì dan ga\'nïn\' ruhuâ riña sā nana\'uî\'t.</p>
+ <ul>
+ <li>Ûta hua chruj riña servidor asi gire\' internet aj. Ginù huin nanâ doj.</li>
+ <li>Na\'ue gātūt riña a\'ngô nej sitio a\'. Gīni\'iaj si huā internet riña si āgâ\' ra.</li>
+ <li>Dugumîn \'ngō frirewall asi \'ngō proxy si āgâ\' raj. Ni\'iāj si huā konfiguradô hue\'ej dadin\' gā\'ue sisī huej \'iaj.</li>
+ <li>Huā nïn\' \'iaj chìj da\' gātū raj. Gā\'mīn ngà duguî\' dû\'uèj internet riñat da\' rugûñu\'ūnj si sò\'.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Na\'ue gātu riña internet</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>Ga\'ue si huā chruj riña sitiô nana si huā rán akuan\' riñanj. Ginùn huin ñû nanâ doj.</li>
+ <li>Sisī huê dan \'iaj daran\' chre nej pâjina, ni\'iāj sisī \'iaj sun hue\'ê si āgâ\'t li asi huā internet riñanj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Sê \'ngō nuguan\' hue\'ê narikî servidor</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>Nu nahuin rā\'a hue\'ê sitio sa gachín nì\'iát guendâ gātūt riña internet nī sī ga\'ue gān\'ānjt ne\' ñāan ngà sa nānâ\'uì\'t.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Nitāj si \'iaj redireksionando hue\'ê pajinâ nan</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>Ganikïn\' sa riñā nana\'uî\'t dadin\' huin ruhuaj nāhuin ra\'a nuguan\' gachrûnt. Nitāj āmān nāyī\'nïn riña sitiô nan si gūrūhuaj.</p>
+ <ul>
+ <li>Huā rán riña nej kokî arâj sun sitiô nan \'iá raj.</li>
+ <li>Sisī ngà gâ\'nïnjt \'iaj sun nej kôki nī huā nï\' na\'ue gi\'iaj sunj, si gūrūhuaj nī servidor hua a\'nan\' nī sê si āgâ\'t \'iaj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Sī gida\'aj internet</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>Nitāj si anûn sa nāhuin rā\'a internet riña si āgâ\'t yi\'ì dan na\'ue nāyi\'nïn riña sa nana\'uî\'t.</p>
+ <ul>
+ <li>\'Iaj sun hue\'ê internet riña si āgâ\' raj.</li>
+ <li>Gūru\'man ra\'a “Ginùn huin ñû” da\' nānùn internet nī nāyi\'nïn si pajinât.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Nitāj si hūaj gātūt dadin\' huā āhī man</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>Dīreksiûn gachrûnt nī nīka \'ngō puerto (dàj rû\' <q>mozilla.org:80</q> guendâ puerto 80 nīkāj mozilla.org) sê guendâ gāchē nun\' riña internet huin man dadin\' <em>nīnïn</em> hua si sunj. Duyichin\' sa riñā nana\'uî\'t \'ngō sa gachín nì\'iát da\' dūgumîn man sò\' ngà gāchē nunt.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Nayi\'ì nākà ñû conexión</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>Hīaj nûn huin gā kōneksiûn sanī giyichin\' ngè si enlace red. Gī\'iaj \'ngō sunūnj un, ginù huin ñû.</p>
+ <ul>
+ <li>Ga\'ue sisī ûta hua chrūj riña sitio asi nitāj si \'iaj sun man akuan\' nïn. Ginù huin ñû yichrá \'ngō nâ nùkuaj.</li>
+ <li>Sisī na\'ue nāyi\'nïn riña à\'ngō pâjina, ni’iāj sisī huā hue\'ê internet riña si āgâ\'t li.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">\'Ngō archivo huā āhīi huin</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Gī\'iaj \'ngō sunnūj nī, nānà\'uì\' ahuin si sī\'iaj huin sitiô nan nī gāchìnj na\'ānjt nù huin saj gire\'ej.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Gire\'ej dadin\' sa nūn riñanj hua a\'nan\'</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>\'Ngō paginâ ruhuât ni\'iājt nan nī na\'ue nāyi\'nïn dadin\' na\'ue gāchīn sà\' nej dâto.</p>
+ <ul>
+ <li>Gi\'iaj \'ngō sunnūj nī nanà\'uì\'t ahuin si si\'iaj huin sitiô nan nī gatāj nan\'ānjt gūnïn si sa huā nan.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Ganarán riña kōntenîdo</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>\'Ngō paginâ ruhuât ni\'iājt nan nī na\'ue nāyi\'nïn dadin\' na\'ue gāchīn sà\' nej dâto.</p>
+ <ul>
+ <li>Gi\'iaj \'ngō sunnūj nī nanà\'uì\'t ahuin si si\'iaj huin sitiô nan nī gatāj nan\'ānjt gūnïn si sa huā nan.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Gire\' si kodigô kōntenîdo</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>\'Ngō paginâ ruhuât ni\'iājt nan nī na\'ue nāyi\'nïn dadin\' nitāj si huā hue\'ê \'ngō kōmpresiûn nīka.</p>
+ <ul>
+ <li>Gi\'iaj \'ngō sunnūj nī nanà\'uì\'t ahuin si si\'iaj huin sitiô nan nī gatāj nan\'ānjt gūnïn si sa huā nan.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Nu nārì\'ìj dīreksiûn</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Nu ga\'ue narì\' riña sā nana\'uî\'t \'ngō servidor guendâ direksiûn gachrûnt.</p>
+ <ul>
+ <li>Gīni\'iāj si nu gāchrūn hue\'êt, dàj rû\':
+ <strong>ww</strong>.example.com luguâ gāchrūnt
+ <strong>www</strong>.example.com.</li>
+ <li>Sisī na\'ue nāyi\'nïn riña gà\' si \'ngō pâjina, ni\'iāj si huā internet riña si āgà\'t li.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nitāj internet hua</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Nātsij ni\'iājt sisī huā internet asi ginù huin ñû nā\'nïnt riña nanâ doj sínj.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Nā\'nïn ñû</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Nitāj si huā hue\'ê direksiûn gachrûnt</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Nu nārì\'t dà gāchrūnt direksiûn. Ni\'iāj dānè gachrûn a\'nâ\'t nī nāchrūn hue\'ê man.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Nitāj si huā hue\'ê direksiûn gachrûnt</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Huā da\'āj nī achrûn\' \'ngō dīreksiûn dànanj <strong>http://www.example.com/</strong></li>
+ <li>Ni\'iāj hue\'ê sisī arâj sunt huìj chrun li nīkïn\' nītïn dan ân (dàj rû\' <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Sê dànanj gāchrūn\'</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Dàj gāchrūn\' \'ngō dīreksiûn (dàj rû\', <q>wxyz://</q>) da\' nāni\'in riña sā nana\'uî\'t, si nitāj nī si na\'nïn riña sitiô ruhuât gātūt.</p>
+ <ul>
+ <li>Sī gātūt riña \'ngō hiūj sê sa achrû\' huin anj. Gīni\'iāj nùj huin doj sa achín sitiô dan ân.</li>
+ <li>Huā da\'āj nej man nī ni\'ña \'ngō software asi plūyîn da\' ga\'ue nāni\'in sa riñā nana\'uî\'t man.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Nu nārì\'ij archîbo</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Nadunâ si yūgui man, nare\' man asi nadunâ man riña hūa aj.</li>
+ <li>Nu gāchrūn hue\'êt, asi garâj sunt mayûskula asi a\'ngô sa huā a\'nan\' gachrûnt riña dīreksiûn anj.</li>
+ <li>Giri\' nì\'iát da\' gātūt hiūj nan bè\'ej.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Nu ga\'ue gā\'nïn gātūt riña archîbo</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>Ga\'ue si nare\'ej asi ganatu a\'ngô hiūj u, asi nu gīrì\' nì\'iát riña archîbo da\' gātūt riñaj.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Nu gā\'nïn servidor proxy gātūt</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Huā nî\'nïn riña sā nana\'uî\'t dan da\' gārāsunj \'ngō servidor proxy, sanī proxy nu gā\'nïn gātūt.</p>
+ <ul>
+ <li>Huā hue\'ê proxy riña sa riñā nana\'uî\'t bè\'ej. Nātsij ni\'iājt nī ginù huin ñû gātūt.</li>
+ <li>Ga\'ue gātūt hiūj nan tāj proxy bè\'ej.</li>
+ <li>Huā nï\' iaj chij aj. Gīni\'iāj gā\'mīnt ngà duguî\' dû\'uèj internet asi sû\' nīkāj ñu\'ūnj man da\' rūgûñu\'ūnj si sò\'.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Nu nārì\'ij servidor proxy</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>Huā nî\'nïn riña sā nana\'uî\'t dan da\' gārāsunj \'ngō servidor proxy, sanī nu nārì\'ij proxy.</p>
+ <ul>
+ <li>Huā hue\'ê proxy riña sa riñā nana\'uî\'t bè\'ej. Nātsij ni\'iājt nī ginù huin ñû gātūt.</li>
+ <li>Huā kōnektadô si āgâ\'t riña \'ngō red \'iaj sun anj.</li>
+ <li>Huā nï\' iaj chij aj. Gīni\'iāj gā\'mīnt ngà duguî\' dû\'uèj internet asi sû\' nīkāj ñu\'ūnj man da\' rūgûñu\'ūnj si sò\'.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Sa huā a\'nan\' riña sitiô yī\'ì nan</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>Sitiô %1$s nī \'ngō sitiô yī\'ìi huin man yī\'ì dan narán aga\' nan riñaj dadin\' daj gà\' gatāj na\'ānjt gūnï.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Sa huā a\'nan\' riña sitiô nāsinùnj nan</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>Sitiô %1$s nī \'ngō sitiô duguane\' software āhīi huin man yī\'ì dan narán aga\' nan riñaj dadin\' daj gà\' gatāj na\'ānjt gūnï.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Sa huā a\'nan\' riña sitiô yī\'ìi</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>Sitiô %1$s nī \'ngō sitiô yī\'ì hīa nïn\'in huin man yī\'ì dan narán aga\' nan riñaj dadin\' daj gà\' gatāj na\'ānjt gūnï.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Sa huā a\'nan\' riña sitiô diga\'ñu\'ūnj un</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>Pajinâ %1$s nī \'ngō pajinâ diga\'ñu\'ūnj un huin man yī\'ì dan narán aga\' nan riñaj dadin\' daj gà\' gatāj na\'ānjt gūnï.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Nitāj si huā akuan’ sitiô hue’è nan</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Ngà ‘hiaj sun sa gū’nàj HTTPS da’ gā nïn doj gāchēt nī huā nï’ nitāj ‘ngō HTTPS nākà guendâ <em>%1$s</em> hua.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Gīnu ngè riña HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..1074f6c0ba
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tt/strings.xml
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Янәдән тырышып карау</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Сорауны тәмамлап булмый</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Бу проблема яки хата турында өстәмә мәгълүмат әлегә юк.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Хәвефсез бәйләнеш кору хатасы</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Күрәсегез килгән сәхифә күрсәтелә алмый, чөнки алынган мәгълүматларның чынлыгын раслау мөмкин түгел</li>
+ <li>Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Хәвефсез бәйләнеш кору хатасы</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Бу сервер көйләүләрендәге хата белән бәйле булырга мөмкин, яисә кемдер Сезгә кирәкле серверны икенче берәве белән алыштырырга тырыша.</li>
+ <li>Моңа кадәр бу серверга уңышлы тоташа торган булсагыз, бу хата вакытлыча гына булырга мөмкин. Берәздән янәдән тырышып карагыз.</li>
+ </ul>
+
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Киңәйтелгән…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Кемдер үзенең сайтын бу сайт дип күрсәтергә тырыша кебек, шунлыктан дәвам итмәвегез хәерле.</label>
+ <br><br>
+ <label>Веб-сайтлар үзләренең чынлыгын сертификатлар ярдәмендә раслый. %1$s кушымтасы <b>%2$s</b> сайтына ышанмый, чөнки сертификатны чыгаручы я билгесез, я ул үзлегеннән имзаланган, я сервер дөрес арадашчы сертификатлар җибәрми.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Кире кайту (киңәш ителә)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Рискны кабул итү һәм дәвам итү</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Бу вебсайт хәвефсез бәйләнеш таләп итә.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Күрәсегез килгән сәхифә күрсәтелә алмый, чөнки бу вебсайт хәвефсез бәйләнеш таләп итә.</li>
+ <li>Проблема сайтта булырга тиеш һәм аны чишү өчен Сез эшли алырлык бернәрсә дә юк.</li>
+ <li>Вебсайтның администраторына проблема турында хәбәр итә аласыз.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Киңәйтелгән…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label><b>%1$s</b> сәхифәсенең HTTP Strict Transport Security (HSTS) дип аталган хәвефсезлек сәясәте бар, шуңа күрә <b>%2$s</b> аңа хәвефсез рәвештә генә тоташа ала. Сез бу сайтка керү өчен чыгарма өсти алмыйсыз.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Кире кайту</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Бәйләнеш өзелде</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Браузер уңышлы тоташты, ләкин мәгълүмат тапшырылганда бәйләнеш өзелеп китте. Янәдән тырышып карагыз.</p>
+ <ul>
+ <li>Сайт вакытлыча я бөтенләй эшләми, я бик мәшгуль. Берәздән янәдән ачып карагыз.</li>
+ <li>Әгәр һичбер сайт та ачылмаса, җиһазыгызның мобиль интернет я Wi-Fi бәйләнешләрен тикшерегез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Бәйләнешне көтү вакыты чыкты</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Соралган сайт тоташу соравына җавап бирмәде һәм браузер җавап көтүдән туктады.</p>
+ <ul>
+ <li>Сайт серверы вакытлыча сүнгән яки артык йөкләнгән булырга мөмкинме? Соңрак янәдән тырышып карагыз.</li>
+ <li>Башка сайтлар да ачылмаса, җиһазыгызның интернетка бәйләнешен тикшерегез.</li>
+ <li>Җиһазыгыз яки челтәрегез иминлек дивары яисә прокси сервер белән сакланган булса, аларның көйләүләрен тикшерегез, чөнки ялгыш көйләүләр вебсайтларның ачылуына киртә була ал.</li>
+ <li>Проблема чишелмәсә, челтәр администраторыгыз яки интернет-провайдерыгыз белән элемтәгә керегез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Тоташып булмады</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Сайт вакытлыча сүнгән я бик мәшгуль булырга мөмкин. Берничә минуттан янәдән тырышып карагыз.</li>
+ <li>Һичбер сәхифә дә ачылмаса җиһазыгызның мобиль Интернет яисә Wi-Fi бәйләнешен тикшерегез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Сервердан көтелмәгән җавап</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Сайт сорауга көтелмәгәнчә җавап бирде һәм браузер дәвам итә алмый.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Сәхифә икенче сәхифәгә дөрес юнәлтми</string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Автоном эш режимы</string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Порт куркынычсызлык саклау максатыннан ябык</string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Бәйләнеш ташланды</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Тоташкан вакытта челтәр аша бәйләнеш өзелеп китте. Зинһар янәдән тырышып карагыз.</p>
+ <ul>
+ <li>Сайт я вакытлыча эшләми, я бик мәшгуль. Берәздән янәдән ачып карагыз.</li>
+ <li>Әгәр һичбер сайт та ачылмаса, җиһазыгызның мобиль интернет яки Wi-Fi бәйләнешен тикшерегез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Куркыныч файл төре</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Бозык эчтәлек хатасы</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Күрәсегез килгән сәхифә күрсәтелә алмый, чөнки мәгълүмат тапшыруда хата табылды.</p>
+ <ul>
+ <li>Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Эчтәлеге ватык</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Күрәсегез килгән сәхифә күрсәтелә алмый, чөнки мәгълүмат тапшыруда хата табылды.</p>
+ <ul>
+ <li>Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Эчтәлекне кодлау хатаcы</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Күрәсегез килгән сәхифәне күрсәтеп булмый, чөнки ул кулланган компрессия ысулы я хаталы, я браузер аны танымый.</p>
+ <ul>
+ <li>Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Адрес табылмады</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Интернетка тоташу юк</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Челтәргә бәйләнешегезне тикшерегез яки бераздан битне яңадан йөкләп карагыз.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Яңарту</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Яраксыз адрес</string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Адрес хаталы</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Веб адреслары гадәттә түбәндәгечә языла — <strong>http://www.example.com/</strong></li>
+ <li>Адресларда сулга түгел, ә бәлки уңга авыш бүлү билгесен кулланасы (ягъни бу билге: <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Билгесез протокол</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Файл табылмады</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Файлдан файдалануга рөхсәт бирелмәде</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Прокси-сервер бәйләнештән бар тартты</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Прокси-сервер табылмады</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Зыянлы программалы сайт</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Кирәкмәс сайт</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Зыянлы сайт</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Ялган сайт</string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Хәвефсез сайт юк</string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP сайтка үтү</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..8534f051a4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Arem daɣ</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Ur teẓḍired ad tsemded asuter </string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Ur tettyafa tansa</string>
+
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Smiren</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Abrutukul arussin</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Ur ittyafa ufaylu</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Anekcum ɣer ufaylu yegdel</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..551fa14087
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ug/strings.xml
@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">قايتا سىناڭ</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">تەلەپنى تاماملىيالمايدۇ</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>ھازىرچە بۇ مەسىلە ياكى خاتالىق ھەققىدە تەپسىلىي ھەل قىلىش چارىسى يوق</p>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">بىخەتەر ئۇلىنىش مەغلۇپ بولدى</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>سىز كۆرمەكچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ ، چۈنكى تاپشۇرۇۋالغان سانلىق مەلۇماتلارنىڭ راست-يالغانلىقىنى تەكشۈرگىلى بولمايدۇ.</li>
+ <li>توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ، ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">بىخەتەر ئۇلىنىش مەغلۇپ بولدى</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>بۇ مۇلازىمىتىرنىڭ سەپلىمىسىدە مەسىلە بولغانلىقى، ياكى نامەلۈم كىشلەرنىڭ مۇلازىمىتىرنى دوراشقا ئۇرۇنغانلىق سەۋەبلىك بولۇشى مۇمكىن.</li>
+ <li>ئەگەر سىز بۇرۇن بۇ مۇلازىمېتىرغا مۇۋەپپەقىيەتلىك ئۇلىنالىغان بولسىڭىز، خاتالىق ۋاقىتلىق بولۇشى مۇمكىن، شۇنداقلا قايتا سىناپ باقسىڭىز بولىدۇ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">ئالىي…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>بەزىلەر تور بېكەتنى تەقلىد قىلىشقا ئۇرۇنغان بولۇشى مۇمكىن، سىز داۋاملاشتۇرماسلىقىڭىز كېرەك.</label>
+ <br> <br>
+ <label> تور بېكەتلەر گۇۋاھنامە ئارقىلىق ئۆزىنىڭ كىملىكىنى ئىسپاتلايدۇ. %1$s <b>%2$s</b> غا ئىشەنمەيدۇ ، چۈنكى ئۇنىڭ گۇۋاھنامىسىنى تارقاتقۇچى نامەلۇم، گۇۋاھنامىگە ئۆزى ئىمزا قويغان ياكى مۇلازىمېتىر توغرا بولغان ئارىلىق گۇۋاھنامىنى ئەۋەتمىگەن. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">قايتىش (تەۋسىيە)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">خەتەرنى قوبۇل قىلىپ داۋاملاشتۇرۇش</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">بۇ تور بېكەت بىخەتەر ئۇلىنىشنى تەلەپ قىلىدۇ.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>سىز زىيارەت قىلماقچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ، چۈنكى بۇ تور بېكەت بىخەتەر ئۇلىنىشنى تەلەپ قىلىدۇ.</li>
+ <li>مەسىلە تور بېكەتتە بولۇشى مۇمكىن، ئۇنى سىز ھەل قىلالمايسىز.</li>
+ <li>مەسىلىنى تور بېكەت باشقۇرغۇچىسىغا ئۇقتۇرۇپ قويالايسىز.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">ئالىي…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label><b>%1$s</b>نىڭ HTTP قاتتىق قاتناش بىخەتەرلىكى (HSTS) دەپ ئاتىلىدىغان بىخەتەرلىك سىياسىتى بار ، يەنى <b>%2$s </b> پەقەت بىخەتەر ئۇلىنالايدۇ. بۇ تور بېكەتنى زىيارەت قىلىش ئۈچۈن مۇستەسنا قوشالمايسىز. </label>
+
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">قايتىش</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">ئۇلىنىش ئۈزۈلۈپ قالدى</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>توركۆرگۈچ مۇۋەپپەقىيەتلىك ئۇلاندى ، ئەمما ئۇچۇر يوللىغاندا ئۇلىنىش ئۈزۈلۈپ قالدى. قايتا سىناڭ.</p>
+ <ul>
+ <li>تور بېكەتنى ۋاقتىنچە زىيارەت قىلغىلى بولمايدۇ ياكى مۇلازىمىتىر ئالدىراش. بىر نەچچە مىنۇتتىن كېيىن قايتا سىناڭ.</li>
+ <li>ئەگەر سىز ھېچقانداق بەتنى يۈكلىيەلمىسىڭىز ، ئۈسكۈنىڭىزنىڭ تور ئۇلىنىشىنى تەكشۈرۈپ بىقىڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">ۋاقتىدا ئۇلىنالمىدى</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>ئىلتىماس قىلىنغان تور بېكەت ئۇلىنىش تەلىپىگە جاۋاب قايتۇرمىغاچقا توركۆرگۈ جاۋاب كۈتۈشنى توختاتتى.</p>
+ <ul>
+ <li> تور بېكەتنىڭ زىيارەتچىسى زىيادە كۆپ بولۇپ مۇلازىمېتىردا توسۇلۇش يۈز بەرگەن بولۇشى مۇمكىنمۇ؟ سەل تۇرۇپ قايتا سىناڭ.</li>
+ <li> باشقا تور بېكەتلەرنىمۇ كۆرەلمەيۋاتامسىز؟ ئۈسكۈنىنىڭ تور ئۇلىنىشىنى تەكشۈرۈڭ. </li>
+ <li> ئۈسكۈنىڭىز ياكى تورىڭىز مۇداپىئە تام ياكى ۋاكالەتچى تەرىپىدىن قوغدىلامدۇ؟ خاتا تەڭشەكلەر تور زىيارىتىگە دەخلى قىلىدۇ.</li>
+ <li> يەنىلا ھەل بولمىدىمۇ؟ تور باشقۇرغۇچى ياكى ئىنتېرنېت مۇلازىمىتى تەمىنلىگۈچى بىلەن ئالاقىلىشىپ ياردەم سوراڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">ئۇلىنالمىدى</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>تور بېكەتنى ۋاقتىنچە ئىشلەتكىلى بولمايدۇ ياكى مۇلازىمىتىر ئالدىراش بولۇشى مۇمكىن. سەل تۇرۇپ قايتا سىناڭ.</li>
+ <li>ئەگەر ھېچقانداق بەتنى يۈكلىيەلمىسىڭىز، ئۈسكۈنىڭىزنىڭ سانلىق مەلۇمات تورى ياكى Wi-Fi ئۇلىنىشىنى تەكشۈرۈڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">كۈتۈلمىگەن مۇلازىمېتىر ئىنكاسى</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>تور بېكەتنىڭ تور تەلىپىگە قايتۇرغان ئىنكاسى مۆلچەردىكىگە ئۇيغۇن ئەمەس، تور كۆرگۈچ داۋاملاشتۇرۇشقا ئامالسىز.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">بۇ بەت توغرا قايتا نىشانلىيالمىدى</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p> توركۆرگۈ تەلەپ قىلىنغان تۈرنى ئەسلىگە كەلتۈرۈشنى توختاتتى. تور بېكەت تەلەپنى ھەرگىز تاماملىمايدىغان ئۇسۇلدا قايتا نىشانلايدۇ. </p>
+ <ul>
+ <li> بۇ تور بېكەت تەلەپ قىلغان ساقلانمىلارنى چەكلەپ ياكى توستىڭىزمۇ؟ </li>
+ <li> ئەگەر تور بېكەتنىڭ ساقلانما تەلىپىگە قوشۇلۇپمۇ مەسىلە ھەل بولمىسا، ئۇ بەلكىم مۇلازىمېتىر سەپلەش مەسىلىسى بولۇشى مۇمكىن ، ئۈسكۈنىڭىز ئەمەس. </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">تورسىز ھالەت</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>تور كۆرگۈچ تورسىز ھالەتتە، تەلەپ قىلغان تۈرگە ئۇلىنالمايدۇ.</p>
+ <ul>
+ <li>ئۈسكۈنە ئاكتىپ تورغا ئۇلانغانمۇ؟</li>
+ <li> “قايتا سىناش” نى بېسىپ ئاكتىپ ھالەتكە ئالماشتۇرۇڭ ۋە بەتنى قايتا يۈكلەڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">بىخەتەرلىك سەۋەبىدىن پورت چەكلەنگەن</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p> تەلەپ قىلىنغان ئادرېس تورنى زىيارەت قىلىشتىن <em>باشقا</em> مەقسەتلەردە ئىشلىتىلىدىغان ئېغىزنى (مەسىلەن، mozilla.org دىكى 80-نومۇرلۇق ئېغىز <q>mozilla.org:80</q>) ئۈچۈن بەلگىلىدى. توركۆرگۈ بىخەتەرلىكىڭىزنى قوغداش ئۈچۈن ئىلتىماسنى بىكار قىلدى. </p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">ئۇلىنىش ئەسلىگە كەلتۈرۈلدى</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>باغلىنىش ھەققىدە سۆھبەتلىشىۋاتقاندا تور ئۇلىنىشى ئۈزۈلۈپ قالدى. قايتا سىناڭ.</p>
+ <ul>
+ <li>تور بېكەتنى ۋاقتىنچە زىيارەت قىلغىلى بولمايدۇ ياكى مۇلازىمىتىر ئالدىراش. بىر نەچچە مىنۇتتىن كېيىن قايتا سىناڭ.</li>
+ <li>ئەگەر سىز ھېچقانداق بەتنى يۈكلىيەلمىسىڭىز، ئۈسكۈنىڭىزنىڭ كۆچمە تورى ياكى Wi-Fi باغلىنىشىنى تەكشۈرۈپ بىقىڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">خەتەرلىك ھۆججەت تىپى</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ، ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">بۇزۇلغان مەزمۇن خاتالىقى</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p> سىز كۆرمەكچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ چۈنكى سانلىق مەلۇمات يوللاشتا خاتالىق بايقالدى.</p>
+ <ul>
+ <li> توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">مەزمۇن بۇزۇلغان</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p> سىز كۆرمەكچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ چۈنكى سانلىق مەلۇمات يوللاشتا خاتالىق بايقالدى.</p>
+ <ul>
+ <li> توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">مەزمۇن كودلاش خاتالىقى</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p> سىز كۆرمەكچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ ، چۈنكى ئۇ ئىناۋەتسىز ياكى قوللىمايدىغان پىرىسلاش شەكلىنى قوللىنىدۇ. </p>
+ <ul>
+ <li> توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ، ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ. </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">ئادرېس تېپىلمىدى</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p> توركۆرگۈ تەمىنلەنگەن ئادرېسنىڭ مۇلازىمېتىرىنى تاپالمىدى.</p>
+ <ul>
+ <li>خاتا كىرگۈزگەن ئادرېس
+ <strong> ww </strong> .example.com نىڭ ئورنىغا
+ <strong> www </strong> .example.com. </li> نى كىرگۈزۈڭ
+ <li> ئەگەر ھېچقانداق بەتنى يۈكلىيەلمىسىڭىز، ئۈسكۈنىڭىزنىڭ كۆچمە تورى ياكى Wi-Fi باغلىنىشىنى تەكشۈرۈڭ. </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">تورغا ئۇلانمىغان</string>
+
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">تور ئۇلىنىشىڭىزنى تەكشۈرۈڭ ياكى بىر نەچچە مىنۇتتىن كېيىن بەتنى قايتا يۈكلەڭ.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">قايتا يۈكلە</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ئىناۋەتسىز ئادرېس</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>تەمىنلەنگەن ئادرېس تونۇيالايدىغان پىچىمدا ئەمەس. ئورۇن بالداقتىن خاتالىقنى تەكشۈرۈپ ئاندىن قايتا سىناڭ.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">ئادرېس ئىناۋەتسىز</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>تور ئادرېسى ئادەتتە مىسالدىكىدەك يېزىلىدۇ، مىسال- <strong>http://www.example.com/</strong></li>
+ <li>ئىشلەتكىنىڭىزنىڭ يانتۇ سېزىق ئىكەنلىكىنى جەزىملەشتۈرۈڭ.(مىسال. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">نامەلۇم كېلىشىم</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p> ئادرېس كېلىشىمنامە (مەسىلەن، <q>wxyz://</q>) سىدە بەلگىلەنگەننى توركۆرگۈ تونۇمىدى، شۇڭا توركۆرگۈ تور بېكەتكە توغرا ئۇلىنالمايدۇ. </p>
+ <ul>
+ <li> كۆپ ۋاسىتە ياكى باشقا تېكىستسىز مۇلازىمەتنى زىيارەت قىلامسىز؟ تور بېكەتنىڭ قوشۇمچە تەلىپىنى تەكشۈرۈڭ.</li>
+ <li> بەزى كېلىشىملەر توركۆرگۈ ئۇلارنى تونۇشتىن بۇرۇن ئۈچىنچى تەرەپ يۇمشاق دېتالى ياكى قىستۇرما تەلەپ قىلىشى مۇمكىن.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">ھۆججەت تېپىلمىدى</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li> بۇ تۈر نامى ئۆزگەرتىلگەن، ئۆچۈرۈلگەن ياكى كۆچۈرۈلگەنمۇ؟ </li>
+ <li> ئادرېسنىڭ ئىملاسىدا ياكى چوڭ-كىچىك يېزىلىشى ياكى باشقا يېزىق خاتالىقى بارمۇ؟ </li>
+ <li> تەلەپ قىلىنغان تۈرنى زىيارەت قىلىش ئۈچۈن يېتەرلىك ھوقۇقىڭىز بارمۇ؟ </li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">ھۆججەتنى زىيارەت قىلىش رەت قىلىندى</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>ھۆججەت يۇيۇلغان ياكى يۆتكەلگەن ۋە ياكى زىيارەت ھوقۇقى يوق بولۇشى مۇمكىن</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">ۋاكالەتچى مۇلازىمېتىر ئۇلىنىشنى رەت قىلدى</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p> توركۆرگۈ ۋاكالەتچى مۇلازىمېتىر ئىشلىتىشكە سەپلەنگەن، ئەمما ۋاكالەتچى ئۇلىنىشنى رەت قىلدى.</p>
+ <ul>
+ <li> توركۆرگۈنىڭ ۋاكالەتچى سەپلىمىسى توغرىمۇ؟ تەڭشەكلەرنى تەكشۈرۈپ قايتا سىناڭ.</li>
+ <li> ۋاكالەتچى مۇلازىمىتى بۇ توردىن ئۇلىنىشقا يول قويامدۇ؟</li>
+ <li> يەنىلا مەسىلە بارمۇ؟ ياردەمگە ئېرىشىش ئۈچۈن تور باشقۇرغۇچىڭىز ياكى تور مۇلازىمىتى تەمىنلىگۈچى بىلەن ئالاقىلىشىڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">ۋاكالەتچى مۇلازىمېتىر تېپىلمىدى</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p> توركۆرگۈ ۋاكالەتچى مۇلازىمېتىر ئىشلىتىشكە سەپلەنگەن، ئەمما ۋاكالەتچىنى تاپالمىدى.</p>
+ <ul>
+ <li> توركۆرگۈنىڭ ۋاكالەتچى سەپلىمىسى توغرىمۇ؟ تەڭشەكلەرنى تەكشۈرۈپ قايتا سىناڭ.</li>
+ <li>ئۈسكۈنە ئاكتىپ تورغا ئۇلانغانمۇ؟</li>
+ <li> يەنىلا مەسىلە بارمۇ؟ ياردەمگە ئېرىشىش ئۈچۈن تور باشقۇرغۇچىڭىز ياكى تور مۇلازىمىتى تەمىنلىگۈچى بىلەن ئالاقىلىشىڭ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">يامان غەرەزلىك تور بېكەت مەسىلىسى</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>%1$s دىكى تور بېكەت ھۇجۇم تور بېكىتى دەپ خەۋەر قىلىنغان ۋە بىخەتەرلىك مايىللىقىڭىزغا ئاساسەن چەكلەنگەن. </p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">كېرەكسىز بېكەت مەسىلىسى</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s دىكى تور بېكەتنىڭ لۈكچەك يۇمتاللارغا مۇلازىمەت قىلىدىغانلىقى مەلۇم بولدى ۋە بىخەتەرلىك مايىللىقىڭىزغا ئاساسەن توسۇۋېتىلدى. </p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">زىيانلىق تور بېكەت مەسىلىسى</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s دىكى تور بېكەتنىڭ يۇشۇرۇن زىيانلىق تور بېكەت ئىكەنلىكى مەلۇم قىلىنغانلىقى ئۈچۈن بىخەتەرلىك مايىللىقىڭىزغا ئاساسەن توسۇۋېتىلدى. </p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">ئالدامچىلىق تور بېكىتى مەسىلىسى</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s دىكى بۇ تور بېكەت ئالدامچىلىق تور بېكىتى دەپ دوكلات قىلىنغان ۋە بىخەتەرلىك مايىللىقىڭىزغا ئاساسەن چەكلەنگەن. </p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">بىخەتەر تور بېكەتنى ئىشلەتكىلى بولمايدۇ</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[بىخەتەرلىكنى كۈچەيتىش ئۈچۈن پەقەت HTTPS-لا ھالىتىنى قوزغاتتىڭىز، <em>%1$s</em> نىڭ HTTPS نەشرىنى ئىشلەتكىلى بولمايدۇ.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP تور بېتىنى داۋاملاشتۇرۇش</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..52a99db4ee
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-uk/strings.xml
@@ -0,0 +1,310 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Спробувати знову</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Неможливо виконати запит</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Подробиці про цю проблему чи помилку наразі недоступні.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Не вдалося встановити безпечне з’єднання</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Неможливо відобразити сторінку, яку ви намагаєтесь переглянути, тому що не вдається перевірити справжність отриманих даних.</li>
+ <li>Будь ласка, зв’яжіться з власниками вебсайту, щоб повідомити їх про цю проблему.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Не вдалося встановити безпечне з’єднання</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Це може бути проблемою конфігурації сервера, або, можливо, хтось намагається імітувати сервер.</li>
+ <li>Якщо раніше ви успішно отримували доступ до цього сервера, то це може бути тимчасовою помилкою і ви можете спробувати ще раз пізніше.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Додатково…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Хтось може намагатися видати себе за сайт, тому вас не слід продовжувати.</label>
+ <br><br>
+ <label>Вебсайти засвідчують свою справжність за допомогою сертифікатів. %1$s не довіряє <b>%2$s</b>, тому що видавець його сертифіката невідомий, сертифікат самопідписаний, або сервер не надсилає правильні посередницькі сертифікати</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Назад (Рекомендовано)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Погодитись на ризик і продовжити</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Цей вебсайт вимагає захищеного з’єднання.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Сторінку, яку ви намагаєтеся переглянути, неможливо показати, оскільки для цього вебсайту потрібне захищене з’єднання.</li>
+ <li>Імовірно, проблема пов’язана з вебсайтом, і ви нічого не можете зробити, щоб усунути ваду.</li>
+ <li>Ви можете повідомити адміністратора вебсайту про проблему.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Додатково…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> має політику безпеки під назвою HTTP Strict Transport Security (HSTS), що означає, що <b>%2$s</b> може підключитися до нього лише безпечно. Ви не можете додати виняток для відвідування цього сайту. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Назад</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">З’єднання перервано</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Браузер успішно підключився, але при передачі інформації з’єднання було перервано. Будь ласка, спробуйте знову.</p>
+ <ul>
+ <li>Сайт може бути тимчасово недоступним, або перевантаженим запитами. Спробуйте знову через кілька хвилин.</li>
+ <li>Якщо вам не вдається завантажити жодну сторінку, перевірте з’єднання з Інтернетом свого пристрою.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Перевищено термін очікування з’єднання</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Завершився час очікування відповіді під час спроби з’єднання з сайтом.</p>
+ <ul>
+ <li>Можливо, сайт тимчасово недоступний, або перевантажений запитами. Спробуйте знову пізніше.</li>
+ <li>Якщо жодна сторінка не завантажується, перевірте з’єднання свого пристрою з Інтернетом.</li>
+ <li>Якщо ваш комп’ютер чи мережа захищені мережевим екраном чи проксі-сервером, переконайтеся, що для браузера дозволено доступ до Інтернету.</li>
+ <li>Якщо проблема не зникає, проконсультуйтесь зі своїм системним адміністратором чи інтернет-провайдером.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Неможливо з’єднатися</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Сайт може бути тимчасово недоступний або перевантажений. Спробуйте знову через деякий час.</li>
+ <li>Якщо ви не можете завантажити жодної сторінки, перевірте з’єднання вашого пристрою з мобільною або Wi-Fi мережею.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Неочікувана відповідь сервера</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Сайт надав неочікувану відповідь на мережевий запит і браузер не може його обробити.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Неналежне перенаправлення на сторінці</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Браузер припинив спроби завантажити запитаний елемент. Сайт переспрямовує запит так, що він ніколи не завершиться.</p>
+ <ul>
+ <li>Можливо, ви вимкнули чи заблокували файли cookie для цього сайту.</li>
+ <li>Якщо дозвіл на прийняття файлів cookie сайту не розв’язує проблему, ймовірно, це спричинено конфігурацією сервера, а не вашим пристроєм.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Автономний режим</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Браузер працює в автономному режимі і не може зв’язатись із запитаним ресурсом.</p>
+ <ul>
+ <li>Перевірте, чи ваш комп’ютер підключений до активної мережі.</li>
+ <li>Натисніть “Спробувати знову”, щоб перемкнутись в онлайновий режим і перезавантажити сторінку.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Порт заборонений з міркувань безпеки</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Для запитаної адреси вказано порт (наприклад, <q>mozilla.org:80</q> означає порт 80 на mozilla.org), який зазвичай <em>не використовується</em> для роботи з вебсайтами. Браузер скасував запит для вашої безпеки.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">З’єднання скинуто</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Зв’язок з сайтом був перерваний при встановленні з’єднання. Будь ласка, спробуйте знову.</p>
+ <ul>
+ <li>Сайт може бути тимчасово недоступним, або перевантаженим запитами. Спробуйте знову через кілька хвилин.</li>
+ <li>Якщо вам не вдається завантажити жодну сторінку, перевірте з’єднання з Інтернетом свого пристрою.</li>
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Небезпечний тип файлу</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Будь ласка, зв’яжіться з власниками вебсайту і повідомте їх про цю проблему.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Помилка пошкодженого вмісту</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Не вдається відобразити сторінку, яку ви намагаєтесь переглянути, тому що було виявлено помилку в передачі даних.</p>
+ <ul>
+ <li>Будь ласка, зв’яжіться з власниками вебсайту та повідомте їх про цю проблему.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Вміст пошкоджено</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Не вдається відобразити сторінку, яку ви намагаєтесь переглянути, тому що було виявлено помилку в передачі даних.</p>
+ <ul>
+ <li>Будь ласка, зв’яжіться з власниками вебсайту та повідомте їх про цю проблему.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Помилка кодування вмісту</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Не вдається показати сторінку, яку ви намагаєтеся переглянути, оскільки вона використовує неправильну чи непідтримувану форму стиснення даних.</p>
+ <ul>
+ <li>Будь ласка, повідомте власника сайту про цю проблему.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Адресу не знайдено</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Браузер не зміг знайти сервер за вказаною адресою.</p>
+ <ul>
+ <li>Перевірте правильність введення адреси, наприклад,
+ <strong>ww</strong>.example.com замість
+ <strong>www</strong>.example.com.</li>
+ <li>Якщо вам не вдається завантажити жодну сторінку, перевірте з’єднання вашого пристрою з Інтернетом.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Немає з’єднання з Інтернетом</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Перевірте з’єднання з Інтернетом або спробуйте перезавантажити сторінку через кілька хвилин.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Перезавантажити</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Недійсна адреса</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Неможливо розпізнати формат вказаної адреси. Перевірте правильність введення адреси в панелі та спробуйте знову.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Недійсна адреса</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Зазвичай, веб-адреса має такий вигляд <strong>http://www.example.com/</strong></li>
+ <li>Переконайтеся, що ви використовуєте дробову риску (тобто <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Невідомий протокол</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Адреса містить невідомий браузеру протокол, (наприклад, <q>wxyz://</q>), тому браузер не може встановити з’єднання з сайтом.</p>
+ <ul>
+ <li>Можливо, ви намагаєтесь отримати доступ до мультимедіа чи інших сервісів? Перевірте сайт на наявність особливих вимог.</li>
+ <li>Деякі протоколи можуть потребувати додаткових сторонніх програм або плагінів, перш ніж браузер зможе з ними працювати.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Файл не знайдено</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Перевірте, чи файл не був перейменований, видалений чи переміщений.</li>
+ <li>Переконайтеся, що адреса не містить помилок введення.</li>
+ <li>Переконайтеся, що у вас є дозвіл на доступ до запитаного елемента.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Доступ до файлу було заборонено</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Він міг бути вилучений, переміщений або заборонено доступ до файлу.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Проксі-сервер відмовляється приймати з’єднання</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Браузер налаштовано на використання проксі-сервера, але проксі відхилив з’єднання.</p>
+ <ul>
+ <li>Перевірте правильність конфігурації проксі та спробуйте знову.</li>
+ <li>Перевірте, чи дозволяє сервіс проксі з’єднання з цієї мережі.</li>
+ <li>Все ще є проблеми? Проконсультуйтесь зі своїм системним адміністратором чи інтернет-провайдером.</li>
+ </ul>
+]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Проксі-сервер не знайдено</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Браузер налаштований для використання сервера проксі, але проксі не вдалося знайти.</p>
+ <ul>
+ <li>Перевірте правильність конфігурації проксі та спробуйте знову.</li>
+ <li>Перевірте, чи комп’ютер підключений до активної мережі.</li>
+ <li>Все ще є проблеми? Проконсультуйтесь зі своїм системним адміністратором чи Інтернет-провайдером для отримання допомоги.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Зловмисний сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Сайт %1$s відомий як зловмисний, і тому був заблокований згідно з вашими налаштуваннями безпеки.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Небажаний сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Сайт %1$s відомий як такий, що розповсюджує небажане програмне забезпечення, і тому був заблокований згідно з вашими налаштуваннями безпеки.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Небезпечний сайт</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Сайт %1$s відомий, як потенційно небезпечний, і був заблокований згідно з вашими налаштуваннями безпеки.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Шахрайський сайт</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Сайт %1$s відомий, як шахрайський, і був заблокований згідно з вашими налаштуваннями безпеки.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Захищений сайт недоступний</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Ви увімкнули HTTPS-режим для поліпшення безпеки, але HTTPS версія для <em>%1$s</em> недоступна.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Продовжити на HTTP-сайті</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..cd2bf3a206
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-ur/strings.xml
@@ -0,0 +1,204 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">دوبارہ کوشش کریں</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">درخواست مکمل نہیں کی جا سکتی</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>اس مسئلے کے متعلق اضافی معلومات فی الحال دستیاب نہیں ہے۔</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">قابل بھروسا کنکشن ناکام ہو گیا ہے</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>جو صفحہ آپ دیکھنا چاہ رہے ہیں، دکھایا نہیں جا سکتا کیونکہ موصول قوائف کی اصلیت کی تصدیق نہیں ہو سکی۔</li>
+ <li>ویب سائٹ کے مالک کو اس مسلے کا بتانے کے لیے ان سے رابطہ کریں۔</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">قابل بھروسا کنکشن ناکام ہو گیا ہے</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>یہ سرور کانفیگوریشن کا کوئی مسلہ ہو سکتا ہے یا ہو سکتا ہے کی کوئی سرور کی نکل کرنے کی کوشش کر رہا ہو۔</li>
+ <li>اگر آپ ماضی میں اس سرور سے جڑیں ہیں تو ہو سکتا ہے خرابی کچھ وقت کے لئے ہو اور آپ کچھ دیر میں پھر سے کوشش کر سکتے ہیں۔</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">اعلٰی…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[<label>ہو سکتا ہے کی کوئی سائٹ کی نقل بنا رہا ہو اور آپ کو آگے نہیں بڑھنی چاہیے۔</label>
+ <br><br>
+ <label>ویب سائٹس سرٹیفکیٹ کے ذریعہ اپنی سناخت ثابت کرتی ہیں۔ %1$s، اس <b>%2$s</b> پر بھروسہ نہیں کرتا کیونکہ سرٹیفکیٹ جاری کرنے والا نامعلوم ہے، سرٹیفکیٹ پر خود کے دستخط کی ہوئی ہے یا سرور صحیح درمیانی سرٹیفیکیٹ نہیں بھیج رہا ہے۔</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">واپس جائیں (تجویز شدہ)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">خطرے کو قبول کریں اور جاری رکھیں</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">کنکشن خراب ہو گیا</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>براؤزر کامیابی کے ساتھ جڑ چکا ہے، لیکن معلومات کی منتقلی کے دوران کنکشن میں رکاوٹ پیدا ہوئی۔ براہ کرمپھر سے کوشش کریں۔</p>
+ <ul>
+ <li>سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف ہو سکتی ہے۔ کچھ دیر بعد پھر سے کوشش کریں۔</li>
+ <li>اگر آپ کوئی بھی صفحات لوڈ نہیں کر پا رہے ہیں، تو اپنے آلے کی ڈیٹا یا وائی-فائی کنیکشن کی جانچ کریں۔ </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">کنکشن ٹائم آوٹ ہو گیا ہے</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>درخواست کی گئی سائٹ نے کنکشن کی التجا کا جواب نہیں دیا اور براؤزر نے جواب کا انتظار کرنا بند کر دیا ہے۔</p>
+ <ul>
+ <li>ہو سکتا ہے کی سرور زیادہ مانگ یا عارضی وقفے کا سامنا کر رہا ہو؟ بعد میں پھر سے کوشش کریں۔</li>
+ <li> کیا آپ دوسرے سائٹ بھی براؤز نہیں کر پا رہے ہیں؟ آلے کی نیٹ ورک کنکشن کی جانچ کریں۔</li>
+ <li>کیا آپ کا آلہ یا نیٹ ورک کسی فائروال یا پراکسی کے ذریعے محفوظ ہے؟غلط سیٹنگ ویب براؤز کرنے میں مداخلت کر سکتی ہے۔</li>
+ <li>ابھی بھی مسئلہ درپیش ہے؟ مدد کے لئے اپنے نیٹ ورک کے نگراں یا انٹرنیٹ مہیا کار سے رابطہ کریں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">جڑنے میں ناکام رہا</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف ہو سکتی ہے۔ کچھ دیر بعد پھر سے کوشش کریں۔</li>
+ <li>اگر آپ کوئی بھی صفحات لوڈ نہیں کر پا رہے ہیں، تو اپنے آلے کی ڈیٹا یا وائی-فائی کنکشن کی جانچ کریں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">سرور کی طرف سے غیر متوقع جواب</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>سائٹ نے نیٹ ورک فرمائش کا ایک غیر متوقع طریقے سے جواب دیا اور براؤزر جاری نہیں رہ سکتا۔</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">صفحہ ٹھیک طرح ری ڈائریکٹ نہیں ہو رہا</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>براؤزر نے درخواست کی گئی چیز کو پانے کی کوشش روک کر دی ہے۔ سائٹ اس درخواست کو اس طرح سے دوسری سمت بھیج رہا ہے جو کبھی مکمل نہیں ہوگی۔</p>
+ <ul>
+ <li>کیا اپ نے سائٹ کے لئے ضروری کوکیز کو معزور یا نا اہل کی ہے؟</li>
+ <li>اگر سائٹ کی کوکیز کو قبول کرنے سے بھی مسئلہ ٹھیک نہیں ہوتا، تو ممکن ہے کہ یہ سرور کنفگریشن کا کوئی مسئلہ ہو نا کہ آپکے آلے کا۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">آف لائن موڈ</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>براؤزر اپنے آف لائن موڈ میں عمل کاری ہے اور درخواست کی گئی چیز سے نہیں جڑ سکتا۔
+</p>
+ <ul>
+ <li>کیا آلہ ایک زیر عمل نیٹ ورک سے جڑا ہوا ہے؟</li>
+ <li>آن لائن موڈ میں لانے کے لئے “دوبارہ کوشش کریں” دبایں اور صفہ کو فر سے لوڈ کریں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">سلامتی وجوہات کے لیے دہانہ محدود کر دیا گیا</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p>درخواست کیا گیا پتا ایک خاص پورٹ (مثلاً، mozilla.org پر پورٹ 80 کے لئے <q>mozilla.org:80</q>)
+ کو مخصوص کرتا ہے جو عام طور پر ویب براؤز کرنے کے علاوہ <em>کچھ اور</em> مقاصد کے لئے استعمال ہوتا ہے۔ آپ کی حفاظت اور سلامتی کے لئے براؤزر نے درخواست کو منسوخ کر دی ہے۔</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">کنکشن ریسٹ ہو گیا</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>کنکشن کو قائم کرنے کے دوران نیٹ ورک لنک میں رکاوٹ پیدا ہوئی۔ براہ کرم دوبارہ کوشش کریں۔</p>
+ <ul>
+ <li>سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف ہو سکتی ہے۔ کچھ دیر بعد دوبارہ کوشش کریں۔</li>
+ <li>اگر آپ کوئی بھی صفحات لوڈ نہیں کر پا رہے ہیں، تو اپنے آلے کی ڈیٹا یا وائی-فائی کنکشن کی جانچ کریں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">غیر محفوظ فائل قسم</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>ویب سائٹ کے مالکین کو اس مسلے کا بتانے کے لیے رابطہ کریں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">خراب مواد نقص</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>آپ جس صفہ کو دیکھنا چاہتے ہیں اسے دکھایا نہیں جا سکتا کیونکہ ڈیٹا ترسیل میں ایک خرابی کی جانکاری ملی ہے۔</p>
+ <ul>
+ <li>براہ کرم ویب سائٹ کے مالکان کو اس مسلے کا بتانے کے لیے ان سے رابطہ کریں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">مواد تباہ ہو گیا</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>آپ جس صفہ کو دیکھنا چاہتے ہیں اسے دکھایا نہیں جا سکتا کیونکہ ڈیٹا ترسیل میں ایک خرابی کی جانکاری ملی ہے۔</p>
+ <ul>
+ <li>براہ کرم ویب سائٹ کے مالکان کو اس مسلے کا بتانے کے لیے ان سے رابطہ کریں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">مواد انکوڈنگ نقص</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>آپ جس صفہ کو دیکھنا چاہتے ہیں اسے دکھایا نہیں جا سکتا کیونکہ یہ ایک غیر موثر یا غیر معاون قسم کی کمپریشن کا استعمال کرتا ہے۔</p>
+ <ul>
+ <li>براہ کرم ویب سائٹ کے مالکان کو اس مسلے کا بتانے کے لیے ان سے رابطہ کریں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">پتہ نہیں ملا</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">کوئی انٹرنیٹ کنکشن نہیں</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">اپنے نیٹ ورک کنکشن کی پڑتال کریں یا چند لمحات بعد دوبارہ لوڈ کرنے کی کوشش کریں۔</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">پھر لوڈ کریں</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">ناجائز پتہ</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>مہیا کیا گیا پتہ ایک پہچان کردہ وضع میں نہیں ہے۔ برائے مہربانى غلطیوں کے لئے محل وقوع بار کی پڑتال کریں اور دوبارہ کوشش کریں۔</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">پتہ جائز نہیں ہے</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>ویب پتے عمومی طور پر ایسے ہی <strong>http://www.example.com/</strong> لکھے جاتے ہیں </li>
+ <li>یقینی بنائیں کہ آپ فارورڈ سلیشیز (جیسے <strong>/</strong>) کا استعمال کر رہے ہیں۔</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">نامعلوم پروٹوکول</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">مسل نہیں ملی</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">فائل تک رسائی مسترد کردی گئی ہے</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">پراکسی سرور نے کنکشن سے انکار کردیا</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">پراکسی سرور نہیں ملا</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">میلویئر سائٹ مسئلہ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">ناپسندیدہ سائٹ مسئلہ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">نقصان دہ سائٹ کا مسئلہ</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">فریب دہ سائٹ کا مسئلہ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..8b3b850406
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-uz/strings.xml
@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Qayta urining</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Soʻrov bajarilmadi</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Bu muammo yoki xato haqida qoʻshimcha maʼlumot hozirda mavjud emas.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Xavfsiz ulanmadi</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>Siz koʻrmoqchi boʻlgan sahifani koʻrsatib boʻlmaydi, chunki olingan maʼlumotlarning haqiqiyligini tekshirib boʻlmadi.</li>
+ <li>Iltimos, ushbu muammo haqida veb-sayt egalariga murojaat qiling.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Xavfsiz ulanish amalga oshmadi</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Bu server konfiguratsiyasida muammo boʻlishi mumkin yoki kimdir oʻzini serverga oʻxshatmoqchi boʻlgan boʻlishi mumkin.</li>
+ <li>Agar ilgari ushbu serverga muvaffaqiyatli ulangan boʻlsangiz, xato vaqtinchalik boʻlishi mumkin va keyinroq qayta urinib koʻrishingiz mumkin.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Qoʻshimcha…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Kimdir saytdan notoʻgʻri maqsadda foydalanmoqchi boʻlishi mumkin va siz davom ettirmasligingiz kerak.</label>
+ <br><br>
+ <label>Veb-saytlar oʻzlarining shaxsiy maʼlumotlarini sertifikatlar orqali tasdiqlashadi. %1$sga ishonmaydi<b>%2$s</b> chunki uning sertifikat beruvchisi nomaʼlum, sertifikat oʻz-oʻzidan imzolangan yoki server toʻgʻri sertifikatlarni yubormayapti.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Orqaga qaytish (Tavsiya etiladi)</string>
+
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Xavfni oʻz zimmamga olaman va davom etaman</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Bu sayt xavfsiz ulanishni talab qiladi.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Siz ochmoqchi boʻlgan sayt ochilmaydi, chunki bu sayt xavfsiz ulanishni talab qiladi.</li>
+ <li>Muammo katta ehtimollik bilan saytda. Uni siz tuzata olmaysiz.</li>
+ <li>Muammo haqida sayt administratoriga xabar berishingiz mumkin.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Qoʻshimcha…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> HTTP Strict Transport Security (HSTS) deb nomlangan xavfsizlik siyosatiga ega, yaʼni <b>%2$s</b> unga faqat xavfsiz ulanishi mumkin. Bu saytga tashrif buyurish uchun istisno qoʻsha olmaysiz.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Orqaga qaytish</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Aloqa uzilib qoldi</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Brauzer muvaffaqiyatli bogʻlandi, lekin maʼlumot uzatish vaqtida ulanish toʻxtatildi. Iltimos, qayta urinib koʻring.</p>
+ <ul>
+ <li>Sayt vaqtincha ishlamay qolishi yoki band boʻlishi mumkin. Oz vaqtdan soʻng yana urinib koʻring.</li>
+ <li>Agar siz biron bir sahifani yuklay olmasangiz, qurilmangiz maʼlumotlarini yoki Wi-Fi ulanishini tekshiring.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Ulanish muddati tugadi</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Soʻralgan sayt ulanish soʻroviga javob bermadi va brauzer javob uchun kutishni toʻxtatdi.</p>
+ <ul>
+ <li>Server band yoki vaqtincha ishlamay qolishi mumkinmi? Keyinroq yana urinib koʻring.</li>
+ <li>Boshqa saytlarni koʻrib chiqa olmaysizmi? Qurilmaning tarmoqqa ulanishini tekshiring.</li>
+ <li>Qurilmangiz yoki tarmogʻingiz xavfsizlik devori yoki ishonchli server bilan himoyalanganmi? Notoʻgʻri sozlamalar veb-brauzerga xalaqit berishi mumkin.</li>
+ <li>Hali ham muammo bormi? Yordam uchun tarmoq administratori yoki Internet-provayderingiz bilan maslahatlashing.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Ulana olmadi</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Sayt vaqtincha ishlamay qolgan yoki juda band boʻlishi mumkin. Birozdan soʻng yana urinib koʻring.</li>
+ <li>Agar siz biron bir sahifani yuklay olmasangiz, qurilmangiz maʼlumotlarini yoki Wi-Fi ulanishini tekshiring</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Serverdan kutilmagan javob</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Sayt tarmoq soʻroviga kutilmagan usulda javob berdi, shuning uchun brauzer ishni davom ettira olmaydi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Sahifa toʻgʻri yoʻnaltirilmagan</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Brauzer soʻralgan elementni qayta tiklashga urinishni toʻxtatdi. Sayt soʻrovni hech qachon bajarilmaydigan tarzda yoʻnaltirmoqda.</p>
+ <ul>
+ <li>Ushbu sayt talab qiladigan kukilarni oʻchirib qoʻydingizmi yoki blokladingizmi?</li>
+ <li>Agar sayt kukilarini qabul qilish muammoni hal qilmasa, ehtimol bu sizning qurilmangiz emas, balki server konfiguratsiyasi muammosi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Oflayn rejim</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Brauzer oflayn rejimida ishlaydi va soʻralgan elementga ulana olmaydi.</p>
+ <ul>
+ <li>Qurilma faol tarmoqqa ulanganmi?</li>
+ <li>Onlayn rejimga oʻtish va sahifani qayta yuklash uchun "Qayta urinib koʻrish" tugmasini bosing.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port xavfsizlik sabablariga koʻra cheklangan</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+<p>Soʻralgan manzil uchun (masalan, mozilla.orgda 80 porti uchun <q>mozilla.org:80</q>) internetni koʻrishdan <em>boshqa</em> maqsadlar uchun foydalaniladigan port koʻrsatilgan. Brauzer himoya va xavfsizligingiz uchun soʻrovni bekor qildi.</p>
+]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ulanish uzilib qolgan</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Tarmoq havolasi bilan ulanish aloqasi uzilib qoldi. Iltimos, qayta urinib koʻring.</p>
+ <ul>
+ <li>Sayt vaqtincha ishlamay qolishi yoki juda band boʻlishi mumkin. Birozdan soʻng qayta urinib koʻring</li>
+ <li>Agar siz biron bir sahifafni yuklay olmasangiz, qurilmangiz maʼlumotlarini yoki Wi-Fi ulanishini tekshiring.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Xavfli fayl turi</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[<ul>
+ <li>Bu muammo haqida xabar berish uchun sayt egalari bilan bogʻlaning.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Tarkibi buzilgan</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Koʻrish uchun urinilayotgan sayt ochilmasligi mumkin, chunki maʼlumotlarni uzatishda xatolik aniqlandi.</p>
+ <ul>
+ <li>Bu muammo haqida xabar berish uchun sayt egalari bilan bogʻlaning.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Tarkib buzildi</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Siz koʻrmoqchi bo‘lgan sahifani koʻrsatib boʻlmaydi, chunki maʼlumotlarni uzatishda xatolik aniqlandi.</p>
+ <ul>
+ <li>Bu muammo haqida xabar berish uchun sayt egasi bilan bogʻlaning.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Xato siqish algoritmi</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Siz koʻrmoqchi boʻlgan sahifani yuklab boʻlmaydi, chunki u xato yoki mos kelmaydigan siqish usulidan foydalanadi.</p>
+ <ul>
+ <li>Bu muammo haqida xabar berish uchun veb sayt egalari bilan bogʻlaning.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Manzil topilmadi</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>Brauzer kiritilgan manzil uchun hosting serverini topa olmadi.</p>
+ <ul>
+ <li>Kiritilgan manzilni tekshiring, xato yoʻqmi? Masalan,
+ <strong>ww</strong>.example.com kiritilgan boʻlishi mumkin.
+ Aslida, <strong>www</strong>.example.com.</li> kiritilishi lozim.
+ <li>Agar birorta ham sahifa yuklanmasa, mobil internetni yoki Wi-Fi ulanishini tekshiring.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Internetga ulanmagan</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Tarmoqqa ulanishni tekshiring yoki bir necha daqiqadan soʻng sahifani yangilang.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Qayta yuklash</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Xato manzil</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[<p>Kiritilgan manzil aniqlab boʻlmaydigan formatda. Manzilni xatosiz kiritilganligini tekshiring va qaytadan urinib koʻring.</p>]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Manzil xato</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[<ul>
+ <li>Sayt manzillari odatda quyidagicha yoziladi: <strong>http://www.example.com/</strong></li>
+ <li>Yoʻnaltiruvchi qiya chiziq (slesh) toʻgʻri kiritilganligini tekshiring (masalan, <strong>/</strong>).</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Nomaʼlum protokol</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>Manzil brauzerga notanish boʻlgan protokol (masalan, <q>wxyz://</q>) bilan boshlanmoqda, shuning uchun brauzer saytga ulana olmaydi.</p>
+ <ul>
+ <li>Multimedia yoki matn boʻlmagan xizmatlari mavjud saytga ulanishga urinyapsizmi? Saytning qoʻshimcha dasturiy taʼminot boʻyicha talablari bilan tanishing.</li>
+ <li>Ayrim protokollar brauzer tanishi uchun begona dastur yoki plaginlarni talab qilishi mumkin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Fayl topilmadi</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[<ul>
+ <li>Faylning nomi oʻzgarmadimi, koʻchirilgani yoki oʻchirilgani yoʻqmi?</li>
+ <li>Manzilni yozishda imlo yoki katta-kichik harfda yozishda xato qilmadingizmi?</li>
+ <li>Soʻralgan fayldan foydalanish uchun sizga yetarlicha huquq berilganmi?</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Fayldan foydalanishga ruxsat berilmadi</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[<ul>
+ <li>U oʻchirilgan, koʻchirilgan yoki ruxsat olib tashlangan boʻlishi mumkin.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proksi server ulanishni rad etdi</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p>Brauzer proksi serverdan foydalanish uchun sozlangan, ammo brauzer ulanishni rad qildi.</p>
+ <ul>
+ <li>Brauzer proksi sozlamalari toʻgʻrimi? Tekshiring va qaytadan urinib koʻring.</li>
+ <li>Proksi xizmatida shu tarmoqdan ulanishga ruxsat berilganmi?</li>
+ <li>Hali ham muammo mavjudmi? Maslahat olish uchun internet provayderingiz xodimlari yoki tarmoq administratori bilan bogʻlaning.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proksi server topilmadi</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Brauzer proksi-serverdan foydalanish uchun sozlangan, ammo proksi-server topilmadi.</p>
+ <ul>
+ <li>Brauzerning proksi-server konfiguratsiyasi toʻgʻrimi? Sozlamalarni tekshiring va qayta urinib koʻring.</li>
+ <li>Qurilma faol tarmoqqa ulanganmi?</li>
+ <li>Hali ham muammo mavjudmi? Maslahat olish uchun internet provayderingiz xodimlari yoki tarmoq administratorlari bilan bogʻlaning.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">“Zararli sayt” muammosi</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>sayti %1$s hujum qiluvchi sayt sifatida maʼlumot berilgan va xavfsizlik moslamalaringiz asosida u bloklandi.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Kutilmagan sayt muammosi</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Saytida%1$s zararli dastur bor deb xabar berilgan, shuning uchun xavfsizlik moslamalaringizga asoslanib bloklandi.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Zararli sayt muammosi</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>sayti%1$s jiddiy xavfli sayt sifatida maʼlumot berilgan va xavfsizlik parametrlariga asoslanib bloklangan.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Yolgʻon sayt muammosi</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s veb sahifasi aldamchi sayt ekanligi xabar etilgan va sizning xavfsizlik sozlamalaringizga asoslangan holda bloklandi.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Xavfsiz sayt mavjud emas</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Siz kengaytirilgan xavfsizlik uchun faqat HTTPS rejimini yoqdingiz. <em>%1$s</em> HTTPS versiyasi mavjud emas.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">HTTP saytga kirishda davom etish</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..05c3a94e6f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-vec/strings.xml
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Riprova</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Inposibiƚe conpletare ƚa dimanda</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Dèso no xe mìa disponibili altre informasioni che ƚe riguarda sto problema o erore.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Ƚa conesion sicura no ƚa xe riusìa</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[<ul>
+ <li>Ƚa pàgina che se xe drio sercare de vixuaƚixare no ƚa poƚe èsare mostrà parché no xe posibiƚe verificare l’autentisità de i dati ricevui.</li>
+ <li> Contatare el responsabiƚe de el sito web par informarƚo de’l problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Ƚa conesion sicura no ƚa xe riusìa</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[<ul>
+ <li>Poderìa èsare de on problema ne ƚa configurasion de’l server o de on tentativo da parte de cualcheduni de sostituirse al server steso.</li>
+ <li> Se xe stà posibiƚe coneterse a sto server ne el passà, el problema el poderia èsare soƚo tenporaneo. Se consija de riprovare pì tardi.</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Avansate…</string>
+
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Poderìa tratarse de on tentativo de sostituirse a’l sito originaƚe. Xe sconsijà proseguire.</label>
+ <br><br>
+ <label>I siti web i garantise ƚa propria identità traverso certificati. %1$s no considera el sito <b>%2$s</b> atendibiƚe parché l’otorità emittente de’l certificato ƚa xe sconosuda, el certificato el xe autofirmà o el server no el ga invià i certificati intermedi previsti.</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Torna indrio (racomandà)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Aceta el ris-cio e continua</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Par ’sto sito serve na conesion segura.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+<ul>
+ <li>Ła pàjina che te si drio sercar de védar no ła połe mìa èsar mostrà parvìa che ’sto sito el dimanda na conesion segura.</li>
+ <li>Ze probàbiłe che’l problema el rive da’l sito e che no te posi far gnente par resòlvarło.</li>
+ <li>Te połi segnałarghe el problema a l’aministrador de’l sito.</li>
+</ul>
+]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Vansà…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> el gà na połìtega de seguresa ciamada HTTP Strict Transport Security (HSTS), che vołe dir che <b>%2$s</b> el połe conétarse soło en magnera segura. No te połi zontar nissuna ecesion par vardar ’sto sito. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Indrìo</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Ƚa conesion ƚa xe stà interota</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>El browser el xe stà coneso coretamente, però ƚa conesion ƚa xe stà interota durante el trasferimento de ƚe informasioni. Riprovare. </p>
+ <ul>
+ <li> El sito el poderìa èsare tenporaneamente no disponibiƚe o masa ocupà. Riprovare tra cualche istante. </li>
+ <li> Se no se rièse a cargare nisuna pàgina, controƚare i dati de el dispoxitivo o ƚa conesion Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">El tenpo par ƚa conesion el xe exaurìo</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>El sito richiesto no ga risposto a na richiesta de conesion e el browser el ga smesso de spetare na risposta.</p>
+ <ul>
+ <li>Xe posibiƚe che el server el sia sogeto a n’eƚevata dimanda o n’interusion tenporanea. Riprovare pì tardi.</li>
+ <li>Se riese a navigare so altri siti? Controƚare ƚa conesion de rete de el computer. </li>
+ <li>El computer o ƚa rete i xe proteti da on firewall o on proxy? Inpostasioni sbajà ƚe poƚe interferire co ƚa navigasion so el web. </li>
+ <li> Se riscontra ancora problemi? Consultare l’amministradore de rete o el provider Internet par risevere asistensa. </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Conesion no riusìa</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li> El sito el poderìa èsare tenporaneamente no disponibiƚe o masa ocupà. Riprovare tra poco. </li>
+ <li> Se non si riese a cargare nisuna pàgina, controƚare i dati de el dispoxitivo o ƚa conesion Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Resposta inaspetà de el server</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[<p>El sito el ga resposto a ƚa dimanda de rete en modo no previsto, cuindi el browser no el poƚe continuare.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Ƚa pàgina no ƚa reinderisa en modo coreto</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p> El browser el ga smeso de sercare de recuperare ’l eƚemento dimandà. El sito el xe drio reindirisare ƚa richiesta en on modo che no vegnerà mai conpletà. </p>
+ <ul>
+ <li> Xe sta dixabiƚità o blocà i cookie dimandà da sto sito? </li>
+ <li> Se asetando i cookie de el sito no risolve el problema, xe probabiƚe che se trati de on problema de configurasion de el server e no de el computer. </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">No en ƚinea</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[<p>El browser el xe en modaƚità no en ƚinea e no xe posibiƚe conetarse co’l eƚemento dimandà.</p>
+ <ul>
+ <li>El computer xeƚo coƚegà a na rete ativa?</li>
+ <li>Seƚesionare “Riprova” par pasare a ƚa modaƚità en ƚinea e ricargare ƚa pàgina.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Porta blocà par motivi de sicuresa</string>
+
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[<p> L’indiriso dimandà spesificava na porta (par es. <q> Mozilla.org:80</q> par ƚa porta 80 su mozilla.org) normalmente utilixà par scopi <em>diversi</em> da ƚa navigasion so’l web. El browser el ga anuƚà ƚa dimandà par garantire ƚa protesion e ƚa sicuresa de’l utente. </p>]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ƚa conesion ƚa xe stà anuƚà</string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>El coƚegamento de rete el xe stà interroto durante ƚa negosiasion de na conesion. Riprovare. </p>
+ <ul>
+ <li> El sito el poderìa èsare tenporaneamente no disponibiƚe o masa ocupà. Riprovare tra cualche istante. </li>
+ <li> Se no se riese a cargare nisuna pàgina, controƚare i dati de el dispoxitivo o ƚa conesion Wi-Fi.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Tipo de file no sicuro</string>
+
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Contatare el proprietario de el sito web par informarƚo de el problema.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Erore: contenudo danegià</string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[<p>Ƚa pàgina che se sta sercando de vixualixare no ƚa pole èsare mostrà par colpa de on erore de trasmision dati.</p>
+ <ul>
+ <li>Contatare l’aministradore de’l sito par segnaƚare el problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Aresto anomaƚo de el contenudo</string>
+
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[<p>Ƚa pàgina che se sta sercando de vixualixare no ƚa poƚe èsare mostrà par colpa de on erore de trasmision dati.</p>
+ <ul>
+ <li>Contatare l’aministradore de’l sito par segnaƚare el problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Erore de codifega de el contenudo</string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[<p>Ƚa pàgina che se sta sercando de vixualixare no’l poƚe èsare mostrà parchè ƚa fa uxo de na forma de conpresion no valida o no suportà.</p>
+ <ul>
+ <li>Contatare el paron de’l sito web par informarƚo de el problema.</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Indiriso no catà</string>
+
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>El browser no el xe riusìo a catare el server host par el indiriso fornìo.</p>
+ <ul>
+ <li>Verificare se l’indiriso contien erori de batitura de el tipo
+ <strong>ww</strong>.example.com invese de
+ <strong>www</strong>.example.com</li>
+ <li>Se no xe posibiƚe cargare nisuna pàgina, controƚare ƚa conesione dati o Wi-Fi de el dispoxitivo.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Nisuna conesion a Internet</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Verifega ƚa to conesion de rete o prova a recargare ƚa pàgina tra cualche istante.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Recarga</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Indiriso mìa vaƚido</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p> El indiriso forniìo non el xe en un formato riconosuo. Controƚare ƚa bara de i indirisi par individuare eventuaƚi erori e riprovare. </p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">El indiriso no el xe vaƚido</string>
+
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>I indirisi internet de soƚito i se scìve ne ƚa forma <strong>http://www.example.com/</strong></li>
+ <li>Verifegare se xe drio utilixare ƚe bare corete (a exempio <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Protocoƚo no conosùo</string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[<p>El indiriso dimanda on protocoƚo (a ex. <q>wxyz://</q>) che el browser no riconose, indi non poƚe coƚegarse coretamente a el sito.</p>
+<ul>
+ <li>Se stà asedendo a servisi multimediali o no testuaƚi? Verificare so el sito i recuisiti necesari.</li>
+ <li>Alcuni protocoli i dimanda software esterni o plugin en modo che el browser li possa riconosere.</li>
+</ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">File no catà</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>L’ogeto el poderìa èsare stà rinominà, rimoso o spostà.</li>
+ <li>Poderìa eserghe on erore de ortografia ne el indiriso.</li>
+ <li>Se gà i parmesi par asedere a el ogeto specificà?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Aceso a el file no consentìo</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>El file el poderìa èsare stà spostà o scanceƚà opure i parmesi so el file i poderìa inpedirne l’aceso.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Conesion rifiutà da el server proxy</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[<p> El browser el xe configurà par utilixare on server proxy, ma el proxy el ga rifiutà na conesion. </p>
+ <ul>
+ <li> Ƚa configurasione proxy de el browser ƚa xe coreta? Controƚare ƚe inpostasioni e riprovare. </li>
+ <li> el servisio proxy el parmete ƚe conesioni da sta rete? </li>
+ <li> Se riscontra ancora problemi? Consultare ’l aministradore de rete o el provider Internet par risevere asistensa. </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Inposibiƚe contatare el server proxy</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p> El browser el xe configurà par uxare on server proxy, ma el proxy no el xe stà riƚevà. </p>
+ <ul>
+ <li> Ƚa configurasion proxy de el browser ƚa xe coreta? Controƚare ƚe inpostasioni e riprovare. </li>
+ <li> El computer el xe coƚegà a na rete funsionante? </li>
+ <li> Se riscontra ancora problemi? Consultare l’aministradore de rete o el provider Internet par risevere asistensa. </li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Problema de sito co malware</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[<p>El sito web %1$s el xe stà segnalà cofà sito web maƚevoƚo e el xe stà blocà so ƚa baxe de ƚe inpostasion de sicuresa.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Problema de sito no desiderà</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[<p>El sito web %1$s el xe stà segnalà cofà on sito contenente software indesiderà e el xe stà blocà so ƚa baxe de ƚe inpostasion de sicuresa.</p>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Problema de sito pericoƚoso</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[<p>El sito web %1$s el xe stà segnalà cofà sito web potensialmente pericoƚoso e el xe stà blocà so ƚa baxe de ƚe inpostasioni de sicuresa.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Problema co sito inganevoƚe</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[<p>El sito web %1$s el xe stà segnalà cofà sito inganevoƚe e el xe stà blocà so ƚa baxe de ƚe inpostasion de sicuresa.</p>]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Sito seguro mìa disponìbiłe</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Te ghè ativà ła modałidà soło HTTPS par na seguresa pì forte, ma na varsion HTTPS de <em>%1$s</em> no ła ze mìa disponìbiłe.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Và vanti so’l sito HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..d0b1f30107
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-vi/strings.xml
@@ -0,0 +1,310 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Thử lại</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Không thể hoàn tất yêu cầu</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[<p>Hiện không có thông tin bổ sung về sự cố hay lỗi này.</p>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Kết nối bảo mật đã thất bại</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[ <ul>
+ <li>Trang bạn đang cố gắng truy cập không thể hiển thị vì không thể xác minh tính xác thực của dữ liệu nhận được.</li>
+ <li>Bạn hãy liên hệ với chủ sở hữu trang web để thông báo về sự cố này nếu có thể.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Kết nối bảo mật đã thất bại</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Đây có thể là một vấn đề với cấu hình máy chủ, hoặc có thể ai đó đang cố gắng mạo danh máy chủ này.</li>
+ <li>Nếu trước đây bạn đã kết nối với máy chủ này thành công thì có khả năng lỗi này chỉ là tạm thời và bạn có thể thử lại sau.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Nâng cao…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Ai đó có thể đang cố gắng mạo danh trang web và bạn không nên tiếp tục.</label>
+ <br><br>
+ <label>Trang web chứng minh danh tính của họ thông qua các chứng chỉ. %1$s không tin tưởng <b>%2$s</b> vì tổ chức phát hành chứng chỉ không xác định, chứng chỉ tự ký, hoặc máy chủ không gửi chứng chỉ trung gian chính xác.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Quay lại (Khuyến nghị)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Chấp nhận rủi ro và tiếp tục</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Trang web này yêu cầu kết nối an toàn.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>Không thể hiển thị trang web bạn đang cố gắng truy cập vì nó yêu cầu kết nối an toàn.</li>
+ <li>Rất có thể đã xảy ra sự cố với trang web và bạn không thể làm gì để giải quyết nó.</li>
+ <li>Bạn có thể thông báo cho quản trị viên của trang web về sự cố.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Nâng cao…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> có chính sách bảo mật được gọi là Bảo mật truyền tải nghiêm ngặt HTTP (HSTS), có nghĩa là <b>%2$s</b> chỉ có thể kết nối với nó một cách an toàn. Bạn không thể thêm ngoại lệ để truy cập trang web này. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Quay lại</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Kết nối đã bị gián đoạn</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>Trình duyệt đã kết nối thành công, nhưng kết nối bị gián đoạn trong khi truyền thông tin. Vui lòng thử lại.</p>
+ <ul>
+ <li>Trang web có thể tạm thời không có hoặc quá bận rộn. Hãy thử lại trong một vài giây.</li>
+ <li>Nếu bạn không thể tải bất kỳ trang nào, hãy kiểm tra kết nối dữ liệu hoặc kết nối Wi-Fi trên thiết bị của bạn.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Kết nối đã quá hạn</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Trang web được yêu cầu không phản hồi yêu cầu kết nối và trình duyệt đã dừng chờ trả lời.</p>
+ <ul>
+ <li>Máy chủ có thể gặp phải nhu cầu cao hoặc ngừng hoạt động tạm thời? Thử lại sau.</li>
+ <li>Bạn không thể duyệt các trang web khác? Kiểm tra kết nối mạng máy tính.</li>
+ <li>Máy tính hoặc mạng của bạn được bảo vệ bởi tường lửa hoặc proxy? Cài đặt không chính xác có thể can thiệp vào trình duyệt Web.</li>
+ <li>Vẫn gặp sự cố? Tham khảo ý kiến ​​quản trị viên mạng hoặc nhà cung cấp Internet của bạn để được hỗ trợ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Không thể kết nối</string>
+
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Trang web có thể tạm thời không có hoặc quá tải. Hãy thử lại sau.</li>
+ <li>Nếu bạn không thể tải bất kỳ trang nào, hãy kiểm tra kết nối dữ liệu hoặc kết nối Wi-Fi trên thiết bị của bạn.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Phản hồi không mong muốn từ máy chủ</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[ <p>Trang web đã phản hồi yêu cầu mạng theo cách không mong muốn và trình duyệt không thể tiếp tục.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Trang này chuyển hướng không đúng cách</string>
+
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>Trình duyệt đã ngừng cố gắng truy xuất mục được yêu cầu. Trang web đang chuyển hướng yêu cầu theo cách không bao giờ hoàn thành.</p>
+ <ul>
+ <li>Có phải bạn đã vô hiệu hóa hoặc chặn cookie theo yêu cầu của trang web này?</li>
+ <li>Nếu chấp nhận cookie của trang web không giải quyết được vấn đề, có thể cấu hình máy chủ và máy tính của bạn có vấn đề .</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Chế độ ngoại tuyến</string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Trình duyệt đang hoạt động ở chế độ ngoại tuyến và không thể kết nối với mục được yêu cầu.</p>
+ <ul>
+ <li>Máy tính có được kết nối với mạng đang hoạt động không?</li>
+ <li>Nhấp “Thử lại” để chuyển sang chế độ trực tuyến và tải lại trang.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port bị hạn chế vì lý do an ninh</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Địa chỉ được yêu cầu chỉ định một cổng (ví dụ: <q>mozilla.org:80</q> cho port 80 trên mozilla.org) thường được sử dụng cho những mục đích <em>khác</em> hơn là duyệt Web. Trình duyệt đã hủy yêu cầu vì lý do bảo vệ và bảo mật của bạn.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Kết nối đã được thiết lập lại</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Liên kết mạng đã bị gián đoạn trong khi đàm phán kết nối. Vui lòng thử lại.</p>
+ <ul>
+ <li>Trang web có thể tạm thời không có hoặc quá bận rộn. Hãy thử lại sau.</li>
+ <li>Nếu bạn không thể tải bất kỳ trang nào, hãy kiểm tra kết nối dữ liệu hoặc kết nối Wi-Fi trên thiết bị của bạn.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Loại tập tin không an toàn</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Vui lòng liên hệ với các chủ sở hữu trang web để thông báo cho họ về vấn đề này.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Lỗi nội dung bị hỏng</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Trang bạn đang cố truy cập không thể được hiển thị vì đã phát hiện lỗi trong quá trình truyền dữ liệu.</p>
+ <ul>
+ <li>Vui lòng liên hệ với các chủ sở hữu trang web để thông báo cho họ về vấn đề này.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Nội dung bị lỗi</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Trang bạn đang cố truy cập không thể hiển thị vì đã phát hiện lỗi trong quá trình truyền dữ liệu.</p>
+ <ul>
+ <li>Vui lòng liên hệ với các chủ sở hữu trang web để thông báo cho họ về vấn đề này.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Lỗi mã hóa nội dung</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[ <p>Trang bạn đang cố truy cập không thể hiển thị vì nó sử dụng hình thức nén không hợp lệ hoặc không được hỗ trợ.</p>
+ <ul>
+ <li> Vui lòng liên hệ với các chủ sở hữu trang web để thông báo cho họ về vấn đề này.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Không tìm thấy địa chỉ</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Trình duyệt không thể tìm thấy máy chủ lưu trữ cho địa chỉ được cung cấp.</p>
+ <ul>
+ <li>Kiểm tra địa chỉ cho các lỗi đánh máy như
+ <strong>ww</strong>.example.com thay cho
+ <strong>www</strong>.example.com.</li>
+ <li>Nếu bạn không thể tải bất kỳ trang nào, hãy kiểm tra kết nối dữ liệu hoặc kết nối Wi-Fi trên thiết bị của bạn.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Không có kết nối mạng</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Kiểm tra kết nối mạng của bạn hoặc thử tải lại trang trong giây lát.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Tải lại</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Địa chỉ không hợp lệ</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Địa chỉ bạn cung cấp không đúng định dạng. Vui lòng kiểm tra lại thanh địa chỉ để tìm lỗi và thử lại.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Địa chỉ không hợp lệ</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Địa chỉ web thường được viết như <strong>http://www.example.com/</strong></li>
+ <li>Đảm bảo rằng bạn sử dụng dấu gạch chéo về phía trước (như <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Giao thức không xác định</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Địa chỉ chỉ định một giao thức (e.g., <q>wxyz://</q>) trình duyệt không nhận ra, vì vậy trình duyệt không thể kết nối đúng với trang web.</p>
+ <ul>
+ <li>Bạn đang cố gắng truy cập đa phương tiện hoặc các dịch vụ phi văn bản khác? Kiểm tra các trang web cho các yêu cầu thêm.</li>
+ <li>Một số giao thức có thể yêu cầu phần mềm hoặc plugin của bên thứ ba trước khi trình duyệt có thể nhận ra chúng.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">Không tìm thấy tập tin</string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Mục này đã được đổi tên, loại bỏ hoặc di chuyển không?</li>
+ <li>Có lỗi chính tả, viết hoa hoặc lỗi đánh máy khác trong địa chỉ không?</li>
+ <li>Bạn có đủ quyền truy cập vào mục được yêu cầu không?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Quyền truy cập tập tin đã bị từ chối</string>
+
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Nó có thể đã được gỡ bỏ, di chuyển, hoặc quyền truy cập tập tin có thể đã bị từ chối.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Máy chủ proxy từ chối kết nối</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Trình duyệt được cấu hình để sử dụng máy chủ proxy, nhưng proxy đã từ chối kết nối.</p>
+ <ul>
+ <li>Cấu hình proxy của trình duyệt có đúng không? Kiểm tra cài đặt và thử lại.</li>
+ <li>Dịch vụ proxy có cho phép kết nối từ mạng này không?</li>
+ <li>Vẫn gặp sự cố? Tham khảo ý kiến ​​quản trị viên mạng hoặc nhà cung cấp dịch vụ Internet của bạn để được hỗ trợ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Không tìm thấy máy chủ proxy</string>
+
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>Trình duyệt được cấu hình để sử dụng máy chủ proxy, nhưng không thể tìm thấy proxy.</p>
+ <ul>
+ <li>Cấu hình proxy của trình duyệt có đúng không? Kiểm tra cài đặt và thử lại.</li>
+ <li>Máy tính có được kết nối với mạng đang hoạt động không?</li>
+ <li>Vẫn gặp sự cố? Tham khảo ý kiến ​​quản trị viên mạng hoặc nhà cung cấp dịch vụ Internet của bạn để được hỗ trợ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Sự cố với trang web có mã độc</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Trang web tại %1$s đã được báo cáo là một trang web tấn công và đã bị chặn dựa trên tùy chọn bảo mật của bạn.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Sự cố với trang web không mong muốn</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Trang web tại %1$s đã được báo cáo là cung cấp phần mềm không mong muốn và đã bị chặn dựa trên tùy chọn bảo mật của bạn.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Sự cố trang web gây hại</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Trang web tại %1$s đã được báo cáo là một trang web có khả năng gây hại và đã bị chặn dựa trên các tùy chọn bảo mật của bạn.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Sự cố với trang web lừa đảo</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+      <p>Trang web này tại %1$s đã được báo cáo là trang lừa đảo và đã bị chặn dựa trên tùy chọn bảo mật của bạn.</p>
+    ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Trang web an toàn không khả dụng</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[Bạn đã bật Chế độ chỉ HTTPS để tăng cường bảo mật, và phiên bản HTTPS của <em>%1$s</em> không khả dụng.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Tiếp tục đến trang web HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..bd61ab58a7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-yo/strings.xml
@@ -0,0 +1,316 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Gbìyànjú lẹ́ẹ̀kan sí i</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">A kò lè parí ìbéèrè rẹ</string>
+
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Àfikún ìfitónilétí nípa ìsòro tàbí àsìṣe yìí kò sí lọ́wọ́ lọ́wọ́ báyìí</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Ìsomọ́ra Onífọ̀kànbalẹ̀ Kùnà</string>
+
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>ojú-ìwé tí ò ń gbìyànjú àti wò, kò ṣe é fi hàn báyìí nítorí pé ìjẹ́-òtítọ́ data tí a gbà ni a kò lè fi ìdí rẹ̀ múlẹ̀. </li>
+ <li> Jọ̀wọ́ kàn sí ẹni tí ó ni ìkànnì náà láti fi ìsòro yìí tó wọn létí.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Ìsomọ́ra onífọkànbalẹ̀ kùnà</string>
+
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>Ìsòro yìí lè jẹ́ ìfisáfà ṣiṣẹ́ tàbí kí ẹlòmíràn fẹ́ gbáwọ̀ sáfà wọ̀.</li>
+ <li>Bí o bá ti ṣe àṣeyege àti wọlé sórí sáfà yìí tẹ́lẹ̀ rí, àsìṣe náà lè jẹ́ èyí tí kò ní pẹ́, ó sì le gbìyànjú sí i bó bá yá .</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Ìjìnlẹ̀…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Ẹnìkan le máa gbìyànjú àti gbáwọ̀ ìkànnì náà wọ̀, má tẹ̀síwájú.</label>
+ <br><br>
+ <label>Ìkànnì ṣe ìfidánilójú ìdánimọ̀ wọn pẹ̀lú ẹ̀rí. %1$s má gbẹ́kẹ̀lé <b>%2$s</b> nítorí pé kìí ṣe ẹni mímọ̀ ni ó fi ìwé-ẹ̀rí rẹ̀ lóde, ìfọ́wọ́sí-ara-ẹni ni ìwé-ẹ̀rí náà jẹ́, tàbí ìkànnì náà kò fi ojúlówó ìwé-ẹ̀rí abàárín ránṣẹ́.</label>
+ ]]></string>
+
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Padà Sẹ́yìn (Ìmọ̀ràn tó dára)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Gba Ewu náà kí o sì tẹ̀síwájú</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">Ìkọ̀nì yìí nílò ìsopọ̀ tó pamọ́.</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[<ul> <li>O kò lè rí abala tí ò ń gbìyànjú láti ṣí yìí nítorí pé ìkànní yìí nílò ààbò.</li> <li>Ìṣòro yìí lè jẹ́ èyí tó wá láti orí ìkànnì yìí, kò sì sí ohun tí o lè ṣe si láti wá ọ̀nà àbáyọ si.</li> <li>O sì lè pe àkíyèsí alámòjútó ìkànnì náà sí ìṣòro yíì.</li> </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Ní ìlọsíwájú…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label> <b>%1$s</b> gẹ́gẹ́ bíi ètò ààbò tí a pè ní HTTP ààbò ìlọmabọ̀ tó le (HSTS), tó túmọ̀ sí pé <b>%2$s</b> ó lè ní ìbátan pẹ̀lú rẹ̀ nígbà tí ààbò bá wà. O kò lè fi ìyàsọ́tọ̀ kún-un láti ṣàbẹ̀wò sí sáìtì yìí. </label>
+]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Padà Sẹ́yìn</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">Akùdé bá ìsomọ́ra náà</string>
+
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>ìtàkùn ìgbáyé so mọ́ra dáadáa, ṣùgbọ́n àkùdé bá ìsomọ́ra náà nígbà tí à ń fi ìfitónilétí ránṣẹ́. Jọ̀wọ́ gbìyànjú sí i.</p>
+ <ul>
+ <li> Ìkànnì náà lè má siṣẹ́ fún ìgbà díẹ̀ tàbí kí ọwọ́ kún un. Gbìyànjú rẹ̀ sí i ní àìpẹ́.</li>
+ <li>Bí o kò bá lè jẹ́ kí ojú-ìwé rẹ siṣẹ́, yẹ ohun tí data rẹ fi ń siṣẹ́ wò tàbí ìsomọ́ Wi-Fi rẹ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">Àkókò ìsomọ́ ti kọjá</string>
+
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>Ìkànnì tí ó bèèrè kò dáhùn sí ìsomọ́ ti o béèrè àti pé ìtàkùn àgbáyé ti dúró iṣẹ́, ó ń dúró fún èsì.</p>
+ <ul>
+ <li>Ǹjẹ́ ó ṣe é ṣe kí sáfà máa dojúkọ ìpè púpọ̀ tàbí kí ó má ṣiṣẹ́ fún ìgbà díẹ̀? Gbìyànjú bí ó bá yá.</li>
+ <li>Ǹjẹ́ o ní ìṣòro àti sàwárí àwọn ìkànnì mìíràn? Yẹ ohun tí o fi ṣe àsomọ́ wò.</li>
+ <li>Ǹjẹ́ ìdáabòbò ohun èlò rẹ tàbí nẹ́tíwòkì jẹ́ láti ọwọ́ ojúlówó tàbí asojú? àsìsẹ ààtò lè nípa lórí wíwá nǹkna lórí ìkànnì.</li>
+ <li>O sì ní ìsòro síbẹ̀? Kàn sí alákòóso nẹ́tiwọkì rẹ tàbí àpèse íntánẹ́ẹ̀tì rẹ fún ìrànlọ́wọ́.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Kùnà láti somọ́ra</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>Ìkànnì náà lè má sí ní àrọ́wọ́tó fún ìgbà díẹ̀ tàbí kí ọwọ́ kún un. Gbìyànjú sí i láìpẹ́.</li>
+ <li>Bí o kò bá lè jẹ́ kí ojú-ìwé siṣẹ́, yẹ ohun èlò data rẹ wò tàbí àsomọ́ Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Èsì tí a kò retí láti ọ̀dọ̀ sáfà</string>
+
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>Ìkànnì ń fèsì sí nẹ́tíwọ̀kì lọ́nà ti a kò retí àti pé ìtàkùn àgbáyé kò le tẹ̀síwájú.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">Ojú-ìwé náà kò ṣe atọ̀nà dáadáa</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>ìtàkùn àgbáyé dáwọ́ àtimú àwọn ohun tí a bèèrè dúró. Ìkànnì náà ń ṣe àtúntọ́sọ́nà lọ́nà tí kò lè parí láéláé.</p>
+ <ul>
+ <li>Ǹjẹ́ o ti ṣo ọ́ di aláìlágbára tàbí dínà àwọn adásiṣẹ́ tí ìkànnì yìí nílò?</li>
+ <li>Bí gbígba àwọn adásiṣẹ́ ìkànnì yìí kò bá yanjú ìsòro yìí, a jẹ́ pé ó ṣe é ṣe kí ó jẹ́ ìsòro ìfisáfàṣiṣẹ́ ni, kìí sì ṣe ohun èlò rẹ.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Ipò àìsí lórí íntánẹ́ẹ̀tì </string>
+
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>Ìtàkùn-àgbáyé ń ṣiṣẹ́ lóríipò àìsí lórí íntánẹ́ẹ̀tì, nítorí náà, kò lè so mọ́ ohun tí a bèèrè fún.</p>
+ <ul>
+ <li>Ǹjẹ́ ohun èlò náà wà ní sísomọ́ nẹ́tíwọ̀kì tó ń ṣiṣẹ́?</li>
+ <li>Tẹ “Gbìyànjú sí i” láti bọ́ sí ipo íntánẹ́ẹ̀tì, kí o si ojú-ìwé náà siṣẹ́ lẹ́ẹ̀kan sí.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Ojú yìí kò ṣiṣẹ́ nítorí ààbò</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>Àdírẹ́sì tí o bèèrè fún nílò ojú kan pàtó (e.g., <q>mozilla.org:80</q> fún ojú 80 lórí mozilla.org) ni a máa ń sábà lò fún àwọn ìdí <em>yàtọ̀</em> sí ìwá nǹkan kíri orí ìkànnì. Ìtàkùn-àgbáyé ti gbégi lé ìbéérè náà fún ààbò rẹ.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">Ìsomọ́ra di àtúntò </string>
+
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>Akùdé bá òpónà nẹ́tíwọ̀kì nígbà tí ìdúnàándúrà ìsomọ́ra ń lọ lọ́wọ́. Jọ̀wọ́ gbìyànjú sí i.</p>
+ <ul>
+ <li>Ìkànnì náà lè má siṣẹ́ fún ìgbà díẹ̀ tàbí kí ọwọ́ kún un. Gbìyànjú lẹ́yìn ìgbà díẹ̀.</li>
+ <li>Bí o kò bá lè jẹ́ kí ojú ìwé kankan siṣẹ́, yẹ ohun èlò data rẹ wò tàbí ìsomọ́ Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Ẹ̀yà fáìlì tó léwu</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Jọ̀wọ́ kàn sí àwọn tí ó ni ìkànnì láti fi ìsòro yìí tó wọn létí.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Àṣìṣe ìbàjẹ́ àkóónú </string>
+
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>Ojú-ìwé tí ò ń gbìyànjú àti wò kò ṣe é fihàn nítorí pé a rí àṣìṣe kan nínú data fífiránṣẹ́.</p>
+ <ul>
+ <li>Jọ̀wọ́ kan sí àwọn tí ó ni ìkànnì láti fi ìsòro yìí tó wọn létí.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Àkóónú ti bàjẹ́</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>Ojú-ìwé tí ò ń gbíyànjú àti wò kò ṣe é fi hàn nítorí pé, a rí àṣìṣe kan nínú ìfidátà ránsẹ́.</p>
+ <ul>
+ <li>Jọ̀wọ́ kàn sí àwọn tó ni ìkànnì láti fi ìsòro yìí tó wọn létí.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Àṣìṣe ìṣàrokò àkóónú </string>
+
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>Ojú-iwé tí ò ń gbìyànjú àti wò kò ṣe é fi hàn nítorí tí ó le ìsọdikékeré alápiṣeégbà tàbí aláìfọwọ́sí.</p>
+ <ul>
+ <li>Jọ̀wọ́ kàn sí àwọn tí wọ́n ni ìkànnì láti fi ìsòro yìí tó wọn létí.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">A kò rí àdírẹ́sì </string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>Ìtàkùn-àgbáyé kò rí agbàlejò sáfà fún àdírẹ́sì.</p>
+ <ul>
+ <li>yẹ àdírẹ́sì náà wò fún àṣìtẹ̀ gẹ́gẹ́ bí i
+ <strong>ww</strong>.example.com dípò
+ <strong>www</strong>.example.com.</li>
+ <li>Bí o kò bá lè mú kí ojú ìwé kankan ṣiṣẹ́, yẹ ohun èlò data rẹ wò tàbí ìsomọ́ Wi-Fi.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">Kò sí ìsomọ́ íntánẹ́ẹ̀tì </string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Yẹ àsomọ́ nẹ́tiwọ̀kì rẹ wò tàbí gbìyànjú àti mú ojú-ìwé ṣiṣẹ́ lẹ́yìn ìgbà díẹ̀ </string>
+
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Tún mú un ṣiṣẹ́</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Àdírẹ́sì aláìṣeégbà </string>
+
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>Àdírẹ́sì tí o pèsè kò sí ní ìlànà tí a dámọ̀. Jọ̀wọ́ yẹ àmì ìfi-ọ̀gangan-hàn wò fún àṣìṣe, kì ò sì gbìyànjú lẹ́ẹ̀kan sí i.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">Àdírẹ́sì náà kò ṣe é gbà</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Àdírẹ́sì ìkànnì ni a sábà máa ń kọ bí i <strong>http://www.example.com/</strong></li>
+ <li>Rí i dájú pé àkámọ́ asùnsọ́wọ́-wájú ni ò ń lò (b.a. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Ìlànà tí a kò mọ̀ </string>
+
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>Àdírẹ́sì náà sọ ìlànà kan (e.g., <q>wxyz://</q>) ìtàkùn-àgbáyé kò dá a mọ̀, nítorí náà àtàkùn-àgbáyé kò lè so ó mọ́ ìkànnì.</p>
+ <ul>
+ <li>Ǹjẹ́ ò gbìyànjú àti ní àǹfààní sí ìbánisọ̀rọ̀-alárànbarà tàbí àwọn iṣẹ́ mìíràn tí kìí ṣe alátẹ̀jíṣẹ́? Wo ìkànnì náà fún àwọn ìbéérè tí ó tún kù.</li>
+ <li>Àwọn ìlànà mìíràn a máa alàgàta amẹ́rọṣiṣẹ́ kí ìtàkùn-àgbáyé tó lè dá wọn mọ̀.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">A kò rí fáìlì </string>
+
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Ǹjẹ́ a lè ti pa orúkọ nǹkan náà dà, yọ ọ́ kúrò tàbí mú un lọ síbòmíràn?</li>
+ <li>Ǹjẹ́ àṣìṣe wa lè wà níbi sípẹ́lì, ìlo-lẹ́tà-ńlá, tàbí àṣìtẹ̀ níbi àdírẹ́sì bí?</li>
+ <li>Ǹjẹ́ o ní ìyọ́nda tótó fún ohun tí ò ń bèèrè?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Ìdènà wà sí àṣe sí fáìlì náà </string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>Ó ṣe é ṣe kí wọ́n ti yọ ọ́, gbé e tàbí àṣẹ sí fáìlì lè máa dènà àti wọlé.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Asojú sáfà kọ asomọ́ra</string>
+
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>Ìtàkùn-àgbáyé ni a ṣe lanà tí ó gbọ́dọ̀ lo asojú fáfà, ṣùgbọ́n asojú náà kọ ìsomọ́ra.</p>
+ <ul>
+ <li>Ǹjẹ́ ìfiṣiṣẹ́ asojú ìtàkùn-àgbáyé náà tọ̀nà? Yẹ ààtò wò kí o sì gbìyànjú sí i.</li>
+ <li>Ǹjẹ́ iṣẹ́ asojú fàyè gba ìsomọ́ra láti ọ̀dọ̀ nẹ́tíwọ̀kì yìí?</li>
+ <li>O sì ní ìsòro síbẹ̀? Kàn sí alákòóso nẹ́tíwọ̀kì rẹ tàbí olùpèsè íntánẹ́ẹ̀tì rẹ fún ìrànlọ́wọ́.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">A kò sí asokú sáfà </string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>A ṣe ìtànkùn-àgbáyé láti ṣiṣẹ́ pẹ̀lú asojú sáfà ṣùgbọ́n a kò rí asojú.</p>
+ <ul>
+ <li>Ǹjẹ́ ìfiṣiṣẹ́ ìtàkùn-àgbáyé tọ̀nà? Yẹ ààtò wò kí o sì gbìyànjú sí i.</li>
+ <li>Ǹjẹ́ ohun èlò rẹ wà ní ìsomọ́ nẹ́tíwọ̀kì tó ń ṣiṣẹ́?</li>
+ <li>Ǹjẹ́ o sì ní ìsòro síbẹ̀? Kàn sí asàkòóso nẹ́tíwọ̀kì rẹ tàbí apèsè íntánẹ́ẹ̀tì rẹ fún ìrànlọ́wọ́.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Ìṣòro ìkànnì mẹ́rọṣisẹ́-onísùtá</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>Ìkànnì ní %1$s ni wan ti jábọ̀ pé wọ́n ti kọ lù ú, wọ́n sì ti dènà rẹ̀ nítorí ìdáàbòbò tí o yàn.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Ìṣòro ìkànnì tí a kò fẹ́</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>Ìkànnì ní %1$s ni wọ́n jábọ̀ pé ó ní amsọṣiṣẹ́ tí a kò fẹ́, wọ́n sì ti dènà rẹ̀ ítorí ìdáàbòbò tí o yàn.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Ìṣòro ìkànnì tó léwu</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>Ìkànnì ní %1$s ni wọ́n fi sùn pé ó jẹ́ ìkànnì tí ó ṣe é ṣe kí ó léwu, wọ́n sì di dènà rẹ̀ nítorí ìdàábòbò tí o yàn.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Ìṣòro ìkànnì atànnijẹ</string>
+
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>Ojú-ìwé ìkànnì yìí %1$s ni wọ́n ti fi sùn gẹ́gẹ́ ìkànnì àtànnijẹ, wọ́n sì ti dènà rẹ̀ nítorí ìdàábòbò tí o yàn.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Ìkànnì adánilójú kò sí </string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[O ti fàyè gba HTTPS-Only Mode láti kún ààbo rẹ nípa, àti ẹ̀dà HTTPS ti <em>%1$s</em> kò sí.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Tẹ̀síwájú sí ìkànnì HTTP</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..4269273d20
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,307 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">重试</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">无法完成请求</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>暂无此问题或错误的详细解释信息。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">安全连接失败</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>您尝试查看的网页无法显示,因为接收到的数据的真实性无法验证。</li>
+ <li>建议联系向这个网站的拥有者反馈此问题。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">安全连接失败</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>这可能是服务器配置的问题,或是有人尝试冒充该服务器所致。</li>
+ <li>如果您之前曾成功连接该服务器,错误可能是暂时的,您可以稍后再尝试。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">高级…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>可能有人试图冒充该网站,您不应该继续访问。</label>
+ <br><br>
+ <label>网站通过证书证明其身份。%1$s 不信任 <b>%2$s</b>,其证书颁发者未知,属于自签名证书,或服务器未妥善发送中间证书。</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">返回上一页(推荐)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">接受风险并继续</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">此网站要求使用安全连接。</string>
+
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>由于此网站要求使用安全连接,您尝试查看的页面无法显示。</li>
+ <li>这个问题大多与网站有关,无法通过您的操作解决。</li>
+ <li>您可以向此网站的管理者反馈该问题。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">高级…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> 启用了被称为 HTTP 严格传输安全(HSTS)的安全策略,<b>%2$s</b> 只能与其建立安全连接。您无法为此网站添加例外,以访问此网站。</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">返回</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">连接被中断</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>浏览器连接成功,但连接在传输数据时中断了。请稍后重试。</p>
+ <ul>
+ <li>该网站可能暂时无法使用或太忙。请稍后再试一次。</li>
+ <li>如果您无法加载任何页面,请检查设备的数据或 Wi-Fi 连接。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">连接超时</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>指定的网站一直没有回应,浏览器停止了等待。</p>
+ <ul>
+ <li>该网站可能目前访问人数过多?请稍后重试。</li>
+ <li>您是否也无法浏览其他网站?请检查您网络连接。</li>
+ <li>设备或网络是否受防火墙或代理保护?请确定这些设置是否正确。</li>
+ <li>仍然不行?请联系您的网络管理员或者电信运营商以寻求协助。</li>
+ </ul>
+
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">无法连接</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>该网站可能暂时无法使用或太忙。请稍后再试一次。</li>
+ <li>如果您无法加载任何页面,请检查设备的数据或 Wi-Fi 连接。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">意外的服务器响应</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>该网站响应网络请求的方式与预期不符,浏览器无法继续。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">页面未正确重定向</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>浏览器已停止尝试获取请求的项。网站正在将请求无限循环重定向。</p>
+ <ul>
+ <li>您是否禁用或拦截了该网站必须的 Cookie?</li>
+ <li>如果接受该网站的 Cookie 仍然不能解决问题,则很可能是该网站服务器配置的问题而不是您计算机的问题。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">脱机模式</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>浏览器当前处于脱机模式,无法连接请求的项。</p>
+ <ul>
+ <li>计算机是否连接了可用的网络?</li>
+ <li>按“重试”切换到联机模式并重新加载此页面。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">安全原因导致访问端口受限</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>请求的网址指定的端口(例如 <q>mozilla.org:80</q> 表示使用 mozilla.org 的 80 端口)通常<em>不是</em>用于网络浏览。为了保护您的安全,浏览器已取消请求。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">连接被重置</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>协商连接时网络链路中断。请重试。</p>
+ <ul>
+ <li>该网站可能暂时无法使用或太忙。请稍后再试一次。</li>
+ <li>如果您无法加载任何页面,请检查设备的数据或 Wi-Fi 连接。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">不安全的文件类型</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>请联系网站所有者,向其通报这一问题。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">内容损坏错误</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>您尝试查看的页面无法显示,因为检测到传输的数据含有错误。</p>
+ <ul>
+ <li>请联系网站所有者,向其通报这一问题。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">内容已崩溃</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>您尝试查看的页面无法显示,因为检测到传输的数据含有错误。</p>
+ <ul>
+ <li>请联系网站所有者,向其通报这一问题。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">内容编码错误</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>您尝试查看的页面无法显示,因为它使用了无效的或者不支持的压缩格式。</p>
+ <ul>
+ <li>请联系网站所有者,向其通报这一问题。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">找不到网址</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>浏览器无法找到该网址对应的服务器。</p>
+ <ul>
+ <li>检查网址有没有输入错误,比如把
+ <strong>www</strong>.example.com 输入成了
+ <strong>ww</strong>.example.com。</li>
+ <li>如果您无法加载任何页面,请检查设备的数据或 Wi-Fi 连接。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">无互联网连接</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">请检查您的网络连接,或在稍后尝试重新加载页面。</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">重新加载</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">不正确的网址</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>无法识别这种格式的网址。请检查网址是否有误并重试。</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">网址无效</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>网址的格式通常是:<strong>http://www.example.com/</strong>。</li>
+ <li>请确保您使用的是正斜杠(即 <strong>/</strong>)。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">未知协议</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>浏览器无法识别该网址的协议(例如 <q>wxyz://</q>),因此浏览器无法连接至该网站。</p>
+ <ul>
+ <li>您是否在尝试访问多媒体或其他非文字服务?请检查该站点是否有其他要求。</li>
+ <li>某些协议可能需要安装第三方软件或插件后浏览器才能识别。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">未找到文件</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>或许该项目已经被改名、移除或者移走?</li>
+ <li>网址是否有拼写错误、大小写错误或其他笔误?</li>
+ <li>您是否有访问该项目的权限?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">访问该文件被拒绝</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>文件可能已被移走、移除,或者没有访问该文件的权限。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">代理服务器拒绝连接</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>浏览器被设置为使用代理服务器,但是该代理服务器拒绝了该连接。</p>
+ <ul>
+ <li>浏览器的代理服务器设置是否正确?请检查并重试。</li>
+ <li>该代理服务是否允许来自此网络的连接?</li>
+ <li>仍然不行?请联系您的网络管理员或者电信运营商以寻求协助。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">无法找到代理服务器</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>浏览器被设置为使用代理服务器,但是无法找到该代理服务器。</p>
+ <ul>
+ <li>浏览器的代理服务器设置是否正确?请检查并重试。</li>
+ <li>计算机是否连接了可用的网络?</li>
+ <li>仍然不行?请联系您的网络管理员或者互联网服务提供商以寻求协助。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">有恶意软件网站问题</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>根据举报,位于 %1$s 的此网站有攻击行为。现已根据您的安全选项予以拦截。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">有流氓软件网站问题</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>根据举报,位于 %1$s 的此网站提供流氓软件。现已根据您的安全选项予以拦截。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">有恶意网站问题</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>根据举报,位于 %1$s 的此网站可能有攻击行为。现已根据您的安全选项予以拦截。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">有欺诈网站问题</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>根据举报,位于 %1$s 的此网站为欺诈网站。现已根据您的安全选项予以拦截。</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">安全网站不可用</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[为增强安全性,您已启用 HTTPS-Only 模式,但 <em>%1$s</em> 的 HTTPS 版本不可用。]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">继续前往 HTTP 网站</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..9d8ae3a412
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">重試</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">無法完成要求</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>沒有關於此問題或錯誤的詳細解說資訊。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">安全連線失敗</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>因為無法驗證已接收資料的真實性,無法顯示您嘗試檢視的頁面。</li>
+ <li>請向網站擁有者回報此問題。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">安全連線失敗</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>可能是伺服器設定問題造成,或是有人嘗試偽裝成該伺服器。</li>
+ <li>若您以前可以與該伺服器正常連線,那麼這個錯誤可能只是暫時的,請稍候再試試看。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">進階…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[ <label>有心人士可能正在嘗試將別的網站偽裝成您想造訪的網站,不應繼續開啟。</label>
+ <br><br>
+ <label>網站會透過憑證來證明自己的身分。因為簽發者未知、憑證是自簽憑證,或伺服器並未送出正確的中介憑證的關係,%1$s 無法信任 <b>%2$s</b>。</label> ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">返回上一頁(建議)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">接受風險並繼續</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">此網站要求必須使用安全連線。</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[ <ul>
+ <li>由於此網站必須使用安全性連線,無法顯示您嘗試檢視的頁面。</li>
+ <li>這個問題最有可能是由於網站端的設定不正確,無法由您調整設定解決。</li>
+ <li>可以通知網站管理員處理。</li>
+ </ul>]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">進階…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[<label><b>%1$s</b> 有一條稱為 HTTP Strict Transport Security (HSTS) 的安全性政策,讓 <b>%2$s</b> 僅能與其進行安全連線。您無法加入例外,手動排除此政策。</label>]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">回上一頁</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">連線中斷</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[<p>瀏覽器連線成功,但傳輸被中斷了;請稍候重試。</p>
+ <ul>
+ <li>網站可能太忙碌或暫時無法使用,請稍候再試一次。</li>
+ <li>若您無法載入任何頁面,請檢查裝置的數據或 Wi-Fi 連線。</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">網路連線逾時</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[<p>指定的網站一直沒有回應,瀏覽器已停止等待。</p>
+ <ul>
+ <li>該網站可能暫時流量過高?請稍候再試。</li>
+ <li>無法瀏覽其它網站?請檢查裝置的網路連線。</li>
+ <li>裝置需經過防火牆或 Proxy 才能連線?請確定這些設定是否正確。</li>
+ <li>仍有其它問題?請洽詢您的網路管理員或網路業者。</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">連線失敗</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[<ul>
+ <li>可能是網站暫停服務或忙碌中?請稍候重試。</li>
+ <li>若您無法載入任何頁面,請檢查裝置的數據或 Wi-Fi 連線。</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">錯誤的回應</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>網站回應錯誤,瀏覽器無法繼續。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">頁面重新導向不正確</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[<p>瀏覽器已停止試圖取得資料。因為網站似乎在無止盡的重新導向。</p>
+ <ul>
+ <li>您是否關掉或封鎖此網站運作必需的 Cookie?</li>
+ <li>若接受此網站的 Cookie 也無法解決此問題,通常是伺服器設定錯誤而非您的裝置的問題。</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">離線模式</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>瀏覽器正在離線模式,無法連線到指定位置。</p>
+ <ul>
+ <li>目前的網路連線正常嗎?</li>
+ <li>請按下「重試」以切換到連線模式並重新載入頁面。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">因安全考量禁止使用的 Port</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>網址所指定的 Port(例如 <q>mozilla.org:80</q> 表示使用 Port 80)通常<em>不是</em>給正常網站所使用的。為了安全考量,已取消對該網址的連線。</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">連線被重設</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[<p>在嘗試連線時,此網路連線被中斷。請再試一次。</p>
+ <ul>
+ <li>網站可能太忙碌或暫時無法使用,請稍候再試一次。</li>
+ <li>若您無法載入任何頁面,請檢查裝置的數據或 Wi-Fi 連線。</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">不安全的檔案格式</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>請向網站擁有者回報此問題。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">內容毀損錯誤</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>因為在資料傳輸過程當中偵測到錯誤,無法顯示您正要檢視的頁面。</p>
+ <ul>
+ <li>請通知網站管理者以讓他們知道這個問題。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">內容發生錯誤</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>因為在資料傳輸過程當中偵測到錯誤,無法顯示您正要檢視的頁面。</p>
+ <ul>
+ <li>請通知網站管理者以讓他們知道這個問題。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">內容編碼錯誤</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>您嘗試檢視的頁面無法顯示,因為其中使用了無效或不支援的壓縮類型。</p>
+ <ul>
+ <li>請向網站擁有者回報此問題。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">找不到網址</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[<p>瀏覽器找不到網址指定的伺服器主機。</p>
+ <ul>
+ <li>請檢查網址是否有打錯?例如把
+ <strong>www</strong>.example.com 打成
+ <strong>ww</strong>.example.com</li>
+ <li>若無法載入任何網站,請檢查裝置的網路連線狀態。</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">沒有網路連線</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">請檢查您的網路連線是否正常,或者稍後再重新載入頁面。</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">重新載入</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">不正確的網址</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>網址不正確。請檢查網址是否有誤後再重試。</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">不正確的網址</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>網址通常長得像 <strong>http://www.example.com/</strong>。</li>
+ <li>請確定您用的是斜線(例: <strong>/</strong>)。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">未知的通訊協定</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>指定的網址使用了瀏覽器無法辨識的通訊協定(如 <q>wxyz://</q>),無法正確連線。</p>
+ <ul>
+ <li>您是要存取非文字的影音多媒體服務嗎?請檢查網站上是否有更多需求。</li>
+ <li>某些通訊協定需要另外安裝其它軟體或外掛程式才能使用。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">找不到檔案</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>或許此項目已被改名或刪除了?</li>
+ <li>是否有拼錯字、大小寫錯誤?</li>
+ <li>您有存取該項目的權限嗎?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">對檔案的存取要求已被拒絕</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>可能是檔案被刪除、移動了,或存取權限不足。</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy 伺服器拒絕連線</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>瀏覽器被設定為使用 Proxy 代理伺服器,但該伺服器拒絕連線。</p>
+ <ul>
+ <li>瀏覽器的 Proxy 設定正確嗎?請檢查並重試。</li>
+ <li>該 Proxy 允許您從這個區域的網路連線嗎?</li>
+ <li>仍有問題?請洽詢您的網路管理員或網路業者。</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">找不到 Proxy 伺服器</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[<p>有設定瀏覽器使用 Proxy 代理伺服器,但找不到該伺服器。</p>
+ <ul>
+ <li>瀏覽器的 Proxy 設定正確嗎?請檢查並重試。</li>
+ <li>裝置的網路連線正常嗎?</li>
+ <li>仍有問題?請洽詢您的網路管理員或網路業者。</li>
+ </ul>]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">惡意軟體網站問題</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>%1$s 這個網站被回報為有害網站,已依照您的安全性偏好設定予以封鎖。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">不安全網站問題</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>%1$s 這個網站被回報為提供不安全的軟體,已依照您的安全性偏好設定予以封鎖。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">危險網站問題</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>%1$s 這個網站被回報為危險網站,已依照您的安全性偏好設定予以封鎖。</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">詐騙網站問題</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>%1$s 這個網站被回報為詐騙網站,已依照您的安全性偏好設定予以封鎖。</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">無法使用安全網站</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[為了加強安全性,您開啟了純 HTTPS 模式,但 <em>%1$s</em> 的 HTTPS 版本無法使用。]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">繼續前往 HTTP 網站</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/main/res/values/strings.xml b/mobile/android/android-components/components/browser/errorpages/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..2519b0d065
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/main/res/values/strings.xml
@@ -0,0 +1,307 @@
+<?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 xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- The button that appears at the bottom of an error page. -->
+ <string name="mozac_browser_errorpages_page_refresh">Try Again</string>
+
+ <!-- The document title and heading of an error page shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_title">Cannot Complete Request</string>
+ <!-- The error message shown when a website cannot be loaded for an unknown reason. -->
+ <string name="mozac_browser_errorpages_generic_message"><![CDATA[
+ <p>Additional information about this problem or error is currently unavailable.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_title">Secure Connection Failed</string>
+ <!-- The error message shown when a website sends back unusual and incorrect credentials for an SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_ssl_message"><![CDATA[
+ <ul>
+ <li>The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.</li>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_title">Secure Connection Failed</string>
+ <!-- The error message shown when a website sends has an invalid or expired SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_message"><![CDATA[
+ <ul>
+ <li>This could be a problem with the server’s configuration, or it could be someone trying to impersonate the server.</li>
+ <li>If you have connected to this server successfully in the past, the error may be temporary, and you can try again later.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_advanced">Advanced…</string>
+ <!-- The advanced certificate information shown when a website sends has an invalid SSL certificate. The %1$s will be replaced by the app name and %2$s will be replaced by website URL. It's only shown when a website has an invalid SSL certificate. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_techInfo"><![CDATA[
+ <label>Someone could be trying to impersonate the site and you should not continue.</label>
+ <br><br>
+ <label>Websites prove their identity via certificates. %1$s does not trust <b>%2$s</b> because its certificate issuer is unknown, the certificate is self-signed, or the server is not sending the correct intermediate certificates.</label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_back">Go Back (Recommended)</string>
+ <!-- The text shown inside the advanced options button used to bypass the invalid SSL certificate. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_cert_accept_temporary">Accept the Risk and Continue</string>
+
+ <!-- The document title and heading of the error page shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_title">This website requires a secure connection.</string>
+ <!-- The error message shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_message"><![CDATA[
+ <ul>
+ <li>The page you are trying to view cannot be shown because this website requires a secure connection.</li>
+ <li>The issue is most likely with the website, and there is nothing you can do to resolve it.</li>
+ <li>You can notify the website’s administrator about the problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The text shown inside the advanced button used to expand the advanced options. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_advanced">Advanced…</string>
+
+ <!-- The advanced certificate information shown when a website uses HSTS. The %1$s will be replaced by the website URL and %2$s will be replaced by the app name. It's only shown when a website uses HSTS. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_techInfo2"><![CDATA[
+ <label> <b>%1$s</b> has a security policy called HTTP Strict Transport Security (HSTS), which means that <b>%2$s</b> can only connect to it securely. You can’t add an exception to visit this site. </label>
+ ]]></string>
+ <!-- The text shown inside the advanced options button used to go back. It's only shown if the user has expanded the advanced options. -->
+ <string name="mozac_browser_errorpages_security_bad_hsts_cert_back">Go Back</string>
+
+ <!-- The document title and heading of the error page shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_title">The connection was interrupted</string>
+ <!-- The error message shown when the user's network connection is interrupted while connecting to a website. -->
+ <string name="mozac_browser_errorpages_net_interrupt_message"><![CDATA[
+ <p>The browser connected successfully, but the connection was interrupted while transferring information. Please try again.</p>
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website takes too long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_title">The connection has timed out</string>
+ <!-- The error message shown when a website took long to load. -->
+ <string name="mozac_browser_errorpages_net_timeout_message"><![CDATA[
+ <p>The requested site did not respond to a connection request and the browser has stopped waiting for a reply.</p>
+ <ul>
+ <li>Could the server be experiencing high demand or a temporary outage? Try again later.</li>
+ <li>Are you unable to browse other sites? Check the device’s network connection.</li>
+ <li>Is your device or network protected by a firewall or proxy? Incorrect settings can interfere with Web browsing.</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_title">Unable to connect</string>
+ <!-- The error message shown when a website could not be reached. -->
+ <string name="mozac_browser_errorpages_connection_failure_message"><![CDATA[
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_title">Unexpected response from server</string>
+ <!-- The error message shown when a website responds in an unexpected way and the browser cannot continue. -->
+ <string name="mozac_browser_errorpages_unknown_socket_type_message"><![CDATA[
+ <p>The site responded to the network request in an unexpected way and the browser cannot continue.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_title">The page isn’t redirecting properly</string>
+ <!-- The error message shown when the browser gets stuck in an infinite loop when loading a website. -->
+ <string name="mozac_browser_errorpages_redirect_loop_message"><![CDATA[
+ <p>The browser has stopped trying to retrieve the requested item. The site is redirecting the request in a way that will never complete.</p>
+ <ul>
+ <li>Have you disabled or blocked cookies required by this site?</li>
+ <li>If accepting the site’s cookies does not resolve the problem, it is likely a server configuration issue and not your device.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_title">Offline Mode</string>
+ <!-- The error message shown when a website cannot be loaded because the browser is in offline mode. -->
+ <string name="mozac_browser_errorpages_offline_message"><![CDATA[
+ <p>The browser is operating in its offline mode and cannot connect to the requested item.</p>
+ <ul>
+ <li>Is the device connected to an active network?</li>
+ <li>Press “Try Again” to switch to online mode and reload the page.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_title">Port restricted for security reasons</string>
+ <!-- The error message shown when the browser prevents loading a website on a restricted port. -->
+ <string name="mozac_browser_errorpages_port_blocked_message"><![CDATA[
+ <p>The requested address specified a port (e.g., <q>mozilla.org:80</q> for port 80 on mozilla.org) normally used for purposes <em>other</em> than Web browsing. The browser has canceled the request for your protection and security.</p>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_title">The connection was reset</string>
+ <!-- The error message shown when the Internet connection is disrupted while loading a website. -->
+ <string name="mozac_browser_errorpages_net_reset_message"><![CDATA[
+ <p>The network link was interrupted while negotiating a connection. Please try again.</p>
+ <ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_title">Unsafe File Type</string>
+ <!-- The error message shown when the browser refuses to load a type of file that is considered unsafe. -->
+ <string name="mozac_browser_errorpages_unsafe_content_type_message"><![CDATA[
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of the error page shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_title">Corrupted Content Error</string>
+ <!-- The error message shown when shown when a file cannot be loaded because of a detected data corruption. -->
+ <string name="mozac_browser_errorpages_corrupted_content_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_content_crashed_title">Content crashed</string>
+ <string name="mozac_browser_errorpages_content_crashed_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_invalid_content_encoding_title">Content Encoding Error</string>
+ <string name="mozac_browser_errorpages_invalid_content_encoding_message"><![CDATA[
+ <p>The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.</p>
+ <ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_host_title">Address Not Found</string>
+ <!-- In the example, the two URLs in markup do not need to be translated. -->
+ <string name="mozac_browser_errorpages_unknown_host_message"><![CDATA[
+ <p>The browser could not find the host server for the provided address.</p>
+ <ul>
+ <li>Check the address for typing errors such as
+ <strong>ww</strong>.example.com instead of
+ <strong>www</strong>.example.com.</li>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_no_internet_title">No internet connection</string>
+ <!-- The main body text of this error page. It will be shown beneath the title -->
+ <string name="mozac_browser_errorpages_no_internet_message">Check your network connection or try reloading the page in a few moments.</string>
+ <!-- Text that will show up on the button at the bottom of the error page -->
+ <string name="mozac_browser_errorpages_no_internet_refresh_button">Reload</string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title">Invalid Address</string>
+ <string name="mozac_browser_errorpages_malformed_uri_message"><![CDATA[
+ <p>The provided address is not in a recognized format. Please check the location bar for mistakes and try again.</p>
+ ]]></string>
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_malformed_uri_title_alternative">The address isn’t valid</string>
+ <!-- This string contains markup. The URL should not be localized. -->
+ <string name="mozac_browser_errorpages_malformed_uri_message_alternative"><![CDATA[
+ <ul>
+ <li>Web addresses are usually written like <strong>http://www.example.com/</strong></li>
+ <li>Make sure that you’re using forward slashes (i.e. <strong>/</strong>).</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_protocol_title">Unknown Protocol</string>
+ <string name="mozac_browser_errorpages_unknown_protocol_message"><![CDATA[
+ <p>The address specifies a protocol (e.g., <q>wxyz://</q>) the browser does not recognize, so the browser cannot properly connect to the site.</p>
+ <ul>
+ <li>Are you trying to access multimedia or other non-text services? Check the site for extra requirements.</li>
+ <li>Some protocols may require third-party software or plugins before the browser can recognize them.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_not_found_title">File Not Found</string>
+ <string name="mozac_browser_errorpages_file_not_found_message"><![CDATA[
+ <ul>
+ <li>Could the item have been renamed, removed, or relocated?</li>
+ <li>Is there a spelling, capitalization, or other typographical error in the address?</li>
+ <li>Do you have sufficient access permissions to the requested item?</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_file_access_denied_title">Access to the file was denied</string>
+ <string name="mozac_browser_errorpages_file_access_denied_message"><![CDATA[
+ <ul>
+ <li>It may have been removed, moved, or file permissions may be preventing access.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_proxy_connection_refused_title">Proxy Server Refused Connection</string>
+ <string name="mozac_browser_errorpages_proxy_connection_refused_message"><![CDATA[
+ <p>The browser is configured to use a proxy server, but the proxy refused a connection.</p>
+ <ul>
+ <li>Is the browser’s proxy configuration correct? Check the settings and try again.</li>
+ <li>Does the proxy service allow connections from this network?</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_unknown_proxy_host_title">Proxy Server Not Found</string>
+ <string name="mozac_browser_errorpages_unknown_proxy_host_message"><![CDATA[
+ <p>The browser is configured to use a proxy server, but the proxy could not be found.</p>
+ <ul>
+ <li>Is the browser’s proxy configuration correct? Check the settings and try again.</li>
+ <li>Is the device connected to an active network?</li>
+ <li>Still having trouble? Consult your network administrator or Internet provider for assistance.</li>
+ </ul>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_title">Malware site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_malware_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as an attack site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_title">Unwanted site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_browsing_unwanted_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as serving unwanted software and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_title">Harmful site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_harmful_uri_message"><![CDATA[
+ <p>The site at %1$s has been reported as a potentially harmful site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The document title and heading of an error page. -->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_title">Deceptive site issue</string>
+ <!-- The %1$s will be replaced by the malicious website URL-->
+ <string name="mozac_browser_errorpages_safe_phishing_uri_message"><![CDATA[
+ <p>This web page at %1$s has been reported as a deceptive site and has been blocked based on your security preferences.</p>
+ ]]></string>
+
+ <!-- The title of the error page for websites that do not support HTTPS when HTTPS-Only is turned on -->
+ <string name="mozac_browser_errorpages_httpsonly_title">Secure Site Not Available</string>
+ <!-- The text of the error page for websites that do not support HTTPS when HTTPS-Only is turned on. %1$s will be replaced with the URL of the website. -->
+ <string name="mozac_browser_errorpages_httpsonly_message"><![CDATA[You’ve enabled HTTPS-Only Mode for enhanced security, and a HTTPS version of <em>%1$s</em> is not available.]]></string>
+ <!-- Button on error page for websites that do not support HTTPS when HTTPS-Only is turned on. Clicking the button allows the user to nevertheless load the website using HTTP. -->
+ <string name="mozac_browser_errorpages_httpsonly_button">Continue to HTTP Site</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/errorpages/src/test/java/mozilla/components/browser/errorpages/ErrorPagesTest.kt b/mobile/android/android-components/components/browser/errorpages/src/test/java/mozilla/components/browser/errorpages/ErrorPagesTest.kt
new file mode 100644
index 0000000000..c15028f498
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/test/java/mozilla/components/browser/errorpages/ErrorPagesTest.kt
@@ -0,0 +1,107 @@
+/* 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.browser.errorpages
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.errorpages.ErrorPages.createUrlEncodedErrorPage
+import mozilla.components.support.ktx.kotlin.urlEncode
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ErrorPagesTest {
+
+ @Test
+ fun `createUrlEncodedErrorPage allows overriding title and description`() {
+ val errorPage = createUrlEncodedErrorPage(
+ testContext,
+ ErrorType.ERROR_HTTPS_ONLY,
+ "https://localhost/",
+ )
+
+ assertFalse(errorPage.contains("radio"))
+ assertFalse(errorPage.contains("spider"))
+
+ val customErrorPage = createUrlEncodedErrorPage(
+ testContext,
+ ErrorType.ERROR_HTTPS_ONLY,
+ "https://localhost/",
+ titleOverride = { errorType ->
+ assertEquals(ErrorType.ERROR_HTTPS_ONLY, errorType)
+ "radio"
+ },
+ descriptionOverride = { errorType ->
+ assertEquals(ErrorType.ERROR_HTTPS_ONLY, errorType)
+ "spider"
+ },
+ )
+
+ assertTrue(customErrorPage.contains("radio"))
+ assertTrue(customErrorPage.contains("spider"))
+ }
+
+ @Test
+ fun `createUrlEncodedErrorPage should encoded error information into the URL`() {
+ assertUrlEncodingIsValid(ErrorType.UNKNOWN)
+ assertUrlEncodingIsValid(ErrorType.ERROR_SECURITY_SSL)
+ assertUrlEncodingIsValid(ErrorType.ERROR_SECURITY_BAD_CERT)
+ assertUrlEncodingIsValid(ErrorType.ERROR_NET_INTERRUPT)
+ assertUrlEncodingIsValid(ErrorType.ERROR_NET_TIMEOUT)
+ assertUrlEncodingIsValid(ErrorType.ERROR_CONNECTION_REFUSED)
+ assertUrlEncodingIsValid(ErrorType.ERROR_UNKNOWN_SOCKET_TYPE)
+ assertUrlEncodingIsValid(ErrorType.ERROR_REDIRECT_LOOP)
+ assertUrlEncodingIsValid(ErrorType.ERROR_OFFLINE)
+ assertUrlEncodingIsValid(ErrorType.ERROR_PORT_BLOCKED)
+ assertUrlEncodingIsValid(ErrorType.ERROR_NET_RESET)
+ assertUrlEncodingIsValid(ErrorType.ERROR_UNSAFE_CONTENT_TYPE)
+ assertUrlEncodingIsValid(ErrorType.ERROR_CORRUPTED_CONTENT)
+ assertUrlEncodingIsValid(ErrorType.ERROR_CONTENT_CRASHED)
+ assertUrlEncodingIsValid(ErrorType.ERROR_INVALID_CONTENT_ENCODING)
+ assertUrlEncodingIsValid(ErrorType.ERROR_UNKNOWN_HOST)
+ assertUrlEncodingIsValid(ErrorType.ERROR_MALFORMED_URI)
+ assertUrlEncodingIsValid(ErrorType.ERROR_UNKNOWN_PROTOCOL)
+ assertUrlEncodingIsValid(ErrorType.ERROR_FILE_NOT_FOUND)
+ assertUrlEncodingIsValid(ErrorType.ERROR_FILE_ACCESS_DENIED)
+ assertUrlEncodingIsValid(ErrorType.ERROR_PROXY_CONNECTION_REFUSED)
+ assertUrlEncodingIsValid(ErrorType.ERROR_UNKNOWN_PROXY_HOST)
+ assertUrlEncodingIsValid(ErrorType.ERROR_SAFEBROWSING_MALWARE_URI)
+ assertUrlEncodingIsValid(ErrorType.ERROR_SAFEBROWSING_UNWANTED_URI)
+ assertUrlEncodingIsValid(ErrorType.ERROR_SAFEBROWSING_HARMFUL_URI)
+ assertUrlEncodingIsValid(ErrorType.ERROR_SAFEBROWSING_PHISHING_URI)
+ assertUrlEncodingIsValid(ErrorType.ERROR_HTTPS_ONLY)
+ assertUrlEncodingIsValid(ErrorType.ERROR_BAD_HSTS_CERT)
+ }
+
+ private fun assertUrlEncodingIsValid(errorType: ErrorType) {
+ val htmlFilename = "htmlResource.html"
+
+ val uri = "sampleUri"
+
+ val errorPage = createUrlEncodedErrorPage(
+ testContext,
+ errorType,
+ uri,
+ htmlFilename,
+ )
+
+ val expectedImageName = if (errorType.imageNameRes != null) {
+ testContext.resources.getString(errorType.imageNameRes!!) + ".svg"
+ } else {
+ ""
+ }
+
+ assertTrue(errorPage.startsWith("resource://android/assets/$htmlFilename"))
+ assertTrue(errorPage.contains("&button=${testContext.resources.getString(errorType.refreshButtonRes).urlEncode()}"))
+
+ val description = testContext.resources.getString(errorType.messageRes, uri).replace("<ul>", "<ul role=\"presentation\">")
+
+ assertTrue(errorPage.contains("&description=${description.urlEncode()}"))
+ assertTrue(errorPage.contains("&image=$expectedImageName"))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/errorpages/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/errorpages/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/errorpages/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/browser/icons/.gitignore b/mobile/android/android-components/components/browser/icons/.gitignore
new file mode 100644
index 0000000000..2ddf5f27b1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/.gitignore
@@ -0,0 +1 @@
+manifest.json
diff --git a/mobile/android/android-components/components/browser/icons/README.md b/mobile/android/android-components/components/browser/icons/README.md
new file mode 100644
index 0000000000..eeb521fc70
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Browser > Icons
+
+A component for loading and storing website icons (like [Favicons](https://en.wikipedia.org/wiki/Favicon)).
+
+## Usage
+
+### 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:browser-icons:{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/browser/icons/build.gradle b/mobile/android/android-components/components/browser/icons/build.gradle
new file mode 100644
index 0000000000..a0117897fb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/build.gradle
@@ -0,0 +1,92 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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']
+ }
+ }
+
+ sourceSets {
+ androidTest {
+ // Use the same resources as the unit tests
+ resources.srcDirs += ['src/test/resources']
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.browser.icons'
+}
+
+tasks.register("updateBuiltInExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/browser-icons')
+}
+
+dependencies {
+ implementation project(':concept-base')
+ implementation project(':concept-engine')
+ implementation project(':concept-fetch')
+ implementation project(':browser-state')
+ implementation project(':support-images')
+ implementation project(':support-ktx')
+
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_compose_material
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_palette
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.thirdparty_disklrucache
+
+ implementation ComponentsDependencies.thirdparty_androidsvg
+
+ testImplementation project(':support-test')
+ testImplementation project(':lib-fetch-httpurlconnection')
+ testImplementation project(':lib-fetch-okhttp')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.kotlin_reflect
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.testing_coroutines
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
+
+preBuild.dependsOn updateBuiltInExtensionVersion
diff --git a/mobile/android/android-components/components/browser/icons/proguard-rules.pro b/mobile/android/android-components/components/browser/icons/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/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/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt b/mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt
new file mode 100644
index 0000000000..61dc01d348
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt
@@ -0,0 +1,70 @@
+/* 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.browser.icons
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.icons.generator.IconGenerator
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+
+class OnDeviceBrowserIconsTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun dataUriLoad() = runTest {
+ val request = IconRequest(
+ url = "https://www.mozilla.org",
+ size = IconRequest.Size.DEFAULT,
+ resources = listOf(
+ IconRequest.Resource(
+ url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAA21BMVE" +
+ "UAAAADAwMREREhISExMTFBQUFRUVFhYWFxcXGBgYFQUlBgYmCQkJA/Gxt8Hx9BQEFQUVBQj0JenE+A" +
+ "gICgoKAgISA7Li5cODhQUFBgYGBudmx9hXywsLAwMDA5OlJISWF5eWiBgX+Qj5Cgar+vo7bAwMBAQE" +
+ "A7PnxJTIpycm7Cwj2YmIagn6CujMG/t8PPz89wcHCAgH+Sko2foJ+vr6+/v7/f399fX19vb2+mdFmd" +
+ "ioCfn5+Xy8u11dXv7+9/f3+lh3anm5Wk2dnD5OT8/PyPj4/e3t/u7u7///9PuU9UAAAAq0lEQVR4Ae" +
+ "3YIQ7CQBCF4T4ou0uQaCxX4P5XAtVicA8zadIsTabh/9W6TzzRdDQ4bfY6DNsFsiYQEK3uFKSgo2OT" +
+ "JAgIiL7K5Sff88mvxibpEBCQqzt3NLkWxCZJEBAQ3Ra/2LNfdf//8SAgILpzufsjBATkGdQ6kquOTZ" +
+ "IgICB69NzmuNztDAEBkauLTU6uBDVXHJtkQUBAqivu7V5ObnZjUHGjY5MkCAjIBymjUnvFUjKoAAAA" +
+ "AElFTkSuQmCC",
+ sizes = listOf(Size(64, 64)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ ),
+ )
+
+ val icon = BrowserIcons(
+ context,
+ httpClient = object : Client() {
+ override fun fetch(request: Request): Response {
+ @Suppress("TooGenericExceptionThrown")
+ throw RuntimeException("Client execution not expected")
+ }
+ },
+ generator = object : IconGenerator {
+ override fun generate(context: Context, request: IconRequest): Icon {
+ @Suppress("TooGenericExceptionThrown")
+ throw RuntimeException("Generator execution not expected")
+ }
+ },
+ ).loadIcon(request).await()
+
+ assertNotNull(icon)
+
+ val bitmap = icon.bitmap
+ assertEquals(100, bitmap.width)
+ assertEquals(100, bitmap.height)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt b/mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt
new file mode 100644
index 0000000000..7310666416
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt
@@ -0,0 +1,129 @@
+/* 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 mozilla.components.browser.icons.decoder.ICOIconDecoder
+import mozilla.components.browser.icons.decoder.ico.decodeDirectoryEntries
+import mozilla.components.support.images.DesiredSize
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+
+class ICOIconDecoderTest {
+ @Test
+ fun testIconSizesOfMicrosoftFavicon() {
+ val icon = loadIcon("microsoft_favicon.ico")
+ val entries = decodeDirectoryEntries(icon, 1024)
+
+ assertEquals(6, entries.size)
+
+ val bitmaps = entries
+ .mapNotNull { entry -> entry.toBitmap(icon) }
+ .sortedBy { bitmap -> bitmap.width }
+
+ assertEquals(6, bitmaps.size)
+
+ assertEquals(16, bitmaps[0].width)
+ assertEquals(16, bitmaps[0].height)
+
+ assertEquals(24, bitmaps[1].width)
+ assertEquals(24, bitmaps[1].height)
+
+ assertEquals(32, bitmaps[2].width)
+ assertEquals(32, bitmaps[2].height)
+
+ assertEquals(48, bitmaps[3].width)
+ assertEquals(48, bitmaps[3].height)
+
+ assertEquals(72, bitmaps[4].width)
+ assertEquals(72, bitmaps[4].height)
+
+ assertEquals(128, bitmaps[5].width)
+ assertEquals(128, bitmaps[5].height)
+ }
+
+ @Test
+ fun testBestMicrosoftIconTarget192Max256() {
+ val icon = loadIcon("microsoft_favicon.ico")
+
+ val decoder = ICOIconDecoder()
+ val bitmap = decoder.decode(icon, DesiredSize(192, 192, 256, 2.0f))
+
+ assertNotNull(bitmap)
+
+ assertEquals(128, bitmap!!.width)
+ assertEquals(128, bitmap.height)
+ }
+
+ @Test
+ fun testBestMicrosoftIconTarget64Max120() {
+ val icon = loadIcon("microsoft_favicon.ico")
+
+ val decoder = ICOIconDecoder()
+ val bitmap = decoder.decode(icon, DesiredSize(64, 64, 120, 2.0f))
+
+ assertNotNull(bitmap)
+
+ assertEquals(72, bitmap!!.width)
+ assertEquals(72, bitmap.height)
+ }
+
+ @Test
+ fun testIconSizesOfGolemFavicon() {
+ val icon = loadIcon("golem_favicon.ico")
+
+ val entries = decodeDirectoryEntries(icon, 1024)
+
+ assertEquals(5, entries.size)
+
+ val bitmaps = entries
+ .mapNotNull { entry -> entry.toBitmap(icon) }
+ .sortedBy { bitmap -> bitmap.width }
+
+ assertEquals(5, bitmaps.size)
+
+ assertEquals(16, bitmaps[0].width)
+ assertEquals(16, bitmaps[0].height)
+
+ assertEquals(24, bitmaps[1].width)
+ assertEquals(24, bitmaps[1].height)
+
+ assertEquals(32, bitmaps[2].width)
+ assertEquals(32, bitmaps[2].height)
+
+ assertEquals(48, bitmaps[3].width)
+ assertEquals(48, bitmaps[3].height)
+
+ assertEquals(256, bitmaps[4].width)
+ assertEquals(256, bitmaps[4].height)
+ }
+
+ @Test
+ fun testIconSizesOfNvidiaFavicon() {
+ val icon = loadIcon("nvidia_favicon.ico")
+
+ val entries = decodeDirectoryEntries(icon, 1024)
+
+ assertEquals(3, entries.size)
+
+ val bitmaps = entries
+ .mapNotNull { entry -> entry.toBitmap(icon) }
+ .sortedBy { bitmap -> bitmap.width }
+
+ assertEquals(3, bitmaps.size)
+
+ assertEquals(16, bitmaps[0].width)
+ assertEquals(16, bitmaps[0].height)
+
+ assertEquals(32, bitmaps[1].width)
+ assertEquals(32, bitmaps[1].height)
+
+ assertEquals(48, bitmaps[2].width)
+ assertEquals(48, bitmaps[2].height)
+ }
+
+ private fun loadIcon(fileName: String): ByteArray =
+ javaClass.getResourceAsStream("/ico/$fileName")!!
+ .buffered()
+ .readBytes()
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/icons/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/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/browser/icons/src/main/assets/extensions/browser-icons/icons.js b/mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/icons.js
new file mode 100644
index 0000000000..20eada9a19
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/icons.js
@@ -0,0 +1,81 @@
+/* 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/. */
+
+ /*
+ * This web extension looks for known icon tags, collects URLs and available
+ * meta data (e.g. sizes) and passes that to the app code.
+ */
+
+/**
+ * Takes a DOMTokenList and returns a String array.
+ */
+function sizesToList(sizes) {
+ if (sizes == null) {
+ return []
+ }
+
+ if (!(sizes instanceof DOMTokenList)) {
+ return []
+ }
+
+ return Array.from(sizes)
+}
+
+function collect_link_icons(icons, rel) {
+ document.querySelectorAll('link[rel="' + rel + '"]').forEach(
+ function(currentValue, currentIndex, listObj) {
+ icons.push({
+ 'type': rel,
+ 'href': currentValue.href,
+ 'sizes': sizesToList(currentValue.sizes),
+ 'mimeType': currentValue.type
+ });
+ })
+}
+
+function collect_meta_property_icons(icons, property) {
+ document.querySelectorAll('meta[property="' + property + '"]').forEach(
+ function(currentValue, currentIndex, listObj) {
+ icons.push({
+ 'type': property,
+ 'href': currentValue.content
+ })
+ }
+ )
+}
+
+function collect_meta_name_icons(icons, name) {
+ document.querySelectorAll('meta[name="' + name + '"]').forEach(
+ function(currentValue, currentIndex, listObj) {
+ icons.push({
+ 'type': name,
+ 'href': currentValue.content
+ })
+ }
+ )
+}
+
+let icons = [];
+
+collect_link_icons(icons, 'icon');
+collect_link_icons(icons, 'shortcut icon');
+collect_link_icons(icons, 'fluid-icon')
+collect_link_icons(icons, 'apple-touch-icon')
+collect_link_icons(icons, 'image_src')
+collect_link_icons(icons, 'apple-touch-icon image_src')
+collect_link_icons(icons, 'apple-touch-icon-precomposed')
+
+collect_meta_property_icons(icons, 'og:image')
+collect_meta_property_icons(icons, 'og:image:url')
+collect_meta_property_icons(icons, 'og:image:secure_url')
+
+collect_meta_name_icons(icons, 'twitter:image')
+collect_meta_name_icons(icons, 'msapplication-TileImage')
+
+let message = {
+ 'url': document.location.href,
+ 'icons': icons
+}
+
+browser.runtime.sendNativeMessage("MozacBrowserIcons", message);
diff --git a/mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.template.json b/mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.template.json
new file mode 100644
index 0000000000..846fc92101
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.template.json
@@ -0,0 +1,22 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "icons@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - Browser Icons",
+ "version": "${version}",
+ "content_scripts": [
+ {
+ "matches": ["*://*/*"],
+ "js": ["icons.js"],
+ "run_at": "document_end"
+ }
+ ],
+ "permissions": [
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent"
+ ]
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/assets/mozac.browser.icons/icons-top200.json b/mobile/android/android-components/components/browser/icons/src/main/assets/mozac.browser.icons/icons-top200.json
new file mode 100644
index 0000000000..270a3570f9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/assets/mozac.browser.icons/icons-top200.json
@@ -0,0 +1,1056 @@
+[
+ {
+ "image_url": "https://youradchoices.com/./DAA_style/YAC/icon.png",
+ "domains": [
+ "aboutads.info"
+ ]
+ },
+ {
+ "image_url": "https://accounts.google.com/favicon.ico",
+ "domains": [
+ "accounts.google.com"
+ ]
+ },
+ {
+ "image_url": "https://static.addtoany.com/images/icon-180.png",
+ "domains": [
+ "addtoany.com"
+ ]
+ },
+ {
+ "image_url": "https://amazon.ca/favicon.ico",
+ "domains": [
+ "amazon.ca"
+ ]
+ },
+ {
+ "image_url": "https://amazon.cn/favicon.ico",
+ "domains": [
+ "amazon.cn"
+ ]
+ },
+ {
+ "image_url": "https://amazon.co.jp/favicon.ico",
+ "domains": [
+ "amazon.co.jp"
+ ]
+ },
+ {
+ "image_url": "https://amazon.co.uk/favicon.ico",
+ "domains": [
+ "amazon.co.uk",
+ "amazon.co.uk"
+ ]
+ },
+ {
+ "image_url": "https://amazon.com/favicon.ico",
+ "domains": [
+ "amazon.com",
+ "amazon.com"
+ ]
+ },
+ {
+ "image_url": "https://amazon.com.au/favicon.ico",
+ "domains": [
+ "amazon.com.au"
+ ]
+ },
+ {
+ "image_url": "https://amazon.com.br/favicon.ico",
+ "domains": [
+ "amazon.com.br"
+ ]
+ },
+ {
+ "image_url": "https://amazon.com.mx/favicon.ico",
+ "domains": [
+ "amazon.com.mx"
+ ]
+ },
+ {
+ "image_url": "https://amazon.de/favicon.ico",
+ "domains": [
+ "amazon.de"
+ ]
+ },
+ {
+ "image_url": "https://amazon.es/favicon.ico",
+ "domains": [
+ "amazon.es"
+ ]
+ },
+ {
+ "image_url": "https://amazon.fr/favicon.ico",
+ "domains": [
+ "amazon.fr"
+ ]
+ },
+ {
+ "image_url": "https://amazon.in/favicon.ico",
+ "domains": [
+ "amazon.in"
+ ]
+ },
+ {
+ "image_url": "https://amazon.it/favicon.ico",
+ "domains": [
+ "amazon.it"
+ ]
+ },
+ {
+ "image_url": "https://amzn.to/favicon.ico",
+ "domains": [
+ "amzn.to"
+ ]
+ },
+ {
+ "image_url": "https://apache.org/favicons/favicon-194x194.png",
+ "domains": [
+ "apache.org"
+ ]
+ },
+ {
+ "image_url": "https://apple.com/favicon.ico",
+ "domains": [
+ "apple.com"
+ ]
+ },
+ {
+ "image_url": "https://apps.apple.com/favicon.ico",
+ "domains": [
+ "apps.apple.com"
+ ]
+ },
+ {
+ "image_url": "https://archive.org/offshoot_assets/favicon.ico",
+ "domains": [
+ "archive.org"
+ ]
+ },
+ {
+ "image_url": "https://psstatic.cdn.bcebos.com/video/wiseindex/aa6eef91f8b5b1a33b454c401_1660835115000.png",
+ "domains": [
+ "baidu.com"
+ ]
+ },
+ {
+ "image_url": "https://bbc.co.uk/favicon.ico",
+ "domains": [
+ "bbc.co.uk"
+ ]
+ },
+ {
+ "image_url": "https://gn-web-assets.api.bbc.com/wwhp/20230821-1053-ff8cbd1fdf502854de8eb5a063ed1023f172e519/responsive/img/apple-touch/apple-touch-180.jpg",
+ "domains": [
+ "bbc.com"
+ ]
+ },
+ {
+ "image_url": "https://behance.net/favicon.ico",
+ "domains": [
+ "behance.net"
+ ]
+ },
+ {
+ "image_url": "https://www.bing.com:443/sa/simg/favicon-trans-bg-blue-mg-png.png",
+ "domains": [
+ "bing.com"
+ ]
+ },
+ {
+ "image_url": "https://docrdsfx76ssb.cloudfront.net/static/1695195096/pages/wp-content/uploads/2019/02/favicon.ico",
+ "domains": [
+ "bit.ly"
+ ]
+ },
+ {
+ "image_url": "https://blogger.com/favicon.ico",
+ "domains": [
+ "blogger.com"
+ ]
+ },
+ {
+ "image_url": "https://blogspot.com/favicon.ico",
+ "domains": [
+ "blogspot.com"
+ ]
+ },
+ {
+ "image_url": "https://www.bloomberg.com/favicon-black.png",
+ "domains": [
+ "bloomberg.com"
+ ]
+ },
+ {
+ "image_url": "https://www.businessinsider.com/public/assets/BI/US/favicons/apple-touch-icon-192x192.png?v=2023-06",
+ "domains": [
+ "businessinsider.com"
+ ]
+ },
+ {
+ "image_url": "https://www.ca.gov/images/apple-touch-icon-192x192.png",
+ "domains": [
+ "ca.gov"
+ ]
+ },
+ {
+ "image_url": "https://www.cbsnews.com/fly/bundles/cbsnewscore/icons/icon-192x192.png?v=cc1d20369924eaddf626a3a17b75fcb0",
+ "domains": [
+ "cbsnews.com"
+ ]
+ },
+ {
+ "image_url": "https://www.cdc.gov/TemplatePackage/4.0/assets/imgs/apple-touch-icon-180x180.png",
+ "domains": [
+ "cdc.gov"
+ ]
+ },
+ {
+ "image_url": "https://www.cloudflare.com/favicon.ico",
+ "domains": [
+ "cloudflare.com"
+ ]
+ },
+ {
+ "image_url": "https://www.cnbc.com/favicon.ico",
+ "domains": [
+ "cnbc.com"
+ ]
+ },
+ {
+ "image_url": "https://www.cnet.com/favicon-256-v3.png",
+ "domains": [
+ "cnet.com"
+ ]
+ },
+ {
+ "image_url": "https://www.cnn.com/media/sites/cnn/apple-touch-icon.png",
+ "domains": [
+ "cnn.com"
+ ]
+ },
+ {
+ "image_url": "https://cpanel.net/wp-content/themes/cPbase/assets/img/apple-touch-icon.png",
+ "domains": [
+ "cpanel.net"
+ ]
+ },
+ {
+ "image_url": "https://creativecommons.org/wp-content/uploads/2016/05/cc-site-icon-300x300.png",
+ "domains": [
+ "creativecommons.org"
+ ]
+ },
+ {
+ "image_url": "https://dailymail.co.uk/favicon.ico",
+ "domains": [
+ "dailymail.co.uk"
+ ]
+ },
+ {
+ "image_url": "https://www.debian.org/favicon.ico",
+ "domains": [
+ "debian.org"
+ ]
+ },
+ {
+ "image_url": "https://www.gstatic.com/devrel-devsite/prod/v47c000584df8fd5ed12554bcabcc16cd4fd28aee940bdc8ae9e35cab77cbb7da/developers/images/touchicon-180-new.png",
+ "domains": [
+ "developers.google.com"
+ ]
+ },
+ {
+ "image_url": "https://docs.google.com/favicon.ico",
+ "domains": [
+ "docs.google.com"
+ ]
+ },
+ {
+ "image_url": "https://www.doi.org/images/favicons/android-chrome-512x512.png",
+ "domains": [
+ "doi.org"
+ ]
+ },
+ {
+ "image_url": "https://ssl.gstatic.com/images/branding/product/2x/hh_drive_36dp.png",
+ "domains": [
+ "drive.google.com"
+ ]
+ },
+ {
+ "image_url": "https://cfl.dropboxstatic.com/static/images/favicon.ico",
+ "domains": [
+ "dropbox.com"
+ ]
+ },
+ {
+ "image_url": "https://pages.ebay.com/favicon.ico",
+ "domains": [
+ "ebay.com"
+ ]
+ },
+ {
+ "image_url": "https://commission.europa.eu/profiles/contrib/ewcms/themes/ewcms_theme/favicon.ico",
+ "domains": [
+ "ec.europa.eu"
+ ]
+ },
+ {
+ "image_url": "https://en.m.wikipedia.org/static/apple-touch/wikipedia.png",
+ "domains": [
+ "en.wikipedia.org"
+ ]
+ },
+ {
+ "image_url": "https://www.etsy.com/images/apple-touch-icon.png",
+ "domains": [
+ "etsy.com"
+ ]
+ },
+ {
+ "image_url": "https://european-union.europa.eu/profiles/contrib/ewcms/themes/ewcms_theme/favicon.ico",
+ "domains": [
+ "europa.eu"
+ ]
+ },
+ {
+ "image_url": "https://cdn.evbstatic.com/s3-build/prod/1383882-rc2023-09-26_16.04-94fcd55/django/images/favicons/favicon-194x194.png",
+ "domains": [
+ "eventbrite.com"
+ ]
+ },
+ {
+ "image_url": "https://static.xx.fbcdn.net/rsrc.php/v3/yN/r/EWLVhDVJTum.png",
+ "domains": [
+ "facebook.com"
+ ]
+ },
+ {
+ "image_url": "https://combo.staticflickr.com/pw/images/favicons/favicon-228.png",
+ "domains": [
+ "flickr.com"
+ ]
+ },
+ {
+ "image_url": "https://i.forbesimg.com/media/assets/appicons/forbes-app-icon_144x144.png",
+ "domains": [
+ "forbes.com"
+ ]
+ },
+ {
+ "image_url": "https://www.free.fr/assets/img/shared/fav/favicon-196x196.png",
+ "domains": [
+ "free.fr"
+ ]
+ },
+ {
+ "image_url": "https://www.ft.com/__origami/service/image/v2/images/raw/ftlogo-v1%3Abrand-ft-logo-square-coloured?source=update-logos&format=svg",
+ "domains": [
+ "ft.com"
+ ]
+ },
+ {
+ "image_url": "https://www.google.com/business/static/icons/favicon.ico?cache=2cf40ae",
+ "domains": [
+ "g.page"
+ ]
+ },
+ {
+ "image_url": "https://www.canada.ca/etc/designs/canada/wet-boew/assets/favicon.ico",
+ "domains": [
+ "gc.ca"
+ ]
+ },
+ {
+ "image_url": "https://github.githubassets.com/favicons/favicon.svg",
+ "domains": [
+ "github.com",
+ "github.com"
+ ]
+ },
+ {
+ "image_url": "https://pages.github.com/favicon.ico",
+ "domains": [
+ "github.io"
+ ]
+ },
+ {
+ "image_url": "https://www.gnu.org/graphics/gnu-head-mini.png",
+ "domains": [
+ "gnu.org"
+ ]
+ },
+ {
+ "image_url": "https://lumiere-a.akamaihd.net/v1/images/favicon-94e3862e7fb9_2bdfd7d9.png?region=0%2C0%2C64%2C64",
+ "domains": [
+ "go.com"
+ ]
+ },
+ {
+ "image_url": "https://google.com/favicon.ico",
+ "domains": [
+ "google.com"
+ ]
+ },
+ {
+ "image_url": "https://blog.google/static/blogv2/images/apple-touch-icon.png",
+ "domains": [
+ "googleblog.com"
+ ]
+ },
+ {
+ "image_url": "https://gravatar.com/favicon.ico",
+ "domains": [
+ "gravatar.com"
+ ]
+ },
+ {
+ "image_url": "https://www.harvard.edu/wp-content/uploads/2020/10/cropped-logo-branding-compressed-300x300.png",
+ "domains": [
+ "harvard.edu"
+ ]
+ },
+ {
+ "image_url": "https://www.hp.com/content/dam/sites/worldwide/dems/favicons/hp-blue-favicon.png",
+ "domains": [
+ "hp.com"
+ ]
+ },
+ {
+ "image_url": "https://www.huffpost.com/favicon.ico",
+ "domains": [
+ "huffingtonpost.com"
+ ]
+ },
+ {
+ "image_url": "https://www.ibm.com/content/dam/adobe-cms/default-images/favicon.svg",
+ "domains": [
+ "ibm.com"
+ ]
+ },
+ {
+ "image_url": "https://ietf.org/favicon.ico",
+ "domains": [
+ "ietf.org"
+ ]
+ },
+ {
+ "image_url": "https://m.media-amazon.com/images/G/01/imdb/images-ANDW73HA/android-mobile-196x196._CB479962153_.png",
+ "domains": [
+ "imdb.com"
+ ]
+ },
+ {
+ "image_url": "https://s.imgur.com/images/icons/icon-152.png",
+ "domains": [
+ "imgur.com"
+ ]
+ },
+ {
+ "image_url": "https://www.independent.co.uk/img/shortcut-icons/icon-512x512.png",
+ "domains": [
+ "independent.co.uk"
+ ]
+ },
+ {
+ "image_url": "https://static.cdninstagram.com/rsrc.php/v3/yI/r/VsNE-OHk_8a.png",
+ "domains": [
+ "instagram.com"
+ ]
+ },
+ {
+ "image_url": "https://issuu.com/icon.svg",
+ "domains": [
+ "issuu.com"
+ ]
+ },
+ {
+ "image_url": "https://itunes.apple.com/favicon.ico",
+ "domains": [
+ "itunes.apple.com"
+ ]
+ },
+ {
+ "image_url": "https://www.latimes.com:443/apple-touch-icon.png",
+ "domains": [
+ "latimes.com"
+ ]
+ },
+ {
+ "image_url": "https://line.me/favicon.ico",
+ "domains": [
+ "line.me"
+ ]
+ },
+ {
+ "image_url": "https://static.licdn.com/aero-v1/sc/h/al2o9zrvru7aqj8e1x2rzsrca",
+ "domains": [
+ "linkedin.com"
+ ]
+ },
+ {
+ "image_url": "https://website.linktr.ee/icons/icon-512x512.png",
+ "domains": [
+ "linktr.ee"
+ ]
+ },
+ {
+ "image_url": "https://loc.gov/favicon.ico",
+ "domains": [
+ "loc.gov"
+ ]
+ },
+ {
+ "image_url": "https://mail.google.com/favicon.ico",
+ "domains": [
+ "mail.google.com",
+ "mail.google.com"
+ ]
+ },
+ {
+ "image_url": "https://maps.gstatic.com/mapfiles/maps_lite/pwa/icons/maps15_bnuw3a_ios_192x192.png",
+ "domains": [
+ "maps.google.com"
+ ]
+ },
+ {
+ "image_url": "https://miro.medium.com/v2/resize:fill:152:152/1*sHhtYhaCe2Uc3IU0IgKwIQ.png",
+ "domains": [
+ "medium.com"
+ ]
+ },
+ {
+ "image_url": "https://www.microsoft.com/favicon.ico?v2",
+ "domains": [
+ "microsoft.com",
+ "live.com",
+ "outlook.com"
+ ]
+ },
+ {
+ "image_url": "https://www.miit.gov.cn/favicon.ico",
+ "domains": [
+ "miit.gov.cn"
+ ]
+ },
+ {
+ "image_url": "https://web.mit.edu/themes/mit/assets/favicon/favicon.svg",
+ "domains": [
+ "mit.edu"
+ ]
+ },
+ {
+ "image_url": "https://www.mozilla.org/media/img/favicons/mozilla/favicon-196x196.2af054fea211.png",
+ "domains": [
+ "mozilla.org"
+ ]
+ },
+ {
+ "image_url": "https://res.wx.qq.com/a/wx_fed/assets/res/OTE0YTAw.png",
+ "domains": [
+ "mp.weixin.qq.com"
+ ]
+ },
+ {
+ "image_url": "https://msn.com/favicon.ico",
+ "domains": [
+ "msn.com"
+ ]
+ },
+ {
+ "image_url": "https://cdn.shopify.com/shopifycloud/shopify/assets/favicon-bdd4952d510d9607e893c45e36bba6b0a8c9c59cb8344e7a75ebe7215112b7f5.png",
+ "domains": [
+ "myshopify.com"
+ ]
+ },
+ {
+ "image_url": "https://x.myspacecdn.com/new/common/images/favicons/144-Retina-iPad.png",
+ "domains": [
+ "myspace.com"
+ ]
+ },
+ {
+ "image_url": "https://www.nasa.gov/sites/all/themes/custom/nasatwo/images/apple-touch-icon-152x152.png",
+ "domains": [
+ "nasa.gov"
+ ]
+ },
+ {
+ "image_url": "https://www.nature.com/static/images/favicons/nature/apple-touch-icon-f39cb19454.png",
+ "domains": [
+ "nature.com"
+ ]
+ },
+ {
+ "image_url": "https://www.nginx.com/wp-content/uploads/2019/10/favicon-64x46.ico",
+ "domains": [
+ "nginx.com"
+ ]
+ },
+ {
+ "image_url": "https://nginx.org/favicon.ico",
+ "domains": [
+ "nginx.org"
+ ]
+ },
+ {
+ "image_url": "https://www.nih.gov/sites/all/themes/nih/apple-touch-icon.png",
+ "domains": [
+ "nih.gov"
+ ]
+ },
+ {
+ "image_url": "https://static-assets.npr.org/static/images/favicon/favicon-180x180.png",
+ "domains": [
+ "npr.org"
+ ]
+ },
+ {
+ "image_url": "https://www.nytimes.com/vi-assets/static-assets/apple-touch-icon-28865b72953380a40aa43318108876cb.png",
+ "domains": [
+ "nytimes.com"
+ ]
+ },
+ {
+ "image_url": "https://res.cdn.office.net/officehub/images/content/images/favicon_m365-67350a08e8.ico",
+ "domains": [
+ "office.com"
+ ]
+ },
+ {
+ "image_url": "https://cdn-production-opera-website.operacdn.com/staticfiles/assets/images/favicon/apple-touch-icon.555ee4c450b1.png",
+ "domains": [
+ "opera.com"
+ ]
+ },
+ {
+ "image_url": "https://www.oracle.com/favicon.ico",
+ "domains": [
+ "oracle.com"
+ ]
+ },
+ {
+ "image_url": "https://global.oup.com/system/images/favicon-180.png",
+ "domains": [
+ "oup.com"
+ ]
+ },
+ {
+ "image_url": "https://www.paypalobjects.com/webstatic/icon/pp258.png",
+ "domains": [
+ "paypal.com"
+ ]
+ },
+ {
+ "image_url": "https://www.php.net/favicon.svg?v=2",
+ "domains": [
+ "php.net"
+ ]
+ },
+ {
+ "image_url": "https://s.pinimg.com/webapp/logo_trans_144x144-5e37c0c6.png",
+ "domains": [
+ "pinterest.com"
+ ]
+ },
+ {
+ "image_url": "https://www.gstatic.com/android/market_images/web/favicon_v3.ico",
+ "domains": [
+ "play.google.com"
+ ]
+ },
+ {
+ "image_url": "https://workspaceupdates.googleblog.com/favicon.ico",
+ "domains": [
+ "plus.google.com",
+ "workspaceupdates.googleblog.com"
+ ]
+ },
+ {
+ "image_url": "https://podcasts.apple.com/favicon.ico",
+ "domains": [
+ "podcasts.apple.com"
+ ]
+ },
+ {
+ "image_url": "https://ssl.gstatic.com/policies/favicon.ico",
+ "domains": [
+ "policies.google.com"
+ ]
+ },
+ {
+ "image_url": "https://www.prnewswire.com/content/dam/prnewswire/icons/2019-Q4-PRN-Icon-32-32.png",
+ "domains": [
+ "prnewswire.com"
+ ]
+ },
+ {
+ "image_url": "https://cdn.ncbi.nlm.nih.gov/coreutils/nwds/img/favicons/favicon-192.png",
+ "domains": [
+ "pubmed.ncbi.nlm.nih.gov"
+ ]
+ },
+ {
+ "image_url": "https://mat1.gtimg.com/www/icon/favicon2.ico",
+ "domains": [
+ "qq.com"
+ ]
+ },
+ {
+ "image_url": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
+ "domains": [
+ "reddit.com"
+ ]
+ },
+ {
+ "image_url": "https://www.researchgate.net/favicon-96x96.png",
+ "domains": [
+ "researchgate.net"
+ ]
+ },
+ {
+ "image_url": "https://www.reuters.com/pf/resources/images/reuters/favicon/tr_kinesis.svg?d=157",
+ "domains": [
+ "reuters.com"
+ ]
+ },
+ {
+ "image_url": "https://cdn.shopify.com/static/shopify-favicon.png",
+ "domains": [
+ "shopify.com"
+ ]
+ },
+ {
+ "image_url": "https://mjs.sinaimg.cn/wap/online/public/images/addToHome/sina_114x114_v1.png",
+ "domains": [
+ "sina.com.cn"
+ ]
+ },
+ {
+ "image_url": "https://public.slidesharecdn.com/_next/static/media/favicon.7bc3d920.ico",
+ "domains": [
+ "slideshare.net"
+ ]
+ },
+ {
+ "image_url": "https://m.sndcdn.com/_next/static/images/apple-touch-icon-180-893d0d532e8fbba714cceb8d9eae9567.png",
+ "domains": [
+ "soundcloud.com"
+ ]
+ },
+ {
+ "image_url": "https://a.fsdn.com/con/img/sandiego/svg/originals/sf-icon-orange-no_sf.svg",
+ "domains": [
+ "sourceforge.net"
+ ]
+ },
+ {
+ "image_url": "https://open.spotifycdn.com/cdn/images/favicon.0f31d2ea.ico",
+ "domains": [
+ "spotify.com",
+ "open.spotify.com"
+ ]
+ },
+ {
+ "image_url": "https://www.springer.com/public/images/springer-icon.svg",
+ "domains": [
+ "springer.com"
+ ]
+ },
+ {
+ "image_url": "https://media-www.sqspcdn.com/logos/apple-touch-icon-1024.png",
+ "domains": [
+ "squarespace.com"
+ ]
+ },
+ {
+ "image_url": "https://cdn.sstatic.net/Sites/stackoverflow/Img/apple-touch-icon.png?v=c78bd457575a",
+ "domains": [
+ "stackoverflow.com"
+ ]
+ },
+ {
+ "image_url": "https://www-media.stanford.edu/assets/favicon/favicon-196x196.png",
+ "domains": [
+ "stanford.edu"
+ ]
+ },
+ {
+ "image_url": "https://cdn.statcdn.com/static/favicon.svg",
+ "domains": [
+ "statista.com"
+ ]
+ },
+ {
+ "image_url": "https://support.google.com/favicon.ico",
+ "domains": [
+ "support.google.com"
+ ]
+ },
+ {
+ "image_url": "https://prod.smassets.net/assets/static/images/surveymonkey/favicon.svg",
+ "domains": [
+ "surveymonkey.com"
+ ]
+ },
+ {
+ "image_url": "https://abs.twimg.com/favicons/favicon.ico",
+ "domains": [
+ "t.co"
+ ]
+ },
+ {
+ "image_url": "https://telegram.org/img/website_icon.svg?4",
+ "domains": [
+ "t.me",
+ "telegram.me"
+ ]
+ },
+ {
+ "image_url": "https://techcrunch.com/wp-content/uploads/2015/02/cropped-cropped-favicon-gradient.png?w=192",
+ "domains": [
+ "techcrunch.com"
+ ]
+ },
+ {
+ "image_url": "https://pa.tedcdn.com/apple-touch-icon.png",
+ "domains": [
+ "ted.com"
+ ]
+ },
+ {
+ "image_url": "https://www.telegraph.co.uk/etc.clientlibs/settings/wcm/designs/telegraph/core/clientlibs/core/resources/icons/favicon.svg",
+ "domains": [
+ "telegraph.co.uk"
+ ]
+ },
+ {
+ "image_url": "https://assets.guim.co.uk/static/frontend/icons/homescreen/apple-touch-icon.svg",
+ "domains": [
+ "theguardian.com"
+ ]
+ },
+ {
+ "image_url": "https://assets.market-storefront.envato-static.com/storefront/assets/favicons/themeforest/apple-touch-icon-144x144-precomposed-e158f10207a0bcb58e6e9ae62482d9cb5de11794f5ecb56a25e0501dce624bd2.png",
+ "domains": [
+ "themeforest.net"
+ ]
+ },
+ {
+ "image_url": "https://www.theverge.com/icons/android_chrome_512x512.png",
+ "domains": [
+ "theverge.com"
+ ]
+ },
+ {
+ "image_url": "https://tiktok.com/favicon.ico",
+ "domains": [
+ "tiktok.com"
+ ]
+ },
+ {
+ "image_url": "https://time.com/img/favicons/favicon-192.png",
+ "domains": [
+ "time.com"
+ ]
+ },
+ {
+ "image_url": "https://tinyurl.com/images/icons/favicon-192.png",
+ "domains": [
+ "tinyurl.com"
+ ]
+ },
+ {
+ "image_url": "https://assets.tumblr.com/pop/manifest/favicon-cfddd25f.svg",
+ "domains": [
+ "tumblr.com"
+ ]
+ },
+ {
+ "image_url": "https://m.twitch.tv/static/images/pwa/icons/pwaicon-180.png",
+ "domains": [
+ "twitch.tv",
+ "go.twitch.tv"
+ ]
+ },
+ {
+ "image_url": "https://abs.twimg.com/responsive-web/client-web-legacy/icon-ios.77d25eba.png",
+ "domains": [
+ "twitter.com"
+ ]
+ },
+ {
+ "image_url": "https://unsplash.com/apple-touch-icon.png",
+ "domains": [
+ "unsplash.com"
+ ]
+ },
+ {
+ "image_url": "https://usatoday.com/favicon.ico",
+ "domains": [
+ "usatoday.com"
+ ]
+ },
+ {
+ "image_url": "https://validator.w3.org/images/favicon.ico",
+ "domains": [
+ "validator.w3.org"
+ ]
+ },
+ {
+ "image_url": "https://i.vimeocdn.com/favicon/main-touch_180",
+ "domains": [
+ "vimeo.com",
+ "player.vimeo.com"
+ ]
+ },
+ {
+ "image_url": "https://m.vk.com/images/icons/pwa/apple/default.png?15",
+ "domains": [
+ "vk.com"
+ ]
+ },
+ {
+ "image_url": "https://w3.org/favicon.ico",
+ "domains": [
+ "w3.org"
+ ]
+ },
+ {
+ "image_url": "https://www.washingtonpost.com/favicon.svg",
+ "domains": [
+ "washingtonpost.com"
+ ]
+ },
+ {
+ "image_url": "https://web.archive.org/_static/images/archive.ico",
+ "domains": [
+ "web.archive.org"
+ ]
+ },
+ {
+ "image_url": "https://www.webmd.com/favico/apple-touch-icon-114x114-precomposed.png",
+ "domains": [
+ "webmd.com"
+ ]
+ },
+ {
+ "image_url": "https://weebly.com/favicon.ico",
+ "domains": [
+ "weebly.com"
+ ]
+ },
+ {
+ "image_url": "https://h5.sinaimg.cn/m/weibo-lite/appicon.png",
+ "domains": [
+ "weibo.com"
+ ]
+ },
+ {
+ "image_url": "https://static.whatsapp.net/rsrc.php/v3/yz/r/ujTY9i_Jhs1.png",
+ "domains": [
+ "whatsapp.com",
+ "api.whatsapp.com",
+ "wa.me"
+ ]
+ },
+ {
+ "image_url": "https://www.who.int/apple-touch-icon-precomposed.png",
+ "domains": [
+ "who.int"
+ ]
+ },
+ {
+ "image_url": "https://foundation.wikimedia.org/favicon.ico",
+ "domains": [
+ "wikimedia.org"
+ ]
+ },
+ {
+ "image_url": "https://www.wikipedia.org/static/apple-touch/wikipedia.png",
+ "domains": [
+ "wikipedia.org"
+ ]
+ },
+ {
+ "image_url": "https://www.wiley.com/etc.clientlibs/wiley/clientlibs/clientlib-consumer/resources/images/icons/favicon.svg",
+ "domains": [
+ "wiley.com"
+ ]
+ },
+ {
+ "image_url": "https://c.s-microsoft.com/favicon.ico",
+ "domains": [
+ "windows.microsoft.com"
+ ]
+ },
+ {
+ "image_url": "https://www.wix.com/favicon.ico",
+ "domains": [
+ "wixsite.com",
+ "wix.com"
+ ]
+ },
+ {
+ "image_url": "https://0.gravatar.com/blavatar/653166773dc88127bd3afe0b6dfe5ea7?s=114&d=https%3A%2F%2Fs1.wp.com%2Fi%2Fwebclip.png",
+ "domains": [
+ "wordpress.com",
+ "wp.com"
+ ]
+ },
+ {
+ "image_url": "https://s.w.org/images/wmark.png",
+ "domains": [
+ "wordpress.org"
+ ]
+ },
+ {
+ "image_url": "https://s.wsj.net/img/meta/wsj_favicon.svg",
+ "domains": [
+ "wsj.com"
+ ]
+ },
+ {
+ "image_url": "https://www.gov.uk/assets/static/govuk-apple-touch-icon-180x180-026deaa34fa328ae5f1f519a37dbd15e6555c5086e1ba83986cd0827a7209902.png",
+ "domains": [
+ "www.gov.uk"
+ ]
+ },
+ {
+ "image_url": "https://www.ncbi.nlm.nih.gov/favicon.ico",
+ "domains": [
+ "www.ncbi.nlm.nih.gov"
+ ]
+ },
+ {
+ "image_url": "https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo.png",
+ "domains": [
+ "yahoo.com"
+ ]
+ },
+ {
+ "image_url": "https://s3-media0.fl.yelpcdn.com/assets/srv0/yelp_large_assets/dcfe403147fc/assets/img/logos/favicon.ico",
+ "domains": [
+ "yelp.com"
+ ]
+ },
+ {
+ "image_url": "https://www.youtube.com/img/favicon_144.png",
+ "domains": [
+ "youtube-nocookie.com"
+ ]
+ },
+ {
+ "image_url": "https://m.youtube.com/static/apple-touch-icon-180x180-precomposed.png",
+ "domains": [
+ "youtube.com",
+ "youtu.be"
+ ]
+ },
+ {
+ "image_url": "https://st1.zoom.us/zoom.ico",
+ "domains": [
+ "zoom.us"
+ ]
+ }
+]
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt
new file mode 100644
index 0000000000..3bf5c2b2ff
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt
@@ -0,0 +1,476 @@
+/* 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.browser.icons
+
+import android.annotation.SuppressLint
+import android.content.ComponentCallbacks2
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.widget.ImageView
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import mozilla.components.browser.icons.compose.IconLoaderScope
+import mozilla.components.browser.icons.compose.IconLoaderState
+import mozilla.components.browser.icons.compose.InternalIconLoaderScope
+import mozilla.components.browser.icons.decoder.ICOIconDecoder
+import mozilla.components.browser.icons.decoder.SvgIconDecoder
+import mozilla.components.browser.icons.extension.IconMessageHandler
+import mozilla.components.browser.icons.generator.DefaultIconGenerator
+import mozilla.components.browser.icons.generator.IconGenerator
+import mozilla.components.browser.icons.loader.DataUriIconLoader
+import mozilla.components.browser.icons.loader.DiskIconLoader
+import mozilla.components.browser.icons.loader.HttpIconLoader
+import mozilla.components.browser.icons.loader.IconLoader
+import mozilla.components.browser.icons.loader.MemoryIconLoader
+import mozilla.components.browser.icons.loader.NonBlockingHttpIconLoader
+import mozilla.components.browser.icons.pipeline.IconResourceComparator
+import mozilla.components.browser.icons.preparer.DiskIconPreparer
+import mozilla.components.browser.icons.preparer.IconPreprarer
+import mozilla.components.browser.icons.preparer.MemoryIconPreparer
+import mozilla.components.browser.icons.preparer.TippyTopIconPreparer
+import mozilla.components.browser.icons.processor.DiskIconProcessor
+import mozilla.components.browser.icons.processor.IconProcessor
+import mozilla.components.browser.icons.processor.MemoryIconProcessor
+import mozilla.components.browser.icons.utils.IconDiskCache
+import mozilla.components.browser.icons.utils.IconMemoryCache
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.memory.MemoryConsumer
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.fetch.Client
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.NamedThreadFactory
+import mozilla.components.support.images.CancelOnDetach
+import mozilla.components.support.images.DesiredSize
+import mozilla.components.support.images.decoder.AndroidImageDecoder
+import mozilla.components.support.images.decoder.ImageDecoder
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
+import java.lang.ref.WeakReference
+import java.util.concurrent.Executors
+
+@VisibleForTesting
+internal const val MAXIMUM_SCALE_FACTOR = 2.0f
+
+private const val EXTENSION_MESSAGING_NAME = "MozacBrowserIcons"
+
+// Number of worker threads we are using internally.
+private const val THREADS = 3
+
+internal val sharedMemoryCache = IconMemoryCache()
+internal val sharedDiskCache = IconDiskCache()
+
+/**
+ * Entry point for loading icons for websites.
+ *
+ * @param generator The [IconGenerator] to generate an icon if no icon could be loaded.
+ * @param decoders List of [ImageDecoder] instances to use when decoding a loaded icon into a [android.graphics.Bitmap].
+ */
+class BrowserIcons constructor(
+ private val context: Context,
+ httpClient: Client,
+ private val generator: IconGenerator = DefaultIconGenerator(),
+ private val preparers: List<IconPreprarer> = listOf(
+ TippyTopIconPreparer(context.assets),
+ MemoryIconPreparer(sharedMemoryCache),
+ DiskIconPreparer(sharedDiskCache),
+ ),
+ internal var loaders: List<IconLoader> = listOf(
+ MemoryIconLoader(sharedMemoryCache),
+ DiskIconLoader(sharedDiskCache),
+ HttpIconLoader(httpClient),
+ DataUriIconLoader(),
+ ),
+ private val decoders: List<ImageDecoder> = listOf(
+ AndroidImageDecoder(),
+ ICOIconDecoder(),
+ SvgIconDecoder(),
+ ),
+ private val processors: List<IconProcessor> = listOf(
+ MemoryIconProcessor(sharedMemoryCache),
+ DiskIconProcessor(sharedDiskCache),
+ ),
+ jobDispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(
+ THREADS,
+ NamedThreadFactory("BrowserIcons"),
+ ).asCoroutineDispatcher(),
+) : MemoryConsumer {
+ private val logger = Logger("BrowserIcons")
+ private val maximumSize = context.resources.getDimensionPixelSize(R.dimen.mozac_browser_icons_maximum_size)
+ private val minimumSize = context.resources.getDimensionPixelSize(R.dimen.mozac_browser_icons_minimum_size)
+ private val scope = CoroutineScope(jobDispatcher)
+ private val backgroundHttpIconLoader = NonBlockingHttpIconLoader(httpClient) { request, resource, result ->
+ val desiredSize = request.getDesiredSize(context, minimumSize, maximumSize)
+
+ val icon = decodeIconLoaderResult(result, decoders, desiredSize)
+ ?: generator.generate(context, request)
+
+ process(context, processors, request, resource, icon, desiredSize)
+ }
+
+ /**
+ * Asynchronously loads an [Icon] for the given [IconRequest].
+ */
+ fun loadIcon(request: IconRequest): Deferred<Icon> = scope.async {
+ loadIconInternalAsync(request).await().also { loadedIcon ->
+ logger.debug("Loaded icon (source = ${loadedIcon.source}): ${request.url}")
+ }
+ }
+
+ /**
+ * Synchronously loads an [Icon] for the given [IconRequest] using an in-memory loader.
+ */
+ private fun loadIconMemoryOnly(initialRequest: IconRequest, desiredSize: DesiredSize): Icon? {
+ val preparers = listOf(MemoryIconPreparer(sharedMemoryCache))
+ val loaders = listOf(MemoryIconLoader(sharedMemoryCache))
+ val request = prepare(context, preparers, initialRequest)
+
+ load(context, request, loaders, decoders, desiredSize)?.let {
+ return it.first
+ }
+
+ return null
+ }
+
+ @WorkerThread
+ @VisibleForTesting
+ internal fun loadIconInternalAsync(
+ initialRequest: IconRequest,
+ size: DesiredSize? = null,
+ ): Deferred<Icon> = scope.async {
+ val desiredSize = size ?: desiredSizeForRequest(initialRequest)
+
+ // (1) First prepare the request.
+ val request = prepare(context, preparers, initialRequest)
+
+ // (2) Check whether icons should be downloaded in background.
+ val updatedLoaders = loaders.map {
+ if (it is HttpIconLoader && !initialRequest.waitOnNetworkLoad) {
+ backgroundHttpIconLoader
+ } else {
+ it
+ }
+ }
+
+ // (3) Then try to load an icon.
+ val (icon, resource) = load(context, request, updatedLoaders, decoders, desiredSize)
+ ?: (generator.generate(context, request) to null)
+
+ // (4) Finally process the icon.
+ process(context, processors, request, resource, icon, desiredSize)
+ ?: generator.generate(context, request)
+ }
+
+ /**
+ * Installs the "icons" extension in the engine in order to dynamically load icons for loaded websites.
+ */
+ fun install(engine: Engine, store: BrowserStore) {
+ engine.installBuiltInWebExtension(
+ id = "icons@mozac.org",
+ url = "resource://android/assets/extensions/browser-icons/",
+ onSuccess = { extension ->
+ Logger.debug("Installed browser-icons extension")
+
+ store.flowScoped { flow -> subscribeToUpdates(store, flow, extension) }
+ },
+ onError = { throwable ->
+ Logger.error("Could not install browser-icons extension", throwable)
+ },
+ )
+ }
+
+ /**
+ * Loads an icon using [BrowserIcons] and then displays it in the [ImageView]. Synchronous loading
+ * via an in-memory cache is attempted first, followed by an asynchronous load as a fallback.
+ * If the view is detached from the window before loading is completed, then loading is cancelled.
+ *
+ * @param view [ImageView] to load icon into.
+ * @param request Load icon for this given [IconRequest].
+ * @param placeholder [Drawable] to display while icon is loading.
+ * @param error [Drawable] to display if loading fails.
+ */
+ fun loadIntoView(
+ view: ImageView,
+ request: IconRequest,
+ placeholder: Drawable? = null,
+ error: Drawable? = null,
+ ): Job {
+ return loadIntoViewInternal(WeakReference(view), request, placeholder, error)
+ }
+
+ @MainThread
+ @VisibleForTesting
+ @Suppress("UndocumentedPublicFunction") // this is visible only for tests
+ fun loadIntoViewInternal(
+ view: WeakReference<ImageView>,
+ request: IconRequest,
+ placeholder: Drawable?,
+ error: Drawable?,
+ ): Job {
+ // If we previously started loading into the view, cancel the job.
+ val existingJob = view.get()?.getTag(R.id.mozac_browser_icons_tag_job) as? Job
+ existingJob?.cancel()
+
+ view.get()?.setImageDrawable(placeholder)
+
+ // Happy path: try to load icon synchronously from an in-memory cache.
+ val desiredSize = desiredSizeForRequest(request)
+ val inMemoryIcon = loadIconMemoryOnly(request, desiredSize)
+ if (inMemoryIcon != null) {
+ view.get()?.setImageBitmap(inMemoryIcon.bitmap)
+ return Job().also { it.complete() }
+ }
+
+ // Unhappy path: if the in-memory load didn't succeed, try the expensive IO loaders.
+ @SuppressLint("WrongThread")
+ val deferredIcon = loadIconInternalAsync(request, desiredSize)
+ view.get()?.setTag(R.id.mozac_browser_icons_tag_job, deferredIcon)
+ val onAttachStateChangeListener = CancelOnDetach(deferredIcon).also {
+ view.get()?.addOnAttachStateChangeListener(it)
+ }
+
+ return scope.launch(Dispatchers.Main) {
+ try {
+ val icon = deferredIcon.await()
+ view.get()?.setImageBitmap(icon.bitmap)
+ } catch (e: CancellationException) {
+ view.get()?.setImageDrawable(error)
+ } finally {
+ view.get()?.removeOnAttachStateChangeListener(onAttachStateChangeListener)
+ view.get()?.setTag(R.id.mozac_browser_icons_tag_job, null)
+ }
+ }
+ }
+
+ /**
+ * Loads an icon using [BrowserIcons] into the given Composable [content]. Synchronous loading
+ * via an in-memory cache is attempted first, followed by an asynchronous load as a fallback.
+ *
+ * @param url The URL of the website an icon should be loaded for.
+ * @param iconResource Optional [IconRequest.Resource] to load the icon from.
+ * @param iconSize The preferred size of the icon that should be loaded.
+ * @param isPrivate Whether this request for this icon came from a private session.
+ * @param content The Composable content block to render the icon.
+ */
+ @Composable
+ fun LoadableImage(
+ url: String,
+ iconResource: IconRequest.Resource? = null,
+ iconSize: IconRequest.Size = IconRequest.Size.DEFAULT,
+ isPrivate: Boolean = false,
+ content: @Composable IconLoaderScope.() -> Unit,
+ ) {
+ val iconResources = iconResource?.let { listOf(it) } ?: emptyList()
+ val request = IconRequest(url, iconSize, iconResources, null, isPrivate)
+ val iconLoaderScope = remember(request) { InternalIconLoaderScope() }
+
+ // Happy path: try to load icon synchronously from an in-memory cache.
+ val desiredSize = desiredSizeForRequest(request)
+ val inMemoryIcon = loadIconMemoryOnly(request, desiredSize)
+ if (inMemoryIcon != null) {
+ iconLoaderScope.state.value = IconLoaderState.Icon(
+ BitmapPainter(inMemoryIcon.bitmap.asImageBitmap()),
+ inMemoryIcon.color,
+ inMemoryIcon.source,
+ inMemoryIcon.maskable,
+ )
+ } else {
+ // Unhappy path: if the in-memory load didn't succeed, try the expensive IO loaders.
+ val deferredIcon = loadIconInternalAsync(request, desiredSize)
+
+ LaunchedEffect(request) {
+ try {
+ val icon = deferredIcon.await()
+ iconLoaderScope.state.value = IconLoaderState.Icon(
+ BitmapPainter(icon.bitmap.asImageBitmap()),
+ icon.color,
+ icon.source,
+ icon.maskable,
+ )
+ } catch (e: CancellationException) {
+ Logger.debug("Could not retrieve icon for $url", e)
+ }
+ }
+ }
+
+ iconLoaderScope.content()
+ }
+
+ private fun desiredSizeForRequest(request: IconRequest) = DesiredSize(
+ targetSize = context.resources.getDimensionPixelSize(request.size.dimen),
+ minSize = minimumSize,
+ maxSize = maximumSize,
+ maxScaleFactor = MAXIMUM_SCALE_FACTOR,
+ )
+
+ /**
+ * The device is running low on memory. This component should trim its memory usage.
+ */
+ @Deprecated("Use onTrimMemory instead.", replaceWith = ReplaceWith("onTrimMemory"))
+ fun onLowMemory() {
+ sharedMemoryCache.clear()
+ }
+
+ override fun onTrimMemory(level: Int) {
+ val shouldClearMemoryCache = when (level) {
+ // Foreground: The device is running much lower on memory. The app is running and not killable, but the
+ // system wants us to release unused resources to improve system performance.
+ ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
+ // Foreground: The device is running extremely low on memory. The app is not yet considered a killable
+ // process, but the system will begin killing background processes if apps do not release resources.
+ ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
+ -> true
+
+ // Background: The system is running low on memory and our process is near the middle of the LRU list.
+ // If the system becomes further constrained for memory, there's a chance our process will be killed.
+ ComponentCallbacks2.TRIM_MEMORY_MODERATE,
+ // Background: The system is running low on memory and our process is one of the first to be killed
+ // if the system does not recover memory now.
+ ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
+ -> true
+
+ else -> false
+ }
+
+ if (shouldClearMemoryCache) {
+ sharedMemoryCache.clear()
+ }
+ }
+
+ /**
+ * Clears all icons and metadata from disk and memory.
+ *
+ * This will clear the default disk and memory cache that is used by the default configuration.
+ * If custom [IconLoader] and [IconProcessor] instances with a custom storage are provided to
+ * [BrowserIcons] then the calling app is responsible for clearing that data.
+ */
+ fun clear() {
+ sharedDiskCache.clear(context)
+ sharedMemoryCache.clear()
+ }
+
+ private suspend fun subscribeToUpdates(
+ store: BrowserStore,
+ flow: Flow<BrowserState>,
+ extension: WebExtension,
+ ) {
+ // Whenever we see a new EngineSession in the store then we register our content message
+ // handler if it has not been added yet.
+
+ flow.map { it.tabs }
+ .filterChanged { it.engineState.engineSession }
+ .collect { state ->
+ val engineSession = state.engineState.engineSession ?: return@collect
+
+ if (extension.hasContentMessageHandler(engineSession, EXTENSION_MESSAGING_NAME)) {
+ return@collect
+ }
+
+ val handler = IconMessageHandler(store, state.id, state.content.private, this)
+ extension.registerContentMessageHandler(engineSession, EXTENSION_MESSAGING_NAME, handler)
+ }
+ }
+}
+
+private fun prepare(context: Context, preparers: List<IconPreprarer>, request: IconRequest): IconRequest =
+ preparers.fold(request) { preparedRequest, preparer ->
+ preparer.prepare(context, preparedRequest)
+ }
+
+private fun load(
+ context: Context,
+ request: IconRequest,
+ loaders: List<IconLoader>,
+ decoders: List<ImageDecoder>,
+ desiredSize: DesiredSize,
+): Pair<Icon, IconRequest.Resource>? {
+ request.resources
+ .asSequence()
+ .distinct()
+ .sortedWith(IconResourceComparator)
+ .forEach { resource ->
+ loaders.forEach { loader ->
+ val result = loader.load(context, request, resource)
+
+ val icon = decodeIconLoaderResult(result, decoders, desiredSize)
+
+ if (icon != null) {
+ return Pair(icon, resource)
+ }
+ }
+ }
+
+ return null
+}
+
+private fun decodeIconLoaderResult(
+ result: IconLoader.Result,
+ decoders: List<ImageDecoder>,
+ desiredSize: DesiredSize,
+): Icon? = when (result) {
+ IconLoader.Result.NoResult -> null
+
+ is IconLoader.Result.BitmapResult -> Icon(result.bitmap, source = result.source)
+
+ is IconLoader.Result.BytesResult ->
+ decodeBytes(result.bytes, decoders, desiredSize)?.let { Icon(it, source = result.source) }
+}
+
+@VisibleForTesting
+internal fun IconRequest.getDesiredSize(context: Context, minimumSize: Int, maximumSize: Int) =
+ DesiredSize(
+ targetSize = context.resources.getDimensionPixelSize(size.dimen),
+ minSize = minimumSize,
+ maxSize = maximumSize,
+ maxScaleFactor = MAXIMUM_SCALE_FACTOR,
+ )
+
+private fun decodeBytes(
+ data: ByteArray,
+ decoders: List<ImageDecoder>,
+ desiredSize: DesiredSize,
+): Bitmap? {
+ decoders.forEach { decoder ->
+ val bitmap = decoder.decode(data, desiredSize)
+
+ if (bitmap != null) {
+ return bitmap
+ }
+ }
+
+ return null
+}
+
+private fun process(
+ context: Context,
+ processors: List<IconProcessor>,
+ request: IconRequest,
+ resource: IconRequest.Resource?,
+ icon: Icon?,
+ desiredSize: DesiredSize,
+): Icon? =
+ processors.fold(icon) { processedIcon, processor ->
+ if (processedIcon == null) return null
+ processor.process(context, request, resource, processedIcon, desiredSize)
+ }
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/Icon.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/Icon.kt
new file mode 100644
index 0000000000..6974ee0420
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/Icon.kt
@@ -0,0 +1,52 @@
+/* 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.browser.icons
+
+import android.graphics.Bitmap
+
+/**
+ * An [Icon] returned by [BrowserIcons] after processing an [IconRequest]
+ *
+ * @property bitmap The loaded icon as a [Bitmap].
+ * @property color The dominant color of the icon. Will be null if no color could be extracted.
+ * @property source The source of the icon.
+ * @property maskable True if the icon represents as full-bleed icon that can be cropped to other shapes.
+ */
+data class Icon(
+ val bitmap: Bitmap,
+ val color: Int? = null,
+ val source: Source,
+ val maskable: Boolean = false,
+) {
+ /**
+ * The source of an [Icon].
+ */
+ enum class Source {
+ /**
+ * This icon was generated.
+ */
+ GENERATOR,
+
+ /**
+ * This icon was downloaded.
+ */
+ DOWNLOAD,
+
+ /**
+ * This icon was inlined in the document.
+ */
+ INLINE,
+
+ /**
+ * This icon was loaded from an in-memory cache.
+ */
+ MEMORY,
+
+ /**
+ * This icon was loaded from a disk cache.
+ */
+ DISK,
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequest.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequest.kt
new file mode 100644
index 0000000000..9df9e3404e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequest.kt
@@ -0,0 +1,140 @@
+/* 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.browser.icons
+
+import androidx.annotation.ColorInt
+import androidx.annotation.DimenRes
+import mozilla.components.concept.engine.manifest.Size as HtmlSize
+
+/**
+ * A request to load an [Icon].
+ *
+ * @property url The URL of the website an icon should be loaded for.
+ * @property size The preferred size of the icon that should be loaded.
+ * @property resources An optional list of icon resources to load the icon from.
+ * @property color The suggested dominant color of the icon.
+ * @property isPrivate Whether this request for this icon came from a private session.
+ * @property waitOnNetworkLoad Whether client code should wait on the resource being loaded or
+ * loading can continue in background.
+ */
+data class IconRequest(
+ val url: String,
+ val size: Size = Size.DEFAULT,
+ val resources: List<Resource> = emptyList(),
+ @ColorInt val color: Int? = null,
+ val isPrivate: Boolean = false,
+ val waitOnNetworkLoad: Boolean = true,
+) {
+
+ /**
+ * Supported sizes.
+ *
+ * We are trying to limit the supported sizes in order to optimize our caching strategy.
+ */
+ enum class Size(@DimenRes val dimen: Int) {
+ DEFAULT(R.dimen.mozac_browser_icons_size_default),
+ LAUNCHER(R.dimen.mozac_browser_icons_size_launcher),
+ LAUNCHER_ADAPTIVE(R.dimen.mozac_browser_icons_size_launcher_adaptive),
+ }
+
+ /**
+ * An icon resource that can be loaded.
+ *
+ * @param url URL the icon resource can be fetched from.
+ * @param type The type of the icon.
+ * @param sizes Optional list of icon sizes provided by this resource (if known).
+ * @param mimeType Optional MIME type of this icon resource (if known).
+ * @param maskable True if the icon represents as full-bleed icon that can be cropped to other shapes.
+ */
+ data class Resource(
+ val url: String,
+ val type: Type,
+ val sizes: List<HtmlSize> = emptyList(),
+ val mimeType: String? = null,
+ val maskable: Boolean = false,
+ ) {
+ /**
+ * An icon resource type.
+ */
+ enum class Type {
+ /**
+ * A favicon ("icon" or "shortcut icon").
+ *
+ * https://en.wikipedia.org/wiki/Favicon
+ */
+ FAVICON,
+
+ /**
+ * An Apple touch icon.
+ *
+ * Originally used for adding an icon to the home screen of an iOS device.
+ *
+ * https://realfavicongenerator.net/blog/apple-touch-icon-the-good-the-bad-the-ugly/
+ */
+ APPLE_TOUCH_ICON,
+
+ /**
+ * A "fluid" icon.
+ *
+ * Fluid is a macOS application that wraps website to look and behave like native desktop
+ * applications.
+ *
+ * https://fluidapp.com/
+ */
+ FLUID_ICON,
+
+ /**
+ * An "image_src" icon.
+ *
+ * Yahoo and Facebook used this icon for previewing web content. Since then Facebook seems to use
+ * OpenGraph instead. However website still define "image_src" icons.
+ *
+ * https://www.niallkennedy.com/blog/2009/03/enhanced-social-share.html
+ */
+ IMAGE_SRC,
+
+ /**
+ * An "Open Graph" image.
+ *
+ * "An image URL which should represent your object within the graph."
+ *
+ * http://ogp.me/
+ */
+ OPENGRAPH,
+
+ /**
+ * A "Twitter Card" image.
+ *
+ * "URL of image to use in the card."
+ *
+ * https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/markup.html
+ */
+ TWITTER,
+
+ /**
+ * A "Microsoft tile" image.
+ *
+ * When pinning sites on Windows this image is used.
+ *
+ * "Specifies the background image for live tile."
+ *
+ * https://technet.microsoft.com/en-us/windows/dn255024(v=vs.60)
+ */
+ MICROSOFT_TILE,
+
+ /**
+ * An icon found in Mozilla's "tippy top" list.
+ */
+ TIPPY_TOP,
+
+ /**
+ * A Web App Manifest image.
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/Manifest/icons
+ */
+ MANIFEST_ICON,
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderScope.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderScope.kt
new file mode 100644
index 0000000000..f9d283471f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderScope.kt
@@ -0,0 +1,59 @@
+/* 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.browser.icons.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import mozilla.components.browser.icons.BrowserIcons
+
+/**
+ * The scope of a [BrowserIcons.Loader] block.
+ */
+interface IconLoaderScope {
+ val state: MutableState<IconLoaderState>
+}
+
+/**
+ * Renders the inner [content] block once an icon was loaded.
+ */
+@Composable
+fun IconLoaderScope.WithIcon(
+ content: @Composable (icon: IconLoaderState.Icon) -> Unit,
+) {
+ WithInternalState {
+ val state = state.value
+ if (state is IconLoaderState.Icon) {
+ content(state)
+ }
+ }
+}
+
+/**
+ * Renders the inner [content] block until an icon was loaded.
+ */
+@Composable
+fun IconLoaderScope.Placeholder(
+ content: @Composable () -> Unit,
+) {
+ WithInternalState {
+ val state = state.value
+ if (state is IconLoaderState.Loading) {
+ content()
+ }
+ }
+}
+
+@Composable
+private fun IconLoaderScope.WithInternalState(
+ content: @Composable InternalIconLoaderScope.() -> Unit,
+) {
+ val internalScope = this as InternalIconLoaderScope
+ internalScope.content()
+}
+
+internal class InternalIconLoaderScope(
+ override val state: MutableState<IconLoaderState> = mutableStateOf(IconLoaderState.Loading),
+) : IconLoaderScope
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderState.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderState.kt
new file mode 100644
index 0000000000..c04f658fa4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderState.kt
@@ -0,0 +1,35 @@
+/* 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.browser.icons.compose
+
+import androidx.compose.ui.graphics.painter.Painter
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.Icon.Source
+
+/**
+ * The state an [IconLoaderScope] is in.
+ */
+sealed class IconLoaderState {
+ /**
+ * The [BrowserIcons.Loader] is currently loading the icon.
+ */
+ object Loading : IconLoaderState()
+
+ /**
+ * The [BrowserIcons.Loader] has completed loading the icon and it is available through the
+ * attached [painter].
+ *
+ * @property painter The loaded or generated icon as a [Painter].
+ * @property color The dominant color of the icon. Will be null if no color could be extracted.
+ * @property source The source of the icon.
+ * @property maskable True if the icon represents as full-bleed icon that can be cropped to other shapes.
+ */
+ data class Icon(
+ val painter: Painter,
+ val color: Int?,
+ val source: Source,
+ val maskable: Boolean,
+ ) : IconLoaderState()
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/Loader.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/Loader.kt
new file mode 100644
index 0000000000..7bc98a9dd1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/Loader.kt
@@ -0,0 +1,50 @@
+/* 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.browser.icons.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * Loads an icon for the given [url] (or generates one) and makes it available to the inner
+ * [IconLoaderScope].
+ *
+ * The loaded image will be available through the [WithIcon] composable. While the icon is still
+ * loading [Placeholder] will get rendered.
+ *
+ * @param url The URL of the website an icon should be loaded for. Note that this is the URL of the
+ * website the icon is *for* (e.g. https://github.com) and not the URL of the icon itself (e.g.
+ * https://github.com/favicon.ico)
+ * @param size The preferred size of the icon that should be loaded.
+ * @param isPrivate Whether or not a private request (like in private browsing) should be used to
+ * download the icon (if needed).
+ */
+@Composable
+fun BrowserIcons.Loader(
+ url: String,
+ size: IconRequest.Size = IconRequest.Size.DEFAULT,
+ isPrivate: Boolean = false,
+ content: @Composable IconLoaderScope.() -> Unit,
+) {
+ val request = IconRequest(url, size, emptyList(), null, isPrivate)
+ val scope = remember(request) { InternalIconLoaderScope() }
+
+ LaunchedEffect(request) {
+ val icon = loadIcon(request).await()
+ scope.state.value = IconLoaderState.Icon(
+ BitmapPainter(icon.bitmap.asImageBitmap()),
+ icon.color,
+ icon.source,
+ icon.maskable,
+ )
+ }
+
+ scope.content()
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ICOIconDecoder.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ICOIconDecoder.kt
new file mode 100644
index 0000000000..35ac3f6006
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ICOIconDecoder.kt
@@ -0,0 +1,42 @@
+/* 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.browser.icons.decoder
+
+import android.graphics.Bitmap
+import mozilla.components.browser.icons.decoder.ico.decodeDirectoryEntries
+import mozilla.components.browser.icons.utils.findBestSize
+import mozilla.components.support.images.DesiredSize
+import mozilla.components.support.images.decoder.ImageDecoder
+
+// Some geometry of an ICO file.
+internal const val HEADER_LENGTH_BYTES = 6
+internal const val ICON_DIRECTORY_ENTRY_LENGTH_BYTES = 16
+
+internal const val ZERO_BYTE = 0.toByte()
+
+/**
+ * [ImageDecoder] implementation for decoding ICO files.
+ *
+ * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format.
+ * A mixture of image types may not exist.
+ */
+class ICOIconDecoder : ImageDecoder {
+ override fun decode(data: ByteArray, desiredSize: DesiredSize): Bitmap? {
+ val (targetSize, _, maxSize, maxScaleFactor) = desiredSize
+ val entries = decodeDirectoryEntries(data, maxSize)
+
+ val bestEntry = entries.map { entry ->
+ Pair(entry.width, entry.height)
+ }.findBestSize(targetSize, maxSize, maxScaleFactor) ?: return null
+
+ for (entry in entries) {
+ if (entry.width == bestEntry.first && entry.height == bestEntry.second) {
+ return entry.toBitmap(data)
+ }
+ }
+
+ return null
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/SvgIconDecoder.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/SvgIconDecoder.kt
new file mode 100644
index 0000000000..b4a845e153
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/SvgIconDecoder.kt
@@ -0,0 +1,108 @@
+/* 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.browser.icons.decoder
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.ARGB_8888
+import android.graphics.Canvas
+import android.graphics.RectF
+import androidx.annotation.VisibleForTesting
+import androidx.core.graphics.createBitmap
+import com.caverock.androidsvg.SVG
+import com.caverock.androidsvg.SVGParseException
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.images.DesiredSize
+import mozilla.components.support.images.decoder.ImageDecoder
+
+/**
+ * [ImageDecoder] that will use the AndroidSVG in order to decode the byte data.
+ *
+ * The code is largely borrowed from [coil-svg](https://github.com/coil-kt/coil/blob/2.4.0/coil-svg/src/main/java/coil/decode/SvgDecoder.kt)
+ * with some fixed options.
+ */
+class SvgIconDecoder : ImageDecoder {
+ private val logger = Logger("SvgIconDecoder")
+
+ @Suppress("TooGenericExceptionCaught")
+ override fun decode(data: ByteArray, desiredSize: DesiredSize): Bitmap? =
+ try {
+ maybeDecode(data, desiredSize)
+ } catch (throwable: Throwable) {
+ when (throwable) {
+ is IllegalArgumentException,
+ is NullPointerException,
+ is SVGParseException,
+ -> {
+ logger.error("Failed to parse the byte data to Bitmap", throwable)
+ }
+ is OutOfMemoryError -> {
+ logger.error("Failed to decode the byte data due to OutOfMemoryError")
+ }
+ else -> {
+ logger.error("Failed to decode byte data: " + throwable.message.toString(), throwable)
+ }
+ }
+ null
+ }
+
+ /**
+ * Decodes an SVG image.
+ *
+ * @param data Image bytes to decode.
+ * @param desiredSize Desired size for the image.
+ * @return decoded image Bitmap.
+ * @throws IllegalArgumentException in case the parsed SVG document is empty.
+ * @throws NullPointerException in case of malformed image bytes.
+ * @throws SVGParseException in case of incorrect SVG element.
+ * @throws OutOfMemoryError in case of out of memory when decoding image bytes.
+ */
+ @Throws(
+ IllegalArgumentException::class,
+ NullPointerException::class,
+ SVGParseException::class,
+ OutOfMemoryError::class,
+ )
+ @VisibleForTesting
+ internal fun maybeDecode(data: ByteArray, desiredSize: DesiredSize): Bitmap {
+ val svg = SVG.getFromInputStream(data.inputStream())
+
+ val svgWidth: Float
+ val svgHeight: Float
+ val viewBox: RectF? = svg.documentViewBox
+ if (viewBox != null) {
+ svgWidth = viewBox.width()
+ svgHeight = viewBox.height()
+ } else {
+ svgWidth = svg.documentWidth
+ svgHeight = svg.documentHeight
+ }
+
+ var bitmapWidth = desiredSize.targetSize
+ var bitmapHeight = desiredSize.targetSize
+
+ // Scale the bitmap to SVG maintaining the aspect ratio
+ if (svgWidth > 0 && svgHeight > 0) {
+ val widthPercent = bitmapWidth / svgWidth.toDouble()
+ val heightPercent = bitmapHeight / svgHeight.toDouble()
+ val multiplier = minOf(widthPercent, heightPercent)
+
+ bitmapWidth = (multiplier * svgWidth).toInt()
+ bitmapHeight = (multiplier * svgHeight).toInt()
+ }
+
+ // Set the SVG's view box to enable scaling if it is not set.
+ if (viewBox == null && svgWidth > 0 && svgHeight > 0) {
+ svg.setDocumentViewBox(0f, 0f, svgWidth, svgHeight)
+ }
+
+ svg.setDocumentWidth("100%")
+ svg.setDocumentHeight("100%")
+
+ val bitmap = createBitmap(bitmapWidth, bitmapHeight, ARGB_8888)
+ svg.renderToCanvas(Canvas(bitmap))
+
+ return bitmap
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ico/IconDirectoryEntry.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ico/IconDirectoryEntry.kt
new file mode 100644
index 0000000000..b39c72d26f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ico/IconDirectoryEntry.kt
@@ -0,0 +1,315 @@
+/* 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.browser.icons.decoder.ico
+
+import android.graphics.Bitmap
+import mozilla.components.browser.icons.decoder.HEADER_LENGTH_BYTES
+import mozilla.components.browser.icons.decoder.ICON_DIRECTORY_ENTRY_LENGTH_BYTES
+import mozilla.components.browser.icons.decoder.ZERO_BYTE
+import mozilla.components.support.images.decoder.ImageDecoder
+import mozilla.components.support.ktx.kotlin.containsAtOffset
+import mozilla.components.support.ktx.kotlin.toBitmap
+
+const val MAX_BITS_PER_PIXEL = 32
+
+internal data class IconDirectoryEntry(
+ val width: Int,
+ val height: Int,
+ val paletteSize: Int,
+ val bitsPerPixel: Int,
+ val payloadSize: Int,
+ val payloadOffset: Int,
+ val payloadIsPNG: Boolean,
+ val directoryIndex: Int,
+) : Comparable<IconDirectoryEntry> {
+
+ override fun compareTo(other: IconDirectoryEntry): Int = when {
+ width > other.width -> 1
+ width < other.width -> -1
+
+ // Where both images exceed the max BPP, take the smaller of the two BPP values.
+ bitsPerPixel >= MAX_BITS_PER_PIXEL && other.bitsPerPixel >= MAX_BITS_PER_PIXEL &&
+ bitsPerPixel < other.bitsPerPixel -> 1
+ bitsPerPixel >= MAX_BITS_PER_PIXEL && other.bitsPerPixel >= MAX_BITS_PER_PIXEL &&
+ bitsPerPixel > other.bitsPerPixel -> -1
+
+ // Otherwise, take the larger of the BPP values.
+ bitsPerPixel > other.bitsPerPixel -> 1
+ bitsPerPixel < other.bitsPerPixel -> -1
+
+ // Prefer large palettes.
+ paletteSize > other.paletteSize -> 1
+ paletteSize < other.paletteSize -> -1
+
+ // Prefer smaller payloads.
+ payloadSize < other.payloadSize -> 1
+ payloadSize > other.payloadSize -> -1
+
+ // If all else fails, prefer PNGs over BMPs. They tend to be smaller.
+ payloadIsPNG && !other.payloadIsPNG -> 1
+ !payloadIsPNG && other.payloadIsPNG -> -1
+
+ else -> 0
+ }
+
+ @Suppress("MagicNumber")
+ fun toBitmap(data: ByteArray): Bitmap? {
+ if (payloadIsPNG) {
+ // PNG payload. Simply extract it and let Android decode it.
+ return data.toBitmap(payloadOffset, payloadSize)
+ }
+
+ // The payload is a BMP, so we need to do some magic to get the decoder to do what we want.
+ // We construct an ICO containing just the image we want, and let Android do the rest.
+ val decodeTarget = ByteArray(HEADER_LENGTH_BYTES + ICON_DIRECTORY_ENTRY_LENGTH_BYTES + payloadSize)
+
+ // Set the type field in the ICO header.
+ decodeTarget[2] = 1.toByte()
+
+ // Set the num-images field in the header to 1.
+ decodeTarget[4] = 1.toByte()
+
+ // Copy the ICONDIRENTRY we need into the new buffer.
+ val offset = HEADER_LENGTH_BYTES + (directoryIndex * ICON_DIRECTORY_ENTRY_LENGTH_BYTES)
+ System.arraycopy(data, offset, decodeTarget, HEADER_LENGTH_BYTES, ICON_DIRECTORY_ENTRY_LENGTH_BYTES)
+
+ val singlePayloadOffset = HEADER_LENGTH_BYTES + ICON_DIRECTORY_ENTRY_LENGTH_BYTES
+
+ System.arraycopy(data, payloadOffset, decodeTarget, singlePayloadOffset, payloadSize)
+
+ // Update the offset field of the ICONDIRENTRY to make the new ICO valid.
+ decodeTarget[HEADER_LENGTH_BYTES + 12] = singlePayloadOffset.toByte()
+ decodeTarget[HEADER_LENGTH_BYTES + 13] = singlePayloadOffset.ushr(8).toByte()
+ decodeTarget[HEADER_LENGTH_BYTES + 14] = singlePayloadOffset.ushr(16).toByte()
+ decodeTarget[HEADER_LENGTH_BYTES + 15] = singlePayloadOffset.ushr(24).toByte()
+
+ return decodeTarget.toBitmap()
+ }
+}
+
+/**
+ * The format consists of a header specifying the number, n, of images, followed by the Icon Directory.
+ *
+ * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for
+ * the corresponding image, the dimensions, colour information, payload size, and location in the file.
+ *
+ * All numerical fields follow a little-endian byte ordering.
+ *
+ * Header format:
+ *
+ * ```
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Image count (n) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * ```
+ *
+ * The type field is expected to always be 1. CUR format images should not be used for Favicons.
+ *
+ *
+ * Icon Directory Entry format:
+ * ```
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Image width | Image height | Palette size | Reserved (0) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Colour plane count | Bits per pixel |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Size of image data, in bytes |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Start of image data, as an offset from start of file |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * ```
+ *
+ * Image dimensions of zero are to be interpreted as image dimensions of 256.
+ *
+ * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero
+ * if the payload is a PNG or no palette is in use.
+ *
+ * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be
+ * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field.
+ * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.)
+ *
+ * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps.
+ */
+@Suppress("MagicNumber", "ReturnCount", "ComplexMethod", "NestedBlockDepth", "ComplexCondition")
+internal fun decodeDirectoryEntries(data: ByteArray, maxSize: Int): List<IconDirectoryEntry> {
+ // Fail if we don't have enough space for the header.
+ if (data.size < HEADER_LENGTH_BYTES) {
+ return emptyList()
+ }
+
+ // Check that the reserved fields in the header are indeed zero, and that the type field
+ // specifies ICO. If not, we've probably been given something that isn't really an ICO.
+ if (data[0] != ZERO_BYTE ||
+ data[1] != ZERO_BYTE ||
+ data[2] != 1.toByte() ||
+ data[3] != ZERO_BYTE
+ ) {
+ return emptyList()
+ }
+
+ // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java
+ // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned
+ // interpretation of the byte of interest, we do this.
+ val numEncodedImages = (data[4].toInt() and 0xFF) or ((data[5].toInt() and 0xFF) shl 8)
+
+ // Fail if there are no images or the field is corrupt.
+ if (numEncodedImages <= 0) {
+ return emptyList()
+ }
+
+ val headerAndDirectorySize = HEADER_LENGTH_BYTES + numEncodedImages * ICON_DIRECTORY_ENTRY_LENGTH_BYTES
+
+ // Fail if there is not enough space in the buffer for the stated number of icondir entries,
+ // let alone the data.
+ if (data.size < headerAndDirectorySize) {
+ return emptyList()
+ }
+
+ // Put the pointer on the first byte of the first Icon Directory Entry.
+ var bufferIndex = HEADER_LENGTH_BYTES
+
+ // We now iterate over the Icon Directory, decoding each entry as we go. We also need to
+ // discard all entries except one >= the maximum interesting size.
+
+ // Size of the smallest image larger than the limit encountered.
+ var minimumMaximum = Integer.MAX_VALUE
+
+ // Used to track the best entry for each size. The entries we want to keep.
+ val iconMap = mutableMapOf<Int, IconDirectoryEntry>()
+
+ var i = 0
+ while (i < numEncodedImages) {
+ // Decode the Icon Directory Entry at this offset.
+ val newEntry = createIconDirectoryEntry(data, bufferIndex, i)
+
+ if (newEntry == null) {
+ i++
+ bufferIndex += ICON_DIRECTORY_ENTRY_LENGTH_BYTES
+ continue
+ }
+
+ if (newEntry.width > maxSize) {
+ // If we already have a smaller image larger than the maximum size of interest, we
+ // don't care about the new one which is larger than the smallest image larger than
+ // the maximum size.
+ if (newEntry.width >= minimumMaximum) {
+ i++
+ bufferIndex += ICON_DIRECTORY_ENTRY_LENGTH_BYTES
+ continue
+ }
+
+ // Remove the previous minimum-maximum.
+ iconMap.remove(minimumMaximum)
+
+ minimumMaximum = newEntry.width
+ }
+
+ val oldEntry = iconMap[newEntry.width]
+ if (oldEntry == null) {
+ iconMap[newEntry.width] = newEntry
+ i++
+ bufferIndex += ICON_DIRECTORY_ENTRY_LENGTH_BYTES
+ continue
+ }
+
+ if (oldEntry < newEntry) {
+ iconMap[newEntry.width] = newEntry
+ }
+ i++
+ bufferIndex += ICON_DIRECTORY_ENTRY_LENGTH_BYTES
+ }
+
+ val count = iconMap.size
+
+ // Abort if no entries are desired (Perhaps all are corrupt?)
+ if (count == 0) {
+ return emptyList()
+ }
+
+ return iconMap.values.toList()
+}
+
+@Suppress("MagicNumber")
+internal fun createIconDirectoryEntry(
+ data: ByteArray,
+ entryOffset: Int,
+ directoryIndex: Int,
+): IconDirectoryEntry? {
+ // Verify that the reserved field is really zero.
+ if (data[entryOffset + 3] != ZERO_BYTE) {
+ return null
+ }
+
+ // Verify that the entry points to a region that actually exists in the buffer, else bin it.
+ var fieldPtr = entryOffset + 8
+ val entryLength = data[fieldPtr].toInt() and 0xFF or (
+ (data[fieldPtr + 1].toInt() and 0xFF) shl 8
+ ) or (
+ (data[fieldPtr + 2].toInt() and 0xFF) shl 16
+ ) or (
+ (data[fieldPtr + 3].toInt() and 0xFF) shl 24
+ )
+
+ // Advance to the offset field.
+ fieldPtr += 4
+
+ val payloadOffset = data[fieldPtr].toInt() and 0xFF or (
+ (data[fieldPtr + 1].toInt() and 0xFF) shl 8
+ ) or (
+ (data[fieldPtr + 2].toInt() and 0xFF) shl 16
+ ) or (
+ (data[fieldPtr + 3].toInt() and 0xFF) shl 24
+ )
+
+ // Fail if the entry describes a region outside the buffer.
+ if (payloadOffset < 0 || entryLength < 0 || payloadOffset + entryLength > data.size) {
+ return null
+ }
+
+ // Extract the image dimensions.
+ var imageWidth = data[entryOffset].toInt() and 0xFF
+ var imageHeight = data[entryOffset + 1].toInt() and 0xFF
+
+ // Because Microsoft, a size value of zero represents an image size of 256.
+ if (imageWidth == 0) {
+ imageWidth = 256
+ }
+
+ if (imageHeight == 0) {
+ imageHeight = 256
+ }
+
+ // If the image uses a colour palette, this is the number of colours, otherwise this is zero.
+ val paletteSize = data[entryOffset + 2].toInt() and 0xFF
+
+ // The plane count - usually 0 or 1. When > 1, taken as multiplier on bitsPerPixel.
+ val colorPlanes = data[entryOffset + 4].toInt() and 0xFF
+
+ var bitsPerPixel = (data[entryOffset + 6].toInt() and 0xFF) or ((data[entryOffset + 7].toInt() and 0xFF) shl 8)
+
+ if (colorPlanes > 1) {
+ bitsPerPixel *= colorPlanes
+ }
+
+ // Look for PNG magic numbers at the start of the payload.
+ val payloadIsPNG = data.containsAtOffset(payloadOffset, ImageDecoder.Companion.ImageMagicNumbers.PNG.value)
+
+ return IconDirectoryEntry(
+ imageWidth,
+ imageHeight,
+ paletteSize,
+ bitsPerPixel,
+ entryLength,
+ payloadOffset,
+ payloadIsPNG,
+ directoryIndex,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessage.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessage.kt
new file mode 100644
index 0000000000..155f93bbe9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessage.kt
@@ -0,0 +1,118 @@
+/* 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.browser.icons.extension
+
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.org.json.asSequence
+import mozilla.components.support.ktx.android.org.json.toJSONArray
+import mozilla.components.support.ktx.android.org.json.tryGetString
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+private val typeMap: Map<String, IconRequest.Resource.Type> = mutableMapOf(
+ "manifest" to IconRequest.Resource.Type.MANIFEST_ICON,
+ "icon" to IconRequest.Resource.Type.FAVICON,
+ "shortcut icon" to IconRequest.Resource.Type.FAVICON,
+ "fluid-icon" to IconRequest.Resource.Type.FLUID_ICON,
+ "apple-touch-icon" to IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ "image_src" to IconRequest.Resource.Type.IMAGE_SRC,
+ "apple-touch-icon image_src" to IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ "apple-touch-icon-precomposed" to IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ "og:image" to IconRequest.Resource.Type.OPENGRAPH,
+ "og:image:url" to IconRequest.Resource.Type.OPENGRAPH,
+ "og:image:secure_url" to IconRequest.Resource.Type.OPENGRAPH,
+ "twitter:image" to IconRequest.Resource.Type.TWITTER,
+ "msapplication-TileImage" to IconRequest.Resource.Type.MICROSOFT_TILE,
+)
+
+private fun Map<String, IconRequest.Resource.Type>.reverseLookup(type: IconRequest.Resource.Type): String {
+ forEach { (value, currentType) ->
+ if (currentType == type) {
+ return value
+ }
+ }
+
+ throw IllegalArgumentException("Unknown type: $type")
+}
+
+internal fun List<IconRequest.Resource>.toJSON(): JSONArray {
+ return mapNotNull { resource ->
+ if (resource.type == IconRequest.Resource.Type.TIPPY_TOP) {
+ // Ignore the URLs coming from the "tippy top" list.
+ return@mapNotNull null
+ }
+
+ JSONObject().apply {
+ put("href", resource.url)
+
+ resource.mimeType?.let { put("mimeType", it) }
+
+ put("type", typeMap.reverseLookup(resource.type))
+
+ val sizeArray = resource.sizes.map { size -> size.toString() }.toJSONArray()
+ put("sizes", sizeArray)
+
+ put("maskable", resource.maskable)
+ }
+ }.toJSONArray()
+}
+
+internal fun JSONObject.toIconRequest(isPrivate: Boolean): IconRequest? {
+ return try {
+ val url = getString("url")
+
+ IconRequest(url, isPrivate = isPrivate, resources = getJSONArray("icons").toIconResources())
+ } catch (e: JSONException) {
+ Logger.warn("Could not parse message from icons extensions", e)
+ null
+ }
+}
+
+internal fun JSONArray.toIconResources(): List<IconRequest.Resource> {
+ return asSequence { i -> getJSONObject(i) }
+ .mapNotNull { it.toIconResource() }
+ .toList()
+}
+
+private fun JSONObject.toIconResource(): IconRequest.Resource? {
+ try {
+ val url = getString("href")
+ val type = typeMap[getString("type")] ?: return null
+ val sizes = optJSONArray("sizes").toResourceSizes()
+ val mimeType = tryGetString("mimeType")
+ val maskable = optBoolean("maskable", false)
+
+ return IconRequest.Resource(
+ url = url.sanitizeURL(),
+ type = type,
+ sizes = sizes,
+ mimeType = if (mimeType.isNullOrEmpty()) null else mimeType,
+ maskable = maskable,
+ )
+ } catch (e: JSONException) {
+ Logger.warn("Could not parse message from icons extensions", e)
+ return null
+ }
+}
+
+private fun JSONArray?.toResourceSizes(): List<Size> {
+ val array = this ?: return emptyList()
+
+ return try {
+ array.asSequence { i -> getString(i) }
+ .mapNotNull { raw -> Size.parse(raw) }
+ .toList()
+ } catch (e: JSONException) {
+ Logger.warn("Could not parse message from icons extensions", e)
+ emptyList()
+ } catch (e: NumberFormatException) {
+ Logger.warn("Could not parse message from icons extensions", e)
+ emptyList()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt
new file mode 100644
index 0000000000..ee147b6854
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt
@@ -0,0 +1,53 @@
+/* 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.browser.icons.extension
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.MessageHandler
+import org.json.JSONObject
+
+/**
+ * [MessageHandler] implementation that receives messages from the icons web extensions and performs icon loads.
+ */
+internal class IconMessageHandler(
+ private val store: BrowserStore,
+ private val sessionId: String,
+ private val private: Boolean,
+ private val icons: BrowserIcons,
+) : MessageHandler {
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) // This only exists so that we can wait in tests.
+ internal var lastJob: Job? = null
+
+ override fun onMessage(message: Any, source: EngineSession?): Any {
+ if (message is JSONObject) {
+ message.toIconRequest(private)?.let { loadRequest(it) }
+ } else {
+ throw IllegalStateException("Received unexpected message: $message")
+ }
+
+ // Needs to return something that is not null and not Unit:
+ // https://github.com/mozilla-mobile/android-components/issues/2969
+ return ""
+ }
+
+ private fun loadRequest(request: IconRequest) {
+ lastJob = scope.launch {
+ val icon = icons.loadIcon(request).await()
+
+ store.dispatch(ContentAction.UpdateIconAction(sessionId, request.url, icon.bitmap))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/WebAppManifest.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/WebAppManifest.kt
new file mode 100644
index 0000000000..482e2b1da0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/WebAppManifest.kt
@@ -0,0 +1,49 @@
+/* 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.browser.icons.extension
+
+import android.graphics.Color
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.IconRequest.Resource.Type.MANIFEST_ICON
+import mozilla.components.browser.icons.IconRequest.Size.LAUNCHER
+import mozilla.components.browser.icons.IconRequest.Size.LAUNCHER_ADAPTIVE
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifest.Icon.Purpose
+
+/**
+ * Creates an [IconRequest] for retrieving the icon specified in the manifest.
+ */
+fun WebAppManifest.toIconRequest() = IconRequest(
+ url = startUrl,
+ size = if (SDK_INT >= Build.VERSION_CODES.O) LAUNCHER_ADAPTIVE else LAUNCHER,
+ resources = icons
+ .filter { Purpose.MASKABLE in it.purpose || Purpose.ANY in it.purpose }
+ .map { it.toIconResource() },
+ color = backgroundColor,
+)
+
+/**
+ * Creates an [IconRequest] for retrieving a monochrome icon specified in the manifest.
+ */
+fun WebAppManifest.toMonochromeIconRequest() = IconRequest(
+ url = startUrl,
+ size = IconRequest.Size.DEFAULT,
+ resources = icons
+ .filter { Purpose.MONOCHROME in it.purpose }
+ .map { it.toIconResource() },
+ color = Color.WHITE,
+)
+
+private fun WebAppManifest.Icon.toIconResource(): IconRequest.Resource {
+ return IconRequest.Resource(
+ url = src,
+ type = MANIFEST_ICON,
+ sizes = sizes,
+ mimeType = type,
+ maskable = Purpose.MASKABLE in purpose,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/DefaultIconGenerator.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/DefaultIconGenerator.kt
new file mode 100644
index 0000000000..6b12009755
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/DefaultIconGenerator.kt
@@ -0,0 +1,106 @@
+/* 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.browser.icons.generator
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.ARGB_8888
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.RectF
+import android.util.TypedValue
+import androidx.annotation.ArrayRes
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorRes
+import androidx.annotation.DimenRes
+import androidx.core.content.ContextCompat
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.R
+import mozilla.components.support.ktx.kotlin.getRepresentativeCharacter
+import mozilla.components.support.ktx.kotlin.getRepresentativeSnippet
+import kotlin.math.abs
+
+/**
+ * [IconGenerator] implementation that will generate an icon with a background color, rounded corners and a letter
+ * representing the URL.
+ */
+class DefaultIconGenerator(
+ @DimenRes private val cornerRadiusDimen: Int? = R.dimen.mozac_browser_icons_generator_default_corner_radius,
+ @ColorRes private val textColorRes: Int = R.color.mozac_browser_icons_generator_default_text_color,
+ @ArrayRes private val backgroundColorsRes: Int = R.array.mozac_browser_icons_photon_palette,
+) : IconGenerator {
+
+ override fun generate(context: Context, request: IconRequest): Icon {
+ val size = context.resources.getDimension(request.size.dimen)
+ val sizePx = size.toInt()
+
+ val bitmap = Bitmap.createBitmap(sizePx, sizePx, ARGB_8888)
+ val canvas = Canvas(bitmap)
+
+ val backgroundColor = request.color ?: pickColor(context.resources, request.url)
+
+ val paint = Paint()
+ paint.color = backgroundColor
+
+ val sizeRect = RectF(0f, 0f, size, size)
+ val cornerRadius = cornerRadiusDimen?.let { context.resources.getDimension(it) } ?: 0f
+ canvas.drawRoundRect(sizeRect, cornerRadius, cornerRadius, paint)
+
+ val character = request.url.getRepresentativeCharacter()
+
+ // The text size is calculated dynamically based on the target icon size (1/8th). For an icon
+ // size of 112dp we'd use a text size of 14dp (112 / 8).
+ val textSize = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ size * TARGET_ICON_RATIO,
+ context.resources.displayMetrics,
+ )
+
+ paint.color = ContextCompat.getColor(context, textColorRes)
+ paint.textAlign = Paint.Align.CENTER
+ paint.textSize = textSize
+ paint.isAntiAlias = true
+
+ canvas.drawText(
+ character,
+ canvas.width / 2f,
+ (canvas.height / 2f) - ((paint.descent() + paint.ascent()) / 2f),
+ paint,
+ )
+
+ return Icon(
+ bitmap = bitmap,
+ color = backgroundColor,
+ source = Icon.Source.GENERATOR,
+ maskable = cornerRadius == 0f,
+ )
+ }
+
+ /**
+ * Return a color for this [url]. Colors will be based on the host. URLs with the same host will
+ * return the same color.
+ */
+ @ColorInt
+ internal fun pickColor(resources: Resources, url: String): Int {
+ val backgroundColors = resources.obtainTypedArray(backgroundColorsRes)
+ val color = if (url.isEmpty()) {
+ backgroundColors.getColor(0, 0)
+ } else {
+ val snippet = url.getRepresentativeSnippet()
+ val index = abs(snippet.hashCode() % backgroundColors.length())
+
+ backgroundColors.getColor(index, 0)
+ }
+
+ backgroundColors.recycle()
+ return color
+ }
+
+ companion object {
+ private const val TARGET_ICON_RATIO = 1 / 8f
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/IconGenerator.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/IconGenerator.kt
new file mode 100644
index 0000000000..bfc98e5fa4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/IconGenerator.kt
@@ -0,0 +1,18 @@
+/* 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.browser.icons.generator
+
+import android.content.Context
+import android.graphics.Bitmap
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * A [IconGenerator] implementation can generate a [Bitmap] for an [IconRequest]. It's a fallback if no icon could be
+ * loaded for a specific URL.
+ */
+interface IconGenerator {
+ fun generate(context: Context, request: IconRequest): Icon
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DataUriIconLoader.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DataUriIconLoader.kt
new file mode 100644
index 0000000000..5364e25b52
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DataUriIconLoader.kt
@@ -0,0 +1,36 @@
+/* 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.browser.icons.loader
+
+import android.content.Context
+import android.util.Base64
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * An [IconLoader] implementation that will base64 decode the image bytes from a data:image uri.
+ */
+class DataUriIconLoader : IconLoader {
+ override fun load(context: Context, request: IconRequest, resource: IconRequest.Resource): IconLoader.Result {
+ if (!resource.url.startsWith("data:image/")) {
+ return IconLoader.Result.NoResult
+ }
+
+ val offset = resource.url.indexOf(',') + 1
+ if (offset == 0) {
+ return IconLoader.Result.NoResult
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ return try {
+ IconLoader.Result.BytesResult(
+ Base64.decode(resource.url.substring(offset), Base64.DEFAULT),
+ Icon.Source.INLINE,
+ )
+ } catch (e: Exception) {
+ IconLoader.Result.NoResult
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DiskIconLoader.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DiskIconLoader.kt
new file mode 100644
index 0000000000..f4178af68e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DiskIconLoader.kt
@@ -0,0 +1,26 @@
+/* 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.browser.icons.loader
+
+import android.content.Context
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * [IconLoader] implementation that loads icons from a disk cache.
+ */
+class DiskIconLoader(
+ private val cache: LoaderDiskCache,
+) : IconLoader {
+ interface LoaderDiskCache {
+ fun getIconData(context: Context, resource: IconRequest.Resource): ByteArray?
+ }
+
+ override fun load(context: Context, request: IconRequest, resource: IconRequest.Resource): IconLoader.Result {
+ return cache.getIconData(context, resource)?.let { data ->
+ IconLoader.Result.BytesResult(data, Icon.Source.DISK)
+ } ?: IconLoader.Result.NoResult
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/HttpIconLoader.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/HttpIconLoader.kt
new file mode 100644
index 0000000000..430f46f3ec
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/HttpIconLoader.kt
@@ -0,0 +1,116 @@
+/* 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.browser.icons.loader
+
+import android.content.Context
+import android.os.SystemClock
+import android.util.LruCache
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.net.isHttpOrHttps
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+private const val CONNECT_TIMEOUT = 2L // Seconds
+private const val READ_TIMEOUT = 10L // Seconds
+
+/**
+ * [IconLoader] implementation that will try to download the icon for resources that point to an http(s) URL.
+ */
+open class HttpIconLoader(
+ private val httpClient: Client,
+) : IconLoader {
+ private val logger = Logger("HttpIconLoader")
+ private val failureCache = FailureCache()
+
+ override fun load(context: Context, request: IconRequest, resource: IconRequest.Resource): IconLoader.Result {
+ if (!shouldDownload(resource)) {
+ return IconLoader.Result.NoResult
+ }
+
+ // Right now we always perform a download. We shouldn't retry to download from URLs that have failed just
+ // recently: https://github.com/mozilla-mobile/android-components/issues/2591
+
+ return internalLoad(request, resource)
+ }
+
+ protected fun internalLoad(request: IconRequest, resource: IconRequest.Resource): IconLoader.Result {
+ val downloadRequest = Request(
+ url = resource.url.sanitizeURL(),
+ method = Request.Method.GET,
+ cookiePolicy = if (request.isPrivate) {
+ Request.CookiePolicy.OMIT
+ } else {
+ Request.CookiePolicy.INCLUDE
+ },
+ connectTimeout = Pair(CONNECT_TIMEOUT, TimeUnit.SECONDS),
+ readTimeout = Pair(READ_TIMEOUT, TimeUnit.SECONDS),
+ redirect = Request.Redirect.FOLLOW,
+ useCaches = true,
+ private = request.isPrivate,
+ )
+
+ return try {
+ val response = httpClient.fetch(downloadRequest)
+ if (response.isSuccess) {
+ response.toIconLoaderResult()
+ } else {
+ response.close()
+ failureCache.rememberFailure(resource.url)
+ IconLoader.Result.NoResult
+ }
+ } catch (e: IOException) {
+ logger.debug("IOException while trying to download icon resource", e)
+ IconLoader.Result.NoResult
+ }
+ }
+
+ protected fun shouldDownload(resource: IconRequest.Resource): Boolean {
+ return resource.url.sanitizeURL().toUri().isHttpOrHttps && !failureCache.hasFailedRecently(resource.url)
+ }
+}
+
+private fun Response.toIconLoaderResult() = body.useStream {
+ IconLoader.Result.BytesResult(it.readBytes(), Icon.Source.DOWNLOAD)
+}
+
+private const val MAX_FAILURE_URLS = 25
+private const val FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 30 // 30 Minutes
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal class FailureCache {
+ private val cache = LruCache<String, Long>(MAX_FAILURE_URLS)
+
+ /**
+ * Remembers this [url] after loading from it has failed.
+ */
+ fun rememberFailure(url: String) {
+ cache.put(url, now())
+ }
+
+ /**
+ * Has loading from this [url] failed previously and recently?
+ */
+ fun hasFailedRecently(url: String) =
+ cache.get(url)?.let { failedAt ->
+ if (failedAt + FAILURE_RETRY_MILLISECONDS < now()) {
+ cache.remove(url)
+ false
+ } else {
+ true
+ }
+ } ?: false
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun now() = SystemClock.elapsedRealtime()
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconLoader.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconLoader.kt
new file mode 100644
index 0000000000..963fea68e9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconLoader.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.browser.icons.loader
+
+import android.content.Context
+import android.graphics.Bitmap
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * A loader that can load an icon from an [IconRequest.Resource].
+ */
+interface IconLoader {
+ /**
+ * Tries to load the [IconRequest.Resource] for the given [IconRequest].
+ */
+ fun load(context: Context, request: IconRequest, resource: IconRequest.Resource): Result
+
+ sealed class Result {
+ object NoResult : Result()
+
+ class BitmapResult(
+ val bitmap: Bitmap,
+ val source: Icon.Source,
+ ) : Result()
+
+ class BytesResult(
+ val bytes: ByteArray,
+ val source: Icon.Source,
+ ) : Result()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/MemoryIconLoader.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/MemoryIconLoader.kt
new file mode 100644
index 0000000000..fe8f063a9f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/MemoryIconLoader.kt
@@ -0,0 +1,27 @@
+/* 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.browser.icons.loader
+
+import android.content.Context
+import android.graphics.Bitmap
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * An [IconLoader] implementation that loads icons from an in-memory cache.
+ */
+class MemoryIconLoader(
+ private val cache: LoaderMemoryCache,
+) : IconLoader {
+ interface LoaderMemoryCache {
+ fun getBitmap(request: IconRequest, resource: IconRequest.Resource): Bitmap?
+ }
+
+ override fun load(context: Context, request: IconRequest, resource: IconRequest.Resource): IconLoader.Result {
+ return cache.getBitmap(request, resource)?.let { bitmap ->
+ IconLoader.Result.BitmapResult(bitmap, Icon.Source.MEMORY)
+ } ?: IconLoader.Result.NoResult
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoader.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoader.kt
new file mode 100644
index 0000000000..c3203dc13f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoader.kt
@@ -0,0 +1,43 @@
+/* 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.browser.icons.loader
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.fetch.Client
+
+/**
+ * [HttpIconLoader] variation that will immediately resolve a [load] request with [IconLoader.Result.NoResult]
+ * and then continue to actually download the icon in the background finally calling [loadCallback]
+ * with the actual result and details about the request.
+ *
+ * @property httpClient [Client] used for downloading the icon.
+ * @property scope [CoroutineScope] used for downloading the icon in the background.
+ * Defaults to a new scope using [Dispatchers.IO] for allowing multiple requests to block their threads
+ * while waiting for the download to complete.
+ * @property loadCallback Callback for when the network icon finished downloading or an error or timeout occurred.
+ */
+class NonBlockingHttpIconLoader(
+ httpClient: Client,
+ private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+ private val loadCallback: (IconRequest, IconRequest.Resource, IconLoader.Result) -> Unit,
+) : HttpIconLoader(httpClient) {
+ override fun load(context: Context, request: IconRequest, resource: IconRequest.Resource): IconLoader.Result {
+ if (!shouldDownload(resource)) {
+ return IconLoader.Result.NoResult
+ }
+
+ scope.launch {
+ val icon = internalLoad(request, resource)
+
+ loadCallback(request, resource, icon)
+ }
+
+ return IconLoader.Result.NoResult
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/pipeline/IconResourceComparator.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/pipeline/IconResourceComparator.kt
new file mode 100644
index 0000000000..07ff548bd8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/pipeline/IconResourceComparator.kt
@@ -0,0 +1,75 @@
+/* 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.browser.icons.pipeline
+
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * This [Comparator] implementations compares [IconRequest.Resource] objects to determine which icon to try to load
+ * first.
+ */
+internal object IconResourceComparator : Comparator<IconRequest.Resource> {
+
+ /**
+ * Compare two icon resources. If [resource] is more important, a negative number is returned.
+ * If [other] is more important, a positive number is returned.
+ * If the two resources are of equal importance, 0 is returned.
+ * Importance represents which icon we should try to load first.
+ */
+ override fun compare(resource: IconRequest.Resource, other: IconRequest.Resource) = when {
+ // Two resources pointing to the same URL are always referencing the same icon. So treat them as equal.
+ resource.url == other.url -> 0
+ resource.maskable != other.maskable -> -resource.maskable.compareTo(other.maskable)
+ resource.type != other.type -> -resource.type.rank().compareTo(other.type.rank())
+ resource.maxSize != other.maxSize -> -resource.maxSize.compareTo(other.maxSize)
+ else -> {
+ // If there's no other way to choose, we prefer container types.
+ // They *might* contain an image larger than the size given in the <link> tag.
+ val isResourceContainerType = resource.isContainerType
+ if (isResourceContainerType != other.isContainerType) {
+ if (isResourceContainerType) -1 else 1
+ } else {
+ // There's no way to know which icon might be better. However we need to pick a consistent one
+ // to avoid breaking set implementations (See Fennec Bug 1331808).
+ // Therefore we are picking one by just comparing the URLs.
+ resource.url.compareTo(other.url)
+ }
+ }
+ }
+}
+
+@Suppress("MagicNumber", "ComplexMethod")
+private fun IconRequest.Resource.Type.rank(): Int {
+ return when (this) {
+ // An icon from our "tippy top" list should always be preferred
+ IconRequest.Resource.Type.TIPPY_TOP -> 25
+ IconRequest.Resource.Type.MANIFEST_ICON -> 20
+ // We prefer touch icons because they tend to have a higher resolution than ordinary favicons.
+ IconRequest.Resource.Type.APPLE_TOUCH_ICON -> 15
+ IconRequest.Resource.Type.FAVICON -> 10
+
+ // Fallback icon types:
+ IconRequest.Resource.Type.IMAGE_SRC -> 6
+ IconRequest.Resource.Type.FLUID_ICON -> 5
+ IconRequest.Resource.Type.OPENGRAPH -> 4
+ IconRequest.Resource.Type.TWITTER -> 3
+ IconRequest.Resource.Type.MICROSOFT_TILE -> 2
+ }
+}
+
+private val IconRequest.Resource.maxSize: Int
+ get() = sizes.asSequence().map { size -> size.minLength }.maxOrNull() ?: 0
+
+private val IconRequest.Resource.isContainerType: Boolean
+ get() = mimeType != null && containerTypes.contains(mimeType)
+
+private val containerTypes = listOf(
+ "image/vnd.microsoft.icon",
+ "image/ico",
+ "image/icon",
+ "image/x-icon",
+ "text/ico",
+ "application/ico",
+)
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/DiskIconPreparer.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/DiskIconPreparer.kt
new file mode 100644
index 0000000000..f57ec6388e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/DiskIconPreparer.kt
@@ -0,0 +1,32 @@
+/* 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.browser.icons.preparer
+
+import android.content.Context
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * [IconPreprarer] implementation implementation that will add known resource URLs (from a disk cache) to the request
+ * if the request doesn't contain a list of resources yet.
+ */
+class DiskIconPreparer(
+ private val cache: PreparerDiskCache,
+) : IconPreprarer {
+ interface PreparerDiskCache {
+ fun getResources(context: Context, request: IconRequest): List<IconRequest.Resource>
+ }
+
+ override fun prepare(context: Context, request: IconRequest): IconRequest {
+ return if (request.resources.isEmpty()) {
+ // Only try to load resources for this request from disk if there are no resources attached to this
+ // request yet: Avoid disk reads for *every* request - especially if they can be satisfied by the memory
+ // cache.
+ val resources = cache.getResources(context, request)
+ request.copy(resources = resources)
+ } else {
+ request
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/IconPreprarer.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/IconPreprarer.kt
new file mode 100644
index 0000000000..4d8c138bc3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/IconPreprarer.kt
@@ -0,0 +1,16 @@
+/* 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.browser.icons.preparer
+
+import android.content.Context
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * An [IconPreparer] implementation receives an [IconRequest] before it is getting loaded. The preparer has the option
+ * to rewrite the [IconRequest] and return a new instance.
+ */
+interface IconPreprarer {
+ fun prepare(context: Context, request: IconRequest): IconRequest
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/MemoryIconPreparer.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/MemoryIconPreparer.kt
new file mode 100644
index 0000000000..ec4c5665e7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/MemoryIconPreparer.kt
@@ -0,0 +1,29 @@
+/* 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.browser.icons.preparer
+
+import android.content.Context
+import mozilla.components.browser.icons.IconRequest
+
+/**
+ * An [IconPreprarer] implementation that will add known resource URLs (from an in-memory cache) to the request if the
+ * request doesn't contain a list of resources yet.
+ */
+class MemoryIconPreparer(
+ private val cache: PreparerMemoryCache,
+) : IconPreprarer {
+ interface PreparerMemoryCache {
+ fun getResources(request: IconRequest): List<IconRequest.Resource>
+ }
+
+ override fun prepare(context: Context, request: IconRequest): IconRequest {
+ return if (request.resources.isNotEmpty()) {
+ request
+ } else {
+ val resources = cache.getResources(request)
+ request.copy(resources = resources)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparer.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparer.kt
new file mode 100644
index 0000000000..3763da85ad
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparer.kt
@@ -0,0 +1,89 @@
+/* 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.browser.icons.preparer
+
+import android.content.Context
+import android.content.res.AssetManager
+import android.net.Uri
+import androidx.core.net.toUri
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
+import mozilla.components.support.ktx.android.net.isHttpOrHttps
+import mozilla.components.support.ktx.android.org.json.asSequence
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+private const val LIST_FILE_PATH = "mozac.browser.icons/icons-top200.json"
+
+// Make sure domain added here have the corresponding image_url in icons-top200.json
+private val commonDomain = listOf("wikipedia.org")
+
+/**
+ * Returns the host's common domain if found, else null is returned
+ */
+private val Uri.hostWithCommonDomain: String?
+ get() {
+ val host = host ?: return null
+ for (domain in commonDomain) {
+ if (host.endsWith(domain)) return domain
+ }
+ return null
+ }
+
+/**
+ * [IconPreprarer] implementation that looks up the host in our "tippy top" list. If it can find a match then it inserts
+ * the icon URL into the request.
+ *
+ * The "tippy top" list is a list of "good" icons for top pages maintained by Mozilla:
+ * https://github.com/mozilla/tippy-top-sites
+ */
+class TippyTopIconPreparer(
+ assetManager: AssetManager,
+) : IconPreprarer {
+ private val iconMap: Map<String, String> by lazy { parseList(assetManager) }
+
+ override fun prepare(context: Context, request: IconRequest): IconRequest {
+ val uri = request.url.toUri()
+ if (!uri.isHttpOrHttps) {
+ return request
+ }
+
+ val host = uri.hostWithCommonDomain ?: uri.hostWithoutCommonPrefixes
+
+ return if (host != null && iconMap.containsKey(host)) {
+ val resource = IconRequest.Resource(
+ url = iconMap.getValue(host),
+ type = IconRequest.Resource.Type.TIPPY_TOP,
+ )
+
+ request.copy(resources = request.resources + resource)
+ } else {
+ request
+ }
+ }
+}
+
+private fun parseList(assetManager: AssetManager): Map<String, String> = try {
+ JSONArray(assetManager.open(LIST_FILE_PATH).bufferedReader().readText())
+ .asSequence()
+ .flatMap { entry ->
+ val json = entry as JSONObject
+ val domains = json.getJSONArray("domains")
+ val iconUrl = json.getString("image_url")
+
+ domains.asSequence().map { domain -> Pair(domain.toString(), iconUrl) }
+ }
+ .toMap()
+} catch (e: JSONException) {
+ Log.log(
+ priority = Log.Priority.ERROR,
+ tag = "TippyTopIconPreparer",
+ message = "Could not load tippy top list from assets",
+ throwable = e,
+ )
+ emptyMap()
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessor.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessor.kt
new file mode 100644
index 0000000000..1781b949a1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessor.kt
@@ -0,0 +1,76 @@
+/* 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.browser.icons.processor
+
+import android.content.Context
+import android.graphics.Paint
+import android.graphics.Paint.ANTI_ALIAS_FLAG
+import android.graphics.Rect
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.core.graphics.applyCanvas
+import androidx.core.graphics.createBitmap
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.images.DesiredSize
+import kotlin.math.max
+
+/**
+ * [IconProcessor] implementation that builds maskable icons.
+ */
+class AdaptiveIconProcessor : IconProcessor {
+
+ /**
+ * Creates an adaptive icon using the base icon.
+ * On older devices, non-maskable icons are not transformed.
+ */
+ override fun process(
+ context: Context,
+ request: IconRequest,
+ resource: IconRequest.Resource?,
+ icon: Icon,
+ desiredSize: DesiredSize,
+ ): Icon {
+ val maskable = resource?.maskable == true
+ if (!maskable && SDK_INT < Build.VERSION_CODES.O) {
+ return icon
+ }
+
+ val originalBitmap = icon.bitmap
+
+ val paddingRatio = if (maskable) {
+ MASKABLE_ICON_PADDING_RATIO
+ } else {
+ TRANSPARENT_ICON_PADDING_RATIO
+ }
+ val maskedIconSize = max(originalBitmap.width, originalBitmap.height)
+ val padding = (paddingRatio * maskedIconSize).toInt()
+
+ // The actual size of the icon asset, before masking, in pixels.
+ val rawIconSize = 2 * padding + maskedIconSize
+ val maskBounds = Rect(0, 0, maskedIconSize, maskedIconSize).apply {
+ offset(padding, padding)
+ }
+
+ val paint = Paint(ANTI_ALIAS_FLAG).apply { isFilterBitmap = true }
+
+ val paddedBitmap = createBitmap(rawIconSize, rawIconSize).applyCanvas {
+ icon.color?.also { drawColor(it) }
+ drawBitmap(originalBitmap, null, maskBounds, paint)
+ }
+
+ return icon.copy(bitmap = paddedBitmap, maskable = true).also { originalBitmap.recycle() }
+ }
+
+ companion object {
+ private const val MASKABLE_ICON_SAFE_ZONE = 4 / 5f
+ private const val ADAPTIVE_ICON_SAFE_ZONE = 66 / 108f
+ private const val TRANSPARENT_ICON_SAFE_ZONE = 192 / 176f
+ private const val MASKABLE_ICON_PADDING_RATIO =
+ ((MASKABLE_ICON_SAFE_ZONE / ADAPTIVE_ICON_SAFE_ZONE) - 1) / 2
+ private const val TRANSPARENT_ICON_PADDING_RATIO =
+ ((TRANSPARENT_ICON_SAFE_ZONE / ADAPTIVE_ICON_SAFE_ZONE) - 1) / 2
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ColorProcessor.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ColorProcessor.kt
new file mode 100644
index 0000000000..02563f9ffc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ColorProcessor.kt
@@ -0,0 +1,37 @@
+/* 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.browser.icons.processor
+
+import android.content.Context
+import android.graphics.Color
+import androidx.palette.graphics.Palette
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.images.DesiredSize
+
+/**
+ * [IconProcessor] implementation to extract the dominant color from the icon.
+ */
+class ColorProcessor : IconProcessor {
+
+ override fun process(
+ context: Context,
+ request: IconRequest,
+ resource: IconRequest.Resource?,
+ icon: Icon,
+ desiredSize: DesiredSize,
+ ): Icon {
+ // If the icon already has a color set, just return
+ if (icon.color != null) return icon
+ // If the request already has a color set, then return.
+ // Some PWAs just set the background color to white (such as Twitter, Starbucks)
+ // but the icons would work better if we fill the background using the Palette API.
+ // If a PWA really want a white background a maskable icon can be used.
+ if (request.color != null && request.color != Color.WHITE) return icon.copy(color = request.color)
+
+ val swatch = Palette.from(icon.bitmap).generate().dominantSwatch
+ return swatch?.run { icon.copy(color = rgb) } ?: icon
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/DiskIconProcessor.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/DiskIconProcessor.kt
new file mode 100644
index 0000000000..4e0d74f49d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/DiskIconProcessor.kt
@@ -0,0 +1,52 @@
+/* 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.browser.icons.processor
+
+import android.content.Context
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.Icon.Source
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.images.DesiredSize
+
+/**
+ * [IconProcessor] implementation that saves icons in the disk cache.
+ */
+class DiskIconProcessor(
+ private val cache: ProcessorDiskCache,
+) : IconProcessor {
+ interface ProcessorDiskCache {
+ /**
+ * Saves icon resources to cache.
+ * */
+ fun putResources(context: Context, request: IconRequest)
+
+ /**
+ * Saves icon bitmap to cache.
+ * */
+ fun putIcon(context: Context, resource: IconRequest.Resource, icon: Icon)
+ }
+
+ override fun process(
+ context: Context,
+ request: IconRequest,
+ resource: IconRequest.Resource?,
+ icon: Icon,
+ desiredSize: DesiredSize,
+ ): Icon {
+ if (resource != null && !request.isPrivate) {
+ cache.putResources(context, request)
+ if (icon.shouldCacheOnDisk) {
+ cache.putIcon(context, resource, icon)
+ }
+ }
+ return icon
+ }
+}
+
+private val Icon.shouldCacheOnDisk: Boolean
+ get() = when (source) {
+ Source.DOWNLOAD, Source.INLINE -> true
+ Source.GENERATOR, Source.MEMORY, Source.DISK -> false
+ }
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/IconProcessor.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/IconProcessor.kt
new file mode 100644
index 0000000000..afcb0dddbb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/IconProcessor.kt
@@ -0,0 +1,24 @@
+/* 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.browser.icons.processor
+
+import android.content.Context
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.images.DesiredSize
+
+/**
+ * An [IconProcessor] implementation receives the [Icon] with the [IconRequest] and [IconRequest.Resource] after
+ * the icon was loaded. The [IconProcessor] has the option to rewrite a loaded [Icon] and return a new instance.
+ */
+interface IconProcessor {
+ fun process(
+ context: Context,
+ request: IconRequest,
+ resource: IconRequest.Resource?,
+ icon: Icon,
+ desiredSize: DesiredSize,
+ ): Icon?
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/MemoryIconProcessor.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/MemoryIconProcessor.kt
new file mode 100644
index 0000000000..b13d2f6134
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/MemoryIconProcessor.kt
@@ -0,0 +1,42 @@
+/* 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.browser.icons.processor
+
+import android.content.Context
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.Icon.Source
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.images.DesiredSize
+
+/**
+ * An [IconProcessor] implementation that saves icons in the in-memory cache.
+ */
+class MemoryIconProcessor(
+ private val cache: ProcessorMemoryCache,
+) : IconProcessor {
+ interface ProcessorMemoryCache {
+ fun put(request: IconRequest, resource: IconRequest.Resource, icon: Icon)
+ }
+
+ override fun process(
+ context: Context,
+ request: IconRequest,
+ resource: IconRequest.Resource?,
+ icon: Icon,
+ desiredSize: DesiredSize,
+ ): Icon {
+ if (resource != null && icon.shouldCacheInMemory) {
+ cache.put(request, resource, icon)
+ }
+
+ return icon
+ }
+}
+
+private val Icon.shouldCacheInMemory: Boolean
+ get() = when (source) {
+ Source.DOWNLOAD, Source.INLINE, Source.DISK -> true
+ Source.GENERATOR, Source.MEMORY -> false
+ }
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ResizingProcessor.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ResizingProcessor.kt
new file mode 100644
index 0000000000..d9daadf77f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ResizingProcessor.kt
@@ -0,0 +1,67 @@
+/* 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.browser.icons.processor
+
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.images.DesiredSize
+import kotlin.math.roundToInt
+
+/**
+ * [IconProcessor] implementation for resizing the loaded icon based on the target size.
+ */
+class ResizingProcessor(
+ private val discardSmallIcons: Boolean = true,
+) : IconProcessor {
+
+ override fun process(
+ context: Context,
+ request: IconRequest,
+ resource: IconRequest.Resource?,
+ icon: Icon,
+ desiredSize: DesiredSize,
+ ): Icon? {
+ val originalBitmap = icon.bitmap
+ val size = originalBitmap.width
+ val (targetSize, _, _, maxScaleFactor) = desiredSize
+
+ // The bitmap has exactly the size we are looking for.
+ if (size == targetSize) return icon
+
+ val resizedBitmap = if (size > targetSize) {
+ resize(originalBitmap, targetSize)
+ } else {
+ // Our largest primary is smaller than the desired size. Upscale it (to a limit)!
+ // 'largestSize' now reflects the maximum size we can upscale to.
+ val largestSize = (size * maxScaleFactor).roundToInt()
+
+ if (largestSize > targetSize) {
+ // Perfect! WE can upscale and reach the needed size.
+ resize(originalBitmap, targetSize)
+ } else {
+ // We don't have enough information to make the target size look non-terrible.
+ // Best effort scale, unless we're told to throw away small icons.
+ if (discardSmallIcons) null else resize(originalBitmap, largestSize)
+ }
+ }
+
+ return icon.copy(bitmap = resizedBitmap ?: return null)
+ }
+
+ /**
+ * Create a new bitmap scaled from an existing bitmap.
+ */
+ @VisibleForTesting
+ internal fun resize(bitmap: Bitmap, targetSize: Int) = try {
+ Bitmap.createScaledBitmap(bitmap, targetSize, targetSize, true)
+ } catch (e: OutOfMemoryError) {
+ // There's not enough memory to create a resized copy of the bitmap in memory. Let's just
+ // use what we have.
+ bitmap
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconDiskCache.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconDiskCache.kt
new file mode 100644
index 0000000000..a34a4cdd6c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconDiskCache.kt
@@ -0,0 +1,185 @@
+/* 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.browser.icons.utils
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+import com.jakewharton.disklrucache.DiskLruCache
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.extension.toIconResources
+import mozilla.components.browser.icons.extension.toJSON
+import mozilla.components.browser.icons.loader.DiskIconLoader
+import mozilla.components.browser.icons.preparer.DiskIconPreparer
+import mozilla.components.browser.icons.processor.DiskIconProcessor
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.sha1
+import org.json.JSONArray
+import org.json.JSONException
+import java.io.File
+import java.io.IOException
+
+private const val RESOURCES_DISK_CACHE_VERSION = 1
+private const val ICON_DATA_DISK_CACHE_VERSION = 1
+
+private const val MAXIMUM_CACHE_RESOURCES_BYTES: Long = 1024L * 1024L * 10L // 10 MB
+private const val MAXIMUM_CACHE_ICON_DATA_BYTES: Long = 1024L * 1024L * 100L // 100 MB
+
+private const val WEBP_QUALITY = 90
+
+/**
+ * Caching bitmaps and resource URLs on disk.
+ */
+class IconDiskCache :
+ DiskIconLoader.LoaderDiskCache,
+ DiskIconPreparer.PreparerDiskCache,
+ DiskIconProcessor.ProcessorDiskCache {
+ private val logger = Logger("Icons/IconDiskCache")
+
+ @VisibleForTesting
+ internal var iconResourcesCache: DiskLruCache? = null
+
+ @VisibleForTesting
+ internal var iconDataCache: DiskLruCache? = null
+ private val iconResourcesCacheWriteLock = Any()
+ private val iconDataCacheWriteLock = Any()
+
+ override fun getResources(context: Context, request: IconRequest): List<IconRequest.Resource> {
+ val key = createKey(request.url)
+ val snapshot: DiskLruCache.Snapshot = getIconResourcesCache(context).get(key)
+ ?: return emptyList()
+
+ try {
+ val data = snapshot.getInputStream(0).use {
+ it.buffered().reader().readText()
+ }
+
+ return JSONArray(data).toIconResources()
+ } catch (e: IOException) {
+ logger.info("Failed to load resources from disk", e)
+ } catch (e: JSONException) {
+ logger.warn("Failed to parse resources from disk", e)
+ }
+
+ return emptyList()
+ }
+
+ override fun putResources(context: Context, request: IconRequest) {
+ try {
+ synchronized(iconResourcesCacheWriteLock) {
+ val key = createKey(request.url)
+ val editor = getIconResourcesCache(context)
+ .edit(key) ?: return
+
+ val data = request.resources.toJSON().toString()
+ editor.set(0, data)
+
+ editor.commit()
+ }
+ } catch (e: IOException) {
+ logger.info("Failed to save resources to disk", e)
+ } catch (e: JSONException) {
+ logger.warn("Failed to serialize resources")
+ }
+ }
+
+ override fun putIcon(context: Context, resource: IconRequest.Resource, icon: Icon) {
+ putIconBitmap(context, resource, icon.bitmap)
+ }
+
+ override fun getIconData(context: Context, resource: IconRequest.Resource): ByteArray? {
+ val key = createKey(resource.url)
+
+ val snapshot = getIconDataCache(context).get(key)
+ ?: return null
+
+ return try {
+ snapshot.getInputStream(0).use {
+ it.buffered().readBytes()
+ }
+ } catch (e: IOException) {
+ logger.info("Failed to read icon bitmap from disk", e)
+ null
+ }
+ }
+
+ internal fun putIconBitmap(context: Context, resource: IconRequest.Resource, bitmap: Bitmap) {
+ val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Bitmap.CompressFormat.WEBP_LOSSY
+ } else {
+ @Suppress("DEPRECATION")
+ Bitmap.CompressFormat.WEBP
+ }
+ try {
+ synchronized(iconDataCacheWriteLock) {
+ val editor = getIconDataCache(context)
+ .edit(createKey(resource.url)) ?: return
+
+ editor.newOutputStream(0).use { stream ->
+ bitmap.compress(compressFormat, WEBP_QUALITY, stream)
+ }
+
+ editor.commit()
+ }
+ } catch (e: IOException) {
+ logger.info("Failed to save icon bitmap to disk", e)
+ }
+ }
+
+ internal fun clear(context: Context) {
+ try {
+ getIconResourcesCache(context).delete()
+ } catch (e: IOException) {
+ logger.warn("Icon resource cache could not be cleared. Perhaps there is none?")
+ }
+
+ try {
+ getIconDataCache(context).delete()
+ } catch (e: IOException) {
+ logger.warn("Icon data cache could not be cleared. Perhaps there is none?")
+ }
+
+ iconResourcesCache = null
+ iconDataCache = null
+ }
+
+ @Synchronized
+ private fun getIconResourcesCache(context: Context): DiskLruCache {
+ iconResourcesCache?.let { return it }
+
+ return DiskLruCache.open(
+ getIconResourcesCacheDirectory(context),
+ RESOURCES_DISK_CACHE_VERSION,
+ 1,
+ MAXIMUM_CACHE_RESOURCES_BYTES,
+ ).also { iconResourcesCache = it }
+ }
+
+ private fun getIconResourcesCacheDirectory(context: Context): File {
+ val cacheDirectory = File(context.cacheDir, "mozac_browser_icons")
+ return File(cacheDirectory, "resources")
+ }
+
+ @Synchronized
+ private fun getIconDataCache(context: Context): DiskLruCache {
+ iconDataCache?.let { return it }
+
+ return DiskLruCache.open(
+ getIconDataCacheDirectory(context),
+ ICON_DATA_DISK_CACHE_VERSION,
+ 1,
+ MAXIMUM_CACHE_ICON_DATA_BYTES,
+ ).also { iconDataCache = it }
+ }
+
+ private fun getIconDataCacheDirectory(context: Context): File {
+ val cacheDirectory = File(context.cacheDir, "mozac_browser_icons")
+ return File(cacheDirectory, "icons")
+ }
+}
+
+private fun createKey(rawKey: String): String = rawKey.sha1()
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconMemoryCache.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconMemoryCache.kt
new file mode 100644
index 0000000000..33b4864a37
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconMemoryCache.kt
@@ -0,0 +1,62 @@
+/* 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.browser.icons.utils
+
+import android.graphics.Bitmap
+import android.util.LruCache
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.loader.MemoryIconLoader.LoaderMemoryCache
+import mozilla.components.browser.icons.preparer.MemoryIconPreparer
+import mozilla.components.browser.icons.processor.MemoryIconProcessor.ProcessorMemoryCache
+
+private const val MAXIMUM_CACHE_URLS = 1000
+
+// Use a better mechanism for determining the cache size:
+// https://github.com/mozilla-mobile/android-components/issues/2764
+private const val MAXIMUM_CACHE_BITMAP_BYTES = 1024 * 1024 * 25 // 25 MB
+
+class IconMemoryCache : ProcessorMemoryCache, LoaderMemoryCache, MemoryIconPreparer.PreparerMemoryCache {
+ private val iconResourcesCache = LruCache<String, List<IconRequest.Resource>>(MAXIMUM_CACHE_URLS)
+ private val iconBitmapCache = object : LruCache<String, Bitmap>(MAXIMUM_CACHE_BITMAP_BYTES) {
+ override fun sizeOf(key: String, value: Bitmap): Int {
+ return value.byteCount
+ }
+ }
+
+ override fun getResources(request: IconRequest): List<IconRequest.Resource> {
+ return iconResourcesCache[request.url] ?: emptyList()
+ }
+
+ override fun getBitmap(request: IconRequest, resource: IconRequest.Resource): Bitmap? {
+ return iconBitmapCache[resource.url]
+ }
+
+ override fun put(request: IconRequest, resource: IconRequest.Resource, icon: Icon) {
+ if (icon.source.shouldCacheInMemory) {
+ iconBitmapCache.put(resource.url, icon.bitmap)
+ }
+
+ if (request.resources.isNotEmpty()) {
+ iconResourcesCache.put(request.url, request.resources)
+ }
+ }
+
+ internal fun clear() {
+ iconResourcesCache.evictAll()
+ iconBitmapCache.evictAll()
+ }
+}
+
+private val Icon.Source.shouldCacheInMemory: Boolean
+ get() {
+ return when (this) {
+ Icon.Source.DOWNLOAD -> true
+ Icon.Source.INLINE -> true
+ Icon.Source.GENERATOR -> false
+ Icon.Source.MEMORY -> false
+ Icon.Source.DISK -> true
+ }
+ }
diff --git a/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/Utils.kt b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/Utils.kt
new file mode 100644
index 0000000000..0ef339f2c8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/Utils.kt
@@ -0,0 +1,64 @@
+/* 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.browser.icons.utils
+
+/**
+ * For a list of size [Pair]s (width, height) returns the best [Pair] for the given [targetSize], [maxSize] and
+ * [maxScaleFactor].
+ */
+@Suppress("ReturnCount")
+internal fun List<Pair<Int, Int>>.findBestSize(targetSize: Int, maxSize: Int, maxScaleFactor: Float): Pair<Int, Int>? {
+ // First look for a pair that is close to (but not smaller than) our target size.
+ val ideal = filter { (x, y) ->
+ x >= targetSize && y >= targetSize && x <= maxSize && y <= maxSize
+ }.filter { (x, y) ->
+ x == y
+ }.minByOrNull { (x, _) ->
+ x
+ }
+
+ if (ideal != null) {
+ // We found an icon that is exactly in the range we want, yay!
+ return ideal
+ }
+
+ // Next try to find a pair that is larger than our target size but that we can downscale to be in our target range.
+ val downscalable = filter { (x, y) ->
+ val downScaledX = (x.toFloat() * (1.0f / maxScaleFactor)).toInt()
+ val downScaledY = (y.toFloat() * (1.0f / maxScaleFactor)).toInt()
+
+ downScaledX >= targetSize &&
+ downScaledY >= targetSize &&
+ downScaledX <= maxSize &&
+ downScaledY <= maxSize
+ }.minByOrNull { (x, _) ->
+ x
+ }
+
+ if (downscalable != null) {
+ // We found an icon we can downscale to our desired size.
+ return downscalable
+ }
+
+ // Finally try to find a pair that is smaller than our target size but that we can upscale to our target range.
+ val upscalable = filter { (x, y) ->
+ val upscaledX = x * maxScaleFactor
+ val upscaledY = y * maxScaleFactor
+
+ upscaledX >= targetSize &&
+ upscaledY >= targetSize &&
+ upscaledX <= maxSize &&
+ upscaledY <= maxSize
+ }.maxByOrNull { (x, _) ->
+ x
+ }
+
+ if (upscalable != null) {
+ // We found an icon we can upscale to our desired size.
+ return upscalable
+ }
+
+ return null
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/main/res/values/colors.xml b/mobile/android/android-components/components/browser/icons/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..809f1d7a80
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/res/values/colors.xml
@@ -0,0 +1,18 @@
+<?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>
+ <color name="mozac_browser_icons_generator_default_text_color">@android:color/white</color>
+ <!-- Mozilla's Visual Design Colour Palette: http://firefoxux.github.io/StyleGuide/#/visualDesign/colours -->
+ <array name="mozac_browser_icons_photon_palette">
+ <item>#9A4C00</item>
+ <item>#AB008D</item>
+ <item>#4C009C</item>
+ <item>#002E9C</item>
+ <item>#009EC2</item>
+ <item>#009D02</item>
+ <item>#51AB00</item>
+ <item>#36385A</item>
+ </array>
+</resources>
diff --git a/mobile/android/android-components/components/browser/icons/src/main/res/values/dimens.xml b/mobile/android/android-components/components/browser/icons/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..03f785dbdc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/res/values/dimens.xml
@@ -0,0 +1,13 @@
+<?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 xmlns:tools="http://schemas.android.com/tools">
+ <dimen name="mozac_browser_icons_size_default">32dp</dimen>
+ <dimen name="mozac_browser_icons_size_launcher">48dp</dimen>
+ <dimen name="mozac_browser_icons_size_launcher_adaptive">102dp</dimen>
+
+ <dimen name="mozac_browser_icons_maximum_size" tools:ignore="PxUsage">1024px</dimen>
+ <dimen name="mozac_browser_icons_minimum_size" tools:ignore="PxUsage">32px</dimen>
+ <dimen name="mozac_browser_icons_generator_default_corner_radius">2dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/icons/src/main/res/values/tags.xml b/mobile/android/android-components/components/browser/icons/src/main/res/values/tags.xml
new file mode 100644
index 0000000000..de0c5625d6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/main/res/values/tags.xml
@@ -0,0 +1,7 @@
+<?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>
+ <item name="mozac_browser_icons_tag_job" type="id" />
+</resources>
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt
new file mode 100644
index 0000000000..67a392d0a2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt
@@ -0,0 +1,339 @@
+/* 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.browser.icons
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.Looper.getMainLooper
+import android.widget.ImageView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import mozilla.components.browser.icons.generator.IconGenerator
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okio.buffer
+import okio.source
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertSame
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows.shadowOf
+import java.io.OutputStream
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class BrowserIconsTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ @After
+ fun cleanUp() {
+ sharedDiskCache.clear(testContext)
+ sharedMemoryCache.clear()
+ }
+
+ @Test
+ fun `Uses generator`() = runTestOnMain {
+ val mockedIcon: Icon = mock()
+
+ val generator: IconGenerator = mock()
+ `when`(generator.generate(any(), any())).thenReturn(mockedIcon)
+
+ val request = IconRequest(url = "https://www.mozilla_test.org")
+ val icon = BrowserIcons(testContext, httpClient = mock(), generator = generator)
+ .loadIcon(request)
+
+ assertEquals(mockedIcon, icon.await())
+
+ verify(generator).generate(testContext, request)
+ }
+
+ @Test
+ fun `WHEN resources are provided THEN an icon will be downloaded from one of them`() = runTestOnMain {
+ val server = MockWebServer()
+
+ server.enqueue(
+ MockResponse().setBody(
+
+ javaClass.getResourceAsStream("/png/mozac.png")!!.source().buffer().buffer,
+ ),
+ )
+
+ server.start()
+
+ try {
+ val request = IconRequest(
+ url = server.url("/").toString(),
+ size = IconRequest.Size.DEFAULT,
+ resources = listOf(
+ IconRequest.Resource(
+ url = server.url("icon64.png").toString(),
+ sizes = listOf(Size(64, 64)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = server.url("icon128.png").toString(),
+ sizes = listOf(Size(128, 128)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = server.url("icon128.png").toString(),
+ sizes = listOf(Size(180, 180)),
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ ),
+ )
+
+ val icon = BrowserIcons(
+ testContext,
+ httpClient = HttpURLConnectionClient(),
+ ).loadIcon(request).await()
+
+ assertNotNull(icon)
+ assertNotNull(icon.bitmap)
+
+ val serverRequest = server.takeRequest()
+ assertEquals("/icon128.png", serverRequest.requestUrl?.encodedPath)
+ } finally {
+ server.shutdown()
+ }
+ }
+
+ @Test
+ fun `WHEN icon is loaded twice THEN second load is delivered from memory cache`() = runTestOnMain {
+ val server = MockWebServer()
+
+ server.enqueue(
+ MockResponse().setBody(
+ javaClass.getResourceAsStream("/png/mozac.png")!!.source().buffer().buffer,
+ ),
+ )
+
+ server.start()
+
+ try {
+ val icons = BrowserIcons(testContext, httpClient = HttpURLConnectionClient())
+
+ val request = IconRequest(
+ url = "https://www.mozilla.org",
+ resources = listOf(
+ IconRequest.Resource(
+ url = server.url("icon64.png").toString(),
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ ),
+ )
+
+ val icon = icons.loadIcon(request).await()
+
+ assertEquals(Icon.Source.DOWNLOAD, icon.source)
+ assertNotNull(icon.bitmap)
+
+ val secondIcon = icons.loadIcon(
+ IconRequest("https://www.mozilla.org"), // Without resources!
+ ).await()
+
+ assertEquals(Icon.Source.MEMORY, secondIcon.source)
+ assertNotNull(secondIcon.bitmap)
+
+ assertSame(icon.bitmap, secondIcon.bitmap)
+ } finally {
+ server.shutdown()
+ }
+ }
+
+ @Test
+ fun `WHEN icon is loaded again and not in memory cache THEN second load is delivered from disk cache`() = runTestOnMain {
+ val server = MockWebServer()
+
+ server.enqueue(
+ MockResponse().setBody(
+ javaClass.getResourceAsStream("/png/mozac.png")!!.source().buffer().buffer,
+ ),
+ )
+
+ server.start()
+
+ try {
+ val icons = BrowserIcons(testContext, httpClient = HttpURLConnectionClient())
+
+ val request = IconRequest(
+ url = "https://www.mozilla.org",
+ resources = listOf(
+ IconRequest.Resource(
+ url = server.url("icon64.png").toString(),
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ ),
+ )
+
+ val icon = icons.loadIcon(request).await()
+
+ assertEquals(Icon.Source.DOWNLOAD, icon.source)
+ assertNotNull(icon.bitmap)
+
+ sharedMemoryCache.clear()
+
+ val secondIcon = icons.loadIcon(
+ IconRequest("https://www.mozilla.org"), // Without resources!
+ ).await()
+
+ assertEquals(Icon.Source.DISK, secondIcon.source)
+ assertNotNull(secondIcon.bitmap)
+ } finally {
+ server.shutdown()
+ }
+ }
+
+ @Test
+ fun `Automatically load icon into image view`() {
+ val mockedBitmap: Bitmap = mock()
+ val mockedIcon: Icon = mock()
+ val result = CompletableDeferred<Icon>()
+ val view: ImageView = mock()
+ val icons = spy(BrowserIcons(testContext, httpClient = mock()))
+
+ val request = IconRequest(url = "https://www.mozilla.org")
+
+ doReturn(mockedBitmap).`when`(mockedIcon).bitmap
+ doReturn(result).`when`(icons).loadIconInternalAsync(eq(request), any())
+
+ val job = icons.loadIntoView(view, request)
+
+ verify(view).setImageDrawable(null)
+ verify(view).addOnAttachStateChangeListener(any())
+ verify(view).setTag(eq(R.id.mozac_browser_icons_tag_job), any())
+ verify(view, never()).setImageBitmap(any())
+
+ result.complete(mockedIcon)
+ shadowOf(getMainLooper()).idle()
+ job.joinBlocking()
+
+ verify(view).setImageBitmap(mockedBitmap)
+ verify(view).removeOnAttachStateChangeListener(any())
+ verify(view).setTag(R.id.mozac_browser_icons_tag_job, null)
+ }
+
+ @Test
+ fun `loadIntoView sets drawable to error if cancelled`() {
+ val result = CompletableDeferred<Icon>()
+ val view: ImageView = mock()
+ val placeholder: Drawable = mock()
+ val error: Drawable = mock()
+ val icons = spy(BrowserIcons(testContext, httpClient = mock()))
+
+ val request = IconRequest(url = "https://www.mozilla.org")
+
+ doReturn(result).`when`(icons).loadIconInternalAsync(eq(request), any())
+
+ val job = icons.loadIntoView(view, request, placeholder = placeholder, error = error)
+
+ verify(view).setImageDrawable(placeholder)
+
+ result.cancel()
+ shadowOf(getMainLooper()).idle()
+ job.joinBlocking()
+
+ verify(view).setImageDrawable(error)
+ verify(view).removeOnAttachStateChangeListener(any())
+ verify(view).setTag(R.id.mozac_browser_icons_tag_job, null)
+ }
+
+ @Test
+ fun `loadIntoView cancels previous jobs`() {
+ val result = CompletableDeferred<Icon>()
+ val view: ImageView = mock()
+ val previousJob: Job = mock()
+ val icons = spy(BrowserIcons(testContext, httpClient = mock()))
+
+ val request = IconRequest(url = "https://www.mozilla.org")
+
+ doReturn(previousJob).`when`(view).getTag(R.id.mozac_browser_icons_tag_job)
+ doReturn(result).`when`(icons).loadIconInternalAsync(eq(request), any())
+
+ icons.loadIntoView(view, request)
+
+ verify(previousJob).cancel()
+
+ result.cancel()
+ }
+
+ @Test
+ fun `clear should delete all disk and memory data`() {
+ // Test the effect of clear by first adding some icons data
+ val icons = BrowserIcons(testContext, httpClient = HttpURLConnectionClient())
+ val resource = IconRequest.Resource(
+ url = "https://www.mozilla.org/icon64.png",
+ sizes = listOf(Size(64, 64)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ )
+ val request = IconRequest(url = "https://www.mozilla.org", resources = listOf(resource))
+ sharedDiskCache.putResources(testContext, request)
+ val bitmap: Bitmap = mock()
+ `when`(bitmap.compress(any(), ArgumentMatchers.anyInt(), any())).thenAnswer {
+ @Suppress("DEPRECATION")
+ // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/9555
+ assertEquals(Bitmap.CompressFormat.WEBP, it.arguments[0] as Bitmap.CompressFormat)
+ assertEquals(90, it.arguments[1] as Int) // Quality
+
+ val stream = it.arguments[2] as OutputStream
+ stream.write("Hello World".toByteArray())
+ true
+ }
+ val icon = Icon(bitmap, source = Icon.Source.DOWNLOAD)
+ sharedMemoryCache.put(request, resource, icon)
+
+ // Verifying it's all there
+ assertEquals(listOf(resource), sharedDiskCache.getResources(testContext, request))
+ assertEquals(listOf(resource), sharedMemoryCache.getResources(request))
+
+ icons.clear()
+
+ // Verifying it's not anymore
+ assertEquals(0, sharedDiskCache.getResources(testContext, request).size)
+ assertEquals(0, sharedMemoryCache.getResources(request).size)
+ }
+
+ @Test
+ fun `GIVEN an IconRequest WHEN getDesiredSize is called THEN set min and max bounds to the request target size`() {
+ val request = IconRequest("https://mozilla.org", IconRequest.Size.LAUNCHER_ADAPTIVE)
+
+ val result = request.getDesiredSize(testContext, 11, 101)
+
+ assertEquals(
+ testContext.resources.getDimensionPixelSize(IconRequest.Size.LAUNCHER_ADAPTIVE.dimen),
+ result.targetSize,
+ )
+ assertEquals(11, result.minSize)
+ assertEquals(101, result.maxSize)
+ assertEquals(MAXIMUM_SCALE_FACTOR, result.maxScaleFactor)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt
new file mode 100644
index 0000000000..bd8ec59cd8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt
@@ -0,0 +1,238 @@
+/* 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.browser.icons.decoder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.decoder.ico.IconDirectoryEntry
+import mozilla.components.browser.icons.decoder.ico.decodeDirectoryEntries
+import mozilla.components.support.images.DesiredSize
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ICOIconDecoderTest {
+
+ @Test
+ fun `Decoding microsoft favicon`() {
+ val icon = loadIcon("microsoft_favicon.ico")
+ val decoder = ICOIconDecoder()
+
+ val bitmap = decoder.decode(icon, DesiredSize(192, 192, 1024, 2.0f))
+ assertNotNull(bitmap)
+ }
+
+ @Test
+ fun `Decoding golem favicon`() {
+ val icon = loadIcon("golem_favicon.ico")
+ val decoder = ICOIconDecoder()
+
+ val bitmap = decoder.decode(icon, DesiredSize(192, 192, 1024, 2.0f))
+ assertNotNull(bitmap)
+ }
+
+ @Test
+ fun `Decoding nivida favicon`() {
+ val icon = loadIcon("nvidia_favicon.ico")
+ val decoder = ICOIconDecoder()
+
+ val bitmap = decoder.decode(icon, DesiredSize(96, 96, 1024, 2.0f))
+ assertNotNull(bitmap)
+ }
+
+ @Test
+ fun testGolemIconEntries() {
+ val icon = loadIcon("golem_favicon.ico")
+
+ val entries = decodeDirectoryEntries(icon, 1024)
+ assertEquals(5, entries.size)
+
+ val expectedEntries = arrayOf(
+ IconDirectoryEntry(
+ 16,
+ 16,
+ 0,
+ 32,
+ 1128,
+ 39520,
+ false,
+ 4,
+ ),
+ IconDirectoryEntry(
+ 24,
+ 24,
+ 0,
+ 32,
+ 2488,
+ 37032,
+ false,
+ 3,
+ ),
+ IconDirectoryEntry(
+ 32,
+ 32,
+ 0,
+ 32,
+ 4392,
+ 32640,
+ false,
+ 2,
+ ),
+ IconDirectoryEntry(
+ 48,
+ 48,
+ 0,
+ 32,
+ 9832,
+ 22808,
+ false,
+ 1,
+ ),
+ IconDirectoryEntry(
+ 256,
+ 256,
+ 0,
+ 32,
+ 22722,
+ 86,
+ true,
+ 0,
+ ),
+ )
+
+ entries
+ .sortedBy { entry -> entry.width }
+ .forEachIndexed { index, entry ->
+ assertEquals(expectedEntries[index], entry)
+ }
+ }
+
+ @Test
+ fun testNvidiaIconEntries() {
+ val icon = loadIcon("nvidia_favicon.ico")
+
+ val entries = decodeDirectoryEntries(icon, 1024)
+ assertEquals(3, entries.size)
+
+ // Verify the best entry is correctly chosen for each width. We expect 32 bpp in all cases.
+ val expectedEntries = arrayOf(
+ IconDirectoryEntry(
+ 16,
+ 16,
+ 0,
+ 32,
+ 1128,
+ 24086,
+ false,
+ 8,
+ ),
+ IconDirectoryEntry(
+ 32,
+ 32,
+ 0,
+ 32,
+ 4264,
+ 19822,
+ false,
+ 7,
+ ),
+ IconDirectoryEntry(
+ 48,
+ 48,
+ 0,
+ 32,
+ 9640,
+ 10182,
+ false,
+ 6,
+ ),
+ )
+
+ entries
+ .sortedBy { entry -> entry.width }
+ .forEachIndexed { index, entry -> assertEquals(expectedEntries[index], entry) }
+ }
+
+ @Test
+ fun testMicrosoftIconEntries() {
+ val icon = loadIcon("microsoft_favicon.ico")
+
+ val entries = decodeDirectoryEntries(icon, 1024)
+ assertEquals(6, entries.size)
+
+ val expectedEntries = arrayOf(
+ IconDirectoryEntry(
+ 128,
+ 128,
+ 16,
+ 0,
+ 10344,
+ 102,
+ false,
+ 0,
+ ),
+ IconDirectoryEntry(
+ 72,
+ 72,
+ 16,
+ 0,
+ 3560,
+ 10446,
+ false,
+ 1,
+ ),
+ IconDirectoryEntry(
+ 48,
+ 48,
+ 16,
+ 0,
+ 1640,
+ 14006,
+ false,
+ 2,
+ ),
+ IconDirectoryEntry(
+ 32,
+ 32,
+ 16,
+ 0,
+ 744,
+ 15646,
+ false,
+ 3,
+ ),
+ IconDirectoryEntry(
+ 24,
+ 24,
+ 16,
+ 0,
+ 488,
+ 16390,
+ false,
+ 4,
+ ),
+ IconDirectoryEntry(
+ 16,
+ 16,
+ 16,
+ 0,
+ 296,
+ 16878,
+ false,
+ 5,
+ ),
+ )
+
+ entries
+ .sortedByDescending { entry -> entry.width }
+ .forEachIndexed { index, entry -> assertEquals(expectedEntries[index], entry) }
+ }
+
+ private fun loadIcon(fileName: String): ByteArray =
+ javaClass.getResourceAsStream("/ico/$fileName")!!
+ .buffered()
+ .readBytes()
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/SvgIconDecoderTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/SvgIconDecoderTest.kt
new file mode 100644
index 0000000000..0be22538ee
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/SvgIconDecoderTest.kt
@@ -0,0 +1,156 @@
+/* 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.browser.icons.decoder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.caverock.androidsvg.SVGParseException
+import mozilla.components.support.images.DesiredSize
+import mozilla.components.support.test.any
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.spy
+
+@RunWith(AndroidJUnit4::class)
+class SvgIconDecoderTest {
+ private val desiredSize = DesiredSize(
+ targetSize = 32,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ )
+
+ private val validSvg: ByteArray = """<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"></svg>""".toByteArray()
+
+ // SVGParseException for unbalancedClose
+ private val invalidSvg: ByteArray = """<svg xmlns="http://www.w3.org/2000/svg"></svg></svg>""".toByteArray()
+
+ // IllegalArgumentException: SVG document is empty
+ private val illegalArgumentSvg: ByteArray = """<html lang="en"></html>""".toByteArray()
+
+ // NullPointerException: Attempt to read from field on a null object reference in method
+ private val nullPointerSvg: ByteArray = """<!DOCTYPE html><html lang="en"><style>a{background-image:url('data:image/svg+xml,<svg width="24" height="25" viewBox="0 0 24 25" xmlns="http://www.w3.org/2000/svg"><path fill="black" fill-rule="evenodd"/></svg>')}</style></html>""".toByteArray()
+
+ // -------------------------------------------------------------------------------------
+ // Tests for SvgIconDecoder.decode
+ // -------------------------------------------------------------------------------------
+ @Test
+ fun `WHEN SVG is valid THEN decode returns non-null bitmap`() {
+ val decoder = SvgIconDecoder()
+
+ val bitmap = decoder.decode(
+ validSvg,
+ desiredSize,
+ )
+
+ assertNotNull(bitmap!!)
+ }
+
+ @Test
+ fun `WHEN decoder throwing NullPointerException THEN returns null`() {
+ val decoder = spy(SvgIconDecoder())
+ doThrow(NullPointerException()).`when`(decoder).maybeDecode(any(), any())
+
+ val bitmap = decoder.decode(
+ ByteArray(0),
+ desiredSize,
+ )
+
+ assertNull(bitmap)
+ }
+
+ @Test
+ fun `WHEN decoder throwing IllegalArgumentException THEN returns null`() {
+ val decoder = spy(SvgIconDecoder())
+ doThrow(IllegalArgumentException()).`when`(decoder).maybeDecode(any(), any())
+
+ val bitmap = decoder.decode(
+ ByteArray(0),
+ desiredSize,
+ )
+
+ assertNull(bitmap)
+ }
+
+ @Test
+ fun `WHEN out of memory THEN returns null`() {
+ val decoder = spy(SvgIconDecoder())
+ doThrow(OutOfMemoryError()).`when`(decoder).maybeDecode(any(), any())
+
+ val bitmap = decoder.decode(
+ ByteArray(0),
+ desiredSize,
+ )
+
+ assertNull(bitmap)
+ }
+
+ // We couldn't instantiate SVGParseException since it doesn't have public constructor
+ @Test
+ fun `WHEN SVGParseException THEN returns null`() {
+ val decoder = SvgIconDecoder()
+
+ val bitmap = decoder.decode(
+ invalidSvg,
+ desiredSize,
+ )
+
+ assertNull(bitmap)
+ }
+
+ // -------------------------------------------------------------------------------------
+ // Tests for SvgIconDecoder.maybeDecode
+ // -------------------------------------------------------------------------------------
+ @Test
+ fun `WHEN SVG is valid THEN maybeDecode returns non-null bitmap`() {
+ val decoder = SvgIconDecoder()
+
+ val bitmap = decoder.maybeDecode(
+ validSvg,
+ desiredSize,
+ )
+
+ assertNotNull(bitmap)
+ }
+
+ @Test
+ fun `WHEN parsing error THEN throws SVGParseException`() {
+ val decoder = SvgIconDecoder()
+
+ assertThrows(SVGParseException::class.java) {
+ decoder.maybeDecode(
+ invalidSvg,
+ desiredSize,
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN input is malformed THEN throws NullPointerException`() {
+ val decoder = SvgIconDecoder()
+
+ assertThrows(NullPointerException::class.java) {
+ decoder.maybeDecode(
+ nullPointerSvg,
+ desiredSize,
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN input's document is empty THEN throws IllegalArgumentException`() {
+ val decoder = SvgIconDecoder()
+
+ assertThrows(IllegalArgumentException::class.java) {
+ decoder.maybeDecode(
+ illegalArgumentSvg,
+ desiredSize,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt
new file mode 100644
index 0000000000..5f9120d986
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt
@@ -0,0 +1,223 @@
+/* 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.browser.icons.extension
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.state.action.TrackingProtectionAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class IconMessageHandlerTest {
+
+ @ExperimentalCoroutinesApi
+ @OptIn(DelicateCoroutinesApi::class)
+ @Test
+ fun `Complex message (TheVerge) is transformed into IconRequest and loaded`() {
+ runTest {
+ val bitmap: Bitmap = mock()
+ val icon = Icon(bitmap, source = Icon.Source.DOWNLOAD)
+ val deferredIcon = GlobalScope.async { icon }
+
+ val store: BrowserStore = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(url = "https://www.theverge.com/", id = "test-url"),
+ ),
+ ),
+ )
+
+ store.state.findTab("test-url")!!.apply {
+ assertNotNull(this)
+ assertNull(content.icon)
+ }
+
+ val icons: BrowserIcons = mock()
+ doReturn(deferredIcon).`when`(icons).loadIcon(any())
+
+ val handler = IconMessageHandler(store, "test-url", false, icons)
+
+ val message = """
+ {
+ "url": "https:\/\/www.theverge.com\/",
+ "icons": [
+ {
+ "mimeType": "image\/png",
+ "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395367\/favicon-16x16.0.png",
+ "type": "icon",
+ "sizes": [
+ "16x16"
+ ]
+ },
+ {
+ "mimeType": "image\/png",
+ "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395363\/favicon-32x32.0.png",
+ "type": "icon",
+ "sizes": [
+ "32x32"
+ ]
+ },
+ {
+ "mimeType": "image\/png",
+ "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395365\/favicon-96x96.0.png",
+ "type": "icon",
+ "sizes": [
+ "96x96"
+ ]
+ },
+ {
+ "mimeType": "image\/png",
+ "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395351\/android-chrome-192x192.0.png",
+ "type": "icon",
+ "sizes": [
+ "192x192"
+ ]
+ },
+ {
+ "mimeType": "",
+ "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395361\/favicon-64x64.0.ico",
+ "type": "shortcut icon",
+ "sizes": []
+ },
+ {
+ "mimeType": "",
+ "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395359\/ios-icon.0.png",
+ "type": "apple-touch-icon",
+ "sizes": [
+ "180x180"
+ ]
+ },
+ {
+ "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/9672633\/VergeOG.0_1200x627.0.png",
+ "type": "og:image"
+ },
+ {
+ "href": "https:\/\/cdn.vox-cdn.com\/community_logos\/52803\/VER_Logomark_175x92..png",
+ "type": "twitter:image"
+ },
+ {
+ "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7396113\/221a67c8-a10f-11e6-8fae-983107008690.0.png",
+ "type": "msapplication-TileImage"
+ }
+ ]
+ }
+ """.trimIndent()
+
+ handler.onMessage(JSONObject(message), source = null)
+
+ assertNotNull(handler.lastJob)
+ handler.lastJob!!.join()
+
+ // Examine IconRequest
+ val captor = argumentCaptor<IconRequest>()
+ verify(icons).loadIcon(captor.capture())
+
+ val request = captor.value
+ assertEquals("https://www.theverge.com/", request.url)
+ assertEquals(9, request.resources.size)
+
+ with(request.resources[0]) {
+ assertEquals("image/png", mimeType)
+ assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395367/favicon-16x16.0.png", url)
+ assertEquals(IconRequest.Resource.Type.FAVICON, type)
+ assertEquals(1, sizes.size)
+ assertEquals(Size(16, 16), sizes[0])
+ }
+
+ with(request.resources[1]) {
+ assertEquals("image/png", mimeType)
+ assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395363/favicon-32x32.0.png", url)
+ assertEquals(IconRequest.Resource.Type.FAVICON, type)
+ assertEquals(1, sizes.size)
+ assertEquals(Size(32, 32), sizes[0])
+ }
+
+ with(request.resources[2]) {
+ assertEquals("image/png", mimeType)
+ assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395365/favicon-96x96.0.png", url)
+ assertEquals(IconRequest.Resource.Type.FAVICON, type)
+ assertEquals(1, sizes.size)
+ assertEquals(Size(96, 96), sizes[0])
+ }
+
+ with(request.resources[3]) {
+ assertEquals("image/png", mimeType)
+ assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395351/android-chrome-192x192.0.png", url)
+ assertEquals(IconRequest.Resource.Type.FAVICON, type)
+ assertEquals(1, sizes.size)
+ assertEquals(Size(192, 192), sizes[0])
+ }
+
+ with(request.resources[4]) {
+ assertNull(mimeType)
+ assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395361/favicon-64x64.0.ico", url)
+ assertEquals(IconRequest.Resource.Type.FAVICON, type)
+ assertEquals(0, sizes.size)
+ }
+
+ with(request.resources[5]) {
+ assertNull(mimeType)
+ assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395359/ios-icon.0.png", url)
+ assertEquals(IconRequest.Resource.Type.APPLE_TOUCH_ICON, type)
+ assertEquals(1, sizes.size)
+ assertEquals(Size(180, 180), sizes[0])
+ }
+
+ with(request.resources[6]) {
+ assertNull(mimeType)
+ assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/9672633/VergeOG.0_1200x627.0.png", url)
+ assertEquals(IconRequest.Resource.Type.OPENGRAPH, type)
+ assertEquals(0, sizes.size)
+ }
+
+ with(request.resources[7]) {
+ assertNull(mimeType)
+ assertEquals("https://cdn.vox-cdn.com/community_logos/52803/VER_Logomark_175x92..png", url)
+ assertEquals(IconRequest.Resource.Type.TWITTER, type)
+ assertEquals(0, sizes.size)
+ }
+
+ with(request.resources[8]) {
+ assertNull(mimeType)
+ assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7396113/221a67c8-a10f-11e6-8fae-983107008690.0.png", url)
+ assertEquals(IconRequest.Resource.Type.MICROSOFT_TILE, type)
+ assertEquals(0, sizes.size)
+ }
+
+ store.dispatch(TrackingProtectionAction.ClearTrackersAction("test-url"))
+ .join()
+
+ // Loaded icon will be set on session
+
+ store.state.findTab("test-url")!!.apply {
+ assertNotNull(this)
+ assertNotNull(content.icon)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageKtTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageKtTest.kt
new file mode 100644
index 0000000000..5781463183
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageKtTest.kt
@@ -0,0 +1,62 @@
+/* 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.browser.icons.extension
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.engine.manifest.Size
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class IconMessageKtTest {
+
+ @Test
+ fun `Serializing and deserializing icon resources`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/icon64.png",
+ sizes = listOf(Size(64, 64)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/icon128.png",
+ sizes = listOf(Size(128, 128)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/icon128.png",
+ sizes = listOf(Size(180, 180)),
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ val json = resources.toJSON()
+ assertEquals(3, json.length())
+
+ val restoredResources = json.toIconResources()
+ assertEquals(resources, restoredResources)
+ }
+
+ @Test
+ fun `Url must be sanitized`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "\nhttps://www.mozilla.org/icon64.png\n",
+ sizes = listOf(Size(64, 64)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ )
+
+ val json = resources.toJSON()
+
+ val restoredResource = json.toIconResources().first()
+ assertEquals("https://www.mozilla.org/icon64.png", restoredResource.url)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt
new file mode 100644
index 0000000000..16d7ff3564
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt
@@ -0,0 +1,66 @@
+/* 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.browser.icons.generator
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DefaultIconGeneratorTest {
+
+ @Test
+ fun pickColor() {
+ val generator = DefaultIconGenerator()
+ val res = testContext.resources
+
+ val color = generator.pickColor(res, "http://m.facebook.com")
+
+ // Color does not change
+ for (i in 0..99) {
+ assertEquals(color, generator.pickColor(res, "http://m.facebook.com"))
+ }
+
+ // Color is stable for "similar" hosts.
+ assertEquals(color, generator.pickColor(res, "https://m.facebook.com"))
+ assertEquals(color, generator.pickColor(res, "http://facebook.com"))
+ assertEquals(color, generator.pickColor(res, "http://www.facebook.com"))
+ assertEquals(color, generator.pickColor(res, "http://www.facebook.com/foo/bar/foobar?mobile=1"))
+
+ // Returns a color for an empty string
+ assertNotEquals(0, generator.pickColor(res, ""))
+ }
+
+ @Test
+ fun generate() {
+ val generator = DefaultIconGenerator()
+
+ val icon = generator.generate(
+ testContext,
+ IconRequest(
+ url = "https://m.facebook.com",
+ ),
+ )
+
+ assertNotNull(icon.bitmap)
+ assertNotNull(icon.color)
+
+ val dp32 = 32.dpToPx(testContext.resources.displayMetrics)
+ assertEquals(dp32, icon.bitmap.width)
+ assertEquals(dp32, icon.bitmap.height)
+
+ assertEquals(Bitmap.Config.ARGB_8888, icon.bitmap.config)
+
+ assertEquals(Icon.Source.GENERATOR, icon.source)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DataUriIconLoaderTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DataUriIconLoaderTest.kt
new file mode 100644
index 0000000000..7fe5ec3e0a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DataUriIconLoaderTest.kt
@@ -0,0 +1,93 @@
+/* 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.browser.icons.loader
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DataUriIconLoaderTest {
+
+ @Test
+ fun `Loader returns null for http(s) urls`() {
+ val loader = DataUriIconLoader()
+
+ assertEquals(
+ IconLoader.Result.NoResult,
+ loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "https://www.mozilla.org",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ ),
+ )
+
+ assertEquals(
+ IconLoader.Result.NoResult,
+ loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "http://example.org",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Loader returns bytes for data uri containing png`() {
+ val loader = DataUriIconLoader()
+
+ val result = loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91Jpz" +
+ "AAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ )
+
+ assertTrue(result is IconLoader.Result.BytesResult)
+
+ val data = (result as IconLoader.Result.BytesResult).bytes
+ assertEquals(Icon.Source.INLINE, result.source)
+
+ assertNotNull(data)
+ assertEquals(75, data.size)
+ }
+
+ @Test
+ fun `Loader returns base64 decoded data`() {
+ val loader = DataUriIconLoader()
+
+ val result = loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "data:image/png;base64,dGhpcyBpcyBhIHRlc3Q=",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ )
+
+ assertTrue(result is IconLoader.Result.BytesResult)
+
+ val data = (result as IconLoader.Result.BytesResult).bytes
+ assertEquals(Icon.Source.INLINE, result.source)
+
+ val text = String(data, Charsets.UTF_8)
+ assertEquals("this is a test", text)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DiskIconLoaderTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DiskIconLoaderTest.kt
new file mode 100644
index 0000000000..b3c7c5f028
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DiskIconLoaderTest.kt
@@ -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/. */
+
+package mozilla.components.browser.icons.loader
+
+import android.content.Context
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class DiskIconLoaderTest {
+ @Test
+ fun `DiskIconLoader returns bitmap from cache`() {
+ val cache = object : DiskIconLoader.LoaderDiskCache {
+ override fun getIconData(context: Context, resource: IconRequest.Resource): ByteArray? {
+ return "Hello World".toByteArray()
+ }
+ }
+
+ val loader = DiskIconLoader(cache)
+
+ val request = IconRequest("https://www.mozilla.org")
+ val resource = IconRequest.Resource(
+ url = "https://www.mozilla.org/favicon.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ )
+
+ val result = loader.load(mock(), request, resource)
+
+ assertTrue(result is IconLoader.Result.BytesResult)
+
+ val bytesResult = result as IconLoader.Result.BytesResult
+
+ assertEquals("Hello World", String(bytesResult.bytes))
+ }
+
+ @Test
+ fun `DiskIconLoader returns NoResult if cache does not contain entry`() {
+ val cache = object : DiskIconLoader.LoaderDiskCache {
+ override fun getIconData(context: Context, resource: IconRequest.Resource): ByteArray? {
+ return null
+ }
+ }
+
+ val loader = DiskIconLoader(cache)
+
+ val request = IconRequest("https://www.mozilla.org")
+ val resource = IconRequest.Resource(
+ url = "https://www.mozilla.org/favicon.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ )
+
+ val result = loader.load(mock(), request, resource)
+
+ assertTrue(result is IconLoader.Result.NoResult)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/FailureCacheTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/FailureCacheTest.kt
new file mode 100644
index 0000000000..9240e4514f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/FailureCacheTest.kt
@@ -0,0 +1,51 @@
+/* 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.browser.icons.loader
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+
+@RunWith(AndroidJUnit4::class)
+class FailureCacheTest {
+
+ @Test
+ fun `Cache should remember URLs for limited amount of time`() {
+ val cache = spy(FailureCache())
+
+ cache.withFixedTime(0L) {
+ assertFalse(hasFailedRecently("https://www.mozilla.org"))
+ assertFalse(hasFailedRecently("https://www.firefox.com"))
+ }
+
+ cache.withFixedTime(50L) {
+ rememberFailure("https://www.mozilla.org")
+
+ assertTrue(hasFailedRecently("https://www.mozilla.org"))
+ assertFalse(hasFailedRecently("https://www.firefox.com"))
+ }
+
+ // 15 Minutes later
+ cache.withFixedTime(50L + 1000L * 60L * 15L) {
+ assertTrue(hasFailedRecently("https://www.mozilla.org"))
+ assertFalse(hasFailedRecently("https://www.firefox.com"))
+ }
+
+ // 40 Minutes later
+ cache.withFixedTime(50L + 1000L * 60L * 40L) {
+ assertFalse(hasFailedRecently("https://www.mozilla.org"))
+ assertFalse(hasFailedRecently("https://www.firefox.com"))
+ }
+ }
+}
+
+private fun FailureCache.withFixedTime(now: Long, block: FailureCache.() -> Unit) {
+ doReturn(now).`when`(this).now()
+ block()
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/HttpIconLoaderTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/HttpIconLoaderTest.kt
new file mode 100644
index 0000000000..3670066921
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/HttpIconLoaderTest.kt
@@ -0,0 +1,273 @@
+/* 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.browser.icons.loader
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.lib.fetch.okhttp.OkHttpClient
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.io.IOException
+import java.io.InputStream
+
+@RunWith(AndroidJUnit4::class)
+class HttpIconLoaderTest {
+
+ @Test
+ fun `Loader downloads data and uses appropriate headers`() {
+ val clients = listOf(
+ HttpURLConnectionClient(),
+ OkHttpClient(),
+ )
+
+ clients.forEach { client ->
+ val server = MockWebServer()
+
+ server.enqueue(
+ MockResponse().setBody(
+ javaClass.getResourceAsStream("/misc/test.txt")!!
+ .bufferedReader()
+ .use { it.readText() },
+ ),
+ )
+
+ server.start()
+
+ try {
+ val loader = HttpIconLoader(client)
+ val result = loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = server.url("/some/path").toString(),
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ assertTrue(result is IconLoader.Result.BytesResult)
+
+ val data = (result as IconLoader.Result.BytesResult).bytes
+
+ assertTrue(data.isNotEmpty())
+
+ val text = String(data, Charsets.UTF_8)
+
+ assertEquals("Hello World!", text)
+
+ val request = server.takeRequest()
+
+ assertEquals("GET", request.method)
+
+ val headers = request.headers
+ for (i in 0 until headers.size) {
+ println(headers.name(i) + ": " + headers.value(i))
+ }
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Loader will not perform any requests for data uris`() {
+ val client: Client = mock()
+
+ val result = HttpIconLoader(client).load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAA" +
+ "AAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ )
+
+ assertEquals(IconLoader.Result.NoResult, result)
+ verify(client, never()).fetch(any())
+ }
+
+ @Test
+ fun `Request has timeouts applied`() {
+ val client: Client = mock()
+
+ val loader = HttpIconLoader(client)
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 404,
+ body = Response.Body.empty(),
+ ),
+ ).`when`(client).fetch(any())
+
+ loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ val captor = argumentCaptor<Request>()
+ verify(client).fetch(captor.capture())
+
+ val request = captor.value
+ assertNotNull(request)
+ assertNotNull(request.connectTimeout)
+ assertNotNull(request.readTimeout)
+ }
+
+ @Test
+ fun `NoResult is returned for non-successful requests`() {
+ val client: Client = mock()
+
+ val loader = HttpIconLoader(client)
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 404,
+ body = Response.Body.empty(),
+ ),
+ ).`when`(client).fetch(any())
+
+ val result = loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ assertEquals(IconLoader.Result.NoResult, result)
+ }
+
+ @Test
+ fun `Loader will not try to load URL again that just recently failed`() {
+ val client: Client = mock()
+
+ val loader = HttpIconLoader(client)
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 404,
+ body = Response.Body.empty(),
+ ),
+ ).`when`(client).fetch(any())
+
+ val resource = IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ )
+
+ assertEquals(IconLoader.Result.NoResult, loader.load(mock(), mock(), resource))
+
+ // First load tries to fetch, but load fails (404)
+ verify(client).fetch(any())
+ verifyNoMoreInteractions(client)
+ reset(client)
+
+ assertEquals(IconLoader.Result.NoResult, loader.load(mock(), mock(), resource))
+
+ // Second load does not try to fetch again.
+ verify(client, never()).fetch(any())
+ }
+
+ @Test
+ fun `Loader will return NoResult for IOExceptions happening during fetch`() {
+ val client: Client = mock()
+ doThrow(IOException("Mock")).`when`(client).fetch(any())
+
+ val loader = HttpIconLoader(client)
+
+ val resource = IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ )
+
+ assertEquals(IconLoader.Result.NoResult, loader.load(testContext, mock(), resource))
+ }
+
+ @Test
+ fun `Loader will return NoResult for IOExceptions happening during toIconLoaderResult`() {
+ val client: Client = mock()
+
+ val failingStream: InputStream = object : InputStream() {
+ override fun read(): Int {
+ throw IOException("Kaboom")
+ }
+ }
+
+ val loader = HttpIconLoader(client)
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 200,
+ body = Response.Body(failingStream),
+ ),
+ ).`when`(client).fetch(any())
+
+ val resource = IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ )
+
+ assertEquals(IconLoader.Result.NoResult, loader.load(mock(), mock(), resource))
+ }
+
+ @Test
+ fun `Loader will sanitize URL`() {
+ val client: Client = mock()
+
+ val loader = HttpIconLoader(client)
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 404,
+ body = Response.Body.empty(),
+ ),
+ ).`when`(client).fetch(any())
+
+ loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = " \n\n https://www.example.org \n\n ",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ val captor = argumentCaptor<Request>()
+ verify(client).fetch(captor.capture())
+
+ val request = captor.value
+ assertEquals("https://www.example.org", request.url)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/MemoryIconLoaderTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/MemoryIconLoaderTest.kt
new file mode 100644
index 0000000000..74854818a6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/MemoryIconLoaderTest.kt
@@ -0,0 +1,62 @@
+/* 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.browser.icons.loader
+
+import android.graphics.Bitmap
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class MemoryIconLoaderTest {
+ @Test
+ fun `MemoryIconLoader returns bitmap from cache`() {
+ val bitmap: Bitmap = mock()
+
+ val cache = object : MemoryIconLoader.LoaderMemoryCache {
+ override fun getBitmap(request: IconRequest, resource: IconRequest.Resource): Bitmap? {
+ return bitmap
+ }
+ }
+
+ val loader = MemoryIconLoader(cache)
+
+ val request = IconRequest("https://www.mozilla.org")
+ val resource = IconRequest.Resource(
+ url = "https://www.mozilla.org/favicon.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ )
+
+ val result = loader.load(mock(), request, resource)
+
+ assertTrue(result is IconLoader.Result.BitmapResult)
+
+ val bitmapResult = result as IconLoader.Result.BitmapResult
+
+ assertEquals(bitmap, bitmapResult.bitmap)
+ }
+
+ @Test
+ fun `MemoryIconLoader returns NoResult if cache does not contain entry`() {
+ val cache = object : MemoryIconLoader.LoaderMemoryCache {
+ override fun getBitmap(request: IconRequest, resource: IconRequest.Resource): Bitmap? {
+ return null
+ }
+ }
+
+ val loader = MemoryIconLoader(cache)
+
+ val request = IconRequest("https://www.mozilla.org")
+ val resource = IconRequest.Resource(
+ url = "https://www.mozilla.org/favicon.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ )
+
+ val result = loader.load(mock(), request, resource)
+
+ assertTrue(result is IconLoader.Result.NoResult)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.kt
new file mode 100644
index 0000000000..6b2fe8d8ad
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.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.browser.icons.loader
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.lib.fetch.okhttp.OkHttpClient
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.io.IOException
+import java.io.InputStream
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class NonBlockingHttpIconLoaderTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `Loader will return IconLoader#Result#NoResult for a load request and respond with the result through a callback`() = runTestOnMain {
+ val clients = listOf(
+ HttpURLConnectionClient(),
+ OkHttpClient(),
+ )
+
+ clients.forEach { client ->
+
+ val server = MockWebServer()
+
+ server.enqueue(
+ MockResponse().setBody(
+ javaClass.getResourceAsStream("/misc/test.txt")!!
+ .bufferedReader()
+ .use { it.readText() },
+ ),
+ )
+
+ server.start()
+
+ try {
+ var callbackIconRequest: IconRequest? = null
+ var callbackResource: IconRequest.Resource? = null
+ var callbackIcon: IconLoader.Result? = null
+ val loader = NonBlockingHttpIconLoader(client, scope) { request, resource, icon ->
+ callbackIconRequest = request
+ callbackResource = resource
+ callbackIcon = icon
+ }
+ val iconRequest: IconRequest = mock()
+
+ val result = loader.load(
+ mock(),
+ iconRequest,
+ IconRequest.Resource(
+ url = server.url("/some/path").toString(),
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ assertTrue(result is IconLoader.Result.NoResult)
+ val downloadedResource = String(((callbackIcon as IconLoader.Result.BytesResult).bytes), Charsets.UTF_8)
+ assertEquals("Hello World!", downloadedResource)
+ assertSame(Icon.Source.DOWNLOAD, ((callbackIcon as IconLoader.Result.BytesResult).source))
+ assertTrue(callbackResource!!.url.endsWith("/some/path"))
+ assertSame(IconRequest.Resource.Type.APPLE_TOUCH_ICON, callbackResource?.type)
+ assertSame(iconRequest, callbackIconRequest)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Loader will not perform any requests for data uris`() = runTestOnMain {
+ val client: Client = mock()
+ var callbackIconRequest: IconRequest? = null
+ var callbackResource: IconRequest.Resource? = null
+ var callbackIcon: IconLoader.Result? = null
+ val loader = NonBlockingHttpIconLoader(client, scope) { request, resource, icon ->
+ callbackIconRequest = request
+ callbackResource = resource
+ callbackIcon = icon
+ }
+
+ val result = loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAA" +
+ "AAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ )
+
+ assertEquals(IconLoader.Result.NoResult, result)
+ assertNull(callbackIconRequest)
+ assertNull(callbackResource)
+ assertNull(callbackIcon)
+ verify(client, never()).fetch(any())
+ }
+
+ @Test
+ fun `Request has timeouts applied`() = runTestOnMain {
+ val client: Client = mock()
+ val loader = NonBlockingHttpIconLoader(client, scope) { _, _, _ -> }
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 404,
+ body = Response.Body.empty(),
+ ),
+ ).`when`(client).fetch(any())
+
+ loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ val captor = argumentCaptor<Request>()
+ verify(client).fetch(captor.capture())
+ val request = captor.value
+ assertNotNull(request)
+ assertNotNull(request.connectTimeout)
+ assertNotNull(request.readTimeout)
+ }
+
+ @Test
+ fun `NoResult is returned for non-successful requests`() = runTestOnMain {
+ val client: Client = mock()
+ var callbackIconRequest: IconRequest? = null
+ var callbackResource: IconRequest.Resource? = null
+ var callbackIcon: IconLoader.Result? = null
+ val loader = NonBlockingHttpIconLoader(client, scope) { request, resource, icon ->
+ callbackIconRequest = request
+ callbackResource = resource
+ callbackIcon = icon
+ }
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 404,
+ body = Response.Body.empty(),
+ ),
+ ).`when`(client).fetch(any())
+
+ val result = loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ assertEquals(IconLoader.Result.NoResult, result)
+ assertEquals(IconLoader.Result.NoResult, callbackIcon)
+ assertNotNull(callbackIconRequest)
+ assertEquals("https://www.example.org", callbackResource!!.url)
+ assertSame(IconRequest.Resource.Type.APPLE_TOUCH_ICON, callbackResource?.type)
+ }
+
+ @Test
+ fun `Loader will not try to load URL again that just recently failed`() = runTestOnMain {
+ val client: Client = mock()
+ val loader = NonBlockingHttpIconLoader(client, scope) { _, _, _ -> }
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 404,
+ body = Response.Body.empty(),
+ ),
+ ).`when`(client).fetch(any())
+ val resource = IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ )
+
+ val result = loader.load(mock(), mock(), resource)
+
+ assertEquals(IconLoader.Result.NoResult, result)
+ // First load tries to fetch, but load fails (404)
+ verify(client).fetch(any())
+ verifyNoMoreInteractions(client)
+ reset(client)
+ assertEquals(IconLoader.Result.NoResult, loader.load(mock(), mock(), resource))
+ // Second load does not try to fetch again.
+ verify(client, never()).fetch(any())
+ }
+
+ @Test
+ fun `Loader will return NoResult for IOExceptions happening during fetch`() = runTestOnMain {
+ val client: Client = mock()
+ doThrow(IOException("Mock")).`when`(client).fetch(any())
+ var callbackIconRequest: IconRequest? = null
+ var callbackResource: IconRequest.Resource? = null
+ var callbackIcon: IconLoader.Result? = null
+ val loader = NonBlockingHttpIconLoader(client, scope) { request, resource, icon ->
+ callbackIconRequest = request
+ callbackResource = resource
+ callbackIcon = icon
+ }
+
+ val resource = IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ )
+
+ val result = loader.load(testContext, mock(), resource)
+ assertEquals(IconLoader.Result.NoResult, result)
+ assertEquals(IconLoader.Result.NoResult, callbackIcon)
+ assertNotNull(callbackIconRequest)
+ assertEquals("https://www.example.org", callbackResource!!.url)
+ assertSame(IconRequest.Resource.Type.APPLE_TOUCH_ICON, callbackResource?.type)
+ }
+
+ @Test
+ fun `Loader will return NoResult for IOExceptions happening during toIconLoaderResult`() = runTestOnMain {
+ val client: Client = mock()
+ var callbackIconRequest: IconRequest? = null
+ var callbackResource: IconRequest.Resource? = null
+ var callbackIcon: IconLoader.Result? = null
+ val loader = NonBlockingHttpIconLoader(client, scope) { request, resource, icon ->
+ callbackIconRequest = request
+ callbackResource = resource
+ callbackIcon = icon
+ }
+ val failingStream: InputStream = object : InputStream() {
+ override fun read(): Int {
+ throw IOException("Kaboom")
+ }
+ }
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 200,
+ body = Response.Body(failingStream),
+ ),
+ ).`when`(client).fetch(any())
+ val resource = IconRequest.Resource(
+ url = "https://www.example.org",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ )
+
+ val result = loader.load(testContext, mock(), resource)
+
+ assertEquals(IconLoader.Result.NoResult, result)
+ assertEquals(IconLoader.Result.NoResult, callbackIcon)
+ assertNotNull(callbackIconRequest)
+ assertEquals("https://www.example.org", callbackResource!!.url)
+ assertSame(IconRequest.Resource.Type.APPLE_TOUCH_ICON, callbackResource?.type)
+ }
+
+ @Test
+ fun `Loader will sanitize URL`() = runTestOnMain {
+ val client: Client = mock()
+ val captor = argumentCaptor<Request>()
+ val loader = NonBlockingHttpIconLoader(client, scope) { _, _, _ -> }
+ doReturn(
+ Response(
+ url = "https://www.example.org",
+ headers = MutableHeaders(),
+ status = 404,
+ body = Response.Body.empty(),
+ ),
+ ).`when`(client).fetch(any())
+
+ loader.load(
+ mock(),
+ mock(),
+ IconRequest.Resource(
+ url = " \n\n https://www.example.org \n\n ",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ verify(client).fetch(captor.capture())
+ val request = captor.value
+ assertEquals("https://www.example.org", request.url)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/pipeline/IconResourceComparatorTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/pipeline/IconResourceComparatorTest.kt
new file mode 100644
index 0000000000..d3fd7cd222
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/pipeline/IconResourceComparatorTest.kt
@@ -0,0 +1,354 @@
+/* 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.browser.icons.pipeline
+
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.engine.manifest.Size
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class IconResourceComparatorTest {
+ @Test
+ fun `compare mozilla-org icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/media/img/favicon/favicon-196x196.c80e6abe0767.png",
+ type = IconRequest.Resource.Type.FAVICON,
+ sizes = listOf(Size(196, 196)),
+ ),
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/media/img/favicon.d4f1f46b91f4.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.8772ec154918.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(180, 180)),
+ ),
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/media/img/mozorg/mozilla-256.4720741d4108.jpg",
+ type = IconRequest.Resource.Type.OPENGRAPH,
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.8772ec154918.png",
+ "https://www.mozilla.org/media/img/favicon/favicon-196x196.c80e6abe0767.png",
+ "https://www.mozilla.org/media/img/favicon.d4f1f46b91f4.ico",
+ "https://www.mozilla.org/media/img/mozorg/mozilla-256.4720741d4108.jpg",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare m-youtube-com icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://s.ytimg.com/yts/img/favicon-vfl8qSV2F.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ mimeType = "image/x-icon",
+ ),
+ IconRequest.Resource(
+ url = "https://s.ytimg.com/yts/img/favicon-vfl8qSV2F.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ mimeType = "image/x-icon",
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://s.ytimg.com/yts/img/favicon-vfl8qSV2F.ico",
+ "https://s.ytimg.com/yts/img/favicon-vfl8qSV2F.ico",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare m-facebook-com icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://static.xx.fbcdn.net/rsrc.php/v3/ya/r/O2aKM2iSbOw.png",
+ type = IconRequest.Resource.Type.FAVICON,
+ sizes = listOf(Size(196, 196)),
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://static.xx.fbcdn.net/rsrc.php/v3/ya/r/O2aKM2iSbOw.png",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare baidu-com icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "http://sm.bdimg.com/static/wiseindex/img/favicon64.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "http://sm.bdimg.com/static/wiseindex/img/screen_icon_new.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "http://sm.bdimg.com/static/wiseindex/img/screen_icon_new.png",
+ "http://sm.bdimg.com/static/wiseindex/img/favicon64.ico",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare wikipedia-org icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://www.wikipedia.org/static/favicon/wikipedia.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://www.wikipedia.org/static/apple-touch/wikipedia.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://www.wikipedia.org/static/apple-touch/wikipedia.png",
+ "https://www.wikipedia.org/static/favicon/wikipedia.ico",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare amazon-com icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_57x57._CB368212015_.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(57, 57)),
+ ),
+ IconRequest.Resource(
+ url = "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_72x72._CB368212002_.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(72, 72)),
+ ),
+ IconRequest.Resource(
+ url = "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_114x114._CB368212020_.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(114, 114)),
+ ),
+ IconRequest.Resource(
+ url = "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_120x120._CB368246573_.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(120, 120)),
+ ),
+ IconRequest.Resource(
+ url = "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_144x144._CB368211973_.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(144, 144)),
+ ),
+ IconRequest.Resource(
+ url = "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_152x152._CB368246573_.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(152, 152)),
+ ),
+ IconRequest.Resource(
+ url = "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_196x196._CB368246573_.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(196, 196)),
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_196x196._CB368246573_.png",
+ "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_152x152._CB368246573_.png",
+ "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_144x144._CB368211973_.png",
+ "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_120x120._CB368246573_.png",
+ "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_114x114._CB368212020_.png",
+ "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_72x72._CB368212002_.png",
+ "https://images-na.ssl-images-amazon.com/images/G/01/anywhere/a_smile_57x57._CB368212015_.png",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare twitter-com icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://abs.twimg.com/favicons/favicon.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://abs.twimg.com/responsive-web/web/icon-ios.8ea219d08eafdfa41.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://abs.twimg.com/responsive-web/web/icon-ios.8ea219d08eafdfa41.png",
+ "https://abs.twimg.com/favicons/favicon.ico",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare github-com icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://github.githubassets.com/favicon.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://github.com/fluidicon.png",
+ type = IconRequest.Resource.Type.FLUID_ICON,
+ ),
+ IconRequest.Resource(
+ url = "https://github.githubassets.com/images/modules/open_graph/github-logo.png",
+ type = IconRequest.Resource.Type.OPENGRAPH,
+ ),
+ IconRequest.Resource(
+ url = "https://github.githubassets.com/images/modules/open_graph/github-mark.png",
+ type = IconRequest.Resource.Type.OPENGRAPH,
+ ),
+ IconRequest.Resource(
+ url = "https://github.githubassets.com/images/modules/open_graph/github-octocat.png",
+ type = IconRequest.Resource.Type.OPENGRAPH,
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://github.githubassets.com/favicon.ico",
+ "https://github.com/fluidicon.png",
+ "https://github.githubassets.com/images/modules/open_graph/github-logo.png",
+ "https://github.githubassets.com/images/modules/open_graph/github-mark.png",
+ "https://github.githubassets.com/images/modules/open_graph/github-octocat.png",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare theverge-com icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395367/favicon-16x16.0.png",
+ type = IconRequest.Resource.Type.FAVICON,
+ sizes = listOf(Size(16, 16)),
+ ),
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395363/favicon-32x32.0.png",
+ type = IconRequest.Resource.Type.FAVICON,
+ sizes = listOf(Size(32, 32)),
+ ),
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395365/favicon-96x96.0.png",
+ type = IconRequest.Resource.Type.FAVICON,
+ sizes = listOf(Size(96, 96)),
+ ),
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395351/android-chrome-192x192.0.png",
+ type = IconRequest.Resource.Type.FAVICON,
+ sizes = listOf(Size(192, 192)),
+ ),
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395361/favicon-64x64.0.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395359/ios-icon.0.png",
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ sizes = listOf(Size(180, 180)),
+ ),
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/uploads/chorus_asset/file/9672633/VergeOG.0_1200x627.0.png",
+ type = IconRequest.Resource.Type.OPENGRAPH,
+ ),
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/community_logos/52803/VER_Logomark_175x92..png",
+ type = IconRequest.Resource.Type.TWITTER,
+ ),
+ IconRequest.Resource(
+ url = "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7396113/221a67c8-a10f-11e6-8fae-983107008690.0.png",
+ type = IconRequest.Resource.Type.MICROSOFT_TILE,
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395359/ios-icon.0.png",
+ "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395351/android-chrome-192x192.0.png",
+ "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395365/favicon-96x96.0.png",
+ "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395363/favicon-32x32.0.png",
+ "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395367/favicon-16x16.0.png",
+ "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395361/favicon-64x64.0.ico",
+ "https://cdn.vox-cdn.com/uploads/chorus_asset/file/9672633/VergeOG.0_1200x627.0.png",
+ "https://cdn.vox-cdn.com/community_logos/52803/VER_Logomark_175x92..png",
+ "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7396113/221a67c8-a10f-11e6-8fae-983107008690.0.png",
+ ),
+ urls,
+ )
+ }
+
+ @Test
+ fun `compare proxx-app icons`() {
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://proxx.app/assets/icon-05a70868.png",
+ type = IconRequest.Resource.Type.MANIFEST_ICON,
+ sizes = listOf(Size(1024, 1024)),
+ mimeType = "image/png",
+ ),
+ IconRequest.Resource(
+ url = "https://proxx.app/assets/icon-maskable-7a2eb399.png",
+ type = IconRequest.Resource.Type.MANIFEST_ICON,
+ sizes = listOf(Size(1024, 1024)),
+ mimeType = "image/png",
+ maskable = true,
+ ),
+ )
+
+ val urls = resources.sortedWith(IconResourceComparator).map { it.url }
+
+ assertEquals(
+ listOf(
+ "https://proxx.app/assets/icon-maskable-7a2eb399.png",
+ "https://proxx.app/assets/icon-05a70868.png",
+ ),
+ urls,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/DiskIconPreparerTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/DiskIconPreparerTest.kt
new file mode 100644
index 0000000000..9cc92cfbf7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/DiskIconPreparerTest.kt
@@ -0,0 +1,69 @@
+/* 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.browser.icons.preparer
+
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+
+class DiskIconPreparerTest {
+ @Test
+ fun `Preparer will add resources from cache`() {
+ val resources = listOf(
+ IconRequest.Resource("https://www.mozilla.org", type = IconRequest.Resource.Type.FAVICON),
+ IconRequest.Resource("https://www.firefox.com", type = IconRequest.Resource.Type.APPLE_TOUCH_ICON),
+ )
+
+ val cache: DiskIconPreparer.PreparerDiskCache = mock()
+ Mockito.doReturn(resources).`when`(cache).getResources(any(), any())
+
+ val preparer = DiskIconPreparer(cache)
+
+ val initialRequest = IconRequest(url = "example.org")
+
+ val request = preparer.prepare(mock(), initialRequest)
+
+ assertEquals(2, request.resources.size)
+ assertEquals(
+ listOf(
+ "https://www.mozilla.org",
+ "https://www.firefox.com",
+ ),
+ request.resources.map { it.url },
+ )
+ }
+
+ @Test
+ fun `Preparer will not add resources if request already has resources`() {
+ val resources = listOf(
+ IconRequest.Resource("https://www.mozilla.org", type = IconRequest.Resource.Type.FAVICON),
+ IconRequest.Resource("https://www.firefox.com", type = IconRequest.Resource.Type.APPLE_TOUCH_ICON),
+ )
+
+ val cache: DiskIconPreparer.PreparerDiskCache = mock()
+ Mockito.doReturn(resources).`when`(cache).getResources(any(), any())
+
+ val preparer = DiskIconPreparer(cache)
+
+ val initialRequest = IconRequest(
+ url = "https://www.example.org",
+ resources = listOf(
+ IconRequest.Resource("https://getpocket.com", type = IconRequest.Resource.Type.FAVICON),
+ ),
+ )
+
+ val request = preparer.prepare(mock(), initialRequest)
+
+ assertEquals(
+ listOf(
+ "https://getpocket.com",
+ ),
+ request.resources.map { it.url },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/MemoryIconPreparerTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/MemoryIconPreparerTest.kt
new file mode 100644
index 0000000000..d563d8d968
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/MemoryIconPreparerTest.kt
@@ -0,0 +1,69 @@
+/* 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.browser.icons.preparer
+
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+
+class MemoryIconPreparerTest {
+ @Test
+ fun `Preparer will add resources from cache`() {
+ val resources = listOf(
+ IconRequest.Resource("https://www.mozilla.org", type = IconRequest.Resource.Type.FAVICON),
+ IconRequest.Resource("https://www.firefox.com", type = IconRequest.Resource.Type.APPLE_TOUCH_ICON),
+ )
+
+ val cache: MemoryIconPreparer.PreparerMemoryCache = mock()
+ doReturn(resources).`when`(cache).getResources(any())
+
+ val preparer = MemoryIconPreparer(cache)
+
+ val initialRequest = IconRequest(url = "example.org")
+
+ val request = preparer.prepare(mock(), initialRequest)
+
+ assertEquals(2, request.resources.size)
+ assertEquals(
+ listOf(
+ "https://www.mozilla.org",
+ "https://www.firefox.com",
+ ),
+ request.resources.map { it.url },
+ )
+ }
+
+ @Test
+ fun `Preparer will not add resources if request already has resources`() {
+ val resources = listOf(
+ IconRequest.Resource("https://www.mozilla.org", type = IconRequest.Resource.Type.FAVICON),
+ IconRequest.Resource("https://www.firefox.com", type = IconRequest.Resource.Type.APPLE_TOUCH_ICON),
+ )
+
+ val cache: MemoryIconPreparer.PreparerMemoryCache = mock()
+ doReturn(resources).`when`(cache).getResources(any())
+
+ val preparer = MemoryIconPreparer(cache)
+
+ val initialRequest = IconRequest(
+ url = "https://www.example.org",
+ resources = listOf(
+ IconRequest.Resource("https://getpocket.com", type = IconRequest.Resource.Type.FAVICON),
+ ),
+ )
+
+ val request = preparer.prepare(mock(), initialRequest)
+
+ assertEquals(
+ listOf(
+ "https://getpocket.com",
+ ),
+ request.resources.map { it.url },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparerTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparerTest.kt
new file mode 100644
index 0000000000..b1f02ff560
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparerTest.kt
@@ -0,0 +1,110 @@
+/* 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.browser.icons.preparer
+
+import android.content.res.AssetManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+
+@RunWith(AndroidJUnit4::class)
+class TippyTopIconPreparerTest {
+ @Test
+ fun `WHEN url is not in list THEN no resource is added`() {
+ val preparer = TippyTopIconPreparer(testContext.assets)
+
+ val request = IconRequest("https://thispageisnotpartofthetippytopylist.org")
+ assertEquals(0, request.resources.size)
+
+ val preparedRequest = preparer.prepare(testContext, request)
+ assertEquals(0, preparedRequest.resources.size)
+ }
+
+ @Test
+ fun `WHEN url is not http(s) THEN no resource is added`() {
+ val preparer = TippyTopIconPreparer(testContext.assets)
+
+ val request = IconRequest("about://www.github.com")
+ assertEquals(0, request.resources.size)
+
+ val preparedRequest = preparer.prepare(testContext, request)
+ assertEquals(0, preparedRequest.resources.size)
+ }
+
+ @Test
+ fun `WHEN list could not be read THEN no resource is added`() {
+ val assetManager: AssetManager = mock()
+ doReturn("{".toByteArray().inputStream()).`when`(assetManager).open(any())
+
+ val preparer = TippyTopIconPreparer(assetManager)
+
+ val request = IconRequest("https://www.github.com")
+ assertEquals(0, request.resources.size)
+
+ val preparedRequest = preparer.prepare(testContext, request)
+ assertEquals(0, preparedRequest.resources.size)
+ }
+
+ @Test
+ fun `WHEN url is Wikipedia THEN prefix is ignored`() {
+ val preparer = TippyTopIconPreparer(testContext.assets)
+
+ var request = IconRequest("https://www.wikipedia.org")
+ assertEquals(0, request.resources.size)
+
+ var preparedRequest = preparer.prepare(testContext, request)
+ assertEquals(1, preparedRequest.resources.size)
+
+ var resource = preparedRequest.resources[0]
+
+ assertEquals("https://www.wikipedia.org/static/apple-touch/wikipedia.png", resource.url)
+ assertEquals(IconRequest.Resource.Type.TIPPY_TOP, resource.type)
+
+ request = IconRequest("https://en.wikipedia.org")
+ assertEquals(0, request.resources.size)
+
+ preparedRequest = preparer.prepare(testContext, request)
+ assertEquals(1, preparedRequest.resources.size)
+
+ resource = preparedRequest.resources[0]
+
+ assertEquals("https://www.wikipedia.org/static/apple-touch/wikipedia.png", resource.url)
+ assertEquals(IconRequest.Resource.Type.TIPPY_TOP, resource.type)
+
+ request = IconRequest("https://de.wikipedia.org")
+ assertEquals(0, request.resources.size)
+
+ preparedRequest = preparer.prepare(testContext, request)
+ assertEquals(1, preparedRequest.resources.size)
+
+ resource = preparedRequest.resources[0]
+
+ assertEquals("https://www.wikipedia.org/static/apple-touch/wikipedia.png", resource.url)
+ assertEquals(IconRequest.Resource.Type.TIPPY_TOP, resource.type)
+
+ request = IconRequest("https://de.m.wikipedia.org")
+ assertEquals(0, request.resources.size)
+
+ preparedRequest = preparer.prepare(testContext, request)
+ assertEquals(1, preparedRequest.resources.size)
+
+ resource = preparedRequest.resources[0]
+
+ assertEquals("https://www.wikipedia.org/static/apple-touch/wikipedia.png", resource.url)
+ assertEquals(IconRequest.Resource.Type.TIPPY_TOP, resource.type)
+
+ request = IconRequest("https://abc.wikipedia.org.com")
+ assertEquals(0, request.resources.size)
+
+ preparedRequest = preparer.prepare(testContext, request)
+ assertEquals(0, preparedRequest.resources.size)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessorTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessorTest.kt
new file mode 100644
index 0000000000..d113b6e2a1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessorTest.kt
@@ -0,0 +1,89 @@
+/* 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.browser.icons.processor
+
+import android.os.Build
+import androidx.core.graphics.createBitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.IconRequest.Resource.Type.MANIFEST_ICON
+import mozilla.components.support.test.mock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.util.ReflectionHelpers.setStaticField
+import kotlin.reflect.jvm.javaField
+
+@RunWith(AndroidJUnit4::class)
+class AdaptiveIconProcessorTest {
+
+ @Before
+ fun setup() {
+ setSdkInt(0)
+ }
+
+ @After
+ fun teardown() = setSdkInt(0)
+
+ @Test
+ fun `process returns non-maskable icons on legacy devices`() {
+ val icon = Icon(mock(), source = Icon.Source.GENERATOR)
+
+ assertEquals(
+ icon,
+ AdaptiveIconProcessor().process(mock(), mock(), mock(), icon, mock()),
+ )
+ }
+
+ @Test
+ fun `process adds padding to legacy icons`() {
+ setSdkInt(Build.VERSION_CODES.O)
+ val bitmap = spy(createBitmap(128, 128))
+
+ val icon = AdaptiveIconProcessor().process(
+ mock(),
+ mock(),
+ IconRequest.Resource("", MANIFEST_ICON, maskable = false),
+ Icon(bitmap, source = Icon.Source.DISK),
+ mock(),
+ )
+
+ assertEquals(228, icon.bitmap.width)
+ assertEquals(228, icon.bitmap.height)
+
+ assertEquals(Icon.Source.DISK, icon.source)
+ assertTrue(icon.maskable)
+ verify(bitmap).recycle()
+ }
+
+ @Test
+ fun `process adjusts the size of maskable icons`() {
+ val bitmap = createBitmap(256, 256)
+
+ val icon = AdaptiveIconProcessor().process(
+ mock(),
+ mock(),
+ IconRequest.Resource("", MANIFEST_ICON, maskable = true),
+ Icon(bitmap, source = Icon.Source.INLINE),
+ mock(),
+ )
+
+ assertEquals(334, icon.bitmap.width)
+ assertEquals(334, icon.bitmap.height)
+
+ assertEquals(Icon.Source.INLINE, icon.source)
+ assertTrue(icon.maskable)
+ }
+
+ private fun setSdkInt(sdkVersion: Int) {
+ setStaticField(Build.VERSION::SDK_INT.javaField, sdkVersion)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ColorProcessorTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ColorProcessorTest.kt
new file mode 100644
index 0000000000..62c2d35398
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ColorProcessorTest.kt
@@ -0,0 +1,53 @@
+/* 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.browser.icons.processor
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import mozilla.components.browser.icons.Icon
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+
+class ColorProcessorTest {
+ @Test
+ fun `test extracting color`() {
+ val icon = Icon(mockRedBitmap(1), source = Icon.Source.DISK)
+ val processed = ColorProcessor().process(mock(), mock(), mock(), icon, mock())
+
+ assertEquals(icon.bitmap, processed.bitmap)
+ assertNotNull(processed.color)
+ }
+
+ @Test
+ fun `test extracting color from larger bitmap`() {
+ val icon = Icon(mockRedBitmap(3), source = Icon.Source.DISK)
+ val processed = ColorProcessor().process(mock(), mock(), mock(), icon, mock())
+
+ assertEquals(icon.bitmap, processed.bitmap)
+ assertNotNull(processed.color)
+ }
+
+ private fun mockRedBitmap(size: Int): Bitmap {
+ val bitmap: Bitmap = mock()
+ doReturn(size).`when`(bitmap).height
+ doReturn(size).`when`(bitmap).width
+
+ doAnswer {
+ val pixels: IntArray = it.getArgument(0)
+ for (i in 0 until pixels.size) {
+ pixels[i] = Color.RED
+ }
+ null
+ }.`when`(bitmap).getPixels(any(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt())
+
+ return bitmap
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/DiskIconProcessorTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/DiskIconProcessorTest.kt
new file mode 100644
index 0000000000..c90b522261
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/DiskIconProcessorTest.kt
@@ -0,0 +1,117 @@
+/* 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.browser.icons.processor
+
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class DiskIconProcessorTest {
+ @Test
+ fun `Generated icons are not saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.GENERATOR)
+ val cache: DiskIconProcessor.ProcessorDiskCache = mock()
+
+ val processor = DiskIconProcessor(cache)
+ processor.process(mock(), mock(), mock(), icon, mock())
+
+ verify(cache, never()).putIcon(any(), any(), eq(icon))
+ }
+
+ @Test
+ fun `Icon loaded from memory cache are not saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.MEMORY)
+ val cache: DiskIconProcessor.ProcessorDiskCache = mock()
+
+ val processor = DiskIconProcessor(cache)
+ processor.process(mock(), mock(), mock(), icon, mock())
+
+ verify(cache, never()).putIcon(any(), any(), eq(icon))
+ }
+
+ @Test
+ fun `Icon loaded from disk cache are not saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.DISK)
+ val cache: DiskIconProcessor.ProcessorDiskCache = mock()
+
+ val processor = DiskIconProcessor(cache)
+ processor.process(mock(), mock(), mock(), icon, mock())
+
+ verify(cache, never()).putIcon(any(), any(), eq(icon))
+ }
+
+ @Test
+ fun `Downloaded icon is saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.DOWNLOAD)
+ val cache: DiskIconProcessor.ProcessorDiskCache = mock()
+
+ val processor = DiskIconProcessor(cache)
+ val request: IconRequest = mock()
+ val resource: IconRequest.Resource = mock()
+ processor.process(mock(), request, resource, icon, mock())
+
+ verify(cache).putIcon(any(), eq(resource), eq(icon))
+ }
+
+ @Test
+ fun `Inlined icon is saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.INLINE)
+ val cache: DiskIconProcessor.ProcessorDiskCache = mock()
+
+ val processor = DiskIconProcessor(cache)
+ val request: IconRequest = mock()
+ val resource: IconRequest.Resource = mock()
+ processor.process(mock(), request, resource, icon, mock())
+
+ verify(cache).putIcon(any(), eq(resource), eq(icon))
+ }
+
+ @Test
+ fun `Icon without resource is not saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.MEMORY)
+ val cache: DiskIconProcessor.ProcessorDiskCache = mock()
+
+ val processor = DiskIconProcessor(cache)
+ processor.process(context = mock(), request = mock(), resource = null, icon = icon, desiredSize = mock())
+
+ verify(cache, never()).putIcon(any(), any(), eq(icon))
+ }
+
+ @Test
+ fun `Icon loaded in private mode is not saved in cache`() {
+ /* Can be Source.INLINE as well. To ensure that the icon is eligible for caching on the disk. */
+ val icon = Icon(mock(), source = Icon.Source.DOWNLOAD)
+ val cache: DiskIconProcessor.ProcessorDiskCache = mock()
+
+ val processor = DiskIconProcessor(cache)
+ val request: IconRequest = mock()
+ `when`(request.isPrivate).thenReturn(true)
+ val resource: IconRequest.Resource = mock()
+ processor.process(context = mock(), request = request, resource = resource, icon = icon, desiredSize = mock())
+
+ verify(cache, never()).putIcon(any(), any(), eq(icon))
+ }
+
+ @Test
+ fun `Icon loaded in non-private mode is saved in cache`() {
+ /* Can be Source.INLINE as well. To ensure that the icon is eligible for caching on the disk. */
+ val icon = Icon(mock(), source = Icon.Source.DOWNLOAD)
+ val cache: DiskIconProcessor.ProcessorDiskCache = mock()
+
+ val processor = DiskIconProcessor(cache)
+ val request: IconRequest = mock()
+ `when`(request.isPrivate).thenReturn(false)
+ val resource: IconRequest.Resource = mock()
+ processor.process(context = mock(), request = request, resource = resource, icon = icon, desiredSize = mock())
+
+ verify(cache).putIcon(any(), eq(resource), eq(icon))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/MemoryIconProcessorTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/MemoryIconProcessorTest.kt
new file mode 100644
index 0000000000..93b79db442
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/MemoryIconProcessorTest.kt
@@ -0,0 +1,85 @@
+/* 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.browser.icons.processor
+
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class MemoryIconProcessorTest {
+ @Test
+ fun `Generated icons are not saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.GENERATOR)
+ val cache: MemoryIconProcessor.ProcessorMemoryCache = mock()
+
+ val processor = MemoryIconProcessor(cache)
+ processor.process(mock(), mock(), mock(), icon, mock())
+
+ verify(cache, never()).put(any(), any(), any())
+ }
+
+ @Test
+ fun `Icon loaded from memory cache are not saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.MEMORY)
+ val cache: MemoryIconProcessor.ProcessorMemoryCache = mock()
+
+ val processor = MemoryIconProcessor(cache)
+ processor.process(mock(), mock(), mock(), icon, mock())
+
+ verify(cache, never()).put(any(), any(), any())
+ }
+
+ @Test
+ fun `Icon loaded from disk cache are saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.DISK)
+ val cache: MemoryIconProcessor.ProcessorMemoryCache = mock()
+
+ val processor = MemoryIconProcessor(cache)
+ processor.process(mock(), mock(), mock(), icon, mock())
+
+ verify(cache).put(any(), any(), any())
+ }
+
+ @Test
+ fun `Downloaded icon is saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.DOWNLOAD)
+ val cache: MemoryIconProcessor.ProcessorMemoryCache = mock()
+
+ val processor = MemoryIconProcessor(cache)
+ val request: IconRequest = mock()
+ val resource: IconRequest.Resource = mock()
+ processor.process(mock(), request, resource, icon, mock())
+
+ verify(cache).put(request, resource, icon)
+ }
+
+ @Test
+ fun `Inlined icon is saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.INLINE)
+ val cache: MemoryIconProcessor.ProcessorMemoryCache = mock()
+
+ val processor = MemoryIconProcessor(cache)
+ val request: IconRequest = mock()
+ val resource: IconRequest.Resource = mock()
+ processor.process(mock(), request, resource, icon, mock())
+
+ verify(cache).put(request, resource, icon)
+ }
+
+ @Test
+ fun `Icon without resource is not saved in cache`() {
+ val icon = Icon(mock(), source = Icon.Source.MEMORY)
+ val cache: MemoryIconProcessor.ProcessorMemoryCache = mock()
+
+ val processor = MemoryIconProcessor(cache)
+ processor.process(context = mock(), request = mock(), resource = null, icon = icon, desiredSize = mock())
+
+ verify(cache, never()).put(any(), any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ResizingProcessorTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ResizingProcessorTest.kt
new file mode 100644
index 0000000000..67c1135e25
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ResizingProcessorTest.kt
@@ -0,0 +1,124 @@
+/* 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.browser.icons.processor
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.util.DisplayMetrics
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.support.images.DesiredSize
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class ResizingProcessorTest {
+ private lateinit var processor: ResizingProcessor
+
+ @Before
+ fun setup() {
+ processor = spy(ResizingProcessor())
+ }
+
+ @Test
+ fun `icons of the correct size are not resized`() {
+ val icon = Icon(mockBitmap(64), source = Icon.Source.DISK)
+ val resized = process(icon = icon, desiredSize = DesiredSize(64, 64, 100, 3f))
+
+ assertEquals(icon.bitmap, resized?.bitmap)
+
+ verify(processor, never()).resize(any(), anyInt())
+ }
+
+ @Test
+ fun `larger icons are resized`() {
+ val icon = Icon(mockBitmap(120), source = Icon.Source.DISK)
+ val smallerIcon = mockBitmap(64)
+ doReturn(smallerIcon).`when`(processor).resize(eq(icon.bitmap), anyInt())
+ val resized = process(icon = icon, desiredSize = DesiredSize(64, 64, 64, 3f))
+
+ assertNotEquals(icon.bitmap, resized?.bitmap)
+ assertEquals(smallerIcon, resized?.bitmap)
+
+ verify(processor).resize(icon.bitmap, 64)
+ }
+
+ @Test
+ fun `smaller icons are resized`() {
+ val icon = Icon(mockBitmap(34), source = Icon.Source.DISK)
+ val largerIcon = mockBitmap(64)
+ doReturn(largerIcon).`when`(processor).resize(eq(icon.bitmap), anyInt())
+ val resized = process(icon = icon, desiredSize = DesiredSize(64, 64, 70, 3f))
+
+ assertNotEquals(icon.bitmap, resized?.bitmap)
+ assertEquals(largerIcon, resized?.bitmap)
+
+ verify(processor).resize(icon.bitmap, 64)
+ }
+
+ @Test
+ fun `smaller icons are not resized past the max scale factor`() {
+ val icon = Icon(mockBitmap(10), source = Icon.Source.DISK)
+ val largerIcon = mockBitmap(64)
+ doReturn(largerIcon).`when`(processor).resize(eq(icon.bitmap), anyInt())
+ val resized = process(icon = icon)
+
+ assertNotEquals(icon.bitmap, resized?.bitmap)
+ assertNull(resized)
+ }
+
+ @Test
+ fun `smaller icons are resized to the max scale factor if permitted`() {
+ val processor = spy(ResizingProcessor(discardSmallIcons = false))
+
+ val icon = Icon(mockBitmap(10), source = Icon.Source.DISK)
+ val largerIcon = mockBitmap(64)
+ doReturn(largerIcon).`when`(processor).resize(eq(icon.bitmap), anyInt())
+ val resized = process(processor, icon = icon)
+
+ assertNotEquals(icon.bitmap, resized?.bitmap)
+ assertNotNull(resized)
+
+ verify(processor).resize(icon.bitmap, 30)
+ }
+
+ private fun process(
+ p: ResizingProcessor = processor,
+ context: Context = mockContext(2f),
+ request: IconRequest = IconRequest("https://mozilla.org"),
+ resource: IconRequest.Resource = mock(),
+ icon: Icon = Icon(mockBitmap(64), source = Icon.Source.DISK),
+ desiredSize: DesiredSize = DesiredSize(96, 96, 1000, 3f),
+ ) = p.process(context, request, resource, icon, desiredSize)
+
+ private fun mockContext(density: Float): Context {
+ val context: Context = mock()
+ val resources: Resources = mock()
+ val displayMetrics = DisplayMetrics()
+ doReturn(resources).`when`(context).resources
+ doReturn(displayMetrics).`when`(resources).displayMetrics
+ displayMetrics.density = density
+ return context
+ }
+
+ private fun mockBitmap(size: Int): Bitmap {
+ val bitmap: Bitmap = mock()
+ doReturn(size).`when`(bitmap).height
+ doReturn(size).`when`(bitmap).width
+ return bitmap
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconDiskCacheTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconDiskCacheTest.kt
new file mode 100644
index 0000000000..605a4bd9e2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconDiskCacheTest.kt
@@ -0,0 +1,106 @@
+/* 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.browser.icons.utils
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.jakewharton.disklrucache.DiskLruCache
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.`when`
+import java.io.IOException
+import java.io.OutputStream
+
+@RunWith(AndroidJUnit4::class)
+class IconDiskCacheTest {
+
+ @Test
+ fun `Writing and reading resources`() {
+ val cache = IconDiskCache()
+
+ val resources = listOf(
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/icon64.png",
+ sizes = listOf(Size(64, 64)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/icon128.png",
+ sizes = listOf(Size(128, 128)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ ),
+ IconRequest.Resource(
+ url = "https://www.mozilla.org/icon128.png",
+ sizes = listOf(Size(180, 180)),
+ type = IconRequest.Resource.Type.APPLE_TOUCH_ICON,
+ ),
+ )
+
+ val request = IconRequest("https://www.mozilla.org", resources = resources)
+ cache.putResources(testContext, request)
+
+ val restoredResources = cache.getResources(testContext, request)
+ assertEquals(3, restoredResources.size)
+ assertEquals(resources, restoredResources)
+ }
+
+ @Test
+ fun `Writing and reading bitmap bytes`() {
+ val cache = IconDiskCache()
+
+ val resource = IconRequest.Resource(
+ url = "https://www.mozilla.org/icon64.png",
+ sizes = listOf(Size(64, 64)),
+ mimeType = "image/png",
+ type = IconRequest.Resource.Type.FAVICON,
+ )
+
+ val bitmap: Bitmap = mock()
+ `when`(bitmap.compress(any(), anyInt(), any())).thenAnswer {
+ @Suppress("DEPRECATION")
+ // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/9555
+ assertEquals(Bitmap.CompressFormat.WEBP, it.arguments[0] as Bitmap.CompressFormat)
+ assertEquals(90, it.arguments[1] as Int) // Quality
+
+ val stream = it.arguments[2] as OutputStream
+ stream.write("Hello World".toByteArray())
+ true
+ }
+
+ cache.putIconBitmap(testContext, resource, bitmap)
+
+ val data = cache.getIconData(testContext, resource)
+ assertNotNull(data!!)
+ assertEquals("Hello World", String(data))
+ }
+
+ @Test
+ fun `Clearing cache directories catches IOException`() {
+ val cache = IconDiskCache()
+ val dataCache: DiskLruCache = mock()
+ val resCache: DiskLruCache = mock()
+ cache.iconDataCache = dataCache
+ cache.iconResourcesCache = resCache
+
+ `when`(dataCache.delete()).thenThrow(IOException("test"))
+ `when`(resCache.delete()).thenThrow(IOException("test"))
+
+ cache.clear(testContext)
+
+ assertNull(cache.iconDataCache)
+ assertNull(cache.iconResourcesCache)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconMemoryCacheTest.kt b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconMemoryCacheTest.kt
new file mode 100644
index 0000000000..843fa1134c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconMemoryCacheTest.kt
@@ -0,0 +1,62 @@
+/* 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.browser.icons.utils
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.loader.IconLoader
+import mozilla.components.browser.icons.loader.MemoryIconLoader
+import mozilla.components.browser.icons.preparer.MemoryIconPreparer
+import mozilla.components.browser.icons.processor.MemoryIconProcessor
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class IconMemoryCacheTest {
+
+ @Test
+ fun `Verify memory components interaction`() {
+ val cache = IconMemoryCache()
+
+ val preparer = MemoryIconPreparer(cache)
+ val loader = MemoryIconLoader(cache)
+ val processor = MemoryIconProcessor(cache)
+
+ val icon = Icon(bitmap = mock(), source = Icon.Source.DOWNLOAD)
+ val resource = IconRequest.Resource(
+ url = "https://www.mozilla.org/favicon64.ico",
+ type = IconRequest.Resource.Type.FAVICON,
+ )
+ val request = IconRequest("https://www.mozilla.org", resources = listOf(resource))
+
+ assertEquals(IconLoader.Result.NoResult, loader.load(mock(), request, resource))
+
+ // First, save something in the memory cache using the processor
+ processor.process(mock(), request, resource, icon, mock())
+
+ // Then load the same icon from the loader
+ val result = loader.load(mock(), request, resource)
+ assertTrue(result is IconLoader.Result.BitmapResult)
+ assertSame(icon.bitmap, (result as IconLoader.Result.BitmapResult).bitmap)
+ assertEquals(Icon.Source.MEMORY, result.source)
+
+ // Prepare a new request with the same URL
+ val newRequest = IconRequest("https://www.mozilla.org")
+ assertTrue(newRequest.resources.isEmpty())
+ val preparedRequest = preparer.prepare(mock(), newRequest)
+ assertEquals(1, preparedRequest.resources.size)
+
+ // Load prepared request
+ val preparedResult = loader.load(mock(), preparedRequest, preparedRequest.resources[0])
+ assertTrue(preparedResult is IconLoader.Result.BitmapResult)
+ assertSame(icon.bitmap, (preparedResult as IconLoader.Result.BitmapResult).bitmap)
+ assertEquals(Icon.Source.MEMORY, preparedResult.source)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/bmp/test.bmp b/mobile/android/android-components/components/browser/icons/src/test/resources/bmp/test.bmp
new file mode 100644
index 0000000000..340f116fc8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/bmp/test.bmp
Binary files differ
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/gif/cat.gif b/mobile/android/android-components/components/browser/icons/src/test/resources/gif/cat.gif
new file mode 100644
index 0000000000..935cef723c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/gif/cat.gif
Binary files differ
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/ico/golem_favicon.ico b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/golem_favicon.ico
new file mode 100644
index 0000000000..e5f6fd86f4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/golem_favicon.ico
Binary files differ
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/ico/microsoft_favicon.ico b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/microsoft_favicon.ico
new file mode 100644
index 0000000000..bfe873eb22
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/microsoft_favicon.ico
Binary files differ
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/ico/nvidia_favicon.ico b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/nvidia_favicon.ico
new file mode 100644
index 0000000000..424df87200
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/nvidia_favicon.ico
Binary files differ
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/jpg/tonys.jpg b/mobile/android/android-components/components/browser/icons/src/test/resources/jpg/tonys.jpg
new file mode 100644
index 0000000000..d1d34e6b4a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/jpg/tonys.jpg
Binary files differ
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/misc/test.txt b/mobile/android/android-components/components/browser/icons/src/test/resources/misc/test.txt
new file mode 100644
index 0000000000..c57eff55eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/misc/test.txt
@@ -0,0 +1 @@
+Hello World! \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/icons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..49324d83c5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,3 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
+
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/png/mozac.png b/mobile/android/android-components/components/browser/icons/src/test/resources/png/mozac.png
new file mode 100644
index 0000000000..2a03203476
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/png/mozac.png
Binary files differ
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/icons/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/browser/icons/src/test/resources/webp/test.webp b/mobile/android/android-components/components/browser/icons/src/test/resources/webp/test.webp
new file mode 100644
index 0000000000..f0e226f0ae
--- /dev/null
+++ b/mobile/android/android-components/components/browser/icons/src/test/resources/webp/test.webp
Binary files differ
diff --git a/mobile/android/android-components/components/browser/menu/README.md b/mobile/android/android-components/components/browser/menu/README.md
new file mode 100644
index 0000000000..4d12c5c13d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/README.md
@@ -0,0 +1,152 @@
+# [Android Components](../../../README.md) > Browser > Menu
+
+A generic menu with customizable items primarily for browser toolbars.
+
+## Usage
+
+### 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:browser-menu:{latest-version}"
+```
+
+### BrowserMenu
+Sample code can be found in [Sample Toolbar app](https://github.com/mozilla-mobile/android-components/tree/main/samples/toolbar).
+
+There are multiple properties that you customize of the menu browser by just adding them into your dimens.xml file.
+
+```xml
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!--Change how rounded the corners of the menu should be-->
+ <dimen name="mozac_browser_menu_corner_radius" tools:ignore="UnusedResources">4dp</dimen>
+
+ <!--Change how much shadow the menu should have-->
+ <dimen name="mozac_browser_menu_elevation" tools:ignore="UnusedResources">4dp</dimen>
+
+ <!--Change the width of the menu-->
+ <dimen name="mozac_browser_menu_width" tools:ignore="UnusedResources">250dp</dimen>
+
+ <!--Change the dynamic width of the menu-->
+ <dimen name="mozac_browser_menu_width_min" tools:ignore="UnusedResources">200dp</dimen>
+ <dimen name="mozac_browser_menu_width_max" tools:ignore="UnusedResources">300dp</dimen>
+
+ <!--Change the top and bottom padding of the menu-->
+ <dimen name="mozac_browser_menu_padding_vertical" tools:ignore="UnusedResources">8dp</dimen>
+
+</resources>
+```
+BrowserMenu can have a dynamic width:
+- Using the same value for `mozac_browser_menu_width_min` and `mozac_browser_menu_width_max` means BrowserMenu will have a fixed width - `mozac_browser_menu_width`.
+_This is the default behavior_.
+- Different values for `mozac_browser_menu_width_min` and `mozac_browser_menu_width_max` means BrowserMenu will have a dynamic width depending on the widest BrowserMenuItem and between the aforementioned dimensions also taking into account display width.
+
+
+### BrowserMenuDivider
+```kotlin
+
+ BrowserMenuDivider()
+
+```
+
+To customize the divider you could use a 1. Quick customization or a 2. Full customization:
+
+1) If you just want to change the height of the divider, add this item your ``dimes.xml`` file, and your
+prefer height size.
+
+```xml
+ <dimen name="mozac_browser_menu_item_divider_height" tools:ignore="UnusedResources">YOUR_HEIGHT</dimen>
+```
+2) For full customization, override the default style of the divider by adding this style item in your `style.xml` file, and customize to your liking.
+```xml
+ <style name="Mozac.Browser.Menu.Item.Divider.Horizontal" tools:ignore="UnusedResources">
+ <item name="android:background">YOUR_BACKGROUND</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">YOUR_HEIGHT</item>
+ </style>
+```
+
+### BrowserMenuImageText
+```kotlin
+ BrowserMenuImageText(
+ label = "Share",
+ imageResource = R.drawable.mozac_ic_share_android_24,
+ iconTintColorResource = R.color.photonBlue90
+ ) {
+ Toast.makeText(applicationContext, "Share", Toast.LENGTH_SHORT).show()
+ }
+```
+
+To customize the menu you could use separate properties 1 or full access to the style of the menu 2:
+
+1) If you just want to change a specify property, just add one these dimen items to your ``dimes.xml`` file.
+
+```xml
+ <!--Menu Item -->
+ <!--Change the text_size for ALL menu items NOT only for the BrowserMenuImageText -->
+ <dimen name="mozac_browser_menu_item_text_size" tools:ignore="UnusedResources">16sp</dimen>
+ <!--Menu Item -->
+
+ <!--Icon-->
+ <!--Change the icon's width-->
+ <dimen name="mozac_browser_menu_item_image_text_icon_width" tools:ignore="UnusedResources">24dp</dimen> <!--Default value-->
+
+ <!--Change the icon's height-->
+ <dimen name="mozac_browser_menu_item_image_text_icon_height" tools:ignore="UnusedResources">24dp</dimen> <!--Default value-->
+
+ <!--Icon-->
+
+ <!--Label-->
+ <!--Change the separation between the label and the icon-->
+ <dimen name="mozac_browser_menu_item_image_text_label_padding_start" tools:ignore="UnusedResources">20dp</dimen> <!--Default value-->
+
+ <!--Label-->
+```
+
+2) For full customization, override the default style of menu by adding this style item in your `style.xml` file, and customize to your liking.
+
+```xml
+ <!--Change the appearance of all text menu items-->
+ <style name="Mozac.Browser.Menu.Item.Text" parent="@android:style/TextAppearance.Material.Menu" tools:ignore="UnusedResources">
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:textSize">@dimen/mozac_browser_menu_item_text_size</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:lines">1</item>
+ <item name="android:focusable">true</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.ImageText.Icon" parent="" tools:ignore="UnusedResources">
+ <item name="android:layout_width">@dimen/mozac_browser_menu_item_image_text_icon_width</item>
+ <item name="android:layout_height">@dimen/mozac_browser_menu_item_image_text_icon_height</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.ImageText.Label" parent="Mozac.Browser.Menu.Item.Text" tools:ignore="UnusedResources">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:paddingStart">@dimen/mozac_browser_menu_item_image_text_label_padding_start</item>
+ </style>
+```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Extras | Description |
+|--------|-------------------------|-------------------|--------------------------------------|
+| Click | web_extension_menu_item | `menuItemExtras` | Web extension menu item was clicked. |
+
+
+#### `menuItemExtras`
+
+| Key | Type | Value |
+|------|--------|----------------------------------------------------------|
+| "id" | String | Web extension id of the clicked web extension menu item. |
+
+## 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/browser/menu/build.gradle b/mobile/android/android-components/components/browser/menu/build.gradle
new file mode 100644
index 0000000000..7e305cda83
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/build.gradle
@@ -0,0 +1,57 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'mozilla.components.browser.menu'
+}
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':concept-menu')
+ implementation project(':browser-state')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':ui-colors')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_recyclerview
+ implementation ComponentsDependencies.androidx_cardview
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_coordinatorlayout
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+}
+
+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/browser/menu/lint.xml b/mobile/android/android-components/components/browser/menu/lint.xml
new file mode 100644
index 0000000000..81bcc3bfb8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/lint.xml
@@ -0,0 +1,7 @@
+<?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/. -->
+<lint>
+ <issue id="Overdraw" severity="ignore" />
+</lint> \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/menu/proguard-rules.pro b/mobile/android/android-components/components/browser/menu/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/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/browser/menu/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/menu/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..feab9bdd95
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+ <application android:supportsRtl="true" />
+</manifest> \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenu.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenu.kt
new file mode 100644
index 0000000000..bdafd294ca
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenu.kt
@@ -0,0 +1,320 @@
+/* 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.browser.menu
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Build
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.PopupWindow
+import androidx.annotation.VisibleForTesting
+import androidx.cardview.widget.CardView
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.widget.PopupWindowCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.browser.menu.BrowserMenu.Orientation.DOWN
+import mozilla.components.browser.menu.BrowserMenu.Orientation.UP
+import mozilla.components.browser.menu.view.DynamicWidthRecyclerView
+import mozilla.components.browser.menu.view.ExpandableLayout
+import mozilla.components.browser.menu.view.StickyItemPlacement
+import mozilla.components.browser.menu.view.StickyItemsLinearLayoutManager
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.support.ktx.android.view.isRTL
+import mozilla.components.support.ktx.android.view.onNextGlobalLayout
+
+/**
+ * A popup menu composed of BrowserMenuItem objects.
+ */
+open class BrowserMenu internal constructor(
+ internal val adapter: BrowserMenuAdapter,
+) : View.OnAttachStateChangeListener {
+ protected var currentPopup: PopupWindow? = null
+
+ @VisibleForTesting
+ internal var menuList: RecyclerView? = null
+ internal var currAnchor: View? = null
+ internal var isShown = false
+
+ @VisibleForTesting
+ internal lateinit var menuPositioningData: MenuPositioningData
+ internal var backgroundColor: Int = Color.RED
+
+ /**
+ * @param anchor the view on which to pin the popup window.
+ * @param orientation the preferred orientation to show the popup window.
+ * @param style Custom styling for this menu.
+ * @param endOfMenuAlwaysVisible when is set to true makes sure the bottom of the menu is always visible otherwise,
+ * the top of the menu is always visible.
+ */
+ @Suppress("InflateParams", "ComplexMethod")
+ open fun show(
+ anchor: View,
+ orientation: Orientation = DOWN,
+ style: MenuStyle? = null,
+ endOfMenuAlwaysVisible: Boolean = false,
+ onDismiss: () -> Unit = {},
+ ): PopupWindow {
+ var view = LayoutInflater.from(anchor.context).inflate(R.layout.mozac_browser_menu, null)
+
+ adapter.menu = this
+
+ menuList = view.findViewById<DynamicWidthRecyclerView>(R.id.mozac_browser_menu_recyclerView).apply {
+ layoutManager = StickyItemsLinearLayoutManager.get<BrowserMenuAdapter>(
+ anchor.context,
+ StickyItemPlacement.BOTTOM,
+ false,
+ )
+
+ adapter = this@BrowserMenu.adapter
+ minWidth = style?.minWidth ?: resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_width_min)
+ maxWidth = style?.maxWidth ?: resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_width_max)
+ }
+
+ setColors(view, style)
+
+ menuList?.accessibilityDelegate = object : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfo,
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.collectionInfo =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ AccessibilityNodeInfo.CollectionInfo(
+ adapter.interactiveCount,
+ 0,
+ false,
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ AccessibilityNodeInfo.CollectionInfo.obtain(
+ adapter.interactiveCount,
+ 0,
+ false,
+ )
+ }
+ }
+ }
+
+ // Data needed to infer whether to show a collapsed menu
+ // And then to properly place it.
+ menuPositioningData = inferMenuPositioningData(
+ view as ViewGroup,
+ anchor,
+ MenuPositioningData(askedOrientation = orientation),
+ )
+
+ view = configureExpandableMenu(view, endOfMenuAlwaysVisible)
+ return getNewPopupWindow(view).apply {
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ isFocusable = true
+ elevation = view.resources.getDimension(R.dimen.mozac_browser_menu_elevation)
+
+ setOnDismissListener {
+ adapter.menu = null
+ currentPopup = null
+ isShown = false
+ onDismiss()
+ }
+
+ displayPopup(menuPositioningData).also {
+ anchor.addOnAttachStateChangeListener(this@BrowserMenu)
+ currAnchor = anchor
+ }
+ }.also {
+ currentPopup = it
+ isShown = true
+ }
+ }
+
+ @VisibleForTesting
+ internal fun configureExpandableMenu(
+ view: ViewGroup,
+ endOfMenuAlwaysVisible: Boolean,
+ ): ViewGroup {
+ // If the menu is placed at the bottom it should start as collapsed.
+ if (menuPositioningData.inferredMenuPlacement is BrowserMenuPlacement.AnchoredToBottom.Dropdown ||
+ menuPositioningData.inferredMenuPlacement is BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring
+ ) {
+ val collapsingMenuIndexLimit = adapter.visibleItems.indexOfFirst { it.isCollapsingMenuLimit }
+ val stickyFooterPosition = adapter.visibleItems.indexOfLast { it.isSticky }
+ if (collapsingMenuIndexLimit > 0) {
+ return ExpandableLayout.wrapContentInExpandableView(
+ view,
+ collapsingMenuIndexLimit,
+ stickyFooterPosition,
+ ) { dismiss() }
+ }
+ } else {
+ // The menu is by default set as a bottom one. Reconfigure it as a top one.
+ menuList?.layoutManager = StickyItemsLinearLayoutManager.get<BrowserMenuAdapter>(
+ view.context,
+ StickyItemPlacement.TOP,
+ )
+
+ // By default the menu is laid out from and scrolled to top - showing the top most items.
+ // For the top menu it may be desired to initially show the bottom most items.
+ menuList?.let { list ->
+ list.setEndOfMenuAlwaysVisibleCompact(
+ endOfMenuAlwaysVisible,
+ list.layoutManager as LinearLayoutManager,
+ )
+ }
+ }
+
+ return view
+ }
+
+ @VisibleForTesting
+ internal fun getNewPopupWindow(view: ViewGroup): PopupWindow {
+ // If the menu is expandable we need to give it all the possible space to expand.
+ // Also, by setting MATCH_PARENT, expanding the menu will not expand the Window
+ // of the PopupWindow which for a bottom anchored menu means glitchy animations.
+ val popupHeight = if (view is ExpandableLayout) {
+ WindowManager.LayoutParams.MATCH_PARENT
+ } else {
+ // Otherwise wrap the menu. Allowing it to be as big as the parent would result in
+ // layout issues if the menu is smaller than the available screen estate.
+ WindowManager.LayoutParams.WRAP_CONTENT
+ }
+
+ return PopupWindow(
+ view,
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ popupHeight,
+ )
+ }
+
+ private fun RecyclerView.setEndOfMenuAlwaysVisibleCompact(
+ endOfMenuAlwaysVisible: Boolean,
+ layoutManager: LinearLayoutManager,
+ ) {
+ // In devices with Android 6 and below stackFromEnd is not working properly,
+ // as a result, we have to provided a backwards support.
+ // See: https://github.com/mozilla-mobile/android-components/issues/3211
+ if (endOfMenuAlwaysVisible && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
+ scrollOnceToTheBottom(this)
+ } else {
+ layoutManager.stackFromEnd = endOfMenuAlwaysVisible
+ }
+ }
+
+ @VisibleForTesting
+ internal fun scrollOnceToTheBottom(recyclerView: RecyclerView) {
+ recyclerView.onNextGlobalLayout {
+ recyclerView.adapter?.let { recyclerView.scrollToPosition(it.itemCount - 1) }
+ }
+ }
+
+ fun dismiss() {
+ currentPopup?.dismiss()
+ }
+
+ fun invalidate() {
+ menuList?.let { adapter.invalidate(it) }
+ }
+
+ @VisibleForTesting
+ internal fun setColors(menuLayout: View, colorState: MenuStyle?) {
+ val listParent: CardView = menuLayout.findViewById(R.id.mozac_browser_menu_menuView)
+ backgroundColor = colorState?.backgroundColor?.let {
+ listParent.setCardBackgroundColor(it)
+ it.defaultColor
+ } ?: listParent.cardBackgroundColor.defaultColor
+ }
+
+ companion object {
+ /**
+ * Determines the orientation to be used for a menu based on the positioning of the [parent] in the layout.
+ */
+ fun determineMenuOrientation(parent: View?): Orientation {
+ if (parent == null) {
+ return DOWN
+ }
+
+ val params = parent.layoutParams
+ return if (params is CoordinatorLayout.LayoutParams) {
+ if ((params.gravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
+ UP
+ } else {
+ DOWN
+ }
+ } else {
+ DOWN
+ }
+ }
+ }
+
+ enum class Orientation(val concept: mozilla.components.concept.menu.Orientation) {
+ UP(mozilla.components.concept.menu.Orientation.UP),
+ DOWN(mozilla.components.concept.menu.Orientation.DOWN),
+ }
+
+ override fun onViewDetachedFromWindow(v: View) {
+ currentPopup?.dismiss()
+ currAnchor?.removeOnAttachStateChangeListener(this)
+ }
+
+ override fun onViewAttachedToWindow(v: View) {
+ // no-op
+ }
+}
+
+@VisibleForTesting
+internal fun PopupWindow.displayPopup(currentData: MenuPositioningData) {
+ // Try to use the preferred orientation, if doesn't fit fallback to the best fit.
+ when (currentData.inferredMenuPlacement) {
+ is BrowserMenuPlacement.AnchoredToTop.Dropdown -> showPopupWithDownOrientation(currentData)
+ is BrowserMenuPlacement.AnchoredToBottom.Dropdown -> showPopupWithUpOrientation(currentData)
+
+ is BrowserMenuPlacement.AnchoredToTop.ManualAnchoring,
+ is BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring,
+ -> showAtAnchorLocation(currentData)
+ else -> {
+ // no-op
+ }
+ }
+}
+
+@VisibleForTesting
+internal fun PopupWindow.showPopupWithUpOrientation(menuPositioningData: MenuPositioningData) {
+ val anchor = menuPositioningData.inferredMenuPlacement!!.anchor
+ val xOffset = if (anchor.isRTL) -anchor.width else 0
+ animationStyle = menuPositioningData.inferredMenuPlacement.animation
+
+ // Positioning the menu above and overlapping the anchor.
+ val yOffset = if (menuPositioningData.availableHeightToBottom < 0) {
+ // The anchor is partially below of the bottom of the screen, let's make the menu completely visible.
+ menuPositioningData.availableHeightToBottom - menuPositioningData.containerViewHeight
+ } else {
+ -menuPositioningData.containerViewHeight
+ }
+ showAsDropDown(anchor, xOffset, yOffset)
+}
+
+private fun PopupWindow.showPopupWithDownOrientation(menuPositioningData: MenuPositioningData) {
+ val anchor = menuPositioningData.inferredMenuPlacement!!.anchor
+ val xOffset = if (anchor.isRTL) -anchor.width else 0
+ animationStyle = menuPositioningData.inferredMenuPlacement.animation
+ // Menu should overlay the anchor.
+ showAsDropDown(anchor, xOffset, -anchor.height)
+}
+
+private fun PopupWindow.showAtAnchorLocation(menuPositioningData: MenuPositioningData) {
+ val anchor = menuPositioningData.inferredMenuPlacement!!.anchor
+ val anchorPosition = IntArray(2)
+ animationStyle = menuPositioningData.inferredMenuPlacement.animation
+
+ anchor.getLocationOnScreen(anchorPosition)
+ val (x, y) = anchorPosition
+ PopupWindowCompat.setOverlapAnchor(this, true)
+ showAtLocation(anchor, Gravity.START or Gravity.TOP, x, y)
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuAdapter.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuAdapter.kt
new file mode 100644
index 0000000000..f2a8ea954e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuAdapter.kt
@@ -0,0 +1,68 @@
+/* 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.browser.menu
+
+import android.content.Context
+import android.graphics.Color
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.browser.menu.view.StickyItemsAdapter
+
+/**
+ * Adapter implementation used by the browser menu to display menu items in a RecyclerView.
+ */
+internal class BrowserMenuAdapter(
+ context: Context,
+ items: List<BrowserMenuItem>,
+) : RecyclerView.Adapter<BrowserMenuItemViewHolder>(), StickyItemsAdapter {
+ var menu: BrowserMenu? = null
+
+ internal val visibleItems = items.filter { it.visible() }
+ internal val interactiveCount = visibleItems.sumOf { it.interactiveCount() }
+ private val inflater = LayoutInflater.from(context)
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+ BrowserMenuItemViewHolder(inflater.inflate(viewType, parent, false))
+
+ override fun getItemCount() = visibleItems.size
+
+ override fun getItemViewType(position: Int): Int = visibleItems[position].getLayoutResource()
+
+ override fun onBindViewHolder(holder: BrowserMenuItemViewHolder, position: Int) {
+ visibleItems[position].bind(menu!!, holder.itemView)
+ }
+
+ fun invalidate(recyclerView: RecyclerView) {
+ visibleItems.withIndex().forEach {
+ val (index, item) = it
+ recyclerView.findViewHolderForAdapterPosition(index)?.apply {
+ item.invalidate(itemView)
+ }
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ override fun isStickyItem(position: Int): Boolean {
+ return try {
+ visibleItems[position].isSticky
+ } catch (e: IndexOutOfBoundsException) {
+ false
+ }
+ }
+
+ override fun setupStickyItem(stickyItem: View) {
+ menu?.let {
+ stickyItem.setBackgroundColor(it.backgroundColor)
+ }
+ }
+
+ override fun tearDownStickyItem(stickyItem: View) {
+ stickyItem.setBackgroundColor(Color.TRANSPARENT)
+ }
+}
+
+class BrowserMenuItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuBuilder.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuBuilder.kt
new file mode 100644
index 0000000000..fb5e917fa5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuBuilder.kt
@@ -0,0 +1,29 @@
+/* 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.browser.menu
+
+import android.content.Context
+
+/**
+ * Helper class for building browser menus.
+ *
+ * @param items List of BrowserMenuItem objects to compose the menu from.
+ * @param extras Map of extra values that are added to emitted facts
+ * @param endOfMenuAlwaysVisible when is set to true makes sure the bottom of the menu is always visible otherwise,
+ * the top of the menu is always visible.
+ */
+open class BrowserMenuBuilder(
+ val items: List<BrowserMenuItem>,
+ val extras: Map<String, Any> = emptyMap(),
+ val endOfMenuAlwaysVisible: Boolean = false,
+) {
+ /**
+ * Builds and returns a browser menu with [items]
+ */
+ open fun build(context: Context): BrowserMenu {
+ val adapter = BrowserMenuAdapter(context, items)
+ return BrowserMenu(adapter)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuHighlight.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuHighlight.kt
new file mode 100644
index 0000000000..881eff83b5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuHighlight.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.browser.menu
+
+import android.content.Context
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.core.content.ContextCompat
+import mozilla.components.browser.menu.item.NO_ID
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuEffect
+
+/**
+ * Describes how to display a [mozilla.components.browser.menu.item.BrowserMenuHighlightableItem]
+ * when it is highlighted.
+ */
+sealed class BrowserMenuHighlight {
+ abstract val label: String?
+ abstract val canPropagate: Boolean
+
+ /**
+ * Converts the highlight into a corresponding [MenuEffect] from concept-menu.
+ */
+ abstract fun asEffect(context: Context): MenuEffect
+
+ /**
+ * Displays a notification dot.
+ * Used for highlighting new features to the user, such as what's new or a recommended feature.
+ *
+ * @property notificationTint Tint for the notification dot displayed on the icon and menu button.
+ * @property label Label to override the normal label of the menu item.
+ * @property canPropagate Indicate whether other components should consider this highlight when
+ * displaying their own highlight.
+ */
+ data class LowPriority(
+ @ColorInt val notificationTint: Int,
+ override val label: String? = null,
+ override val canPropagate: Boolean = true,
+ ) : BrowserMenuHighlight() {
+ override fun asEffect(context: Context) = LowPriorityHighlightEffect(
+ notificationTint = notificationTint,
+ )
+ }
+
+ /**
+ * Changes the background of the menu item.
+ * Used for errors that require user attention, like sync errors.
+ *
+ * @property backgroundTint Tint for the menu item background color.
+ * Also used to highlight the menu button.
+ * @property label Label to override the normal label of the menu item.
+ * @property endImageResource Icon to display at the end of the menu item when highlighted.
+ * @property canPropagate Indicate whether other components should consider this highlight when
+ * displaying their own highlight.
+ */
+ data class HighPriority(
+ @ColorInt val backgroundTint: Int,
+ override val label: String? = null,
+ val endImageResource: Int = NO_ID,
+ override val canPropagate: Boolean = true,
+ ) : BrowserMenuHighlight() {
+ override fun asEffect(context: Context) = HighPriorityHighlightEffect(
+ backgroundTint = backgroundTint,
+ )
+ }
+
+ /**
+ * Described how to display a highlightable menu item when it is highlighted.
+ * Replaced by [LowPriority] and [HighPriority] which lets a priority be specified.
+ * This class only exists so that [mozilla.components.browser.menu.item.BrowserMenuHighlightableItem.Highlight]
+ * can subclass it.
+ *
+ * @property canPropagate Indicate whether other components should consider this highlight when
+ * displaying their own highlight.
+ */
+ @Deprecated("Replace with LowPriority or HighPriority highlight")
+ open class ClassicHighlight(
+ @DrawableRes val startImageResource: Int,
+ @DrawableRes val endImageResource: Int,
+ @DrawableRes val backgroundResource: Int,
+ @ColorRes val colorResource: Int,
+ override val canPropagate: Boolean = true,
+ ) : BrowserMenuHighlight() {
+ override val label: String? = null
+
+ override fun asEffect(context: Context) = HighPriorityHighlightEffect(
+ backgroundTint = ContextCompat.getColor(context, colorResource),
+ )
+ }
+}
+
+/**
+ * Indicates that a menu item shows a highlight.
+ */
+interface HighlightableMenuItem {
+ /**
+ * Highlight object representing how the menu item will be displayed when highlighted.
+ */
+ val highlight: BrowserMenuHighlight
+
+ /**
+ * Whether or not to display the highlight
+ */
+ val isHighlighted: () -> Boolean
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuItem.kt
new file mode 100644
index 0000000000..b9dd7114d7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuItem.kt
@@ -0,0 +1,64 @@
+/* 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.browser.menu
+
+import android.content.Context
+import android.view.View
+import mozilla.components.browser.menu.view.ExpandableLayout
+import mozilla.components.browser.menu.view.StickyItemsLinearLayoutManager
+import mozilla.components.concept.menu.candidate.MenuCandidate
+
+/**
+ * Interface to be implemented by menu items to be shown in the browser menu.
+ */
+interface BrowserMenuItem {
+ /**
+ * Lambda expression that returns true if this item should be shown in the menu. Returns false
+ * if this item should be hidden.
+ */
+ val visible: () -> Boolean
+
+ /**
+ * Lambda expression that returns the number of interactive elements in this menu item.
+ * For example, a simple item will have 1, divider will have 0, and a composite
+ * item, like a tool bar, will have several.
+ */
+ val interactiveCount: () -> Int get() = { 1 }
+
+ /**
+ * Whether this menu item can serve as the limit of a collapsing menu.
+ *
+ * @see [ExpandableLayout]
+ */
+ val isCollapsingMenuLimit: Boolean get() = false
+
+ /**
+ * Whether this menu item should not be scrollable off-screen.
+ *
+ * @see [StickyItemsLinearLayoutManager]
+ */
+ val isSticky: Boolean get() = false
+
+ /**
+ * Returns the layout resource ID of the layout to be inflated for showing a menu item of this
+ * type.
+ */
+ fun getLayoutResource(): Int
+
+ /**
+ * Called by the browser menu to display the data of this item using the passed view.
+ */
+ fun bind(menu: BrowserMenu, view: View)
+
+ /**
+ * Called by the browser menu to update the displayed data of this item using the passed view.
+ */
+ fun invalidate(view: View) = Unit
+
+ /**
+ * Converts the menu item into a menu candidate.
+ */
+ fun asCandidate(context: Context): MenuCandidate? = null
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPlacement.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPlacement.kt
new file mode 100644
index 0000000000..4a0c4cc06f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPlacement.kt
@@ -0,0 +1,64 @@
+/* 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.browser.menu
+
+import android.view.View
+
+/**
+ * Configuration of where and how a PopupWindow for a menu should be displayed.
+ */
+internal sealed class BrowserMenuPlacement {
+ /**
+ * Android View that the PopupWindow should be anchored to.
+ */
+ abstract val anchor: View
+
+ /**
+ * Menu position specific animation to be used when showing the PopupWindow.
+ */
+ abstract val animation: Int
+
+ /**
+ * Menu placed below the anchor. Anchored to the top.
+ */
+ class AnchoredToTop {
+ /**
+ * The PopupWindow should be anchored to the top and shown as a dropdown.
+ */
+ data class Dropdown(
+ override val anchor: View,
+ override val animation: Int = R.style.Mozac_Browser_Menu_Animation_OverflowMenuTop,
+ ) : BrowserMenuPlacement()
+
+ /**
+ * The PopupWindow should be anchored to the top and placed at a specific location.
+ */
+ data class ManualAnchoring(
+ override val anchor: View,
+ override val animation: Int = R.style.Mozac_Browser_Menu_Animation_OverflowMenuTop,
+ ) : BrowserMenuPlacement()
+ }
+
+ /**
+ * Menu placed above the anchor. Anchored to the bottom.
+ */
+ class AnchoredToBottom {
+ /**
+ * The PopupWindow should be anchored to the bottom and shown as a dropdown.
+ */
+ data class Dropdown(
+ override val anchor: View,
+ override val animation: Int = R.style.Mozac_Browser_Menu_Animation_OverflowMenuBottom,
+ ) : BrowserMenuPlacement()
+
+ /**
+ * The PopupWindow should be anchored to the bottom and placed at a specific location.
+ */
+ data class ManualAnchoring(
+ override val anchor: View,
+ override val animation: Int = R.style.Mozac_Browser_Menu_Animation_OverflowMenuBottom,
+ ) : BrowserMenuPlacement()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPositioning.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPositioning.kt
new file mode 100644
index 0000000000..b6bce41f6b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPositioning.kt
@@ -0,0 +1,143 @@
+/* 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/. */
+
+@file:Suppress("MatchingDeclarationName")
+
+package mozilla.components.browser.menu
+
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.Px
+
+/**
+ * All data needed for menu positioning.
+ */
+internal data class MenuPositioningData(
+ /**
+ * Where and how should the menu be placed in relation to the [BrowserMenuPlacement.anchor].
+ */
+ val inferredMenuPlacement: BrowserMenuPlacement? = null,
+
+ /**
+ * The orientation asked by users of this class when initializing it.
+ */
+ val askedOrientation: BrowserMenu.Orientation = BrowserMenu.Orientation.DOWN,
+
+ /**
+ * Whether the menu fits in the space between [display top, anchor] in a top - down layout.
+ */
+ val fitsUp: Boolean = false,
+
+ /**
+ * Whether the menu fits in the space between [anchor, display top] in a top - down layout.
+ */
+ val fitsDown: Boolean = false,
+
+ /**
+ * Distance between [display top, anchor top margin]. Used for better positioning the menu.
+ */
+ @Px val availableHeightToTop: Int = 0,
+
+ /**
+ * Distance between [display bottom, anchor bottom margin]. Used for better positioning the menu.
+ */
+ @Px val availableHeightToBottom: Int = 0,
+
+ /**
+ * [View#measuredHeight] of the menu. May be bigger than the available screen height.
+ */
+ @Px val containerViewHeight: Int = 0,
+)
+
+/**
+ * Measure, calculate, obtain all data needed to know how the menu shown in a PopupWindow should be positioned.
+ *
+ * This method assumes [currentData] already contains the [MenuPositioningData.askedOrientation].
+ *
+ * @param containerView the menu layout that will be wrapped in the PopupWindow.
+ * @param anchor view the PopupWindow will be aligned to.
+ * @param currentData current known data for how the menu should be positioned.
+ *
+ * @return new [MenuPositioningData] containing the current constraints of the PopupWindow.
+ */
+internal fun inferMenuPositioningData(
+ containerView: ViewGroup,
+ anchor: View,
+ currentData: MenuPositioningData,
+): MenuPositioningData {
+ // Measure the menu allowing it to expand entirely.
+ val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+ containerView.measure(spec, spec)
+
+ val (availableHeightToTop, availableHeightToBottom) = getMaxAvailableHeightToTopAndBottom(anchor)
+ val containerHeight = containerView.measuredHeight
+
+ val fitsUp = availableHeightToTop >= containerHeight || availableHeightToTop > availableHeightToBottom
+ val fitsDown = availableHeightToBottom >= containerHeight || availableHeightToBottom > availableHeightToTop
+
+ return inferMenuPosition(
+ anchor,
+ currentData.copy(
+ fitsUp = fitsUp,
+ fitsDown = fitsDown,
+ availableHeightToTop = availableHeightToTop,
+ availableHeightToBottom = availableHeightToBottom,
+ containerViewHeight = containerHeight,
+ ),
+ )
+}
+
+/**
+ * Infer where and how the PopupWindow should be shown based on the data available in [currentData].
+ * Should be called only once per menu to be shown.
+ *
+ * @param anchor view the PopupWindow will be aligned to.
+ * @param currentData current known data for how the menu should be positioned.
+ *
+ * @return new MenuPositioningData updated to contain the inferred [BrowserMenuPlacement]
+ */
+internal fun inferMenuPosition(anchor: View, currentData: MenuPositioningData): MenuPositioningData {
+ // Try to use the preferred orientation, if doesn't fit fallback to the best fit.
+
+ val menuPlacement: BrowserMenuPlacement =
+ if (currentData.askedOrientation == BrowserMenu.Orientation.DOWN && currentData.fitsDown) {
+ BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor)
+ } else if (currentData.askedOrientation == BrowserMenu.Orientation.UP && currentData.fitsUp) {
+ BrowserMenuPlacement.AnchoredToBottom.Dropdown(anchor)
+ } else {
+ if (!currentData.fitsUp && !currentData.fitsDown) {
+ if (currentData.availableHeightToTop < currentData.availableHeightToBottom) {
+ BrowserMenuPlacement.AnchoredToTop.ManualAnchoring(anchor)
+ } else {
+ BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring(anchor)
+ }
+ } else {
+ if (currentData.fitsDown) {
+ BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor)
+ } else {
+ BrowserMenuPlacement.AnchoredToBottom.Dropdown(anchor)
+ }
+ }
+ }
+
+ return currentData.copy(inferredMenuPlacement = menuPlacement)
+}
+
+private fun getMaxAvailableHeightToTopAndBottom(anchor: View): Pair<Int, Int> {
+ val anchorPosition = IntArray(2)
+ val displayFrame = Rect()
+
+ val appView = anchor.rootView
+ appView.getWindowVisibleDisplayFrame(displayFrame)
+
+ anchor.getLocationOnScreen(anchorPosition)
+
+ val bottomEdge = displayFrame.bottom
+
+ val distanceToBottom = bottomEdge - (anchorPosition[1] + anchor.height)
+ val distanceToTop = anchorPosition[1] - displayFrame.top
+
+ return distanceToTop to distanceToBottom
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenu.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenu.kt
new file mode 100644
index 0000000000..afebfb14dd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenu.kt
@@ -0,0 +1,141 @@
+/* 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.browser.menu
+
+import android.view.View
+import android.widget.PopupWindow
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import mozilla.components.browser.menu.facts.emitOpenMenuItemFact
+import mozilla.components.browser.menu.item.WebExtensionBrowserMenuItem
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.lib.state.ext.flowScoped
+
+/**
+ * A [BrowserMenu] capable of displaying browser and page actions from web extensions.
+ */
+class WebExtensionBrowserMenu internal constructor(
+ adapter: BrowserMenuAdapter,
+ private val store: BrowserStore,
+) : BrowserMenu(adapter) {
+ private var scope: CoroutineScope? = null
+
+ override fun show(
+ anchor: View,
+ orientation: Orientation,
+ style: MenuStyle?,
+ endOfMenuAlwaysVisible: Boolean,
+ onDismiss: () -> Unit,
+ ): PopupWindow {
+ scope = store.flowScoped { flow ->
+ flow.distinctUntilChangedBy { it.selectedTab }
+ .collect { state ->
+ getOrUpdateWebExtensionMenuItems(state, state.selectedTab)
+ invalidate()
+ }
+ }
+
+ return super.show(
+ anchor,
+ orientation,
+ style,
+ endOfMenuAlwaysVisible,
+ onDismiss,
+ ).apply {
+ setOnDismissListener {
+ adapter.menu = null
+ currentPopup = null
+ scope?.cancel()
+ webExtensionBrowserActions.clear()
+ webExtensionPageActions.clear()
+ onDismiss()
+ }
+ }
+ }
+
+ companion object {
+ internal val webExtensionBrowserActions = HashMap<String, WebExtensionBrowserMenuItem>()
+ internal val webExtensionPageActions = HashMap<String, WebExtensionBrowserMenuItem>()
+
+ internal fun getOrUpdateWebExtensionMenuItems(
+ state: BrowserState,
+ tab: SessionState? = null,
+ ): List<WebExtensionBrowserMenuItem> {
+ val menuItems = ArrayList<WebExtensionBrowserMenuItem>()
+ val extensions = state.extensions.values.toList()
+ extensions.filter { it.enabled }.sortedBy { it.name }
+ .forEach { extension ->
+ if (!extension.allowedInPrivateBrowsing && tab?.content?.private == true) {
+ return@forEach
+ }
+
+ extension.browserAction?.let { browserAction ->
+ addOrUpdateAction(
+ extension = extension,
+ globalAction = browserAction,
+ tabAction = tab?.extensionState?.get(extension.id)?.browserAction,
+ menuItems = menuItems,
+ )
+ }
+
+ extension.pageAction?.let { pageAction ->
+ val tabPageAction = tab?.extensionState?.get(extension.id)?.pageAction
+
+ // Unlike browser actions, page actions are not displayed by default (only if enabled):
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action
+ if (pageAction.copyWithOverride(tabPageAction).enabled == true) {
+ addOrUpdateAction(
+ extension = extension,
+ globalAction = pageAction,
+ tabAction = tabPageAction,
+ menuItems = menuItems,
+ isPageAction = true,
+ )
+ }
+ }
+ }
+
+ return menuItems
+ }
+
+ private fun addOrUpdateAction(
+ extension: WebExtensionState,
+ globalAction: Action,
+ tabAction: Action?,
+ menuItems: ArrayList<WebExtensionBrowserMenuItem>,
+ isPageAction: Boolean = false,
+ ): Boolean {
+ val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions
+
+ // Add the global browser/page action if it doesn't exist
+ val browserMenuItem = actionMap.getOrPut(extension.id) {
+ val listener = {
+ emitOpenMenuItemFact(extension.id)
+ globalAction.onClick()
+ }
+ val browserMenuItem = WebExtensionBrowserMenuItem(
+ action = globalAction,
+ listener = listener,
+ id = extension.id,
+ )
+ browserMenuItem
+ }
+
+ // Apply tab-specific override of browser/page action
+ tabAction?.let {
+ browserMenuItem.action = globalAction.copyWithOverride(it)
+ }
+
+ return menuItems.add(browserMenuItem)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilder.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilder.kt
new file mode 100644
index 0000000000..494014bf66
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilder.kt
@@ -0,0 +1,166 @@
+/* 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.browser.menu
+
+import android.content.Context
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import mozilla.components.browser.menu.item.BackPressMenuItem
+import mozilla.components.browser.menu.item.BrowserMenuDivider
+import mozilla.components.browser.menu.item.BrowserMenuImageText
+import mozilla.components.browser.menu.item.NO_ID
+import mozilla.components.browser.menu.item.ParentBrowserMenuItem
+import mozilla.components.browser.menu.item.WebExtensionBrowserMenuItem
+import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * Browser menu builder with web extension support. It allows [WebExtensionBrowserMenu] to add
+ * web extension browser actions in a nested menu item. If there are no web extensions installed
+ * and @param showAddonsInMenu is true the web extension menu item would return an add-on manager menu item instead.
+ *
+ * @param store [BrowserStore] required to render web extension browser actions
+ * @param style Indicates how items should look like.
+ * @param onAddonsManagerTapped Callback to be invoked when add-ons manager menu item is selected.
+ * @param appendExtensionSubMenuAtStart Used when the menu does not have a [WebExtensionPlaceholderMenuItem]
+ * to specify the place the extensions sub-menu should be inserted. True if web extension sub menu
+ * appear at the top (start) of the menu, false if web extensions appear at the bottom of the menu.
+ * Default to false (bottom). This is also used to decide the back press menu item placement at top or bottom.
+ * @param showAddonsInMenu Whether to show the 'Add-ons' item in menu
+ */
+class WebExtensionBrowserMenuBuilder(
+ items: List<BrowserMenuItem>,
+ extras: Map<String, Any> = emptyMap(),
+ endOfMenuAlwaysVisible: Boolean = false,
+ private val store: BrowserStore,
+ private val style: Style = Style(),
+ private val onAddonsManagerTapped: () -> Unit = {},
+ private val appendExtensionSubMenuAtStart: Boolean = false,
+ private val showAddonsInMenu: Boolean = true,
+) : BrowserMenuBuilder(items, extras, endOfMenuAlwaysVisible) {
+
+ /**
+ * Builds and returns a browser menu with combination of [items] and web extension browser actions.
+ */
+ override fun build(context: Context): BrowserMenu {
+ val extensionMenuItems =
+ WebExtensionBrowserMenu.getOrUpdateWebExtensionMenuItems(store.state, store.state.selectedTab)
+
+ val finalList = items.toMutableList()
+
+ val filteredExtensionMenuItems = extensionMenuItems.filter { webExtensionBrowserMenuItem ->
+ replaceMenuPlaceholderWithExtensions(finalList, webExtensionBrowserMenuItem)
+ }
+
+ val menuItems = if (showAddonsInMenu) {
+ createAddonsMenuItems(context, finalList, filteredExtensionMenuItems)
+ } else {
+ finalList
+ }
+
+ val adapter = BrowserMenuAdapter(context, menuItems)
+ return BrowserMenu(adapter)
+ }
+
+ private fun replaceMenuPlaceholderWithExtensions(
+ items: MutableList<BrowserMenuItem>,
+ menuItem: WebExtensionBrowserMenuItem,
+ ): Boolean {
+ // Check if we have a placeholder
+ val index = items.indexOfFirst { browserMenuItem ->
+ (browserMenuItem as? WebExtensionPlaceholderMenuItem)?.id == menuItem.id
+ }
+ // Replace placeholder with corresponding web extension, and remove it from extensions menu list
+ if (index != -1) {
+ menuItem.setIconTint(
+ (items[index] as? WebExtensionPlaceholderMenuItem)?.iconTintColorResource,
+ )
+ items[index] = menuItem
+ }
+ return index == -1
+ }
+
+ private fun createAddonsMenuItems(
+ context: Context,
+ items: MutableList<BrowserMenuItem>,
+ filteredExtensionMenuItems: List<WebExtensionBrowserMenuItem>,
+ ): List<BrowserMenuItem> {
+ val addonsMenuItem = if (filteredExtensionMenuItems.isNotEmpty()) {
+ val backPressMenuItem = BackPressMenuItem(
+ contentDescription = context.getString(R.string.mozac_browser_menu_extensions_content_description),
+ label = context.getString(R.string.mozac_browser_menu_extensions),
+ imageResource = style.backPressMenuItemDrawableRes,
+ iconTintColorResource = style.webExtIconTintColorResource,
+ )
+
+ val addonsManagerMenuItem = BrowserMenuImageText(
+ label = context.getString(R.string.mozac_browser_menu_extensions_manager),
+ imageResource = style.addonsManagerMenuItemDrawableRes,
+ iconTintColorResource = style.webExtIconTintColorResource,
+ ) {
+ onAddonsManagerTapped.invoke()
+ }
+
+ val webExtSubMenuItems = if (appendExtensionSubMenuAtStart) {
+ listOf(backPressMenuItem) + BrowserMenuDivider() +
+ filteredExtensionMenuItems +
+ BrowserMenuDivider() + addonsManagerMenuItem
+ } else {
+ listOf(addonsManagerMenuItem) + BrowserMenuDivider() +
+ filteredExtensionMenuItems +
+ BrowserMenuDivider() + backPressMenuItem
+ }
+
+ val webExtBrowserMenuAdapter = BrowserMenuAdapter(context, webExtSubMenuItems)
+ val webExtMenu = WebExtensionBrowserMenu(webExtBrowserMenuAdapter, store)
+
+ ParentBrowserMenuItem(
+ label = context.getString(R.string.mozac_browser_menu_extensions),
+ imageResource = style.addonsManagerMenuItemDrawableRes,
+ iconTintColorResource = style.webExtIconTintColorResource,
+ subMenu = webExtMenu,
+ endOfMenuAlwaysVisible = endOfMenuAlwaysVisible,
+ )
+ } else {
+ BrowserMenuImageText(
+ label = context.getString(R.string.mozac_browser_menu_extensions),
+ imageResource = style.addonsManagerMenuItemDrawableRes,
+ iconTintColorResource = style.webExtIconTintColorResource,
+ ) {
+ onAddonsManagerTapped.invoke()
+ }
+ }
+ val mainMenuIndex = items.indexOfFirst { browserMenuItem ->
+ (browserMenuItem as? WebExtensionPlaceholderMenuItem)?.id ==
+ WebExtensionPlaceholderMenuItem.MAIN_EXTENSIONS_MENU_ID
+ }
+
+ return if (mainMenuIndex != -1) {
+ items[mainMenuIndex] = addonsMenuItem
+ items
+ // if we do not have a placeholder we should add the extension submenu at top or bottom
+ } else {
+ if (appendExtensionSubMenuAtStart) {
+ listOf(addonsMenuItem) + items
+ } else {
+ items + addonsMenuItem
+ }
+ }
+ }
+
+ /**
+ * Allows to customize how items should look like.
+ */
+ data class Style(
+ @ColorRes
+ val webExtIconTintColorResource: Int = NO_ID,
+ @DrawableRes
+ val backPressMenuItemDrawableRes: Int = iconsR.drawable.mozac_ic_back_24,
+ @DrawableRes
+ val addonsManagerMenuItemDrawableRes: Int = iconsR.drawable.mozac_ic_extension_24,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/BrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/BrowserMenuItem.kt
new file mode 100644
index 0000000000..2411c19cc8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/BrowserMenuItem.kt
@@ -0,0 +1,35 @@
+/* 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.browser.menu.ext
+
+import android.content.Context
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.HighlightableMenuItem
+
+/**
+ * Get the highlight effect present in the list of menu items, if any.
+ */
+@Suppress("Deprecation")
+fun List<BrowserMenuItem>.getHighlight() = asSequence()
+ .filter { it.visible() }
+ .mapNotNull { it as? HighlightableMenuItem }
+ .filter { it.isHighlighted() }
+ .map { it.highlight }
+ .filter { it.canPropagate }
+ .maxByOrNull {
+ // Select the highlight with the highest priority
+ when (it) {
+ is BrowserMenuHighlight.HighPriority -> 2
+ is BrowserMenuHighlight.LowPriority -> 1
+ is BrowserMenuHighlight.ClassicHighlight -> 0
+ }
+ }
+
+/**
+ * Converts the menu items into a menu candidate list.
+ */
+fun List<BrowserMenuItem>.asCandidateList(context: Context) =
+ mapNotNull { it.asCandidate(context) }
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/View.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/View.kt
new file mode 100644
index 0000000000..39e0e647c7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/View.kt
@@ -0,0 +1,16 @@
+/* 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.browser.menu.ext
+
+import android.util.TypedValue
+import android.view.View
+
+/**
+ * Adds ripple effect to the view
+ */
+fun View.addRippleEffect() = with(TypedValue()) {
+ context.theme.resolveAttribute(android.R.attr.selectableItemBackground, this, true)
+ setBackgroundResource(resourceId)
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/facts/BrowserMenuFacts.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/facts/BrowserMenuFacts.kt
new file mode 100644
index 0000000000..e35e3840f0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/facts/BrowserMenuFacts.kt
@@ -0,0 +1,45 @@
+/* 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.browser.menu.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [BrowserMenu].
+ */
+class BrowserMenuFacts {
+ /**
+ * Items that specify which portion of the [BrowserMenu] was interacted with.
+ */
+ object Items {
+ const val WEB_EXTENSION_MENU_ITEM = "web_extension_menu_item"
+ }
+}
+
+private fun emitMenuFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.BROWSER_MENU,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitOpenMenuItemFact(extensionId: String) {
+ emitMenuFact(
+ Action.CLICK,
+ BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM,
+ metadata = mapOf("id" to extensionId),
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItem.kt
new file mode 100644
index 0000000000..729cdfdec5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItem.kt
@@ -0,0 +1,66 @@
+/* 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.browser.menu.item
+
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+
+/**
+ * An abstract menu item for handling nested sub menu items on view click.
+ *
+ * @param subMenu Target sub menu to be shown when this menu item is clicked.
+ * @param endOfMenuAlwaysVisible when is set to true makes sure the bottom of the menu is always visible
+ * otherwise, the top of the menu is always visible.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ */
+abstract class AbstractParentBrowserMenuItem(
+ private val subMenu: BrowserMenu,
+ private val endOfMenuAlwaysVisible: Boolean,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+) : BrowserMenuItem {
+ /**
+ * Listener called when the sub menu is shown.
+ */
+ var onSubMenuShow: () -> Unit = {}
+
+ /**
+ * Listener called when the sub menu is dismissed.
+ */
+ var onSubMenuDismiss: () -> Unit = {}
+ abstract override var visible: () -> Boolean
+ abstract override fun getLayoutResource(): Int
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ view.setOnClickListener {
+ menu.dismiss()
+ subMenu.show(
+ anchor = menu.currAnchor ?: view,
+ orientation = BrowserMenu.determineMenuOrientation(view.parent as? View?),
+ endOfMenuAlwaysVisible = endOfMenuAlwaysVisible,
+ ) {
+ onSubMenuDismiss()
+ }
+ onSubMenuShow()
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal fun onBackPressed(menu: BrowserMenu, view: View) {
+ if (subMenu.isShown) {
+ subMenu.dismiss()
+ onSubMenuDismiss()
+ menu.show(
+ anchor = menu.currAnchor ?: view,
+ orientation = BrowserMenu.determineMenuOrientation(view.parent as? View?),
+ endOfMenuAlwaysVisible = endOfMenuAlwaysVisible,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BackPressMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BackPressMenuItem.kt
new file mode 100644
index 0000000000..c04a91ecf2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BackPressMenuItem.kt
@@ -0,0 +1,72 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import android.view.View.AccessibilityDelegate
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.Button
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+/**
+ * A back press menu item for a nested sub menu entry.
+ *
+ * @param backPressListener Callback to be invoked when the back press menu item is clicked.
+ */
+class BackPressMenuItem(
+ val contentDescription: String,
+ label: String,
+ @DrawableRes
+ imageResource: Int,
+ @ColorRes
+ iconTintColorResource: Int = NO_ID,
+ @ColorRes
+ textColorResource: Int = NO_ID,
+ private var backPressListener: () -> Unit = {},
+) : BrowserMenuImageText(label, imageResource, iconTintColorResource, textColorResource) {
+
+ /**
+ * Binds the view according to its super, but use [backPressListener] for on view clicks.
+ */
+ override fun bind(menu: BrowserMenu, view: View) {
+ super.bind(menu, view)
+
+ view.setOnClickListener {
+ backPressListener.invoke()
+ menu.dismiss()
+ }
+ view.accessibilityDelegate = object : AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.className = Button::class.java.name
+ }
+ }
+ view.contentDescription = contentDescription
+ }
+
+ /**
+ * Sets and replaces the existing [backPressListener] for the back press item.
+ */
+ fun setListener(onClickListener: () -> Unit) {
+ backPressListener = onClickListener
+ }
+
+ override fun asCandidate(context: Context): NestedMenuCandidate {
+ val parentCandidate = super.asCandidate(context) as TextMenuCandidate
+ return NestedMenuCandidate(
+ id = hashCode(),
+ text = parentCandidate.text,
+ start = parentCandidate.start,
+ subMenuItems = null,
+ textStyle = parentCandidate.textStyle,
+ containerStyle = parentCandidate.containerStyle,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCategory.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCategory.kt
new file mode 100644
index 0000000000..9c0b29bbee
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCategory.kt
@@ -0,0 +1,81 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.graphics.Typeface
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat.getColor
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextAlignment
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.concept.menu.candidate.TypefaceStyle
+
+/**
+ * A browser menu item displaying styleable text, usable for menu categories
+ *
+ * @param label The visible label of this menu item.
+ * @param textSize: The size of the label.
+ * @param textColorResource: The color resource to apply to the text.
+ * @param backgroundColorResource: The color resource to apply to the item background.
+ * @param textStyle: The style to apply to the text.
+ * @param textAlignment The alignment of text
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ */
+class BrowserMenuCategory(
+ internal val label: String,
+ private val textSize: Float = NO_ID.toFloat(),
+ @ColorRes
+ private val textColorResource: Int = NO_ID,
+ @ColorRes
+ private val backgroundColorResource: Int = NO_ID,
+ @TypefaceStyle private val textStyle: Int = Typeface.BOLD,
+ @TextAlignment private val textAlignment: Int = View.TEXT_ALIGNMENT_VIEW_START,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+) : BrowserMenuItem {
+ override var visible: () -> Boolean = { true }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_category
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ val textView = view as TextView
+ textView.text = label
+
+ if (textSize != NO_ID.toFloat()) {
+ textView.textSize = textSize
+ }
+
+ if (textColorResource != NO_ID) {
+ textView.setColorResource(textColorResource)
+ }
+
+ textView.setTypeface(textView.typeface, textStyle)
+ textView.textAlignment = textAlignment
+
+ if (backgroundColorResource != NO_ID) {
+ view.setBackgroundResource(backgroundColorResource)
+ }
+ }
+
+ override fun asCandidate(context: Context) = DecorativeTextMenuCandidate(
+ label,
+ textStyle = TextStyle(
+ size = if (textSize == NO_ID.toFloat()) null else textSize,
+ color = if (textColorResource == NO_ID) null else getColor(context, textColorResource),
+ textStyle = textStyle,
+ textAlignment = textAlignment,
+ ),
+ containerStyle = ContainerStyle(isVisible = visible()),
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCheckbox.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCheckbox.kt
new file mode 100644
index 0000000000..e271ecd0bd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCheckbox.kt
@@ -0,0 +1,33 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+
+/**
+ * A simple browser menu checkbox.
+ *
+ * @param label The visible label of this menu item.
+ * @param initialState The initial value the checkbox should have.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param listener Callback to be invoked when this menu item is checked.
+ */
+class BrowserMenuCheckbox(
+ label: String,
+ initialState: () -> Boolean = { false },
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ listener: (Boolean) -> Unit,
+) : BrowserMenuCompoundButton(label, isCollapsingMenuLimit, isSticky, initialState, listener) {
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_checkbox
+
+ override fun asCandidate(context: Context) = super.asCandidate(context).copy(
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButton.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButton.kt
new file mode 100644
index 0000000000..09071b4c39
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButton.kt
@@ -0,0 +1,72 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import android.view.ViewTreeObserver
+import android.widget.CompoundButton
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.ContainerStyle
+
+/**
+ * A browser menu compound button. A basic sub-class would only have to provide a layout resource to
+ * satisfy [BrowserMenuItem.getLayoutResource] which contains a [View] that inherits from [CompoundButton].
+ *
+ * @param label The visible label of this menu item.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param initialState The initial value the checkbox should have.
+ * @param listener Callback to be invoked when this menu item is checked.
+ */
+abstract class BrowserMenuCompoundButton(
+ internal val label: String,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ private val initialState: () -> Boolean = { false },
+ private val listener: (Boolean) -> Unit,
+) : BrowserMenuItem {
+ override var visible: () -> Boolean = { true }
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ // A CompoundButton containing CompoundDrawables needs to know where to place them (LTR / RTL).
+ // If the View is not yet attached to Window the direction inference will fail and the menu item
+ // will return from it's onMeasure a width smaller with the size + padding of the compound drawables.
+ // Work around this by setting a valid layout direction and reset it to inherit from parent later.
+ if (!view.isAttachedToWindow) {
+ view.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+
+ view.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ view.viewTreeObserver.removeOnPreDrawListener(this)
+ view.layoutDirection = View.LAYOUT_DIRECTION_INHERIT
+ return true
+ }
+ },
+ )
+ }
+
+ (view as CompoundButton).apply {
+ text = label
+ isChecked = initialState()
+ setOnCheckedChangeListener { _, checked ->
+ listener(checked)
+ menu.dismiss()
+ }
+ }
+ }
+
+ override fun asCandidate(context: Context) = CompoundMenuCandidate(
+ label,
+ isChecked = initialState(),
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ containerStyle = ContainerStyle(isVisible = visible()),
+ onCheckedChange = listener,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuDivider.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuDivider.kt
new file mode 100644
index 0000000000..6e16b12f53
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuDivider.kt
@@ -0,0 +1,30 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+
+/**
+ * A browser menu item to display a horizontal divider.
+ */
+class BrowserMenuDivider : BrowserMenuItem {
+ override var visible: () -> Boolean = { true }
+
+ override val interactiveCount: () -> Int = { 0 }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_divider
+
+ override fun bind(menu: BrowserMenu, view: View) = Unit
+
+ override fun asCandidate(context: Context) = DividerMenuCandidate(
+ containerStyle = ContainerStyle(isVisible = visible()),
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItem.kt
new file mode 100644
index 0000000000..e21dccf827
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItem.kt
@@ -0,0 +1,212 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageView
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.HighlightableMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+@Suppress("Deprecation")
+private val defaultHighlight = BrowserMenuHighlightableItem.Highlight(0, 0, 0, 0)
+
+/**
+ * A menu item for displaying text with an image icon and a highlight state which sets the
+ * background of the menu item and a second image icon to the right of the text.
+ *
+ * @param label The default visible label of this menu item.
+ * @param startImageResource ID of a drawable resource to be shown as a leftmost icon.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ * @param textColorResource Optional ID of color resource to tint the text.
+ * @param enabled Sets the enabled status for the view. By default, it is true.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param highlight Highlight object representing how the menu item will be displayed when highlighted.
+ * @param isHighlighted Whether or not to display the highlight
+ * @param listener Callback to be invoked when this menu item is clicked.
+ */
+class BrowserMenuHighlightableItem(
+ private val label: String,
+ @DrawableRes private val startImageResource: Int,
+ @ColorRes iconTintColorResource: Int = NO_ID,
+ @ColorRes private val textColorResource: Int = NO_ID,
+ enabled: Boolean = true,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ override val highlight: BrowserMenuHighlight,
+ override val isHighlighted: () -> Boolean = { true },
+ private val listener: () -> Unit = {},
+) : BrowserMenuImageText(
+ label,
+ startImageResource,
+ iconTintColorResource,
+ textColorResource,
+ enabled,
+ isCollapsingMenuLimit,
+ isSticky,
+ listener,
+),
+ HighlightableMenuItem {
+
+ @Deprecated("Use the new constructor")
+ @Suppress("Deprecation") // Constructor uses old highlight type
+ constructor(
+ label: String,
+ @DrawableRes
+ imageResource: Int,
+ @ColorRes
+ iconTintColorResource: Int = NO_ID,
+ @ColorRes
+ textColorResource: Int = NO_ID,
+ enabled: Boolean = true,
+ isCollapsingMenuLimit: Boolean = false,
+ isSticky: Boolean = false,
+ highlight: Highlight? = null,
+ listener: () -> Unit = {},
+ ) : this(
+ label,
+ imageResource,
+ iconTintColorResource,
+ textColorResource,
+ enabled,
+ isCollapsingMenuLimit,
+ isSticky,
+ highlight ?: defaultHighlight,
+ { highlight != null },
+ listener,
+ )
+
+ private var wasHighlighted = false
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_highlightable_item
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ super.bind(menu, view)
+
+ val endImageView = view.findViewById<AppCompatImageView>(R.id.end_image)
+ endImageView.setTintResource(iconTintColorResource)
+
+ val highlightedTextView = view.findViewById<TextView>(R.id.highlight_text)
+ highlightedTextView.text = highlight.label ?: label
+
+ wasHighlighted = isHighlighted()
+ updateHighlight(view, wasHighlighted)
+ }
+
+ override fun invalidate(view: View) {
+ val isNowHighlighted = isHighlighted()
+ if (isNowHighlighted != wasHighlighted) {
+ wasHighlighted = isNowHighlighted
+ updateHighlight(view, isNowHighlighted)
+ }
+ }
+
+ private fun updateHighlight(view: View, isHighlighted: Boolean) {
+ val startImageView = view.findViewById<AppCompatImageView>(R.id.image)
+ val endImageView = view.findViewById<AppCompatImageView>(R.id.end_image)
+ val notificationDotView = view.findViewById<AppCompatImageView>(R.id.notification_dot)
+ val textView = view.findViewById<TextView>(R.id.text)
+ val highlightedTextView = view.findViewById<TextView>(R.id.highlight_text)
+
+ if (isHighlighted) {
+ @Suppress("Deprecation")
+ when (highlight) {
+ is BrowserMenuHighlight.HighPriority -> {
+ textView.visibility = View.INVISIBLE
+ highlightedTextView.visibility = View.VISIBLE
+ view.setBackgroundColor(highlight.backgroundTint)
+ if (highlight.endImageResource != NO_ID) {
+ endImageView.setImageResource(highlight.endImageResource)
+ }
+ endImageView.visibility = View.VISIBLE
+ }
+ is BrowserMenuHighlight.LowPriority -> {
+ textView.visibility = View.INVISIBLE
+ highlightedTextView.visibility = View.VISIBLE
+ notificationDotView.imageTintList = ColorStateList.valueOf(highlight.notificationTint)
+ notificationDotView.visibility = View.VISIBLE
+ view.contentDescription = "${notificationDotView.contentDescription}, ${textView.text}"
+ }
+ is BrowserMenuHighlight.ClassicHighlight -> {
+ view.setBackgroundResource(highlight.backgroundResource)
+ if (highlight.startImageResource != NO_ID) {
+ startImageView.setImageResource(highlight.startImageResource)
+ }
+ if (highlight.endImageResource != NO_ID) {
+ endImageView.setImageResource(highlight.endImageResource)
+ }
+ endImageView.visibility = View.VISIBLE
+ }
+ }
+ } else {
+ textView.visibility = View.VISIBLE
+ highlightedTextView.visibility = View.INVISIBLE
+ view.background = null
+ endImageView.setImageDrawable(null)
+ endImageView.visibility = View.GONE
+ notificationDotView.visibility = View.GONE
+ }
+ }
+
+ override fun asCandidate(context: Context): TextMenuCandidate {
+ val base = super.asCandidate(context) as TextMenuCandidate
+ if (!isHighlighted()) return base
+
+ @Suppress("Deprecation")
+ return when (highlight) {
+ is BrowserMenuHighlight.HighPriority -> base.copy(
+ text = highlight.label ?: label,
+ end = if (highlight.endImageResource == NO_ID) {
+ null
+ } else {
+ DrawableMenuIcon(
+ context,
+ highlight.endImageResource,
+ )
+ },
+ effect = HighPriorityHighlightEffect(
+ backgroundTint = highlight.backgroundTint,
+ ),
+ )
+ is BrowserMenuHighlight.LowPriority -> base.copy(
+ text = highlight.label ?: label,
+ start = (base.start as? DrawableMenuIcon)?.copy(
+ effect = LowPriorityHighlightEffect(notificationTint = highlight.notificationTint),
+ ),
+ )
+ is BrowserMenuHighlight.ClassicHighlight -> base
+ }
+ }
+
+ /**
+ * Described how to display a [BrowserMenuHighlightableItem] when it is highlighted.
+ * Replaced by [BrowserMenuHighlight] which lets a priority be specified.
+ */
+ @Deprecated("Replace with BrowserMenuHighlight.LowPriority or BrowserMenuHighlight.HighPriority")
+ @Suppress("Deprecation")
+ class Highlight(
+ @DrawableRes startImageResource: Int = NO_ID,
+ @DrawableRes endImageResource: Int = NO_ID,
+ @DrawableRes backgroundResource: Int,
+ @ColorRes colorResource: Int,
+ ) : BrowserMenuHighlight.ClassicHighlight(
+ startImageResource,
+ endImageResource,
+ backgroundResource,
+ colorResource,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitch.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitch.kt
new file mode 100644
index 0000000000..4eb2c1cb8e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitch.kt
@@ -0,0 +1,99 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.View
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.SwitchCompat
+import androidx.core.view.isVisible
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.HighlightableMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+
+/**
+ * A browser menu switch that can show a highlighted icon.
+ *
+ * @param label The visible label of this menu item.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param initialState The initial value the checkbox should have.
+ * @param listener Callback to be invoked when this menu item is checked.
+ */
+class BrowserMenuHighlightableSwitch(
+ label: String,
+ @DrawableRes private val startImageResource: Int,
+ @ColorRes private val iconTintColorResource: Int = NO_ID,
+ @ColorRes private val textColorResource: Int = NO_ID,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ override val highlight: BrowserMenuHighlight.LowPriority,
+ override val isHighlighted: () -> Boolean = { true },
+ initialState: () -> Boolean = { false },
+ listener: (Boolean) -> Unit,
+) : BrowserMenuCompoundButton(label, isCollapsingMenuLimit, isSticky, initialState, listener), HighlightableMenuItem {
+
+ private var wasHighlighted = false
+
+ override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_highlightable_switch
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ super.bind(menu, view.findViewById<SwitchCompat>(R.id.switch_widget))
+ setTints(view)
+
+ val notificationDotView = view.findViewById<AppCompatImageView>(R.id.notification_dot)
+ notificationDotView.imageTintList = ColorStateList.valueOf(highlight.notificationTint)
+
+ wasHighlighted = isHighlighted()
+ updateHighlight(view, wasHighlighted)
+ }
+
+ override fun invalidate(view: View) {
+ val isNowHighlighted = isHighlighted()
+ if (isNowHighlighted != wasHighlighted) {
+ wasHighlighted = isNowHighlighted
+ updateHighlight(view, isNowHighlighted)
+ }
+ }
+
+ private fun setTints(view: View) {
+ val switch = view.findViewById<SwitchCompat>(R.id.switch_widget)
+ switch.setColorResource(textColorResource)
+
+ val imageView = view.findViewById<AppCompatImageView>(R.id.image)
+ imageView.setImageResource(startImageResource)
+ imageView.setTintResource(iconTintColorResource)
+ }
+
+ private fun updateHighlight(view: View, isHighlighted: Boolean) {
+ val notificationDotView = view.findViewById<AppCompatImageView>(R.id.notification_dot)
+ val switch = view.findViewById<SwitchCompat>(R.id.switch_widget)
+
+ notificationDotView.isVisible = isHighlighted
+ switch.text = if (isHighlighted) highlight.label ?: label else label
+ }
+
+ override fun asCandidate(context: Context): CompoundMenuCandidate {
+ val base = super.asCandidate(context)
+ return if (isHighlighted()) {
+ base.copy(
+ text = highlight.label ?: label,
+ start = (base.start as? DrawableMenuIcon)?.copy(
+ effect = LowPriorityHighlightEffect(notificationTint = highlight.notificationTint),
+ ),
+ )
+ } else {
+ base
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitch.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitch.kt
new file mode 100644
index 0000000000..d98afe2bb8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitch.kt
@@ -0,0 +1,59 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.SwitchCompat
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import java.lang.reflect.Modifier
+
+/**
+ * A simple browser menu switch.
+ *
+ * @param imageResource ID of a drawable resource to be shown as icon.
+ * @param label The visible label of this menu item.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param initialState The initial value the checkbox should have.
+ * @param listener Callback to be invoked when this menu item is checked.
+ */
+class BrowserMenuImageSwitch(
+ @get:VisibleForTesting(otherwise = Modifier.PRIVATE)
+ @DrawableRes
+ val imageResource: Int,
+ label: String,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ initialState: () -> Boolean = { false },
+ listener: (Boolean) -> Unit,
+) : BrowserMenuCompoundButton(label, isCollapsingMenuLimit, isSticky, initialState, listener) {
+ override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_image_switch
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ super.bind(menu, view)
+ bindImage(view as SwitchCompat)
+ }
+
+ private fun bindImage(switch: SwitchCompat) {
+ switch.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ imageResource,
+ 0,
+ 0,
+ 0,
+ )
+ }
+
+ override fun asCandidate(context: Context) = super.asCandidate(context).copy(
+ start = DrawableMenuIcon(context, imageResource),
+ end = CompoundMenuCandidate.ButtonType.SWITCH,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageText.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageText.kt
new file mode 100644
index 0000000000..25542f6a4e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageText.kt
@@ -0,0 +1,111 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.getColor
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+
+internal const val NO_ID = -1
+
+internal fun ImageView.setTintResource(@ColorRes tintColorResource: Int) {
+ if (tintColorResource != NO_ID) {
+ imageTintList = ContextCompat.getColorStateList(context, tintColorResource)
+ }
+}
+
+internal fun TextView.setColorResource(@ColorRes textColorResource: Int) {
+ if (textColorResource != NO_ID) {
+ setTextColor(ContextCompat.getColor(context, textColorResource))
+ }
+}
+
+/**
+ * A menu item for displaying text with an image icon.
+ *
+ * @param label The visible label of this menu item.
+ * @param imageResource ID of a drawable resource to be shown as icon.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ * @param textColorResource Optional ID of color resource to tint the text.
+ * @param enabled Sets the enabled status for the view. By default, it is true.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param listener Callback to be invoked when this menu item is clicked.
+ */
+open class BrowserMenuImageText(
+ private val label: String,
+ @DrawableRes
+ internal val imageResource: Int,
+ @ColorRes
+ open var iconTintColorResource: Int = NO_ID,
+ @ColorRes
+ private val textColorResource: Int = NO_ID,
+ open var enabled: Boolean = true,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ private val listener: () -> Unit = {},
+) : BrowserMenuItem {
+
+ override var visible: () -> Boolean = { true }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_image_text
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ bindText(view)
+
+ bindImage(view)
+
+ view.setOnClickListener {
+ listener.invoke()
+ menu.dismiss()
+ }
+ view.isEnabled = enabled
+ view.contentDescription = label
+ }
+
+ private fun bindText(view: View) {
+ val textView = view.findViewById<TextView>(R.id.text)
+ textView.text = label
+ textView.setColorResource(textColorResource)
+ textView.isEnabled = enabled
+ }
+
+ private fun bindImage(view: View) {
+ val imageView = view.findViewById<AppCompatImageView>(R.id.image)
+ with(imageView) {
+ setImageResource(imageResource)
+ setTintResource(iconTintColorResource)
+ }
+ }
+
+ override fun asCandidate(context: Context): MenuCandidate = TextMenuCandidate(
+ label,
+ start = DrawableMenuIcon(
+ context,
+ resource = imageResource,
+ tint = if (iconTintColorResource == NO_ID) null else getColor(context, iconTintColorResource),
+ ),
+ textStyle = TextStyle(
+ color = if (textColorResource == NO_ID) null else getColor(context, textColorResource),
+ ),
+ containerStyle = ContainerStyle(isVisible = visible()),
+ onClick = listener,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButton.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButton.kt
new file mode 100644
index 0000000000..8d75b2a90a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButton.kt
@@ -0,0 +1,111 @@
+/* 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.browser.menu.item
+
+import android.view.View
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatCheckBox
+import androidx.core.content.ContextCompat
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.support.ktx.android.util.dpToPx
+
+/**
+ * A browser menu item with image and label and a custom checkbox.
+ *
+ * @param imageResource ID of a drawable resource to be shown as icon.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ * @param label The visible label of this menu item.
+ * @param textColorResource Optional ID of color resource to tint the text.
+ * @param enabled Sets the enabled status for the view. By default, it is true.
+ * @param labelListener Callback to be invoked when this menu item is clicked.
+ * @param primaryStateIconResource ID of a drawable resource for checkbox drawable in primary state.
+ * @param secondaryStateIconResource ID of a drawable resource for checkbox drawable in secondary state.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param iconTintColorResource Optional ID of color resource to tint the checkbox drawable.
+ * @param primaryLabel The visible label of the checkbox in primary state.
+ * @param secondaryLabel The visible label of this menu item in secondary state.
+ * @param isInPrimaryState Lambda to return true/false to indicate checkbox primary or secondary state.
+ * @param onCheckedChangedListener Callback to be invoked when checkbox is clicked.
+ */
+@Suppress("LongParameterList")
+class BrowserMenuImageTextCheckboxButton(
+ @DrawableRes imageResource: Int,
+ private val label: String,
+ @ColorRes iconTintColorResource: Int = NO_ID,
+ @ColorRes internal val textColorResource: Int = NO_ID,
+ enabled: Boolean = true,
+ @get:VisibleForTesting internal val labelListener: () -> Unit,
+ @DrawableRes val primaryStateIconResource: Int,
+ @DrawableRes val secondaryStateIconResource: Int,
+ @ColorRes internal val tintColorResource: Int = NO_ID,
+ private val primaryLabel: String,
+ private val secondaryLabel: String,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ val isInPrimaryState: () -> Boolean = { true },
+ private val onCheckedChangedListener: (Boolean) -> Unit,
+) : BrowserMenuImageText(
+ label,
+ imageResource,
+ iconTintColorResource,
+ textColorResource,
+ enabled,
+ isCollapsingMenuLimit,
+ isSticky,
+ labelListener,
+) {
+ override var visible: () -> Boolean = { true }
+ override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_image_text_checkbox_button
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ super.bind(menu, view)
+
+ view.findViewById<View>(R.id.accessibilityRegion).apply {
+ setOnClickListener { labelListener.invoke() }
+ contentDescription = label
+ }
+
+ bindCheckbox(menu, view.findViewById(R.id.checkbox) as AppCompatCheckBox)
+ }
+
+ private fun bindCheckbox(menu: BrowserMenu, button: AppCompatCheckBox) {
+ val buttonText = if (isInPrimaryState()) primaryLabel else secondaryLabel
+ val tintColor = ContextCompat.getColor(button.context, tintColorResource)
+ val buttonDrawableIcon = if (isInPrimaryState()) {
+ ContextCompat.getDrawable(button.context, primaryStateIconResource)
+ } else {
+ ContextCompat.getDrawable(button.context, secondaryStateIconResource)
+ }
+ buttonDrawableIcon?.setTint(tintColor)
+ val displayMetrics = button.context.resources.displayMetrics
+
+ buttonDrawableIcon?.setBounds(
+ 0,
+ 0,
+ CHECKBOX_ICON_SIZE_DP.dpToPx(displayMetrics),
+ CHECKBOX_ICON_SIZE_DP.dpToPx(displayMetrics),
+ )
+
+ button.apply {
+ text = buttonText
+ setTextColor(tintColor)
+ setCompoundDrawables(buttonDrawableIcon, null, null, null)
+
+ setOnCheckedChangeListener { _, isChecked ->
+ onCheckedChangedListener(isChecked)
+ menu.dismiss()
+ }
+ }
+ }
+
+ companion object {
+ private const val CHECKBOX_ICON_SIZE_DP = 19
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbar.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbar.kt
new file mode 100644
index 0000000000..bd15f5c93b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbar.kt
@@ -0,0 +1,212 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.appcompat.widget.TooltipCompat
+import androidx.core.content.ContextCompat.getColor
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+
+/**
+ * A toolbar of buttons to show inside the browser menu.
+ *
+ * @param items buttons that will be shown in a horizontal layout
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ */
+class BrowserMenuItemToolbar(
+ private val items: List<Button>,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+) : BrowserMenuItem {
+ override var visible: () -> Boolean = { true }
+
+ override val interactiveCount: () -> Int = { items.size }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_toolbar
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ val layout = view as LinearLayout
+ layout.removeAllViews()
+
+ val selectableBackground =
+ layout.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless)
+
+ for (item in items) {
+ val button = AppCompatImageButton(layout.context)
+ item.bind(button)
+
+ button.setFocusable(true)
+ button.setBackgroundResource(selectableBackground)
+ button.setOnClickListener {
+ item.listener()
+ menu.dismiss()
+ }
+ button.setOnLongClickListener {
+ item.longClickListener?.invoke()
+ menu.dismiss()
+ true
+ }
+ button.isLongClickable = item.longClickListener != null
+
+ layout.addView(button, LinearLayout.LayoutParams(0, MATCH_PARENT, 1f))
+ }
+ }
+
+ override fun invalidate(view: View) {
+ val layout = view as LinearLayout
+ items.withIndex().forEach { (index, item) ->
+ item.invalidate(layout.getChildAt(index) as AppCompatImageButton)
+ }
+ }
+
+ override fun asCandidate(context: Context) = RowMenuCandidate(
+ items = items.map { it.asCandidate(context) },
+ containerStyle = ContainerStyle(isVisible = visible()),
+ )
+
+ /**
+ * A button to be shown in a toolbar inside the browser menu.
+ *
+ * @param imageResource ID of a drawable resource to be shown as icon.
+ * @param contentDescription The button's content description, used for accessibility support.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ * @param isEnabled Lambda to return true/false to indicate if this button should be enabled or disabled.
+ * @param longClickListener Callback to be invoked when the button is long clicked.
+ * @param listener Callback to be invoked when the button is pressed.
+ */
+ open class Button(
+ @DrawableRes val imageResource: Int,
+ val contentDescription: String,
+ @ColorRes val iconTintColorResource: Int = NO_ID,
+ val isEnabled: () -> Boolean = { true },
+ val longClickListener: (() -> Unit)? = null,
+ val listener: () -> Unit,
+ ) {
+
+ internal open fun bind(view: ImageView) {
+ view.setImageResource(imageResource)
+ view.contentDescription = contentDescription
+ setTooltipTextCompatible(view, contentDescription)
+ view.setTintResource(iconTintColorResource)
+ view.isEnabled = isEnabled()
+ }
+
+ internal open fun invalidate(view: ImageView) {
+ view.isEnabled = isEnabled()
+ }
+
+ internal open fun asCandidate(context: Context) = SmallMenuCandidate(
+ contentDescription,
+ icon = DrawableMenuIcon(
+ context,
+ resource = imageResource,
+ tint = if (iconTintColorResource == NO_ID) null else getColor(context, iconTintColorResource),
+ ),
+ containerStyle = ContainerStyle(isEnabled = isEnabled()),
+ onClick = listener,
+ )
+
+ internal fun setTooltipTextCompatible(view: ImageView, contentDescription: String) {
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) {
+ CustomTooltip.setTooltipText(view, contentDescription)
+ } else {
+ TooltipCompat.setTooltipText(view, contentDescription)
+ }
+ }
+ }
+
+ /**
+ * A button that either shows an primary state or an secondary state based on the provided
+ * <code>isInPrimaryState</code> lambda.
+ *
+ * @param primaryImageResource ID of a drawable resource to be shown as primary icon.
+ * @param primaryContentDescription The button's primary content description, used for accessibility support.
+ * @param primaryImageTintResource Optional ID of color resource to tint the primary icon.
+ * @param secondaryImageResource Optional ID of a different drawable resource to be shown as secondary icon.
+ * @param secondaryContentDescription Optional secondary content description for button, for accessibility support.
+ * @param secondaryImageTintResource Optional ID of secondary color resource to tint the icon.
+ * @param isInPrimaryState Lambda to return true/false to indicate if this button should be primary or secondary.
+ * @param disableInSecondaryState Optional boolean to disable the button when in secondary state.
+ * @param longClickListener Callback to be invoked when the button is long clicked.
+ * @param listener Callback to be invoked when the button is pressed.
+ */
+ open class TwoStateButton(
+ @DrawableRes val primaryImageResource: Int,
+ val primaryContentDescription: String,
+ @ColorRes val primaryImageTintResource: Int = NO_ID,
+ @DrawableRes val secondaryImageResource: Int = primaryImageResource,
+ val secondaryContentDescription: String = primaryContentDescription,
+ @ColorRes val secondaryImageTintResource: Int = primaryImageTintResource,
+ val isInPrimaryState: () -> Boolean = { true },
+ val disableInSecondaryState: Boolean = false,
+ longClickListener: (() -> Unit)? = null,
+ listener: () -> Unit,
+ ) : Button(
+ primaryImageResource,
+ primaryContentDescription,
+ primaryImageTintResource,
+ isInPrimaryState,
+ longClickListener = longClickListener,
+ listener = listener,
+ ) {
+
+ private var wasInPrimaryState = false
+
+ override fun bind(view: ImageView) {
+ if (isInPrimaryState()) {
+ super.bind(view)
+ } else {
+ view.setImageResource(secondaryImageResource)
+ view.contentDescription = secondaryContentDescription
+ setTooltipTextCompatible(view, secondaryContentDescription)
+ view.setTintResource(secondaryImageTintResource)
+ view.isEnabled = !disableInSecondaryState
+ }
+ wasInPrimaryState = isInPrimaryState()
+ }
+
+ override fun invalidate(view: ImageView) {
+ if (isInPrimaryState() != wasInPrimaryState) {
+ bind(view)
+ }
+ }
+
+ override fun asCandidate(context: Context): SmallMenuCandidate = if (isInPrimaryState()) {
+ super.asCandidate(context)
+ } else {
+ SmallMenuCandidate(
+ secondaryContentDescription,
+ icon = DrawableMenuIcon(
+ context,
+ resource = secondaryImageResource,
+ tint = if (secondaryImageTintResource == NO_ID) {
+ null
+ } else {
+ getColor(context, secondaryImageTintResource)
+ },
+ ),
+ containerStyle = ContainerStyle(isEnabled = !disableInSecondaryState),
+ onClick = listener,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuSwitch.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuSwitch.kt
new file mode 100644
index 0000000000..abea7b218f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuSwitch.kt
@@ -0,0 +1,33 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+
+/**
+ * A simple browser menu switch.
+ *
+ * @param label The visible label of this menu item.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param initialState The initial value the checkbox should have.
+ * @param listener Callback to be invoked when this menu item is checked.
+ */
+class BrowserMenuSwitch(
+ label: String,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ initialState: () -> Boolean = { false },
+ listener: (Boolean) -> Unit,
+) : BrowserMenuCompoundButton(label, isCollapsingMenuLimit, isSticky, initialState, listener) {
+ override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_switch
+
+ override fun asCandidate(context: Context) = super.asCandidate(context).copy(
+ end = CompoundMenuCandidate.ButtonType.SWITCH,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/CustomTooltip.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/CustomTooltip.kt
new file mode 100644
index 0000000000..9e7ce8b674
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/CustomTooltip.kt
@@ -0,0 +1,154 @@
+/* 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.browser.menu.item
+
+import android.annotation.SuppressLint
+import android.text.TextUtils
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnLongClickListener
+import android.view.WindowManager
+import android.widget.LinearLayout
+import android.widget.PopupWindow
+import android.widget.TextView
+import androidx.core.view.ViewCompat
+import androidx.core.widget.PopupWindowCompat
+import mozilla.components.browser.menu.R
+
+/**
+ * A tooltip shown on long click on an anchor view.
+ * There can be only one tooltip shown at a given moment.
+ */
+internal class CustomTooltip private constructor(
+ private val anchor: View,
+ private val tooltipText: CharSequence,
+) : OnLongClickListener, View.OnAttachStateChangeListener {
+ private val hideTooltipRunnable = Runnable { hide() }
+ private var popupWindow: PopupWindow? = null
+
+ init {
+ anchor.setOnLongClickListener(this)
+ }
+
+ override fun onLongClick(view: View): Boolean {
+ if (ViewCompat.isAttachedToWindow(anchor)) {
+ show()
+ anchor.addOnAttachStateChangeListener(this)
+ }
+ return true
+ }
+
+ private fun computeOffsets(): Offset {
+ // Measure pop-up
+ val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+ popupWindow?.contentView?.measure(spec, spec)
+
+ val rootView = anchor.rootView
+ val rootPosition = IntArray(2)
+ val anchorPosition = IntArray(2)
+ rootView.getLocationOnScreen(rootPosition)
+ anchor.getLocationOnScreen(anchorPosition)
+
+ val rootY = rootPosition[1]
+ val anchorY = anchorPosition[1]
+
+ val rootHeight = rootView.height
+ val tooltipHeight = popupWindow?.contentView?.measuredHeight ?: 0
+ val tooltipWidth = popupWindow?.contentView?.measuredWidth ?: 0
+
+ val checkY = rootY + rootHeight - (anchorY + anchor.height + tooltipHeight + TOOLTIP_EXTRA_VERTICAL_OFFSET_DP)
+ val belowY = TOOLTIP_EXTRA_VERTICAL_OFFSET_DP
+ val aboveY = -(anchor.height + tooltipHeight + TOOLTIP_EXTRA_VERTICAL_OFFSET_DP)
+
+ // align anchor center with tooltip center
+ val offsetX = anchor.width / 2 - tooltipWidth / 2
+ // make sure tooltip is visible and it's not displayed below, outside the view
+ val offsetY = if (checkY > 0) belowY else aboveY
+ return Offset(offsetX, offsetY)
+ }
+
+ @SuppressLint("InflateParams")
+ fun show() {
+ activeTooltip?.hide()
+ activeTooltip = this
+
+ val layout = LayoutInflater.from(anchor.context)
+ .inflate(R.layout.mozac_browser_tooltip_layout, null)
+
+ layout.findViewById<TextView>(R.id.mozac_browser_tooltip_text).text = tooltipText
+ popupWindow = PopupWindow(
+ layout,
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ false,
+ )
+
+ val offsets = computeOffsets()
+
+ popupWindow?.let {
+ PopupWindowCompat.setWindowLayoutType(it, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL)
+ it.isTouchable = false
+ it.showAsDropDown(anchor, offsets.x, offsets.y, Gravity.CENTER)
+ }
+
+ anchor.removeCallbacks(hideTooltipRunnable)
+ anchor.postDelayed(hideTooltipRunnable, LONG_CLICK_HIDE_TIMEOUT_MS)
+ }
+
+ fun hide() {
+ if (activeTooltip === this) {
+ activeTooltip = null
+ popupWindow?.let {
+ it.dismiss()
+ popupWindow = null
+ anchor.removeOnAttachStateChangeListener(this)
+ }
+ }
+ }
+
+ override fun onViewAttachedToWindow(v: View) {
+ // no-op
+ }
+
+ override fun onViewDetachedFromWindow(v: View) {
+ hide()
+ anchor.removeCallbacks(hideTooltipRunnable)
+ }
+
+ companion object {
+ private const val LONG_CLICK_HIDE_TIMEOUT_MS: Long = 2500
+ private const val TOOLTIP_EXTRA_VERTICAL_OFFSET_DP = 8
+
+ // The tooltip currently being shown properly disposed in hide() / onViewDetachedFromWindow()
+ @SuppressLint("StaticFieldLeak")
+ private var activeTooltip: CustomTooltip? = null
+
+ /**
+ * Set the tooltip text for the view.
+ * @param view view to set the tooltip for
+ * @param tooltipText the tooltip text
+ */
+ fun setTooltipText(view: View, tooltipText: CharSequence) {
+ // check for dynamic content description
+ if (TextUtils.isEmpty(tooltipText)) {
+ activeTooltip?.let {
+ if (it.anchor === view) {
+ it.hide()
+ }
+ }
+ view.setOnLongClickListener(null)
+ view.isLongClickable = false
+ } else {
+ CustomTooltip(view, tooltipText)
+ }
+ }
+ }
+
+ /**
+ * A data class for storing x and y offsets
+ */
+ data class Offset(val x: Int, val y: Int)
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/ParentBrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/ParentBrowserMenuItem.kt
new file mode 100644
index 0000000000..5d910967ac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/ParentBrowserMenuItem.kt
@@ -0,0 +1,105 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.browser.menu.ext.asCandidateList
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+
+/**
+ * A parent menu item for displaying text and an image icon with a nested sub menu.
+ * It handles back pressing if the sub menu contains a [BackPressMenuItem].
+ *
+ * @param label The visible label of this menu item.
+ * @param imageResource ID of a drawable resource to be shown as icon.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ * @param textColorResource Optional ID of color resource to tint the text.
+ * @property subMenu Target sub menu to be shown when this menu item is clicked.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param endOfMenuAlwaysVisible when is set to true makes sure the bottom of the menu is always visible
+ * otherwise, the top of the menu is always visible.
+ */
+class ParentBrowserMenuItem(
+ internal val label: String,
+ @DrawableRes
+ private val imageResource: Int,
+ @ColorRes
+ private val iconTintColorResource: Int = NO_ID,
+ @ColorRes
+ private val textColorResource: Int = NO_ID,
+ internal val subMenu: BrowserMenu,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ endOfMenuAlwaysVisible: Boolean = false,
+) : AbstractParentBrowserMenuItem(subMenu, isCollapsingMenuLimit, endOfMenuAlwaysVisible) {
+
+ override var visible: () -> Boolean = { true }
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_parent_menu
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ bindText(view)
+ bindImage(view)
+ bindBackPress(menu, view)
+
+ super.bind(menu, view)
+ }
+
+ private fun bindText(view: View) {
+ val textView = view.findViewById<TextView>(R.id.text)
+ textView.text = label
+ textView.setColorResource(textColorResource)
+ }
+
+ private fun bindImage(view: View) {
+ val imageView = view.findViewById<AppCompatImageView>(R.id.image)
+ with(imageView) {
+ setImageResource(imageResource)
+ setTintResource(iconTintColorResource)
+ }
+ val overflowView = view.findViewById<AppCompatImageView>(R.id.overflowImage)
+ with(overflowView) {
+ visibility = View.VISIBLE
+ setTintResource(iconTintColorResource)
+ }
+ }
+
+ private fun bindBackPress(menu: BrowserMenu, view: View) {
+ val backPressMenuItem =
+ subMenu.adapter.visibleItems.find { it is BackPressMenuItem } as? BackPressMenuItem
+ backPressMenuItem?.let {
+ backPressMenuItem.setListener {
+ onBackPressed(menu, view)
+ }
+ }
+ }
+
+ override fun asCandidate(context: Context) = NestedMenuCandidate(
+ id = hashCode(),
+ text = label,
+ start = DrawableMenuIcon(
+ context,
+ resource = imageResource,
+ tint = if (iconTintColorResource == NO_ID) null else ContextCompat.getColor(context, iconTintColorResource),
+ ),
+ subMenuItems = subMenu.adapter.visibleItems.asCandidateList(context),
+ textStyle = TextStyle(
+ color = if (textColorResource == NO_ID) null else ContextCompat.getColor(context, textColorResource),
+ ),
+ containerStyle = ContainerStyle(isVisible = visible()),
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItem.kt
new file mode 100644
index 0000000000..3f7c2803f1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItem.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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.browser.menu.ext.addRippleEffect
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+
+/**
+ * A menu item for displaying text with a highlight state which sets the
+ * background of the menu item.
+ *
+ * @param label The default visible label of this menu item.
+ * @param textColorResource Optional ID of color resource to tint the text.
+ * @param textSize The size of the label.
+ * @param backgroundTint Tint for the menu item background color
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param isHighlighted Whether or not to display the highlight
+ * @param listener Callback to be invoked when this menu item is clicked.
+ */
+class SimpleBrowserMenuHighlightableItem(
+ private val label: String,
+ @ColorRes private val textColorResource: Int = NO_ID,
+ private val textSize: Float = NO_ID.toFloat(),
+ @ColorInt val backgroundTint: Int,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ var isHighlighted: () -> Boolean = { true },
+ private val listener: () -> Unit = {},
+) : BrowserMenuItem {
+
+ override var visible: () -> Boolean = { true }
+ private var wasHighlighted = false
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ bindText(view)
+
+ view.setOnClickListener {
+ listener.invoke()
+ menu.dismiss()
+ }
+
+ wasHighlighted = isHighlighted()
+ updateHighlight(view, wasHighlighted)
+ }
+
+ private fun bindText(view: View) {
+ val textView = view as TextView
+ textView.text = label
+ textView.addRippleEffect()
+
+ if (textColorResource != NO_ID) {
+ textView.setColorResource(textColorResource)
+ }
+
+ if (textSize != NO_ID.toFloat()) {
+ textView.textSize = textSize
+ }
+ }
+
+ override fun invalidate(view: View) {
+ val isNowHighlighted = isHighlighted()
+ if (isNowHighlighted != wasHighlighted) {
+ wasHighlighted = isNowHighlighted
+ updateHighlight(view, isNowHighlighted)
+ }
+ }
+
+ private fun updateHighlight(view: View, isHighlighted: Boolean) {
+ val textView = view as TextView
+
+ if (isHighlighted) {
+ textView.setBackgroundColor(backgroundTint)
+ } else {
+ textView.addRippleEffect()
+ }
+ }
+
+ override fun asCandidate(context: Context): MenuCandidate {
+ val textStyle = TextStyle(
+ size = if (textSize == NO_ID.toFloat()) null else textSize,
+ color = if (textColorResource == NO_ID) null else ContextCompat.getColor(context, textColorResource),
+ )
+ val containerStyle = ContainerStyle(isVisible = visible())
+ return TextMenuCandidate(
+ label,
+ textStyle = textStyle,
+ containerStyle = containerStyle,
+ onClick = listener,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItem.kt
new file mode 100644
index 0000000000..9a350e3317
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItem.kt
@@ -0,0 +1,84 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat.getColor
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+
+/**
+ * A simple browser menu item displaying text.
+ *
+ * @param label The visible label of this menu item.
+ * @param textSize: The size of the label.
+ * @param textColorResource: The color resource to apply to the text.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param listener Callback to be invoked when this menu item is clicked.
+ */
+class SimpleBrowserMenuItem(
+ private val label: String,
+ private val textSize: Float = NO_ID.toFloat(),
+ @ColorRes
+ private val textColorResource: Int = NO_ID,
+ override val isCollapsingMenuLimit: Boolean = false,
+ private val listener: (() -> Unit)? = null,
+) : BrowserMenuItem {
+ override var visible: () -> Boolean = { true }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ val textView = view as TextView
+ textView.text = label
+
+ if (textSize != NO_ID.toFloat()) {
+ textView.textSize = textSize
+ }
+
+ textView.setColorResource(textColorResource)
+
+ if (listener != null) {
+ textView.setOnClickListener {
+ listener.invoke()
+ menu.dismiss()
+ }
+ } else {
+ // Remove the ripple effect
+ textView.background = null
+ }
+ }
+
+ override fun asCandidate(context: Context): MenuCandidate {
+ val textStyle = TextStyle(
+ size = if (textSize == NO_ID.toFloat()) null else textSize,
+ color = if (textColorResource == NO_ID) null else getColor(context, textColorResource),
+ )
+ val containerStyle = ContainerStyle(isVisible = visible())
+ return if (listener != null) {
+ TextMenuCandidate(
+ label,
+ textStyle = textStyle,
+ containerStyle = containerStyle,
+ onClick = listener,
+ )
+ } else {
+ DecorativeTextMenuCandidate(
+ label,
+ textStyle = textStyle,
+ containerStyle = containerStyle,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageText.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageText.kt
new file mode 100644
index 0000000000..6cb2b70eb2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageText.kt
@@ -0,0 +1,133 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+
+/**
+ * A browser menu item with two states, used for displaying text with an image icon
+ *
+ * @param primaryLabel The visible label of the checkbox in primary state.
+ * @param secondaryLabel The visible label of this menu item in secondary state.
+ * @param textColorResource Optional ID of color resource to tint the text.
+ * @param enabled Sets the enabled status for the view. By default, it is true.
+ * @param primaryStateIconResource ID of a drawable resource to be shown as icon in primary state.
+ * @param secondaryStateIconResource ID of a drawable resource to be shown as icon in secondary state.
+ * @param iconTintColorResource Optional ID of color resource to tint the checkbox drawable.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ * @param isInPrimaryState Lambda to return true/false to indicate item is in primary state.
+ * @param isInSecondaryState Lambda to return true/false to indicate item is in secondary state
+ * @param primaryStateAction Callback to be invoked when this menu item is clicked in primary state.
+ * @param secondaryStateAction Callback to be invoked when this menu item is clicked in secondary state.
+ */
+class TwoStateBrowserMenuImageText(
+ private val primaryLabel: String,
+ private val secondaryLabel: String,
+ @ColorRes internal val textColorResource: Int = NO_ID,
+ enabled: Boolean = true,
+ @DrawableRes val primaryStateIconResource: Int,
+ @DrawableRes val secondaryStateIconResource: Int,
+ @ColorRes iconTintColorResource: Int = NO_ID,
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+ val isInPrimaryState: () -> Boolean = { true },
+ val isInSecondaryState: () -> Boolean = { false },
+ private val primaryStateAction: () -> Unit = { },
+ private val secondaryStateAction: () -> Unit = { },
+) : BrowserMenuImageText(
+ primaryLabel,
+ primaryStateIconResource,
+ iconTintColorResource,
+ textColorResource,
+ enabled,
+ isCollapsingMenuLimit,
+ isSticky,
+ primaryStateAction,
+) {
+ override var visible: () -> Boolean = { isInPrimaryState() || isInSecondaryState() }
+
+ override fun getLayoutResource(): Int =
+ R.layout.mozac_browser_menu_item_image_text
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ val isInPrimaryState = isInPrimaryState()
+ bindText(view, isInPrimaryState)
+ bindImage(view, isInPrimaryState)
+
+ val listener = if (isInPrimaryState) primaryStateAction else secondaryStateAction
+ view.setOnClickListener {
+ listener.invoke()
+ menu.dismiss()
+ }
+ }
+
+ private fun bindText(view: View, isInPrimaryState: Boolean) {
+ val textView = view.findViewById<TextView>(R.id.text)
+ textView.text = if (isInPrimaryState) primaryLabel else secondaryLabel
+ textView.setColorResource(textColorResource)
+ }
+
+ private fun bindImage(view: View, isInPrimaryState: Boolean) {
+ val imageView = view.findViewById<AppCompatImageView>(R.id.image)
+ val imageResource =
+ if (isInPrimaryState) primaryStateIconResource else secondaryStateIconResource
+
+ with(imageView) {
+ setImageResource(imageResource)
+ setTintResource(iconTintColorResource)
+ }
+ }
+
+ override fun asCandidate(context: Context): MenuCandidate = TextMenuCandidate(
+ if (isInPrimaryState()) {
+ primaryLabel
+ } else {
+ secondaryLabel
+ },
+ start = DrawableMenuIcon(
+ context,
+ resource = if (isInPrimaryState()) {
+ primaryStateIconResource
+ } else {
+ secondaryStateIconResource
+ },
+ tint = if (iconTintColorResource == NO_ID) {
+ null
+ } else {
+ ContextCompat.getColor(
+ context,
+ iconTintColorResource,
+ )
+ },
+ ),
+ textStyle = TextStyle(
+ color = if (textColorResource == NO_ID) {
+ null
+ } else {
+ ContextCompat.getColor(
+ context,
+ textColorResource,
+ )
+ },
+ ),
+ containerStyle = ContainerStyle(isVisible = visible()),
+ onClick = if (isInPrimaryState()) primaryStateAction else secondaryStateAction,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItem.kt
new file mode 100644
index 0000000000..7eed3ac516
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItem.kt
@@ -0,0 +1,168 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.content.res.AppCompatResources.getDrawable
+import androidx.core.graphics.drawable.toDrawable
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.base.log.Log
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * A browser menu item displaying a web extension action.
+ *
+ * @param action the [Action] to display.
+ * @param listener a callback to be invoked when this menu item is clicked.
+ * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu.
+ * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards
+ * depending on the menu position).
+ */
+class WebExtensionBrowserMenuItem(
+ internal var action: Action,
+ internal val listener: () -> Unit,
+ internal val id: String = "",
+ override val isCollapsingMenuLimit: Boolean = false,
+ override val isSticky: Boolean = false,
+) : BrowserMenuItem {
+ override var visible: () -> Boolean = { true }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_web_extension
+
+ @VisibleForTesting
+ internal var iconTintColorResource: Int? = null
+
+ @Suppress("TooGenericExceptionCaught")
+ override fun bind(menu: BrowserMenu, view: View) {
+ val container = view.findViewById<View>(R.id.container)
+ updateItem(view)
+ container.setOnClickListener {
+ listener.invoke()
+ menu.dismiss()
+ }
+ }
+
+ override fun invalidate(view: View) {
+ val labelView = view.findViewById<TextView>(R.id.action_label)
+ val badgeView = view.findViewById<TextView>(R.id.badge_text)
+ val imageView = view.findViewById<ImageView>(R.id.action_image)
+
+ updateItem(view)
+
+ labelView.invalidate()
+ badgeView.invalidate()
+ imageView.invalidate()
+ }
+
+ @VisibleForTesting
+ internal fun updateItem(view: View) {
+ val imageView = view.findViewById<ImageView>(R.id.action_image)
+ val labelView = view.findViewById<TextView>(R.id.action_label)
+ val badgeView = view.findViewById<TextView>(R.id.badge_text)
+ val container = view.findViewById<View>(R.id.container)
+
+ container.isEnabled.updateIfChange(new = action.enabled ?: true) {
+ container.isEnabled = it
+ }
+
+ imageView.contentDescription.updateIfChange(action.title) {
+ imageView.contentDescription = it
+ }
+ labelView.text.updateIfChange(action.title) {
+ labelView.text = it
+ }
+ badgeView.setBadgeText(action.badgeText)
+ action.badgeTextColor?.let { badgeView.setTextColor(it) }
+ action.badgeBackgroundColor?.let { badgeView.background?.setTint(it) }
+ setupIcon(view, imageView, iconTintColorResource)
+ }
+
+ private inline fun <T> T.updateIfChange(new: T, setter: (T) -> Unit) {
+ if (this != new) {
+ setter(new)
+ }
+ }
+
+ override fun asCandidate(context: Context) = TextMenuCandidate(
+ action.title.orEmpty(),
+ start = AsyncDrawableMenuIcon(
+ loadDrawable = { _, height -> loadIcon(context, height) },
+ ),
+ end = action.badgeText?.let { badgeText ->
+ TextMenuIcon(
+ badgeText,
+ backgroundTint = action.badgeBackgroundColor,
+ textStyle = TextStyle(
+ color = action.badgeTextColor,
+ ),
+ )
+ },
+ containerStyle = ContainerStyle(
+ isVisible = visible(),
+ isEnabled = action.enabled ?: false,
+ ),
+ onClick = listener,
+ )
+
+ @VisibleForTesting
+ internal fun setupIcon(view: View, imageView: ImageView, iconTintColorResource: Int?) {
+ MainScope().launch {
+ loadIcon(view.context, imageView.measuredHeight)?.let {
+ iconTintColorResource?.let { tint -> imageView.setTintResource(tint) }
+ imageView.setImageDrawable(it)
+ }
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ private suspend fun loadIcon(context: Context, height: Int): Drawable? {
+ return try {
+ action.loadIcon?.invoke(height)?.toDrawable(context.resources)
+ } catch (throwable: Throwable) {
+ Log.log(
+ Log.Priority.ERROR,
+ "mozac-webextensions",
+ throwable,
+ "Failed to load browser action icon, falling back to default.",
+ )
+
+ getDrawable(context, iconsR.drawable.mozac_ic_web_extension_default_icon)
+ }
+ }
+
+ /**
+ * Sets the tint to be applied to the extension icon
+ */
+ fun setIconTint(iconTintColorResource: Int?) {
+ iconTintColorResource?.let { this.iconTintColorResource = it }
+ }
+}
+
+/**
+ * Sets the badgeText and the visibility of the TextView based on empty/nullability of the badgeText.
+ */
+fun TextView.setBadgeText(badgeText: String?) {
+ if (badgeText.isNullOrEmpty()) {
+ visibility = View.INVISIBLE
+ } else {
+ visibility = View.VISIBLE
+ text = badgeText
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionPlaceholderMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionPlaceholderMenuItem.kt
new file mode 100644
index 0000000000..b76c55e793
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionPlaceholderMenuItem.kt
@@ -0,0 +1,36 @@
+/* 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.browser.menu.item
+
+import android.view.View
+import androidx.annotation.ColorRes
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.R
+
+/**
+ * A browser menu item that is to be used only as a placeholder for inserting web extensions in main menu.
+ * The id of the web extension to be inserted has to correspond to the id of the browser menu item.
+ *
+ * @param id The id of this menu item.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ */
+class WebExtensionPlaceholderMenuItem(
+ val id: String,
+ @ColorRes
+ val iconTintColorResource: Int = NO_ID,
+) : BrowserMenuItem {
+ override var visible: () -> Boolean = { false }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ // no binding, item not visible.
+ }
+
+ companion object {
+ const val MAIN_EXTENSIONS_MENU_ID = "mainExtensionsMenu"
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerView.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerView.kt
new file mode 100644
index 0000000000..d4d46a9aca
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerView.kt
@@ -0,0 +1,86 @@
+/* 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.browser.menu.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import androidx.annotation.Px
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.browser.menu.R
+
+/**
+ * [RecyclerView] with automatically set width between widthMin / widthMax xml attributes.
+ */
+class DynamicWidthRecyclerView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+) : RecyclerView(context, attrs) {
+ @VisibleForTesting
+ @Px
+ internal var maxWidthOfAllChildren: Int = 0
+ set(value) {
+ if (field == 0) field = value
+ }
+
+ @Px var minWidth: Int = -1
+
+ @Px var maxWidth: Int = -1
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ override fun onMeasure(widthSpec: Int, heightSpec: Int) {
+ if (minWidth in 1 until maxWidth) {
+ // Ignore any bounds set in xml. Allow for children to expand entirely.
+ callParentOnMeasure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightSpec)
+
+ // First measure will report the width/height for the entire list
+ // The first layout pass will actually remove child views that do not fit the screen
+ // so future onMeasure calls will report skewed values.
+ maxWidthOfAllChildren = measuredWidth
+
+ // Children now have "unspecified" width. Let's set some bounds.
+ setReconciledDimensions(maxWidthOfAllChildren, measuredHeight)
+ } else {
+ // Default behavior. layout_width / layout_height properties will be used for measuring.
+ callParentOnMeasure(widthSpec, heightSpec)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun setReconciledDimensions(
+ desiredWidth: Int,
+ desiredHeight: Int,
+ ) {
+ val minimumTapArea = resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_tap_area)
+ val minimumItemWidth = resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_item_width)
+
+ val reconciledWidth = desiredWidth
+ .coerceAtLeast(minWidth)
+ // Follow material guidelines where the minimum width is 112dp.
+ .coerceAtLeast(minimumItemWidth)
+ .coerceAtMost(maxWidth)
+ // Leave at least 48dp as a tappable “exit area” available whenever the menu is open.
+ .coerceAtMost(getScreenWidth() - minimumTapArea)
+
+ callSetMeasuredDimension(reconciledWidth, desiredHeight)
+ }
+
+ @VisibleForTesting
+ internal fun getScreenWidth(): Int = resources.displayMetrics.widthPixels
+
+ @SuppressLint("WrongCall")
+ @VisibleForTesting
+ // Used for testing protected super.onMeasure(..) calls will be executed.
+ internal fun callParentOnMeasure(widthSpec: Int, heightSpec: Int) {
+ super.onMeasure(widthSpec, heightSpec)
+ }
+
+ @VisibleForTesting
+ // Used for testing final protected setMeasuredDimension(..) calls were executed
+ internal fun callSetMeasuredDimension(width: Int, height: Int) {
+ setMeasuredDimension(width, height)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/ExpandableLayout.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/ExpandableLayout.kt
new file mode 100644
index 0000000000..1eb2e56509
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/ExpandableLayout.kt
@@ -0,0 +1,436 @@
+/* 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.browser.menu.view
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+import android.view.ViewGroup
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.widget.FrameLayout
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.doOnEnd
+import androidx.core.view.children
+import androidx.core.view.marginBottom
+import androidx.core.view.marginLeft
+import androidx.core.view.marginRight
+import androidx.core.view.marginTop
+import androidx.core.view.updateLayoutParams
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * ViewGroup intended to wrap another to then allow for the following automatic behavior:
+ * - when first laid out on the screen the wrapped view is collapsed.
+ * - informs about touches in the empty space left by the collapsed view through [blankTouchListener].
+ * - when users swipe up it will expand. Once expanded it remains so.
+ */
+@Suppress("TooManyFunctions", "LargeClass")
+internal class ExpandableLayout private constructor(context: Context) : FrameLayout(context) {
+ /**
+ * The wrapped view that needs to be collapsed / expanded.
+ */
+ @VisibleForTesting
+ internal lateinit var wrappedView: ViewGroup
+
+ /**
+ * Listener of touches in the empty space left by the collapsed view.
+ */
+ @VisibleForTesting
+ internal var blankTouchListener: (() -> Unit)? = null
+
+ /**
+ * Index of the last menu item that should be visible when the wrapped view is collapsed.
+ */
+ @VisibleForTesting
+ internal var lastVisibleItemIndexWhenCollapsed: Int = Int.MAX_VALUE
+
+ /**
+ * Index of the sticky footer, if such an item is set.
+ */
+ @VisibleForTesting
+ internal var stickyItemIndex: Int = RecyclerView.NO_POSITION
+
+ /**
+ * Height of wrapped view when collapsed.
+ * Calculated once based on the position of the "isCollapsingMenuLimit" BrowserMenuItem.
+ * Capped by [parentHeight]
+ */
+ @VisibleForTesting
+ internal var collapsedHeight: Int = NOT_CALCULATED_DEFAULT_HEIGHT
+
+ /**
+ * Height of wrapped view when expanded.
+ * Calculated once based on measuredHeighWithMargins().
+ * Capped by [parentHeight]
+ */
+ @VisibleForTesting
+ internal var expandedHeight: Int = NOT_CALCULATED_DEFAULT_HEIGHT
+
+ /**
+ * Available space given by the parent.
+ */
+ @VisibleForTesting
+ internal var parentHeight: Int = NOT_CALCULATED_DEFAULT_HEIGHT
+
+ /**
+ * Whether to intercept touches while the view is collapsed.
+ * If true:
+ * - a swipe up will be intercepted and used to expand the wrapped view.
+ * - a swipe in the empty space left by the collapsed view will be intercepted
+ * and [blankTouchListener] will be called.
+ * - other touches / gestures will be left to pass through to the children.
+ */
+ @VisibleForTesting
+ internal var isCollapsed = true
+
+ /**
+ * Whether to intercept touches while the view is expanding.
+ * If true:
+ * - all touches / gestures will be intercepted.
+ */
+ @VisibleForTesting
+ internal var isExpandInProgress = false
+
+ /**
+ * Distance in pixels a touch can wander before we think the user is scrolling.
+ * (If this would be bigger than that of a child the child will react to the scroll first)
+ */
+ @VisibleForTesting
+ internal var touchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
+
+ /**
+ * Y axis coordinate of the [MotionEvent.ACTION_DOWN] event.
+ * Used to calculate the distance scrolled, to know when the view should be expanded.
+ */
+ @VisibleForTesting
+ internal var initialYCoord = NOT_CALCULATED_Y_TOUCH_COORD
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ callParentOnMeasure(widthMeasureSpec, heightMeasureSpec)
+
+ // Avoid new separate measure calls specifically for our usecase. Piggyback on the already requested ones.
+ // Calculate our needed dimensions and collapse the menu when based on them.
+ if (isCollapsed && getOrCalculateCollapsedHeight() > 0 && getOrCalculateExpandedHeight(heightMeasureSpec) > 0) {
+ collapse()
+ }
+ }
+
+ // While this view is collapsed (not fully expanded) we want to intercept all vertical scrolls
+ // that will be used as an indicator to expand the view,
+ // while letting all simple touch events get handled by children's click listeners.
+ //
+ // Also if this view is collapsed (full height but translated) we want to treat any touch in the
+ // invisible space as a dismiss event.
+ @Suppress("ComplexMethod", "ReturnCount")
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ if (shouldInterceptTouches()) {
+ return when (ev?.actionMasked) {
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
+ false // Allow click listeners firing for children.
+ }
+ MotionEvent.ACTION_DOWN -> {
+ if (isExpandInProgress) {
+ return true
+ }
+
+ // Check if user clicked in the empty space left by this collapsed View.
+ if (!isTouchingTheWrappedView(ev)) {
+ blankTouchListener?.invoke()
+ }
+
+ initialYCoord = ev.y
+
+ false // Allow click listeners firing for children.
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (isScrollingUp(ev)) {
+ expand()
+ true
+ } else {
+ false
+ }
+ }
+ else -> {
+ // In general, we don't want to intercept touch events.
+ // They should be handled by the child view.
+ return callParentOnInterceptTouchEvent(ev)
+ }
+ }
+ } else {
+ return if (ev != null && !isTouchingTheWrappedView(ev)) {
+ // If the menu is expanded but smaller than the parent height
+ // and the user touches above the menu, in the empty space.
+ blankTouchListener?.invoke()
+ true
+ } else if (isExpandInProgress) {
+ // Swallow all menu touches while the menu is expanding.
+ true
+ } else {
+ callParentOnInterceptTouchEvent(ev)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun shouldInterceptTouches() = isCollapsed && !isExpandInProgress
+
+ @VisibleForTesting
+ internal fun isTouchingTheWrappedView(ev: MotionEvent): Boolean {
+ val childrenBounds = Rect()
+ wrappedView.getHitRect(childrenBounds)
+ return childrenBounds.contains(ev.x.toInt(), ev.y.toInt())
+ }
+
+ @VisibleForTesting
+ internal fun collapse() {
+ wrappedView.translationY = parentHeight.toFloat() - collapsedHeight
+ wrappedView.updateLayoutParams {
+ height = collapsedHeight
+ }
+ }
+
+ @VisibleForTesting
+ internal fun expand() {
+ isCollapsed = false
+ isExpandInProgress = true
+
+ val initialTranslation = wrappedView.translationY
+ val distanceToExpandedHeight = expandedHeight - collapsedHeight
+ getExpandViewAnimator(distanceToExpandedHeight).apply {
+ doOnEnd {
+ isExpandInProgress = false
+ }
+
+ addUpdateListener {
+ wrappedView.translationY = initialTranslation - it.animatedValue as Int
+ wrappedView.updateLayoutParams {
+ height = collapsedHeight + it.animatedValue as Int
+ }
+ }
+ start()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getExpandViewAnimator(expandDelta: Int): ValueAnimator {
+ return ValueAnimator.ofInt(0, expandDelta).apply {
+ this.interpolator = AccelerateDecelerateInterpolator()
+ this.duration = DEFAULT_DURATION_EXPAND_ANIMATOR
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getOrCalculateCollapsedHeight(): Int {
+ // Memoize the value.
+ // Method will be called multiple times. Result will always be the same.
+ if (collapsedHeight < 0) {
+ collapsedHeight = calculateCollapsedHeight()
+ }
+
+ return collapsedHeight
+ }
+
+ @VisibleForTesting
+ internal fun getOrCalculateExpandedHeight(heightSpec: Int): Int {
+ if (expandedHeight < 0) {
+ // Value from a measurement done with MeasureSpec.UNSPECIFIED.
+ // May need to be capped by the parent height.
+ expandedHeight = wrappedView.measuredHeight
+ }
+
+ val heightSpecSize = MeasureSpec.getSize(heightSpec)
+ // heightSpecSize can be 0 for a MeasureSpec.UNSPECIFIED.
+ // Ignore that, wait for a heightSpec that will contain parent height.
+ if (parentHeight < 0 && heightSpecSize > 0) {
+ parentHeight = heightSpecSize
+
+ // Ensure a menu with a bigger height than the parent will be correctly laid out.
+ expandedHeight = minOf(expandedHeight, parentHeight)
+
+ // Ensure the collapsedHeight we calculated is not bigger than the expanded height
+ // now capped by parent height.
+ // This might happen if the menu is shown in landscape and there is no space to show
+ // the lastVisibleItemIndexWhenCollapsed.
+ if (collapsedHeight >= expandedHeight) {
+ // If there's no space to show the lastVisibleItemIndexWhenCollapsed even if the
+ // wrappedView is collapsed there's no need to collapse the view.
+ collapsedHeight = expandedHeight
+ isExpandInProgress = false
+ isCollapsed = false
+ }
+ }
+
+ return expandedHeight
+ }
+
+ @Suppress("WrongCall")
+ @VisibleForTesting
+ // Used for testing protected super.onMeasure(..) calls will be executed.
+ internal fun callParentOnMeasure(widthSpec: Int, heightSpec: Int) {
+ super.onMeasure(widthSpec, heightSpec)
+ }
+
+ @Suppress("WrongCall")
+ @VisibleForTesting
+ // Used for testing protected super.onInterceptTouchEvent(..) calls will be executed.
+ internal fun callParentOnInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ return super.onInterceptTouchEvent(ev)
+ }
+
+ /**
+ * Whether based on the previous movements, when considering this [event]
+ * it can be inferred that the user is currently scrolling up.
+ */
+ @VisibleForTesting
+ internal fun isScrollingUp(event: MotionEvent): Boolean {
+ val yDistance = initialYCoord - event.y
+
+ return yDistance >= touchSlop
+ }
+
+ // We need a dynamic way to get the intended collapsed height of this view before it will be laid out on the screen.
+ // This method assumes the following layout:
+ // ____________________________________________________
+ // this -> | ----------------------------------- |
+ // | ViewGroup -> | ---------------- | |
+ // | | RecyclerView-> | View | | |
+ // | | | View | | |
+ // | | | View | | |
+ // | | | SpecialView | | |
+ // | | | View | | |
+ // | | ---------------- | |
+ // | ----------------------------------- |
+ // ----------------------------------------------------
+ // for which we want to measure the distance (height) between [this#top, half of SpecialView].
+ // That distance will be the collapsed height of the ViewGroup used when this will be first shown on the screen.
+ // Users will be able to afterwards expand the ViewGroup to the full height.
+ @VisibleForTesting
+ @Suppress("ReturnCount")
+ internal fun calculateCollapsedHeight(): Int {
+ val listView = (wrappedView.getChildAt(0) as RecyclerView)
+ // Reconcile adapter positions with listView children positions.
+ // Avoid IndexOutOfBounds / NullPointer exceptions.
+ val validLastVisibleItemIndexWhenCollapsed = getChildPositionForAdapterIndex(
+ listView,
+ lastVisibleItemIndexWhenCollapsed,
+ )
+ val validStickyItemIndex = getChildPositionForAdapterIndex(
+ listView,
+ stickyItemIndex,
+ )
+
+ // Simple sanity check
+ if (validLastVisibleItemIndexWhenCollapsed >= listView.childCount ||
+ validLastVisibleItemIndexWhenCollapsed <= 0
+ ) {
+ return measuredHeight
+ }
+
+ var result = 0
+ result += wrappedView.marginTop
+ result += wrappedView.marginBottom
+ result += wrappedView.paddingTop
+ result += wrappedView.paddingBottom
+ result += listView.marginTop
+ result += listView.marginBottom
+ result += listView.paddingTop
+ result += listView.paddingBottom
+
+ run loop@{
+ listView.children.forEachIndexed { index, view ->
+ if (index < validLastVisibleItemIndexWhenCollapsed) {
+ result += view.marginTop
+ result += view.marginBottom
+ result += view.measuredHeight
+ } else if (index == validLastVisibleItemIndexWhenCollapsed) {
+ result += view.marginTop
+
+ // Edgecase: if the same item is the sticky footer and the lastVisibleItemIndexWhenCollapsed
+ // the menu will be collapsed to this item but shown with full height.
+ if (index == validStickyItemIndex) {
+ result += view.measuredHeight
+ return@loop
+ } else {
+ result += view.measuredHeight / 2
+ }
+ } else {
+ // If there is a sticky item below we need to add it's height as an offset.
+ // Otherwise the sticky item will cover the the view of lastVisibleItemIndexWhenCollapsed.
+ if (index <= validStickyItemIndex) {
+ result += listView.getChildAt(validStickyItemIndex).measuredHeight
+ }
+ return@loop
+ }
+ }
+ }
+
+ return result
+ }
+
+ /**
+ * In a dynamic menu - one in which items or their positions may change the adapter position and
+ * the RecyclerView position for the same item may differ.
+ * This method helps reconcile that.
+ *
+ * @return the RecyclerView position for the item at the [adapterIndex] in the adapter or
+ * [RecyclerView.NO_POSITION] if there is no child for the indicated adapter position.
+ */
+ @VisibleForTesting
+ internal fun getChildPositionForAdapterIndex(listView: RecyclerView, adapterIndex: Int): Int {
+ listView.children.forEachIndexed { index, view ->
+ if (listView.getChildAdapterPosition(view) == adapterIndex) {
+ return index
+ }
+ }
+
+ return RecyclerView.NO_POSITION
+ }
+
+ internal companion object {
+ @VisibleForTesting
+ const val NOT_CALCULATED_DEFAULT_HEIGHT = -1
+
+ @VisibleForTesting
+ const val NOT_CALCULATED_Y_TOUCH_COORD = 0f
+
+ /**
+ * Duration of the expand animation. Same value as the one from [R.android.integer.config_shortAnimTime]
+ */
+ @VisibleForTesting
+ const val DEFAULT_DURATION_EXPAND_ANIMATOR = 200L
+
+ /**
+ * Wraps a content view in an [ExpandableLayout].
+ *
+ * @param contentView the content view to wrap.
+ * @return a [ExpandableLayout] that wraps the content view.
+ */
+ internal fun wrapContentInExpandableView(
+ contentView: ViewGroup,
+ lastVisibleItemIndexWhenCollapsed: Int = Int.MAX_VALUE,
+ stickyFooterItemIndex: Int = RecyclerView.NO_POSITION,
+ blankTouchListener: (() -> Unit)? = null,
+ ): ExpandableLayout {
+ val expandableView = ExpandableLayout(contentView.context)
+ val params = MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
+ .apply {
+ leftMargin = contentView.marginLeft
+ topMargin = contentView.marginTop
+ rightMargin = contentView.marginRight
+ bottomMargin = contentView.marginBottom
+ }
+ expandableView.addView(contentView, params)
+
+ expandableView.wrappedView = contentView
+ expandableView.stickyItemIndex = stickyFooterItemIndex
+ expandableView.blankTouchListener = blankTouchListener
+ expandableView.lastVisibleItemIndexWhenCollapsed = lastVisibleItemIndexWhenCollapsed
+
+ return expandableView
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/MenuButton.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/MenuButton.kt
new file mode 100644
index 0000000000..f9b3db51cd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/MenuButton.kt
@@ -0,0 +1,211 @@
+/* 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.browser.menu.view
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenu.Orientation
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.MenuButton
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.concept.menu.ext.effects
+import mozilla.components.concept.menu.ext.max
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import mozilla.components.support.ktx.android.view.hideKeyboard
+
+/**
+ * A `three-dot` button used for expanding menus.
+ *
+ * If you are using a browser toolbar, do not use this class directly.
+ */
+class MenuButton @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr),
+ MenuButton,
+ View.OnClickListener,
+ Observable<MenuButton.Observer> by ObserverRegistry() {
+
+ private val menuControllerObserver = object : MenuController.Observer {
+ /**
+ * Change the menu button appearance when the menu list changes.
+ */
+ override fun onMenuListSubmit(list: List<MenuCandidate>) {
+ val effect = list.effects().max()
+
+ // If a highlighted item is found, show the indicator
+ setEffect(effect)
+ }
+
+ override fun onDismiss() = notifyObservers { onDismiss() }
+ }
+
+ /**
+ * Listener called when the menu is shown.
+ */
+ @Deprecated("Use the Observable interface to listen for onShow")
+ var onShow: () -> Unit = {}
+
+ /**
+ * Listener called when the menu is dismissed.
+ */
+ @Deprecated("Use the Observable interface to listen for onDismiss")
+ var onDismiss: () -> Unit = {}
+
+ /**
+ * Callback to get the orientation for the menu.
+ * This is called every time the menu should be displayed.
+ * This has no effect when a [MenuController] is set.
+ */
+ var getOrientation: () -> Orientation = {
+ BrowserMenu.determineMenuOrientation(parent as? View?)
+ }
+
+ /**
+ * Sets a [MenuController] that will be used to create a menu when this button is clicked.
+ * If present, [menuBuilder] will be ignored.
+ */
+ override var menuController: MenuController? = null
+ set(value) {
+ // Clean up old controller
+ field?.dismiss()
+ field?.unregister(menuControllerObserver)
+
+ // Attach new controller
+ field = value
+ value?.register(menuControllerObserver, this)
+ }
+
+ /**
+ * Sets a [BrowserMenuBuilder] that will be used to create a menu when this button is clicked.
+ */
+ var menuBuilder: BrowserMenuBuilder? = null
+ set(value) {
+ field = value
+ menu?.dismiss()
+ if (value == null) menu = null
+ }
+
+ var recordClickEvent: () -> Unit = {}
+
+ @VisibleForTesting internal var menu: BrowserMenu? = null
+
+ private val menuIcon: ImageView
+ private val highlightView: ImageView
+ private val notificationIconView: ImageView
+
+ init {
+ View.inflate(context, R.layout.mozac_browser_menu_button, this)
+ setOnClickListener(this)
+ menuIcon = findViewById(R.id.icon)
+ highlightView = findViewById(R.id.highlight)
+ notificationIconView = findViewById(R.id.notification_dot)
+
+ // Hook up deprecated callbacks using new observer system
+ @Suppress("Deprecation")
+ val internalObserver = object : MenuButton.Observer {
+ override fun onShow() = this@MenuButton.onShow()
+ override fun onDismiss() = this@MenuButton.onDismiss()
+ }
+ register(internalObserver)
+ }
+
+ /**
+ * Shows the menu, or dismisses it if already open.
+ */
+ override fun onClick(v: View) {
+ this.hideKeyboard()
+ recordClickEvent()
+
+ // If a legacy menu is open, dismiss it.
+ if (menu != null) {
+ menu?.dismiss()
+ return
+ }
+
+ val menuController = menuController
+ if (menuController != null) {
+ // Use the newer menu controller if set
+ menuController.show(anchor = this)
+ } else {
+ menu = menuBuilder?.build(context)
+ val endAlwaysVisible = menuBuilder?.endOfMenuAlwaysVisible ?: false
+ menu?.show(
+ anchor = this,
+ orientation = getOrientation(),
+ endOfMenuAlwaysVisible = endAlwaysVisible,
+ ) {
+ menu = null
+ notifyObservers { onDismiss() }
+ }
+ }
+ notifyObservers { onShow() }
+ }
+
+ /**
+ * Show the indicator for a browser menu highlight.
+ */
+ fun setHighlight(highlight: BrowserMenuHighlight?) =
+ setEffect(highlight?.asEffect(context))
+
+ /**
+ * Show the indicator for a browser menu effect.
+ */
+ override fun setEffect(effect: MenuEffect?) {
+ when (effect) {
+ is HighPriorityHighlightEffect -> {
+ highlightView.imageTintList = ColorStateList.valueOf(effect.backgroundTint)
+ highlightView.visibility = View.VISIBLE
+ notificationIconView.visibility = View.GONE
+ }
+ is LowPriorityHighlightEffect -> {
+ notificationIconView.setColorFilter(effect.notificationTint)
+ highlightView.visibility = View.GONE
+ notificationIconView.visibility = View.VISIBLE
+ }
+ null -> {
+ highlightView.visibility = View.GONE
+ notificationIconView.visibility = View.GONE
+ }
+ }
+ }
+
+ /**
+ * Sets the tint of the 3-dot menu icon.
+ */
+ override fun setColorFilter(@ColorInt color: Int) {
+ menuIcon.setColorFilter(color)
+ }
+
+ /**
+ * Dismiss the menu, if open.
+ */
+ fun dismissMenu() {
+ menuController?.dismiss()
+ menu?.dismiss()
+ }
+
+ /**
+ * Invalidates the [BrowserMenu], if open.
+ */
+ fun invalidateBrowserMenu() {
+ menu?.invalidate()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManager.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManager.kt
new file mode 100644
index 0000000000..b2c2d6a18a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManager.kt
@@ -0,0 +1,74 @@
+/* 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.browser.menu.view
+
+import android.content.Context
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Vertical LinearLayoutManager that will ensure an item at a position specified through
+ * [StickyItemsAdapter.isStickyItem] will not scroll past the list bottom.
+ *
+ * The list would otherwise scroll normally with the other elements being scrolled beneath the sticky item.
+ *
+ * @param context [Context] needed for various Android interactions.
+ * @param reverseLayout When set to true, layouts from end to start.
+ */
+open class StickyFooterLinearLayoutManager<T> constructor(
+ context: Context,
+ reverseLayout: Boolean = false,
+) : StickyItemsLinearLayoutManager<T>(
+ context,
+ StickyItemPlacement.BOTTOM,
+ reverseLayout,
+) where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter {
+
+ override fun scrollToIndicatedPositionWithOffset(
+ position: Int,
+ offset: Int,
+ actuallyScrollToPositionWithOffset: (Int, Int) -> Unit,
+ ) {
+ // The following scenarios are handled:
+ // - if position is bigger than [stickyItemPosition]
+ // -> the default behavior will have the list scrolled downwards enough to show that.
+ // - if position is the one of the stickyItem
+ // -> the default behavior will scroll to exactly the header. Perfect match.
+ // - if position is before that of the [stickyItem] and does not fit the screen
+ // -> only scenario we need to handle: the sticky footer must be shown and the default implementation
+ // would scroll to show as the last item in the list the item at [position]. But that is where the sticky
+ // item is anchored. Need to scroll to the next position so that that item will be obscured by the sticky
+ // item and not the now above item at [position].
+ //
+ // Providing any offsets with the stickyView shown and the above scenarios handles means they are handled also.
+
+ if (position < stickyItemPosition && getChildAt(position) == null) {
+ actuallyScrollToPositionWithOffset(position + 1, offset)
+ return
+ }
+
+ actuallyScrollToPositionWithOffset(position, offset)
+ }
+
+ override fun shouldStickyItemBeShownForCurrentPosition(): Boolean {
+ if (stickyItemPosition == RecyclerView.NO_POSITION) {
+ return false
+ }
+
+ // The item at [stickyItemPosition] should be anchored to the top if:
+ // - it or a lower indexed item is shown at the bottom of the list
+ // - the last shown item is translated downwards off screen
+ // (happens when [scrollToPositionWithOffset] was called with a big enough offset)
+ val lastVisibleElement = stickyItemView?.let { childCount - 2 } ?: childCount - 1
+ return getAdapterPositionForItemIndex(lastVisibleElement) <= stickyItemPosition
+ }
+
+ override fun getY(itemView: View): Float {
+ return when (reverseLayout) {
+ true -> 0f
+ false -> height - itemView.height.toFloat()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManager.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManager.kt
new file mode 100644
index 0000000000..27a758a891
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManager.kt
@@ -0,0 +1,73 @@
+/* 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.browser.menu.view
+
+import android.content.Context
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Vertical LinearLayoutManager that will ensure an item at a position specified through
+ * [StickyItemsAdapter.isStickyItem] will not scroll past the list's top.
+ *
+ * The list would otherwise scroll normally with the other elements being scrolled beneath the sticky item.
+ *
+ * @param context [Context] needed for various Android interactions.
+ * @param reverseLayout When set to true, layouts from end to start.
+ */
+open class StickyHeaderLinearLayoutManager<T> constructor(
+ context: Context,
+ reverseLayout: Boolean = false,
+) : StickyItemsLinearLayoutManager<T>(
+ context,
+ StickyItemPlacement.TOP,
+ reverseLayout,
+) where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter {
+
+ override fun scrollToIndicatedPositionWithOffset(
+ position: Int,
+ offset: Int,
+ actuallyScrollToPositionWithOffset: (Int, Int) -> Unit,
+ ) {
+ // The following scenarios are handled:
+ // - if position is smaller than [stickyItemPosition]
+ // -> the default behavior will have the list scrolled upwards enough to show that.
+ // - if position is the one of the stickyItem
+ // -> the default behavior will scroll to exactly the header. Perfect match.
+ // - if position is bigger than [stickyItemPosition]
+ // -> only scenario we need to handle: default implementation would scroll to show at the top of the list
+ // the item at that position. But that is where the sticky item is anchored. Need to ask for the item at
+ // the before position being shown at the top of the list and let that be obscured by the sticky item.
+ //
+ // Providing any offsets with the stickyView shown and the above scenarios handles means they are handled also.
+
+ if (position + 1 > stickyItemPosition) {
+ actuallyScrollToPositionWithOffset(position - 1, offset)
+ return
+ }
+
+ actuallyScrollToPositionWithOffset(position, offset)
+ }
+
+ override fun shouldStickyItemBeShownForCurrentPosition(): Boolean {
+ if (stickyItemPosition == RecyclerView.NO_POSITION) {
+ return false
+ }
+
+ // The item at [stickyItemPosition] should be anchored to the top if:
+ // - it or a below item is shown at the top of the list
+ // - the first shown item is translated upwards off screen
+ // (happens when [scrollToPositionWithOffset] was called with a big enough offset)
+ return getAdapterPositionForItemIndex(0) >= stickyItemPosition ||
+ getChildAt(0)?.bottom ?: 1 <= 0 // return false if there is no item at index 0
+ }
+
+ override fun getY(itemView: View): Float {
+ return when (reverseLayout) {
+ true -> height - itemView.height.toFloat()
+ false -> 0f
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyItemLayoutManager.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyItemLayoutManager.kt
new file mode 100644
index 0000000000..f49ec0bf2c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyItemLayoutManager.kt
@@ -0,0 +1,483 @@
+/* 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.browser.menu.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PointF
+import android.os.Parcelable
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.parcelize.Parcelize
+
+// Inspired from
+// https://github.com/qiujayen/sticky-layoutmanager/blob/b1ddb086db5b04ff3c5357dabe1bff47a935dd37/
+// sticky-layoutmanager/src/main/java/com/jay/widget/StickyHeadersLinearLayoutManager.java
+
+/**
+ * Contract needed to be implemented by all [RecyclerView.Adapter]s
+ * that want to display a list with a sticky header / footer.
+ */
+interface StickyItemsAdapter {
+ /**
+ * Whether this should be considered a sticky item.
+ *
+ * All items will be checked. Only the last one presenting as sticky will be used as such.
+ */
+ fun isStickyItem(position: Int): Boolean
+
+ /**
+ * Callback allowing any customization for the view that will become sticky.
+ */
+ fun setupStickyItem(stickyItem: View) {}
+
+ /**
+ * Callback allowing cleanup after the previous sticky view becomes a regular view.
+ */
+ fun tearDownStickyItem(stickyItem: View) {}
+}
+
+/**
+ * Whether the sticky item should be a header or a footer.
+ */
+enum class StickyItemPlacement {
+ /**
+ * The sticky item will be fixed at the top of the list.
+ *
+ * If the list is scrolled down until past the sticky item's position that view
+ * will become a regular view and will be scrolled down as the others.
+ *
+ * If the list is scrolled up past the sticky item's position that view
+ * will be anchored to the top of the list, always being shown as the first item.
+ */
+ TOP,
+
+ /**
+ * The sticky item will be fixed at the bottom of the list.
+ *
+ * If the list is scrolled up until past the sticky item's position that view
+ * will become a regular view and will be scrolled up as the others.
+ *
+ * If the list is scrolled down past the sticky item's position that view
+ * will be anchored to the bottom of the list, always being shown as the last item.
+ */
+ BOTTOM,
+}
+
+/**
+ * Vertical LinearLayoutManager that will prevent certain items from being scrolled off-screen.
+ *
+ * @param context [Context] needed for various Android interactions.
+ * @param stickyItemPlacement whether the sticky item should be blocked from being scrolled off
+ * to the top of the screen or off to the bottom of the screen.
+ * @param reverseLayout When set to true, layouts from end to start.
+ */
+@Suppress("TooManyFunctions")
+abstract class StickyItemsLinearLayoutManager<T> constructor(
+ context: Context,
+ private val stickyItemPlacement: StickyItemPlacement,
+ reverseLayout: Boolean = false,
+) : LinearLayoutManager(context, RecyclerView.VERTICAL, reverseLayout)
+ where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter {
+
+ @VisibleForTesting
+ internal var listAdapter: T? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal var stickyItemPosition = RecyclerView.NO_POSITION
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal var stickyItemView: View? = null
+
+ // Allows to re-evaluate and display a possibly new sticky item if data / adapter changed.
+ @VisibleForTesting
+ internal var stickyItemPositionsObserver = ItemPositionsAdapterDataObserver()
+
+ // Save / Restore scroll state
+ @VisibleForTesting
+ internal var scrollPosition = RecyclerView.NO_POSITION
+
+ @VisibleForTesting
+ internal var scrollOffset = 0
+
+ /**
+ * @see [LinearLayoutManager.scrollToPositionWithOffset]
+ *
+ * @param position list item index which needs to be shown.
+ * @param offset optional distance offset from the top of the list to be applied after scrolling to [position]
+ * @param actuallyScrollToPositionWithOffset callback to be used for actually scrolling to an updated position
+ * ad offset based on the relation with the sticky item.
+ *
+ * Use [setScrollState] before and after
+ */
+ abstract fun scrollToIndicatedPositionWithOffset(
+ position: Int,
+ offset: Int,
+ actuallyScrollToPositionWithOffset: (Int, Int) -> Unit,
+ )
+
+ /**
+ * Whether the sticky item should be shown.
+ *
+ * Expected to return if the sticky header item is scrolled past the list top or the sticky bottom item
+ * is scrolled past the list bottom.
+ */
+ abstract fun shouldStickyItemBeShownForCurrentPosition(): Boolean
+
+ /**
+ * Returns the position in the Y axis to position the header appropriately,
+ * depending on direction and [android.R.attr.clipToPadding].
+ */
+ abstract fun getY(itemView: View): Float
+
+ override fun onAttachedToWindow(recyclerView: RecyclerView) {
+ super.onAttachedToWindow(recyclerView)
+ setAdapter(recyclerView.adapter)
+ }
+
+ override fun onAdapterChanged(
+ oldAdapter: RecyclerView.Adapter<*>?,
+ newAdapter: RecyclerView.Adapter<*>?,
+ ) {
+ super.onAdapterChanged(oldAdapter, newAdapter)
+ setAdapter(newAdapter)
+ }
+
+ override fun onSaveInstanceState(): Parcelable {
+ return SavedState(
+ superState = super.onSaveInstanceState(),
+ scrollPosition = scrollPosition,
+ scrollOffset = scrollOffset,
+ )
+ }
+
+ override fun onRestoreInstanceState(state: Parcelable?) {
+ (state as? mozilla.components.browser.menu.view.SavedState)?.let {
+ scrollPosition = it.scrollPosition
+ scrollOffset = it.scrollOffset
+ super.onRestoreInstanceState(it.superState)
+ }
+ }
+
+ override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
+ restoreView { super.onLayoutChildren(recycler, state) }
+
+ if (!state.isPreLayout) {
+ updateStickyItem(recycler, true)
+ }
+ }
+
+ override fun scrollVerticallyBy(
+ dy: Int,
+ recycler: RecyclerView.Recycler,
+ state: RecyclerView.State?,
+ ): Int {
+ val distanceScrolled = restoreView { super.scrollVerticallyBy(dy, recycler, state) }
+ if (distanceScrolled != 0) {
+ updateStickyItem(recycler, false)
+ }
+ return distanceScrolled
+ }
+
+ override fun findLastVisibleItemPosition(): Int =
+ restoreView { super.findLastVisibleItemPosition() }
+
+ override fun findFirstVisibleItemPosition(): Int =
+ restoreView { super.findFirstVisibleItemPosition() }
+
+ override fun findFirstCompletelyVisibleItemPosition(): Int =
+ restoreView { super.findFirstCompletelyVisibleItemPosition() }
+
+ override fun findLastCompletelyVisibleItemPosition(): Int =
+ restoreView { super.findLastCompletelyVisibleItemPosition() }
+
+ override fun computeVerticalScrollExtent(state: RecyclerView.State): Int =
+ restoreView { super.computeVerticalScrollExtent(state) }
+
+ override fun computeVerticalScrollOffset(state: RecyclerView.State): Int =
+ restoreView { super.computeVerticalScrollOffset(state) }
+
+ override fun computeVerticalScrollRange(state: RecyclerView.State): Int =
+ restoreView { super.computeVerticalScrollRange(state) }
+
+ override fun computeScrollVectorForPosition(targetPosition: Int): PointF? =
+ restoreView { super.computeScrollVectorForPosition(targetPosition) }
+
+ override fun scrollToPosition(position: Int) {
+ if (stickyItemView != null) {
+ scrollToPositionWithOffset(position, INVALID_OFFSET)
+ } else {
+ super.scrollToPosition(position)
+ }
+ }
+
+ override fun scrollToPositionWithOffset(position: Int, offset: Int) {
+ if (stickyItemView != null) {
+ // Reset pending scroll.
+ setScrollState(RecyclerView.NO_POSITION, INVALID_OFFSET)
+
+ scrollToIndicatedPositionWithOffset(position, offset) { updatedPosition, updatedOffset ->
+ super.scrollToPositionWithOffset(updatedPosition, updatedOffset)
+ }
+
+ // Remember this position and offset and scroll to it to trigger creating the sticky view.
+ setScrollState(position, offset)
+ } else {
+ super.scrollToPositionWithOffset(position, offset)
+ }
+ }
+
+ override fun onFocusSearchFailed(
+ focused: View,
+ focusDirection: Int,
+ recycler: RecyclerView.Recycler,
+ state: RecyclerView.State,
+ ): View? = restoreView { super.onFocusSearchFailed(focused, focusDirection, recycler, state) }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal fun getAdapterPositionForItemIndex(index: Int): Int {
+ return (getChildAt(index)?.layoutParams as? RecyclerView.LayoutParams)
+ ?.absoluteAdapterPosition ?: RecyclerView.NO_POSITION
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @VisibleForTesting
+ internal fun setAdapter(newAdapter: RecyclerView.Adapter<*>?) {
+ listAdapter?.unregisterAdapterDataObserver(stickyItemPositionsObserver)
+
+ (newAdapter as? T)?.let {
+ listAdapter = newAdapter
+ listAdapter?.registerAdapterDataObserver(stickyItemPositionsObserver)
+ stickyItemPositionsObserver.onChanged()
+ } ?: run {
+ listAdapter = null
+ stickyItemView = null
+ }
+ }
+
+ /**
+ * Perform any [operation] ignoring the sticky item. Accomplished by:
+ * - detaching the sticky view
+ * - performing the [operation]
+ * - reattaching the sticky view.
+ */
+ @VisibleForTesting
+ internal fun <T> restoreView(operation: () -> T): T {
+ stickyItemView?.let(this::detachView)
+ val result = operation()
+ stickyItemView?.let(this::attachView)
+ return result
+ }
+
+ /**
+ * Updates the sticky item state (creation, binding, display).
+ *
+ * To be called whenever there's a layout or scroll.
+ *
+ * @param recycler [RecyclerView.Recycler] instance handling views recycling
+ * @param layout whether this is called while layout or while scrolling.
+ */
+ @VisibleForTesting
+ internal fun updateStickyItem(recycler: RecyclerView.Recycler, layout: Boolean) {
+ if (shouldStickyItemBeShownForCurrentPosition()) {
+ if (stickyItemView == null) {
+ createStickyView(recycler, stickyItemPosition)
+ }
+
+ if (layout) {
+ bindStickyItem(stickyItemView!!)
+ }
+
+ stickyItemView?.let {
+ it.translationY = getY(it)
+ }
+ } else {
+ stickyItemView?.let {
+ recycleStickyItem(recycler)
+ }
+ }
+ }
+
+ /**
+ * Construct and configure a [RecyclerView.ViewHolder] for [position],
+ * including measure, layout, and data binding and assigns this to [stickyItemView].
+ */
+ @VisibleForTesting
+ internal fun createStickyView(recycler: RecyclerView.Recycler, position: Int) {
+ val stickyItem = recycler.getViewForPosition(position)
+
+ listAdapter?.setupStickyItem(stickyItem)
+
+ // Add sticky item as a child view, to be detached / reattached whenever
+ // LinearLayoutManager#fill() is called, which happens on layout and scroll (see overrides).
+ addView(stickyItem)
+ measureAndLayout(stickyItem)
+
+ // Hide this new sticky item from the parent LayoutManager, as it's fully managed by this LayoutManager.
+ ignoreView(stickyItem)
+
+ stickyItemView = stickyItem
+ }
+
+ /**
+ * Binds a new [stickyItem].
+ */
+ @VisibleForTesting
+ internal fun bindStickyItem(stickyItem: View) {
+ measureAndLayout(stickyItem)
+
+ // If we have a pending scroll wait until the end of layout and scroll again.
+ if (scrollPosition != RecyclerView.NO_POSITION) {
+ stickyItem.viewTreeObserver.addOnGlobalLayoutListener(
+ object :
+ ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ stickyItem.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ if (scrollPosition != RecyclerView.NO_POSITION) {
+ scrollToPositionWithOffset(scrollPosition, scrollOffset)
+ setScrollState(RecyclerView.NO_POSITION, INVALID_OFFSET)
+ }
+ }
+ },
+ )
+ }
+ }
+
+ /**
+ * Measures and lays out [stickyItemView].
+ */
+ @VisibleForTesting
+ internal fun measureAndLayout(stickyItem: View) {
+ measureChildWithMargins(stickyItem, 0, 0)
+ stickyItem.layout(
+ paddingLeft,
+ 0,
+ width - paddingRight,
+ stickyItem.measuredHeight,
+ )
+ }
+
+ /**
+ * Returns a no longer needed [stickyItemView] View to the [RecyclerView]'s [RecyclerView.RecycledViewPool]
+ * allowing it to be recycled and reused later after being re-binded in the Adapter.
+ *
+ * @param recycler [RecyclerView.Recycler] instance handling views recycling.
+ */
+ @VisibleForTesting
+ internal fun recycleStickyItem(recycler: RecyclerView.Recycler?) {
+ val stickyItem = stickyItemView ?: return
+ stickyItemView = null
+
+ stickyItem.translationY = 0f
+
+ listAdapter?.tearDownStickyItem(stickyItem)
+
+ // Stop ignoring sticky header so that it can be recycled.
+ stopIgnoringView(stickyItem)
+
+ removeView(stickyItem)
+ recycler?.recycleView(stickyItem)
+ }
+
+ @VisibleForTesting
+ internal fun setScrollState(position: Int, offset: Int) {
+ scrollPosition = position
+ scrollOffset = offset
+ }
+
+ /**
+ * Observer for any changes in the items displayed or even when the Adapter changes.
+ */
+ @VisibleForTesting
+ internal inner class ItemPositionsAdapterDataObserver : RecyclerView.AdapterDataObserver() {
+ override fun onChanged() = handleChange()
+
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = handleChange()
+
+ override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) = handleChange()
+
+ override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) = handleChange()
+
+ @VisibleForTesting
+ internal fun handleChange() {
+ listAdapter?.let {
+ stickyItemPosition = calculateNewStickyItemPosition(it)
+
+ // Remove sticky header immediately. A layout will follow.
+ if (stickyItemView != null) {
+ recycleStickyItem(null)
+ }
+ }
+ }
+
+ /**
+ * Get the position of the closest to the anchor sticky item.
+ *
+ * @return sticky item's index in the adapter or RecyclerView.NO_POSITION is such an item doesn't exists.
+ */
+ @VisibleForTesting
+ internal fun calculateNewStickyItemPosition(adapter: T): Int {
+ var newStickyItemPosition = RecyclerView.NO_POSITION
+
+ if (stickyItemPlacement == StickyItemPlacement.TOP) {
+ for (i in (itemCount - 1) downTo 0) {
+ if (adapter.isStickyItem(i)) {
+ newStickyItemPosition = i
+ }
+ }
+ } else {
+ for (i in 0 until itemCount) {
+ if (adapter.isStickyItem(i)) {
+ newStickyItemPosition = i
+ }
+ }
+ }
+
+ return newStickyItemPosition
+ }
+ }
+
+ companion object {
+ /**
+ * Get a new instance of a vertical [LinearLayoutManager] that can show one specific item
+ * as a fixed header / footer in the list, be that reversed or not.
+ *
+ * @param stickyItemPlacement whether the sticky item should be anchored to the top or bottom of the list
+ * @param reverseLayout when set to true, layouts from end to start.
+ */
+ fun <T> get(
+ context: Context,
+ stickyItemPlacement: StickyItemPlacement = StickyItemPlacement.TOP,
+ reverseLayout: Boolean = false,
+ ): StickyItemsLinearLayoutManager<T>
+ where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter {
+ return when (stickyItemPlacement) {
+ StickyItemPlacement.TOP -> StickyHeaderLinearLayoutManager(
+ context,
+ reverseLayout,
+ )
+ StickyItemPlacement.BOTTOM -> StickyFooterLinearLayoutManager(
+ context,
+ reverseLayout,
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Save / restore existing [RecyclerView] state and scrolling position and offset.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+@VisibleForTesting
+internal data class SavedState(
+ val superState: Parcelable?,
+ val scrollPosition: Int,
+ val scrollOffset: Int,
+) : Parcelable
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_bottom.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_bottom.xml
new file mode 100644
index 0000000000..6d27c410ea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_bottom.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="5%"
+ android:pivotY="100%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_top.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_top.xml
new file mode 100644
index 0000000000..fc141bdbd0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_top.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="5%"
+ android:pivotY="5%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_bottom.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_bottom.xml
new file mode 100644
index 0000000000..89153ca6e5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_bottom.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="95%"
+ android:pivotY="100%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_top.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_top.xml
new file mode 100644
index 0000000000..f0485403ef
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_top.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="95%"
+ android:pivotY="5%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_exit.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_exit.xml
new file mode 100644
index 0000000000..226166c109
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_exit.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="1"
+ android:toAlpha="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_browser_menu_notification_icon.xml b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_browser_menu_notification_icon.xml
new file mode 100644
index 0000000000..8afb175afe
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_browser_menu_notification_icon.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="8dp"
+ android:height="8dp"
+ android:viewportWidth="10"
+ android:viewportHeight="10">
+ <path
+ android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#fff"
+ android:strokeColor="#000" />
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_indicator.xml b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_indicator.xml
new file mode 100644
index 0000000000..33d8ad19b4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_indicator.xml
@@ -0,0 +1,29 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <group
+ android:scaleX="0.714"
+ android:scaleY="0.714"
+ android:pivotX="20"
+ android:pivotY="20">
+ <path
+ android:pathData="m38.622,27.309a8,8 0,0 0,-11.314 11.314,19.949 19.949,0 0,1 -7.308,1.377c-11.046,0 -20,-8.954 -20,-20s8.954,-20 20,-20 20,8.954 20,20c0,2.58 -0.488,5.045 -1.378,7.309z"
+ android:fillColor="#ffffff"
+ android:fillAlpha=".4" />
+ <path
+ android:pathData="M33,33m-6.4,0a6.4,6.4 0,1 1,12.8 0a6.4,6.4 0,1 1,-12.8 0"
+ android:fillColor="#ffffff"
+ android:fillAlpha=".4" />
+ <path
+ android:pathData="M33,33m-4.3,0a4.3,4.3 0,1 1,8.6 0a4.3,4.3 0,1 1,-8.6 0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#ffffff"
+ android:strokeColor="#000000" />
+ </group>
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_notification.xml b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_notification.xml
new file mode 100644
index 0000000000..b7b5ced0c0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_notification.xml
@@ -0,0 +1,21 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <group
+ android:translateX="25.55"
+ android:translateY="5.55"
+ android:scaleX="0.75"
+ android:scaleY="0.75">
+ <path
+ android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#fff"
+ android:strokeColor="#000" />
+ </group>
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/drawable/rounded_corner.xml b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/rounded_corner.xml
new file mode 100644
index 0000000000..892c34799d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/rounded_corner.xml
@@ -0,0 +1,9 @@
+<?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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="4dp" />
+</shape> \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu.xml
new file mode 100644
index 0000000000..83e075a2e0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu.xml
@@ -0,0 +1,25 @@
+<?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/. -->
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu"
+ android:id="@+id/mozac_browser_menu_menuView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:cardCornerRadius="@dimen/mozac_browser_menu_corner_radius"
+ app:cardElevation="@dimen/mozac_browser_menu_elevation"
+ app:cardUseCompatPadding="true">
+
+ <mozilla.components.browser.menu.view.DynamicWidthRecyclerView
+ android:id="@+id/mozac_browser_menu_recyclerView"
+ android:paddingTop="@dimen/mozac_browser_menu_padding_vertical"
+ android:paddingBottom="@dimen/mozac_browser_menu_padding_vertical"
+ android:overScrollMode="never"
+ android:layout_width="@dimen/mozac_browser_menu_width"
+ android:layout_height="wrap_content"
+ tools:listitem="@layout/mozac_browser_menu_item_simple" />
+
+</androidx.cardview.widget.CardView>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_button.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_button.xml
new file mode 100644
index 0000000000..78193dce10
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_button.xml
@@ -0,0 +1,39 @@
+<?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/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="?android:selectableItemBackgroundBorderless"
+ tools:parentTag="android.widget.FrameLayout">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/highlight"
+ app:srcCompat="@drawable/mozac_menu_indicator"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu_highlighted"
+ android:visibility="gone" />
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ app:srcCompat="@drawable/mozac_ic_ellipsis_vertical_24"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu_button" />
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/notification_dot"
+ app:srcCompat="@drawable/mozac_menu_notification"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu_highlighted"
+ android:visibility="gone" />
+
+</merge>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_category.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_category.xml
new file mode 100644
index 0000000000..15591618df
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_category.xml
@@ -0,0 +1,11 @@
+<?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/. -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/category_text"
+ style="@style/Mozac.Browser.Menu.Item.Category"
+ android:layout_width="match_parent"
+ android:gravity="center_vertical"
+ tools:text="Category" />
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_item.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_item.xml
new file mode 100644
index 0000000000..b956d85bd7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_item.xml
@@ -0,0 +1,92 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu.Item.Container"
+ tools:ignore="UnusedAttribute"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/image"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center"
+ android:background="@android:color/transparent"
+ android:importantForAccessibility="no"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:src="@android:drawable/screen_background_dark" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/notification_dot"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Icon"
+ android:layout_width="8dp"
+ android:layout_height="8dp"
+ android:translationX="@dimen/mozac_browser_menu_highlightable_notification_translate_x"
+ android:translationY="@dimen/mozac_browser_menu_highlightable_notification_translate_y"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/mozac_browser_menu_highlighted"
+ android:visibility="gone"
+ app:layout_constraintTop_toTopOf="@id/image"
+ app:layout_constraintEnd_toEndOf="@id/image"
+ app:srcCompat="@drawable/mozac_browser_menu_notification_icon"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/text"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Label"
+ android:layout_width="wrap_content"
+ android:layout_centerVertical="true"
+ android:background="@android:color/transparent"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/end_image"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toEndOf="@+id/image"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Item" />
+
+ <TextView
+ android:id="@+id/highlight_text"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Label"
+ android:layout_width="wrap_content"
+ android:layout_centerVertical="true"
+ android:background="@android:color/transparent"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/end_image"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toEndOf="@+id/image"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Highlighted Item" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/end_image"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Icon"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:layout_gravity="center_vertical"
+ android:background="@android:color/transparent"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:src="@android:drawable/screen_background_dark" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_switch.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_switch.xml
new file mode 100644
index 0000000000..58d6fcef50
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_switch.xml
@@ -0,0 +1,60 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu.Item.Container"
+ tools:ignore="UnusedAttribute"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/image"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center"
+ android:background="@android:color/transparent"
+ android:importantForAccessibility="no"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:src="@android:drawable/screen_background_dark" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/notification_dot"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Icon"
+ android:layout_width="8dp"
+ android:layout_height="8dp"
+ android:translationX="@dimen/mozac_browser_menu_highlightable_notification_translate_x"
+ android:translationY="@dimen/mozac_browser_menu_highlightable_notification_translate_y"
+ android:background="@android:color/transparent"
+ android:importantForAccessibility="no"
+ android:visibility="gone"
+ app:layout_constraintTop_toTopOf="@id/image"
+ app:layout_constraintEnd_toEndOf="@id/image"
+ app:srcCompat="@drawable/mozac_browser_menu_notification_icon"
+ tools:visibility="visible" />
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/switch_widget"
+ style="@style/Mozac.Browser.Menu.Item.Text"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:orientation="vertical"
+ android:paddingStart="@dimen/mozac_browser_menu_item_image_text_label_padding_start"
+ android:paddingEnd="0dp"
+ android:textAlignment="viewStart"
+ tools:text="Item"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/image"
+ app:layout_constraintTop_toTopOf="parent" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_checkbox.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_checkbox.xml
new file mode 100644
index 0000000000..ebd0991816
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_checkbox.xml
@@ -0,0 +1,16 @@
+<?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/. -->
+<androidx.appcompat.widget.AppCompatCheckBox xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu.Item.Text"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height"
+ android:button="@null"
+ android:drawableEnd="?android:attr/listChoiceIndicatorMultiple"
+ android:drawablePadding="@dimen/mozac_browser_menu_checkbox_padding"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ tools:text="Item" />
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_divider.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_divider.xml
new file mode 100644
index 0000000000..819ead6066
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_divider.xml
@@ -0,0 +1,8 @@
+<?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/. -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Mozac.Browser.Menu.Item.Divider.Horizontal"
+ android:importantForAccessibility="no"
+ android:clickable="false"/>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_switch.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_switch.xml
new file mode 100644
index 0000000000..56a20c16e6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_switch.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/switch_widget"
+ style="@style/Mozac.Browser.Menu.Item.Text"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height"
+ android:drawablePadding="@dimen/mozac_browser_menu_item_image_text_icon_padding"
+ android:paddingStart="@dimen/mozac_browser_menu_item_container_padding_start"
+ android:paddingEnd="@dimen/mozac_browser_menu_item_container_padding_end"
+ android:textAlignment="viewStart"
+ app:drawableStartCompat="@android:drawable/screen_background_dark" />
+
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text.xml
new file mode 100644
index 0000000000..fc72b27707
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text.xml
@@ -0,0 +1,32 @@
+<?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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu.Item.Container"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ tools:ignore="UseCompoundDrawables">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/image"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Icon"
+ android:clickable="false"
+ android:background="@android:color/transparent"
+ android:importantForAccessibility="no"
+ tools:src="@android:drawable/screen_background_dark"/>
+
+ <TextView
+ android:id="@+id/text"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Label"
+ android:clickable="false"
+ android:focusable="false"
+ android:background="@android:color/transparent"
+ android:gravity="center_vertical"
+ tools:text="Item"
+ android:importantForAccessibility="no"/>
+</LinearLayout>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text_checkbox_button.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text_checkbox_button.xml
new file mode 100644
index 0000000000..062d8efd0b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text_checkbox_button.xml
@@ -0,0 +1,69 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu.Item.Checkbox.Container"
+ android:layout_width="match_parent"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ tools:ignore="UseCompoundDrawables">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/image"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Icon"
+ android:background="@android:color/transparent"
+ android:importantForAccessibility="no"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:src="@android:drawable/screen_background_dark" />
+
+ <TextView
+ android:id="@+id/text"
+ style="@style/Mozac.Browser.Menu.Item.Checkbox.Label"
+ android:background="@android:color/transparent"
+ android:gravity="center_vertical"
+ android:importantForAccessibility="no"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/checkbox"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintStart_toEndOf="@id/image"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Item" />
+
+ <View
+ android:id="@+id/accessibilityRegion"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:clickable="true"
+ android:focusable="true"
+ android:importantForAccessibility="yes"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="@id/text"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.appcompat.widget.AppCompatCheckBox
+ android:id="@+id/checkbox"
+ style="@style/Mozac.Browser.Menu.Item.Checkbox.Text"
+ android:button="@null"
+ android:drawablePadding="7dp"
+ android:textAlignment="gravity"
+ android:gravity="center_vertical"
+ app:layout_constraintStart_toEndOf="@id/text"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_default="wrap"
+ tools:drawableStartCompat="@android:drawable/star_big_off"
+ tools:text="Add"
+ tools:textOff="Edit"
+ tools:textOn="Add" />
+</androidx.constraintlayout.widget.ConstraintLayout>
+
+
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_parent_menu.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_parent_menu.xml
new file mode 100644
index 0000000000..3d361dcfbc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_parent_menu.xml
@@ -0,0 +1,48 @@
+<?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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ style="@style/Mozac.Browser.Menu.Item.Container"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ tools:ignore="UseCompoundDrawables">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/image"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Icon"
+ android:clickable="false"
+ android:background="@android:color/transparent"
+ android:importantForAccessibility="no"
+ tools:src="@android:drawable/screen_background_dark"/>
+
+ <TextView
+ android:id="@+id/text"
+ style="@style/Mozac.Browser.Menu.Item.ImageText.Label"
+ android:clickable="false"
+ android:focusable="false"
+ android:background="@android:color/transparent"
+ android:gravity="center_vertical"
+ tools:text="Item"
+ android:importantForAccessibility="no"/>
+
+ <View
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1"/>
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/overflowImage"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:clickable="false"
+ android:visibility="gone"
+ android:background="@android:color/transparent"
+ android:importantForAccessibility="no"
+ app:srcCompat="@drawable/mozac_ic_chevron_right_24"/>
+</LinearLayout>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_simple.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_simple.xml
new file mode 100644
index 0000000000..a7afa796fd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_simple.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/simple_text"
+ style="@style/Mozac.Browser.Menu.Item.Text"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height"
+ android:gravity="start|center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:textAlignment="viewStart"
+ tools:text="Item" />
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_switch.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_switch.xml
new file mode 100644
index 0000000000..9d0ee5c56e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_switch.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Mozac.Browser.Menu.Item.Text"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"/>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_toolbar.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_toolbar.xml
new file mode 100644
index 0000000000..00897559de
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_toolbar.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/TextAppearance.Material.Menu"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu_item_toolbar_height"
+ android:gravity="center_vertical"
+ android:orientation="horizontal" />
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_web_extension.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_web_extension.xml
new file mode 100644
index 0000000000..1ccecbf48d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_web_extension.xml
@@ -0,0 +1,57 @@
+<?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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu_item_toolbar_height"
+ android:id="@+id/container"
+ style="@style/Mozac.Browser.Menu.Item.Container"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/action_image"
+ android:layout_width="@dimen/mozac_browser_menu_item_web_extension_icon_width"
+ android:layout_height="@dimen/mozac_browser_menu_item_web_extension_icon_height"
+ android:layout_gravity="center"
+ android:importantForAccessibility="no"
+ app:srcCompat="@drawable/mozac_browser_menu_notification_icon"/>
+
+ <TextView
+ android:id="@+id/action_label"
+ style="@style/Mozac.Browser.Menu.Item.Text"
+ android:background="@android:color/transparent"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height"
+ android:gravity="center_vertical"
+ android:textAlignment="viewStart"
+ android:paddingEnd="16dp"
+ android:paddingStart="16dp"
+ android:clickable="false"
+ android:focusable="false"
+ tools:ignore="RtlCompat"
+ tools:text="uBlock Origin" />
+
+ <View
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1" />
+
+ <TextView
+ android:id="@+id/badge_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="22dp"
+ android:gravity="center"
+ android:textAlignment="center"
+ android:textStyle="bold"
+ android:textColor="@color/photonWhite"
+ android:background="@drawable/rounded_corner"
+ android:visibility="invisible"
+ android:padding="3dp"
+ android:textSize="12sp"
+ tools:text="18" />
+</LinearLayout>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_tooltip_layout.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_tooltip_layout.xml
new file mode 100644
index 0000000000..aa69465b9a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_tooltip_layout.xml
@@ -0,0 +1,27 @@
+<?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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/mozac_browser_tooltip_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/tooltip_margin"
+ android:background="?attr/tooltipFrameBackground"
+ android:ellipsize="end"
+ android:maxWidth="256dp"
+ android:maxLines="2"
+ android:paddingStart="16dp"
+ android:paddingTop="6dp"
+ android:paddingEnd="16dp"
+ android:paddingBottom="6dp"
+ android:textAppearance="@style/TextAppearance.AppCompat.Tooltip"
+ android:textColor="?attr/tooltipForegroundColor"
+ tools:ignore="PrivateResource" />
+</LinearLayout>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..cec57d8d8a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-am/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">ምናሌ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">የተተኮረበት</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎች</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">ቅጥያዎች</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎች አስተዳዳሪ</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">የቅጥያዎች ማስተዳደሪያ</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ወደ ላይ አስስ</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎች፣ ወደ ላይ ዳስስ</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">ቅጥያዎች፣ ወደ ላይ ዳስስ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..20c8245203
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-an/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Destacaus</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Chestor de complementos</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..5b80338dce
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ar/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">القائمة</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">عليها الإبراز</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">الإضافات</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">مدير الإضافات</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">انتقل لأعلى</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..d30d41ab85
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ast/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Rescamplóse</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Xestor de complementos</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..1a5d4fd289
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-az/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Vurğulanmış</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Əlavələr</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Əlavə idarəçisi</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..89ca673be7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-azb/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">منو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">هایلایت اولدو</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">اوزانتی‌لار</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان مودیریتی</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">اوزانتی مودیری</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">یوخاریا گئت</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان‌لار، یوخاری گئت</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">اوزانتی‌ْلار، یوخاریْ گئت</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..6955c0d4a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ban/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Kasorot</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Pengaya</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Manajer Pengaya</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..f3b3bb1755
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-be/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Вылучаны(я)</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Дадаткі</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Пашырэнні</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Менеджар дадаткаў</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Перайсці ўверх</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..99bd63bc40
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-bg/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Откроено</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Добавки</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Разширения</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Управление на добавки</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Управление на разширения</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Придвижване нагоре</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Добавки, навигиране нагоре</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Разширения, навигиране нагоре</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..76d355b1e4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-bn/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">মেনু</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">হাইলাইট করা হয়েছে</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">অ্যাড-অন</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">অ্যাড-অন ব্যবস্থাপক</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..548cbd7785
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-br/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Lañser</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Usskedet</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Askouezhioù</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Askouezhioù</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Ardoer an askouezhioù</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Merañ an askouezhioù</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Adpignat</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..a0c32fdba8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-bs/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Istaknuto</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-oni</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Ekstenzije</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Upravnik add-onima</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Menadžer ekstenzija</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Idi prema gore</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Dodaci, idite gore</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Ekstenzije, idi gore</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..85424c3ba1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ca/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">S’ha ressaltat</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Complements</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Gestor de complements</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navega amunt</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..7ccb1be616
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-cak/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">K\'utsamaj</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Ya\'on ruq\'ij</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Taq tz\'aqat</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Kinuk\'samajel taq Tz\'aqat</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Tib\'an okem ajsik</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description">Taq tz\'aqat, tijote\' chi rokem</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..e02f99d729
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Gipasiugda</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Add-on</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Add-on Manager</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..ee5821c024
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">پێڕست</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ئاماژەپێکراو</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">پێوەکراوەکان</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">بەڕێوەبەری پێوەکراوەکان</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..fb805d3142
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-co/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Listinu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Sopralineatu</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Moduli addiziunali</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Estensioni</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Ghjestiunariu di moduli addiziunali</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Ghjestiunariu d’estensioni</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigà insù</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Moduli addiziunali, ricullà</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Estensioni, ricullà</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..450256d049
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-cs/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Nabídka</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Zvýrazněné</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Doplňky</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Rozšíření</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Správce doplňků</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Správce rozšíření</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Přejít nahoru</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Doplňky, přejít nahoru</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Rozšíření, přejít nahoru</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..7543b08a9e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-cy/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Dewislen</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Amlygwyd</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Ychwanegion</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Estyniadau</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Rheolwr Ychwanegion</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Rheolwr Estyniadau</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Llywio i fyny</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Ychwanegion, symud i fyny</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Estyniadau, llywio i fyny</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..31fd32a217
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-da/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Fremhævet</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tilføjelser</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Udvidelser</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Håndtering af tilføjelser</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Håndtering af udvidelser</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Naviger op</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tilføjelser, naviger op</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Udvidelser, naviger op</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..4d0ec0bccc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-de/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Hervorgehoben</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Erweiterungen</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons-Verwaltung</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Erweiterungs-Manager</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Nach oben navigieren</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, nach oben navigieren</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Erweiterungen, nach oben navigieren</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..868f5ca76e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Wuzwignjony</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Dodanki</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Rozšyrjenja</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Zastojnik dodankow</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Zastojnik rozšyrjenjow</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Górjej</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Dodanki, górjej nawigěrowaś</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Rozšyrjenja, górjej nawigěrowaś</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..37c1c075af
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-el/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Μενού</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Επισημασμένο</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Πρόσθετα</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Επεκτάσεις</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Διαχείριση προσθέτων</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Διαχείριση επεκτάσεων</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Πλοήγηση προς τα πάνω</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Πρόσθετα, πλοήγηση προς τα πάνω</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Επεκτάσεις, πλοήγηση προς τα πάνω</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..9e33b4ffcd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Highlighted</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensions</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons Manager</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Extensions Manager</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigate up</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, navigate up</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensions, navigate up</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..9e33b4ffcd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Highlighted</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensions</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons Manager</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Extensions Manager</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigate up</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, navigate up</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensions, navigate up</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..63b30a2150
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-eo/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menuo</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Elstarigitaj</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Aldonaĵoj</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Administrilo de aldonaĵoj</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Iri supren</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description">Aldonaĵoj, supren</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..b527c2e9bb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Resaltado</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administrador de complementos</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Administrador de complementos</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navegar hacia arriba</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navegar hacia arriba</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Complementos, navegar hacia arriba</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..954a4a0678
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Destacado</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensiones</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administrador de complementos</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Administrador de extensiones</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navegar hacia arriba</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navegar hacia arriba</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensiones, navegar hacia arriba</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..971fcb13c1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Resaltado</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensiones</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administrador de complementos</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Administrador de extensiones</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navegar hacia arriba</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navega hacia arriba</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensiones, navega hacia arriba</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..b699871b37
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Destacado</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Administrador de complementos</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navegar hacia arriba</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..971fcb13c1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Resaltado</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensiones</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administrador de complementos</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Administrador de extensiones</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navegar hacia arriba</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navega hacia arriba</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensiones, navega hacia arriba</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..e21a050061
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-et/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menüü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Esiletõstetud</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Lisad</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Lisade haldur</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Liigu üles</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..53f1a556a3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-eu/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menua</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Nabarmendua</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Gehigarriak</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Gehigarrien kudeatzailea</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Nabigatu gora</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description">Gehigarriak, nabigatu gora</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..af1bd783a6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fa/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">منو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">برجسته‌شده</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">افزونه‌ها</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">مدیریت افزونه‌ها</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ناوش به بالا</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..2ad3a89319
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ff/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Ɓeyditte</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Topitorde Ɓeyditte</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..e78d8971b6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fi/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Valikko</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Korostettu</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosat</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Laajennukset</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosien hallinta</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Laajennusten hallinta</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Liiku ylöspäin</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosat, liiku ylös</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Laajennukset, liiku ylös</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..00c13d5fe1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fr/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Sélectionné</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Modules complémentaires</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensions</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestionnaire de modules</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Gestionnaire d’extensions</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Remonter</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Modules complémentaires, remonter</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensions, remonter</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..211c5d879f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fur/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Evidenziât</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Components adizionâi</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Estensions</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gjestôr comp. adizionâi</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Gjestôr estensions</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navighe in sù</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Components adizionâi, torne indaûr</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Estensions, torne indaûr</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..39e9c4e032
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Markearre</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Utwreidingen</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-onbehearder</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Utwreidingsbehearder</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Omheech</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, omheech navigearje</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Utwreidingen, omheech navigearje</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..89524181f6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-gd/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">An clàr-taice</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Soillsichte</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Tuilleadain</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Manaidsear nan tuilleadan</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..20db3e0dc0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-gl/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Realzado</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensións</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Xestor de complementos</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Xestor de extensións</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navegar cara arriba</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navegar cara arriba</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensións, navega cara arriba</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..25fc596f2d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-gn/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Poravorã</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Hechaukaveha</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Moĩmbaha</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Jepysokue</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Moĩmbaha ñangarekohára</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Jepysokue ñangarekoha</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Eikundaha yvate gotyo</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Moĩmbaha, eikundaha yvate gotyo</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Jepysokue, eikundaha yvate gotyo</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..fde91325c9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">મેનુ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">પ્રકાશિત કરેલ</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">ઍડ-ઑન્સ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">એડ-ઑન્સ સંચાલક</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..c5eab9cbd7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">मेन्यू</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">दर्शाए गए</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">ऐड-ऑन</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">ऐड-ऑन प्रबंधक</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..c6b11e8350
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hil/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Highlighted</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Mga Add-on</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Mga add-on sang Manager</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..ee6f3922ce
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hr/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Izbornik</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Istaknuto</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Dodaci</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Upravljač dodataka</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigiraj gore</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..efb7ef3879
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Wuzběhnjeny</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Přidatki</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Rozšěrjenja</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Zrjadowak přidatkow</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Zrjadowak rozšěrjenjow</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Horje</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Přidatki, horje nawigěrować</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Rozšěrjenja, horje nawigěrować</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..5f0c7c32af
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hu/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Kiemelt</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítők</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Kiegészítők</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítőkezelő</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Kiegészítőkezelő</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigálás fel</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítők, navigáció felfelé</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Kiegészítők, navigáció felfelé</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..1eb1bf7ab9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Ցանկ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Գունանշված</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումներ</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Ընդլայնումներ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումների կառավարիչ</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Ընդլայնումների կառավարիչ</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Նավարկել վերև</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումներ, նավարկեք վերև</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Ընդլայնումներ, նավարկեք վերև</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..97991d144c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ia/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Evidentiate</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Additivos</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensiones</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestor de additivos</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Gestor de extensiones</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigar</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Additivos, navigar retro</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensiones, remontar</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..f23b41f900
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-in/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Tersorot</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Pengaya</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Ekstensi</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Pengelola Pengaya</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Pengelola Ekstensi</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Arahkan ke atas</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Pengaya, navigasikan ke atas</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Ekstensi, navigasikan ke atas</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..3b1d78f3b5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-is/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Valmynd</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Undirstrikað</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Viðbætur</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Forritsaukar</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Viðbótastjóri</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Umsýsla forritsauka</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Flakka upp</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Viðbætur, fara upp</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Forritsaukar, fara upp</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..f7e7f9720c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-it/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Evidenziato</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Componenti aggiuntivi</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Estensioni</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestore comp. aggiuntivi</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Gestione estensioni</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Passa a livello superiore</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Componenti aggiuntivi, torna indietro</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Estensioni, torna indietro</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..e164d78059
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-iw/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">תפריט</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">מודגש</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">תוספות</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">הרחבות</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">מנהל התוספות</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">מנהל ההרחבות</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ניווט למעלה</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">תוספות, ניווט למעלה</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">הרחבות, ניווט למעלה</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..a5f5383eae
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ja/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">メニュー</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">強調</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">アドオン</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">拡張機能</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">アドオンマネージャー</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">拡張機能マネージャー</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">上へ移動します</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">アドオン、上へ移動します</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">拡張機能、上へ移動します</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..5971c5f9fd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ka/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">მენიუ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">მონიშნული</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">დამატებები</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">დამატებების მმართველი</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ზემოთ გადასვლა</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..4a3bd0164c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Belgilengen</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Qosımshalar</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Qosımshalar menedjeri</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..f36a1014b1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kab/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Umuɣ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ittwag deg uqerru</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Izegrar</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Isiɣzaf</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Amsefrak n izegrar</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Inig d asawen</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..6a25654110
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kk/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Мәзір</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Ерекшеленген</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшалар</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Кеңейтулер</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшалар басқарушысы</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Кеңейтулер басқарушысы</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Жоғары жылжу</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшалар, жоғары өту</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Кеңейтулер, жоғары өту</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..c977c1edc4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menû</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Berbiçavkirî</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Pêvek</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Rêvebera pêvekan</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Here jorê</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description">Pêvek, bi jorê ve here</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..ca1bfb47be
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kn/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">ಮೆನು</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ಹೈಲೈಟ್ ಮಾಡಲಾಗಿದೆ</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">ಆಡ್-ಆನ್‌ಗಳು</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">ಆಡ್‌-ಆನ್‌ಗಳ ವ್ಯವಸ್ಥಾಪಕ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..f661f30b1e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ko/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">메뉴</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">강조 표시됨</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">확장 기능</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능 관리자</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">확장 기능 관리자</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">위로 이동</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능, 위로 이동</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">확장 기능, 위로 이동</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kw/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kw/strings.xml
new file mode 100644
index 0000000000..2dc45ab574
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kw/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Rol</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Golowboyntys</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Keworansow</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Restrer Keworansow</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Brennya war-vann</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ldrtl/dimens.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ldrtl/dimens.xml
new file mode 100644
index 0000000000..10219fc900
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ldrtl/dimens.xml
@@ -0,0 +1,7 @@
+<?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>
+ <dimen name="mozac_browser_menu_highlightable_notification_translate_x">-4dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..c07f67427c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-lij/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">In evidensa</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Conponenti azonti</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..82a2c4998a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-lo/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">ເມນູ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ຈຸດເດັ່ນ</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">ຕົວຈັດການ Add-ons</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ນຳທາງຂຶ້ນໄປທາງເທິງ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..4fbc9bf311
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-lt/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meniu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Paryškinta</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Priedai</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Priedų tvarkytuvė</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..228f3e66cf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-mix/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Katsi</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Tu^un nchichi</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Komplementos</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Ku\'ntyeé kutyi</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description">Complemento, ku\'ntyeé kutyi</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..69f21eed4c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-mr/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">मेनू</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ठळक</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">ॲड-ऑन</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">अ‍ॅड-ऑन व्यवस्थापक</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..f01b0ddac4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-my/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">မီနူး</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">အသားပေးအရာ</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">အတ်အွန်များ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">အတ်အွန် စီမံရေး</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..d6dbb1debb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Uthevet</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Utvidelser</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Tilleggsbehandler</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Behandling av utvidelser</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Naviger opp</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg, naviger opp</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Utvidelser, naviger opp</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..d7ca7d7694
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">मेनु</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">हाइलाइट गरियो</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">एड-अनहरू</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">एड-अन म्यानेजर</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..b11eaaa5e2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-nl/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Gemarkeerd</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensies</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-onbeheerder</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Extensiebeheerder</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Omhoog</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, omhoog navigeren</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensies, omhoog navigeren</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..2bdfe10f1c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Utheva</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Utvidingar</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Tilleggshandsamar</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Utvidingshandsamar</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Naviger opp</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg, naviger opp</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Utvidingar, naviger opp</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..ad88a95a37
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-oc/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Notables</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Moduls complementaris</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensions</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestionari de moduls complementaris</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Gestionari d’extensions</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Remontar</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Moduls complementaris, montar</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extension, montar</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..94b86a71fe
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-or/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">ମେନୁ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ହାଇଲାଇଟ୍ କରାଯାଇଥିବା</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">ଆଡ଼-ଅନସମୂହ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">ଆଡ-ଅନ ପରିଚାଳକ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..342ab91a44
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">ਮੀਨੂ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ਉਘਾੜੇ</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">ਇਕਸਟੈਨਸ਼ਨਾਂ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ ਮੈਨੇਜਰ</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">ਇਕਸਟੈਨਸ਼ਨ ਮੈਨੇਜਰ</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ਉੱਤੇ ਜਾਓ</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ, ਉੱਤੇ</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">ਇਕਸਟੈਨਸ਼ਨਾਂ, ਉੱਤੇ ਵੱਲ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..295865d138
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">مینو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">اُگھاڑے</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">وادھے والے</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">وادھیاں والیاں دیاں سیٹنگاں</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">اُتے جاؤ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..1929685c94
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pl/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Wyróżnione</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Rozszerzenia</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Zarządzaj dodatkami</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Zarządzaj rozszerzeniami</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Przejdź w górę</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki, przejdź w górę</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Rozszerzenia, przejdź w górę</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..44cbedefab
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Destacado</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Extensões</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensões</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gerenciador de extensões</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Gerenciador de extensões</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Ir para o topo</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Extensões, voltar</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensões, voltar</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..75b9647a29
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Destacado</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Extras</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensões</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestor de extras</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Gestor de Extensões</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navegar para cima</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Extras, navegar para cima</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensões, navegar para cima</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..8c8152c946
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-rm/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Cun emfasa</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Supplements</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensiuns</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administraziun da supplements</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Administraziun dad extensiuns</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigar ensi</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Supplements, returnar ensi</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensiuns, turnar ensi</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..ab9f724ab0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ro/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meniu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Evidențiat</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Suplimente</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Manager de suplimente</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navighează în sus</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..3a7c4eceab
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ru/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Выделено</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Дополнения</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Расширения</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Менеджер дополнений</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Менеджер расширений</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Перейти наверх</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Дополнения, перейти вверх</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Расширения, перейти вверх</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..4650592f55
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sat/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">ᱢᱤᱱᱭᱩ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ᱩᱪᱷᱟᱹᱱᱟᱜ</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰᱼᱚᱱᱥ</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">ᱮᱠᱥᱴᱮᱱᱥᱚᱱᱠᱚ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰᱼᱚᱱᱥ ᱵᱮᱵᱚᱥᱛᱟ</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱢᱮᱱᱮᱡᱚᱨ</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ᱪᱮᱛᱟᱱ ᱥᱮᱱ ᱱᱮᱣᱤᱜᱮᱴ ᱢᱮ</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰ-ᱚᱱᱥ, ᱪᱮᱛᱟᱱ ᱥᱮᱫ</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">ᱮᱠᱥᱴᱮᱱᱥᱚᱱ, ᱪᱮᱛᱟᱱ ᱛᱮ ᱥᱮᱱᱚᱜ ᱢᱮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..4fd82314b3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sc/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">In evidèntzia</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Cumplementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Gestore de cumplementos</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Nàviga in artu</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..08f69d548f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-si/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">වට්ටෝරුව</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ත්‍රීවාලෝකිත</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">දිගු</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු කළමනාකරු</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">දිගු කළමනාකරු</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ඉහළට යාත්‍රණය</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..3a119e7b06
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sk/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Ponuka</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Zvýraznené</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Doplnky</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Rozšírenia</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Správca doplnkov</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Správca rozšírení</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Prejsť nahor</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Doplnky, prejsť nahor</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Rozšírenia, prejsť nahor</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..cd91a8395b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-skr/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">مینیو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">نمایاں کیتا ڳیا</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ ــ آن</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">ایکسٹینشنز</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ ــ آن منیجر</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">ایکسٹنشنز منیجر</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">اُتے ون٘ڄو</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ آن, اُتے نیویگیٹ کرو</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">ایکسٹنشناں, اُتے نیویگیٹ کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..f08c3d07ce
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sl/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Označeno</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Razširitve</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Upravitelj dodatkov</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Upravitelj razširitev</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Pojdi gor</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki, pomakni se gor</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Razširitve, pomakni se gor</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..54f6a4112c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sq/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">E theksuar</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Shtesa</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Zgjerime</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Përgjegjës Shtesash</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Përgjegjës Zgjerimesh</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Lëvizni për sipër</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Shtesa, shkoni sipër</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Zgjerime, shkoni sipër</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..5853163b79
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sr/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Мени</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Истакнуто</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Додаци</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Управник додатака</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Иди нагоре</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..1a786bee7f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-su/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Disorot</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Émboh</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Éksténsi</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Manajer Émboh</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Pangatur éksténsi</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Pindah ka luhur</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ins, tuduhkeun ka luhur</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Éksténsi, tuduhkeun ka luhur</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..002fa3390f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Markerad</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tillägg</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Tillägg</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Tilläggshanterare</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Tilläggshanterare</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigera uppåt</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tillägg, navigera uppåt</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Tillägg, navigera upp</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..497be7b9bf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-szl/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Myni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Ôbznoczōne</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Rozszyrzynia</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Regiyrowanie rozszyrzyniami</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Zōńdź na wiyrch</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..a00d99cf17
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ta/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">பட்டி</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">மிளிர்ப்புகள்</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">துணை நிரல்கள்</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">துணை நிரல் நிர்வாகி</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..c951083b0a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-te/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">మెనూ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">హైలైట్ చేసినవి</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">పొడగింతలు</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">పొడగింతల నిర్వాహకి</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..e3c7eecde1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tg/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Таъкид</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Ҷузъи иловагӣ</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Васеъшавиҳо</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Мудири ҷузъи иловагӣ</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Мудири васеъшавиҳо</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Ба боло гузаред</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Ҷузъҳои иловагӣ, гузариш ба боло</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Васеъшавиҳо, гузариш ба боло</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..4ab63a3cdd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-th/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">เมนู</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">เน้น</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ส่วนเสริม</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">ส่วนขยาย</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ตัวจัดการส่วนเสริม</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">ตัวจัดการส่วนขยาย</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">นำทางขึ้นไปด้านบน</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ส่วนเสริม, นำทางไปด้านบน</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">ส่วนขยาย, นำทางไปด้านบน</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..45b5f15738
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tl/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Mga naka-highlight</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Mga Add-on</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Add-on Manager</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..f946769c8c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tok/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">ilo pi kepeken sin</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">lawa pi kepeken sin</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..7e4c87c3f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tr/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Vurgulu</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Eklentiler</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Uzantılar</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Eklenti yöneticisi</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Uzantı yöneticisi</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Yukarı git</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Eklentiler, yukarı git</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Uzantılar, yukarı git</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..25b10e1df4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-trs/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menû</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Sa ña\'āan</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Sa gā\'ue nūtò\'</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Sa nīkāj ñu\'ūnj nej sa gā\'ue nūtò\'</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Gāchē nun gan’ānj nāhuīt</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..68f7abc358
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tt/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Аерылган</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Кушымчалар</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Кушымчалар менеджеры</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..0c2827eb8f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Umuɣ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..df31e6911e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ug/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">تىزىملىك</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">ئالاھىدە</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">قىستۇرما</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">كېڭەيتمە</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">قىستۇرما باشقۇرغۇچ</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">كېڭەيتمە باشقۇرۇش</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">ئۈستىگە يول باشلا</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">قوشۇلما، ئۈستىگە يول باشلا</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">كېڭەيتمە، ئۈستىگە يول باشلا</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..3788a0d5c8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-uk/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Виділено</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Додатки</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Розширення</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Керувати додатками</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Менеджер розширень</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Вгору</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Додатки, перейти вгору</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Розширення, перейти вгору</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..c6070a6ec2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ur/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">مینیو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">نمایاں کیا گیا</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">ایڈ اون</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">ایڈ اون مینیجر</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..940434e75a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-uz/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Belgilangan</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Qoʻshimcha dasturlar</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Qoʻshimcha dasturlar boshqaruvchisi</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..c999198e19
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-vec/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Evidensià</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Conponenti che se pole xontare</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Gestion Estension</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..8100e82a1c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-vi/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Đã tô sáng</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tiện ích</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Tiện ích</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Quản lí tiện ích</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Quản lý tiện ích</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Điều hướng lên</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tiện ích, điều hướng lên</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Tiện ích, điều hướng lên</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..6cacf9d220
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-yo/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Mẹ́nù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Afàmìsí</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons">Àfikún</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager">Àsàmójútó Àfikún</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..bd7fd2ad36
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">菜单</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">高亮</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">附加组件</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">扩展</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">附加组件管理器</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">扩展管理器</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">向上导航</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">附加组件,向上导航</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">扩展,向上导航</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..e566379f79
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">選單</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">強調</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">附加元件</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">擴充套件</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">附加元件管理員</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">擴充套件管理員</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">向上導航</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">附加元件,向上導航</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">擴充套件,向上導航</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values/colors.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..8b76cc158d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Empty by default, allows others to theme as they see fit -->
+ <color name="mozac_browser_menu_background"></color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values/dimens.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..c815522515
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values/dimens.xml
@@ -0,0 +1,80 @@
+<?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>
+ <dimen name="mozac_browser_menu_corner_radius">4dp</dimen>
+ <dimen name="mozac_browser_menu_elevation">8dp</dimen>
+ <dimen name="mozac_browser_menu_width">250dp</dimen>
+ <dimen name="mozac_browser_menu_width_min">250dp</dimen>
+ <dimen name="mozac_browser_menu_width_max">250dp</dimen>
+ <dimen name="mozac_browser_menu_padding_vertical">0dp</dimen>
+
+ <!--Menu Item -->
+ <dimen name="mozac_browser_menu_item_text_size">16sp</dimen>
+ <dimen name="mozac_browser_menu_item_container_layout_height">48dp</dimen>
+ <dimen name="mozac_browser_menu_item_container_padding_start">16dp</dimen>
+ <dimen name="mozac_browser_menu_item_container_padding_end">16dp</dimen>
+ <!--Menu Item -->
+
+ <!--Checkbox Menu Item -->
+ <dimen name="mozac_browser_menu_item_checkbox_container_layout_height">48dp</dimen>
+ <dimen name="mozac_browser_menu_item_checkbox_container_padding_start">16dp</dimen>
+ <dimen name="mozac_browser_menu_item_checkbox_container_padding_end">16dp</dimen>
+ <!--Checkbox Menu Item -->
+
+ <!--DynamicWidthRecyclerView -->
+ <dimen name="mozac_browser_menu_material_min_tap_area">48dp</dimen>
+ <dimen name="mozac_browser_menu_material_min_item_width">112dp</dimen>
+ <!--DynamicWidthRecyclerView -->
+
+ <!--BrowserMenuDivider -->
+ <dimen name="mozac_browser_menu_item_divider_height">1dp</dimen>
+ <!--BrowserMenuDivider -->
+
+ <!--BrowserMenuHighlightableItem -->
+ <dimen name="mozac_browser_menu_highlightable_notification_translate_x">4dp</dimen>
+ <dimen name="mozac_browser_menu_highlightable_notification_translate_y">-4dp</dimen>
+ <dimen name="mozac_browser_menu_highlightable_notification_dot_size">8dp</dimen>
+ <!--BrowserMenuHighlightableItem -->
+
+ <!-- BrowserMenuCategory -->
+ <dimen name="mozac_browser_menu_category_text_size">14sp</dimen>
+ <dimen name="mozac_browser_menu_category_layout_height">40dp</dimen>
+ <dimen name="mozac_browser_menu_category_padding_start">16dp</dimen>
+ <dimen name="mozac_browser_menu_category_padding_end">16dp</dimen>
+ <!-- BrowserMenuCategory -->
+
+ <!--BrowserMenuCheckbox -->
+ <dimen name="mozac_browser_menu_checkbox_padding">12dp</dimen>
+ <!--BrowserMenuCheckbox -->
+
+ <!--WebExtensionBrowserMenuItem -->
+ <dimen name="mozac_browser_menu_item_web_extension_icon_width">24dp</dimen>
+ <dimen name="mozac_browser_menu_item_web_extension_icon_height">24dp</dimen>
+ <!--WebExtensionBrowserMenuItem -->
+
+ <!--BrowserMenuImageText-->
+
+ <!--Icon-->
+ <dimen name="mozac_browser_menu_item_image_text_icon_width">24dp</dimen>
+ <dimen name="mozac_browser_menu_item_image_text_icon_height">24dp</dimen>
+ <dimen name="mozac_browser_menu_item_image_text_icon_padding">20dp</dimen>
+ <!--Icon-->
+
+ <!--Label-->
+ <dimen name="mozac_browser_menu_item_image_text_label_padding_start">20dp</dimen>
+ <dimen name="mozac_browser_menu_item_checkbox_text_label_padding_start">20dp</dimen>
+ <dimen name="mozac_browser_menu_item_checkbox_text_label_padding_end">20dp</dimen>
+ <!--Label-->
+
+ <!--Checkbox-->
+ <dimen name="mozac_browser_menu_item_image_checkbox_padding_start">12dp</dimen>
+ <dimen name="mozac_browser_menu_item_image_checkbox_padding_end">12dp</dimen>
+ <!--Checkbox-->
+
+ <!--BrowserMenuImageText-->
+
+ <!-- BrowserMenuItemToolbar -->
+ <dimen name="mozac_browser_menu_item_toolbar_height">56dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..dae4c81308
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?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 xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu_highlighted">Highlighted</string>
+ <!-- Label for add-ons submenu section -->
+ <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string>
+ <!-- Label for extensions submenu section -->
+ <string name="mozac_browser_menu_extensions">Extensions</string>
+ <!-- Label for add-ons sub menu item for add-ons manager -->
+ <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons Manager</string>
+ <!-- Label for extensions sub menu item for extensions manager -->
+ <string name="mozac_browser_menu_extensions_manager">Extensions Manager</string>
+ <!-- Content description for the action bar "up" button -->
+ <string name="action_bar_up_description">Navigate up</string>
+ <!-- Content description for the action bar "up" button of the add-ons sub menu item -->
+ <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, navigate up</string>
+ <!-- Content description for the action bar "up" button of the extensions sub menu item -->
+ <string name="mozac_browser_menu_extensions_content_description">Extensions, navigate up</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values/style.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values/style.xml
new file mode 100644
index 0000000000..078d14e7fa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/main/res/values/style.xml
@@ -0,0 +1,106 @@
+<?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>
+
+ <style name="Mozac.Browser.Menu" parent="">
+ <item name="cardBackgroundColor">@color/mozac_browser_menu_background</item>
+ </style>
+
+ <!-- Item Divider -->
+ <style name="Mozac.Browser.Menu.Item.Divider" parent="">
+ <item name="android:background">?android:attr/listDivider</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.Divider.Horizontal">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/mozac_browser_menu_item_divider_height</item>
+ </style>
+ <!-- Item Divider -->
+
+ <style name="Mozac.Browser.Menu.Item.Container" parent="">
+ <item name="android:layout_height">@dimen/mozac_browser_menu_item_container_layout_height</item>
+ <item name="android:paddingStart">@dimen/mozac_browser_menu_item_container_padding_start</item>
+ <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_container_padding_end</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.CandidateContainer" parent="Mozac.Browser.Menu.Item.Container">
+ <item name="android:paddingStart">16dp</item>
+ <item name="android:paddingEnd">16dp</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.Text" parent="@android:style/TextAppearance.Material.Menu">
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:textSize">@dimen/mozac_browser_menu_item_text_size</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:lines">1</item>
+ <item name="android:focusable">true</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <!-- BrowserMenuCategory -->
+ <style name="Mozac.Browser.Menu.Item.Category" parent="">
+ <item name="android:layout_height">@dimen/mozac_browser_menu_category_layout_height</item>
+ <item name="android:textSize">@dimen/mozac_browser_menu_category_text_size</item>
+ <item name="android:paddingStart">@dimen/mozac_browser_menu_category_padding_start</item>
+ <item name="android:paddingEnd">@dimen/mozac_browser_menu_category_padding_end</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ </style>
+ <!-- BrowserMenuCategory -->
+
+ <!-- BrowserMenuImageText -->
+ <style name="Mozac.Browser.Menu.Item.ImageText.Icon" parent="">
+ <item name="android:layout_width">@dimen/mozac_browser_menu_item_image_text_icon_width</item>
+ <item name="android:layout_height">@dimen/mozac_browser_menu_item_image_text_icon_height</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.CandidateIcon" parent="Mozac.Browser.Menu.Item.ImageText.Icon">
+ <item name="android:layout_width">24dp</item>
+ <item name="android:layout_height">24dp</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.ImageText.Label" parent="Mozac.Browser.Menu.Item.Text">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:paddingStart">@dimen/mozac_browser_menu_item_image_text_label_padding_start</item>
+ <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_image_text_label_padding_start</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.Checkbox.Label" parent="Mozac.Browser.Menu.Item.ImageText.Label">
+ <item name="android:paddingStart">@dimen/mozac_browser_menu_item_checkbox_text_label_padding_start</item>
+ <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_checkbox_text_label_padding_end</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.Checkbox.Text" parent="Mozac.Browser.Menu.Item.Text">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">0dp</item>
+ <item name="android:paddingStart">@dimen/mozac_browser_menu_item_image_checkbox_padding_start</item>
+ <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_image_checkbox_padding_end</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.Checkbox.Container" parent="Mozac.Browser.Menu.Item.Container">
+ <item name="android:layout_height">@dimen/mozac_browser_menu_item_checkbox_container_layout_height</item>
+ <item name="android:paddingStart">@dimen/mozac_browser_menu_item_checkbox_container_padding_start</item>
+ <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_checkbox_container_padding_end</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Item.CandidateLabel" parent="Mozac.Browser.Menu.Item.Text">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+ <!-- BrowserMenuImageText -->
+
+ <!-- Animation -->
+ <style name="Mozac.Browser.Menu.Animation.OverflowMenuTop" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_top</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu.Animation.OverflowMenuBottom" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_bottom</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+ <!-- Animation -->
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuAdapterTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuAdapterTest.kt
new file mode 100644
index 0000000000..64d7c41260
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuAdapterTest.kt
@@ -0,0 +1,210 @@
+/* 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.browser.menu
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuAdapterTest {
+
+ @Test
+ fun `items that return false from the visible lambda will be filtered out`() {
+ val items = listOf(
+ createMenuItem(1, { true }),
+ createMenuItem(2, { true }),
+ createMenuItem(3, { false }),
+ createMenuItem(4, { false }),
+ createMenuItem(5, { true }),
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ assertEquals(3, adapter.visibleItems.size)
+
+ adapter.visibleItems.assertTrueForOne { it.getLayoutResource() == 1 }
+ adapter.visibleItems.assertTrueForOne { it.getLayoutResource() == 2 }
+ adapter.visibleItems.assertTrueForOne { it.getLayoutResource() == 5 }
+
+ adapter.visibleItems.assertTrueForAll { it.visible() }
+
+ assertEquals(3, adapter.itemCount)
+ }
+
+ @Test
+ fun `layout resource ID is used as view type`() {
+ val items = listOf(
+ createMenuItem(23),
+ createMenuItem(42),
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ assertEquals(2, adapter.itemCount)
+
+ assertEquals(23, adapter.getItemViewType(0))
+ assertEquals(42, adapter.getItemViewType(1))
+ }
+
+ @Test
+ fun `bind will be forwarded to item implementation`() {
+ val item1 = spy(createMenuItem())
+ val item2 = spy(createMenuItem())
+
+ val menu = mock(BrowserMenu::class.java)
+
+ val adapter = BrowserMenuAdapter(testContext, listOf(item1, item2))
+ adapter.menu = menu
+
+ val view = mock(View::class.java)
+ val holder = BrowserMenuItemViewHolder(view)
+
+ adapter.onBindViewHolder(holder, 0)
+
+ verify(item1).bind(menu, view)
+ verify(item2, never()).bind(menu, view)
+
+ reset(item1)
+ reset(item2)
+
+ adapter.onBindViewHolder(holder, 1)
+
+ verify(item1, never()).bind(menu, view)
+ verify(item2).bind(menu, view)
+ }
+
+ @Test
+ fun `invalidate will be forwarded to item implementation`() {
+ val item1 = spy(createMenuItem())
+ val item2 = spy(createMenuItem())
+
+ val menu = mock(BrowserMenu::class.java)
+
+ val adapter = BrowserMenuAdapter(testContext, listOf(item1, item2))
+ adapter.menu = menu
+ val recyclerView = mock(RecyclerView::class.java)
+
+ val view = mock(View::class.java)
+ val holder = BrowserMenuItemViewHolder(view)
+ `when`(recyclerView.findViewHolderForAdapterPosition(0)).thenReturn(holder)
+ `when`(recyclerView.findViewHolderForAdapterPosition(1)).thenReturn(null)
+
+ adapter.invalidate(recyclerView)
+
+ verify(item1).invalidate(view)
+ verify(item2, never()).invalidate(view)
+ }
+
+ @Test
+ fun `total interactive item count is given provided adapter`() {
+ val items = listOf(
+ createMenuItem(1, { true }, { 1 }),
+ createMenuItem(2, { true }, { 0 }),
+ createMenuItem(3, { false }, { 10 }),
+ createMenuItem(4, { true }, { 5 }),
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ assertEquals(6, adapter.interactiveCount)
+ }
+
+ @Test
+ fun `GIVEN a stickyItem exists in the visible items WHEN isStickyItem is called THEN it returns true`() {
+ val items = listOf(
+ createMenuItem(1, { true }, { 1 }),
+ createMenuItem(3, { true }, { 10 }, true),
+ createMenuItem(4, { true }, { 5 }),
+ createMenuItem(3, { false }, { 3 }, true),
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ assertFalse(adapter.isStickyItem(0))
+ assertTrue(adapter.isStickyItem(1))
+ assertFalse(adapter.isStickyItem(2))
+ assertFalse(adapter.isStickyItem(3))
+ assertFalse(adapter.isStickyItem(4))
+ }
+
+ @Test
+ fun `GIVEN a BrowserMenu exists WHEN setupStickyItem is called THEN the item background color is set for the View parameter`() {
+ val adapter = BrowserMenuAdapter(testContext, emptyList())
+ val menu: BrowserMenu = mock()
+ menu.backgroundColor = Color.CYAN
+ adapter.menu = menu
+ val view = View(testContext)
+
+ adapter.setupStickyItem(view)
+
+ assertEquals(menu.backgroundColor, (view.background as ColorDrawable).color)
+ }
+
+ @Test
+ fun `GIVEN BrowserMenuAdapter WHEN tearDownStickyItem is called THEN the item background is reset to transparent`() {
+ val adapter = BrowserMenuAdapter(testContext, emptyList())
+ val view = View(testContext)
+ view.setBackgroundColor(Color.CYAN)
+
+ adapter.tearDownStickyItem(view)
+
+ assertEquals(Color.TRANSPARENT, (view.background as ColorDrawable).color)
+ }
+
+ private fun List<BrowserMenuItem>.assertTrueForOne(predicate: (BrowserMenuItem) -> Boolean) {
+ for (item in this) {
+ if (predicate(item)) {
+ return
+ }
+ }
+ fail("Predicate false for all items")
+ }
+
+ private fun List<BrowserMenuItem>.assertTrueForAll(predicate: (BrowserMenuItem) -> Boolean) {
+ for (item in this) {
+ if (!predicate(item)) {
+ fail("Predicate not true for all items")
+ }
+ }
+ }
+
+ private fun createMenuItem(
+ layout: Int = 0,
+ visible: () -> Boolean = { true },
+ interactiveCount: () -> Int = { 1 },
+ isSticky: Boolean = false,
+ ): BrowserMenuItem {
+ return object : BrowserMenuItem {
+ override val visible = visible
+
+ override val interactiveCount = interactiveCount
+
+ override fun getLayoutResource() = layout
+
+ override fun bind(menu: BrowserMenu, view: View) {}
+
+ override fun invalidate(view: View) {}
+
+ override val isSticky = isSticky
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuBuilderTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuBuilderTest.kt
new file mode 100644
index 0000000000..6f39e7cd64
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuBuilderTest.kt
@@ -0,0 +1,44 @@
+/* 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.browser.menu
+
+import android.view.View
+import android.widget.ImageButton
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuBuilderTest {
+
+ @Test
+ fun `items are forwarded from builder to menu`() {
+ val builder = BrowserMenuBuilder(listOf(mockMenuItem(), mockMenuItem()))
+
+ val menu = builder.build(testContext)
+
+ val anchor = ImageButton(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter!!
+ assertNotNull(recyclerAdapter)
+ assertEquals(2, recyclerAdapter.itemCount)
+ }
+
+ private fun mockMenuItem() = object : BrowserMenuItem {
+ override val visible: () -> Boolean = { true }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple
+
+ override fun bind(menu: BrowserMenu, view: View) {}
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuHighlightTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuHighlightTest.kt
new file mode 100644
index 0000000000..aa4f6d33cd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuHighlightTest.kt
@@ -0,0 +1,49 @@
+/* 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.browser.menu
+
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import mozilla.components.ui.colors.R as colorsR
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuHighlightTest {
+
+ @Test
+ fun `low priority effect keeps notification tint`() {
+ val highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = Color.RED,
+ )
+ assertEquals(LowPriorityHighlightEffect(Color.RED), highlight.asEffect(mock()))
+ }
+
+ @Test
+ fun `high priority effect keeps background tint`() {
+ val highlight = BrowserMenuHighlight.HighPriority(
+ backgroundTint = Color.RED,
+ )
+ assertEquals(HighPriorityHighlightEffect(Color.RED), highlight.asEffect(mock()))
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `classic highlight effect converts background tint`() {
+ val colorId = colorsR.color.photonRed50
+ val highlight = BrowserMenuHighlight.ClassicHighlight(
+ startImageResource = 0,
+ endImageResource = 0,
+ backgroundResource = 0,
+ colorResource = colorId,
+ )
+ assertEquals(HighPriorityHighlightEffect(testContext.getColor(colorId)), highlight.asEffect(testContext))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuPositioningTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuPositioningTest.kt
new file mode 100644
index 0000000000..898733464a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuPositioningTest.kt
@@ -0,0 +1,163 @@
+/* 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.browser.menu
+
+import android.view.View
+import android.view.ViewGroup
+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 org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.spy
+import org.robolectric.Shadows
+import org.robolectric.shadows.ShadowDisplay
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuPositioningTest {
+
+ @Test
+ fun `GIVEN inferMenuPositioningData WHEN called with the menu layout, anchor and current menu data THEN it returns a new MenuPositioningData populated with all data needed to show a PopupWindow`() {
+ val view: ViewGroup = mock()
+ Mockito.doReturn(70).`when`(view).measuredHeight
+ val anchor = View(testContext)
+ anchor.layoutParams = ViewGroup.LayoutParams(20, 40)
+ setScreenHeight(100)
+
+ val result = inferMenuPositioningData(view, anchor, MenuPositioningData())
+
+ val expected = MenuPositioningData(
+ BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor), // orientation DOWN and fitsDown
+ askedOrientation = BrowserMenu.Orientation.DOWN, // default
+ fitsUp = false, // availableHeightToTop(0) is smaller than containerHeight(70)
+ fitsDown = true, // availableHeightToBottom(470) is bigger than containerHeight(70)
+ availableHeightToTop = 0,
+ availableHeightToBottom = 100, // mocked by us above
+ containerViewHeight = 70, // mocked by us above
+ )
+ Assert.assertEquals(expected, result)
+ }
+
+ @Test
+ fun `GIVEN inferMenuPositioningData WHEN availableHeightToBottom is bigger than availableHeightToTop THEN it returns a new MenuPositioningData populated with all data needed to show a PopupWindow that fits down`() {
+ val view: ViewGroup = mock()
+ Mockito.doReturn(70).`when`(view).measuredHeight
+ val anchor = View(testContext)
+ anchor.layoutParams = ViewGroup.LayoutParams(20, 40)
+
+ setScreenHeight(50)
+
+ val result = inferMenuPositioningData(view, anchor, MenuPositioningData())
+
+ val expected = MenuPositioningData(
+ BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor), // orientation DOWN and fitsDown
+ askedOrientation = BrowserMenu.Orientation.DOWN, // default
+ fitsUp = false, // availableHeightToTop(0) is smaller than containerHeight(70) and smaller than availableHeightToBottom(50)
+ fitsDown = true, // availableHeightToBottom(50) is smaller than containerHeight(70) and bigger than availableHeightToTop(0)
+ availableHeightToTop = 0,
+ availableHeightToBottom = 50, // mocked by us above
+ containerViewHeight = 70, // mocked by us above
+ )
+ Assert.assertEquals(expected, result)
+ }
+
+ @Test
+ fun `GIVEN inferMenuPositioningData WHEN availableHeightToTop is bigger than availableHeightToBottom THEN it returns a new MenuPositioningData populated with all data needed to show a PopupWindow that fits up`() {
+ val view: ViewGroup = mock()
+ Mockito.doReturn(70).`when`(view).measuredHeight
+ val anchor = spy(View(testContext))
+ anchor.layoutParams = ViewGroup.LayoutParams(20, 40)
+
+ whenever(anchor.getLocationOnScreen(IntArray(2))).thenAnswer { invocation ->
+ val args = invocation.arguments
+ val location = args[0] as IntArray
+ location[0] = 0
+ location[1] = 60
+ location
+ }
+
+ setScreenHeight(100)
+
+ val result = inferMenuPositioningData(view, anchor, MenuPositioningData())
+
+ val expected = MenuPositioningData(
+ BrowserMenuPlacement.AnchoredToBottom.Dropdown(anchor), // orientation UP and fitsUp
+ askedOrientation = BrowserMenu.Orientation.DOWN, // default
+ fitsUp = true, // availableHeightToTop(60) is smaller than containerHeight(70) and bigger than availableHeightToBottom(40)
+ fitsDown = false, // availableHeightToBottom(40) is smaller than containerHeight(70) and smaller than availableHeightToTop(60)
+ availableHeightToTop = 60, // mocked by us above
+ availableHeightToBottom = 40,
+ containerViewHeight = 70, // mocked by us above
+ )
+
+ Assert.assertEquals(expected, result)
+ }
+
+ @Test
+ fun `GIVEN inferMenuPosition WHEN called with an anchor and the current menu data THEN it returns a new MenuPositioningData with data about positioning the menu`() {
+ val view: View = mock()
+
+ var data = MenuPositioningData(askedOrientation = BrowserMenu.Orientation.DOWN, fitsDown = true)
+ var result = inferMenuPosition(view, data)
+ Assert.assertEquals(
+ BrowserMenuPlacement.AnchoredToTop.Dropdown(view),
+ result.inferredMenuPlacement,
+ )
+
+ data = MenuPositioningData(askedOrientation = BrowserMenu.Orientation.UP, fitsUp = true)
+ result = inferMenuPosition(view, data)
+ Assert.assertEquals(
+ BrowserMenuPlacement.AnchoredToBottom.Dropdown(view),
+ result.inferredMenuPlacement,
+ )
+
+ data = MenuPositioningData(
+ fitsUp = false,
+ fitsDown = false,
+ availableHeightToTop = 1,
+ availableHeightToBottom = 2,
+ )
+ result = inferMenuPosition(view, data)
+ Assert.assertEquals(
+ BrowserMenuPlacement.AnchoredToTop.ManualAnchoring(view),
+ result.inferredMenuPlacement,
+ )
+
+ data = MenuPositioningData(
+ fitsUp = false,
+ fitsDown = false,
+ availableHeightToTop = 1,
+ availableHeightToBottom = 0,
+ )
+ result = inferMenuPosition(view, data)
+ Assert.assertEquals(
+ BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring(view),
+ result.inferredMenuPlacement,
+ )
+
+ data = MenuPositioningData(askedOrientation = BrowserMenu.Orientation.DOWN, fitsUp = true)
+ result = inferMenuPosition(view, data)
+ Assert.assertEquals(
+ BrowserMenuPlacement.AnchoredToBottom.Dropdown(view),
+ result.inferredMenuPlacement,
+ )
+
+ data = MenuPositioningData(askedOrientation = BrowserMenu.Orientation.UP, fitsDown = true)
+ result = inferMenuPosition(view, data)
+ Assert.assertEquals(
+ BrowserMenuPlacement.AnchoredToTop.Dropdown(view),
+ result.inferredMenuPlacement,
+ )
+ }
+
+ private fun setScreenHeight(value: Int) {
+ val display = ShadowDisplay.getDefaultDisplay()
+ val shadow = Shadows.shadowOf(display)
+ shadow.setHeight(value)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuTest.kt
new file mode 100644
index 0000000000..aff7c709db
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuTest.kt
@@ -0,0 +1,496 @@
+/* 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.browser.menu
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Build
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.Button
+import android.widget.FrameLayout
+import androidx.cardview.widget.CardView
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu.Orientation.DOWN
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.browser.menu.view.DynamicWidthRecyclerView
+import mozilla.components.browser.menu.view.ExpandableLayout
+import mozilla.components.browser.menu.view.StickyHeaderLinearLayoutManager
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowDisplay
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuTest {
+
+ @Test
+ fun `show returns non-null popup window`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ val menu = BrowserMenu(adapter)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertNotNull(popup)
+ }
+
+ @Test
+ fun `show assigns currAnchor and isShown`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ val menu = BrowserMenu(adapter)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertNotNull(popup)
+ assertEquals(anchor, menu.currAnchor)
+ assertTrue(menu.isShown)
+ }
+
+ @Test
+ fun `show assigns width and background color`() {
+ val items = listOf(SimpleBrowserMenuItem("Hello") {})
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ val menu = spy(BrowserMenu(adapter))
+
+ val anchor = Button(testContext)
+ val menuStyle = MenuStyle(
+ backgroundColor = Color.RED,
+ minWidth = 20,
+ maxWidth = 500,
+ )
+ val popup = menu.show(anchor, style = menuStyle)
+
+ assertNotNull(popup)
+ assertEquals(anchor, menu.currAnchor)
+ assertTrue(menu.isShown)
+
+ val cardView = popup.contentView.findViewById<CardView>(R.id.mozac_browser_menu_menuView)
+ val recyclerView = popup.contentView.findViewById<DynamicWidthRecyclerView>(R.id.mozac_browser_menu_recyclerView)
+
+ verify(menu).setColors(any(), eq(menuStyle))
+ assertEquals(ColorStateList.valueOf(Color.RED), cardView.cardBackgroundColor)
+ assertEquals(20, recyclerView.minWidth)
+ assertEquals(500, recyclerView.maxWidth)
+ }
+
+ @Test
+ fun `dismiss sets isShown to false`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ val menu = BrowserMenu(adapter)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+ popup.dismiss()
+
+ assertFalse(menu.isShown)
+ }
+
+ @Test
+ fun `recyclerview adapter will have items for every menu item`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ val menu = BrowserMenu(adapter)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter!!
+ assertNotNull(recyclerAdapter)
+ assertEquals(2, recyclerAdapter.itemCount)
+ }
+
+ @Test
+ fun `endOfMenuAlwaysVisible will be forwarded to recyclerview layoutManager`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = spy(BrowserMenuAdapter(testContext, items))
+ val menu = BrowserMenu(adapter)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor, endOfMenuAlwaysVisible = true)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+ assertTrue(layoutManager.stackFromEnd)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `endOfMenuAlwaysVisible will be forwarded to scrollOnceToTheBottom on devices with Android M and below`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+ val menu = spy(BrowserMenu(adapter))
+ doNothing().`when`(menu).scrollOnceToTheBottom(any())
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor, endOfMenuAlwaysVisible = true)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+
+ assertFalse(layoutManager.stackFromEnd)
+ verify(menu).scrollOnceToTheBottom(any())
+ }
+
+ @Test
+ fun `invalidate will be forwarded to recyclerview adapter`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = spy(BrowserMenuAdapter(testContext, items))
+
+ val menu = BrowserMenu(adapter)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+ assertNotNull(recyclerView.adapter)
+
+ menu.invalidate()
+ Mockito.verify(adapter).invalidate(recyclerView)
+ }
+
+ @Test
+ fun `invalidate is a no-op if the menu is closed`() {
+ val items = listOf(SimpleBrowserMenuItem("Hello") {})
+ val menu = BrowserMenu(BrowserMenuAdapter(testContext, items))
+
+ menu.invalidate()
+ }
+
+ @Test
+ fun `created popup window is displayed automatically`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ val menu = BrowserMenu(adapter)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertTrue(popup.isShowing)
+ }
+
+ @Test
+ fun `dismissing the browser menu will dismiss the popup`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+
+ val menu = BrowserMenu(adapter)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertTrue(popup.isShowing)
+
+ menu.dismiss()
+
+ assertFalse(popup.isShowing)
+ }
+
+ @Test
+ fun `determineMenuOrientation returns Orientation-DOWN by default`() {
+ assertEquals(
+ BrowserMenu.Orientation.DOWN,
+ BrowserMenu.determineMenuOrientation(mock()),
+ )
+ }
+
+ @Test
+ fun `determineMenuOrientation returns Orientation-UP for views with bottom gravity in CoordinatorLayout`() {
+ val params = CoordinatorLayout.LayoutParams(100, 100)
+ params.gravity = Gravity.BOTTOM
+
+ val view = View(testContext)
+ view.layoutParams = params
+
+ assertEquals(
+ BrowserMenu.Orientation.UP,
+ BrowserMenu.determineMenuOrientation(view),
+ )
+ }
+
+ @Test
+ fun `determineMenuOrientation returns Orientation-DOWN for views with top gravity in CoordinatorLayout`() {
+ val params = CoordinatorLayout.LayoutParams(100, 100)
+ params.gravity = Gravity.TOP
+
+ val view = View(testContext)
+ view.layoutParams = params
+
+ assertEquals(
+ BrowserMenu.Orientation.DOWN,
+ BrowserMenu.determineMenuOrientation(view),
+ )
+ }
+
+ @Test
+ fun `Popup#show will initialize the menuPositioningData`() {
+ val adapter = BrowserMenuAdapter(testContext, emptyList())
+ val menu = BrowserMenu(adapter)
+ val anchor = Button(testContext)
+ setScreenHeight(100)
+
+ menu.show(anchor)
+
+ val expected = MenuPositioningData(
+ BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor),
+ DOWN,
+ false,
+ true,
+ 0,
+ 100,
+ 28,
+ )
+ assertEquals(expected, menu.menuPositioningData)
+ }
+
+ @Test
+ fun `configureExpandableMenu will setup a new ExpandabeLayout for a AnchoredToBottom#ManualAnchoring menu`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World", isCollapsingMenuLimit = true) {},
+ )
+ val adapter = BrowserMenuAdapter(testContext, items)
+ val menu = BrowserMenu(adapter)
+ val view = FrameLayout(testContext)
+ val anchor = Button(testContext)
+ menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToBottom.Dropdown(anchor))
+
+ val result = menu.configureExpandableMenu(view, true)
+
+ assertTrue(result is ExpandableLayout)
+ assertTrue(result.getChildAt(0) == view)
+ }
+
+ @Test
+ fun `configureExpandableMenu will setup a new ExpandabeLayout for a AnchoredToBottom#Dropdown menu`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World", isCollapsingMenuLimit = true) {},
+ )
+ val adapter = BrowserMenuAdapter(testContext, items)
+ val menu = BrowserMenu(adapter)
+ val view = FrameLayout(testContext)
+ val anchor = Button(testContext)
+ menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring(anchor))
+
+ val result = menu.configureExpandableMenu(view, true)
+
+ assertTrue(result is ExpandableLayout)
+ assertTrue(result.getChildAt(0) == view)
+ }
+
+ @Test
+ fun `configureExpandableMenu will not setup a new ExpandableLayout if none of the items can serve as a collapsingMenuLimit`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+ val adapter = BrowserMenuAdapter(testContext, items)
+ val menu = BrowserMenu(adapter)
+ val view = FrameLayout(testContext)
+ val anchor = Button(testContext)
+ menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring(anchor))
+
+ val result = menu.configureExpandableMenu(view, true)
+
+ assertFalse(result is ExpandableLayout)
+ assertTrue(result == view)
+ }
+
+ @Test
+ fun `GIVEN a top anchored menu WHEN configureExpandableMenu is called THEN it a new layout manager with sticky item at top is set`() {
+ val menu = spy(BrowserMenu(mock()))
+ // Call show to have a default layout manager set
+ menu.show(View(testContext))
+ val initialLayoutManager = menu.menuList!!.layoutManager
+ menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToTop.Dropdown(mock()))
+
+ menu.configureExpandableMenu(menu.menuList!!, false)
+
+ assertNotSame(initialLayoutManager, menu.menuList!!.layoutManager)
+ assertTrue(menu.menuList!!.layoutManager is StickyHeaderLinearLayoutManager<*>)
+ }
+
+ @Test
+ fun `GIVEN a top anchored menu WHEN configureExpandableMenu is called THEN stackFromEnd is false`() {
+ val menu = spy(BrowserMenu(mock()))
+ // Call show to have a default layout manager set
+ menu.show(View(testContext))
+ menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToTop.Dropdown(mock()))
+
+ menu.configureExpandableMenu(menu.menuList!!, false)
+
+ assertFalse((menu.menuList!!.layoutManager as LinearLayoutManager).stackFromEnd)
+ }
+
+ @Test
+ fun `GIVEN a top anchored menu WHEN configureExpandableMenu is called THEN stackFromEnd is true`() {
+ val menu = spy(BrowserMenu(mock()))
+ // Call show to have a default layout manager set
+ menu.show(View(testContext))
+ menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToTop.Dropdown(mock()))
+
+ menu.configureExpandableMenu(menu.menuList!!, true)
+
+ assertTrue((menu.menuList!!.layoutManager as LinearLayoutManager).stackFromEnd)
+ }
+
+ @Test
+ fun `getNewPopupWindow will return a PopupWindow with MATCH_PARENT height if the view is ExpandableLayout`() {
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(FrameLayout(testContext), 0) { }
+
+ val result = BrowserMenu(mock()).getNewPopupWindow(expandableLayout)
+
+ assertSame(expandableLayout, result.contentView)
+ assertTrue(result.height == MATCH_PARENT)
+ assertTrue(result.width == WRAP_CONTENT)
+ }
+
+ @Test
+ fun `getNewPopupWindow will return a PopupWindow with WRAP_CONTENT height if the view is not ExpandableLayout`() {
+ val notExpandableLayout = FrameLayout(testContext)
+
+ val result = BrowserMenu(mock()).getNewPopupWindow(notExpandableLayout)
+
+ assertSame(notExpandableLayout, result.contentView)
+ assertTrue(result.height == WRAP_CONTENT)
+ assertTrue(result.width == WRAP_CONTENT)
+ }
+
+ @Test
+ fun `popup is dismissed when anchor is detached`() {
+ val items = listOf(
+ SimpleBrowserMenuItem("Mock") {},
+ SimpleBrowserMenuItem("Menu") {},
+ )
+ val adapter = BrowserMenuAdapter(testContext, items)
+ val menu = BrowserMenu(adapter)
+ val anchor = Button(testContext)
+ val popupWindow = menu.show(anchor)
+
+ assertTrue(popupWindow.isShowing)
+
+ menu.onViewDetachedFromWindow(anchor)
+
+ assertFalse(popupWindow.isShowing)
+ }
+
+ @Test
+ fun `GIVEN BrowserMenu WHEN setColor is called with a null MenuStyle THEN the color of the menuView is not changed but cached in backgroundColor`() {
+ val menu = BrowserMenu(mock())
+ val menuParent = CardView(testContext).apply {
+ id = R.id.mozac_browser_menu_menuView
+ setCardBackgroundColor(Color.YELLOW)
+ }
+ val menuLayout = FrameLayout(testContext).also { it.addView(menuParent) }
+ assertEquals(Color.RED, menu.backgroundColor)
+
+ menu.setColors(menuLayout, null)
+
+ assertEquals(Color.YELLOW, menuParent.cardBackgroundColor.defaultColor)
+ assertEquals(Color.YELLOW, menu.backgroundColor)
+ }
+
+ @Test
+ fun `GIVEN BrowserMenu WHEN setColor is called with a valid MenuStyle THEN the color of the menuView is changed and cached in backgroundColor`() {
+ val menu = BrowserMenu(mock())
+ val menuParent = CardView(testContext).apply {
+ id = R.id.mozac_browser_menu_menuView
+ setCardBackgroundColor(Color.YELLOW)
+ }
+ val menuLayout = FrameLayout(testContext).also { it.addView(menuParent) }
+ val menuStyle = MenuStyle(
+ backgroundColor = Color.GREEN,
+ minWidth = 20,
+ maxWidth = 500,
+ )
+ assertEquals(Color.RED, menu.backgroundColor)
+
+ menu.setColors(menuLayout, menuStyle)
+
+ assertEquals(menuStyle.backgroundColor!!.defaultColor, menuParent.cardBackgroundColor.defaultColor)
+ assertEquals(menuStyle.backgroundColor!!.defaultColor, menu.backgroundColor)
+ }
+
+ private fun setScreenHeight(value: Int) {
+ val display = ShadowDisplay.getDefaultDisplay()
+ val shadow = Shadows.shadowOf(display)
+ shadow.setHeight(value)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilderTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilderTest.kt
new file mode 100644
index 0000000000..0c5feb0d5b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilderTest.kt
@@ -0,0 +1,510 @@
+/* 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.browser.menu
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageButton
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.item.BackPressMenuItem
+import mozilla.components.browser.menu.item.BrowserMenuImageText
+import mozilla.components.browser.menu.item.ParentBrowserMenuItem
+import mozilla.components.browser.menu.item.WebExtensionBrowserMenuItem
+import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem
+import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem.Companion.MAIN_EXTENSIONS_MENU_ID
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import androidx.appcompat.R as appcompatR
+import mozilla.components.ui.icons.R as iconsR
+
+@RunWith(AndroidJUnit4::class)
+class WebExtensionBrowserMenuBuilderTest {
+
+ private val submenuPlaceholderMenuItem = WebExtensionPlaceholderMenuItem(id = MAIN_EXTENSIONS_MENU_ID)
+
+ @Test
+ fun `WHEN there are no web extension actions THEN add-ons menu item invokes onAddonsManagerTapped`() {
+ var isAddonsManagerTapped = false
+ val store = BrowserStore()
+ val builder = WebExtensionBrowserMenuBuilder(
+ listOf(mockMenuItem(), submenuPlaceholderMenuItem, mockMenuItem()),
+ store = store,
+ onAddonsManagerTapped = { isAddonsManagerTapped = true },
+ appendExtensionSubMenuAtStart = true,
+ )
+
+ val menu = builder.build(testContext)
+
+ val addonsManagerItem = menu.adapter.visibleItems[1] as? BrowserMenuImageText
+ val addonsManagerItemView =
+ LayoutInflater.from(testContext).inflate(addonsManagerItem!!.getLayoutResource(), null)
+ addonsManagerItem.bind(menu, addonsManagerItemView)
+ assertFalse(isAddonsManagerTapped)
+ addonsManagerItemView.performClick()
+ assertTrue(isAddonsManagerTapped)
+ }
+
+ @Test
+ fun `GIVEN style is provided WHEN creating extension menu THEN styles should be applied to items`() {
+ val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {}
+ val extensions = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = browserAction,
+ ),
+ )
+
+ val store = BrowserStore(BrowserState(extensions = extensions))
+ val style = WebExtensionBrowserMenuBuilder.Style(
+ addonsManagerMenuItemDrawableRes = iconsR.drawable.mozac_ic_extension_24,
+ backPressMenuItemDrawableRes = iconsR.drawable.mozac_ic_back_24,
+ )
+ val builder = WebExtensionBrowserMenuBuilder(
+ listOf(mockMenuItem()),
+ store = store,
+ style = style,
+ appendExtensionSubMenuAtStart = true,
+ )
+
+ val menu = builder.build(testContext)
+ val anchor = ImageButton(testContext)
+ menu.show(anchor)
+
+ val parentMenuItem = menu.adapter.visibleItems[0] as ParentBrowserMenuItem
+ val subMenuItemIndex = parentMenuItem.subMenu.adapter.visibleItems.lastIndex
+ val backPressMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as BackPressMenuItem
+ val addonsManagerItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemIndex] as BrowserMenuImageText
+
+ assertEquals(style.backPressMenuItemDrawableRes, backPressMenuItem.imageResource)
+ assertEquals(style.webExtIconTintColorResource, backPressMenuItem.iconTintColorResource)
+
+ assertEquals(style.webExtIconTintColorResource, addonsManagerItem.iconTintColorResource)
+ assertEquals(style.webExtIconTintColorResource, addonsManagerItem.iconTintColorResource)
+ }
+
+ @Test
+ fun `web extension sub menu add-ons manager sub menu item invokes onAddonsManagerTapped when clicked`() {
+ val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {}
+ val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {}
+
+ val extensions = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = browserAction,
+ pageAction = pageAction,
+ ),
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+
+ var isAddonsManagerTapped = false
+ val builder = WebExtensionBrowserMenuBuilder(
+ listOf(mockMenuItem(), submenuPlaceholderMenuItem, mockMenuItem()),
+ store = store,
+ onAddonsManagerTapped = { isAddonsManagerTapped = true },
+ appendExtensionSubMenuAtStart = true,
+ )
+
+ val menu = builder.build(testContext)
+
+ val parentMenuItem = menu.adapter.visibleItems[1] as? ParentBrowserMenuItem
+ val subMenuItemSize = parentMenuItem!!.subMenu.adapter.visibleItems.size
+ assertEquals(6, subMenuItemSize)
+ val addOnsManagerMenuItem =
+ parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BrowserMenuImageText
+ val addonsManagerItemView =
+ LayoutInflater.from(testContext).inflate(addOnsManagerMenuItem!!.getLayoutResource(), null)
+ addOnsManagerMenuItem.bind(menu, addonsManagerItemView)
+ assertFalse(isAddonsManagerTapped)
+ addonsManagerItemView.performClick()
+ assertTrue(isAddonsManagerTapped)
+ assertNotNull(addOnsManagerMenuItem)
+ }
+
+ @Test
+ fun `web extension submenu is added at the top when usingBottomToolbar is true with no placeholder for submenu`() {
+ val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {}
+ val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {}
+
+ val extensions = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = browserAction,
+ pageAction = pageAction,
+ ),
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+
+ val builder = WebExtensionBrowserMenuBuilder(
+ listOf(mockMenuItem(), mockMenuItem(), mockMenuItem()),
+ store = store,
+ appendExtensionSubMenuAtStart = true,
+ )
+
+ val menu = builder.build(testContext)
+ val anchor = ImageButton(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter!!
+ assertNotNull(recyclerAdapter)
+ assertEquals(4, recyclerAdapter.itemCount)
+
+ val parentMenuItem = menu.adapter.visibleItems[0] as? ParentBrowserMenuItem
+ val subMenuItemSize = parentMenuItem!!.subMenu.adapter.visibleItems.size
+ assertEquals(6, subMenuItemSize)
+ val backMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as? BackPressMenuItem
+ val subMenuExtItemBrowserAction = parentMenuItem.subMenu.adapter.visibleItems[2] as? WebExtensionBrowserMenuItem
+ val subMenuExtItemPageAction = parentMenuItem.subMenu.adapter.visibleItems[3] as? WebExtensionBrowserMenuItem
+ val addOnsManagerMenuItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BrowserMenuImageText
+ assertNotNull(backMenuItem)
+ assertEquals("browser_action", subMenuExtItemBrowserAction!!.action.title)
+ assertEquals("page_action", subMenuExtItemPageAction!!.action.title)
+ assertNotNull(addOnsManagerMenuItem)
+ }
+
+ @Test
+ fun `web extension submenu is added at the bottom when usingBottomToolbar is false with no placeholder for submenu `() {
+ val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {}
+ val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {}
+
+ val extensions = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = browserAction,
+ pageAction = pageAction,
+ ),
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+
+ val builder =
+ WebExtensionBrowserMenuBuilder(
+ listOf(mockMenuItem(), mockMenuItem(), mockMenuItem()),
+ store = store,
+ appendExtensionSubMenuAtStart = false,
+ )
+
+ val menu = builder.build(testContext)
+ val anchor = ImageButton(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter!!
+ assertNotNull(recyclerAdapter)
+ assertEquals(4, recyclerAdapter.itemCount)
+
+ val parentMenuItem = menu.adapter.visibleItems[recyclerAdapter.itemCount - 1] as? ParentBrowserMenuItem
+ val subMenuItemSize = parentMenuItem!!.subMenu.adapter.visibleItems.size
+ assertEquals(6, subMenuItemSize)
+ val backMenuItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BackPressMenuItem
+ val subMenuExtItemBrowserAction = parentMenuItem.subMenu.adapter.visibleItems[2] as? WebExtensionBrowserMenuItem
+ val subMenuExtItemPageAction = parentMenuItem.subMenu.adapter.visibleItems[3] as? WebExtensionBrowserMenuItem
+ val addOnsManagerMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as? BrowserMenuImageText
+ assertNotNull(backMenuItem)
+ assertEquals("browser_action", subMenuExtItemBrowserAction!!.action.title)
+ assertEquals("page_action", subMenuExtItemPageAction!!.action.title)
+ assertNotNull(addOnsManagerMenuItem)
+ }
+
+ @Test
+ fun `web extension is moved to main menu when extension id equals WebExtensionPlaceholderMenuItem id`() {
+ val promotableWebExtensionId = "promotable extension id"
+ val promotableWebExtensionTitle = "promotable extension action title"
+ val testIconTintColorResource = appcompatR.color.accent_material_dark
+
+ val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {}
+ val pageActionPromotableWebExtension = WebExtensionBrowserAction(promotableWebExtensionTitle, true, mock(), "", 0, 0) {}
+
+ // just 2 extensions in the extension menu
+ val extensions = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = null,
+ pageAction = pageAction,
+ ),
+ promotableWebExtensionId to WebExtensionState(
+ promotableWebExtensionId,
+ "url",
+ "name",
+ true,
+ browserAction = null,
+ pageAction = pageActionPromotableWebExtension,
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+
+ // 4 items initially on the main menu
+ val items = listOf(
+ WebExtensionPlaceholderMenuItem(
+ id = promotableWebExtensionId,
+ iconTintColorResource = testIconTintColorResource,
+ ),
+ mockMenuItem(),
+ submenuPlaceholderMenuItem,
+ mockMenuItem(),
+ )
+
+ val builder =
+ WebExtensionBrowserMenuBuilder(
+ items,
+ store = store,
+ appendExtensionSubMenuAtStart = false,
+ )
+
+ val menu = builder.build(testContext)
+ val anchor = ImageButton(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter!! as BrowserMenuAdapter
+ assertNotNull(recyclerAdapter)
+
+ // main menu should have the 4 initial items, one replaced by web extension one replaced by the extensions menu
+ assertEquals(4, recyclerAdapter.itemCount)
+
+ val replacedItem = recyclerAdapter.visibleItems[0]
+ // the replaced item should be a WebExtensionBrowserMenuItem
+ assertEquals("WebExtensionBrowserMenuItem", replacedItem.javaClass.simpleName)
+
+ // the replaced item should have the action title of the WebExtensionBrowserMenuItem
+ assertEquals(promotableWebExtensionTitle, (replacedItem as WebExtensionBrowserMenuItem).action.title)
+
+ // the replaced item should have the icon tint set by placeholder
+ assertEquals(testIconTintColorResource, replacedItem.iconTintColorResource)
+
+ val parentMenuItem = menu.adapter.visibleItems[2] as? ParentBrowserMenuItem
+ val subMenuItemSize = parentMenuItem!!.subMenu.adapter.visibleItems.size
+
+ // add-ons should only have one extension, 2 dividers, 1 add-on manager item and 1 back menu item
+ assertEquals(5, subMenuItemSize)
+
+ val backMenuItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BackPressMenuItem
+ val subMenuExtItemPageAction = parentMenuItem.subMenu.adapter.visibleItems[2] as? WebExtensionBrowserMenuItem
+ val addOnsManagerMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as? BrowserMenuImageText
+
+ assertNotNull(backMenuItem)
+ assertEquals("page_action", subMenuExtItemPageAction!!.action.title)
+ assertNotNull(addOnsManagerMenuItem)
+ }
+
+ @Test
+ fun `GIVEN a placeholder with the id MAIN_EXTENSIONS_MENU_ID WHEN the menu is built THEN the extensions sub-menu is inserted in its place`() {
+ val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {}
+
+ val extensions = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = null,
+ pageAction = pageAction,
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+
+ // 3 items initially on the main menu
+ val items = listOf(
+ mockMenuItem(),
+ submenuPlaceholderMenuItem,
+ mockMenuItem(),
+ )
+
+ val builder =
+ WebExtensionBrowserMenuBuilder(
+ items,
+ store = store,
+ appendExtensionSubMenuAtStart = false,
+ )
+
+ val menu = builder.build(testContext)
+ val anchor = ImageButton(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter!! as BrowserMenuAdapter
+ assertNotNull(recyclerAdapter)
+
+ // main menu should have the 3 initial items, one replaced by extensions sub-menu
+ assertEquals(3, recyclerAdapter.itemCount)
+
+ val parentMenuItem = recyclerAdapter.visibleItems[1] as ParentBrowserMenuItem
+ // the replaced item should be a ParentBrowserMenuItem
+ assertEquals("ParentBrowserMenuItem", parentMenuItem.javaClass.simpleName)
+
+ // the replaced item should have the action title of the WebExtensionBrowserMenuItem
+ assertEquals(testContext.getString(R.string.mozac_browser_menu_extensions), parentMenuItem.label)
+
+ val subMenuItemSize = parentMenuItem.subMenu.adapter.visibleItems.size
+
+ // add-ons should only have one extension, 2 dividers, 1 add-on manager item and 1 back menu item
+ assertEquals(5, subMenuItemSize)
+
+ val backMenuItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BackPressMenuItem
+ val subMenuExtItemPageAction = parentMenuItem.subMenu.adapter.visibleItems[2] as? WebExtensionBrowserMenuItem
+ val addOnsManagerMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as? BrowserMenuImageText
+
+ assertNotNull(backMenuItem)
+ assertEquals("page_action", subMenuExtItemPageAction!!.action.title)
+ assertNotNull(addOnsManagerMenuItem)
+ }
+
+ @Test
+ fun `GIVEN showAddonsInMenu with value true WHEN the menu is built THEN the Add-ons item is added at the bottom`() {
+ val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {}
+
+ val extensions = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = null,
+ pageAction = pageAction,
+ ),
+ )
+ val store = BrowserStore(BrowserState(extensions = extensions))
+
+ // 2 items initially on the main menu
+ val items = listOf(
+ mockMenuItem(),
+ mockMenuItem(),
+ )
+
+ val builder =
+ WebExtensionBrowserMenuBuilder(
+ items,
+ store = store,
+ showAddonsInMenu = true,
+ )
+
+ val menu = builder.build(testContext)
+ val anchor = ImageButton(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter as BrowserMenuAdapter
+ assertNotNull(recyclerAdapter)
+
+ // main menu should have 3 items and the last one should be the "Add-ons" item
+ assertEquals(3, recyclerAdapter.itemCount)
+
+ val lastItem = recyclerAdapter.visibleItems[2]
+ assert(lastItem is ParentBrowserMenuItem && lastItem.label == testContext.getString(R.string.mozac_browser_menu_extensions))
+ }
+
+ @Test
+ fun `GIVEN showAddonsInMenu with value false WHEN the menu is built THEN the Add-ons item is not added`() {
+ val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {}
+
+ val extensions = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = null,
+ pageAction = pageAction,
+ ),
+ )
+ val store = BrowserStore(BrowserState(extensions = extensions))
+
+ // 2 items initially on the main menu
+ val items = listOf(
+ mockMenuItem(),
+ mockMenuItem(),
+ )
+
+ val builder =
+ WebExtensionBrowserMenuBuilder(
+ items,
+ store = store,
+ showAddonsInMenu = false,
+ )
+
+ val menu = builder.build(testContext)
+ val anchor = ImageButton(testContext)
+ val popup = menu.show(anchor)
+
+ val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter as BrowserMenuAdapter
+ assertNotNull(recyclerAdapter)
+
+ // main menu should have 2 items
+ assertEquals(2, recyclerAdapter.itemCount)
+
+ recyclerAdapter.visibleItems.forEach { item ->
+ assert(item !is ParentBrowserMenuItem)
+ }
+ }
+
+ private fun mockMenuItem() = object : BrowserMenuItem {
+ override val visible: () -> Boolean = { true }
+
+ override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple
+
+ override fun bind(menu: BrowserMenu, view: View) {}
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt
new file mode 100644
index 0000000000..77769db9c4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt
@@ -0,0 +1,525 @@
+/* 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.browser.menu
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.WebExtensionBrowserMenu.Companion.getOrUpdateWebExtensionMenuItems
+import mozilla.components.browser.menu.WebExtensionBrowserMenu.Companion.webExtensionBrowserActions
+import mozilla.components.browser.menu.WebExtensionBrowserMenu.Companion.webExtensionPageActions
+import mozilla.components.browser.menu.facts.BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
+import mozilla.components.concept.engine.webextension.WebExtensionPageAction
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import mozilla.components.support.base.facts.Action as FactsAction
+
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class WebExtensionBrowserMenuTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ webExtensionBrowserActions.clear()
+ webExtensionPageActions.clear()
+ }
+
+ @Test
+ fun `actions are only updated when the menu is shown`() {
+ webExtensionBrowserActions.clear()
+ val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {}
+ val pageAction = WebExtensionPageAction("browser_action", true, mock(), "", 0, 0) {}
+ val extensions = mapOf(
+ "browser_action" to WebExtensionState(
+ "browser_action",
+ "url",
+ "name",
+ true,
+ browserAction = browserAction,
+ pageAction = pageAction,
+ ),
+ )
+
+ val store =
+ BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+ val items = listOf(
+ SimpleBrowserMenuItem("Hello") {},
+ SimpleBrowserMenuItem("World") {},
+ )
+
+ val adapter = BrowserMenuAdapter(testContext, items)
+ val menu = WebExtensionBrowserMenu(adapter, store)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertNotNull(popup)
+
+ val defaultBrowserAction =
+ WebExtensionBrowserAction("default_title", true, mock(), "", 0, 0) {}
+ val defaultPageAction =
+ WebExtensionPageAction("default_title", true, mock(), "", 0, 0) {}
+ val defaultExtensions: Map<String, WebExtensionState> = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = defaultBrowserAction,
+ pageAction = defaultPageAction,
+ ),
+ )
+
+ createTab(
+ "https://www.example.org",
+ id = "tab1",
+ extensions = defaultExtensions,
+ )
+ assertEquals(1, webExtensionBrowserActions.size)
+ assertEquals(1, webExtensionPageActions.size)
+
+ menu.dismiss()
+ val anotherBrowserAction =
+ WebExtensionBrowserAction("another_title", true, mock(), "", 0, 0) {}
+ val anotherPageAction =
+ WebExtensionBrowserAction("another_title", true, mock(), "", 0, 0) {}
+ val anotherExtension: Map<String, WebExtensionState> = mapOf(
+ "id2" to WebExtensionState(
+ "id2",
+ "url",
+ "name",
+ true,
+ browserAction = anotherBrowserAction,
+ pageAction = anotherPageAction,
+ ),
+ )
+
+ createTab(
+ "https://www.example2.org",
+ id = "tab2",
+ extensions = anotherExtension,
+ )
+ assertEquals(0, webExtensionBrowserActions.size)
+ assertEquals(0, webExtensionPageActions.size)
+ }
+
+ @Test
+ fun `render web extension actions from browser state`() {
+ val defaultBrowserAction =
+ WebExtensionBrowserAction("default_browser_action_title", true, mock(), "", 0, 0) {}
+ val defaultPageAction =
+ WebExtensionPageAction("default_page_action_title", true, mock(), "", 0, 0) {}
+ val overriddenBrowserAction =
+ WebExtensionBrowserAction("overridden_browser_action_title", true, mock(), "", 0, 0) {}
+ val overriddenPageAction =
+ WebExtensionBrowserAction("overridden_page_action_title", true, mock(), "", 0, 0) {}
+
+ val extensions: Map<String, WebExtensionState> = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = defaultBrowserAction,
+ pageAction = defaultPageAction,
+ ),
+ )
+ val overriddenExtensions: Map<String, WebExtensionState> = mapOf(
+ "id" to WebExtensionState(
+ "id",
+ "url",
+ "name",
+ true,
+ browserAction = overriddenBrowserAction,
+ pageAction = overriddenPageAction,
+ ),
+ )
+ val store =
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.example.org",
+ id = "tab1",
+ extensions = overriddenExtensions,
+ ),
+ ),
+ selectedTabId = "tab1",
+ extensions = extensions,
+ ),
+ )
+
+ val browserMenuItems =
+ getOrUpdateWebExtensionMenuItems(store.state, store.state.selectedTab)
+ assertEquals(2, browserMenuItems.size)
+
+ var actionMenu = browserMenuItems[0]
+ assertEquals(
+ "overridden_browser_action_title",
+ actionMenu.action.title,
+ )
+
+ actionMenu = browserMenuItems[1]
+ assertEquals(
+ "overridden_page_action_title",
+ actionMenu.action.title,
+ )
+ }
+
+ @Test
+ fun `getOrUpdateWebExtensionMenuItems does not include actions from disabled extensions`() {
+ val enabledPageAction =
+ WebExtensionBrowserAction("enabled_page_action", true, mock(), "", 0, 0) {}
+ val disabledPageAction =
+ WebExtensionBrowserAction("disabled_page_action", true, mock(), "", 0, 0) {}
+ val enabledBrowserAction =
+ WebExtensionBrowserAction("enabled_browser_action", true, mock(), "", 0, 0) {}
+ val disabledBrowserAction =
+ WebExtensionBrowserAction("disabled_browser_action", true, mock(), "", 0, 0) {}
+
+ val extensions = mapOf(
+ "enabled" to WebExtensionState(
+ "enabled",
+ "url",
+ "name",
+ true,
+ browserAction = enabledBrowserAction,
+ pageAction = enabledPageAction,
+ ),
+ "disabled" to WebExtensionState(
+ "disabled",
+ "url",
+ "name",
+ false,
+ browserAction = disabledBrowserAction,
+ pageAction = disabledPageAction,
+ ),
+ )
+
+ val store =
+ BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+
+ val browserMenuItems = getOrUpdateWebExtensionMenuItems(store.state)
+ assertEquals(2, browserMenuItems.size)
+
+ var menuAction = browserMenuItems[0]
+ assertEquals(
+ "enabled_browser_action",
+ menuAction.action.title,
+ )
+ menuAction = browserMenuItems[1]
+ assertEquals(
+ "enabled_page_action",
+ menuAction.action.title,
+ )
+ }
+
+ @Test
+ fun `browser actions can be overridden per tab`() {
+ webExtensionBrowserActions.clear()
+ val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() }
+ val pageAction = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val pageActionOverride = Action(
+ title = "updatedTitle",
+ loadIcon = null,
+ enabled = false,
+ badgeText = "updatedText",
+ badgeTextColor = Color.RED,
+ badgeBackgroundColor = Color.GREEN,
+ ) {}
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val browserActionOverride = Action(
+ title = "updatedTitle",
+ loadIcon = null,
+ enabled = false,
+ badgeText = "updatedText",
+ badgeTextColor = Color.RED,
+ badgeBackgroundColor = Color.GREEN,
+ ) {}
+
+ val browserExtensions = HashMap<String, WebExtensionState>()
+ browserExtensions["1"] =
+ WebExtensionState(id = "1", browserAction = browserAction, pageAction = pageAction)
+
+ val browserState = BrowserState(extensions = browserExtensions)
+ getOrUpdateWebExtensionMenuItems(browserState, mock())
+
+ // Verifying global browser action
+ assertTrue(webExtensionBrowserActions.size == 1)
+ var ext1 = webExtensionBrowserActions["1"]
+ assertTrue(ext1?.action?.enabled!!)
+ assertEquals("badgeText", ext1.action.badgeText!!)
+ assertEquals("title", ext1.action.title!!)
+ assertEquals(loadIcon, ext1.action.loadIcon!!)
+ assertEquals(Color.WHITE, ext1.action.badgeTextColor!!)
+ assertEquals(Color.BLUE, ext1.action.badgeBackgroundColor!!)
+
+ // Verifying global page action
+ assertTrue(webExtensionPageActions.size == 1)
+ ext1 = webExtensionPageActions["1"]!!
+ assertTrue(ext1.action.enabled!!)
+ assertEquals("badgeText", ext1.action.badgeText!!)
+ assertEquals("title", ext1.action.title!!)
+ assertEquals(loadIcon, ext1.action.loadIcon!!)
+ assertEquals(Color.WHITE, ext1.action.badgeTextColor!!)
+ assertEquals(Color.BLUE, ext1.action.badgeBackgroundColor!!)
+
+ val tabExtensions = HashMap<String, WebExtensionState>()
+ tabExtensions["1"] = WebExtensionState(
+ id = "1",
+ browserAction = browserActionOverride,
+ pageAction = pageActionOverride,
+ )
+
+ val tabSessionState = TabSessionState(
+ content = mock(),
+ extensionState = tabExtensions,
+ )
+
+ getOrUpdateWebExtensionMenuItems(browserState, tabSessionState)
+
+ // Verify rendering session-specific browser action override
+ assertTrue(webExtensionBrowserActions.size == 1)
+ var updatedExt1 = webExtensionBrowserActions["1"]
+ assertFalse(updatedExt1?.action?.enabled!!)
+ assertEquals("updatedText", updatedExt1.action.badgeText!!)
+ assertEquals("updatedTitle", updatedExt1.action.title!!)
+ assertEquals(loadIcon, updatedExt1.action.loadIcon!!)
+ assertEquals(Color.RED, updatedExt1.action.badgeTextColor!!)
+ assertEquals(Color.GREEN, updatedExt1.action.badgeBackgroundColor!!)
+
+ // Verify rendering session-specific page action override
+ assertTrue(webExtensionPageActions.size == 1)
+ updatedExt1 = webExtensionBrowserActions["1"]!!
+ assertFalse(updatedExt1.action.enabled!!)
+ assertEquals("updatedText", updatedExt1.action.badgeText!!)
+ assertEquals("updatedTitle", updatedExt1.action.title!!)
+ assertEquals(loadIcon, updatedExt1.action.loadIcon!!)
+ assertEquals(Color.RED, updatedExt1.action.badgeTextColor!!)
+ assertEquals(Color.GREEN, updatedExt1.action.badgeBackgroundColor!!)
+ }
+
+ @Test
+ fun `actions are sorted per extension name`() {
+ val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() }
+
+ val actionExt1 = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val actionExt2 = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val browserExtensions = HashMap<String, WebExtensionState>()
+ browserExtensions["1"] =
+ WebExtensionState(id = "1", name = "extensionA", browserAction = actionExt1)
+ browserExtensions["2"] =
+ WebExtensionState(id = "2", name = "extensionB", browserAction = actionExt2)
+
+ val tabSessionState = TabSessionState(
+ content = mock(),
+ extensionState = emptyMap(),
+ )
+
+ val browserState = BrowserState(extensions = browserExtensions)
+ val actionItems = getOrUpdateWebExtensionMenuItems(browserState, tabSessionState)
+ assertEquals(2, actionItems.size)
+ assertEquals(actionExt1, actionItems[0].action)
+ assertEquals(actionExt2, actionItems[1].action)
+ }
+
+ @Test
+ fun `clicking on the menu item should emit a BrowserMenuFacts with the web extension id`() {
+ val imageView: ImageView = mock()
+ val badgeView: TextView = mock()
+ val labelView = TextView(testContext)
+ val container = View(testContext)
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(mock())
+
+ val browserAction =
+ WebExtensionBrowserAction("title", true, mock(), "", 0, 0) {}
+ val pageAction =
+ WebExtensionPageAction("title", true, mock(), "", 0, 0) {}
+ val extensions: Map<String, WebExtensionState> = mapOf(
+ "some_example_id" to WebExtensionState(
+ "some_example_id",
+ "url",
+ "name",
+ true,
+ browserAction = browserAction,
+ pageAction = pageAction,
+ ),
+ )
+
+ val store =
+ BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+
+ val browserMenuItems = getOrUpdateWebExtensionMenuItems(store.state)
+ val menuItem = browserMenuItems[1]
+ val menu: WebExtensionBrowserMenu = mock()
+
+ menuItem.bind(menu, view)
+
+ CollectionProcessor.withFactCollection { facts ->
+ container.performClick()
+
+ val fact = facts[0]
+ assertEquals(FactsAction.CLICK, fact.action)
+ assertEquals(WEB_EXTENSION_MENU_ITEM, fact.item)
+ assertEquals(1, fact.metadata?.size)
+ assertTrue(fact.metadata?.containsKey("id")!!)
+ assertEquals("some_example_id", fact.metadata?.get("id"))
+ }
+ }
+
+ @Test
+ fun `hides browser and page actions in private tabs if extension is not allowed to run`() {
+ val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() }
+
+ val actionExt1 = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val tabSessionState = TabSessionState(
+ content = mock(),
+ extensionState = emptyMap(),
+ )
+ whenever(tabSessionState.content.private).thenReturn(true)
+
+ val browserExtensions = HashMap<String, WebExtensionState>()
+ browserExtensions["1"] =
+ WebExtensionState(id = "1", name = "extensionA", browserAction = actionExt1)
+ val browserState = BrowserState(extensions = browserExtensions)
+ val actionItems = getOrUpdateWebExtensionMenuItems(browserState, tabSessionState)
+ assertEquals(0, actionItems.size)
+
+ val browserExtensionsAllowedInPrivateBrowsing = HashMap<String, WebExtensionState>()
+ browserExtensionsAllowedInPrivateBrowsing["1"] =
+ WebExtensionState(id = "1", allowedInPrivateBrowsing = true, name = "extensionA", browserAction = actionExt1)
+ val browserStateAllowedInPrivateBrowsing = BrowserState(extensions = browserExtensionsAllowedInPrivateBrowsing)
+ val actionItemsAllowedInPrivateBrowsing = getOrUpdateWebExtensionMenuItems(browserStateAllowedInPrivateBrowsing, tabSessionState)
+ assertEquals(1, actionItemsAllowedInPrivateBrowsing.size)
+ assertEquals(actionExt1, actionItemsAllowedInPrivateBrowsing[0].action)
+ }
+
+ @Test
+ fun `does not include menu item for disabled paged actions`() {
+ val enabledPageAction =
+ WebExtensionBrowserAction("enabled_page_action", true, mock(), "", 0, 0) {}
+ val disabledPageAction =
+ WebExtensionBrowserAction("disabled_page_action", false, mock(), "", 0, 0) {}
+
+ val extensions = mapOf(
+ "ext1" to WebExtensionState(
+ "ext1",
+ "url",
+ "name",
+ true,
+ pageAction = enabledPageAction,
+ ),
+ "ext2" to WebExtensionState(
+ "ext2",
+ "url",
+ "name",
+ true,
+ pageAction = disabledPageAction,
+ ),
+ )
+
+ val store =
+ BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ )
+
+ val browserMenuItems = getOrUpdateWebExtensionMenuItems(store.state)
+ assertEquals(1, browserMenuItems.size)
+
+ var menuAction = browserMenuItems[0]
+ assertEquals(
+ "enabled_page_action",
+ menuAction.action.title,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/ext/BrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/ext/BrowserMenuItemTest.kt
new file mode 100644
index 0000000000..36e7fb15ba
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/ext/BrowserMenuItemTest.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.browser.menu.ext
+
+import android.graphics.Color
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
+import mozilla.components.browser.menu.item.BrowserMenuImageText
+import org.junit.Assert
+import org.junit.Test
+
+class BrowserMenuItemTest {
+
+ @Test
+ fun `highest prio item gets selected`() {
+ val highlightLow1 = BrowserMenuHighlight.LowPriority(Color.YELLOW)
+ val highlightLow2 = BrowserMenuHighlight.LowPriority(Color.RED)
+ val highlightHigh = BrowserMenuHighlight.HighPriority(Color.GREEN)
+
+ val list = listOf(
+ BrowserMenuHighlightableItem(
+ label = "Test1",
+ startImageResource = 0,
+ highlight = highlightLow1,
+ isHighlighted = { true },
+ ),
+ BrowserMenuHighlightableItem(
+ label = "Test2",
+ startImageResource = 0,
+ highlight = highlightLow2,
+ isHighlighted = { true },
+ ),
+ BrowserMenuImageText(
+ label = "Test3",
+ imageResource = 0,
+ ),
+ BrowserMenuHighlightableItem(
+ label = "Test4",
+ startImageResource = 0,
+ highlight = highlightHigh,
+ isHighlighted = { true },
+ ),
+ )
+ Assert.assertEquals(highlightHigh, list.getHighlight())
+ }
+
+ @Test
+ fun `invisible item does not get selected`() {
+ val highlightedVisible = BrowserMenuHighlightableItem(
+ label = "Test1",
+ startImageResource = 0,
+ highlight = BrowserMenuHighlight.LowPriority(Color.YELLOW),
+ isHighlighted = { true },
+ )
+ val highlightedInvisible = BrowserMenuHighlightableItem(
+ label = "Test2",
+ startImageResource = 0,
+ highlight = BrowserMenuHighlight.HighPriority(Color.GREEN),
+ isHighlighted = { true },
+ )
+ highlightedInvisible.visible = { false }
+
+ val list = listOf(highlightedVisible, highlightedInvisible)
+ Assert.assertEquals(highlightedVisible.highlight, list.getHighlight())
+ }
+
+ @Test
+ fun `non highlightable item does not get selected`() {
+ val highlight = BrowserMenuHighlight.LowPriority(Color.YELLOW)
+ val highlight2 = BrowserMenuHighlight.HighPriority(Color.GREEN)
+ val list = listOf(
+ BrowserMenuHighlightableItem(
+ label = "Test1",
+ startImageResource = 0,
+ highlight = highlight,
+ isHighlighted = { true },
+ ),
+ BrowserMenuHighlightableItem(
+ label = "Test2",
+ startImageResource = 0,
+ highlight = highlight2,
+ isHighlighted = { false },
+ ),
+ )
+ Assert.assertEquals(highlight, list.getHighlight())
+ }
+
+ @Test
+ fun `higher prio highlight which cannot propagate does not get selected`() {
+ val highlight = BrowserMenuHighlight.LowPriority(Color.YELLOW)
+ val highlightNonPropagate = BrowserMenuHighlight.HighPriority(
+ Color.GREEN,
+ canPropagate = false,
+ )
+ val list = listOf(
+ BrowserMenuHighlightableItem(
+ label = "Test1",
+ startImageResource = 0,
+ highlight = highlight,
+ isHighlighted = { true },
+ ),
+ BrowserMenuHighlightableItem(
+ label = "Test2",
+ startImageResource = 0,
+ highlight = highlightNonPropagate,
+ isHighlighted = { true },
+ ),
+ )
+ Assert.assertEquals(highlight, list.getHighlight())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItemTest.kt
new file mode 100644
index 0000000000..3b22a871cc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItemTest.kt
@@ -0,0 +1,90 @@
+/* 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.browser.menu.item
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuAdapter
+import mozilla.components.browser.menu.R
+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
+
+@RunWith(AndroidJUnit4::class)
+class AbstractParentBrowserMenuItemTest {
+
+ @Test
+ fun bind() {
+ val view = View(testContext)
+ var subMenuShowCalled = false
+ var subMenuDismissCalled = false
+
+ val subMenuItem = SimpleBrowserMenuItem("test")
+ val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(subMenuItem))
+ val subMenu = BrowserMenu(subMenuAdapter)
+ val parentMenuItem = DummyParentBrowserMenuItem(subMenu)
+ val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem))
+ val nestedMenu = BrowserMenu(nestedMenuAdapter)
+
+ parentMenuItem.onSubMenuShow = {
+ subMenuShowCalled = true
+ }
+ parentMenuItem.onSubMenuDismiss = {
+ subMenuDismissCalled = true
+ }
+
+ parentMenuItem.bind(nestedMenu, view)
+ nestedMenu.show(view)
+ assertTrue(nestedMenu.isShown)
+
+ view.performClick()
+ assertFalse(nestedMenu.isShown)
+ assertTrue(subMenu.isShown)
+ assertTrue(subMenuShowCalled)
+
+ subMenu.dismiss()
+ assertTrue(subMenuDismissCalled)
+ }
+
+ @Test
+ fun onBackPressed() {
+ val view = View(testContext)
+ var subMenuDismissCalled = false
+
+ val subMenuItem = SimpleBrowserMenuItem("test")
+ val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(subMenuItem))
+ val subMenu = BrowserMenu(subMenuAdapter)
+ val parentMenuItem = DummyParentBrowserMenuItem(subMenu)
+ val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem))
+ val nestedMenu = BrowserMenu(nestedMenuAdapter)
+
+ parentMenuItem.onSubMenuDismiss = {
+ subMenuDismissCalled = true
+ }
+
+ parentMenuItem.bind(nestedMenu, view)
+ // verify onBackPressed while sub menu is not shown does nothing.
+ parentMenuItem.onBackPressed(nestedMenu, view)
+ assertFalse(subMenuDismissCalled)
+
+ nestedMenu.show(view)
+ view.performClick()
+ parentMenuItem.onBackPressed(nestedMenu, view)
+ assertTrue(nestedMenu.isShown)
+ assertFalse(subMenu.isShown)
+ assertTrue(subMenuDismissCalled)
+ }
+}
+
+class DummyParentBrowserMenuItem(
+ subMenu: BrowserMenu,
+ endOfMenuAlwaysVisible: Boolean = false,
+) : AbstractParentBrowserMenuItem(subMenu, endOfMenuAlwaysVisible) {
+ override var visible: () -> Boolean = { true }
+ override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_simple
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCategoryTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCategoryTest.kt
new file mode 100644
index 0000000000..8c94ffe6e9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCategoryTest.kt
@@ -0,0 +1,183 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.graphics.Typeface
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuCategoryTest {
+ private lateinit var menuCategory: BrowserMenuCategory
+ private val context: Context get() = ApplicationProvider.getApplicationContext()
+ private val label = "test"
+
+ @Before
+ fun setup() {
+ menuCategory = BrowserMenuCategory(label)
+ }
+
+ @Test
+ fun `menu category uses correct layout`() {
+ assertEquals(R.layout.mozac_browser_menu_category, menuCategory.getLayoutResource())
+ }
+
+ @Test
+ fun `menu category has correct label`() {
+ assertEquals(label, menuCategory.label)
+ }
+
+ @Test
+ fun `menu category should handle initialization with text size`() {
+ val menuCategoryWithTextSize = BrowserMenuCategory(label, 12f)
+
+ val view = inflate(menuCategoryWithTextSize)
+ val textView = view.findViewById<TextView>(R.id.category_text)
+
+ assertEquals(12f, textView.textSize)
+ }
+
+ @Test
+ fun `menu category should handle initialization with text colour resource`() {
+ val menuCategoryWithTextColour = BrowserMenuCategory(label, textColorResource = android.R.color.holo_red_dark)
+
+ val view = inflate(menuCategoryWithTextColour)
+ val textView = view.findViewById<TextView>(R.id.category_text)
+ val expectedColour = ContextCompat.getColor(textView.context, android.R.color.holo_red_dark)
+
+ assertEquals(expectedColour, textView.currentTextColor)
+ }
+
+ @Test
+ fun `GIVEN a BrowserMenuCategory, WHEN backgroundColorResource is provided, THEN the background resource is set to that value`() {
+ val expectedColour = android.R.color.holo_red_dark
+ val menuCategoryWithBackgroundColour = BrowserMenuCategory(label, backgroundColorResource = expectedColour)
+ val view: TextView = mock()
+ val menu: BrowserMenu = mock()
+
+ menuCategoryWithBackgroundColour.bind(menu, view)
+
+ verify(view).setBackgroundResource(expectedColour)
+ }
+
+ @Test
+ fun `GIVEN a BrowserMenuCategory, WHEN backgroundColorResource is not provided, THEN no background is set`() {
+ val menuCategoryWithNoBackgroundColour = BrowserMenuCategory(label)
+ val view: TextView = mock()
+ val menu: BrowserMenu = mock()
+
+ menuCategoryWithNoBackgroundColour.bind(menu, view)
+
+ verify(view, never()).setBackgroundResource(anyInt())
+ }
+
+ @Test
+ fun `menu category should handle initialization with text style`() {
+ val menuCategoryWithTextStyle = BrowserMenuCategory(label, textStyle = Typeface.ITALIC)
+
+ val view = inflate(menuCategoryWithTextStyle)
+ val textView = view.findViewById<TextView>(R.id.category_text)
+
+ assertEquals(Typeface.ITALIC, textView.typeface.style)
+ }
+
+ @Test
+ fun `menu category should handle initialization with text alignment`() {
+ val menuCategoryWithTextAlignment = BrowserMenuCategory(label, textAlignment = View.TEXT_ALIGNMENT_VIEW_END)
+
+ val view = inflate(menuCategoryWithTextAlignment)
+ val textView = view.findViewById<TextView>(R.id.category_text)
+
+ assertEquals(View.TEXT_ALIGNMENT_VIEW_END, textView.textAlignment)
+ }
+
+ @Test
+ fun `menu category can be converted to candidate`() {
+ assertEquals(
+ DecorativeTextMenuCandidate(
+ label,
+ textStyle = TextStyle(
+ textStyle = Typeface.BOLD,
+ textAlignment = View.TEXT_ALIGNMENT_VIEW_START,
+ ),
+ ),
+ BrowserMenuCategory(label).asCandidate(context),
+ )
+
+ assertEquals(
+ DecorativeTextMenuCandidate(
+ label,
+ textStyle = TextStyle(
+ size = 12f,
+ textStyle = Typeface.BOLD,
+ textAlignment = View.TEXT_ALIGNMENT_VIEW_START,
+ ),
+ ),
+ BrowserMenuCategory(label, 12f).asCandidate(context),
+ )
+
+ assertEquals(
+ DecorativeTextMenuCandidate(
+ label,
+ textStyle = TextStyle(
+ color = ContextCompat.getColor(context, android.R.color.holo_red_dark),
+ textStyle = Typeface.BOLD,
+ textAlignment = View.TEXT_ALIGNMENT_VIEW_START,
+ ),
+ ),
+ BrowserMenuCategory(
+ label,
+ textColorResource = android.R.color.holo_red_dark,
+ ).asCandidate(context),
+ )
+
+ assertEquals(
+ DecorativeTextMenuCandidate(
+ label,
+ textStyle = TextStyle(
+ textStyle = Typeface.ITALIC,
+ textAlignment = View.TEXT_ALIGNMENT_VIEW_START,
+ ),
+ ),
+ BrowserMenuCategory(label, textStyle = Typeface.ITALIC).asCandidate(context),
+ )
+
+ assertEquals(
+ DecorativeTextMenuCandidate(
+ label,
+ textStyle = TextStyle(
+ textStyle = Typeface.BOLD,
+ textAlignment = View.TEXT_ALIGNMENT_VIEW_END,
+ ),
+ ),
+ BrowserMenuCategory(label, textAlignment = View.TEXT_ALIGNMENT_VIEW_END).asCandidate(context),
+ )
+ }
+
+ private fun inflate(browserMenuCategory: BrowserMenuCategory): View {
+ val view = LayoutInflater.from(context).inflate(browserMenuCategory.getLayoutResource(), null)
+ val mockMenu = Mockito.mock(BrowserMenu::class.java)
+ browserMenuCategory.bind(mockMenu, view)
+ return view
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCheckboxTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCheckboxTest.kt
new file mode 100644
index 0000000000..38bcb45c89
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCheckboxTest.kt
@@ -0,0 +1,38 @@
+/* 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.browser.menu.item
+
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BrowserMenuCheckboxTest {
+
+ @Test
+ fun `browser checkbox uses correct layout`() {
+ val item = BrowserMenuCheckbox("Hello") {}
+ assertEquals(R.layout.mozac_browser_menu_item_checkbox, item.getLayoutResource())
+ }
+
+ @Test
+ fun `checkbox can be converted to candidate with correct end type`() {
+ val listener = { _: Boolean -> }
+
+ assertEquals(
+ CompoundMenuCandidate(
+ "Hello",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ onCheckedChange = listener,
+ ),
+ BrowserMenuCheckbox(
+ "Hello",
+ listener = listener,
+ ).asCandidate(mock()),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButtonTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButtonTest.kt
new file mode 100644
index 0000000000..25a09bd77c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButtonTest.kt
@@ -0,0 +1,190 @@
+/* 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.browser.menu.item
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewTreeObserver
+import android.widget.CheckBox
+import androidx.appcompat.widget.SwitchCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuCompoundButtonTest {
+
+ @Test
+ fun `simple menu items are always visible by default`() {
+ val item = SimpleTestBrowserCompoundButton("Hello") {
+ // do nothing
+ }
+
+ assertTrue(item.visible())
+ }
+
+ @Test
+ fun `layout resource can be inflated`() {
+ val item = SimpleTestBrowserCompoundButton("Hello") {
+ // do nothing
+ }
+
+ val view = LayoutInflater.from(testContext)
+ .inflate(item.getLayoutResource(), null)
+
+ assertNotNull(view)
+ }
+
+ @Test
+ fun `clicking bound view will invoke callback and dismiss menu`() {
+ var callbackInvoked = false
+
+ val item = SimpleTestBrowserCompoundButton("Hello") { checked ->
+ callbackInvoked = checked
+ }
+
+ val menu = mock(BrowserMenu::class.java)
+ val view = CheckBox(testContext)
+
+ item.bind(menu, view)
+
+ view.isChecked = true
+
+ assertTrue(callbackInvoked)
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `initialState is invoked on bind`() {
+ val initialState: () -> Boolean = { true }
+ val item = SimpleTestBrowserCompoundButton("Hello", initialState) {}
+
+ val menu = mock(BrowserMenu::class.java)
+ val view = spy(CheckBox(testContext))
+ item.bind(menu, view)
+
+ verify(view).isChecked = true
+ }
+
+ @Test
+ fun `hitting default methods`() {
+ val item = SimpleTestBrowserCompoundButton("") {}
+ item.invalidate(mock(View::class.java))
+ }
+
+ @Test
+ fun `menu compound button can be converted to candidate`() {
+ val listener = { _: Boolean -> }
+
+ assertEquals(
+ CompoundMenuCandidate(
+ "Hello",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ onCheckedChange = listener,
+ ),
+ SimpleTestBrowserCompoundButton(
+ "Hello",
+ listener = listener,
+ ).asCandidate(testContext),
+ )
+
+ assertEquals(
+ CompoundMenuCandidate(
+ "Hello",
+ isChecked = true,
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ onCheckedChange = listener,
+ ),
+ SimpleTestBrowserCompoundButton(
+ "Hello",
+ initialState = { true },
+ listener = listener,
+ ).asCandidate(testContext),
+ )
+ }
+
+ @Test
+ fun `GIVEN the View is attached to Window WHEN bind is called THEN the layout direction is not updated`() {
+ val item = SimpleTestBrowserCompoundButton("Hello") {}
+ val menu = mock(BrowserMenu::class.java)
+ val view: SwitchCompat = mock()
+ doReturn(true).`when`(view).isAttachedToWindow
+ doReturn(mock<ViewTreeObserver>()).`when`(view).viewTreeObserver
+
+ item.bind(menu, view)
+
+ verify(view, never()).layoutDirection = ArgumentMatchers.anyInt()
+ }
+
+ @Test
+ fun `GIVEN the View is not attached to Window WHEN bind is called THEN the layout direction is changed to locale`() {
+ val item = SimpleTestBrowserCompoundButton("Hello") {}
+ val menu = mock(BrowserMenu::class.java)
+ val view: SwitchCompat = mock()
+ doReturn(false).`when`(view).isAttachedToWindow
+ doReturn(mock<ViewTreeObserver>()).`when`(view).viewTreeObserver
+
+ item.bind(menu, view)
+
+ verify(view).layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+ }
+
+ @Test
+ fun `GIVEN the View is not attached to Window WHEN bind is called THEN the a viewTreeObserver for preDraw is set`() {
+ val item = SimpleTestBrowserCompoundButton("Hello") {}
+ val menu = mock(BrowserMenu::class.java)
+ val view: SwitchCompat = mock()
+ val viewTreeObserver: ViewTreeObserver = mock()
+ doReturn(false).`when`(view).isAttachedToWindow
+ doReturn(viewTreeObserver).`when`(view).viewTreeObserver
+
+ item.bind(menu, view)
+
+ verify(viewTreeObserver).addOnPreDrawListener(any())
+ }
+
+ @Test
+ fun `GIVEN a view with updated layout direction WHEN it is about to be drawn THEN the layout direction reset`() {
+ val item = SimpleTestBrowserCompoundButton("Hello") {}
+ val menu = mock(BrowserMenu::class.java)
+ val view: SwitchCompat = mock()
+ val viewTreeObserver: ViewTreeObserver = mock()
+ doReturn(false).`when`(view).isAttachedToWindow
+ doReturn(viewTreeObserver).`when`(view).viewTreeObserver
+ val captor = argumentCaptor<ViewTreeObserver.OnPreDrawListener>()
+
+ item.bind(menu, view)
+ verify(viewTreeObserver).addOnPreDrawListener(captor.capture())
+
+ captor.value.onPreDraw()
+ verify(viewTreeObserver).removeOnPreDrawListener(captor.value)
+ verify(view).layoutDirection = View.LAYOUT_DIRECTION_INHERIT
+ }
+
+ class SimpleTestBrowserCompoundButton(
+ label: String,
+ initialState: () -> Boolean = { false },
+ listener: (Boolean) -> Unit,
+ ) : BrowserMenuCompoundButton(label, false, false, initialState, listener) {
+ override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_simple
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuDividerTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuDividerTest.kt
new file mode 100644
index 0000000000..1a3979dd1e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuDividerTest.kt
@@ -0,0 +1,47 @@
+/* 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.browser.menu.item
+
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class BrowserMenuDividerTest {
+
+ @Test
+ fun `browser divider uses correct layout`() {
+ val item = BrowserMenuDivider()
+ assertEquals(R.layout.mozac_browser_menu_item_divider, item.getLayoutResource())
+ }
+
+ @Test
+ fun `hitting default methods`() {
+ val item = BrowserMenuDivider()
+ assertTrue(item.visible())
+ item.bind(mock(), mock())
+ item.invalidate(mock())
+ }
+
+ @Test
+ fun `menu divider can be converted to candidate`() {
+ assertEquals(
+ DividerMenuCandidate(),
+ BrowserMenuDivider().asCandidate(mock()),
+ )
+
+ assertEquals(
+ DividerMenuCandidate(
+ containerStyle = ContainerStyle(isVisible = true),
+ ),
+ BrowserMenuDivider().apply {
+ visible = { true }
+ }.asCandidate(mock()),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItemTest.kt
new file mode 100644
index 0000000000..49bd310a00
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItemTest.kt
@@ -0,0 +1,334 @@
+/* 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.browser.menu.item
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat.getColor
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.robolectric.Shadows
+import mozilla.components.ui.colors.R as colorsR
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuHighlightableItemTest {
+
+ private val colorId = colorsR.color.photonRed50
+
+ @Suppress("Deprecation")
+ @Test
+ fun `browser menu highlightable item should be inflated`() {
+ var onClickWasPress = false
+ val item = BrowserMenuHighlightableItem(
+ "label",
+ imageResource = android.R.drawable.ic_menu_report_image,
+ iconTintColorResource = android.R.color.black,
+ textColorResource = android.R.color.black,
+ highlight = BrowserMenuHighlightableItem.Highlight(
+ endImageResource = android.R.drawable.ic_menu_report_image,
+ backgroundResource = colorId,
+ colorResource = colorId,
+ ),
+ ) {
+ onClickWasPress = true
+ }
+
+ val view = inflate(item)
+
+ view.performClick()
+ assertTrue(onClickWasPress)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `browser menu highlightable item should properly handle classic highlighting`() {
+ var shouldHighlight = false
+ val item = BrowserMenuHighlightableItem(
+ label = "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ iconTintColorResource = android.R.color.black,
+ textColorResource = android.R.color.black,
+ highlight = BrowserMenuHighlightableItem.Highlight(
+ startImageResource = android.R.drawable.ic_menu_camera,
+ endImageResource = android.R.drawable.ic_menu_add,
+ backgroundResource = colorId,
+ colorResource = colorId,
+ ),
+ isHighlighted = { shouldHighlight },
+ )
+
+ val view = inflate(item)
+
+ assertEquals("label", view.textView.text)
+
+ // Highlight should not exist before set
+ val oldDrawable = view.startImageView.drawable
+ assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(oldDrawable).createdFromResId)
+ assertFalse(view.endImageView.isVisible)
+
+ shouldHighlight = true
+ item.invalidate(view)
+
+ // Highlight should now exist
+ assertTrue(view.endImageView.isVisible)
+ assertNotEquals(oldDrawable, view.startImageView.drawable)
+ assertEquals(android.R.drawable.ic_menu_camera, Shadows.shadowOf(view.startImageView.drawable).createdFromResId)
+ assertEquals(android.R.drawable.ic_menu_add, Shadows.shadowOf(view.endImageView.drawable).createdFromResId)
+ assertNotNull(view.endImageView.imageTintList)
+ assertEquals(colorId, Shadows.shadowOf(view.background).createdFromResId)
+ }
+
+ @Test
+ fun `browser menu highlightable item should properly handle high priority highlighting`() {
+ var shouldHighlight = false
+ val item = BrowserMenuHighlightableItem(
+ label = "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ iconTintColorResource = android.R.color.black,
+ textColorResource = android.R.color.black,
+ highlight = BrowserMenuHighlight.HighPriority(
+ endImageResource = android.R.drawable.ic_menu_add,
+ backgroundTint = Color.RED,
+ label = "highlight",
+ ),
+ isHighlighted = { shouldHighlight },
+ )
+
+ val view = inflate(item)
+
+ assertEquals("label", view.textView.text)
+ assertEquals("highlight", view.highlightedTextView.text)
+
+ // Highlight should not exist before set
+ assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(view.startImageView.drawable).createdFromResId)
+ assertFalse(view.highlightedTextView.isVisible)
+ assertFalse(view.endImageView.isVisible)
+
+ shouldHighlight = true
+ item.invalidate(view)
+
+ // Highlight should now exist
+ assertTrue(view.highlightedTextView.isVisible)
+ assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(view.startImageView.drawable).createdFromResId)
+ assertEquals(android.R.drawable.ic_menu_add, Shadows.shadowOf(view.endImageView.drawable).createdFromResId)
+ assertNotNull(view.endImageView.imageTintList)
+ assertTrue(view.endImageView.isVisible)
+ }
+
+ @Test
+ fun `browser menu highlightable item should properly handle low priority highlighting`() {
+ var shouldHighlight = false
+ val item = BrowserMenuHighlightableItem(
+ label = "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ iconTintColorResource = android.R.color.black,
+ textColorResource = android.R.color.black,
+ highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = Color.RED,
+ label = "highlight",
+ ),
+ isHighlighted = { shouldHighlight },
+ )
+
+ val view = inflate(item)
+
+ assertEquals("label", view.textView.text)
+ assertEquals("highlight", view.highlightedTextView.text)
+
+ val startImageView = view.findViewById<AppCompatImageView>(R.id.image)
+ val highlightImageView = view.findViewById<AppCompatImageView>(R.id.end_image)
+
+ // Highlight should not exist before set
+ assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(startImageView.drawable).createdFromResId)
+ assertFalse(view.highlightedTextView.isVisible)
+ assertFalse(highlightImageView.isVisible)
+
+ shouldHighlight = true
+ item.invalidate(view)
+
+ // Highlight should now exist
+ assertTrue(view.findViewById<TextView>(R.id.highlight_text).isVisible)
+ assertFalse(view.findViewById<AppCompatImageView>(R.id.end_image).isVisible)
+ assertNull(view.background)
+ }
+
+ @Test
+ fun `browser menu highlightable item with with no iconTintColorResource must not have a tinted icon`() {
+ val item = BrowserMenuHighlightableItem(
+ "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ highlight = BrowserMenuHighlight.HighPriority(
+ endImageResource = android.R.drawable.ic_menu_report_image,
+ backgroundTint = Color.RED,
+ ),
+ )
+
+ val view = inflate(item)
+
+ assertEquals("label", view.textView.text)
+ assertNull(view.startImageView.drawable!!.colorFilter)
+ assertNull(view.endImageView.imageTintList)
+ }
+
+ @Test
+ fun `bind highlightable item with with default high priority`() {
+ val item = BrowserMenuHighlightableItem(
+ "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ highlight = BrowserMenuHighlight.HighPriority(
+ backgroundTint = Color.RED,
+ ),
+ )
+
+ val view = inflate(item)
+
+ assertEquals("label", view.textView.text)
+ assertEquals("label", view.highlightedTextView.text)
+ assertTrue(view.highlightedTextView.isVisible)
+ assertTrue(view.background is ColorDrawable)
+ assertNull(view.endImageView.drawable)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `browser menu highlightable item with with no highlight must not have highlightImageView visible`() {
+ val item = BrowserMenuHighlightableItem(
+ "label",
+ android.R.drawable.ic_menu_report_image,
+ highlight = null,
+ )
+
+ val view = inflate(item)
+ val endImageView = view.findViewById<AppCompatImageView>(R.id.end_image)
+ val textView = view.findViewById<TextView>(R.id.text)
+ assertEquals("label", textView.text)
+ assertEquals(endImageView.visibility, View.GONE)
+ }
+
+ @Test
+ fun `menu item can be converted to candidate`() {
+ val listener = {}
+
+ var shouldHighlight = false
+ val highPriorityItem = BrowserMenuHighlightableItem(
+ label = "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ iconTintColorResource = android.R.color.black,
+ textColorResource = android.R.color.black,
+ highlight = BrowserMenuHighlight.HighPriority(
+ endImageResource = android.R.drawable.ic_menu_add,
+ backgroundTint = Color.RED,
+ label = "highlight",
+ ),
+ isHighlighted = { shouldHighlight },
+ listener = listener,
+ )
+
+ assertEquals(
+ TextMenuCandidate(
+ "label",
+ start = DrawableMenuIcon(
+ null,
+ tint = getColor(testContext, android.R.color.black),
+ ),
+ textStyle = TextStyle(
+ color = getColor(testContext, android.R.color.black),
+ ),
+ onClick = listener,
+ ),
+ highPriorityItem.asCandidate(testContext).removeDrawables(),
+ )
+
+ shouldHighlight = true
+ assertEquals(
+ TextMenuCandidate(
+ "highlight",
+ start = DrawableMenuIcon(
+ null,
+ tint = getColor(testContext, android.R.color.black),
+ ),
+ end = DrawableMenuIcon(null),
+ textStyle = TextStyle(
+ color = getColor(testContext, android.R.color.black),
+ ),
+ effect = HighPriorityHighlightEffect(
+ backgroundTint = Color.RED,
+ ),
+ onClick = listener,
+ ),
+ highPriorityItem.asCandidate(testContext).removeDrawables(),
+ )
+
+ assertEquals(
+ TextMenuCandidate(
+ "highlight",
+ start = DrawableMenuIcon(
+ null,
+ tint = getColor(testContext, android.R.color.black),
+ effect = LowPriorityHighlightEffect(
+ notificationTint = Color.RED,
+ ),
+ ),
+ textStyle = TextStyle(
+ color = getColor(testContext, android.R.color.black),
+ ),
+ onClick = listener,
+ ),
+ BrowserMenuHighlightableItem(
+ label = "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ iconTintColorResource = android.R.color.black,
+ textColorResource = android.R.color.black,
+ highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = Color.RED,
+ label = "highlight",
+ ),
+ isHighlighted = { true },
+ listener = listener,
+ ).asCandidate(testContext).removeDrawables(),
+ )
+ }
+
+ private fun inflate(item: BrowserMenuHighlightableItem): ConstraintLayout {
+ val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null)
+ val mockMenu = mock(BrowserMenu::class.java)
+ item.bind(mockMenu, view)
+ return view as ConstraintLayout
+ }
+
+ private val ConstraintLayout.startImageView: ImageView get() = findViewById(R.id.image)
+ private val ConstraintLayout.endImageView: ImageView get() = findViewById(R.id.end_image)
+ private val ConstraintLayout.textView: TextView get() = findViewById(R.id.text)
+ private val ConstraintLayout.highlightedTextView: TextView get() = findViewById(R.id.highlight_text)
+
+ private fun TextMenuCandidate.removeDrawables() = copy(
+ start = (start as? DrawableMenuIcon)?.copy(drawable = null),
+ end = (end as? DrawableMenuIcon)?.copy(drawable = null),
+ )
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitchTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitchTest.kt
new file mode 100644
index 0000000000..5df2dab9a8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitchTest.kt
@@ -0,0 +1,124 @@
+/* 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.browser.menu.item
+
+import android.graphics.Color
+import android.view.LayoutInflater
+import android.widget.ImageView
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.SwitchCompat
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.R
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.robolectric.Shadows
+import mozilla.components.ui.colors.R as colorsR
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuHighlightableSwitchTest {
+
+ @Test
+ fun `menu item uses correct layout`() {
+ val item = BrowserMenuHighlightableSwitch(
+ label = "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = Color.RED,
+ ),
+ ) {}
+
+ assertEquals(R.layout.mozac_browser_menu_highlightable_switch, item.getLayoutResource())
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `browser menu highlightable item should be inflated`() {
+ var onClickWasPress = false
+ val item = BrowserMenuHighlightableSwitch(
+ label = "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = Color.RED,
+ ),
+ ) {
+ onClickWasPress = true
+ }
+
+ val view = inflate(item)
+
+ view.switch.performClick()
+ assertTrue(onClickWasPress)
+ }
+
+ @Test
+ fun `browser menu highlightable item should properly handle low priority highlighting`() {
+ var shouldHighlight = false
+ val item = BrowserMenuHighlightableSwitch(
+ label = "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ iconTintColorResource = android.R.color.black,
+ textColorResource = android.R.color.black,
+ highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = colorsR.color.photonRed50,
+ label = "highlight",
+ ),
+ isHighlighted = { shouldHighlight },
+ ) {}
+
+ val view = inflate(item)
+
+ assertEquals("label", view.switch.text)
+
+ val startImageView = view.findViewById<AppCompatImageView>(R.id.image)
+
+ // Highlight should not exist before set
+ assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(startImageView.drawable).createdFromResId)
+ assertFalse(view.notificationDot.isVisible)
+
+ shouldHighlight = true
+ item.invalidate(view)
+
+ // Highlight should now exist
+ assertEquals("highlight", view.switch.text)
+ assertTrue(view.notificationDot.isVisible)
+ }
+
+ @Test
+ fun `browser menu highlightable item with with no iconTintColorResource must not have a tinted icon`() {
+ val item = BrowserMenuHighlightableSwitch(
+ "label",
+ startImageResource = android.R.drawable.ic_menu_report_image,
+ highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = Color.RED,
+ ),
+ ) {}
+
+ val view = inflate(item)
+
+ assertEquals("label", view.switch.text)
+ assertNull(view.startImageView.drawable!!.colorFilter)
+ }
+
+ private fun inflate(item: BrowserMenuHighlightableSwitch): ConstraintLayout {
+ val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null)
+ val mockMenu = mock(BrowserMenu::class.java)
+ item.bind(mockMenu, view)
+ return view as ConstraintLayout
+ }
+
+ private val ConstraintLayout.startImageView: ImageView get() = findViewById(R.id.image)
+ private val ConstraintLayout.notificationDot: ImageView get() = findViewById(R.id.notification_dot)
+ private val ConstraintLayout.switch: SwitchCompat get() = findViewById(R.id.switch_widget)
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitchTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitchTest.kt
new file mode 100644
index 0000000000..ae3c82aa7b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitchTest.kt
@@ -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/. */
+
+package mozilla.components.browser.menu.item
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuImageSwitchTest {
+ private lateinit var menuItem: BrowserMenuImageSwitch
+ private val label = "test"
+ private val icon = android.R.drawable.ic_menu_call
+
+ @Before
+ fun setup() {
+ menuItem = BrowserMenuImageSwitch(icon, label) {}
+ }
+
+ @Test
+ fun `menu item uses correct layout`() {
+ assertEquals(R.layout.mozac_browser_menu_item_image_switch, menuItem.getLayoutResource())
+ }
+
+ @Test
+ fun `menu item has correct label`() {
+ assertEquals(label, menuItem.label)
+ }
+
+ @Test
+ fun `menu item has correct icon`() {
+ assertEquals(icon, menuItem.imageResource)
+ }
+
+ @Test
+ fun `menu switch can be converted to candidate`() {
+ val listener = { _: Boolean -> }
+
+ assertEquals(
+ CompoundMenuCandidate(
+ label,
+ isChecked = false,
+ start = DrawableMenuIcon(null),
+ end = CompoundMenuCandidate.ButtonType.SWITCH,
+ onCheckedChange = listener,
+ ),
+ BrowserMenuImageSwitch(icon, label, listener = listener).asCandidate(testContext).run {
+ copy(start = (start as DrawableMenuIcon?)?.copy(drawable = null))
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButtonTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButtonTest.kt
new file mode 100644
index 0000000000..8969fcd94e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButtonTest.kt
@@ -0,0 +1,204 @@
+/* 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.browser.menu.item
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.CheckBox
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.widget.ImageViewCompat.getImageTintList
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuImageTextCheckboxButtonTest {
+
+ private lateinit var item: BrowserMenuImageTextCheckboxButton
+ private lateinit var secondaryItem: BrowserMenuImageTextCheckboxButton
+
+ private val label = "Bookmarks"
+ private val imageResource = android.R.drawable.ic_menu_report_image
+ private val iconTintColorResource = android.R.color.holo_red_dark
+
+ private val tintColorResource = android.R.color.holo_purple
+ private val labelListener = { }
+ private val primaryLabel = "Add"
+ private val secondaryLabel = "Edit"
+ private val primaryStateIconResource = android.R.drawable.star_big_off
+ private val secondaryStateIconResource = android.R.drawable.star_big_on
+ private val isInPrimaryState: () -> Boolean = { true }
+ private val onCheckedChangedListener: (Boolean) -> Unit = { }
+
+ @Before
+ fun setUp() {
+ item = spy(
+ BrowserMenuImageTextCheckboxButton(
+ imageResource = imageResource,
+ label = label,
+ iconTintColorResource = iconTintColorResource,
+ textColorResource = tintColorResource,
+ labelListener = labelListener,
+ primaryLabel = primaryLabel,
+ secondaryLabel = secondaryLabel,
+ primaryStateIconResource = primaryStateIconResource,
+ secondaryStateIconResource = secondaryStateIconResource,
+ tintColorResource = tintColorResource,
+ isInPrimaryState = isInPrimaryState,
+ onCheckedChangedListener = onCheckedChangedListener,
+ ),
+ )
+
+ secondaryItem = spy(
+ BrowserMenuImageTextCheckboxButton(
+ imageResource = imageResource,
+ label = label,
+ iconTintColorResource = iconTintColorResource,
+ textColorResource = tintColorResource,
+ labelListener = labelListener,
+ primaryLabel = primaryLabel,
+ secondaryLabel = secondaryLabel,
+ primaryStateIconResource = primaryStateIconResource,
+ secondaryStateIconResource = secondaryStateIconResource,
+ tintColorResource = tintColorResource,
+ isInPrimaryState = { false },
+ onCheckedChangedListener = onCheckedChangedListener,
+ ),
+ )
+ }
+
+ @Test
+ fun `layout resource can be inflated`() {
+ val view = LayoutInflater.from(testContext)
+ .inflate(item.getLayoutResource(), null)
+
+ assertNotNull(view)
+ }
+
+ @Test
+ fun `item uses correct layout`() {
+ assertEquals(R.layout.mozac_browser_menu_item_image_text_checkbox_button, item.getLayoutResource())
+ }
+
+ @Test
+ fun `item is visible by default`() {
+ assertTrue(item.visible())
+ }
+
+ @Test
+ fun `initialState is invoked on bind and properly sets label`() {
+ val menu = mock(BrowserMenu::class.java)
+ val view = LayoutInflater.from(testContext)
+ .inflate(item.getLayoutResource(), null)
+
+ item.bind(menu, view)
+ val checkBox = view.findViewById<CheckBox>(R.id.checkbox)
+ var expectedLabel = if (item.isInPrimaryState()) primaryLabel else secondaryLabel
+
+ assertEquals(expectedLabel, primaryLabel)
+ assertEquals(expectedLabel, checkBox.text)
+
+ secondaryItem.bind(menu, view)
+ val secondaryCheckBox = view.findViewById<CheckBox>(R.id.checkbox)
+ expectedLabel = if (secondaryItem.isInPrimaryState()) primaryLabel else secondaryLabel
+
+ assertEquals(expectedLabel, secondaryLabel)
+ assertEquals(expectedLabel, secondaryCheckBox.text)
+ }
+
+ @Test
+ fun `item has the correct text color`() {
+ val view = inflate(item)
+
+ val textView = view.findViewById<TextView>(R.id.text)
+ val expectedColour = ContextCompat.getColor(view.context, item.textColorResource)
+
+ assertEquals(textView.text, label)
+ assertEquals(expectedColour, textView.currentTextColor)
+ }
+
+ @Test
+ fun `item has icon with correct resource and tint`() {
+ val view = inflate(item)
+
+ val icon = view.findViewById<ImageView>(R.id.image)
+ val expectedColour = ContextCompat.getColor(view.context, item.iconTintColorResource)
+
+ assertNotNull(icon.drawable)
+ assertEquals(getImageTintList(icon)?.defaultColor, expectedColour)
+ }
+
+ @Test
+ fun `item accessibilityRegion has label text as content description`() {
+ val view = inflate(item)
+
+ val accessibilityRegion = view.findViewById<View>(R.id.accessibilityRegion)
+
+ assertEquals(label, accessibilityRegion.contentDescription)
+ }
+
+ @Test
+ fun `item accessibilityRegion is clickable`() {
+ val view = inflate(item)
+
+ val accessibilityRegion = view.findViewById<View>(R.id.accessibilityRegion)
+
+ assertTrue(accessibilityRegion.isClickable)
+ assertTrue(accessibilityRegion.callOnClick())
+ }
+
+ @Test
+ fun `clicking item dismisses menu`() {
+ val view = inflate(item)
+ val menu = mock(BrowserMenu::class.java)
+
+ item.bind(menu, view)
+ view.callOnClick()
+
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `clicking checkbox dismisses menu`() {
+ val view = inflate(item)
+ val menu = mock(BrowserMenu::class.java)
+
+ item.bind(menu, view)
+ val checkBox = view.findViewById<CheckBox>(R.id.checkbox)
+
+ checkBox.performClick()
+
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `item checkbox has text with correct tint`() {
+ val view = inflate(item)
+
+ val checkbox = view.findViewById<CheckBox>(R.id.checkbox)
+ val expectedColour = ContextCompat.getColor(view.context, tintColorResource)
+
+ assertEquals(expectedColour, checkbox.currentTextColor)
+ }
+
+ private fun inflate(item: BrowserMenuImageText): View {
+ val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null)
+ val mockMenu = mock(BrowserMenu::class.java)
+ item.bind(mockMenu, view)
+ return view
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextTest.kt
new file mode 100644
index 0000000000..951a68e2df
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextTest.kt
@@ -0,0 +1,131 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat.getColor
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuImageTextTest {
+
+ private val context: Context get() = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun `browser menu ImageText should be inflated`() {
+ var onClickWasPress = false
+ val item = BrowserMenuImageText(
+ "label",
+ android.R.drawable.ic_menu_report_image,
+ android.R.color.black,
+ ) {
+ onClickWasPress = true
+ }
+
+ val view = inflate(item)
+
+ view.performClick()
+ assertTrue(onClickWasPress)
+ }
+
+ @Test
+ fun `browser menu ImageText should have the right text, image, and iconTintColorResource`() {
+ val item = BrowserMenuImageText(
+ "label",
+ android.R.drawable.ic_menu_report_image,
+ android.R.color.black,
+ ) {
+ }
+
+ val view = inflate(item)
+
+ val textView = view.findViewById<TextView>(R.id.text)
+ assertEquals(textView.text, "label")
+
+ val imageView = view.findViewById<AppCompatImageView>(R.id.image)
+
+ assertNotNull(imageView.drawable)
+
+ assertNotNull(imageView.imageTintList)
+ }
+
+ @Test
+ fun `browser menu ImageText with with no iconTintColorResource must not have an imageTintList`() {
+ val item = BrowserMenuImageText(
+ "label",
+ android.R.drawable.ic_menu_report_image,
+ )
+
+ val view = inflate(item)
+
+ val imageView = view.findViewById<AppCompatImageView>(R.id.image)
+
+ assertNull(imageView.imageTintList)
+ }
+
+ @Test
+ fun `menu image text item can be converted to candidate`() {
+ val listener = {}
+
+ assertEquals(
+ TextMenuCandidate(
+ "label",
+ start = DrawableMenuIcon(null),
+ onClick = listener,
+ ),
+ BrowserMenuImageText(
+ "label",
+ android.R.drawable.ic_menu_report_image,
+ listener = listener,
+ ).asCandidate(context).let {
+ val text = it as TextMenuCandidate
+ text.copy(start = (text.start as? DrawableMenuIcon)?.copy(drawable = null))
+ },
+ )
+
+ assertEquals(
+ TextMenuCandidate(
+ "label",
+ start = DrawableMenuIcon(
+ null,
+ tint = getColor(context, android.R.color.black),
+ ),
+ onClick = listener,
+ ),
+ BrowserMenuImageText(
+ "label",
+ android.R.drawable.ic_menu_report_image,
+ android.R.color.black,
+ listener = listener,
+ ).asCandidate(context).let {
+ val text = it as TextMenuCandidate
+ text.copy(start = (text.start as? DrawableMenuIcon)?.copy(drawable = null))
+ },
+ )
+ }
+
+ private fun inflate(item: BrowserMenuImageText): View {
+ val view = LayoutInflater.from(context).inflate(item.getLayoutResource(), null)
+ val mockMenu = mock(BrowserMenu::class.java)
+ item.bind(mockMenu, view)
+ return view
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbarTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbarTest.kt
new file mode 100644
index 0000000000..a5f729a321
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbarTest.kt
@@ -0,0 +1,439 @@
+/* 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.browser.menu.item
+
+import android.view.LayoutInflater
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.appcompat.R
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat.getColor
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuItemToolbarTest {
+
+ @Test
+ fun `toolbar is visible by default`() {
+ val toolbar = BrowserMenuItemToolbar(emptyList())
+
+ assertTrue(toolbar.visible())
+ }
+
+ @Test
+ fun `layout resource can be inflated`() {
+ val toolbar = BrowserMenuItemToolbar(emptyList())
+
+ val view = LayoutInflater.from(testContext)
+ .inflate(toolbar.getLayoutResource(), null)
+
+ assertNotNull(view)
+ }
+
+ @Test
+ fun `empty toolbar does not add anything to view group`() {
+ val layout = LinearLayout(testContext)
+
+ val menu = mock(BrowserMenu::class.java)
+
+ val toolbar = BrowserMenuItemToolbar(emptyList())
+ toolbar.bind(menu, layout)
+
+ assertEquals(0, layout.childCount)
+ }
+
+ @Test
+ fun `toolbar removes previously existing child views from view group`() {
+ val layout = LinearLayout(testContext)
+ layout.addView(TextView(testContext))
+ layout.addView(TextView(testContext))
+
+ assertEquals(2, layout.childCount)
+
+ val menu = mock(BrowserMenu::class.java)
+
+ val toolbar = BrowserMenuItemToolbar(emptyList())
+ toolbar.bind(menu, layout)
+
+ assertEquals(0, layout.childCount)
+ }
+
+ @Test
+ fun `items are added as ImageButton to view group`() {
+ val buttons = listOf(
+ BrowserMenuItemToolbar.Button(
+ R.drawable.abc_ic_ab_back_material,
+ "Button01",
+ ) {},
+ BrowserMenuItemToolbar.Button(
+ R.drawable.abc_ic_ab_back_material,
+ "Button02",
+ ) {},
+ BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = R.drawable.abc_ic_go_search_api_material,
+ primaryContentDescription = "TwoStatePrimary",
+ secondaryImageResource = R.drawable.abc_ic_clear_material,
+ secondaryContentDescription = "TwoStateSecondary",
+ ) {},
+ )
+
+ val menu = mock(BrowserMenu::class.java)
+ val layout = LinearLayout(testContext)
+
+ val toolbar = BrowserMenuItemToolbar(buttons)
+ toolbar.bind(menu, layout)
+
+ assertEquals(3, layout.childCount)
+
+ val child1 = layout.getChildAt(0)
+ val child2 = layout.getChildAt(1)
+ val child3 = layout.getChildAt(2)
+
+ assertTrue(child1 is ImageButton)
+ assertTrue(child2 is ImageButton)
+ assertTrue(child3 is ImageButton)
+
+ assertEquals("Button01", child1.contentDescription)
+ assertEquals("Button02", child2.contentDescription)
+ assertEquals("TwoStatePrimary", child3.contentDescription)
+ }
+
+ @Test
+ fun `Disabled Button is not enabled`() {
+ val buttons = listOf(
+ BrowserMenuItemToolbar.Button(
+ imageResource = R.drawable.abc_ic_go_search_api_material,
+ contentDescription = "Button01",
+ iconTintColorResource = R.color.accent_material_light,
+ isEnabled = { false },
+ ) {},
+ )
+
+ val menu = mock(BrowserMenu::class.java)
+ val layout = LinearLayout(testContext)
+
+ val toolbar = BrowserMenuItemToolbar(buttons)
+ toolbar.bind(menu, layout)
+
+ val child1 = layout.getChildAt(0)
+ assertEquals("Button01", child1.contentDescription)
+ assertFalse(child1.isEnabled)
+ }
+
+ @Test
+ fun `Button redraws when invalidate is triggered`() {
+ var isEnabled = false
+ val buttons = listOf(
+ BrowserMenuItemToolbar.Button(
+ imageResource = R.drawable.abc_ic_go_search_api_material,
+ contentDescription = "Button01",
+ isEnabled = { isEnabled },
+ ) {},
+ )
+
+ val menu = mock(BrowserMenu::class.java)
+ val layout = LinearLayout(testContext)
+
+ val toolbar = BrowserMenuItemToolbar(buttons)
+ toolbar.bind(menu, layout)
+
+ val child1 = layout.getChildAt(0)
+ assertEquals("Button01", child1.contentDescription)
+ assertFalse(child1.isEnabled)
+
+ isEnabled = true
+ toolbar.invalidate(layout)
+ assertTrue(child1.isEnabled)
+ }
+
+ @Test
+ fun `Disabled TwoState Button in secondary state is disabled`() {
+ val buttons = listOf(
+ BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = R.drawable.abc_ic_go_search_api_material,
+ primaryContentDescription = "TwoStateEnabled",
+ secondaryImageResource = R.drawable.abc_ic_clear_material,
+ secondaryContentDescription = "TwoStateDisabled",
+ isInPrimaryState = { false },
+ disableInSecondaryState = true,
+ ) {},
+ )
+
+ val menu = mock(BrowserMenu::class.java)
+ val layout = LinearLayout(testContext)
+
+ val toolbar = BrowserMenuItemToolbar(buttons)
+ toolbar.bind(menu, layout)
+
+ val child1 = layout.getChildAt(0)
+ assertEquals("TwoStateDisabled", child1.contentDescription)
+ assertFalse(child1.isEnabled)
+ }
+
+ @Test
+ fun `TwoStateButton has primary and secondary state invoked`() {
+ val primaryResource = R.drawable.abc_ic_go_search_api_material
+ val secondaryResource = R.drawable.abc_ic_clear_material
+
+ var reloadPageAction = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = primaryResource,
+ primaryContentDescription = "primary",
+ primaryImageTintResource = R.color.accent_material_dark,
+ secondaryImageResource = secondaryResource,
+ secondaryContentDescription = "secondary",
+ secondaryImageTintResource = R.color.accent_material_light,
+ ) {}
+ assertTrue(reloadPageAction.isInPrimaryState.invoke())
+
+ reloadPageAction = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = primaryResource,
+ primaryContentDescription = "primary",
+ primaryImageTintResource = R.color.accent_material_dark,
+ secondaryImageResource = secondaryResource,
+ secondaryContentDescription = "secondary",
+ secondaryImageTintResource = R.color.accent_material_light,
+ isInPrimaryState = { false },
+ ) {}
+ assertFalse(reloadPageAction.isInPrimaryState.invoke())
+ }
+
+ @Test
+ fun `TwoStateButton redraws when invalidate is triggered`() {
+ var isInPrimaryState = true
+ val buttons = listOf(
+ BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = R.drawable.abc_ic_go_search_api_material,
+ primaryContentDescription = "TwoStateEnabled",
+ secondaryImageResource = R.drawable.abc_ic_clear_material,
+ secondaryContentDescription = "TwoStateDisabled",
+ isInPrimaryState = { isInPrimaryState },
+ ) {},
+ )
+
+ val menu = mock(BrowserMenu::class.java)
+ val layout = LinearLayout(testContext)
+
+ val toolbar = BrowserMenuItemToolbar(buttons)
+ toolbar.bind(menu, layout)
+
+ val child1 = layout.getChildAt(0)
+ assertEquals("TwoStateEnabled", child1.contentDescription)
+
+ isInPrimaryState = false
+ toolbar.invalidate(layout)
+ assertEquals("TwoStateDisabled", child1.contentDescription)
+ }
+
+ @Test
+ fun `TwoStateButton doesn't redraw if state hasn't changed`() {
+ val isInPrimaryState = true
+ val button = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = R.drawable.abc_ic_go_search_api_material,
+ primaryContentDescription = "TwoStateEnabled",
+ secondaryImageResource = R.drawable.abc_ic_clear_material,
+ secondaryContentDescription = "TwoStateDisabled",
+ isInPrimaryState = { isInPrimaryState },
+ disableInSecondaryState = true,
+ ) {}
+
+ val view = mock(AppCompatImageView::class.java)
+
+ button.bind(view)
+ verify(view).contentDescription = "TwoStateEnabled"
+
+ reset(view)
+
+ button.invalidate(view)
+ verify(view, never()).contentDescription = "TwoStateEnabled"
+ }
+
+ @Test
+ fun `clicking item view invokes callback and dismisses menu`() {
+ var callbackInvoked = false
+
+ val button = BrowserMenuItemToolbar.Button(
+ R.drawable.abc_ic_ab_back_material,
+ "Test",
+ ) {
+ callbackInvoked = true
+ }
+
+ assertFalse(callbackInvoked)
+
+ val menu = mock(BrowserMenu::class.java)
+ val layout = LinearLayout(testContext)
+
+ val toolbar = BrowserMenuItemToolbar(listOf(button))
+ toolbar.bind(menu, layout)
+
+ assertEquals(1, layout.childCount)
+
+ val view = layout.getChildAt(0)
+
+ assertFalse(callbackInvoked)
+ verify(menu, never()).dismiss()
+
+ view.performClick()
+
+ assertTrue(callbackInvoked)
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `long clicking item view invokes callback and dismisses menu`() {
+ var callbackInvoked = false
+
+ val button = BrowserMenuItemToolbar.Button(
+ R.drawable.abc_ic_ab_back_material,
+ "Test",
+ longClickListener = {
+ callbackInvoked = true
+ },
+ ) {}
+
+ assertFalse(callbackInvoked)
+
+ val menu = mock(BrowserMenu::class.java)
+ val layout = LinearLayout(testContext)
+
+ val toolbar = BrowserMenuItemToolbar(listOf(button))
+ toolbar.bind(menu, layout)
+
+ assertEquals(1, layout.childCount)
+
+ val view = layout.getChildAt(0)
+
+ assertFalse(callbackInvoked)
+ verify(menu, never()).dismiss()
+
+ view.performLongClick()
+
+ assertTrue(callbackInvoked)
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `toolbar can be converted to candidate`() {
+ val listener = {}
+
+ assertEquals(
+ RowMenuCandidate(emptyList()),
+ BrowserMenuItemToolbar(emptyList()).asCandidate(testContext),
+ )
+
+ var isEnabled = false
+ var isInPrimaryState = true
+ val toolbarWithTwoState = BrowserMenuItemToolbar(
+ listOf(
+ BrowserMenuItemToolbar.Button(
+ R.drawable.abc_ic_ab_back_material,
+ "Button01",
+ isEnabled = { isEnabled },
+ listener = listener,
+ ),
+ BrowserMenuItemToolbar.Button(
+ R.drawable.abc_ic_ab_back_material,
+ "Button02",
+ iconTintColorResource = R.color.accent_material_light,
+ listener = listener,
+ ),
+ BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = R.drawable.abc_ic_go_search_api_material,
+ primaryContentDescription = "TwoStatePrimary",
+ secondaryImageResource = R.drawable.abc_ic_clear_material,
+ secondaryContentDescription = "TwoStateSecondary",
+ isInPrimaryState = { isInPrimaryState },
+ listener = listener,
+ ),
+ ),
+ )
+
+ assertEquals(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate(
+ "Button01",
+ icon = DrawableMenuIcon(null),
+ containerStyle = ContainerStyle(isEnabled = false),
+ onClick = listener,
+ ),
+ SmallMenuCandidate(
+ "Button02",
+ icon = DrawableMenuIcon(
+ null,
+ tint = getColor(testContext, R.color.accent_material_light),
+ ),
+ onClick = listener,
+ ),
+ SmallMenuCandidate(
+ "TwoStatePrimary",
+ icon = DrawableMenuIcon(null),
+ onClick = listener,
+ ),
+ ),
+ ),
+ toolbarWithTwoState.asCandidate(testContext).run {
+ copy(
+ items = items.map {
+ it.copy(icon = it.icon.copy(drawable = null))
+ },
+ )
+ },
+ )
+
+ isEnabled = true
+ isInPrimaryState = false
+
+ assertEquals(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate(
+ "Button01",
+ icon = DrawableMenuIcon(null),
+ containerStyle = ContainerStyle(isEnabled = true),
+ onClick = listener,
+ ),
+ SmallMenuCandidate(
+ "Button02",
+ icon = DrawableMenuIcon(
+ null,
+ tint = getColor(testContext, R.color.accent_material_light),
+ ),
+ onClick = listener,
+ ),
+ SmallMenuCandidate(
+ "TwoStateSecondary",
+ icon = DrawableMenuIcon(null),
+ onClick = listener,
+ ),
+ ),
+ ),
+ toolbarWithTwoState.asCandidate(testContext).run {
+ copy(
+ items = items.map {
+ it.copy(icon = it.icon.copy(drawable = null))
+ },
+ )
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuSwitchTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuSwitchTest.kt
new file mode 100644
index 0000000000..9943e11212
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuSwitchTest.kt
@@ -0,0 +1,38 @@
+/* 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.browser.menu.item
+
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BrowserMenuSwitchTest {
+
+ @Test
+ fun `browser switch uses correct layout`() {
+ val item = BrowserMenuSwitch("Hello") {}
+ assertEquals(R.layout.mozac_browser_menu_item_switch, item.getLayoutResource())
+ }
+
+ @Test
+ fun `switch can be converted to candidate with correct end type`() {
+ val listener = { _: Boolean -> }
+
+ assertEquals(
+ CompoundMenuCandidate(
+ "Hello",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.SWITCH,
+ onCheckedChange = listener,
+ ),
+ BrowserMenuSwitch(
+ "Hello",
+ listener = listener,
+ ).asCandidate(mock()),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/ParentBrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/ParentBrowserMenuItemTest.kt
new file mode 100644
index 0000000000..2335571a25
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/ParentBrowserMenuItemTest.kt
@@ -0,0 +1,150 @@
+/* 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.browser.menu.item
+
+import android.view.LayoutInflater
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuAdapter
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.support.test.robolectric.testContext
+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.Test
+import org.junit.runner.RunWith
+import mozilla.components.ui.icons.R as iconsR
+
+@RunWith(AndroidJUnit4::class)
+class ParentBrowserMenuItemTest {
+
+ @Test
+ fun `menu item ImageText should have the right text, image, and iconTintColorResource`() {
+ val subMenuItem = SimpleBrowserMenuItem("test")
+ val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(subMenuItem))
+ val subMenu = BrowserMenu(subMenuAdapter)
+ val parentMenuItem = ParentBrowserMenuItem(
+ label = "label",
+ imageResource = android.R.drawable.ic_menu_report_image,
+ iconTintColorResource = android.R.color.black,
+ textColorResource = android.R.color.black,
+ subMenu = subMenu,
+ )
+ val view = LayoutInflater.from(testContext).inflate(parentMenuItem.getLayoutResource(), null)
+ val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem))
+ val nestedMenu = BrowserMenu(nestedMenuAdapter)
+
+ parentMenuItem.bind(nestedMenu, view)
+ val textView = view.findViewById<TextView>(R.id.text)
+ val imageView = view.findViewById<AppCompatImageView>(R.id.image)
+ val overflowView = view.findViewById<AppCompatImageView>(R.id.overflowImage)
+
+ assertEquals("label", textView.text)
+ assertNotNull(imageView.drawable)
+ assertNotNull(imageView.imageTintList)
+ assertNotNull(overflowView.imageTintList)
+ }
+
+ @Test
+ fun `menu item ImageText with no iconTintColorResource must not have an imageTintList`() {
+ val subMenuItem = SimpleBrowserMenuItem("test")
+ val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(subMenuItem))
+ val subMenu = BrowserMenu(subMenuAdapter)
+ val parentMenuItem = ParentBrowserMenuItem(
+ label = "label",
+ imageResource = android.R.drawable.ic_menu_report_image,
+ subMenu = subMenu,
+ )
+ val view = LayoutInflater.from(testContext).inflate(parentMenuItem.getLayoutResource(), null)
+ val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem))
+ val nestedMenu = BrowserMenu(nestedMenuAdapter)
+
+ parentMenuItem.bind(nestedMenu, view)
+ val imageView = view.findViewById<AppCompatImageView>(R.id.image)
+
+ assertNull(imageView.imageTintList)
+ }
+
+ @Test
+ fun `onBackPressed after sub menu is shown will dismiss the sub menu`() {
+ val backPressMenuItem = BackPressMenuItem(
+ contentDescription = "Navigate up",
+ label = "back",
+ imageResource = iconsR.drawable.mozac_ic_back_24,
+ )
+ val backPressView = LayoutInflater.from(testContext).inflate(backPressMenuItem.getLayoutResource(), null)
+ val subMenuItem = SimpleBrowserMenuItem("test")
+ val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(backPressMenuItem, subMenuItem))
+ val subMenu = BrowserMenu(subMenuAdapter)
+ backPressMenuItem.bind(subMenu, backPressView)
+
+ val parentMenuItem = ParentBrowserMenuItem(
+ label = "label",
+ imageResource = android.R.drawable.ic_menu_report_image,
+ subMenu = subMenu,
+ )
+ val view = LayoutInflater.from(testContext).inflate(parentMenuItem.getLayoutResource(), null)
+ val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem))
+ val nestedMenu = BrowserMenu(nestedMenuAdapter)
+ parentMenuItem.bind(nestedMenu, view)
+
+ nestedMenu.show(view)
+ view.performClick()
+ assertTrue(subMenu.isShown)
+ assertFalse(nestedMenu.isShown)
+
+ backPressView.performClick()
+ assertFalse(subMenu.isShown)
+ assertTrue(nestedMenu.isShown)
+ }
+
+ @Test
+ fun `menu item image text item can be converted to candidate`() {
+ val backPressMenuItem = BackPressMenuItem(
+ contentDescription = "Navigate up",
+ label = "back",
+ imageResource = iconsR.drawable.mozac_ic_back_24,
+ )
+ val subMenuItem = SimpleBrowserMenuItem("test")
+ val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(backPressMenuItem, subMenuItem))
+ val subMenu = BrowserMenu(subMenuAdapter)
+ val menuItem = ParentBrowserMenuItem(
+ "label",
+ android.R.drawable.ic_menu_report_image,
+ subMenu = subMenu,
+ )
+
+ val candidate = menuItem.asCandidate(testContext)
+
+ assertEquals(menuItem.hashCode(), candidate.id)
+ assertEquals("label", candidate.text)
+ assertEquals(2, candidate.subMenuItems!!.size)
+
+ val backCandidate = candidate.subMenuItems!![0] as NestedMenuCandidate
+ val testCandidate = candidate.subMenuItems!![1] as DecorativeTextMenuCandidate
+ assertEquals(
+ NestedMenuCandidate(
+ id = backPressMenuItem.hashCode(),
+ text = "back",
+ start = DrawableMenuIcon(null),
+ subMenuItems = null,
+ ),
+ backCandidate.run {
+ copy(start = (start as? DrawableMenuIcon)?.copy(drawable = null))
+ },
+ )
+ assertEquals(
+ DecorativeTextMenuCandidate("test"),
+ testCandidate,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItemTest.kt
new file mode 100644
index 0000000000..7a2fe33d20
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItemTest.kt
@@ -0,0 +1,152 @@
+/* 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.browser.menu.item
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SimpleBrowserMenuHighlightableItemTest {
+
+ @Test
+ fun `GIVEN a simple item, WHEN we try to inflate it in the menu, THEN the item should be inflated`() {
+ var onClickWasPress = false
+ val item = SimpleBrowserMenuHighlightableItem(
+ "label",
+ textColorResource = android.R.color.black,
+ backgroundTint = Color.RED,
+ ) {
+ onClickWasPress = true
+ }
+
+ val view = inflate(item)
+ view.performClick()
+
+ assertTrue(onClickWasPress)
+ }
+
+ @Test
+ fun `GIVEN a simple item, WHEN we inflate it, THEN it should be visible by default`() {
+ val listener = {}
+ val item = SimpleBrowserMenuHighlightableItem(
+ label = "label",
+ textColorResource = android.R.color.black,
+ backgroundTint = Color.RED,
+ listener = listener,
+ )
+
+ assertTrue(item.visible())
+ }
+
+ @Test
+ fun `GIVEN a simple item, WHEN clicking bound view, THEN callback is invoked and the menu dismissed`() {
+ var callbackInvoked = false
+
+ val item = SimpleBrowserMenuHighlightableItem(
+ label = "label",
+ textColorResource = android.R.color.black,
+ backgroundTint = Color.RED,
+ ) {
+ callbackInvoked = true
+ }
+
+ val menu = mock(BrowserMenu::class.java)
+ val view = TextView(testContext)
+
+ item.bind(menu, view)
+
+ view.performClick()
+
+ assertTrue(callbackInvoked)
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `GIVEN a simple item, WHEN we inflate it, THEN it should have the right properties`() {
+ val listener = {}
+ val item = SimpleBrowserMenuHighlightableItem(
+ label = "label",
+ textSize = 10f,
+ textColorResource = android.R.color.black,
+ backgroundTint = Color.RED,
+ isHighlighted = { false },
+ listener = listener,
+ )
+
+ var view = inflate(item)
+ var textView = view.findViewById<TextView>(R.id.simple_text)
+
+ assertEquals(textView.text, "label")
+ assertEquals(textView.textSize, 10f)
+ assertEquals(textView.currentTextColor, testContext.getColor(android.R.color.black))
+
+ val highlightedItem = SimpleBrowserMenuHighlightableItem(
+ label = "label",
+ textSize = 10f,
+ textColorResource = android.R.color.black,
+ backgroundTint = Color.RED,
+ isHighlighted = { true },
+ listener = listener,
+ )
+
+ view = inflate(highlightedItem)
+ textView = view.findViewById(R.id.simple_text)
+
+ assertEquals(textView.text, "label")
+ assertEquals(textView.textSize, 10f)
+ assertEquals(textView.currentTextColor, testContext.getColor(android.R.color.black))
+ assertEquals((textView.background as ColorDrawable).color, Color.RED)
+ }
+
+ @Test
+ fun `GIVEN a simple item, WHEN it converts to candidate, THEN it should have the correct properties`() {
+ val listener = {}
+ val shouldHighlight = false
+ val item = SimpleBrowserMenuHighlightableItem(
+ label = "label",
+ textColorResource = android.R.color.black,
+ backgroundTint = Color.RED,
+ isHighlighted = { shouldHighlight },
+ listener = listener,
+ )
+
+ assertEquals(
+ TextMenuCandidate(
+ "label",
+ textStyle = TextStyle(
+ color = ContextCompat.getColor(testContext, android.R.color.black),
+ ),
+ containerStyle = ContainerStyle(true),
+ onClick = listener,
+ ),
+ item.asCandidate(testContext),
+ )
+ }
+
+ private fun inflate(item: SimpleBrowserMenuHighlightableItem): View {
+ val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null)
+ val mockMenu = Mockito.mock(BrowserMenu::class.java)
+ item.bind(mockMenu, view)
+ return view
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItemTest.kt
new file mode 100644
index 0000000000..bd5da09b52
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItemTest.kt
@@ -0,0 +1,122 @@
+/* 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.browser.menu.item
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat.getColor
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SimpleBrowserMenuItemTest {
+
+ @Test
+ fun `simple menu items are always visible by default`() {
+ val item = SimpleBrowserMenuItem("Hello") {
+ // do nothing
+ }
+
+ assertTrue(item.visible())
+ }
+
+ @Test
+ fun `layout resource can be inflated`() {
+ val item = SimpleBrowserMenuItem("Hello") {
+ // do nothing
+ }
+
+ val view = LayoutInflater.from(testContext)
+ .inflate(item.getLayoutResource(), null)
+
+ assertNotNull(view)
+ }
+
+ @Test
+ fun `clicking bound view will invoke callback and dismiss menu`() {
+ var callbackInvoked = false
+
+ val item = SimpleBrowserMenuItem("Hello") {
+ callbackInvoked = true
+ }
+
+ val menu = mock(BrowserMenu::class.java)
+ val view = TextView(testContext)
+
+ item.bind(menu, view)
+
+ view.performClick()
+
+ assertTrue(callbackInvoked)
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `simple browser menu item should have the right text, textSize, and textColorResource`() {
+ val item = SimpleBrowserMenuItem(
+ "Powered by Mozilla",
+ 10f,
+ android.R.color.holo_green_dark,
+ )
+
+ val view = inflate(item)
+
+ val textView = view.findViewById<TextView>(R.id.simple_text)
+ assertEquals(textView.text, "Powered by Mozilla")
+ assertEquals(textView.textSize, 10f)
+ assertEquals(textView.currentTextColor, testContext.getColor(android.R.color.holo_green_dark))
+ }
+
+ @Test
+ fun `simple menu item can be converted to candidate`() {
+ val listener = {}
+
+ assertEquals(
+ TextMenuCandidate(
+ "Hello",
+ onClick = listener,
+ ),
+ SimpleBrowserMenuItem(
+ "Hello",
+ listener = listener,
+ ).asCandidate(testContext),
+ )
+
+ assertEquals(
+ DecorativeTextMenuCandidate(
+ "Powered by Mozilla",
+ textStyle = TextStyle(
+ size = 10f,
+ color = getColor(testContext, android.R.color.holo_green_dark),
+ ),
+ ),
+ SimpleBrowserMenuItem(
+ "Powered by Mozilla",
+ 10f,
+ android.R.color.holo_green_dark,
+ ).asCandidate(testContext),
+ )
+ }
+
+ private fun inflate(item: SimpleBrowserMenuItem): View {
+ val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null)
+ val mockMenu = mock(BrowserMenu::class.java)
+ item.bind(mockMenu, view)
+ return view
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageTextTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageTextTest.kt
new file mode 100644
index 0000000000..c8030091cc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageTextTest.kt
@@ -0,0 +1,87 @@
+/* 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.browser.menu.item
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.R
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@RunWith(AndroidJUnit4::class)
+class TwoStateBrowserMenuImageTextTest {
+
+ private val context: Context get() = ApplicationProvider.getApplicationContext()
+ private lateinit var menuItemPrimary: TwoStateBrowserMenuImageText
+ private lateinit var menuItemSecondary: TwoStateBrowserMenuImageText
+
+ private var primaryPressed = false
+ private var secondaryPressed = false
+
+ private val primaryLabel: String = "primaryLabel"
+ private val secondaryLabel: String = "secondaryLabel"
+
+ @Before
+ fun setup() {
+ menuItemPrimary = TwoStateBrowserMenuImageText(
+ primaryLabel = primaryLabel,
+ secondaryLabel = secondaryLabel,
+ primaryStateIconResource = android.R.drawable.ic_delete,
+ secondaryStateIconResource = android.R.drawable.ic_input_add,
+ isInPrimaryState = { true },
+ primaryStateAction = { primaryPressed = true },
+ )
+
+ menuItemSecondary = TwoStateBrowserMenuImageText(
+ primaryLabel = primaryLabel,
+ secondaryLabel = secondaryLabel,
+ primaryStateIconResource = android.R.drawable.ic_delete,
+ secondaryStateIconResource = android.R.drawable.ic_input_add,
+ isInPrimaryState = { false },
+ isInSecondaryState = { true },
+ secondaryStateAction = { secondaryPressed = true },
+ )
+ }
+
+ @Test
+ fun `browser menu should be inflated`() {
+ val view = inflate(menuItemPrimary)
+ view.performClick()
+ assertTrue(primaryPressed)
+
+ val secondView = inflate(menuItemSecondary)
+ secondView.performClick()
+ assertTrue(secondaryPressed)
+ }
+
+ @Test
+ fun `browser menu should have the right text`() {
+ val view = inflate(menuItemPrimary)
+ val textView = view.findViewById<TextView>(R.id.text)
+
+ assertEquals(textView.text, primaryLabel)
+
+ val secondView = inflate(menuItemSecondary)
+ val secondTextView = secondView.findViewById<TextView>(R.id.text)
+
+ assertEquals(secondTextView.text, secondaryLabel)
+ }
+
+ private fun inflate(item: BrowserMenuImageText): View {
+ val view = LayoutInflater.from(context).inflate(item.getLayoutResource(), null)
+ val mockMenu = mock(BrowserMenu::class.java)
+ item.bind(mockMenu, view)
+ return view
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt
new file mode 100644
index 0000000000..60419f4bfb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt
@@ -0,0 +1,355 @@
+/* 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.browser.menu.item
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.menu.R
+import mozilla.components.browser.menu.WebExtensionBrowserMenu
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.notNull
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+class WebExtensionBrowserMenuItemTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun `web extension menu item is visible by default`() {
+ val webExtMenuItem = WebExtensionBrowserMenuItem(mock(), mock())
+
+ assertTrue(webExtMenuItem.visible())
+ }
+
+ @Test
+ fun `layout resource can be inflated`() {
+ val webExtMenuItem = WebExtensionBrowserMenuItem(mock(), mock())
+
+ val view = LayoutInflater.from(testContext)
+ .inflate(webExtMenuItem.getLayoutResource(), null)
+
+ assertNotNull(view)
+ }
+
+ @Test
+ fun `view is disabled if browser action is disabled`() {
+ val icon: Bitmap = mock()
+ val imageView: ImageView = mock()
+ val badgeView: TextView = mock()
+ val labelView: TextView = mock()
+ val container = View(testContext)
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(mock())
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { icon },
+ enabled = false,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val action = WebExtensionBrowserMenuItem(browserAction, {})
+ action.bind(mock(), view)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertFalse(view.isEnabled)
+ }
+
+ @Test
+ fun bind() {
+ val icon: Bitmap = mock()
+ val imageView: ImageView = mock()
+ val badgeView: TextView = mock()
+ val background: Drawable = mock()
+ val labelView: TextView = mock()
+ val container = View(testContext)
+ val view: View = mock()
+
+ whenever(badgeView.background).thenReturn(background)
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(testContext)
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { icon },
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val action = WebExtensionBrowserMenuItem(browserAction, {})
+ action.bind(mock(), view)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ val iconCaptor = argumentCaptor<BitmapDrawable>()
+ verify(imageView).setImageDrawable(iconCaptor.capture())
+ assertEquals(icon, iconCaptor.value.bitmap)
+
+ verify(imageView).contentDescription = "title"
+ verify(labelView).text = "title"
+ verify(badgeView).text = "badgeText"
+ verify(badgeView).setTextColor(Color.WHITE)
+ verify(background).setTint(Color.BLUE)
+ }
+
+ @Test
+ fun `badge text view is invisible if action badge text is empty`() {
+ val icon: Bitmap = mock()
+ val imageView: ImageView = mock()
+ val badgeView: TextView = spy(TextView(testContext))
+ val labelView: TextView = mock()
+ val container = View(testContext)
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(testContext)
+
+ val badgeText = ""
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { icon },
+ enabled = true,
+ badgeText = badgeText,
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val action = WebExtensionBrowserMenuItem(browserAction, {})
+ action.bind(mock(), view)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(badgeView).setBadgeText(badgeText)
+ assertEquals(View.INVISIBLE, badgeView.visibility)
+ }
+
+ @Test
+ fun fallbackToDefaultIcon() {
+ val imageView: ImageView = mock()
+ val badgeView: TextView = mock()
+ val labelView: TextView = mock()
+ val container = View(testContext)
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(testContext)
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { throw IllegalArgumentException() },
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val action = WebExtensionBrowserMenuItem(browserAction, {})
+ action.bind(mock(), view)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(imageView).setImageDrawable(notNull())
+ }
+
+ @Test
+ fun `clicking item view invokes callback and dismisses menu`() {
+ var callbackInvoked = false
+
+ val icon: Bitmap = mock()
+ val imageView: ImageView = mock()
+ val badgeView: TextView = mock()
+ val labelView = TextView(testContext)
+ val container = View(testContext)
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(mock())
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { icon },
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val item = WebExtensionBrowserMenuItem(browserAction, { callbackInvoked = true })
+
+ val menu: WebExtensionBrowserMenu = mock()
+
+ item.bind(menu, view)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ container.performClick()
+
+ assertTrue(callbackInvoked)
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `labelView and badgeView redraws when invalidate is triggered`() {
+ val icon: Bitmap = mock()
+ val imageView: ImageView = mock()
+ val badgeView: TextView = mock()
+ val labelView: TextView = mock()
+ val container = View(testContext)
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(mock())
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { icon },
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val item = WebExtensionBrowserMenuItem(browserAction, {})
+
+ val menu: WebExtensionBrowserMenu = mock()
+
+ item.bind(menu, view)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(labelView).text = "title"
+ verify(badgeView).text = "badgeText"
+
+ val browserActionOverride = Action(
+ title = "override",
+ loadIcon = { icon },
+ enabled = true,
+ badgeText = "overrideBadge",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ item.action = browserActionOverride
+ item.invalidate(view)
+
+ verify(labelView).text = "override"
+ verify(badgeView).text = "overrideBadge"
+ verify(labelView).invalidate()
+ verify(badgeView).invalidate()
+ }
+
+ @Test
+ fun `GIVEN setIcon was called, WHEN bind is called, icon setup uses the tint set`() = runTest {
+ val webExtMenuItem = spy(WebExtensionBrowserMenuItem(mock(), mock()))
+ val testIconTintColorResource = appcompatR.color.accent_material_dark
+ val menu: WebExtensionBrowserMenu = mock()
+ val imageView: ImageView = mock()
+ val badgeView: TextView = mock()
+ val labelView: TextView = mock()
+ val container = View(testContext)
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(mock())
+ whenever(imageView.measuredHeight).thenReturn(2)
+
+ webExtMenuItem.setIconTint(testIconTintColorResource)
+ webExtMenuItem.bind(menu, view)
+
+ val viewCaptor = argumentCaptor<View>()
+ val imageViewCaptor = argumentCaptor<ImageView>()
+ val tintCaptor = argumentCaptor<Int>()
+
+ verify(webExtMenuItem).setupIcon(viewCaptor.capture(), imageViewCaptor.capture(), tintCaptor.capture())
+
+ assertEquals(testIconTintColorResource, tintCaptor.value)
+ assertEquals(view, viewCaptor.value)
+ assertEquals(imageView, imageViewCaptor.value)
+
+ assertEquals(testIconTintColorResource, webExtMenuItem.iconTintColorResource)
+ }
+
+ @Test
+ fun `WHEN invalidate is called THEN setupIcon is called`() = runTest {
+ val webExtMenuItem = spy(WebExtensionBrowserMenuItem(mock(), mock()))
+ val imageView: ImageView = mock()
+ val badgeView: TextView = mock()
+ val labelView: TextView = mock()
+ val container = View(testContext)
+ val testIconTintColorResource = appcompatR.color.accent_material_dark
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView)
+ whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView)
+ whenever(view.findViewById<View>(R.id.container)).thenReturn(container)
+ whenever(view.context).thenReturn(mock())
+ whenever(imageView.measuredHeight).thenReturn(2)
+
+ webExtMenuItem.setIconTint(testIconTintColorResource)
+ webExtMenuItem.invalidate(view)
+
+ val viewCaptor = argumentCaptor<View>()
+ val imageViewCaptor = argumentCaptor<ImageView>()
+ val tintCaptor = argumentCaptor<Int>()
+
+ verify(webExtMenuItem).setupIcon(
+ viewCaptor.capture(),
+ imageViewCaptor.capture(),
+ tintCaptor.capture(),
+ )
+
+ assertEquals(view, viewCaptor.value)
+ assertEquals(imageView, imageViewCaptor.value)
+ assertEquals(testIconTintColorResource, webExtMenuItem.iconTintColorResource)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerViewTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerViewTest.kt
new file mode 100644
index 0000000000..48d8cf9ee3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerViewTest.kt
@@ -0,0 +1,226 @@
+/* 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.browser.menu.view
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.R
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class DynamicWidthRecyclerViewTest {
+
+ @Test
+ fun `minWidth and maxWidth should be initialized from xml attributes`() {
+ val dynamicRecyclerView = buildRecyclerView(100)
+ dynamicRecyclerView.minWidth = 123
+ dynamicRecyclerView.maxWidth = 456
+
+ assertEquals(123, dynamicRecyclerView.minWidth)
+ assertEquals(456, dynamicRecyclerView.maxWidth)
+ }
+
+ @Test
+ fun `If minWidth and maxWidth are not provided view should use layout_width`() {
+ val dynamicRecyclerView = buildRecyclerView(100)
+
+ dynamicRecyclerView.measure(100, 100)
+
+ // View should not try calculate/reconcile new dimensions, it should just call super.onMeasure(..)
+ verify(dynamicRecyclerView).callParentOnMeasure(100, 100)
+ verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt())
+ }
+
+ @Test
+ fun `If only minWidth is provided view should use layout_width`() {
+ val dynamicRecyclerView = buildRecyclerView(100)
+ dynamicRecyclerView.minWidth = 50
+
+ dynamicRecyclerView.measure(100, 100)
+
+ // View should not try calculate/reconcile new dimensions, it should just call super.onMeasure(..)
+ verify(dynamicRecyclerView).callParentOnMeasure(100, 100)
+ verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt())
+ }
+
+ @Test
+ fun `If only maxWidth is provided view should use layout_width`() {
+ val dynamicRecyclerView = buildRecyclerView(100)
+ dynamicRecyclerView.maxWidth = 300
+
+ dynamicRecyclerView.measure(100, 100)
+
+ // View should not try calculate/reconcile new dimensions, it should just call super.onMeasure(..)
+ verify(dynamicRecyclerView).callParentOnMeasure(100, 100)
+ verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt())
+ }
+
+ @Test
+ fun `Should only allow for dynamic width if minWidth has a positive value`() {
+ val dynamicRecyclerView = buildRecyclerView(100)
+ dynamicRecyclerView.minWidth = -1
+ dynamicRecyclerView.maxWidth = 100
+
+ dynamicRecyclerView.measure(1, 2)
+
+ // If minWidth has a negative value we should only just call super.onMeasure(..)
+ verify(dynamicRecyclerView).callParentOnMeasure(1, 2)
+ verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt())
+ }
+
+ @Test
+ fun `Should only allow for dynamic width if minWidth is smaller than maxWidth`() {
+ val dynamicRecyclerView = buildRecyclerView(100)
+ dynamicRecyclerView.minWidth = 100
+ dynamicRecyclerView.maxWidth = 100
+
+ dynamicRecyclerView.measure(1, 2)
+
+ // If minWidth is >= to maxWidth we should only just call super.onMeasure(..)
+ verify(dynamicRecyclerView).callParentOnMeasure(1, 2)
+ verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt())
+ }
+
+ @Test
+ fun `To allow for dynamic width children can expand entirely between minWidth and maxWidth`() {
+ val dynamicRecyclerView = buildRecyclerView(100)
+ dynamicRecyclerView.minWidth = 50
+ dynamicRecyclerView.maxWidth = 100
+
+ dynamicRecyclerView.measure(10, 10)
+
+ // To allow for children to be as wide as they want widthSpec should be 0
+ verify(dynamicRecyclerView).callParentOnMeasure(0, 10)
+ // Robolectric doesn't do any kind of measuring and always returns 0 for View measurements.
+ verify(dynamicRecyclerView).setReconciledDimensions(0, 0)
+ }
+
+ @Test
+ @Config(qualifiers = "w333dp")
+ fun `getScreenWidth() should return display's width in pixels`() {
+ val dynamicRecyclerView = DynamicWidthRecyclerView(testContext, null)
+
+ assertEquals(333, dynamicRecyclerView.getScreenWidth())
+ }
+
+ @Test
+ fun `setReconciledDimensions() must set material minimum width even if childs are smaller`() {
+ val childrenWidth = 20
+ val materialMinWidth = testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_item_width)
+ // Layout width is *2 to allow bigger sizes. minWidth is /2 to verify the material min width is used.
+ val dynamicRecyclerView = buildRecyclerView(materialMinWidth * 2)
+ dynamicRecyclerView.minWidth = materialMinWidth / 2
+ dynamicRecyclerView.maxWidth = 500
+
+ dynamicRecyclerView.setReconciledDimensions(childrenWidth, 500)
+
+ verify(dynamicRecyclerView).callSetMeasuredDimension(materialMinWidth, 500)
+ }
+
+ @Test
+ fun `setReconciledDimensions() must set minWidth even if children width is smaller`() {
+ val childrenWidth = 20
+ // minWidth set in xml. Ensure it is bigger than the default.
+ val minWidth = testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_item_width) + 10
+ val dynamicRecyclerView = buildRecyclerView(minWidth * 2)
+ dynamicRecyclerView.minWidth = minWidth
+ dynamicRecyclerView.maxWidth = 500
+
+ dynamicRecyclerView.setReconciledDimensions(childrenWidth, 500)
+
+ verify(dynamicRecyclerView).callSetMeasuredDimension(minWidth, 500)
+ }
+
+ @Test
+ fun `setReconciledDimensions() will set children width if it is bigger than minWidth and smaller than maxWidth`() {
+ val materialMinWidth = testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_item_width)
+ val childrenWidth = materialMinWidth + 10
+ // Layout width is *2 to allow bigger sizes. minWidth is /2 to verify the material min width is used.
+ val dynamicRecyclerView = buildRecyclerView(materialMinWidth * 2)
+ dynamicRecyclerView.minWidth = materialMinWidth
+ dynamicRecyclerView.maxWidth = 500
+
+ dynamicRecyclerView.setReconciledDimensions(childrenWidth, 500)
+
+ verify(dynamicRecyclerView).callSetMeasuredDimension(childrenWidth, 500)
+ }
+
+ @Test
+ @Config(qualifiers = "w500dp")
+ @Suppress("UnnecessaryVariable")
+ fun `setReconciledDimensions() must set maxWidth when children width is bigger`() {
+ val materialMaxWidth = 500 - testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_tap_area)
+ val childrenWidth = materialMaxWidth
+ val maxWidth = materialMaxWidth - 10
+ val dynamicRecyclerView = buildRecyclerView(1000)
+ dynamicRecyclerView.minWidth = 100
+ dynamicRecyclerView.maxWidth = maxWidth
+
+ dynamicRecyclerView.setReconciledDimensions(childrenWidth, 100)
+
+ verify(dynamicRecyclerView).callSetMeasuredDimension(maxWidth, 100)
+ }
+
+ @Test
+ @Config(qualifiers = "w500dp")
+ fun `setReconciledDimensions must set material maximum width when children width is bigger`() {
+ val materialMaxWidth = 500 - testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_tap_area)
+ val maxWidth = 500
+ val childrenWidth = maxWidth + 10
+ val dynamicRecyclerView = buildRecyclerView(1000)
+ dynamicRecyclerView.minWidth = 100
+ dynamicRecyclerView.maxWidth = maxWidth
+
+ dynamicRecyclerView.setReconciledDimensions(childrenWidth, 100)
+
+ verify(dynamicRecyclerView).callSetMeasuredDimension(materialMaxWidth, 100)
+ }
+
+ @Test
+ fun `maxWidthOfAllChildren can only be initialized once with a positive value`() {
+ val dynamicRecyclerView = DynamicWidthRecyclerView(testContext)
+
+ assertEquals(0, dynamicRecyclerView.maxWidthOfAllChildren)
+
+ dynamicRecyclerView.maxWidthOfAllChildren = 42
+ assertEquals(42, dynamicRecyclerView.maxWidthOfAllChildren)
+
+ dynamicRecyclerView.maxWidthOfAllChildren = 24
+ assertEquals(42, dynamicRecyclerView.maxWidthOfAllChildren)
+ }
+
+ @Test
+ fun `onMeasure will call setReconciledDimensions with maxWidthOfAllChildren`() {
+ val dynamicRecyclerView = spy(DynamicWidthRecyclerView(testContext))
+ doReturn(100).`when`(dynamicRecyclerView).measuredHeight
+ doReturn(100).`when`(dynamicRecyclerView).measuredWidth
+ doReturn(100).`when`(dynamicRecyclerView).height
+ dynamicRecyclerView.maxWidthOfAllChildren = 42
+ dynamicRecyclerView.minWidth = 10
+ dynamicRecyclerView.maxWidth = Int.MAX_VALUE
+
+ dynamicRecyclerView.measure(0, 0)
+
+ verify(dynamicRecyclerView).setReconciledDimensions(42, 100)
+ }
+
+ private fun buildRecyclerView(layoutWidth: Int): DynamicWidthRecyclerView {
+ val customAttributeSet = Robolectric.buildAttributeSet().apply {
+ // android.R.attr.layout_width needs to always be set
+ addAttribute(android.R.attr.layout_width, "${layoutWidth}dp")
+ }.build()
+
+ return spy(DynamicWidthRecyclerView(testContext, customAttributeSet))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/ExpandableLayoutTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/ExpandableLayoutTest.kt
new file mode 100644
index 0000000000..4a5dfa1bd1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/ExpandableLayoutTest.kt
@@ -0,0 +1,822 @@
+/* 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.browser.menu.view
+
+import android.animation.ValueAnimator
+import android.graphics.Rect
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.ViewGroup
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.widget.FrameLayout
+import androidx.core.view.marginBottom
+import androidx.core.view.marginLeft
+import androidx.core.view.marginRight
+import androidx.core.view.marginTop
+import androidx.core.view.updateLayoutParams
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ExpandableLayoutTest {
+ @Test
+ fun `GIVEN ExpandableLayout WHEN wrapContentInExpandableView is called THEN it should properly setup a new ExpandableLayout`() {
+ val wrappedView = FrameLayout(testContext)
+ val blankTouchListener: (() -> Unit) = mock()
+ wrappedView.layoutParams = ViewGroup.MarginLayoutParams(11, 12).apply {
+ setMargins(13, 14, 15, 16)
+ }
+
+ val result = ExpandableLayout.wrapContentInExpandableView(
+ wrappedView,
+ 42,
+ 33,
+ blankTouchListener,
+ )
+
+ assertEquals(FrameLayout.LayoutParams.WRAP_CONTENT, result.wrappedView.layoutParams.height)
+ assertEquals(FrameLayout.LayoutParams.WRAP_CONTENT, result.wrappedView.layoutParams.width)
+ assertEquals(13, result.wrappedView.marginLeft)
+ assertEquals(14, result.wrappedView.marginTop)
+ assertEquals(15, result.wrappedView.marginRight)
+ assertEquals(16, result.wrappedView.marginBottom)
+ assertEquals(1, result.childCount)
+ assertSame(wrappedView, result.wrappedView)
+ assertSame(blankTouchListener, result.blankTouchListener)
+
+ // Also test the default configuration of a newly built ExpandableLayout.
+ assertEquals(42, result.lastVisibleItemIndexWhenCollapsed)
+ assertEquals(33, result.stickyItemIndex)
+ assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.collapsedHeight)
+ assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.expandedHeight)
+ assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.parentHeight)
+ assertTrue(result.isCollapsed)
+ assertFalse(result.isExpandInProgress)
+ assertEquals(ViewConfiguration.get(testContext).scaledTouchSlop.toFloat(), result.touchSlop)
+ assertEquals(ExpandableLayout.NOT_CALCULATED_Y_TOUCH_COORD, result.initialYCoord)
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout WHEN onMeasure is called THEN it delegates the parent for measuring`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ expandableLayout.isCollapsed = false
+
+ expandableLayout.measure(123, 123)
+
+ verify(expandableLayout).callParentOnMeasure(123, 123)
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout in the collapsed state and the height values available WHEN onMeasure is called THEN it will trigger collapse()`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ expandableLayout.isCollapsed = true
+ doReturn(100).`when`(expandableLayout).getOrCalculateCollapsedHeight()
+ doReturn(100).`when`(expandableLayout).getOrCalculateExpandedHeight(anyInt())
+
+ expandableLayout.measure(123, 123)
+
+ verify(expandableLayout).collapse()
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout not in the collapsed state but all height values known WHEN onMeasure is called THEN collapse() is not called`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ expandableLayout.isCollapsed = false
+ doReturn(100).`when`(expandableLayout).getOrCalculateCollapsedHeight()
+ doReturn(100).`when`(expandableLayout).getOrCalculateExpandedHeight(anyInt())
+
+ expandableLayout.measure(123, 123)
+
+ verify(expandableLayout, never()).collapse()
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout in the collapsed state but with collapsedHeight unknown WHEN onMeasure is called THEN collapse() is be called`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ expandableLayout.isCollapsed = true
+ doReturn(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT).`when`(expandableLayout).getOrCalculateCollapsedHeight()
+ doReturn(100).`when`(expandableLayout).getOrCalculateExpandedHeight(anyInt())
+
+ expandableLayout.measure(123, 123)
+
+ verify(expandableLayout, never()).collapse()
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout in the collapsed state but with expandedHeight unknown WHEN onMeasure is called THEN collapse() is not be called`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ expandableLayout.isCollapsed = true
+ doReturn(100).`when`(expandableLayout).getOrCalculateCollapsedHeight()
+ doReturn(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT).`when`(expandableLayout).getOrCalculateExpandedHeight(anyInt())
+
+ expandableLayout.measure(123, 123)
+
+ verify(expandableLayout, never()).collapse()
+ }
+
+ @Test
+ fun `GIVEN an expanded menu WHEN onInterceptTouchEvent is called for a touch on the menu THEN super is called`() {
+ val blankTouchListener = spy {}
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ blankTouchListener = blankTouchListener,
+ ),
+ )
+ val event: MotionEvent = mock()
+ doReturn(false).`when`(expandableLayout).shouldInterceptTouches()
+ doReturn(true).`when`(expandableLayout).isTouchingTheWrappedView(any())
+
+ expandableLayout.onInterceptTouchEvent(event)
+
+ verify(blankTouchListener, never()).invoke()
+ verify(expandableLayout).callParentOnInterceptTouchEvent(event)
+ }
+
+ @Test
+ fun `GIVEN a menu currently expanding WHEN onInterceptTouchEvent is called for a touch on the menu THEN the touch is swallowed`() {
+ val blankTouchListener = spy {}
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ blankTouchListener = blankTouchListener,
+ ),
+ )
+ val event: MotionEvent = mock()
+ doReturn(false).`when`(expandableLayout).shouldInterceptTouches()
+ doReturn(true).`when`(expandableLayout).isTouchingTheWrappedView(any())
+ expandableLayout.isExpandInProgress = true
+
+ expandableLayout.onInterceptTouchEvent(event)
+
+ verify(blankTouchListener, never()).invoke()
+ verify(expandableLayout, never()).callParentOnInterceptTouchEvent(event)
+ }
+
+ @Test
+ fun `GIVEN an expanded menu WHEN onInterceptTouchEvent is called for a outside the menu THEN super blankTouchListener is invoked`() {
+ val blankTouchListener = spy {}
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ blankTouchListener = blankTouchListener,
+ ),
+ )
+ val event: MotionEvent = mock()
+ doReturn(false).`when`(expandableLayout).shouldInterceptTouches()
+ doReturn(false).`when`(expandableLayout).isTouchingTheWrappedView(any())
+
+ expandableLayout.onInterceptTouchEvent(event)
+
+ verify(blankTouchListener).invoke()
+ verify(expandableLayout, never()).callParentOnInterceptTouchEvent(event)
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout WHEN onInterceptTouchEvent is called for ACTION_CANCEL or ACTION_UP THEN the events are not intercepted`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ val actionCancel = MotionEvent.obtain(0, 0, ACTION_UP, 0f, 0f, 0)
+ val actionUp = MotionEvent.obtain(0, 0, ACTION_CANCEL, 0f, 0f, 0)
+ doReturn(true).`when`(expandableLayout).shouldInterceptTouches()
+
+ assertFalse(expandableLayout.onInterceptTouchEvent(actionCancel))
+ assertFalse(expandableLayout.onInterceptTouchEvent(actionUp))
+
+ verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any())
+ }
+
+ @Test
+ fun `GIVEN the wrappedView is in the expand process WHEN onInterceptTouchEvent is called while for new touches THEN they are intercepted`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0)
+ doReturn(true).`when`(expandableLayout).shouldInterceptTouches()
+ expandableLayout.isExpandInProgress = true
+
+ assertTrue(expandableLayout.onInterceptTouchEvent(actionDown))
+ verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any())
+ }
+
+ @Test
+ fun `GIVEN the wrappedView not in the expand process WHEN onInterceptTouchEvent is called for new touches THEN they are not intercepted`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0)
+ doReturn(true).`when`(expandableLayout).shouldInterceptTouches()
+ expandableLayout.isExpandInProgress = false
+
+ assertFalse(expandableLayout.onInterceptTouchEvent(actionDown))
+ verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any())
+ }
+
+ @Test
+ fun `GIVEN blankTouchListener set WHEN onInterceptTouchEvent is called for a touch that does not intersect wrappedView's bounds THEN blankTouchListener is called`() {
+ var listenerCalled = false
+ val listener = spy { listenerCalled = true }
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ blankTouchListener = listener,
+ ),
+ )
+ doReturn(false).`when`(expandableLayout).isTouchingTheWrappedView(any())
+ val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0)
+
+ expandableLayout.onInterceptTouchEvent(actionDown)
+
+ assertTrue(listenerCalled)
+ }
+
+ @Test
+ fun `GIVEN blankTouchListener set WHEN onInterceptTouchEvent is called for a touch that intersects wrappedView's bounds THEN blankTouchListener is not called`() {
+ var listenerCalled = false
+ val listener = spy { listenerCalled = true }
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ blankTouchListener = listener,
+ ),
+ )
+ doReturn(true).`when`(expandableLayout).isTouchingTheWrappedView(any())
+ val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0)
+
+ expandableLayout.onInterceptTouchEvent(actionDown)
+
+ assertFalse(listenerCalled)
+ }
+
+ @Test
+ fun `GIVEN initialYCoord set WHEN onInterceptTouchEvent is called for ACTION_DOWN THEN initialYCoord will be reset to the new value`() {
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { }
+
+ val actionDown1 = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 22f, 0)
+ expandableLayout.onInterceptTouchEvent(actionDown1)
+ assertEquals(22f, expandableLayout.initialYCoord)
+
+ val actionDown2 = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, -33f, 0)
+ expandableLayout.onInterceptTouchEvent(actionDown2)
+ assertEquals(-33f, expandableLayout.initialYCoord)
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout is in the expand process WHEN onInterceptTouchEvent is called for scroll events THEN these events are intercepted`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ val actionDown = MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, 0f, 0)
+ doReturn(false).`when`(expandableLayout).shouldInterceptTouches()
+ doReturn(false).`when`(expandableLayout).isTouchingTheWrappedView(any())
+
+ assertTrue(expandableLayout.onInterceptTouchEvent(actionDown))
+ verify(expandableLayout, never()).expand()
+ verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any())
+ }
+
+ @Test
+ fun `GIVEN the wrappedView is not expanding WHEN onInterceptTouchEvent is called for an event that is not a scroll THEN this event is not intercepted`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ val actionDown = MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, 0f, 0)
+ doReturn(true).`when`(expandableLayout).shouldInterceptTouches()
+ doReturn(false).`when`(expandableLayout).isScrollingUp(any())
+
+ assertFalse(expandableLayout.onInterceptTouchEvent(actionDown))
+ verify(expandableLayout, never()).expand()
+ verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any())
+ }
+
+ @Test
+ fun `GIVEN the wrappedView is not expanding WHEN onInterceptTouchEvent is called for scroll up events THEN the events are intercepted and expand() is called`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ val actionDown = MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, 0f, 0)
+ doReturn(true).`when`(expandableLayout).shouldInterceptTouches()
+ doReturn(true).`when`(expandableLayout).isScrollingUp(any())
+
+ assertTrue(expandableLayout.onInterceptTouchEvent(actionDown))
+ verify(expandableLayout).expand()
+ verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any())
+ }
+
+ @Test
+ fun `GIVEN isTouchForWrappedView() WHEN called with a touch event that is within the wrappedView bounds THEN it returns true`() {
+ val wrappedView = spy(FrameLayout(testContext))
+ doAnswer {
+ val rect = it.arguments[0] as Rect
+ rect.set(0, 0, 100, 100)
+ }.`when`(wrappedView).getHitRect(any())
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ wrappedView,
+ 1,
+ ) { }
+ val inBoundsEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 5f, 5f, 0)
+
+ assertTrue(expandableLayout.isTouchingTheWrappedView(inBoundsEvent))
+ }
+
+ @Test
+ fun `GIVEN isTouchForWrappedView WHEN called with a touch event that is not within the wrappedView bounds THEN it returns false`() {
+ val wrappedView = spy(FrameLayout(testContext))
+ doAnswer {
+ val rect = it.arguments[0] as Rect
+ rect.set(0, 0, 100, 100)
+ }.`when`(wrappedView).getHitRect(any())
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ wrappedView,
+ 1,
+ ) { }
+ val outOfBoundsEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 105f, 105f, 0)
+
+ assertFalse(expandableLayout.isTouchingTheWrappedView(outOfBoundsEvent))
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout in the collapsed state WHEN shouldInterceptTouches is called THEN it returns true`() {
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { }
+ expandableLayout.isCollapsed = true
+
+ assertTrue(expandableLayout.shouldInterceptTouches())
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout not collapsed WHEN shouldInterceptTouches is called THEN it returns false`() {
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { }
+ expandableLayout.isCollapsed = false
+
+ assertFalse(expandableLayout.shouldInterceptTouches())
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout currently expanding WHEN shouldInterceptTouches is called THEN it returns false`() {
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { }
+ expandableLayout.isCollapsed = true
+ expandableLayout.isExpandInProgress = false
+
+ assertTrue(expandableLayout.shouldInterceptTouches())
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout WHEN collapse is called THEN it sets a positive translation and a smaller height for the wrappedView`() {
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ spy(FrameLayout(testContext)),
+ 1,
+ ) { }
+ expandableLayout.wrappedView.updateLayoutParams {
+ height = 100
+ }
+ expandableLayout.parentHeight = 200
+ expandableLayout.collapsedHeight = 50
+
+ expandableLayout.collapse()
+
+ // If the available height is 200, with the layout starting at 0,0
+ // to properly "anchor" a 50px height wrappedView we need to translate it 150px.
+ verify(expandableLayout.wrappedView).translationY = 150f
+ assertEquals(50, expandableLayout.wrappedView.layoutParams.height)
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout WHEN getExpandViewAnimator is called THEN it returns a new ValueAnimator`() {
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { }
+
+ val result = expandableLayout.getExpandViewAnimator(100)
+
+ assertTrue(result.interpolator is AccelerateDecelerateInterpolator)
+ assertEquals(ExpandableLayout.DEFAULT_DURATION_EXPAND_ANIMATOR, result.duration)
+ }
+
+ @Test
+ fun `GIVEN ExpandableLayout WHEN expand is called THEN it updates the translationY and height to show the wrappedView with expandedHeight`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ val animator = ValueAnimator.ofInt(0, 100)
+ doAnswer {
+ animator
+ }.`when`(expandableLayout).getExpandViewAnimator(anyInt())
+ expandableLayout.expandedHeight = 100
+ expandableLayout.collapsedHeight = 50
+ expandableLayout.wrappedView.translationY = 50f
+
+ expandableLayout.expand()
+ animator.end()
+
+ verify(expandableLayout).getExpandViewAnimator(50)
+ assertEquals(-50f, expandableLayout.wrappedView.translationY)
+ assertEquals(150, expandableLayout.wrappedView.layoutParams.height)
+ assertTrue(System.currentTimeMillis() > 0)
+ }
+
+ @Test
+ fun `GIVEN collapsedHeight if already calculated WHEN getOrCalculateCollapsedHeight is called THEN it returns collapsedHeight`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ expandableLayout.collapsedHeight = 123
+
+ val result = expandableLayout.getOrCalculateCollapsedHeight()
+
+ verify(expandableLayout, never()).calculateCollapsedHeight()
+ assertEquals(123, result)
+ }
+
+ @Test
+ fun `GIVEN collapsedHeight is not already calculated WHEN getOrCalculateCollapsedHeight is called THEN it delegates calculateCollapsedHeight and returns the value from that`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ doReturn(42).`when`(expandableLayout).calculateCollapsedHeight()
+
+ val result = expandableLayout.getOrCalculateCollapsedHeight()
+
+ verify(expandableLayout).calculateCollapsedHeight()
+ assertEquals(42, result)
+ }
+
+ @Test
+ fun `GIVEN expandedHeight not calculated WHEN getOrCalculateExpandedHeight is called THEN it sets expandedHeight with the value of measuredHeight`() {
+ val wrappedView = spy(FrameLayout(testContext))
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ wrappedView,
+ 1,
+ ) { },
+ )
+
+ doReturn(100).`when`(wrappedView).measuredHeight
+ assertEquals(100, expandableLayout.getOrCalculateExpandedHeight(0))
+ assertEquals(100, expandableLayout.expandedHeight)
+
+ doReturn(200).`when`(wrappedView).measuredHeight
+ assertEquals(100, expandableLayout.getOrCalculateExpandedHeight(0))
+ assertEquals(100, expandableLayout.expandedHeight)
+ }
+
+ @Test
+ fun `GIVEN parentHeight not already calculated WHEN getOrCalculateExpandedHeight is called THEN it sets parentHeight with the value from the heightSpec size`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+
+ expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(123, View.MeasureSpec.EXACTLY))
+ assertEquals(123, expandableLayout.parentHeight)
+
+ expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(321, View.MeasureSpec.EXACTLY))
+ assertEquals(123, expandableLayout.parentHeight)
+ }
+
+ @Test
+ fun `GIVEN parentHeight not calculated WHEN getOrCalculateExpandedHeight is called with a parent height THEN it sets the expandedHeight as the minimum of between expandedHeight and parent height`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ expandableLayout.expandedHeight = 123
+
+ expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(101, View.MeasureSpec.EXACTLY))
+ assertEquals(101, expandableLayout.expandedHeight)
+
+ expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(222, View.MeasureSpec.EXACTLY))
+ assertEquals(101, expandableLayout.expandedHeight)
+ }
+
+ @Test
+ fun `GIVEN getOrCalculateExpandedHeight() WHEN calculating the collapsed height to be bigger than the available screen height THEN it cancels collapsing`() {
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { },
+ )
+ expandableLayout.collapsedHeight = 50
+ expandableLayout.expandedHeight = 100
+
+ var result = expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY))
+ assertEquals(100, result)
+ assertTrue(expandableLayout.isCollapsed)
+ assertFalse(expandableLayout.isExpandInProgress)
+
+ // Reset parent height. Simulate entirely new calculations to have the code passing an if check
+ expandableLayout.parentHeight = -1
+ expandableLayout.collapsedHeight = 1_000
+ result = expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY))
+ assertEquals(100, result)
+ assertFalse(expandableLayout.isCollapsed)
+ assertFalse(expandableLayout.isExpandInProgress)
+ }
+
+ @Test
+ fun `GIVEN a set touchSlop WHEN isScrollingUp calculates that is was exceeded THEN it returns true `() {
+ val expandableLayout = ExpandableLayout.wrapContentInExpandableView(
+ FrameLayout(testContext),
+ 1,
+ ) { }
+ expandableLayout.initialYCoord = 0f
+ expandableLayout.touchSlop = 10f
+
+ var distanceScrolledDown = 11f
+ assertFalse(expandableLayout.isScrollingUp(MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, distanceScrolledDown, 0)))
+ distanceScrolledDown = 5f
+ assertFalse(expandableLayout.isScrollingUp(MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, distanceScrolledDown, 0)))
+
+ var distanceScrolledUp = -11f
+ assertTrue(expandableLayout.isScrollingUp(MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, distanceScrolledUp, 0)))
+ distanceScrolledUp = -5f
+ assertFalse(expandableLayout.isScrollingUp(MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, distanceScrolledUp, 0)))
+ }
+
+ @Test
+ fun `GIVEN a list of items WHEN calculateCollapsedHeight is called with an item index not found in the items list THEN it returns the value of measuredHeight`() {
+ val list = RecyclerView(testContext).apply {
+ layoutManager = mock()
+ addView(mock(), mock<RecyclerView.LayoutParams>())
+ addView(mock(), mock<RecyclerView.LayoutParams>())
+ }
+ val wrappedView = FrameLayout(testContext).apply { addView(list) }
+ val measuredHeight = -42
+ val expandableLayout = spy(
+ ExpandableLayout.wrapContentInExpandableView(
+ wrappedView,
+ 0,
+ ) { },
+ )
+
+ doReturn(measuredHeight).`when`(expandableLayout).measuredHeight
+
+ doReturn(0).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), anyInt())
+ assertEquals(measuredHeight, expandableLayout.calculateCollapsedHeight())
+
+ // Here we test the list of two items collapsed to the first.
+ expandableLayout.lastVisibleItemIndexWhenCollapsed = 1
+ doReturn(1).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), anyInt())
+ assertEquals(0, expandableLayout.calculateCollapsedHeight())
+
+ expandableLayout.lastVisibleItemIndexWhenCollapsed = 2
+ doReturn(2).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), anyInt())
+ assertEquals(measuredHeight, expandableLayout.calculateCollapsedHeight())
+ }
+
+ @Test
+ fun `GIVEN calculateCollapsedHeight() WHEN called without a sticky footer index THEN it returns the distance between parent top and half of the SpecialView`() {
+ val viewHeightForEachProperty = 1_000
+ val listHeightForEachProperty = 100
+ val itemHeightForEachProperty = 10
+ val layoutManager = mock<RecyclerView.LayoutManager>()
+ // Although correctly set below (in configureWithHeight) the params get overwritten in RecyclerView when adding items
+ // So we need to fake RecyclerView's LayoutParams response.
+ val layoutParams = mock<RecyclerView.LayoutParams>()
+ .also { it.configureMarginResponse(itemHeightForEachProperty) }
+ doReturn(layoutParams).`when`(layoutManager).generateLayoutParams(any())
+ // Adding Views and creating spies in two stages because otherwise
+ // the addView call for a spy will not get us the expected result.
+ var list = RecyclerView(testContext).apply {
+ this.layoutManager = layoutManager
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ }
+ list = spy(list).configureWithHeight(listHeightForEachProperty)
+ var wrappedView = FrameLayout(testContext).apply { addView(list) }
+ wrappedView = spy(wrappedView).configureWithHeight(viewHeightForEachProperty)
+ val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(wrappedView, 1) { })
+ doReturn(1).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), eq(1))
+
+ val result = expandableLayout.calculateCollapsedHeight()
+
+ var expected = 0
+ expected += viewHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
+ expected += listHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
+ expected += itemHeightForEachProperty * 3 // height + marginTop + marginBottom for the top item shown in entirety
+ expected += itemHeightForEachProperty // marginTop for the special view
+ expected += itemHeightForEachProperty / 2 // as per the specs, show only half of the special view
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `GIVEN calculateCollapsedHeight() WHEN called with a sticky footer index THEN it returns the distance between parent top and half of the SpecialView + height of sticky`() {
+ val viewHeightForEachProperty = 1_000
+ val listHeightForEachProperty = 100
+ val itemHeightForEachProperty = 10
+ val layoutManager = mock<RecyclerView.LayoutManager>()
+ // Although correctly set below (in configureWithHeight) the params get overwritten in RecyclerView when adding items
+ // So we need to fake RecyclerView's LayoutParams response.
+ val layoutParams = mock<RecyclerView.LayoutParams>()
+ .also { it.configureMarginResponse(itemHeightForEachProperty) }
+ doReturn(layoutParams).`when`(layoutManager).generateLayoutParams(any())
+ // Adding Views and creating spies in two stages because otherwise
+ // the addView call for a spy will not get us the expected result.
+ var list = RecyclerView(testContext).apply {
+ this.layoutManager = layoutManager
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ }
+ list = spy(list).configureWithHeight(listHeightForEachProperty)
+ var wrappedView = FrameLayout(testContext).apply { addView(list) }
+ wrappedView = spy(wrappedView).configureWithHeight(viewHeightForEachProperty)
+ val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(wrappedView, 1, 2) { })
+ doReturn(1).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), eq(1))
+ doReturn(2).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), eq(2))
+
+ val result = expandableLayout.calculateCollapsedHeight()
+
+ var expected = 0
+ expected += viewHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
+ expected += listHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
+ expected += itemHeightForEachProperty * 3 // height + marginTop + marginBottom for the top item shown in entirety
+ expected += itemHeightForEachProperty // marginTop for the special view
+ expected += itemHeightForEachProperty / 2 // as per the specs, show only half of the special view
+ expected += itemHeightForEachProperty // height of the sticky item
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `GIVEN calculateCollapsedHeight() WHEN called the same item as limit and sticky THEN it returns the distance between parent top and bottom of sticky`() {
+ val viewHeightForEachProperty = 1_000
+ val listHeightForEachProperty = 100
+ val itemHeightForEachProperty = 10
+ val layoutManager = mock<RecyclerView.LayoutManager>()
+ // Although correctly set below (in configureWithHeight) the params get overwritten in RecyclerView when adding items
+ // So we need to fake RecyclerView's LayoutParams response.
+ val layoutParams = mock<RecyclerView.LayoutParams>()
+ .also { it.configureMarginResponse(itemHeightForEachProperty) }
+ doReturn(layoutParams).`when`(layoutManager).generateLayoutParams(any())
+ // Adding Views and creating spies in two stages because otherwise
+ // the addView call for a spy will not get us the expected result.
+ var list = RecyclerView(testContext).apply {
+ this.layoutManager = layoutManager
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
+ }
+ list = spy(list).configureWithHeight(listHeightForEachProperty)
+ var wrappedView = FrameLayout(testContext).apply { addView(list) }
+ wrappedView = spy(wrappedView).configureWithHeight(viewHeightForEachProperty)
+ val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(wrappedView, 1, 1) { })
+ doReturn(1).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), eq(1))
+
+ val result = expandableLayout.calculateCollapsedHeight()
+
+ var expected = 0
+ expected += viewHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
+ expected += listHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
+ expected += itemHeightForEachProperty * 3 // height + marginTop + marginBottom for the top item shown in entirety
+ expected += itemHeightForEachProperty // marginTop for the special view
+ expected += itemHeightForEachProperty // height of the sticky item
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `GIVEN a RecyclerView with a set Adapter WHEN getChildPositionForAdapterIndex is called THEN it returns the list position of the item in Adapter`() {
+ val layoutManager = mock<RecyclerView.LayoutManager>()
+ // Although correctly set below (in configureWithHeight) the params get overwritten in RecyclerView when adding items
+ // So we need to fake RecyclerView's LayoutParams response.
+ val layoutParams = mock<RecyclerView.LayoutParams>()
+ doReturn(layoutParams).`when`(layoutManager).generateLayoutParams(any())
+ val list = spy(
+ RecyclerView(testContext).apply {
+ this.layoutManager = layoutManager
+ addView(View(testContext).apply { setLayoutParams(ViewGroup.LayoutParams(10, 10)) })
+ addView(View(testContext).apply { setLayoutParams(ViewGroup.LayoutParams(10, 10)) })
+ addView(View(testContext).apply { setLayoutParams(ViewGroup.LayoutParams(10, 10)) })
+ },
+ )
+ val wrappedView = FrameLayout(testContext).apply { addView(list) }
+ val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(wrappedView))
+ doReturn(3).`when`(list).getChildAdapterPosition(any())
+
+ assertEquals(-1, expandableLayout.getChildPositionForAdapterIndex(list, 2))
+ // We'll get a match based on the above "doReturn().." and adapterIndex: 3 for the first child.
+ assertEquals(0, expandableLayout.getChildPositionForAdapterIndex(list, 3))
+ }
+}
+
+/**
+ * Convenience method to set the same value - the received [height] parameter as the
+ * height, margins and paddings values for the current View.
+ */
+fun <V> V.configureWithHeight(height: Int): V where V : View {
+ doReturn(height).`when`(this).measuredHeight
+ layoutParams = ViewGroup.MarginLayoutParams(height, height).apply {
+ setMargins(height, height, height, height)
+ }
+ setPadding(height, height, height, height)
+
+ return this
+}
+
+/**
+ * Convenience method for setting the [margin] value to all LayoutParams margins.
+ */
+fun <T> T.configureMarginResponse(margin: Int) where T : ViewGroup.MarginLayoutParams {
+ this.topMargin = margin
+ this.rightMargin = margin
+ this.bottomMargin = margin
+ this.leftMargin = margin
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemLayoutManager.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemLayoutManager.kt
new file mode 100644
index 0000000000..50f87d17d9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemLayoutManager.kt
@@ -0,0 +1,32 @@
+/* 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.browser.menu.view
+
+import android.content.Context
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * A default implementation of a the abstract [StickyItemsLinearLayoutManager] to be used in tests.
+ */
+open class FakeStickyItemLayoutManager<T> constructor(
+ context: Context,
+ internal val stickyItemPlacement: StickyItemPlacement = StickyItemPlacement.TOP,
+ reverseLayout: Boolean = false,
+) : StickyItemsLinearLayoutManager<T>(
+ context,
+ stickyItemPlacement,
+ reverseLayout,
+) where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter {
+ override fun scrollToIndicatedPositionWithOffset(
+ position: Int,
+ offset: Int,
+ actuallyScrollToPositionWithOffset: (Int, Int) -> Unit,
+ ) { }
+
+ override fun shouldStickyItemBeShownForCurrentPosition() = true
+
+ override fun getY(itemView: View) = 0f
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemsAdapter.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemsAdapter.kt
new file mode 100644
index 0000000000..d2187baad3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemsAdapter.kt
@@ -0,0 +1,23 @@
+/* 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.browser.menu.view
+
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.support.test.mock
+
+/**
+ * A default implementation of [StickyItemsAdapter] to be used in tests.
+ */
+class FakeStickyItemsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>(), StickyItemsAdapter {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
+ mock()
+
+ override fun getItemCount(): Int = 42
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
+
+ override fun isStickyItem(position: Int): Boolean = false
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/MenuButtonTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/MenuButtonTest.kt
new file mode 100644
index 0000000000..3d1b74b49a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/MenuButtonTest.kt
@@ -0,0 +1,172 @@
+/* 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.browser.menu.view
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.widget.ImageView
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+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.anyBoolean
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MenuButtonTest {
+ private lateinit var menuController: MenuController
+ private lateinit var menuBuilder: BrowserMenuBuilder
+ private lateinit var menu: BrowserMenu
+ private lateinit var menuButton: MenuButton
+ private lateinit var menuIcon: ImageView
+ private lateinit var highlightView: ImageView
+ private lateinit var notificationIconView: ImageView
+
+ @Before
+ fun setup() {
+ menuController = mock()
+ menu = mock()
+ menuBuilder = mock()
+ doReturn(menu).`when`(menuBuilder).build(testContext)
+
+ menuButton = MenuButton(testContext)
+ val images = menuButton.children.mapNotNull { it as? AppCompatImageView }.toList()
+ highlightView = images[0]
+ menuIcon = images[1]
+ notificationIconView = images[2]
+ }
+
+ @Test
+ fun `changing menu controller dismisses old menu`() {
+ menuButton.menuController = menuController
+ menuButton.performClick()
+
+ verify(menuController).show(menuButton)
+
+ menuButton.menuController = mock()
+ verify(menuController).dismiss()
+ }
+
+ @Test
+ fun `changing menu builder dismisses old menu`() {
+ menuButton.menuBuilder = menuBuilder
+ menuButton.performClick()
+
+ verify(menu).show(eq(menuButton), any(), any(), anyBoolean(), any())
+
+ menuButton.menuBuilder = mock()
+ verify(menu).dismiss()
+ }
+
+ @Test
+ fun `opening a new menu will prefer using the controller`() {
+ menuButton.menuController = menuController
+ menuButton.menuBuilder = menuBuilder
+
+ menuButton.performClick()
+
+ verify(menuController).show(menuButton)
+ verify(menuBuilder, never()).build(testContext)
+ verify(menu, never()).show(any(), any(), any(), anyBoolean(), any())
+ }
+
+ @Test
+ fun `trying to open a new menu when we already have one will dismiss the current`() {
+ menuButton.menuBuilder = menuBuilder
+
+ menuButton.performClick()
+ menuButton.performClick()
+
+ verify(menu, times(1)).show(eq(menuButton), any(), any(), anyBoolean(), any())
+ verify(menu, times(1)).dismiss()
+ }
+
+ @Test
+ fun `icon has content description`() {
+ assertEquals("Menu", menuIcon.contentDescription)
+ assertNotNull(menuIcon.drawable)
+ }
+
+ @Test
+ fun `icon color filter can be changed`() {
+ assertNull(menuIcon.colorFilter)
+
+ menuButton.setColorFilter(0xffffff)
+ assertEquals(PorterDuffColorFilter(0xffffff, PorterDuff.Mode.SRC_ATOP), menuIcon.colorFilter)
+ }
+
+ @Test
+ fun `icon can invalidate menu`() {
+ menuButton.menuBuilder = menuBuilder
+ menuButton.performClick()
+
+ verify(menu).show(eq(menuButton), any(), any(), anyBoolean(), any())
+
+ menuButton.invalidateBrowserMenu()
+ verify(menu).invalidate()
+ }
+
+ @Test
+ fun `icon displays high priority highlight`() {
+ assertFalse(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ menuButton.setHighlight(
+ BrowserMenuHighlight.HighPriority(Color.RED),
+ )
+
+ assertTrue(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ assertEquals(ColorStateList.valueOf(Color.RED), highlightView.imageTintList)
+ }
+
+ @Test
+ fun `icon displays low priority highlight`() {
+ assertFalse(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ menuButton.setHighlight(
+ BrowserMenuHighlight.LowPriority(Color.BLUE),
+ )
+
+ assertFalse(highlightView.isVisible)
+ assertTrue(notificationIconView.isVisible)
+
+ assertEquals(PorterDuffColorFilter(Color.BLUE, PorterDuff.Mode.SRC_ATOP), notificationIconView.colorFilter)
+ }
+
+ @Test
+ fun `menu can be dismissed`() {
+ menuButton.menuController = menuController
+ menuButton.menu = menu
+
+ menuButton.dismissMenu()
+
+ verify(menuButton.menuController)?.dismiss()
+ verify(menuButton.menu)?.dismiss()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManagerTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManagerTest.kt
new file mode 100644
index 0000000000..f4e095d7dd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManagerTest.kt
@@ -0,0 +1,189 @@
+/* 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.browser.menu.view
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.support.test.mock
+import org.junit.Assert
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class StickyFooterLinearLayoutManagerTest {
+ private lateinit var manager: StickyFooterLinearLayoutManager<FakeStickyItemsAdapter>
+
+ @Before
+ fun setup() {
+ manager = StickyFooterLinearLayoutManager(mock(), false)
+ }
+
+ @Test
+ fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position smaller than stickyItemPosition THEN will scroll to after that`() {
+ manager.stickyItemPosition = 5
+ var positionToScrollResult = -1
+ var offsetToScrollResult = -1
+ val scrollCallback: (Int, Int) -> Unit = { position, offset ->
+ positionToScrollResult = position
+ offsetToScrollResult = offset
+ }
+
+ manager.scrollToIndicatedPositionWithOffset(4, 22, scrollCallback)
+ Assert.assertEquals(5, positionToScrollResult)
+ Assert.assertEquals(22, offsetToScrollResult)
+
+ manager.scrollToIndicatedPositionWithOffset(0, 22, scrollCallback)
+ Assert.assertEquals(1, positionToScrollResult)
+ Assert.assertEquals(22, offsetToScrollResult)
+ }
+
+ @Test
+ fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position smaller than stickyItemPosition which is displayed THEN will scroll to that`() {
+ manager = spy(manager)
+ doReturn(mock<View>()).`when`(manager).getChildAt(ArgumentMatchers.anyInt())
+ manager.stickyItemPosition = 5
+ var positionToScrollResult = -1
+ var offsetToScrollResult = -1
+ val scrollCallback: (Int, Int) -> Unit = { position, offset ->
+ positionToScrollResult = position
+ offsetToScrollResult = offset
+ }
+
+ manager.scrollToIndicatedPositionWithOffset(4, 22, scrollCallback)
+ Assert.assertEquals(4, positionToScrollResult)
+ Assert.assertEquals(22, offsetToScrollResult)
+
+ manager.scrollToIndicatedPositionWithOffset(0, 22, scrollCallback)
+ Assert.assertEquals(0, positionToScrollResult)
+ Assert.assertEquals(22, offsetToScrollResult)
+ }
+
+ @Test
+ fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position equal to stickyItemPosition THEN will scroll to that position`() {
+ manager.stickyItemPosition = 6
+ var positionToScrollResult = -1
+ var offsetToScrollResult = -1
+ val scrollCallback: (Int, Int) -> Unit = { position, offset ->
+ positionToScrollResult = position
+ offsetToScrollResult = offset
+ }
+
+ manager.scrollToIndicatedPositionWithOffset(6, 22, scrollCallback)
+ Assert.assertEquals(6, positionToScrollResult)
+ Assert.assertEquals(22, offsetToScrollResult)
+ }
+
+ @Test
+ fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position bigger than stickyItemPosition THEN will scroll to that position`() {
+ manager.stickyItemPosition = 6
+ var positionToScrollResult = -1
+ var offsetToScrollResult = -1
+ val scrollCallback: (Int, Int) -> Unit = { position, offset ->
+ positionToScrollResult = position
+ offsetToScrollResult = offset
+ }
+
+ manager.scrollToIndicatedPositionWithOffset(7, 22, scrollCallback)
+ Assert.assertEquals(7, positionToScrollResult)
+ Assert.assertEquals(22, offsetToScrollResult)
+
+ manager.scrollToIndicatedPositionWithOffset(10, 22, scrollCallback)
+ Assert.assertEquals(10, positionToScrollResult)
+ Assert.assertEquals(22, offsetToScrollResult)
+
+ // Negative positions are handled by Android'd LayoutManager. We should pass any to it.
+ manager.scrollToIndicatedPositionWithOffset(3333, 22, scrollCallback)
+ Assert.assertEquals(3333, positionToScrollResult)
+ Assert.assertEquals(22, offsetToScrollResult)
+ }
+
+ @Test
+ fun `GIVEN stickyItemPosition not set WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns false`() {
+ manager.stickyItemPosition = RecyclerView.NO_POSITION
+
+ Assert.assertFalse(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun `GIVEN sticky item shown WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it checks the item above the sticky one`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 5
+ manager.stickyItemView = mock()
+ doReturn(10).`when`(manager).childCount
+
+ manager.shouldStickyItemBeShownForCurrentPosition()
+
+ verify(manager).getAdapterPositionForItemIndex(8)
+ }
+
+ @Test
+ fun `GIVEN sticky item not shown WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it checks the bottom most item`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 5
+ doReturn(10).`when`(manager).childCount
+
+ manager.shouldStickyItemBeShownForCurrentPosition()
+
+ verify(manager).getAdapterPositionForItemIndex(9)
+ }
+
+ @Test
+ fun ` GIVEN sticky item being the last shown item WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 5
+ doReturn(5).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt())
+
+ assertTrue(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun ` GIVEN sticky item being scrolled upwards from the bottom WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns false`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 5
+
+ doReturn(6).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt())
+ assertFalse(manager.shouldStickyItemBeShownForCurrentPosition())
+
+ doReturn(60).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt())
+ assertFalse(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun ` GIVEN sticky item being scrolled downwards offscreen WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 5
+
+ doReturn(4).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt())
+ assertTrue(manager.shouldStickyItemBeShownForCurrentPosition())
+
+ doReturn(0).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt())
+ assertTrue(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun `GIVEN a default layout menu WHEN getY is called THEN it returns the translation needed to push the sticky item to the top`() {
+ manager = spy(manager)
+ val stickyView: View = mock()
+ doReturn(100).`when`(manager).height
+ doReturn(33).`when`(stickyView).height
+
+ Assert.assertEquals(67f, manager.getY(stickyView))
+ }
+
+ @Test
+ fun `GIVEN a reverseLayout menu WHEN getY is called THEN it returns 0 as the translation to be set for the sticky item`() {
+ manager = spy(StickyFooterLinearLayoutManager(mock(), true))
+ doReturn(100).`when`(manager).height
+ val stickyView: View = mock()
+ doReturn(33).`when`(stickyView).height
+
+ Assert.assertEquals(0f, manager.getY(stickyView))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManagerTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManagerTest.kt
new file mode 100644
index 0000000000..897a8ca8c1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManagerTest.kt
@@ -0,0 +1,155 @@
+/* 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.browser.menu.view
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+
+class StickyHeaderLinearLayoutManagerTest {
+ private lateinit var manager: StickyHeaderLinearLayoutManager<FakeStickyItemsAdapter>
+
+ @Before
+ fun setup() {
+ manager = StickyHeaderLinearLayoutManager(mock(), false)
+ }
+
+ @Test
+ fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position bigger than stickyItemPosition THEN will scroll to before that`() {
+ manager.stickyItemPosition = 5
+ var positionToScrollResult = -1
+ var offsetToScrollResult = -1
+ val scrollCallback: (Int, Int) -> Unit = { position, offset ->
+ positionToScrollResult = position
+ offsetToScrollResult = offset
+ }
+
+ manager.scrollToIndicatedPositionWithOffset(6, 22, scrollCallback)
+ assertEquals(5, positionToScrollResult)
+ assertEquals(22, offsetToScrollResult)
+
+ manager.scrollToIndicatedPositionWithOffset(10, 22, scrollCallback)
+ assertEquals(9, positionToScrollResult)
+ assertEquals(22, offsetToScrollResult)
+ }
+
+ @Test
+ fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position equal to stickyItemPosition THEN will scroll to before that`() {
+ manager.stickyItemPosition = 6
+ var positionToScrollResult = -1
+ var offsetToScrollResult = -1
+ val scrollCallback: (Int, Int) -> Unit = { position, offset ->
+ positionToScrollResult = position
+ offsetToScrollResult = offset
+ }
+
+ manager.scrollToIndicatedPositionWithOffset(6, 22, scrollCallback)
+ assertEquals(5, positionToScrollResult)
+ assertEquals(22, offsetToScrollResult)
+ }
+
+ @Test
+ fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position smaller than stickyItemPosition THEN will scroll to that position`() {
+ manager.stickyItemPosition = 6
+ var positionToScrollResult = -1
+ var offsetToScrollResult = -1
+ val scrollCallback: (Int, Int) -> Unit = { position, offset ->
+ positionToScrollResult = position
+ offsetToScrollResult = offset
+ }
+
+ manager.scrollToIndicatedPositionWithOffset(5, 22, scrollCallback)
+ assertEquals(5, positionToScrollResult)
+ assertEquals(22, offsetToScrollResult)
+
+ manager.scrollToIndicatedPositionWithOffset(0, 22, scrollCallback)
+ assertEquals(0, positionToScrollResult)
+ assertEquals(22, offsetToScrollResult)
+
+ // Negative positions are handled by Android'd LayoutManager. We should pass any to it.
+ manager.scrollToIndicatedPositionWithOffset(-33, 22, scrollCallback)
+ assertEquals(-33, positionToScrollResult)
+ assertEquals(22, offsetToScrollResult)
+ }
+
+ @Test
+ fun `GIVEN stickyItemPosition not set WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns false`() {
+ manager.stickyItemPosition = RecyclerView.NO_POSITION
+
+ assertFalse(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun `GIVEN the top item is the sticky one WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 3
+ doReturn(3).`when`(manager).getAdapterPositionForItemIndex(0)
+
+ assertTrue(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun `GIVEN items below the sticky item are scrolled upwards offscreen WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 3
+
+ doReturn(4).`when`(manager).getAdapterPositionForItemIndex(0)
+ assertTrue(manager.shouldStickyItemBeShownForCurrentPosition())
+
+ doReturn(5).`when`(manager).getAdapterPositionForItemIndex(0)
+ assertTrue(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun `GIVEN items above the sticky item shown at top but ofsetted offscreen WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 3
+ doReturn(1).`when`(manager).getAdapterPositionForItemIndex(0)
+ val topMostItem: View = mock()
+ doReturn(topMostItem).`when`(manager).getChildAt(0)
+
+ doReturn(0).`when`(topMostItem).bottom
+ assertTrue(manager.shouldStickyItemBeShownForCurrentPosition())
+
+ doReturn(-5).`when`(topMostItem).bottom
+ assertTrue(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun `GIVEN the sticky item is shown below the top of the list WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns false`() {
+ val manager = spy(manager)
+ manager.stickyItemPosition = 3
+ doReturn(1).`when`(manager).getAdapterPositionForItemIndex(0)
+
+ assertFalse(manager.shouldStickyItemBeShownForCurrentPosition())
+ }
+
+ @Test
+ fun `GIVEN a default layout menu WHEN getY is called THEN it returns 0 as the translation to be set for the sticky item`() {
+ manager = spy(manager)
+ val stickyView: View = mock()
+ doReturn(100).`when`(manager).height
+ doReturn(33).`when`(stickyView).height
+
+ assertEquals(0f, manager.getY(stickyView))
+ }
+
+ @Test
+ fun `GIVEN a reverseLayout menu WHEN getY is called THEN it returns the translation needed to push the sticky item to the top`() {
+ manager = spy(StickyHeaderLinearLayoutManager(mock(), true))
+ doReturn(100).`when`(manager).height
+ val stickyView: View = mock()
+ doReturn(33).`when`(stickyView).height
+
+ assertEquals(67f, manager.getY(stickyView))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyItemsLinearLayoutManagerTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyItemsLinearLayoutManagerTest.kt
new file mode 100644
index 0000000000..5e7d00314f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyItemsLinearLayoutManagerTest.kt
@@ -0,0 +1,631 @@
+/* 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.browser.menu.view
+
+import android.graphics.PointF
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager.INVALID_OFFSET
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class StickyItemsLinearLayoutManagerTest {
+ // For shorter test names "StickyItemsLinearLayoutManager" is referred to as SILLM.
+
+ private lateinit var manager: FakeStickyItemLayoutManager<FakeStickyItemsAdapter>
+
+ @Before
+ fun setup() {
+ manager = FakeStickyItemLayoutManager(mock())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN a new instance is contructed THEN it has specific default values`() {
+ assertEquals(RecyclerView.NO_POSITION, manager.stickyItemPosition)
+ assertEquals(RecyclerView.NO_POSITION, manager.scrollPosition)
+ assertEquals(0, manager.scrollOffset)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN onAttachedToWindow called THEN it calls super and sets the new adapter`() {
+ manager = spy(manager)
+ val list = Mockito.mock(RecyclerView::class.java)
+
+ manager.onAttachedToWindow(list)
+
+ verify(manager).setAdapter(list.adapter)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN onSaveInstanceState called THEN it returns a new SavedState with the scroll data`() {
+ manager.scrollPosition = 42
+ manager.scrollOffset = 422
+
+ val result: SavedState = manager.onSaveInstanceState() as SavedState
+
+ assertTrue(result.superState is LinearLayoutManager.SavedState)
+ assertEquals(42, result.scrollPosition)
+ assertEquals(422, result.scrollOffset)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN onRestoreInstanceState is called with a new state THEN it updates scrollPosition and scrollOffset`() {
+ val newState = SavedState(
+ null,
+ scrollPosition = 222,
+ scrollOffset = 221,
+ )
+
+ manager.onRestoreInstanceState(newState)
+
+ assertEquals(222, manager.scrollPosition)
+ assertEquals(221, manager.scrollOffset)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN onRestoreInstanceState is called with a null state THEN scrollPosition and scrollOffset are left unchanged`() {
+ manager.onRestoreInstanceState(null)
+
+ assertEquals(RecyclerView.NO_POSITION, manager.scrollPosition)
+ assertEquals(0, manager.scrollOffset)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN onLayoutChildren is called while in preLayout THEN it execute the super with the sticky item detached and not updates stickyItem`() {
+ manager = spy(manager)
+ val listState: RecyclerView.State = mock()
+ doReturn(true).`when`(listState).isPreLayout
+
+ manager.onLayoutChildren(mock(), listState)
+
+ verify(manager).restoreView<Unit>(any())
+ verify(manager, never()).updateStickyItem(any(), ArgumentMatchers.anyBoolean())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN onLayoutChildren is called while not in preLayout THEN it execute the super with the sticky item detached and updates stickyItem`() {
+ manager = spy(manager)
+ val recycler: RecyclerView.Recycler = mock()
+ val listState: RecyclerView.State = mock()
+ doReturn(false).`when`(listState).isPreLayout
+ // Prevent side effects following the "manager.onLayoutChildren" call
+ doReturn(false).`when`(manager).shouldStickyItemBeShownForCurrentPosition()
+
+ manager.onLayoutChildren(recycler, listState)
+
+ verify(manager).restoreView<Unit>(any())
+ verify(manager).updateStickyItem(recycler, true)
+ }
+
+ @Test
+ fun `GIVEN A SILLM WHEN scrollVerticallyBy is called THEN it detaches the sticky item to scroll using parent and not updates the sticky item`() {
+ manager = spy(manager)
+
+ val result = manager.scrollVerticallyBy(0, mock(), mock())
+
+ verify(manager).restoreView<Int>(any())
+ verify(manager, never()).updateStickyItem(any(), ArgumentMatchers.anyBoolean())
+ assertEquals(0, result)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN findLastVisibleItemPosition is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.findLastVisibleItemPosition()
+
+ verify(manager).restoreView<Int>(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN findFirstVisibleItemPosition is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.findFirstVisibleItemPosition()
+
+ verify(manager).restoreView<Int>(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN findFirstCompletelyVisibleItemPosition is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.findFirstCompletelyVisibleItemPosition()
+
+ verify(manager).restoreView<Int>(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN findLastCompletelyVisibleItemPosition is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.findLastCompletelyVisibleItemPosition()
+
+ verify(manager).restoreView<Int>(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN computeVerticalScrollExtent is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.computeVerticalScrollExtent(mock())
+
+ verify(manager).restoreView<Int>(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN computeVerticalScrollOffset is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.computeVerticalScrollOffset(mock())
+
+ verify(manager).restoreView<Int>(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN computeVerticalScrollRange is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.computeVerticalScrollRange(mock())
+
+ verify(manager).restoreView<Int>(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN computeScrollVectorForPosition is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.computeScrollVectorForPosition(33)
+
+ verify(manager).restoreView<PointF>(any())
+ }
+
+ @Test
+ fun `GIVEN sticky item is null WHEN scrollToPosition is called THEN scrollToPositionWithOffset is not called`() {
+ manager = spy(manager)
+
+ manager.scrollToPosition(32)
+
+ verify(manager, never()).scrollToPositionWithOffset(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
+ }
+
+ @Test
+ fun `GIVEN sticky item is not null WHEN scrollToPosition is called THEN it calls scrollToPositionWithOffset with INVALID_OFFSET`() {
+ manager = spy(manager)
+ manager.stickyItemView = mock()
+
+ manager.scrollToPosition(32)
+
+ verify(manager).scrollToPositionWithOffset(32, INVALID_OFFSET)
+ }
+
+ @Test
+ fun `GIVEN sticky item is not null WHEN scrollToPositionWithOffset is called THEN scrollToIndicatedPositionWithOffset is delegated`() {
+ manager = spy(manager)
+ manager.stickyItemView = mock()
+
+ manager.scrollToPositionWithOffset(23, 9)
+
+ verify(manager).setScrollState(RecyclerView.NO_POSITION, INVALID_OFFSET)
+ verify(manager).scrollToIndicatedPositionWithOffset(eq(23), eq(9), any())
+ verify(manager).setScrollState(23, 9)
+ }
+
+ @Test
+ fun `GIVEN sticky item is null WHEN onFocusSearchFailed is called THEN it detaches the sticky item to call the super method`() {
+ manager = spy(manager)
+
+ manager.onFocusSearchFailed(mock(), 3, mock(), mock())
+
+ verify(manager).restoreView<View?>(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN getAdapterPositionForItemIndex is called with a index for which there is no bound view THEN it returns -1`() {
+ assertEquals(RecyclerView.NO_POSITION, manager.getAdapterPositionForItemIndex(22))
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN getAdapterPositionForItemIndex is called with a index of an existing view THEN it returns it's absoluteAdapterPosition`() {
+ manager = spy(manager)
+ val params: RecyclerView.LayoutParams = mock()
+ doReturn(7).`when`(params).absoluteAdapterPosition
+ val view: View = mock()
+ doReturn(params).`when`(view).layoutParams
+ doReturn(view).`when`(manager).getChildAt(ArgumentMatchers.anyInt())
+
+ assertEquals(7, manager.getAdapterPositionForItemIndex(22))
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN setAdapter is called with a null argument THEN the current adapter and stickyItem are set to null`() {
+ val initialAdapter = mock<FakeStickyItemsAdapter>()
+ manager.listAdapter = initialAdapter
+
+ manager.setAdapter(null)
+
+ verify(initialAdapter).unregisterAdapterDataObserver(manager.stickyItemPositionsObserver)
+ assertNull(manager.listAdapter)
+ assertNull(manager.stickyItemView)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN setAdapter is called with a new valid adapter THEN the current adapter is reset`() {
+ val initialAdapter: FakeStickyItemsAdapter = mock()
+ val newAdapter: FakeStickyItemsAdapter = mock()
+ manager.listAdapter = initialAdapter
+ manager.stickyItemPositionsObserver = spy(manager.stickyItemPositionsObserver)
+
+ manager.setAdapter(newAdapter)
+
+ verify(initialAdapter).unregisterAdapterDataObserver(manager.stickyItemPositionsObserver)
+ assertSame(newAdapter, manager.listAdapter)
+ verify(newAdapter).registerAdapterDataObserver(manager.stickyItemPositionsObserver)
+ verify(manager.stickyItemPositionsObserver).onChanged()
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN restoreView is called with a method parameter THEN the sticky item is detached, method executed, item reattached`() {
+ manager = spy(manager)
+ val stickyView: View = mock()
+ manager.stickyItemView = stickyView
+ doNothing().`when`(manager).detachView(any())
+ doNothing().`when`(manager).attachView(any())
+ val orderVerifier = Mockito.inOrder(manager)
+
+ val result = manager.restoreView { 3 }
+
+ orderVerifier.verify(manager).detachView(stickyView)
+ orderVerifier.verify(manager).attachView(stickyView)
+ assertEquals(3, result)
+ }
+
+ @Test
+ fun `GIVEN sticky item should not be shown WHEN updateStickyItem is called THEN the stickyItemView is recycled`() {
+ manager = spy(manager)
+ manager.stickyItemView = mock()
+ doReturn(false).`when`(manager).shouldStickyItemBeShownForCurrentPosition()
+ doNothing().`when`(manager).recycleStickyItem(any())
+ val recycler: RecyclerView.Recycler = mock()
+
+ manager.updateStickyItem(recycler, true)
+
+ verify(manager).recycleStickyItem(recycler)
+ }
+
+ @Test
+ fun `GIVEN sticky item should be shown and not exists WHEN updateStickyItem is called THEN a new stickyItemView is created`() {
+ manager = spy(manager)
+ doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition()
+ doReturn(0f).`when`(manager).getY(any())
+ manager.stickyItemPosition = 42
+ val recycler: RecyclerView.Recycler = mock()
+ doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt())
+
+ manager.updateStickyItem(recycler, false)
+
+ verify(manager).createStickyView(recycler, 42)
+ verify(manager, never()).recycleStickyItem(any())
+ }
+
+ @Test
+ fun `GIVEN sticky item should be shown and exists WHEN updateStickyItem is called THEN another stickyItemView is not created`() {
+ manager = spy(manager)
+ val stickyView: View = mock()
+ manager.stickyItemView = stickyView
+ doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition()
+ doReturn(0f).`when`(manager).getY(any())
+ manager.stickyItemPosition = 42
+ val recycler: RecyclerView.Recycler = mock()
+ doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt())
+
+ manager.updateStickyItem(recycler, false)
+
+ verify(manager, never()).createStickyView(any(), ArgumentMatchers.anyInt())
+ verify(manager, never()).recycleStickyItem(any())
+ }
+
+ @Test
+ fun `GIVEN sticky item should be shown WHEN updateStickyItem is called while layout THEN bindStickyItem is called`() {
+ manager = spy(manager)
+ val stickyView: View = mock()
+ manager.stickyItemView = stickyView
+ doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition()
+ doReturn(0f).`when`(manager).getY(any())
+ doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt())
+ doNothing().`when`(manager).bindStickyItem(any())
+
+ manager.updateStickyItem(mock(), true)
+
+ verify(manager).bindStickyItem(stickyView)
+ verify(manager, never()).recycleStickyItem(any())
+ }
+
+ @Test
+ fun `GIVEN sticky item should be shown WHEN updateStickyItem is called while not layout THEN bindStickyItem is not called`() {
+ manager = spy(manager)
+ val stickyView: View = mock()
+ manager.stickyItemView = stickyView
+ doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition()
+ doReturn(0f).`when`(manager).getY(any())
+ doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt())
+ doNothing().`when`(manager).bindStickyItem(any())
+
+ manager.updateStickyItem(mock(), false)
+
+ verify(manager, never()).bindStickyItem(any())
+ verify(manager, never()).recycleStickyItem(any())
+ }
+
+ @Test
+ fun `GIVEN sticky item should be shown and it's view exists WHEN updateStickyItem is called THEN the stickyItemView gets set a new Y translation`() {
+ manager = spy(manager)
+ val stickyView: View = mock()
+ manager.stickyItemView = stickyView
+ doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition()
+ doReturn(44f).`when`(manager).getY(any())
+ doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt())
+
+ manager.updateStickyItem(mock(), false)
+
+ verify(manager).getY(stickyView)
+ verify(stickyView).translationY = 44f
+ verify(manager, never()).recycleStickyItem(any())
+ }
+
+ @Test
+ fun `GIVEN SILLM WHEN createStickyView is called THEN a new View is created and cached in stickyItemView`() {
+ manager = spy(manager)
+ val adapter: FakeStickyItemsAdapter = mock()
+ manager.listAdapter = adapter
+ val recycler: RecyclerView.Recycler = mock()
+ val newStickyView: View = mock()
+ doReturn(newStickyView).`when`(recycler).getViewForPosition(ArgumentMatchers.anyInt())
+ doNothing().`when`(manager).addView(any())
+ doNothing().`when`(manager).measureAndLayout(any())
+ doNothing().`when`(manager).ignoreView(any())
+
+ manager.createStickyView(recycler, 22)
+
+ verify(adapter).setupStickyItem(newStickyView)
+ verify(manager).addView(newStickyView)
+ verify(manager).measureAndLayout(newStickyView)
+ verify(manager).ignoreView(newStickyView)
+ assertSame(newStickyView, manager.stickyItemView)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN bindStickyItem is called for a new View THEN the view is measured and layout`() {
+ manager = spy(manager)
+ val view: View = mock()
+ doNothing().`when`(manager).measureAndLayout(any())
+
+ manager.bindStickyItem(view)
+
+ verify(manager).measureAndLayout(view)
+ }
+
+ @Test
+ fun `GIVEN a pending scroll WHEN bindStickyItem is called for a new View THEN a OnGlobalLayoutListener is set`() {
+ manager = spy(manager)
+ manager.scrollPosition = 22
+ val view: View = mock()
+ val viewObserver: ViewTreeObserver = mock()
+ doReturn(viewObserver).`when`(view).viewTreeObserver
+ doNothing().`when`(manager).measureAndLayout(any())
+
+ manager.bindStickyItem(view)
+
+ verify(manager).measureAndLayout(view)
+ verify(viewObserver).addOnGlobalLayoutListener(any())
+ }
+
+ @Test
+ fun `GIVEN no pending scroll WHEN bindStickyItem is called for a new View THEN no OnGlobalLayoutListener is set`() {
+ manager = spy(manager)
+ val view: View = mock()
+ val viewObserver: ViewTreeObserver = mock()
+ doReturn(viewObserver).`when`(view).viewTreeObserver
+ doNothing().`when`(manager).measureAndLayout(any())
+
+ manager.bindStickyItem(view)
+
+ verify(manager).measureAndLayout(view)
+ verify(viewObserver, never()).addOnGlobalLayoutListener(any())
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN measureAndLayout is called for a new View THEN it is measured and layout`() {
+ manager = spy(manager)
+ val newView: View = mock()
+ doReturn(22).`when`(manager).paddingLeft
+ doReturn(33).`when`(manager).paddingRight
+ doReturn(100).`when`(manager).width
+ doReturn(112).`when`(newView).measuredHeight
+
+ doNothing().`when`(manager).measureChildWithMargins(any(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
+
+ manager.measureAndLayout(newView)
+
+ verify(manager).measureChildWithMargins(newView, 0, 0)
+ verify(newView).layout(22, 0, (100 - 33), 112)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN recycleStickyItem is called THEN the view holder is reset and allowed to be recycled`() {
+ manager = spy(manager)
+ val stickyView: View = mock()
+ manager.stickyItemView = stickyView
+ val adapter: FakeStickyItemsAdapter = mock()
+ manager.listAdapter = adapter
+ val recycler: RecyclerView.Recycler = mock()
+ val captor = argumentCaptor<View>()
+ doNothing().`when`(manager).stopIgnoringView(any())
+ doNothing().`when`(manager).removeView(any())
+
+ manager.recycleStickyItem(recycler)
+
+ verify(adapter).tearDownStickyItem(captor.capture())
+ verify(manager).stopIgnoringView(captor.value)
+ verify(manager).removeView(captor.value)
+ verify(recycler).recycleView(captor.value)
+ }
+
+ @Test
+ fun `GIVEN a SILLM WHEN is called with a new position and offset THEN they are cached in scrollPosition and scrollOffset properties`() {
+ manager.setScrollState(222, 333)
+
+ assertEquals(222, manager.scrollPosition)
+ assertEquals(333, manager.scrollOffset)
+ }
+
+ @Test
+ fun `GIVEN an ItemPositionsAdapterDataObserver WHEN onChanged is called THEN handleChange() is delegated`() {
+ val observer = spy(manager.stickyItemPositionsObserver)
+ manager.stickyItemPositionsObserver = observer
+
+ observer.onChanged()
+
+ verify(observer).handleChange()
+ }
+
+ @Test
+ fun `GIVEN an ItemPositionsAdapterDataObserver WHEN onItemRangeInserted is called THEN handleChange() is delegated`() {
+ val observer = spy(manager.stickyItemPositionsObserver)
+ manager.stickyItemPositionsObserver = observer
+
+ observer.onItemRangeInserted(22, 33)
+
+ verify(observer).handleChange()
+ }
+
+ @Test
+ fun `GIVEN an ItemPositionsAdapterDataObserver WHEN onItemRangeRemoved is called THEN handleChange() is delegated`() {
+ val observer = spy(manager.stickyItemPositionsObserver)
+ manager.stickyItemPositionsObserver = observer
+
+ observer.onItemRangeRemoved(22, 33)
+
+ verify(observer).handleChange()
+ }
+
+ @Test
+ fun `GIVEN an ItemPositionsAdapterDataObserver WHEN onItemRangeMoved is called THEN handleChange() is delegated`() {
+ val observer = spy(manager.stickyItemPositionsObserver)
+ manager.stickyItemPositionsObserver = observer
+
+ observer.onItemRangeMoved(11, 22, 33)
+
+ verify(observer).handleChange()
+ }
+
+ @Test
+ fun `GIVEN an ItemPositionsAdapterDataObserver WHEN handleChange is called THEN the sticky item is updated`() {
+ manager = spy(manager)
+ val adapter: FakeStickyItemsAdapter = mock()
+ manager.listAdapter = adapter
+ val stickyView: View = mock()
+ manager.stickyItemView = stickyView
+ val observer = spy(manager.ItemPositionsAdapterDataObserver())
+ manager.stickyItemPositionsObserver = observer
+ doReturn(23).`when`(observer).calculateNewStickyItemPosition(any())
+ doNothing().`when`(manager).recycleStickyItem(any())
+
+ observer.handleChange()
+
+ verify(observer).calculateNewStickyItemPosition(adapter)
+ verify(manager).recycleStickyItem(null)
+ }
+
+ @Test
+ fun `GIVEN an ItemPositionsAdapterDataObserver WHEN calculateNewStickyItemPosition is called for a top item the sticky position is first in adaptor`() {
+ manager = spy(FakeStickyItemLayoutManager(mock(), StickyItemPlacement.TOP))
+ val adapter: FakeStickyItemsAdapter = mock()
+ doReturn(true).`when`(adapter).isStickyItem(3)
+ doReturn(true).`when`(adapter).isStickyItem(5)
+ doReturn(10).`when`(manager).itemCount
+ manager.stickyItemPositionsObserver = manager.ItemPositionsAdapterDataObserver()
+
+ assertEquals(3, manager.stickyItemPositionsObserver.calculateNewStickyItemPosition(adapter))
+ }
+
+ @Test
+ fun `GIVEN an ItemPositionsAdapterDataObserver WHEN calculateNewStickyItemPosition is called for a bottom item the sticky position is last in adaptor`() {
+ manager = spy(FakeStickyItemLayoutManager(mock(), StickyItemPlacement.BOTTOM))
+ val adapter: FakeStickyItemsAdapter = mock()
+ doReturn(true).`when`(adapter).isStickyItem(3)
+ doReturn(true).`when`(adapter).isStickyItem(5)
+ doReturn(10).`when`(manager).itemCount
+ manager.stickyItemPositionsObserver = manager.ItemPositionsAdapterDataObserver()
+
+ assertEquals(5, manager.stickyItemPositionsObserver.calculateNewStickyItemPosition(adapter))
+ }
+
+ @Test
+ fun `WHEN get is called for a reversed StickyItemPlacement#TOP layout manager THEN a StickyHeaderLinearLayoutManager is returned`() {
+ val result = StickyItemsLinearLayoutManager.get<FakeStickyItemsAdapter>(
+ mock(),
+ StickyItemPlacement.TOP,
+ true,
+ )
+
+ assertTrue(result is StickyHeaderLinearLayoutManager)
+ assertTrue(result.reverseLayout)
+ }
+
+ @Test
+ fun `WHEN get is called for a not reversed StickyItemPlacement#TOP layout manager THEN a StickyHeaderLinearLayoutManager is returned`() {
+ val result = StickyItemsLinearLayoutManager.get<FakeStickyItemsAdapter>(
+ mock(),
+ StickyItemPlacement.TOP,
+ false,
+ )
+
+ assertTrue(result is StickyHeaderLinearLayoutManager)
+ assertFalse(result.reverseLayout)
+ }
+
+ @Test
+ fun `WHEN get is called for a reversed StickyItemPlacement#BOTTOM layout manager THEN a StickyFooterLinearLayoutManager is returned`() {
+ val result = StickyItemsLinearLayoutManager.get<FakeStickyItemsAdapter>(
+ mock(),
+ StickyItemPlacement.BOTTOM,
+ true,
+ )
+
+ assertTrue(result is StickyFooterLinearLayoutManager)
+ assertTrue(result.reverseLayout)
+ }
+
+ @Test
+ fun `WHEN get is called for a not reversed StickyItemPlacement#BOTTOM layout manager THEN a StickyFooterLinearLayoutManager is returned`() {
+ val result = StickyItemsLinearLayoutManager.get<FakeStickyItemsAdapter>(
+ mock(),
+ StickyItemPlacement.BOTTOM,
+ false,
+ )
+
+ assertTrue(result is StickyFooterLinearLayoutManager)
+ assertFalse(result.reverseLayout)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..49324d83c5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,3 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
+
diff --git a/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/robolectric.properties b/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/browser/menu/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/menu/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/browser/menu2/README.md b/mobile/android/android-components/components/browser/menu2/README.md
new file mode 100644
index 0000000000..dc1b129b65
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/README.md
@@ -0,0 +1,51 @@
+# [Android Components](../../../README.md) > Browser > Menu 2
+
+A generic menu with customizable items primarily for browser toolbars.
+
+This replaces the [browser-menu](../menu) component with a new API using immutable objects,
+designed to work well with [lib-state](../../lib/state).
+
+## Usage
+
+### 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:browser-menu2:{latest-version}"
+```
+
+### MenuController
+The menu controller is used to control the items in the menu as well as displaying the menu popup.
+
+Sample code can be found in [Sample Toolbar app](https://github.com/mozilla-mobile/android-components/tree/main/samples/toolbar).
+
+There are multiple properties that you customize of the browser menu by just adding them into your dimens.xml file.
+
+```xml
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!--Change how rounded the corners of the menu should be-->
+ <dimen name="mozac_browser_menu2_corner_radius" tools:ignore="UnusedResources">4dp</dimen>
+
+ <!--Change how much shadow the menu should have-->
+ <dimen name="mozac_browser_menu2_elevation" tools:ignore="UnusedResources">4dp</dimen>
+
+ <!--Change the width of the menu - can also be set in MenuController#show()-->
+ <dimen name="mozac_browser_menu2_width" tools:ignore="UnusedResources">250dp</dimen>
+
+ <!--Change the top and bottom padding of the menu-->
+ <dimen name="mozac_browser_menu2_padding_vertical" tools:ignore="UnusedResources">8dp</dimen>
+
+</resources>
+```
+
+Options displayed in the menu are configured by using a list of `MenuCandidate` objects.
+The list of options can be sent to the menu by calling `MenuController#submitList()`.
+To change the displayed options, simply call `submitList` again with a new list.
+
+## 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/browser/menu2/build.gradle b/mobile/android/android-components/components/browser/menu2/build.gradle
new file mode 100644
index 0000000000..ccb4e0bdd8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/build.gradle
@@ -0,0 +1,50 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.browser.menu2'
+}
+
+dependencies {
+ implementation project(':concept-menu')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_recyclerview
+ implementation ComponentsDependencies.androidx_cardview
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_coordinatorlayout
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/browser/menu2/lint.xml b/mobile/android/android-components/components/browser/menu2/lint.xml
new file mode 100644
index 0000000000..81bcc3bfb8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/lint.xml
@@ -0,0 +1,7 @@
+<?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/. -->
+<lint>
+ <issue id="Overdraw" severity="ignore" />
+</lint> \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/menu2/proguard-rules.pro b/mobile/android/android-components/components/browser/menu2/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/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/browser/menu2/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/menu2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1eccdee26a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt
new file mode 100644
index 0000000000..eaf1998661
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt
@@ -0,0 +1,182 @@
+/* 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.browser.menu2
+
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.PopupWindow
+import androidx.annotation.VisibleForTesting
+import androidx.core.widget.PopupWindowCompat
+import mozilla.components.browser.menu2.ext.MenuPositioningData
+import mozilla.components.browser.menu2.ext.inferMenuPositioningData
+import mozilla.components.browser.menu2.view.MenuView
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Orientation
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.ext.findNestedMenuCandidate
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+
+/**
+ * Controls a popup menu composed of MenuCandidate objects.
+ * @param visibleSide Sets the menu to open with either the start or end visible.
+ * @param style Custom styling for this menu controller.
+ */
+class BrowserMenuController(
+ private val visibleSide: Side = Side.START,
+ private val style: MenuStyle? = null,
+) : MenuController, Observable<MenuController.Observer> by ObserverRegistry() {
+
+ private var currentPopupInfo: PopupMenuInfo? = null
+ private var menuCandidates: List<MenuCandidate> = emptyList()
+
+ private val menuDismissListener = PopupWindow.OnDismissListener {
+ currentPopupInfo = null
+ notifyObservers { onDismiss() }
+ }
+
+ override fun show(
+ anchor: View,
+ orientation: Orientation?,
+ autoDismiss: Boolean,
+ ): PopupWindow {
+ // If the menu is already displayed do not display it again.
+ currentPopupInfo?.window?.let {
+ return it
+ }
+
+ val view = MenuView(anchor.context).apply {
+ // Show nested list if present, or the standard menu candidates list.
+ submitList(menuCandidates)
+ setVisibleSide(visibleSide)
+ style?.let { setStyle(it) }
+ }
+
+ if (autoDismiss) {
+ // Monitor for changes to the parent layout and dismiss pop up if displayed, else the menu
+ // could be displayed in the wrong position. For example, if the menu is displayed and the
+ // device orientation changes from portrait to landscape and vice versa.
+ anchor.rootView.addOnLayoutChangeListener { _, _, _, right, bottom, _, _, oldRight, oldBottom ->
+ if (bottom != oldBottom || right != oldRight) {
+ dismiss()
+ }
+ }
+ }
+
+ return MenuPopupWindow(view).apply {
+ view.onDismiss = ::dismiss
+ view.onReopenMenu = ::reopenMenu
+ setOnDismissListener(menuDismissListener)
+ inferMenuPositioningData(
+ containerView = view,
+ anchor = anchor,
+ style = style,
+ orientation = orientation,
+ )?.let {
+ displayPopup(it)
+ }
+ }.also {
+ currentPopupInfo = PopupMenuInfo(
+ window = it,
+ anchor = anchor,
+ orientation = orientation,
+ nested = null,
+ )
+ }
+ }
+
+ /**
+ * Re-opens the menu and displays the given nested list.
+ * No-op if the menu is not yet open.
+ */
+ private fun reopenMenu(nested: NestedMenuCandidate?) {
+ val info = currentPopupInfo ?: return
+ info.window.run {
+ // Dismiss silently
+ setOnDismissListener(null)
+ dismiss()
+ setOnDismissListener(menuDismissListener)
+
+ // Quickly remove the current list
+ view.submitList(null)
+ // Display the new nested list
+ view.submitList(nested?.subMenuItems ?: menuCandidates)
+ // Attempt tp reopen the menu
+ inferMenuPositioningData(
+ containerView = view,
+ anchor = info.anchor,
+ style = style,
+ orientation = info.orientation,
+ )?.let {
+ displayPopup(it)
+ }
+ }
+ currentPopupInfo = info.copy(nested = nested)
+ }
+
+ /**
+ * Dismiss the menu popup if the menu is visible.
+ */
+ override fun dismiss() {
+ currentPopupInfo?.window?.dismiss()
+ }
+
+ /**
+ * Changes the contents of the menu.
+ */
+ override fun submitList(list: List<MenuCandidate>) {
+ menuCandidates = list
+ val info = currentPopupInfo
+
+ // If menu is already open, update the displayed items
+ if (info != null) {
+ // If a nested menu is open, it should be displayed
+ val displayedItems = if (info.nested != null) {
+ list.findNestedMenuCandidate(info.nested.id)?.subMenuItems
+ } else {
+ list
+ }
+
+ // If the new menu is null, close & reopen the popup on the main list
+ if (displayedItems == null) {
+ // close & reopen popup
+ reopenMenu(nested = null)
+ } else {
+ info.window.view.submitList(displayedItems)
+ }
+ }
+
+ notifyObservers { onMenuListSubmit(list) }
+ }
+
+ private class MenuPopupWindow(
+ val view: MenuView,
+ ) : PopupWindow(view, WRAP_CONTENT, WRAP_CONTENT, true)
+
+ private data class PopupMenuInfo(
+ val window: MenuPopupWindow,
+ val anchor: View,
+ val orientation: Orientation?,
+ val nested: NestedMenuCandidate? = null,
+ )
+}
+
+/**
+ * Show a [PopupWindow] given the positioning data.
+ */
+@VisibleForTesting
+internal fun PopupWindow.displayPopup(positioningData: MenuPositioningData) {
+ inputMethodMode = PopupWindow.INPUT_METHOD_NOT_NEEDED
+
+ animationStyle = positioningData.animation
+ height = positioningData.containerHeight
+
+ PopupWindowCompat.setOverlapAnchor(this, true)
+ showAtLocation(positioningData.anchor, Gravity.NO_GRAVITY, positioningData.x, positioningData.y)
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt
new file mode 100644
index 0000000000..eb592656c2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt
@@ -0,0 +1,81 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.CompoundButton
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.icons.MenuIconAdapter
+import mozilla.components.browser.menu2.ext.applyBackgroundEffect
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate.ButtonType
+
+internal abstract class CompoundMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ private val dismiss: () -> Unit,
+) : MenuCandidateViewHolder<CompoundMenuCandidate>(itemView, inflater), CompoundButton.OnCheckedChangeListener {
+
+ private val layout = itemView as ConstraintLayout
+ private val compoundButton: CompoundButton = itemView.findViewById(R.id.label)
+ private val startIcon = MenuIconAdapter(layout, inflater, Side.START, dismiss)
+ private var onCheckedChangeListener: ((Boolean) -> Unit)? = null
+
+ override fun bind(newCandidate: CompoundMenuCandidate, oldCandidate: CompoundMenuCandidate?) {
+ super.bind(newCandidate, oldCandidate)
+ onCheckedChangeListener = newCandidate.onCheckedChange
+ compoundButton.text = newCandidate.text
+ startIcon.bind(newCandidate.start, oldCandidate?.start)
+ compoundButton.applyStyle(newCandidate.textStyle, oldCandidate?.textStyle)
+ itemView.applyBackgroundEffect(newCandidate.effect, oldCandidate?.effect)
+
+ // isChecked calls the listener automatically
+ compoundButton.setOnCheckedChangeListener(null)
+ compoundButton.isChecked = newCandidate.isChecked
+ compoundButton.setOnCheckedChangeListener(this)
+ }
+
+ override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
+ onCheckedChangeListener?.invoke(isChecked)
+ dismiss()
+ }
+
+ companion object {
+ @LayoutRes
+ fun getLayoutResource(candidate: CompoundMenuCandidate) = when (candidate.end) {
+ ButtonType.CHECKBOX -> CompoundCheckboxMenuCandidateViewHolder.layoutResource
+ ButtonType.SWITCH -> CompoundSwitchMenuCandidateViewHolder.layoutResource
+ }
+ }
+}
+
+internal class CompoundCheckboxMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ dismiss: () -> Unit,
+) : CompoundMenuCandidateViewHolder(itemView, inflater, dismiss) {
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_compound_checkbox
+ }
+}
+
+internal class CompoundSwitchMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ dismiss: () -> Unit,
+) : CompoundMenuCandidateViewHolder(itemView, inflater, dismiss) {
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_compound_switch
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..84ea1b89d6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt
@@ -0,0 +1,47 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.core.view.updateLayoutParams
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+
+internal class DecorativeTextMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+) : MenuCandidateViewHolder<DecorativeTextMenuCandidate>(itemView, inflater) {
+
+ private val textView: TextView get() = itemView as TextView
+
+ override fun bind(
+ newCandidate: DecorativeTextMenuCandidate,
+ oldCandidate: DecorativeTextMenuCandidate?,
+ ) {
+ super.bind(newCandidate, oldCandidate)
+
+ textView.text = newCandidate.text
+ textView.applyStyle(newCandidate.textStyle, oldCandidate?.textStyle)
+ applyHeight(newCandidate.height, oldCandidate?.height)
+ }
+
+ private fun applyHeight(newHeight: Int?, oldHeight: Int?) {
+ if (newHeight != oldHeight) {
+ textView.updateLayoutParams {
+ height = newHeight ?: itemView.resources
+ .getDimensionPixelSize(R.dimen.mozac_browser_menu2_candidate_container_layout_height)
+ }
+ }
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_decorative_text
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..aad9ecffe7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt
@@ -0,0 +1,25 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import androidx.annotation.LayoutRes
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+
+/**
+ * View holder that displays a divider.
+ */
+internal class DividerMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+) : MenuCandidateViewHolder<DividerMenuCandidate>(itemView, inflater) {
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_divider
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt
new file mode 100644
index 0000000000..3ea96d2418
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt
@@ -0,0 +1,33 @@
+/* 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.browser.menu2.adapter
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * View holders that extend this base class are passed both the new value and last value
+ * when [bind] is called. Use this information to diff the changes between the two values.
+ */
+internal abstract class LastItemViewHolder<T>(
+ itemView: View,
+) : RecyclerView.ViewHolder(itemView) {
+
+ protected var lastCandidate: T? = null
+
+ /**
+ * Updates the held view to reflect changes in the menu option.
+ *
+ * @param newCandidate New value to use.
+ * @param oldCandidate Previously set value.
+ * If this is the first time [bind] was called, null is passed.
+ */
+ protected abstract fun bind(newCandidate: T, oldCandidate: T?)
+
+ fun bind(option: T) {
+ bind(option, lastCandidate)
+ lastCandidate = option
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt
new file mode 100644
index 0000000000..bc39865177
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt
@@ -0,0 +1,80 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+internal class MenuCandidateListAdapter(
+ private val inflater: LayoutInflater,
+ private val dismiss: () -> Unit,
+ private val reopenMenu: (NestedMenuCandidate) -> Unit,
+) : ListAdapter<MenuCandidate, MenuCandidateViewHolder<out MenuCandidate>>(MenuCandidateDiffer) {
+
+ @LayoutRes
+ override fun getItemViewType(position: Int) = when (val item = getItem(position)) {
+ is TextMenuCandidate -> TextMenuCandidateViewHolder.layoutResource
+ is DecorativeTextMenuCandidate -> DecorativeTextMenuCandidateViewHolder.layoutResource
+ is CompoundMenuCandidate -> CompoundMenuCandidateViewHolder.getLayoutResource(item)
+ is NestedMenuCandidate -> NestedMenuCandidateViewHolder.layoutResource
+ is RowMenuCandidate -> RowMenuCandidateViewHolder.layoutResource
+ is DividerMenuCandidate -> DividerMenuCandidateViewHolder.layoutResource
+ }
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ @LayoutRes viewType: Int,
+ ): MenuCandidateViewHolder<out MenuCandidate> {
+ val view = inflater.inflate(viewType, parent, false)
+ return when (viewType) {
+ TextMenuCandidateViewHolder.layoutResource ->
+ TextMenuCandidateViewHolder(view, inflater, dismiss)
+ DecorativeTextMenuCandidateViewHolder.layoutResource ->
+ DecorativeTextMenuCandidateViewHolder(view, inflater)
+ CompoundCheckboxMenuCandidateViewHolder.layoutResource ->
+ CompoundCheckboxMenuCandidateViewHolder(view, inflater, dismiss)
+ CompoundSwitchMenuCandidateViewHolder.layoutResource ->
+ CompoundSwitchMenuCandidateViewHolder(view, inflater, dismiss)
+ NestedMenuCandidateViewHolder.layoutResource ->
+ NestedMenuCandidateViewHolder(view, inflater, dismiss, reopenMenu)
+ RowMenuCandidateViewHolder.layoutResource ->
+ RowMenuCandidateViewHolder(view, inflater, dismiss)
+ DividerMenuCandidateViewHolder.layoutResource ->
+ DividerMenuCandidateViewHolder(view, inflater)
+ else -> throw IllegalArgumentException("Invalid viewType $viewType")
+ }
+ }
+
+ override fun onBindViewHolder(holder: MenuCandidateViewHolder<out MenuCandidate>, position: Int) {
+ val item = getItem(position)
+ when (holder) {
+ is TextMenuCandidateViewHolder -> holder.bind(item as TextMenuCandidate)
+ is DecorativeTextMenuCandidateViewHolder -> holder.bind(item as DecorativeTextMenuCandidate)
+ is CompoundMenuCandidateViewHolder -> holder.bind(item as CompoundMenuCandidate)
+ is NestedMenuCandidateViewHolder -> holder.bind(item as NestedMenuCandidate)
+ is RowMenuCandidateViewHolder -> holder.bind(item as RowMenuCandidate)
+ is DividerMenuCandidateViewHolder -> holder.bind(item as DividerMenuCandidate)
+ }
+ }
+}
+
+private object MenuCandidateDiffer : DiffUtil.ItemCallback<MenuCandidate>() {
+ override fun areItemsTheSame(oldItem: MenuCandidate, newItem: MenuCandidate) =
+ oldItem::class == newItem::class
+
+ @Suppress("DiffUtilEquals")
+ override fun areContentsTheSame(oldItem: MenuCandidate, newItem: MenuCandidate) =
+ oldItem == newItem
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..98212b9b36
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt
@@ -0,0 +1,22 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import androidx.annotation.CallSuper
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.candidate.MenuCandidate
+
+internal abstract class MenuCandidateViewHolder<T : MenuCandidate>(
+ itemView: View,
+ protected val inflater: LayoutInflater,
+) : LastItemViewHolder<T>(itemView) {
+
+ @CallSuper
+ override fun bind(newCandidate: T, oldCandidate: T?) {
+ itemView.applyStyle(newCandidate.containerStyle, oldCandidate?.containerStyle)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..c899f5e17f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt
@@ -0,0 +1,53 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.icons.MenuIconAdapter
+import mozilla.components.browser.menu2.ext.applyBackgroundEffect
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+
+internal class NestedMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ dismiss: () -> Unit,
+ private val reopenMenu: (NestedMenuCandidate) -> Unit,
+) : MenuCandidateViewHolder<NestedMenuCandidate>(itemView, inflater), View.OnClickListener {
+
+ private val layout = itemView as ConstraintLayout
+ private val textView: TextView get() = itemView.findViewById(R.id.label)
+ private val startIcon = MenuIconAdapter(layout, inflater, Side.START, dismiss)
+ private val endIcon = MenuIconAdapter(layout, inflater, Side.END, dismiss)
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ override fun bind(newCandidate: NestedMenuCandidate, oldCandidate: NestedMenuCandidate?) {
+ super.bind(newCandidate, oldCandidate)
+
+ textView.text = newCandidate.text
+ textView.applyStyle(newCandidate.textStyle, oldCandidate?.textStyle)
+ itemView.applyBackgroundEffect(newCandidate.effect, oldCandidate?.effect)
+ startIcon.bind(newCandidate.start, oldCandidate?.start)
+ endIcon.bind(newCandidate.end, oldCandidate?.end)
+ }
+
+ override fun onClick(v: View?) {
+ lastCandidate?.let { reopenMenu(it) }
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_nested
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..a2fb8d0b0e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt
@@ -0,0 +1,55 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import androidx.annotation.LayoutRes
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+
+/**
+ * Displays a row of small menu options.
+ */
+internal class RowMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ private val dismiss: () -> Unit,
+) : MenuCandidateViewHolder<RowMenuCandidate>(itemView, inflater) {
+
+ private val layout = itemView as LinearLayout
+ private var buttonViewHolders = emptyList<SmallMenuCandidateViewHolder>()
+
+ override fun bind(newCandidate: RowMenuCandidate, oldCandidate: RowMenuCandidate?) {
+ super.bind(newCandidate, oldCandidate)
+
+ // If the number of children in the row changes,
+ // build new holders for each of them.
+ if (newCandidate.items.size != oldCandidate?.items?.size) {
+ layout.removeAllViews()
+ // Create new view holders list
+ buttonViewHolders = newCandidate.items.map {
+ val button = inflater.inflate(
+ SmallMenuCandidateViewHolder.layoutResource,
+ layout,
+ false,
+ )
+ layout.addView(button)
+ SmallMenuCandidateViewHolder(button, dismiss)
+ }
+ }
+
+ // Use the button view holders to compare individual menu items in the row.
+ buttonViewHolders.zip(newCandidate.items).forEach { (viewHolder, item) ->
+ viewHolder.bind(item)
+ }
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_row
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..fb77f1b325
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt
@@ -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/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.View
+import androidx.annotation.LayoutRes
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.appcompat.widget.TooltipCompat
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.ext.applyIcon
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+
+internal class SmallMenuCandidateViewHolder(
+ itemView: View,
+ private val dismiss: () -> Unit,
+) : LastItemViewHolder<SmallMenuCandidate>(itemView),
+ View.OnClickListener,
+ View.OnLongClickListener {
+
+ private val iconView = itemView as AppCompatImageButton
+ private var onClickListener: (() -> Unit)? = null
+ private var onLongClickListener: (() -> Boolean)? = null
+
+ init {
+ iconView.setOnClickListener(this)
+ iconView.setOnLongClickListener(this)
+ iconView.isLongClickable = false
+ }
+
+ override fun bind(newCandidate: SmallMenuCandidate, oldCandidate: SmallMenuCandidate?) {
+ if (newCandidate.contentDescription != oldCandidate?.contentDescription) {
+ iconView.contentDescription = newCandidate.contentDescription
+ TooltipCompat.setTooltipText(iconView, newCandidate.contentDescription)
+ }
+ onClickListener = newCandidate.onClick
+ onLongClickListener = newCandidate.onLongClick
+ iconView.isLongClickable = newCandidate.onLongClick != null
+ iconView.applyIcon(newCandidate.icon, oldCandidate?.icon)
+ iconView.applyStyle(newCandidate.containerStyle, oldCandidate?.containerStyle)
+ }
+
+ override fun onClick(v: View?) {
+ onClickListener?.invoke()
+ dismiss()
+ }
+
+ override fun onLongClick(v: View?): Boolean {
+ val result = onLongClickListener?.invoke() ?: false
+ dismiss()
+ return result
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_row_small_icon
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..f9fcf1839d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt
@@ -0,0 +1,55 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.icons.MenuIconAdapter
+import mozilla.components.browser.menu2.ext.applyBackgroundEffect
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+internal class TextMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ private val dismiss: () -> Unit,
+) : MenuCandidateViewHolder<TextMenuCandidate>(itemView, inflater), View.OnClickListener {
+
+ private val layout = itemView as ConstraintLayout
+ private val textView: TextView get() = itemView.findViewById(R.id.label)
+ private val startIcon = MenuIconAdapter(layout, inflater, Side.START, dismiss)
+ private val endIcon = MenuIconAdapter(layout, inflater, Side.END, dismiss)
+ private var onClickListener: (() -> Unit)? = null
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ override fun bind(newCandidate: TextMenuCandidate, oldCandidate: TextMenuCandidate?) {
+ super.bind(newCandidate, oldCandidate)
+
+ textView.text = newCandidate.text
+ textView.applyStyle(newCandidate.textStyle, oldCandidate?.textStyle)
+ onClickListener = newCandidate.onClick
+ itemView.applyBackgroundEffect(newCandidate.effect, oldCandidate?.effect)
+ startIcon.bind(newCandidate.start, oldCandidate?.start)
+ endIcon.bind(newCandidate.end, oldCandidate?.end)
+ }
+
+ override fun onClick(v: View?) {
+ onClickListener?.invoke()
+ dismiss()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_text
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt
new file mode 100644
index 0000000000..4ffc031a20
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt
@@ -0,0 +1,224 @@
+/* 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.browser.menu2.adapter.icons
+
+import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageButton
+import android.widget.ImageView
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.core.view.isVisible
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.ext.applyIcon
+import mozilla.components.browser.menu2.ext.applyNotificationEffect
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableButtonMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuIcon
+import mozilla.components.support.base.log.logger.Logger
+
+internal abstract class MenuIconWithDrawableViewHolder<T : MenuIcon>(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+) : MenuIconViewHolder<T>(parent, inflater) {
+
+ protected abstract val imageView: ImageView
+
+ protected fun setup(imageView: View, side: Side) {
+ updateConstraints {
+ connect(R.id.icon, TOP, PARENT_ID, TOP)
+ connect(R.id.icon, BOTTOM, PARENT_ID, BOTTOM)
+ val margin = parent.resources
+ .getDimensionPixelSize(R.dimen.mozac_browser_menu2_icon_padding_start)
+ when (side) {
+ Side.START -> {
+ connect(imageView.id, START, PARENT_ID, START)
+ connect(imageView.id, END, R.id.label, START, margin)
+ connect(R.id.label, START, imageView.id, END)
+ }
+ Side.END -> {
+ connect(imageView.id, END, PARENT_ID, END)
+ connect(imageView.id, START, R.id.label, END, margin)
+ connect(R.id.label, END, imageView.id, START)
+ }
+ }
+ }
+ }
+
+ override fun disconnect() {
+ parent.removeView(imageView)
+ super.disconnect()
+ }
+}
+
+internal class DrawableMenuIconViewHolder(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+ side: Side,
+) : MenuIconWithDrawableViewHolder<DrawableMenuIcon>(parent, inflater) {
+
+ override val imageView: ImageView = inflate(layoutResource).findViewById(R.id.icon)
+ private var effectView: ImageView? = null
+
+ init {
+ setup(imageView, side)
+ }
+
+ override fun bind(newIcon: DrawableMenuIcon, oldIcon: DrawableMenuIcon?) {
+ imageView.applyIcon(newIcon, oldIcon)
+
+ // Only inflate the effect container if needed
+ if (newIcon.effect != null) {
+ createEffectView().applyNotificationEffect(newIcon.effect as LowPriorityHighlightEffect, oldIcon?.effect)
+ } else {
+ effectView?.isVisible = false
+ }
+ }
+
+ private fun createEffectView(): ImageView {
+ if (effectView == null) {
+ val effect: ImageView = inflate(notificationDotLayoutResource).findViewById(R.id.notification_dot)
+ updateConstraints {
+ connect(effect.id, TOP, imageView.id, TOP)
+ connect(effect.id, END, imageView.id, END)
+ }
+ effectView = effect
+ }
+ return effectView!!
+ }
+
+ override fun disconnect() {
+ effectView?.let { parent.removeView(it) }
+ super.disconnect()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_icon_drawable
+ val notificationDotLayoutResource = R.layout.mozac_browser_menu2_icon_notification_dot
+ }
+}
+
+internal class DrawableButtonMenuIconViewHolder(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+ side: Side,
+ private val dismiss: () -> Unit,
+) : MenuIconWithDrawableViewHolder<DrawableButtonMenuIcon>(parent, inflater), View.OnClickListener {
+
+ override val imageView: ImageButton = inflate(layoutResource).findViewById(R.id.icon)
+ private var onClickListener: (() -> Unit)? = null
+
+ init {
+ setup(imageView, side)
+ imageView.setOnClickListener(this)
+ }
+
+ override fun bind(newIcon: DrawableButtonMenuIcon, oldIcon: DrawableButtonMenuIcon?) {
+ imageView.applyIcon(newIcon, oldIcon)
+ onClickListener = newIcon.onClick
+ }
+
+ override fun onClick(v: View?) {
+ onClickListener?.invoke()
+ dismiss()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_icon_button
+ }
+}
+
+internal class AsyncDrawableMenuIconViewHolder(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+ side: Side,
+ private val logger: Logger = Logger("mozac-menu2-AsyncDrawableMenuIconViewHolder"),
+) : MenuIconWithDrawableViewHolder<AsyncDrawableMenuIcon>(parent, inflater) {
+
+ private val scope = MainScope()
+ override val imageView: ImageView = inflate(layoutResource).findViewById(R.id.icon)
+ private var effectView: ImageView? = null
+ private var iconJob: Job? = null
+
+ init {
+ setup(imageView, side)
+ }
+
+ override fun bind(newIcon: AsyncDrawableMenuIcon, oldIcon: AsyncDrawableMenuIcon?) {
+ if (newIcon.loadDrawable != oldIcon?.loadDrawable) {
+ imageView.setImageDrawable(newIcon.loadingDrawable)
+ iconJob?.cancel()
+ iconJob = scope.launch { loadIcon(newIcon.loadDrawable, newIcon.fallbackDrawable) }
+ }
+
+ if (newIcon.tint != oldIcon?.tint) {
+ imageView.imageTintList = newIcon.tint?.let { ColorStateList.valueOf(it) }
+ }
+
+ // Only inflate the effect container if needed
+ if (newIcon.effect != null) {
+ createEffectView().applyNotificationEffect(newIcon.effect as LowPriorityHighlightEffect, oldIcon?.effect)
+ } else {
+ effectView?.isVisible = false
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ private suspend fun loadIcon(
+ loadDrawable: suspend (width: Int, height: Int) -> Drawable?,
+ fallback: Drawable?,
+ ) {
+ val drawable = try {
+ loadDrawable(imageView.measuredWidth, imageView.measuredHeight)
+ } catch (throwable: Throwable) {
+ logger.error(
+ message = "Failed to load browser action icon, falling back to default.",
+ throwable = throwable,
+ )
+ fallback
+ }
+ imageView.setImageDrawable(drawable)
+ }
+
+ private fun createEffectView(): ImageView {
+ if (effectView == null) {
+ val effect: ImageView = inflate(notificationDotLayoutResource).findViewById(R.id.notification_dot)
+ updateConstraints {
+ connect(effect.id, TOP, imageView.id, TOP)
+ connect(effect.id, END, imageView.id, END)
+ }
+ effectView = effect
+ }
+ return effectView!!
+ }
+
+ override fun disconnect() {
+ effectView?.let { parent.removeView(it) }
+ scope.cancel()
+ super.disconnect()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_icon_drawable
+ val notificationDotLayoutResource = R.layout.mozac_browser_menu2_icon_notification_dot
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt
new file mode 100644
index 0000000000..737796c682
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt
@@ -0,0 +1,51 @@
+/* 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.browser.menu2.adapter.icons
+
+import android.view.LayoutInflater
+import androidx.annotation.VisibleForTesting
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableButtonMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.MenuIcon
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+
+/**
+ * Helper class to manage a [ConstraintLayout] that can contain menu icon views.
+ * Different holder classes are used to swap out the child views.
+ */
+internal class MenuIconAdapter(
+ private val parent: ConstraintLayout,
+ private val inflater: LayoutInflater,
+ private val side: Side,
+ private val dismiss: () -> Unit,
+) {
+
+ private var viewHolder: MenuIconViewHolder<out MenuIcon>? = null
+
+ fun bind(newIcon: MenuIcon?, oldIcon: MenuIcon?) {
+ if (newIcon == null && oldIcon != null) {
+ viewHolder?.disconnect()
+ viewHolder = null
+ } else if (newIcon != null) {
+ if (oldIcon == null || newIcon::class != oldIcon::class) {
+ viewHolder?.disconnect()
+ viewHolder = createViewHolder(newIcon)
+ }
+
+ viewHolder?.bindAndCast(newIcon, oldIcon)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createViewHolder(item: MenuIcon): MenuIconViewHolder<*> = when (item) {
+ is DrawableMenuIcon -> DrawableMenuIconViewHolder(parent, inflater, side)
+ is DrawableButtonMenuIcon -> DrawableButtonMenuIconViewHolder(parent, inflater, side, dismiss)
+ is AsyncDrawableMenuIcon -> AsyncDrawableMenuIconViewHolder(parent, inflater, side)
+ is TextMenuIcon -> TextMenuIconViewHolder(parent, inflater, side)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt
new file mode 100644
index 0000000000..8af37ce947
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt
@@ -0,0 +1,71 @@
+/* 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.browser.menu2.adapter.icons
+
+import android.view.LayoutInflater
+import android.view.View
+import androidx.annotation.CallSuper
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.MenuIcon
+
+/**
+ * View holder with a [bind] method that passes the previously bound value.
+ */
+internal abstract class MenuIconViewHolder<T : MenuIcon>(
+ protected val parent: ConstraintLayout,
+ protected val inflater: LayoutInflater,
+) {
+
+ @Suppress("Unchecked_Cast")
+ fun bindAndCast(newIcon: MenuIcon, oldIcon: MenuIcon?) {
+ bind(newIcon as T, oldIcon as T?)
+ }
+
+ /**
+ * Updates the held view to reflect changes in the menu option icon.
+ *
+ * @param newIcon New values to use.
+ * @param oldIcon Previously set values.
+ */
+ protected abstract fun bind(newIcon: T, oldIcon: T?)
+
+ /**
+ * Inflates the layout resource and adds it to the parent layout.
+ */
+ protected fun inflate(@LayoutRes layoutResource: Int): View {
+ val view = inflater.inflate(layoutResource, parent, false)
+ parent.addView(view)
+ return view
+ }
+
+ /**
+ * Changes the constraints applied to [parent].
+ */
+ protected inline fun updateConstraints(update: ConstraintSet.() -> Unit) {
+ ConstraintSet().apply {
+ clone(parent)
+ update()
+ applyTo(parent)
+ }
+ }
+
+ /**
+ * Resets the layout and removes any child views.
+ * Called when the view holder is removed.
+ */
+ @CallSuper
+ open fun disconnect() {
+ updateConstraints {
+ connect(R.id.label, START, PARENT_ID, START)
+ connect(R.id.label, END, PARENT_ID, END)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt
new file mode 100644
index 0000000000..9f1612ef9b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt
@@ -0,0 +1,62 @@
+/* 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.browser.menu2.adapter.icons
+
+import android.view.LayoutInflater
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+
+internal class TextMenuIconViewHolder(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+ side: Side,
+) : MenuIconViewHolder<TextMenuIcon>(parent, inflater) {
+
+ private val textView: TextView = inflate(layoutResource).findViewById(R.id.icon)
+
+ init {
+ updateConstraints {
+ connect(textView.id, TOP, PARENT_ID, TOP)
+ connect(textView.id, BOTTOM, PARENT_ID, BOTTOM)
+ when (side) {
+ Side.START -> {
+ connect(textView.id, START, PARENT_ID, START)
+ connect(textView.id, END, R.id.label, START)
+ connect(R.id.label, START, textView.id, END)
+ }
+ Side.END -> {
+ connect(textView.id, END, PARENT_ID, END)
+ connect(textView.id, START, R.id.label, END)
+ connect(R.id.label, END, textView.id, START)
+ }
+ }
+ }
+ }
+
+ override fun bind(newIcon: TextMenuIcon, oldIcon: TextMenuIcon?) {
+ textView.text = newIcon.text
+ textView.applyStyle(newIcon.textStyle, oldIcon?.textStyle)
+ }
+
+ override fun disconnect() {
+ parent.removeView(textView)
+ super.disconnect()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_icon_text
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt
new file mode 100644
index 0000000000..61fb98aaca
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt
@@ -0,0 +1,261 @@
+/* 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.browser.menu2.ext
+
+import android.graphics.Rect
+import android.view.View
+import androidx.annotation.Px
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Orientation
+import mozilla.components.support.ktx.android.view.isRTL
+import kotlin.math.roundToInt
+
+const val HALF_MENU_ITEM = 0.5
+
+@Suppress("ComplexMethod")
+internal fun inferMenuPositioningData(
+ containerView: View,
+ anchor: View,
+ style: MenuStyle? = null,
+ orientation: Orientation?,
+): MenuPositioningData? {
+ // Measure the menu allowing it to expand entirely.
+ val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+ containerView.measure(spec, spec)
+
+ val recyclerView = containerView.findViewById<RecyclerView>(R.id.mozac_browser_menu_recyclerView)
+ val recyclerViewAdapter = recyclerView.adapter ?: run {
+ // We might want to track how often and in what circumstances the menu gets called without
+ // valid parameters once we have a system for that.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1814816
+ return null
+ }
+
+ val hasViewInitializedCorrectly = containerView.measuredHeight > 0 && containerView.measuredWidth > 0 &&
+ recyclerView.measuredHeight > 0 && recyclerViewAdapter.itemCount > 0
+ if (!hasViewInitializedCorrectly) {
+ // Same as above: https://bugzilla.mozilla.org/show_bug.cgi?id=1814816
+ return null
+ }
+
+ val menuHorizontalPadding = containerView.measuredWidth - recyclerView.measuredWidth
+ val menuVerticalPadding = containerView.measuredHeight - recyclerView.measuredHeight
+ var horizontalOffset = style?.horizontalOffset ?: 0
+ var verticalOffset = style?.verticalOffset ?: 0
+
+ // Elevation creates some padding between the menu and its container, so that the start corner
+ // of the menu doesn't match the corner of the anchor view. If the user wants the menu to hide
+ // the anchor completely, we have to adjust the position of the menu to compensate for the inner
+ // padding of the menu container.
+ if (style?.completelyOverlap == true) {
+ horizontalOffset -= menuHorizontalPadding / 2
+ verticalOffset -= menuVerticalPadding / 2
+ }
+
+ // The menu height might be adjusted: if there is not enough space to show all the items,
+ // it will crop the last visible item in half, to give the user a hint that it is scrollable.
+ val containerViewHeight = calculateContainerHeight(
+ recyclerView.measuredHeight,
+ recyclerViewAdapter.itemCount,
+ containerView.measuredHeight,
+ style?.verticalOffset ?: 0,
+ anchor,
+ )
+
+ val (availableHeightToTop, availableHeightToBottom) = getMaxAvailableHeightToTopAndBottom(anchor)
+ val (availableWidthToLeft, availableWidthToRight) = getMaxAvailableWidthToLeftAndRight(anchor)
+
+ val fitsUp = availableHeightToTop + anchor.height >= containerViewHeight
+ val fitsDown = availableHeightToBottom + anchor.height >= containerViewHeight
+ val fitsRight = availableWidthToRight + anchor.width >= containerView.measuredWidth
+ val fitsLeft = availableWidthToLeft + anchor.width >= containerView.measuredWidth
+
+ val notEnoughHorizontalSpace = !fitsRight && !fitsLeft
+ val fitsBothHorizontalDirections = fitsRight && fitsLeft
+ val drawingLeft = if (notEnoughHorizontalSpace || fitsBothHorizontalDirections) {
+ anchor.isRTL
+ } else {
+ !fitsRight
+ }
+
+ val anchorPosition = IntArray(2)
+ anchor.getLocationInWindow(anchorPosition)
+ var (anchorX, anchorY) = anchorPosition
+
+ // Position the menu above the anchor if the orientation is UP and there is enough space.
+ if (orientation == Orientation.UP && fitsUp) {
+ anchorY -= containerViewHeight - anchor.height
+ verticalOffset = -verticalOffset
+ }
+
+ if (drawingLeft) {
+ anchorX -= containerView.measuredWidth - anchor.width
+ horizontalOffset = -horizontalOffset
+ }
+
+ return MenuPositioningData(
+ anchor = anchor,
+ x = anchorX + horizontalOffset,
+ y = anchorY + verticalOffset,
+ containerHeight = containerViewHeight,
+ animation = getAnimation(fitsUp, fitsDown, drawingLeft, orientation),
+ )
+}
+
+private fun getMaxAvailableHeightToTopAndBottom(anchor: View): Pair<Int, Int> {
+ val anchorPosition = IntArray(2)
+ val displayFrame = Rect()
+
+ val appView = anchor.rootView
+ appView.getWindowVisibleDisplayFrame(displayFrame)
+
+ anchor.getLocationOnScreen(anchorPosition)
+
+ val bottomEdge = displayFrame.bottom
+
+ val distanceToBottom = bottomEdge - (anchorPosition[1] + anchor.height)
+ val distanceToTop = anchorPosition[1] - displayFrame.top
+
+ return distanceToTop to distanceToBottom
+}
+
+private fun getMaxAvailableWidthToLeftAndRight(anchor: View): Pair<Int, Int> {
+ val anchorPosition = IntArray(2)
+ val displayFrame = Rect()
+
+ val appView = anchor.rootView
+ appView.getWindowVisibleDisplayFrame(displayFrame)
+
+ anchor.getLocationOnScreen(anchorPosition)
+
+ val distanceToLeft = anchorPosition[0] - displayFrame.left
+ val distanceToRight = displayFrame.right - (anchorPosition[0] + anchor.width)
+
+ return distanceToLeft to distanceToRight
+}
+
+/**
+ * Determine whether the container view can display all menu items (without scrolling) within
+ * the available height.
+ *
+ * @return The original container height if the container view can display all menu items
+ * (without scrolling), else calculate the maximum available container height for a scrollable
+ * view with a half menu item.
+ */
+private fun calculateContainerHeight(
+ recyclerViewHeight: Int,
+ recyclerViewItemCount: Int,
+ containerViewHeight: Int,
+ menuStylePadding: Int,
+ anchor: View,
+): Int {
+ // Get the total screen display height.
+ val totalHeight = anchor.rootView.measuredHeight
+
+ // Note: We cannot use getWindowVisibleDisplayFrame() as the height is dynamic based on whether
+ // the keyboard is open.
+ // Get any displayed system bars e.g. top status bar, bottom navigation bar or soft buttons bar.
+ val systemBars =
+ ViewCompat.getRootWindowInsets(anchor)?.getInsets(WindowInsetsCompat.Type.systemBars())
+ // Store the vertical status bars.
+ val topSystemBarHeight = systemBars?.top ?: 0
+ val bottomSystemBarHeight = systemBars?.bottom ?: 0
+
+ // Deduct any status bar heights from the total height.
+ val availableHeight = totalHeight - (bottomSystemBarHeight + topSystemBarHeight)
+
+ val menuItemHeight = recyclerViewHeight / recyclerViewItemCount
+
+ // We must take the menu container padding into account as this will be applied to the final height.
+ val containerPadding = containerViewHeight - recyclerViewHeight
+
+ val maxAvailableHeightForRecyclerView = availableHeight - containerPadding - menuStylePadding
+
+ // The number of menu items that can fit exactly (no cropping) within the max app height.
+ // Round the number of items to the closet Int value to ensure the max space available is utilized.
+ // E.g if 6.9 items fit, round to 7 so the calculation below will show 6.5 items instead of 5.5 .
+ val numberOfItemsFitExactly =
+ (maxAvailableHeightForRecyclerView.toFloat() / menuItemHeight.toFloat()).roundToInt()
+
+ val itemsAlreadyFitContainerHeight = recyclerViewItemCount <= numberOfItemsFitExactly
+
+ return if (itemsAlreadyFitContainerHeight) {
+ containerViewHeight
+ } else {
+ getCroppedMenuContainerHeight(numberOfItemsFitExactly, menuItemHeight, containerPadding)
+ }
+}
+
+private fun getAnimation(
+ fitsUp: Boolean,
+ fitsDown: Boolean,
+ drawingLeft: Boolean,
+ orientation: Orientation?,
+): Int {
+ val isUpOrientation = orientation == Orientation.UP
+ return when {
+ isUpOrientation && fitsUp -> if (drawingLeft) {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom
+ } else {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom
+ }
+ fitsDown -> if (drawingLeft) {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop
+ } else {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop
+ }
+ else -> if (drawingLeft) {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight
+ } else {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft
+ }
+ }
+}
+
+private fun getCroppedMenuContainerHeight(
+ numberOfItemsFitExactly: Int,
+ menuItemHeight: Int,
+ containerPadding: Int,
+): Int {
+ // The number of menu items that fit exactly, minus a half menu item (indicates more menu items exist).
+ val numberOfItemsFitWithOverflow = numberOfItemsFitExactly - HALF_MENU_ITEM
+ val updatedRecyclerViewHeight = (numberOfItemsFitWithOverflow * menuItemHeight).toInt()
+
+ return updatedRecyclerViewHeight + containerPadding
+}
+
+/**
+ * Data needed for menu positioning.
+ */
+data class MenuPositioningData(
+ /**
+ * Android View that the PopupWindow should be anchored to.
+ */
+ val anchor: View,
+
+ /**
+ * [WindowManager#LayoutParams#x] of params the menu will be added with.
+ */
+ @Px val x: Int = 0,
+
+ /**
+ * [WindowManager#LayoutParams#y] of params the menu will be added with.
+ */
+ @Px val y: Int = 0,
+
+ /**
+ * [View#measuredHeight] of the menu.
+ */
+ @Px val containerHeight: Int = 0,
+
+ /**
+ * [PopupWindow#animationStyle] of the menu.
+ */
+ val animation: Int,
+)
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt
new file mode 100644
index 0000000000..b40e893955
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt
@@ -0,0 +1,95 @@
+/* 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.browser.menu2.ext
+
+import android.content.res.ColorStateList
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuCandidateEffect
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.concept.menu.candidate.MenuIconWithDrawable
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+
+/**
+ * Apply container styles if different from the previous styling.
+ */
+internal fun View.applyStyle(newStyle: ContainerStyle, oldStyle: ContainerStyle?) {
+ if (newStyle != oldStyle) {
+ isVisible = newStyle.isVisible
+ isEnabled = newStyle.isEnabled
+ }
+}
+
+/**
+ * Apply text styles if different from the previous styling.
+ */
+internal fun TextView.applyStyle(newStyle: TextStyle, oldStyle: TextStyle?) {
+ if (newStyle != oldStyle) {
+ newStyle.size?.let { textSize = it }
+ newStyle.color?.let { setTextColor(it) }
+ setTypeface(typeface, newStyle.textStyle)
+ textAlignment = newStyle.textAlignment
+ }
+}
+
+/**
+ * Set the image to display based on the [MenuIconWithDrawable].
+ */
+internal fun ImageView.applyIcon(newIcon: MenuIconWithDrawable, oldIcon: MenuIconWithDrawable?) {
+ if (newIcon != oldIcon) {
+ setImageDrawable(newIcon.drawable)
+ imageTintList = newIcon.tint?.let { ColorStateList.valueOf(it) }
+ }
+}
+
+internal fun ImageView.applyNotificationEffect(
+ newEffect: LowPriorityHighlightEffect?,
+ oldEffect: MenuEffect?,
+) {
+ if (newEffect != oldEffect) {
+ isVisible = newEffect != null
+ imageTintList = newEffect?.notificationTint?.let { ColorStateList.valueOf(it) }
+ }
+}
+
+/**
+ * Build a drawable to be used for the background of a menu option.
+ */
+internal fun View.applyBackgroundEffect(
+ newEffect: MenuCandidateEffect?,
+ oldEffect: MenuCandidateEffect?,
+) {
+ if (newEffect == oldEffect) return
+
+ val highlight = newEffect as? HighPriorityHighlightEffect
+ val selectableBackgroundRes = context.theme
+ .resolveAttribute(android.R.attr.selectableItemBackground)
+
+ if (highlight != null) {
+ val selectableBackground = ContextCompat.getDrawable(
+ context,
+ selectableBackgroundRes,
+ )
+
+ setBackgroundColor(highlight.backgroundTint)
+ if (SDK_INT >= Build.VERSION_CODES.M) {
+ foreground = selectableBackground
+ }
+ } else {
+ setBackgroundResource(selectableBackgroundRes)
+ if (SDK_INT >= Build.VERSION_CODES.M) {
+ foreground = null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt
new file mode 100644
index 0000000000..eeba85dede
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt
@@ -0,0 +1,118 @@
+/* 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.browser.menu2.view
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.MenuButton
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.concept.menu.ext.effects
+import mozilla.components.concept.menu.ext.max
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import mozilla.components.support.ktx.android.view.hideKeyboard
+
+/**
+ * A `three-dot` button used for expanding menus.
+ *
+ * If you are using a browser toolbar, do not use this class directly.
+ */
+class MenuButton2 @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr),
+ MenuButton,
+ View.OnClickListener,
+ Observable<MenuButton.Observer> by ObserverRegistry() {
+
+ /**
+ * Sets a [MenuController] that will be used to create a menu when this button is clicked.
+ */
+ override var menuController: MenuController? = null
+ set(value) {
+ // Clean up old controller
+ field?.dismiss()
+ field?.unregister(menuControllerObserver)
+
+ // Attach new controller
+ field = value
+ value?.register(menuControllerObserver, this)
+ }
+
+ private val menuControllerObserver = object : MenuController.Observer {
+ /**
+ * Change the menu button appearance when the menu list changes.
+ */
+ override fun onMenuListSubmit(list: List<MenuCandidate>) {
+ val effect = list.effects().max()
+
+ // If a highlighted item is found, show the indicator
+ setEffect(effect)
+ }
+
+ override fun onDismiss() = notifyObservers { onDismiss() }
+ }
+
+ private val menuIcon: ImageView
+ private val highlightView: ImageView
+ private val notificationIconView: ImageView
+
+ init {
+ View.inflate(context, R.layout.mozac_browser_menu2_button, this)
+ setOnClickListener(this)
+ menuIcon = findViewById(R.id.icon)
+ highlightView = findViewById(R.id.highlight)
+ notificationIconView = findViewById(R.id.notification_dot)
+ }
+
+ /**
+ * Shows the menu.
+ */
+ override fun onClick(v: View) {
+ this.hideKeyboard()
+ val menuController = menuController ?: return
+
+ menuController.show(anchor = this)
+ notifyObservers { onShow() }
+ }
+
+ /**
+ * Show the indicator for a browser menu effect.
+ */
+ override fun setEffect(effect: MenuEffect?) {
+ when (effect) {
+ is HighPriorityHighlightEffect -> {
+ highlightView.imageTintList = ColorStateList.valueOf(effect.backgroundTint)
+ highlightView.visibility = View.VISIBLE
+ notificationIconView.visibility = View.GONE
+ }
+ is LowPriorityHighlightEffect -> {
+ notificationIconView.setColorFilter(effect.notificationTint)
+ highlightView.visibility = View.GONE
+ notificationIconView.visibility = View.VISIBLE
+ }
+ null -> {
+ highlightView.visibility = View.GONE
+ notificationIconView.visibility = View.GONE
+ }
+ }
+ }
+
+ /**
+ * Sets the tint of the 3-dot menu icon.
+ */
+ override fun setColorFilter(@ColorInt color: Int) = menuIcon.setColorFilter(color)
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt
new file mode 100644
index 0000000000..d0d0e5d96b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt
@@ -0,0 +1,95 @@
+/* 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.browser.menu2.view
+
+import android.content.Context
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import androidx.annotation.VisibleForTesting
+import androidx.cardview.widget.CardView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.MenuCandidateListAdapter
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.support.ktx.android.view.onNextGlobalLayout
+
+/**
+ * A popup menu composed of [MenuCandidate] objects.
+ */
+class MenuView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ private val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ private val menuAdapter = MenuCandidateListAdapter(
+ inflater = LayoutInflater.from(context),
+ dismiss = { onDismiss() },
+ reopenMenu = { onReopenMenu(it) },
+ )
+ private val cardView: CardView
+ private val recyclerView: RecyclerView
+
+ /**
+ * Called when the menu is clicked and should be dismissed.
+ */
+ var onDismiss: () -> Unit = {}
+
+ /**
+ * Called when a nested menu should be opened.
+ */
+ var onReopenMenu: (NestedMenuCandidate?) -> Unit = {}
+
+ init {
+ View.inflate(context, R.layout.mozac_browser_menu2_view, this)
+
+ cardView = findViewById(R.id.mozac_browser_menu_cardView)
+ recyclerView = findViewById(R.id.mozac_browser_menu_recyclerView)
+ recyclerView.layoutManager = layoutManager
+ recyclerView.adapter = menuAdapter
+ }
+
+ /**
+ * Changes the contents of the menu.
+ */
+ fun submitList(list: List<MenuCandidate>?) = menuAdapter.submitList(list)
+
+ /**
+ * Displays either the start or the end of the list.
+ */
+ fun setVisibleSide(side: Side) {
+ if (SDK_INT >= Build.VERSION_CODES.N) {
+ layoutManager.stackFromEnd = side == Side.END
+ } else {
+ // In devices with Android 6 and below stackFromEnd is not working properly,
+ // as a result, we have to provided a backwards support.
+ // See: https://github.com/mozilla-mobile/android-components/issues/3211
+ if (side == Side.END) scrollOnceToTheBottom(recyclerView)
+ }
+ }
+
+ /**
+ * Sets the background color for the menu view.
+ */
+ fun setStyle(style: MenuStyle) {
+ style.backgroundColor?.let { cardView.setCardBackgroundColor(it) }
+ }
+
+ @VisibleForTesting
+ internal fun scrollOnceToTheBottom(recyclerView: RecyclerView) {
+ recyclerView.onNextGlobalLayout {
+ recyclerView.adapter?.let { recyclerView.scrollToPosition(it.itemCount - 1) }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.xml
new file mode 100644
index 0000000000..de510ecb12
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="5%"
+ android:pivotY="50%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.xml
new file mode 100644
index 0000000000..6d27c410ea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="5%"
+ android:pivotY="100%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.xml
new file mode 100644
index 0000000000..fc141bdbd0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="5%"
+ android:pivotY="5%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.xml
new file mode 100644
index 0000000000..0ca98399a7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="95%"
+ android:pivotY="50%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.xml
new file mode 100644
index 0000000000..89153ca6e5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="95%"
+ android:pivotY="100%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.xml
new file mode 100644
index 0000000000..f0485403ef
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="95%"
+ android:pivotY="5%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml
new file mode 100644
index 0000000000..226166c109
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="1"
+ android:toAlpha="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml
new file mode 100644
index 0000000000..33d8ad19b4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml
@@ -0,0 +1,29 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <group
+ android:scaleX="0.714"
+ android:scaleY="0.714"
+ android:pivotX="20"
+ android:pivotY="20">
+ <path
+ android:pathData="m38.622,27.309a8,8 0,0 0,-11.314 11.314,19.949 19.949,0 0,1 -7.308,1.377c-11.046,0 -20,-8.954 -20,-20s8.954,-20 20,-20 20,8.954 20,20c0,2.58 -0.488,5.045 -1.378,7.309z"
+ android:fillColor="#ffffff"
+ android:fillAlpha=".4" />
+ <path
+ android:pathData="M33,33m-6.4,0a6.4,6.4 0,1 1,12.8 0a6.4,6.4 0,1 1,-12.8 0"
+ android:fillColor="#ffffff"
+ android:fillAlpha=".4" />
+ <path
+ android:pathData="M33,33m-4.3,0a4.3,4.3 0,1 1,8.6 0a4.3,4.3 0,1 1,-8.6 0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#ffffff"
+ android:strokeColor="#000000" />
+ </group>
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml
new file mode 100644
index 0000000000..b7b5ced0c0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml
@@ -0,0 +1,21 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <group
+ android:translateX="25.55"
+ android:translateY="5.55"
+ android:scaleX="0.75"
+ android:scaleY="0.75">
+ <path
+ android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#fff"
+ android:strokeColor="#000" />
+ </group>
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml
new file mode 100644
index 0000000000..8afb175afe
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="8dp"
+ android:height="8dp"
+ android:viewportWidth="10"
+ android:viewportHeight="10">
+ <path
+ android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#fff"
+ android:strokeColor="#000" />
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml
new file mode 100644
index 0000000000..0d31537664
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml
@@ -0,0 +1,39 @@
+<?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/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="?android:selectableItemBackgroundBorderless"
+ tools:parentTag="android.widget.FrameLayout">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/highlight"
+ app:srcCompat="@drawable/mozac_browser_menu2_indicator"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu2_highlighted"
+ android:visibility="gone" />
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ app:srcCompat="@drawable/mozac_ic_ellipsis_vertical_24"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu2_button" />
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/notification_dot"
+ app:srcCompat="@drawable/mozac_browser_menu2_notification"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu2_highlighted"
+ android:visibility="gone" />
+
+</merge>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml
new file mode 100644
index 0000000000..eba14d8e6d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml
@@ -0,0 +1,36 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu2.Candidate.Container"
+ android:layout_width="match_parent"
+ android:paddingStart="@dimen/mozac_browser_menu2_candidate_container_padding_start"
+ android:paddingEnd="0dp"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ tools:ignore="DisableBaselineAlignment,KeyboardInaccessibleWidget">
+
+ <androidx.appcompat.widget.AppCompatCheckBox
+ android:id="@+id/label"
+ style="@style/Mozac.Browser.Menu2.Candidate.Label"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/mozac_browser_menu2_candidate_container_layout_height"
+ android:background="@null"
+ android:button="@null"
+ android:drawableEnd="?android:attr/listChoiceIndicatorMultiple"
+ android:drawablePadding="@dimen/mozac_browser_menu2_checkbox_padding"
+ android:paddingStart="0dp"
+ android:paddingEnd="@dimen/mozac_browser_menu2_candidate_container_padding_end"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:gravity="center_vertical"
+ tools:text="Item" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml
new file mode 100644
index 0000000000..d702796ce8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml
@@ -0,0 +1,29 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu2.Candidate.Container"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ tools:ignore="DisableBaselineAlignment,KeyboardInaccessibleWidget">
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/label"
+ style="@style/Mozac.Browser.Menu2.Candidate.Label"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/mozac_browser_menu2_candidate_container_layout_height"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:gravity="center_vertical"
+ tools:text="Item" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml
new file mode 100644
index 0000000000..f2fc0aa9d3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml
@@ -0,0 +1,18 @@
+<?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/. -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/simple_text"
+ style="@style/Mozac.Browser.Menu2.Candidate.Text"
+ android:background="@null"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu2_candidate_container_layout_height"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="start|center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:textAlignment="viewStart"
+ tools:text="Item" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml
new file mode 100644
index 0000000000..b716250ca9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml
@@ -0,0 +1,8 @@
+<?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/. -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Mozac.Browser.Menu2.Candidate.Divider.Horizontal"
+ android:importantForAccessibility="no"
+ android:clickable="false"/>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml
new file mode 100644
index 0000000000..4eff0a72a4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml
@@ -0,0 +1,29 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu2.Candidate.Container"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center_vertical">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/label"
+ style="@style/Mozac.Browser.Menu2.Candidate.Label"
+ android:layout_width="0dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ android:background="@null"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ tools:text="Item" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml
new file mode 100644
index 0000000000..a46826e4b4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/TextAppearance.Material.Menu"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu2_candidate_row_height"
+ android:gravity="center_vertical"
+ android:orientation="horizontal" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml
new file mode 100644
index 0000000000..02f5bb4ebc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<androidx.appcompat.widget.AppCompatImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ tools:src="@android:color/background_dark" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml
new file mode 100644
index 0000000000..4eff0a72a4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml
@@ -0,0 +1,29 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu2.Candidate.Container"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center_vertical">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/label"
+ style="@style/Mozac.Browser.Menu2.Candidate.Label"
+ android:layout_width="0dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ android:background="@null"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ tools:text="Item" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml
new file mode 100644
index 0000000000..bb60166414
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<androidx.appcompat.widget.AppCompatImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/icon"
+ style="@style/Mozac.Browser.Menu2.Icon"
+ tools:src="@android:drawable/ic_menu_add" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml
new file mode 100644
index 0000000000..b10ae873e0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<androidx.appcompat.widget.AppCompatImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/icon"
+ style="@style/Mozac.Browser.Menu2.Icon"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ tools:src="@android:drawable/ic_menu_add" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml
new file mode 100644
index 0000000000..54c6c509d6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<androidx.appcompat.widget.AppCompatImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/notification_dot"
+ android:layout_gravity="top|end"
+ android:contentDescription="@string/mozac_browser_menu2_highlighted"
+ android:layout_width="@dimen/mozac_browser_menu2_icon_notification_dot_size"
+ android:layout_height="@dimen/mozac_browser_menu2_icon_notification_dot_size"
+ android:translationX="@dimen/mozac_browser_menu2_icon_notification_dot_translate_x"
+ android:translationY="@dimen/mozac_browser_menu2_icon_notification_dot_translate_y"
+ android:background="@android:color/transparent"
+ app:srcCompat="@drawable/mozac_browser_menu2_notification_icon" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml
new file mode 100644
index 0000000000..9e94057898
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<androidx.appcompat.widget.AppCompatTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/icon"
+ style="@style/Mozac.Browser.Menu2.Icon.Text"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAlignment="viewEnd"
+ tools:text="Ctrl+X" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml
new file mode 100644
index 0000000000..1e05515a6f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml
@@ -0,0 +1,31 @@
+<?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/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:parentTag="FrameLayout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <androidx.cardview.widget.CardView
+ style="@style/Mozac.Browser.Menu2"
+ android:id="@+id/mozac_browser_menu_cardView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:cardCornerRadius="@dimen/mozac_browser_menu2_corner_radius"
+ app:cardElevation="@dimen/mozac_browser_menu2_elevation"
+ app:cardUseCompatPadding="true">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/mozac_browser_menu_recyclerView"
+ android:paddingTop="@dimen/mozac_browser_menu2_padding_vertical"
+ android:paddingBottom="@dimen/mozac_browser_menu2_padding_vertical"
+ android:layout_width="@dimen/mozac_browser_menu2_width"
+ android:layout_height="wrap_content"
+ android:overScrollMode="never"
+ tools:listitem="@layout/mozac_browser_menu2_candidate_text" />
+
+ </androidx.cardview.widget.CardView>
+</merge>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..79d77db735
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ምናሌ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">የተተኮረበት</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..3fc9124078
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacaus</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..41f0bf6f11
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">القائمة</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">عليها الإبراز</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..08ab9cea55
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Rescamplóse</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..d3163532fb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Vurğulanmış</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..6e0898fb50
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">منو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">هایلایت اولدو</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..5a0a67e133
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Kasorot</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..c335f20e0b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Вылучаны(я)</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..6a42322297
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Откроено</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..35491cbc2b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">মেনু</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">হাইলাইট করা হয়েছে</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..3377f9e8ec
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Lañser</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Usskedet</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..e4be501bc9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Istaknuto</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..f34fc74e2e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">S’ha ressaltat</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..32d2e53097
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">K\'utsamaj</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Ya\'on ruq\'ij</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..c31b0a90da
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Gipasiugda</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..d935386713
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">پێڕست</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ئاماژەپێکراو</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..afa726fc94
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Listinu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Sopralineatu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..b027ce6479
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Nabídka</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Zvýrazněné</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..fa069ca454
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Dewislen</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Amlygwyd</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..dca8b4f467
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Fremhævet</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..be63b6937a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Hervorgehoben</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..97cc5b8efc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Wuzwignjony</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..c6dfb2b15a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Μενού</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Επισημασμένο</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..592d520cac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Highlighted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..592d520cac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Highlighted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..36f886c443
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menuo</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Elstarigitaj</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..85c3558fdf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Resaltado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..ff96f0b1a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..85c3558fdf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Resaltado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..ff96f0b1a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..85c3558fdf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Resaltado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..48d020d392
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menüü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Esiletõstetud</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..fe9175c4cd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menua</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Nabarmendua</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..c725cf390f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">منو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">برجسته‌شده</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..2df10018d3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Valikko</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Korostettu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..0c17d18f2f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Sélectionné</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..295ae072f1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidenziât</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..df69165f85
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Markearre</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..085e65afa1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">An clàr-taice</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Soillsichte</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..3cc8bc80c7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Realzado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..99dd9a29bc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Poravorã</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Hechaukaveha</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..a3e12d11ea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">મેનુ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">પ્રકાશિત કરેલ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..c26c684891
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">मेन्यू</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">दर्शाए गए</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..592d520cac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Highlighted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..6b6f279bf5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Izbornik</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Istaknuto</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..a646978d65
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Wuzběhnjeny</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..8b3a5be2d7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Kiemelt</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..95863f1c45
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Ցանկ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Գունանշված</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..273756232a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidentiate</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..bdbc257bca
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Tersorot</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..25470860c2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Valmynd</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Undirstrikað</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..bcda59c692
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidenziato</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..1e259b99f4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">תפריט</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">מודגש</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..573ca56cea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">メニュー</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">強調</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..bcda2c40f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">მენიუ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">მონიშნული</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..5933838259
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Belgilengen</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..01b0200a52
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Umuɣ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ittwag deg uqerru</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..dc3e56e0a3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Мәзір</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Ерекшеленген</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..fef72a1d0e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menû</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Berbiçavkirî</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..eedd3a4d96
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ಮೆನು</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ಹೈಲೈಟ್ ಮಾಡಲಾಗಿದೆ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..6106e64081
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">메뉴</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">강조 표시됨</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml
new file mode 100644
index 0000000000..6ed87c5d94
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Rol</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Golowboyntys</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml
new file mode 100644
index 0000000000..5dea33699b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml
@@ -0,0 +1,7 @@
+<?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>
+ <dimen name="mozac_browser_menu2_icon_notification_dot_translate_x">-4dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..3e379e7a7e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">In evidensa</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..0feac7d979
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ເມນູ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ຈຸດເດັ່ນ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..fef1bedd5b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meniu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Paryškinta</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..fff2691aea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Katsi</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Tu^un nchichi</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..c03e62b772
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">मेनू</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ठळक</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..1ddc43e6f8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">မီနူး</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">အသားပေးအရာ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..33264b5c05
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Uthevet</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..3040e760b8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">मेनु</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">हाइलाइट गरियो</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..0b7f6dfe19
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Gemarkeerd</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..1ae9d7d81f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Utheva</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..9ead4cdd58
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Notables</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..dcf46d2ff3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ମେନୁ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ହାଇଲାଇଟ୍ କରାଯାଇଥିବା</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..e9704bf09f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ਮੀਨੂ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ਉਘਾੜੇ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..0034100a2a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">مینو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">اُگھاڑے</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..d430f3d2b3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Wyróżnione</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..17464e70f5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..17464e70f5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..3ff1b693a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Cun emfasa</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..36c3bbc33e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meniu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidențiat</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..edaedb47a7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Выделено</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..d0d78ee8df
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ᱢᱮᱱᱩ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ᱩᱪᱷᱟᱹᱱᱟᱜ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..47781dced8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">In evidèntzia</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..360b4ba100
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">වට්ටෝරුව</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ත්‍රීවාලෝකිත</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..83ef834d61
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Ponuka</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Zvýraznené</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..92d8c77be3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">مینیو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">نمایاں کیتا ڳیا</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..5f76d3d0a4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Označeno</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..5059b3ddc0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">E theksuar</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..771684ae37
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Мени</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Истакнуто</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..f5694fa656
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Disorot</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..53e1a2c120
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Markerad</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..050fdcd8f7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Myni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Ôbznoczōne</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..ef67a234f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">பட்டி</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">மிளிர்ப்புகள்</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..6a8494e72c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">మెనూ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">హైలైట్ చేసినవి</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..f9874801ec
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Таъкид</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..3b39bcac10
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">เมนู</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">เน้น</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..83bba47bf4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Mga naka-highlight</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..4faac7b60a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Vurgulu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..da59e49395
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menû</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Sa ña\'āan</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..33e7485245
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Аерылган</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..671152685f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Umuɣ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..444cf11219
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">تىزىملىك</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ئالاھىدە</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..f5ecb3600e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Виділено</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..b19fad90ee
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">مینیو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">نمایاں کیا گیا</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..45ec9ab80f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Belgilangan</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..e0e190062a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidensià</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..f3544ed037
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Đã tô sáng</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..14377ebe7c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">mẹ́nù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Ti fàmìsí</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..fedb88a7f7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">菜单</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">高亮</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..d7ce12c735
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">選單</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">強調</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..a77289bfe3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Empty by default, allows others to theme as they see fit -->
+ <color name="mozac_browser_menu2_background"></color>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..fa390df86f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml
@@ -0,0 +1,48 @@
+<?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>
+ <dimen name="mozac_browser_menu2_corner_radius">4dp</dimen>
+ <dimen name="mozac_browser_menu2_elevation">6dp</dimen>
+ <dimen name="mozac_browser_menu2_width">250dp</dimen>
+ <dimen name="mozac_browser_menu2_padding_vertical">0dp</dimen>
+
+ <!--Menu Item -->
+ <dimen name="mozac_browser_menu2_candidate_text_size">16sp</dimen>
+ <dimen name="mozac_browser_menu2_candidate_container_layout_height">48dp</dimen>
+ <dimen name="mozac_browser_menu2_candidate_container_padding_start">16dp</dimen>
+ <dimen name="mozac_browser_menu2_candidate_container_padding_end">16dp</dimen>
+ <!--Menu Item -->
+
+ <!--BrowserMenuDivider -->
+ <dimen name="mozac_browser_menu2_candidate_divider_height">1dp</dimen>
+ <!--BrowserMenuDivider -->
+
+ <!--BrowserMenuHighlightableItem -->
+ <dimen name="mozac_browser_menu2_icon_notification_dot_translate_x">4dp</dimen>
+ <dimen name="mozac_browser_menu2_icon_notification_dot_translate_y">-4dp</dimen>
+ <dimen name="mozac_browser_menu2_icon_notification_dot_size">8dp</dimen>
+ <!--BrowserMenuHighlightableItem -->
+
+ <!--BrowserMenuCheckbox -->
+ <dimen name="mozac_browser_menu2_checkbox_padding">12dp</dimen>
+ <!--BrowserMenuCheckbox -->
+
+ <!--BrowserMenuImageText-->
+
+ <!--Icon-->
+ <dimen name="mozac_browser_menu2_icon_width">24dp</dimen>
+ <dimen name="mozac_browser_menu2_icon_height">24dp</dimen>
+ <dimen name="mozac_browser_menu2_icon_text_size">14sp</dimen>
+ <!--Icon-->
+
+ <!--Label-->
+ <dimen name="mozac_browser_menu2_icon_padding_start">20dp</dimen>
+ <!--Label-->
+
+ <!--BrowserMenuImageText-->
+
+ <!-- BrowserMenuItemToolbar -->
+ <dimen name="mozac_browser_menu2_candidate_row_height">56dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..ebcceba5ad
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+<?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>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Highlighted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml
new file mode 100644
index 0000000000..eb2702e6f1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml
@@ -0,0 +1,88 @@
+<?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>
+
+ <style name="Mozac.Browser.Menu2" parent="">
+ <item name="cardBackgroundColor">@color/mozac_browser_menu2_background</item>
+ </style>
+
+ <!-- Item Divider -->
+ <style name="Mozac.Browser.Menu2.Candidate.Divider" parent="">
+ <item name="android:background">?android:attr/listDivider</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Candidate.Divider.Horizontal">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/mozac_browser_menu2_candidate_divider_height</item>
+ </style>
+ <!-- Item Divider -->
+
+ <style name="Mozac.Browser.Menu2.Candidate.Container" parent="">
+ <item name="android:layout_height">@dimen/mozac_browser_menu2_candidate_container_layout_height</item>
+ <item name="android:paddingStart">@dimen/mozac_browser_menu2_candidate_container_padding_start</item>
+ <item name="android:paddingEnd">@dimen/mozac_browser_menu2_candidate_container_padding_end</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Candidate.Text" parent="@android:style/TextAppearance.Material.Menu">
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:textSize">@dimen/mozac_browser_menu2_candidate_text_size</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:lines">1</item>
+ <item name="android:focusable">true</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <!-- BrowserMenuImageText -->
+ <style name="Mozac.Browser.Menu2.Icon" parent="">
+ <item name="android:layout_width">@dimen/mozac_browser_menu2_icon_width</item>
+ <item name="android:layout_height">@dimen/mozac_browser_menu2_icon_height</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Icon.Text" parent="Mozac.Browser.Menu2.Icon">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:textSize">@dimen/mozac_browser_menu2_icon_text_size</item>
+ <item name="android:lines">1</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Candidate.Label" parent="Mozac.Browser.Menu2.Candidate.Text">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+ <!-- BrowserMenuImageText -->
+
+ <!-- Animation -->
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuLeftTop" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_left_top</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuRightTop" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_right_top</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuLeftBottom" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_left_bottom</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuRightBottom" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_right_bottom</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuLeft" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_left</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuRight" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_right</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+ <!-- Animation -->
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt
new file mode 100644
index 0000000000..688b77b578
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt
@@ -0,0 +1,132 @@
+/* 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.browser.menu2
+
+import android.view.Gravity
+import android.widget.Button
+import android.widget.PopupWindow
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu2.ext.MenuPositioningData
+import mozilla.components.browser.menu2.ext.createAnchor
+import mozilla.components.browser.menu2.ext.createContainerView
+import mozilla.components.browser.menu2.ext.getTargetCoordinates
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuControllerTest {
+
+ @Test
+ fun `created popup window is displayed automatically`() {
+ val menu: MenuController = BrowserMenuController()
+ menu.submitList(listOf(DecorativeTextMenuCandidate("Hello")))
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertTrue(popup.isShowing)
+ }
+
+ @Test
+ fun `WHEN an empty list is submitted to the menu THEN the menu isn't shown`() {
+ val menu: MenuController = BrowserMenuController()
+ menu.submitList(emptyList())
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertFalse(popup.isShowing)
+ }
+
+ @Test
+ fun `observer is notified when submitList is called`() {
+ var submitted: List<MenuCandidate>? = null
+ val menu: MenuController = BrowserMenuController()
+ menu.register(
+ object : MenuController.Observer {
+ override fun onMenuListSubmit(list: List<MenuCandidate>) {
+ submitted = list
+ }
+
+ override fun onDismiss() = Unit
+ },
+ )
+
+ assertNull(submitted)
+
+ menu.submitList(listOf(DecorativeTextMenuCandidate("Hello")))
+ assertEquals(listOf(DecorativeTextMenuCandidate("Hello")), submitted)
+ }
+
+ @Test
+ fun `dismissing the browser menu will dismiss the popup`() {
+ var dismissed = false
+ val menu: MenuController = BrowserMenuController()
+ menu.submitList(listOf(DecorativeTextMenuCandidate("Hello")))
+
+ menu.register(
+ object : MenuController.Observer {
+ override fun onDismiss() {
+ dismissed = true
+ }
+
+ override fun onMenuListSubmit(list: List<MenuCandidate>) = Unit
+ },
+ )
+
+ menu.dismiss()
+ assertFalse(dismissed)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertTrue(popup.isShowing)
+ assertFalse(dismissed)
+
+ menu.dismiss()
+
+ assertFalse(popup.isShowing)
+ assertTrue(dismissed)
+ }
+
+ @Test
+ fun `WHEN displayPopup is called with provided positioning data THEN showAtLocation is called with provided positioning values & menu height and animation are set`() {
+ val containerView = createContainerView()
+ val popupWindow = Mockito.spy(PopupWindow())
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor)
+ val positioningData = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ popupWindow.displayPopup(positioningData)
+
+ assertEquals(containerView.measuredHeight, popupWindow.height)
+ assertEquals(positioningData.animation, popupWindow.animationStyle)
+
+ Mockito.verify(popupWindow).showAtLocation(
+ positioningData.anchor,
+ Gravity.NO_GRAVITY,
+ positioningData.x,
+ positioningData.y,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..13128cbad6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt
@@ -0,0 +1,99 @@
+/* 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.browser.menu2.adapter
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.Typeface
+import android.view.View
+import android.widget.CompoundButton
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class CompoundMenuCandidateViewHolderTest {
+
+ private val baseCandidate = CompoundMenuCandidate(
+ "hello",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ )
+ private lateinit var view: ConstraintLayout
+ private lateinit var compoundButton: CompoundButton
+
+ @Before
+ fun setup() {
+ val context: Context = mock()
+ view = mock()
+ compoundButton = mock()
+
+ doReturn(context).`when`(view).context
+ doReturn(compoundButton).`when`(view).findViewById<TextView>(R.id.label)
+
+ val resources: Resources = mock()
+ doReturn(resources).`when`(context).resources
+
+ doReturn(mock<Resources.Theme>()).`when`(context).theme
+ }
+
+ @Test
+ fun `sets container state and text on view`() {
+ val holder = CompoundCheckboxMenuCandidateViewHolder(view, mock(), mock())
+
+ holder.bind(baseCandidate)
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ verify(compoundButton).text = "hello"
+ verify(compoundButton).isChecked = false
+ verify(compoundButton).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(compoundButton).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+ }
+
+ @Test
+ fun `sets highlight effect`() {
+ val holder = CompoundSwitchMenuCandidateViewHolder(view, mock(), mock())
+
+ holder.bind(baseCandidate)
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view, never()).setBackgroundResource(anyInt())
+
+ holder.bind(baseCandidate.copy(effect = HighPriorityHighlightEffect(Color.RED)))
+ verify(view).setBackgroundColor(Color.RED)
+ verify(view, never()).setBackgroundResource(anyInt())
+
+ clearInvocations(view)
+
+ holder.bind(baseCandidate.copy(effect = null))
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view).setBackgroundResource(anyInt())
+ }
+
+ @Test
+ fun `sets change listener`() {
+ var dismissed = false
+ val holder = CompoundSwitchMenuCandidateViewHolder(view, mock()) { dismissed = true }
+
+ val candidate = baseCandidate.copy(onCheckedChange = mock())
+ holder.bind(candidate)
+ holder.onCheckedChanged(compoundButton, false)
+
+ assertTrue(dismissed)
+ verify(candidate.onCheckedChange).invoke(false)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..a3fde39c97
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt
@@ -0,0 +1,61 @@
+/* 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.browser.menu2.adapter
+
+import android.content.res.Resources
+import android.graphics.Typeface
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class DecorativeTextMenuCandidateViewHolderTest {
+
+ @Test
+ fun `sets container state and text on view`() {
+ val view: TextView = mock()
+ val holder = DecorativeTextMenuCandidateViewHolder(view, mock())
+
+ holder.bind(DecorativeTextMenuCandidate("hello"))
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ verify(view).text = "hello"
+ verify(view).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(view).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+ verify(view, never()).layoutParams = any()
+ }
+
+ @Test
+ fun `sets view height`() {
+ val view: TextView = mock()
+ val params = ViewGroup.LayoutParams(0, 0)
+ val resources: Resources = mock()
+ doReturn(params).`when`(view).layoutParams
+ doReturn(resources).`when`(view).resources
+ doReturn(48).`when`(resources).getDimensionPixelSize(anyInt())
+
+ val holder = DecorativeTextMenuCandidateViewHolder(view, mock())
+
+ holder.bind(DecorativeTextMenuCandidate("hello", height = 30))
+ assertEquals(30, params.height)
+ verify(view).layoutParams = params
+
+ clearInvocations(view)
+
+ holder.bind(DecorativeTextMenuCandidate("hello"))
+ assertEquals(48, params.height)
+ verify(view).layoutParams = params
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..493c1d517a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt
@@ -0,0 +1,24 @@
+/* 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.browser.menu2.adapter
+
+import android.view.View
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class DividerMenuCandidateViewHolderTest {
+
+ @Test
+ fun `sets visible and enabled state on divider view`() {
+ val view: View = mock()
+ val holder = DividerMenuCandidateViewHolder(view, mock())
+
+ holder.bind(DividerMenuCandidate())
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt
new file mode 100644
index 0000000000..144422e618
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt
@@ -0,0 +1,75 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MenuCandidateListAdapterTest {
+
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var dismiss: () -> Unit
+ private lateinit var reopenMenu: (NestedMenuCandidate) -> Unit
+ private lateinit var adapter: MenuCandidateListAdapter
+
+ @Before
+ fun setup() {
+ layoutInflater = spy(LayoutInflater.from(testContext))
+ dismiss = mock()
+ reopenMenu = mock()
+ adapter = MenuCandidateListAdapter(layoutInflater, dismiss, reopenMenu)
+ }
+
+ @Test
+ fun `items use layout resource as view type`() {
+ val items = listOf(
+ DecorativeTextMenuCandidate("one"),
+ TextMenuCandidate("two"),
+ CompoundMenuCandidate("three", false, end = CompoundMenuCandidate.ButtonType.CHECKBOX),
+ CompoundMenuCandidate("four", false, end = CompoundMenuCandidate.ButtonType.SWITCH),
+ DividerMenuCandidate(),
+ RowMenuCandidate(emptyList()),
+ )
+ adapter.submitList(items)
+
+ assertEquals(6, adapter.itemCount)
+ assertEquals(DecorativeTextMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(0))
+ assertEquals(TextMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(1))
+ assertEquals(CompoundCheckboxMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(2))
+ assertEquals(CompoundSwitchMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(3))
+ assertEquals(DividerMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(4))
+ assertEquals(RowMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(5))
+ }
+
+ @Test
+ fun `bind will be forwarded to item implementation`() {
+ adapter.submitList(
+ listOf(
+ DividerMenuCandidate(),
+ ),
+ )
+
+ val holder: DividerMenuCandidateViewHolder = mock()
+
+ adapter.onBindViewHolder(holder, 0)
+
+ verify(holder).bind(DividerMenuCandidate())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..ca8a0fea9a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt
@@ -0,0 +1,103 @@
+/* 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.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+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
+
+@RunWith(AndroidJUnit4::class)
+class RowMenuCandidateViewHolderTest {
+
+ private lateinit var view: LinearLayout
+ private lateinit var button: AppCompatImageButton
+ private lateinit var inflater: LayoutInflater
+
+ @Before
+ fun setup() {
+ view = mock()
+ button = spy(AppCompatImageButton(testContext))
+ inflater = mock()
+
+ doReturn(button).`when`(inflater).inflate(
+ SmallMenuCandidateViewHolder.layoutResource,
+ view,
+ false,
+ )
+ }
+
+ @Test
+ fun `sets container state on view`() {
+ val holder = RowMenuCandidateViewHolder(view, inflater, mock())
+
+ holder.bind(RowMenuCandidate(emptyList()))
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ verify(view, never()).addView(button)
+ }
+
+ @Test
+ fun `creates buttons for small items`() {
+ val holder = RowMenuCandidateViewHolder(view, inflater, mock())
+
+ holder.bind(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate("hello", DrawableMenuIcon(null)),
+ SmallMenuCandidate("hello", DrawableMenuIcon(null)),
+ ),
+ ),
+ )
+ verify(view, times(2)).addView(button)
+
+ clearInvocations(view)
+
+ holder.bind(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate("test", DrawableMenuIcon(null)),
+ SmallMenuCandidate("hello", DrawableMenuIcon(null)),
+ ),
+ ),
+ )
+ verify(view, never()).removeAllViews()
+ verify(view, never()).addView(button)
+ }
+
+ @Test
+ fun `binds buttons for small items`() {
+ val holder = RowMenuCandidateViewHolder(view, inflater, mock())
+
+ holder.bind(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate("hello", DrawableMenuIcon(null)),
+ ),
+ ),
+ )
+
+ verify(button).contentDescription = "hello"
+ verify(button).setImageDrawable(null)
+ verify(button).imageTintList = null
+ verify(button).visibility = View.VISIBLE
+ verify(button).isEnabled = true
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..f128297c2b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt
@@ -0,0 +1,130 @@
+/* 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.browser.menu2.adapter
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+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.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SmallMenuCandidateViewHolderTest {
+
+ private lateinit var view: AppCompatImageButton
+
+ @Before
+ fun setup() {
+ view = spy(AppCompatImageButton(testContext))
+ }
+
+ @Test
+ fun `binds button data`() {
+ val holder = SmallMenuCandidateViewHolder(view, mock())
+
+ verify(view).setOnClickListener(holder)
+
+ holder.bind(SmallMenuCandidate("hello", DrawableMenuIcon(null)))
+
+ verify(view).contentDescription = "hello"
+ verify(view).setImageDrawable(null)
+ verify(view).imageTintList = null
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+
+ clearInvocations(view)
+
+ holder.bind(
+ SmallMenuCandidate(
+ "hello",
+ DrawableMenuIcon(null, tint = Color.BLUE),
+ ),
+ )
+ verify(view).setImageDrawable(null)
+ verify(view).imageTintList = ColorStateList.valueOf(Color.BLUE)
+ }
+
+ @Test
+ fun `sets on click listener`() {
+ var dismissed = false
+ var clicked = false
+ val holder = SmallMenuCandidateViewHolder(view) { dismissed = true }
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ holder.bind(SmallMenuCandidate("hello", DrawableMenuIcon(null)))
+ dismissed = false
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ dismissed = false
+ holder.bind(
+ SmallMenuCandidate(
+ "hello",
+ DrawableMenuIcon(null),
+ ) {
+ clicked = true
+ },
+ )
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertTrue(clicked)
+ }
+
+ @Test
+ fun `sets on long click listener`() {
+ var dismissed = false
+ var clicked = false
+ val holder = SmallMenuCandidateViewHolder(view) { dismissed = true }
+
+ holder.onLongClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ holder.bind(SmallMenuCandidate("hello", DrawableMenuIcon(null)))
+ verify(view).setOnClickListener(holder)
+ verify(view, atLeastOnce()).isLongClickable = false
+ dismissed = false
+
+ assertFalse(holder.onLongClick(null))
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ dismissed = false
+ holder.bind(
+ SmallMenuCandidate(
+ "hello",
+ DrawableMenuIcon(null),
+ onLongClick = {
+ clicked = true
+ true
+ },
+ ) {},
+ )
+ verify(view).isLongClickable = true
+
+ assertTrue(holder.onLongClick(null))
+ assertTrue(dismissed)
+ assertTrue(clicked)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..1c7f61fe25
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt
@@ -0,0 +1,117 @@
+/* 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.browser.menu2.adapter
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.Typeface
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class TextMenuCandidateViewHolderTest {
+
+ private lateinit var view: ConstraintLayout
+ private lateinit var textView: TextView
+
+ @Before
+ fun setup() {
+ val context: Context = mock()
+ view = mock()
+ textView = mock()
+
+ doReturn(context).`when`(view).context
+ doReturn(textView).`when`(view).findViewById<TextView>(R.id.label)
+
+ val resources: Resources = mock()
+ doReturn(resources).`when`(context).resources
+
+ doReturn(mock<Resources.Theme>()).`when`(context).theme
+ }
+
+ @Test
+ fun `sets container state and text on view`() {
+ val holder = TextMenuCandidateViewHolder(view, mock(), mock())
+
+ verify(view).setOnClickListener(holder)
+
+ holder.bind(TextMenuCandidate("hello"))
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ verify(textView).text = "hello"
+ verify(textView).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(textView).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+ }
+
+ @Test
+ fun `sets on click listener`() {
+ var dismissed = false
+ var clicked = false
+ val holder = TextMenuCandidateViewHolder(view, mock()) { dismissed = true }
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ holder.bind(TextMenuCandidate("hello"))
+ dismissed = false
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ dismissed = false
+ holder.bind(TextMenuCandidate("hello") { clicked = true })
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertTrue(clicked)
+ }
+
+ @Test
+ fun `sets highlight effect`() {
+ val holder = TextMenuCandidateViewHolder(view, mock(), mock())
+
+ holder.bind(TextMenuCandidate("hello"))
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view, never()).setBackgroundResource(anyInt())
+
+ holder.bind(
+ TextMenuCandidate(
+ "hello",
+ effect = HighPriorityHighlightEffect(Color.RED),
+ ),
+ )
+ verify(view).setBackgroundColor(Color.RED)
+ verify(view, never()).setBackgroundResource(anyInt())
+
+ clearInvocations(view)
+
+ holder.bind(
+ TextMenuCandidate(
+ "hello",
+ effect = null,
+ ),
+ )
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view).setBackgroundResource(anyInt())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt
new file mode 100644
index 0000000000..9df9c642e8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt
@@ -0,0 +1,177 @@
+/* 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.browser.menu2.adapter.icons
+
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableButtonMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertFalse
+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.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class DrawableMenuIconViewHoldersTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var parent: ConstraintLayout
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var imageView: ImageView
+ private lateinit var imageButton: ImageButton
+
+ @Before
+ fun setup() {
+ parent = mock()
+ layoutInflater = mock()
+ imageView = mock()
+ imageButton = mock()
+
+ doReturn(testContext).`when`(parent).context
+ doReturn(testContext.resources).`when`(parent).resources
+ doReturn(imageView).`when`(layoutInflater).inflate(DrawableMenuIconViewHolder.layoutResource, parent, false)
+ doReturn(imageView).`when`(layoutInflater).inflate(AsyncDrawableMenuIconViewHolder.layoutResource, parent, false)
+ doReturn(imageButton).`when`(layoutInflater).inflate(DrawableButtonMenuIconViewHolder.layoutResource, parent, false)
+ doReturn(imageView).`when`(imageView).findViewById<TextView>(R.id.icon)
+ doReturn(imageButton).`when`(imageButton).findViewById<TextView>(R.id.icon)
+ }
+
+ @Test
+ fun `icon view holder sets icon on view`() {
+ val holder = DrawableMenuIconViewHolder(parent, layoutInflater, Side.END)
+
+ val drawable = mock<Drawable>()
+ holder.bindAndCast(DrawableMenuIcon(drawable), null)
+ verify(imageView).setImageDrawable(drawable)
+ verify(imageView).imageTintList = null
+ }
+
+ @Test
+ fun `button view holder sets icon on view`() {
+ val holder = DrawableButtonMenuIconViewHolder(parent, layoutInflater, Side.START) {}
+ verify(imageButton).setOnClickListener(holder)
+
+ val drawable = mock<Drawable>()
+ holder.bindAndCast(DrawableButtonMenuIcon(drawable), null)
+ verify(imageButton).setImageDrawable(drawable)
+ verify(imageButton).imageTintList = null
+ }
+
+ @Test
+ fun `async view holder sets icon on view`() = runTestOnMain {
+ val holder = AsyncDrawableMenuIconViewHolder(parent, layoutInflater, Side.END)
+
+ val drawable = mock<Drawable>()
+ holder.bindAndCast(AsyncDrawableMenuIcon(loadDrawable = { _, _ -> drawable }), null)
+ verify(imageView).setImageDrawable(null)
+ verify(imageView).setImageDrawable(drawable)
+ }
+
+ @Test
+ fun `async view holder uses loading icon and fallback icon`() = runTestOnMain {
+ val logger = mock<Logger>()
+ val holder = AsyncDrawableMenuIconViewHolder(parent, layoutInflater, Side.END, logger)
+
+ val loading = mock<Drawable>()
+ val fallback = mock<Drawable>()
+ holder.bindAndCast(
+ AsyncDrawableMenuIcon(
+ loadDrawable = { _, _ -> throw Exception() },
+ loadingDrawable = loading,
+ fallbackDrawable = fallback,
+ ),
+ null,
+ )
+ verify(imageView, never()).setImageDrawable(null)
+ verify(imageView).setImageDrawable(loading)
+ verify(imageView).setImageDrawable(fallback)
+ }
+
+ @Test
+ fun `icon holder removes image view on disconnect`() {
+ val holder = DrawableMenuIconViewHolder(parent, layoutInflater, Side.START)
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).addView(imageView)
+ clearInvocations(parent)
+
+ holder.disconnect()
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).removeView(imageView)
+ }
+
+ @Test
+ fun `button holder removes image view on disconnect`() {
+ val holder = DrawableButtonMenuIconViewHolder(parent, layoutInflater, Side.END) {}
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).addView(imageButton)
+ clearInvocations(parent)
+
+ holder.disconnect()
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).removeView(imageButton)
+ }
+
+ @Test
+ fun `async holder removes image view on disconnect`() {
+ val holder = AsyncDrawableMenuIconViewHolder(parent, layoutInflater, Side.START)
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).addView(imageView)
+ clearInvocations(parent)
+
+ holder.disconnect()
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).removeView(imageView)
+ }
+
+ @Test
+ fun `button view holder calls dismiss when clicked`() {
+ var dismissed = false
+ var clicked = false
+
+ val holder = DrawableButtonMenuIconViewHolder(parent, layoutInflater, Side.START) {
+ dismissed = true
+ }
+
+ holder.onClick(imageButton)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ val button = DrawableButtonMenuIcon(mock(), onClick = { clicked = true })
+ holder.bindAndCast(button, null)
+ holder.onClick(imageButton)
+ assertTrue(dismissed)
+ assertTrue(clicked)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt
new file mode 100644
index 0000000000..510389e995
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt
@@ -0,0 +1,86 @@
+/* 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.browser.menu2.adapter.icons
+
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.DrawableButtonMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class MenuIconAdapterTest {
+
+ private lateinit var layout: ConstraintLayout
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var dismiss: () -> Unit
+ private lateinit var adapter: MenuIconAdapter
+
+ @Before
+ fun setup() {
+ layout = mock()
+ layoutInflater = mock()
+ dismiss = mock()
+ adapter = spy(MenuIconAdapter(layout, layoutInflater, Side.START, dismiss))
+ }
+
+ @Test
+ fun `creates viewholder when icon type changes`() {
+ val mockViewHolder: MenuIconViewHolder<*> = mock()
+ doReturn(mockViewHolder).`when`(adapter).createViewHolder(any())
+
+ adapter.bind(null, null)
+ adapter.bind(TextMenuIcon("hello"), TextMenuIcon("world"))
+ adapter.bind(null, TextMenuIcon("world"))
+ verify(adapter, never()).createViewHolder(any())
+
+ adapter.bind(TextMenuIcon("hello"), null)
+ verify(adapter).createViewHolder(TextMenuIcon("hello"))
+
+ clearInvocations(adapter)
+ adapter.bind(TextMenuIcon("hello"), DrawableMenuIcon(mock()))
+ verify(adapter).createViewHolder(TextMenuIcon("hello"))
+ }
+
+ @Test
+ fun `disconnects viewholder when icon is changed`() {
+ val mockViewHolder: MenuIconViewHolder<*> = mock()
+ doReturn(mockViewHolder).`when`(adapter).createViewHolder(any())
+ adapter.bind(TextMenuIcon("hello"), null)
+
+ verify(mockViewHolder, never()).disconnect()
+ adapter.bind(DrawableButtonMenuIcon(mock()), TextMenuIcon("hello"))
+ verify(mockViewHolder).disconnect()
+
+ clearInvocations(mockViewHolder)
+ adapter.bind(null, DrawableButtonMenuIcon(mock()))
+ verify(mockViewHolder).disconnect()
+ }
+
+ @Test
+ fun `always bind new icon`() {
+ val mockViewHolder: MenuIconViewHolder<*> = mock()
+ doReturn(mockViewHolder).`when`(adapter).createViewHolder(any())
+
+ adapter.bind(TextMenuIcon("hello"), null)
+ verify(mockViewHolder).bindAndCast(TextMenuIcon("hello"), null)
+
+ adapter.bind(TextMenuIcon("hello"), TextMenuIcon("hello"))
+ verify(mockViewHolder).bindAndCast(TextMenuIcon("hello"), TextMenuIcon("hello"))
+
+ clearInvocations(mockViewHolder)
+ adapter.bind(null, TextMenuIcon("hello"))
+ verify(mockViewHolder, never()).bindAndCast(any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt
new file mode 100644
index 0000000000..bdba1c20f7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt
@@ -0,0 +1,68 @@
+/* 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.browser.menu2.adapter.icons
+
+import android.graphics.Typeface
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class TextMenuIconViewHolderTest {
+
+ private lateinit var parent: ConstraintLayout
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var textView: TextView
+
+ @Before
+ fun setup() {
+ parent = mock()
+ layoutInflater = mock()
+ textView = mock()
+
+ doReturn(testContext).`when`(parent).context
+ doReturn(textView).`when`(layoutInflater).inflate(TextMenuIconViewHolder.layoutResource, parent, false)
+ doReturn(textView).`when`(textView).findViewById<TextView>(R.id.icon)
+ }
+
+ @Test
+ fun `sets container state and text on view`() {
+ val holder = TextMenuIconViewHolder(parent, layoutInflater, Side.START)
+
+ holder.bindAndCast(TextMenuIcon("hello"), null)
+ verify(textView).text = "hello"
+ verify(textView).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(textView).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+ }
+
+ @Test
+ fun `removes text view on disconnect`() {
+ val holder = TextMenuIconViewHolder(parent, layoutInflater, Side.END)
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).addView(textView)
+ clearInvocations(parent)
+
+ holder.disconnect()
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).removeView(textView)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt
new file mode 100644
index 0000000000..d9be746793
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt
@@ -0,0 +1,856 @@
+/* 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.browser.menu2.ext
+
+import android.graphics.Rect
+import android.view.View
+import android.widget.PopupWindow
+import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.MenuCandidateListAdapter
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Orientation
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
+import kotlin.math.roundToInt
+
+/**
+ * [PopupWindow] const's.
+ */
+private const val HALF_MENU_ITEM = 0.5
+
+/**
+ * [PopupWindow] UI components.
+ */
+private const val SCREEN_ROOT_VIEW_HEIGHT = 1000
+private const val SCREEN_ROOT_VIEW_WIDTH = 400
+private const val MENU_ITEM_HEIGHT = 50
+private const val DEFAULT_ITEM_COUNT = 10
+private const val MENU_CONTAINER_WIDTH = 100
+private const val MENU_CONTAINER_PADDING = 10
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuPositioningTest {
+ private lateinit var popupWindow: PopupWindow
+ private lateinit var overlapStyle: MenuStyle
+ private lateinit var offsetStyle: MenuStyle
+ private lateinit var offsetOverlapStyle: MenuStyle
+
+ @Before
+ fun setUp() {
+ overlapStyle = MenuStyle(completelyOverlap = true)
+ offsetStyle = MenuStyle(horizontalOffset = 10, verticalOffset = 10)
+ offsetOverlapStyle =
+ MenuStyle(completelyOverlap = true, horizontalOffset = 10, verticalOffset = 10)
+
+ popupWindow = spy(PopupWindow())
+ }
+
+ @Test
+ fun `WHEN recycler view has no adapter THEN menu positioning data is null`() {
+ val containerView = createContainerView(hasAdapter = false)
+ val anchor = mock(View::class.java)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN container view has no measured height THEN menu positioning data is null`() {
+ val containerView = createContainerView()
+ val anchor = mock(View::class.java)
+
+ `when`(containerView.measuredHeight).thenReturn(0)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN container view has no measured width THEN menu positioning data is null`() {
+ val containerView = createContainerView()
+ val anchor = mock(View::class.java)
+
+ `when`(containerView.measuredWidth).thenReturn(0)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN recycler view has no measured height THEN menu positioning data is null`() {
+ val containerView = createContainerView(recyclerViewHeight = 0)
+ val anchor = mock(View::class.java)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN recycler view has no items THEN menu positioning data is null`() {
+ val containerView = createContainerView(recyclerViewHeight = 100, itemCount = 0)
+ val anchor = mock(View::class.java)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN orientation up & fits available height THEN original height & positioned from the bottom of the anchor with leftBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation up & fits available height & is RTL THEN original height & positioned from the bottom right of the anchor with rightBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation up & does not fit available height & fits down THEN original height & positioned from the top left of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation up & does not fit up or down THEN original height & positioned from the top left of the anchor with leftAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = 20 to notEnoughHeight
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation up & does not fit up or down & is RTL THEN original height & positioned from the bottom right of the anchor with rightAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = SCREEN_ROOT_VIEW_HEIGHT - 20 to notEnoughHeight
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation down & fits available height THEN original height & positioned from the top left of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation down & fits available height & is RTL THEN original height & positioned from the top right of the anchor with rightTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.DOWN)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN number of menu items don't fit available height THEN set popup with calculated height`() {
+ val containerView = createContainerView(itemCount = 22)
+ val anchor = createAnchor(20, 25)
+
+ val positioningData = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val availableHeight = anchor.rootView.measuredHeight
+ val maxAvailableHeightForRecyclerView = availableHeight - MENU_CONTAINER_PADDING
+ val numberOfItemsFitExactly =
+ (maxAvailableHeightForRecyclerView.toFloat() / MENU_ITEM_HEIGHT.toFloat()).roundToInt()
+ val numberOfItemsFitWithOverFlow = numberOfItemsFitExactly - HALF_MENU_ITEM
+ val updatedRecyclerViewHeight = (numberOfItemsFitWithOverFlow * MENU_ITEM_HEIGHT).toInt()
+ val calculatedHeight = updatedRecyclerViewHeight + MENU_CONTAINER_PADDING
+
+ assertEquals(calculatedHeight, positioningData?.containerHeight)
+ }
+
+ @Test
+ fun `WHEN orientation is null & menu fits down THEN original height & positioned from the top left of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = null)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation is null & menu does not fit & is RTL THEN showAtLocation with original height & positioned from the top right of the anchor with rightAnimation`() {
+ val containerView = createContainerView(itemCount = 20)
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = null)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation up & fits up THEN original height & bottom of the menu positioned exactly to the bottom of the anchor with leftBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation up & fits up & is RTL THEN original height & bottom right of the menu positioned exactly to the bottom right of the anchor with rightBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation down & fits down THEN showAtLocation with original height & positioned exactly from the top of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.DOWN)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation down & fits down & is RTL THEN original height & positioned exactly from the top of the anchor with rightTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.DOWN)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation up & does not fit up & fits down THEN original height & positioned exactly from the top left of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation up & does not fit up or down THEN original height & positioned exactly from the top left of the anchor with leftAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = 20 to notEnoughHeight
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation up & fits up THEN original height & positioned from the bottom left of the anchor with applied offsets and leftBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation up & fits up & is RTL THEN original height & positioned from the bottom right of the anchor with applied offsets and rightBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation down & fits down THEN original height & positioned from the top left of the anchor with applied offsets and leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation down & fits down & is RTL THEN original height & positioned from the top right of the anchor with applied offsets and rightTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation up & does not fit up & fits down THEN original height & positioned from the top left of the anchor with applied offsets and leftAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation up & does not fit up or down THEN original height & positioned from the top left of the anchor with applied offsets leftAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = 20 to notEnoughHeight
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation up & fits up THEN original height & positioned exactly from the bottom left of the anchor with applied offsets leftBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation up & fits up & is RTL THEN original height & positioned exactly to the bottom right of the anchor with applied offsets rightBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation down & fits down THEN original height & positioned exactly from the top of the anchor with applied offsets leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation down & fits down & is RTL THEN original height & positioned exactly from the top of the anchor with applied offsets and rightTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation up & fits down only THEN original height & positioned exactly from the top left of the anchor with applied offsets and leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation up & does not fit up or down THEN original height & positioned exactly from the top left of the anchor with applied offsets and leftAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = 20 to notEnoughHeight
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft,
+ )
+
+ assertEquals(expected, result)
+ }
+}
+
+internal fun createContainerView(
+ itemCount: Int = DEFAULT_ITEM_COUNT,
+ hasAdapter: Boolean = true,
+ recyclerViewHeight: Int? = null,
+): View {
+ val containerView = mock(View::class.java)
+
+ // Mimicking elevation added spacing.
+ val recyclerView = createRecyclerView(itemCount, hasAdapter, recyclerViewHeight)
+ val containerHeight = recyclerView.measuredHeight.plus(MENU_CONTAINER_PADDING)
+ val containerWidth = recyclerView.measuredWidth.plus(MENU_CONTAINER_PADDING)
+ `when`(containerView.measuredHeight).thenReturn(containerHeight)
+ `when`(containerView.measuredWidth).thenReturn(containerWidth)
+
+ doReturn(recyclerView).`when`(containerView)
+ .findViewById<RecyclerView>(R.id.mozac_browser_menu_recyclerView)
+ return containerView
+}
+
+private fun createRecyclerView(
+ itemCount: Int = DEFAULT_ITEM_COUNT,
+ hasAdapter: Boolean = true,
+ recyclerViewHeight: Int? = null,
+): RecyclerView {
+ val recyclerView = mock(RecyclerView::class.java)
+
+ if (hasAdapter) {
+ val adapter = mock(MenuCandidateListAdapter::class.java)
+ `when`(adapter.itemCount).thenReturn(itemCount)
+ `when`(recyclerView.adapter).thenReturn(adapter)
+ }
+
+ `when`(recyclerView.measuredHeight).thenReturn(
+ recyclerViewHeight
+ ?: (itemCount * MENU_ITEM_HEIGHT),
+ )
+
+ `when`(recyclerView.measuredWidth).thenReturn(MENU_CONTAINER_WIDTH)
+
+ return recyclerView
+}
+
+internal fun createAnchor(x: Int, y: Int, isRTL: Boolean = false): View {
+ val view = spy(View(testContext))
+
+ doAnswer { invocation ->
+ val locationOnScreen = (invocation.getArgument(0) as IntArray)
+ locationOnScreen[0] = x
+ locationOnScreen[1] = y
+ locationOnScreen
+ }.`when`(view).getLocationOnScreen(any())
+
+ doAnswer { invocation ->
+ val locationInWindow = (invocation.getArgument(0) as IntArray)
+ locationInWindow[0] = x
+ locationInWindow[1] = y
+ locationInWindow
+ }.`when`(view).getLocationInWindow(any())
+
+ if (isRTL) {
+ doReturn(ViewCompat.LAYOUT_DIRECTION_RTL).`when`(view).layoutDirection
+ } else {
+ doReturn(ViewCompat.LAYOUT_DIRECTION_LTR).`when`(view).layoutDirection
+ }
+ doReturn(10).`when`(view).height
+ doReturn(15).`when`(view).width
+
+ val anchorRootView = createAnchorRootView()
+ `when`(view.rootView).thenReturn(anchorRootView)
+
+ return view
+}
+
+private fun createAnchorRootView(): View {
+ val view = spy(View(testContext))
+ doAnswer { invocation ->
+ val displayFrame = (invocation.getArgument(0) as Rect)
+ displayFrame.left = 0
+ displayFrame.right = SCREEN_ROOT_VIEW_WIDTH
+ displayFrame.bottom = SCREEN_ROOT_VIEW_HEIGHT
+ displayFrame
+ }.`when`(view).getWindowVisibleDisplayFrame(any())
+
+ `when`(view.measuredHeight).thenReturn(SCREEN_ROOT_VIEW_HEIGHT)
+
+ return view
+}
+
+internal fun getTargetCoordinates(
+ anchorX: Int,
+ anchorY: Int,
+ containerView: View,
+ anchor: View,
+ style: MenuStyle? = null,
+ directionUp: Boolean = true,
+ directionRight: Boolean = true,
+): Pair<Int, Int> {
+ val targetX = getTargetX(
+ anchorX,
+ containerView,
+ anchor,
+ directionRight,
+ style?.completelyOverlap ?: false,
+ style?.horizontalOffset ?: 0,
+ )
+ val targetY = getTargetY(
+ anchorY,
+ containerView,
+ anchor,
+ directionUp,
+ style?.completelyOverlap ?: false,
+ style?.verticalOffset ?: 0,
+ )
+ return targetX to targetY
+}
+
+private fun getTargetX(
+ anchorX: Int,
+ containerView: View,
+ anchor: View,
+ directionRight: Boolean,
+ shouldOverlap: Boolean,
+ horizontalOffset: Int,
+): Int {
+ val targetX = when {
+ directionRight && shouldOverlap -> anchorX - (MENU_CONTAINER_PADDING / 2)
+ directionRight && !shouldOverlap -> anchorX
+ !directionRight && shouldOverlap -> anchorX - (containerView.measuredWidth - anchor.width) + (MENU_CONTAINER_PADDING / 2)
+ else -> anchorX - (containerView.measuredWidth - anchor.width)
+ }
+
+ return if (directionRight) {
+ targetX + horizontalOffset
+ } else {
+ targetX - horizontalOffset
+ }
+}
+
+private fun getTargetY(
+ anchorY: Int,
+ containerView: View,
+ anchor: View,
+ directionUp: Boolean,
+ shouldOverlap: Boolean,
+ verticalOffset: Int,
+): Int {
+ val targetY = when {
+ directionUp && shouldOverlap -> anchorY - (containerView.measuredHeight - anchor.height) + (MENU_CONTAINER_PADDING / 2)
+ directionUp && !shouldOverlap -> anchorY - (containerView.measuredHeight - anchor.height)
+ !directionUp && shouldOverlap -> anchorY - (MENU_CONTAINER_PADDING / 2)
+ else -> anchorY
+ }
+
+ return if (directionUp) {
+ targetY - verticalOffset
+ } else {
+ targetY + verticalOffset
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt
new file mode 100644
index 0000000000..6b5e406bd5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt
@@ -0,0 +1,143 @@
+/* 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.browser.menu2.ext
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ViewTest {
+
+ @Test
+ fun `apply container style affects visible and enabled`() {
+ val view: View = mock()
+ view.applyStyle(ContainerStyle(), ContainerStyle())
+ verify(view, never()).visibility = View.VISIBLE
+ verify(view, never()).isEnabled = true
+
+ view.applyStyle(ContainerStyle(isVisible = false), ContainerStyle())
+ verify(view).visibility = View.GONE
+ verify(view).isEnabled = true
+
+ view.applyStyle(ContainerStyle(isEnabled = false), ContainerStyle())
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = false
+ }
+
+ @Test
+ fun `apply text style affects text styling`() {
+ val view: TextView = mock()
+ view.applyStyle(TextStyle(), TextStyle())
+ verify(view, never()).textSize = anyFloat()
+ verify(view, never()).setTextColor(anyInt())
+ verify(view, never()).setTypeface(any(), anyInt())
+ verify(view, never()).textAlignment = anyInt()
+
+ view.applyStyle(TextStyle(), TextStyle(size = 0f))
+ verify(view, never()).textSize = anyFloat()
+ verify(view, never()).setTextColor(anyInt())
+ verify(view).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(view).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+
+ view.applyStyle(
+ TextStyle(
+ size = 4f,
+ color = Color.RED,
+ textStyle = Typeface.ITALIC,
+ textAlignment = View.TEXT_ALIGNMENT_CENTER,
+ ),
+ TextStyle(),
+ )
+ verify(view).textSize = 4f
+ verify(view).setTextColor(Color.RED)
+ verify(view).setTypeface(any(), eq(Typeface.ITALIC))
+ verify(view).textAlignment = View.TEXT_ALIGNMENT_CENTER
+ }
+
+ @Test
+ fun `apply drawable icon`() {
+ val view: ImageView = mock()
+ view.applyIcon(DrawableMenuIcon(null), DrawableMenuIcon(null))
+ verify(view, never()).setImageDrawable(any())
+ verify(view, never()).imageTintList = any()
+
+ val drawable: Drawable = mock()
+ view.applyIcon(DrawableMenuIcon(drawable), DrawableMenuIcon(null))
+ verify(view).setImageDrawable(drawable)
+ verify(view).imageTintList = null
+
+ view.applyIcon(DrawableMenuIcon(null, Color.RED), DrawableMenuIcon(drawable))
+ verify(view).setImageDrawable(drawable)
+ verify(view).imageTintList = ColorStateList.valueOf(Color.RED)
+ }
+
+ @Test
+ fun `apply notification effect`() {
+ val view: ImageView = mock()
+ view.applyNotificationEffect(LowPriorityHighlightEffect(Color.BLUE), LowPriorityHighlightEffect(Color.BLUE))
+ view.applyNotificationEffect(null, null)
+ verify(view, never()).visibility = anyInt()
+ verify(view, never()).imageTintList = any()
+
+ view.applyNotificationEffect(LowPriorityHighlightEffect(Color.BLUE), null)
+ verify(view).visibility = View.VISIBLE
+ verify(view).imageTintList = ColorStateList.valueOf(Color.BLUE)
+
+ view.applyNotificationEffect(null, LowPriorityHighlightEffect(Color.BLUE))
+ verify(view).visibility = View.GONE
+ verify(view).imageTintList = null
+ }
+
+ @Test
+ fun `sets highlight effect`() {
+ val view: View = mock()
+ doReturn(testContext).`when`(view).context
+
+ view.applyBackgroundEffect(null, null)
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view, never()).setBackgroundResource(anyInt())
+ verify(view, never()).foreground = any()
+
+ view.applyBackgroundEffect(HighPriorityHighlightEffect(Color.RED), HighPriorityHighlightEffect(Color.RED))
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view, never()).setBackgroundResource(anyInt())
+ verify(view, never()).foreground = any()
+
+ view.applyBackgroundEffect(HighPriorityHighlightEffect(Color.RED), null)
+ verify(view).setBackgroundColor(Color.RED)
+ verify(view, never()).setBackgroundResource(anyInt())
+ verify(view).foreground = any()
+
+ clearInvocations(view)
+
+ view.applyBackgroundEffect(null, HighPriorityHighlightEffect(Color.RED))
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view).setBackgroundResource(anyInt())
+ verify(view).foreground = null
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt
new file mode 100644
index 0000000000..29874f78ff
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt
@@ -0,0 +1,120 @@
+/* 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.browser.menu2.view
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.widget.ImageView
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+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.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MenuButton2Test {
+ private lateinit var menuController: MenuController
+ private lateinit var menuButton: MenuButton2
+ private lateinit var menuIcon: ImageView
+ private lateinit var highlightView: ImageView
+ private lateinit var notificationIconView: ImageView
+
+ @Before
+ fun setup() {
+ menuController = mock()
+ menuButton = MenuButton2(testContext)
+
+ val images = menuButton.children.mapNotNull { it as? AppCompatImageView }.toList()
+ highlightView = images[0]
+ menuIcon = images[1]
+ notificationIconView = images[2]
+ }
+
+ @Test
+ fun `changing menu controller dismisses old menu`() {
+ menuButton.menuController = menuController
+ menuButton.performClick()
+
+ verify(menuController).register(any(), eq(menuButton))
+ verify(menuController).show(menuButton)
+
+ menuButton.menuController = mock()
+ verify(menuController).dismiss()
+ verify(menuController).unregister(any())
+ }
+
+ @Test
+ fun `changing menu controller to null dismisses old menu`() {
+ menuButton.menuController = menuController
+ menuButton.performClick()
+
+ verify(menuController).register(any(), eq(menuButton))
+
+ menuButton.menuController = null
+ verify(menuController).dismiss()
+ verify(menuController).unregister(any())
+ }
+
+ @Test
+ fun `icon has content description`() {
+ assertEquals("Menu", menuIcon.contentDescription)
+ assertNotNull(menuIcon.drawable)
+ }
+
+ @Test
+ fun `icon color filter can be changed`() {
+ assertNull(menuIcon.colorFilter)
+
+ menuButton.setColorFilter(0xffffff)
+ assertEquals(PorterDuffColorFilter(0xffffff, PorterDuff.Mode.SRC_ATOP), menuIcon.colorFilter)
+ }
+
+ @Test
+ fun `icon displays high priority highlight`() {
+ assertFalse(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ menuButton.setEffect(
+ HighPriorityHighlightEffect(Color.RED),
+ )
+
+ assertTrue(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ assertEquals(ColorStateList.valueOf(Color.RED), highlightView.imageTintList)
+ }
+
+ @Test
+ fun `icon displays low priority highlight`() {
+ assertFalse(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ menuButton.setEffect(
+ LowPriorityHighlightEffect(Color.BLUE),
+ )
+
+ assertFalse(highlightView.isVisible)
+ assertTrue(notificationIconView.isVisible)
+
+ assertEquals(PorterDuffColorFilter(Color.BLUE, PorterDuff.Mode.SRC_ATOP), notificationIconView.colorFilter)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt
new file mode 100644
index 0000000000..5f39e68c56
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt
@@ -0,0 +1,92 @@
+/* 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.browser.menu2.view
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Build
+import androidx.cardview.widget.CardView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class MenuViewTest {
+
+ private val items = listOf(
+ DecorativeTextMenuCandidate("Hello"),
+ DecorativeTextMenuCandidate("World"),
+ )
+ private lateinit var menuView: MenuView
+ private lateinit var cardView: CardView
+ private lateinit var recyclerView: RecyclerView
+
+ @Before
+ fun setup() {
+ menuView = spy(MenuView(testContext))
+ cardView = menuView.findViewById(R.id.mozac_browser_menu_cardView)
+ recyclerView = menuView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ }
+
+ @Test
+ fun `recyclerview adapter will have items for every menu item`() {
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter!!
+ assertNotNull(recyclerAdapter)
+ assertEquals(0, recyclerAdapter.itemCount)
+
+ menuView.submitList(items)
+ assertEquals(2, recyclerAdapter.itemCount)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `setVisibleSide will be forwarded to scrollOnceToTheBottom on devices with Android M and below`() {
+ doNothing().`when`(menuView).scrollOnceToTheBottom(any())
+
+ menuView.setVisibleSide(Side.END)
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+
+ assertFalse(layoutManager.stackFromEnd)
+ verify(menuView).scrollOnceToTheBottom(any())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `setVisibleSide changes stackFromEnd on devices with Android N and above`() {
+ doNothing().`when`(menuView).scrollOnceToTheBottom(any())
+
+ menuView.setVisibleSide(Side.END)
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+
+ assertTrue(layoutManager.stackFromEnd)
+ verify(menuView, never()).scrollOnceToTheBottom(any())
+ }
+
+ @Test
+ fun `setStyle changes background color`() {
+ menuView.setStyle(MenuStyle(backgroundColor = Color.BLUE))
+ assertEquals(ColorStateList.valueOf(Color.BLUE), cardView.cardBackgroundColor)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..49324d83c5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,3 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
+
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/browser/session-storage/README.md b/mobile/android/android-components/components/browser/session-storage/README.md
new file mode 100644
index 0000000000..2d0a5e2e23
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Browser > Session-Storage
+
+This component offers mechanisms for saving and restoring a browsing session.
+
+## Usage
+
+### 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:browser-session-storage:{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/browser/session-storage/build.gradle b/mobile/android/android-components/components/browser/session-storage/build.gradle
new file mode 100644
index 0000000000..c302770254
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/build.gradle
@@ -0,0 +1,74 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArgument "clearPackageData", "true"
+ testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.browser.session.storage'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ api project(':browser-state')
+
+ implementation project(':concept-engine')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_browser
+ implementation ComponentsDependencies.androidx_drawerlayout
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+ implementation ComponentsDependencies.androidx_lifecycle_process
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-fakes')
+ testImplementation project(':support-test-libstate')
+ testImplementation project(':feature-tabs')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation project(':browser-engine-gecko')
+ androidTestImplementation project(':support-android-test')
+ androidTestImplementation project(':feature-tabs')
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.androidx_test_junit
+ androidTestImplementation ComponentsDependencies.androidx_test_uiautomator
+ androidTestImplementation ComponentsDependencies.androidx_espresso_core
+ androidTestImplementation ComponentsDependencies.testing_leakcanary
+ androidTestImplementation ComponentsDependencies.testing_mockwebserver
+}
+
+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/browser/session-storage/proguard-rules.pro b/mobile/android/android-components/components/browser/session-storage/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/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/browser/session-storage/src/androidTest/assets/index.html b/mobile/android/android-components/components/browser/session-storage/src/androidTest/assets/index.html
new file mode 100644
index 0000000000..b511ab2f19
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/androidTest/assets/index.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Restore Test</title>
+</head>
+<body>
+ <h1>Hello World</h1>
+</body>
+</html>
diff --git a/mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/FullRestoreTest.kt b/mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/FullRestoreTest.kt
new file mode 100644
index 0000000000..6b4dba965a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/FullRestoreTest.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.browser.session.storage
+
+import android.content.Context
+import android.os.SystemClock
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.engine.gecko.GeckoEngine
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.android.test.rules.WebserverRule
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.TimeoutException
+
+class FullRestoreTest {
+ @get:Rule
+ val webserverRule: WebserverRule = WebserverRule()
+
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ /**
+ * In this test we use GeckoView Nightly to load a test page and then we save and restore the
+ * browsing session, asserting that we end up with the same state as before.
+ */
+ @Test
+ fun loadAndRestore() {
+ val engine = createEngine()
+
+ run {
+ // -------------------------------------------------------------------------------------
+ // Set up
+ // -------------------------------------------------------------------------------------
+
+ val store = createStore(engine)
+ val tabsUseCases = TabsUseCases(store)
+
+ // -------------------------------------------------------------------------------------
+ // Add a tab
+ // -------------------------------------------------------------------------------------
+
+ tabsUseCases.addTab(webserverRule.url())
+
+ waitFor { store.state.selectedTab?.content?.title == "Restore Test" }
+ waitFor { store.state.selectedTab?.content?.loading == false }
+ waitFor { store.state.selectedTab?.engineState?.engineSessionState != null }
+
+ // -------------------------------------------------------------------------------------
+ // Save state
+ // -------------------------------------------------------------------------------------
+
+ val storage = SessionStorage(context, engine)
+ storage.save(store.state)
+ }
+
+ run {
+ // -------------------------------------------------------------------------------------
+ // Restore into new classes
+ // -------------------------------------------------------------------------------------
+
+ val storage = SessionStorage(context, engine)
+
+ val newStore = createStore(engine)
+ val newUseCases = TabsUseCases(newStore)
+
+ assertNull(newStore.state.selectedTab)
+
+ val browsingSession = storage.restore()
+ assertNotNull(browsingSession)
+ newUseCases.restore(browsingSession!!)
+
+ waitFor { newStore.state.selectedTab?.engineState != null }
+ waitFor { newStore.state.selectedTab?.content?.title == "Restore Test" }
+ }
+ }
+
+ private fun createEngine(): Engine {
+ return runBlocking(Dispatchers.Main) {
+ GeckoEngine(context)
+ }
+ }
+
+ private fun createStore(
+ engine: Engine,
+ ): BrowserStore {
+ return runBlocking(Dispatchers.Main) {
+ BrowserStore(middleware = EngineMiddleware.create(engine))
+ }
+ }
+}
+
+private fun waitFor(timeoutMs: Long = 10000, cadence: Long = 100, predicate: () -> Boolean) {
+ val start = SystemClock.elapsedRealtime()
+
+ do {
+ if (predicate()) {
+ return
+ }
+ Thread.sleep(cadence)
+ } while (SystemClock.elapsedRealtime() - start < timeoutMs)
+
+ throw TimeoutException()
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/RestoringBrowsingSessionsTest.kt b/mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/RestoringBrowsingSessionsTest.kt
new file mode 100644
index 0000000000..8b5d23c391
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/RestoringBrowsingSessionsTest.kt
@@ -0,0 +1,310 @@
+/* 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.browser.session.storage
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.engine.gecko.GeckoEngine
+import mozilla.components.support.ktx.util.writeString
+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.Test
+
+/**
+ * The test cases in this class restore actual browsing sessions taken from devices. If a test case
+ * breaks then this is a strong indication that the restore code changed in a non backwards compatible
+ * way and that users may lose data.
+ *
+ * The tests here do also restore the "engine" state using GeckoView Nightly. Breaking changes in
+ * GeckoView may also cause session restore to break in a non backwards compatible way. Such breakages
+ * need to be reported to the GeckoView team to be fixed before reaching release branches.
+ */
+@Suppress("TestFunctionName")
+class RestoringBrowsingSessionsTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ /**
+ * App: Sample Browser
+ * Version: 2020-12-16
+ * GeckoView: 84.0 Beta (3147bf02ca678985f814ea2b091a194a9ac22f71)
+ */
+ @Test
+ fun SampleBrowser_2020_12_16() = runBlocking(Dispatchers.Main) {
+ val json = """
+ {"version":2,"selectedTabId":"55a0b8ce-d2ef-4a19-b921-5d28fbe6906f","sessionStateTuples":[{"session":{"url":"https://www.mozilla.org/en-US/about/manifesto/","uuid":"497e5d15-c03d-434e-8b06-9186886c42ed","parentUuid":"","title":"The Mozilla Manifesto","contextId":null,"readerMode":false,"lastAccess":0},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1977,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"presState\":[{\"scroll\":\"0,19140\",\"scrollOriginDowngrade\":false,\"stateKey\":\"0>3>fp>1>0>\"}],\"persist\":true,\"cacheKey\":0,\"ID\":0,\"url\":\"https:\\/\\/www.mozilla.org\\/en-US\\/\",\"title\":\"Internet for people, not profit — Mozilla\",\"loadReplace\":true,\"docIdentifier\":2147483649,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7OTM1ZGYxMjAtMDJlNS00MzcyLTg5MTQtZmFhYmM5OTdmZjBmfSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7OTI1MzlkYjMtNGYwZS00YTliLWE0NzUtMjllMWMwZTdiYmUyfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7OTM1ZGYxMjAtMDJlNS00MzcyLTg5MTQtZmFhYmM5OTdmZjBmfSJ9fQ==\",\"resultPrincipalURI\":\"https:\\/\\/www.mozilla.org\\/en-US\\/\",\"hasUserInteraction\":true,\"originalURI\":\"http:\\/\\/mozilla.org\\/\",\"docshellUUID\":\"{94b7df7c-ce82-4489-9cbf-b317dde845db}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAHmh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLwAAAAEBAQAAAB5odHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8BAA==\",\"persist\":true,\"cacheKey\":0,\"ID\":2,\"csp\":\"CdntGuXUQAS\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\/\\/\\/\\/\\/wAAAbsBAAAAHmh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLwAAAAAAAAAFAAAACAAAAA8AAAAA\\/\\/\\/\\/\\/wAAAAD\\/\\/\\/\\/\\/AAAACAAAAA8AAAAXAAAABwAAABcAAAAHAAAAFwAAAAcAAAAeAAAAAAAAAAD\\/\\/\\/\\/\\/AAAAAP\\/\\/\\/\\/8AAAAA\\/\\/\\/\\/\\/wAAAAD\\/\\/\\/\\/\\/AQAAAAAAAAAAACx7IjEiOnsiMCI6Imh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLyJ9fQAAAAEAAAg3AGQAZQBmAGEAdQBsAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtADsAIABzAGMAcgBpAHAAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgACcAdQBuAHMAYQBmAGUALQBlAHYAYQBsACcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdABhAGcAbQBhAG4AYQBnAGUAcgAuAGcAbwBvAGcAbABlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AcwAuAHkAdABpAG0AZwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGMAZABuAC0AMwAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBwAHAALgBjAG8AbgB2AGUAcgB0AC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AZABhAHQAYQAuAHQAcgBhAGMAawAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AMQAwADAAMwAzADUAMAAuAHQAcgBhAGMAawAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AMQAwADAAMwAzADQAMwAuAHQAcgBhAGMAawAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AOwAgAHMAdAB5AGwAZQAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgAGgAdAB0AHAAcwA6AC8ALwBhAHAAcAAuAGMAbwBuAHYAZQByAHQALgBjAG8AbQA7ACAAYwBoAGkAbABkAC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALQBuAG8AYwBvAG8AawBpAGUALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB0AHIAYQBjAGsAZQByAHQAZQBzAHQALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHMAdQByAHYAZQB5AGcAaQB6AG0AbwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALgBjAG4AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AOwAgAGkAbQBnAC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgAGQAYQB0AGEAOgAgAGgAdAB0AHAAcwA6AC8ALwBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AZgBpAHIAZQBmAG8AeAAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AZABlACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBkAGsAIABoAHQAdABwAHMAOgAvAC8AYwByAGUAYQB0AGkAdgBlAGMAbwBtAG0AbwBuAHMALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwBjAGQAbgAtADMALgBjAG8AbgB2AGUAcgB0AGUAeABwAGUAcgBpAG0AZQBuAHQAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGwAbwBnAHMALgBjAG8AbgB2AGUAcgB0AGUAeABwAGUAcgBpAG0AZQBuAHQAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAZAAuAGQAbwB1AGIAbABlAGMAbABpAGMAawAuAG4AZQB0ADsAIABjAG8AbgBuAGUAYwB0AC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBsAG8AZwBzAC4AYwBvAG4AdgBlAHIAdABlAHgAcABlAHIAaQBtAGUAbgB0AHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwAxADAAMAAzADMANQAwAC4AbQBlAHQAcgBpAGMAcwAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AMQAwADAAMwAzADQAMwAuAG0AZQB0AHIAaQBjAHMALgBjAG8AbgB2AGUAcgB0AGUAeABwAGUAcgBpAG0AZQBuAHQAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHMAZQBuAHQAcgB5AC4AcAByAG8AZAAuAG0AbwB6AGEAdwBzAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC8AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuAC8AOwAgAGYAcgBhAG0AZQAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC0AbgBvAGMAbwBvAGsAaQBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdAByAGEAYwBrAGUAcgB0AGUAcwB0AC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBzAHUAcgB2AGUAeQBnAGkAegBtAG8ALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtAAA=\",\"url\":\"https:\\/\\/www.mozilla.org\\/en-US\\/about\\/manifesto\\/\",\"title\":\"The Mozilla Manifesto\",\"loadReplace\":false,\"docIdentifier\":2147483651,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8ifX0=\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8ifX0=\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8ifX0=\",\"resultPrincipalURI\":\"https:\\/\\/www.mozilla.org\\/en-US\\/about\\/manifesto\\/\",\"hasUserInteraction\":false,\"originalURI\":\"https:\\/\\/www.mozilla.org\\/en-US\\/about\\/manifesto\\/\",\"docshellUUID\":\"{94b7df7c-ce82-4489-9cbf-b317dde845db}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":2,\"userContextId\":0}}"}},{"session":{"url":"https://www.mozilla.org/en-US/firefox/new/?redirect_source=firefox-com","uuid":"55a0b8ce-d2ef-4a19-b921-5d28fbe6906f","parentUuid":"","title":"Download Firefox Browser — Fast, Private & Free — from Mozilla","contextId":null,"readerMode":false,"lastAccess":1608115274898},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1977,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"persist\":true,\"cacheKey\":0,\"ID\":4,\"url\":\"https:\\/\\/www.mozilla.org\\/en-US\\/firefox\\/new\\/?redirect_source=firefox-com\",\"title\":\"Download Firefox Browser — Fast, Private & Free — from Mozilla\",\"loadReplace\":true,\"docIdentifier\":2147483653,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MjAzN2M0MTktOTQwMC00NTdhLTgxNGEtNjNlODI0MWNmY2Y0fSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ODE4NzVjYzItZjZkMC00MDg5LWE4ZTAtMzlkY2I2MmMyOGU4fSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MjAzN2M0MTktOTQwMC00NTdhLTgxNGEtNjNlODI0MWNmY2Y0fSJ9fQ==\",\"resultPrincipalURI\":\"https:\\/\\/www.mozilla.org\\/en-US\\/firefox\\/new\\/?redirect_source=firefox-com\",\"hasUserInteraction\":true,\"originalURI\":\"http:\\/\\/firefox.com\\/\",\"docshellUUID\":\"{75f3a875-5651-4881-82f1-68f333aa2062}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":1,\"userContextId\":0}}"}}]}
+ """.trimIndent()
+
+ val engine = GeckoEngine(context)
+
+ assertTrue(
+ getFileForEngine(context, engine).writeString { json },
+ )
+
+ val storage = SessionStorage(context, engine)
+ val state = storage.restore()
+
+ assertNotNull(state)
+
+ assertEquals(2, state!!.tabs.size)
+ assertEquals("55a0b8ce-d2ef-4a19-b921-5d28fbe6906f", state.selectedTabId)
+
+ val tab1 = state.tabs[0]
+ val tab2 = state.tabs[1]
+
+ assertEquals("497e5d15-c03d-434e-8b06-9186886c42ed", tab1.state.id)
+ assertEquals("The Mozilla Manifesto", tab1.state.title)
+ assertNull(tab1.state.contextId)
+ assertNull(tab1.state.parentId)
+ assertFalse(tab1.state.readerState.active)
+ assertNull(tab1.state.readerState.activeUrl)
+ assertEquals(0, tab1.state.lastAccess)
+ assertNotNull(tab1.engineSessionState)
+
+ assertEquals("55a0b8ce-d2ef-4a19-b921-5d28fbe6906f", tab2.state.id)
+ assertEquals("Download Firefox Browser — Fast, Private & Free — from Mozilla", tab2.state.title)
+ assertNull(tab1.state.contextId)
+ assertNull(tab1.state.parentId)
+ assertFalse(tab2.state.readerState.active)
+ assertNull(tab2.state.readerState.activeUrl)
+ assertEquals(1608115274898, tab2.state.lastAccess)
+ assertNotNull(tab2.engineSessionState)
+ }
+
+ /**
+ * App: Firefox for Android (Fenix)
+ * Version: 80.1.3
+ */
+ @Test
+ fun Fenix_80_1_3() {
+ runBlocking(Dispatchers.Main) {
+ val json = """
+ {"version":1,"selectedSessionIndex":2,"sessionStateTuples":[{"session":{"url":"https:\/\/en.m.wikipedia.org\/wiki\/Fox","uuid":"f40b3385-7e75-47b3-9c54-15a02afa27a4","parentUuid":"","title":"Fox - Wikipedia","readerMode":false},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1823,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"presState\":[{\"scroll\":\"0,15488\",\"scrollOriginDowngrade\":false,\"stateKey\":\"0>3>fp>0>4>\"}],\"persist\":true,\"cacheKey\":0,\"ID\":0,\"url\":\"https:\\\/\\\/www.wikipedia.org\\\/\",\"title\":\"Wikipedia\",\"loadReplace\":false,\"docIdentifier\":1,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ODdkZDY0OTItYzMxMi00OGMzLWJmMDMtOGZmOGI5ZTI2NjlifSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7OTNhMGUwOWYtNTJmYS00MmZmLWFiNTEtMGEyMjQ0ZGIwMzdmfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ODdkZDY0OTItYzMxMi00OGMzLWJmMDMtOGZmOGI5ZTI2NjlifSJ9fQ==\",\"resultPrincipalURI\":\"https:\\\/\\\/www.wikipedia.org\\\/\",\"hasUserInteraction\":true,\"originalURI\":\"https:\\\/\\\/www.wikipedia.org\\\/\",\"docshellUUID\":\"{06a76895-6632-41cb-8dbe-0ce6d9d63418}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAGmh0dHBzOi8vd3d3Lndpa2lwZWRpYS5vcmcvAAAAAQEBAAAAGmh0dHBzOi8vd3d3Lndpa2lwZWRpYS5vcmcvAQA=\",\"persist\":true,\"cacheKey\":0,\"ID\":1,\"csp\":\"CdntGuXUQAS\\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\\/\\\/\\\/\\\/\\\/wAAAbsBAAAAGmh0dHBzOi8vd3d3Lndpa2lwZWRpYS5vcmcvAAAAAAAAAAUAAAAIAAAAEQAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8AAAAIAAAAEQAAABkAAAABAAAAGQAAAAEAAAAZAAAAAQAAABoAAAAAAAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8BAAAAAAAAAAAAKHsiMSI6eyIwIjoiaHR0cHM6Ly93d3cud2lraXBlZGlhLm9yZy8ifX0AAAAA\",\"url\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Fox\",\"title\":\"Fox - Wikipedia\",\"loadReplace\":true,\"docIdentifier\":2,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ZGY5YmEyNmItN2MyNy00YmFlLWI1ZGItYzYwZmVjYWRjMWQwfSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy53aWtpcGVkaWEub3JnLyJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ZGY5YmEyNmItN2MyNy00YmFlLWI1ZGItYzYwZmVjYWRjMWQwfSJ9fQ==\",\"resultPrincipalURI\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Fox\",\"hasUserInteraction\":false,\"originalURI\":\"https:\\\/\\\/www.wikipedia.org\\\/search-redirect.php?family=wikipedia&language=en&search=fox&language=en&go=Go\",\"docshellUUID\":\"{06a76895-6632-41cb-8dbe-0ce6d9d63418}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":2,\"userContextId\":0}}"}},{"session":{"url":"https:\/\/www.theverge.com\/2021\/1\/4\/22212347\/google-employees-contractors-announce-union-cwa-alphabet","uuid":"c0c7939d-b0be-408e-97f2-ef45bc8fb36e","parentUuid":"","title":"Google workers announce plans to unionize - The Verge","readerMode":false},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1823,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"presState\":[{\"scroll\":\"0,19281\",\"stateKey\":\"0>html>1\"}],\"persist\":true,\"cacheKey\":0,\"ID\":2,\"url\":\"https:\\\/\\\/www.theverge.com\\\/\",\"title\":\"The Verge\",\"structuredCloneVersion\":8,\"docIdentifier\":3,\"structuredCloneState\":\"AgAAAAAA8f8AAAAACAD\\\/\\\/wAAAAATAP\\\/\\\/\",\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7N2MxMmNkZjItODA1Ny00Nzk2LTkwMmItOTgyZjY0YTRlNjA2fSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MmJlZWNjOGQtMTUyNy00Yjk1LTg5MmYtZTE1ODMyNTkzNmIzfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7N2MxMmNkZjItODA1Ny00Nzk2LTkwMmItOTgyZjY0YTRlNjA2fSJ9fQ==\",\"resultPrincipalURI\":null,\"hasUserInteraction\":true,\"originalURI\":\"https:\\\/\\\/www.theverge.com\\\/\",\"docshellUUID\":\"{bb6f2e9a-23b7-4cd1-9521-1c41cf40059d}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAGWh0dHBzOi8vd3d3LnRoZXZlcmdlLmNvbS8AAAAIAQEAAAAZaHR0cHM6Ly93d3cudGhldmVyZ2UuY29tLwEA\",\"persist\":true,\"cacheKey\":0,\"ID\":21,\"csp\":\"CdntGuXUQAS\\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\\/\\\/\\\/\\\/\\\/wAAAbsBAAAAGWh0dHBzOi8vd3d3LnRoZXZlcmdlLmNvbS8AAAAAAAAABQAAAAgAAAAQAAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wAAAAgAAAAQAAAAGAAAAAEAAAAYAAAAAQAAABgAAAABAAAAGQAAAAAAAAAA\\\/\\\/\\\/\\\/\\\/wAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wEAAAAAAAAAAAAneyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19AAAAAgAAAWsAZABlAGYAYQB1AGwAdAAtAHMAcgBjACAAaAB0AHQAcABzADoAIABkAGEAdABhADoAIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgACcAdQBuAHMAYQBmAGUALQBlAHYAYQBsACcAOwAgAGMAaABpAGwAZAAtAHMAcgBjACAAaAB0AHQAcABzADoAIABkAGEAdABhADoAIABiAGwAbwBiADoAOwAgAGMAbwBuAG4AZQBjAHQALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABmAG8AbgB0AC0AcwByAGMAIABoAHQAdABwAHMAOgAgAGQAYQB0AGEAOgA7ACAAaQBtAGcALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABtAGUAZABpAGEALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABvAGIAagBlAGMAdAAtAHMAcgBjACAAaAB0AHQAcABzADoAOwAgAHMAYwByAGkAcAB0AC0AcwByAGMAIABoAHQAdABwAHMAOgAgAGQAYQB0AGEAOgAgAGIAbABvAGIAOgAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnACAAJwB1AG4AcwBhAGYAZQAtAGUAdgBhAGwAJwA7ACAAcwB0AHkAbABlAC0AcwByAGMAIABoAHQAdABwAHMAOgAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnADsAIABiAGwAbwBjAGsALQBhAGwAbAAtAG0AaQB4AGUAZAAtAGMAbwBuAHQAZQBuAHQAOwAgAHUAcABnAHIAYQBkAGUALQBpAG4AcwBlAGMAdQByAGUALQByAGUAcQB1AGUAcwB0AHMAAAAAABkAdQBwAGcAcgBhAGQAZQAtAGkAbgBzAGUAYwB1AHIAZQAtAHIAZQBxAHUAZQBzAHQAcwAB\",\"url\":\"https:\\\/\\\/www.theverge.com\\\/2021\\\/1\\\/4\\\/22212347\\\/google-employees-contractors-announce-union-cwa-alphabet\",\"title\":\"Google workers announce plans to unionize - The Verge\",\"structuredCloneVersion\":8,\"docIdentifier\":22,\"structuredCloneState\":\"AgAAAAAA8f8AAAAACAD\\\/\\\/wAAAAATAP\\\/\\\/\",\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19\",\"resultPrincipalURI\":null,\"hasUserInteraction\":false,\"originalURI\":\"https:\\\/\\\/www.theverge.com\\\/2021\\\/1\\\/4\\\/22212347\\\/google-employees-contractors-announce-union-cwa-alphabet\",\"docshellUUID\":\"{bb6f2e9a-23b7-4cd1-9521-1c41cf40059d}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":2,\"userContextId\":0}}"}},{"session":{"url":"https:\/\/getpocket.com\/explore?src=ff_android&cdn=0","uuid":"b1c1b5b1-3fea-4151-be11-e1989ff4e43c","parentUuid":"","title":"Discover stories on Pocket","readerMode":false},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1823,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"persist\":true,\"cacheKey\":0,\"ID\":32,\"url\":\"https:\\\/\\\/getpocket.com\\\/explore?src=ff_android&cdn=0\",\"title\":\"Discover stories on Pocket\",\"structuredCloneVersion\":8,\"docIdentifier\":34,\"structuredCloneState\":\"AgAAAAAA8f8AAAAACAD\\\/\\\/wMAAIAEAP\\\/\\\/dXJsAAAAAAABAACABAD\\\/\\\/y8AAAAAAAAAAgAAgAQA\\\/\\\/9hcwAAAAAAAB0AAIAEAP\\\/\\\/L2V4cGxvcmU\\\/c3JjPWZmX2FuZHJvaWQmY2RuPTAAAAAHAACABAD\\\/\\\/29wdGlvbnMAAAAAAAgA\\\/\\\/8AAAAAEwD\\\/\\\/wMAAIAEAP\\\/\\\/X19OAAAAAAABAAAAAgD\\\/\\\/wAAAAATAP\\\/\\\/\",\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MzI3MjYwYzYtYWE2Yi00OTRhLThlNDEtNTU3MzI0MWYzNGRifSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7YWU4ODAzOWUtMzYyZS00ZmZmLWJlZWUtNTMwYTI2ZDczNDUxfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MzI3MjYwYzYtYWE2Yi00OTRhLThlNDEtNTU3MzI0MWYzNGRifSJ9fQ==\",\"resultPrincipalURI\":null,\"hasUserInteraction\":true,\"originalURI\":\"https:\\\/\\\/getpocket.com\\\/explore?src=ff_android&cdn=0\",\"docshellUUID\":\"{49f25198-b9b3-4fd0-b347-721ef8af3b93}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":1,\"userContextId\":0}}"}},{"session":{"url":"moz-extension:\/\/c182f1be-1c1b-4305-a329-63268bb4b424\/readerview.html?url=https%3A%2F%2Fwww.pewresearch.org%2Ffact-tank%2F2020%2F12%2F11%2F20-striking-findings-from-2020%2F&id=137438953514","uuid":"ebca3522-5625-4791-9c13-46d0138a375b","parentUuid":"b1c1b5b1-3fea-4151-be11-e1989ff4e43c","title":"20 striking findings from 2020","readerMode":true,"readerModeArticleUrl":"https:\/\/www.pewresearch.org\/fact-tank\/2020\/12\/11\/20-striking-findings-from-2020\/"},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1977,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAzWh0dHBzOi8vZ2V0cG9ja2V0LmNvbS9yZWRpcmVjdD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cucGV3cmVzZWFyY2gub3JnJTJGZmFjdC10YW5rJTJGMjAyMCUyRjEyJTJGMTElMkYyMC1zdHJpa2luZy1maW5kaW5ncy1mcm9tLTIwMjAlMkYmaD0zMGUxMjNiMjU0YTk2MGU0NzI0MmNlMmM1ODJiMzgwZjk1ZTkwYzU4MGRiNmE2Yjk0NDdjMzE0NGExMjcyMmZkJm50PTAAAAADAQEAAAAWaHR0cHM6Ly9nZXRwb2NrZXQuY29tLwEA\",\"persist\":true,\"cacheKey\":0,\"ID\":3451829976,\"csp\":\"CdntGuXUQAS\\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\\/\\\/\\\/\\\/\\\/wAAAbsBAAAAzWh0dHBzOi8vZ2V0cG9ja2V0LmNvbS9yZWRpcmVjdD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cucGV3cmVzZWFyY2gub3JnJTJGZmFjdC10YW5rJTJGMjAyMCUyRjEyJTJGMTElMkYyMC1zdHJpa2luZy1maW5kaW5ncy1mcm9tLTIwMjAlMkYmaD0zMGUxMjNiMjU0YTk2MGU0NzI0MmNlMmM1ODJiMzgwZjk1ZTkwYzU4MGRiNmE2Yjk0NDdjMzE0NGExMjcyMmZkJm50PTAAAAAAAAAABQAAAAgAAAANAAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wAAAAgAAAANAAAAFQAAALgAAAAVAAAACQAAABUAAAABAAAAFgAAAAgAAAAA\\\/\\\/\\\/\\\/\\\/wAAAAD\\\/\\\/\\\/\\\/\\\/AAAAHwAAAK4AAAAA\\\/\\\/\\\/\\\/\\\/wEAAAAAAAAAAADbeyIxIjp7IjAiOiJodHRwczovL2dldHBvY2tldC5jb20vcmVkaXJlY3Q\\\/dXJsPWh0dHBzJTNBJTJGJTJGd3d3LnBld3Jlc2VhcmNoLm9yZyUyRmZhY3QtdGFuayUyRjIwMjAlMkYxMiUyRjExJTJGMjAtc3RyaWtpbmctZmluZGluZ3MtZnJvbS0yMDIwJTJGJmg9MzBlMTIzYjI1NGE5NjBlNDcyNDJjZTJjNTgyYjM4MGY5NWU5MGM1ODBkYjZhNmI5NDQ3YzMxNDRhMTI3MjJmZCZudD0wIn19AAAAAA==\",\"url\":\"https:\\\/\\\/www.pewresearch.org\\\/fact-tank\\\/2020\\\/12\\\/11\\\/20-striking-findings-from-2020\\\/\",\"title\":\"20 striking findings from 2020 | Pew Research Center\",\"loadReplace\":false,\"docIdentifier\":4,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2dldHBvY2tldC5jb20vcmVkaXJlY3Q\\\/dXJsPWh0dHBzJTNBJTJGJTJGd3d3LnBld3Jlc2VhcmNoLm9yZyUyRmZhY3QtdGFuayUyRjIwMjAlMkYxMiUyRjExJTJGMjAtc3RyaWtpbmctZmluZGluZ3MtZnJvbS0yMDIwJTJGJmg9MzBlMTIzYjI1NGE5NjBlNDcyNDJjZTJjNTgyYjM4MGY5NWU5MGM1ODBkYjZhNmI5NDQ3YzMxNDRhMTI3MjJmZCZudD0wIn19\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2dldHBvY2tldC5jb20vcmVkaXJlY3Q\\\/dXJsPWh0dHBzJTNBJTJGJTJGd3d3LnBld3Jlc2VhcmNoLm9yZyUyRmZhY3QtdGFuayUyRjIwMjAlMkYxMiUyRjExJTJGMjAtc3RyaWtpbmctZmluZGluZ3MtZnJvbS0yMDIwJTJGJmg9MzBlMTIzYjI1NGE5NjBlNDcyNDJjZTJjNTgyYjM4MGY5NWU5MGM1ODBkYjZhNmI5NDQ3YzMxNDRhMTI3MjJmZCZudD0wIn19\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2dldHBvY2tldC5jb20vcmVkaXJlY3Q\\\/dXJsPWh0dHBzJTNBJTJGJTJGd3d3LnBld3Jlc2VhcmNoLm9yZyUyRmZhY3QtdGFuayUyRjIwMjAlMkYxMiUyRjExJTJGMjAtc3RyaWtpbmctZmluZGluZ3MtZnJvbS0yMDIwJTJGJmg9MzBlMTIzYjI1NGE5NjBlNDcyNDJjZTJjNTgyYjM4MGY5NWU5MGM1ODBkYjZhNmI5NDQ3YzMxNDRhMTI3MjJmZCZudD0wIn19\",\"resultPrincipalURI\":\"https:\\\/\\\/www.pewresearch.org\\\/fact-tank\\\/2020\\\/12\\\/11\\\/20-striking-findings-from-2020\\\/\",\"hasUserInteraction\":false,\"originalURI\":\"https:\\\/\\\/www.pewresearch.org\\\/fact-tank\\\/2020\\\/12\\\/11\\\/20-striking-findings-from-2020\\\/\",\"docshellUUID\":\"{f5bfa81e-7e4c-4d07-ae5a-bc0219ca845a}\"},{\"persist\":true,\"cacheKey\":0,\"ID\":4,\"url\":\"moz-extension:\\\/\\\/c182f1be-1c1b-4305-a329-63268bb4b424\\\/readerview.html?url=https%3A%2F%2Fwww.pewresearch.org%2Ffact-tank%2F2020%2F12%2F11%2F20-striking-findings-from-2020%2F&id=137438953514\",\"title\":\"20 striking findings from 2020\",\"docIdentifier\":5,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vYzE4MmYxYmUtMWMxYi00MzA1LWEzMjktNjMyNjhiYjRiNDI0L3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cucGV3cmVzZWFyY2gub3JnJTJGZmFjdC10YW5rJTJGMjAyMCUyRjEyJTJGMTElMkYyMC1zdHJpa2luZy1maW5kaW5ncy1mcm9tLTIwMjAlMkYmaWQ9MTM3NDM4OTUzNTE0In19\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vYzE4MmYxYmUtMWMxYi00MzA1LWEzMjktNjMyNjhiYjRiNDI0L3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cucGV3cmVzZWFyY2gub3JnJTJGZmFjdC10YW5rJTJGMjAyMCUyRjEyJTJGMTElMkYyMC1zdHJpa2luZy1maW5kaW5ncy1mcm9tLTIwMjAlMkYmaWQ9MTM3NDM4OTUzNTE0In19\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vYzE4MmYxYmUtMWMxYi00MzA1LWEzMjktNjMyNjhiYjRiNDI0L3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cucGV3cmVzZWFyY2gub3JnJTJGZmFjdC10YW5rJTJGMjAyMCUyRjEyJTJGMTElMkYyMC1zdHJpa2luZy1maW5kaW5ncy1mcm9tLTIwMjAlMkYmaWQ9MTM3NDM4OTUzNTE0In19\",\"resultPrincipalURI\":null,\"hasUserInteraction\":false,\"docshellUUID\":\"{f5bfa81e-7e4c-4d07-ae5a-bc0219ca845a}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":2,\"userContextId\":0}}"}}]}
+ """.trimIndent()
+
+ val engine = GeckoEngine(context)
+
+ assertTrue(
+ getFileForEngine(context, engine).writeString { json },
+ )
+
+ val storage = SessionStorage(context, engine)
+ val state = storage.restore()
+
+ assertNotNull(state)
+
+ assertEquals(4, state!!.tabs.size)
+ assertEquals("b1c1b5b1-3fea-4151-be11-e1989ff4e43c", state.selectedTabId)
+
+ state.tabs[0].state.apply {
+ assertEquals("f40b3385-7e75-47b3-9c54-15a02afa27a4", id)
+ assertEquals("Fox - Wikipedia", title)
+ assertEquals("https://en.m.wikipedia.org/wiki/Fox", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertFalse(readerState.active)
+ assertNull(readerState.activeUrl)
+ assertEquals(0, lastAccess)
+ assertNotNull(state)
+ }
+
+ state.tabs[1].state.apply {
+ assertEquals("c0c7939d-b0be-408e-97f2-ef45bc8fb36e", id)
+ assertEquals("Google workers announce plans to unionize - The Verge", title)
+ assertEquals("https://www.theverge.com/2021/1/4/22212347/google-employees-contractors-announce-union-cwa-alphabet", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertFalse(readerState.active)
+ assertNull(readerState.activeUrl)
+ assertEquals(0, lastAccess)
+ assertNotNull(state)
+ }
+
+ state.tabs[2].state.apply {
+ assertEquals("b1c1b5b1-3fea-4151-be11-e1989ff4e43c", id)
+ assertEquals("Discover stories on Pocket", title)
+ assertEquals("https://getpocket.com/explore?src=ff_android&cdn=0", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertFalse(readerState.active)
+ assertNull(readerState.activeUrl)
+ assertEquals(0, lastAccess)
+ assertNotNull(state)
+ }
+
+ state.tabs[3].state.apply {
+ assertEquals("ebca3522-5625-4791-9c13-46d0138a375b", id)
+ assertEquals("20 striking findings from 2020", title)
+ assertEquals("moz-extension://c182f1be-1c1b-4305-a329-63268bb4b424/readerview.html?url=https%3A%2F%2Fwww.pewresearch.org%2Ffact-tank%2F2020%2F12%2F11%2F20-striking-findings-from-2020%2F&id=137438953514", url)
+ assertNull(contextId)
+ assertEquals("b1c1b5b1-3fea-4151-be11-e1989ff4e43c", parentId)
+ assertTrue(readerState.active)
+ assertEquals("https://www.pewresearch.org/fact-tank/2020/12/11/20-striking-findings-from-2020/", readerState.activeUrl)
+ assertEquals(0, lastAccess)
+ assertNotNull(state)
+ }
+ }
+ }
+
+ /**
+ * App: Firefox for Android (Fenix)
+ * Version: 83.1.0
+ */
+ @Test
+ fun Fenix_83_1_0() {
+ runBlocking(Dispatchers.Main) {
+ val json = """
+ {"version":2,"selectedTabId":"cbea9370-719e-47ef-931b-dee03f15761f","sessionStateTuples":[{"session":{"url":"https://byo.com/projects/","uuid":"ae4e089e-7efe-4f7f-a2c5-290308119f64","parentUuid":"","title":"Projects - Brew Your Own","contextId":null,"readerMode":false,"lastAccess":1609762083437},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1823,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"persist\":true,\"cacheKey\":0,\"ID\":1,\"url\":\"https:\\/\\/byo.com\\/\",\"title\":\"Home - Brew Your Own\",\"loadReplace\":true,\"docIdentifier\":2147483650,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7YTBmMjQ5NDAtNWU3Mi00OGQ0LWI4ZTAtODE1YzMyY2MyMTQyfSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7Njc0NmEwYjYtNzIyMi00YmRkLTllZjUtZDQ5Mjk0ZDUxYjIxfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7YTBmMjQ5NDAtNWU3Mi00OGQ0LWI4ZTAtODE1YzMyY2MyMTQyfSJ9fQ==\",\"resultPrincipalURI\":\"https:\\/\\/byo.com\\/\",\"hasUserInteraction\":true,\"originalURI\":\"http:\\/\\/byo.com\\/\",\"docshellUUID\":\"{fa2256f5-042f-4203-900b-3ec7bedad886}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAEGh0dHBzOi8vYnlvLmNvbS8AAAAFAQEAAAAQaHR0cHM6Ly9ieW8uY29tLwEA\",\"persist\":true,\"cacheKey\":0,\"ID\":8,\"csp\":\"CdntGuXUQAS\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\/\\/\\/\\/\\/wAAAbsBAAAAEGh0dHBzOi8vYnlvLmNvbS8AAAAAAAAABQAAAAgAAAAHAAAAAP\\/\\/\\/\\/8AAAAA\\/\\/\\/\\/\\/wAAAAgAAAAHAAAADwAAAAEAAAAPAAAAAQAAAA8AAAABAAAAEAAAAAAAAAAA\\/\\/\\/\\/\\/wAAAAD\\/\\/\\/\\/\\/AAAAAP\\/\\/\\/\\/8AAAAA\\/\\/\\/\\/\\/wEAAAAAAAAAAAAeeyIxIjp7IjAiOiJodHRwczovL2J5by5jb20vIn19AAAAAA==\",\"url\":\"https:\\/\\/byo.com\\/projects\\/\",\"title\":\"Projects - Brew Your Own\",\"loadReplace\":false,\"docIdentifier\":2147483657,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2J5by5jb20vIn19\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2J5by5jb20vIn19\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2J5by5jb20vIn19\",\"resultPrincipalURI\":\"https:\\/\\/byo.com\\/projects\\/\",\"hasUserInteraction\":false,\"originalURI\":\"https:\\/\\/byo.com\\/projects\\/\",\"docshellUUID\":\"{fa2256f5-042f-4203-900b-3ec7bedad886}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":2,\"userContextId\":0}}"}},{"session":{"url":"https://www.theverge.com/2021/1/3/22206649/samsung-officially-confirms-galaxy-s21-event-on-january-14th","uuid":"cbea9370-719e-47ef-931b-dee03f15761f","parentUuid":"","title":"Samsung officially confirms Galaxy S21 event for January 14th","contextId":null,"readerMode":true,"lastAccess":1609762120069,"readerModeArticleUrl":"https://www.theverge.com/2021/1/3/22206649/samsung-officially-confirms-galaxy-s21-event-on-january-14th"},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1977,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"persist\":true,\"cacheKey\":0,\"ID\":3444447746,\"url\":\"https:\\/\\/www.theverge.com\\/\",\"title\":\"The Verge\",\"structuredCloneVersion\":8,\"docIdentifier\":4,\"structuredCloneState\":\"AgAAAAAA8f8AAAAACAD\\/\\/wAAAAATAP\\/\\/\",\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7NGJlMDM3YmUtNGZiZC00YjJkLTg4YjUtYWNiMWQ5YjZlMjY1fSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7NzJhNDE1ZDEtOWM5My00M2Y2LTg3NWQtMGRhNzgxMGUzMjhmfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7NGJlMDM3YmUtNGZiZC00YjJkLTg4YjUtYWNiMWQ5YjZlMjY1fSJ9fQ==\",\"resultPrincipalURI\":null,\"hasUserInteraction\":true,\"originalURI\":\"https:\\/\\/www.theverge.com\\/\",\"docshellUUID\":\"{cd02153d-b7f7-429d-bce3-8f2fd2005ea4}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAGWh0dHBzOi8vd3d3LnRoZXZlcmdlLmNvbS8AAAAIAQEAAAAZaHR0cHM6Ly93d3cudGhldmVyZ2UuY29tLwEA\",\"persist\":true,\"cacheKey\":0,\"ID\":3444447767,\"csp\":\"CdntGuXUQAS\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\/\\/\\/\\/\\/wAAAbsBAAAAGWh0dHBzOi8vd3d3LnRoZXZlcmdlLmNvbS8AAAAAAAAABQAAAAgAAAAQAAAAAP\\/\\/\\/\\/8AAAAA\\/\\/\\/\\/\\/wAAAAgAAAAQAAAAGAAAAAEAAAAYAAAAAQAAABgAAAABAAAAGQAAAAAAAAAA\\/\\/\\/\\/\\/wAAAAD\\/\\/\\/\\/\\/AAAAAP\\/\\/\\/\\/8AAAAA\\/\\/\\/\\/\\/wEAAAAAAAAAAAAneyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19AAAAAgAAAWsAZABlAGYAYQB1AGwAdAAtAHMAcgBjACAAaAB0AHQAcABzADoAIABkAGEAdABhADoAIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgACcAdQBuAHMAYQBmAGUALQBlAHYAYQBsACcAOwAgAGMAaABpAGwAZAAtAHMAcgBjACAAaAB0AHQAcABzADoAIABkAGEAdABhADoAIABiAGwAbwBiADoAOwAgAGMAbwBuAG4AZQBjAHQALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABmAG8AbgB0AC0AcwByAGMAIABoAHQAdABwAHMAOgAgAGQAYQB0AGEAOgA7ACAAaQBtAGcALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABtAGUAZABpAGEALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABvAGIAagBlAGMAdAAtAHMAcgBjACAAaAB0AHQAcABzADoAOwAgAHMAYwByAGkAcAB0AC0AcwByAGMAIABoAHQAdABwAHMAOgAgAGQAYQB0AGEAOgAgAGIAbABvAGIAOgAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnACAAJwB1AG4AcwBhAGYAZQAtAGUAdgBhAGwAJwA7ACAAcwB0AHkAbABlAC0AcwByAGMAIABoAHQAdABwAHMAOgAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnADsAIABiAGwAbwBjAGsALQBhAGwAbAAtAG0AaQB4AGUAZAAtAGMAbwBuAHQAZQBuAHQAOwAgAHUAcABnAHIAYQBkAGUALQBpAG4AcwBlAGMAdQByAGUALQByAGUAcQB1AGUAcwB0AHMAAAAAABkAdQBwAGcAcgBhAGQAZQAtAGkAbgBzAGUAYwB1AHIAZQAtAHIAZQBxAHUAZQBzAHQAcwAB\",\"url\":\"https:\\/\\/www.theverge.com\\/2021\\/1\\/3\\/22206649\\/samsung-officially-confirms-galaxy-s21-event-on-january-14th\",\"title\":\"Samsung officially confirms Galaxy S21 event for January 14th - The Verge\",\"structuredCloneVersion\":8,\"docIdentifier\":5,\"structuredCloneState\":\"AgAAAAAA8f8AAAAACAD\\/\\/wAAAAATAP\\/\\/\",\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19\",\"resultPrincipalURI\":null,\"hasUserInteraction\":false,\"originalURI\":\"https:\\/\\/www.theverge.com\\/2021\\/1\\/3\\/22206649\\/samsung-officially-confirms-galaxy-s21-event-on-january-14th\",\"docshellUUID\":\"{cd02153d-b7f7-429d-bce3-8f2fd2005ea4}\"},{\"persist\":true,\"cacheKey\":0,\"ID\":5,\"url\":\"moz-extension:\\/\\/de686481-5025-4447-9228-8cc85aa1acbd\\/readerview.html?url=https%3A%2F%2Fwww.theverge.com%2F2021%2F1%2F3%2F22206649%2Fsamsung-officially-confirms-galaxy-s21-event-on-january-14th&id=137438953499\",\"title\":\"Samsung officially confirms Galaxy S21 event for January 14th\",\"docIdentifier\":6,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vZGU2ODY0ODEtNTAyNS00NDQ3LTkyMjgtOGNjODVhYTFhY2JkL3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cudGhldmVyZ2UuY29tJTJGMjAyMSUyRjElMkYzJTJGMjIyMDY2NDklMkZzYW1zdW5nLW9mZmljaWFsbHktY29uZmlybXMtZ2FsYXh5LXMyMS1ldmVudC1vbi1qYW51YXJ5LTE0dGgmaWQ9MTM3NDM4OTUzNDk5In19\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vZGU2ODY0ODEtNTAyNS00NDQ3LTkyMjgtOGNjODVhYTFhY2JkL3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cudGhldmVyZ2UuY29tJTJGMjAyMSUyRjElMkYzJTJGMjIyMDY2NDklMkZzYW1zdW5nLW9mZmljaWFsbHktY29uZmlybXMtZ2FsYXh5LXMyMS1ldmVudC1vbi1qYW51YXJ5LTE0dGgmaWQ9MTM3NDM4OTUzNDk5In19\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vZGU2ODY0ODEtNTAyNS00NDQ3LTkyMjgtOGNjODVhYTFhY2JkL3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cudGhldmVyZ2UuY29tJTJGMjAyMSUyRjElMkYzJTJGMjIyMDY2NDklMkZzYW1zdW5nLW9mZmljaWFsbHktY29uZmlybXMtZ2FsYXh5LXMyMS1ldmVudC1vbi1qYW51YXJ5LTE0dGgmaWQ9MTM3NDM4OTUzNDk5In19\",\"resultPrincipalURI\":null,\"hasUserInteraction\":false,\"docshellUUID\":\"{cd02153d-b7f7-429d-bce3-8f2fd2005ea4}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":3,\"userContextId\":0}}"}}]}
+ """.trimIndent()
+
+ val engine = GeckoEngine(context)
+
+ assertTrue(
+ getFileForEngine(context, engine).writeString { json },
+ )
+
+ val storage = SessionStorage(context, engine)
+ val state = storage.restore()
+
+ assertNotNull(state)
+
+ assertEquals(2, state!!.tabs.size)
+ assertEquals("cbea9370-719e-47ef-931b-dee03f15761f", state.selectedTabId)
+
+ state.tabs[0].state.apply {
+ assertEquals("ae4e089e-7efe-4f7f-a2c5-290308119f64", id)
+ assertEquals("Projects - Brew Your Own", title)
+ assertEquals("https://byo.com/projects/", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertFalse(readerState.active)
+ assertNull(readerState.activeUrl)
+ assertEquals(1609762083437, lastAccess)
+ assertNotNull(state)
+ }
+
+ state.tabs[1].state.apply {
+ assertEquals("cbea9370-719e-47ef-931b-dee03f15761f", id)
+ assertEquals("Samsung officially confirms Galaxy S21 event for January 14th", title)
+ assertEquals("https://www.theverge.com/2021/1/3/22206649/samsung-officially-confirms-galaxy-s21-event-on-january-14th", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertTrue(readerState.active)
+ assertEquals("https://www.theverge.com/2021/1/3/22206649/samsung-officially-confirms-galaxy-s21-event-on-january-14th", readerState.activeUrl)
+ assertEquals(1609762120069, lastAccess)
+ assertNotNull(state)
+ }
+ }
+ }
+
+ /**
+ * App: Firefox for Android (Fenix)
+ * Version: master branch (GeckoView Nightly), 2021-01-05
+ */
+ @Test
+ fun Fenix_Master_2021_01_5() {
+ runBlocking(Dispatchers.Main) {
+ val json = """
+ {"version":2,"selectedTabId":"7f4fd2c9-2bb1-4c23-a06c-7fbd2b78922f","sessionStateTuples":[{"session":{"url":"https://airhorner.com/","uuid":"0e556bb8-9120-48af-bc93-2bba0d4ec346","parentUuid":"","title":"The Air Horner","contextId":null,"readerMode":false,"lastAccess":1609784156118},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1823,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"persist\":true,\"cacheKey\":0,\"ID\":0,\"url\":\"https:\\/\\/airhorner.com\\/\",\"title\":\"The Air Horner\",\"loadReplace\":true,\"docIdentifier\":2147483649,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7YjRjZTU0NjItMGQ0Yy00MDc3LTgwMzctYjk2YTI5MGEyMDRifSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7NmE1YzZhODItODdlNy00ODQ0LTljMWItYjY0ZWRiZTA1MDY1fSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7YjRjZTU0NjItMGQ0Yy00MDc3LTgwMzctYjk2YTI5MGEyMDRifSJ9fQ==\",\"resultPrincipalURI\":\"https:\\/\\/airhorner.com\\/\",\"hasUserInteraction\":false,\"originalURI\":\"http:\\/\\/airhorner.com\\/\",\"docshellUUID\":\"{16bc3b14-e47f-4839-b809-095f0b0f79b4}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":1,\"userContextId\":0}}"}},{"session":{"url":"https://www.wikipedia.org/","uuid":"7f4fd2c9-2bb1-4c23-a06c-7fbd2b78922f","parentUuid":"","title":"","contextId":null,"readerMode":false,"lastAccess":0},"engineSession":{}}]}
+ """.trimIndent()
+
+ val engine = GeckoEngine(context)
+
+ assertTrue(
+ getFileForEngine(context, engine).writeString { json },
+ )
+
+ val storage = SessionStorage(context, engine)
+ val state = storage.restore()
+
+ assertNotNull(state)
+
+ assertEquals(2, state!!.tabs.size)
+ assertEquals("7f4fd2c9-2bb1-4c23-a06c-7fbd2b78922f", state.selectedTabId)
+
+ state.tabs[0].state.apply {
+ assertEquals("0e556bb8-9120-48af-bc93-2bba0d4ec346", id)
+ assertEquals("The Air Horner", title)
+ assertEquals("https://airhorner.com/", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertFalse(readerState.active)
+ assertNull(readerState.activeUrl)
+ assertEquals(1609784156118, lastAccess)
+ assertNotNull(state)
+ }
+
+ state.tabs[1].state.apply {
+ assertEquals("7f4fd2c9-2bb1-4c23-a06c-7fbd2b78922f", id)
+ assertEquals("", title)
+ assertEquals("https://www.wikipedia.org/", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertFalse(readerState.active)
+ assertNull(readerState.activeUrl)
+ assertEquals(0, lastAccess)
+ assertNotNull(state)
+ }
+ }
+ }
+
+ /**
+ * App: Firefox for Android (Fenix)
+ * Version: master branch (GeckoView Nightly), 2021-01-08
+ */
+ @Test
+ fun Fenix_Master_2021_01_8() {
+ runBlocking(Dispatchers.Main) {
+ val json = """
+ {"version":2,"selectedTabId":"7f4fd2c9-2bb1-4c23-a06c-7fbd2b78922f","sessionStateTuples":[{"session":{"url":"https://airhorner.com/","uuid":"0e556bb8-9120-48af-bc93-2bba0d4ec346","parentUuid":"","title":"The Air Horner","contextId":null,"readerMode":false,"lastAccess":1609784156118, "createdAt":1609784156118},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1823,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"persist\":true,\"cacheKey\":0,\"ID\":0,\"url\":\"https:\\/\\/airhorner.com\\/\",\"title\":\"The Air Horner\",\"loadReplace\":true,\"docIdentifier\":2147483649,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7YjRjZTU0NjItMGQ0Yy00MDc3LTgwMzctYjk2YTI5MGEyMDRifSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7NmE1YzZhODItODdlNy00ODQ0LTljMWItYjY0ZWRiZTA1MDY1fSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7YjRjZTU0NjItMGQ0Yy00MDc3LTgwMzctYjk2YTI5MGEyMDRifSJ9fQ==\",\"resultPrincipalURI\":\"https:\\/\\/airhorner.com\\/\",\"hasUserInteraction\":false,\"originalURI\":\"http:\\/\\/airhorner.com\\/\",\"docshellUUID\":\"{16bc3b14-e47f-4839-b809-095f0b0f79b4}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":1,\"userContextId\":0}}"}},{"session":{"url":"https://www.wikipedia.org/","uuid":"7f4fd2c9-2bb1-4c23-a06c-7fbd2b78922f","parentUuid":"","title":"","contextId":null,"readerMode":false,"lastAccess":0},"engineSession":{}}]}
+ """.trimIndent()
+
+ val engine = GeckoEngine(context)
+
+ assertTrue(
+ getFileForEngine(context, engine).writeString { json },
+ )
+
+ val storage = SessionStorage(context, engine)
+ val state = storage.restore()
+
+ assertNotNull(state)
+
+ assertEquals(2, state!!.tabs.size)
+ assertEquals("7f4fd2c9-2bb1-4c23-a06c-7fbd2b78922f", state.selectedTabId)
+
+ state.tabs[0].state.apply {
+ assertEquals("0e556bb8-9120-48af-bc93-2bba0d4ec346", id)
+ assertEquals("The Air Horner", title)
+ assertEquals("https://airhorner.com/", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertFalse(readerState.active)
+ assertNull(readerState.activeUrl)
+ assertEquals(1609784156118, lastAccess)
+ assertEquals(1609784156118, createdAt)
+ assertNotNull(state)
+ }
+
+ state.tabs[1].state.apply {
+ assertEquals("7f4fd2c9-2bb1-4c23-a06c-7fbd2b78922f", id)
+ assertEquals("", title)
+ assertEquals("https://www.wikipedia.org/", url)
+ assertNull(contextId)
+ assertNull(parentId)
+ assertFalse(readerState.active)
+ assertNull(readerState.activeUrl)
+ assertEquals(0, lastAccess)
+ assertEquals(0, createdAt)
+ assertNotNull(state)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/session-storage/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/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/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt
new file mode 100644
index 0000000000..51b1944fe0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt
@@ -0,0 +1,247 @@
+/* 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.browser.session.storage
+
+import android.os.SystemClock
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.selector.normalTabs
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.ext.flow
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.NamedThreadFactory
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+class AutoSave(
+ private val store: BrowserStore,
+ private val sessionStorage: Storage,
+ private val minimumIntervalMs: Long,
+) {
+ interface Storage {
+ /**
+ * Saves the provided [BrowserState].
+ *
+ * @param state the state to save.
+ * @return true if save was successful, otherwise false.
+ */
+ fun save(state: BrowserState): Boolean
+ }
+
+ internal val logger = Logger("SessionStorage/AutoSave")
+ internal var saveJob: Job? = null
+ private var lastSaveTimestamp: Long = now()
+
+ /**
+ * Saves the state periodically when the app is in the foreground.
+ *
+ * @param interval The interval in which the state should be saved to disk.
+ * @param unit The time unit of the [interval] parameter.
+ */
+ fun periodicallyInForeground(
+ interval: Long = 300,
+ unit: TimeUnit = TimeUnit.SECONDS,
+ scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(
+ NamedThreadFactory("AutoSave"),
+ ),
+ lifecycle: Lifecycle = ProcessLifecycleOwner.get().lifecycle,
+ ): AutoSave {
+ lifecycle.addObserver(
+ AutoSavePeriodically(
+ this,
+ scheduler,
+ interval,
+ unit,
+ ),
+ )
+ return this
+ }
+
+ /**
+ * Saves the state automatically when the app goes to the background.
+ */
+ fun whenGoingToBackground(
+ lifecycle: Lifecycle = ProcessLifecycleOwner.get().lifecycle,
+ ): AutoSave {
+ lifecycle.addObserver(AutoSaveBackground(this))
+ return this
+ }
+
+ /**
+ * Saves the state automatically when the sessions change, e.g. sessions get added and removed.
+ */
+ fun whenSessionsChange(
+ scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+ ): AutoSave {
+ scope.launch {
+ val monitoring = StateMonitoring(this@AutoSave)
+ monitoring.monitor(store.flow())
+ }
+ return this
+ }
+
+ /**
+ * Triggers saving the current state to disk.
+ *
+ * This method will not schedule a new save job if a job is already in flight. Additionally it will obey the
+ * interval passed to [SessionStorage.autoSave]; job may get delayed.
+ *
+ * @param delaySave Whether to delay the save job to obey the interval passed to [SessionStorage.autoSave].
+ */
+ @Synchronized
+ internal fun triggerSave(delaySave: Boolean = true): Job {
+ val currentJob = saveJob
+
+ if (currentJob != null && currentJob.isActive) {
+ logger.debug("Skipping save, other job already in flight")
+ return currentJob
+ }
+
+ val now = now()
+ val delayMs = lastSaveTimestamp + minimumIntervalMs - now
+ lastSaveTimestamp = now
+
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(Dispatchers.IO) {
+ if (delaySave && delayMs > 0) {
+ logger.debug("Delaying save (${delayMs}ms)")
+ delay(delayMs)
+ }
+
+ val start = now()
+
+ try {
+ val state = store.state
+ sessionStorage.save(state)
+ } finally {
+ val took = now() - start
+ logger.debug("Saved state to disk [${took}ms]")
+ }
+ }.also {
+ saveJob = it
+ return it
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun now() = SystemClock.elapsedRealtime()
+
+ companion object {
+ // Minimum interval between saving states.
+ const val DEFAULT_INTERVAL_MILLISECONDS = 2000L
+ }
+}
+
+/**
+ * [LifecycleObserver] to start/stop a task that saves the state at a periodic interval.
+ */
+private class AutoSavePeriodically(
+ private val autoSave: AutoSave,
+ private val scheduler: ScheduledExecutorService,
+ private val interval: Long,
+ private val unit: TimeUnit,
+) : DefaultLifecycleObserver {
+ private var scheduledFuture: ScheduledFuture<*>? = null
+
+ override fun onStart(owner: LifecycleOwner) {
+ scheduledFuture = scheduler.scheduleAtFixedRate(
+ {
+ autoSave.logger.info("Save: Periodic")
+ autoSave.triggerSave()
+ },
+ interval,
+ interval,
+ unit,
+ )
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ scheduledFuture?.cancel(false)
+ }
+}
+
+/**
+ * [LifecycleObserver] to save the state when the app goes to the background.
+ */
+private class AutoSaveBackground(
+ private val autoSave: AutoSave,
+) : DefaultLifecycleObserver {
+ override fun onStop(owner: LifecycleOwner) {
+ autoSave.logger.info("Save: Background")
+
+ autoSave.triggerSave(delaySave = false)
+ }
+}
+
+private class StateMonitoring(
+ private val autoSave: AutoSave,
+) {
+ private var lastObservation: Observation? = null
+
+ suspend fun monitor(flow: Flow<BrowserState>) {
+ flow
+ .map { state ->
+ Observation(
+ state.selectedTabId,
+ state.normalTabs.size,
+ state.selectedTab?.content?.loading,
+ )
+ }
+ .distinctUntilChanged()
+ .collect { observation -> onChange(observation) }
+ }
+
+ private fun onChange(observation: Observation) {
+ if (lastObservation == null) {
+ // If this is the first observation then just remember it. We only want to react to
+ // changes and not the initial state.
+ lastObservation = observation
+ return
+ }
+
+ val triggerSave = if (lastObservation!!.selectedTabId != observation.selectedTabId) {
+ autoSave.logger.info("Save: New tab selected")
+ true
+ } else if (lastObservation!!.tabs != observation.tabs) {
+ autoSave.logger.info("Save: Number of tabs changed")
+ true
+ } else if (lastObservation!!.loading != observation.loading && observation.loading == false) {
+ autoSave.logger.info("Save: Load finished")
+ true
+ } else {
+ false
+ }
+
+ lastObservation = observation
+
+ if (triggerSave) {
+ autoSave.triggerSave()
+ }
+ }
+
+ private data class Observation(
+ val selectedTabId: String?,
+ val tabs: Int,
+ val loading: Boolean?,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorage.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorage.kt
new file mode 100644
index 0000000000..6fa74038aa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorage.kt
@@ -0,0 +1,66 @@
+/* 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.browser.session.storage
+
+import android.content.Context
+import android.util.AtomicFile
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.EngineSessionStateStorage
+import mozilla.components.support.ktx.java.io.truncateDirectory
+import mozilla.components.support.ktx.util.readAndDeserialize
+import mozilla.components.support.ktx.util.streamJSON
+import org.json.JSONObject
+import java.io.File
+
+/**
+ * Implementation of [EngineSessionStateStorage] that reads/writes [EngineSessionState] as JSON
+ * files onto disk.
+ *
+ * This is used by components that need to persist [EngineSessionState] instances separately from
+ * [RecoverableBrowserState].
+ *
+ * @param context A [Context] used for accessing file system.
+ * @param engine An [Engine] instance used for rehydrating persisted [EngineSessionState].
+ */
+class FileEngineSessionStateStorage(
+ private val context: Context,
+ private val engine: Engine,
+) : EngineSessionStateStorage {
+ private val filesDir by lazy { context.filesDir }
+
+ override suspend fun write(uuid: String, state: EngineSessionState): Boolean {
+ return getStateFile(uuid).streamJSON {
+ state.writeTo(this)
+ }
+ }
+
+ override suspend fun read(uuid: String): EngineSessionState? {
+ val jsonObject = getStateFile(uuid).readAndDeserialize { json ->
+ JSONObject(json)
+ }
+ return jsonObject?.let { engine.createSessionState(it) }
+ }
+
+ override suspend fun delete(uuid: String) {
+ getStateFile(uuid).delete()
+ }
+
+ override suspend fun deleteAll() {
+ getStateDirectory(filesDir).truncateDirectory()
+ }
+
+ private fun getStateFile(uuid: String): AtomicFile {
+ return AtomicFile(File(getStateDirectory(filesDir), uuid))
+ }
+
+ private fun getStateDirectory(filesDir: File): File {
+ // NB: This code used to live in feature-recentlyclosedtabs, hence the hardcoded name below.
+ // It wasn't renamed during a refactor to avoid having to force consumers to run data migrations.
+ return File(filesDir, "mozac.feature.recentlyclosed").apply {
+ mkdirs()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt
new file mode 100644
index 0000000000..b4ffcf2dad
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt
@@ -0,0 +1,18 @@
+/* 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.browser.session.storage
+
+import mozilla.components.browser.state.state.recover.RecoverableTab
+
+/**
+ * A restored browser state, read from disk.
+ *
+ * @param tabs The list of restored tabs.
+ * @param selectedTabId The ID of the selected tab in [tabs]. Or `null` if no selection was restored.
+ */
+data class RecoverableBrowserState(
+ val tabs: List<RecoverableTab>,
+ val selectedTabId: String?,
+)
diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt
new file mode 100644
index 0000000000..7d77152215
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt
@@ -0,0 +1,128 @@
+/* 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.browser.session.storage
+
+import android.content.Context
+import android.util.AtomicFile
+import androidx.annotation.CheckResult
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import mozilla.components.browser.session.storage.serialize.BrowserStateReader
+import mozilla.components.browser.session.storage.serialize.BrowserStateWriter
+import mozilla.components.browser.state.selector.normalTabs
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.Engine
+import mozilla.components.support.base.log.logger.Logger
+import java.io.File
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+
+private const val STORE_FILE_NAME_FORMAT = "mozilla_components_session_storage_%s.json"
+
+private val sessionFileLock = Any()
+
+/**
+ * Session storage for (partially) persisting the state of [BrowserStore] to disk.
+ */
+class SessionStorage(
+ private val context: Context,
+ private val engine: Engine,
+ private val crashReporting: CrashReporting? = null,
+) : AutoSave.Storage {
+ private val logger = Logger("SessionStorage")
+ private val stateWriter = BrowserStateWriter()
+ private val stateReader = BrowserStateReader()
+
+ /**
+ * Reads the saved state from disk. Returns null if no state was found on disk or if reading the file failed.
+ *
+ * @param predicate an optional predicate applied to each tab to determine if it should be restored.
+ */
+ @WorkerThread
+ fun restore(predicate: (RecoverableTab) -> Boolean = { true }): RecoverableBrowserState? {
+ synchronized(sessionFileLock) {
+ val file = getFileForEngine(context, engine)
+ return stateReader.read(engine, file, predicate)
+ }
+ }
+
+ /**
+ * Clears the state saved on disk.
+ */
+ @WorkerThread
+ fun clear() {
+ removeSnapshotFromDisk(context, engine)
+ }
+
+ /**
+ * Saves the given state to disk.
+ */
+ @WorkerThread
+ override fun save(state: BrowserState): Boolean {
+ if (state.normalTabs.isEmpty()) {
+ clear()
+ return true
+ }
+
+ // "about:crashparent" is meant for testing purposes only. If saved/restored then it will
+ // continue to crash the app until data is cleared. Therefore, we are filtering it out.
+ val updatedTabList = state.tabs.filterNot { it.content.url == "about:crashparent" }
+ val updatedState = state.copy(tabs = updatedTabList)
+
+ val stateToPersist = if (updatedState.selectedTabId != null && updatedState.selectedTab == null) {
+ // Needs investigation to figure out and prevent cause:
+ // https://github.com/mozilla-mobile/android-components/issues/8417
+ logger.error("Selected tab ID set, but tab with matching ID not found. Clearing selection.")
+ updatedState.copy(selectedTabId = null)
+ } else {
+ updatedState
+ }
+
+ return synchronized(sessionFileLock) {
+ try {
+ val file = getFileForEngine(context, engine)
+ stateWriter.write(stateToPersist, file)
+ } catch (e: OutOfMemoryError) {
+ crashReporting?.submitCaughtException(e)
+ logger.error("Failed to save state to disk due to OutOfMemoryError", e)
+ false
+ }
+ }
+ }
+
+ /**
+ * Starts configuring automatic saving of the state.
+ */
+ @CheckResult
+ fun autoSave(
+ store: BrowserStore,
+ interval: Long = AutoSave.DEFAULT_INTERVAL_MILLISECONDS,
+ unit: TimeUnit = TimeUnit.MILLISECONDS,
+ ): AutoSave {
+ return AutoSave(store, this, unit.toMillis(interval))
+ }
+}
+
+private fun removeSnapshotFromDisk(context: Context, engine: Engine) {
+ synchronized(sessionFileLock) {
+ getFileForEngine(context, engine)
+ .delete()
+ }
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun getFileForEngine(context: Context, engine: Engine): AtomicFile {
+ return AtomicFile(
+ File(
+ context.filesDir,
+ String.format(STORE_FILE_NAME_FORMAT, engine.name())
+ .lowercase(Locale.ROOT),
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt
new file mode 100644
index 0000000000..7b26a59e44
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt
@@ -0,0 +1,258 @@
+/* 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.browser.session.storage.serialize
+
+import android.util.AtomicFile
+import android.util.JsonReader
+import android.util.JsonToken
+import mozilla.components.browser.session.storage.RecoverableBrowserState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.LastMediaAccessState
+import mozilla.components.browser.state.state.ReaderState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.support.ktx.android.util.nextBooleanOrNull
+import mozilla.components.support.ktx.android.util.nextIntOrNull
+import mozilla.components.support.ktx.android.util.nextStringOrNull
+import mozilla.components.support.ktx.util.readJSON
+import java.util.UUID
+
+/**
+ * Reads a [RecoverableBrowserState] (partial, serialized [BrowserState]) or a single [RecoverableTab]
+ * (partial, serialized [TabSessionState]) from disk.
+ */
+class BrowserStateReader {
+ /**
+ * Reads a serialized [RecoverableBrowserState] from the given [AtomicFile].
+ *
+ * @param engine The [Engine] implementation for restoring the engine state.
+ * @param file The [AtomicFile] to read the the recoverable state from.
+ * @param predicate an optional predicate applied to each tab to determine if it should be restored.
+ */
+ fun read(
+ engine: Engine,
+ file: AtomicFile,
+ predicate: (RecoverableTab) -> Boolean = { true },
+ ): RecoverableBrowserState? {
+ return file.readJSON {
+ browsingSession(
+ engine,
+ restoreSessionId = true,
+ restoreParentId = true,
+ predicate = predicate,
+ )
+ }
+ }
+
+ /**
+ * Reads a single [RecoverableTab] from the given [file].
+ *
+ * @param engine The [Engine] implementation for restoring the engine state.
+ * @param restoreSessionId Whether the original tab ID should be restored or whether a new ID
+ * should be generated for the tab.
+ * @param restoreParentId Whether the original parent tab ID should be restored or whether it
+ * should be set to `null`.
+ */
+ fun readTab(
+ engine: Engine,
+ file: AtomicFile,
+ restoreSessionId: Boolean = true,
+ restoreParentId: Boolean = true,
+ ): RecoverableTab? {
+ return file.readJSON { tab(engine, restoreSessionId, restoreParentId) }
+ }
+}
+
+@Suppress("ComplexMethod")
+private fun JsonReader.browsingSession(
+ engine: Engine,
+ restoreSessionId: Boolean = true,
+ restoreParentId: Boolean = true,
+ predicate: (RecoverableTab) -> Boolean = { true },
+): RecoverableBrowserState? {
+ beginObject()
+
+ var version = 1 // Initially we didn't save a version. If there's none then we assume it is version 1.
+ var tabs: List<RecoverableTab>? = null
+ var selectedIndex: Int? = null
+ var selectedTabId: String? = null
+
+ while (hasNext()) {
+ when (nextName()) {
+ Keys.VERSION_KEY -> version = nextInt()
+ Keys.SELECTED_SESSION_INDEX_KEY -> selectedIndex = nextInt()
+ Keys.SELECTED_TAB_ID_KEY -> selectedTabId = nextStringOrNull()
+ Keys.SESSION_STATE_TUPLES_KEY -> tabs = tabs(engine, restoreSessionId, restoreParentId, predicate)
+ }
+ }
+
+ endObject()
+
+ if (selectedTabId == null && version == 1 && selectedIndex != null) {
+ // In the first version we only saved the selected index in the list of all tabs instead
+ // of the ID of the selected tab. If we come across such an older version then we try
+ // to find the tab and determine the selected ID ourselves.
+ selectedTabId = tabs?.getOrNull(selectedIndex)?.state?.id
+ }
+
+ return if (tabs != null && tabs.isNotEmpty()) {
+ // Check if selected tab still exists after restoring/filtering and
+ // use most recently accessed tab otherwise.
+ if (tabs.find { it.state.id == selectedTabId } == null) {
+ selectedTabId = tabs.sortedByDescending { it.state.lastAccess }.first().state.id
+ }
+
+ RecoverableBrowserState(tabs, selectedTabId)
+ } else {
+ null
+ }
+}
+
+private fun JsonReader.tabs(
+ engine: Engine,
+ restoreSessionId: Boolean = true,
+ restoreParentId: Boolean = true,
+ predicate: (RecoverableTab) -> Boolean = { true },
+): List<RecoverableTab> {
+ beginArray()
+
+ val tabs = mutableListOf<RecoverableTab>()
+ while (peek() != JsonToken.END_ARRAY) {
+ val tab = tab(engine, restoreSessionId, restoreParentId)
+ if (tab != null && predicate(tab)) {
+ tabs.add(tab)
+ }
+ }
+
+ endArray()
+
+ return tabs
+}
+
+private fun JsonReader.tab(
+ engine: Engine,
+ restoreSessionId: Boolean = true,
+ restoreParentId: Boolean = true,
+): RecoverableTab? {
+ beginObject()
+
+ var engineSessionState: EngineSessionState? = null
+ var tab: RecoverableTab? = null
+
+ while (hasNext()) {
+ when (nextName()) {
+ Keys.SESSION_KEY -> tab = tabSession()
+ Keys.ENGINE_SESSION_KEY -> engineSessionState = engine.createSessionStateFrom(this)
+ }
+ }
+
+ endObject()
+
+ return tab?.copy(
+ engineSessionState = engineSessionState,
+ state = tab.state.copy(
+ id = if (restoreSessionId) tab.state.id else UUID.randomUUID().toString(),
+ parentId = if (restoreParentId) tab.state.parentId else null,
+ ),
+ )
+}
+
+@Suppress("ComplexMethod", "LongMethod")
+private fun JsonReader.tabSession(): RecoverableTab {
+ var id: String? = null
+ var parentId: String? = null
+ var url: String? = null
+ var title: String? = null
+ var searchTerm: String = ""
+ var contextId: String? = null
+ var lastAccess: Long? = null
+ var createdAt: Long? = null
+ var lastMediaUrl: String? = null
+ var lastMediaAccess: Long? = null
+ var mediaSessionActive: Boolean? = null
+
+ var readerStateActive: Boolean? = null
+ var readerActiveUrl: String? = null
+ var readerScrollY: Int? = null
+
+ var historyMetadataUrl: String? = null
+ var historyMetadataSearchTerm: String? = null
+ var historyMetadataReferrerUrl: String? = null
+
+ var sourceId: Int? = null
+ var externalSourcePackageId: String? = null
+ var externalSourceCategory: Int? = null
+
+ beginObject()
+
+ while (hasNext()) {
+ when (val name = nextName()) {
+ Keys.SESSION_URL_KEY -> url = nextString()
+ Keys.SESSION_UUID_KEY -> id = nextString()
+ Keys.SESSION_CONTEXT_ID_KEY -> contextId = nextStringOrNull()
+ Keys.SESSION_PARENT_UUID_KEY -> parentId = nextStringOrNull()?.takeIf { it.isNotEmpty() }
+ Keys.SESSION_TITLE -> title = nextStringOrNull() ?: ""
+ Keys.SESSION_SEARCH_TERM -> searchTerm = nextStringOrNull() ?: ""
+ Keys.SESSION_READER_MODE_KEY -> readerStateActive = nextBooleanOrNull()
+ Keys.SESSION_READER_MODE_ACTIVE_URL_KEY -> readerActiveUrl = nextStringOrNull()
+ Keys.SESSION_READER_MODE_SCROLLY_KEY -> readerScrollY = nextIntOrNull()
+ Keys.SESSION_HISTORY_METADATA_URL -> historyMetadataUrl = nextStringOrNull()
+ Keys.SESSION_HISTORY_METADATA_SEARCH_TERM -> historyMetadataSearchTerm = nextStringOrNull()
+ Keys.SESSION_HISTORY_METADATA_REFERRER_URL -> historyMetadataReferrerUrl = nextStringOrNull()
+ Keys.SESSION_LAST_ACCESS -> lastAccess = nextLong()
+ Keys.SESSION_CREATED_AT -> createdAt = nextLong()
+ Keys.SESSION_LAST_MEDIA_URL -> lastMediaUrl = nextString()
+ Keys.SESSION_LAST_MEDIA_SESSION_ACTIVE -> mediaSessionActive = nextBoolean()
+ Keys.SESSION_LAST_MEDIA_ACCESS -> lastMediaAccess = nextLong()
+ Keys.SESSION_SOURCE_ID -> sourceId = nextInt()
+ Keys.SESSION_EXTERNAL_SOURCE_PACKAGE_ID -> externalSourcePackageId = nextStringOrNull()
+ Keys.SESSION_EXTERNAL_SOURCE_PACKAGE_CATEGORY -> externalSourceCategory = nextIntOrNull()
+ Keys.SESSION_DEPRECATED_SOURCE_KEY -> nextString()
+ else -> throw IllegalArgumentException("Unknown session key: $name")
+ }
+ }
+
+ endObject()
+
+ return RecoverableTab(
+ engineSessionState = null, // This will be deserialized and added separately
+ state = TabState(
+ id = requireNotNull(id),
+ parentId = parentId,
+ url = requireNotNull(url),
+ title = requireNotNull(title),
+ searchTerm = requireNotNull(searchTerm),
+ contextId = contextId,
+ readerState = ReaderState(
+ active = readerStateActive ?: false,
+ activeUrl = readerActiveUrl,
+ scrollY = readerScrollY,
+ ),
+ historyMetadata = if (historyMetadataUrl != null) {
+ HistoryMetadataKey(
+ historyMetadataUrl,
+ historyMetadataSearchTerm,
+ historyMetadataReferrerUrl,
+ )
+ } else {
+ null
+ },
+ private = false, // We never serialize private sessions
+ lastAccess = lastAccess ?: 0,
+ createdAt = createdAt ?: 0,
+ lastMediaAccessState = LastMediaAccessState(
+ lastMediaUrl ?: "",
+ lastMediaAccess = lastMediaAccess ?: 0,
+ mediaSessionActive = mediaSessionActive ?: false,
+ ),
+ source = SessionState.Source.restore(sourceId, externalSourcePackageId, externalSourceCategory),
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt
new file mode 100644
index 0000000000..ffb6cf34aa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt
@@ -0,0 +1,164 @@
+/* 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.browser.session.storage.serialize
+
+import android.util.AtomicFile
+import android.util.JsonWriter
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.support.ktx.util.streamJSON
+
+/**
+ * Writes a [BrowserState] or a single [TabSessionState] to disk to be restored later using
+ * [BrowserStateReader].
+ */
+class BrowserStateWriter {
+ /**
+ * Writes the [BrowserState] to [file] as JSON.
+ */
+ fun write(
+ state: BrowserState,
+ file: AtomicFile,
+ ): Boolean = file.streamJSON { state(state) }
+
+ /**
+ * Writes a single [TabSessionState] to [file] in JSON format.
+ */
+ fun writeTab(
+ tab: TabSessionState,
+ file: AtomicFile,
+ ): Boolean = file.streamJSON { tab(tab) }
+}
+
+/**
+ * Writes [BrowserState] to [JsonWriter].
+ */
+private fun JsonWriter.state(
+ state: BrowserState,
+) {
+ beginObject()
+
+ name(Keys.VERSION_KEY)
+ value(VERSION)
+
+ name(Keys.SELECTED_TAB_ID_KEY)
+ value(state.selectedTabId)
+
+ name(Keys.SESSION_STATE_TUPLES_KEY)
+
+ beginArray()
+
+ state.tabs.filter { !it.content.private }.forEachIndexed { _, tab ->
+ tab(tab)
+ }
+
+ endArray()
+
+ endObject()
+}
+
+/**
+ * Writes a [TabSessionState] to [JsonWriter].
+ */
+private fun JsonWriter.tab(
+ tab: TabSessionState,
+) {
+ beginObject()
+
+ name(Keys.SESSION_KEY)
+ beginObject().apply {
+ name(Keys.SESSION_URL_KEY)
+ value(tab.content.url)
+
+ name(Keys.SESSION_UUID_KEY)
+ value(tab.id)
+
+ name(Keys.SESSION_PARENT_UUID_KEY)
+ value(tab.parentId ?: "")
+
+ name(Keys.SESSION_TITLE)
+ value(tab.content.title)
+
+ name(Keys.SESSION_SEARCH_TERM)
+ value(tab.content.searchTerms)
+
+ name(Keys.SESSION_CONTEXT_ID_KEY)
+ value(tab.contextId)
+
+ name(Keys.SESSION_READER_MODE_KEY)
+ value(tab.readerState.active)
+
+ name(Keys.SESSION_LAST_ACCESS)
+ value(tab.lastAccess)
+
+ name(Keys.SESSION_CREATED_AT)
+ value(tab.createdAt)
+
+ name(Keys.SESSION_LAST_MEDIA_URL)
+ value(tab.lastMediaAccessState.lastMediaUrl)
+
+ name(Keys.SESSION_LAST_MEDIA_SESSION_ACTIVE)
+ value(tab.lastMediaAccessState.mediaSessionActive)
+
+ name(Keys.SESSION_LAST_MEDIA_ACCESS)
+ value(tab.lastMediaAccessState.lastMediaAccess)
+
+ if (tab.readerState.active && tab.readerState.activeUrl != null) {
+ name(Keys.SESSION_READER_MODE_ACTIVE_URL_KEY)
+ value(tab.readerState.activeUrl)
+ }
+
+ if (tab.readerState.active && tab.readerState.scrollY != null) {
+ name(Keys.SESSION_READER_MODE_SCROLLY_KEY)
+ value(tab.readerState.scrollY)
+ }
+
+ val metadata = tab.historyMetadata
+ if (metadata != null) {
+ name(Keys.SESSION_HISTORY_METADATA_URL)
+ value(metadata.url)
+
+ name(Keys.SESSION_HISTORY_METADATA_SEARCH_TERM)
+ value(metadata.searchTerm)
+
+ name(Keys.SESSION_HISTORY_METADATA_REFERRER_URL)
+ value(metadata.referrerUrl)
+ }
+
+ name(Keys.SESSION_SOURCE_ID)
+ value(tab.source.id)
+
+ (tab.source as? SessionState.Source.External)?.let { externalSource ->
+ externalSource.caller?.let { caller ->
+ name(Keys.SESSION_EXTERNAL_SOURCE_PACKAGE_ID)
+ value(caller.packageId)
+
+ name(Keys.SESSION_EXTERNAL_SOURCE_PACKAGE_CATEGORY)
+ value(caller.category.id)
+ }
+ }
+
+ endObject()
+ }
+
+ name(Keys.ENGINE_SESSION_KEY)
+ engineSession(tab.engineState.engineSessionState)
+
+ endObject()
+}
+
+/**
+ * Writes a (nullable) [EngineSessionState] to [JsonWriter].
+ */
+private fun JsonWriter.engineSession(engineSessionState: EngineSessionState?) {
+ if (engineSessionState == null) {
+ beginObject()
+ endObject()
+ } else {
+ engineSessionState.writeTo(this)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt
new file mode 100644
index 0000000000..afde044728
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt
@@ -0,0 +1,47 @@
+/* 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.browser.session.storage.serialize
+
+internal const val VERSION = 2
+
+/**
+ * JSON keys used in state getting read/written by [BrowserStateReader] and [BrowserStateWriter].
+ */
+internal object Keys {
+ const val SELECTED_SESSION_INDEX_KEY = "selectedSessionIndex"
+ const val SELECTED_TAB_ID_KEY = "selectedTabId"
+ const val SESSION_STATE_TUPLES_KEY = "sessionStateTuples"
+
+ const val SESSION_URL_KEY = "url"
+ const val SESSION_UUID_KEY = "uuid"
+ const val SESSION_CONTEXT_ID_KEY = "contextId"
+ const val SESSION_PARENT_UUID_KEY = "parentUuid"
+ const val SESSION_READER_MODE_KEY = "readerMode"
+ const val SESSION_READER_MODE_ACTIVE_URL_KEY = "readerModeArticleUrl"
+ const val SESSION_READER_MODE_SCROLLY_KEY = "readerModeScrollY"
+ const val SESSION_TITLE = "title"
+ const val SESSION_SEARCH_TERM = "searchTerm"
+ const val SESSION_LAST_ACCESS = "lastAccess"
+ const val SESSION_CREATED_AT = "createdAt"
+ const val SESSION_LAST_MEDIA_URL = "lastMediaUrl"
+ const val SESSION_LAST_MEDIA_ACCESS = "lastMediaAccess"
+ const val SESSION_LAST_MEDIA_SESSION_ACTIVE = "mediaSessionActive"
+
+ // Deprecated for SESSION_SOURCE_ID, kept around for backwards compatibility.
+ const val SESSION_DEPRECATED_SOURCE_KEY = "source"
+
+ const val SESSION_HISTORY_METADATA_URL = "historyMetadataUrl"
+ const val SESSION_HISTORY_METADATA_SEARCH_TERM = "historyMetadataSearchTerm"
+ const val SESSION_HISTORY_METADATA_REFERRER_URL = "historyMetadataReferrerUrl"
+
+ const val SESSION_SOURCE_ID = "sourceId"
+ const val SESSION_EXTERNAL_SOURCE_PACKAGE_ID = "externalPackageId"
+ const val SESSION_EXTERNAL_SOURCE_PACKAGE_CATEGORY = "externalPackageCategory"
+
+ const val SESSION_KEY = "session"
+ const val ENGINE_SESSION_KEY = "engineSession"
+
+ const val VERSION_KEY = "version"
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt
new file mode 100644
index 0000000000..85e90e9d81
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt
@@ -0,0 +1,384 @@
+/* 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.browser.session.storage
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.Job
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+class AutoSaveTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `AutoSave - when going to background`() {
+ runTestOnMain {
+ // Keep the "owner" in scope to avoid it getting garbage collected and therefore lifecycle events
+ // not getting propagated (See #1428).
+ val owner = mock(LifecycleOwner::class.java)
+ val lifecycle = LifecycleRegistry(owner)
+
+ val sessionStorage: SessionStorage = mock()
+
+ val state = BrowserState()
+ val store = BrowserStore(state)
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ ).whenGoingToBackground(lifecycle)
+
+ verifyNoMoreInteractions(sessionStorage)
+
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+
+ verifyNoMoreInteractions(sessionStorage)
+
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+
+ autoSave.saveJob!!.join()
+
+ verify(sessionStorage).save(state)
+ }
+ }
+
+ @Test
+ fun `AutoSave - when tab gets added`() {
+ runTestOnMain {
+ val state = BrowserState()
+ val store = BrowserStore(state)
+
+ val sessionStorage: SessionStorage = mock()
+
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ ).whenSessionsChange(scope)
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(autoSave.saveJob)
+ verify(sessionStorage, never()).save(any())
+
+ store.dispatch(
+ TabListAction.AddTabAction(
+ createTab("https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ autoSave.saveJob?.join()
+
+ verify(sessionStorage).save(any())
+ }
+ }
+
+ @Test
+ fun `AutoSave - when tab gets removed`() {
+ runTestOnMain {
+ val sessionStorage: SessionStorage = mock()
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://www.firefox.com", id = "firefox"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ ).whenSessionsChange(scope)
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(autoSave.saveJob)
+ verify(sessionStorage, never()).save(any())
+
+ store.dispatch(TabListAction.RemoveTabAction("mozilla")).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ autoSave.saveJob?.join()
+
+ verify(sessionStorage).save(any())
+ }
+ }
+
+ @Test
+ fun `AutoSave - when all tabs get removed`() {
+ runTestOnMain {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.firefox.com", id = "firefox"),
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val sessionStorage: SessionStorage = mock()
+
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ ).whenSessionsChange(scope)
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(autoSave.saveJob)
+ verify(sessionStorage, never()).save(any())
+
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ autoSave.saveJob?.join()
+
+ verify(sessionStorage).save(any())
+ }
+ }
+
+ @Test
+ fun `AutoSave - when no tabs are left`() {
+ runTestOnMain {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.firefox.com", id = "firefox")),
+ selectedTabId = "firefox",
+ ),
+ )
+
+ val sessionStorage: SessionStorage = mock()
+
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ ).whenSessionsChange(scope)
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(autoSave.saveJob)
+ verify(sessionStorage, never()).save(any())
+
+ store.dispatch(TabListAction.RemoveTabAction("firefox")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ autoSave.saveJob?.join()
+
+ verify(sessionStorage).save(any())
+ }
+ }
+
+ @Test
+ fun `AutoSave - when tab gets selected`() {
+ runTestOnMain {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.firefox.com", id = "firefox"),
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "firefox",
+ ),
+ )
+
+ val sessionStorage: SessionStorage = mock()
+
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ ).whenSessionsChange(scope)
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(autoSave.saveJob)
+ verify(sessionStorage, never()).save(any())
+
+ store.dispatch(TabListAction.SelectTabAction("mozilla")).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ autoSave.saveJob?.join()
+
+ verify(sessionStorage).save(any())
+ }
+ }
+
+ @Test
+ fun `AutoSave - when tab loading state changes`() {
+ runTestOnMain {
+ val sessionStorage: SessionStorage = mock()
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ ).whenSessionsChange(scope)
+
+ store.dispatch(
+ ContentAction.UpdateLoadingStateAction(
+ sessionId = "mozilla",
+ loading = true,
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(autoSave.saveJob)
+ verify(sessionStorage, never()).save(any())
+
+ store.dispatch(
+ ContentAction.UpdateLoadingStateAction(
+ sessionId = "mozilla",
+ loading = false,
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ autoSave.saveJob?.join()
+
+ verify(sessionStorage).save(any())
+ }
+ }
+
+ @Test
+ fun `AutoSave - periodically in foreground`() {
+ val engine: Engine = mock()
+ val scheduler: ScheduledExecutorService = mock()
+ val scheduledFuture = mock(ScheduledFuture::class.java)
+ `when`(
+ scheduler.scheduleAtFixedRate(
+ any(),
+ eq(300L),
+ eq(300L),
+ eq(TimeUnit.SECONDS),
+ ),
+ ).thenReturn(scheduledFuture)
+
+ // LifecycleRegistry only keeps a weak reference to the owner, so it is important to keep
+ // a reference here too during the test run.
+ // See https://github.com/mozilla-mobile/android-components/issues/5166
+ val owner = mock(LifecycleOwner::class.java)
+ val lifecycle = LifecycleRegistry(owner)
+
+ val state = BrowserState()
+ val store = BrowserStore(state)
+ val storage = SessionStorage(testContext, engine)
+ storage.autoSave(store)
+ .periodicallyInForeground(300, TimeUnit.SECONDS, scheduler, lifecycle)
+
+ verifyNoMoreInteractions(scheduler)
+
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
+
+ verify(scheduler).scheduleAtFixedRate(
+ any(),
+ eq(300L),
+ eq(300L),
+ eq(TimeUnit.SECONDS),
+ )
+
+ verifyNoMoreInteractions(scheduler)
+
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+
+ verify(scheduledFuture).cancel(false)
+ }
+
+ @Test
+ fun `AutoSave - No new job triggered while save in flight`() {
+ val sessionStorage: SessionStorage = mock()
+
+ val state = BrowserState()
+ val store = BrowserStore(state)
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ )
+
+ val runningJob: Job = mock()
+ doReturn(true).`when`(runningJob).isActive
+
+ val saveJob = autoSave.triggerSave()
+ assertSame(saveJob, saveJob)
+ }
+
+ @Test
+ fun `AutoSave - New job triggered if current job is done`() {
+ val sessionStorage: SessionStorage = mock()
+
+ val state = BrowserState()
+ val store = BrowserStore(state)
+ val autoSave = AutoSave(
+ store = store,
+ sessionStorage = sessionStorage,
+ minimumIntervalMs = 0,
+ )
+
+ val completed: Job = mock()
+ doReturn(false).`when`(completed).isActive
+
+ val saveJob = autoSave.triggerSave()
+ assertNotSame(completed, saveJob)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt
new file mode 100644
index 0000000000..317124d226
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt
@@ -0,0 +1,84 @@
+/* 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.browser.session.storage
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.fakes.engine.FakeEngine
+import mozilla.components.support.test.fakes.engine.FakeEngineSessionState
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FileEngineSessionStateStorageTest {
+ @Test
+ fun `able to read and write engine session states`() = runTest {
+ val storage = FileEngineSessionStateStorage(testContext, FakeEngine())
+
+ // reading non-existing tab
+ assertNull(storage.read("test-tab"))
+
+ storage.write("test-tab", FakeEngineSessionState("some-engine-state"))
+ val state = storage.read("test-tab")
+
+ assertEquals("some-engine-state", (state as FakeEngineSessionState).value)
+
+ assertNull(storage.read("another-tab"))
+ }
+
+ @Test
+ fun `able to delete specific engine session states`() = runTest {
+ val storage = FileEngineSessionStateStorage(testContext, FakeEngine())
+ storage.write("test-tab-1", FakeEngineSessionState("some-engine-state-1"))
+ storage.write("test-tab-2", FakeEngineSessionState("some-engine-state-2"))
+ storage.write("test-tab-3", FakeEngineSessionState("some-engine-state-3"))
+ storage.write("test-tab-4", FakeEngineSessionState("some-engine-state-4"))
+
+ // deleting non-existing tab
+ assertNull(storage.read("does-not-exist"))
+ storage.delete("does-not-exist")
+ assertNull(storage.read("does-not-exist"))
+
+ assertNotNull(storage.read("test-tab-3"))
+ storage.delete("test-tab-3")
+ assertNull(storage.read("test-tab-3"))
+
+ assertNotNull(storage.read("test-tab-1"))
+ storage.delete("test-tab-1")
+ assertNull(storage.read("test-tab-1"))
+
+ assertNotNull(storage.read("test-tab-2"))
+ storage.delete("test-tab-2")
+ assertNull(storage.read("test-tab-2"))
+
+ assertNotNull(storage.read("test-tab-4"))
+ storage.delete("test-tab-4")
+ assertNull(storage.read("test-tab-4"))
+ }
+
+ @Test
+ fun `able to delete all engine states`() = runTest {
+ val storage = FileEngineSessionStateStorage(testContext, FakeEngine())
+
+ // already empty storage
+ storage.deleteAll()
+
+ storage.write("test-tab-1", FakeEngineSessionState("some-engine-state-1"))
+ storage.write("test-tab-2", FakeEngineSessionState("some-engine-state-2"))
+ storage.write("test-tab-3", FakeEngineSessionState("some-engine-state-3"))
+ storage.write("test-tab-4", FakeEngineSessionState("some-engine-state-4"))
+
+ storage.deleteAll()
+
+ assertNull(storage.read("test-tab-1"))
+ assertNull(storage.read("test-tab-2"))
+ assertNull(storage.read("test-tab-3"))
+ assertNull(storage.read("test-tab-4"))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt
new file mode 100644
index 0000000000..914c0c2299
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt
@@ -0,0 +1,405 @@
+/* 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.browser.session.storage
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.ReaderState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.support.ktx.util.writeString
+import mozilla.components.support.test.fakes.engine.FakeEngine
+import mozilla.components.support.test.fakes.engine.FakeEngineSessionState
+import mozilla.components.support.test.robolectric.testContext
+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.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SessionStorageTest {
+ @Test
+ fun `Restored browser state should contain tabs of saved state`() {
+ // Build the state
+
+ val engineSessionState1 = FakeEngineSessionState("engineState1")
+
+ val tab1 = createTab("https://www.mozilla.org", id = "tab1").copy(
+ engineState = EngineState(engineSessionState = engineSessionState1),
+ )
+ val tab2 = createTab("https://getpocket.com", id = "tab2", contextId = "context2")
+ val tab3 = createTab("https://www.firefox.com", id = "tab3", parent = tab1)
+
+ val state = BrowserState(
+ tabs = listOf(tab1, tab2, tab3),
+ selectedTabId = tab1.id,
+ )
+
+ // Persist the state
+
+ val engine = FakeEngine()
+
+ val storage = SessionStorage(testContext, engine)
+ val persisted = storage.save(state)
+ assertTrue(persisted)
+
+ // Read it back
+
+ val restoredState = storage.restore()
+ assertNotNull(restoredState!!)
+
+ assertEquals(3, restoredState.tabs.size)
+ assertEquals("tab1", restoredState.selectedTabId)
+
+ tab1.assertSameAs(restoredState.tabs[0])
+ tab2.assertSameAs(restoredState.tabs[1])
+ tab3.assertSameAs(restoredState.tabs[2])
+ }
+
+ @Test
+ fun `Predicate is applied when restoring browser state`() {
+ // Build the state
+
+ val engineSessionState1 = FakeEngineSessionState("engineState1")
+
+ val tab1 = createTab("https://www.mozilla.org", id = "tab1").copy(
+ engineState = EngineState(engineSessionState = engineSessionState1),
+ )
+ val tab2 = createTab("https://getpocket.com", id = "tab2", contextId = "context")
+ val tab3 = createTab("https://www.firefox.com", id = "tab3", parent = tab1)
+ val tab4 = createTab(
+ "https://example.com",
+ id = "tab4",
+ contextId = "context",
+ lastAccess = System.currentTimeMillis(),
+ )
+
+ val state = BrowserState(
+ tabs = listOf(tab1, tab2, tab3, tab4),
+ selectedTabId = tab1.id,
+ )
+
+ // Persist the state
+
+ val engine = FakeEngine()
+
+ val storage = SessionStorage(testContext, engine)
+ val persisted = storage.save(state)
+ assertTrue(persisted)
+
+ // Read it back and filter using predicate
+ val restoredState = storage.restore {
+ it.state.contextId == "context"
+ }
+ assertNotNull(restoredState!!)
+
+ // Only the two "context" tabs should be restored
+ assertEquals(2, restoredState.tabs.size)
+ tab2.assertSameAs(restoredState.tabs[0])
+ tab4.assertSameAs(restoredState.tabs[1])
+
+ // The selected tab was not restored so the most recently accessed restored tab
+ // should be selected.
+ assertEquals("tab4", restoredState.selectedTabId)
+ }
+
+ @Test
+ fun `Saving empty state`() {
+ val engine = FakeEngine()
+
+ val storage = spy(SessionStorage(testContext, engine))
+ storage.save(BrowserState())
+
+ verify(storage).clear()
+
+ assertNull(storage.restore())
+ }
+
+ @Test
+ fun `Should return empty browser state after clearing`() {
+ val engine = FakeEngine()
+
+ val tab1 = createTab("https://www.mozilla.org", id = "tab1")
+ val tab2 = createTab("https://getpocket.com", id = "tab2")
+
+ val state = BrowserState(
+ tabs = listOf(tab1, tab2),
+ selectedTabId = tab1.id,
+ )
+
+ // Persist the state
+
+ val storage = SessionStorage(testContext, engine)
+ val persisted = storage.save(state)
+ assertTrue(persisted)
+
+ storage.clear()
+
+ // Read it back. Expect null, indicating empty.
+
+ val restoredState = storage.restore()
+ assertNull(restoredState)
+ }
+
+ /**
+ * This test is deserializing a version 2 snapshot taken from an actual device.
+ *
+ * This snapshot was written with the legacy org.json serializer implementation.
+ *
+ * If this test fails then this is an indication that we may have introduced a regression. We
+ * should NOT change the test input to make the test pass since such an input does exist on
+ * actual devices too.
+ */
+ @Test
+ fun deserializeVersion2BrowsingSessionLegacyOrgJson() {
+ // Do not change this string! (See comment above)
+ val json = """
+ {"version":2,"selectedTabId":"c367f2ec-c456-4184-9e47-6ed102a3c32c","sessionStateTuples":[{"session":{"url":"https:\/\/www.mozilla.org\/en-US\/firefox\/","uuid":"e4bc40f1-6da5-4da2-8e32-352f2acdc2bb","parentUuid":"","title":"Firefox - Protect your life online with privacy-first products — Mozilla","readerMode":false,"lastAccess":0},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1731,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"presState\":[{\"scroll\":\"0,14962\",\"scrollOriginDowngrade\":false,\"stateKey\":\"0>3>fp>1>0>\"}],\"persist\":true,\"cacheKey\":0,\"ID\":1,\"url\":\"https:\\\/\\\/www.mozilla.org\\\/en-US\\\/\",\"title\":\"Internet for people, not profit — Mozilla\",\"loadReplace\":true,\"docIdentifier\":2147483650,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ZjBlNTI0M2EtMjA3MS00NjNjLWExMzAtZmU1MTljODU2YTQxfSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MDBkNTJjYjYtYTEyOC00NjNkLWExNzItMDhjYWVhYzIzZjRkfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ZjBlNTI0M2EtMjA3MS00NjNjLWExMzAtZmU1MTljODU2YTQxfSJ9fQ==\",\"resultPrincipalURI\":\"https:\\\/\\\/www.mozilla.org\\\/en-US\\\/\",\"hasUserInteraction\":true,\"originalURI\":\"http:\\\/\\\/mozilla.org\\\/\",\"docshellUUID\":\"{21708b71-e22f-4996-99ff-71593e48e7c7}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAHmh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLwAAAAEBAQAAAB5odHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8BAA==\",\"persist\":true,\"cacheKey\":0,\"ID\":2,\"csp\":\"CdntGuXUQAS\\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\\/\\\/\\\/\\\/\\\/wAAAbsBAAAAHmh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLwAAAAAAAAAFAAAACAAAAA8AAAAA\\\/\\\/\\\/\\\/\\\/wAAAAD\\\/\\\/\\\/\\\/\\\/AAAACAAAAA8AAAAXAAAABwAAABcAAAAHAAAAFwAAAAcAAAAeAAAAAAAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wAAAAD\\\/\\\/\\\/\\\/\\\/AQAAAAAAAAAAACx7IjEiOnsiMCI6Imh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLyJ9fQAAAAEAAAfsAGMAaABpAGwAZAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC0AbgBvAGMAbwBvAGsAaQBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdAByAGEAYwBrAGUAcgB0AGUAcwB0AC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBzAHUAcgB2AGUAeQBnAGkAegBtAG8ALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtADsAIABzAGMAcgBpAHAAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgACcAdQBuAHMAYQBmAGUALQBlAHYAYQBsACcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdABhAGcAbQBhAG4AYQBnAGUAcgAuAGcAbwBvAGcAbABlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AcwAuAHkAdABpAG0AZwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGMAZABuAC0AMwAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBwAHAALgBjAG8AbgB2AGUAcgB0AC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AZABhAHQAYQAuAHQAcgBhAGMAawAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AMQAwADAAMwAzADUAMAAuAHQAcgBhAGMAawAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AMQAwADAAMwAzADQAMwAuAHQAcgBhAGMAawAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AOwAgAGkAbQBnAC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgAGQAYQB0AGEAOgAgAGgAdAB0AHAAcwA6AC8ALwBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBkAHMAZQByAHYAaQBjAGUALgBnAG8AbwBnAGwAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBkAGUAIABoAHQAdABwAHMAOgAvAC8AYQBkAHMAZQByAHYAaQBjAGUALgBnAG8AbwBnAGwAZQAuAGQAawAgAGgAdAB0AHAAcwA6AC8ALwBjAHIAZQBhAHQAaQB2AGUAYwBvAG0AbQBvAG4AcwAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAGMAZABuAC0AMwAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AbABvAGcAcwAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBkAC4AZABvAHUAYgBsAGUAYwBsAGkAYwBrAC4AbgBlAHQAOwAgAGYAcgBhAG0AZQAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC0AbgBvAGMAbwBvAGsAaQBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdAByAGEAYwBrAGUAcgB0AGUAcwB0AC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBzAHUAcgB2AGUAeQBnAGkAegBtAG8ALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtADsAIABzAHQAeQBsAGUALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtACAAJwB1AG4AcwBhAGYAZQAtAGkAbgBsAGkAbgBlACcAIABoAHQAdABwAHMAOgAvAC8AYQBwAHAALgBjAG8AbgB2AGUAcgB0AC4AYwBvAG0AOwAgAGMAbwBuAG4AZQBjAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGwAbwBnAHMALgBjAG8AbgB2AGUAcgB0AGUAeABwAGUAcgBpAG0AZQBuAHQAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvADEAMAAwADMAMwA1ADAALgBtAGUAdAByAGkAYwBzAC4AYwBvAG4AdgBlAHIAdABlAHgAcABlAHIAaQBtAGUAbgB0AHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwAxADAAMAAzADMANAAzAC4AbQBlAHQAcgBpAGMAcwAuAGMAbwBuAHYAZQByAHQAZQB4AHAAZQByAGkAbQBlAG4AdABzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC8AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuAC8AOwAgAGQAZQBmAGEAdQBsAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtAAA=\",\"url\":\"https:\\\/\\\/www.mozilla.org\\\/en-US\\\/firefox\\\/\",\"title\":\"Firefox - Protect your life online with privacy-first products — Mozilla\",\"loadReplace\":false,\"docIdentifier\":2147483651,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8ifX0=\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8ifX0=\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8ifX0=\",\"resultPrincipalURI\":\"https:\\\/\\\/www.mozilla.org\\\/en-US\\\/firefox\\\/\",\"hasUserInteraction\":false,\"originalURI\":\"https:\\\/\\\/www.mozilla.org\\\/en-US\\\/firefox\\\/\",\"docshellUUID\":\"{21708b71-e22f-4996-99ff-71593e48e7c7}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":2,\"userContextId\":0}}"}},{"session":{"url":"https:\/\/en.m.wikipedia.org\/wiki\/Mona_Lisa","uuid":"c367f2ec-c456-4184-9e47-6ed102a3c32c","parentUuid":"","title":"Mona Lisa - Wikipedia","readerMode":false,"lastAccess":1599582163154},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1731,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"presState\":[{\"scroll\":\"0,14352\",\"scrollOriginDowngrade\":false,\"stateKey\":\"0>3>fp>0>4>\"}],\"persist\":true,\"cacheKey\":0,\"ID\":4,\"url\":\"https:\\\/\\\/www.wikipedia.org\\\/\",\"title\":\"Wikipedia\",\"loadReplace\":true,\"docIdentifier\":2147483653,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MjFhYTQzNjEtZDcyZi00ZDUyLThjYjUtM2ZiMzlkOTg2OWFmfSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7NTgxYWY4MTUtZDg2Yy00MTQ3LTg2ODMtN2U2YWJhMjYzYjlkfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MjFhYTQzNjEtZDcyZi00ZDUyLThjYjUtM2ZiMzlkOTg2OWFmfSJ9fQ==\",\"resultPrincipalURI\":\"https:\\\/\\\/www.wikipedia.org\\\/\",\"hasUserInteraction\":true,\"originalURI\":\"http:\\\/\\\/wikipedia.org\\\/\",\"docshellUUID\":\"{37d2c68a-4432-4246-8427-ad1348ca07e9}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAGmh0dHBzOi8vd3d3Lndpa2lwZWRpYS5vcmcvAAAAAQEBAAAAGmh0dHBzOi8vd3d3Lndpa2lwZWRpYS5vcmcvAQA=\",\"persist\":true,\"cacheKey\":0,\"ID\":5,\"csp\":\"CdntGuXUQAS\\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\\/\\\/\\\/\\\/\\\/wAAAbsBAAAAGmh0dHBzOi8vd3d3Lndpa2lwZWRpYS5vcmcvAAAAAAAAAAUAAAAIAAAAEQAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8AAAAIAAAAEQAAABkAAAABAAAAGQAAAAEAAAAZAAAAAQAAABoAAAAAAAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8BAAAAAAAAAAAAKHsiMSI6eyIwIjoiaHR0cHM6Ly93d3cud2lraXBlZGlhLm9yZy8ifX0AAAAA\",\"url\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Main_Page\",\"title\":\"Wikipedia, the free encyclopedia\",\"loadReplace\":true,\"docIdentifier\":2147483654,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7N2RkZmU5MDQtNjgyMC00MThiLTg0MWEtNTMwYWIzMmUxZDBjfSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy53aWtpcGVkaWEub3JnLyJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7N2RkZmU5MDQtNjgyMC00MThiLTg0MWEtNTMwYWIzMmUxZDBjfSJ9fQ==\",\"resultPrincipalURI\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Main_Page\",\"hasUserInteraction\":true,\"originalURI\":\"https:\\\/\\\/en.wikipedia.org\\\/\",\"docshellUUID\":\"{37d2c68a-4432-4246-8427-ad1348ca07e9}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAKWh0dHBzOi8vZW4ubS53aWtpcGVkaWEub3JnL3dpa2kvTWFpbl9QYWdlAAAABAEBAAAAKWh0dHBzOi8vZW4ubS53aWtpcGVkaWEub3JnL3dpa2kvTWFpbl9QYWdlAQA=\",\"persist\":true,\"cacheKey\":0,\"ID\":6,\"csp\":\"CdntGuXUQAS\\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\\/\\\/\\\/\\\/\\\/wAAAbsBAAAAKWh0dHBzOi8vZW4ubS53aWtpcGVkaWEub3JnL3dpa2kvTWFpbl9QYWdlAAAAAAAAAAUAAAAIAAAAEgAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8AAAAIAAAAEgAAABoAAAAPAAAAGgAAAA8AAAAaAAAABgAAACAAAAAJAAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8BAAAAAAAAAAAAN3siMSI6eyIwIjoiaHR0cHM6Ly9lbi5tLndpa2lwZWRpYS5vcmcvd2lraS9NYWluX1BhZ2UifX0AAAAA\",\"url\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Portal:Arts\",\"title\":\"Portal:Arts - Wikipedia\",\"loadReplace\":false,\"docIdentifier\":2147483655,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2VuLm0ud2lraXBlZGlhLm9yZy93aWtpL01haW5fUGFnZSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2VuLm0ud2lraXBlZGlhLm9yZy93aWtpL01haW5fUGFnZSJ9fQ==\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2VuLm0ud2lraXBlZGlhLm9yZy93aWtpL01haW5fUGFnZSJ9fQ==\",\"resultPrincipalURI\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Portal:Arts\",\"hasUserInteraction\":true,\"originalURI\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Portal:Arts\",\"docshellUUID\":\"{37d2c68a-4432-4246-8427-ad1348ca07e9}\"},{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAK2h0dHBzOi8vZW4ubS53aWtpcGVkaWEub3JnL3dpa2kvUG9ydGFsOkFydHMAAAAEAQEAAAAraHR0cHM6Ly9lbi5tLndpa2lwZWRpYS5vcmcvd2lraS9Qb3J0YWw6QXJ0cwEA\",\"persist\":true,\"cacheKey\":0,\"ID\":7,\"csp\":\"CdntGuXUQAS\\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\\/\\\/\\\/\\\/\\\/wAAAbsBAAAAK2h0dHBzOi8vZW4ubS53aWtpcGVkaWEub3JnL3dpa2kvUG9ydGFsOkFydHMAAAAAAAAABQAAAAgAAAASAAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wAAAAgAAAASAAAAGgAAABEAAAAaAAAAEQAAABoAAAAGAAAAIAAAAAsAAAAA\\\/\\\/\\\/\\\/\\\/wAAAAD\\\/\\\/\\\/\\\/\\\/AAAAAP\\\/\\\/\\\/\\\/8AAAAA\\\/\\\/\\\/\\\/\\\/wEAAAAAAAAAAAA5eyIxIjp7IjAiOiJodHRwczovL2VuLm0ud2lraXBlZGlhLm9yZy93aWtpL1BvcnRhbDpBcnRzIn19AAAAAA==\",\"url\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Mona_Lisa\",\"title\":\"Mona Lisa - Wikipedia\",\"loadReplace\":false,\"docIdentifier\":2147483656,\"loadReplace2\":true,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2VuLm0ud2lraXBlZGlhLm9yZy93aWtpL1BvcnRhbDpBcnRzIn19\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2VuLm0ud2lraXBlZGlhLm9yZy93aWtpL1BvcnRhbDpBcnRzIn19\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJodHRwczovL2VuLm0ud2lraXBlZGlhLm9yZy93aWtpL1BvcnRhbDpBcnRzIn19\",\"resultPrincipalURI\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Mona_Lisa\",\"hasUserInteraction\":false,\"originalURI\":\"https:\\\/\\\/en.m.wikipedia.org\\\/wiki\\\/Mona_Lisa\",\"docshellUUID\":\"{37d2c68a-4432-4246-8427-ad1348ca07e9}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":4,\"userContextId\":0}}"}}]}
+ """.trimIndent()
+
+ val engine = FakeEngine(expectToRestoreRealEngineSessionState = true)
+
+ assertTrue(
+ getFileForEngine(testContext, engine).writeString { json },
+ )
+
+ val storage = SessionStorage(testContext, engine)
+ val browsingSession = storage.restore()
+
+ assertNotNull(browsingSession)
+ assertEquals(2, browsingSession!!.tabs.size)
+
+ browsingSession.tabs[0].state.apply {
+ assertEquals("https://www.mozilla.org/en-US/firefox/", url)
+ assertEquals(
+ "Firefox - Protect your life online with privacy-first products — Mozilla",
+ title,
+ )
+ assertEquals("e4bc40f1-6da5-4da2-8e32-352f2acdc2bb", id)
+ assertNull(parentId)
+ assertEquals(0, lastAccess)
+ assertNotNull(readerState)
+ assertFalse(readerState.active)
+ }
+
+ browsingSession.tabs[1].state.apply {
+ assertEquals("https://en.m.wikipedia.org/wiki/Mona_Lisa", url)
+ assertEquals("Mona Lisa - Wikipedia", title)
+ assertEquals("c367f2ec-c456-4184-9e47-6ed102a3c32c", id)
+ assertNull(parentId)
+ assertEquals(1599582163154, lastAccess)
+ assertNotNull(readerState)
+ assertFalse(readerState.active)
+ }
+ }
+
+ /**
+ * This test is deserializing a version 2 snapshot taken from an actual device.
+ *
+ * This snapshot was written with the JsonWriter serializer implementation.
+ *
+ * If this test fails then this is an indication that we may have introduced a regression. We
+ * should NOT change the test input to make the test pass since such an input does exist on
+ * actual devices too.
+ */
+ @Test
+ fun deserializeVersion2BrowsingSessionJsonWriter() {
+ // Do not change this string! (See comment above)
+ val json = """
+ {"version":2,"selectedTabId":"d6facb8a-0775-45a1-8bc1-2397e2d2bc53","sessionStateTuples":[{"session":{"url":"https://www.theverge.com/","uuid":"28f428ba-2b19-4c24-993b-763fda2be65c","parentUuid":"","title":"The Verge","contextId":null,"readerMode":false,"lastAccess":1599815361779},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"scroll\":\"0,1074\",\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1584,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYAAAAAAAEBAAAAAAEA\",\"persist\":true,\"cacheKey\":0,\"ID\":2086656668,\"url\":\"https:\\/\\/www.theverge.com\\/\",\"title\":\"The Verge\",\"structuredCloneVersion\":8,\"docIdentifier\":2147483649,\"structuredCloneState\":\"AgAAAAAA8f8AAAAACAD\\/\\/wAAAAATAP\\/\\/\",\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MGU4ZmNlMTQtNDE5Yi00MWQ5LTg2YjQtMGVlM2VkYmE5Zjc4fSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7OGM1NTlmYWQtMmRjZC00NjY5LWE5MjEtODViYTFiODBmNWJhfSJ9fQ==\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7MGU4ZmNlMTQtNDE5Yi00MWQ5LTg2YjQtMGVlM2VkYmE5Zjc4fSJ9fQ==\",\"resultPrincipalURI\":null,\"hasUserInteraction\":true,\"originalURI\":\"https:\\/\\/www.theverge.com\\/\",\"docshellUUID\":\"{9464a0d3-6687-4179-a3a4-15817791801d}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":1,\"userContextId\":0}}"}},{"session":{"url":"https://www.theverge.com/2020/9/10/21429838/google-android-11-go-edition-devices-2gb-ram-20-percent","uuid":"d6facb8a-0775-45a1-8bc1-2397e2d2bc53","parentUuid":"28f428ba-2b19-4c24-993b-763fda2be65c","title":"Android 11 Go is available today, and it will launch apps 20 percent faster","contextId":null,"readerMode":true,"lastAccess":1599815364500,"readerModeArticleUrl":"https://www.theverge.com/2020/9/10/21429838/google-android-11-go-edition-devices-2gb-ram-20-percent"},"engineSession":{"GECKO_STATE":"{\"scrolldata\":{\"zoom\":{\"resolution\":1,\"displaySize\":{\"height\":1731,\"width\":1080}}},\"history\":{\"entries\":[{\"referrerInfo\":\"BBoSnxDOS9qmDeAnom1e0AAAAAAAAAAAwAAAAAAAAEYBAAAAGWh0dHBzOi8vd3d3LnRoZXZlcmdlLmNvbS8AAAAIAQEAAAAZaHR0cHM6Ly93d3cudGhldmVyZ2UuY29tLwEA\",\"persist\":true,\"cacheKey\":0,\"ID\":2087434596,\"csp\":\"CdntGuXUQAS\\/4CfOuSPZrAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC\\/\\/\\/\\/\\/wAAAbsBAAAAGWh0dHBzOi8vd3d3LnRoZXZlcmdlLmNvbS8AAAAAAAAABQAAAAgAAAAQAAAACP\\/\\/\\/\\/8AAAAI\\/\\/\\/\\/\\/wAAAAgAAAAQAAAAGAAAAAEAAAAYAAAAAQAAABgAAAABAAAAGQAAAAAAAAAZ\\/\\/\\/\\/\\/wAAAAD\\/\\/\\/\\/\\/AAAAGP\\/\\/\\/\\/8AAAAY\\/\\/\\/\\/\\/wEAAAAAAAAAAAAneyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19AAAAAQAAAWsAZABlAGYAYQB1AGwAdAAtAHMAcgBjACAAaAB0AHQAcABzADoAIABkAGEAdABhADoAIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgACcAdQBuAHMAYQBmAGUALQBlAHYAYQBsACcAOwAgAGMAaABpAGwAZAAtAHMAcgBjACAAaAB0AHQAcABzADoAIABkAGEAdABhADoAIABiAGwAbwBiADoAOwAgAGMAbwBuAG4AZQBjAHQALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABmAG8AbgB0AC0AcwByAGMAIABoAHQAdABwAHMAOgAgAGQAYQB0AGEAOgA7ACAAaQBtAGcALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABtAGUAZABpAGEALQBzAHIAYwAgAGgAdAB0AHAAcwA6ACAAZABhAHQAYQA6ACAAYgBsAG8AYgA6ADsAIABvAGIAagBlAGMAdAAtAHMAcgBjACAAaAB0AHQAcABzADoAOwAgAHMAYwByAGkAcAB0AC0AcwByAGMAIABoAHQAdABwAHMAOgAgAGQAYQB0AGEAOgAgAGIAbABvAGIAOgAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnACAAJwB1AG4AcwBhAGYAZQAtAGUAdgBhAGwAJwA7ACAAcwB0AHkAbABlAC0AcwByAGMAIABoAHQAdABwAHMAOgAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnADsAIABiAGwAbwBjAGsALQBhAGwAbAAtAG0AaQB4AGUAZAAtAGMAbwBuAHQAZQBuAHQAOwAgAHUAcABnAHIAYQBkAGUALQBpAG4AcwBlAGMAdQByAGUALQByAGUAcQB1AGUAcwB0AHMAAA==\",\"url\":\"https:\\/\\/www.theverge.com\\/2020\\/9\\/10\\/21429838\\/google-android-11-go-edition-devices-2gb-ram-20-percent\",\"title\":\"Android 11 Go is available today, and it will launch apps 20 percent faster - The Verge\",\"structuredCloneVersion\":8,\"docIdentifier\":7,\"structuredCloneState\":\"AgAAAAAA8f8AAAAACAD\\/\\/wAAAAATAP\\/\\/\",\"partitionedPrincipalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ZjQ4NDNkYjYtMDlmYi00ZWJiLTg4ODQtMjE4MzdjNWNhOTIwfSJ9fQ==\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy50aGV2ZXJnZS5jb20vIn19\",\"principalToInherit_base64\":\"eyIwIjp7IjAiOiJtb3otbnVsbHByaW5jaXBhbDp7ZjQ4NDNkYjYtMDlmYi00ZWJiLTg4ODQtMjE4MzdjNWNhOTIwfSJ9fQ==\",\"resultPrincipalURI\":null,\"hasUserInteraction\":false,\"originalURI\":\"https:\\/\\/www.theverge.com\\/2020\\/9\\/10\\/21429838\\/google-android-11-go-edition-devices-2gb-ram-20-percent\",\"docshellUUID\":\"{22a1a76f-6d3c-496a-bbd7-345bc32a8ba9}\"},{\"persist\":true,\"cacheKey\":0,\"ID\":7,\"url\":\"moz-extension:\\/\\/249b38c7-13b9-45de-b5ca-93a37c7321e1\\/readerview.html?url=https%3A%2F%2Fwww.theverge.com%2F2020%2F9%2F10%2F21429838%2Fgoogle-android-11-go-edition-devices-2gb-ram-20-percent&id=137438953484\",\"title\":\"Android 11 Go is available today, and it will launch apps 20 percent faster\",\"docIdentifier\":8,\"partitionedPrincipalToInherit_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vMjQ5YjM4YzctMTNiOS00NWRlLWI1Y2EtOTNhMzdjNzMyMWUxL3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cudGhldmVyZ2UuY29tJTJGMjAyMCUyRjklMkYxMCUyRjIxNDI5ODM4JTJGZ29vZ2xlLWFuZHJvaWQtMTEtZ28tZWRpdGlvbi1kZXZpY2VzLTJnYi1yYW0tMjAtcGVyY2VudCZpZD0xMzc0Mzg5NTM0ODQifX0=\",\"triggeringPrincipal_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vMjQ5YjM4YzctMTNiOS00NWRlLWI1Y2EtOTNhMzdjNzMyMWUxL3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cudGhldmVyZ2UuY29tJTJGMjAyMCUyRjklMkYxMCUyRjIxNDI5ODM4JTJGZ29vZ2xlLWFuZHJvaWQtMTEtZ28tZWRpdGlvbi1kZXZpY2VzLTJnYi1yYW0tMjAtcGVyY2VudCZpZD0xMzc0Mzg5NTM0ODQifX0=\",\"principalToInherit_base64\":\"eyIxIjp7IjAiOiJtb3otZXh0ZW5zaW9uOi8vMjQ5YjM4YzctMTNiOS00NWRlLWI1Y2EtOTNhMzdjNzMyMWUxL3JlYWRlcnZpZXcuaHRtbD91cmw9aHR0cHMlM0ElMkYlMkZ3d3cudGhldmVyZ2UuY29tJTJGMjAyMCUyRjklMkYxMCUyRjIxNDI5ODM4JTJGZ29vZ2xlLWFuZHJvaWQtMTEtZ28tZWRpdGlvbi1kZXZpY2VzLTJnYi1yYW0tMjAtcGVyY2VudCZpZD0xMzc0Mzg5NTM0ODQifX0=\",\"resultPrincipalURI\":null,\"hasUserInteraction\":false,\"docshellUUID\":\"{22a1a76f-6d3c-496a-bbd7-345bc32a8ba9}\"}],\"requestedIndex\":0,\"fromIdx\":-1,\"index\":2,\"userContextId\":0}}"}}]}
+ """.trimIndent()
+
+ val engine = FakeEngine(expectToRestoreRealEngineSessionState = true)
+
+ assertTrue(
+ getFileForEngine(testContext, engine).writeString { json },
+ )
+
+ val storage = SessionStorage(testContext, engine)
+ val browsingSession = storage.restore()
+
+ assertNotNull(browsingSession)
+ assertEquals(2, browsingSession!!.tabs.size)
+
+ browsingSession.tabs[0].state.apply {
+ assertEquals("https://www.theverge.com/", url)
+ assertEquals("The Verge", title)
+ assertEquals("28f428ba-2b19-4c24-993b-763fda2be65c", id)
+ assertNull(parentId)
+ assertEquals(1599815361779, lastAccess)
+ assertNotNull(readerState)
+ assertFalse(readerState.active)
+ }
+
+ browsingSession.tabs[1].state.apply {
+ assertEquals(
+ "https://www.theverge.com/2020/9/10/21429838/google-android-11-go-edition-devices-2gb-ram-20-percent",
+ url,
+ )
+ assertEquals(
+ "Android 11 Go is available today, and it will launch apps 20 percent faster",
+ title,
+ )
+ assertEquals("d6facb8a-0775-45a1-8bc1-2397e2d2bc53", id)
+ assertEquals("28f428ba-2b19-4c24-993b-763fda2be65c", parentId)
+ assertEquals(1599815364500, lastAccess)
+ assertNotNull(readerState)
+ assertTrue(readerState.active)
+ assertEquals(
+ "https://www.theverge.com/2020/9/10/21429838/google-android-11-go-edition-devices-2gb-ram-20-percent",
+ readerState.activeUrl,
+ )
+ }
+ }
+
+ @Test
+ fun `Restored browsing session contains all expected session properties`() {
+ val firstTab = createTab(
+ id = "first-tab",
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ lastAccess = 101010,
+ ).copy(
+ engineState = EngineState(
+ engineSessionState = FakeEngineSessionState("engine-state-of-first-tab"),
+ ),
+ )
+
+ val secondTab = createTab(
+ id = "second-tab",
+ url = "https://www.firefox.com",
+ title = "Firefox",
+ contextId = "Work",
+ lastAccess = 12345678,
+ parent = firstTab,
+ readerState = ReaderState(
+ readerable = true,
+ active = true,
+ ),
+ ).copy(
+ engineState = EngineState(
+ engineSessionState = FakeEngineSessionState("engine-state-of-second-tab"),
+ ),
+ )
+
+ val state = BrowserState(
+ tabs = listOf(firstTab, secondTab),
+ selectedTabId = firstTab.id,
+ )
+
+ val engine = FakeEngine()
+
+ val storage = SessionStorage(testContext, engine)
+ val persisted = storage.save(state)
+ assertTrue(persisted)
+
+ // Read it back
+ val browsingSession = storage.restore()
+
+ assertNotNull(browsingSession)
+ assertEquals(2, browsingSession!!.tabs.size)
+
+ browsingSession.tabs[0].state.apply {
+ assertEquals("https://www.mozilla.org", url)
+ assertEquals("Mozilla", title)
+ assertEquals("first-tab", id)
+ assertNull(parentId)
+ assertEquals(101010, lastAccess)
+ assertNotNull(readerState)
+ assertNull(contextId)
+ assertFalse(readerState.active)
+ }
+
+ browsingSession.tabs[1].state.apply {
+ assertEquals("https://www.firefox.com", url)
+ assertEquals("Firefox", title)
+ assertEquals("second-tab", id)
+ assertEquals("first-tab", parentId)
+ assertEquals(12345678, lastAccess)
+ assertEquals("Work", contextId)
+ assertNotNull(readerState)
+ assertTrue(readerState.active)
+ }
+ }
+
+ @Test
+ fun `Saving state with selected tab id for a tab that does not exist`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(url = "https://www.mozilla.org", id = "mozilla"),
+ createTab(url = "https://getpocket.com", id = "pocket"),
+ ),
+ selectedTabId = "Does Not Exist",
+ )
+
+ val engine = FakeEngine()
+
+ val storage = SessionStorage(testContext, engine)
+ val persisted = storage.save(state)
+ assertTrue(persisted)
+
+ // Read it back
+ val browsingSession = storage.restore()
+ assertNotNull(browsingSession!!)
+
+ assertEquals(2, browsingSession.tabs.size)
+ assertEquals("https://www.mozilla.org", browsingSession.tabs[0].state.url)
+ assertEquals("https://getpocket.com", browsingSession.tabs[1].state.url)
+
+ // Selected tab doesn't exist so we take to most recently accessed one, or
+ // the first one if all tabs have the same last access value.
+ assertEquals("mozilla", browsingSession.selectedTabId)
+ }
+
+ @Test
+ fun `WHEN saving state with crash parent tab THEN don't save tab`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(url = "about:crashparent", id = "crash"),
+ createTab(url = "https://getpocket.com", id = "pocket"),
+ ),
+ selectedTabId = "crash",
+ )
+
+ val engine = FakeEngine()
+
+ val storage = SessionStorage(testContext, engine)
+ val persisted = storage.save(state)
+ assertTrue(persisted)
+
+ // Read it back
+ val browsingSession = storage.restore()
+ assertNotNull(browsingSession!!)
+
+ assertEquals(1, browsingSession.tabs.size)
+ assertEquals("https://getpocket.com", browsingSession.tabs[0].state.url)
+
+ // Selected tab doesn't exist so we take to most recently accessed one, or
+ // the first one if all tabs have the same last access value.
+ assertEquals("pocket", browsingSession.selectedTabId)
+ }
+}
+
+internal fun TabSessionState.assertSameAs(tab: RecoverableTab) {
+ assertEquals(content.url, tab.state.url)
+ assertEquals(content.title, tab.state.title)
+ assertEquals(id, tab.state.id)
+ assertEquals(parentId, tab.state.parentId)
+ assertEquals(contextId, tab.state.contextId)
+ assertEquals(lastAccess, tab.state.lastAccess)
+ assertEquals(readerState, tab.state.readerState)
+ assertEquals(content.private, tab.state.private)
+ assertEquals(
+ (engineState.engineSessionState as? FakeEngineSessionState)?.value ?: "---",
+ (tab.engineSessionState as FakeEngineSessionState).value,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt
new file mode 100644
index 0000000000..b16a59a8de
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt
@@ -0,0 +1,441 @@
+/* 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.browser.session.storage.serialize
+
+import android.util.AtomicFile
+import android.util.JsonReader
+import android.util.JsonWriter
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.ExternalPackage
+import mozilla.components.browser.state.state.LastMediaAccessState
+import mozilla.components.browser.state.state.PackageCategory
+import mozilla.components.browser.state.state.ReaderState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.support.ktx.util.streamJSON
+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.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.util.UUID
+
+@RunWith(AndroidJUnit4::class)
+class BrowserStateWriterReaderTest {
+ @Test
+ fun `Read and write single tab`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ ).copy(
+ engineState = EngineState(engineSessionState = engineState),
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertEquals("https://www.mozilla.org", restoredTab.state.url)
+ assertEquals("Mozilla", restoredTab.state.title)
+ assertEquals("work", restoredTab.state.contextId)
+ assertEquals(engineState, restoredTab.engineSessionState)
+ assertFalse(restoredTab.state.readerState.active)
+ assertNull(restoredTab.state.readerState.activeUrl)
+ assertNull(restoredTab.state.readerState.scrollY)
+ }
+
+ @Test
+ fun `Read and write tab with reader state`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ readerState = ReaderState(active = true, activeUrl = "https://www.example.org", scrollY = 1234),
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertTrue(restoredTab.state.readerState.active)
+ assertEquals("https://www.example.org", restoredTab.state.readerState.activeUrl)
+ assertEquals(1234, restoredTab.state.readerState.scrollY)
+ }
+
+ @Test
+ fun `Read tab with deprecated session source`() {
+ // We don't persist session source of tabs unless it's an external source.
+ // However, in older versions we did persist other types of sources so need to be tolerant
+ // if these older cases are encountered in JSON to remain backward compatible.
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+ val tab = createTab(url = "https://www.mozilla.org", title = "Mozilla")
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+ writeTabWithDeprecatedSource(tab, file)
+
+ // When reading a tab that didn't have a source persisted, we just need to make sure
+ // it is deserialized correctly. In this case, source defaults to `Internal.Restored`.
+ val reader = BrowserStateReader()
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+ assertEquals(SessionState.Source.Internal.None, restoredTab.state.source)
+
+ assertEquals("https://www.mozilla.org", restoredTab.state.url)
+ assertEquals("Mozilla", restoredTab.state.title)
+ }
+
+ @Test
+ fun `Read and write tab with history metadata`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ historyMetadata = HistoryMetadataKey(
+ "https://www.mozilla.org",
+ searchTerm = "test",
+ referrerUrl = "https://firefox.com",
+ ),
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertNotNull(restoredTab.state.historyMetadata)
+ assertEquals(tab.content.url, restoredTab.state.historyMetadata!!.url)
+ }
+
+ @Test
+ fun `Read and write tab with external custom tab source and full caller`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ source = SessionState.Source.External.CustomTab(
+ caller = ExternalPackage("com.mozilla.test", PackageCategory.PRODUCTIVITY),
+ ),
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertNotNull(restoredTab.state.source)
+ assertTrue(restoredTab.state.source is SessionState.Source.External.CustomTab)
+ with(restoredTab.state.source as SessionState.Source.External.CustomTab) {
+ assertEquals("com.mozilla.test", this.caller!!.packageId)
+ assertEquals(PackageCategory.PRODUCTIVITY, this.caller!!.category)
+ }
+ }
+
+ @Test
+ fun `Read and write tab with external action view source and partial caller`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ source = SessionState.Source.External.ActionView(
+ caller = ExternalPackage("com.mozilla.test", category = PackageCategory.UNKNOWN),
+ ),
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertNotNull(restoredTab.state.source)
+ assertTrue(restoredTab.state.source is SessionState.Source.External.ActionView)
+ with(restoredTab.state.source as SessionState.Source.External.ActionView) {
+ assertEquals("com.mozilla.test", this.caller!!.packageId)
+ assertEquals(PackageCategory.UNKNOWN, this.caller!!.category)
+ }
+ }
+
+ @Test
+ fun `Read and write tab with external action send source and missing caller`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ source = SessionState.Source.External.ActionSend(caller = null),
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertNotNull(restoredTab.state.source)
+ assertTrue(restoredTab.state.source is SessionState.Source.External.ActionSend)
+ with(restoredTab.state.source as SessionState.Source.External.ActionSend) {
+ assertNull(this.caller)
+ }
+ }
+
+ @Test
+ fun `Read and write tab with LastMediaAccessState`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ lastMediaAccessState = LastMediaAccessState("https://www.mozilla.org", lastMediaAccess = 333L, true),
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertEquals("https://www.mozilla.org", restoredTab.state.lastMediaAccessState.lastMediaUrl)
+ assertEquals(333L, restoredTab.state.lastMediaAccessState.lastMediaAccess)
+ assertTrue(restoredTab.state.lastMediaAccessState.mediaSessionActive)
+ }
+
+ @Test
+ fun `Read and write tab with createdAt`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+ val currentTime = System.currentTimeMillis()
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ createdAt = currentTime,
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertEquals(currentTime, restoredTab.state.createdAt)
+ }
+
+ @Test
+ fun `Read and write tab without createdAt`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertNotNull(restoredTab.state.createdAt)
+ }
+
+ @Test
+ fun `Read and write tab with search term`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ searchTerms = "test search",
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertEquals("test search", restoredTab.state.searchTerm)
+ }
+
+ @Test
+ fun `Read and write tab without search term`() {
+ val engineState = createFakeEngineState()
+ val engine = createFakeEngine(engineState)
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ contextId = "work",
+ )
+
+ val writer = BrowserStateWriter()
+ val reader = BrowserStateReader()
+
+ val file = AtomicFile(
+ File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString()),
+ )
+
+ assertTrue(writer.writeTab(tab, file))
+
+ val restoredTab = reader.readTab(engine, file)
+ assertNotNull(restoredTab!!)
+
+ assertEquals("", restoredTab.state.searchTerm)
+ }
+}
+
+private fun createFakeEngineState(): EngineSessionState {
+ val state: EngineSessionState = mock()
+ whenever(state.writeTo(any())).then {
+ val writer = it.arguments[0] as JsonWriter
+ writer.beginObject()
+ writer.endObject()
+ }
+ return state
+}
+
+private fun createFakeEngine(engineState: EngineSessionState): Engine {
+ val engine: Engine = mock()
+ whenever(engine.createSessionStateFrom(any())).then {
+ val reader = it.arguments[0] as JsonReader
+ reader.beginObject()
+ reader.endObject()
+ engineState
+ }
+ return engine
+}
+
+private fun writeTabWithDeprecatedSource(tab: TabSessionState, file: AtomicFile) {
+ file.streamJSON { tabWithDeprecatedSource(tab) }
+}
+
+private fun JsonWriter.tabWithDeprecatedSource(
+ tab: TabSessionState,
+) {
+ beginObject()
+
+ name(Keys.SESSION_KEY)
+ beginObject().apply {
+ name(Keys.SESSION_URL_KEY)
+ value(tab.content.url)
+
+ name(Keys.SESSION_UUID_KEY)
+ value(tab.id)
+
+ name(Keys.SESSION_TITLE)
+ value(tab.content.title)
+
+ name(Keys.SESSION_DEPRECATED_SOURCE_KEY)
+ value(tab.source.toString())
+
+ endObject()
+ }
+
+ endObject()
+}
diff --git a/mobile/android/android-components/components/browser/session-storage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/session-storage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/session-storage/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/browser/state/README.md b/mobile/android/android-components/components/browser/state/README.md
new file mode 100644
index 0000000000..c4ecdf39b4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/README.md
@@ -0,0 +1,23 @@
+# [Android Components](../../../README.md) > Browser > State
+
+The `browser-state` component is responsible for maintaining the centralized state of a [browser engine](../../concept/engine/README.md).
+
+The immutable `BrowserState` can be accessed and observed via the `BrowserStore`. Apps and other components can dispatch `Action`s on the store in order to trigger the creation of a new `BrowserState`.
+
+Patterns and concepts this component uses are heavily inspired by Redux. Therefore the [Redux documentation](https://redux.js.org/introduction/getting-started) is an excellent resource for learning about some of those concepts.
+
+## Usage
+
+### 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:browser-state:{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/browser/state/build.gradle b/mobile/android/android-components/components/browser/state/build.gradle
new file mode 100644
index 0000000000..1cee9a2eb1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/build.gradle
@@ -0,0 +1,82 @@
+/* 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: 'kotlin-parcelize'
+
+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'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ packagingOptions {
+ resources {
+ excludes += ['META-INF/proguard/androidx-annotations.pro']
+ }
+ }
+
+ namespace 'mozilla.components.browser.state'
+
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ api project(':lib-state')
+
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':concept-awesomebar')
+ implementation project(':concept-engine')
+ implementation project(':concept-storage')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+
+ // We expose this as API because we are using Response in our public API and do not want every
+ // consumer to have to manually import "concept-fetch".
+ api project(':concept-fetch')
+
+ implementation ComponentsDependencies.androidx_browser
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation ComponentsDependencies.androidx_test_junit
+ androidTestImplementation ComponentsDependencies.androidx_compose_ui_test_manifest
+ androidTestImplementation ComponentsDependencies.androidx_compose_ui_test
+}
+
+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/browser/state/proguard-rules.pro b/mobile/android/android-components/components/browser/state/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/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/browser/state/src/androidTest/java/mozilla/components/browser/state/helper/OnDeviceTargetTest.kt b/mobile/android/android-components/components/browser/state/src/androidTest/java/mozilla/components/browser/state/helper/OnDeviceTargetTest.kt
new file mode 100644
index 0000000000..44da79b294
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/androidTest/java/mozilla/components/browser/state/helper/OnDeviceTargetTest.kt
@@ -0,0 +1,177 @@
+/* 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.browser.state.helper
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * On-device tests for [Target].
+ */
+
+class OnDeviceTargetTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun observingSelectedTab() {
+ val store = BrowserStore()
+
+ val target = Target.SelectedTab
+ var observedTabId: String? = null
+
+ rule.setContent {
+ val state = target.observeAsComposableStateFrom(
+ store = store,
+ observe = { tab -> tab?.id },
+ )
+ observedTabId = state.value?.id
+ }
+
+ assertNull(observedTabId)
+
+ store.dispatchBlockingOnIdle(
+ TabListAction.AddTabAction(createTab("https://www.mozilla.org", id = "mozilla")),
+ )
+
+ rule.runOnIdle {
+ assertEquals("mozilla", observedTabId)
+ }
+
+ store.dispatchBlockingOnIdle(
+ TabListAction.AddTabAction(createTab("https://example.org", id = "example")),
+ )
+
+ rule.runOnIdle {
+ assertEquals("mozilla", observedTabId)
+ }
+
+ store.dispatchBlockingOnIdle(
+ TabListAction.SelectTabAction("example"),
+ )
+
+ rule.runOnIdle {
+ assertEquals("example", observedTabId)
+ }
+
+ store.dispatchBlockingOnIdle(
+ TabListAction.RemoveTabAction("example"),
+ )
+
+ rule.runOnIdle {
+ assertEquals("mozilla", observedTabId)
+ }
+
+ store.dispatchBlockingOnIdle(TabListAction.RemoveAllTabsAction())
+
+ rule.runOnIdle {
+ assertNull(observedTabId)
+ }
+ }
+
+ @Test
+ fun observingPinnedTab() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://www.example.org", id = "example"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val target = Target.Tab("mozilla")
+ var observedTabId: String? = null
+
+ rule.setContent {
+ val state = target.observeAsComposableStateFrom(
+ store = store,
+ observe = { tab -> tab?.id },
+ )
+ observedTabId = state.value?.id
+ }
+
+ assertEquals("mozilla", observedTabId)
+
+ store.dispatchBlockingOnIdle(TabListAction.SelectTabAction("example"))
+
+ rule.runOnIdle {
+ assertEquals("mozilla", observedTabId)
+ }
+
+ store.dispatchBlockingOnIdle(TabListAction.RemoveTabAction("mozilla"))
+
+ rule.runOnIdle {
+ assertNull(observedTabId)
+ }
+ }
+
+ @Test
+ fun observingCustomTab() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://www.example.org", id = "example"),
+ ),
+ customTabs = listOf(
+ createCustomTab("https://www.reddit.com/r/firefox/", id = "reddit"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val target = Target.CustomTab("reddit")
+
+ var observedTabId: String? = null
+
+ rule.setContent {
+ val state = target.observeAsComposableStateFrom(
+ store = store,
+ observe = { tab -> tab?.id },
+ )
+ observedTabId = state.value?.id
+ }
+
+ assertEquals("reddit", observedTabId)
+
+ store.dispatchBlockingOnIdle(TabListAction.SelectTabAction("example"))
+
+ rule.runOnIdle {
+ assertEquals("reddit", observedTabId)
+ }
+
+ store.dispatchBlockingOnIdle(TabListAction.RemoveTabAction("mozilla"))
+
+ rule.runOnIdle {
+ assertEquals("reddit", observedTabId)
+ }
+
+ store.dispatchBlockingOnIdle(CustomTabListAction.RemoveCustomTabAction("reddit"))
+
+ rule.runOnIdle {
+ assertNull(observedTabId)
+ }
+ }
+
+ private fun BrowserStore.dispatchBlockingOnIdle(action: BrowserAction) {
+ rule.runOnIdle {
+ val job = dispatch(action)
+ runBlocking { job.join() }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/state/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/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/browser/state/src/main/java/mozilla/components/browser/state/action/ActionWithTab.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/ActionWithTab.kt
new file mode 100644
index 0000000000..60152244be
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/ActionWithTab.kt
@@ -0,0 +1,32 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.lib.state.Store
+
+/**
+ * Interface for [BrowserAction]s that reference a tab ([SessionState]) via the provided [tabId].
+ */
+interface ActionWithTab {
+ val tabId: String
+}
+
+/**
+ * Looks up the tab referenced by this [ActionWithTab] in the provided [Store] and returns it.
+ * Returns `null` if the tab could not be found.
+ */
+fun ActionWithTab.lookupTabIn(store: Store<BrowserState, BrowserAction>): SessionState? {
+ return store.state.findTabOrCustomTab(tabId)
+}
+
+/**
+ * Casts this [ActionWithTab] to a [BrowserAction].
+ */
+fun ActionWithTab.toBrowserAction(): BrowserAction {
+ return this as BrowserAction
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/AwesomeBarAction.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/AwesomeBarAction.kt
new file mode 100644
index 0000000000..e82d908545
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/AwesomeBarAction.kt
@@ -0,0 +1,30 @@
+/* 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.browser.state.action
+
+import mozilla.components.concept.awesomebar.AwesomeBar
+
+/**
+ * [BrowserAction]s related to interactions with the [AwesomeBar].
+ */
+sealed class AwesomeBarAction : BrowserAction() {
+ /**
+ * Indicates that the suggestions displayed in the [AwesomeBar] have changed.
+ */
+ data class VisibilityStateUpdated(val visibilityState: AwesomeBar.VisibilityState) : AwesomeBarAction()
+
+ /**
+ * Indicates that the user clicked a [suggestion] in the [AwesomeBar].
+ */
+ data class SuggestionClicked(val suggestion: AwesomeBar.Suggestion) : AwesomeBarAction()
+
+ /**
+ * Indicates that the user has finished engaging with the [AwesomeBar].
+ *
+ * An [abandoned] engagement means that the user dismissed the [AwesomeBar] without either clicking on a
+ * suggestion, or entering a search term or URL.
+ */
+ data class EngagementFinished(val abandoned: Boolean) : AwesomeBarAction()
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt
new file mode 100644
index 0000000000..cd492b18e5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt
@@ -0,0 +1,1838 @@
+/* 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.browser.state.action
+
+import android.content.ComponentCallbacks2
+import android.graphics.Bitmap
+import android.os.SystemClock
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.AppIntentState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContainerState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.LoadRequestState
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.browser.state.state.ReaderState
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabGroup
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.TrackingProtectionState
+import mozilla.components.browser.state.state.UndoHistoryState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.FindResultState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.history.HistoryItem
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.media.RecordingDevice
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.search.SearchRequest
+import mozilla.components.concept.engine.translate.Language
+import mozilla.components.concept.engine.translate.LanguageModel
+import mozilla.components.concept.engine.translate.LanguageSetting
+import mozilla.components.concept.engine.translate.TranslationDownloadSize
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.translate.TranslationOptions
+import mozilla.components.concept.engine.translate.TranslationPageSettingOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettings
+import mozilla.components.concept.engine.translate.TranslationSupport
+import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
+import mozilla.components.concept.engine.webextension.WebExtensionPageAction
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.DelicateAction
+import mozilla.components.support.base.android.Clock
+import java.util.Locale
+
+/**
+ * [Action] implementation related to [BrowserState].
+ */
+sealed class BrowserAction : Action
+
+/**
+ * [BrowserAction] dispatched to indicate that the store is initialized and
+ * ready to use. This action is dispatched automatically before any other
+ * action is processed. Its main purpose is to trigger initialization logic
+ * in middlewares. The action itself has no effect on the [BrowserState].
+ */
+object InitAction : BrowserAction()
+
+/**
+ * [BrowserAction] to indicate that restoring [BrowserState] is complete.
+ */
+object RestoreCompleteAction : BrowserAction()
+
+/**
+ * [BrowserAction] implementations to react to extensions process events.
+ */
+sealed class ExtensionsProcessAction : BrowserAction() {
+ /**
+ * [BrowserAction] to indicate when the crash prompt should be displayed to the user.
+ */
+ data class ShowPromptAction(val show: Boolean) : ExtensionsProcessAction()
+
+ /**
+ * [BrowserAction] to indicate that the process has been re-enabled by the user.
+ */
+ object EnabledAction : ExtensionsProcessAction()
+
+ /**
+ * [BrowserAction] to indicate that the process has been left disabled by the user.
+ */
+ object DisabledAction : ExtensionsProcessAction()
+}
+
+/**
+ * [BrowserAction] implementations to react to system events.
+ */
+sealed class SystemAction : BrowserAction() {
+ /**
+ * Optimizes the [BrowserState] by removing unneeded and optional resources if the system is in
+ * a low memory condition.
+ *
+ * @param level The context of the trim, giving a hint of the amount of trimming the application
+ * may like to perform. See constants in [ComponentCallbacks2].
+ */
+ data class LowMemoryAction(
+ val level: Int,
+ ) : SystemAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the [Locale] inside [BrowserState].
+ */
+sealed class LocaleAction : BrowserAction() {
+ /**
+ * Updating the [BrowserState] to reflect app [Locale] changes.
+ *
+ * @property locale the updated [Locale]
+ */
+ data class UpdateLocaleAction(val locale: Locale?) : LocaleAction()
+
+ /**
+ * Restores the [BrowserState.locale] state from storage.
+ */
+ object RestoreLocaleStateAction : LocaleAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the list of [ClosedTabSessionState] inside [BrowserState].
+ */
+sealed class RecentlyClosedAction : BrowserAction() {
+ /**
+ * Adds a list of [RecoverableTab] to the [BrowserState.closedTabs] list.
+ *
+ * @property tabs the [RecoverableTab]s to add
+ */
+ data class AddClosedTabsAction(val tabs: List<RecoverableTab>) : RecentlyClosedAction()
+
+ /**
+ * Removes a [RecoverableTab] from the [BrowserState.closedTabs] list.
+ *
+ * @property tab the [RecoverableTab] to remove
+ */
+ data class RemoveClosedTabAction(val tab: TabState) : RecentlyClosedAction()
+
+ /**
+ * Removes all [RecoverableTab]s from the [BrowserState.closedTabs] list.
+ */
+ object RemoveAllClosedTabAction : RecentlyClosedAction()
+
+ /**
+ * Prunes [RecoverableTab]s from the [BrowserState.closedTabs] list to keep only [maxTabs].
+ */
+ data class PruneClosedTabsAction(val maxTabs: Int) : RecentlyClosedAction()
+
+ /**
+ * Updates [BrowserState.closedTabs] to register the given list of [ClosedTab].
+ */
+ data class ReplaceTabsAction(val tabs: List<TabState>) : RecentlyClosedAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the list of [TabSessionState] inside [BrowserState].
+ */
+sealed class TabListAction : BrowserAction() {
+ /**
+ * Adds a new [TabSessionState] to the [BrowserState.tabs] list.
+ *
+ * @property tab the [TabSessionState] to add
+ * @property select whether or not to the tab should be selected.
+ */
+ data class AddTabAction(val tab: TabSessionState, val select: Boolean = false) : TabListAction()
+
+ /**
+ * Adds multiple [TabSessionState] objects to the [BrowserState.tabs] list.
+ */
+ data class AddMultipleTabsAction(val tabs: List<TabSessionState>) : TabListAction()
+
+ /**
+ * Moves a set of tabIDs into a new position (maintaining internal ordering)
+ *
+ * @property tabIds The IDs of the tabs to move.
+ * @property targetTabId A tab that the moved tabs will be moved next to
+ * @property placeAfter True if the moved tabs should be placed after the target,
+ * False for placing before the target. Irrelevant if the target is one of the tabs being moved,
+ * since then the whole list is moved to where the target was. Ordering of the moved tabs
+ * relative to each other is preserved.
+ */
+ data class MoveTabsAction(
+ val tabIds: List<String>,
+ val targetTabId: String,
+ val placeAfter: Boolean,
+ ) : TabListAction()
+
+ /**
+ * Marks the [TabSessionState] with the given [tabId] as selected tab.
+ *
+ * @property tabId the ID of the tab to select.
+ */
+ data class SelectTabAction(val tabId: String) : TabListAction()
+
+ /**
+ * Removes the [TabSessionState] with the given [tabId] from the list of sessions.
+ *
+ * @property tabId the ID of the tab to remove.
+ * @property selectParentIfExists whether or not a parent tab should be
+ * selected if one exists, defaults to true.
+ */
+ data class RemoveTabAction(val tabId: String, val selectParentIfExists: Boolean = true) :
+ TabListAction()
+
+ /**
+ * Removes the [TabSessionState]s with the given [tabId]s from the list of sessions.
+ *
+ * @property tabIds the IDs of the tabs to remove.
+ */
+ data class RemoveTabsAction(val tabIds: List<String>) : TabListAction()
+
+ /**
+ * Restores state from a (partial) previous state.
+ *
+ * @property tabs the [TabSessionState]s to restore.
+ * @property selectedTabId the ID of the tab to select.
+ */
+ data class RestoreAction(
+ val tabs: List<RecoverableTab>,
+ val selectedTabId: String? = null,
+ val restoreLocation: RestoreLocation,
+ ) : TabListAction() {
+
+ /**
+ * Indicates what location the tabs should be restored at
+ *
+ */
+ enum class RestoreLocation {
+ BEGINNING,
+ END,
+ AT_INDEX,
+ }
+ }
+
+ /**
+ * Removes both private and normal [TabSessionState]s.
+ * @property recoverable Indicates whether removed tabs should be recoverable.
+ */
+ data class RemoveAllTabsAction(val recoverable: Boolean = true) : TabListAction()
+
+ /**
+ * Removes all private [TabSessionState]s.
+ */
+ object RemoveAllPrivateTabsAction : TabListAction()
+
+ /**
+ * Removes all non-private [TabSessionState]s.
+ */
+ object RemoveAllNormalTabsAction : TabListAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating tab partitions and groups inside [BrowserState].
+ */
+sealed class TabGroupAction : BrowserAction() {
+ /**
+ * Adds a new group to [BrowserState.tabPartitions]. If the corresponding partition
+ * doesn't exist it will be created.
+ *
+ * @property partition the ID of the partition the group belongs to.
+ * @property group the [TabGroup] to add.
+ */
+ data class AddTabGroupAction(
+ val partition: String,
+ val group: TabGroup,
+ ) : TabGroupAction()
+
+ /**
+ * Removes a group from [BrowserState.tabPartitions]. Empty partitions will be
+ * be removed i.e., if the last group in a partition is removed, the partition
+ * is removed as well.
+ *
+ * @property partition the ID of the partition the group belongs to.
+ * @property group the ID of the group to remove.
+ */
+ data class RemoveTabGroupAction(
+ val partition: String,
+ val group: String,
+ ) : TabGroupAction()
+
+ /**
+ * Adds the provided tab to a group in [BrowserState].
+ *
+ * @property partition the ID of the partition the group belongs to. If the corresponding
+ * partition doesn't exist it will be created.
+ * @property group the ID of the group.
+ * @property tabId the ID of the tab to add to the group. If the corresponding tab is
+ * already in the group, it won't be added again.
+ */
+ data class AddTabAction(
+ val partition: String,
+ val group: String,
+ val tabId: String,
+ ) : TabGroupAction()
+
+ /**
+ * Adds the provided tabs to a group in [BrowserState].
+ *
+ * @property partition the ID of the partition the group belongs to. If the corresponding
+ * partition doesn't exist it will be created.
+ * @property group the ID of the group.
+ * @property tabIds the IDs of the tabs to add to the group. If a tab is already in the
+ * group, it won't be added again.
+ */
+ data class AddTabsAction(
+ val partition: String,
+ val group: String,
+ val tabIds: List<String>,
+ ) : TabGroupAction()
+
+ /**
+ * Removes the provided tab from a group in [BrowserState].
+ *
+ * @property partition the ID of the partition the group belongs to.
+ * @property group the ID of the group.
+ * @property tabId the ID of the tab to remove from the group.
+ */
+ data class RemoveTabAction(
+ val partition: String,
+ val group: String,
+ val tabId: String,
+ ) : TabGroupAction()
+
+ /**
+ * Removes the provided tabs from a group in [BrowserState].
+ *
+ * @property partition the ID of the partition the group belongs to.
+ * @property group the ID of the group.
+ * @property tabIds the IDs of the tabs to remove from the group.
+ */
+ data class RemoveTabsAction(
+ val partition: String,
+ val group: String,
+ val tabIds: List<String>,
+ ) : TabGroupAction()
+}
+
+/**
+ * [BrowserAction] implementations dealing with "undo" after removing a tab.
+ */
+sealed class UndoAction : BrowserAction() {
+ /**
+ * Adds the list of [tabs] to [UndoHistoryState] with the given [tag].
+ */
+ data class AddRecoverableTabs(
+ val tag: String,
+ val tabs: List<RecoverableTab>,
+ val selectedTabId: String?,
+ ) : UndoAction()
+
+ /**
+ * Clears the tabs from [UndoHistoryState] for the given [tag].
+ */
+ data class ClearRecoverableTabs(
+ val tag: String,
+ ) : UndoAction()
+
+ /**
+ * Restores the tabs in [UndoHistoryState].
+ */
+ object RestoreRecoverableTabs : UndoAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the [TabSessionState] inside [BrowserState].
+ */
+sealed class LastAccessAction : BrowserAction() {
+ /**
+ * Updates the [TabSessionState.lastAccess] timestamp of the tab with the given [tabId].
+ *
+ * @property tabId the ID of the tab to update.
+ * @property lastAccess the value to signify when the tab was last accessed; defaults to [System.currentTimeMillis].
+ */
+ data class UpdateLastAccessAction(
+ val tabId: String,
+ val lastAccess: Long = System.currentTimeMillis(),
+ ) : LastAccessAction()
+
+ /**
+ * Updates [TabSessionState.lastMediaAccessState] for when media started playing in the tab identified by [tabId].
+ *
+ * @property tabId the ID of the tab to update.
+ * @property lastMediaAccess the value to signify when the tab last started playing media.
+ * Defaults to [System.currentTimeMillis].
+ */
+ data class UpdateLastMediaAccessAction(
+ val tabId: String,
+ val lastMediaAccess: Long = System.currentTimeMillis(),
+ ) : LastAccessAction()
+
+ /**
+ * Updates [TabSessionState.lastMediaAccessState] when the media session of this tab is deactivated.
+ *
+ * @property tabId the ID of the tab to update.
+ */
+ data class ResetLastMediaSessionAction(
+ val tabId: String,
+ ) : LastAccessAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating [BrowserState.customTabs].
+ */
+sealed class CustomTabListAction : BrowserAction() {
+ /**
+ * Adds a new [CustomTabSessionState] to [BrowserState.customTabs].
+ *
+ * @property tab the [CustomTabSessionState] to add.
+ */
+ data class AddCustomTabAction(val tab: CustomTabSessionState) : CustomTabListAction()
+
+ /**
+ * Removes an existing [CustomTabSessionState] to [BrowserState.customTabs].
+ *
+ * @property tabId the ID of the custom tab to remove.
+ */
+ data class RemoveCustomTabAction(val tabId: String) : CustomTabListAction()
+
+ /**
+ * Converts an existing [CustomTabSessionState] to a regular/normal [TabSessionState].
+ */
+ data class TurnCustomTabIntoNormalTabAction(val tabId: String) : CustomTabListAction()
+
+ /**
+ * Removes all custom tabs [TabSessionState]s.
+ */
+ object RemoveAllCustomTabsAction : CustomTabListAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the [ContentState] of a single [SessionState] inside
+ * [BrowserState].
+ */
+sealed class ContentAction : BrowserAction() {
+ /**
+ * Removes the icon of the [ContentState] with the given [sessionId].
+ */
+ data class RemoveIconAction(val sessionId: String) : ContentAction()
+
+ /**
+ * Updates the URL of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateUrlAction(
+ val sessionId: String,
+ val url: String,
+ val hasUserGesture: Boolean = false,
+ ) : ContentAction()
+
+ /**
+ * Updates the progress of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateProgressAction(val sessionId: String, val progress: Int) : ContentAction()
+
+ /**
+ * Updates permissions highlights of the [ContentState] with the given [sessionId].
+ */
+ sealed class UpdatePermissionHighlightsStateAction : ContentAction() {
+ /**
+ * Updates the notificationChanged property of the [PermissionHighlightsState] with the given [tabId].
+ */
+ data class NotificationChangedAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the cameraChanged property of the [PermissionHighlightsState] with the given [tabId].
+ */
+ data class CameraChangedAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the locationChanged property of the [PermissionHighlightsState] with the given [tabId].
+ */
+ data class LocationChangedAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the microphoneChanged property of the [PermissionHighlightsState] with the given [tabId].
+ */
+ data class MicrophoneChangedAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the persistentStorageChanged property of the [PermissionHighlightsState] with the given [tabId].
+ */
+ data class PersistentStorageChangedAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the mediaKeySystemAccessChanged property of the [PermissionHighlightsState]
+ * with the given [tabId].
+ */
+ data class MediaKeySystemAccesChangedAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the autoPlayAudibleChanged property of the [PermissionHighlightsState]
+ * with the given [tabId].
+ */
+ data class AutoPlayAudibleChangedAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the autoPlayInaudibleChanged property of the [PermissionHighlightsState] with the given [tabId].
+ */
+ data class AutoPlayInAudibleChangedAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the autoPlayAudibleBlocking property of the [PermissionHighlightsState] with the given [tabId].
+ */
+ data class AutoPlayAudibleBlockingAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates the autoPlayInaudibleBlocking property of the [PermissionHighlightsState] with the given [tabId].
+ */
+ data class AutoPlayInAudibleBlockingAction(val tabId: String, val value: Boolean) :
+ UpdatePermissionHighlightsStateAction()
+
+ /**
+ * Updates permissions highlights of the [ContentState] with the given [tabId]
+ * to its default value.
+ */
+ data class Reset(val tabId: String) : UpdatePermissionHighlightsStateAction()
+ }
+
+ /**
+ * Updates the title of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateTitleAction(val sessionId: String, val title: String) : ContentAction()
+
+ /**
+ * Updates the preview image URL of the [ContentState] with the given [sessionId].
+ */
+ data class UpdatePreviewImageAction(val sessionId: String, val previewImageUrl: String) :
+ ContentAction()
+
+ /**
+ * Updates the loading state of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateLoadingStateAction(val sessionId: String, val loading: Boolean) :
+ ContentAction()
+
+ /**
+ * Updates the refreshCanceled state of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateRefreshCanceledStateAction(val sessionId: String, val refreshCanceled: Boolean) :
+ ContentAction()
+
+ /**
+ * Updates the search terms of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateSearchTermsAction(val sessionId: String, val searchTerms: String) :
+ ContentAction()
+
+ /**
+ * Updates the isSearch state and optionally the search engine name of the [ContentState] with
+ * the given [sessionId].
+ */
+ data class UpdateIsSearchAction(
+ val sessionId: String,
+ val isSearch: Boolean,
+ val searchEngineName: String? = null,
+ ) : ContentAction()
+
+ /**
+ * Updates the [SecurityInfoState] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateSecurityInfoAction(
+ val sessionId: String,
+ val securityInfo: SecurityInfoState,
+ ) : ContentAction()
+
+ /**
+ * Updates the icon of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateIconAction(val sessionId: String, val pageUrl: String, val icon: Bitmap) :
+ ContentAction()
+
+ /**
+ * Updates the thumbnail of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateThumbnailAction(val sessionId: String, val thumbnail: Bitmap) : ContentAction()
+
+ /**
+ * Updates the [DownloadState] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateDownloadAction(val sessionId: String, val download: DownloadState) :
+ ContentAction()
+
+ /**
+ * Closes the [DownloadState.response] of the [ContentState.download]
+ * and removes the [DownloadState] of the [ContentState] with the given [sessionId].
+ */
+ data class CancelDownloadAction(val sessionId: String, val downloadId: String) : ContentAction()
+
+ /**
+ * Removes the [DownloadState] of the [ContentState] with the given [sessionId].
+ */
+ data class ConsumeDownloadAction(val sessionId: String, val downloadId: String) : ContentAction()
+
+ /**
+ * Updates the [HitResult] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateHitResultAction(val sessionId: String, val hitResult: HitResult) :
+ ContentAction()
+
+ /**
+ * Removes the [HitResult] of the [ContentState] with the given [sessionId].
+ */
+ data class ConsumeHitResultAction(val sessionId: String) : ContentAction()
+
+ /**
+ * Updates the [PromptRequest] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdatePromptRequestAction(val sessionId: String, val promptRequest: PromptRequest) :
+ ContentAction()
+
+ /**
+ * Removes the [PromptRequest] of the [ContentState] with the given [sessionId].
+ */
+ data class ConsumePromptRequestAction(val sessionId: String, val promptRequest: PromptRequest) :
+ ContentAction()
+
+ /**
+ * Replaces a prompt request from [ContentState] with [promptRequest] based on the [previousPromptUid].
+ */
+ data class ReplacePromptRequestAction(
+ val sessionId: String,
+ val previousPromptUid: String,
+ val promptRequest: PromptRequest,
+ ) : ContentAction()
+
+ /**
+ * Adds a [FindResultState] to the [ContentState] with the given [sessionId].
+ */
+ data class AddFindResultAction(val sessionId: String, val findResult: FindResultState) :
+ ContentAction()
+
+ /**
+ * Removes all [FindResultState]s of the [ContentState] with the given [sessionId].
+ */
+ data class ClearFindResultsAction(val sessionId: String) : ContentAction()
+
+ /**
+ * Updates the [WindowRequest] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateWindowRequestAction(val sessionId: String, val windowRequest: WindowRequest) :
+ ContentAction()
+
+ /**
+ * Removes the [WindowRequest] of the [ContentState] with the given [sessionId].
+ */
+ data class ConsumeWindowRequestAction(val sessionId: String) : ContentAction()
+
+ /**
+ * Updates the [SearchRequest] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateSearchRequestAction(val sessionId: String, val searchRequest: SearchRequest) :
+ ContentAction()
+
+ /**
+ * Removes the [SearchRequest] of the [ContentState] with the given [sessionId].
+ */
+ data class ConsumeSearchRequestAction(val sessionId: String) : ContentAction()
+
+ /**
+ * Updates [fullScreenEnabled] with the given [sessionId].
+ */
+ data class FullScreenChangedAction(val sessionId: String, val fullScreenEnabled: Boolean) :
+ ContentAction()
+
+ /**
+ * Updates [pipEnabled] with the given [sessionId].
+ */
+ data class PictureInPictureChangedAction(val sessionId: String, val pipEnabled: Boolean) :
+ ContentAction()
+
+ /**
+ * Updates the [layoutInDisplayCutoutMode] with the given [sessionId].
+ *
+ * @property sessionId the ID of the session
+ * @property layoutInDisplayCutoutMode value of defined in https://developer.android.com/reference/android/view/WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ */
+ data class ViewportFitChangedAction(val sessionId: String, val layoutInDisplayCutoutMode: Int) :
+ ContentAction()
+
+ /**
+ * Updates the [ContentState] of the given [sessionId] to indicate whether or not a back navigation is possible.
+ */
+ data class UpdateBackNavigationStateAction(val sessionId: String, val canGoBack: Boolean) :
+ ContentAction()
+
+ /**
+ * Updates the [ContentState] of the given [sessionId] to indicate whether the first contentful paint has happened.
+ */
+ data class UpdateFirstContentfulPaintStateAction(
+ val sessionId: String,
+ val firstContentfulPaint: Boolean,
+ ) : ContentAction()
+
+ /**
+ * Updates the [ContentState] of the given [sessionId] to indicate whether or not a forward navigation is possible.
+ */
+ data class UpdateForwardNavigationStateAction(
+ val sessionId: String,
+ val canGoForward: Boolean,
+ ) : ContentAction()
+
+ /**
+ * Updates the [WebAppManifest] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateWebAppManifestAction(
+ val sessionId: String,
+ val webAppManifest: WebAppManifest,
+ ) : ContentAction()
+
+ /**
+ * Removes the [WebAppManifest] of the [ContentState] with the given [sessionId].
+ */
+ data class RemoveWebAppManifestAction(val sessionId: String) : ContentAction()
+
+ /**
+ * Updates the [ContentState] of the given [sessionId] to indicate the current history state.
+ */
+ data class UpdateHistoryStateAction(
+ val sessionId: String,
+ val historyList: List<HistoryItem>,
+ val currentIndex: Int,
+ ) : ContentAction()
+
+ /**
+ * Updates the [LoadRequestState] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateLoadRequestAction(val sessionId: String, val loadRequest: LoadRequestState) : ContentAction()
+
+ /**
+ * Adds a new content permission request to the [ContentState] list.
+ * */
+ data class UpdatePermissionsRequest(
+ val sessionId: String,
+ val permissionRequest: PermissionRequest,
+ ) : ContentAction()
+
+ /**
+ * Deletes a content permission request from the [ContentState] list.
+ * */
+ data class ConsumePermissionsRequest(
+ val sessionId: String,
+ val permissionRequest: PermissionRequest,
+ ) : ContentAction()
+
+ /**
+ * Removes all content permission requests from the [ContentState] list.
+ * */
+ data class ClearPermissionRequests(
+ val sessionId: String,
+ ) : ContentAction()
+
+ /**
+ * Adds a new app permission request to the [ContentState] list.
+ * */
+ data class UpdateAppPermissionsRequest(
+ val sessionId: String,
+ val appPermissionRequest: PermissionRequest,
+ ) : ContentAction()
+
+ /**
+ * Deletes an app permission request from the [ContentState] list.
+ * */
+ data class ConsumeAppPermissionsRequest(
+ val sessionId: String,
+ val appPermissionRequest: PermissionRequest,
+ ) : ContentAction()
+
+ /**
+ * Removes all app permission requests from the [ContentState] list.
+ * */
+ data class ClearAppPermissionRequests(
+ val sessionId: String,
+ ) : ContentAction()
+
+ /**
+ * Sets the list of active recording devices (webcam, microphone, ..) used by web content.
+ */
+ data class SetRecordingDevices(
+ val sessionId: String,
+ val devices: List<RecordingDevice>,
+ ) : ContentAction()
+
+ /**
+ * Updates the [ContentState] of the given [sessionId] to indicate whether or not desktop mode is enabled.
+ */
+ data class UpdateDesktopModeAction(val sessionId: String, val enabled: Boolean) : ContentAction()
+
+ /**
+ * Updates the [AppIntentState] of the [ContentState] with the given [sessionId].
+ */
+ data class UpdateAppIntentAction(val sessionId: String, val appIntent: AppIntentState) :
+ ContentAction()
+
+ /**
+ * Removes the [AppIntentState] of the [ContentState] with the given [sessionId].
+ */
+ data class ConsumeAppIntentAction(val sessionId: String) : ContentAction()
+
+ /**
+ * Updates whether the toolbar should be forced to expand or have it follow the default behavior.
+ */
+ data class UpdateExpandedToolbarStateAction(val sessionId: String, val expanded: Boolean) : ContentAction()
+
+ /**
+ * Updates the [ContentState] with the provided [tabId] to the appropriate priority based on any
+ * existing form data.
+ */
+ data class UpdateHasFormDataAction(val tabId: String, val containsFormData: Boolean) : ContentAction()
+
+ /**
+ * Lowers priority of the [tabId] to default after certain period of time
+ */
+ data class UpdatePriorityToDefaultAfterTimeoutAction(val tabId: String) : ContentAction()
+
+ /**
+ * Indicates the given [tabId] was unable to be checked for form data.
+ */
+ data class CheckForFormDataExceptionAction(val tabId: String, val throwable: Throwable) : ContentAction()
+
+ /**
+ * Updates the [ContentState.isProductUrl] state for the non private tab with the given [tabId].
+ */
+ data class UpdateProductUrlStateAction(
+ val tabId: String,
+ val isProductUrl: Boolean,
+ ) : ContentAction()
+}
+
+/**
+ * [BrowserAction] implementations related to translating a web content page.
+ */
+sealed class TranslationsAction : BrowserAction() {
+
+ /**
+ * Requests that the initialization data for the global translations engine state
+ * be fetched from the translations engine and set on [BrowserState.translationEngine].
+ */
+ object InitTranslationsBrowserState : TranslationsAction()
+
+ /**
+ * Indicates that the translations engine expects the user may want to translate the page on
+ * the given [tabId].
+ *
+ * For example, could be used to show toolbar UI that translations are an option.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ */
+ data class TranslateExpectedAction(
+ override val tabId: String,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Indicates that the translations engine suggests the user should be notified of the ability to
+ * translate on the given [tabId].
+ *
+ * For example, could be used to show a reminder UI popup or a star beside the toolbar UI to strongly signal that
+ * translations are an option.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property isOfferTranslate If the engine should offer translating the page to the user.
+ */
+ data class TranslateOfferAction(
+ override val tabId: String,
+ val isOfferTranslate: Boolean,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Indicates the translation state on the given [tabId].
+ *
+ * This provides the translations engine state. Not to be confused with
+ * the browser engine state of the translations component.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property translationEngineState The state of the translation engine for the
+ * page.
+ */
+ data class TranslateStateChangeAction(
+ override val tabId: String,
+ val translationEngineState: TranslationEngineState,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Used to translate the page for a given [tabId].
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property fromLanguage The BCP 47 language tag that the page should be translated from.
+ * @property toLanguage The BCP 47 language tag that the page should be translated to.
+ * @property options Options for how the translation should be processed.
+ */
+ data class TranslateAction(
+ override val tabId: String,
+ val fromLanguage: String,
+ val toLanguage: String,
+ val options: TranslationOptions?,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Indicates the given [tabId] should restore the original pre-translated content.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ */
+ data class TranslateRestoreAction(
+ override val tabId: String,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Fetch the translation download size for the given [tabId]. Will use the specified
+ * [fromLanguage] and [toLanguage] to query the download size.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should set the state on.
+ * @property fromLanguage The from [Language] in the translation pair.
+ * @property toLanguage The to [Language] in the translation pair.
+ */
+ data class FetchTranslationDownloadSizeAction(
+ override val tabId: String,
+ val fromLanguage: Language,
+ val toLanguage: Language,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Set the [TranslationDownloadSize] for the given [tabId].
+ *
+ * @property tabId The ID of the tab the [EngineSession] should set the state on.
+ * @property translationSize The [TranslationDownloadSize] that contains a to/from translations
+ * pair and a download size.
+ */
+ data class SetTranslationDownloadSizeAction(
+ override val tabId: String,
+ val translationSize: TranslationDownloadSize,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Indicates the given [tabId] was successful in translating or restoring the page
+ * or acquiring a necessary resource.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property operation The translation operation that was successful.
+ */
+ data class TranslateSuccessAction(
+ override val tabId: String,
+ val operation: TranslationOperation,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Indicates the given [tabId] was unable to translate or restore the page or acquire a
+ * necessary resource.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property operation The translation operation that failed.
+ * @property translationError The error that occurred.
+ */
+ data class TranslateExceptionAction(
+ override val tabId: String,
+ val operation: TranslationOperation,
+ val translationError: TranslationError,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Indicates an app level translations error occurred and to set the [TranslationError] on
+ * [BrowserState.translationEngine].
+ *
+ * @property error The [TranslationError] that occurred.
+ */
+ data class EngineExceptionAction(
+ val error: TranslationError,
+ ) : TranslationsAction()
+
+ /**
+ * Indicates that the given [operation] data should be fetched for the given [tabId].
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property operation The translation operation that failed.
+ */
+ data class OperationRequestedAction(
+ override val tabId: String,
+ val operation: TranslationOperation,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Sets whether the device architecture supports translations or not on
+ * [BrowserState.translationEngine].
+ *
+ * @property isEngineSupported If the engine supports translations on this device.
+ */
+ data class SetEngineSupportedAction(
+ val isEngineSupported: Boolean,
+ ) : TranslationsAction()
+
+ /**
+ * Sets the languages that are supported by the translations engine on the
+ * [BrowserState.translationEngine].
+ *
+ * @property supportedLanguages The languages the engine supports for translation.
+ */
+ data class SetSupportedLanguagesAction(
+ val supportedLanguages: TranslationSupport?,
+ ) : TranslationsAction()
+
+ /**
+ * Sets the given page settings on the page on the given [tabId]'s store.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property pageSettings The new page settings.
+ */
+ data class SetPageSettingsAction(
+ override val tabId: String,
+ val pageSettings: TranslationPageSettings?,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Updates the specified page setting operation on the translation engine and ensures the final
+ * state on the given [tabId]'s store remains in-sync.
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property operation The page setting update operation to perform.
+ * @property setting The boolean value of the corresponding [operation].
+ */
+ data class UpdatePageSettingAction(
+ override val tabId: String,
+ val operation: TranslationPageSettingOperation,
+ val setting: Boolean,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Sets the map of BCP 47 language codes (key) and the [LanguageSetting] option (value).
+ *
+ * @property languageSettings A map containing a key of BCP 47 language code and its
+ * [LanguageSetting].
+ */
+ data class SetLanguageSettingsAction(
+ val languageSettings: Map<String, LanguageSetting>,
+ ) : TranslationsAction()
+
+ /**
+ * Sets the list of sites that the user has opted to never translate.
+ *
+ * @property tabId The ID of the tab the [EngineSession] that requested the list.
+ * @property neverTranslateSites The never translate sites.
+ */
+ data class SetNeverTranslateSitesAction(
+ override val tabId: String,
+ val neverTranslateSites: List<String>,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Remove from the list of sites the user has opted to never translate.
+ *
+ * @property tabId The ID of the tab the [EngineSession] that requested the removal.
+ * @property origin A site origin URI that will have the specified never translate permission set.
+ */
+ data class RemoveNeverTranslateSiteAction(
+ override val tabId: String,
+ val origin: String,
+ ) : TranslationsAction(), ActionWithTab
+
+ /**
+ * Sets the list of language machine learning translation models the translation engine has available.
+ *
+ * @property languageModels The list of language machine learning translation models.
+ */
+ data class SetLanguageModelsAction(
+ val languageModels: List<LanguageModel>,
+ ) : TranslationsAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the [TrackingProtectionState] of a single [SessionState] inside
+ * [BrowserState].
+ */
+sealed class TrackingProtectionAction : BrowserAction() {
+ /**
+ * Updates the [TrackingProtectionState.enabled] flag.
+ */
+ data class ToggleAction(val tabId: String, val enabled: Boolean) : TrackingProtectionAction()
+
+ /**
+ * Updates the [TrackingProtectionState.ignoredOnTrackingProtection] flag.
+ */
+ data class ToggleExclusionListAction(val tabId: String, val excluded: Boolean) :
+ TrackingProtectionAction()
+
+ /**
+ * Adds a [Tracker] to the [TrackingProtectionState.blockedTrackers] list.
+ */
+ data class TrackerBlockedAction(val tabId: String, val tracker: Tracker) :
+ TrackingProtectionAction()
+
+ /**
+ * Adds a [Tracker] to the [TrackingProtectionState.loadedTrackers] list.
+ */
+ data class TrackerLoadedAction(val tabId: String, val tracker: Tracker) :
+ TrackingProtectionAction()
+
+ /**
+ * Clears the [TrackingProtectionState.blockedTrackers] and [TrackingProtectionState.blockedTrackers] lists.
+ */
+ data class ClearTrackersAction(val tabId: String) : TrackingProtectionAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the [SessionState.cookieBanner] of a single [SessionState] inside
+ * [BrowserState].
+ */
+sealed class CookieBannerAction : BrowserAction() {
+ /**
+ * Updates the [SessionState.cookieBanner] state or a a single [SessionState].
+ */
+ data class UpdateStatusAction(val tabId: String, val status: CookieBannerHandlingStatus) :
+ CookieBannerAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating [BrowserState.extensions] and
+ * [TabSessionState.extensionState].
+ */
+sealed class WebExtensionAction : BrowserAction() {
+ /**
+ * Updates [BrowserState.extensions] to register the given [extension] as installed.
+ */
+ data class InstallWebExtensionAction(val extension: WebExtensionState) : WebExtensionAction()
+
+ /**
+ * Updates [BrowserState.webExtensionPromptRequest] give the given [promptRequest].
+ */
+ data class UpdatePromptRequestWebExtensionAction(val promptRequest: WebExtensionPromptRequest) :
+ WebExtensionAction()
+
+ /**
+ * Removes the actual [WebExtensionPromptRequest] of the [BrowserState].
+ */
+ object ConsumePromptRequestWebExtensionAction : WebExtensionAction()
+
+ /**
+ * Removes all state of the uninstalled extension from [BrowserState.extensions]
+ * and [TabSessionState.extensionState].
+ */
+ data class UninstallWebExtensionAction(val extensionId: String) : WebExtensionAction()
+
+ /**
+ * Removes state of all extensions from [BrowserState.extensions]
+ * and [TabSessionState.extensionState].
+ */
+ object UninstallAllWebExtensionsAction : WebExtensionAction()
+
+ /**
+ * Updates the [WebExtensionState.enabled] flag.
+ */
+ data class UpdateWebExtensionEnabledAction(val extensionId: String, val enabled: Boolean) :
+ WebExtensionAction()
+
+ /**
+ * Updates the [WebExtensionState.allowedInPrivateBrowsing] flag.
+ */
+ data class UpdateWebExtensionAllowedInPrivateBrowsingAction(
+ val extensionId: String,
+ val allowed: Boolean,
+ ) :
+ WebExtensionAction()
+
+ /**
+ * Updates the given [updatedExtension] in the [BrowserState.extensions].
+ */
+ data class UpdateWebExtensionAction(val updatedExtension: WebExtensionState) :
+ WebExtensionAction()
+
+ /**
+ * Updates a browser action of a given [extensionId].
+ */
+ data class UpdateBrowserAction(
+ val extensionId: String,
+ val browserAction: WebExtensionBrowserAction,
+ ) : WebExtensionAction()
+
+ /**
+ * Updates a page action of a given [extensionId].
+ */
+ data class UpdatePageAction(
+ val extensionId: String,
+ val pageAction: WebExtensionPageAction,
+ ) : WebExtensionAction()
+
+ /**
+ * Keeps track of the last session used to display an extension action popup.
+ */
+ data class UpdatePopupSessionAction(
+ val extensionId: String,
+ val popupSessionId: String? = null,
+ val popupSession: EngineSession? = null,
+ ) : WebExtensionAction()
+
+ /**
+ * Updates a tab-specific browser action that belongs to the given [sessionId] and [extensionId] on the
+ * [TabSessionState.extensionState].
+ */
+ data class UpdateTabBrowserAction(
+ val sessionId: String,
+ val extensionId: String,
+ val browserAction: WebExtensionBrowserAction,
+ ) : WebExtensionAction()
+
+ /**
+ * Updates a page action that belongs to the given [sessionId] and [extensionId] on the
+ * [TabSessionState.extensionState].
+ */
+ data class UpdateTabPageAction(
+ val sessionId: String,
+ val extensionId: String,
+ val pageAction: WebExtensionPageAction,
+ ) : WebExtensionAction()
+
+ /**
+ * Updates the [BrowserState.activeWebExtensionTabId] to mark a tab active for web extensions
+ * e.g. to support tabs.query({active: true}).
+ */
+ data class UpdateActiveWebExtensionTabAction(
+ val activeWebExtensionTabId: String?,
+ ) : WebExtensionAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the [EngineState] of a single [SessionState] inside
+ * [BrowserState].
+ */
+sealed class EngineAction : BrowserAction() {
+ /**
+ * Creates an [EngineSession] for the given [tabId] if none exists yet.
+ */
+ data class CreateEngineSessionAction(
+ override val tabId: String,
+ val skipLoading: Boolean = false,
+ val followupAction: BrowserAction? = null,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Loads the given [url] in the tab with the given [tabId].
+ */
+ data class LoadUrlAction(
+ override val tabId: String,
+ val url: String,
+ val flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ val additionalHeaders: Map<String, String>? = null,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Indicates to observers that a [LoadUrlAction] was shortcutted and a direct
+ * load on the engine occurred instead.
+ */
+ data class OptimizedLoadUrlTriggeredAction(
+ override val tabId: String,
+ val url: String,
+ val flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ val additionalHeaders: Map<String, String>? = null,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Loads [data] in the tab with the given [tabId].
+ */
+ data class LoadDataAction(
+ override val tabId: String,
+ val data: String,
+ val mimeType: String = "text/html",
+ val encoding: String = "UTF-8",
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Reloads the tab with the given [tabId].
+ */
+ data class ReloadAction(
+ override val tabId: String,
+ val flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Navigates back in the tab with the given [tabId].
+ */
+ data class GoBackAction(
+ override val tabId: String,
+ val userInteraction: Boolean = true,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Navigates forward in the tab with the given [tabId].
+ */
+ data class GoForwardAction(
+ override val tabId: String,
+ val userInteraction: Boolean = true,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Navigates to the specified index in the history of the tab with the given [tabId].
+ */
+ data class GoToHistoryIndexAction(
+ override val tabId: String,
+ val index: Int,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Enables/disables desktop mode in the tabs with the given [tabId].
+ */
+ data class ToggleDesktopModeAction(
+ override val tabId: String,
+ val enable: Boolean,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Exits fullscreen mode in the tabs with the given [tabId].
+ */
+ data class ExitFullScreenModeAction(
+ override val tabId: String,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Indicates the given [tabId] is to print the page content.
+ */
+ data class PrintContentAction(
+ override val tabId: String,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Indicates the given [tabId] completed printing the page content.
+ */
+ data class PrintContentCompletedAction(
+ override val tabId: String,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Indicates the given [tabId] was unable to print the page content.
+ * [isPrint] indicates if it is in response to a print (true) or PDF saving (false).
+ */
+ data class PrintContentExceptionAction(
+ override val tabId: String,
+ val isPrint: Boolean,
+ val throwable: Throwable,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Navigates back in the tab with the given [tabId].
+ */
+ data class SaveToPdfAction(
+ override val tabId: String,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Indicates the given [tabId] was successful in generating a requested PDF page.
+ */
+ data class SaveToPdfCompleteAction(
+ override val tabId: String,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Indicates the given [tabId] was unable to generate a requested save to PDF page.
+ */
+ data class SaveToPdfExceptionAction(
+ override val tabId: String,
+ val throwable: Throwable,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Clears browsing data for the tab with the given [tabId].
+ */
+ data class ClearDataAction(
+ override val tabId: String,
+ val data: Engine.BrowsingData,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Attaches the provided [EngineSession] to the session with the provided [tabId].
+ *
+ * @property tabId The ID of the tab the [EngineSession] should be linked to.
+ * @property engineSession The [EngineSession] that should be linked to the tab.
+ * @property timestamp Timestamp (milliseconds) of when the linking has happened (By default
+ * set to [SystemClock.elapsedRealtime].
+ */
+ data class LinkEngineSessionAction(
+ override val tabId: String,
+ val engineSession: EngineSession,
+ val timestamp: Long = Clock.elapsedRealtime(),
+ val skipLoading: Boolean = false,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Suspends the [EngineSession] of the session with the provided [tabId].
+ */
+ data class SuspendEngineSessionAction(
+ override val tabId: String,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Marks the [EngineSession] of the session with the provided [tabId] as killed (The matching
+ * content process was killed).
+ */
+ data class KillEngineSessionAction(
+ override val tabId: String,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Detaches the current [EngineSession] from the session with the provided [tabId].
+ */
+ data class UnlinkEngineSessionAction(
+ override val tabId: String,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Updates the [EngineState.initializing] flag of the session with the provided [tabId].
+ */
+ data class UpdateEngineSessionInitializingAction(
+ override val tabId: String,
+ val initializing: Boolean,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Updates the [EngineSessionState] of the session with the provided [tabId].
+ */
+ data class UpdateEngineSessionStateAction(
+ override val tabId: String,
+ val engineSessionState: EngineSessionState,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Updates the [EngineSession.Observer] of the session with the provided [tabId].
+ */
+ data class UpdateEngineSessionObserverAction(
+ override val tabId: String,
+ val engineSessionObserver: EngineSession.Observer,
+ ) : EngineAction(), ActionWithTab
+
+ /**
+ * Purges the back/forward history of all tabs and custom tabs.
+ */
+ object PurgeHistoryAction : EngineAction()
+}
+
+/**
+ * [BrowserAction] implementations to react to crashes.
+ */
+sealed class CrashAction : BrowserAction() {
+ /**
+ * Updates the [SessionState] of the session with provided ID to mark it as crashed.
+ */
+ data class SessionCrashedAction(val tabId: String) : CrashAction()
+
+ /**
+ * Updates the [SessionState] of the session with provided ID to mark it as restored.
+ */
+ data class RestoreCrashedSessionAction(val tabId: String) : CrashAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the [ReaderState] of a single [TabSessionState] inside
+ * [BrowserState].
+ */
+sealed class ReaderAction : BrowserAction() {
+ /**
+ * Updates the [ReaderState.readerable] flag.
+ */
+ data class UpdateReaderableAction(val tabId: String, val readerable: Boolean) : ReaderAction()
+
+ /**
+ * Updates the [ReaderState.active] flag.
+ */
+ data class UpdateReaderActiveAction(val tabId: String, val active: Boolean) : ReaderAction()
+
+ /**
+ * Updates the [ReaderState.checkRequired] flag.
+ */
+ data class UpdateReaderableCheckRequiredAction(val tabId: String, val checkRequired: Boolean) :
+ ReaderAction()
+
+ /**
+ * Updates the [ReaderState.connectRequired] flag.
+ */
+ data class UpdateReaderConnectRequiredAction(val tabId: String, val connectRequired: Boolean) :
+ ReaderAction()
+
+ /**
+ * Updates the [ReaderState.baseUrl].
+ */
+ data class UpdateReaderBaseUrlAction(val tabId: String, val baseUrl: String) : ReaderAction()
+
+ /**
+ * Updates the [ReaderState.activeUrl].
+ */
+ data class UpdateReaderActiveUrlAction(val tabId: String, val activeUrl: String) :
+ ReaderAction()
+
+ /**
+ * Updates the [ReaderState.scrollY].
+ */
+ data class UpdateReaderScrollYAction(val tabId: String, val scrollY: Int) : ReaderAction()
+
+ /**
+ * Clears the [ReaderState.activeUrl].
+ */
+ data class ClearReaderActiveUrlAction(val tabId: String) : ReaderAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the [MediaSessionState].
+ */
+sealed class MediaSessionAction : BrowserAction() {
+ /**
+ * Activates [MediaSession] owned by the tab with id [tabId].
+ */
+ data class ActivatedMediaSessionAction(
+ val tabId: String,
+ val mediaSessionController: MediaSession.Controller,
+ ) : MediaSessionAction()
+
+ /**
+ * Activates [MediaSession] owned by the tab with id [tabId].
+ */
+ data class DeactivatedMediaSessionAction(
+ val tabId: String,
+ ) : MediaSessionAction()
+
+ /**
+ * Updates the [MediaSession.Metadata] owned by the tab with id [tabId].
+ */
+ data class UpdateMediaMetadataAction(
+ val tabId: String,
+ val metadata: MediaSession.Metadata,
+ ) : MediaSessionAction()
+
+ /**
+ * Updates the [MediaSession.PlaybackState] owned by the tab with id [tabId].
+ */
+ data class UpdateMediaPlaybackStateAction(
+ val tabId: String,
+ val playbackState: MediaSession.PlaybackState,
+ ) : MediaSessionAction()
+
+ /**
+ * Updates the [MediaSession.Feature] owned by the tab with id [tabId].
+ */
+ data class UpdateMediaFeatureAction(
+ val tabId: String,
+ val features: MediaSession.Feature,
+ ) : MediaSessionAction()
+
+ /**
+ * Updates the [MediaSession.PositionState] owned by the tab with id [tabId].
+ */
+ data class UpdateMediaPositionStateAction(
+ val tabId: String,
+ val positionState: MediaSession.PositionState,
+ ) : MediaSessionAction()
+
+ /**
+ * Updates the [muted] owned by the tab with id [tabId].
+ */
+ data class UpdateMediaMutedAction(
+ val tabId: String,
+ val muted: Boolean,
+ ) : MediaSessionAction()
+
+ /**
+ * Updates the [fullScreen] and [MediaSession.ElementMetadata] owned by the tab with id [tabId].
+ */
+ data class UpdateMediaFullscreenAction(
+ val tabId: String,
+ val fullScreen: Boolean,
+ val elementMetadata: MediaSession.ElementMetadata?,
+ ) : MediaSessionAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the global download state.
+ */
+sealed class DownloadAction : BrowserAction() {
+ /**
+ * Updates the [BrowserState] to track the provided [download] as added.
+ */
+ data class AddDownloadAction(val download: DownloadState) : DownloadAction()
+
+ /**
+ * Updates the [BrowserState] to remove the download with the provided [downloadId].
+ */
+ data class RemoveDownloadAction(val downloadId: String) : DownloadAction()
+
+ /**
+ * Updates the [BrowserState] to remove all downloads.
+ */
+ object RemoveAllDownloadsAction : DownloadAction()
+
+ /**
+ * Updates the provided [download] on the [BrowserState].
+ */
+ data class UpdateDownloadAction(val download: DownloadState) : DownloadAction()
+
+ /**
+ * Mark the download notification of the provided [downloadId] as removed from the status bar.
+ */
+ data class DismissDownloadNotificationAction(val downloadId: String) : DownloadAction()
+
+ /**
+ * Restores the [BrowserState.downloads] state from the storage.
+ */
+ object RestoreDownloadsStateAction : DownloadAction()
+
+ /**
+ * Restores the given [download] from the storage.
+ */
+ data class RestoreDownloadStateAction(val download: DownloadState) : DownloadAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the session state of internet resources to be shared.
+ */
+sealed class ShareInternetResourceAction : BrowserAction() {
+ /**
+ * Starts the sharing process of an Internet resource.
+ */
+ data class AddShareAction(
+ val tabId: String,
+ val internetResource: ShareInternetResourceState,
+ ) : ShareInternetResourceAction()
+
+ /**
+ * Previous share request is considered completed.
+ * File was successfully shared with other apps / user may have aborted the process or the operation
+ * may have failed. In either case the previous share request is considered completed.
+ */
+ data class ConsumeShareAction(
+ val tabId: String,
+ ) : ShareInternetResourceAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating the session state of internet resources to be copied.
+ */
+sealed class CopyInternetResourceAction : BrowserAction() {
+ /**
+ * Starts the copying process of an Internet resource.
+ */
+ data class AddCopyAction(
+ val tabId: String,
+ val internetResource: ShareInternetResourceState,
+ ) : CopyInternetResourceAction()
+
+ /**
+ * Previous copy request is considered completed.
+ * File was successfully copied / user may have aborted the process or the operation
+ * may have failed. In either case the previous copy request is considered completed.
+ */
+ data class ConsumeCopyAction(
+ val tabId: String,
+ ) : CopyInternetResourceAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating [BrowserState.containers]
+ */
+sealed class ContainerAction : BrowserAction() {
+ /**
+ * Updates [BrowserState.containers] to register the given added [container].
+ */
+ data class AddContainerAction(val container: ContainerState) : ContainerAction()
+
+ /**
+ * Updates [BrowserState.containers] to register the given list of [containers].
+ */
+ data class AddContainersAction(val containers: List<ContainerState>) : ContainerAction()
+
+ /**
+ * Removes all state of the removed container from [BrowserState.containers].
+ */
+ data class RemoveContainerAction(val contextId: String) : ContainerAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating [TabSessionState.historyMetadata].
+ */
+sealed class HistoryMetadataAction : BrowserAction() {
+
+ /**
+ * Associates a tab with a history metadata record described by the provided [historyMetadataKey].
+ */
+ data class SetHistoryMetadataKeyAction(
+ val tabId: String,
+ val historyMetadataKey: HistoryMetadataKey,
+ ) : HistoryMetadataAction()
+
+ /**
+ * Removes [searchTerm] (and referrer) from any history metadata associated with tabs.
+ */
+ data class DisbandSearchGroupAction(
+ val searchTerm: String,
+ ) : HistoryMetadataAction()
+}
+
+/**
+ * [BrowserAction] implementations related to updating search engines in [SearchState].
+ */
+sealed class SearchAction : BrowserAction() {
+ /**
+ * Refreshes the list of search engines.
+ */
+ object RefreshSearchEnginesAction : SearchAction()
+
+ /**
+ * Sets the [RegionState] (region of the user).
+ * distribution is a [String] that specifies a set of default search engines if available
+ */
+ data class SetRegionAction(val regionState: RegionState, val distribution: String? = null) : SearchAction()
+
+ /**
+ * Sets the list of search engines and default search engine IDs.
+ */
+ data class SetSearchEnginesAction(
+ val regionSearchEngines: List<SearchEngine>,
+ val customSearchEngines: List<SearchEngine>,
+ val hiddenSearchEngines: List<SearchEngine>,
+ val disabledSearchEngineIds: List<String>,
+ val additionalSearchEngines: List<SearchEngine>,
+ val additionalAvailableSearchEngines: List<SearchEngine>,
+ val userSelectedSearchEngineId: String?,
+ val userSelectedSearchEngineName: String?,
+ val regionDefaultSearchEngineId: String,
+ val regionSearchEnginesOrder: List<String>,
+ ) : SearchAction()
+
+ /**
+ * Updates [BrowserState.search] to add/modify a custom [SearchEngine].
+ */
+ data class UpdateCustomSearchEngineAction(val searchEngine: SearchEngine) : SearchAction()
+
+ /**
+ * Updates [BrowserState.search] to remove a custom [SearchEngine].
+ */
+ data class RemoveCustomSearchEngineAction(val searchEngineId: String) : SearchAction()
+
+ /**
+ * Updates [BrowserState.search] to update [SearchState.userSelectedSearchEngineId] and
+ * [SearchState.userSelectedSearchEngineName].
+ */
+ data class SelectSearchEngineAction(
+ val searchEngineId: String,
+ val searchEngineName: String?,
+ ) : SearchAction()
+
+ /**
+ * Shows a previously hidden, bundled search engine in [SearchState.regionSearchEngines] again
+ * and removes it from [SearchState.hiddenSearchEngines].
+ */
+ data class ShowSearchEngineAction(val searchEngineId: String) : SearchAction()
+
+ /**
+ * Hides a bundled search engine in [SearchState.regionSearchEngines] and adds it to
+ * [SearchState.hiddenSearchEngines] instead.
+ */
+ data class HideSearchEngineAction(val searchEngineId: String) : SearchAction()
+
+ /**
+ * Adds an additional search engine from [SearchState.additionalAvailableSearchEngines] to
+ * [SearchState.additionalSearchEngines].
+ */
+ data class AddAdditionalSearchEngineAction(val searchEngineId: String) : SearchAction()
+
+ /**
+ * Removes and additional search engine from [SearchState.additionalSearchEngines] and adds it
+ * back to [SearchState.additionalAvailableSearchEngines].
+ */
+ data class RemoveAdditionalSearchEngineAction(val searchEngineId: String) : SearchAction()
+
+ /**
+ * Updates [SearchState.disabledSearchEngineIds] list inside [BrowserState.search].
+ */
+ data class UpdateDisabledSearchEngineIdsAction(
+ val searchEngineId: String,
+ val isEnabled: Boolean,
+ ) : SearchAction()
+
+ /**
+ * Restores hidden engines from [SearchState.hiddenSearchEngines] back to [SearchState.regionSearchEngines]
+ */
+ object RestoreHiddenSearchEnginesAction : SearchAction()
+}
+
+/**
+ * [BrowserAction] implementations for updating state needed for debugging. These actions should
+ * be carefully considered before being used.
+ *
+ * Every action **should** be annotated with [DelicateAction] to bring consumers to attention that
+ * this is a delicate action.
+ */
+sealed class DebugAction : BrowserAction() {
+
+ /**
+ * Updates the [TabSessionState.createdAt] timestamp of the tab with the given [tabId].
+ *
+ * @property tabId the ID of the tab to update.
+ * @property createdAt the value to signify when the tab was created.
+ */
+ @DelicateAction
+ data class UpdateCreatedAtAction(val tabId: String, val createdAt: Long) : DebugAction()
+}
+
+/**
+ * [BrowserAction] implementations related to the application lifecycle.
+ */
+sealed class AppLifecycleAction : BrowserAction() {
+
+ /**
+ * The application has received an ON_RESUME event.
+ */
+ object ResumeAction : AppLifecycleAction()
+
+ /**
+ * The application has received an ON_PAUSE event.
+ */
+ object PauseAction : AppLifecycleAction()
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineMiddleware.kt
new file mode 100644
index 0000000000..b0c9e57406
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineMiddleware.kt
@@ -0,0 +1,61 @@
+/* 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.browser.state.engine
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.engine.middleware.CrashMiddleware
+import mozilla.components.browser.state.engine.middleware.CreateEngineSessionMiddleware
+import mozilla.components.browser.state.engine.middleware.EngineDelegateMiddleware
+import mozilla.components.browser.state.engine.middleware.ExtensionsProcessMiddleware
+import mozilla.components.browser.state.engine.middleware.LinkingMiddleware
+import mozilla.components.browser.state.engine.middleware.SuspendMiddleware
+import mozilla.components.browser.state.engine.middleware.TabsRemovedMiddleware
+import mozilla.components.browser.state.engine.middleware.TranslationsMiddleware
+import mozilla.components.browser.state.engine.middleware.TrimMemoryMiddleware
+import mozilla.components.browser.state.engine.middleware.WebExtensionMiddleware
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.lib.state.Middleware
+
+/**
+ * Helper for creating a list of [Middleware] instances for supporting all [EngineAction]s.
+ */
+object EngineMiddleware {
+ /**
+ * Creates a list of [Middleware] to be installed on a [BrowserStore] in order to support all
+ * [EngineAction]s.
+ *
+ * @param trimMemoryAutomatically Whether a middleware should listen to LowMemoryAction and
+ * automatically trim memory by suspending tabs.
+ */
+ fun create(
+ engine: Engine,
+ scope: CoroutineScope = MainScope(),
+ trimMemoryAutomatically: Boolean = true,
+ ): List<Middleware<BrowserState, BrowserAction>> {
+ return listOf(
+ EngineDelegateMiddleware(scope),
+ CreateEngineSessionMiddleware(
+ engine,
+ scope,
+ ),
+ LinkingMiddleware(scope),
+ TabsRemovedMiddleware(scope),
+ SuspendMiddleware(scope),
+ WebExtensionMiddleware(),
+ CrashMiddleware(),
+ ExtensionsProcessMiddleware(engine),
+ TranslationsMiddleware(engine, scope),
+ ) + if (trimMemoryAutomatically) {
+ listOf(TrimMemoryMiddleware())
+ } else {
+ emptyList()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt
new file mode 100644
index 0000000000..1668b5b57e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt
@@ -0,0 +1,491 @@
+/* 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.browser.state.engine
+
+import android.content.Intent
+import android.os.Environment
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CookieBannerAction
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.action.ReaderAction
+import mozilla.components.browser.state.action.TrackingProtectionAction
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.AppIntentState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.LoadRequestState
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED
+import mozilla.components.browser.state.state.content.FindResultState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.history.HistoryItem
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.media.RecordingDevice
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.concept.fetch.Response
+import mozilla.components.lib.state.Store
+
+/**
+ * [EngineSession.Observer] implementation responsible to update the state of a [Session] from the events coming out of
+ * an [EngineSession].
+ */
+@Suppress("TooManyFunctions", "LargeClass")
+internal class EngineObserver(
+ private val tabId: String,
+ private val store: Store<BrowserState, BrowserAction>,
+) : EngineSession.Observer {
+
+ override fun onScrollChange(scrollX: Int, scrollY: Int) {
+ store.dispatch(ReaderAction.UpdateReaderScrollYAction(tabId, scrollY))
+ }
+
+ override fun onNavigateBack() {
+ store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
+ }
+
+ override fun onNavigateForward() {
+ store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
+ }
+
+ override fun onGotoHistoryIndex() {
+ store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
+ }
+
+ override fun onLoadData() {
+ store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
+ }
+
+ override fun onLoadUrl() {
+ if (store.state.findTabOrCustomTab(tabId)?.content?.isSearch == true) {
+ store.dispatch(ContentAction.UpdateIsSearchAction(tabId, false))
+ } else {
+ store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
+ }
+ }
+
+ override fun onFirstContentfulPaint() {
+ store.dispatch(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, true))
+ }
+
+ override fun onPaintStatusReset() {
+ store.dispatch(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, false))
+ }
+
+ override fun onLocationChange(url: String, hasUserGesture: Boolean) {
+ store.dispatch(ContentAction.UpdateUrlAction(tabId, url, hasUserGesture))
+ }
+
+ @Suppress("DEPRECATION") // Session observable is deprecated
+ override fun onLoadRequest(
+ url: String,
+ triggeredByRedirect: Boolean,
+ triggeredByWebContent: Boolean,
+ ) {
+ if (triggeredByWebContent) {
+ store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
+ }
+
+ val loadRequest = LoadRequestState(url, triggeredByRedirect, triggeredByWebContent)
+ store.dispatch(ContentAction.UpdateLoadRequestAction(tabId, loadRequest))
+ }
+
+ override fun onLaunchIntentRequest(url: String, appIntent: Intent?) {
+ store.dispatch(ContentAction.UpdateAppIntentAction(tabId, AppIntentState(url, appIntent)))
+ }
+
+ override fun onTitleChange(title: String) {
+ store.dispatch(ContentAction.UpdateTitleAction(tabId, title))
+ }
+
+ override fun onPreviewImageChange(previewImageUrl: String) {
+ store.dispatch(ContentAction.UpdatePreviewImageAction(tabId, previewImageUrl))
+ }
+
+ override fun onProgress(progress: Int) {
+ store.dispatch(ContentAction.UpdateProgressAction(tabId, progress))
+ }
+
+ override fun onLoadingStateChange(loading: Boolean) {
+ store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, loading))
+
+ if (loading) {
+ store.dispatch(ContentAction.ClearFindResultsAction(tabId))
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, false))
+ store.dispatch(TrackingProtectionAction.ClearTrackersAction(tabId))
+ }
+ }
+
+ override fun onNavigationStateChange(canGoBack: Boolean?, canGoForward: Boolean?) {
+ canGoBack?.let {
+ store.dispatch(ContentAction.UpdateBackNavigationStateAction(tabId, canGoBack))
+ }
+ canGoForward?.let {
+ store.dispatch(ContentAction.UpdateForwardNavigationStateAction(tabId, canGoForward))
+ }
+ }
+
+ override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
+ store.dispatch(
+ ContentAction.UpdateSecurityInfoAction(
+ tabId,
+ SecurityInfoState(secure, host ?: "", issuer ?: ""),
+ ),
+ )
+ }
+
+ override fun onTrackerBlocked(tracker: Tracker) {
+ store.dispatch(TrackingProtectionAction.TrackerBlockedAction(tabId, tracker))
+ }
+
+ override fun onTrackerLoaded(tracker: Tracker) {
+ store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId, tracker))
+ }
+
+ override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) {
+ store.dispatch(TrackingProtectionAction.ToggleExclusionListAction(tabId, excluded))
+ }
+
+ override fun onTrackerBlockingEnabledChange(enabled: Boolean) {
+ store.dispatch(TrackingProtectionAction.ToggleAction(tabId, enabled))
+ }
+
+ override fun onCookieBannerChange(status: EngineSession.CookieBannerHandlingStatus) {
+ store.dispatch(CookieBannerAction.UpdateStatusAction(tabId, status))
+ }
+
+ override fun onProductUrlChange(isProductUrl: Boolean) {
+ store.dispatch(ContentAction.UpdateProductUrlStateAction(tabId, isProductUrl))
+ }
+
+ override fun onLongPress(hitResult: HitResult) {
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(tabId, hitResult),
+ )
+ }
+
+ override fun onFind(text: String) {
+ store.dispatch(ContentAction.ClearFindResultsAction(tabId))
+ }
+
+ override fun onFindResult(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean) {
+ store.dispatch(
+ ContentAction.AddFindResultAction(
+ tabId,
+ FindResultState(
+ activeMatchOrdinal,
+ numberOfMatches,
+ isDoneCounting,
+ ),
+ ),
+ )
+ }
+
+ override fun onExternalResource(
+ url: String,
+ fileName: String?,
+ contentLength: Long?,
+ contentType: String?,
+ cookie: String?,
+ userAgent: String?,
+ isPrivate: Boolean,
+ skipConfirmation: Boolean,
+ openInApp: Boolean,
+ response: Response?,
+ ) {
+ // We want to avoid negative contentLength values
+ // For more info see https://bugzilla.mozilla.org/show_bug.cgi?id=1632594
+ val fileSize = if (contentLength != null && contentLength < 0) null else contentLength
+ val download = DownloadState(
+ url,
+ fileName,
+ contentType,
+ fileSize,
+ 0,
+ INITIATED,
+ userAgent,
+ Environment.DIRECTORY_DOWNLOADS,
+ private = isPrivate,
+ skipConfirmation = skipConfirmation,
+ openInApp = openInApp,
+ response = response,
+ )
+
+ store.dispatch(
+ ContentAction.UpdateDownloadAction(
+ tabId,
+ download,
+ ),
+ )
+ }
+
+ override fun onDesktopModeChange(enabled: Boolean) {
+ store.dispatch(
+ ContentAction.UpdateDesktopModeAction(
+ tabId,
+ enabled,
+ ),
+ )
+ }
+
+ override fun onFullScreenChange(enabled: Boolean) {
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ tabId,
+ enabled,
+ ),
+ )
+ }
+
+ override fun onMetaViewportFitChanged(layoutInDisplayCutoutMode: Int) {
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ tabId,
+ layoutInDisplayCutoutMode,
+ ),
+ )
+ }
+
+ override fun onContentPermissionRequest(permissionRequest: PermissionRequest) {
+ store.dispatch(
+ ContentAction.UpdatePermissionsRequest(
+ tabId,
+ permissionRequest,
+ ),
+ )
+ }
+
+ override fun onCancelContentPermissionRequest(permissionRequest: PermissionRequest) {
+ store.dispatch(
+ ContentAction.ConsumePermissionsRequest(
+ tabId,
+ permissionRequest,
+ ),
+ )
+ }
+
+ override fun onAppPermissionRequest(permissionRequest: PermissionRequest) {
+ store.dispatch(
+ ContentAction.UpdateAppPermissionsRequest(
+ tabId,
+ permissionRequest,
+ ),
+ )
+ }
+
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ store.dispatch(
+ ContentAction.UpdatePromptRequestAction(
+ tabId,
+ promptRequest,
+ ),
+ )
+ }
+
+ override fun onPromptDismissed(promptRequest: PromptRequest) {
+ store.dispatch(
+ ContentAction.ConsumePromptRequestAction(tabId, promptRequest),
+ )
+ }
+
+ override fun onPromptUpdate(previousPromptRequestUid: String, promptRequest: PromptRequest) {
+ store.dispatch(
+ ContentAction.ReplacePromptRequestAction(tabId, previousPromptRequestUid, promptRequest),
+ )
+ }
+
+ override fun onRepostPromptCancelled() {
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, true))
+ }
+
+ override fun onBeforeUnloadPromptDenied() {
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, true))
+ }
+
+ override fun onWindowRequest(windowRequest: WindowRequest) {
+ store.dispatch(
+ ContentAction.UpdateWindowRequestAction(
+ tabId,
+ windowRequest,
+ ),
+ )
+ }
+
+ override fun onShowDynamicToolbar() {
+ store.dispatch(
+ ContentAction.UpdateExpandedToolbarStateAction(tabId, true),
+ )
+ }
+
+ override fun onMediaActivated(mediaSessionController: MediaSession.Controller) {
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ tabId,
+ mediaSessionController,
+ ),
+ )
+ }
+
+ override fun onMediaDeactivated() {
+ store.dispatch(MediaSessionAction.DeactivatedMediaSessionAction(tabId))
+ }
+
+ override fun onMediaMetadataChanged(metadata: MediaSession.Metadata) {
+ store.dispatch(MediaSessionAction.UpdateMediaMetadataAction(tabId, metadata))
+ }
+
+ override fun onMediaPlaybackStateChanged(playbackState: MediaSession.PlaybackState) {
+ store.dispatch(
+ MediaSessionAction.UpdateMediaPlaybackStateAction(
+ tabId,
+ playbackState,
+ ),
+ )
+ }
+
+ override fun onMediaFeatureChanged(features: MediaSession.Feature) {
+ store.dispatch(
+ MediaSessionAction.UpdateMediaFeatureAction(
+ tabId,
+ features,
+ ),
+ )
+ }
+
+ override fun onMediaPositionStateChanged(positionState: MediaSession.PositionState) {
+ store.dispatch(
+ MediaSessionAction.UpdateMediaPositionStateAction(
+ tabId,
+ positionState,
+ ),
+ )
+ }
+
+ override fun onMediaMuteChanged(muted: Boolean) {
+ store.dispatch(
+ MediaSessionAction.UpdateMediaMutedAction(
+ tabId,
+ muted,
+ ),
+ )
+ }
+
+ override fun onMediaFullscreenChanged(
+ fullscreen: Boolean,
+ elementMetadata: MediaSession.ElementMetadata?,
+ ) {
+ store.dispatch(
+ MediaSessionAction.UpdateMediaFullscreenAction(
+ tabId,
+ fullscreen,
+ elementMetadata,
+ ),
+ )
+ }
+
+ override fun onWebAppManifestLoaded(manifest: WebAppManifest) {
+ store.dispatch(ContentAction.UpdateWebAppManifestAction(tabId, manifest))
+ }
+
+ override fun onCrash() {
+ store.dispatch(
+ CrashAction.SessionCrashedAction(
+ tabId,
+ ),
+ )
+ }
+
+ override fun onProcessKilled() {
+ store.dispatch(
+ EngineAction.KillEngineSessionAction(
+ tabId,
+ ),
+ )
+ }
+
+ override fun onStateUpdated(state: EngineSessionState) {
+ store.dispatch(
+ EngineAction.UpdateEngineSessionStateAction(
+ tabId,
+ state,
+ ),
+ )
+ }
+
+ override fun onRecordingStateChanged(devices: List<RecordingDevice>) {
+ store.dispatch(
+ ContentAction.SetRecordingDevices(
+ tabId,
+ devices,
+ ),
+ )
+ }
+
+ override fun onHistoryStateChanged(historyList: List<HistoryItem>, currentIndex: Int) {
+ store.dispatch(
+ ContentAction.UpdateHistoryStateAction(
+ tabId,
+ historyList,
+ currentIndex,
+ ),
+ )
+ }
+
+ override fun onSaveToPdfException(throwable: Throwable) {
+ store.dispatch(EngineAction.SaveToPdfExceptionAction(tabId, throwable))
+ }
+
+ override fun onPrintFinish() {
+ store.dispatch(EngineAction.PrintContentCompletedAction(tabId))
+ }
+
+ override fun onPrintException(isPrint: Boolean, throwable: Throwable) {
+ store.dispatch(EngineAction.PrintContentExceptionAction(tabId, isPrint, throwable))
+ }
+
+ override fun onSaveToPdfComplete() {
+ store.dispatch(EngineAction.SaveToPdfCompleteAction(tabId))
+ }
+
+ override fun onCheckForFormData(containsFormData: Boolean) {
+ store.dispatch(ContentAction.UpdateHasFormDataAction(tabId, containsFormData))
+ }
+
+ override fun onCheckForFormDataException(throwable: Throwable) {
+ store.dispatch(ContentAction.CheckForFormDataExceptionAction(tabId, throwable))
+ }
+
+ override fun onTranslateExpected() {
+ store.dispatch(TranslationsAction.TranslateExpectedAction(tabId))
+ }
+
+ override fun onTranslateOffer() {
+ store.dispatch(TranslationsAction.TranslateOfferAction(tabId = tabId, isOfferTranslate = true))
+ }
+
+ override fun onTranslateStateChange(state: TranslationEngineState) {
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId, state))
+ }
+
+ override fun onTranslateComplete(operation: TranslationOperation) {
+ store.dispatch(TranslationsAction.TranslateSuccessAction(tabId, operation))
+ }
+
+ override fun onTranslateException(operation: TranslationOperation, translationError: TranslationError) {
+ store.dispatch(TranslationsAction.TranslateExceptionAction(tabId, operation, translationError))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CrashMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CrashMiddleware.kt
new file mode 100644
index 0000000000..ff13fab04a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CrashMiddleware.kt
@@ -0,0 +1,47 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] responsible for recovering crashed [EngineSession] instances.
+ */
+internal class CrashMiddleware : Middleware<BrowserState, BrowserAction> {
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ next(action)
+
+ // We need to do this after updating the crashed flag in the reducer
+ // because we want observers to see the crash state change before the
+ // engine session is cleared. This way the observers can react to
+ // crashes and will not request a new engine session until the user
+ // explicitly asked to restore the session.
+ if (action is CrashAction.SessionCrashedAction) {
+ onCrash(context, action)
+ }
+ }
+
+ private fun onCrash(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ action: CrashAction.SessionCrashedAction,
+ ) {
+ // We suspend the crashed session here. After that the reducer will mark it as "crashed".
+ // That will prevent it from getting recreated until explicitly handling the crash by
+ // restoring.
+ context.dispatch(
+ EngineAction.SuspendEngineSessionAction(action.tabId),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddleware.kt
new file mode 100644
index 0000000000..6f807eff16
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddleware.kt
@@ -0,0 +1,134 @@
+/* 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.browser.state.engine.middleware
+
+import androidx.annotation.MainThread
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * [Middleware] responsible for creating [EngineSession] instances whenever an [EngineAction.CreateEngineSessionAction]
+ * is getting dispatched.
+ */
+internal class CreateEngineSessionMiddleware(
+ private val engine: Engine,
+ private val scope: CoroutineScope,
+) : Middleware<BrowserState, BrowserAction> {
+ private val logger = Logger("CreateEngineSessionMiddleware")
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ if (action is EngineAction.CreateEngineSessionAction) {
+ val engineState = context.state.findTabOrCustomTab(action.tabId)?.engineState
+ if (engineState?.initializing == false && engineState.engineSession == null && !engineState.crashed) {
+ context.dispatch(EngineAction.UpdateEngineSessionInitializingAction(action.tabId, true))
+ createEngineSession(context.store, action)
+ } else {
+ // Initialization is in progress by a pending CreateEngineSessionAction. Let's
+ // schedule dispatching the follow-up action when the engine session is ready.
+ // We launch this on main to guarantee this happens after the engine session
+ // is created which has been launched on main already at this point.
+ action.followupAction?.let {
+ scope.launch {
+ context.store.dispatch(it)
+ }
+ }
+ }
+ } else {
+ next(action)
+ }
+ }
+
+ private fun createEngineSession(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.CreateEngineSessionAction,
+ ) {
+ logger.debug("Request to create engine session for tab ${action.tabId}")
+
+ scope.launch {
+ // We only need to ask for an EngineSession here. If needed this method will internally
+ // create one and dispatch a LinkEngineSessionAction to add it to BrowserState.
+ getOrCreateEngineSession(
+ engine,
+ logger,
+ store,
+ action.tabId,
+ )
+
+ action.followupAction?.let {
+ store.dispatch(it)
+ }
+ }
+ }
+}
+
+@MainThread
+@Suppress("ReturnCount")
+private fun getOrCreateEngineSession(
+ engine: Engine,
+ logger: Logger,
+ store: Store<BrowserState, BrowserAction>,
+ tabId: String,
+): EngineSession? {
+ val tab = store.state.findTabOrCustomTab(tabId)
+ if (tab == null) {
+ logger.warn("Requested engine session for tab. But tab does not exist. ($tabId)")
+ return null
+ }
+
+ if (tab.engineState.crashed) {
+ logger.warn("Not creating engine session, since tab is crashed. Waiting for restore.")
+ return null
+ }
+
+ tab.engineState.engineSession?.let {
+ logger.debug("Engine session already exists for tab $tabId")
+ return it
+ }
+
+ return createEngineSession(engine, logger, store, tab)
+}
+
+@MainThread
+private fun createEngineSession(
+ engine: Engine,
+ logger: Logger,
+ store: Store<BrowserState, BrowserAction>,
+ tab: SessionState,
+): EngineSession {
+ val engineSession = engine.createSession(tab.content.private, tab.contextId)
+ logger.debug("Created engine session for tab ${tab.id}")
+
+ val engineSessionState = tab.engineState.engineSessionState
+ val skipLoading = if (engineSessionState != null) {
+ engineSession.restoreState(engineSessionState)
+ } else {
+ false
+ }
+
+ store.dispatch(
+ EngineAction.LinkEngineSessionAction(
+ tab.id,
+ engineSession,
+ skipLoading = skipLoading,
+ ),
+ )
+
+ return engineSession
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddleware.kt
new file mode 100644
index 0000000000..1b7744ff41
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddleware.kt
@@ -0,0 +1,224 @@
+/* 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.browser.state.engine.middleware
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.ActionWithTab
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.action.lookupTabIn
+import mozilla.components.browser.state.action.toBrowserAction
+import mozilla.components.browser.state.selector.allTabs
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+
+/**
+ * [Middleware] responsible for delegating calls to the appropriate [EngineSession] instance for
+ * actions like [EngineAction.LoadUrlAction].
+ */
+internal class EngineDelegateMiddleware(
+ private val scope: CoroutineScope,
+) : Middleware<BrowserState, BrowserAction> {
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is EngineAction.LoadUrlAction -> loadUrl(context.store, action)
+ is EngineAction.LoadDataAction -> loadData(context.store, action)
+ is EngineAction.ReloadAction -> reload(context.store, action)
+ is EngineAction.GoBackAction -> goBack(context.store, action)
+ is EngineAction.GoForwardAction -> goForward(context.store, action)
+ is EngineAction.GoToHistoryIndexAction -> goToHistoryIndex(context.store, action)
+ is EngineAction.ToggleDesktopModeAction -> toggleDesktopMode(context.store, action)
+ is EngineAction.ExitFullScreenModeAction -> exitFullScreen(context.store, action)
+ is EngineAction.SaveToPdfAction -> saveToPdf(context.store, action)
+ is EngineAction.PrintContentAction -> printContent(context.store, action)
+ is EngineAction.ClearDataAction -> clearData(context.store, action)
+ is EngineAction.PurgeHistoryAction -> purgeHistory(context.state)
+ is TranslationsAction.TranslateAction -> {
+ next(action)
+ translate(context.store, action)
+ }
+ is TranslationsAction.TranslateRestoreAction -> {
+ next(action)
+ translateRestoreOriginal(context.store, action)
+ }
+ else -> next(action)
+ }
+ }
+
+ private fun loadUrl(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.LoadUrlAction,
+ ) = scope.launch {
+ val tab = store.state.findTabOrCustomTab(action.tabId) ?: return@launch
+ val engineSession = tab.engineState.engineSession
+
+ if (engineSession == null && tab.content.url == action.url) {
+ // This tab does not have an engine session and we are asked to load the URL this
+ // session is already pointing to. Creating an EngineSession will do exactly
+ // that in the linking step. So let's do that. Otherwise we would load the URL
+ // twice.
+ store.dispatch(EngineAction.CreateEngineSessionAction(action.tabId))
+ return@launch
+ }
+
+ val parentEngineSession = if (tab is TabSessionState) {
+ tab.parentId?.let { store.state.findTabOrCustomTab(it)?.engineState?.engineSession }
+ } else {
+ null
+ }
+
+ getEngineSessionOrDispatch(store, action)?.loadUrl(
+ url = action.url,
+ parent = parentEngineSession,
+ flags = action.flags,
+ additionalHeaders = action.additionalHeaders,
+ )
+ }
+
+ private fun loadData(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.LoadDataAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.loadData(action.data, action.mimeType, action.encoding)
+ }
+
+ private fun reload(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.ReloadAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.reload(action.flags)
+ }
+
+ private fun goBack(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.GoBackAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.goBack(action.userInteraction)
+ }
+
+ private fun goForward(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.GoForwardAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.goForward(action.userInteraction)
+ }
+
+ private fun goToHistoryIndex(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.GoToHistoryIndexAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.goToHistoryIndex(action.index)
+ }
+
+ private fun toggleDesktopMode(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.ToggleDesktopModeAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.toggleDesktopMode(action.enable, reload = true)
+ }
+
+ private fun exitFullScreen(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.ExitFullScreenModeAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.exitFullScreenMode()
+ }
+
+ private fun saveToPdf(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.SaveToPdfAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.requestPdfToDownload()
+ }
+
+ private fun printContent(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.PrintContentAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.requestPrintContent()
+ }
+
+ private fun translate(
+ store: Store<BrowserState, BrowserAction>,
+ action: TranslationsAction.TranslateAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.requestTranslate(action.fromLanguage, action.toLanguage, action.options)
+ }
+
+ private fun translateRestoreOriginal(
+ store: Store<BrowserState, BrowserAction>,
+ action: TranslationsAction.TranslateRestoreAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.requestTranslationRestore()
+ }
+
+ private fun clearData(
+ store: Store<BrowserState, BrowserAction>,
+ action: EngineAction.ClearDataAction,
+ ) = scope.launch {
+ getEngineSessionOrDispatch(store, action)
+ ?.clearData(action.data)
+ }
+
+ private fun purgeHistory(
+ state: BrowserState,
+ ) = scope.launch {
+ state.allTabs
+ .mapNotNull { tab -> tab.engineState.engineSession }
+ .forEach { engineSession -> engineSession.purgeHistory() }
+ }
+}
+
+/**
+ * Returns the [EngineSession] of the tab targeted by the provided action. If the tab
+ * does not have an engine session yet a new one will be created by dispatching a
+ * [EngineAction.CreateEngineSessionAction]. The provided [action] will be dispatched
+ * as a follow up once the [EngineSession] has been created and initialized.
+ *
+ * @param store a reference to the browser store.
+ * @param action the action to dispatch in case the engine session still has to be created.
+ */
+private fun getEngineSessionOrDispatch(
+ store: Store<BrowserState, BrowserAction>,
+ action: ActionWithTab,
+): EngineSession? {
+ val tab = action.lookupTabIn(store) ?: return null
+
+ val engineSession = tab.engineState.engineSession
+
+ return if (engineSession == null) {
+ store.dispatch(
+ EngineAction.CreateEngineSessionAction(
+ action.tabId,
+ followupAction = action.toBrowserAction(),
+ ),
+ )
+ null
+ } else {
+ engineSession
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddleware.kt
new file mode 100644
index 0000000000..b232acaed5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddleware.kt
@@ -0,0 +1,45 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ExtensionsProcessAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.concept.engine.Engine
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] implementation responsible for enabling and disabling the extensions process (spawning).
+ *
+ * @property engine An [Engine] instance used for handling extension process spawning.
+ */
+internal class ExtensionsProcessMiddleware(
+ private val engine: Engine,
+) : Middleware<BrowserState, BrowserAction> {
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ // Pre process actions
+
+ next(action)
+
+ // Post process actions
+ when (action) {
+ is ExtensionsProcessAction.EnabledAction -> {
+ engine.enableExtensionProcessSpawning()
+ }
+ is ExtensionsProcessAction.DisabledAction -> {
+ engine.disableExtensionProcessSpawning()
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/LinkingMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/LinkingMiddleware.kt
new file mode 100644
index 0000000000..c6f1d01d39
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/LinkingMiddleware.kt
@@ -0,0 +1,137 @@
+/* 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.browser.state.engine.middleware
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.engine.EngineObserver
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.ktx.kotlin.isExtensionUrl
+import java.lang.IllegalArgumentException
+
+/**
+ * [Middleware] that handles side-effects of linking a session to an engine session.
+ */
+internal class LinkingMiddleware(
+ private val scope: CoroutineScope,
+) : Middleware<BrowserState, BrowserAction> {
+
+ @Suppress("ComplexMethod")
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ var engineObserver: Pair<String, EngineObserver>? = null
+ when (action) {
+ is TabListAction.AddTabAction -> {
+ if (action.tab.engineState.engineSession != null && action.tab.engineState.engineObserver == null) {
+ engineObserver = link(context, action.tab.engineState.engineSession, action.tab)
+ }
+ }
+ is TabListAction.AddMultipleTabsAction -> {
+ if (action.tabs.any { it.engineState.engineSession != null }) {
+ throw IllegalArgumentException("AddMultipleTabsAction does not support tabs with engine sessions")
+ }
+ }
+ is EngineAction.UnlinkEngineSessionAction -> {
+ unlink(context, action)
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+
+ when (action) {
+ is EngineAction.LinkEngineSessionAction -> {
+ context.state.findTabOrCustomTab(action.tabId)?.let { tab ->
+ engineObserver = link(context, action.engineSession, tab, action.skipLoading)
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ engineObserver?.let {
+ context.dispatch(EngineAction.UpdateEngineSessionObserverAction(it.first, it.second))
+ context.dispatch(EngineAction.UpdateEngineSessionInitializingAction(it.first, false))
+ }
+ }
+
+ private fun link(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ engineSession: EngineSession,
+ tab: SessionState,
+ skipLoading: Boolean = true,
+ ): Pair<String, EngineObserver> {
+ val observer = EngineObserver(tab.id, context.store)
+ engineSession.register(observer)
+
+ if (skipLoading) {
+ return Pair(tab.id, observer)
+ }
+
+ if (tab.content.url.isExtensionUrl()) {
+ // The parent tab/session is used as a referrer which is not accurate
+ // for extension pages. The extension page is not loaded by the parent
+ // tab, but opened by an extension e.g. via browser.tabs.update.
+ performLoadOnMainThread(engineSession, tab.content.url, loadFlags = tab.engineState.initialLoadFlags)
+ } else {
+ val parentEngineSession = if (tab is TabSessionState) {
+ tab.parentId?.let { context.state.findTabOrCustomTab(it)?.engineState?.engineSession }
+ } else {
+ null
+ }
+
+ performLoadOnMainThread(
+ engineSession = engineSession,
+ url = tab.content.url,
+ parent = parentEngineSession,
+ loadFlags = tab.engineState.initialLoadFlags,
+ additionalHeaders = tab.engineState.initialAdditionalHeaders,
+ )
+ }
+
+ return Pair(tab.id, observer)
+ }
+
+ private fun performLoadOnMainThread(
+ engineSession: EngineSession,
+ url: String,
+ parent: EngineSession? = null,
+ loadFlags: EngineSession.LoadUrlFlags,
+ additionalHeaders: Map<String, String>? = null,
+ ) = scope.launch {
+ engineSession.loadUrl(
+ url = url,
+ parent = parent,
+ flags = loadFlags,
+ additionalHeaders = additionalHeaders,
+ )
+ }
+
+ private fun unlink(
+ store: MiddlewareContext<BrowserState, BrowserAction>,
+ action: EngineAction.UnlinkEngineSessionAction,
+ ) {
+ val tab = store.state.findTabOrCustomTab(action.tabId) ?: return
+
+ tab.engineState.engineObserver?.let {
+ tab.engineState.engineSession?.unregister(it)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt
new file mode 100644
index 0000000000..5e84962f47
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt
@@ -0,0 +1,145 @@
+/* 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.browser.state.engine.middleware
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.AppLifecycleAction
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.SessionPriority.DEFAULT
+import mozilla.components.concept.engine.EngineSession.SessionPriority.HIGH
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.coroutines.Dispatchers as MozillaDispatchers
+
+/**
+ * [Middleware] implementation responsible for updating the priority of the selected [EngineSession]
+ * to [HIGH] and the rest to [DEFAULT].
+ *
+ * @property updatePriorityAfterMillis Update priority to default after timeout.
+ */
+class SessionPrioritizationMiddleware(
+ // Allow a tab to stay high priority for 3 minutes, an estimate for how long a user may take to return to a tab
+ private val updatePriorityAfterMillis: Long = 180000,
+ private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main),
+ private val waitScope: CoroutineScope = CoroutineScope(MozillaDispatchers.Cached),
+) : Middleware<BrowserState, BrowserAction> {
+ private val logger = Logger("SessionPrioritizationMiddleware")
+ private var updatePriorityToDefaultJobs = mutableMapOf<String, Job>()
+
+ @VisibleForTesting
+ internal var previousHighestPriorityTabId = ""
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is EngineAction.UnlinkEngineSessionAction -> {
+ val activeTab = context.state.findTab(action.tabId)
+ activeTab?.engineState?.engineSession?.updateSessionPriority(DEFAULT)
+ logger.info("Update the tab ${activeTab?.id} priority to ${DEFAULT.name}")
+ }
+ is ContentAction.UpdateHasFormDataAction -> {
+ val tab = context.state.findTab(action.tabId)
+ if (action.containsFormData) {
+ tab?.engineState?.engineSession?.updateSessionPriority(HIGH)
+ logger.info("Update the tab ${tab?.id} priority to ${HIGH.name}")
+ tab?.let {
+ updatePriorityToDefault(context, it.id, updatePriorityAfterMillis)
+ }
+ } else {
+ tab?.engineState?.engineSession?.updateSessionPriority(DEFAULT)
+ logger.info("Update the tab ${tab?.id} priority to ${DEFAULT.name}")
+ }
+ }
+ is ContentAction.UpdatePriorityToDefaultAfterTimeoutAction -> {
+ // remove finished job from map
+ val tab = context.state.findTab(action.tabId)
+ tab?.engineState?.engineSession?.updateSessionPriority(DEFAULT)
+ logger.info("Update the tab ${tab?.id} priority back to ${DEFAULT.name}")
+ updatePriorityToDefaultJobs.remove(action.tabId)
+ return // Do not let the action continue through to the reducer
+ }
+ is AppLifecycleAction.PauseAction -> {
+ // Check for form data for the selected tab when the app is backgrounded.
+ mainScope.launch {
+ context.state.selectedTab?.engineState?.engineSession?.checkForFormData()
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+
+ when (action) {
+ is TabListAction,
+ is EngineAction.LinkEngineSessionAction,
+ -> {
+ // if it exists in the map of high priority tabs to be cleared, cancel the job and remove it
+ val state = context.state
+ updatePriorityToDefaultJobs[state.selectedTabId]?.cancel()
+ updatePriorityToDefaultJobs.remove(state.selectedTabId)
+
+ if (previousHighestPriorityTabId != state.selectedTabId) {
+ updatePriorityIfNeeded(state)
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+
+ private fun updatePriorityIfNeeded(state: BrowserState) = mainScope.launch {
+ val currentSelectedTab = state.selectedTabId?.let { state.findTab(it) }
+ val previousSelectedTab = state.findTab(previousHighestPriorityTabId)
+ val currentEngineSession: EngineSession? = currentSelectedTab?.engineState?.engineSession
+
+ // We need to make sure we alter the previousHighestPriorityTabId, after the session is linked.
+ // So we update the priority on the engine session, as we could get actions where the tab
+ // is selected but not linked yet, causing out sync issues,
+ // when previousHighestPriorityTabId didn't call updateSessionPriority()
+ if (currentEngineSession != null) {
+ mainScope.launch {
+ // check for existing form data here and if there is, set tab to DEFAULT
+ previousSelectedTab?.engineState?.engineSession?.checkForFormData()
+ }
+
+ currentEngineSession.updateSessionPriority(HIGH)
+ logger.info("Update the currentSelectedTab ${currentSelectedTab.id} priority to ${HIGH.name}")
+ previousHighestPriorityTabId = currentSelectedTab.id
+ }
+ }
+
+ private fun updatePriorityToDefault(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ updatePriorityAfterMillis: Long,
+ ) {
+ // store and launch the new job related to the tabId
+ var updateJob: Job = waitScope.launch {
+ delay(updatePriorityAfterMillis)
+ context.store.dispatch(ContentAction.UpdatePriorityToDefaultAfterTimeoutAction(tabId))
+ }
+ updatePriorityToDefaultJobs[tabId] = updateJob
+ logger.info("Tab $tabId will return to ${DEFAULT.name} priority after $updatePriorityAfterMillis ms")
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SuspendMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SuspendMiddleware.kt
new file mode 100644
index 0000000000..f302d1179a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SuspendMiddleware.kt
@@ -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/. */
+
+package mozilla.components.browser.state.engine.middleware
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] implementation responsible for suspending an [EngineSession].
+ *
+ * Suspending an [EngineSession] means that we will take the last [EngineSessionState], attach that
+ * to [EngineState] and then clear the [EngineSession] reference and close it. The next time we
+ * need an [EngineSession] for this tab we will create a new instance and restore the attached
+ * [EngineSessionState].
+ */
+internal class SuspendMiddleware(
+ private val scope: CoroutineScope,
+) : Middleware<BrowserState, BrowserAction> {
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is EngineAction.SuspendEngineSessionAction -> suspend(context, action.tabId)
+ is EngineAction.KillEngineSessionAction -> suspend(context, action.tabId)
+ else -> next(action)
+ }
+ }
+
+ private fun suspend(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ sessionId: String,
+ ) {
+ val tab = context.state.findTabOrCustomTab(sessionId) ?: return
+
+ // First we unlink (which clearsEngineSession and state)
+ context.dispatch(
+ EngineAction.UnlinkEngineSessionAction(
+ tab.id,
+ ),
+ )
+
+ // Now we can close the unlinked EngineSession (on the main thread).
+ scope.launch {
+ tab.engineState.engineSession?.close()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddleware.kt
new file mode 100644
index 0000000000..b482e9bff6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddleware.kt
@@ -0,0 +1,75 @@
+/* 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.browser.state.engine.middleware
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.normalTabs
+import mozilla.components.browser.state.selector.privateTabs
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] responsible for closing and unlinking [EngineSession] instances whenever tabs get
+ * removed.
+ */
+internal class TabsRemovedMiddleware(
+ private val scope: CoroutineScope,
+) : Middleware<BrowserState, BrowserAction> {
+ @Suppress("ComplexMethod")
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is TabListAction.RemoveAllNormalTabsAction -> onTabsRemoved(context, context.state.normalTabs)
+ is TabListAction.RemoveAllPrivateTabsAction -> onTabsRemoved(context, context.state.privateTabs)
+ is TabListAction.RemoveAllTabsAction -> onTabsRemoved(context, context.state.tabs)
+ is TabListAction.RemoveTabAction -> context.state.findTab(action.tabId)?.let {
+ onTabsRemoved(context, listOf(it))
+ }
+ is TabListAction.RemoveTabsAction -> action.tabIds.mapNotNull { context.state.findTab(it) }.let {
+ onTabsRemoved(context, it)
+ }
+ is CustomTabListAction.RemoveAllCustomTabsAction -> onTabsRemoved(context, context.state.customTabs)
+ is CustomTabListAction.RemoveCustomTabAction -> context.state.findCustomTab(action.tabId)?.let {
+ onTabsRemoved(context, listOf(it))
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+ }
+
+ private fun onTabsRemoved(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabs: List<SessionState>,
+ ) {
+ tabs.forEach { tab ->
+ if (tab.engineState.engineSession != null) {
+ context.dispatch(
+ EngineAction.UnlinkEngineSessionAction(
+ tab.id,
+ ),
+ )
+ scope.launch {
+ tab.engineState.engineSession?.close()
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt
new file mode 100644
index 0000000000..81e3b8b48b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt
@@ -0,0 +1,845 @@
+/* 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.browser.state.engine.middleware
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.translate.Language
+import mozilla.components.concept.engine.translate.LanguageSetting
+import mozilla.components.concept.engine.translate.TranslationDownloadSize
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettingOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettings
+import mozilla.components.concept.engine.translate.findLanguage
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.base.log.logger.Logger
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+/**
+ * This middleware is for use with managing any states or resources required for translating a
+ * webpage.
+ */
+class TranslationsMiddleware(
+ private val engine: Engine,
+ private val scope: CoroutineScope,
+) : Middleware<BrowserState, BrowserAction> {
+ private val logger = Logger("TranslationsMiddleware")
+
+ @Suppress("LongMethod", "CyclomaticComplexMethod")
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ // Pre process actions
+ when (action) {
+ is InitAction ->
+ context.store.dispatch(TranslationsAction.InitTranslationsBrowserState)
+
+ is TranslationsAction.InitTranslationsBrowserState -> {
+ scope.launch {
+ val engineIsSupported = requestEngineSupport(context)
+ if (engineIsSupported == true) {
+ initializeBrowserStore(context)
+ }
+ }
+ }
+
+ is TranslationsAction.TranslateExpectedAction -> {
+ requestDefaultModelDownloadSize(context, action.tabId)
+ }
+
+ is TranslationsAction.OperationRequestedAction -> {
+ when (action.operation) {
+ TranslationOperation.FETCH_SUPPORTED_LANGUAGES -> {
+ scope.launch {
+ requestSupportedLanguages(context, action.tabId)
+ }
+ }
+ TranslationOperation.FETCH_LANGUAGE_MODELS -> {
+ scope.launch {
+ requestLanguageModels(context, action.tabId)
+ }
+ }
+ TranslationOperation.FETCH_PAGE_SETTINGS -> {
+ scope.launch {
+ requestPageSettings(context, action.tabId)
+ }
+ }
+ TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS -> {
+ scope.launch {
+ requestLanguageSettings(context, action.tabId)
+ }
+ }
+ TranslationOperation.FETCH_NEVER_TRANSLATE_SITES -> {
+ scope.launch {
+ getNeverTranslateSites(context, action.tabId)
+ }
+ }
+ TranslationOperation.TRANSLATE,
+ TranslationOperation.RESTORE,
+ -> Unit
+ }
+ }
+
+ is TranslationsAction.FetchTranslationDownloadSizeAction -> {
+ scope.launch {
+ requestTranslationSize(
+ context = context,
+ tabId = action.tabId,
+ fromLanguage = action.fromLanguage,
+ toLanguage = action.toLanguage,
+ )
+ }
+ }
+
+ is TranslationsAction.RemoveNeverTranslateSiteAction -> {
+ scope.launch {
+ removeNeverTranslateSite(context, action.tabId, action.origin)
+ }
+ }
+
+ is TranslationsAction.UpdatePageSettingAction -> {
+ when (action.operation) {
+ TranslationPageSettingOperation.UPDATE_ALWAYS_OFFER_POPUP ->
+ scope.launch {
+ updateAlwaysOfferPopupPageSetting(
+ setting = action.setting,
+ )
+ }
+
+ TranslationPageSettingOperation.UPDATE_ALWAYS_TRANSLATE_LANGUAGE ->
+ scope.launch {
+ updateLanguagePageSetting(
+ context = context,
+ tabId = action.tabId,
+ setting = action.setting,
+ settingType = LanguageSetting.ALWAYS,
+ )
+ }
+
+ TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_LANGUAGE ->
+ scope.launch {
+ updateLanguagePageSetting(
+ context = context,
+ tabId = action.tabId,
+ setting = action.setting,
+ settingType = LanguageSetting.NEVER,
+ )
+ }
+
+ TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_SITE ->
+ scope.launch {
+ updateNeverTranslateSitePageSetting(
+ context = context,
+ tabId = action.tabId,
+ setting = action.setting,
+ )
+ }
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ // Continue to post process actions
+ next(action)
+ }
+
+ /**
+ * Use this to initialize translations data for [BrowserState.translationEngine]. If an
+ * issue occurs, the relevant error will be set on [BrowserState.translationEngine].
+ *
+ * This will populate:
+ * Language Support - [requestSupportedLanguages]
+ * Language Models - [requestLanguageModels]
+ * Language Settings - [requestLanguageSettings]
+ *
+ * @param context Context to use to dispatch to the store.
+ */
+ private fun initializeBrowserStore(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ ) {
+ requestSupportedLanguages(context)
+ requestLanguageModels(context)
+ requestLanguageSettings(context)
+ }
+
+ /**
+ * Checks if the translations engine supports the device architecture and updates the state on
+ * [BrowserState.translationEngine].
+ *
+ * @param context Context to use to dispatch to the store.
+ * @return Whether the engine is supported or not, or null when the support cannot be
+ * determined.
+ */
+ private suspend fun requestEngineSupport(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ ): Boolean? {
+ return suspendCoroutine { continuation ->
+ engine.isTranslationsEngineSupported(
+ onSuccess = { isEngineSupported ->
+ context.store.dispatch(
+ TranslationsAction.SetEngineSupportedAction(
+ isEngineSupported = isEngineSupported,
+ ),
+ )
+ logger.info("Success requesting engine support. isEngineSupported: $isEngineSupported")
+ continuation.resume(isEngineSupported)
+ },
+
+ onError = { error ->
+ context.store.dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.UnknownEngineSupportError(error),
+ ),
+ )
+ logger.error("Error requesting engine support: ", error)
+ continuation.resume(null)
+ },
+ )
+ }
+ }
+
+ /**
+ * Retrieves the list of supported languages and dispatches the result to the
+ * [BrowserState.translationEngine] via [TranslationsAction.SetSupportedLanguagesAction] or
+ * else dispatches the failure.
+ *
+ * For failure dispatching:
+ * If a tab ID is not provided, then only [TranslationsAction.EngineExceptionAction] will be
+ * dispatched to set the error on the [BrowserState.translationEngine].
+ *
+ * If a tab ID is provided, then [TranslationsAction.EngineExceptionAction]
+ * AND [TranslationsAction.TranslateExceptionAction] will be dispatched
+ * to set the error both on the [BrowserState.translationEngine] and
+ * [SessionState.translationsState].
+ *
+ * @param context Context to use to dispatch to the store.
+ * @param tabId If a Tab ID associated with the request for error handling.
+ * If null, this will only dispatch errors on the global translations browser state.
+ *
+ */
+ private fun requestSupportedLanguages(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String? = null,
+ ) {
+ engine.getSupportedTranslationLanguages(
+ onSuccess = {
+ context.store.dispatch(
+ TranslationsAction.SetSupportedLanguagesAction(
+ supportedLanguages = it,
+ ),
+ )
+
+ // Ensures error is cleared, if a tab made this request.
+ if (tabId != null) {
+ context.store.dispatch(
+ TranslationsAction.TranslateSuccessAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ ),
+ )
+ }
+
+ logger.info("Success requesting supported languages.")
+ },
+ onError = {
+ context.store.dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.CouldNotLoadLanguagesError(it),
+ ),
+ )
+
+ if (tabId != null) {
+ context.store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ translationError = TranslationError.CouldNotLoadLanguagesError(it),
+ ),
+ )
+ }
+
+ logger.error("Error requesting supported languages: ", it)
+ },
+ )
+ }
+
+ /**
+ * Retrieves the list of language machine learning translation models the translation engine
+ * has available and dispatches the result to the [BrowserState.translationEngine]
+ * via [TranslationsAction.SetLanguageModelsAction] or else dispatches the failure.
+ *
+ * For failure dispatching:
+ * If a tab ID is not provided, then only [TranslationsAction.EngineExceptionAction] will be
+ * dispatched to set the error on the [BrowserState.translationEngine].
+ *
+ * If a tab ID is provided, then [TranslationsAction.EngineExceptionAction]
+ * AND [TranslationsAction.TranslateExceptionAction] will be dispatched
+ * to set the error both on the [BrowserState.translationEngine] and
+ * [SessionState.translationsState].
+ *
+ * @param context Context to use to dispatch to the store.
+ * @param tabId If a Tab ID associated with the request for error handling.
+ * If null, this will only dispatch errors on the global translations browser state.
+ *
+ */
+ private fun requestLanguageModels(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String? = null,
+ ) {
+ engine.getTranslationsModelDownloadStates(
+ onSuccess = {
+ context.store.dispatch(
+ TranslationsAction.SetLanguageModelsAction(
+ languageModels = it,
+ ),
+ )
+ logger.info("Success requesting language models.")
+ },
+
+ onError = { error ->
+ context.store.dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.ModelCouldNotRetrieveError(error),
+ ),
+ )
+
+ if (tabId != null) {
+ context.store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_LANGUAGE_MODELS,
+ translationError = TranslationError.ModelCouldNotRetrieveError(error),
+ ),
+ )
+ }
+
+ logger.error("Error requesting language models: ", error)
+ },
+ )
+ }
+
+ /**
+ * Retrieves the list of never translate sites using [scope] and dispatches the result to the
+ * store via [TranslationsAction.SetNeverTranslateSitesAction] or else dispatches the failure
+ * [TranslationsAction.TranslateExceptionAction].
+ *
+ * @param context Context to use to dispatch to the store.
+ * @param tabId Tab ID associated with the request.
+ */
+ private fun getNeverTranslateSites(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ ) {
+ engine.getNeverTranslateSiteList(
+ onSuccess = {
+ context.store.dispatch(
+ TranslationsAction.SetNeverTranslateSitesAction(
+ tabId = tabId,
+ neverTranslateSites = it,
+ ),
+ )
+ logger.info("Success requesting never translate sites.")
+ },
+
+ onError = {
+ context.store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
+ translationError = TranslationError.CouldNotLoadNeverTranslateSites(it),
+ ),
+ )
+ logger.error("Error requesting never translate sites: ", it)
+ },
+ )
+ }
+
+ /**
+ * Removes the site from the list of never translate sites using [scope] and dispatches the result to the
+ * store via [TranslationsAction.SetNeverTranslateSitesAction] or else dispatches the failure
+ * [TranslationsAction.TranslateExceptionAction].
+ *
+ * @param context Context to use to dispatch to the store.
+ * @param tabId Tab ID associated with the request.
+ * @param origin A site origin URI that will have the specified never translate permission set.
+ */
+ private fun removeNeverTranslateSite(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ origin: String,
+ ) {
+ engine.setNeverTranslateSpecifiedSite(
+ origin = origin,
+ setting = false,
+ onSuccess = {
+ logger.info("Success requesting never translate sites.")
+
+ // Fetch page settings to ensure the state matches the engine.
+ context.store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ )
+ },
+ onError = {
+ logger.error("Error removing site from never translate list: ", it)
+
+ // Fetch never translate sites to ensure the state matches the engine.
+ context.store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
+ ),
+ )
+ },
+ )
+ }
+
+ /**
+ * Retrieves the page settings and dispatches the result to the
+ * store via [TranslationsAction.SetPageSettingsAction] or else dispatches the failure
+ * [TranslationsAction.TranslateExceptionAction].
+ *
+ * @param context Context to use to dispatch to the store.
+ * @param tabId Tab ID associated with the request.
+ */
+ private suspend fun requestPageSettings(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ ) {
+ logger.info("Requesting page settings.")
+ // Always offer setting
+ val alwaysOfferPopup: Boolean = engine.getTranslationsOfferPopup()
+
+ // Page language settings
+ val pageLanguage = context.store.state.findTab(tabId)
+ ?.translationsState?.translationEngineState?.detectedLanguages?.documentLangTag
+ val setting = pageLanguage?.let { getLanguageSetting(it) }
+ val alwaysTranslateLanguage = setting?.toBoolean(LanguageSetting.ALWAYS)
+ val neverTranslateLanguage = setting?.toBoolean(LanguageSetting.NEVER)
+
+ // Never translate site
+ val engineSession = context.store.state.findTab(tabId)
+ ?.engineState?.engineSession
+ val neverTranslateSite = engineSession?.let { getNeverTranslateSiteSetting(it) }
+
+ if (
+ alwaysTranslateLanguage != null &&
+ neverTranslateLanguage != null &&
+ neverTranslateSite != null
+ ) {
+ logger.info("Successfully found all page settings.")
+ context.store.dispatch(
+ TranslationsAction.SetPageSettingsAction(
+ tabId = tabId,
+ pageSettings = TranslationPageSettings(
+ alwaysOfferPopup = alwaysOfferPopup,
+ alwaysTranslateLanguage = alwaysTranslateLanguage,
+ neverTranslateLanguage = neverTranslateLanguage,
+ neverTranslateSite = neverTranslateSite,
+ ),
+ ),
+ )
+ } else {
+ logger.error("Could not find all page settings.")
+ // Any null values indicate something went wrong, alert an error occurred
+ context.store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ translationError = TranslationError.CouldNotLoadPageSettingsError(null),
+ ),
+ )
+ }
+ }
+
+ /**
+ * Fetches the always or never language setting synchronously from the engine. Will
+ * return null if an error occurs.
+ *
+ * @param pageLanguage Page language to check the translation preferences for.
+ * @return The page translate language setting or null.
+ */
+ private suspend fun getLanguageSetting(pageLanguage: String): LanguageSetting? {
+ return suspendCoroutine { continuation ->
+ engine.getLanguageSetting(
+ languageCode = pageLanguage,
+
+ onSuccess = { setting ->
+ logger.info("Success requesting language settings.")
+ continuation.resume(setting)
+ },
+
+ onError = {
+ logger.error("Could not retrieve language settings: $it")
+ continuation.resume(null)
+ },
+ )
+ }
+ }
+
+ /**
+ * Retrieves the list of languages and their settings and dispatches the result to the
+ * [BrowserState.translationEngine] via [TranslationsAction.SetLanguageSettingsAction] or
+ * else dispatches the failure.
+ *
+ * For failure dispatching:
+ * If a tab ID is not provided, then only [TranslationsAction.EngineExceptionAction] will be
+ * dispatched to set the error on the [BrowserState.translationEngine].
+ *
+ * If a tab ID is provided, then [TranslationsAction.EngineExceptionAction]
+ * AND [TranslationsAction.TranslateExceptionAction] will be dispatched
+ * to set the error both on the [BrowserState.translationEngine] and
+ * [SessionState.translationsState].
+ *
+ * @param context Context to use to dispatch to the store.
+ * @param tabId If a Tab ID is associated with the request for error handling.
+ * If null, this will only dispatch errors on the global translations browser state.
+ */
+ private fun requestLanguageSettings(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String? = null,
+ ) {
+ engine.getLanguageSettings(
+
+ onSuccess = { settings ->
+ context.store.dispatch(
+ TranslationsAction.SetLanguageSettingsAction(
+ languageSettings = settings,
+ ),
+ )
+ logger.info("Success requesting language settings.")
+ },
+
+ onError = {
+ context.store.dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.CouldNotLoadLanguageSettingsError(it),
+ ),
+ )
+
+ if (tabId != null) {
+ context.store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
+ translationError = TranslationError.CouldNotLoadLanguageSettingsError(it),
+ ),
+ )
+ }
+
+ logger.error("Error requesting language settings: ", it)
+ },
+ )
+ }
+
+ /**
+ * Fetches the never translate site setting synchronously from the [EngineSession]. Will
+ * return null if an error occurs.
+ *
+ * @param engineSession With page context on how to complete this operation.
+ * @return The never translate site setting from the [EngineSession] or null.
+ */
+ private suspend fun getNeverTranslateSiteSetting(engineSession: EngineSession): Boolean? {
+ return suspendCoroutine { continuation ->
+ engineSession.getNeverTranslateSiteSetting(
+ onResult = { setting ->
+ logger.info("Success requesting never translate site settings.")
+ continuation.resume(setting)
+ },
+ onException = {
+ logger.error("Could not retrieve never translate site settings: $it")
+ continuation.resume(null)
+ },
+ )
+ }
+ }
+
+ /**
+ * Retrieves the download size and dispatches the result to the
+ * [SessionState.translationsState] on the [BrowserStore]
+ * via [TranslationsAction.SetTranslationDownloadSizeAction].
+ *
+ * @param context Context to use to dispatch to the store.
+ * @param tabId Tab ID associated with the request.
+ * @param fromLanguage The from language to request the translation download size for.
+ * @param toLanguage The to language to request the translation download size for.
+ */
+ private fun requestTranslationSize(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ fromLanguage: Language,
+ toLanguage: Language,
+ ) {
+ engine.getTranslationsPairDownloadSize(
+ fromLanguage = fromLanguage.code,
+ toLanguage = toLanguage.code,
+
+ onSuccess = { size ->
+ context.store.dispatch(
+ TranslationsAction.SetTranslationDownloadSizeAction(
+ tabId = tabId,
+ translationSize = TranslationDownloadSize(
+ fromLanguage = fromLanguage,
+ toLanguage = toLanguage,
+ size = size,
+ error = null,
+ ),
+ ),
+ )
+ logger.info("Success requesting download size.")
+ },
+
+ onError = { error ->
+ context.store.dispatch(
+ TranslationsAction.SetTranslationDownloadSizeAction(
+ tabId = tabId,
+ translationSize = TranslationDownloadSize(
+ fromLanguage = fromLanguage,
+ toLanguage = toLanguage,
+ size = null,
+ error = TranslationError.CouldNotDetermineDownloadSizeError(null),
+ ),
+ ),
+ )
+ logger.error("Error requesting download size: ", error)
+ },
+ )
+ }
+
+ /**
+ * Fetches the expected translation model download size assuming the user intends to complete
+ * a translation using the detected default `from` (page language) and `to` (user preferred)
+ * languages.
+ *
+ * If the detected default languages are available, then this will fetch and set the
+ * corresponding model download size on [SessionState.translationsState].
+ *
+ * If no defaults are available, then no action will occur.
+ *
+ * @param context Context to use to dispatch to the store.
+ * @param tabId Tab ID associated with the request.
+ */
+ private fun requestDefaultModelDownloadSize(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ ) {
+ val fromLanguage = getDefaultFromLanguage(context, tabId) ?: return
+ val toLanguage = getDefaultToLanguage(context, tabId) ?: return
+ context.store.dispatch(
+ TranslationsAction.FetchTranslationDownloadSizeAction(
+ tabId = tabId,
+ fromLanguage = fromLanguage,
+ toLanguage = toLanguage,
+ ),
+ )
+ }
+
+ /**
+ * Updates the always offer popup setting with the [Engine].
+ *
+ * @param setting The value of the always offer setting to update.
+ */
+ private fun updateAlwaysOfferPopupPageSetting(
+ setting: Boolean,
+ ) {
+ logger.info("Setting the always offer translations popup preference.")
+ engine.setTranslationsOfferPopup(setting)
+ }
+
+ /**
+ * Updates the language settings with the [Engine].
+ *
+ * If an error occurs, then the method will request the page settings be re-fetched and set on
+ * the browser store.
+ *
+ * @param context The context used to request the page settings.
+ * @param tabId Tab ID associated with the request.
+ * @param setting The value of the always offer setting to update.
+ * @param settingType If the boolean to update is from the
+ * [LanguageSetting.ALWAYS] or [LanguageSetting.NEVER] perspective.
+ */
+ private fun updateLanguagePageSetting(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ setting: Boolean,
+ settingType: LanguageSetting,
+ ) {
+ logger.info("Preparing to update the translations language preference.")
+
+ val pageLanguage = context.store.state.findTab(tabId)
+ ?.translationsState?.translationEngineState?.detectedLanguages?.documentLangTag
+ val convertedSetting = settingType.toLanguageSetting(setting)
+
+ if (pageLanguage == null || convertedSetting == null) {
+ logger.info("An issue occurred while preparing to update the language setting.")
+
+ // Fetch page settings to ensure the state matches the engine.
+ context.store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ )
+ } else {
+ logger.info("Updating language setting.")
+ updateLanguageSetting(context, tabId, pageLanguage, convertedSetting)
+ }
+ }
+
+ /**
+ * Updates the language settings with the [Engine].
+ *
+ * If an error occurs, then the method will request the page settings be re-fetched and set on
+ * the browser store.
+ *
+ * @param context The context used to request the page settings.
+ * @param tabId Tab ID associated with the request.
+ * @param languageCode The BCP-47 language to update.
+ * @param setting The new language setting for the [languageCode].
+ */
+ private fun updateLanguageSetting(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ languageCode: String,
+ setting: LanguageSetting,
+ ) {
+ logger.info("Setting the translations language preference.")
+
+ engine.setLanguageSetting(
+ languageCode = languageCode,
+ languageSetting = setting,
+
+ onSuccess = {
+ // Ensure the session's page settings remain in sync with this update.
+ context.store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
+ ),
+ )
+ logger.info("Successfully updated the language preference.")
+ },
+
+ onError = {
+ logger.error("Could not update the language preference.", it)
+
+ // Fetch page settings to ensure the state matches the engine.
+ context.store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ )
+ },
+ )
+ }
+
+ /**
+ * Updates the never translate site settings with the [EngineSession] and ensures the global
+ * list of never translate sites remains in sync.
+ *
+ * If an error occurs, then the method will request the page settings be re-fetched and set on
+ * the browser store.
+ *
+ * Note: This method should be used when on the same page as the requested change.
+ *
+ * @param context The context used to request the page settings.
+ * @param tabId Tab ID associated with the request.
+ * @param setting The value of the site setting to update.
+ */
+ private fun updateNeverTranslateSitePageSetting(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ setting: Boolean,
+ ) {
+ val engineSession = context.store.state.findTab(tabId)
+ ?.engineState?.engineSession
+
+ if (engineSession == null) {
+ logger.error("Did not receive an engine session to set the never translate site preference.")
+ } else {
+ engineSession.setNeverTranslateSiteSetting(
+ setting = setting,
+ onResult = {
+ logger.info("Successfully updated the never translate site preference.")
+
+ // Ensure the global sites store is in-sync with the page settings.
+ context.store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
+ ),
+ )
+ },
+ onException = {
+ logger.error("Could not update the never translate site preference.", it)
+
+ // Fetch page settings to ensure the state matches the engine.
+ context.store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tabId,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ )
+ },
+ )
+ }
+ }
+
+ /**
+ * Helper to find the default "from" language for a site using the page detected language and
+ * engine supported languages.
+ *
+ * @param context The context used to request the information from the store.
+ * @param tabId Tab ID associated with the request.
+ * @return The default expected translate "from" language, which is the page language or null
+ * if unavailable or an unsupported language by the engine.
+ */
+ private fun getDefaultFromLanguage(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ ): Language? {
+ val pageLang = context.store.state.findTab(tabId)
+ ?.translationsState?.translationEngineState?.detectedLanguages?.documentLangTag ?: return null
+ val supportedLanguages = context.store.state.translationEngine.supportedLanguages ?: return null
+ return supportedLanguages.findLanguage(pageLang)
+ }
+
+ /**
+ * Helper to find the default "to" language using the user's preferred language and
+ * engine supported languages.
+ *
+ * @param context The context used to request the information from the store.
+ * @param tabId Tab ID associated with the request.
+ * @return The default translate "to" language, which is the user's preferred language or null
+ * if unavailable or an unsupported language by the engine.
+ */
+ private fun getDefaultToLanguage(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabId: String,
+ ): Language? {
+ val userPreferredLang = context.store.state.findTab(tabId)
+ ?.translationsState?.translationEngineState?.detectedLanguages?.userPreferredLangTag ?: return null
+ val supportedLanguages = context.store.state.translationEngine.supportedLanguages ?: return null
+ return supportedLanguages.findLanguage(userPreferredLang)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddleware.kt
new file mode 100644
index 0000000000..23c49d9262
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddleware.kt
@@ -0,0 +1,92 @@
+/* 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.browser.state.engine.middleware
+
+import android.content.ComponentCallbacks2
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.SystemAction
+import mozilla.components.browser.state.selector.allTabs
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.base.log.logger.Logger
+
+// The number of tabs we keep active and do not suspend (in addition to the selected tab)
+private const val MIN_ACTIVE_TABS = 3
+
+/**
+ * [Middleware] responsible for suspending [EngineSession] instances on low memory.
+ */
+internal class TrimMemoryMiddleware : Middleware<BrowserState, BrowserAction> {
+ private val logger = Logger("TrimMemoryMiddleware")
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ next(action)
+
+ if (action is SystemAction.LowMemoryAction) {
+ trimMemory(context, action)
+ }
+ }
+
+ private fun trimMemory(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ action: SystemAction.LowMemoryAction,
+ ) {
+ if (!shouldCloseEngineSessions(action.level)) {
+ return
+ }
+
+ val suspendTabs = determineTabsToSuspend(context.state)
+
+ logger.info("Trim memory (tabs=${context.state.allTabs.size}, suspending=${suspendTabs.size})")
+
+ // This is not the most efficient way of doing this. We are looping over all tabs and then
+ // dispatching a SuspendEngineSessionAction for each tab that is no longer needed.
+ suspendTabs.forEach { tab ->
+ context.dispatch(EngineAction.SuspendEngineSessionAction(tab.id))
+ }
+ }
+
+ private fun determineTabsToSuspend(
+ state: BrowserState,
+ ): List<SessionState> {
+ return state.allTabs.filter { tab ->
+ // We never suspend the currently selected tab
+ tab.id != state.selectedTabId
+ }.filter { tab ->
+ // Only tabs with an engine session can get suspended
+ tab.engineState.engineSession != null
+ }.sortedByDescending { tab ->
+ if (tab is TabSessionState) {
+ // We want to suspend the tabs that haven't been accessed for a while first
+ tab.lastAccess
+ } else {
+ // We are more aggressive with custom tabs an always consider them for suspension
+ 0
+ }
+ }.drop(MIN_ACTIVE_TABS) // Keep n [MIN_ACTIVE_TABS] most recently accessed tabs.
+ }
+}
+
+private fun shouldCloseEngineSessions(level: Int): Boolean {
+ return when (level) {
+ // Foreground: The device is running extremely low on memory. The app is not yet considered a killable
+ // process, but the system will begin killing background processes if apps do not release resources.
+ ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> true
+
+ // Background: The system is running low on memory and our process is one of the first to be killed
+ // if the system does not recover memory now.
+ ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> true
+
+ else -> false
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddleware.kt
new file mode 100644
index 0000000000..390b7b0f4b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddleware.kt
@@ -0,0 +1,78 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * [Middleware] implementation responsible for calling [EngineSession.markActiveForWebExtensions] on
+ * [EngineSession] instances.
+ */
+internal class WebExtensionMiddleware : Middleware<BrowserState, BrowserAction> {
+ private val logger = Logger("WebExtensionsMiddleware")
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is EngineAction.UnlinkEngineSessionAction -> {
+ if (context.state.activeWebExtensionTabId == action.tabId) {
+ val activeTab = context.state.findTab(action.tabId)
+ activeTab?.engineState?.engineSession?.markActiveForWebExtensions(false)
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+
+ when (action) {
+ is TabListAction,
+ is EngineAction.LinkEngineSessionAction,
+ -> {
+ switchActiveStateIfNeeded(context)
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+
+ private fun switchActiveStateIfNeeded(context: MiddlewareContext<BrowserState, BrowserAction>) {
+ val state = context.state
+ if (state.activeWebExtensionTabId == state.selectedTabId) {
+ return
+ }
+
+ val previousActiveTab = state.activeWebExtensionTabId?.let { state.findTab(it) }
+ previousActiveTab?.engineState?.engineSession?.markActiveForWebExtensions(false)
+
+ val nextActiveTab = state.selectedTabId?.let { state.findTab(it) }
+ val engineSession = nextActiveTab?.engineState?.engineSession
+
+ if (engineSession == null) {
+ logger.debug("No engine session for new active tab (${nextActiveTab?.id})")
+ context.dispatch(WebExtensionAction.UpdateActiveWebExtensionTabAction(null))
+ return
+ } else {
+ logger.debug("New active tab (${nextActiveTab.id})")
+ engineSession.markActiveForWebExtensions(true)
+ context.dispatch(WebExtensionAction.UpdateActiveWebExtensionTabAction(nextActiveTab.id))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt
new file mode 100644
index 0000000000..4d044266c2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt
@@ -0,0 +1,19 @@
+/* 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.browser.state.ext
+
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+
+internal fun CustomTabSessionState.toTab(): TabSessionState {
+ return TabSessionState(
+ id = id,
+ content = content,
+ trackingProtection = trackingProtection,
+ engineState = engineState,
+ extensionState = extensionState,
+ contextId = contextId,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt
new file mode 100644
index 0000000000..a081438635
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt
@@ -0,0 +1,15 @@
+/* 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.browser.state.ext
+import mozilla.components.concept.engine.permission.PermissionRequest
+
+/**
+ * @returns true if the given [permissionRequest] is contained in this list otherwise false
+ * */
+fun List<PermissionRequest>.containsPermission(permissionRequest: PermissionRequest): Boolean {
+ return this.any {
+ it.uri == permissionRequest.uri &&
+ it.permissions == permissionRequest.permissions
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/helper/Target.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/helper/Target.kt
new file mode 100644
index 0000000000..2558b60e01
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/helper/Target.kt
@@ -0,0 +1,96 @@
+/* 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.browser.state.helper
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.ext.observeAsComposableState
+
+/**
+ * Helper for allowing a component consumer to specify which tab a component should target (e.g.
+ * the selected tab, a specific pinned tab or a custom tab). Additional helper methods make it
+ * easier to lookup the current state of the tab or observe changes.
+ */
+sealed class Target {
+ /**
+ * Looks up this target in the given [BrowserStore] and returns the matching [SessionState] if
+ * available. Otherwise returns `null`.
+ *
+ * @param store to lookup this target in.
+ */
+ fun lookupIn(store: BrowserStore): SessionState? = lookupIn(store.state)
+
+ /**
+ * Looks up this target in the given [BrowserState] and returns the matching [SessionState] if
+ * available. Otherwise returns `null`.
+ *
+ * @param state to lookup this target in.
+ */
+ abstract fun lookupIn(state: BrowserState): SessionState?
+
+ /**
+ * Observes this target and represents the mapped state (using [map]) via [State].
+ *
+ * Everytime the [Store] state changes and the result of the [observe] function changes for this
+ * state, the returned [State] will be updated causing recomposition of every [State.value] usage.
+ *
+ * The [Store] observer will automatically be removed when this composable disposes or the current
+ * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
+ *
+ * @param store that should get observed
+ * @param observe function that maps a [SessionState] to the (sub) state that should get observed
+ * for changes.
+ */
+ @Composable
+ fun <R> observeAsComposableStateFrom(
+ store: BrowserStore,
+ observe: (SessionState?) -> R,
+ ): State<SessionState?> {
+ return store.observeAsComposableState(
+ map = { state -> lookupIn(state) },
+ observe = { state -> observe(lookupIn(state)) },
+ )
+ }
+
+ /**
+ * Targets the selected tab.
+ */
+ object SelectedTab : Target() {
+ override fun lookupIn(state: BrowserState): SessionState? {
+ return state.selectedTab
+ }
+ }
+
+ /**
+ * Targets a specific tab by its [tabId].
+ *
+ * @param tabId The ID of the tab to be targeted.
+ */
+ class Tab(val tabId: String) : Target() {
+ override fun lookupIn(state: BrowserState): SessionState? {
+ return state.findTab(tabId)
+ }
+ }
+
+ /**
+ * Targets a specific custom tab by its [customTabId].
+ *
+ * @param customTabId The ID of the custom tab to be targeted.
+ */
+ class CustomTab(val customTabId: String) : Target() {
+ override fun lookupIn(state: BrowserState): SessionState? {
+ return state.findCustomTab(customTabId)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/AwesomeBarStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/AwesomeBarStateReducer.kt
new file mode 100644
index 0000000000..4bf61267a0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/AwesomeBarStateReducer.kt
@@ -0,0 +1,30 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.AwesomeBarAction
+import mozilla.components.browser.state.state.AwesomeBarState
+import mozilla.components.browser.state.state.BrowserState
+
+/**
+ * An [AwesomeBarAction] reducer that updates [BrowserState.awesomeBarState].
+ */
+internal object AwesomeBarStateReducer {
+ fun reduce(state: BrowserState, action: AwesomeBarAction): BrowserState {
+ return when (action) {
+ is AwesomeBarAction.VisibilityStateUpdated -> {
+ val awesomeBarState = state.awesomeBarState.copy(visibilityState = action.visibilityState)
+ state.copy(awesomeBarState = awesomeBarState)
+ }
+ is AwesomeBarAction.SuggestionClicked -> {
+ val awesomeBarState = state.awesomeBarState.copy(clickedSuggestion = action.suggestion)
+ state.copy(awesomeBarState = awesomeBarState)
+ }
+ is AwesomeBarAction.EngagementFinished -> {
+ state.copy(awesomeBarState = AwesomeBarState())
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt
new file mode 100644
index 0000000000..a2ae9e4547
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt
@@ -0,0 +1,161 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.AppLifecycleAction
+import mozilla.components.browser.state.action.AwesomeBarAction
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContainerAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CookieBannerAction
+import mozilla.components.browser.state.action.CopyInternetResourceAction
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.DebugAction
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.ExtensionsProcessAction
+import mozilla.components.browser.state.action.HistoryMetadataAction
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.action.LastAccessAction
+import mozilla.components.browser.state.action.LocaleAction
+import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.action.ReaderAction
+import mozilla.components.browser.state.action.RecentlyClosedAction
+import mozilla.components.browser.state.action.RestoreCompleteAction
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.action.ShareInternetResourceAction
+import mozilla.components.browser.state.action.SystemAction
+import mozilla.components.browser.state.action.TabGroupAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.TrackingProtectionAction
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.action.UndoAction
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.Action
+
+/**
+ * Reducers for [BrowserStore].
+ *
+ * A reducer is a function that receives the current [BrowserState] and an [Action] and then returns a new
+ * [BrowserState].
+ */
+internal object BrowserStateReducer {
+ fun reduce(state: BrowserState, action: BrowserAction): BrowserState {
+ return when (action) {
+ is InitAction -> state
+ is AppLifecycleAction -> state
+ is RestoreCompleteAction -> state.copy(restoreComplete = true)
+ is ContainerAction -> ContainerReducer.reduce(state, action)
+ is RecentlyClosedAction -> RecentlyClosedReducer.reduce(state, action)
+ is ContentAction -> ContentStateReducer.reduce(state, action)
+ is CustomTabListAction -> CustomTabListReducer.reduce(state, action)
+ is EngineAction -> EngineStateReducer.reduce(state, action)
+ is ReaderAction -> ReaderStateReducer.reduce(state, action)
+ is SystemAction -> SystemReducer.reduce(state, action)
+ is TabListAction -> TabListReducer.reduce(state, action)
+ is TabGroupAction -> TabGroupReducer.reduce(state, action)
+ is TrackingProtectionAction -> TrackingProtectionStateReducer.reduce(state, action)
+ is TranslationsAction -> TranslationsStateReducer.reduce(state, action)
+ is CookieBannerAction -> CookieBannerStateReducer.reduce(state, action)
+ is WebExtensionAction -> WebExtensionReducer.reduce(state, action)
+ is MediaSessionAction -> MediaSessionReducer.reduce(state, action)
+ is DownloadAction -> DownloadStateReducer.reduce(state, action)
+ is SearchAction -> SearchReducer.reduce(state, action)
+ is CrashAction -> CrashReducer.reduce(state, action)
+ is LastAccessAction -> LastAccessReducer.reduce(state, action)
+ is UndoAction -> UndoReducer.reduce(state, action)
+ is ShareInternetResourceAction -> ShareInternetResourceStateReducer.reduce(state, action)
+ is CopyInternetResourceAction -> CopyInternetResourceStateReducer.reduce(state, action)
+ is LocaleAction -> LocaleStateReducer.reduce(state, action)
+ is HistoryMetadataAction -> HistoryMetadataReducer.reduce(state, action)
+ is DebugAction -> DebugReducer.reduce(state, action)
+ is ExtensionsProcessAction -> ExtensionsProcessStateReducer.reduce(state, action)
+ is AwesomeBarAction -> AwesomeBarStateReducer.reduce(state, action)
+ }
+ }
+}
+
+/**
+ * Finds the corresponding tab or custom tab in the [BrowserState] and updates it using [update].
+ *
+ * Consider using the specialized [updateTabState] or [updateCustomTabState] to limit the tabs to be updated
+ * if the properties you want changed are not common to both [SessionState] implementations.
+ *
+ * @param tabId ID of the tab to change.
+ * @param update Returns a new version of the tab state. Must be the same class,
+ * preferably using [SessionState.createCopy].
+ */
+@Suppress("Unchecked_Cast")
+internal fun BrowserState.updateTabOrCustomTabState(
+ tabId: String,
+ update: (SessionState) -> SessionState,
+): BrowserState {
+ val newTabs = tabs.updateTabs(tabId, update) as List<TabSessionState>?
+ if (newTabs != null) return copy(tabs = newTabs)
+
+ val newCustomTabs = customTabs.updateTabs(tabId, update) as List<CustomTabSessionState>?
+ if (newCustomTabs != null) return copy(customTabs = newCustomTabs)
+
+ return this
+}
+
+/**
+ * Finds the corresponding tab in the [BrowserState] and replaces it using [update].
+ *
+ * This will only update a [TabSessionState] if such exists with the given [tabId].
+ * Consider using the other specialized [updateCustomTabState] method for updating only [CustomTabSessionState]
+ * or the general [updateTabOrCustomTabState] to update any tab or custom tab with a given [tabId].
+ *
+ * @param tabId ID of the tab to change.
+ * @param update Returns a new version of [TabSessionState].
+ */
+internal fun BrowserState.updateTabState(
+ tabId: String,
+ update: (TabSessionState) -> TabSessionState,
+): BrowserState {
+ return tabs.updateTabs(tabId, update)?.let {
+ copy(tabs = it)
+ } ?: this
+}
+
+/**
+ * Finds the corresponding custom tab in the [BrowserState] and replaces it using [update].
+ *
+ * This will only update a [CustomTabSessionState] if such exists with the given [tabId].
+ * Consider using the other specialized [updateTabState] method for updating only [TabSessionState]
+ * or the general [updateTabOrCustomTabState] to update any tab or custom tab with a given [tabId].
+ *
+ * @param tabId ID of the tab to change.
+ * @param update Returns a new version of [CustomTabSessionState].
+ */
+internal fun BrowserState.updateCustomTabState(
+ tabId: String,
+ update: (CustomTabSessionState) -> CustomTabSessionState,
+): BrowserState {
+ return customTabs.updateTabs(tabId, update)?.let {
+ copy(customTabs = it)
+ } ?: this
+}
+
+/**
+ * Finds the corresponding tab in the list and replaces it using [update].
+ * @param tabId ID of the tab to change.
+ * @param update Returns a new version of the tab state.
+ */
+internal fun <T : SessionState> List<T>.updateTabs(
+ tabId: String,
+ update: (T) -> T,
+): List<T>? {
+ val tabIndex = indexOfFirst { it.id == tabId }
+ if (tabIndex == -1) return null
+
+ return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size)
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContainerReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContainerReducer.kt
new file mode 100644
index 0000000000..ff9f0bde89
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContainerReducer.kt
@@ -0,0 +1,38 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.ContainerAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object ContainerReducer {
+ fun reduce(state: BrowserState, action: ContainerAction): BrowserState {
+ return when (action) {
+ is ContainerAction.AddContainerAction -> {
+ val existingContainer = state.containers[action.container.contextId]
+ if (existingContainer == null) {
+ state.copy(
+ containers = state.containers + (action.container.contextId to action.container),
+ )
+ } else {
+ state
+ }
+ }
+ is ContainerAction.AddContainersAction -> {
+ state.copy(
+ containers = state.containers + (
+ action.containers.map { it.contextId to it }
+ .toMap()
+ ),
+ )
+ }
+ is ContainerAction.RemoveContainerAction -> {
+ state.copy(
+ containers = state.containers - action.contextId,
+ )
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt
new file mode 100644
index 0000000000..45165477b3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt
@@ -0,0 +1,385 @@
+/* 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.browser.state.reducer
+
+import android.net.Uri
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction
+import mozilla.components.browser.state.ext.containsPermission
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.content.HistoryState
+import mozilla.components.browser.state.state.content.PermissionHighlightsState
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.support.ktx.android.net.isInScope
+import mozilla.components.support.ktx.android.net.sameSchemeAndHostAs
+import mozilla.components.support.ktx.kotlin.isSameOriginAs
+
+@Suppress("LargeClass")
+internal object ContentStateReducer {
+ /**
+ * [ContentAction] Reducer function for modifying a specific [ContentState] of a [SessionState].
+ */
+ @Suppress("LongMethod", "ThrowsCount")
+ fun reduce(state: BrowserState, action: ContentAction): BrowserState {
+ return when (action) {
+ is ContentAction.RemoveIconAction -> updateContentState(state, action.sessionId) {
+ it.copy(icon = null)
+ }
+ is ContentAction.UpdateUrlAction -> updateContentState(state, action.sessionId) {
+ it.copy(
+ url = action.url,
+ icon = if (!isHostEquals(it.url, action.url)) {
+ null
+ } else {
+ it.icon
+ },
+ title = if (!isUrlSame(it.url, action.url)) {
+ ""
+ } else {
+ it.title
+ },
+ previewImageUrl = if (!isUrlSame(it.url, action.url)) {
+ null
+ } else {
+ it.previewImageUrl
+ },
+ webAppManifest = if (!isInScope(it.webAppManifest, action.url)) {
+ null
+ } else {
+ it.webAppManifest
+ },
+ permissionRequestsList = if (!it.url.isSameOriginAs(action.url)) {
+ emptyList()
+ } else {
+ it.permissionRequestsList
+ },
+ searchTerms = if (action.hasUserGesture) {
+ ""
+ } else {
+ it.searchTerms
+ },
+ )
+ }
+ is ContentAction.UpdateProgressAction -> updateContentState(state, action.sessionId) {
+ it.copy(progress = action.progress)
+ }
+ is ContentAction.UpdateTitleAction -> updateContentState(state, action.sessionId) {
+ it.copy(title = action.title)
+ }
+ is ContentAction.UpdatePreviewImageAction -> updateContentState(state, action.sessionId) {
+ it.copy(previewImageUrl = action.previewImageUrl)
+ }
+ is ContentAction.UpdateLoadingStateAction -> updateContentState(state, action.sessionId) {
+ it.copy(loading = action.loading)
+ }
+ is ContentAction.UpdateRefreshCanceledStateAction -> updateContentState(state, action.sessionId) {
+ it.copy(refreshCanceled = action.refreshCanceled)
+ }
+ is ContentAction.UpdateSearchTermsAction -> updateContentState(state, action.sessionId) {
+ it.copy(searchTerms = action.searchTerms)
+ }
+ is ContentAction.UpdateIsSearchAction -> updateContentState(state, action.sessionId) {
+ it.copy(isSearch = action.isSearch)
+ }
+ is ContentAction.UpdateSecurityInfoAction -> updateContentState(state, action.sessionId) {
+ it.copy(securityInfo = action.securityInfo)
+ }
+ is ContentAction.UpdateIconAction -> updateContentState(state, action.sessionId) {
+ if (action.pageUrl == it.url) {
+ // Only update the icon of the state if we are still on this page. The user may
+ // have navigated away by the time the icon is loaded.
+ it.copy(icon = action.icon)
+ } else {
+ it
+ }
+ }
+ is ContentAction.UpdateThumbnailAction -> {
+ throw IllegalStateException("You need to add ThumbnailsMiddleware to your BrowserStore. ($action)")
+ }
+ is ContentAction.UpdateDownloadAction -> updateContentState(state, action.sessionId) {
+ it.copy(download = action.download.copy(sessionId = action.sessionId))
+ }
+ is ContentAction.ConsumeDownloadAction -> consumeDownload(state, action.sessionId, action.downloadId)
+ is ContentAction.CancelDownloadAction -> consumeDownload(state, action.sessionId, action.downloadId)
+
+ is ContentAction.UpdateHitResultAction -> updateContentState(state, action.sessionId) {
+ it.copy(hitResult = action.hitResult)
+ }
+ is ContentAction.ConsumeHitResultAction -> updateContentState(state, action.sessionId) {
+ it.copy(hitResult = null)
+ }
+ is ContentAction.UpdatePromptRequestAction -> updateContentState(state, action.sessionId) {
+ it.copy(promptRequests = it.promptRequests + action.promptRequest)
+ }
+ is ContentAction.ConsumePromptRequestAction -> updateContentState(state, action.sessionId) {
+ it.copy(promptRequests = it.promptRequests - action.promptRequest)
+ }
+ is ContentAction.ReplacePromptRequestAction -> updateContentState(
+ state,
+ action.sessionId,
+ ) { contentState ->
+ val updated = contentState.promptRequests
+ .filter { it.uid != action.previousPromptUid }
+ .plus(action.promptRequest)
+ contentState.copy(promptRequests = updated)
+ }
+ is ContentAction.AddFindResultAction -> updateContentState(state, action.sessionId) {
+ it.copy(findResults = it.findResults + action.findResult)
+ }
+ is ContentAction.ClearFindResultsAction -> updateContentState(state, action.sessionId) {
+ it.copy(findResults = emptyList())
+ }
+ is ContentAction.UpdateWindowRequestAction -> updateContentState(state, action.sessionId) {
+ it.copy(windowRequest = action.windowRequest)
+ }
+ is ContentAction.ConsumeWindowRequestAction -> updateContentState(state, action.sessionId) {
+ it.copy(windowRequest = null)
+ }
+ is ContentAction.UpdateSearchRequestAction -> updateContentState(state, action.sessionId) {
+ it.copy(searchRequest = action.searchRequest)
+ }
+ is ContentAction.ConsumeSearchRequestAction -> updateContentState(state, action.sessionId) {
+ it.copy(searchRequest = null)
+ }
+ is ContentAction.FullScreenChangedAction -> updateContentState(state, action.sessionId) {
+ it.copy(fullScreen = action.fullScreenEnabled)
+ }
+ is ContentAction.PictureInPictureChangedAction -> updateContentState(state, action.sessionId) {
+ it.copy(pictureInPictureEnabled = action.pipEnabled)
+ }
+ is ContentAction.ViewportFitChangedAction -> updateContentState(state, action.sessionId) {
+ it.copy(layoutInDisplayCutoutMode = action.layoutInDisplayCutoutMode)
+ }
+ is ContentAction.UpdateBackNavigationStateAction -> updateContentState(state, action.sessionId) {
+ it.copy(canGoBack = action.canGoBack)
+ }
+ is ContentAction.UpdateForwardNavigationStateAction -> updateContentState(state, action.sessionId) {
+ it.copy(canGoForward = action.canGoForward)
+ }
+ is ContentAction.UpdateWebAppManifestAction -> updateContentState(state, action.sessionId) {
+ it.copy(webAppManifest = action.webAppManifest)
+ }
+ is ContentAction.RemoveWebAppManifestAction -> updateContentState(state, action.sessionId) {
+ it.copy(webAppManifest = null)
+ }
+ is ContentAction.UpdateFirstContentfulPaintStateAction -> updateContentState(state, action.sessionId) {
+ it.copy(firstContentfulPaint = action.firstContentfulPaint)
+ }
+ is ContentAction.UpdateHistoryStateAction -> updateContentState(state, action.sessionId) {
+ it.copy(history = HistoryState(action.historyList, action.currentIndex))
+ }
+ is ContentAction.UpdatePermissionsRequest -> updateContentState(
+ state,
+ action.sessionId,
+ ) {
+ if (!it.permissionRequestsList.containsPermission(action.permissionRequest)) {
+ it.copy(
+ permissionRequestsList = it.permissionRequestsList + action.permissionRequest,
+ )
+ } else {
+ it
+ }
+ }
+ is ContentAction.ConsumePermissionsRequest -> updateContentState(
+ state,
+ action.sessionId,
+ ) {
+ if (it.permissionRequestsList.containsPermission(action.permissionRequest)) {
+ it.copy(
+ permissionRequestsList = it.permissionRequestsList - action.permissionRequest,
+ )
+ } else {
+ it
+ }
+ }
+ is ContentAction.UpdateAppPermissionsRequest -> updateContentState(
+ state,
+ action.sessionId,
+ ) {
+ if (!it.appPermissionRequestsList.containsPermission(action.appPermissionRequest)) {
+ it.copy(
+ appPermissionRequestsList = it.appPermissionRequestsList + action.appPermissionRequest,
+ )
+ } else {
+ it
+ }
+ }
+ is ContentAction.ConsumeAppPermissionsRequest -> updateContentState(
+ state,
+ action.sessionId,
+ ) {
+ if (it.appPermissionRequestsList.containsPermission(action.appPermissionRequest)) {
+ it.copy(
+ appPermissionRequestsList = it.appPermissionRequestsList - action.appPermissionRequest,
+ )
+ } else {
+ it
+ }
+ }
+ is ContentAction.ClearPermissionRequests -> updateContentState(
+ state,
+ action.sessionId,
+ ) {
+ it.copy(permissionRequestsList = emptyList())
+ }
+ is ContentAction.ClearAppPermissionRequests -> updateContentState(
+ state,
+ action.sessionId,
+ ) {
+ it.copy(appPermissionRequestsList = emptyList())
+ }
+ is ContentAction.UpdateLoadRequestAction -> updateContentState(state, action.sessionId) {
+ it.copy(loadRequest = action.loadRequest)
+ }
+ is ContentAction.SetRecordingDevices -> updateContentState(state, action.sessionId) {
+ it.copy(recordingDevices = action.devices)
+ }
+ is ContentAction.UpdateDesktopModeAction -> updateContentState(state, action.sessionId) {
+ it.copy(desktopMode = action.enabled)
+ }
+ is UpdatePermissionHighlightsStateAction.NotificationChangedAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(notificationChanged = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.CameraChangedAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(cameraChanged = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.LocationChangedAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(locationChanged = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.MediaKeySystemAccesChangedAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(mediaKeySystemAccessChanged = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.MicrophoneChangedAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(microphoneChanged = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.PersistentStorageChangedAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(persistentStorageChanged = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.AutoPlayAudibleBlockingAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(autoPlayAudibleBlocking = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.AutoPlayInAudibleChangedAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(autoPlayInaudibleChanged = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.AutoPlayInAudibleBlockingAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(autoPlayInaudibleBlocking = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.AutoPlayAudibleChangedAction -> {
+ updatePermissionHighlightsState(state, action.tabId) {
+ it.copy(autoPlayAudibleChanged = action.value)
+ }
+ }
+ is UpdatePermissionHighlightsStateAction.Reset -> {
+ updatePermissionHighlightsState(state, action.tabId) { PermissionHighlightsState() }
+ }
+
+ is ContentAction.UpdateAppIntentAction -> updateContentState(state, action.sessionId) {
+ it.copy(appIntent = action.appIntent)
+ }
+ is ContentAction.ConsumeAppIntentAction -> updateContentState(state, action.sessionId) {
+ it.copy(appIntent = null)
+ }
+ is ContentAction.UpdateExpandedToolbarStateAction -> updateContentState(state, action.sessionId) {
+ it.copy(showToolbarAsExpanded = action.expanded)
+ }
+ is ContentAction.UpdateHasFormDataAction -> updateContentState(state, action.tabId) {
+ it.copy(hasFormData = action.containsFormData)
+ }
+ is ContentAction.UpdatePriorityToDefaultAfterTimeoutAction,
+ is ContentAction.CheckForFormDataExceptionAction,
+ -> {
+ throw IllegalStateException("You need to add SessionPrioritizationMiddleware. ($action)")
+ }
+ is ContentAction.UpdateProductUrlStateAction -> {
+ updateContentState(state, action.tabId) {
+ if (it.private) {
+ it
+ } else {
+ it.copy(isProductUrl = action.isProductUrl)
+ }
+ }
+ }
+ }
+ }
+ private fun consumeDownload(state: BrowserState, sessionId: String, downloadId: String): BrowserState {
+ return updateContentState(state, sessionId) {
+ if (it.download != null && it.download.id == downloadId) {
+ it.copy(download = null)
+ } else {
+ it
+ }
+ }
+ }
+}
+
+private inline fun updatePermissionHighlightsState(
+ state: BrowserState,
+ tabId: String,
+ crossinline update: (PermissionHighlightsState) -> PermissionHighlightsState,
+): BrowserState {
+ return updateContentState(state, tabId) {
+ it.copy(permissionHighlights = update(it.permissionHighlights))
+ }
+}
+
+private inline fun updateContentState(
+ state: BrowserState,
+ tabId: String,
+ crossinline update: (ContentState) -> ContentState,
+): BrowserState {
+ return state.updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(content = update(current.content))
+ }
+}
+
+private fun isHostEquals(sessionUrl: String, newUrl: String): Boolean {
+ val sessionUri = Uri.parse(sessionUrl)
+ val newUri = Uri.parse(newUrl)
+
+ return sessionUri.sameSchemeAndHostAs(newUri)
+}
+
+private fun isUrlSame(originalUrl: String, newUrl: String): Boolean {
+ val originalUri = Uri.parse(originalUrl)
+ val uri = Uri.parse(newUrl)
+
+ return uri.port == originalUri.port &&
+ uri.host == originalUri.host &&
+ uri.path?.trimStart('/') == originalUri.path?.trimStart('/') &&
+ uri.query == originalUri.query
+}
+
+/**
+ * Checks that the [newUrl] is in scope of the web app manifest.
+ *
+ * https://www.w3.org/TR/appmanifest/#dfn-within-scope
+ */
+private fun isInScope(manifest: WebAppManifest?, newUrl: String): Boolean {
+ val scope = manifest?.scope ?: manifest?.startUrl ?: return false
+ val scopeUri = Uri.parse(scope)
+ val newUri = Uri.parse(newUrl)
+
+ return newUri.isInScope(listOf(scopeUri))
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CookieBannerStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CookieBannerStateReducer.kt
new file mode 100644
index 0000000000..3bf8eba98f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CookieBannerStateReducer.kt
@@ -0,0 +1,17 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.CookieBannerAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object CookieBannerStateReducer {
+
+ fun reduce(state: BrowserState, action: CookieBannerAction): BrowserState = when (action) {
+ is CookieBannerAction.UpdateStatusAction -> state.updateTabOrCustomTabState(action.tabId) { current ->
+ current.createCopy(cookieBanner = action.status)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducer.kt
new file mode 100644
index 0000000000..4eb1df3427
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducer.kt
@@ -0,0 +1,28 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.CopyInternetResourceAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object CopyInternetResourceStateReducer {
+ fun reduce(state: BrowserState, action: CopyInternetResourceAction): BrowserState {
+ return when (action) {
+ is CopyInternetResourceAction.AddCopyAction -> updateTheContentState(
+ state,
+ action.tabId,
+ ) {
+ it.copy(copy = action.internetResource)
+ }
+
+ is CopyInternetResourceAction.ConsumeCopyAction -> updateTheContentState(
+ state,
+ action.tabId,
+ ) {
+ it.copy(copy = null)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt
new file mode 100644
index 0000000000..fd3d543a5c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt
@@ -0,0 +1,32 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object CrashReducer {
+ /**
+ * [CrashAction] Reducer function for modifying [BrowserState].
+ */
+ fun reduce(state: BrowserState, action: CrashAction): BrowserState = when (action) {
+ is CrashAction.SessionCrashedAction -> state.updateTabOrCustomTabState(action.tabId) { tab ->
+ tab.createCopy(
+ engineState = tab.engineState.copy(
+ crashed = true,
+ ),
+ )
+ }
+ is CrashAction.RestoreCrashedSessionAction -> state.updateTabOrCustomTabState(action.tabId) { tab ->
+ // We only update the flag in the reducer. With that the next action trying to get
+ // the engine session will automatically restore this tab lazily.
+ tab.createCopy(
+ engineState = tab.engineState.copy(
+ crashed = false,
+ ),
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt
new file mode 100644
index 0000000000..183812b3ed
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt
@@ -0,0 +1,42 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.ext.toTab
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.state.BrowserState
+
+internal object CustomTabListReducer {
+ /**
+ * [CustomTabListAction] Reducer function for modifying [BrowserState.customTabs].
+ */
+ fun reduce(state: BrowserState, action: CustomTabListAction): BrowserState {
+ return when (action) {
+ is CustomTabListAction.AddCustomTabAction -> state.copy(customTabs = state.customTabs + action.tab)
+
+ is CustomTabListAction.RemoveCustomTabAction -> {
+ state.copy(customTabs = state.customTabs.filter { it.id != action.tabId })
+ }
+
+ is CustomTabListAction.RemoveAllCustomTabsAction -> {
+ state.copy(customTabs = emptyList())
+ }
+
+ is CustomTabListAction.TurnCustomTabIntoNormalTabAction -> {
+ val customTab = state.findCustomTab(action.tabId)
+ if (customTab == null) {
+ state
+ } else {
+ val tab = customTab.toTab()
+ state.copy(
+ customTabs = state.customTabs - customTab,
+ tabs = state.tabs + tab,
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DebugReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DebugReducer.kt
new file mode 100644
index 0000000000..974917823b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DebugReducer.kt
@@ -0,0 +1,24 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.DebugAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.lib.state.DelicateAction
+
+internal object DebugReducer {
+ /**
+ * [DebugAction] Reducer function for modifying internal state for debugging purposes only.
+ */
+ @OptIn(DelicateAction::class)
+ fun reduce(state: BrowserState, action: DebugAction): BrowserState {
+ return when (action) {
+ is DebugAction.UpdateCreatedAtAction ->
+ state.updateTabState(action.tabId) { sessionState ->
+ sessionState.copy(createdAt = action.createdAt)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DownloadStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DownloadStateReducer.kt
new file mode 100644
index 0000000000..aa066f4a7b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DownloadStateReducer.kt
@@ -0,0 +1,43 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.content.DownloadState
+
+internal object DownloadStateReducer {
+
+ /**
+ * [DownloadAction] Reducer function for modifying [BrowserState.downloads].
+ */
+ fun reduce(state: BrowserState, action: DownloadAction): BrowserState {
+ return when (action) {
+ is DownloadAction.AddDownloadAction -> updateDownloads(state, action.download)
+ is DownloadAction.UpdateDownloadAction -> {
+ updateDownloads(state, action.download)
+ }
+ is DownloadAction.DismissDownloadNotificationAction -> {
+ val download = state.downloads[action.downloadId]
+ if (download != null) {
+ updateDownloads(state, download.copy(notificationId = null))
+ } else {
+ state
+ }
+ }
+ is DownloadAction.RemoveDownloadAction -> {
+ state.copy(downloads = state.downloads - action.downloadId)
+ }
+ is DownloadAction.RemoveAllDownloadsAction -> {
+ state.copy(downloads = emptyMap())
+ }
+ is DownloadAction.RestoreDownloadsStateAction -> state
+ is DownloadAction.RestoreDownloadStateAction -> updateDownloads(state, action.download)
+ }
+ }
+
+ private fun updateDownloads(state: BrowserState, download: DownloadState) =
+ state.copy(downloads = state.downloads + (download.id to download))
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt
new file mode 100644
index 0000000000..c1deac4384
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt
@@ -0,0 +1,110 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.SessionState
+
+internal object EngineStateReducer {
+ /**
+ * [EngineAction] Reducer function for modifying a specific [EngineState]
+ * of a [SessionState].
+ */
+ fun reduce(state: BrowserState, action: EngineAction): BrowserState = when (action) {
+ is EngineAction.LinkEngineSessionAction -> state.copyWithEngineState(action.tabId) {
+ it.copy(
+ engineSession = action.engineSession,
+ timestamp = action.timestamp,
+ )
+ }
+ is EngineAction.UnlinkEngineSessionAction -> state.copyWithEngineState(action.tabId) {
+ it.copy(
+ engineSession = null,
+ engineObserver = null,
+ )
+ }
+ is EngineAction.UpdateEngineSessionObserverAction -> state.copyWithEngineState(action.tabId) {
+ it.copy(engineObserver = action.engineSessionObserver)
+ }
+ is EngineAction.UpdateEngineSessionStateAction -> state.copyWithEngineState(action.tabId) { engineState ->
+ if (engineState.crashed) {
+ // We ignore state updates for a crashed engine session. We want to keep the last state until
+ // this tab gets restored (or closed).
+ engineState
+ } else {
+ engineState.copy(engineSessionState = action.engineSessionState)
+ }
+ }
+ is EngineAction.UpdateEngineSessionInitializingAction -> state.copyWithEngineState(action.tabId) {
+ it.copy(initializing = action.initializing)
+ }
+ is EngineAction.OptimizedLoadUrlTriggeredAction -> {
+ state
+ }
+ is EngineAction.SaveToPdfExceptionAction,
+ is EngineAction.SaveToPdfCompleteAction,
+ -> {
+ throw IllegalStateException(
+ "You need to add a middleware to handle this action in your BrowserStore. ($action)",
+ )
+ }
+ is EngineAction.SuspendEngineSessionAction,
+ is EngineAction.CreateEngineSessionAction,
+ is EngineAction.LoadDataAction,
+ is EngineAction.LoadUrlAction,
+ is EngineAction.ReloadAction,
+ is EngineAction.GoBackAction,
+ is EngineAction.GoForwardAction,
+ is EngineAction.GoToHistoryIndexAction,
+ is EngineAction.ToggleDesktopModeAction,
+ is EngineAction.ExitFullScreenModeAction,
+ is EngineAction.SaveToPdfAction,
+ is EngineAction.PrintContentAction,
+ is EngineAction.PrintContentCompletedAction,
+ is EngineAction.PrintContentExceptionAction,
+ is EngineAction.KillEngineSessionAction,
+ is EngineAction.ClearDataAction,
+ -> {
+ throw IllegalStateException("You need to add EngineMiddleware to your BrowserStore. ($action)")
+ }
+ is EngineAction.PurgeHistoryAction -> {
+ state.copy(
+ tabs = purgeEngineStates(state.tabs),
+ customTabs = purgeEngineStates(state.customTabs),
+ )
+ }
+ }
+}
+
+private inline fun BrowserState.copyWithEngineState(
+ tabId: String,
+ crossinline update: (EngineState) -> EngineState,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(engineState = update(current.engineState))
+ }
+}
+
+/**
+ * When `PurgeHistoryAction` gets dispatched `EngineDelegateMiddleware` will take care of calling
+ * `purgeHistory()` on all `EngineSession` instances. However some tabs may not have an `EngineSession`
+ * assigned (yet), instead we keep track of the `EngineSessionState` to restore when needed. Creating
+ * an `EngineSession` for every tab, just to call `purgeHistory()` on them, is wasteful and may cause
+ * problems if there are a lot of tabs. So instead we just remove the EngineSessionState from those
+ * sessions. The next time they get rendered we will only load the assigned URL and since they have
+ * no state to restore, they will have no history.
+ */
+@Suppress("UNCHECKED_CAST")
+private fun <T : SessionState> purgeEngineStates(tabs: List<T>): List<T> {
+ return tabs.map { session ->
+ if (session.engineState.engineSession == null && session.engineState.engineSessionState != null) {
+ session.createCopy(engineState = session.engineState.copy(engineSessionState = null))
+ } else {
+ session
+ } as T
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducer.kt
new file mode 100644
index 0000000000..873e06dc83
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducer.kt
@@ -0,0 +1,17 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.ExtensionsProcessAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object ExtensionsProcessStateReducer {
+
+ fun reduce(state: BrowserState, action: ExtensionsProcessAction): BrowserState = when (action) {
+ is ExtensionsProcessAction.ShowPromptAction -> state.copy(showExtensionsProcessDisabledPrompt = action.show)
+ is ExtensionsProcessAction.DisabledAction -> state.copy(extensionsProcessDisabled = true)
+ is ExtensionsProcessAction.EnabledAction -> state.copy(extensionsProcessDisabled = false)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/HistoryMetadataReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/HistoryMetadataReducer.kt
new file mode 100644
index 0000000000..a31065df03
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/HistoryMetadataReducer.kt
@@ -0,0 +1,50 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.HistoryMetadataAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.concept.storage.HistoryMetadataKey
+
+internal object HistoryMetadataReducer {
+
+ /**
+ * [HistoryMetadataAction] reducer function for modifying [TabSessionState.historyMetadata].
+ */
+ fun reduce(state: BrowserState, action: HistoryMetadataAction): BrowserState {
+ return when (action) {
+ is HistoryMetadataAction.SetHistoryMetadataKeyAction ->
+ state.updateHistoryMetadataKey(action.tabId, action.historyMetadataKey)
+
+ is HistoryMetadataAction.DisbandSearchGroupAction ->
+ state.disbandSearchGroup(action.searchTerm)
+ }
+ }
+}
+
+private fun BrowserState.updateHistoryMetadataKey(
+ tabId: String,
+ key: HistoryMetadataKey,
+): BrowserState {
+ return copy(
+ tabs = tabs.updateTabs(tabId) { current ->
+ current.copy(historyMetadata = key)
+ } ?: tabs,
+ )
+}
+
+private fun BrowserState.disbandSearchGroup(searchTerm: String): BrowserState {
+ val searchTermLowerCase = searchTerm.lowercase()
+ return copy(
+ tabs = tabs.map { tab ->
+ if (tab.historyMetadata?.searchTerm?.lowercase() == searchTermLowerCase) {
+ tab.copy(historyMetadata = HistoryMetadataKey(url = tab.historyMetadata.url))
+ } else {
+ tab
+ }
+ },
+ )
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtils.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtils.kt
new file mode 100644
index 0000000000..d1597afd63
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtils.kt
@@ -0,0 +1,18 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+
+internal inline fun updateTheContentState(
+ state: BrowserState,
+ tabId: String,
+ crossinline update: (ContentState) -> ContentState,
+): BrowserState {
+ return state.updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(content = update(current.content))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LastAccessReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LastAccessReducer.kt
new file mode 100644
index 0000000000..6f4de9db02
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LastAccessReducer.kt
@@ -0,0 +1,45 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.LastAccessAction
+import mozilla.components.browser.state.action.LastAccessAction.ResetLastMediaSessionAction
+import mozilla.components.browser.state.action.LastAccessAction.UpdateLastAccessAction
+import mozilla.components.browser.state.action.LastAccessAction.UpdateLastMediaAccessAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+
+internal object LastAccessReducer {
+ /**
+ * [LastAccessAction] Reducer function for modifying [TabSessionState.lastAccess] state.
+ */
+ fun reduce(state: BrowserState, action: LastAccessAction): BrowserState = when (action) {
+ is UpdateLastAccessAction -> {
+ state.updateTabState(action.tabId) { sessionState ->
+ sessionState.copy(lastAccess = action.lastAccess)
+ }
+ }
+ is UpdateLastMediaAccessAction -> {
+ state.updateTabState(action.tabId) { sessionState ->
+ sessionState.copy(
+ lastMediaAccessState = sessionState.lastMediaAccessState.copy(
+ lastMediaUrl = sessionState.content.url,
+ lastMediaAccess = action.lastMediaAccess,
+ mediaSessionActive = true,
+ ),
+ )
+ }
+ }
+ is ResetLastMediaSessionAction -> {
+ state.updateTabState(action.tabId) { sessionState ->
+ sessionState.copy(
+ lastMediaAccessState = sessionState.lastMediaAccessState.copy(
+ mediaSessionActive = false,
+ ),
+ )
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LocaleStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LocaleStateReducer.kt
new file mode 100644
index 0000000000..ea0803eab0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LocaleStateReducer.kt
@@ -0,0 +1,21 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.LocaleAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object LocaleStateReducer {
+
+ /**
+ * [LocaleAction] Reducer function for modifying [BrowserState.locale].
+ */
+ fun reduce(state: BrowserState, action: LocaleAction): BrowserState {
+ return when (action) {
+ is LocaleAction.UpdateLocaleAction -> state.copy(locale = action.locale)
+ is LocaleAction.RestoreLocaleStateAction -> state
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/MediaSessionReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/MediaSessionReducer.kt
new file mode 100644
index 0000000000..60ede123d3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/MediaSessionReducer.kt
@@ -0,0 +1,142 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.mediasession.MediaSession
+
+internal object MediaSessionReducer {
+ /**
+ * [MediaSessionAction] Reducer function for modifying the [MediaSessionState] of a [SessionState].
+ */
+ fun reduce(state: BrowserState, action: MediaSessionAction): BrowserState {
+ return when (action) {
+ is MediaSessionAction.ActivatedMediaSessionAction ->
+ state.addMediaSession(action.tabId, action.mediaSessionController)
+ is MediaSessionAction.DeactivatedMediaSessionAction ->
+ state.removeMediaSession(action.tabId)
+ is MediaSessionAction.UpdateMediaMetadataAction ->
+ state.updateMediaMetadata(action.tabId, action.metadata)
+ is MediaSessionAction.UpdateMediaPlaybackStateAction ->
+ state.updatePlaybackState(action.tabId, action.playbackState)
+ is MediaSessionAction.UpdateMediaFeatureAction ->
+ state.updateMediaFeature(action.tabId, action.features)
+ is MediaSessionAction.UpdateMediaPositionStateAction ->
+ state.updatePositionState(action.tabId, action.positionState)
+ is MediaSessionAction.UpdateMediaMutedAction ->
+ state.updateMuted(action.tabId, action.muted)
+ is MediaSessionAction.UpdateMediaFullscreenAction ->
+ state.updateFullscreen(
+ action.tabId,
+ action.fullScreen,
+ action.elementMetadata,
+ )
+ }
+ }
+}
+
+private fun BrowserState.addMediaSession(
+ tabId: String,
+ mediaSessionController: MediaSession.Controller,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(
+ mediaSessionState = MediaSessionState(
+ controller = mediaSessionController,
+ ),
+ )
+ }
+}
+
+private fun BrowserState.removeMediaSession(
+ tabId: String,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(mediaSessionState = null)
+ }
+}
+
+private fun BrowserState.updateMediaMetadata(
+ tabId: String,
+ metadata: MediaSession.Metadata,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(
+ mediaSessionState = current.mediaSessionState?.copy(
+ metadata = metadata,
+ ),
+ )
+ }
+}
+
+private fun BrowserState.updatePlaybackState(
+ tabId: String,
+ playbackState: MediaSession.PlaybackState,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(
+ mediaSessionState = current.mediaSessionState?.copy(
+ playbackState = playbackState,
+ ),
+ )
+ }
+}
+
+private fun BrowserState.updateMediaFeature(
+ tabId: String,
+ features: MediaSession.Feature,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(
+ mediaSessionState = current.mediaSessionState?.copy(
+ features = features,
+ ),
+ )
+ }
+}
+
+private fun BrowserState.updatePositionState(
+ tabId: String,
+ positionState: MediaSession.PositionState,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(
+ mediaSessionState = current.mediaSessionState?.copy(
+ positionState = positionState,
+ ),
+ )
+ }
+}
+
+private fun BrowserState.updateMuted(
+ tabId: String,
+ muted: Boolean,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(
+ mediaSessionState = current.mediaSessionState?.copy(
+ muted = muted,
+ ),
+ )
+ }
+}
+
+private fun BrowserState.updateFullscreen(
+ tabId: String,
+ fullscreen: Boolean,
+ elementMetadata: MediaSession.ElementMetadata?,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(
+ mediaSessionState = current.mediaSessionState?.copy(
+ fullscreen = fullscreen,
+ elementMetadata = elementMetadata,
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ReaderStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ReaderStateReducer.kt
new file mode 100644
index 0000000000..d304e432e4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ReaderStateReducer.kt
@@ -0,0 +1,59 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.ReaderAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ReaderState
+
+internal object ReaderStateReducer {
+
+ /**
+ * [EngineAction] Reducer function for modifying a specific [ReaderState]
+ * of a [BrowserState].
+ */
+ fun reduce(state: BrowserState, action: ReaderAction): BrowserState = when (action) {
+ is ReaderAction.UpdateReaderableAction -> state.copyWithReaderState(action.tabId) {
+ it.copy(readerable = action.readerable)
+ }
+ is ReaderAction.UpdateReaderActiveAction -> state.copyWithReaderState(action.tabId) {
+ it.copy(active = action.active)
+ }
+ is ReaderAction.UpdateReaderableCheckRequiredAction -> state.copyWithReaderState(action.tabId) {
+ it.copy(checkRequired = action.checkRequired)
+ }
+ is ReaderAction.UpdateReaderConnectRequiredAction -> state.copyWithReaderState(action.tabId) {
+ it.copy(connectRequired = action.connectRequired)
+ }
+ is ReaderAction.UpdateReaderBaseUrlAction -> state.copyWithReaderState(action.tabId) {
+ it.copy(baseUrl = action.baseUrl)
+ }
+ is ReaderAction.UpdateReaderActiveUrlAction -> state.copyWithReaderState(action.tabId) {
+ it.copy(activeUrl = action.activeUrl)
+ }
+ is ReaderAction.ClearReaderActiveUrlAction -> state.copyWithReaderState(action.tabId) {
+ it.copy(activeUrl = null)
+ }
+ is ReaderAction.UpdateReaderScrollYAction -> state.copyWithReaderState(action.tabId) {
+ if (it.active) {
+ it.copy(scrollY = action.scrollY)
+ } else {
+ it
+ }
+ }
+ }
+}
+
+private fun BrowserState.copyWithReaderState(
+ tabId: String,
+ update: (ReaderState) -> ReaderState,
+): BrowserState {
+ return copy(
+ tabs = tabs.updateTabs(tabId) { current ->
+ current.copy(readerState = update.invoke(current.readerState))
+ } ?: tabs,
+ )
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/RecentlyClosedReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/RecentlyClosedReducer.kt
new file mode 100644
index 0000000000..e35211dcd1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/RecentlyClosedReducer.kt
@@ -0,0 +1,35 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.RecentlyClosedAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object RecentlyClosedReducer {
+ fun reduce(state: BrowserState, action: RecentlyClosedAction): BrowserState {
+ return when (action) {
+ is RecentlyClosedAction.AddClosedTabsAction -> {
+ state.copy(
+ closedTabs = state.closedTabs + action.tabs.map { it.state },
+ )
+ }
+ is RecentlyClosedAction.PruneClosedTabsAction -> {
+ state.copy(
+ closedTabs = state.closedTabs.sortedByDescending { it.lastAccess }
+ .take(action.maxTabs),
+ )
+ }
+ is RecentlyClosedAction.ReplaceTabsAction -> state.copy(closedTabs = action.tabs)
+ is RecentlyClosedAction.RemoveClosedTabAction -> {
+ state.copy(
+ closedTabs = state.closedTabs - action.tab,
+ )
+ }
+ is RecentlyClosedAction.RemoveAllClosedTabAction -> {
+ state.copy(closedTabs = listOf())
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SearchReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SearchReducer.kt
new file mode 100644
index 0000000000..1c056888c7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SearchReducer.kt
@@ -0,0 +1,206 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SearchState
+
+internal object SearchReducer {
+ /**
+ * [SearchAction] Reducer function for modifying [SearchState].
+ */
+ fun reduce(state: BrowserState, action: SearchAction): BrowserState {
+ return when (action) {
+ is SearchAction.RefreshSearchEnginesAction -> state // This is handled in [RegionMiddleware].
+ is SearchAction.SetSearchEnginesAction -> state.setSearchEngines(action)
+ is SearchAction.SetRegionAction -> state.setRegion(action)
+ is SearchAction.UpdateCustomSearchEngineAction -> state.updateCustomSearchEngine(action)
+ is SearchAction.RemoveCustomSearchEngineAction -> state.removeSearchEngine(action)
+ is SearchAction.SelectSearchEngineAction -> state.selectSearchEngine(action)
+ is SearchAction.ShowSearchEngineAction -> state.maybeShowSearchEngine(action)
+ is SearchAction.HideSearchEngineAction -> state.hideSearchEngine(action)
+ is SearchAction.AddAdditionalSearchEngineAction -> state.addAdditionalSearchEngine(action)
+ is SearchAction.RemoveAdditionalSearchEngineAction -> state.removeAdditionalSearchEngine(action)
+ is SearchAction.UpdateDisabledSearchEngineIdsAction ->
+ state.updateDisabledSearchEngineIds(action)
+ is SearchAction.RestoreHiddenSearchEnginesAction -> state.restoreHiddenSearchEngines()
+ }
+ }
+}
+
+private fun BrowserState.setSearchEngines(
+ action: SearchAction.SetSearchEnginesAction,
+): BrowserState {
+ return copy(
+ search = search.copy(
+ regionSearchEngines = action.regionSearchEngines,
+ customSearchEngines = action.customSearchEngines,
+ userSelectedSearchEngineId = action.userSelectedSearchEngineId,
+ userSelectedSearchEngineName = action.userSelectedSearchEngineName,
+ regionDefaultSearchEngineId = action.regionDefaultSearchEngineId,
+ hiddenSearchEngines = action.hiddenSearchEngines,
+ disabledSearchEngineIds = action.disabledSearchEngineIds,
+ additionalSearchEngines = action.additionalSearchEngines,
+ additionalAvailableSearchEngines = action.additionalAvailableSearchEngines,
+ regionSearchEnginesOrder = action.regionSearchEnginesOrder,
+ complete = true,
+ ),
+ )
+}
+
+private fun BrowserState.setRegion(
+ action: SearchAction.SetRegionAction,
+): BrowserState {
+ return copy(
+ search = search.copy(
+ region = action.regionState,
+ ),
+ )
+}
+
+private fun BrowserState.updateCustomSearchEngine(
+ action: SearchAction.UpdateCustomSearchEngineAction,
+): BrowserState {
+ val searchEngines = search.customSearchEngines
+ val index = searchEngines.indexOfFirst { searchEngine -> searchEngine.id == action.searchEngine.id }
+
+ val updatedSearchEngines = if (index != -1) {
+ searchEngines.subList(0, index) + action.searchEngine + searchEngines.subList(index + 1, searchEngines.size)
+ } else {
+ searchEngines + action.searchEngine
+ }
+
+ return copy(
+ search = search.copy(
+ customSearchEngines = updatedSearchEngines,
+ ),
+ )
+}
+
+private fun BrowserState.removeSearchEngine(
+ action: SearchAction.RemoveCustomSearchEngineAction,
+): BrowserState {
+ return copy(
+ search = search.copy(
+ customSearchEngines = search.customSearchEngines.filter { it.id != action.searchEngineId },
+ ),
+ )
+}
+
+private fun BrowserState.selectSearchEngine(
+ action: SearchAction.SelectSearchEngineAction,
+): BrowserState {
+ // We allow setting an ID of a search engine that is not in the state since loading the search
+ // engines may happen asynchronously and the search engine may not be loaded yet at this point.
+ return copy(
+ search = search.copy(
+ userSelectedSearchEngineId = action.searchEngineId,
+ userSelectedSearchEngineName = action.searchEngineName,
+ ),
+ )
+}
+
+private fun BrowserState.maybeShowSearchEngine(
+ action: SearchAction.ShowSearchEngineAction,
+): BrowserState {
+ val searchEngine = search.hiddenSearchEngines.find { searchEngine -> searchEngine.id == action.searchEngineId }
+ return if (searchEngine != null) {
+ return showSearchEngine(searchEngine)
+ } else {
+ this
+ }
+}
+
+private fun BrowserState.showSearchEngine(
+ searchEngine: SearchEngine,
+): BrowserState {
+ return copy(
+ search = search.copy(
+ hiddenSearchEngines = search.hiddenSearchEngines - searchEngine,
+ regionSearchEngines = (search.regionSearchEngines + searchEngine).sortedBy {
+ search.regionSearchEnginesOrder.indexOf(it.id)
+ },
+ ),
+ )
+}
+
+private fun BrowserState.hideSearchEngine(
+ action: SearchAction.HideSearchEngineAction,
+): BrowserState {
+ val searchEngine = search.regionSearchEngines.find { searchEngine -> searchEngine.id == action.searchEngineId }
+
+ return if (searchEngine != null) {
+ copy(
+ search = search.copy(
+ regionSearchEngines = search.regionSearchEngines - searchEngine,
+ hiddenSearchEngines = search.hiddenSearchEngines + searchEngine,
+ ),
+ )
+ } else {
+ this
+ }
+}
+
+private fun BrowserState.addAdditionalSearchEngine(
+ action: SearchAction.AddAdditionalSearchEngineAction,
+): BrowserState {
+ val searchEngine = search.additionalAvailableSearchEngines.find { searchEngine ->
+ searchEngine.id == action.searchEngineId
+ }
+
+ return if (searchEngine != null) {
+ copy(
+ search = search.copy(
+ additionalSearchEngines = search.additionalSearchEngines + searchEngine,
+ additionalAvailableSearchEngines = search.additionalAvailableSearchEngines - searchEngine,
+ ),
+ )
+ } else {
+ this
+ }
+}
+
+private fun BrowserState.removeAdditionalSearchEngine(
+ action: SearchAction.RemoveAdditionalSearchEngineAction,
+): BrowserState {
+ val searchEngine = search.additionalSearchEngines.find { searchEngine ->
+ searchEngine.id == action.searchEngineId
+ }
+
+ return if (searchEngine != null) {
+ copy(
+ search = search.copy(
+ additionalAvailableSearchEngines = search.additionalAvailableSearchEngines + searchEngine,
+ additionalSearchEngines = search.additionalSearchEngines - searchEngine,
+ ),
+ )
+ } else {
+ this
+ }
+}
+
+private fun BrowserState.updateDisabledSearchEngineIds(
+ action: SearchAction.UpdateDisabledSearchEngineIdsAction,
+): BrowserState {
+ val updatedDisabledSearchEngineShortcutIds = if (action.isEnabled) {
+ search.disabledSearchEngineIds - action.searchEngineId
+ } else {
+ search.disabledSearchEngineIds + action.searchEngineId
+ }
+
+ return copy(
+ search = search.copy(
+ disabledSearchEngineIds = updatedDisabledSearchEngineShortcutIds,
+ ),
+ )
+}
+
+private fun BrowserState.restoreHiddenSearchEngines(): BrowserState {
+ return search.hiddenSearchEngines.fold(this) { state, engine ->
+ state.showSearchEngine(engine)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducer.kt
new file mode 100644
index 0000000000..c96f982c0d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducer.kt
@@ -0,0 +1,21 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.ShareInternetResourceAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object ShareInternetResourceStateReducer {
+ fun reduce(state: BrowserState, action: ShareInternetResourceAction): BrowserState {
+ return when (action) {
+ is ShareInternetResourceAction.AddShareAction -> updateTheContentState(state, action.tabId) {
+ it.copy(share = action.internetResource)
+ }
+ is ShareInternetResourceAction.ConsumeShareAction -> updateTheContentState(state, action.tabId) {
+ it.copy(share = null)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt
new file mode 100644
index 0000000000..94aa209453
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt
@@ -0,0 +1,23 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.SystemAction
+import mozilla.components.browser.state.state.BrowserState
+
+internal object SystemReducer {
+ /**
+ * [SystemAction] Reducer function for modifying [BrowserState].
+ */
+ fun reduce(state: BrowserState, action: SystemAction): BrowserState {
+ return when (action) {
+ is SystemAction.LowMemoryAction -> {
+ // Do nothing for now; our previous implementation here isn't currently needed.
+ // This reducer has value for future actions.
+ state
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabGroupReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabGroupReducer.kt
new file mode 100644
index 0000000000..d8e7607fe0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabGroupReducer.kt
@@ -0,0 +1,169 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.TabGroupAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabGroup
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.getGroupById
+
+internal object TabGroupReducer {
+
+ /**
+ * [TabGroupAction] reducer function for modifying tab groups in [BrowserState.tabPartitions].
+ */
+ fun reduce(state: BrowserState, action: TabGroupAction): BrowserState {
+ return when (action) {
+ is TabGroupAction.AddTabGroupAction -> {
+ action.group.tabIds.forEach { state.assertTabExists(it) }
+ state.addTabGroup(action.partition, action.group)
+ }
+
+ is TabGroupAction.RemoveTabGroupAction -> {
+ state.removeTabGroup(action.partition, action.group)
+ }
+
+ is TabGroupAction.AddTabAction -> {
+ state.assertTabExists(action.tabId)
+
+ if (!state.groupExists(action.partition, action.group)) {
+ state.addTabGroup(action.partition, TabGroup(action.group, tabIds = listOf(action.tabId)))
+ } else {
+ state.updateTabGroup(action.partition, action.group) {
+ it.copy(tabIds = (it.tabIds + action.tabId).distinct())
+ }
+ }
+ }
+
+ is TabGroupAction.AddTabsAction -> {
+ action.tabIds.forEach { state.assertTabExists(it) }
+
+ if (!state.groupExists(action.partition, action.group)) {
+ state.addTabGroup(action.partition, TabGroup(action.group, tabIds = action.tabIds.distinct()))
+ } else {
+ state.updateTabGroup(action.partition, action.group) {
+ it.copy(tabIds = (it.tabIds + action.tabIds).distinct())
+ }
+ }
+ }
+
+ is TabGroupAction.RemoveTabAction -> {
+ state.updateTabGroup(action.partition, action.group) {
+ it.copy(tabIds = it.tabIds - action.tabId)
+ }
+ }
+
+ is TabGroupAction.RemoveTabsAction -> {
+ state.updateTabGroup(action.partition, action.group) {
+ it.copy(tabIds = it.tabIds - action.tabIds)
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Adds the provided tab group and creates the partition if needed.
+ */
+private fun BrowserState.addTabGroup(partitionId: String, group: TabGroup): BrowserState {
+ val partition = tabPartitions[partitionId]
+ val updatedPartition = if (partition != null) {
+ if (partition.getGroupById(group.id) != null) {
+ throw IllegalArgumentException("Tab group with same ID already exists")
+ }
+ partition.copy(tabGroups = partition.tabGroups + group)
+ } else {
+ TabPartition(partitionId, tabGroups = listOf(group))
+ }
+ return copy(tabPartitions = tabPartitions + (partitionId to updatedPartition))
+}
+
+/**
+ * Removes a tab group from the provided partition.
+ */
+private fun BrowserState.removeTabGroup(partitionId: String, groupId: String): BrowserState {
+ val partition = tabPartitions[partitionId]
+ val group = partition?.getGroupById(groupId)
+ return if (group != null) {
+ val updatedPartition = partition.copy(tabGroups = partition.tabGroups - group)
+ if (updatedPartition.tabGroups.isEmpty()) {
+ copy(tabPartitions = tabPartitions - partitionId)
+ } else {
+ copy(tabPartitions = tabPartitions + (partitionId to updatedPartition))
+ }
+ } else {
+ this
+ }
+}
+
+/**
+ * Checks if a tab group exists in the provided partition.
+ */
+private fun BrowserState.groupExists(partitionId: String, groupId: String): Boolean {
+ return tabPartitions[partitionId]?.getGroupById(groupId) != null
+}
+
+/**
+ * Checks that the provided tab exists and throws an
+ * [IllegalArgumentException] otherwise.
+ *
+ * @param tabId the id of the [TabSessionState] to check.
+ */
+private fun BrowserState.assertTabExists(tabId: String) {
+ require(tabs.find { it.id == tabId } != null) {
+ "Tab does not exist"
+ }
+}
+
+/**
+ * Utility function to update a [TabGroup] within a [TabPartition] in [BrowserState].
+ */
+private fun BrowserState.updateTabGroup(
+ partitionId: String,
+ groupId: String,
+ update: (TabGroup) -> TabGroup,
+): BrowserState {
+ return updateTabPartition(partitionId) { partition ->
+ partition.updateTabGroup(groupId, update)
+ }
+}
+
+/**
+ * Updates the specified tab partition by invoking [update].
+ */
+private inline fun BrowserState.updateTabPartition(
+ partitionId: String,
+ crossinline update: (TabPartition) -> TabPartition,
+): BrowserState {
+ val partition = tabPartitions[partitionId] ?: return this
+ return copy(tabPartitions = tabPartitions + (partitionId to update(partition)))
+}
+
+/**
+ * Updates the specified tab group within this partition by invoking [update].
+ */
+private inline fun TabPartition.updateTabGroup(
+ groupId: String,
+ crossinline update: (TabGroup) -> TabGroup,
+): TabPartition {
+ return tabGroups.update(groupId, update)?.let {
+ copy(tabGroups = it)
+ } ?: this
+}
+
+/**
+ * Updates the provided tab group by invoking [update].
+ */
+private inline fun List<TabGroup>.update(
+ groupId: String,
+ crossinline update: (TabGroup) -> TabGroup,
+): List<TabGroup>? {
+ val groupIndex = indexOfFirst { it.id == groupId }
+ if (groupIndex == -1) return null
+
+ return subList(0, groupIndex) + update(get(groupIndex)) + subList(groupIndex + 1, size)
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt
new file mode 100644
index 0000000000..74a866a238
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt
@@ -0,0 +1,358 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.recover.toTabSessionStates
+import kotlin.math.max
+
+internal object TabListReducer {
+ /**
+ * [TabListAction] Reducer function for modifying the list of [TabSessionState] objects in [BrowserState.tabs].
+ */
+ @Suppress("LongMethod")
+ fun reduce(state: BrowserState, action: TabListAction): BrowserState {
+ return when (action) {
+ is TabListAction.AddTabAction -> {
+ // Verify that tab doesn't already exist
+ requireUniqueTab(state, action.tab)
+
+ val updatedTabList = if (action.tab.parentId != null) {
+ val parentIndex = state.tabs.indexOfFirst { it.id == action.tab.parentId }
+ if (parentIndex == -1) {
+ throw IllegalArgumentException("The parent does not exist")
+ }
+
+ // Add the child tab next to its parent
+ val childIndex = parentIndex + 1
+ state.tabs.subList(0, childIndex) + action.tab + state.tabs.subList(childIndex, state.tabs.size)
+ } else {
+ state.tabs + action.tab
+ }
+
+ state.copy(
+ tabs = updatedTabList,
+ selectedTabId = if (action.select || state.selectedTabId == null) {
+ action.tab.id
+ } else {
+ state.selectedTabId
+ },
+ )
+ }
+
+ is TabListAction.AddMultipleTabsAction -> {
+ action.tabs.forEach { requireUniqueTab(state, it) }
+
+ action.tabs.find { tab -> tab.parentId != null }?.let {
+ throw IllegalArgumentException("Adding multiple tabs with a parent id is not supported")
+ }
+
+ state.copy(
+ tabs = state.tabs + action.tabs,
+ selectedTabId = if (state.selectedTabId == null) {
+ action.tabs.find { tab -> !tab.content.private }?.id
+ } else {
+ state.selectedTabId
+ },
+ )
+ }
+
+ is TabListAction.MoveTabsAction -> {
+ val tabTarget = state.findTab(action.targetTabId)
+ if (tabTarget == null) {
+ state
+ } else {
+ val targetPosition = state.tabs.indexOf(tabTarget) + (if (action.placeAfter) 1 else 0)
+ val positionOffset = state.tabs.filterIndexed { index, tab ->
+ (index < targetPosition && tab.id in action.tabIds)
+ }.count()
+ val finalPos = targetPosition - positionOffset
+ val (movedTabs, unmovedTabs) = state.tabs.partition { it.id in action.tabIds }
+ val updatedTabList = unmovedTabs.subList(0, finalPos) +
+ movedTabs +
+ unmovedTabs.subList(finalPos, unmovedTabs.size)
+
+ state.copy(tabs = updatedTabList)
+ }
+ }
+
+ is TabListAction.SelectTabAction -> {
+ state.copy(selectedTabId = action.tabId)
+ }
+
+ is TabListAction.RemoveTabAction -> {
+ val tabToRemove = state.findTab(action.tabId)
+
+ if (tabToRemove == null) {
+ state
+ } else {
+ // Remove tab and update child tabs in case their parent was removed
+ val updatedTabList = (state.tabs - tabToRemove).map {
+ if (it.parentId == tabToRemove.id) it.copy(parentId = tabToRemove.parentId) else it
+ }
+
+ val updatedSelection = if (action.selectParentIfExists && tabToRemove.parentId != null) {
+ // The parent tab should be selected if one exists
+ tabToRemove.parentId
+ } else if (state.selectedTabId == tabToRemove.id) {
+ // The selected tab was removed and we need to find a new one
+ val previousIndex = state.tabs.indexOf(tabToRemove)
+ findNewSelectedTabId(updatedTabList, tabToRemove.content.private, previousIndex)
+ } else {
+ // The selected tab is not affected and can stay the same
+ state.selectedTabId
+ }
+
+ state.copy(
+ tabs = updatedTabList,
+ selectedTabId = updatedSelection,
+ tabPartitions = state.tabPartitions.removeTabs(listOf(action.tabId)),
+ )
+ }
+ }
+
+ is TabListAction.RemoveTabsAction -> {
+ val tabsToRemove = action.tabIds.mapNotNull { state.findTab(it) }
+
+ if (tabsToRemove.isNullOrEmpty()) {
+ state
+ } else {
+ // Remove tabs and update child tabs' parentId if their parent is in removed list
+ val updatedTabList = (state.tabs - tabsToRemove).map { tabState ->
+ tabsToRemove.firstOrNull { removedTab -> tabState.parentId == removedTab.id }
+ ?.let { tabState.copy(parentId = findNewParentId(it, tabsToRemove)) }
+ ?: tabState
+ }
+
+ val updatedSelection =
+ if (action.tabIds.contains(state.selectedTabId)) {
+ val removedSelectedTab =
+ tabsToRemove.first { it.id == state.selectedTabId }
+ // The selected tab was removed and we need to find a new one
+ val previousIndex = state.tabs.indexOf(removedSelectedTab)
+ findNewSelectedTabId(
+ updatedTabList,
+ removedSelectedTab.content.private,
+ previousIndex,
+ )
+ } else {
+ // The selected tab is not affected and can stay the same
+ state.selectedTabId
+ }
+
+ state.copy(
+ tabs = updatedTabList,
+ selectedTabId = updatedSelection,
+ tabPartitions = state.tabPartitions.removeTabs(action.tabIds),
+ )
+ }
+ }
+
+ is TabListAction.RestoreAction -> {
+ // Verify that none of the tabs to restore already exist
+ val restoredTabs = action.tabs.toTabSessionStates()
+ restoredTabs.forEach { requireUniqueTab(state, it) }
+
+ // Using the enum, action.restoreLocation, we are adding the restored tabs at
+ // either the beginning of the tab list, the end of the tab list, or at a
+ // specified index (RecoverableTab.index). If the index for some reason is -1,
+ // the tab will be restored to the end of the tab list. Upon restoration, the
+ // index will be reset to -1 when added to the combined list.
+ val combinedTabList: List<TabSessionState> = when (action.restoreLocation) {
+ TabListAction.RestoreAction.RestoreLocation.BEGINNING -> restoredTabs + state.tabs
+ TabListAction.RestoreAction.RestoreLocation.END -> state.tabs + restoredTabs
+ TabListAction.RestoreAction.RestoreLocation.AT_INDEX -> mutableListOf<TabSessionState>().apply {
+ addAll(state.tabs)
+ restoredTabs.forEachIndexed { index, restoredTab ->
+ val tabIndex = action.tabs[index].state.index
+ val restoreIndex =
+ if (tabIndex > size || tabIndex < 0) {
+ size
+ } else {
+ tabIndex
+ }
+ add(restoreIndex, restoredTab)
+ }
+ }
+ }
+
+ state.copy(
+ tabs = combinedTabList,
+ selectedTabId = if (action.selectedTabId != null && state.selectedTabId == null) {
+ // We only want to update the selected tab if none has been already selected. Otherwise we may
+ // switch to a restored tab even though the user is already looking at an existing tab (e.g.
+ // a tab that came from an intent).
+ action.selectedTabId
+ } else {
+ state.selectedTabId
+ },
+ )
+ }
+
+ is TabListAction.RemoveAllTabsAction -> {
+ state.copy(
+ tabs = emptyList(),
+ selectedTabId = null,
+ tabPartitions = state.tabPartitions.removeAllTabs(),
+ )
+ }
+
+ is TabListAction.RemoveAllPrivateTabsAction -> {
+ val selectionAffected = state.selectedTab?.content?.private == true
+ val partition = state.tabs.partition { it.content.private }
+ val normalTabs = partition.second
+ state.copy(
+ tabs = normalTabs,
+ selectedTabId = if (selectionAffected) {
+ // If the selection is affected, we'll set it to null as there's no
+ // normal tab left and NO normal tab should get selected instead.
+ null
+ } else {
+ state.selectedTabId
+ },
+ tabPartitions = state.tabPartitions.removeTabs(partition.first.map { it.id }),
+ )
+ }
+
+ is TabListAction.RemoveAllNormalTabsAction -> {
+ val selectionAffected = state.selectedTab?.content?.private == false
+ val partition = state.tabs.partition { it.content.private }
+ val privateTabs = partition.first
+ state.copy(
+ tabs = privateTabs,
+ selectedTabId = if (selectionAffected) {
+ // If the selection is affected, we'll set it to null as there's no
+ // normal tab left and NO private tab should get selected instead.
+ null
+ } else {
+ state.selectedTabId
+ },
+ tabPartitions = state.tabPartitions.removeTabs(partition.second.map { it.id }),
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Looks for an appropriate new parentId for a tab by checking if the parent is being removed,
+ * and if so, will recursively check the parent's parent and so on.
+ */
+private fun findNewParentId(
+ tabToFindNewParent: TabSessionState,
+ tabsToBeRemoved: List<TabSessionState>,
+): String? {
+ return if (tabsToBeRemoved.map { it.id }.contains(tabToFindNewParent.parentId)) {
+ // The parent tab is being removed, let's check the parent's parent
+ findNewParentId(
+ tabsToBeRemoved.first { tabToFindNewParent.parentId == it.id },
+ tabsToBeRemoved,
+ )
+ } else {
+ tabToFindNewParent.parentId
+ }
+}
+
+/**
+ * Find a new selected tab and return its id after the tab at [previousIndex] was removed.
+ */
+private fun findNewSelectedTabId(
+ tabs: List<TabSessionState>,
+ isPrivate: Boolean,
+ previousIndex: Int,
+): String? {
+ if (tabs.isEmpty()) {
+ // There's no tab left to select.
+ return null
+ }
+
+ val predicate: (TabSessionState) -> Boolean = { tab -> tab.content.private == isPrivate }
+
+ // If the previous index is still a valid index and if this is a private/normal tab we are looking for then
+ // let's use the tab at the same index.
+ if (previousIndex <= tabs.lastIndex && predicate(tabs[previousIndex])) {
+ return tabs[previousIndex].id
+ }
+
+ // Find a tab that matches the predicate and is nearby the tab that was selected previously
+ val nearbyTab = findNearbyTab(tabs, previousIndex, predicate)
+
+ return when {
+ // We found a nearby tab, let's select it.
+ nearbyTab != null -> nearbyTab.id
+
+ // We have run out of tabs of the same type of mode
+ else -> null
+ }
+}
+
+/**
+ * Find a tab that is near the provided [index] and matches the [predicate].
+ */
+private fun findNearbyTab(
+ tabs: List<TabSessionState>,
+ index: Int,
+ predicate: (TabSessionState) -> Boolean,
+): TabSessionState? {
+ val maxSteps = max(tabs.lastIndex - index, index)
+ if (maxSteps < 0) {
+ return null
+ }
+
+ // Try tabs oscillating near the index.
+ for (steps in 1..maxSteps) {
+ listOf(index - steps, index + steps).forEach { current ->
+ if (current in 0..tabs.lastIndex &&
+ predicate(tabs[current])
+ ) {
+ return tabs[current]
+ }
+ }
+ }
+
+ return null
+}
+
+/**
+ * Checks that the provided tab doesn't already exist and throws an
+ * [IllegalArgumentException] otherwise.
+ *
+ * @param state the current [BrowserState] (including all existing tabs).
+ * @param tab the [TabSessionState] to check.
+ */
+private fun requireUniqueTab(state: BrowserState, tab: TabSessionState) {
+ require(state.tabs.find { it.id == tab.id } == null) {
+ "Tab with same ID already exists"
+ }
+}
+
+/**
+ * Removes references to the provided tabs from all [TabPartition]s.
+ */
+private fun Map<String, TabPartition>.removeTabs(removedTabIds: List<String>) =
+ mapValues {
+ val partition = it.value
+ partition.copy(
+ tabGroups = partition.tabGroups.map { group ->
+ group.copy(tabIds = group.tabIds.filterNot { tabId -> removedTabIds.contains(tabId) })
+ },
+ )
+ }
+
+/**
+ * Removes references to the provided tabs from all [TabPartition]s.
+ */
+private fun Map<String, TabPartition>.removeAllTabs() =
+ mapValues {
+ val partition = it.value
+ partition.copy(
+ tabGroups = partition.tabGroups.map { group -> group.copy(tabIds = emptyList()) },
+ )
+ }
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TrackingProtectionStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TrackingProtectionStateReducer.kt
new file mode 100644
index 0000000000..0af56c9b70
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TrackingProtectionStateReducer.kt
@@ -0,0 +1,45 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.TrackingProtectionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TrackingProtectionState
+
+internal object TrackingProtectionStateReducer {
+ /**
+ * [TrackingProtectionAction] Reducer function for modifying a specific [TrackingProtectionState]
+ * of a [SessionState].
+ */
+ fun reduce(state: BrowserState, action: TrackingProtectionAction): BrowserState = when (action) {
+ is TrackingProtectionAction.ToggleAction -> state.copyWithTrackingProtectionState(action.tabId) {
+ it.copy(enabled = action.enabled)
+ }
+ is TrackingProtectionAction.TrackerBlockedAction -> state.copyWithTrackingProtectionState(action.tabId) {
+ it.copy(blockedTrackers = it.blockedTrackers + action.tracker)
+ }
+ is TrackingProtectionAction.TrackerLoadedAction -> state.copyWithTrackingProtectionState(action.tabId) {
+ it.copy(loadedTrackers = it.loadedTrackers + action.tracker)
+ }
+ is TrackingProtectionAction.ClearTrackersAction -> state.copyWithTrackingProtectionState(action.tabId) {
+ it.copy(loadedTrackers = emptyList(), blockedTrackers = emptyList())
+ }
+ is TrackingProtectionAction.ToggleExclusionListAction -> state.copyWithTrackingProtectionState(
+ action.tabId,
+ ) {
+ it.copy(ignoredOnTrackingProtection = action.excluded)
+ }
+ }
+}
+
+private inline fun BrowserState.copyWithTrackingProtectionState(
+ tabId: String,
+ crossinline update: (TrackingProtectionState) -> TrackingProtectionState,
+): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(trackingProtection = update(current.trackingProtection))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt
new file mode 100644
index 0000000000..266964455e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt
@@ -0,0 +1,446 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TranslationsState
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettingOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettings
+
+internal object TranslationsStateReducer {
+
+ /**
+ * Reducer for [BrowserState.translationEngine] and [SessionState.translationsState]
+ */
+ @Suppress("LongMethod")
+ fun reduce(state: BrowserState, action: TranslationsAction): BrowserState = when (action) {
+ TranslationsAction.InitTranslationsBrowserState -> {
+ // No state change on this operation
+ state
+ }
+
+ is TranslationsAction.TranslateExpectedAction -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isExpectedTranslate = true,
+ )
+ }
+ }
+
+ is TranslationsAction.TranslateOfferAction -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isOfferTranslate = action.isOfferTranslate,
+ )
+ }
+ }
+
+ is TranslationsAction.TranslateStateChangeAction -> {
+ var isExpectedTranslate = state.findTab(action.tabId)?.translationsState?.isExpectedTranslate ?: true
+ var isOfferTranslate = state.findTab(action.tabId)?.translationsState?.isOfferTranslate ?: true
+
+ // Checking if a translation can be anticipated or not based on
+ // the new translation engine state detected metadata.
+ if (action.translationEngineState.detectedLanguages == null ||
+ action.translationEngineState.detectedLanguages?.supportedDocumentLang == false ||
+ action.translationEngineState.detectedLanguages?.userPreferredLangTag == null
+ ) {
+ // Value can also update through [TranslateExpectedAction]
+ // via the translations engine.
+ isExpectedTranslate = false
+
+ // Value can also update through [TranslateOfferAction]
+ // via the translations engine.
+ isOfferTranslate = false
+ }
+
+ // Checking for if the translations engine is in the fully translated state or not based
+ // on the values of the translation pair.
+ if (action.translationEngineState.requestedTranslationPair == null ||
+ action.translationEngineState.requestedTranslationPair?.fromLanguage == null ||
+ action.translationEngineState.requestedTranslationPair?.toLanguage == null
+ ) {
+ // In an untranslated state
+ var translationsError: TranslationError? = null
+ if (action.translationEngineState.detectedLanguages?.supportedDocumentLang == false) {
+ translationsError = TranslationError.LanguageNotSupportedError(cause = null)
+ }
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isOfferTranslate = isOfferTranslate,
+ isExpectedTranslate = isExpectedTranslate,
+ isTranslated = false,
+ translationEngineState = action.translationEngineState,
+ translationError = translationsError,
+ )
+ }
+ } else {
+ // In a translated state
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isOfferTranslate = isOfferTranslate,
+ isExpectedTranslate = isExpectedTranslate,
+ isTranslated = true,
+ translationError = null,
+ translationEngineState = action.translationEngineState,
+ )
+ }
+ }
+ }
+
+ is TranslationsAction.TranslateAction ->
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isOfferTranslate = false,
+ isTranslateProcessing = true,
+ )
+ }
+
+ is TranslationsAction.TranslateRestoreAction ->
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(isRestoreProcessing = true)
+ }
+
+ is TranslationsAction.TranslateSuccessAction -> {
+ when (action.operation) {
+ TranslationOperation.TRANSLATE -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isTranslated = true,
+ isTranslateProcessing = false,
+ translationError = null,
+ )
+ }
+ }
+
+ TranslationOperation.RESTORE -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isTranslated = false,
+ isRestoreProcessing = false,
+ translationError = null,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_SUPPORTED_LANGUAGES -> {
+ // Reset the error state, and then generally expect
+ // [TranslationsAction.SetSupportedLanguagesAction] to update state in the
+ // success case.
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ translationError = null,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_LANGUAGE_MODELS -> {
+ // Reset the error state, and then generally expect
+ // [TranslationsAction.SetLanguageModelsAction] to update state in the
+ // success case.
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ translationError = null,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_PAGE_SETTINGS -> {
+ // Reset the error state, and then generally expect
+ // [TranslationsAction.SetPageSettingsAction] to update state in the
+ // success case.
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ settingsError = null,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS -> {
+ state.copy(
+ translationEngine = state.translationEngine.copy(
+ engineError = null,
+ ),
+ )
+ }
+
+ TranslationOperation.FETCH_NEVER_TRANSLATE_SITES -> {
+ // Reset the error state, and then generally expect
+ // [TranslationsAction.SetNeverTranslateSitesAction] to update
+ // state in the success case.
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ neverTranslateSites = null,
+ )
+ }
+ }
+ }
+ }
+
+ is TranslationsAction.TranslateExceptionAction -> {
+ when (action.operation) {
+ TranslationOperation.TRANSLATE -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isTranslateProcessing = false,
+ translationError = action.translationError,
+ )
+ }
+ }
+
+ TranslationOperation.RESTORE -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ isRestoreProcessing = false,
+ translationError = action.translationError,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_SUPPORTED_LANGUAGES -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ translationError = action.translationError,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_LANGUAGE_MODELS -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ translationError = action.translationError,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_PAGE_SETTINGS -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ pageSettings = null,
+ settingsError = action.translationError,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ translationError = action.translationError,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_NEVER_TRANSLATE_SITES -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ neverTranslateSites = null,
+ settingsError = action.translationError,
+ )
+ }
+ }
+ }
+ }
+
+ is TranslationsAction.EngineExceptionAction -> {
+ state.copy(translationEngine = state.translationEngine.copy(engineError = action.error))
+ }
+
+ is TranslationsAction.SetSupportedLanguagesAction ->
+ state.copy(
+ translationEngine = state.translationEngine.copy(
+ supportedLanguages = action.supportedLanguages,
+ engineError = null,
+ ),
+ )
+
+ is TranslationsAction.SetLanguageModelsAction ->
+ state.copy(
+ translationEngine = state.translationEngine.copy(
+ languageModels = action.languageModels,
+ engineError = null,
+ ),
+ )
+
+ is TranslationsAction.SetPageSettingsAction ->
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ pageSettings = action.pageSettings,
+ settingsError = null,
+ )
+ }
+
+ is TranslationsAction.SetNeverTranslateSitesAction ->
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ neverTranslateSites = action.neverTranslateSites,
+ )
+ }
+
+ is TranslationsAction.RemoveNeverTranslateSiteAction -> {
+ val neverTranslateSites = state.findTab(action.tabId)?.translationsState?.neverTranslateSites
+ val updatedNeverTranslateSites = neverTranslateSites?.filter { it != action.origin }?.toList()
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ neverTranslateSites = updatedNeverTranslateSites,
+ )
+ }
+ }
+
+ is TranslationsAction.OperationRequestedAction ->
+ when (action.operation) {
+ TranslationOperation.FETCH_SUPPORTED_LANGUAGES -> {
+ state.copy(
+ translationEngine = state.translationEngine.copy(
+ supportedLanguages = null,
+ ),
+ )
+ }
+ TranslationOperation.FETCH_LANGUAGE_MODELS -> {
+ state.copy(
+ translationEngine = state.translationEngine.copy(
+ languageModels = null,
+ ),
+ )
+ }
+
+ TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS -> {
+ state.copy(
+ translationEngine = state.translationEngine.copy(
+ languageSettings = null,
+ ),
+ )
+ }
+
+ TranslationOperation.FETCH_PAGE_SETTINGS -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ pageSettings = null,
+ )
+ }
+ }
+
+ TranslationOperation.FETCH_NEVER_TRANSLATE_SITES -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ neverTranslateSites = null,
+ )
+ }
+ }
+ TranslationOperation.TRANSLATE, TranslationOperation.RESTORE -> {
+ // No state change for these operations
+ state
+ }
+ }
+
+ is TranslationsAction.UpdatePageSettingAction -> {
+ val currentPageSettings =
+ state.findTab(action.tabId)?.translationsState?.pageSettings ?: TranslationPageSettings()
+
+ when (action.operation) {
+ TranslationPageSettingOperation.UPDATE_ALWAYS_OFFER_POPUP -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ pageSettings = currentPageSettings.copy(alwaysOfferPopup = action.setting),
+ )
+ }
+ }
+
+ TranslationPageSettingOperation.UPDATE_ALWAYS_TRANSLATE_LANGUAGE -> {
+ val alwaysTranslateLang = action.setting
+ var neverTranslateLang = currentPageSettings.neverTranslateLanguage
+
+ if (alwaysTranslateLang) {
+ // Always and never translate sites are always opposites when the other is true.
+ neverTranslateLang = false
+ }
+
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ pageSettings = currentPageSettings.copy(
+ alwaysTranslateLanguage = alwaysTranslateLang,
+ neverTranslateLanguage = neverTranslateLang,
+ ),
+ )
+ }
+ }
+
+ TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_LANGUAGE -> {
+ var alwaysTranslateLang = currentPageSettings.alwaysTranslateLanguage
+ val neverTranslateLang = action.setting
+
+ if (neverTranslateLang) {
+ // Always and never translate sites are always opposites when the other is true.
+ alwaysTranslateLang = false
+ }
+
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ pageSettings = currentPageSettings.copy(
+ alwaysTranslateLanguage = alwaysTranslateLang,
+ neverTranslateLanguage = neverTranslateLang,
+ ),
+ )
+ }
+ }
+
+ TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_SITE -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ pageSettings = currentPageSettings.copy(neverTranslateSite = action.setting),
+ )
+ }
+ }
+ }
+ }
+
+ is TranslationsAction.SetEngineSupportedAction -> {
+ state.copy(
+ translationEngine = state.translationEngine.copy(
+ isEngineSupported = action.isEngineSupported,
+ engineError = null,
+ ),
+ )
+ }
+
+ is TranslationsAction.FetchTranslationDownloadSizeAction -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ translationDownloadSize = null,
+ )
+ }
+ }
+
+ is TranslationsAction.SetTranslationDownloadSizeAction -> {
+ state.copyWithTranslationsState(action.tabId) {
+ it.copy(
+ translationDownloadSize = action.translationSize,
+ )
+ }
+ }
+
+ is TranslationsAction.SetLanguageSettingsAction -> {
+ state.copy(
+ translationEngine = state.translationEngine.copy(
+ languageSettings = action.languageSettings,
+ engineError = null,
+ ),
+ )
+ }
+ }
+
+ private inline fun BrowserState.copyWithTranslationsState(
+ tabId: String,
+ crossinline update: (TranslationsState) -> TranslationsState,
+ ): BrowserState {
+ return updateTabOrCustomTabState(tabId) { current ->
+ current.createCopy(translationsState = update(current.translationsState))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/UndoReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/UndoReducer.kt
new file mode 100644
index 0000000000..40a8489a82
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/UndoReducer.kt
@@ -0,0 +1,46 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.UndoAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.UndoHistoryState
+
+internal object UndoReducer {
+ /**
+ * [UndoAction] Reducer function for modifying the [UndoHistoryState] used to undo the removal
+ * of tabs.
+ */
+ fun reduce(state: BrowserState, action: UndoAction): BrowserState {
+ return when (action) {
+ is UndoAction.AddRecoverableTabs -> {
+ // A middleware will take care of observing tabs getting removed and will then
+ // dispatch an this action to remember those tabs. We only remember the last set
+ // of tabs that got removed and replace them here.
+ state.copy(
+ undoHistory = UndoHistoryState(action.tag, action.tabs, action.selectedTabId),
+ )
+ }
+
+ is UndoAction.RestoreRecoverableTabs -> {
+ // The actual restore is handled by a middleware. Here we only need to clear the
+ // state since we assume it got restored.
+ state.copy(
+ undoHistory = UndoHistoryState(),
+ )
+ }
+
+ is UndoAction.ClearRecoverableTabs -> {
+ if (action.tag == state.undoHistory.tag) {
+ state.copy(
+ undoHistory = UndoHistoryState(),
+ )
+ } else {
+ state
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/WebExtensionReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/WebExtensionReducer.kt
new file mode 100644
index 0000000000..f2488b7c6b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/WebExtensionReducer.kt
@@ -0,0 +1,117 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.WebExtensionState
+
+internal object WebExtensionReducer {
+ /**
+ * [WebExtensionAction] Reducer function for modifying a specific [WebExtensionState] in
+ * both [SessionState.extensionState] or [BrowserState.extensions].
+ */
+ @Suppress("LongMethod")
+ fun reduce(state: BrowserState, action: WebExtensionAction): BrowserState {
+ return when (action) {
+ is WebExtensionAction.InstallWebExtensionAction -> {
+ val existingExtension = state.extensions[action.extension.id]
+ if (existingExtension == null) {
+ state.copy(
+ extensions = state.extensions + (action.extension.id to action.extension),
+ )
+ } else {
+ state.updateWebExtensionState(action.extension.id) {
+ // Keep existing browser and page actions in case we received them before the install action
+ action.extension.copy(browserAction = it.browserAction, pageAction = it.pageAction)
+ }
+ }
+ }
+ is WebExtensionAction.UninstallWebExtensionAction -> {
+ state.copy(
+ extensions = state.extensions - action.extensionId,
+ tabs = state.tabs.map { it.copy(extensionState = it.extensionState - action.extensionId) },
+ )
+ }
+ is WebExtensionAction.UninstallAllWebExtensionsAction -> {
+ state.copy(
+ extensions = emptyMap(),
+ tabs = state.tabs.map { it.copy(extensionState = emptyMap()) },
+ )
+ }
+ is WebExtensionAction.UpdateWebExtensionEnabledAction -> {
+ state.updateWebExtensionState(action.extensionId) {
+ it.copy(enabled = action.enabled)
+ }
+ }
+ is WebExtensionAction.UpdateWebExtensionAllowedInPrivateBrowsingAction -> {
+ state.updateWebExtensionState(action.extensionId) {
+ it.copy(allowedInPrivateBrowsing = action.allowed)
+ }
+ }
+ is WebExtensionAction.UpdateBrowserAction -> {
+ state.updateWebExtensionState(action.extensionId) {
+ it.copy(browserAction = action.browserAction)
+ }
+ }
+ is WebExtensionAction.UpdatePageAction -> {
+ state.updateWebExtensionState(action.extensionId) {
+ it.copy(pageAction = action.pageAction)
+ }
+ }
+ is WebExtensionAction.UpdatePopupSessionAction -> {
+ state.updateWebExtensionState(action.extensionId) {
+ it.copy(popupSessionId = action.popupSessionId, popupSession = action.popupSession)
+ }
+ }
+ is WebExtensionAction.UpdateTabBrowserAction -> {
+ state.updateWebExtensionTabState(action.sessionId, action.extensionId) {
+ it.copy(browserAction = action.browserAction)
+ }
+ }
+ is WebExtensionAction.UpdateTabPageAction -> {
+ state.updateWebExtensionTabState(action.sessionId, action.extensionId) {
+ it.copy(pageAction = action.pageAction)
+ }
+ }
+ is WebExtensionAction.UpdateWebExtensionAction -> {
+ state.updateWebExtensionState(action.updatedExtension.id) { action.updatedExtension }
+ }
+ is WebExtensionAction.UpdateActiveWebExtensionTabAction -> {
+ state.copy(activeWebExtensionTabId = action.activeWebExtensionTabId)
+ }
+ is WebExtensionAction.UpdatePromptRequestWebExtensionAction -> {
+ state.copy(webExtensionPromptRequest = action.promptRequest)
+ }
+ is WebExtensionAction.ConsumePromptRequestWebExtensionAction -> {
+ state.copy(webExtensionPromptRequest = null)
+ }
+ }
+ }
+
+ private fun BrowserState.updateWebExtensionTabState(
+ tabId: String,
+ extensionId: String,
+ update: (WebExtensionState) -> WebExtensionState,
+ ): BrowserState {
+ return copy(
+ tabs = tabs.updateTabs(tabId) { current ->
+ val existingExtension = current.extensionState[extensionId]
+ val newExtension = extensionId to update(existingExtension ?: WebExtensionState(extensionId))
+ current.copy(extensionState = current.extensionState + newExtension)
+ } ?: tabs,
+ )
+ }
+
+ private fun BrowserState.updateWebExtensionState(
+ extensionId: String,
+ update: (WebExtensionState) -> WebExtensionState,
+ ): BrowserState {
+ val existingExtension = extensions[extensionId]
+ val newExtension = extensionId to update(existingExtension ?: WebExtensionState(extensionId))
+ return copy(extensions = extensions + newExtension)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/RegionState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/RegionState.kt
new file mode 100644
index 0000000000..980849cf62
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/RegionState.kt
@@ -0,0 +1,25 @@
+/* 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.browser.state.search
+
+/**
+ * Data class keeping track of the region of the user.
+ *
+ * @param home The "home" region of the user, which will change only if the user stays in the same
+ * region for an extended time.
+ * @param current The "current" region of the user. May change more frequently and may eventually
+ * become the new "home" region after some time.
+ */
+data class RegionState(
+ val home: String,
+ val current: String,
+) {
+ companion object {
+ /**
+ * The default region when the region of the user could not be detected.
+ */
+ val Default = RegionState("XX", "XX")
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt
new file mode 100644
index 0000000000..ddb76253ec
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt
@@ -0,0 +1,75 @@
+/* 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.browser.state.search
+
+import android.graphics.Bitmap
+import android.net.Uri
+
+// OpenSearch parameter for search terms.
+const val OS_SEARCH_ENGINE_TERMS_PARAM = "{" + "searchTerms" + "}"
+
+/**
+ * A data class representing a search engine.
+ *
+ * @property id the ID of this search engine.
+ * @property name the name of this search engine.
+ * @property icon the icon of this search engine.
+ * @property inputEncoding the input encoding of this search engine.
+ * @property type the type of this search engine.
+ * @property resultUrls the list of the queried suggestions result urls.
+ * @property suggestUrl the search suggestion url.
+ * @property isGeneral whether the search engine is a general search engine.
+ */
+data class SearchEngine(
+ val id: String,
+ val name: String,
+ val icon: Bitmap,
+ val inputEncoding: String? = null,
+ val type: Type,
+ val resultUrls: List<String> = emptyList(),
+ val suggestUrl: String? = null,
+ val isGeneral: Boolean = false,
+) {
+ /**
+ * A enum class representing a search engine type.
+ */
+ enum class Type {
+ /**
+ * A bundled search engine.
+ */
+ BUNDLED,
+
+ /**
+ * A bundled search engine that was loaded additionally, requested by the application.
+ */
+ BUNDLED_ADDITIONAL,
+
+ /**
+ * A custom search engine added by the user.
+ */
+ CUSTOM,
+
+ /**
+ * A search engine add by the application.
+ */
+ APPLICATION,
+ }
+
+ // Cache these parameters to avoid repeated parsing.
+ // Assume we always have at least one entry in `resultUrls`.
+ val resultsUrl: Uri by lazy { Uri.parse(this.resultUrls[0]) }
+
+ // This assumes that search parameters are always "on their own" within the param value,
+ // e.g. always in a form of ?q={searchTerms}, never ?q=somePrefix-{searchTerms}
+ val searchParameterName by lazy {
+ resultsUrl.queryParameterNames.find {
+ try {
+ resultsUrl.getQueryParameter(it) == OS_SEARCH_ENGINE_TERMS_PARAM
+ } catch (e: UnsupportedOperationException) {
+ false
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/selector/Selectors.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/selector/Selectors.kt
new file mode 100644
index 0000000000..6516122834
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/selector/Selectors.kt
@@ -0,0 +1,187 @@
+/* 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.browser.state.selector
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.support.base.log.logger.Logger
+import java.net.URI
+import java.net.URISyntaxException
+
+// Extension functions for querying and dissecting [BrowserState]
+
+/**
+ * The currently selected tab if there's one.
+ *
+ * Only one [TabSessionState] can be selected at a given time.
+ */
+val BrowserState.selectedTab: TabSessionState?
+ get() = selectedTabId?.let { id -> findTab(id) }
+
+/**
+ * The currently selected tab if there's one that is not private.
+ */
+val BrowserState.selectedNormalTab: TabSessionState?
+ get() = selectedTabId?.let { id -> findNormalTab(id) }
+
+/**
+ * Finds and returns the tab with the given id. Returns null if no matching tab could be
+ * found.
+ *
+ * @param tabId The ID of the tab to search for.
+ * @return The [TabSessionState] with the provided [tabId] or null if it could not be found.
+ */
+fun BrowserState.findTab(tabId: String): TabSessionState? {
+ return tabs.firstOrNull { it.id == tabId }
+}
+
+/**
+ * Finds and returns the Custom Tab with the given id. Returns null if no matching tab could be
+ * found.
+ *
+ * @param tabId The ID of the custom tab to search for.
+ * @return The [CustomTabSessionState] with the provided [tabId] or null if it could not be found.
+ */
+fun BrowserState.findCustomTab(tabId: String): CustomTabSessionState? {
+ return customTabs.firstOrNull { it.id == tabId }
+}
+
+/**
+ * Finds and returns the normal (non-private) tab with the given id. Returns null if no
+ * matching tab could be found.
+ *
+ * @param tabId The ID of the tab to search for.
+ * @return The [TabSessionState] with the provided [tabId] or null if it could not be found.
+ */
+fun BrowserState.findNormalTab(tabId: String): TabSessionState? {
+ return normalTabs.firstOrNull { it.id == tabId }
+}
+
+/**
+ * Finds and returns the [TabSessionState] or [CustomTabSessionState] with the given [tabId].
+ */
+fun BrowserState.findTabOrCustomTab(tabId: String): SessionState? {
+ return findTab(tabId) ?: findCustomTab(tabId)
+}
+
+/**
+ * Finds and returns the tab with the given id or the selected tab if no id was provided (null). Returns null
+ * if no matching tab could be found or if no selected tab exists.
+ *
+ * @param customTabId An optional ID of a custom tab. If not provided or null then the selected tab will be returned.
+ * @return The custom tab with the provided ID or the selected tav if no id was provided.
+ */
+fun BrowserState.findCustomTabOrSelectedTab(customTabId: String? = null): SessionState? {
+ return if (customTabId != null) {
+ findCustomTab(customTabId)
+ } else {
+ selectedTab
+ }
+}
+
+/**
+ * Finds and returns the tab with the given id or the selected tab if no id was provided (null). Returns null
+ * if no matching tab could be found or if no selected tab exists.
+ *
+ * @param tabId An optional ID of a tab. If not provided or null then the selected tab will be returned.
+ * @return The custom tab with the provided ID or the selected tav if no id was provided.
+ */
+fun BrowserState.findTabOrCustomTabOrSelectedTab(tabId: String? = null): SessionState? {
+ return if (tabId != null) {
+ findTabOrCustomTab(tabId)
+ } else {
+ selectedTab
+ }
+}
+
+/**
+ * Finds and returns the tab with the given url. Returns null if no matching tab could be found.
+ *
+ * @param url A mandatory url of the searched tab.
+ * @param private Whether to look for a matching private or normal tab
+ * @return The tab with the provided url
+ */
+fun BrowserState.findNormalOrPrivateTabByUrl(url: String, private: Boolean): TabSessionState? {
+ return tabs.firstOrNull { tab -> tab.content.url == url && tab.content.private == private }
+}
+
+/**
+ * Finds and returns the tab with the given url ignoring the fragment identifier part of the url.
+ * Returns null if no matching tab could be found.
+ *
+ * @param url A mandatory url of the searched tab.
+ * @param private Whether to look for a matching private or normal tab.
+ * @return The tab with the provided url.
+ */
+fun BrowserState.findNormalOrPrivateTabByUrlIgnoringFragment(
+ url: String,
+ private: Boolean,
+): TabSessionState? {
+ return tabs.firstOrNull { tab ->
+ isSameUrlIgnoringFragment(tab.content.url, url) && tab.content.private == private
+ }
+}
+
+/**
+ * Gets a list of normal or private tabs depending on the requested type.
+ * @param private If true, all private tabs will be returned.
+ * If false, all normal tabs will be returned.
+ */
+fun BrowserState.getNormalOrPrivateTabs(private: Boolean): List<TabSessionState> {
+ return tabs.filter { it.content.private == private }
+}
+
+/**
+ * List of private tabs.
+ */
+val BrowserState.privateTabs: List<TabSessionState>
+ get() = getNormalOrPrivateTabs(private = true)
+
+/**
+ * List of normal (non-private) tabs.
+ */
+val BrowserState.normalTabs: List<TabSessionState>
+ get() = getNormalOrPrivateTabs(private = false)
+
+/**
+ * List of all tabs (normal, private and CustomTabs).
+ */
+val BrowserState.allTabs: List<SessionState>
+ get() = tabs + customTabs
+
+/**
+ * Returns true if the two urls are the same ignoring the fragment identifier - the string after
+ * the # in a url (eg, http://foo/bar#buzz).
+ *
+ * @param tabUrl A mandatory url of the tab.
+ * @param url A mandatory url that's being checked.
+ * @return true if the check passes, otherwise false.
+ */
+private fun isSameUrlIgnoringFragment(tabUrl: String, url: String): Boolean {
+ return try {
+ val tabUri = URI.create(tabUrl)
+ val uri = URI.create(url)
+ URI(tabUri.scheme, tabUri.authority, tabUri.path, tabUri.query, null).removeTrailingSlash() ==
+ URI(uri.scheme, uri.authority, uri.path, uri.query, null).removeTrailingSlash()
+ } catch (e: URISyntaxException) {
+ Logger.error("Unable to compare urls", e)
+ false
+ } catch (e: IllegalArgumentException) {
+ Logger.error("Unable to compare urls", e)
+ false
+ }
+}
+
+/**
+ * Removes trailing slash on the URI if present.
+ *
+ * @return String with trailing slash removed if it's the last character,
+ * otherwise the original string.
+ */
+private fun URI.removeTrailingSlash(): String {
+ return toString().trimEnd('/')
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AppIntentState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AppIntentState.kt
new file mode 100644
index 0000000000..65d590d646
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AppIntentState.kt
@@ -0,0 +1,18 @@
+/* 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.browser.state.state
+
+import android.content.Intent
+
+/**
+ * State keeping track of app intents to launch.
+ *
+ * @param url the URL to launch in an external app.
+ * @param appIntent the [Intent] to launch.
+ */
+data class AppIntentState(
+ val url: String,
+ val appIntent: Intent?,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AwesomeBarState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AwesomeBarState.kt
new file mode 100644
index 0000000000..5c0fd2f600
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AwesomeBarState.kt
@@ -0,0 +1,19 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.awesomebar.AwesomeBar
+
+/**
+ * State for interactions with the [AwesomeBar].
+ *
+ * @property visibilityState The suggestions and groups that are currently displayed in the [AwesomeBar].
+ * @property clickedSuggestion The [AwesomeBar.Suggestion] that the user clicked. This is `null` if the user is still
+ * interacting with the [AwesomeBar], or entered a search term or URL instead of clicking on a suggestion.
+ */
+data class AwesomeBarState(
+ val visibilityState: AwesomeBar.VisibilityState = AwesomeBar.VisibilityState(),
+ val clickedSuggestion: AwesomeBar.Suggestion? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt
new file mode 100644
index 0000000000..94ded431dd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt
@@ -0,0 +1,59 @@
+/* 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.browser.state.state
+
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.lib.state.State
+import java.util.Locale
+
+/**
+ * Value type that represents the complete state of the browser/engine.
+ *
+ * @property tabs the list of open tabs, defaults to an empty list.
+ * @property tabPartitions a mapping of IDs to the corresponding [TabPartition]. A partition
+ * is used to store tab groups for a specific feature.
+ * @property closedTabs the list of recently closed tabs if a [RecentlyClosedMiddleware] is added,
+ * defaults to an empty list.
+ * @property selectedTabId the ID of the currently selected (active) tab.
+ * @property customTabs the list of custom tabs, defaults to an empty list.
+ * @property containers A map of [SessionState.contextId] and their respective container [ContainerState].
+ * @property extensions A map of extension IDs and web extensions of all installed web extensions.
+ * The extensions here represent the default values for all [BrowserState.extensions] and can
+ * be overridden per [SessionState].
+ * @property webExtensionPromptRequest the actual active web extension prompt request.
+ * @property activeWebExtensionTabId the ID of the tab that is marked active for web extensions
+ * to support tabs.query({active: true}).
+ * @property search the state of search for this browser state.
+ * @property downloads Downloads ([DownloadState]s) mapped to their IDs.
+ * @property undoHistory History of recently closed tabs to support "undo" (Requires UndoMiddleware).
+ * @property restoreComplete Whether or not restoring [BrowserState] has completed. This can be used
+ * on application startup e.g. as an indicator that tabs have been restored.
+ * @property locale The current locale of the app. Will be null when following the system default.
+ * @property awesomeBarState Holds state for interactions with the [AwesomeBar].
+ * @property translationEngine Holds translation state that applies to the browser.
+ */
+data class BrowserState(
+ val tabs: List<TabSessionState> = emptyList(),
+ val tabPartitions: Map<String, TabPartition> = emptyMap(),
+ val customTabs: List<CustomTabSessionState> = emptyList(),
+ val closedTabs: List<TabState> = emptyList(),
+ val selectedTabId: String? = null,
+ val containers: Map<String, ContainerState> = emptyMap(),
+ val extensions: Map<String, WebExtensionState> = emptyMap(),
+ val webExtensionPromptRequest: WebExtensionPromptRequest? = null,
+ val activeWebExtensionTabId: String? = null,
+ val downloads: Map<String, DownloadState> = emptyMap(),
+ val search: SearchState = SearchState(),
+ val undoHistory: UndoHistoryState = UndoHistoryState(),
+ val restoreComplete: Boolean = false,
+ val locale: Locale? = null,
+ val showExtensionsProcessDisabledPrompt: Boolean = false,
+ val extensionsProcessDisabled: Boolean = false,
+ val awesomeBarState: AwesomeBarState = AwesomeBarState(),
+ val translationEngine: TranslationsBrowserState = TranslationsBrowserState(),
+) : State
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContainerState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContainerState.kt
new file mode 100644
index 0000000000..991939645f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContainerState.kt
@@ -0,0 +1,56 @@
+/* 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.browser.state.state
+
+/**
+ * Value type that represents the state of a container also known as a contextual identity.
+ *
+ * @property contextId The session context ID also known as cookie store ID for the container.
+ * @property name Name of the container.
+ * @property color The color for the container. This can be shown in tabs belonging to this container.
+ * @property icon The icon for the container.
+ */
+data class ContainerState(
+ val contextId: String,
+ val name: String,
+ val color: Color,
+ val icon: Icon,
+) {
+ /**
+ * Enum of container color.
+ */
+ enum class Color(val color: String) {
+ BLUE("blue"),
+ TURQUOISE("turquoise"),
+ GREEN("green"),
+ YELLOW("yellow"),
+ ORANGE("orange"),
+ RED("red"),
+ PINK("pink"),
+ PURPLE("purple"),
+ TOOLBAR("toolbar"),
+ }
+
+ /**
+ * Enum of container icon.
+ */
+ enum class Icon(val icon: String) {
+ FINGERPRINT("fingerprint"),
+ BRIEFCASE("briefcase"),
+ DOLLAR("dollar"),
+ CART("cart"),
+ CIRCLE("circle"),
+ GIFT("gift"),
+ VACATION("vacation"),
+ FOOD("food"),
+ FRUIT("fruit"),
+ PET("pet"),
+ TREE("tree"),
+ CHILL("chill"),
+ FENCE("fence"),
+ }
+}
+
+typealias Container = ContainerState
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt
new file mode 100644
index 0000000000..9eeaf0aae8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.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.browser.state.state
+
+import android.graphics.Bitmap
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.FindResultState
+import mozilla.components.browser.state.state.content.HistoryState
+import mozilla.components.browser.state.state.content.PermissionHighlightsState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.media.RecordingDevice
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.search.SearchRequest
+import mozilla.components.concept.engine.window.WindowRequest
+
+/**
+ * Value type that represents the state of the content within a [SessionState].
+ *
+ * @property url The loading or loaded URL.
+ * @property private Whether or not the session is private.
+ * @property title The title of the current page.
+ * @property progress The loading progress of the current page denoted as 0-100.
+ * @property loading True if state is loading.
+ * @property searchTerms The last used search terms, or an empty string if no
+ * search was executed for this session.
+ * @property securityInfo The security information as [SecurityInfoState],
+ * describing whether or not the this session is for a secure URL, as well
+ * as the host and SSL certificate authority.
+ * @property icon The icon of the page currently loaded by this session.
+ * @property download Last unhandled download request.
+ * @property share Last unhandled request to share an internet resource that first needs to be downloaded.
+ * @property copy Last unhandled request to copy an internet resource that first needs to be downloaded.
+ * @property hitResult The target of the latest long click operation.
+ * @property promptRequests Current[PromptRequest]s.
+ * @property findResults The list of results of the latest "find in page" operation.
+ * @property windowRequest The last received [WindowRequest].
+ * @property searchRequest The last received [SearchRequest].
+ * @property fullScreen True if the page is full screen, false if not.
+ * @property layoutInDisplayCutoutMode The display layout cutout mode state.
+ * @property canGoBack Whether or not there's a history item to navigate back to.
+ * @property canGoForward Whether or not there's a history item to navigate forward to.
+ * @property webAppManifest The Web App Manifest for the currently visited page (or null).
+ * @property firstContentfulPaint Whether or not the first contentful paint has happened.
+ * @property history The [HistoryState] of this state.
+ * @property permissionHighlights Holds the state of any site permission that was granted/denied
+ * that should be brought to the user's attention, for example when media content is not able to
+ * play because the autoplay settings.
+ * @property permissionRequestsList Holds unprocessed content requests.
+ * @property appPermissionRequestsList Holds unprocessed app requests.
+ * @property pictureInPictureEnabled True if the session is being displayed in PIP mode.
+ * @property loadRequest The last [LoadRequestState] of this session.
+ * @property refreshCanceled Indicates if an intent of refreshing was canceled.
+ * True if a page refresh was cancelled by the user, defaults to false. Note that this is not about
+ * stopping an ongoing page load but useful in cases like swipe-to-refresh which allow users to
+ * cancel or abort before a page is refreshed.
+ * @property recordingDevices List of recording devices (e.g. camera or microphone) currently in use
+ * by web content.
+ * @property desktopMode True if desktop mode is enabled, otherwise false.
+ * @property appIntent The last received [AppIntentState].
+ * @property showToolbarAsExpanded Whether the dynamic toolbar should be forced as expanded.
+ * @property previewImageUrl The preview image of the page (e.g. the hero image), if available.
+ * @property isSearch Whether or not the last url load request is the result of a search.
+ * @property hasFormData Whether or not the content has filled out form data.
+ * @property isProductUrl Indicates if the [url] is a supported product page.
+ */
+data class ContentState(
+ val url: String,
+ val private: Boolean = false,
+ val title: String = "",
+ val progress: Int = 0,
+ val loading: Boolean = false,
+ val searchTerms: String = "",
+ val securityInfo: SecurityInfoState = SecurityInfoState(),
+ val icon: Bitmap? = null,
+ val download: DownloadState? = null,
+ val share: ShareInternetResourceState? = null,
+ val copy: ShareInternetResourceState? = null,
+ val hitResult: HitResult? = null,
+ val promptRequests: List<PromptRequest> = emptyList(),
+ val findResults: List<FindResultState> = emptyList(),
+ val windowRequest: WindowRequest? = null,
+ val searchRequest: SearchRequest? = null,
+ val fullScreen: Boolean = false,
+ val layoutInDisplayCutoutMode: Int = 0,
+ val canGoBack: Boolean = false,
+ val canGoForward: Boolean = false,
+ val webAppManifest: WebAppManifest? = null,
+ val firstContentfulPaint: Boolean = false,
+ val history: HistoryState = HistoryState(),
+ val permissionHighlights: PermissionHighlightsState = PermissionHighlightsState(),
+ val permissionRequestsList: List<PermissionRequest> = emptyList(),
+ val appPermissionRequestsList: List<PermissionRequest> = emptyList(),
+ val pictureInPictureEnabled: Boolean = false,
+ val loadRequest: LoadRequestState? = null,
+ val refreshCanceled: Boolean = false,
+ val recordingDevices: List<RecordingDevice> = emptyList(),
+ val desktopMode: Boolean = false,
+ val appIntent: AppIntentState? = null,
+ val showToolbarAsExpanded: Boolean = false,
+ val previewImageUrl: String? = null,
+ val isSearch: Boolean = false,
+ val hasFormData: Boolean = false,
+ val isProductUrl: Boolean = false,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabConfig.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabConfig.kt
new file mode 100644
index 0000000000..3b59b0e8c9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabConfig.kt
@@ -0,0 +1,107 @@
+/* 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.browser.state.state
+
+import android.app.PendingIntent
+import android.graphics.Bitmap
+import android.os.Bundle
+import androidx.annotation.ColorInt
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK
+import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT
+import androidx.browser.customtabs.CustomTabsIntent.ColorScheme
+import androidx.browser.customtabs.CustomTabsSessionToken
+
+/**
+ * Holds configuration data for a Custom Tab.
+ *
+ * @property colorScheme Optional [ColorScheme] to apply to the Custom Tab UI.
+ * @property colorSchemes Optional collection of [ColorSchemeParams] for each [ColorScheme].
+ * @property closeButtonIcon Optional custom icon of the back button on the toolbar.
+ * @property enableUrlbarHiding Enables the toolbar to hide as the user scrolls down on the page.
+ * @property actionButtonConfig Optional custom action button on the toolbar.
+ * @property showCloseButton Specifies whether the close button will be shown on the toolbar.
+ * @property showShareMenuItem Specifies whether a default share button will be shown in the menu.
+ * @property menuItems Custom overflow menu items.
+ * @property exitAnimations Optional [Bundle] containing custom exit animations for the tab.
+ * @property titleVisible Whether the title should be shown in the custom tab.
+ * @property sessionToken Optional token associated with the custom tab.
+ * @property externalAppType How this custom tab is being displayed.
+ */
+data class CustomTabConfig(
+ @ColorScheme val colorScheme: Int? = null,
+ val colorSchemes: ColorSchemes? = null,
+ val closeButtonIcon: Bitmap? = null,
+ val enableUrlbarHiding: Boolean = false,
+ val actionButtonConfig: CustomTabActionButtonConfig? = null,
+ val showCloseButton: Boolean = true,
+ val showShareMenuItem: Boolean = false,
+ val menuItems: List<CustomTabMenuItem> = emptyList(),
+ val exitAnimations: Bundle? = null,
+ val titleVisible: Boolean = false,
+ val sessionToken: CustomTabsSessionToken? = null,
+ val externalAppType: ExternalAppType = ExternalAppType.CUSTOM_TAB,
+)
+
+/**
+ * Represents different contexts that a custom tab session can be displayed in.
+ */
+enum class ExternalAppType {
+ /**
+ * Custom tab is displayed as a normal custom tab with toolbar.
+ */
+ CUSTOM_TAB,
+
+ /**
+ * Custom tab toolbar is hidden inside a Progressive Web App created by the browser.
+ */
+ PROGRESSIVE_WEB_APP,
+
+ /**
+ * Custom tab is displayed fullscreen inside a Trusted Web Activity from an external app.
+ */
+ TRUSTED_WEB_ACTIVITY,
+}
+
+data class CustomTabActionButtonConfig(
+ val description: String,
+ val icon: Bitmap,
+ val pendingIntent: PendingIntent,
+ val id: Int = CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID,
+ val tint: Boolean = false,
+)
+
+data class CustomTabMenuItem(
+ val name: String,
+ val pendingIntent: PendingIntent,
+)
+
+/**
+ * Holds color data for Custom Tab visual elements.
+ *
+ * @property toolbarColor Optional background color for the toolbar.
+ * @property toolbarColor Optional background color for the secondary toolbar.
+ * @property navigationBarColor Optional background color for the navigation bar.
+ * @property navigationBarDividerColor Optional background color for the navigation bar divider.
+ */
+data class ColorSchemeParams(
+ @ColorInt val toolbarColor: Int? = null,
+ @ColorInt val secondaryToolbarColor: Int? = null,
+ @ColorInt val navigationBarColor: Int? = null,
+ @ColorInt val navigationBarDividerColor: Int? = null,
+)
+
+/**
+ * Holds the [ColorSchemeParams] for each possible color scheme.
+ *
+ * @property defaultColorSchemeParams Optional default [ColorSchemeParams].
+ * @property lightColorSchemeParams Optional [ColorSchemeParams] for [COLOR_SCHEME_LIGHT].
+ * @property darkColorSchemeParams Optional [ColorSchemeParams] for [COLOR_SCHEME_DARK].
+ */
+data class ColorSchemes(
+ val defaultColorSchemeParams: ColorSchemeParams? = null,
+ val lightColorSchemeParams: ColorSchemeParams? = null,
+ val darkColorSchemeParams: ColorSchemeParams? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt
new file mode 100644
index 0000000000..e111095dfb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt
@@ -0,0 +1,98 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import java.util.UUID
+
+/**
+ * Value type that represents the state of a Custom Tab.
+ *
+ * @property id the ID of this custom tab and session.
+ * @property content the [ContentState] of this custom tab.
+ * @property trackingProtection the [TrackingProtectionState] of this custom tab.
+ * @property translationsState the [TranslationsState] of this custom tab.
+ * @property config the [CustomTabConfig] used to create this custom tab.
+ * @property extensionState a map of web extension ids and extensions, that contains the overridden
+ * values for this tab.
+ * @property mediaSessionState the [MediaSessionState] of this session.
+ * @property contextId the session context ID of this custom tab.
+ * @property source the [SessionState.Source] of this session.
+ */
+data class CustomTabSessionState(
+ override val id: String = UUID.randomUUID().toString(),
+ override val content: ContentState,
+ override val trackingProtection: TrackingProtectionState = TrackingProtectionState(),
+ override val translationsState: TranslationsState = TranslationsState(),
+ val config: CustomTabConfig,
+ override val engineState: EngineState = EngineState(),
+ override val extensionState: Map<String, WebExtensionState> = emptyMap(),
+ override val mediaSessionState: MediaSessionState? = null,
+ override val contextId: String? = null,
+ override val source: SessionState.Source = SessionState.Source.Internal.CustomTab,
+ override val restored: Boolean = false,
+ override val cookieBanner: CookieBannerHandlingStatus = CookieBannerHandlingStatus.NO_DETECTED,
+) : SessionState {
+
+ override fun createCopy(
+ id: String,
+ content: ContentState,
+ trackingProtection: TrackingProtectionState,
+ translationsState: TranslationsState,
+ engineState: EngineState,
+ extensionState: Map<String, WebExtensionState>,
+ mediaSessionState: MediaSessionState?,
+ contextId: String?,
+ cookieBanner: CookieBannerHandlingStatus,
+ ) = copy(
+ id = id,
+ content = content,
+ trackingProtection = trackingProtection,
+ translationsState = translationsState,
+ engineState = engineState,
+ extensionState = extensionState,
+ mediaSessionState = mediaSessionState,
+ contextId = contextId,
+ )
+}
+
+/**
+ * Convenient function for creating a custom tab.
+ */
+fun createCustomTab(
+ url: String,
+ id: String = UUID.randomUUID().toString(),
+ config: CustomTabConfig = CustomTabConfig(),
+ title: String = "",
+ contextId: String? = null,
+ engineSession: EngineSession? = null,
+ mediaSessionState: MediaSessionState? = null,
+ crashed: Boolean = false,
+ source: SessionState.Source = SessionState.Source.Internal.CustomTab,
+ private: Boolean = false,
+ webAppManifest: WebAppManifest? = null,
+ initialLoadFlags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+): CustomTabSessionState {
+ return CustomTabSessionState(
+ id = id,
+ source = source,
+ content = ContentState(
+ url = url,
+ title = title,
+ private = private,
+ webAppManifest = webAppManifest,
+ ),
+ config = config,
+ mediaSessionState = mediaSessionState,
+ contextId = contextId,
+ engineState = EngineState(
+ engineSession = engineSession,
+ crashed = crashed,
+ initialLoadFlags = initialLoadFlags,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt
new file mode 100644
index 0000000000..1bd30e6974
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt
@@ -0,0 +1,39 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSessionState
+
+/**
+ * Value type that holds the browser engine state of a session.
+ *
+ * @property engineSession the engine's representation of this session.
+ * @property engineSessionState serializable and restorable state of an engine session, see
+ * [EngineSession.saveState] and [EngineSession.restoreState].
+ * @property engineObserver the [EngineSession.Observer] linked to [engineSession]. It is
+ * used to observe engine events and update the store. It should become obsolete, once the
+ * migration to browser state is complete, as the engine will then have direct access to
+ * the store.
+ * @property crashed Whether this session has crashed. In conjunction with a `concept-engine`
+ * implementation that uses a multi-process architecture, single sessions can crash without crashing
+ * the whole app. A crashed session may still be operational (since the underlying engine implementation
+ * has recovered its content process), but further action may be needed to restore the last state
+ * before the session has crashed (if desired).
+ * @property timestamp Timestamp of when the [EngineSession] was linked.
+ * @property initialLoadFlags [EngineSession.LoadUrlFlags] to use for the first load of this session.
+ * @property initializing whether or not the [EngineSession] is currently being initialized.
+ * @property initialAdditionalHeaders The extra headers to use for the first load of this session.
+ */
+data class EngineState(
+ val engineSession: EngineSession? = null,
+ val engineSessionState: EngineSessionState? = null,
+ val initializing: Boolean = false,
+ val engineObserver: EngineSession.Observer? = null,
+ val crashed: Boolean = false,
+ val timestamp: Long? = null,
+ val initialLoadFlags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ val initialAdditionalHeaders: Map<String, String>? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LastMediaAccessState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LastMediaAccessState.kt
new file mode 100644
index 0000000000..089897c663
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LastMediaAccessState.kt
@@ -0,0 +1,29 @@
+/* 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.browser.state.state
+
+/**
+ * Details about the last playing media in this tab.
+ *
+ * @property lastMediaUrl - [ContentState.url] when media started playing.
+ * This is not the URL of the media but of the page when media started.
+ * Defaults to "" (an empty String) if media hasn't started playing.
+ * This value is only updated when media starts playing.
+ * Can be used as a backup to [mediaSessionActive] for knowing the user is still on the same website
+ * on which media was playing before media started playing in another tab.
+ *
+ * @property lastMediaAccess The last time media started playing in the current web document.
+ * Defaults to [0] if media hasn't started playing.
+ * This value is only updated when media starts playing.
+ *
+ * @property mediaSessionActive Whether or not the last accessed media is still active.
+ * Can be used as a backup to [lastMediaUrl] on websites which allow media to continue playing
+ * even when the users accesses another page (with another URL) in that same HTML document.
+ */
+data class LastMediaAccessState(
+ val lastMediaUrl: String = "",
+ val lastMediaAccess: Long = 0,
+ val mediaSessionActive: Boolean = false,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LoadRequestState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LoadRequestState.kt
new file mode 100644
index 0000000000..d4a298e593
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LoadRequestState.kt
@@ -0,0 +1,18 @@
+/* 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.browser.state.state
+
+/**
+ * A value type holding information about an ongoing load request.
+ *
+ * @property url the URL being loaded
+ * @property triggeredByRedirect True if the request is due to a redirect then true, otherwise false.
+ * @property triggeredByUser True if the request is due to user interaction, otherwise false.
+ */
+data class LoadRequestState(
+ val url: String,
+ val triggeredByRedirect: Boolean,
+ val triggeredByUser: Boolean,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/MediaSessionState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/MediaSessionState.kt
new file mode 100644
index 0000000000..a2a1c91e16
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/MediaSessionState.kt
@@ -0,0 +1,40 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.mediasession.MediaSession
+
+/**
+ * Value type representing a media session on a website.
+ *
+ * @property controller The [MediaSession.Controller] for controlling playback of this media session.
+ * @property metadata The [MediaSession.Metadata] for this media session.
+ * @property elementMetadata The [MediaSession.ElementMetadata] for this media session.
+ * @property playbackState The current simplified [MediaSession.PlaybackState] of this media session.
+ * @property features The [MediaSession.Feature] for this media session.
+ * @property positionState The current simplified [MediaSession.PositionState] of this media session.
+ * @property muted True if media session is muted.
+ * @property fullscreen True if media session is fullscreen.
+ * @property timestamp The timestamp of when [MediaSessionState] was created.
+ */
+data class MediaSessionState(
+ val controller: MediaSession.Controller,
+ val metadata: MediaSession.Metadata? = null,
+ val elementMetadata: MediaSession.ElementMetadata? = null,
+ val playbackState: MediaSession.PlaybackState = MediaSession.PlaybackState.UNKNOWN,
+ val features: MediaSession.Feature = MediaSession.Feature(),
+ val positionState: MediaSession.PositionState = MediaSession.PositionState(),
+ val muted: Boolean = false,
+ val fullscreen: Boolean = false,
+ val timestamp: Long = System.currentTimeMillis(),
+) : Comparable<MediaSessionState> {
+ override operator fun compareTo(other: MediaSessionState): Int {
+ if (playbackState == other.playbackState) {
+ return timestamp.compareTo(other.timestamp)
+ }
+
+ return playbackState.compareTo(other.playbackState)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ReaderState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ReaderState.kt
new file mode 100644
index 0000000000..c71f8fa0b7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ReaderState.kt
@@ -0,0 +1,30 @@
+/* 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.browser.state.state
+
+/**
+ * Value type that represents the state of reader mode/view.
+ *
+ * @property readerable whether or not the current page can be transformed to
+ * be displayed in a reader view.
+ * @property active whether or not reader view is active.
+ * @property checkRequired whether or not a readerable check is required for the
+ * current page.
+ * @property connectRequired whether or not a new connection to the reader view
+ * content script is required.
+ * @property baseUrl the base URL of the reader view extension page.
+ * @property activeUrl the URL of the page currently displayed in reader view.
+ * @property scrollY the vertical scroll position of the page currently
+ * displayed in reader view.
+ */
+data class ReaderState(
+ val readerable: Boolean = false,
+ val active: Boolean = false,
+ val checkRequired: Boolean = false,
+ val connectRequired: Boolean = false,
+ val baseUrl: String? = null,
+ val activeUrl: String? = null,
+ val scrollY: Int? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt
new file mode 100644
index 0000000000..8cf873e899
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt
@@ -0,0 +1,94 @@
+/* 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.browser.state.state
+
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+
+/**
+ * Value type that represents the state of search.
+ *
+ * @property region The region of the user.
+ * @property regionSearchEngines The list of bundled [SearchEngine]s for the "home" region of the user.
+ * @property customSearchEngines The list of custom [SearchEngine]s, added by the user.
+ * @property applicationSearchEngines The list of optional [SearchEngine]s, added by application.
+ * @property additionalSearchEngines Additional [SearchEngine]s that the application decided to load
+ * and that the user explicitly added to their list of search engines.
+ * @property additionalAvailableSearchEngines Additional [SearchEngine]s that the application decided
+ * to load and that are available for the user to be added to their list of search engines.
+ * @property hiddenSearchEngines The list of bundled [SearchEngine]s the user has explicitly hidden.
+ * @property disabledSearchEngineIds The list of [SearchEngine]s ids the user has explicitly disabled
+ * from being shown in the quick search list.
+ * @property userSelectedSearchEngineId The ID of the default [SearchEngine] selected by the user. Or
+ * `null` if the user hasn't made an explicit choice.
+ * @property userSelectedSearchEngineName The name of the default [SearchEngine] selected by the user.
+ * Or `null` if the user hasn't made an explicit choice.
+ * @property regionDefaultSearchEngineId The ID of the default [SearchEngine] of the "home" region
+ * of the user.
+ * @property regionSearchEnginesOrder Ordered list of [SearchEngine] IDs in the preferred order for
+ * this region. Can be used when [regionSearchEngines] needs to be reordered.
+ * @property complete Flag that indicates whether loading the list of search engines has completed.
+ * This can be used for waiting for specific values (e.g. the default search engine) to be available.
+ */
+data class SearchState(
+ val region: RegionState? = null,
+ val regionSearchEngines: List<SearchEngine> = emptyList(),
+ val customSearchEngines: List<SearchEngine> = emptyList(),
+ val applicationSearchEngines: List<SearchEngine> = emptyList(),
+ val additionalSearchEngines: List<SearchEngine> = emptyList(),
+ val additionalAvailableSearchEngines: List<SearchEngine> = emptyList(),
+ val hiddenSearchEngines: List<SearchEngine> = emptyList(),
+ val disabledSearchEngineIds: List<String> = emptyList(),
+ val userSelectedSearchEngineId: String? = null,
+ val userSelectedSearchEngineName: String? = null,
+ val regionDefaultSearchEngineId: String? = null,
+ val regionSearchEnginesOrder: List<String> = emptyList(),
+ val complete: Boolean = false,
+)
+
+/**
+ * The list of search engines to be used for searches (bundled and custom search engines).
+ */
+val SearchState.searchEngines: List<SearchEngine>
+ get() = (regionSearchEngines + additionalSearchEngines + customSearchEngines + applicationSearchEngines)
+
+/**
+ * The list of search engines that are available for the user to be added to their list of search
+ * engines.
+ */
+val SearchState.availableSearchEngines: List<SearchEngine>
+ get() = (hiddenSearchEngines + additionalAvailableSearchEngines)
+
+/**
+ * The primary search engine to be used by default for searches. This will either be the user
+ * selected search engine, if the user has made an explicit choice, or the default search engine for
+ * the user's region.
+ */
+val SearchState.selectedOrDefaultSearchEngine: SearchEngine?
+ get() {
+ // Does the user have a default search engine id set and is it in the list of available search engines?
+ if (userSelectedSearchEngineId != null) {
+ searchEngines.find { engine -> userSelectedSearchEngineId == engine.id }?.let { return it }
+ }
+
+ // Did we save a default search engine name for this user and can we find it in the list of
+ // available search engines?
+ if (userSelectedSearchEngineName != null) {
+ searchEngines.find { engine -> userSelectedSearchEngineName == engine.name }?.let { return it }
+ }
+
+ // Do we have a default search engine for the region of the user and is it available?
+ if (regionDefaultSearchEngineId != null) {
+ searchEngines.find { engine -> regionDefaultSearchEngineId == engine.id }?.let { return it }
+ }
+
+ // Fallback: Use the first search engine in the list
+ if (searchEngines.isNotEmpty()) {
+ return searchEngines[0]
+ }
+
+ // We couldn't find anything.
+ return null
+ }
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SecurityInfoState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SecurityInfoState.kt
new file mode 100644
index 0000000000..a9466289f8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SecurityInfoState.kt
@@ -0,0 +1,19 @@
+/* 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.browser.state.state
+
+/**
+ * A value type holding security information for a Session.
+ *
+ * @property secure true if the tab is currently pointed to a URL with
+ * a valid SSL certificate, otherwise false.
+ * @property host domain for which the SSL certificate was issued.
+ * @property issuer name of the certificate authority who issued the SSL certificate.
+ */
+data class SecurityInfoState(
+ val secure: Boolean = false,
+ val host: String = "",
+ val issuer: String = "",
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt
new file mode 100644
index 0000000000..7bf2e3359f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt
@@ -0,0 +1,201 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus
+import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
+import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE
+import mozilla.components.support.utils.SafeIntent
+
+/**
+ * Interface for states that contain a [ContentState] and can be accessed via an [id].
+ *
+ * @property id the unique id of the session.
+ * @property content the [ContentState] of this session.
+ * @property trackingProtection the [TrackingProtectionState] of this session.
+ * @property translationsState the [TranslationsState] of this session.
+ * @property cookieBanner Indicates the state of cookie banner for this session.
+ * @property engineState the [EngineState] of this session.
+ * @property extensionState a map of extension id and web extension states
+ * specific to this [SessionState].
+ * @property mediaSessionState the [MediaSessionState] of this session.
+ * @property contextId the session context ID of the session. The session context ID specifies the
+ * contextual identity to use for the session's cookie store.
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Work_with_contextual_identities
+ * @property restored Indicates if this session was restored from a hydrated state.
+ */
+interface SessionState {
+ val id: String
+ val content: ContentState
+ val trackingProtection: TrackingProtectionState
+ val translationsState: TranslationsState
+ val cookieBanner: CookieBannerHandlingStatus
+ val engineState: EngineState
+ val extensionState: Map<String, WebExtensionState>
+ val mediaSessionState: MediaSessionState?
+ val contextId: String?
+ val source: Source
+ val restored: Boolean
+
+ /**
+ * Copy the class and override some parameters.
+ */
+ fun createCopy(
+ id: String = this.id,
+ content: ContentState = this.content,
+ trackingProtection: TrackingProtectionState = this.trackingProtection,
+ translationsState: TranslationsState = this.translationsState,
+ engineState: EngineState = this.engineState,
+ extensionState: Map<String, WebExtensionState> = this.extensionState,
+ mediaSessionState: MediaSessionState? = this.mediaSessionState,
+ contextId: String? = this.contextId,
+ cookieBanner: CookieBannerHandlingStatus = this.cookieBanner,
+ ): SessionState
+
+ /**
+ * Represents the origin of a session to describe how and why it was created.
+ * @param id A unique identifier, exists for serialization purposes.
+ */
+ @Suppress("MagicNumber")
+ sealed class Source(val id: Int) {
+ companion object {
+ /**
+ * Initializes a [Source] of a correct type from its component properties.
+ * Intended use is for restoring persisted state.
+ */
+ fun restore(sourceId: Int?, packageId: String?, packageCategory: Int?): Source {
+ val caller = if (packageId != null) {
+ ExternalPackage(packageId, PackageCategory.fromInt(packageCategory))
+ } else {
+ null
+ }
+ return when (sourceId) {
+ 1 -> External.ActionSend(caller)
+ 2 -> External.ActionView(caller)
+ 3 -> External.ActionSearch(caller)
+ 4 -> External.CustomTab(caller)
+ 5 -> Internal.HomeScreen
+ 6 -> Internal.Menu
+ 7 -> Internal.NewTab
+ 8 -> Internal.None
+ 9 -> Internal.TextSelection
+ 10 -> Internal.UserEntered
+ 11 -> Internal.CustomTab
+ // Silently handle abnormalities (like invalid or null sourceId).
+ else -> Internal.None
+ }
+ }
+ }
+
+ /**
+ * Describes sessions of external origins, i.e. from outside of the application.
+ */
+ sealed class External(id: Int, open val caller: ExternalPackage?) : Source(id) {
+ /**
+ * Created to handle an ACTION_SEND (share) intent.
+ */
+ data class ActionSend(override val caller: ExternalPackage?) : External(1, caller)
+
+ /**
+ * Created to handle an ACTION_VIEW intent.
+ */
+ data class ActionView(override val caller: ExternalPackage?) : External(2, caller)
+
+ /**
+ * Created to handle an ACTION_SEARCH and ACTION_WEB_SEARCH intent.
+ */
+ data class ActionSearch(override val caller: ExternalPackage?) : External(3, caller)
+
+ /**
+ * Created to handle a CustomTabs intent of external origin.
+ */
+ data class CustomTab(override val caller: ExternalPackage?) : External(4, caller)
+ }
+
+ /**
+ * Describes sessions of internal origin, i.e. from within of the application.
+ */
+ sealed class Internal(id: Int) : Source(id) {
+ /**
+ * User interacted with the home screen.
+ */
+ object HomeScreen : Internal(5)
+
+ /**
+ * User interacted with a menu.
+ */
+ object Menu : Internal(6)
+
+ /**
+ * User opened a new tab.
+ */
+ object NewTab : Internal(7)
+
+ /**
+ * Default value and for testing purposes.
+ */
+ object None : Internal(8)
+
+ /**
+ * Default value and for testing purposes.
+ */
+ object TextSelection : Internal(9)
+
+ /**
+ * User entered a URL or search term.
+ */
+ object UserEntered : Internal(10)
+
+ /**
+ * Created to handle a CustomTabs intent of internal origin.
+ */
+ object CustomTab : Internal(11)
+ }
+ }
+}
+
+/**
+ * Describes a category of an external package.
+ */
+@Suppress("MagicNumber")
+enum class PackageCategory(val id: Int) {
+ UNKNOWN(-1),
+ GAME(0),
+ AUDIO(1),
+ VIDEO(2),
+ IMAGE(3),
+ SOCIAL(4),
+ NEWS(5),
+ MAPS(6),
+ PRODUCTIVITY(7),
+ ;
+
+ companion object {
+ /**
+ * Maps an int category (as it can be obtained from a package manager) to our internal representation.
+ */
+ fun fromInt(id: Int?): PackageCategory = values().find { category -> category.id == id } ?: UNKNOWN
+ }
+}
+
+/**
+ * Describes an external package.
+ * @param packageId An Android package id.
+ * @param category A [PackageCategory] as defined by the application.
+ */
+data class ExternalPackage(val packageId: String, val category: PackageCategory)
+
+/**
+ * Produces an [ExternalPackage] based on extras present in this intent.
+ */
+fun SafeIntent.externalPackage(): ExternalPackage? {
+ val referrerPackage = this.getStringExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE)
+ val referrerCategory = this.getIntExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, -1)
+ return if (referrerPackage != null) {
+ ExternalPackage(referrerPackage, PackageCategory.fromInt(referrerCategory))
+ } else {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabPartition.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabPartition.kt
new file mode 100644
index 0000000000..5ae4186e4f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabPartition.kt
@@ -0,0 +1,70 @@
+/* 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.browser.state.state
+
+import java.util.UUID
+
+/**
+ * Value type representing a tab partition. Partitions can overlap i.e., a tab
+ * can be in multiple partitions at the same time.
+ *
+ * @property id The ID of a tab partition. This should uniquely identify
+ * the feature responsible for managing those groups.
+ * @property tabGroups The groups of tabs in this partition. A partition can
+ * have one or more groups, depending on use case. Empty partitions will be
+ * removed by the system.
+ */
+data class TabPartition(
+ val id: String,
+ val tabGroups: List<TabGroup> = emptyList(),
+)
+
+/**
+ * Value type representing a tab group.
+ *
+ * @property id The unique ID of this tab group.
+ * @property name The name of this tab group.
+ * @property tabIds The IDs of all tabs in this group.
+ */
+data class TabGroup(
+ val id: String = UUID.randomUUID().toString(),
+ val name: String = "",
+ val tabIds: List<String> = emptyList(),
+)
+
+/**
+ * Returns the first tab group matching the provided [name], or null if no match
+ * was found. Note that we allow multiple groups with the same name in a
+ * partition but disambiguation needs to be handled on a feature level.
+ */
+fun TabPartition.getGroupByName(name: String) = this.tabGroups.firstOrNull {
+ it.name.equals(name, ignoreCase = true)
+}
+
+/**
+ * Returns the tab group matching the provided [id], or null if not match was found.
+ */
+fun TabPartition.getGroupById(id: String) = this.tabGroups.firstOrNull {
+ it.id == id
+}
+
+/**
+ * Check if a [TabPartition] has no tabs
+ *
+ * @return true if the [TabPartition] has no tabs, false otherwise.
+ */
+fun TabPartition?.isEmpty(): Boolean {
+ return this?.tabGroups?.filter { tabGroup -> tabGroup.tabIds.isNotEmpty() }
+ .isNullOrEmpty()
+}
+
+/**
+ * Check if a [TabPartition] has tabs
+ *
+ * @return true if the [TabPartition] has tabs, false otherwise.
+ */
+fun TabPartition?.isNotEmpty(): Boolean {
+ return isEmpty().not()
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt
new file mode 100644
index 0000000000..638a9706ba
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt
@@ -0,0 +1,150 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.storage.HistoryMetadataKey
+import java.util.UUID
+
+/**
+ * Value type that represents the state of a tab (private or normal).
+ *
+ * @property id the ID of this tab and session.
+ * @property content the [ContentState] of this tab.
+ * @property trackingProtection the [TrackingProtectionState] of this tab.
+ * @property translationsState the [TranslationsState] of this tab.
+ * @property cookieBanner the [CookieBannerHandlingStatus] of this tab.
+ * @property parentId the parent ID of this tab or null if this tab has no
+ * parent. The parent tab is usually the tab that initiated opening this
+ * tab (e.g. the user clicked a link with target="_blank" or selected
+ * "open in new tab" or a "window.open" was triggered).
+ * @property extensionState a map of web extension ids to extensions,
+ * that contains the overridden values for this tab.
+ * @property readerState the [ReaderState] of this tab.
+ * @property contextId the session context ID of this tab.
+ * @property lastAccess The last time this tab was selected (requires LastAccessMiddleware).
+ * @property createdAt Timestamp of this tab's creation.
+ * @property lastMediaAccessState - [LastMediaAccessState] detailing the tab state when media started playing.
+ * Requires [LastMediaAccessMiddleware] to update the value when playback starts.
+ * @property restored Indicates if this page was restored from a persisted state.
+ */
+data class TabSessionState(
+ override val id: String = UUID.randomUUID().toString(),
+ override val content: ContentState,
+ override val trackingProtection: TrackingProtectionState = TrackingProtectionState(),
+ override val translationsState: TranslationsState = TranslationsState(),
+ override val cookieBanner: CookieBannerHandlingStatus = CookieBannerHandlingStatus.NO_DETECTED,
+ override val engineState: EngineState = EngineState(),
+ override val extensionState: Map<String, WebExtensionState> = emptyMap(),
+ override val mediaSessionState: MediaSessionState? = null,
+ override val contextId: String? = null,
+ override val source: SessionState.Source = SessionState.Source.Internal.None,
+ override val restored: Boolean = false,
+ val parentId: String? = null,
+ val lastAccess: Long = 0L,
+ val createdAt: Long = System.currentTimeMillis(),
+ val lastMediaAccessState: LastMediaAccessState = LastMediaAccessState(),
+ val readerState: ReaderState = ReaderState(),
+ val historyMetadata: HistoryMetadataKey? = null,
+) : SessionState {
+
+ override fun createCopy(
+ id: String,
+ content: ContentState,
+ trackingProtection: TrackingProtectionState,
+ translationsState: TranslationsState,
+ engineState: EngineState,
+ extensionState: Map<String, WebExtensionState>,
+ mediaSessionState: MediaSessionState?,
+ contextId: String?,
+ cookieBanner: CookieBannerHandlingStatus,
+ ): SessionState = copy(
+ id = id,
+ content = content,
+ trackingProtection = trackingProtection,
+ translationsState = translationsState,
+ engineState = engineState,
+ extensionState = extensionState,
+ mediaSessionState = mediaSessionState,
+ contextId = contextId,
+ cookieBanner = cookieBanner,
+ )
+}
+
+/**
+ * Convenient function for creating a tab.
+ */
+fun createTab(
+ url: String,
+ private: Boolean = false,
+ id: String = UUID.randomUUID().toString(),
+ parent: TabSessionState? = null,
+ parentId: String? = null,
+ extensions: Map<String, WebExtensionState> = emptyMap(),
+ readerState: ReaderState = ReaderState(),
+ title: String = "",
+ contextId: String? = null,
+ lastAccess: Long = 0L,
+ createdAt: Long = System.currentTimeMillis(),
+ lastMediaAccessState: LastMediaAccessState = LastMediaAccessState(),
+ source: SessionState.Source = SessionState.Source.Internal.None,
+ restored: Boolean = false,
+ isProductUrl: Boolean = false,
+ engineSession: EngineSession? = null,
+ engineSessionState: EngineSessionState? = null,
+ crashed: Boolean = false,
+ mediaSessionState: MediaSessionState? = null,
+ historyMetadata: HistoryMetadataKey? = null,
+ webAppManifest: WebAppManifest? = null,
+ searchTerms: String = "",
+ initialLoadFlags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ initialAdditionalHeaders: Map<String, String>? = null,
+ previewImageUrl: String? = null,
+ hasFormData: Boolean = false,
+): TabSessionState {
+ return TabSessionState(
+ id = id,
+ content = ContentState(
+ url,
+ private,
+ title = title,
+ webAppManifest = webAppManifest,
+ searchTerms = searchTerms,
+ previewImageUrl = previewImageUrl,
+ hasFormData = hasFormData,
+ isProductUrl = isProductUrl,
+ ),
+ parentId = parentId ?: parent?.id,
+ extensionState = extensions,
+ readerState = readerState,
+ contextId = contextId,
+ lastAccess = lastAccess,
+ createdAt = createdAt,
+ lastMediaAccessState = lastMediaAccessState,
+ source = source,
+ restored = restored,
+ engineState = EngineState(
+ engineSession = engineSession,
+ engineSessionState = engineSessionState,
+ crashed = crashed,
+ initialLoadFlags = initialLoadFlags,
+ initialAdditionalHeaders = initialAdditionalHeaders,
+ ),
+ mediaSessionState = mediaSessionState,
+ historyMetadata = historyMetadata,
+ )
+}
+
+/**
+ * Indicates if the specified tab should be considered "inactive"
+ */
+fun TabSessionState.isActive(maxActiveTime: Long): Boolean {
+ val lastActiveTime = maxOf(lastAccess, createdAt)
+ val now = System.currentTimeMillis()
+ return (now - lastActiveTime <= maxActiveTime)
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TrackingProtectionState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TrackingProtectionState.kt
new file mode 100644
index 0000000000..7396234a26
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TrackingProtectionState.kt
@@ -0,0 +1,24 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.content.blocking.Tracker
+
+/**
+ * Value type that represents the state of tracking protection within a [SessionState].
+ *
+ * @property enabled Whether tracking protection is enabled or not for this [SessionState].
+ * @property blockedTrackers List of trackers that have been blocked for the currently loaded site.
+ * @property loadedTrackers List of trackers that have been loaded (not blocked) for the currently
+ * loaded site.
+ * @property ignoredOnTrackingProtection Whether tracking protection should be enabled or not for
+ * this [SessionState]
+ */
+data class TrackingProtectionState(
+ val enabled: Boolean = false,
+ val blockedTrackers: List<Tracker> = emptyList(),
+ val loadedTrackers: List<Tracker> = emptyList(),
+ val ignoredOnTrackingProtection: Boolean = false,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt
new file mode 100644
index 0000000000..2fb937f9f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt
@@ -0,0 +1,29 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.translate.LanguageModel
+import mozilla.components.concept.engine.translate.LanguageSetting
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationSupport
+
+/**
+ * Value type that represents the state of the translations engine within a [BrowserState].
+ *
+ * @property isEngineSupported Whether the translations engine supports the device architecture.
+ * @property supportedLanguages Set of languages the translation engine supports.
+ * @property languageModels Set of language machine learning translation models the translation engine has available.
+ * @property languageSettings A map containing a key of BCP 47 language code and its
+ * [LanguageSetting] to represent the automatic language settings.
+ * @property engineError Holds the error state of the translations engine.
+ * See [TranslationsState.translationError] for session level errors.
+ */
+data class TranslationsBrowserState(
+ val isEngineSupported: Boolean? = null,
+ val supportedLanguages: TranslationSupport? = null,
+ val languageModels: List<LanguageModel>? = null,
+ val languageSettings: Map<String, LanguageSetting>? = null,
+ val engineError: TranslationError? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsState.kt
new file mode 100644
index 0000000000..8c05340928
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsState.kt
@@ -0,0 +1,43 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.translate.TranslationDownloadSize
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationPageSettings
+
+/**
+ * Value type that represents the state of a translation within a [SessionState].
+ *
+ * @property isExpectedTranslate Expect the user to be interested in translating the page.
+ * @property isOfferTranslate Offer translating the page to the user.
+ * @property translationEngineState The state and expectations of the translation engine for the
+ * page.
+ * @property isTranslated The page is currently translated.
+ * @property isTranslateProcessing The page is currently attempting a translation.
+ * @property isRestoreProcessing The page is currently attempting a restoration.
+ * @property translationDownloadSize The download size for the given to/from translation pair. The
+ * translation engine requires the pair's ML models to be present on the device to complete a
+ * translation.
+ * @property pageSettings The translation engine settings that relate to the current page.
+ * @property neverTranslateSites List of sites the user has opted to never translate.
+ * @property translationError Type of error that occurred when acquiring resources, translating, or
+ * restoring a translation.
+ * @property settingsError Type of error that occurred when acquiring resources or setting preferences.
+ */
+data class TranslationsState(
+ val isExpectedTranslate: Boolean = false,
+ val isOfferTranslate: Boolean = false,
+ val translationEngineState: TranslationEngineState? = null,
+ val isTranslated: Boolean = false,
+ val isTranslateProcessing: Boolean = false,
+ val isRestoreProcessing: Boolean = false,
+ val translationDownloadSize: TranslationDownloadSize? = null,
+ val pageSettings: TranslationPageSettings? = null,
+ val neverTranslateSites: List<String>? = null,
+ val translationError: TranslationError? = null,
+ val settingsError: TranslationError? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt
new file mode 100644
index 0000000000..644088ae98
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt
@@ -0,0 +1,24 @@
+/* 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.browser.state.state
+
+import mozilla.components.browser.state.state.recover.RecoverableTab
+
+/**
+ * State keeping track of removed tabs to allow "undo".
+ *
+ * Currently the undo history only saves the tabs from the last remove operation. This is so far
+ * "good enough" since we also only show one undo snackbar for the last operation in the UI.
+ *
+ * @param tag A tag (usually a UUID) identifying this specific undo state. This tag can be used to
+ * avoid removing/restoring the wrong state in a multi-threaded environment.
+ * @param tabs List of previously removed tabs.
+ * @param selectedTabId Id of the tab in [tabs] that was selected and should get reselected on restore.
+ */
+data class UndoHistoryState(
+ val tag: String = "",
+ val tabs: List<RecoverableTab> = emptyList(),
+ val selectedTabId: String? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/WebExtensionState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/WebExtensionState.kt
new file mode 100644
index 0000000000..b66496ce4d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/WebExtensionState.kt
@@ -0,0 +1,37 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
+import mozilla.components.concept.engine.webextension.WebExtensionPageAction
+
+/**
+ * Value type that represents the state of a web extension.
+ *
+ * @property id The unique identifier for this web extension.
+ * @property url The url pointing to a resources path for locating the extension
+ * within the APK file e.g. resource://android/assets/extensions/my_web_ext.
+ * @property name The name of this web extension.
+ * @property enabled Whether or not this web extension is enabled, defaults to true.
+ * @property allowedInPrivateBrowsing Whether or not this web extension is allowed in private browsing
+ * mode. Defaults to false.
+ * @property browserAction The browser action state of this extension.
+ * @property pageAction The page action state of this extension.
+ * @property popupSessionId The ID of the session displaying
+ * the browser action popup.
+ * @property popupSession The [EngineSession] displaying the browser or page action popup.
+ */
+data class WebExtensionState(
+ val id: String,
+ val url: String? = null,
+ val name: String? = null,
+ val enabled: Boolean = true,
+ val allowedInPrivateBrowsing: Boolean = false,
+ val browserAction: WebExtensionBrowserAction? = null,
+ val pageAction: WebExtensionPageAction? = null,
+ val popupSessionId: String? = null,
+ val popupSession: EngineSession? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt
new file mode 100644
index 0000000000..f564f1c376
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt
@@ -0,0 +1,97 @@
+/* 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.browser.state.state.content
+
+import android.os.Environment
+import mozilla.components.concept.fetch.Response
+import java.io.File
+import java.util.UUID
+
+/**
+ * Value type that represents a download request.
+ *
+ * @property url The full url to the content that should be downloaded.
+ * @property fileName A canonical filename for this download.
+ * @property contentType Content type (MIME type) to indicate the media type of the download.
+ * @property contentLength The file size reported by the server.
+ * @property currentBytesCopied The number of current bytes copied.
+ * @property status The current status of the download.
+ * @property userAgent The user agent to be used for the download.
+ * @property destinationDirectory The matching destination directory for this type of download.
+ * @property filePath The file path the file was saved at.
+ * @property referrerUrl The site that linked to this download.
+ * @property skipConfirmation Whether or not the confirmation dialog should be shown before the download begins.
+ * @property openInApp Whether or not the file associated with this download should be opened in a
+ * third party app after downloaded successfully.
+ * @property id The unique identifier of this download.
+ * @property private Indicates if the download was created from a private session.
+ * @property createdTime A timestamp when the download was created.
+ * @property response A response object associated with this request, when provided can be
+ * used instead of performing a manual a download.
+ * @property notificationId Identifies the download notification in the status bar, if this
+ * [DownloadState] has one otherwise null.
+ */
+@Suppress("Deprecation")
+data class DownloadState(
+ val url: String,
+ val fileName: String? = null,
+ val contentType: String? = null,
+ val contentLength: Long? = null,
+ val currentBytesCopied: Long = 0,
+ val status: Status = Status.INITIATED,
+ val userAgent: String? = null,
+ val destinationDirectory: String = Environment.DIRECTORY_DOWNLOADS,
+ val referrerUrl: String? = null,
+ val skipConfirmation: Boolean = false,
+ val openInApp: Boolean = false,
+ val id: String = UUID.randomUUID().toString(),
+ val sessionId: String? = null,
+ val private: Boolean = false,
+ val createdTime: Long = System.currentTimeMillis(),
+ val response: Response? = null,
+ val notificationId: Int? = null,
+) {
+ val filePath: String get() =
+ Environment.getExternalStoragePublicDirectory(destinationDirectory).path + File.separatorChar + fileName
+
+ val directoryPath: String get() = Environment.getExternalStoragePublicDirectory(destinationDirectory).path
+
+ /**
+ * Status that represents every state that a download can be in.
+ */
+ @Suppress("MagicNumber")
+ enum class Status(val id: Int) {
+ /**
+ * Indicates that the download is in the first state after creation but not yet [DOWNLOADING].
+ */
+ INITIATED(1),
+
+ /**
+ * Indicates that an [INITIATED] download is now actively being downloaded.
+ */
+ DOWNLOADING(2),
+
+ /**
+ * Indicates that the download that has been [DOWNLOADING] has been paused.
+ */
+ PAUSED(3),
+
+ /**
+ * Indicates that the download that has been [DOWNLOADING] has been cancelled.
+ */
+ CANCELLED(4),
+
+ /**
+ * Indicates that the download that has been [DOWNLOADING] has moved to failed because
+ * something unexpected has happened.
+ */
+ FAILED(5),
+
+ /**
+ * Indicates that the [DOWNLOADING] download has been completed.
+ */
+ COMPLETED(6),
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/FindResultState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/FindResultState.kt
new file mode 100644
index 0000000000..c005a39b63
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/FindResultState.kt
@@ -0,0 +1,14 @@
+/* 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.browser.state.state.content
+
+/**
+ * A value type representing a result of a "find in page" operation.
+ *
+ * @property activeMatchOrdinal the zero-based ordinal of the currently selected match.
+ * @property numberOfMatches the match count
+ * @property isDoneCounting true if the find operation has completed, otherwise false.
+ */
+data class FindResultState(val activeMatchOrdinal: Int, val numberOfMatches: Int, val isDoneCounting: Boolean)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/HistoryState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/HistoryState.kt
new file mode 100644
index 0000000000..c290f54a4b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/HistoryState.kt
@@ -0,0 +1,20 @@
+/* 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.browser.state.state.content
+
+import mozilla.components.concept.engine.history.HistoryItem
+
+/**
+ * Value type that represents browser history.
+ *
+ * @property items All the items in the browser history.
+ * @property currentIndex The index of the currently selected [HistoryItem].
+ * If this is equal to lastIndex, then there are no pages to go "forward" to.
+ * If this is 0, then there are no pages to go "back" to.
+ */
+data class HistoryState(
+ val items: List<HistoryItem> = emptyList(),
+ val currentIndex: Int = 0,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt
new file mode 100644
index 0000000000..06930df565
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt
@@ -0,0 +1,52 @@
+/* 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.browser.state.state.content
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Value type that represents any information about permissions that should
+ * be brought to user's attention.
+ *
+ * @property notificationChanged indicates if the notification permission has changed from
+ * its default value.
+ * @property cameraChanged indicates if the camera permission has changed from its default value.
+ * @property locationChanged indicates if the location permission has changed from its default value.
+ * @property microphoneChanged indicates if the microphone permission has changed from its default value.
+ * @property persistentStorageChanged indicates if the persistent storage permission has
+ * changed from its default value.
+ * @property mediaKeySystemAccessChanged indicates if the media key systemAccess
+ * permission has changed from its default value.
+ * @property autoPlayAudibleChanged indicates if the autoplay audible permission has changed
+ * from its default value.
+ * @property autoPlayInaudibleChanged indicates if the autoplay inaudible permission has changed
+ * from its default value.
+ * @property autoPlayAudibleBlocking indicates if the autoplay audible setting disabled some
+ * web content from playing.
+ * @property autoPlayInaudibleBlocking indicates if the autoplay inaudible setting disabled
+ * some web content from playing.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class PermissionHighlightsState(
+ val notificationChanged: Boolean = false,
+ val cameraChanged: Boolean = false,
+ val locationChanged: Boolean = false,
+ val microphoneChanged: Boolean = false,
+ val persistentStorageChanged: Boolean = false,
+ val mediaKeySystemAccessChanged: Boolean = false,
+ val autoPlayAudibleChanged: Boolean = false,
+ val autoPlayInaudibleChanged: Boolean = false,
+ val autoPlayAudibleBlocking: Boolean = false,
+ val autoPlayInaudibleBlocking: Boolean = false,
+) : Parcelable {
+ val isAutoPlayBlocking get() = autoPlayAudibleBlocking || autoPlayInaudibleBlocking
+ val permissionsChanged
+ get() = notificationChanged || cameraChanged || locationChanged ||
+ microphoneChanged || persistentStorageChanged || mediaKeySystemAccessChanged ||
+ autoPlayAudibleChanged || autoPlayInaudibleChanged
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/ShareInternetResourceState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/ShareInternetResourceState.kt
new file mode 100644
index 0000000000..ad61b4b4c4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/ShareInternetResourceState.kt
@@ -0,0 +1,25 @@
+/* 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.browser.state.state.content
+
+import mozilla.components.concept.fetch.Response
+
+/**
+ * Value type that represents an Internet resource selected to be shared.
+ *
+ * @property url The full url to the content that should be shared.
+ * @property contentType Content type (MIME type) to indicate the media type of the resource.
+ * @property private Indicates if the share operation initiated from a private session.
+ * @property response A response object associated with this request, when provided can be
+ * used instead of performing a manual a download.
+ * @property referrerUrl An optional url of the referrer.
+ */
+data class ShareInternetResourceState(
+ val url: String,
+ val contentType: String? = null,
+ val private: Boolean = false,
+ val response: Response? = null,
+ val referrerUrl: String? = null,
+)
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/extension/WebExtensionPromptRequest.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/extension/WebExtensionPromptRequest.kt
new file mode 100644
index 0000000000..7f41264668
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/extension/WebExtensionPromptRequest.kt
@@ -0,0 +1,80 @@
+/* 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.browser.state.state.extension
+
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionInstallException
+
+/**
+ * Value type that represents a request for showing a native dialog from a [WebExtension].
+ *
+ * @param extension The [WebExtension] that requested the dialog to be shown.
+ */
+sealed class WebExtensionPromptRequest {
+
+ /**
+ * Value type that represents a request for showing a native dialog from a [WebExtension] before
+ * the installation succeeds.
+ */
+ sealed class BeforeInstallation(open val extension: WebExtension?) :
+ WebExtensionPromptRequest() {
+ /**
+ * Value type that represents a request for showing error prompt when an installation failed.
+ * @property extension The exception with failed to installed.
+ * @property exception The reason why the installation failed.
+ */
+ data class InstallationFailed(
+ override val extension: WebExtension?,
+ val exception: WebExtensionInstallException,
+ ) : BeforeInstallation(extension)
+ }
+
+ /**
+ * Value type that represents a request for showing a native dialog from a [WebExtension] after
+ * installation succeeds.
+ *
+ * @param extension The [WebExtension] that requested the dialog to be shown.
+ */
+ sealed class AfterInstallation(open val extension: WebExtension) : WebExtensionPromptRequest() {
+ /**
+ * Value type that represents a request for showing a permissions prompt.
+ */
+ sealed class Permissions(
+ override val extension: WebExtension,
+ open val onConfirm: (Boolean) -> Unit,
+ ) : AfterInstallation(extension) {
+ /**
+ * Value type that represents a request for a required permissions prompt.
+ * @property extension The [WebExtension] that requested the dialog to be shown.
+ * @property onConfirm A callback indicating whether the permissions were granted or not.
+ */
+ data class Required(
+ override val extension: WebExtension,
+ override val onConfirm: (Boolean) -> Unit,
+ ) : Permissions(extension, onConfirm)
+
+ /**
+ * Value type that represents a request for an optional permissions prompt.
+ * @property extension The [WebExtension] that requested the dialog to be shown.
+ * @property permissions The optional permissions to list in the dialog.
+ * @property onConfirm A callback indicating whether the permissions were granted or not.
+ */
+ data class Optional(
+ override val extension: WebExtension,
+ val permissions: List<String>,
+ override val onConfirm: (Boolean) -> Unit,
+ ) : Permissions(extension, onConfirm)
+ }
+
+ /**
+ * Value type that represents a request for showing post-installation prompt.
+ * Normally used to give users an opportunity to enable the [extension] in private browsing mode.
+ * @property extension The installed extension.
+ */
+ data class PostInstallation(
+ override val extension: WebExtension,
+ ) : AfterInstallation(extension)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/recover/RecoverableTab.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/recover/RecoverableTab.kt
new file mode 100644
index 0000000000..cf2b684b1b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/recover/RecoverableTab.kt
@@ -0,0 +1,119 @@
+/* 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.browser.state.state.recover
+
+import mozilla.components.browser.state.state.LastMediaAccessState
+import mozilla.components.browser.state.state.ReaderState
+import mozilla.components.browser.state.state.SessionState.Source
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.storage.HistoryMetadataKey
+
+/**
+ * A tab that is no longer open and in the list of tabs, but that can be restored (recovered) at
+ * any time if it's combined with an [EngineSessionState] to form a [RecoverableTab].
+ *
+ * The values of this data class are usually filled with the values of a [TabSessionState] when
+ * getting closed.
+ *
+ * @property id Unique ID identifying this tab.
+ * @property url The last URL of this tab.
+ * @property parentId The unique ID of the parent tab if this tab was opened from another tab (e.g. via
+ * the context menu).
+ * @property title The last title of this tab (or an empty String).
+ * @property searchTerm The last used search terms, or an empty string if no
+ * search was executed for this session.
+ * @property contextId The context ID ("container") this tab used (or null).
+ * @property readerState The last [ReaderState] of the tab.
+ * @property lastAccess The last time this tab was selected.
+ * @property createdAt Timestamp of the tab's creation.
+ * @property lastMediaAccessState Details about the last time was playing in this tab.
+ * @property private If tab was private.
+ * @property historyMetadata The last [HistoryMetadataKey] of the tab.
+ * @property source The last [Source] of the tab.
+ * @property index The index the tab should be restored at.
+ */
+data class TabState(
+ val id: String,
+ val url: String,
+ val parentId: String? = null,
+ val title: String = "",
+ val searchTerm: String = "",
+ val contextId: String? = null,
+ val readerState: ReaderState = ReaderState(),
+ val lastAccess: Long = 0,
+ val createdAt: Long = 0,
+ val lastMediaAccessState: LastMediaAccessState = LastMediaAccessState(),
+ val private: Boolean = false,
+ val historyMetadata: HistoryMetadataKey? = null,
+ val source: Source = Source.Internal.None,
+ val index: Int = -1,
+ val hasFormData: Boolean = false,
+)
+
+/**
+ * A recoverable version of [TabState].
+ *
+ * @property engineSessionState The [EngineSessionState] needed for restoring the previous state of this tab.
+ * @property state A [TabState] instance containing basic tab state.
+ */
+data class RecoverableTab(
+ val engineSessionState: EngineSessionState?,
+ val state: TabState,
+)
+
+/**
+ * Creates a [RecoverableTab] from this [TabSessionState].
+ */
+fun TabSessionState.toRecoverableTab(index: Int = -1): RecoverableTab {
+ return RecoverableTab(
+ engineSessionState = engineState.engineSessionState,
+ state = TabState(
+ id = id,
+ parentId = parentId,
+ url = content.url,
+ title = content.title,
+ searchTerm = content.searchTerms,
+ contextId = contextId,
+ readerState = readerState,
+ lastAccess = lastAccess,
+ createdAt = createdAt,
+ lastMediaAccessState = lastMediaAccessState,
+ private = content.private,
+ historyMetadata = historyMetadata,
+ source = source,
+ index = index,
+ hasFormData = content.hasFormData,
+ ),
+ )
+}
+
+/**
+ * Creates a [TabSessionState] from this [RecoverableTab].
+ */
+fun RecoverableTab.toTabSessionState() = createTab(
+ id = state.id,
+ url = state.url,
+ parentId = state.parentId,
+ title = state.title,
+ searchTerms = state.searchTerm,
+ contextId = state.contextId,
+ engineSessionState = engineSessionState,
+ readerState = state.readerState,
+ lastAccess = state.lastAccess,
+ createdAt = state.createdAt,
+ lastMediaAccessState = state.lastMediaAccessState,
+ private = state.private,
+ historyMetadata = state.historyMetadata,
+ source = state.source,
+ restored = true,
+ hasFormData = state.hasFormData,
+)
+
+/**
+ * Creates a list of [TabSessionState]s from a List of [RecoverableTab]s.
+ */
+fun List<RecoverableTab>.toTabSessionStates() = map { it.toTabSessionState() }
diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/store/BrowserStore.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/store/BrowserStore.kt
new file mode 100644
index 0000000000..573701f69f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/store/BrowserStore.kt
@@ -0,0 +1,49 @@
+/* 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.browser.state.store
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.reducer.BrowserStateReducer
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.Store
+import java.lang.IllegalArgumentException
+
+/**
+ * The [BrowserStore] holds the [BrowserState] (state tree).
+ *
+ * The only way to change the [BrowserState] inside [BrowserStore] is to dispatch an [Action] on it.
+ */
+class BrowserStore(
+ initialState: BrowserState = BrowserState(),
+ middleware: List<Middleware<BrowserState, BrowserAction>> = emptyList(),
+) : Store<BrowserState, BrowserAction>(
+ initialState,
+ BrowserStateReducer::reduce,
+ middleware,
+ "BrowserStore",
+) {
+ init {
+ initialState.selectedTabId?.let {
+ if (state.findTab(it) == null) {
+ throw IllegalArgumentException("Selected tab does not exist")
+ }
+ }
+
+ if (initialState.tabs
+ .groupingBy { it.id }
+ .eachCount()
+ .filter { it.value > 1 }
+ .isNotEmpty()
+ ) {
+ throw IllegalArgumentException("Duplicate tabs found")
+ }
+
+ dispatch(InitAction)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/AwesomeBarActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/AwesomeBarActionTest.kt
new file mode 100644
index 0000000000..25ab6bec04
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/AwesomeBarActionTest.kt
@@ -0,0 +1,111 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.state.AwesomeBarState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class AwesomeBarActionTest {
+ @Test
+ fun `VisibilityStateUpdated - Stores updated visibility state`() {
+ val store = BrowserStore()
+
+ assertTrue(store.state.awesomeBarState.visibilityState.visibleProviderGroups.isEmpty())
+ assertNull(store.state.awesomeBarState.clickedSuggestion)
+
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(AwesomeBar.Suggestion(provider))
+
+ store.dispatch(
+ AwesomeBarAction.VisibilityStateUpdated(
+ AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ ),
+ ).joinBlocking()
+
+ assertEquals(1, store.state.awesomeBarState.visibilityState.visibleProviderGroups.size)
+ assertEquals(providerGroupSuggestions, store.state.awesomeBarState.visibilityState.visibleProviderGroups[providerGroup])
+ assertNull(store.state.awesomeBarState.clickedSuggestion)
+ }
+
+ @Test
+ fun `SuggestionClicked - Stores clicked suggestion`() {
+ val store = BrowserStore()
+
+ assertTrue(store.state.awesomeBarState.visibilityState.visibleProviderGroups.isEmpty())
+ assertNull(store.state.awesomeBarState.clickedSuggestion)
+
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val suggestion = AwesomeBar.Suggestion(provider)
+
+ store.dispatch(AwesomeBarAction.SuggestionClicked(suggestion)).joinBlocking()
+
+ assertTrue(store.state.awesomeBarState.visibilityState.visibleProviderGroups.isEmpty())
+ assertEquals(suggestion, store.state.awesomeBarState.clickedSuggestion)
+ }
+
+ @Test
+ fun `EngagementFinished - Completed engagement resets state`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val suggestion = AwesomeBar.Suggestion(provider)
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(suggestion)
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ clickedSuggestion = suggestion,
+ ),
+ ),
+ )
+
+ assertTrue(store.state.awesomeBarState.visibilityState.visibleProviderGroups.isNotEmpty())
+ assertNotNull(store.state.awesomeBarState.clickedSuggestion)
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertTrue(store.state.awesomeBarState.visibilityState.visibleProviderGroups.isEmpty())
+ assertNull(store.state.awesomeBarState.clickedSuggestion)
+ }
+
+ @Test
+ fun `EngagementFinished - Abandoned engagement resets state`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val suggestion = AwesomeBar.Suggestion(provider)
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(suggestion)
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ clickedSuggestion = suggestion,
+ ),
+ ),
+ )
+
+ assertTrue(store.state.awesomeBarState.visibilityState.visibleProviderGroups.isNotEmpty())
+ assertNotNull(store.state.awesomeBarState.clickedSuggestion)
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = true)).joinBlocking()
+
+ assertTrue(store.state.awesomeBarState.visibilityState.visibleProviderGroups.isEmpty())
+ assertNull(store.state.awesomeBarState.clickedSuggestion)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContainerActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContainerActionTest.kt
new file mode 100644
index 0000000000..8a6d000d47
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContainerActionTest.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.browser.state.action
+
+import mozilla.components.browser.state.state.ContainerState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ContainerActionTest {
+
+ @Test
+ fun `AddContainerAction - Adds a container to the BrowserState containers`() {
+ val store = BrowserStore()
+
+ assertTrue(store.state.containers.isEmpty())
+
+ val container = ContainerState(
+ contextId = "contextId",
+ name = "Personal",
+ color = ContainerState.Color.GREEN,
+ icon = ContainerState.Icon.CART,
+ )
+ store.dispatch(ContainerAction.AddContainerAction(container)).joinBlocking()
+
+ assertFalse(store.state.containers.isEmpty())
+ assertEquals(container, store.state.containers.values.first())
+
+ val state = store.state
+ store.dispatch(ContainerAction.AddContainerAction(container)).joinBlocking()
+ assertSame(state, store.state)
+ }
+
+ @Test
+ fun `AddContainersAction - Adds a list of containers to the BrowserState containers`() {
+ val store = BrowserStore()
+
+ assertTrue(store.state.containers.isEmpty())
+
+ val container1 = ContainerState(
+ contextId = "1",
+ name = "Personal",
+ color = ContainerState.Color.GREEN,
+ icon = ContainerState.Icon.CART,
+ )
+ val container2 = ContainerState(
+ contextId = "2",
+ name = "Work",
+ color = ContainerState.Color.RED,
+ icon = ContainerState.Icon.FINGERPRINT,
+ )
+ val container3 = ContainerState(
+ contextId = "3",
+ name = "Shopping",
+ color = ContainerState.Color.BLUE,
+ icon = ContainerState.Icon.BRIEFCASE,
+ )
+ store.dispatch(ContainerAction.AddContainersAction(listOf(container1, container2))).joinBlocking()
+
+ assertFalse(store.state.containers.isEmpty())
+ assertEquals(container1, store.state.containers.values.first())
+ assertEquals(container2, store.state.containers.values.last())
+
+ // Assert that the state remains the same if the existing containers are re-added.
+ val state = store.state
+ store.dispatch(ContainerAction.AddContainersAction(listOf(container1, container2))).joinBlocking()
+ assertSame(state, store.state)
+
+ // Assert that only non-existing containers are added.
+ store.dispatch(ContainerAction.AddContainersAction(listOf(container1, container2, container3))).joinBlocking()
+ assertEquals(3, store.state.containers.size)
+ assertEquals(container1, store.state.containers.values.first())
+ assertEquals(container2, store.state.containers.values.elementAt(1))
+ assertEquals(container3, store.state.containers.values.last())
+ }
+
+ @Test
+ fun `RemoveContainerAction - Removes a container from the BrowserState containers`() {
+ val store = BrowserStore()
+
+ assertTrue(store.state.containers.isEmpty())
+
+ val container1 = ContainerState(
+ contextId = "1",
+ name = "Personal",
+ color = ContainerState.Color.BLUE,
+ icon = ContainerState.Icon.BRIEFCASE,
+ )
+ val container2 = ContainerState(
+ contextId = "2",
+ name = "Shopping",
+ color = ContainerState.Color.GREEN,
+ icon = ContainerState.Icon.CIRCLE,
+ )
+ store.dispatch(ContainerAction.AddContainerAction(container1)).joinBlocking()
+ store.dispatch(ContainerAction.AddContainerAction(container2)).joinBlocking()
+
+ assertFalse(store.state.containers.isEmpty())
+ assertEquals(container1, store.state.containers.values.first())
+ assertEquals(container2, store.state.containers.values.last())
+
+ store.dispatch(ContainerAction.RemoveContainerAction(container1.contextId)).joinBlocking()
+
+ assertEquals(1, store.state.containers.size)
+ assertEquals(container2, store.state.containers.values.first())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt
new file mode 100644
index 0000000000..d62157da48
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt
@@ -0,0 +1,894 @@
+/* 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.browser.state.action
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayAudibleBlockingAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayAudibleChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayInAudibleBlockingAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayInAudibleChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.CameraChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.LocationChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.MediaKeySystemAccesChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.MicrophoneChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.NotificationChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.PersistentStorageChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.Reset
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.state.AppIntentState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.LoadRequestState
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.FindResultState
+import mozilla.components.browser.state.state.content.HistoryState
+import mozilla.components.browser.state.state.content.PermissionHighlightsState
+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.HitResult
+import mozilla.components.concept.engine.history.HistoryItem
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+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.Mockito.spy
+
+@RunWith(AndroidJUnit4::class)
+class ContentActionTest {
+ private lateinit var store: BrowserStore
+ private lateinit var tabId: String
+ private lateinit var otherTabId: String
+
+ private val tab: TabSessionState
+ get() = store.state.tabs.find { it.id == tabId }!!
+
+ private val otherTab: TabSessionState
+ get() = store.state.tabs.find { it.id == otherTabId }!!
+
+ @Before
+ fun setUp() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(url = "https://www.mozilla.org").also {
+ tabId = it.id
+ },
+ createTab(url = "https://www.firefox.com").also {
+ otherTabId = it.id
+ },
+ ),
+ )
+
+ store = BrowserStore(state)
+ }
+
+ @Test
+ fun `UpdateUrlAction updates URL`() {
+ val newUrl = "https://www.example.org"
+
+ assertNotEquals(newUrl, tab.content.url)
+ assertNotEquals(newUrl, otherTab.content.url)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(tab.id, newUrl),
+ ).joinBlocking()
+
+ assertEquals(newUrl, tab.content.url)
+ assertNotEquals(newUrl, otherTab.content.url)
+ }
+
+ @Test
+ fun `UpdateUrlAction clears icon`() {
+ val icon = spy(Bitmap::class.java)
+
+ assertNotEquals(icon, tab.content.icon)
+ assertNotEquals(icon, otherTab.content.icon)
+
+ store.dispatch(
+ ContentAction.UpdateIconAction(tab.id, tab.content.url, icon),
+ ).joinBlocking()
+
+ assertEquals(icon, tab.content.icon)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(tab.id, "https://www.example.org"),
+ ).joinBlocking()
+
+ assertNull(tab.content.icon)
+ }
+
+ @Test
+ fun `UpdateUrlAction does not clear icon if host is the same`() {
+ val icon = spy(Bitmap::class.java)
+
+ assertNotEquals(icon, tab.content.icon)
+ assertNotEquals(icon, otherTab.content.icon)
+
+ store.dispatch(
+ ContentAction.UpdateIconAction(tab.id, tab.content.url, icon),
+ ).joinBlocking()
+
+ assertEquals(icon, tab.content.icon)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(tab.id, "https://www.mozilla.org/firefox"),
+ ).joinBlocking()
+
+ assertEquals(icon, tab.content.icon)
+ }
+
+ @Test
+ fun `WHEN UpdateUrlAction is dispatched by user gesture THEN the search terms are cleared`() {
+ val searchTerms = "Firefox"
+ store.dispatch(
+ ContentAction.UpdateSearchTermsAction(tab.id, searchTerms),
+ ).joinBlocking()
+
+ assertEquals(searchTerms, tab.content.searchTerms)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(tab.id, "https://www.mozilla.org", false),
+ ).joinBlocking()
+
+ assertEquals(searchTerms, tab.content.searchTerms)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(tab.id, "https://www.mozilla.org/firefox", true),
+ ).joinBlocking()
+
+ assertEquals("", tab.content.searchTerms)
+ }
+
+ @Test
+ fun `UpdateLoadingStateAction updates loading state`() {
+ assertFalse(tab.content.loading)
+ assertFalse(otherTab.content.loading)
+
+ store.dispatch(
+ ContentAction.UpdateLoadingStateAction(tab.id, true),
+ ).joinBlocking()
+
+ assertTrue(tab.content.loading)
+ assertFalse(otherTab.content.loading)
+
+ store.dispatch(
+ ContentAction.UpdateLoadingStateAction(tab.id, false),
+ ).joinBlocking()
+
+ assertFalse(tab.content.loading)
+ assertFalse(otherTab.content.loading)
+
+ store.dispatch(
+ ContentAction.UpdateLoadingStateAction(tab.id, true),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.UpdateLoadingStateAction(otherTab.id, true),
+ ).joinBlocking()
+
+ assertTrue(tab.content.loading)
+ assertTrue(otherTab.content.loading)
+ }
+
+ @Test
+ fun `UpdateRefreshCanceledStateAction updates refreshCanceled state`() {
+ assertFalse(tab.content.refreshCanceled)
+ assertFalse(otherTab.content.refreshCanceled)
+
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.refreshCanceled)
+ assertFalse(otherTab.content.refreshCanceled)
+
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tab.id, false)).joinBlocking()
+
+ assertFalse(tab.content.refreshCanceled)
+ assertFalse(otherTab.content.refreshCanceled)
+
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tab.id, true)).joinBlocking()
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(otherTab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.refreshCanceled)
+ assertTrue(otherTab.content.refreshCanceled)
+ }
+
+ @Test
+ fun `UpdateTitleAction updates title`() {
+ val newTitle = "This is a title"
+
+ assertNotEquals(newTitle, tab.content.title)
+ assertNotEquals(newTitle, otherTab.content.title)
+
+ store.dispatch(
+ ContentAction.UpdateTitleAction(tab.id, newTitle),
+ ).joinBlocking()
+
+ assertEquals(newTitle, tab.content.title)
+ assertNotEquals(newTitle, otherTab.content.title)
+ }
+
+ @Test
+ fun `UpdatePreviewImageAction updates previewImageUrl state`() {
+ val newPreviewImageUrl = "https://test.com/og-image-url"
+
+ assertNotEquals(newPreviewImageUrl, tab.content.previewImageUrl)
+ assertNotEquals(newPreviewImageUrl, otherTab.content.previewImageUrl)
+
+ store.dispatch(
+ ContentAction.UpdatePreviewImageAction(tab.id, newPreviewImageUrl),
+ ).joinBlocking()
+
+ assertEquals(newPreviewImageUrl, tab.content.previewImageUrl)
+ assertNotEquals(newPreviewImageUrl, otherTab.content.previewImageUrl)
+ }
+
+ @Test
+ fun `UpdateProgressAction updates progress`() {
+ assertEquals(0, tab.content.progress)
+ assertEquals(0, otherTab.content.progress)
+
+ store.dispatch(ContentAction.UpdateProgressAction(tab.id, 75)).joinBlocking()
+
+ assertEquals(75, tab.content.progress)
+ assertEquals(0, otherTab.content.progress)
+
+ store.dispatch(ContentAction.UpdateProgressAction(otherTab.id, 25)).joinBlocking()
+ store.dispatch(ContentAction.UpdateProgressAction(tab.id, 85)).joinBlocking()
+
+ assertEquals(85, tab.content.progress)
+ assertEquals(25, otherTab.content.progress)
+ }
+
+ @Test
+ fun `UpdateSearchTermsAction updates URL`() {
+ val searchTerms = "Hello World"
+
+ assertNotEquals(searchTerms, tab.content.searchTerms)
+ assertNotEquals(searchTerms, otherTab.content.searchTerms)
+
+ store.dispatch(
+ ContentAction.UpdateSearchTermsAction(tab.id, searchTerms),
+ ).joinBlocking()
+
+ assertEquals(searchTerms, tab.content.searchTerms)
+ assertNotEquals(searchTerms, otherTab.content.searchTerms)
+ }
+
+ @Test
+ fun `UpdateSecurityInfo updates securityInfo`() {
+ val newSecurityInfo = SecurityInfoState(true, "mozilla.org", "The Mozilla Team")
+
+ assertNotEquals(newSecurityInfo, tab.content.securityInfo)
+ assertNotEquals(newSecurityInfo, otherTab.content.securityInfo)
+
+ store.dispatch(
+ ContentAction.UpdateSecurityInfoAction(tab.id, newSecurityInfo),
+ ).joinBlocking()
+
+ assertEquals(newSecurityInfo, tab.content.securityInfo)
+ assertNotEquals(newSecurityInfo, otherTab.content.securityInfo)
+
+ assertEquals(true, tab.content.securityInfo.secure)
+ assertEquals("mozilla.org", tab.content.securityInfo.host)
+ assertEquals("The Mozilla Team", tab.content.securityInfo.issuer)
+ }
+
+ @Test
+ fun `UpdateIconAction updates icon`() {
+ val icon = spy(Bitmap::class.java)
+
+ assertNotEquals(icon, tab.content.icon)
+ assertNotEquals(icon, otherTab.content.icon)
+
+ store.dispatch(
+ ContentAction.UpdateIconAction(tab.id, tab.content.url, icon),
+ ).joinBlocking()
+
+ assertEquals(icon, tab.content.icon)
+ assertNotEquals(icon, otherTab.content.icon)
+ }
+
+ @Test
+ fun `UpdateIconAction does not update icon if page URL is different`() {
+ val icon = spy(Bitmap::class.java)
+
+ assertNotEquals(icon, tab.content.icon)
+ assertNotEquals(icon, otherTab.content.icon)
+
+ store.dispatch(
+ ContentAction.UpdateIconAction(tab.id, "https://different.example.org", icon),
+ ).joinBlocking()
+
+ assertNull(tab.content.icon)
+ }
+
+ @Test
+ fun `RemoveIconAction removes icon`() {
+ val icon = spy(Bitmap::class.java)
+
+ assertNotEquals(icon, tab.content.icon)
+
+ store.dispatch(
+ ContentAction.UpdateIconAction(tab.id, tab.content.url, icon),
+ ).joinBlocking()
+
+ assertEquals(icon, tab.content.icon)
+
+ store.dispatch(
+ ContentAction.RemoveIconAction(tab.id),
+ ).joinBlocking()
+
+ assertNull(tab.content.icon)
+ }
+
+ @Test
+ fun `Updating custom tab`() {
+ val customTab = createCustomTab("https://getpocket.com")
+ val otherCustomTab = createCustomTab("https://www.google.com")
+
+ store.dispatch(CustomTabListAction.AddCustomTabAction(customTab)).joinBlocking()
+ store.dispatch(CustomTabListAction.AddCustomTabAction(otherCustomTab)).joinBlocking()
+
+ store.dispatch(ContentAction.UpdateUrlAction(customTab.id, "https://www.example.org")).joinBlocking()
+ store.dispatch(ContentAction.UpdateTitleAction(customTab.id, "I am a custom tab")).joinBlocking()
+
+ val updatedCustomTab = store.state.findCustomTab(customTab.id)!!
+ val updatedOtherCustomTab = store.state.findCustomTab(otherCustomTab.id)!!
+
+ assertEquals("https://www.example.org", updatedCustomTab.content.url)
+ assertNotEquals("https://www.example.org", updatedOtherCustomTab.content.url)
+ assertNotEquals("https://www.example.org", tab.content.url)
+ assertNotEquals("https://www.example.org", otherTab.content.url)
+
+ assertEquals("I am a custom tab", updatedCustomTab.content.title)
+ assertNotEquals("I am a custom tab", updatedOtherCustomTab.content.title)
+ assertNotEquals("I am a custom tab", tab.content.title)
+ assertNotEquals("I am a custom tab", otherTab.content.title)
+ }
+
+ @Test
+ fun `UpdateDownloadAction updates download`() {
+ assertNull(tab.content.download)
+
+ val download1 = DownloadState(
+ url = "https://www.mozilla.org",
+ sessionId = tab.id,
+ )
+
+ store.dispatch(
+ ContentAction.UpdateDownloadAction(tab.id, download1),
+ ).joinBlocking()
+
+ assertEquals(download1.url, tab.content.download?.url)
+ assertEquals(download1.sessionId, tab.content.download?.sessionId)
+
+ val download2 = DownloadState(
+ url = "https://www.wikipedia.org",
+ sessionId = tab.id,
+ )
+
+ store.dispatch(
+ ContentAction.UpdateDownloadAction(tab.id, download2),
+ ).joinBlocking()
+
+ assertEquals(download2.url, tab.content.download?.url)
+ assertEquals(download2.sessionId, tab.content.download?.sessionId)
+ }
+
+ @Test
+ fun `ConsumeDownloadAction removes download`() {
+ val download = DownloadState(
+ id = "1337",
+ url = "https://www.mozilla.org",
+ sessionId = tab.id,
+ )
+
+ store.dispatch(
+ ContentAction.UpdateDownloadAction(tab.id, download),
+ ).joinBlocking()
+
+ assertEquals(download, tab.content.download)
+
+ store.dispatch(
+ ContentAction.ConsumeDownloadAction(tab.id, downloadId = "1337"),
+ ).joinBlocking()
+
+ assertNull(tab.content.download)
+ }
+
+ @Test
+ fun `CancelDownloadAction removes download`() {
+ val download = DownloadState(
+ id = "1337",
+ url = "https://www.mozilla.org",
+ sessionId = tab.id,
+ )
+
+ store.dispatch(
+ ContentAction.UpdateDownloadAction(tab.id, download),
+ ).joinBlocking()
+
+ assertEquals(download, tab.content.download)
+
+ store.dispatch(
+ ContentAction.CancelDownloadAction(tab.id, downloadId = "1337"),
+ ).joinBlocking()
+
+ assertNull(tab.content.download)
+ }
+
+ @Test
+ fun `ConsumeDownloadAction does not remove download with different id`() {
+ val download = DownloadState(
+ id = "1337",
+ url = "https://www.mozilla.org",
+ sessionId = tab.id,
+ )
+
+ store.dispatch(
+ ContentAction.UpdateDownloadAction(tab.id, download),
+ ).joinBlocking()
+
+ assertEquals(download, tab.content.download)
+
+ store.dispatch(
+ ContentAction.ConsumeDownloadAction(tab.id, downloadId = "4223"),
+ ).joinBlocking()
+
+ assertNotNull(tab.content.download)
+ }
+
+ @Test
+ fun `UpdateHitResultAction updates hit result`() {
+ assertNull(tab.content.hitResult)
+
+ val hitResult1: HitResult = HitResult.UNKNOWN("file://foo")
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(tab.id, hitResult1),
+ ).joinBlocking()
+
+ assertEquals(hitResult1, tab.content.hitResult)
+
+ val hitResult2: HitResult = HitResult.UNKNOWN("file://bar")
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(tab.id, hitResult2),
+ ).joinBlocking()
+
+ assertEquals(hitResult2, tab.content.hitResult)
+ }
+
+ @Test
+ fun `ConsumeHitResultAction removes hit result`() {
+ val hitResult: HitResult = HitResult.UNKNOWN("file://foo")
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(tab.id, hitResult),
+ ).joinBlocking()
+
+ assertEquals(hitResult, tab.content.hitResult)
+
+ store.dispatch(
+ ContentAction.ConsumeHitResultAction(tab.id),
+ ).joinBlocking()
+
+ assertNull(tab.content.hitResult)
+ }
+
+ @Test
+ fun `UpdatePromptRequestAction updates requests`() {
+ assertTrue(tab.content.promptRequests.isEmpty())
+
+ val promptRequest1: PromptRequest = mock<PromptRequest.SingleChoice>()
+
+ store.dispatch(
+ ContentAction.UpdatePromptRequestAction(tab.id, promptRequest1),
+ ).joinBlocking()
+
+ assertEquals(1, tab.content.promptRequests.size)
+ assertEquals(promptRequest1, tab.content.promptRequests[0])
+
+ val promptRequest2: PromptRequest = mock<PromptRequest.MultipleChoice>()
+
+ store.dispatch(
+ ContentAction.UpdatePromptRequestAction(tab.id, promptRequest2),
+ ).joinBlocking()
+
+ assertEquals(2, tab.content.promptRequests.size)
+ assertEquals(promptRequest1, tab.content.promptRequests[0])
+ assertEquals(promptRequest2, tab.content.promptRequests[1])
+ }
+
+ @Test
+ fun `ConsumePromptRequestAction removes request`() {
+ val promptRequest: PromptRequest = mock<PromptRequest.SingleChoice>()
+
+ store.dispatch(
+ ContentAction.UpdatePromptRequestAction(tab.id, promptRequest),
+ ).joinBlocking()
+
+ assertEquals(1, tab.content.promptRequests.size)
+ assertEquals(promptRequest, tab.content.promptRequests[0])
+
+ store.dispatch(
+ ContentAction.ConsumePromptRequestAction(tab.id, promptRequest),
+ ).joinBlocking()
+
+ assertTrue(tab.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `AddFindResultAction adds result`() {
+ assertTrue(tab.content.findResults.isEmpty())
+
+ val result: FindResultState = mock()
+ store.dispatch(
+ ContentAction.AddFindResultAction(tab.id, result),
+ ).joinBlocking()
+
+ assertEquals(1, tab.content.findResults.size)
+ assertEquals(result, tab.content.findResults.last())
+
+ val result2: FindResultState = mock()
+ store.dispatch(
+ ContentAction.AddFindResultAction(tab.id, result2),
+ ).joinBlocking()
+
+ assertEquals(2, tab.content.findResults.size)
+ assertEquals(result2, tab.content.findResults.last())
+ }
+
+ @Test
+ fun `ClearFindResultsAction removes all results`() {
+ store.dispatch(
+ ContentAction.AddFindResultAction(tab.id, mock()),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.AddFindResultAction(tab.id, mock()),
+ ).joinBlocking()
+
+ assertEquals(2, tab.content.findResults.size)
+
+ store.dispatch(
+ ContentAction.ClearFindResultsAction(tab.id),
+ ).joinBlocking()
+
+ assertTrue(tab.content.findResults.isEmpty())
+ }
+
+ @Test
+ fun `UpdateWindowRequestAction updates request`() {
+ assertNull(tab.content.windowRequest)
+
+ val windowRequest1: WindowRequest = mock()
+
+ store.dispatch(
+ ContentAction.UpdateWindowRequestAction(tab.id, windowRequest1),
+ ).joinBlocking()
+
+ assertEquals(windowRequest1, tab.content.windowRequest)
+
+ val windowRequest2: WindowRequest = mock()
+
+ store.dispatch(
+ ContentAction.UpdateWindowRequestAction(tab.id, windowRequest2),
+ ).joinBlocking()
+
+ assertEquals(windowRequest2, tab.content.windowRequest)
+ }
+
+ @Test
+ fun `ConsumeWindowRequestAction removes request`() {
+ val windowRequest: WindowRequest = mock()
+
+ store.dispatch(
+ ContentAction.UpdateWindowRequestAction(tab.id, windowRequest),
+ ).joinBlocking()
+
+ assertEquals(windowRequest, tab.content.windowRequest)
+
+ store.dispatch(
+ ContentAction.ConsumeWindowRequestAction(tab.id),
+ ).joinBlocking()
+
+ assertNull(tab.content.windowRequest)
+ }
+
+ @Test
+ fun `UpdateBackNavigationStateAction updates canGoBack`() {
+ assertFalse(tab.content.canGoBack)
+ assertFalse(otherTab.content.canGoBack)
+
+ store.dispatch(ContentAction.UpdateBackNavigationStateAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.canGoBack)
+ assertFalse(otherTab.content.canGoBack)
+
+ store.dispatch(ContentAction.UpdateBackNavigationStateAction(tab.id, false)).joinBlocking()
+
+ assertFalse(tab.content.canGoBack)
+ assertFalse(otherTab.content.canGoBack)
+ }
+
+ @Test
+ fun `UpdateForwardNavigationStateAction updates canGoForward`() {
+ assertFalse(tab.content.canGoForward)
+ assertFalse(otherTab.content.canGoForward)
+
+ store.dispatch(ContentAction.UpdateForwardNavigationStateAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.canGoForward)
+ assertFalse(otherTab.content.canGoForward)
+
+ store.dispatch(ContentAction.UpdateForwardNavigationStateAction(tab.id, false)).joinBlocking()
+
+ assertFalse(tab.content.canGoForward)
+ assertFalse(otherTab.content.canGoForward)
+ }
+
+ @Test
+ fun `UpdateWebAppManifestAction updates web app manifest`() {
+ val manifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ )
+
+ assertNotEquals(manifest, tab.content.webAppManifest)
+ assertNotEquals(manifest, otherTab.content.webAppManifest)
+
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(tab.id, manifest),
+ ).joinBlocking()
+
+ assertEquals(manifest, tab.content.webAppManifest)
+ assertNotEquals(manifest, otherTab.content.webAppManifest)
+ }
+
+ @Test
+ fun `RemoveWebAppManifestAction removes web app manifest`() {
+ val manifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ )
+
+ assertNotEquals(manifest, tab.content.webAppManifest)
+
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(tab.id, manifest),
+ ).joinBlocking()
+
+ assertEquals(manifest, tab.content.webAppManifest)
+
+ store.dispatch(
+ ContentAction.RemoveWebAppManifestAction(tab.id),
+ ).joinBlocking()
+
+ assertNull(tab.content.webAppManifest)
+ }
+
+ @Test
+ fun `UpdateHistoryStateAction updates history state`() {
+ val historyState = HistoryState(
+ items = listOf(
+ HistoryItem("Mozilla", "https://mozilla.org"),
+ HistoryItem("Firefox", "https://firefox.com"),
+ ),
+ currentIndex = 1,
+ )
+
+ assertNotEquals(historyState, tab.content.history)
+ assertNotEquals(historyState, otherTab.content.history)
+
+ store.dispatch(
+ ContentAction.UpdateHistoryStateAction(tab.id, historyState.items, historyState.currentIndex),
+ ).joinBlocking()
+
+ assertEquals(historyState, tab.content.history)
+ assertNotEquals(historyState, otherTab.content.history)
+ }
+
+ @Test
+ fun `UpdateLoadRequestAction updates load request state`() {
+ val loadRequestUrl = "https://mozilla.org"
+
+ store.dispatch(
+ ContentAction.UpdateLoadRequestAction(tab.id, LoadRequestState(loadRequestUrl, true, false)),
+ ).joinBlocking()
+
+ assertNotNull(tab.content.loadRequest)
+ assertEquals(loadRequestUrl, tab.content.loadRequest!!.url)
+ assertTrue(tab.content.loadRequest!!.triggeredByRedirect)
+ assertFalse(tab.content.loadRequest!!.triggeredByUser)
+ }
+
+ @Test
+ fun `UpdateDesktopModeEnabledAction updates desktopModeEnabled`() {
+ assertFalse(tab.content.desktopMode)
+ assertFalse(otherTab.content.desktopMode)
+
+ store.dispatch(ContentAction.UpdateDesktopModeAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.desktopMode)
+ assertFalse(otherTab.content.desktopMode)
+
+ store.dispatch(ContentAction.UpdateDesktopModeAction(tab.id, false)).joinBlocking()
+
+ assertFalse(tab.content.desktopMode)
+ assertFalse(otherTab.content.desktopMode)
+ }
+
+ @Test
+ fun `WHEN dispatching NotificationChangedAction THEN notificationChanged state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.notificationChanged)
+
+ store.dispatch(NotificationChangedAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.notificationChanged)
+ }
+
+ @Test
+ fun `WHEN dispatching CameraChangedAction THEN cameraChanged state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.cameraChanged)
+
+ store.dispatch(CameraChangedAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.cameraChanged)
+ }
+
+ @Test
+ fun `WHEN dispatching LocationChangedAction THEN locationChanged state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.locationChanged)
+
+ store.dispatch(LocationChangedAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.locationChanged)
+ }
+
+ @Test
+ fun `WHEN dispatching MicrophoneChangedAction THEN locationChanged state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.microphoneChanged)
+
+ store.dispatch(MicrophoneChangedAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.microphoneChanged)
+ }
+
+ @Test
+ fun `WHEN dispatching PersistentStorageChangedAction THEN persistentStorageChanged state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.persistentStorageChanged)
+
+ store.dispatch(PersistentStorageChangedAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.persistentStorageChanged)
+ }
+
+ @Test
+ fun `WHEN dispatching MediaKeySystemAccesChangedAction THEN mediaKeySystemAccessChanged state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.mediaKeySystemAccessChanged)
+
+ store.dispatch(MediaKeySystemAccesChangedAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.mediaKeySystemAccessChanged)
+ }
+
+ @Test
+ fun `WHEN dispatching AutoPlayAudibleChangedAction THEN autoPlayAudibleChanged state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.autoPlayAudibleChanged)
+
+ store.dispatch(AutoPlayAudibleChangedAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.autoPlayAudibleChanged)
+ }
+
+ @Test
+ fun `WHEN dispatching AutoPlayInAudibleChangedAction THEN autoPlayAudibleChanged state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.autoPlayInaudibleChanged)
+
+ store.dispatch(AutoPlayInAudibleChangedAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.autoPlayInaudibleChanged)
+ }
+
+ @Test
+ fun `WHEN dispatching AutoPlayAudibleBlockingAction THEN autoPlayAudibleBlocking state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.autoPlayAudibleBlocking)
+
+ store.dispatch(AutoPlayAudibleBlockingAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.autoPlayAudibleBlocking)
+ }
+
+ @Test
+ fun `WHEN dispatching AutoPlayInAudibleBlockingAction THEN autoPlayInaudibleBlocking state will be updated`() {
+ assertFalse(tab.content.permissionHighlights.autoPlayInaudibleBlocking)
+
+ store.dispatch(AutoPlayInAudibleBlockingAction(tab.id, true)).joinBlocking()
+
+ assertTrue(tab.content.permissionHighlights.autoPlayInaudibleBlocking)
+ }
+
+ @Test
+ fun `WHEN dispatching Reset THEN permissionHighlights state will be update to its default value`() {
+ store.dispatch(AutoPlayInAudibleBlockingAction(tab.id, true)).joinBlocking()
+
+ assertEquals(
+ PermissionHighlightsState(autoPlayInaudibleBlocking = true),
+ tab.content.permissionHighlights,
+ )
+
+ with(store) { dispatch(Reset(tab.id)).joinBlocking() }
+
+ assertEquals(PermissionHighlightsState(), tab.content.permissionHighlights)
+ }
+
+ @Test
+ fun `UpdateAppIntentAction updates request`() {
+ assertTrue(tab.content.promptRequests.isEmpty())
+
+ val appIntent1: AppIntentState = mock()
+
+ store.dispatch(
+ ContentAction.UpdateAppIntentAction(tab.id, appIntent1),
+ ).joinBlocking()
+
+ assertEquals(appIntent1, tab.content.appIntent)
+
+ val appIntent2: AppIntentState = mock()
+
+ store.dispatch(
+ ContentAction.UpdateAppIntentAction(tab.id, appIntent2),
+ ).joinBlocking()
+
+ assertEquals(appIntent2, tab.content.appIntent)
+ }
+
+ @Test
+ fun `ConsumeAppIntentAction removes request`() {
+ val appIntent: AppIntentState = mock()
+
+ store.dispatch(
+ ContentAction.UpdateAppIntentAction(tab.id, appIntent),
+ ).joinBlocking()
+
+ assertEquals(appIntent, tab.content.appIntent)
+
+ store.dispatch(
+ ContentAction.ConsumeAppIntentAction(tab.id),
+ ).joinBlocking()
+
+ assertNull(tab.content.appIntent)
+ }
+
+ @Test
+ fun `CheckForFormDataAction updates hasFormData`() {
+ assertFalse(tab.content.hasFormData)
+
+ store.dispatch(
+ ContentAction.UpdateHasFormDataAction(tab.id, true),
+ ).joinBlocking()
+
+ assertTrue(tab.content.hasFormData)
+
+ store.dispatch(
+ ContentAction.UpdateHasFormDataAction(tab.id, false),
+ ).joinBlocking()
+
+ assertFalse(tab.content.hasFormData)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CookieBannerActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CookieBannerActionTest.kt
new file mode 100644
index 0000000000..b63c2f43c4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CookieBannerActionTest.kt
@@ -0,0 +1,49 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus.DETECTED
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus.HANDLED
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus.NO_DETECTED
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class CookieBannerActionTest {
+ private lateinit var tab: TabSessionState
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setUp() {
+ tab = createTab("https://www.mozilla.org")
+
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+ }
+
+ private fun tabState(): TabSessionState = store.state.findTab(tab.id)!!
+
+ @Test
+ fun `WHEN an UpdateStatusAction is dispatched THEN update cookieBanner on the given session`() {
+ assertEquals(NO_DETECTED, tabState().cookieBanner)
+
+ store.dispatch(CookieBannerAction.UpdateStatusAction(tabId = tab.id, status = HANDLED))
+ .joinBlocking()
+
+ assertEquals(HANDLED, tabState().cookieBanner)
+
+ store.dispatch(CookieBannerAction.UpdateStatusAction(tabId = tab.id, status = DETECTED))
+ .joinBlocking()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CustomTabListActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CustomTabListActionTest.kt
new file mode 100644
index 0000000000..22527e2d98
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CustomTabListActionTest.kt
@@ -0,0 +1,92 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.CustomTabConfig
+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.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class CustomTabListActionTest {
+
+ @Test
+ fun `AddCustomTabAction - Adds provided tab`() {
+ val store = BrowserStore()
+
+ assertEquals(0, store.state.tabs.size)
+ assertEquals(0, store.state.customTabs.size)
+
+ val config = CustomTabConfig()
+ val customTab = createCustomTab(
+ "https://www.mozilla.org",
+ config = config,
+ source = SessionState.Source.Internal.CustomTab,
+ )
+
+ store.dispatch(CustomTabListAction.AddCustomTabAction(customTab)).joinBlocking()
+
+ assertEquals(0, store.state.tabs.size)
+ assertEquals(1, store.state.customTabs.size)
+ assertEquals(SessionState.Source.Internal.CustomTab, store.state.customTabs[0].source)
+ assertEquals(customTab, store.state.customTabs[0])
+ assertSame(config, store.state.customTabs[0].config)
+ }
+
+ @Test
+ fun `RemoveCustomTabAction - Removes tab with given id`() {
+ val customTab1 = createCustomTab("https://www.mozilla.org")
+ val customTab2 = createCustomTab("https://www.firefox.com")
+
+ val state = BrowserState(customTabs = listOf(customTab1, customTab2))
+ val store = BrowserStore(state)
+
+ assertEquals(2, store.state.customTabs.size)
+
+ store.dispatch(CustomTabListAction.RemoveCustomTabAction(customTab2.id)).joinBlocking()
+
+ assertEquals(1, store.state.customTabs.size)
+ assertEquals(customTab1, store.state.customTabs[0])
+ }
+
+ @Test
+ fun `RemoveCustomTabAction - Noop for unknown id`() {
+ val customTab1 = createCustomTab("https://www.mozilla.org")
+ val customTab2 = createCustomTab("https://www.firefox.com")
+
+ val state = BrowserState(customTabs = listOf(customTab1, customTab2))
+ val store = BrowserStore(state)
+
+ assertEquals(2, store.state.customTabs.size)
+
+ store.dispatch(CustomTabListAction.RemoveCustomTabAction("unknown id")).joinBlocking()
+
+ assertEquals(2, store.state.customTabs.size)
+ assertEquals(customTab1, store.state.customTabs[0])
+ assertEquals(customTab2, store.state.customTabs[1])
+ }
+
+ @Test
+ fun `RemoveAllCustomTabsAction - Removes all custom tabs (but not regular tabs)`() {
+ val customTab1 = createCustomTab("https://www.mozilla.org")
+ val customTab2 = createCustomTab("https://www.firefox.com")
+ val regularTab = createTab(url = "https://www.mozilla.org")
+
+ val state = BrowserState(customTabs = listOf(customTab1, customTab2), tabs = listOf(regularTab))
+ val store = BrowserStore(state)
+
+ assertEquals(2, store.state.customTabs.size)
+ assertEquals(1, store.state.tabs.size)
+
+ store.dispatch(CustomTabListAction.RemoveAllCustomTabsAction).joinBlocking()
+ assertEquals(0, store.state.customTabs.size)
+ assertEquals(1, store.state.tabs.size)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DebugActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DebugActionTest.kt
new file mode 100644
index 0000000000..6f8eee6980
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DebugActionTest.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.browser.state.action
+
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.DelicateAction
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@DelicateAction
+class DebugActionTest {
+ @Test
+ fun `UpdateCreatedAtAction - updates createdAt when the tab was first created`() {
+ val existingTab = createTab("https://www.mozilla.org")
+
+ val state = BrowserState(
+ tabs = listOf(existingTab),
+ selectedTabId = existingTab.id,
+ )
+
+ val store = BrowserStore(state)
+ val timestamp = System.currentTimeMillis()
+
+ store.dispatch(DebugAction.UpdateCreatedAtAction(existingTab.id, timestamp)).joinBlocking()
+
+ assertEquals(timestamp, store.state.selectedTab?.createdAt)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DownloadActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DownloadActionTest.kt
new file mode 100644
index 0000000000..bfffd5041b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DownloadActionTest.kt
@@ -0,0 +1,132 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class DownloadActionTest {
+
+ @Test
+ fun `AddDownloadAction adds download`() {
+ val store = BrowserStore(BrowserState())
+
+ val download1 = DownloadState("https://mozilla.org/download1", destinationDirectory = "")
+ store.dispatch(DownloadAction.AddDownloadAction(download1)).joinBlocking()
+ assertEquals(download1, store.state.downloads[download1.id])
+ assertEquals(1, store.state.downloads.size)
+
+ val download2 = DownloadState("https://mozilla.org/download2", destinationDirectory = "")
+ store.dispatch(DownloadAction.AddDownloadAction(download2)).joinBlocking()
+ assertEquals(download2, store.state.downloads[download2.id])
+ assertEquals(2, store.state.downloads.size)
+ }
+
+ @Test
+ fun `WHEN DismissDownloadNotificationAction is dispatched THEN notificationId is set to null`() {
+ val store = BrowserStore(BrowserState())
+
+ val download = DownloadState("https://mozilla.org/download1", destinationDirectory = "", notificationId = 100)
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ assertNotNull(store.state.downloads[download.id]!!.notificationId)
+
+ store.dispatch(DownloadAction.DismissDownloadNotificationAction(download.id)).joinBlocking()
+ assertNull(store.state.downloads[download.id]!!.notificationId)
+ }
+
+ @Test
+ fun `WHEN DismissDownloadNotificationAction is dispatched with an invalid downloadId THEN the state must not change`() {
+ val store = BrowserStore(BrowserState())
+
+ val download = DownloadState("https://mozilla.org/download1", destinationDirectory = "", notificationId = 100)
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ assertNotNull(store.state.downloads[download.id]!!.notificationId)
+ assertEquals(1, store.state.downloads.size)
+
+ store.dispatch(DownloadAction.DismissDownloadNotificationAction("-1")).joinBlocking()
+ assertNotNull(store.state.downloads[download.id]!!.notificationId)
+ assertEquals(download, store.state.downloads[download.id])
+ }
+
+ @Test
+ fun `RestoreDownloadStateAction adds download`() {
+ val store = BrowserStore(BrowserState())
+
+ val download1 = DownloadState("https://mozilla.org/download1", destinationDirectory = "")
+ store.dispatch(DownloadAction.RestoreDownloadStateAction(download1)).joinBlocking()
+ assertEquals(download1, store.state.downloads[download1.id])
+ assertEquals(1, store.state.downloads.size)
+
+ val download2 = DownloadState("https://mozilla.org/download2", destinationDirectory = "")
+ store.dispatch(DownloadAction.RestoreDownloadStateAction(download2)).joinBlocking()
+ assertEquals(download2, store.state.downloads[download2.id])
+ assertEquals(2, store.state.downloads.size)
+ }
+
+ @Test
+ fun `RestoreDownloadsStateAction does nothing`() {
+ val store = BrowserStore(BrowserState())
+
+ val state = store.state
+ store.dispatch(DownloadAction.RestoreDownloadsStateAction).joinBlocking()
+ assertSame(store.state, state)
+ }
+
+ @Test
+ fun `RemoveDownloadAction removes download`() {
+ val store = BrowserStore(BrowserState())
+
+ val download = DownloadState("https://mozilla.org/download1", destinationDirectory = "")
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ assertEquals(download, store.state.downloads[download.id])
+ assertFalse(store.state.downloads.isEmpty())
+
+ store.dispatch(DownloadAction.RemoveDownloadAction(download.id)).joinBlocking()
+ assertTrue(store.state.downloads.isEmpty())
+ }
+
+ @Test
+ fun `RemoveAllDownloadsAction removes all downloads`() {
+ val store = BrowserStore(BrowserState())
+
+ val download = DownloadState("https://mozilla.org/download1", destinationDirectory = "")
+ val download2 = DownloadState("https://mozilla.org/download2", destinationDirectory = "")
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ store.dispatch(DownloadAction.AddDownloadAction(download2)).joinBlocking()
+
+ assertFalse(store.state.downloads.isEmpty())
+ assertEquals(2, store.state.downloads.size)
+
+ store.dispatch(DownloadAction.RemoveAllDownloadsAction).joinBlocking()
+ assertTrue(store.state.downloads.isEmpty())
+ }
+
+ @Test
+ fun `UpdateDownloadAction updates the provided download`() {
+ val store = BrowserStore(BrowserState())
+ val download = DownloadState("https://mozilla.org/download1", destinationDirectory = "")
+ val download2 = DownloadState("https://mozilla.org/download2", destinationDirectory = "")
+
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ store.dispatch(DownloadAction.AddDownloadAction(download2)).joinBlocking()
+
+ val updatedDownload = download.copy(fileName = "filename.txt")
+
+ store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload)).joinBlocking()
+
+ assertFalse(store.state.downloads.isEmpty())
+ assertEquals(2, store.state.downloads.size)
+ assertEquals(updatedDownload, store.state.downloads[updatedDownload.id])
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt
new file mode 100644
index 0000000000..268d56a935
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt
@@ -0,0 +1,140 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.TabSessionState
+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.concept.engine.EngineSessionState
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+class EngineActionTest {
+ private lateinit var tab: TabSessionState
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setUp() {
+ tab = createTab("https://www.mozilla.org")
+
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+ }
+
+ private fun engineState() = store.state.findTab(tab.id)!!.engineState
+
+ @Test
+ fun `LinkEngineSessionAction - Attaches engine session`() {
+ assertNull(engineState().engineSession)
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession, timestamp = 1234)).joinBlocking()
+
+ assertNotNull(engineState().engineSession)
+ assertEquals(engineSession, engineState().engineSession)
+ assertEquals(1234L, engineState().timestamp)
+ }
+
+ @Test
+ fun `UnlinkEngineSessionAction - Detaches engine session`() {
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, mock())).joinBlocking()
+ store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, mock())).joinBlocking()
+ store.dispatch(EngineAction.UpdateEngineSessionObserverAction(tab.id, mock())).joinBlocking()
+ assertNotNull(engineState().engineSession)
+ assertNotNull(engineState().engineSessionState)
+ assertNotNull(engineState().engineObserver)
+
+ store.dispatch(EngineAction.UnlinkEngineSessionAction(tab.id)).joinBlocking()
+ assertNull(engineState().engineSession)
+ assertNotNull(engineState().engineSessionState)
+ assertNull(engineState().engineObserver)
+ }
+
+ @Test
+ fun `UpdateEngineSessionStateAction - Updates engine session state`() {
+ assertNull(engineState().engineSessionState)
+
+ val engineSessionState: EngineSessionState = mock()
+ store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, engineSessionState)).joinBlocking()
+ assertNotNull(engineState().engineSessionState)
+ assertEquals(engineSessionState, engineState().engineSessionState)
+ }
+
+ @Test
+ fun `UpdateEngineSessionObserverAction - Updates engine session observer`() {
+ assertNull(engineState().engineObserver)
+
+ val engineObserver: EngineSession.Observer = mock()
+ store.dispatch(EngineAction.UpdateEngineSessionObserverAction(tab.id, engineObserver)).joinBlocking()
+ assertNotNull(engineState().engineObserver)
+ assertEquals(engineObserver, engineState().engineObserver)
+ }
+
+ @Test
+ fun `PurgeHistoryAction - Removes state from sessions without history`() {
+ val tab1 = createTab("https://www.mozilla.org").copy(
+ engineState = EngineState(engineSession = null, engineSessionState = mock()),
+ )
+
+ val tab2 = createTab("https://www.firefox.com").copy(
+ engineState = EngineState(engineSession = mock(), engineSessionState = mock()),
+ )
+
+ val customTab1 = createCustomTab("http://www.theverge.com").copy(
+ engineState = EngineState(engineSession = null, engineSessionState = mock()),
+ )
+
+ val customTab2 = createCustomTab("https://www.google.com").copy(
+ engineState = EngineState(engineSession = mock(), engineSessionState = mock()),
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2),
+ customTabs = listOf(customTab1, customTab2),
+ ),
+ )
+
+ store.dispatch(EngineAction.PurgeHistoryAction).joinBlocking()
+
+ assertNull(store.state.findTab(tab1.id)!!.engineState.engineSessionState)
+ assertNotNull(store.state.findTab(tab2.id)!!.engineState.engineSessionState)
+
+ assertNull(store.state.findCustomTab(customTab1.id)!!.engineState.engineSessionState)
+ assertNotNull(store.state.findCustomTab(customTab2.id)!!.engineState.engineSessionState)
+ }
+
+ @Test
+ fun `UpdateEngineSessionInitializingAction - Updates initializing flag`() {
+ assertFalse(engineState().initializing)
+
+ store.dispatch(EngineAction.UpdateEngineSessionInitializingAction(tab.id, true)).joinBlocking()
+ assertTrue(engineState().initializing)
+ }
+
+ @Test
+ fun `OptimizedLoadUrlTriggeredAction - State is not changed`() {
+ val state = store.state
+ store.dispatch(EngineAction.OptimizedLoadUrlTriggeredAction(tab.id, "https://mozilla.org")).joinBlocking()
+ assertSame(store.state, state)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/HistoryMetadataActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/HistoryMetadataActionTest.kt
new file mode 100644
index 0000000000..a68d7c3129
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/HistoryMetadataActionTest.kt
@@ -0,0 +1,67 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class HistoryMetadataActionTest {
+
+ @Test
+ fun `SetHistoryMetadataKeyAction - Associates tab with history metadata`() {
+ val tab = createTab("https://www.mozilla.org")
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+ assertNull(store.state.findTab(tab.id)?.historyMetadata)
+
+ val historyMetadata = HistoryMetadataKey(
+ url = tab.content.url,
+ referrerUrl = "https://firefox.com",
+ )
+
+ store.dispatch(HistoryMetadataAction.SetHistoryMetadataKeyAction(tab.id, historyMetadata)).joinBlocking()
+ assertEquals(historyMetadata, store.state.findTab(tab.id)?.historyMetadata)
+ }
+
+ @Test
+ fun `DisbandSearchGroupAction - clears specific search terms from any existing tab history metadata`() {
+ val tab1 = createTab("https://www.mozilla.org")
+ val tab2 = createTab("https://www.mozilla.org/downloads")
+ val store = BrowserStore(BrowserState(tabs = listOf(tab1, tab2)))
+ val historyMetadata1 = HistoryMetadataKey(
+ url = tab1.content.url,
+ referrerUrl = "https://firefox.com",
+ )
+ val historyMetadata2 = HistoryMetadataKey(
+ url = tab2.content.url,
+ searchTerm = "download Firefox",
+ referrerUrl = "https://google.com/?q=download+firefox",
+ )
+
+ // Okay to do this without any metadata associated with tabs.
+ store.dispatch(HistoryMetadataAction.DisbandSearchGroupAction("Download firefox")).joinBlocking()
+
+ // Okay to do this with an empty search term string.
+ store.dispatch(HistoryMetadataAction.DisbandSearchGroupAction("")).joinBlocking()
+
+ store.dispatch(HistoryMetadataAction.SetHistoryMetadataKeyAction(tab1.id, historyMetadata1)).joinBlocking()
+ store.dispatch(HistoryMetadataAction.SetHistoryMetadataKeyAction(tab2.id, historyMetadata2)).joinBlocking()
+
+ // Search term matching is case-insensitive.
+ store.dispatch(HistoryMetadataAction.DisbandSearchGroupAction("Download firefox")).joinBlocking()
+
+ // tab1 is unchanged.
+ assertEquals(historyMetadata1, store.state.findTab(tab1.id)?.historyMetadata)
+
+ // tab2 has its search term and referrer cleared.
+ assertEquals(HistoryMetadataKey(url = tab2.content.url), store.state.findTab(tab2.id)?.historyMetadata)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LastAccessActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LastAccessActionTest.kt
new file mode 100644
index 0000000000..03c2f94cc6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LastAccessActionTest.kt
@@ -0,0 +1,32 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class LastAccessActionTest {
+ @Test
+ fun `UpdateLastAccessAction - updates the timestamp when the tab was last accessed`() {
+ val existingTab = createTab("https://www.mozilla.org")
+
+ val state = BrowserState(
+ tabs = listOf(existingTab),
+ selectedTabId = existingTab.id,
+ )
+
+ val store = BrowserStore(state)
+ val timestamp = System.currentTimeMillis()
+
+ store.dispatch(LastAccessAction.UpdateLastAccessAction(existingTab.id, timestamp)).joinBlocking()
+
+ assertEquals(timestamp, store.state.selectedTab?.lastAccess)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LocaleActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LocaleActionTest.kt
new file mode 100644
index 0000000000..ac9b4f8c0a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LocaleActionTest.kt
@@ -0,0 +1,32 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Test
+import java.util.Locale
+
+class LocaleActionTest {
+ @Test
+ fun `WHEN a new locale is selected THEN it is updated in the store`() {
+ val store = BrowserStore(BrowserState())
+ val locale1 = Locale("es")
+ store.dispatch(LocaleAction.UpdateLocaleAction(locale1)).joinBlocking()
+ assertEquals(locale1, store.state.locale)
+ }
+
+ @Test
+ fun `WHEN the state is restored from disk THEN the store receives the state`() {
+ val store = BrowserStore(BrowserState())
+
+ val state = store.state
+ store.dispatch(LocaleAction.RestoreLocaleStateAction).joinBlocking()
+ assertSame(state, store.state)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/MediaSessionActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/MediaSessionActionTest.kt
new file mode 100644
index 0000000000..059f792c3a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/MediaSessionActionTest.kt
@@ -0,0 +1,304 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class MediaSessionActionTest {
+ @Test
+ fun `ActivatedMediaSessionAction updates media session state`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val mediaSessionController: MediaSession.Controller = mock()
+
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ "test-tab",
+ mediaSessionController,
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNotNull(mediaSessionState)
+ assertEquals(mediaSessionController, mediaSessionState?.controller)
+ }
+
+ @Test
+ fun `DeactivatedMediaSessionAction updates media session state`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val mediaSessionController: MediaSession.Controller = mock()
+
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ "test-tab",
+ mediaSessionController,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ MediaSessionAction.DeactivatedMediaSessionAction(
+ "test-tab",
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNull(mediaSessionState)
+ }
+
+ @Test
+ fun `UpdateMediaMetadataAction updates media session state`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val mediaSessionController: MediaSession.Controller = mock()
+ val metadata: MediaSession.Metadata = mock()
+
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ "test-tab",
+ mediaSessionController,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaMetadataAction(
+ "test-tab",
+ metadata,
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNotNull(mediaSessionState)
+ assertEquals(mediaSessionController, mediaSessionState?.controller)
+ assertEquals(metadata, mediaSessionState?.metadata)
+ }
+
+ @Test
+ fun `UpdateMediaPlaybackStateAction updates media session state`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val mediaSessionController: MediaSession.Controller = mock()
+ val playbackState: MediaSession.PlaybackState = mock()
+
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ "test-tab",
+ mediaSessionController,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaPlaybackStateAction(
+ "test-tab",
+ playbackState,
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNotNull(mediaSessionState)
+ assertEquals(mediaSessionController, mediaSessionState?.controller)
+ assertEquals(playbackState, mediaSessionState?.playbackState)
+ }
+
+ @Test
+ fun `UpdateMediaFeatureAction updates media session state`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val mediaSessionController: MediaSession.Controller = mock()
+ val features: MediaSession.Feature = mock()
+
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ "test-tab",
+ mediaSessionController,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaFeatureAction(
+ "test-tab",
+ features,
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNotNull(mediaSessionState)
+ assertEquals(mediaSessionController, mediaSessionState?.controller)
+ assertEquals(features, mediaSessionState?.features)
+ }
+
+ @Test
+ fun `UpdateMediaPositionStateAction updates media session state`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val mediaSessionController: MediaSession.Controller = mock()
+ val positionState: MediaSession.PositionState = mock()
+
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ "test-tab",
+ mediaSessionController,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaPositionStateAction(
+ "test-tab",
+ positionState,
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNotNull(mediaSessionState)
+ assertEquals(mediaSessionController, mediaSessionState?.controller)
+ assertEquals(positionState, mediaSessionState?.positionState)
+ }
+
+ @Test
+ fun `UpdateMediaMutedAction updates media session state`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val mediaSessionController: MediaSession.Controller = mock()
+
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ "test-tab",
+ mediaSessionController,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaMutedAction(
+ "test-tab",
+ true,
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNotNull(mediaSessionState)
+ assertEquals(mediaSessionController, mediaSessionState?.controller)
+ assertEquals(true, mediaSessionState?.muted)
+ }
+
+ @Test
+ fun `UpdateMediaFullscreenAction updates media session state`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val mediaSessionController: MediaSession.Controller = mock()
+ val elementMetadata: MediaSession.ElementMetadata = mock()
+
+ store.dispatch(
+ MediaSessionAction.ActivatedMediaSessionAction(
+ "test-tab",
+ mediaSessionController,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaFullscreenAction(
+ "test-tab",
+ true,
+ elementMetadata,
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNotNull(mediaSessionState)
+ assertEquals(mediaSessionController, mediaSessionState?.controller)
+ assertEquals(true, mediaSessionState?.fullscreen)
+ assertEquals(elementMetadata, mediaSessionState?.elementMetadata)
+ }
+
+ @Test
+ fun `updates are ignore if media session is not activated`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ val elementMetadata: MediaSession.ElementMetadata = mock()
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaFullscreenAction(
+ "test-tab",
+ true,
+ elementMetadata,
+ ),
+ ).joinBlocking()
+
+ val mediaSessionState: MediaSessionState? = store.state.findTab("test-tab")?.mediaSessionState
+ assertNull(mediaSessionState)
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaMutedAction(
+ "test-tab",
+ true,
+ ),
+ ).joinBlocking()
+ assertNull(mediaSessionState)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ReaderActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ReaderActionTest.kt
new file mode 100644
index 0000000000..9462340bc7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ReaderActionTest.kt
@@ -0,0 +1,154 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+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
+
+class ReaderActionTest {
+ private lateinit var tab: TabSessionState
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setUp() {
+ tab = createTab("https://www.mozilla.org")
+
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+ }
+
+ private fun tabState(): TabSessionState = store.state.findTab(tab.id)!!
+ private fun readerState() = tabState().readerState
+
+ @Test
+ fun `UpdateReaderableAction - Updates readerable flag of ReaderState`() {
+ assertFalse(readerState().readerable)
+
+ store.dispatch(ReaderAction.UpdateReaderableAction(tabId = tab.id, readerable = true))
+ .joinBlocking()
+
+ assertTrue(readerState().readerable)
+
+ store.dispatch(ReaderAction.UpdateReaderableAction(tabId = tab.id, readerable = false))
+ .joinBlocking()
+
+ assertFalse(readerState().readerable)
+ }
+
+ @Test
+ fun `UpdateReaderActiveAction - Updates active flag of ReaderState`() {
+ assertFalse(readerState().active)
+
+ store.dispatch(ReaderAction.UpdateReaderActiveAction(tabId = tab.id, active = true))
+ .joinBlocking()
+
+ assertTrue(readerState().active)
+
+ store.dispatch(ReaderAction.UpdateReaderActiveAction(tabId = tab.id, active = false))
+ .joinBlocking()
+
+ assertFalse(readerState().active)
+ }
+
+ @Test
+ fun `UpdateReaderableCheckRequiredAction - Updates check required flag of ReaderState`() {
+ assertFalse(readerState().active)
+
+ store.dispatch(ReaderAction.UpdateReaderableCheckRequiredAction(tabId = tab.id, checkRequired = true))
+ .joinBlocking()
+
+ assertTrue(readerState().checkRequired)
+
+ store.dispatch(ReaderAction.UpdateReaderableCheckRequiredAction(tabId = tab.id, checkRequired = false))
+ .joinBlocking()
+
+ assertFalse(readerState().checkRequired)
+ }
+
+ @Test
+ fun `UpdateReaderConnectRequiredAction - Updates connect required flag of ReaderState`() {
+ assertFalse(readerState().active)
+
+ store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(tabId = tab.id, connectRequired = true))
+ .joinBlocking()
+
+ assertTrue(readerState().connectRequired)
+
+ store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(tabId = tab.id, connectRequired = false))
+ .joinBlocking()
+
+ assertFalse(readerState().connectRequired)
+ }
+
+ @Test
+ fun `UpdateReaderBaseUrlAction - Updates base url of ReaderState`() {
+ assertNull(readerState().baseUrl)
+
+ store.dispatch(ReaderAction.UpdateReaderBaseUrlAction(tabId = tab.id, baseUrl = "moz-extension://test"))
+ .joinBlocking()
+
+ assertEquals("moz-extension://test", readerState().baseUrl)
+ }
+
+ @Test
+ fun `UpdateReaderActiveUrlAction - Updates active url of ReaderState`() {
+ assertNull(readerState().activeUrl)
+
+ store.dispatch(ReaderAction.UpdateReaderActiveUrlAction(tabId = tab.id, activeUrl = "https://mozilla.org"))
+ .joinBlocking()
+
+ assertEquals("https://mozilla.org", readerState().activeUrl)
+ }
+
+ @Test
+ fun `UpdateReaderScrollYAction - Updates scrollY of ReaderState when active`() {
+ assertFalse(readerState().active)
+
+ store.dispatch(ReaderAction.UpdateReaderActiveAction(tabId = tab.id, active = true))
+ .joinBlocking()
+
+ assertTrue(readerState().active)
+
+ store.dispatch(ReaderAction.UpdateReaderScrollYAction(tabId = tab.id, scrollY = 1234))
+ .joinBlocking()
+
+ assertEquals(1234, readerState().scrollY)
+ }
+
+ @Test
+ fun `UpdateReaderScrollYAction - Does not update scrollY of ReaderState when not active`() {
+ assertFalse(readerState().active)
+
+ store.dispatch(ReaderAction.UpdateReaderScrollYAction(tabId = tab.id, scrollY = 1234))
+ .joinBlocking()
+
+ assertNull(readerState().scrollY)
+ }
+
+ @Test
+ fun `ClearReaderActiveUrlAction - Clears active url of ReaderState`() {
+ assertNull(readerState().activeUrl)
+
+ store.dispatch(ReaderAction.UpdateReaderActiveUrlAction(tabId = tab.id, activeUrl = "https://mozilla.org"))
+ .joinBlocking()
+ assertEquals("https://mozilla.org", readerState().activeUrl)
+
+ store.dispatch(ReaderAction.ClearReaderActiveUrlAction(tabId = tab.id)).joinBlocking()
+ assertNull(readerState().activeUrl)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/SearchActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/SearchActionTest.kt
new file mode 100644
index 0000000000..e66a5eae61
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/SearchActionTest.kt
@@ -0,0 +1,409 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+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.Test
+
+class SearchActionTest {
+ @Test
+ fun `SetSearchEnginesAction - Set sets region search engines in state`() {
+ val engine1 = SearchEngine(
+ id = "id1",
+ name = "search1",
+ icon = mock(),
+ type = SearchEngine.Type.BUNDLED,
+ )
+ val engine2 = SearchEngine(
+ id = "id2",
+ name = "search2",
+ icon = mock(),
+ type = SearchEngine.Type.BUNDLED,
+ )
+
+ val store = BrowserStore(BrowserState())
+ val searchEngineList = listOf(engine1, engine2)
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetSearchEnginesAction(
+ regionSearchEngines = searchEngineList,
+ regionDefaultSearchEngineId = "id2",
+ customSearchEngines = emptyList(),
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ hiddenSearchEngines = emptyList(),
+ disabledSearchEngineIds = emptyList(),
+ additionalSearchEngines = emptyList(),
+ additionalAvailableSearchEngines = emptyList(),
+ regionSearchEnginesOrder = listOf("id1", "id2"),
+ ),
+ ).joinBlocking()
+
+ val searchEngines = store.state.search.regionSearchEngines
+ assertFalse(searchEngines.isEmpty())
+ assertEquals(2, searchEngines.size)
+ assertEquals(engine1, searchEngines[0])
+ assertEquals(engine2, searchEngines[1])
+ }
+
+ @Test
+ fun `SetSearchEnginesAction - sets custom search engines in state`() {
+ val engine1 = SearchEngine(
+ id = "id1",
+ name = "search1",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ )
+ val engine2 = SearchEngine(
+ id = "id2",
+ name = "search2",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ )
+
+ val store = BrowserStore(BrowserState())
+ val searchEngineList = listOf(engine1, engine2)
+ assertTrue(store.state.search.customSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetSearchEnginesAction(
+ customSearchEngines = searchEngineList,
+ regionSearchEngines = emptyList(),
+ regionDefaultSearchEngineId = "default",
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ hiddenSearchEngines = emptyList(),
+ disabledSearchEngineIds = emptyList(),
+ additionalSearchEngines = emptyList(),
+ additionalAvailableSearchEngines = emptyList(),
+ regionSearchEnginesOrder = emptyList(),
+ ),
+ ).joinBlocking()
+
+ val searchEngines = store.state.search.customSearchEngines
+ assertFalse(searchEngines.isEmpty())
+ assertEquals(2, searchEngines.size)
+ assertEquals(engine1, searchEngines[0])
+ assertEquals(engine2, searchEngines[1])
+ }
+
+ @Test
+ fun `UpdateCustomSearchEngineAction sets a new custom search engine`() {
+ val store = BrowserStore(BrowserState())
+
+ assertTrue(store.state.search.customSearchEngines.isEmpty())
+
+ val customSearchEngine = SearchEngine(
+ id = "customId1",
+ name = "custom_search",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ )
+
+ // Add a custom search engine
+ store.dispatch(SearchAction.UpdateCustomSearchEngineAction(customSearchEngine)).joinBlocking()
+
+ store.state.search.customSearchEngines.let { searchEngines ->
+ assertTrue(searchEngines.isNotEmpty())
+ assertEquals(1, searchEngines.size)
+ assertEquals(customSearchEngine, searchEngines[0])
+ }
+
+ val customSearchEngine2 = SearchEngine(
+ id = "customId2",
+ name = "custom_search_second",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ )
+
+ // Add another search engine
+ store.dispatch(SearchAction.UpdateCustomSearchEngineAction(customSearchEngine2)).joinBlocking()
+
+ store.state.search.customSearchEngines.let { searchEngines ->
+ assertTrue(searchEngines.isNotEmpty())
+ assertEquals(2, searchEngines.size)
+ assertEquals(customSearchEngine, searchEngines[0])
+ assertEquals(customSearchEngine2, searchEngines[1])
+ }
+
+ // Update first search engine
+ val updated = customSearchEngine.copy(
+ name = "My awesome search engine",
+ )
+ store.dispatch(SearchAction.UpdateCustomSearchEngineAction(updated)).joinBlocking()
+
+ store.state.search.customSearchEngines.let { searchEngines ->
+ assertTrue(searchEngines.isNotEmpty())
+ assertEquals(2, searchEngines.size)
+ assertEquals(updated, searchEngines[0])
+ assertEquals(customSearchEngine2, searchEngines[1])
+ }
+ }
+
+ @Test
+ fun `RemoveCustomSearchEngineAction removes a new custom search engine`() {
+ val customSearchEngine = SearchEngine(
+ id = "customId1",
+ name = "custom_search",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ customSearchEngines = listOf(customSearchEngine),
+ ),
+ ),
+ )
+
+ assertEquals(1, store.state.search.customSearchEngines.size)
+
+ store.dispatch(SearchAction.RemoveCustomSearchEngineAction("unrecognized_id")).joinBlocking()
+ assertEquals(1, store.state.search.customSearchEngines.size)
+
+ store.dispatch(SearchAction.RemoveCustomSearchEngineAction(customSearchEngine.id)).joinBlocking()
+ assertTrue(store.state.search.customSearchEngines.isEmpty())
+ }
+
+ @Test
+ fun `SelectSearchEngineAction sets a default search engine id`() {
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "search1",
+ icon = mock(),
+ type = SearchEngine.Type.BUNDLED,
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(searchEngine),
+ ),
+ ),
+ )
+
+ assertNull(store.state.search.userSelectedSearchEngineId)
+
+ store.dispatch(SearchAction.SelectSearchEngineAction(searchEngine.id, null)).joinBlocking()
+ assertEquals(searchEngine.id, store.state.search.userSelectedSearchEngineId)
+
+ assertEquals(searchEngine.id, store.state.search.userSelectedSearchEngineId)
+
+ store.dispatch(SearchAction.SelectSearchEngineAction("unrecognized_id", null)).joinBlocking()
+ // We allow setting an ID of a search engine that is not in the state since loading happens
+ // asynchronously and the search engine may not be loaded yet.
+ assertEquals("unrecognized_id", store.state.search.userSelectedSearchEngineId)
+ }
+
+ @Test
+ fun `Setting region of user`() {
+ val store = BrowserStore()
+ assertNull(store.state.search.region)
+
+ store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking()
+
+ assertNotNull(store.state.search.region)
+ assertEquals("DE", store.state.search.region!!.home)
+ assertEquals("FR", store.state.search.region!!.current)
+ }
+
+ @Test
+ fun `WHEN restore hidden search engines action GIVEN there are hidden engines THEN hidden engines are added back to the bundled engine list`() {
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(
+ SearchEngine(id = "google", name = "Google", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine(id = "bing", name = "Bing", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine(id = "duckduckgo", name = "DuckDuckGo", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.search.regionSearchEngines.size)
+ assertEquals(1, store.state.search.hiddenSearchEngines.size)
+
+ assertEquals("google", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bing", store.state.search.regionSearchEngines[1].id)
+ assertEquals("duckduckgo", store.state.search.hiddenSearchEngines[0].id)
+
+ store.dispatch(
+ SearchAction.RestoreHiddenSearchEnginesAction,
+ ).joinBlocking()
+
+ assertEquals(3, store.state.search.regionSearchEngines.size)
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+
+ assertEquals("google", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bing", store.state.search.regionSearchEngines[1].id)
+ assertEquals("duckduckgo", store.state.search.regionSearchEngines[2].id)
+ }
+
+ @Test
+ fun `WHEN restore hidden search engines action GIVEN there are no hidden engines THEN there are no changes`() {
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(
+ SearchEngine(id = "google", name = "Google", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine(id = "bing", name = "Bing", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine(id = "duckduckgo", name = "DuckDuckGo", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ hiddenSearchEngines = listOf(),
+ ),
+ ),
+ )
+
+ assertEquals(3, store.state.search.regionSearchEngines.size)
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+
+ assertEquals("google", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bing", store.state.search.regionSearchEngines[1].id)
+ assertEquals("duckduckgo", store.state.search.regionSearchEngines[2].id)
+
+ store.dispatch(
+ SearchAction.RestoreHiddenSearchEnginesAction,
+ ).joinBlocking()
+
+ assertEquals(3, store.state.search.regionSearchEngines.size)
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+
+ assertEquals("google", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bing", store.state.search.regionSearchEngines[1].id)
+ assertEquals("duckduckgo", store.state.search.regionSearchEngines[2].id)
+ }
+
+ @Test
+ fun `ShowSearchEngineAction - Adds hidden search engines back to region search engines`() {
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(
+ SearchEngine(id = "google", name = "Google", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine(id = "bing", name = "Bing", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine(id = "duckduckgo", name = "DuckDuckGo", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ SearchAction.ShowSearchEngineAction("duckduckgo"),
+ ).joinBlocking()
+
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+ assertEquals(3, store.state.search.regionSearchEngines.size)
+
+ assertEquals("google", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bing", store.state.search.regionSearchEngines[1].id)
+ assertEquals("duckduckgo", store.state.search.regionSearchEngines[2].id)
+ }
+
+ @Test
+ fun `HideSearchEngineAction - Adds region search engine to hidden search engines`() {
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(
+ SearchEngine(id = "google", name = "Google", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine(id = "bing", name = "Bing", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine(id = "duckduckgo", name = "DuckDuckGo", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ SearchAction.HideSearchEngineAction("google"),
+ ).joinBlocking()
+
+ assertEquals(2, store.state.search.hiddenSearchEngines.size)
+ assertEquals(1, store.state.search.regionSearchEngines.size)
+
+ assertEquals("bing", store.state.search.regionSearchEngines[0].id)
+
+ assertEquals("duckduckgo", store.state.search.hiddenSearchEngines[0].id)
+ assertEquals("google", store.state.search.hiddenSearchEngines[1].id)
+ }
+
+ @Test
+ fun `ShowSearchEngineAction, HideSearchEngineAction - Does nothing for unknown or custom search engines`() {
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(
+ SearchEngine(id = "google", name = "Google", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine(id = "bing", name = "Bing", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine(id = "duckduckgo", name = "DuckDuckGo", icon = mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ customSearchEngines = listOf(
+ SearchEngine(id = "banana", name = "Banana Search", icon = mock(), type = SearchEngine.Type.CUSTOM),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ SearchAction.ShowSearchEngineAction("banana"),
+ ).joinBlocking()
+
+ store.dispatch(
+ SearchAction.HideSearchEngineAction("banana"),
+ ).joinBlocking()
+
+ store.dispatch(
+ SearchAction.HideSearchEngineAction("unknown-search"),
+ ).joinBlocking()
+
+ store.dispatch(
+ SearchAction.ShowSearchEngineAction("also-unknown-search"),
+ ).joinBlocking()
+
+ assertEquals(2, store.state.search.regionSearchEngines.size)
+ assertEquals(1, store.state.search.hiddenSearchEngines.size)
+ assertEquals(1, store.state.search.customSearchEngines.size)
+
+ assertEquals("google", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bing", store.state.search.regionSearchEngines[1].id)
+
+ assertEquals("duckduckgo", store.state.search.hiddenSearchEngines[0].id)
+
+ assertEquals("banana", store.state.search.customSearchEngines[0].id)
+ }
+
+ @Test
+ fun `GIVEN the search state of the browser WHEN refreshing the list of search engines THEN do not modify the state`() {
+ val state = BrowserState(
+ search = mock(),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(SearchAction.RefreshSearchEnginesAction).joinBlocking()
+
+ assertEquals(state.search, store.state.search)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt
new file mode 100644
index 0000000000..46e131eb8b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt
@@ -0,0 +1,319 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabGroup
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.getGroupById
+import mozilla.components.browser.state.state.getGroupByName
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class TabGroupActionTest {
+
+ @Test
+ fun `AddTabGroupAction - Adds provided group and creates partition if needed`() {
+ val store = BrowserStore()
+
+ val partition = "testFeaturePartition"
+ val testGroup = TabGroup("test", "testGroup")
+ store.dispatch(TabGroupAction.AddTabGroupAction(partition = partition, group = testGroup)).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[partition]
+ assertNotNull(expectedPartition)
+ assertSame(testGroup, expectedPartition?.getGroupById(testGroup.id))
+ assertSame(testGroup, expectedPartition?.getGroupByName(testGroup.name))
+ }
+
+ @Test
+ fun `AddTabGroupAction - Adds provided group with tabs`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "tab1", url = "https://firefox.com"),
+ createTab(id = "tab2", url = "https://mozilla.org"),
+ ),
+ ),
+ )
+
+ val partition = "testFeaturePartition"
+ val testGroup = TabGroup("test", tabIds = listOf("tab1", "tab2"))
+ store.dispatch(TabGroupAction.AddTabGroupAction(partition = partition, group = testGroup)).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[partition]
+ assertNotNull(expectedPartition)
+ assertSame(testGroup, expectedPartition?.getGroupById(testGroup.id))
+ assertEquals(listOf("tab1", "tab2"), expectedPartition?.getGroupById(testGroup.id)?.tabIds)
+ }
+
+ @Test
+ fun `RemoveTabGroupAction - Removes provided group`() {
+ val tabGroup1 = TabGroup("test1", tabIds = listOf("tab1", "tab2"))
+ val tabGroup2 = TabGroup("test2", tabIds = listOf("tab1", "tab2"))
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup1, tabGroup2))
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "tab1", url = "https://firefox.com"),
+ createTab(id = "tab2", url = "https://mozilla.org"),
+ ),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ assertNotNull(store.state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup1.id))
+ assertNotNull(store.state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup2.id))
+ store.dispatch(TabGroupAction.RemoveTabGroupAction(tabPartition.id, tabGroup1.id)).joinBlocking()
+ assertNull(store.state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup1.id))
+ assertNotNull(store.state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup2.id))
+ }
+
+ @Test
+ fun `RemoveTabGroupAction - Empty partitions are removed`() {
+ val tabGroup = TabGroup("test1", tabIds = listOf("tab1", "tab2"))
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "tab1", url = "https://firefox.com"),
+ createTab(id = "tab2", url = "https://mozilla.org"),
+ ),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ assertNotNull(store.state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup.id))
+ store.dispatch(TabGroupAction.RemoveTabGroupAction(tabPartition.id, tabGroup.id)).joinBlocking()
+ assertNull(store.state.tabPartitions[tabPartition.id])
+ }
+
+ @Test
+ fun `AddTabAction - Adds provided tab to groups`() {
+ val tabGroup = TabGroup("test1", tabIds = emptyList())
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+ val tab = createTab(id = "tab1", url = "https://firefox.com")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ store.dispatch(TabGroupAction.AddTabAction(tabPartition.id, tabGroup.id, tab.id)).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertTrue(expectedGroup!!.tabIds.contains(tab.id))
+ }
+
+ @Test
+ fun `AddTabAction - Creates partition if needed`() {
+ val tabGroup = TabGroup("test1", tabIds = emptyList())
+ val tabPartition = TabPartition("testFeaturePartition")
+ val tab = createTab(id = "tab1", url = "https://firefox.com")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(TabGroupAction.AddTabAction(tabPartition.id, tabGroup.id, tab.id)).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertTrue(expectedGroup!!.tabIds.contains(tab.id))
+ }
+
+ @Test
+ fun `AddTabAction - Doesn't add tab if already in group`() {
+ val tabGroup = TabGroup("test1", tabIds = listOf("tab1"))
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+ val tab = createTab(id = "tab1", url = "https://firefox.com")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ store.dispatch(TabGroupAction.AddTabAction(tabPartition.id, tabGroup.id, tab.id)).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertTrue(expectedGroup!!.tabIds.contains(tab.id))
+ assertEquals(1, expectedGroup.tabIds.size)
+ }
+
+ @Test
+ fun `AddTabsAction - Adds provided tab to groups`() {
+ val tabGroup = TabGroup("test1", tabIds = emptyList())
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+ val tab1 = createTab(id = "tab1", url = "https://firefox.com")
+ val tab2 = createTab(id = "tab2", url = "https://mozilla.org")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ store.dispatch(
+ TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, listOf(tab1.id, tab2.id)),
+ ).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertTrue(expectedGroup!!.tabIds.contains(tab1.id))
+ assertTrue(expectedGroup.tabIds.contains(tab2.id))
+ }
+
+ @Test
+ fun `AddTabsAction - Creates partition if needed`() {
+ val tabGroup = TabGroup("test1", tabIds = emptyList())
+ val tabPartition = TabPartition("testFeaturePartition")
+ val tab1 = createTab(id = "tab1", url = "https://firefox.com")
+ val tab2 = createTab(id = "tab2", url = "https://mozilla.org")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2),
+ ),
+ )
+
+ store.dispatch(
+ TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, listOf(tab1.id, tab2.id)),
+ ).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertTrue(expectedGroup!!.tabIds.contains(tab1.id))
+ assertTrue(expectedGroup.tabIds.contains(tab2.id))
+ }
+
+ @Test
+ fun `AddTabsAction - Doesn't add tabs if already in group`() {
+ val tabGroup = TabGroup("test1", tabIds = listOf("tab1"))
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+ val tab1 = createTab(id = "tab1", url = "https://firefox.com")
+ val tab2 = createTab(id = "tab2", url = "https://mozilla.org")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ store.dispatch(
+ TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, listOf(tab1.id, tab2.id)),
+ ).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertTrue(expectedGroup!!.tabIds.contains(tab1.id))
+ assertTrue(expectedGroup.tabIds.contains(tab2.id))
+ assertEquals(2, expectedGroup.tabIds.size)
+ }
+
+ @Test
+ fun `AddTabsAction - Creates partition if needed but only adds distinct tabs`() {
+ val tabGroup = TabGroup("test1", tabIds = emptyList())
+ val tabPartition = TabPartition("testFeaturePartition")
+ val tab1 = createTab(id = "tab1", url = "https://firefox.com")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1),
+ ),
+ )
+
+ store.dispatch(
+ TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, listOf(tab1.id, tab1.id)),
+ ).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertTrue(expectedGroup!!.tabIds.contains(tab1.id))
+ assertEquals(1, expectedGroup.tabIds.size)
+ }
+
+ @Test
+ fun `RemoveTabAction - Removes tab from group`() {
+ val tab1 = createTab(id = "tab1", url = "https://firefox.com")
+ val tab2 = createTab(id = "tab2", url = "https://mozilla.org")
+ val tabGroup = TabGroup("test1", tabIds = listOf(tab1.id, tab2.id))
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ store.dispatch(TabGroupAction.RemoveTabAction(tabPartition.id, tabGroup.id, tab1.id)).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertFalse(expectedGroup!!.tabIds.contains(tab1.id))
+ assertTrue(expectedGroup.tabIds.contains(tab2.id))
+ }
+
+ @Test
+ fun `RemoveTabsAction - Removes tabs from group`() {
+ val tab1 = createTab(id = "tab1", url = "https://firefox.com")
+ val tab2 = createTab(id = "tab2", url = "https://mozilla.org")
+ val tabGroup = TabGroup("test1", tabIds = listOf(tab1.id, tab2.id))
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ store.dispatch(
+ TabGroupAction.RemoveTabsAction(tabPartition.id, tabGroup.id, listOf(tab1.id, tab2.id)),
+ ).joinBlocking()
+
+ val expectedPartition = store.state.tabPartitions[tabPartition.id]
+ assertNotNull(expectedPartition)
+ val expectedGroup = expectedPartition!!.getGroupById(tabGroup.id)
+ assertNotNull(expectedGroup)
+ assertTrue(expectedGroup!!.tabIds.isEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt
new file mode 100644
index 0000000000..18e644513b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt
@@ -0,0 +1,1477 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.normalTabs
+import mozilla.components.browser.state.selector.privateTabs
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabGroup
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.getGroupById
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class TabListActionTest {
+
+ @Test
+ fun `AddTabAction - Adds provided SessionState`() {
+ val store = BrowserStore()
+
+ assertEquals(0, store.state.tabs.size)
+ assertNull(store.state.selectedTabId)
+
+ val tab = createTab(url = "https://www.mozilla.org")
+
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(tab.id, store.state.selectedTabId)
+ }
+
+ @Test
+ fun `AddTabAction - Add tab and update selection`() {
+ val existingTab = createTab("https://www.mozilla.org")
+
+ val state = BrowserState(
+ tabs = listOf(existingTab),
+ selectedTabId = existingTab.id,
+ )
+
+ val store = BrowserStore(state)
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(existingTab.id, store.state.selectedTabId)
+
+ val newTab = createTab("https://firefox.com")
+
+ store.dispatch(TabListAction.AddTabAction(newTab, select = true)).joinBlocking()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(newTab.id, store.state.selectedTabId)
+ }
+
+ @Test
+ fun `AddTabAction - Select first tab automatically`() {
+ val existingTab = createTab("https://www.mozilla.org")
+
+ val store = BrowserStore()
+
+ assertEquals(0, store.state.tabs.size)
+ assertNull(existingTab.id, store.state.selectedTabId)
+
+ val newTab = createTab("https://firefox.com")
+ store.dispatch(TabListAction.AddTabAction(newTab, select = false)).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(newTab.id, store.state.selectedTabId)
+ }
+
+ @Test
+ fun `AddTabAction - Specify parent tab`() {
+ val store = BrowserStore()
+
+ val tab1 = createTab("https://www.mozilla.org")
+ val tab2 = createTab("https://www.firefox.com")
+ val tab3 = createTab("https://wiki.mozilla.org", parent = tab1)
+ val tab4 = createTab("https://github.com/mozilla-mobile/android-components", parent = tab2)
+
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab3)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab4)).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertNull(store.state.tabs[0].parentId)
+ assertNull(store.state.tabs[2].parentId)
+ assertEquals(tab1.id, store.state.tabs[1].parentId)
+ assertEquals(tab2.id, store.state.tabs[3].parentId)
+ }
+
+ @Test
+ fun `AddTabAction - Specify source`() {
+ val store = BrowserStore()
+
+ val tab1 = createTab("https://www.mozilla.org")
+ val tab2 = createTab("https://www.firefox.com", source = SessionState.Source.Internal.Menu)
+
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(SessionState.Source.Internal.None, store.state.tabs[0].source)
+ assertEquals(SessionState.Source.Internal.Menu, store.state.tabs[1].source)
+ }
+
+ @Test
+ fun `AddTabAction - Tabs with parent are added after (next to) parent`() {
+ val store = BrowserStore()
+
+ val parent01 = createTab("https://www.mozilla.org")
+ val parent02 = createTab("https://getpocket.com")
+ val tab1 = createTab("https://www.firefox.com")
+ val tab2 = createTab("https://developer.mozilla.org/en-US/")
+ val child001 = createTab("https://www.mozilla.org/en-US/internet-health/", parent = parent01)
+ val child002 = createTab("https://www.mozilla.org/en-US/technology/", parent = parent01)
+ val child003 = createTab("https://getpocket.com/add/", parent = parent02)
+
+ store.dispatch(TabListAction.AddTabAction(parent01)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(child001)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(parent02)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(child002)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(child003)).joinBlocking()
+
+ assertEquals(parent01.id, store.state.tabs[0].id) // ├── parent 1
+ assertEquals(child002.id, store.state.tabs[1].id) // │ ├── child 2
+ assertEquals(child001.id, store.state.tabs[2].id) // │ └── child 1
+ assertEquals(tab1.id, store.state.tabs[3].id) // ├──tab 1
+ assertEquals(tab2.id, store.state.tabs[4].id) // ├──tab 2
+ assertEquals(parent02.id, store.state.tabs[5].id) // └── parent 2
+ assertEquals(child003.id, store.state.tabs[6].id) // └── child 3
+ }
+
+ @Test
+ fun `SelectTabAction - Selects SessionState by id`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ )
+ val store = BrowserStore(state)
+
+ assertNull(store.state.selectedTabId)
+
+ store.dispatch(TabListAction.SelectTabAction("a"))
+ .joinBlocking()
+
+ assertEquals("a", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveTabAction - Removes SessionState`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(TabListAction.RemoveTabAction("a"))
+ .joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.firefox.com", store.state.tabs[0].content.url)
+ }
+
+ @Test
+ fun `RemoveTabAction - Removes tab from partition`() {
+ val tabGroup = TabGroup("test1", tabIds = listOf("a", "b"))
+ val tabPartition = TabPartition("testPartition", tabGroups = listOf(tabGroup))
+
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ tabPartitions = mapOf(tabPartition.id to tabPartition),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.firefox.com", store.state.tabs[0].content.url)
+ assertEquals(listOf("b"), store.state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup.id)?.tabIds)
+ }
+
+ @Test
+ fun `RemoveTabsAction - Removes SessionState`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ createTab(id = "c", url = "https://www.getpocket.com"),
+ ),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf("a", "b")))
+ .joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.getpocket.com", store.state.tabs[0].content.url)
+ }
+
+ @Test
+ fun `RemoveTabsAction - Removes tabs from partition`() {
+ val tabGroup = TabGroup("test1", tabIds = listOf("a", "b"))
+ val tabPartition = TabPartition("testPartition", tabGroups = listOf(tabGroup))
+
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ tabPartitions = mapOf(tabPartition.id to tabPartition),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf("a", "b"))).joinBlocking()
+ assertEquals(0, store.state.tabs.size)
+ assertEquals(0, store.state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup.id)?.tabIds?.size)
+ }
+
+ @Test
+ fun `RemoveTabAction - Noop for unknown id`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(TabListAction.RemoveTabAction("c"))
+ .joinBlocking()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertEquals("https://www.firefox.com", store.state.tabs[1].content.url)
+ }
+
+ @Test
+ fun `RemoveTabAction - Selected tab id is set to null if selected and last tab is removed`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ ),
+ selectedTabId = "a",
+ )
+
+ val store = BrowserStore(state)
+
+ assertEquals("a", store.state.selectedTabId)
+
+ store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
+
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveTabAction - Does not select custom tab`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "b", url = "https://www.firefox.com"),
+ createCustomTab(id = "c", url = "https://www.firefox.com/hello", source = SessionState.Source.External.CustomTab(mock())),
+ ),
+ selectedTabId = "a",
+ )
+
+ val store = BrowserStore(state)
+
+ assertEquals("a", store.state.selectedTabId)
+
+ store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
+
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveTabAction - Will select next nearby tab after removing selected tab`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ createTab(id = "c", url = "https://www.example.org"),
+ createTab(id = "d", url = "https://getpocket.com"),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "a1", url = "https://www.firefox.com"),
+ ),
+ selectedTabId = "c",
+ )
+
+ val store = BrowserStore(state)
+
+ assertEquals("c", store.state.selectedTabId)
+
+ store.dispatch(TabListAction.RemoveTabAction("c")).joinBlocking()
+ assertEquals("d", store.state.selectedTabId)
+
+ store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
+ assertEquals("d", store.state.selectedTabId)
+
+ store.dispatch(TabListAction.RemoveTabAction("d")).joinBlocking()
+ assertEquals("b", store.state.selectedTabId)
+
+ store.dispatch(TabListAction.RemoveTabAction("b")).joinBlocking()
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveTabAction - Selects private tab after private tab was removed`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = true),
+ createTab(id = "b", url = "https://www.firefox.com", private = false),
+ createTab(id = "c", url = "https://www.example.org", private = false),
+ createTab(id = "d", url = "https://getpocket.com", private = true),
+ createTab(id = "e", url = "https://developer.mozilla.org/", private = true),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "a1", url = "https://www.firefox.com"),
+ createCustomTab(id = "b1", url = "https://hubs.mozilla.com", source = SessionState.Source.External.CustomTab(mock())),
+ ),
+ selectedTabId = "d",
+ )
+
+ val store = BrowserStore(state)
+
+ // [a*, b, c, (d*), e*] -> [a*, b, c, (e*)]
+ store.dispatch(TabListAction.RemoveTabAction("d")).joinBlocking()
+ assertEquals("e", store.state.selectedTabId)
+
+ // [a*, b, c, (e*)] -> [(a*), b, c]
+ store.dispatch(TabListAction.RemoveTabAction("e")).joinBlocking()
+ assertEquals("a", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveTabAction - Selects normal tab after normal tab was removed`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ createTab(id = "c", url = "https://www.example.org", private = true),
+ createTab(id = "d", url = "https://getpocket.com", private = false),
+ createTab(id = "e", url = "https://developer.mozilla.org/", private = false),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "a1", url = "https://www.firefox.com"),
+ createCustomTab(id = "b1", url = "https://hubs.mozilla.com", source = SessionState.Source.External.CustomTab(mock())),
+ ),
+ selectedTabId = "d",
+ )
+
+ val store = BrowserStore(state)
+
+ // [a, b*, c*, (d), e] -> [a, b*, c* (e)]
+ store.dispatch(TabListAction.RemoveTabAction("d")).joinBlocking()
+ assertEquals("e", store.state.selectedTabId)
+
+ // [a, b*, c*, (e)] -> [(a), b*, c*]
+ store.dispatch(TabListAction.RemoveTabAction("e")).joinBlocking()
+ assertEquals("a", store.state.selectedTabId)
+
+ // After removing the last normal tab NO private tab should get selected
+ // [(a), b*, c*] -> [b*, c*]
+ store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test
+ fun `GIVEN last normal tab WHEN removed THEN no new tab is selected`() {
+ val normalTab = createTab("normal", private = false)
+ val privateTab = createTab("private", private = true)
+ val initialState = BrowserState(tabs = listOf(normalTab, privateTab), selectedTabId = normalTab.id)
+ val store = BrowserStore(initialState)
+
+ store.dispatch(TabListAction.RemoveTabAction(normalTab.id)).joinBlocking()
+
+ assertNull(store.state.selectedTabId)
+ assertEquals(1, store.state.tabs.size)
+ }
+
+ @Test
+ fun `GIVEN last private tab WHEN removed THEN no new tab is selected`() {
+ val normalTab = createTab("normal", private = false)
+ val privateTab = createTab("private", private = true)
+ val initialState = BrowserState(tabs = listOf(normalTab, privateTab), selectedTabId = privateTab.id)
+ val store = BrowserStore(initialState)
+
+ store.dispatch(TabListAction.RemoveTabAction(privateTab.id)).joinBlocking()
+
+ assertNull(store.state.selectedTabId)
+ assertEquals(1, store.state.tabs.size)
+ }
+
+ @Test
+ fun `GIVEN normal tabs and one private tab WHEN all normal tabs are removed THEN no new tab is selected`() {
+ val tabs = List(5) { createTab("$it", private = false) } + createTab("private", private = true)
+ val initialState = BrowserState(tabs = tabs, selectedTabId = tabs.first().id)
+ val store = BrowserStore(initialState)
+
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+
+ assertNull(store.state.selectedTabId)
+ assertEquals(1, store.state.tabs.size)
+ }
+
+ @Test
+ fun `GIVEN one normal tab and private tabs WHEN all private tabs are removed THEN no new tab is selected`() {
+ val tabs = List(5) { createTab("$it", private = true) } + createTab("normal", private = false)
+ val initialState = BrowserState(tabs = tabs, selectedTabId = tabs.first().id)
+ val store = BrowserStore(initialState)
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ assertNull(store.state.selectedTabId)
+ assertEquals(1, store.state.tabs.size)
+ }
+
+ @Test
+ fun `RemoveTabAction - Parent will be selected if child is removed and flag is set to true (default)`() {
+ val store = BrowserStore()
+
+ val parent = createTab("https://www.mozilla.org")
+ val tab1 = createTab("https://www.firefox.com")
+ val tab2 = createTab("https://getpocket.com")
+ val child = createTab("https://www.mozilla.org/en-US/internet-health/", parent = parent)
+
+ store.dispatch(TabListAction.AddTabAction(parent)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(child)).joinBlocking()
+
+ store.dispatch(TabListAction.SelectTabAction(child.id)).joinBlocking()
+ store.dispatch(TabListAction.RemoveTabAction(child.id, selectParentIfExists = true)).joinBlocking()
+
+ assertEquals(parent.id, store.state.selectedTabId)
+ assertEquals("https://www.mozilla.org", store.state.selectedTab?.content?.url)
+ }
+
+ @Test
+ fun `RemoveTabAction - Parent will not be selected if child is removed and flag is set to false`() {
+ val store = BrowserStore()
+
+ val parent = createTab("https://www.mozilla.org")
+
+ val tab1 = createTab("https://www.firefox.com")
+ val tab2 = createTab("https://getpocket.com")
+ val child1 = createTab("https://www.mozilla.org/en-US/internet-health/", parent = parent)
+ val child2 = createTab("https://www.mozilla.org/en-US/technology/", parent = parent)
+
+ store.dispatch(TabListAction.AddTabAction(parent)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(child1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(child2)).joinBlocking()
+
+ store.dispatch(TabListAction.SelectTabAction(child1.id)).joinBlocking()
+ store.dispatch(TabListAction.RemoveTabAction(child1.id, selectParentIfExists = false)).joinBlocking()
+
+ assertEquals(tab1.id, store.state.selectedTabId)
+ assertEquals("https://www.firefox.com", store.state.selectedTab?.content?.url)
+ }
+
+ @Test
+ fun `RemoveTabAction - Providing selectParentIfExists when removing tab without parent has no effect`() {
+ val store = BrowserStore()
+
+ val tab1 = createTab("https://www.firefox.com")
+ val tab2 = createTab("https://getpocket.com")
+ val tab3 = createTab("https://www.mozilla.org/en-US/internet-health/")
+
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab3)).joinBlocking()
+
+ store.dispatch(TabListAction.SelectTabAction(tab3.id)).joinBlocking()
+ store.dispatch(TabListAction.RemoveTabAction(tab3.id, selectParentIfExists = true)).joinBlocking()
+
+ assertEquals(tab2.id, store.state.selectedTabId)
+ assertEquals("https://getpocket.com", store.state.selectedTab?.content?.url)
+ }
+
+ @Test
+ fun `RemoveTabAction - Children are updated when parent is removed`() {
+ val store = BrowserStore()
+
+ val tab0 = createTab("https://www.firefox.com")
+ val tab1 = createTab("https://developer.mozilla.org/en-US/", parent = tab0)
+ val tab2 = createTab("https://www.mozilla.org/en-US/internet-health/", parent = tab1)
+ val tab3 = createTab("https://www.mozilla.org/en-US/technology/", parent = tab2)
+
+ store.dispatch(TabListAction.AddTabAction(tab0)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab3)).joinBlocking()
+
+ // tab0 <- tab1 <- tab2 <- tab3
+ assertEquals(tab0.id, store.state.tabs[0].id)
+ assertEquals(tab1.id, store.state.tabs[1].id)
+ assertEquals(tab2.id, store.state.tabs[2].id)
+ assertEquals(tab3.id, store.state.tabs[3].id)
+
+ assertNull(store.state.tabs[0].parentId)
+ assertEquals(tab0.id, store.state.tabs[1].parentId)
+ assertEquals(tab1.id, store.state.tabs[2].parentId)
+ assertEquals(tab2.id, store.state.tabs[3].parentId)
+
+ store.dispatch(TabListAction.RemoveTabAction(tab2.id)).joinBlocking()
+
+ // tab0 <- tab1 <- tab3
+ assertEquals(tab0.id, store.state.tabs[0].id)
+ assertEquals(tab1.id, store.state.tabs[1].id)
+ assertEquals(tab3.id, store.state.tabs[2].id)
+
+ assertNull(store.state.tabs[0].parentId)
+ assertEquals(tab0.id, store.state.tabs[1].parentId)
+ assertEquals(tab1.id, store.state.tabs[2].parentId)
+
+ store.dispatch(TabListAction.RemoveTabAction(tab0.id)).joinBlocking()
+
+ // tab1 <- tab3
+ assertEquals(tab1.id, store.state.tabs[0].id)
+ assertEquals(tab3.id, store.state.tabs[1].id)
+
+ assertNull(store.state.tabs[0].parentId)
+ assertEquals(tab1.id, store.state.tabs[1].parentId)
+ }
+
+ @Test
+ fun `RestoreAction - Adds restored tabs and updates selected tab`() {
+ val store = BrowserStore()
+
+ assertEquals(0, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "a", url = "https://www.mozilla.org", private = false),
+ ),
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "c", url = "https://www.example.org", private = true),
+ ),
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "d", url = "https://getpocket.com", private = false),
+ ),
+ ),
+ selectedTabId = "d",
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals("b", store.state.tabs[1].id)
+ assertEquals("c", store.state.tabs[2].id)
+ assertEquals("d", store.state.tabs[3].id)
+ assertEquals("d", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RestoreAction - Adds restored tabs to the beginning of existing tabs without updating selection`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "c", url = "https://www.example.org", private = true),
+ ),
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "d", url = "https://getpocket.com", private = false),
+ ),
+ ),
+ selectedTabId = "d",
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("c", store.state.tabs[0].id)
+ assertEquals("d", store.state.tabs[1].id)
+ assertEquals("a", store.state.tabs[2].id)
+ assertEquals("b", store.state.tabs[3].id)
+ assertEquals("a", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RestoreAction - Adds restored tabs to the end of existing tabs without updating selection`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "c", url = "https://www.example.org", private = true),
+ ),
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "d", url = "https://getpocket.com", private = false),
+ ),
+ ),
+ selectedTabId = "d",
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.END,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals("b", store.state.tabs[1].id)
+ assertEquals("c", store.state.tabs[2].id)
+ assertEquals("d", store.state.tabs[3].id)
+ assertEquals("a", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RestoreAction - Adds restored tabs to beginning of existing tabs with updating selection`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "c", url = "https://www.example.org", private = true),
+ ),
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "d", url = "https://getpocket.com", private = false),
+ ),
+ ),
+ selectedTabId = "d",
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("c", store.state.tabs[0].id)
+ assertEquals("d", store.state.tabs[1].id)
+ assertEquals("a", store.state.tabs[2].id)
+ assertEquals("b", store.state.tabs[3].id)
+ assertEquals("d", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RestoreAction - Adds restored tabs to end of existing tabs with updating selection`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "c", url = "https://www.example.org", private = true),
+ ),
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(id = "d", url = "https://getpocket.com", private = false),
+ ),
+ ),
+ selectedTabId = "d",
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.END,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals("b", store.state.tabs[1].id)
+ assertEquals("c", store.state.tabs[2].id)
+ assertEquals("d", store.state.tabs[3].id)
+ assertEquals("d", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RestoreAction - Does not update selection if none was provided`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(engineSessionState = null, state = TabState(id = "c", url = "https://www.example.org", private = true)),
+ RecoverableTab(engineSessionState = null, state = TabState(id = "d", url = "https://getpocket.com", private = false)),
+ ),
+ selectedTabId = null,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("c", store.state.tabs[0].id)
+ assertEquals("d", store.state.tabs[1].id)
+ assertEquals("a", store.state.tabs[2].id)
+ assertEquals("b", store.state.tabs[3].id)
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RestoreAction - Add tab back to correct location (beginning)`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(engineSessionState = null, state = TabState(id = "c", url = "https://www.example.org", index = 0)),
+ ),
+ selectedTabId = null,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX,
+ ),
+ ).joinBlocking()
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("c", store.state.tabs[0].id)
+ assertEquals("a", store.state.tabs[1].id)
+ assertEquals("b", store.state.tabs[2].id)
+ }
+
+ @Test
+ fun `RestoreAction - Add tab back to correct location (middle)`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(engineSessionState = null, state = TabState(id = "c", url = "https://www.example.org", index = 1)),
+ ),
+ selectedTabId = null,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX,
+ ),
+ ).joinBlocking()
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals("c", store.state.tabs[1].id)
+ assertEquals("b", store.state.tabs[2].id)
+ }
+
+ @Test
+ fun `RestoreAction - Add tab back to correct location (end)`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(engineSessionState = null, state = TabState(id = "c", url = "https://www.example.org", index = 2)),
+ ),
+ selectedTabId = null,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX,
+ ),
+ ).joinBlocking()
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals("b", store.state.tabs[1].id)
+ assertEquals("c", store.state.tabs[2].id)
+ }
+
+ @Test
+ fun `RestoreAction - Add tab back to correct location with index beyond size of total tabs`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(engineSessionState = null, state = TabState(id = "c", url = "https://www.example.org", index = 4)),
+ ),
+ selectedTabId = null,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX,
+ ),
+ ).joinBlocking()
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals("b", store.state.tabs[1].id)
+ assertEquals("c", store.state.tabs[2].id)
+ }
+
+ @Test
+ fun `RestoreAction - Add tabs back to correct locations`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(engineSessionState = null, state = TabState(id = "c", url = "https://www.example.org", index = 3)),
+ RecoverableTab(engineSessionState = null, state = TabState(id = "d", url = "https://www.example.org", index = 0)),
+ ),
+ selectedTabId = null,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("d", store.state.tabs[0].id)
+ assertEquals("a", store.state.tabs[1].id)
+ assertEquals("b", store.state.tabs[2].id)
+ assertEquals("c", store.state.tabs[3].id)
+ }
+
+ @Test
+ fun `RestoreAction - Add tabs with matching indices back to correct locations`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(engineSessionState = null, state = TabState(id = "c", url = "https://www.example.org", index = 0)),
+ RecoverableTab(engineSessionState = null, state = TabState(id = "d", url = "https://www.example.org", index = 0)),
+ ),
+ selectedTabId = null,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("d", store.state.tabs[0].id)
+ assertEquals("c", store.state.tabs[1].id)
+ assertEquals("a", store.state.tabs[2].id)
+ assertEquals("b", store.state.tabs[3].id)
+ }
+
+ @Test
+ fun `RestoreAction - Add tabs with a -1 removal index`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ ),
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs = listOf(
+ RecoverableTab(engineSessionState = null, state = TabState(id = "c", url = "https://www.example.org", index = -1)),
+ RecoverableTab(engineSessionState = null, state = TabState(id = "d", url = "https://www.example.org")),
+ ),
+ selectedTabId = null,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX,
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals("b", store.state.tabs[1].id)
+ assertEquals("c", store.state.tabs[2].id)
+ assertEquals("d", store.state.tabs[3].id)
+ }
+
+ @Test
+ fun `RemoveAllTabsAction - Removes both private and non-private tabs (but not custom tabs)`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "a1", url = "https://www.firefox.com"),
+ createCustomTab(id = "a2", url = "https://www.firefox.com/hello", source = SessionState.Source.External.CustomTab(mock())),
+ ),
+ selectedTabId = "a",
+ )
+
+ val store = BrowserStore(state)
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+
+ assertTrue(store.state.tabs.isEmpty())
+ assertNull(store.state.selectedTabId)
+ assertEquals(2, store.state.customTabs.size)
+ assertEquals("a2", store.state.customTabs.last().id)
+ }
+
+ @Test
+ fun `RemoveAllTabsAction - Removes tabs from partition`() {
+ val tabGroup = TabGroup("test1", tabIds = listOf("a", "b"))
+ val tabPartition = TabPartition("testPartition", tabGroups = listOf(tabGroup))
+
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ tabPartitions = mapOf(tabPartition.id to tabPartition),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+ assertEquals(0, store.state.tabs.size)
+ assertEquals(0, store.state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup.id)?.tabIds?.size)
+ }
+
+ @Test
+ fun `RemoveAllPrivateTabsAction - Removes only private tabs`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "a1", url = "https://www.firefox.com"),
+ ),
+ selectedTabId = "a",
+ )
+
+ val store = BrowserStore(state)
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals("a", store.state.selectedTabId)
+
+ assertEquals(1, store.state.customTabs.size)
+ assertEquals("a1", store.state.customTabs.last().id)
+ }
+
+ @Test
+ fun `RemoveAllPrivateTabsAction - Updates selection if affected`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "a1", url = "https://www.firefox.com"),
+ ),
+ selectedTabId = "b",
+ )
+
+ val store = BrowserStore(state)
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("a", store.state.tabs[0].id)
+ assertEquals(null, store.state.selectedTabId)
+
+ assertEquals(1, store.state.customTabs.size)
+ assertEquals("a1", store.state.customTabs.last().id)
+ }
+
+ @Test
+ fun `RemoveAllPrivateTabsAction - Removes tabs from partition`() {
+ val normalTabGroup = TabGroup("test1", tabIds = listOf("a"))
+ val privateTabGroup = TabGroup("test2", tabIds = listOf("b"))
+ val tabPartition = TabPartition("testPartition", tabGroups = listOf(normalTabGroup, privateTabGroup))
+
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ tabPartitions = mapOf(tabPartition.id to tabPartition),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(1, store.state.tabPartitions[tabPartition.id]?.getGroupById(normalTabGroup.id)?.tabIds?.size)
+ assertEquals(0, store.state.tabPartitions[tabPartition.id]?.getGroupById(privateTabGroup.id)?.tabIds?.size)
+ }
+
+ @Test
+ fun `RemoveAllNormalTabsAction - Removes only normal (non-private) tabs`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "a1", url = "https://www.firefox.com"),
+ ),
+ selectedTabId = "b",
+ )
+
+ val store = BrowserStore(state)
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("b", store.state.tabs[0].id)
+ assertEquals("b", store.state.selectedTabId)
+
+ assertEquals(1, store.state.customTabs.size)
+ assertEquals("a1", store.state.customTabs.last().id)
+ }
+
+ @Test
+ fun `RemoveAllNormalTabsAction - Updates selection if affected`() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ customTabs = listOf(
+ createCustomTab(id = "a1", url = "https://www.firefox.com"),
+ ),
+ selectedTabId = "a",
+ )
+
+ val store = BrowserStore(state)
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("b", store.state.tabs[0].id)
+ // After removing the last normal tab NO private tab should get selected
+ assertNull(store.state.selectedTabId)
+
+ assertEquals(1, store.state.customTabs.size)
+ assertEquals("a1", store.state.customTabs.last().id)
+ }
+
+ @Test
+ fun `RemoveAllNormalTabsAction - Removes tabs from partition`() {
+ val normalTabGroup = TabGroup("test1", tabIds = listOf("a"))
+ val privateTabGroup = TabGroup("test2", tabIds = listOf("b"))
+ val tabPartition = TabPartition("testPartition", tabGroups = listOf(normalTabGroup, privateTabGroup))
+
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ tabPartitions = mapOf(tabPartition.id to tabPartition),
+ )
+ val store = BrowserStore(state)
+
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(0, store.state.tabPartitions[tabPartition.id]?.getGroupById(normalTabGroup.id)?.tabIds?.size)
+ assertEquals(1, store.state.tabPartitions[tabPartition.id]?.getGroupById(privateTabGroup.id)?.tabIds?.size)
+ }
+
+ @Test
+ fun `AddMultipleTabsAction - Adds multiple tabs and updates selection`() {
+ val store = BrowserStore()
+
+ assertEquals(0, store.state.tabs.size)
+ assertNull(store.state.selectedTabId)
+
+ store.dispatch(
+ TabListAction.AddMultipleTabsAction(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ ),
+ ).joinBlocking()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertEquals("https://www.firefox.com", store.state.tabs[1].content.url)
+ assertNotNull(store.state.selectedTabId)
+ assertEquals("a", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `AddMultipleTabsAction - Adds multiple tabs and does not update selection if one exists already`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab(id = "z", url = "https://getpocket.com")),
+ selectedTabId = "z",
+ ),
+ )
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("z", store.state.selectedTabId)
+
+ store.dispatch(
+ TabListAction.AddMultipleTabsAction(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = false),
+ createTab(id = "b", url = "https://www.firefox.com", private = true),
+ ),
+ ),
+ ).joinBlocking()
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.tabs[0].content.url)
+ assertEquals("https://www.mozilla.org", store.state.tabs[1].content.url)
+ assertEquals("https://www.firefox.com", store.state.tabs[2].content.url)
+ assertEquals("z", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `AddMultipleTabsAction - Non private tab will be selected`() {
+ val store = BrowserStore()
+
+ assertEquals(0, store.state.tabs.size)
+ assertNull(store.state.selectedTabId)
+
+ store.dispatch(
+ TabListAction.AddMultipleTabsAction(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = true),
+ createTab(id = "b", url = "https://www.example.org", private = true),
+ createTab(id = "c", url = "https://www.firefox.com", private = false),
+ createTab(id = "d", url = "https://getpocket.com", private = true),
+ ),
+ ),
+ ).joinBlocking()
+
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertEquals("https://www.example.org", store.state.tabs[1].content.url)
+ assertEquals("https://www.firefox.com", store.state.tabs[2].content.url)
+ assertEquals("https://getpocket.com", store.state.tabs[3].content.url)
+ assertNotNull(store.state.selectedTabId)
+ assertEquals("c", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `AddMultipleTabsAction - No tab will be selected if only private tabs are added`() {
+ val store = BrowserStore()
+
+ assertEquals(0, store.state.tabs.size)
+ assertNull(store.state.selectedTabId)
+
+ store.dispatch(
+ TabListAction.AddMultipleTabsAction(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = true),
+ createTab(id = "b", url = "https://www.example.org", private = true),
+ createTab(id = "c", url = "https://getpocket.com", private = true),
+ ),
+ ),
+ ).joinBlocking()
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertEquals("https://www.example.org", store.state.tabs[1].content.url)
+ assertEquals("https://getpocket.com", store.state.tabs[2].content.url)
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveAllNormalTabsAction with private tab selected`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = true),
+ createTab(id = "b", url = "https://www.example.org", private = false),
+ createTab(id = "c", url = "https://www.firefox.com", private = false),
+ createTab(id = "d", url = "https://getpocket.com", private = true),
+ ),
+ selectedTabId = "d",
+ ),
+ )
+
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+
+ assertEquals(0, store.state.normalTabs.size)
+ assertEquals(2, store.state.privateTabs.size)
+ assertEquals("d", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveAllNormalTabsAction with normal tab selected`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = true),
+ createTab(id = "b", url = "https://www.example.org", private = false),
+ createTab(id = "c", url = "https://www.firefox.com", private = false),
+ createTab(id = "d", url = "https://getpocket.com", private = true),
+ ),
+ selectedTabId = "b",
+ ),
+ )
+
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+
+ assertEquals(0, store.state.normalTabs.size)
+ assertEquals(2, store.state.privateTabs.size)
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveAllPrivateTabsAction with private tab selected`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = true),
+ createTab(id = "b", url = "https://www.example.org", private = false),
+ createTab(id = "c", url = "https://www.firefox.com", private = false),
+ createTab(id = "d", url = "https://getpocket.com", private = true),
+ ),
+ selectedTabId = "d",
+ ),
+ )
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ assertEquals(2, store.state.normalTabs.size)
+ assertEquals(0, store.state.privateTabs.size)
+ assertEquals(null, store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveAllPrivateTabsAction with private tab selected and no normal tabs`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = true),
+ createTab(id = "b", url = "https://getpocket.com", private = true),
+ ),
+ selectedTabId = "b",
+ ),
+ )
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ assertEquals(0, store.state.normalTabs.size)
+ assertEquals(0, store.state.privateTabs.size)
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveAllPrivateTabsAction with normal tab selected`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org", private = true),
+ createTab(id = "b", url = "https://www.example.org", private = false),
+ createTab(id = "c", url = "https://www.firefox.com", private = false),
+ createTab(id = "d", url = "https://getpocket.com", private = true),
+ ),
+ selectedTabId = "b",
+ ),
+ )
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ assertEquals(2, store.state.normalTabs.size)
+ assertEquals(0, store.state.privateTabs.size)
+ assertEquals("b", store.state.selectedTabId)
+ }
+
+ private fun assertSameTabs(a: BrowserStore, b: List<TabSessionState>, str: String? = null) {
+ val aMap = a.state.tabs.map { "<" + it.id + "," + it.content.url + ">\n" }
+ val bMap = b.map { "<" + it.id + "," + it.content.url + ">\n" }
+ assertEquals(str, aMap.toString(), bMap.toString())
+ }
+ private fun dispatchJoinMoveAction(store: BrowserStore, tabIds: List<String>, targetTabId: String, placeAfter: Boolean) {
+ store.dispatch(
+ TabListAction.MoveTabsAction(
+ tabIds,
+ targetTabId,
+ placeAfter,
+ ),
+ ).joinBlocking()
+ }
+
+ @Test
+ fun `MoveTabsAction - Tabs move as expected`() {
+ val tabList = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ createTab(id = "c", url = "https://getpocket.com"),
+ createTab(id = "d", url = "https://www.example.org"),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ tabs = tabList,
+ selectedTabId = "a",
+ ),
+ )
+
+ dispatchJoinMoveAction(store, listOf("a"), "a", false)
+ assertSameTabs(store, tabList, "a to a-")
+ dispatchJoinMoveAction(store, listOf("a"), "a", true)
+ assertSameTabs(store, tabList, "a to a+")
+ dispatchJoinMoveAction(store, listOf("a"), "b", false)
+ assertSameTabs(store, tabList, "a to b-")
+
+ dispatchJoinMoveAction(store, listOf("a", "b"), "a", false)
+ assertSameTabs(store, tabList, "a,b to a-")
+ dispatchJoinMoveAction(store, listOf("a", "b"), "a", true)
+ assertSameTabs(store, tabList, "a,b to a+")
+ dispatchJoinMoveAction(store, listOf("a", "b"), "b", false)
+ assertSameTabs(store, tabList, "a,b to b-")
+ dispatchJoinMoveAction(store, listOf("a", "b"), "b", true)
+ assertSameTabs(store, tabList, "a,b to b+")
+ dispatchJoinMoveAction(store, listOf("a", "b"), "c", false)
+ assertSameTabs(store, tabList, "a,b to c-")
+
+ dispatchJoinMoveAction(store, listOf("c", "d"), "c", false)
+ assertSameTabs(store, tabList, "c,d to c-")
+ dispatchJoinMoveAction(store, listOf("c", "d"), "d", true)
+ assertSameTabs(store, tabList, "c,d to d+")
+
+ val movedTabList = listOf(
+ createTab(id = "b", url = "https://www.firefox.com"),
+ createTab(id = "c", url = "https://getpocket.com"),
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "d", url = "https://www.example.org"),
+ )
+ dispatchJoinMoveAction(store, listOf("a"), "d", false)
+ assertSameTabs(store, movedTabList, "a to d-")
+ dispatchJoinMoveAction(store, listOf("b", "c"), "a", true)
+ assertSameTabs(store, tabList, "b,c to a+")
+
+ dispatchJoinMoveAction(store, listOf("a", "d"), "c", true)
+ assertSameTabs(store, movedTabList, "a,d to c+")
+
+ dispatchJoinMoveAction(store, listOf("b", "c"), "d", false)
+ assertSameTabs(store, tabList, "b,c to d-")
+ assertEquals("a", store.state.selectedTabId)
+ }
+
+ @Test
+ fun `MoveTabsAction - Complex moves work`() {
+ val tabList = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ createTab(id = "c", url = "https://getpocket.com"),
+ createTab(id = "d", url = "https://www.example.org"),
+ createTab(id = "e", url = "https://www.mozilla.org/en-US/firefox/features/"),
+ createTab(id = "f", url = "https://www.mozilla.org/en-US/firefox/products/"),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ tabs = tabList,
+ selectedTabId = "a",
+ ),
+ )
+ dispatchJoinMoveAction(store, listOf("a", "b", "c", "d", "e", "f"), "a", false)
+ assertSameTabs(store, tabList, "all to a-")
+
+ val movedTabList = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ createTab(id = "c", url = "https://getpocket.com"),
+ createTab(id = "b", url = "https://www.firefox.com"),
+ createTab(id = "e", url = "https://www.mozilla.org/en-US/firefox/features/"),
+ createTab(id = "d", url = "https://www.example.org"),
+ createTab(id = "f", url = "https://www.mozilla.org/en-US/firefox/products/"),
+ )
+ dispatchJoinMoveAction(store, listOf("b", "e"), "d", false)
+ assertSameTabs(store, movedTabList, "b,e to d-")
+
+ dispatchJoinMoveAction(store, listOf("c", "d"), "b", true)
+ assertSameTabs(store, tabList, "c,d to b+")
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TrackingProtectionActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TrackingProtectionActionTest.kt
new file mode 100644
index 0000000000..eedf767407
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TrackingProtectionActionTest.kt
@@ -0,0 +1,171 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+class TrackingProtectionActionTest {
+ private lateinit var tab: TabSessionState
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setUp() {
+ tab = createTab("https://www.mozilla.org")
+
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+ }
+
+ private fun tabState(): TabSessionState = store.state.findTab(tab.id)!!
+ private fun trackingProtectionState() = tabState().trackingProtection
+
+ @Test
+ fun `ToggleAction - Updates enabled flag of TrackingProtectionState`() {
+ assertFalse(trackingProtectionState().enabled)
+
+ store.dispatch(TrackingProtectionAction.ToggleAction(tabId = tab.id, enabled = true))
+ .joinBlocking()
+
+ assertTrue(trackingProtectionState().enabled)
+
+ store.dispatch(TrackingProtectionAction.ToggleAction(tabId = tab.id, enabled = true))
+ .joinBlocking()
+
+ assertTrue(trackingProtectionState().enabled)
+
+ store.dispatch(TrackingProtectionAction.ToggleAction(tabId = tab.id, enabled = false))
+ .joinBlocking()
+
+ assertFalse(trackingProtectionState().enabled)
+
+ store.dispatch(TrackingProtectionAction.ToggleAction(tabId = tab.id, enabled = true))
+ .joinBlocking()
+
+ assertTrue(trackingProtectionState().enabled)
+ }
+
+ @Test
+ fun `ToggleExclusionListAction - Updates enabled flag of TrackingProtectionState`() {
+ assertFalse(trackingProtectionState().ignoredOnTrackingProtection)
+
+ store.dispatch(
+ TrackingProtectionAction.ToggleExclusionListAction(
+ tabId = tab.id,
+ excluded = true,
+ ),
+ ).joinBlocking()
+
+ assertTrue(trackingProtectionState().ignoredOnTrackingProtection)
+
+ store.dispatch(
+ TrackingProtectionAction.ToggleExclusionListAction(
+ tabId = tab.id,
+ excluded = true,
+ ),
+ ).joinBlocking()
+
+ assertTrue(trackingProtectionState().ignoredOnTrackingProtection)
+
+ store.dispatch(
+ TrackingProtectionAction.ToggleExclusionListAction(
+ tabId = tab.id,
+ excluded = false,
+ ),
+ ).joinBlocking()
+
+ assertFalse(trackingProtectionState().ignoredOnTrackingProtection)
+
+ store.dispatch(
+ TrackingProtectionAction.ToggleExclusionListAction(
+ tabId = tab.id,
+ excluded = true,
+ ),
+ ).joinBlocking()
+
+ assertTrue(trackingProtectionState().ignoredOnTrackingProtection)
+ }
+
+ @Test
+ fun `TrackerBlockedAction - Adds tackers to TrackingProtectionState`() {
+ assertTrue(trackingProtectionState().blockedTrackers.isEmpty())
+ assertTrue(trackingProtectionState().loadedTrackers.isEmpty())
+
+ store.dispatch(TrackingProtectionAction.TrackerBlockedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ assertEquals(1, trackingProtectionState().blockedTrackers.size)
+ assertEquals(0, trackingProtectionState().loadedTrackers.size)
+
+ store.dispatch(TrackingProtectionAction.TrackerBlockedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ store.dispatch(TrackingProtectionAction.TrackerBlockedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ assertEquals(3, trackingProtectionState().blockedTrackers.size)
+ assertEquals(0, trackingProtectionState().loadedTrackers.size)
+ }
+
+ @Test
+ fun `TrackerLoadedAction - Adds tackers to TrackingProtectionState`() {
+ assertTrue(trackingProtectionState().blockedTrackers.isEmpty())
+ assertTrue(trackingProtectionState().loadedTrackers.isEmpty())
+
+ store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ assertEquals(0, trackingProtectionState().blockedTrackers.size)
+ assertEquals(1, trackingProtectionState().loadedTrackers.size)
+
+ store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ assertEquals(0, trackingProtectionState().blockedTrackers.size)
+ assertEquals(3, trackingProtectionState().loadedTrackers.size)
+ }
+
+ @Test
+ fun `ClearTrackers - Removes trackers from TrackingProtectionState`() {
+ store.dispatch(TrackingProtectionAction.TrackerBlockedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ store.dispatch(TrackingProtectionAction.TrackerBlockedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId = tab.id, tracker = mock()))
+ .joinBlocking()
+
+ assertEquals(2, trackingProtectionState().blockedTrackers.size)
+ assertEquals(3, trackingProtectionState().loadedTrackers.size)
+
+ store.dispatch(TrackingProtectionAction.ClearTrackersAction(tab.id)).joinBlocking()
+
+ assertEquals(0, trackingProtectionState().blockedTrackers.size)
+ assertEquals(0, trackingProtectionState().loadedTrackers.size)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt
new file mode 100644
index 0000000000..2a12cda264
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt
@@ -0,0 +1,903 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.translate.DetectedLanguages
+import mozilla.components.concept.engine.translate.Language
+import mozilla.components.concept.engine.translate.LanguageModel
+import mozilla.components.concept.engine.translate.LanguageSetting
+import mozilla.components.concept.engine.translate.TranslationDownloadSize
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettingOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettings
+import mozilla.components.concept.engine.translate.TranslationPair
+import mozilla.components.concept.engine.translate.TranslationSupport
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+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 java.lang.Exception
+
+class TranslationsActionTest {
+ private lateinit var tab: TabSessionState
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setUp() {
+ tab = createTab("https://www.mozilla.org")
+
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+ }
+
+ private fun tabState(): TabSessionState = store.state.findTab(tab.id)!!
+
+ @Test
+ fun `WHEN a TranslateExpectedAction is dispatched THEN update translation expected status`() {
+ assertEquals(false, tabState().translationsState.isExpectedTranslate)
+
+ store.dispatch(TranslationsAction.TranslateExpectedAction(tabId = tab.id))
+ .joinBlocking()
+
+ assertEquals(true, tabState().translationsState.isExpectedTranslate)
+ }
+
+ @Test
+ fun `WHEN a TranslateOfferAction is dispatched THEN update translation expected status`() {
+ assertEquals(false, tabState().translationsState.isOfferTranslate)
+
+ store.dispatch(TranslationsAction.TranslateOfferAction(tabId = tab.id, isOfferTranslate = true))
+ .joinBlocking()
+
+ assertEquals(true, tabState().translationsState.isOfferTranslate)
+
+ store.dispatch(TranslationsAction.TranslateOfferAction(tabId = tab.id, isOfferTranslate = false))
+ .joinBlocking()
+
+ assertFalse(tabState().translationsState.isOfferTranslate)
+ }
+
+ @Test
+ fun `WHEN a TranslateStateChangeAction is dispatched THEN update translation expected status`() {
+ assertEquals(null, tabState().translationsState.translationEngineState)
+
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, mock()))
+ .joinBlocking()
+
+ assertEquals(true, tabState().translationsState.translationEngineState != null)
+ }
+
+ @Test
+ fun `WHEN a TranslateStateChangeAction is dispatched THEN the translation status updates accordingly`() {
+ assertNull(tabState().translationsState.translationEngineState)
+ assertFalse(tabState().translationsState.isTranslated)
+ assertFalse(tabState().translationsState.isExpectedTranslate)
+
+ val translatedEngineState = TranslationEngineState(
+ detectedLanguages = DetectedLanguages(documentLangTag = "es", supportedDocumentLang = true, userPreferredLangTag = "en"),
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = TranslationPair(fromLanguage = "es", toLanguage = "en"),
+ )
+
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = translatedEngineState))
+ .joinBlocking()
+
+ // Translated state
+ assertEquals(translatedEngineState, tabState().translationsState.translationEngineState)
+ assertTrue(tabState().translationsState.isTranslated)
+ assertFalse(tabState().translationsState.isExpectedTranslate)
+
+ val nonTranslatedEngineState = TranslationEngineState(
+ detectedLanguages = DetectedLanguages(documentLangTag = "es", supportedDocumentLang = true, userPreferredLangTag = "en"),
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = TranslationPair(fromLanguage = null, toLanguage = null),
+ )
+
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, nonTranslatedEngineState))
+ .joinBlocking()
+
+ // Non-translated state
+ assertEquals(nonTranslatedEngineState, tabState().translationsState.translationEngineState)
+ assertFalse(tabState().translationsState.isTranslated)
+ assertFalse(tabState().translationsState.isExpectedTranslate)
+ }
+
+ @Test
+ fun `GIVEN isOfferTranslate is true WHEN a TranslateAction is dispatched THEN isOfferTranslate should be set to false`() {
+ // Initial State
+ assertFalse(tabState().translationsState.isOfferTranslate)
+
+ // Initial Offer State
+ store.dispatch(TranslationsAction.TranslateOfferAction(tabId = tab.id, true)).joinBlocking()
+ assertTrue(tabState().translationsState.isOfferTranslate)
+
+ // Action
+ store.dispatch(TranslationsAction.TranslateAction(tabId = tab.id, fromLanguage = "en", toLanguage = "en", options = null)).joinBlocking()
+
+ // Should revert to false
+ assertFalse(tabState().translationsState.isOfferTranslate)
+ }
+
+ @Test
+ fun `WHEN a TranslateStateChangeAction is dispatched THEN the isExpectedTranslate status updates accordingly`() {
+ // Initial State
+ assertNull(tabState().translationsState.translationEngineState)
+
+ // Sending an initial request to set state; however, the engine hasn't decided if it is an
+ // expected state
+ var translatedEngineState = TranslationEngineState(
+ detectedLanguages = DetectedLanguages(documentLangTag = "es", supportedDocumentLang = true, userPreferredLangTag = "en"),
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = TranslationPair(fromLanguage = "es", toLanguage = "en"),
+ )
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = translatedEngineState))
+ .joinBlocking()
+ assertFalse(tabState().translationsState.isExpectedTranslate)
+
+ // Engine is sending a translation expected action
+ store.dispatch(TranslationsAction.TranslateExpectedAction(tabId = tab.id))
+ .joinBlocking()
+
+ // Initial expected translation state
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = translatedEngineState))
+ .joinBlocking()
+ assertTrue(tabState().translationsState.isExpectedTranslate)
+
+ // Not expected translation state, because it is no longer supported
+ translatedEngineState = TranslationEngineState(
+ detectedLanguages = DetectedLanguages(documentLangTag = "es", supportedDocumentLang = false, userPreferredLangTag = "en"),
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = TranslationPair(fromLanguage = "es", toLanguage = "en"),
+ )
+
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = translatedEngineState))
+ .joinBlocking()
+ assertFalse(tabState().translationsState.isExpectedTranslate)
+ }
+
+ @Test
+ fun `WHEN a TranslateStateChangeAction is dispatched THEN the isOfferTranslate status updates accordingly`() {
+ // Initial State
+ assertNull(tabState().translationsState.translationEngineState)
+
+ // Sending an initial request to set state; however, the engine hasn't decided if it is an
+ // offered state
+ var translatedEngineState = TranslationEngineState(
+ detectedLanguages = DetectedLanguages(documentLangTag = "es", supportedDocumentLang = true, userPreferredLangTag = "en"),
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = TranslationPair(fromLanguage = "es", toLanguage = "en"),
+ )
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = translatedEngineState))
+ .joinBlocking()
+ assertFalse(tabState().translationsState.isOfferTranslate)
+
+ // Engine is sending a translation offer action
+ store.dispatch(TranslationsAction.TranslateOfferAction(tabId = tab.id, isOfferTranslate = true))
+ .joinBlocking()
+
+ // Initial expected translation state
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = translatedEngineState))
+ .joinBlocking()
+ assertTrue(tabState().translationsState.isOfferTranslate)
+
+ // Not in an offer translation state, because it is no longer supported
+ translatedEngineState = TranslationEngineState(
+ detectedLanguages = DetectedLanguages(documentLangTag = "es", supportedDocumentLang = false, userPreferredLangTag = "en"),
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = TranslationPair(fromLanguage = "es", toLanguage = "en"),
+ )
+
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = translatedEngineState))
+ .joinBlocking()
+ assertFalse(tabState().translationsState.isOfferTranslate)
+ }
+
+ @Test
+ fun `WHEN a TranslateStateChangeAction is dispatched THEN the translationError status updates accordingly`() {
+ // Initial State
+ assertNull(tabState().translationsState.translationEngineState)
+ assertNull(tabState().translationsState.translationError)
+
+ // Sending an initial request to set state, notice the supportedDocumentLang isn't supported
+ val noSupportedState = TranslationEngineState(
+ detectedLanguages = DetectedLanguages(documentLangTag = "unknown", supportedDocumentLang = false, userPreferredLangTag = "en"),
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = null,
+ )
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = noSupportedState))
+ .joinBlocking()
+
+ // Response state
+ assertEquals(noSupportedState, tabState().translationsState.translationEngineState)
+ assertNotNull(tabState().translationsState.translationError)
+
+ // Sending a request to show state change, notice the supportedDocumentLang is now supported
+ val supportedState = TranslationEngineState(
+ detectedLanguages = DetectedLanguages(documentLangTag = "es", supportedDocumentLang = true, userPreferredLangTag = "en"),
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = null,
+ )
+ store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId = tab.id, translationEngineState = supportedState))
+ .joinBlocking()
+
+ // Response state
+ assertEquals(supportedState, tabState().translationsState.translationEngineState)
+ assertNull(tabState().translationsState.translationError)
+ }
+
+ @Test
+ fun `WHEN a TranslateAction is dispatched AND successful THEN update translation processing status`() {
+ // Initial
+ assertEquals(false, tabState().translationsState.isTranslateProcessing)
+
+ // Action started
+ store.dispatch(TranslationsAction.TranslateAction(tabId = tab.id, "en", "es", null))
+ .joinBlocking()
+ assertEquals(true, tabState().translationsState.isTranslateProcessing)
+
+ // Action success
+ store.dispatch(TranslationsAction.TranslateSuccessAction(tabId = tab.id, operation = TranslationOperation.TRANSLATE))
+ .joinBlocking()
+ assertEquals(false, tabState().translationsState.isTranslateProcessing)
+ assertEquals(true, tabState().translationsState.isTranslated)
+ assertEquals(null, tabState().translationsState.translationError)
+ }
+
+ @Test
+ fun `WHEN a TranslateAction is dispatched AND fails THEN update translation processing status`() {
+ // Initial
+ assertEquals(false, tabState().translationsState.isTranslateProcessing)
+
+ // Action started
+ store.dispatch(TranslationsAction.TranslateAction(tabId = tab.id, "en", "es", null))
+ .joinBlocking()
+ assertEquals(true, tabState().translationsState.isTranslateProcessing)
+
+ // Action failure
+ val error = TranslationError.UnknownError(Exception())
+ store.dispatch(TranslationsAction.TranslateExceptionAction(tabId = tab.id, operation = TranslationOperation.TRANSLATE, error))
+ .joinBlocking()
+ assertEquals(false, tabState().translationsState.isTranslateProcessing)
+ assertEquals(false, tabState().translationsState.isTranslated)
+ assertEquals(error, tabState().translationsState.translationError)
+ }
+
+ @Test
+ fun `WHEN a TranslateRestoreAction is dispatched AND successful THEN update translation processing status`() {
+ // Initial
+ assertEquals(false, tabState().translationsState.isRestoreProcessing)
+
+ // Action started
+ store.dispatch(TranslationsAction.TranslateRestoreAction(tabId = tab.id))
+ .joinBlocking()
+ assertEquals(true, tabState().translationsState.isRestoreProcessing)
+
+ // Action success
+ store.dispatch(TranslationsAction.TranslateSuccessAction(tabId = tab.id, operation = TranslationOperation.RESTORE))
+ .joinBlocking()
+ assertEquals(false, tabState().translationsState.isRestoreProcessing)
+ assertEquals(false, tabState().translationsState.isTranslated)
+ assertEquals(null, tabState().translationsState.translationError)
+ }
+
+ @Test
+ fun `WHEN a TranslateRestoreAction is dispatched AND fails THEN update translation processing status`() {
+ // Initial
+ assertEquals(false, tabState().translationsState.isRestoreProcessing)
+
+ // Action started
+ store.dispatch(TranslationsAction.TranslateRestoreAction(tabId = tab.id))
+ .joinBlocking()
+ assertEquals(true, tabState().translationsState.isRestoreProcessing)
+
+ // Action failure
+ val error = TranslationError.UnknownError(Exception())
+ store.dispatch(TranslationsAction.TranslateExceptionAction(tabId = tab.id, operation = TranslationOperation.RESTORE, error))
+ .joinBlocking()
+ assertEquals(false, tabState().translationsState.isRestoreProcessing)
+ assertEquals(false, tabState().translationsState.isTranslated)
+ assertEquals(error, tabState().translationsState.translationError)
+ }
+
+ @Test
+ fun `WHEN a SetSupportedLanguagesAction is dispatched AND successful THEN update supportedLanguages`() {
+ // Initial
+ assertNull(store.state.translationEngine.supportedLanguages)
+
+ // Action started
+ val toLanguage = Language("de", "German")
+ val fromLanguage = Language("es", "Spanish")
+ val supportedLanguages = TranslationSupport(listOf(fromLanguage), listOf(toLanguage))
+ store.dispatch(
+ TranslationsAction.SetSupportedLanguagesAction(
+ supportedLanguages = supportedLanguages,
+ ),
+ )
+ .joinBlocking()
+
+ // Action success
+ assertEquals(supportedLanguages, store.state.translationEngine.supportedLanguages)
+ }
+
+ @Test
+ fun `WHEN a SetNeverTranslateSitesAction is dispatched AND successful THEN update neverTranslateSites`() {
+ // Initial
+ assertEquals(null, tabState().translationsState.neverTranslateSites)
+
+ // Action started
+ val neverTranslateSites = listOf("google.com")
+ store.dispatch(
+ TranslationsAction.SetNeverTranslateSitesAction(
+ tabId = tab.id,
+ neverTranslateSites = neverTranslateSites,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertEquals(neverTranslateSites, tabState().translationsState.neverTranslateSites)
+ }
+
+ @Test
+ fun `WHEN a RemoveNeverTranslateSiteAction is dispatched AND successful THEN update neverTranslateSites`() {
+ // Initial add to neverTranslateSites
+ assertEquals(null, tabState().translationsState.neverTranslateSites)
+ val neverTranslateSites = listOf("google.com")
+ store.dispatch(
+ TranslationsAction.SetNeverTranslateSitesAction(
+ tabId = tab.id,
+ neverTranslateSites = neverTranslateSites,
+ ),
+ ).joinBlocking()
+ assertEquals(neverTranslateSites, tabState().translationsState.neverTranslateSites)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.RemoveNeverTranslateSiteAction(
+ tabId = tab.id,
+ origin = "google.com",
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertEquals(listOf<String>(), tabState().translationsState.neverTranslateSites)
+ }
+
+ @Test
+ fun `WHEN a TranslateExceptionAction is dispatched due to an error THEN update the error condition according to the operation`() {
+ // Initial state
+ assertEquals(null, tabState().translationsState.translationError)
+
+ // TRANSLATE usage
+ val translateError = TranslationError.CouldNotLoadLanguagesError(null)
+ store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.TRANSLATE,
+ translationError = translateError,
+ ),
+ ).joinBlocking()
+ assertEquals(translateError, tabState().translationsState.translationError)
+
+ // RESTORE usage
+ val restoreError = TranslationError.CouldNotRestoreError(null)
+ store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.RESTORE,
+ translationError = restoreError,
+ ),
+ ).joinBlocking()
+ assertEquals(restoreError, tabState().translationsState.translationError)
+
+ // FETCH_LANGUAGES usage
+ val fetchLanguagesError = TranslationError.CouldNotLoadLanguagesError(null)
+
+ // Testing setting tab level error
+ store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ translationError = fetchLanguagesError,
+ ),
+ ).joinBlocking()
+ assertEquals(fetchLanguagesError, tabState().translationsState.translationError)
+
+ // Testing setting browser level error
+ store.dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = fetchLanguagesError,
+ ),
+ ).joinBlocking()
+ assertEquals(fetchLanguagesError, store.state.translationEngine.engineError)
+ }
+
+ @Test
+ fun `WHEN a TranslateSuccessAction is dispatched THEN update the condition according to the operation`() {
+ // Initial state
+ assertEquals(null, tabState().translationsState.translationError)
+ assertEquals(false, tabState().translationsState.isTranslated)
+ assertEquals(false, tabState().translationsState.isTranslateProcessing)
+
+ // TRANSLATE usage
+ store.dispatch(
+ TranslationsAction.TranslateSuccessAction(
+ tabId = tab.id,
+ operation = TranslationOperation.TRANSLATE,
+ ),
+ ).joinBlocking()
+ assertEquals(null, tabState().translationsState.translationError)
+ assertEquals(true, tabState().translationsState.isTranslated)
+ assertEquals(false, tabState().translationsState.isTranslateProcessing)
+
+ // RESTORE usage
+ store.dispatch(
+ TranslationsAction.TranslateSuccessAction(
+ tabId = tab.id,
+ operation = TranslationOperation.RESTORE,
+ ),
+ ).joinBlocking()
+ assertEquals(null, tabState().translationsState.translationError)
+ assertEquals(false, tabState().translationsState.isTranslated)
+ assertEquals(false, tabState().translationsState.isRestoreProcessing)
+
+ // FETCH_LANGUAGES usage
+ store.dispatch(
+ TranslationsAction.TranslateSuccessAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ ),
+ ).joinBlocking()
+ assertEquals(null, tabState().translationsState.translationError)
+ assertEquals(false, tabState().translationsState.isTranslated)
+ }
+
+ @Test
+ fun `WHEN a SetPageSettingsAction is dispatched THEN set pageSettings`() {
+ // Initial
+ assertNull(tabState().translationsState.pageSettings)
+
+ // Action started
+ val pageSettings = TranslationPageSettings(
+ alwaysOfferPopup = true,
+ alwaysTranslateLanguage = true,
+ neverTranslateLanguage = true,
+ neverTranslateSite = true,
+ )
+ store.dispatch(
+ TranslationsAction.SetPageSettingsAction(
+ tabId = tab.id,
+ pageSettings = pageSettings,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertEquals(pageSettings, tabState().translationsState.pageSettings)
+ }
+
+ @Test
+ fun `WHEN a SetTranslationDownloadSize is dispatched THEN set translationSize is set`() {
+ // Initial
+ assertNull(tabState().translationsState.translationDownloadSize)
+
+ // Action started
+ val translationSize = TranslationDownloadSize(
+ fromLanguage = Language("en", "English"),
+ toLanguage = Language("fr", "French"),
+ size = 10000L,
+ error = null,
+ )
+ store.dispatch(
+ TranslationsAction.SetTranslationDownloadSizeAction(
+ tabId = tab.id,
+ translationSize = translationSize,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertEquals(translationSize, tabState().translationsState.translationDownloadSize)
+ }
+
+ @Test
+ fun `WHEN a FetchTranslationDownloadSize is dispatched THEN translationSize is cleared`() {
+ // Initial setting size for a more robust test
+ val translationSize = TranslationDownloadSize(
+ fromLanguage = Language("en", "English"),
+ toLanguage = Language("fr", "French"),
+ size = 10000L,
+ error = null,
+ )
+ store.dispatch(
+ TranslationsAction.SetTranslationDownloadSizeAction(
+ tabId = tab.id,
+ translationSize = translationSize,
+ ),
+ ).joinBlocking()
+
+ assertEquals(translationSize, tabState().translationsState.translationDownloadSize)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.FetchTranslationDownloadSizeAction(
+ tabId = tab.id,
+ fromLanguage = Language("en", "English"),
+ toLanguage = Language("fr", "French"),
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertNull(tabState().translationsState.translationDownloadSize)
+ }
+
+ @Test
+ fun `WHEN a OperationRequestedAction is dispatched for FETCH_PAGE_SETTINGS THEN clear pageSettings`() {
+ // Setting first to have a more robust initial state
+ assertNull(tabState().translationsState.pageSettings)
+
+ val pageSettings = TranslationPageSettings(
+ alwaysOfferPopup = true,
+ alwaysTranslateLanguage = true,
+ neverTranslateLanguage = true,
+ neverTranslateSite = true,
+ )
+
+ store.dispatch(
+ TranslationsAction.SetPageSettingsAction(
+ tabId = tab.id,
+ pageSettings = pageSettings,
+ ),
+ ).joinBlocking()
+
+ assertEquals(pageSettings, tabState().translationsState.pageSettings)
+ assertNull(tabState().translationsState.settingsError)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertNull(tabState().translationsState.pageSettings)
+ }
+
+ @Test
+ fun `WHEN a OperationRequestedAction is dispatched for FETCH_SUPPORTED_LANGUAGES THEN clear supportLanguages`() {
+ // Setting first to have a more robust initial state
+ assertNull(store.state.translationEngine.supportedLanguages)
+
+ val supportLanguages = TranslationSupport(
+ fromLanguages = listOf(Language("en", "English")),
+ toLanguages = listOf(Language("en", "English")),
+ )
+
+ store.dispatch(
+ TranslationsAction.SetSupportedLanguagesAction(
+ supportedLanguages = supportLanguages,
+ ),
+ ).joinBlocking()
+
+ assertEquals(supportLanguages, store.state.translationEngine.supportedLanguages)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertNull(store.state.translationEngine.supportedLanguages)
+ }
+
+ @Test
+ fun `WHEN a UpdatePageSettingAction is dispatched for UPDATE_ALWAYS_OFFER_POPUP THEN set page settings for alwaysOfferPopup `() {
+ // Initial State
+ assertNull(tabState().translationsState.pageSettings?.alwaysOfferPopup)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_ALWAYS_OFFER_POPUP,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertTrue(tabState().translationsState.pageSettings?.alwaysOfferPopup!!)
+ }
+
+ @Test
+ fun `WHEN a UpdatePageSettingAction is dispatched for UPDATE_ALWAYS_TRANSLATE_LANGUAGE THEN set page settings for alwaysTranslateLanguage `() {
+ // Initial State
+ assertNull(tabState().translationsState.pageSettings?.alwaysTranslateLanguage)
+ assertNull(tabState().translationsState.pageSettings?.neverTranslateLanguage)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_ALWAYS_TRANSLATE_LANGUAGE,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertTrue(tabState().translationsState.pageSettings?.alwaysTranslateLanguage!!)
+ assertFalse(tabState().translationsState.pageSettings?.neverTranslateLanguage!!)
+ }
+
+ @Test
+ fun `WHEN a UpdatePageSettingAction is dispatched for UPDATE_NEVER_TRANSLATE_LANGUAGE THEN set page settings for alwaysTranslateLanguage `() {
+ // Initial State
+ assertNull(tabState().translationsState.pageSettings?.neverTranslateLanguage)
+ assertNull(tabState().translationsState.pageSettings?.alwaysTranslateLanguage)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_LANGUAGE,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertTrue(tabState().translationsState.pageSettings?.neverTranslateLanguage!!)
+ assertFalse(tabState().translationsState.pageSettings?.alwaysTranslateLanguage!!)
+ }
+
+ @Test
+ fun `WHEN a UpdatePageSettingAction is dispatched for UPDATE_NEVER_TRANSLATE_SITE THEN set page settings for neverTranslateSite`() {
+ // Initial State
+ assertNull(tabState().translationsState.pageSettings?.neverTranslateLanguage)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_SITE,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertTrue(tabState().translationsState.pageSettings?.neverTranslateSite!!)
+ }
+
+ @Test
+ fun `WHEN an UpdatePageSettingAction is dispatched for UPDATE_ALWAYS_TRANSLATE_LANGUAGE AND UPDATE_ALWAYS_TRANSLATE_LANGUAGE THEN must be opposites of each other or both must be false `() {
+ // Initial state
+ assertNull(tabState().translationsState.pageSettings?.alwaysTranslateLanguage)
+ assertNull(tabState().translationsState.pageSettings?.neverTranslateLanguage)
+
+ // Action started to update the always offer setting to true
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_ALWAYS_TRANSLATE_LANGUAGE,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ // When always is true, never should be false
+ assertTrue(tabState().translationsState.pageSettings?.alwaysTranslateLanguage!!)
+ assertFalse(tabState().translationsState.pageSettings?.neverTranslateLanguage!!)
+
+ // Action started to update the never offer setting to true
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_LANGUAGE,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ // When never is true, always should be false
+ assertFalse(tabState().translationsState.pageSettings?.alwaysTranslateLanguage!!)
+ assertTrue(tabState().translationsState.pageSettings?.neverTranslateLanguage!!)
+
+ // Action started to update the never language setting to false
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_LANGUAGE,
+ setting = false,
+ ),
+ ).joinBlocking()
+
+ // When never is false, always may also be false
+ assertFalse(tabState().translationsState.pageSettings?.alwaysTranslateLanguage!!)
+ assertFalse(tabState().translationsState.pageSettings?.neverTranslateLanguage!!)
+ }
+
+ @Test
+ fun `WHEN a UpdatePageSettingAction is dispatched for each option THEN the page setting is consistent`() {
+ // Initial State
+ assertNull(tabState().translationsState.pageSettings?.alwaysOfferPopup)
+ assertNull(tabState().translationsState.pageSettings?.alwaysTranslateLanguage)
+ assertNull(tabState().translationsState.pageSettings?.neverTranslateLanguage)
+ assertNull(tabState().translationsState.pageSettings?.neverTranslateSite)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_ALWAYS_OFFER_POPUP,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_ALWAYS_TRANSLATE_LANGUAGE,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_LANGUAGE,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_SITE,
+ setting = true,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertTrue(tabState().translationsState.pageSettings?.alwaysOfferPopup!!)
+ // neverTranslateLanguage was posted last and will prevent a contradictory state on the alwaysTranslateLanguage state.
+ assertFalse(tabState().translationsState.pageSettings?.alwaysTranslateLanguage!!)
+ assertTrue(tabState().translationsState.pageSettings?.neverTranslateLanguage!!)
+ assertTrue(tabState().translationsState.pageSettings?.neverTranslateSite!!)
+ }
+
+ @Test
+ fun `WHEN a SetLanguageSettingsAction is dispatched THEN the browser store is updated to match`() {
+ // Initial state
+ assertNull(store.state.translationEngine.languageSettings)
+
+ // Dispatch
+ val languageSetting = mapOf("es" to LanguageSetting.OFFER)
+ store.dispatch(
+ TranslationsAction.SetLanguageSettingsAction(
+ languageSettings = languageSetting,
+ ),
+ ).joinBlocking()
+
+ // Final state
+ assertEquals(store.state.translationEngine.languageSettings!!, languageSetting)
+ }
+
+ @Test
+ fun `WHEN a OperationRequestedAction is dispatched for FETCH_AUTOMATIC_LANGUAGE_SETTINGS THEN clear languageSettings`() {
+ // Setting first to have a more robust initial state
+ val languageSetting = mapOf("es" to LanguageSetting.OFFER)
+ store.dispatch(
+ TranslationsAction.SetLanguageSettingsAction(
+ languageSettings = languageSetting,
+ ),
+ ).joinBlocking()
+ assertEquals(store.state.translationEngine.languageSettings, languageSetting)
+
+ // Action started
+ store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertNull(store.state.translationEngine.languageSettings)
+ }
+
+ @Test
+ fun `WHEN a TranslateExceptionAction is dispatched for FETCH_AUTOMATIC_LANGUAGE_SETTINGS THEN set the error`() {
+ // Action started
+ val error = TranslationError.UnknownError(IllegalStateException())
+ store.dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
+ translationError = error,
+ ),
+ ).joinBlocking()
+
+ // Action success
+ assertEquals(error, tabState().translationsState.translationError)
+ }
+
+ @Test
+ fun `WHEN a SetEngineSupportAction is dispatched THEN the browser store is updated to match`() {
+ // Initial state
+ assertNull(store.state.translationEngine.isEngineSupported)
+
+ // Dispatch
+ store.dispatch(
+ TranslationsAction.SetEngineSupportedAction(
+ isEngineSupported = true,
+ ),
+ ).joinBlocking()
+
+ // Final state
+ assertTrue(store.state.translationEngine.isEngineSupported!!)
+ }
+
+ @Test
+ fun `WHEN an EngineExceptionAction is dispatched THEN the browser store is updated to match`() {
+ // Initial state
+ assertNull(store.state.translationEngine.engineError)
+
+ // Dispatch
+ val error = TranslationError.UnknownError(Throwable())
+ store.dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = error,
+ ),
+ ).joinBlocking()
+
+ // Final state
+ assertEquals(store.state.translationEngine.engineError!!, error)
+ }
+
+ @Test
+ fun `WHEN a SetLanguageModelsAction is dispatched and successful THEN the browser store is updated to match`() {
+ // Initial state
+ assertNull(store.state.translationEngine.languageModels)
+
+ val code = "es"
+ val localizedDisplayName = "Spanish"
+ val isDownloaded = true
+ val size: Long = 1234
+ val language = Language(code, localizedDisplayName)
+ val languageModel = LanguageModel(language, isDownloaded, size)
+ val languageModels = mutableListOf(languageModel)
+
+ // Dispatch
+ store.dispatch(
+ TranslationsAction.SetLanguageModelsAction(
+ languageModels = languageModels,
+ ),
+ ).joinBlocking()
+
+ // Final state
+ assertEquals(languageModels, store.state.translationEngine.languageModels)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/UpdateProductUrlStateActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/UpdateProductUrlStateActionTest.kt
new file mode 100644
index 0000000000..49f348300a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/UpdateProductUrlStateActionTest.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.browser.state.action
+
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+
+class UpdateProductUrlStateActionTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `WHEN shopping product action is dispatched THEN isProductUrl of the relevant tab should reflect that`() {
+ val tab1 = TabSessionState(
+ id = "tab1",
+ content = ContentState(
+ url = "https://mozilla.org",
+ private = false,
+ isProductUrl = false,
+ ),
+ )
+ val tab2 = TabSessionState(
+ id = "tab2",
+ content = ContentState(
+ url = "https://www.amazon.com/product/123",
+ private = false,
+ isProductUrl = false,
+ ),
+ )
+ val browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ val browserStore = BrowserStore(initialState = browserState)
+
+ browserStore.dispatch(
+ ContentAction.UpdateProductUrlStateAction(tabId = "tab2", isProductUrl = true),
+ ).joinBlocking()
+
+ val actual = browserStore.state.findTab("tab2")!!.content.isProductUrl
+
+ assertTrue(actual)
+ }
+
+ @Test
+ fun `WHEN shopping product action is dispatched THEN private tab should not be affected`() {
+ val tab1 = TabSessionState(
+ id = "tab1",
+ content = ContentState(
+ url = "https://www.amazon.com/product/123",
+ private = true,
+ isProductUrl = false,
+ ),
+ )
+ val browserState = BrowserState(tabs = listOf(tab1))
+
+ val browserStore = BrowserStore(initialState = browserState)
+
+ browserStore.dispatch(
+ ContentAction.UpdateProductUrlStateAction(tabId = "tab1", isProductUrl = true),
+ ).joinBlocking()
+
+ val actual = browserStore.state.findTab("tab1")!!.content.isProductUrl
+
+ assertFalse(actual)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/WebExtensionActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/WebExtensionActionTest.kt
new file mode 100644
index 0000000000..97f8f27476
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/WebExtensionActionTest.kt
@@ -0,0 +1,420 @@
+/* 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.browser.state.action
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
+import mozilla.components.concept.engine.webextension.WebExtensionPageAction
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class WebExtensionActionTest {
+
+ @Test
+ fun `InstallWebExtension - Adds an extension to the BrowserState extensions`() {
+ val store = BrowserStore()
+
+ assertTrue(store.state.extensions.isEmpty())
+
+ val extension = WebExtensionState("id", "url")
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension)).joinBlocking()
+
+ assertFalse(store.state.extensions.isEmpty())
+ assertEquals(extension, store.state.extensions.values.first())
+
+ // Installing the same extension twice should have no effect
+ val state = store.state
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension)).joinBlocking()
+ assertSame(state, store.state)
+ }
+
+ @Test
+ fun `InstallWebExtension - Keeps existing browser and page actions`() {
+ val store = BrowserStore()
+ assertTrue(store.state.extensions.isEmpty())
+
+ val extension = WebExtensionState("id", "url", "name")
+ val mockedBrowserAction = mock<WebExtensionBrowserAction>()
+ val mockedPageAction = mock<WebExtensionPageAction>()
+ store.dispatch(WebExtensionAction.UpdateBrowserAction(extension.id, mockedBrowserAction)).joinBlocking()
+ store.dispatch(WebExtensionAction.UpdatePageAction(extension.id, mockedPageAction)).joinBlocking()
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension)).joinBlocking()
+
+ assertFalse(store.state.extensions.isEmpty())
+ assertEquals(
+ extension.copy(
+ browserAction = mockedBrowserAction,
+ pageAction = mockedPageAction,
+ ),
+ store.state.extensions.values.first(),
+ )
+ }
+
+ @Test
+ fun `UninstallWebExtension - Removes all state of the uninstalled extension`() {
+ val tab1 = createTab("url")
+ val tab2 = createTab("url")
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab1, tab2),
+ ),
+ )
+
+ assertTrue(store.state.extensions.isEmpty())
+
+ val extension1 = WebExtensionState("id1", "url")
+ val extension2 = WebExtensionState("i2", "url")
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension1)).joinBlocking()
+
+ assertFalse(store.state.extensions.isEmpty())
+ assertEquals(extension1, store.state.extensions.values.first())
+
+ val mockedBrowserAction = mock<WebExtensionBrowserAction>()
+ store.dispatch(WebExtensionAction.UpdateBrowserAction(extension1.id, mockedBrowserAction)).joinBlocking()
+ assertEquals(mockedBrowserAction, store.state.extensions.values.first().browserAction)
+
+ store.dispatch(WebExtensionAction.UpdateTabBrowserAction(tab1.id, extension1.id, mockedBrowserAction))
+ .joinBlocking()
+ val extensionsTab1 = store.state.tabs.first().extensionState
+ assertEquals(mockedBrowserAction, extensionsTab1.values.first().browserAction)
+
+ store.dispatch(WebExtensionAction.UpdateTabBrowserAction(tab2.id, extension2.id, mockedBrowserAction))
+ .joinBlocking()
+ val extensionsTab2 = store.state.tabs.last().extensionState
+ assertEquals(mockedBrowserAction, extensionsTab2.values.last().browserAction)
+
+ store.dispatch(WebExtensionAction.UninstallWebExtensionAction(extension1.id)).joinBlocking()
+ assertTrue(store.state.extensions.isEmpty())
+ assertTrue(store.state.tabs.first().extensionState.isEmpty())
+ assertFalse(store.state.tabs.last().extensionState.isEmpty())
+ assertEquals(mockedBrowserAction, extensionsTab2.values.last().browserAction)
+ }
+
+ @Test
+ fun `UninstallAllWebExtensions - Removes all state of all extensions`() {
+ val tab1 = createTab("url")
+ val tab2 = createTab("url")
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab1, tab2),
+ ),
+ )
+ assertTrue(store.state.extensions.isEmpty())
+
+ val extension1 = WebExtensionState("id1", "url")
+ val extension2 = WebExtensionState("i2", "url")
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension1)).joinBlocking()
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension2)).joinBlocking()
+ assertEquals(2, store.state.extensions.size)
+
+ val mockedBrowserAction = mock<WebExtensionBrowserAction>()
+ store.dispatch(WebExtensionAction.UpdateBrowserAction(extension1.id, mockedBrowserAction)).joinBlocking()
+ assertEquals(mockedBrowserAction, store.state.extensions["id1"]?.browserAction)
+ store.dispatch(WebExtensionAction.UpdateTabBrowserAction(tab1.id, extension1.id, mockedBrowserAction)).joinBlocking()
+ store.dispatch(WebExtensionAction.UpdateTabBrowserAction(tab2.id, extension2.id, mockedBrowserAction)).joinBlocking()
+
+ store.dispatch(WebExtensionAction.UninstallAllWebExtensionsAction).joinBlocking()
+ assertTrue(store.state.extensions.isEmpty())
+ assertTrue(store.state.tabs.first().extensionState.isEmpty())
+ assertTrue(store.state.tabs.last().extensionState.isEmpty())
+ }
+
+ @Test
+ fun `UpdateBrowserAction - Updates a global browser action of an existing WebExtensionState on the BrowserState`() {
+ val store = BrowserStore()
+ val mockedBrowserAction = mock<WebExtensionBrowserAction>()
+ val mockedBrowserAction2 = mock<WebExtensionBrowserAction>()
+
+ assertTrue(store.state.extensions.isEmpty())
+ store.dispatch(WebExtensionAction.UpdateBrowserAction("id", mockedBrowserAction)).joinBlocking()
+ assertEquals(mockedBrowserAction, store.state.extensions.values.first().browserAction)
+
+ val extension = WebExtensionState("id", "url")
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension)).joinBlocking()
+ assertFalse(store.state.extensions.isEmpty())
+ assertEquals(mockedBrowserAction, store.state.extensions.values.first().browserAction)
+
+ store.dispatch(WebExtensionAction.UpdateBrowserAction("id", mockedBrowserAction2)).joinBlocking()
+ assertEquals(mockedBrowserAction2, store.state.extensions.values.first().browserAction)
+ }
+
+ @Test
+ fun `UpdateTabBrowserAction - Updates the browser action of an existing WebExtensionState on a given tab`() {
+ val tab = createTab("url")
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+ val mockedBrowserAction = mock<WebExtensionBrowserAction>()
+
+ assertTrue(tab.extensionState.isEmpty())
+
+ val extension = WebExtensionState("id", "url")
+
+ store.dispatch(
+ WebExtensionAction.UpdateTabBrowserAction(
+ tab.id,
+ extension.id,
+ mockedBrowserAction,
+ ),
+ ).joinBlocking()
+
+ val extensions = store.state.tabs.first().extensionState
+
+ assertEquals(mockedBrowserAction, extensions.values.first().browserAction)
+ }
+
+ @Test
+ fun `UpdateTabBrowserAction - Updates an existing browser action`() {
+ val mockedBrowserAction1 = mock<WebExtensionBrowserAction>()
+ val mockedBrowserAction2 = mock<WebExtensionBrowserAction>()
+
+ val tab = createTab(
+ "url",
+ extensions = mapOf(
+ "extensionId" to WebExtensionState(
+ "extensionId",
+ "url",
+ "name",
+ true,
+ browserAction = mockedBrowserAction1,
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ WebExtensionAction.UpdateTabBrowserAction(
+ tab.id,
+ "extensionId",
+ mockedBrowserAction2,
+ ),
+ ).joinBlocking()
+
+ val extensions = store.state.tabs.first().extensionState
+
+ assertEquals(mockedBrowserAction2, extensions.values.first().browserAction)
+ }
+
+ @Test
+ fun `UpdatePageAction - Updates a global page action of an existing WebExtensionState on the BrowserState`() {
+ val store = BrowserStore()
+ val mockedPageAction = mock<WebExtensionPageAction>()
+ val mockedPageAction2 = mock<WebExtensionPageAction>()
+
+ assertTrue(store.state.extensions.isEmpty())
+ store.dispatch(WebExtensionAction.UpdatePageAction("id", mockedPageAction)).joinBlocking()
+ assertEquals(mockedPageAction, store.state.extensions.values.first().pageAction)
+
+ val extension = WebExtensionState("id", "url")
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension)).joinBlocking()
+ assertFalse(store.state.extensions.isEmpty())
+ assertEquals(mockedPageAction, store.state.extensions.values.first().pageAction)
+
+ store.dispatch(WebExtensionAction.UpdatePageAction("id", mockedPageAction2)).joinBlocking()
+ assertEquals(mockedPageAction2, store.state.extensions.values.first().pageAction)
+ }
+
+ @Test
+ fun `UpdateTabPageAction - Updates the page action of an existing WebExtensionState on a given tab`() {
+ val tab = createTab("url")
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+ val mockedPageAction = mock<WebExtensionPageAction>()
+
+ assertTrue(tab.extensionState.isEmpty())
+
+ val extension = WebExtensionState("id", "url")
+
+ store.dispatch(
+ WebExtensionAction.UpdateTabPageAction(
+ tab.id,
+ extension.id,
+ mockedPageAction,
+ ),
+ ).joinBlocking()
+
+ val extensions = store.state.tabs.first().extensionState
+
+ assertEquals(mockedPageAction, extensions.values.first().pageAction)
+ }
+
+ @Test
+ fun `UpdateTabPageAction - Updates an existing page action`() {
+ val mockedPageAction1 = mock<WebExtensionPageAction>()
+ val mockedPageAction2 = mock<WebExtensionPageAction>()
+
+ val tab = createTab(
+ "url",
+ extensions = mapOf(
+ "extensionId" to WebExtensionState(
+ "extensionId",
+ "url",
+ "name",
+ true,
+ pageAction = mockedPageAction1,
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ WebExtensionAction.UpdateTabPageAction(
+ tab.id,
+ "extensionId",
+ mockedPageAction2,
+ ),
+ ).joinBlocking()
+
+ val extensions = store.state.tabs.first().extensionState
+
+ assertEquals(mockedPageAction2, extensions.values.first().pageAction)
+ }
+
+ @Test
+ fun `UpdatePopupSessionAction - Adds popup session to the web extension state`() {
+ val store = BrowserStore()
+
+ val extension = WebExtensionState("id", "url")
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension)).joinBlocking()
+
+ assertEquals(extension, store.state.extensions[extension.id])
+ assertNull(store.state.extensions[extension.id]?.popupSessionId)
+ assertNull(store.state.extensions[extension.id]?.popupSession)
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(WebExtensionAction.UpdatePopupSessionAction(extension.id, popupSession = engineSession)).joinBlocking()
+ assertEquals(engineSession, store.state.extensions[extension.id]?.popupSession)
+
+ store.dispatch(WebExtensionAction.UpdatePopupSessionAction(extension.id, popupSession = null)).joinBlocking()
+ assertNull(store.state.extensions[extension.id]?.popupSession)
+
+ store.dispatch(WebExtensionAction.UpdatePopupSessionAction(extension.id, "popupId")).joinBlocking()
+ assertEquals("popupId", store.state.extensions[extension.id]?.popupSessionId)
+
+ store.dispatch(WebExtensionAction.UpdatePopupSessionAction(extension.id, null)).joinBlocking()
+ assertNull(store.state.extensions[extension.id]?.popupSessionId)
+ }
+
+ @Test
+ fun `UpdateWebExtensionEnabledAction - Updates enabled state of an existing web extension`() {
+ val store = BrowserStore()
+ val extension = WebExtensionState("id", "url")
+
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension)).joinBlocking()
+ assertTrue(store.state.extensions[extension.id]?.enabled!!)
+
+ store.dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(extension.id, false)).joinBlocking()
+ assertFalse(store.state.extensions[extension.id]?.enabled!!)
+
+ store.dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(extension.id, true)).joinBlocking()
+ assertTrue(store.state.extensions[extension.id]?.enabled!!)
+ }
+
+ @Test
+ fun `UpdateWebExtension - Update an existing extension`() {
+ val existingExtension = WebExtensionState("id", "url")
+ val updatedExtension = WebExtensionState("id", "url2")
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ extensions = mapOf("id" to existingExtension),
+ ),
+ )
+
+ store.dispatch(WebExtensionAction.UpdateWebExtensionAction(updatedExtension)).joinBlocking()
+ assertEquals(updatedExtension, store.state.extensions.values.first())
+ assertSame(updatedExtension, store.state.extensions.values.first())
+ }
+
+ @Test
+ fun `UpdateWebExtensionAllowedInPrivateBrowsingAction - Updates allowedInPrivateBrowsing state of an existing web extension`() {
+ val store = BrowserStore()
+ val extension = WebExtensionState("id", "url", allowedInPrivateBrowsing = false)
+
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(extension)).joinBlocking()
+ assertFalse(store.state.extensions[extension.id]?.allowedInPrivateBrowsing!!)
+
+ store.dispatch(WebExtensionAction.UpdateWebExtensionAllowedInPrivateBrowsingAction(extension.id, true)).joinBlocking()
+ assertTrue(store.state.extensions[extension.id]?.allowedInPrivateBrowsing!!)
+
+ store.dispatch(WebExtensionAction.UpdateWebExtensionAllowedInPrivateBrowsingAction(extension.id, false)).joinBlocking()
+ assertFalse(store.state.extensions[extension.id]?.allowedInPrivateBrowsing!!)
+ }
+
+ @Test
+ fun `UpdateWebExtensionTabAction - Marks tab active for web extensions`() {
+ val tab = createTab(url = "https://mozilla.org")
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ assertNull(store.state.activeWebExtensionTabId)
+
+ store.dispatch(WebExtensionAction.UpdateActiveWebExtensionTabAction(tab.id)).joinBlocking()
+ assertEquals(tab.id, store.state.activeWebExtensionTabId)
+
+ store.dispatch(WebExtensionAction.UpdateActiveWebExtensionTabAction(null)).joinBlocking()
+ assertNull(store.state.activeWebExtensionTabId)
+ }
+
+ @Test
+ fun `WHEN UpdatePromptRequestWebExtensionAction is dispatched THEN a WebExtensionPromptRequest is added to the store`() {
+ val store = BrowserStore()
+
+ assertNull(store.state.webExtensionPromptRequest)
+
+ val promptRequest = WebExtensionPromptRequest.AfterInstallation.Permissions.Required(mock(), mock())
+
+ store.dispatch(WebExtensionAction.UpdatePromptRequestWebExtensionAction(promptRequest))
+ .joinBlocking()
+
+ assertEquals(promptRequest, store.state.webExtensionPromptRequest)
+ }
+
+ @Test
+ fun `WHEN ConsumePromptRequestWebExtensionAction is dispatched THEN the actual WebExtensionPromptRequest is removed from the store`() {
+ val promptRequest = WebExtensionPromptRequest.AfterInstallation.Permissions.Required(mock(), mock())
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ webExtensionPromptRequest = promptRequest,
+ ),
+ )
+
+ assertNotNull(store.state.webExtensionPromptRequest)
+
+ store.dispatch(WebExtensionAction.ConsumePromptRequestWebExtensionAction)
+ .joinBlocking()
+
+ assertNull(store.state.webExtensionPromptRequest)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt
new file mode 100644
index 0000000000..8c5503df8e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt
@@ -0,0 +1,76 @@
+/* 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.browser.state.engine
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.engine.middleware.TrimMemoryMiddleware
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+
+class EngineMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `Dispatching CreateEngineSessionAction multiple times should only create one engine session`() {
+ val session: EngineSession = mock()
+ val engine: Engine = mock()
+ Mockito.doReturn(session).`when`(engine).createSession(false, null)
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ ),
+ middleware = EngineMiddleware.create(engine, scope),
+ )
+
+ store.dispatch(
+ EngineAction.CreateEngineSessionAction("mozilla"),
+ )
+
+ store.dispatch(
+ EngineAction.CreateEngineSessionAction("mozilla"),
+ )
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine, Mockito.times(1)).createSession(false, null)
+ }
+
+ @Test
+ fun `TrimMemoryMiddleware will be added by default`() {
+ val list = EngineMiddleware.create(
+ engine = mock(),
+ )
+
+ assertTrue(list.any { it is TrimMemoryMiddleware })
+ }
+
+ @Test
+ fun `TrimMemoryMiddleware will not be added if trimMemoryAutomatically is set to false`() {
+ val list = EngineMiddleware.create(
+ engine = mock(),
+ trimMemoryAutomatically = false,
+ )
+
+ assertTrue(list.none { it is TrimMemoryMiddleware })
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt
new file mode 100644
index 0000000000..2967d43fbf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt
@@ -0,0 +1,1832 @@
+/* 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.browser.state.engine
+
+import android.content.Intent
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CookieBannerAction
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.action.ReaderAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.TrackingProtectionAction
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.AppIntentState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.LoadRequestState
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.content.FindResultState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus.HANDLED
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.history.HistoryItem
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.shopping.ProductAnalysis
+import mozilla.components.concept.engine.shopping.ProductAnalysisStatus
+import mozilla.components.concept.engine.shopping.ProductRecommendation
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.translate.TranslationOptions
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+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.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class EngineObserverTest {
+ // TO DO: add tests for product URL after a test endpoint is implemented in desktop (Bug 1846341)
+ @Test
+ fun engineSessionObserver() {
+ val engineSession = object : EngineSession() {
+ override val settings: Settings = mock()
+ override fun goBack(userInteraction: Boolean) {}
+ override fun goForward(userInteraction: Boolean) {}
+ override fun goToHistoryIndex(index: Int) {}
+ override fun reload(flags: LoadUrlFlags) {}
+ override fun stopLoading() {}
+ override fun restoreState(state: EngineSessionState): Boolean { return false }
+ override fun updateTrackingProtection(policy: TrackingProtectionPolicy) {}
+ override fun toggleDesktopMode(enable: Boolean, reload: Boolean) {
+ notifyObservers { onDesktopModeChange(enable) }
+ }
+ override fun hasCookieBannerRuleForSession(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun checkForPdfViewer(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun requestProductAnalysis(
+ url: String,
+ onResult: (ProductAnalysis) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun requestProductRecommendations(
+ url: String,
+ onResult: (List<ProductRecommendation>) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun reanalyzeProduct(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun requestAnalysisStatus(
+ url: String,
+ onResult: (ProductAnalysisStatus) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendClickAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendImpressionAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendPlacementAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun reportBackInStock(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun requestTranslate(
+ fromLanguage: String,
+ toLanguage: String,
+ options: TranslationOptions?,
+ ) {}
+ override fun requestTranslationRestore() {}
+ override fun getNeverTranslateSiteSetting(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun setNeverTranslateSiteSetting(
+ setting: Boolean,
+ onResult: () -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun findAll(text: String) {}
+ override fun findNext(forward: Boolean) {}
+ override fun clearFindMatches() {}
+ override fun exitFullScreenMode() {}
+ override fun purgeHistory() {}
+ override fun loadData(data: String, mimeType: String, encoding: String) {
+ notifyObservers { onLocationChange(data, false) }
+ notifyObservers { onProgress(100) }
+ notifyObservers { onLoadingStateChange(true) }
+ notifyObservers { onNavigationStateChange(true, true) }
+ }
+ override fun requestPdfToDownload() = Unit
+ override fun requestPrintContent() = Unit
+ override fun loadUrl(
+ url: String,
+ parent: EngineSession?,
+ flags: LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {
+ notifyObservers { onLocationChange(url, false) }
+ notifyObservers { onProgress(100) }
+ notifyObservers { onLoadingStateChange(true) }
+ notifyObservers { onNavigationStateChange(true, true) }
+ }
+ }
+
+ val store = BrowserStore()
+ store.dispatch(TabListAction.AddTabAction(createTab("https://www.mozilla.org", id = "mozilla")))
+
+ engineSession.register(EngineObserver("mozilla", store))
+ engineSession.loadUrl("http://mozilla.org")
+ engineSession.toggleDesktopMode(true)
+
+ store.waitUntilIdle()
+
+ assertEquals("http://mozilla.org", store.state.selectedTab?.content?.url)
+ assertEquals(100, store.state.selectedTab?.content?.progress)
+ assertEquals(true, store.state.selectedTab?.content?.loading)
+
+ val tab = store.state.findTab("mozilla")
+ assertNotNull(tab!!)
+ assertTrue(tab.content.canGoForward)
+ assertTrue(tab.content.canGoBack)
+ }
+
+ @Test
+ fun engineSessionObserverWithSecurityChanges() {
+ val engineSession = object : EngineSession() {
+ override val settings: Settings = mock()
+ override fun goBack(userInteraction: Boolean) {}
+ override fun goForward(userInteraction: Boolean) {}
+ override fun goToHistoryIndex(index: Int) {}
+ override fun stopLoading() {}
+ override fun reload(flags: LoadUrlFlags) {}
+ override fun restoreState(state: EngineSessionState): Boolean { return false }
+ override fun updateTrackingProtection(policy: TrackingProtectionPolicy) {}
+ override fun toggleDesktopMode(enable: Boolean, reload: Boolean) {}
+ override fun hasCookieBannerRuleForSession(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun checkForPdfViewer(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun requestProductAnalysis(
+ url: String,
+ onResult: (ProductAnalysis) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun requestProductRecommendations(
+ url: String,
+ onResult: (List<ProductRecommendation>) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun reanalyzeProduct(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun requestAnalysisStatus(
+ url: String,
+ onResult: (ProductAnalysisStatus) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendClickAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendImpressionAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendPlacementAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun reportBackInStock(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun requestTranslate(
+ fromLanguage: String,
+ toLanguage: String,
+ options: TranslationOptions?,
+ ) {}
+ override fun requestTranslationRestore() {}
+ override fun getNeverTranslateSiteSetting(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun setNeverTranslateSiteSetting(
+ setting: Boolean,
+ onResult: () -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun findAll(text: String) {}
+ override fun findNext(forward: Boolean) {}
+ override fun clearFindMatches() {}
+ override fun exitFullScreenMode() {}
+ override fun purgeHistory() {}
+ override fun loadData(data: String, mimeType: String, encoding: String) {}
+ override fun requestPdfToDownload() = Unit
+ override fun requestPrintContent() = Unit
+ override fun loadUrl(
+ url: String,
+ parent: EngineSession?,
+ flags: LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {
+ if (url.startsWith("https://")) {
+ notifyObservers { onSecurityChange(true, "host", "issuer") }
+ } else {
+ notifyObservers { onSecurityChange(false) }
+ }
+ }
+ }
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ ),
+ )
+
+ engineSession.register(EngineObserver("mozilla", store))
+
+ engineSession.loadUrl("http://mozilla.org")
+ store.waitUntilIdle()
+ assertEquals(SecurityInfoState(secure = false), store.state.tabs[0].content.securityInfo)
+
+ engineSession.loadUrl("https://mozilla.org")
+ store.waitUntilIdle()
+ assertEquals(SecurityInfoState(secure = true, "host", "issuer"), store.state.tabs[0].content.securityInfo)
+ }
+
+ @Test
+ fun engineSessionObserverWithTrackingProtection() {
+ val engineSession = object : EngineSession() {
+ override val settings: Settings = mock()
+ override fun goBack(userInteraction: Boolean) {}
+ override fun goForward(userInteraction: Boolean) {}
+ override fun goToHistoryIndex(index: Int) {}
+ override fun stopLoading() {}
+ override fun reload(flags: LoadUrlFlags) {}
+ override fun restoreState(state: EngineSessionState): Boolean { return false }
+ override fun updateTrackingProtection(policy: TrackingProtectionPolicy) {}
+ override fun toggleDesktopMode(enable: Boolean, reload: Boolean) {}
+ override fun hasCookieBannerRuleForSession(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun checkForPdfViewer(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun requestProductRecommendations(
+ url: String,
+ onResult: (List<ProductRecommendation>) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun requestProductAnalysis(
+ url: String,
+ onResult: (ProductAnalysis) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun reanalyzeProduct(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun requestAnalysisStatus(
+ url: String,
+ onResult: (ProductAnalysisStatus) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendClickAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendImpressionAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun sendPlacementAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun reportBackInStock(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun requestTranslate(
+ fromLanguage: String,
+ toLanguage: String,
+ options: TranslationOptions?,
+ ) {}
+ override fun requestTranslationRestore() {}
+ override fun getNeverTranslateSiteSetting(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun setNeverTranslateSiteSetting(
+ setting: Boolean,
+ onResult: () -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+ override fun loadUrl(
+ url: String,
+ parent: EngineSession?,
+ flags: LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {}
+ override fun loadData(data: String, mimeType: String, encoding: String) {}
+ override fun requestPdfToDownload() = Unit
+ override fun requestPrintContent() = Unit
+ override fun findAll(text: String) {}
+ override fun findNext(forward: Boolean) {}
+ override fun clearFindMatches() {}
+ override fun exitFullScreenMode() {}
+ override fun purgeHistory() {}
+ }
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ ),
+ )
+ val observer = EngineObserver("mozilla", store)
+ engineSession.register(observer)
+
+ val tracker1 = Tracker("tracker1", emptyList())
+ val tracker2 = Tracker("tracker2", emptyList())
+
+ observer.onTrackerBlocked(tracker1)
+ store.waitUntilIdle()
+
+ assertEquals(listOf(tracker1), store.state.tabs[0].trackingProtection.blockedTrackers)
+
+ observer.onTrackerBlocked(tracker2)
+ store.waitUntilIdle()
+
+ assertEquals(listOf(tracker1, tracker2), store.state.tabs[0].trackingProtection.blockedTrackers)
+ }
+
+ @Test
+ fun engineSessionObserverExcludedOnTrackingProtection() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onExcludedOnTrackingProtectionChange(true)
+
+ verify(store).dispatch(
+ TrackingProtectionAction.ToggleExclusionListAction(
+ "mozilla",
+ true,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN onCookieBannerChange is called THEN dispatch an CookieBannerAction UpdateStatusAction`() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onCookieBannerChange(HANDLED)
+
+ verify(store).dispatch(
+ CookieBannerAction.UpdateStatusAction(
+ "mozilla",
+ HANDLED,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN onTranslateComplete is called THEN dispatch a TranslationsAction TranslateSuccessAction`() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onTranslateComplete(operation = TranslationOperation.TRANSLATE)
+
+ verify(store).dispatch(
+ TranslationsAction.TranslateSuccessAction("mozilla", operation = TranslationOperation.TRANSLATE),
+ )
+ }
+
+ @Test
+ fun `WHEN onTranslateException is called THEN dispatch a TranslationsAction TranslateExceptionAction`() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("mozilla", store)
+ val exception = TranslationError.UnknownError(Exception())
+
+ observer.onTranslateException(operation = TranslationOperation.TRANSLATE, exception)
+
+ verify(store).dispatch(
+ TranslationsAction.TranslateExceptionAction("mozilla", operation = TranslationOperation.TRANSLATE, exception),
+ )
+ }
+
+ @Test
+ fun engineObserverClearsWebsiteTitleIfNewPageStartsLoading() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ title = "Hello World",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ observer.onTitleChange("Mozilla")
+ store.waitUntilIdle()
+
+ assertEquals("Mozilla", store.state.tabs[0].content.title)
+
+ observer.onLocationChange("https://getpocket.com", false)
+ store.waitUntilIdle()
+
+ assertEquals("", store.state.tabs[0].content.title)
+ }
+
+ @Test
+ fun `EngineObserver does not clear title if the URL did not change`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ title = "Hello World",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onTitleChange("Mozilla")
+ store.waitUntilIdle()
+
+ assertEquals("Mozilla", store.state.tabs[0].content.title)
+
+ observer.onLocationChange("https://www.mozilla.org", false)
+ store.waitUntilIdle()
+
+ assertEquals("Mozilla", store.state.tabs[0].content.title)
+ }
+
+ @Test
+ fun `EngineObserver does not clear title if the URL changes hash`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ title = "Hello World",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onTitleChange("Mozilla")
+ store.waitUntilIdle()
+
+ assertEquals("Mozilla", store.state.tabs[0].content.title)
+
+ observer.onLocationChange("https://www.mozilla.org/#something", false)
+ store.waitUntilIdle()
+
+ assertEquals("Mozilla", store.state.tabs[0].content.title)
+ }
+
+ @Test
+ fun `EngineObserver clears previewImageUrl if new page starts loading`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ title = "Hello World",
+ ),
+ ),
+ ),
+ )
+ val previewImageUrl = "https://test.com/og-image-url"
+
+ val observer = EngineObserver("mozilla", store)
+ observer.onPreviewImageChange(previewImageUrl)
+ store.waitUntilIdle()
+
+ assertEquals(previewImageUrl, store.state.tabs[0].content.previewImageUrl)
+
+ observer.onLocationChange("https://getpocket.com", false)
+ store.waitUntilIdle()
+
+ assertNull(store.state.tabs[0].content.previewImageUrl)
+ }
+
+ @Test
+ fun `EngineObserver does not clear previewImageUrl if the URL did not change`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ title = "Hello World",
+ ),
+ ),
+ ),
+ )
+ val previewImageUrl = "https://test.com/og-image-url"
+
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onPreviewImageChange(previewImageUrl)
+ store.waitUntilIdle()
+
+ assertEquals(previewImageUrl, store.state.tabs[0].content.previewImageUrl)
+
+ observer.onLocationChange("https://www.mozilla.org", false)
+ store.waitUntilIdle()
+
+ assertEquals(previewImageUrl, store.state.tabs[0].content.previewImageUrl)
+
+ observer.onLocationChange("https://www.mozilla.org/#something", false)
+ store.waitUntilIdle()
+
+ assertEquals(previewImageUrl, store.state.tabs[0].content.previewImageUrl)
+ }
+
+ @Test
+ fun engineObserverClearsBlockedTrackersIfNewPageStartsLoading() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ val tracker1 = Tracker("tracker1")
+ val tracker2 = Tracker("tracker2")
+
+ observer.onTrackerBlocked(tracker1)
+ observer.onTrackerBlocked(tracker2)
+ store.waitUntilIdle()
+
+ assertEquals(listOf(tracker1, tracker2), store.state.tabs[0].trackingProtection.blockedTrackers)
+
+ observer.onLoadingStateChange(true)
+ store.waitUntilIdle()
+
+ assertEquals(emptyList<String>(), store.state.tabs[0].trackingProtection.blockedTrackers)
+ }
+
+ @Test
+ fun engineObserverClearsLoadedTrackersIfNewPageStartsLoading() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ val tracker1 = Tracker("tracker1")
+ val tracker2 = Tracker("tracker2")
+
+ observer.onTrackerLoaded(tracker1)
+ observer.onTrackerLoaded(tracker2)
+ store.waitUntilIdle()
+
+ assertEquals(listOf(tracker1, tracker2), store.state.tabs[0].trackingProtection.loadedTrackers)
+
+ observer.onLoadingStateChange(true)
+ store.waitUntilIdle()
+
+ assertEquals(emptyList<String>(), store.state.tabs[0].trackingProtection.loadedTrackers)
+ }
+
+ @Test
+ fun engineObserverClearsWebAppManifestIfNewPageStartsLoading() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org")
+
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onWebAppManifestLoaded(manifest)
+ store.waitUntilIdle()
+
+ assertEquals(manifest, store.state.tabs[0].content.webAppManifest)
+
+ observer.onLocationChange("https://getpocket.com", false)
+ store.waitUntilIdle()
+
+ assertNull(store.state.tabs[0].content.webAppManifest)
+ }
+
+ @Test
+ fun engineObserverClearsContentPermissionRequestIfNewPageStartsLoading() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ val request: PermissionRequest = mock()
+
+ store.dispatch(ContentAction.UpdatePermissionsRequest("mozilla", request))
+ store.waitUntilIdle()
+
+ assertEquals(listOf(request), store.state.tabs[0].content.permissionRequestsList)
+
+ observer.onLocationChange("https://getpocket.com", false)
+ store.waitUntilIdle()
+
+ assertEquals(emptyList<PermissionRequest>(), store.state.tabs[0].content.permissionRequestsList)
+ }
+
+ @Test
+ fun engineObserverDoesNotClearContentPermissionRequestIfSamePageStartsLoading() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ val request: PermissionRequest = mock()
+
+ store.dispatch(ContentAction.UpdatePermissionsRequest("mozilla", request))
+ store.waitUntilIdle()
+
+ assertEquals(listOf(request), store.state.tabs[0].content.permissionRequestsList)
+
+ observer.onLocationChange("https://www.mozilla.org/hello.html", false)
+ store.waitUntilIdle()
+
+ assertEquals(listOf(request), store.state.tabs[0].content.permissionRequestsList)
+ }
+
+ @Test
+ fun engineObserverDoesNotClearWebAppManifestIfNewPageInStartUrlScope() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://www.mozilla.org")
+
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onWebAppManifestLoaded(manifest)
+ store.waitUntilIdle()
+
+ assertEquals(manifest, store.state.tabs[0].content.webAppManifest)
+
+ observer.onLocationChange("https://www.mozilla.org/hello.html", false)
+ store.waitUntilIdle()
+
+ assertEquals(manifest, store.state.tabs[0].content.webAppManifest)
+ }
+
+ @Test
+ fun engineObserverDoesNotClearWebAppManifestIfNewPageInScope() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val manifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://www.mozilla.org",
+ scope = "https://www.mozilla.org/hello/",
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onWebAppManifestLoaded(manifest)
+ store.waitUntilIdle()
+
+ assertEquals(manifest, store.state.tabs[0].content.webAppManifest)
+
+ observer.onLocationChange("https://www.mozilla.org/hello/page2.html", false)
+ store.waitUntilIdle()
+
+ assertEquals(manifest, store.state.tabs[0].content.webAppManifest)
+
+ observer.onLocationChange("https://www.mozilla.org/hello.html", false)
+ store.waitUntilIdle()
+ assertNull(store.state.tabs[0].content.webAppManifest)
+ }
+
+ @Test
+ fun engineObserverPassingHitResult() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ val hitResult = HitResult.UNKNOWN("data://foobar")
+
+ observer.onLongPress(hitResult)
+ store.waitUntilIdle()
+
+ assertEquals(hitResult, store.state.tabs[0].content.hitResult)
+ }
+
+ @Test
+ fun engineObserverClearsFindResults() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+ val observer = EngineObserver("tab", store)
+
+ observer.onFindResult(0, 1, false)
+ store.waitUntilIdle()
+ middleware.assertFirstAction(ContentAction.AddFindResultAction::class) { action ->
+ assertEquals("tab", action.sessionId)
+ assertEquals(FindResultState(0, 1, false), action.findResult)
+ }
+
+ observer.onFind("mozilla")
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.ClearFindResultsAction::class) { action ->
+ assertEquals("tab", action.sessionId)
+ }
+ }
+
+ @Test
+ fun engineObserverClearsFindResultIfNewPageStartsLoading() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onFindResult(0, 1, false)
+ store.waitUntilIdle()
+ middleware.assertFirstAction(ContentAction.AddFindResultAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertEquals(FindResultState(0, 1, false), action.findResult)
+ }
+
+ observer.onFindResult(1, 2, true)
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.AddFindResultAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertEquals(FindResultState(1, 2, true), action.findResult)
+ }
+
+ observer.onLoadingStateChange(true)
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.ClearFindResultsAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ }
+ }
+
+ @Test
+ fun engineObserverClearsRefreshCanceledIfNewPageStartsLoading() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onRepostPromptCancelled()
+ store.waitUntilIdle()
+ middleware.assertFirstAction(ContentAction.UpdateRefreshCanceledStateAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertTrue(action.refreshCanceled)
+ }
+
+ observer.onLoadingStateChange(true)
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.UpdateRefreshCanceledStateAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertFalse(action.refreshCanceled)
+ }
+ }
+
+ @Test
+ fun engineObserverHandlesOnRepostPromptCancelled() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onRepostPromptCancelled()
+ verify(store).dispatch(ContentAction.UpdateRefreshCanceledStateAction("tab-id", true))
+ }
+
+ @Test
+ fun engineObserverHandlesOnBeforeUnloadDenied() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onBeforeUnloadPromptDenied()
+ verify(store).dispatch(ContentAction.UpdateRefreshCanceledStateAction("tab-id", true))
+ }
+
+ @Test
+ fun engineObserverNotifiesFullscreenMode() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onFullScreenChange(true)
+ store.waitUntilIdle()
+ middleware.assertFirstAction(ContentAction.FullScreenChangedAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertTrue(action.fullScreenEnabled)
+ }
+
+ observer.onFullScreenChange(false)
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.FullScreenChangedAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertFalse(action.fullScreenEnabled)
+ }
+ }
+
+ @Test
+ fun engineObserverNotifiesDesktopMode() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onDesktopModeChange(true)
+ store.waitUntilIdle()
+ middleware.assertFirstAction(ContentAction.UpdateDesktopModeAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertTrue(action.enabled)
+ }
+
+ observer.onDesktopModeChange(false)
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.UpdateDesktopModeAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertFalse(action.enabled)
+ }
+ }
+
+ @Test
+ fun engineObserverNotifiesMetaViewportFitChange() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT)
+ store.waitUntilIdle()
+ middleware.assertFirstAction(ContentAction.ViewportFitChangedAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertEquals(
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,
+ action.layoutInDisplayCutoutMode,
+ )
+ }
+
+ observer.onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.ViewportFitChangedAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertEquals(
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,
+ action.layoutInDisplayCutoutMode,
+ )
+ }
+
+ observer.onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER)
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.ViewportFitChangedAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertEquals(
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER,
+ action.layoutInDisplayCutoutMode,
+ )
+ }
+
+ observer.onMetaViewportFitChanged(123)
+ store.waitUntilIdle()
+ middleware.assertLastAction(ContentAction.ViewportFitChangedAction::class) { action ->
+ assertEquals("tab-id", action.sessionId)
+ assertEquals(123, action.layoutInDisplayCutoutMode)
+ }
+ }
+
+ @Test
+ fun engineObserverNotifiesWebAppManifest() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val manifest = WebAppManifest(
+ name = "Minimal",
+ startUrl = "/",
+ )
+
+ observer.onWebAppManifestLoaded(manifest)
+ store.waitUntilIdle()
+
+ assertEquals(manifest, store.state.tabs[0].content.webAppManifest)
+ }
+
+ @Test
+ fun engineSessionObserverWithContentPermissionRequests() = runTest {
+ val permissionRequest: PermissionRequest = mock()
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("tab-id", store)
+ val action = ContentAction.UpdatePermissionsRequest(
+ "tab-id",
+ permissionRequest,
+ )
+ doReturn(Job()).`when`(store).dispatch(action)
+
+ observer.onContentPermissionRequest(permissionRequest)
+ verify(store).dispatch(action)
+ }
+
+ @Test
+ fun engineSessionObserverWithAppPermissionRequests() = runTest {
+ val permissionRequest: PermissionRequest = mock()
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("tab-id", store)
+ val action = ContentAction.UpdateAppPermissionsRequest(
+ "tab-id",
+ permissionRequest,
+ )
+
+ observer.onAppPermissionRequest(permissionRequest)
+ verify(store).dispatch(action)
+ }
+
+ @Test
+ fun engineObserverHandlesPromptRequest() {
+ val promptRequest: PromptRequest = mock<PromptRequest.SingleChoice>()
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onPromptRequest(promptRequest)
+ verify(store).dispatch(
+ ContentAction.UpdatePromptRequestAction(
+ "tab-id",
+ promptRequest,
+ ),
+ )
+ }
+
+ @Test
+ fun engineObserverHandlesOnPromptUpdate() {
+ val promptRequest: PromptRequest = mock<PromptRequest.SingleChoice>()
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("tab-id", store)
+ val previousPromptUID = "prompt-uid"
+
+ observer.onPromptUpdate(previousPromptUID, promptRequest)
+ verify(store).dispatch(
+ ContentAction.ReplacePromptRequestAction(
+ "tab-id",
+ previousPromptUID,
+ promptRequest,
+ ),
+ )
+ }
+
+ @Test
+ fun engineObserverHandlesWindowRequest() {
+ val windowRequest: WindowRequest = mock()
+ val store: BrowserStore = mock()
+ whenever(store.state).thenReturn(mock())
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onWindowRequest(windowRequest)
+ verify(store).dispatch(
+ ContentAction.UpdateWindowRequestAction(
+ "tab-id",
+ windowRequest,
+ ),
+ )
+ }
+
+ @Test
+ fun engineObserverHandlesFirstContentfulPaint() {
+ val store: BrowserStore = mock()
+ whenever(store.state).thenReturn(mock())
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onFirstContentfulPaint()
+ verify(store).dispatch(
+ ContentAction.UpdateFirstContentfulPaintStateAction(
+ "tab-id",
+ true,
+ ),
+ )
+ }
+
+ @Test
+ fun engineObserverHandlesPaintStatusReset() {
+ val store: BrowserStore = mock()
+ whenever(store.state).thenReturn(mock())
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onPaintStatusReset()
+ verify(store).dispatch(
+ ContentAction.UpdateFirstContentfulPaintStateAction(
+ "tab-id",
+ false,
+ ),
+ )
+ }
+
+ @Test
+ fun engineObserverHandlesOnShowDynamicToolbar() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onShowDynamicToolbar()
+ verify(store).dispatch(ContentAction.UpdateExpandedToolbarStateAction("tab-id", true))
+ }
+
+ @Test
+ fun `onMediaActivated will update the store`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val mediaSessionController: MediaSession.Controller = mock()
+
+ assertNull(store.state.tabs[0].mediaSessionState)
+
+ observer.onMediaActivated(mediaSessionController)
+ store.waitUntilIdle()
+
+ val observedMediaSessionState = store.state.tabs[0].mediaSessionState
+ assertNotNull(observedMediaSessionState)
+ assertEquals(mediaSessionController, observedMediaSessionState?.controller)
+ }
+
+ @Test
+ fun `onMediaDeactivated will update the store`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ assertNotNull(store.state.findTab("mozilla")?.mediaSessionState)
+
+ observer.onMediaDeactivated()
+ store.waitUntilIdle()
+
+ val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState
+ assertNull(observedMediaSessionState)
+ }
+
+ @Test
+ fun `onMediaMetadataChanged will update the store`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val mediaSessionController: MediaSession.Controller = mock()
+ val metaData: MediaSession.Metadata = mock()
+
+ observer.onMediaActivated(mediaSessionController)
+ store.waitUntilIdle()
+ observer.onMediaMetadataChanged(metaData)
+ store.waitUntilIdle()
+
+ val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState
+ assertNotNull(observedMediaSessionState)
+ assertEquals(mediaSessionController, observedMediaSessionState?.controller)
+ assertEquals(metaData, observedMediaSessionState?.metadata)
+ }
+
+ @Test
+ fun `onMediaPlaybackStateChanged will update the store`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val mediaSessionController: MediaSession.Controller = mock()
+ val playbackState: MediaSession.PlaybackState = mock()
+
+ observer.onMediaActivated(mediaSessionController)
+ store.waitUntilIdle()
+ observer.onMediaPlaybackStateChanged(playbackState)
+ store.waitUntilIdle()
+
+ val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState
+ assertNotNull(observedMediaSessionState)
+ assertEquals(mediaSessionController, observedMediaSessionState?.controller)
+ assertEquals(playbackState, observedMediaSessionState?.playbackState)
+ }
+
+ @Test
+ fun `onMediaFeatureChanged will update the store`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val mediaSessionController: MediaSession.Controller = mock()
+ val features: MediaSession.Feature = mock()
+
+ observer.onMediaActivated(mediaSessionController)
+ store.waitUntilIdle()
+ observer.onMediaFeatureChanged(features)
+ store.waitUntilIdle()
+
+ val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState
+ assertNotNull(observedMediaSessionState)
+ assertEquals(mediaSessionController, observedMediaSessionState?.controller)
+ assertEquals(features, observedMediaSessionState?.features)
+ }
+
+ @Test
+ fun `onMediaPositionStateChanged will update the store`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val mediaSessionController: MediaSession.Controller = mock()
+ val positionState: MediaSession.PositionState = mock()
+
+ observer.onMediaActivated(mediaSessionController)
+ store.waitUntilIdle()
+ observer.onMediaPositionStateChanged(positionState)
+ store.waitUntilIdle()
+
+ val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState
+ assertNotNull(observedMediaSessionState)
+ assertEquals(mediaSessionController, observedMediaSessionState?.controller)
+ assertEquals(positionState, observedMediaSessionState?.positionState)
+ }
+
+ @Test
+ fun `onMediaMuteChanged will update the store`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val mediaSessionController: MediaSession.Controller = mock()
+
+ observer.onMediaActivated(mediaSessionController)
+ store.waitUntilIdle()
+ observer.onMediaMuteChanged(true)
+ store.waitUntilIdle()
+
+ val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState
+ assertNotNull(observedMediaSessionState)
+ assertEquals(mediaSessionController, observedMediaSessionState?.controller)
+ assertEquals(true, observedMediaSessionState?.muted)
+ }
+
+ @Test
+ fun `onMediaFullscreenChanged will update the store`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val mediaSessionController: MediaSession.Controller = mock()
+ val elementMetadata: MediaSession.ElementMetadata = mock()
+
+ observer.onMediaActivated(mediaSessionController)
+ store.waitUntilIdle()
+ observer.onMediaFullscreenChanged(true, elementMetadata)
+ store.waitUntilIdle()
+
+ val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState
+ assertNotNull(observedMediaSessionState)
+ assertEquals(mediaSessionController, observedMediaSessionState?.controller)
+ assertEquals(true, observedMediaSessionState?.fullscreen)
+ assertEquals(elementMetadata, observedMediaSessionState?.elementMetadata)
+ }
+
+ @Test
+ fun `updates are ignored when media session is deactivated`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+ val elementMetadata: MediaSession.ElementMetadata = mock()
+
+ observer.onMediaFullscreenChanged(true, elementMetadata)
+ store.waitUntilIdle()
+
+ assertNull(store.state.findTab("mozilla")?.mediaSessionState)
+
+ observer.onMediaMuteChanged(true)
+ store.waitUntilIdle()
+ assertNull(store.state.findTab("mozilla")?.mediaSessionState)
+ }
+
+ @Test
+ fun `onExternalResource will update the store`() {
+ val response = mock<Response>()
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "mozilla",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("mozilla", store)
+
+ observer.onExternalResource(
+ url = "mozilla.org/file.txt",
+ fileName = "file.txt",
+ userAgent = "userAgent",
+ contentType = "text/plain",
+ isPrivate = true,
+ contentLength = 100L,
+ response = response,
+ )
+
+ store.waitUntilIdle()
+
+ val tab = store.state.findTab("mozilla")!!
+
+ assertEquals("mozilla.org/file.txt", tab.content.download?.url)
+ assertEquals("file.txt", tab.content.download?.fileName)
+ assertEquals("userAgent", tab.content.download?.userAgent)
+ assertEquals("text/plain", tab.content.download?.contentType)
+ assertEquals(100L, tab.content.download?.contentLength)
+ assertEquals(true, tab.content.download?.private)
+ assertEquals(response, tab.content.download?.response)
+ }
+
+ @Test
+ fun `onExternalResource with negative contentLength`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://www.mozilla.org",
+ id = "test-tab",
+ mediaSessionState = MediaSessionState(
+ controller = mock(),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val observer = EngineObserver("test-tab", store)
+
+ observer.onExternalResource(url = "mozilla.org/file.txt", contentLength = -1)
+
+ store.waitUntilIdle()
+
+ val tab = store.state.findTab("test-tab")!!
+
+ assertNull(tab.content.download?.contentLength)
+ }
+
+ @Test
+ fun `onCrashStateChanged will update session and notify observer`() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("test-id", store)
+
+ observer.onCrash()
+
+ verify(store).dispatch(
+ CrashAction.SessionCrashedAction(
+ "test-id",
+ ),
+ )
+ }
+
+ @Test
+ fun `onLocationChange does not clear search terms`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ val observer = EngineObserver("test-id", store)
+ observer.onLocationChange("https://www.mozilla.org/en-US/", false)
+
+ store.waitUntilIdle()
+
+ middleware.assertNotDispatched(ContentAction.UpdateSearchTermsAction::class)
+ }
+
+ @Test
+ fun `onLoadRequest clears search terms for requests triggered by web content`() {
+ val url = "https://www.mozilla.org"
+
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ val observer = EngineObserver("test-id", store)
+ observer.onLoadRequest(url = url, triggeredByRedirect = false, triggeredByWebContent = true)
+
+ store.waitUntilIdle()
+
+ middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals("", action.searchTerms)
+ assertEquals("test-id", action.sessionId)
+ }
+ }
+
+ @Test
+ @Suppress("DEPRECATION") // Session observable is deprecated
+ fun `onLoadRequest notifies session observers`() {
+ val url = "https://www.mozilla.org"
+ val store: BrowserStore = mock()
+
+ val observer = EngineObserver("test-id", store)
+ observer.onLoadRequest(url = url, triggeredByRedirect = true, triggeredByWebContent = false)
+
+ verify(store)
+ .dispatch(
+ ContentAction.UpdateLoadRequestAction(
+ "test-id",
+ LoadRequestState(url, triggeredByRedirect = true, triggeredByUser = false),
+ ),
+ )
+ }
+
+ @Test
+ fun `onLoadRequest does not clear search terms for requests not triggered by user interacting with web content`() {
+ val url = "https://www.mozilla.org"
+
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ val observer = EngineObserver("test-id", store)
+ observer.onLoadRequest(url = url, triggeredByRedirect = false, triggeredByWebContent = false)
+
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(ContentAction.UpdateSearchTermsAction::class)
+ }
+
+ @Test
+ fun `onLaunchIntentRequest dispatches UpdateAppIntentAction`() {
+ val url = "https://www.mozilla.org"
+
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("test-id", store)
+ val intent: Intent = mock()
+ observer.onLaunchIntentRequest(url = url, appIntent = intent)
+
+ verify(store).dispatch(ContentAction.UpdateAppIntentAction("test-id", AppIntentState(url, intent)))
+ }
+
+ @Test
+ fun `onNavigateBack clears search terms when navigating back`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ val observer = EngineObserver("test-id", store)
+ observer.onNavigateBack()
+ store.waitUntilIdle()
+
+ middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals("", action.searchTerms)
+ assertEquals("test-id", action.sessionId)
+ }
+ }
+
+ @Test
+ fun `WHEN navigating forward THEN search terms are cleared`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ val observer = EngineObserver("test-id", store)
+ observer.onNavigateForward()
+ store.waitUntilIdle()
+
+ middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals("", action.searchTerms)
+ assertEquals("test-id", action.sessionId)
+ }
+ }
+
+ @Test
+ fun `WHEN navigating to history index THEN search terms are cleared`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ val observer = EngineObserver("test-id", store)
+ observer.onGotoHistoryIndex()
+ store.waitUntilIdle()
+
+ middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals("", action.searchTerms)
+ assertEquals("test-id", action.sessionId)
+ }
+ }
+
+ @Test
+ fun `WHEN loading data THEN the search terms are cleared`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ val observer = EngineObserver("test-id", store)
+ observer.onLoadData()
+ store.waitUntilIdle()
+
+ middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals("", action.searchTerms)
+ assertEquals("test-id", action.sessionId)
+ }
+ }
+
+ @Test
+ fun `GIVEN a search is not performed WHEN loading the URL THEN the search terms are cleared`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(ContentAction.UpdateIsSearchAction("mozilla", false))
+ store.waitUntilIdle()
+
+ val observer = EngineObserver("test-id", store)
+ observer.onLoadUrl()
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals("", action.searchTerms)
+ assertEquals("test-id", action.sessionId)
+ }
+ }
+
+ @Test
+ fun `GIVEN a search is performed WHEN loading the URL THEN the search terms are cleared`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-id"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(ContentAction.UpdateIsSearchAction("test-id", true))
+ store.waitUntilIdle()
+
+ val observer = EngineObserver("test-id", store)
+ observer.onLoadUrl()
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(ContentAction.UpdateIsSearchAction::class) { action ->
+ assertEquals(false, action.isSearch)
+ assertEquals("test-id", action.sessionId)
+ }
+ }
+
+ @Test
+ fun `GIVEN a search is performed WHEN the location is changed without user interaction THEN the search terms are not cleared`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-id"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(ContentAction.UpdateIsSearchAction("test-id", true))
+ store.waitUntilIdle()
+
+ val observer = EngineObserver("test-id", store)
+ observer.onLocationChange("testUrl", false)
+ store.waitUntilIdle()
+
+ middleware.assertNotDispatched(ContentAction.UpdateSearchTermsAction::class)
+ }
+
+ @Test
+ fun `onHistoryStateChanged dispatches UpdateHistoryStateAction`() {
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("test-id", store)
+
+ observer.onHistoryStateChanged(emptyList(), 0)
+ verify(store).dispatch(
+ ContentAction.UpdateHistoryStateAction(
+ "test-id",
+ emptyList(),
+ currentIndex = 0,
+ ),
+ )
+
+ observer.onHistoryStateChanged(
+ listOf(
+ HistoryItem("Firefox", "https://firefox.com"),
+ HistoryItem("Mozilla", "http://mozilla.org"),
+ ),
+ 1,
+ )
+
+ verify(store).dispatch(
+ ContentAction.UpdateHistoryStateAction(
+ "test-id",
+ listOf(
+ HistoryItem("Firefox", "https://firefox.com"),
+ HistoryItem("Mozilla", "http://mozilla.org"),
+ ),
+ currentIndex = 1,
+ ),
+ )
+ }
+
+ @Test
+ fun `onScrollChange dispatches UpdateReaderScrollYAction`() {
+ val store: BrowserStore = mock()
+ whenever(store.state).thenReturn(mock())
+ val observer = EngineObserver("tab-id", store)
+
+ observer.onScrollChange(4321, 1234)
+ verify(store).dispatch(
+ ReaderAction.UpdateReaderScrollYAction(
+ "tab-id",
+ 1234,
+ ),
+ )
+ }
+
+ @Test
+ fun `equality between tracking protection policies`() {
+ val strict = EngineSession.TrackingProtectionPolicy.strict()
+ val recommended = EngineSession.TrackingProtectionPolicy.recommended()
+ val none = EngineSession.TrackingProtectionPolicy.none()
+ val custom = EngineSession.TrackingProtectionPolicy.select(
+ trackingCategories = emptyArray(),
+ cookiePolicy = EngineSession.TrackingProtectionPolicy.CookiePolicy.ACCEPT_ONLY_FIRST_PARTY,
+ cookiePurging = true,
+ strictSocialTrackingProtection = true,
+ )
+ val custom2 = EngineSession.TrackingProtectionPolicy.select(
+ trackingCategories = emptyArray(),
+ cookiePolicy = EngineSession.TrackingProtectionPolicy.CookiePolicy.ACCEPT_ONLY_FIRST_PARTY,
+ cookiePurging = true,
+ strictSocialTrackingProtection = true,
+ )
+
+ val customNone = EngineSession.TrackingProtectionPolicy.select(
+ trackingCategories = none.trackingCategories,
+ cookiePolicy = none.cookiePolicy,
+ cookiePurging = none.cookiePurging,
+ strictSocialTrackingProtection = false,
+ )
+
+ assertTrue(strict == EngineSession.TrackingProtectionPolicy.strict())
+ assertTrue(recommended == EngineSession.TrackingProtectionPolicy.recommended())
+ assertTrue(none == EngineSession.TrackingProtectionPolicy.none())
+ assertTrue(custom == custom2)
+
+ assertFalse(strict == EngineSession.TrackingProtectionPolicy.strict().forPrivateSessionsOnly())
+ assertFalse(recommended == EngineSession.TrackingProtectionPolicy.recommended().forPrivateSessionsOnly())
+ assertFalse(custom == custom2.forPrivateSessionsOnly())
+
+ assertFalse(strict == EngineSession.TrackingProtectionPolicy.strict().forRegularSessionsOnly())
+ assertFalse(recommended == EngineSession.TrackingProtectionPolicy.recommended().forRegularSessionsOnly())
+ assertFalse(custom == custom2.forRegularSessionsOnly())
+
+ assertFalse(none == customNone)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt
new file mode 100644
index 0000000000..65fded8a32
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt
@@ -0,0 +1,148 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+
+class CrashMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `Crash and restore scenario`() {
+ val engineSession1: EngineSession = mock()
+ val engineSession2: EngineSession = mock()
+ val engineSession3: EngineSession = mock()
+
+ val engine: Engine = mock()
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "tab1").copy(
+ engineState = EngineState(engineSession1),
+ ),
+ createTab("https://www.firefox.com", id = "tab2").copy(
+ engineState = EngineState(engineSession2),
+ ),
+ createTab("https://getpocket.com", id = "tab3").copy(
+ engineState = EngineState(engineSession3),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ CrashAction.SessionCrashedAction(
+ "tab1",
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ CrashAction.SessionCrashedAction(
+ "tab3",
+ ),
+ ).joinBlocking()
+
+ assertTrue(store.state.tabs[0].engineState.crashed)
+ assertFalse(store.state.tabs[1].engineState.crashed)
+ assertTrue(store.state.tabs[2].engineState.crashed)
+
+ // Restoring crashed session
+ store.dispatch(
+ CrashAction.RestoreCrashedSessionAction(
+ "tab1",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertFalse(store.state.tabs[0].engineState.crashed)
+ assertFalse(store.state.tabs[1].engineState.crashed)
+ assertTrue(store.state.tabs[2].engineState.crashed)
+
+ // Restoring a non crashed session
+ store.dispatch(
+ CrashAction.RestoreCrashedSessionAction(
+ "tab2",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ // Restoring unknown session
+ store.dispatch(
+ CrashAction.RestoreCrashedSessionAction(
+ "unknown",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertFalse(store.state.tabs[0].engineState.crashed)
+ assertFalse(store.state.tabs[1].engineState.crashed)
+ assertTrue(store.state.tabs[2].engineState.crashed)
+ }
+
+ @Test
+ fun `Restoring a crashed session without an engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "tab1"),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ CrashAction.SessionCrashedAction(
+ "tab1",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertTrue(store.state.tabs[0].engineState.crashed)
+
+ store.dispatch(
+ CrashAction.RestoreCrashedSessionAction(
+ "tab1",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertFalse(store.state.tabs[0].engineState.crashed)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt
new file mode 100644
index 0000000000..4e374690c4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt
@@ -0,0 +1,213 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+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.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.support.test.any
+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 mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class CreateEngineSessionMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `creates engine session if needed`() = runTestOnMain {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
+
+ val middleware = CreateEngineSessionMiddleware(engine, scope)
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+ assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
+
+ store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+ verify(engine, times(1)).createSession(false)
+ assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
+
+ store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+ verify(engine, times(1)).createSession(false)
+ assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
+ }
+
+ @Test
+ fun `restores engine session state if available`() = runTestOnMain {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
+ val engineSessionState: EngineSessionState = mock()
+
+ val middleware = CreateEngineSessionMiddleware(engine, scope)
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+ assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
+
+ store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, engineSessionState)).joinBlocking()
+ store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engineSession).restoreState(engineSessionState)
+ Unit
+ }
+
+ @Test
+ fun `creates no engine session if tab does not exist`() = runTestOnMain {
+ val engine: Engine = mock()
+ `when`(engine.createSession(anyBoolean(), anyString())).thenReturn(mock())
+
+ val middleware = CreateEngineSessionMiddleware(engine, scope)
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf()),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(EngineAction.CreateEngineSessionAction("invalid")).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engine, never()).createSession(anyBoolean(), any())
+ Unit
+ }
+
+ @Test
+ fun `creates no engine session if session does not exist`() = runTestOnMain {
+ val engine: Engine = mock()
+ `when`(engine.createSession(anyBoolean(), anyString())).thenReturn(mock())
+
+ val middleware = CreateEngineSessionMiddleware(engine, scope)
+ val tab = createTab("https://www.mozilla.org", id = "1")
+
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(
+ EngineAction.CreateEngineSessionAction("non-existent"),
+ ).joinBlocking()
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engine, never()).createSession(anyBoolean(), any())
+ Unit
+ }
+
+ @Test
+ fun `dispatches follow-up action after engine session is created`() = runTestOnMain {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
+
+ val middleware = CreateEngineSessionMiddleware(engine, scope)
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+ assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
+
+ val followupAction = ContentAction.UpdateTitleAction(tab.id, "test")
+ store.dispatch(EngineAction.CreateEngineSessionAction(tab.id, followupAction = followupAction)).joinBlocking()
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engine, times(1)).createSession(false)
+ assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
+ assertEquals(followupAction.title, store.state.findTab(tab.id)?.content?.title)
+ }
+
+ @Test
+ fun `dispatches follow-up action once engine session is created by pending action`() = runTestOnMain {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
+
+ val middleware = CreateEngineSessionMiddleware(engine, scope)
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+ assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
+
+ val followupAction = ContentAction.UpdateTitleAction(tab.id, "test")
+
+ // Simulate two concurrent CreateEngineSessionActions
+ store.dispatch(EngineAction.CreateEngineSessionAction(tab.id))
+ store.dispatch(EngineAction.CreateEngineSessionAction(tab.id, followupAction = followupAction))
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engine, times(1)).createSession(false)
+ assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
+ assertEquals(followupAction.title, store.state.findTab(tab.id)?.content?.title)
+ }
+
+ @Test
+ fun `creating engine session for custom tab`() {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
+
+ val middleware = CreateEngineSessionMiddleware(engine, scope)
+ val customTab = createCustomTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(customTabs = listOf(customTab)),
+ middleware = listOf(middleware),
+ )
+ assertNull(store.state.findCustomTab(customTab.id)?.engineState?.engineSession)
+
+ val followupAction = ContentAction.UpdateTitleAction(customTab.id, "test")
+ store.dispatch(EngineAction.CreateEngineSessionAction(customTab.id, followupAction = followupAction)).joinBlocking()
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engine, times(1)).createSession(false)
+ assertEquals(engineSession, store.state.findCustomTab(customTab.id)?.engineState?.engineSession)
+ assertEquals(followupAction.title, store.state.findCustomTab(customTab.id)?.content?.title)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt
new file mode 100644
index 0000000000..5eaeb328ee
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt
@@ -0,0 +1,813 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.EngineState
+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.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.any
+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.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class EngineDelegateMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `LoadUrlAction for tab without engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).loadUrl("https://www.firefox.com")
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for private tab without engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession(private = true)
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab", private = true)
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = true, contextId = null)
+ verify(engineSession, times(1)).loadUrl("https://www.firefox.com")
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for container tab without engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession(contextId = "test-container")
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab", contextId = "test-container")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = "test-container")
+ verify(engineSession, times(1)).loadUrl("https://www.firefox.com")
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for tab with engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab").copy(
+ engineState = EngineState(engineSession),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString())
+ verify(engineSession, times(1)).loadUrl("https://www.firefox.com")
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for private tab with engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab", private = true).copy(
+ engineState = EngineState(engineSession),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString())
+ verify(engineSession, times(1)).loadUrl("https://www.firefox.com")
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for container tab with engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab", contextId = "test-container").copy(
+ engineState = EngineState(engineSession),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString())
+ verify(engineSession, times(1)).loadUrl("https://www.firefox.com")
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for tab with parent tab`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val parentEngineSession: EngineSession = mock()
+
+ val parent = createTab("https://getpocket.com", id = "parent-tab").copy(
+ engineState = EngineState(parentEngineSession),
+ )
+ val tab = createTab("https://www.mozilla.org", id = "test-tab", parent = parent)
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(parent, tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).loadUrl("https://www.firefox.com", parentEngineSession)
+ assertEquals(parentEngineSession, store.state.tabs[0].engineState.engineSession)
+ assertEquals(engineSession, store.state.tabs[1].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for tab with parent tab without engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val parent = createTab("https://getpocket.com", id = "parent-tab")
+ val tab = createTab("https://www.mozilla.org", id = "test-tab", parent = parent)
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(parent, tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine, times(1)).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).loadUrl("https://www.firefox.com")
+ assertEquals(engineSession, store.state.tabs[1].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction with flags and additional headers`() {
+ val engineSession: EngineSession = mock()
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = mock(),
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab").copy(
+ engineState = EngineState(engineSession),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.firefox.com",
+ EngineSession.LoadUrlFlags.external(),
+ mapOf(
+ "X-Coffee" to "Large",
+ "X-Sugar" to "None",
+ ),
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engineSession, times(1)).loadUrl(
+ "https://www.firefox.com",
+ flags = EngineSession.LoadUrlFlags.external(),
+ additionalHeaders = mapOf(
+ "X-Coffee" to "Large",
+ "X-Sugar" to "None",
+ ),
+ )
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for tab with same url and without engine session`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "test-tab",
+ "https://www.mozilla.org",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).loadUrl("https://www.mozilla.org")
+
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadUrlAction for not existing tab`() {
+ val engine: Engine = mock()
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ "unknown-tab",
+ "https://www.mozilla.org",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString())
+ assertNull(store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `LoadDataAction for tab without EngineSession`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LoadDataAction(
+ "test-tab",
+ data = "foobar data",
+ mimeType = "something/important",
+ encoding = "UTF-16",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).loadData(
+ data = "foobar data",
+ mimeType = "something/important",
+ encoding = "UTF-16",
+ )
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `ReloadAction for tab without EngineSession`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.ReloadAction(
+ "test-tab",
+ flags = EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.BYPASS_CACHE),
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).reload(
+ EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.BYPASS_CACHE),
+ )
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `GoForwardAction for tab without EngineSession`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.GoForwardAction(
+ "test-tab",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).goForward()
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `GoBackAction for tab without EngineSession`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.GoBackAction(
+ "test-tab",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).goBack()
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `GoToHistoryIndexAction for tab without EngineSession`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.GoToHistoryIndexAction(
+ "test-tab",
+ index = 42,
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).goToHistoryIndex(42)
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `ToggleDesktopModeAction - Enable desktop mode`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.ToggleDesktopModeAction(
+ "test-tab",
+ enable = true,
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).toggleDesktopMode(enable = true, reload = true)
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `ToggleDesktopModeAction - Disable desktop mode`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.ToggleDesktopModeAction(
+ "test-tab",
+ enable = false,
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).toggleDesktopMode(enable = false, reload = true)
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `ExitFullscreenModeAction for tab without EngineSession`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.ExitFullScreenModeAction(
+ "test-tab",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).exitFullScreenMode()
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `ClearDataAction for tab without EngineSession`() {
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.ClearDataAction(
+ "test-tab",
+ data = Engine.BrowsingData.allCaches(),
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engine).createSession(private = false, contextId = null)
+ verify(engineSession, times(1)).clearData(Engine.BrowsingData.allCaches())
+ assertEquals(engineSession, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `PurgeHistoryAction - calls purgeHistory on engine session instances`() {
+ val engineSession1: EngineSession = mock()
+ val engineSession2: EngineSession = mock()
+
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = mock(),
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org").copy(
+ engineState = EngineState(engineSession = null, engineSessionState = mock()),
+ ),
+ createTab("https://www.firefox.com").copy(
+ engineState = EngineState(engineSession = engineSession1, engineSessionState = mock()),
+ ),
+ ),
+ customTabs = listOf(
+ createCustomTab("http://www.theverge.com").copy(
+ engineState = EngineState(engineSession = null, engineSessionState = mock()),
+ ),
+ createCustomTab("https://www.google.com").copy(
+ engineState = EngineState(engineSession = engineSession2, engineSessionState = mock()),
+ ),
+ ),
+ ),
+ )
+
+ store.dispatch(EngineAction.PurgeHistoryAction).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engineSession1).purgeHistory()
+ verify(engineSession2).purgeHistory()
+ }
+
+ @Test
+ fun `TranslateAction correctly sets progress state AND begins a translation`() {
+ val tab = createTab("https://www.mozilla.org")
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ assertFalse(store.state.findTab(tab.id)?.translationsState?.isTranslateProcessing!!)
+
+ store.dispatch(
+ TranslationsAction.TranslateAction(
+ tabId = tab.id,
+ fromLanguage = "es",
+ toLanguage = "en",
+ options = null,
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engineSession).requestTranslate(any(), any(), any())
+ assertTrue(store.state.findTab(tab.id)?.translationsState?.isTranslateProcessing!!)
+ }
+
+ @Test
+ fun `TranslateRestoreAction correctly sets progress state AND begins a restore`() {
+ val tab = createTab("https://www.mozilla.org")
+ val engineSession: EngineSession = mock()
+ val engine: Engine = mock()
+ doReturn(engineSession).`when`(engine).createSession()
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ scope = scope,
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ assertFalse(store.state.findTab(tab.id)?.translationsState?.isRestoreProcessing!!)
+
+ store.dispatch(
+ TranslationsAction.TranslateRestoreAction(tabId = tab.id),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(engineSession).requestTranslationRestore()
+ assertTrue(store.state.findTab(tab.id)?.translationsState?.isRestoreProcessing!!)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddlewareTest.kt
new file mode 100644
index 0000000000..4dc5e9152c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddlewareTest.kt
@@ -0,0 +1,44 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.ExtensionsProcessAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+
+class ExtensionsProcessMiddlewareTest {
+ private lateinit var engine: Engine
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setUp() {
+ engine = Mockito.mock()
+ store = BrowserStore(
+ middleware = listOf(ExtensionsProcessMiddleware(engine)),
+ initialState = BrowserState(),
+ )
+ }
+
+ @Test
+ fun `WHEN EnabledAction is dispatched THEN enable the process spawning`() {
+ store.dispatch(ExtensionsProcessAction.EnabledAction).joinBlocking()
+
+ Mockito.verify(engine).enableExtensionProcessSpawning()
+ Mockito.verify(engine, Mockito.never()).disableExtensionProcessSpawning()
+ }
+
+ @Test
+ fun `WHEN DisabledAction is dispatched THEN disable the process spawning`() {
+ store.dispatch(ExtensionsProcessAction.DisabledAction).joinBlocking()
+
+ Mockito.verify(engine).disableExtensionProcessSpawning()
+ Mockito.verify(engine, Mockito.never()).enableExtensionProcessSpawning()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt
new file mode 100644
index 0000000000..89939b71a2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt
@@ -0,0 +1,246 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.any
+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 mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class LinkingMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `loads URL after linking`() {
+ val middleware = LinkingMiddleware(scope)
+
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engineSession).loadUrl(tab.content.url)
+ }
+
+ @Test
+ fun `loads URL with load URL flags and additional headers after linking`() {
+ val middleware = LinkingMiddleware(scope)
+
+ val loadFlags = EngineSession.LoadUrlFlags.external()
+ val additionalHeaders = mapOf("X-Extra-Header" to "true")
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ id = "1",
+ initialLoadFlags = loadFlags,
+ initialAdditionalHeaders = additionalHeaders,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engineSession).loadUrl(
+ url = tab.content.url,
+ flags = loadFlags,
+ additionalHeaders = additionalHeaders,
+ )
+ }
+
+ @Test
+ fun `loads URL with parent after linking`() {
+ val middleware = LinkingMiddleware(scope)
+
+ val parent = createTab("https://www.mozilla.org", id = "1")
+ val child = createTab("https://www.firefox.com", id = "2", parent = parent)
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(parent, child),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ val parentEngineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(parent.id, parentEngineSession)).joinBlocking()
+
+ val childEngineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(child.id, childEngineSession)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(childEngineSession).loadUrl(child.content.url, parentEngineSession)
+ }
+
+ @Test
+ fun `loads URL without parent for extension URLs`() {
+ val middleware = LinkingMiddleware(scope)
+
+ val parent = createTab("https://www.mozilla.org", id = "1")
+ val child = createTab("moz-extension://1234", id = "2", parent = parent)
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(parent, child),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ val parentEngineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(parent.id, parentEngineSession)).joinBlocking()
+
+ val childEngineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(child.id, childEngineSession)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(childEngineSession).loadUrl(child.content.url)
+ }
+
+ @Test
+ fun `skips loading URL if specified in action`() {
+ val middleware = LinkingMiddleware(scope)
+
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession, skipLoading = true)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engineSession, never()).loadUrl(tab.content.url)
+ }
+
+ @Test
+ fun `does nothing if linked tab does not exist`() {
+ val middleware = LinkingMiddleware(scope)
+
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf()),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("invalid", engineSession)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(engineSession, never()).loadUrl(anyString(), any(), any(), any())
+ }
+
+ @Test
+ fun `registers engine observer after linking`() = runTestOnMain {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val tab2 = createTab("https://www.mozilla.org", id = "2")
+
+ val middleware = LinkingMiddleware(scope)
+
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab1, tab2)),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession1: EngineSession = mock()
+ val engineSession2: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab1.id, engineSession1)).joinBlocking()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab2.id, engineSession2)).joinBlocking()
+ store.waitUntilIdle()
+
+ // We only have a session for tab2 so we should only register an observer for tab2
+ val engineObserver = store.state.findTab(tab2.id)?.engineState?.engineObserver
+ assertNotNull(engineObserver)
+
+ verify(engineSession2).register(engineObserver!!)
+ engineObserver.onTitleChange("test")
+
+ store.waitUntilIdle()
+
+ assertEquals("test", store.state.tabs[1].content.title)
+ }
+
+ @Test
+ fun `unregisters engine observer before unlinking`() = runTestOnMain {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val tab2 = createTab("https://www.mozilla.org", id = "2")
+
+ val middleware = LinkingMiddleware(scope)
+
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab1, tab2)),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab1.id, engineSession)).joinBlocking()
+ store.waitUntilIdle()
+ assertNotNull(store.state.findTab(tab1.id)?.engineState?.engineObserver)
+ assertNull(store.state.findTab(tab2.id)?.engineState?.engineObserver)
+
+ store.dispatch(EngineAction.UnlinkEngineSessionAction(tab1.id)).joinBlocking()
+ store.dispatch(EngineAction.UnlinkEngineSessionAction(tab2.id)).joinBlocking()
+ store.waitUntilIdle()
+ assertNull(store.state.findTab(tab1.id)?.engineState?.engineObserver)
+ assertNull(store.state.findTab(tab2.id)?.engineState?.engineObserver)
+ }
+
+ @Test
+ fun `registers engine observer when tab is added with engine session`() = runTestOnMain {
+ val engineSession: EngineSession = mock()
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val tab2 = createTab("https://www.mozilla.org", id = "2", engineSession = engineSession)
+
+ val middleware = LinkingMiddleware(scope)
+
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ store.waitUntilIdle()
+
+ // We only have a session for tab2 so we should only register an observer for tab2
+ val engineObserver = store.state.findTab(tab2.id)?.engineState?.engineObserver
+ assertNotNull(engineObserver)
+ verify(engineSession).register(engineObserver!!)
+ engineObserver.onTitleChange("test")
+ store.waitUntilIdle()
+
+ assertEquals("test", store.state.tabs[1].content.title)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt
new file mode 100644
index 0000000000..89f23b0909
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt
@@ -0,0 +1,155 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.SessionPriority.DEFAULT
+import mozilla.components.concept.engine.EngineSession.SessionPriority.HIGH
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class SessionPrioritizationMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun `GIVEN a linked session WHEN UnlinkEngineSessionAction THEN set the DEFAULT priority to the unlinked tab`() {
+ val middleware = SessionPrioritizationMiddleware()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+ val engineSession1: EngineSession = mock()
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+ store.dispatch(EngineAction.UnlinkEngineSessionAction("1")).joinBlocking()
+
+ verify(engineSession1).updateSessionPriority(DEFAULT)
+ assertEquals("", middleware.previousHighestPriorityTabId)
+ }
+
+ @Test
+ fun `GIVEN a linked session WHEN CheckForFormDataAction THEN update the selected linked tab priority to DEFAULT if there is no form data and HIGH when there is form data`() = runTestOnMain {
+ val middleware = SessionPrioritizationMiddleware(updatePriorityAfterMillis = 0, waitScope = coroutinesTestRule.scope)
+ val capture = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ ),
+ ),
+ middleware = listOf(capture, middleware),
+ )
+ val engineSession1: EngineSession = mock()
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+ store.dispatch(ContentAction.UpdateHasFormDataAction("1", false)).joinBlocking()
+ verify(engineSession1).updateSessionPriority(DEFAULT)
+
+ store.dispatch(ContentAction.UpdateHasFormDataAction("1", true)).joinBlocking()
+ verify(engineSession1).updateSessionPriority(HIGH)
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ capture.assertLastAction(ContentAction.UpdatePriorityToDefaultAfterTimeoutAction::class) {}
+ }
+
+ @Test
+ fun `GIVEN a previous selected tab WHEN LinkEngineSessionAction THEN update the selected linked tab priority to HIGH`() = runTestOnMain {
+ val middleware = SessionPrioritizationMiddleware()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+ val engineSession1: EngineSession = mock()
+
+ store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking()
+
+ assertEquals("", middleware.previousHighestPriorityTabId)
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+
+ assertEquals("1", middleware.previousHighestPriorityTabId)
+ verify(engineSession1).updateSessionPriority(HIGH)
+ }
+
+ @Test
+ fun `GIVEN a previous selected tab with priority DEFAULT WHEN selecting and linking a new tab THEN update the new one to HIGH and the previous tab based on if it contains form data`() = runTestOnMain {
+ val middleware = SessionPrioritizationMiddleware()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ createTab("https://www.firefox.com", id = "2"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+ val engineSession1: EngineSession = mock()
+ val engineSession2: EngineSession = mock()
+
+ store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking()
+
+ assertEquals("", middleware.previousHighestPriorityTabId)
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+
+ assertEquals("1", middleware.previousHighestPriorityTabId)
+ verify(engineSession1).updateSessionPriority(HIGH)
+
+ store.dispatch(TabListAction.SelectTabAction("2")).joinBlocking()
+
+ assertEquals("1", middleware.previousHighestPriorityTabId)
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("2", engineSession2)).joinBlocking()
+
+ assertEquals("2", middleware.previousHighestPriorityTabId)
+ verify(engineSession1).checkForFormData()
+ verify(engineSession2).updateSessionPriority(HIGH)
+ }
+
+ @Test
+ fun `GIVEN no linked tab WHEN SelectTabAction THEN no changes in priority show happened`() {
+ val middleware = SessionPrioritizationMiddleware()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ createTab("https://www.firefox.com", id = "2"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking()
+
+ assertEquals("", middleware.previousHighestPriorityTabId)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt
new file mode 100644
index 0000000000..2a14b04607
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt
@@ -0,0 +1,155 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.BrowserState
+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.concept.engine.EngineSessionState
+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 mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class SuspendMiddlewareTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `suspends engine session for tab`() = runTestOnMain {
+ val middleware = SuspendMiddleware(scope)
+
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+
+ val state: EngineSessionState = mock()
+ store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, state)).joinBlocking()
+
+ store.dispatch(EngineAction.SuspendEngineSessionAction(tab.id)).joinBlocking()
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTabOrCustomTab(tab.id)?.engineState?.engineSession)
+ assertEquals(state, store.state.findTabOrCustomTab(tab.id)?.engineState?.engineSessionState)
+ verify(engineSession).close()
+ }
+
+ @Test
+ fun `suspends engine session for custom tab`() = runTestOnMain {
+ val middleware = SuspendMiddleware(scope)
+
+ val tab = createCustomTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(customTabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+
+ val state: EngineSessionState = mock()
+ store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, state)).joinBlocking()
+
+ store.dispatch(EngineAction.SuspendEngineSessionAction(tab.id)).joinBlocking()
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTabOrCustomTab(tab.id)?.engineState?.engineSession)
+ assertEquals(state, store.state.findTabOrCustomTab(tab.id)?.engineState?.engineSessionState)
+ verify(engineSession).close()
+ }
+
+ @Test
+ fun `does nothing if tab doesn't exist`() {
+ val middleware = SuspendMiddleware(scope)
+
+ val store = spy(
+ BrowserStore(
+ initialState = BrowserState(tabs = listOf()),
+ middleware = listOf(middleware),
+ ),
+ )
+
+ store.dispatch(EngineAction.SuspendEngineSessionAction("invalid")).joinBlocking()
+ verify(store, never()).dispatch(EngineAction.UnlinkEngineSessionAction("invalid"))
+ }
+
+ @Test
+ fun `does nothing if engine session doesn't exist`() {
+ val middleware = SuspendMiddleware(scope)
+
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = spy(
+ BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ ),
+ )
+
+ store.dispatch(EngineAction.SuspendEngineSessionAction(tab.id)).joinBlocking()
+ verify(store, never()).dispatch(EngineAction.UnlinkEngineSessionAction(tab.id))
+ }
+
+ @Test
+ fun `SuspendEngineSessionAction and KillEngineSessionAction process state the same`() {
+ val middleware = SuspendMiddleware(scope)
+
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val suspendStore = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+ val killStore = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession: EngineSession = mock()
+ suspendStore.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+ killStore.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+
+ val state: EngineSessionState = mock()
+ suspendStore.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, state)).joinBlocking()
+ killStore.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, state)).joinBlocking()
+
+ suspendStore.dispatch(EngineAction.SuspendEngineSessionAction(tab.id)).joinBlocking()
+ killStore.dispatch(EngineAction.KillEngineSessionAction(tab.id)).joinBlocking()
+
+ suspendStore.waitUntilIdle()
+ killStore.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(suspendStore.state.findTabOrCustomTab(tab.id)?.engineState?.engineSession)
+ assertEquals(state, suspendStore.state.findTabOrCustomTab(tab.id)?.engineState?.engineSessionState)
+
+ assertNull(killStore.state.findTabOrCustomTab(tab.id)?.engineState?.engineSession)
+ assertEquals(state, killStore.state.findTabOrCustomTab(tab.id)?.engineState?.engineSessionState)
+
+ assertEquals(suspendStore.state, killStore.state)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt
new file mode 100644
index 0000000000..89aea2cc99
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt
@@ -0,0 +1,246 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.BrowserState
+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.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+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 mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class TabsRemovedMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `closes and unlinks engine session when tab is removed`() = runTestOnMain {
+ val middleware = TabsRemovedMiddleware(scope)
+
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()),
+ )
+
+ val engineSession = linkEngineSession(store, tab.id)
+ store.dispatch(TabListAction.RemoveTabAction(tab.id)).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
+ verify(engineSession).close()
+ }
+
+ @Test
+ fun `closes and unlinks engine session when list of tabs are removed`() = runTestOnMain {
+ val middleware = TabsRemovedMiddleware(scope)
+
+ val tab1 = createTab("https://www.mozilla.org", id = "1", private = false)
+ val tab2 = createTab("https://www.firefox.com", id = "2", private = false)
+ val tab3 = createTab("https://www.getpocket.com", id = "3", private = false)
+
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab1, tab2, tab3)),
+ middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()),
+ )
+
+ val engineSession1 = linkEngineSession(store, tab1.id)
+ val engineSession2 = linkEngineSession(store, tab2.id)
+ val engineSession3 = linkEngineSession(store, tab3.id)
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf(tab1.id, tab2.id))).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession)
+ assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession)
+ assertNotNull(store.state.findTab(tab3.id)?.engineState?.engineSession)
+ verify(engineSession1).close()
+ verify(engineSession2).close()
+ verify(engineSession3, never()).close()
+ }
+
+ @Test
+ fun `closes and unlinks engine session when all normal tabs are removed`() = runTestOnMain {
+ val middleware = TabsRemovedMiddleware(scope)
+
+ val tab1 = createTab("https://www.mozilla.org", id = "1", private = false)
+ val tab2 = createTab("https://www.firefox.com", id = "2", private = false)
+ val tab3 = createTab("https://www.getpocket.com", id = "3", private = true)
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab1, tab2, tab3)),
+ middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()),
+ )
+
+ val engineSession1 = linkEngineSession(store, tab1.id)
+ val engineSession2 = linkEngineSession(store, tab2.id)
+ val engineSession3 = linkEngineSession(store, tab3.id)
+
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession)
+ assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession)
+ assertNotNull(store.state.findTab(tab3.id)?.engineState?.engineSession)
+ verify(engineSession1).close()
+ verify(engineSession2).close()
+ verify(engineSession3, never()).close()
+ }
+
+ @Test
+ fun `closes and unlinks engine session when all private tabs are removed`() = runTestOnMain {
+ val middleware = TabsRemovedMiddleware(scope)
+
+ val tab1 = createTab("https://www.mozilla.org", id = "1", private = true)
+ val tab2 = createTab("https://www.firefox.com", id = "2", private = true)
+ val tab3 = createTab("https://www.getpocket.com", id = "3", private = false)
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab1, tab2, tab3)),
+ middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()),
+ )
+
+ val engineSession1 = linkEngineSession(store, tab1.id)
+ val engineSession2 = linkEngineSession(store, tab2.id)
+ val engineSession3 = linkEngineSession(store, tab3.id)
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession)
+ assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession)
+ assertNotNull(store.state.findTab(tab3.id)?.engineState?.engineSession)
+ verify(engineSession1).close()
+ verify(engineSession2).close()
+ verify(engineSession3, never()).close()
+ }
+
+ @Test
+ fun `closes and unlinks engine session when all tabs are removed`() = runTestOnMain {
+ val middleware = TabsRemovedMiddleware(scope)
+
+ val tab1 = createTab("https://www.mozilla.org", id = "1", private = true)
+ val tab2 = createTab("https://www.firefox.com", id = "2", private = false)
+ val tab3 = createCustomTab("https://www.getpocket.com", id = "3")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab1, tab2), customTabs = listOf(tab3)),
+ middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()),
+ )
+
+ val engineSession1 = linkEngineSession(store, tab1.id)
+ val engineSession2 = linkEngineSession(store, tab2.id)
+ val engineSession3 = linkEngineSession(store, tab3.id)
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession)
+ assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession)
+ assertNotNull(store.state.findCustomTab(tab3.id)?.engineState?.engineSession)
+ verify(engineSession1).close()
+ verify(engineSession2).close()
+ verify(engineSession3, never()).close()
+ }
+
+ @Test
+ fun `closes and unlinks engine session when custom tab is removed`() = runTestOnMain {
+ val middleware = TabsRemovedMiddleware(scope)
+
+ val tab = createCustomTab("https://www.mozilla.org", id = "1")
+ val store = BrowserStore(
+ initialState = BrowserState(customTabs = listOf(tab)),
+ middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()),
+ )
+
+ val engineSession = linkEngineSession(store, tab.id)
+ store.dispatch(CustomTabListAction.RemoveCustomTabAction(tab.id)).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
+ verify(engineSession).close()
+ }
+
+ @Test
+ fun `closes and unlinks engine session when all custom tabs are removed`() = runTestOnMain {
+ val middleware = TabsRemovedMiddleware(scope)
+
+ val tab1 = createCustomTab("https://www.mozilla.org", id = "1")
+ val tab2 = createCustomTab("https://www.firefox.com", id = "2")
+ val tab3 = createTab("https://www.getpocket.com", id = "3")
+ val store = BrowserStore(
+ initialState = BrowserState(customTabs = listOf(tab1, tab2), tabs = listOf(tab3)),
+ middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()),
+ )
+
+ val engineSession1 = linkEngineSession(store, tab1.id)
+ val engineSession2 = linkEngineSession(store, tab2.id)
+ val engineSession3 = linkEngineSession(store, tab3.id)
+
+ store.dispatch(CustomTabListAction.RemoveAllCustomTabsAction).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findCustomTab(tab1.id)?.engineState?.engineSession)
+ assertNull(store.state.findCustomTab(tab2.id)?.engineState?.engineSession)
+ assertNotNull(store.state.findTab(tab3.id)?.engineState?.engineSession)
+ verify(engineSession1).close()
+ verify(engineSession2).close()
+ verify(engineSession3, never()).close()
+ }
+
+ private fun linkEngineSession(store: BrowserStore, tabId: String): EngineSession {
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tabId, engineSession)).joinBlocking()
+ assertNotNull(store.state.findTabOrCustomTab(tabId)?.engineState?.engineSession)
+ return engineSession
+ }
+
+ // This is to consume remove tab actions so we can assert that we properly unlink tabs
+ // before they get removed. If we didn't do this the tab would already be gone once
+ // TabsRemovedMiddleware processed the action.
+ private class ConsumeRemoveTabActionsMiddleware : Middleware<BrowserState, BrowserAction> {
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is TabListAction.RemoveAllNormalTabsAction,
+ is TabListAction.RemoveAllPrivateTabsAction,
+ is TabListAction.RemoveAllTabsAction,
+ is TabListAction.RemoveTabAction,
+ is CustomTabListAction.RemoveAllCustomTabsAction,
+ is CustomTabListAction.RemoveCustomTabAction,
+ -> return
+ else -> next(action)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt
new file mode 100644
index 0000000000..cf30f7060e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt
@@ -0,0 +1,945 @@
+/* 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.browser.state.engine.middleware
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.TranslationsBrowserState
+import mozilla.components.browser.state.state.TranslationsState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.translate.DetectedLanguages
+import mozilla.components.concept.engine.translate.Language
+import mozilla.components.concept.engine.translate.LanguageModel
+import mozilla.components.concept.engine.translate.LanguageSetting
+import mozilla.components.concept.engine.translate.TranslationDownloadSize
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettingOperation
+import mozilla.components.concept.engine.translate.TranslationPageSettings
+import mozilla.components.concept.engine.translate.TranslationSupport
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+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 mozilla.components.support.test.whenever
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class TranslationsMiddlewareTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+ private val engine: Engine = mock()
+ private val engineSession: EngineSession = mock()
+ private val tab: TabSessionState = spy(
+ createTab(
+ url = "https://www.firefox.com",
+ title = "Firefox",
+ id = "1",
+ engineSession = engineSession,
+ ),
+ )
+ private val translationsMiddleware = TranslationsMiddleware(engine = engine, scope = scope)
+ private val tabs = spy(listOf(tab))
+ private val state = spy(BrowserState(tabs = tabs))
+ private val store = spy(BrowserStore(middleware = listOf(translationsMiddleware), initialState = state))
+ private val context = mock<MiddlewareContext<BrowserState, BrowserAction>>()
+
+ // Mock Variables
+ private val mockFrom = Language(code = "es", localizedDisplayName = "Spanish")
+ private val mockTo = Language(code = "en", localizedDisplayName = "English")
+ private val mockSupportedLanguages = TranslationSupport(
+ fromLanguages = listOf(mockFrom, mockTo),
+ toLanguages = listOf(mockFrom, mockTo),
+ )
+ private val mockIsDownloaded = true
+ private val mockSize: Long = 1234
+ private val mockLanguage = Language(mockFrom.code, mockFrom.localizedDisplayName)
+ private val mockLanguageModel = LanguageModel(mockLanguage, mockIsDownloaded, mockSize)
+ private val mockLanguageModels = mutableListOf(mockLanguageModel)
+
+ @Before
+ fun setup() {
+ whenever(context.store).thenReturn(store)
+ whenever(context.state).thenReturn(state)
+ }
+
+ private fun waitForIdle() {
+ scope.testScheduler.runCurrent()
+ scope.testScheduler.advanceUntilIdle()
+ coroutinesTestRule.testDispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+ }
+
+ /**
+ * Use with tests that need a mock translations engine state and supported languages.
+ */
+ private fun setupMockState() {
+ val mockDetectedLanguages = DetectedLanguages(
+ documentLangTag = mockFrom.code,
+ supportedDocumentLang = true,
+ userPreferredLangTag = mockTo.code,
+ )
+ val mockSessionState = TranslationsState(
+ translationEngineState = TranslationEngineState(mockDetectedLanguages),
+ )
+ whenever(store.state.findTab(tab.id)?.translationsState).thenReturn(mockSessionState)
+
+ val mockBrowserState = TranslationsBrowserState(isEngineSupported = true, supportedLanguages = mockSupportedLanguages)
+ whenever(store.state.translationEngine).thenReturn(mockBrowserState)
+ }
+
+ @Test
+ fun `WHEN OperationRequestedAction is dispatched for FETCH_SUPPORTED_LANGUAGES AND succeeds THEN SetSupportedLanguagesAction is dispatched`() = runTest {
+ // Initial Action
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ )
+
+ translationsMiddleware.invoke(context = context, next = {}, action = action)
+
+ // Verify results
+ val languageCallback = argumentCaptor<((TranslationSupport) -> Unit)>()
+ // Verifying at least once because `InitAction` also occurred
+ verify(engine, atLeastOnce()).getSupportedTranslationLanguages(onSuccess = languageCallback.capture(), onError = any())
+ val supportedLanguages = TranslationSupport(
+ fromLanguages = listOf(Language("en", "English")),
+ toLanguages = listOf(Language("en", "English")),
+ )
+ languageCallback.value.invoke(supportedLanguages)
+
+ waitForIdle()
+
+ verify(context.store, atLeastOnce()).dispatch(
+ TranslationsAction.SetSupportedLanguagesAction(
+ supportedLanguages = supportedLanguages,
+ ),
+ )
+
+ verify(context.store, atLeastOnce()).dispatch(
+ TranslationsAction.TranslateSuccessAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN OperationRequestedAction is dispatched for FETCH_SUPPORTED_LANGUAGES AND fails THEN EngineExceptionAction is dispatched`() {
+ // Initial Action
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ )
+
+ translationsMiddleware.invoke(context = context, next = {}, action = action)
+
+ // Verify results
+ val errorCaptor = argumentCaptor<((Throwable) -> Unit)>()
+ // Verifying at least once because `InitAction` also occurred
+ verify(engine, atLeastOnce()).getSupportedTranslationLanguages(onSuccess = any(), onError = errorCaptor.capture())
+ errorCaptor.value.invoke(Throwable())
+
+ waitForIdle()
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.CouldNotLoadLanguagesError(any()),
+ ),
+ )
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
+ translationError = TranslationError.CouldNotLoadLanguagesError(any()),
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN InitAction is dispatched THEN InitTranslationsBrowserState is also dispatched`() = runTest {
+ // Send Action
+ // Note: Will cause a double InitAction
+ translationsMiddleware.invoke(context = context, next = {}, action = InitAction)
+ waitForIdle()
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.InitTranslationsBrowserState,
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN InitTranslationsBrowserState is dispatched AND the engine is supported THEN SetSupportedLanguagesAction is also dispatched`() = runTest {
+ // Send Action
+ translationsMiddleware.invoke(context = context, next = {}, action = TranslationsAction.InitTranslationsBrowserState)
+
+ // Set the engine to support
+ val engineSupportedCallback = argumentCaptor<((Boolean) -> Unit)>()
+ // At least once, since InitAction also will trigger this
+ verify(engine, atLeastOnce()).isTranslationsEngineSupported(
+ onSuccess = engineSupportedCallback.capture(),
+ onError = any(),
+ )
+ engineSupportedCallback.value.invoke(true)
+
+ // Verify results for language query
+ val languageCallback = argumentCaptor<((TranslationSupport) -> Unit)>()
+ verify(engine, atLeastOnce()).getSupportedTranslationLanguages(onSuccess = languageCallback.capture(), onError = any())
+ val supportedLanguages = TranslationSupport(
+ fromLanguages = listOf(Language("en", "English")),
+ toLanguages = listOf(Language("en", "English")),
+ )
+ languageCallback.value.invoke(supportedLanguages)
+
+ waitForIdle()
+
+ // Verifying at least once
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.SetSupportedLanguagesAction(
+ supportedLanguages = supportedLanguages,
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN InitTranslationsBrowserState is dispatched AND the engine is supported THEN SetLanguageSettingsAction is also dispatched`() = runTest {
+ // Send Action
+ translationsMiddleware.invoke(context = context, next = {}, action = TranslationsAction.InitTranslationsBrowserState)
+ waitForIdle()
+
+ // Set the engine to support
+ val engineSupportedCallback = argumentCaptor<((Boolean) -> Unit)>()
+ // At least once, since InitAction also will trigger this
+ verify(engine, atLeastOnce()).isTranslationsEngineSupported(
+ onSuccess = engineSupportedCallback.capture(),
+ onError = any(),
+ )
+ engineSupportedCallback.value.invoke(true)
+
+ // Check expectations
+ val languageSettingsCallback = argumentCaptor<((Map<String, LanguageSetting>) -> Unit)>()
+ verify(engine, atLeastOnce()).getLanguageSettings(
+ onSuccess = languageSettingsCallback.capture(),
+ onError = any(),
+ )
+ val mockLanguageSetting = mapOf("en" to LanguageSetting.OFFER)
+ languageSettingsCallback.value.invoke(mockLanguageSetting)
+ waitForIdle()
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.SetLanguageSettingsAction(
+ languageSettings = mockLanguageSetting,
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN InitTranslationsBrowserState is dispatched AND an error occurs THEN TranslateExceptionAction is dispatched for language settings`() = runTest() {
+ // Send Action
+ translationsMiddleware.invoke(context = context, next = {}, action = TranslationsAction.InitTranslationsBrowserState)
+ waitForIdle()
+
+ // Set the engine to support
+ val engineSupportedCallback = argumentCaptor<((Boolean) -> Unit)>()
+ // At least once, since InitAction also will trigger this
+ verify(engine, atLeastOnce()).isTranslationsEngineSupported(
+ onSuccess = engineSupportedCallback.capture(),
+ onError = any(),
+ )
+ engineSupportedCallback.value.invoke(true)
+
+ // Check expectations
+ val errorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, atLeastOnce()).getLanguageSettings(
+ onSuccess = any(),
+ onError = errorCallback.capture(),
+ )
+ errorCallback.value.invoke(Throwable())
+ waitForIdle()
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.CouldNotLoadLanguageSettingsError(any()),
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN InitTranslationsBrowserState is dispatched AND the engine is supported THEN SetLanguageModelsAction is also dispatched`() = runTest {
+ // Send Action
+ translationsMiddleware.invoke(context = context, next = {}, action = TranslationsAction.InitTranslationsBrowserState)
+
+ // Set the engine to support
+ val engineSupportedCallback = argumentCaptor<((Boolean) -> Unit)>()
+ // At least once, since InitAction also will trigger this
+ verify(engine, atLeastOnce()).isTranslationsEngineSupported(
+ onSuccess = engineSupportedCallback.capture(),
+ onError = any(),
+ )
+ engineSupportedCallback.value.invoke(true)
+
+ val languageCallback = argumentCaptor<((List<LanguageModel>) -> Unit)>()
+ verify(engine, atLeastOnce()).getTranslationsModelDownloadStates(onSuccess = languageCallback.capture(), onError = any())
+ languageCallback.value.invoke(mockLanguageModels)
+
+ waitForIdle()
+
+ // Verifying at least once
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.SetLanguageModelsAction(
+ languageModels = mockLanguageModels,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN InitTranslationsBrowserState is dispatched AND has an issue with the engine THEN EngineExceptionAction is dispatched`() = runTest() {
+ // Send Action
+ // Note: Implicitly called once due to connection with InitAction
+ translationsMiddleware.invoke(context = context, next = {}, action = TranslationsAction.InitTranslationsBrowserState)
+ waitForIdle()
+
+ // Check expectations
+ val errorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, atLeastOnce()).isTranslationsEngineSupported(
+ onSuccess = any(),
+ onError = errorCallback.capture(),
+ )
+ errorCallback.value.invoke(IllegalStateException())
+ waitForIdle()
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.UnknownEngineSupportError(any()),
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN InitTranslationsBrowserState is dispatched AND the engine is not supported THEN SetSupportedLanguagesAction and SetLanguageModelsAction are NOT dispatched`() = runTest {
+ // Send Action
+ // Will invoke a double InitAction
+ translationsMiddleware.invoke(context = context, next = {}, action = TranslationsAction.InitTranslationsBrowserState)
+
+ // Set the engine to not support
+ val engineNotSupportedCallback = argumentCaptor<((Boolean) -> Unit)>()
+ verify(engine, atLeastOnce()).isTranslationsEngineSupported(
+ onSuccess = engineNotSupportedCallback.capture(),
+ onError = any(),
+ )
+ engineNotSupportedCallback.value.invoke(false)
+
+ // Verify language query was never called
+ verify(engine, never()).getSupportedTranslationLanguages(onSuccess = any(), onError = any())
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN TranslateExpectedAction is dispatched THEN FetchTranslationDownloadSizeAction is also dispatched`() = runTest {
+ // Set up the state of defaults on the engine.
+ setupMockState()
+
+ // Action
+ translationsMiddleware.invoke(context = context, next = {}, action = TranslationsAction.TranslateExpectedAction(tab.id))
+
+ waitForIdle()
+
+ // Verifying at least once
+ verify(store).dispatch(
+ TranslationsAction.FetchTranslationDownloadSizeAction(
+ tabId = tab.id,
+ fromLanguage = mockFrom,
+ toLanguage = mockTo,
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN TranslateExpectedAction is dispatched AND the defaults are NOT available THEN FetchTranslationDownloadSizeAction is NOT dispatched`() = runTest {
+ // Note, no state is set on the engine, so no default values are available.
+ // Action
+ translationsMiddleware.invoke(context = context, next = {}, action = TranslationsAction.TranslateExpectedAction(tab.id))
+
+ waitForIdle()
+
+ // Verifying no dispatch
+ verify(store, never()).dispatch(
+ TranslationsAction.FetchTranslationDownloadSizeAction(
+ tabId = tab.id,
+ fromLanguage = mockFrom,
+ toLanguage = mockTo,
+ ),
+ )
+
+ // Verify language query was never called
+ verify(engine, never()).getTranslationsModelDownloadStates(onSuccess = any(), onError = any())
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN OperationRequestedAction is dispatched WITH FETCH_PAGE_SETTINGS AND fetching settings is successful THEN TranslationPageSettings is dispatched`() = runTest {
+ // Setup
+ setupMockState()
+
+ val mockPageSettings = TranslationPageSettings(
+ alwaysOfferPopup = true,
+ alwaysTranslateLanguage = true,
+ neverTranslateLanguage = false,
+ neverTranslateSite = true,
+ )
+
+ whenever(engine.getTranslationsOfferPopup()).thenAnswer { mockPageSettings.alwaysOfferPopup }
+
+ // Send Action
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ waitForIdle()
+
+ // Check Behavior
+ // Popup always offer behavior
+ verify(engine).getTranslationsOfferPopup()
+
+ // Page language behavior
+ val languageSettingCallback = argumentCaptor<((LanguageSetting) -> Unit)>()
+ verify(engine).getLanguageSetting(
+ languageCode = any(),
+ onSuccess = languageSettingCallback.capture(),
+ onError = any(),
+ )
+ val languageResponse = LanguageSetting.ALWAYS
+ languageSettingCallback.value.invoke(languageResponse)
+
+ // Never translate site behavior behavior
+ val neverTranslateSiteCallback = argumentCaptor<((Boolean) -> Unit)>()
+ verify(engineSession).getNeverTranslateSiteSetting(
+ onResult = neverTranslateSiteCallback.capture(),
+ onException = any(),
+ )
+ neverTranslateSiteCallback.value.invoke(mockPageSettings.neverTranslateSite!!)
+
+ verify(store).dispatch(
+ TranslationsAction.SetPageSettingsAction(
+ tabId = tab.id,
+ pageSettings = mockPageSettings,
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN OperationRequestedAction WITH FETCH_PAGE_SETTINGS AND fetching settings fails THEN TranslateExceptionAction is dispatched`() {
+ // Setup
+ setupMockState()
+ whenever(engine.getTranslationsOfferPopup()).thenAnswer { false }
+
+ // Send Action
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ waitForIdle()
+
+ // Check Behavior
+ // Page language behavior
+ val languageErrorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine).getLanguageSetting(
+ languageCode = any(),
+ onSuccess = any(),
+ onError = languageErrorCallback.capture(),
+ )
+ languageErrorCallback.value.invoke(Throwable())
+
+ // Never translate site behavior behavior
+ val neverTranslateSiteErrorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engineSession).getNeverTranslateSiteSetting(
+ onResult = any(),
+ onException = neverTranslateSiteErrorCallback.capture(),
+ )
+ neverTranslateSiteErrorCallback.value.invoke(Throwable())
+
+ verify(store).dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ translationError = TranslationError.CouldNotLoadPageSettingsError(any()),
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN UpdatePageSettingAction is dispatched WITH UPDATE_ALWAYS_TRANSLATE_LANGUAGE AND updating the setting is unsuccessful THEN OperationRequestedAction with FETCH_PAGE_SETTINGS is dispatched`() = runTest {
+ // Setup
+ setupMockState()
+ val errorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ whenever(
+ engine.setLanguageSetting(
+ languageCode = any(),
+ languageSetting = any(),
+ onSuccess = any(),
+ onError = errorCallback.capture(),
+ ),
+ ).thenAnswer { errorCallback.value.invoke(Throwable()) }
+
+ // Send Action
+ val action =
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_ALWAYS_TRANSLATE_LANGUAGE,
+ setting = true,
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ waitForIdle()
+
+ // Verify Dispatch
+ verify(store).dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN an Operation to FETCH_AUTOMATIC_LANGUAGE_SETTINGS is dispatched THEN SetLanguageSettingsAction is dispatched`() = runTest {
+ // Send Action
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
+ )
+ translationsMiddleware.invoke(context = context, next = {}, action = action)
+ waitForIdle()
+
+ // Check expectations
+ val languageSettingsCallback = argumentCaptor<((Map<String, LanguageSetting>) -> Unit)>()
+ // Checking atLeastOnce, because InitAction is also implicitly called earlier
+ verify(engine, atLeastOnce()).getLanguageSettings(
+ onSuccess = languageSettingsCallback.capture(),
+ onError = any(),
+ )
+ val mockLanguageSetting = mapOf("en" to LanguageSetting.OFFER)
+ languageSettingsCallback.value.invoke(mockLanguageSetting)
+ waitForIdle()
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.SetLanguageSettingsAction(
+ languageSettings = mockLanguageSetting,
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN an Operation to UpdatePageSettings for UPDATE_ALWAYS_TRANSLATE_LANGUAGE is dispatched THEN SetLanguageSettingsAction is dispatched`() = runTest {
+ // Page settings needs additional setup
+ setupMockState()
+ val pageSettingCallback = argumentCaptor<(() -> Unit)>()
+ whenever(
+ engine.setLanguageSetting(
+ languageCode = any(),
+ languageSetting = any(),
+ onSuccess = pageSettingCallback.capture(),
+ onError = any(),
+ ),
+ ).thenAnswer { pageSettingCallback.value.invoke() }
+
+ // Send Action
+ val action =
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_ALWAYS_TRANSLATE_LANGUAGE,
+ setting = true,
+ )
+ translationsMiddleware.invoke(context = context, next = {}, action = action)
+ waitForIdle()
+
+ // Check expectations
+ val languageSettingsCallback = argumentCaptor<((Map<String, LanguageSetting>) -> Unit)>()
+ verify(engine).getLanguageSettings(
+ onSuccess = languageSettingsCallback.capture(),
+ onError = any(),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN an Operation to FETCH_AUTOMATIC_LANGUAGE_SETTINGS has an error THEN EngineExceptionAction and TranslateExceptionAction are dispatched for language setting`() = runTest() {
+ // Send Action
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
+ )
+ translationsMiddleware.invoke(context = context, next = {}, action = action)
+ waitForIdle()
+
+ // Check expectations
+ val errorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, atLeastOnce()).getLanguageSettings(
+ onSuccess = any(),
+ onError = errorCallback.capture(),
+ )
+ errorCallback.value.invoke(Throwable())
+ waitForIdle()
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.CouldNotLoadLanguageSettingsError(any()),
+ ),
+ )
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
+ translationError = TranslationError.CouldNotLoadLanguageSettingsError(any()),
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN UpdatePageSettingAction is dispatched WITH UPDATE_NEVER_TRANSLATE_LANGUAGE AND updating the setting is unsuccessful THEN OperationRequestedAction with FETCH_PAGE_SETTINGS is dispatched`() = runTest {
+ // Setup
+ setupMockState()
+ val errorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ whenever(
+ engine.setLanguageSetting(
+ languageCode = any(),
+ languageSetting = any(),
+ onSuccess = any(),
+ onError = errorCallback.capture(),
+ ),
+ )
+ .thenAnswer { errorCallback.value.invoke(Throwable()) }
+
+ // Send Action
+ val action =
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_LANGUAGE,
+ setting = true,
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ waitForIdle()
+
+ // Verify Dispatch
+ verify(store).dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN UpdatePageSettingAction is dispatched WITH UPDATE_NEVER_TRANSLATE_SITE AND updating the setting is unsuccessful THEN OperationRequestedAction with FETCH_PAGE_SETTINGS is dispatched`() = runTest {
+ // Setup
+ setupMockState()
+ val errorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ whenever(
+ engineSession.setNeverTranslateSiteSetting(
+ setting = anyBoolean(),
+ onResult = any(),
+ onException = errorCallback.capture(),
+ ),
+ )
+ .thenAnswer { errorCallback.value.invoke(Throwable()) }
+
+ // Send Action
+ val action =
+ TranslationsAction.UpdatePageSettingAction(
+ tabId = tab.id,
+ operation = TranslationPageSettingOperation.UPDATE_NEVER_TRANSLATE_SITE,
+ setting = true,
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ waitForIdle()
+
+ // Verify Dispatch
+ verify(store).dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN OperationRequestedAction is dispatched to fetch never translate sites AND succeeds THEN SetNeverTranslateSitesAction is dispatched`() = runTest {
+ val neverTranslateSites = listOf("google.com")
+ val sitesCallback = argumentCaptor<((List<String>) -> Unit)>()
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ verify(engine).getNeverTranslateSiteList(onSuccess = sitesCallback.capture(), onError = any())
+ sitesCallback.value.invoke(neverTranslateSites)
+
+ verify(context.store).dispatch(
+ TranslationsAction.SetNeverTranslateSitesAction(
+ tabId = tab.id,
+ neverTranslateSites = neverTranslateSites,
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN OperationRequestedAction is dispatched to fetch never translate sites AND fails THEN TranslateExceptionAction is dispatched`() = runTest {
+ store.dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
+ ),
+ ).joinBlocking()
+ waitForIdle()
+
+ verify(store).dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
+ translationError = TranslationError.CouldNotLoadNeverTranslateSites(any()),
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN FetchTranslationDownloadSize is requested AND succeeds THEN SetTranslationDownloadSize is dispatched`() = runTest {
+ val translationSize = TranslationDownloadSize(
+ fromLanguage = Language("en", "English"),
+ toLanguage = Language("fr", "French"),
+ size = 10000L,
+ error = null,
+ )
+
+ val action =
+ TranslationsAction.FetchTranslationDownloadSizeAction(
+ tabId = tab.id,
+ fromLanguage = translationSize.fromLanguage,
+ toLanguage = translationSize.toLanguage,
+ )
+ translationsMiddleware.invoke(context = context, next = {}, action = action)
+
+ val sizeCaptor = argumentCaptor<((Long) -> Unit)>()
+ verify(engine).getTranslationsPairDownloadSize(
+ fromLanguage = any(),
+ toLanguage = any(),
+ onSuccess = sizeCaptor.capture(),
+ onError = any(),
+ )
+ sizeCaptor.value.invoke(translationSize.size!!)
+
+ verify(context.store).dispatch(
+ TranslationsAction.SetTranslationDownloadSizeAction(
+ tabId = tab.id,
+ translationSize = translationSize,
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN FetchTranslationDownloadSize is requested AND fails THEN SetTranslationDownloadSize is dispatched`() = runTest {
+ val action =
+ TranslationsAction.FetchTranslationDownloadSizeAction(
+ tabId = tab.id,
+ fromLanguage = Language("en", "English"),
+ toLanguage = Language("fr", "French"),
+ )
+ translationsMiddleware.invoke(context = context, next = {}, action = action)
+
+ val errorCaptor = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine).getTranslationsPairDownloadSize(
+ fromLanguage = any(),
+ toLanguage = any(),
+ onSuccess = any(),
+ onError = errorCaptor.capture(),
+ )
+ errorCaptor.value.invoke(TranslationError.CouldNotDetermineDownloadSizeError(cause = null))
+
+ verify(context.store).dispatch(
+ TranslationsAction.SetTranslationDownloadSizeAction(
+ tabId = tab.id,
+ translationSize = TranslationDownloadSize(
+ fromLanguage = Language("en", "English"),
+ toLanguage = Language("fr", "French"),
+ size = null,
+ error = any(),
+ ),
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN RemoveNeverTranslateSiteAction is dispatched AND removing is unsuccessful THEN FETCH_NEVER_TRANSLATE_SITES is dispatched`() = runTest {
+ val errorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ whenever(
+ engine.setNeverTranslateSpecifiedSite(
+ origin = any(),
+ setting = anyBoolean(),
+ onSuccess = any(),
+ onError = errorCallback.capture(),
+ ),
+ ).thenAnswer { errorCallback.value.invoke(Throwable()) }
+
+ val action =
+ TranslationsAction.RemoveNeverTranslateSiteAction(
+ tabId = tab.id,
+ origin = "google.com",
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ waitForIdle()
+
+ // Verify Dispatch
+ verify(store).dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_NEVER_TRANSLATE_SITES,
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN RemoveNeverTranslateSiteAction is dispatched AND removing is successful THEN FETCH_PAGE_SETTINGS is dispatched`() = runTest {
+ val sitesCallback = argumentCaptor<(() -> Unit)>()
+ val action =
+ TranslationsAction.RemoveNeverTranslateSiteAction(
+ tabId = tab.id,
+ origin = "google.com",
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ verify(engine).setNeverTranslateSpecifiedSite(
+ origin = any(),
+ setting = anyBoolean(),
+ onSuccess = sitesCallback.capture(),
+ onError = any(),
+ )
+ sitesCallback.value.invoke()
+ waitForIdle()
+
+ // Verify Dispatch
+ verify(store).dispatch(
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_PAGE_SETTINGS,
+ ),
+ )
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN OperationRequestedAction is dispatched to FETCH_LANGUAGE_MODELS AND succeeds THEN SetLanguageModelsAction is dispatched`() = runTest {
+ val languageCallback = argumentCaptor<((List<LanguageModel>) -> Unit)>()
+
+ // Initial Action
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_LANGUAGE_MODELS,
+ )
+ translationsMiddleware.invoke(context, {}, action)
+
+ // Verify results
+ verify(engine, atLeastOnce()).getTranslationsModelDownloadStates(onSuccess = languageCallback.capture(), onError = any())
+ languageCallback.value.invoke(mockLanguageModels)
+
+ verify(context.store, atLeastOnce()).dispatch(
+ TranslationsAction.SetLanguageModelsAction(
+ languageModels = mockLanguageModels,
+ ),
+ )
+
+ waitForIdle()
+ }
+
+ @Test
+ fun `WHEN OperationRequestedAction is dispatched to FETCH_LANGUAGE_MODELS AND fails THEN TranslateExceptionAction is dispatched`() = runTest {
+ val errorCallback = argumentCaptor<((Throwable) -> Unit)>()
+ whenever(
+ engine.getTranslationsModelDownloadStates(
+ onSuccess = any(),
+ onError = errorCallback.capture(),
+ ),
+ ).thenAnswer { errorCallback.value.invoke(Throwable()) }
+
+ val action =
+ TranslationsAction.OperationRequestedAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_LANGUAGE_MODELS,
+ )
+ translationsMiddleware.invoke(context, {}, action)
+ waitForIdle()
+
+ // Verify Dispatch
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.EngineExceptionAction(
+ error = TranslationError.ModelCouldNotRetrieveError(any()),
+ ),
+ )
+
+ verify(store, atLeastOnce()).dispatch(
+ TranslationsAction.TranslateExceptionAction(
+ tabId = tab.id,
+ operation = TranslationOperation.FETCH_LANGUAGE_MODELS,
+ translationError = TranslationError.ModelCouldNotRetrieveError(any()),
+ ),
+ )
+
+ waitForIdle()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt
new file mode 100644
index 0000000000..f0ba85d952
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt
@@ -0,0 +1,257 @@
+/* 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.browser.state.engine.middleware
+
+import android.content.ComponentCallbacks2
+import mozilla.components.browser.state.action.SystemAction
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.EngineState
+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.concept.engine.EngineSessionState
+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.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class TrimMemoryMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ private lateinit var engineSessionReddit: EngineSession
+ private lateinit var engineSessionTheVerge: EngineSession
+ private lateinit var engineSessionTwitch: EngineSession
+ private lateinit var engineSessionGoogleNews: EngineSession
+ private lateinit var engineSessionAmazon: EngineSession
+ private lateinit var engineSessionYouTube: EngineSession
+ private lateinit var engineSessionFacebook: EngineSession
+
+ private lateinit var store: BrowserStore
+
+ private lateinit var engineSessionStateReddit: EngineSessionState
+ private lateinit var engineSessionStateTheVerge: EngineSessionState
+ private lateinit var engineSessionStateTwitch: EngineSessionState
+
+ @Before
+ fun setUp() {
+ engineSessionTheVerge = mock()
+ engineSessionReddit = mock()
+ engineSessionTwitch = mock()
+ engineSessionGoogleNews = mock()
+ engineSessionAmazon = mock()
+ engineSessionYouTube = mock()
+ engineSessionFacebook = mock()
+
+ engineSessionStateReddit = mock()
+ engineSessionStateTheVerge = mock()
+ engineSessionStateTwitch = mock()
+
+ store = BrowserStore(
+ middleware = listOf(
+ TrimMemoryMiddleware(),
+ SuspendMiddleware(scope),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla").copy(
+ lastAccess = 5,
+ ),
+ createTab("https://www.theverge.com/", id = "theverge").copy(
+ engineState = EngineState(
+ engineSession = engineSessionTheVerge,
+ engineSessionState = engineSessionStateTheVerge,
+ engineObserver = mock(),
+ ),
+ lastAccess = 2,
+ ),
+ createTab(
+ "https://www.reddit.com/r/firefox/",
+ id = "reddit",
+ private = true,
+ ).copy(
+ engineState = EngineState(
+ engineSession = engineSessionReddit,
+ engineSessionState = engineSessionStateReddit,
+ engineObserver = mock(),
+ ),
+ lastAccess = 20,
+ ),
+ createTab("https://github.com/", id = "github").copy(
+ lastAccess = 12,
+ ),
+ createTab("https://news.google.com", id = "google-news").copy(
+ engineState = EngineState(engineSessionGoogleNews, engineObserver = mock()),
+ lastAccess = 10,
+ ),
+ createTab("https://www.amazon.com", id = "amazon").copy(
+ engineState = EngineState(engineSessionAmazon, engineObserver = mock()),
+ lastAccess = 4,
+ ),
+ createTab("https://www.youtube.com", id = "youtube").copy(
+ engineState = EngineState(engineSessionYouTube, engineObserver = mock()),
+ lastAccess = 4,
+ ),
+ createTab("https://www.facebook.com", id = "facebook").copy(
+ engineState = EngineState(engineSessionFacebook, engineObserver = mock()),
+ lastAccess = 7,
+ ),
+ ),
+ customTabs = listOf(
+ createCustomTab("https://www.twitch.tv/", id = "twitch").copy(
+ engineState = EngineState(
+ engineSession = engineSessionTwitch,
+ engineSessionState = engineSessionStateTwitch,
+ engineObserver = mock(),
+ ),
+ ),
+ createCustomTab("https://twitter.com/home", id = "twitter"),
+ ),
+ selectedTabId = "reddit",
+ ),
+ )
+ }
+
+ @Test
+ fun `TrimMemoryMiddleware - TRIM_MEMORY_UI_HIDDEN`() {
+ store.dispatch(
+ SystemAction.LowMemoryAction(
+ level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
+ ),
+ ).joinBlocking()
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ store.state.findTab("theverge")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ assertNotNull(engineSessionState)
+ }
+
+ store.state.findTab("reddit")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ assertNotNull(engineSessionState)
+ }
+
+ store.state.findTab("google-news")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ assertNull(engineSessionState)
+ }
+
+ store.state.findTab("amazon")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ assertNull(engineSessionState)
+ }
+
+ store.state.findTab("youtube")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ assertNull(engineSessionState)
+ }
+
+ store.state.findTab("facebook")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ assertNull(engineSessionState)
+ }
+
+ store.state.findCustomTab("twitch")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ assertNotNull(engineSessionState)
+ }
+
+ store.state.findCustomTab("twitter")!!.engineState.apply {
+ assertNull(engineSession)
+ assertNull(engineObserver)
+ assertNull(engineSessionState)
+ }
+
+ verify(engineSessionTheVerge, never()).close()
+ verify(engineSessionReddit, never()).close()
+ verify(engineSessionTwitch, never()).close()
+ }
+
+ @Test
+ fun `TrimMemoryMiddleware - TRIM_MEMORY_RUNNING_CRITICAL`() {
+ store.dispatch(
+ SystemAction.LowMemoryAction(
+ level = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
+ ),
+ ).joinBlocking()
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ store.state.findTab("theverge")!!.engineState.apply {
+ assertNull(engineSession)
+ assertNull(engineObserver)
+ assertNotNull(engineSessionState)
+ }
+
+ store.state.findTab("reddit")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ assertNotNull(engineSessionState)
+ }
+
+ store.state.findTab("google-news")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ }
+
+ store.state.findTab("facebook")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ }
+
+ store.state.findTab("amazon")!!.engineState.apply {
+ assertNotNull(engineSession)
+ assertNotNull(engineObserver)
+ }
+
+ store.state.findTab("youtube")!!.engineState.apply {
+ assertNull(engineSession)
+ assertNull(engineObserver)
+ }
+
+ store.state.findCustomTab("twitch")!!.engineState.apply {
+ assertNull(engineSession)
+ assertNull(engineObserver)
+ assertNotNull(engineSessionState)
+ }
+
+ store.state.findCustomTab("twitter")!!.engineState.apply {
+ assertNull(engineSession)
+ assertNull(engineObserver)
+ assertNull(engineSessionState)
+ }
+
+ verify(engineSessionTheVerge).close()
+ verify(engineSessionYouTube).close()
+ verify(engineSessionTwitch).close()
+
+ verify(engineSessionReddit, never()).close()
+ verify(engineSessionGoogleNews, never()).close()
+ verify(engineSessionFacebook, never()).close()
+ verify(engineSessionAmazon, never()).close()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddlewareTest.kt
new file mode 100644
index 0000000000..db795a62ca
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddlewareTest.kt
@@ -0,0 +1,135 @@
+/* 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.browser.state.engine.middleware
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class WebExtensionMiddlewareTest {
+
+ @Test
+ fun `marks engine session as active when selected`() {
+ val middleware = WebExtensionMiddleware()
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ createTab("https://www.firefox.com", id = "2"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession1: EngineSession = mock()
+ val engineSession2: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+ store.dispatch(EngineAction.LinkEngineSessionAction("2", engineSession2)).joinBlocking()
+
+ assertNull(store.state.activeWebExtensionTabId)
+ verify(engineSession1, never()).markActiveForWebExtensions(anyBoolean())
+ verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean())
+
+ store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking()
+ assertEquals("1", store.state.activeWebExtensionTabId)
+ verify(engineSession1).markActiveForWebExtensions(true)
+ verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean())
+
+ store.dispatch(TabListAction.SelectTabAction("2")).joinBlocking()
+ assertEquals("2", store.state.activeWebExtensionTabId)
+ verify(engineSession1).markActiveForWebExtensions(false)
+ verify(engineSession2).markActiveForWebExtensions(true)
+ }
+
+ @Test
+ fun `marks selected engine session as active when linked`() {
+ val middleware = WebExtensionMiddleware()
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ createTab("https://www.firefox.com", id = "2"),
+ ),
+ selectedTabId = "1",
+ ),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession1: EngineSession = mock()
+ val engineSession2: EngineSession = mock()
+ assertNull(store.state.activeWebExtensionTabId)
+ verify(engineSession1, never()).markActiveForWebExtensions(anyBoolean())
+ verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean())
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+ assertEquals("1", store.state.activeWebExtensionTabId)
+ verify(engineSession1).markActiveForWebExtensions(true)
+ verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean())
+ }
+
+ @Test
+ fun `marks selected engine session as inactive when unlinked`() {
+ val middleware = WebExtensionMiddleware()
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ ),
+ selectedTabId = "1",
+ ),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession1: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+ assertEquals("1", store.state.activeWebExtensionTabId)
+ verify(engineSession1).markActiveForWebExtensions(true)
+
+ store.dispatch(EngineAction.UnlinkEngineSessionAction("1")).joinBlocking()
+ verify(engineSession1).markActiveForWebExtensions(false)
+ }
+
+ @Test
+ fun `marks new selected engine session as active when previous one is removed`() {
+ val middleware = WebExtensionMiddleware()
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ createTab("https://www.firefox.com", id = "2"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ val engineSession1: EngineSession = mock()
+ val engineSession2: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+ store.dispatch(EngineAction.LinkEngineSessionAction("2", engineSession2)).joinBlocking()
+
+ store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking()
+ assertEquals("1", store.state.activeWebExtensionTabId)
+ verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean())
+
+ store.dispatch(TabListAction.RemoveTabAction("1")).joinBlocking()
+ assertEquals("2", store.state.activeWebExtensionTabId)
+ verify(engineSession2).markActiveForWebExtensions(true)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/helper/TargetTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/helper/TargetTest.kt
new file mode 100644
index 0000000000..85051f185f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/helper/TargetTest.kt
@@ -0,0 +1,138 @@
+/* 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.browser.state.helper
+
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class TargetTest {
+ @Test
+ fun lookupInStore() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://www.example.org", id = "example"),
+ createTab("https://theverge.com", id = "theverge", private = true),
+ ),
+ customTabs = listOf(
+ createCustomTab("https://www.reddit.com/r/firefox/", id = "reddit"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ assertEquals(
+ "https://www.mozilla.org",
+ Target.SelectedTab.lookupIn(store)?.content?.url,
+ )
+
+ assertEquals(
+ "https://www.mozilla.org",
+ Target.Tab("mozilla").lookupIn(store)?.content?.url,
+ )
+
+ assertEquals(
+ "https://theverge.com",
+ Target.Tab("theverge").lookupIn(store)?.content?.url,
+ )
+
+ assertNull(
+ Target.Tab("unknown").lookupIn(store),
+ )
+
+ assertNull(
+ Target.Tab("reddit").lookupIn(store),
+ )
+
+ assertEquals(
+ "https://www.reddit.com/r/firefox/",
+ Target.CustomTab("reddit").lookupIn(store)?.content?.url,
+ )
+
+ assertNull(
+ Target.CustomTab("unknown").lookupIn(store),
+ )
+
+ assertNull(
+ Target.CustomTab("mozilla").lookupIn(store),
+ )
+
+ store.dispatch(
+ TabListAction.SelectTabAction("example"),
+ ).joinBlocking()
+
+ assertEquals(
+ "https://www.example.org",
+ Target.SelectedTab.lookupIn(store)?.content?.url,
+ )
+
+ store.dispatch(
+ TabListAction.RemoveAllTabsAction(),
+ ).joinBlocking()
+
+ assertNull(
+ Target.SelectedTab.lookupIn(store),
+ )
+ }
+
+ @Test
+ fun lookupInState() {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://www.example.org", id = "example"),
+ createTab("https://theverge.com", id = "theverge", private = true),
+ ),
+ customTabs = listOf(
+ createCustomTab("https://www.reddit.com/r/firefox/", id = "reddit"),
+ ),
+ selectedTabId = "mozilla",
+ )
+
+ assertEquals(
+ "https://www.mozilla.org",
+ Target.SelectedTab.lookupIn(state)?.content?.url,
+ )
+
+ assertEquals(
+ "https://www.mozilla.org",
+ Target.Tab("mozilla").lookupIn(state)?.content?.url,
+ )
+
+ assertEquals(
+ "https://theverge.com",
+ Target.Tab("theverge").lookupIn(state)?.content?.url,
+ )
+
+ assertNull(
+ Target.Tab("unknown").lookupIn(state),
+ )
+
+ assertNull(
+ Target.Tab("reddit").lookupIn(state),
+ )
+
+ assertEquals(
+ "https://www.reddit.com/r/firefox/",
+ Target.CustomTab("reddit").lookupIn(state)?.content?.url,
+ )
+
+ assertNull(
+ Target.CustomTab("unknown").lookupIn(state),
+ )
+
+ assertNull(
+ Target.CustomTab("mozilla").lookupIn(state),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/BrowserStateReducerKtTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/BrowserStateReducerKtTest.kt
new file mode 100644
index 0000000000..237b340f4e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/BrowserStateReducerKtTest.kt
@@ -0,0 +1,242 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BrowserStateReducerKtTest {
+ private val initialUrl = "https://mozilla.com"
+ private val updatedUrl = "https://firefox.com"
+
+ @Test
+ fun `GIVEN tabs and custom tabs exist WHEN updateTabOrCustomTabState is called with unknown id THEN no tab is updated`() {
+ val normalTab = TabSessionState(id = "tab1", content = ContentState(url = initialUrl))
+ val privateTab = TabSessionState(id = "tab2", content = ContentState(url = initialUrl, private = true))
+ val customTab = CustomTabSessionState(
+ id = "tab3",
+ content = ContentState(url = initialUrl),
+ config = mock(),
+ )
+ var browserState = BrowserState(tabs = listOf(normalTab, privateTab), customTabs = listOf(customTab))
+
+ browserState = browserState.updateTabOrCustomTabState("tab1111") {
+ it.createCopy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.tabs.size)
+ assertEquals(1, browserState.customTabs.size)
+ assertEquals(initialUrl, browserState.tabs[0].content.url)
+ assertEquals(initialUrl, browserState.tabs[1].content.url)
+ assertEquals(initialUrl, browserState.customTabs[0].content.url)
+ }
+
+ @Test
+ fun `GIVEN a normal tab exists WHEN updateTabOrCustomTabState is called with the id of that tab THEN the tab is updated`() {
+ val tab1 = TabSessionState(id = "tab1", content = ContentState(url = initialUrl))
+ val tab2 = TabSessionState(id = "tab2", content = ContentState(url = initialUrl))
+ var browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ browserState = browserState.updateTabOrCustomTabState("tab1") {
+ it.createCopy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.tabs.size)
+ assertEquals(updatedUrl, browserState.tabs[0].content.url)
+ assertEquals(initialUrl, browserState.tabs[1].content.url)
+ }
+
+ @Test
+ fun `GIVEN a private tab exists WHEN updateTabOrCustomTabState is called with the id of that tab THEN the tab is updated`() {
+ val tab1 = TabSessionState(id = "tab1", content = ContentState(url = initialUrl, private = true))
+ val tab2 = TabSessionState(id = "tab2", content = ContentState(url = initialUrl, private = true))
+ var browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ browserState = browserState.updateTabOrCustomTabState("tab1") {
+ it.createCopy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.tabs.size)
+ assertEquals(updatedUrl, browserState.tabs[0].content.url)
+ assertEquals(initialUrl, browserState.tabs[1].content.url)
+ }
+
+ @Test
+ fun `GIVEN a normal custom tab exists WHEN updateTabOrCustomTabState is called with the id of that tab THEN the tab is updated`() {
+ val tab1 = CustomTabSessionState(id = "tab1", content = ContentState(url = initialUrl), config = mock())
+ val tab2 = CustomTabSessionState(id = "tab2", content = ContentState(url = initialUrl), config = mock())
+ var browserState = BrowserState(customTabs = listOf(tab1, tab2))
+
+ browserState = browserState.updateTabOrCustomTabState("tab1") {
+ it.createCopy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.customTabs.size)
+ assertEquals(updatedUrl, browserState.customTabs[0].content.url)
+ assertEquals(initialUrl, browserState.customTabs[1].content.url)
+ }
+
+ @Test
+ fun `GIVEN a private custom tab exists WHEN updateTabOrCustomTabState is called with the id of that tab THEN the tab is updated`() {
+ val tab1 = CustomTabSessionState(
+ id = "tab1",
+ content = ContentState(url = initialUrl, private = true),
+ config = mock(),
+ )
+ val tab2 = CustomTabSessionState(
+ id = "tab2",
+ content = ContentState(url = initialUrl, private = true),
+ config = mock(),
+ )
+ var browserState = BrowserState(customTabs = listOf(tab1, tab2))
+
+ browserState = browserState.updateTabOrCustomTabState("tab2") {
+ it.createCopy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.customTabs.size)
+ assertEquals(initialUrl, browserState.customTabs[0].content.url)
+ assertEquals(updatedUrl, browserState.customTabs[1].content.url)
+ }
+
+ @Test
+ fun `GIVEN tabs and custom tabs exist WHEN updateTabState is called with the id of the custom tab THEN the custom tab is not updated`() {
+ val normalTab = TabSessionState(id = "tab1", content = ContentState(url = initialUrl, private = true))
+ val privateTab = TabSessionState(id = "tab2", content = ContentState(url = initialUrl))
+ val customTab = CustomTabSessionState(
+ id = "tab3",
+ content = ContentState(url = initialUrl),
+ config = mock(),
+ )
+ var browserState = BrowserState(tabs = listOf(normalTab, privateTab), customTabs = listOf(customTab))
+
+ browserState = browserState.updateTabState("tab3") {
+ it.copy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.tabs.size)
+ assertEquals(1, browserState.customTabs.size)
+ assertEquals(initialUrl, browserState.tabs[0].content.url)
+ assertEquals(initialUrl, browserState.tabs[1].content.url)
+ assertEquals(initialUrl, browserState.customTabs[0].content.url)
+ }
+
+ @Test
+ fun `GIVEN a normal tab exists WHEN updateTabState is called with the id of that tab THEN the tab is updated`() {
+ val tab1 = TabSessionState(id = "tab1", content = ContentState(url = initialUrl))
+ val tab2 = TabSessionState(id = "tab2", content = ContentState(url = initialUrl))
+ var browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ browserState = browserState.updateTabState("tab1") {
+ it.copy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.tabs.size)
+ assertEquals(updatedUrl, browserState.tabs[0].content.url)
+ assertEquals(initialUrl, browserState.tabs[1].content.url)
+ }
+
+ @Test
+ fun `GIVEN a private tab exists WHEN updateTabState is called with the id of that tab THEN the tab is updated`() {
+ val tab1 = TabSessionState(id = "tab1", content = ContentState(url = initialUrl, private = true))
+ val tab2 = TabSessionState(id = "tab2", content = ContentState(url = initialUrl, private = true))
+ var browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ browserState = browserState.updateTabState("tab1") {
+ it.copy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.tabs.size)
+ assertEquals(updatedUrl, browserState.tabs[0].content.url)
+ assertEquals(initialUrl, browserState.tabs[1].content.url)
+ }
+
+ @Test
+ fun `GIVEN tabs and custom tabs exist WHEN updateCustomTabState is called with the id of a normal tab THEN no tab is updated`() {
+ val normalTab = TabSessionState(id = "tab1", content = ContentState(url = initialUrl, private = true))
+ val privateTab = TabSessionState(id = "tab2", content = ContentState(url = initialUrl))
+ val customTab = CustomTabSessionState(
+ id = "tab3",
+ content = ContentState(url = initialUrl),
+ config = mock(),
+ )
+ var browserState = BrowserState(tabs = listOf(normalTab, privateTab), customTabs = listOf(customTab))
+
+ browserState = browserState.updateCustomTabState("tab1") {
+ it.copy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.tabs.size)
+ assertEquals(1, browserState.customTabs.size)
+ assertEquals(initialUrl, browserState.tabs[0].content.url)
+ assertEquals(initialUrl, browserState.tabs[1].content.url)
+ assertEquals(initialUrl, browserState.customTabs[0].content.url)
+ }
+
+ @Test
+ fun `GIVEN tabs and custom tabs exist WHEN updateCustomTabState is called with the id of a private tab THEN no tab is updated`() {
+ val normalTab = TabSessionState(id = "tab1", content = ContentState(url = initialUrl, private = true))
+ val privateTab = TabSessionState(id = "tab2", content = ContentState(url = initialUrl))
+ val customTab = CustomTabSessionState(
+ id = "tab3",
+ content = ContentState(url = initialUrl),
+ config = mock(),
+ )
+ var browserState = BrowserState(tabs = listOf(normalTab, privateTab), customTabs = listOf(customTab))
+
+ browserState = browserState.updateCustomTabState("tab2") {
+ it.copy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.tabs.size)
+ assertEquals(1, browserState.customTabs.size)
+ assertEquals(initialUrl, browserState.tabs[0].content.url)
+ assertEquals(initialUrl, browserState.tabs[1].content.url)
+ assertEquals(initialUrl, browserState.customTabs[0].content.url)
+ }
+
+ @Test
+ fun `GIVEN a custom tab exists WHEN updateCustomTabState is called with the id of that tab THEN the tab is updated`() {
+ val tab1 = CustomTabSessionState(id = "tab1", content = ContentState(url = initialUrl), config = mock())
+ val tab2 = CustomTabSessionState(id = "tab2", content = ContentState(url = initialUrl), config = mock())
+ var browserState = BrowserState(customTabs = listOf(tab1, tab2))
+
+ browserState = browserState.updateCustomTabState("tab1") {
+ it.copy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.customTabs.size)
+ assertEquals(updatedUrl, browserState.customTabs[0].content.url)
+ assertEquals(initialUrl, browserState.customTabs[1].content.url)
+ }
+
+ @Test
+ fun `GIVEN a private tab exists WHEN updateCustomTabState is called with the id of that tab THEN the tab is updated`() {
+ val tab1 = CustomTabSessionState(
+ id = "tab1",
+ content = ContentState(url = initialUrl, private = true),
+ config = mock(),
+ )
+ val tab2 = CustomTabSessionState(
+ id = "tab2",
+ content = ContentState(url = initialUrl, private = true),
+ config = mock(),
+ )
+ var browserState = BrowserState(customTabs = listOf(tab1, tab2))
+
+ browserState = browserState.updateCustomTabState("tab1") {
+ it.copy(content = it.content.copy(url = updatedUrl))
+ }
+
+ assertEquals(2, browserState.customTabs.size)
+ assertEquals(updatedUrl, browserState.customTabs[0].content.url)
+ assertEquals(initialUrl, browserState.customTabs[1].content.url)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducerTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducerTest.kt
new file mode 100644
index 0000000000..fa7c642ea2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducerTest.kt
@@ -0,0 +1,59 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.CopyInternetResourceAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class CopyInternetResourceStateReducerTest {
+ @Test
+ fun `reduce - AddCopyAction should add the internetResource in the ContentState`() {
+ val reducer = CopyInternetResourceStateReducer
+ val state = BrowserState(tabs = listOf(TabSessionState("tabId", ContentState("contentStateUrl"))))
+ val response: Response = mock()
+ val action = CopyInternetResourceAction.AddCopyAction(
+ "tabId",
+ ShareInternetResourceState("internetResourceUrl", "type", true, response),
+ )
+
+ assertNull(state.tabs[0].content.copy)
+
+ val result = reducer.reduce(state, action)
+
+ val copyState = result.tabs[0].content.copy!!
+ assertEquals("internetResourceUrl", copyState.url)
+ assertEquals("type", copyState.contentType)
+ assertTrue(copyState.private)
+ assertEquals(response, copyState.response)
+ }
+
+ @Test
+ fun `reduce - ConsumeCopyAction should remove the CopyInternetResourceState ContentState`() {
+ val reducer = CopyInternetResourceStateReducer
+ val shareState: ShareInternetResourceState = mock()
+ val state = BrowserState(
+ tabs = listOf(
+ TabSessionState("tabId", ContentState("contentStateUrl", copy = shareState)),
+ ),
+ )
+ val action = CopyInternetResourceAction.ConsumeCopyAction("tabId")
+
+ assertNotNull(state.tabs[0].content.copy)
+
+ val result = reducer.reduce(state, action)
+
+ assertNull(result.tabs[0].content.copy)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/DebugReducerTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/DebugReducerTest.kt
new file mode 100644
index 0000000000..ae376a3ada
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/DebugReducerTest.kt
@@ -0,0 +1,33 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.DebugAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.lib.state.DelicateAction
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+
+@DelicateAction
+class DebugReducerTest {
+ @Test
+ fun `WHEN the reducer is called for UpdateCreatedAtAction THEN a new state with updated createdAt is returned`() {
+ val tab1 = TabSessionState(id = "tab1", content = mock())
+ val tab2 = TabSessionState(id = "tab2", content = mock())
+ val browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ val updatedState = DebugReducer.reduce(
+ browserState,
+ DebugAction.UpdateCreatedAtAction(tabId = "tab1", createdAt = 345L),
+ )
+
+ assertEquals(2, updatedState.tabs.size)
+ assertEquals(345, updatedState.tabs[0].createdAt)
+ assertNotEquals(345, updatedState.tabs[1].createdAt)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducerTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducerTest.kt
new file mode 100644
index 0000000000..59f4c4dee9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducerTest.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.browser.state.reducer
+
+import mozilla.components.browser.state.action.ExtensionsProcessAction
+import mozilla.components.browser.state.state.BrowserState
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ExtensionsProcessStateReducerTest {
+ @Test
+ fun `GIVEN ShowPromptAction THEN showExtensionsProcessDisabledPrompt is updated`() {
+ var state = BrowserState()
+ assertFalse(state.showExtensionsProcessDisabledPrompt)
+
+ state = ExtensionsProcessStateReducer.reduce(state, ExtensionsProcessAction.ShowPromptAction(show = true))
+ assertTrue(state.showExtensionsProcessDisabledPrompt)
+
+ state = ExtensionsProcessStateReducer.reduce(state, ExtensionsProcessAction.ShowPromptAction(show = false))
+ assertFalse(state.showExtensionsProcessDisabledPrompt)
+ }
+
+ @Test
+ fun `GIVEN EnabledAction THEN extensionsProcessDisabled is set to false`() {
+ var state = BrowserState(extensionsProcessDisabled = true)
+
+ state = ExtensionsProcessStateReducer.reduce(state, ExtensionsProcessAction.EnabledAction)
+ assertFalse(state.extensionsProcessDisabled)
+ }
+
+ @Test
+ fun `GIVEN DisabledAction THEN extensionsProcessDisabled is set to true`() {
+ var state = BrowserState(extensionsProcessDisabled = false)
+
+ state = ExtensionsProcessStateReducer.reduce(state, ExtensionsProcessAction.DisabledAction)
+ assertTrue(state.extensionsProcessDisabled)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtilsTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtilsTest.kt
new file mode 100644
index 0000000000..2b1db1f4d5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtilsTest.kt
@@ -0,0 +1,26 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.TabSessionState
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+
+class InternetResourceReducerUtilsTest {
+ @Test
+ fun `updateTheContentState will return a new BrowserState with updated ContentState`() {
+ val initialContentState = ContentState("emptyStateUrl")
+ val browserState =
+ BrowserState(tabs = listOf(TabSessionState("tabId", initialContentState)))
+
+ val result = updateTheContentState(browserState, "tabId") { it.copy(url = "updatedUrl") }
+
+ assertFalse(browserState == result)
+ assertEquals("updatedUrl", result.tabs[0].content.url)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LastAccessReducerTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LastAccessReducerTest.kt
new file mode 100644
index 0000000000..796c1b8302
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LastAccessReducerTest.kt
@@ -0,0 +1,128 @@
+/* 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.browser.state.reducer
+
+import mozilla.components.browser.state.action.LastAccessAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.LastMediaAccessState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LastAccessReducerTest {
+ @Test
+ fun `WHEN the reducer is called for UpdateLastAccessAction THEN a new state with updated lastAccess is returned`() {
+ val tab1 = TabSessionState(id = "tab1", lastAccess = 111, content = mock())
+ val tab2 = TabSessionState(id = "tab2", lastAccess = 222, content = mock())
+ val browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ val updatedState = LastAccessReducer.reduce(
+ browserState,
+ LastAccessAction.UpdateLastAccessAction(tabId = "tab1", lastAccess = 345),
+ )
+
+ assertEquals(2, updatedState.tabs.size)
+ assertEquals(345, updatedState.tabs[0].lastAccess)
+ assertEquals(222, updatedState.tabs[1].lastAccess)
+ }
+
+ @Test
+ fun `WHEN the reducer is called for UpdateLastMediaAccessAction THEN a new state with updated LastMediaAccessState is returned`() {
+ val tab1 = TabSessionState(id = "tab1", content = ContentState("https://mozilla.org"))
+ val tab2 = TabSessionState(id = "tab2", content = mock())
+ val browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ val updatedState = LastAccessReducer.reduce(
+ browserState,
+ LastAccessAction.UpdateLastMediaAccessAction(tabId = "tab1", lastMediaAccess = 345),
+ )
+
+ assertEquals(2, updatedState.tabs.size)
+ assertEquals(345, updatedState.tabs[0].lastMediaAccessState.lastMediaAccess)
+ assertTrue(updatedState.tabs[0].lastMediaAccessState.mediaSessionActive)
+ assertEquals("https://mozilla.org", updatedState.tabs[0].lastMediaAccessState.lastMediaUrl)
+ assertEquals(0, updatedState.tabs[1].lastMediaAccessState.lastMediaAccess)
+ assertEquals("", updatedState.tabs[1].lastMediaAccessState.lastMediaUrl)
+ assertFalse(updatedState.tabs[1].lastMediaAccessState.mediaSessionActive)
+ }
+
+ @Test
+ fun `WHEN the reducer is called for UpdateLastMediaAccessAction on a custom tab THEN the BrowserState is not updated`() {
+ val normalTab = TabSessionState(id = "tab1", content = ContentState(url = "https:mozilla.org"))
+ val privateTab = TabSessionState(id = "tab2", content = ContentState(url = "https:mozilla.org", private = true))
+ val customTab = CustomTabSessionState(
+ id = "tab3",
+ content = ContentState(url = "https://mozilla.org"),
+ config = mock(),
+ )
+ val browserState = BrowserState(tabs = listOf(normalTab, privateTab), customTabs = listOf(customTab))
+
+ val updatedState = LastAccessReducer.reduce(
+ browserState,
+ LastAccessAction.UpdateLastMediaAccessAction(tabId = "tab3", lastMediaAccess = 345),
+ )
+
+ assertEquals(2, updatedState.tabs.size)
+ assertEquals(1, updatedState.customTabs.size)
+ assertEquals(normalTab, updatedState.tabs[0])
+ assertEquals(privateTab, updatedState.tabs[1])
+ assertEquals(customTab, updatedState.customTabs[0])
+ }
+
+ @Test
+ fun `WHEN the reducer is called for ResetLastMediaSessionAction THEN a new state with a false mediaSessionActive is returned`() {
+ val tab1 = TabSessionState(id = "tab1", content = mock())
+ val tab2 = TabSessionState(
+ id = "tab2",
+ content = mock(),
+ lastMediaAccessState = LastMediaAccessState("https://mozilla.org", 222, true),
+ )
+ val browserState = BrowserState(tabs = listOf(tab1, tab2))
+
+ val updatedState = LastAccessReducer.reduce(
+ browserState,
+ LastAccessAction.ResetLastMediaSessionAction(tabId = "tab2"),
+ )
+
+ assertEquals(2, updatedState.tabs.size)
+ assertEquals(0, updatedState.tabs[0].lastMediaAccessState.lastMediaAccess)
+ assertEquals("", updatedState.tabs[0].lastMediaAccessState.lastMediaUrl)
+ assertEquals(222, updatedState.tabs[1].lastMediaAccessState.lastMediaAccess)
+ assertEquals("https://mozilla.org", updatedState.tabs[1].lastMediaAccessState.lastMediaUrl)
+ assertFalse(updatedState.tabs[1].lastMediaAccessState.mediaSessionActive)
+ }
+
+ @Test
+ fun `WHEN the reducer is called for ResetLastMediaSessionAction on a custom tab THEN the BrowserState is not updated`() {
+ val normalTab = TabSessionState(
+ id = "tab2",
+ content = mock(),
+ lastMediaAccessState = LastMediaAccessState("https://mozilla.org", 222, true),
+ )
+ val privateTab = TabSessionState(id = "tab2", content = ContentState(url = "https:mozilla.org", private = true))
+ val customTab = CustomTabSessionState(
+ id = "tab3",
+ content = ContentState(url = "https://mozilla.org"),
+ config = mock(),
+ )
+ val browserState = BrowserState(tabs = listOf(normalTab, privateTab), customTabs = listOf(customTab))
+
+ val updatedState = LastAccessReducer.reduce(
+ browserState,
+ LastAccessAction.UpdateLastMediaAccessAction(tabId = "tab3", lastMediaAccess = 345),
+ )
+
+ assertEquals(2, updatedState.tabs.size)
+ assertEquals(1, updatedState.customTabs.size)
+ assertEquals(normalTab, updatedState.tabs[0])
+ assertEquals(privateTab, updatedState.tabs[1])
+ assertEquals(customTab, updatedState.customTabs[0])
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LocaleStateReducerTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LocaleStateReducerTest.kt
new file mode 100644
index 0000000000..3ec02fe0b3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LocaleStateReducerTest.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.browser.state.reducer
+
+import mozilla.components.browser.state.action.LocaleAction
+import mozilla.components.browser.state.state.BrowserState
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import java.util.Locale
+
+class LocaleStateReducerTest {
+ @Test
+ fun `WHEN updating locale action is called THEN the locale state is updated`() {
+ val reducer = LocaleStateReducer
+ val state = BrowserState()
+ val locale = Locale("es")
+ val action = LocaleAction.UpdateLocaleAction(locale)
+
+ assertNull(state.locale)
+
+ val result = reducer.reduce(state, action)
+
+ assertEquals(result.locale, locale)
+ }
+
+ @Test
+ fun `WHEN restoring locale action is called THEN the locale state is persisted`() {
+ val reducer = LocaleStateReducer
+ val state = BrowserState()
+ val action = LocaleAction.RestoreLocaleStateAction
+
+ assertNull(state.locale)
+
+ val result = reducer.reduce(state, action)
+
+ assertNull(result.locale)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducerTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducerTest.kt
new file mode 100644
index 0000000000..0124e3dadf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducerTest.kt
@@ -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/. */
+
+package mozilla.components.browser.state.reducer
+
+import mozilla.components.browser.state.action.ShareInternetResourceAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ShareInternetResourceStateReducerTest {
+
+ @Test
+ fun `reduce - AddShareAction should add the internetResource in the ContentState`() {
+ val reducer = ShareInternetResourceStateReducer
+ val state = BrowserState(tabs = listOf(TabSessionState("tabId", ContentState("contentStateUrl"))))
+ val response: Response = mock()
+ val action = ShareInternetResourceAction.AddShareAction(
+ "tabId",
+ ShareInternetResourceState("internetResourceUrl", "type", true, response),
+ )
+
+ assertNull(state.tabs[0].content.share)
+
+ val result = reducer.reduce(state, action)
+
+ val shareState = result.tabs[0].content.share!!
+ assertEquals("internetResourceUrl", shareState.url)
+ assertEquals("type", shareState.contentType)
+ assertTrue(shareState.private)
+ assertEquals(response, shareState.response)
+ }
+
+ @Test
+ fun `reduce - ConsumeShareAction should remove the ShareInternetResourceState ContentState`() {
+ val reducer = ShareInternetResourceStateReducer
+ val shareState: ShareInternetResourceState = mock()
+ val state = BrowserState(
+ tabs = listOf(
+ TabSessionState("tabId", ContentState("contentStateUrl", share = shareState)),
+ ),
+ )
+ val action = ShareInternetResourceAction.ConsumeShareAction("tabId")
+
+ assertNotNull(state.tabs[0].content.share)
+
+ val result = reducer.reduce(state, action)
+
+ assertNull(result.tabs[0].content.share)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/selector/SelectorsKtTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/selector/SelectorsKtTest.kt
new file mode 100644
index 0000000000..63400839fb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/selector/SelectorsKtTest.kt
@@ -0,0 +1,306 @@
+/* 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.browser.state.selector
+
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class SelectorsKtTest {
+ @Test
+ fun `selectedTab extension property`() {
+ val store = BrowserStore()
+ val tabLastAccessTimeStamp = 123L
+
+ assertNull(store.state.selectedTab)
+
+ store.dispatch(
+ CustomTabListAction.AddCustomTabAction(createCustomTab("https://www.mozilla.org")),
+ ).joinBlocking()
+
+ assertNull(store.state.selectedTab)
+
+ val tab = createTab("https://www.firefox.com")
+ store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking()
+
+ assertEquals(tab, store.state.selectedTab)
+
+ val otherTab = createTab("https://getpocket.com", lastAccess = tabLastAccessTimeStamp)
+ store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
+
+ assertEquals(tab, store.state.selectedTab)
+
+ store.dispatch(TabListAction.SelectTabAction(otherTab.id)).joinBlocking()
+
+ assertEquals(otherTab, store.state.selectedTab)
+ }
+
+ @Test
+ fun `selectedNormalTab extension property`() {
+ val store = BrowserStore()
+ val tabLastAccessTimeStamp = 123L
+ assertNull(store.state.selectedNormalTab)
+
+ store.dispatch(
+ CustomTabListAction.AddCustomTabAction(createCustomTab("https://www.mozilla.org")),
+ ).joinBlocking()
+
+ assertNull(store.state.selectedNormalTab)
+
+ val privateTab = createTab("https://www.firefox.com", private = true)
+ store.dispatch(TabListAction.AddTabAction(privateTab, select = true)).joinBlocking()
+ assertNull(store.state.selectedNormalTab)
+
+ val normalTab = createTab("https://getpocket.com", lastAccess = tabLastAccessTimeStamp)
+ store.dispatch(TabListAction.AddTabAction(normalTab)).joinBlocking()
+ assertNull(store.state.selectedNormalTab)
+
+ store.dispatch(TabListAction.SelectTabAction(normalTab.id)).joinBlocking()
+ assertEquals(normalTab, store.state.selectedNormalTab)
+ }
+
+ @Test
+ fun `selectedTab extension property - ignores unknown id`() {
+ val state = BrowserState(
+ selectedTabId = "no valid id",
+ )
+
+ assertNull(state.selectedTab)
+ }
+
+ @Test
+ fun `selectedNormalTab extension property - ignores unknown id`() {
+ val state = BrowserState(
+ selectedTabId = "no valid id",
+ )
+
+ assertNull(state.selectedNormalTab)
+ }
+
+ @Test
+ fun `findTab extension function`() {
+ val tab = createTab("https://www.firefox.com")
+ val otherTab = createTab("https://getpocket.com")
+ val customTab = createCustomTab("https://www.mozilla.org")
+
+ val state = BrowserState(
+ tabs = listOf(tab, otherTab),
+ customTabs = listOf(customTab),
+ )
+
+ assertEquals(tab, state.findTab(tab.id))
+ assertEquals(otherTab, state.findTab(otherTab.id))
+ assertNull(state.findTab(customTab.id))
+ }
+
+ @Test
+ fun `findNormalTab extension function`() {
+ val privateTab = createTab("https://www.firefox.com", private = true)
+ val normalTab = createTab("https://getpocket.com")
+ val customTab = createCustomTab("https://www.mozilla.org")
+
+ val state = BrowserState(
+ tabs = listOf(privateTab, normalTab),
+ customTabs = listOf(customTab),
+ )
+
+ assertEquals(normalTab, state.findNormalTab(normalTab.id))
+ assertNull(state.findNormalTab(privateTab.id))
+ assertNull(state.findNormalTab(customTab.id))
+ }
+
+ @Test
+ fun `findTabCustomTab extension function`() {
+ val tab = createTab("https://www.firefox.com")
+ val otherTab = createTab("https://getpocket.com")
+ val customTab = createCustomTab("https://www.mozilla.org")
+
+ val state = BrowserState(
+ tabs = listOf(tab, otherTab),
+ customTabs = listOf(customTab),
+ )
+
+ assertNull(state.findCustomTab(tab.id))
+ assertNull(state.findCustomTab(otherTab.id))
+ assertEquals(customTab, state.findCustomTab(customTab.id))
+ }
+
+ @Test
+ fun `findCustomTabOrSelectedTab extension function`() {
+ val tab = createTab("https://www.firefox.com")
+ val otherTab = createTab("https://getpocket.com")
+ val customTab = createCustomTab("https://www.mozilla.org")
+
+ val state = BrowserState(
+ tabs = listOf(tab, otherTab),
+ customTabs = listOf(customTab),
+ selectedTabId = tab.id,
+ )
+
+ assertEquals(tab, state.findCustomTabOrSelectedTab())
+ assertEquals(tab, state.findCustomTabOrSelectedTab(null))
+ assertEquals(customTab, state.findCustomTabOrSelectedTab(customTab.id))
+ assertNull(state.findCustomTabOrSelectedTab(tab.id))
+ assertNull(state.findCustomTabOrSelectedTab(otherTab.id))
+ }
+
+ @Test
+ fun `findTabOrCustomTabOrSelectedTab extension function`() {
+ val tab = createTab("https://www.firefox.com")
+ val otherTab = createTab("https://getpocket.com")
+ val customTab = createCustomTab("https://www.mozilla.org")
+
+ val state = BrowserState(
+ tabs = listOf(tab, otherTab),
+ customTabs = listOf(customTab),
+ selectedTabId = tab.id,
+ )
+
+ assertEquals(tab, state.findTabOrCustomTabOrSelectedTab())
+ assertEquals(tab, state.findTabOrCustomTabOrSelectedTab(null))
+ assertEquals(tab, state.findTabOrCustomTabOrSelectedTab(tab.id))
+ assertEquals(otherTab, state.findTabOrCustomTabOrSelectedTab(otherTab.id))
+ assertEquals(customTab, state.findTabOrCustomTabOrSelectedTab(customTab.id))
+ }
+
+ @Test
+ fun `getNormalOrPrivateTabs extension function`() {
+ val tab1 = createTab("https://www.firefox.com")
+ val tab2 = createTab("https://www.mozilla.org")
+ val privateTab1 = createTab("https://getpocket.com", private = true)
+ val privateTab2 = createTab("https://www.example.org", private = true)
+
+ val state = BrowserState(
+ tabs = listOf(tab1, privateTab1, tab2, privateTab2),
+ customTabs = listOf(createCustomTab("https://www.google.com")),
+ )
+
+ assertEquals(listOf(tab1, tab2), state.getNormalOrPrivateTabs(private = false))
+ assertEquals(listOf(privateTab1, privateTab2), state.getNormalOrPrivateTabs(private = true))
+
+ assertEquals(emptyList<TabSessionState>(), BrowserState().getNormalOrPrivateTabs(private = true))
+ assertEquals(emptyList<TabSessionState>(), BrowserState().getNormalOrPrivateTabs(private = false))
+ }
+
+ @Test
+ fun `privateTabs and normalTabs extension properties`() {
+ val tab1 = createTab("https://www.firefox.com")
+ val tab2 = createTab("https://www.mozilla.org")
+ val privateTab1 = createTab("https://getpocket.com", private = true)
+ val privateTab2 = createTab("https://www.example.org", private = true)
+
+ val state = BrowserState(
+ tabs = listOf(tab1, privateTab1, tab2, privateTab2),
+ customTabs = listOf(createCustomTab("https://www.google.com")),
+ )
+
+ assertEquals(listOf(tab1, tab2), state.normalTabs)
+ assertEquals(listOf(privateTab1, privateTab2), state.privateTabs)
+
+ assertEquals(emptyList<TabSessionState>(), BrowserState().normalTabs)
+ assertEquals(emptyList<TabSessionState>(), BrowserState().privateTabs)
+ }
+
+ @Test
+ fun `findTabOrCustomTab finds normal and custom tabs`() {
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "test-id")),
+ ).also { state ->
+ assertNotNull(state.findTabOrCustomTab("test-id"))
+ assertEquals(
+ "https://www.mozilla.org",
+ state.findTabOrCustomTab("test-id")!!.content.url,
+ )
+ }
+
+ BrowserState(
+ customTabs = listOf(createCustomTab("https://www.mozilla.org", id = "test-id")),
+ ).also { state ->
+ assertNotNull(state.findTabOrCustomTab("test-id"))
+ assertEquals(
+ "https://www.mozilla.org",
+ state.findTabOrCustomTab("test-id")!!.content.url,
+ )
+ }
+ }
+
+ @Test
+ fun `findNormalOrPrivateTabByUrl finds a matching normal tab`() {
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "test-id", private = false)),
+ ).also { state ->
+ assertNotNull(state.findNormalOrPrivateTabByUrl("https://www.mozilla.org", false))
+ assertEquals(
+ "https://www.mozilla.org",
+ state.findNormalOrPrivateTabByUrl("https://www.mozilla.org", false)!!.content.url,
+ )
+ assertEquals(
+ "test-id",
+ state.findNormalOrPrivateTabByUrl("https://www.mozilla.org", false)!!.id,
+ )
+ }
+ }
+
+ @Test
+ fun `findNormalOrPrivateTabByUrl finds no matching normal tab`() {
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "test-id", private = true)),
+ ).also { state ->
+ assertNull(state.findNormalOrPrivateTabByUrl("https://www.mozilla.org", false))
+ }
+ }
+
+ @Test
+ fun `findNormalOrPrivateTabByUrl finds a matching private tab`() {
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "test-id", private = true)),
+ ).also { state ->
+ assertNotNull(state.findNormalOrPrivateTabByUrl("https://www.mozilla.org", true))
+ assertEquals(
+ "https://www.mozilla.org",
+ state.findNormalOrPrivateTabByUrl("https://www.mozilla.org", true)!!.content.url,
+ )
+ assertEquals(
+ "test-id",
+ state.findNormalOrPrivateTabByUrl("https://www.mozilla.org", true)!!.id,
+ )
+ }
+ }
+
+ @Test
+ fun `findNormalOrPrivateTabByUrl finds no matching private tab`() {
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "test-id", private = false)),
+ ).also { state ->
+ assertNull(state.findNormalOrPrivateTabByUrl("https://www.mozilla.org", true))
+ }
+ }
+
+ @Test
+ fun `findNormalOrPrivateTabByUrlIgnoringFragment extension function`() {
+ val tab1 = createTab("https://www.firefox.com/query?isMorning=yes#hello", private = false)
+ val tab2 = createTab("moz-extension://4d1a24b3-bdd1-4763-a766-b5a8c1a0012c/dashboard.html#settings.html", private = false)
+ val privateTab1 = createTab("https://getpocket.com", private = true)
+ val privateTab2 = createTab("https://mozilla.org", private = true)
+ val state = BrowserState(tabs = listOf(tab1, privateTab1, tab2, privateTab2))
+
+ assertEquals(tab1, state.findNormalOrPrivateTabByUrlIgnoringFragment("https://www.firefox.com/query?isMorning=yes", private = false))
+ assertEquals(tab1, state.findNormalOrPrivateTabByUrlIgnoringFragment("https://www.firefox.com/query?isMorning=yes#bye", private = false))
+ assertEquals(tab2, state.findNormalOrPrivateTabByUrlIgnoringFragment("moz-extension://4d1a24b3-bdd1-4763-a766-b5a8c1a0012c/dashboard.html", private = false))
+ assertEquals(privateTab2, state.findNormalOrPrivateTabByUrlIgnoringFragment("https://mozilla.org/", private = true))
+ assertNull(state.findNormalOrPrivateTabByUrlIgnoringFragment("https://firefox.com/query?isMorning=yes", private = false))
+ // This asserts that the function doesn't throw if an illegal url is checked
+ assertNull(state.findNormalOrPrivateTabByUrlIgnoringFragment("https://getpocket.com/#/private#now", private = true))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/LanguageSettingTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/LanguageSettingTest.kt
new file mode 100644
index 0000000000..0d565cbb2f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/LanguageSettingTest.kt
@@ -0,0 +1,85 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.translate.LanguageSetting.ALWAYS
+import mozilla.components.concept.engine.translate.LanguageSetting.NEVER
+import mozilla.components.concept.engine.translate.LanguageSetting.OFFER
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LanguageSettingTest {
+ @Test
+ fun `GIVEN an OFFER LanguageSetting THEN find its Boolean counterpart`() {
+ val isAlways = OFFER.toBoolean(categoryToSetFor = ALWAYS)
+ assertFalse(isAlways!!)
+
+ val isOffer = OFFER.toBoolean(categoryToSetFor = OFFER)
+ assertTrue(isOffer!!)
+
+ val isNever = OFFER.toBoolean(categoryToSetFor = NEVER)
+ assertFalse(isNever!!)
+ }
+
+ @Test
+ fun `GIVEN an ALWAYS LanguageSetting THEN find its Boolean counterpart`() {
+ val isAlways = ALWAYS.toBoolean(categoryToSetFor = ALWAYS)
+ assertTrue(isAlways!!)
+
+ val isOffer = ALWAYS.toBoolean(categoryToSetFor = OFFER)
+ assertNull(isOffer)
+
+ val isNever = ALWAYS.toBoolean(categoryToSetFor = NEVER)
+ assertFalse(isNever!!)
+ }
+
+ @Test
+ fun `GIVEN a NEVER LanguageSetting THEN find its Boolean counterpart`() {
+ val isAlways = NEVER.toBoolean(categoryToSetFor = ALWAYS)
+ assertFalse(isAlways!!)
+
+ val isOffer = NEVER.toBoolean(categoryToSetFor = OFFER)
+ assertNull(isOffer)
+
+ val isNever = NEVER.toBoolean(categoryToSetFor = NEVER)
+ assertTrue(isNever!!)
+ }
+
+ @Test
+ fun `GIVEN a Boolean corresponding to Always THEN find its LanguageSetting counterpart`() {
+ var isAlways = true
+ var conversion = ALWAYS.toLanguageSetting(value = isAlways)
+ assertEquals(conversion, ALWAYS)
+
+ isAlways = false
+ conversion = ALWAYS.toLanguageSetting(value = isAlways)
+ assertEquals(conversion, OFFER)
+ }
+
+ @Test
+ fun `GIVEN a Boolean corresponding to Never THEN find its LanguageSetting counterpart`() {
+ var isNever = true
+ var conversion = NEVER.toLanguageSetting(value = isNever)
+ assertEquals(conversion, NEVER)
+
+ isNever = false
+ conversion = NEVER.toLanguageSetting(value = isNever)
+ assertEquals(conversion, OFFER)
+ }
+
+ @Test
+ fun `GIVEN a Boolean corresponding to Offer THEN find its LanguageSetting counterpart`() {
+ var isOffer = true
+ var conversion = OFFER.toLanguageSetting(value = isOffer)
+ assertEquals(conversion, OFFER)
+
+ isOffer = false
+ conversion = OFFER.toLanguageSetting(value = isOffer)
+ assertNull(conversion)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt
new file mode 100644
index 0000000000..757802f1e1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt
@@ -0,0 +1,248 @@
+/* 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.browser.state.state
+
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class SearchStateTest {
+ @Test
+ fun `selectedOrDefaultSearchEngine - selects engine by user selected search engine id`() {
+ val state = SearchState(
+ region = RegionState("US", "US"),
+ regionSearchEngines = listOf(
+ SearchEngine("engine-a", "Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-b", "Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-c", "Engine C", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ customSearchEngines = listOf(
+ SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
+ SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM),
+ ),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
+ additionalSearchEngines = listOf(
+ SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ additionalAvailableSearchEngines = listOf(
+ SearchEngine("engine-g", "Engine G", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ SearchEngine("engine-h", "Engine H", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine("engine-i", "Engine I", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ regionDefaultSearchEngineId = "engine-b",
+ userSelectedSearchEngineId = "engine-e",
+ userSelectedSearchEngineName = "Engine H", // Purposefully using name of other engine here
+ )
+
+ val searchEngine = state.selectedOrDefaultSearchEngine
+ assertNotNull(searchEngine!!)
+ assertEquals("engine-e", searchEngine.id)
+ assertEquals("Engine E", searchEngine.name)
+ }
+
+ @Test
+ fun `selectedOrDefaultSearchEngine - selects engine by user selected search engine name`() {
+ val state = SearchState(
+ region = RegionState("US", "US"),
+ regionSearchEngines = listOf(
+ SearchEngine("engine-a", "Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-b", "Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-c", "Engine C", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ customSearchEngines = listOf(
+ SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
+ SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM),
+ ),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
+ additionalSearchEngines = listOf(
+ SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ additionalAvailableSearchEngines = listOf(
+ SearchEngine("engine-g", "Engine G", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ SearchEngine("engine-h", "Engine H", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine("engine-i", "Engine I", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ regionDefaultSearchEngineId = "engine-b",
+ userSelectedSearchEngineId = "engine-x",
+ userSelectedSearchEngineName = "Engine D",
+ )
+
+ val searchEngine = state.selectedOrDefaultSearchEngine
+ assertNotNull(searchEngine!!)
+ assertEquals("engine-d", searchEngine.id)
+ assertEquals("Engine D", searchEngine.name)
+ }
+
+ @Test
+ fun `selectedOrDefaultSearchEngine - uses region default if user has made no choice`() {
+ val state = SearchState(
+ region = RegionState("US", "US"),
+ regionSearchEngines = listOf(
+ SearchEngine("engine-a", "Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-b", "Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-c", "Engine C", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ customSearchEngines = listOf(
+ SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
+ SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM),
+ ),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
+ additionalSearchEngines = listOf(
+ SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ additionalAvailableSearchEngines = listOf(
+ SearchEngine("engine-g", "Engine G", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ SearchEngine("engine-h", "Engine H", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine("engine-i", "Engine I", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ regionDefaultSearchEngineId = "engine-b",
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ )
+
+ val searchEngine = state.selectedOrDefaultSearchEngine
+ assertNotNull(searchEngine!!)
+ assertEquals("engine-b", searchEngine.id)
+ assertEquals("Engine B", searchEngine.name)
+ }
+
+ @Test
+ fun `selectedOrDefaultSearchEngine - fallback - use first region engine`() {
+ val state = SearchState(
+ region = RegionState("US", "US"),
+ regionSearchEngines = listOf(
+ SearchEngine("engine-a", "Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-b", "Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-c", "Engine C", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ customSearchEngines = listOf(
+ SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
+ SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM),
+ ),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
+ additionalSearchEngines = listOf(
+ SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ additionalAvailableSearchEngines = listOf(
+ SearchEngine("engine-g", "Engine G", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ SearchEngine("engine-h", "Engine H", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine("engine-i", "Engine I", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ regionDefaultSearchEngineId = null,
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ )
+
+ val searchEngine = state.selectedOrDefaultSearchEngine
+ assertNotNull(searchEngine!!)
+ assertEquals("engine-a", searchEngine.id)
+ assertEquals("Engine A", searchEngine.name)
+ }
+
+ @Test
+ fun `selectedOrDefaultSearchEngine - is null by default`() {
+ val state = SearchState()
+ assertNull(state.selectedOrDefaultSearchEngine)
+ }
+
+ @Test
+ fun `searchEngines - combines lists`() {
+ val state = SearchState(
+ region = RegionState("US", "US"),
+ regionSearchEngines = listOf(
+ SearchEngine("engine-a", "Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-b", "Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-c", "Engine C", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ customSearchEngines = listOf(
+ SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
+ SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM),
+ ),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
+ additionalSearchEngines = listOf(
+ SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ additionalAvailableSearchEngines = listOf(
+ SearchEngine("engine-g", "Engine G", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ SearchEngine("engine-h", "Engine H", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine("engine-i", "Engine I", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ regionDefaultSearchEngineId = null,
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ )
+
+ val searchEngines = state.searchEngines
+ assertEquals(7, searchEngines.size)
+ assertEquals("engine-a", searchEngines[0].id)
+ assertEquals("engine-b", searchEngines[1].id)
+ assertEquals("engine-c", searchEngines[2].id)
+ assertEquals("engine-f", searchEngines[3].id)
+ assertEquals("engine-d", searchEngines[4].id)
+ assertEquals("engine-e", searchEngines[5].id)
+ }
+
+ @Test
+ fun `availableSearchEngines - combines lists`() {
+ val state = SearchState(
+ region = RegionState("US", "US"),
+ regionSearchEngines = listOf(
+ SearchEngine("engine-a", "Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-b", "Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-c", "Engine C", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ customSearchEngines = listOf(
+ SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
+ SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM),
+ ),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
+ additionalSearchEngines = listOf(
+ SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ additionalAvailableSearchEngines = listOf(
+ SearchEngine("engine-g", "Engine G", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ SearchEngine("engine-h", "Engine H", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ hiddenSearchEngines = listOf(
+ SearchEngine("engine-i", "Engine I", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ regionDefaultSearchEngineId = null,
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ )
+
+ val available = state.availableSearchEngines
+ assertEquals(3, available.size)
+ assertEquals("engine-i", available[0].id)
+ assertEquals("engine-g", available[1].id)
+ assertEquals("engine-h", available[2].id)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TabPartitionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TabPartitionTest.kt
new file mode 100644
index 0000000000..201e4d30e7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TabPartitionTest.kt
@@ -0,0 +1,65 @@
+/* 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.browser.state.state
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class TabPartitionTest {
+
+ @Test
+ fun `GIVEN a null tab partition THEN tab partition is empty`() {
+ val tabPartition: TabPartition? = null
+
+ assertTrue(tabPartition.isEmpty())
+ assertFalse(tabPartition.isNotEmpty())
+ }
+
+ @Test
+ fun `GIVEN a tab partition with no tab group THEN tab partition is empty`() {
+ val tabPartition = TabPartition("test")
+
+ assertTrue(tabPartition.isEmpty())
+ assertFalse(tabPartition.isNotEmpty())
+ }
+
+ @Test
+ fun `GIVEN a tab partition with empty tab groups THEN tab partition is empty`() {
+ val tabPartition = TabPartition("test", listOf(TabGroup(), TabGroup()))
+
+ assertTrue(tabPartition.isEmpty())
+ assertFalse(tabPartition.isNotEmpty())
+ }
+
+ @Test
+ fun `GIVEN a tab partition with non-empty tab group THEN tab partition is not empty`() {
+ val tabPartition = TabPartition("test", listOf(TabGroup("test", "test", listOf("tab1"))))
+
+ assertTrue(tabPartition.isNotEmpty())
+ assertFalse(tabPartition.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN a tab partition with non-empty tab group THEN get group by name will return the group`() {
+ val tabPartition = TabPartition("test", listOf(TabGroup("test id", "abc", listOf("tab1", "tab2"))))
+
+ assertTrue(tabPartition.getGroupByName("abc") != null)
+ assertEquals(listOf("tab1", "tab2"), tabPartition.getGroupByName("abc")?.tabIds)
+ assertTrue(tabPartition.isNotEmpty())
+ assertFalse(tabPartition.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN a tab partition with non-empty tab group THEN get group by ID will return the group`() {
+ val tabPartition = TabPartition("test", listOf(TabGroup("test id", "abc", listOf("tab1", "tab2"))))
+
+ assertTrue(tabPartition.getGroupById("test id") != null)
+ assertEquals(listOf("tab1", "tab2"), tabPartition.getGroupById("test id")?.tabIds)
+ assertTrue(tabPartition.isNotEmpty())
+ assertFalse(tabPartition.isEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationEngineStateTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationEngineStateTest.kt
new file mode 100644
index 0000000000..5c45d82966
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationEngineStateTest.kt
@@ -0,0 +1,123 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.translate.DetectedLanguages
+import mozilla.components.concept.engine.translate.Language
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.concept.engine.translate.TranslationPair
+import mozilla.components.concept.engine.translate.initialFromLanguage
+import mozilla.components.concept.engine.translate.initialToLanguage
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+private val mockCandidateLanguages = listOf(
+ Language("de", "German"),
+ Language("fr", "French"),
+ Language("en", "English"),
+ Language("es", "Spanish"),
+)
+class TranslationEngineStateTest {
+ @Test
+ fun `GIVEN an untranslated case THEN initialToLanguage and initialFromLanguage match the expected tags`() {
+ val detectedLanguages = DetectedLanguages(
+ documentLangTag = "en",
+ supportedDocumentLang = true,
+ userPreferredLangTag = "es",
+ )
+
+ val translationEngineState = TranslationEngineState(
+ detectedLanguages = detectedLanguages,
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = null,
+ )
+
+ assertEquals(translationEngineState.initialToLanguage(mockCandidateLanguages)?.code, "es")
+ assertEquals(translationEngineState.initialFromLanguage(mockCandidateLanguages)?.code, "en")
+ }
+
+ @Test
+ fun `GIVEN a translated case THEN initialToLanguage and initialFromLanguage match the translated tags`() {
+ val detectedLanguages = DetectedLanguages(
+ documentLangTag = "en",
+ supportedDocumentLang = true,
+ userPreferredLangTag = "es",
+ )
+
+ val translationPair = TranslationPair(
+ fromLanguage = "fr",
+ toLanguage = "de",
+ )
+
+ val translationEngineState = TranslationEngineState(
+ detectedLanguages = detectedLanguages,
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = translationPair,
+ )
+
+ assertEquals(translationEngineState.initialToLanguage(mockCandidateLanguages)?.code, "de")
+ assertEquals(translationEngineState.initialFromLanguage(mockCandidateLanguages)?.code, "fr")
+ }
+
+ @Test
+ fun `GIVEN invalid codes WHEN not translated THEN initialToLanguage and initialFromLanguage are null`() {
+ val detectedLanguages = DetectedLanguages(
+ documentLangTag = "not-a-code",
+ supportedDocumentLang = true,
+ userPreferredLangTag = "not-a-code",
+ )
+
+ val translationEngineState = TranslationEngineState(
+ detectedLanguages = detectedLanguages,
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = null,
+ )
+
+ assertNull(translationEngineState.initialToLanguage(mockCandidateLanguages))
+ assertNull(translationEngineState.initialFromLanguage(mockCandidateLanguages))
+ }
+
+ @Test
+ fun `GIVEN invalid codes WHEN translated THEN initialToLanguage and initialFromLanguage are null`() {
+ val detectedLanguages = DetectedLanguages(
+ documentLangTag = "en",
+ supportedDocumentLang = true,
+ userPreferredLangTag = "es",
+ )
+
+ // This would be a highly unexpected state
+ val translationPair = TranslationPair(
+ fromLanguage = "not-a-code",
+ toLanguage = "not-a-code",
+ )
+
+ val translationEngineState = TranslationEngineState(
+ detectedLanguages = detectedLanguages,
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = translationPair,
+ )
+
+ assertNull(translationEngineState.initialToLanguage(mockCandidateLanguages))
+ assertNull(translationEngineState.initialFromLanguage(mockCandidateLanguages))
+ }
+
+ @Test
+ fun `GIVEN unexpected THEN initialToLanguage and initialFromLanguage are null`() {
+ val translationEngineState = TranslationEngineState(
+ detectedLanguages = null,
+ error = null,
+ isEngineReady = true,
+ requestedTranslationPair = null,
+ )
+
+ assertNull(translationEngineState.initialToLanguage(mockCandidateLanguages))
+ assertNull(translationEngineState.initialFromLanguage(mockCandidateLanguages))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationSupportTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationSupportTest.kt
new file mode 100644
index 0000000000..9d134ff5e4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationSupportTest.kt
@@ -0,0 +1,141 @@
+/* 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.browser.state.state
+
+import mozilla.components.concept.engine.translate.Language
+import mozilla.components.concept.engine.translate.LanguageSetting
+import mozilla.components.concept.engine.translate.LanguageSetting.ALWAYS
+import mozilla.components.concept.engine.translate.LanguageSetting.NEVER
+import mozilla.components.concept.engine.translate.LanguageSetting.OFFER
+import mozilla.components.concept.engine.translate.TranslationSupport
+import mozilla.components.concept.engine.translate.findLanguage
+import mozilla.components.concept.engine.translate.mapLanguageSettings
+import mozilla.components.concept.engine.translate.toLanguageMap
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class TranslationSupportTest {
+
+ private val spanish = Language(code = "es", "Spanish")
+ private val english = Language(code = "en", "English")
+ private val german = Language(code = "de", "German")
+
+ @Test
+ fun `GIVEN a populated TranslationSupport THEN map to a language map`() {
+ val translationsSupport = TranslationSupport(
+ fromLanguages = listOf(english, spanish),
+ toLanguages = listOf(english, spanish),
+ )
+
+ val map = translationsSupport.toLanguageMap()
+
+ assertTrue(map!!.contains(spanish.code))
+ assertTrue(map.contains(english.code))
+ assertEquals(map.size, 2)
+ }
+
+ @Test
+ fun `GIVEN a partially populated TranslationSupport THEN map to a language map`() {
+ val supportToPopulated = TranslationSupport(
+ fromLanguages = null,
+ toLanguages = listOf(english, spanish),
+ )
+
+ val toMap = supportToPopulated.toLanguageMap()
+ assertTrue(toMap!!.contains(spanish.code))
+ assertTrue(toMap.contains(english.code))
+ assertEquals(toMap.size, 2)
+
+ val supportFromPopulated = TranslationSupport(
+ fromLanguages = listOf(english, spanish),
+ toLanguages = null,
+ )
+
+ val fromMap = supportFromPopulated.toLanguageMap()
+ assertTrue(fromMap!!.contains(spanish.code))
+ assertTrue(fromMap.contains(english.code))
+ assertEquals(fromMap.size, 2)
+ }
+
+ @Test
+ fun `GIVEN a null TranslationSupport THEN map to a null language map`() {
+ val translationsSupport = TranslationSupport(
+ fromLanguages = null,
+ toLanguages = null,
+ )
+
+ val map = translationsSupport.toLanguageMap()
+ assertNull(map)
+ }
+
+ @Test
+ fun `GIVEN a populated TranslationSupport THEN find a language`() {
+ val translationsSupport = TranslationSupport(
+ fromLanguages = listOf(spanish, english),
+ toLanguages = listOf(spanish, english),
+ )
+
+ assertEquals(translationsSupport.findLanguage("es"), spanish)
+ assertEquals(translationsSupport.findLanguage("en"), english)
+ assertNull(translationsSupport.findLanguage("de"))
+ }
+
+ @Test
+ fun `GIVEN a null TranslationSupport THEN do not find a language`() {
+ val translationsSupport = TranslationSupport(
+ fromLanguages = null,
+ toLanguages = null,
+ )
+
+ assertNull(translationsSupport.findLanguage("es"))
+ }
+
+ @Test
+ fun `GIVEN a populated TranslationSupport THEN map the language settings`() {
+ val translationsSupport = TranslationSupport(
+ fromLanguages = listOf(spanish, english, german),
+ toLanguages = listOf(spanish, english),
+ )
+
+ val languageSettings = mapOf<String, LanguageSetting>(
+ spanish.code to ALWAYS,
+ english.code to NEVER,
+ german.code to OFFER,
+ "some unknown code" to OFFER,
+ "some unknown code2" to OFFER,
+ )
+
+ val map = translationsSupport.mapLanguageSettings(languageSettings)
+ assertTrue(map!!.contains(spanish))
+ assertTrue(map.contains(english))
+ assertTrue(map.contains(german))
+ assertEquals(map.size, 3)
+ }
+
+ @Test
+ fun `GIVEN an unpopulated TranslationSupport THEN map the language settings`() {
+ val translationsSupport = TranslationSupport(
+ fromLanguages = null,
+ toLanguages = null,
+ )
+
+ val languageSettings = mapOf<String, LanguageSetting>(
+ spanish.code to ALWAYS,
+ english.code to NEVER,
+ german.code to OFFER,
+ "some unknown code" to OFFER,
+ "some unknown code2" to OFFER,
+ )
+
+ val map = translationsSupport.mapLanguageSettings(languageSettings)
+ assertFalse(map!!.contains(spanish))
+ assertFalse(map.contains(english))
+ assertFalse(map.contains(german))
+ assertEquals(map.size, 0)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/content/PermissionHighlightsStateTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/content/PermissionHighlightsStateTest.kt
new file mode 100644
index 0000000000..832862427b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/content/PermissionHighlightsStateTest.kt
@@ -0,0 +1,78 @@
+/* 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.browser.state.state.content
+
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class PermissionHighlightsStateTest {
+
+ @Test
+ fun `WHEN a site has blocked both autoplay audible and inaudible THEN isAutoplayBlocking is true`() {
+ val highlightsState = PermissionHighlightsState(
+ autoPlayAudibleBlocking = true,
+ autoPlayInaudibleBlocking = true,
+ )
+
+ assertTrue(highlightsState.isAutoPlayBlocking)
+ }
+
+ @Test
+ fun `WHEN a site has blocked either autoplay audible or inaudible autoplay THEN isAutoplayBlocking is true`() {
+ var highlightsState = PermissionHighlightsState(
+ autoPlayAudibleBlocking = true,
+ autoPlayInaudibleBlocking = false,
+ )
+
+ assertTrue(highlightsState.isAutoPlayBlocking)
+
+ highlightsState = highlightsState.copy(
+ autoPlayAudibleBlocking = false,
+ autoPlayInaudibleBlocking = true,
+ )
+
+ assertTrue(highlightsState.isAutoPlayBlocking)
+
+ highlightsState = highlightsState.copy(
+ autoPlayAudibleBlocking = false,
+ autoPlayInaudibleBlocking = false,
+ )
+
+ assertFalse(highlightsState.isAutoPlayBlocking)
+ }
+
+ @Test
+ fun `WHEN all permissions has not changed from default value THEN permissionsChanged is false`() {
+ val highlightsState = PermissionHighlightsState(
+ notificationChanged = false,
+ cameraChanged = false,
+ locationChanged = false,
+ microphoneChanged = false,
+ persistentStorageChanged = false,
+ mediaKeySystemAccessChanged = false,
+ autoPlayAudibleChanged = false,
+ autoPlayInaudibleChanged = false,
+ )
+
+ assertFalse(highlightsState.permissionsChanged)
+ }
+
+ @Test
+ fun `WHEN some permissions has changed from default value THEN permissionsChanged is true`() {
+ val highlightsState = PermissionHighlightsState(
+ notificationChanged = false,
+ cameraChanged = true,
+ locationChanged = true,
+ microphoneChanged = true,
+ persistentStorageChanged = true,
+ mediaKeySystemAccessChanged = true,
+ autoPlayAudibleChanged = true,
+ autoPlayInaudibleChanged = true,
+ )
+
+ assertTrue(highlightsState.permissionsChanged)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt
new file mode 100644
index 0000000000..83f9de3dd8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt
@@ -0,0 +1,201 @@
+/* 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.browser.state.store
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.TabGroupAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabGroup
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.toRecoverableTab
+import mozilla.components.lib.state.StoreException
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.shadows.ShadowLooper
+import java.lang.IllegalArgumentException
+
+// These tests are in a separate class because they needs to run with
+// Robolectric (different runner, slower) while all other tests only
+// need a Java VM (fast).
+@RunWith(AndroidJUnit4::class)
+class BrowserStoreExceptionTest {
+
+ @Test(expected = java.lang.IllegalArgumentException::class)
+ fun `AddTabAction - Exception is thrown if parent doesn't exist`() {
+ unwrapStoreExceptionAndRethrow {
+ val store = BrowserStore()
+ val parent = createTab("https://www.mozilla.org")
+ val child = createTab("https://www.firefox.com", parent = parent)
+
+ store.dispatch(TabListAction.AddTabAction(child)).joinBlocking()
+ }
+ }
+
+ @Test(expected = java.lang.IllegalArgumentException::class)
+ fun `AddTabAction - Exception is thrown if tab already exists`() {
+ unwrapStoreExceptionAndRethrow {
+ val store = BrowserStore()
+ val tab1 = createTab("https://www.mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ }
+ }
+
+ @Test(expected = java.lang.IllegalArgumentException::class)
+ fun `RestoreTabAction - Exception is thrown if tab already exists`() {
+ unwrapStoreExceptionAndRethrow {
+ val store = BrowserStore()
+ val tab1 = createTab("https://www.mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+
+ store.dispatch(TabListAction.RestoreAction(listOf(tab1.toRecoverableTab()), restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING)).joinBlocking()
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `AddMultipleTabsAction - Exception is thrown in tab with id already exists`() {
+ unwrapStoreExceptionAndRethrow {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.mozilla.org"),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ TabListAction.AddMultipleTabsAction(
+ tabs = listOf(
+ createTab(id = "a", url = "https://www.example.org"),
+ ),
+ ),
+ ).joinBlocking()
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `AddMultipleTabsAction - Exception is thrown if parent id is set`() {
+ unwrapStoreExceptionAndRethrow {
+ val store = BrowserStore()
+
+ val tab1 = createTab(
+ id = "a",
+ url = "https://www.mozilla.org",
+ )
+
+ val tab2 = createTab(
+ id = "b",
+ url = "https://www.firefox.com",
+ private = true,
+ parent = tab1,
+ )
+
+ store.dispatch(
+ TabListAction.AddMultipleTabsAction(
+ tabs = listOf(tab1, tab2),
+ ),
+ ).joinBlocking()
+ }
+ }
+
+ @Test(expected = java.lang.IllegalArgumentException::class)
+ fun `AddTabGroupAction - Exception is thrown when group already exists`() {
+ unwrapStoreExceptionAndRethrow {
+ val partitionId = "testFeaturePartition"
+ val testGroup = TabGroup("test")
+ val store = BrowserStore(
+ BrowserState(
+ tabPartitions = mapOf(partitionId to TabPartition(partitionId, tabGroups = listOf(testGroup))),
+ ),
+ )
+
+ store.dispatch(
+ TabGroupAction.AddTabGroupAction(
+ partition = partitionId,
+ group = testGroup,
+ ),
+ ).joinBlocking()
+ }
+ }
+
+ @Test(expected = java.lang.IllegalArgumentException::class)
+ fun `AddTabGroupAction - Asserts that tabs exist`() {
+ unwrapStoreExceptionAndRethrow {
+ val store = BrowserStore()
+
+ val partition = "testFeaturePartition"
+ val testGroup = TabGroup("test", tabIds = listOf("invalid"))
+ store.dispatch(
+ TabGroupAction.AddTabGroupAction(
+ partition = partition,
+ group = testGroup,
+ ),
+ ).joinBlocking()
+ }
+ }
+
+ @Test(expected = java.lang.IllegalArgumentException::class)
+ fun `AddTabAction - Asserts that tab exists when adding to group`() {
+ unwrapStoreExceptionAndRethrow {
+ val tabGroup = TabGroup("test1", tabIds = emptyList())
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ val tab = createTab(id = "tab1", url = "https://firefox.com")
+ store.dispatch(TabGroupAction.AddTabAction(tabPartition.id, tabGroup.id, tab.id)).joinBlocking()
+ }
+ }
+
+ @Test(expected = java.lang.IllegalArgumentException::class)
+ fun `AddTabsAction - Asserts that tabs exist when adding to group`() {
+ unwrapStoreExceptionAndRethrow {
+ val tabGroup = TabGroup("test1", tabIds = emptyList())
+ val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup))
+ val tab1 = createTab(id = "tab1", url = "https://firefox.com")
+ val tab2 = createTab(id = "tab2", url = "https://mozilla.org")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1),
+ tabPartitions = mapOf("testFeaturePartition" to tabPartition),
+ ),
+ )
+
+ store.dispatch(
+ TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, listOf(tab1.id, tab2.id)),
+ ).joinBlocking()
+ }
+ }
+
+ private fun unwrapStoreExceptionAndRethrow(block: () -> Unit) {
+ try {
+ block()
+
+ // Wait for the main looper to process the re-thrown exception.
+ ShadowLooper.idleMainLooper()
+
+ fail("Did not throw StoreException")
+ } catch (e: StoreException) {
+ val cause = e.cause
+ if (cause != null) {
+ throw cause
+ }
+ } catch (e: Throwable) {
+ fail("Did throw a different exception $e")
+ }
+
+ fail("Did not throw StoreException with wrapped exception")
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt
new file mode 100644
index 0000000000..cc2eff24d1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt
@@ -0,0 +1,95 @@
+/* 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.browser.state.store
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.action.RestoreCompleteAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.lib.state.Middleware
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class BrowserStoreTest {
+
+ @Test
+ fun `Initial state is empty by default`() {
+ val store = BrowserStore()
+ assertEquals(0, store.state.tabs.size)
+ assertNull(store.state.selectedTabId)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `Initial state is validated and rejected if selected tab does not exist`() {
+ val initialState = BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org")),
+ selectedTabId = "invalid",
+ )
+ BrowserStore(initialState)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `Initial state is validated and rejected if it contains duplicate tabs`() {
+ val tabs = listOf(
+ createTab(id = "1", url = "https://www.mozilla.org"),
+ createTab(id = "2", url = "https://www.getpocket.com"),
+ createTab(id = "1", url = "https://www.mozilla.org"),
+ )
+ val initialState = BrowserState(tabs)
+ BrowserStore(initialState)
+ }
+
+ @Test
+ fun `Adding a tab`() = runTest {
+ val store = BrowserStore()
+
+ assertEquals(0, store.state.tabs.size)
+ assertNull(store.state.selectedTabId)
+
+ val tab = createTab(url = "https://www.mozilla.org")
+
+ store.dispatch(TabListAction.AddTabAction(tab))
+ .join()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(tab.id, store.state.selectedTabId)
+ }
+
+ @Test
+ fun `Dispatches init action when created`() {
+ var initActionObserved = false
+ val testMiddleware: Middleware<BrowserState, BrowserAction> = { _, next, action ->
+ if (action == InitAction) {
+ initActionObserved = true
+ }
+
+ next(action)
+ }
+
+ val store = BrowserStore(middleware = listOf(testMiddleware))
+ store.waitUntilIdle()
+ assertTrue(initActionObserved)
+ }
+
+ @Test
+ fun `RestoreCompleteAction updates state`() {
+ val store = BrowserStore()
+ assertFalse(store.state.restoreComplete)
+
+ store.dispatch(RestoreCompleteAction).joinBlocking()
+ assertTrue(store.state.restoreComplete)
+
+ store.dispatch(RestoreCompleteAction).joinBlocking()
+ assertTrue(store.state.restoreComplete)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/state/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/browser/storage-sync/README.md b/mobile/android/android-components/components/browser/storage-sync/README.md
new file mode 100644
index 0000000000..95278e97f6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/README.md
@@ -0,0 +1,22 @@
+# [Android Components](../../../README.md) > Browser > Sync Storage
+
+A syncable implementation of `concept-storage` backed by [application-services' Places lib](https://github.com/mozilla/application-services).
+
+## Before using this component
+Products sending telemetry and using this component *must request* a data-review following [this process](https://wiki.mozilla.org/Firefox/Data_Collection).
+This component provides data collection using the [Glean SDK](https://mozilla.github.io/glean/book/index.html).
+The list of metrics being collected is available in the [metrics documentation](https://github.com/mozilla/application-services/tree/main/components/sync_manager/android/metrics.md).
+
+### 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:browser-storage-sync:{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/browser/storage-sync/build.gradle b/mobile/android/android-components/components/browser/storage-sync/build.gradle
new file mode 100644
index 0000000000..21457fdef8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/build.gradle
@@ -0,0 +1,64 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.browser.storage.sync'
+}
+
+dependencies {
+ // These dependencies are part of this module's public API.
+ api (ComponentsDependencies.mozilla_appservices_places) {
+ // Use our own version of the Glean dependency,
+ // which might be different from the version declared by A-S.
+ exclude group: 'org.mozilla.components', module: 'service-glean'
+ }
+
+ api ComponentsDependencies.mozilla_appservices_tabs
+ api project(':concept-storage')
+ api project(':concept-sync')
+
+ implementation project(':concept-toolbar')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.androidx_work_runtime
+ implementation ComponentsDependencies.mozilla_appservices_syncmanager
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.androidx_work_testing
+ testImplementation ComponentsDependencies.kotlin_reflect
+
+ testImplementation ComponentsDependencies.mozilla_appservices_places
+ testImplementation ComponentsDependencies.mozilla_appservices_tabs
+ testImplementation ComponentsDependencies.testing_mockwebserver
+
+ testImplementation ComponentsDependencies.mozilla_appservices_full_megazord_forUnitTests
+ testImplementation ComponentsDependencies.mozilla_glean_forUnitTests
+}
+
+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/browser/storage-sync/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/storage-sync/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/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/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Connection.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Connection.kt
new file mode 100644
index 0000000000..d579da8756
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Connection.kt
@@ -0,0 +1,127 @@
+/* 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.browser.storage.sync
+
+import androidx.annotation.GuardedBy
+import mozilla.appservices.places.PlacesApi
+import mozilla.appservices.places.PlacesReaderConnection
+import mozilla.appservices.places.PlacesWriterConnection
+import mozilla.appservices.syncmanager.SyncTelemetry
+import mozilla.components.concept.sync.SyncAuthInfo
+import java.io.Closeable
+import java.io.File
+
+const val DB_NAME = "places.sqlite"
+
+/**
+ * A slight abstraction over [PlacesApi].
+ *
+ * A single reader is assumed here, which isn't a limitation placed on use by [PlacesApi].
+ * We can switch to pooling multiple readers as the need arises. Underneath, these are connections
+ * to a SQLite database, and so opening and maintaining them comes with a memory/IO burden.
+ *
+ * Writer is always the same, as guaranteed by [PlacesApi].
+ */
+internal interface Connection : Closeable {
+ fun registerWithSyncManager()
+
+ /**
+ * Allows to read history, bookmarks, and other data from persistent storage.
+ * All calls on the same reader are queued. Multiple readers are allowed.
+ */
+ fun reader(): PlacesReaderConnection
+
+ /**
+ * Create a new reader for history, bookmarks and other data from persistent storage.
+ * Allows for disbanded calls from the default reader from [reader] and so being able to
+ * easily start and cancel any data requests without impacting others using another reader.
+ *
+ * All [PlacesApi] requests are synchronized at lower levels so even with using multiple readers
+ * all requests are ordered with concurrent reads not possible.
+ */
+ fun newReader(): PlacesReaderConnection
+
+ /**
+ * Allowed to add history, bookmarks and other data to persistent storage.
+ * All calls are queued and synchronized at lower levels. Only one writer is recommended.
+ */
+ fun writer(): PlacesWriterConnection
+
+ // Until we get a real SyncManager in application-services libraries, we'll have to live with this
+ // strange split that doesn't quite map all that well to our internal storage model.
+ fun syncHistory(syncInfo: SyncAuthInfo)
+ fun syncBookmarks(syncInfo: SyncAuthInfo)
+}
+
+/**
+ * A singleton implementation of the [Connection] interface backed by the Rust Places library.
+ */
+internal object RustPlacesConnection : Connection {
+ @GuardedBy("this")
+ private var api: PlacesApi? = null
+
+ @GuardedBy("this")
+ private var cachedReader: PlacesReaderConnection? = null
+
+ /**
+ * Creates a long-lived [PlacesApi] instance, and caches a reader connection.
+ * Writer connection is maintained by [PlacesApi] itself, and is created upon its initialization.
+ *
+ * @param parentDir Location of the parent directory in which database is/will be stored.
+ */
+ fun init(parentDir: File) = synchronized(this) {
+ if (api == null) {
+ api = PlacesApi(File(parentDir, DB_NAME).canonicalPath)
+ }
+ cachedReader = api!!.openReader()
+ }
+
+ override fun registerWithSyncManager() {
+ val api = safeGetApi()
+ check(api != null) { "must call init first" }
+ api.registerWithSyncManager()
+ }
+
+ override fun reader(): PlacesReaderConnection = synchronized(this) {
+ check(cachedReader != null) { "must call init first" }
+ return cachedReader!!
+ }
+
+ override fun newReader(): PlacesReaderConnection = synchronized(this) {
+ val api = safeGetApi()
+ check(api != null) { "must call init first" }
+ return api.openReader()
+ }
+
+ override fun writer(): PlacesWriterConnection {
+ val api = safeGetApi()
+ check(api != null) { "must call init first" }
+ return api.getWriter()
+ }
+
+ override fun syncHistory(syncInfo: SyncAuthInfo) {
+ val api = safeGetApi()
+ check(api != null) { "must call init first" }
+ val ping = api.syncHistory(syncInfo.into())
+ SyncTelemetry.processHistoryPing(ping)
+ }
+
+ override fun syncBookmarks(syncInfo: SyncAuthInfo) {
+ val api = safeGetApi()
+ check(api != null) { "must call init first" }
+ val ping = api.syncBookmarks(syncInfo.into())
+ SyncTelemetry.processBookmarksPing(ping)
+ }
+
+ override fun close() = synchronized(this) {
+ check(api != null) { "must call init first" }
+ api!!.close()
+ api = null
+ }
+
+ private fun safeGetApi(): PlacesApi? = synchronized(this) {
+ return this.api
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProvider.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProvider.kt
new file mode 100644
index 0000000000..2f3f04c47c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProvider.kt
@@ -0,0 +1,36 @@
+/* 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.browser.storage.sync
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Provides global access to the dependencies needed for places storage operations.
+ * */
+object GlobalPlacesDependencyProvider {
+
+ @VisibleForTesting
+ internal var placesStorage: PlacesStorage? = null
+
+ /**
+ * Initializes places storage for running the maintenance task via [PlacesHistoryStorageWorker].
+ * This method should be called in client application's onCreate method and before
+ * [PlacesHistoryStorage.registerStorageMaintenanceWorker] in order to run the worker while
+ * the app is not running.
+ * */
+ fun initialize(placesStorage: PlacesStorage) {
+ this.placesStorage = placesStorage
+ }
+
+ /**
+ * Provides [PlacesStorage] globally when needed for [PlacesHistoryStorageWorker]
+ * to run maintenance on the storage.
+ * */
+ internal fun requirePlacesStorage(): PlacesStorage {
+ return requireNotNull(placesStorage) {
+ "GlobalPlacesDependencyProvider.initialize must be called before accessing the Places storage"
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorage.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorage.kt
new file mode 100644
index 0000000000..25cbf1aae9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorage.kt
@@ -0,0 +1,299 @@
+/* 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.browser.storage.sync
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.withContext
+import mozilla.appservices.places.PlacesApi
+import mozilla.appservices.places.uniffi.PlacesApiException
+import mozilla.components.concept.storage.BookmarkInfo
+import mozilla.components.concept.storage.BookmarkNode
+import mozilla.components.concept.storage.BookmarksStorage
+import mozilla.components.concept.sync.SyncAuthInfo
+import mozilla.components.concept.sync.SyncStatus
+import mozilla.components.concept.sync.SyncableStore
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.concept.toolbar.AutocompleteResult
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.doesUrlStartsWithText
+import mozilla.components.support.utils.segmentAwareDomainMatch
+
+@VisibleForTesting
+internal const val BOOKMARKS_AUTOCOMPLETE_SOURCE_NAME = "placesBookmarks"
+
+/**
+ * How many bookmarks to try and find from which to pick one that can be an autocomplete suggestion.
+ */
+private const val BOOKMARKS_AUTOCOMPLETE_QUERY_LIMIT = 20
+
+/**
+ * Implementation of the [BookmarksStorage] which is backed by a Rust Places lib via [PlacesApi].
+ */
+open class PlacesBookmarksStorage(
+ context: Context,
+ override val autocompletePriority: Int = 0,
+) : PlacesStorage(context),
+ BookmarksStorage,
+ SyncableStore,
+ AutocompleteProvider {
+
+ override val logger = Logger("PlacesBookmarksStorage")
+
+ /**
+ * Produces a bookmarks tree for the given guid string.
+ *
+ * @param guid The bookmark guid to obtain.
+ * @param recursive Whether to recurse and obtain all levels of children.
+ * @return The populated root starting from the guid.
+ */
+ override suspend fun getTree(guid: String, recursive: Boolean): BookmarkNode? {
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("getTree", default = null) {
+ reader.getBookmarksTree(guid, recursive)?.asBookmarkNode()
+ }
+ }
+ }
+
+ /**
+ * Obtains the details of a bookmark without children, if one exists with that guid. Otherwise, null.
+ *
+ * @param guid The bookmark guid to obtain.
+ * @return The bookmark node or null if it does not exist.
+ */
+ override suspend fun getBookmark(guid: String): BookmarkNode? {
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("getBookmark", default = null) {
+ reader.getBookmark(guid)?.asBookmarkNode()
+ }
+ }
+ }
+
+ /**
+ * Produces a list of all bookmarks with the given URL.
+ *
+ * @param url The URL string.
+ * @return The list of bookmarks that match the URL
+ */
+ override suspend fun getBookmarksWithUrl(url: String): List<BookmarkNode> {
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("getBookmarkWithUrl", default = emptyList()) {
+ reader.getBookmarksWithURL(url).map { it.asBookmarkNode() }
+ }
+ }
+ }
+
+ /**
+ * Searches bookmarks with a query string.
+ *
+ * @param query The query string to search.
+ * @param limit The maximum number of entries to return.
+ * @return The list of matching bookmark nodes up to the limit number of items.
+ */
+ override suspend fun searchBookmarks(query: String, limit: Int): List<BookmarkNode> {
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("searchBookmarks", default = emptyList()) {
+ reader.searchBookmarks(query, limit).map { it.asBookmarkNode() }
+ }
+ }
+ }
+
+ /**
+ * Retrieves a list of recently added bookmarks.
+ *
+ * @param limit The maximum number of entries to return.
+ * @param maxAge Optional parameter used to filter out entries older than this number of milliseconds.
+ * @param currentTime Optional parameter for current time. Defaults toSystem.currentTimeMillis()
+ * @return The list of matching bookmark nodes up to the limit number of items.
+ */
+ override suspend fun getRecentBookmarks(
+ limit: Int,
+ maxAge: Long?,
+ @VisibleForTesting currentTime: Long,
+ ): List<BookmarkNode> {
+ return withContext(readScope.coroutineContext) {
+ val threshold = if (maxAge != null) {
+ currentTime - maxAge
+ } else {
+ 0
+ }
+ handlePlacesExceptions("getRecentBookmarks", default = emptyList()) {
+ reader.getRecentBookmarks(limit)
+ .map { it.asBookmarkNode() }
+ .filter { it.dateAdded >= threshold }
+ }
+ }
+ }
+
+ /**
+ * Adds a new bookmark item to a given node.
+ *
+ * Sync behavior: will add new bookmark item to remote devices.
+ *
+ * @param parentGuid The parent guid of the new node.
+ * @param url The URL of the bookmark item to add.
+ * @param title The title of the bookmark item to add.
+ * @param position The optional position to add the new node or null to append.
+ * @return The guid of the newly inserted bookmark item.
+ */
+ override suspend fun addItem(parentGuid: String, url: String, title: String, position: UInt?): String {
+ return withContext(writeScope.coroutineContext) {
+ try {
+ writer.createBookmarkItem(parentGuid, url, title, position)
+ } catch (e: PlacesApiException.UrlParseFailed) {
+ // We re-throw this exception, it should be handled by the caller
+ throw e
+ } catch (e: PlacesApiException.UnexpectedPlacesException) {
+ // this is a fatal error, and should be rethrown
+ throw e
+ } catch (e: PlacesApiException) {
+ crashReporter?.submitCaughtException(e)
+ logger.warn("Ignoring PlacesApiException while running addItem", e)
+ // Should not return an empty string here. The function should be nullable
+ // however, it is better than the app crashing.
+ ""
+ }
+ }
+ }
+
+ /**
+ * Adds a new bookmark folder to a given node.
+ *
+ * Sync behavior: will add new separator to remote devices.
+ *
+ * @param parentGuid The parent guid of the new node.
+ * @param title The title of the bookmark folder to add.
+ * @param position The optional position to add the new node or null to append.
+ * @return The guid of the newly inserted bookmark item.
+ */
+ override suspend fun addFolder(parentGuid: String, title: String, position: UInt?): String {
+ return withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("addFolder", default = "") {
+ writer.createFolder(parentGuid, title, position)
+ }
+ }
+ }
+
+ /**
+ * Adds a new bookmark separator to a given node.
+ *
+ * Sync behavior: will add new separator to remote devices.
+ *
+ * @param parentGuid The parent guid of the new node.
+ * @param position The optional position to add the new node or null to append.
+ * @return The guid of the newly inserted bookmark item.
+ */
+ override suspend fun addSeparator(parentGuid: String, position: UInt?): String {
+ return withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("addSeparator", default = "") {
+ writer.createSeparator(parentGuid, position)
+ }
+ }
+ }
+
+ /**
+ * Edits the properties of an existing bookmark item and/or moves an existing one underneath a new parent guid.
+ *
+ * Sync behavior: will alter bookmark item on remote devices.
+ *
+ * @param guid The guid of the item to update.
+ * @param info The info to change in the bookmark.
+ */
+ override suspend fun updateNode(guid: String, info: BookmarkInfo) {
+ return withContext(writeScope.coroutineContext) {
+ try {
+ writer.updateBookmark(guid, info.parentGuid, info.position, info.title, info.url)
+ } catch (e: PlacesApiException.InvalidBookmarkOperation) {
+ // We re-throw this exception, it should be handled by the caller
+ throw e
+ } catch (e: PlacesApiException.UnexpectedPlacesException) {
+ // this is a fatal error, and should be rethrown
+ throw e
+ } catch (e: PlacesApiException) {
+ crashReporter?.submitCaughtException(e)
+ logger.warn("Ignoring PlacesApiException while running updateNode", e)
+ }
+ }
+ }
+
+ /**
+ * Deletes a bookmark node and all of its children, if any.
+ *
+ * Sync behavior: will remove bookmark from remote devices.
+ *
+ * @return Whether the bookmark existed or not.
+ */
+ override suspend fun deleteNode(guid: String): Boolean = withContext(writeScope.coroutineContext) {
+ try {
+ writer.deleteBookmarkNode(guid)
+ } catch (e: PlacesApiException.InvalidBookmarkOperation) {
+ // We re-throw this exception, it should be handled by the caller
+ throw e
+ } catch (e: PlacesApiException.UnexpectedPlacesException) {
+ // this is a fatal error, and should be rethrown
+ throw e
+ } catch (e: PlacesApiException) {
+ crashReporter?.submitCaughtException(e)
+ logger.warn("Ignoring PlacesApiException while running deleteNode", e)
+ false
+ }
+ }
+
+ /**
+ * Counts the number of items in the bookmark trees under the specified GUIDs.
+
+ * @param guids The guids of folders to query.
+ * @return Count of all bookmark items (ie, not folders or separators) in all specified folders
+ * recursively. Empty folders, non-existing GUIDs and non-existing items will return zero.
+ * The result is implementation dependant if the trees overlap.
+ */
+ override suspend fun countBookmarksInTrees(guids: List<String>): UInt {
+ return withContext(readScope.coroutineContext) {
+ try {
+ reader.countBookmarksInTrees(guids)
+ } catch (e: PlacesApiException) {
+ crashReporter?.submitCaughtException(e)
+ logger.warn("Ignoring PlacesApiException while running countBookmarksInTrees", e)
+ 0U
+ }
+ }
+ }
+
+ /**
+ * Runs syncBookmarks() method on the places Connection
+ *
+ * @param authInfo The authentication information to sync with.
+ * @return Sync status of OK or Error
+ */
+ suspend fun sync(authInfo: SyncAuthInfo): SyncStatus {
+ return withContext(writeScope.coroutineContext) {
+ syncAndHandleExceptions {
+ places.syncBookmarks(authInfo)
+ }
+ }
+ }
+
+ override fun registerWithSyncManager() {
+ places.registerWithSyncManager()
+ }
+
+ override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
+ val bookmarkUrl = searchBookmarks(query, BOOKMARKS_AUTOCOMPLETE_QUERY_LIMIT)
+ .mapNotNull { it.url }
+ .firstOrNull { doesUrlStartsWithText(it, query) }
+ ?: return null
+
+ val resultText = segmentAwareDomainMatch(query, arrayListOf(bookmarkUrl))
+ return resultText?.let {
+ AutocompleteResult(
+ input = query,
+ text = it.matchedSegment,
+ url = it.url,
+ source = BOOKMARKS_AUTOCOMPLETE_SOURCE_NAME,
+ totalItems = 1,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt
new file mode 100644
index 0000000000..e7b9161035
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt
@@ -0,0 +1,409 @@
+/* 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.browser.storage.sync
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.WorkManager
+import kotlinx.coroutines.withContext
+import mozilla.appservices.places.PlacesApi
+import mozilla.appservices.places.PlacesReaderConnection
+import mozilla.appservices.places.uniffi.VisitObservation
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.storage.FrecencyThresholdOption
+import mozilla.components.concept.storage.HistoryHighlight
+import mozilla.components.concept.storage.HistoryHighlightWeights
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.concept.storage.HistoryMetadataObservation
+import mozilla.components.concept.storage.HistoryMetadataStorage
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.concept.storage.PageObservation
+import mozilla.components.concept.storage.PageVisit
+import mozilla.components.concept.storage.RedirectSource
+import mozilla.components.concept.storage.SearchResult
+import mozilla.components.concept.storage.TopFrecentSiteInfo
+import mozilla.components.concept.storage.VisitInfo
+import mozilla.components.concept.storage.VisitType
+import mozilla.components.concept.sync.SyncAuthInfo
+import mozilla.components.concept.sync.SyncStatus
+import mozilla.components.concept.sync.SyncableStore
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.concept.toolbar.AutocompleteResult
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.segmentAwareDomainMatch
+
+private const val AUTOCOMPLETE_SOURCE_NAME = "placesHistory"
+
+/**
+ * Implementation of the [HistoryStorage] which is backed by a Rust Places lib via [PlacesApi].
+ */
+@Suppress("TooManyFunctions", "LargeClass")
+open class PlacesHistoryStorage(
+ private val context: Context,
+ crashReporter: CrashReporting? = null,
+ override val autocompletePriority: Int = 0,
+) : PlacesStorage(context, crashReporter),
+ HistoryStorage,
+ HistoryMetadataStorage,
+ SyncableStore,
+ AutocompleteProvider {
+ /**
+ * Separate reader used only for autocomplete suggestions allowing to decouple this functionality
+ * from the history suggestions feature and independent reader management.
+ */
+ @VisibleForTesting
+ internal val autocompleteReader: PlacesReaderConnection by lazy { places.newReader() }
+
+ override val logger = Logger("PlacesHistoryStorage")
+
+ override suspend fun recordVisit(uri: String, visit: PageVisit) {
+ if (!canAddUri(uri)) {
+ logger.debug("Not recording visit (canAddUri=false) for: $uri")
+ return
+ }
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("recordVisit") {
+ places.writer().noteObservation(
+ VisitObservation(
+ uri,
+ visitType = visit.visitType.into(),
+ isRedirectSource = visit.redirectSource != null,
+ isPermanentRedirectSource = visit.redirectSource == RedirectSource.PERMANENT,
+ ),
+ )
+ }
+ }
+ }
+
+ override suspend fun recordObservation(uri: String, observation: PageObservation) {
+ if (!canAddUri(uri)) {
+ logger.debug("Not recording observation (canAddUri=false) for: $uri")
+ return
+ }
+ // NB: visitType being null means "record meta information about this URL".
+ withContext(writeScope.coroutineContext) {
+ // Ignore exceptions related to uris. This means we may drop some of the data on the floor
+ // if the underlying storage layer refuses it.
+ handlePlacesExceptions("recordObservation") {
+ places.writer().noteObservation(
+ VisitObservation(
+ url = uri,
+ visitType = null,
+ title = observation.title,
+ previewImageUrl = observation.previewImageUrl,
+ ),
+ )
+ }
+ }
+ }
+
+ override suspend fun getVisited(uris: List<String>): List<Boolean> {
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("getVisited", default = uris.map { false }) {
+ places.reader().getVisited(uris)
+ }
+ }
+ }
+
+ override suspend fun getVisited(): List<String> {
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("getVisited", default = emptyList()) {
+ places.reader().getVisitedUrlsInRange(
+ start = 0,
+ end = System.currentTimeMillis(),
+ includeRemote = true,
+ )
+ }
+ }
+ }
+
+ override suspend fun getDetailedVisits(start: Long, end: Long, excludeTypes: List<VisitType>): List<VisitInfo> {
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("getDetailedVisits", default = emptyList()) {
+ places.reader().getVisitInfos(start, end, excludeTypes.map { it.into() }).map { it.into() }
+ }
+ }
+ }
+
+ override suspend fun getVisitsPaginated(offset: Long, count: Long, excludeTypes: List<VisitType>): List<VisitInfo> {
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("getVisitsPaginated", default = emptyList()) {
+ places.reader().getVisitPage(offset, count, excludeTypes.map { it.into() }).map { it.into() }
+ }
+ }
+ }
+
+ override suspend fun getTopFrecentSites(
+ numItems: Int,
+ frecencyThreshold: FrecencyThresholdOption,
+ ): List<TopFrecentSiteInfo> {
+ if (numItems <= 0) {
+ return emptyList()
+ }
+
+ return withContext(readScope.coroutineContext) {
+ handlePlacesExceptions("getTopFrecentSites", default = emptyList()) {
+ places.reader().getTopFrecentSiteInfos(numItems, frecencyThreshold.into())
+ .map { it.into() }
+ }
+ }
+ }
+
+ override fun getSuggestions(query: String, limit: Int): List<SearchResult> {
+ require(limit >= 0) { "Limit must be a positive integer" }
+ return handlePlacesExceptions("getSuggestions", default = emptyList()) {
+ places.reader().queryAutocomplete(query, limit = limit).map {
+ SearchResult(it.url, it.url, it.frecency.toInt(), it.title)
+ }
+ }
+ }
+
+ override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
+ val url = handlePlacesExceptions("getAutoCompleteSuggestions", default = null) {
+ autocompleteReader.interrupt()
+ autocompleteReader.matchUrl(query)
+ } ?: return null
+
+ val resultText = segmentAwareDomainMatch(query, arrayListOf(url))
+ return resultText?.let {
+ AutocompleteResult(
+ input = query,
+ text = it.matchedSegment,
+ url = it.url,
+ source = AUTOCOMPLETE_SOURCE_NAME,
+ totalItems = 1,
+ )
+ }
+ }
+
+ /**
+ * Sync behaviour: will not remove any history from remote devices, but it will prevent deleted
+ * history from returning.
+ */
+ override suspend fun deleteEverything() {
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("deleteEverything") {
+ places.writer().deleteEverything()
+ }
+ }
+ }
+
+ /**
+ * Sync behaviour: may remove history from remote devices, if the removed visits were the only
+ * ones for a URL.
+ */
+ override suspend fun deleteVisitsSince(since: Long) {
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("deleteVisitsSince") {
+ places.writer().deleteVisitsSince(since)
+ }
+ }
+ }
+
+ /**
+ * Sync behaviour: may remove history from remote devices, if the removed visits were the only
+ * ones for a URL.
+ */
+ override suspend fun deleteVisitsBetween(startTime: Long, endTime: Long) {
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("deleteVisitsBetween") {
+ places.writer().deleteVisitsBetween(startTime, endTime)
+ }
+ }
+ }
+
+ /**
+ * Sync behaviour: will remove history from remote devices.
+ */
+ override suspend fun deleteVisitsFor(url: String) {
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("deleteVisitsFor") {
+ places.writer().deleteVisitsFor(url)
+ }
+ }
+ }
+
+ /**
+ * Sync behaviour: will remove history from remote devices if this was the only visit for [url].
+ * Otherwise, remote devices are not affected.
+ */
+ override suspend fun deleteVisit(url: String, timestamp: Long) {
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("deleteVisit") {
+ places.writer().deleteVisit(url, timestamp)
+ }
+ }
+ }
+
+ /**
+ * Enqueues a periodic storage maintenance worker to WorkManager that prunes database entries
+ * when it exceeds [PlacesHistoryStorageWorker.DB_SIZE_LIMIT_IN_BYTES].
+ */
+ override fun registerStorageMaintenanceWorker() {
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(
+ PlacesHistoryStorageWorker.UNIQUE_NAME,
+ ExistingPeriodicWorkPolicy.KEEP,
+ periodicStorageWorkRequest<PlacesHistoryStorageWorker>(
+ tag = PlacesHistoryStorageWorker.UNIQUE_NAME,
+ ) {
+ constraints {
+ setRequiresBatteryNotLow(true)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ setRequiresDeviceIdle(true)
+ }
+ }
+ },
+ )
+ }
+
+ /**
+ * Runs syncHistory() method on the places Connection
+ *
+ * @param authInfo The authentication information to sync with.
+ * @return Sync status of OK or Error
+ */
+ suspend fun sync(authInfo: SyncAuthInfo): SyncStatus {
+ return withContext(writeScope.coroutineContext) {
+ syncAndHandleExceptions {
+ places.syncHistory(authInfo)
+ }
+ }
+ }
+
+ override fun registerWithSyncManager() {
+ return places.registerWithSyncManager()
+ }
+
+ override suspend fun getLatestHistoryMetadataForUrl(url: String): HistoryMetadata? {
+ return handlePlacesExceptions("getLatestHistoryMetadataForUrl", default = null) {
+ places.reader().getLatestHistoryMetadataForUrl(url)?.into()
+ }
+ }
+
+ override suspend fun getHistoryMetadataSince(since: Long): List<HistoryMetadata> {
+ return handlePlacesExceptions("getHistoryMetadataSince", default = emptyList()) {
+ places.reader().getHistoryMetadataSince(since).into()
+ }
+ }
+
+ override suspend fun getHistoryMetadataBetween(start: Long, end: Long): List<HistoryMetadata> {
+ return handlePlacesExceptions("getHistoryMetadataBetween", default = emptyList()) {
+ places.reader().getHistoryMetadataBetween(start, end).into()
+ }
+ }
+
+ override suspend fun queryHistoryMetadata(query: String, limit: Int): List<HistoryMetadata> {
+ return handlePlacesExceptions("queryHistoryMetadata", default = emptyList()) {
+ places.reader().queryHistoryMetadata(query, limit).into()
+ }
+ }
+
+ override suspend fun getHistoryHighlights(
+ weights: HistoryHighlightWeights,
+ limit: Int,
+ ): List<HistoryHighlight> {
+ return handlePlacesExceptions("getHistoryHighlights", default = emptyList()) {
+ places.reader().getHighlights(weights.into(), limit).intoHighlights()
+ }
+ }
+
+ override suspend fun noteHistoryMetadataObservation(
+ key: HistoryMetadataKey,
+ observation: HistoryMetadataObservation,
+ ) {
+ if (!canAddUri(key.url)) {
+ logger.debug("Not recording metadata (canAddUri=false) for: ${key.url}")
+ return
+ }
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("noteHistoryMetadataObservation") {
+ places.writer().noteHistoryMetadataObservation(observation.into(key))
+ }
+ }
+ }
+
+ override suspend fun deleteHistoryMetadataOlderThan(olderThan: Long) {
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("deleteHistoryMetadataOlderThan") {
+ places.writer().deleteHistoryMetadataOlderThan(olderThan)
+ }
+ }
+ }
+
+ override suspend fun deleteHistoryMetadata(key: HistoryMetadataKey) {
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("deleteHistoryMetadata") {
+ places.writer().deleteHistoryMetadata(key.into())
+ }
+ }
+ }
+
+ override suspend fun deleteHistoryMetadata(searchTerm: String) {
+ deleteHistoryMetadata {
+ // NB: searchTerms are always lower-case in the database.
+ it.searchTerm == searchTerm.lowercase()
+ }
+ }
+
+ override suspend fun deleteHistoryMetadataForUrl(url: String) {
+ deleteHistoryMetadata {
+ it.url == url
+ }
+ }
+
+ private suspend fun deleteHistoryMetadata(
+ predicate: (mozilla.appservices.places.uniffi.HistoryMetadata) -> Boolean,
+ ) {
+ // Ideally, we want this to live in A-S as a simple DELETE statement.
+ // As-is, this isn't an atomic operation. For how we're using these data, both lack of
+ // atomicity and a performance penalty is acceptable for now.
+ withContext(writeScope.coroutineContext) {
+ handlePlacesExceptions("deleteHistoryMetadata") {
+ places.reader().getHistoryMetadataSince(Long.MIN_VALUE)
+ .filter(predicate)
+ .forEach {
+ places.writer().deleteHistoryMetadata(
+ HistoryMetadataKey(
+ url = it.url,
+ searchTerm = it.searchTerm,
+ referrerUrl = it.referrerUrl,
+ ).into(),
+ )
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("ReturnCount")
+ override fun canAddUri(uri: String): Boolean {
+ // Filter out unwanted URIs, such as "chrome:", "about:", etc.
+ // Ported from nsAndroidHistory::CanAddURI
+ // See https://dxr.mozilla.org/mozilla-central/source/mobile/android/components/build/nsAndroidHistory.cpp#326
+ val parsedUri = Uri.parse(uri)
+ val scheme = parsedUri.normalizeScheme().scheme ?: return false
+
+ // Short-circuit most common schemes.
+ if (scheme == "http" || scheme == "https") {
+ return true
+ }
+
+ // Allow about about:reader uris. They are of the form:
+ // about:reader?url=http://some.interesting.page/to/read.html
+ if (uri.startsWith("about:reader")) {
+ return true
+ }
+
+ val schemasToIgnore = listOf(
+ "", "about", "imap", "news", "mailbox", "moz-anno", "moz-extension",
+ "view-source", "chrome", "resource", "data", "javascript", "blob",
+ )
+
+ return !schemasToIgnore.contains(scheme)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.kt
new file mode 100644
index 0000000000..f167f87c7b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.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.browser.storage.sync
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A WorkManager Worker that executes [PlacesStorage.runMaintenance].
+ *
+ * If there is a failure or the worker constraints are no longer met during execution,
+ * active write operations on [PlacesStorage] are cancelled.
+ *
+ * See also [StorageMaintenanceWorker].
+ */
+internal class PlacesHistoryStorageWorker(context: Context, params: WorkerParameters) :
+ StorageMaintenanceWorker(context, params) {
+
+ val logger = Logger(PLACES_HISTORY_STORAGE_WORKER_TAG)
+
+ override suspend fun operate() {
+ GlobalPlacesDependencyProvider.requirePlacesStorage()
+ .runMaintenance(DB_SIZE_LIMIT_IN_BYTES.toUInt())
+ }
+
+ override fun onError(exception: Exception) {
+ GlobalPlacesDependencyProvider.requirePlacesStorage().cancelWrites()
+ logger.error("An exception occurred while running the maintenance task: ${exception.message}")
+ }
+
+ companion object {
+ private const val IDENTIFIER_PREFIX = "mozilla.components.browser.storage.sync"
+ private const val PLACES_HISTORY_STORAGE_WORKER_TAG = "$IDENTIFIER_PREFIX.PlacesHistoryStorageWorker"
+
+ internal const val DB_SIZE_LIMIT_IN_BYTES = 75 * 1024 * 1024 // corresponds to 75MiB (in bytes)
+ internal const val UNIQUE_NAME = "$IDENTIFIER_PREFIX.PlacesHistoryStorageWorker"
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesStorage.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesStorage.kt
new file mode 100644
index 0000000000..3895f01a08
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesStorage.kt
@@ -0,0 +1,228 @@
+/* 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.browser.storage.sync
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.withContext
+import mozilla.appservices.places.PlacesReaderConnection
+import mozilla.appservices.places.PlacesWriterConnection
+import mozilla.appservices.places.uniffi.PlacesApiException
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.storage.Storage
+import mozilla.components.concept.storage.StorageMaintenanceRegistry
+import mozilla.components.concept.sync.SyncStatus
+import mozilla.components.concept.sync.SyncableStore
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.NamedThreadFactory
+import mozilla.components.support.utils.logElapsedTime
+import java.nio.charset.MalformedInputException
+import java.util.concurrent.Executors
+
+/**
+ * A base class for concrete implementations of PlacesStorages
+ */
+abstract class PlacesStorage(
+ context: Context,
+ val crashReporter: CrashReporting? = null,
+) : Storage, SyncableStore, StorageMaintenanceRegistry {
+ internal var writeScope =
+ CoroutineScope(
+ Executors.newSingleThreadExecutor(
+ NamedThreadFactory("PlacesStorageWriteScope"),
+ ).asCoroutineDispatcher(),
+ )
+ @VisibleForTesting internal set
+
+ internal var readScope = CoroutineScope(Dispatchers.IO)
+ @VisibleForTesting internal set
+ private val storageDir by lazy { context.filesDir }
+
+ /**
+ * Cache of the last value with which [cancelReads] was called.
+ * Used to check whether a new call to [cancelReads] should trigger a cancellation or not.
+ */
+ private var lastCancelledQuery = ""
+
+ abstract val logger: Logger
+
+ internal open val places: Connection by lazy {
+ RustPlacesConnection.init(storageDir)
+ RustPlacesConnection
+ }
+
+ internal open val writer: PlacesWriterConnection by lazy { places.writer() }
+ internal open val reader: PlacesReaderConnection by lazy { places.reader() }
+
+ override suspend fun warmUp() {
+ logElapsedTime(logger, "Warming up places storage") {
+ writer
+ reader
+ }
+ }
+
+ /**
+ * Internal database maintenance tasks. Ideally this should be called once a day.
+ *
+ * @param dbSizeLimit Maximum DB size to aim for, in bytes. If the
+ * database exceeds this size, a small number of visits will be pruned.
+ */
+ override suspend fun runMaintenance(dbSizeLimit: UInt) {
+ withContext(writeScope.coroutineContext) {
+ places.writer().runMaintenance(dbSizeLimit)
+ }
+ }
+
+ @Deprecated(
+ "Use `cancelWrites` and `cancelReads` to get a similar functionality. " +
+ "See https://github.com/mozilla-mobile/android-components/issues/7348 for a description of the issues " +
+ "for when using this method",
+ )
+ override fun cleanup() {
+ writeScope.coroutineContext.cancelChildren()
+ readScope.coroutineContext.cancelChildren()
+ places.close()
+ }
+
+ override fun cancelWrites() {
+ interruptCurrentWrites()
+ writeScope.coroutineContext.cancelChildren()
+ }
+
+ override fun cancelReads() {
+ interruptCurrentReads()
+ readScope.coroutineContext.cancelChildren()
+ }
+
+ /**
+ * Cleans up pending read operations of a specific query.
+ *
+ * @param nextQuery Previous query to cancel reads for.
+ * Calling cancel multiple times for the same query has effect only the first time.
+ * Use this in scenarios where the same instance is used in multiple scenarios to prevent cases
+ * in which a general cancel operation for one scenario cancels other reads for the same query.
+ * If the value is an empty string all current reads are immediately cancelled.
+ */
+ override fun cancelReads(nextQuery: String) {
+ if (nextQuery.isEmpty() || lastCancelledQuery != nextQuery) {
+ lastCancelledQuery = nextQuery
+ interruptCurrentReads()
+ readScope.coroutineContext.cancelChildren()
+ }
+ }
+
+ /**
+ * Stop all current write operations.
+ * Allows immediately dismissing all write operations and clearing the write queue.
+ */
+ internal fun interruptCurrentWrites() {
+ handlePlacesExceptions("interruptCurrentWrites") {
+ writer.interrupt()
+ }
+ }
+
+ /**
+ * Stop all current read queries.
+ * Allows avoiding having to wait for stale queries responses and clears the queries queue.
+ */
+ internal fun interruptCurrentReads() {
+ handlePlacesExceptions("interruptCurrentReads") {
+ reader.interrupt()
+ }
+ }
+
+ /**
+ * Runs [block] described by [operation], ignoring and logging non-fatal exceptions.
+ *
+ * @param operation the name of the operation to run.
+ * @param block the operation to run.
+ */
+ protected inline fun handlePlacesExceptions(operation: String, block: () -> Unit) {
+ try {
+ block()
+ } catch (e: MalformedInputException) {
+ crashReporter?.submitCaughtException(e)
+ logger.debug("Ignoring invalid invalid non utf-8 character when running $operation", e)
+ } catch (e: PlacesApiException.OperationInterrupted) {
+ logger.debug("Ignoring expected OperationInterrupted exception when running $operation", e)
+ } catch (e: PlacesApiException.UrlParseFailed) {
+ // it's not uncommon to get a mal-formed url, probably the user typing.
+ logger.debug("Ignoring invalid URL while running $operation", e)
+ } catch (e: PlacesApiException) {
+ crashReporter?.submitCaughtException(e)
+ logger.warn("Ignoring PlacesApiException while running $operation", e)
+ }
+ }
+
+ /**
+ * Runs [block] described by [operation] to return a result of type [T], ignoring and
+ * logging non-fatal exceptions. In case of a non-fatal exception, the provided
+ * [default] value is returned.
+ *
+ * @param operation the name of the operation to run.
+ * @param block the operation to run.
+ * @param default the default value to return in case of errors.
+ */
+ inline fun <T> handlePlacesExceptions(
+ operation: String,
+ default: T,
+ block: () -> T,
+ ): T {
+ return try {
+ block()
+ } catch (e: PlacesApiException.OperationInterrupted) {
+ logger.debug("Ignoring expected OperationInterrupted exception when running $operation", e)
+ default
+ } catch (e: PlacesApiException.UrlParseFailed) {
+ // it's not uncommon to get a mal-formed url, probably the user typing.
+ logger.debug("Ignoring invalid URL while running $operation", e)
+ default
+ } catch (e: PlacesApiException) {
+ crashReporter?.submitCaughtException(e)
+ logger.warn("Ignoring PlacesApiException while running $operation", e)
+ default
+ }
+ }
+
+ /**
+ * Runs a [syncBlock], re-throwing any panics that may be encountered.
+ * @return [SyncStatus.Ok] on success, or [SyncStatus.Error] on non-panic [PlacesApiException].
+ * (Note that a panic is represented by an mozilla.appservices.places.uniffi.InternalException,
+ * which isn't part of the [PlacesApiException] error hierarchy)
+ */
+ protected inline fun syncAndHandleExceptions(syncBlock: () -> Unit): SyncStatus {
+ return try {
+ logger.debug("Syncing...")
+ syncBlock()
+ logger.debug("Successfully synced.")
+ SyncStatus.Ok
+ } catch (e: PlacesApiException) {
+ crashReporter?.submitCaughtException(e)
+ logger.error("Places exception while syncing", e)
+ SyncStatus.Error(e)
+ }
+ }
+
+ /**
+ * Registers a storage maintenance worker that prunes database when its size exceeds a size limit.
+ * */
+ override fun registerStorageMaintenanceWorker() {
+ // See child classes for implementation details, it is not implemented by default
+ }
+
+ /**
+ * Unregisters the storage maintenance worker that is registered
+ * by [PlacesStorage.registerStorageMaintenanceWorker].
+ *
+ * @param uniqueWorkName Unique name of the work request that needs to be unregistered
+ * */
+ override fun unregisterStorageMaintenanceWorker(uniqueWorkName: String) {
+ // See child classes for implementation details, it is not implemented by default
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt
new file mode 100644
index 0000000000..0becb645af
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt
@@ -0,0 +1,154 @@
+/* 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.browser.storage.sync
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.withContext
+import mozilla.appservices.remotetabs.RemoteTab
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.storage.Storage
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.SyncableStore
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.logElapsedTime
+import java.io.File
+import mozilla.appservices.remotetabs.TabsApiException as RemoteTabProviderException
+import mozilla.appservices.remotetabs.TabsStore as RemoteTabsProvider
+
+private const val TABS_DB_NAME = "tabs.sqlite"
+
+/**
+ * An interface which defines read/write methods for remote tabs data.
+ */
+open class RemoteTabsStorage(
+ private val context: Context,
+ private val crashReporter: CrashReporting? = null,
+) : Storage, SyncableStore {
+ internal val api by lazy { RemoteTabsProvider(File(context.filesDir, TABS_DB_NAME).canonicalPath) }
+ private val scope by lazy { CoroutineScope(Dispatchers.IO) }
+ internal val logger = Logger("RemoteTabsStorage")
+
+ override suspend fun warmUp() {
+ logElapsedTime(logger, "Warming up storage") { api }
+ }
+
+ /**
+ * Store the locally opened tabs.
+ * @param tabs The list of opened tabs, for all opened non-private windows, on this device.
+ */
+ suspend fun store(tabs: List<Tab>) {
+ return withContext(scope.coroutineContext) {
+ try {
+ api.setLocalTabs(
+ tabs.map {
+ val activeTab = it.active()
+ val urlHistory = listOf(activeTab.url) + it.previous().reversed().map { it.url }
+ RemoteTab(activeTab.title, urlHistory, activeTab.iconUrl, it.lastUsed, it.inactive)
+ },
+ )
+ } catch (e: RemoteTabProviderException) {
+ crashReporter?.submitCaughtException(e)
+ }
+ }
+ }
+
+ /**
+ * Get all remote devices tabs.
+ * @return A mapping of opened tabs per device.
+ */
+ suspend fun getAll(): Map<SyncClient, List<Tab>> {
+ return withContext(scope.coroutineContext) {
+ try {
+ api.getAll().map { device ->
+ val tabs = device.remoteTabs.map { tab ->
+ // Map RemoteTab to TabEntry
+ val title = tab.title
+ val icon = tab.icon
+ val lastUsed = tab.lastUsed
+ val history = tab.urlHistory.reversed().map { url -> TabEntry(title, url, icon) }
+ Tab(history, tab.urlHistory.lastIndex, lastUsed, tab.inactive)
+ }
+ // Map device to tabs
+ SyncClient(device.clientId) to tabs
+ }.toMap()
+ } catch (e: RemoteTabProviderException) {
+ crashReporter?.submitCaughtException(e)
+ return@withContext emptyMap()
+ }
+ }
+ }
+
+ override suspend fun runMaintenance(dbSizeLimit: UInt) {
+ // Storage maintenance workflow for remote tabs is not implemented yet.
+ }
+
+ override fun cleanup() {
+ scope.coroutineContext.cancelChildren()
+ }
+
+ override fun registerWithSyncManager() {
+ return api.registerWithSyncManager()
+ }
+}
+
+/**
+ * Represents a Sync client that can be associated with a list of opened tabs.
+ */
+data class SyncClient(val id: String)
+
+/**
+ * A tab, which is defined by an history (think the previous/next button in your web browser) and
+ * a currently active history entry.
+ */
+data class Tab(
+ val history: List<TabEntry>,
+ val active: Int,
+ val lastUsed: Long,
+ val inactive: Boolean,
+) {
+ /**
+ * The current active tab entry. In other words, this is the page that's currently shown for a
+ * tab.
+ */
+ fun active(): TabEntry {
+ return history[active]
+ }
+
+ /**
+ * The list of tabs history entries that come before this tab. In other words, the "previous"
+ * navigation button history list.
+ */
+ fun previous(): List<TabEntry> {
+ return history.subList(0, active)
+ }
+
+ /**
+ * The list of tabs history entries that come after this tab. In other words, the "next"
+ * navigation button history list.
+ */
+ fun next(): List<TabEntry> {
+ return history.subList(active + 1, history.lastIndex + 1)
+ }
+}
+
+/**
+ * A synced device and its list of tabs.
+ */
+data class SyncedDeviceTabs(
+ val device: Device,
+ val tabs: List<Tab>,
+)
+
+/**
+ * A Tab history entry.
+ */
+data class TabEntry(
+ val title: String,
+ val url: String,
+ val iconUrl: String?,
+)
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageExtensions.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageExtensions.kt
new file mode 100644
index 0000000000..79bba0480c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageExtensions.kt
@@ -0,0 +1,44 @@
+/* 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.browser.storage.sync
+
+import androidx.work.Constraints
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import mozilla.components.concept.storage.Storage
+import java.util.concurrent.TimeUnit
+
+/**
+ * Builds and returns a [PeriodicWorkRequest] based on [PeriodicWorkRequestBuilder]
+ * with a given [StorageMaintenanceWorker].
+ *
+ * @param repeatInterval Repeat interval of the periodic work request.
+ * @param repeatIntervalTimeUnit Time unit for the repeat interval of the work request.
+ * @param tag Work request's tag.
+ * @param constraints A block that returns the [Constraints] for the work.
+ * @return [PeriodicWorkRequest].
+ * */
+inline fun <reified T : StorageMaintenanceWorker> Storage.periodicStorageWorkRequest(
+ repeatInterval: Long = StorageMaintenanceWorker.WORKER_PERIOD_IN_HOURS,
+ repeatIntervalTimeUnit: TimeUnit = TimeUnit.HOURS,
+ tag: String?,
+ constraints: Storage.() -> Constraints,
+): PeriodicWorkRequest {
+ return PeriodicWorkRequestBuilder<T>(
+ repeatInterval,
+ repeatIntervalTimeUnit,
+ ).apply {
+ setConstraints(constraints())
+ tag?.let { addTag(tag) }
+ }.build()
+}
+
+/**
+ * Builds and returns a [Constraints] based on [Constraints.Builder] provided by [block].
+ * @return [Constraints].
+ * */
+fun constraints(block: Constraints.Builder.() -> Unit): Constraints {
+ return Constraints.Builder().apply { block() }.build()
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageMaintenanceWorker.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageMaintenanceWorker.kt
new file mode 100644
index 0000000000..873c68a082
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageMaintenanceWorker.kt
@@ -0,0 +1,48 @@
+/* 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.browser.storage.sync
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * An abstract WorkManager Worker class that executes maintenance task provided via [operate].
+ *
+ * If there is a failure or the constraints of worker are no longer met during execution,
+ * cancellation tasks are executed via [onError].
+ */
+abstract class StorageMaintenanceWorker(context: Context, params: WorkerParameters) :
+ CoroutineWorker(context, params) {
+
+ @Suppress("TooGenericExceptionCaught")
+ final override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
+ try {
+ operate()
+ Result.success()
+ } catch (exception: Exception) {
+ onError(exception)
+ Result.failure()
+ }
+ }
+
+ /**
+ * Called when [doWork] is being executed.
+ * */
+ abstract suspend fun operate()
+
+ /**
+ * Called when [doWork] causes an exception while being executed.
+ *
+ * @param exception Exception is passed to the child overriding the method.
+ * */
+ abstract fun onError(exception: Exception)
+
+ companion object {
+ const val WORKER_PERIOD_IN_HOURS = 12L
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Types.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Types.kt
new file mode 100644
index 0000000000..67974ef098
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Types.kt
@@ -0,0 +1,241 @@
+/* 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/. */
+
+@file:Suppress("MatchingDeclarationName")
+
+package mozilla.components.browser.storage.sync
+
+import mozilla.appservices.places.SyncAuthInfo
+import mozilla.appservices.places.uniffi.BookmarkItem
+import mozilla.components.concept.storage.BookmarkNode
+import mozilla.components.concept.storage.BookmarkNodeType
+import mozilla.components.concept.storage.DocumentType
+import mozilla.components.concept.storage.FrecencyThresholdOption
+import mozilla.components.concept.storage.HistoryHighlight
+import mozilla.components.concept.storage.HistoryHighlightWeights
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.concept.storage.HistoryMetadataObservation
+import mozilla.components.concept.storage.TopFrecentSiteInfo
+import mozilla.components.concept.storage.VisitInfo
+import mozilla.components.concept.storage.VisitType
+
+// We have type definitions at the concept level, and "external" types defined within Places.
+// In practice these two types are largely the same, and this file is the conversion point.
+
+/**
+ * Conversion from our SyncAuthInfo into its "native" version used at the interface boundary.
+ */
+internal fun mozilla.components.concept.sync.SyncAuthInfo.into(): SyncAuthInfo {
+ return SyncAuthInfo(
+ kid = this.kid,
+ fxaAccessToken = this.fxaAccessToken,
+ syncKey = this.syncKey,
+ tokenserverURL = this.tokenServerUrl,
+ )
+}
+
+/**
+ * Conversion from a generic [FrecencyThresholdOption] into its richer comrade within the 'places' lib.
+ */
+internal fun FrecencyThresholdOption.into() = when (this) {
+ FrecencyThresholdOption.NONE -> mozilla.appservices.places.uniffi.FrecencyThresholdOption.NONE
+ FrecencyThresholdOption.SKIP_ONE_TIME_PAGES ->
+ mozilla.appservices.places.uniffi.FrecencyThresholdOption.SKIP_ONE_TIME_PAGES
+}
+
+/**
+ * Conversion from a generic [VisitType] into its richer comrade within the 'places' lib.
+ */
+internal fun VisitType.into() = when (this) {
+ VisitType.LINK -> mozilla.appservices.places.uniffi.VisitType.LINK
+ VisitType.RELOAD -> mozilla.appservices.places.uniffi.VisitType.RELOAD
+ VisitType.TYPED -> mozilla.appservices.places.uniffi.VisitType.TYPED
+ VisitType.BOOKMARK -> mozilla.appservices.places.uniffi.VisitType.BOOKMARK
+ VisitType.EMBED -> mozilla.appservices.places.uniffi.VisitType.EMBED
+ VisitType.REDIRECT_PERMANENT -> mozilla.appservices.places.uniffi.VisitType.REDIRECT_PERMANENT
+ VisitType.REDIRECT_TEMPORARY -> mozilla.appservices.places.uniffi.VisitType.REDIRECT_TEMPORARY
+ VisitType.DOWNLOAD -> mozilla.appservices.places.uniffi.VisitType.DOWNLOAD
+ VisitType.FRAMED_LINK -> mozilla.appservices.places.uniffi.VisitType.FRAMED_LINK
+}
+
+internal fun mozilla.appservices.places.uniffi.VisitType.into() = when (this) {
+ mozilla.appservices.places.uniffi.VisitType.UPDATE_PLACE -> VisitType.LINK
+ mozilla.appservices.places.uniffi.VisitType.LINK -> VisitType.LINK
+ mozilla.appservices.places.uniffi.VisitType.RELOAD -> VisitType.RELOAD
+ mozilla.appservices.places.uniffi.VisitType.TYPED -> VisitType.TYPED
+ mozilla.appservices.places.uniffi.VisitType.BOOKMARK -> VisitType.BOOKMARK
+ mozilla.appservices.places.uniffi.VisitType.EMBED -> VisitType.EMBED
+ mozilla.appservices.places.uniffi.VisitType.REDIRECT_PERMANENT -> VisitType.REDIRECT_PERMANENT
+ mozilla.appservices.places.uniffi.VisitType.REDIRECT_TEMPORARY -> VisitType.REDIRECT_TEMPORARY
+ mozilla.appservices.places.uniffi.VisitType.DOWNLOAD -> VisitType.DOWNLOAD
+ mozilla.appservices.places.uniffi.VisitType.FRAMED_LINK -> VisitType.FRAMED_LINK
+}
+
+internal fun mozilla.appservices.places.uniffi.HistoryVisitInfo.into(): VisitInfo {
+ return VisitInfo(
+ url = this.url,
+ title = this.title,
+ visitTime = this.timestamp,
+ visitType = this.visitType.into(),
+ previewImageUrl = this.previewImageUrl,
+ isRemote = this.isRemote,
+ )
+}
+
+internal fun mozilla.appservices.places.uniffi.TopFrecentSiteInfo.into(): TopFrecentSiteInfo {
+ return TopFrecentSiteInfo(
+ url = this.url,
+ title = this.title,
+ )
+}
+
+internal fun BookmarkItem.asBookmarkNode(): BookmarkNode {
+ return when (this) {
+ is BookmarkItem.Bookmark -> {
+ BookmarkNode(
+ BookmarkNodeType.ITEM,
+ this.b.guid,
+ this.b.parentGuid,
+ this.b.position,
+ this.b.title,
+ this.b.url,
+ this.b.dateAdded,
+ null,
+ )
+ }
+ is BookmarkItem.Folder -> {
+ BookmarkNode(
+ BookmarkNodeType.FOLDER,
+ this.f.guid,
+ this.f.parentGuid,
+ this.f.position,
+ this.f.title,
+ null,
+ this.f.dateAdded,
+ this.f.childNodes?.map(BookmarkItem::asBookmarkNode),
+ )
+ }
+ is BookmarkItem.Separator -> {
+ BookmarkNode(
+ BookmarkNodeType.SEPARATOR,
+ this.s.guid,
+ this.s.parentGuid,
+ this.s.position,
+ null,
+ null,
+ this.s.dateAdded,
+ null,
+ )
+ }
+ }
+}
+
+internal fun HistoryMetadataKey.into(): mozilla.appservices.places.HistoryMetadataKey {
+ return mozilla.appservices.places.HistoryMetadataKey(
+ url = this.url,
+ referrerUrl = this.referrerUrl,
+ searchTerm = this.searchTerm,
+ )
+}
+
+internal fun mozilla.appservices.places.HistoryMetadataKey.into(): HistoryMetadataKey {
+ return HistoryMetadataKey(
+ url = this.url,
+ referrerUrl = if (this.referrerUrl.isNullOrEmpty()) { null } else { this.referrerUrl },
+ searchTerm = if (this.searchTerm.isNullOrEmpty()) { null } else { this.searchTerm },
+ )
+}
+
+internal fun mozilla.appservices.places.uniffi.DocumentType.into(): DocumentType {
+ return when (this) {
+ mozilla.appservices.places.uniffi.DocumentType.REGULAR -> DocumentType.Regular
+ mozilla.appservices.places.uniffi.DocumentType.MEDIA -> DocumentType.Media
+ }
+}
+
+internal fun mozilla.appservices.places.uniffi.HistoryMetadata.into(): HistoryMetadata {
+ // Protobuf doesn't support passing around `null` value, so these get converted to some defaults
+ // as they go from Rust to Kotlin. E.g. an empty string in place of a `null`.
+ // That means places.HistoryMetadata will never have `null` values.
+ // But, we actually do want a real `null` value here - hence the explicit check.
+ return HistoryMetadata(
+ key = HistoryMetadataKey(url = this.url, searchTerm = this.searchTerm, referrerUrl = this.referrerUrl),
+ title = if (this.title.isNullOrEmpty()) null else this.title,
+ createdAt = this.createdAt,
+ updatedAt = this.updatedAt,
+ totalViewTime = this.totalViewTime,
+ documentType = this.documentType.into(),
+ previewImageUrl = this.previewImageUrl,
+ )
+}
+
+internal fun List<mozilla.appservices.places.uniffi.HistoryMetadata>.into(): List<HistoryMetadata> {
+ return map { it.into() }
+}
+
+internal fun mozilla.appservices.places.uniffi.HistoryHighlight.into(): HistoryHighlight {
+ return HistoryHighlight(
+ score = this.score,
+ placeId = this.placeId,
+ url = this.url,
+ title = this.title,
+ previewImageUrl = this.previewImageUrl,
+ )
+}
+
+internal fun List<mozilla.appservices.places.uniffi.HistoryHighlight>.intoHighlights(): List<HistoryHighlight> {
+ return map { it.into() }
+}
+
+internal fun HistoryHighlightWeights.into(): mozilla.appservices.places.uniffi.HistoryHighlightWeights {
+ return mozilla.appservices.places.uniffi.HistoryHighlightWeights(
+ viewTime = this.viewTime,
+ frequency = this.frequency,
+ )
+}
+
+internal fun DocumentType.into(): mozilla.appservices.places.uniffi.DocumentType {
+ return when (this) {
+ DocumentType.Regular -> mozilla.appservices.places.uniffi.DocumentType.REGULAR
+ DocumentType.Media -> mozilla.appservices.places.uniffi.DocumentType.MEDIA
+ }
+}
+
+internal fun HistoryMetadata.into(): mozilla.appservices.places.uniffi.HistoryMetadata {
+ return mozilla.appservices.places.uniffi.HistoryMetadata(
+ url = this.key.url,
+ searchTerm = this.key.searchTerm,
+ referrerUrl = this.key.referrerUrl,
+ title = this.title,
+ createdAt = this.createdAt,
+ updatedAt = this.updatedAt,
+ totalViewTime = this.totalViewTime,
+ documentType = this.documentType.into(),
+ previewImageUrl = this.previewImageUrl,
+ )
+}
+
+internal fun HistoryMetadataObservation.into(
+ key: HistoryMetadataKey,
+): mozilla.appservices.places.uniffi.HistoryMetadataObservation {
+ return when (this) {
+ is HistoryMetadataObservation.ViewTimeObservation -> {
+ mozilla.appservices.places.uniffi.HistoryMetadataObservation(
+ url = key.url,
+ searchTerm = key.searchTerm,
+ referrerUrl = key.referrerUrl,
+ viewTime = this.viewTime,
+ )
+ }
+ is HistoryMetadataObservation.DocumentTypeObservation -> {
+ mozilla.appservices.places.uniffi.HistoryMetadataObservation(
+ url = key.url,
+ searchTerm = key.searchTerm,
+ referrerUrl = key.referrerUrl,
+ documentType = this.documentType.into(),
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProviderTest.kt b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProviderTest.kt
new file mode 100644
index 0000000000..6d071aa85d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProviderTest.kt
@@ -0,0 +1,32 @@
+/* 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.browser.storage.sync
+
+import mozilla.components.support.test.mock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class GlobalPlacesDependencyProviderTest {
+
+ @Before
+ @After
+ fun cleanUp() {
+ GlobalPlacesDependencyProvider.placesStorage = null
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `requirePlacesStorage called without calling initialize, exception returned`() {
+ GlobalPlacesDependencyProvider.requirePlacesStorage()
+ }
+
+ @Test
+ fun `requirePlacesStorage called after calling initialize, placesStorage returned`() {
+ val placesStorage = mock<PlacesStorage>()
+ GlobalPlacesDependencyProvider.initialize(placesStorage)
+ assertEquals(placesStorage, GlobalPlacesDependencyProvider.requirePlacesStorage())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt
new file mode 100644
index 0000000000..3ff44d46c2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt
@@ -0,0 +1,239 @@
+/* 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.browser.storage.sync
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.appservices.places.BookmarkRoot
+import mozilla.appservices.places.uniffi.PlacesApiException
+import mozilla.components.concept.storage.BookmarkInfo
+import mozilla.components.concept.storage.BookmarkNode
+import mozilla.components.concept.storage.BookmarkNodeType
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class PlacesBookmarksStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var bookmarks: PlacesBookmarksStorage
+
+ @Before
+ fun setup() = runTestOnMain {
+ bookmarks = PlacesBookmarksStorage(testContext)
+ // There's a database on disk which needs to be cleaned up between tests.
+ bookmarks.writer.deleteEverything()
+ }
+
+ @After
+ @Suppress("DEPRECATION")
+ fun cleanup() = runTestOnMain {
+ bookmarks.cleanup()
+ }
+
+ @Test
+ fun `get bookmarks tree by root, recursive or not`() = runTestOnMain {
+ val tree = bookmarks.getTree(BookmarkRoot.Root.id)!!
+ assertEquals(BookmarkRoot.Root.id, tree.guid)
+ assertNotNull(tree.children)
+ assertEquals(4, tree.children!!.size)
+
+ var children = tree.children!!.map { it.guid }
+ assertTrue(BookmarkRoot.Mobile.id in children)
+ assertTrue(BookmarkRoot.Unfiled.id in children)
+ assertTrue(BookmarkRoot.Toolbar.id in children)
+ assertTrue(BookmarkRoot.Menu.id in children)
+
+ // Non-recursive means children of children aren't fetched.
+ for (child in tree.children!!) {
+ assertNull(child.children)
+ assertEquals(BookmarkRoot.Root.id, child.parentGuid)
+ assertEquals(BookmarkNodeType.FOLDER, child.type)
+ }
+
+ val deepTree = bookmarks.getTree(BookmarkRoot.Root.id, true)!!
+ assertEquals(BookmarkRoot.Root.id, deepTree.guid)
+ assertNotNull(deepTree.children)
+ assertEquals(4, deepTree.children!!.size)
+
+ children = deepTree.children!!.map { it.guid }
+ assertTrue(BookmarkRoot.Mobile.id in children)
+ assertTrue(BookmarkRoot.Unfiled.id in children)
+ assertTrue(BookmarkRoot.Toolbar.id in children)
+ assertTrue(BookmarkRoot.Menu.id in children)
+
+ // Recursive means children of children are fetched.
+ for (child in deepTree.children!!) {
+ // For an empty tree, we expect to see empty lists.
+ assertEquals(emptyList<BookmarkNode>(), child.children)
+ assertEquals(BookmarkRoot.Root.id, child.parentGuid)
+ assertEquals(BookmarkNodeType.FOLDER, child.type)
+ }
+ }
+
+ @Test
+ fun `bookmarks APIs smoke testing - basic operations`() = runTestOnMain {
+ val url = "http://www.mozilla.org"
+
+ assertEquals(emptyList<BookmarkNode>(), bookmarks.getBookmarksWithUrl(url))
+ assertEquals(emptyList<BookmarkNode>(), bookmarks.searchBookmarks("mozilla"))
+
+ val insertedItem = bookmarks.addItem(BookmarkRoot.Mobile.id, url, "Mozilla", 5u)
+
+ with(bookmarks.getBookmarksWithUrl(url)) {
+ assertEquals(1, this.size)
+ with(this[0]) {
+ assertEquals(insertedItem, this.guid)
+ assertEquals(BookmarkNodeType.ITEM, this.type)
+ assertEquals("Mozilla", this.title)
+ assertEquals(BookmarkRoot.Mobile.id, this.parentGuid)
+ // Clamped to actual range. 'Mobile' was empty, so we get 0 back.
+ assertEquals(0u, this.position)
+ assertEquals("http://www.mozilla.org/", this.url)
+ }
+ }
+
+ val folderGuid = bookmarks.addFolder(BookmarkRoot.Mobile.id, "Test Folder", null)
+ bookmarks.updateNode(
+ insertedItem,
+ BookmarkInfo(
+ parentGuid = folderGuid,
+ title = null,
+ position = 9999u,
+ url = null,
+ ),
+ )
+ with(bookmarks.getBookmarksWithUrl(url)) {
+ assertEquals(1, this.size)
+ with(this[0]) {
+ assertEquals(insertedItem, this.guid)
+ assertEquals(BookmarkNodeType.ITEM, this.type)
+ assertEquals("Mozilla", this.title)
+ assertEquals(folderGuid, this.parentGuid)
+ assertEquals(0u, this.position)
+ assertEquals("http://www.mozilla.org/", this.url)
+ }
+ }
+
+ val separatorGuid = bookmarks.addSeparator(folderGuid, 1u)
+ with(bookmarks.getTree(folderGuid)!!) {
+ assertEquals(2, this.children!!.size)
+ assertEquals(BookmarkNodeType.SEPARATOR, this.children!![1].type)
+ }
+
+ assertTrue(bookmarks.deleteNode(separatorGuid))
+ with(bookmarks.getTree(folderGuid)!!) {
+ assertEquals(1, this.children!!.size)
+ assertEquals(BookmarkNodeType.ITEM, this.children!![0].type)
+ }
+
+ with(bookmarks.searchBookmarks("mozilla")) {
+ assertEquals(1, this.size)
+ assertEquals("http://www.mozilla.org/", this[0].url)
+ }
+
+ with(bookmarks.getBookmark(folderGuid)!!) {
+ assertEquals(folderGuid, this.guid)
+ assertEquals("Test Folder", this.title)
+ assertEquals(BookmarkRoot.Mobile.id, this.parentGuid)
+ }
+
+ with(bookmarks.getRecentBookmarks(1)) {
+ assertEquals(insertedItem, this[0].guid)
+ }
+
+ with(bookmarks.getRecentBookmarks(1, TimeUnit.DAYS.toMillis(1))) {
+ assertEquals(insertedItem, this[0].guid)
+ }
+
+ with(bookmarks.getRecentBookmarks(1, 99, System.currentTimeMillis() + 100)) {
+ assertTrue(this.isEmpty())
+ }
+
+ val secondInsertedItem = bookmarks.addItem(BookmarkRoot.Unfiled.id, url, "Mozilla", 6u)
+
+ with(bookmarks.getRecentBookmarks(2)) {
+ assertEquals(secondInsertedItem, this[0].guid)
+ assertEquals(insertedItem, this[1].guid)
+ }
+
+ with(bookmarks.getRecentBookmarks(2, TimeUnit.DAYS.toMillis(1))) {
+ assertEquals(secondInsertedItem, this[0].guid)
+ assertEquals(insertedItem, this[1].guid)
+ }
+
+ with(bookmarks.getRecentBookmarks(2, 99, System.currentTimeMillis() + 100)) {
+ assertTrue(this.isEmpty())
+ }
+
+ assertTrue(bookmarks.deleteNode(secondInsertedItem))
+ assertTrue(bookmarks.deleteNode(folderGuid))
+
+ for (
+ root in listOf(
+ BookmarkRoot.Mobile,
+ BookmarkRoot.Root,
+ BookmarkRoot.Menu,
+ BookmarkRoot.Toolbar,
+ BookmarkRoot.Unfiled,
+ )
+ ) {
+ try {
+ bookmarks.deleteNode(root.id)
+ fail("Expected root deletion for ${root.id} to fail")
+ } catch (e: PlacesApiException.InvalidBookmarkOperation) {}
+ }
+
+ with(bookmarks.searchBookmarks("mozilla")) {
+ assertTrue(this.isEmpty())
+ }
+ }
+
+ @Test
+ fun `GIVEN bookmarks exist WHEN asked for autocomplete suggestions THEN return the first matching bookmark`() = runTest {
+ bookmarks.apply {
+ addItem(BookmarkRoot.Mobile.id, "https://www.mozilla.org/en-us/firefox", "Mozilla", 5u)
+ addItem(BookmarkRoot.Toolbar.id, "https://support.mozilla.org/", "Support", 2u)
+ }
+
+ // Try querying for a bookmarks that doesn't exist
+ var suggestion = bookmarks.getAutocompleteSuggestion("test")
+ assertNull(suggestion)
+
+ // And now for ones that do exist
+ suggestion = bookmarks.getAutocompleteSuggestion("moz")
+ assertNotNull(suggestion)
+ assertEquals("moz", suggestion?.input)
+ // There are multiple bookmarks from the mozilla host with no guarantee about the read order.
+ // Use a smaller URL that would match all.
+ assertTrue(suggestion?.text?.startsWith("mozilla.org/en-us/") ?: false)
+ assertTrue(suggestion?.url?.startsWith("https://www.mozilla.org/en-us/") ?: false)
+ assertEquals(BOOKMARKS_AUTOCOMPLETE_SOURCE_NAME, suggestion?.source)
+ assertEquals(1, suggestion?.totalItems)
+
+ suggestion = bookmarks.getAutocompleteSuggestion("sup")
+ assertNotNull(suggestion)
+ assertEquals("sup", suggestion?.input)
+ assertEquals("support.mozilla.org/", suggestion?.text)
+ assertEquals("https://support.mozilla.org/", suggestion?.url)
+ assertEquals(BOOKMARKS_AUTOCOMPLETE_SOURCE_NAME, suggestion?.source)
+ assertEquals(1, suggestion?.totalItems)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt
new file mode 100644
index 0000000000..885ef1619a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt
@@ -0,0 +1,1312 @@
+/* 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.browser.storage.sync
+
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.Configuration
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.testing.WorkManagerTestInitHelper
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import mozilla.appservices.places.PlacesReaderConnection
+import mozilla.appservices.places.PlacesWriterConnection
+import mozilla.appservices.places.uniffi.InternalException
+import mozilla.appservices.places.uniffi.PlacesApiException
+import mozilla.appservices.places.uniffi.VisitObservation
+import mozilla.components.concept.storage.DocumentType
+import mozilla.components.concept.storage.FrecencyThresholdOption
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.concept.storage.HistoryMetadataObservation
+import mozilla.components.concept.storage.PageObservation
+import mozilla.components.concept.storage.PageVisit
+import mozilla.components.concept.storage.VisitType
+import mozilla.components.concept.sync.SyncAuthInfo
+import mozilla.components.concept.sync.SyncStatus
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.core.Is.`is`
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class PlacesHistoryStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var history: PlacesHistoryStorage
+
+ @Before
+ fun setup() = runTestOnMain {
+ history = PlacesHistoryStorage(testContext, mock())
+ // There's a database on disk which needs to be cleaned up between tests.
+ history.deleteEverything()
+ }
+
+ @After
+ @Suppress("DEPRECATION")
+ fun cleanup() = runTestOnMain {
+ history.cleanup()
+ }
+
+ @Test
+ fun `storage allows recording and querying visits of different types`() = runTestOnMain {
+ history.recordVisit("http://www.firefox.com/1", PageVisit(VisitType.LINK))
+ history.recordVisit("http://www.firefox.com/2", PageVisit(VisitType.RELOAD))
+ history.recordVisit("http://www.firefox.com/3", PageVisit(VisitType.TYPED))
+ history.recordVisit("http://www.firefox.com/4", PageVisit(VisitType.REDIRECT_TEMPORARY))
+ history.recordVisit("http://www.firefox.com/5", PageVisit(VisitType.REDIRECT_PERMANENT))
+ history.recordVisit("http://www.firefox.com/6", PageVisit(VisitType.FRAMED_LINK))
+ history.recordVisit("http://www.firefox.com/7", PageVisit(VisitType.EMBED))
+ history.recordVisit("http://www.firefox.com/8", PageVisit(VisitType.BOOKMARK))
+ history.recordVisit("http://www.firefox.com/9", PageVisit(VisitType.DOWNLOAD))
+
+ val recordedVisits = history.getDetailedVisits(0)
+ assertEquals(9, recordedVisits.size)
+ assertEquals("http://www.firefox.com/1", recordedVisits[0].url)
+ assertEquals(VisitType.LINK, recordedVisits[0].visitType)
+ assertEquals("http://www.firefox.com/2", recordedVisits[1].url)
+ assertEquals(VisitType.RELOAD, recordedVisits[1].visitType)
+ assertEquals("http://www.firefox.com/3", recordedVisits[2].url)
+ assertEquals(VisitType.TYPED, recordedVisits[2].visitType)
+ assertEquals("http://www.firefox.com/4", recordedVisits[3].url)
+ assertEquals(VisitType.REDIRECT_TEMPORARY, recordedVisits[3].visitType)
+ assertEquals("http://www.firefox.com/5", recordedVisits[4].url)
+ assertEquals(VisitType.REDIRECT_PERMANENT, recordedVisits[4].visitType)
+ assertEquals("http://www.firefox.com/6", recordedVisits[5].url)
+ assertEquals(VisitType.FRAMED_LINK, recordedVisits[5].visitType)
+ assertEquals("http://www.firefox.com/7", recordedVisits[6].url)
+ assertEquals(VisitType.EMBED, recordedVisits[6].visitType)
+ assertEquals("http://www.firefox.com/8", recordedVisits[7].url)
+ assertEquals(VisitType.BOOKMARK, recordedVisits[7].visitType)
+ assertEquals("http://www.firefox.com/9", recordedVisits[8].url)
+ assertEquals(VisitType.DOWNLOAD, recordedVisits[8].visitType)
+
+ // Can use WebView-style getVisited API.
+ assertEquals(
+ listOf(
+ "http://www.firefox.com/1", "http://www.firefox.com/2", "http://www.firefox.com/3",
+ "http://www.firefox.com/4", "http://www.firefox.com/5", "http://www.firefox.com/6",
+ "http://www.firefox.com/7", "http://www.firefox.com/8", "http://www.firefox.com/9",
+ ),
+ history.getVisited(),
+ )
+
+ // Can use GeckoView-style getVisited API.
+ assertEquals(
+ listOf(false, true, true, true, true, true, true, false, true, true, true),
+ history.getVisited(
+ listOf(
+ "http://www.mozilla.com",
+ "http://www.firefox.com/1", "http://www.firefox.com/2", "http://www.firefox.com/3",
+ "http://www.firefox.com/4", "http://www.firefox.com/5", "http://www.firefox.com/6",
+ "http://www.firefox.com/oops",
+ "http://www.firefox.com/7", "http://www.firefox.com/8", "http://www.firefox.com/9",
+ ),
+ ),
+ )
+
+ // Can query using pagination.
+ val page1 = history.getVisitsPaginated(0, 3)
+ assertEquals(3, page1.size)
+ assertEquals("http://www.firefox.com/9", page1[0].url)
+ assertEquals("http://www.firefox.com/8", page1[1].url)
+ assertEquals("http://www.firefox.com/7", page1[2].url)
+
+ // Can exclude visit types during pagination.
+ val page1Limited = history.getVisitsPaginated(0, 10, listOf(VisitType.REDIRECT_PERMANENT, VisitType.REDIRECT_TEMPORARY))
+ assertEquals(7, page1Limited.size)
+ assertEquals("http://www.firefox.com/9", page1Limited[0].url)
+ assertEquals("http://www.firefox.com/8", page1Limited[1].url)
+ assertEquals("http://www.firefox.com/7", page1Limited[2].url)
+ assertEquals("http://www.firefox.com/6", page1Limited[3].url)
+ assertEquals("http://www.firefox.com/3", page1Limited[4].url)
+ assertEquals("http://www.firefox.com/2", page1Limited[5].url)
+ assertEquals("http://www.firefox.com/1", page1Limited[6].url)
+
+ val page2 = history.getVisitsPaginated(3, 3)
+ assertEquals(3, page2.size)
+ assertEquals("http://www.firefox.com/6", page2[0].url)
+ assertEquals("http://www.firefox.com/5", page2[1].url)
+ assertEquals("http://www.firefox.com/4", page2[2].url)
+
+ val page3 = history.getVisitsPaginated(6, 10)
+ assertEquals(3, page3.size)
+ assertEquals("http://www.firefox.com/3", page3[0].url)
+ assertEquals("http://www.firefox.com/2", page3[1].url)
+ assertEquals("http://www.firefox.com/1", page3[2].url)
+ }
+
+ @Test
+ fun `storage passes through recordObservation calls`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.LINK))
+ history.recordObservation("http://www.mozilla.org", PageObservation(title = "Mozilla"))
+
+ var recordedVisits = history.getDetailedVisits(0)
+ assertEquals(1, recordedVisits.size)
+ assertEquals("Mozilla", recordedVisits[0].title)
+ assertNull(recordedVisits[0].previewImageUrl)
+
+ history.recordObservation("http://www.mozilla.org", PageObservation(previewImageUrl = "https://test.com/og-image-url"))
+
+ recordedVisits = history.getDetailedVisits(0)
+ assertEquals(1, recordedVisits.size)
+ assertEquals("Mozilla", recordedVisits[0].title)
+ assertEquals("https://test.com/og-image-url", recordedVisits[0].previewImageUrl)
+ }
+
+ @Test
+ fun `store can be used to query top frecent site information`() = runTestOnMain {
+ val toAdd = listOf(
+ "https://www.example.com/123",
+ "https://www.example.com/123",
+ "https://www.example.com/12345",
+ "https://www.mozilla.com/foo/bar/baz",
+ "https://www.mozilla.com/foo/bar/baz",
+ "https://mozilla.com/a1/b2/c3",
+ "https://news.ycombinator.com/",
+ "https://www.mozilla.com/foo/bar/baz",
+ )
+
+ for (url in toAdd) {
+ history.recordVisit(url, PageVisit(VisitType.LINK))
+ }
+
+ var infos = history.getTopFrecentSites(0, frecencyThreshold = FrecencyThresholdOption.NONE)
+ assertEquals(0, infos.size)
+
+ infos = history.getTopFrecentSites(0, frecencyThreshold = FrecencyThresholdOption.SKIP_ONE_TIME_PAGES)
+ assertEquals(0, infos.size)
+
+ infos = history.getTopFrecentSites(3, frecencyThreshold = FrecencyThresholdOption.NONE)
+ assertEquals(3, infos.size)
+ assertEquals("https://www.mozilla.com/foo/bar/baz", infos[0].url)
+ assertEquals("https://www.example.com/123", infos[1].url)
+ assertEquals("https://news.ycombinator.com/", infos[2].url)
+
+ infos = history.getTopFrecentSites(3, frecencyThreshold = FrecencyThresholdOption.SKIP_ONE_TIME_PAGES)
+ assertEquals(2, infos.size)
+ assertEquals("https://www.mozilla.com/foo/bar/baz", infos[0].url)
+ assertEquals("https://www.example.com/123", infos[1].url)
+
+ infos = history.getTopFrecentSites(5, frecencyThreshold = FrecencyThresholdOption.NONE)
+ assertEquals(5, infos.size)
+ assertEquals("https://www.mozilla.com/foo/bar/baz", infos[0].url)
+ assertEquals("https://www.example.com/123", infos[1].url)
+ assertEquals("https://news.ycombinator.com/", infos[2].url)
+ assertEquals("https://mozilla.com/a1/b2/c3", infos[3].url)
+ assertEquals("https://www.example.com/12345", infos[4].url)
+
+ infos = history.getTopFrecentSites(5, frecencyThreshold = FrecencyThresholdOption.SKIP_ONE_TIME_PAGES)
+ assertEquals(2, infos.size)
+ assertEquals("https://www.mozilla.com/foo/bar/baz", infos[0].url)
+ assertEquals("https://www.example.com/123", infos[1].url)
+
+ infos = history.getTopFrecentSites(100, frecencyThreshold = FrecencyThresholdOption.NONE)
+ assertEquals(5, infos.size)
+ assertEquals("https://www.mozilla.com/foo/bar/baz", infos[0].url)
+ assertEquals("https://www.example.com/123", infos[1].url)
+ assertEquals("https://news.ycombinator.com/", infos[2].url)
+ assertEquals("https://mozilla.com/a1/b2/c3", infos[3].url)
+ assertEquals("https://www.example.com/12345", infos[4].url)
+
+ infos = history.getTopFrecentSites(100, frecencyThreshold = FrecencyThresholdOption.SKIP_ONE_TIME_PAGES)
+ assertEquals(2, infos.size)
+ assertEquals("https://www.mozilla.com/foo/bar/baz", infos[0].url)
+ assertEquals("https://www.example.com/123", infos[1].url)
+
+ infos = history.getTopFrecentSites(-4, frecencyThreshold = FrecencyThresholdOption.SKIP_ONE_TIME_PAGES)
+ assertEquals(0, infos.size)
+ }
+
+ @Test
+ fun `store can be used to query detailed visit information`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.LINK))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.RELOAD))
+ history.recordObservation(
+ "http://www.mozilla.org",
+ PageObservation("Mozilla", "https://test.com/og-image-url"),
+ )
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.LINK))
+
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.REDIRECT_TEMPORARY))
+
+ val visits = history.getDetailedVisits(0, excludeTypes = listOf(VisitType.REDIRECT_TEMPORARY))
+ assertEquals(3, visits.size)
+ assertEquals("http://www.mozilla.org/", visits[0].url)
+ assertEquals("Mozilla", visits[0].title)
+ assertEquals("https://test.com/og-image-url", visits[0].previewImageUrl)
+ assertEquals(VisitType.LINK, visits[0].visitType)
+
+ assertEquals("http://www.mozilla.org/", visits[1].url)
+ assertEquals("Mozilla", visits[1].title)
+ assertEquals(VisitType.RELOAD, visits[1].visitType)
+
+ assertEquals("http://www.firefox.com/", visits[2].url)
+ assertEquals(null, visits[2].title)
+ assertEquals(VisitType.LINK, visits[2].visitType)
+
+ val visitsAll = history.getDetailedVisits(0)
+ assertEquals(4, visitsAll.size)
+ }
+
+ @Test
+ fun `store can be used to record and retrieve history via webview-style callbacks`() = runTestOnMain {
+ // Empty.
+ assertEquals(0, history.getVisited().size)
+
+ // Regular visits are tracked.
+ history.recordVisit("https://www.mozilla.org", PageVisit(VisitType.LINK))
+ assertEquals(listOf("https://www.mozilla.org/"), history.getVisited())
+
+ // Multiple visits can be tracked, results ordered by "URL's first seen first".
+ history.recordVisit("https://www.firefox.com", PageVisit(VisitType.LINK))
+ assertEquals(listOf("https://www.mozilla.org/", "https://www.firefox.com/"), history.getVisited())
+
+ // Visits marked as reloads can be tracked.
+ history.recordVisit("https://www.firefox.com", PageVisit(VisitType.RELOAD))
+ assertEquals(listOf("https://www.mozilla.org/", "https://www.firefox.com/"), history.getVisited())
+
+ // Visited urls are certainly a set.
+ history.recordVisit("https://www.firefox.com", PageVisit(VisitType.LINK))
+ history.recordVisit("https://www.mozilla.org", PageVisit(VisitType.LINK))
+ history.recordVisit("https://www.wikipedia.org", PageVisit(VisitType.LINK))
+ assertEquals(
+ listOf("https://www.mozilla.org/", "https://www.firefox.com/", "https://www.wikipedia.org/"),
+ history.getVisited(),
+ )
+ }
+
+ @Test
+ fun `store can be used to record and retrieve history via gecko-style callbacks`() = runTestOnMain {
+ assertEquals(0, history.getVisited(listOf()).size)
+
+ // Regular visits are tracked
+ history.recordVisit("https://www.mozilla.org", PageVisit(VisitType.LINK))
+ assertEquals(listOf(true), history.getVisited(listOf("https://www.mozilla.org")))
+
+ // Duplicate requests are handled.
+ assertEquals(listOf(true, true), history.getVisited(listOf("https://www.mozilla.org", "https://www.mozilla.org")))
+
+ // Visit map is returned in correct order.
+ assertEquals(listOf(true, false), history.getVisited(listOf("https://www.mozilla.org", "https://www.unknown.com")))
+
+ assertEquals(listOf(false, true), history.getVisited(listOf("https://www.unknown.com", "https://www.mozilla.org")))
+
+ // Multiple visits can be tracked. Reloads can be tracked.
+ history.recordVisit("https://www.firefox.com", PageVisit(VisitType.LINK))
+ history.recordVisit("https://www.mozilla.org", PageVisit(VisitType.RELOAD))
+ history.recordVisit("https://www.wikipedia.org", PageVisit(VisitType.LINK))
+ assertEquals(listOf(true, true, false, true), history.getVisited(listOf("https://www.firefox.com", "https://www.wikipedia.org", "https://www.unknown.com", "https://www.mozilla.org")))
+ }
+
+ @Test
+ fun `store can be used to track page meta information - title and previewImageUrl changes`() = runTestOnMain {
+ // Title and previewImageUrl changes are recorded.
+ history.recordVisit("https://www.wikipedia.org", PageVisit(VisitType.TYPED))
+ history.recordObservation(
+ "https://www.wikipedia.org",
+ PageObservation("Wikipedia", "https://test.com/og-image-url"),
+ )
+ var recorded = history.getDetailedVisits(0)
+ assertEquals(1, recorded.size)
+ assertEquals("Wikipedia", recorded[0].title)
+ assertEquals("https://test.com/og-image-url", recorded[0].previewImageUrl)
+
+ history.recordObservation("https://www.wikipedia.org", PageObservation("Википедия"))
+ recorded = history.getDetailedVisits(0)
+ assertEquals(1, recorded.size)
+ assertEquals("Википедия", recorded[0].title)
+ assertEquals("https://test.com/og-image-url", recorded[0].previewImageUrl)
+
+ // Titles for different pages are recorded.
+ history.recordVisit("https://www.firefox.com", PageVisit(VisitType.TYPED))
+ history.recordObservation("https://www.firefox.com", PageObservation("Firefox"))
+ history.recordVisit("https://www.mozilla.org", PageVisit(VisitType.TYPED))
+ history.recordObservation("https://www.mozilla.org", PageObservation("Мозилла"))
+ recorded = history.getDetailedVisits(0)
+ assertEquals(3, recorded.size)
+ assertEquals("Википедия", recorded[0].title)
+ assertEquals("https://test.com/og-image-url", recorded[0].previewImageUrl)
+ assertEquals("Firefox", recorded[1].title)
+ assertNull(recorded[1].previewImageUrl)
+ assertEquals("Мозилла", recorded[2].title)
+ assertNull(recorded[2].previewImageUrl)
+ }
+
+ @Test
+ fun `store can provide suggestions`() = runTestOnMain {
+ assertEquals(0, history.getSuggestions("Mozilla", 100).size)
+
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.LINK))
+ val search = history.getSuggestions("Mozilla", 100)
+ assertEquals(0, search.size)
+
+ history.recordVisit("http://www.wikipedia.org", PageVisit(VisitType.LINK))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.LINK))
+ history.recordVisit("http://www.moscow.ru", PageVisit(VisitType.LINK))
+ history.recordObservation("http://www.mozilla.org", PageObservation("Mozilla"))
+ history.recordObservation("http://www.firefox.com", PageObservation("Mozilla Firefox"))
+ history.recordObservation("http://www.moscow.ru", PageObservation("Moscow City"))
+ history.recordObservation("http://www.moscow.ru/notitle", PageObservation(""))
+
+ // Empty search.
+ assertEquals(4, history.getSuggestions("", 100).size)
+
+ val search2 = history.getSuggestions("Mozilla", 100).sortedByDescending { it.url }
+ assertEquals(2, search2.size)
+ assertEquals("http://www.mozilla.org/", search2[0].id)
+ assertEquals("http://www.mozilla.org/", search2[0].url)
+ assertEquals("Mozilla", search2[0].title)
+ assertEquals("http://www.firefox.com/", search2[1].id)
+ assertEquals("http://www.firefox.com/", search2[1].url)
+ assertEquals("Mozilla Firefox", search2[1].title)
+
+ val search3 = history.getSuggestions("Mo", 100).sortedByDescending { it.url }
+ assertEquals(3, search3.size)
+
+ assertEquals("http://www.mozilla.org/", search3[0].id)
+ assertEquals("http://www.mozilla.org/", search3[0].url)
+ assertEquals("Mozilla", search3[0].title)
+
+ assertEquals("http://www.moscow.ru/", search3[1].id)
+ assertEquals("http://www.moscow.ru/", search3[1].url)
+ assertEquals("Moscow City", search3[1].title)
+
+ assertEquals("http://www.firefox.com/", search3[2].id)
+ assertEquals("http://www.firefox.com/", search3[2].url)
+ assertEquals("Mozilla Firefox", search3[2].title)
+
+ // Respects the limit
+ val search4 = history.getSuggestions("Mo", 1)
+ assertEquals("http://www.moscow.ru/", search4[0].id)
+ assertEquals("http://www.moscow.ru/", search4[0].url)
+ assertEquals("Moscow City", search4[0].title)
+ }
+
+ @Test
+ fun `store can provide autocomplete suggestions`() = runTestOnMain {
+ assertNull(history.getAutocompleteSuggestion("moz"))
+
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.LINK))
+ var res = history.getAutocompleteSuggestion("moz")!!
+ assertEquals("mozilla.org/", res.text)
+ assertEquals("http://www.mozilla.org/", res.url)
+ assertEquals("placesHistory", res.source)
+ assertEquals(1, res.totalItems)
+
+ history.recordVisit("http://firefox.com", PageVisit(VisitType.LINK))
+ res = history.getAutocompleteSuggestion("firefox")!!
+ assertEquals("firefox.com/", res.text)
+ assertEquals("http://firefox.com/", res.url)
+ assertEquals("placesHistory", res.source)
+ assertEquals(1, res.totalItems)
+
+ history.recordVisit("https://en.wikipedia.org/wiki/Mozilla", PageVisit(VisitType.LINK))
+ res = history.getAutocompleteSuggestion("en")!!
+ assertEquals("en.wikipedia.org/", res.text)
+ assertEquals("https://en.wikipedia.org/", res.url)
+ assertEquals("placesHistory", res.source)
+ assertEquals(1, res.totalItems)
+
+ res = history.getAutocompleteSuggestion("en.wikipedia.org/wi")!!
+ assertEquals("en.wikipedia.org/wiki/", res.text)
+ assertEquals("https://en.wikipedia.org/wiki/", res.url)
+ assertEquals("placesHistory", res.source)
+ assertEquals(1, res.totalItems)
+
+ // Path segment matching along a long path
+ history.recordVisit("https://www.reddit.com/r/vancouver/comments/quu9lt/hwy_1_just_north_of_lytton_is_gone", PageVisit(VisitType.LINK))
+ res = history.getAutocompleteSuggestion("reddit.com/r")!!
+ assertEquals("reddit.com/r/", res.text)
+ assertEquals("https://www.reddit.com/r/", res.url)
+ assertEquals("placesHistory", res.source)
+ assertEquals(1, res.totalItems)
+ res = history.getAutocompleteSuggestion("reddit.com/r/van")!!
+ assertEquals("reddit.com/r/vancouver/", res.text)
+ assertEquals("https://www.reddit.com/r/vancouver/", res.url)
+ assertEquals("placesHistory", res.source)
+ assertEquals(1, res.totalItems)
+ res = history.getAutocompleteSuggestion("reddit.com/r/vancouver/comments/q")!!
+ assertEquals("reddit.com/r/vancouver/comments/quu9lt/", res.text)
+ assertEquals("https://www.reddit.com/r/vancouver/comments/quu9lt/", res.url)
+ assertEquals("placesHistory", res.source)
+ assertEquals(1, res.totalItems)
+ res = history.getAutocompleteSuggestion("reddit.com/r/vancouver/comments/quu9lt/h")!!
+ assertEquals("reddit.com/r/vancouver/comments/quu9lt/hwy_1_just_north_of_lytton_is_gone", res.text)
+ assertEquals("https://www.reddit.com/r/vancouver/comments/quu9lt/hwy_1_just_north_of_lytton_is_gone", res.url)
+ assertEquals("placesHistory", res.source)
+ assertEquals(1, res.totalItems)
+
+ assertNull(history.getAutocompleteSuggestion("hello"))
+ }
+
+ @Test
+ fun `store uses a different reader for autocomplete suggestions`() = runTest {
+ val connection: RustPlacesConnection = mock()
+ doReturn(mock<PlacesReaderConnection>()).`when`(connection).reader()
+ doReturn(mock<PlacesReaderConnection>()).`when`(connection).newReader()
+ val storage = MockingPlacesHistoryStorage(connection)
+
+ storage.getAutocompleteSuggestion("Test")
+
+ assertNotEquals(storage.reader, storage.autocompleteReader)
+ verify(storage.autocompleteReader).interrupt()
+ verify(storage.autocompleteReader).matchUrl("Test")
+ }
+
+ @Test
+ fun `store ignores url parse exceptions during record operations`() = runTestOnMain {
+ // These aren't valid URIs, and if we're not explicitly ignoring exceptions from the underlying
+ // storage layer, these calls will throw.
+ history.recordVisit("mozilla.org", PageVisit(VisitType.LINK))
+ history.recordObservation("mozilla.org", PageObservation("mozilla"))
+ }
+
+ @Test
+ fun `store can delete everything`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.TYPED))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.DOWNLOAD))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.BOOKMARK))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.RELOAD))
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.EMBED))
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.REDIRECT_PERMANENT))
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.REDIRECT_TEMPORARY))
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.LINK))
+
+ history.recordObservation("http://www.firefox.com", PageObservation("Firefox"))
+
+ assertEquals(2, history.getVisited().size)
+
+ history.deleteEverything()
+
+ assertEquals(0, history.getVisited().size)
+ }
+
+ @Test
+ fun `store can delete by url`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.TYPED))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.DOWNLOAD))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.BOOKMARK))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.RELOAD))
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.EMBED))
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.REDIRECT_PERMANENT))
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.REDIRECT_TEMPORARY))
+ history.recordVisit("http://www.firefox.com", PageVisit(VisitType.LINK))
+
+ history.recordObservation("http://www.firefox.com", PageObservation("Firefox"))
+
+ assertEquals(2, history.getVisited().size)
+
+ history.deleteVisitsFor("http://www.mozilla.org")
+
+ assertEquals(1, history.getVisited().size)
+ assertEquals("http://www.firefox.com/", history.getVisited()[0])
+
+ history.deleteVisitsFor("http://www.firefox.com")
+ assertEquals(0, history.getVisited().size)
+ }
+
+ @Test
+ fun `store can delete by 'since'`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.TYPED))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.DOWNLOAD))
+ history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.BOOKMARK))
+
+ history.deleteVisitsSince(0)
+ val visits = history.getVisited()
+ assertEquals(0, visits.size)
+ }
+
+ @Ignore("Disabled: https://bugzilla.mozilla.org/show_bug.cgi?id=1853687")
+ @Test
+ fun `store can delete by 'range'`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org/1", PageVisit(VisitType.TYPED))
+ advanceUntilIdle()
+ history.recordVisit("http://www.mozilla.org/2", PageVisit(VisitType.DOWNLOAD))
+ advanceUntilIdle()
+ history.recordVisit("http://www.mozilla.org/3", PageVisit(VisitType.BOOKMARK))
+
+ var visits = history.getDetailedVisits(0, Long.MAX_VALUE)
+ assertEquals(3, visits.size)
+ val ts = visits[1].visitTime
+
+ history.deleteVisitsBetween(ts - 1, ts + 1)
+
+ visits = history.getDetailedVisits(0, Long.MAX_VALUE)
+
+ assertEquals(2, visits.size)
+
+ assertEquals("http://www.mozilla.org/1", visits[0].url)
+ assertEquals("http://www.mozilla.org/3", visits[1].url)
+ }
+
+ @Test
+ fun `store can delete visit by 'url' and 'timestamp'`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org/1", PageVisit(VisitType.TYPED))
+ Thread.sleep(10)
+ history.recordVisit("http://www.mozilla.org/2", PageVisit(VisitType.DOWNLOAD))
+ Thread.sleep(10)
+ history.recordVisit("http://www.mozilla.org/3", PageVisit(VisitType.BOOKMARK))
+
+ var visits = history.getDetailedVisits(0, Long.MAX_VALUE)
+ assertEquals(3, visits.size)
+ val ts = visits[1].visitTime
+
+ history.deleteVisit("http://www.mozilla.org/4", 111)
+ // There are no visits for this url, delete is a no-op.
+ assertEquals(3, history.getDetailedVisits(0, Long.MAX_VALUE).size)
+
+ history.deleteVisit("http://www.mozilla.org/1", ts)
+ // There is no such visit for this url, delete is a no-op.
+ assertEquals(3, history.getDetailedVisits(0, Long.MAX_VALUE).size)
+
+ history.deleteVisit("http://www.mozilla.org/2", ts)
+
+ visits = history.getDetailedVisits(0, Long.MAX_VALUE)
+ assertEquals(2, visits.size)
+
+ assertEquals("http://www.mozilla.org/1", visits[0].url)
+ assertEquals("http://www.mozilla.org/3", visits[1].url)
+ }
+
+ @Test
+ fun `can run maintenance on the store`() = runTestOnMain {
+ history.runMaintenance(0U)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `When periodicStorageWorkRequest is called, worker with input specs is created`() {
+ val request = history.periodicStorageWorkRequest<PlacesHistoryStorageWorker>(
+ tag = PlacesHistoryStorageWorker.UNIQUE_NAME,
+ ) {
+ constraints {
+ setRequiresBatteryNotLow(true)
+ setRequiresDeviceIdle(true)
+ }
+ }
+
+ assertEquals(request.workSpec.isPeriodic, true)
+ assertEquals(request.workSpec.intervalDuration, TimeUnit.HOURS.toMillis(StorageMaintenanceWorker.WORKER_PERIOD_IN_HOURS))
+ assertEquals(request.workSpec.constraints.requiresBatteryNotLow(), true)
+ assertEquals(request.workSpec.constraints.requiresDeviceIdle(), true)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `When storage maintenance work request is registered, the worker is enqueued`() {
+ val config = Configuration.Builder().build()
+ WorkManagerTestInitHelper.initializeTestWorkManager(testContext, config)
+
+ val request = history.periodicStorageWorkRequest<PlacesHistoryStorageWorker>(
+ tag = PlacesHistoryStorageWorker.UNIQUE_NAME,
+ ) {
+ constraints {
+ setRequiresBatteryNotLow(true)
+ setRequiresDeviceIdle(true)
+ }
+ }
+
+ val workManager = WorkManager.getInstance(testContext)
+ val testDriver = WorkManagerTestInitHelper.getTestDriver(testContext)
+ workManager.enqueue(request).result.get()
+ testDriver?.setPeriodDelayMet(request.id)
+
+ val workInfo = workManager.getWorkInfoById(request.id).get()
+ assertThat(workInfo.state, `is`(WorkInfo.State.ENQUEUED))
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `storage validates calls to getSuggestion`() {
+ history.getSuggestions("Hello!", -1)
+ }
+
+ // We can't test 'sync' stuff yet, since that exercises the network and we can't mock that out currently.
+ // Instead, we test that our wrappers act correctly.
+ internal class MockingPlacesHistoryStorage(override val places: Connection) : PlacesHistoryStorage(testContext)
+
+ @Test
+ fun `storage passes through sync calls`() = runTestOnMain {
+ var passedAuthInfo: SyncAuthInfo? = null
+ val conn = object : Connection {
+ override fun reader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun newReader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun writer(): PlacesWriterConnection {
+ fail()
+ return mock()
+ }
+
+ override fun syncHistory(syncInfo: SyncAuthInfo) {
+ assertNull(passedAuthInfo)
+ passedAuthInfo = syncInfo
+ }
+
+ override fun syncBookmarks(syncInfo: SyncAuthInfo) {
+ fail()
+ }
+
+ override fun close() {
+ fail()
+ }
+
+ override fun registerWithSyncManager() {
+ fail()
+ }
+ }
+ val storage = MockingPlacesHistoryStorage(conn)
+
+ storage.sync(SyncAuthInfo("kid", "token", 123L, "key", "serverUrl"))
+
+ assertEquals("kid", passedAuthInfo!!.kid)
+ assertEquals("serverUrl", passedAuthInfo!!.tokenServerUrl)
+ assertEquals("token", passedAuthInfo!!.fxaAccessToken)
+ assertEquals(123L, passedAuthInfo!!.fxaAccessTokenExpiresAt)
+ assertEquals("key", passedAuthInfo!!.syncKey)
+ }
+
+ @Test
+ fun `storage passes through sync OK results`() = runTestOnMain {
+ val conn = object : Connection {
+ override fun reader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun newReader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun writer(): PlacesWriterConnection {
+ fail()
+ return mock()
+ }
+
+ override fun syncHistory(syncInfo: SyncAuthInfo) {}
+
+ override fun syncBookmarks(syncInfo: SyncAuthInfo) {}
+
+ override fun close() {
+ fail()
+ }
+
+ override fun registerWithSyncManager() {
+ fail()
+ }
+ }
+ val storage = MockingPlacesHistoryStorage(conn)
+
+ val result = storage.sync(SyncAuthInfo("kid", "token", 123L, "key", "serverUrl"))
+ assertEquals(SyncStatus.Ok, result)
+ }
+
+ @Test
+ fun `storage passes through sync exceptions`() = runTestOnMain {
+ // Can be any PlacesApiException
+ val exception = PlacesApiException.UrlParseFailed("test error")
+ val conn = object : Connection {
+ override fun reader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun newReader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun writer(): PlacesWriterConnection {
+ fail()
+ return mock()
+ }
+
+ override fun syncHistory(syncInfo: SyncAuthInfo) {
+ throw exception
+ }
+
+ override fun syncBookmarks(syncInfo: SyncAuthInfo) {
+ fail()
+ }
+
+ override fun close() {
+ fail()
+ }
+
+ override fun registerWithSyncManager() {
+ fail()
+ }
+ }
+ val storage = MockingPlacesHistoryStorage(conn)
+
+ val result = storage.sync(SyncAuthInfo("kid", "token", 123L, "key", "serverUrl"))
+
+ assertTrue(result is SyncStatus.Error)
+ }
+
+ @Test
+ fun `storage does not re-throw unexpected places exceptions`() = runTestOnMain {
+ val exception = PlacesApiException.UnexpectedPlacesException("unexpected exception")
+ val conn = object : Connection {
+ override fun reader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun newReader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun writer(): PlacesWriterConnection {
+ fail()
+ return mock()
+ }
+
+ override fun syncHistory(syncInfo: SyncAuthInfo) {
+ throw exception
+ }
+
+ override fun syncBookmarks(syncInfo: SyncAuthInfo) {
+ fail()
+ }
+
+ override fun close() {
+ fail()
+ }
+
+ override fun registerWithSyncManager() {
+ fail()
+ }
+ }
+ val storage = MockingPlacesHistoryStorage(conn)
+ val result = storage.sync(SyncAuthInfo("kid", "token", 123L, "key", "serverUrl"))
+ assertTrue(result is SyncStatus.Error)
+ }
+
+ @Test(expected = InternalException::class)
+ fun `storage re-throws sync panics`() = runTestOnMain {
+ val exception = InternalException("sync paniced")
+ val conn = object : Connection {
+ override fun reader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun newReader(): PlacesReaderConnection {
+ fail()
+ return mock()
+ }
+
+ override fun writer(): PlacesWriterConnection {
+ fail()
+ return mock()
+ }
+
+ override fun syncHistory(syncInfo: SyncAuthInfo) {
+ throw exception
+ }
+
+ override fun syncBookmarks(syncInfo: SyncAuthInfo) {
+ fail()
+ }
+
+ override fun close() {
+ fail()
+ }
+
+ override fun registerWithSyncManager() {
+ fail()
+ }
+ }
+ val storage = MockingPlacesHistoryStorage(conn)
+ storage.sync(SyncAuthInfo("kid", "token", 123L, "key", "serverUrl"))
+ fail()
+ }
+
+ @Test
+ fun `record and get latest history metadata by url`() = runTestOnMain {
+ val metaKey = HistoryMetadataKey(
+ url = "https://doc.rust-lang.org/std/macro.assert_eq.html",
+ searchTerm = "rust assert_eq",
+ referrerUrl = "http://www.google.com/",
+ )
+
+ assertNull(history.getLatestHistoryMetadataForUrl(metaKey.url))
+ history.noteHistoryMetadataObservation(metaKey, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular))
+ history.noteHistoryMetadataObservation(metaKey, HistoryMetadataObservation.ViewTimeObservation(5000))
+
+ val dbMeta = history.getLatestHistoryMetadataForUrl(metaKey.url)
+ assertNotNull(dbMeta)
+ assertHistoryMetadataRecord(metaKey, 5000, DocumentType.Regular, dbMeta!!)
+ }
+
+ @Test
+ fun `get history query`() = runTestOnMain {
+ assertEquals(0, history.queryHistoryMetadata("keystore", 1).size)
+
+ val metaKey1 = HistoryMetadataKey(
+ url = "https://sql.telemetry.mozilla.org/dashboard/android-keystore-reliability-experiment",
+ searchTerm = "keystore reliability",
+ referrerUrl = "http://self.mozilla.com/",
+ )
+ history.noteHistoryMetadataObservation(metaKey1, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular))
+ history.noteHistoryMetadataObservation(metaKey1, HistoryMetadataObservation.ViewTimeObservation(20000))
+
+ val metaKey2 = HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=F7PQdCDiE44",
+ searchTerm = "crisis",
+ referrerUrl = "https://www.google.com/search?client=firefox-b-d&q=dw+crisis",
+ )
+ history.noteHistoryMetadataObservation(metaKey2, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media))
+ history.noteHistoryMetadataObservation(metaKey2, HistoryMetadataObservation.ViewTimeObservation(30000))
+
+ history.writer.noteObservation(
+ VisitObservation(
+ url = "https://www.youtube.com/watch?v=F7PQdCDiE44",
+ title = "DW next crisis",
+ visitType = mozilla.appservices.places.uniffi.VisitType.LINK,
+ ),
+ )
+
+ val metaKey3 = HistoryMetadataKey(
+ url = "https://www.cbc.ca/news/canada/toronto/covid-19-ontario-april-16-2021-new-restrictions-modelling-1.5990092",
+ searchTerm = "ford covid19",
+ referrerUrl = "https://duckduckgo.com/?q=ford+covid19&t=hc&va=u&ia=web",
+ )
+ history.noteHistoryMetadataObservation(metaKey3, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular))
+ history.noteHistoryMetadataObservation(metaKey3, HistoryMetadataObservation.ViewTimeObservation(20000))
+
+ val metaKey4 = HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=TfXbzbJQHuw",
+ searchTerm = "dw nyc rich",
+ referrerUrl = "https://duckduckgo.com/?q=dw+nyc+rich&t=hc&va=u&ia=web",
+ )
+ history.noteHistoryMetadataObservation(metaKey4, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media))
+ history.noteHistoryMetadataObservation(metaKey4, HistoryMetadataObservation.ViewTimeObservation(20000))
+
+ assertEquals(0, history.queryHistoryMetadata("keystore", 0).size)
+
+ // query by url
+ with(history.queryHistoryMetadata("dashboard", 10)) {
+ assertEquals(1, this.size)
+ assertHistoryMetadataRecord(metaKey1, 20000, DocumentType.Regular, this[0])
+ }
+
+ // query by title
+ with(history.queryHistoryMetadata("next crisis", 10)) {
+ assertEquals(1, this.size)
+ assertHistoryMetadataRecord(metaKey2, 30000, DocumentType.Media, this[0])
+ }
+
+ // query by search term
+ with(history.queryHistoryMetadata("covid19", 10)) {
+ assertEquals(1, this.size)
+ assertHistoryMetadataRecord(metaKey3, 20000, DocumentType.Regular, this[0])
+ }
+
+ // multiple results, mixed search targets
+ with(history.queryHistoryMetadata("dw", 10)) {
+ assertEquals(2, this.size)
+ assertHistoryMetadataRecord(metaKey2, 30000, DocumentType.Media, this[0])
+ assertHistoryMetadataRecord(metaKey4, 20000, DocumentType.Media, this[1])
+ }
+
+ // limit is respected
+ with(history.queryHistoryMetadata("dw", 1)) {
+ assertEquals(1, this.size)
+ assertHistoryMetadataRecord(metaKey2, 30000, DocumentType.Media, this[0])
+ }
+ }
+
+ @Ignore("Disabled: https://bugzilla.mozilla.org/show_bug.cgi?id=1853687")
+ @Test
+ fun `get history metadata between`() = runTestOnMain {
+ assertEquals(0, history.getHistoryMetadataBetween(-1, 0).size)
+ assertEquals(0, history.getHistoryMetadataBetween(0, Long.MAX_VALUE).size)
+ assertEquals(0, history.getHistoryMetadataBetween(Long.MAX_VALUE, Long.MIN_VALUE).size)
+ assertEquals(0, history.getHistoryMetadataBetween(Long.MIN_VALUE, Long.MAX_VALUE).size)
+
+ val beginning = System.currentTimeMillis()
+
+ val metaKey1 = HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=lNeRQuiKBd4",
+ referrerUrl = "http://www.twitter.com",
+ )
+ history.noteHistoryMetadataObservation(metaKey1, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media))
+ history.noteHistoryMetadataObservation(metaKey1, HistoryMetadataObservation.ViewTimeObservation(20000))
+ val afterMeta1 = System.currentTimeMillis()
+
+ val metaKey2 = HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=Cs1b5qvCZ54",
+ searchTerm = "путин валдай",
+ referrerUrl = "http://www.yandex.ru",
+ )
+ history.noteHistoryMetadataObservation(metaKey2, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media))
+ history.noteHistoryMetadataObservation(metaKey2, HistoryMetadataObservation.ViewTimeObservation(200))
+ val afterMeta2 = System.currentTimeMillis()
+
+ val metaKey3 = HistoryMetadataKey(
+ url = "https://www.ifixit.com/News/35377/which-wireless-earbuds-are-the-least-evil",
+ searchTerm = "repairable wireless headset",
+ referrerUrl = "http://www.google.com",
+ )
+ history.noteHistoryMetadataObservation(metaKey3, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular))
+ history.noteHistoryMetadataObservation(metaKey3, HistoryMetadataObservation.ViewTimeObservation(2000))
+
+ assertEquals(3, history.getHistoryMetadataBetween(0, Long.MAX_VALUE).size)
+ assertEquals(0, history.getHistoryMetadataBetween(Long.MAX_VALUE, 0).size)
+ assertEquals(0, history.getHistoryMetadataBetween(Long.MIN_VALUE, 0).size)
+
+ with(history.getHistoryMetadataBetween(beginning, afterMeta1)) {
+ assertEquals(1, this.size)
+ assertEquals("https://www.youtube.com/watch?v=lNeRQuiKBd4", this[0].key.url)
+ }
+ with(history.getHistoryMetadataBetween(beginning, afterMeta2)) {
+ assertEquals(2, this.size)
+ assertEquals("https://www.youtube.com/watch?v=Cs1b5qvCZ54", this[0].key.url)
+ assertEquals("https://www.youtube.com/watch?v=lNeRQuiKBd4", this[1].key.url)
+ }
+ with(history.getHistoryMetadataBetween(afterMeta1, afterMeta2)) {
+ assertEquals(1, this.size)
+ assertEquals("https://www.youtube.com/watch?v=Cs1b5qvCZ54", this[0].key.url)
+ }
+ }
+
+ @Test
+ fun `get history metadata since`() = runTestOnMain {
+ val beginning = System.currentTimeMillis()
+
+ assertEquals(0, history.getHistoryMetadataSince(-1).size)
+ assertEquals(0, history.getHistoryMetadataSince(0).size)
+ assertEquals(0, history.getHistoryMetadataSince(Long.MIN_VALUE).size)
+ assertEquals(0, history.getHistoryMetadataSince(Long.MAX_VALUE).size)
+ assertEquals(0, history.getHistoryMetadataSince(beginning).size)
+
+ val metaKey1 = HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=lNeRQuiKBd4",
+ referrerUrl = "http://www.twitter.com/",
+ )
+ history.noteHistoryMetadataObservation(metaKey1, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media))
+ history.noteHistoryMetadataObservation(metaKey1, HistoryMetadataObservation.ViewTimeObservation(20000))
+ Thread.sleep(10)
+ val afterMeta1 = System.currentTimeMillis()
+
+ val metaKey2 = HistoryMetadataKey(
+ url = "https://www.ifixit.com/News/35377/which-wireless-earbuds-are-the-least-evil",
+ searchTerm = "repairable wireless headset",
+ referrerUrl = "http://www.google.com/",
+ )
+ history.noteHistoryMetadataObservation(metaKey2, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular))
+ history.noteHistoryMetadataObservation(metaKey2, HistoryMetadataObservation.ViewTimeObservation(2000))
+
+ Thread.sleep(10)
+ val afterMeta2 = System.currentTimeMillis()
+
+ val metaKey3 = HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=Cs1b5qvCZ54",
+ searchTerm = "путин валдай",
+ referrerUrl = "http://www.yandex.ru/",
+ )
+ history.noteHistoryMetadataObservation(metaKey3, HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media))
+ history.noteHistoryMetadataObservation(metaKey3, HistoryMetadataObservation.ViewTimeObservation(200))
+
+ // order is by updatedAt
+ with(history.getHistoryMetadataSince(beginning)) {
+ assertEquals(3, this.size)
+ assertHistoryMetadataRecord(metaKey3, 200, DocumentType.Media, this[0])
+ assertHistoryMetadataRecord(metaKey2, 2000, DocumentType.Regular, this[1])
+ assertHistoryMetadataRecord(metaKey1, 20000, DocumentType.Media, this[2])
+ }
+
+ // search is inclusive of time
+ with(history.getHistoryMetadataSince(afterMeta1)) {
+ assertEquals(2, this.size)
+ assertHistoryMetadataRecord(metaKey3, 200, DocumentType.Media, this[0])
+ assertHistoryMetadataRecord(metaKey2, 2000, DocumentType.Regular, this[1])
+ }
+
+ with(history.getHistoryMetadataSince(afterMeta2)) {
+ assertEquals(1, this.size)
+ assertHistoryMetadataRecord(metaKey3, 200, DocumentType.Media, this[0])
+ }
+ }
+
+ @Test
+ fun `delete history metadata by search term`() = runTestOnMain {
+ // Able to operate against an empty db
+ history.deleteHistoryMetadata("test")
+ history.deleteHistoryMetadata("")
+
+ // Observe some items.
+ with(
+ HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=lNeRQuiKBd4",
+ referrerUrl = "http://www.twitter.com/",
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media),
+ )
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.ViewTimeObservation(20000),
+ )
+ }
+
+ // For the ifixit case, let's create a scenario with metadata entries that will dedupe by url,
+ // and will have at least some 0 view time records. In a search term group, these 4 metadata
+ // records will show up as 2, so a naive delete (iterate through group, delete) will leave records
+ // behind.
+ with(
+ HistoryMetadataKey(
+ url = "https://www.ifixit.com/News/35377/which-wireless-earbuds-are-the-least-evil",
+ searchTerm = "repairable wireless headset",
+ referrerUrl = "http://www.google.com/",
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular),
+ )
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.ViewTimeObservation(2000),
+ )
+ }
+ // Same search term as above, different url/referrer.
+ with(
+ HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=rfdshufsSfsd",
+ searchTerm = "repairable wireless headset",
+ referrerUrl = null,
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media),
+ )
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.ViewTimeObservation(25000),
+ )
+ }
+ // Same search term as above, same url, different referrer.
+ with(
+ HistoryMetadataKey(
+ url = "https://www.ifixit.com/News/35377/which-wireless-earbuds-are-the-least-evil",
+ searchTerm = "repairable wireless headset",
+ referrerUrl = "http://www.yandex.ru/",
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular),
+ )
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.ViewTimeObservation(1000),
+ )
+ }
+ // Again, but without view time.
+ with(
+ HistoryMetadataKey(
+ url = "https://www.ifixit.com/News/35377/which-wireless-earbuds-are-the-least-evil",
+ searchTerm = "repairable wireless headset",
+ referrerUrl = null,
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular),
+ )
+ }
+
+ // No view time for this one.
+ with(
+ HistoryMetadataKey(
+ url = "https://www.youtube.com/watch?v=Cs1b5qvCZ54",
+ searchTerm = "путин валдай",
+ referrerUrl = "http://www.yandex.ru/",
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Media),
+ )
+ }
+
+ assertEquals(6, history.getHistoryMetadataSince(0).count())
+
+ history.deleteHistoryMetadata("repairable wireless headset")
+ assertEquals(2, history.getHistoryMetadataSince(0).count())
+
+ history.deleteHistoryMetadata("Путин Валдай")
+ assertEquals(1, history.getHistoryMetadataSince(0).count())
+ }
+
+ @Test
+ fun `safe read from places`() = runTestOnMain {
+ val result = history.handlePlacesExceptions("test", default = emptyList<HistoryMetadata>()) {
+ // Can be any PlacesException error
+ throw PlacesApiException.PlacesConnectionBusy("test")
+ }
+ assertEquals(emptyList<HistoryMetadata>(), result)
+ }
+
+ @Test
+ fun `interrupted read from places is not reported to crash services and returns the default`() = runTestOnMain {
+ val result = history.handlePlacesExceptions("test", default = emptyList<HistoryMetadata>()) {
+ throw PlacesApiException.OperationInterrupted("An interrupted in progress query will throw")
+ }
+
+ verify(history.crashReporter!!, never()).submitCaughtException(any())
+ assertEquals(emptyList<HistoryMetadata>(), result)
+ }
+
+ @Test
+ fun `history delegate's shouldStoreUri works as expected`() {
+ // Not an excessive list of allowed schemes.
+ assertTrue(history.canAddUri("http://www.mozilla.com"))
+ assertTrue(history.canAddUri("https://www.mozilla.com"))
+ assertTrue(history.canAddUri("ftp://files.mozilla.com/stuff/fenix.apk"))
+ assertTrue(history.canAddUri("about:reader?url=http://www.mozilla.com/interesting-article.html"))
+ assertTrue(history.canAddUri("https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top"))
+ assertTrue(history.canAddUri("ldap://2001:db8::7/c=GB?objectClass?one"))
+ assertTrue(history.canAddUri("telnet://192.0.2.16:80/"))
+
+ assertFalse(history.canAddUri("withoutSchema.html"))
+ assertFalse(history.canAddUri("about:blank"))
+ assertFalse(history.canAddUri("news:comp.infosystems.www.servers.unix"))
+ assertFalse(history.canAddUri("imap://mail.example.com/~mozilla"))
+ assertFalse(history.canAddUri("chrome://config"))
+ assertFalse(history.canAddUri("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D"))
+ assertFalse(history.canAddUri("data:text/html,<script>alert('hi');</script>"))
+ assertFalse(history.canAddUri("resource://internal-thingy-js-inspector/script.js"))
+ assertFalse(history.canAddUri("javascript:alert('hello!');"))
+ assertFalse(history.canAddUri("blob:https://api.mozilla.com/resource.png"))
+ assertFalse(history.canAddUri("://example.com"))
+ }
+
+ @Test
+ fun `delete history metadata by url`() = runTestOnMain {
+ // Able to operate against an empty db
+ history.deleteHistoryMetadataForUrl("https://mozilla.org")
+ history.deleteHistoryMetadataForUrl("")
+
+ // Observe some items.
+ with(
+ HistoryMetadataKey(
+ url = "https://firefox.com/",
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular),
+ )
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.ViewTimeObservation(20000),
+ )
+ }
+
+ with(
+ HistoryMetadataKey(
+ url = "https://mozilla.org/",
+ searchTerm = "firefox",
+ referrerUrl = "https://google.com/",
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular),
+ )
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.ViewTimeObservation(20000),
+ )
+ }
+
+ with(
+ HistoryMetadataKey(
+ url = "https://getpocket.com/",
+ ),
+ ) {
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.DocumentTypeObservation(DocumentType.Regular),
+ )
+ history.noteHistoryMetadataObservation(
+ this,
+ HistoryMetadataObservation.ViewTimeObservation(20000),
+ )
+ }
+
+ assertEquals(3, history.getHistoryMetadataSince(0).count())
+
+ history.deleteHistoryMetadataForUrl("https://firefox.com/")
+ assertEquals(2, history.getHistoryMetadataSince(0).count())
+ assertEquals("https://getpocket.com/", history.getHistoryMetadataSince(0)[0].key.url)
+ assertEquals("https://mozilla.org/", history.getHistoryMetadataSince(0)[1].key.url)
+
+ history.deleteHistoryMetadataForUrl("https://mozilla.org/")
+ assertEquals(1, history.getHistoryMetadataSince(0).count())
+ assertEquals("https://getpocket.com/", history.getHistoryMetadataSince(0)[0].key.url)
+
+ history.deleteHistoryMetadataForUrl("https://getpocket.com/")
+ assertEquals(0, history.getHistoryMetadataSince(0).count())
+ }
+
+ private fun assertHistoryMetadataRecord(
+ expectedKey: HistoryMetadataKey,
+ expectedTotalViewTime: Int,
+ expectedDocumentType: DocumentType,
+ db_meta: HistoryMetadata,
+ ) {
+ assertEquals(expectedKey, db_meta.key)
+ assertEquals(expectedTotalViewTime, db_meta.totalViewTime)
+ assertEquals(expectedDocumentType, db_meta.documentType)
+ }
+}
+
+fun getTestPath(path: String): File {
+ return PlacesHistoryStorage::class.java.classLoader!!
+ .getResource(path).file
+ .let { File(it) }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorkerTest.kt b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorkerTest.kt
new file mode 100644
index 0000000000..ddef59ca8f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorkerTest.kt
@@ -0,0 +1,93 @@
+/* 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.browser.storage.sync
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker.Result
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import kotlin.reflect.KVisibility
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class PlacesHistoryStorageWorkerTest {
+
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
+ @After
+ fun tearDown() {
+ GlobalPlacesDependencyProvider.placesStorage = null
+ }
+
+ @Test
+ fun `PlacesHistoryStorage's runMaintenance is called when worker's startWork is called`() =
+ runTestOnMain {
+ val placesStorage = mock<PlacesStorage>()
+ GlobalPlacesDependencyProvider.initialize(placesStorage)
+ val worker =
+ TestListenableWorkerBuilder<PlacesHistoryStorageWorker>(testContext).build()
+
+ worker.doWork()
+ verify(placesStorage).runMaintenance(PlacesHistoryStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt())
+ }
+
+ @Test
+ fun `PlacesHistoryStorage's runMaintenance operation is successful, successful result returned by the worker`() =
+ runTestOnMain {
+ val placesStorage = mock<PlacesStorage>()
+ GlobalPlacesDependencyProvider.initialize(placesStorage)
+ val worker =
+ TestListenableWorkerBuilder<PlacesHistoryStorageWorker>(testContext).build()
+
+ val result = worker.doWork()
+ assertEquals(Result.success(), result)
+ }
+
+ @Test
+ fun `PlacesHistoryStorage's runMaintenance is called, exception is thrown and failure result is returned`() =
+ runTestOnMain {
+ val placesStorage = mock<PlacesStorage>()
+ `when`(placesStorage.runMaintenance(PlacesHistoryStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt()))
+ .thenThrow(CancellationException())
+ GlobalPlacesDependencyProvider.initialize(placesStorage)
+ val worker =
+ TestListenableWorkerBuilder<PlacesHistoryStorageWorker>(testContext).build()
+
+ val result = worker.doWork()
+ assertEquals(Result.failure(), result)
+ }
+
+ @Test
+ fun `PlacesHistoryStorage's runMaintenance is called, exception is thrown and active write operations are cancelled`() =
+ runTestOnMain {
+ val placesStorage = mock<PlacesStorage>()
+ `when`(placesStorage.runMaintenance(PlacesHistoryStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt()))
+ .thenThrow(CancellationException())
+ GlobalPlacesDependencyProvider.initialize(placesStorage)
+ val worker =
+ TestListenableWorkerBuilder<PlacesHistoryStorageWorker>(testContext).build()
+
+ worker.doWork()
+ verify(placesStorage).cancelWrites()
+ }
+
+ @Test
+ fun `PlacesHistoryStorageWorker's visibility is internal`() {
+ assertEquals(PlacesHistoryStorageWorker::class.visibility, KVisibility.INTERNAL)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesStorageTest.kt b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesStorageTest.kt
new file mode 100644
index 0000000000..ceff36c3c3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesStorageTest.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.browser.storage.sync
+
+import android.content.Context
+import kotlinx.coroutines.cancelChildren
+import mozilla.appservices.places.PlacesReaderConnection
+import mozilla.appservices.places.PlacesWriterConnection
+import mozilla.appservices.places.uniffi.PlacesApiException
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import kotlin.coroutines.CoroutineContext
+
+class PlacesStorageTest {
+ private val storage = FakePlacesStorage()
+
+ @Test
+ fun `WHEN all reads are interrupted THEN no exception is thrown`() {
+ doAnswer {
+ throw PlacesApiException.OperationInterrupted("This should be caught")
+ }.`when`(storage.reader).interrupt()
+
+ storage.interruptCurrentReads()
+
+ verify(storage.reader).interrupt()
+ }
+
+ @Test
+ fun `WHEN all writes are interrupted THEN no exception is thrown`() {
+ doAnswer {
+ throw PlacesApiException.OperationInterrupted("This should be caught")
+ }.`when`(storage.writer).interrupt()
+
+ storage.interruptCurrentWrites()
+
+ verify(storage.writer).interrupt()
+ }
+
+ @Test
+ fun `WHEN an unexpected places exception is thrown it is consumed`() {
+ doAnswer {
+ throw PlacesApiException.UnexpectedPlacesException("This should be caught")
+ }.`when`(storage.writer).interrupt()
+
+ storage.interruptCurrentWrites()
+
+ verify(storage.writer).interrupt()
+ }
+
+ @Test
+ fun `WHEN a call is made to clean all reads THEN they are cancelled`() {
+ storage.readScope = mock {
+ doReturn(mock<CoroutineContext>()).`when`(this).coroutineContext
+ }
+
+ storage.cancelReads()
+
+ verify(storage.reader).interrupt()
+ verify(storage.readScope.coroutineContext).cancelChildren()
+ }
+
+ @Test
+ fun `GIVEN a specific query WHEN a call is made to clean all reads THEN they are cancelled only if the query is different from the previous call`() {
+ storage.readScope = mock {
+ doReturn(mock<CoroutineContext>()).`when`(this).coroutineContext
+ }
+
+ storage.cancelReads("test")
+ verify(storage.reader, times(1)).interrupt()
+ verify(storage.readScope.coroutineContext, times(1)).cancelChildren()
+
+ storage.cancelReads("test")
+ verify(storage.reader, times(1)).interrupt()
+ verify(storage.readScope.coroutineContext, times(1)).cancelChildren()
+
+ storage.cancelReads("tset")
+ verify(storage.reader, times(2)).interrupt()
+ verify(storage.readScope.coroutineContext, times(2)).cancelChildren()
+ }
+
+ @Test
+ fun `WHEN a call is made to clean all writes THEN they are cancelled`() {
+ storage.writeScope = mock {
+ doReturn(mock<CoroutineContext>()).`when`(this).coroutineContext
+ }
+
+ storage.cancelWrites()
+
+ verify(storage.writer).interrupt()
+ verify(storage.writeScope.coroutineContext).cancelChildren()
+ }
+}
+
+class FakePlacesStorage(
+ context: Context = mock(),
+) : PlacesStorage(context) {
+ override val logger = Logger("FakePlacesStorage")
+ override fun registerWithSyncManager() {}
+
+ override val writer: PlacesWriterConnection = mock()
+ override val reader: PlacesReaderConnection = mock()
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt
new file mode 100644
index 0000000000..b708b95b65
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt
@@ -0,0 +1,174 @@
+/* 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.browser.storage.sync
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.appservices.remotetabs.ClientRemoteTabs
+import mozilla.appservices.remotetabs.RemoteTab
+import mozilla.appservices.sync15.DeviceType
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import mozilla.appservices.remotetabs.TabsApiException as RemoteTabProviderException
+import mozilla.appservices.remotetabs.TabsStore as RemoteTabsProvider
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class RemoteTabsStorageTest {
+ private lateinit var remoteTabs: RemoteTabsStorage
+ private lateinit var apiMock: RemoteTabsProvider
+ private lateinit var crashReporter: CrashReporting
+
+ @Before
+ fun setup() {
+ crashReporter = mock()
+ remoteTabs = spy(RemoteTabsStorage(testContext, crashReporter))
+ apiMock = mock(RemoteTabsProvider::class.java)
+ `when`(remoteTabs.api).thenReturn(apiMock)
+ }
+
+ @After
+ fun cleanup() {
+ remoteTabs.cleanup()
+ }
+
+ @Test
+ fun `store() translates tabs to rust format`() = runTest {
+ remoteTabs.store(
+ listOf(
+ Tab(
+ listOf(
+ TabEntry("Bar", "https://bar", null),
+ ),
+ 0,
+ 1574458165555,
+ false,
+ ),
+ Tab(
+ listOf(
+ TabEntry("Foo bar", "https://foo.bar", null),
+ TabEntry("Foo bar 1", "https://foo.bar/1", null),
+ TabEntry("Foo bar 2", "https://foo.bar/2", null),
+ ),
+ 2,
+ 0,
+ false,
+ ),
+ Tab(
+ listOf(
+ TabEntry("Foo 1", "https://foo", "https://foo/icon"),
+ TabEntry("Foo 2", "https://foo/1", "https://foo/icon2"),
+ TabEntry("Foo 3", "https://foo/1/1", "https://foo/icon3"),
+ ),
+ 1,
+ 1574457405635,
+ false,
+ ),
+ ),
+ )
+
+ verify(apiMock).setLocalTabs(
+ listOf(
+ RemoteTab("Bar", listOf("https://bar"), null, 1574458165555),
+ RemoteTab("Foo bar 2", listOf("https://foo.bar/2", "https://foo.bar/1", "https://foo.bar"), null, 0),
+ RemoteTab("Foo 2", listOf("https://foo/1", "https://foo"), "https://foo/icon2", 1574457405635),
+ ),
+ )
+ }
+
+ @Test
+ fun `getAll() translates tabs to our format`() = runTest {
+ `when`(apiMock.getAll()).thenReturn(
+ listOf(
+ ClientRemoteTabs(
+ "client1",
+ "",
+ DeviceType.MOBILE,
+ 0, // any value for the timestamp is OK for these tests.
+ listOf(
+ RemoteTab("Foo", listOf("https://foo/1/1", "https://foo/1", "https://foo"), "https://foo/icon", 1574457405635),
+ ),
+ ),
+ ClientRemoteTabs(
+ "client2",
+ "",
+ DeviceType.MOBILE,
+ 0, // any value for the timestamp is OK for these tests.
+ listOf(
+ RemoteTab("Bar", listOf("https://bar"), null, 1574458165555),
+ RemoteTab("Foo Bar", listOf("https://foo.bar"), "https://foo.bar/icon", 0),
+ ),
+ ),
+ ),
+ )
+
+ assertEquals(
+ mapOf(
+ SyncClient("client1") to listOf(
+ Tab(
+ listOf(
+ TabEntry("Foo", "https://foo", "https://foo/icon"),
+ TabEntry("Foo", "https://foo/1", "https://foo/icon"),
+ TabEntry("Foo", "https://foo/1/1", "https://foo/icon"),
+ ),
+ 2,
+ 1574457405635,
+ false,
+ ),
+ ),
+ SyncClient("client2") to listOf(
+ Tab(
+ listOf(
+ TabEntry("Bar", "https://bar", null),
+ ),
+ 0,
+ 1574458165555,
+ false,
+ ),
+ Tab(
+ listOf(
+ TabEntry("Foo Bar", "https://foo.bar", "https://foo.bar/icon"),
+ ),
+ 0,
+ 0,
+ false,
+ ),
+ ),
+ ),
+ remoteTabs.getAll(),
+ )
+ }
+
+ @Test
+ fun `exceptions from getAll are propagated to the crash reporter`() = runTest {
+ val throwable = RemoteTabProviderException.UnexpectedTabsException("test")
+ `when`(apiMock.getAll()).thenAnswer { throw throwable }
+
+ remoteTabs.getAll()
+
+ verify(crashReporter).submitCaughtException(throwable)
+
+ `when`(apiMock.setLocalTabs(any())).thenAnswer { throw throwable }
+
+ remoteTabs.store(emptyList())
+
+ verify(crashReporter, times(2)).submitCaughtException(throwable)
+
+ Unit
+ }
+}
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/bookmarks-v23.db b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/bookmarks-v23.db
new file mode 100644
index 0000000000..886f8318ad
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/bookmarks-v23.db
Binary files differ
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/empty-v0.db b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/empty-v0.db
new file mode 100644
index 0000000000..534c47cd51
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/empty-v0.db
Binary files differ
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/history-v34.db b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/history-v34.db
new file mode 100644
index 0000000000..31ec0f0b44
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/history-v34.db
Binary files differ
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/pinnedSites-v39.db b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/pinnedSites-v39.db
new file mode 100644
index 0000000000..158dbf1709
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/pinnedSites-v39.db
Binary files differ
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v38.db b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v38.db
new file mode 100644
index 0000000000..d9ed0df2fb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v38.db
Binary files differ
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db
new file mode 100644
index 0000000000..d673bf031e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db
Binary files differ
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-shm b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-shm
new file mode 100644
index 0000000000..fe9ac2845e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-shm
Binary files differ
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-wal b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-wal
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-wal
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/withHistory-v39.db b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/withHistory-v39.db
new file mode 100644
index 0000000000..0920fc8e12
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/withHistory-v39.db
Binary files differ
diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..ca6ee9cea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/storage-sync/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/tabstray/README.md b/mobile/android/android-components/components/browser/tabstray/README.md
new file mode 100644
index 0000000000..dc291eaf9f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Browser > Tabstray
+
+A customizable tabs tray for browsers.
+
+## Usage
+
+### 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:browser-tabstray:{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/browser/tabstray/build.gradle b/mobile/android/android-components/components/browser/tabstray/build.gradle
new file mode 100644
index 0000000000..72e76233b4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/build.gradle
@@ -0,0 +1,55 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.browser.tabstray'
+}
+
+dependencies {
+ api project(':concept-tabstray')
+
+ implementation project(':ui-icons')
+ implementation project(':ui-colors')
+ implementation project(':concept-base')
+ implementation project(':browser-state')
+ implementation project(':support-images')
+ implementation project(':support-ktx')
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_cardview
+ api ComponentsDependencies.androidx_recyclerview
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/browser/tabstray/proguard-rules.pro b/mobile/android/android-components/components/browser/tabstray/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/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/browser/tabstray/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/tabstray/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1eccdee26a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt
new file mode 100644
index 0000000000..8a9aa7b952
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt
@@ -0,0 +1,17 @@
+/* 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.browser.tabstray
+
+import android.view.View
+
+/**
+ * A contract for selectable ViewHolders for "tab" items.
+ */
+abstract class SelectableTabViewHolder(view: View) : TabViewHolder(view) {
+ /**
+ * Indicates the multi select state of tab item has changed based on [isSelected] .
+ */
+ abstract fun showTabIsMultiSelectEnabled(selectedMaskView: View?, isSelected: Boolean)
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabTouchCallback.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabTouchCallback.kt
new file mode 100644
index 0000000000..3c9ac7c0ef
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabTouchCallback.kt
@@ -0,0 +1,53 @@
+/* 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.browser.tabstray
+
+import android.graphics.Canvas
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.browser.state.state.TabSessionState
+
+/**
+ * An [ItemTouchHelper.Callback] for support gestures on tabs in the tray.
+ *
+ * @param onRemoveTab A callback invoked when a tab is removed.
+ */
+open class TabTouchCallback(
+ private val onRemoveTab: (TabSessionState) -> Unit,
+) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+ with(viewHolder as TabViewHolder) {
+ tab?.let { onRemoveTab(it) }
+ }
+ }
+
+ override fun onChildDraw(
+ c: Canvas,
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ dX: Float,
+ dY: Float,
+ actionState: Int,
+ isCurrentlyActive: Boolean,
+ ) {
+ if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
+ // Alpha on an itemView being swiped should decrease to a min over a distance equal to
+ // the width of the item being swiped.
+ viewHolder.itemView.alpha = alphaForItemSwipe(dX, viewHolder.itemView.width)
+ }
+
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
+ }
+
+ /**
+ * Sets the alpha value for a swipe gesture. This is useful for inherited classes to provide their own values.
+ */
+ open fun alphaForItemSwipe(dX: Float, distanceToAlphaMin: Int): Float {
+ return 1f
+ }
+
+ override fun onMove(p0: RecyclerView, p1: RecyclerView.ViewHolder, p2: RecyclerView.ViewHolder): Boolean = false
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabViewHolder.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabViewHolder.kt
new file mode 100644
index 0000000000..6c9a537af6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabViewHolder.kt
@@ -0,0 +1,152 @@
+/* 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.browser.tabstray
+
+import android.content.res.ColorStateList
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
+import mozilla.components.concept.base.images.ImageLoadRequest
+import mozilla.components.concept.base.images.ImageLoader
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+
+/**
+ * An abstract ViewHolder implementation for "tab" items.
+ */
+abstract class TabViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ abstract var tab: TabSessionState?
+
+ /**
+ * Binds the ViewHolder to the `Tab`.
+ * @param tab the `Tab` used to bind the viewHolder.
+ * @param isSelected boolean to describe whether or not the `Tab` is selected.
+ * @param observable message bus to pass events to Observers of the TabsTray.
+ * // TODO fix comment
+ */
+ abstract fun bind(
+ tab: TabSessionState,
+ isSelected: Boolean,
+ styling: TabsTrayStyling,
+ delegate: TabsTray.Delegate,
+ )
+
+ /**
+ * Ask for a partial update of the current tab.
+ * Allows for overriding the current behavior and add or remove the 'selected tab' UI decorator.
+ *
+ * When implementing this do not call super.
+ */
+ open fun updateSelectedTabIndicator(showAsSelected: Boolean) {
+ // Not an abstract fun since not all clients of this library might be interested in this functionality.
+ // But throwing an exception if this is called without an actual implementation in clients.
+ throw UnsupportedOperationException("Method not yet implemented")
+ }
+}
+
+/**
+ * The default implementation of `TabViewHolder`
+ */
+class DefaultTabViewHolder(
+ itemView: View,
+ private val thumbnailLoader: ImageLoader? = null,
+) : TabViewHolder(itemView) {
+ @VisibleForTesting
+ internal val iconView: ImageView? = itemView.findViewById(R.id.mozac_browser_tabstray_icon)
+
+ @VisibleForTesting
+ internal val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
+
+ @VisibleForTesting
+ internal val closeView: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close)
+ private val thumbnailView: TabThumbnailView = itemView.findViewById(R.id.mozac_browser_tabstray_thumbnail)
+ private val urlView: TextView? = itemView.findViewById(R.id.mozac_browser_tabstray_url)
+
+ override var tab: TabSessionState? = null
+
+ @VisibleForTesting
+ internal var styling: TabsTrayStyling? = null
+
+ /**
+ * Displays the data of the given session and notifies the given observable about events.
+ */
+ override fun bind(
+ tab: TabSessionState,
+ isSelected: Boolean,
+ styling: TabsTrayStyling,
+ delegate: TabsTray.Delegate,
+ ) {
+ this.tab = tab
+ this.styling = styling
+
+ val title = if (tab.content.title.isNotEmpty()) {
+ tab.content.title
+ } else {
+ tab.content.url
+ }
+
+ titleView.text = title
+ urlView?.text = tab.content.url.tryGetHostFromUrl()
+
+ itemView.setOnClickListener {
+ delegate.onTabSelected(tab)
+ }
+
+ closeView.setOnClickListener {
+ delegate.onTabClosed(tab)
+ }
+
+ updateSelectedTabIndicator(isSelected)
+
+ // In the final else case, we have no cache or fresh screenshot; do nothing instead of clearing the image.
+ if (thumbnailLoader != null) {
+ val thumbnailSize = THUMBNAIL_SIZE.dpToPx(thumbnailView.context.resources.displayMetrics)
+ thumbnailLoader.loadIntoView(
+ thumbnailView,
+ ImageLoadRequest(id = tab.id, size = thumbnailSize, isPrivate = tab.content.private),
+ )
+ }
+
+ iconView?.setImageBitmap(tab.content.icon)
+ }
+
+ override fun updateSelectedTabIndicator(showAsSelected: Boolean) {
+ if (showAsSelected) {
+ showItemAsSelected()
+ } else {
+ showItemAsNotSelected()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun showItemAsSelected() {
+ styling?.let { styling ->
+ titleView.setTextColor(styling.selectedItemTextColor)
+ itemView.setBackgroundColor(styling.selectedItemBackgroundColor)
+ closeView.imageTintList = ColorStateList.valueOf(styling.selectedItemTextColor)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun showItemAsNotSelected() {
+ styling?.let { styling ->
+ titleView.setTextColor(styling.itemTextColor)
+ itemView.setBackgroundColor(styling.itemBackgroundColor)
+ closeView.imageTintList = ColorStateList.valueOf(styling.itemTextColor)
+ }
+ }
+
+ companion object {
+ @Dimension(unit = DP)
+ private const val THUMBNAIL_SIZE = 100
+ }
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt
new file mode 100644
index 0000000000..4df5bd934f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt
@@ -0,0 +1,105 @@
+/* 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.browser.tabstray
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.concept.base.images.ImageLoader
+
+/**
+ * Function responsible for creating a `TabViewHolder` in the `TabsAdapter`.
+ */
+typealias ViewHolderProvider = (ViewGroup) -> TabViewHolder
+
+/**
+ * RecyclerView adapter implementation to display a list of tabs.
+ *
+ * @param thumbnailLoader an implementation of an [ImageLoader] for loading thumbnail images in the tabs tray.
+ * @param viewHolderProvider a function that creates a [TabViewHolder].
+ * @param styling the default styling for the [TabsTrayStyling].
+ * @param delegate a delegate to handle interactions in the tabs tray.
+ */
+open class TabsAdapter(
+ thumbnailLoader: ImageLoader? = null,
+ private val viewHolderProvider: ViewHolderProvider = { parent ->
+ DefaultTabViewHolder(
+ LayoutInflater.from(parent.context).inflate(R.layout.mozac_browser_tabstray_item, parent, false),
+ thumbnailLoader,
+ )
+ },
+ private val styling: TabsTrayStyling = TabsTrayStyling(),
+ private val delegate: TabsTray.Delegate,
+) : ListAdapter<TabSessionState, TabViewHolder>(DiffCallback), TabsTray {
+
+ private var selectedTabId: String? = null
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder {
+ return viewHolderProvider.invoke(parent)
+ }
+
+ override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
+ val tab = getItem(position)
+
+ holder.bind(tab, tab.id == selectedTabId, styling, delegate)
+ }
+
+ override fun onBindViewHolder(
+ holder: TabViewHolder,
+ position: Int,
+ payloads: List<Any>,
+ ) {
+ val tabs = currentList
+ if (tabs.isEmpty()) return
+
+ if (payloads.isEmpty()) {
+ onBindViewHolder(holder, position)
+ return
+ }
+
+ val tab = getItem(position)
+
+ if (payloads.contains(PAYLOAD_HIGHLIGHT_SELECTED_ITEM) && tab.id == selectedTabId) {
+ holder.updateSelectedTabIndicator(true)
+ } else if (payloads.contains(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM) && tab.id == selectedTabId) {
+ holder.updateSelectedTabIndicator(false)
+ }
+ }
+
+ override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) {
+ this.selectedTabId = selectedTabId
+
+ submitList(tabs)
+ }
+
+ companion object {
+ /**
+ * Payload used in onBindViewHolder for a partial update of the current view.
+ *
+ * Signals that the currently selected tab should be highlighted. This is the default behavior.
+ */
+ val PAYLOAD_HIGHLIGHT_SELECTED_ITEM: Int = R.id.payload_highlight_selected_item
+
+ /**
+ * Payload used in onBindViewHolder for a partial update of the current view.
+ *
+ * Signals that the currently selected tab should NOT be highlighted. No tabs would appear as highlighted.
+ */
+ val PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM: Int = R.id.payload_dont_highlight_selected_item
+ }
+
+ private object DiffCallback : DiffUtil.ItemCallback<TabSessionState>() {
+ override fun areItemsTheSame(oldItem: TabSessionState, newItem: TabSessionState): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: TabSessionState, newItem: TabSessionState): Boolean {
+ return oldItem == newItem
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt
new file mode 100644
index 0000000000..99a9f6b552
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt
@@ -0,0 +1,35 @@
+/* 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.browser.tabstray
+
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.TabSessionState
+
+/**
+ * An interface to display a list of tabs.
+ */
+interface TabsTray {
+
+ /**
+ * Interface to be implemented by classes that want to observe or react to the interactions on the tabs list.
+ */
+ interface Delegate {
+
+ /**
+ * A new tab has been selected.
+ */
+ fun onTabSelected(tab: TabSessionState, source: String? = null)
+
+ /**
+ * A tab has been closed.
+ */
+ fun onTabClosed(tab: TabSessionState, source: String? = null)
+ }
+
+ /**
+ * Called when the list of tabs are updated.
+ */
+ fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?)
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTrayStyling.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTrayStyling.kt
new file mode 100644
index 0000000000..75808f77f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTrayStyling.kt
@@ -0,0 +1,31 @@
+/* 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.browser.tabstray
+
+const val DEFAULT_ITEM_BACKGROUND_COLOR = 0xFFFFFFFF.toInt()
+const val DEFAULT_ITEM_BACKGROUND_SELECTED_COLOR = 0xFFFF45A1FF.toInt()
+const val DEFAULT_ITEM_TEXT_COLOR = 0xFF111111.toInt()
+const val DEFAULT_ITEM_TEXT_SELECTED_COLOR = 0xFFFFFFFF.toInt()
+
+/**
+ * Tabs tray styling for items in the [TabsAdapter]. If a custom [TabViewHolder]
+ * is used with [TabsAdapter.viewHolderProvider], the styling can be applied
+ * when [TabViewHolder.bind] is invoked.
+ *
+ * @property itemBackgroundColor the background color for all non-selected tabs.
+ * @property selectedItemBackgroundColor the background color for the selected tab.
+ * @property itemTextColor the text color for all non-selected tabs.
+ * @property selectedItemTextColor the text color for the selected tabs.
+ * @property itemUrlTextColor the URL text color for all non-selected tabs.
+ * @property selectedItemUrlTextColor the URL text color for the selected tab.
+ */
+data class TabsTrayStyling(
+ val itemBackgroundColor: Int = DEFAULT_ITEM_BACKGROUND_COLOR,
+ val selectedItemBackgroundColor: Int = DEFAULT_ITEM_BACKGROUND_SELECTED_COLOR,
+ val itemTextColor: Int = DEFAULT_ITEM_TEXT_COLOR,
+ val selectedItemTextColor: Int = DEFAULT_ITEM_TEXT_SELECTED_COLOR,
+ val itemUrlTextColor: Int = DEFAULT_ITEM_TEXT_COLOR,
+ val selectedItemUrlTextColor: Int = DEFAULT_ITEM_TEXT_SELECTED_COLOR,
+)
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailView.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailView.kt
new file mode 100644
index 0000000000..bbb2db2d22
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailView.kt
@@ -0,0 +1,33 @@
+/* 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.browser.tabstray.thumbnail
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatImageView
+
+class TabThumbnailView(context: Context, attrs: AttributeSet) : AppCompatImageView(context, attrs) {
+
+ init {
+ scaleType = ScaleType.MATRIX
+ }
+
+ @VisibleForTesting
+ public override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
+ val result = super.setFrame(l, t, r, b)
+
+ val matrix = imageMatrix
+ val scaleFactor = if (drawable != null) {
+ width / drawable.intrinsicWidth.toFloat()
+ } else {
+ 1F
+ }
+ matrix.setScale(scaleFactor, scaleFactor, 0f, 0f)
+ imageMatrix = matrix
+
+ return result
+ }
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/res/layout/mozac_browser_tabstray_item.xml b/mobile/android/android-components/components/browser/tabstray/src/main/res/layout/mozac_browser_tabstray_item.xml
new file mode 100644
index 0000000000..fe701265b3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/res/layout/mozac_browser_tabstray_item.xml
@@ -0,0 +1,68 @@
+<?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/. -->
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="150dp"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_margin="4dp"
+ android:clickable="true"
+ android:focusable="true">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/mozac_browser_tabstray_icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:importantForAccessibility="no"
+ android:padding="4dp" />
+
+ <TextView
+ android:id="@+id/mozac_browser_tabstray_title"
+ android:layout_width="match_parent"
+ android:layout_height="36dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="42dp"
+ android:layout_toStartOf="@id/mozac_browser_tabstray_close"
+ android:ellipsize="end"
+ android:lines="1"
+ android:padding="8dp"
+ tools:text="Mozilla" />
+
+ <TextView
+ android:id="@+id/mozac_browser_tabstray_url"
+ android:layout_width="match_parent"
+ android:layout_height="36dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="42dp"
+ android:layout_toStartOf="@id/mozac_browser_tabstray_close"
+ android:ellipsize="end"
+ android:lines="1"
+ android:padding="8dp"
+ android:visibility="gone"
+ tools:text="www.mozilla.org" />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/mozac_browser_tabstray_close"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true"
+ android:contentDescription="@string/mozac_browser_tabstray_close_tab"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ app:srcCompat="@drawable/mozac_ic_cross_20" />
+
+ <mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
+ android:id="@+id/mozac_browser_tabstray_thumbnail"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@id/mozac_browser_tabstray_title"
+ android:contentDescription="@string/mozac_browser_tabstray_open_tab" />
+
+ </RelativeLayout>
+</androidx.cardview.widget.CardView>
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/res/values/ids.xml b/mobile/android/android-components/components/browser/tabstray/src/main/res/values/ids.xml
new file mode 100644
index 0000000000..5926a7437d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/res/values/ids.xml
@@ -0,0 +1,8 @@
+<?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>
+ <item name="payload_highlight_selected_item" type="id"/>
+ <item name="payload_dont_highlight_selected_item" type="id"/>
+</resources>
diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/res/values/mozac_browser_tabstray_strings.xml b/mobile/android/android-components/components/browser/tabstray/src/main/res/values/mozac_browser_tabstray_strings.xml
new file mode 100644
index 0000000000..acd1b1256d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/main/res/values/mozac_browser_tabstray_strings.xml
@@ -0,0 +1,10 @@
+<?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>
+
+ <!-- Content description (not visible, for screen readers etc.): Description for button closing a tab. -->
+ <string name="mozac_browser_tabstray_close_tab">Close Tab</string>
+ <string name="mozac_browser_tabstray_open_tab">Open Tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/DefaultTabViewHolderTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/DefaultTabViewHolderTest.kt
new file mode 100644
index 0000000000..833a3f1358
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/DefaultTabViewHolderTest.kt
@@ -0,0 +1,223 @@
+/* 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.browser.tabstray
+
+import android.content.res.ColorStateList
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.base.images.ImageLoadRequest
+import mozilla.components.concept.base.images.ImageLoader
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.nullable
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class DefaultTabViewHolderTest {
+
+ @Test
+ fun `URL from session is assigned to view`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val titleView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_title)
+ val urlView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_url)
+
+ val holder = DefaultTabViewHolder(view)
+
+ assertEquals("", titleView.text)
+
+ val session = createTab(id = "a", url = "https://www.mozilla.org")
+
+ holder.bind(session, isSelected = false, styling = mock(), mock())
+
+ assertEquals("https://www.mozilla.org", titleView.text)
+ assertEquals("www.mozilla.org", urlView.text)
+ }
+
+ @Test
+ fun `URL text is set to tab URL when exception is thrown`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val urlView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_url)
+ val holder = DefaultTabViewHolder(view)
+ val session = createTab(id = "a", url = "about:home")
+
+ holder.bind(session, isSelected = false, styling = mock(), mock())
+
+ assertEquals("about:home", urlView.text)
+ }
+
+ @Test
+ fun `observer gets notified if item is clicked`() {
+ val delegate: TabsTray.Delegate = mock()
+
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val holder = DefaultTabViewHolder(view)
+
+ val session = createTab(url = "https://www.mozilla.org", id = "a")
+ holder.bind(session, isSelected = false, styling = mock(), delegate)
+
+ view.performClick()
+
+ verify(delegate).onTabSelected(session)
+ }
+
+ @Test
+ fun `observer gets notified if tab gets closed`() {
+ val delegate: TabsTray.Delegate = mock()
+
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val holder = DefaultTabViewHolder(view)
+
+ val session = createTab(url = "https://www.mozilla.org", id = "a")
+ holder.bind(session, isSelected = true, styling = mock(), delegate)
+
+ view.findViewById<View>(R.id.mozac_browser_tabstray_close).performClick()
+
+ verify(delegate).onTabClosed(session)
+ }
+
+ @Test
+ fun `url from session is displayed by default`() {
+ val delegate: TabsTray.Delegate = mock()
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val holder = DefaultTabViewHolder(view)
+
+ val session = createTab(url = "https://www.mozilla.org", id = "a")
+ val titleView = holder.itemView.findViewById<TextView>(R.id.mozac_browser_tabstray_title)
+ val urlView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_url)
+
+ holder.bind(session, isSelected = true, styling = mock(), delegate)
+
+ assertEquals(session.content.url, titleView.text)
+ assertEquals("www.mozilla.org", urlView.text)
+ }
+
+ @Test
+ fun `title from session is displayed if available`() {
+ val delegate: TabsTray.Delegate = mock()
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val holder = DefaultTabViewHolder(view)
+
+ val session = createTab(url = "https://www.mozilla.org", title = "Mozilla Firefox", id = "a")
+ val titleView = holder.itemView.findViewById<TextView>(R.id.mozac_browser_tabstray_title)
+ val urlView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_url)
+
+ holder.bind(session, isSelected = true, styling = mock(), delegate)
+ assertEquals("Mozilla Firefox", titleView.text)
+ assertEquals("www.mozilla.org", urlView.text)
+ }
+
+ @Test
+ fun `thumbnail is set from loader`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val loader: ImageLoader = mock()
+ val viewHolder = DefaultTabViewHolder(view, loader)
+ val tab = createTab(id = "123", url = "https://example.com")
+
+ viewHolder.bind(tab, false, mock(), mock())
+
+ verify(loader).loadIntoView(any(), eq(ImageLoadRequest("123", 100, false)), nullable(), nullable())
+ }
+
+ @Test
+ fun `thumbnailView does not change when there is no cache or new thumbnail`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val viewHolder = DefaultTabViewHolder(view)
+ val tab = createTab(id = "123", url = "https://example.com")
+ val thumbnailView = view.findViewById<ImageView>(R.id.mozac_browser_tabstray_thumbnail)
+
+ thumbnailView.setImageBitmap(mock())
+ val drawable = thumbnailView.drawable
+
+ viewHolder.bind(tab, false, mock(), mock())
+
+ assertEquals(drawable, thumbnailView.drawable)
+ }
+
+ @Test
+ fun `bind sets the values for this instance's Tab and TabsTrayStyling properties`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val viewHolder = DefaultTabViewHolder(view)
+ val tab = createTab(id = "123", url = "https://example.com")
+ val styling: TabsTrayStyling = mock()
+
+ assertNull(viewHolder.tab)
+ assertNull(viewHolder.styling)
+
+ viewHolder.bind(tab, false, styling, mock())
+
+ assertSame(tab, viewHolder.tab)
+ assertSame(styling, viewHolder.styling)
+ }
+
+ @Test
+ fun `bind shows an item as selected or not`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val tab = createTab(id = "123", url = "https://example.com")
+ val viewHolder = spy(DefaultTabViewHolder(view))
+
+ viewHolder.bind(tab, false, mock(), mock())
+ verify(viewHolder).updateSelectedTabIndicator(false)
+
+ viewHolder.bind(tab, true, mock(), mock())
+ verify(viewHolder).updateSelectedTabIndicator(true)
+ }
+
+ @Test
+ fun `updateSelectedTabIndicator should further delegate to the appropriate method`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val viewHolder = spy(DefaultTabViewHolder(view))
+
+ viewHolder.updateSelectedTabIndicator(showAsSelected = true)
+ verify(viewHolder).showItemAsSelected()
+
+ viewHolder.updateSelectedTabIndicator(showAsSelected = false)
+ verify(viewHolder).showItemAsNotSelected()
+ }
+
+ @Test
+ fun `showItemAsSelected should use TabsTrayStyling for indicating that an item is currently selected`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val viewHolder = spy(DefaultTabViewHolder(view))
+ val tab = createTab(id = "123", url = "https://example.com")
+ val styling = TabsTrayStyling()
+
+ // Need to be called first to set the styling for this holder
+ viewHolder.bind(tab, false, TabsTrayStyling(), mock())
+ viewHolder.updateSelectedTabIndicator(true)
+
+ assertEquals(styling.selectedItemTextColor, viewHolder.titleView.textColors.defaultColor)
+ assertEquals(styling.selectedItemBackgroundColor, (viewHolder.itemView.background as ColorDrawable).color)
+ assertEquals(ColorStateList.valueOf(styling.selectedItemTextColor), viewHolder.closeView.imageTintList)
+ }
+
+ @Test
+ fun `showItemAsSelected should use TabsTrayStyling for indicating that an item is not currently selected`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val viewHolder = spy(DefaultTabViewHolder(view))
+ val tab = createTab(id = "123", url = "https://example.com")
+ val styling = TabsTrayStyling()
+
+ // Need to be called first to set the styling for this holder
+ viewHolder.bind(tab, true, TabsTrayStyling(), mock())
+ viewHolder.updateSelectedTabIndicator(false)
+
+ assertEquals(styling.itemTextColor, viewHolder.titleView.textColors.defaultColor)
+ assertEquals(styling.itemBackgroundColor, (viewHolder.itemView.background as ColorDrawable).color)
+ assertEquals(ColorStateList.valueOf(styling.itemTextColor), viewHolder.closeView.imageTintList)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabTouchCallbackTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabTouchCallbackTest.kt
new file mode 100644
index 0000000000..3df5abdead
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabTouchCallbackTest.kt
@@ -0,0 +1,73 @@
+/* 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.browser.tabstray
+
+import android.view.LayoutInflater
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class TabTouchCallbackTest {
+
+ @Test
+ fun `onSwiped notifies observers`() {
+ var onTabClosedWasCalled = false
+
+ val onTabClosed: (TabSessionState) -> Unit = {
+ onTabClosedWasCalled = true
+ }
+ val touchCallback = TabTouchCallback(onTabClosed)
+ val viewHolder: TabViewHolder = mock()
+
+ touchCallback.onSwiped(viewHolder, 0)
+
+ assertFalse(onTabClosedWasCalled)
+
+ // With a session available.
+ `when`(viewHolder.tab).thenReturn(mock())
+
+ touchCallback.onSwiped(viewHolder, 0)
+
+ assertTrue(onTabClosedWasCalled)
+ }
+
+ @Test
+ fun `onChildDraw alters alpha of ViewHolder on swipe gesture`() {
+ val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null)
+ val holder = DefaultTabViewHolder(view)
+ val callback = TabTouchCallback(mock())
+
+ holder.itemView.alpha = 0f
+
+ callback.onChildDraw(mock(), mock(), holder, 0f, 0f, ItemTouchHelper.ACTION_STATE_DRAG, true)
+
+ assertEquals(0f, holder.itemView.alpha)
+
+ callback.onChildDraw(mock(), mock(), holder, 0f, 0f, ItemTouchHelper.ACTION_STATE_SWIPE, true)
+
+ assertEquals(1f, holder.itemView.alpha)
+ }
+
+ @Test
+ fun `alpha default is full`() {
+ val touchCallback = TabTouchCallback(mock())
+ assertEquals(1f, touchCallback.alphaForItemSwipe(0f, 0))
+ }
+
+ @Test
+ fun `onMove is not implemented`() {
+ val touchCallback = TabTouchCallback(mock())
+ assertFalse(touchCallback.onMove(mock(), mock(), mock()))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabViewHolderTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabViewHolderTest.kt
new file mode 100644
index 0000000000..4a69343cb1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabViewHolderTest.kt
@@ -0,0 +1,52 @@
+/* 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.browser.tabstray
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import junit.framework.TestCase
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.support.test.expectException
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TabViewHolderTest : TestCase() {
+
+ @Test
+ fun `updateSelectedTabIndicator needs to have a provided implementation`() {
+ val simpleTabViewHolder = object : TabViewHolder(View(testContext)) {
+ override var tab: TabSessionState? = null
+ override fun bind(
+ tab: TabSessionState,
+ isSelected: Boolean,
+ styling: TabsTrayStyling,
+ delegate: TabsTray.Delegate,
+ ) { /* noop */ }
+ }
+
+ expectException(UnsupportedOperationException::class) {
+ simpleTabViewHolder.updateSelectedTabIndicator(true)
+ }
+ }
+
+ @Test
+ fun `updateSelectedTabIndicator with a provided implementation just works`() {
+ val tabViewHolder = object : TabViewHolder(View(testContext)) {
+ override var tab: TabSessionState? = null
+ override fun bind(
+ tab: TabSessionState,
+ isSelected: Boolean,
+ styling: TabsTrayStyling,
+ delegate: TabsTray.Delegate,
+ ) { /* noop */ }
+ override fun updateSelectedTabIndicator(showAsSelected: Boolean) { /* noop */ }
+ }
+
+ // Simply test that this would not fail the test like it would happen above.
+ tabViewHolder.updateSelectedTabIndicator(true)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt
new file mode 100644
index 0000000000..20fa366b52
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt
@@ -0,0 +1,161 @@
+/* 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.browser.tabstray
+
+import android.view.View
+import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
+import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+private class TestTabViewHolder(view: View) : TabViewHolder(view) {
+ override var tab: TabSessionState? = null
+ override fun bind(
+ tab: TabSessionState,
+ isSelected: Boolean,
+ styling: TabsTrayStyling,
+ delegate: TabsTray.Delegate,
+ ) { // noop
+ }
+
+ override fun updateSelectedTabIndicator(showAsSelected: Boolean) { // noop
+ }
+}
+
+@RunWith(AndroidJUnit4::class)
+class TabsAdapterTest {
+
+ @Test
+ fun `onCreateViewHolder will create a DefaultTabViewHolder`() {
+ val adapter = TabsAdapter(delegate = mock())
+
+ val type = adapter.onCreateViewHolder(FrameLayout(testContext), 0)
+
+ assertTrue(type is DefaultTabViewHolder)
+ }
+
+ @Test
+ fun `onCreateViewHolder will create whatever TabViewHolder is provided`() {
+ val adapter = TabsAdapter(
+ viewHolderProvider = { _ -> TestTabViewHolder(View(testContext)) },
+ delegate = mock(),
+ )
+
+ val type = adapter.onCreateViewHolder(FrameLayout(testContext), 0)
+
+ assertTrue(type is TestTabViewHolder)
+ }
+
+ @Test
+ fun `itemCount will reflect number of sessions`() {
+ val adapter = TabsAdapter(delegate = mock())
+ assertEquals(0, adapter.itemCount)
+
+ adapter.updateTabs(
+ listOf(
+ createTab(id = "A", url = "https://www.mozilla.org"),
+ createTab(id = "B", url = "https://www.firefox.com"),
+ ),
+ tabPartition = null,
+ selectedTabId = "A",
+ )
+ assertEquals(2, adapter.itemCount)
+ }
+
+ @Test
+ fun `onBindViewHolder calls bind on matching holder`() {
+ val styling = TabsTrayStyling()
+ val delegate = mock<TabsTray.Delegate>()
+ val adapter = TabsAdapter(delegate = delegate, styling = styling)
+
+ val holder: TabViewHolder = mock()
+
+ val tab = createTab(id = "A", url = "https://www.mozilla.org")
+
+ adapter.updateTabs(
+ listOf(tab),
+ null,
+ "A",
+ )
+
+ adapter.onBindViewHolder(holder, 0)
+
+ verify(holder).bind(tab, true, styling, delegate)
+ }
+
+ @Test
+ fun `onBindViewHolder will use payloads to indicate if this item is selected`() {
+ val adapter = TabsAdapter(delegate = mock())
+ val holder = spy(TestTabViewHolder(View(testContext)))
+ val tab = createTab(id = "A", url = "https://www.mozilla.org")
+
+ adapter.updateTabs(listOf(mock(), tab), tabPartition = null, selectedTabId = "A")
+
+ adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM))
+ verify(holder, never()).updateSelectedTabIndicator(ArgumentMatchers.anyBoolean())
+
+ adapter.onBindViewHolder(holder, 1, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM))
+ verify(holder).updateSelectedTabIndicator(true)
+ }
+
+ @Test
+ fun `onBindViewHolder will use payloads to indicate if this item is not selected`() {
+ val adapter = TabsAdapter(delegate = mock())
+ val holder = spy(TestTabViewHolder(View(testContext)))
+ val tab = createTab(id = "A", url = "https://www.mozilla.org")
+ adapter.updateTabs(listOf(mock(), tab), tabPartition = null, selectedTabId = "A")
+
+ adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
+ verify(holder, never()).updateSelectedTabIndicator(ArgumentMatchers.anyBoolean())
+
+ adapter.onBindViewHolder(holder, 1, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
+ verify(holder).updateSelectedTabIndicator(false)
+ }
+
+ @Test
+ fun `onBindViewHolder with payloads will return early if there are currently no open tabs`() {
+ val adapter = TabsAdapter(delegate = mock())
+ val holder = TestTabViewHolder(View(testContext))
+ val payloads = spy(arrayListOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
+
+ adapter.onBindViewHolder(holder, 0, payloads)
+ // verify that calls we expect further down are not happening after the null check
+ verify(payloads, never()).isEmpty()
+ verify(payloads, never()).contains(ArgumentMatchers.anyInt())
+
+ adapter.updateTabs(emptyList(), tabPartition = null, selectedTabId = null)
+ adapter.onBindViewHolder(holder, 0, payloads)
+ // verify that calls we expect further down are not happening after the null check
+ verify(payloads, never()).isEmpty()
+ verify(payloads, never()).contains(ArgumentMatchers.anyInt())
+ }
+
+ @Test
+ fun `onBindViewHolder with empty payloads will call onBindViewHolder and return early for a full bind`() {
+ val adapter = TabsAdapter(delegate = mock())
+ val holder = TestTabViewHolder(View(testContext))
+ val emptyPayloads = spy(arrayListOf<String>())
+
+ adapter.updateTabs(listOf(mock()), tabPartition = null, selectedTabId = null)
+
+ adapter.onBindViewHolder(holder, 0, emptyPayloads)
+
+ verify(emptyPayloads).isEmpty()
+ // verify that calls we expect further down are not happening after the isEmpty check
+ verify(emptyPayloads, never()).contains(ArgumentMatchers.anyString())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailViewTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailViewTest.kt
new file mode 100644
index 0000000000..9842186874
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailViewTest.kt
@@ -0,0 +1,81 @@
+/* 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.browser.tabstray.thumbnail
+
+import android.graphics.Matrix
+import android.graphics.drawable.Drawable
+import android.widget.ImageView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.Robolectric.buildAttributeSet
+
+@RunWith(AndroidJUnit4::class)
+class TabThumbnailViewTest {
+
+ @Test
+ fun `view should always use Matrix ScaleType`() {
+ val view = TabThumbnailView(testContext, emptyAttributeSet())
+ assertEquals(ImageView.ScaleType.MATRIX, view.scaleType)
+ }
+
+ @Test
+ fun `view updates matrix when changed`() {
+ val view = TabThumbnailView(testContext, emptyAttributeSet())
+ val matrix = view.imageMatrix
+ val drawable: Drawable = mock()
+
+ `when`(drawable.intrinsicWidth).thenReturn(5)
+ `when`(drawable.intrinsicHeight).thenReturn(5)
+
+ view.setImageDrawable(drawable)
+ view.setFrame(5, 5, 5, 5)
+
+ val matrix2 = view.imageMatrix
+
+ assertNotEquals(matrix, matrix2)
+ }
+
+ @Test
+ fun `view updates don't change matrix if no changes to frame`() {
+ val view = TabThumbnailView(testContext, emptyAttributeSet())
+ val drawable: Drawable = mock()
+
+ `when`(drawable.intrinsicWidth).thenReturn(5)
+ `when`(drawable.intrinsicHeight).thenReturn(5)
+
+ view.setImageDrawable(drawable)
+ view.setFrame(5, 5, 5, 5)
+
+ val matrix = view.imageMatrix
+
+ view.setFrame(5, 5, 5, 5)
+
+ val matrix2 = view.imageMatrix
+
+ assertEquals(matrix, matrix2)
+ }
+
+ @Test
+ fun `view scaleFactor does not change if there is no drawable`() {
+ val view = spy(TabThumbnailView(testContext, emptyAttributeSet()))
+ val matrix: Matrix = spy(Matrix())
+
+ `when`(view.imageMatrix).thenReturn(matrix)
+
+ view.setFrame(5, 5, 5, 5)
+
+ verify(matrix).setScale(1f, 1f, 0f, 0f)
+ }
+}
+
+private fun emptyAttributeSet() = buildAttributeSet().build()
diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/tabstray/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/browser/thumbnails/README.md b/mobile/android/android-components/components/browser/thumbnails/README.md
new file mode 100644
index 0000000000..ba465c867f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/README.md
@@ -0,0 +1,81 @@
+# [Android Components](../../../README.md) > Browser > Thumbnails
+
+A component for loading and storing website thumbnails (screenshot of the website).
+
+## Usage
+
+### 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:browser-thumbnails:{latest-version}"
+```
+
+## Requesting thumbnails
+
+To get thumbnail images from the browser, we need to request them from the `EngineView`. This can
+be done easily by using `BrowserThumbnails` which will then notify the `BrowserStore` when a
+thumbnail has been received:
+
+```kotlin
+browserThumbnails.set(
+ feature = BrowserThumbnails(context, layout.engineView, browserStore),
+ owner = this,
+ view = layout
+)
+```
+
+`BrowserThumbnails` tries to make requests as frequent as possible in order to get the most
+up-to-date state of the site in the images.
+
+The various situations when we try to request a thumbnail:
+ - During the Android lifecycle event `onStart`.
+ - When the selected tab's `loading` state changes.
+ - More to be added..
+
+## Storing to disk
+
+When we receive new thumbnails, we may want to persist them to disk as these images can be quite large.
+
+To do this, we need to add the `BrowserMiddleware` to receive the image from the store
+and put it in our storage:
+
+```kotlin
+val thumbnailStorage by lazy { ThumbnailStorage(applicationContext) }
+
+val store by lazy {
+ BrowserStore(middleware = listOf(
+ ThumbnailsMiddleware(thumbnailStorage)
+ ))
+}
+```
+
+## Loading from disk
+
+Now that we have the thumbnails stored to disk, we can access them via the `ThumbnailStorage`
+directly:
+
+```kotlin
+runBlocking {
+ val bitmap = thumbnailStorage.loadThumbnail(
+ request = ImageLoadRequest("thumbnailId", maxPreferredImageDimen)
+ )
+}
+```
+
+A better way, is to use the `ThumbnailLoader`:
+
+```kotlin
+val thumbnailLoader = ThumbnailLoader(components.thumbnailStorage)
+thumbnailLoader.loadIntoView(
+ view = thumbnailView,
+ request = ImageLoadRequest(id = tab.id, size = thumbnailSize)
+)
+```
+
+## 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/browser/thumbnails/build.gradle b/mobile/android/android-components/components/browser/thumbnails/build.gradle
new file mode 100644
index 0000000000..b803fd23a5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/build.gradle
@@ -0,0 +1,63 @@
+/* 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'
+
+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.browser.thumbnails'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':concept-base')
+ implementation project(':support-ktx')
+ implementation project(':support-images')
+
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.thirdparty_disklrucache
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/browser/thumbnails/proguard-rules.pro b/mobile/android/android-components/components/browser/thumbnails/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/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/browser/thumbnails/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/thumbnails/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/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/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/BrowserThumbnails.kt b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/BrowserThumbnails.kt
new file mode 100644
index 0000000000..c61ae638fc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/BrowserThumbnails.kt
@@ -0,0 +1,86 @@
+/* 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.browser.thumbnails
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.android.content.isOSOnLowMemory
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
+
+/**
+ * Feature implementation for automatically taking thumbnails of sites.
+ * The feature will take a screenshot when the page finishes loading,
+ * and will add it to the [ContentState.thumbnail] property.
+ *
+ * If the OS is under low memory conditions, the screenshot will be not taken.
+ * Ideally, this should be used in conjunction with `SessionManager.onLowMemory` to allow
+ * free up some [ContentState.thumbnail] from memory.
+ */
+class BrowserThumbnails(
+ private val context: Context,
+ private val engineView: EngineView,
+ private val store: BrowserStore,
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Starts observing the selected session to listen for when a session finishes loading.
+ */
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map { it.selectedTab }
+ .ifAnyChanged { arrayOf(it?.content?.loading, it?.content?.firstContentfulPaint) }
+ .collect { state ->
+ if (state?.content?.loading == false && state.content.firstContentfulPaint) {
+ requestScreenshot()
+ }
+ }
+ }
+ }
+
+ /**
+ * Requests a screenshot to be taken that can be observed from [BrowserStore] if successful. The request can fail
+ * if the device is low on memory or if there is no tab attached to the [EngineView].
+ */
+ fun requestScreenshot() {
+ if (!isLowOnMemory()) {
+ // Create a local reference to prevent capturing "this" in the lambda
+ // which would leak the context if the view is destroyed before the
+ // callback is invoked. This is a workaround for:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1678364
+ val store = this.store
+ engineView.captureThumbnail {
+ val bitmap = it ?: return@captureThumbnail
+ val tabId = store.state.selectedTabId ?: return@captureThumbnail
+
+ store.dispatch(ContentAction.UpdateThumbnailAction(tabId, bitmap))
+ }
+ }
+ }
+
+ /**
+ * Stops observing the selected session.
+ */
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ @VisibleForTesting
+ internal var testLowMemory = false
+
+ private fun isLowOnMemory() = testLowMemory || context.isOSOnLowMemory()
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsMiddleware.kt b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsMiddleware.kt
new file mode 100644
index 0000000000..1d8bdca7bb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsMiddleware.kt
@@ -0,0 +1,72 @@
+/* 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.browser.thumbnails
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
+import mozilla.components.concept.base.images.ImageSaveRequest
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] implementation for handling [ContentAction.UpdateThumbnailAction] and storing
+ * the thumbnail to the disk cache.
+ */
+class ThumbnailsMiddleware(
+ private val thumbnailStorage: ThumbnailStorage,
+) : Middleware<BrowserState, BrowserAction> {
+ @Suppress("ComplexMethod")
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is TabListAction.RemoveAllNormalTabsAction -> {
+ context.state.tabs.filterNot { it.content.private }.forEach { tab ->
+ thumbnailStorage.deleteThumbnail(tab.id, isPrivate = false)
+ }
+ }
+ is TabListAction.RemoveAllPrivateTabsAction -> {
+ context.state.tabs.filter { it.content.private }.forEach { tab ->
+ thumbnailStorage.deleteThumbnail(tab.id, isPrivate = true)
+ }
+ }
+ is TabListAction.RemoveAllTabsAction -> {
+ thumbnailStorage.clearThumbnails()
+ }
+ is TabListAction.RemoveTabAction -> {
+ // Delete the tab screenshot from the storage when the tab is removed.
+ val isPrivate = context.state.isTabIdPrivate(action.tabId)
+ thumbnailStorage.deleteThumbnail(action.tabId, isPrivate)
+ }
+ is TabListAction.RemoveTabsAction -> {
+ action.tabIds.forEach { id ->
+ val isPrivate = context.state.isTabIdPrivate(id)
+ thumbnailStorage.deleteThumbnail(id, isPrivate)
+ }
+ }
+ is ContentAction.UpdateThumbnailAction -> {
+ // Store the captured tab screenshot from the EngineView when the session's
+ // thumbnail is updated.
+ context.store.state.tabs.find { it.id == action.sessionId }?.let { session ->
+ val request = ImageSaveRequest(session.id, session.content.private)
+ thumbnailStorage.saveThumbnail(request, action.thumbnail)
+ }
+ return // Do not let the thumbnail actions continue through to the reducer.
+ }
+ else -> {
+ // no-op
+ }
+ }
+ next(action)
+ }
+
+ private fun BrowserState.isTabIdPrivate(id: String): Boolean =
+ tabs.any { it.id == id && it.content.private }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt
new file mode 100644
index 0000000000..19382b69bf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt
@@ -0,0 +1,71 @@
+/* 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.browser.thumbnails.loader
+
+import android.graphics.drawable.Drawable
+import android.widget.ImageView
+import androidx.annotation.MainThread
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import mozilla.components.browser.thumbnails.R
+import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
+import mozilla.components.concept.base.images.ImageLoadRequest
+import mozilla.components.concept.base.images.ImageLoader
+import mozilla.components.support.images.CancelOnDetach
+import java.lang.ref.WeakReference
+
+/**
+ * An implementation of [ImageLoader] for loading thumbnails into a [ImageView].
+ */
+class ThumbnailLoader(private val storage: ThumbnailStorage) : ImageLoader {
+
+ override fun loadIntoView(
+ view: ImageView,
+ request: ImageLoadRequest,
+ placeholder: Drawable?,
+ error: Drawable?,
+ ) {
+ CoroutineScope(Dispatchers.Main).launch {
+ loadIntoViewInternal(WeakReference(view), request, placeholder, error)
+ }
+ }
+
+ @MainThread
+ private suspend fun loadIntoViewInternal(
+ view: WeakReference<ImageView>,
+ request: ImageLoadRequest,
+ placeholder: Drawable?,
+ error: Drawable?,
+ ) {
+ // If we previously started loading into the view, cancel the job.
+ val existingJob = view.get()?.getTag(R.id.mozac_browser_thumbnails_tag_job) as? Job
+ existingJob?.cancel()
+
+ // Create a loading job
+ val deferredThumbnail = storage.loadThumbnail(request)
+
+ view.get()?.setTag(R.id.mozac_browser_thumbnails_tag_job, deferredThumbnail)
+ val onAttachStateChangeListener = CancelOnDetach(deferredThumbnail).also {
+ view.get()?.addOnAttachStateChangeListener(it)
+ }
+
+ try {
+ val thumbnail = deferredThumbnail.await()
+ if (thumbnail != null) {
+ view.get()?.setImageBitmap(thumbnail)
+ } else {
+ view.get()?.setImageDrawable(placeholder)
+ }
+ } catch (e: CancellationException) {
+ view.get()?.setImageDrawable(error)
+ } finally {
+ view.get()?.removeOnAttachStateChangeListener(onAttachStateChangeListener)
+ view.get()?.setTag(R.id.mozac_browser_thumbnails_tag_job, null)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorage.kt b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorage.kt
new file mode 100644
index 0000000000..a2e7e97e30
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorage.kt
@@ -0,0 +1,133 @@
+/* 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.browser.thumbnails.storage
+
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.annotation.WorkerThread
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import mozilla.components.browser.thumbnails.R
+import mozilla.components.browser.thumbnails.utils.ThumbnailDiskCache
+import mozilla.components.concept.base.images.ImageLoadRequest
+import mozilla.components.concept.base.images.ImageSaveRequest
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.NamedThreadFactory
+import mozilla.components.support.images.DesiredSize
+import mozilla.components.support.images.decoder.AndroidImageDecoder
+import java.util.concurrent.Executors
+
+private const val MAXIMUM_SCALE_FACTOR = 2.0f
+
+// Number of worker threads we are using internally.
+private const val THREADS = 3
+
+internal val sharedDiskCache = ThumbnailDiskCache()
+internal val privateDiskCache = ThumbnailDiskCache(isPrivate = true)
+
+/**
+ * Thumbnail storage layer which handles saving and loading the thumbnail from the disk cache.
+ */
+class ThumbnailStorage(
+ private val context: Context,
+ jobDispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(
+ THREADS,
+ NamedThreadFactory("ThumbnailStorage"),
+ ).asCoroutineDispatcher(),
+) {
+ private val decoders = AndroidImageDecoder()
+ private val logger = Logger("ThumbnailStorage")
+ private val maximumSize =
+ context.resources.getDimensionPixelSize(R.dimen.mozac_browser_thumbnails_maximum_size)
+ private val scope = CoroutineScope(jobDispatcher)
+
+ init {
+ privateDiskCache.clear(context)
+ }
+
+ /**
+ * Clears all the stored thumbnails in the disk cache.
+ */
+ fun clearThumbnails(): Job =
+ scope.launch {
+ logger.debug("Cleared all thumbnails from disk")
+ sharedDiskCache.clear(context)
+ privateDiskCache.clear(context)
+ }
+
+ /**
+ * Deletes the given thumbnail [Bitmap] from the disk cache with the provided session ID or url
+ * as its key.
+ */
+ fun deleteThumbnail(sessionIdOrUrl: String, isPrivate: Boolean): Job =
+ scope.launch {
+ logger.debug("Removed thumbnail from disk (sessionIdOrUrl = $sessionIdOrUrl)")
+ if (isPrivate) {
+ privateDiskCache.removeThumbnailData(context, sessionIdOrUrl)
+ } else {
+ sharedDiskCache.removeThumbnailData(context, sessionIdOrUrl)
+ }
+ }
+
+ /**
+ * Asynchronously loads a thumbnail [Bitmap] for the given [ImageLoadRequest].
+ */
+ fun loadThumbnail(request: ImageLoadRequest): Deferred<Bitmap?> = scope.async {
+ loadThumbnailInternal(request).also { loadedThumbnail ->
+ if (loadedThumbnail != null) {
+ logger.debug(
+ "Loaded thumbnail from disk (id = ${request.id}, " +
+ "generationId = ${loadedThumbnail.generationId})",
+ )
+ } else {
+ logger.debug("No thumbnail loaded (id = ${request.id})")
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun loadThumbnailInternal(request: ImageLoadRequest): Bitmap? {
+ val desiredSize = DesiredSize(
+ targetSize = request.size,
+ minSize = request.size,
+ maxSize = maximumSize,
+ maxScaleFactor = MAXIMUM_SCALE_FACTOR,
+ )
+
+ val data = if (request.isPrivate) {
+ privateDiskCache.getThumbnailData(context, request)
+ } else {
+ sharedDiskCache.getThumbnailData(context, request)
+ }
+
+ if (data != null) {
+ return decoders.decode(data, desiredSize)
+ }
+
+ return null
+ }
+
+ /**
+ * Stores the given thumbnail [Bitmap] into the disk cache with the provided [ImageLoadRequest]
+ * as its key.
+ */
+ fun saveThumbnail(request: ImageSaveRequest, bitmap: Bitmap): Job =
+ scope.launch {
+ logger.debug(
+ "Saved thumbnail to disk (id = $request, " +
+ "generationId = ${bitmap.generationId})",
+ )
+ if (request.isPrivate) {
+ privateDiskCache.putThumbnailBitmap(context, request, bitmap)
+ } else {
+ sharedDiskCache.putThumbnailBitmap(context, request, bitmap)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt
new file mode 100644
index 0000000000..d3e53ae334
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt
@@ -0,0 +1,130 @@
+/* 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.browser.thumbnails.utils
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+import com.jakewharton.disklrucache.DiskLruCache
+import mozilla.components.concept.base.images.ImageLoadRequest
+import mozilla.components.concept.base.images.ImageSaveRequest
+import mozilla.components.support.base.log.logger.Logger
+import java.io.File
+import java.io.IOException
+
+private const val MAXIMUM_CACHE_THUMBNAIL_DATA_BYTES: Long = 1024L * 1024L * 100L // 100 MB
+private const val THUMBNAIL_DISK_CACHE_VERSION = 1
+private const val WEBP_QUALITY = 90
+private const val BASE_DIR_NAME = "thumbnails"
+
+/**
+ * Caching thumbnail bitmaps on disk.
+ *
+ * @property isPrivate whether this cache is intended for private browsing thumbnails
+ */
+class ThumbnailDiskCache(private val isPrivate: Boolean = false) {
+ private val logger = Logger("ThumbnailDiskCache")
+
+ @VisibleForTesting
+ internal var thumbnailCache: DiskLruCache? = null
+ private val thumbnailCacheWriteLock = Any()
+
+ internal fun clear(context: Context) {
+ synchronized(thumbnailCacheWriteLock) {
+ try {
+ getThumbnailCache(context).delete()
+ } catch (e: IOException) {
+ logger.warn("Thumbnail cache could not be cleared. Perhaps there are none?")
+ }
+ thumbnailCache = null
+ }
+ }
+
+ /**
+ * Retrieves the thumbnail data from the disk cache for the given session ID or URL.
+ *
+ * @param context the application [Context].
+ * @param request [ImageLoadRequest] providing the session ID or URL of the thumbnail to retrieve.
+ * @return the [ByteArray] of the thumbnail or null if the snapshot of the entry does not exist.
+ */
+ internal fun getThumbnailData(context: Context, request: ImageLoadRequest): ByteArray? {
+ val snapshot = getThumbnailCache(context).get(request.id) ?: return null
+
+ return try {
+ snapshot.getInputStream(0).use {
+ it.buffered().readBytes()
+ }
+ } catch (e: IOException) {
+ logger.info("Failed to read thumbnail bitmap from disk", e)
+ null
+ }
+ }
+
+ /**
+ * Stores the given session ID or URL's thumbnail [Bitmap] into the disk cache.
+ *
+ * @param context the application [Context].
+ * @param request [ImageSaveRequest] providing the session ID or URL of the thumbnail to retrieve.
+ * @param bitmap the thumbnail [Bitmap] to store.
+ */
+ internal fun putThumbnailBitmap(context: Context, request: ImageSaveRequest, bitmap: Bitmap) {
+ val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Bitmap.CompressFormat.WEBP_LOSSY
+ } else {
+ @Suppress("DEPRECATION")
+ Bitmap.CompressFormat.WEBP
+ }
+
+ try {
+ synchronized(thumbnailCacheWriteLock) {
+ val editor = getThumbnailCache(context)
+ .edit(request.id) ?: return
+
+ editor.newOutputStream(0).use { stream ->
+ bitmap.compress(compressFormat, WEBP_QUALITY, stream)
+ }
+
+ editor.commit()
+ }
+ } catch (e: IOException) {
+ logger.info("Failed to save thumbnail bitmap to disk", e)
+ }
+ }
+
+ /**
+ * Removes the given session ID or URL's thumbnail [Bitmap] from the disk cache.
+ *
+ * @param context the application [Context].
+ * @param sessionIdOrUrl the session ID or URL.
+ */
+ internal fun removeThumbnailData(context: Context, sessionIdOrUrl: String) {
+ try {
+ synchronized(thumbnailCacheWriteLock) {
+ getThumbnailCache(context).remove(sessionIdOrUrl)
+ }
+ } catch (e: IOException) {
+ logger.info("Failed to remove thumbnail bitmap from disk", e)
+ }
+ }
+
+ private fun getThumbnailCacheDirectory(context: Context): File {
+ val dirName = if (isPrivate) "private_$BASE_DIR_NAME" else BASE_DIR_NAME
+ val cacheDirectory = File(context.cacheDir, "mozac_browser_thumbnails")
+ return File(cacheDirectory, dirName)
+ }
+
+ @Synchronized
+ private fun getThumbnailCache(context: Context): DiskLruCache {
+ thumbnailCache?.let { return it }
+
+ return DiskLruCache.open(
+ getThumbnailCacheDirectory(context),
+ THUMBNAIL_DISK_CACHE_VERSION,
+ 1,
+ MAXIMUM_CACHE_THUMBNAIL_DATA_BYTES,
+ ).also { thumbnailCache = it }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/main/res/values/dimens.xml b/mobile/android/android-components/components/browser/thumbnails/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..b6c009463a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Maximum size to save thumbnails at. We want full size thumbnails, so we use a large value -->
+ <dimen name="mozac_browser_thumbnails_maximum_size">99999dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/main/res/values/tags.xml b/mobile/android/android-components/components/browser/thumbnails/src/main/res/values/tags.xml
new file mode 100644
index 0000000000..5152cd5a83
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/main/res/values/tags.xml
@@ -0,0 +1,8 @@
+<?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>
+ <item name="mozac_browser_thumbnails_tag_job" type="id" />
+</resources>
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt
new file mode 100644
index 0000000000..b3bdeb864f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt
@@ -0,0 +1,139 @@
+/* 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.browser.thumbnails
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class BrowserThumbnailsTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var store: BrowserStore
+ private lateinit var engineView: EngineView
+ private lateinit var thumbnails: BrowserThumbnails
+ private val tabId = "test-tab"
+
+ @Before
+ fun setup() {
+ store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = tabId),
+ ),
+ selectedTabId = tabId,
+ ),
+ ),
+ )
+ engineView = mock()
+ thumbnails = BrowserThumbnails(testContext, engineView, store)
+ }
+
+ @Test
+ fun `do not capture thumbnail when feature is stopped and a site finishes loading`() {
+ thumbnails.start()
+ thumbnails.stop()
+
+ store.dispatch(ContentAction.UpdateThumbnailAction(tabId, mock())).joinBlocking()
+
+ verifyNoMoreInteractions(engineView)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun `feature must capture thumbnail when a site finishes loading and first paint`() {
+ val bitmap: Bitmap? = mock()
+
+ store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, true)).joinBlocking()
+
+ thumbnails.start()
+
+ `when`(engineView.captureThumbnail(any()))
+ .thenAnswer { // if engineView responds with a bitmap
+ (it.arguments[0] as (Bitmap?) -> Unit).invoke(bitmap)
+ }
+
+ verify(store, never()).dispatch(ContentAction.UpdateThumbnailAction(tabId, bitmap!!))
+
+ store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, false)).joinBlocking()
+ store.dispatch(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, true)).joinBlocking()
+
+ verify(store).dispatch(ContentAction.UpdateThumbnailAction(tabId, bitmap))
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun `feature never updates the store if there is no thumbnail bitmap`() {
+ val store: BrowserStore = mock()
+ val state: BrowserState = mock()
+ val engineView: EngineView = mock()
+ val feature = BrowserThumbnails(testContext, engineView, store)
+
+ `when`(store.state).thenReturn(state)
+ `when`(engineView.captureThumbnail(any()))
+ .thenAnswer { // if engineView responds with a bitmap
+ (it.arguments[0] as (Bitmap?) -> Unit).invoke(null)
+ }
+
+ feature.requestScreenshot()
+
+ verifyNoInteractions(store)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun `feature never updates the store if there is no tab ID`() {
+ val store: BrowserStore = mock()
+ val state: BrowserState = mock()
+ val engineView: EngineView = mock()
+ val feature = BrowserThumbnails(testContext, engineView, store)
+ val bitmap: Bitmap = mock()
+
+ `when`(store.state).thenReturn(state)
+ `when`(state.selectedTabId).thenReturn(tabId)
+ `when`(engineView.captureThumbnail(any()))
+ .thenAnswer { // if engineView responds with a bitmap
+ (it.arguments[0] as (Bitmap?) -> Unit).invoke(bitmap)
+ }
+
+ feature.requestScreenshot()
+
+ verify(store).dispatch(ContentAction.UpdateThumbnailAction(tabId, bitmap))
+ }
+
+ @Test
+ fun `when a page is loaded and the os is in low memory condition thumbnail should not be captured`() {
+ store.dispatch(ContentAction.UpdateThumbnailAction(tabId, mock())).joinBlocking()
+
+ thumbnails.testLowMemory = true
+
+ thumbnails.start()
+
+ verify(engineView, never()).captureThumbnail(any())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsMiddlewareTest.kt b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsMiddlewareTest.kt
new file mode 100644
index 0000000000..424e3cabae
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsMiddlewareTest.kt
@@ -0,0 +1,199 @@
+/* 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.browser.thumbnails
+
+import android.graphics.Bitmap
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
+import mozilla.components.concept.base.images.ImageSaveRequest
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class ThumbnailsMiddlewareTest {
+
+ @Test
+ fun `thumbnail storage stores the provided thumbnail on update thumbnail action`() {
+ val request = ImageSaveRequest("test-tab1", false)
+ val tab = createTab("https://www.mozilla.org", id = "test-tab1")
+ val thumbnailStorage: ThumbnailStorage = mock()
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)),
+ )
+
+ val bitmap: Bitmap = mock()
+ store.dispatch(ContentAction.UpdateThumbnailAction(request.id, bitmap)).joinBlocking()
+ verify(thumbnailStorage).saveThumbnail(request, bitmap)
+ }
+
+ @Test
+ fun `WHEN update thumbnail action called with private tab THEN storage stores provided thumbnail`() {
+ val request = ImageSaveRequest("test-tab1", true)
+ val tab = createTab("https://www.mozilla.org", id = "test-tab1", private = true)
+ val thumbnailStorage: ThumbnailStorage = mock()
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)),
+ )
+
+ val bitmap: Bitmap = mock()
+ store.dispatch(ContentAction.UpdateThumbnailAction(request.id, bitmap)).joinBlocking()
+ verify(thumbnailStorage).saveThumbnail(request, bitmap)
+ }
+
+ @Test
+ fun `thumbnail storage removes the thumbnail on remove all normal tabs action`() {
+ val thumbnailStorage: ThumbnailStorage = mock()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ createTab("https://www.wikipedia.com", id = "test-tab3"),
+ createTab("https://www.example.org", private = true, id = "test-tab4"),
+ ),
+ ),
+ middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)),
+ )
+
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+ verify(thumbnailStorage).deleteThumbnail("test-tab1", false)
+ verify(thumbnailStorage).deleteThumbnail("test-tab2", false)
+ verify(thumbnailStorage).deleteThumbnail("test-tab3", false)
+ verify(thumbnailStorage, never()).deleteThumbnail("test-tab4", true)
+ }
+
+ @Test
+ fun `thumbnail storage removes the thumbnail on remove all private tabs action`() {
+ val thumbnailStorage: ThumbnailStorage = mock()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", private = true, id = "test-tab2"),
+ createTab("https://www.wikipedia.com", private = true, id = "test-tab3"),
+ createTab("https://www.example.org", private = true, id = "test-tab4"),
+ ),
+ ),
+ middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)),
+ )
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+ verify(thumbnailStorage, never()).deleteThumbnail("test-tab1", false)
+ verify(thumbnailStorage).deleteThumbnail("test-tab2", true)
+ verify(thumbnailStorage).deleteThumbnail("test-tab3", true)
+ verify(thumbnailStorage).deleteThumbnail("test-tab4", true)
+ }
+
+ @Test
+ fun `thumbnail storage removes the thumbnail on remove all tabs action`() {
+ val thumbnailStorage: ThumbnailStorage = mock()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ ),
+ ),
+ middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)),
+ )
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+ verify(thumbnailStorage).clearThumbnails()
+ }
+
+ @Test
+ fun `thumbnail storage removes the thumbnail on remove tab action`() {
+ val sessionIdOrUrl = "test-tab1"
+ val thumbnailStorage: ThumbnailStorage = mock()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ ),
+ ),
+ middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)),
+ )
+
+ store.dispatch(TabListAction.RemoveTabAction(sessionIdOrUrl)).joinBlocking()
+ verify(thumbnailStorage).deleteThumbnail(sessionIdOrUrl, false)
+ }
+
+ @Test
+ fun `WHEN remove tab action with private tab THEN thumbnail storage removes the thumbnail`() {
+ val sessionIdOrUrl = "test-tab1"
+ val thumbnailStorage: ThumbnailStorage = mock()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1", private = true),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ ),
+ ),
+ middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)),
+ )
+
+ store.dispatch(TabListAction.RemoveTabAction(sessionIdOrUrl)).joinBlocking()
+ verify(thumbnailStorage).deleteThumbnail(sessionIdOrUrl, true)
+ }
+
+ @Test
+ fun `thumbnail storage removes the thumbnail on remove tabs action`() {
+ val sessionIdOrUrl = "test-tab1"
+ val thumbnailStorage: ThumbnailStorage = mock()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ ),
+ ),
+ middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)),
+ )
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf(sessionIdOrUrl))).joinBlocking()
+ verify(thumbnailStorage).deleteThumbnail(sessionIdOrUrl, false)
+ }
+
+ @Test
+ fun `thumbnail actions are the only ones consumed by the middleware`() {
+ val capture = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ ),
+ ),
+ middleware = listOf(
+ ThumbnailsMiddleware(mock()),
+ capture,
+ ),
+ )
+
+ store.dispatch(ContentAction.UpdateThumbnailAction("test-tab1", mock())).joinBlocking()
+ store.dispatch(TabListAction.RemoveTabAction("test-tab1")).joinBlocking()
+
+ // We shouldn't allow thumbnail actions to continue being processed.
+ capture.assertNotDispatched(ContentAction.UpdateThumbnailAction::class)
+ // TabListActions that we also observe in the middleware _should_ continue being processed.
+ capture.assertLastAction(TabListAction.RemoveTabAction::class) {}
+
+ // All other actions should also continue being processed.
+ store.dispatch(EngineAction.KillEngineSessionAction("test-tab1")).joinBlocking()
+ capture.assertLastAction(EngineAction.KillEngineSessionAction::class) {}
+ }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt
new file mode 100644
index 0000000000..397ff63e76
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt
@@ -0,0 +1,93 @@
+/* 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.browser.thumbnails.loader
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.widget.ImageView
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Job
+import mozilla.components.browser.thumbnails.R
+import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
+import mozilla.components.concept.base.images.ImageLoadRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+class ThumbnailLoaderTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `automatically load thumbnails into image view`() {
+ val mockedBitmap: Bitmap = mock()
+ val result = CompletableDeferred<Bitmap>()
+ val view: ImageView = mock()
+ val storage: ThumbnailStorage = mock()
+ val loader = spy(ThumbnailLoader(storage))
+ val request = ImageLoadRequest("123", 100, false)
+
+ doReturn(result).`when`(storage).loadThumbnail(request)
+
+ loader.loadIntoView(view, request)
+
+ verify(view).addOnAttachStateChangeListener(any())
+ verify(view).setTag(eq(R.id.mozac_browser_thumbnails_tag_job), any())
+ verify(view, never()).setImageBitmap(any())
+
+ result.complete(mockedBitmap)
+
+ verify(view).setImageBitmap(mockedBitmap)
+ verify(view).removeOnAttachStateChangeListener(any())
+ verify(view).setTag(R.id.mozac_browser_thumbnails_tag_job, null)
+ }
+
+ @Test
+ fun `loadIntoView sets drawable to error if cancelled`() {
+ val result = CompletableDeferred<Bitmap>()
+ val view: ImageView = mock()
+ val placeholder: Drawable = mock()
+ val error: Drawable = mock()
+ val storage: ThumbnailStorage = mock()
+ val loader = spy(ThumbnailLoader(storage))
+ val request = ImageLoadRequest("123", 100, false)
+
+ doReturn(result).`when`(storage).loadThumbnail(request)
+
+ loader.loadIntoView(view, request, placeholder = placeholder, error = error)
+
+ result.cancel()
+
+ verify(view).setImageDrawable(error)
+ verify(view).removeOnAttachStateChangeListener(any())
+ verify(view).setTag(R.id.mozac_browser_thumbnails_tag_job, null)
+ }
+
+ @Test
+ fun `loadIntoView cancels previous jobs`() {
+ val result = CompletableDeferred<Bitmap>()
+ val view: ImageView = mock()
+ val previousJob: Job = mock()
+ val storage: ThumbnailStorage = mock()
+ val loader = spy(ThumbnailLoader(storage))
+ val request = ImageLoadRequest("123", 100, false)
+
+ doReturn(previousJob).`when`(view).getTag(R.id.mozac_browser_thumbnails_tag_job)
+ doReturn(result).`when`(storage).loadThumbnail(request)
+
+ loader.loadIntoView(view, request)
+
+ verify(previousJob).cancel()
+
+ result.cancel()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt
new file mode 100644
index 0000000000..53bd03208c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt
@@ -0,0 +1,142 @@
+/* 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.browser.thumbnails.storage
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CompletableDeferred
+import mozilla.components.concept.base.images.ImageLoadRequest
+import mozilla.components.concept.base.images.ImageSaveRequest
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class ThumbnailStorageTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val testDispatcher = coroutinesTestRule.testDispatcher
+
+ @Before
+ @After
+ fun cleanUp() {
+ sharedDiskCache.clear(testContext)
+ privateDiskCache.clear(testContext)
+ }
+
+ @Test
+ fun `clearThumbnails`() = runTestOnMain {
+ val bitmap: Bitmap = mock()
+ val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))
+
+ thumbnailStorage.saveThumbnail(ImageSaveRequest("test-tab1", false), bitmap).joinBlocking()
+ thumbnailStorage.saveThumbnail(ImageSaveRequest("test-tab2", false), bitmap).joinBlocking()
+ var thumbnail1 = thumbnailStorage.loadThumbnail(ImageLoadRequest("test-tab1", 100, false)).await()
+ var thumbnail2 = thumbnailStorage.loadThumbnail(ImageLoadRequest("test-tab2", 100, false)).await()
+ assertNotNull(thumbnail1)
+ assertNotNull(thumbnail2)
+
+ thumbnailStorage.clearThumbnails()
+ thumbnail1 = thumbnailStorage.loadThumbnail(ImageLoadRequest("test-tab1", 100, false)).await()
+ thumbnail2 = thumbnailStorage.loadThumbnail(ImageLoadRequest("test-tab2", 100, false)).await()
+ assertNull(thumbnail1)
+ assertNull(thumbnail2)
+ }
+
+ @Test
+ fun `deleteThumbnail`() = runTestOnMain {
+ val request = ImageSaveRequest("test-tab1", false)
+ val bitmap: Bitmap = mock()
+ val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))
+
+ thumbnailStorage.saveThumbnail(request, bitmap).joinBlocking()
+ var thumbnail = thumbnailStorage.loadThumbnail(ImageLoadRequest(request.id, 100, request.isPrivate)).await()
+ assertNotNull(thumbnail)
+
+ thumbnailStorage.deleteThumbnail(request.id, request.isPrivate).joinBlocking()
+ thumbnail = thumbnailStorage.loadThumbnail(ImageLoadRequest(request.id, 100, request.isPrivate)).await()
+ assertNull(thumbnail)
+ }
+
+ @Test
+ fun `saveThumbnail`() = runTestOnMain {
+ val request = ImageLoadRequest("test-tab1", 100, false)
+ val bitmap: Bitmap = mock()
+ val thumbnailStorage = spy(ThumbnailStorage(testContext))
+ var thumbnail = thumbnailStorage.loadThumbnail(request).await()
+
+ assertNull(thumbnail)
+
+ thumbnailStorage.saveThumbnail(ImageSaveRequest(request.id, request.isPrivate), bitmap).joinBlocking()
+ thumbnail = thumbnailStorage.loadThumbnail(request).await()
+ assertNotNull(thumbnail)
+ }
+
+ @Test
+ fun `WHEN private save request THEN placed in private cache`() = runTestOnMain {
+ val request = ImageLoadRequest("test-tab1", 100, true)
+ val bitmap: Bitmap = mock()
+ val thumbnailStorage = spy(ThumbnailStorage(testContext))
+ var thumbnail = thumbnailStorage.loadThumbnail(request).await()
+
+ assertNull(thumbnail)
+
+ thumbnailStorage.saveThumbnail(ImageSaveRequest(request.id, request.isPrivate), bitmap).joinBlocking()
+ thumbnail = thumbnailStorage.loadThumbnail(request).await()
+ assertNotNull(thumbnail)
+ }
+
+ @Test
+ fun `loadThumbnail`() = runTestOnMain {
+ val request = ImageLoadRequest("test-tab1", 100, false)
+ val bitmap: Bitmap = mock()
+ val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))
+
+ thumbnailStorage.saveThumbnail(ImageSaveRequest(request.id, request.isPrivate), bitmap)
+ `when`(thumbnailStorage.loadThumbnail(request)).thenReturn(CompletableDeferred(bitmap))
+
+ val thumbnail = thumbnailStorage.loadThumbnail(request).await()
+ assertEquals(bitmap, thumbnail)
+ }
+
+ @Test
+ fun `WHEN private load request THEN loaded from private cache`() = runTestOnMain {
+ val request = ImageLoadRequest("test-tab1", 100, true)
+ val bitmap: Bitmap = mock()
+ val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))
+
+ thumbnailStorage.saveThumbnail(ImageSaveRequest(request.id, request.isPrivate), bitmap)
+ `when`(thumbnailStorage.loadThumbnail(request)).thenReturn(CompletableDeferred(bitmap))
+
+ val thumbnail = thumbnailStorage.loadThumbnail(request).await()
+ assertEquals(bitmap, thumbnail)
+ assertNull(thumbnailStorage.loadThumbnail(ImageLoadRequest(request.id, request.size, false)).await())
+ }
+
+ @Test
+ fun `WHEN storage is initialized THEN private cache is cleared`() {
+ val request = ImageLoadRequest("test-tab1", 100, true)
+ val bitmap: Bitmap = mock()
+
+ privateDiskCache.putThumbnailBitmap(testContext, ImageSaveRequest(request.id, request.isPrivate), bitmap)
+ assertNotNull(privateDiskCache.getThumbnailData(testContext, request))
+ ThumbnailStorage(testContext, testDispatcher)
+
+ assertNull(privateDiskCache.getThumbnailData(testContext, request))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt
new file mode 100644
index 0000000000..d80f18af43
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt
@@ -0,0 +1,148 @@
+/* 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.browser.thumbnails.utils
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.jakewharton.disklrucache.DiskLruCache
+import mozilla.components.concept.base.images.ImageLoadRequest
+import mozilla.components.concept.base.images.ImageSaveRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.`when`
+import org.robolectric.annotation.Config
+import java.io.IOException
+import java.io.OutputStream
+
+@RunWith(AndroidJUnit4::class)
+class ThumbnailDiskCacheTest {
+
+ @Test
+ fun `Writing and reading bitmap bytes for sdk higher than 29`() {
+ val cache = ThumbnailDiskCache()
+ val request = ImageLoadRequest("123", 100, false)
+
+ val bitmap: Bitmap = mock()
+ `when`(bitmap.compress(any(), ArgumentMatchers.anyInt(), any())).thenAnswer {
+ Assert.assertEquals(
+ Bitmap.CompressFormat.WEBP_LOSSY,
+ it.arguments[0] as Bitmap.CompressFormat,
+ )
+ Assert.assertEquals(90, it.arguments[1] as Int) // Quality
+
+ val stream = it.arguments[2] as OutputStream
+ stream.write("Hello World".toByteArray())
+ true
+ }
+
+ cache.putThumbnailBitmap(testContext, ImageSaveRequest(request.id, request.isPrivate), bitmap)
+
+ val data = cache.getThumbnailData(testContext, request)
+ assertNotNull(data!!)
+ Assert.assertEquals("Hello World", String(data))
+ }
+
+ @Test
+ fun `Writing and reading bitmap bytes for private cache`() {
+ val cache = ThumbnailDiskCache(isPrivate = true)
+ val request = ImageLoadRequest("123", 100, true)
+
+ val bitmap: Bitmap = mock()
+ `when`(bitmap.compress(any(), ArgumentMatchers.anyInt(), any())).thenAnswer {
+ Assert.assertEquals(
+ Bitmap.CompressFormat.WEBP_LOSSY,
+ it.arguments[0] as Bitmap.CompressFormat,
+ )
+ Assert.assertEquals(90, it.arguments[1] as Int) // Quality
+
+ val stream = it.arguments[2] as OutputStream
+ stream.write("Hello World".toByteArray())
+ true
+ }
+
+ cache.putThumbnailBitmap(testContext, ImageSaveRequest(request.id, request.isPrivate), bitmap)
+
+ val data = cache.getThumbnailData(testContext, request)
+ assertNotNull(data!!)
+ Assert.assertEquals("Hello World", String(data))
+ }
+
+ @Config(sdk = [29])
+ @Test
+ fun `Writing and reading bitmap bytes for sdk lower or equal to 29`() {
+ val cache = ThumbnailDiskCache()
+ val request = ImageLoadRequest("123", 100, false)
+
+ val bitmap: Bitmap = mock()
+ `when`(bitmap.compress(any(), ArgumentMatchers.anyInt(), any())).thenAnswer {
+ Assert.assertEquals(
+ @Suppress("DEPRECATION") // not deprecated in sdk 29
+ Bitmap.CompressFormat.WEBP,
+ it.arguments[0] as Bitmap.CompressFormat,
+ )
+ Assert.assertEquals(90, it.arguments[1] as Int) // Quality
+
+ val stream = it.arguments[2] as OutputStream
+ stream.write("Hello World".toByteArray())
+ true
+ }
+
+ cache.putThumbnailBitmap(testContext, ImageSaveRequest(request.id, request.isPrivate), bitmap)
+
+ val data = cache.getThumbnailData(testContext, request)
+ assertNotNull(data!!)
+ Assert.assertEquals("Hello World", String(data))
+ }
+
+ @Test
+ fun `Removing bitmap from disk cache`() {
+ val cache = ThumbnailDiskCache()
+ val request = ImageLoadRequest("123", 100, false)
+ val bitmap: Bitmap = mock()
+
+ cache.putThumbnailBitmap(testContext, ImageSaveRequest(request.id, request.isPrivate), bitmap)
+ var data = cache.getThumbnailData(testContext, request)
+ assertNotNull(data!!)
+
+ cache.removeThumbnailData(testContext, request.id)
+ data = cache.getThumbnailData(testContext, request)
+ assertNull(data)
+ }
+
+ @Test
+ fun `Clearing bitmap from disk cache`() {
+ val cache = ThumbnailDiskCache()
+ val request = ImageLoadRequest("123", 100, false)
+ val bitmap: Bitmap = mock()
+
+ cache.putThumbnailBitmap(testContext, ImageSaveRequest(request.id, request.isPrivate), bitmap)
+ var data = cache.getThumbnailData(testContext, request)
+ assertNotNull(data!!)
+
+ cache.clear(testContext)
+ data = cache.getThumbnailData(testContext, request)
+ assertNull(data)
+ }
+
+ @Test
+ fun `Clearing bitmap from disk catch IOException`() {
+ val cache = ThumbnailDiskCache()
+ val lruCache: DiskLruCache = mock()
+ cache.thumbnailCache = lruCache
+
+ `when`(lruCache.delete()).thenThrow(IOException("test"))
+
+ cache.clear(testContext)
+
+ assertNull(cache.thumbnailCache)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/thumbnails/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/thumbnails/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/thumbnails/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/browser/toolbar/README.md b/mobile/android/android-components/components/browser/toolbar/README.md
new file mode 100644
index 0000000000..1dcd5a115b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/README.md
@@ -0,0 +1,37 @@
+# [Android Components](../../../README.md) > Browser > Toolbar
+
+A customizable toolbar for browsers.
+
+## Usage
+
+### 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:browser-toolbar:{latest-version}"
+```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Extras | Description |
+|--------|---------|----------------|------------------------------------|
+| CLICK | menu | `menuExtras` | The user opened the overflow menu. |
+| COMMIT | toolbar | `commitExtras` | The user has edited the URL. |
+
+`menuExtras` are additional extras set on the `BrowserMenuBuilder` passed to the `BrowserToolbar` (see [browser-menu](../menu/README.md)).
+
+#### `commitExtras`
+
+| Key | Type | Value |
+|--------------|---------|-----------------------------------|
+| autocomplete | Boolean | Whether the URL was autocompleted |
+| source | String? | Which autocomplete list was used |
+
+## 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/browser/toolbar/build.gradle b/mobile/android/android-components/components/browser/toolbar/build.gradle
new file mode 100644
index 0000000000..d18b1ebd48
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/build.gradle
@@ -0,0 +1,56 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.browser.toolbar'
+}
+
+dependencies {
+ api project(':concept-toolbar')
+ api project(':ui-autocomplete')
+ api project(':support-base')
+
+ implementation project(':concept-engine')
+ implementation project(':concept-menu')
+ implementation project(':browser-menu')
+ implementation project(':browser-menu2')
+ implementation project(':ui-icons')
+ implementation project(':ui-colors')
+ implementation project(':ui-widgets')
+ implementation project(':support-ktx')
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.google_material
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/browser/toolbar/proguard-rules.pro b/mobile/android/android-components/components/browser/toolbar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/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/browser/toolbar/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/toolbar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/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/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt
new file mode 100644
index 0000000000..9251a5752c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt
@@ -0,0 +1,691 @@
+/* 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.browser.toolbar
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.NO_ID
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.ContextCompat
+import androidx.core.view.forEach
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import mozilla.components.browser.toolbar.display.DisplayToolbar
+import mozilla.components.browser.toolbar.edit.EditToolbar
+import mozilla.components.concept.toolbar.AutocompleteDelegate
+import mozilla.components.concept.toolbar.AutocompleteResult
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.concept.toolbar.Toolbar.Highlight
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.trimmed
+import mozilla.components.ui.autocomplete.AutocompleteView
+import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
+import mozilla.components.ui.autocomplete.OnFilterListener
+import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior
+import kotlin.coroutines.CoroutineContext
+
+internal fun ImageView.setTintResource(@ColorRes tintColorResource: Int) {
+ if (tintColorResource != NO_ID) {
+ imageTintList = ContextCompat.getColorStateList(context, tintColorResource)
+ }
+}
+
+/**
+ * A customizable toolbar for browsers.
+ *
+ * The toolbar can switch between two modes: display and edit. The display mode displays the current
+ * URL and controls for navigation. In edit mode the current URL can be edited. Those two modes are
+ * implemented by the DisplayToolbar and EditToolbar classes.
+ *
+ * ```
+ * +----------------+
+ * | BrowserToolbar |
+ * +--------+-------+
+ * +
+ * +-------+-------+
+ * | |
+ * +---------v------+ +-------v--------+
+ * | DisplayToolbar | | EditToolbar |
+ * +----------------+ +----------------+
+ * ```
+ */
+@Suppress("TooManyFunctions")
+class BrowserToolbar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ViewGroup(context, attrs, defStyleAttr), Toolbar {
+ private var state: State = State.DISPLAY
+
+ @VisibleForTesting
+ internal var searchTerms: String = ""
+ private var urlCommitListener: ((String) -> Boolean)? = null
+ var isNavBarEnabled: Boolean = false
+
+ /**
+ * Toolbar in "display mode".
+ */
+ var display = DisplayToolbar(
+ context,
+ this,
+ LayoutInflater.from(context).inflate(
+ R.layout.mozac_browser_toolbar_displaytoolbar,
+ this,
+ false,
+ ),
+ )
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal set
+
+ /**
+ * Toolbar in "edit mode".
+ */
+ var edit = EditToolbar(
+ context,
+ this,
+ LayoutInflater.from(context).inflate(
+ R.layout.mozac_browser_toolbar_edittoolbar,
+ this,
+ false,
+ ),
+ )
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal set
+
+ override var title: String
+ get() = display.title
+ set(value) { display.title = value }
+
+ override var url: CharSequence
+ get() = display.url.toString()
+ set(value) {
+ // We update the display toolbar immediately. We do not do that for the edit toolbar to not
+ // mess with what the user is entering. Instead we will remember the value and update the
+ // edit toolbar whenever we switch to it.
+ display.url = (value as String).trimmed()
+ }
+
+ override var siteSecure: Toolbar.SiteSecurity
+ get() = display.siteSecurity
+ set(value) { display.siteSecurity = value }
+
+ override var highlight: Highlight = Highlight.NONE
+ set(value) {
+ if (field != value) {
+ display.setHighlight(value)
+ field = value
+ }
+ }
+
+ override var siteTrackingProtection: Toolbar.SiteTrackingProtection =
+ Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+ set(value) {
+ if (field != value) {
+ display.setTrackingProtectionState(value)
+ field = value
+ }
+ }
+
+ override var private: Boolean
+ get() = edit.private
+ set(value) { edit.private = value }
+
+ /**
+ * Registers the given listener to be invoked when the user edits the URL.
+ */
+ override fun setOnEditListener(listener: Toolbar.OnEditListener) {
+ edit.editListener = listener
+ }
+
+ /**
+ * Registers the given function to be invoked when users changes text in the toolbar.
+ *
+ * @param filter A function which will perform autocompletion and send results to [AutocompleteDelegate].
+ */
+ override fun setAutocompleteListener(filter: suspend (String, AutocompleteDelegate) -> Unit) {
+ // Our 'filter' knows how to autocomplete, and the 'urlView' knows how to apply results of
+ // autocompletion. Which gives us a lovely delegate chain!
+ // urlView decides when it's appropriate to ask for autocompletion, and in turn we invoke
+ // our 'filter' and send results back to 'urlView'.
+ edit.setAutocompleteListener(filter)
+ }
+
+ override fun refreshAutocomplete() {
+ edit.refreshAutocompleteSuggestion()
+ }
+
+ init {
+ addView(display.rootView)
+ addView(edit.rootView)
+
+ updateState(State.DISPLAY)
+ }
+
+ // We layout the toolbar ourselves to avoid the overhead from using complex ViewGroup implementations
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ forEach { child ->
+ child.layout(
+ 0 + paddingLeft,
+ 0 + paddingTop,
+ paddingLeft + child.measuredWidth,
+ paddingTop + child.measuredHeight,
+ )
+ }
+ }
+
+ // We measure the views manually to avoid overhead by using complex ViewGroup implementations
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ // Our toolbar will always use the full width and a fixed height (default) or the provided
+ // height if it's an exact value.
+ val width = MeasureSpec.getSize(widthMeasureSpec)
+ val height = if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
+ MeasureSpec.getSize(heightMeasureSpec)
+ } else {
+ resources.getDimensionPixelSize(R.dimen.mozac_browser_toolbar_default_toolbar_height)
+ }
+
+ setMeasuredDimension(width, height)
+
+ // Let the children measure themselves using our fixed size (with padding subtracted)
+ val childWidth = width - paddingLeft - paddingRight
+ val childHeight = height - paddingTop - paddingBottom
+
+ val childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY)
+ val childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
+
+ forEach { child -> child.measure(childWidthSpec, childHeightSpec) }
+ }
+
+ override fun onBackPressed(): Boolean {
+ if (state == State.EDIT) {
+ displayMode()
+ return true
+ }
+ return false
+ }
+
+ override fun onStop() {
+ display.onStop()
+ }
+
+ override fun setSearchTerms(searchTerms: String) {
+ this.searchTerms = searchTerms.trimmed()
+
+ if (state == State.EDIT) {
+ edit.editSuggestion(this.searchTerms)
+ }
+ }
+
+ override fun displayProgress(progress: Int) {
+ display.updateProgress(progress)
+ }
+
+ override fun setOnUrlCommitListener(listener: (String) -> Boolean) {
+ this.urlCommitListener = listener
+ }
+
+ /**
+ * Declare that the actions (navigation actions, browser actions, page actions) have changed and
+ * should be updated if needed.
+ *
+ * The toolbar will call the <code>visible</code> lambda of every action to determine whether a
+ * view for this action should be added or removed. Additionally <code>bind</code> will be
+ * called on every visible action to update its view.
+ */
+ override fun invalidateActions() {
+ display.invalidateActions()
+ edit.invalidateActions()
+ }
+
+ /**
+ * Adds an action to be displayed on the right side of the toolbar (outside of the URL bounding
+ * box) in display mode.
+ *
+ * If there is not enough room to show all icons then some icons may be moved to an overflow
+ * menu.
+ *
+ * Related:
+ * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Browser_action
+ */
+ override fun addBrowserAction(action: Toolbar.Action) {
+ display.addBrowserAction(action)
+ }
+
+ /**
+ * Removes a previously added browser action (see [addBrowserAction]). If the provided
+ * action was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ override fun removeBrowserAction(action: Toolbar.Action) {
+ display.removeBrowserAction(action)
+ }
+
+ /**
+ * Removes a previously added page action (see [addPageAction]). If the provided
+ * action was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ override fun removePageAction(action: Toolbar.Action) {
+ display.removePageAction(action)
+ }
+
+ /**
+ * Adds an action to be displayed on the right side of the URL in display mode.
+ *
+ * Related:
+ * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Page_actions
+ */
+ override fun addPageAction(action: Toolbar.Action) {
+ display.addPageAction(action)
+ }
+
+ /**
+ * Adds an action to be display on the far left side of the toolbar. This area is usually used
+ * on larger devices for navigation actions like "back" and "forward".
+ */
+ override fun addNavigationAction(action: Toolbar.Action) {
+ display.addNavigationAction(action)
+ }
+
+ /**
+ * Removes a previously added navigation action (see [addNavigationAction]). If the provided
+ * action was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ override fun removeNavigationAction(action: Toolbar.Action) {
+ display.removeNavigationAction(action)
+ }
+
+ /**
+ * Adds an action to be displayed at the start of the URL in edit mode.
+ */
+ override fun addEditActionStart(action: Toolbar.Action) {
+ edit.addEditActionStart(action)
+ }
+
+ /**
+ * Adds an action to be displayed at the end of the URL in edit mode.
+ */
+ override fun addEditActionEnd(action: Toolbar.Action) {
+ edit.addEditActionEnd(action)
+ }
+
+ /**
+ * Removes an action end of the URL in edit mode.
+ */
+ override fun removeEditActionEnd(action: Toolbar.Action) {
+ edit.removeEditActionEnd(action)
+ }
+
+ /**
+ * Hides the menu button in display mode.
+ */
+ override fun hideMenuButton() {
+ display.hideMenuButton()
+ }
+
+ /**
+ * Shows the menu button in display mode.
+ */
+ override fun showMenuButton() {
+ display.showMenuButton()
+ }
+
+ /**
+ * Sets the horizontal padding in display mode.
+ */
+ override fun setDisplayHorizontalPadding(horizontalPadding: Int) {
+ display.setHorizontalPadding(horizontalPadding)
+ }
+
+ /**
+ * Hides the page action separator in display/edit mode.
+ */
+ override fun hidePageActionSeparator() {
+ display.hidePageActionSeparator()
+ edit.hidePageActionSeparator()
+ }
+
+ /**
+ * Shows the page action separator in display/edit mode.
+ */
+ override fun showPageActionSeparator() {
+ display.showPageActionSeparator()
+ edit.showPageActionSeparator()
+ }
+
+ /**
+ * Switches to URL editing mode.
+ *
+ * @param cursorPlacement Where the cursor should be placed after focusing on the URL input field.
+ */
+ override fun editMode(cursorPlacement: Toolbar.CursorPlacement) {
+ val urlValue = if (searchTerms.isEmpty()) url else searchTerms
+ edit.updateUrl(urlValue.toString(), false)
+ updateState(State.EDIT)
+ edit.focus()
+
+ when (cursorPlacement) {
+ Toolbar.CursorPlacement.ALL -> {
+ edit.selectAll()
+ }
+ Toolbar.CursorPlacement.END -> {
+ edit.selectEnd()
+ }
+ }
+ }
+
+ /**
+ * Switches to URL displaying mode.
+ */
+ override fun displayMode() {
+ updateState(State.DISPLAY)
+ }
+
+ /**
+ * Dismisses the display toolbar popup menu.
+ */
+ override fun dismissMenu() {
+ display.views.menu.dismissMenu()
+ }
+
+ override fun enableScrolling() {
+ // Behavior can be changed without us knowing. Not safe to use a memoized value.
+ (layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
+ (behavior as? EngineViewScrollingBehavior)?.enableScrolling()
+ }
+ }
+
+ override fun disableScrolling() {
+ // Behavior can be changed without us knowing. Not safe to use a memoized value.
+ (layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
+ (behavior as? EngineViewScrollingBehavior)?.disableScrolling()
+ }
+ }
+
+ override fun expand() {
+ (layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
+ (behavior as? EngineViewScrollingBehavior)?.forceExpand(this@BrowserToolbar)
+ }
+ }
+
+ override fun collapse() {
+ (layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
+ (behavior as? EngineViewScrollingBehavior)?.forceCollapse(this@BrowserToolbar)
+ }
+ }
+
+ internal fun onUrlEntered(url: String) {
+ if (urlCommitListener?.invoke(url) != false) {
+ // Return to display mode if there's no urlCommitListener or if it returned true. This lets
+ // the app control whether we should switch to display mode automatically.
+ displayMode()
+ }
+ }
+
+ private fun updateState(state: State) {
+ this.state = state
+
+ val (show, hide) = when (state) {
+ State.DISPLAY -> {
+ edit.stopEditing()
+ Pair(display.rootView, edit.rootView)
+ }
+ State.EDIT -> {
+ edit.startEditing()
+ Pair(edit.rootView, display.rootView)
+ }
+ }
+
+ show.visibility = View.VISIBLE
+ hide.visibility = View.GONE
+ }
+
+ private enum class State {
+ DISPLAY,
+ EDIT,
+ }
+
+ /**
+ * An action button to be added to the toolbar.
+ *
+ * @param imageDrawable The drawable to be shown.
+ * @param contentDescription The content description to use.
+ * @param visible Lambda that returns true or false to indicate whether this button should be shown.
+ * @param autoHide Lambda that returns true or false to indicate whether this button should auto hide.
+ * @param weight Lambda that returns an integer to indicate weight of an action. The lesser the weight,
+ * the closer it is to the url. A default weight -1 indicates, the position is not cared for
+ * and action will be appended at the end.
+ * @param background A custom (stateful) background drawable resource to be used.
+ * @param padding a custom [Padding] for this Button.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ * @param longClickListener Callback that will be invoked whenever the button is long-pressed.
+ * @param listener Callback that will be invoked whenever the button is pressed
+ */
+ open class Button(
+ imageDrawable: Drawable,
+ contentDescription: String,
+ visible: () -> Boolean = { true },
+ autoHide: () -> Boolean = { false },
+ weight: () -> Int = { -1 },
+ @DrawableRes background: Int = 0,
+ val padding: Padding = DEFAULT_PADDING,
+ @ColorRes iconTintColorResource: Int = NO_ID,
+ longClickListener: (() -> Unit)? = null,
+ listener: () -> Unit,
+ ) : Toolbar.ActionButton(
+ imageDrawable,
+ contentDescription,
+ visible,
+ autoHide,
+ weight,
+ background,
+ padding,
+ iconTintColorResource,
+ longClickListener,
+ listener,
+ )
+
+ /**
+ * An action button with two states, selected and unselected. When the button is pressed, the
+ * state changes automatically.
+ *
+ * @param image The drawable to be shown if the button is in unselected state.
+ * @param imageSelected The drawable to be shown if the button is in selected state.
+ * @param contentDescription The content description to use if the button is in unselected state.
+ * @param contentDescriptionSelected The content description to use if the button is in selected state.
+ * @param visible Lambda that returns true or false to indicate whether this button should be shown.
+ * @param weight Lambda that returns an integer to indicate weight of an action. The lesser the weight,
+ * the closer it is to the url. A default weight -1 indicates, the position is not cared for
+ * and action will be appended at the end.
+ * @param selected Sets whether this button should be selected initially.
+ * @param background A custom (stateful) background drawable resource to be used.
+ * @param padding a custom [Padding] for this Button.
+ * @param listener Callback that will be invoked whenever the checked state changes.
+ */
+ open class ToggleButton(
+ image: Drawable,
+ imageSelected: Drawable,
+ contentDescription: String,
+ contentDescriptionSelected: String,
+ visible: () -> Boolean = { true },
+ weight: () -> Int = { -1 },
+ selected: Boolean = false,
+ @DrawableRes background: Int = 0,
+ val padding: Padding = DEFAULT_PADDING,
+ listener: (Boolean) -> Unit,
+ ) : Toolbar.ActionToggleButton(
+ image,
+ imageSelected,
+ contentDescription,
+ contentDescriptionSelected,
+ visible,
+ weight,
+ selected,
+ background,
+ padding,
+ listener,
+ )
+
+ /**
+ * An action that either shows an active button or an inactive button based on the provided
+ * <code>isInPrimaryState</code> lambda. All secondary characteristics default to their
+ * corresponding primary.
+ *
+ * @param primaryImage: The drawable to be shown if the button is in the primary/enabled state
+ * @param primaryContentDescription: The content description to use if the button is in the primary state.
+ * @param secondaryImage: The drawable to be shown if the button is in the secondary/disabled state.
+ * @param secondaryContentDescription: The content description to use if the button is in the secondary state.
+ * @param isInPrimaryState: Lambda that returns whether this button should be in the primary or secondary state.
+ * @param primaryImageTintResource: Optional ID of color resource to tint the icon in the primary state.
+ * @param secondaryImageTintResource: ID of color resource to tint the icon in the secondary state.
+ * @param disableInSecondaryState: Disable the button entirely when in the secondary state?
+ * @param weight Lambda that returns an integer to indicate weight of an action. The lesser the weight,
+ * the closer it is to the url. A default weight -1 indicates, the position is not cared for
+ * and action will be appended at the end.
+ * @param background A custom (stateful) background drawable resource to be used.
+ * @param longClickListener Callback that will be invoked whenever the button is long-pressed.
+ * @param listener Callback that will be invoked whenever the button is pressed.
+ */
+ open class TwoStateButton(
+ val primaryImage: Drawable,
+ val primaryContentDescription: String,
+ val secondaryImage: Drawable = primaryImage,
+ val secondaryContentDescription: String = primaryContentDescription,
+ val isInPrimaryState: () -> Boolean = { true },
+ @ColorRes val primaryImageTintResource: Int = NO_ID,
+ @ColorRes val secondaryImageTintResource: Int = primaryImageTintResource,
+ val disableInSecondaryState: Boolean = true,
+ override val weight: () -> Int = { -1 },
+ background: Int = 0,
+ longClickListener: (() -> Unit)? = null,
+ listener: () -> Unit,
+ ) : Button(
+ primaryImage,
+ primaryContentDescription,
+ weight = weight,
+ background = background,
+ longClickListener = longClickListener,
+ listener = listener,
+ ) {
+ var enabled: Boolean = false
+ private set
+
+ override fun bind(view: View) {
+ enabled = isInPrimaryState.invoke()
+
+ val button = view as ImageButton
+ if (enabled) {
+ button.setImageDrawable(primaryImage)
+ button.contentDescription = primaryContentDescription
+ button.setTintResource(primaryImageTintResource)
+ button.isEnabled = true
+ } else {
+ button.setImageDrawable(secondaryImage)
+ button.contentDescription = secondaryContentDescription
+ button.setTintResource(secondaryImageTintResource)
+ button.isEnabled = !disableInSecondaryState
+ }
+ }
+ }
+
+ companion object {
+ internal const val ACTION_PADDING_DP = 16
+ internal val DEFAULT_PADDING =
+ Padding(ACTION_PADDING_DP, ACTION_PADDING_DP, ACTION_PADDING_DP, ACTION_PADDING_DP)
+ }
+}
+
+/**
+ * Wraps [filter] execution in a coroutine context, cancelling prior executions on every invocation.
+ * [coroutineContext] must be of type that doesn't propagate cancellation of its children upwards.
+ */
+class AsyncFilterListener(
+ private val urlView: AutocompleteView,
+ override val coroutineContext: CoroutineContext,
+ private val filter: suspend (String, AutocompleteDelegate) -> Unit,
+ private val uiContext: CoroutineContext = Dispatchers.Main,
+) : OnFilterListener, CoroutineScope {
+ override fun invoke(text: String) {
+ // We got a new input, so whatever past autocomplete queries we still have running are
+ // irrelevant. We cancel them, but do not depend on cancellation to take place.
+ coroutineContext.cancelChildren()
+
+ CoroutineScope(coroutineContext).launch {
+ filter(text, AsyncAutocompleteDelegate(urlView, this, uiContext))
+ }
+ }
+}
+
+/**
+ * An autocomplete delegate which is aware of its parent scope (to check for cancellations).
+ * Responsible for processing autocompletion results and discarding stale results when [urlView] moved on.
+ */
+private class AsyncAutocompleteDelegate(
+ private val urlView: AutocompleteView,
+ private val parentScope: CoroutineScope,
+ override val coroutineContext: CoroutineContext,
+ private val logger: Logger = Logger("AsyncAutocompleteDelegate"),
+) : AutocompleteDelegate, CoroutineScope {
+ override fun applyAutocompleteResult(result: AutocompleteResult, onApplied: () -> Unit) {
+ // Bail out if we were cancelled already.
+ if (!parentScope.isActive) {
+ logger.debug("Autocomplete request cancelled. Discarding results.")
+ return
+ }
+
+ // Process results on the UI dispatcher.
+ CoroutineScope(coroutineContext).launch {
+ // Ignore this result if the query is stale.
+ if (result.input == urlView.originalText.lowercase()) {
+ urlView.applyAutocompleteResult(
+ InlineAutocompleteEditText.AutocompleteResult(
+ text = result.text,
+ source = result.source,
+ totalItems = result.totalItems,
+ ),
+ )
+ onApplied()
+ } else {
+ logger.debug("Discarding stale autocomplete result.")
+ }
+ }
+ }
+
+ override fun noAutocompleteResult(input: String) {
+ // Bail out if we were cancelled already.
+ if (!parentScope.isActive) {
+ logger.debug("Autocomplete request cancelled. Discarding 'noAutocompleteResult'.")
+ return
+ }
+
+ // Process results on the UI thread.
+ CoroutineScope(coroutineContext).launch {
+ // Ignore this result if the query is stale.
+ if (input == urlView.originalText) {
+ urlView.noAutocompleteResult()
+ } else {
+ logger.debug("Discarding stale lack of autocomplete results.")
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt
new file mode 100644
index 0000000000..2ad177a67f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt
@@ -0,0 +1,711 @@
+/* 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.browser.toolbar.display
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.TypedValue
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import android.widget.ImageView
+import android.widget.ProgressBar
+import androidx.annotation.ColorInt
+import androidx.appcompat.content.res.AppCompatResources.getDrawable
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.browser.toolbar.R
+import mozilla.components.browser.toolbar.internal.ActionContainer
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.ktx.android.content.isScreenReaderEnabled
+import mozilla.components.ui.colors.R.color as photonColors
+
+/**
+ * Sub-component of the browser toolbar responsible for displaying the URL and related controls ("display mode").
+ *
+ * Structure:
+ * ```
+ * +-------------+------------+-----------------------+----------+------+
+ * | navigation | indicators | url [ page ] | browser | menu |
+ * | actions | | [ actions ] | actions | |
+ * +-------------+------------+-----------------------+----------+------+
+ * +------------------------progress------------------------------------+
+ * ```
+ *
+ * Navigation actions (optional):
+ * A dynamic list of clickable icons usually used for navigation on larger devices
+ * (e.g. “back”/”forward” buttons.)
+ *
+ * Indicators (optional):
+ * Tracking protection indicator icon (e.g. “shield” icon) that may show a doorhanger when clicked.
+ * Separator icon: a vertical line that separate the above and below icons.
+ * Site security indicator icon (e.g. “Lock” icon) that may show a doorhanger when clicked.
+ * Empty indicator: Icon that will be displayed if the URL is empty.
+ *
+ * URL:
+ * Section that displays the current URL (read-only)
+ *
+ * Page actions (optional):
+ * A dynamic list of clickable icons inside the URL section (e.g. “reader mode” icon)
+ *
+ * Browser actions (optional):
+ * A list of dynamic clickable icons on the toolbar (e.g. tabs tray button)
+ *
+ * Menu (optional):
+ * A button that shows an overflow menu when clicked (constructed using the browser-menu
+ * component)
+ *
+ * Progress (optional):
+ * A horizontal progress bar showing the loading progress (at the top or bottom of the toolbar).
+ */
+@Suppress("LargeClass")
+class DisplayToolbar internal constructor(
+ private val context: Context,
+ private val toolbar: BrowserToolbar,
+ internal val rootView: View,
+) {
+ /**
+ * Enum of indicators that can be displayed in the toolbar.
+ */
+ enum class Indicators {
+ SECURITY,
+ TRACKING_PROTECTION,
+ EMPTY,
+ HIGHLIGHT,
+ }
+
+ /**
+ * Data class holding the customizable colors in "display mode".
+ *
+ * @property securityIconSecure Color tint for the "secure connection" icon (lock).
+ * @property securityIconInsecure Color tint for the "insecure connection" icon (broken lock).
+ * @property emptyIcon Color tint for the icon shown when the URL is empty.
+ * @property menu Color tint for the menu icon.
+ * @property hint Text color of the hint shown when the URL is empty.
+ * @property title Text color of the website title.
+ * @property text Text color of the URL.
+ * @property trackingProtection Color tint for the tracking protection icons.
+ * @property separator Color tint for the separator shown between indicators.
+ * @property pageActionSeparator Color tint of separator dividing url and page actions.
+ * @property highlight Color tint for the highlight icon.
+ *
+ * Set/Get the site security icon colours. It uses a pair of color integers which represent the
+ * insecure and secure colours respectively.
+ */
+ data class Colors(
+ @ColorInt val securityIconSecure: Int,
+ @ColorInt val securityIconInsecure: Int,
+ @ColorInt val emptyIcon: Int,
+ @ColorInt val menu: Int,
+ @ColorInt val hint: Int,
+ @ColorInt val title: Int,
+ @ColorInt val text: Int,
+ @ColorInt val trackingProtection: Int?,
+ @ColorInt val separator: Int,
+ @ColorInt val pageActionSeparator: Int,
+ @ColorInt val highlight: Int?,
+ )
+
+ /**
+ * Data class holding the customizable icons in "display mode".
+ *
+ * @property emptyIcon An icon that is shown in front of the URL if the URL is empty.
+ * @property trackingProtectionTrackersBlocked An icon that is shown if tracking protection is
+ * enabled and trackers have been blocked.
+ * @property trackingProtectionNothingBlocked An icon that is shown if tracking protection is
+ * enabled and no trackers have been blocked.
+ * @property trackingProtectionException An icon that is shown if tracking protection is enabled
+ * but the current page is in the "exception list".
+ * @property highlight An icon that is shown if any event needs to be brought
+ * to the user's attention. Like the autoplay permission been blocked.
+ */
+ data class Icons(
+ val emptyIcon: Drawable?,
+ val trackingProtectionTrackersBlocked: Drawable,
+ val trackingProtectionNothingBlocked: Drawable,
+ val trackingProtectionException: Drawable,
+ val highlight: Drawable,
+ )
+
+ /**
+ * Gravity enum for positioning the progress bar.
+ */
+ enum class Gravity {
+ TOP,
+ BOTTOM,
+ }
+
+ internal val views = DisplayToolbarViews(
+ browserActions = rootView.findViewById(R.id.mozac_browser_toolbar_browser_actions),
+ pageActions = rootView.findViewById(R.id.mozac_browser_toolbar_page_actions),
+ navigationActions = rootView.findViewById(R.id.mozac_browser_toolbar_navigation_actions),
+ background = rootView.findViewById(R.id.mozac_browser_toolbar_background),
+ separator = rootView.findViewById(R.id.mozac_browser_toolbar_separator),
+ pageActionSeparator = rootView.findViewById(R.id.mozac_browser_toolbar_action_separator),
+ emptyIndicator = rootView.findViewById(R.id.mozac_browser_toolbar_empty_indicator),
+ menu = MenuButton(rootView.findViewById(R.id.mozac_browser_toolbar_menu)),
+ securityIndicator = rootView.findViewById(R.id.mozac_browser_toolbar_security_indicator),
+ trackingProtectionIndicator = rootView.findViewById(
+ R.id.mozac_browser_toolbar_tracking_protection_indicator,
+ ),
+ origin = rootView.findViewById<OriginView>(R.id.mozac_browser_toolbar_origin_view).also {
+ it.toolbar = toolbar
+ },
+ progress = rootView.findViewById<ProgressBar>(R.id.mozac_browser_toolbar_progress),
+ highlight = rootView.findViewById(R.id.mozac_browser_toolbar_permission_indicator),
+ )
+
+ /**
+ * Customizable colors in "display mode".
+ */
+ var colors: Colors = Colors(
+ securityIconSecure = ContextCompat.getColor(context, photonColors.photonWhite),
+ securityIconInsecure = ContextCompat.getColor(context, photonColors.photonWhite),
+ emptyIcon = ContextCompat.getColor(context, photonColors.photonWhite),
+ menu = ContextCompat.getColor(context, photonColors.photonWhite),
+ hint = views.origin.hintColor,
+ title = views.origin.titleColor,
+ text = views.origin.textColor,
+ trackingProtection = null,
+ separator = ContextCompat.getColor(context, photonColors.photonGrey80),
+ pageActionSeparator = ContextCompat.getColor(context, photonColors.photonGrey80),
+ highlight = null,
+ )
+ set(value) {
+ field = value
+
+ updateSiteSecurityIcon()
+ views.emptyIndicator.setColorFilter(value.emptyIcon)
+ views.menu.setColorFilter(value.menu)
+ views.origin.hintColor = value.hint
+ views.origin.titleColor = value.title
+ views.origin.textColor = value.text
+ views.separator.setColorFilter(value.separator)
+ views.pageActionSeparator.setBackgroundColor(value.pageActionSeparator)
+
+ if (value.trackingProtection != null) {
+ views.trackingProtectionIndicator.setTint(value.trackingProtection)
+ views.trackingProtectionIndicator.setColorFilter(value.trackingProtection)
+ }
+
+ if (value.highlight != null) {
+ views.highlight.setTint(value.highlight)
+ }
+ }
+
+ /**
+ * Customizable icons in "edit mode".
+ */
+ var icons: Icons = Icons(
+ emptyIcon = null,
+ trackingProtectionTrackersBlocked = requireNotNull(
+ getDrawable(context, TrackingProtectionIconView.DEFAULT_ICON_ON_TRACKERS_BLOCKED),
+ ),
+ trackingProtectionNothingBlocked = requireNotNull(
+ getDrawable(context, TrackingProtectionIconView.DEFAULT_ICON_ON_NO_TRACKERS_BLOCKED),
+ ),
+ trackingProtectionException = requireNotNull(
+ getDrawable(context, TrackingProtectionIconView.DEFAULT_ICON_OFF_FOR_A_SITE),
+ ),
+ highlight = requireNotNull(
+ getDrawable(context, R.drawable.mozac_dot_notification),
+ ),
+ )
+ set(value) {
+ field = value
+
+ views.emptyIndicator.setImageDrawable(value.emptyIcon)
+
+ views.trackingProtectionIndicator.setIcons(
+ value.trackingProtectionNothingBlocked,
+ value.trackingProtectionTrackersBlocked,
+ value.trackingProtectionException,
+ )
+ views.highlight.setIcon(value.highlight)
+ }
+
+ /**
+ * Allows customization of URL for display purposes.
+ */
+ var urlFormatter: ((CharSequence) -> CharSequence)? = null
+
+ /**
+ * Sets a listener to be invoked when the site security indicator icon is clicked.
+ */
+ fun setOnSiteSecurityClickedListener(listener: (() -> Unit)?) {
+ if (listener == null) {
+ views.securityIndicator.setOnClickListener(null)
+ views.securityIndicator.background = null
+ } else {
+ views.securityIndicator.setOnClickListener {
+ listener.invoke()
+ }
+
+ val outValue = TypedValue()
+ context.theme.resolveAttribute(
+ android.R.attr.selectableItemBackgroundBorderless,
+ outValue,
+ true,
+ )
+
+ views.securityIndicator.setBackgroundResource(outValue.resourceId)
+ }
+ }
+
+ /**
+ * Sets a listener to be invoked when the site tracking protection indicator icon is clicked.
+ */
+ fun setOnTrackingProtectionClickedListener(listener: (() -> Unit)?) {
+ if (listener == null) {
+ views.trackingProtectionIndicator.setOnClickListener(null)
+ views.trackingProtectionIndicator.background = null
+ } else {
+ views.trackingProtectionIndicator.setOnClickListener {
+ listener.invoke()
+ }
+
+ val outValue = TypedValue()
+ context.theme.resolveAttribute(
+ android.R.attr.selectableItemBackgroundBorderless,
+ outValue,
+ true,
+ )
+
+ views.trackingProtectionIndicator.setBackgroundResource(outValue.resourceId)
+ }
+ }
+
+ /**
+ * Sets a lambda to be invoked when the menu is dismissed
+ */
+ fun setMenuDismissAction(onDismiss: () -> Unit) {
+ views.menu.setMenuDismissAction(onDismiss)
+ }
+
+ /**
+ * List of indicators that should be displayed next to the URL.
+ */
+ var indicators: List<Indicators> = listOf(Indicators.SECURITY)
+ set(value) {
+ field = value
+
+ updateIndicatorVisibility()
+ }
+
+ var displayIndicatorSeparator: Boolean = true
+ set(value) {
+ field = value
+ updateIndicatorVisibility()
+ }
+
+ /**
+ * Sets the background that should be drawn behind the URL, page actions an indicators.
+ */
+ fun setUrlBackground(background: Drawable?) {
+ views.background.setImageDrawable(background)
+ }
+
+ /**
+ * Whether the progress bar should be drawn at the top or bottom of the toolbar.
+ */
+ var progressGravity: Gravity = Gravity.BOTTOM
+ set(value) {
+ field = value
+
+ val layout = rootView as ConstraintLayout
+ layout.hashCode()
+
+ val constraintSet = ConstraintSet()
+ constraintSet.clone(layout)
+ constraintSet.clear(views.progress.id, ConstraintSet.TOP)
+ constraintSet.clear(views.progress.id, ConstraintSet.BOTTOM)
+ constraintSet.connect(
+ views.progress.id,
+ if (value == Gravity.TOP) ConstraintSet.TOP else ConstraintSet.BOTTOM,
+ ConstraintSet.PARENT_ID,
+ if (value == Gravity.TOP) ConstraintSet.TOP else ConstraintSet.BOTTOM,
+ )
+ constraintSet.applyTo(layout)
+ }
+
+ /**
+ * Sets a lambda that will be invoked whenever the URL in display mode was clicked. Only if this
+ * lambda returns <code>true</code> the toolbar will switch to editing mode. Return
+ * <code>false</code> to not switch to editing mode and handle the click manually.
+ */
+ var onUrlClicked: () -> Boolean
+ get() = views.origin.onUrlClicked
+ set(value) {
+ views.origin.onUrlClicked = value
+ }
+
+ /**
+ * Sets the text to be displayed when the URL of the toolbar is empty.
+ */
+ var hint: String
+ get() = views.origin.hint
+ set(value) {
+ views.origin.hint = value
+ }
+
+ /**
+ * Sets the size of the text for the title displayed in the toolbar.
+ */
+ var titleTextSize: Float
+ get() = views.origin.titleTextSize
+ set(value) {
+ views.origin.titleTextSize = value
+ }
+
+ /**
+ * Sets the size of the text for the URL/search term displayed in the toolbar.
+ */
+ var textSize: Float
+ get() = views.origin.textSize
+ set(value) {
+ views.origin.textSize = value
+ }
+
+ /**
+ * Sets the typeface of the text for the URL/search term displayed in the toolbar.
+ */
+ var typeface: Typeface
+ get() = views.origin.typeface
+ set(value) {
+ views.origin.typeface = value
+ }
+
+ /**
+ * Sets a [BrowserMenuBuilder] that will be used to create a menu when the menu button is clicked.
+ * The menu button will only be visible if a builder or controller has been set.
+ */
+ var menuBuilder: BrowserMenuBuilder?
+ get() = views.menu.menuBuilder
+ set(value) {
+ views.menu.menuBuilder = value
+ }
+
+ /**
+ * Sets a [MenuController] that will be used to create a menu when the menu button is clicked.
+ * The menu button will only be visible if a builder or controller has been set.
+ * If both a [menuBuilder] and controller are present, only the controller will be used.
+ */
+ var menuController: MenuController?
+ get() = views.menu.menuController
+ set(value) {
+ views.menu.menuController = value
+ }
+
+ /**
+ * Set a LongClickListener to the urlView of the toolbar.
+ */
+ fun setOnUrlLongClickListener(handler: ((View) -> Boolean)?) = views.origin.setOnUrlLongClickListener(handler)
+
+ private fun updateIndicatorVisibility() {
+ val urlEmpty = url.isEmpty()
+
+ views.securityIndicator.visibility = if (!urlEmpty && indicators.contains(Indicators.SECURITY)) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ views.trackingProtectionIndicator.visibility = if (
+ !urlEmpty && indicators.contains(Indicators.TRACKING_PROTECTION)
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ views.emptyIndicator.visibility = if (urlEmpty && indicators.contains(Indicators.EMPTY)) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ views.highlight.visibility = if (!urlEmpty && indicators.contains(Indicators.HIGHLIGHT)) {
+ setHighlight(toolbar.highlight)
+ } else {
+ View.GONE
+ }
+
+ updateSeparatorVisibility()
+ }
+
+ private fun updateSeparatorVisibility() {
+ views.separator.visibility = if (
+ displayIndicatorSeparator &&
+ views.trackingProtectionIndicator.isVisible &&
+ views.securityIndicator.isVisible
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ // In Fenix (which is using a beta release of ConstraintLayout) we are seeing issues after
+ // early visibility changes. Children of the ConstraintLayout are not visible and have a
+ // size of 0x0 (even though they have a fixed size in the layout XML). Explicitly requesting
+ // to layout the ConstraintLayout fixes that issue. This may be a bug in the beta of
+ // ConstraintLayout and in the future we may be able to just remove this call.
+ rootView.requestLayout()
+ }
+
+ /**
+ * Updates the title to be displayed.
+ */
+ internal var title: String
+ get() = views.origin.title
+ set(value) {
+ views.origin.title = value
+ }
+
+ /**
+ * Updates the URL to be displayed.
+ */
+ internal var url: CharSequence = ""
+ set(value) {
+ field = value
+ views.origin.url = urlFormatter?.invoke(value) ?: value
+ updateIndicatorVisibility()
+ }
+
+ /**
+ * Sets the site's security icon as secure if true, else the regular globe.
+ */
+ internal var siteSecurity: Toolbar.SiteSecurity = Toolbar.SiteSecurity.INSECURE
+ set(value) {
+ field = value
+ updateSiteSecurityIcon()
+ }
+
+ private fun updateSiteSecurityIcon() {
+ @ColorInt val color = when (siteSecurity) {
+ Toolbar.SiteSecurity.INSECURE -> colors.securityIconInsecure
+ Toolbar.SiteSecurity.SECURE -> colors.securityIconSecure
+ }
+ if (color == Color.TRANSPARENT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ views.securityIndicator.clearColorFilter()
+ } else {
+ views.securityIndicator.setColorFilter(color)
+ }
+
+ views.securityIndicator.siteSecurity = siteSecurity
+ }
+
+ internal fun setTrackingProtectionState(state: Toolbar.SiteTrackingProtection) {
+ views.trackingProtectionIndicator.siteTrackingProtection = state
+ updateSeparatorVisibility()
+ }
+
+ internal fun setHighlight(state: Toolbar.Highlight): Int {
+ if (!indicators.contains(Indicators.HIGHLIGHT)) {
+ return views.highlight.visibility
+ }
+
+ views.highlight.state = state
+
+ return views.highlight.visibility
+ }
+
+ internal fun onStop() {
+ views.menu.dismissMenu()
+ }
+
+ /**
+ * Updates the progress to be displayed.
+ *
+ * Accessibility note:
+ * ProgressBars can be made accessible to TalkBack by setting `android:accessibilityLiveRegion`.
+ * They will emit TYPE_VIEW_SELECTED events. TalkBack will format those events into percentage
+ * announcements along with a pitch-change earcon. We are not using that feature here for
+ * several reasons:
+ * 1. They are dispatched via a 200ms timeout. Since loading a page can be a short process,
+ * and since we only update the bar a handful of times, these events often never fire and
+ * they don't give the user a true sense of the progress.
+ * 2. The last 100% event is dispatched after the view is hidden. This prevents the event
+ * from being fired, so the user never gets a "complete" event.
+ * 3. Live regions in TalkBack have their role announced, so the user will hear
+ * "Progress bar, 25%". For a common feature like page load this is very chatty and unintuitive.
+ * 4. We can provide custom strings instead of the less useful percentage utterance, but
+ * TalkBack will not play an earcon if an event has its own text.
+ *
+ * For all those reasons, we are going another route here with a "loading" announcement
+ * when the progress bar first appears along with scroll events that have the same
+ * pitch-change earcon in TalkBack (although they are a bit louder). This gives a concise and
+ * consistent feedback to the user that they can depend on.
+ *
+ */
+ internal fun updateProgress(progress: Int) {
+ if (!views.progress.isVisible && progress > 0) {
+ // Loading has just started, make visible.
+ views.progress.visibility = View.VISIBLE
+
+ // Announce "loading" for accessibility if it has not been completed
+ if (progress < views.progress.max) {
+ views.progress.announceForAccessibility(
+ context.getString(R.string.mozac_browser_toolbar_progress_loading),
+ )
+ }
+ }
+
+ views.progress.progress = progress
+ val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ AccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED)
+ } else {
+ @Suppress("DEPRECATION")
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED)
+ }.apply {
+ scrollY = progress
+ maxScrollY = views.progress.max
+ }
+
+ if (context.isScreenReaderEnabled) {
+ views.progress.parent.requestSendAccessibilityEvent(views.progress, event)
+ }
+
+ if (progress >= views.progress.max) {
+ // Loading is done, hide progress bar.
+ views.progress.visibility = View.GONE
+ }
+ }
+
+ /**
+ * Declare that the actions (navigation actions, browser actions, page actions) have changed and
+ * should be updated if needed.
+ */
+ internal fun invalidateActions() {
+ views.menu.invalidateMenu()
+
+ views.browserActions.invalidateActions()
+ views.pageActions.invalidateActions()
+ views.navigationActions.invalidateActions()
+ }
+
+ /**
+ * Adds an action to be displayed on the right side of the toolbar (outside of the URL bounding
+ * box) in display mode.
+ *
+ * If there is not enough room to show all icons then some icons may be moved to an overflow
+ * menu.
+ *
+ * Related:
+ * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Browser_action
+ */
+ internal fun addBrowserAction(action: Toolbar.Action) {
+ views.browserActions.addAction(action)
+ }
+
+ /**
+ * Removes a previously added browser action (see [addBrowserAction]). If the provided
+ * action was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ internal fun removeBrowserAction(action: Toolbar.Action) {
+ views.browserActions.removeAction(action)
+ }
+
+ /**
+ * Removes a previously added page action (see [addBrowserAction]). If the provided
+ * action was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ internal fun removePageAction(action: Toolbar.Action) {
+ views.pageActions.removeAction(action)
+ }
+
+ /**
+ * Adds an action to be displayed on the right side of the URL in display mode.
+ *
+ * Related:
+ * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Page_actions
+ */
+ internal fun addPageAction(action: Toolbar.Action) {
+ views.pageActions.addAction(action)
+ }
+
+ /**
+ * Adds an action to be display on the far left side of the toolbar. This area is usually used
+ * on larger devices for navigation actions like "back" and "forward".
+ */
+ internal fun addNavigationAction(action: Toolbar.Action) {
+ views.navigationActions.addAction(action)
+ }
+
+ /**
+ * Removes a previously added navigation action (see [addNavigationAction]). If the provided
+ * action was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ internal fun removeNavigationAction(action: Toolbar.Action) {
+ views.navigationActions.removeAction(action)
+ }
+
+ /**
+ * Hides the menu button in display mode.
+ */
+ fun hideMenuButton() {
+ views.menu.setShouldBeHidden(true)
+ }
+
+ /**
+ * Shows the menu button in display mode.
+ */
+ internal fun showMenuButton() {
+ views.menu.setShouldBeHidden(false)
+ }
+
+ /**
+ * Sets the horizontal padding.
+ */
+ fun setHorizontalPadding(horizontalPadding: Int) {
+ rootView.setPadding(horizontalPadding, 0, horizontalPadding, 0)
+ }
+
+ /**
+ * Hides the page action separator in display mode.
+ */
+ fun hidePageActionSeparator() {
+ views.pageActionSeparator.isVisible = false
+ }
+
+ /**
+ * Shows the page action separator in display mode.
+ */
+ internal fun showPageActionSeparator() {
+ views.pageActionSeparator.isVisible = true
+ }
+}
+
+/**
+ * Internal holder for view references.
+ */
+@Suppress("LongParameterList")
+internal class DisplayToolbarViews(
+ val browserActions: ActionContainer,
+ val pageActions: ActionContainer,
+ val navigationActions: ActionContainer,
+ val background: ImageView,
+ val separator: ImageView,
+ val pageActionSeparator: View,
+ val emptyIndicator: ImageView,
+ val menu: MenuButton,
+ val securityIndicator: SiteSecurityIconView,
+ val trackingProtectionIndicator: TrackingProtectionIconView,
+ val origin: OriginView,
+ val progress: ProgressBar,
+ val highlight: HighlightView,
+)
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbarView.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbarView.kt
new file mode 100644
index 0000000000..733684895d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbarView.kt
@@ -0,0 +1,51 @@
+/* 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.browser.toolbar.display
+
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.toolbar.R
+
+/**
+ * Custom ConstraintLayout for DisplayToolbar that allows us to draw ripple backgrounds on the toolbar
+ * by setting a background to transparent.
+ */
+class DisplayToolbarView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+ init {
+ // Forcing transparent background so that draw methods will get called and ripple effect
+ // for children will be drawn on this layout.
+ setBackgroundColor(0x00000000)
+ }
+
+ lateinit var backgroundView: ImageView
+
+ override fun onFinishInflate() {
+ backgroundView = findViewById(R.id.mozac_browser_toolbar_background)
+ backgroundView.visibility = View.INVISIBLE
+
+ super.onFinishInflate()
+ }
+
+ // Overriding draw instead of onDraw since we want to draw the background before the actual
+ // (transparent) background (with a ripple effect) is drawn.
+ override fun draw(canvas: Canvas) {
+ canvas.save()
+ canvas.translate(backgroundView.x, backgroundView.y)
+
+ backgroundView.drawable?.draw(canvas)
+
+ canvas.restore()
+
+ super.draw(canvas)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/HighlightView.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/HighlightView.kt
new file mode 100644
index 0000000000..1120c3eaea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/HighlightView.kt
@@ -0,0 +1,91 @@
+/* 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.browser.toolbar.display
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.view.isVisible
+import mozilla.components.browser.toolbar.R
+import mozilla.components.concept.toolbar.Toolbar.Highlight
+import mozilla.components.concept.toolbar.Toolbar.Highlight.NONE
+import mozilla.components.concept.toolbar.Toolbar.Highlight.PERMISSIONS_CHANGED
+
+/**
+ * Internal widget to display a dot notification.
+ */
+internal class HighlightView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : AppCompatImageView(context, attrs, defStyleAttr) {
+
+ init {
+ visibility = GONE
+ }
+
+ var state: Highlight = NONE
+ set(value) {
+ if (value != field) {
+ field = value
+ updateIcon()
+ }
+ }
+
+ @VisibleForTesting
+ internal var highlightTint: Int? = null
+
+ private var highlightIcon: Drawable =
+ requireNotNull(AppCompatResources.getDrawable(context, DEFAULT_ICON))
+
+ fun setTint(tint: Int) {
+ highlightTint = tint
+ setColorFilter(tint)
+ }
+
+ fun setIcon(icons: Drawable) {
+ this.highlightIcon = icons
+
+ updateIcon()
+ }
+
+ @Synchronized
+ @VisibleForTesting
+ internal fun updateIcon() {
+ val update = state.toUpdate()
+
+ isVisible = update.visible
+
+ contentDescription = if (update.contentDescription != null) {
+ context.getString(update.contentDescription)
+ } else {
+ null
+ }
+
+ highlightTint?.let { setColorFilter(it) }
+ setImageDrawable(update.drawable)
+ }
+
+ companion object {
+ val DEFAULT_ICON = R.drawable.mozac_dot_notification
+ }
+
+ private fun Highlight.toUpdate(): Update = when (this) {
+ PERMISSIONS_CHANGED -> Update(
+ highlightIcon,
+ R.string.mozac_browser_toolbar_content_description_autoplay_blocked,
+ true,
+ )
+
+ NONE -> Update(
+ null,
+ null,
+ false,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/MenuButton.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/MenuButton.kt
new file mode 100644
index 0000000000..621e20032b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/MenuButton.kt
@@ -0,0 +1,106 @@
+/* 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.browser.toolbar.display
+
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.core.view.isVisible
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.menu.ext.asCandidateList
+import mozilla.components.browser.menu.ext.getHighlight
+import mozilla.components.browser.toolbar.facts.emitOpenMenuFact
+import mozilla.components.concept.menu.MenuController
+
+internal class MenuButton(
+ @get:VisibleForTesting internal val impl: mozilla.components.browser.menu.view.MenuButton,
+) {
+
+ init {
+ impl.isVisible = false
+ impl.register(
+ object : mozilla.components.concept.menu.MenuButton.Observer {
+ override fun onShow() {
+ emitOpenMenuFact(impl.menuBuilder?.extras)
+ }
+ },
+ )
+ }
+
+ /**
+ * Reference to the [MenuController].
+ * If present, [menuBuilder] will be ignored.
+ */
+ var menuController: MenuController?
+ get() = impl.menuController
+ set(value) {
+ impl.menuController = value
+ impl.isVisible = shouldBeVisible()
+ }
+
+ /**
+ * Legacy [BrowserMenuBuilder] reference.
+ * Used to build the menu.
+ */
+ var menuBuilder: BrowserMenuBuilder?
+ get() = impl.menuBuilder
+ set(value) {
+ impl.menuBuilder = value
+ impl.isVisible = shouldBeVisible()
+ }
+
+ /**
+ * Declare that the menu items should be updated if needed.
+ * This should only be used once a [menuBuilder] is set.
+ * To update items in the [menuController], call [MenuController.submitList] directly.
+ */
+ fun invalidateMenu() {
+ val menuController = menuController
+ if (menuController != null) {
+ // Convert the menu builder items into a menu candidate list,
+ // if the menu builder is present
+ menuBuilder?.items?.let { items ->
+ val list = items.asCandidateList(impl.context)
+ menuController.submitList(list)
+ }
+ } else {
+ // Invalidate the BrowserMenu
+ impl.invalidateBrowserMenu()
+ impl.setHighlight(menuBuilder?.items?.getHighlight())
+ }
+ }
+
+ fun dismissMenu() {
+ val menuController = menuController
+ if (menuController != null) {
+ // Use the controller to dismiss the menu
+ menuController.dismiss()
+ } else {
+ // Use the button to dismiss the legacy menu
+ impl.dismissMenu()
+ }
+ }
+
+ /**
+ * Sets a lambda to be invoked when the menu is dismissed
+ */
+ @Suppress("Deprecation")
+ fun setMenuDismissAction(onDismiss: () -> Unit) {
+ impl.onDismiss = onDismiss
+ }
+
+ fun setColorFilter(@ColorInt color: Int) = impl.setColorFilter(color)
+
+ /**
+ * Hides the menu button.
+ *
+ * @param shouldBeHidden A [Boolean] that determines the visibility of the menu button.
+ */
+ fun setShouldBeHidden(shouldBeHidden: Boolean) {
+ impl.isVisible = !shouldBeHidden && shouldBeVisible()
+ }
+
+ @VisibleForTesting
+ internal fun shouldBeVisible() = impl.menuBuilder != null || impl.menuController != null
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/OriginView.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/OriginView.kt
new file mode 100644
index 0000000000..56cb43cfd2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/OriginView.kt
@@ -0,0 +1,198 @@
+/* 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.browser.toolbar.display
+
+import android.animation.LayoutTransition
+import android.content.Context
+import android.graphics.Typeface
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.core.view.isVisible
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.browser.toolbar.R
+
+private const val TITLE_VIEW_WEIGHT = 5.7f
+private const val URL_VIEW_WEIGHT = 4.3f
+
+/**
+ * View displaying the URL and optionally the title of a website.
+ */
+internal class OriginView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : LinearLayout(context, attrs, defStyleAttr) {
+ internal lateinit var toolbar: BrowserToolbar
+
+ private val textSizeUrlNormal = context.resources.getDimension(
+ R.dimen.mozac_browser_toolbar_url_textsize,
+ )
+ private val textSizeUrlWithTitle = context.resources.getDimension(
+ R.dimen.mozac_browser_toolbar_url_with_title_textsize,
+ )
+ private val textSizeTitle = context.resources.getDimension(
+ R.dimen.mozac_browser_toolbar_title_textsize,
+ )
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val urlView = TextView(context).apply {
+ id = R.id.mozac_browser_toolbar_url_view
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizeUrlNormal)
+ gravity = Gravity.CENTER_VERTICAL
+
+ setSingleLine()
+ isClickable = true
+ isFocusable = true
+
+ setOnClickListener {
+ if (onUrlClicked()) {
+ toolbar.editMode()
+ }
+ }
+
+ val fadingEdgeSize = resources.getDimensionPixelSize(
+ R.dimen.mozac_browser_toolbar_url_fading_edge_size,
+ )
+
+ setFadingEdgeLength(fadingEdgeSize)
+ isHorizontalFadingEdgeEnabled = fadingEdgeSize > 0
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val titleView = TextView(context).apply {
+ id = R.id.mozac_browser_toolbar_title_view
+ visibility = View.GONE
+
+ setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ textSizeTitle,
+ )
+ gravity = Gravity.CENTER_VERTICAL
+
+ setSingleLine()
+
+ val fadingEdgeSize = resources.getDimensionPixelSize(
+ R.dimen.mozac_browser_toolbar_url_fading_edge_size,
+ )
+
+ setFadingEdgeLength(fadingEdgeSize)
+ isHorizontalFadingEdgeEnabled = fadingEdgeSize > 0
+ }
+
+ init {
+ orientation = VERTICAL
+
+ addView(
+ titleView,
+ LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ 0,
+ TITLE_VIEW_WEIGHT,
+ ),
+ )
+
+ addView(
+ urlView,
+ LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ 0,
+ URL_VIEW_WEIGHT,
+ ),
+ )
+
+ layoutTransition = LayoutTransition()
+ }
+
+ internal var title: String
+ get() = titleView.text.toString()
+ set(value) {
+ titleView.text = value
+
+ titleView.isVisible = value.isNotEmpty()
+
+ urlView.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ if (value.isNotEmpty()) {
+ textSizeUrlWithTitle
+ } else {
+ textSizeUrlNormal
+ },
+ )
+ }
+
+ internal var onUrlClicked: () -> Boolean = { true }
+
+ fun setOnUrlLongClickListener(handler: ((View) -> Boolean)?) {
+ urlView.isLongClickable = true
+ titleView.isLongClickable = true
+
+ urlView.setOnLongClickListener(handler)
+ titleView.setOnLongClickListener(handler)
+ }
+
+ internal var url: CharSequence
+ get() = urlView.text
+ set(value) { urlView.text = value }
+
+ /**
+ * Sets the colour of the text to be displayed when the URL of the toolbar is empty.
+ */
+ var hintColor: Int
+ get() = urlView.currentHintTextColor
+ set(value) {
+ urlView.setHintTextColor(value)
+ }
+
+ /**
+ * Sets the text to be displayed when the URL of the toolbar is empty.
+ */
+ var hint: String
+ get() = urlView.hint.toString()
+ set(value) { urlView.hint = value }
+
+ /**
+ * Sets the colour of the text for title displayed in the toolbar.
+ */
+ var titleColor: Int
+ get() = urlView.currentTextColor
+ set(value) { titleView.setTextColor(value) }
+
+ /**
+ * Sets the colour of the text for the URL/search term displayed in the toolbar.
+ */
+ var textColor: Int
+ get() = urlView.currentTextColor
+ set(value) { urlView.setTextColor(value) }
+
+ /**
+ * Sets the size of the text for the title displayed in the toolbar.
+ */
+ var titleTextSize: Float
+ get() = titleView.textSize
+ set(value) { titleView.textSize = value }
+
+ /**
+ * Sets the size of the text for the URL/search term displayed in the toolbar.
+ */
+ var textSize: Float
+ get() = urlView.textSize
+ set(value) {
+ urlView.textSize = value
+ }
+
+ /**
+ * Sets the typeface of the text for the URL/search term displayed in the toolbar.
+ */
+ var typeface: Typeface
+ get() = urlView.typeface
+ set(value) {
+ urlView.typeface = value
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/SiteSecurityIconView.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/SiteSecurityIconView.kt
new file mode 100644
index 0000000000..879897bc56
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/SiteSecurityIconView.kt
@@ -0,0 +1,48 @@
+/* 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.browser.toolbar.display
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageView
+import mozilla.components.browser.toolbar.R
+import mozilla.components.concept.toolbar.Toolbar.SiteSecurity
+
+/**
+ * Internal widget to display the different icons of site security, relies on the
+ * [SiteSecurity] state of each page.
+ */
+internal class SiteSecurityIconView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : AppCompatImageView(context, attrs, defStyleAttr) {
+
+ // We allow null here because in some situations, onCreateDrawableState is getting called from
+ // the super() constructor on the View class, way before this class properties get
+ // initialized causing a null pointer exception.
+ // See for more details: https://github.com/mozilla-mobile/android-components/issues/4058
+ var siteSecurity: SiteSecurity? = SiteSecurity.INSECURE
+ set(value) {
+ if (value != field) {
+ field = value
+ refreshDrawableState()
+ }
+
+ field = value
+ }
+
+ override fun onCreateDrawableState(extraSpace: Int): IntArray {
+ return when (siteSecurity) {
+ SiteSecurity.INSECURE, null -> super.onCreateDrawableState(extraSpace)
+ SiteSecurity.SECURE -> {
+ val drawableState = super.onCreateDrawableState(extraSpace + 1)
+ View.mergeDrawableStates(drawableState, intArrayOf(R.attr.state_site_secure))
+ drawableState
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt
new file mode 100644
index 0000000000..7dde3dc251
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt
@@ -0,0 +1,135 @@
+/* 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.browser.toolbar.display
+
+import android.content.Context
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.view.isVisible
+import mozilla.components.browser.toolbar.R
+import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection
+import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection.OFF_FOR_A_SITE
+import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED
+import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection.ON_TRACKERS_BLOCKED
+
+/**
+ * Internal widget to display the different icons of tracking protection, relies on the
+ * [SiteTrackingProtection] state of each page.
+ */
+internal class TrackingProtectionIconView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : AppCompatImageView(context, attrs, defStyleAttr) {
+ var siteTrackingProtection: SiteTrackingProtection? = null
+ set(value) {
+ if (value != field) {
+ field = value
+ updateIcon()
+ }
+ }
+
+ @VisibleForTesting
+ internal var trackingProtectionTint: Int? = null
+
+ private var iconOnNoTrackersBlocked: Drawable =
+ requireNotNull(AppCompatResources.getDrawable(context, DEFAULT_ICON_ON_NO_TRACKERS_BLOCKED))
+ private var iconOnTrackersBlocked: Drawable =
+ requireNotNull(AppCompatResources.getDrawable(context, DEFAULT_ICON_ON_TRACKERS_BLOCKED))
+ private var disabledForSite: Drawable =
+ requireNotNull(AppCompatResources.getDrawable(context, DEFAULT_ICON_OFF_FOR_A_SITE))
+
+ fun setTint(tint: Int) {
+ trackingProtectionTint = tint
+ }
+
+ fun setIcons(
+ iconOnNoTrackersBlocked: Drawable,
+ iconOnTrackersBlocked: Drawable,
+ disabledForSite: Drawable,
+ ) {
+ this.iconOnNoTrackersBlocked = iconOnNoTrackersBlocked
+ this.iconOnTrackersBlocked = iconOnTrackersBlocked
+ this.disabledForSite = disabledForSite
+
+ updateIcon()
+ }
+
+ @Synchronized
+ private fun updateIcon() {
+ val update = siteTrackingProtection?.toUpdate() ?: return
+
+ isVisible = update.visible
+
+ contentDescription = if (update.contentDescription != null) {
+ context.getString(update.contentDescription)
+ } else {
+ null
+ }
+
+ setOrClearColorFilter(update.drawable)
+ setImageDrawable(update.drawable)
+
+ if (update.drawable is Animatable) {
+ update.drawable.start()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun setOrClearColorFilter(drawable: Drawable?) {
+ if (drawable is Animatable) {
+ clearColorFilter()
+ } else {
+ trackingProtectionTint?.let { setColorFilter(it) }
+ }
+ }
+
+ companion object {
+ val DEFAULT_ICON_ON_NO_TRACKERS_BLOCKED =
+ R.drawable.mozac_ic_tracking_protection_on_no_trackers_blocked
+ val DEFAULT_ICON_ON_TRACKERS_BLOCKED =
+ R.drawable.mozac_ic_tracking_protection_on_trackers_blocked
+ val DEFAULT_ICON_OFF_FOR_A_SITE =
+ R.drawable.mozac_ic_tracking_protection_off_for_a_site
+ }
+
+ private fun SiteTrackingProtection.toUpdate(): Update = when (this) {
+ ON_NO_TRACKERS_BLOCKED -> Update(
+ iconOnNoTrackersBlocked,
+ R.string.mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked,
+ true,
+ )
+
+ ON_TRACKERS_BLOCKED -> Update(
+ iconOnTrackersBlocked,
+ R.string.mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1,
+ true,
+ )
+
+ OFF_FOR_A_SITE -> Update(
+ disabledForSite,
+ R.string.mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1,
+ true,
+ )
+
+ OFF_GLOBALLY -> Update(
+ null,
+ null,
+ false,
+ )
+ }
+}
+
+internal class Update(
+ val drawable: Drawable?,
+ @StringRes val contentDescription: Int?,
+ val visible: Boolean,
+)
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt
new file mode 100644
index 0000000000..ae1be8e844
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt
@@ -0,0 +1,415 @@
+/* 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.browser.toolbar.edit
+
+import android.content.Context
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.view.KeyEvent
+import android.view.View
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.core.content.ContextCompat
+import androidx.core.view.inputmethod.EditorInfoCompat
+import androidx.core.view.isVisible
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.asCoroutineDispatcher
+import mozilla.components.browser.toolbar.AsyncFilterListener
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.browser.toolbar.R
+import mozilla.components.browser.toolbar.facts.emitCommitFact
+import mozilla.components.browser.toolbar.internal.ActionContainer
+import mozilla.components.concept.toolbar.AutocompleteDelegate
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.NamedThreadFactory
+import mozilla.components.support.ktx.android.view.showKeyboard
+import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
+import java.util.concurrent.Executors
+import mozilla.components.ui.colors.R as colorsR
+
+private const val AUTOCOMPLETE_QUERY_THREADS = 3
+
+/**
+ * Sub-component of the browser toolbar responsible for allowing the user to edit the URL ("edit mode").
+ *
+ * Structure:
+ * +------+--------------------+---------------------------+------------------+------+
+ * | icon | edit actions start | url | edit actions end | exit |
+ * +------+--------------------+---------------------------+------------------+------+
+ *
+ * - icon: Optional icon that will be shown in front of the URL.
+ * - edit actions start: Optional action icons injected by other components in front of the URL
+ * (e.g. search engines).
+ * - url: Editable URL of the currently displayed website.
+ * - edit actions end: Optional action icons injected by other components after the URL
+ * (e.g. barcode scanner).
+ * - exit: Button that switches back to display mode or invoke an app-defined callback.
+ */
+@Suppress("LargeClass")
+class EditToolbar internal constructor(
+ context: Context,
+ private val toolbar: BrowserToolbar,
+ internal val rootView: View,
+) {
+ private val logger = Logger("EditToolbar")
+
+ /**
+ * Data class holding the customizable colors in "edit mode".
+ *
+ * @property clear Color tint used for the "cancel" icon to leave "edit mode".
+ * @property icon Color tint of the icon displayed in front of the URL.
+ * @property hint Text color of the hint shown when the URL field is empty.
+ * @property text Text color of the URL.
+ * @property suggestionBackground The background color used for autocomplete suggestions.
+ * @property suggestionForeground The foreground color used for autocomplete suggestions.
+ * @property pageActionSeparator Color tint of separator dividing page actions.
+ */
+ data class Colors(
+ @ColorInt val clear: Int,
+ @ColorInt val erase: Int,
+ @ColorInt val icon: Int?,
+ @ColorInt val hint: Int,
+ @ColorInt val text: Int,
+ @ColorInt val suggestionBackground: Int,
+ @ColorInt val suggestionForeground: Int?,
+ @ColorInt val pageActionSeparator: Int,
+ )
+
+ private val autocompleteDispatcher = SupervisorJob() +
+ Executors.newFixedThreadPool(
+ AUTOCOMPLETE_QUERY_THREADS,
+ NamedThreadFactory("EditToolbar"),
+ ).asCoroutineDispatcher() +
+ CoroutineExceptionHandler { _, throwable ->
+ logger.error("Error while processing autocomplete input", throwable)
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal val views = EditToolbarViews(
+ background = rootView.findViewById(R.id.mozac_browser_toolbar_background),
+ icon = rootView.findViewById(R.id.mozac_browser_toolbar_edit_icon),
+ editActionsStart = rootView.findViewById(R.id.mozac_browser_toolbar_edit_actions_start),
+ editActionsEnd = rootView.findViewById(R.id.mozac_browser_toolbar_edit_actions_end),
+ clear = rootView.findViewById<ImageView>(R.id.mozac_browser_toolbar_clear_view).apply {
+ setOnClickListener {
+ onClear()
+ }
+ },
+ erase = rootView.findViewById<ImageView>(R.id.mozac_browser_toolbar_erase_view).apply {
+ setOnClickListener {
+ onClear()
+ }
+ },
+ url = rootView.findViewById<InlineAutocompleteEditText>(
+ R.id.mozac_browser_toolbar_edit_url_view,
+ ).apply {
+ setOnCommitListener {
+ // We emit the fact before notifying the listener because otherwise the listener may cause a focus
+ // change which may reset the autocomplete state that we want to report here.
+ emitCommitFact(autocompleteResult)
+
+ toolbar.onUrlEntered(text.toString())
+ }
+
+ setOnTextChangeListener { text, _ ->
+ onTextChanged(text)
+ }
+
+ setUrlGoneMargin(
+ ConstraintSet.END,
+ context.resources.getDimensionPixelSize(R.dimen.mozac_browser_toolbar_url_gone_margin_end),
+ )
+
+ setOnDispatchKeyEventPreImeListener { event ->
+ if (event?.keyCode == KeyEvent.KEYCODE_BACK && editListener?.onCancelEditing() != false) {
+ toolbar.displayMode()
+ }
+ false
+ }
+ },
+ pageActionSeparator = rootView.findViewById(R.id.mozac_browser_action_separator),
+ )
+
+ /**
+ * Customizable colors in "edit mode".
+ */
+ var colors: Colors = Colors(
+ clear = ContextCompat.getColor(context, colorsR.color.photonWhite),
+ erase = ContextCompat.getColor(context, colorsR.color.photonWhite),
+ icon = null,
+ hint = views.url.currentHintTextColor,
+ text = views.url.currentTextColor,
+ suggestionBackground = views.url.autoCompleteBackgroundColor,
+ suggestionForeground = views.url.autoCompleteForegroundColor,
+ pageActionSeparator = ContextCompat.getColor(context, colorsR.color.photonGrey80),
+ )
+ set(value) {
+ field = value
+
+ views.clear.setColorFilter(value.clear)
+
+ views.erase.setColorFilter(value.erase)
+
+ if (value.icon != null) {
+ views.icon.setColorFilter(value.icon)
+ }
+
+ views.url.setHintTextColor(value.hint)
+ views.url.setTextColor(value.text)
+ views.url.autoCompleteBackgroundColor = value.suggestionBackground
+ views.url.autoCompleteForegroundColor = value.suggestionForeground
+ views.pageActionSeparator.setBackgroundColor(value.pageActionSeparator)
+ }
+
+ /**
+ * Sets the background that will be drawn behind the URL, icon and edit actions.
+ */
+ fun setUrlBackground(background: Drawable?) {
+ views.background.setImageDrawable(background)
+ }
+
+ /**
+ * Sets an icon that will be drawn in front of the URL.
+ */
+ fun setIcon(icon: Drawable, contentDescription: String) {
+ views.icon.setImageDrawable(icon)
+ views.icon.contentDescription = contentDescription
+ views.icon.visibility = View.VISIBLE
+ }
+
+ /**
+ * Sets a click listener on the icon view
+ */
+ fun setIconClickListener(listener: ((View) -> Unit)?) {
+ views.icon.setOnClickListener(listener)
+ }
+
+ /**
+ * Sets the text to be displayed when the URL of the toolbar is empty.
+ */
+ var hint: String
+ get() = views.url.hint.toString()
+ set(value) { views.url.hint = value }
+
+ /**
+ * Sets the size of the text for the URL/search term displayed in the toolbar.
+ */
+ var textSize: Float
+ get() = views.url.textSize
+ set(value) {
+ views.url.textSize = value
+ }
+
+ /**
+ * Sets the typeface of the text for the URL/search term displayed in the toolbar.
+ */
+ var typeface: Typeface
+ get() = views.url.typeface
+ set(value) { views.url.typeface = value }
+
+ /**
+ * Sets a listener to be invoked when focus of the URL input view (in edit mode) changed.
+ */
+ fun setOnEditFocusChangeListener(listener: (Boolean) -> Unit) {
+ views.url.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
+ listener.invoke(hasFocus)
+ }
+ }
+
+ /**
+ * Focuses the url input field and shows the virtual keyboard if needed.
+ */
+ fun focus() {
+ views.url.run {
+ if (!hasFocus()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // On Android 14 this needs to be called before requestFocus() in order to receive focus.
+ isFocusableInTouchMode = true
+ }
+ requestFocus()
+ showKeyboard()
+ }
+ }
+ }
+
+ internal fun stopEditing() {
+ editListener?.onStopEditing()
+ }
+
+ internal fun startEditing() {
+ editListener?.onStartEditing()
+ }
+
+ internal var editListener: Toolbar.OnEditListener? = null
+
+ internal fun setAutocompleteListener(filter: suspend (String, AutocompleteDelegate) -> Unit) {
+ views.url.setOnFilterListener(
+ AsyncFilterListener(views.url, autocompleteDispatcher, filter),
+ )
+ }
+
+ /**
+ * Attempt to restart the autocomplete functionality with the current user input.
+ */
+ internal fun refreshAutocompleteSuggestion() {
+ views.url.refreshAutocompleteSuggestions()
+ }
+
+ internal fun invalidateActions() {
+ views.editActionsStart.invalidateActions()
+ views.editActionsEnd.invalidateActions()
+ }
+
+ internal fun addEditActionStart(action: Toolbar.Action) {
+ views.editActionsStart.addAction(action)
+ }
+
+ internal fun addEditActionEnd(action: Toolbar.Action) {
+ views.editActionsEnd.addAction(action)
+ }
+
+ internal fun removeEditActionEnd(action: Toolbar.Action) {
+ views.editActionsEnd.removeAction(action)
+ }
+
+ /**
+ * Updates the text of the URL input field. Note: this does *not* affect the value of url itself
+ * and is only a visual change
+ */
+ fun updateUrl(
+ url: String,
+ shouldAutoComplete: Boolean = false,
+ shouldHighlight: Boolean = false,
+ shouldAppend: Boolean = false,
+ ): String {
+ if (shouldAppend) {
+ views.url.appendText(url, shouldAutoComplete)
+ } else {
+ views.url.setText(url, shouldAutoComplete)
+ }
+ views.clear.isVisible = url.isNotBlank() && !toolbar.isNavBarEnabled
+ views.erase.isVisible = url.isNotBlank() && toolbar.isNavBarEnabled
+
+ if (shouldHighlight) {
+ views.url.setSelection(views.url.text.length - url.length, views.url.text.length)
+ }
+ return views.url.text.toString()
+ }
+
+ /**
+ * Select the entire text in the URL input field.
+ */
+ internal fun selectAll() {
+ views.url.selectAll()
+ }
+
+ /**
+ * Places the cursor at the end of the URL input field.
+ */
+ internal fun selectEnd() {
+ views.url.setSelection(views.url.text.length)
+ }
+
+ /**
+ * Applies the given search terms for further editing, requesting new suggestions along the way.
+ */
+ internal fun editSuggestion(searchTerms: String) {
+ updateUrl(searchTerms)
+ views.url.setSelection(views.url.text.length)
+ focus()
+
+ editListener?.onTextChanged(searchTerms)
+ }
+
+ /**
+ * Sets/gets private mode.
+ *
+ * In private mode the IME should not update any personalized data such as typing history and personalized language
+ * model based on what the user typed.
+ */
+ internal var private: Boolean
+ get() = (views.url.imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) != 0
+ set(value) {
+ views.url.imeOptions = if (value) {
+ views.url.imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
+ } else {
+ views.url.imeOptions and (EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv())
+ }
+ }
+
+ private fun onClear() {
+ // We set text to an empty string instead of using clear to avoid #3612.
+ views.url.setText("")
+ editListener?.onInputCleared()
+ }
+
+ private fun setUrlGoneMargin(anchor: Int, dimen: Int) {
+ val set = ConstraintSet()
+ val container = rootView.findViewById<ConstraintLayout>(
+ R.id.mozac_browser_toolbar_container,
+ )
+ set.clone(container)
+ set.setGoneMargin(R.id.mozac_browser_toolbar_edit_url_view, anchor, dimen)
+ set.applyTo(container)
+ }
+
+ private fun onTextChanged(text: String) {
+ views.clear.isVisible = text.isNotBlank() && !toolbar.isNavBarEnabled
+ views.erase.isVisible = text.isNotBlank() && toolbar.isNavBarEnabled
+ views.editActionsEnd.autoHideAction(text.isEmpty())
+
+ /*
+ We use margin_gone instead of margin to take into account both the actionContainer(which in
+ most cases is gone) and the clear button.
+ */
+ if (text.isNotBlank()) {
+ setUrlGoneMargin(ConstraintSet.END, 0)
+ } else {
+ setUrlGoneMargin(
+ ConstraintSet.END,
+ rootView.resources.getDimensionPixelSize(
+ R.dimen.mozac_browser_toolbar_url_gone_margin_end,
+ ),
+ )
+ }
+ editListener?.onTextChanged(text)
+ }
+
+ /**
+ * Hides the page action separator in edit mode.
+ */
+ fun hidePageActionSeparator() {
+ views.pageActionSeparator.isVisible = false
+ }
+
+ /**
+ * Shows the page action separator in edit mode.
+ */
+ fun showPageActionSeparator() {
+ views.pageActionSeparator.isVisible = true
+ }
+}
+
+/**
+ * Internal holder for view references.
+ */
+@Suppress("LongParameterList")
+internal class EditToolbarViews(
+ val background: ImageView,
+ val icon: ImageView,
+ val editActionsStart: ActionContainer,
+ val editActionsEnd: ActionContainer,
+ val clear: ImageView,
+ val erase: ImageView,
+ val url: InlineAutocompleteEditText,
+ val pageActionSeparator: View,
+)
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/facts/ToolbarFacts.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/facts/ToolbarFacts.kt
new file mode 100644
index 0000000000..dd8ec851f2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/facts/ToolbarFacts.kt
@@ -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/. */
+
+package mozilla.components.browser.toolbar.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
+
+/**
+ * Facts emitted for telemetry related to [ToolbarFeature]
+ */
+class ToolbarFacts {
+ /**
+ * Items that specify which portion of the [ToolbarFeature] was interacted with
+ */
+ object Items {
+ const val TOOLBAR = "toolbar"
+ const val MENU = "menu"
+ }
+}
+
+private fun emitToolbarFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.BROWSER_TOOLBAR,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitOpenMenuFact(extras: Map<String, Any>?) {
+ emitToolbarFact(Action.CLICK, ToolbarFacts.Items.MENU, metadata = extras)
+}
+
+internal fun emitCommitFact(
+ autocompleteResult: InlineAutocompleteEditText.AutocompleteResult?,
+) {
+ val metadata = if (autocompleteResult == null) {
+ mapOf(
+ "autocomplete" to false,
+ )
+ } else {
+ mapOf(
+ "autocomplete" to true,
+ "source" to autocompleteResult.source,
+ )
+ }
+
+ emitToolbarFact(Action.COMMIT, ToolbarFacts.Items.TOOLBAR, metadata = metadata)
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionContainer.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionContainer.kt
new file mode 100644
index 0000000000..ca6e761ba1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionContainer.kt
@@ -0,0 +1,134 @@
+/* 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.browser.toolbar.internal
+
+import android.content.Context
+import android.transition.TransitionManager
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.widget.LinearLayout
+import androidx.annotation.VisibleForTesting
+import androidx.core.view.isVisible
+import mozilla.components.browser.toolbar.R
+import mozilla.components.concept.toolbar.Toolbar
+
+/**
+ * A container [View] for displaying [Toolbar.Action] objects.
+ */
+internal class ActionContainer @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : LinearLayout(context, attrs, defStyleAttr) {
+ private val actions = mutableListOf<ActionWrapper>()
+ private var actionSize: Int? = null
+
+ init {
+ gravity = Gravity.CENTER_VERTICAL
+ orientation = HORIZONTAL
+ visibility = View.GONE
+
+ context.obtainStyledAttributes(
+ attrs,
+ R.styleable.ActionContainer,
+ defStyleAttr,
+ 0,
+ ).run {
+ actionSize = attrs?.let {
+ getDimensionPixelSize(R.styleable.ActionContainer_actionContainerItemSize, 0)
+ }
+
+ recycle()
+ }
+ }
+
+ fun addAction(action: Toolbar.Action) {
+ val wrapper = ActionWrapper(action)
+
+ if (action.visible()) {
+ visibility = View.VISIBLE
+
+ action.createView(this).let {
+ wrapper.view = it
+ val insertionIndex = calculateInsertionIndex(action)
+ addActionView(it, insertionIndex)
+ }
+ }
+
+ actions.add(wrapper)
+ }
+
+ /**
+ * Essentially calculates the index of an action on toolbar based on a
+ * map [visibleActionIndicesWithWeights] that holds the order
+ * of visible action indices to their weights sorted by weights.
+ * An index is now calculated by finding the immediate next larger weight
+ * compared to the new action's weight. Index of this find becomes the index of the new action.
+ * If not found, action is appended at the end.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun calculateInsertionIndex(newAction: Toolbar.Action): Int {
+ if (newAction.weight() == -1) {
+ return -1
+ }
+ val visibleActionIndicesWithWeights = actions.filter { it.actual.visible() }
+ .mapNotNull { actionWrapper ->
+ val index = indexOfChild(actionWrapper.view)
+ if (index != -1) index to actionWrapper.actual.weight() else null
+ }.sortedBy { it.second }
+
+ val insertionIndex = visibleActionIndicesWithWeights.firstOrNull { it.second > newAction.weight() }?.first
+
+ return insertionIndex ?: childCount
+ }
+
+ fun removeAction(action: Toolbar.Action) {
+ actions.find { it.actual == action }?.let {
+ actions.remove(it)
+ removeView(it.view)
+ }
+ }
+
+ fun invalidateActions() {
+ TransitionManager.beginDelayedTransition(this)
+ var updatedVisibility = View.GONE
+
+ for (action in actions) {
+ val visible = action.actual.visible()
+
+ if (visible) {
+ updatedVisibility = View.VISIBLE
+ }
+
+ if (!visible && action.view != null) {
+ removeView(action.view)
+ action.view = null
+ } else if (visible && action.view == null) {
+ action.actual.createView(this).let {
+ action.view = it
+ val insertionIndex = calculateInsertionIndex(action.actual)
+ addActionView(it, insertionIndex)
+ }
+ }
+
+ action.view?.let { action.actual.bind(it) }
+ }
+
+ visibility = updatedVisibility
+ }
+
+ fun autoHideAction(isVisible: Boolean) {
+ for (action in actions) {
+ if (action.actual.autoHide()) {
+ action.view?.isVisible = isVisible
+ }
+ }
+ }
+
+ private fun addActionView(view: View, index: Int) {
+ addView(view, index, LayoutParams(actionSize ?: 0, actionSize ?: 0))
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionWrapper.kt b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionWrapper.kt
new file mode 100644
index 0000000000..723cd84e20
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionWrapper.kt
@@ -0,0 +1,16 @@
+/* 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.browser.toolbar.internal
+
+import android.view.View
+import mozilla.components.concept.toolbar.Toolbar
+
+/**
+ * A wrapper helper to pair a Toolbar.Action with an optional View.
+ */
+internal class ActionWrapper(
+ var actual: Toolbar.Action,
+ var view: View? = null,
+)
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_browser_toolbar_icons_vertical_separator.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_browser_toolbar_icons_vertical_separator.xml
new file mode 100644
index 0000000000..2e366640df
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_browser_toolbar_icons_vertical_separator.xml
@@ -0,0 +1,9 @@
+<?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/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <size android:width="@dimen/mozac_browser_toolbar_icons_separator_width"
+ android:height="@dimen/mozac_browser_toolbar_icons_separator_height" />
+ <solid android:color="#aaaaaa" />
+</shape> \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_dot_notification.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_dot_notification.xml
new file mode 100644
index 0000000000..5469b822af
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_dot_notification.xml
@@ -0,0 +1,16 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="8dp"
+ android:height="8dp"
+ android:viewportWidth="10"
+ android:viewportHeight="10">
+ <path
+ android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#fff"
+ android:strokeColor="#000" />
+</vector>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_site_security.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_site_security.xml
new file mode 100644
index 0000000000..ab2b8d0e37
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_site_security.xml
@@ -0,0 +1,11 @@
+<?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/. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:ac="http://schemas.android.com/apk/res-auto">
+ <item
+ android:drawable="@drawable/mozac_ic_lock_24"
+ ac:state_site_secure="true" />
+ <item
+ android:drawable="@drawable/mozac_ic_broken_lock" />
+</selector>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_off_for_a_site.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_off_for_a_site.xml
new file mode 100644
index 0000000000..250bfbfc4c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_off_for_a_site.xml
@@ -0,0 +1,12 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M17.9,10.83c-0.27,2.81 -0.75,4.18 -2,5.86A6.48,6.48 0,0 1,12 19a7,7 0,0 1,-2.32 -0.92l-1.44,1.43A8.44,8.44 0,0 0,11.89 21h0.22a8.36,8.36 0,0 0,5.33 -3.08c1.53,-2 2.14,-3.72 2.45,-6.89C20,10.28 20,9 20,7.76l-2,2c-0.06,0.47 -0.08,0.82 -0.1,1.07zM20.21,3.83a1,1 0,0 0,-1.42 0l-0.3,0.31a1.86,1.86 0,0 0,-0.31 -0.1L12,3 5.82,4A2.14,2.14 0,0 0,4 6.1c0,1.67 0,3.9 0.11,4.9a12.43,12.43 0,0 0,1.69 5.79l-2,2a1,1 0,0 0,0 1.42,1 1,0 0,0 1.42,0l15,-15a1,1 0,0 0,-0.01 -1.42zM12,10.59L12,7l-4,0.7c0,2 0.07,2.74 0.09,3a11.65,11.65 0,0 0,0.63 3.17l-1.46,1.46a11.13,11.13 0,0 1,-1.16 -4.5C6,10.08 6,8.4 6,6.11A0.15,0.15 0,0 1,6.14 6L12,5l4.79,0.79z" />
+</vector>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_no_trackers_blocked.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_no_trackers_blocked.xml
new file mode 100644
index 0000000000..4f81a5602b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_no_trackers_blocked.xml
@@ -0,0 +1,12 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M20 6c0-1-0.8-1.9-1.8-2L12 3 5.8 4C4.8 4 4 5 4 6l0.1 5c0.3 3.2 1 5 2.5 7a8.4 8.4 0 0 0 5.3 3h0.2c2.1-0.3 4-1.4 5.3-3 1.6-2 2.2-3.8 2.5-7l0.1-5zm-2.1 4.8a10 10 0 0 1-2 6c-1 1.1-2.4 2-3.9 2.3a6.5 6.5 0 0 1-3.9-2.4 9.9 9.9 0 0 1-2-5.9 67.3 67.3 0 0 1 0-4.9L12 5l5.9 1 0.1 0.2-0.1 4.7zM8 7.6v3c0.3 2.7 0.8 3.7 1.7 5 0.6 0.6 1.4 1.2 2.3 1.4V7l-4 0.6z" />
+</vector>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_trackers_blocked.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_trackers_blocked.xml
new file mode 100644
index 0000000000..4f81a5602b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_trackers_blocked.xml
@@ -0,0 +1,12 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M20 6c0-1-0.8-1.9-1.8-2L12 3 5.8 4C4.8 4 4 5 4 6l0.1 5c0.3 3.2 1 5 2.5 7a8.4 8.4 0 0 0 5.3 3h0.2c2.1-0.3 4-1.4 5.3-3 1.6-2 2.2-3.8 2.5-7l0.1-5zm-2.1 4.8a10 10 0 0 1-2 6c-1 1.1-2.4 2-3.9 2.3a6.5 6.5 0 0 1-3.9-2.4 9.9 9.9 0 0 1-2-5.9 67.3 67.3 0 0 1 0-4.9L12 5l5.9 1 0.1 0.2-0.1 4.7zM8 7.6v3c0.3 2.7 0.8 3.7 1.7 5 0.6 0.6 1.4 1.2 2.3 1.4V7l-4 0.6z" />
+</vector>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml
new file mode 100644
index 0000000000..c969713fad
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml
@@ -0,0 +1,175 @@
+<?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/. -->
+<mozilla.components.browser.toolbar.display.DisplayToolbarView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:mozac="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:orientation="vertical">
+
+ <!-- Navigation -->
+
+ <mozilla.components.browser.toolbar.internal.ActionContainer
+ android:id="@+id/mozac_browser_toolbar_navigation_actions"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginTop="4dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ mozac:actionContainerItemSize="48dp"
+ tools:layout_width="48dp" />
+
+ <!-- URL container -->
+
+ <ImageView
+ android:id="@+id/mozac_browser_toolbar_background"
+ android:layout_width="0dp"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:importantForAccessibility="no"
+ app:layout_constraintEnd_toStartOf="@+id/mozac_browser_toolbar_browser_actions"
+ app:layout_constraintStart_toEndOf="@+id/mozac_browser_toolbar_navigation_actions"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_goneMarginEnd="0dp" />
+
+ <!-- URL indicators (lock, tracking protection, ..) -->
+
+ <ImageView
+ android:id="@+id/mozac_browser_toolbar_empty_indicator"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:scaleType="center"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="@id/mozac_browser_toolbar_background"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_search_24"
+ tools:ignore="ContentDescription" />
+
+ <mozilla.components.browser.toolbar.display.TrackingProtectionIconView
+ android:id="@+id/mozac_browser_toolbar_tracking_protection_indicator"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:scaleType="center"
+ android:visibility="gone"
+ app:layout_constraintStart_toEndOf="@id/mozac_browser_toolbar_empty_indicator"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_tracking_protection_on_no_trackers_blocked" />
+
+ <ImageView
+ android:id="@+id/mozac_browser_toolbar_separator"
+ android:layout_width="@dimen/mozac_browser_toolbar_icons_separator_width"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ android:visibility="gone"
+ app:layout_constraintStart_toEndOf="@+id/mozac_browser_toolbar_tracking_protection_indicator"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_browser_toolbar_icons_vertical_separator" />
+
+ <mozilla.components.browser.toolbar.display.SiteSecurityIconView
+ android:id="@+id/mozac_browser_toolbar_security_indicator"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:contentDescription="@string/mozac_browser_toolbar_content_description_site_info"
+ android:scaleType="center"
+ app:layout_constraintStart_toEndOf="@+id/mozac_browser_toolbar_separator"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_site_security" />
+
+ <mozilla.components.browser.toolbar.display.HighlightView
+ android:id="@+id/mozac_browser_toolbar_permission_indicator"
+ android:layout_width="10dp"
+ android:layout_height="10dp"
+ android:importantForAccessibility="no"
+ app:layout_constraintBottom_toBottomOf="@+id/mozac_browser_toolbar_security_indicator"
+ app:layout_constraintEnd_toEndOf="@+id/mozac_browser_toolbar_security_indicator"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="8dp"
+ android:visibility="gone"
+ android:tint="@color/photonBlue40"
+ app:srcCompat="@drawable/mozac_dot_notification" />
+
+ <!-- URL & Title -->
+
+ <mozilla.components.browser.toolbar.display.OriginView
+ android:id="@+id/mozac_browser_toolbar_origin_view"
+ android:layout_width="0dp"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:paddingEnd="@dimen/mozac_browser_toolbar_origin_padding_end"
+ app:layout_constraintEnd_toStartOf="@+id/mozac_browser_toolbar_action_separator"
+ app:layout_constraintStart_toEndOf="@+id/mozac_browser_toolbar_security_indicator"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_goneMarginStart="8dp"
+ app:layout_goneMarginTop="8dp" />
+
+ <View
+ android:id="@+id/mozac_browser_toolbar_action_separator"
+ android:layout_width="@dimen/mozac_browser_toolbar_page_action_separator_width"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginStart="8dp"
+ android:visibility="gone"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ app:layout_constraintStart_toEndOf="@+id/mozac_browser_toolbar_origin_view"
+ app:layout_constraintEnd_toStartOf="@id/mozac_browser_toolbar_page_actions"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <!-- Page actions -->
+
+ <mozilla.components.browser.toolbar.internal.ActionContainer
+ android:id="@+id/mozac_browser_toolbar_page_actions"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginTop="4dp"
+ android:scaleType="center"
+ app:layout_constraintEnd_toEndOf="@+id/mozac_browser_toolbar_background"
+ app:layout_constraintTop_toTopOf="parent"
+ mozac:actionContainerItemSize="48dp"
+ tools:layout_width="48dp" />
+
+ <!-- Browser Actions -->
+
+ <mozilla.components.browser.toolbar.internal.ActionContainer
+ android:id="@+id/mozac_browser_toolbar_browser_actions"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginTop="4dp"
+ app:layout_constraintEnd_toStartOf="@id/mozac_browser_toolbar_menu"
+ app:layout_constraintTop_toTopOf="parent"
+ mozac:actionContainerItemSize="48dp"
+ tools:layout_width="48dp" />
+
+ <!-- Menu -->
+
+ <mozilla.components.browser.menu.view.MenuButton
+ android:id="@+id/mozac_browser_toolbar_menu"
+ android:layout_width="36dp"
+ android:layout_height="48dp"
+ android:layout_marginTop="4dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <!-- Progress Bar -->
+
+ <ProgressBar
+ android:id="@+id/mozac_browser_toolbar_progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/mozac_browser_toolbar_progress_bar_height"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</mozilla.components.browser.toolbar.display.DisplayToolbarView>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_edittoolbar.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_edittoolbar.xml
new file mode 100644
index 0000000000..a45d0902e3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_edittoolbar.xml
@@ -0,0 +1,116 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/mozac_browser_toolbar_container"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:mozac="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:layout_height="56dp">
+
+ <ImageView
+ android:id="@+id/mozac_browser_toolbar_background"
+ android:layout_width="0dp"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:importantForAccessibility="no"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <ImageView
+ android:id="@+id/mozac_browser_toolbar_edit_icon"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:scaleType="center"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="@id/mozac_browser_toolbar_background"
+ app:srcCompat="@drawable/mozac_ic_search_24"
+ android:visibility="gone"
+ tools:ignore="ContentDescription"
+ android:layout_marginTop="8dp" />
+
+ <mozilla.components.browser.toolbar.internal.ActionContainer
+ android:id="@+id/mozac_browser_toolbar_edit_actions_start"
+ android:layout_width="wrap_content"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/mozac_browser_toolbar_edit_icon"
+ mozac:actionContainerItemSize="56dp"
+ tools:layout_width="56dp" />
+
+ <mozilla.components.ui.autocomplete.InlineAutocompleteEditText
+ android:id="@+id/mozac_browser_toolbar_edit_url_view"
+ android:layout_width="0dp"
+ android:layout_marginTop="8dp"
+ android:layout_height="40dp"
+ android:width="100dp"
+ android:height="100dp"
+ android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
+ android:inputType="textUri|text"
+ android:lines="1"
+ android:gravity="center_vertical"
+ android:background="#00000000"
+ android:textSize="15sp"
+ app:layout_goneMarginStart="8dp"
+ app:layout_constraintStart_toEndOf="@id/mozac_browser_toolbar_edit_actions_start"
+ app:layout_constraintEnd_toStartOf="@id/mozac_browser_toolbar_erase_view"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <ImageView
+ android:id="@+id/mozac_browser_toolbar_erase_view"
+ android:layout_width="44dp"
+ android:layout_height="44dp"
+ android:contentDescription="@string/mozac_clear_button_description"
+ android:scaleType="center"
+ app:srcCompat="@drawable/mozac_ic_cross_circle_fill_20"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/mozac_browser_toolbar_edit_url_view"
+ app:layout_constraintEnd_toStartOf="@id/mozac_browser_action_separator"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <View
+ android:id="@+id/mozac_browser_action_separator"
+ android:layout_width="@dimen/mozac_browser_toolbar_page_action_separator_width"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:visibility="gone"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ app:layout_constraintStart_toEndOf="@+id/mozac_browser_toolbar_erase_view"
+ app:layout_constraintEnd_toStartOf="@id/mozac_browser_toolbar_edit_actions_end"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <mozilla.components.browser.toolbar.internal.ActionContainer
+ android:id="@+id/mozac_browser_toolbar_edit_actions_end"
+ android:layout_width="wrap_content"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/mozac_browser_toolbar_clear_view"
+ mozac:actionContainerItemSize="40dp"
+ tools:layout_width="48dp" />
+
+ <ImageView
+ android:id="@+id/mozac_browser_toolbar_clear_view"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:width="100dp"
+ android:height="100dp"
+ android:contentDescription="@string/mozac_clear_button_description"
+ android:scaleType="center"
+ app:srcCompat="@drawable/mozac_ic_cross_circle_fill_24"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="@+id/mozac_browser_toolbar_background"
+ app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..eda99f879b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-am/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">ምናሌ</string>
+ <string name="mozac_clear_button_description">አጽዳ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">የመከታተያ ጥበቃ በርቷል</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">የክትትል ጥበቃ መከታተያዎችን አግዷል</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">የክትትል ጥበቃ ለዚህ ድረ-ገፅ ጠፍቷል</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">የድረ-ገፅ መረጃ</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">በመጫን ላይ</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">አንዳንድ ይዘቶች በራስ አጫውት ቅንብር ታግደዋል</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..14b40de38e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-an/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Borrar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protección contra seguimiento ye activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Las protección contra seguimiento ha blocau elementos de seguimiento</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protección de seguimiento ye desactivada en este puesto</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Información d’o puesto</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Se ye cargando</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..a4a01aa836
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ar/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">القائمة</string>
+ <string name="mozac_clear_button_description">امسح</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">الحماية من التعقّب مفعّلة</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">حجبت الحماية من التعقّب بعض المتعقّبات</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">عُطّلت الحماية من التعقب في هذا الموقع</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">معلومات الموقع</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">يُحمّل</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">حجب إعداد التشغيل التلقائي بعض المحتوى</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..6cb4e96a9a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ast/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Borrar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La proteición antirrastrexu ta activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La proteición antirrastrexu bloquió rastrexadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La proteición antirrastrexu ta desactivada nesti sitiu</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Información del sitiu</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargando</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">La configuración de la reproducción automática bloquió parte del conteníu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..0ccb692a99
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-az/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menyu</string>
+ <string name="mozac_clear_button_description">Təmizlə</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">İzlənmə Qoruması açıqdır</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">İzlənmə Qoruması izləyiciləri əngəllədi</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">İzlənmə Qoruması bu sayt üçün sönülüdür</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Sayt məlumatları</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Yüklənir</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..f3d084938f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-azb/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">منو</string>
+ <string name="mozac_clear_button_description">تمیزله</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ایزله‌مه قورونماسی آچیق</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ایزله‌مه قوروماسی ایزله‌ییجی‌لری مسدود ائلیب</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">بو سایت اوچون ایزله‌مه قورونماسی باغلیدیر.</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">سایت بیلگی‌لری</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">دولور</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">بعضی موحتوالار اوتوماتیک‌چال تنظیمی طرفیندن مسدود اولموش</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..037114093f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ban/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Puyung</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Jantosang dumun</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..7cdd7f116b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-be/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Меню</string>
+ <string name="mozac_clear_button_description">Ачысціць</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ахова ад сачэння ўключана</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ахова ад сачэння перапыніла высочвальнікаў</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ахова ад сачэння выключана на гэтым сайце</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Інфармацыя пра сайт</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Загрузка</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Некаторае змесціва было заблакавана наладамі аўтапрайгравання</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..e6e6ae23e5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bg/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Меню</string>
+ <string name="mozac_clear_button_description">Изчистване</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Защита от проследяване включена</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Защитата от проследяване е спряла проследяване</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Защитата от проследяване е изключена за сайта</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Показване на информация за сайта</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Зареждане</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Част от съдържанието е спряно от настройките за автоматично възпроизвеждане</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..8c93a8c6e7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bn/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">মেনু</string>
+ <string name="mozac_clear_button_description">পরিষ্কার করুন</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ট্র্যাকিং সুরক্ষা চালু আছে</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ট্র্যাকিং সুরক্ষা ট্র্যাকারদের অবরুদ্ধ করেছে</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">এই সাইটের জন্য ট্র্যাকিং সুরক্ষা বন্ধ</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">সাইটের তথ্য</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">লোড হচ্ছে</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">কিছু বিষয়বস্তু অটোপ্লে সেটিং দ্বারা ব্লক করা হয়েছে</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..189cb05917
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-br/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Lañser</string>
+ <string name="mozac_clear_button_description">Skarzhañ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Gweredekaet eo ar gware heuliañ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Stanket ez eus bet heulierien gant ar gwarez heuliañ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Diweredekaet eo bet ar gwarez heuliañ evit al lecʼhienn-mañ</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Titouroù al lecʼhienn</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">O kargañ</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Elfennoù ’zo a zo bet stanket gant arventenn al lenn emgefreek</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..2542f069fc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-bs/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meni</string>
+ <string name="mozac_clear_button_description">Očisti</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Zaštita od praćenja uključena</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Zaštita od praćenja je blokirala pratioce</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Zaštita od praćenja je isključena za ovu stranicu</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informacije o stranici</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Učitavanje</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Neki sadržaj je blokiran postavkom automatske reprodukcije</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..44f350dcf2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ca/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Esborra</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protecció contra el seguiment està activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protecció contra el seguiment ha blocat elements de seguiment coneguts</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">S’ha desactivat la protecció contra el seguiment per a aquest lloc</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informació del lloc</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">S’està carregant</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Una part del contingut s’ha blocat per la configuració de la reproducció automàtica</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..b3cd42e4f8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cak/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">K\'utsamaj</string>
+ <string name="mozac_clear_button_description">Tijosq\'ïx</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Tzijïl ri Chajinïk Chuwäch Ojqanïk</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Eruq\'aton ojqanela\' ri i Chajinïk chuwäch Ojqanïk</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Chupül ri Chajinïk chuwäch Ojqanem pa re ruxaq re\'</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Rutzijol ruxaq</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Nusamajij</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Jun peraj chi re ri rupam xq\'at ruma ri runuk\'ulem ri ruyon nitzij</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..4a03205a32
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Panas</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ang Tracking Protection on</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ang Tracking Protection nibara ug tracker</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Wala\'y Tracking Protection ani nga site</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Detalye sa site</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Loading</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Ang uban content gibara sa setting sa autoplay</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..95e99bea91
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">پێڕست</string>
+ <string name="mozac_clear_button_description">پاککردنەوە</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">پارێزگاری لە چاودێری کارایە</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">پارێزگاری چاودێری توانی چەند چاودێریکەرێک بلۆک بکات</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">پارێزگاری لە چاودێری ناکارایە بۆ ئەم ماڵپەڕە</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">زانیاری ماڵپەڕ</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">باردەکرێت</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">هەندێک ناوەڕۆک بلۆک کران لە لایەن ڕێکخستنی خۆپێکردنەوە</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..5c1db9a3b0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-co/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Interfaccia</string>
+ <!-- Content description: For the clear URL text button. -->
+ <string name="mozac_clear_button_description">Squassà</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">A prutezzione contr’à u spiunagiu hè attivata</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">A prutezzione contr’à u spiunagiu hà bluccatu perseguitatori</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">A prutezzione contr’à u spiunagiu hè disattivata per stu situ</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Infurmazioni nant’à u situ</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Caricamentu…</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Certi cuntenuti sò stati bluccati da a preferenza di lettura autumatica</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..10675ef8b8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cs/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Nabídka</string>
+ <string name="mozac_clear_button_description">Vymazat</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ochrana proti sledování je zapnuta</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ochrana proti sledování zablokovala sledovací prvky</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ochrana proti sledování je pro tento web vypnuta</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informace o stránce</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Načítání</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Část obsahu byla zablokována nastavením automatického přehrávání</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..8545de8b42
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-cy/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Dewislen</string>
+ <string name="mozac_clear_button_description">Clirio</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Mae Diogelwch rhag Tracio ymlaen</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Mae Diogelwch rhag Tracio wedi rhwystro tracwyr</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Mae Diogelwch rhag Tracio wedi ei ddiffodd ar gyfer y wefan hon</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Manylion y wefan</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Llwytho</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Mae rhywfaint o gynnwys wedi’i rwystro gan osodiadau awtochwarae</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..fff93d6f89
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-da/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Ryd</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Beskyttelse mod sporing er slået til</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Beskyttelse mod sporing har blokeret sporings-tjenester</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Beskyttelse mod sporing er slået fra for dette websted</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Information om webstedet</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Indlæser</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Noget indhold er blevet blokeret af indstillingen for automatisk afspilning</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..51e545591b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-de/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menü</string>
+ <string name="mozac_clear_button_description">Leeren</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Schutz vor Aktivitätenverfolgung ist an</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Der Tracking-Schutz hat Tracker blockiert</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Der Tracking-Schutz ist für diese Website deaktiviert</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Seiteninformation</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Wird geladen</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Einige Inhalte wurden durch die Einstellung zur automatischen Wiedergabe blockiert</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..3b22b7bcb4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meni</string>
+ <string name="mozac_clear_button_description">Wuprozniś</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Slědowański šćit jo zašaltowany</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Slědowański šćit jo blokěrował pśeslědowaki</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Slědowański šćit jo něnto znjemóžnjony za toś to sedło</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Sedłowe informacije</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Zacytujo se</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Někake wopśimjeśe jo se pśez wótgrawańske nastajenje zablokěrował</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..69d6e8c949
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-el/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Μενού</string>
+ <string name="mozac_clear_button_description">Απαλοιφή</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Η προστασία από καταγραφή είναι ενεργή</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Η προστασία από καταγραφή έχει αποκλείσει ιχνηλάτες</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Η προστασία από καταγραφή είναι ανενεργή για τον ιστότοπο</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Πληροφορίες ιστοτόπου</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Φόρτωση</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Ορισμένο περιεχόμενο έχει αποκλειστεί από τη ρύθμιση αυτόματης αναπαραγωγής</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..63bd4a5769
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Clear</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Tracking Protection is on</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Tracking Protection has blocked trackers</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Tracking Protection is off for this site</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Site information</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Loading</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Some content has been blocked by the autoplay setting</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..63bd4a5769
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Clear</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Tracking Protection is on</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Tracking Protection has blocked trackers</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Tracking Protection is off for this site</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Site information</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Loading</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Some content has been blocked by the autoplay setting</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..19f08c2553
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-eo/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menuo</string>
+ <string name="mozac_clear_button_description">Viŝi</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Protekto kontraŭ spurado ŝaltita</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protekto kontraŭ spurado blokis spurilojn</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Protekto kontraŭ spurado malŝaltita por tiu ĉi retejo</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informo pri retejo</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Ŝargado</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Parto de la enhavo estis blokita de la agordo pri aŭtomata ludado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..a8002e20c1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Eliminar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protección contra rastreo está habilitada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protección contra rastreo bloqueó los rastreadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protección de rastreo ahora está deshabilitada para este sitio</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Información del sitio</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargando</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Se bloquearon algunos contenidos debido a la configuración de reproducción automática</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..d949ef8b58
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Limpiar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Protección de seguimiento activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La Protección de seguimiento ha bloqueado rastreadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Protección de seguimiento desactivada para este sitio</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Información del sitio</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargando</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Algunos contenidos han sido bloqueados por los ajustes de reproducción automática</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..f4e14a2865
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Limpiar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protección contra rastreo está activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protección contra rastreo ha bloqueado rastreadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protección contra rastreo está desactivada para este sitio web</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Información del sitio</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargando</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Algunos contenidos han sido bloqueados por los ajustes de reproducción automática</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..cd4388769d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Limpiar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protección contra rastreo está activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protección contra rastreo ha bloqueado rastreadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protección contra rastreo está desactivada para este sitio</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Información del sitio</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargando</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Parte del contenido ha sido bloqueado por la configuración de reproducción automática</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..f4e14a2865
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-es/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Limpiar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protección contra rastreo está activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protección contra rastreo ha bloqueado rastreadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protección contra rastreo está desactivada para este sitio web</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Información del sitio</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargando</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Algunos contenidos han sido bloqueados por los ajustes de reproducción automática</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..7fb1096fd7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-et/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menüü</string>
+ <string name="mozac_clear_button_description">Tühjenda</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Jälitamisvastane kaitse on sees</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Jälitamisvastane kaitse on jälitajaid blokkinud</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Täiustatud jälitamisvastane kaitse on sellel saidil väljas</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Saidi teave</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Laadimine</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Osa sisu on automaatse esitamise sättega blokitud</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..5a1ee5a682
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-eu/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menua</string>
+ <string name="mozac_clear_button_description">Garbitu</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Jarraipenaren babesa gaituta dago</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Jarraipenaren babesak jarraipen-elementuak blokeatu ditu</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Jarraipenaren babesa desgaituta dago webgune honetarako</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Gunearen informazioa</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Kargatzen</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Eduki batzuk blokeatu egin dira erreprodukzio automatikoko ezarpenetan oinarrituta</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..f381a2069f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fa/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">منو</string>
+ <string name="mozac_clear_button_description">پاک کردن</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">حفاظت در برابر ردیابی روشن است</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">حفاظت در برابر ردیابی، ردیاب‌ها را مسدود کرده است</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">حفاظت در برابر ردیابی برای این پایگاه خاموش است</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">اطلاعات پایگاه</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">در حال بار کردن</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">برخی از محتواها توسط تنظیمات پخش خودکار مسدود شده‌اند</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..b774316dfb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ff/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Dosol</string>
+ <string name="mozac_clear_button_description">Momtu</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ndeenka Dewindol nani e</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ndeenka Dewindol faliima rewindotooɓe</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ndeenka Dewindol ko ko ñifi e ndee lowre</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Humpito lowre</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Nana loowa</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..53206238cc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fi/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Valikko</string>
+ <string name="mozac_clear_button_description">Tyhjennä</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Seurannan suojaus on päällä</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Seurannan suojaus on estänyt seuraimia</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Seurannan suojaus ei ole käytössä tällä sivustolla</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Sivustotiedot</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Ladataan</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Automaattisen toiston asetus on estänyt osan sisällöstä</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..13e1cf9c99
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fr/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Effacer</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protection contre le pistage est activée</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protection contre le pistage a bloqué des traqueurs</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protection contre le pistage est désactivée pour ce site</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informations sur le site</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Chargement en cours</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Certains contenus ont été bloqués par le paramètre de lecture automatique</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..40ef2bdb1c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fur/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menù</string>
+ <string name="mozac_clear_button_description">Nete</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protezion da lis spiis e je ative</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protezion da lis spiis e à blocât spiis</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protezion da lis spiis e je disativade par chest sît</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informazions sît</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Daûr a cjamâ</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Cualchi contignût al è stât blocât de impostazion pe riproduzion automatiche</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..d00bd6b3c1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Wiskje</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Beskerming tsjin folgjen is ynskeakele</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Beskerming tsjin folgjen hat trackers blokkearre</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Beskerming tsjin folgjen is út foar dizze website</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Website-ynformaasje</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Lade</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Guon ynhâld is blokkearre troch de ynstelling foar automatysk ôfspyljen</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..ba4f41d62f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Roghchlár</string>
+ <string name="mozac_clear_button_description">Bánaigh</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Tá Cosaint ar Lorgaireacht ar siúl</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Chuir Cosaint ar Lorgaireacht cosc ar lorgairí</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Tá Cosaint ar Lorgaireacht múchta don suíomh seo</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Eolas faoin suíomh</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Á lódáil</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..d694698b3d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gd/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Clàr-taice</string>
+ <string name="mozac_clear_button_description">Falamhaich</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Tha an dìon o thracadh air</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Bhac gleus an dìon o thracadh tracaichean</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Tha an dìon o thracadh dheth air an làrach seo</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Fiosrachadh mun làrach</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Ga luchdadh</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Chaidh cuid dhen t-susbaint a bhacadh an cois roghainn na fèin-chluich</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..c5ca829d30
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gl/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Limpar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Protección contra o rastrexo activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">A protección contra o rastrexo bloqueou rastrexadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">A protección contra o rastrexo está desactivada para este sitio</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Información sobre o sitio</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">A cargar</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">A configuración de reprodución automática bloqueou algúns contidos</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..8926b42b0a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gn/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Poravorã</string>
+ <string name="mozac_clear_button_description">Mopotĩ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Tapykuehoha ñemo’ã oñemyandy</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ñemo’ã jehekaha ojoko tapykuehohápe</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ñemo’ã jehekaha ndoikovéima ko tendápe g̃uarã</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Marandu tenda rehegua</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Henyhẽhína</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Ojejoko ndahetái tetepy ñemboheta ijeheguíva ñemboheko rupive</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..a6ff2d26b0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">મેનુ</string>
+ <string name="mozac_clear_button_description">સાફ કરો</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ટ્રેકિંગ સુરક્ષા ચાલુ છે</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ટ્રેકિંગ સુરક્ષા દ્વારા ટ્રેકર્સને અવરોધિત કરવામાં આવ્યા છે</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">આ સાઇટ માટે ટ્રેકિંગ સુરક્ષા બંધ છે</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">સાઇટ માહિતી</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">લોડ કરી રહ્યું છે</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..2d3c7d9b42
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">मेन्यू</string>
+ <string name="mozac_clear_button_description">साफ करें</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ट्रैकिंग सुरक्षा चालू है</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ट्रैकिंग सुरक्षा ने ट्रैकरों को अवरुद्ध कर दिया है</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">इस साइट के लिए ट्रैकिंग सुरक्षा बंद है</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">साइट सूचना</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">लोड हो रहा है</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..b02bc03a94
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hil/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Klaro</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Loading</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..420583468b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hr/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Izbornik</string>
+ <string name="mozac_clear_button_description">Izbriši</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Zaštita od praćenja je uključena</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Zaštita od praćenja je blokirala programe za praćenje</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Zaštita od praćenja je isključena za ovu stranicu</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informacije o web mjestu</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Učitavanje</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Neki sadržaji su blokirani zbog postavki automatske reprodukcije</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..a11de2c512
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meni</string>
+ <string name="mozac_clear_button_description">Wuprózdnić</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Slědowanski škit je zmóžnjeny</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Slědowanski škit je přesćěhowaki blokował</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Slědowanski škit je znjemóžnjeny za tute sydło</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Sydłowe informacije</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Začituje so</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Někajki wobsah je so přez wothrawanske nastajenje zablokował</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..cc8955eddc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hu/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menü</string>
+ <string name="mozac_clear_button_description">Törlés</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Követés elleni védelem bekapcsolva</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">A követés elleni védelem nyomkövetőket blokkolt</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">A követés elleni védelem le van tiltva ezen az oldalon</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Oldalinformációk</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Betöltés</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Bizonyos tartalmakat letiltott az automatikus lejátszási beállítás</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..c6cfc9bd94
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Ցանկ</string>
+ <string name="mozac_clear_button_description">Մաքրել</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Հետագծման պաշտպանությունը միաց. է</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Հետագծման պաշտպանությունն արգելափակել է հետագծիչներին</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Հետագծման պաշտպանությունը անջատված է այս կայքի համար</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Կայքի տեղեկությունները</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Բեռնում է</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Որոշ բովանդակություն արգելափակվել է ինքնանվագարկման կարգավորումով</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..1a929bb9ed
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ia/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Vacuar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Protection contra le traciamento active</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Le protection contra le traciamento ha blocate traciatores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Le protection contra le traciamento es disactivate pro iste sito.</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informationes del sito</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargamento</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Alcun contento ha essite blocate per le parametros del autoreproduction</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..2d7074c6ba
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-in/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Bersihkan</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Perlindungan Pelacakan aktif</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Perlindungan Pelacakan telah memblokir pelacak</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Perlindungan Pelacakan dinonaktifkan untuk situs ini</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informasi situs</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Memuat</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Beberapa konten telah diblokir dengan pengaturan putar-otomatis</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..a40c2a72d4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-is/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Valmynd</string>
+ <string name="mozac_clear_button_description">Hreinsa</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Vörn gegn gagnasöfnun virk</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Vörn gegn gagnasöfnun hefur lokað á gagnasafnara</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Vörn gegn gagnasöfnun er ekki virk fyrir þetta svæði</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Upplýsingar um vefsvæði</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Hleður</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Lokað hefur verið sumt efni með stillingum fyrir sjálfvirka afspilun</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..a443d319ae
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-it/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Cancella</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protezione antitracciamento è attiva</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protezione antitracciamento ha bloccato contenuti traccianti</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protezione antitracciamento è disattivata per questo sito</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informazioni sito</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Caricamento…</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Alcuni contenuti sono stati bloccati dall’impostazione per la riproduzione automatica</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..22c7da4e39
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-iw/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">תפריט</string>
+ <string name="mozac_clear_button_description">ניקוי</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">הגנת מעקב פעילה</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">הגנת מעקב חסמה רכיבי מעקב</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">הגנת מעקב כבויה עבור אתר זה</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">פרטי האתר</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">בטעינה</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">תוכן מסויים נחסם על־ידי ההגדרה של הניגון האוטומטי</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..f1c8802dcd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ja/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">メニュー</string>
+ <string name="mozac_clear_button_description">消去</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">トラッキング防止はオンです</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">トラッキング防止によりトラッカーをブロックしました</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">このサイトではトラッキング防止がオフです</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">サイト情報</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">読み込み中</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">一部のコンテンツは自動再生設定によってブロックされています</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..bf438a8ed7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ka/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">მენიუ</string>
+ <string name="mozac_clear_button_description">გასუფთავება</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">თვალთვალისგან დაცვა ჩართულია</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">თვალთვალისგან დაცვამ შეზღუდა მეთვალყურეები</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">თვალთვალისგან დაცვა გამორთულია ამ საიტზე</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">საიტის მონაცემები</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">იტვირთება</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">ზოგიერთი მასალა შეიზღუდა თვითგაშვების პარამეტრებით</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..e4ad1073b0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menyu</string>
+ <string name="mozac_clear_button_description">Tazalaw</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Baqlaw qorǵanıwı qosılǵan</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Baqlaw qorǵanıw funkciyası trekkerlerdi blokladı</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Bul sayt ushın baqlaw qorǵanıwı óshirilgen</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Sayt maǵlıwmatları</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Júklenbekte</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Ayrım kontentler avtomatik sazlawlar tárepinen bloklanǵan</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..9ded9a9925
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kab/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Umuɣ</string>
+ <string name="mozac_clear_button_description">Sfeḍ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ammesten mgal aḍfaṛ yermed</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ammesten mgal aḍfaṛ yessewḥel ineḍfaṛen</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ammesten mgal aḍfaṛ insa akka tura i usmel-a</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Asmel n telɣut</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Asali</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Yettusewḥel kra n yigburen s aɣewwar n tɣuri tawurmant</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..b066e651a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kk/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Мәзір</string>
+ <string name="mozac_clear_button_description">Тазарту</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Бақылаудан қорғаныс іске қосулы</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Бақылаудан қорғаныс трекерлерді бұғаттады</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Бақылаудан қорғаныс бұл сайт үшін сөндірілген.</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Сайт ақпараты</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Жүктелуде</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Кейбір құрама автоойнату баптауларымен бұғатталған</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..29542924a0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menû</string>
+ <string name="mozac_clear_button_description">Paqij bike</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Parastina ji Şopandinê vekirî ye</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Parastina ji şopandinê şopdar asteng kirin</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Parastina ji şopadinê, ji bo vê malperê girtî ye</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Agahiyên malperê</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Tê barkirin</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Hin naverok ji aliyê eyara lêdana-otomatîk ve hatin astengkirin</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..ec07a646a6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-kn/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">ಪರಿವಿಡಿ</string>
+ <string name="mozac_clear_button_description">ಅಳಿಸು</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ಜಾಡು ಇರಿಸುವಿಕೆ ಇಂದ ರಕ್ಷಣೆ ಶುರುವಾಗಿದೆ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ಟ್ರ್ಯಾಕಿಂಗ್ ಪ್ರೊಟೆಕ್ಷನ್ ಟ್ರ್ಯಾಕರ್‌ಗಳನ್ನು ನಿರ್ಬಂಧಿಸಿದೆ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ಈ ಸೈಟ್‌ಗಾಗಿ ಟ್ರ್ಯಾಕಿಂಗ್ ಪ್ರೊಟೆಕ್ಷನ್ ಆಫ್ ಆಗಿದೆ</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">ತಾಣದ ಮಾಹಿತಿ</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">ಲೋಡ್ ಆಗುತ್ತಿದೆ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..365b186580
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ko/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">메뉴</string>
+ <!-- Content description: For the clear URL text button. -->
+ <string name="mozac_clear_button_description">지우기</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">추적 방지 기능이 켜져 있음</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">추적 방지 기능이 추적기를 차단함</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">이 사이트에 추적 방지 기능이 꺼져 있음</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">사이트 정보</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">읽는 중</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">자동 재생 설정에 의해 일부 콘텐츠가 차단되었습니다.</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ldrtl/dimens.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ldrtl/dimens.xml
new file mode 100644
index 0000000000..a36c7c4366
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ldrtl/dimens.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- DisplayToolbar -->
+ <dimen name="mozac_browser_toolbar_origin_padding_end">16dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..31fd025526
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lij/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menû</string>
+ <string name="mozac_clear_button_description">Scancella</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Proteçion anti-traciamento açeiza</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">A proteçion anti-traciamento a l\'à blocou di traciatoî</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">A proteçion anti-traciamento a l\'é asmortâ pe sto scito</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informaçioin do scito</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Carego</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..d9327a731d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lo/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">ເມນູ</string>
+ <string name="mozac_clear_button_description">ລົບລ້າງ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ການປ້ອງກັນການຕິດຕາມກຳລັງເປີດຢູ່</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ການປ້ອງກັນການຕິດຕາມໄດ້ບັອກຕົວຕິດຕາມ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ການປ້ອງກັນການຕິດຕາມໄດ້ປິດສຳລັບເວັບໄຊທນີ້</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">ຂໍ້ມູນເວັບໄຊ</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">ກຳລັງໂຫລດ</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">ເນື້ອຫາບາງອັນຖືກບລັອກໂດຍການຕັ້ງຄ່າການຫຼີ້ນອັດຕະໂນມັດ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..433b57f18e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-lt/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meniu</string>
+ <string name="mozac_clear_button_description">Išvalyti</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Apsauga nuo stebėjimo įjungta</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Apsaugo nuo stebėjimo užblokavo stebėjimo elementus</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Apsauga nuo stebėjimo šioje svetainėje išjungta</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Svetainės informacija</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Įkeliama</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Dalį turinio užblokavo automatinio grojimo nuostatos</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..1636f653d5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-mix/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Katsi</string>
+ <string name="mozac_clear_button_description">Ku^un</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Chika va^a ña sau</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Chika va^a ña sau</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Chika va^a ña sau nu pagina yo^o </string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Tu^tu sitio yo^o</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Sachuin</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Ma ku kunu ña ku reproducción automática</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..9f376452a4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ml/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">മെനു</string>
+ <string name="mozac_clear_button_description">മായ്ക്കുക</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ട്രാക്കിങ്ങ് സംരക്ഷണം ഓൺ ആണ്</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ട്രാക്കിംഗ് സംരക്ഷണം ട്രാക്കറുകളെ തടഞ്ഞു</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ഈ സൈറ്റിന് ട്രാക്കിംഗ് പരിരക്ഷ ഇപ്പോൾ ഓഫാണ്</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">സൈറ്റ് വിവരങ്ങള്‍</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">ലഭ്യമാക്കുന്നു</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..5430ce7327
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-mr/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">मेनू</string>
+ <string name="mozac_clear_button_description">साफ करा</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ट्रॅकिंग संरक्षण चालू आहे</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ट्रॅकिंग संरक्षणने ट्रॅकर्स अवरोधित केले आहेत</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">या साइटसाठी ट्रॅकिंग संरक्षण बंद आहे</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">साईट माहिती</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">लोड होत आहे</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..006be61ee2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-my/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">စာရင်း</string>
+ <string name="mozac_clear_button_description">ရှင်းလင်းပါ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ခြေရာခံကာကွယ်မှုကို ဖွင့်ထားသည်</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ခြေရာခံကာကွယ်မှုသည် ခြေရာခံသူများကိုပိတ်ဆို့ထားသည်။</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ဒီ site အတွက်အကာအကွယ်ပေးမှုကိုပိတ်ထားသည်</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">ဆိုက်အချက်အလက်</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">အလုပ်လုပ်နေတယ်</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">အလိုအလျောက်ဖွင့်ခြင်း ဆက်တင်မှ အကြောင်းအရာအချို့အား ပိတ်ထားသည်။</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..f9cb99c55d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meny</string>
+ <string name="mozac_clear_button_description">Tøm</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Sporingsbeskyttelse er på</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Sporingsbeskyttelse har blokkert sporere</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Sporingsbeskyttelse er slått av for dette nettstedet</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informasjon om nettstedet</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Laster</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Noe av innholdet er blokkert av autoavspillings-innstillingene </string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..6a4341aafd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">मेनु</string>
+ <string name="mozac_clear_button_description">खाली गर्नुहोस्</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ट्र्याकिंग संरक्षण सक्रिय छ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ट्र्याकिङ्ग सुरक्षाले ट्रयाकरहरुलाई रोकेको छ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">हाल यस साइटको लागी ट्रयाकिङ् सुरक्षा बन्द गरिएको छ।</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">साइट जानकारी</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">लोड हुँदैछ</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">केहि सामग्री स्वतः प्ले सेटिङ्ग द्वारा रोकिएको छ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..b6e9b732c1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nl/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Wissen</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Bescherming tegen volgen is ingeschakeld</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Bescherming tegen volgen heeft trackers geblokkeerd</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Bescherming tegen volgen is uit voor deze website</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Website-informatie</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Laden</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Sommige inhoud is geblokkeerd door de instelling voor automatisch afspelen</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..c77b3373d0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meny</string>
+ <!-- Content description: For the clear URL text button. -->
+ <string name="mozac_clear_button_description">Tøm</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Sporingsvern er på</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Sporingsvern har blokkert sporarar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Sporingsvern er slått av for denne nettstaden</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informasjon om nettstaden</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Lastar</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Noko innhald har vorte blokkert av autoavspelings-innstillingane</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..4cc824a694
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-oc/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menú</string>
+ <string name="mozac_clear_button_description">Escafar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La proteccion contra lo seguiment es activada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La proteccion contra lo seguiment a blocat de traçadors</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La proteccion contra lo seguiment es desactivada</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informacions del site</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargament</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Una part del contengut es estat blocat per la configuracion de la lectura automatica</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..9ca67e8959
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-or/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">ମେନୁ</string>
+ <string name="mozac_clear_button_description">ଖାଲି କରନ୍ତୁ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ଟ୍ରାକିଂ ସୁରକ୍ଷା ଚାଲୁଛି</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ଟ୍ରାକିଂ ସୁରକ୍ଷା ଟ୍ରାକରଗୁଡ଼ିକୁ ରୋକି ଦେଇଛି</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ଟ୍ରାକିଂ ସୁରକ୍ଷା ଏହି ସାଇଟ ପାଇଁ ବନ୍ଦ ଅଛି</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">ସାଇଟ ସୂଚନା</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">ଧାରଣ କରୁଅଛି</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..6a85a4ac94
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">ਮੇਨੂ</string>
+ <string name="mozac_clear_button_description">ਸਾਫ਼ ਕਰੋ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ਟਰੈਕ ਕਰਨ ਤੋਂ ਸੁਰੱਖਿਆ ਚਾਲੂ ਹੈ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ਟਰੈਕਿੰਗ ਸੁਰੱਖਿਆ ਟਰੈਕਾਂ ਉੱਤੇ ਪਾਬੰਦੀ ਲਗਾਉਂਦੀ ਹੈ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ਇਸ ਸਾਈਟ ਲਈ ਟਰੈਕਿੰਗ ਸੁਰੱਖਿਆ ਬੰਦ ਹੈ</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">ਸਾਈਟ ਜਾਣਕਾਰੀ</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">ਕੁਝ ਸਮੱਗਰੀ ਨੂੰ ਆਪੇ-ਪਲੇਅ ਦੀ ਸੈਟਿੰਗ ਰਾਹੀਂ ਪਾਬੰਦੀ ਲਾਈ ਹੈ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..f48af403b2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">مینو</string>
+ <string name="mozac_clear_button_description">صاف کرو</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ٹوہ لاوݨ توں سرکھیا چالو اے</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ٹوہ لاوݨ توں کجھ روک لاۓ گئے ہن</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ٹوہ لاوݨ توں سرکھیا بند ہو گیا</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">سائٹ جاݨکاری</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">لوڈ کیتا جا رہا اے</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">خود بخود چلݨ نال کجھ وستوآں روکے گئے</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..e3a08b4901
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pl/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Wyczyść</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ochrona przed śledzeniem jest włączona</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ochrona przed śledzeniem zablokowała elementy śledzące</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ochrona przed śledzeniem jest wyłączona na tej witrynie</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informacje o witrynie</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Wczytywanie</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Część treści została zablokowana przez ustawienie automatycznego odtwarzania</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..2eb5de65bb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Limpar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Proteção contra rastreamento ativada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">A proteção contra rastreamento bloqueou rastreadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">A proteção contra rastreamento está desativada neste site</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informações do site</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Carregando</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Algum conteúdo foi bloqueado pela configuração de reprodução automática</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..c3bbee9e20
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Limpar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Proteção contra monitorização está ativada</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">A proteção contra a monitorização bloqueou rastreadores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">A proteção contra monitorização está desativada para este site</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informação do site</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">A carregar</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Algum conteúdo foi bloqueado pela configuração de reprodução automática</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..667b07fb64
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-rm/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Stizzar</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">La protecziun cunter il fastizar è activa</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">La protecziun cunter il fastizar ha bloccà fastizaders</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">La protecziun cunter il fastizar è deactivada per questa website</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Infurmaziuns davart la website</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Chargiar</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Tscherts cuntegns èn vegnids bloccads dal parameter da la reproducziun automatica</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..72dee0c902
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ro/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meniu</string>
+ <string name="mozac_clear_button_description">Șterge</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Protecția împotriva urmăririi este activată</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Protecția împotriva urmăririi a blocat elementele de urmărire</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Protecția împotriva urmăririi este dezactivată pentru acest site</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informații despre site</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Se încarcă</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..e80b24a880
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ru/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Меню</string>
+ <string name="mozac_clear_button_description">Очистить</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Защита от отслеживания включена</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Защита от отслеживания заблокировала трекеры</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Защита от отслеживания отключена для этого сайта</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Сведения о сайте</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Загрузка</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Некоторое содержимое было заблокировано настройками автовоспроизведения</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..a80afdc3a6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sat/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">ᱢᱮᱱᱩ</string>
+ <string name="mozac_clear_button_description">ᱯᱷᱟᱨᱪᱟ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ᱯᱟᱸᱡᱟ ᱟᱰ ᱪᱟᱹᱞᱩ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ᱯᱟᱸᱡᱟ ᱨᱚᱯᱷᱟ ᱯᱟᱧᱡᱟ ᱫᱟᱱᱟᱲ ᱠᱚ ᱟᱠᱚᱴ ᱠᱮᱜᱼᱟᱭ</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ᱯᱟᱸᱡᱟ ᱨᱚᱯᱷᱟ ᱵᱚᱸᱫᱚ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">ᱥᱟᱭᱤᱴ ᱨᱮᱭᱟᱜ ᱠᱷᱚᱵᱚᱨ</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">ᱞᱚᱰᱤᱝ</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">ᱟᱡ ᱛᱮ ᱮᱛᱦᱚᱵ ᱥᱟᱡᱟᱣ ᱠᱚ ᱠᱷᱟᱹᱛᱤᱨ ᱛᱮ ᱠᱤᱪᱷᱤ ᱡᱤᱱᱤᱥ ᱠᱚ ᱵᱞᱚᱠ ᱠᱟᱱᱟ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..5194a4765e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sc/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menù</string>
+ <string name="mozac_clear_button_description">Isbòida</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">S’amparu contra sa sighidura est ativu</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">S’amparu contra sa sighidura at blocadu sighidores</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Sa protetzione contra sa sighidura est disativada pro custu situ</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informatziones de su situ</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Carrighende</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..9f233e7acc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-si/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">වට්ටෝරුව</string>
+ <!-- Content description: For the clear URL text button. -->
+ <string name="mozac_clear_button_description">මකන්න</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ලුහුබැඳීමේ රැකවරණය සක්‍රියයි</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ලුහුබැඳීමේ රැකවරණයෙන් අවහිරයි</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">අඩවිය සඳහා ලුහුබැඳීමේ රැකවරණය අබලයි</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">අඩවියේ තොරතුරු</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">පූරණය වෙමින්</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">ස්වයං වාදන සැකසුම මගින් ඇතැම් අන්තර්ගත අවහිර කර ඇත</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..471775b209
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sk/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Ponuka</string>
+ <string name="mozac_clear_button_description">Vymazať</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ochrana pred sledovaním je zapnutá</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ochrana pred sledovaním zablokovala sledovacie prvky</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ochrana pred sledovaním je na tejto stránke vypnutá</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informácie o stránke</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Načítava sa</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Niektorý obsah bol zablokovaný nastavením automatického prehrávania</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..9a10e8b1c5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">مینیو</string>
+ <string name="mozac_clear_button_description">صاف کرو</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">سراغ کاری تحفظ چالو ہے</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">سراغ کاری تحفظ نے سُراغ رساں کوں بلاک کر ݙتا ہے</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">سراغ کاری تحفظ ایں سائٹ کیتے بند ہے</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">سائٹ ڄاݨکاری</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">لوڈ تھیندا پئے</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">کجھ مواد کوں آٹو پلے ترتیباں نال بلاک کر ݙتا ڳئے</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..5ae6ff80cb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sl/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meni</string>
+ <string name="mozac_clear_button_description">Počisti</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Zaščita pred sledenjem je vključena</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Zaščita pred sledenjem je zavrnila sledilce</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Zaščita pred sledenjem je za to stran izključena</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Podatki o strani</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Nalaganje</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Nastavitev samodejnega predvajanja je zavrnila nekaj vsebine</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..21085e31c7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sq/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Spastroje</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Mbrojtje Nga Gjurmimet është aktive</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Mbrojtja Nga Gjurmimet ka bllokuar gjurmues</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Mbrojtja Nga Gjurmimet është e çaktivizuar për këtë sajt</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Hollësi sajti</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Po ngarkohet</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Është bllokuar lëndë nga rregullimi i vetëluajtjes</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..5e70cab0c1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sr/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Мени</string>
+ <string name="mozac_clear_button_description">Обриши</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Заштита од праћења је укључена</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Заштита од праћења је блокирала пратиоце</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Заштита од праћења је искључена за ову страницу</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Информације о страници</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Учитавање</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Неки садржај је блокиран због подешавања аутоматске репродукције</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..0d3478fd00
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-su/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Beresihan</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Kilung Palacakan keur hurung</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Kilung Palacakan geus meungpeuk palacak</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Kilung Palacakan pareum pikeun ieu loka</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Émbaran loka</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Ngamuat</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Sababaraha kontén dipeungpeuk ku setélan otoplay</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..66022b1088
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Meny</string>
+ <string name="mozac_clear_button_description">Rensa</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Spårningsskydd är på</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Spårningsskydd har blockerat spårare</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Spårningsskydd är avstängt för den här webbplatsen</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Webbplatsinformation</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Laddar</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">En del innehåll har blockerats av inställningen för automatisk uppspelning</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..e6537d32b1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-szl/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Myni</string>
+ <string name="mozac_clear_button_description">Wypucuj</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ôchrōna ôd śledzynio je załōnczōno</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ôchrōna ôd śledzynio szperuje śledzōnce elymynty</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Na tyj strōnie ôchrōna ôd śledzynio je wyłōnczōno</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informacyje ô strōnie</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Ladowanie</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Nasztalowanie autōmatycznego puszczanio zaszperowało kōnsek zawartości</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..9b6e317f6c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ta/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">பட்டி</string>
+ <string name="mozac_clear_button_description">துடை</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">தடமறியல் பாதுகாப்பு இயக்கத்தில்</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">தடமறியல் பாதுகாப்பு தடமறிவான்களை முடக்கியது</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">தடமறியல் பாதுகாப்பு இத்தளத்தில் அணைக்கப்பட்டுள்ளது</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">தளத்தகவல்கள்</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">ஏற்றுகிறது</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">தன்னியக்க அமைப்பால் சில உள்ளடக்கம் தடுக்கப்பட்டுள்ளது</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..504c84b58d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-te/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">మెనూ</string>
+ <string name="mozac_clear_button_description">తుడిచివేయి</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ట్రాకింగ్ సంరక్షణ చేతనంగా ఉంది</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ట్రాకింగ్ సంరక్షణ ట్రాకర్లను నిరోధించింది</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">ఈ సైటుకి ట్రాకింగ్ సంరక్షణ ఆపివేయబడింది</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">సైటు సమాచారం</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">వస్తోంది</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">ఆటోప్లే అమరిక ద్వారా కొంత కంటెంట్ నిరోధించబడింది</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..af36f39670
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tg/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Меню</string>
+ <string name="mozac_clear_button_description">Пок кардан</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Муҳофизат аз пайгирӣ фаъол аст</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Муҳофизат аз пайгирӣ васоити пайгириро манъ кард</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Муҳофизат аз пайгирӣ барои ин сомона хомӯш аст</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Маълумот дар бораи сомона</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Бор шуда истодааст</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Баъзеи муҳтаво тавассути танзими пахши худкор манъ карда шуд</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..55f06924fa
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-th/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">เมนู</string>
+ <string name="mozac_clear_button_description">ล้าง</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">การป้องกันการติดตามเปิดอยู่</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">การป้องกันการติดตามได้ปิดกั้นตัวติดตาม</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">การป้องกันการติดตามปิดอยู่สำหรับไซต์นี้</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">ข้อมูลไซต์</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">กำลังโหลด</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">เนื้อหาบางส่วนถูกปิดกั้นด้วยการตั้งค่าเล่นอัตโนมัติ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..4e5e6b3300
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tl/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Burahin</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Nakabukas ang Tracking Protection</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">May naharang na mga tracker ang Tracking Protection</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Nakasara ang Tracking Protection sa site na ito</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Impormasyon sa site</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Naglo-load</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">May ilang content na naharang dahil sa autoplay setting</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..d9f3149d1b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tok/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_clear_button_description">o weka ale</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">lipu li ken ala lukin e sona sina</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">lipu li weka e lipu lukin</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">lipu ni la weka pi lipu lukin li lon ala</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">sona lipu</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">o awen</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..c7ede965df
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tr/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menü</string>
+ <string name="mozac_clear_button_description">Temizle</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">İzlenme koruması açık</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">İzlenme koruması, takip kodlarını engelledi</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Bu sitede izlenme koruması kapalı</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Site bilgileri</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Yükleniyor</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Otomatik oynatma ayarınız bazı içerikleri engelledi</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..ef86e00be9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-trs/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menû</string>
+ <string name="mozac_clear_button_description">Nā\'nïn\'</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Sa narrán riña sa naga\'nāj a</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Narrán sa dugumî ñù\' riña nej sa naga\'nāj a</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Nitāj si \'iaj sun sa narán riña sa naga\'nāj a riña sitiô nan</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Nuguan\' huā rayi\'î sitiô nan</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Hìaj ayì\'ij</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Huā da’āj nej sa mà riñaj narán gi’iaj guendâ nù sa duguachín man’an sa ni’io’</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..3a463ee5cf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tt/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Меню</string>
+ <string name="mozac_clear_button_description">Чистарту</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Күзәтелүдән Саклау кабызылган</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Күзәтүдән саклау күзәтеп торучыларны блоклады</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Бу сайт өчен Күзәтелүдән Саклау сүндерелгән</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Сайт турында мәгълүмат</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Йөкләү</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Автоуйнату көйләүләре аркасында кайбер эчтәлекләр блокланды</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..d451c5972c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Umuɣ</string>
+ <string name="mozac_clear_button_description">Sfeḍ</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Asmel n wasit</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Asali</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..59c98b81f7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ug/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">تىزىملىك</string>
+ <string name="mozac_clear_button_description">تازىلاش</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">ئىزلاشتىن توسۇش ئىقتىدارى ئوچۇق</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">ئىز قوغلاش قوغدىغۇچىسى ئىز قوغلىغۇچىلارنى توستى</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">بۇ توربېكەتكە نىسبەتەن ئىزلاش ئېتىلگەن</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">بېكەت ئۇچۇرى</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">يۈكلەۋاتىدۇ</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">بەزى مەزمۇنلار «ئاپتوماتىك قويۇش» تەرىپىدىن توسۇلدى</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..66c444cdb2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-uk/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Меню</string>
+ <string name="mozac_clear_button_description">Очистити</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Захист від стеження увімкнено</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Захист від стеження заблокував стеження</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Захист від стеження вимкнено для цього сайту</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Інформація про сайт</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Завантаження</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Деякий вміст заблоковано налаштуванням автовідтворення</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..78470d254c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-ur/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">مینیو</string>
+ <string name="mozac_clear_button_description">صاف کریں</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">سراغ کاری تحفظ چالو ہے</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">سراغ کاری تحفظ نے سُراغ رساں کو مسدود کردیا ہے</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">سراغ کاری تحفظ اس سائٹ کے لیئے بند ہے</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">سائٹ کی معلومات</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">لوڈ کر رہا ہے</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">کچھ مشمولات کو آٹو پلے سیٹنگ سے مسدود کردیا گیا ہے</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..ec69af71f6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-uz/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menyu</string>
+ <string name="mozac_clear_button_description">Tozalash</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Kuzatuvdan himoya yoniq</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Kuzatuvdan himoya funksiyasi kuzatuvchilarni blokladi</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Bu sayt uchun kuzatuvdan himoya funksiyasi oʻchirilgan</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Sayt maʼlumoti</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Yuklanmoqda</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Avtomatik ishga tushirish sozlamasi tufayli ayrim kontentlar bloklandi</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..15a625bcbc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-vec/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Pulisi</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ƚa protesion antitraciamento ƚa xe ativa</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ƚa protesion antitraciamento ƚa ga blocà contenudi tracianti</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ƚa protesion antitraciamento ƚa xe disativà par sto sito</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Informasioni sito</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Cargamento</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..e64db6cc20
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-vi/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Dọn dẹp</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Trình chống theo dõi đang bật</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Trình chống theo dõi đã chặn trình theo dõi</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Đã tắt Trình chống theo dõi cho trang web này</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Thông tin về trang web</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Đang tải</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Một số nội dung đã bị chặn bởi cài đặt tự động phát</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..6f46be1286
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-yo/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Mẹ́nù</string>
+ <string name="mozac_clear_button_description">Paárẹ́</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Ìtọpinpin Ìdàábòbò wà ní títàn </string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Ìtọpinpin ìdàábòbò ti dènà atọpinpin</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Ìtọpinpin ìdàábòbò ti di pípa fún ìkànnì yìí</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Ìfitóniléti ìkànnì</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Ó ń gbáradì</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Àwọn àkòónú kan ti di dídénà ààtò ìfi-ara-ẹni-ṣisẹ́</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..a82dafc588
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">菜单</string>
+ <string name="mozac_clear_button_description">清除</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">已开启跟踪保护</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">跟踪保护已拦截跟踪器</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">已关闭对此网站的跟踪保护</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">网站信息</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">正在加载</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">某些内容已被自动播放设置阻止</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..09884c0633
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">選單</string>
+ <string name="mozac_clear_button_description">清除</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">追蹤保護功能已開啟</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">追蹤保護功能已封鎖追蹤器</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">已關閉針對此網站的追蹤保護功能</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">網站資訊</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">載入中</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">某些內容已由自動播放設定封鎖</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values/attrs_browser_toolbar.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values/attrs_browser_toolbar.xml
new file mode 100644
index 0000000000..431bb6b080
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values/attrs_browser_toolbar.xml
@@ -0,0 +1,33 @@
+<?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>
+ <declare-styleable name="BrowserToolbar">
+ <attr name="browserToolbarHintColor" format="color"/>
+ <attr name="browserToolbarTextColor" format="color"/>
+ <attr name="browserToolbarTextSize" format="dimension"/>
+ <attr name="browserToolbarSecurityIcon" format="reference"/>
+ <attr name="browserToolbarInsecureColor" format="color"/>
+ <attr name="browserToolbarSecureColor" format="color"/>
+ <attr name="browserToolbarMenuColor" format="color"/>
+ <attr name="browserToolbarSuggestionBackgroundColor" format="color" />
+ <attr name="browserToolbarSuggestionForegroundColor" format="color" />
+ <attr name="browserToolbarClearColor" format="color"/>
+ <attr name="browserToolbarTrackingProtectionAndSecurityIndicatorSeparatorColor" format="color"/>
+ <attr name="browserToolbarFadingEdgeSize" format="dimension" />
+ <attr name="browserToolbarProgressBarGravity">
+ <enum name="bottom" value="0" />
+ <enum name="top" value="1" />
+ </attr>
+ </declare-styleable>
+
+ <declare-styleable name="BrowserToolbarSiteSecurityState">
+ <attr name="state_site_secure" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="ActionContainer">
+ <attr name="actionContainerItemSize" format="dimension" />
+ </declare-styleable>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values/dimens.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..23fd755686
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values/dimens.xml
@@ -0,0 +1,31 @@
+<?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>
+ <dimen name="mozac_browser_toolbar_default_toolbar_height">56dp</dimen>
+
+ <!-- DisplayToolbar -->
+ <dimen name="mozac_browser_toolbar_progress_bar_height">3dp</dimen>
+ <dimen name="mozac_browser_toolbar_icons_separator_height">24dp</dimen>
+ <dimen name="mozac_browser_toolbar_icons_separator_width">1dp</dimen>
+ <dimen name="mozac_browser_toolbar_page_action_separator_width">4dp</dimen>
+ <dimen name="mozac_browser_toolbar_url_fading_edge_size">24dp</dimen>
+ <dimen name="mozac_browser_toolbar_icon_padding">12dp</dimen>
+ <dimen name="mozac_browser_toolbar_icon_size">24dp</dimen>
+ <dimen name="mozac_browser_toolbar_menu_padding">16dp</dimen>
+ <dimen name="mozac_browser_toolbar_origin_padding_end">0dp</dimen>
+
+ <!-- EditToolbar -->
+ <dimen name="mozac_browser_toolbar_url_horizontal_padding">8dp</dimen>
+ <dimen name="mozac_browser_toolbar_url_vertical_padding">0dp</dimen>
+ <dimen name="mozac_browser_toolbar_url_gone_margin_end">8dp</dimen>
+ <dimen name="mozac_browser_toolbar_cancel_padding">16dp</dimen>
+
+ <dimen name="mozac_browser_toolbar_url_textsize">15sp</dimen>
+ <dimen name="mozac_browser_toolbar_title_textsize">15sp</dimen>
+ <dimen name="mozac_browser_toolbar_url_with_title_textsize">12sp</dimen>
+
+ <dimen name="mozac_browser_toolbar_pageaction_size">48dp</dimen>
+ <dimen name="mozac_browser_toolbar_browseraction_size">48dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values/ids.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values/ids.xml
new file mode 100644
index 0000000000..20e28a0c58
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values/ids.xml
@@ -0,0 +1,8 @@
+<?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>
+ <item name="mozac_browser_toolbar_title_view" type="id"/>
+ <item name="mozac_browser_toolbar_url_view" type="id"/>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/main/res/values/strings.xml b/mobile/android/android-components/components/browser/toolbar/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..3c2d7dcf1b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?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>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_toolbar_menu_button">Menu</string>
+ <string name="mozac_clear_button_description">Clear</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, but none trackers have been blocked or detected. -->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_no_trackers_blocked">Tracking Protection is on</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection enabled, and trackers have been blocked or detected.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_on_trackers_blocked1">Tracking Protection has blocked trackers</string>
+ <!-- Content description: For the tracking protection toolbar icon, it is set when the site has tracking protection disabled.-->
+ <string name="mozac_browser_toolbar_content_description_tracking_protection_off_for_a_site1">Tracking Protection is off for this site</string>
+ <!-- Content description: For the site security information icon (the site security icon).-->
+ <string name="mozac_browser_toolbar_content_description_site_info">Site information</string>
+ <!-- Announcement made by the screen reader when the progress bar is shown and a page is loading -->
+ <string name="mozac_browser_toolbar_progress_loading">Loading</string>
+ <!-- Content description: For the autoplay toolbar icon, it is set when the auto play permission is blocking content playing.-->
+ <string name="mozac_browser_toolbar_content_description_autoplay_blocked">Some content has been blocked by the autoplay setting</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt
new file mode 100644
index 0000000000..e9af1506d1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt
@@ -0,0 +1,350 @@
+/* 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.browser.toolbar
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.toolbar.AutocompleteDelegate
+import mozilla.components.concept.toolbar.AutocompleteResult
+import mozilla.components.support.test.mock
+import mozilla.components.ui.autocomplete.AutocompleteView
+import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import org.mockito.Mockito.atLeast
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.concurrent.Executor
+
+@ExperimentalCoroutinesApi // for runTest
+class AsyncFilterListenerTest {
+ @Test
+ fun `filter listener cancels prior filter executions`() = runTest {
+ val urlView: AutocompleteView = mock()
+ val filter: suspend (String, AutocompleteDelegate) -> Unit = mock()
+
+ val dispatcher = spy(
+ Executor {
+ it.run()
+ }.asCoroutineDispatcher(),
+ )
+
+ val listener = AsyncFilterListener(urlView, dispatcher, filter)
+
+ verify(dispatcher, never()).cancelChildren()
+
+ listener("test")
+
+ verify(dispatcher, atLeastOnce()).cancelChildren()
+ }
+
+ @Test
+ fun `filter delegate checks for cancellations before it runs, passes results to autocomplete view`() = runTest {
+ var filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
+ assertEquals("test", query)
+ delegate.applyAutocompleteResult(
+ AutocompleteResult(
+ input = "test",
+ text = "testing.com",
+ url = "http://www.testing.com",
+ source = "asyncTest",
+ totalItems = 1,
+ ),
+ )
+ }
+
+ val dispatcher = spy(
+ Executor {
+ it.run()
+ }.asCoroutineDispatcher(),
+ )
+
+ var didCallApply = 0
+
+ var listener = AsyncFilterListener(
+ object : AutocompleteView {
+ override val originalText: String = "test"
+
+ override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
+ assertEquals("asyncTest", result.source)
+ assertEquals("testing.com", result.text)
+ assertEquals(1, result.totalItems)
+ didCallApply += 1
+ }
+
+ override fun noAutocompleteResult() {
+ fail()
+ }
+ },
+ dispatcher,
+ filter,
+ this.coroutineContext,
+ )
+
+ verify(dispatcher, never()).isActive
+
+ async { listener("test") }.await()
+
+ // Checked if parent scope is still active. Somehow, each access to 'isActive' registers as 4?
+ verify(dispatcher, atLeast(4)).isActive
+ // Passed the result to the view's apply method exactly once.
+ assertEquals(1, didCallApply)
+
+ filter = { query, delegate ->
+ assertEquals("moz", query)
+ delegate.applyAutocompleteResult(
+ AutocompleteResult(
+ input = "moz",
+ text = "mozilla.com",
+ url = "http://www.mozilla.com",
+ source = "asyncTestTwo",
+ totalItems = 2,
+ ),
+ )
+ }
+ listener = AsyncFilterListener(
+ object : AutocompleteView {
+ override val originalText: String = "moz"
+
+ override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
+ assertEquals("asyncTestTwo", result.source)
+ assertEquals("mozilla.com", result.text)
+ assertEquals(2, result.totalItems)
+ didCallApply += 1
+ }
+
+ override fun noAutocompleteResult() {
+ fail()
+ }
+ },
+ dispatcher,
+ filter,
+ this.coroutineContext,
+ )
+
+ async { listener("moz") }.await()
+
+ verify(dispatcher, atLeast(8)).isActive
+ assertEquals(2, didCallApply)
+ }
+
+ @Test
+ fun `delegate discards stale results`() = runTest {
+ val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
+ assertEquals("test", query)
+ delegate.applyAutocompleteResult(
+ AutocompleteResult(
+ input = "test",
+ text = "testing.com",
+ url = "http://www.testing.com",
+ source = "asyncTest",
+ totalItems = 1,
+ ),
+ )
+ }
+
+ val dispatcher = Executor {
+ it.run()
+ }.asCoroutineDispatcher()
+
+ val listener = AsyncFilterListener(
+ object : AutocompleteView {
+ override val originalText: String = "nolongertest"
+
+ override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
+ fail()
+ }
+
+ override fun noAutocompleteResult() {
+ fail()
+ }
+ },
+ dispatcher,
+ filter,
+ this.coroutineContext,
+ )
+
+ listener("test")
+ }
+
+ @Test
+ fun `delegate discards stale lack of results`() = runTest {
+ val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
+ assertEquals("test", query)
+ delegate.noAutocompleteResult("test")
+ }
+
+ val dispatcher = Executor {
+ it.run()
+ }.asCoroutineDispatcher()
+
+ val listener = AsyncFilterListener(
+ object : AutocompleteView {
+ override val originalText: String = "nolongertest"
+
+ override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
+ fail()
+ }
+
+ override fun noAutocompleteResult() {
+ fail()
+ }
+ },
+ dispatcher,
+ filter,
+ this.coroutineContext,
+ )
+
+ listener("test")
+ }
+
+ @Test
+ fun `delegate passes through non-stale lack of results`() = runTest {
+ val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
+ assertEquals("test", query)
+ delegate.noAutocompleteResult("test")
+ }
+
+ val dispatcher = Executor {
+ it.run()
+ }.asCoroutineDispatcher()
+
+ var calledNoResults = 0
+ val listener = AsyncFilterListener(
+ object : AutocompleteView {
+ override val originalText: String = "test"
+
+ override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
+ fail()
+ }
+
+ override fun noAutocompleteResult() {
+ calledNoResults += 1
+ }
+ },
+ dispatcher,
+ filter,
+ this.coroutineContext,
+ )
+
+ async { listener("test") }.await()
+
+ assertEquals(1, calledNoResults)
+ }
+
+ @Test
+ fun `delegate discards results if parent scope was cancelled`() = runTest {
+ var preservedDelegate: AutocompleteDelegate? = null
+
+ val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
+ preservedDelegate = delegate
+ assertEquals("test", query)
+ delegate.applyAutocompleteResult(
+ AutocompleteResult(
+ input = "test",
+ text = "testing.com",
+ url = "http://www.testing.com",
+ source = "asyncTest",
+ totalItems = 1,
+ ),
+ )
+ }
+
+ val dispatcher = Executor {
+ it.run()
+ }.asCoroutineDispatcher()
+
+ var calledResults = 0
+ val listener = AsyncFilterListener(
+ object : AutocompleteView {
+ override val originalText: String = "test"
+
+ override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
+ assertEquals("asyncTest", result.source)
+ assertEquals("testing.com", result.text)
+ assertEquals(1, result.totalItems)
+ calledResults += 1
+ }
+
+ override fun noAutocompleteResult() {
+ fail()
+ }
+ },
+ dispatcher,
+ filter,
+ this.coroutineContext,
+ )
+
+ async {
+ listener("test")
+ listener("test")
+ }.await()
+
+ // This result application should be discarded, because scope has been cancelled by the second
+ // 'listener' call above.
+ preservedDelegate!!.applyAutocompleteResult(
+ AutocompleteResult(
+ input = "test",
+ text = "testing.com",
+ url = "http://www.testing.com",
+ source = "asyncCancelled",
+ totalItems = 1,
+ ),
+ )
+
+ assertEquals(2, calledResults)
+ }
+
+ @Test
+ fun `delegate discards lack of results if parent scope was cancelled`() = runTest {
+ var preservedDelegate: AutocompleteDelegate? = null
+
+ val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
+ preservedDelegate = delegate
+ assertEquals("test", query)
+ delegate.noAutocompleteResult("test")
+ }
+
+ val dispatcher = Executor {
+ it.run()
+ }.asCoroutineDispatcher()
+
+ var calledResults = 0
+ val listener = AsyncFilterListener(
+ object : AutocompleteView {
+ override val originalText: String = "test"
+
+ override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
+ fail()
+ }
+
+ override fun noAutocompleteResult() {
+ calledResults += 1
+ }
+ },
+ dispatcher,
+ filter,
+ this.coroutineContext,
+ )
+
+ async {
+ listener("test")
+ listener("test")
+ }.await()
+
+ // This "no results" call should be discarded, because scope has been cancelled by the second
+ // 'listener' call above.
+ preservedDelegate!!.noAutocompleteResult("test")
+
+ assertEquals(2, calledResults)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt
new file mode 100644
index 0000000000..3e535faa96
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt
@@ -0,0 +1,1044 @@
+/* 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.browser.toolbar
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewParent
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+import android.widget.ImageButton
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.inputmethod.EditorInfoCompat
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.toolbar.display.DisplayToolbar
+import mozilla.components.browser.toolbar.display.DisplayToolbarViews
+import mozilla.components.browser.toolbar.display.MenuButton
+import mozilla.components.browser.toolbar.edit.EditToolbar
+import mozilla.components.concept.toolbar.AutocompleteDelegate
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.concept.toolbar.Toolbar.SiteSecurity
+import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection
+import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior
+import mozilla.components.ui.widgets.behavior.ViewPosition
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class BrowserToolbarTest {
+
+ @Test
+ fun `display toolbar is visible by default`() {
+ val toolbar = BrowserToolbar(testContext)
+ assertTrue(toolbar.display.rootView.visibility == View.VISIBLE)
+ assertTrue(toolbar.edit.rootView.visibility == View.GONE)
+ }
+
+ @Test
+ fun `calling editMode() makes edit toolbar visible`() {
+ val toolbar = BrowserToolbar(testContext)
+ assertTrue(toolbar.display.rootView.visibility == View.VISIBLE)
+ assertTrue(toolbar.edit.rootView.visibility == View.GONE)
+
+ toolbar.editMode()
+
+ assertTrue(toolbar.display.rootView.visibility == View.GONE)
+ assertTrue(toolbar.edit.rootView.visibility == View.VISIBLE)
+ }
+
+ @Test
+ fun `calling displayMode() makes display toolbar visible`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.editMode()
+
+ assertTrue(toolbar.display.rootView.visibility == View.GONE)
+ assertTrue(toolbar.edit.rootView.visibility == View.VISIBLE)
+
+ toolbar.displayMode()
+
+ assertTrue(toolbar.display.rootView.visibility == View.VISIBLE)
+ assertTrue(toolbar.edit.rootView.visibility == View.GONE)
+ }
+
+ @Test
+ fun `back presses will not be handled in display mode`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.displayMode()
+
+ assertFalse(toolbar.onBackPressed())
+
+ assertTrue(toolbar.display.rootView.visibility == View.VISIBLE)
+ assertTrue(toolbar.edit.rootView.visibility == View.GONE)
+ }
+
+ @Test
+ fun `back presses will switch from edit mode to display mode`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.editMode()
+
+ assertTrue(toolbar.display.rootView.visibility == View.GONE)
+ assertTrue(toolbar.edit.rootView.visibility == View.VISIBLE)
+
+ assertTrue(toolbar.onBackPressed())
+
+ assertTrue(toolbar.display.rootView.visibility == View.VISIBLE)
+ assertTrue(toolbar.edit.rootView.visibility == View.GONE)
+ }
+
+ @Test
+ fun `displayUrl will be forwarded to display toolbar immediately`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+ val edit: EditToolbar = mock()
+
+ toolbar.display = display
+ toolbar.edit = edit
+
+ toolbar.url = "https://www.mozilla.org"
+
+ verify(display).url = "https://www.mozilla.org"
+ verify(edit, never()).updateUrl(ArgumentMatchers.anyString(), ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyBoolean())
+ }
+
+ @Test
+ fun `displayUrl is truncated to prevent extreme cases from slowing down the UI`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+ val edit: EditToolbar = mock()
+
+ toolbar.display = display
+ toolbar.edit = edit
+
+ toolbar.url = "a".repeat(MAX_URI_LENGTH + 1)
+ toolbar.url = "b".repeat(MAX_URI_LENGTH)
+ toolbar.url = "c".repeat(MAX_URI_LENGTH - 1)
+
+ val urlCaptor = argumentCaptor<String>()
+ verify(display, times(3)).url = urlCaptor.capture()
+
+ val capturedValues = urlCaptor.allValues
+ // Value was too long and should've been truncated
+ assertEquals("a".repeat(MAX_URI_LENGTH), capturedValues[0])
+ // Values should be the same as before
+ assertEquals("b".repeat(MAX_URI_LENGTH), capturedValues[1])
+ assertEquals("c".repeat(MAX_URI_LENGTH - 1), capturedValues[2])
+ }
+
+ @Test
+ fun `searchTerms is truncated in case it is greater than MAX_URI_LENGTH`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = spy(toolbar.edit)
+ toolbar.editMode()
+
+ toolbar.setSearchTerms("a".repeat(MAX_URI_LENGTH + 1))
+
+ // Value was too long and should've been truncated
+ assertEquals(toolbar.searchTerms.length, MAX_URI_LENGTH)
+ verify(toolbar.edit).editSuggestion("a".repeat(MAX_URI_LENGTH))
+ }
+
+ @Test
+ fun `searchTerms is not truncated in case it is equal or less than MAX_URI_LENGTH`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = spy(toolbar.edit)
+ toolbar.editMode()
+
+ toolbar.setSearchTerms("b".repeat(MAX_URI_LENGTH))
+
+ // Value should be the same as before
+ assertEquals(toolbar.searchTerms.length, MAX_URI_LENGTH)
+ verify(toolbar.edit).editSuggestion("b".repeat(MAX_URI_LENGTH))
+
+ toolbar.setSearchTerms("c".repeat(MAX_URI_LENGTH - 1))
+
+ // Value should be the same as before
+ assertEquals(toolbar.searchTerms.length, MAX_URI_LENGTH - 1)
+ verify(toolbar.edit).editSuggestion("c".repeat(MAX_URI_LENGTH - 1))
+ }
+
+ @Test
+ fun `last URL will be forwarded to edit toolbar when switching mode`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = spy(toolbar.edit)
+
+ toolbar.url = "https://www.mozilla.org"
+ verify(toolbar.edit, never()).updateUrl("https://www.mozilla.org", false)
+
+ toolbar.editMode()
+
+ verify(toolbar.edit).updateUrl("https://www.mozilla.org", false)
+ }
+
+ @Test
+ fun `displayProgress will send accessibility events`() {
+ val toolbar = BrowserToolbar(testContext)
+ val root = mock(ViewParent::class.java)
+ shadowOf(toolbar).setMyParent(root)
+ `when`(root.requestSendAccessibilityEvent(any(), any())).thenReturn(false)
+
+ val shadowAccessibilityManager = shadowOf(testContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager)
+ shadowAccessibilityManager.setEnabled(true)
+ shadowAccessibilityManager.setTouchExplorationEnabled(true)
+
+ toolbar.displayProgress(10)
+ toolbar.displayProgress(50)
+ toolbar.displayProgress(100)
+
+ // make sure multiple calls to 100% does not trigger "loading" announcement
+ toolbar.displayProgress(100)
+
+ val captor = ArgumentCaptor.forClass(AccessibilityEvent::class.java)
+
+ verify(root, times(5)).requestSendAccessibilityEvent(any(), captor.capture())
+
+ assertEquals(AccessibilityEvent.TYPE_ANNOUNCEMENT, captor.allValues[0].eventType)
+ assertEquals(testContext.getString(R.string.mozac_browser_toolbar_progress_loading), captor.allValues[0].text[0])
+
+ assertEquals(AccessibilityEvent.TYPE_VIEW_SCROLLED, captor.allValues[1].eventType)
+ assertEquals(10, captor.allValues[1].scrollY)
+ assertEquals(100, captor.allValues[1].maxScrollY)
+
+ assertEquals(AccessibilityEvent.TYPE_VIEW_SCROLLED, captor.allValues[2].eventType)
+ assertEquals(50, captor.allValues[2].scrollY)
+ assertEquals(100, captor.allValues[2].maxScrollY)
+
+ assertEquals(AccessibilityEvent.TYPE_VIEW_SCROLLED, captor.allValues[3].eventType)
+ assertEquals(100, captor.allValues[3].scrollY)
+ assertEquals(100, captor.allValues[3].maxScrollY)
+
+ assertEquals(AccessibilityEvent.TYPE_VIEW_SCROLLED, captor.allValues[4].eventType)
+ assertEquals(100, captor.allValues[3].scrollY)
+ assertEquals(100, captor.allValues[3].maxScrollY)
+ }
+
+ @Test
+ fun `displayProgress will not send send view scrolled accessibility events if touch exploration is disabled`() {
+ val toolbar = BrowserToolbar(testContext)
+ val root = mock(ViewParent::class.java)
+ shadowOf(toolbar).setMyParent(root)
+ `when`(root.requestSendAccessibilityEvent(any(), any())).thenReturn(false)
+
+ val shadowAccessibilityManager = shadowOf(testContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager)
+ shadowAccessibilityManager.setEnabled(true)
+ shadowAccessibilityManager.setTouchExplorationEnabled(false)
+
+ toolbar.displayProgress(10)
+ toolbar.displayProgress(50)
+ toolbar.displayProgress(100)
+
+ // make sure multiple calls to 100% does not trigger "loading" announcement
+ toolbar.displayProgress(100)
+
+ val captor = ArgumentCaptor.forClass(AccessibilityEvent::class.java)
+
+ verify(root, times(1)).requestSendAccessibilityEvent(any(), captor.capture())
+
+ assertEquals(AccessibilityEvent.TYPE_ANNOUNCEMENT, captor.allValues[0].eventType)
+ assertEquals(testContext.getString(R.string.mozac_browser_toolbar_progress_loading), captor.allValues[0].text[0])
+ }
+
+ @Test
+ fun `displayProgress will be forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+
+ toolbar.display = display
+
+ toolbar.displayProgress(10)
+ toolbar.displayProgress(50)
+ toolbar.displayProgress(75)
+ toolbar.displayProgress(100)
+
+ verify(display).updateProgress(10)
+ verify(display).updateProgress(50)
+ verify(display).updateProgress(75)
+ verify(display).updateProgress(100)
+
+ verifyNoMoreInteractions(display)
+ }
+
+ @Test
+ fun `internal onUrlEntered callback will be forwarded to urlChangeListener`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val mockedListener = object {
+ var called = false
+ var url: String? = null
+
+ fun invoke(url: String): Boolean {
+ this.called = true
+ this.url = url
+ return true
+ }
+ }
+
+ toolbar.setOnUrlCommitListener(mockedListener::invoke)
+ toolbar.onUrlEntered("https://www.mozilla.org")
+
+ assertTrue(mockedListener.called)
+ assertEquals("https://www.mozilla.org", mockedListener.url)
+ }
+
+ /*
+ @Test
+ fun `internal onEditCancelled callback will be forwarded to editListener`() {
+ val toolbar = BrowserToolbar(testContext)
+ val listener: Toolbar.OnEditListener = mock()
+ toolbar.setOnEditListener(listener)
+ assertEquals(toolbar.edit.editListener, listener)
+
+ toolbar.edit.views.url.onKeyPreIme(
+ KeyEvent.KEYCODE_BACK,
+ KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)
+ )
+ verify(listener, times(1)).onCancelEditing()
+ }*/
+
+ @Test
+ fun `toolbar measure will use full width and fixed 56dp height`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.AT_MOST)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(800, View.MeasureSpec.AT_MOST)
+
+ toolbar.measure(widthSpec, heightSpec)
+
+ assertEquals(1024, toolbar.measuredWidth)
+ assertEquals(56, toolbar.measuredHeight)
+ }
+
+ @Test
+ fun `toolbar will use provided height with EXACTLY measure spec`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.AT_MOST)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(800, View.MeasureSpec.EXACTLY)
+
+ toolbar.measure(widthSpec, heightSpec)
+
+ assertEquals(1024, toolbar.measuredWidth)
+ assertEquals(800, toolbar.measuredHeight)
+ }
+
+ @Test
+ fun `display and edit toolbar will use full size of browser toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ assertEquals(0, toolbar.display.rootView.measuredWidth)
+ assertEquals(0, toolbar.display.rootView.measuredHeight)
+ assertEquals(0, toolbar.edit.rootView.measuredWidth)
+ assertEquals(0, toolbar.edit.rootView.measuredHeight)
+
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.AT_MOST)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(800, View.MeasureSpec.AT_MOST)
+
+ toolbar.measure(widthSpec, heightSpec)
+
+ assertEquals(1024, toolbar.display.rootView.measuredWidth)
+ assertEquals(56, toolbar.display.rootView.measuredHeight)
+ assertEquals(1024, toolbar.edit.rootView.measuredWidth)
+ assertEquals(56, toolbar.edit.rootView.measuredHeight)
+ }
+
+ @Test
+ fun `toolbar will switch back to display mode after an URL has been entered`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.editMode()
+
+ assertTrue(toolbar.display.rootView.visibility == View.GONE)
+ assertTrue(toolbar.edit.rootView.visibility == View.VISIBLE)
+
+ toolbar.onUrlEntered("https://www.mozilla.org")
+
+ assertTrue(toolbar.display.rootView.visibility == View.VISIBLE)
+ assertTrue(toolbar.edit.rootView.visibility == View.GONE)
+ }
+
+ @Test
+ fun `toolbar will switch back to display mode if URL commit listener returns true`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.setOnUrlCommitListener { true }
+ toolbar.editMode()
+
+ assertTrue(toolbar.display.rootView.isGone)
+ assertTrue(toolbar.edit.rootView.isVisible)
+
+ toolbar.onUrlEntered("https://www.mozilla.org")
+
+ assertTrue(toolbar.display.rootView.isVisible)
+ assertTrue(toolbar.edit.rootView.isGone)
+ }
+
+ @Test
+ fun `toolbar will stay in edit mode if URL commit listener returns false`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.setOnUrlCommitListener { false }
+ toolbar.editMode()
+
+ assertTrue(toolbar.display.rootView.isGone)
+ assertTrue(toolbar.edit.rootView.isVisible)
+
+ toolbar.onUrlEntered("https://www.mozilla.org")
+
+ assertTrue(toolbar.display.rootView.isGone)
+ assertTrue(toolbar.edit.rootView.isVisible)
+ }
+
+ @Test
+ fun `add browser action will be forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+
+ toolbar.display = display
+
+ val action = BrowserToolbar.Button(mock(), "Hello") {
+ // Do nothing
+ }
+
+ toolbar.addBrowserAction(action)
+
+ verify(display).addBrowserAction(action)
+ }
+
+ @Test
+ fun `remove browser action will be forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+
+ toolbar.display = display
+
+ val action = BrowserToolbar.Button(mock(), "Hello") {
+ // Do nothing
+ }
+
+ toolbar.removeBrowserAction(action)
+
+ verify(display).removeBrowserAction(action)
+ }
+
+ @Test
+ fun `remove navigation action will be forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+
+ toolbar.display = display
+
+ val action = BrowserToolbar.Button(mock(), "Hello") {
+ // Do nothing
+ }
+
+ toolbar.removeNavigationAction(action)
+
+ verify(display).removeNavigationAction(action)
+ }
+
+ @Test
+ fun `remove page action will be forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+
+ toolbar.display = display
+
+ val action = BrowserToolbar.Button(mock(), "Hello") {
+ // Do nothing
+ }
+
+ toolbar.removePageAction(action)
+
+ verify(display).removePageAction(action)
+ }
+
+ @Test
+ fun `add page action will be forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val display: DisplayToolbar = mock()
+
+ toolbar.display = display
+
+ val action = BrowserToolbar.Button(mock(), "World") {
+ // Do nothing
+ }
+
+ toolbar.addPageAction(action)
+
+ verify(display).addPageAction(action)
+ }
+
+ @Test
+ fun `add edit action start will be forwarded to edit toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val edit: EditToolbar = mock()
+ toolbar.edit = edit
+
+ val action = BrowserToolbar.Button(mock(), "QR code scanner") {
+ // Do nothing
+ }
+
+ toolbar.addEditActionStart(action)
+
+ verify(edit).addEditActionStart(action)
+ }
+
+ @Test
+ fun `add edit action end will be forwarded to edit toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val edit: EditToolbar = mock()
+ toolbar.edit = edit
+
+ val action = BrowserToolbar.Button(mock(), "QR code scanner") {
+ // Do nothing
+ }
+
+ toolbar.addEditActionEnd(action)
+
+ verify(edit).addEditActionEnd(action)
+ }
+
+ @Test
+ fun `WHEN removing action end THEN it will be forwarded to the edit toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val edit: EditToolbar = mock()
+ toolbar.edit = edit
+
+ val action = BrowserToolbar.Button(mock(), "QR code scanner") {
+ // Do nothing
+ }
+
+ toolbar.removeEditActionEnd(action)
+
+ verify(edit).removeEditActionEnd(action)
+ }
+
+ @Test
+ fun `WHEN hideMenuButton is sent to BrowserToolbar THEN it will be forwarded to the DisplayToolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val display: DisplayToolbar = mock()
+ toolbar.display = display
+
+ toolbar.hideMenuButton()
+
+ verify(display).hideMenuButton()
+ }
+
+ @Test
+ fun `WHEN showMenuButton is sent to BrowserToolbar THEN it will be forwarded to the DisplayToolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val display: DisplayToolbar = mock()
+ toolbar.display = display
+
+ toolbar.showMenuButton()
+
+ verify(display).showMenuButton()
+ }
+
+ @Test
+ fun `WHEN showPageActionSeparator is sent to BrowserToolbar THEN it will be forwarded to the DisplayToolbar and EditToolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val display: DisplayToolbar = mock()
+ val edit: EditToolbar = mock()
+ toolbar.display = display
+ toolbar.edit = edit
+
+ toolbar.showPageActionSeparator()
+
+ verify(display).showPageActionSeparator()
+ verify(edit).showPageActionSeparator()
+ }
+
+ @Test
+ fun `WHEN hidePageActionSeparator is sent to BrowserToolbar THEN it will be forwarded to the DisplayToolbar and EditToolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val display: DisplayToolbar = mock()
+ val edit: EditToolbar = mock()
+ toolbar.display = display
+ toolbar.edit = edit
+
+ toolbar.hidePageActionSeparator()
+
+ verify(display).hidePageActionSeparator()
+ verify(edit).hidePageActionSeparator()
+ }
+
+ @Test
+ fun `WHEN setDisplayHorizontalPadding is sent to BrowserToolbar THEN it will be forwarded to the DisplayToolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val display: DisplayToolbar = mock()
+ toolbar.display = display
+ toolbar.edit = mock()
+
+ toolbar.setDisplayHorizontalPadding(123)
+ verify(display).setHorizontalPadding(123)
+
+ toolbar.setDisplayHorizontalPadding(0)
+ verify(display).setHorizontalPadding(0)
+ }
+
+ @Test
+ fun `cast to view`() {
+ // Given
+ val toolbar = BrowserToolbar(testContext)
+
+ // When
+ val view = toolbar.asView()
+
+ // Then
+ assertNotNull(view)
+ }
+
+ @Test
+ fun `URL update does not override search terms in edit mode`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ toolbar.display = spy(toolbar.display)
+ toolbar.edit = spy(toolbar.edit)
+
+ toolbar.setSearchTerms("mozilla android")
+ toolbar.url = "https://www.mozilla.com"
+ toolbar.editMode()
+ verify(toolbar.display).url = "https://www.mozilla.com"
+ verify(toolbar.edit).updateUrl("mozilla android", false)
+
+ toolbar.setSearchTerms("")
+ verify(toolbar.edit).updateUrl("", false)
+
+ toolbar.url = "https://www.mozilla.org"
+ toolbar.editMode()
+ verify(toolbar.display).url = "https://www.mozilla.org"
+ verify(toolbar.edit).updateUrl("https://www.mozilla.org", false)
+ }
+
+ @Test
+ fun `add navigation action will be forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+ toolbar.display = display
+
+ val action = BrowserToolbar.Button(mock(), "Back") {
+ // Do nothing
+ }
+
+ toolbar.addNavigationAction(action)
+
+ verify(display).addNavigationAction(action)
+ }
+
+ @Test
+ fun `invalidate actions is forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ val display: DisplayToolbar = mock()
+ toolbar.display = display
+
+ verify(display, never()).invalidateActions()
+
+ toolbar.invalidateActions()
+
+ verify(display).invalidateActions()
+ }
+
+ @Test
+ fun `invalidate actions is forwarded to edit toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ val edit: EditToolbar = mock()
+ toolbar.edit = edit
+
+ verify(edit, never()).invalidateActions()
+
+ toolbar.invalidateActions()
+
+ verify(edit).invalidateActions()
+ }
+
+ @Test
+ fun `search terms (if set) are forwarded to edit toolbar instead of URL`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ toolbar.edit = spy(toolbar.edit)
+
+ toolbar.url = "https://www.mozilla.org"
+ toolbar.setSearchTerms("Mozilla Firefox")
+
+ verify(toolbar.edit, never()).updateUrl("https://www.mozilla.org")
+ verify(toolbar.edit, never()).updateUrl("Mozilla Firefox")
+
+ toolbar.editMode()
+
+ verify(toolbar.edit, never()).updateUrl("https://www.mozilla.org")
+ verify(toolbar.edit).updateUrl("Mozilla Firefox")
+ }
+
+ @Test
+ fun `search terms are forwarded to edit toolbar when it is active`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ toolbar.edit = spy(toolbar.edit)
+
+ toolbar.editMode()
+
+ toolbar.setSearchTerms("Mozilla Firefox")
+
+ verify(toolbar.edit).editSuggestion("Mozilla Firefox")
+ }
+
+ @Test
+ fun `editListener is set on edit`() {
+ val toolbar = BrowserToolbar(testContext)
+ assertNull(toolbar.edit.editListener)
+
+ val listener: Toolbar.OnEditListener = mock()
+ toolbar.setOnEditListener(listener)
+
+ assertEquals(listener, toolbar.edit.editListener)
+ }
+
+ @Test
+ fun `editListener is invoked when switching between modes`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val listener: Toolbar.OnEditListener = mock()
+ toolbar.setOnEditListener(listener)
+
+ toolbar.editMode()
+
+ verify(listener).onStartEditing()
+ verifyNoMoreInteractions(listener)
+
+ toolbar.displayMode()
+
+ verify(listener).onStopEditing()
+ verifyNoMoreInteractions(listener)
+ }
+
+ @Test
+ fun `editListener is invoked when text changes`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val listener: Toolbar.OnEditListener = mock()
+ toolbar.setOnEditListener(listener)
+
+ toolbar.edit.views.url.onAttachedToWindow()
+
+ toolbar.editMode()
+
+ toolbar.edit.views.url.setText("Hello")
+ toolbar.edit.views.url.setText("Hello World")
+
+ verify(listener).onStartEditing()
+ verify(listener).onTextChanged("Hello")
+ verify(listener).onTextChanged("Hello World")
+ }
+
+ @Test
+ fun `titleView visibility is based on being set`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ assertEquals(toolbar.display.views.origin.titleView.visibility, View.GONE)
+ toolbar.title = "Mozilla"
+ assertEquals(toolbar.display.views.origin.titleView.visibility, View.VISIBLE)
+ toolbar.title = ""
+ assertEquals(toolbar.display.views.origin.titleView.visibility, View.GONE)
+ }
+
+ @Test
+ fun `titleView text is set properly`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ toolbar.title = "Mozilla"
+ assertEquals("Mozilla", toolbar.display.views.origin.titleView.text)
+ assertEquals("Mozilla", toolbar.title)
+ }
+
+ @Test
+ fun `titleView fading is set properly with non-null attrs`() {
+ val attributeSet: AttributeSet = Robolectric.buildAttributeSet().build()
+
+ val toolbar = BrowserToolbar(testContext, attributeSet)
+ val titleView = toolbar.display.views.origin.titleView
+ val edgeLength = testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_toolbar_url_fading_edge_size)
+
+ assertTrue(titleView.isHorizontalFadingEdgeEnabled)
+ assertEquals(edgeLength, titleView.horizontalFadingEdgeLength)
+ }
+
+ @Test
+ fun `Button constructor with drawable`() {
+ val buttonDefault = BrowserToolbar.Button(mock(), "imageDrawable") {}
+
+ assertEquals(true, buttonDefault.visible())
+ assertEquals(BrowserToolbar.DEFAULT_PADDING, buttonDefault.padding)
+ assertEquals("imageDrawable", buttonDefault.contentDescription)
+
+ val button = BrowserToolbar.Button(mock(), "imageDrawable", visible = { false }) {}
+
+ assertEquals(false, button.visible())
+ }
+
+ @Test
+ fun `ToggleButton constructor with drawable`() {
+ val buttonDefault =
+ BrowserToolbar.ToggleButton(mock(), mock(), "imageDrawable", "imageSelectedDrawable") {}
+
+ assertEquals(true, buttonDefault.visible())
+ assertEquals(BrowserToolbar.DEFAULT_PADDING, buttonDefault.padding)
+
+ val button = BrowserToolbar.ToggleButton(
+ mock(),
+ mock(),
+ "imageDrawable",
+ "imageSelectedDrawable",
+ visible = { false },
+ ) {}
+
+ assertEquals(false, button.visible())
+ }
+
+ @Test
+ fun `ReloadPageAction visibility changes update image`() {
+ val reloadImage: Drawable = mock()
+ val stopImage: Drawable = mock()
+ val view: ImageButton = mock()
+ var reloadPageAction = BrowserToolbar.TwoStateButton(reloadImage, "reload", stopImage, "stop") {}
+ assertFalse(reloadPageAction.enabled)
+ reloadPageAction.bind(view)
+ verify(view).setImageDrawable(reloadImage)
+ verify(view).contentDescription = "reload"
+
+ reloadPageAction = BrowserToolbar.TwoStateButton(reloadImage, "reload", stopImage, "stop", { false }) {}
+ reloadPageAction.bind(view)
+ verify(view).setImageDrawable(stopImage)
+ verify(view).contentDescription = "stop"
+ }
+
+ @Test
+ fun `siteSecure updates the display`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.display = spy(toolbar.display)
+ assertEquals(SiteSecurity.INSECURE, toolbar.siteSecure)
+
+ toolbar.siteSecure = SiteSecurity.SECURE
+
+ verify(toolbar.display).siteSecurity = SiteSecurity.SECURE
+ }
+
+ @Test
+ fun `siteTrackingProtection updates the display`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.display = spy(toolbar.display)
+ assertEquals(SiteTrackingProtection.OFF_GLOBALLY, toolbar.siteTrackingProtection)
+
+ toolbar.siteTrackingProtection = SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED
+
+ verify(toolbar.display).setTrackingProtectionState(SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED)
+
+ toolbar.siteTrackingProtection = SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED
+ verifyNoMoreInteractions(toolbar.display)
+ }
+
+ @Test
+ fun `private flag sets IME_FLAG_NO_PERSONALIZED_LEARNING on url edit view`() {
+ val toolbar = BrowserToolbar(testContext)
+ val edit = toolbar.edit
+
+ // By default "private mode" is off.
+ assertEquals(
+ 0,
+ edit.views.url.imeOptions and
+ EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING,
+ )
+ assertEquals(false, toolbar.private)
+
+ // Turning on private mode sets flag
+ toolbar.private = true
+ assertNotEquals(
+ 0,
+ edit.views.url.imeOptions and
+ EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING,
+ )
+ assertTrue(toolbar.private)
+
+ // Turning private mode off again - should remove flag
+ toolbar.private = false
+ assertEquals(
+ 0,
+ edit.views.url.imeOptions and
+ EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING,
+ )
+ assertEquals(false, toolbar.private)
+ }
+
+ @Test
+ fun `setAutocompleteListener is forwarded to edit toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = mock()
+
+ val filter: suspend (String, AutocompleteDelegate) -> Unit = { _, _ ->
+ // Do nothing
+ }
+
+ toolbar.setAutocompleteListener(filter)
+
+ verify(toolbar.edit).setAutocompleteListener(filter)
+ }
+
+ @Test
+ fun `WHEN an attempt to refresh autocomplete suggestions is made THEN forward the call to edit toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = mock()
+ toolbar.setAutocompleteListener { _, _ -> }
+
+ toolbar.refreshAutocomplete()
+
+ verify(toolbar.edit).refreshAutocompleteSuggestion()
+ }
+
+ @Test
+ fun `onStop is forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.display = mock()
+
+ toolbar.onStop()
+
+ verify(toolbar.display).onStop()
+ }
+
+ @Test
+ fun `dismiss menu is forwarded to display toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.display = mock()
+ val displayToolbarViews: DisplayToolbarViews = mock()
+ val menuButton: MenuButton = mock()
+
+ whenever(toolbar.display.views).thenReturn(displayToolbarViews)
+ whenever(displayToolbarViews.menu).thenReturn(menuButton)
+
+ toolbar.dismissMenu()
+ verify(menuButton).dismissMenu()
+ }
+
+ @Test
+ fun `enable scrolling is forwarded to the toolbar behavior`() {
+ // Seems like real instances are needed for things to be set properly
+ val toolbar = BrowserToolbar(testContext)
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val params = CoordinatorLayout.LayoutParams(10, 10).apply {
+ this.behavior = behavior
+ }
+ toolbar.layoutParams = params
+
+ toolbar.enableScrolling()
+
+ verify(behavior).enableScrolling()
+ }
+
+ @Test
+ fun `disable scrolling is forwarded to the toolbar behavior`() {
+ // Seems like real instances are needed for things to be set properly
+ val toolbar = BrowserToolbar(testContext)
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val params = CoordinatorLayout.LayoutParams(10, 10).apply {
+ this.behavior = behavior
+ }
+ toolbar.layoutParams = params
+
+ toolbar.disableScrolling()
+
+ verify(behavior).disableScrolling()
+ }
+
+ @Test
+ fun `expand is forwarded to the toolbar behavior`() {
+ // Seems like real instances are needed for things to be set properly
+ val toolbar = BrowserToolbar(testContext)
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val params = CoordinatorLayout.LayoutParams(10, 10).apply {
+ this.behavior = behavior
+ }
+ toolbar.layoutParams = params
+
+ toolbar.expand()
+
+ verify(behavior).forceExpand(toolbar)
+ }
+
+ @Test
+ fun `collapse is forwarded to the toolbar behavior`() {
+ // Seems like real instances are needed for things to be set properly
+ val toolbar = BrowserToolbar(testContext)
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val params = CoordinatorLayout.LayoutParams(10, 10).apply {
+ this.behavior = behavior
+ }
+ toolbar.layoutParams = params
+
+ toolbar.collapse()
+
+ verify(behavior).forceCollapse(toolbar)
+ }
+
+ @Test
+ fun `WHEN search terms changes THEN edit listener is notified`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = spy(toolbar.edit)
+ toolbar.edit.editListener = mock()
+
+ toolbar.setSearchTerms("")
+ toolbar.editMode()
+
+ toolbar.setSearchTerms("test")
+ verify(toolbar.edit.editListener)?.onTextChanged("test")
+
+ toolbar.setSearchTerms("")
+ verify(toolbar.edit.editListener)?.onTextChanged("")
+ }
+
+ @Test
+ fun `WHEN switching to edit mode AND the cursor placement parameter is specified THEN call the correct method to place the cursor`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = spy(toolbar.edit)
+
+ toolbar.editMode(Toolbar.CursorPlacement.ALL)
+
+ verify(toolbar.edit).selectAll()
+
+ toolbar.editMode(Toolbar.CursorPlacement.END)
+
+ verify(toolbar.edit).selectEnd()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt
new file mode 100644
index 0000000000..a1cfb4c791
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt
@@ -0,0 +1,824 @@
+/* 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.browser.toolbar.display
+
+import android.graphics.Color
+import android.os.Build
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.browser.toolbar.R
+import mozilla.components.concept.menu.MenuButton
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.concept.toolbar.Toolbar.SiteSecurity
+import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.util.ReflectionHelpers
+import mozilla.components.ui.icons.R as iconsR
+
+@RunWith(AndroidJUnit4::class)
+class DisplayToolbarTest {
+ private fun createDisplayToolbar(): Pair<BrowserToolbar, DisplayToolbar> {
+ val toolbar: BrowserToolbar = mock()
+ val displayToolbar = DisplayToolbar(
+ testContext,
+ toolbar,
+ View.inflate(testContext, R.layout.mozac_browser_toolbar_displaytoolbar, null),
+ )
+ return Pair(toolbar, displayToolbar)
+ }
+
+ @Test
+ fun `clicking on the URL switches the toolbar to editing mode`() {
+ val (toolbar, displayToolbar) = createDisplayToolbar()
+
+ val urlView = displayToolbar.views.origin.urlView
+ assertTrue(urlView.performClick())
+
+ verify(toolbar).editMode()
+ }
+
+ @Test
+ fun `progress is forwarded to progress bar`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val progressView = displayToolbar.views.progress
+
+ displayToolbar.updateProgress(0)
+ assertEquals(0, progressView.progress)
+ assertEquals(View.GONE, progressView.visibility)
+
+ displayToolbar.updateProgress(10)
+ assertEquals(10, progressView.progress)
+ assertEquals(View.VISIBLE, progressView.visibility)
+
+ displayToolbar.updateProgress(50)
+ assertEquals(50, progressView.progress)
+ assertEquals(View.VISIBLE, progressView.visibility)
+
+ displayToolbar.updateProgress(75)
+ assertEquals(75, progressView.progress)
+ assertEquals(View.VISIBLE, progressView.visibility)
+
+ displayToolbar.updateProgress(100)
+ assertEquals(100, progressView.progress)
+ assertEquals(View.GONE, progressView.visibility)
+ }
+
+ @Test
+ fun `trackingProtectionViewColor will change the color of the trackingProtectionIconView`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertNull(displayToolbar.views.trackingProtectionIndicator.colorFilter)
+
+ displayToolbar.colors = displayToolbar.colors.copy(
+ trackingProtection = Color.BLUE,
+ )
+
+ assertNotNull(displayToolbar.views.trackingProtectionIndicator.colorFilter)
+ assertNotNull(displayToolbar.views.trackingProtectionIndicator.trackingProtectionTint)
+ }
+
+ @Test
+ fun `highlightView will change the color of the dot`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertNull(displayToolbar.views.highlight.colorFilter)
+
+ displayToolbar.colors = displayToolbar.colors.copy(highlight = Color.BLUE)
+
+ assertNotNull(displayToolbar.views.highlight.colorFilter)
+ assertNotNull(displayToolbar.views.highlight.highlightTint)
+ }
+
+ @Test
+ fun `tracking protection and separator views become visible when states ON OR ACTIVE are set to siteTrackingProtection`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val trackingView = displayToolbar.views.trackingProtectionIndicator
+ val separatorView = displayToolbar.views.separator
+
+ assertTrue(trackingView.visibility == View.GONE)
+ assertTrue(separatorView.visibility == View.GONE)
+
+ displayToolbar.indicators = listOf(
+ DisplayToolbar.Indicators.SECURITY,
+ DisplayToolbar.Indicators.TRACKING_PROTECTION,
+ )
+ displayToolbar.url = "https://www.mozilla.org"
+ displayToolbar.displayIndicatorSeparator = true
+ displayToolbar.setTrackingProtectionState(SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED)
+
+ assertTrue(trackingView.isVisible)
+ assertTrue(separatorView.isVisible)
+
+ displayToolbar.setTrackingProtectionState(SiteTrackingProtection.OFF_GLOBALLY)
+
+ assertTrue(trackingView.visibility == View.GONE)
+ assertTrue(separatorView.visibility == View.GONE)
+
+ displayToolbar.setTrackingProtectionState(SiteTrackingProtection.ON_TRACKERS_BLOCKED)
+
+ assertTrue(trackingView.isVisible)
+ assertTrue(separatorView.isVisible)
+ }
+
+ @Test
+ fun `setTrackingProtectionIcons will forward to TrackingProtectionIconView`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ displayToolbar.indicators = listOf(DisplayToolbar.Indicators.TRACKING_PROTECTION)
+ displayToolbar.setTrackingProtectionState(SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED)
+
+ val oldTrackingProtectionIcon = displayToolbar.views.trackingProtectionIndicator.drawable
+ assertNotNull(oldTrackingProtectionIcon)
+
+ val drawable1 =
+ testContext.getDrawable(TrackingProtectionIconView.DEFAULT_ICON_ON_NO_TRACKERS_BLOCKED)!!
+ val drawable2 =
+ testContext.getDrawable(TrackingProtectionIconView.DEFAULT_ICON_ON_TRACKERS_BLOCKED)!!
+ val drawable3 =
+ testContext.getDrawable(TrackingProtectionIconView.DEFAULT_ICON_OFF_FOR_A_SITE)!!
+
+ displayToolbar.icons = displayToolbar.icons.copy(
+ trackingProtectionTrackersBlocked = drawable1,
+ trackingProtectionNothingBlocked = drawable2,
+ trackingProtectionException = drawable3,
+ )
+
+ assertNotEquals(
+ oldTrackingProtectionIcon,
+ displayToolbar.views.trackingProtectionIndicator.drawable,
+ )
+
+ assertEquals(drawable2, displayToolbar.views.trackingProtectionIndicator.drawable)
+
+ displayToolbar.setTrackingProtectionState(SiteTrackingProtection.ON_TRACKERS_BLOCKED)
+
+ assertNotEquals(
+ oldTrackingProtectionIcon,
+ displayToolbar.views.trackingProtectionIndicator.drawable,
+ )
+
+ assertEquals(
+ drawable1,
+ displayToolbar.views.trackingProtectionIndicator.drawable,
+ )
+ }
+
+ @Test
+ fun `setHighlight will forward to HighlightView`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val oldPermissionIcon = displayToolbar.views.highlight.drawable
+ assertNotNull(oldPermissionIcon)
+
+ val drawable1 = testContext.getDrawable(HighlightView.DEFAULT_ICON)!!
+
+ displayToolbar.indicators = listOf(DisplayToolbar.Indicators.HIGHLIGHT)
+ displayToolbar.icons = displayToolbar.icons.copy(
+ highlight = drawable1,
+ )
+
+ assertNotEquals(
+ oldPermissionIcon,
+ displayToolbar.views.highlight.drawable,
+ )
+
+ displayToolbar.setHighlight(Toolbar.Highlight.PERMISSIONS_CHANGED)
+
+ assertNotEquals(
+ oldPermissionIcon,
+ displayToolbar.views.highlight.drawable,
+ )
+ }
+
+ @Test
+ fun `menu view is gone by default`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val menuView = displayToolbar.views.menu
+
+ assertNotNull(menuView)
+ assertEquals(View.GONE, menuView.impl.visibility)
+ }
+
+ @Test
+ fun `menu view becomes visible once a menu builder is set`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val menuView = displayToolbar.views.menu
+
+ assertNotNull(menuView)
+
+ assertEquals(View.GONE, menuView.impl.visibility)
+
+ displayToolbar.menuBuilder = BrowserMenuBuilder(emptyList())
+
+ assertEquals(View.VISIBLE, menuView.impl.visibility)
+
+ displayToolbar.menuBuilder = null
+
+ assertEquals(View.GONE, menuView.impl.visibility)
+ }
+
+ @Test
+ fun `no menu builder is set by default`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertNull(displayToolbar.menuBuilder)
+ }
+
+ @Test
+ fun `menu builder will be used to create and show menu when button is clicked`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+ val menuView = displayToolbar.views.menu
+
+ val menuBuilder = mock(BrowserMenuBuilder::class.java)
+ val menu = mock(BrowserMenu::class.java)
+ doReturn(menu).`when`(menuBuilder).build(testContext)
+
+ displayToolbar.menuBuilder = menuBuilder
+
+ verify(menuBuilder, never()).build(testContext)
+ verify(menu, never()).show(menuView.impl)
+
+ menuView.impl.performClick()
+
+ verify(menuBuilder).build(testContext)
+ verify(menu).show(eq(menuView.impl), any(), any(), anyBoolean(), any())
+ verify(menu, never()).invalidate()
+
+ displayToolbar.invalidateActions()
+
+ verify(menu).invalidate()
+ }
+
+ @Test
+ fun `browser action gets added as view to toolbar`() {
+ val contentDescription = "Mozilla"
+
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertEquals(0, displayToolbar.views.browserActions.childCount)
+
+ val action = BrowserToolbar.Button(mock(), contentDescription) {}
+ displayToolbar.addBrowserAction(action)
+
+ assertEquals(1, displayToolbar.views.browserActions.childCount)
+
+ val view = displayToolbar.views.browserActions.getChildAt(0)
+ assertEquals(contentDescription, view.contentDescription)
+ }
+
+ @Test
+ fun `clicking browser action view triggers listener of action`() {
+ var callbackExecuted = false
+
+ val action = BrowserToolbar.Button(mock(), "Button") {
+ callbackExecuted = true
+ }
+
+ val (_, displayToolbar) = createDisplayToolbar()
+ displayToolbar.addBrowserAction(action)
+
+ assertEquals(1, displayToolbar.views.browserActions.childCount)
+ val view = displayToolbar.views.browserActions.getChildAt(0)
+
+ assertNotNull(view)
+
+ assertFalse(callbackExecuted)
+
+ view?.performClick()
+
+ assertTrue(callbackExecuted)
+ }
+
+ @Test
+ fun `browser action can be removed`() {
+ val contentDescription = "to-be-removed"
+
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val action = BrowserToolbar.Button(mock(), contentDescription) {}
+ // Removing action which was never added has no effect
+ displayToolbar.removeBrowserAction(action)
+
+ displayToolbar.addBrowserAction(action)
+ assertEquals(1, displayToolbar.views.browserActions.childCount)
+
+ displayToolbar.removeBrowserAction(action)
+ assertEquals(0, displayToolbar.views.browserActions.childCount)
+ }
+
+ @Test
+ fun `navigation action can be removed`() {
+ val contentDescription = "to-be-removed"
+
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val action = BrowserToolbar.Button(mock(), contentDescription) {}
+ // Removing action which was never added has no effect
+ displayToolbar.removeNavigationAction(action)
+
+ displayToolbar.addNavigationAction(action)
+ assertEquals(1, displayToolbar.views.navigationActions.childCount)
+
+ displayToolbar.removeNavigationAction(action)
+ assertEquals(0, displayToolbar.views.navigationActions.childCount)
+ }
+
+ @Test
+ fun `page action can be removed`() {
+ val contentDescription = "to-be-removed"
+
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val action = BrowserToolbar.Button(mock(), contentDescription) {}
+ // Removing action which was never added has no effect
+ displayToolbar.removePageAction(action)
+
+ displayToolbar.addPageAction(action)
+ assertEquals(1, displayToolbar.views.pageActions.childCount)
+
+ displayToolbar.removePageAction(action)
+ assertEquals(0, displayToolbar.views.pageActions.childCount)
+ }
+
+ @Test
+ fun `page actions will be added as view to the toolbar`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertEquals(0, displayToolbar.views.pageActions.childCount)
+
+ val action = BrowserToolbar.Button(mock(), "Reader Mode") {}
+ displayToolbar.addPageAction(action)
+
+ assertEquals(1, displayToolbar.views.pageActions.childCount)
+ val view = displayToolbar.views.pageActions.getChildAt(0)
+ assertEquals("Reader Mode", view.contentDescription)
+ }
+
+ @Test
+ fun `clicking a page action view will execute the listener of the action`() {
+ var listenerExecuted = false
+
+ val action = BrowserToolbar.Button(mock(), "Reload") {
+ listenerExecuted = true
+ }
+
+ val (_, displayToolbar) = createDisplayToolbar()
+ displayToolbar.addPageAction(action)
+
+ assertFalse(listenerExecuted)
+
+ assertEquals(1, displayToolbar.views.pageActions.childCount)
+ val view = displayToolbar.views.pageActions.getChildAt(0)
+
+ assertNotNull(view)
+ view!!.performClick()
+
+ assertTrue(listenerExecuted)
+ }
+
+ @Test
+ fun `navigation actions will be added as view to the toolbar`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertEquals(0, displayToolbar.views.navigationActions.childCount)
+
+ displayToolbar.addNavigationAction(BrowserToolbar.Button(mock(), "Back") {})
+ displayToolbar.addNavigationAction(BrowserToolbar.Button(mock(), "Forward") {})
+
+ assertEquals(2, displayToolbar.views.navigationActions.childCount)
+ }
+
+ @Test
+ fun `clicking on navigation action will execute listener of the action`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ var listenerExecuted = false
+ val action = BrowserToolbar.Button(mock(), "Back") {
+ listenerExecuted = true
+ }
+
+ displayToolbar.addNavigationAction(action)
+
+ assertFalse(listenerExecuted)
+
+ assertEquals(1, displayToolbar.views.navigationActions.childCount)
+ val view = displayToolbar.views.navigationActions.getChildAt(0)
+ view.performClick()
+
+ assertTrue(listenerExecuted)
+ }
+
+ @Test
+ fun `view of not visible navigation action gets removed after invalidating`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ var shouldActionBeDisplayed = true
+
+ val action = BrowserToolbar.Button(
+ mock(),
+ "Back",
+ visible = { shouldActionBeDisplayed },
+ ) { /* Do nothing */ }
+
+ displayToolbar.addNavigationAction(action)
+
+ assertEquals(1, displayToolbar.views.navigationActions.childCount)
+
+ shouldActionBeDisplayed = false
+ displayToolbar.invalidateActions()
+
+ assertEquals(0, displayToolbar.views.navigationActions.childCount)
+
+ shouldActionBeDisplayed = true
+ displayToolbar.invalidateActions()
+
+ assertEquals(1, displayToolbar.views.navigationActions.childCount)
+ }
+
+ @Test
+ fun `toolbar should call bind with view argument on action after invalidating`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val action = spy(BrowserToolbar.Button(mock(), "Reload") {})
+
+ displayToolbar.addPageAction(action)
+
+ assertEquals(1, displayToolbar.views.pageActions.childCount)
+ val view = displayToolbar.views.pageActions.getChildAt(0)
+
+ verify(action, never()).bind(view!!)
+
+ displayToolbar.invalidateActions()
+
+ verify(action).bind(view)
+ }
+
+ @Test
+ fun `page action will not be added if visible lambda of action returns false`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val visibleAction = BrowserToolbar.Button(mock(), "Reload") {}
+ val invisibleAction = BrowserToolbar.Button(
+ mock(),
+ "Reader Mode",
+ visible = { false },
+ ) {}
+
+ displayToolbar.addPageAction(visibleAction)
+ displayToolbar.addPageAction(invisibleAction)
+
+ assertEquals(1, displayToolbar.views.pageActions.childCount)
+
+ val view = displayToolbar.views.pageActions.getChildAt(0)
+ assertEquals("Reload", view.contentDescription)
+ }
+
+ @Test
+ fun `browser action will not be added if visible lambda of action returns false`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val visibleAction = BrowserToolbar.Button(mock(), "Tabs") {}
+ val invisibleAction = BrowserToolbar.Button(
+ mock(),
+ "Settings",
+ visible = { false },
+ ) {}
+
+ displayToolbar.addBrowserAction(visibleAction)
+ displayToolbar.addBrowserAction(invisibleAction)
+
+ assertEquals(1, displayToolbar.views.browserActions.childCount)
+
+ val view = displayToolbar.views.browserActions.getChildAt(0)
+ assertEquals("Tabs", view.contentDescription)
+ }
+
+ @Test
+ fun `navigation action will not be added if visible lambda of action returns false`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ val visibleAction = BrowserToolbar.Button(mock(), "Forward") {}
+ val invisibleAction = BrowserToolbar.Button(
+ mock(),
+ "Back",
+ visible = { false },
+ ) {}
+
+ displayToolbar.addNavigationAction(visibleAction)
+ displayToolbar.addNavigationAction(invisibleAction)
+
+ assertEquals(1, displayToolbar.views.navigationActions.childCount)
+
+ val view = displayToolbar.views.navigationActions.getChildAt(0)
+ assertEquals("Forward", view.contentDescription)
+ }
+
+ @Test
+ fun `url background will be added and removed from display layout`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertNull(displayToolbar.views.background.drawable)
+
+ displayToolbar.setUrlBackground(
+ ContextCompat.getDrawable(
+ testContext,
+ iconsR.drawable.mozac_ic_broken_lock,
+ ),
+ )
+
+ assertNotNull(displayToolbar.views.background.drawable)
+
+ displayToolbar.setUrlBackground(null)
+
+ assertNull(displayToolbar.views.background.drawable)
+ }
+
+ @Test
+ fun `titleView does not display when there is no title text`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertTrue(displayToolbar.views.origin.titleView.isGone)
+
+ displayToolbar.title = "Hello World"
+
+ assertTrue(displayToolbar.views.origin.titleView.isVisible)
+ }
+
+ @Test
+ fun `toolbar only switches to editing mode if onUrlClicked returns true`() {
+ val (toolbar, displayToolbar) = createDisplayToolbar()
+
+ displayToolbar.views.origin.urlView.performClick()
+
+ verify(toolbar).editMode()
+
+ reset(toolbar)
+ displayToolbar.onUrlClicked = { false }
+ displayToolbar.views.origin.urlView.performClick()
+
+ verify(toolbar, never()).editMode()
+
+ reset(toolbar)
+ displayToolbar.onUrlClicked = { true }
+ displayToolbar.views.origin.urlView.performClick()
+
+ verify(toolbar).editMode()
+ }
+
+ @Test
+ fun `urlView delegates long click when set`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ var longUrlClicked = false
+
+ displayToolbar.setOnUrlLongClickListener {
+ longUrlClicked = true
+ false
+ }
+
+ assertFalse(longUrlClicked)
+ displayToolbar.views.origin.urlView.performLongClick()
+ assertTrue(longUrlClicked)
+ }
+
+ @Test
+ fun `urlView longClickListener can be unset`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ var longClicked = false
+ displayToolbar.setOnUrlLongClickListener {
+ longClicked = true
+ true
+ }
+
+ displayToolbar.views.origin.urlView.performLongClick()
+ assertTrue(longClicked)
+ longClicked = false
+
+ displayToolbar.setOnUrlLongClickListener(null)
+ displayToolbar.views.origin.urlView.performLongClick()
+
+ assertFalse(longClicked)
+ }
+
+ @Test
+ fun `iconView changes site secure state when site security changes`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+ assertEquals(SiteSecurity.INSECURE, displayToolbar.views.securityIndicator.siteSecurity)
+
+ displayToolbar.siteSecurity = SiteSecurity.SECURE
+
+ assertEquals(SiteSecurity.SECURE, displayToolbar.views.securityIndicator.siteSecurity)
+
+ displayToolbar.siteSecurity = SiteSecurity.INSECURE
+
+ assertEquals(SiteSecurity.INSECURE, displayToolbar.views.securityIndicator.siteSecurity)
+ }
+
+ @Test
+ fun `securityIconColor is set when securityIconColor changes`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertNull(displayToolbar.views.securityIndicator.colorFilter)
+
+ displayToolbar.colors = displayToolbar.colors.copy(
+ securityIconSecure = Color.BLUE,
+ securityIconInsecure = Color.BLUE,
+ )
+
+ assertNotNull(displayToolbar.views.securityIndicator.colorFilter)
+ }
+
+ @Test
+ fun `color filter is set with transparent when securityIconColor changes to transparent and api version is lower than 23`() {
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertNull(displayToolbar.views.securityIndicator.colorFilter)
+
+ displayToolbar.colors = displayToolbar.colors.copy(
+ securityIconSecure = Color.TRANSPARENT,
+ securityIconInsecure = Color.TRANSPARENT,
+ )
+
+ assertNotNull(displayToolbar.views.securityIndicator.colorFilter)
+ }
+
+ @Test
+ fun `color filter is cleared when securityIconColor changes to transparent and api version is bigger than 22`() {
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 23)
+
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertNull(displayToolbar.views.securityIndicator.colorFilter)
+
+ displayToolbar.colors = displayToolbar.colors.copy(
+ securityIconSecure = Color.TRANSPARENT,
+ securityIconInsecure = Color.TRANSPARENT,
+ )
+
+ assertNull(displayToolbar.views.securityIndicator.colorFilter)
+ }
+
+ @Test
+ fun `clicking menu button emits facts with additional extras from builder set`() {
+ CollectionProcessor.withFactCollection { facts ->
+ val (_, displayToolbar) = createDisplayToolbar()
+ val menuView = displayToolbar.views.menu
+
+ val menuBuilder = BrowserMenuBuilder(
+ listOf(SimpleBrowserMenuItem("Mozilla")),
+ mapOf(
+ "customTab" to true,
+ "test" to "23",
+ ),
+ )
+ displayToolbar.menuBuilder = menuBuilder
+
+ assertEquals(0, facts.size)
+
+ menuView.impl.performClick()
+
+ assertEquals(1, facts.size)
+
+ val fact = facts[0]
+
+ assertEquals(Component.BROWSER_TOOLBAR, fact.component)
+ assertEquals(Action.CLICK, fact.action)
+ assertEquals("menu", fact.item)
+ assertNull(fact.value)
+
+ assertNotNull(fact.metadata)
+
+ val metadata = fact.metadata!!
+ assertEquals(2, metadata.size)
+ assertTrue(metadata.containsKey("customTab"))
+ assertTrue(metadata.containsKey("test"))
+ assertEquals(true, metadata["customTab"])
+ assertEquals("23", metadata["test"])
+ }
+ }
+
+ @Test
+ fun `clicking on site security indicator invokes listener`() {
+ var listenerInvoked = false
+
+ val (_, displayToolbar) = createDisplayToolbar()
+
+ assertNull(displayToolbar.views.securityIndicator.background)
+
+ displayToolbar.setOnSiteSecurityClickedListener {
+ listenerInvoked = true
+ }
+
+ assertNotNull(displayToolbar.views.securityIndicator.background)
+
+ displayToolbar.views.securityIndicator.performClick()
+
+ assertTrue(listenerInvoked)
+
+ listenerInvoked = false
+
+ displayToolbar.setOnSiteSecurityClickedListener { }
+
+ assertNotNull(displayToolbar.views.securityIndicator.background)
+
+ displayToolbar.views.securityIndicator.performClick()
+
+ assertFalse(listenerInvoked)
+
+ displayToolbar.setOnSiteSecurityClickedListener(null)
+
+ assertNull(displayToolbar.views.securityIndicator.background)
+ }
+
+ @Test
+ fun `Security icon has proper content description`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+ val siteSecurityIconView = displayToolbar.views.securityIndicator
+
+ assertNotNull(siteSecurityIconView.contentDescription)
+ assertEquals(
+ testContext.getString(R.string.mozac_browser_toolbar_content_description_site_info),
+ siteSecurityIconView.contentDescription,
+ )
+ }
+
+ @Test
+ fun `Backgrounding the app dismisses menu if already open`() {
+ var wasDismissed = false
+ val (_, displayToolbar) = createDisplayToolbar()
+ val menuView = displayToolbar.views.menu
+ menuView.impl.register(
+ object : MenuButton.Observer {
+ override fun onDismiss() {
+ wasDismissed = true
+ }
+ },
+ )
+ menuView.menuBuilder = BrowserMenuBuilder(emptyList())
+ menuView.impl.performClick()
+
+ displayToolbar.onStop()
+
+ assertTrue(wasDismissed)
+ }
+
+ @Test
+ fun `set a dismiss lambda on the menu button`() {
+ var wasDismissed = false
+ val (_, displayToolbar) = createDisplayToolbar()
+ displayToolbar.setMenuDismissAction { wasDismissed = true }
+ val menuView = displayToolbar.views.menu
+ menuView.menuBuilder = BrowserMenuBuilder(emptyList())
+ menuView.impl.performClick()
+
+ menuView.dismissMenu()
+ assertTrue(wasDismissed)
+ }
+
+ @Test
+ fun `url formatter used if provided`() {
+ val (_, displayToolbar) = createDisplayToolbar()
+ displayToolbar.url = "https://mozilla.org"
+ assertEquals(displayToolbar.url, displayToolbar.views.origin.url)
+
+ displayToolbar.urlFormatter = { it.replace("https://".toRegex(), "") }
+ displayToolbar.url = "https://mozilla.org"
+ assertEquals("mozilla.org", displayToolbar.views.origin.url)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/HighlightViewTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/HighlightViewTest.kt
new file mode 100644
index 0000000000..3b4b68bfae
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/HighlightViewTest.kt
@@ -0,0 +1,67 @@
+/* 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.browser.toolbar.display
+
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.toolbar.R
+import mozilla.components.concept.toolbar.Toolbar.Highlight.NONE
+import mozilla.components.concept.toolbar.Toolbar.Highlight.PERMISSIONS_CHANGED
+import mozilla.components.support.test.robolectric.testContext
+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.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class HighlightViewTest {
+
+ @Test
+ fun `after setting tint, can get trackingProtectionTint`() {
+ val view = HighlightView(testContext)
+ view.setTint(android.R.color.black)
+ assertEquals(android.R.color.black, view.highlightTint)
+ }
+
+ @Test
+ fun `setting status will trigger an icon updated`() {
+ val view = HighlightView(testContext)
+
+ view.state = PERMISSIONS_CHANGED
+
+ assertEquals(PERMISSIONS_CHANGED, view.state)
+ assertTrue(view.isVisible)
+ assertNotNull(view.drawable)
+ assertEquals(
+ view.contentDescription,
+ testContext.getString(R.string.mozac_browser_toolbar_content_description_autoplay_blocked),
+ )
+
+ view.state = NONE
+
+ assertEquals(NONE, view.state)
+ assertNull(view.drawable)
+ assertFalse(view.isVisible)
+ assertNull(view.contentDescription)
+ }
+
+ @Test
+ fun `setIcons will trigger an icon updated`() {
+ val view = spy(HighlightView(testContext))
+
+ view.setIcon(
+ testContext.getDrawable(
+ TrackingProtectionIconView.DEFAULT_ICON_ON_NO_TRACKERS_BLOCKED,
+ )!!,
+ )
+
+ verify(view).updateIcon()
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/MenuButtonTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/MenuButtonTest.kt
new file mode 100644
index 0000000000..865c1a74ee
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/MenuButtonTest.kt
@@ -0,0 +1,156 @@
+/* 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.browser.toolbar.display
+
+import android.graphics.Color
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.ext.getHighlight
+import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidJUnit4::class)
+class MenuButtonTest {
+ @Mock private lateinit var menuBuilder: BrowserMenuBuilder
+
+ @Mock private lateinit var menuController: MenuController
+
+ @Mock private lateinit var menu: BrowserMenu
+
+ @Mock private lateinit var menuButtonInternal: mozilla.components.browser.menu.view.MenuButton
+ private lateinit var menuButton: MenuButton
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ `when`(menuBuilder.build(testContext)).thenReturn(menu)
+ `when`(menuButtonInternal.context).thenReturn(testContext)
+
+ menuButton = MenuButton(menuButtonInternal)
+ }
+
+ @Test
+ fun `menu button is visible only if menu builder attached`() {
+ verify(menuButtonInternal).visibility = View.GONE
+
+ `when`(menuButtonInternal.menuBuilder).thenReturn(mock())
+ assertTrue(menuButton.shouldBeVisible())
+
+ `when`(menuButtonInternal.menuBuilder).thenReturn(null)
+ assertFalse(menuButton.shouldBeVisible())
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `menu button sets onDismiss action`() {
+ val action = {}
+ menuButton.setMenuDismissAction(action)
+
+ verify(menuButtonInternal).onDismiss = action
+ }
+
+ @Test
+ fun `icon displays dot if low highlighted item is present in menu`() {
+ verify(menuButtonInternal, never()).invalidateBrowserMenu()
+ verify(menuButtonInternal, never()).setHighlight(any())
+
+ var isHighlighted = false
+ val highlight = BrowserMenuHighlight.LowPriority(Color.YELLOW)
+ val highlightMenuBuilder = spy(
+ BrowserMenuBuilder(
+ listOf(
+ BrowserMenuHighlightableItem(
+ label = "Test",
+ startImageResource = 0,
+ highlight = highlight,
+ isHighlighted = { isHighlighted },
+ ),
+ ),
+ ),
+ )
+ doReturn(menu).`when`(highlightMenuBuilder).build(testContext)
+
+ menuButton.menuBuilder = highlightMenuBuilder
+ `when`(menuButtonInternal.menuBuilder).thenReturn(highlightMenuBuilder)
+ menuButton.invalidateMenu()
+
+ verify(menuButtonInternal).setHighlight(null)
+
+ isHighlighted = true
+ menuButton.invalidateMenu()
+
+ assertEquals(highlight, highlightMenuBuilder.items.getHighlight())
+ verify(menuButtonInternal).setHighlight(highlight)
+ }
+
+ @Test
+ fun `invalidateMenu should invalidate the internal menu`() {
+ `when`(menuButtonInternal.menuController).thenReturn(null)
+ `when`(menuButtonInternal.menuBuilder).thenReturn(mock())
+ verify(menuButtonInternal, never()).invalidateBrowserMenu()
+
+ menuButton.invalidateMenu()
+
+ verify(menuButtonInternal).invalidateBrowserMenu()
+ }
+
+ @Test
+ fun `invalidateMenu should do nothing if using the menu controller`() {
+ `when`(menuButtonInternal.menuController).thenReturn(menuController)
+ `when`(menuButtonInternal.menuBuilder).thenReturn(null)
+ verify(menuButtonInternal, never()).invalidateBrowserMenu()
+
+ menuButton.invalidateMenu()
+
+ verify(menuButtonInternal, never()).invalidateBrowserMenu()
+ }
+
+ @Test
+ fun `invalidateMenu should automatically upgrade menu items if both builder and controller are present`() {
+ val onClick = {}
+ `when`(menuButtonInternal.menuController).thenReturn(menuController)
+ `when`(menuButtonInternal.menuBuilder).thenReturn(
+ BrowserMenuBuilder(
+ listOf(
+ SimpleBrowserMenuItem("Item 1", listener = onClick),
+ SimpleBrowserMenuItem("Item 2"),
+ ),
+ ),
+ )
+ verify(menuButtonInternal, never()).invalidateBrowserMenu()
+
+ menuButton.invalidateMenu()
+
+ verify(menuButtonInternal, never()).invalidateBrowserMenu()
+ verify(menuController).submitList(
+ listOf(
+ TextMenuCandidate("Item 1", onClick = onClick),
+ DecorativeTextMenuCandidate("Item 2"),
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconViewTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconViewTest.kt
new file mode 100644
index 0000000000..5f77d4aa04
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconViewTest.kt
@@ -0,0 +1,43 @@
+/* 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.browser.toolbar.display
+
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TrackingProtectionIconViewTest {
+
+ @Test
+ fun `After setting tint, can get trackingProtectionTint`() {
+ val view = TrackingProtectionIconView(testContext)
+ view.setTint(android.R.color.black)
+ assertEquals(android.R.color.black, view.trackingProtectionTint)
+ }
+
+ @Test
+ fun `colorFilter is cleared on animatable drawables`() {
+ val view = TrackingProtectionIconView(testContext)
+ view.trackingProtectionTint = android.R.color.black
+
+ val drawable = mock<Drawable>()
+ val animatedDrawable = mock<AnimatedVectorDrawable>()
+
+ view.setOrClearColorFilter(drawable)
+ assertEquals(PorterDuffColorFilter(android.R.color.black, PorterDuff.Mode.SRC_ATOP), view.colorFilter)
+
+ view.setOrClearColorFilter(animatedDrawable)
+ assertNotEquals(PorterDuffColorFilter(android.R.color.black, PorterDuff.Mode.SRC_ATOP), view.colorFilter)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt
new file mode 100644
index 0000000000..49aff8dadd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt
@@ -0,0 +1,290 @@
+/* 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.browser.toolbar.edit
+
+import android.view.KeyEvent
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.browser.toolbar.R
+import mozilla.components.concept.toolbar.AutocompleteDelegate
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
+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.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class EditToolbarTest {
+ private fun createEditToolbar(): Pair<BrowserToolbar, EditToolbar> {
+ val toolbar: BrowserToolbar = mock()
+ val displayToolbar = EditToolbar(
+ testContext,
+ toolbar,
+ View.inflate(testContext, R.layout.mozac_browser_toolbar_edittoolbar, null),
+ )
+ return Pair(toolbar, displayToolbar)
+ }
+
+ @Test
+ fun `entered text is forwarded to async autocomplete filter`() = runTest {
+ val toolbar = BrowserToolbar(testContext)
+
+ toolbar.edit.views.url.onAttachedToWindow()
+
+ val latch = CountDownLatch(1)
+ var invokedWithParams: List<Any?>? = null
+ toolbar.setAutocompleteListener { p1, p2 ->
+ invokedWithParams = listOf(p1, p2)
+ latch.countDown()
+ }
+
+ toolbar.edit.views.url.setText("Hello")
+
+ // Autocomplete filter will be invoked on a worker thread.
+ // Serialize here for the sake of tests.
+ latch.await()
+
+ assertEquals("Hello", invokedWithParams!![0])
+ assertTrue(invokedWithParams!![1] is AutocompleteDelegate)
+ }
+
+ @Test
+ fun `GIVEN existing user input WHEN a call to refresh autocomplete suggestions is made THEN retart the autocomplete functionality with the current text`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit.views.url.onAttachedToWindow()
+ // Fake existing user input.
+ toolbar.edit.views.url.setText("Test")
+ val latch = CountDownLatch(1)
+ var invokedWithParams: List<Any?>? = null
+ // Only now enable the autocomplete functionality.
+ toolbar.setAutocompleteListener { p1, p2 ->
+ invokedWithParams = listOf(p1, p2)
+ latch.countDown()
+ }
+
+ toolbar.refreshAutocomplete()
+
+ // Autocomplete filter will be invoked on a worker thread.
+ // Serialize here for the sake of tests.
+ latch.await()
+ assertEquals("Test", invokedWithParams!![0])
+ assertTrue(invokedWithParams!![1] is AutocompleteDelegate)
+ }
+
+ @Test
+ fun `focus change is forwarded to listener`() {
+ var listenerInvoked = false
+ var value = false
+
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit.setOnEditFocusChangeListener { hasFocus ->
+ listenerInvoked = true
+ value = hasFocus
+ }
+
+ // Switch to editing mode and focus view.
+ toolbar.editMode()
+ toolbar.edit.views.url.requestFocus()
+
+ assertTrue(listenerInvoked)
+ assertTrue(value)
+
+ // Switch back to display mode
+ listenerInvoked = false
+ toolbar.displayMode()
+
+ assertTrue(listenerInvoked)
+ assertFalse(value)
+ }
+
+ @Test
+ fun `entering text emits facts`() {
+ CollectionProcessor.withFactCollection { facts ->
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit.views.url.onAttachedToWindow()
+
+ assertEquals(0, facts.size)
+
+ toolbar.edit.views.url.setText("https://www.mozilla.org")
+ toolbar.edit.views.url.dispatchKeyEvent(
+ KeyEvent(
+ System.currentTimeMillis(),
+ System.currentTimeMillis(),
+ KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_ENTER,
+ 0,
+ ),
+ )
+
+ assertEquals(2, facts.size)
+
+ val factDetail = facts[0]
+ assertEquals(Component.UI_AUTOCOMPLETE, factDetail.component)
+ assertEquals(Action.IMPLEMENTATION_DETAIL, factDetail.action)
+ assertEquals("onTextChanged", factDetail.item)
+ assertEquals("InlineAutocompleteEditText", factDetail.value)
+
+ val fact = facts[1]
+ assertEquals(Component.BROWSER_TOOLBAR, fact.component)
+ assertEquals(Action.COMMIT, fact.action)
+ assertEquals("toolbar", fact.item)
+ assertNull(fact.value)
+
+ val metadata = fact.metadata
+ assertNotNull(metadata!!)
+ assertEquals(1, metadata.size)
+ assertTrue(metadata.contains("autocomplete"))
+ assertTrue(metadata["autocomplete"] is Boolean)
+ assertFalse(metadata["autocomplete"] as Boolean)
+ }
+ }
+
+ @Test
+ fun `entering text emits facts with autocomplete metadata`() {
+ CollectionProcessor.withFactCollection { facts ->
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit.views.url.onAttachedToWindow()
+
+ assertEquals(0, facts.size)
+
+ toolbar.edit.views.url.setText("https://www.mozilla.org")
+
+ // Fake autocomplete
+ toolbar.edit.views.url.autocompleteResult = InlineAutocompleteEditText.AutocompleteResult(
+ text = "hello world",
+ source = "test-source",
+ totalItems = 100,
+ )
+
+ toolbar.edit.views.url.dispatchKeyEvent(
+ KeyEvent(
+ System.currentTimeMillis(),
+ System.currentTimeMillis(),
+ KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_ENTER,
+ 0,
+ ),
+ )
+
+ assertEquals(2, facts.size)
+
+ val factDetail = facts[0]
+ assertEquals(Component.UI_AUTOCOMPLETE, factDetail.component)
+ assertEquals(Action.IMPLEMENTATION_DETAIL, factDetail.action)
+ assertEquals("onTextChanged", factDetail.item)
+ assertEquals("InlineAutocompleteEditText", factDetail.value)
+
+ val factCommit = facts[1]
+ assertEquals(Component.BROWSER_TOOLBAR, factCommit.component)
+ assertEquals(Action.COMMIT, factCommit.action)
+ assertEquals("toolbar", factCommit.item)
+ assertNull(factCommit.value)
+
+ val metadata = factCommit.metadata
+ assertNotNull(metadata!!)
+ assertEquals(2, metadata.size)
+
+ assertTrue(metadata.contains("autocomplete"))
+ assertTrue(metadata["autocomplete"] is Boolean)
+ assertTrue(metadata["autocomplete"] as Boolean)
+
+ assertTrue(metadata.contains("source"))
+ assertEquals("test-source", metadata["source"])
+ }
+ }
+
+ @Test
+ fun `clearView gone on init`() {
+ val (_, editToolbar) = createEditToolbar()
+ val clearView = editToolbar.views.clear
+ assertTrue(clearView.visibility == View.GONE)
+ }
+
+ @Test
+ fun `clearView visible on updateUrl`() {
+ val (_, editToolbar) = createEditToolbar()
+ val clearView = editToolbar.views.clear
+
+ editToolbar.updateUrl("TestUrl", false)
+ assertTrue(clearView.visibility == View.VISIBLE)
+ }
+
+ @Test
+ fun `WHEN shouldAppend is set to true updateUrl should append text`() {
+ val (_, editToolbar) = createEditToolbar()
+
+ // Initial state
+ editToolbar.updateUrl(url = "what ")
+
+ // Simulate text update with voice input
+ val actual = editToolbar.updateUrl(url = "is this", shouldAppend = true, shouldHighlight = true)
+ val expected = "what is this"
+
+ assertEquals(expected, actual)
+ assertEquals(expected, editToolbar.views.url.text.toString())
+ assertEquals(5, editToolbar.views.url.selectionStart)
+ assertEquals(12, editToolbar.views.url.selectionEnd)
+ }
+
+ @Test
+ fun `setIconClickListener sets a click listener on the icon view`() {
+ val (_, editToolbar) = createEditToolbar()
+ val iconView = editToolbar.views.icon
+ assertFalse(iconView.hasOnClickListeners())
+ editToolbar.setIconClickListener { /* noop */ }
+ assertTrue(iconView.hasOnClickListeners())
+ }
+
+ @Test
+ fun `clearView clears text in urlView`() {
+ val (_, editToolbar) = createEditToolbar()
+ val clearView = editToolbar.views.clear
+
+ editToolbar.views.url.setText("https://www.mozilla.org")
+ assertTrue(editToolbar.views.url.text.isNotBlank())
+
+ assertNotNull(clearView)
+ clearView.performClick()
+ assertTrue(editToolbar.views.url.text.isBlank())
+ }
+
+ @Test
+ fun `editSuggestion sets text in urlView`() {
+ val (_, editToolbar) = createEditToolbar()
+ val url = editToolbar.views.url
+
+ url.setText("https://www.mozilla.org")
+ assertEquals("https://www.mozilla.org", url.text.toString())
+
+ var callbackCalled = false
+
+ editToolbar.editListener = object : Toolbar.OnEditListener {
+ override fun onTextChanged(text: String) {
+ callbackCalled = true
+ }
+ }
+
+ editToolbar.editSuggestion("firefox")
+
+ assertEquals("firefox", url.text.toString())
+ assertTrue(callbackCalled)
+ assertEquals("firefox".length, url.selectionStart)
+ assertTrue(url.hasFocus())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/internal/ActionContainerTest.kt b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/internal/ActionContainerTest.kt
new file mode 100644
index 0000000000..d3f1ddda18
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/internal/ActionContainerTest.kt
@@ -0,0 +1,99 @@
+/* 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.browser.toolbar.internal
+
+import android.view.View
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class ActionContainerTest {
+ private lateinit var actionContainer: ActionContainer
+ private lateinit var browserAction: Toolbar.Action
+
+ @Before
+ fun setUp() {
+ browserAction = BrowserToolbar.Button(
+ imageDrawable = mock(),
+ contentDescription = "Test",
+ visible = { true },
+ autoHide = { true },
+ weight = { 2 },
+ listener = mock(),
+ )
+ actionContainer = ActionContainer(testContext)
+ }
+
+ @Test
+ fun `GIVEN multiple actions with different weights WHEN calculateInsertionIndex is called THEN action is placed at right index`() {
+ actionContainer.addAction(
+ BrowserToolbar.Button(
+ imageDrawable = mock(),
+ contentDescription = "Share",
+ visible = { true },
+ weight = { 1 },
+ listener = mock(),
+ ),
+ )
+ actionContainer.addAction(
+ BrowserToolbar.Button(
+ imageDrawable = mock(),
+ contentDescription = "Reload",
+ visible = { true },
+ weight = { 3 },
+ listener = mock(),
+ ),
+ )
+ val newAction =
+ BrowserToolbar.Button(
+ imageDrawable = mock(),
+ contentDescription = "Translation",
+ visible = { true },
+ weight = { 2 },
+ listener = mock(),
+ )
+
+ val insertionIndex = actionContainer.calculateInsertionIndex(newAction)
+
+ assertEquals("The insertion index should be", 1, insertionIndex)
+ }
+
+ @Test
+ fun `WHEN addAction is called THEN child views are increased`() {
+ actionContainer.addAction(browserAction)
+
+ assertEquals(1, actionContainer.childCount)
+ }
+
+ @Test
+ fun `WHEN removeAction is called THEN child views are decreased`() {
+ actionContainer.addAction(browserAction)
+ actionContainer.removeAction(browserAction)
+
+ assertEquals(0, actionContainer.childCount)
+ }
+
+ @Test
+ fun `WHEN invalidateAction is called THEN action visibility is reconsidered`() {
+ val browserToolbarAction = BrowserToolbar.Button(
+ imageDrawable = mock(),
+ contentDescription = "Translation",
+ visible = { false },
+ weight = { 2 },
+ listener = mock(),
+ )
+ actionContainer.addAction(browserToolbarAction)
+ actionContainer.invalidateActions()
+
+ assertEquals(View.GONE, actionContainer.visibility)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/toolbar/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/compose/awesomebar/README.md b/mobile/android/android-components/components/compose/awesomebar/README.md
new file mode 100644
index 0000000000..6d1e606e39
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Compose > Awesomebar
+
+A customizable awesomebar for browsers using Jetpack Compose.
+
+## Usage
+
+### 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:compose-awesomebar:{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/compose/awesomebar/build.gradle b/mobile/android/android-components/components/compose/awesomebar/build.gradle
new file mode 100644
index 0000000000..65691db0e5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/build.gradle
@@ -0,0 +1,68 @@
+/* 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: 'kotlin-parcelize'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.compose.browser.awesomebar'
+
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-Xjvm-default=all"
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(":concept-awesomebar")
+ implementation project(":browser-state")
+ implementation project(":support-base")
+ implementation project(":support-utils")
+ implementation project(":ui-icons")
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+
+ debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_compose_ui_test
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/compose/awesomebar/proguard-rules.pro b/mobile/android/android-components/components/compose/awesomebar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/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/compose/awesomebar/src/main/AndroidManifest.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/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/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBar.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBar.kt
new file mode 100644
index 0000000000..a5d3f10e2a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBar.kt
@@ -0,0 +1,121 @@
+/* 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.compose.browser.awesomebar
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import mozilla.components.compose.browser.awesomebar.internal.SuggestionFetcher
+import mozilla.components.compose.browser.awesomebar.internal.Suggestions
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.base.profiler.Profiler
+
+/**
+ * An awesome bar displaying suggestions from the list of provided [AwesomeBar.SuggestionProvider]s.
+ *
+ * @param text The text entered by the user and for which the AwesomeBar should show suggestions for.
+ * @param colors The color scheme the AwesomeBar will use for the UI.
+ * @param providers The list of suggestion providers to query whenever the [text] changes.
+ * @param orientation Whether the AwesomeBar is oriented to the top or the bottom of the screen.
+ * @param onSuggestionClicked Gets invoked whenever the user clicks on a suggestion in the AwesomeBar.
+ * @param onAutoComplete Gets invoked when the user clicks on the "autocomplete" icon of a suggestion.
+ * @param onVisibilityStateUpdated Gets invoked when the list of currently displayed suggestions changes.
+ * @param onScroll Gets invoked at the beginning of the user performing a scroll gesture.
+ */
+@Composable
+fun AwesomeBar(
+ text: String,
+ colors: AwesomeBarColors = AwesomeBarDefaults.colors(),
+ providers: List<AwesomeBar.SuggestionProvider>,
+ orientation: AwesomeBarOrientation = AwesomeBarOrientation.TOP,
+ onSuggestionClicked: (AwesomeBar.Suggestion) -> Unit,
+ onAutoComplete: (AwesomeBar.Suggestion) -> Unit,
+ onVisibilityStateUpdated: (AwesomeBar.VisibilityState) -> Unit = {},
+ onScroll: () -> Unit = {},
+ profiler: Profiler? = null,
+) {
+ val groups = remember(providers) {
+ providers
+ .groupBy { it.groupTitle() }
+ .map {
+ AwesomeBar.SuggestionProviderGroup(
+ providers = it.value,
+ title = it.key,
+ )
+ }
+ }
+
+ AwesomeBar(
+ text = text,
+ colors = colors,
+ groups = groups,
+ orientation = orientation,
+ onSuggestionClicked = { _, suggestion -> onSuggestionClicked(suggestion) },
+ onAutoComplete = { _, suggestion -> onAutoComplete(suggestion) },
+ onVisibilityStateUpdated = onVisibilityStateUpdated,
+ onScroll = onScroll,
+ profiler = profiler,
+ )
+}
+
+/**
+ * An awesome bar displaying suggestions in groups from the list of provided [AwesomeBar.SuggestionProviderGroup]s.
+ *
+ * @param text The text entered by the user and for which the AwesomeBar should show suggestions for.
+ * @param colors The color scheme the AwesomeBar will use for the UI.
+ * @param groups The list of groups of suggestion providers to query whenever the [text] changes.
+ * @param orientation Whether the AwesomeBar is oriented to the top or the bottom of the screen.
+ * @param onSuggestionClicked Gets invoked whenever the user clicks on a suggestion in the AwesomeBar.
+ * @param onAutoComplete Gets invoked when the user clicks on the "autocomplete" icon of a suggestion.
+ * @param onVisibilityStateUpdated Gets invoked when the list of currently displayed suggestions changes.
+ * @param onScroll Gets invoked at the beginning of the user performing a scroll gesture.
+ */
+@Composable
+fun AwesomeBar(
+ text: String,
+ colors: AwesomeBarColors = AwesomeBarDefaults.colors(),
+ groups: List<AwesomeBar.SuggestionProviderGroup>,
+ orientation: AwesomeBarOrientation = AwesomeBarOrientation.TOP,
+ onSuggestionClicked: (AwesomeBar.SuggestionProviderGroup, AwesomeBar.Suggestion) -> Unit,
+ onAutoComplete: (AwesomeBar.SuggestionProviderGroup, AwesomeBar.Suggestion) -> Unit,
+ onVisibilityStateUpdated: (AwesomeBar.VisibilityState) -> Unit = {},
+ onScroll: () -> Unit = {},
+ profiler: Profiler? = null,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag("mozac.awesomebar")
+ .background(colors.background),
+ ) {
+ val fetcher = remember(groups) { SuggestionFetcher(groups, profiler) }
+
+ // This state does not need to be remembered, because it can change if the providers list changes.
+ @SuppressLint("UnrememberedMutableState")
+ val suggestions = derivedStateOf { fetcher.state.value }.value.toList()
+ .sortedByDescending { it.first.priority }.toMap(LinkedHashMap())
+
+ LaunchedEffect(text, fetcher) {
+ fetcher.fetch(text)
+ }
+
+ Suggestions(
+ suggestions,
+ colors,
+ orientation,
+ onSuggestionClicked,
+ onAutoComplete,
+ onVisibilityStateUpdated,
+ onScroll,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarColors.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarColors.kt
new file mode 100644
index 0000000000..fe31e36153
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarColors.kt
@@ -0,0 +1,18 @@
+/* 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.compose.browser.awesomebar
+
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Represents the colors used by the AwesomeBar.
+ */
+data class AwesomeBarColors(
+ val background: Color,
+ val title: Color,
+ val description: Color,
+ val autocompleteIcon: Color,
+ val groupTitle: Color,
+)
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarDefaults.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarDefaults.kt
new file mode 100644
index 0000000000..a804ad86df
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarDefaults.kt
@@ -0,0 +1,39 @@
+/* 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.compose.browser.awesomebar
+
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Contains the default values used by the AwesomeBar.
+ */
+object AwesomeBarDefaults {
+ /**
+ * Creates an [AwesomeBarColors] that represents the default colors used in an AwesomeBar.
+ *
+ * @param background The background of the AwesomeBar.
+ * @param title The text color for the title of a suggestion.
+ * @param description The text color for the description of a suggestion.
+ */
+ @Composable
+ fun colors(
+ background: Color = MaterialTheme.colors.background,
+ title: Color = MaterialTheme.colors.onBackground,
+ description: Color = MaterialTheme.colors.onBackground.copy(
+ alpha = ContentAlpha.medium,
+ ),
+ autocompleteIcon: Color = MaterialTheme.colors.onSurface,
+ groupTitle: Color = MaterialTheme.colors.onBackground,
+ ) = AwesomeBarColors(
+ background,
+ title,
+ description,
+ autocompleteIcon,
+ groupTitle,
+ )
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarFacts.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarFacts.kt
new file mode 100644
index 0000000000..bfa748cb06
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarFacts.kt
@@ -0,0 +1,44 @@
+/* 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.compose.browser.awesomebar
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the AwesomeBar composable.
+ */
+object AwesomeBarFacts {
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val PROVIDER_DURATION = "provider_duration"
+ }
+
+ /**
+ * Keys used to record metadata about [Items].
+ */
+ object MetadataKeys {
+ const val DURATION_PAIR = "duration_pair"
+ }
+
+ internal fun emitAwesomeBarFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+ ) {
+ Fact(
+ Component.COMPOSE_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+ }
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarOrientation.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarOrientation.kt
new file mode 100644
index 0000000000..246f917c1e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/AwesomeBarOrientation.kt
@@ -0,0 +1,13 @@
+/* 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.compose.browser.awesomebar
+
+/**
+ * The orientation of the AwesomeBar, whether it's oriented to the bottom or the top.
+ */
+enum class AwesomeBarOrientation {
+ TOP,
+ BOTTOM,
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/Suggestion.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/Suggestion.kt
new file mode 100644
index 0000000000..275e336414
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/Suggestion.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.compose.browser.awesomebar.internal
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.graphics.drawable.toBitmap
+import mozilla.components.compose.browser.awesomebar.AwesomeBarColors
+import mozilla.components.compose.browser.awesomebar.AwesomeBarOrientation
+import mozilla.components.compose.browser.awesomebar.R
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.ui.icons.R as iconsR
+
+// We only show one row of text, covering at max screen width.
+// Limit bigger texts that could cause slowdowns or even crashes.
+private const val SUGGESTION_TEXT_MAX_LENGTH = 100
+
+@Composable
+internal fun Suggestion(
+ suggestion: AwesomeBar.Suggestion,
+ colors: AwesomeBarColors,
+ orientation: AwesomeBarOrientation,
+ onSuggestionClicked: () -> Unit,
+ onAutoComplete: () -> Unit,
+) {
+ Row(
+ modifier = Modifier
+ .clickable { onSuggestionClicked() }
+ .defaultMinSize(minHeight = 56.dp)
+ .testTag("mozac.awesomebar.suggestion")
+ .padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 8.dp),
+ ) {
+ val icon = suggestion.icon
+ if (icon != null) {
+ SuggestionIcon(
+ icon = icon,
+ indicator = suggestion.indicatorIcon,
+ )
+ }
+ SuggestionTitleAndDescription(
+ title = suggestion.title?.take(SUGGESTION_TEXT_MAX_LENGTH),
+ description = suggestion.description?.take(SUGGESTION_TEXT_MAX_LENGTH),
+ colors = colors,
+ modifier = Modifier
+ .weight(1f)
+ .align(Alignment.CenterVertically),
+ )
+ if (suggestion.editSuggestion != null) {
+ AutocompleteButton(
+ onAutoComplete = onAutoComplete,
+ orientation = orientation,
+ colors = colors,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ }
+}
+
+@Composable
+private fun SuggestionTitleAndDescription(
+ title: String?,
+ description: String?,
+ colors: AwesomeBarColors,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ ) {
+ Text(
+ text = if (title.isNullOrEmpty()) {
+ description ?: ""
+ } else {
+ title
+ },
+ color = colors.title,
+ fontSize = 15.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier
+ .width(IntrinsicSize.Max)
+ .padding(start = 2.dp, end = 8.dp),
+ )
+ if (description?.isNotEmpty() == true) {
+ Text(
+ text = description,
+ color = colors.description,
+ fontSize = 12.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier
+ .width(IntrinsicSize.Max)
+ .padding(start = 2.dp, end = 8.dp),
+ )
+ }
+ }
+}
+
+@Composable
+private fun SuggestionIcon(
+ icon: Bitmap,
+ indicator: Drawable?,
+) {
+ Box(
+ modifier = Modifier
+ .width(30.dp)
+ .height(38.dp),
+ ) {
+ Image(
+ icon.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .width(24.dp)
+ .height(24.dp),
+ contentScale = ContentScale.Crop,
+ )
+ if (indicator != null) {
+ Image(
+ indicator.toBitmap().asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 22.dp, start = 14.dp)
+ .width(16.dp)
+ .height(16.dp),
+ )
+ }
+ }
+}
+
+@Composable
+@Suppress("MagicNumber")
+private fun AutocompleteButton(
+ onAutoComplete: () -> Unit,
+ colors: AwesomeBarColors,
+ orientation: AwesomeBarOrientation,
+ modifier: Modifier,
+) {
+ Image(
+ painterResource(iconsR.drawable.mozac_ic_append_up_left_24),
+ colorFilter = ColorFilter.tint(colors.autocompleteIcon),
+ contentDescription = stringResource(R.string.mozac_browser_awesomebar_edit_suggestion),
+ modifier = modifier
+ .size(48.dp)
+ .rotate(
+ if (orientation == AwesomeBarOrientation.BOTTOM) {
+ 270f
+ } else {
+ 0f
+ },
+ )
+ .clickable { onAutoComplete() }
+ .padding(12.dp),
+ )
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionFetcher.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionFetcher.kt
new file mode 100644
index 0000000000..354d2a25c5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionFetcher.kt
@@ -0,0 +1,160 @@
+/* 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.compose.browser.awesomebar.internal
+
+import android.os.SystemClock
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import mozilla.components.compose.browser.awesomebar.AwesomeBarFacts
+import mozilla.components.compose.browser.awesomebar.AwesomeBarFacts.emitAwesomeBarFact
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.base.profiler.Profiler
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.utils.NamedThreadFactory
+import mozilla.components.support.utils.ThreadUtils
+import java.util.concurrent.Executors
+
+/**
+ * Class responsible for fetching search suggestions and exposing a [state] to observe the current
+ * list of suggestions from a composable.
+ */
+internal class SuggestionFetcher(
+ private val groups: List<AwesomeBar.SuggestionProviderGroup>,
+ private val profiler: Profiler?,
+) : RememberObserver {
+ private val dispatcher = Executors.newFixedThreadPool(
+ groups.fold(0, { acc, group -> acc + group.providers.size }),
+ NamedThreadFactory("SuggestionFetcher"),
+ ).asCoroutineDispatcher()
+
+ @VisibleForTesting
+ internal var fetchJob: Job? = null
+
+ /**
+ * The current list of suggestions as an observable list.
+ */
+ val state = mutableStateOf<Map<AwesomeBar.SuggestionProviderGroup, List<AwesomeBar.Suggestion>>>(emptyMap())
+
+ /**
+ * Fetches suggestions for [text] from all providers in all [groups] asynchronously.
+ *
+ * The [state] property will be updated whenever new suggestions are available.
+ */
+ suspend fun fetch(text: String) {
+ profiler?.addMarker("SuggestionFetcher.fetch") // DO NOT ADD ANYTHING ABOVE THIS addMarker CALL.
+
+ fetchJob?.cancel()
+
+ fetchJob = CoroutineScope(dispatcher).launch {
+ groups.forEach { group ->
+ group.providers.forEach { provider ->
+ val profilerStartTime = profiler?.getProfilerTime() // DO NOT ADD ANYTHING ABOVE getProfilerTime.
+ launch(dispatcher) { fetchFrom(group, provider, text, profilerStartTime) }
+ }
+ }
+ }
+ }
+
+ /**
+ * Fetches suggestions from [provider].
+ */
+ @VisibleForTesting
+ internal suspend fun fetchFrom(
+ group: AwesomeBar.SuggestionProviderGroup,
+ provider: AwesomeBar.SuggestionProvider,
+ text: String,
+ profilerStartTime: Double?,
+ ) {
+ // At this point, we have a timing value for a provider.
+ // We have a choice here - we can try grouping different timings together for a
+ // single user input value, or we can just treat them as entirely independent values.
+ // These timings are correlated with each other in a sense that they act on the same
+ // inputs, and are executed at the same time. However, our goal here is to track performance
+ // of the providers. Each provider acts independently from another; recording their performance
+ // at an individual level will allow us to track that performance over time.
+ // Tracked value will be reflected both in perceived user experience (how quickly results from
+ // a provider show up), and in a purely technical interpretation of how quickly providers
+ // fulfill requests.
+ // Grouping also poses timing challenges - as user is typing, we're trying to cancel these
+ // provider requests. Given that each request can take an arbitrary amount of time to execute,
+ // grouping correctly becomes tricky and we run a risk of omitting certain values - or, of
+ // adding a bunch of complexity just for the sake of "correct grouping".
+ val start = SystemClock.elapsedRealtimeNanos()
+ val suggestions = provider.onInputChanged(text)
+ val end = SystemClock.elapsedRealtimeNanos()
+ emitProviderQueryTimingFact(provider, timingNs = end - start)
+
+ processResultFrom(group, provider, suggestions, profilerStartTime)
+ }
+
+ /**
+ * Updates [state] to include the [suggestions] from [provider].
+ */
+ @Synchronized
+ @VisibleForTesting
+ internal fun processResultFrom(
+ group: AwesomeBar.SuggestionProviderGroup,
+ provider: AwesomeBar.SuggestionProvider,
+ suggestions: List<AwesomeBar.Suggestion>,
+ profilerStartTime: Double?,
+ ) {
+ val suggestionMap = state.value
+
+ val updatedSuggestions = (suggestionMap[group] ?: emptyList())
+ .filter { suggestion -> suggestion.provider != provider }
+ .toMutableList()
+
+ updatedSuggestions.addAll(suggestions)
+ updatedSuggestions.sortByDescending { suggestion -> suggestion.score }
+
+ if (updatedSuggestions.isNotEmpty()) {
+ group.priority = updatedSuggestions[0].score
+ }
+
+ val updatedSuggestionMap = suggestionMap.toMutableMap()
+ updatedSuggestionMap[group] = updatedSuggestions
+ state.value = updatedSuggestionMap
+ val profilerEndTime = profiler?.getProfilerTime() // THIS MUST OCCUR RIGHT AFTER STATE UPDATE.
+
+ // Markers can only be added on the main thread right now.
+ profiler?.let {
+ ThreadUtils.postToMainThread {
+ profiler.addMarker(
+ "Suggestion update",
+ profilerStartTime,
+ profilerEndTime,
+ provider::class.simpleName,
+ )
+ }
+ }
+ }
+
+ override fun onAbandoned() {
+ dispatcher.close()
+ }
+
+ override fun onForgotten() {
+ dispatcher.close()
+ }
+
+ override fun onRemembered() = Unit
+}
+
+@Suppress("MagicNumber")
+internal fun emitProviderQueryTimingFact(provider: AwesomeBar.SuggestionProvider, timingNs: Long) {
+ emitAwesomeBarFact(
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.PROVIDER_DURATION,
+ metadata = mapOf(
+ // We only care about millisecond precision here, so convert from ns to ms before emitting.
+ AwesomeBarFacts.MetadataKeys.DURATION_PAIR to (provider to (timingNs / 1_000_000L)),
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionGroup.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionGroup.kt
new file mode 100644
index 0000000000..0d76d0c21a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionGroup.kt
@@ -0,0 +1,35 @@
+/* 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.compose.browser.awesomebar.internal
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import mozilla.components.compose.browser.awesomebar.AwesomeBarColors
+
+/**
+ * Renders a header for a group of suggestions.
+ */
+@Composable
+internal fun SuggestionGroup(
+ title: String,
+ colors: AwesomeBarColors,
+) {
+ Text(
+ title,
+ color = colors.groupTitle,
+ modifier = Modifier
+ .padding(
+ vertical = 12.dp,
+ horizontal = 16.dp,
+ )
+ .fillMaxWidth(),
+ fontSize = 14.sp,
+ )
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/Suggestions.kt b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/Suggestions.kt
new file mode 100644
index 0000000000..b505d1503f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/java/mozilla/components/compose/browser/awesomebar/internal/Suggestions.kt
@@ -0,0 +1,168 @@
+/* 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.compose.browser.awesomebar.internal
+
+import android.os.Parcelable
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.parcelize.Parcelize
+import mozilla.components.compose.browser.awesomebar.AwesomeBarColors
+import mozilla.components.compose.browser.awesomebar.AwesomeBarOrientation
+import mozilla.components.concept.awesomebar.AwesomeBar
+
+@Composable
+internal fun Suggestions(
+ suggestions: Map<AwesomeBar.SuggestionProviderGroup, List<AwesomeBar.Suggestion>>,
+ colors: AwesomeBarColors,
+ orientation: AwesomeBarOrientation,
+ onSuggestionClicked: (AwesomeBar.SuggestionProviderGroup, AwesomeBar.Suggestion) -> Unit,
+ onAutoComplete: (AwesomeBar.SuggestionProviderGroup, AwesomeBar.Suggestion) -> Unit,
+ onVisibilityStateUpdated: (AwesomeBar.VisibilityState) -> Unit,
+ onScroll: () -> Unit,
+) {
+ val state = rememberLazyListState()
+
+ ScrollHandler(state, onScroll)
+
+ LazyColumn(
+ state = state,
+ modifier = Modifier.testTag("mozac.awesomebar.suggestions"),
+ ) {
+ suggestions.forEach { (group, suggestions) ->
+ val title = group.title
+ if (suggestions.isNotEmpty() && !title.isNullOrEmpty()) {
+ item(ItemKey.SuggestionGroup(group.id)) {
+ SuggestionGroup(title, colors)
+ }
+ }
+
+ items(
+ items = suggestions.take(group.limit),
+ key = { suggestion -> ItemKey.Suggestion(group.id, suggestion.provider.id, suggestion.id) },
+ ) { suggestion ->
+ Suggestion(
+ suggestion,
+ colors,
+ orientation,
+ onSuggestionClicked = { onSuggestionClicked(group, suggestion) },
+ onAutoComplete = { onAutoComplete(group, suggestion) },
+ )
+ }
+ }
+ }
+
+ val currentSuggestions by rememberUpdatedState(suggestions)
+ val currentOnVisibilityStateUpdated by rememberUpdatedState(onVisibilityStateUpdated)
+
+ LaunchedEffect(Unit) {
+ // This effect is launched on the initial composition, and cancelled when `Suggestions` leaves the Composition.
+ // The flow below emits new values when either the latest list of suggestions changes, or the visibility or
+ // position of any suggestion in that list changes.
+ snapshotFlow { currentSuggestions }
+ .combine(snapshotFlow { state.layoutInfo.visibleItemsInfo }) { suggestions, visibleItemsInfo ->
+ VisibleItems(
+ suggestions = suggestions,
+ visibleItemKeys = visibleItemsInfo.map { it.key as ItemKey },
+ )
+ }
+ .distinctUntilChangedBy { it.visibleItemKeys }
+ .collect { currentOnVisibilityStateUpdated(it.toVisibilityState()) }
+ }
+}
+
+/**
+ * An effect for handling scrolls in a [LazyColumn]. Will invoke [onScroll] at the beginning
+ * of a scroll gesture.
+ */
+@Composable
+private fun ScrollHandler(
+ state: LazyListState,
+ onScroll: () -> Unit,
+) {
+ val scrollInProgress = state.isScrollInProgress
+ remember(scrollInProgress) {
+ ScrollHandlerImpl(scrollInProgress, onScroll)
+ }
+}
+
+/**
+ * [RememberObserver] implementation that will make sure that [onScroll] get called only once as
+ * long as [scrollInProgress] doesn't change.
+ */
+private class ScrollHandlerImpl(
+ private val scrollInProgress: Boolean,
+ private val onScroll: () -> Unit,
+) : RememberObserver {
+ override fun onAbandoned() = Unit
+ override fun onForgotten() = Unit
+
+ override fun onRemembered() {
+ if (scrollInProgress) {
+ onScroll()
+ }
+ }
+}
+
+/**
+ * A stable, unique key for an item in the [Suggestions] list.
+ */
+internal sealed interface ItemKey {
+ @Parcelize
+ data class SuggestionGroup(val id: String) : ItemKey, Parcelable
+
+ @Parcelize
+ data class Suggestion(
+ val groupId: String,
+ val providerId: String,
+ val suggestionId: String,
+ ) : ItemKey, Parcelable
+}
+
+/**
+ * A snapshot of all the fetched suggestions to show in the [Suggestions] list, and the keys of the visible items
+ * in that list, ordered top to bottom. The intersection of the two is the current [AwesomeBar.VisibilityState].
+ */
+internal data class VisibleItems(
+ val suggestions: Map<AwesomeBar.SuggestionProviderGroup, List<AwesomeBar.Suggestion>>,
+ val visibleItemKeys: List<ItemKey>,
+) {
+ fun toVisibilityState(): AwesomeBar.VisibilityState =
+ if (visibleItemKeys.isEmpty()) {
+ AwesomeBar.VisibilityState()
+ } else {
+ AwesomeBar.VisibilityState(
+ // `suggestions` is insertion-ordered, and `toMap()` preserves that order, so the groups in
+ // `visibleProviderGroups` are in the same order as they're shown in the awesomebar.
+ visibleProviderGroups = suggestions.mapNotNull { (group, suggestions) ->
+ val visibleSuggestions = suggestions.filter { suggestion ->
+ val suggestionItemKey = ItemKey.Suggestion(
+ groupId = group.id,
+ providerId = suggestion.provider.id,
+ suggestionId = suggestion.id,
+ )
+ visibleItemKeys.contains(suggestionItemKey)
+ }
+ if (visibleSuggestions.isNotEmpty()) {
+ group to visibleSuggestions
+ } else {
+ null
+ }
+ }.toMap(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..9c811b081b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">ጥቆማን ተቀበል እና አርትዕ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..fca9a6d723
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ar/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">اقبل الاقتراح وحرّره</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..c3681052e8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceptar ya editar la suxerencia</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..6856d82e37
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">تکلیف‌لری قبول و دوزه‌لیش ائدین</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..5592ff756e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Cumpu miwah uah saran</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..7a05d5c150
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Прыняць і рэдагаваць прапанову</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..fee7ceb76d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Приемане и редакция на предложението</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..647d3ccc51
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Asantiñ hag embann ar c‘hinnig</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..12124a89c9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Prihvati i uredi prijedlog</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..db7a40ac20
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Accepta i edita el suggeriment</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..e0ec236f51
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Tik\'ul chuqa\' tinuk\' chilab\'enïk</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..33fd4d981a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Dawata ug usba ang sugyot</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..32a9820f12
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">ڕازیبە و پێشنیارەکە دەستکاریبکە</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..7485b24bbe
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Accettà è mudificà a suggestione</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..ef57156331
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Přijmout a upravit návrh</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..8476dd6ad7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Derbyn a golygu awgrym</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..4dd702661c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Accepter og rediger forslag</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..a3e5839463
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Vorschlag annehmen und bearbeiten</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..6e0d778296
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Naraźenje akceptěrowaś a wobźěłaś</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..f66fb02587
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Αποδοχή και επεξεργασία πρότασης</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..a5fd6b47e3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Accept and edit suggestion</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..a5fd6b47e3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Accept and edit suggestion</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..79f9e026ed
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Akcepti kaj modifi sugeston</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..76061a6811
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceptar y editar sugerencia</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..76061a6811
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceptar y editar sugerencia</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..76061a6811
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceptar y editar sugerencia</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..76061a6811
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceptar y editar sugerencia</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..76061a6811
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceptar y editar sugerencia</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..30d1bc57c6
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Nõustu ja muuda soovitust</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..c2d90b206e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Onartu eta editatu iradokizuna</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..51d494d857
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">پذیرش و ویرایش پیشنهاد</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..26c1bf745d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Hyväksy ehdotus ja muokkaa sitä</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..4854ede352
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Accepter et modifier la suggestion</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..3fce06ba6c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Acete e modifiche sugjeriment</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..e3be14f405
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Suggestje akseptearje en bewurkje</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..48b3251c4b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Gabh ris is deasaich am moladh</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..cc1aafdc78
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceptar e editar a suxestión</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..041524a8c8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Emoneĩ ha embosako’i ñe’ẽporã</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..4bcd2e4b94
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">सुझाव को स्वीकारें और संपादित करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..12124a89c9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Prihvati i uredi prijedlog</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..e83f6c018c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Namjet přiwzać a wobdźěłać</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..bc890be834
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Elfogadás és a javaslat szerkesztése</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..5283d8d707
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Ընդունել և խմբագրել առաջարկությունը</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..5631621fdc
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Acceptar e rediger suggestion</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..54967054e7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Terima dan ubah saran</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..10c6b79c14
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Samþykkja og breyta tillögu</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..c6812c4865
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Accetta e modifica suggerimento</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..d595def498
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">לקבל ולערוך את ההצעה</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..ea9a12d306
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">提案を受け入れて編集</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..1a1664e0a0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">მიღება და შემოთავაზების ჩასწორება</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..450efcd9a1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Usınıslardı qabıllaw hám ózgertiw</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..3117f7591b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Qbel syen ẓreg asumer</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..83d4d9261a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Ұсынысты қабылдау және түзету</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..d7dc068c21
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Pêşniyarê bipejirîne û sererast bike</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..308e45797f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">제안 수락 및 편집</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..eed7f6e8c0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">ຍອມຮັບ ແລະ ແກ້ໄຂຄຳແນະນຳ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..2b5c72b442
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Priimti ir redaguoti pasiūlymą</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..42c0a8df5b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-mr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">सूचना स्वीकारा व संपादित करा</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..63b5bee53a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-my/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">အကြံပြုချက်အားလက်ခံ၍တည်းဖြတ်မည်</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..f141385354
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Godta og rediger forslag</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..905e09f84b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">सुझाव स्वीकार्नुहोस् र सम्पादन गर्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..f2379344d1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Suggestie accepteren en bewerken</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..f141385354
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Godta og rediger forslag</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..fce8718fd2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Acceptar e modificar la suggestion</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..2e861cce89
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-or/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">ସ୍ୱୀକାର କରି ପରାମର୍ଶଟିକୁ ସମ୍ପାଦନ କରନ୍ତୁ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..ba64f33a8a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">ਸੁਝਾਅ ਮੰਨੋ ਤੇ ਸੋਧੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..1ea85cff8d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">تجویز منو تے سودھو</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..7de1109e3d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Przyjmij i modyfikuj podpowiedź</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..478fab2ca3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceitar e editar sugestão</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..478fab2ca3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Aceitar e editar sugestão</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..693ab380d5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Acceptar la proposta e modifitgar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..c690872c02
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Выбрать и изменить подсказку</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..a57202cf0c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">ᱵᱟᱛᱟᱣᱢᱮ ᱟᱨ ᱥᱩᱡᱷᱟᱹᱣ ᱥᱟᱯᱲᱟᱣ ᱢᱮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..ea90dc6e82
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Atzeta e modìfica su cussìgiu</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..2ebd54bc30
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">යෝජනාව පිළිගෙන සංස්කරණය</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..e9b00f0de7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Akceptovať návrh a pokračovať v úprave</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..10bff0878f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">تجویز کوں قبول کرو تے تبدیلی کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..7fe0c78a16
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Sprejmi in uredi predlog</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..d4a0920d65
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Pranoni dhe përpunoni sugjerimin</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..f361a3045d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Прихвати и уреди предлог</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..4b049e8e1f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Tampa jeung édit saran</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..e9e8f2d57e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Acceptera och redigera förslag</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..01a3221de9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-szl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Akceptuj i edytuj dorada</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..822e2ca5da
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ta/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">பரிந்துரையை ஏற்று திருத்து</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..d7716c120b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Пешниҳодро қабул ва таҳрир намоед</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..4f22694ca9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">ยอมรับและแก้ไขข้อเสนอแนะ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..5df1097dc7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Tanggapin at i-edit ang mungkahi</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..1cdbb57528
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tok/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">o kama jo, o ante</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..a50a78776c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Öneriyi kabul et ve düzenle</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..087edecc87
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Gārāyina nī nādūnāt nuguan\’ narikij</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..c15a889dbb
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Тәкъдимне кабул итү һәм үзгәртү</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..476b29d8a2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">تەكلىپنى قوبۇل قىلىپ تەھرىرلەيدۇ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..0ae5b6ef4f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Прийняти та змінити пропозицію</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..defe8f4812
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-ur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">تجویز کو قبول کریں اور ترمیم کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..4dcf317d8c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Tavsiyani qabul qilish va tahrir qilish</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..292a5cd7fb
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Chấp nhận và chỉnh sửa đề xuất</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..9112ee0856
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Gbà á, kí o sì ṣe àtuṣe àbá</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..b21cab3d6c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">接受并编辑建议</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..87481af094
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">接受並編輯建議</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/main/res/values/strings.xml b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..467a778b07
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Description for the button to accept the search suggestion and continue editing the search. -->
+ <string name="mozac_browser_awesomebar_edit_suggestion">Accept and edit suggestion</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/test/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionFetcherTest.kt b/mobile/android/android-components/components/compose/awesomebar/src/test/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionFetcherTest.kt
new file mode 100644
index 0000000000..489b13c834
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/test/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionFetcherTest.kt
@@ -0,0 +1,69 @@
+/* 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.compose.browser.awesomebar.internal
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.awesomebar.AwesomeBar.SuggestionProvider
+import mozilla.components.concept.awesomebar.AwesomeBar.SuggestionProviderGroup
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.spy
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+class SuggestionFetcherTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN a new fetch request THEN all previous queries are cancelled`() = runTestOnMain {
+ val provider: SuggestionProvider = mock()
+ val providerGroup = SuggestionProviderGroup(listOf(provider))
+ val fetcher = spy(SuggestionFetcher(listOf(providerGroup), null))
+ val previousFetchJob: Job = mock()
+ fetcher.fetchJob = previousFetchJob
+ doAnswer {}.`when`(fetcher).fetchFrom(any(), any(), any(), any())
+ val orderVerifier = inOrder(previousFetchJob, fetcher)
+
+ fetcher.fetch("test")
+
+ orderVerifier.verify(previousFetchJob)!!.cancel()
+ orderVerifier.verify(fetcher).fetchFrom(providerGroup, provider, "test", null)
+ }
+
+ @Test
+ fun `GIVEN a suggestion group THEN the group's priority becomes highest suggestions' score within the group`() = runTestOnMain {
+ val provider: SuggestionProvider = mock()
+ val providerGroup = SuggestionProviderGroup(listOf(provider))
+ val suggestions = listOf(
+ AwesomeBar.Suggestion(
+ provider = provider,
+ score = Int.MAX_VALUE,
+ ),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ score = Int.MIN_VALUE,
+ ),
+ )
+ val fetcher = spy(SuggestionFetcher(listOf(providerGroup), null))
+
+ fetcher.processResultFrom(
+ group = providerGroup,
+ provider = provider,
+ suggestions = suggestions,
+ profilerStartTime = null,
+ )
+
+ assertEquals(providerGroup.priority, Int.MAX_VALUE)
+ }
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/test/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionsTest.kt b/mobile/android/android-components/components/compose/awesomebar/src/test/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionsTest.kt
new file mode 100644
index 0000000000..488103f99a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/src/test/java/mozilla/components/compose/browser/awesomebar/internal/SuggestionsTest.kt
@@ -0,0 +1,173 @@
+/* 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.compose.browser.awesomebar.internal
+
+import mozilla.components.concept.awesomebar.AwesomeBar.Suggestion
+import mozilla.components.concept.awesomebar.AwesomeBar.SuggestionProvider
+import mozilla.components.concept.awesomebar.AwesomeBar.SuggestionProviderGroup
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SuggestionsTest {
+ @Test
+ fun `GIVEN 1 suggestion in 1 group WHEN neither are visible THEN return an empty visibility state`() {
+ val provider: SuggestionProvider = mock()
+
+ val visibleItems = VisibleItems(
+ suggestions = mapOf(SuggestionProviderGroup(listOf(provider)) to listOf(Suggestion(provider))),
+ visibleItemKeys = emptyList(),
+ )
+
+ val visibilityState = visibleItems.toVisibilityState()
+ assertTrue(visibilityState.visibleProviderGroups.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN 1 suggestion in 1 group WHEN both are visible THEN return a visibility state containing the suggestion`() {
+ val provider: SuggestionProvider = mock()
+ whenever(provider.id).thenReturn("provider")
+ val providerGroup = SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(Suggestion(provider))
+
+ val visibleItems = VisibleItems(
+ suggestions = mapOf(providerGroup to providerGroupSuggestions),
+ visibleItemKeys = listOf(
+ ItemKey.SuggestionGroup(providerGroup.id),
+ ItemKey.Suggestion(providerGroup.id, provider.id, providerGroupSuggestions[0].id),
+ ),
+ )
+
+ val visibilityState = visibleItems.toVisibilityState()
+ assertEquals(
+ visibilityState.visibleProviderGroups,
+ mapOf(
+ providerGroup to providerGroupSuggestions,
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN 1 suggestion in 1 group WHEN only the suggestion is visible THEN return a visibility state containing the suggestion`() {
+ val provider: SuggestionProvider = mock()
+ whenever(provider.id).thenReturn("provider")
+ val providerGroup = SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(Suggestion(provider))
+
+ val visibleItems = VisibleItems(
+ suggestions = mapOf(providerGroup to providerGroupSuggestions),
+ visibleItemKeys = listOf(
+ ItemKey.Suggestion(providerGroup.id, provider.id, providerGroupSuggestions[0].id),
+ ),
+ )
+
+ val visibilityState = visibleItems.toVisibilityState()
+ assertEquals(
+ visibilityState.visibleProviderGroups,
+ mapOf(
+ providerGroup to providerGroupSuggestions,
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN 2 suggestions in 1 group WHEN all are visible THEN return a visibility state containing the suggestions`() {
+ val provider: SuggestionProvider = mock()
+ whenever(provider.id).thenReturn("provider")
+ val providerGroup = SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(Suggestion(provider), Suggestion(provider))
+
+ val visibleItems = VisibleItems(
+ suggestions = mapOf(providerGroup to providerGroupSuggestions),
+ visibleItemKeys = listOf(
+ ItemKey.SuggestionGroup(providerGroup.id),
+ ItemKey.Suggestion(providerGroup.id, provider.id, providerGroupSuggestions[0].id),
+ ItemKey.Suggestion(providerGroup.id, provider.id, providerGroupSuggestions[1].id),
+ ),
+ )
+
+ val visibilityState = visibleItems.toVisibilityState()
+ assertEquals(
+ visibilityState.visibleProviderGroups,
+ mapOf(
+ providerGroup to providerGroupSuggestions,
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN 2 suggestions in 2 groups WHEN 1 suggestion is visible in 1 group THEN return a visibility state containing the suggestion`() {
+ val firstProvider: SuggestionProvider = mock()
+ whenever(firstProvider.id).thenReturn("firstProvider")
+ val secondProvider: SuggestionProvider = mock()
+ whenever(secondProvider.id).thenReturn("secondProvider")
+ val thirdProvider: SuggestionProvider = mock()
+ whenever(thirdProvider.id).thenReturn("thirdProvider")
+ val firstProviderGroup = SuggestionProviderGroup(listOf(firstProvider, secondProvider))
+ val firstProviderGroupSuggestions = listOf(Suggestion(firstProvider), Suggestion(secondProvider))
+ val secondProviderGroup = SuggestionProviderGroup(listOf(secondProvider, thirdProvider))
+ val secondProviderGroupSuggestions = listOf(Suggestion(secondProvider), Suggestion(thirdProvider))
+
+ val visibleItems = VisibleItems(
+ suggestions = mapOf(
+ firstProviderGroup to firstProviderGroupSuggestions,
+ secondProviderGroup to secondProviderGroupSuggestions,
+ ),
+ visibleItemKeys = listOf(
+ ItemKey.SuggestionGroup(firstProviderGroup.id),
+ ItemKey.Suggestion(firstProviderGroup.id, secondProvider.id, firstProviderGroupSuggestions[1].id),
+ ),
+ )
+
+ val visibilityState = visibleItems.toVisibilityState()
+ assertEquals(
+ visibilityState.visibleProviderGroups,
+ mapOf(
+ firstProviderGroup to listOf(firstProviderGroupSuggestions[1]),
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN 2 suggestions in 2 groups WHEN 1 suggestion is visible in each group THEN return a visibility state containing the suggestions`() {
+ val firstProvider: SuggestionProvider = mock()
+ whenever(firstProvider.id).thenReturn("firstProvider")
+
+ val secondProvider: SuggestionProvider = mock()
+ whenever(secondProvider.id).thenReturn("secondProvider")
+
+ val thirdProvider: SuggestionProvider = mock()
+ whenever(thirdProvider.id).thenReturn("thirdProvider")
+
+ val firstProviderGroup = SuggestionProviderGroup(listOf(firstProvider, secondProvider))
+ val firstProviderGroupSuggestions = listOf(Suggestion(firstProvider), Suggestion(secondProvider))
+
+ val secondProviderGroup = SuggestionProviderGroup(listOf(secondProvider, thirdProvider))
+ val secondProviderGroupSuggestions = listOf(Suggestion(secondProvider), Suggestion(thirdProvider))
+
+ val visibleItems = VisibleItems(
+ suggestions = mapOf(
+ firstProviderGroup to firstProviderGroupSuggestions,
+ secondProviderGroup to secondProviderGroupSuggestions,
+ ),
+ visibleItemKeys = listOf(
+ ItemKey.SuggestionGroup(firstProviderGroup.id),
+ ItemKey.Suggestion(firstProviderGroup.id, firstProvider.id, firstProviderGroupSuggestions[0].id),
+ ItemKey.SuggestionGroup(secondProviderGroup.id),
+ ItemKey.Suggestion(secondProviderGroup.id, thirdProvider.id, secondProviderGroupSuggestions[1].id),
+ ),
+ )
+ val visibilityState = visibleItems.toVisibilityState()
+ assertEquals(
+ visibilityState.visibleProviderGroups,
+ mapOf(
+ firstProviderGroup to listOf(firstProviderGroupSuggestions[0]),
+ secondProviderGroup to listOf(secondProviderGroupSuggestions[1]),
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/compose/awesomebar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/compose/awesomebar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/awesomebar/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/compose/browser-toolbar/README.md b/mobile/android/android-components/components/compose/browser-toolbar/README.md
new file mode 100644
index 0000000000..1f6bdde813
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Compose > Browser Toolbar
+
+A customizable toolbar for browsers using Jetpack Compose.
+
+## Usage
+
+### 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:compose-browser-toolbar:{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/compose/browser-toolbar/build.gradle b/mobile/android/android-components/components/compose/browser-toolbar/build.gradle
new file mode 100644
index 0000000000..cf81cb173f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/build.gradle
@@ -0,0 +1,54 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.compose.browser.toolbar'
+}
+
+dependencies {
+ implementation project(":concept-engine")
+ implementation project(":browser-state")
+ implementation project(":feature-session")
+ implementation project(":ui-icons")
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+
+ debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_compose_ui_test
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/compose/browser-toolbar/proguard-rules.pro b/mobile/android/android-components/components/compose/browser-toolbar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/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/compose/browser-toolbar/src/main/AndroidManifest.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/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/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserDisplayToolbar.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserDisplayToolbar.kt
new file mode 100644
index 0000000000..f4169184aa
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserDisplayToolbar.kt
@@ -0,0 +1,62 @@
+/* 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.compose.browser.toolbar
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.contentColorFor
+import androidx.compose.material.primarySurface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+/**
+ * Sub-component of the [BrowserToolbar] responsible for displaying the URL and related
+ * controls ("display mode").
+ *
+ * @param url The URL to be displayed.
+ * @param onUrlClicked Will be called when the user clicks on the URL.
+ * @param onMenuClicked Will be called when the user clicks on the menu button.
+ * @param browserActions Additional browser actions to be displayed on the right side of the toolbar
+ * (outside of the URL bounding box) in display mode. Also see:
+ * [MDN docs](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Browser_action)
+ */
+@Composable
+fun BrowserDisplayToolbar(
+ url: String,
+ onUrlClicked: () -> Unit = {},
+ onMenuClicked: () -> Unit = {},
+ browserActions: @Composable () -> Unit = {},
+) {
+ val backgroundColor = MaterialTheme.colors.primarySurface
+ val foregroundColor = contentColorFor(backgroundColor)
+
+ Row(
+ Modifier.background(backgroundColor),
+ ) {
+ Text(
+ url,
+ color = foregroundColor,
+ modifier = Modifier
+ .clickable { onUrlClicked() }
+ .padding(8.dp)
+ .weight(1f)
+ .align(Alignment.CenterVertically),
+ maxLines = 1,
+ )
+
+ browserActions()
+
+ Button(onClick = { onMenuClicked() }) {
+ Text(":")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserEditToolbar.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserEditToolbar.kt
new file mode 100644
index 0000000000..455fc1b2a0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserEditToolbar.kt
@@ -0,0 +1,93 @@
+/* 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.compose.browser.toolbar
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.TextField
+import androidx.compose.material.TextFieldDefaults
+import androidx.compose.material.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * Sub-component of the [BrowserToolbar] responsible for allowing the user to edit the current
+ * URL ("edit mode").
+ *
+ * @param url The initial URL to be edited.
+ * @param onUrlEdit Will be called when the URL value changes. An updated text value comes as a
+ * parameter of the callback.
+ * @param onUrlCommitted Will be called when the user has finished editing and wants to initiate
+ * loading the entered URL. The committed text value comes as a parameter of the callback.
+ * @param editActions Optional actions to be displayed on the right side of the toolbar.
+ */
+@Composable
+fun BrowserEditToolbar(
+ url: String,
+ onUrlEdit: (String) -> Unit = {},
+ onUrlCommitted: (String) -> Unit = {},
+ editActions: @Composable () -> Unit = {},
+) {
+ val backgroundColor = MaterialTheme.colors.surface
+ val foregroundColor = contentColorFor(backgroundColor)
+
+ TextField(
+ url,
+ onValueChange = { value ->
+ onUrlEdit(value)
+ },
+ colors = TextFieldDefaults.textFieldColors(
+ textColor = foregroundColor,
+ backgroundColor = backgroundColor,
+ ),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Uri,
+ imeAction = ImeAction.Go,
+ ),
+ keyboardActions = KeyboardActions(
+ onGo = { onUrlCommitted(url) },
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ trailingIcon = {
+ editActions()
+
+ if (url.isNotEmpty()) {
+ ClearButton(onButtonClicked = { onUrlEdit("") })
+ }
+ },
+ )
+}
+
+/**
+ * Sub-component of the [BrowserEditToolbar] responsible for displaying a clear icon button.
+ *
+ * @param onButtonClicked Will be called when the user clicks on the button.
+ */
+@Composable
+fun ClearButton(onButtonClicked: () -> Unit = {}) {
+ IconButton(
+ modifier = Modifier.requiredSize(40.dp),
+ onClick = { onButtonClicked() },
+ ) {
+ Icon(
+ painter = painterResource(iconsR.drawable.mozac_ic_cross_circle_fill_24),
+ contentDescription = stringResource(R.string.mozac_clear_button_description),
+ tint = Color.Black,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserToolbar.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserToolbar.kt
new file mode 100644
index 0000000000..37c4b8f19f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserToolbar.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.compose.browser.toolbar
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import mozilla.components.browser.state.helper.Target
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+
+/**
+ * A customizable toolbar for browsers.
+ *
+ * The toolbar can switch between two modes: display and edit. The display mode displays the current
+ * URL and controls for navigation. In edit mode the current URL can be edited. Those two modes are
+ * implemented by the [BrowserDisplayToolbar] and [BrowserEditToolbar] composables.
+ *
+ * @param store The store to observe the [target] from.
+ * @param target The target tab to observe.
+ * @param onDisplayMenuClicked Function to get executed when the user clicks on the menu button in
+ * "display" mode.
+ * @param onTextEdit Function to get executed whenever the user edits the text in the toolbar in
+ * "edit" mode.
+ * @param onTextCommit Function to get executed when the user has finished editing the URL and wants
+ * to load the entered text.
+ * @param onDisplayToolbarClick Function to get executed when the user clicks on the URL in "display"
+ * mode.
+ * @param hint Text displayed in the toolbar when there's no URL to display (no tab or empty URL)
+ * @param editMode Whether the toolbar is in "edit" or "display" mode.
+ * @param editText The text the user is editing in "edit" mode.
+ * @param browserActions Additional browser actions to be displayed on the right side of the toolbar
+ * (outside of the URL bounding box) in display mode. Also see:
+ * [MDN docs](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Browser_action)
+ */
+@Composable
+fun BrowserToolbar(
+ store: BrowserStore,
+ target: Target,
+ onDisplayMenuClicked: () -> Unit,
+ onTextEdit: (String) -> Unit,
+ onTextCommit: (String) -> Unit,
+ onDisplayToolbarClick: () -> Unit,
+ hint: String = "",
+ editMode: Boolean = false,
+ editText: String? = null,
+ browserActions: @Composable () -> Unit = {},
+) {
+ val selectedTab: SessionState? by target.observeAsComposableStateFrom(
+ store = store,
+ observe = { tab -> tab?.content?.url },
+ )
+
+ val url = selectedTab?.content?.url ?: ""
+ val input = when (editText) {
+ null -> url
+ else -> editText
+ }
+
+ if (editMode) {
+ BrowserEditToolbar(
+ url = input,
+ onUrlCommitted = { text -> onTextCommit(text) },
+ onUrlEdit = { text -> onTextEdit(text) },
+ )
+ } else {
+ BrowserDisplayToolbar(
+ url = selectedTab?.content?.url ?: hint,
+ onUrlClicked = {
+ onDisplayToolbarClick()
+ },
+ onMenuClicked = { onDisplayMenuClicked() },
+ browserActions = browserActions,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..cc83ed16e6
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">አጽዳ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..c9d3504f7a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ar/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">امسح</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..8969801e9c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Borrar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..6f4f648a48
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">تمیزله</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..d5f16e8163
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Puyung</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..70c10b132a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Ачысціць</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..0f5a47334b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Изчистване</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..d38dc27f6b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Skarzhañ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..8bc86d499b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Očisti</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..018996c9c9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Esborra</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..bbd7009b91
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Tijosq\'ïx</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..2925ea5493
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">i-Clear</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..086ca6fc70
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">پاککردنەوە</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..b5fe5d43e5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Squassà</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..32faa95f9c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Vymazat</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..69f49ff160
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Clirio</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..3b0a1d032f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Ryd</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..6858b4ca9c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Löschen</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..b63020c065
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Lašowaś</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..081fa1408f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Απαλοιφή</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..4623e3ea6f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Clear</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..4623e3ea6f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Clear</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..147fe28ba9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Viŝi</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..3bee2874ce
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Eliminar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..4d4c14d193
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Limpiar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..8969801e9c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Borrar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..4d4c14d193
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Limpiar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..8969801e9c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Borrar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..2257c3c05b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Tühjenda</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..ff2b1734b6
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Garbitu</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..d5f3070338
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">پاک کردن</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..a2b754496a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Tyhjennä</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..2fa9bb437d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Effacer</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..0dac627d5a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Nete</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..91cb2126f6
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Wiskje</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..f3515f5318
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Falamhaich</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..8969801e9c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Borrar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..f1f838a3fa
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Mopotĩ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..3066768614
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">साफ करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..f213be78ce
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hil/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Maathag</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..8bc86d499b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Očisti</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..db7ed389d1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Zhašeć</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..240457e51a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Törlés</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..3161e41469
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Մաքրել</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..9e74fc636d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Vacuar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..5b7255a507
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Bersihkan</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..937c569911
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Hreinsa</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..50fac45c09
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Cancella</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..6d2a870121
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">ניקוי</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..aac287abba
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">消去</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..8f2d68db5a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">გასუფთავება</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..d353a36ede
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Tazalaw</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..57e718ffd4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Sfeḍ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..f37edf1617
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Тазарту</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..ea5900bfd7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Paqij bike</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..35026ad285
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">지우기</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..fc861c9736
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">ລົບລ້າງ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..b13f1a2607
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Išvalyti</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..32ec654860
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-mix/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Stòo</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..15f1015506
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-mr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">पुसा</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..e6d5127701
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-my/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">ရှင်းမည်</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..cc9fc95eaf
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Tøm</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..5701e65481
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">मेटाउनुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..d0a8d68480
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Wissen</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..cc9fc95eaf
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Tøm</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..7c2f957fea
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Escafar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..146f91fc44
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">ਮਿਟਾਓ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..638b35e354
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">صاف کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..fba0dba88d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Wyczyść</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..2bc6e2c75c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Limpar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..2bc6e2c75c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Limpar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..119b5bb88a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Stizzar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..0047495c9b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Очистить</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..45acd3ce30
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">ᱯᱷᱟᱨᱪᱟ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..5714c62681
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Isbòida</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..f293461379
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">මකන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..0947ba13c0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Vymazať</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..638b35e354
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">صاف کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..a7c9e71591
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Počisti</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..a64bf5d1cd
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Spastroje</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..c177a14273
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Избриши</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..ff2dd9505c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Beresihan</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..5c48a34f29
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Rensa</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..2d780d888d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-szl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Wypucuj</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..5b59215996
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ta/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">துடை</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..d7291694e8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-te/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">తుడిచివేయి</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..04bcc69e4c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Пок кардан</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..19dfaf0570
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">ล้าง</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..2c4826414a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Malinaw</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..bfb8cb0176
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tok/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">o weka ale</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..7f12eb014a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Temizle</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..d4117dca83
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Nā\'nïn\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..38e4b155a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Чистарту</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..57e718ffd4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Sfeḍ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..b77a27beb2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">تازىلاش</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..21148d9315
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Очистити</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..9ba273e59b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-ur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">صاف کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..2424d47c2a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Tozalash</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..5f2923d53a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-vec/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Pulisi</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..192ec68ca5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Xóa</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..e49144b27c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Paárẹ́</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..21720f256a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">清除</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..21720f256a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">清除</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..ae85fd5c80
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Content description for the clear URL text button. -->
+ <string name="mozac_clear_button_description">Clear</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/compose/browser-toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/browser-toolbar/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/compose/cfr/README.md b/mobile/android/android-components/components/compose/cfr/README.md
new file mode 100644
index 0000000000..843ac13f7f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/README.md
@@ -0,0 +1,49 @@
+# [Android Components](../../../README.md) > Compose > Tabs tray
+
+A standard Contextual Feature Recommendation popup using Jetpack Compose.
+
+## Usage
+
+```kotlin
+CFRPopup(
+ anchor = <View>,
+ properties = CFRPopupProperties(
+ popupWidth = 256.dp,
+ popupAlignment = INDICATOR_CENTERED_IN_ANCHOR,
+ popupBodyColors = listOf(
+ ContextCompat.getColor(context, R.color.color1),
+ ContextCompat.getColor(context, R.color.color2)
+ ),
+ dismissButtonColor = ContextCompat.getColor(context, R.color.color3),
+ ),
+ onDismiss = { <method call> },
+ text = {
+ Text(
+ text = stringResource(R.string.string1),
+ style = MaterialTheme.typography.body2,
+ )
+ },
+ action = {
+ Button(onClick = { <method call> }) {
+ Text(text = stringResource(R.string.string2))
+ }
+ },
+).apply {
+ show()
+}
+```
+
+
+### 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:compose-cfr:{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/compose/cfr/build.gradle b/mobile/android/android-components/components/compose/cfr/build.gradle
new file mode 100644
index 0000000000..84e722924e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/build.gradle
@@ -0,0 +1,63 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ kotlinOptions {
+ freeCompilerArgs += "-Xjvm-default=all"
+ }
+
+ namespace 'mozilla.components.compose.cfr'
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':support-ktx')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+ implementation ComponentsDependencies.androidx_core
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+ implementation ComponentsDependencies.androidx_savedstate
+
+ debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+}
+
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/compose/cfr/proguard-rules.pro b/mobile/android/android-components/components/compose/cfr/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/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/compose/cfr/src/main/AndroidManifest.xml b/mobile/android/android-components/components/compose/cfr/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/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/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt
new file mode 100644
index 0000000000..7d18e795b3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt
@@ -0,0 +1,195 @@
+/* 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.compose.cfr
+
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment
+import java.lang.ref.WeakReference
+
+/**
+ * Properties used to customize the behavior of a [CFRPopup].
+ *
+ * @property popupWidth Width of the popup. Defaults to [CFRPopup.DEFAULT_WIDTH]. To be used as maximum
+ * width when alignment is set to [PopupAlignment.BODY_CENTERED_IN_SCREEN].
+ * @property popupAlignment Where in relation to it's anchor should the popup be placed.
+ * @property popupBodyColors One or more colors serving as the popup background.
+ * If more colors are provided they will be used in a gradient.
+ * @property popupVerticalOffset Vertical distance between the indicator arrow and the anchor.
+ * This only applies if [overlapAnchor] is `false`.
+ * @property dismissButtonColor The tint color that should be applied to the dismiss button.
+ * @property dismissOnBackPress Whether the popup can be dismissed by pressing the back button.
+ * If true, pressing the back button will also call onDismiss().
+ * @property dismissOnClickOutside Whether the popup can be dismissed by clicking outside the
+ * popup's bounds. If true, clicking outside the popup will call onDismiss().
+ * @property overlapAnchor How the popup's indicator will be shown in relation to the anchor:
+ * - true - indicator will be shown exactly in the middle horizontally and vertically
+ * - false - indicator will be shown horizontally in the middle of the anchor but immediately below or above it
+ * @property indicatorDirection The direction the indicator arrow is pointing.
+ * @property indicatorArrowStartOffset Maximum distance between the popup start and the indicator arrow.
+ * If there isn't enough space this could automatically be overridden up to 0 such that
+ * the indicator arrow will be pointing to the middle of the anchor.
+ */
+data class CFRPopupProperties(
+ val popupWidth: Dp = CFRPopup.DEFAULT_WIDTH.dp,
+ val popupAlignment: PopupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ val popupBodyColors: List<Int> = listOf(Color.Blue.toArgb()),
+ val popupVerticalOffset: Dp = CFRPopup.DEFAULT_VERTICAL_OFFSET.dp,
+ val showDismissButton: Boolean = true,
+ val dismissButtonColor: Int = Color.Black.toArgb(),
+ val dismissOnBackPress: Boolean = false,
+ val dismissOnClickOutside: Boolean = false,
+ val overlapAnchor: Boolean = false,
+ val indicatorDirection: IndicatorDirection = IndicatorDirection.UP,
+ val indicatorArrowStartOffset: Dp = CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp,
+)
+
+/**
+ * CFR - Contextual Feature Recommendation popup.
+ *
+ * @param anchor [View] that will serve as the anchor of the popup and serve as lifecycle owner
+ * for this popup also.
+ * @param properties [CFRPopupProperties] allowing to customize the popup appearance and behavior.
+ * @param onDismiss Callback for when the popup is dismissed indicating also if the dismissal
+ * was explicit - by tapping the "X" button or not.
+ * @param text [Text] already styled and ready to be shown in the popup.
+ * @param action Optional other composable to show just below the popup text.
+ */
+class CFRPopup(
+ @get:VisibleForTesting internal val anchor: View,
+ @get:VisibleForTesting internal val properties: CFRPopupProperties,
+ @get:VisibleForTesting internal val onDismiss: (Boolean) -> Unit = {},
+ @get:VisibleForTesting internal val text: @Composable (() -> Unit),
+ @get:VisibleForTesting internal val action: @Composable (() -> Unit) = {},
+) {
+ // This is just a facade for the CFRPopupFullScreenLayout composable offering a cleaner API.
+
+ @VisibleForTesting
+ internal var popup: WeakReference<CFRPopupFullscreenLayout>? = null
+
+ /**
+ * Construct and display a styled CFR popup shown at the coordinates of [anchor].
+ * This popup will be dismissed when the user clicks on the "x" button or based on other user actions
+ * with such behavior set in [CFRPopupProperties].
+ */
+ fun show() {
+ anchor.post {
+ // When we're in this Runnable, the 'show' method might have been called right before
+ // the activity is no longer attached to the WindowManager. When we get to calling
+ // the CFRPopupFullscreenLayout#show method below, we are now trying to attach the View
+ // with the WindowManager that has an unusable Activity.
+ //
+ // To protect against this, within this same Runnable, we check if the anchor view is
+ // safe to use before continuing.
+ //
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1799996
+ if (anchor.context == null || !anchor.isAttachedToWindow) {
+ return@post
+ }
+
+ CFRPopupFullscreenLayout(anchor, properties, onDismiss, text, action).apply {
+ this.show()
+ popup = WeakReference(this)
+ }
+ }
+ }
+
+ /**
+ * Immediately dismiss this CFR popup.
+ * The [onDismiss] callback won't be fired.
+ */
+ fun dismiss() {
+ popup?.get()?.dismiss()
+ }
+
+ /**
+ * Possible direction for the arrow indicator of a CFR popup.
+ * The direction is expressed in relation with the popup body containing the text.
+ */
+ enum class IndicatorDirection {
+ UP,
+ DOWN,
+ }
+
+ /**
+ * Possible alignments of the popup in relation to it's anchor.
+ */
+ enum class PopupAlignment {
+ /**
+ * The popup body will be centered in the space occupied by the anchor.
+ * Recommended to be used when the anchor is wider than the popup.
+ */
+ BODY_TO_ANCHOR_CENTER,
+
+ /**
+ * The popup body will be shown aligned to exactly the anchor start.
+ */
+ BODY_TO_ANCHOR_START,
+
+ /**
+ * The popup will be aligned such that the indicator arrow will point to exactly the middle of the anchor.
+ * Recommended to be used when there are multiple widgets displayed horizontally so that this will allow
+ * to indicate exactly which widget the popup refers to.
+ */
+ INDICATOR_CENTERED_IN_ANCHOR,
+
+ /**
+ * If the popup doesn't have enough space to expand to its full [CFRPopupProperties.popupWidth],
+ * it will be centred in the screen.
+ * If the popup does have enough space, it defaults to [INDICATOR_CENTERED_IN_ANCHOR].
+ * Recommended to be used when the popup text is very long.
+ */
+ BODY_CENTERED_IN_SCREEN,
+ }
+
+ companion object {
+ /**
+ * Default width for all CFRs.
+ */
+ internal const val DEFAULT_WIDTH = 335
+
+ /**
+ * Fixed horizontal padding.
+ * Allows the close button to extend with 10dp more to the end and intercept touches to
+ * a bit outside of the popup to ensure it respects a11y recommendations of 48dp size while
+ * also offer a bit more space to the text.
+ */
+ internal const val DEFAULT_EXTRA_HORIZONTAL_PADDING = 10
+
+ /**
+ * How tall the indicator arrow should be.
+ * This will also affect the width of the indicator's base which is double the height value.
+ */
+ internal const val DEFAULT_INDICATOR_HEIGHT = 7
+
+ /**
+ * Maximum distance between the popup start and the indicator.
+ */
+ internal const val DEFAULT_INDICATOR_START_OFFSET = 30
+
+ /**
+ * Corner radius for the popup body.
+ */
+ internal const val DEFAULT_CORNER_RADIUS = 12
+
+ /**
+ * Vertical distance between the indicator arrow and the anchor.
+ */
+ internal const val DEFAULT_VERTICAL_OFFSET = 9
+
+ /**
+ * Horizontal margin between the popup and viewport edges used to center the popup when alignment
+ * is set to [PopupAlignment.BODY_CENTERED_IN_SCREEN].
+ */
+ internal const val DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP = 16
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt
new file mode 100644
index 0000000000..318bf16829
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt
@@ -0,0 +1,186 @@
+/* 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.compose.cfr
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.semantics.testTagsAsResourceId
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.DOWN
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.UP
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * Complete content of the popup.
+ * [CFRPopupShape] with a gradient background containing [text] and a dismiss ("X") button.
+ *
+ * @param popupBodyColors One or more colors serving as the popup background.
+ * @param dismissButtonColor The tint color that should be applied to the dismiss button.
+ * @param indicatorDirection The direction the indicator arrow is pointing to.
+ * @param indicatorArrowStartOffset Maximum distance between the popup start and the indicator arrow.
+ * If there isn't enough space this could automatically be overridden up to 0.
+ * @param onDismiss Callback for when the popup is dismissed indicating also if the dismissal
+ * was explicit - by tapping the "X" button or not.
+ * @param text [Text] already styled and ready to be shown in the popup.
+ * @param action Optional other composable to show just below the popup text.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+@Suppress("LongMethod")
+fun CFRPopupContent(
+ popupBodyColors: List<Int>,
+ showDismissButton: Boolean,
+ dismissButtonColor: Int,
+ indicatorDirection: CFRPopup.IndicatorDirection,
+ indicatorArrowStartOffset: Dp,
+ onDismiss: (Boolean) -> Unit,
+ popupWidth: Dp = CFRPopup.DEFAULT_WIDTH.dp,
+ text: @Composable (() -> Unit),
+ action: @Composable (() -> Unit) = {},
+) {
+ val popupShape = CFRPopupShape(
+ indicatorDirection,
+ indicatorArrowStartOffset,
+ CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp,
+ CFRPopup.DEFAULT_CORNER_RADIUS.dp,
+ )
+
+ Box(modifier = Modifier.width(popupWidth + CFRPopup.DEFAULT_EXTRA_HORIZONTAL_PADDING.dp)) {
+ Surface(
+ color = Color.Transparent,
+ // Need to override the default RectangleShape to avoid casting shadows for that shape.
+ shape = popupShape,
+ modifier = Modifier
+ .align(Alignment.CenterStart)
+ .background(
+ shape = popupShape,
+ brush = Brush.linearGradient(
+ colors = popupBodyColors.map { Color(it) },
+ end = Offset(0f, Float.POSITIVE_INFINITY),
+ start = Offset(Float.POSITIVE_INFINITY, 0f),
+ ),
+ )
+ .wrapContentHeight()
+ .width(popupWidth),
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(
+ start = 16.dp,
+ top = 16.dp + if (indicatorDirection == CFRPopup.IndicatorDirection.UP) {
+ CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp
+ } else {
+ 0.dp
+ },
+ end = 16.dp,
+ bottom = 16.dp +
+ if (indicatorDirection == CFRPopup.IndicatorDirection.DOWN) {
+ CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp
+ } else {
+ 0.dp
+ },
+ ),
+ ) {
+ Box(
+ modifier = Modifier.padding(
+ end = if (showDismissButton) 24.dp else 16.dp, // 8.dp extra padding to the "X" icon
+ ),
+ ) {
+ text()
+ }
+
+ action()
+ }
+ }
+
+ if (showDismissButton) {
+ IconButton(
+ onClick = { onDismiss(true) },
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(
+ end = 6.dp,
+ )
+ .size(48.dp)
+ .semantics {
+ testTagsAsResourceId = true
+ testTag = "cfr.dismiss"
+ },
+ ) {
+ Icon(
+ painter = painterResource(iconsR.drawable.mozac_ic_cross_20),
+ contentDescription = stringResource(R.string.mozac_cfr_dismiss_button_content_description),
+ modifier = Modifier
+ // Following alignment and padding are intended to visually align the middle
+ // of the "X" button with the top of the text.
+ .align(Alignment.Center)
+ .padding(
+ top = if (indicatorDirection == CFRPopup.IndicatorDirection.UP) 9.dp else 0.dp,
+ )
+ .size(24.dp),
+ tint = Color(dismissButtonColor),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+@Preview(locale = "en", name = "LTR")
+@Preview(locale = "ar", name = "RTL")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
+private fun CFRPopupAbovePreview() {
+ CFRPopupContent(
+ popupBodyColors = listOf(Color.Cyan.toArgb(), Color.Blue.toArgb()),
+ showDismissButton = true,
+ dismissButtonColor = Color.Black.toArgb(),
+ indicatorDirection = DOWN,
+ indicatorArrowStartOffset = CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp,
+ onDismiss = { },
+ text = { Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod") },
+ )
+}
+
+@Composable
+@Preview(locale = "en", name = "LTR")
+@Preview(locale = "ar", name = "RTL")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
+private fun CFRPopupBelowPreview() {
+ CFRPopupContent(
+ popupBodyColors = listOf(Color.Cyan.toArgb(), Color.Blue.toArgb()),
+ showDismissButton = true,
+ dismissButtonColor = Color.Black.toArgb(),
+ indicatorDirection = UP,
+ indicatorArrowStartOffset = CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp,
+ onDismiss = { },
+ text = { Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod") },
+ )
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt
new file mode 100644
index 0000000000..2ef47182ab
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt
@@ -0,0 +1,537 @@
+/* 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.compose.cfr
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.View
+import android.view.WindowManager
+import androidx.annotation.Px
+import androidx.annotation.VisibleForTesting
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.AbstractComposeView
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.ViewRootForInspector
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.LayoutDirection.Ltr
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.DOWN
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.UP
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.BODY_CENTERED_IN_SCREEN
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.BODY_TO_ANCHOR_CENTER
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.BODY_TO_ANCHOR_START
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR
+import mozilla.components.compose.cfr.CFRPopupShape.Companion
+import mozilla.components.compose.cfr.helper.DisplayOrientationListener
+import mozilla.components.compose.cfr.helper.ViewDetachedListener
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.ktx.android.view.toScope
+import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
+
+@VisibleForTesting
+internal const val SHOW_AFTER_SCREEN_ORIENTATION_CHANGE_DELAY = 500L
+
+/**
+ * Value class allowing to easily reason about what an `Int` represents.
+ * This is compiled to the underlying `Int` type so incurs no performance penalty.
+ */
+@JvmInline
+@VisibleForTesting
+internal value class Pixels(val value: Int)
+
+/**
+ * Simple wrapper over the absolute x-coordinates of the popup. Includes any paddings.
+ */
+@VisibleForTesting
+internal data class PopupHorizontalBounds(
+ val startCoord: Pixels,
+ val endCoord: Pixels,
+)
+
+/**
+ * [AbstractComposeView] that can be added or removed dynamically in the current window to display
+ * a [Composable] based popup anywhere on the screen.
+ *
+ * @param anchor [View] that will serve as the anchor of the popup and serve as lifecycle owner
+ * for this popup also.
+ * @param properties [CFRPopupProperties] allowing to customize the popup behavior.
+ * @param onDismiss Callback for when the popup is dismissed indicating also if the dismissal
+ * was explicit - by tapping the "X" button or not.
+ * @param text [Text] already styled and ready to be shown in the popup.
+ * @param action Optional other composable to show just below the popup text.
+ */
+@SuppressLint("ViewConstructor") // Intended to be used only in code, don't need a View constructor
+internal class CFRPopupFullscreenLayout(
+ private val anchor: View,
+ private val properties: CFRPopupProperties,
+ private val onDismiss: (Boolean) -> Unit,
+ private val text: @Composable (() -> Unit),
+ private val action: @Composable (() -> Unit) = {},
+) : AbstractComposeView(anchor.context), ViewRootForInspector {
+ private val windowManager = anchor.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+ /**
+ * Listener for when the anchor is removed from the screen.
+ * Useful in the following situations:
+ * - lack of purpose - if there is no anchor the context/action to which this popup refers to disappeared
+ * - leak from WindowManager - if removing the app from task manager while the popup is shown.
+ *
+ * Will not inform client about this since the user did not expressly dismissed this popup.
+ */
+ private val anchorDetachedListener = ViewDetachedListener {
+ dismiss()
+ }
+
+ /**
+ * When the screen is rotated the popup may get improperly anchored
+ * because of the async nature of insets and screen rotation.
+ * To avoid any improper anchorage the popups are automatically dismissed.
+ *
+ * Will not inform client about this since the user did not expressly dismissed this popup.
+ *
+ * Since a UX decision has been made here:
+ * [link](https://github.com/mozilla-mobile/fenix/issues/27033#issuecomment-1302363014)
+ * to redisplay any **implicitly** dismissed CFRs, a short delay will be added,
+ * after which the CFR will be shown again.
+ *
+ */
+ @VisibleForTesting
+ internal lateinit var orientationChangeListener: DisplayOrientationListener
+
+ override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
+ private set
+
+ /**
+ * Add a new CFR popup to the current window overlaying everything already displayed.
+ * This popup will be dismissed when the user clicks on the "x" button or based on other user actions
+ * with such behavior set in [CFRPopupProperties].
+ */
+ fun show() {
+ setViewTreeLifecycleOwner(anchor.findViewTreeLifecycleOwner())
+ this.setViewTreeSavedStateRegistryOwner(anchor.findViewTreeSavedStateRegistryOwner())
+ anchor.addOnAttachStateChangeListener(anchorDetachedListener)
+ orientationChangeListener = getDisplayOrientationListener(anchor.context).also {
+ it.start()
+ }
+ windowManager.addView(this, createLayoutParams())
+ }
+
+ @Composable
+ override fun Content() {
+ val anchorLocation = IntArray(2).apply {
+ anchor.getLocationInWindow(this)
+ }
+
+ val anchorXCoordMiddle = Pixels(anchorLocation.first() + anchor.width / 2)
+ val indicatorArrowHeight = Pixels(
+ CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp.toPx(),
+ )
+
+ val popupBounds = computePopupHorizontalBounds(
+ anchorMiddleXCoord = anchorXCoordMiddle,
+ arrowIndicatorWidth = Pixels(Companion.getIndicatorBaseWidthForHeight(indicatorArrowHeight.value)),
+ screenWidth = Pixels(LocalConfiguration.current.screenWidthDp.dp.toPx()),
+ layoutDirection = LocalConfiguration.current.layoutDirection,
+ )
+
+ val indicatorOffset = computeIndicatorArrowStartCoord(
+ anchorMiddleXCoord = anchorXCoordMiddle,
+ popupStartCoord = popupBounds.startCoord,
+ arrowIndicatorWidth = Pixels(
+ Companion.getIndicatorBaseWidthForHeight(indicatorArrowHeight.value),
+ ),
+ )
+
+ Popup(
+ popupPositionProvider = getPopupPositionProvider(
+ anchorLocation = anchorLocation,
+ popupBounds = popupBounds,
+ ),
+ properties = PopupProperties(
+ focusable = true,
+ dismissOnBackPress = properties.dismissOnBackPress,
+ dismissOnClickOutside = properties.dismissOnClickOutside,
+ ),
+ onDismissRequest = {
+ // For when tapping outside the popup.
+ dismiss()
+ onDismiss(false)
+ },
+ ) {
+ CFRPopupContent(
+ popupBodyColors = properties.popupBodyColors,
+ showDismissButton = properties.showDismissButton,
+ dismissButtonColor = properties.dismissButtonColor,
+ indicatorDirection = properties.indicatorDirection,
+ indicatorArrowStartOffset = with(LocalDensity.current) {
+ indicatorOffset.value.toDp()
+ },
+ onDismiss = {
+ // For when tapping the "X" button.
+ dismiss()
+ onDismiss(true)
+ },
+ popupWidth = if (shouldCenterPopup(LocalConfiguration.current.screenWidthDp.dp.toPx())) {
+ (LocalConfiguration.current.screenWidthDp - 2 * CFRPopup.DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP).dp
+ } else {
+ properties.popupWidth
+ },
+ text = text,
+ action = action,
+ )
+ }
+ }
+
+ @Composable
+ private fun getPopupPositionProvider(
+ anchorLocation: IntArray,
+ popupBounds: PopupHorizontalBounds,
+ ): PopupPositionProvider {
+ return object : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize,
+ ): IntOffset {
+ // Popup will be anchored such that the indicator arrow will point to the middle of the anchor View
+ // but the popup is allowed some space as start padding in which it can be displayed such that the
+ // indicator arrow is exactly at the top-start/bottom-start corner but slightly translated to end.
+ // Values are in pixels.
+ return IntOffset(
+ when (layoutDirection) {
+ Ltr -> popupBounds.startCoord.value
+ else -> popupBounds.endCoord.value
+ },
+ when (properties.indicatorDirection) {
+ UP -> {
+ when (properties.overlapAnchor) {
+ true -> anchorLocation.last() + anchor.height / 2
+ else -> anchorLocation.last() + anchor.height + properties.popupVerticalOffset.toPx()
+ }
+ }
+ DOWN -> {
+ when (properties.overlapAnchor) {
+ true -> anchorLocation.last() - popupContentSize.height + anchor.height / 2
+ else -> anchorLocation.last() - popupContentSize.height -
+ properties.popupVerticalOffset.toPx()
+ }
+ }
+ },
+ )
+ }
+ }
+ }
+
+ /**
+ * Whether or not the popup body should be centered in the screen, this is only true if the screen does not
+ * allow the popup to be centered at its maximum width.
+ */
+ private fun shouldCenterPopup(
+ screenWidth: Int,
+ ): Boolean {
+ val maximumPopupWidth = properties.popupWidth.toPx() +
+ 2 * CFRPopup.DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP.dp.toPx()
+ return properties.popupAlignment == BODY_CENTERED_IN_SCREEN && maximumPopupWidth > screenWidth
+ }
+
+ /**
+ * Compute the x-coordinates for the absolute start and end position of the popup, including any padding.
+ * This assumes anchoring is indicated with an arrow to the horizontal middle of the anchor with the popup's
+ * body potentially extending to the `start` of the arrow indicator.
+ *
+ * @param anchorMiddleXCoord x-coordinate for the middle of the anchor.
+ * @param arrowIndicatorWidth x-distance the arrow indicator occupies.
+ * @param screenWidth available width in which the popup will be shown.
+ * @param layoutDirection the layout direction of the anchor layout.
+ */
+ @VisibleForTesting
+ @Suppress("LongMethod")
+ internal fun computePopupHorizontalBounds(
+ anchorMiddleXCoord: Pixels,
+ arrowIndicatorWidth: Pixels,
+ screenWidth: Pixels,
+ layoutDirection: Int,
+ ): PopupHorizontalBounds {
+ val arrowIndicatorHalfWidth = arrowIndicatorWidth.value / 2
+
+ return if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ computeHorizontalBoundsLTR(
+ anchorStart = Pixels(anchorMiddleXCoord.value - arrowIndicatorHalfWidth),
+ screenWidth = screenWidth,
+ )
+ } else {
+ computeHorizontalBoundsRTL(
+ anchorStart = Pixels(anchorMiddleXCoord.value + arrowIndicatorHalfWidth),
+ screenWidth = screenWidth,
+ )
+ }
+ }
+
+ private fun computeHorizontalBoundsLTR(
+ anchorStart: Pixels,
+ screenWidth: Pixels,
+ ): PopupHorizontalBounds {
+ val popupPadding = Pixels(CFRPopup.DEFAULT_EXTRA_HORIZONTAL_PADDING.dp.toPx())
+ val leftInsets = Pixels(getLeftInsets())
+ val popupWidth = Pixels(properties.popupWidth.toPx())
+ val viewportMargin =
+ CFRPopup.DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP.dpToPx(anchor.resources.displayMetrics)
+ var startCoord = when (properties.popupAlignment) {
+ BODY_TO_ANCHOR_START -> {
+ Pixels(anchor.x.roundToInt() + leftInsets.value)
+ }
+
+ BODY_TO_ANCHOR_CENTER -> {
+ Pixels(
+ anchor.x.roundToInt()
+ .plus((anchor.width - popupWidth.value) / 2)
+ .plus(leftInsets.value),
+ )
+ }
+
+ INDICATOR_CENTERED_IN_ANCHOR,
+ BODY_CENTERED_IN_SCREEN,
+ -> {
+ if (shouldCenterPopup(screenWidth.value)) {
+ Pixels(viewportMargin + leftInsets.value)
+ } else {
+ Pixels(
+ (anchorStart.value)
+ .minus(properties.indicatorArrowStartOffset.toPx())
+ .coerceAtLeast(leftInsets.value),
+ )
+ }
+ }
+ }
+
+ val endCoord = when (properties.popupAlignment) {
+ BODY_CENTERED_IN_SCREEN,
+ INDICATOR_CENTERED_IN_ANCHOR,
+ -> {
+ if (shouldCenterPopup(screenWidth.value)) {
+ Pixels(screenWidth.value - viewportMargin)
+ } else {
+ var maybeEndCoord = Pixels(
+ startCoord.value
+ .plus(popupWidth.value)
+ .plus(popupPadding.value),
+ )
+ // Handle the scenario in which the popup would get pass the end of the screen.
+ // Allow it to only be shown between [0, screenWidth] and if these bounds are surpassed
+ // translate it horizontally to the start to show as much of it as possible.
+ val endCoordOverflow = maybeEndCoord.value - screenWidth.value
+ if (endCoordOverflow > 0) {
+ startCoord = Pixels(
+ startCoord.value
+ .minus(endCoordOverflow)
+ .coerceAtLeast(leftInsets.value),
+ )
+ maybeEndCoord =
+ Pixels(maybeEndCoord.value.coerceAtMost(screenWidth.value + leftInsets.value))
+ }
+ maybeEndCoord
+ }
+ }
+
+ else -> {
+ Pixels(
+ startCoord.value
+ .plus(popupWidth.value)
+ .plus(popupPadding.value)
+ .coerceAtMost(screenWidth.value + leftInsets.value),
+ )
+ }
+ }
+
+ return PopupHorizontalBounds(
+ startCoord = startCoord,
+ endCoord = endCoord,
+ )
+ }
+
+ private fun computeHorizontalBoundsRTL(
+ anchorStart: Pixels,
+ screenWidth: Pixels,
+ ): PopupHorizontalBounds {
+ val popupPadding = Pixels(CFRPopup.DEFAULT_EXTRA_HORIZONTAL_PADDING.dp.toPx())
+ val leftInsets = Pixels(getLeftInsets())
+ val popupWidth = Pixels(properties.popupWidth.toPx())
+ val viewportMargin =
+ CFRPopup.DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP.dpToPx(anchor.resources.displayMetrics)
+ var startCoord = when (properties.popupAlignment) {
+ BODY_TO_ANCHOR_START -> {
+ Pixels(anchor.x.roundToInt() + anchor.width + leftInsets.value)
+ }
+ BODY_TO_ANCHOR_CENTER -> {
+ val anchorEndCoord = anchor.x.roundToInt() + anchor.width
+ Pixels(
+ anchorEndCoord
+ .minus((anchor.width - popupWidth.value) / 2)
+ .plus(leftInsets.value),
+ )
+ }
+
+ BODY_CENTERED_IN_SCREEN,
+ INDICATOR_CENTERED_IN_ANCHOR,
+ -> {
+ if (shouldCenterPopup(screenWidth.value)) {
+ Pixels(screenWidth.value - viewportMargin)
+ } else {
+ Pixels(
+ // Push the popup as far to the start (in RTL) as possible.
+ anchorStart.value
+ .plus(properties.indicatorArrowStartOffset.toPx())
+ .coerceAtMost(screenWidth.value + leftInsets.value),
+ )
+ }
+ }
+ }
+
+ val endCoord = when (properties.popupAlignment) {
+ BODY_CENTERED_IN_SCREEN,
+ INDICATOR_CENTERED_IN_ANCHOR,
+ -> {
+ if (shouldCenterPopup(screenWidth.value)) {
+ Pixels(viewportMargin - leftInsets.value)
+ } else {
+ var maybeEndCoord = Pixels(
+ startCoord.value
+ .minus(popupWidth.value)
+ .minus(popupPadding.value),
+ )
+ val endCoordOverflow = leftInsets.value - maybeEndCoord.value
+ // Handle the scenario in which the popup would get pass the end of the screen (in RTL)
+ // Allow it to only be shown between [0, screenWidth] and if these bounds are surpassed
+ // translate it horizontally to the start to show as much of it as possible.
+ if (endCoordOverflow > 0) {
+ startCoord = Pixels(
+ startCoord.value
+ .plus(endCoordOverflow.absoluteValue)
+ .coerceAtMost(screenWidth.value + leftInsets.value),
+ )
+ maybeEndCoord =
+ Pixels(maybeEndCoord.value.coerceAtLeast(leftInsets.value))
+ }
+ maybeEndCoord
+ }
+ }
+
+ else -> {
+ Pixels(
+ startCoord.value
+ .minus(popupWidth.value)
+ .minus(popupPadding.value)
+ .coerceAtLeast(leftInsets.value),
+ )
+ }
+ }
+
+ return PopupHorizontalBounds(startCoord, endCoord)
+ }
+
+ /**
+ * Compute the x-coordinate for where the popup's indicator arrow should start
+ * relative to the available distance between it and the popup's starting x-coordinate.
+ *
+ * @param anchorMiddleXCoord x-coordinate for the middle of the anchor.
+ * @param popupStartCoord x-coordinate for the popup start
+ * @param arrowIndicatorWidth Width of the arrow indicator.
+ */
+ @Composable
+ private fun computeIndicatorArrowStartCoord(
+ anchorMiddleXCoord: Pixels,
+ popupStartCoord: Pixels,
+ arrowIndicatorWidth: Pixels,
+ ): Pixels {
+ return when (properties.popupAlignment) {
+ BODY_TO_ANCHOR_START,
+ BODY_TO_ANCHOR_CENTER,
+ -> Pixels(properties.indicatorArrowStartOffset.toPx())
+ BODY_CENTERED_IN_SCREEN,
+ INDICATOR_CENTERED_IN_ANCHOR,
+ -> {
+ val arrowIndicatorHalfWidth = arrowIndicatorWidth.value / 2
+ if (LocalConfiguration.current.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ Pixels(anchorMiddleXCoord.value - arrowIndicatorHalfWidth - popupStartCoord.value)
+ } else {
+ val visiblePopupEndCoord = popupStartCoord.value
+ Pixels(visiblePopupEndCoord - anchorMiddleXCoord.value - arrowIndicatorHalfWidth)
+ }
+ }
+ }
+ }
+
+ /**
+ * Cleanup and remove the current popup from the screen.
+ * Clients are not automatically informed about this. Use a separate call to [onDismiss] if needed.
+ */
+ internal fun dismiss() {
+ anchor.removeOnAttachStateChangeListener(anchorDetachedListener)
+ orientationChangeListener.stop()
+ disposeComposition()
+ setViewTreeLifecycleOwner(null)
+ this.setViewTreeSavedStateRegistryOwner(null)
+ windowManager.removeViewImmediate(this)
+ }
+
+ /**
+ * Create fullscreen translucent layout params.
+ * This will allow placing the visible popup anywhere on the screen.
+ */
+ @VisibleForTesting
+ internal fun createLayoutParams(): WindowManager.LayoutParams =
+ WindowManager.LayoutParams().apply {
+ type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
+ token = anchor.applicationWindowToken
+ width = WindowManager.LayoutParams.MATCH_PARENT
+ height = WindowManager.LayoutParams.MATCH_PARENT
+ format = PixelFormat.TRANSLUCENT
+ flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ }
+
+ private fun getDisplayOrientationListener(context: Context) = DisplayOrientationListener(context) {
+ dismiss()
+ anchor.toScope().launch {
+ delay(SHOW_AFTER_SCREEN_ORIENTATION_CHANGE_DELAY)
+ show()
+ }
+ }
+
+ /**
+ * Intended to allow querying the insets of the navigation bar.
+ * Value will be `0` except for when the screen is rotated by 90 degrees.
+ */
+ private fun getLeftInsets() = ViewCompat.getRootWindowInsets(anchor)
+ ?.getInsets(WindowInsetsCompat.Type.systemBars())?.left
+ ?: 0.coerceAtLeast(0)
+
+ @Px
+ internal fun Dp.toPx(): Int {
+ return this.value
+ .dpToPx(anchor.resources.displayMetrics)
+ .roundToInt()
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt
new file mode 100644
index 0000000000..8606bdcebc
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt
@@ -0,0 +1,272 @@
+/* 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.compose.cfr
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.DOWN
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.UP
+import kotlin.math.roundToInt
+
+/**
+ * How wide the base of the indicator should be in relation with the indicator's height.
+ */
+private const val INDICATOR_BASE_TO_HEIGHT_RATIO = 2f
+
+/**
+ * A [Shape] describing a popup with an indicator triangle shown above or below the popup.
+ *
+ * @param indicatorDirection The direction the indicator arrow is pointing to.
+ * @param indicatorArrowStartOffset Distance between the popup start and the indicator arrow start
+ * @param indicatorArrowHeight Height of the indicator triangle. This influences the base length.
+ * @param cornerRadius The radius of the popup's corners.
+ * If [indicatorArrowStartOffset] is `0` then the top-start corner will not be rounded.
+ */
+class CFRPopupShape(
+ private val indicatorDirection: CFRPopup.IndicatorDirection,
+ private val indicatorArrowStartOffset: Dp,
+ private val indicatorArrowHeight: Dp,
+ private val cornerRadius: Dp,
+) : Shape {
+ @Suppress("LongMethod")
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density,
+ ): Outline {
+ val indicatorArrowStartOffsetPx = indicatorArrowStartOffset.value * density.density
+ val indicatorArrowHeightPx = indicatorArrowHeight.value * density.density
+ val indicatorArrowBasePx =
+ getIndicatorBaseWidthForHeight((indicatorArrowHeight.value * density.density).roundToInt())
+ val cornerRadiusPx = cornerRadius.value * density.density
+ val indicatorCornerRadiusPx = cornerRadiusPx.coerceAtMost(indicatorArrowStartOffsetPx)
+
+ // All outlines are drawn in a LTR space but with accounting for the LTR direction.
+ return when (indicatorDirection) {
+ CFRPopup.IndicatorDirection.UP -> {
+ Outline.Generic(
+ path = Path().apply {
+ reset()
+
+ lineTo(0f, size.height - cornerRadiusPx)
+ quadraticBezierTo(
+ 0f,
+ size.height,
+ cornerRadiusPx,
+ size.height,
+ )
+
+ lineTo(size.width - cornerRadiusPx, size.height)
+ quadraticBezierTo(
+ size.width,
+ size.height,
+ size.width,
+ size.height - cornerRadiusPx,
+ )
+
+ if (layoutDirection == LayoutDirection.Ltr) {
+ lineTo(size.width, cornerRadiusPx + indicatorArrowHeightPx)
+ quadraticBezierTo(
+ size.width,
+ indicatorArrowHeightPx,
+ size.width - cornerRadiusPx,
+ indicatorArrowHeightPx,
+ )
+
+ lineTo(indicatorArrowStartOffsetPx + indicatorArrowBasePx, indicatorArrowHeightPx)
+ lineTo(indicatorArrowStartOffsetPx + indicatorArrowBasePx / 2, 0f)
+ lineTo(indicatorArrowStartOffsetPx, indicatorArrowHeightPx)
+
+ lineTo(indicatorCornerRadiusPx, indicatorArrowHeightPx)
+ quadraticBezierTo(
+ 0f,
+ indicatorArrowHeightPx,
+ 0f,
+ indicatorArrowHeightPx + indicatorCornerRadiusPx,
+ )
+ } else {
+ lineTo(size.width, indicatorCornerRadiusPx + indicatorArrowHeightPx)
+ quadraticBezierTo(
+ size.width,
+ indicatorArrowHeightPx,
+ size.width - indicatorCornerRadiusPx,
+ indicatorArrowHeightPx,
+ )
+
+ val indicatorEnd = size.width - indicatorArrowStartOffsetPx
+ lineTo(indicatorEnd, indicatorArrowHeightPx)
+ lineTo(indicatorEnd - indicatorArrowBasePx / 2, 0f)
+ lineTo(indicatorEnd - indicatorArrowBasePx, indicatorArrowHeightPx)
+
+ lineTo(cornerRadiusPx, indicatorArrowHeightPx)
+ quadraticBezierTo(
+ 0f,
+ indicatorArrowHeightPx,
+ 0f,
+ indicatorArrowHeightPx + cornerRadiusPx,
+ )
+ }
+
+ close()
+ },
+ )
+ }
+ CFRPopup.IndicatorDirection.DOWN -> {
+ val messageBodyHeightPx = size.height - indicatorArrowHeightPx
+
+ Outline.Generic(
+ path = Path().apply {
+ reset()
+
+ if (layoutDirection == LayoutDirection.Ltr) {
+ lineTo(0f, messageBodyHeightPx - indicatorCornerRadiusPx)
+ quadraticBezierTo(
+ 0f,
+ size.height - indicatorArrowHeightPx,
+ indicatorCornerRadiusPx,
+ size.height - indicatorArrowHeightPx,
+ )
+
+ lineTo(indicatorArrowStartOffsetPx, messageBodyHeightPx)
+ lineTo(indicatorArrowStartOffsetPx + indicatorArrowBasePx / 2, size.height)
+ lineTo(indicatorArrowStartOffsetPx + indicatorArrowBasePx, messageBodyHeightPx)
+
+ lineTo(size.width - cornerRadiusPx, messageBodyHeightPx)
+ quadraticBezierTo(
+ size.width,
+ messageBodyHeightPx,
+ size.width,
+ messageBodyHeightPx - cornerRadiusPx,
+ )
+ } else {
+ lineTo(0f, messageBodyHeightPx - cornerRadiusPx)
+ quadraticBezierTo(
+ 0f,
+ messageBodyHeightPx,
+ cornerRadiusPx,
+ messageBodyHeightPx,
+ )
+
+ val indicatorStartPx = size.width - indicatorArrowStartOffsetPx - indicatorArrowBasePx
+ lineTo(indicatorStartPx, messageBodyHeightPx)
+ lineTo(indicatorStartPx + indicatorArrowBasePx / 2, size.height)
+ lineTo(indicatorStartPx + indicatorArrowBasePx, messageBodyHeightPx)
+
+ lineTo(size.width - indicatorCornerRadiusPx, messageBodyHeightPx)
+ quadraticBezierTo(
+ size.width,
+ messageBodyHeightPx,
+ size.width,
+ messageBodyHeightPx - indicatorCornerRadiusPx,
+ )
+ }
+
+ lineTo(size.width, cornerRadiusPx)
+ quadraticBezierTo(
+ size.width,
+ 0f,
+ size.width - cornerRadiusPx,
+ 0f,
+ )
+
+ lineTo(cornerRadiusPx, 0f)
+ quadraticBezierTo(
+ 0f,
+ 0f,
+ 0f,
+ cornerRadiusPx,
+ )
+
+ close()
+ },
+ )
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * This [Shape]'s arrow indicator will have an automatic width depending on the set height.
+ * This method allows knowing what the base width will be before instantiating the class.
+ */
+ fun getIndicatorBaseWidthForHeight(height: Int): Int {
+ return (height * INDICATOR_BASE_TO_HEIGHT_RATIO).roundToInt()
+ }
+ }
+}
+
+@Composable
+@Preview(locale = "en", name = "LTR")
+@Preview(locale = "ar", name = "RTL")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
+private fun CFRPopupBelowShapePreview() {
+ Box(
+ modifier = Modifier
+ .height(100.dp)
+ .width(200.dp)
+ .background(
+ shape = CFRPopupShape(UP, 10.dp, 10.dp, 10.dp),
+ brush = Brush.linearGradient(
+ colors = listOf(Color.Cyan, Color.Blue),
+ end = Offset(0f, Float.POSITIVE_INFINITY),
+ start = Offset(Float.POSITIVE_INFINITY, 0f),
+ ),
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = "This is just a test",
+ color = MaterialTheme.colors.onPrimary,
+ )
+ }
+}
+
+@Composable
+@Preview(locale = "en", name = "LTR")
+@Preview(locale = "ar", name = "RTL")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
+private fun CFRPopupAboveShapePreview() {
+ Box(
+ modifier = Modifier
+ .height(100.dp)
+ .width(200.dp)
+ .background(
+ shape = CFRPopupShape(DOWN, 10.dp, 10.dp, 10.dp),
+ brush = Brush.linearGradient(
+ colors = listOf(Color.Cyan, Color.Blue),
+ end = Offset(0f, Float.POSITIVE_INFINITY),
+ start = Offset(Float.POSITIVE_INFINITY, 0f),
+ ),
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = "This is just a test",
+ color = MaterialTheme.colors.onPrimary,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt
new file mode 100644
index 0000000000..4a44ac6127
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt
@@ -0,0 +1,65 @@
+/* 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.compose.cfr.helper
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.DisplayListener
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Inform when the rotation of the screen changes.
+ * Since this is using a [DisplayManager] listener it's important to call [start] and [stop]
+ * at the appropriate moments to register and unregister said listener.
+ *
+ * @param context Android context needed to interact with the [DisplayManager]
+ * @param onDisplayRotationChanged Listener for when the display rotation changes.
+ * This will be called when the display changes to any of the four main orientations:
+ * [PORTRAIT, LANDSCAPE, REVERSE_PORTRAIT, REVERSE_LANDSCAPE].
+ * No updates will be triggered if the "Auto-rotate" functionality is disabled for the device.
+ */
+internal class DisplayOrientationListener(
+ private val context: Context,
+ val onDisplayRotationChanged: () -> Unit,
+) : DisplayListener {
+ private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+
+ @VisibleForTesting
+ internal var currentOrientation = getCurrentOrientation()
+
+ /**
+ * Start listening for display orientation changes.
+ * It's important to also call [stop] when done listening to prevent leaking the listener.
+ */
+ fun start() {
+ displayManager.registerDisplayListener(this, null)
+ }
+
+ /**
+ * Stop listening for display orientation changes and cleanup the current [DisplayManager] listener.
+ */
+ fun stop() {
+ displayManager.unregisterDisplayListener(this)
+ }
+
+ override fun onDisplayAdded(displayId: Int) = Unit
+
+ override fun onDisplayRemoved(displayId: Int) = Unit
+
+ override fun onDisplayChanged(displayId: Int) {
+ val newOrientation = getCurrentOrientation(displayId)
+
+ if (newOrientation != this.currentOrientation) {
+ this.currentOrientation = newOrientation
+ onDisplayRotationChanged()
+ }
+ }
+
+ private fun getCurrentOrientation(displayId: Int = 0): Int = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ true -> context.resources.configuration.orientation
+ false -> displayManager.getDisplay(displayId)?.rotation ?: currentOrientation
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt
new file mode 100644
index 0000000000..da6782604a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt
@@ -0,0 +1,19 @@
+/* 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.compose.cfr.helper
+
+import android.view.View
+
+/**
+ * Simpler [View.OnAttachStateChangeListener] only informing about
+ * [View.OnAttachStateChangeListener.onViewDetachedFromWindow].
+ */
+internal class ViewDetachedListener(val onDismiss: () -> Unit) : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) = Unit
+
+ override fun onViewDetachedFromWindow(v: View) {
+ onDismiss()
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..c97e99ec94
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">አሰናብት</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..0676ead800
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Escartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..77a2c88eb2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">باغلا</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..19d6f18471
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Адхіліць</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..12463e5dda
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Прекратяване</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..6b257c25c7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Argas</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..d8b54cb34b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Odbaci</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..c5e24af615
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descarta</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..3152b578ba
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Tichup ruwäch</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..437dd527f5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">پشتگوێخستن</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..6ac10d7f41
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ricusà</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..7931a04581
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zavřít</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..cc82da5e42
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Cau</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..d03d570df3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Afvis</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..fc26b597f7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Schließen</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..2c84c274e2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zachyśiś</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..5a3bba4948
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Απόρριψη</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..d3515657b4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..d3515657b4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..af51005a75
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignori</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..e4a97a05b3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ocultar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..e6ef08c0b2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Peida</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..6002549de1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Baztertu</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..7d50c5d847
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">رد کردن</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..dae33bc1fd
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Hylkää</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..68a4823ca3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Fermer</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..4a414e4c5b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Siere</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..105ba034c4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Slute</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..13c3244a40
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Leig seachad</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..fdc1c7e79f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Rexeitar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..9615457637
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Mboyke</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..d8b54cb34b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Odbaci</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..d4b638b5be
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zaćisnyć</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..00bc0d7781
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Eltüntetés</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..6a33629dd2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Բաց թողնել</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..716e14b9a7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dimitter</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..a1d014b221
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Tutup</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..6ef45a90ca
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Hafna</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..07debd3b5c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Chiudi</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..d7c5324959
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">סגירה</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..a1efc5c03d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">閉じる</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..49bea70e48
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">არიდება</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..927fa4b2d3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Jabıw</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..05b7c36c7a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zgel</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..1e137a1fb1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Тайдыру</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..0a59a2e76f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Bigire</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..a8f2e61c9b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">닫기</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..1200deb331
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ປິດ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..d2d923b1b9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Paslėpti</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..a403088daf
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignorer</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..db4b674192
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Sluiten</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..a403088daf
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignorer</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..7726a050b9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignorar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..495a1e811e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ਖਾਰਜ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..27ee637bfd
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">بند کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..6fb5777d89
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zamknij</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..da76c02be9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dispensar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..12c3220442
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Serrar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..c9748091ec
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Скрыть</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..e432bced8a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ᱵᱚᱱᱫ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..2cf10b5097
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Iscarta</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..692fe43d68
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ඉවතලන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..335a45ecc7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zavrieť</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..ed279b9081
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">فارغ کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..b476df56f0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zapri</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..23c71495b1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Hidhe tej</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..f535bf88e7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Одбаци</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..a1d014b221
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Tutup</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..87506c9962
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignorera</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..ca2214b892
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ôdkoż</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..e77dfe593c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Нодида гузарондан</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..4ab6fc629d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ปิด</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..76f55fe0e8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Kapat</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..b5bdf60de2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Si gi\'hiaj guendô\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..acdeb1ed5b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Яшерү</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..1eaac24e2a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">بولدىلا</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..1d1f11dbbb
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Відхилити</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..716312dfd9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Rad qilish</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..8c4ff51166
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Sara</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..3d9e560fee
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Bỏ qua</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..1c5ff52f06
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">知道了</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..9414216213
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">知道了!</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..86c00ee3c0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt
new file mode 100644
index 0000000000..a0259e0bc5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt
@@ -0,0 +1,561 @@
+/* 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.compose.cfr
+
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.graphics.PixelFormat
+import android.view.View
+import android.view.ViewManager
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class CFRPopupFullscreenLayoutTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `WHEN the popup is shown THEN setup lifecycle owners`() {
+ val anchor = View(testContext).apply {
+ setViewTreeLifecycleOwner(mock())
+ this.setViewTreeSavedStateRegistryOwner(mock())
+ }
+
+ val popupView = spy(
+ CFRPopupFullscreenLayout(
+ anchor = anchor,
+ properties = mock(),
+ onDismiss = mock(),
+ text = { },
+ action = { },
+ ),
+ )
+ popupView.show()
+
+ assertNotNull(popupView.findViewTreeLifecycleOwner())
+ assertEquals(
+ anchor.findViewTreeLifecycleOwner(),
+ popupView.findViewTreeLifecycleOwner(),
+ )
+ assertNotNull(popupView.findViewTreeSavedStateRegistryOwner())
+ assertEquals(
+ assertNotNull(anchor.findViewTreeSavedStateRegistryOwner()),
+ assertNotNull(popupView.findViewTreeSavedStateRegistryOwner()),
+ )
+ }
+
+ @Test
+ fun `WHEN the popup is dismissed THEN cleanup lifecycle owners and detach from window`() {
+ val context = spy(testContext)
+ val anchor = View(context).apply {
+ setViewTreeLifecycleOwner(mock())
+ this.setViewTreeSavedStateRegistryOwner(mock())
+ }
+ val windowManager = spy(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
+ doReturn(windowManager).`when`(context).getSystemService(Context.WINDOW_SERVICE)
+ val popupView = CFRPopupFullscreenLayout(anchor, mock(), mock(), { }, { })
+ popupView.show()
+ assertNotNull(popupView.findViewTreeLifecycleOwner())
+ assertNotNull(popupView.findViewTreeSavedStateRegistryOwner())
+
+ popupView.dismiss()
+
+ assertNull(popupView.findViewTreeLifecycleOwner())
+ assertNull(popupView.findViewTreeSavedStateRegistryOwner())
+ verify(windowManager).removeViewImmediate(popupView)
+ }
+
+ @Test
+ fun `GIVEN a popup WHEN adding it to window THEN use translucent layout params`() {
+ val context = spy(testContext)
+ val anchor = View(context)
+ val windowManager = spy(context.getSystemService(Context.WINDOW_SERVICE))
+ doReturn(windowManager).`when`(context).getSystemService(Context.WINDOW_SERVICE)
+ val popupView = CFRPopupFullscreenLayout(anchor, mock(), mock(), { }, { })
+ val layoutParamsCaptor = argumentCaptor<LayoutParams>()
+
+ popupView.show()
+
+ verify(windowManager as ViewManager).addView(eq(popupView), layoutParamsCaptor.capture())
+ assertEquals(LayoutParams.TYPE_APPLICATION_PANEL, layoutParamsCaptor.value.type)
+ assertEquals(anchor.applicationWindowToken, layoutParamsCaptor.value.token)
+ assertEquals(LayoutParams.MATCH_PARENT, layoutParamsCaptor.value.width)
+ assertEquals(LayoutParams.MATCH_PARENT, layoutParamsCaptor.value.height)
+ assertEquals(PixelFormat.TRANSLUCENT, layoutParamsCaptor.value.format)
+ assertEquals(
+ LayoutParams.FLAG_LAYOUT_IN_SCREEN or LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ layoutParamsCaptor.value.flags,
+ )
+ }
+
+ @Test
+ fun `WHEN creating layout params THEN get fullscreen translucent layout params`() {
+ val anchor = View(testContext)
+ val popupView = CFRPopupFullscreenLayout(anchor, mock(), mock(), { }, { })
+
+ val result = popupView.createLayoutParams()
+
+ assertEquals(LayoutParams.TYPE_APPLICATION_PANEL, result.type)
+ assertEquals(anchor.applicationWindowToken, result.token)
+ assertEquals(LayoutParams.MATCH_PARENT, result.width)
+ assertEquals(LayoutParams.MATCH_PARENT, result.height)
+ assertEquals(PixelFormat.TRANSLUCENT, result.format)
+ assertEquals(
+ LayoutParams.FLAG_LAYOUT_IN_SCREEN or LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ result.flags,
+ )
+ }
+
+ @Test
+ fun `GIVEN LTR and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(200),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(190, result.startCoord.value)
+ assertEquals(400, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds THEN account for the provided indicator offset`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(200),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ // The popup should be translated to the start to ensure the offset to the indicator is respected.
+ assertEquals(140, result.startCoord.value)
+ assertEquals(350, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds and the popup doesn't fit THEN return the right X coordinates`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 900.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(200),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ // The popup should be translated to the start to ensure it fits the screen.
+ assertEquals(90, result.startCoord.value)
+ assertEquals(1000, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(800),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(810, result.startCoord.value)
+ assertEquals(600, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds THEN account for the provided indicator offset`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(800),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ // The popup should be translated to the start to ensure the offset to the indicator is respected.
+ assertEquals(860, result.startCoord.value)
+ assertEquals(650, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds and the popup doesn't fit THEN return the right X coordinates`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 900.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(800),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ // The popup should be translated to the start to ensure it fits the screen.
+ assertEquals(910, result.startCoord.value)
+ assertEquals(0, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and BODY_TO_ANCHOR_START WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 300.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_START,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(200, result.startCoord.value)
+ assertEquals(510, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and BODY_TO_ANCHOR_START WHEN computing popup bounds THEN return the right X coordinates and don't account for the provided indicator offset`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 300.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_START,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(200, result.startCoord.value)
+ assertEquals(510, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and BODY_TO_ANCHOR_START WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_START,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(600, result.startCoord.value)
+ assertEquals(390, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and BODY_TO_ANCHOR_START WHEN computing popup bounds THEN return the right X coordinates and don't account for the provided indicator offset`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_START,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(600, result.startCoord.value)
+ assertEquals(390, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and BODY_TO_ANCHOR_CENTER WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = spy(View(testContext))
+ doReturn(600).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(400),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(300, result.startCoord.value)
+ assertEquals(710, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and BODY_TO_ANCHOR_CENTER WHEN computing popup bounds THEN return the right X coordinates and don't account for the provided indicator offset`() {
+ val anchor = spy(View(testContext))
+ doReturn(600).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(400),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(300, result.startCoord.value)
+ assertEquals(710, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and BODY_TO_ANCHOR_CENTER WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = spy(View(testContext))
+ doReturn(600).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(700, result.startCoord.value)
+ assertEquals(290, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and BODY_TO_ANCHOR_CENTER WHEN computing popup bounds THEN return the right X coordinates and don't account for the provided indicator offset`() {
+ val anchor = spy(View(testContext))
+ doReturn(600).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(700, result.startCoord.value)
+ assertEquals(290, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR direction and popup is larger than viewport width WHEN computing popup bounds for CENTERED_IN_SCREEN alignment THEN return the correct horizontal bounds`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 500.dp,
+ popupAlignment = PopupAlignment.BODY_CENTERED_IN_SCREEN,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(400),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(500),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(16, result.startCoord.value)
+ // The screen width minus the viewport margin
+ assertEquals(484, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR direction and popup fits inside the viewport WHEN computing popup bounds for CENTERED_IN_SCREEN alignment THEN the horizontal bounds are calculated for BODY_TO_ANCHOR_CENTER alignment`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_CENTERED_IN_SCREEN,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(200),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(190, result.startCoord.value)
+ assertEquals(600, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL direction and popup is larger than viewport width WHEN computing popup bounds for CENTERED_IN_SCREEN alignment THEN return the correct horizontal bounds`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 500.dp,
+ popupAlignment = PopupAlignment.BODY_CENTERED_IN_SCREEN,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(400),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(500),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ // The screen width minus the viewport margin
+ assertEquals(484, result.startCoord.value)
+ assertEquals(16, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL direction and popup fits inside the viewport WHEN computing popup bounds for CENTERED_IN_SCREEN alignment THEN the horizontal bounds are calculated for BODY_TO_ANCHOR_CENTER alignment`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_CENTERED_IN_SCREEN,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(700),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(710, result.startCoord.value)
+ assertEquals(300, result.endCoord.value)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `GIVEN there is a CFR Popup showing WHEN the orientation of the device changes THEN the CFR will be dismissed and shown again after a delay`() = runTestOnMain {
+ val context = spy(testContext)
+ val anchor = View(context).apply {
+ setViewTreeLifecycleOwner(mock())
+ this.setViewTreeSavedStateRegistryOwner(mock())
+ }
+ val popupView = spy(CFRPopupFullscreenLayout(anchor, mock(), mock(), { }, { }))
+ popupView.show()
+
+ testContext.resources.configuration.orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ popupView.orientationChangeListener.onDisplayChanged(1)
+
+ advanceTimeBy(SHOW_AFTER_SCREEN_ORIENTATION_CHANGE_DELAY)
+ verify(popupView, times(1)).dismiss()
+ verify(popupView, times(1)).show()
+ // Test that show() is called the second time after exactly the expected delay.
+ advanceTimeBy(1)
+ verify(popupView, times(2)).show()
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt
new file mode 100644
index 0000000000..5f8a0874e0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt
@@ -0,0 +1,136 @@
+/* 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.compose.cfr.helper
+
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.view.Display
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(sdk = [Build.VERSION_CODES.N])
+class DisplayOrientationListenerTest {
+ private val context: Context = mock()
+ private val displayManager: DisplayManager = mock()
+
+ @Before
+ fun setup() {
+ doReturn(displayManager).`when`(context).getSystemService(Context.DISPLAY_SERVICE)
+
+ val display: Display = mock()
+ doReturn(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT).`when`(display).rotation
+ doReturn(display).`when`(displayManager).getDisplay(0)
+ }
+
+ @Test
+ fun `WHEN started THEN register it as a display listener`() {
+ val listener = DisplayOrientationListener(context) { }
+
+ listener.start()
+
+ verify(displayManager).registerDisplayListener(listener, null)
+ }
+
+ @Test
+ fun `WHEN stopped THEN unregister from being a display listener`() {
+ val listener = DisplayOrientationListener(context) { }
+
+ listener.stop()
+
+ verify(displayManager).unregisterDisplayListener(listener)
+ }
+
+ @Test
+ fun `WHEN a display is added THEN don't inform the client`() {
+ var hasRotationChanged = false
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+
+ listener.onDisplayAdded(1)
+
+ assertFalse(hasRotationChanged)
+ }
+
+ @Test
+ fun `WHEN a display is removed THEN don't inform the client`() {
+ var hasRotationChanged = false
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+
+ listener.onDisplayRemoved(1)
+
+ assertFalse(hasRotationChanged)
+ }
+
+ @Test
+ fun `GIVEN display is null WHEN a display is changed THEN don't inform the client`() {
+ val onDisplayRotationChanged = mock<() -> Unit>()
+ val listener = DisplayOrientationListener(context, onDisplayRotationChanged)
+ doReturn(null).`when`(displayManager).getDisplay(1)
+
+ listener.onDisplayChanged(1)
+
+ verifyNoInteractions(onDisplayRotationChanged)
+ }
+
+ @Test
+ fun `WHEN a display is changed but doesn't have a new rotation THEN don't inform the client`() {
+ var hasRotationChanged = false
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+ val display: Display = mock()
+ doReturn(listener.currentOrientation).`when`(display).rotation
+ doReturn(display).`when`(displayManager).getDisplay(1)
+
+ listener.onDisplayChanged(1)
+
+ assertFalse(hasRotationChanged)
+ }
+
+ @Test
+ fun `GIVEN an old Android version WHEN a display is changed and has a new rotation THEN inform the client and remember the new rotation`() {
+ var hasRotationChanged = false
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+ val display: Display = mock()
+ doReturn(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE).`when`(display).rotation
+ doReturn(display).`when`(displayManager).getDisplay(1)
+
+ listener.onDisplayChanged(1)
+
+ assertTrue(hasRotationChanged)
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, listener.currentOrientation)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.S])
+ fun `GIVEN a new Android version WHEN a display is changed and has a new rotation THEN inform the client and remember the new rotation`() {
+ var hasRotationChanged = false
+ val config = Configuration().apply {
+ orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ }
+ val resources: Resources = mock()
+ doReturn(config).`when`(resources).configuration
+ doReturn(resources).`when`(context).resources
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+
+ config.orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
+ listener.onDisplayChanged(1)
+
+ assertTrue(hasRotationChanged)
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE, listener.currentOrientation)
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.kt b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.kt
new file mode 100644
index 0000000000..bb6467f44c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.kt
@@ -0,0 +1,32 @@
+/* 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.compose.cfr.helper
+
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ViewDetachedListenerTest {
+ @Test
+ fun `WHEN the View is attached THEN don't inform the client`() {
+ var wasCallbackCalled = false
+ val listener = ViewDetachedListener { wasCallbackCalled = true }
+
+ listener.onViewAttachedToWindow(mock())
+
+ assertFalse(wasCallbackCalled)
+ }
+
+ @Test
+ fun `WHEN the View is detached THEN don't inform the client`() {
+ var wasCallbackCalled = false
+ val listener = ViewDetachedListener { wasCallbackCalled = true }
+
+ listener.onViewDetachedFromWindow(mock())
+
+ assertTrue(wasCallbackCalled)
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/compose/cfr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/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/compose/engine/README.md b/mobile/android/android-components/components/compose/engine/README.md
new file mode 100644
index 0000000000..4ea4cd1be2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/engine/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Compose > Engine
+
+A component for integrating a `concept-engine` implementation into Jetpack Compose UI.
+
+## Usage
+
+### 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:compose-engine:{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/compose/engine/build.gradle b/mobile/android/android-components/components/compose/engine/build.gradle
new file mode 100644
index 0000000000..5bbeb6d699
--- /dev/null
+++ b/mobile/android/android-components/components/compose/engine/build.gradle
@@ -0,0 +1,61 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.compose.engine'
+}
+
+dependencies {
+ implementation project(":browser-state")
+ implementation project(":concept-engine")
+ implementation project(":support-ktx")
+
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+
+ debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_compose_ui_test
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+
+ androidTestImplementation ComponentsDependencies.androidx_test_junit
+ androidTestImplementation ComponentsDependencies.androidx_compose_ui_test_manifest
+ androidTestImplementation ComponentsDependencies.androidx_compose_ui_test
+ androidTestImplementation ComponentsDependencies.testing_mockito
+}
+
+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/compose/engine/proguard-rules.pro b/mobile/android/android-components/components/compose/engine/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/engine/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/compose/engine/src/main/AndroidManifest.xml b/mobile/android/android-components/components/compose/engine/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/compose/engine/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/compose/engine/src/main/java/mozilla/components/compose/engine/WebContent.kt b/mobile/android/android-components/components/compose/engine/src/main/java/mozilla/components/compose/engine/WebContent.kt
new file mode 100644
index 0000000000..0d2bfb02f1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/engine/src/main/java/mozilla/components/compose/engine/WebContent.kt
@@ -0,0 +1,62 @@
+/* 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.compose.engine
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.helper.Target
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineView
+
+/**
+ * Composes an [EngineView] obtained from the given [Engine] and renders the web content of the
+ * [target] from the [store] on it.
+ */
+@Composable
+fun WebContent(
+ engine: Engine,
+ store: BrowserStore,
+ target: Target,
+) {
+ val selectedTab = target.observeAsComposableStateFrom(
+ store = store,
+ observe = { tab ->
+ // Render if the tab itself changed or when the state of the linked engine session changes
+ arrayOf(
+ tab?.id,
+ tab?.engineState?.engineSession,
+ tab?.engineState?.crashed,
+ tab?.content?.firstContentfulPaint,
+ )
+ },
+ )
+
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = { context -> engine.createView(context).asView() },
+ update = { view ->
+ val engineView = view as EngineView
+
+ val tab = selectedTab.value
+ if (tab == null) {
+ engineView.release()
+ } else {
+ val session = tab.engineState.engineSession
+ if (session == null) {
+ // This tab does not have an EngineSession that we can render yet. Let's dispatch an
+ // action to request creating one. Once one was created and linked to this session, this
+ // method will get invoked again.
+ store.dispatch(EngineAction.CreateEngineSessionAction(tab.id))
+ } else {
+ engineView.render(session)
+ }
+ }
+ },
+ )
+}
diff --git a/mobile/android/android-components/components/compose/engine/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/compose/engine/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/engine/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/compose/engine/src/test/resources/robolectric.properties b/mobile/android/android-components/components/compose/engine/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/compose/engine/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/compose/tabstray/README.md b/mobile/android/android-components/components/compose/tabstray/README.md
new file mode 100644
index 0000000000..6a70022ee0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Compose > Tabs tray
+
+A customizable tabs tray using Jetpack Compose.
+
+## Usage
+
+### 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:compose-tabstray:{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/compose/tabstray/build.gradle b/mobile/android/android-components/components/compose/tabstray/build.gradle
new file mode 100644
index 0000000000..1b12b0ac53
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/build.gradle
@@ -0,0 +1,58 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.compose.browser.tabstray'
+}
+
+dependencies {
+ implementation project(":concept-tabstray")
+
+ implementation project(":browser-state")
+
+ implementation project(":ui-icons")
+
+ implementation project(":feature-tabs")
+
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+
+ debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_compose_ui_test
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/compose/tabstray/proguard-rules.pro b/mobile/android/android-components/components/compose/tabstray/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/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/compose/tabstray/src/main/AndroidManifest.xml b/mobile/android/android-components/components/compose/tabstray/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/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/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/Tab.kt b/mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/Tab.kt
new file mode 100644
index 0000000000..20801e0609
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/Tab.kt
@@ -0,0 +1,97 @@
+/* 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.compose.tabstray
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.ui.icons.R
+
+/**
+ * Renders a single [TabSessionState] as a list item.
+ *
+ * @param tab The tab to render.
+ * @param selected Whether this tab is selected or not.
+ * @param onClick Gets invoked when the tab gets clicked.
+ * @param onClose Gets invoked when tab gets closed.
+ */
+@Composable
+fun Tab(
+ tab: TabSessionState,
+ selected: Boolean = false,
+ onClick: (String) -> Unit = {},
+ onClose: (String) -> Unit = {},
+) {
+ Box(
+ modifier = Modifier
+ .background(if (selected) Color(0xFFFF45A1FF.toInt()) else Color.Unspecified)
+ .size(width = Dp.Unspecified, height = 72.dp)
+ .fillMaxWidth()
+ .clickable { onClick.invoke(tab.id) }
+ .padding(8.dp),
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceAround,
+ ) {
+ // BrowserThumbnail(tab)
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .align(Alignment.CenterVertically)
+ .padding(8.dp),
+ ) {
+ Text(
+ text = tab.content.title,
+ fontWeight = FontWeight.Bold,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ color = Color.White,
+ )
+ Text(
+ text = tab.content.url,
+ style = MaterialTheme.typography.body2,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ color = Color.White.copy(alpha = ContentAlpha.medium),
+ )
+ }
+ IconButton(
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .requiredSize(24.dp),
+ onClick = { onClose.invoke(tab.id) },
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.mozac_ic_cross_24),
+ contentDescription = "close",
+ tint = Color.White,
+ )
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/TabCounterButton.kt b/mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/TabCounterButton.kt
new file mode 100644
index 0000000000..2f73136d7a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/TabCounterButton.kt
@@ -0,0 +1,79 @@
+/* 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.compose.tabstray
+
+import androidx.compose.foundation.Image
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.contentColorFor
+import androidx.compose.material.primarySurface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.sp
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.compose.browser.tabstray.R
+import mozilla.components.lib.state.ext.observeAsComposableState
+
+private const val MAX_VISIBLE_TABS = 99
+private const val SO_MANY_TABS_OPEN = "∞"
+
+/**
+ * A button showing the count of tabs in the [store] using the provided [tabsFilter].
+ *
+ * @param store The store to observe.
+ * @param onClicked Gets invoked when the user clicks the button.
+ * @param tabsFilter Used for filtering the list of tabs.
+ */
+@Composable
+fun TabCounterButton(
+ store: BrowserStore,
+ onClicked: () -> Unit,
+ tabsFilter: (TabSessionState) -> Boolean = { true },
+) {
+ IconButton(
+ onClick = onClicked,
+ ) {
+ val backgroundColor = MaterialTheme.colors.primarySurface
+ val foregroundColor = contentColorFor(backgroundColor)
+ val tabs = store.observeAsComposableState { state -> state.tabs.filter(tabsFilter) }
+ val count = tabs.value?.size ?: 0
+
+ Image(
+ painter = painterResource(R.drawable.mozac_tabcounter_background),
+ contentDescription = createContentDescription(count),
+ colorFilter = ColorFilter.tint(foregroundColor),
+ )
+
+ Text(
+ createButtonText(count),
+ fontSize = 12.sp,
+ color = foregroundColor,
+ )
+ }
+}
+
+private fun createButtonText(count: Int): String {
+ return if (count > MAX_VISIBLE_TABS) {
+ SO_MANY_TABS_OPEN
+ } else {
+ count.toString()
+ }
+}
+
+@Composable
+private fun createContentDescription(count: Int): String {
+ return if (count == 1) {
+ stringResource(R.string.mozac_tab_counter_open_tab_tray_single)
+ } else {
+ String.format(
+ stringResource(R.string.mozac_tab_counter_open_tab_tray_plural),
+ count.toString(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/TabList.kt b/mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/TabList.kt
new file mode 100644
index 0000000000..e862854687
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/java/mozilla/components/compose/tabstray/TabList.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.compose.tabstray
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.ext.observeAsComposableState
+
+/**
+ * Renders a list of tabs from the given [store], using the provided [tabsFilter].
+ *
+ * @param store The store to observe.
+ * @param modifier The modifier to apply to this layout.
+ * @param tabsFilter Used to filter the list of tabs from the [store].
+ * @param onTabClosed Gets invoked when the user closes a tab.
+ * @param onTabSelected Gets invoked when the user selects a tab.
+ */
+@Composable
+fun TabList(
+ store: BrowserStore,
+ modifier: Modifier = Modifier,
+ tabsFilter: (TabSessionState) -> Boolean = { true },
+ onTabSelected: (TabSessionState) -> Unit = {},
+ onTabClosed: (TabSessionState) -> Unit = {},
+) {
+ val tabs = store.observeAsComposableState { state -> state.tabs.filter(tabsFilter) }
+ val selectedTabId = store.observeAsComposableState { state -> state.selectedTabId }
+ TabList(
+ tabs.value ?: emptyList(),
+ modifier,
+ selectedTabId.value,
+ onTabSelected,
+ onTabClosed,
+ )
+}
+
+/**
+ * Renders the given list of [tabs].
+ *
+ * @param tabs The list of tabs to render.
+ * @param selectedTabId the currently selected tab ID.
+ * @param modifier The modifier to apply to this layout.
+ * @param onTabClosed Gets invoked when the user closes a tab.
+ * @param onTabSelected Gets invoked when the user selects a tab.
+ */
+@Composable
+fun TabList(
+ tabs: List<TabSessionState>,
+ modifier: Modifier = Modifier,
+ selectedTabId: String? = null,
+ onTabSelected: (TabSessionState) -> Unit,
+ onTabClosed: (TabSessionState) -> Unit,
+) {
+ LazyColumn(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colors.surface),
+ ) {
+ items(tabs) { tab ->
+ Tab(
+ tab,
+ selected = selectedTabId == tab.id,
+ onClick = { onTabSelected(tab) },
+ onClose = { onTabClosed(tab) },
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/drawable/mozac_tabcounter_background.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/drawable/mozac_tabcounter_background.xml
new file mode 100644
index 0000000000..489efcb568
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/drawable/mozac_tabcounter_background.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+<path
+ android:pathData="M4.5,4A2.5,2.5 0,0 0,2 6.5v11A2.5,2.5 0,0 0,4.5 20h15a2.5,2.5 0,0 0,2.5 -2.5v-11A2.5,2.5 0,0 0,19.5 4h-15zM20.5,17.7 L19.7,18.5L4.3,18.5l-0.8,-0.8L3.5,6.3l0.8,-0.8h15.4l0.8,0.8v11.4z"
+ android:strokeWidth="1"
+ android:fillColor="@color/mozac_ui_tabcounter_default_tint"/>
+</vector>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..5cb872a827
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-am/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ትር ክፈት። ትሮችን ለመቀየር መታ ያድርጉ።</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ትሮች ክፍት። ትሮችን ለመቀየር መታ ያድርጉ።</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..bbd8b69682
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ar/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">لسان واحد مفتوح. انقر لتبديل الألسنة.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s من الألسنة مفتوح. انقر لتبديل الألسنة.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..966c990739
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ast/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 llingüeta abierta. Toca pa cambiar de llingüeta.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s llingüetes abiertes. Toca pa cambiar de llingüeta.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..acc68b47f3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-azb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">آچیق سئکمه. سئکمه‌لری دگیشدیرمک اوچون توخونون.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s آچیق سئکمه‌. دگیشدیرمک اوچون توخونون.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..3a96d5f1ce
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-be/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 адкрытая картка. Націсніце, каб пераключыць карткі.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Адкрытых картак: %1$s. Націсніце, каб пераключыць карткі.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..14638b0375
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-bg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 отворен раздел. Докоснете, за да превключите разделите.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s отворени раздела. Докоснете, за да превключите разделите.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..9d8952f911
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-br/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ivinell digor. Pouezit evit cheñch ivinell.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ivinell digor. Pouezit evit cheñch ivinell.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..7d1b51b3f4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-bs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 otvoren tab. Dodirnite za promjenu tabova.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s otvorenih tabova. Dodirnite za promjenu tabova.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..5dbcca54f0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ca/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestanya oberta. Toqueu per canviar de pestanya.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestanyes obertes. Toqueu per canviar de pestanya.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..cb7c638e0c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cak/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ruwi\' jaqon. Tachapa\' richin nak\'ëx ruwi\'.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ruwi\' ejaqon. Tachapa\' richin nak\'ëx ruwi\'.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..26943c382c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ang abli nga tab. i-Tap para mobalhin ug mga tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ang abli nga mga tab. i-Tap para mobalhin ug mga tab.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..7152d88845
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">١ بازدەر کراوەیە. پەنجەدابگرە بۆ گۆڕینی بازدەرەکان.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s بازدەر کراوەیە. پەنجەدابگرە بۆ گۆڕینی بازدەرەکان.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..4c1798bd68
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-co/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 unghjetta aperta. Picchichjà per cambià d’unghjetta.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s unghjette aperte. Picchichjà per cambià d’unghjetta.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..cb7c806931
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Jeden otevřený panel. Klepnutím panely přepnete.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s otevřených panelů. Klepnutím přepnete panely.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..77d02443e7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-cy/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tab ar agor. Tapio i newid tabiau.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tab ar agor. Tapio i newid tabiau.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..ac794bbf47
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-da/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 åbent faneblad. Tryk for at skifte faneblade.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s åbne faneblade. Tryk for at skifte faneblade.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..704e927f0e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-de/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 offener Tab. Antippen, um Tabs zu wechseln.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s offene Tabs. Antippen, um Tabs zu wechseln.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..afb4eb2be2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 wócynjony rejtarik. Pótusniśo, aby rejtariki pśešaltował.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Wócynjone rejtariki: %1$s. Pótusniśo, aby rejtariki pśešaltował.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..f25b3f5377
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-el/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ανοικτή καρτέλα. Πατήστε για εναλλαγή καρτελών.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ανοικτές καρτέλες. Πατήστε για εναλλαγή καρτελών.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..4ca62a8c45
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..4ca62a8c45
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..d013c4a3a5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-eo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 malfermita langeto. Tuŝetu por ŝanĝi langeton.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s malfermitaj langetoj. Tuŝetu por ŝanĝi langeton.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..6344a3e800
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Tocá para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Tocá para cambiar de pestaña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..1fc73bee57
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Toca para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Toca para cambiar de pestaña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..1fc73bee57
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Toca para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Toca para cambiar de pestaña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..93340efa67
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Tocar para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Tocar para cambiar de pestaña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..1fc73bee57
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-es/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Toca para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Toca para cambiar de pestaña.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..9bb9d80603
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-et/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 avatud kaart. Kaartide vahetamiseks puuduta.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s avatud kaarti. Kaartide vahetamiseks puuduta.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..52ef09e49b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-eu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Irekitako fitxa bat. Sakatu fitxaz aldatzeko.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Irekitako %1$s fitxa. Sakatu fitxaz aldatzeko.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..c28779d31a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 زبانه باز. برای تغییر زبانه ها ضربه بزنید.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s زبانه‌های باز. برای تغییر زبانه‌ها ضربه بزنید.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..9043061f21
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 avoin välilehti. Napauta vaihtaaksesi.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s avointa välilehteä. Napauta vaihtaaksesi.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..5b0e434e7a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 onglet ouvert. Appuyez pour changer d’onglet.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s onglets ouverts. Appuyez pour changer d’onglet.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..66634fc877
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 schede vierte. Tocje par cambiâ schede.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s schedis viertis. Tocje par cambiâ schede.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..f6662bab43
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 iepen ljepblêd. Tik om tusken ljepblêden te wikseljen.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s iepen ljepblêden. Tik om tusken ljepblêden te wikseljen.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..c93c8ec7a6
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gd/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Tha taba fosgailte. Thoir gnogag airson leum a ghearradh gu taba eile.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Tha tabaichean (%1$s) fosgailte. Thoir gnogag airson leum a ghearradh gu taba eile.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..3148297284
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 separador aberto. Toque para cambiar de separador.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s separadores abertos. Toque para cambiar de separador.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..2716e6a454
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-gn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tendayke mbojuruja. Eikutu emoambue hag̃ua tendayke.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tendayke ijurujáva. Eikutu emoambue hag̃ua tendayke.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..5700f3b171
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 खुले टैब। टैब स्विच करने के लिए टैप करें।</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s खुले टैब। टैब स्विच करने के लिए टैप करें।</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..71af2cc3cd
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 otvorena kartica. Dodirni za prebacivanje kartica.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s otvorene kartice. Dodirni za prebacivanje kartica.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..f1a123a29e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 wočinjeny rajtark. Podótkńće so, zo byšće rajtarki přepinał.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Wočinjene rajtarki: %1$s. Podótkńće so, zo byšće rajtarki přepinał.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..1d101ccdd3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 nyitott lap. Koppintson a lapváltáshoz.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s nyitott lap. Koppintson a lapváltáshoz.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..4dcf37a304
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 բաց ներդիր: Հպեք՝ ներդիրին անցնելու համար:</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s բաց ներդիրներ: Հպեք՝ ներդիրին անցնելու համար:</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..978d967aab
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ia/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 scheda aperte. Tocca pro cambiar de scheda.
+</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s schedas aperte. Tocca pro cambiar de scheda.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..bce1963ede
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-in/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tab terbuka. Ketuk untuk beralih tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tab terbuka. Ketuk untuk beralih tab.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..a66a6df882
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-is/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 opinn flipi. Snertu til að skipta um flipa.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s opnir flipar. Snertu til að skipta um flipa.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..140462654e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-it/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Aperta 1 scheda. Tocca per passare alla scheda.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Aperte %1$s schede. Tocca per passare alle schede.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..4f1393a5e8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-iw/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">לשונית אחת פתוחה. יש להקיש כדי להחליף לשוניות.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s לשוניות פתוחות. יש להקיש כדי להחליף לשוניות.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..c9d1a62ca7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ja/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">開いているタブ 1 個。タップしてタブを切り替えます。</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">開いているタブ %1$s 個。タップしてタブを切り替えます。</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..934c9fea2d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ka/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 გახსნილი ჩანართი. შეეხეთ ჩანართების გადასართველად.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s გახსნილი ჩანართი. შეეხეთ ჩანართების გადასართველად.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..44c0e17a69
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 bet ashıq. Basqa betlerge ótiw ushın basıń.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s bet ashıq. Basqa betlerge ótiw ushın basıń.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..8c487c45bb
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kab/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 yiccer i yeldin. Sit akken ad tettbeddileḍ accaren.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s waccaren yeldin. Sit akken ad tettbeddileḍ gar waccaren.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..ac04517a9a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ашық бет. Беттерді ауыстыру үшін шертіңіз.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ашық бет. Беттерді ауыстыру үшін шертіңіз.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..822e18a09a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 hilpekîna vekirî. Ji bo hilpekînê biguherînî, bitikîne.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s hilpekînên vekirî. Ji bo hilpekînan biguherînî, bitikîne.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..5f133435b1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ko/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">열린 탭 1개. 탭을 전환하려면 누르세요.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">열린 탭 %1$s개. 탭을 전환하려면 누르세요.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..0b4ad9c0bc
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-lo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ແທັບທີ່ເປີດ. ແຕະເພື່ອປ່ຽນແທັບ.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ແທັບທີ່ເປີດ. ແຕະເພື່ອປ່ຽນແທັບ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..e11a4df1f6
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-lt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 atverta kortelė. Bakstelėkite, norėdami pereiti tarp kortelių.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s atvertos kortelės. Bakstelėkite, norėdami pereiti tarp kortelių.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..c5eb4b33bd
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-mr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 टॅब उघडी. टॅब स्विच करण्यासाठी टॅप करा.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s टॅब उघड्या. टॅब स्विच करण्यासाठी टॅप करा.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..da0d02a362
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-my/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ဖွင်ထားသော တပ်ဘ်. တပ်ဘ်များပြောင်းလဲရန်နှိပ်ပါ။</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">ဖွင့်ထားသော တက်ဗ်များ %1$s ခု။ တက်ဗ်များပြောင်းလဲရန်နှိပ်ပါ။</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..ce10fee7ea
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 åpen fane. Trykk for å bytte fane.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s åpne faner. Trykk for å bytte fane.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..297d697276
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1ट्याब खोल्नुहोस् । ट्याबहरु बिच स्वीच गर्नको लागि ट्याप गर्नुहोस् ।</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ट्याबहरु खोल्नुहोस्। ट्याबहरु बिच स्वीच गर्नको लागि ट्याप गर्नुहोस्।</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..02eb5b97e8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open tabblad. Tik om tussen tabbladen te wisselen.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open tabbladen. Tik om tussen tabbladen te wisselen.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..a743c17ac4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open fane. Trykk for å byte fane.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s opne faner. Trykk for å byte fane.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..6e5eb1f335
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-oc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 onglet dobèrt. Tocatz per i bascular.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s onglets dobèrts. Tocatz per cambiar d’onglet.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..b6941d927d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ਟੈਬ ਖੋਲ੍ਹੋ। ਟੈਬਾਂ ਬਦਲਣ ਲਈ ਛੂਹੋ।</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ਟੈਬਾਂ ਖੋਲ੍ਹੋ। ਟੈਬਾਂ ਬਦਲਣ ਲਈ ਛੂਹੋ।</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..fee2e99440
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">اک ٹیب کھُلھی اے۔ ہورناں ٹیب جاوݨ لئی اِتھے چھوہو۔</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ٹیباں کھُلھیاں ہن۔ ہورناں ٹیب جاوݨ لئی اِتھے چھوہو۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..95eff57834
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Otwarte karty: 1. Stuknij, aby przełączyć karty.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Otwarte karty: %1$s. Stuknij, aby przełączyć karty.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..c26434a581
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 aba aberta. Toque para alternar abas.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s abas abertas. Toque para alternar abas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..b0481b37f4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 separador aberto. Toque para mudar de separador.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s separadores abertos. Toque para mudar de separador.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..1a085d3aa4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-rm/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tab avert. Tutgar per midar tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tabs averts. Tutgar per midar tab.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..4e5f9a7a12
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ro/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 filă deschisă. Atinge pentru a comuta între file.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s file deschise. Atinge pentru a comuta între file.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..084301cc50
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ru/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 открытая вкладка. Нажмите, чтобы переключить вкладки.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Открытых вкладок: %1$s. Нажмите, чтобы переключить вкладки.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..31dce75121
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sat/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ᱴᱮᱵᱽ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ ᱾ ᱴᱮᱵᱽ ᱵᱚᱫᱚᱞ ᱞᱟᱹᱜᱤᱫ ᱴᱤᱯᱟᱹᱣ ᱢᱮ ᱾</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ᱴᱮᱵᱽ ᱠᱚ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ ᱾ ᱴᱮᱵᱽ ᱵᱚᱫᱚᱞ ᱞᱟᱹᱜᱤᱫ ᱴᱤᱯᱟᱹᱣ ᱢᱮ ᱾</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..aba35fe993
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ischeda aberta. Toca pro cuncambiare ischedas.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ischedas abertas. Toca pro cuncambiare ischedas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..fa81c6c72e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-si/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">විවෘත පටිති 1. මාරු වීමට ඔබන්න.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">විවෘත පටිති %1$s. මාරු වීමට ඔබන්න.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..6d9c07e007
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Počet otvorených kariet: 1. Ťuknutím prepnete karty.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Počet otvorených kariet: %1$s. Ťuknutím prepnete karty.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..259bf6e843
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-skr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ٹیب کھولو۔ ٹیبز بدلن کیتے دباؤ۔</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ٹیبز کھولو۔ ٹیبز بدلن کیتے دباؤ۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..da63aabfe3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 odprt zavihek. Tapnite za preklop zavihkov.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Odprtih zavihkov: %1$s. Tapnite za preklop zavihkov.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..ab5ac214b6
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sq/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 skedë e hapur. Prekeni që të ndërroni skeda.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s skeda të hapura. Prekeni që të ndërroni skeda.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..c2b36f1e84
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 отворен језичак. Додирни за пребацивање.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s отоврених језичака. Додирни за пребацивање.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..6ca8fedea8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-su/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tab muka. Toél pikeun ngagilir tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tab muka. Toél pikeun ngagilir tab.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..4dccc57f82
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 öppen flik. Tryck för att växla mellan flikar.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s öppna flikar. Tryck för att växla mellan flikar.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..d8a8f70ecd
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-szl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ôtwarto karta. Tyknij, coby przełōnczyć karty.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Ôtwarte karty: %1$s. Tyknij, coby je zmiynić.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..dd73074d4e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ta/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 திறந்த கீற்று. கீற்றுகளிடையே மாற தட்டு.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s திறந்த கீற்றுகள். கீற்றுகளிடையே மாற தட்டு.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..ec5a7e0129
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 варақаи кушодашуда. Барои гузариш байни варақаҳо зарба занед.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s варақаи кушодашуда. Барои гузариш байни варақаҳо зарба занед.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..8dd4edebd2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-th/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 แท็บที่เปิด แตะเพื่อสลับไปยังแท็บ</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s แท็บที่เปิด แตะเพื่อสลับไปยังแท็บ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..b90ceee0ac
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open na tab. i-Tap para mag switch ng tabs.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open na tabs. i-Tap para mag switch ng tabs.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..6fd9d0a795
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 açık sekme. Sekme değiştirmek için dokunun.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s açık sekme. Sekme değiştirmek için dokunun.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..73ab55b438
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-trs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 nā\'nïn rakïj ñanj. Gūru\'man ra\'a da\' nādūnāt rakïj ñanj.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s nā\'nïn nej rakïj ñanj. Gūru\'man ra\'a da\' nādūnāt nej rakïj ñanj.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..cccb0f592c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-tt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ачык таб. Табларны алмаштыру өчен басыгыз.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ачык таб. Табларны алмаштыру өчен басыгыз.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..b06335ccd7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ug/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 بەتكۈچ ئوچۇق. بەتكۈچنى ئالماشتۇرۇش ئۈچۈن چېكىڭ.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s بەتكۈچ ئوچۇق. بەتكۈچنى ئالماشتۇرۇش ئۈچۈن چېكىڭ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..7c4eac5634
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-uk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 відкрита вкладка. Торкніться, щоб перемкнути вкладки.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s відкритих вкладок. Торкніться, щоб перемкнути вкладки.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..2aff9ce62a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-ur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 کھلا ٹیب۔ ٹیبز بدلنے کے لئے دبائیں۔</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s کھلے ٹیب۔ ٹیب بدلنے کے لئے دبائیں۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..27d543d25d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-uz/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ta ochiq varaq. Boshqa varaqqa oʻtish uchun bosing.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ta ochiq varaq. Boshqa varaqqa oʻtish uchun bosing.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..df02a32682
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-vec/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Verta 1 scheda. Toca par pasare a ƚa scheda.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Verte %1$s schede. Toca par pasare a ƚe schede.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..b72a0a985d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-vi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 thẻ đang mở. Chạm để chuyển thẻ.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s thẻ đang mở. Chạm để chuyển thẻ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..702c4c49ec
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-yo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 sí táàbù. Fọwọ́ kàn án láti tan táàbù.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s sí táàbù. Fọwọ́ kàn án láti tan táàbù.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..56655683a4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">打开了 1 个标签页,点按即可切换。</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">打开了 %1$s 个标签页,点按即可切换。</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..69ffb4a021
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">開啟了 1 個分頁,點擊即可切換分頁。</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">開啟了 %1$s 個分頁,點擊即可切換分頁。</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/main/res/values/strings.xml b/mobile/android/android-components/components/compose/tabstray/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..961847b251
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+<?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>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs. %1$s is getting replaced with the number of open tabs. -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/compose/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/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/compose/tabstray/src/test/resources/robolectric.properties b/mobile/android/android-components/components/compose/tabstray/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/compose/tabstray/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/concept/awesomebar/README.md b/mobile/android/android-components/components/concept/awesomebar/README.md
new file mode 100644
index 0000000000..73b39c7b51
--- /dev/null
+++ b/mobile/android/android-components/components/concept/awesomebar/README.md
@@ -0,0 +1,47 @@
+# [Android Components](../../../README.md) > Concept > Awesomebar
+
+An abstract definition of an awesome bar component.
+
+## Usage
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md)):
+
+```Groovy
+implementation "org.mozilla.components:concept-awesomebar:{latest-version}"
+```
+
+### Implementing an Awesome Bar
+
+An Awesome Bar can be any [Android View](https://developer.android.com/reference/android/view/View.html) that implements the `AwesomeBar` interface.
+
+An `AwesomeBar` implementation needs to react to the following events:
+
+* `onInputStarted()`: The user starts interacting with the awesome bar by entering text in the [toolbar](../toolbar/README.md). This callback is a good place to initialize code that will be required once the user starts typing.
+* `onInputChanged(text: String)`: The user changed the text in the [toolbar](../toolbar/README.md). The awesome bar implementation should update its suggestions based on the text entered now.
+* `onInputCancelled()`: The user has cancelled their interaction with the awesome bar. This callback is a good place to free resources that are no longer needed.
+
+The suggestions an awesome bar displays are provided by an `SuggestionProvider`. Those providers are passed by the app (or another component) to the awesome bar by calling `addProviders()`. Once the text changes the awesome bar queries the `SuggestionProvider` instances and receives a list of `Suggestion` objects.
+
+Once the user selects a suggestion and the awesome bar wants to stop the interaction it can invoke the callback provided via the `setOnStopListener()` method. This is required as the awesome bar implementation is unaware of how it gets displayed and how interaction with it should be stopped (e.g. leaving the [toolbar's](../toolbar/README.md) editing mode).
+
+### Suggestions
+
+A `Suggestion` object contains the data required to be displayed and callbacks for when a suggestion was selected by the user.
+
+It is up to the suggestion or its provider to define the behavior that should happen in that situation (e.g. loading a URL, performing a search, switching tabs..).
+
+All data in the `Suggestion` object is optional. It is up to the awesome bar implementation to handle missing data (e.g. show the `description` instead of a missing `title`).
+
+Every `Suggestion` has an `id`. By default the `Suggestion` will generate a random ID. This ID can be used by the awesome bar to determine whether two suggestions are the same even though they are containing different/updated data. For example a `Suggestion` showing search suggestions from a search engine might use a constant ID when it is showing new search suggestions - to avoid the awesome bar implementation animating the previous suggestion leaving and a new suggestion appearing.
+
+### Implementing a Suggestion Provider
+
+For implementing a Suggestion Provider the `SuggestionProvider` interface needs to be implemented. The awesome bar forwards the events it receives to every provider: `onInputStarted()`, `onInputCancelled()`, `onInputChanged(text: String)`. A provider is required to return a list of `Suggestion` objects from `onInputChanged()`. This implementation can be synchronous. The awesome bar implementation takes care of performing the requests from worker threads.
+
+## 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/concept/awesomebar/build.gradle b/mobile/android/android-components/components/concept/awesomebar/build.gradle
new file mode 100644
index 0000000000..b3fec1fd8e
--- /dev/null
+++ b/mobile/android/android-components/components/concept/awesomebar/build.gradle
@@ -0,0 +1,33 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.awesomebar'
+}
+
+dependencies {
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+}
+
+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/concept/awesomebar/proguard-rules.pro b/mobile/android/android-components/components/concept/awesomebar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/awesomebar/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/concept/awesomebar/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/awesomebar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/awesomebar/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/concept/awesomebar/src/main/java/mozilla/components/concept/awesomebar/AwesomeBar.kt b/mobile/android/android-components/components/concept/awesomebar/src/main/java/mozilla/components/concept/awesomebar/AwesomeBar.kt
new file mode 100644
index 0000000000..73ce2ad57b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/awesomebar/src/main/java/mozilla/components/concept/awesomebar/AwesomeBar.kt
@@ -0,0 +1,222 @@
+/* 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.concept.awesomebar
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.view.View
+import java.util.UUID
+
+/**
+ * Interface to be implemented by awesome bar implementations.
+ *
+ * An awesome bar has multiple duties:
+ * - Display [Suggestion] instances and invoking its callbacks once selected
+ * - React to outside events: [onInputStarted], [onInputChanged], [onInputCancelled].
+ * - Query [SuggestionProvider] instances for new suggestions when the text changes.
+ */
+interface AwesomeBar {
+
+ /**
+ * Adds the following [SuggestionProvider] instances to be queried for [Suggestion]s whenever the text changes.
+ */
+ fun addProviders(vararg providers: SuggestionProvider)
+
+ /**
+ * Removes the following [SuggestionProvider]
+ */
+ fun removeProviders(vararg providers: SuggestionProvider)
+
+ /**
+ * Removes all [SuggestionProvider]s
+ */
+ fun removeAllProviders()
+
+ /**
+ * Returns whether or not this awesome bar contains the following [SuggestionProvider]
+ */
+ fun containsProvider(provider: SuggestionProvider): Boolean
+
+ /**
+ * Fired when the user starts interacting with the awesome bar by entering text in the toolbar.
+ */
+ fun onInputStarted() = Unit
+
+ /**
+ * Fired whenever the user changes their input, after they have started interacting with the awesome bar.
+ *
+ * @param text The current user input in the toolbar.
+ */
+ fun onInputChanged(text: String)
+
+ /**
+ * Fired when the user has cancelled their interaction with the awesome bar.
+ */
+ fun onInputCancelled() = Unit
+
+ /**
+ * Casts this awesome bar to an Android View object.
+ */
+ fun asView(): View = this as View
+
+ /**
+ * Adds a lambda to be invoked when the user has finished interacting with the awesome bar (e.g. selected a
+ * suggestion).
+ */
+ fun setOnStopListener(listener: () -> Unit)
+
+ /**
+ * Adds a lambda to be invoked when the user selected a suggestion to be edited further.
+ */
+ fun setOnEditSuggestionListener(listener: (String) -> Unit)
+
+ /**
+ * Information about the [Suggestion]s that are currently displayed by the [AwesomeBar].
+ */
+ data class VisibilityState(
+ /**
+ * An ordered map of the currently visible [SuggestionProviderGroup]s, and the visible [Suggestion]s in each
+ * group. The groups and their suggestions are ordered top to bottom.
+ */
+ val visibleProviderGroups: Map<SuggestionProviderGroup, List<Suggestion>> = emptyMap(),
+ )
+
+ /**
+ * A [Suggestion] to be displayed by an [AwesomeBar] implementation.
+ *
+ * @property provider The provider this suggestion came from.
+ * @property id A unique ID (provider scope) identifying this [Suggestion]. A stable ID but different data indicates
+ * to the [AwesomeBar] that this is the same [Suggestion] with new data. This will affect how the [AwesomeBar]
+ * animates showing the new suggestion.
+ * @property title A user-readable title for the [Suggestion].
+ * @property description A user-readable description for the [Suggestion].
+ * @property editSuggestion The string that will be set to the url bar when using the edit suggestion arrow.
+ * @property icon A lambda that can be invoked by the [AwesomeBar] implementation to receive an icon [Bitmap] for
+ * this [Suggestion]. The [AwesomeBar] will pass in its desired width and height for the Bitmap.
+ * @property indicatorIcon A drawable for indicating different types of [Suggestion].
+ * @property chips A list of [Chip] instances to be displayed.
+ * @property flags A set of [Flag] values for this [Suggestion].
+ * @property onSuggestionClicked A callback to be executed when the [Suggestion] was clicked by the user.
+ * @property onChipClicked A callback to be executed when a [Chip] was clicked by the user.
+ * @property score A score used to rank suggestions of this provider against each other. A suggestion with a higher
+ * score will be shown on top of suggestions with a lower score.
+ * @property metadata Opaque metadata associated with this [Suggestion]. A [SuggestionProvider] can use this field
+ * to pass additional information about this suggestion.
+ */
+ data class Suggestion(
+ val provider: SuggestionProvider,
+ val id: String = UUID.randomUUID().toString(),
+ val title: String? = null,
+ val description: String? = null,
+ val editSuggestion: String? = null,
+ val icon: Bitmap? = null,
+ val indicatorIcon: Drawable? = null,
+ val chips: List<Chip> = emptyList(),
+ val flags: Set<Flag> = emptySet(),
+ val onSuggestionClicked: (() -> Unit)? = null,
+ val onChipClicked: ((Chip) -> Unit)? = null,
+ val score: Int = 0,
+ val metadata: Map<String, Any>? = null,
+ ) {
+ /**
+ * Chips are compact actions that are shown as part of a suggestion. For example a [Suggestion] from a search
+ * engine may offer multiple search suggestion chips for different search terms.
+ */
+ data class Chip(
+ val title: String,
+ )
+
+ /**
+ * Flags can be added by a [SuggestionProvider] to help the [AwesomeBar] implementation decide how to display
+ * a specific [Suggestion]. For example an [AwesomeBar] could display a bookmark star icon next to [Suggestion]s
+ * that contain the [BOOKMARK] flag.
+ */
+ enum class Flag {
+ BOOKMARK,
+ HISTORY,
+ OPEN_TAB,
+ CLIPBOARD,
+ SYNC_TAB,
+ }
+
+ /**
+ * Returns true if the content of the two suggestions is the same.
+ *
+ * This is used by [AwesomeBar] implementations to decide whether an updated suggestion (same id) needs its
+ * view to be updated in order to display new data.
+ */
+ fun areContentsTheSame(other: Suggestion): Boolean {
+ return title == other.title &&
+ description == other.description &&
+ chips == other.chips &&
+ flags == other.flags
+ }
+ }
+
+ /**
+ * A [SuggestionProvider] is queried by an [AwesomeBar] whenever the text in the address bar is changed by the user.
+ * It returns a list of [Suggestion]s to be displayed by the [AwesomeBar].
+ */
+ interface SuggestionProvider {
+ /**
+ * A unique ID used for identifying this provider.
+ *
+ * The recommended approach for a [SuggestionProvider] implementation is to generate a UUID.
+ */
+ val id: String
+
+ /**
+ * A header title for grouping the suggestions.
+ **/
+ fun groupTitle(): String? = null
+
+ /**
+ * Fired when the user starts interacting with the awesome bar by entering text in the toolbar.
+ *
+ * The provider has the option to return an initial list of suggestions that will be displayed before the
+ * user has entered/modified any of the text.
+ */
+ fun onInputStarted(): List<Suggestion> = emptyList()
+
+ /**
+ * Fired whenever the user changes their input, after they have started interacting with the awesome bar.
+ *
+ * This is a suspending function. An [AwesomeBar] implementation is expected to invoke this method from a
+ * [Coroutine](https://kotlinlang.org/docs/reference/coroutines-overview.html). This allows the [AwesomeBar]
+ * implementation to group and cancel calls to multiple providers.
+ *
+ * Coroutine cancellation is cooperative. A coroutine code has to cooperate to be cancellable:
+ * https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/cancellation-and-timeouts.md
+ *
+ * @param text The current user input in the toolbar.
+ * @return A list of suggestions to be displayed by the [AwesomeBar].
+ */
+ suspend fun onInputChanged(text: String): List<Suggestion>
+
+ /**
+ * Fired when the user has cancelled their interaction with the awesome bar.
+ */
+ fun onInputCancelled() = Unit
+ }
+
+ /**
+ * A group of [SuggestionProvider]s.
+ *
+ * @property providers The list of [SuggestionProvider]s in this group.
+ * @property priority An optional priority for this group. Decides the order of this group
+ * in the AwesomeBar suggestions. Group having the highest integer value will have the highest priority.
+ * @property title An optional title for this group. The title may be rendered by an AwesomeBar
+ * implementation.
+ * @property limit The maximum number of suggestions that will be shown in this group.
+ * @property id A unique ID for this group (uses a generated UUID by default)
+ */
+ data class SuggestionProviderGroup(
+ val providers: List<SuggestionProvider>,
+ var priority: Int = 0,
+ val title: String? = null,
+ val limit: Int = Integer.MAX_VALUE,
+ val id: String = UUID.randomUUID().toString(),
+ )
+}
diff --git a/mobile/android/android-components/components/concept/base/README.md b/mobile/android/android-components/components/concept/base/README.md
new file mode 100644
index 0000000000..b98fc60e53
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/README.md
@@ -0,0 +1,21 @@
+# [Android Components](../../../README.md) > Concept > Base
+
+A component for basic interfaces needed by multiple components and that do not warrant a standalone component.
+
+## Usage
+
+Usually this component is not used by apps directly. Instead it will be referenced by other components as a transitive dependency.
+
+### 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:concept-base:{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/concept/base/build.gradle b/mobile/android/android-components/components/concept/base/build.gradle
new file mode 100644
index 0000000000..75de219239
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/build.gradle
@@ -0,0 +1,46 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ buildConfigField("String", "LIBRARY_VERSION", "\"" + config.componentsVersion + "\"")
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ namespace 'mozilla.components.concept.base'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_annotation
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockwebserver
+
+ testImplementation project(':support-test')
+}
+
+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/concept/base/proguard-rules.pro b/mobile/android/android-components/components/concept/base/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/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/concept/base/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/base/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/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/concept/base/src/main/java/mozilla/components/concept/base/crash/Breadcrumb.kt b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/Breadcrumb.kt
new file mode 100644
index 0000000000..061420ee47
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/Breadcrumb.kt
@@ -0,0 +1,134 @@
+/* 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.concept.base.crash
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import org.json.JSONObject
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * Represents a single crash breadcrumb.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class Breadcrumb(
+ /**
+ * Message of the crash breadcrumb.
+ */
+ val message: String = "",
+
+ /**
+ * Data related to the crash breadcrumb.
+ */
+ val data: Map<String, String> = emptyMap(),
+
+ /**
+ * Category of the crash breadcrumb.
+ */
+ val category: String = "",
+
+ /**
+ * Level of the crash breadcrumb.
+ */
+ val level: Level = Level.DEBUG,
+
+ /**
+ * Type of the crash breadcrumb.
+ */
+ val type: Type = Type.DEFAULT,
+
+ /**
+ * Date of the crash breadcrumb.
+ */
+ val date: Date = Date(),
+) : Parcelable, Comparable<Breadcrumb> {
+ /**
+ * Crash breadcrumb priority level.
+ */
+ enum class Level(val value: String) {
+ /**
+ * DEBUG level.
+ */
+ DEBUG("Debug"),
+
+ /**
+ * INFO level.
+ */
+ INFO("Info"),
+
+ /**
+ * WARNING level.
+ */
+ WARNING("Warning"),
+
+ /**
+ * ERROR level.
+ */
+ ERROR("Error"),
+
+ /**
+ * CRITICAL level.
+ */
+ CRITICAL("Critical"),
+ }
+
+ /**
+ * Crash breadcrumb type.
+ */
+ enum class Type(val value: String) {
+ /**
+ * DEFAULT type.
+ */
+ DEFAULT("Default"),
+
+ /**
+ * HTTP type.
+ */
+ HTTP("Http"),
+
+ /**
+ * NAVIGATION type.
+ */
+ NAVIGATION("Navigation"),
+
+ /**
+ * USER type.
+ */
+ USER("User"),
+ }
+
+ override fun compareTo(other: Breadcrumb): Int {
+ return this.date.compareTo(other.date)
+ }
+
+ /**
+ * Converts Breadcrumb into a JSON object
+ *
+ * @return A [JSONObject] that contains the information within the [Breadcrumb]
+ */
+ fun toJson(): JSONObject {
+ val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
+ simpleDateFormat.timeZone = TimeZone.getTimeZone("GMT")
+ val jsonObject = JSONObject()
+ jsonObject.put("timestamp", simpleDateFormat.format(this.date))
+ jsonObject.put("message", this.message)
+ jsonObject.put("category", this.category)
+ jsonObject.put("level", this.level.value)
+ jsonObject.put("type", this.type.value)
+
+ val dataJsonObject = JSONObject()
+ for ((k, v) in this.data) {
+ dataJsonObject.put(k, v)
+ }
+
+ jsonObject.put("data", dataJsonObject)
+ return jsonObject
+ }
+}
diff --git a/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/CrashReporting.kt b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/CrashReporting.kt
new file mode 100644
index 0000000000..6f6dc01907
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/CrashReporting.kt
@@ -0,0 +1,23 @@
+/* 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.concept.base.crash
+
+import kotlinx.coroutines.Job
+
+/**
+ * A crash reporter interface that can report caught exception to multiple services.
+ */
+interface CrashReporting {
+
+ /**
+ * Submit a caught exception report to all registered services.
+ */
+ fun submitCaughtException(throwable: Throwable): Job
+
+ /**
+ * Add a crash breadcrumb to all registered services with breadcrumb support.
+ */
+ fun recordCrashBreadcrumb(breadcrumb: Breadcrumb)
+}
diff --git a/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/RustCrashReport.kt b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/RustCrashReport.kt
new file mode 100644
index 0000000000..636e3bcb8b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/crash/RustCrashReport.kt
@@ -0,0 +1,19 @@
+/* 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.concept.base.crash
+
+/**
+ * Crash report for rust errors
+ *
+ * We implement this on exception classes that correspond to Rust errors to
+ * customize how the crash reports look.
+ *
+ * CrashReporting implementors should test if exceptions implement this
+ * interface. If so, they should try to customize their crash reports to match.
+ */
+interface RustCrashReport {
+ val typeName: String
+ val message: String
+}
diff --git a/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/images/ImageLoader.kt b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/images/ImageLoader.kt
new file mode 100644
index 0000000000..15f7d45af3
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/images/ImageLoader.kt
@@ -0,0 +1,32 @@
+/* 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.concept.base.images
+
+import android.graphics.drawable.Drawable
+import android.widget.ImageView
+import androidx.annotation.MainThread
+
+/**
+ * A loader that can load an image from an ID directly into an [ImageView].
+ */
+interface ImageLoader {
+
+ /**
+ * Loads an image asynchronously and then displays it in the [ImageView].
+ * If the view is detached from the window before loading is completed, then loading is cancelled.
+ *
+ * @param view [ImageView] to load the image into.
+ * @param request [ImageLoadRequest] Load image for this given request.
+ * @param placeholder [Drawable] to display while image is loading.
+ * @param error [Drawable] to display if loading fails.
+ */
+ @MainThread
+ fun loadIntoView(
+ view: ImageView,
+ request: ImageLoadRequest,
+ placeholder: Drawable? = null,
+ error: Drawable? = null,
+ )
+}
diff --git a/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/images/ImageRequest.kt b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/images/ImageRequest.kt
new file mode 100644
index 0000000000..f72d08a89b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/images/ImageRequest.kt
@@ -0,0 +1,28 @@
+/* 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.concept.base.images
+
+import androidx.annotation.Px
+
+/**
+ * A request to save an image. This is an alias for the id of the image.
+ *
+ * @property id The id of the image to save
+ * @property isPrivate Whether the image is related to a private tab.
+ */
+data class ImageSaveRequest(val id: String, val isPrivate: Boolean)
+
+/**
+ * A request to load an image.
+ *
+ * @property id The id of the image to retrieve.
+ * @property size The preferred size of the image that should be loaded in pixels.
+ * @property isPrivate Whether the image is related to a private tab.
+ */
+data class ImageLoadRequest(
+ val id: String,
+ @Px val size: Int,
+ val isPrivate: Boolean,
+)
diff --git a/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/memory/MemoryConsumer.kt b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/memory/MemoryConsumer.kt
new file mode 100644
index 0000000000..0713d5b0ce
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/memory/MemoryConsumer.kt
@@ -0,0 +1,26 @@
+/* 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.concept.base.memory
+
+import android.content.ComponentCallbacks2
+
+/**
+ * Interface for components that can seize large amounts of memory and support trimming in low
+ * memory situations.
+ *
+ * Also see [ComponentCallbacks2].
+ */
+interface MemoryConsumer {
+ /**
+ * Notifies this component that it should try to release memory.
+ *
+ * Should be called from a [ComponentCallbacks2] providing the level passed to
+ * [ComponentCallbacks2.onTrimMemory].
+ *
+ * @param level The context of the trim, giving a hint of the amount of
+ * trimming the application may like to perform. See constants in [ComponentCallbacks2].
+ */
+ fun onTrimMemory(level: Int)
+}
diff --git a/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/profiler/Profiler.kt b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/profiler/Profiler.kt
new file mode 100644
index 0000000000..93a9f2c647
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/src/main/java/mozilla/components/concept/base/profiler/Profiler.kt
@@ -0,0 +1,155 @@
+/* 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.concept.base.profiler
+
+/**
+ * [Profiler] is being used to manage Firefox Profiler related features.
+ *
+ * If you want to add a profiler marker to mark a point in time (without a duration)
+ * you can directly use `engine.profiler?.addMarker("marker name")`.
+ * Or if you want to provide more information, you can use
+ * `engine.profiler?.addMarker("marker name", "extra information")`.
+ *
+ * If you want to add a profiler marker with a duration (with start and end time)
+ * you can use it like this, it will automatically get the end time inside the addMarker:
+ * ```
+ * val startTime = engine.profiler?.getProfilerTime()
+ * ...some code you want to measure...
+ * engine.profiler?.addMarker("name", startTime)
+ * ```
+ *
+ * Or you can capture start and end time in somewhere, then add the marker in somewhere else:
+ * ```
+ * val startTime = engine.profiler?.getProfilerTime()
+ * ...some code you want to measure (or end time can be collected in a callback)...
+ * val endTime = engine.profiler?.getProfilerTime()
+ *
+ * ...somewhere else in the codebase...
+ * engine.profiler?.addMarker("name", startTime, endTime)
+ * ```
+ *
+ * Here's an [Profiler.addMarker] example with all the possible parameters:
+ * ```
+ * val startTime = engine.profiler?.getProfilerTime()
+ * ...some code you want to measure...
+ * val endTime = engine.profiler?.getProfilerTime()
+ *
+ * ...somewhere else in the codebase...
+ * engine.profiler?.addMarker("name", startTime, endTime, "extra information")
+ * ```
+ *
+ * [Profiler.isProfilerActive] method is handy when you want to get more information to
+ * add inside the marker, but you think it's going to be computationally heavy (and useless)
+ * when profiler is not running:
+ * ```
+ * val startTime = engine.profiler?.getProfilerTime()
+ * ...some code you want to measure...
+ * if (engine.profiler?.isProfilerActive()) {
+ * val info = aFunctionYouDoNotWantToCallWhenProfilerIsNotActive()
+ * engine.profiler?.addMarker("name", startTime, info)
+ * }
+ * ```
+ */
+interface Profiler {
+ /**
+ * Returns true if profiler is active and it's allowed the add markers.
+ * It's useful when it's computationally heavy to get startTime or the
+ * additional text for the marker. That code can be wrapped with
+ * isProfilerActive if check to reduce the overhead of it.
+ *
+ * @return true if profiler is active and safe to add a new marker.
+ */
+ fun isProfilerActive(): Boolean
+
+ /**
+ * Get the profiler time to be able to mark the start of the marker events.
+ * can be used like this:
+ *
+ * <code>
+ * val startTime = engine.profiler?.getProfilerTime()
+ * ...some code you want to measure...
+ * engine.profiler?.addMarker("name", startTime)
+ * </code>
+ *
+ * @return profiler time as Double or null if the profiler is not active.
+ */
+ fun getProfilerTime(): Double?
+
+ /**
+ * Add a profiler marker to Gecko Profiler with the given arguments.
+ * It can be used for either adding a point-in-time marker or a duration marker.
+ * No-op if profiler is not active.
+ *
+ * @param markerName Name of the event as a string.
+ * @param startTime Start time as Double. It can be null if you want to mark a point of time.
+ * @param endTime End time as Double. If it's null, this function implicitly gets the end time.
+ * @param text An optional string field for more information about the marker.
+ */
+ fun addMarker(markerName: String, startTime: Double?, endTime: Double?, text: String?)
+
+ /**
+ * Add a profiler marker to Gecko Profiler with the given arguments.
+ * End time will be added automatically with the current profiler time when the function is called.
+ * No-op if profiler is not active.
+ * This is an overload of [Profiler.addMarker] for convenience.
+ *
+ * @param aMarkerName Name of the event as a string.
+ * @param aStartTime Start time as Double. It can be null if you want to mark a point of time.
+ * @param aText An optional string field for more information about the marker.
+ */
+ fun addMarker(markerName: String, startTime: Double?, text: String?)
+
+ /**
+ * Add a profiler marker to Gecko Profiler with the given arguments.
+ * End time will be added automatically with the current profiler time when the function is called.
+ * No-op if profiler is not active.
+ * This is an overload of [Profiler.addMarker] for convenience.
+ *
+ * @param markerName Name of the event as a string.
+ * @param startTime Start time as Double. It can be null if you want to mark a point of time.
+ */
+ fun addMarker(markerName: String, startTime: Double?)
+
+ /**
+ * Add a profiler marker to Gecko Profiler with the given arguments.
+ * Time will be added automatically with the current profiler time when the function is called.
+ * No-op if profiler is not active.
+ * This is an overload of [Profiler.addMarker] for convenience.
+ *
+ * @param markerName Name of the event as a string.
+ * @param text An optional string field for more information about the marker.
+ */
+ fun addMarker(markerName: String, text: String?)
+
+ /**
+ * Add a profiler marker to Gecko Profiler with the given arguments.
+ * Time will be added automatically with the current profiler time when the function is called.
+ * No-op if profiler is not active.
+ * This is an overload of [Profiler.addMarker] for convenience.
+ *
+ * @param markerName Name of the event as a string.
+ */
+ fun addMarker(markerName: String)
+
+ /**
+ * Start the Gecko profiler with the given settings. This is used by embedders which want to
+ * control the profiler from the embedding app. This allows them to provide an easier access point
+ * to profiling, as an alternative to the traditional way of using a desktop Firefox instance
+ * connected via USB + adb.
+ *
+ * @param aFilters The list of threads to profile, as an array of string of thread names filters.
+ * Each filter is used as a case-insensitive substring match against the actual thread names.
+ * @param aFeaturesArr The list of profiler features to enable for profiling, as a string array.
+ */
+ fun startProfiler(filters: Array<String>, features: Array<String>)
+
+ /**
+ * Stop the profiler and capture the recorded profile. This method is asynchronous.
+ *
+ * @return GeckoResult for the captured profile. The profile is returned as a byte[] buffer
+ * containing a gzip-compressed payload (with gzip header) of the profile JSON.
+ */
+ fun stopProfiler(onSuccess: (ByteArray?) -> Unit, onError: (Throwable) -> Unit)
+}
diff --git a/mobile/android/android-components/components/concept/base/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/concept/base/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/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/concept/base/src/test/resources/robolectric.properties b/mobile/android/android-components/components/concept/base/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/concept/base/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/concept/engine/README.md b/mobile/android/android-components/components/concept/engine/README.md
new file mode 100644
index 0000000000..6a0b66bf84
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/README.md
@@ -0,0 +1,45 @@
+# [Android Components](../../../README.md) > Concept > Engine
+
+The `concept-engine` component contains interfaces and abstract classes that hide the actual browser engine implementation from other components needing access to the browser engine.
+
+There are implementations for [WebView](https://developer.android.com/reference/android/webkit/WebView) and multiple release channels of [GeckoView](https://wiki.mozilla.org/Mobile/GeckoView) available.
+
+Other components and apps only referencing `concept-engine` makes it possible to:
+
+* Build components that work independently of the engine being used.
+* Build apps that can work with multiple engines (Compile-time or Run-time).
+* Build apps that can be build against different GeckoView release channels (Nightly/Beta/Release).
+
+## Usage
+
+### 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:concept-engine:{latest-version}"
+```
+
+### Integration
+
+Usually it is not needed to interact with the `Engine` component directly. The [browser-session](../../browser/session/README.md) component will take care of making the state accessible and link a `Session` to an `EngineSession` internally. The [feature-session](../../feature/session/README.md) component will provide "use cases" to perform actions like loading URLs and takes care of rendering the selected `Session` on an `EngineView`.
+``
+### Observing changes
+
+Every `EngineSession` can be observed for changes by registering an `EngineSession.Observer` instance.
+
+```Kotlin
+engineSession.register(object : EngineSession.Observer {
+ onLocationChange(url: String) {
+ // This session is pointing to a different URL now.
+ }
+})
+```
+
+`EngineSession.Observer` provides empty default implementation of every method so that only the needed ones need to be overridden. See the API reference of the current version to see all available methods.
+
+## 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/concept/engine/build.gradle b/mobile/android/android-components/components/concept/engine/build.gradle
new file mode 100644
index 0000000000..a84492bacb
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/build.gradle
@@ -0,0 +1,52 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.engine'
+}
+
+dependencies {
+ implementation project(':support-ktx')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_paging
+
+ // We expose this as API because we are using Observable in our public API and do not want every
+ // consumer to have to manually import "base".
+ api project(':support-base')
+ api project(':browser-errorpages')
+ api project(':concept-storage')
+ api project(':concept-fetch')
+
+ testImplementation project(':support-utils')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.kotlin_reflect
+
+ testImplementation project(':support-test')
+}
+
+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/concept/engine/proguard-rules.pro b/mobile/android/android-components/components/concept/engine/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/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/concept/engine/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/engine/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/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/concept/engine/src/main/java/mozilla/components/concept/engine/CancellableOperation.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/CancellableOperation.kt
new file mode 100644
index 0000000000..ce819011ab
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/CancellableOperation.kt
@@ -0,0 +1,31 @@
+/* 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.concept.engine
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+
+/**
+ * Represents an async operation that can be cancelled.
+ */
+interface CancellableOperation {
+
+ /**
+ * Implementation of [CancellableOperation] that does nothing (for
+ * testing purposes or implementing default methods.)
+ */
+ class Noop : CancellableOperation {
+ override fun cancel(): Deferred<Boolean> {
+ return CompletableDeferred(true)
+ }
+ }
+
+ /**
+ * Cancels this operation.
+ *
+ * @return a deferred value indicating whether or not cancellation was successful.
+ */
+ fun cancel(): Deferred<Boolean>
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/DataCleanable.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/DataCleanable.kt
new file mode 100644
index 0000000000..bd69001857
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/DataCleanable.kt
@@ -0,0 +1,26 @@
+/* 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.concept.engine
+
+/**
+ * Contract to indicate how objects with the ability to clear data should behave.
+ */
+interface DataCleanable {
+ /**
+ * Clears browsing data stored.
+ *
+ * @param data the type of data that should be cleared, defaults to all.
+ * @param host (optional) name of the host for which data should be cleared. If
+ * omitted data will be cleared for all hosts.
+ * @param onSuccess (optional) callback invoked if the data was cleared successfully.
+ * @param onError (optional) callback invoked if clearing the data caused an exception.
+ */
+ fun clearData(
+ data: Engine.BrowsingData = Engine.BrowsingData.all(),
+ host: String? = null,
+ onSuccess: (() -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ): Unit = onError(UnsupportedOperationException("Clearing browsing data is not supported."))
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt
new file mode 100644
index 0000000000..9c37662514
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt
@@ -0,0 +1,282 @@
+/* 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.concept.engine
+
+import android.content.Context
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.util.JsonReader
+import androidx.annotation.MainThread
+import mozilla.components.concept.base.profiler.Profiler
+import mozilla.components.concept.engine.activity.ActivityDelegate
+import mozilla.components.concept.engine.activity.OrientationDelegate
+import mozilla.components.concept.engine.content.blocking.TrackerLog
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
+import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
+import mozilla.components.concept.engine.translate.TranslationsRuntime
+import mozilla.components.concept.engine.utils.EngineVersion
+import mozilla.components.concept.engine.webextension.WebExtensionRuntime
+import mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
+import mozilla.components.concept.engine.webpush.WebPushDelegate
+import mozilla.components.concept.engine.webpush.WebPushHandler
+import org.json.JSONObject
+
+/**
+ * Entry point for interacting with the engine implementation.
+ */
+interface Engine : WebExtensionRuntime, TranslationsRuntime, DataCleanable {
+
+ /**
+ * Describes a combination of browsing data types stored by the engine.
+ */
+ class BrowsingData internal constructor(val types: Int) {
+ companion object {
+ const val COOKIES: Int = 1 shl 0
+ const val NETWORK_CACHE: Int = 1 shl 1
+ const val IMAGE_CACHE: Int = 1 shl 2
+ const val DOM_STORAGES: Int = 1 shl 4
+ const val AUTH_SESSIONS: Int = 1 shl 5
+ const val PERMISSIONS: Int = 1 shl 6
+ const val ALL_CACHES: Int = NETWORK_CACHE + IMAGE_CACHE
+ const val ALL_SITE_SETTINGS: Int = (1 shl 7) + PERMISSIONS
+ const val ALL_SITE_DATA: Int = (1 shl 8) + COOKIES + DOM_STORAGES + ALL_CACHES + ALL_SITE_SETTINGS
+ const val ALL: Int = 1 shl 9
+
+ fun allCaches() = BrowsingData(ALL_CACHES)
+ fun allSiteSettings() = BrowsingData(ALL_SITE_SETTINGS)
+ fun allSiteData() = BrowsingData(ALL_SITE_DATA)
+ fun all() = BrowsingData(ALL)
+ fun select(vararg types: Int) = BrowsingData(types.sum())
+ }
+
+ fun contains(type: Int) = (types and type) != 0 || types == ALL
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BrowsingData) return false
+ if (types != other.types) return false
+ return true
+ }
+
+ override fun hashCode() = types
+ }
+
+ /**
+ * HTTPS-Only mode: Connections will be upgraded to HTTPS.
+ */
+ enum class HttpsOnlyMode {
+ /**
+ * HTTPS-Only Mode disabled: Allow all insecure connections.
+ */
+ DISABLED,
+
+ /**
+ * HTTPS-Only Mode enabled only in private tabs: Allow insecure connections in normal
+ * browsing, but only HTTPS in private browsing.
+ */
+ ENABLED_PRIVATE_ONLY,
+
+ /**
+ * HTTPS-Only Mode enabled: Only allow HTTPS connections.
+ */
+ ENABLED,
+ }
+
+ /**
+ * Makes sure all required engine initialization logic is executed. The
+ * details are specific to individual implementations, but the following must be true:
+ *
+ * - The engine must be operational after this method was called successfully
+ * - Calling this method on an engine that is already initialized has no effect
+ */
+ @MainThread
+ fun warmUp() = Unit
+
+ /**
+ * Creates a new view for rendering web content.
+ *
+ * @param context an application context
+ * @param attrs optional set of attributes
+ *
+ * @return new newly created [EngineView].
+ */
+ fun createView(context: Context, attrs: AttributeSet? = null): EngineView
+
+ /**
+ * Creates a new engine session. If [speculativeCreateSession] is supported this
+ * method returns the prepared [EngineSession] if it is still applicable i.e.
+ * the parameter(s) ([private]) are equal.
+ *
+ * @param private whether or not this session should use private mode.
+ * @param contextId the session context ID for this session.
+ *
+ * @return the newly created [EngineSession].
+ */
+ @MainThread
+ fun createSession(private: Boolean = false, contextId: String? = null): EngineSession
+
+ /**
+ * Create a new [EngineSessionState] instance from the serialized JSON representation.
+ */
+ fun createSessionState(json: JSONObject): EngineSessionState
+
+ /**
+ * Creates a new [EngineSessionState] instances from the serialized JSON representation.
+ */
+ fun createSessionStateFrom(reader: JsonReader): EngineSessionState
+
+ /**
+ * Returns the name of this engine. The returned string might be used
+ * in filenames and must therefore only contain valid filename
+ * characters.
+ *
+ * @return the engine name as specified by concrete implementations.
+ */
+ fun name(): String
+
+ /**
+ * Opens a speculative connection to the host of [url].
+ *
+ * This is useful if an app thinks it may be making a request to that host in the near future. If no request
+ * is made, the connection will be cleaned up after an unspecified.
+ *
+ * Not all [Engine] implementations may actually implement this.
+ */
+ fun speculativeConnect(url: String)
+
+ /**
+ * Informs the engine that an [EngineSession] is likely to be requested soon
+ * via [createSession]. This is useful in case creating an engine session is
+ * costly and an application wants to decide when the session should be created
+ * without having to manage the session itself i.e. when it may or may not
+ * need it.
+ *
+ * @param private whether or not the session should use private mode.
+ * @param contextId the session context ID for the session.
+ */
+ @MainThread
+ fun speculativeCreateSession(private: Boolean = false, contextId: String? = null) = Unit
+
+ /**
+ * Removes and closes a speculative session created by [speculativeCreateSession]. This is
+ * useful in case the session should no longer be used e.g. because engine settings have
+ * changed.
+ */
+ @MainThread
+ fun clearSpeculativeSession() = Unit
+
+ /**
+ * Registers a [WebNotificationDelegate] to be notified of engine events
+ * related to web notifications
+ *
+ * @param webNotificationDelegate callback to be invoked for web notification events.
+ */
+ fun registerWebNotificationDelegate(
+ webNotificationDelegate: WebNotificationDelegate,
+ ): Unit = throw UnsupportedOperationException("Web notification support is not available in this engine")
+
+ /**
+ * Registers a [WebPushDelegate] to be notified of engine events related to web extensions.
+ *
+ * @return A [WebPushHandler] to notify the engine with messages and subscriptions when are delivered.
+ */
+ fun registerWebPushDelegate(
+ webPushDelegate: WebPushDelegate,
+ ): WebPushHandler = throw UnsupportedOperationException("Web Push support is not available in this engine")
+
+ /**
+ * Registers an [ActivityDelegate] to be notified on activity events that are needed by the engine.
+ */
+ fun registerActivityDelegate(
+ activityDelegate: ActivityDelegate,
+ ): Unit = throw UnsupportedOperationException("This engine does not have support for an Activity delegate.")
+
+ /**
+ * Un-registers the attached [ActivityDelegate] if one was added with [registerActivityDelegate].
+ */
+ fun unregisterActivityDelegate(): Unit =
+ throw UnsupportedOperationException("This engine does not have support for an Activity delegate.")
+
+ /**
+ * Registers an [OrientationDelegate] to be notified when a website asked the engine
+ * to lock the the app on a certain screen orientation.
+ */
+ fun registerScreenOrientationDelegate(
+ delegate: OrientationDelegate,
+ ): Unit = throw UnsupportedOperationException("This engine does not have support for an Activity delegate.")
+
+ /**
+ * Un-registers the attached [OrientationDelegate] if one was added with
+ * [registerScreenOrientationDelegate].
+ */
+ fun unregisterScreenOrientationDelegate(): Unit =
+ throw UnsupportedOperationException("This engine does not have support for an Activity delegate.")
+
+ /**
+ * Registers a [ServiceWorkerDelegate] to be notified of service workers events and requests.
+ *
+ * @param serviceWorkerDelegate [ServiceWorkerDelegate] responding to all service workers events and requests.
+ */
+ fun registerServiceWorkerDelegate(
+ serviceWorkerDelegate: ServiceWorkerDelegate,
+ ): Unit = throw UnsupportedOperationException("Service workers support not available in this engine")
+
+ /**
+ * Un-registers the attached [ServiceWorkerDelegate] if one was added with
+ * [registerServiceWorkerDelegate].
+ */
+ fun unregisterServiceWorkerDelegate(): Unit =
+ throw UnsupportedOperationException("Service workers support not available in this engine")
+
+ /**
+ * Handles user interacting with a web notification.
+ *
+ * @param webNotification [Parcelable] representing a web notification.
+ * If the `Parcelable` is not a web notification this method will be no-op.
+ *
+ * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification">MDN Notification docs</a>
+ */
+ fun handleWebNotificationClick(webNotification: Parcelable): Unit =
+ throw UnsupportedOperationException("Web notification clicks not yet supported in this engine")
+
+ /**
+ * Fetch a list of trackers logged for a given [session] .
+ *
+ * @param session the session where the trackers were logged.
+ * @param onSuccess callback invoked if the data was fetched successfully.
+ * @param onError (optional) callback invoked if fetching the data caused an exception.
+ */
+ fun getTrackersLog(
+ session: EngineSession,
+ onSuccess: (List<TrackerLog>) -> Unit,
+ onError: (Throwable) -> Unit = { },
+ ): Unit = onError(
+ UnsupportedOperationException(
+ "getTrackersLog is not supported by this engine.",
+ ),
+ )
+
+ /**
+ * Provides access to the tracking protection exception list for this engine.
+ */
+ val trackingProtectionExceptionStore: TrackingProtectionExceptionStorage
+ get() = throw UnsupportedOperationException("TrackingProtectionExceptionStorage not supported by this engine.")
+
+ /**
+ * Provides access to Firefox Profiler features.
+ * See [Profiler] for more information.
+ */
+ val profiler: Profiler?
+
+ /**
+ * Provides access to the settings of this engine.
+ */
+ val settings: Settings
+
+ /**
+ * Returns the version of the engine as [EngineVersion] object.
+ */
+ val version: EngineVersion
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt
new file mode 100644
index 0000000000..1250f0f35c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt
@@ -0,0 +1,1103 @@
+/* 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.concept.engine
+
+import android.content.Intent
+import androidx.annotation.CallSuper
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy.ACCEPT_ALL
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.history.HistoryItem
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.media.RecordingDevice
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.shopping.ProductAnalysis
+import mozilla.components.concept.engine.shopping.ProductAnalysisStatus
+import mozilla.components.concept.engine.shopping.ProductRecommendation
+import mozilla.components.concept.engine.translate.TranslationEngineState
+import mozilla.components.concept.engine.translate.TranslationError
+import mozilla.components.concept.engine.translate.TranslationOperation
+import mozilla.components.concept.engine.translate.TranslationOptions
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+
+/**
+ * Class representing a single engine session.
+ *
+ * In browsers usually a session corresponds to a tab.
+ */
+@Suppress("TooManyFunctions")
+abstract class EngineSession(
+ private val delegate: Observable<Observer> = ObserverRegistry(),
+) : Observable<EngineSession.Observer> by delegate, DataCleanable {
+ /**
+ * Interface to be implemented by classes that want to observe this engine session.
+ */
+ interface Observer {
+ /**
+ * Event to indicate the scroll position of the content has changed.
+ *
+ * @param scrollX The new horizontal scroll position in pixels.
+ * @param scrollY The new vertical scroll position in pixels.
+ */
+ fun onScrollChange(scrollX: Int, scrollY: Int) = Unit
+
+ fun onLocationChange(url: String, hasUserGesture: Boolean) = Unit
+ fun onTitleChange(title: String) = Unit
+
+ /**
+ * Event to indicate a preview image URL was discovered in the content after the content loaded.
+ *
+ * @param previewImageUrl The preview image URL sent from the content.
+ */
+ fun onPreviewImageChange(previewImageUrl: String) = Unit
+
+ fun onProgress(progress: Int) = Unit
+ fun onLoadingStateChange(loading: Boolean) = Unit
+ fun onNavigationStateChange(canGoBack: Boolean? = null, canGoForward: Boolean? = null) = Unit
+ fun onSecurityChange(secure: Boolean, host: String? = null, issuer: String? = null) = Unit
+ fun onTrackerBlockingEnabledChange(enabled: Boolean) = Unit
+
+ /**
+ * Event to indicate a new [CookieBannerHandlingStatus] is available.
+ */
+ fun onCookieBannerChange(status: CookieBannerHandlingStatus) = Unit
+ fun onTrackerBlocked(tracker: Tracker) = Unit
+ fun onTrackerLoaded(tracker: Tracker) = Unit
+ fun onNavigateBack() = Unit
+
+ /**
+ * Event to indicate a product URL is currently open.
+ */
+ fun onProductUrlChange(isProductUrl: Boolean) = Unit
+
+ /**
+ * Event to indicate that a url was loaded to this session.
+ */
+ fun onLoadUrl() = Unit
+
+ /**
+ * Event to indicate that the session was requested to navigate to a specified index.
+ */
+ fun onGotoHistoryIndex() = Unit
+
+ /**
+ * Event to indicate that the session was requested to render data.
+ */
+ fun onLoadData() = Unit
+
+ /**
+ * Event to indicate that the session was requested to navigate forward in history
+ */
+ fun onNavigateForward() = Unit
+
+ /**
+ * Event to indicate whether or not this [EngineSession] should be [excluded] from tracking protection.
+ */
+ fun onExcludedOnTrackingProtectionChange(excluded: Boolean) = Unit
+
+ /**
+ * Event to indicate that this session has had it's first engine contentful paint of page content.
+ */
+ fun onFirstContentfulPaint() = Unit
+
+ /**
+ * Event to indicate that this session has had it's paint status reset.
+ */
+ fun onPaintStatusReset() = Unit
+ fun onLongPress(hitResult: HitResult) = Unit
+ fun onDesktopModeChange(enabled: Boolean) = Unit
+ fun onFind(text: String) = Unit
+ fun onFindResult(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean) = Unit
+ fun onFullScreenChange(enabled: Boolean) = Unit
+
+ /**
+ * @param layoutInDisplayCutoutMode value of defined in https://developer.android.com/reference/android/view/WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ */
+ fun onMetaViewportFitChanged(layoutInDisplayCutoutMode: Int) = Unit
+ fun onAppPermissionRequest(permissionRequest: PermissionRequest) = permissionRequest.reject()
+ fun onContentPermissionRequest(permissionRequest: PermissionRequest) = permissionRequest.reject()
+ fun onCancelContentPermissionRequest(permissionRequest: PermissionRequest) = Unit
+ fun onPromptRequest(promptRequest: PromptRequest) = Unit
+
+ /**
+ * The engine has requested a prompt be dismissed.
+ */
+ fun onPromptDismissed(promptRequest: PromptRequest) = Unit
+
+ /**
+ * The engine has requested a prompt update.
+ */
+ fun onPromptUpdate(previousPromptRequestUid: String, promptRequest: PromptRequest) = Unit
+
+ /**
+ * User cancelled a repost prompt. Page will not be reloaded.
+ */
+ fun onRepostPromptCancelled() = Unit
+
+ /**
+ * User cancelled a beforeunload prompt. Navigating to another page is cancelled.
+ */
+ fun onBeforeUnloadPromptDenied() = Unit
+
+ /**
+ * The engine received a request to open or close a window.
+ *
+ * @param windowRequest the request to describing the required window action.
+ */
+ fun onWindowRequest(windowRequest: WindowRequest) = Unit
+
+ /**
+ * Based on the webpage current state the toolbar should be expanded to it's full height
+ * previously specified in [EngineView.setDynamicToolbarMaxHeight].
+ */
+ fun onShowDynamicToolbar() = Unit
+
+ /**
+ * Notify that the given media session has become active.
+ *
+ * @param mediaSessionController The associated [MediaSession.Controller].
+ */
+ fun onMediaActivated(mediaSessionController: MediaSession.Controller) = Unit
+
+ /**
+ * Notify that the given media session has become inactive.
+ * Inactive media sessions can not be controlled.
+ */
+ fun onMediaDeactivated() = Unit
+
+ /**
+ * Notify on updated metadata.
+ *
+ * @param metadata The updated [MediaSession.Metadata].
+ */
+ fun onMediaMetadataChanged(metadata: MediaSession.Metadata) = Unit
+
+ /**
+ * Notify on updated supported features.
+ *
+ * @param features A combination of [MediaSession.Feature].
+ */
+ fun onMediaFeatureChanged(features: MediaSession.Feature) = Unit
+
+ /**
+ * Notify that playback has changed for the given media session.
+ *
+ * @param playbackState The updated [MediaSession.PlaybackState].
+ */
+ fun onMediaPlaybackStateChanged(playbackState: MediaSession.PlaybackState) = Unit
+
+ /**
+ * Notify on updated position state.
+ *
+ * @param positionState The updated [MediaSession.PositionState].
+ */
+ fun onMediaPositionStateChanged(positionState: MediaSession.PositionState) = Unit
+
+ /**
+ * Notify changed audio mute state.
+ *
+ * @param muted True if audio of this media session is muted.
+ */
+ fun onMediaMuteChanged(muted: Boolean) = Unit
+
+ /**
+ * Notify on changed fullscreen state.
+ *
+ * @param fullscreen True when this media session in in fullscreen mode.
+ * @param elementMetadata An instance of [MediaSession.ElementMetadata], if enabled.
+ */
+ fun onMediaFullscreenChanged(
+ fullscreen: Boolean,
+ elementMetadata: MediaSession.ElementMetadata?,
+ ) = Unit
+
+ fun onWebAppManifestLoaded(manifest: WebAppManifest) = Unit
+ fun onCrash() = Unit
+ fun onProcessKilled() = Unit
+ fun onRecordingStateChanged(devices: List<RecordingDevice>) = Unit
+
+ /**
+ * Event to indicate that a new saved [EngineSessionState] is available.
+ */
+ fun onStateUpdated(state: EngineSessionState) = Unit
+
+ /**
+ * The engine received a request to load a request.
+ *
+ * @param url The string url that was requested.
+ * @param triggeredByRedirect True if and only if the request was triggered by an HTTP redirect.
+ * @param triggeredByWebContent True if and only if the request was triggered from within
+ * web content (as opposed to via the browser chrome).
+ *
+ * Unlike the name LoadRequest.isRedirect may imply this flag is not about http redirects.
+ * The flag is "True if and only if the request was triggered by an HTTP redirect."
+ * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1545170
+ */
+ fun onLoadRequest(
+ url: String,
+ triggeredByRedirect: Boolean,
+ triggeredByWebContent: Boolean,
+ ) = Unit
+
+ /**
+ * The engine received a request to launch a app intent.
+ *
+ * @param url The string url that was requested.
+ * @param appIntent The Android Intent that was requested.
+ * web content (as opposed to via the browser chrome).
+ */
+ fun onLaunchIntentRequest(
+ url: String,
+ appIntent: Intent?,
+ ) = Unit
+
+ /**
+ * The engine received a request to download a file.
+ *
+ * @param url The string url that was requested.
+ * @param fileName The file name.
+ * @param contentLength The size of the file to be downloaded.
+ * @param contentType The type of content to be downloaded.
+ * @param cookie The cookie related to request.
+ * @param userAgent The user agent of the engine.
+ * @param skipConfirmation Whether or not the confirmation dialog should be shown before the download begins.
+ * @param openInApp Whether or not the associated resource should be opened in a third party
+ * app after processed successfully.
+ * @param isPrivate Indicates if the download was requested from a private session.
+ * @param response A response object associated with this request, when provided can be
+ * used instead of performing a manual a download.
+ */
+ fun onExternalResource(
+ url: String,
+ fileName: String? = null,
+ contentLength: Long? = null,
+ contentType: String? = null,
+ cookie: String? = null,
+ userAgent: String? = null,
+ isPrivate: Boolean = false,
+ skipConfirmation: Boolean = false,
+ openInApp: Boolean = false,
+ response: Response? = null,
+ ) = Unit
+
+ /**
+ * Event to indicate that this session has changed its history state.
+ *
+ * @param historyList The list of items in the session history.
+ * @param currentIndex Index of the current page in the history list.
+ */
+ fun onHistoryStateChanged(historyList: List<HistoryItem>, currentIndex: Int) = Unit
+
+ /**
+ * Event to indicate that an exception was thrown while generating a PDF.
+ *
+ * @param throwable The throwable from the exception.
+ */
+ fun onSaveToPdfException(throwable: Throwable) = Unit
+
+ /**
+ * Event to indicate that printing finished.
+ */
+ fun onPrintFinish() = Unit
+
+ /**
+ * Event to indicate that an exception was thrown while preparing to print or save as pdf.
+ *
+ * @param isPrint true for a true print error or false for a Save as PDF error.
+ * @param throwable The exception throwable. Usually a GeckoPrintException.
+ */
+ fun onPrintException(isPrint: Boolean, throwable: Throwable) = Unit
+
+ /**
+ * Event to indicate that the PDF was successfully generated.
+ */
+ fun onSaveToPdfComplete() = Unit
+
+ /**
+ * Event to indicate that this session needs to be checked for form data.
+ *
+ * @param containsFormData Indicates if the session has form data.
+ */
+ fun onCheckForFormData(containsFormData: Boolean) = Unit
+
+ /**
+ * Event to indicate that an exception was thrown while checking for form data.
+ *
+ * @param throwable The throwable from the exception.
+ */
+ fun onCheckForFormDataException(throwable: Throwable) = Unit
+
+ /**
+ * Event to indicate that the translations engine expects that the user will likely
+ * request page translation.
+ *
+ * The usual use case is to show a prominent translations UI entrypoint on the toolbar.
+ */
+ fun onTranslateExpected() = Unit
+
+ /**
+ * Event to indicate that the translations engine suggests notifying the user that
+ * translations are available or else offering to translate.
+ *
+ * The usual use case is to show a popup or UI notification that translations are available.
+ */
+ fun onTranslateOffer() = Unit
+
+ /**
+ * Event to indicate the translations state. Translations state change
+ * occurs generally during navigation and after translation operations are requested.
+ *
+ * @param state The translations state.
+ */
+ fun onTranslateStateChange(state: TranslationEngineState) = Unit
+
+ /**
+ * Event to indicate that the translation operation completed successfully.
+ *
+ * @param operation The operation that the translation engine completed.
+ */
+ fun onTranslateComplete(operation: TranslationOperation) = Unit
+
+ /**
+ * Event to indicate that the translation operation was unsuccessful.
+ *
+ * @param operation The operation that the translation engine attempted.
+ * @param translationError The exception that occurred during the operation.
+ */
+ fun onTranslateException(
+ operation: TranslationOperation,
+ translationError: TranslationError,
+ ) = Unit
+ }
+
+ /**
+ * Provides access to the settings of this engine session.
+ */
+ abstract val settings: Settings
+
+ /**
+ * Represents a safe browsing policy, which is indicates with type of site should be alerted
+ * to user as possible harmful.
+ */
+ @Suppress("MagicNumber")
+ enum class SafeBrowsingPolicy(val id: Int) {
+ NONE(0),
+
+ /**
+ * Blocks malware sites.
+ */
+ MALWARE(1 shl 10),
+
+ /**
+ * Blocks unwanted sites.
+ */
+ UNWANTED(1 shl 11),
+
+ /**
+ * Blocks harmful sites.
+ */
+ HARMFUL(1 shl 12),
+
+ /**
+ * Blocks phishing sites.
+ */
+ PHISHING(1 shl 13),
+
+ /**
+ * Blocks all unsafe sites.
+ */
+ RECOMMENDED(MALWARE.id + UNWANTED.id + HARMFUL.id + PHISHING.id),
+ }
+
+ /**
+ * Represents a tracking protection policy, which is a combination of
+ * tracker categories that should be blocked. Unless otherwise specified,
+ * a [TrackingProtectionPolicy] is applicable to all session types (see
+ * [TrackingProtectionPolicyForSessionTypes]).
+ */
+ open class TrackingProtectionPolicy internal constructor(
+ val trackingCategories: Array<TrackingCategory> = arrayOf(TrackingCategory.RECOMMENDED),
+ val useForPrivateSessions: Boolean = true,
+ val useForRegularSessions: Boolean = true,
+ val cookiePolicy: CookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
+ val cookiePolicyPrivateMode: CookiePolicy = cookiePolicy,
+ val strictSocialTrackingProtection: Boolean? = null,
+ val cookiePurging: Boolean = false,
+ ) {
+
+ /**
+ * Indicates how cookies should behave for a given [TrackingProtectionPolicy].
+ * The ids of each cookiePolicy is aligned with the GeckoView @CookieBehavior constants.
+ */
+ @Suppress("MagicNumber")
+ enum class CookiePolicy(val id: Int) {
+ /**
+ * Accept first-party and third-party cookies and site data.
+ */
+ ACCEPT_ALL(0),
+
+ /**
+ * Accept only first-party cookies and site data to block cookies which are
+ * not associated with the domain of the visited site.
+ */
+ ACCEPT_ONLY_FIRST_PARTY(1),
+
+ /**
+ * Do not store any cookies and site data.
+ */
+ ACCEPT_NONE(2),
+
+ /**
+ * Accept first-party and third-party cookies and site data only from
+ * sites previously visited in a first-party context.
+ */
+ ACCEPT_VISITED(3),
+
+ /**
+ * Accept only first-party and non-tracking third-party cookies and site data
+ * to block cookies which are not associated with the domain of the visited
+ * site set by known trackers.
+ */
+ ACCEPT_NON_TRACKERS(4),
+
+ /**
+ * Enable dynamic first party isolation (dFPI); this will block third-party tracking
+ * cookies in accordance with the ETP level and isolate non-tracking third-party
+ * cookies.
+ */
+ ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS(5),
+ }
+
+ @Suppress("MagicNumber")
+ enum class TrackingCategory(val id: Int) {
+
+ NONE(0),
+
+ /**
+ * Blocks advertisement trackers from the ads-track-digest256 list.
+ */
+ AD(1 shl 1),
+
+ /**
+ * Blocks analytics trackers from the analytics-track-digest256 list.
+ */
+ ANALYTICS(1 shl 2),
+
+ /**
+ * Blocks social trackers from the social-track-digest256 list.
+ */
+ SOCIAL(1 shl 3),
+
+ /**
+ * Blocks content trackers from the content-track-digest256 list.
+ * May cause issues with some web sites.
+ */
+ CONTENT(1 shl 4),
+
+ // This policy is just to align categories with GeckoView
+ TEST(1 shl 5),
+
+ /**
+ * Blocks cryptocurrency miners.
+ */
+ CRYPTOMINING(1 shl 6),
+
+ /**
+ * Blocks fingerprinting trackers.
+ */
+ FINGERPRINTING(1 shl 7),
+
+ /**
+ * Blocks social trackers from the social-tracking-protection-digest256 list.
+ */
+ MOZILLA_SOCIAL(1 shl 8),
+
+ /**
+ * Blocks email trackers.
+ */
+ EMAIL(1 shl 9),
+
+ /**
+ * Blocks content like scripts and sub-resources.
+ */
+ SCRIPTS_AND_SUB_RESOURCES(1 shl 31),
+
+ RECOMMENDED(
+ AD.id + ANALYTICS.id + SOCIAL.id + TEST.id + MOZILLA_SOCIAL.id +
+ CRYPTOMINING.id + FINGERPRINTING.id,
+ ),
+
+ /**
+ * Combining the [RECOMMENDED] categories plus [SCRIPTS_AND_SUB_RESOURCES] & getAntiTracking[EMAIL].
+ */
+ STRICT(RECOMMENDED.id + SCRIPTS_AND_SUB_RESOURCES.id + EMAIL.id),
+ }
+
+ companion object {
+ fun none() = TrackingProtectionPolicy(
+ trackingCategories = arrayOf(TrackingCategory.NONE),
+ cookiePolicy = ACCEPT_ALL,
+ )
+
+ /**
+ * Strict policy.
+ * Combining the [TrackingCategory.STRICT] plus a cookiePolicy of [ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS].
+ * This is the strictest setting and may cause issues on some web sites.
+ */
+ fun strict() = TrackingProtectionPolicyForSessionTypes(
+ trackingCategory = arrayOf(TrackingCategory.STRICT),
+ cookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
+ strictSocialTrackingProtection = true,
+ cookiePurging = true,
+ )
+
+ /**
+ * Recommended policy.
+ * Combining the [TrackingCategory.RECOMMENDED] plus a [CookiePolicy]
+ * of [ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS].
+ * This is the recommended setting.
+ */
+ fun recommended() = TrackingProtectionPolicyForSessionTypes(
+ trackingCategory = arrayOf(TrackingCategory.RECOMMENDED),
+ cookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
+ strictSocialTrackingProtection = false,
+ cookiePurging = true,
+ )
+
+ /**
+ * Creates a custom [TrackingProtectionPolicyForSessionTypes] using the provide values .
+ * @param trackingCategories a list of tracking categories to apply.
+ * @param cookiePolicy indicates how cookies should behave for this policy.
+ * @param cookiePolicyPrivateMode indicates how cookies should behave in private mode for this policy,
+ * default to [cookiePolicy] if not set.
+ * @param strictSocialTrackingProtection indicate if content should be blocked from the
+ * social-tracking-protection-digest256 list, when given a null value,
+ * it is only applied when the [EngineSession.TrackingProtectionPolicy.TrackingCategory.STRICT]
+ * is set.
+ * @param cookiePurging Whether or not to automatically purge tracking cookies. This will
+ * purge cookies from tracking sites that do not have recent user interaction provided.
+ */
+ fun select(
+ trackingCategories: Array<TrackingCategory> = arrayOf(TrackingCategory.RECOMMENDED),
+ cookiePolicy: CookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
+ cookiePolicyPrivateMode: CookiePolicy = cookiePolicy,
+ strictSocialTrackingProtection: Boolean? = null,
+ cookiePurging: Boolean = false,
+ ) = TrackingProtectionPolicyForSessionTypes(
+ trackingCategory = trackingCategories,
+ cookiePolicy = cookiePolicy,
+ cookiePolicyPrivateMode = cookiePolicyPrivateMode,
+ strictSocialTrackingProtection = strictSocialTrackingProtection,
+ cookiePurging = cookiePurging,
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TrackingProtectionPolicy) return false
+ if (hashCode() != other.hashCode()) return false
+ if (useForPrivateSessions != other.useForPrivateSessions) return false
+ if (useForRegularSessions != other.useForRegularSessions) return false
+ if (cookiePurging != other.cookiePurging) return false
+ if (cookiePolicyPrivateMode != other.cookiePolicyPrivateMode) return false
+ if (strictSocialTrackingProtection != other.strictSocialTrackingProtection) return false
+ return true
+ }
+
+ override fun hashCode() = trackingCategories.sumOf { it.id } + cookiePolicy.id
+
+ fun contains(category: TrackingCategory) =
+ (trackingCategories.sumOf { it.id } and category.id) != 0
+ }
+
+ /**
+ * Represents settings options for cookie banner handling.
+ */
+ @Suppress("MagicNumber")
+ enum class CookieBannerHandlingMode(val mode: Int) {
+ /**
+ * The feature is turned off and cookie banners are not handled
+ */
+ DISABLED(0),
+
+ /**
+ * Reject cookies if possible
+ */
+ REJECT_ALL(1),
+
+ /**
+ * Reject cookies if possible. If rejecting is not possible, accept cookies
+ */
+ REJECT_OR_ACCEPT_ALL(2),
+ }
+
+ /**
+ * Represents a status for cookie banner handling.
+ */
+ enum class CookieBannerHandlingStatus {
+ /**
+ * Indicates a cookie banner was detected.
+ */
+ DETECTED,
+
+ /**
+ * Indicates a cookie banner was handled.
+ */
+ HANDLED,
+
+ /**
+ * Indicates a cookie banner has not been detected yet.
+ */
+ NO_DETECTED,
+ }
+
+ /**
+ * Subtype of [TrackingProtectionPolicy] to control the type of session this policy
+ * should be applied to. By default, a policy will be applied to all sessions.
+ * @param trackingCategory a list of tracking categories to apply.
+ * @param cookiePolicy indicates how cookies should behave for this policy.
+ * @param cookiePolicyPrivateMode indicates how cookies should behave in private mode for this policy,
+ * default to [cookiePolicy] if not set.
+ * @param strictSocialTrackingProtection indicate if content should be blocked from the
+ * social-tracking-protection-digest256 list, when given a null value,
+ * it is only applied when the [EngineSession.TrackingProtectionPolicy.TrackingCategory.STRICT]
+ * is set.
+ * @param cookiePurging Whether or not to automatically purge tracking cookies. This will
+ * purge cookies from tracking sites that do not have recent user interaction provided.
+ */
+ class TrackingProtectionPolicyForSessionTypes internal constructor(
+ trackingCategory: Array<TrackingCategory> = arrayOf(TrackingCategory.RECOMMENDED),
+ cookiePolicy: CookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
+ cookiePolicyPrivateMode: CookiePolicy = cookiePolicy,
+ strictSocialTrackingProtection: Boolean? = null,
+ cookiePurging: Boolean = false,
+ ) : TrackingProtectionPolicy(
+ trackingCategories = trackingCategory,
+ cookiePolicy = cookiePolicy,
+ cookiePolicyPrivateMode = cookiePolicyPrivateMode,
+ strictSocialTrackingProtection = strictSocialTrackingProtection,
+ cookiePurging = cookiePurging,
+ ) {
+ /**
+ * Marks this policy to be used for private sessions only.
+ */
+ fun forPrivateSessionsOnly() = TrackingProtectionPolicy(
+ trackingCategories = trackingCategories,
+ useForPrivateSessions = true,
+ useForRegularSessions = false,
+ cookiePolicy = cookiePolicy,
+ cookiePolicyPrivateMode = cookiePolicyPrivateMode,
+ strictSocialTrackingProtection = false,
+ cookiePurging = cookiePurging,
+ )
+
+ /**
+ * Marks this policy to be used for regular (non-private) sessions only.
+ */
+ fun forRegularSessionsOnly() = TrackingProtectionPolicy(
+ trackingCategories = trackingCategories,
+ useForPrivateSessions = false,
+ useForRegularSessions = true,
+ cookiePolicy = cookiePolicy,
+ cookiePolicyPrivateMode = cookiePolicyPrivateMode,
+ strictSocialTrackingProtection = strictSocialTrackingProtection,
+ cookiePurging = cookiePurging,
+ )
+ }
+
+ /**
+ * Describes a combination of flags provided to the engine when loading a URL.
+ */
+ class LoadUrlFlags internal constructor(val value: Int) {
+ companion object {
+ const val NONE: Int = 0
+ const val BYPASS_CACHE: Int = 1 shl 0
+ const val BYPASS_PROXY: Int = 1 shl 1
+ const val EXTERNAL: Int = 1 shl 2
+ const val ALLOW_POPUPS: Int = 1 shl 3
+ const val BYPASS_CLASSIFIER: Int = 1 shl 4
+ const val LOAD_FLAGS_FORCE_ALLOW_DATA_URI: Int = 1 shl 5
+ const val LOAD_FLAGS_REPLACE_HISTORY: Int = 1 shl 6
+ const val LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE: Int = 1 shl 7
+ const val ALLOW_ADDITIONAL_HEADERS: Int = 1 shl 15
+ const val ALLOW_JAVASCRIPT_URL: Int = 1 shl 16
+ internal const val ALL = BYPASS_CACHE + BYPASS_PROXY + EXTERNAL + ALLOW_POPUPS +
+ BYPASS_CLASSIFIER + LOAD_FLAGS_FORCE_ALLOW_DATA_URI + LOAD_FLAGS_REPLACE_HISTORY +
+ LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE + ALLOW_ADDITIONAL_HEADERS + ALLOW_JAVASCRIPT_URL
+
+ fun all() = LoadUrlFlags(ALL)
+ fun none() = LoadUrlFlags(NONE)
+ fun external() = LoadUrlFlags(EXTERNAL)
+ fun select(vararg types: Int) = LoadUrlFlags(types.sum())
+ }
+
+ fun contains(flag: Int) = (value and flag) != 0
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is LoadUrlFlags) return false
+ if (value != other.value) return false
+ return true
+ }
+
+ override fun hashCode() = value
+ }
+
+ /**
+ * Represents a session priority, which signals to the engine that it should give
+ * a different prioritization to a given session.
+ */
+ @Suppress("MagicNumber")
+ enum class SessionPriority(val id: Int) {
+ /**
+ * Signals to the engine that this session has a default priority.
+ */
+ DEFAULT(0),
+
+ /**
+ * Signals to the engine that this session is important, and the Engine should keep
+ * the session alive for as long as possible.
+ */
+ HIGH(1),
+ }
+
+ /**
+ * Loads the given URL.
+ *
+ * @param url the url to load.
+ * @param parent the parent (referring) [EngineSession] i.e. the session that
+ * triggered creating this one.
+ * @param flags the [LoadUrlFlags] to use when loading the provided url.
+ * @param additionalHeaders the extra headers to use when loading the provided url.
+ */
+ abstract fun loadUrl(
+ url: String,
+ parent: EngineSession? = null,
+ flags: LoadUrlFlags = LoadUrlFlags.none(),
+ additionalHeaders: Map<String, String>? = null,
+ )
+
+ /**
+ * Loads the data with the given mimeType.
+ * Example:
+ * ```
+ * engineSession.loadData("<html><body>Example HTML content here</body></html>", "text/html")
+ * ```
+ *
+ * If the data is base64 encoded, you can override the default encoding (UTF-8) with 'base64'.
+ * Example:
+ * ```
+ * engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", "text/plain", "base64")
+ * ```
+ *
+ * @param data The data that should be rendering.
+ * @param mimeType the data type needed by the engine to know how to render it.
+ * @param encoding specifies whether the data is base64 encoded; use 'base64' else defaults to "UTF-8".
+ */
+ abstract fun loadData(data: String, mimeType: String = "text/html", encoding: String = "UTF-8")
+
+ /**
+ * Requests the [EngineSession] to download the current session's contents as a PDF.
+ *
+ * A typical implementation would have the same flow that feeds into [EngineSession.Observer.onExternalResource].
+ */
+ abstract fun requestPdfToDownload()
+
+ /**
+ * Requests the [EngineSession] to print the current session's contents.
+ *
+ * This will open the Android Print Spooler.
+ */
+ abstract fun requestPrintContent()
+
+ /**
+ * Stops loading the current session.
+ */
+ abstract fun stopLoading()
+
+ /**
+ * Reloads the current URL.
+ *
+ * @param flags the [LoadUrlFlags] to use when reloading the current url.
+ */
+ abstract fun reload(flags: LoadUrlFlags = LoadUrlFlags.none())
+
+ /**
+ * Navigates back in the history of this session.
+ *
+ * @param userInteraction informs the engine whether the action was user invoked.
+ */
+ abstract fun goBack(userInteraction: Boolean = true)
+
+ /**
+ * Navigates forward in the history of this session.
+ *
+ * @param userInteraction informs the engine whether the action was user invoked.
+ */
+ abstract fun goForward(userInteraction: Boolean = true)
+
+ /**
+ * Navigates to the specified index in the [HistoryState] of this session. The current index of
+ * this session's [HistoryState] will be updated but the items within it will be unchanged.
+ * Invalid index values are ignored.
+ *
+ * @param index the index of the session's [HistoryState] to navigate to
+ */
+ abstract fun goToHistoryIndex(index: Int)
+
+ /**
+ * Restore a saved state; only data that is saved (history, scroll position, zoom, and form data)
+ * will be restored.
+ *
+ * @param state A saved session state.
+ * @return true if the engine session has successfully been restored with the provided state,
+ * false otherwise.
+ */
+ abstract fun restoreState(state: EngineSessionState): Boolean
+
+ /**
+ * Updates the tracking protection [policy] for this engine session.
+ * If you want to disable tracking protection use [TrackingProtectionPolicy.none].
+ *
+ * @param policy the tracking protection policy to use, defaults to blocking all trackers.
+ */
+ abstract fun updateTrackingProtection(policy: TrackingProtectionPolicy = TrackingProtectionPolicy.strict())
+
+ /**
+ * Enables/disables Desktop Mode with an optional ability to reload the session right after.
+ */
+ abstract fun toggleDesktopMode(enable: Boolean, reload: Boolean = false)
+
+ /**
+ * Checks if there is a rule for handling a cookie banner for the current website in the session.
+ *
+ * @param onSuccess callback invoked if the engine API returned a valid response. Please note
+ * that the response can be null - which can indicate a bug, a miscommunication
+ * or other unexpected failure.
+ * @param onError callback invoked if there was an error getting the response.
+ */
+ abstract fun hasCookieBannerRuleForSession(onResult: (Boolean) -> Unit, onException: (Throwable) -> Unit)
+
+ /**
+ * Checks if the current session is using a PDF viewer.
+ *
+ * @param onSuccess callback invoked if the engine API returned a valid response. Please note
+ * that the response can be null - which can indicate a bug, a miscommunication
+ * or other unexpected failure.
+ * @param onError callback invoked if there was an error getting the response.
+ */
+ abstract fun checkForPdfViewer(onResult: (Boolean) -> Unit, onException: (Throwable) -> Unit)
+
+ /**
+ * Requests product recommendations given a specific product url.
+ *
+ * @param onResult callback invoked if the engine API returned a valid response. Please note
+ * that the response can be null - which can indicate a bug, a miscommunication
+ * or other unexpected failure.
+ * @param onException callback invoked if there was an error getting the response.
+ */
+ abstract fun requestProductRecommendations(
+ url: String,
+ onResult: (List<ProductRecommendation>) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Requests the analysis results for a given product page URL.
+ *
+ * @param onResult callback invoked if the engine API returns a valid response.
+ * @param onException callback invoked if there was an error getting the response.
+ */
+ abstract fun requestProductAnalysis(
+ url: String,
+ onResult: (ProductAnalysis) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Requests the reanalysis of a product for a given product page URL.
+ *
+ * @param onResult callback invoked if the engine API returns a valid response.
+ * @param onException callback invoked if there was an error getting the response.
+ */
+ abstract fun reanalyzeProduct(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Requests the status of a product analysis for a given product page URL.
+ *
+ * @param onResult callback invoked if the engine API returns a valid response.
+ * @param onException callback invoked if there was an error getting the response.
+ */
+ abstract fun requestAnalysisStatus(
+ url: String,
+ onResult: (ProductAnalysisStatus) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Sends a click attribution event for a given product aid.
+ *
+ * @param onResult callback invoked if the engine API returns a valid response.
+ * @param onException callback invoked if there was an error getting the response.
+ */
+ abstract fun sendClickAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Sends an impression attribution event for a given product aid.
+ *
+ * @param onResult callback invoked if the engine API returns a valid response.
+ * @param onException callback invoked if there was an error getting the response.
+ */
+ abstract fun sendImpressionAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Sends a placement attribution event for a given product aid.
+ *
+ * @param onResult callback invoked if the engine API returns a valid response.
+ * @param onException callback invoked if there was an error getting the response.
+ */
+ abstract fun sendPlacementAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Reports when a product is back in stock.
+ *
+ * @param onResult callback invoked if the engine API returns a valid response.
+ * @param onException callback invoked if there was an error getting the response.
+ */
+ abstract fun reportBackInStock(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Requests the [EngineSession] to translate the current session's contents.
+ *
+ * @param fromLanguage The BCP 47 language tag that the page should be translated from.
+ * @param toLanguage The BCP 47 language tag that the page should be translated to.
+ * @param options Options for how the translation should be processed.
+ */
+ abstract fun requestTranslate(
+ fromLanguage: String,
+ toLanguage: String,
+ options: TranslationOptions?,
+ )
+
+ /**
+ * Requests the [EngineSession] to restore the current session's contents.
+ * Will be a no-op on the Gecko side if the page is not translated.
+ */
+ abstract fun requestTranslationRestore()
+
+ /**
+ * Requests the [EngineSession] retrieve the current site's never translate preference.
+ */
+ abstract fun getNeverTranslateSiteSetting(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Requests the [EngineSession] to set the current site's never translate preference.
+ *
+ * @param setting True if the site should never be translated. False if the site should be
+ * translated.
+ */
+ abstract fun setNeverTranslateSiteSetting(
+ setting: Boolean,
+ onResult: () -> Unit,
+ onException: (Throwable) -> Unit,
+ )
+
+ /**
+ * Finds and highlights all occurrences of the provided String and highlights them asynchronously.
+ *
+ * @param text the String to search for
+ */
+ abstract fun findAll(text: String)
+
+ /**
+ * Finds and highlights the next or previous match found by [findAll].
+ *
+ * @param forward true if the next match should be highlighted, false for
+ * the previous match.
+ */
+ abstract fun findNext(forward: Boolean)
+
+ /**
+ * Clears the highlighted results of previous calls to [findAll] / [findNext].
+ */
+ abstract fun clearFindMatches()
+
+ /**
+ * Exits fullscreen mode if currently in it that state.
+ */
+ abstract fun exitFullScreenMode()
+
+ /**
+ * Marks this session active/inactive for web extensions to support
+ * tabs.query({active: true}).
+ *
+ * @param active whether this session should be marked as active or inactive.
+ */
+ open fun markActiveForWebExtensions(active: Boolean) = Unit
+
+ /**
+ * Updates the priority for this session.
+ *
+ * @param priority the new priority for this session.
+ */
+ open fun updateSessionPriority(priority: SessionPriority) = Unit
+
+ /**
+ * Checks this session for existing user form data.
+ */
+ open fun checkForFormData() = Unit
+
+ /**
+ * Purges the history for the session (back and forward history).
+ */
+ abstract fun purgeHistory()
+
+ /**
+ * Close the session. This may free underlying objects. Call this when you are finished using
+ * this session.
+ */
+ @CallSuper
+ open fun close() = delegate.unregisterObservers()
+
+ /**
+ * Returns the list of URL schemes that are blocked from loading.
+ */
+ open fun getBlockedSchemes(): List<String> = emptyList()
+
+ /**
+ * Set the display member in Web App Manifest for this session.
+ *
+ * @param displayMode the display mode value for this session.
+ */
+ open fun setDisplayMode(displayMode: WebAppManifest.DisplayMode) = Unit
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSessionState.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSessionState.kt
new file mode 100644
index 0000000000..39cf4fca63
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSessionState.kt
@@ -0,0 +1,51 @@
+/* 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.concept.engine
+
+import android.util.JsonWriter
+
+/**
+ * The state of an [EngineSession]. An instance can be obtained from [EngineSession.saveState]. Creating a new
+ * [EngineSession] and calling [EngineSession.restoreState] with the same state instance should restore the previous
+ * session.
+ */
+interface EngineSessionState {
+ /**
+ * Writes this state as JSON to the given [JsonWriter].
+ *
+ * When reading JSON from disk [Engine.createSessionState] can be used to turn it back into an [EngineSessionState]
+ * instance.
+ */
+ fun writeTo(writer: JsonWriter)
+}
+
+/**
+ * An interface describing a storage layer for an [EngineSessionState].
+ */
+interface EngineSessionStateStorage {
+ /**
+ * Writes a [state] with a provided [uuid] as its identifier.
+ *
+ * @return A boolean flag indicating if the write was a success.
+ */
+ suspend fun write(uuid: String, state: EngineSessionState): Boolean
+
+ /**
+ * Reads an [EngineSessionState] given a provided [uuid] as its identifier.
+ *
+ * @return A [EngineSessionState] if one is present for the given [uuid], `null` otherwise.
+ */
+ suspend fun read(uuid: String): EngineSessionState?
+
+ /**
+ * Deletes persisted [EngineSessionState] for a given [uuid].
+ */
+ suspend fun delete(uuid: String)
+
+ /**
+ * Deletes all persisted [EngineSessionState] instances.
+ */
+ suspend fun deleteAll()
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineView.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineView.kt
new file mode 100644
index 0000000000..e217f511d8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineView.kt
@@ -0,0 +1,201 @@
+/* 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.concept.engine
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.view.View
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+
+/**
+ * View component that renders web content.
+ */
+interface EngineView {
+
+ /**
+ * Convenience method to cast the implementation of this interface to an Android View object.
+ */
+ fun asView(): View = this as View
+
+ /**
+ * Render the content of the given session.
+ */
+ fun render(session: EngineSession)
+
+ /**
+ * Releases an [EngineSession] that is currently rendered by this view (after calling [render]).
+ *
+ * Usually an app does not need to call this itself since [EngineView] will take care of that if it gets detached.
+ * However there are situations where an app wants to hand-off rendering of an [EngineSession] to a different
+ * [EngineView] without the current [EngineView] getting detached immediately.
+ */
+ fun release()
+
+ /**
+ * To be called in response to [Lifecycle.Event.ON_RESUME]. See [EngineView]
+ * implementations for details.
+ */
+ fun onResume() = Unit
+
+ /**
+ * To be called in response to [Lifecycle.Event.ON_PAUSE]. See [EngineView]
+ * implementations for details.
+ */
+ fun onPause() = Unit
+
+ /**
+ * To be called in response to [Lifecycle.Event.ON_START]. See [EngineView]
+ * implementations for details.
+ */
+ fun onStart() = Unit
+
+ /**
+ * To be called in response to [Lifecycle.Event.ON_STOP]. See [EngineView]
+ * implementations for details.
+ */
+ fun onStop() = Unit
+
+ /**
+ * To be called in response to [Lifecycle.Event.ON_CREATE]. See [EngineView]
+ * implementations for details.
+ */
+ fun onCreate() = Unit
+
+ /**
+ * To be called in response to [Lifecycle.Event.ON_DESTROY]. See [EngineView]
+ * implementations for details.
+ */
+ fun onDestroy() = Unit
+
+ /**
+ * Check if [EngineView] can clear the selection.
+ * true if can and false otherwise.
+ */
+ fun canClearSelection(): Boolean = false
+
+ /**
+ * Check if [EngineView] can be scrolled vertically up.
+ * true if can and false otherwise.
+ */
+ fun canScrollVerticallyUp(): Boolean = true
+
+ /**
+ * Check if [EngineView] can be scrolled vertically down.
+ * true if can and false otherwise.
+ */
+ fun canScrollVerticallyDown(): Boolean = true
+
+ /**
+ * @return [InputResult] indicating how user's last [android.view.MotionEvent] was handled.
+ */
+ @Deprecated("Not enough data about how the touch was handled", ReplaceWith("getInputResultDetail()"))
+ @Suppress("DEPRECATION")
+ fun getInputResult(): InputResult = InputResult.INPUT_RESULT_UNHANDLED
+
+ /**
+ * @return [InputResultDetail] indicating how user's last [android.view.MotionEvent] was handled.
+ */
+ fun getInputResultDetail(): InputResultDetail = InputResultDetail.newInstance()
+
+ /**
+ * Request a screenshot of the visible portion of the web page currently being rendered.
+ * @param onFinish A callback to inform that process of capturing a
+ * thumbnail has finished. Important for engine-gecko: Make sure not to reference the
+ * context or view in this callback to prevent memory leaks:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1678364
+ */
+ fun captureThumbnail(onFinish: (Bitmap?) -> Unit)
+
+ /**
+ * Clears the current selection if possible.
+ */
+ fun clearSelection() = Unit
+
+ /**
+ * Updates the amount of vertical space that is clipped or visibly obscured in the bottom portion of the view.
+ * Tells the [EngineView] where to put bottom fixed elements so they are fully visible.
+ *
+ * @param clippingHeight The height of the bottom clipped space in screen pixels.
+ */
+ fun setVerticalClipping(clippingHeight: Int)
+
+ /**
+ * Sets the maximum height of the dynamic toolbar(s).
+ *
+ * @param height The maximum possible height of the toolbar.
+ */
+ fun setDynamicToolbarMaxHeight(height: Int)
+
+ /**
+ * Sets the Activity context for GeckoView.
+ *
+ * @param context The Activity context.
+ */
+ fun setActivityContext(context: Context?)
+
+ /**
+ * A delegate that will handle interactions with text selection context menus.
+ */
+ var selectionActionDelegate: SelectionActionDelegate?
+
+ /**
+ * Enumeration of all possible ways user's [android.view.MotionEvent] was handled.
+ *
+ * @see [INPUT_RESULT_UNHANDLED]
+ * @see [INPUT_RESULT_HANDLED]
+ * @see [INPUT_RESULT_HANDLED_CONTENT]
+ */
+ @Deprecated("Not enough data about how the touch was handled", ReplaceWith("InputResultDetail"))
+ @Suppress("DEPRECATION")
+ enum class InputResult(val value: Int) {
+ /**
+ * Last [android.view.MotionEvent] was not handled by neither us nor the webpage.
+ */
+ INPUT_RESULT_UNHANDLED(0),
+
+ /**
+ * We handled the last [android.view.MotionEvent].
+ */
+ INPUT_RESULT_HANDLED(1),
+
+ /**
+ * Webpage handled the last [android.view.MotionEvent].
+ * (through it's own touch event listeners)
+ */
+ INPUT_RESULT_HANDLED_CONTENT(2),
+ }
+}
+
+/**
+ * [LifecycleObserver] which dispatches lifecycle events to an [EngineView].
+ */
+class LifecycleObserver(val engineView: EngineView) : DefaultLifecycleObserver {
+
+ override fun onPause(owner: LifecycleOwner) {
+ engineView.onPause()
+ }
+ override fun onResume(owner: LifecycleOwner) {
+ engineView.onResume()
+ }
+
+ override fun onStart(owner: LifecycleOwner) {
+ engineView.onStart()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ engineView.onStop()
+ }
+
+ override fun onCreate(owner: LifecycleOwner) {
+ engineView.onCreate()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ engineView.onDestroy()
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/HitResult.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/HitResult.kt
new file mode 100644
index 0000000000..ff9637e0f3
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/HitResult.kt
@@ -0,0 +1,52 @@
+/* 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.concept.engine
+
+/**
+ * Represents all the different supported types of data that can be found from long clicking
+ * an element.
+ */
+@Suppress("ClassNaming", "ClassName")
+sealed class HitResult(open val src: String) {
+ /**
+ * Default type if we're unable to match the type to anything. It may or may not have a src.
+ */
+ data class UNKNOWN(override val src: String) : HitResult(src)
+
+ /**
+ * If the HTML element was of type 'HTMLImageElement'.
+ */
+ data class IMAGE(override val src: String, val title: String? = null) : HitResult(src)
+
+ /**
+ * If the HTML element was of type 'HTMLVideoElement'.
+ */
+ data class VIDEO(override val src: String, val title: String? = null) : HitResult(src)
+
+ /**
+ * If the HTML element was of type 'HTMLAudioElement'.
+ */
+ data class AUDIO(override val src: String, val title: String? = null) : HitResult(src)
+
+ /**
+ * If the HTML element was of type 'HTMLImageElement' and contained a URI.
+ */
+ data class IMAGE_SRC(override val src: String, val uri: String) : HitResult(src)
+
+ /**
+ * The type used if the URI is prepended with 'tel:'.
+ */
+ data class PHONE(override val src: String) : HitResult(src)
+
+ /**
+ * The type used if the URI is prepended with 'mailto:'.
+ */
+ data class EMAIL(override val src: String) : HitResult(src)
+
+ /**
+ * The type used if the URI is prepended with 'geo:'.
+ */
+ data class GEO(override val src: String) : HitResult(src)
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/InputResultDetail.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/InputResultDetail.kt
new file mode 100644
index 0000000000..e56d59bee8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/InputResultDetail.kt
@@ -0,0 +1,378 @@
+/* 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.concept.engine
+
+import android.view.MotionEvent
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Don't yet have a response from the browser about how the touch was handled.
+ */
+const val INPUT_HANDLING_UNKNOWN = -1
+
+// The below top-level values are following the same from [org.mozilla.geckoview.PanZoomController]
+/**
+ * The content has no scrollable element.
+ *
+ * @see [InputResultDetail.isTouchUnhandled]
+ */
+const val INPUT_UNHANDLED = 0
+
+/**
+ * The touch event is consumed by the [EngineView]
+ *
+ * @see [InputResultDetail.isTouchHandledByBrowser]
+ */
+const val INPUT_HANDLED = 1
+
+/**
+ * The touch event is consumed by the website through it's own touch listeners.
+ *
+ * @see [InputResultDetail.isTouchHandledByWebsite]
+ */
+const val INPUT_HANDLED_CONTENT = 2
+
+/**
+ * The website content is not scrollable.
+ */
+@VisibleForTesting
+internal const val SCROLL_DIRECTIONS_NONE = 0
+
+/**
+ * The website content can be scrolled to the top.
+ *
+ * @see [InputResultDetail.canScrollToTop]
+ */
+@VisibleForTesting
+internal const val SCROLL_DIRECTIONS_TOP = 1 shl 0
+
+/**
+ * The website content can be scrolled to the right.
+ *
+ * @see [InputResultDetail.canScrollToRight]
+ */
+@VisibleForTesting
+internal const val SCROLL_DIRECTIONS_RIGHT = 1 shl 1
+
+/**
+ * The website content can be scrolled to the bottom.
+ *
+ * @see [InputResultDetail.canScrollToBottom]
+ */
+@VisibleForTesting
+internal const val SCROLL_DIRECTIONS_BOTTOM = 1 shl 2
+
+/**
+ * The website content can be scrolled to the left.
+ *
+ * @see [InputResultDetail.canScrollToLeft]
+ */
+@VisibleForTesting
+internal const val SCROLL_DIRECTIONS_LEFT = 1 shl 3
+
+/**
+ * The website content cannot be overscrolled.
+ */
+@VisibleForTesting
+internal const val OVERSCROLL_DIRECTIONS_NONE = 0
+
+/**
+ * The website content can be overscrolled horizontally.
+ *
+ * @see [InputResultDetail.canOverscrollRight]
+ * @see [InputResultDetail.canOverscrollLeft]
+ */
+@VisibleForTesting
+internal const val OVERSCROLL_DIRECTIONS_HORIZONTAL = 1 shl 0
+
+/**
+ * The website content can be overscrolled vertically.
+ *
+ * @see [InputResultDetail.canOverscrollTop]
+ * @see [InputResultDetail.canOverscrollBottom]
+ */
+@VisibleForTesting
+internal const val OVERSCROLL_DIRECTIONS_VERTICAL = 1 shl 1
+
+/**
+ * All data about how a touch will be handled by the browser.
+ * - whether the event is used for panning/zooming by the browser / by the website or will be ignored.
+ * - whether the event can scroll the page and in what direction.
+ * - whether the event can overscroll the page and in what direction.
+ *
+ * @param inputResult Indicates who will use the current [MotionEvent].
+ * Possible values: [[INPUT_HANDLING_UNKNOWN], [INPUT_UNHANDLED], [INPUT_HANDLED], [INPUT_HANDLED_CONTENT]].
+ *
+ * @param scrollDirections Bitwise ORed value of the directions the page can be scrolled to.
+ * This is the same as GeckoView's [org.mozilla.geckoview.PanZoomController.ScrollableDirections].
+ *
+ * @param overscrollDirections Bitwise ORed value of the directions the page can be overscrolled to.
+ * This is the same as GeckoView's [org.mozilla.geckoview.PanZoomController.OverscrollDirections].
+ */
+@Suppress("TooManyFunctions")
+class InputResultDetail private constructor(
+ val inputResult: Int = INPUT_HANDLING_UNKNOWN,
+ val scrollDirections: Int = SCROLL_DIRECTIONS_NONE,
+ val overscrollDirections: Int = OVERSCROLL_DIRECTIONS_NONE,
+) {
+
+ override fun equals(other: Any?): Boolean {
+ return if (this !== other) {
+ if (other is InputResultDetail) {
+ return inputResult == other.inputResult &&
+ scrollDirections == other.scrollDirections &&
+ overscrollDirections == other.overscrollDirections
+ } else {
+ false
+ }
+ } else {
+ true
+ }
+ }
+
+ @Suppress("MagicNumber")
+ override fun hashCode(): Int {
+ var hash = inputResult.hashCode()
+ hash += (scrollDirections.hashCode()) * 10
+ hash += (overscrollDirections.hashCode()) * 100
+
+ return hash
+ }
+
+ override fun toString(): String {
+ return StringBuilder("InputResultDetail \$${hashCode()} (")
+ .append("Input ${getInputResultHandledDescription()}. ")
+ .append("Content ${getScrollDirectionsDescription()} and ${getOverscrollDirectionsDescription()}")
+ .append(')')
+ .toString()
+ }
+
+ /**
+ * Create a new instance of [InputResultDetail] with the option of keep some of the current values.
+ *
+ * The provided new values will be filtered out if not recognized and could corrupt the current state.
+ */
+ fun copy(
+ inputResult: Int? = this.inputResult,
+ scrollDirections: Int? = this.scrollDirections,
+ overscrollDirections: Int? = this.overscrollDirections,
+ ): InputResultDetail {
+ // Ensure this data will not get corrupted by users sending unknown arguments
+
+ val newValidInputResult = if (inputResult in INPUT_UNHANDLED..INPUT_HANDLED_CONTENT) {
+ inputResult
+ } else {
+ this.inputResult
+ }
+ val newValidScrollDirections = if (scrollDirections in
+ SCROLL_DIRECTIONS_NONE..(SCROLL_DIRECTIONS_LEFT or (SCROLL_DIRECTIONS_LEFT - 1))
+ ) {
+ scrollDirections
+ } else {
+ this.scrollDirections
+ }
+ val newValidOverscrollDirections = if (overscrollDirections in
+ OVERSCROLL_DIRECTIONS_NONE..(OVERSCROLL_DIRECTIONS_VERTICAL or (OVERSCROLL_DIRECTIONS_VERTICAL - 1))
+ ) {
+ overscrollDirections
+ } else {
+ this.overscrollDirections
+ }
+
+ // The range check automatically checks for null but doesn't yet have a contract to say so.
+ // As such it it safe to use the not-null assertion operator.
+ return InputResultDetail(newValidInputResult!!, newValidScrollDirections!!, newValidOverscrollDirections!!)
+ }
+
+ /**
+ * The [EngineView] has not yet responded on how it handled the [MotionEvent].
+ */
+ fun isTouchHandlingUnknown() = inputResult == INPUT_HANDLING_UNKNOWN
+
+ /**
+ * The [EngineView] handled the last [MotionEvent] to pan or zoom the content.
+ */
+ fun isTouchHandledByBrowser() = inputResult == INPUT_HANDLED
+
+ /**
+ * The website handled the last [MotionEvent] through it's own touch listeners
+ * and consumed it without the [EngineView] panning or zooming the website
+ */
+ fun isTouchHandledByWebsite() = inputResult == INPUT_HANDLED_CONTENT
+
+ /**
+ * Neither the [EngineView], nor the website will handle this [MotionEvent].
+ *
+ * This might happen on a website without touch listeners that is not bigger than the screen
+ * or when the content has no scrollable element.
+ */
+ fun isTouchUnhandled() = inputResult == INPUT_UNHANDLED
+
+ /**
+ * Whether the width of the webpage exceeds the display and the webpage can be scrolled to left.
+ */
+ fun canScrollToLeft(): Boolean =
+ inputResult == INPUT_HANDLED &&
+ scrollDirections and SCROLL_DIRECTIONS_LEFT != 0
+
+ /**
+ * Whether the height of the webpage exceeds the display and the webpage can be scrolled to top.
+ */
+ fun canScrollToTop(): Boolean =
+ inputResult == INPUT_HANDLED &&
+ scrollDirections and SCROLL_DIRECTIONS_TOP != 0
+
+ /**
+ * Whether the width of the webpage exceeds the display and the webpage can be scrolled to right.
+ */
+ fun canScrollToRight(): Boolean =
+ inputResult == INPUT_HANDLED &&
+ scrollDirections and SCROLL_DIRECTIONS_RIGHT != 0
+
+ /**
+ * Whether the height of the webpage exceeds the display and the webpage can be scrolled to bottom.
+ */
+ fun canScrollToBottom(): Boolean =
+ inputResult == INPUT_HANDLED &&
+ scrollDirections and SCROLL_DIRECTIONS_BOTTOM != 0
+
+ /**
+ * Whether the webpage can be overscrolled to the left.
+ *
+ * @return `true` if the page is already scrolled to the left most part
+ * and the touch event is not handled by the webpage.
+ */
+ fun canOverscrollLeft(): Boolean =
+ inputResult != INPUT_HANDLED_CONTENT &&
+ (scrollDirections and SCROLL_DIRECTIONS_LEFT == 0) &&
+ (overscrollDirections and OVERSCROLL_DIRECTIONS_HORIZONTAL != 0)
+
+ /**
+ * Whether the webpage can be overscrolled to the top.
+ *
+ * @return `true` if the page is already scrolled to the top most part
+ * and the touch event is not handled by the webpage.
+ */
+ fun canOverscrollTop(): Boolean =
+ inputResult != INPUT_HANDLED_CONTENT &&
+ (scrollDirections and SCROLL_DIRECTIONS_TOP == 0) &&
+ (overscrollDirections and OVERSCROLL_DIRECTIONS_VERTICAL != 0)
+
+ /**
+ * Whether the webpage can be overscrolled to the right.
+ *
+ * @return `true` if the page is already scrolled to the right most part
+ * and the touch event is not handled by the webpage.
+ */
+ fun canOverscrollRight(): Boolean =
+ inputResult != INPUT_HANDLED_CONTENT &&
+ (scrollDirections and SCROLL_DIRECTIONS_RIGHT == 0) &&
+ (overscrollDirections and OVERSCROLL_DIRECTIONS_HORIZONTAL != 0)
+
+ /**
+ * Whether the webpage can be overscrolled to the bottom.
+ *
+ * @return `true` if the page is already scrolled to the bottom most part
+ * and the touch event is not handled by the webpage.
+ */
+ fun canOverscrollBottom(): Boolean =
+ inputResult != INPUT_HANDLED_CONTENT &&
+ (scrollDirections and SCROLL_DIRECTIONS_BOTTOM == 0) &&
+ (overscrollDirections and OVERSCROLL_DIRECTIONS_VERTICAL != 0)
+
+ @VisibleForTesting
+ internal fun getInputResultHandledDescription() = when (inputResult) {
+ INPUT_HANDLING_UNKNOWN -> INPUT_UNKNOWN_HANDLING_DESCRIPTION
+ INPUT_HANDLED -> INPUT_HANDLED_TOSTRING_DESCRIPTION
+ INPUT_HANDLED_CONTENT -> INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION
+ else -> INPUT_UNHANDLED_TOSTRING_DESCRIPTION
+ }
+
+ @VisibleForTesting
+ internal fun getScrollDirectionsDescription(): String {
+ if (scrollDirections == SCROLL_DIRECTIONS_NONE) {
+ return SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
+ }
+
+ val scrollDirections = StringBuilder()
+ .append(if (canScrollToLeft()) "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
+ .append(if (canScrollToTop()) "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
+ .append(if (canScrollToRight()) "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
+ .append(if (canScrollToBottom()) SCROLL_BOTTOM_TOSTRING_DESCRIPTION else "")
+ .removeSuffix(TOSTRING_SEPARATOR)
+ .toString()
+
+ return if (scrollDirections.trim().isEmpty()) {
+ SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
+ } else {
+ SCROLL_TOSTRING_DESCRIPTION + scrollDirections
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getOverscrollDirectionsDescription(): String {
+ if (overscrollDirections == OVERSCROLL_DIRECTIONS_NONE) {
+ return OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
+ }
+
+ val overscrollDirections = StringBuilder()
+ .append(if (canOverscrollLeft()) "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
+ .append(if (canOverscrollTop()) "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
+ .append(if (canOverscrollRight()) "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "")
+ .append(if (canOverscrollBottom()) SCROLL_BOTTOM_TOSTRING_DESCRIPTION else "")
+ .removeSuffix(TOSTRING_SEPARATOR)
+ .toString()
+
+ return if (overscrollDirections.trim().isEmpty()) {
+ OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
+ } else {
+ OVERSCROLL_TOSTRING_DESCRIPTION + overscrollDirections
+ }
+ }
+
+ companion object {
+ /**
+ * Create a new instance of [InputResultDetail].
+ *
+ * @param verticalOverscrollInitiallyEnabled optional parameter for enabling pull to refresh
+ * in the cases in which this class can be used before valid values being set and it helps more to have
+ * overscroll vertically allowed and then stop depending on the values with which this class is updated
+ * rather than start with a disabled overscroll functionality for the current gesture.
+ */
+ fun newInstance(verticalOverscrollInitiallyEnabled: Boolean = false) = InputResultDetail(
+ overscrollDirections = if (verticalOverscrollInitiallyEnabled) {
+ OVERSCROLL_DIRECTIONS_VERTICAL
+ } else {
+ OVERSCROLL_DIRECTIONS_NONE
+ },
+ )
+
+ @VisibleForTesting internal const val TOSTRING_SEPARATOR = ", "
+
+ @VisibleForTesting internal const val INPUT_UNKNOWN_HANDLING_DESCRIPTION = "with unknown handling"
+
+ @VisibleForTesting internal const val INPUT_HANDLED_TOSTRING_DESCRIPTION = "handled by the browser"
+
+ @VisibleForTesting internal const val INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION = "handled by the website"
+
+ @VisibleForTesting internal const val INPUT_UNHANDLED_TOSTRING_DESCRIPTION = "unhandled"
+
+ @VisibleForTesting internal const val SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION = "cannot be scrolled"
+
+ @VisibleForTesting internal const val OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION = "cannot be overscrolled"
+
+ @VisibleForTesting internal const val SCROLL_TOSTRING_DESCRIPTION = "can be scrolled to "
+
+ @VisibleForTesting internal const val OVERSCROLL_TOSTRING_DESCRIPTION = "can be overscrolled to "
+
+ @VisibleForTesting internal const val SCROLL_LEFT_TOSTRING_DESCRIPTION = "left"
+
+ @VisibleForTesting internal const val SCROLL_TOP_TOSTRING_DESCRIPTION = "top"
+
+ @VisibleForTesting internal const val SCROLL_RIGHT_TOSTRING_DESCRIPTION = "right"
+
+ @VisibleForTesting internal const val SCROLL_BOTTOM_TOSTRING_DESCRIPTION = "bottom"
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/Settings.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/Settings.kt
new file mode 100644
index 0000000000..b76377cfee
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/Settings.kt
@@ -0,0 +1,325 @@
+/* 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.concept.engine
+
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode
+import mozilla.components.concept.engine.EngineSession.SafeBrowsingPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import mozilla.components.concept.engine.request.RequestInterceptor
+import kotlin.reflect.KProperty
+
+/**
+ * Holds settings of an engine or session. Concrete engine
+ * implementations define how these settings are applied i.e.
+ * whether a setting is applied on an engine or session instance.
+ */
+@Suppress("UnnecessaryAbstractClass")
+abstract class Settings {
+ /**
+ * Setting to control whether or not JavaScript is enabled.
+ */
+ open var javascriptEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not DOM Storage is enabled.
+ */
+ open var domStorageEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not Web fonts are enabled.
+ */
+ open var webFontsEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether the fonts adjust size with the system accessibility settings.
+ */
+ open var automaticFontSizeAdjustment: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether the [Accept-Language] headers are altered with system locale
+ * settings.
+ */
+ open var automaticLanguageAdjustment: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control tracking protection.
+ */
+ open var trackingProtectionPolicy: TrackingProtectionPolicy? by UnsupportedSetting()
+
+ /**
+ * Setting to control the cookie banner handling feature.
+ */
+ open var cookieBannerHandlingMode: CookieBannerHandlingMode by UnsupportedSetting()
+
+ /**
+ * Setting to control the cookie banner handling feature in the private browsing mode.
+ */
+ open var cookieBannerHandlingModePrivateBrowsing: CookieBannerHandlingMode by UnsupportedSetting()
+
+ /**
+ * Setting to control tracking protection.
+ */
+ open var safeBrowsingPolicy: Array<SafeBrowsingPolicy> by UnsupportedSetting()
+
+ /**
+ * Setting to control the cookie banner handling feature detect only mode.
+ */
+ open var cookieBannerHandlingDetectOnlyMode: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control the cookie banner handling global rules feature.
+ */
+ open var cookieBannerHandlingGlobalRules: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control the cookie banner handling global rules subFrames feature.
+ */
+ open var cookieBannerHandlingGlobalRulesSubFrames: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control the cookie banner enables / disables the URL query string
+ * stripping in normal browsing mode which strips query parameters from loading
+ * URIs to prevent bounce (redirect) tracking.
+ */
+ open var queryParameterStripping: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control the cookie banner enables / disables the URL query string
+ * stripping in private browsing mode which strips query parameters from loading
+ * URIs to prevent bounce (redirect) tracking.
+ */
+ open var queryParameterStrippingPrivateBrowsing: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control the list that contains sites where should
+ * exempt from query stripping.
+ */
+ open var queryParameterStrippingAllowList: String by UnsupportedSetting()
+
+ /**
+ * Setting to control the list which contains query parameters that are needed to be stripped
+ * from URIs. The query parameters are separated by a space.
+ */
+ open var queryParameterStrippingStripList: String by UnsupportedSetting()
+
+ /**
+ * Setting to intercept and override requests.
+ */
+ open var requestInterceptor: RequestInterceptor? by UnsupportedSetting()
+
+ /**
+ * Setting to provide a history delegate to the engine.
+ */
+ open var historyTrackingDelegate: HistoryTrackingDelegate? by UnsupportedSetting()
+
+ /**
+ * Setting to control the user agent string.
+ */
+ open var userAgentString: String? by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not a user gesture is required to play media.
+ */
+ open var mediaPlaybackRequiresUserGesture: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not window.open can be called from JavaScript.
+ */
+ open var javaScriptCanOpenWindowsAutomatically: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not zoom controls should be displayed.
+ */
+ open var displayZoomControls: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not the engine zooms out the content to fit on screen by width.
+ */
+ open var loadWithOverviewMode: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether to support the viewport HTML meta tag or if a wide viewport
+ * should be used. If not null, this value overrides useWideViePort webSettings in
+ * [EngineSession.toggleDesktopMode].
+ */
+ open var useWideViewPort: Boolean? by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not file access is allowed.
+ */
+ open var allowFileAccess: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not JavaScript running in the context of a file scheme URL
+ * should be allowed to access content from other file scheme URLs.
+ */
+ open var allowFileAccessFromFileURLs: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not JavaScript running in the context of a file scheme URL
+ * should be allowed to access content from any origin.
+ */
+ open var allowUniversalAccessFromFileURLs: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not the engine is allowed to load content from a content
+ * provider installed in the system.
+ */
+ open var allowContentAccess: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not vertical scrolling is enabled.
+ */
+ open var verticalScrollBarEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not horizontal scrolling is enabled.
+ */
+ open var horizontalScrollBarEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not remote debugging is enabled.
+ */
+ open var remoteDebuggingEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not multiple windows are supported.
+ */
+ open var supportMultipleWindows: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether or not testing mode is enabled.
+ */
+ open var testingModeEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to alert the content that the user prefers a particular theme. This affects the
+ * [@media(prefers-color-scheme)] query.
+ */
+ open var preferredColorScheme: PreferredColorScheme by UnsupportedSetting()
+
+ /**
+ * Setting to control whether media should be suspended when the session is inactive.
+ */
+ open var suspendMediaWhenInactive: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control whether font inflation is enabled.
+ */
+ open var fontInflationEnabled: Boolean? by UnsupportedSetting()
+
+ /**
+ * Setting to control the font size factor. All font sizes will be multiplied by this factor.
+ */
+ open var fontSizeFactor: Float? by UnsupportedSetting()
+
+ /**
+ * Setting to control login autofill.
+ */
+ open var loginAutofillEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to force the ability to scale the content
+ */
+ open var forceUserScalableContent: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control the clear color while drawing.
+ */
+ open var clearColor: Int? by UnsupportedSetting()
+
+ /**
+ * Setting to control whether enterprise root certs are enabled.
+ */
+ open var enterpriseRootsEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting the HTTPS-Only mode for upgrading connections to HTTPS.
+ */
+ open var httpsOnlyMode: Engine.HttpsOnlyMode by UnsupportedSetting()
+
+ /**
+ * Setting to control whether Global Privacy Control isenabled.
+ */
+ open var globalPrivacyControlEnabled: Boolean by UnsupportedSetting()
+
+ /**
+ * Setting to control the email tracker blocking feature in the private browsing mode.
+ */
+ open var emailTrackerBlockingPrivateBrowsing: Boolean by UnsupportedSetting()
+}
+
+/**
+ * [Settings] implementation used to set defaults for [Engine] and [EngineSession].
+ */
+data class DefaultSettings(
+ override var javascriptEnabled: Boolean = true,
+ override var domStorageEnabled: Boolean = true,
+ override var webFontsEnabled: Boolean = true,
+ override var automaticFontSizeAdjustment: Boolean = true,
+ override var automaticLanguageAdjustment: Boolean = true,
+ override var mediaPlaybackRequiresUserGesture: Boolean = true,
+ override var trackingProtectionPolicy: TrackingProtectionPolicy? = null,
+ override var requestInterceptor: RequestInterceptor? = null,
+ override var historyTrackingDelegate: HistoryTrackingDelegate? = null,
+ override var userAgentString: String? = null,
+ override var javaScriptCanOpenWindowsAutomatically: Boolean = false,
+ override var displayZoomControls: Boolean = true,
+ override var loadWithOverviewMode: Boolean = false,
+ override var useWideViewPort: Boolean? = null,
+ override var allowFileAccess: Boolean = true,
+ override var allowFileAccessFromFileURLs: Boolean = false,
+ override var allowUniversalAccessFromFileURLs: Boolean = false,
+ override var allowContentAccess: Boolean = true,
+ override var verticalScrollBarEnabled: Boolean = true,
+ override var horizontalScrollBarEnabled: Boolean = true,
+ override var remoteDebuggingEnabled: Boolean = false,
+ override var supportMultipleWindows: Boolean = false,
+ override var preferredColorScheme: PreferredColorScheme = PreferredColorScheme.System,
+ override var testingModeEnabled: Boolean = false,
+ override var suspendMediaWhenInactive: Boolean = false,
+ override var fontInflationEnabled: Boolean? = null,
+ override var fontSizeFactor: Float? = null,
+ override var forceUserScalableContent: Boolean = false,
+ override var loginAutofillEnabled: Boolean = false,
+ override var clearColor: Int? = null,
+ override var enterpriseRootsEnabled: Boolean = false,
+ override var httpsOnlyMode: Engine.HttpsOnlyMode = Engine.HttpsOnlyMode.DISABLED,
+ override var globalPrivacyControlEnabled: Boolean = false,
+ override var cookieBannerHandlingMode: CookieBannerHandlingMode = CookieBannerHandlingMode.DISABLED,
+ override var cookieBannerHandlingModePrivateBrowsing: CookieBannerHandlingMode =
+ CookieBannerHandlingMode.DISABLED,
+ override var cookieBannerHandlingDetectOnlyMode: Boolean = false,
+ override var cookieBannerHandlingGlobalRules: Boolean = false,
+ override var cookieBannerHandlingGlobalRulesSubFrames: Boolean = false,
+ override var queryParameterStripping: Boolean = false,
+ override var queryParameterStrippingPrivateBrowsing: Boolean = false,
+ override var queryParameterStrippingAllowList: String = "",
+ override var queryParameterStrippingStripList: String = "",
+ override var emailTrackerBlockingPrivateBrowsing: Boolean = false,
+) : Settings()
+
+class UnsupportedSetting<T> {
+ operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
+ throw UnsupportedSettingException(
+ "The setting ${prop.name} is not supported by this engine or session. " +
+ "Check both the engine and engine session implementation.",
+ )
+ }
+
+ operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: T) {
+ throw UnsupportedSettingException(
+ "The setting ${prop.name} is not supported by this engine or session. " +
+ "Check both the engine and engine session implementation.",
+ )
+ }
+}
+
+/**
+ * Exception thrown by default if a setting is not supported by an engine or session.
+ */
+class UnsupportedSettingException(message: String = "Setting not supported by this engine") : RuntimeException(message)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/activity/ActivityDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/activity/ActivityDelegate.kt
new file mode 100644
index 0000000000..aa270d9bf6
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/activity/ActivityDelegate.kt
@@ -0,0 +1,22 @@
+/* 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.concept.engine.activity
+
+import android.content.Intent
+import android.content.IntentSender
+
+/**
+ * Notifies applications or other components of engine events that require interaction with an Android Activity.
+ */
+interface ActivityDelegate {
+
+ /**
+ * Requests an [IntentSender] is started on behalf of the engine.
+ *
+ * @param intent The [IntentSender] to be started through an Android Activity.
+ * @param onResult The callback to be invoked when we receive the result with the intent data.
+ */
+ fun startIntentSenderForResult(intent: IntentSender, onResult: (Intent?) -> Unit) = Unit
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/activity/OrientationDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/activity/OrientationDelegate.kt
new file mode 100644
index 0000000000..a44c91e759
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/activity/OrientationDelegate.kt
@@ -0,0 +1,28 @@
+/* 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.concept.engine.activity
+
+import android.content.pm.ActivityInfo
+
+/**
+ * Notifies applications or other components of engine orientation lock events.
+ */
+interface OrientationDelegate {
+ /**
+ * Request to force a certain screen orientation on the current activity.
+ *
+ * @param requestedOrientation The screen orientation which should be set.
+ * Values can be any of screen orientation values defined in [ActivityInfo].
+ *
+ * @return Whether the request to set a screen orientation is promised to be fulfilled or denied.
+ */
+ fun onOrientationLock(requestedOrientation: Int): Boolean = true
+
+ /**
+ * Request to restore the natural device orientation, what it was before [onOrientationLock].
+ * Implementers should usually set [ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED].
+ */
+ fun onOrientationUnlock() = Unit
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/Tracker.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/Tracker.kt
new file mode 100644
index 0000000000..d5996c8896
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/Tracker.kt
@@ -0,0 +1,20 @@
+/* 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.concept.engine.content.blocking
+
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
+
+/**
+ * Represents a blocked content tracker.
+ * @property url The URL of the tracker.
+ * @property trackingCategories The anti-tracking category types of the blocked resource.
+ * @property cookiePolicies The cookie types of the blocked resource.
+ */
+class Tracker(
+ val url: String,
+ val trackingCategories: List<TrackingCategory> = emptyList(),
+ val cookiePolicies: List<CookiePolicy> = emptyList(),
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackerLog.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackerLog.kt
new file mode 100644
index 0000000000..6939e1d16b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackerLog.kt
@@ -0,0 +1,23 @@
+/* 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.concept.engine.content.blocking
+
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
+
+/**
+ * Represents a blocked content tracker.
+ * @property url The URL of the tracker.
+ * @property loadedCategories A list of tracking categories loaded for this tracker.
+ * @property blockedCategories A list of tracking categories blocked for this tracker.
+ * @property unBlockedBySmartBlock Indicates if the content of the [blockedCategories]
+ * has been partially unblocked by the SmartBlock feature.
+ */
+data class TrackerLog(
+ val url: String,
+ val loadedCategories: List<TrackingCategory> = emptyList(),
+ val blockedCategories: List<TrackingCategory> = emptyList(),
+ val cookiesHasBeenBlocked: Boolean = false,
+ val unBlockedBySmartBlock: Boolean = false,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackingProtectionException.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackingProtectionException.kt
new file mode 100644
index 0000000000..5715efc817
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackingProtectionException.kt
@@ -0,0 +1,16 @@
+/* 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.concept.engine.content.blocking
+
+/**
+ * Represents a site that will be ignored by the tracking protection policies.
+ */
+interface TrackingProtectionException {
+
+ /**
+ * The url of the site to be ignored.
+ */
+ val url: String
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackingProtectionExceptionStorage.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackingProtectionExceptionStorage.kt
new file mode 100644
index 0000000000..e838c7676c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/content/blocking/TrackingProtectionExceptionStorage.kt
@@ -0,0 +1,57 @@
+/* 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.concept.engine.content.blocking
+
+import mozilla.components.concept.engine.EngineSession
+
+/**
+ * A contract that define how a tracking protection storage must behave.
+ */
+interface TrackingProtectionExceptionStorage {
+
+ /**
+ * Fetch all domains that will be ignored for tracking protection.
+ * @param onResult A callback to inform that the domains in the exception list has been fetched,
+ * it provides a list of all the domains that are on the exception list, if there are none
+ * domains in the exception list, an empty list will be provided.
+ */
+ fun fetchAll(onResult: (List<TrackingProtectionException>) -> Unit)
+
+ /**
+ * Adds a new [session] to the exception list.
+ * @param session The [session] that will be added to the exception list.
+ * @param persistInPrivateMode Indicates if the exception should be persistent in private mode
+ * defaults to false.
+ */
+ fun add(session: EngineSession, persistInPrivateMode: Boolean = false)
+
+ /**
+ * Removes a [session] from the exception list.
+ * @param session The [session] that will be removed from the exception list.
+ */
+ fun remove(session: EngineSession)
+
+ /**
+ * Removes a [exception] from the exception list.
+ * @param exception The [TrackingProtectionException] that will be removed from the exception list.
+ */
+ fun remove(exception: TrackingProtectionException)
+
+ /**
+ * Indicates if a given [session] is in the exception list.
+ * @param session The [session] to be verified.
+ * @param onResult A callback to inform if the given [session] is in
+ * the exception list, true if it is in, otherwise false.
+ */
+ fun contains(session: EngineSession, onResult: (Boolean) -> Unit)
+
+ /**
+ * Removes all domains from the exception list.
+ * @param activeSessions A list of all active sessions (including CustomTab
+ * sessions) to be notified.
+ * @param onRemove A callback to inform that the list of active sessions has been removed
+ */
+ fun removeAll(activeSessions: List<EngineSession>? = null, onRemove: () -> Unit = {})
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/cookiehandling/CookieBannersStorage.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/cookiehandling/CookieBannersStorage.kt
new file mode 100644
index 0000000000..8ccbdc0f65
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/cookiehandling/CookieBannersStorage.kt
@@ -0,0 +1,68 @@
+/* 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.concept.engine.cookiehandling
+
+import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode
+
+/**
+ * Represents a storage to manage [CookieBannerHandlingMode] exceptions.
+ */
+interface CookieBannersStorage {
+ /**
+ * Set the [CookieBannerHandlingMode.DISABLED] mode for the given [uri] and [privateBrowsing].
+ * @param uri the [uri] for the site to be updated.
+ * @param privateBrowsing Indicates if given [uri] should be in private browsing or not.
+ */
+ suspend fun addException(
+ uri: String,
+ privateBrowsing: Boolean,
+ )
+
+ /**
+ * Check if the given site's domain url is saved locally.
+ * @param siteDomain the [siteDomain] that will be checked.
+ */
+ suspend fun isSiteDomainReported(siteDomain: String): Boolean
+
+ /**
+ * Save the given site's domain url in datastore to keep it persistent locally.
+ * This method gets called after the site domain was reported with Nimbus.
+ * @param siteDomain the [siteDomain] that will be saved.
+ */
+ suspend fun saveSiteDomain(siteDomain: String)
+
+ /**
+ * Set persistently the [CookieBannerHandlingMode.DISABLED] mode for the given [uri] in
+ * private browsing.
+ * @param uri the [uri] for the site to be updated.
+ */
+ suspend fun addPersistentExceptionInPrivateMode(uri: String)
+
+ /**
+ * Find a [CookieBannerHandlingMode] that matches the given [uri] and browsing mode.
+ * @param uri the [uri] to be used as filter in the search.
+ * @param privateBrowsing Indicates if given [uri] should be in private browsing or not.
+ * @return the [CookieBannerHandlingMode] for the provided [uri] and browsing mode,
+ * if an error occurs null will be returned.
+ */
+ suspend fun findExceptionFor(uri: String, privateBrowsing: Boolean): CookieBannerHandlingMode?
+
+ /**
+ * Indicates if the given [uri] and browsing mode has the [CookieBannerHandlingMode.DISABLED] mode.
+ * @param uri the [uri] to be used as filter in the search.
+ * @param privateBrowsing Indicates if given [uri] should be in private browsing or not.
+ * @return A [Boolean] indicating if the [CookieBannerHandlingMode] has been updated, from the
+ * default value, if an error occurs null will be returned.
+ */
+ suspend fun hasException(uri: String, privateBrowsing: Boolean): Boolean?
+
+ /**
+ * Remove any [CookieBannerHandlingMode] exception that has been applied to the given [uri] and
+ * browsing mode.
+ * @param uri the [uri] to be used as filter in the search.
+ * @param privateBrowsing Indicates if given [uri] should be in private browsing or not.
+ */
+ suspend fun removeException(uri: String, privateBrowsing: Boolean)
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/history/HistoryItem.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/history/HistoryItem.kt
new file mode 100644
index 0000000000..f4a07d0d99
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/history/HistoryItem.kt
@@ -0,0 +1,15 @@
+/* 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.concept.engine.history
+
+/**
+ * A representation of an entry in browser history.
+ * @property title The title of this history element.
+ * @property uri The URI of this history element.
+ */
+data class HistoryItem(
+ val title: String,
+ val uri: String,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/history/HistoryTrackingDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/history/HistoryTrackingDelegate.kt
new file mode 100644
index 0000000000..2793f2377c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/history/HistoryTrackingDelegate.kt
@@ -0,0 +1,47 @@
+/* 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.concept.engine.history
+
+import mozilla.components.concept.storage.PageVisit
+
+/**
+ * An interface used for providing history information to an engine (e.g. for link highlighting),
+ * and receiving history updates from the engine (visits to URLs, title changes).
+ *
+ * Even though this interface is defined at the "concept" layer, its get* methods are tailored to
+ * two types of engines which we support (system's WebView and GeckoView).
+ */
+interface HistoryTrackingDelegate {
+ /**
+ * A URI visit happened that an engine considers worthy of being recorded in browser's history.
+ */
+ suspend fun onVisited(uri: String, visit: PageVisit)
+
+ /**
+ * Title changed for a given URI.
+ */
+ suspend fun onTitleChanged(uri: String, title: String)
+
+ /**
+ * Preview image changed for a given URI.
+ */
+ suspend fun onPreviewImageChange(uri: String, previewImageUrl: String)
+
+ /**
+ * An engine needs to know "visited" (true/false) status for provided URIs.
+ */
+ suspend fun getVisited(uris: List<String>): List<Boolean>
+
+ /**
+ * An engine needs to know a list of all visited URIs.
+ */
+ suspend fun getVisited(): List<String>
+
+ /**
+ * Allows an engine to check if this URI is going to be accepted by the delegate.
+ * This helps avoid unnecessary coroutine overhead for URIs which won't be accepted.
+ */
+ fun shouldStoreUri(uri: String): Boolean
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/Size.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/Size.kt
new file mode 100644
index 0000000000..ba2ae8affa
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/Size.kt
@@ -0,0 +1,59 @@
+/* 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.concept.engine.manifest
+
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Represents dimensions for an image.
+ * Corresponds to values of the "sizes" HTML attribute.
+ *
+ * @property width Width of the image.
+ * @property height Height of the image.
+ */
+data class Size(
+ val width: Int,
+ val height: Int,
+) {
+
+ /**
+ * Gets the longest length between width and height.
+ */
+ val maxLength get() = max(width, height)
+
+ /**
+ * Gets the shortest length between width and height.
+ */
+ val minLength get() = min(width, height)
+
+ override fun toString() = if (this == ANY) "any" else "${width}x$height"
+
+ companion object {
+ /**
+ * Represents the "any" size.
+ */
+ val ANY = Size(Int.MAX_VALUE, Int.MAX_VALUE)
+
+ /**
+ * Parses a value from an HTML sizes attribute (512x512, 16x16, etc).
+ * Returns null if the value was invalid.
+ */
+ fun parse(raw: String): Size? {
+ if (raw == "any") return ANY
+
+ val size = raw.split("x")
+ if (size.size != 2) return null
+
+ return try {
+ val width = size[0].toInt()
+ val height = size[1].toInt()
+ Size(width, height)
+ } catch (e: NumberFormatException) {
+ null
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifest.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifest.kt
new file mode 100644
index 0000000000..b193beaccc
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifest.kt
@@ -0,0 +1,253 @@
+/* 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.concept.engine.manifest
+
+import androidx.annotation.ColorInt
+import mozilla.components.concept.engine.manifest.WebAppManifest.ExternalApplicationResource.Fingerprint
+
+/**
+ * The web app manifest provides information about an application (such as its name, author, icon, and description).
+ *
+ * Web app manifests are part of a collection of web technologies called progressive web apps, which are websites
+ * that can be installed to a device’s homescreen without an app store, along with other capabilities like working
+ * offline and receiving push notifications.
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/Manifest
+ * https://www.w3.org/TR/appmanifest/
+ * https://developers.google.com/web/fundamentals/web-app-manifest/
+ *
+ * @property name Provides a human-readable name for the site when displayed to the user. For example, among a list of
+ * other applications or as a label for an icon.
+ * @property shortName Provides a short human-readable name for the application. This is intended for when there is
+ * insufficient space to display the full name of the web application, like device homescreens.
+ * @property startUrl The URL that loads when a user launches the application (e.g. when added to home screen),
+ * typically the index. Note that this has to be a relative URL, relative to the manifest url.
+ * @property display Defines the developers’ preferred display mode for the website.
+ * @property backgroundColor Defines the expected “background color” for the website. This value repeats what is
+ * already available in the site’s CSS, but can be used by browsers to draw the background color of a shortcut when
+ * the manifest is available before the stylesheet has loaded. This creates a smooth transition between launching the
+ * web application and loading the site's content.
+ * @property description Provides a general description of what the pinned website does.
+ * @property icons Specifies a list of image files that can serve as application icons, depending on context. For
+ * example, they can be used to represent the web application amongst a list of other applications, or to integrate the
+ * web application with an OS's task switcher and/or system preferences.
+ * @property dir Specifies the primary text direction for the name, short_name, and description members. Together with
+ * the lang member, it helps the correct display of right-to-left languages.
+ * @property lang Specifies the primary language for the values in the name and short_name members. This value is a
+ * string containing a single language tag (e.g. en-US).
+ * @property orientation Defines the default orientation for all the website's top level browsing contexts.
+ * @property scope Defines the navigation scope of this website's context. This restricts what web pages can be viewed
+ * while the manifest is applied. If the user navigates outside the scope, it returns to a normal web page inside a
+ * browser tab/window.
+ * @property themeColor Defines the default theme color for an application. This sometimes affects how the OS displays
+ * the site (e.g., on Android's task switcher, the theme color surrounds the site).
+ * @property relatedApplications List of native applications related to the web app.
+ * @property preferRelatedApplications If true, related applications should be preferred over the web app.
+ */
+data class WebAppManifest(
+ val name: String,
+ val startUrl: String,
+ val shortName: String? = null,
+ val display: DisplayMode = DisplayMode.BROWSER,
+ @ColorInt val backgroundColor: Int? = null,
+ val description: String? = null,
+ val icons: List<Icon> = emptyList(),
+ val dir: TextDirection = TextDirection.AUTO,
+ val lang: String? = null,
+ val orientation: Orientation = Orientation.ANY,
+ val scope: String? = null,
+ @ColorInt val themeColor: Int? = null,
+ val relatedApplications: List<ExternalApplicationResource> = emptyList(),
+ val preferRelatedApplications: Boolean = false,
+ val shareTarget: ShareTarget? = null,
+) {
+ /**
+ * Defines the developers’ preferred display mode for the website.
+ */
+ enum class DisplayMode {
+ /**
+ * All of the available display area is used and no user agent chrome is shown.
+ */
+ FULLSCREEN,
+
+ /**
+ * The application will look and feel like a standalone application. This can include the application having a
+ * different window, its own icon in the application launcher, etc. In this mode, the user agent will exclude
+ * UI elements for controlling navigation, but can include other UI elements such as a status bar.
+ */
+ STANDALONE,
+
+ /**
+ * The application will look and feel like a standalone application, but will have a minimal set of UI elements
+ * for controlling navigation. The elements will vary by browser.
+ */
+ MINIMAL_UI,
+
+ /**
+ * The application opens in a conventional browser tab or new window, depending on the browser and platform.
+ * This is the default.
+ */
+ BROWSER,
+ }
+
+ /**
+ * An image file that can serve as application icon.
+ *
+ * @property src The path to the image file. If src is a relative URL, the base URL will be the URL of the manifest.
+ * @property sizes A list of image dimensions.
+ * @property type A hint as to the media type of the image. The purpose of this member is to allow a user agent to
+ * quickly ignore images of media types it does not support.
+ * @property purpose Defines the purposes of the image, for example that the image is intended to serve some special
+ * purpose in the context of the host OS (i.e., for better integration).
+ */
+ data class Icon(
+ val src: String,
+ val sizes: List<Size> = emptyList(),
+ val type: String? = null,
+ val purpose: Set<Purpose> = setOf(Purpose.ANY),
+ ) {
+ enum class Purpose {
+ /**
+ * A user agent can present this icon where space constraints and/or color requirements differ from those
+ * of the application icon.
+ */
+ MONOCHROME,
+
+ /**
+ * The image is designed with icon masks and safe zone in mind, such that any part of the image that is
+ * outside the safe zone can safely be ignored and masked away by the user agent.
+ *
+ * https://w3c.github.io/manifest/#icon-masks
+ */
+ MASKABLE,
+
+ /**
+ * The user agent is free to display the icon in any context (this is the default value).
+ */
+ ANY,
+ }
+ }
+
+ /**
+ * Defines the default orientation for all the website's top level browsing contexts.
+ */
+ enum class Orientation {
+ ANY,
+ NATURAL,
+ LANDSCAPE,
+ LANDSCAPE_PRIMARY,
+ LANDSCAPE_SECONDARY,
+ PORTRAIT,
+ PORTRAIT_PRIMARY,
+ PORTRAIT_SECONDARY,
+ }
+
+ /**
+ * Specifies the primary text direction for the name, short_name, and description members. Together with the lang
+ * member, it helps the correct display of right-to-left languages.
+ */
+ enum class TextDirection {
+ /**
+ * Left-to-right (LTR).
+ */
+ LTR,
+
+ /**
+ * Right-to-left (RTL).
+ */
+ RTL,
+
+ /**
+ * If the value is set to auto, the browser will use the Unicode bidirectional algorithm to make a best guess
+ * about the text's direction.
+ */
+ AUTO,
+ }
+
+ /**
+ * An external native application that is related to the web app.
+ *
+ * @property platform The platform the native app is associated with.
+ * @property url The URL where it can be found.
+ * @property id Information additional to or instead of the URL, depending on the platform.
+ * @property minVersion The minimum version of an application related to this web app.
+ * @property fingerprints [Fingerprint] objects used for verifying the application.
+ */
+ data class ExternalApplicationResource(
+ val platform: String,
+ val url: String? = null,
+ val id: String? = null,
+ val minVersion: String? = null,
+ val fingerprints: List<Fingerprint> = emptyList(),
+ ) {
+
+ /**
+ * Represents a set of cryptographic fingerprints used for verifying the application.
+ * The syntax and semantics of [type] and [value] are platform-defined.
+ */
+ data class Fingerprint(
+ val type: String,
+ val value: String,
+ )
+ }
+
+ /**
+ * Used to define how the web app receives share data.
+ * If present, a share target should be created so that other Android apps can share to this web app.
+ *
+ * @property action URL to open on share
+ * @property method Method to use with [action]. Either "GET" or "POST".
+ * @property encType MIME type to specify how the params are encoded.
+ * @property params Specifies what query parameters correspond to share data.
+ */
+ data class ShareTarget(
+ val action: String,
+ val method: RequestMethod = RequestMethod.GET,
+ val encType: EncodingType = EncodingType.URL_ENCODED,
+ val params: Params = Params(),
+ ) {
+
+ /**
+ * Specifies what query parameters correspond to share data.
+ *
+ * @property title Name of the query parameter used for the title of the data being shared.
+ * @property text Name of the query parameter used for the body of the data being shared.
+ * @property url Name of the query parameter used for a URL referring to a shared resource.
+ * @property files Form fields used to share files.
+ */
+ data class Params(
+ val title: String? = null,
+ val text: String? = null,
+ val url: String? = null,
+ val files: List<Files> = emptyList(),
+ )
+
+ /**
+ * Specifies a form field member used to share files.
+ *
+ * @property name Name of the form field.
+ * @property accept Accepted MIME types or file extensions.
+ */
+ data class Files(
+ val name: String,
+ val accept: List<String>,
+ )
+
+ /**
+ * Valid HTTP methods for [ShareTarget.method].
+ */
+ enum class RequestMethod {
+ GET, POST
+ }
+
+ /**
+ * Valid encoding MIME types for [ShareTarget.encType].
+ */
+ enum class EncodingType(val type: String) {
+ URL_ENCODED("application/x-www-form-urlencoded"),
+ MULTIPART("multipart/form-data"),
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifestParser.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifestParser.kt
new file mode 100644
index 0000000000..b5c0cd5812
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/WebAppManifestParser.kt
@@ -0,0 +1,238 @@
+/* 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.concept.engine.manifest
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import mozilla.components.concept.engine.manifest.parser.ShareTargetParser
+import mozilla.components.concept.engine.manifest.parser.parseIcons
+import mozilla.components.concept.engine.manifest.parser.serializeEnumName
+import mozilla.components.concept.engine.manifest.parser.serializeIcons
+import mozilla.components.support.ktx.android.org.json.asSequence
+import mozilla.components.support.ktx.android.org.json.tryGetString
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+/**
+ * Parser for constructing a [WebAppManifest] from JSON.
+ */
+class WebAppManifestParser {
+ /**
+ * A parsing result.
+ */
+ sealed class Result {
+ /**
+ * The JSON was parsed successful.
+ *
+ * @property manifest The parsed [WebAppManifest] object.
+ */
+ data class Success(val manifest: WebAppManifest) : Result()
+
+ /**
+ * Parsing the JSON failed.
+ *
+ * @property exception The exception that was thrown while parsing the manifest.
+ */
+ data class Failure(val exception: JSONException) : Result()
+ }
+
+ /**
+ * Parses the provided JSON and returns a [WebAppManifest] (wrapped in [Result.Success] if parsing was successful.
+ * Otherwise [Result.Failure].
+ *
+ * Gecko performs some initial parsing on the Web App Manifest, so the [JSONObject] we work with
+ * does not match what was originally provided by the website. Gecko:
+ * - Changes relative URLs to be absolute
+ * - Changes some space-separated strings into arrays (purpose, sizes)
+ * - Changes colors to follow Android format (#AARRGGBB)
+ * - Removes invalid enum values (ie display: halfscreen)
+ * - Ensures display, dir, start_url, and scope always have a value
+ * - Trims most strings (name, short_name, ...)
+ * See https://searchfox.org/mozilla-central/source/dom/manifest/ManifestProcessor.jsm
+ */
+ fun parse(json: JSONObject): Result {
+ return try {
+ val shortName = json.tryGetString("short_name")
+ val name = json.tryGetString("name") ?: shortName
+ ?: return Result.Failure(JSONException("Missing manifest name"))
+
+ Result.Success(
+ WebAppManifest(
+ name = name,
+ shortName = shortName,
+ startUrl = json.getString("start_url"),
+ display = parseDisplayMode(json),
+ backgroundColor = parseColor(json.tryGetString("background_color")),
+ description = json.tryGetString("description"),
+ icons = parseIcons(json),
+ scope = json.tryGetString("scope"),
+ themeColor = parseColor(json.tryGetString("theme_color")),
+ dir = parseTextDirection(json),
+ lang = json.tryGetString("lang"),
+ orientation = parseOrientation(json),
+ relatedApplications = parseRelatedApplications(json),
+ preferRelatedApplications = json.optBoolean("prefer_related_applications", false),
+ shareTarget = ShareTargetParser.parse(json.optJSONObject("share_target")),
+ ),
+ )
+ } catch (e: JSONException) {
+ Result.Failure(e)
+ }
+ }
+
+ /**
+ * Parses the provided JSON and returns a [WebAppManifest] (wrapped in [Result.Success] if parsing was successful.
+ * Otherwise [Result.Failure].
+ */
+ fun parse(json: String) = try {
+ parse(JSONObject(json))
+ } catch (e: JSONException) {
+ Result.Failure(e)
+ }
+
+ fun serialize(manifest: WebAppManifest) = JSONObject().apply {
+ put("name", manifest.name)
+ putOpt("short_name", manifest.shortName)
+ put("start_url", manifest.startUrl)
+ putOpt("display", serializeEnumName(manifest.display.name))
+ putOpt("background_color", serializeColor(manifest.backgroundColor))
+ putOpt("description", manifest.description)
+ putOpt("icons", serializeIcons(manifest.icons))
+ putOpt("scope", manifest.scope)
+ putOpt("theme_color", serializeColor(manifest.themeColor))
+ putOpt("dir", serializeEnumName(manifest.dir.name))
+ putOpt("lang", manifest.lang)
+ putOpt("orientation", serializeEnumName(manifest.orientation.name))
+ putOpt("orientation", serializeEnumName(manifest.orientation.name))
+ put("related_applications", serializeRelatedApplications(manifest.relatedApplications))
+ put("prefer_related_applications", manifest.preferRelatedApplications)
+ putOpt("share_target", ShareTargetParser.serialize(manifest.shareTarget))
+ }
+}
+
+/**
+ * Returns the encapsulated value if this instance represents success or `null` if it is failure.
+ */
+fun WebAppManifestParser.Result.getOrNull(): WebAppManifest? = when (this) {
+ is WebAppManifestParser.Result.Success -> manifest
+ is WebAppManifestParser.Result.Failure -> null
+}
+
+private fun parseDisplayMode(json: JSONObject): WebAppManifest.DisplayMode {
+ return when (json.optString("display")) {
+ "standalone" -> WebAppManifest.DisplayMode.STANDALONE
+ "fullscreen" -> WebAppManifest.DisplayMode.FULLSCREEN
+ "minimal-ui" -> WebAppManifest.DisplayMode.MINIMAL_UI
+ "browser" -> WebAppManifest.DisplayMode.BROWSER
+ else -> WebAppManifest.DisplayMode.BROWSER
+ }
+}
+
+@ColorInt
+private fun parseColor(color: String?): Int? {
+ if (color == null || !color.startsWith("#")) {
+ return null
+ }
+
+ return try {
+ Color.parseColor(color)
+ } catch (e: IllegalArgumentException) {
+ null
+ }
+}
+
+private fun parseTextDirection(json: JSONObject): WebAppManifest.TextDirection {
+ return when (json.optString("dir")) {
+ "ltr" -> WebAppManifest.TextDirection.LTR
+ "rtl" -> WebAppManifest.TextDirection.RTL
+ "auto" -> WebAppManifest.TextDirection.AUTO
+ else -> WebAppManifest.TextDirection.AUTO
+ }
+}
+
+private fun parseOrientation(json: JSONObject) = when (json.optString("orientation")) {
+ "any" -> WebAppManifest.Orientation.ANY
+ "natural" -> WebAppManifest.Orientation.NATURAL
+ "landscape" -> WebAppManifest.Orientation.LANDSCAPE
+ "portrait" -> WebAppManifest.Orientation.PORTRAIT
+ "portrait-primary" -> WebAppManifest.Orientation.PORTRAIT_PRIMARY
+ "portrait-secondary" -> WebAppManifest.Orientation.PORTRAIT_SECONDARY
+ "landscape-primary" -> WebAppManifest.Orientation.LANDSCAPE_PRIMARY
+ "landscape-secondary" -> WebAppManifest.Orientation.LANDSCAPE_SECONDARY
+ else -> WebAppManifest.Orientation.ANY
+}
+
+private fun parseRelatedApplications(json: JSONObject): List<WebAppManifest.ExternalApplicationResource> {
+ val array = json.optJSONArray("related_applications") ?: return emptyList()
+
+ return array
+ .asSequence { i -> getJSONObject(i) }
+ .mapNotNull { app -> parseRelatedApplication(app) }
+ .toList()
+}
+
+private fun parseRelatedApplication(app: JSONObject): WebAppManifest.ExternalApplicationResource? {
+ val platform = app.tryGetString("platform")
+ val url = app.tryGetString("url")
+ val id = app.tryGetString("id")
+ return if (platform != null && (url != null || id != null)) {
+ WebAppManifest.ExternalApplicationResource(
+ platform = platform,
+ url = url,
+ id = id,
+ minVersion = app.tryGetString("min_version"),
+ fingerprints = parseFingerprints(app),
+ )
+ } else {
+ null
+ }
+}
+
+private fun parseFingerprints(app: JSONObject): List<WebAppManifest.ExternalApplicationResource.Fingerprint> {
+ val array = app.optJSONArray("fingerprints") ?: return emptyList()
+
+ return array
+ .asSequence { i -> getJSONObject(i) }
+ .map {
+ WebAppManifest.ExternalApplicationResource.Fingerprint(
+ type = it.getString("type"),
+ value = it.getString("value"),
+ )
+ }
+ .toList()
+}
+
+@Suppress("MagicNumber")
+private fun serializeColor(color: Int?): String? = color?.let {
+ String.format("#%06X", 0xFFFFFF and it)
+}
+
+private fun serializeRelatedApplications(
+ relatedApplications: List<WebAppManifest.ExternalApplicationResource>,
+): JSONArray {
+ val list = relatedApplications.map { app ->
+ JSONObject().apply {
+ put("platform", app.platform)
+ putOpt("url", app.url)
+ putOpt("id", app.id)
+ putOpt("min_version", app.minVersion)
+ put("fingerprints", serializeFingerprints(app.fingerprints))
+ }
+ }
+ return JSONArray(list)
+}
+
+private fun serializeFingerprints(
+ fingerprints: List<WebAppManifest.ExternalApplicationResource.Fingerprint>,
+): JSONArray {
+ val list = fingerprints.map {
+ JSONObject().apply {
+ put("type", it.type)
+ put("value", it.value)
+ }
+ }
+ return JSONArray(list)
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/ShareTargetParser.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/ShareTargetParser.kt
new file mode 100644
index 0000000000..13b4af45be
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/ShareTargetParser.kt
@@ -0,0 +1,129 @@
+/* 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.concept.engine.manifest.parser
+
+import mozilla.components.concept.engine.manifest.WebAppManifest.ShareTarget
+import mozilla.components.support.ktx.android.org.json.asSequence
+import mozilla.components.support.ktx.android.org.json.toJSONArray
+import mozilla.components.support.ktx.android.org.json.tryGetString
+import org.json.JSONArray
+import org.json.JSONObject
+import java.util.Locale
+
+internal object ShareTargetParser {
+
+ /**
+ * Parses a share target inside a web app manifest.
+ */
+ fun parse(json: JSONObject?): ShareTarget? {
+ val action = json?.tryGetString("action") ?: return null
+ val method = parseMethod(json.tryGetString("method"))
+ val encType = parseEncType(json.tryGetString("enctype"))
+ val params = json.optJSONObject("params")
+
+ return if (method != null && encType != null && validMethodAndEncType(method, encType)) {
+ return ShareTarget(
+ action = action,
+ method = method,
+ encType = encType,
+ params = ShareTarget.Params(
+ title = params?.tryGetString("title"),
+ text = params?.tryGetString("text"),
+ url = params?.tryGetString("url"),
+ files = parseFiles(params),
+ ),
+ )
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Serializes a share target to JSON for a web app manifest.
+ */
+ fun serialize(shareTarget: ShareTarget?): JSONObject? {
+ shareTarget ?: return null
+ return JSONObject().apply {
+ put("action", shareTarget.action)
+ put("method", shareTarget.method.name)
+ put("enctype", shareTarget.encType.type)
+
+ val params = JSONObject().apply {
+ put("title", shareTarget.params.title)
+ put("text", shareTarget.params.text)
+ put("url", shareTarget.params.url)
+ put(
+ "files",
+ shareTarget.params.files.asSequence()
+ .map { file ->
+ JSONObject().apply {
+ put("name", file.name)
+ putOpt("accept", file.accept.toJSONArray())
+ }
+ }
+ .asIterable()
+ .toJSONArray(),
+ )
+ }
+ put("params", params)
+ }
+ }
+
+ /**
+ * Convert string to [ShareTarget.RequestMethod]. Returns null if the string is invalid.
+ */
+ private fun parseMethod(method: String?): ShareTarget.RequestMethod? {
+ method ?: return ShareTarget.RequestMethod.GET
+ return try {
+ ShareTarget.RequestMethod.valueOf(method.uppercase(Locale.ROOT))
+ } catch (e: IllegalArgumentException) {
+ null
+ }
+ }
+
+ /**
+ * Convert string to [ShareTarget.EncodingType]. Returns null if the string is invalid.
+ */
+ private fun parseEncType(encType: String?): ShareTarget.EncodingType? {
+ val typeString = encType?.lowercase(Locale.ROOT) ?: return ShareTarget.EncodingType.URL_ENCODED
+ return ShareTarget.EncodingType.values().find { it.type == typeString }
+ }
+
+ /**
+ * Checks that [encType] is URL_ENCODED (if [method] is GET or POST) or MULTIPART (only if POST)
+ */
+ private fun validMethodAndEncType(
+ method: ShareTarget.RequestMethod,
+ encType: ShareTarget.EncodingType,
+ ) = when (encType) {
+ ShareTarget.EncodingType.URL_ENCODED -> true
+ ShareTarget.EncodingType.MULTIPART -> method == ShareTarget.RequestMethod.POST
+ }
+
+ private fun parseFiles(params: JSONObject?) =
+ when (val files = params?.opt("files")) {
+ is JSONObject -> listOfNotNull(parseFile(files))
+ is JSONArray -> files.asSequence { i -> getJSONObject(i) }
+ .mapNotNull(::parseFile)
+ .toList()
+ else -> emptyList()
+ }
+
+ private fun parseFile(file: JSONObject): ShareTarget.Files? {
+ val name = file.tryGetString("name")
+ val accept = file.opt("accept")
+
+ if (name.isNullOrEmpty()) return null
+
+ return ShareTarget.Files(
+ name = name,
+ accept = when (accept) {
+ is String -> listOf(accept)
+ is JSONArray -> accept.asSequence { i -> getString(i) }.toList()
+ else -> emptyList()
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/WebAppManifestIconParser.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/WebAppManifestIconParser.kt
new file mode 100644
index 0000000000..6880f66652
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/manifest/parser/WebAppManifestIconParser.kt
@@ -0,0 +1,86 @@
+/* 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.concept.engine.manifest.parser
+
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.support.ktx.android.org.json.asSequence
+import mozilla.components.support.ktx.android.org.json.tryGet
+import mozilla.components.support.ktx.android.org.json.tryGetString
+import org.json.JSONArray
+import org.json.JSONObject
+import java.util.Locale
+
+private val whitespace = "\\s+".toRegex()
+
+/**
+ * Parses the icons array from a web app manifest.
+ */
+internal fun parseIcons(json: JSONObject): List<WebAppManifest.Icon> {
+ val array = json.optJSONArray("icons") ?: return emptyList()
+
+ return array
+ .asSequence { i -> getJSONObject(i) }
+ .mapNotNull { obj ->
+ val purpose = parsePurposes(obj).ifEmpty {
+ return@mapNotNull null
+ }
+ WebAppManifest.Icon(
+ src = obj.getString("src"),
+ sizes = parseIconSizes(obj),
+ type = obj.tryGetString("type"),
+ purpose = purpose,
+ )
+ }
+ .toList()
+}
+
+/**
+ * Parses a string set, which is expressed as either a space-delimited string or JSONArray of strings.
+ *
+ * Gecko returns a JSONArray to represent the intermediate infra type for some properties.
+ */
+private fun parseStringSet(set: Any?): Sequence<String>? = when (set) {
+ is String -> set.split(whitespace).asSequence()
+ is JSONArray -> set.asSequence { i -> getString(i) }
+ else -> null
+}
+
+private fun parseIconSizes(json: JSONObject): List<Size> {
+ val sizes = parseStringSet(json.tryGet("sizes"))
+ ?: return emptyList()
+
+ return sizes.mapNotNull { Size.parse(it) }.toList()
+}
+
+private fun parsePurposes(json: JSONObject): Set<WebAppManifest.Icon.Purpose> {
+ val purpose = parseStringSet(json.tryGet("purpose"))
+ ?: return setOf(WebAppManifest.Icon.Purpose.ANY)
+
+ return purpose
+ .mapNotNull {
+ when (it.lowercase(Locale.ROOT)) {
+ "monochrome" -> WebAppManifest.Icon.Purpose.MONOCHROME
+ "maskable" -> WebAppManifest.Icon.Purpose.MASKABLE
+ "any" -> WebAppManifest.Icon.Purpose.ANY
+ else -> null
+ }
+ }
+ .toSet()
+}
+
+internal fun serializeEnumName(name: String) = name.lowercase(Locale.ROOT).replace('_', '-')
+
+internal fun serializeIcons(icons: List<WebAppManifest.Icon>): JSONArray {
+ val list = icons.map { icon ->
+ JSONObject().apply {
+ put("src", icon.src)
+ put("sizes", icon.sizes.joinToString(" ") { it.toString() })
+ putOpt("type", icon.type)
+ put("purpose", icon.purpose.joinToString(" ") { serializeEnumName(it.name) })
+ }
+ }
+ return JSONArray(list)
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/media/RecordingDevice.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/media/RecordingDevice.kt
new file mode 100644
index 0000000000..9b15fa6205
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/media/RecordingDevice.kt
@@ -0,0 +1,32 @@
+/* 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.concept.engine.media
+
+/**
+ * A recording device that can be used by web content.
+ *
+ * @property type The type of recording device (e.g. camera or microphone)
+ * @property status The status of the recording device (e.g. whether this device is recording)
+ */
+data class RecordingDevice(
+ val type: Type,
+ val status: Status,
+) {
+ /**
+ * Types of recording devices.
+ */
+ enum class Type {
+ CAMERA,
+ MICROPHONE,
+ }
+
+ /**
+ * States a recording device can be in.
+ */
+ enum class Status {
+ INACTIVE,
+ RECORDING,
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/mediaquery/PreferredColorScheme.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/mediaquery/PreferredColorScheme.kt
new file mode 100644
index 0000000000..1f694a53af
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/mediaquery/PreferredColorScheme.kt
@@ -0,0 +1,17 @@
+/* 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.concept.engine.mediaquery
+
+/**
+ * A simple data class used to suggest to page content that the user prefers a particular color
+ * scheme.
+ */
+sealed class PreferredColorScheme {
+ companion object
+
+ object Light : PreferredColorScheme()
+ object Dark : PreferredColorScheme()
+ object System : PreferredColorScheme()
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/mediasession/MediaSession.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/mediasession/MediaSession.kt
new file mode 100644
index 0000000000..08051f923b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/mediasession/MediaSession.kt
@@ -0,0 +1,193 @@
+/* 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.concept.engine.mediasession
+
+import android.graphics.Bitmap
+
+/**
+ * Value type that represents a media session that is present on the currently displayed page in a session.
+ */
+class MediaSession {
+
+ /**
+ * The representation of a media element's metadata.
+ *
+ * @property source The media URI.
+ * @property duration The media duration in seconds.
+ * @property width The video width in device pixels.
+ * @property height The video height in device pixels.
+ * @property audioTrackCount The audio track count.
+ * @property videoTrackCount The video track count.
+ */
+ data class ElementMetadata(
+ val source: String? = null,
+ val duration: Double = -1.0,
+ val width: Long = 0L,
+ val height: Long = 0L,
+ val audioTrackCount: Int = 0,
+ val videoTrackCount: Int = 0,
+ ) {
+ val portrait: Boolean
+ get() = height > width
+ }
+
+ /**
+ * The representation of a media session's metadata.
+ *
+ * @property title The media title string.
+ * @property artist The media artist string.
+ * @property album The media album string.
+ * @property getArtwork Get the media artwork.
+ */
+ data class Metadata(
+ val title: String? = null,
+ val artist: String? = null,
+ val album: String? = null,
+ val getArtwork: (suspend () -> Bitmap?)?,
+ )
+
+ /**
+ * Holds the details of the media session's playback state.
+ *
+ * @property duration The media duration in seconds.
+ * @property position The current media playback position in seconds.
+ * @property playbackRate The playback rate coefficient.
+ */
+ data class PositionState(
+ val duration: Double = -1.0,
+ val position: Double = 0.0,
+ val playbackRate: Double = 0.0,
+ )
+
+ /**
+ * Flags for supported media session features.
+ *
+ * Implementation note: This is a 1:1 mapping of the features that GeckoView notifies us about.
+ * https://github.com/mozilla/gecko-dev/blob/master/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/MediaSession.java
+ */
+ data class Feature(val flags: Long = 0) {
+ companion object {
+ const val NONE: Long = 0
+ const val PLAY: Long = 1L shl 0
+ const val PAUSE: Long = 1L shl 1
+ const val STOP: Long = 1L shl 2
+ const val SEEK_TO: Long = 1L shl 3
+ const val SEEK_FORWARD: Long = 1L shl 4
+ const val SEEK_BACKWARD: Long = 1L shl 5
+ const val SKIP_AD: Long = 1L shl 6
+ const val NEXT_TRACK: Long = 1L shl 7
+ const val PREVIOUS_TRACK: Long = 1L shl 8
+ const val FOCUS: Long = 1L shl 9
+ }
+
+ /**
+ * Returns `true` if this [Feature] contains the [type].
+ */
+ fun contains(flag: Long): Boolean = (flags and flag) != 0L
+
+ /**
+ * Returns `true` if this is [Feature] equal to the [other] [Feature].
+ */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Feature) return false
+ if (flags != other.flags) return false
+ return true
+ }
+
+ override fun hashCode() = flags.hashCode()
+ }
+
+ /**
+ * A simplified media session playback state.
+ */
+ enum class PlaybackState {
+ /**
+ * Unknown. No state has been received from the engine yet.
+ */
+ UNKNOWN,
+
+ /**
+ * Playback of this [MediaSession] has stopped (either completed or aborted).
+ */
+ STOPPED,
+
+ /**
+ * This [MediaSession] is paused.
+ */
+ PAUSED,
+
+ /**
+ * This [MediaSession] is currently playing.
+ */
+ PLAYING,
+ }
+
+ /**
+ * Controller for controlling playback of a media element.
+ */
+ interface Controller {
+ /**
+ * Pauses the media.
+ */
+ fun pause()
+
+ /**
+ * Stop playback for the media session.
+ */
+ fun stop()
+
+ /**
+ * Plays the media.
+ */
+ fun play()
+
+ /**
+ * Seek to a specific time.
+ * Prefer using fast seeking when calling this in a sequence.
+ * Don't use fast seeking for the last or only call in a sequence.
+ *
+ * @param time The time in seconds to move the playback time to.
+ * @param fast Whether fast seeking should be used.
+ */
+ fun seekTo(time: Double, fast: Boolean)
+
+ /**
+ * Seek forward by a sensible number of seconds.
+ */
+ fun seekForward()
+
+ /**
+ * Seek backward by a sensible number of seconds.
+ */
+ fun seekBackward()
+
+ /**
+ * Select and play the next track.
+ * Move playback to the next item in the playlist when supported.
+ */
+ fun nextTrack()
+
+ /**
+ * Select and play the previous track.
+ * Move playback to the previous item in the playlist when supported.
+ */
+ fun previousTrack()
+
+ /**
+ * Skip the advertisement that is currently playing.
+ */
+ fun skipAd()
+
+ /**
+ * Set whether audio should be muted.
+ * Muting audio is supported by default and does not require the media
+ * session to be active.
+ *
+ * @param mute True if audio for this media session should be muted.
+ */
+ fun muteAudio(mute: Boolean)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt
new file mode 100644
index 0000000000..4c00505398
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt
@@ -0,0 +1,158 @@
+/* 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.concept.engine.permission
+
+/**
+ * Represents a permission request, used when engines need access to protected
+ * resources. Every request must be handled by either calling [grant] or [reject].
+ */
+interface PermissionRequest {
+ /**
+ * The origin URI which caused the permissions to be requested.
+ */
+ val uri: String?
+
+ /**
+ * A unique identifier for the request.
+ */
+ val id: String
+
+ /**
+ * List of requested permissions.
+ */
+ val permissions: List<Permission>
+
+ /**
+ * Grants the provided permissions, or all requested permissions, if none
+ * are provided.
+ *
+ * @param permissions the permissions to grant.
+ */
+ fun grant(permissions: List<Permission> = this.permissions)
+
+ /**
+ * Grants this permission request if the provided predicate is true
+ * for any of the requested permissions.
+ *
+ * @param predicate predicate to test for.
+ * @return true if the permission request was granted, otherwise false.
+ */
+ fun grantIf(predicate: (Permission) -> Boolean): Boolean {
+ return if (permissions.any(predicate)) {
+ this.grant()
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Rejects the requested permissions.
+ */
+ fun reject()
+
+ fun containsVideoAndAudioSources() = false
+}
+
+/**
+ * Represents all the different supported permission types.
+ *
+ * @property id an optional native engine-specific ID of this permission.
+ * @property desc an optional description of what this permission type is for.
+ * @property name permission name allowing to easily identify and differentiate one from the other.
+ */
+@Suppress("UndocumentedPublicClass")
+sealed class Permission {
+ abstract val id: String?
+ abstract val desc: String?
+ val name: String = with(this::class.java) {
+ // Using the canonicalName is safer - see https://github.com/mozilla-mobile/android-components/pull/10810
+ // simpleName is used as a backup to the avoid not null assertion (!!) operator.
+ canonicalName?.substringAfterLast('.') ?: simpleName
+ }
+
+ data class ContentAudioCapture(
+ override val id: String? = "ContentAudioCapture",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentAudioMicrophone(
+ override val id: String? = "ContentAudioMicrophone",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentAudioOther(
+ override val id: String? = "ContentAudioOther",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentGeoLocation(
+ override val id: String? = "ContentGeoLocation",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentNotification(
+ override val id: String? = "ContentNotification",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentProtectedMediaId(
+ override val id: String? = "ContentProtectedMediaId",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentVideoCamera(
+ override val id: String? = "ContentVideoCamera",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentVideoCapture(
+ override val id: String? = "ContentVideoCapture",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentVideoScreen(
+ override val id: String? = "ContentVideoScreen",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentVideoOther(
+ override val id: String? = "ContentVideoOther",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentAutoPlayAudible(
+ override val id: String? = "ContentAutoPlayAudible",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentAutoPlayInaudible(
+ override val id: String? = "ContentAutoPlayInaudible",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentPersistentStorage(
+ override val id: String? = "ContentPersistentStorage",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentMediaKeySystemAccess(
+ override val id: String? = "ContentMediaKeySystemAccess",
+ override val desc: String? = "",
+ ) : Permission()
+ data class ContentCrossOriginStorageAccess(
+ override val id: String? = "ContentCrossOriginStorageAccess",
+ override val desc: String? = "",
+ ) : Permission()
+
+ data class AppCamera(
+ override val id: String? = "AppCamera",
+ override val desc: String? = "",
+ ) : Permission()
+ data class AppAudio(
+ override val id: String? = "AppAudio",
+ override val desc: String? = "",
+ ) : Permission()
+ data class AppLocationCoarse(
+ override val id: String? = "AppLocationCoarse",
+ override val desc: String? = "",
+ ) : Permission()
+ data class AppLocationFine(
+ override val id: String? = "AppLocationFine",
+ override val desc: String? = "",
+ ) : Permission()
+
+ data class Generic(
+ override val id: String? = "Generic",
+ override val desc: String? = "",
+ ) : Permission()
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissions.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissions.kt
new file mode 100644
index 0000000000..146f796b95
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissions.kt
@@ -0,0 +1,96 @@
+/* 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.concept.engine.permission
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission
+
+/**
+ * A site permissions and its state.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class SitePermissions(
+ val origin: String,
+ val location: Status = NO_DECISION,
+ val notification: Status = NO_DECISION,
+ val microphone: Status = NO_DECISION,
+ val camera: Status = NO_DECISION,
+ val bluetooth: Status = NO_DECISION,
+ val localStorage: Status = NO_DECISION,
+ val autoplayAudible: AutoplayStatus = AutoplayStatus.BLOCKED,
+ val autoplayInaudible: AutoplayStatus = AutoplayStatus.ALLOWED,
+ val mediaKeySystemAccess: Status = NO_DECISION,
+ val crossOriginStorageAccess: Status = NO_DECISION,
+ val savedAt: Long,
+) : Parcelable {
+ enum class Status(
+ val id: Int,
+ ) {
+ BLOCKED(-1), NO_DECISION(0), ALLOWED(1);
+
+ fun isAllowed() = this == ALLOWED
+
+ fun doNotAskAgain() = this == ALLOWED || this == BLOCKED
+
+ fun toggle(): Status = when (this) {
+ BLOCKED, NO_DECISION -> ALLOWED
+ ALLOWED -> BLOCKED
+ }
+
+ /**
+ * Converts from [SitePermissions.Status] to [AutoplayStatus].
+ */
+ fun toAutoplayStatus(): AutoplayStatus {
+ return when (this) {
+ NO_DECISION, BLOCKED -> AutoplayStatus.BLOCKED
+ ALLOWED -> AutoplayStatus.ALLOWED
+ }
+ }
+ }
+
+ /**
+ * An enum that represents the status that autoplay can have.
+ */
+ enum class AutoplayStatus(val id: Int) {
+ BLOCKED(Status.BLOCKED.id), ALLOWED(Status.ALLOWED.id);
+
+ /**
+ * Indicates if the status is allowed.
+ */
+ fun isAllowed() = this == ALLOWED
+
+ /**
+ * Convert from a AutoplayStatus to Status.
+ */
+ fun toStatus(): Status {
+ return when (this) {
+ BLOCKED -> Status.BLOCKED
+ ALLOWED -> Status.ALLOWED
+ }
+ }
+ }
+
+ /**
+ * Gets the current status for a [Permission] type
+ */
+ operator fun get(permissionType: Permission): Status {
+ return when (permissionType) {
+ Permission.MICROPHONE -> microphone
+ Permission.BLUETOOTH -> bluetooth
+ Permission.CAMERA -> camera
+ Permission.LOCAL_STORAGE -> localStorage
+ Permission.NOTIFICATION -> notification
+ Permission.LOCATION -> location
+ Permission.AUTOPLAY_AUDIBLE -> autoplayAudible.toStatus()
+ Permission.AUTOPLAY_INAUDIBLE -> autoplayInaudible.toStatus()
+ Permission.MEDIA_KEY_SYSTEM_ACCESS -> mediaKeySystemAccess
+ Permission.STORAGE_ACCESS -> crossOriginStorageAccess
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissionsStorage.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissionsStorage.kt
new file mode 100644
index 0000000000..79f4dc6ac9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissionsStorage.kt
@@ -0,0 +1,82 @@
+/* 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.concept.engine.permission
+
+import androidx.paging.DataSource
+
+/**
+ * Represents a storage to store [SitePermissions].
+ */
+interface SitePermissionsStorage {
+ /**
+ * Persists the [sitePermissions] provided as a parameter.
+ * @param sitePermissions the [sitePermissions] to be stored.
+ * @param request the [PermissionRequest] to be stored, default to null.
+ * @param private indicates if the [SitePermissions] belongs to a private session.
+ */
+ suspend fun save(sitePermissions: SitePermissions, request: PermissionRequest? = null, private: Boolean)
+
+ /**
+ * Saves the permission temporarily until the user navigates away.
+ * @param request The requested permission to be save temporarily.
+ */
+ fun saveTemporary(request: PermissionRequest? = null) = Unit
+
+ /**
+ * Clears any temporary permissions.
+ */
+ fun clearTemporaryPermissions() = Unit
+
+ /**
+ * Replaces an existing SitePermissions with the values of [sitePermissions] provided as a parameter.
+ * @param sitePermissions the sitePermissions to be updated.
+ * @param private indicates if the [SitePermissions] belongs to a private session.
+ */
+ suspend fun update(sitePermissions: SitePermissions, private: Boolean)
+
+ /**
+ * Finds all SitePermissions that match the [origin].
+ * @param origin the site to be used as filter in the search.
+ * @param private indicates if the [origin] belongs to a private session.
+ */
+ suspend fun findSitePermissionsBy(
+ origin: String,
+ includeTemporary: Boolean = false,
+ private: Boolean,
+ ): SitePermissions?
+
+ /**
+ * Deletes all sitePermissions that match the sitePermissions provided as a parameter.
+ * @param sitePermissions the sitePermissions to be deleted from the storage.
+ * @param private indicates if the [SitePermissions] belongs to a private session.
+ */
+ suspend fun remove(sitePermissions: SitePermissions, private: Boolean)
+
+ /**
+ * Deletes all sitePermissions sitePermissions.
+ */
+ suspend fun removeAll()
+
+ /**
+ * Returns all sitePermissions in the store.
+ */
+ suspend fun all(): List<SitePermissions>
+
+ /**
+ * Returns all saved [SitePermissions] instances as a [DataSource.Factory].
+ *
+ * A consuming app can transform the data source into a `LiveData<PagedList>` of when using RxJava2 into a
+ * `Flowable<PagedList>` or `Observable<PagedList>`, that can be observed.
+ *
+ * - https://developer.android.com/topic/libraries/architecture/paging/data
+ * - https://developer.android.com/topic/libraries/architecture/paging/ui
+ */
+ suspend fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissions>
+
+ enum class Permission {
+ MICROPHONE, BLUETOOTH, CAMERA, LOCAL_STORAGE, NOTIFICATION, LOCATION, AUTOPLAY_AUDIBLE,
+ AUTOPLAY_INAUDIBLE, MEDIA_KEY_SYSTEM_ACCESS, STORAGE_ACCESS
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/Choice.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/Choice.kt
new file mode 100644
index 0000000000..185c8c9267
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/Choice.kt
@@ -0,0 +1,63 @@
+/* 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.concept.engine.prompt
+
+import android.os.Parcel
+import android.os.Parcelable
+
+/**
+ * Value type that represents a select option, optgroup or menuitem html element.
+ *
+ * @property id of the option, optgroup or menuitem.
+ * @property enable indicate if item should be selectable or not.
+ * @property label The label for displaying the option, optgroup or menuitem.
+ * @property selected Indicate if the item should be pre-selected.
+ * @property isASeparator Indicating if the item should be a menu separator (only valid for menus).
+ * @property children Sub-items in a group, or null if not a group.
+ */
+data class Choice(
+ val id: String,
+ var enable: Boolean = true,
+ var label: String,
+ var selected: Boolean = false,
+ val isASeparator:
+ Boolean = false,
+ val children: Array<Choice>? = null,
+) : Parcelable {
+
+ val isGroupType get() = children != null
+
+ internal constructor(parcel: Parcel) : this(
+ parcel.readString() ?: "",
+ parcel.readByte() != 0.toByte(),
+ parcel.readString() ?: "",
+ parcel.readByte() != 0.toByte(),
+ parcel.readByte() != 0.toByte(),
+ parcel.createTypedArray(CREATOR),
+ )
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeString(id)
+ parcel.writeByte(if (enable) 1 else 0)
+ parcel.writeString(label)
+ parcel.writeByte(if (selected) 1 else 0)
+ parcel.writeByte(if (isASeparator) 1 else 0)
+ parcel.writeTypedArray(children, flags)
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ companion object CREATOR : Parcelable.Creator<Choice> {
+ override fun createFromParcel(parcel: Parcel): Choice {
+ return Choice(parcel)
+ }
+
+ override fun newArray(size: Int): Array<Choice?> {
+ return arrayOfNulls(size)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt
new file mode 100644
index 0000000000..fccfad6018
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt
@@ -0,0 +1,445 @@
+/* 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.concept.engine.prompt
+
+import android.content.Context
+import android.net.Uri
+import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Level
+import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Method
+import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type
+import mozilla.components.concept.identitycredential.Account
+import mozilla.components.concept.identitycredential.Provider
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import java.util.UUID
+
+/**
+ * Value type that represents a request for showing a native dialog for prompt web content.
+ *
+ * @param shouldDismissOnLoad Whether or not the dialog should automatically be dismissed when a new page is loaded.
+ * Defaults to `true`.
+ * @param uid [PromptRequest] unique identifier. Defaults to a random UUID.
+ * (This two parameters, though present in all subclasses are not evaluated in subclasses equals() calls)
+ */
+sealed class PromptRequest(
+ val shouldDismissOnLoad: Boolean = true,
+ val uid: String = UUID.randomUUID().toString(),
+) {
+ /**
+ * Value type that represents a request for a single choice prompt.
+ * @property choices All the possible options.
+ * @property onConfirm A callback indicating which option was selected.
+ * @property onDismiss A callback executed when dismissed.
+ */
+ data class SingleChoice(
+ val choices: Array<Choice>,
+ val onConfirm: (Choice) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for a multiple choice prompt.
+ * @property choices All the possible options.
+ * @property onConfirm A callback indicating witch options has been selected.
+ * @property onDismiss A callback executed when dismissed.
+ */
+ data class MultipleChoice(
+ val choices: Array<Choice>,
+ val onConfirm: (Array<Choice>) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for a menu choice prompt.
+ * @property choices All the possible options.
+ * @property onConfirm A callback indicating which option was selected.
+ * @property onDismiss A callback executed when dismissed.
+ */
+ data class MenuChoice(
+ val choices: Array<Choice>,
+ val onConfirm: (Choice) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for an alert prompt.
+ * @property title of the dialog.
+ * @property message the body of the dialog.
+ * @property hasShownManyDialogs tells if this page has shown multiple prompts within a short period of time.
+ * @property onConfirm tells the web page if it should continue showing alerts or not.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class Alert(
+ val title: String,
+ val message: String,
+ val hasShownManyDialogs: Boolean = false,
+ val onConfirm: (Boolean) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * BeforeUnloadPrompt represents the onbeforeunload prompt.
+ * This prompt is shown when a user is leaving a website and there is formation pending to be saved.
+ * For more information see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload.
+ * @property title of the dialog.
+ * @property onLeave callback to notify that the user wants leave the site.
+ * @property onStay callback to notify that the user wants stay in the site.
+ */
+ data class BeforeUnload(
+ val title: String,
+ val onLeave: () -> Unit,
+ val onStay: () -> Unit,
+ ) : PromptRequest()
+
+ /**
+ * Value type that represents a request for a save credit card prompt.
+ * @property creditCard the [CreditCardEntry] to save or update.
+ * @property onConfirm callback that is called when the user confirms the save credit card request.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class SaveCreditCard(
+ val creditCard: CreditCardEntry,
+ val onConfirm: (CreditCardEntry) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(shouldDismissOnLoad = false), Dismissible
+
+ /**
+ * Value type that represents Identity Credential request prompts.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ sealed class IdentityCredential(
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(shouldDismissOnLoad = false), Dismissible {
+ /**
+ * Value type that represents Identity Credential request for selecting a [Provider] prompt.
+ * @property providers A list of providers which the user could select from.
+ * @property onConfirm callback to let the page know the user selected a provider.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class SelectProvider(
+ val providers: List<Provider>,
+ val onConfirm: (Provider) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : IdentityCredential(onDismiss), Dismissible
+
+ /**
+ * Value type that represents Identity Credential request for selecting an [Account] prompt.
+ * @property accounts A list of accounts which the user could select from.
+ * @property providerName The name of the provider that will be used for the login
+ * @property onConfirm callback to let the page know the user selected an account.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class SelectAccount(
+ val accounts: List<Account>,
+ val provider: Provider,
+ val onConfirm: (Account) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : IdentityCredential(onDismiss), Dismissible
+
+ /**
+ * Value type that represents Identity Credential request for a privacy policy prompt.
+ * @property privacyPolicyUrl A The URL where the policy for using this provider is hosted.
+ * @property termsOfServiceUrl The URL where the terms of service for using this provider are.
+ * @property providerDomain The domain of the provider.
+ * @property host The host of the provider.
+ * @property icon A base64 string for given icon for the provider; may be null.
+ * @property onConfirm callback to let the page know the user have confirmed or not the privacy policy.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class PrivacyPolicy(
+ val privacyPolicyUrl: String,
+ val termsOfServiceUrl: String,
+ val providerDomain: String,
+ val host: String,
+ val icon: String?,
+ val onConfirm: (Boolean) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : IdentityCredential(onDismiss), Dismissible
+ }
+
+ /**
+ * Value type that represents a request for a select credit card prompt.
+ * @property creditCards a list of [CreditCardEntry]s to select from.
+ * @property onConfirm callback that is called when the user confirms the credit card selection.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class SelectCreditCard(
+ val creditCards: List<CreditCardEntry>,
+ val onConfirm: (CreditCardEntry) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for a save login prompt.
+ * @property hint a value that helps to determine the appropriate prompting behavior.
+ * @property logins a list of logins that are associated with the current domain.
+ * @property onConfirm callback that is called when the user wants to save the login.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class SaveLoginPrompt(
+ val hint: Int,
+ val logins: List<LoginEntry>,
+ val onConfirm: (LoginEntry) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(shouldDismissOnLoad = false), Dismissible
+
+ /**
+ * Value type that represents a request for a select login prompt.
+ * @property logins a list of logins that are associated with the current domain.
+ * @property generatedPassword the suggested strong password that was generated.
+ * @property onConfirm callback that is called when the user wants to save the login.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class SelectLoginPrompt(
+ val logins: List<Login>,
+ val generatedPassword: String?,
+ val onConfirm: (Login) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for a select address prompt.
+ *
+ * This prompt is triggered by the user focusing on an address field.
+ *
+ * @property addresses List of addresses for the user to choose from.
+ * @property onConfirm Callback used to confirm the selected address.
+ * @property onDismiss Callback used to dismiss the address prompt.
+ */
+ data class SelectAddress(
+ val addresses: List<Address>,
+ val onConfirm: (Address) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for an alert prompt to enter a message.
+ * @property title title of the dialog.
+ * @property inputLabel the label of the field the user should fill.
+ * @property inputValue the default value of the field.
+ * @property hasShownManyDialogs tells if this page has shown multiple prompts within a short period of time.
+ * @property onConfirm tells the web page if it should continue showing alerts or not.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class TextPrompt(
+ val title: String,
+ val inputLabel: String,
+ val inputValue: String,
+ val hasShownManyDialogs: Boolean = false,
+ val onConfirm: (Boolean, String) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for a date prompt for picking a year, month, and day.
+ * @property title of the dialog.
+ * @property initialDate date that dialog should be set by default.
+ * @property minimumDate date allow to be selected.
+ * @property maximumDate date allow to be selected.
+ * @property type indicate which [Type] of selection de user wants.
+ * @property onConfirm callback that is called when the date is selected.
+ * @property onClear callback that is called when the user requests the picker to be clear up.
+ * @property onDismiss A callback executed when dismissed.
+ */
+ @Suppress("LongParameterList")
+ class TimeSelection(
+ val title: String,
+ val initialDate: java.util.Date,
+ val minimumDate: java.util.Date?,
+ val maximumDate: java.util.Date?,
+ val stepValue: String? = null,
+ val type: Type = Type.DATE,
+ val onConfirm: (java.util.Date) -> Unit,
+ val onClear: () -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible {
+ enum class Type {
+ DATE, DATE_AND_TIME, TIME, MONTH
+ }
+ }
+
+ /**
+ * Value type that represents a request for a selecting one or multiple files.
+ * @property mimeTypes a set of allowed mime types. Only these file types can be selected.
+ * @property isMultipleFilesSelection true if the user can select more that one file false otherwise.
+ * @property captureMode indicates if the local media capturing capabilities should be used,
+ * such as the camera or microphone.
+ * @property onSingleFileSelected callback to notify that the user has selected a single file.
+ * @property onMultipleFilesSelected callback to notify that the user has selected multiple files.
+ * @property onDismiss callback to notify that the user has canceled the file selection.
+ */
+ data class File(
+ val mimeTypes: Array<out String>,
+ val isMultipleFilesSelection: Boolean = false,
+ val captureMode: FacingMode = FacingMode.NONE,
+ val onSingleFileSelected: (Context, Uri) -> Unit,
+ val onMultipleFilesSelected: (Context, Array<Uri>) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible {
+
+ /**
+ * @deprecated Use the new primary constructor.
+ */
+ constructor(
+ mimeTypes: Array<out String>,
+ isMultipleFilesSelection: Boolean,
+ onSingleFileSelected: (Context, Uri) -> Unit,
+ onMultipleFilesSelected: (Context, Array<Uri>) -> Unit,
+ onDismiss: () -> Unit,
+ ) : this(
+ mimeTypes,
+ isMultipleFilesSelection,
+ FacingMode.NONE,
+ onSingleFileSelected,
+ onMultipleFilesSelected,
+ onDismiss,
+ )
+
+ enum class FacingMode {
+ NONE, ANY, FRONT_CAMERA, BACK_CAMERA
+ }
+ companion object {
+ /**
+ * Default default directory name for temporary uploads.
+ */
+ const val DEFAULT_UPLOADS_DIR_NAME = "/uploads"
+ }
+ }
+
+ /**
+ * Value type that represents a request for an authentication prompt.
+ * For more related info take a look at
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication>MDN docs</a>
+ * @property uri The URI for the auth request or null if unknown.
+ * @property title of the dialog.
+ * @property message the body of the dialog.
+ * @property userName default value provide for this session.
+ * @property password default value provide for this session.
+ * @property method type of authentication, valid values [Method.HOST] and [Method.PROXY].
+ * @property level indicates the level of security of the authentication like [Level.NONE],
+ * [Level.SECURED] and [Level.PASSWORD_ENCRYPTED].
+ * @property onlyShowPassword indicates if the dialog should only include a password field.
+ * @property previousFailed indicates if this request is the result of a previous failed attempt to login.
+ * @property isCrossOrigin indicates if this request is from a cross-origin sub-resource.
+ * @property onConfirm callback to indicate the user want to start the authentication flow.
+ * @property onDismiss callback to indicate the user dismissed this request.
+ */
+ data class Authentication(
+ val uri: String?,
+ val title: String,
+ val message: String,
+ val userName: String,
+ val password: String,
+ val method: Method,
+ val level: Level,
+ val onlyShowPassword: Boolean = false,
+ val previousFailed: Boolean = false,
+ val isCrossOrigin: Boolean = false,
+ val onConfirm: (String, String) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible {
+
+ enum class Level {
+ NONE, PASSWORD_ENCRYPTED, SECURED
+ }
+
+ enum class Method {
+ HOST, PROXY
+ }
+ }
+
+ /**
+ * Value type that represents a request for a selecting one or multiple files.
+ * @property defaultColor true if the user can select more that one file false otherwise.
+ * @property onConfirm callback to notify that the user has selected a color.
+ * @property onDismiss callback to notify that the user has canceled the dialog.
+ */
+ data class Color(
+ val defaultColor: String,
+ val onConfirm: (String) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for showing a pop-pup prompt.
+ * This occurs when content attempts to open a new window,
+ * in a way that doesn't appear to be the result of user input.
+ *
+ * @property targetUri the uri that the page is trying to open.
+ * @property onAllow callback to notify that the user wants to open the [targetUri].
+ * @property onDeny callback to notify that the user doesn't want to open the [targetUri].
+ */
+ data class Popup(
+ val targetUri: String,
+ val onAllow: () -> Unit,
+ val onDeny: () -> Unit,
+ override val onDismiss: () -> Unit = { onDeny() },
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for showing a
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm>confirm prompt</a>.
+ *
+ * The prompt can have up to three buttons, they could be positive, negative and neutral.
+ *
+ * @property title of the dialog.
+ * @property message the body of the dialog.
+ * @property hasShownManyDialogs tells if this page has shown multiple prompts within a short period of time.
+ * @property positiveButtonTitle optional title for the positive button.
+ * @property negativeButtonTitle optional title for the negative button.
+ * @property neutralButtonTitle optional title for the neutral button.
+ * @property onConfirmPositiveButton callback to notify that the user has clicked the positive button.
+ * @property onConfirmNegativeButton callback to notify that the user has clicked the negative button.
+ * @property onConfirmNeutralButton callback to notify that the user has clicked the neutral button.
+ * @property onDismiss callback to notify that the user has canceled the dialog.
+ */
+ data class Confirm(
+ val title: String,
+ val message: String,
+ val hasShownManyDialogs: Boolean = false,
+ val positiveButtonTitle: String = "",
+ val negativeButtonTitle: String = "",
+ val neutralButtonTitle: String = "",
+ val onConfirmPositiveButton: (Boolean) -> Unit,
+ val onConfirmNegativeButton: (Boolean) -> Unit,
+ val onConfirmNeutralButton: (Boolean) -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request to share data.
+ * https://w3c.github.io/web-share/
+ * @property data Share data containing title, text, and url of the request.
+ * @property onSuccess Callback to notify that the user hared with another app successfully.
+ * @property onFailure Callback to notify that the user attempted to share with another app, but it failed.
+ * @property onDismiss Callback to notify that the user aborted the share.
+ */
+ data class Share(
+ val data: ShareData,
+ val onSuccess: () -> Unit,
+ val onFailure: () -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ /**
+ * Value type that represents a request for a repost prompt.
+ *
+ * This prompt is shown whenever refreshing or navigating to a page needs resubmitting
+ * POST data that has been submitted already.
+ *
+ * @property onConfirm callback to notify that the user wants to refresh the webpage.
+ * @property onDismiss callback to notify that the user wants stay in the current webpage and not refresh it.
+ */
+ data class Repost(
+ val onConfirm: () -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : PromptRequest(), Dismissible
+
+ interface Dismissible {
+ val onDismiss: () -> Unit
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/ShareData.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/ShareData.kt
new file mode 100644
index 0000000000..fe264e4fe5
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/ShareData.kt
@@ -0,0 +1,24 @@
+/* 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.concept.engine.prompt
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Represents data to share for the Web Share and Web Share Target APIs.
+ * https://w3c.github.io/web-share/
+ * @property title Title for the share request.
+ * @property text Text for the share request.
+ * @property url URL for the share request.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class ShareData(
+ val title: String? = null,
+ val text: String? = null,
+ val url: String? = null,
+) : Parcelable
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/request/RequestInterceptor.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/request/RequestInterceptor.kt
new file mode 100644
index 0000000000..54acc1e66b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/request/RequestInterceptor.kt
@@ -0,0 +1,107 @@
+/* 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.concept.engine.request
+
+import android.content.Intent
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+
+/**
+ * Interface for classes that want to intercept load requests to allow custom behavior.
+ */
+interface RequestInterceptor {
+
+ /**
+ * An alternative response for an intercepted request.
+ */
+ sealed class InterceptionResponse {
+ data class Content(
+ val data: String,
+ val mimeType: String = "text/html",
+ val encoding: String = "UTF-8",
+ ) : InterceptionResponse()
+
+ /**
+ * The intercepted request URL to load.
+ *
+ * @param url The URL of the request.
+ * @param flags The [LoadUrlFlags] to use when loading the provided [url].
+ * @param additionalHeaders The extra headers to use when loading the provided [url].
+ */
+ data class Url(
+ val url: String,
+ val flags: LoadUrlFlags = LoadUrlFlags.select(
+ LoadUrlFlags.EXTERNAL,
+ LoadUrlFlags.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE,
+ ),
+ val additionalHeaders: Map<String, String>? = null,
+ ) : InterceptionResponse()
+
+ data class AppIntent(val appIntent: Intent, val url: String) : InterceptionResponse()
+
+ /**
+ * Deny request without further action.
+ */
+ object Deny : InterceptionResponse()
+ }
+
+ /**
+ * An alternative response for an error request.
+ * Used to load an encoded URI directly.
+ */
+ data class ErrorResponse(val uri: String)
+
+ /**
+ * A request to open an URI. This is called before each page load to allow
+ * providing custom behavior.
+ *
+ * @param engineSession The engine session that initiated the callback.
+ * @param uri The URI of the request.
+ * @param lastUri The URI of the last request.
+ * @param hasUserGesture If the request is triggered by the user then true, else false.
+ * @param isSameDomain If the request is the same domain as the current URL then true, else false.
+ * @param isRedirect If the request is due to a redirect then true, else false.
+ * @param isDirectNavigation If the request is due to a direct navigation then true, else false.
+ * @param isSubframeRequest If the request is coming from a subframe then true, else false.
+ * @return An [InterceptionResponse] object containing alternative content
+ * or an alternative URL. Null if the original request should continue to
+ * be loaded.
+ */
+ @Suppress("LongParameterList")
+ fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): InterceptionResponse? = null
+
+ /**
+ * A request that the engine wasn't able to handle that resulted in an error.
+ *
+ * @param session The engine session that initiated the callback.
+ * @param errorType The error that was provided by the engine related to the
+ * type of error caused.
+ * @param uri The uri that resulted in the error.
+ * @return An [ErrorResponse] object containing content to display for the
+ * provided error type.
+ */
+ fun onErrorRequest(session: EngineSession, errorType: ErrorType, uri: String?): ErrorResponse? = null
+
+ /**
+ * Returns whether or not this [RequestInterceptor] should intercept load
+ * requests initiated by the app (via direct calls to [EngineSession.loadUrl]).
+ * All other requests triggered by users interacting with web content
+ * (e.g. following links) or redirects will always be intercepted.
+ *
+ * @return true if app initiated requests should be intercepted,
+ * otherwise false. Defaults to false.
+ */
+ fun interceptsAppInitiatedRequests() = false
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/search/SearchRequest.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/search/SearchRequest.kt
new file mode 100644
index 0000000000..8af1bf0c1e
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/search/SearchRequest.kt
@@ -0,0 +1,10 @@
+/* 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.concept.engine.search
+
+/**
+ * Value type that represents a request for showing a search to the user.
+ */
+data class SearchRequest(val isPrivate: Boolean, val query: String)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/selection/SelectionActionDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/selection/SelectionActionDelegate.kt
new file mode 100644
index 0000000000..c358bcfe40
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/selection/SelectionActionDelegate.kt
@@ -0,0 +1,47 @@
+/* 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.concept.engine.selection
+
+/**
+ * Generic delegate for handling the context menu that is shown when text is selected.
+ */
+interface SelectionActionDelegate {
+ /**
+ * Gets Strings representing all possible selection actions.
+ *
+ * @returns String IDs for each action that could possibly be shown in the context menu. This
+ * array must include all actions, available or not, and must not change over the class lifetime.
+ */
+ fun getAllActions(): Array<String>
+
+ /**
+ * Checks if an action can be shown on a new selection context menu.
+ *
+ * @returns whether or not the the custom action with the id of [id] is currently available
+ * which may be informed by [selectedText].
+ */
+ fun isActionAvailable(id: String, selectedText: String): Boolean
+
+ /**
+ * Gets a title to be shown in the selection context menu.
+ *
+ * @returns the text that should be shown on the action.
+ */
+ fun getActionTitle(id: String): CharSequence?
+
+ /**
+ * Should perform the action with the id of [id].
+ *
+ * @returns [true] if the action was consumed.
+ */
+ fun performAction(id: String, selectedText: String): Boolean
+
+ /**
+ * Takes in a list of actions and sorts them.
+ *
+ * @returns the sorted list.
+ */
+ fun sortedActions(actions: Array<String>): Array<String>
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/serviceworker/ServiceWorkerDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/serviceworker/ServiceWorkerDelegate.kt
new file mode 100644
index 0000000000..75ee052a1e
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/serviceworker/ServiceWorkerDelegate.kt
@@ -0,0 +1,25 @@
+/* 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.concept.engine.serviceworker
+
+import mozilla.components.concept.engine.EngineSession
+
+/**
+ * Application delegate for handling all service worker requests.
+ */
+interface ServiceWorkerDelegate {
+ /**
+ * Handles requests to open a new tab using the provided [engineSession].
+ * Implementations should not try to load any url, this will be executed by the service worker
+ * through the [engineSession].
+ *
+ * @param engineSession New [EngineSession] in which a service worker will try to load a specific url.
+ *
+ * @return
+ * - `true` when a new tab is created and a service worker is allowed to open an url in it,
+ * - `false` otherwise.
+ */
+ fun addNewTab(engineSession: EngineSession): Boolean
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductAnalysis.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductAnalysis.kt
new file mode 100644
index 0000000000..110f7923e9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductAnalysis.kt
@@ -0,0 +1,51 @@
+/* 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.concept.engine.shopping
+
+/**
+ * Holds the result of the analysis of a shopping product.
+ *
+ * @property productId Product identifier (ASIN/SKU)
+ * @property analysisURL Analysis URL
+ * @property grade Reliability grade for the product's reviews
+ * @property adjustedRating Product rating adjusted to exclude untrusted reviews
+ * @property needsAnalysis Boolean indicating if the analysis is stale
+ * @property pageNotSupported Boolean indicating true if the page is not supported and false if supported
+ * @property notEnoughReviews Boolean indicating if there are not enough reviews
+ * @property lastAnalysisTime Time since the last analysis was performed
+ * @property deletedProductReported Boolean indicating if reported that this product has been deleted
+ * @property deletedProduct Boolean indicating if this product is now deleted
+ * @property highlights Object containing highlights for product
+ */
+data class ProductAnalysis(
+ val productId: String?,
+ val analysisURL: String?,
+ val grade: String?,
+ val adjustedRating: Double?,
+ val needsAnalysis: Boolean,
+ val pageNotSupported: Boolean,
+ val notEnoughReviews: Boolean,
+ val lastAnalysisTime: Long,
+ val deletedProductReported: Boolean,
+ val deletedProduct: Boolean,
+ val highlights: Highlight?,
+)
+
+/**
+ * Contains information about highlights of a product's reviews.
+ *
+ * @property quality Highlights about the quality of a product
+ * @property price Highlights about the price of a product
+ * @property shipping Highlights about the shipping of a product
+ * @property appearance Highlights about the appearance of a product
+ * @property competitiveness Highlights about the competitiveness of a product
+ */
+data class Highlight(
+ val quality: List<String>?,
+ val price: List<String>?,
+ val shipping: List<String>?,
+ val appearance: List<String>?,
+ val competitiveness: List<String>?,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductAnalysisStatus.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductAnalysisStatus.kt
new file mode 100644
index 0000000000..ae104254fd
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductAnalysisStatus.kt
@@ -0,0 +1,16 @@
+/* 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.concept.engine.shopping
+
+/**
+ * Holds the result of the analysis status of a shopping product.
+ *
+ * @property status String indicating the current status of the analysis
+ * @property progress Number indicating the progress of the analysis
+ */
+data class ProductAnalysisStatus(
+ val status: String,
+ val progress: Double,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductRecommendation.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductRecommendation.kt
new file mode 100644
index 0000000000..5ef4e751d8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/shopping/ProductRecommendation.kt
@@ -0,0 +1,32 @@
+/* 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.concept.engine.shopping
+
+/**
+ * Contains information about a product recommendation.
+ *
+ * @property url Url of recommended product.
+ * @property analysisUrl Analysis URL.
+ * @property adjustedRating Adjusted rating.
+ * @property sponsored Whether or not it is a sponsored recommendation.
+ * @property imageUrl Url of product recommendation image.
+ * @property aid Unique identifier for the ad entity.
+ * @property name Name of recommended product.
+ * @property grade Grade of recommended product.
+ * @property price Price of recommended product.
+ * @property currency Currency of recommended product.
+ */
+data class ProductRecommendation(
+ val url: String,
+ val analysisUrl: String,
+ val adjustedRating: Double,
+ val sponsored: Boolean,
+ val imageUrl: String,
+ val aid: String,
+ val name: String,
+ val grade: String,
+ val price: String,
+ val currency: String,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/DetectedLanguages.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/DetectedLanguages.kt
new file mode 100644
index 0000000000..1aca245cb1
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/DetectedLanguages.kt
@@ -0,0 +1,20 @@
+/* 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.concept.engine.translate
+
+/**
+* The representation of a translations detected document and user language.
+*
+* @property documentLangTag The auto-detected language tag of page. Usually used for determining the
+* best guess for translating "from".
+* @property supportedDocumentLang If the translation engine supports the document language.
+* @property userPreferredLangTag The user's preferred language tag. Usually used for determining the
+ * best guess for translating "to".
+*/
+data class DetectedLanguages(
+ val documentLangTag: String? = null,
+ val supportedDocumentLang: Boolean? = false,
+ val userPreferredLangTag: String? = null,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/Language.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/Language.kt
new file mode 100644
index 0000000000..d2a0e8b695
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/Language.kt
@@ -0,0 +1,16 @@
+/* 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.concept.engine.translate
+
+/**
+ * The language container for presenting language information to the user.
+ *
+ * @property code The BCP 47 code that represents the language.
+ * @property localizedDisplayName The translations engine localized display name of the language.
+ */
+data class Language(
+ val code: String,
+ val localizedDisplayName: String? = null,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/LanguageModel.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/LanguageModel.kt
new file mode 100644
index 0000000000..ec9cfa04ee
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/LanguageModel.kt
@@ -0,0 +1,22 @@
+/* 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.concept.engine.translate
+
+/**
+ * The language model container for representing language model state to the user.
+ *
+ * Please note, a single LanguageModel is usually comprised of
+ * an aggregation of multiple machine learning models on the translations engine level. The engine
+ * has already handled this abstraction.
+ *
+ * @property language The specified language the language model set can process.
+ * @property isDownloaded If all the necessary models are downloaded.
+ * @property size The size of the total model download(s).
+ */
+data class LanguageModel(
+ val language: Language? = null,
+ val isDownloaded: Boolean = false,
+ val size: Long? = null,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/LanguageSetting.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/LanguageSetting.kt
new file mode 100644
index 0000000000..d5f742c451
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/LanguageSetting.kt
@@ -0,0 +1,125 @@
+/* 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.concept.engine.translate
+
+/**
+ * The preferences setting a given language may have on the translations engine.
+ *
+ * @param languageSetting The specified language setting.
+ */
+enum class LanguageSetting(private val languageSetting: String) {
+ /**
+ * The translations engine should always expect a given language to be translated and
+ * automatically translate on page load.
+ */
+ ALWAYS("always"),
+
+ /**
+ * The translations engine should offer a given language to be translated. This is the default
+ * setting. Note, this means the language will parallel the global offer setting
+ */
+ OFFER("offer"),
+
+ /**
+ * The translations engine should never offer to translate a given language.
+ */
+ NEVER("never"),
+ ;
+
+ companion object {
+ /**
+ * Convenience method to map a string name to the enumerated type.
+ *
+ * @param languageSetting The specified language setting.
+ */
+ fun fromValue(languageSetting: String): LanguageSetting = when (languageSetting) {
+ "always" -> ALWAYS
+ "offer" -> OFFER
+ "never" -> NEVER
+ else ->
+ throw IllegalArgumentException("The language setting $languageSetting is not mapped.")
+ }
+ }
+
+ /**
+ * Helper function to transform a given [LanguageSetting] setting into its boolean counterpart.
+ *
+ * @param categoryToSetFor The [LanguageSetting] type that we would like to determine the
+ * boolean value for. For example, if trying to calculate a boolean 'isAlways',
+ * [categoryToSetFor] would be [LanguageSetting.ALWAYS].
+ *
+ * @return A boolean that corresponds to the language setting. Will return null if not enough
+ * information is present to make a determination.
+ */
+ fun toBoolean(
+ categoryToSetFor: LanguageSetting,
+ ): Boolean? {
+ when (this) {
+ ALWAYS -> {
+ return when (categoryToSetFor) {
+ ALWAYS -> true
+ // Cannot determine offer without more information
+ OFFER -> null
+ NEVER -> false
+ }
+ }
+
+ OFFER -> {
+ return when (categoryToSetFor) {
+ ALWAYS -> false
+ OFFER -> true
+ NEVER -> false
+ }
+ }
+
+ NEVER -> {
+ return when (categoryToSetFor) {
+ ALWAYS -> false
+ // Cannot determine offer without more information
+ OFFER -> null
+ NEVER -> true
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function to transform a given [LanguageSetting] that represents a category and the given boolean to its
+ * correct [LanguageSetting]. The calling object should be the object to set for.
+ *
+ * For example, if trying to calculate a value for an `isAlways` boolean, then `this` should be [ALWAYS].
+ *
+ * @param value The given [Boolean] to convert to a [LanguageSetting].
+ * @return A language setting that corresponds to the boolean. Will return null if not enough information is present
+ * to make a determination.
+ */
+ fun toLanguageSetting(
+ value: Boolean,
+ ): LanguageSetting? {
+ when (this) {
+ ALWAYS -> {
+ return when (value) {
+ true -> ALWAYS
+ false -> OFFER
+ }
+ }
+
+ OFFER -> {
+ return when (value) {
+ true -> OFFER
+ // Cannot determine if it should be ALWAYS or NEVER without more information
+ false -> null
+ }
+ }
+
+ NEVER -> {
+ return when (value) {
+ true -> NEVER
+ false -> OFFER
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/ModelManagementOptions.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/ModelManagementOptions.kt
new file mode 100644
index 0000000000..eddb8b1672
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/ModelManagementOptions.kt
@@ -0,0 +1,19 @@
+/* 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.concept.engine.translate
+
+/**
+ * The operations that can be performed on a given language model.
+ *
+ * @property languageToManage The BCP 47 language code to manage the models for.
+ * May be null when performing operations not at the "language" scope or level.
+ * @property operation The operation to perform.
+ * @property operationLevel At what scope or level the operations should be performed at.
+ */
+data class ModelManagementOptions(
+ val languageToManage: String? = null,
+ val operation: ModelOperation,
+ val operationLevel: OperationLevel,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/ModelOperation.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/ModelOperation.kt
new file mode 100644
index 0000000000..66ee50227c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/ModelOperation.kt
@@ -0,0 +1,20 @@
+/* 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.concept.engine.translate
+
+/**
+ * The operations that can be performed on a language model.
+ */
+enum class ModelOperation(val operation: String) {
+ /**
+ * Download the model(s).
+ */
+ DOWNLOAD("download"),
+
+ /**
+ * Delete the model(s).
+ */
+ DELETE("delete"),
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/OperationLevel.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/OperationLevel.kt
new file mode 100644
index 0000000000..e39a3239ec
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/OperationLevel.kt
@@ -0,0 +1,26 @@
+/* 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.concept.engine.translate
+
+/**
+ * The level or scope of a model operation.
+ */
+enum class OperationLevel(val operationLevel: String) {
+ /**
+ * Complete the operation for a given language.
+ */
+ LANGUAGE("language"),
+
+ /**
+ * Complete the operation on cache elements.
+ * (Elements that do not fully make a downloaded language package or [LanguageModel].)
+ */
+ CACHE("cache"),
+
+ /**
+ * Complete the operation all models.
+ */
+ ALL("all"),
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationDownloadSize.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationDownloadSize.kt
new file mode 100644
index 0000000000..37782dfde4
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationDownloadSize.kt
@@ -0,0 +1,27 @@
+/* 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.concept.engine.translate
+
+/**
+ * A data class to contain information related to the download size required for a given
+ * translation to/from pair.
+ *
+ * For the translations engine to complete a translation on a specified to/from pair,
+ * first, the necessary ML models must be downloaded to the device.
+ * This class represents the download state of the ML models necessary to translate the
+ * given to/from pair.
+ *
+ * @property fromLanguage The [Language] to translate from on a given translation.
+ * @property toLanguage The [Language] to translate to on a given translation.
+ * @property size The size of the download to perform the translation in bytes. Null means the value has
+ * yet to be received or an error occurred. Zero means no download required or else a model does not exist.
+ * @property error The [TranslationError] reported if an error occurred while fetching the size.
+ */
+data class TranslationDownloadSize(
+ val fromLanguage: Language,
+ val toLanguage: Language,
+ val size: Long? = null,
+ val error: TranslationError? = null,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationEngineState.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationEngineState.kt
new file mode 100644
index 0000000000..1885c650a4
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationEngineState.kt
@@ -0,0 +1,46 @@
+/* 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.concept.engine.translate
+
+/**
+* The representation of the translations engine state.
+*
+* @property detectedLanguages Detected information about preferences and page information.
+* @property error If an error state occurred or an error was reported.
+* @property isEngineReady If the translation engine is primed for use or will need to be loaded.
+* @property requestedTranslationPair The language pair to translate. Usually populated after first request.
+*/
+
+data class TranslationEngineState(
+ val detectedLanguages: DetectedLanguages? = null,
+ val error: String? = null,
+ val isEngineReady: Boolean? = false,
+ val requestedTranslationPair: TranslationPair? = null,
+)
+
+/**
+ * Determines the best initial "to" language based on the translation state and user preferred
+ * languages.
+ *
+ * @param candidateLanguages The language options available to select as a final initial value.
+ * @return The best determined "to" language or null if a determination cannot be made.
+ */
+fun TranslationEngineState.initialToLanguage(candidateLanguages: List<Language>?): Language? {
+ return candidateLanguages?.find {
+ it.code == (requestedTranslationPair?.toLanguage ?: detectedLanguages?.userPreferredLangTag)
+ }
+}
+
+/**
+ * Determines the best initial "from" language based on the translation state and page state.
+ *
+ * @param candidateLanguages The language options available to select as a final initial value.
+ * @return The best determined "from" language or null if a determination cannot be made.
+ */
+fun TranslationEngineState.initialFromLanguage(candidateLanguages: List<Language>?): Language? {
+ return candidateLanguages?.find {
+ it.code == (requestedTranslationPair?.fromLanguage ?: detectedLanguages?.documentLangTag)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationError.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationError.kt
new file mode 100644
index 0000000000..1a1bd12319
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationError.kt
@@ -0,0 +1,175 @@
+/* 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.concept.engine.translate
+
+/**
+ * The types of translation errors that can occur. Has features for determining telemetry error
+ * names and determining if an error needs to be displayed.
+ *
+ * @param errorName The translation error name. The expected use is for telemetry.
+ * @param displayError Signal to determine if we need to specifically display an error for
+ * this given issue. (Some errors should only silently report telemetry or simply revert to the
+ * prior UI state.)
+ * @param cause The original throwable before it was converted into this error state.
+ */
+sealed class TranslationError(
+ val errorName: String,
+ val displayError: Boolean,
+ override val cause: Throwable?,
+) : Throwable(cause = cause) {
+
+ /**
+ * Default error for unexpected issues.
+ *
+ * @param cause The original throwable that lead us to the unknown error state.
+ */
+ class UnknownError(override val cause: Throwable) :
+ TranslationError(errorName = "unknown", displayError = false, cause = cause)
+
+ /**
+ * Default error for unexpected null value received on a non-null translations call.
+ */
+ class UnexpectedNull :
+ TranslationError(errorName = "unexpected-null", displayError = false, cause = null)
+
+ /**
+ * Default error when a translation session coordinator is not available.
+ */
+ class MissingSessionCoordinator :
+ TranslationError(errorName = "missing-session-coordinator", displayError = false, cause = null)
+
+ /**
+ * Translations engine does not work on the device architecture.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class EngineNotSupportedError(override val cause: Throwable?) :
+ TranslationError(errorName = "engine-not-supported", displayError = false, cause = cause)
+
+ /**
+ * Could not determine if the translations engine works on the device architecture.
+ *
+ * @param cause The original [Throwable] before it was converted into this error state.
+ */
+ class UnknownEngineSupportError(override val cause: Throwable?) :
+ TranslationError(errorName = "unknown-engine-support", displayError = false, cause = cause)
+
+ /**
+ * Generic could not compete a translation error.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class CouldNotTranslateError(override val cause: Throwable?) :
+ TranslationError(errorName = "could-not-translate", displayError = true, cause = cause)
+
+ /**
+ * Generic could not restore the page after a translation error.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class CouldNotRestoreError(override val cause: Throwable?) :
+ TranslationError(errorName = "could-not-restore", displayError = false, cause = cause)
+
+ /**
+ * Could not determine the translation download size between a given "to" and "from" language
+ * translation pair.
+ *
+ * @param cause The original [Throwable] before it was converted into this error state.
+ */
+ class CouldNotDetermineDownloadSizeError(override val cause: Throwable?) :
+ TranslationError(
+ errorName = "could-not-determine-translation-download-size",
+ displayError = false,
+ cause = cause,
+ )
+
+ /**
+ * Could not load language options error.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class CouldNotLoadLanguagesError(override val cause: Throwable?) :
+ TranslationError(errorName = "could-not-load-languages", displayError = true, cause = cause)
+
+ /**
+ * Could not load page settings error.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class CouldNotLoadPageSettingsError(override val cause: Throwable?) :
+ TranslationError(errorName = "could-not-load-settings", displayError = false, cause = cause)
+
+ /**
+ * Could not load language settings error.
+ *
+ * @param cause The original [Throwable] before it was converted into this error state.
+ */
+ class CouldNotLoadLanguageSettingsError(override val cause: Throwable?) :
+ TranslationError(errorName = "could-not-load-language-settings", displayError = false, cause = cause)
+
+ /**
+ * Could not load never translate sites error.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class CouldNotLoadNeverTranslateSites(override val cause: Throwable?) :
+ TranslationError(errorName = "could-not-load-never-translate-sites", displayError = false, cause = cause)
+
+ /**
+ * The language is not supported for translation.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class LanguageNotSupportedError(override val cause: Throwable?) :
+ TranslationError(errorName = "language-not-supported", displayError = true, cause = cause)
+
+ /**
+ * Could not retrieve information on the language model.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class ModelCouldNotRetrieveError(override val cause: Throwable?) :
+ TranslationError(
+ errorName = "model-could-not-retrieve",
+ displayError = false,
+ cause = cause,
+ )
+
+ /**
+ * Could not delete the language model.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class ModelCouldNotDeleteError(override val cause: Throwable?) :
+ TranslationError(errorName = "model-could-not-delete", displayError = false, cause = cause)
+
+ /**
+ * Could not download the language model.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class ModelCouldNotDownloadError(override val cause: Throwable?) :
+ TranslationError(
+ errorName = "model-could-not-download",
+ displayError = false,
+ cause = cause,
+ )
+
+ /**
+ * A language is required for language scoped requests.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class ModelLanguageRequiredError(override val cause: Throwable?) :
+ TranslationError(errorName = "model-language-required", displayError = false, cause = cause)
+
+ /**
+ * A download is required and the translate request specified do not download.
+ *
+ * @param cause The original throwable before it was converted into this error state.
+ */
+ class ModelDownloadRequiredError(override val cause: Throwable?) :
+ TranslationError(errorName = "model-download-required", displayError = false, cause = cause)
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationOperation.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationOperation.kt
new file mode 100644
index 0000000000..0f9b62029f
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationOperation.kt
@@ -0,0 +1,48 @@
+/* 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.concept.engine.translate
+
+/**
+ * The operation the translations engine is performing.
+ */
+enum class TranslationOperation {
+ /**
+ * The page should be translated.
+ */
+ TRANSLATE,
+
+ /**
+ * A translated page should be restored.
+ */
+ RESTORE,
+
+ /**
+ * The list of languages that the translation engine should fetch. This includes
+ * the languages supported for translating both "to" and "from" with their BCP-47 language tag
+ * and localized name.
+ */
+ FETCH_SUPPORTED_LANGUAGES,
+
+ /**
+ * The list of available language machine learning translation models the translation engine should fetch.
+ */
+ FETCH_LANGUAGE_MODELS,
+
+ /**
+ * The page related settings the translation engine should fetch.
+ */
+ FETCH_PAGE_SETTINGS,
+
+ /**
+ * Fetch the user preference on whether to offer, always translate, or never translate for
+ * all supported language settings.
+ */
+ FETCH_AUTOMATIC_LANGUAGE_SETTINGS,
+
+ /**
+ * The list of never translate sites the translation engine should fetch.
+ */
+ FETCH_NEVER_TRANSLATE_SITES,
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationOptions.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationOptions.kt
new file mode 100644
index 0000000000..0c17cfcaff
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationOptions.kt
@@ -0,0 +1,15 @@
+/* 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.concept.engine.translate
+
+/**
+ * Translation options that map to the Gecko Translations Options.
+ *
+ * @property downloadModel If the necessary models should be downloaded on request. If false, then
+ * the translation will not complete and throw an exception if the models are not already available.
+ */
+data class TranslationOptions(
+ val downloadModel: Boolean = true,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPageSettingOperation.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPageSettingOperation.kt
new file mode 100644
index 0000000000..82367408b3
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPageSettingOperation.kt
@@ -0,0 +1,32 @@
+/* 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.concept.engine.translate
+
+/**
+ * The container for referring to the different page settings.
+ *
+ * See [TranslationPageSettings] for the corresponding data model
+ */
+enum class TranslationPageSettingOperation {
+ /**
+ * The system should offer a translation on a page.
+ */
+ UPDATE_ALWAYS_OFFER_POPUP,
+
+ /**
+ * The page's always translate language setting.
+ */
+ UPDATE_ALWAYS_TRANSLATE_LANGUAGE,
+
+ /**
+ * The page's never translate language setting.
+ */
+ UPDATE_NEVER_TRANSLATE_LANGUAGE,
+
+ /**
+ * The page's never translate site setting.
+ */
+ UPDATE_NEVER_TRANSLATE_SITE,
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPageSettings.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPageSettings.kt
new file mode 100644
index 0000000000..7c253d6af2
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPageSettings.kt
@@ -0,0 +1,26 @@
+/* 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.concept.engine.translate
+
+/**
+ * Translation settings that relate to the page
+ *
+ * @property alwaysOfferPopup The setting for whether translations should automatically be offered.
+ * When true, the engine will offer to translate the page if the detected translatable page language
+ * is different from the user's preferred languages.
+ * @property alwaysTranslateLanguage The setting for whether the current page language should be
+ * automatically translated or not. When true, the page will automatically be translated by the
+ * translations engine.
+ * @property neverTranslateLanguage The setting for whether the current page language should offer a
+ * translation or not. When true, the engine will not offer a translation.
+ * @property neverTranslateSite The setting for whether the current site should be translated or not.
+ * When true, the engine will not offer a translation on the current host site.
+ */
+data class TranslationPageSettings(
+ val alwaysOfferPopup: Boolean? = null,
+ val alwaysTranslateLanguage: Boolean? = null,
+ val neverTranslateLanguage: Boolean? = null,
+ val neverTranslateSite: Boolean? = null,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPair.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPair.kt
new file mode 100644
index 0000000000..60a848fe5a
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationPair.kt
@@ -0,0 +1,16 @@
+/* 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.concept.engine.translate
+
+/**
+* The representation of the translation state.
+*
+* @property fromLanguage The language the page is translated from originally.
+* @property toLanguage The language the page is translated to that the user knows.
+*/
+data class TranslationPair(
+ val fromLanguage: String? = null,
+ val toLanguage: String? = null,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationSupport.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationSupport.kt
new file mode 100644
index 0000000000..033f55bb39
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationSupport.kt
@@ -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/. */
+
+package mozilla.components.concept.engine.translate
+
+/**
+ * The list of supported languages that may be translated to and translated from. Usually
+ * a given language will be bi-directional (translate both to and from),
+ * but this is not guaranteed, which is why the support response is two lists.
+ *
+ * @property fromLanguages The languages that the machine learning model may translate from.
+ * @property toLanguages The languages that the machine learning model may translate to.
+ */
+data class TranslationSupport(
+ val fromLanguages: List<Language>? = null,
+ val toLanguages: List<Language>? = null,
+)
+
+/**
+ * Convenience method to convert [this.fromLanguages] and [this.toLanguages] to a single language
+ * map for BCP 47 code to [Language] lookup.
+ *
+ * @return A combined map of the language options with the BCP 47 language as the key and the
+ * [Language] object as the value or null.
+ */
+fun TranslationSupport.toLanguageMap(): Map<String, Language>? {
+ val fromLanguagesMap = fromLanguages?.associate { it.code to it }
+ val toLanguagesMap = toLanguages?.associate { it.code to it }
+
+ return if (toLanguagesMap != null && fromLanguagesMap != null) {
+ toLanguagesMap + fromLanguagesMap
+ } else {
+ toLanguagesMap
+ ?: fromLanguagesMap
+ }
+}
+
+/**
+ * Convenience method to find a [Language] given a BCP 47 language code.
+ *
+ * @param languageCode The BCP 47 language code.
+ *
+ * @return The [Language] associated with the language code or null.
+ */
+fun TranslationSupport.findLanguage(languageCode: String): Language? {
+ return toLanguageMap()?.get(languageCode)
+}
+
+/**
+ * Convenience method to convert a language setting map using a BCP 47 code as a key to a map using
+ * [Language] as a key.
+ *
+ * @param languageSettings The map of language settings, where the key, [String], is a BCP 47 code.
+ */
+fun TranslationSupport.mapLanguageSettings(
+ languageSettings: Map<String, LanguageSetting>?,
+): Map<Language?, LanguageSetting>? {
+ return languageSettings?.mapKeys { findLanguage(it.key) }?.filterKeys { it != null }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationsRuntime.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationsRuntime.kt
new file mode 100644
index 0000000000..2f348d30b5
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationsRuntime.kt
@@ -0,0 +1,215 @@
+/* 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.concept.engine.translate
+
+import mozilla.components.concept.engine.EngineSession
+
+private var unsupportedError = "Translations support is not available in this engine."
+
+/**
+ * Entry point for interacting with runtime translation options.
+ */
+interface TranslationsRuntime {
+
+ /**
+ * Checks if the translations engine is supported or not. The engine only
+ * supports certain architectures.
+ *
+ * An example use case is checking if translations options should ever be displayed.
+ *
+ * @param onSuccess Callback invoked when successful with the compatibility status of running
+ * translations.
+ * @param onError Callback invoked if an issue occurred when determining status.
+ */
+ fun isTranslationsEngineSupported(
+ onSuccess: (Boolean) -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Queries what language models are downloaded and will return the download size
+ * for the given language pair or else return an error.
+ *
+ * An example use case is checking how large of a download will occur for a given
+ * specifc translation.
+ *
+ * @param fromLanguage The language the translations engine will use to translate from.
+ * @param toLanguage The language the translations engine will use to translate to.
+ * @param onSuccess Callback invoked if the pair download size was fetched successfully. With
+ * the size in bytes that will be required to complete for the download. Zero bytes indicates
+ * no download is required.
+ * @param onError Callback invoked if an issue occurred when checking sizes.
+ */
+ fun getTranslationsPairDownloadSize(
+ fromLanguage: String,
+ toLanguage: String,
+ onSuccess: (Long) -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Aggregates the states of complete models downloaded. Note, this function does not aggregate
+ * the cache or state of incomplete models downloaded.
+ *
+ * An example use case is listing the current install states of the language models.
+ *
+ * @param onSuccess Callback invoked if the states were correctly aggregated as a list.
+ * @param onError Callback invoked if an issue occurred when aggregating model state.
+ */
+ fun getTranslationsModelDownloadStates(
+ onSuccess: (List<LanguageModel>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Fetches a list of to and from languages supported by the translations engine.
+ *
+ * An example use case is is for populating translation options.
+ *
+ * @param onSuccess Callback invoked if the list of to and from languages was retrieved.
+ * @param onError Callback invoked if an issue occurred.
+ */
+ fun getSupportedTranslationLanguages(
+ onSuccess: (TranslationSupport) -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Use to download and delete complete model sets for a given language. Can bulk update all
+ * models, a given language set, or the cache or incomplete models (models that are not a part
+ * of a complete language set).
+ *
+ * An example use case is for managing deleting and installing model sets.
+ *
+ * @param options The options for the operation.
+ * @param onSuccess Callback invoked if the operation completed successfully.
+ * @param onError Callback invoked if an issue occurred.
+ */
+ fun manageTranslationsLanguageModel(
+ options: ModelManagementOptions,
+ onSuccess: () -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Retrieves the user preferred languages using the app language(s), web requested language(s),
+ * and OS language(s).
+ *
+ * An example use case is presenting translate "to language" options for the user. Note, the
+ * user's predicted first choice is also available via the state of the translation.
+ *
+ * @param onSuccess Callback invoked if the operation completed successfully with a list of user
+ * preferred languages.
+ * @param onError Callback invoked if an issue occurred.
+ */
+ fun getUserPreferredLanguages(
+ onSuccess: (List<String>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Retrieves the user preference on whether they would like translations to offer to translate
+ * on supported pages.
+ *
+ * @return The current translation offer preference value.
+ */
+ fun getTranslationsOfferPopup(): Boolean = throw UnsupportedOperationException(unsupportedError)
+
+ /**
+ * Sets the user preference on whether they would like translations to offer to translate
+ * on supported pages.
+ *
+ * @param offer The popup preference. True if the user would like to receive a popup
+ * recommendation to translate. False if they do not want translations suggestions.
+ */
+ fun setTranslationsOfferPopup(offer: Boolean): Unit =
+ throw UnsupportedOperationException(unsupportedError)
+
+ /**
+ * Gets the user preference on whether to offer, always translate, or never translate for a
+ * given BCP 47 language code. Note, when offer is set, this means the user has not specified
+ * an option or has else opted for default behavior.
+ *
+ * @param languageCode The BCP 47 language code to check the preference for.
+ * @param onSuccess Callback invoked if the operation completed successfully with the
+ * corresponding language setting.
+ * @param onError Callback invoked if an issue occurred.
+ */
+ fun getLanguageSetting(
+ languageCode: String,
+ onSuccess: (LanguageSetting) -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Sets the user preference on whether to offer, always translate, or never translate for a
+ * given BCP 47 language code.
+ *
+ * @param languageCode The BCP 47 language code to check the preference for.
+ * @param languageSetting The language setting for the language.
+ * @param onSuccess Callback invoked if the operation completed successfully with the
+ * corresponding language setting.
+ * @param onError Callback invoked if an issue occurred.
+ */
+ fun setLanguageSetting(
+ languageCode: String,
+ languageSetting: LanguageSetting,
+ onSuccess: () -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Gets the user preference on whether to offer, always translate, or never translate for all
+ * supported languages. Note, when offer is set, this means the user has not specified
+ * an option or has else opted for default behavior.
+ *
+ * @param onSuccess Callback invoked if the operation completed successfully with the
+ * corresponding setting in a map of key of BCP 47 language code and value of LanguageSetting
+ * preference.
+ * @param onError Callback invoked if an issue occurred.
+ */
+ fun getLanguageSettings(
+ onSuccess: (Map<String, LanguageSetting>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Retrieves the list of sites that a user has specified to never translate.
+ *
+ * @param onSuccess Callback invoked if the operation completed successfully with a
+ * display-ready list of URI/URLs.
+ * @param onError Callback invoked if an issue occurred.
+ */
+ fun getNeverTranslateSiteList(
+ onSuccess: (List<String>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+
+ /**
+ * Sets if a given site should be never translated or not. This function is for use when making
+ * global translation settings adjustments to never translate a specified site.
+ *
+ * Note, ideally only use results from {@link [getNeverTranslateSiteList]} to set the
+ * siteURL on this function to ensure correct scope.
+ *
+ * For setting the never translate preference on the currently displayed site, the best practice
+ * is to use {@link [EngineSession.setNeverTranslateSiteSetting]}.
+ *
+ * @param origin The website's URI/URL to set the never translate preference on. Recommend
+ * only using results from {@link getNeverTranslateSiteList} as this parameter to ensure proper
+ * scope. To set the current site, use instead
+ * {@link [EngineSession.setNeverTranslateSiteSetting]}.
+ * @param setting True if the site should never be translated. False if the site should be
+ * translated.
+ * @param onSuccess Callback invoked if the operation completed successfully.
+ * @param onError Callback invoked if an issue occurred.
+ */
+ fun setNeverTranslateSpecifiedSite(
+ origin: String,
+ setting: Boolean,
+ onSuccess: () -> Unit,
+ onError: (Throwable) -> Unit,
+ ): Unit = onError(UnsupportedOperationException(unsupportedError))
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/utils/EngineVersion.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/utils/EngineVersion.kt
new file mode 100644
index 0000000000..07842037c7
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/utils/EngineVersion.kt
@@ -0,0 +1,115 @@
+/* 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.concept.engine.utils
+
+/**
+ * Release type - as compiled - of the engine.
+ */
+enum class EngineReleaseChannel {
+ UNKNOWN,
+ NIGHTLY,
+ BETA,
+ RELEASE,
+}
+
+/**
+ * Data class for engine versions using semantic versioning (major.minor.patch).
+ *
+ * @param major Major version number
+ * @param minor Minor version number
+ * @param patch Patch version number
+ * @param metadata Additional and optional metadata appended to the version number, e.g. for a version number of
+ * "68.0a1" [metadata] will contain "a1".
+ * @param releaseChannel Additional property indicating the release channel of this version.
+ */
+data class EngineVersion(
+ val major: Int,
+ val minor: Int,
+ val patch: Long,
+ val metadata: String? = null,
+ val releaseChannel: EngineReleaseChannel = EngineReleaseChannel.UNKNOWN,
+) {
+ operator fun compareTo(other: EngineVersion): Int {
+ return when {
+ major != other.major -> major - other.major
+ minor != other.minor -> minor - other.minor
+ patch != other.patch -> (patch - other.patch).toInt()
+ metadata != other.metadata -> when {
+ metadata == null -> -1
+ other.metadata == null -> 1
+ else -> metadata.compareTo(other.metadata)
+ }
+ releaseChannel != other.releaseChannel -> releaseChannel.compareTo(other.releaseChannel)
+ else -> 0
+ }
+ }
+
+ /**
+ * Returns true if this version number equals or is higher than the provided [major], [minor], [patch] version
+ * numbers.
+ */
+ fun isAtLeast(major: Int, minor: Int = 0, patch: Long = 0): Boolean {
+ return when {
+ this.major > major -> true
+ this.major < major -> false
+ this.minor > minor -> true
+ this.minor < minor -> false
+ this.patch >= patch -> true
+ else -> false
+ }
+ }
+
+ override fun toString(): String {
+ return buildString {
+ append(major)
+ append(".")
+ append(minor)
+ append(".")
+ append(patch)
+ if (metadata != null) {
+ append(metadata)
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Parses the given [version] string and returns an [EngineVersion]. Returns null if the [version] string could
+ * not be parsed successfully.
+ */
+ @Suppress("MagicNumber", "ReturnCount")
+ fun parse(version: String, releaseChannel: String? = null): EngineVersion? {
+ val majorRegex = "([0-9]+)"
+ val minorRegex = "\\.([0-9]+)"
+ val patchRegex = "(?:\\.([0-9]+))?"
+ val metadataRegex = "([^0-9].*)?"
+ val regex = "$majorRegex$minorRegex$patchRegex$metadataRegex".toRegex()
+ val result = regex.matchEntire(version) ?: return null
+
+ val major = result.groups[1]?.value ?: return null
+ val minor = result.groups[2]?.value ?: return null
+ val patch = result.groups[3]?.value ?: "0"
+ val metadata = result.groups[4]?.value
+ val engineReleaseChannel = when (releaseChannel) {
+ "nightly" -> EngineReleaseChannel.NIGHTLY
+ "beta" -> EngineReleaseChannel.BETA
+ "release" -> EngineReleaseChannel.RELEASE
+ else -> EngineReleaseChannel.UNKNOWN
+ }
+
+ return try {
+ EngineVersion(
+ major.toInt(),
+ minor.toInt(),
+ patch.toLong(),
+ metadata,
+ engineReleaseChannel,
+ )
+ } catch (e: NumberFormatException) {
+ null
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/Action.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/Action.kt
new file mode 100644
index 0000000000..9dd6b02740
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/Action.kt
@@ -0,0 +1,54 @@
+/* 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.concept.engine.webextension
+
+import android.graphics.Bitmap
+
+/**
+ * Value type that represents the state of a browser or page action within a [WebExtension].
+ *
+ * @property title The title of the browser action to be visible in the user interface.
+ * @property enabled Indicates if the browser action should be enabled or disabled.
+ * @property loadIcon A suspending function returning the icon in the provided size.
+ * @property badgeText The browser action's badge text.
+ * @property badgeTextColor The browser action's badge text color.
+ * @property badgeBackgroundColor The browser action's badge background color.
+ * @property onClick A callback to be executed when this browser action is clicked.
+ */
+data class Action(
+ val title: String?,
+ val enabled: Boolean?,
+ val loadIcon: (suspend (Int) -> Bitmap?)?,
+ val badgeText: String?,
+ val badgeTextColor: Int?,
+ val badgeBackgroundColor: Int?,
+ val onClick: () -> Unit,
+) {
+ /**
+ * Returns a copy of this [Action] with the provided override applied e.g. for tab-specific overrides.
+ * If the override is null, the original class is returned without making a new instance.
+ *
+ * @param override the action to use for overriding properties. Note that only the provided
+ * (non-null) properties of the override will be applied, all other properties will remain
+ * unchanged. An extension can send a tab-specific action and only include the properties
+ * it wants to override for the tab.
+ */
+ fun copyWithOverride(override: Action?) = if (override != null) {
+ Action(
+ title = override.title ?: title,
+ enabled = override.enabled ?: enabled,
+ badgeText = override.badgeText ?: badgeText,
+ badgeBackgroundColor = override.badgeBackgroundColor ?: badgeBackgroundColor,
+ badgeTextColor = override.badgeTextColor ?: badgeTextColor,
+ loadIcon = override.loadIcon ?: loadIcon,
+ onClick = override.onClick,
+ )
+ } else {
+ this
+ }
+}
+
+typealias WebExtensionBrowserAction = Action
+typealias WebExtensionPageAction = Action
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/InstallationMethod.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/InstallationMethod.kt
new file mode 100644
index 0000000000..13cbac20d6
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/InstallationMethod.kt
@@ -0,0 +1,20 @@
+/* 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.concept.engine.webextension
+
+/**
+ * The method used to install a [WebExtension].
+ */
+enum class InstallationMethod {
+ /**
+ * Indicates the [WebExtension] was installed from the add-ons manager.
+ */
+ MANAGER,
+
+ /**
+ * Indicates the [WebExtension] was installed from a file.
+ */
+ FROM_FILE,
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt
new file mode 100644
index 0000000000..de5077dda1
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt
@@ -0,0 +1,677 @@
+/* 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.concept.engine.webextension
+
+import android.graphics.Bitmap
+import android.net.Uri
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.Settings
+import org.json.JSONObject
+
+/**
+ * Represents a browser extension based on the WebExtension API:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
+ *
+ * @property id the unique ID of this extension.
+ * @property url the url pointing to a resources path for locating the extension
+ * within the APK file e.g. resource://android/assets/extensions/my_web_ext.
+ * @property supportActions whether or not browser and page actions are handled when
+ * received from the web extension
+ */
+abstract class WebExtension(
+ val id: String,
+ val url: String,
+ val supportActions: Boolean,
+) {
+ /**
+ * Registers a [MessageHandler] for message events from background scripts.
+ *
+ * @param name the name of the native "application". This can either be the
+ * name of an application, web extension or a specific feature in case
+ * the web extension opens multiple [Port]s. There can only be one handler
+ * with this name per extension and the same name has to be used in
+ * JavaScript when calling `browser.runtime.connectNative` or
+ * `browser.runtime.sendNativeMessage`. Note that name must match
+ * /^\w+(\.\w+)*$/).
+ * @param messageHandler the message handler to be notified of messaging
+ * events e.g. a port was connected or a message received.
+ */
+ abstract fun registerBackgroundMessageHandler(name: String, messageHandler: MessageHandler)
+
+ /**
+ * Registers a [MessageHandler] for message events from content scripts.
+ *
+ * @param session the session to be observed / attach the message handler to.
+ * @param name the name of the native "application". This can either be the
+ * name of an application, web extension or a specific feature in case
+ * the web extension opens multiple [Port]s. There can only be one handler
+ * with this name per extension and session, and the same name has to be
+ * used in JavaScript when calling `browser.runtime.connectNative` or
+ * `browser.runtime.sendNativeMessage`. Note that name must match
+ * /^\w+(\.\w+)*$/).
+ * @param messageHandler the message handler to be notified of messaging
+ * events e.g. a port was connected or a message received.
+ */
+ abstract fun registerContentMessageHandler(session: EngineSession, name: String, messageHandler: MessageHandler)
+
+ /**
+ * Checks whether there is an existing content message handler for the provided
+ * session and "application" name.
+ *
+ * @param session the session the message handler was registered for.
+ * @param name the "application" name the message handler was registered for.
+ * @return true if a content message handler is active, otherwise false.
+ */
+ abstract fun hasContentMessageHandler(session: EngineSession, name: String): Boolean
+
+ /**
+ * Returns a connected port with the given name and for the provided
+ * [EngineSession], if one exists.
+ *
+ * @param name the name as provided to connectNative.
+ * @param session (optional) session to check for, null if port is from a
+ * background script.
+ * @return a matching port, or null if none is connected.
+ */
+ abstract fun getConnectedPort(name: String, session: EngineSession? = null): Port?
+
+ /**
+ * Disconnect a [Port] of the provided [EngineSession]. This method has
+ * no effect if there's no connected port with the given name.
+ *
+ * @param name the name as provided to connectNative, see
+ * [registerContentMessageHandler] and [registerBackgroundMessageHandler].
+ * @param session (options) session for which ports should disconnected,
+ * null if port is from a background script.
+ */
+ abstract fun disconnectPort(name: String, session: EngineSession? = null)
+
+ /**
+ * Registers an [ActionHandler] for this web extension. The handler will
+ * be invoked whenever browser and page action defaults change. To listen
+ * for session-specific overrides see registerActionHandler(
+ * EngineSession, ActionHandler).
+ *
+ * @param actionHandler the [ActionHandler] to be invoked when a browser or
+ * page action is received.
+ */
+ abstract fun registerActionHandler(actionHandler: ActionHandler)
+
+ /**
+ * Registers an [ActionHandler] for the provided [EngineSession]. The handler
+ * will be invoked whenever browser and page action overrides are received
+ * for the provided session.
+ *
+ * @param session the [EngineSession] the handler should be registered for.
+ * @param actionHandler the [ActionHandler] to be invoked when a
+ * session-specific browser or page action is received.
+ */
+ abstract fun registerActionHandler(session: EngineSession, actionHandler: ActionHandler)
+
+ /**
+ * Checks whether there is an existing action handler for the provided
+ * session.
+ *
+ * @param session the session the action handler was registered for.
+ * @return true if an action handler is registered, otherwise false.
+ */
+ abstract fun hasActionHandler(session: EngineSession): Boolean
+
+ /**
+ * Registers a [TabHandler] for this web extension. This handler will
+ * be invoked whenever a web extension wants to open a new tab. To listen
+ * for session-specific events (such as [TabHandler.onCloseTab]) use
+ * registerTabHandler(EngineSession, TabHandler) instead.
+ *
+ * @param tabHandler the [TabHandler] to be invoked when the web extension
+ * wants to open a new tab.
+ * @param defaultSettings used to pass default tab settings to any tabs opened by
+ * a web extension.
+ */
+ abstract fun registerTabHandler(tabHandler: TabHandler, defaultSettings: Settings?)
+
+ /**
+ * Registers a [TabHandler] for the provided [EngineSession]. The handler
+ * will be invoked whenever an existing tab should be closed or updated.
+ *
+ * @param tabHandler the [TabHandler] to be invoked when the web extension
+ * wants to update or close an existing tab.
+ */
+ abstract fun registerTabHandler(session: EngineSession, tabHandler: TabHandler)
+
+ /**
+ * Checks whether there is an existing tab handler for the provided
+ * session.
+ *
+ * @param session the session the tab handler was registered for.
+ * @return true if an tab handler is registered, otherwise false.
+ */
+ abstract fun hasTabHandler(session: EngineSession): Boolean
+
+ /**
+ * Returns additional information about this extension.
+ *
+ * @return extension [Metadata], or null if the extension isn't
+ * installed and there is no meta data available.
+ */
+ abstract fun getMetadata(): Metadata?
+
+ /**
+ * Checks whether or not this extension is built-in (packaged with the
+ * APK file) or coming from an external source.
+ */
+ open fun isBuiltIn(): Boolean = Uri.parse(url).scheme == "resource"
+
+ /**
+ * Checks whether or not this extension is enabled.
+ */
+ abstract fun isEnabled(): Boolean
+
+ /**
+ * Checks whether or not this extension is allowed in private browsing.
+ */
+ abstract fun isAllowedInPrivateBrowsing(): Boolean
+
+ /**
+ * Returns the icon of this extension as specified in the extension's manifest:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/icons
+ *
+ * @param size the desired size of the icon. The returned icon will be the closest
+ * available icon to the provided size.
+ */
+ abstract suspend fun loadIcon(size: Int): Bitmap?
+}
+
+/**
+ * A handler for web extension (browser and page) actions.
+ *
+ * Page action support will be addressed in:
+ * https://github.com/mozilla-mobile/android-components/issues/4470
+ */
+interface ActionHandler {
+
+ /**
+ * Invoked when a browser action is defined or updated.
+ *
+ * @param extension the extension that defined the browser action.
+ * @param session the [EngineSession] if this action is to be updated for a
+ * specific session, or null if this is to set a new default value.
+ * @param action the browser action as [Action].
+ */
+ fun onBrowserAction(extension: WebExtension, session: EngineSession?, action: Action) = Unit
+
+ /**
+ * Invoked when a page action is defined or updated.
+ *
+ * @param extension the extension that defined the browser action.
+ * @param session the [EngineSession] if this action is to be updated for a
+ * specific session, or null if this is to set a new default value.
+ * @param action the [Action]
+ */
+ fun onPageAction(extension: WebExtension, session: EngineSession?, action: Action) = Unit
+
+ /**
+ * Invoked when a browser or page action wants to toggle a popup view.
+ *
+ * @param extension the extension that defined the browser or page action.
+ * @param action the action as [Action].
+ * @return the [EngineSession] that was used for displaying the popup,
+ * or null if the popup was closed.
+ */
+ fun onToggleActionPopup(extension: WebExtension, action: Action): EngineSession? = null
+}
+
+/**
+ * A handler for all messaging related events, usable for both content and
+ * background scripts.
+ *
+ * [Port]s are exposed to consumers (higher level components) because
+ * how ports are used, how many there are and how messages map to it
+ * is feature-specific and depends on the design of the web extension.
+ * Therefore it makes most sense to let the extensions (higher-level
+ * features) deal with the management of ports.
+ */
+interface MessageHandler {
+
+ /**
+ * Invoked when a [Port] was connected as a result of a
+ * `browser.runtime.connectNative` call in JavaScript.
+ *
+ * @param port the connected port.
+ */
+ fun onPortConnected(port: Port) = Unit
+
+ /**
+ * Invoked when a [Port] was disconnected or the corresponding session was
+ * destroyed.
+ *
+ * @param port the disconnected port.
+ */
+ fun onPortDisconnected(port: Port) = Unit
+
+ /**
+ * Invoked when a message was received on the provided port.
+ *
+ * @param message the received message, either be a primitive type
+ * or a org.json.JSONObject.
+ * @param port the port the message was received on.
+ */
+ fun onPortMessage(message: Any, port: Port) = Unit
+
+ /**
+ * Invoked when a message was received as a result of a
+ * `browser.runtime.sendNativeMessage` call in JavaScript.
+ *
+ * @param message the received message, either be a primitive type
+ * or a org.json.JSONObject.
+ * @param source the session this message originated from if from a content
+ * script, otherwise null.
+ * @return the response to be sent for this message, either a primitive
+ * type or a org.json.JSONObject, null if no response should be sent.
+ */
+ fun onMessage(message: Any, source: EngineSession?): Any? = Unit
+}
+
+/**
+ * A handler for all tab related events (triggered by browser.tabs.* methods).
+ */
+interface TabHandler {
+
+ /**
+ * Invoked when a web extension attempts to open a new tab via
+ * browser.tabs.create.
+ *
+ * @param webExtension The [WebExtension] that wants to open the tab.
+ * @param engineSession an instance of engine session to open a new tab with.
+ * @param active whether or not the new tab should be active/selected.
+ * @param url the target url to be loaded in a new tab.
+ */
+ fun onNewTab(webExtension: WebExtension, engineSession: EngineSession, active: Boolean, url: String) = Unit
+
+ /**
+ * Invoked when a web extension attempts to update a tab via
+ * browser.tabs.update.
+ *
+ * @param webExtension The [WebExtension] that wants to update the tab.
+ * @param engineSession an instance of engine session to open a new tab with.
+ * @param active whether or not the new tab should be active/selected.
+ * @param url the (optional) target url to be loaded in a new tab if it has changed.
+ * @return true if the tab was updated, otherwise false.
+ */
+ fun onUpdateTab(webExtension: WebExtension, engineSession: EngineSession, active: Boolean, url: String?) = false
+
+ /**
+ * Invoked when a web extension attempts to close a tab via
+ * browser.tabs.remove.
+ *
+ * @param webExtension The [WebExtension] that wants to remove the tab.
+ * @param engineSession then engine session of the tab to be closed.
+ * @return true if the tab was closed, otherwise false.
+ */
+ fun onCloseTab(webExtension: WebExtension, engineSession: EngineSession) = false
+}
+
+/**
+ * Represents a port for exchanging messages:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port
+ */
+abstract class Port(val engineSession: EngineSession? = null) {
+
+ /**
+ * Sends a message to this port.
+ *
+ * @param message the message to send.
+ */
+ abstract fun postMessage(message: JSONObject)
+
+ /**
+ * Returns the name of this port.
+ */
+ abstract fun name(): String
+
+ /**
+ * Returns the URL of the port sender.
+ */
+ abstract fun senderUrl(): String
+
+ /**
+ * Disconnects this port.
+ */
+ abstract fun disconnect()
+}
+
+/**
+ * Provides information about a [WebExtension].
+ */
+data class Metadata(
+ /**
+ * Version string:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version
+ */
+ val version: String,
+
+ /**
+ * Required extension permissions:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#API_permissions
+ */
+ val permissions: List<String>,
+
+ /**
+ * Optional permissions requested or granted to this extension:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/optional_permissions
+ */
+ val optionalPermissions: List<String>,
+
+ /**
+ * Optional permissions granted to this extension:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/optional_permissions
+ */
+ val grantedOptionalPermissions: List<String>,
+
+ /**
+ * Optional origin permissions requested or granted to this extension:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/optional_permissions
+ */
+ val optionalOrigins: List<String>,
+
+ /**
+ * Optional origin permissions granted to this extension:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/optional_permissions
+ */
+ val grantedOptionalOrigins: List<String>,
+ /**
+ * Required host permissions:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#Host_permissions
+ */
+ val hostPermissions: List<String>,
+
+ /**
+ * Name of the extension:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/name
+ */
+ val name: String?,
+
+ /**
+ * Description of the extension:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/description
+ */
+ val description: String?,
+
+ /**
+ * Name of the extension developer:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/developer
+ */
+ val developerName: String?,
+
+ /**
+ * Url of the developer:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/developer
+ */
+ val developerUrl: String?,
+
+ /**
+ * Url of extension's homepage:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/homepage_url
+ */
+ val homepageUrl: String?,
+
+ /**
+ * Options page:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui
+ */
+ val optionsPageUrl: String?,
+
+ /**
+ * Whether or not the options page should be opened in a new tab:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui#syntax
+ */
+ val openOptionsPageInTab: Boolean,
+
+ /**
+ * Describes the reason (or reasons) why an extension is disabled.
+ */
+ val disabledFlags: DisabledFlags,
+
+ /**
+ * Base URL for pages of this extension. Can be used to determine if a page
+ * is from / belongs to this extension.
+ */
+ val baseUrl: String,
+
+ /**
+ * The full description of this extension.
+ */
+ val fullDescription: String?,
+
+ /**
+ * The URL used to install this extension.
+ */
+ val downloadUrl: String?,
+
+ /**
+ * The string representation of the date that this extension was most recently updated
+ * (simplified ISO 8601 format).
+ */
+ val updateDate: String?,
+
+ /**
+ * The average rating of this extension.
+ */
+ val averageRating: Float,
+
+ /**
+ * The link to the review page for this extension.
+ */
+ val reviewUrl: String?,
+
+ /**
+ * The average rating of this extension.
+ */
+ val reviewCount: Int,
+
+ /**
+ * The creator name of this extension.
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/developer
+ */
+ val creatorName: String?,
+
+ /**
+ * The creator url of this extension.
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/developer
+ */
+ val creatorUrl: String?,
+
+ /**
+ * Whether or not this extension is temporary i.e. installed using a debug tool
+ * such as web-ext, and won't be retained when the application exits.
+ */
+ val temporary: Boolean = false,
+
+ /**
+ * The URL to the detail page of this extension.
+ */
+ val detailUrl: String?,
+
+ /**
+ * Indicates how this extension works with private browsing windows.
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/incognito
+ */
+ val incognito: Incognito,
+)
+
+/**
+ * Provides additional information about why an extension is being enabled or disabled.
+ */
+@Suppress("MagicNumber")
+enum class EnableSource(val id: Int) {
+ /**
+ * The extension is enabled or disabled by the user.
+ */
+ USER(1),
+
+ /**
+ * The extension is enabled or disabled by the application based
+ * on available support.
+ */
+ APP_SUPPORT(1 shl 1),
+}
+
+/**
+ * Flags to check for different reasons why an extension is disabled.
+ */
+class DisabledFlags internal constructor(val value: Int) {
+ companion object {
+ const val USER: Int = 1 shl 1
+ const val BLOCKLIST: Int = 1 shl 2
+ const val APP_SUPPORT: Int = 1 shl 3
+ const val SIGNATURE: Int = 1 shl 4
+ const val APP_VERSION: Int = 1 shl 5
+
+ /**
+ * Selects a combination of flags.
+ *
+ * @param flags the flags to select.
+ */
+ fun select(vararg flags: Int) = DisabledFlags(flags.sum())
+ }
+
+ /**
+ * Checks if the provided flag is set.
+ *
+ * @param flag the flag to check.
+ */
+ fun contains(flag: Int) = (value and flag) != 0
+}
+
+/**
+ * Incognito values that control how an extension works with private browsing windows.
+ */
+enum class Incognito {
+ /**
+ * The extension will see events from private and non-private windows and tabs.
+ */
+ SPANNING,
+
+ /**
+ * The extension will be split between private and non-private windows.
+ */
+ SPLIT,
+
+ /**
+ * Private tabs and windows are invisible to the extension.
+ */
+ NOT_ALLOWED,
+
+ ;
+
+ companion object {
+ /**
+ * Safely returns an Incognito value based on the input nullable string.
+ */
+ fun fromString(value: String?): Incognito {
+ return when (value) {
+ "split" -> SPLIT
+ "not_allowed" -> NOT_ALLOWED
+ else -> SPANNING
+ }
+ }
+ }
+}
+
+/**
+ * Returns whether or not the extension is disabled because it is unsupported.
+ */
+fun WebExtension.isUnsupported(): Boolean {
+ val flags = getMetadata()?.disabledFlags
+ return flags?.contains(DisabledFlags.APP_SUPPORT) == true
+}
+
+/**
+ * Returns whether or not the extension is disabled because it has been blocklisted.
+ */
+fun WebExtension.isBlockListed(): Boolean {
+ val flags = getMetadata()?.disabledFlags
+ return flags?.contains(DisabledFlags.BLOCKLIST) == true
+}
+
+/**
+ * Returns whether the extension is disabled because it isn't correctly signed.
+ */
+fun WebExtension.isDisabledUnsigned(): Boolean {
+ val flags = getMetadata()?.disabledFlags
+ return flags?.contains(DisabledFlags.SIGNATURE) == true
+}
+
+/**
+ * Returns whether the extension is disabled because it isn't compatible with the application version.
+ */
+fun WebExtension.isDisabledIncompatible(): Boolean {
+ val flags = getMetadata()?.disabledFlags
+ return flags?.contains(DisabledFlags.APP_VERSION) == true
+}
+
+/**
+ * An unexpected event that occurs when trying to perform an action on the extension like
+ * (but not exclusively) installing/uninstalling, removing or updating.
+ */
+open class WebExtensionException(throwable: Throwable, open val isRecoverable: Boolean = true) : Exception(throwable)
+
+/**
+ * An unexpected event that occurs when installing an extension.
+ */
+sealed class WebExtensionInstallException(
+ open val extensionName: String? = null,
+ throwable: Throwable,
+ override val isRecoverable: Boolean = true,
+) : WebExtensionException(throwable) {
+ /**
+ * The extension install was canceled by the user.
+ */
+ class UserCancelled(override val extensionName: String? = null, throwable: Throwable) :
+ WebExtensionInstallException(throwable = throwable)
+
+ /**
+ * The extension install was cancelled because the extension is blocklisted.
+ */
+ class Blocklisted(override val extensionName: String? = null, throwable: Throwable) :
+ WebExtensionInstallException(throwable = throwable)
+
+ /**
+ * The extension install was cancelled because the downloaded file
+ * seems to be corrupted in some way.
+ */
+ class CorruptFile(throwable: Throwable) :
+ WebExtensionInstallException(throwable = throwable, extensionName = null)
+
+ /**
+ * The extension install was cancelled because the file must be signed and isn't.
+ */
+ class NotSigned(throwable: Throwable) :
+ WebExtensionInstallException(throwable = throwable, extensionName = null)
+
+ /**
+ * The extension install was cancelled because it is incompatible.
+ */
+ class Incompatible(override val extensionName: String? = null, throwable: Throwable) :
+ WebExtensionInstallException(throwable = throwable)
+
+ /**
+ * The extension install failed because of a network error.
+ */
+ class NetworkFailure(override val extensionName: String? = null, throwable: Throwable) :
+ WebExtensionInstallException(throwable = throwable)
+
+ /**
+ * The extension install failed with an unknown error.
+ */
+ class Unknown(override val extensionName: String? = null, throwable: Throwable) :
+ WebExtensionInstallException(throwable = throwable)
+
+ /**
+ * The extension install failed because the extension type is not supported.
+ */
+ class UnsupportedAddonType(override val extensionName: String? = null, throwable: Throwable) :
+ WebExtensionInstallException(throwable = throwable)
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.kt
new file mode 100644
index 0000000000..fce18e3863
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.kt
@@ -0,0 +1,177 @@
+/* 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.concept.engine.webextension
+
+import mozilla.components.concept.engine.EngineSession
+
+/**
+ * Notifies applications or other components of engine events related to web
+ * extensions e.g. an extension was installed, or an extension wants to open
+ * a new tab.
+ */
+interface WebExtensionDelegate {
+
+ /**
+ * Invoked when a web extension was installed successfully.
+ *
+ * @param extension The installed extension.
+ */
+ fun onInstalled(extension: WebExtension) = Unit
+
+ /**
+ * Invoked when a web extension was uninstalled successfully.
+ *
+ * @param extension The uninstalled extension.
+ */
+ fun onUninstalled(extension: WebExtension) = Unit
+
+ /**
+ * Invoked when a web extension was enabled successfully.
+ *
+ * @param extension The enabled extension.
+ */
+ fun onEnabled(extension: WebExtension) = Unit
+
+ /**
+ * Invoked when a web extension was disabled successfully.
+ *
+ * @param extension The disabled extension.
+ */
+ fun onDisabled(extension: WebExtension) = Unit
+
+ /**
+ * Invoked when a web extension was started successfully.
+ *
+ * @param extension The extension that has completed its startup.
+ */
+ fun onReady(extension: WebExtension) = Unit
+
+ /**
+ * Invoked when a web extension in private browsing allowed is set.
+ *
+ * @param extension the modified [WebExtension] instance.
+ */
+ fun onAllowedInPrivateBrowsingChanged(extension: WebExtension) = Unit
+
+ /**
+ * Invoked when a web extension attempts to open a new tab via
+ * browser.tabs.create. Note that browser.tabs.update and browser.tabs.remove
+ * can only be observed using session-specific handlers,
+ * see [WebExtension.registerTabHandler].
+ *
+ * @param extension The [WebExtension] that wants to open a new tab.
+ * @param engineSession an instance of engine session to open a new tab with.
+ * @param active whether or not the new tab should be active/selected.
+ * @param url the target url to be loaded in a new tab.
+ */
+ fun onNewTab(extension: WebExtension, engineSession: EngineSession, active: Boolean, url: String) = Unit
+
+ /**
+ * Invoked when a web extension defines a browser action. To listen for session-specific
+ * overrides of [Action]s and other action-specific events (e.g. opening a popup)
+ * see [WebExtension.registerActionHandler].
+ *
+ * @param extension The [WebExtension] defining the browser action.
+ * @param action the defined browser [Action].
+ */
+ fun onBrowserActionDefined(extension: WebExtension, action: Action) = Unit
+
+ /**
+ * Invoked when a web extension defines a page action. To listen for session-specific
+ * overrides of [Action]s and other action-specific events (e.g. opening a popup)
+ * see [WebExtension.registerActionHandler].
+ *
+ * @param extension The [WebExtension] defining the browser action.
+ * @param action the defined page [Action].
+ */
+ fun onPageActionDefined(extension: WebExtension, action: Action) = Unit
+
+ /**
+ * Invoked when a browser or page action wants to toggle a popup view.
+ *
+ * @param extension The [WebExtension] that wants to display the popup.
+ * @param engineSession The [EngineSession] to use for displaying the popup.
+ * @param action the [Action] that defines the popup.
+ * @return the [EngineSession] used to display the popup, or null if no popup
+ * was displayed.
+ */
+ fun onToggleActionPopup(
+ extension: WebExtension,
+ engineSession: EngineSession,
+ action: Action,
+ ): EngineSession? = null
+
+ /**
+ * Invoked during installation of a [WebExtension] to confirm the required permissions.
+ *
+ * @param extension the extension being installed. The required permissions can be
+ * accessed using [WebExtension.getMetadata] and [Metadata.permissions].
+ * @param onPermissionsGranted A callback to indicate whether the user has granted the [extension] permissions
+ * @return whether or not installation should process i.e. the permissions have been
+ * granted.
+ */
+ fun onInstallPermissionRequest(
+ extension: WebExtension,
+ onPermissionsGranted: ((Boolean) -> Unit),
+ ) = Unit
+
+ /**
+ * Invoked whenever the installation of a [WebExtension] failed.
+ *
+ * @param extension extension the extension that failed to be installed. It can be null when the
+ * extension couldn't be downloaded or the extension couldn't be parsed for example.
+ * @param exception the reason why the installation failed.
+ */
+ fun onInstallationFailedRequest(
+ extension: WebExtension?,
+ exception: WebExtensionInstallException,
+ ) = Unit
+
+ /**
+ * Invoked when a web extension has changed its permissions while trying to update to a
+ * new version. This requires user interaction as the updated extension will not be installed,
+ * until the user grants the new permissions.
+ *
+ * @param current The current [WebExtension].
+ * @param updated The update [WebExtension] that requires extra permissions.
+ * @param newPermissions Contains a list of all the new permissions.
+ * @param onPermissionsGranted A callback to indicate if the new permissions from the [updated] extension
+ * are granted or not.
+ */
+ fun onUpdatePermissionRequest(
+ current: WebExtension,
+ updated: WebExtension,
+ newPermissions: List<String>,
+ onPermissionsGranted: ((Boolean) -> Unit),
+ ) = Unit
+
+ /**
+ * Invoked when a web extension requests optional permissions. This requires user interaction since the
+ * user needs to grant or revoke these optional permissions.
+ *
+ * @param extension The [WebExtension].
+ * @param permissions The list of all the optional permissions.
+ * @param onPermissionsGranted A callback to indicate if the optional permissions have been granted or not.
+ */
+ fun onOptionalPermissionsRequest(
+ extension: WebExtension,
+ permissions: List<String>,
+ onPermissionsGranted: ((Boolean) -> Unit),
+ ) = Unit
+
+ /**
+ * Invoked when the list of installed extensions has been updated in the engine
+ * (the web extension runtime). This happens as a result of debugging tools (e.g
+ * web-ext) installing temporary extensions. It does not happen in the regular flow
+ * of installing / uninstalling extensions by the user.
+ */
+ fun onExtensionListUpdated() = Unit
+
+ /**
+ * Invoked when the extension process spawning has been disabled. This can occur because
+ * it has been killed or crashed too many times. A client should determine what to do next.
+ */
+ fun onDisabledExtensionProcessSpawning() = Unit
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionRuntime.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionRuntime.kt
new file mode 100644
index 0000000000..534da7e56e
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionRuntime.kt
@@ -0,0 +1,220 @@
+/* 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.concept.engine.webextension
+
+import mozilla.components.concept.engine.CancellableOperation
+import java.lang.UnsupportedOperationException
+
+/**
+ * Entry point for interacting with the web extensions.
+ */
+interface WebExtensionRuntime {
+
+ /**
+ * Installs the provided built-in extension in this engine.
+ *
+ * @param id the unique ID of the extension.
+ * @param url the url pointing to either a resources path for locating the extension
+ * within the APK file (e.g. resource://android/assets/extensions/my_web_ext) or to a
+ * local (e.g. resource://android/assets/extensions/my_web_ext.xpi) XPI file. An error
+ * is thrown if a non-resource URL is passed.
+ * @param onSuccess (optional) callback invoked if the extension was installed successfully,
+ * providing access to the [WebExtension] object for bi-directional messaging.
+ * @param onError (optional) callback invoked if there was an error installing the extension.
+ * This callback is invoked with an [UnsupportedOperationException] in case the engine doesn't
+ * have web extension support.
+ */
+ fun installBuiltInWebExtension(
+ id: String,
+ url: String,
+ onSuccess: ((WebExtension) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { _ -> },
+ ): CancellableOperation {
+ onError(UnsupportedOperationException("Web extension support is not available in this engine"))
+ return CancellableOperation.Noop()
+ }
+
+ /**
+ * Installs a [WebExtension] from the provided [url] in this engine.
+ *
+ * @param url the url pointing to an XPI file. An error is thrown when a resource URL is passed.
+ * @param onSuccess (optional) callback invoked if the extension was installed successfully,
+ * providing access to the [WebExtension] object for bi-directional messaging.
+ * @param installationMethod (optional) the method used to install a [WebExtension].
+ * @param onError (optional) callback invoked if there was an error installing the extension.
+ * This callback is invoked with an [UnsupportedOperationException] in case the engine doesn't
+ * have web extension support.
+ */
+ fun installWebExtension(
+ url: String,
+ installationMethod: InstallationMethod? = null,
+ onSuccess: ((WebExtension) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { _ -> },
+ ): CancellableOperation {
+ onError(UnsupportedOperationException("Web extension support is not available in this engine"))
+ return CancellableOperation.Noop()
+ }
+
+ /**
+ * Updates the provided [extension] if a new version is available.
+ *
+ * @param extension the extension to be updated.
+ * @param onSuccess (optional) callback invoked if the extension was updated successfully,
+ * providing access to the [WebExtension] object for bi-directional messaging, if null is provided
+ * that means that the [WebExtension] hasn't been change since the last update.
+ * @param onError (optional) callback invoked if there was an error updating the extension.
+ * This callback is invoked with an [UnsupportedOperationException] in case the engine doesn't
+ * have web extension support.
+ */
+ fun updateWebExtension(
+ extension: WebExtension,
+ onSuccess: ((WebExtension?) -> Unit) = { },
+ onError: ((String, Throwable) -> Unit) = { _, _ -> },
+ ): Unit = onError(
+ extension.id,
+ UnsupportedOperationException("Web extension support is not available in this engine"),
+ )
+
+ /**
+ * Uninstalls the provided extension from this engine.
+ *
+ * @param ext the [WebExtension] to uninstall.
+ * @param onSuccess (optional) callback invoked if the extension was uninstalled successfully.
+ * @param onError (optional) callback invoked if there was an error uninstalling the extension.
+ * This callback is invoked with an [UnsupportedOperationException] in case the engine doesn't
+ * have web extension support.
+ */
+ fun uninstallWebExtension(
+ ext: WebExtension,
+ onSuccess: (() -> Unit) = { },
+ onError: ((String, Throwable) -> Unit) = { _, _ -> },
+ ): Unit = onError(ext.id, UnsupportedOperationException("Web extension support is not available in this engine"))
+
+ /**
+ * Lists the currently installed web extensions in this engine.
+ *
+ * @param onSuccess callback invoked with the list of of installed [WebExtension]s.
+ * @param onError (optional) callback invoked if there was an error querying
+ * the installed extensions. This callback is invoked with an [UnsupportedOperationException]
+ * in case the engine doesn't have web extension support.
+ */
+ fun listInstalledWebExtensions(
+ onSuccess: ((List<WebExtension>) -> Unit),
+ onError: ((Throwable) -> Unit) = { },
+ ): Unit = onError(UnsupportedOperationException("Web extension support is not available in this engine"))
+
+ /**
+ * Enables the provided [WebExtension]. If the extension is already enabled the [onSuccess]
+ * callback will be invoked, but this method has no effect on the extension.
+ *
+ * @param extension the extension to enable.
+ * @param source [EnableSource] to indicate why the extension is enabled.
+ * @param onSuccess (optional) callback invoked with the enabled [WebExtension]
+ * @param onError (optional) callback invoked if there was an error enabling
+ * the extensions. This callback is invoked with an [UnsupportedOperationException]
+ * in case the engine doesn't have web extension support.
+ */
+ fun enableWebExtension(
+ extension: WebExtension,
+ source: EnableSource = EnableSource.USER,
+ onSuccess: ((WebExtension) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ): Unit = onError(UnsupportedOperationException("Web extension support is not available in this engine"))
+
+ /**
+ * Add the provided [permissions] and [origins] to the [WebExtension].
+ *
+ * @param extensionId the id of the [WebExtension].
+ * @param permissions [List] the list of permissions to be added to the [WebExtension].
+ * @param origins [List] the list of origins to be added to the [WebExtension].
+ * @param onSuccess (optional) callback invoked when permissions are added to the [WebExtension].
+ * @param onError (optional) callback invoked if there was an error adding permissions to
+ * the [WebExtension]. This callback is invoked with an [UnsupportedOperationException]
+ * in case the engine doesn't have web extension support.
+ */
+ fun addOptionalPermissions(
+ extensionId: String,
+ permissions: List<String> = emptyList(),
+ origins: List<String> = emptyList(),
+ onSuccess: ((WebExtension) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ): Unit = onError(UnsupportedOperationException("Web extension support is not available in this engine"))
+
+ /**
+ * Remove the provided [permissions] and [origins] from the [WebExtension].
+ *
+ * @param extensionId the id of the [WebExtension].
+ * @param permissions [List] the list of permissions to be removed from the [WebExtension].
+ * @param origins [List] the list of origins to be removed from the [WebExtension].
+ * @param onSuccess (optional) callback invoked when permissions are removed from the [WebExtension].
+ * @param onError (optional) callback invoked if there was an error removing permissions from
+ * the [WebExtension]. This callback is invoked with an [UnsupportedOperationException]
+ * in case the engine doesn't have web extension support.
+ */
+ fun removeOptionalPermissions(
+ extensionId: String,
+ permissions: List<String> = emptyList(),
+ origins: List<String> = emptyList(),
+ onSuccess: ((WebExtension) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ): Unit = onError(UnsupportedOperationException("Web extension support is not available in this engine"))
+
+ /**
+ * Disables the provided [WebExtension]. If the extension is already disabled the [onSuccess]
+ * callback will be invoked, but this method has no effect on the extension.
+ *
+ * @param extension the extension to disable.
+ * @param source [EnableSource] to indicate why the extension is disabled.
+ * @param onSuccess (optional) callback invoked with the enabled [WebExtension]
+ * @param onError (optional) callback invoked if there was an error disabling
+ * the installed extensions. This callback is invoked with an [UnsupportedOperationException]
+ * in case the engine doesn't have web extension support.
+ */
+ fun disableWebExtension(
+ extension: WebExtension,
+ source: EnableSource = EnableSource.USER,
+ onSuccess: ((WebExtension) -> Unit),
+ onError: ((Throwable) -> Unit) = { },
+ ): Unit = onError(UnsupportedOperationException("Web extension support is not available in this engine"))
+
+ /**
+ * Registers a [WebExtensionDelegate] to be notified of engine events
+ * related to web extensions
+ *
+ * @param webExtensionDelegate callback to be invoked for web extension events.
+ */
+ fun registerWebExtensionDelegate(
+ webExtensionDelegate: WebExtensionDelegate,
+ ): Unit = throw UnsupportedOperationException("Web extension support is not available in this engine")
+
+ /**
+ * Sets whether the provided [WebExtension] should be allowed to run in private browsing or not.
+ *
+ * @param extension the [WebExtension] instance to modify.
+ * @param allowed true if this extension should be allowed to run in private browsing pages, false otherwise.
+ * @param onSuccess (optional) callback invoked with modified [WebExtension] instance.
+ * @param onError (optional) callback invoked if there was an error setting private browsing preference
+ * the installed extensions. This callback is invoked with an [UnsupportedOperationException]
+ * in case the engine doesn't have web extension support.
+ */
+ fun setAllowedInPrivateBrowsing(
+ extension: WebExtension,
+ allowed: Boolean,
+ onSuccess: ((WebExtension) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ): Unit = throw UnsupportedOperationException("Web extension support is not available in this engine")
+
+ /**
+ * Enable the extensions process spawning.
+ */
+ fun enableExtensionProcessSpawning(): Unit =
+ throw UnsupportedOperationException("Enabling extension process spawning is not available in this engine")
+
+ /**
+ * Disable the extensions process spawning.
+ */
+ fun disableExtensionProcessSpawning(): Unit =
+ throw UnsupportedOperationException("Disabling extension process spawning is not available in this engine")
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotification.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotification.kt
new file mode 100644
index 0000000000..bd77a3af02
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotification.kt
@@ -0,0 +1,44 @@
+/* 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.concept.engine.webnotifications
+
+import android.os.Parcelable
+import mozilla.components.concept.engine.Engine
+
+/**
+ * A notification sent by the Web Notifications API.
+ *
+ * @property title Title of the notification to be displayed in the first row.
+ * @property tag Tag used to identify the notification.
+ * @property body Body of the notification to be displayed in the second row.
+ * @property sourceUrl The URL of the page or Service Worker that generated the notification.
+ * @property iconUrl Large icon url to display in the notification.
+ * Corresponds to [android.app.Notification.Builder.setLargeIcon].
+ * @property direction Preference for text direction.
+ * @property lang language of the notification.
+ * @property requireInteraction Preference flag that indicates the notification should remain.
+ * @property engineNotification Notification instance native to [Engine] which can be
+ * sent across processes or persisted and restored later.
+ * @property timestamp Time when the notification was created.
+ * @property triggeredByWebExtension True if this notification was triggered by a
+ * web extension, otherwise false.
+ * @property privateBrowsing indicates if the [WebNotification] belongs to a private session.
+ * @property silent Whether or not the notification should be silent.
+ */
+data class WebNotification(
+ val title: String?,
+ val tag: String,
+ val body: String?,
+ val sourceUrl: String?,
+ val iconUrl: String?,
+ val direction: String?,
+ val lang: String?,
+ val requireInteraction: Boolean,
+ val engineNotification: Parcelable,
+ val timestamp: Long = System.currentTimeMillis(),
+ val triggeredByWebExtension: Boolean = false,
+ val privateBrowsing: Boolean,
+ val silent: Boolean = true,
+)
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotificationDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotificationDelegate.kt
new file mode 100644
index 0000000000..9a5dca952d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotificationDelegate.kt
@@ -0,0 +1,25 @@
+/* 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.concept.engine.webnotifications
+
+/**
+ * Notifies applications or other components of engine events related to web
+ * notifications e.g. an notification is to be shown or is to be closed
+ */
+interface WebNotificationDelegate {
+ /**
+ * Invoked when a web notification is to be shown.
+ *
+ * @param webNotification The web notification intended to be shown.
+ */
+ fun onShowNotification(webNotification: WebNotification) = Unit
+
+ /**
+ * Invoked when a web notification is to be closed.
+ *
+ * @param webNotification The web notification intended to be closed.
+ */
+ fun onCloseNotification(webNotification: WebNotification) = Unit
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webpush/WebPush.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webpush/WebPush.kt
new file mode 100644
index 0000000000..bc76fd13af
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webpush/WebPush.kt
@@ -0,0 +1,80 @@
+/* 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.concept.engine.webpush
+
+import mozilla.components.concept.engine.Engine
+
+/**
+ * A handler for all WebPush messages and [subscriptions][0] to be delivered to the [Engine].
+ *
+ * [0]: https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription
+ */
+interface WebPushHandler {
+
+ /**
+ * Invoked when a push message has been delivered.
+ *
+ * @param scope The subscription identifier which usually represents the website's URI.
+ * @param message A [ByteArray] message.
+ */
+ fun onPushMessage(scope: String, message: ByteArray?)
+
+ /**
+ * Invoked when a subscription has now changed/expired.
+ */
+ fun onSubscriptionChanged(scope: String) = Unit
+}
+
+/**
+ * A data class representation of the [PushSubscription][0] web specification.
+ *
+ * [0]: https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription
+ *
+ * @param scope The subscription identifier which usually represents the website's URI.
+ * @param endpoint The Web Push endpoint for this subscription.
+ * This is the URL of a web service which implements the Web Push protocol.
+ * @param appServerKey A public key a server will use to send messages to client apps via a push server.
+ * @param publicKey The public key generated, to be provided to the app server for message encryption.
+ * @param authSecret A secret key generated, to be provided to the app server for use in encrypting
+ * and authenticating messages sent to the endpoint.
+ */
+data class WebPushSubscription(
+ val scope: String,
+ val endpoint: String,
+ val appServerKey: ByteArray?,
+ val publicKey: ByteArray,
+ val authSecret: ByteArray,
+) {
+ @Suppress("ComplexMethod")
+ override fun equals(other: Any?): Boolean {
+ /* auto-generated */
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as WebPushSubscription
+
+ if (scope != other.scope) return false
+ if (endpoint != other.endpoint) return false
+ if (appServerKey != null) {
+ if (other.appServerKey == null) return false
+ if (!appServerKey.contentEquals(other.appServerKey)) return false
+ } else if (other.appServerKey != null) return false
+ if (!publicKey.contentEquals(other.publicKey)) return false
+ if (!authSecret.contentEquals(other.authSecret)) return false
+
+ return true
+ }
+
+ @Suppress("MagicNumber")
+ override fun hashCode(): Int {
+ /* auto-generated */
+ var result = scope.hashCode()
+ result = 31 * result + endpoint.hashCode()
+ result = 31 * result + (appServerKey?.contentHashCode() ?: 0)
+ result = 31 * result + publicKey.contentHashCode()
+ result = 31 * result + authSecret.contentHashCode()
+ return result
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webpush/WebPushDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webpush/WebPushDelegate.kt
new file mode 100644
index 0000000000..92d4d2e135
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webpush/WebPushDelegate.kt
@@ -0,0 +1,28 @@
+/* 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.concept.engine.webpush
+
+/**
+ * Notifies applications or other components of engine events related to Web Push notifications.
+ */
+interface WebPushDelegate {
+
+ /**
+ * Requests a WebPush subscription for the given Service Worker scope.
+ */
+ fun onGetSubscription(scope: String, onSubscription: (WebPushSubscription?) -> Unit) = Unit
+
+ /**
+ * Create a WebPush subscription for the given Service Worker scope.
+ */
+ fun onSubscribe(scope: String, serverKey: ByteArray?, onSubscribe: (WebPushSubscription?) -> Unit) = Unit
+
+ /**
+ * Remove a subscription for the given Service Worker scope.
+ *
+ * @return whether the unsubscribe was successful or not.
+ */
+ fun onUnsubscribe(scope: String, onUnsubscribe: (Boolean) -> Unit) = Unit
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/window/WindowRequest.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/window/WindowRequest.kt
new file mode 100644
index 0000000000..1237a9659c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/window/WindowRequest.kt
@@ -0,0 +1,44 @@
+/* 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.concept.engine.window
+
+import mozilla.components.concept.engine.EngineSession
+
+/**
+ * Represents a request to open or close a browser window.
+ */
+interface WindowRequest {
+
+ /**
+ * Describes the different types of window requests.
+ */
+ enum class Type { OPEN, CLOSE }
+
+ /**
+ * The [Type] of this window request, indicating whether to open or
+ * close a window.
+ */
+ val type: Type
+
+ /**
+ * The URL which should be opened in a new window. May be
+ * empty if the request was created from JavaScript (using
+ * window.open()).
+ */
+ val url: String
+
+ /**
+ * Prepares an [EngineSession] for the window request. This is used to
+ * attach state (e.g. a native session or view) to the engine session.
+ *
+ * @return the prepared and ready-to-use [EngineSession].
+ */
+ fun prepare(): EngineSession
+
+ /**
+ * Starts the window request.
+ */
+ fun start() = Unit
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/identitycredential/Account.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/identitycredential/Account.kt
new file mode 100644
index 0000000000..f1a67b471f
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/identitycredential/Account.kt
@@ -0,0 +1,25 @@
+/* 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.concept.identitycredential
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Represents an Identity credential account:
+ * @property id An identifier for this [Account].
+ * @property email The email associated to this [Account].
+ * @property name The name of this [Account].
+ * @property icon An icon for the [Account], normally the profile picture
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class Account(
+ val id: Int,
+ val email: String,
+ val name: String,
+ val icon: String?,
+) : Parcelable
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/identitycredential/Provider.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/identitycredential/Provider.kt
new file mode 100644
index 0000000000..7ebc8521b8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/identitycredential/Provider.kt
@@ -0,0 +1,24 @@
+/* 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.concept.identitycredential
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Represents an Identity credential provider:
+ * @property id An identifier for this [Provider].
+ * @property icon An icon of the provider, normally the logo of the brand.
+ * @property name The name of this [Provider].
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class Provider(
+ val id: Int,
+ val icon: String?,
+ val name: String,
+ val domain: String,
+) : Parcelable
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineSessionTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineSessionTest.kt
new file mode 100644
index 0000000000..440cb53cb3
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineSessionTest.kt
@@ -0,0 +1,1099 @@
+/* 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.concept.engine
+
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.concept.engine.history.HistoryItem
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.shopping.ProductAnalysis
+import mozilla.components.concept.engine.shopping.ProductAnalysisStatus
+import mozilla.components.concept.engine.shopping.ProductRecommendation
+import mozilla.components.concept.engine.translate.TranslationOptions
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.lang.reflect.Modifier
+
+class EngineSessionTest {
+ private val unknownHitResult = HitResult.UNKNOWN("file://foobar")
+
+ @Test
+ fun `registered observers will be notified`() {
+ val session = spy(DummyEngineSession())
+
+ val observer = mock(EngineSession.Observer::class.java)
+ val permissionRequest = mock(PermissionRequest::class.java)
+ val windowRequest = mock(WindowRequest::class.java)
+ session.register(observer)
+
+ val mediaSessionController: MediaSession.Controller = mock()
+ val mediaSessionMetadata: MediaSession.Metadata = mock()
+ val mediaSessionFeature: MediaSession.Feature = mock()
+ val mediaSessionPositionState: MediaSession.PositionState = mock()
+ val mediaSessionElementMetadata: MediaSession.ElementMetadata = mock()
+ val tracker = Tracker("tracker")
+
+ session.notifyInternalObservers { onScrollChange(1234, 4321) }
+ session.notifyInternalObservers { onScrollChange(2345, 5432) }
+ session.notifyInternalObservers { onLocationChange("https://www.mozilla.org", false) }
+ session.notifyInternalObservers { onLocationChange("https://www.firefox.com", false) }
+ session.notifyInternalObservers { onProgress(25) }
+ session.notifyInternalObservers { onProgress(100) }
+ session.notifyInternalObservers { onLoadingStateChange(true) }
+ session.notifyInternalObservers { onSecurityChange(true, "mozilla.org", "issuer") }
+ session.notifyInternalObservers { onTrackerBlockingEnabledChange(true) }
+ session.notifyInternalObservers { onTrackerBlocked(tracker) }
+ session.notifyInternalObservers { onExcludedOnTrackingProtectionChange(true) }
+ session.notifyInternalObservers { onLongPress(unknownHitResult) }
+ session.notifyInternalObservers { onDesktopModeChange(true) }
+ session.notifyInternalObservers { onFind("search") }
+ session.notifyInternalObservers { onFindResult(0, 1, true) }
+ session.notifyInternalObservers { onFullScreenChange(true) }
+ session.notifyInternalObservers { onMetaViewportFitChanged(1) }
+ session.notifyInternalObservers { onContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onCancelContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onAppPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onWindowRequest(windowRequest) }
+ session.notifyInternalObservers { onMediaActivated(mediaSessionController) }
+ session.notifyInternalObservers { onMediaDeactivated() }
+ session.notifyInternalObservers { onMediaMetadataChanged(mediaSessionMetadata) }
+ session.notifyInternalObservers { onMediaFeatureChanged(mediaSessionFeature) }
+ session.notifyInternalObservers { onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING) }
+ session.notifyInternalObservers { onMediaPositionStateChanged(mediaSessionPositionState) }
+ session.notifyInternalObservers { onMediaMuteChanged(true) }
+ session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
+ session.notifyInternalObservers { onCrash() }
+ session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
+ session.notifyInternalObservers { onLaunchIntentRequest("https://www.mozilla.org", null) }
+ session.notifyInternalObservers { onProcessKilled() }
+ session.notifyInternalObservers { onShowDynamicToolbar() }
+
+ verify(observer).onLocationChange("https://www.mozilla.org", false)
+ verify(observer).onLocationChange("https://www.firefox.com", false)
+ verify(observer).onScrollChange(1234, 4321)
+ verify(observer).onScrollChange(2345, 5432)
+ verify(observer).onProgress(25)
+ verify(observer).onProgress(100)
+ verify(observer).onLoadingStateChange(true)
+ verify(observer).onSecurityChange(true, "mozilla.org", "issuer")
+ verify(observer).onTrackerBlockingEnabledChange(true)
+ verify(observer).onTrackerBlocked(tracker)
+ verify(observer).onExcludedOnTrackingProtectionChange(true)
+ verify(observer).onLongPress(unknownHitResult)
+ verify(observer).onDesktopModeChange(true)
+ verify(observer).onFind("search")
+ verify(observer).onFindResult(0, 1, true)
+ verify(observer).onFullScreenChange(true)
+ verify(observer).onMetaViewportFitChanged(1)
+ verify(observer).onAppPermissionRequest(permissionRequest)
+ verify(observer).onContentPermissionRequest(permissionRequest)
+ verify(observer).onCancelContentPermissionRequest(permissionRequest)
+ verify(observer).onWindowRequest(windowRequest)
+ verify(observer).onMediaActivated(mediaSessionController)
+ verify(observer).onMediaDeactivated()
+ verify(observer).onMediaMetadataChanged(mediaSessionMetadata)
+ verify(observer).onMediaFeatureChanged(mediaSessionFeature)
+ verify(observer).onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
+ verify(observer).onMediaPositionStateChanged(mediaSessionPositionState)
+ verify(observer).onMediaMuteChanged(true)
+ verify(observer).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
+ verify(observer).onCrash()
+ verify(observer).onLoadRequest("https://www.mozilla.org", true, true)
+ verify(observer).onLaunchIntentRequest("https://www.mozilla.org", null)
+ verify(observer).onProcessKilled()
+ verify(observer).onShowDynamicToolbar()
+ verifyNoMoreInteractions(observer)
+ }
+
+ @Test
+ fun `observer will not be notified after calling unregister`() {
+ val session = spy(DummyEngineSession())
+ val observer = mock(EngineSession.Observer::class.java)
+ val otherHitResult = HitResult.UNKNOWN("file://foobaz")
+ val permissionRequest = mock(PermissionRequest::class.java)
+ val otherPermissionRequest = mock(PermissionRequest::class.java)
+ val windowRequest = mock(WindowRequest::class.java)
+ val otherWindowRequest = mock(WindowRequest::class.java)
+ val tracker = Tracker("tracker")
+
+ session.register(observer)
+
+ session.notifyInternalObservers { onScrollChange(1234, 4321) }
+ session.notifyInternalObservers { onLocationChange("https://www.mozilla.org", false) }
+ session.notifyInternalObservers { onProgress(25) }
+ session.notifyInternalObservers { onLoadingStateChange(true) }
+ session.notifyInternalObservers { onSecurityChange(true, "mozilla.org", "issuer") }
+ session.notifyInternalObservers { onTrackerBlockingEnabledChange(true) }
+ session.notifyInternalObservers { onTrackerBlocked(tracker) }
+ session.notifyInternalObservers { onLongPress(unknownHitResult) }
+ session.notifyInternalObservers { onDesktopModeChange(true) }
+ session.notifyInternalObservers { onFind("search") }
+ session.notifyInternalObservers { onFindResult(0, 1, true) }
+ session.notifyInternalObservers { onFullScreenChange(true) }
+ session.notifyInternalObservers { onMetaViewportFitChanged(1) }
+ session.notifyInternalObservers { onContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onCancelContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onAppPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onWindowRequest(windowRequest) }
+ session.notifyInternalObservers { onCrash() }
+ session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
+ session.notifyInternalObservers { onLaunchIntentRequest("https://www.mozilla.org", null) }
+ session.notifyInternalObservers { onShowDynamicToolbar() }
+ session.unregister(observer)
+
+ val mediaSessionController: MediaSession.Controller = mock()
+ val mediaSessionMetadata: MediaSession.Metadata = mock()
+ val mediaSessionFeature: MediaSession.Feature = mock()
+ val mediaSessionPositionState: MediaSession.PositionState = mock()
+ val mediaSessionElementMetadata: MediaSession.ElementMetadata = mock()
+
+ session.notifyInternalObservers { onScrollChange(2345, 5432) }
+ session.notifyInternalObservers { onLocationChange("https://www.firefox.com", false) }
+ session.notifyInternalObservers { onProgress(100) }
+ session.notifyInternalObservers { onLoadingStateChange(false) }
+ session.notifyInternalObservers { onSecurityChange(false, "", "") }
+ session.notifyInternalObservers { onTrackerBlocked(tracker) }
+ session.notifyInternalObservers { onTrackerBlockingEnabledChange(false) }
+ session.notifyInternalObservers { onLongPress(otherHitResult) }
+ session.notifyInternalObservers { onDesktopModeChange(false) }
+ session.notifyInternalObservers { onFind("search2") }
+ session.notifyInternalObservers { onFindResult(0, 1, false) }
+ session.notifyInternalObservers { onFullScreenChange(false) }
+ session.notifyInternalObservers { onMetaViewportFitChanged(2) }
+ session.notifyInternalObservers { onContentPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onCancelContentPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onAppPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onWindowRequest(windowRequest) }
+ session.notifyInternalObservers { onMediaActivated(mediaSessionController) }
+ session.notifyInternalObservers { onMediaDeactivated() }
+ session.notifyInternalObservers { onMediaMetadataChanged(mediaSessionMetadata) }
+ session.notifyInternalObservers { onMediaFeatureChanged(mediaSessionFeature) }
+ session.notifyInternalObservers { onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING) }
+ session.notifyInternalObservers { onMediaPositionStateChanged(mediaSessionPositionState) }
+ session.notifyInternalObservers { onMediaMuteChanged(true) }
+ session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
+ session.notifyInternalObservers { onCrash() }
+ session.notifyInternalObservers { onLoadRequest("https://www.mozilla.org", true, true) }
+ session.notifyInternalObservers { onLaunchIntentRequest("https://www.firefox.com", null) }
+ session.notifyInternalObservers { onShowDynamicToolbar() }
+
+ verify(observer).onScrollChange(1234, 4321)
+ verify(observer).onLocationChange("https://www.mozilla.org", false)
+ verify(observer).onProgress(25)
+ verify(observer).onLoadingStateChange(true)
+ verify(observer).onSecurityChange(true, "mozilla.org", "issuer")
+ verify(observer).onTrackerBlockingEnabledChange(true)
+ verify(observer).onTrackerBlocked(tracker)
+ verify(observer).onLongPress(unknownHitResult)
+ verify(observer).onDesktopModeChange(true)
+ verify(observer).onFind("search")
+ verify(observer).onFindResult(0, 1, true)
+ verify(observer).onFullScreenChange(true)
+ verify(observer).onMetaViewportFitChanged(1)
+ verify(observer).onAppPermissionRequest(permissionRequest)
+ verify(observer).onContentPermissionRequest(permissionRequest)
+ verify(observer).onCancelContentPermissionRequest(permissionRequest)
+ verify(observer).onWindowRequest(windowRequest)
+ verify(observer).onCrash()
+ verify(observer).onLoadRequest("https://www.mozilla.org", true, true)
+ verify(observer).onLaunchIntentRequest("https://www.mozilla.org", null)
+ verify(observer).onShowDynamicToolbar()
+ verify(observer, never()).onScrollChange(2345, 5432)
+ verify(observer, never()).onLocationChange("https://www.firefox.com", false)
+ verify(observer, never()).onProgress(100)
+ verify(observer, never()).onLoadingStateChange(false)
+ verify(observer, never()).onSecurityChange(false, "", "")
+ verify(observer, never()).onTrackerBlockingEnabledChange(false)
+ verify(observer, never()).onTrackerBlocked(Tracker("Tracker"))
+ verify(observer, never()).onLongPress(otherHitResult)
+ verify(observer, never()).onDesktopModeChange(false)
+ verify(observer, never()).onFind("search2")
+ verify(observer, never()).onFindResult(0, 1, false)
+ verify(observer, never()).onFullScreenChange(false)
+ verify(observer, never()).onMetaViewportFitChanged(2)
+ verify(observer, never()).onAppPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onContentPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onCancelContentPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onWindowRequest(otherWindowRequest)
+ verify(observer, never()).onMediaActivated(mediaSessionController)
+ verify(observer, never()).onMediaDeactivated()
+ verify(observer, never()).onMediaMetadataChanged(mediaSessionMetadata)
+ verify(observer, never()).onMediaFeatureChanged(mediaSessionFeature)
+ verify(observer, never()).onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
+ verify(observer, never()).onMediaPositionStateChanged(mediaSessionPositionState)
+ verify(observer, never()).onMediaMuteChanged(true)
+ verify(observer, never()).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
+ verify(observer, never()).onLoadRequest("https://www.mozilla.org", false, true)
+ verify(observer, never()).onLaunchIntentRequest("https://www.firefox.com", null)
+ verifyNoMoreInteractions(observer)
+ }
+
+ @Test
+ fun `observers will not be notified after calling unregisterObservers`() {
+ val session = spy(DummyEngineSession())
+ val observer = mock(EngineSession.Observer::class.java)
+ val otherObserver = mock(EngineSession.Observer::class.java)
+ val permissionRequest = mock(PermissionRequest::class.java)
+ val otherPermissionRequest = mock(PermissionRequest::class.java)
+ val windowRequest = mock(WindowRequest::class.java)
+ val otherWindowRequest = mock(WindowRequest::class.java)
+ val otherHitResult = HitResult.UNKNOWN("file://foobaz")
+ val tracker = Tracker("tracker")
+
+ session.register(observer)
+ session.register(otherObserver)
+
+ session.notifyInternalObservers { onScrollChange(1234, 4321) }
+ session.notifyInternalObservers { onLocationChange("https://www.mozilla.org", false) }
+ session.notifyInternalObservers { onProgress(25) }
+ session.notifyInternalObservers { onLoadingStateChange(true) }
+ session.notifyInternalObservers { onSecurityChange(true, "mozilla.org", "issuer") }
+ session.notifyInternalObservers { onTrackerBlockingEnabledChange(true) }
+ session.notifyInternalObservers { onTrackerBlocked(tracker) }
+ session.notifyInternalObservers { onLongPress(unknownHitResult) }
+ session.notifyInternalObservers { onDesktopModeChange(true) }
+ session.notifyInternalObservers { onFind("search") }
+ session.notifyInternalObservers { onFindResult(0, 1, true) }
+ session.notifyInternalObservers { onFullScreenChange(true) }
+ session.notifyInternalObservers { onMetaViewportFitChanged(1) }
+ session.notifyInternalObservers { onContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onCancelContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onAppPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onWindowRequest(windowRequest) }
+ session.notifyInternalObservers { onShowDynamicToolbar() }
+
+ session.unregisterObservers()
+
+ var mediaSessionController: MediaSession.Controller = mock()
+ val mediaSessionMetadata: MediaSession.Metadata = mock()
+ val mediaSessionFeature: MediaSession.Feature = mock()
+ val mediaSessionPositionState: MediaSession.PositionState = mock()
+ val mediaSessionElementMetadata: MediaSession.ElementMetadata = mock()
+
+ session.notifyInternalObservers { onScrollChange(2345, 5432) }
+ session.notifyInternalObservers { onLocationChange("https://www.firefox.com", false) }
+ session.notifyInternalObservers { onProgress(100) }
+ session.notifyInternalObservers { onLoadingStateChange(false) }
+ session.notifyInternalObservers { onSecurityChange(false, "", "") }
+ session.notifyInternalObservers { onTrackerBlocked(tracker) }
+ session.notifyInternalObservers { onTrackerBlockingEnabledChange(false) }
+ session.notifyInternalObservers { onLongPress(otherHitResult) }
+ session.notifyInternalObservers { onDesktopModeChange(false) }
+ session.notifyInternalObservers { onFind("search2") }
+ session.notifyInternalObservers { onFindResult(0, 1, false) }
+ session.notifyInternalObservers { onFullScreenChange(false) }
+ session.notifyInternalObservers { onMetaViewportFitChanged(2) }
+ session.notifyInternalObservers { onContentPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onCancelContentPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onAppPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onWindowRequest(windowRequest) }
+ session.notifyInternalObservers { onMediaActivated(mediaSessionController) }
+ session.notifyInternalObservers { onMediaDeactivated() }
+ session.notifyInternalObservers { onMediaMetadataChanged(mediaSessionMetadata) }
+ session.notifyInternalObservers { onMediaFeatureChanged(mediaSessionFeature) }
+ session.notifyInternalObservers { onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING) }
+ session.notifyInternalObservers { onMediaPositionStateChanged(mediaSessionPositionState) }
+ session.notifyInternalObservers { onMediaMuteChanged(true) }
+ session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
+ session.notifyInternalObservers { onShowDynamicToolbar() }
+
+ verify(observer).onScrollChange(1234, 4321)
+ verify(observer).onLocationChange("https://www.mozilla.org", false)
+ verify(observer).onProgress(25)
+ verify(observer).onLoadingStateChange(true)
+ verify(observer).onSecurityChange(true, "mozilla.org", "issuer")
+ verify(observer).onTrackerBlockingEnabledChange(true)
+ verify(observer).onTrackerBlocked(tracker)
+ verify(observer).onLongPress(unknownHitResult)
+ verify(observer).onDesktopModeChange(true)
+ verify(observer).onFind("search")
+ verify(observer).onFindResult(0, 1, true)
+ verify(observer).onFullScreenChange(true)
+ verify(observer).onMetaViewportFitChanged(1)
+ verify(observer).onAppPermissionRequest(permissionRequest)
+ verify(observer).onContentPermissionRequest(permissionRequest)
+ verify(observer).onCancelContentPermissionRequest(permissionRequest)
+ verify(observer).onWindowRequest(windowRequest)
+ verify(observer).onShowDynamicToolbar()
+ verify(observer, never()).onScrollChange(2345, 5432)
+ verify(observer, never()).onLocationChange("https://www.firefox.com", false)
+ verify(observer, never()).onProgress(100)
+ verify(observer, never()).onLoadingStateChange(false)
+ verify(observer, never()).onSecurityChange(false, "", "")
+ verify(observer, never()).onTrackerBlockingEnabledChange(false)
+ verify(observer, never()).onTrackerBlocked(Tracker("Tracker"))
+ verify(observer, never()).onLongPress(otherHitResult)
+ verify(observer, never()).onDesktopModeChange(false)
+ verify(observer, never()).onFind("search2")
+ verify(observer, never()).onFindResult(0, 1, false)
+ verify(observer, never()).onFullScreenChange(false)
+ verify(observer, never()).onMetaViewportFitChanged(2)
+ verify(observer, never()).onAppPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onContentPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onCancelContentPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onWindowRequest(otherWindowRequest)
+ verify(observer, never()).onMediaActivated(mediaSessionController)
+ verify(observer, never()).onMediaDeactivated()
+ verify(observer, never()).onMediaMetadataChanged(mediaSessionMetadata)
+ verify(observer, never()).onMediaFeatureChanged(mediaSessionFeature)
+ verify(observer, never()).onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
+ verify(observer, never()).onMediaPositionStateChanged(mediaSessionPositionState)
+ verify(observer, never()).onMediaMuteChanged(true)
+ verify(observer, never()).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
+ verify(otherObserver, never()).onScrollChange(2345, 5432)
+ verify(otherObserver, never()).onLocationChange("https://www.firefox.com", false)
+ verify(otherObserver, never()).onProgress(100)
+ verify(otherObserver, never()).onLoadingStateChange(false)
+ verify(otherObserver, never()).onSecurityChange(false, "", "")
+ verify(otherObserver, never()).onTrackerBlockingEnabledChange(false)
+ verify(otherObserver, never()).onTrackerBlocked(Tracker("Tracker"))
+ verify(otherObserver, never()).onLongPress(otherHitResult)
+ verify(otherObserver, never()).onDesktopModeChange(false)
+ verify(otherObserver, never()).onFind("search2")
+ verify(otherObserver, never()).onFindResult(0, 1, false)
+ verify(otherObserver, never()).onFullScreenChange(false)
+ verify(otherObserver, never()).onMetaViewportFitChanged(2)
+ verify(otherObserver, never()).onAppPermissionRequest(otherPermissionRequest)
+ verify(otherObserver, never()).onContentPermissionRequest(otherPermissionRequest)
+ verify(otherObserver, never()).onCancelContentPermissionRequest(otherPermissionRequest)
+ verify(otherObserver, never()).onWindowRequest(otherWindowRequest)
+ verify(otherObserver, never()).onMediaActivated(mediaSessionController)
+ verify(otherObserver, never()).onMediaDeactivated()
+ verify(otherObserver, never()).onMediaMetadataChanged(mediaSessionMetadata)
+ verify(otherObserver, never()).onMediaFeatureChanged(mediaSessionFeature)
+ verify(otherObserver, never()).onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
+ verify(otherObserver, never()).onMediaPositionStateChanged(mediaSessionPositionState)
+ verify(otherObserver, never()).onMediaMuteChanged(true)
+ verify(otherObserver, never()).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
+ }
+
+ @Test
+ fun `observer will not be notified after session is closed`() {
+ val session = spy(DummyEngineSession())
+ val observer = mock(EngineSession.Observer::class.java)
+ val otherHitResult = HitResult.UNKNOWN("file://foobaz")
+ val permissionRequest = mock(PermissionRequest::class.java)
+ val otherPermissionRequest = mock(PermissionRequest::class.java)
+ val windowRequest = mock(WindowRequest::class.java)
+ val otherWindowRequest = mock(WindowRequest::class.java)
+ val tracker = Tracker("tracker")
+
+ session.register(observer)
+
+ session.notifyInternalObservers { onScrollChange(1234, 4321) }
+ session.notifyInternalObservers { onLocationChange("https://www.mozilla.org", false) }
+ session.notifyInternalObservers { onProgress(25) }
+ session.notifyInternalObservers { onLoadingStateChange(true) }
+ session.notifyInternalObservers { onSecurityChange(true, "mozilla.org", "issuer") }
+ session.notifyInternalObservers { onTrackerBlockingEnabledChange(true) }
+ session.notifyInternalObservers { onTrackerBlocked(tracker) }
+ session.notifyInternalObservers { onLongPress(unknownHitResult) }
+ session.notifyInternalObservers { onDesktopModeChange(true) }
+ session.notifyInternalObservers { onFind("search") }
+ session.notifyInternalObservers { onFindResult(0, 1, true) }
+ session.notifyInternalObservers { onFullScreenChange(true) }
+ session.notifyInternalObservers { onMetaViewportFitChanged(1) }
+ session.notifyInternalObservers { onContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onCancelContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onAppPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onWindowRequest(windowRequest) }
+ session.notifyInternalObservers { onShowDynamicToolbar() }
+
+ session.close()
+
+ var mediaSessionController: MediaSession.Controller = mock()
+ val mediaSessionMetadata: MediaSession.Metadata = mock()
+ val mediaSessionFeature: MediaSession.Feature = mock()
+ val mediaSessionPositionState: MediaSession.PositionState = mock()
+ val mediaSessionElementMetadata: MediaSession.ElementMetadata = mock()
+
+ session.notifyInternalObservers { onScrollChange(2345, 5432) }
+ session.notifyInternalObservers { onLocationChange("https://www.firefox.com", false) }
+ session.notifyInternalObservers { onProgress(100) }
+ session.notifyInternalObservers { onLoadingStateChange(false) }
+ session.notifyInternalObservers { onSecurityChange(false, "", "") }
+ session.notifyInternalObservers { onTrackerBlocked(tracker) }
+ session.notifyInternalObservers { onTrackerBlockingEnabledChange(false) }
+ session.notifyInternalObservers { onLongPress(otherHitResult) }
+ session.notifyInternalObservers { onDesktopModeChange(false) }
+ session.notifyInternalObservers { onFind("search2") }
+ session.notifyInternalObservers { onFindResult(0, 1, false) }
+ session.notifyInternalObservers { onFullScreenChange(false) }
+ session.notifyInternalObservers { onMetaViewportFitChanged(2) }
+ session.notifyInternalObservers { onContentPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onCancelContentPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onAppPermissionRequest(otherPermissionRequest) }
+ session.notifyInternalObservers { onWindowRequest(otherWindowRequest) }
+ session.notifyInternalObservers { onMediaActivated(mediaSessionController) }
+ session.notifyInternalObservers { onMediaDeactivated() }
+ session.notifyInternalObservers { onMediaMetadataChanged(mediaSessionMetadata) }
+ session.notifyInternalObservers { onMediaFeatureChanged(mediaSessionFeature) }
+ session.notifyInternalObservers { onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING) }
+ session.notifyInternalObservers { onMediaPositionStateChanged(mediaSessionPositionState) }
+ session.notifyInternalObservers { onMediaMuteChanged(true) }
+ session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
+ session.notifyInternalObservers { onShowDynamicToolbar() }
+
+ verify(observer).onScrollChange(1234, 4321)
+ verify(observer).onLocationChange("https://www.mozilla.org", false)
+ verify(observer).onProgress(25)
+ verify(observer).onLoadingStateChange(true)
+ verify(observer).onSecurityChange(true, "mozilla.org", "issuer")
+ verify(observer).onTrackerBlockingEnabledChange(true)
+ verify(observer).onTrackerBlocked(tracker)
+ verify(observer).onLongPress(unknownHitResult)
+ verify(observer).onDesktopModeChange(true)
+ verify(observer).onFind("search")
+ verify(observer).onFindResult(0, 1, true)
+ verify(observer).onFullScreenChange(true)
+ verify(observer).onMetaViewportFitChanged(1)
+ verify(observer).onAppPermissionRequest(permissionRequest)
+ verify(observer).onContentPermissionRequest(permissionRequest)
+ verify(observer).onCancelContentPermissionRequest(permissionRequest)
+ verify(observer).onWindowRequest(windowRequest)
+ verify(observer).onShowDynamicToolbar()
+ verify(observer, never()).onScrollChange(2345, 5432)
+ verify(observer, never()).onLocationChange("https://www.firefox.com", false)
+ verify(observer, never()).onProgress(100)
+ verify(observer, never()).onLoadingStateChange(false)
+ verify(observer, never()).onSecurityChange(false, "", "")
+ verify(observer, never()).onTrackerBlockingEnabledChange(false)
+ verify(observer, never()).onTrackerBlocked(Tracker("Tracker"))
+ verify(observer, never()).onLongPress(otherHitResult)
+ verify(observer, never()).onDesktopModeChange(false)
+ verify(observer, never()).onFind("search2")
+ verify(observer, never()).onFindResult(0, 1, false)
+ verify(observer, never()).onFullScreenChange(false)
+ verify(observer, never()).onMetaViewportFitChanged(2)
+ verify(observer, never()).onAppPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onContentPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onCancelContentPermissionRequest(otherPermissionRequest)
+ verify(observer, never()).onWindowRequest(otherWindowRequest)
+ verify(observer, never()).onMediaActivated(mediaSessionController)
+ verify(observer, never()).onMediaDeactivated()
+ verify(observer, never()).onMediaMetadataChanged(mediaSessionMetadata)
+ verify(observer, never()).onMediaFeatureChanged(mediaSessionFeature)
+ verify(observer, never()).onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
+ verify(observer, never()).onMediaPositionStateChanged(mediaSessionPositionState)
+ verify(observer, never()).onMediaMuteChanged(true)
+ verify(observer, never()).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
+ verifyNoMoreInteractions(observer)
+ }
+
+ @Test
+ fun `registered observers are instance specific`() {
+ val session = spy(DummyEngineSession())
+ val otherSession = spy(DummyEngineSession())
+ val permissionRequest = mock(PermissionRequest::class.java)
+ val windowRequest = mock(WindowRequest::class.java)
+ val observer = mock(EngineSession.Observer::class.java)
+ val tracker = Tracker("tracker")
+ var mediaSessionController: MediaSession.Controller = mock()
+ val mediaSessionMetadata: MediaSession.Metadata = mock()
+ val mediaSessionFeature: MediaSession.Feature = mock()
+ val mediaSessionPositionState: MediaSession.PositionState = mock()
+ val mediaSessionElementMetadata: MediaSession.ElementMetadata = mock()
+ session.register(observer)
+
+ otherSession.notifyInternalObservers { onScrollChange(1234, 4321) }
+ otherSession.notifyInternalObservers { onLocationChange("https://www.mozilla.org", false) }
+ otherSession.notifyInternalObservers { onLocationChange("https://www.mozilla.org", false) }
+ otherSession.notifyInternalObservers { onProgress(25) }
+ otherSession.notifyInternalObservers { onLoadingStateChange(true) }
+ otherSession.notifyInternalObservers { onSecurityChange(true, "mozilla.org", "issuer") }
+ otherSession.notifyInternalObservers { onTrackerBlockingEnabledChange(true) }
+ otherSession.notifyInternalObservers { onTrackerBlocked(tracker) }
+ otherSession.notifyInternalObservers { onLongPress(unknownHitResult) }
+ otherSession.notifyInternalObservers { onDesktopModeChange(true) }
+ otherSession.notifyInternalObservers { onFind("search") }
+ otherSession.notifyInternalObservers { onFindResult(0, 1, true) }
+ otherSession.notifyInternalObservers { onFullScreenChange(true) }
+ otherSession.notifyInternalObservers { onMetaViewportFitChanged(1) }
+ otherSession.notifyInternalObservers { onContentPermissionRequest(permissionRequest) }
+ otherSession.notifyInternalObservers { onCancelContentPermissionRequest(permissionRequest) }
+ otherSession.notifyInternalObservers { onAppPermissionRequest(permissionRequest) }
+ otherSession.notifyInternalObservers { onWindowRequest(windowRequest) }
+ otherSession.notifyInternalObservers { onMediaActivated(mediaSessionController) }
+ otherSession.notifyInternalObservers { onMediaDeactivated() }
+ otherSession.notifyInternalObservers { onMediaMetadataChanged(mediaSessionMetadata) }
+ otherSession.notifyInternalObservers { onMediaFeatureChanged(mediaSessionFeature) }
+ otherSession.notifyInternalObservers { onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING) }
+ otherSession.notifyInternalObservers { onMediaPositionStateChanged(mediaSessionPositionState) }
+ otherSession.notifyInternalObservers { onMediaMuteChanged(true) }
+ otherSession.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
+ otherSession.notifyInternalObservers { onShowDynamicToolbar() }
+ verify(observer, never()).onScrollChange(1234, 4321)
+ verify(observer, never()).onLocationChange("https://www.mozilla.org", false)
+ verify(observer, never()).onProgress(25)
+ verify(observer, never()).onLoadingStateChange(true)
+ verify(observer, never()).onSecurityChange(true, "mozilla.org", "issuer")
+ verify(observer, never()).onTrackerBlockingEnabledChange(true)
+ verify(observer, never()).onTrackerBlocked(tracker)
+ verify(observer, never()).onLongPress(unknownHitResult)
+ verify(observer, never()).onDesktopModeChange(true)
+ verify(observer, never()).onFind("search")
+ verify(observer, never()).onFindResult(0, 1, true)
+ verify(observer, never()).onFullScreenChange(true)
+ verify(observer, never()).onMetaViewportFitChanged(1)
+ verify(observer, never()).onAppPermissionRequest(permissionRequest)
+ verify(observer, never()).onContentPermissionRequest(permissionRequest)
+ verify(observer, never()).onCancelContentPermissionRequest(permissionRequest)
+ verify(observer, never()).onWindowRequest(windowRequest)
+ verify(observer, never()).onMediaActivated(mediaSessionController)
+ verify(observer, never()).onMediaDeactivated()
+ verify(observer, never()).onMediaMetadataChanged(mediaSessionMetadata)
+ verify(observer, never()).onMediaFeatureChanged(mediaSessionFeature)
+ verify(observer, never()).onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
+ verify(observer, never()).onMediaPositionStateChanged(mediaSessionPositionState)
+ verify(observer, never()).onMediaMuteChanged(true)
+ verify(observer, never()).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
+ verify(observer, never()).onShowDynamicToolbar()
+
+ session.notifyInternalObservers { onScrollChange(1234, 4321) }
+ session.notifyInternalObservers { onLocationChange("https://www.mozilla.org", false) }
+ session.notifyInternalObservers { onProgress(25) }
+ session.notifyInternalObservers { onLoadingStateChange(true) }
+ session.notifyInternalObservers { onSecurityChange(true, "mozilla.org", "issuer") }
+ session.notifyInternalObservers { onTrackerBlockingEnabledChange(true) }
+ session.notifyInternalObservers { onTrackerBlocked(tracker) }
+ session.notifyInternalObservers { onLongPress(unknownHitResult) }
+ session.notifyInternalObservers { onDesktopModeChange(false) }
+ session.notifyInternalObservers { onFind("search") }
+ session.notifyInternalObservers { onFindResult(0, 1, true) }
+ session.notifyInternalObservers { onFullScreenChange(true) }
+ session.notifyInternalObservers { onMetaViewportFitChanged(1) }
+ session.notifyInternalObservers { onContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onCancelContentPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onAppPermissionRequest(permissionRequest) }
+ session.notifyInternalObservers { onWindowRequest(windowRequest) }
+ session.notifyInternalObservers { onMediaActivated(mediaSessionController) }
+ session.notifyInternalObservers { onMediaDeactivated() }
+ session.notifyInternalObservers { onMediaMetadataChanged(mediaSessionMetadata) }
+ session.notifyInternalObservers { onMediaFeatureChanged(mediaSessionFeature) }
+ session.notifyInternalObservers { onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING) }
+ session.notifyInternalObservers { onMediaPositionStateChanged(mediaSessionPositionState) }
+ session.notifyInternalObservers { onMediaMuteChanged(true) }
+ session.notifyInternalObservers { onMediaFullscreenChanged(true, mediaSessionElementMetadata) }
+ session.notifyInternalObservers { onShowDynamicToolbar() }
+ verify(observer, times(1)).onScrollChange(1234, 4321)
+ verify(observer, times(1)).onLocationChange("https://www.mozilla.org", false)
+ verify(observer, times(1)).onProgress(25)
+ verify(observer, times(1)).onLoadingStateChange(true)
+ verify(observer, times(1)).onSecurityChange(true, "mozilla.org", "issuer")
+ verify(observer, times(1)).onTrackerBlockingEnabledChange(true)
+ verify(observer, times(1)).onTrackerBlocked(tracker)
+ verify(observer, times(1)).onLongPress(unknownHitResult)
+ verify(observer, times(1)).onDesktopModeChange(false)
+ verify(observer, times(1)).onFind("search")
+ verify(observer, times(1)).onFindResult(0, 1, true)
+ verify(observer, times(1)).onFullScreenChange(true)
+ verify(observer, times(1)).onMetaViewportFitChanged(1)
+ verify(observer, times(1)).onAppPermissionRequest(permissionRequest)
+ verify(observer, times(1)).onContentPermissionRequest(permissionRequest)
+ verify(observer, times(1)).onCancelContentPermissionRequest(permissionRequest)
+ verify(observer, times(1)).onWindowRequest(windowRequest)
+ verify(observer, times(1)).onMediaActivated(mediaSessionController)
+ verify(observer, times(1)).onMediaDeactivated()
+ verify(observer, times(1)).onMediaMetadataChanged(mediaSessionMetadata)
+ verify(observer, times(1)).onMediaFeatureChanged(mediaSessionFeature)
+ verify(observer, times(1)).onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
+ verify(observer, times(1)).onMediaPositionStateChanged(mediaSessionPositionState)
+ verify(observer, times(1)).onMediaMuteChanged(true)
+ verify(observer, times(1)).onMediaFullscreenChanged(true, mediaSessionElementMetadata)
+ verify(observer, times(1)).onShowDynamicToolbar()
+ verifyNoMoreInteractions(observer)
+ }
+
+ @Test
+ fun `all HitResults are supported`() {
+ val session = spy(DummyEngineSession())
+ val observer = mock(EngineSession.Observer::class.java)
+ session.register(observer)
+
+ var hitResult: HitResult = HitResult.UNKNOWN("file://foobaz")
+ session.notifyInternalObservers { onLongPress(hitResult) }
+ verify(observer, times(1)).onLongPress(hitResult)
+
+ hitResult = HitResult.EMAIL("mailto:asa@mozilla.com")
+ session.notifyInternalObservers { onLongPress(hitResult) }
+ verify(observer, times(1)).onLongPress(hitResult)
+
+ hitResult = HitResult.PHONE("tel:+1234567890")
+ session.notifyInternalObservers { onLongPress(hitResult) }
+ verify(observer, times(1)).onLongPress(hitResult)
+
+ hitResult = HitResult.IMAGE_SRC("file.png", "https://mozilla.org")
+ session.notifyInternalObservers { onLongPress(hitResult) }
+ verify(observer, times(1)).onLongPress(hitResult)
+
+ hitResult = HitResult.IMAGE("file.png")
+ session.notifyInternalObservers { onLongPress(hitResult) }
+ verify(observer, times(1)).onLongPress(hitResult)
+
+ hitResult = HitResult.AUDIO("file.mp3")
+ session.notifyInternalObservers { onLongPress(hitResult) }
+ verify(observer, times(1)).onLongPress(hitResult)
+
+ hitResult = HitResult.GEO("geo:1,-1")
+ session.notifyInternalObservers { onLongPress(hitResult) }
+ verify(observer, times(1)).onLongPress(hitResult)
+
+ hitResult = HitResult.VIDEO("file.mp4")
+ session.notifyInternalObservers { onLongPress(hitResult) }
+ verify(observer, times(1)).onLongPress(hitResult)
+ }
+
+ @Test
+ fun `registered observer will be notified about download`() {
+ val session = spy(DummyEngineSession())
+
+ val observer = mock(EngineSession.Observer::class.java)
+ session.register(observer)
+
+ session.notifyInternalObservers {
+ onExternalResource(
+ url = "https://download.mozilla.org",
+ fileName = "firefox.apk",
+ contentLength = 1927392,
+ contentType = "application/vnd.android.package-archive",
+ cookie = "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;",
+ isPrivate = true,
+ skipConfirmation = false,
+ openInApp = false,
+ userAgent = "Components/1.0",
+ )
+ }
+
+ verify(observer).onExternalResource(
+ url = "https://download.mozilla.org",
+ fileName = "firefox.apk",
+ contentLength = 1927392,
+ contentType = "application/vnd.android.package-archive",
+ cookie = "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;",
+ isPrivate = true,
+ skipConfirmation = false,
+ openInApp = false,
+ userAgent = "Components/1.0",
+ )
+ }
+
+ @Test
+ fun `registered observer will be notified about history state`() {
+ val session = spy(DummyEngineSession())
+
+ val observer = mock(EngineSession.Observer::class.java)
+ session.register(observer)
+
+ session.notifyInternalObservers {
+ onHistoryStateChanged(
+ listOf(HistoryItem("Firefox download", "https://download.mozilla.org")),
+ currentIndex = 0,
+ )
+ }
+
+ verify(observer).onHistoryStateChanged(
+ historyList = listOf(
+ HistoryItem("Firefox download", "https://download.mozilla.org"),
+ ),
+ currentIndex = 0,
+ )
+ }
+
+ @Test
+ fun `tracking protection policies have correct categories`() {
+ val recommendedPolicy = TrackingProtectionPolicy.recommended()
+
+ assertEquals(
+ recommendedPolicy.trackingCategories.sumOf { it.id },
+ TrackingCategory.RECOMMENDED.id,
+ )
+
+ assertEquals(recommendedPolicy.cookiePolicy.id, CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS.id)
+ assertEquals(recommendedPolicy.cookiePolicyPrivateMode.id, recommendedPolicy.cookiePolicy.id)
+
+ val strictPolicy = TrackingProtectionPolicy.strict()
+
+ assertEquals(
+ strictPolicy.trackingCategories.sumOf { it.id },
+ TrackingCategory.STRICT.id,
+ )
+
+ assertEquals(strictPolicy.cookiePolicy.id, CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS.id)
+ assertEquals(strictPolicy.cookiePolicyPrivateMode.id, strictPolicy.cookiePolicy.id)
+
+ val nonePolicy = TrackingProtectionPolicy.none()
+
+ assertEquals(
+ nonePolicy.trackingCategories.sumOf { it.id },
+ TrackingCategory.NONE.id,
+ )
+
+ assertEquals(nonePolicy.cookiePolicy.id, CookiePolicy.ACCEPT_ALL.id)
+ assertEquals(nonePolicy.cookiePolicyPrivateMode.id, CookiePolicy.ACCEPT_ALL.id)
+
+ val newPolicy = TrackingProtectionPolicy.select(
+ trackingCategories = arrayOf(
+ TrackingCategory.AD,
+ TrackingCategory.SOCIAL,
+ TrackingCategory.ANALYTICS,
+ TrackingCategory.CONTENT,
+ TrackingCategory.CRYPTOMINING,
+ TrackingCategory.FINGERPRINTING,
+ TrackingCategory.TEST,
+ ),
+ )
+
+ assertEquals(
+ newPolicy.trackingCategories.sumOf { it.id },
+ arrayOf(
+ TrackingCategory.AD,
+ TrackingCategory.SOCIAL,
+ TrackingCategory.ANALYTICS,
+ TrackingCategory.CONTENT,
+ TrackingCategory.CRYPTOMINING,
+ TrackingCategory.FINGERPRINTING,
+ TrackingCategory.TEST,
+ ).sumOf { it.id },
+ )
+ }
+
+ @Test
+ fun `tracking protection policies can be specified for session type`() {
+ val all = TrackingProtectionPolicy.strict()
+ val selected = TrackingProtectionPolicy.select(
+ trackingCategories = arrayOf(TrackingCategory.AD),
+
+ )
+
+ // Tracking protection policies should be applied to all sessions by default
+ assertTrue(all.useForPrivateSessions)
+ assertTrue(all.useForRegularSessions)
+ assertTrue(selected.useForPrivateSessions)
+ assertTrue(selected.useForRegularSessions)
+
+ val allForPrivate = TrackingProtectionPolicy.strict().forPrivateSessionsOnly()
+ assertTrue(allForPrivate.useForPrivateSessions)
+ assertFalse(allForPrivate.useForRegularSessions)
+
+ val selectedForRegular =
+ TrackingProtectionPolicy.select(trackingCategories = arrayOf(TrackingCategory.AD))
+ .forRegularSessionsOnly()
+
+ assertTrue(selectedForRegular.useForRegularSessions)
+ assertFalse(selectedForRegular.useForPrivateSessions)
+ }
+
+ @Test
+ fun `load flags can be selected`() {
+ assertEquals(LoadUrlFlags.NONE, LoadUrlFlags.none().value)
+ assertEquals(LoadUrlFlags.ALL, LoadUrlFlags.all().value)
+ assertEquals(LoadUrlFlags.EXTERNAL, LoadUrlFlags.external().value)
+
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.BYPASS_CACHE).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.EXTERNAL).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.ALLOW_POPUPS).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.BYPASS_CLASSIFIER).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.LOAD_FLAGS_FORCE_ALLOW_DATA_URI).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.LOAD_FLAGS_REPLACE_HISTORY).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS).value))
+ assertTrue(LoadUrlFlags.all().contains(LoadUrlFlags.select(LoadUrlFlags.ALLOW_JAVASCRIPT_URL).value))
+
+ val flags = LoadUrlFlags.select(LoadUrlFlags.EXTERNAL)
+ assertTrue(flags.contains(LoadUrlFlags.EXTERNAL))
+ assertFalse(flags.contains(LoadUrlFlags.NONE))
+ assertFalse(flags.contains(LoadUrlFlags.ALLOW_POPUPS))
+ assertFalse(flags.contains(LoadUrlFlags.BYPASS_CACHE))
+ assertFalse(flags.contains(LoadUrlFlags.BYPASS_CLASSIFIER))
+ assertFalse(flags.contains(LoadUrlFlags.BYPASS_PROXY))
+ assertFalse(flags.contains(LoadUrlFlags.LOAD_FLAGS_FORCE_ALLOW_DATA_URI))
+ assertFalse(flags.contains(LoadUrlFlags.LOAD_FLAGS_REPLACE_HISTORY))
+ assertFalse(flags.contains(LoadUrlFlags.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE))
+ assertFalse(flags.contains(LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS))
+ assertFalse(flags.contains(LoadUrlFlags.ALLOW_JAVASCRIPT_URL))
+ }
+
+ @Test
+ fun `engine observer has default methods`() {
+ val defaultObserver = object : EngineSession.Observer {}
+
+ defaultObserver.onTitleChange("")
+ defaultObserver.onScrollChange(0, 0)
+ defaultObserver.onLocationChange("", false)
+ defaultObserver.onPreviewImageChange("")
+ defaultObserver.onLongPress(HitResult.UNKNOWN(""))
+ defaultObserver.onExternalResource("", "")
+ defaultObserver.onDesktopModeChange(true)
+ defaultObserver.onSecurityChange(true)
+ defaultObserver.onTrackerBlocked(mock())
+ defaultObserver.onTrackerLoaded(mock())
+ defaultObserver.onTrackerBlockingEnabledChange(true)
+ defaultObserver.onExcludedOnTrackingProtectionChange(true)
+ defaultObserver.onFindResult(0, 0, false)
+ defaultObserver.onFind("text")
+ defaultObserver.onExternalResource("", "")
+ defaultObserver.onNavigationStateChange()
+ defaultObserver.onProgress(123)
+ defaultObserver.onLoadingStateChange(true)
+ defaultObserver.onFullScreenChange(true)
+ defaultObserver.onMetaViewportFitChanged(1)
+ defaultObserver.onAppPermissionRequest(mock(PermissionRequest::class.java))
+ defaultObserver.onContentPermissionRequest(mock(PermissionRequest::class.java))
+ defaultObserver.onCancelContentPermissionRequest(mock(PermissionRequest::class.java))
+ defaultObserver.onWindowRequest(mock(WindowRequest::class.java))
+ defaultObserver.onCrash()
+ defaultObserver.onShowDynamicToolbar()
+ }
+
+ @Test
+ fun `engine doesn't notify observers of clear data`() {
+ val session = spy(DummyEngineSession())
+ val observer = mock(EngineSession.Observer::class.java)
+ session.register(observer)
+
+ session.clearData()
+
+ verifyNoInteractions(observer)
+ }
+
+ @Test
+ fun `trackingProtectionPolicy contains should work with compound categories`() {
+ val recommendedPolicy = TrackingProtectionPolicy.recommended()
+
+ assertTrue(recommendedPolicy.contains(TrackingCategory.RECOMMENDED))
+ assertTrue(recommendedPolicy.contains(TrackingCategory.AD))
+ assertTrue(recommendedPolicy.contains(TrackingCategory.ANALYTICS))
+ assertTrue(recommendedPolicy.contains(TrackingCategory.SOCIAL))
+ assertTrue(recommendedPolicy.contains(TrackingCategory.TEST))
+ assertTrue(recommendedPolicy.contains(TrackingCategory.FINGERPRINTING))
+
+ assertTrue(recommendedPolicy.contains(TrackingCategory.CRYPTOMINING))
+ assertFalse(recommendedPolicy.contains(TrackingCategory.CONTENT))
+
+ val strictPolicy = TrackingProtectionPolicy.strict()
+
+ assertTrue(strictPolicy.contains(TrackingCategory.RECOMMENDED))
+ assertTrue(strictPolicy.contains(TrackingCategory.AD))
+ assertTrue(strictPolicy.contains(TrackingCategory.ANALYTICS))
+ assertTrue(strictPolicy.contains(TrackingCategory.SOCIAL))
+ assertTrue(strictPolicy.contains(TrackingCategory.TEST))
+ assertTrue(strictPolicy.contains(TrackingCategory.CRYPTOMINING))
+ assertFalse(strictPolicy.contains(TrackingCategory.CONTENT))
+ }
+
+ @Test
+ fun `TrackingSessionPolicies retain all expected fields during privacy transformations`() {
+ val strict = TrackingProtectionPolicy.strict()
+ val default = TrackingProtectionPolicy.recommended()
+ val customNormal = TrackingProtectionPolicy.select(
+ trackingCategories = emptyArray(),
+ cookiePolicy = CookiePolicy.ACCEPT_ONLY_FIRST_PARTY,
+ strictSocialTrackingProtection = true,
+ )
+ val customPrivate = TrackingProtectionPolicy.select(
+ trackingCategories = emptyArray(),
+ cookiePolicy = CookiePolicy.ACCEPT_ONLY_FIRST_PARTY,
+ strictSocialTrackingProtection = false,
+ )
+ val changedFields = listOf("useForPrivateSessions", "useForRegularSessions")
+
+ fun checkSavedFields(expect: TrackingProtectionPolicy, actual: TrackingProtectionPolicy) {
+ TrackingProtectionPolicy::class.java.declaredMethods
+ .filter { method -> changedFields.all { !method.name.lowercase().contains(it.lowercase()) } }
+ .filter { it.parameterCount == 0 } // Only keep getters
+ .filter { it.modifiers and Modifier.PUBLIC != 0 }
+ .filter { it.modifiers and Modifier.STATIC == 0 }
+ .forEach {
+ assertEquals(it.invoke(expect), it.invoke(actual))
+ }
+ }
+
+ listOf(
+ strict,
+ default,
+ customNormal,
+ ).forEach {
+ checkSavedFields(it, it.forRegularSessionsOnly())
+ }
+
+ checkSavedFields(customPrivate, customPrivate.forPrivateSessionsOnly())
+ }
+
+ @Test
+ fun `engine session observer has default methods`() {
+ val observer = object : EngineSession.Observer { }
+ val permissionRequest = mock(PermissionRequest::class.java)
+ val windowRequest = mock(WindowRequest::class.java)
+ val tracker: Tracker = mock()
+
+ observer.onScrollChange(1234, 4321)
+ observer.onLocationChange("https://www.mozilla.org", false)
+ observer.onLocationChange("https://www.firefox.com", false)
+ observer.onProgress(25)
+ observer.onProgress(100)
+ observer.onLoadingStateChange(true)
+ observer.onSecurityChange(true, "mozilla.org", "issuer")
+ observer.onTrackerBlockingEnabledChange(true)
+ observer.onTrackerBlocked(tracker)
+ observer.onExcludedOnTrackingProtectionChange(true)
+ observer.onLongPress(unknownHitResult)
+ observer.onDesktopModeChange(true)
+ observer.onFind("search")
+ observer.onFindResult(0, 1, true)
+ observer.onFullScreenChange(true)
+ observer.onMetaViewportFitChanged(1)
+ observer.onContentPermissionRequest(permissionRequest)
+ observer.onCancelContentPermissionRequest(permissionRequest)
+ observer.onAppPermissionRequest(permissionRequest)
+ observer.onWindowRequest(windowRequest)
+ observer.onCrash()
+ observer.onLoadRequest("https://www.mozilla.org", true, true)
+ observer.onLaunchIntentRequest("https://www.mozilla.org", null)
+ observer.onProcessKilled()
+ observer.onShowDynamicToolbar()
+ }
+}
+
+open class DummyEngineSession : EngineSession() {
+ override val settings: Settings
+ get() = mock(Settings::class.java)
+
+ override fun restoreState(state: EngineSessionState): Boolean { return false }
+
+ override fun loadUrl(
+ url: String,
+ parent: EngineSession?,
+ flags: LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {}
+
+ override fun loadData(data: String, mimeType: String, encoding: String) {}
+
+ override fun requestPdfToDownload() {}
+
+ override fun requestPrintContent() {}
+
+ override fun stopLoading() {}
+
+ override fun reload(flags: LoadUrlFlags) {}
+
+ override fun goBack(userInteraction: Boolean) {}
+
+ override fun goForward(userInteraction: Boolean) {}
+
+ override fun goToHistoryIndex(index: Int) {}
+
+ override fun updateTrackingProtection(policy: TrackingProtectionPolicy) {}
+
+ override fun toggleDesktopMode(enable: Boolean, reload: Boolean) {}
+
+ override fun hasCookieBannerRuleForSession(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun checkForPdfViewer(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun requestProductRecommendations(
+ url: String,
+ onResult: (List<ProductRecommendation>) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun requestProductAnalysis(
+ url: String,
+ onResult: (ProductAnalysis) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun reanalyzeProduct(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun requestAnalysisStatus(
+ url: String,
+ onResult: (ProductAnalysisStatus) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun sendClickAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun sendImpressionAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun sendPlacementAttributionEvent(
+ aid: String,
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun reportBackInStock(
+ url: String,
+ onResult: (String) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun requestTranslate(
+ fromLanguage: String,
+ toLanguage: String,
+ options: TranslationOptions?,
+ ) {}
+
+ override fun requestTranslationRestore() {}
+
+ override fun getNeverTranslateSiteSetting(
+ onResult: (Boolean) -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun setNeverTranslateSiteSetting(
+ setting: Boolean,
+ onResult: () -> Unit,
+ onException: (Throwable) -> Unit,
+ ) {}
+
+ override fun findAll(text: String) {}
+
+ override fun findNext(forward: Boolean) {}
+
+ override fun clearFindMatches() {}
+
+ override fun exitFullScreenMode() {}
+
+ override fun purgeHistory() {}
+
+ // Helper method to access the protected method from test cases.
+ fun notifyInternalObservers(block: Observer.() -> Unit) {
+ notifyObservers(block)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineTest.kt
new file mode 100644
index 0000000000..2449245343
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineTest.kt
@@ -0,0 +1,140 @@
+/* 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.concept.engine
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.JsonReader
+import mozilla.components.concept.base.profiler.Profiler
+import mozilla.components.concept.engine.Engine.BrowsingData
+import mozilla.components.concept.engine.utils.EngineVersion
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.lang.UnsupportedOperationException
+
+class EngineTest {
+
+ private val testEngine = object : Engine {
+ override val version: EngineVersion
+ get() = throw NotImplementedError("Not needed for test")
+
+ override fun createView(context: Context, attrs: AttributeSet?): EngineView {
+ throw NotImplementedError("Not needed for test")
+ }
+
+ override fun createSession(private: Boolean, contextId: String?): EngineSession {
+ throw NotImplementedError("Not needed for test")
+ }
+
+ override fun createSessionState(json: JSONObject): EngineSessionState {
+ throw NotImplementedError("Not needed for test")
+ }
+
+ override fun createSessionStateFrom(reader: JsonReader): EngineSessionState {
+ throw NotImplementedError("Not needed for test")
+ }
+
+ override fun name(): String {
+ throw NotImplementedError("Not needed for test")
+ }
+
+ override fun speculativeConnect(url: String) {
+ throw NotImplementedError("Not needed for test")
+ }
+
+ override val profiler: Profiler?
+ get() = throw NotImplementedError("Not needed for test")
+
+ override val settings: Settings
+ get() = throw NotImplementedError("Not needed for test")
+ }
+
+ @Test
+ fun `invokes default functions on trackingProtectionExceptionStore`() {
+ var wasExecuted = false
+ try {
+ testEngine.trackingProtectionExceptionStore
+ } catch (_: Exception) {
+ wasExecuted = true
+ }
+ assertTrue(wasExecuted)
+ }
+
+ @Test
+ fun `invokes error callback if webextensions not supported`() {
+ var exception: Throwable? = null
+ testEngine.installWebExtension("resource://path", onError = { e -> exception = e })
+ assertNotNull(exception)
+ assertTrue(exception is UnsupportedOperationException)
+
+ exception = null
+ testEngine.installBuiltInWebExtension("a-built-in", "resource://path", onError = { e -> exception = e })
+ assertNotNull(exception)
+ assertTrue(exception is UnsupportedOperationException)
+
+ exception = null
+ testEngine.listInstalledWebExtensions(onSuccess = { }, onError = { e -> exception = e })
+ assertNotNull(exception)
+ assertTrue(exception is UnsupportedOperationException)
+ }
+
+ @Test
+ fun `invokes error callback if clear data not supported`() {
+ var exception: Throwable? = null
+ testEngine.clearData(Engine.BrowsingData.all(), onError = { exception = it })
+ assertNotNull(exception)
+ assertTrue(exception is UnsupportedOperationException)
+ }
+
+ @Test
+ fun `browsing data types can be combined`() {
+ assertEquals(BrowsingData.ALL, BrowsingData.all().types)
+ assertTrue(BrowsingData.all().contains(BrowsingData.NETWORK_CACHE))
+ assertTrue(BrowsingData.all().contains(BrowsingData.IMAGE_CACHE))
+ assertTrue(BrowsingData.all().contains(BrowsingData.PERMISSIONS))
+ assertTrue(BrowsingData.all().contains(BrowsingData.DOM_STORAGES))
+ assertTrue(BrowsingData.all().contains(BrowsingData.COOKIES))
+ assertTrue(BrowsingData.all().contains(BrowsingData.AUTH_SESSIONS))
+ assertTrue(BrowsingData.all().contains(BrowsingData.allSiteSettings().types))
+ assertTrue(BrowsingData.all().contains(BrowsingData.allSiteData().types))
+ assertTrue(BrowsingData.all().contains(BrowsingData.allCaches().types))
+
+ assertEquals(BrowsingData.ALL_CACHES, BrowsingData.allCaches().types)
+ assertTrue(BrowsingData.allCaches().contains(BrowsingData.NETWORK_CACHE))
+ assertTrue(BrowsingData.allCaches().contains(BrowsingData.IMAGE_CACHE))
+ assertFalse(BrowsingData.allCaches().contains(BrowsingData.PERMISSIONS))
+ assertFalse(BrowsingData.allCaches().contains(BrowsingData.AUTH_SESSIONS))
+ assertFalse(BrowsingData.allCaches().contains(BrowsingData.COOKIES))
+ assertFalse(BrowsingData.allCaches().contains(BrowsingData.DOM_STORAGES))
+
+ assertEquals(BrowsingData.ALL_SITE_DATA, BrowsingData.allSiteData().types)
+ assertTrue(BrowsingData.allSiteData().contains(BrowsingData.NETWORK_CACHE))
+ assertTrue(BrowsingData.allSiteData().contains(BrowsingData.IMAGE_CACHE))
+ assertTrue(BrowsingData.allSiteData().contains(BrowsingData.PERMISSIONS))
+ assertTrue(BrowsingData.allSiteData().contains(BrowsingData.DOM_STORAGES))
+ assertTrue(BrowsingData.allSiteData().contains(BrowsingData.COOKIES))
+ assertFalse(BrowsingData.allSiteData().contains(BrowsingData.AUTH_SESSIONS))
+
+ assertEquals(BrowsingData.ALL_SITE_SETTINGS, BrowsingData.allSiteSettings().types)
+ assertTrue(BrowsingData.allSiteSettings().contains(BrowsingData.PERMISSIONS))
+ assertFalse(BrowsingData.allSiteSettings().contains(BrowsingData.IMAGE_CACHE))
+ assertFalse(BrowsingData.allSiteSettings().contains(BrowsingData.NETWORK_CACHE))
+ assertFalse(BrowsingData.allSiteSettings().contains(BrowsingData.AUTH_SESSIONS))
+ assertFalse(BrowsingData.allSiteSettings().contains(BrowsingData.COOKIES))
+ assertFalse(BrowsingData.allSiteSettings().contains(BrowsingData.DOM_STORAGES))
+
+ val browsingData = BrowsingData.select(BrowsingData.COOKIES, BrowsingData.DOM_STORAGES)
+ assertTrue(browsingData.contains(BrowsingData.COOKIES))
+ assertTrue(browsingData.contains(BrowsingData.DOM_STORAGES))
+ assertFalse(browsingData.contains(BrowsingData.AUTH_SESSIONS))
+ assertFalse(browsingData.contains(BrowsingData.IMAGE_CACHE))
+ assertFalse(browsingData.contains(BrowsingData.NETWORK_CACHE))
+ assertFalse(browsingData.contains(BrowsingData.PERMISSIONS))
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineViewTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineViewTest.kt
new file mode 100644
index 0000000000..9b58d9bcfc
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineViewTest.kt
@@ -0,0 +1,84 @@
+/* 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.concept.engine
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class EngineViewTest {
+
+ @Test
+ fun `asView method returns underlying Android view`() {
+ val engineView = createDummyEngineView(testContext)
+
+ val view = engineView.asView()
+
+ assertTrue(view is FrameLayout)
+ }
+
+ @Test(expected = ClassCastException::class)
+ fun `asView method fails if class is not a view`() {
+ val engineView = BrokenEngineView()
+ engineView.asView()
+ }
+
+ @Test
+ fun lifecycleObserver() {
+ val engineView = spy(createDummyEngineView(testContext))
+ val observer = LifecycleObserver(engineView)
+
+ observer.onCreate(mock())
+ verify(engineView).onCreate()
+
+ observer.onDestroy(mock())
+ verify(engineView).onDestroy()
+
+ observer.onStart(mock())
+ verify(engineView).onStart()
+
+ observer.onStop(mock())
+ verify(engineView).onStop()
+
+ observer.onPause(mock())
+ verify(engineView).onPause()
+
+ observer.onResume(mock())
+ verify(engineView).onResume()
+ }
+
+ private fun createDummyEngineView(context: Context): EngineView = DummyEngineView(context)
+
+ open class DummyEngineView(context: Context) : FrameLayout(context), EngineView {
+ override fun setVerticalClipping(clippingHeight: Int) {}
+ override fun setDynamicToolbarMaxHeight(height: Int) {}
+ override fun setActivityContext(context: Context?) {}
+ override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) = Unit
+ override fun render(session: EngineSession) {}
+ override fun release() {}
+ override var selectionActionDelegate: SelectionActionDelegate? = null
+ }
+
+ // Class it not actually a View!
+ open class BrokenEngineView : EngineView {
+ override fun setVerticalClipping(clippingHeight: Int) {}
+ override fun setDynamicToolbarMaxHeight(height: Int) {}
+ override fun setActivityContext(context: Context?) {}
+ override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) = Unit
+ override fun render(session: EngineSession) {}
+ override fun release() {}
+ override var selectionActionDelegate: SelectionActionDelegate? = null
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/HitResultTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/HitResultTest.kt
new file mode 100644
index 0000000000..547463fd8b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/HitResultTest.kt
@@ -0,0 +1,25 @@
+/* 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.concept.engine
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class HitResultTest {
+ @Test
+ fun constructor() {
+ var result: HitResult = HitResult.UNKNOWN("file://foobar")
+ assertTrue(result is HitResult.UNKNOWN)
+ assertEquals(result.src, "file://foobar")
+
+ result = HitResult.IMAGE("https://mozilla.org/i.png")
+ assertEquals(result.src, "https://mozilla.org/i.png")
+
+ result = HitResult.IMAGE_SRC("https://mozilla.org/i.png", "https://firefox.com")
+ assertEquals(result.src, "https://mozilla.org/i.png")
+ assertEquals(result.uri, "https://firefox.com")
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/InputResultDetailTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/InputResultDetailTest.kt
new file mode 100644
index 0000000000..9793fed6f8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/InputResultDetailTest.kt
@@ -0,0 +1,474 @@
+/* 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.concept.engine
+
+import mozilla.components.concept.engine.InputResultDetail.Companion.INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.INPUT_HANDLED_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.INPUT_UNHANDLED_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.INPUT_UNKNOWN_HANDLING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.OVERSCROLL_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.SCROLL_BOTTOM_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.SCROLL_LEFT_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.SCROLL_RIGHT_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.SCROLL_TOP_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.SCROLL_TOSTRING_DESCRIPTION
+import mozilla.components.concept.engine.InputResultDetail.Companion.TOSTRING_SEPARATOR
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+class InputResultDetailTest {
+ private lateinit var inputResultDetail: InputResultDetail
+
+ @Before
+ fun setup() {
+ inputResultDetail = InputResultDetail.newInstance()
+ }
+
+ @Test
+ fun `GIVEN InputResultDetail WHEN newInstance() is called with default parameters THEN a new instance with default values is returned`() {
+ assertEquals(INPUT_HANDLING_UNKNOWN, inputResultDetail.inputResult)
+ assertEquals(SCROLL_DIRECTIONS_NONE, inputResultDetail.scrollDirections)
+ assertEquals(OVERSCROLL_DIRECTIONS_NONE, inputResultDetail.overscrollDirections)
+ }
+
+ @Test
+ fun `GIVEN InputResultDetail WHEN newInstance() is called specifying overscroll enabled THEN a new instance with overscroll enabled is returned`() {
+ inputResultDetail = InputResultDetail.newInstance(true)
+ // Handling unknown but can overscroll. We need to preemptively allow for this,
+ // otherwise pull to refresh would not work for the entirety of the touch.
+ assertEquals(INPUT_HANDLING_UNKNOWN, inputResultDetail.inputResult)
+ assertEquals(SCROLL_DIRECTIONS_NONE, inputResultDetail.scrollDirections)
+ assertEquals(OVERSCROLL_DIRECTIONS_VERTICAL, inputResultDetail.overscrollDirections)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN copy is called with new values THEN the new values are set for the instance`() {
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT)
+ assertEquals(INPUT_HANDLED_CONTENT, inputResultDetail.inputResult)
+ assertEquals(SCROLL_DIRECTIONS_NONE, inputResultDetail.scrollDirections)
+ assertEquals(OVERSCROLL_DIRECTIONS_NONE, inputResultDetail.overscrollDirections)
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED, SCROLL_DIRECTIONS_RIGHT)
+ assertEquals(INPUT_HANDLED, inputResultDetail.inputResult)
+ assertEquals(SCROLL_DIRECTIONS_RIGHT, inputResultDetail.scrollDirections)
+ assertEquals(OVERSCROLL_DIRECTIONS_NONE, inputResultDetail.overscrollDirections)
+
+ inputResultDetail = inputResultDetail.copy(
+ INPUT_UNHANDLED,
+ SCROLL_DIRECTIONS_NONE,
+ OVERSCROLL_DIRECTIONS_HORIZONTAL,
+ )
+ assertEquals(INPUT_UNHANDLED, inputResultDetail.inputResult)
+ assertEquals(SCROLL_DIRECTIONS_NONE, inputResultDetail.scrollDirections)
+ assertEquals(OVERSCROLL_DIRECTIONS_HORIZONTAL, inputResultDetail.overscrollDirections)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN copy is called with new values THEN the invalid ones are filtered out`() {
+ inputResultDetail = inputResultDetail.copy(42, 42, 42)
+ assertEquals(INPUT_HANDLING_UNKNOWN, inputResultDetail.inputResult)
+ assertEquals(SCROLL_DIRECTIONS_NONE, inputResultDetail.scrollDirections)
+ assertEquals(OVERSCROLL_DIRECTIONS_NONE, inputResultDetail.overscrollDirections)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance with known touch handling WHEN copy is called with INPUT_HANDLING_UNKNOWN THEN this is not set`() {
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT)
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLING_UNKNOWN)
+
+ assertEquals(INPUT_HANDLED_CONTENT, inputResultDetail.inputResult)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN equals is called with another object of different type THEN it returns false`() {
+ assertFalse(inputResultDetail == Any())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN equals is called with another instance with different values THEN it returns false`() {
+ var differentInstance = InputResultDetail.newInstance(true)
+ assertFalse(inputResultDetail == differentInstance)
+
+ differentInstance = differentInstance.copy(SCROLL_DIRECTIONS_LEFT, OVERSCROLL_DIRECTIONS_NONE)
+ assertFalse(inputResultDetail == differentInstance)
+
+ differentInstance = differentInstance.copy(INPUT_HANDLED_CONTENT, SCROLL_DIRECTIONS_NONE)
+ assertFalse(inputResultDetail == differentInstance)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN equals is called with another instance with equal values THEN it returns true`() {
+ val equalValuesInstance = InputResultDetail.newInstance()
+
+ assertTrue(inputResultDetail == equalValuesInstance)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN equals is called with the same instance THEN it returns true`() {
+ assertTrue(inputResultDetail == inputResultDetail)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN hashCode is called for same values objects THEN it returns the same result`() {
+ assertEquals(inputResultDetail.hashCode(), inputResultDetail.hashCode())
+
+ assertEquals(inputResultDetail.hashCode(), InputResultDetail.newInstance().hashCode())
+
+ inputResultDetail = inputResultDetail.copy(overscrollDirections = OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertEquals(inputResultDetail.hashCode(), InputResultDetail.newInstance(true).hashCode())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN hashCode is called for different values objects THEN it returns different results`() {
+ assertNotEquals(inputResultDetail.hashCode(), InputResultDetail.newInstance(true).hashCode())
+
+ inputResultDetail = inputResultDetail.copy(OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertNotEquals(inputResultDetail.hashCode(), InputResultDetail.newInstance().hashCode())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertNotEquals(inputResultDetail.hashCode(), InputResultDetail.newInstance().hashCode())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN toString is called THEN it returns a string referring to all data`() {
+ // Add as many details as possible. Scroll and overscroll is not possible at the same time.
+ inputResultDetail = inputResultDetail.copy(
+ inputResult = INPUT_HANDLED,
+ scrollDirections = SCROLL_DIRECTIONS_LEFT or SCROLL_DIRECTIONS_RIGHT or
+ SCROLL_DIRECTIONS_TOP or SCROLL_DIRECTIONS_BOTTOM,
+ )
+
+ val result = inputResultDetail.toString()
+
+ assertEquals(
+ StringBuilder("InputResultDetail \$${inputResultDetail.hashCode()} (")
+ .append("Input ${inputResultDetail.getInputResultHandledDescription()}. ")
+ .append(
+ "Content ${inputResultDetail.getScrollDirectionsDescription()} " +
+ "and ${inputResultDetail.getOverscrollDirectionsDescription()}",
+ )
+ .append(')')
+ .toString(),
+ result,
+ )
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN getInputResultHandledDescription is called THEN returns a string describing who will handle the touch`() {
+ assertEquals(INPUT_UNKNOWN_HANDLING_DESCRIPTION, inputResultDetail.getInputResultHandledDescription())
+
+ assertEquals(
+ INPUT_UNHANDLED_TOSTRING_DESCRIPTION,
+ inputResultDetail.copy(INPUT_UNHANDLED).getInputResultHandledDescription(),
+ )
+
+ assertEquals(
+ INPUT_HANDLED_TOSTRING_DESCRIPTION,
+ inputResultDetail.copy(INPUT_HANDLED).getInputResultHandledDescription(),
+ )
+
+ assertEquals(
+ INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION,
+ inputResultDetail.copy(INPUT_HANDLED_CONTENT).getInputResultHandledDescription(),
+ )
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN getScrollDirectionsDescription is called THEN it returns a string describing what scrolling is possible`() {
+ assertEquals(SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION, inputResultDetail.getScrollDirectionsDescription())
+
+ inputResultDetail = inputResultDetail.copy(
+ inputResult = INPUT_HANDLED,
+ scrollDirections = SCROLL_DIRECTIONS_LEFT or SCROLL_DIRECTIONS_RIGHT or
+ SCROLL_DIRECTIONS_TOP or SCROLL_DIRECTIONS_BOTTOM,
+ )
+
+ assertEquals(
+ SCROLL_TOSTRING_DESCRIPTION +
+ "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" +
+ "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" +
+ "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" +
+ SCROLL_BOTTOM_TOSTRING_DESCRIPTION,
+ inputResultDetail.getScrollDirectionsDescription(),
+ )
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN getScrollDirectionsDescription is called for an unhandled touch THEN returns a string describing impossible scroll`() {
+ assertEquals(SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION, inputResultDetail.getScrollDirectionsDescription())
+
+ inputResultDetail = inputResultDetail.copy(
+ scrollDirections = SCROLL_DIRECTIONS_LEFT or SCROLL_DIRECTIONS_RIGHT or
+ SCROLL_DIRECTIONS_TOP or SCROLL_DIRECTIONS_BOTTOM,
+ )
+
+ assertEquals(SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION, inputResultDetail.getScrollDirectionsDescription())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN getOverscrollDirectionsDescription is called THEN it returns a string describing what overscrolling is possible`() {
+ assertEquals(
+ OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION,
+ inputResultDetail.getOverscrollDirectionsDescription(),
+ )
+
+ inputResultDetail = inputResultDetail.copy(
+ inputResult = INPUT_HANDLED,
+ overscrollDirections = OVERSCROLL_DIRECTIONS_VERTICAL or OVERSCROLL_DIRECTIONS_HORIZONTAL,
+ )
+
+ assertEquals(
+ OVERSCROLL_TOSTRING_DESCRIPTION +
+ "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" +
+ "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" +
+ "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" +
+ SCROLL_BOTTOM_TOSTRING_DESCRIPTION,
+ inputResultDetail.getOverscrollDirectionsDescription(),
+ )
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail WHEN getOverscrollDirectionsDescription is called for unhandled touch THEN returns a string describing impossible overscroll`() {
+ assertEquals(
+ OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION,
+ inputResultDetail.getOverscrollDirectionsDescription(),
+ )
+
+ inputResultDetail = inputResultDetail.copy(
+ inputResult = INPUT_HANDLED_CONTENT,
+ overscrollDirections = OVERSCROLL_DIRECTIONS_VERTICAL or OVERSCROLL_DIRECTIONS_HORIZONTAL,
+ )
+
+ assertEquals(
+ OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION,
+ inputResultDetail.getOverscrollDirectionsDescription(),
+ )
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN isTouchHandlingUnknown is called THEN it returns true only if the inputResult is INPUT_HANDLING_UNKNOWN`() {
+ assertTrue(inputResultDetail.isTouchHandlingUnknown())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.isTouchHandlingUnknown())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT)
+ assertFalse(inputResultDetail.isTouchHandlingUnknown())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_UNHANDLED)
+ assertFalse(inputResultDetail.isTouchHandlingUnknown())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN isTouchHandledByBrowser is called THEN it returns true only if the inputResult is INPUT_HANDLED`() {
+ assertFalse(inputResultDetail.isTouchHandledByBrowser())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertTrue(inputResultDetail.isTouchHandledByBrowser())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT)
+ assertFalse(inputResultDetail.isTouchHandledByBrowser())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN isTouchHandledByWebsite is called THEN it returns true only if the inputResult is INPUT_HANDLED_CONTENT`() {
+ assertFalse(inputResultDetail.isTouchHandledByWebsite())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.isTouchHandledByWebsite())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT)
+ assertTrue(inputResultDetail.isTouchHandledByWebsite())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN isTouchUnhandled is called THEN it returns true only if the inputResult is INPUT_UNHANDLED`() {
+ assertFalse(inputResultDetail.isTouchUnhandled())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.isTouchUnhandled())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT)
+ assertFalse(inputResultDetail.isTouchUnhandled())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_UNHANDLED)
+ assertTrue(inputResultDetail.isTouchUnhandled())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN canScrollToLeft is called THEN it returns true only if the browser can scroll the page to left`() {
+ assertFalse(inputResultDetail.canScrollToLeft())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.canScrollToLeft())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT, SCROLL_DIRECTIONS_LEFT)
+ assertFalse(inputResultDetail.canScrollToLeft())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertTrue(inputResultDetail.canScrollToLeft())
+
+ inputResultDetail = inputResultDetail.copy(overscrollDirections = OVERSCROLL_DIRECTIONS_NONE)
+ assertTrue(inputResultDetail.canScrollToLeft())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN canScrollToTop is called THEN it returns true only if the browser can scroll the page to top`() {
+ assertFalse(inputResultDetail.canScrollToTop())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.canScrollToTop())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT, SCROLL_DIRECTIONS_TOP)
+ assertFalse(inputResultDetail.canScrollToTop())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertTrue(inputResultDetail.canScrollToTop())
+
+ inputResultDetail = inputResultDetail.copy(overscrollDirections = OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertTrue(inputResultDetail.canScrollToTop())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN canScrollToRight is called THEN it returns true only if the browser can scroll the page to right`() {
+ assertFalse(inputResultDetail.canScrollToRight())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.canScrollToRight())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT, SCROLL_DIRECTIONS_RIGHT)
+ assertFalse(inputResultDetail.canScrollToRight())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertTrue(inputResultDetail.canScrollToRight())
+
+ inputResultDetail = inputResultDetail.copy(overscrollDirections = OVERSCROLL_DIRECTIONS_HORIZONTAL)
+ assertTrue(inputResultDetail.canScrollToRight())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN canScrollToBottom is called THEN it returns true only if the browser can scroll the page to bottom`() {
+ assertFalse(inputResultDetail.canScrollToBottom())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.canScrollToBottom())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT, SCROLL_DIRECTIONS_BOTTOM)
+ assertFalse(inputResultDetail.canScrollToBottom())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertTrue(inputResultDetail.canScrollToBottom())
+
+ inputResultDetail = inputResultDetail.copy(overscrollDirections = OVERSCROLL_DIRECTIONS_NONE)
+ assertTrue(inputResultDetail.canScrollToBottom())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN canOverscrollLeft is called THEN it returns true only in certain scenarios`() {
+ // The scenarios (for which there is not enough space in the method name) being:
+ // - event is not handled by the webpage
+ // - webpage cannot be scrolled to the left in which case scroll would need to happen first
+ // - the content can be overscrolled to the left. Webpages can request overscroll to be disabled.
+
+ assertFalse(inputResultDetail.canOverscrollLeft())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED, SCROLL_DIRECTIONS_BOTTOM, OVERSCROLL_DIRECTIONS_HORIZONTAL)
+ assertTrue(inputResultDetail.canOverscrollLeft())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_UNHANDLED)
+ assertTrue(inputResultDetail.canOverscrollLeft())
+
+ inputResultDetail = inputResultDetail.copy(scrollDirections = SCROLL_DIRECTIONS_LEFT)
+ assertFalse(inputResultDetail.canOverscrollLeft())
+
+ inputResultDetail = inputResultDetail.copy(scrollDirections = SCROLL_DIRECTIONS_TOP, overscrollDirections = OVERSCROLL_DIRECTIONS_HORIZONTAL)
+ assertTrue(inputResultDetail.canOverscrollLeft())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED, SCROLL_DIRECTIONS_RIGHT, OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertFalse(inputResultDetail.canOverscrollLeft())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN canOverscrollTop is called THEN it returns true only in certain scenarios`() {
+ // The scenarios (for which there is not enough space in the method name) being:
+ // - event is not handled by the webpage
+ // - webpage cannot be scrolled to the top in which case scroll would need to happen first
+ // - the content can be overscrolled to the top. Webpages can request overscroll to be disabled.
+
+ assertFalse(inputResultDetail.canOverscrollTop())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT)
+ assertFalse(inputResultDetail.canOverscrollTop())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.canOverscrollTop())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT, SCROLL_DIRECTIONS_TOP, OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertFalse(inputResultDetail.canOverscrollTop())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_UNHANDLED, SCROLL_DIRECTIONS_LEFT, OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertTrue(inputResultDetail.canOverscrollTop())
+
+ inputResultDetail = inputResultDetail.copy(overscrollDirections = OVERSCROLL_DIRECTIONS_HORIZONTAL)
+ assertFalse(inputResultDetail.canOverscrollTop())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN canOverscrollRight is called THEN it returns true only in certain scenarios`() {
+ // The scenarios (for which there is not enough space in the method name) being:
+ // - event is not handled by the webpage
+ // - webpage cannot be scrolled to the right in which case scroll would need to happen first
+ // - the content can be overscrolled to the right. Webpages can request overscroll to be disabled.
+
+ assertFalse(inputResultDetail.canOverscrollRight())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED, SCROLL_DIRECTIONS_BOTTOM, OVERSCROLL_DIRECTIONS_HORIZONTAL)
+ assertTrue(inputResultDetail.canOverscrollRight())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_UNHANDLED)
+ assertTrue(inputResultDetail.canOverscrollRight())
+
+ inputResultDetail = inputResultDetail.copy(scrollDirections = SCROLL_DIRECTIONS_RIGHT)
+ assertFalse(inputResultDetail.canOverscrollRight())
+
+ inputResultDetail = inputResultDetail.copy(scrollDirections = SCROLL_DIRECTIONS_TOP, overscrollDirections = OVERSCROLL_DIRECTIONS_HORIZONTAL)
+ assertTrue(inputResultDetail.canOverscrollRight())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED, SCROLL_DIRECTIONS_LEFT, OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertFalse(inputResultDetail.canOverscrollRight())
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail instance WHEN canOverscrollBottom is called THEN it returns true only in certain scenarios`() {
+ // The scenarios (for which there is not enough space in the method name) being:
+ // - event is not handled by the webpage
+ // - webpage cannot be scrolled to the bottom in which case scroll would need to happen first
+ // - the content can be overscrolled to the bottom. Webpages can request overscroll to be disabled.
+
+ assertFalse(inputResultDetail.canOverscrollBottom())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT)
+ assertFalse(inputResultDetail.canOverscrollBottom())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED)
+ assertFalse(inputResultDetail.canOverscrollBottom())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_HANDLED_CONTENT, SCROLL_DIRECTIONS_BOTTOM, OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertFalse(inputResultDetail.canOverscrollBottom())
+
+ inputResultDetail = inputResultDetail.copy(INPUT_UNHANDLED, SCROLL_DIRECTIONS_LEFT, OVERSCROLL_DIRECTIONS_VERTICAL)
+ assertTrue(inputResultDetail.canOverscrollBottom())
+
+ inputResultDetail = inputResultDetail.copy(overscrollDirections = OVERSCROLL_DIRECTIONS_HORIZONTAL)
+ assertFalse(inputResultDetail.canOverscrollBottom())
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/SettingsTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/SettingsTest.kt
new file mode 100644
index 0000000000..abdb1e8424
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/SettingsTest.kt
@@ -0,0 +1,227 @@
+/* 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.concept.engine
+
+import android.graphics.Color
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.support.test.expectException
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SettingsTest {
+
+ @Test
+ fun settingsThrowByDefault() {
+ val settings = object : Settings() { }
+
+ expectUnsupportedSettingException(
+ { settings.javascriptEnabled },
+ { settings.javascriptEnabled = false },
+ { settings.domStorageEnabled },
+ { settings.domStorageEnabled = false },
+ { settings.webFontsEnabled },
+ { settings.webFontsEnabled = false },
+ { settings.automaticFontSizeAdjustment },
+ { settings.automaticFontSizeAdjustment = false },
+ { settings.automaticLanguageAdjustment },
+ { settings.automaticLanguageAdjustment = false },
+ { settings.trackingProtectionPolicy },
+ { settings.trackingProtectionPolicy = TrackingProtectionPolicy.strict() },
+ { settings.historyTrackingDelegate },
+ { settings.historyTrackingDelegate = null },
+ { settings.requestInterceptor },
+ { settings.requestInterceptor = null },
+ { settings.userAgentString },
+ { settings.userAgentString = null },
+ { settings.mediaPlaybackRequiresUserGesture },
+ { settings.mediaPlaybackRequiresUserGesture = false },
+ { settings.javaScriptCanOpenWindowsAutomatically },
+ { settings.javaScriptCanOpenWindowsAutomatically = false },
+ { settings.displayZoomControls },
+ { settings.displayZoomControls = false },
+ { settings.loadWithOverviewMode },
+ { settings.loadWithOverviewMode = false },
+ { settings.useWideViewPort },
+ { settings.useWideViewPort = null },
+ { settings.allowFileAccess },
+ { settings.allowFileAccess = false },
+ { settings.allowContentAccess },
+ { settings.allowContentAccess = false },
+ { settings.allowFileAccessFromFileURLs },
+ { settings.allowFileAccessFromFileURLs = false },
+ { settings.allowUniversalAccessFromFileURLs },
+ { settings.allowUniversalAccessFromFileURLs = false },
+ { settings.verticalScrollBarEnabled },
+ { settings.verticalScrollBarEnabled = false },
+ { settings.horizontalScrollBarEnabled },
+ { settings.horizontalScrollBarEnabled = false },
+ { settings.remoteDebuggingEnabled },
+ { settings.remoteDebuggingEnabled = false },
+ { settings.supportMultipleWindows },
+ { settings.supportMultipleWindows = false },
+ { settings.preferredColorScheme },
+ { settings.preferredColorScheme = PreferredColorScheme.System },
+ { settings.testingModeEnabled },
+ { settings.testingModeEnabled = false },
+ { settings.suspendMediaWhenInactive },
+ { settings.suspendMediaWhenInactive = false },
+ { settings.fontInflationEnabled },
+ { settings.fontInflationEnabled = false },
+ { settings.fontSizeFactor },
+ { settings.fontSizeFactor = 1.0F },
+ { settings.forceUserScalableContent },
+ { settings.forceUserScalableContent = true },
+ { settings.loginAutofillEnabled },
+ { settings.loginAutofillEnabled = false },
+ { settings.clearColor },
+ { settings.clearColor = Color.BLUE },
+ { settings.enterpriseRootsEnabled },
+ { settings.enterpriseRootsEnabled = false },
+ { settings.emailTrackerBlockingPrivateBrowsing },
+ )
+ }
+
+ private fun expectUnsupportedSettingException(vararg blocks: () -> Unit) {
+ blocks.forEach { block ->
+ expectException(UnsupportedSettingException::class, block)
+ }
+ }
+
+ @Test
+ fun defaultSettings() {
+ val settings = DefaultSettings()
+ assertTrue(settings.domStorageEnabled)
+ assertTrue(settings.javascriptEnabled)
+ assertNull(settings.trackingProtectionPolicy)
+ assertNull(settings.historyTrackingDelegate)
+ assertNull(settings.requestInterceptor)
+ assertNull(settings.userAgentString)
+ assertTrue(settings.mediaPlaybackRequiresUserGesture)
+ assertFalse(settings.javaScriptCanOpenWindowsAutomatically)
+ assertTrue(settings.displayZoomControls)
+ assertTrue(settings.automaticFontSizeAdjustment)
+ assertTrue(settings.automaticLanguageAdjustment)
+ assertFalse(settings.loadWithOverviewMode)
+ assertNull(settings.useWideViewPort)
+ assertTrue(settings.allowContentAccess)
+ assertTrue(settings.allowFileAccess)
+ assertFalse(settings.allowFileAccessFromFileURLs)
+ assertFalse(settings.allowUniversalAccessFromFileURLs)
+ assertTrue(settings.verticalScrollBarEnabled)
+ assertTrue(settings.horizontalScrollBarEnabled)
+ assertFalse(settings.remoteDebuggingEnabled)
+ assertFalse(settings.supportMultipleWindows)
+ assertEquals(PreferredColorScheme.System, settings.preferredColorScheme)
+ assertFalse(settings.testingModeEnabled)
+ assertFalse(settings.suspendMediaWhenInactive)
+ assertNull(settings.fontInflationEnabled)
+ assertNull(settings.fontSizeFactor)
+ assertFalse(settings.forceUserScalableContent)
+ assertFalse(settings.loginAutofillEnabled)
+ assertNull(settings.clearColor)
+ assertFalse(settings.enterpriseRootsEnabled)
+ assertFalse(settings.queryParameterStripping)
+ assertFalse(settings.queryParameterStrippingPrivateBrowsing)
+ assertFalse(settings.emailTrackerBlockingPrivateBrowsing)
+ assertEquals("", settings.queryParameterStrippingAllowList)
+ assertEquals("", settings.queryParameterStrippingStripList)
+ assertEquals(EngineSession.CookieBannerHandlingMode.DISABLED, settings.cookieBannerHandlingMode)
+ assertEquals(EngineSession.CookieBannerHandlingMode.DISABLED, settings.cookieBannerHandlingModePrivateBrowsing)
+
+ val interceptor: RequestInterceptor = mock()
+ val historyTrackingDelegate: HistoryTrackingDelegate = mock()
+
+ val defaultSettings = DefaultSettings(
+ javascriptEnabled = false,
+ domStorageEnabled = false,
+ webFontsEnabled = false,
+ automaticFontSizeAdjustment = false,
+ automaticLanguageAdjustment = false,
+ trackingProtectionPolicy = TrackingProtectionPolicy.strict(),
+ historyTrackingDelegate = historyTrackingDelegate,
+ requestInterceptor = interceptor,
+ userAgentString = "userAgent",
+ mediaPlaybackRequiresUserGesture = false,
+ javaScriptCanOpenWindowsAutomatically = true,
+ displayZoomControls = false,
+ loadWithOverviewMode = true,
+ useWideViewPort = true,
+ allowContentAccess = false,
+ allowFileAccess = false,
+ allowFileAccessFromFileURLs = true,
+ allowUniversalAccessFromFileURLs = true,
+ verticalScrollBarEnabled = false,
+ horizontalScrollBarEnabled = false,
+ remoteDebuggingEnabled = true,
+ supportMultipleWindows = true,
+ preferredColorScheme = PreferredColorScheme.Dark,
+ testingModeEnabled = true,
+ suspendMediaWhenInactive = true,
+ fontInflationEnabled = false,
+ fontSizeFactor = 2.0F,
+ forceUserScalableContent = true,
+ loginAutofillEnabled = true,
+ clearColor = Color.BLUE,
+ enterpriseRootsEnabled = true,
+ queryParameterStripping = true,
+ queryParameterStrippingPrivateBrowsing = true,
+ queryParameterStrippingAllowList = "AllowList",
+ queryParameterStrippingStripList = "StripList",
+ cookieBannerHandlingModePrivateBrowsing = EngineSession.CookieBannerHandlingMode.REJECT_ALL,
+ cookieBannerHandlingDetectOnlyMode = true,
+ cookieBannerHandlingGlobalRules = true,
+ cookieBannerHandlingGlobalRulesSubFrames = true,
+ emailTrackerBlockingPrivateBrowsing = true,
+ )
+
+ assertFalse(defaultSettings.domStorageEnabled)
+ assertFalse(defaultSettings.javascriptEnabled)
+ assertFalse(defaultSettings.webFontsEnabled)
+ assertFalse(defaultSettings.automaticFontSizeAdjustment)
+ assertFalse(defaultSettings.automaticLanguageAdjustment)
+ assertEquals(TrackingProtectionPolicy.strict(), defaultSettings.trackingProtectionPolicy)
+ assertEquals(historyTrackingDelegate, defaultSettings.historyTrackingDelegate)
+ assertEquals(interceptor, defaultSettings.requestInterceptor)
+ assertEquals("userAgent", defaultSettings.userAgentString)
+ assertFalse(defaultSettings.mediaPlaybackRequiresUserGesture)
+ assertTrue(defaultSettings.javaScriptCanOpenWindowsAutomatically)
+ assertFalse(defaultSettings.displayZoomControls)
+ assertTrue(defaultSettings.loadWithOverviewMode)
+ assertEquals(defaultSettings.useWideViewPort, true)
+ assertFalse(defaultSettings.allowContentAccess)
+ assertFalse(defaultSettings.allowFileAccess)
+ assertTrue(defaultSettings.allowFileAccessFromFileURLs)
+ assertTrue(defaultSettings.allowUniversalAccessFromFileURLs)
+ assertFalse(defaultSettings.verticalScrollBarEnabled)
+ assertFalse(defaultSettings.horizontalScrollBarEnabled)
+ assertTrue(defaultSettings.remoteDebuggingEnabled)
+ assertTrue(defaultSettings.supportMultipleWindows)
+ assertEquals(PreferredColorScheme.Dark, defaultSettings.preferredColorScheme)
+ assertTrue(defaultSettings.testingModeEnabled)
+ assertTrue(defaultSettings.suspendMediaWhenInactive)
+ assertFalse(defaultSettings.fontInflationEnabled!!)
+ assertEquals(2.0F, defaultSettings.fontSizeFactor)
+ assertTrue(defaultSettings.forceUserScalableContent)
+ assertEquals(Color.BLUE, defaultSettings.clearColor)
+ assertTrue(defaultSettings.enterpriseRootsEnabled)
+ assertTrue(defaultSettings.queryParameterStripping)
+ assertTrue(defaultSettings.queryParameterStrippingPrivateBrowsing)
+ assertEquals("AllowList", defaultSettings.queryParameterStrippingAllowList)
+ assertEquals("StripList", defaultSettings.queryParameterStrippingStripList)
+ assertEquals(EngineSession.CookieBannerHandlingMode.DISABLED, defaultSettings.cookieBannerHandlingMode)
+ assertEquals(EngineSession.CookieBannerHandlingMode.REJECT_ALL, defaultSettings.cookieBannerHandlingModePrivateBrowsing)
+ assertTrue(defaultSettings.cookieBannerHandlingDetectOnlyMode)
+ assertTrue(defaultSettings.cookieBannerHandlingGlobalRules)
+ assertTrue(defaultSettings.cookieBannerHandlingGlobalRulesSubFrames)
+ assertTrue(defaultSettings.emailTrackerBlockingPrivateBrowsing)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/SizeTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/SizeTest.kt
new file mode 100644
index 0000000000..e3a8c493ae
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/SizeTest.kt
@@ -0,0 +1,45 @@
+/* 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.concept.engine.manifest
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class SizeTest {
+
+ @Test
+ fun `parse standard sizes`() {
+ assertEquals(Size(512, 512), Size.parse("512x512"))
+ assertEquals(Size(16, 16), Size.parse("16x16"))
+ assertEquals(Size(100, 250), Size.parse("100x250"))
+ }
+
+ @Test
+ fun `get max and min lengths`() {
+ assertEquals(512, Size(512, 256).maxLength)
+ assertEquals(256, Size(512, 256).minLength)
+ assertEquals(250, Size(100, 250).maxLength)
+ assertEquals(100, Size(100, 250).minLength)
+ }
+
+ @Test
+ fun `parse any size`() {
+ assertEquals(Size.ANY, Size.parse("any"))
+ assertEquals(Size.ANY.width, Size.parse("any")!!.maxLength)
+ assertEquals(Size.ANY.height, Size.parse("any")!!.maxLength)
+ assertEquals(Size.ANY.width, Size.parse("any")!!.minLength)
+ assertEquals(Size.ANY.height, Size.parse("any")!!.minLength)
+ }
+
+ @Test
+ fun `return null for invalid sizes`() {
+ assertNull(Size.parse("192"))
+ assertNull(Size.parse("anywhere"))
+ assertNull(Size.parse("fooxbar"))
+ assertNull(Size.parse("x256"))
+ assertNull(Size.parse("64x"))
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/WebAppManifestParserTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/WebAppManifestParserTest.kt
new file mode 100644
index 0000000000..a00c111e2c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/manifest/WebAppManifestParserTest.kt
@@ -0,0 +1,603 @@
+/* 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.concept.engine.manifest
+
+import android.graphics.Color
+import android.graphics.Color.rgb
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.file.loadResourceAsString
+import org.json.JSONObject
+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.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WebAppManifestParserTest {
+
+ @Test
+ fun `getOrNull returns parsed manifest`() {
+ val sucessfulResult = WebAppManifestParser().parse(loadManifest("example_mdn.json"))
+ assertNotNull(sucessfulResult.getOrNull())
+
+ val failedResult = WebAppManifestParser().parse(loadManifest("invalid_json.json"))
+ assertNull(failedResult.getOrNull())
+ }
+
+ @Test
+ fun `Parsing example manifest from MDN`() {
+ val json = loadManifest("example_mdn.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("HackerWeb", manifest.name)
+ assertEquals("HackerWeb", manifest.shortName)
+ assertEquals(".", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.STANDALONE, manifest.display)
+ assertEquals(Color.WHITE, manifest.backgroundColor)
+ assertEquals("A simply readable Hacker News app.", manifest.description)
+ assertEquals(WebAppManifest.TextDirection.AUTO, manifest.dir)
+ assertNull(manifest.lang)
+ assertEquals(WebAppManifest.Orientation.ANY, manifest.orientation)
+ assertNull(manifest.scope)
+ assertNull(manifest.themeColor)
+
+ assertEquals(6, manifest.icons.size)
+
+ assertEquals("images/touch/homescreen48.png", manifest.icons[0].src)
+ assertEquals("images/touch/homescreen72.png", manifest.icons[1].src)
+ assertEquals("images/touch/homescreen96.png", manifest.icons[2].src)
+ assertEquals("images/touch/homescreen144.png", manifest.icons[3].src)
+ assertEquals("images/touch/homescreen168.png", manifest.icons[4].src)
+ assertEquals("images/touch/homescreen192.png", manifest.icons[5].src)
+
+ assertEquals("image/png", manifest.icons[0].type)
+ assertEquals("image/png", manifest.icons[1].type)
+ assertEquals("image/png", manifest.icons[2].type)
+ assertEquals("image/png", manifest.icons[3].type)
+ assertEquals("image/png", manifest.icons[4].type)
+ assertEquals("image/png", manifest.icons[5].type)
+
+ assertEquals(1, manifest.icons[0].sizes.size)
+ assertEquals(1, manifest.icons[1].sizes.size)
+ assertEquals(1, manifest.icons[2].sizes.size)
+ assertEquals(1, manifest.icons[3].sizes.size)
+ assertEquals(1, manifest.icons[4].sizes.size)
+ assertEquals(1, manifest.icons[5].sizes.size)
+
+ assertEquals(48, manifest.icons[0].sizes[0].width)
+ assertEquals(72, manifest.icons[1].sizes[0].width)
+ assertEquals(96, manifest.icons[2].sizes[0].width)
+ assertEquals(144, manifest.icons[3].sizes[0].width)
+ assertEquals(168, manifest.icons[4].sizes[0].width)
+ assertEquals(192, manifest.icons[5].sizes[0].width)
+
+ assertEquals(48, manifest.icons[0].sizes[0].height)
+ assertEquals(72, manifest.icons[1].sizes[0].height)
+ assertEquals(96, manifest.icons[2].sizes[0].height)
+ assertEquals(144, manifest.icons[3].sizes[0].height)
+ assertEquals(168, manifest.icons[4].sizes[0].height)
+ assertEquals(192, manifest.icons[5].sizes[0].height)
+
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), manifest.icons[0].purpose)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), manifest.icons[1].purpose)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), manifest.icons[2].purpose)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), manifest.icons[3].purpose)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), manifest.icons[4].purpose)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), manifest.icons[5].purpose)
+ }
+
+ @Test
+ fun `Parsing example manifest from Google`() {
+ val json = loadManifest("example_google.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("Google Maps", manifest.name)
+ assertEquals("Maps", manifest.shortName)
+ assertEquals("/maps/?source=pwa", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.STANDALONE, manifest.display)
+ assertEquals(rgb(51, 103, 214), manifest.backgroundColor)
+ assertNull(manifest.description)
+ assertEquals(WebAppManifest.TextDirection.AUTO, manifest.dir)
+ assertNull(manifest.lang)
+ assertEquals(WebAppManifest.Orientation.ANY, manifest.orientation)
+ assertEquals("/maps/", manifest.scope)
+ assertEquals(rgb(51, 103, 214), manifest.themeColor)
+
+ assertEquals(2, manifest.icons.size)
+
+ manifest.icons[0].apply {
+ assertEquals("/images/icons-192.png", src)
+ assertEquals("image/png", type)
+ assertEquals(1, sizes.size)
+ assertEquals(192, sizes[0].width)
+ assertEquals(192, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+
+ manifest.icons[1].apply {
+ assertEquals("/images/icons-512.png", src)
+ assertEquals("image/png", type)
+ assertEquals(1, sizes.size)
+ assertEquals(512, sizes[0].width)
+ assertEquals(512, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+ }
+
+ @Test
+ fun `Parsing twitter mobile manifest`() {
+ val json = loadManifest("twitter_mobile.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("Twitter", manifest.name)
+ assertEquals("Twitter", manifest.shortName)
+ assertEquals("/", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.STANDALONE, manifest.display)
+ assertEquals(Color.WHITE, manifest.backgroundColor)
+ assertEquals(
+ "It's what's happening. From breaking news and entertainment, sports and politics, " +
+ "to big events and everyday interests.",
+ manifest.description,
+ )
+ assertEquals(WebAppManifest.TextDirection.AUTO, manifest.dir)
+ assertNull(manifest.lang)
+ assertEquals(WebAppManifest.Orientation.ANY, manifest.orientation)
+ assertEquals("/", manifest.scope)
+ assertEquals(Color.WHITE, manifest.themeColor)
+
+ assertEquals(2, manifest.icons.size)
+
+ manifest.icons[0].apply {
+ assertEquals("https://abs.twimg.com/responsive-web/web/icon-default.604e2486a34a2f6e1.png", src)
+ assertEquals("image/png", type)
+ assertEquals(1, sizes.size)
+ assertEquals(192, sizes[0].width)
+ assertEquals(192, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+
+ manifest.icons[1].apply {
+ assertEquals("https://abs.twimg.com/responsive-web/web/icon-default.604e2486a34a2f6e1.png", src)
+ assertEquals("image/png", type)
+ assertEquals(1, sizes.size)
+ assertEquals(512, sizes[0].width)
+ assertEquals(512, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+ }
+
+ @Test
+ fun `Parsing minimal manifest`() {
+ val json = loadManifest("minimal.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("Minimal", manifest.name)
+ assertNull(manifest.shortName)
+ assertEquals("/", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.BROWSER, manifest.display)
+ assertNull(manifest.backgroundColor)
+ assertNull(manifest.description)
+ assertEquals(WebAppManifest.TextDirection.AUTO, manifest.dir)
+ assertNull(manifest.lang)
+ assertEquals(WebAppManifest.Orientation.ANY, manifest.orientation)
+ assertNull(manifest.scope)
+ assertNull(manifest.themeColor)
+
+ assertEquals(0, manifest.icons.size)
+ }
+
+ @Test
+ fun `Parsing manifest with no name`() {
+ val json = loadManifest("minimal_short_name.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("Minimal with Short Name", manifest.name)
+ assertEquals("Minimal with Short Name", manifest.shortName)
+ assertEquals("/", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.BROWSER, manifest.display)
+ assertNull(manifest.backgroundColor)
+ assertNull(manifest.description)
+ assertEquals(WebAppManifest.TextDirection.AUTO, manifest.dir)
+ assertNull(manifest.lang)
+ assertEquals(WebAppManifest.Orientation.ANY, manifest.orientation)
+ assertNull(manifest.scope)
+ assertNull(manifest.themeColor)
+
+ assertEquals(0, manifest.icons.size)
+ }
+
+ @Test
+ fun `Parsing typical manifest from W3 spec`() {
+ val json = loadManifest("spec_typical.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("Super Racer 3000", manifest.name)
+ assertEquals("Racer3K", manifest.shortName)
+ assertEquals("/racer/start.html", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.FULLSCREEN, manifest.display)
+ assertEquals(Color.RED, manifest.backgroundColor)
+ assertEquals("The ultimate futuristic racing game from the future!", manifest.description)
+ assertEquals(WebAppManifest.TextDirection.LTR, manifest.dir)
+ assertEquals("en", manifest.lang)
+ assertEquals(WebAppManifest.Orientation.LANDSCAPE, manifest.orientation)
+ assertEquals("/racer/", manifest.scope)
+ assertEquals(rgb(240, 248, 255), manifest.themeColor)
+
+ assertEquals(3, manifest.icons.size)
+
+ manifest.icons[0].apply {
+ assertEquals("icon/lowres.webp", src)
+ assertEquals("image/webp", type)
+ assertEquals(1, sizes.size)
+ assertEquals(64, sizes[0].width)
+ assertEquals(64, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+
+ manifest.icons[1].apply {
+ assertEquals("icon/lowres.png", src)
+ assertNull(type)
+ assertEquals(1, sizes.size)
+ assertEquals(64, sizes[0].width)
+ assertEquals(64, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+
+ manifest.icons[2].apply {
+ assertEquals("icon/hd_hi", src)
+ assertNull(type)
+ assertEquals(1, sizes.size)
+ assertEquals(128, sizes[0].width)
+ assertEquals(128, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+
+ assertEquals(2, manifest.relatedApplications.size)
+ assertFalse(manifest.preferRelatedApplications)
+
+ manifest.relatedApplications[0].apply {
+ assertEquals("play", platform)
+ assertEquals("https://play.google.com/store/apps/details?id=com.example.app1", url)
+ assertEquals("com.example.app1", id)
+ assertEquals("2", minVersion)
+ assertEquals(1, fingerprints.size)
+
+ assertEquals(
+ WebAppManifest.ExternalApplicationResource.Fingerprint(
+ type = "sha256_cert",
+ value = "92:5A:39:05:C5:B9:EA:BC:71:48:5F:F2",
+ ),
+ fingerprints[0],
+ )
+ }
+
+ manifest.relatedApplications[1].apply {
+ assertEquals("itunes", platform)
+ assertEquals("https://itunes.apple.com/app/example-app1/id123456789", url)
+ assertNull(id)
+ assertNull(minVersion)
+ assertEquals(0, fingerprints.size)
+ }
+ }
+
+ @Test
+ fun `Parsing manifest from Squoosh`() {
+ val json = loadManifest("squoosh.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("Squoosh", manifest.name)
+ assertEquals("Squoosh", manifest.shortName)
+ assertEquals("/", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.STANDALONE, manifest.display)
+ assertEquals(Color.WHITE, manifest.backgroundColor)
+ assertEquals(WebAppManifest.TextDirection.AUTO, manifest.dir)
+ assertEquals(WebAppManifest.Orientation.ANY, manifest.orientation)
+ assertNull(manifest.scope)
+ assertEquals(rgb(247, 143, 33), manifest.themeColor)
+
+ assertEquals(1, manifest.icons.size)
+
+ manifest.icons[0].apply {
+ assertEquals("/assets/icon-large.png", src)
+ assertEquals("image/png", type)
+ assertEquals(listOf(Size(1024, 1024)), sizes)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+
+ manifest.shareTarget!!.apply {
+ assertEquals("/?share-target", action)
+ assertEquals(WebAppManifest.ShareTarget.RequestMethod.POST, method)
+ assertEquals(WebAppManifest.ShareTarget.EncodingType.MULTIPART, encType)
+ assertEquals(
+ WebAppManifest.ShareTarget.Params(
+ title = "title",
+ text = "body",
+ url = "uri",
+ files = listOf(
+ WebAppManifest.ShareTarget.Files(
+ name = "file",
+ accept = listOf("image/*"),
+ ),
+ ),
+ ),
+ params,
+ )
+ }
+ }
+
+ @Test
+ fun `Parsing minimal manifest with share target`() {
+ val json = loadManifest("minimal_share_target.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("Minimal", manifest.name)
+ assertEquals("/", manifest.startUrl)
+
+ manifest.shareTarget!!.apply {
+ assertEquals("/share-target", action)
+ assertEquals(WebAppManifest.ShareTarget.RequestMethod.GET, method)
+ assertEquals(WebAppManifest.ShareTarget.EncodingType.URL_ENCODED, encType)
+ assertEquals(
+ WebAppManifest.ShareTarget.Params(
+ files = listOf(
+ WebAppManifest.ShareTarget.Files(
+ name = "file",
+ accept = listOf("image/*"),
+ ),
+ ),
+ ),
+ params,
+ )
+ }
+ }
+
+ @Test
+ fun `Parsing invalid JSON`() {
+ val json = loadManifest("invalid_json.json")
+ val result = WebAppManifestParser().parse(json)
+
+ assertTrue(result is WebAppManifestParser.Result.Failure)
+ }
+
+ @Test
+ fun `Parsing invalid JSON string`() {
+ val json = loadManifestAsString("invalid_json.json")
+ val result = WebAppManifestParser().parse(json)
+
+ assertTrue(result is WebAppManifestParser.Result.Failure)
+ }
+
+ @Test
+ fun `Parsing invalid JSON missing name fields`() {
+ val json = loadManifest("invalid_missing_name.json")
+ val result = WebAppManifestParser().parse(json)
+
+ assertTrue(result is WebAppManifestParser.Result.Failure)
+ }
+
+ @Test
+ fun `Ignore missing share target action`() {
+ val json = loadManifest("minimal.json").apply {
+ put(
+ "share_target",
+ JSONObject().apply {
+ put("method", "POST")
+ },
+ )
+ }
+ val result = WebAppManifestParser().parse(json)
+
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ assertNull(result.getOrNull()!!.shareTarget)
+ }
+
+ @Test
+ fun `Ignore invalid share target method`() {
+ val json = loadManifest("minimal.json").apply {
+ put(
+ "share_target",
+ JSONObject().apply {
+ put("action", "https://mozilla.com/target")
+ put("method", "PATCH")
+ },
+ )
+ }
+ val result = WebAppManifestParser().parse(json)
+
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ assertNull(result.getOrNull()!!.shareTarget)
+ }
+
+ @Test
+ fun `Ignore invalid share target encoding type`() {
+ val json = loadManifest("minimal.json").apply {
+ put(
+ "share_target",
+ JSONObject().apply {
+ put("action", "https://mozilla.com/target")
+ put("enctype", "text/plain")
+ },
+ )
+ }
+ val result = WebAppManifestParser().parse(json)
+
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ assertNull(result.getOrNull()!!.shareTarget)
+ }
+
+ @Test
+ fun `Ignore invalid share target method and encoding type combo`() {
+ val json = loadManifest("minimal.json").apply {
+ put(
+ "share_target",
+ JSONObject().apply {
+ put("action", "https://mozilla.com/target")
+ put("method", "GET")
+ put("enctype", "multipart/form-data")
+ },
+ )
+ }
+ val result = WebAppManifestParser().parse(json)
+
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ assertNull(result.getOrNull()!!.shareTarget)
+ }
+
+ @Test
+ fun `Parsing manifest with unusual values`() {
+ val json = loadManifest("unusual.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("The Sample Manifest", manifest.name)
+ assertEquals("Sample", manifest.shortName)
+ assertEquals("/start", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.MINIMAL_UI, manifest.display)
+ assertNull(manifest.backgroundColor)
+ assertNull(manifest.description)
+ assertEquals(WebAppManifest.TextDirection.RTL, manifest.dir)
+ assertNull(manifest.lang)
+ assertEquals(WebAppManifest.Orientation.PORTRAIT, manifest.orientation)
+ assertEquals("/", manifest.scope)
+ assertNull(manifest.themeColor)
+
+ assertEquals(2, manifest.icons.size)
+
+ manifest.icons[0].apply {
+ assertEquals("/images/icon/favicon.ico", src)
+ assertEquals("image/png", type)
+ assertEquals(3, sizes.size)
+ assertEquals(48, sizes[0].width)
+ assertEquals(48, sizes[0].height)
+ assertEquals(96, sizes[1].width)
+ assertEquals(96, sizes[1].height)
+ assertEquals(128, sizes[2].width)
+ assertEquals(128, sizes[2].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.MONOCHROME), purpose)
+ }
+
+ manifest.icons[1].apply {
+ assertEquals("/images/icon/512-512.png", src)
+ assertEquals("image/png", type)
+ assertEquals(1, sizes.size)
+ assertEquals("image/png", type)
+ assertEquals(512, sizes[0].width)
+ assertEquals(512, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.MASKABLE, WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+ }
+
+ @Test
+ fun `Parsing manifest where purpose field is array instead of string`() {
+ val json = loadManifest("purpose_array.json")
+ val result = WebAppManifestParser().parse(json)
+ assertTrue(result is WebAppManifestParser.Result.Success)
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertNotNull(manifest)
+ assertEquals("The Sample Manifest", manifest.name)
+ assertEquals("Sample", manifest.shortName)
+ assertEquals("/start", manifest.startUrl)
+ assertEquals(WebAppManifest.DisplayMode.MINIMAL_UI, manifest.display)
+ assertNull(manifest.backgroundColor)
+ assertNull(manifest.description)
+ assertEquals(WebAppManifest.TextDirection.RTL, manifest.dir)
+ assertNull(manifest.lang)
+ assertEquals(WebAppManifest.Orientation.PORTRAIT, manifest.orientation)
+ assertEquals("/", manifest.scope)
+ assertNull(manifest.themeColor)
+
+ assertEquals(2, manifest.icons.size)
+
+ manifest.icons[0].apply {
+ assertEquals("/images/icon/favicon.ico", src)
+ assertEquals("image/png", type)
+ assertEquals(3, sizes.size)
+ assertEquals(48, sizes[0].width)
+ assertEquals(48, sizes[0].height)
+ assertEquals(96, sizes[1].width)
+ assertEquals(96, sizes[1].height)
+ assertEquals(128, sizes[2].width)
+ assertEquals(128, sizes[2].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.MONOCHROME), purpose)
+ }
+
+ manifest.icons[1].apply {
+ assertEquals("/images/icon/512-512.png", src)
+ assertEquals("image/png", type)
+ assertEquals(1, sizes.size)
+ assertEquals("image/png", type)
+ assertEquals(512, sizes[0].width)
+ assertEquals(512, sizes[0].height)
+ assertEquals(setOf(WebAppManifest.Icon.Purpose.MASKABLE, WebAppManifest.Icon.Purpose.ANY), purpose)
+ }
+ }
+
+ @Test
+ fun `Serializing minimal manifest`() {
+ val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org")
+ val json = WebAppManifestParser().serialize(manifest)
+
+ assertEquals("Mozilla", json.getString("name"))
+ assertEquals("https://mozilla.org", json.getString("start_url"))
+ }
+
+ @Test
+ fun `Serialize and parse W3 typical manifest`() {
+ val result = WebAppManifestParser().parse(loadManifest("spec_typical.json"))
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertEquals(
+ result,
+ WebAppManifestParser().parse(WebAppManifestParser().serialize(manifest)),
+ )
+ }
+
+ @Test
+ fun `Serialize and parse unusual manifest`() {
+ val result = WebAppManifestParser().parse(loadManifest("unusual.json"))
+ val manifest = (result as WebAppManifestParser.Result.Success).manifest
+
+ assertEquals(
+ result,
+ WebAppManifestParser().parse(WebAppManifestParser().serialize(manifest)),
+ )
+ }
+
+ private fun loadManifestAsString(fileName: String): String =
+ loadResourceAsString("/manifests/$fileName")
+
+ private fun loadManifest(fileName: String): JSONObject =
+ JSONObject(loadManifestAsString(fileName))
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/mediasession/MediaSessionTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/mediasession/MediaSessionTest.kt
new file mode 100644
index 0000000000..15c3423db9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/mediasession/MediaSessionTest.kt
@@ -0,0 +1,230 @@
+/* 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.concept.engine.mediasession
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+
+class MediaSessionTest {
+ @Test
+ fun `media session feature works correctly`() {
+ var features = MediaSession.Feature()
+ assertFalse(features.contains(MediaSession.Feature.PLAY))
+ assertFalse(features.contains(MediaSession.Feature.PAUSE))
+ assertFalse(features.contains(MediaSession.Feature.STOP))
+ assertFalse(features.contains(MediaSession.Feature.SEEK_TO))
+ assertFalse(features.contains(MediaSession.Feature.SEEK_FORWARD))
+ assertFalse(features.contains(MediaSession.Feature.SEEK_BACKWARD))
+ assertFalse(features.contains(MediaSession.Feature.SKIP_AD))
+ assertFalse(features.contains(MediaSession.Feature.NEXT_TRACK))
+ assertFalse(features.contains(MediaSession.Feature.PREVIOUS_TRACK))
+ assertFalse(features.contains(MediaSession.Feature.FOCUS))
+
+ features = MediaSession.Feature(MediaSession.Feature.PLAY + MediaSession.Feature.PAUSE)
+ assert(features.contains(MediaSession.Feature.PLAY))
+ assert(features.contains(MediaSession.Feature.PAUSE))
+ assertFalse(features.contains(MediaSession.Feature.STOP))
+ assertFalse(features.contains(MediaSession.Feature.SEEK_TO))
+ assertFalse(features.contains(MediaSession.Feature.SEEK_FORWARD))
+ assertFalse(features.contains(MediaSession.Feature.SEEK_BACKWARD))
+ assertFalse(features.contains(MediaSession.Feature.SKIP_AD))
+ assertFalse(features.contains(MediaSession.Feature.NEXT_TRACK))
+ assertFalse(features.contains(MediaSession.Feature.PREVIOUS_TRACK))
+ assertFalse(features.contains(MediaSession.Feature.FOCUS))
+
+ features = MediaSession.Feature(MediaSession.Feature.STOP)
+ assertEquals(features, MediaSession.Feature(MediaSession.Feature.STOP))
+ assertEquals(features.hashCode(), MediaSession.Feature(MediaSession.Feature.STOP).hashCode())
+ }
+
+ @Test
+ fun `media session controller interface works correctly`() {
+ val fakeController = FakeController()
+ assertFalse(fakeController.pause)
+ assertFalse(fakeController.stop)
+ assertFalse(fakeController.play)
+ assertFalse(fakeController.seekTo)
+ assertFalse(fakeController.seekForward)
+ assertFalse(fakeController.seekBackward)
+ assertFalse(fakeController.nextTrack)
+ assertFalse(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.pause()
+ assert(fakeController.pause)
+ assertFalse(fakeController.stop)
+ assertFalse(fakeController.play)
+ assertFalse(fakeController.seekTo)
+ assertFalse(fakeController.seekForward)
+ assertFalse(fakeController.seekBackward)
+ assertFalse(fakeController.nextTrack)
+ assertFalse(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.stop()
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assertFalse(fakeController.play)
+ assertFalse(fakeController.seekTo)
+ assertFalse(fakeController.seekForward)
+ assertFalse(fakeController.seekBackward)
+ assertFalse(fakeController.nextTrack)
+ assertFalse(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.play()
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assert(fakeController.play)
+ assertFalse(fakeController.seekTo)
+ assertFalse(fakeController.seekForward)
+ assertFalse(fakeController.seekBackward)
+ assertFalse(fakeController.nextTrack)
+ assertFalse(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.seekTo(123.0, true)
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assert(fakeController.play)
+ assert(fakeController.seekTo)
+ assertFalse(fakeController.seekForward)
+ assertFalse(fakeController.seekBackward)
+ assertFalse(fakeController.nextTrack)
+ assertFalse(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.seekForward()
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assert(fakeController.play)
+ assert(fakeController.seekTo)
+ assert(fakeController.seekForward)
+ assertFalse(fakeController.seekBackward)
+ assertFalse(fakeController.nextTrack)
+ assertFalse(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.seekBackward()
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assert(fakeController.play)
+ assert(fakeController.seekTo)
+ assert(fakeController.seekForward)
+ assert(fakeController.seekBackward)
+ assertFalse(fakeController.nextTrack)
+ assertFalse(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.nextTrack()
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assert(fakeController.play)
+ assert(fakeController.seekTo)
+ assert(fakeController.seekForward)
+ assert(fakeController.seekBackward)
+ assert(fakeController.nextTrack)
+ assertFalse(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.previousTrack()
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assert(fakeController.play)
+ assert(fakeController.seekTo)
+ assert(fakeController.seekForward)
+ assert(fakeController.seekBackward)
+ assert(fakeController.nextTrack)
+ assert(fakeController.previousTrack)
+ assertFalse(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.skipAd()
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assert(fakeController.play)
+ assert(fakeController.seekTo)
+ assert(fakeController.seekForward)
+ assert(fakeController.seekBackward)
+ assert(fakeController.nextTrack)
+ assert(fakeController.previousTrack)
+ assert(fakeController.skipAd)
+ assertFalse(fakeController.muteAudio)
+
+ fakeController.muteAudio(true)
+ assert(fakeController.pause)
+ assert(fakeController.stop)
+ assert(fakeController.play)
+ assert(fakeController.seekTo)
+ assert(fakeController.seekForward)
+ assert(fakeController.seekBackward)
+ assert(fakeController.nextTrack)
+ assert(fakeController.previousTrack)
+ assert(fakeController.skipAd)
+ assert(fakeController.muteAudio)
+ }
+}
+
+private class FakeController : MediaSession.Controller {
+ var pause = false
+ var stop = false
+ var play = false
+ var seekTo = false
+ var seekForward = false
+ var seekBackward = false
+ var nextTrack = false
+ var previousTrack = false
+ var skipAd = false
+ var muteAudio = false
+
+ override fun pause() {
+ pause = true
+ }
+
+ override fun stop() {
+ stop = true
+ }
+
+ override fun play() {
+ play = true
+ }
+
+ override fun seekTo(time: Double, fast: Boolean) {
+ seekTo = true
+ }
+
+ override fun seekForward() {
+ seekForward = true
+ }
+
+ override fun seekBackward() {
+ seekBackward = true
+ }
+
+ override fun nextTrack() {
+ nextTrack = true
+ }
+
+ override fun previousTrack() {
+ previousTrack = true
+ }
+
+ override fun skipAd() {
+ skipAd = true
+ }
+
+ override fun muteAudio(mute: Boolean) {
+ muteAudio = true
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionRequestTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionRequestTest.kt
new file mode 100644
index 0000000000..d5fc5ed50f
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionRequestTest.kt
@@ -0,0 +1,89 @@
+/* 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.concept.engine.permission
+
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class PermissionRequestTest {
+
+ @Test
+ fun `grantIf applies predicate to grant (but not reject) permission requests`() {
+ var request = MockPermissionRequest(listOf(Permission.ContentProtectedMediaId()))
+ request.grantIf { it is Permission.ContentAudioCapture }
+ assertFalse(request.granted)
+ assertFalse(request.rejected)
+
+ request.grantIf { it is Permission.ContentProtectedMediaId }
+ assertTrue(request.granted)
+ assertFalse(request.rejected)
+
+ request = MockPermissionRequest(listOf(Permission.Generic("test"), Permission.ContentProtectedMediaId()))
+ request.grantIf { it is Permission.Generic && it.id == "nomatch" }
+ assertFalse(request.granted)
+ assertFalse(request.rejected)
+
+ request.grantIf { it is Permission.Generic && it.id == "test" }
+ assertTrue(request.granted)
+ assertFalse(request.rejected)
+ }
+
+ @Test
+ fun `permission types are not equal if fields differ`() {
+ assertNotEquals(Permission.Generic("id"), Permission.Generic("id2"))
+ assertNotEquals(Permission.Generic("id"), Permission.Generic("id", "desc"))
+
+ assertNotEquals(Permission.ContentAudioCapture(), Permission.ContentAudioCapture("id"))
+ assertNotEquals(Permission.ContentAudioCapture("id"), Permission.ContentAudioCapture("id", "desc"))
+ assertNotEquals(Permission.ContentAudioMicrophone(), Permission.ContentAudioMicrophone("id"))
+ assertNotEquals(Permission.ContentAudioMicrophone("id"), Permission.ContentAudioMicrophone("id", "desc"))
+ assertNotEquals(Permission.ContentAudioOther(), Permission.ContentAudioOther("id"))
+ assertNotEquals(Permission.ContentAudioOther("id"), Permission.ContentAudioOther("id", "desc"))
+
+ assertNotEquals(Permission.ContentProtectedMediaId(), Permission.ContentProtectedMediaId("id"))
+ assertNotEquals(Permission.ContentProtectedMediaId("id"), Permission.ContentProtectedMediaId("id", "desc"))
+ assertNotEquals(Permission.ContentGeoLocation(), Permission.ContentGeoLocation("id"))
+ assertNotEquals(Permission.ContentGeoLocation("id"), Permission.ContentGeoLocation("id", "desc"))
+ assertNotEquals(Permission.ContentNotification(), Permission.ContentNotification("id"))
+ assertNotEquals(Permission.ContentNotification("id"), Permission.ContentNotification("id", "desc"))
+
+ assertNotEquals(Permission.ContentVideoCamera(), Permission.ContentVideoCamera("id"))
+ assertNotEquals(Permission.ContentVideoCamera("id"), Permission.ContentVideoCamera("id", "desc"))
+ assertNotEquals(Permission.ContentVideoCapture(), Permission.ContentVideoCapture("id"))
+ assertNotEquals(Permission.ContentVideoCapture("id"), Permission.ContentVideoCapture("id", "desc"))
+ assertNotEquals(Permission.ContentVideoScreen(), Permission.ContentVideoScreen("id"))
+ assertNotEquals(Permission.ContentVideoScreen("id"), Permission.ContentVideoScreen("id", "desc"))
+ assertNotEquals(Permission.ContentVideoOther(), Permission.ContentVideoOther("id"))
+ assertNotEquals(Permission.ContentVideoOther("id"), Permission.ContentVideoOther("id", "desc"))
+
+ assertNotEquals(Permission.AppAudio(), Permission.AppAudio("id"))
+ assertNotEquals(Permission.AppAudio("id"), Permission.AppAudio("id", "desc"))
+ assertNotEquals(Permission.AppCamera(), Permission.AppCamera("id"))
+ assertNotEquals(Permission.AppCamera("id"), Permission.AppCamera("id", "desc"))
+ assertNotEquals(Permission.AppLocationCoarse(), Permission.AppLocationCoarse("id"))
+ assertNotEquals(Permission.AppLocationCoarse("id"), Permission.AppLocationCoarse("id", "desc"))
+ assertNotEquals(Permission.AppLocationFine(), Permission.AppLocationFine("id"))
+ assertNotEquals(Permission.AppLocationFine("id"), Permission.AppLocationFine("id", "desc"))
+ }
+
+ private class MockPermissionRequest(
+ override val permissions: List<Permission>,
+ override val uri: String = "",
+ override val id: String = "",
+ ) : PermissionRequest {
+ var granted = false
+ var rejected = false
+
+ override fun grant(permissions: List<Permission>) {
+ granted = true
+ }
+
+ override fun reject() {
+ rejected = true
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionTest.kt
new file mode 100644
index 0000000000..2ece01a66b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionTest.kt
@@ -0,0 +1,32 @@
+/* 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.concept.engine.permission
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import kotlin.reflect.full.createInstance
+
+@RunWith(Parameterized::class)
+class PermissionTest<out T : Permission>(private val permission: T) {
+ @Test
+ fun `GIVEN a permission WHEN asking for it's default id THEN return permission's class name`() {
+ assertEquals(permission::class.java.simpleName, permission.id)
+ }
+
+ @Test
+ fun `GIVEN a permission WHEN asking for it's name THEN return permission's class name`() {
+ assertEquals(permission::class.java.simpleName, permission.name)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun permissions() = Permission::class.sealedSubclasses.map {
+ it.createInstance()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/ChoiceTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/ChoiceTest.kt
new file mode 100644
index 0000000000..c8d85c6342
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/ChoiceTest.kt
@@ -0,0 +1,37 @@
+/* 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.concept.engine.prompt
+
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ChoiceTest {
+
+ @Test
+ fun `Create a choice`() {
+ val choice = Choice(id = "id", label = "label", children = arrayOf())
+ choice.selected = true
+ choice.enable = true
+ choice.label = "label"
+
+ assertEquals(choice.id, "id")
+ assertEquals(choice.label, "label")
+ assertEquals(choice.describeContents(), 0)
+ assertTrue(choice.enable)
+ assertFalse(choice.isASeparator)
+ assertTrue(choice.selected)
+ assertTrue(choice.isGroupType)
+ assertNotNull(choice.children)
+
+ choice.writeToParcel(mock(), 0)
+ Choice(mock())
+ Choice.createFromParcel(mock())
+ Choice.newArray(1)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt
new file mode 100644
index 0000000000..5c63c24202
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt
@@ -0,0 +1,366 @@
+/* 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.concept.engine.prompt
+
+import mozilla.components.concept.engine.prompt.PromptRequest.Alert
+import mozilla.components.concept.engine.prompt.PromptRequest.Authentication
+import mozilla.components.concept.engine.prompt.PromptRequest.Color
+import mozilla.components.concept.engine.prompt.PromptRequest.Confirm
+import mozilla.components.concept.engine.prompt.PromptRequest.File
+import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.Popup
+import mozilla.components.concept.engine.prompt.PromptRequest.Repost
+import mozilla.components.concept.engine.prompt.PromptRequest.SaveLoginPrompt
+import mozilla.components.concept.engine.prompt.PromptRequest.SelectCreditCard
+import mozilla.components.concept.engine.prompt.PromptRequest.SelectLoginPrompt
+import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
+import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection
+import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.support.test.mock
+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.Test
+import java.util.Date
+
+class PromptRequestTest {
+
+ @Test
+ fun `SingleChoice`() {
+ val single = SingleChoice(emptyArray(), {}, {})
+ single.onConfirm(Choice(id = "", label = ""))
+ assertNotNull(single.choices)
+ }
+
+ @Test
+ fun `MultipleChoice`() {
+ val multiple = MultipleChoice(emptyArray(), {}, {})
+ multiple.onConfirm(arrayOf(Choice(id = "", label = "")))
+ assertNotNull(multiple.choices)
+ }
+
+ @Test
+ fun `MenuChoice`() {
+ val menu = MenuChoice(emptyArray(), {}, {})
+ menu.onConfirm(Choice(id = "", label = ""))
+ assertNotNull(menu.choices)
+ }
+
+ @Test
+ fun `Alert`() {
+ val alert = Alert("title", "message", true, {}, {})
+
+ assertEquals(alert.title, "title")
+ assertEquals(alert.message, "message")
+ assertEquals(alert.hasShownManyDialogs, true)
+
+ alert.onDismiss()
+ alert.onConfirm(true)
+
+ assertEquals(alert.title, "title")
+ assertEquals(alert.message, "message")
+ assertEquals(alert.hasShownManyDialogs, true)
+
+ alert.onDismiss()
+ alert.onConfirm(true)
+ }
+
+ @Test
+ fun `TextPrompt`() {
+ val textPrompt = TextPrompt(
+ "title",
+ "label",
+ "value",
+ true,
+ { _, _ -> },
+ {},
+ )
+
+ assertEquals(textPrompt.title, "title")
+ assertEquals(textPrompt.inputLabel, "label")
+ assertEquals(textPrompt.inputValue, "value")
+ assertEquals(textPrompt.hasShownManyDialogs, true)
+
+ textPrompt.onDismiss()
+ textPrompt.onConfirm(true, "")
+ }
+
+ @Test
+ fun `TimeSelection`() {
+ val dateRequest = TimeSelection(
+ "title",
+ Date(),
+ Date(),
+ Date(),
+ "1",
+ Type.DATE,
+ { _ -> },
+ {},
+ {},
+ )
+
+ assertEquals(dateRequest.title, "title")
+ assertEquals(dateRequest.type, Type.DATE)
+ assertEquals("1", dateRequest.stepValue)
+ assertNotNull(dateRequest.initialDate)
+ assertNotNull(dateRequest.minimumDate)
+ assertNotNull(dateRequest.maximumDate)
+
+ dateRequest.onConfirm(Date())
+ dateRequest.onClear()
+ }
+
+ @Test
+ fun `File`() {
+ val filePickerRequest = File(
+ emptyArray(),
+ true,
+ PromptRequest.File.FacingMode.NONE,
+ { _, _ -> },
+ { _, _ -> },
+ {},
+ )
+
+ assertTrue(filePickerRequest.mimeTypes.isEmpty())
+ assertTrue(filePickerRequest.isMultipleFilesSelection)
+ assertEquals(filePickerRequest.captureMode, PromptRequest.File.FacingMode.NONE)
+
+ filePickerRequest.onSingleFileSelected(mock(), mock())
+ filePickerRequest.onMultipleFilesSelected(mock(), emptyArray())
+ filePickerRequest.onDismiss()
+ }
+
+ @Test
+ fun `Authentication`() {
+ val promptRequest = Authentication(
+ "example.org",
+ "title",
+ "message",
+ "username",
+ "password",
+ PromptRequest.Authentication.Method.HOST,
+ PromptRequest.Authentication.Level.NONE,
+ false,
+ false,
+ false,
+ { _, _ -> },
+ {},
+ )
+
+ assertEquals(promptRequest.title, "title")
+ assertEquals(promptRequest.message, "message")
+ assertEquals(promptRequest.userName, "username")
+ assertEquals(promptRequest.password, "password")
+ assertFalse(promptRequest.onlyShowPassword)
+ assertFalse(promptRequest.previousFailed)
+ assertFalse(promptRequest.isCrossOrigin)
+
+ promptRequest.onConfirm("", "")
+ promptRequest.onDismiss()
+ }
+
+ @Test
+ fun `Color`() {
+ val onConfirm: (String) -> Unit = {}
+ val onDismiss: () -> Unit = {}
+
+ val colorRequest = Color("defaultColor", onConfirm, onDismiss)
+
+ assertEquals(colorRequest.defaultColor, "defaultColor")
+
+ colorRequest.onConfirm("")
+ colorRequest.onDismiss()
+ }
+
+ @Test
+ fun `Popup`() {
+ val popupRequest = Popup("http://mozilla.slack.com/", {}, {})
+
+ assertEquals(popupRequest.targetUri, "http://mozilla.slack.com/")
+
+ popupRequest.onAllow()
+ popupRequest.onDeny()
+ }
+
+ @Test
+ fun `Confirm`() {
+ val onConfirmPositiveButton: (Boolean) -> Unit = {}
+ val onConfirmNegativeButton: (Boolean) -> Unit = {}
+ val onConfirmNeutralButton: (Boolean) -> Unit = {}
+
+ val confirmRequest = Confirm(
+ "title",
+ "message",
+ false,
+ "positive",
+ "negative",
+ "neutral",
+ onConfirmPositiveButton,
+ onConfirmNegativeButton,
+ onConfirmNeutralButton,
+ {},
+ )
+
+ assertEquals(confirmRequest.title, "title")
+ assertEquals(confirmRequest.message, "message")
+ assertEquals(confirmRequest.positiveButtonTitle, "positive")
+ assertEquals(confirmRequest.negativeButtonTitle, "negative")
+ assertEquals(confirmRequest.neutralButtonTitle, "neutral")
+
+ confirmRequest.onConfirmPositiveButton(true)
+ confirmRequest.onConfirmNegativeButton(true)
+ confirmRequest.onConfirmNeutralButton(true)
+ }
+
+ @Test
+ fun `SaveLoginPrompt`() {
+ val onLoginDismiss: () -> Unit = {}
+ val onLoginConfirm: (LoginEntry) -> Unit = {}
+ val entry = LoginEntry("origin", username = "username", password = "password")
+
+ val loginSaveRequest = SaveLoginPrompt(0, listOf(entry), onLoginConfirm, onLoginDismiss)
+
+ assertEquals(loginSaveRequest.logins, listOf(entry))
+ assertEquals(loginSaveRequest.hint, 0)
+
+ loginSaveRequest.onConfirm(entry)
+ loginSaveRequest.onDismiss()
+ }
+
+ @Test
+ fun `SelectLoginPrompt`() {
+ val onLoginDismiss: () -> Unit = {}
+ val onLoginConfirm: (Login) -> Unit = {}
+ val login = Login(guid = "test-guid", origin = "origin", username = "username", password = "password")
+ val generatedPassword = "generatedPassword123#"
+
+ val loginSelectRequest =
+ SelectLoginPrompt(listOf(login), generatedPassword, onLoginConfirm, onLoginDismiss)
+
+ assertEquals(loginSelectRequest.logins, listOf(login))
+ assertEquals(loginSelectRequest.generatedPassword, generatedPassword)
+
+ loginSelectRequest.onConfirm(login)
+ loginSelectRequest.onDismiss()
+ }
+
+ @Test
+ fun `Repost`() {
+ var onAcceptWasCalled = false
+ var onDismissWasCalled = false
+
+ val repostRequest = Repost(
+ onConfirm = {
+ onAcceptWasCalled = true
+ },
+ onDismiss = {
+ onDismissWasCalled = true
+ },
+ )
+
+ repostRequest.onConfirm()
+ repostRequest.onDismiss()
+
+ assertTrue(onAcceptWasCalled)
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `GIVEN a list of credit cards WHEN SelectCreditCard is confirmed or dismissed THEN their respective callback is invoked`() {
+ val creditCard = CreditCardEntry(
+ guid = "id",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+ var onDismissCalled = false
+ var onConfirmCalled = false
+ var confirmedCreditCard: CreditCardEntry? = null
+
+ val selectCreditCardRequest = SelectCreditCard(
+ creditCards = listOf(creditCard),
+ onDismiss = {
+ onDismissCalled = true
+ },
+ onConfirm = {
+ confirmedCreditCard = it
+ onConfirmCalled = true
+ },
+ )
+
+ assertEquals(selectCreditCardRequest.creditCards, listOf(creditCard))
+
+ selectCreditCardRequest.onConfirm(creditCard)
+
+ assertTrue(onConfirmCalled)
+ assertFalse(onDismissCalled)
+ assertEquals(creditCard, confirmedCreditCard)
+
+ onConfirmCalled = false
+ confirmedCreditCard = null
+
+ selectCreditCardRequest.onDismiss()
+
+ assertTrue(onDismissCalled)
+ assertFalse(onConfirmCalled)
+ assertNull(confirmedCreditCard)
+ }
+
+ @Test
+ fun `WHEN calling confirm or dismiss on the SelectAddress prompt request THEN the respective callback is invoked`() {
+ val address = Address(
+ guid = "1",
+ name = "Firefox",
+ organization = "-",
+ streetAddress = "street",
+ addressLevel3 = "address3",
+ addressLevel2 = "address2",
+ addressLevel1 = "address1",
+ postalCode = "1",
+ country = "Country",
+ tel = "1",
+ email = "@",
+ )
+ var onDismissCalled = false
+ var onConfirmCalled = false
+ var confirmedAddress: Address? = null
+
+ val selectAddresPromptRequest = PromptRequest.SelectAddress(
+ addresses = listOf(address),
+ onDismiss = {
+ onDismissCalled = true
+ },
+ onConfirm = {
+ confirmedAddress = it
+ onConfirmCalled = true
+ },
+ )
+
+ assertEquals(selectAddresPromptRequest.addresses, listOf(address))
+
+ selectAddresPromptRequest.onConfirm(address)
+
+ assertTrue(onConfirmCalled)
+ assertFalse(onDismissCalled)
+ assertEquals(address, confirmedAddress)
+
+ onConfirmCalled = false
+
+ selectAddresPromptRequest.onDismiss()
+
+ assertTrue(onDismissCalled)
+ assertFalse(onConfirmCalled)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/ShareDataTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/ShareDataTest.kt
new file mode 100644
index 0000000000..adb3bb7077
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/ShareDataTest.kt
@@ -0,0 +1,40 @@
+/* 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.concept.engine.prompt
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.utils.ext.getParcelableCompat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ShareDataTest {
+
+ @Test
+ fun `Create share data`() {
+ val onlyTitle = ShareData(title = "Title")
+ assertEquals("Title", onlyTitle.title)
+
+ val onlyText = ShareData(text = "Text")
+ assertEquals("Text", onlyText.text)
+
+ val onlyUrl = ShareData(url = "https://mozilla.org")
+ assertEquals("https://mozilla.org", onlyUrl.url)
+ }
+
+ @Test
+ fun `Save to bundle`() {
+ val noText = ShareData(title = "Title", url = "https://mozilla.org")
+ val noUrl = ShareData(title = "Title", text = "Text")
+ val bundle = Bundle().apply {
+ putParcelable("noText", noText)
+ putParcelable("noUrl", noUrl)
+ }
+ assertEquals(noText, bundle.getParcelableCompat("noText", ShareData::class.java))
+ assertEquals(noUrl, bundle.getParcelableCompat("noUrl", ShareData::class.java))
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/request/RequestInterceptorTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/request/RequestInterceptorTest.kt
new file mode 100644
index 0000000000..c31c08f4c7
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/request/RequestInterceptorTest.kt
@@ -0,0 +1,37 @@
+/* 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.concept.engine.request
+
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor.InterceptionResponse
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.Mockito.mock
+
+class RequestInterceptorTest {
+
+ @Test
+ fun `match interception response`() {
+ val urlResponse = InterceptionResponse.Url("https://mozilla.org")
+ val contentResponse = InterceptionResponse.Content("data")
+
+ val url: String = urlResponse.url
+
+ val content: Triple<String, String, String> =
+ Triple(contentResponse.data, contentResponse.encoding, contentResponse.mimeType)
+
+ assertEquals("https://mozilla.org", url)
+ assertEquals(Triple("data", "UTF-8", "text/html"), content)
+ }
+
+ @Test
+ fun `interceptor has default methods`() {
+ val engineSession = mock(EngineSession::class.java)
+ val interceptor = object : RequestInterceptor { }
+ interceptor.onLoadRequest(engineSession, "url", null, false, false, false, false, false)
+ interceptor.onErrorRequest(engineSession, ErrorType.ERROR_UNKNOWN_SOCKET_TYPE, null)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/utils/EngineVersionTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/utils/EngineVersionTest.kt
new file mode 100644
index 0000000000..6d1099f815
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/utils/EngineVersionTest.kt
@@ -0,0 +1,186 @@
+/* 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.concept.engine.utils
+
+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.Test
+
+class EngineVersionTest {
+ @Test
+ fun `Parse common Gecko versions`() {
+ EngineVersion.parse("67.0.1").assertIs(67, 0, 1)
+ EngineVersion.parse("68.0").assertIs(68, 0, 0)
+ EngineVersion.parse("69.0a1").assertIs(69, 0, 0, "a1")
+ EngineVersion.parse("70.0b1").assertIs(70, 0, 0, "b1")
+ EngineVersion.parse("68.3esr").assertIs(68, 3, 0, "esr")
+ }
+
+ @Test
+ fun `Parse common Chrome versions`() {
+ EngineVersion.parse("75.0.3770").assertIs(75, 0, 3770)
+ EngineVersion.parse("76.0.3809").assertIs(76, 0, 3809)
+ EngineVersion.parse("77.0").assertIs(77, 0, 0)
+ }
+
+ @Test
+ fun `Parse invalid versions`() {
+ assertNull(EngineVersion.parse("Hello World"))
+ assertNull(EngineVersion.parse("1.a"))
+ }
+
+ @Test
+ fun `Comparing versions`() {
+ assertTrue("68.0".toVersion() > "67.5.9".toVersion())
+ assertTrue("68.0.1".toVersion() == "68.0.1".toVersion())
+ assertTrue("76.0.3809".toVersion() < "77.0".toVersion())
+ assertTrue("69.0a1".toVersion() > "69.0".toVersion())
+ assertTrue("67.0.1 ".toVersion() < "67.0.2".toVersion())
+ assertTrue("68.3esr".toVersion() < "70.0b1".toVersion())
+ assertTrue("67.0".toVersion() < "67.0a1".toVersion())
+ assertTrue("67.0a1".toVersion() < "67.0b1".toVersion())
+ assertEquals(0, "68.0.1".compareTo("68.0.1"))
+ }
+
+ @Test
+ fun `Comparing with isAtLeast`() {
+ assertTrue("68.0.0".toVersion().isAtLeast(68))
+ assertTrue("68.0.0".toVersion().isAtLeast(67, 0, 7))
+ assertFalse("68.0.0".toVersion().isAtLeast(69))
+ assertTrue("76.0.3809".toVersion().isAtLeast(76, 0, 3809))
+ assertTrue("76.0.3809".toVersion().isAtLeast(76, 0, 3808))
+ assertFalse("76.0.3809".toVersion().isAtLeast(76, 0, 3810))
+ assertTrue("1.2.25".toVersion().isAtLeast(1, 1, 10))
+ assertTrue("1.2.25".toVersion().isAtLeast(1, 1, 25))
+ assertTrue("1.2.25".toVersion().isAtLeast(1, 2, 25))
+ }
+
+ @Test
+ fun `toString returns clean version string`() {
+ assertEquals("1.0.0", "1.0.0".toVersion().toString())
+ assertEquals("76.0.3809", "76.0.3809".toVersion().toString())
+ assertEquals("67.0.0a1", "67.0a1".toVersion().toString())
+ assertEquals("68.3.0esr", "68.3esr".toVersion().toString())
+ assertEquals("68.0.0", "68.0".toVersion().toString())
+ }
+
+ @Test
+ fun `GIVEN a nightly build of the engine WHEN parsing the version THEN add the correct release channel`() {
+ val result = EngineVersion.parse("0.0.1", "nightly")?.releaseChannel
+
+ assertEquals(EngineReleaseChannel.NIGHTLY, result)
+ }
+
+ @Test
+ fun `GIVEN a beta build of the engine WHEN parsing the version THEN add the correct release channel`() {
+ val result = EngineVersion.parse("0.0.1", "beta")?.releaseChannel
+
+ assertEquals(EngineReleaseChannel.BETA, result)
+ }
+
+ @Test
+ fun `GIVEN a release build of the engine WHEN parsing the version THEN add the correct release channel`() {
+ val result = EngineVersion.parse("0.0.1", "release")?.releaseChannel
+
+ assertEquals(EngineReleaseChannel.RELEASE, result)
+ }
+
+ @Test
+ fun `GIVEN a different build of the engine WHEN parsing the version THEN add the correct release channel`() {
+ val result = EngineVersion.parse("0.0.1", "aurora")?.releaseChannel
+
+ assertEquals(EngineReleaseChannel.UNKNOWN, result)
+ }
+
+ @Test
+ fun `GIVEN an unknown release type WHEN comparing to other versions THEN return a negative value`() {
+ val version = "0.0.1"
+ val unknown = EngineVersion.parse(version, "canary")
+
+ assertEquals(0, unknown!!.compareTo(unknown))
+ assertTrue(unknown < EngineVersion.parse(version, "nightly")!!)
+ assertTrue(unknown < EngineVersion.parse(version, "beta")!!)
+ assertTrue(unknown < EngineVersion.parse(version, "release")!!)
+ }
+
+ @Test
+ fun `GIVEN an nightly release type WHEN comparing to other versions THEN return the expected result`() {
+ val version = "0.0.1"
+ val nightly = EngineVersion.parse(version, "nightly")
+
+ assertEquals(0, nightly!!.compareTo(nightly))
+ assertTrue(nightly > EngineVersion.parse(version, "unknown")!!)
+ assertTrue(nightly < EngineVersion.parse(version, "beta")!!)
+ assertTrue(nightly < EngineVersion.parse(version, "release")!!)
+ }
+
+ @Test
+ fun `GIVEN an beta release type WHEN comparing to other versions THEN return the expected result`() {
+ val version = "0.0.1"
+ val beta = EngineVersion.parse(version, "beta")
+
+ assertEquals(0, beta!!.compareTo(beta))
+ assertTrue(beta > EngineVersion.parse(version, "unknown")!!)
+ assertTrue(beta > EngineVersion.parse(version, "nightly")!!)
+ assertTrue(beta < EngineVersion.parse(version, "release")!!)
+ }
+
+ @Test
+ fun `GIVEN a release type WHEN comparing to other versions THEN return the expected result`() {
+ val version = "0.0.1"
+ val release = EngineVersion.parse(version, "release")
+
+ assertEquals(0, release!!.compareTo(release))
+ assertTrue(release > EngineVersion.parse(version, "unknown")!!)
+ assertTrue(release > EngineVersion.parse(version, "nightly")!!)
+ assertTrue(release > EngineVersion.parse(version, "beta")!!)
+ }
+
+ @Test
+ fun `GIVEN a newer version of a less stable release WHEN comparing to other versions THEN return the expected result`() {
+ val debug = EngineVersion.parse("103.4567890", "test")
+ val nightly = EngineVersion.parse("102.1.0", "nightly")
+ val beta = EngineVersion.parse("101.0.0", "nightly")
+ val release = EngineVersion.parse("100.1.2", "release")
+
+ assertEquals(0, debug!!.compareTo(debug))
+ assertTrue(debug > nightly!!)
+ assertTrue(debug > beta!!)
+ assertTrue(debug > release!!)
+
+ assertEquals(0, nightly.compareTo(nightly))
+ assertTrue(nightly > beta)
+ assertTrue(nightly > release)
+
+ assertEquals(0, beta.compareTo(beta))
+ assertTrue(beta > release)
+
+ assertEquals(0, release.compareTo(release))
+ }
+}
+
+private fun String.toVersion() = EngineVersion.parse(this)!!
+
+private fun EngineVersion?.assertIs(
+ major: Int,
+ minor: Int,
+ patch: Long,
+ metadata: String? = null,
+) {
+ assertNotNull(this!!)
+
+ assertEquals(major, this.major)
+ assertEquals(minor, this.minor)
+ assertEquals(patch, this.patch)
+
+ if (metadata == null) {
+ assertNull(this.metadata)
+ } else {
+ assertEquals(metadata, this.metadata)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/ActionTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/ActionTest.kt
new file mode 100644
index 0000000000..ebdeb52546
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/ActionTest.kt
@@ -0,0 +1,58 @@
+/* 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.concept.engine.webextension
+
+import android.graphics.Color
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ActionTest {
+
+ private val onClick: () -> Unit = {}
+ private val baseAction = Action(
+ title = "title",
+ enabled = false,
+ loadIcon = null,
+ badgeText = "badge",
+ badgeTextColor = Color.BLACK,
+ badgeBackgroundColor = Color.BLUE,
+ onClick = onClick,
+ )
+
+ @Test
+ fun `override using non-null attributes`() {
+ val overridden = baseAction.copyWithOverride(
+ Action(
+ title = "other",
+ enabled = null,
+ loadIcon = null,
+ badgeText = null,
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = null,
+ onClick = onClick,
+ ),
+ )
+
+ assertEquals(
+ Action(
+ title = "other",
+ enabled = false,
+ loadIcon = null,
+ badgeText = "badge",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ onClick = onClick,
+ ),
+ overridden,
+ )
+ }
+
+ @Test
+ fun `override using null action`() {
+ val overridden = baseAction.copyWithOverride(null)
+
+ assertEquals(baseAction, overridden)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt
new file mode 100644
index 0000000000..b7af664cb6
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt
@@ -0,0 +1,105 @@
+/* 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.concept.engine.webextension
+
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.json.JSONObject
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class WebExtensionTest {
+
+ @Test
+ fun `message handler has default methods`() {
+ val messageHandler = object : MessageHandler {}
+
+ messageHandler.onPortConnected(mock())
+ messageHandler.onPortDisconnected(mock())
+ messageHandler.onPortMessage(mock(), mock())
+ messageHandler.onMessage(mock(), mock())
+ }
+
+ @Test
+ fun `tab handler has default methods`() {
+ val tabHandler = object : TabHandler {}
+
+ tabHandler.onUpdateTab(mock(), mock(), false, "")
+ tabHandler.onCloseTab(mock(), mock())
+ tabHandler.onNewTab(mock(), mock(), false, "")
+ }
+
+ @Test
+ fun `action handler has default methods`() {
+ val actionHandler = object : ActionHandler {}
+
+ actionHandler.onPageAction(mock(), mock(), mock())
+ actionHandler.onBrowserAction(mock(), mock(), mock())
+ actionHandler.onToggleActionPopup(mock(), mock())
+ }
+
+ @Test
+ fun `port holds engine session`() {
+ val engineSession: EngineSession = mock()
+ val port = object : Port(engineSession) {
+ override fun name(): String {
+ return "test"
+ }
+
+ override fun disconnect() {}
+
+ override fun senderUrl(): String {
+ return "https://foo.bar"
+ }
+
+ override fun postMessage(message: JSONObject) { }
+ }
+
+ assertSame(engineSession, port.engineSession)
+ }
+
+ @Test
+ fun `disabled checks`() {
+ val extension: WebExtension = mock()
+ assertFalse(extension.isUnsupported())
+ assertFalse(extension.isBlockListed())
+ assertFalse(extension.isDisabledUnsigned())
+ assertFalse(extension.isDisabledIncompatible())
+
+ val metadata: Metadata = mock()
+ whenever(extension.getMetadata()).thenReturn(metadata)
+ assertFalse(extension.isUnsupported())
+ assertFalse(extension.isBlockListed())
+ assertFalse(extension.isDisabledUnsigned())
+ assertFalse(extension.isDisabledIncompatible())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(DisabledFlags.BLOCKLIST))
+ assertFalse(extension.isUnsupported())
+ assertTrue(extension.isBlockListed())
+ assertFalse(extension.isDisabledUnsigned())
+ assertFalse(extension.isDisabledIncompatible())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(DisabledFlags.APP_SUPPORT))
+ assertTrue(extension.isUnsupported())
+ assertFalse(extension.isBlockListed())
+ assertFalse(extension.isDisabledUnsigned())
+ assertFalse(extension.isDisabledIncompatible())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(DisabledFlags.SIGNATURE))
+ assertFalse(extension.isUnsupported())
+ assertFalse(extension.isBlockListed())
+ assertTrue(extension.isDisabledUnsigned())
+ assertFalse(extension.isDisabledIncompatible())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(DisabledFlags.APP_VERSION))
+ assertFalse(extension.isUnsupported())
+ assertFalse(extension.isBlockListed())
+ assertFalse(extension.isDisabledUnsigned())
+ assertTrue(extension.isDisabledIncompatible())
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webpush/WebPushSubscriptionTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webpush/WebPushSubscriptionTest.kt
new file mode 100644
index 0000000000..efe4a2146a
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webpush/WebPushSubscriptionTest.kt
@@ -0,0 +1,112 @@
+/* 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.concept.engine.webpush
+
+import org.junit.Test
+
+class WebPushSubscriptionTest {
+
+ @Test
+ fun `constructor`() {
+ val scope = "https://mozilla.org"
+ val endpoint = "https://pushendpoint.mozilla.org/send/message/here"
+ val appServerKey = byteArrayOf(10, 2, 15, 11)
+ val publicKey = byteArrayOf(11, 10, 2, 15)
+ val authSecret = byteArrayOf(15, 11, 10, 2)
+ val sub = WebPushSubscription(
+ scope,
+ endpoint,
+ appServerKey,
+ publicKey,
+ authSecret,
+ )
+
+ assert(scope == sub.scope)
+ assert(endpoint == sub.endpoint)
+ assert(appServerKey.contentEquals(sub.appServerKey!!))
+ assert(publicKey.contentEquals(sub.publicKey))
+ assert(authSecret.contentEquals(sub.authSecret))
+ }
+
+ @Test
+ fun `WebPushSubscription equals`() {
+ val sub1 = WebPushSubscription(
+ "https://mozilla.org",
+ "https://pushendpoint.mozilla.org/send/message/here",
+ byteArrayOf(10, 2, 15, 11),
+ byteArrayOf(11, 10, 2, 15),
+ byteArrayOf(15, 11, 10, 2),
+ )
+ val sub2 = WebPushSubscription(
+ "https://mozilla.org",
+ "https://pushendpoint.mozilla.org/send/message/here",
+ byteArrayOf(10, 2, 15, 11),
+ byteArrayOf(11, 10, 2, 15),
+ byteArrayOf(15, 11, 10, 2),
+ )
+
+ assert(sub1 == sub2)
+ }
+
+ @Test
+ fun `WebPushSubscription equals with optional`() {
+ val sub1 = WebPushSubscription(
+ "https://mozilla.org",
+ "https://pushendpoint.mozilla.org/send/message/here",
+ byteArrayOf(10, 2, 15, 11),
+ byteArrayOf(11, 10, 2, 15),
+ byteArrayOf(15, 11, 10, 2),
+ )
+
+ val sub2 = WebPushSubscription(
+ "https://mozilla.org",
+ "https://pushendpoint.mozilla.org/send/message/here",
+ null,
+ byteArrayOf(11, 10, 2, 15),
+ byteArrayOf(15, 11, 10, 2),
+ )
+
+ assert(sub1 != sub2)
+
+ val sub3 = WebPushSubscription(
+ "https://mozilla.org",
+ "https://pushendpoint.mozilla.org/send/message/here",
+ byteArrayOf(10, 2, 15),
+ byteArrayOf(11, 10, 2, 15),
+ byteArrayOf(15, 11, 10, 2),
+ )
+
+ val notSub = "notSub"
+
+ assert(sub1 != sub2)
+ assert(sub2 != sub3)
+ assert(sub1 != sub3)
+ assert(sub3 != sub1)
+ assert(sub3 != sub2)
+ assert(sub1 != notSub as Any)
+ }
+
+ @Test
+ fun `hashCode is generated consistently from the class data`() {
+ val sub1 = WebPushSubscription(
+ "https://mozilla.org",
+ "https://pushendpoint.mozilla.org/send/message/here",
+ byteArrayOf(10, 2, 15, 11),
+ byteArrayOf(11, 10, 2, 15),
+ byteArrayOf(15, 11, 10, 2),
+ )
+
+ val sub2 = WebPushSubscription(
+ "https://mozilla.org",
+ "https://pushendpoint.mozilla.org/send/message/here",
+ null,
+ byteArrayOf(11, 10, 2, 15),
+ byteArrayOf(15, 11, 10, 2),
+ )
+
+ assert(sub1.hashCode() == sub1.hashCode())
+ assert(sub1.hashCode() != sub2.hashCode())
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/example_google.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/example_google.json
new file mode 100644
index 0000000000..16c54b4585
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/example_google.json
@@ -0,0 +1,21 @@
+{
+ "short_name": "Maps",
+ "name": "Google Maps",
+ "icons": [
+ {
+ "src": "/images/icons-192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "/images/icons-512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": "/maps/?source=pwa",
+ "background_color": "#3367D6",
+ "display": "standalone",
+ "scope": "/maps/",
+ "theme_color": "#3367D6"
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/example_mdn.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/example_mdn.json
new file mode 100644
index 0000000000..d08b78f9b7
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/example_mdn.json
@@ -0,0 +1,37 @@
+{
+ "name": "HackerWeb",
+ "short_name": "HackerWeb",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "description": "A simply readable Hacker News app.",
+ "icons": [{
+ "src": "images/touch/homescreen48.png",
+ "sizes": "48x48",
+ "type": "image/png"
+ }, {
+ "src": "images/touch/homescreen72.png",
+ "sizes": "72x72",
+ "type": "image/png"
+ }, {
+ "src": "images/touch/homescreen96.png",
+ "sizes": "96x96",
+ "type": "image/png"
+ }, {
+ "src": "images/touch/homescreen144.png",
+ "sizes": "144x144",
+ "type": "image/png"
+ }, {
+ "src": "images/touch/homescreen168.png",
+ "sizes": "168x168",
+ "type": "image/png"
+ }, {
+ "src": "images/touch/homescreen192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ }],
+ "related_applications": [{
+ "platform": "play",
+ "url": "https://play.google.com/store/apps/details?id=cheeaun.hackerweb"
+ }]
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/invalid_json.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/invalid_json.json
new file mode 100644
index 0000000000..2bd69c8cb4
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/invalid_json.json
@@ -0,0 +1,3 @@
+{
+ "name": 12345
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/invalid_missing_name.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/invalid_missing_name.json
new file mode 100644
index 0000000000..a11c3cd2df
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/invalid_missing_name.json
@@ -0,0 +1,3 @@
+{
+ "start_url": "https://example.com"
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal.json
new file mode 100644
index 0000000000..785d1acefa
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal.json
@@ -0,0 +1,4 @@
+{
+ "name": "Minimal",
+ "start_url": "/"
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal_share_target.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal_share_target.json
new file mode 100644
index 0000000000..de6a3fce16
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal_share_target.json
@@ -0,0 +1,13 @@
+{
+ "name": "Minimal",
+ "start_url": "/",
+ "share_target": {
+ "action": "/share-target",
+ "params": {
+ "files": {
+ "name": "file",
+ "accept": "image/*"
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal_short_name.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal_short_name.json
new file mode 100644
index 0000000000..270102b33a
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/minimal_short_name.json
@@ -0,0 +1,4 @@
+{
+ "short_name": "Minimal with Short Name",
+ "start_url": "/"
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/purpose_array.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/purpose_array.json
new file mode 100644
index 0000000000..a4a289e6f6
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/purpose_array.json
@@ -0,0 +1,23 @@
+{
+ "name": "The Sample Manifest",
+ "short_name": "Sample",
+ "icons": [
+ {
+ "src": "/images/icon/favicon.ico",
+ "type": "image/png",
+ "sizes": "48x48 96x96 128x128",
+ "purpose": ["monochrome"]
+ },
+ {
+ "src": "/images/icon/512-512.png",
+ "type": "image/png",
+ "sizes": ["512x512"],
+ "purpose": ["maskable", "foo", "any"]
+ }
+ ],
+ "start_url": "/start",
+ "scope": "/",
+ "display": "minimal-ui",
+ "dir": "rtl",
+ "orientation": "portrait"
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/spec_typical.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/spec_typical.json
new file mode 100644
index 0000000000..3f180353eb
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/spec_typical.json
@@ -0,0 +1,51 @@
+{
+ "lang": "en",
+ "dir": "ltr",
+ "name": "Super Racer 3000",
+ "description": "The ultimate futuristic racing game from the future!",
+ "short_name": "Racer3K",
+ "icons": [{
+ "src": "icon/lowres.webp",
+ "sizes": "64x64",
+ "type": "image/webp"
+ },{
+ "src": "icon/lowres.png",
+ "sizes": "64x64"
+ }, {
+ "src": "icon/hd_hi",
+ "sizes": "128x128"
+ }],
+ "scope": "/racer/",
+ "start_url": "/racer/start.html",
+ "display": "fullscreen",
+ "orientation": "landscape",
+ "theme_color": "#f0f8ff",
+ "background_color": "#FF0000",
+ "serviceworker": {
+ "src": "sw.js",
+ "scope": "/racer/",
+ "update_via_cache": "none"
+ },
+ "screenshots": [{
+ "src": "screenshots/in-game-1x.jpg",
+ "sizes": "640x480",
+ "type": "image/jpeg"
+ },{
+ "src": "screenshots/in-game-2x.jpg",
+ "sizes": "1280x920",
+ "type": "image/jpeg"
+ }],
+ "related_applications": [{
+ "platform": "play",
+ "url": "https://play.google.com/store/apps/details?id=com.example.app1",
+ "id": "com.example.app1",
+ "min_version": "2",
+ "fingerprints": [{
+ "type": "sha256_cert",
+ "value": "92:5A:39:05:C5:B9:EA:BC:71:48:5F:F2"
+ }]
+ }, {
+ "platform": "itunes",
+ "url": "https://itunes.apple.com/app/example-app1/id123456789"
+ }]
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/squoosh.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/squoosh.json
new file mode 100644
index 0000000000..9f8ebeb03c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/squoosh.json
@@ -0,0 +1,32 @@
+{
+ "name": "Squoosh",
+ "short_name": "Squoosh",
+ "start_url": "/",
+ "display": "standalone",
+ "orientation": "any",
+ "background_color": "#ffffff",
+ "theme_color": "#f78f21",
+ "icons": [
+ {
+ "src": "/assets/icon-large.png",
+ "type": "image/png",
+ "sizes": "1024x1024"
+ }
+ ],
+ "share_target": {
+ "action": "/?share-target",
+ "method": "POST",
+ "enctype": "multipart/form-data",
+ "params": {
+ "title": "title",
+ "text": "body",
+ "url": "uri",
+ "files": [
+ {
+ "name": "file",
+ "accept": ["image/*"]
+ }
+ ]
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/twitter_mobile.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/twitter_mobile.json
new file mode 100644
index 0000000000..142ce0317e
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/twitter_mobile.json
@@ -0,0 +1 @@
+{"background_color":"#ffffff","description":"It's what's happening. From breaking news and entertainment, sports and politics, to big events and everyday interests.","display":"standalone","gcm_sender_id":"49625052041","gcm_user_visible_only":true,"icons":[{"src":"https://abs.twimg.com/responsive-web/web/icon-default.604e2486a34a2f6e1.png","sizes":"192x192","type":"image/png"},{"src":"https://abs.twimg.com/responsive-web/web/icon-default.604e2486a34a2f6e1.png","sizes":"512x512","type":"image/png"}],"name":"Twitter","share_target":{"action":"compose/tweet","params":{"title":"title","text":"text","url":"url"}},"short_name":"Twitter","start_url":"/","theme_color":"#ffffff","scope":"/"}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/unusual.json b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/unusual.json
new file mode 100644
index 0000000000..e2f8212971
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/manifests/unusual.json
@@ -0,0 +1,35 @@
+{
+ "name": "The Sample Manifest",
+ "short_name": "Sample",
+ "icons": [
+ {
+ "src": "/images/icon/favicon.ico",
+ "type": "image/png",
+ "sizes": "48x48 96x96 128x128",
+ "purpose": "monochrome"
+ },
+ {
+ "src": "/images/icon/512-512.png",
+ "type": "image/png",
+ "sizes": "512x512",
+ "purpose": "maskable foo any"
+ }
+ ],
+ "start_url": "/start",
+ "scope": "/",
+ "display": "minimal-ui",
+ "dir": "rtl",
+ "orientation": "portrait",
+ "share_target": {
+ "action": "/",
+ "method": "get",
+ "params": {
+ "title": "title",
+ "url": "uri"
+ },
+ "files": {
+ "name": "file",
+ "accept": "image/*"
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/engine/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/concept/engine/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/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/concept/engine/src/test/resources/robolectric.properties b/mobile/android/android-components/components/concept/engine/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/concept/engine/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/concept/fetch/README.md b/mobile/android/android-components/components/concept/fetch/README.md
new file mode 100644
index 0000000000..e7c4e11f1b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/README.md
@@ -0,0 +1,166 @@
+# [Android Components](../../../README.md) > Concept > Fetch
+
+The `concept-fetch` component contains interfaces for defining an abstract HTTP client for fetching resources.
+
+The primary use of this component is to hide the actual implementation of the HTTP client from components required to make HTTP requests. This allows apps to configure a single app-wide used client without the components enforcing a particular dependency.
+
+The API and name of the component is inspired by the [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
+
+## Usage
+
+### 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:concept-fetch:{latest-version}"
+```
+
+### Performing requests
+
+#### Get a URL
+
+```Kotlin
+val request = Request(url)
+val response = client.fetch(request)
+val body = response.string()
+```
+
+A `Response` may hold references to other resources (e.g. streams). Therefore it's important to always close the `Response` object or its `Body`. This can be done by either consuming the content of the `Body` with one of the available methods or by using Kotlin's extension methods for using `Closeable` implementations (e.g. `use()`):
+
+```Kotlin
+client.fetch(Request(url)).use { response ->
+ val body = response.body.string()
+}
+```
+
+#### Post to a URL
+
+```Kotlin
+val request = Request(
+ url = "...",
+ method = Request.Method.POST,
+ body = Request.Body.fromStream(stream))
+
+client.fetch(request).use { response ->
+ if (response.success) {
+ // ...
+ }
+}
+```
+
+#### Github API example
+
+```Kotlin
+val request = Request(
+ url = "https://api.github.com/repos/mozilla-mobile/android-components/issues",
+ headers = MutableHeaders(
+ "User-Agent" to "AwesomeBrowser/1.0",
+ "Accept" to "application/json; q=0.5",
+ "Accept" to "application/vnd.github.v3+json"))
+
+client.fetch(request).use { response ->
+ val server = response.headers.get('Server')
+ val result = response.body.string()
+}
+```
+
+#### Posting a file
+
+```Kotlin
+val file = File("README.md")
+
+val request = Request(
+ url = "https://api.github.com/markdown/raw",
+ headers = MutableHeaders(
+ "Content-Type", "text/x-markdown; charset=utf-8"
+ ),
+ body = Request.Body.fromFile(file))
+
+client.fetch(request).use { response ->
+ if (request.success) {
+ // Upload was successful!
+ }
+}
+
+```
+
+#### Asynchronous requests
+
+Client implementations are synchronous. For asynchronous requests it's recommended to wrap a client in a Coroutine with a scope the calling code is in control of:
+
+```Kotlin
+val deferredResponse = async { client.fetch(request) }
+val body = deferredResponse.await().body.string()
+```
+
+### Interceptors
+
+Interceptors are a powerful mechanism to monitor, modify, retry, redirect or record requests as well as responses going through a `Client`. Interceptors can be used with any `concept-fetch` implementation.
+
+The `withInterceptors()` extension method can be used to create a wrapped `Client` that will use the provided interceptors for requests.
+
+```kotlin
+val response = HttpURLConnectionClient()
+ .withInterceptors(LoggingInterceptor(), RetryInterceptor())
+ .fetch(request)
+```
+
+The following example implements a simple `Interceptor` that logs requests and how long they took:
+
+```kotlin
+class LoggingInterceptor(
+ private val logger: Logger = Logger("Client")
+): Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ logger.info("Request to ${chain.request.url}")
+
+ val startTime = System.currentTimeMillis()
+
+ val response = chain.proceed(chain.request)
+
+ val took = System.currentTimeMillis() - startTime
+ logger.info("[${response.status}] took $took ms")
+
+ return response
+ }
+}
+```
+
+And the following example is a naive implementation of an interceptor that retries requests:
+
+```kotlin
+class NaiveRetryInterceptor(
+ private val maxRetries: Int = 3
+) : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val response = chain.proceed(chain.request)
+ if (response.isSuccess) {
+ return response
+ }
+
+ return retry(chain) ?: response
+ }
+
+ fun retry(chain: Interceptor.Chain): Response? {
+ var lastResponse: Response? = null
+ var retries = 0
+
+ while (retries < maxRetries) {
+ lastResponse = chain.proceed(chain.request)
+ if (lastResponse.isSuccess) {
+ return lastResponse
+ }
+ retries++
+ }
+
+ return lastResponse
+ }
+}
+```
+
+## 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/concept/fetch/build.gradle b/mobile/android/android-components/components/concept/fetch/build.gradle
new file mode 100644
index 0000000000..ce95c4ec32
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/build.gradle
@@ -0,0 +1,44 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ buildConfigField("String", "LIBRARY_VERSION", "\"" + config.componentsVersion + "\"")
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ namespace 'mozilla.components.concept.fetch'
+}
+
+dependencies {
+ testImplementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ testImplementation project(':support-test')
+}
+
+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/concept/fetch/proguard-rules.pro b/mobile/android/android-components/components/concept/fetch/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/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/concept/fetch/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/fetch/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/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/concept/fetch/src/main/java/mozilla/components/concept/fetch/Client.kt b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Client.kt
new file mode 100644
index 0000000000..fbb9eb7c72
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Client.kt
@@ -0,0 +1,98 @@
+/* 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.concept.fetch
+
+import android.util.Base64
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.net.URLDecoder
+import java.nio.charset.Charset
+
+/**
+ * A generic [Client] for fetching resources via HTTP/s.
+ *
+ * Abstract base class / interface for clients implementing the `concept-fetch` component.
+ *
+ * The [Request]/[Response] API is inspired by the Web Fetch API:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
+ */
+abstract class Client {
+ /**
+ * Starts the process of fetching a resource from the network as described by the [Request] object. This call is
+ * synchronous.
+ *
+ * A [Response] may keep references to open streams. Therefore it's important to always close the [Response] or
+ * its [Response.Body].
+ *
+ * Use the `use()` extension method when performing multiple operations on the [Response] object:
+ *
+ * ```Kotlin
+ * client.fetch(request).use { response ->
+ * // Use response. Resources will get released automatically at the end of the block.
+ * }
+ * ```
+ *
+ * Alternatively you can use multiple `use*()` methods on the [Response.Body] object.
+ *
+ * @param request The request to be executed by this [Client].
+ * @return The [Response] returned by the server.
+ * @throws IOException if the request could not be executed due to cancellation, a connectivity problem or a
+ * timeout.
+ */
+ @Throws(IOException::class)
+ abstract fun fetch(request: Request): Response
+
+ /**
+ * Generates a [Response] based on the provided [Request] for a data URI.
+ *
+ * @param request The [Request] for the data URI.
+ * @return The generated [Response] including the decoded bytes as body.
+ */
+ @Suppress("ComplexMethod", "TooGenericExceptionCaught")
+ protected fun fetchDataUri(request: Request): Response {
+ if (!request.isDataUri()) {
+ throw IOException("Not a data URI")
+ }
+ return try {
+ val dataUri = request.url
+
+ val (contentType, bytes) = if (dataUri.contains(DATA_URI_BASE64_EXT)) {
+ dataUri.substringAfter(DATA_URI_SCHEME).substringBefore(DATA_URI_BASE64_EXT) to
+ Base64.decode(dataUri.substring(dataUri.lastIndexOf(',') + 1), Base64.DEFAULT)
+ } else {
+ val contentType = dataUri.substringAfter(DATA_URI_SCHEME).substringBefore(",")
+ val charset = if (contentType.contains(DATA_URI_CHARSET)) {
+ Charset.forName(contentType.substringAfter(DATA_URI_CHARSET).substringBefore(","))
+ } else {
+ Charsets.UTF_8
+ }
+ contentType to
+ URLDecoder.decode(dataUri.substring(dataUri.lastIndexOf(',') + 1), charset.name()).toByteArray()
+ }
+
+ val headers = MutableHeaders().apply {
+ set(Headers.Names.CONTENT_LENGTH, bytes.size.toString())
+ if (contentType.isNotEmpty()) {
+ set(Headers.Names.CONTENT_TYPE, contentType)
+ }
+ }
+
+ Response(
+ dataUri,
+ Response.SUCCESS,
+ headers,
+ Response.Body(ByteArrayInputStream(bytes), contentType),
+ )
+ } catch (e: Exception) {
+ throw IOException("Failed to decode data URI")
+ }
+ }
+
+ companion object {
+ const val DATA_URI_BASE64_EXT = ";base64"
+ const val DATA_URI_SCHEME = "data:"
+ const val DATA_URI_CHARSET = "charset="
+ }
+}
diff --git a/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Headers.kt b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Headers.kt
new file mode 100644
index 0000000000..9b49884bfe
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Headers.kt
@@ -0,0 +1,168 @@
+/* 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.concept.fetch
+
+/**
+ * A collection of HTTP [Headers] (immutable) of a [Request] or [Response].
+ */
+interface Headers : Iterable<Header> {
+ /**
+ * Returns the number of headers (key / value combinations).
+ */
+ val size: Int
+
+ /**
+ * Gets the [Header] at the specified [index].
+ */
+ operator fun get(index: Int): Header
+
+ /**
+ * Returns the last values corresponding to the specified header field name. Or null if the header does not exist.
+ */
+ operator fun get(name: String): String?
+
+ /**
+ * Returns the list of values corresponding to the specified header field name.
+ */
+ fun getAll(name: String): List<String>
+
+ /**
+ * Sets the [Header] at the specified [index].
+ */
+ operator fun set(index: Int, header: Header)
+
+ /**
+ * Returns true if a [Header] with the given [name] exists.
+ */
+ operator fun contains(name: String): Boolean
+
+ /**
+ * A collection of common HTTP header names.
+ *
+ * A list of common HTTP request headers can be found at
+ * https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Standard_request_fields
+ *
+ * A list of common HTTP response headers can be found at
+ * https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Standard_response_fields
+ *
+ * @see [Headers.Values]
+ */
+ object Names {
+ const val CONTENT_DISPOSITION = "Content-Disposition"
+ const val CONTENT_RANGE = "Content-Range"
+ const val RANGE = "Range"
+ const val CONTENT_LENGTH = "Content-Length"
+ const val CONTENT_TYPE = "Content-Type"
+ const val COOKIE = "Cookie"
+ const val REFERRER = "Referer"
+ const val USER_AGENT = "User-Agent"
+ }
+
+ /**
+ * A collection of common HTTP header values.
+ *
+ * @see [Headers.Names]
+ */
+ object Values {
+ const val CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
+ const val CONTENT_TYPE_APPLICATION_JSON = "application/json"
+ }
+}
+
+/**
+ * Represents a [Header] containing of a [name] and [value].
+ */
+data class Header(
+ val name: String,
+ val value: String,
+) {
+ init {
+ if (name.isEmpty()) {
+ throw IllegalArgumentException("Header name cannot be empty")
+ }
+ }
+}
+
+/**
+ * A collection of HTTP [Headers] (mutable) of a [Request] or [Response].
+ */
+class MutableHeaders(headers: List<Header>) : Headers, MutableIterable<Header> {
+
+ private val headers = headers.toMutableList()
+
+ constructor(vararg pairs: Pair<String, String>) : this(
+ pairs.map { (name, value) -> Header(name, value) }.toMutableList(),
+ )
+
+ /**
+ * Gets the [Header] at the specified [index].
+ */
+ override fun get(index: Int): Header = headers[index]
+
+ /**
+ * Returns the last value corresponding to the specified header field name. Or null if the header does not exist.
+ */
+ override fun get(name: String) =
+ headers.lastOrNull { header -> header.name.equals(name, ignoreCase = true) }?.value
+
+ /**
+ * Returns the list of values corresponding to the specified header field name.
+ */
+ override fun getAll(name: String): List<String> = headers
+ .filter { header -> header.name.equals(name, ignoreCase = true) }
+ .map { header -> header.value }
+
+ /**
+ * Sets the [Header] at the specified [index].
+ */
+ override fun set(index: Int, header: Header) {
+ headers[index] = header
+ }
+
+ /**
+ * Returns an iterator over the headers that supports removing elements during iteration.
+ */
+ override fun iterator(): MutableIterator<Header> = headers.iterator()
+
+ /**
+ * Returns true if a [Header] with the given [name] exists.
+ */
+ override operator fun contains(name: String): Boolean =
+ headers.any { it.name.equals(name, ignoreCase = true) }
+
+ /**
+ * Returns the number of headers (key / value combinations).
+ */
+ override val size: Int
+ get() = headers.size
+
+ /**
+ * Append a header without removing the headers already present.
+ */
+ fun append(name: String, value: String): MutableHeaders {
+ headers.add(Header(name, value))
+ return this
+ }
+
+ /**
+ * Set the only occurrence of the header; potentially overriding an already existing header.
+ */
+ fun set(name: String, value: String): MutableHeaders {
+ headers.forEachIndexed { index, current ->
+ if (current.name.equals(name, ignoreCase = true)) {
+ headers[index] = Header(name, value)
+ return this
+ }
+ }
+
+ return append(name, value)
+ }
+
+ override fun equals(other: Any?) = other is MutableHeaders && headers == other.headers
+
+ override fun hashCode() = headers.hashCode()
+}
+
+fun List<Header>.toMutableHeaders() = MutableHeaders(this)
diff --git a/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Request.kt b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Request.kt
new file mode 100644
index 0000000000..7ea1a46df3
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Request.kt
@@ -0,0 +1,190 @@
+/* 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.concept.fetch
+
+import android.net.Uri
+import mozilla.components.concept.fetch.Request.CookiePolicy
+import java.io.Closeable
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.util.concurrent.TimeUnit
+
+/**
+ * The [Request] data class represents a resource request to be send by a [Client].
+ *
+ * It's API is inspired by the Request interface of the Web Fetch API:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Request
+ *
+ * @property url The URL of the request.
+ * @property method The request method (GET, POST, ..)
+ * @property headers Optional HTTP headers to be send with the request.
+ * @property connectTimeout A timeout to be used when connecting to the resource. If the timeout expires before the
+ * connection can be established, a [java.net.SocketTimeoutException] is raised. A timeout of zero is interpreted as an
+ * infinite timeout.
+ * @property readTimeout A timeout to be used when reading from the resource. If the timeout expires before there is
+ * data available for read, a java.net.SocketTimeoutException is raised. A timeout of zero is interpreted as an infinite
+ * timeout.
+ * @property body An optional body to be send with the request.
+ * @property redirect Whether the [Client] should follow redirects (HTTP 3xx) for this request or not.
+ * @property cookiePolicy A policy to specify whether or not cookies should be
+ * sent with the request, defaults to [CookiePolicy.INCLUDE]
+ * @property useCaches Whether caches should be used or a network request
+ * should be forced, defaults to true (use caches).
+ * @property private Whether the request should be performed in a private context, defaults to false.
+ * The feature is not support in all [Client]s, check support before using.
+ * @see [Headers.Names]
+ * @see [Headers.Values]
+ */
+data class Request(
+ val url: String,
+ val method: Method = Method.GET,
+ val headers: MutableHeaders? = MutableHeaders(),
+ val connectTimeout: Pair<Long, TimeUnit>? = null,
+ val readTimeout: Pair<Long, TimeUnit>? = null,
+ val body: Body? = null,
+ val redirect: Redirect = Redirect.FOLLOW,
+ val cookiePolicy: CookiePolicy = CookiePolicy.INCLUDE,
+ val useCaches: Boolean = true,
+ val private: Boolean = false,
+) {
+ var referrerUrl: String? = null
+ var conservative: Boolean = false
+
+ /**
+ * Create a Request for Backward compatibility.
+ * @property referrerUrl An optional url of the referrer.
+ * @property conservative Whether to turn off bleeding-edge network features to avoid breaking core browser
+ * functionality, defaults to false. Set to true for Mozilla services only.
+ */
+ constructor(
+ url: String,
+ method: Method = Method.GET,
+ headers: MutableHeaders? = MutableHeaders(),
+ connectTimeout: Pair<Long, TimeUnit>? = null,
+ readTimeout: Pair<Long, TimeUnit>? = null,
+ body: Body? = null,
+ redirect: Redirect = Redirect.FOLLOW,
+ cookiePolicy: CookiePolicy = CookiePolicy.INCLUDE,
+ useCaches: Boolean = true,
+ private: Boolean = false,
+ referrerUrl: String? = null,
+ conservative: Boolean = false,
+ ) : this(url, method, headers, connectTimeout, readTimeout, body, redirect, cookiePolicy, useCaches, private) {
+ this.referrerUrl = referrerUrl
+ this.conservative = conservative
+ }
+
+ /**
+ * A [Body] to be send with the [Request].
+ *
+ * @param stream A stream that will be read and send to the resource.
+ */
+ class Body(
+ private val stream: InputStream,
+ ) : Closeable {
+ companion object {
+ /**
+ * Create a [Body] from the provided [String].
+ */
+ fun fromString(value: String): Body = Body(value.byteInputStream())
+
+ /**
+ * Create a [Body] from the provided [File].
+ */
+ fun fromFile(file: File): Body = Body(file.inputStream())
+
+ /**
+ * Create a [Body] from the provided [unencodedParams] in the format of Content-Type
+ * "application/x-www-form-urlencoded". Parameters are formatted as "key1=value1&key2=value2..."
+ * and values are percent-encoded. If the given map is empty, the response body will contain the
+ * empty string.
+ *
+ * @see [Headers.Values.CONTENT_TYPE_FORM_URLENCODED]
+ */
+ fun fromParamsForFormUrlEncoded(vararg unencodedParams: Pair<String, String>): Body {
+ // It's unintuitive to use the Uri class format and encode
+ // but its GET query syntax is exactly what we need.
+ val uriBuilder = Uri.Builder()
+ unencodedParams.forEach { (key, value) -> uriBuilder.appendQueryParameter(key, value) }
+ val encodedBody = uriBuilder.build().encodedQuery ?: "" // null when the given map is empty.
+ return Body(encodedBody.byteInputStream())
+ }
+ }
+
+ /**
+ * Executes the given [block] function on the body's stream and then closes it down correctly whether an
+ * exception is thrown or not.
+ */
+ fun <R> useStream(block: (InputStream) -> R): R = use {
+ block(stream)
+ }
+
+ /**
+ * Closes this body and releases any system resources associated with it.
+ */
+ override fun close() {
+ try {
+ stream.close()
+ } catch (e: IOException) {
+ // Ignore
+ }
+ }
+ }
+
+ /**
+ * Request methods.
+ *
+ * The request method token is the primary source of request semantics;
+ * it indicates the purpose for which the client has made this request
+ * and what is expected by the client as a successful result.
+ *
+ * https://tools.ietf.org/html/rfc7231#section-4
+ */
+ enum class Method {
+ GET,
+ HEAD,
+ POST,
+ PUT,
+ DELETE,
+ CONNECT,
+ OPTIONS,
+ TRACE,
+ }
+
+ enum class Redirect {
+ /**
+ * Automatically follow redirects.
+ */
+ FOLLOW,
+
+ /**
+ * Do not follow redirects and let caller handle them manually.
+ */
+ MANUAL,
+ }
+
+ enum class CookiePolicy {
+ /**
+ * Include cookies when sending the request.
+ */
+ INCLUDE,
+
+ /**
+ * Do not send cookies with the request.
+ */
+ OMIT,
+ }
+}
+
+/**
+ * Checks whether or not the request is for a data URI.
+ */
+fun Request.isDataUri() = url.startsWith("data:")
+
+/**
+ * Checks whether or not the request is for a data blob.
+ */
+fun Request.isBlobUri() = url.startsWith("blob:")
diff --git a/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Response.kt b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Response.kt
new file mode 100644
index 0000000000..b72a0e2ef4
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Response.kt
@@ -0,0 +1,160 @@
+/* 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.concept.fetch
+
+import mozilla.components.concept.fetch.Response.Body
+import mozilla.components.concept.fetch.Response.Companion.CLIENT_ERROR_STATUS_RANGE
+import mozilla.components.concept.fetch.Response.Companion.SUCCESS_STATUS_RANGE
+import java.io.BufferedReader
+import java.io.Closeable
+import java.io.IOException
+import java.io.InputStream
+import java.nio.charset.Charset
+
+/**
+ * The [Response] data class represents a response to a [Request] send by a [Client].
+ *
+ * You can create a [Response] object using the constructor, but you are more likely to encounter a [Response] object
+ * being returned as the result of calling [Client.fetch].
+ *
+ * A [Response] may hold references to other resources (e.g. streams). Therefore it's important to always close the
+ * [Response] object or its [Body]. This can be done by either consuming the content of the [Body] with one of the
+ * available methods or by using Kotlin's extension methods for using [Closeable] implementations (like `use()`):
+ *
+ * ```Kotlin
+ * val response = ...
+ * response.use {
+ * // Use response. Resources will get released automatically at the end of the block.
+ * }
+ * ```
+ */
+data class Response(
+ val url: String,
+ val status: Int,
+ val headers: Headers,
+ val body: Body,
+) : Closeable {
+ /**
+ * Closes this [Response] and its [Body] and releases any system resources associated with it.
+ */
+ override fun close() {
+ body.close()
+ }
+
+ /**
+ * A [Body] returned along with the [Request].
+ *
+ * **The response body can be consumed only once.**.
+ *
+ * @param stream the input stream from which the response body can be read.
+ * @param contentType optional content-type as provided in the response
+ * header. If specified, an attempt will be made to look up the charset
+ * which will be used for decoding the body. If not specified, or if the
+ * charset can't be found, UTF-8 will be used for decoding.
+ */
+ open class Body(
+ private val stream: InputStream,
+ contentType: String? = null,
+ ) : Closeable, AutoCloseable {
+
+ @Suppress("TooGenericExceptionCaught")
+ private val charset = contentType?.let {
+ val charset = it.substringAfter("charset=")
+ try {
+ Charset.forName(charset)
+ } catch (e: Exception) {
+ Charsets.UTF_8
+ }
+ } ?: Charsets.UTF_8
+
+ /**
+ * Creates a usable stream from this body.
+ *
+ * Executes the given [block] function with the stream as parameter and then closes it down correctly
+ * whether an exception is thrown or not.
+ */
+ fun <R> useStream(block: (InputStream) -> R): R = use {
+ block(stream)
+ }
+
+ /**
+ * Creates a buffered reader from this body.
+ *
+ * Executes the given [block] function with the buffered reader as parameter and then closes it down correctly
+ * whether an exception is thrown or not.
+ *
+ * @param charset the optional charset to use when decoding the body. If not specified,
+ * the charset provided in the response content-type header will be used. If the header
+ * is missing or the charset is not supported, UTF-8 will be used.
+ * @param block a function to consume the buffered reader.
+ *
+ */
+ fun <R> useBufferedReader(charset: Charset? = null, block: (BufferedReader) -> R): R = use {
+ block(stream.bufferedReader(charset ?: this.charset))
+ }
+
+ /**
+ * Reads this body completely as a String.
+ *
+ * Takes care of closing the body down correctly whether an exception is thrown or not.
+ *
+ * @param charset the optional charset to use when decoding the body. If not specified,
+ * the charset provided in the response content-type header will be used. If the header
+ * is missing or the charset not supported, UTF-8 will be used.
+ */
+ fun string(charset: Charset? = null): String = use {
+ // We don't use a BufferedReader because it'd unnecessarily allocate more memory: if the
+ // BufferedReader is reading into a buffer whose length >= the BufferedReader's buffer
+ // length, then the BufferedReader reads directly into the other buffer as an optimization
+ // and the BufferedReader's buffer is unused (i.e. you get no benefit from the BufferedReader
+ // and you can just use a Reader). In this case, both the BufferedReader and readText
+ // would allocate a buffer of DEFAULT_BUFFER_SIZE so we removed the unnecessary
+ // BufferedReader and cut memory consumption in half. See
+ // https://github.com/mcomella/android-components/commit/db8488599f9f652b4d5775f70eeb4ab91462cbe6
+ // for code verifying this behavior.
+ //
+ // The allocation can be further optimized by setting the buffer size to Content-Length
+ // header. See https://github.com/mozilla-mobile/android-components/issues/11015
+ stream.reader(charset ?: this.charset).readText()
+ }
+
+ /**
+ * Closes this [Body] and releases any system resources associated with it.
+ */
+ override fun close() {
+ try {
+ stream.close()
+ } catch (e: IOException) {
+ // Ignore
+ }
+ }
+
+ companion object {
+ /**
+ * Creates an empty response body.
+ */
+ fun empty() = Body("".byteInputStream())
+ }
+ }
+
+ companion object {
+ val SUCCESS_STATUS_RANGE = 200..299
+ val CLIENT_ERROR_STATUS_RANGE = 400..499
+ const val SUCCESS = 200
+ const val NO_CONTENT = 204
+ }
+}
+
+/**
+ * Returns true if the response was successful (status in the range 200-299) or false otherwise.
+ */
+val Response.isSuccess: Boolean
+ get() = status in SUCCESS_STATUS_RANGE
+
+/**
+ * Returns true if the response was a client error (status in the range 400-499) or false otherwise.
+ */
+val Response.isClientError: Boolean
+ get() = status in CLIENT_ERROR_STATUS_RANGE
diff --git a/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/interceptor/Interceptor.kt b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/interceptor/Interceptor.kt
new file mode 100644
index 0000000000..d92c5ad5ab
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/main/java/mozilla/components/concept/fetch/interceptor/Interceptor.kt
@@ -0,0 +1,93 @@
+/* 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.concept.fetch.interceptor
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+
+/**
+ * An [Interceptor] for a [Client] implementation.
+ *
+ * Interceptors can monitor, modify, retry, redirect or record requests as well as responses going through a [Client].
+ */
+interface Interceptor {
+ /**
+ * Allows an [Interceptor] to intercept a request and modify request or response.
+ *
+ * An interceptor can retrieve the request by calling [Chain.request].
+ *
+ * If the interceptor wants to continue executing the chain (which will execute potentially other interceptors and
+ * may eventually perform the request) it can call [Chain.proceed] and pass along the original or a modified
+ * request.
+ *
+ * Finally the interceptor needs to return a [Response]. This can either be the [Response] from calling
+ * [Chain.proceed] - modified or unmodified - or a [Response] the interceptor created manually or obtained from
+ * a different source.
+ */
+ fun intercept(chain: Chain): Response
+
+ /**
+ * The request interceptor chain.
+ */
+ interface Chain {
+ /**
+ * The current request. May be modified by a previously executed interceptor.
+ */
+ val request: Request
+
+ /**
+ * Proceed executing the interceptor chain and eventually perform the request.
+ */
+ fun proceed(request: Request): Response
+ }
+}
+
+/**
+ * Creates a new [Client] instance that will use the provided list of [Interceptor] instances.
+ */
+fun Client.withInterceptors(
+ vararg interceptors: Interceptor,
+): Client = InterceptorClient(this, interceptors.toList())
+
+/**
+ * A [Client] instance that will wrap the provided [actualClient] and call the interceptor chain before executing
+ * the request.
+ */
+private class InterceptorClient(
+ private val actualClient: Client,
+ private val interceptors: List<Interceptor>,
+) : Client() {
+ override fun fetch(request: Request): Response =
+ InterceptorChain(actualClient, interceptors.toList(), request)
+ .proceed(request)
+}
+
+/**
+ * [InterceptorChain] implementation that keeps track of executing the chain of interceptors before executing the
+ * request on the provided [client].
+ */
+private class InterceptorChain(
+ private val client: Client,
+ private val interceptors: List<Interceptor>,
+ private var currentRequest: Request,
+) : Interceptor.Chain {
+ private var index = 0
+
+ override val request: Request
+ get() = currentRequest
+
+ override fun proceed(request: Request): Response {
+ currentRequest = request
+
+ return if (index < interceptors.size) {
+ val interceptor = interceptors[index]
+ index++
+ interceptor.intercept(this)
+ } else {
+ client.fetch(request)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ClientTest.kt b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ClientTest.kt
new file mode 100644
index 0000000000..96e0663e7e
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ClientTest.kt
@@ -0,0 +1,36 @@
+/* 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.concept.fetch
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ClientTest {
+ @ExperimentalCoroutinesApi
+ @Test
+ fun `Async request with coroutines`() = runTest {
+ val client = TestClient(responseBody = Response.Body("Hello World".byteInputStream()))
+ val request = Request("https://www.mozilla.org")
+
+ val deferredResponse = async { client.fetch(request) }
+
+ val body = deferredResponse.await().body.string()
+ assertEquals("Hello World", body)
+ }
+}
+
+private class TestClient(
+ private val responseUrl: String? = null,
+ private val responseStatus: Int = 200,
+ private val responseHeaders: Headers = MutableHeaders(),
+ private val responseBody: Response.Body = Response.Body.empty(),
+) : Client() {
+ override fun fetch(request: Request): Response {
+ return Response(responseUrl ?: request.url, responseStatus, responseHeaders, responseBody)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/HeadersTest.kt b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/HeadersTest.kt
new file mode 100644
index 0000000000..c84bd5f53a
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/HeadersTest.kt
@@ -0,0 +1,240 @@
+/* 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.concept.fetch
+
+import mozilla.components.support.test.expectException
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.lang.IllegalArgumentException
+
+class HeadersTest {
+ @Test
+ fun `Creating Headers using constructor`() {
+ val headers = MutableHeaders(
+ "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Encoding" to "gzip, deflate",
+ "Accept-Language" to "en-US,en;q=0.5",
+ "Connection" to "keep-alive",
+ "Dnt" to "1",
+ "User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0",
+ )
+
+ assertEquals(6, headers.size)
+
+ assertEquals("Accept", headers[0].name)
+ assertEquals("Accept-Encoding", headers[1].name)
+ assertEquals("Accept-Language", headers[2].name)
+ assertEquals("Connection", headers[3].name)
+ assertEquals("Dnt", headers[4].name)
+ assertEquals("User-Agent", headers[5].name)
+
+ assertEquals("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", headers[0].value)
+ assertEquals("gzip, deflate", headers[1].value)
+ assertEquals("en-US,en;q=0.5", headers[2].value)
+ assertEquals("keep-alive", headers[3].value)
+ assertEquals("1", headers[4].value)
+ assertEquals("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0", headers[5].value)
+ }
+
+ @Test
+ fun `Setting headers`() {
+ val headers = MutableHeaders()
+
+ headers.set("Accept-Encoding", "gzip, deflate")
+ headers.set("Connection", "keep-alive")
+ headers.set("Accept-Encoding", "gzip")
+ headers.set("Dnt", "1")
+
+ assertEquals(3, headers.size)
+
+ assertEquals("Accept-Encoding", headers[0].name)
+ assertEquals("Connection", headers[1].name)
+ assertEquals("Dnt", headers[2].name)
+
+ assertEquals("gzip", headers[0].value)
+ assertEquals("keep-alive", headers[1].value)
+ assertEquals("1", headers[2].value)
+ }
+
+ @Test
+ fun `Appending headers`() {
+ val headers = MutableHeaders()
+
+ headers.append("Accept-Encoding", "gzip, deflate")
+ headers.append("Connection", "keep-alive")
+ headers.append("Accept-Encoding", "gzip")
+ headers.append("Dnt", "1")
+
+ assertEquals(4, headers.size)
+
+ assertEquals("Accept-Encoding", headers[0].name)
+ assertEquals("Connection", headers[1].name)
+ assertEquals("Accept-Encoding", headers[2].name)
+ assertEquals("Dnt", headers[3].name)
+
+ assertEquals("gzip, deflate", headers[0].value)
+ assertEquals("keep-alive", headers[1].value)
+ assertEquals("gzip", headers[2].value)
+ assertEquals("1", headers[3].value)
+ }
+
+ @Test
+ fun `Overriding headers at index`() {
+ val headers = MutableHeaders().apply {
+ set("User-Agent", "Mozilla/5.0")
+ set("Connection", "keep-alive")
+ set("Accept-Encoding", "gzip")
+ }
+
+ headers[2] = Header("Dnt", "0")
+ headers[0] = Header("Accept-Language", "en-US,en;q=0.5")
+
+ assertEquals(3, headers.size)
+
+ assertEquals("Accept-Language", headers[0].name)
+ assertEquals("Connection", headers[1].name)
+ assertEquals("Dnt", headers[2].name)
+
+ assertEquals("en-US,en;q=0.5", headers[0].value)
+ assertEquals("keep-alive", headers[1].value)
+ assertEquals("0", headers[2].value)
+ }
+
+ @Test
+ fun `Contains header with name`() {
+ val headers = MutableHeaders().apply {
+ set("User-Agent", "Mozilla/5.0")
+ set("Connection", "keep-alive")
+ set("Accept-Encoding", "gzip")
+ }
+
+ assertTrue(headers.contains("User-Agent"))
+ assertTrue(headers.contains("Connection"))
+ assertTrue(headers.contains("Accept-Encoding"))
+
+ assertFalse(headers.contains("Accept-Language"))
+ assertFalse(headers.contains("Dnt"))
+ assertFalse(headers.contains("Accept"))
+ }
+
+ @Test
+ fun `Throws if header name is empty`() {
+ expectException(IllegalArgumentException::class) {
+ MutableHeaders(
+ "" to "Mozilla/5.0",
+ )
+ }
+
+ expectException(IllegalArgumentException::class) {
+ MutableHeaders()
+ .append("", "Mozilla/5.0")
+ }
+
+ expectException(IllegalArgumentException::class) {
+ MutableHeaders()
+ .set("", "Mozilla/5.0")
+ }
+
+ expectException(IllegalArgumentException::class) {
+ Header("", "Mozilla/5.0")
+ }
+ }
+
+ @Test
+ fun `Iterator usage`() {
+ val headers = MutableHeaders().apply {
+ set("User-Agent", "Mozilla/5.0")
+ set("Connection", "keep-alive")
+ set("Accept-Encoding", "gzip")
+ }
+
+ var i = 0
+ headers.forEach { _ -> i++ }
+
+ assertEquals(3, i)
+
+ assertNotNull(headers.firstOrNull { header -> header.name == "User-Agent" })
+ }
+
+ @Test
+ fun `Creating and modifying headers`() {
+ val headers = MutableHeaders(
+ "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Encoding" to "gzip, deflate",
+ "Accept-Language" to "en-US,en;q=0.5",
+ "Connection" to "keep-alive",
+ "Dnt" to "1",
+ "User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0",
+ )
+
+ headers.set("Dnt", "0")
+ headers.set("User-Agent", "Mozilla/6.0")
+ headers.append("Accept", "*/*")
+
+ assertEquals(7, headers.size)
+
+ assertEquals("Accept", headers[0].name)
+ assertEquals("Accept-Encoding", headers[1].name)
+ assertEquals("Accept-Language", headers[2].name)
+ assertEquals("Connection", headers[3].name)
+ assertEquals("Dnt", headers[4].name)
+ assertEquals("User-Agent", headers[5].name)
+ assertEquals("Accept", headers[6].name)
+
+ assertEquals("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", headers[0].value)
+ assertEquals("gzip, deflate", headers[1].value)
+ assertEquals("en-US,en;q=0.5", headers[2].value)
+ assertEquals("keep-alive", headers[3].value)
+ assertEquals("0", headers[4].value)
+ assertEquals("Mozilla/6.0", headers[5].value)
+ assertEquals("*/*", headers[6].value)
+ }
+
+ @Test
+ fun `In operator`() {
+ val headers = MutableHeaders().apply {
+ set("User-Agent", "Mozilla/5.0")
+ set("Connection", "keep-alive")
+ set("Accept-Encoding", "gzip")
+ }
+
+ assertTrue("User-Agent" in headers)
+ assertTrue("Connection" in headers)
+ assertTrue("Accept-Encoding" in headers)
+
+ assertFalse("Accept-Language" in headers)
+ assertFalse("Accept" in headers)
+ assertFalse("Dnt" in headers)
+ }
+
+ @Test
+ fun `Get multiple headers by name`() {
+ val headers = MutableHeaders().apply {
+ append("Accept-Encoding", "gzip")
+ append("Accept-Encoding", "deflate")
+ append("Connection", "keep-alive")
+ }
+
+ val values = headers.getAll("Accept-Encoding")
+ assertEquals(2, values.size)
+ assertEquals("gzip", values[0])
+ assertEquals("deflate", values[1])
+ }
+
+ @Test
+ fun `Getting headers by name`() {
+ val headers = MutableHeaders().apply {
+ append("Accept-Encoding", "gzip")
+ append("Accept-Encoding", "deflate")
+ append("Connection", "keep-alive")
+ }
+
+ assertEquals("deflate", headers["Accept-Encoding"])
+ assertEquals("keep-alive", headers["Connection"])
+ }
+}
diff --git a/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/RequestTest.kt b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/RequestTest.kt
new file mode 100644
index 0000000000..d6b05456da
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/RequestTest.kt
@@ -0,0 +1,191 @@
+/* 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.concept.fetch
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.net.URLEncoder
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+class RequestTest {
+
+ @Test
+ fun `URL-only Request`() {
+ val request = Request("https://www.mozilla.org")
+
+ assertEquals("https://www.mozilla.org", request.url)
+ assertEquals(Request.Method.GET, request.method)
+ }
+
+ @Test
+ fun `Fully configured Request`() {
+ val request = Request(
+ url = "https://www.mozilla.org",
+ method = Request.Method.POST,
+ headers = MutableHeaders(
+ "Accept-Language" to "en-US,en;q=0.5",
+ "Connection" to "keep-alive",
+ "Dnt" to "1",
+ ),
+ connectTimeout = Pair(10, TimeUnit.SECONDS),
+ readTimeout = Pair(1, TimeUnit.MINUTES),
+ body = Request.Body.fromString("Hello World!"),
+ redirect = Request.Redirect.MANUAL,
+ cookiePolicy = Request.CookiePolicy.INCLUDE,
+ useCaches = true,
+ referrerUrl = "https://mozilla.org",
+ conservative = true,
+ )
+
+ assertEquals("https://www.mozilla.org", request.url)
+ assertEquals(Request.Method.POST, request.method)
+
+ assertEquals(10, request.connectTimeout!!.first)
+ assertEquals(TimeUnit.SECONDS, request.connectTimeout!!.second)
+
+ assertEquals(1, request.readTimeout!!.first)
+ assertEquals(TimeUnit.MINUTES, request.readTimeout!!.second)
+
+ assertEquals("Hello World!", request.body!!.useStream { it.bufferedReader().readText() })
+ assertEquals(Request.Redirect.MANUAL, request.redirect)
+ assertEquals(Request.CookiePolicy.INCLUDE, request.cookiePolicy)
+ assertEquals(true, request.useCaches)
+ assertEquals("https://mozilla.org", request.referrerUrl)
+ assertEquals(true, request.conservative)
+
+ val headers = request.headers!!
+ assertEquals(3, headers.size)
+
+ assertEquals("Accept-Language", headers[0].name)
+ assertEquals("Connection", headers[1].name)
+ assertEquals("Dnt", headers[2].name)
+
+ assertEquals("en-US,en;q=0.5", headers[0].value)
+ assertEquals("keep-alive", headers[1].value)
+ assertEquals("1", headers[2].value)
+ }
+
+ @Test
+ fun `Create request body from string`() {
+ val body = Request.Body.fromString("Hello World")
+ assertEquals("Hello World", body.readText())
+ }
+
+ @Test
+ fun `Create request body from file`() {
+ val file = File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString())
+ file.writer().use { it.write("Banana") }
+
+ val body = Request.Body.fromFile(file)
+ assertEquals("Banana", body.readText())
+ }
+
+ @Test
+ fun `WHEN creating a request body from empty params THEN the empty string is returned`() {
+ assertEquals("", Request.Body.fromParamsForFormUrlEncoded().readText())
+ }
+
+ @Test
+ fun `WHEN creating a request body from params with empty keys or values THEN they are represented as the empty string in the result`() {
+ // In practice, we don't expect anyone to do this but this test is here as to documentation of what happens.
+ val expected = "=value&hello=world&key="
+ val body = Request.Body.fromParamsForFormUrlEncoded(
+ "" to "value",
+ "hello" to "world",
+ "key" to "",
+ )
+ assertEquals(expected, body.readText())
+ }
+
+ @Test
+ fun `WHEN creating a request body from non-alphabetized params for urlencoded THEN it's in the correct format and ordering`() {
+ val inputUrl = "https://github.com/mozilla-mobile/android-components/issues/2394"
+ val encodedURL = URLEncoder.encode(inputUrl, Charsets.UTF_8.name())
+ val expected = "v=2&url=$encodedURL"
+
+ val body = Request.Body.fromParamsForFormUrlEncoded(
+ "v" to "2",
+ "url" to inputUrl,
+ )
+ assertEquals(expected, body.readText())
+ }
+
+ @Test
+ fun `Closing body closes stream`() {
+ val stream: InputStream = mock()
+
+ val body = Request.Body(stream)
+
+ verify(stream, never()).close()
+
+ body.close()
+
+ verify(stream).close()
+ }
+
+ @Test
+ fun `Using stream closes stream`() {
+ val stream: InputStream = mock()
+
+ val body = Request.Body(stream)
+
+ verify(stream, never()).close()
+
+ body.useStream {
+ // Do nothing
+ }
+
+ verify(stream).close()
+ }
+
+ @Test
+ fun `Stream throwing on close`() {
+ val stream: InputStream = mock()
+ doThrow(IOException()).`when`(stream).close()
+
+ val body = Request.Body(stream)
+ body.close()
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `useStream rethrows and closes stream`() {
+ val stream: InputStream = mock()
+ val body = Request.Body(stream)
+
+ try {
+ body.useStream {
+ throw IllegalStateException()
+ }
+ } finally {
+ verify(stream).close()
+ }
+ }
+
+ @Test
+ fun `Is a blob Request`() {
+ var request = Request(url = "blob:https://mdn.mozillademos.org/d518464c-5075-9046")
+
+ assertTrue(request.isBlobUri())
+
+ request = Request(url = "https://mdn.mozillademos.org/d518464c-5075-9046")
+
+ assertFalse(request.isBlobUri())
+ }
+}
+
+private fun Request.Body.readText(): String = useStream { it.bufferedReader().readText() }
diff --git a/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ResponseTest.kt b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ResponseTest.kt
new file mode 100644
index 0000000000..625f580d3f
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ResponseTest.kt
@@ -0,0 +1,258 @@
+/* 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.concept.fetch
+
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.io.IOException
+import java.io.InputStream
+
+class ResponseTest {
+ @Test
+ fun `Creating String from Body`() {
+ val stream = "Hello World".byteInputStream()
+
+ val body = spy(Response.Body(stream))
+ assertEquals("Hello World", body.string())
+
+ verify(body).close()
+ }
+
+ @Test
+ fun `Creating BufferedReader from Body`() {
+ val stream = "Hello World".byteInputStream()
+
+ val body = spy(Response.Body(stream))
+
+ var readerUsed = false
+ body.useBufferedReader { reader ->
+ assertEquals("Hello World", reader.readText())
+ readerUsed = true
+ }
+
+ assertTrue(readerUsed)
+
+ verify(body).close()
+ }
+
+ @Test
+ fun `Creating BufferedReader from Body with custom Charset `() {
+ var stream = "ÄäÖöÜü".byteInputStream(Charsets.ISO_8859_1)
+ var body = spy(Response.Body(stream, "text/plain; charset=UTF-8"))
+ var readerUsed = false
+ body.useBufferedReader { reader ->
+ assertNotEquals("ÄäÖöÜü", reader.readText())
+ readerUsed = true
+ }
+ assertTrue(readerUsed)
+
+ stream = "ÄäÖöÜü".byteInputStream(Charsets.ISO_8859_1)
+ body = spy(Response.Body(stream, "text/plain; charset=UTF-8"))
+ readerUsed = false
+ body.useBufferedReader(Charsets.ISO_8859_1) { reader ->
+ assertEquals("ÄäÖöÜü", reader.readText())
+ readerUsed = true
+ }
+ assertTrue(readerUsed)
+
+ verify(body).close()
+ }
+
+ @Test
+ fun `Creating String from Body with custom Charset `() {
+ var stream = "ÄäÖöÜü".byteInputStream(Charsets.ISO_8859_1)
+ var body = spy(Response.Body(stream, "text/plain; charset=UTF-8"))
+ assertNotEquals("ÄäÖöÜü", body.string())
+
+ stream = "ÄäÖöÜü".byteInputStream(Charsets.ISO_8859_1)
+ body = spy(Response.Body(stream, "text/plain; charset=UTF-8"))
+ assertEquals("ÄäÖöÜü", body.string(Charsets.ISO_8859_1))
+
+ verify(body).close()
+ }
+
+ @Test
+ fun `Creating Body with invalid charset falls back to UTF-8`() {
+ var stream = "ÄäÖöÜü".byteInputStream(Charsets.UTF_8)
+ var body = spy(Response.Body(stream, "text/plain; charset=invalid"))
+ var readerUsed = false
+ body.useBufferedReader { reader ->
+ assertEquals("ÄäÖöÜü", reader.readText())
+ readerUsed = true
+ }
+ assertTrue(readerUsed)
+
+ verify(body).close()
+ }
+
+ @Test
+ fun `Using InputStream from Body`() {
+ val body = spy(Response.Body("Hello World".byteInputStream()))
+
+ var streamUsed = false
+ body.useStream { stream ->
+ assertEquals("Hello World", stream.bufferedReader().readText())
+ streamUsed = true
+ }
+
+ assertTrue(streamUsed)
+
+ verify(body).close()
+ }
+
+ @Test
+ fun `Closing Body closes stream`() {
+ val stream = spy("Hello World".byteInputStream())
+
+ val body = spy(Response.Body(stream))
+ body.close()
+
+ verify(stream).close()
+ }
+
+ @Test
+ fun `success() extension function returns true for 2xx response codes`() {
+ assertTrue(Response("https://www.mozilla.org", 200, headers = mock(), body = mock()).isSuccess)
+ assertTrue(Response("https://www.mozilla.org", 203, headers = mock(), body = mock()).isSuccess)
+
+ assertFalse(Response("https://www.mozilla.org", 404, headers = mock(), body = mock()).isSuccess)
+ assertFalse(Response("https://www.mozilla.org", 500, headers = mock(), body = mock()).isSuccess)
+ assertFalse(Response("https://www.mozilla.org", 302, headers = mock(), body = mock()).isSuccess)
+ }
+
+ @Test
+ fun `clientError() extension function returns true for 4xx response codes`() {
+ assertTrue(Response("https://www.mozilla.org", 404, headers = mock(), body = mock()).isClientError)
+ assertTrue(Response("https://www.mozilla.org", 403, headers = mock(), body = mock()).isClientError)
+
+ assertFalse(Response("https://www.mozilla.org", 200, headers = mock(), body = mock()).isClientError)
+ assertFalse(Response("https://www.mozilla.org", 203, headers = mock(), body = mock()).isClientError)
+ assertFalse(Response("https://www.mozilla.org", 500, headers = mock(), body = mock()).isClientError)
+ assertFalse(Response("https://www.mozilla.org", 302, headers = mock(), body = mock()).isClientError)
+ }
+
+ @Test
+ fun `Fully configured Response`() {
+ val response = Response(
+ url = "https://www.mozilla.org",
+ status = 200,
+ headers = MutableHeaders(
+ CONTENT_TYPE to "text/html; charset=utf-8",
+ "Connection" to "Close",
+ "Expires" to "Thu, 08 Nov 2018 15:41:43 GMT",
+ ),
+ body = Response.Body("Hello World".byteInputStream()),
+ )
+
+ assertEquals("https://www.mozilla.org", response.url)
+ assertEquals(200, response.status)
+ assertEquals("Hello World", response.body.string())
+
+ val headers = response.headers
+ assertEquals(3, headers.size)
+
+ assertEquals("Content-Type", headers[0].name)
+ assertEquals("Connection", headers[1].name)
+ assertEquals("Expires", headers[2].name)
+
+ assertEquals("text/html; charset=utf-8", headers[0].value)
+ assertEquals("Close", headers[1].value)
+ assertEquals("Thu, 08 Nov 2018 15:41:43 GMT", headers[2].value)
+ }
+
+ @Test
+ fun `Closing body closes stream of body`() {
+ val stream: InputStream = mock()
+ val response = Response("url", 200, MutableHeaders(), Response.Body(stream))
+
+ verify(stream, never()).close()
+
+ response.body.close()
+
+ verify(stream).close()
+ }
+
+ @Test
+ fun `Closing response closes stream of body`() {
+ val stream: InputStream = mock()
+ val response = Response("url", 200, MutableHeaders(), Response.Body(stream))
+
+ verify(stream, never()).close()
+
+ response.close()
+
+ verify(stream).close()
+ }
+
+ @Test
+ fun `Empty body`() {
+ val body = Response.Body.empty()
+ assertEquals("", body.string())
+ }
+
+ @Test
+ fun `Creating string closes stream`() {
+ val stream: InputStream = spy("".byteInputStream())
+ val body = Response.Body(stream)
+
+ verify(stream, never()).close()
+
+ body.string()
+
+ verify(stream).close()
+ }
+
+ @Test(expected = TestException::class)
+ fun `Using buffered reader closes stream`() {
+ val stream: InputStream = spy("".byteInputStream())
+ val body = Response.Body(stream)
+
+ verify(stream, never()).close()
+
+ try {
+ body.useBufferedReader {
+ throw TestException()
+ }
+ } finally {
+ verify(stream).close()
+ }
+ }
+
+ @Test(expected = TestException::class)
+ fun `Using stream closes stream`() {
+ val stream: InputStream = spy("".byteInputStream())
+ val body = Response.Body(stream)
+
+ verify(stream, never()).close()
+
+ try {
+ body.useStream {
+ throw TestException()
+ }
+ } finally {
+ verify(stream).close()
+ }
+ }
+
+ @Test
+ fun `Stream throwing on close`() {
+ val stream: InputStream = mock()
+ Mockito.doThrow(IOException()).`when`(stream).close()
+
+ val body = Response.Body(stream)
+ body.close()
+ }
+}
+
+private class TestException : RuntimeException()
diff --git a/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/interceptor/InterceptorTest.kt b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/interceptor/InterceptorTest.kt
new file mode 100644
index 0000000000..3237665c12
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/interceptor/InterceptorTest.kt
@@ -0,0 +1,132 @@
+/* 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.concept.fetch.interceptor
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isSuccess
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class InterceptorTest {
+ @Test
+ fun `Interceptors are invoked`() {
+ var interceptorInvoked1 = false
+ var interceptorInvoked2 = false
+
+ val interceptor1 = object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ interceptorInvoked1 = true
+ return chain.proceed(chain.request)
+ }
+ }
+
+ val interceptor2 = object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ interceptorInvoked2 = true
+ return chain.proceed(chain.request)
+ }
+ }
+
+ val fake = FakeClient()
+ val client = fake.withInterceptors(interceptor1, interceptor2)
+
+ assertFalse(interceptorInvoked1)
+ assertFalse(interceptorInvoked2)
+
+ val response = client.fetch(Request(url = "https://www.mozilla.org"))
+ assertTrue(fake.resourceFetched)
+ assertTrue(response.isSuccess)
+
+ assertTrue(interceptorInvoked1)
+ assertTrue(interceptorInvoked2)
+ }
+
+ @Test
+ fun `Interceptors are invoked in order`() {
+ val order = mutableListOf<String>()
+
+ val fake = FakeClient()
+ val client = fake.withInterceptors(
+ object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ assertEquals("https://www.mozilla.org", chain.request.url)
+ order.add("A")
+ return chain.proceed(
+ chain.request.copy(
+ url = chain.request.url + "/a",
+ ),
+ )
+ }
+ },
+ object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ assertEquals("https://www.mozilla.org/a", chain.request.url)
+ order.add("B")
+ return chain.proceed(
+ chain.request.copy(
+ url = chain.request.url + "/b",
+ ),
+ )
+ }
+ },
+ object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ assertEquals("https://www.mozilla.org/a/b", chain.request.url)
+ order.add("C")
+ return chain.proceed(
+ chain.request.copy(
+ url = chain.request.url + "/c",
+ ),
+ )
+ }
+ },
+ )
+
+ val response = client.fetch(Request(url = "https://www.mozilla.org"))
+ assertTrue(fake.resourceFetched)
+ assertTrue(response.isSuccess)
+
+ assertEquals("https://www.mozilla.org/a/b/c", response.url)
+ assertEquals(listOf("A", "B", "C"), order)
+ }
+
+ @Test
+ fun `Intercepted request is never fetched`() {
+ val fake = FakeClient()
+ val client = fake.withInterceptors(
+ object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ return Response("https://www.firefox.com", 203, MutableHeaders(), Response.Body.empty())
+ }
+ },
+ )
+
+ val response = client.fetch(Request(url = "https://www.mozilla.org"))
+ assertFalse(fake.resourceFetched)
+ assertTrue(response.isSuccess)
+ assertEquals(203, response.status)
+ }
+}
+
+private class FakeClient(
+ val response: Response? = null,
+) : Client() {
+ var resourceFetched = false
+
+ override fun fetch(request: Request): Response {
+ resourceFetched = true
+ return response ?: Response(
+ url = request.url,
+ status = 200,
+ body = Response.Body.empty(),
+ headers = MutableHeaders(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/concept/fetch/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/concept/fetch/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/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/concept/fetch/src/test/resources/robolectric.properties b/mobile/android/android-components/components/concept/fetch/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/concept/fetch/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/concept/menu/build.gradle b/mobile/android/android-components/components/concept/menu/build.gradle
new file mode 100644
index 0000000000..3ba2e45a89
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/build.gradle
@@ -0,0 +1,38 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.menu'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':support-test')
+}
+
+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/concept/menu/proguard-rules.pro b/mobile/android/android-components/components/concept/menu/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/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/concept/menu/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/menu/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/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/concept/menu/src/main/java/mozilla/components/concept/menu/MenuButton.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuButton.kt
new file mode 100644
index 0000000000..c59f25cb70
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuButton.kt
@@ -0,0 +1,47 @@
+/* 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.concept.menu
+
+import androidx.annotation.ColorInt
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.support.base.observer.Observable
+
+/**
+ * A `three-dot` button used for expanding menus.
+ *
+ * If you are using a browser toolbar, do not use this class directly.
+ */
+interface MenuButton : Observable<MenuButton.Observer> {
+
+ /**
+ * Sets a [MenuController] that will be used to create a menu when this button is clicked.
+ */
+ var menuController: MenuController?
+
+ /**
+ * Show the indicator for a browser menu effect.
+ */
+ fun setEffect(effect: MenuEffect?)
+
+ /**
+ * Sets the tint of the 3-dot menu icon.
+ */
+ fun setColorFilter(@ColorInt color: Int)
+
+ /**
+ * Observer for the menu button.
+ */
+ interface Observer {
+ /**
+ * Listener called when the menu is shown.
+ */
+ fun onShow() = Unit
+
+ /**
+ * Listener called when the menu is dismissed.
+ */
+ fun onDismiss() = Unit
+ }
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt
new file mode 100644
index 0000000000..d3a8e0f7e6
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt
@@ -0,0 +1,53 @@
+/* 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.concept.menu
+
+import android.view.View
+import android.widget.PopupWindow
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.support.base.observer.Observable
+
+/**
+ * Controls a popup menu composed of MenuCandidate objects.
+ */
+interface MenuController : Observable<MenuController.Observer> {
+
+ /**
+ * @param anchor The view on which to pin the popup window.
+ * @param orientation The preferred orientation to show the popup window.
+ * @param autoDismiss True if the popup window should be dismissed when the device orientation
+ * is changed.
+ */
+ fun show(
+ anchor: View,
+ orientation: Orientation? = null,
+ autoDismiss: Boolean = true,
+ ): PopupWindow
+
+ /**
+ * Dismiss the menu popup if the menu is visible.
+ */
+ fun dismiss()
+
+ /**
+ * Changes the contents of the menu.
+ */
+ fun submitList(list: List<MenuCandidate>)
+
+ /**
+ * Observer for the menu controller.
+ */
+ interface Observer {
+ /**
+ * Called when the menu contents have changed.
+ */
+ fun onMenuListSubmit(list: List<MenuCandidate>) = Unit
+
+ /**
+ * Called when the menu has been dismissed.
+ */
+ fun onDismiss() = Unit
+ }
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuStyle.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuStyle.kt
new file mode 100644
index 0000000000..7db0b70b35
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuStyle.kt
@@ -0,0 +1,44 @@
+/* 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.concept.menu
+
+import android.content.res.ColorStateList
+import androidx.annotation.ColorInt
+import androidx.annotation.Px
+
+/**
+ * Declare custom styles for a menu.
+ *
+ * @property backgroundColor Custom background color for the menu.
+ * @property minWidth Custom minimum width for the menu.
+ * @property maxWidth Custom maximum width for the menu.
+ * @property horizontalOffset Custom horizontal offset for the menu.
+ * @property verticalOffset Custom vertical offset for the menu.
+ * @property completelyOverlap Forces menu to overlap the anchor completely.
+ */
+data class MenuStyle(
+ val backgroundColor: ColorStateList? = null,
+ @Px val minWidth: Int? = null,
+ @Px val maxWidth: Int? = null,
+ @Px val horizontalOffset: Int? = null,
+ @Px val verticalOffset: Int? = null,
+ val completelyOverlap: Boolean = false,
+) {
+ constructor(
+ @ColorInt backgroundColor: Int,
+ @Px minWidth: Int? = null,
+ @Px maxWidth: Int? = null,
+ @Px horizontalOffset: Int? = null,
+ @Px verticalOffset: Int? = null,
+ completelyOverlap: Boolean = false,
+ ) : this(
+ backgroundColor = ColorStateList.valueOf(backgroundColor),
+ minWidth = minWidth,
+ maxWidth = maxWidth,
+ horizontalOffset = horizontalOffset,
+ verticalOffset = verticalOffset,
+ completelyOverlap = completelyOverlap,
+ )
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Orientation.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Orientation.kt
new file mode 100644
index 0000000000..60d453480c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Orientation.kt
@@ -0,0 +1,39 @@
+/* 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.concept.menu
+
+import android.view.Gravity
+
+/**
+ * Indicates the preferred orientation to show the menu.
+ */
+enum class Orientation {
+ /**
+ * Position the menu above the toolbar.
+ */
+ UP,
+
+ /**
+ * Position the menu below the toolbar.
+ */
+ DOWN,
+
+ ;
+
+ companion object {
+
+ /**
+ * Returns an orientation that matches the given [Gravity] value.
+ * Meant to be used with a CoordinatorLayout's gravity.
+ */
+ fun fromGravity(gravity: Int): Orientation {
+ return if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
+ UP
+ } else {
+ DOWN
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Side.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Side.kt
new file mode 100644
index 0000000000..6a956f6e73
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Side.kt
@@ -0,0 +1,20 @@
+/* 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.concept.menu
+
+/**
+ * Indicates the starting or ending side of the menu or an option.
+ */
+enum class Side {
+ /**
+ * Starting side (top or left).
+ */
+ START,
+
+ /**
+ * Ending side (bottom or right).
+ */
+ END,
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/ContainerStyle.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/ContainerStyle.kt
new file mode 100644
index 0000000000..df6242b0e9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/ContainerStyle.kt
@@ -0,0 +1,16 @@
+/* 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.concept.menu.candidate
+
+/**
+ * Describes styling for the menu option container.
+ *
+ * @property isVisible When false, the option will not be displayed.
+ * @property isEnabled When false, the option will be greyed out and disabled.
+ */
+data class ContainerStyle(
+ val isVisible: Boolean = true,
+ val isEnabled: Boolean = true,
+)
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuCandidate.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuCandidate.kt
new file mode 100644
index 0000000000..48a12909d1
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuCandidate.kt
@@ -0,0 +1,123 @@
+/* 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.concept.menu.candidate
+
+/**
+ * Menu option data classes to be shown in the browser menu.
+ */
+sealed class MenuCandidate {
+ abstract val containerStyle: ContainerStyle
+}
+
+/**
+ * Interactive menu option that displays some text.
+ *
+ * @property text Text to display.
+ * @property start Icon to display before the text.
+ * @property end Icon to display after the text.
+ * @property textStyle Styling to apply to the text.
+ * @property containerStyle Styling to apply to the container.
+ * @property effect Effects to apply to the option.
+ * @property onClick Click listener called when this menu option is clicked.
+ */
+data class TextMenuCandidate(
+ val text: String,
+ val start: MenuIcon? = null,
+ val end: MenuIcon? = null,
+ val textStyle: TextStyle = TextStyle(),
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+ val effect: MenuCandidateEffect? = null,
+ val onClick: () -> Unit = {},
+) : MenuCandidate()
+
+/**
+ * Menu option that displays static text.
+ *
+ * @property text Text to display.
+ * @property height Custom height for the menu option.
+ * @property textStyle Styling to apply to the text.
+ * @property containerStyle Styling to apply to the container.
+ */
+data class DecorativeTextMenuCandidate(
+ val text: String,
+ val height: Int? = null,
+ val textStyle: TextStyle = TextStyle(),
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+) : MenuCandidate()
+
+/**
+ * Menu option that shows a switch or checkbox.
+ *
+ * @property text Text to display.
+ * @property start Icon to display before the text.
+ * @property end Compound button to display after the text.
+ * @property textStyle Styling to apply to the text.
+ * @property containerStyle Styling to apply to the container.
+ * @property effect Effects to apply to the option.
+ * @property onCheckedChange Listener called when this menu option is checked or unchecked.
+ */
+data class CompoundMenuCandidate(
+ val text: String,
+ val isChecked: Boolean,
+ val start: MenuIcon? = null,
+ val end: ButtonType,
+ val textStyle: TextStyle = TextStyle(),
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+ val effect: MenuCandidateEffect? = null,
+ val onCheckedChange: (Boolean) -> Unit = {},
+) : MenuCandidate() {
+
+ /**
+ * Compound button types to display with the compound menu option.
+ */
+ enum class ButtonType {
+ CHECKBOX,
+ SWITCH,
+ }
+}
+
+/**
+ * Menu option that opens a nested sub menu.
+ *
+ * @property id Unique ID for this nested menu. Can be a resource ID.
+ * @property text Text to display.
+ * @property start Icon to display before the text.
+ * @property end Icon to display after the text.
+ * @property subMenuItems Nested menu items to display.
+ * If null, this item will instead return to the root menu.
+ * @property textStyle Styling to apply to the text.
+ * @property containerStyle Styling to apply to the container.
+ * @property effect Effects to apply to the option.
+ */
+data class NestedMenuCandidate(
+ val id: Int,
+ val text: String,
+ val start: MenuIcon? = null,
+ val end: DrawableMenuIcon? = null,
+ val subMenuItems: List<MenuCandidate>? = emptyList(),
+ val textStyle: TextStyle = TextStyle(),
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+ val effect: MenuCandidateEffect? = null,
+) : MenuCandidate()
+
+/**
+ * Displays a row of small menu options.
+ *
+ * @property items Small menu options to display.
+ * @property containerStyle Styling to apply to the container.
+ */
+data class RowMenuCandidate(
+ val items: List<SmallMenuCandidate>,
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+) : MenuCandidate()
+
+/**
+ * Menu option to display a horizontal divider.
+ *
+ * @property containerStyle Styling to apply to the divider.
+ */
+data class DividerMenuCandidate(
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+) : MenuCandidate()
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuEffect.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuEffect.kt
new file mode 100644
index 0000000000..b30104a636
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuEffect.kt
@@ -0,0 +1,46 @@
+/* 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.concept.menu.candidate
+
+import androidx.annotation.ColorInt
+
+/**
+ * Describes an effect for the menu.
+ * Effects can also alter the button to open the menu.
+ */
+sealed class MenuEffect
+
+/**
+ * Describes an effect for a menu candidate and its container.
+ * Effects can also alter the button that opens the menu.
+ */
+sealed class MenuCandidateEffect : MenuEffect()
+
+/**
+ * Describes an effect for a menu icon.
+ * Effects can also alter the button that opens the menu.
+ */
+sealed class MenuIconEffect : MenuEffect()
+
+/**
+ * Displays a notification dot.
+ * Used for highlighting new features to the user, such as what's new or a recommended feature.
+ *
+ * @property notificationTint Tint for the notification dot displayed on the icon and menu button.
+ */
+data class LowPriorityHighlightEffect(
+ @ColorInt val notificationTint: Int,
+) : MenuIconEffect()
+
+/**
+ * Changes the background of the menu item.
+ * Used for errors that require user attention, like sync errors.
+ *
+ * @property backgroundTint Tint for the menu item background color.
+ * Also used to highlight the menu button.
+ */
+data class HighPriorityHighlightEffect(
+ @ColorInt val backgroundTint: Int,
+) : MenuCandidateEffect()
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuIcon.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuIcon.kt
new file mode 100644
index 0000000000..84a8135012
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuIcon.kt
@@ -0,0 +1,97 @@
+/* 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.concept.menu.candidate
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.appcompat.content.res.AppCompatResources
+
+/**
+ * Menu option data classes to be shown alongside menu options
+ */
+sealed class MenuIcon
+
+/**
+ * Menu icon that displays a drawable.
+ *
+ * @property drawable Drawable icon to display.
+ * @property tint Tint to apply to the drawable.
+ * @property effect Effects to apply to the icon.
+ */
+data class DrawableMenuIcon(
+ override val drawable: Drawable?,
+ @ColorInt override val tint: Int? = null,
+ val effect: MenuIconEffect? = null,
+) : MenuIcon(), MenuIconWithDrawable {
+
+ constructor(
+ context: Context,
+ @DrawableRes resource: Int,
+ @ColorInt tint: Int? = null,
+ effect: MenuIconEffect? = null,
+ ) : this(AppCompatResources.getDrawable(context, resource), tint, effect)
+}
+
+/**
+ * Menu icon that displays an image button.
+ *
+ * @property drawable Drawable icon to display.
+ * @property tint Tint to apply to the drawable.
+ * @property onClick Click listener called when this menu option is clicked.
+ */
+data class DrawableButtonMenuIcon(
+ override val drawable: Drawable?,
+ @ColorInt override val tint: Int? = null,
+ val onClick: () -> Unit = {},
+) : MenuIcon(), MenuIconWithDrawable {
+
+ constructor(
+ context: Context,
+ @DrawableRes resource: Int,
+ @ColorInt tint: Int? = null,
+ onClick: () -> Unit = {},
+ ) : this(AppCompatResources.getDrawable(context, resource), tint, onClick)
+}
+
+/**
+ * Menu icon that displays a drawable.
+ *
+ * @property loadDrawable Function that creates drawable icon to display.
+ * @property loadingDrawable Drawable that is displayed while loadDrawable is running.
+ * @property fallbackDrawable Drawable that is displayed if loadDrawable fails.
+ * @property tint Tint to apply to the drawable.
+ * @property effect Effects to apply to the icon.
+ */
+data class AsyncDrawableMenuIcon(
+ val loadDrawable: suspend (width: Int, height: Int) -> Drawable?,
+ val loadingDrawable: Drawable? = null,
+ val fallbackDrawable: Drawable? = null,
+ @ColorInt val tint: Int? = null,
+ val effect: MenuIconEffect? = null,
+) : MenuIcon()
+
+/**
+ * Menu icon to display additional text at the end of a menu option.
+ *
+ * @property text Text to display.
+ * @property backgroundTint Color to show behind text.
+ * @property textStyle Styling to apply to the text.
+ */
+data class TextMenuIcon(
+ val text: String,
+ @ColorInt val backgroundTint: Int? = null,
+ val textStyle: TextStyle = TextStyle(),
+) : MenuIcon()
+
+/**
+ * Interface shared by all [MenuIcon]s with drawables.
+ */
+interface MenuIconWithDrawable {
+ val drawable: Drawable?
+
+ @get:ColorInt val tint: Int?
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/SmallMenuCandidate.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/SmallMenuCandidate.kt
new file mode 100644
index 0000000000..51e6fd68cc
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/SmallMenuCandidate.kt
@@ -0,0 +1,22 @@
+/* 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.concept.menu.candidate
+
+/**
+ * Small icon button menu option. Can only be used with [RowMenuCandidate].
+ *
+ * @property contentDescription Description of the icon.
+ * @property icon Icon to display.
+ * @property containerStyle Styling to apply to the container.
+ * @property onLongClick Listener called when this menu option is long clicked.
+ * @property onClick Click listener called when this menu option is clicked.
+ */
+data class SmallMenuCandidate(
+ val contentDescription: String,
+ val icon: DrawableMenuIcon,
+ val containerStyle: ContainerStyle = ContainerStyle(),
+ val onLongClick: (() -> Boolean)? = null,
+ val onClick: () -> Unit = {},
+)
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/TextStyle.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/TextStyle.kt
new file mode 100644
index 0000000000..eb85581f53
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/TextStyle.kt
@@ -0,0 +1,46 @@
+/* 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.concept.menu.candidate
+
+import android.graphics.Typeface
+import android.view.View
+import androidx.annotation.ColorInt
+import androidx.annotation.Dimension
+import androidx.annotation.IntDef
+
+/**
+ * Describes styling for text inside a menu option.
+ *
+ * @param size: The size of the text.
+ * @param color: The color to apply to the text.
+ */
+data class TextStyle(
+ @Dimension(unit = Dimension.PX) val size: Float? = null,
+ @ColorInt val color: Int? = null,
+ @TypefaceStyle val textStyle: Int = Typeface.NORMAL,
+ @TextAlignment val textAlignment: Int = View.TEXT_ALIGNMENT_INHERIT,
+)
+
+/**
+ * Enum for [Typeface] values.
+ */
+@IntDef(value = [Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC])
+annotation class TypefaceStyle
+
+/**
+ * Enum for text alignment values.
+ */
+@IntDef(
+ value = [
+ View.TEXT_ALIGNMENT_GRAVITY,
+ View.TEXT_ALIGNMENT_INHERIT,
+ View.TEXT_ALIGNMENT_CENTER,
+ View.TEXT_ALIGNMENT_TEXT_START,
+ View.TEXT_ALIGNMENT_TEXT_END,
+ View.TEXT_ALIGNMENT_VIEW_START,
+ View.TEXT_ALIGNMENT_VIEW_END,
+ ],
+)
+annotation class TextAlignment
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/ext/MenuCandidate.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/ext/MenuCandidate.kt
new file mode 100644
index 0000000000..b10ebf4ee1
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/ext/MenuCandidate.kt
@@ -0,0 +1,63 @@
+/* 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.concept.menu.ext
+
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.concept.menu.candidate.MenuIcon
+import mozilla.components.concept.menu.candidate.MenuIconEffect
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+private fun MenuIcon?.effect(): MenuIconEffect? =
+ if (this is DrawableMenuIcon) effect else null
+
+/**
+ * Find the effects used by the menu.
+ * Disabled and invisible menu items are not included.
+ */
+fun List<MenuCandidate>.effects(): Sequence<MenuEffect> = this.asSequence()
+ .filter { option -> option.containerStyle.isVisible && option.containerStyle.isEnabled }
+ .flatMap { option ->
+ when (option) {
+ is TextMenuCandidate ->
+ sequenceOf(option.effect, option.start.effect(), option.end.effect()).filterNotNull()
+ is CompoundMenuCandidate ->
+ sequenceOf(option.effect, option.start.effect()).filterNotNull()
+ is NestedMenuCandidate ->
+ sequenceOf(option.effect, option.start.effect(), option.end.effect()).filterNotNull() +
+ option.subMenuItems?.effects().orEmpty()
+ is RowMenuCandidate ->
+ option.items.asSequence()
+ .filter { it.containerStyle.isVisible && it.containerStyle.isEnabled }
+ .mapNotNull { it.icon.effect }
+ is DecorativeTextMenuCandidate, is DividerMenuCandidate -> emptySequence()
+ }
+ }
+
+/**
+ * Find a [NestedMenuCandidate] in the list with a matching [id].
+ */
+fun List<MenuCandidate>.findNestedMenuCandidate(id: Int): NestedMenuCandidate? = this.asSequence()
+ .mapNotNull { it as? NestedMenuCandidate }
+ .find { it.id == id }
+
+/**
+ * Select the highlight with the highest priority.
+ */
+fun Sequence<MenuEffect>.max() = maxByOrNull {
+ // Select the highlight with the highest priority
+ when (it) {
+ is HighPriorityHighlightEffect -> 2
+ is LowPriorityHighlightEffect -> 1
+ }
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/test/java/mozilla/components/concept/menu/ext/MenuCandidateTest.kt b/mobile/android/android-components/components/concept/menu/src/test/java/mozilla/components/concept/menu/ext/MenuCandidateTest.kt
new file mode 100644
index 0000000000..5326143dd4
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/test/java/mozilla/components/concept/menu/ext/MenuCandidateTest.kt
@@ -0,0 +1,179 @@
+/* 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.concept.menu.ext
+
+import android.graphics.Color
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class MenuCandidateTest {
+
+ @Test
+ fun `higher priority items will be selected by max`() {
+ assertEquals(
+ HighPriorityHighlightEffect(Color.BLACK),
+ sequenceOf(
+ LowPriorityHighlightEffect(Color.BLUE),
+ HighPriorityHighlightEffect(Color.BLACK),
+ ).max(),
+ )
+ }
+
+ @Test
+ fun `items earlier in sequence will be selected by max`() {
+ assertEquals(
+ LowPriorityHighlightEffect(Color.BLUE),
+ sequenceOf(
+ LowPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.YELLOW),
+ ).max(),
+ )
+ }
+
+ @Test
+ fun `effects returns effects from row candidate`() {
+ assertEquals(
+ listOf(
+ LowPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.YELLOW),
+ ),
+ listOf(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate(
+ "",
+ icon = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.BLUE),
+ ),
+ ),
+ SmallMenuCandidate(
+ "",
+ icon = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.RED),
+ ),
+ containerStyle = ContainerStyle(isVisible = false),
+ ),
+ SmallMenuCandidate(
+ "",
+ icon = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.RED),
+ ),
+ containerStyle = ContainerStyle(isEnabled = false),
+ ),
+ SmallMenuCandidate(
+ "",
+ icon = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.YELLOW),
+ ),
+ ),
+ ),
+ ),
+ ).effects().toList(),
+ )
+ }
+
+ @Test
+ fun `effects returns effects from text candidates`() {
+ assertEquals(
+ listOf(
+ HighPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.YELLOW),
+ HighPriorityHighlightEffect(Color.BLACK),
+ HighPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.RED),
+ ),
+ listOf(
+ TextMenuCandidate(
+ "",
+ start = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.YELLOW),
+ ),
+ effect = HighPriorityHighlightEffect(Color.BLUE),
+ ),
+ DecorativeTextMenuCandidate(""),
+ TextMenuCandidate(""),
+ DividerMenuCandidate(),
+ TextMenuCandidate(
+ "",
+ effect = HighPriorityHighlightEffect(Color.BLACK),
+ ),
+ TextMenuCandidate(
+ "",
+ containerStyle = ContainerStyle(isVisible = false),
+ effect = HighPriorityHighlightEffect(Color.BLACK),
+ ),
+ TextMenuCandidate(
+ "",
+ end = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.RED),
+ ),
+ effect = HighPriorityHighlightEffect(Color.BLUE),
+ ),
+ ).effects().toList(),
+ )
+ }
+
+ @Test
+ fun `effects returns effects from compound candidates`() {
+ assertEquals(
+ listOf(
+ HighPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.YELLOW),
+ HighPriorityHighlightEffect(Color.BLACK),
+ LowPriorityHighlightEffect(Color.RED),
+ ),
+ listOf(
+ CompoundMenuCandidate(
+ "",
+ isChecked = true,
+ start = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.YELLOW),
+ ),
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ effect = HighPriorityHighlightEffect(Color.BLUE),
+ ),
+ CompoundMenuCandidate(
+ "",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.SWITCH,
+ effect = HighPriorityHighlightEffect(Color.BLACK),
+ ),
+ CompoundMenuCandidate(
+ "",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.SWITCH,
+ containerStyle = ContainerStyle(isEnabled = false),
+ effect = HighPriorityHighlightEffect(Color.BLACK),
+ ),
+ CompoundMenuCandidate(
+ "",
+ isChecked = true,
+ start = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.RED),
+ ),
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ ),
+ ).effects().toList(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/concept/push/README.md b/mobile/android/android-components/components/concept/push/README.md
new file mode 100644
index 0000000000..dd596fdc78
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/README.md
@@ -0,0 +1,23 @@
+# [Android Components](../../../README.md) > Concept > Push
+
+An abstract definition of a push service component.
+
+## Usage
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md)):
+
+```Groovy
+implementation "org.mozilla.components:concept-push:{latest-version}"
+```
+
+### Implementing a Push service.
+
+TBD
+
+## 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/concept/push/build.gradle b/mobile/android/android-components/components/concept/push/build.gradle
new file mode 100644
index 0000000000..a1999b52c9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/build.gradle
@@ -0,0 +1,38 @@
+/* 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/. */
+
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.push'
+}
+
+dependencies {
+ implementation project(':support-base')
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.testing_junit
+}
+
+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/concept/push/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/push/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/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/concept/push/src/main/java/mozilla/components/concept/push/PushProcessor.kt b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushProcessor.kt
new file mode 100644
index 0000000000..909ee23737
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushProcessor.kt
@@ -0,0 +1,87 @@
+/* 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.concept.push
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * A push notification processor that handles registration and new messages from the [PushService] provided.
+ * Starting Push in the Application's onCreate is recommended.
+ */
+interface PushProcessor {
+
+ /**
+ * Start the push processor and any service associated.
+ */
+ fun initialize()
+
+ /**
+ * Removes all push subscriptions from the device.
+ */
+ fun shutdown()
+
+ /**
+ * A new registration token has been received.
+ */
+ fun onNewToken(newToken: String)
+
+ /**
+ * A new push message has been received.
+ * The message contains the payload as sent by the
+ * Autopush server, and it will be read at a lower
+ * abstraction layer.
+ */
+ fun onMessageReceived(message: Map<String, String>)
+
+ /**
+ * An error has occurred.
+ */
+ fun onError(error: PushError)
+
+ /**
+ * Requests the [PushService] to renew it's registration with it's provider.
+ */
+ fun renewRegistration()
+
+ companion object {
+ /**
+ * Initialize and installs the PushProcessor into the application.
+ * This needs to be called in the application's onCreate before a push service has started.
+ */
+ fun install(processor: PushProcessor) {
+ instance = processor
+ }
+
+ @Volatile
+ private var instance: PushProcessor? = null
+
+ @VisibleForTesting
+ internal fun reset() {
+ instance = null
+ }
+ val requireInstance: PushProcessor
+ get() = instance ?: throw IllegalStateException(
+ "You need to call PushProcessor.install() on your Push instance from Application.onCreate().",
+ )
+ }
+}
+
+/**
+ * Various error types.
+ */
+sealed class PushError(override val message: String) : Exception() {
+ data class Registration(override val message: String) : PushError(message)
+ data class Network(override val message: String) : PushError(message)
+
+ /**
+ * @property cause Original exception from Rust code.
+ */
+ data class Rust(
+ override val cause: Throwable?,
+ override val message: String = cause?.message.orEmpty(),
+ ) : PushError(message)
+ data class MalformedMessage(override val message: String) : PushError(message)
+ data class ServiceUnavailable(override val message: String) : PushError(message)
+}
diff --git a/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushService.kt b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushService.kt
new file mode 100644
index 0000000000..4308fb2d1e
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushService.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.concept.push
+
+import android.content.Context
+
+/**
+ * Implemented by push services like Firebase Cloud Messaging SDKs to allow
+ * the [PushProcessor] to manage their lifecycle.
+ */
+interface PushService {
+
+ /**
+ * Starts the push service.
+ */
+ fun start(context: Context)
+
+ /**
+ * Stops the push service.
+ */
+ fun stop()
+
+ /**
+ * Tells the push service to delete the registration token.
+ */
+ fun deleteToken()
+
+ /**
+ * If the push service is support on the device.
+ */
+ fun isServiceAvailable(context: Context): Boolean
+
+ companion object {
+ /**
+ * Message key for "channel ID" in a push message.
+ */
+ const val MESSAGE_KEY_CHANNEL_ID = "chid"
+ }
+}
diff --git a/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/exceptions/SubscriptionException.kt b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/exceptions/SubscriptionException.kt
new file mode 100644
index 0000000000..de60f5b071
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/exceptions/SubscriptionException.kt
@@ -0,0 +1,17 @@
+/* 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/. */
+
+@file:Suppress("MatchingDeclarationName")
+
+package mozilla.components.concept.push.exceptions
+
+/**
+ * Signals that a subscription method has been invoked at an illegal or inappropriate time.
+ *
+ * See also [Exception].
+ */
+class SubscriptionException(
+ override val message: String? = null,
+ override val cause: Throwable? = null,
+) : Exception()
diff --git a/mobile/android/android-components/components/concept/push/src/test/java/mozilla/components/concept/push/PushErrorTest.kt b/mobile/android/android-components/components/concept/push/src/test/java/mozilla/components/concept/push/PushErrorTest.kt
new file mode 100644
index 0000000000..13e2013597
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/src/test/java/mozilla/components/concept/push/PushErrorTest.kt
@@ -0,0 +1,32 @@
+/* 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.concept.push
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class PushErrorTest {
+ @Test
+ fun `all PushError sets description`() {
+ // This test is mostly to satisfy coverage.
+
+ var error: PushError = PushError.MalformedMessage("message")
+ assertEquals("message", error.message)
+
+ error = PushError.Network("network")
+ assertEquals("network", error.message)
+
+ error = PushError.Registration("reg")
+ assertEquals("reg", error.message)
+
+ val exception = IllegalStateException()
+ val rustError = PushError.Rust(exception, "rust")
+ assertEquals("rust", rustError.message)
+ assertEquals(exception, rustError.cause)
+
+ error = PushError.ServiceUnavailable("service")
+ assertEquals("service", error.message)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/push/src/test/java/mozilla/components/concept/push/PushProcessorTest.kt b/mobile/android/android-components/components/concept/push/src/test/java/mozilla/components/concept/push/PushProcessorTest.kt
new file mode 100644
index 0000000000..f8fad03aed
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/src/test/java/mozilla/components/concept/push/PushProcessorTest.kt
@@ -0,0 +1,55 @@
+/* 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.concept.push
+
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+
+class PushProcessorTest {
+
+ @Before
+ fun setup() {
+ PushProcessor.reset()
+ }
+
+ @Test
+ fun install() {
+ val processor: PushProcessor = mock()
+
+ PushProcessor.install(processor)
+
+ assertNotNull(PushProcessor.requireInstance)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `requireInstance throws if install not called first`() {
+ PushProcessor.requireInstance
+ }
+
+ @Test
+ fun init() {
+ val push = TestPushProcessor()
+
+ PushProcessor.install(push)
+
+ assertNotNull(PushProcessor.requireInstance)
+ }
+
+ class TestPushProcessor : PushProcessor {
+ override fun initialize() {}
+
+ override fun shutdown() {}
+
+ override fun onNewToken(newToken: String) {}
+
+ override fun onMessageReceived(message: Map<String, String>) {}
+
+ override fun onError(error: PushError) {}
+
+ override fun renewRegistration() {}
+ }
+}
diff --git a/mobile/android/android-components/components/concept/push/src/test/resources/robolectric.properties b/mobile/android/android-components/components/concept/push/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/concept/push/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/concept/storage/README.md b/mobile/android/android-components/components/concept/storage/README.md
new file mode 100644
index 0000000000..5afcfb9e29
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/README.md
@@ -0,0 +1,28 @@
+# [Android Components](../../../README.md) > Concept > Storage
+
+The `concept-storage` component contains interfaces and abstract classes that describe a "core data" storage layer.
+
+This abstraction makes it possible to build components that work independently of the storage layer being used.
+
+Currently a single store implementation is available:
+- [syncable, Rust Places storage](../../browser/storage-sync) - compatible with the Firefox Sync ecosystem
+
+## Usage
+
+### 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:concept-storage:{latest-version}"
+```
+
+### Integration
+
+One way to interact with a `concept-storage` component is via [feature-storage](../../features/storage/README.md), which provides "glue" implementations that make use of storage. For example, a `features.storage.HistoryTrackingFeature` allows a `concept.engine.Engine` to keep track of visits and page meta information.
+
+## 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/concept/storage/build.gradle b/mobile/android/android-components/components/concept/storage/build.gradle
new file mode 100644
index 0000000000..b12cce53ae
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/build.gradle
@@ -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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.storage'
+}
+
+dependencies {
+ // Necessary because we use 'suspend'. Fun fact: this module will compile just fine without this
+ // dependency, but it will crash at runtime.
+ // Included via 'api' because this module is unusable without coroutines.
+ api ComponentsDependencies.kotlin_coroutines
+
+ implementation project(':support-ktx')
+ implementation ComponentsDependencies.androidx_annotation
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.testing_junit
+}
+
+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/concept/storage/proguard-rules.pro b/mobile/android/android-components/components/concept/storage/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/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/concept/storage/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/storage/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/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/concept/storage/src/main/java/mozilla/components/concept/storage/BookmarksStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/BookmarksStorage.kt
new file mode 100644
index 0000000000..85050cea94
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/BookmarksStorage.kt
@@ -0,0 +1,182 @@
+/* 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.concept.storage
+
+/**
+ * An interface which defines read/write operations for bookmarks data.
+ */
+interface BookmarksStorage : Storage {
+
+ /**
+ * Produces a bookmarks tree for the given guid string.
+ *
+ * @param guid The bookmark guid to obtain.
+ * @param recursive Whether to recurse and obtain all levels of children.
+ * @return The populated root starting from the guid.
+ */
+ suspend fun getTree(guid: String, recursive: Boolean = false): BookmarkNode?
+
+ /**
+ * Obtains the details of a bookmark without children, if one exists with that guid. Otherwise, null.
+ *
+ * @param guid The bookmark guid to obtain.
+ * @return The bookmark node or null if it does not exist.
+ */
+ suspend fun getBookmark(guid: String): BookmarkNode?
+
+ /**
+ * Produces a list of all bookmarks with the given URL.
+ *
+ * @param url The URL string.
+ * @return The list of bookmarks that match the URL
+ */
+ suspend fun getBookmarksWithUrl(url: String): List<BookmarkNode>
+
+ /**
+ * Produces a list of the most recently added bookmarks.
+ *
+ * @param limit The maximum number of entries to return.
+ * @param maxAge Optional parameter used to filter out entries older than this number of milliseconds.
+ * @param currentTime Optional parameter for current time. Defaults toSystem.currentTimeMillis()
+ * @return The list of bookmarks that have been recently added up to the limit number of items.
+ */
+ suspend fun getRecentBookmarks(
+ limit: Int,
+ maxAge: Long? = null,
+ currentTime: Long = System.currentTimeMillis(),
+ ): List<BookmarkNode>
+
+ /**
+ * Searches bookmarks with a query string.
+ *
+ * @param query The query string to search.
+ * @param limit The maximum number of entries to return.
+ * @return The list of matching bookmark nodes up to the limit number of items.
+ */
+ suspend fun searchBookmarks(query: String, limit: Int = defaultBookmarkSearchLimit): List<BookmarkNode>
+
+ /**
+ * Adds a new bookmark item to a given node.
+ *
+ * Sync behavior: will add new bookmark item to remote devices.
+ *
+ * @param parentGuid The parent guid of the new node.
+ * @param url The URL of the bookmark item to add.
+ * @param title The title of the bookmark item to add.
+ * @param position The optional position to add the new node or null to append.
+ * @return The guid of the newly inserted bookmark item.
+ */
+ suspend fun addItem(parentGuid: String, url: String, title: String, position: UInt?): String
+
+ /**
+ * Adds a new bookmark folder to a given node.
+ *
+ * Sync behavior: will add new separator to remote devices.
+ *
+ * @param parentGuid The parent guid of the new node.
+ * @param title The title of the bookmark folder to add.
+ * @param position The optional position to add the new node or null to append.
+ * @return The guid of the newly inserted bookmark item.
+ */
+ suspend fun addFolder(parentGuid: String, title: String, position: UInt? = null): String
+
+ /**
+ * Adds a new bookmark separator to a given node.
+ *
+ * Sync behavior: will add new separator to remote devices.
+ *
+ * @param parentGuid The parent guid of the new node.
+ * @param position The optional position to add the new node or null to append.
+ * @return The guid of the newly inserted bookmark item.
+ */
+ suspend fun addSeparator(parentGuid: String, position: UInt?): String
+
+ /**
+ * Edits the properties of an existing bookmark item and/or moves an existing one underneath a new parent guid.
+ *
+ * Sync behavior: will alter bookmark item on remote devices.
+ *
+ * @param guid The guid of the item to update.
+ * @param info The info to change in the bookmark.
+ */
+ suspend fun updateNode(guid: String, info: BookmarkInfo)
+
+ /**
+ * Deletes a bookmark node and all of its children, if any.
+ *
+ * Sync behavior: will remove bookmark from remote devices.
+ *
+ * @return Whether the bookmark existed or not.
+ */
+ suspend fun deleteNode(guid: String): Boolean
+
+ /**
+ * Counts the number of bookmarks in the trees under the specified GUIDs.
+
+ * @param guids The guids of folders to query.
+ * @return Count of all bookmark items (ie, no folders or separators) in all specified folders
+ * recursively. Empty folders, non-existing GUIDs and non-existing items will return zero.
+ * The result is implementation dependant if the trees overlap.
+ */
+ suspend fun countBookmarksInTrees(guids: List<String>): UInt
+
+ companion object {
+ const val defaultBookmarkSearchLimit = 10
+ }
+}
+
+/**
+ * Represents a bookmark record.
+ *
+ * @property type The [BookmarkNodeType] of this record.
+ * @property guid The id.
+ * @property parentGuid The id of the parent node in the tree.
+ * @property position The position of this node in the tree.
+ * @property title A title of the page.
+ * @property url The url of the page.
+ * @property dateAdded Creation time, in milliseconds since the unix epoch.
+ * @property children The list of children of this bookmark node in the tree.
+ */
+data class BookmarkNode(
+ val type: BookmarkNodeType,
+ val guid: String,
+ val parentGuid: String?,
+ val position: UInt?,
+ val title: String?,
+ val url: String?,
+ val dateAdded: Long,
+ val children: List<BookmarkNode>?,
+) {
+ /**
+ * Removes [children] from [BookmarkNode.children] and returns the new modified [BookmarkNode].
+ *
+ * DOES NOT delete the bookmarks from storage, so this should only be used where you are
+ * batching deletes, or where the deletes are otherwise pending.
+ *
+ * In the general case you should try and avoid using this - just delete the items from
+ * storage then re-fetch the parent node.
+ */
+ operator fun minus(children: Set<BookmarkNode>): BookmarkNode {
+ val removedChildrenGuids = children.map { it.guid }
+ return this.copy(children = this.children?.filterNot { removedChildrenGuids.contains(it.guid) })
+ }
+}
+
+/**
+ * Class for making alterations to any bookmark node
+ */
+data class BookmarkInfo(
+ val parentGuid: String?,
+ val position: UInt?,
+ val title: String?,
+ val url: String?,
+)
+
+/**
+ * The types of bookmark nodes
+ */
+enum class BookmarkNodeType {
+ ITEM, FOLDER, SEPARATOR
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/Cancellable.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/Cancellable.kt
new file mode 100644
index 0000000000..ade91321c9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/Cancellable.kt
@@ -0,0 +1,42 @@
+/* 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.concept.storage
+
+/**
+ * Storage that allows to stop and clean in progress operations.
+ */
+interface Cancellable {
+ /**
+ * Cleans up all background work and operations queue.
+ */
+ fun cleanup() {
+ // no-op
+ }
+
+ /**
+ * Cleans up all pending write operations.
+ */
+ fun cancelWrites() {
+ // no-op
+ }
+
+ /**
+ * Cleans up all pending read operations.
+ */
+ fun cancelReads() {
+ // no-op
+ }
+
+ /**
+ * Cleans up pending read operations in preparation for a new query.
+ * This is useful when the same storage is shared between multiple functionalities and will
+ * allow preventing overlapped cancel requests.
+ *
+ * @param nextQuery Next query to cancel reads for.
+ */
+ fun cancelReads(nextQuery: String) {
+ // no-op
+ }
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt
new file mode 100644
index 0000000000..f619a9d1e9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt
@@ -0,0 +1,503 @@
+/* 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.concept.storage
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import androidx.annotation.VisibleForTesting
+import kotlinx.parcelize.Parcelize
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsesEnd
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsesStart
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsis
+import mozilla.components.support.ktx.kotlin.last4Digits
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+
+/**
+ * An interface which defines read/write methods for credit card and address data.
+ */
+interface CreditCardsAddressesStorage {
+
+ /**
+ * Inserts the provided credit card into the database, and returns
+ * the newly added [CreditCard].
+ *
+ * @param creditCardFields A [NewCreditCardFields] record to add.
+ * @return [CreditCard] for the added credit card.
+ */
+ suspend fun addCreditCard(creditCardFields: NewCreditCardFields): CreditCard
+
+ /**
+ * Updates the fields in the provided credit card.
+ *
+ * @param guid Unique identifier for the desired credit card.
+ * @param creditCardFields A set of credit card fields, wrapped in [UpdatableCreditCardFields], to update.
+ */
+ suspend fun updateCreditCard(guid: String, creditCardFields: UpdatableCreditCardFields)
+
+ /**
+ * Retrieves the credit card from the underlying storage layer by its unique identifier.
+ *
+ * @param guid Unique identifier for the desired credit card.
+ * @return [CreditCard] record if it exists or null otherwise.
+ */
+ suspend fun getCreditCard(guid: String): CreditCard?
+
+ /**
+ * Retrieves a list of all the credit cards.
+ *
+ * @return A list of all [CreditCard].
+ */
+ suspend fun getAllCreditCards(): List<CreditCard>
+
+ /**
+ * Deletes the credit card with the given [guid].
+ *
+ * @param guid Unique identifier for the desired credit card.
+ * @return True if the deletion did anything, false otherwise.
+ */
+ suspend fun deleteCreditCard(guid: String): Boolean
+
+ /**
+ * Marks the credit card with the given [guid] as `in-use`.
+ *
+ * @param guid Unique identifier for the desired credit card.
+ */
+ suspend fun touchCreditCard(guid: String)
+
+ /**
+ * Inserts the provided address into the database, and returns
+ * the newly added [Address].
+ *
+ * @param addressFields A [UpdatableAddressFields] record to add.
+ * @return [Address] for the added address.
+ */
+ suspend fun addAddress(addressFields: UpdatableAddressFields): Address
+
+ /**
+ * Retrieves the address from the underlying storage layer by its unique identifier.
+ *
+ * @param guid Unique identifier for the desired address.
+ * @return [Address] record if it exists or null otherwise.
+ */
+ suspend fun getAddress(guid: String): Address?
+
+ /**
+ * Retrieves a list of all the addresses.
+ *
+ * @return A list of all [Address].
+ */
+ suspend fun getAllAddresses(): List<Address>
+
+ /**
+ * Updates the fields in the provided address.
+ *
+ * @param guid Unique identifier for the desired address.
+ * @param address The address fields to update.
+ */
+ suspend fun updateAddress(guid: String, address: UpdatableAddressFields)
+
+ /**
+ * Delete the address with the given [guid].
+ *
+ * @return True if the deletion did anything, false otherwise.
+ */
+ suspend fun deleteAddress(guid: String): Boolean
+
+ /**
+ * Marks the address with the given [guid] as `in-use`.
+ *
+ * @param guid Unique identifier for the desired address.
+ */
+ suspend fun touchAddress(guid: String)
+
+ /**
+ * Returns an instance of [CreditCardCrypto] that knows how to encrypt and decrypt credit card
+ * numbers.
+ *
+ * @return [CreditCardCrypto] instance.
+ */
+ fun getCreditCardCrypto(): CreditCardCrypto
+
+ /**
+ * Removes any encrypted data from this storage. Useful after encountering key loss.
+ */
+ suspend fun scrubEncryptedData()
+}
+
+/**
+ * An interface that defines methods for encrypting and decrypting a credit card number.
+ */
+interface CreditCardCrypto : KeyProvider {
+
+ /**
+ * Encrypt a [CreditCardNumber.Plaintext] using the provided key. A `null` result means a
+ * bad key was provided. In that case caller should obtain a new key and try again.
+ *
+ * @param key The encryption key to encrypt the plaintext credit card number.
+ * @param plaintextCardNumber A plaintext credit card number to be encrypted.
+ * @return An encrypted credit card number or `null` if a bad [key] was provided.
+ */
+ fun encrypt(
+ key: ManagedKey,
+ plaintextCardNumber: CreditCardNumber.Plaintext,
+ ): CreditCardNumber.Encrypted?
+
+ /**
+ * Decrypt a [CreditCardNumber.Encrypted] using the provided key. A `null` result means a
+ * bad key was provided. In that case caller should obtain a new key and try again.
+ *
+ * @param key The encryption key to decrypt the decrypt credit card number.
+ * @param encryptedCardNumber An encrypted credit card number to be decrypted.
+ * @return A plaintext, non-encrypted credit card number or `null` if a bad [key] was provided.
+ */
+ fun decrypt(
+ key: ManagedKey,
+ encryptedCardNumber: CreditCardNumber.Encrypted,
+ ): CreditCardNumber.Plaintext?
+}
+
+/**
+ * A credit card number. This structure exists to provide better typing at the API surface.
+ *
+ * @property number Either a plaintext or a ciphertext of the credit card number, depending on the subtype.
+ */
+sealed class CreditCardNumber(val number: String) {
+ /**
+ * An encrypted credit card number.
+ */
+ @SuppressLint("ParcelCreator")
+ @Parcelize
+ data class Encrypted(private val data: String) : CreditCardNumber(data), Parcelable
+
+ /**
+ * A plaintext, non-encrypted credit card number.
+ */
+ data class Plaintext(private val data: String) : CreditCardNumber(data)
+}
+
+/**
+ * Information about a credit card.
+ *
+ * @property guid The unique identifier for this credit card.
+ * @property billingName The credit card billing name.
+ * @property encryptedCardNumber The encrypted credit card number.
+ * @property cardNumberLast4 The last 4 digits of the credit card number.
+ * @property expiryMonth The credit card expiry month.
+ * @property expiryYear The credit card expiry year.
+ * @property cardType The credit card network ID.
+ * @property timeCreated Time of creation in milliseconds from the unix epoch.
+ * @property timeLastUsed Time of last use in milliseconds from the unix epoch.
+ * @property timeLastModified Time of last modified in milliseconds from the unix epoch.
+ * @property timesUsed Number of times the credit card was used.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class CreditCard(
+ val guid: String,
+ val billingName: String,
+ val encryptedCardNumber: CreditCardNumber.Encrypted,
+ val cardNumberLast4: String,
+ val expiryMonth: Long,
+ val expiryYear: Long,
+ val cardType: String,
+ val timeCreated: Long = 0L,
+ val timeLastUsed: Long? = 0L,
+ val timeLastModified: Long = 0L,
+ val timesUsed: Long = 0L,
+) : Parcelable {
+ val obfuscatedCardNumber: String
+ get() = ellipsesStart +
+ ellipsis + ellipsis + ellipsis + ellipsis +
+ cardNumberLast4 +
+ ellipsesEnd
+
+ companion object {
+ // Left-To-Right Embedding (LTE) mark
+ const val ellipsesStart = "\u202A"
+
+ // One dot ellipsis
+ const val ellipsis = "\u2022\u2060\u2006\u2060"
+
+ // Pop Directional Formatting (PDF) mark
+ const val ellipsesEnd = "\u202C"
+ }
+}
+
+/**
+ * Credit card autofill entry.
+ *
+ * This contains the data needed to handle autofill but not the data related to the DB record.
+ *
+ * @property guid The unique identifier for this credit card.
+ * @property name The credit card billing name.
+ * @property number The credit card number.
+ * @property expiryMonth The credit card expiry month.
+ * @property expiryYear The credit card expiry year.
+ * @property cardType The credit card network ID.
+ */
+@Parcelize
+data class CreditCardEntry(
+ val guid: String? = null,
+ val name: String,
+ val number: String,
+ val expiryMonth: String,
+ val expiryYear: String,
+ val cardType: String,
+) : Parcelable {
+ val obfuscatedCardNumber: String
+ get() = ellipsesStart +
+ ellipsis + ellipsis + ellipsis + ellipsis +
+ number.last4Digits() +
+ ellipsesEnd
+
+ /**
+ * Credit card expiry date formatted according to the locale. Returns an empty string if either
+ * the expiration month or expiration year is not set.
+ */
+ val expiryDate: String
+ get() {
+ return if (expiryMonth.isEmpty() || expiryYear.isEmpty()) {
+ ""
+ } else {
+ val dateFormat = SimpleDateFormat(DATE_PATTERN, Locale.getDefault())
+
+ val calendar = Calendar.getInstance()
+ calendar.set(Calendar.DAY_OF_MONTH, 1)
+ // Subtract 1 from the expiry month since Calendar.Month is based on a 0-indexed.
+ calendar.set(Calendar.MONTH, expiryMonth.toInt() - 1)
+ calendar.set(Calendar.YEAR, expiryYear.toInt())
+
+ dateFormat.format(calendar.time)
+ }
+ }
+
+ /**
+ * Whether this entry contains all data needed to be considered well-formed.
+ */
+ val isValid: Boolean
+ get() = number.isNotEmpty() && expiryDate.isNotEmpty()
+
+ companion object {
+ // Date format pattern for the credit card expiry date.
+ private const val DATE_PATTERN = "MM/yyyy"
+ }
+}
+
+/**
+ * Information about a new credit card.
+ * Use this when creating a credit card via [CreditCardsAddressesStorage.addCreditCard].
+ *
+ * @property billingName The credit card billing name.
+ * @property plaintextCardNumber A plaintext credit card number.
+ * @property cardNumberLast4 The last 4 digits of the credit card number.
+ * @property expiryMonth The credit card expiry month.
+ * @property expiryYear The credit card expiry year.
+ * @property cardType The credit card network ID.
+ */
+data class NewCreditCardFields(
+ val billingName: String,
+ val plaintextCardNumber: CreditCardNumber.Plaintext,
+ val cardNumberLast4: String,
+ val expiryMonth: Long,
+ val expiryYear: Long,
+ val cardType: String,
+)
+
+/**
+ * Information about a new credit card.
+ * Use this when creating a credit card via [CreditCardsAddressesStorage.updateAddress].
+ *
+ * @property billingName The credit card billing name.
+ * @property cardNumber A [CreditCardNumber] that is either encrypted or plaintext. Passing in plaintext
+ * version will update the stored credit card number.
+ * @property cardNumberLast4 The last 4 digits of the credit card number.
+ * @property expiryMonth The credit card expiry month.
+ * @property expiryYear The credit card expiry year.
+ * @property cardType The credit card network ID.
+ */
+data class UpdatableCreditCardFields(
+ val billingName: String,
+ val cardNumber: CreditCardNumber,
+ val cardNumberLast4: String,
+ val expiryMonth: Long,
+ val expiryYear: Long,
+ val cardType: String,
+)
+
+/**
+ * Information about a address.
+ *
+ * @property guid The unique identifier for this address.
+ * @property name A person's full name, typically made up of a first, middle and last name, e.g. John Joe Doe.
+ * @property organization Organization.
+ * @property streetAddress Street address.
+ * @property addressLevel3 Sublocality (Suburb) name type.
+ * @property addressLevel2 Locality (City/Town) name type.
+ * @property addressLevel1 Province/State name type.
+ * @property postalCode Postal code.
+ * @property country Country.
+ * @property tel Telephone number.
+ * @property email E-mail address.
+ * @property timeCreated Time of creation in milliseconds from the unix epoch.
+ * @property timeLastUsed Time of last use in milliseconds from the unix epoch.
+ * @property timeLastModified Time of last modified in milliseconds from the unix epoch.
+ * @property timesUsed Number of times the address was used.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class Address(
+ val guid: String,
+ val name: String,
+ val organization: String,
+ val streetAddress: String,
+ val addressLevel3: String,
+ val addressLevel2: String,
+ val addressLevel1: String,
+ val postalCode: String,
+ val country: String,
+ val tel: String,
+ val email: String,
+ val timeCreated: Long = 0L,
+ val timeLastUsed: Long? = 0L,
+ val timeLastModified: Long = 0L,
+ val timesUsed: Long = 0L,
+) : Parcelable {
+
+ /**
+ * Returns a label for the [Address]. The ordering is based on the
+ * priorities defined by the desktop code found here:
+ * https://searchfox.org/mozilla-central/rev/d989c65584ded72c2de85cb40bede7ac2f176387/toolkit/components/formautofill/FormAutofillUtils.jsm#323
+ */
+ val addressLabel: String
+ get() = listOf(
+ streetAddress.toOneLineAddress(),
+ addressLevel3,
+ addressLevel2,
+ organization,
+ addressLevel1,
+ country,
+ postalCode,
+ tel,
+ email,
+ ).filter { it.isNotEmpty() }.joinToString(", ")
+
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun String.toOneLineAddress(): String =
+ this.split("\n").joinToString(separator = " ") { it.trim() }
+ }
+}
+
+/**
+ * Information about a new address. This is what you pass to create or update an address.
+ *
+ * @property name A person's full name, typically made up of a first, middle and last name, e.g. John Joe Doe.
+ * @property organization Organization.
+ * @property streetAddress Street address.
+ * @property addressLevel3 Sublocality (Suburb) name type.
+ * @property addressLevel2 Locality (City/Town) name type.
+ * @property addressLevel1 Province/State name type.
+ * @property postalCode Postal code.
+ * @property country Country.
+ * @property tel Telephone number.
+ * @property email E-mail address.
+ */
+data class UpdatableAddressFields(
+ val name: String,
+ val organization: String,
+ val streetAddress: String,
+ val addressLevel3: String,
+ val addressLevel2: String,
+ val addressLevel1: String,
+ val postalCode: String,
+ val country: String,
+ val tel: String,
+ val email: String,
+)
+
+/**
+ * Provides a method for checking whether or not a given credit card can be stored.
+ */
+interface CreditCardValidationDelegate {
+
+ /**
+ * The result from validating a given [CreditCard] against the credit card storage. This will
+ * include whether or not it can be created or updated.
+ */
+ sealed class Result {
+ /**
+ * Indicates that the [CreditCard] does not currently exist in the storage, and a new
+ * credit card entry can be created.
+ */
+ object CanBeCreated : Result()
+
+ /**
+ * Indicates that a matching [CreditCard] was found in the storage, and the [CreditCard]
+ * can be used to update its information.
+ */
+ data class CanBeUpdated(val foundCreditCard: CreditCard) : Result()
+ }
+
+ /**
+ * Determines whether a [CreditCardEntry] can be added or updated in the credit card storage.
+ *
+ * @param creditCard [CreditCardEntry] to be added or updated in the credit card storage.
+ * @return [Result] that indicates whether or not the [CreditCardEntry] should be saved or
+ * updated.
+ */
+ suspend fun shouldCreateOrUpdate(creditCard: CreditCardEntry): Result
+}
+
+/**
+ * Used to handle [Address] and [CreditCard] storage so that the underlying engine doesn't have to.
+ * An instance of this should be attached to the Gecko runtime in order to be used.
+ */
+interface CreditCardsAddressesStorageDelegate : KeyProvider {
+
+ /**
+ * Decrypt a [CreditCardNumber.Encrypted] into its plaintext equivalent or `null` if
+ * it fails to decrypt.
+ *
+ * @param key The encryption key to decrypt the decrypt credit card number.
+ * @param encryptedCardNumber An encrypted credit card number to be decrypted.
+ * @return A plaintext, non-encrypted credit card number.
+ */
+ suspend fun decrypt(
+ key: ManagedKey,
+ encryptedCardNumber: CreditCardNumber.Encrypted,
+ ): CreditCardNumber.Plaintext?
+
+ /**
+ * Returns all stored addresses. This is called when the engine believes an address field
+ * should be autofilled.
+ *
+ * @return A list of all stored addresses.
+ */
+ suspend fun onAddressesFetch(): List<Address>
+
+ /**
+ * Saves the given address to storage.
+ *
+ * @param address [Address] to be saved or updated in the address storage.
+ */
+ suspend fun onAddressSave(address: Address)
+
+ /**
+ * Returns all stored credit cards. This is called when the engine believes a credit card
+ * field should be autofilled.
+ *
+ * @return A list of all stored credit cards.
+ */
+ suspend fun onCreditCardsFetch(): List<CreditCard>
+
+ /**
+ * Saves the given credit card to storage.
+ *
+ * @param creditCard [CreditCardEntry] to be saved or updated in the credit card storage.
+ */
+ suspend fun onCreditCardSave(creditCard: CreditCardEntry)
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/HistoryMetadataStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/HistoryMetadataStorage.kt
new file mode 100644
index 0000000000..56d9315f29
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/HistoryMetadataStorage.kt
@@ -0,0 +1,193 @@
+/* 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.concept.storage
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * The possible document types to record history metadata for.
+ */
+enum class DocumentType {
+ Regular,
+ Media,
+}
+
+/**
+ * Represents the different types of history metadata observations.
+ */
+sealed class HistoryMetadataObservation {
+ /**
+ * A [HistoryMetadataObservation] to increment the total view time.
+ */
+ data class ViewTimeObservation(
+ val viewTime: Int,
+ ) : HistoryMetadataObservation()
+
+ /**
+ * A [HistoryMetadataObservation] to update the document type.
+ */
+ data class DocumentTypeObservation(
+ val documentType: DocumentType,
+ ) : HistoryMetadataObservation()
+}
+
+/**
+ * Represents a set of history metadata values that uniquely identify a record. Note that
+ * when recording observations, the same set of values may or may not cause a new record to be
+ * created, depending on the de-bouncing logic of the underlying storage i.e. recording history
+ * metadata observations with the exact same values may be combined into a single record.
+ *
+ * @property url A url of the page.
+ * @property searchTerm An optional search term if this record was
+ * created as part of a search by the user.
+ * @property referrerUrl An optional url of the parent/referrer if
+ * this record was created in response to a user opening
+ * a page in a new tab.
+ */
+@Parcelize
+data class HistoryMetadataKey(
+ val url: String,
+ val searchTerm: String? = null,
+ val referrerUrl: String? = null,
+) : Parcelable
+
+/**
+ * Represents a history metadata record, which describes metadata for a history visit, such as metadata
+ * about the page itself as well as metadata about how the page was opened.
+ *
+ * @property key The [HistoryMetadataKey] of this record.
+ * @property title A title of the page.
+ * @property createdAt When this metadata record was created.
+ * @property updatedAt The last time this record was updated.
+ * @property totalViewTime Total time the user viewed the page associated with this record.
+ * @property documentType The [DocumentType] of the page.
+ * @property previewImageUrl A preview image of the page (a.k.a. the hero image), if available.
+ */
+data class HistoryMetadata(
+ val key: HistoryMetadataKey,
+ val title: String?,
+ val createdAt: Long,
+ val updatedAt: Long,
+ val totalViewTime: Int,
+ val documentType: DocumentType,
+ val previewImageUrl: String?,
+)
+
+/**
+ * Represents a history highlight, a URL of interest.
+ * The highlights are produced via [HistoryMetadataStorage.getHistoryHighlights].
+ *
+ * @param score A relative score of this highlight. Useful to compare against other highlights.
+ * @param placeId An ID of the history entry ("page") represented by this highlight.
+ * @param url A url of the page.
+ * @param title A title of the page, if available.
+ * @param previewImageUrl A preview image of the page (a.k.a. the hero image), if available.
+ */
+data class HistoryHighlight(
+ val score: Double,
+ val placeId: Int,
+ val url: String,
+ val title: String?,
+ val previewImageUrl: String?,
+)
+
+/**
+ * Weights of factors that contribute to ranking [HistoryHighlight].
+ * An input to [HistoryMetadataStorage.getHistoryHighlights].
+ * For example, (1.0, 1.0) for equal weights. Equal weights represent equal importance of these
+ * factors during ranking.
+ *
+ * @param viewTime A weight specifying importance of cumulative view time of a page.
+ * @param frequency A weight specifying importance of frequency of visits to a page.
+ */
+data class HistoryHighlightWeights(
+ val viewTime: Double,
+ val frequency: Double,
+)
+
+/**
+ * An interface for interacting with a storage that manages [HistoryMetadata].
+ */
+interface HistoryMetadataStorage : Cancellable {
+ /**
+ * Returns the most recent [HistoryMetadata] for the provided [url].
+ *
+ * @param url Url to search by.
+ * @return [HistoryMetadata] if there's a matching record, `null` otherwise.
+ */
+ suspend fun getLatestHistoryMetadataForUrl(url: String): HistoryMetadata?
+
+ /**
+ * Returns all [HistoryMetadata] where [HistoryMetadata.updatedAt] is greater or equal to [since].
+ *
+ * @param since Timestamp to search by.
+ * @return A `List` of matching [HistoryMetadata], ordered by [HistoryMetadata.updatedAt] DESC.
+ * Empty if nothing is found.
+ */
+ suspend fun getHistoryMetadataSince(since: Long): List<HistoryMetadata>
+
+ /**
+ * Returns all [HistoryMetadata] where [HistoryMetadata.updatedAt] is between [start] and [end], inclusive.
+ *
+ * @param start A `start` timestamp.
+ * @param end An `end` timestamp.
+ * @return A `List` of matching [HistoryMetadata], ordered by [HistoryMetadata.updatedAt] DESC.
+ * Empty if nothing is found.
+ */
+ suspend fun getHistoryMetadataBetween(start: Long, end: Long): List<HistoryMetadata>
+
+ /**
+ * Searches through [HistoryMetadata] by [query], matching records by [HistoryMetadataKey.url],
+ * [HistoryMetadata.title] and [HistoryMetadataKey.searchTerm].
+ *
+ * @param query A search query.
+ * @param limit A maximum number of records to return.
+ * @return A `List` of matching [HistoryMetadata], ordered by [HistoryMetadata.updatedAt] DESC.
+ * Empty if nothing is found.
+ */
+ suspend fun queryHistoryMetadata(query: String, limit: Int): List<HistoryMetadata>
+
+ /**
+ * Returns a list of [HistoryHighlight] objects, ranked relative to each other according to [weights].
+ *
+ * @param weights A set of weights used by the ranking algorithm.
+ * @param limit A maximum number of records to return.
+ * @return A `List` of [HistoryHighlight], ordered by [HistoryHighlight.score] DESC.
+ * Empty if nothing is found.
+ */
+ suspend fun getHistoryHighlights(weights: HistoryHighlightWeights, limit: Int): List<HistoryHighlight>
+
+ /**
+ * Records the provided [HistoryMetadataObservation] and updates the record identified by the
+ * provided [HistoryMetadataKey].
+ *
+ * @param key the [HistoryMetadataKey] identifying the metadata records
+ * @param observation the [HistoryMetadataObservation] to record.
+ */
+ suspend fun noteHistoryMetadataObservation(key: HistoryMetadataKey, observation: HistoryMetadataObservation)
+
+ /**
+ * Deletes [HistoryMetadata] with [HistoryMetadata.updatedAt] older than [olderThan].
+ *
+ * @param olderThan A timestamp to delete records by. Exclusive.
+ */
+ suspend fun deleteHistoryMetadataOlderThan(olderThan: Long)
+
+ /**
+ * Deletes metadata records that match [HistoryMetadataKey].
+ */
+ suspend fun deleteHistoryMetadata(key: HistoryMetadataKey)
+
+ /**
+ * Deletes metadata records that match [searchTerm] (case insensitive).
+ */
+ suspend fun deleteHistoryMetadata(searchTerm: String)
+
+ /**
+ * Deletes all metadata records for the provided [url].
+ */
+ suspend fun deleteHistoryMetadataForUrl(url: String)
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/HistoryStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/HistoryStorage.kt
new file mode 100644
index 0000000000..20af370f62
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/HistoryStorage.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.concept.storage
+
+/**
+ * An interface which defines read/write methods for history data.
+ */
+interface HistoryStorage : Storage {
+ /**
+ * Records a visit to a page.
+ * @param uri of the page which was visited.
+ * @param visit Information about the visit; see [PageVisit].
+ */
+ suspend fun recordVisit(uri: String, visit: PageVisit)
+
+ /**
+ * Records an observation about a page.
+ * @param uri of the page for which to record an observation.
+ * @param observation a [PageObservation] which encapsulates meta data observed about the page.
+ */
+ suspend fun recordObservation(uri: String, observation: PageObservation)
+
+ /**
+ * @return True if provided [uri] can be added to the storage layer.
+ */
+ fun canAddUri(uri: String): Boolean
+
+ /**
+ * Maps a list of page URIs to a list of booleans indicating if each URI was visited.
+ * @param uris a list of page URIs about which "visited" information is being requested.
+ * @return A list of booleans indicating visited status of each
+ * corresponding page URI from [uris].
+ */
+ suspend fun getVisited(uris: List<String>): List<Boolean>
+
+ /**
+ * Retrieves a list of all visited pages.
+ * @return A list of all visited page URIs.
+ */
+ suspend fun getVisited(): List<String>
+
+ /**
+ * Retrieves detailed information about all visits that occurred in the given time range.
+ * @param start The (inclusive) start time to bound the query.
+ * @param end The (inclusive) end time to bound the query.
+ * @param excludeTypes List of visit types to exclude.
+ * @return A list of all visits within the specified range, described by [VisitInfo].
+ */
+ suspend fun getDetailedVisits(
+ start: Long,
+ end: Long = Long.MAX_VALUE,
+ excludeTypes: List<VisitType> = listOf(),
+ ): List<VisitInfo>
+
+ /**
+ * Return a "page" of history results. Each page will have visits in descending order
+ * with respect to their visit timestamps. In the case of ties, their row id will
+ * be used.
+ *
+ * Note that you may get surprising results if the items in the database change
+ * while you are paging through records.
+ *
+ * @param offset The offset where the page begins.
+ * @param count The number of items to return in the page.
+ * @param excludeTypes List of visit types to exclude.
+ */
+ suspend fun getVisitsPaginated(
+ offset: Long,
+ count: Long,
+ excludeTypes: List<VisitType> = listOf(),
+ ): List<VisitInfo>
+
+ /**
+ * Returns a list of the top frecent site infos limited by the given number of items and
+ * frecency threshold sorted by most to least frecent.
+ *
+ * @param numItems the number of top frecent sites to return in the list.
+ * @param frecencyThreshold frecency threshold option for filtering visited sites based on
+ * their frecency score.
+ * @return a list of the [TopFrecentSiteInfo], most frecent first.
+ */
+ suspend fun getTopFrecentSites(
+ numItems: Int,
+ frecencyThreshold: FrecencyThresholdOption,
+ ): List<TopFrecentSiteInfo>
+
+ /**
+ * Retrieves suggestions matching the [query].
+ * @param query A query by which to search the underlying store.
+ * @return A List of [SearchResult] matching the query, in no particular order.
+ */
+ fun getSuggestions(query: String, limit: Int): List<SearchResult>
+
+ /**
+ * Remove all locally stored data.
+ */
+ suspend fun deleteEverything()
+
+ /**
+ * Remove history visits in an inclusive range from [since] to now.
+ * @param since A unix timestamp, milliseconds.
+ */
+ suspend fun deleteVisitsSince(since: Long)
+
+ /**
+ * Remove history visits in an inclusive range from [startTime] to [endTime].
+ * @param startTime A unix timestamp, milliseconds.
+ * @param endTime A unix timestamp, milliseconds.
+ */
+ suspend fun deleteVisitsBetween(startTime: Long, endTime: Long)
+
+ /**
+ * Remove all history visits for a given [url].
+ * @param url A page URL for which to remove visits.
+ */
+ suspend fun deleteVisitsFor(url: String)
+
+ /**
+ * Remove a specific visit for a given [url].
+ * @param url A page URL for which to remove a visit.
+ * @param timestamp A unix timestamp, milliseconds, of a visit to be removed.
+ */
+ suspend fun deleteVisit(url: String, timestamp: Long)
+}
+
+/**
+ * Information to record about a visit.
+ *
+ * @property visitType The transition type for this visit. See [VisitType].
+ * @property redirectSource Optional; if this visit is redirecting to another page,
+ * what kind of redirect is it? See [RedirectSource] for the options.
+ */
+data class PageVisit(
+ val visitType: VisitType,
+ val redirectSource: RedirectSource? = null,
+)
+
+/**
+ * A redirect source describes how a page redirected to another page.
+ */
+enum class RedirectSource {
+ // The page temporarily redirected to another page.
+ TEMPORARY,
+
+ // The page permanently redirected to another page.
+ PERMANENT,
+}
+
+/**
+ * Metadata information observed in a page to record.
+ *
+ * @property title The title of the page.
+ * @property previewImageUrl The preview image of the page (e.g. the hero image), if available.
+ */
+data class PageObservation(
+ val title: String? = null,
+ val previewImageUrl: String? = null,
+)
+
+/**
+ * Information about a top frecent site. This represents a most frequently visited site.
+ *
+ * @property url The URL of the page that was visited.
+ * @property title The title of the page that was visited, if known.
+ */
+data class TopFrecentSiteInfo(
+ val url: String,
+ val title: String?,
+)
+
+/**
+ * Frecency threshold options for fetching top frecent sites.
+ */
+enum class FrecencyThresholdOption {
+ /** Returns all visited pages. */
+ NONE,
+
+ /** Skip visited pages that were only visited once. */
+ SKIP_ONE_TIME_PAGES,
+}
+
+/**
+ * Information about a history visit.
+ *
+ * @property url The URL of the page that was visited.
+ * @property title The title of the page that was visited, if known.
+ * @property visitTime The time the page was visited in integer milliseconds since the unix epoch.
+ * @property visitType What the transition type of the visit is, expressed as [VisitType].
+ * @property previewImageUrl The preview image of the page (e.g. the hero image), if available.
+ * @property isRemote Distinguishes visits made on the device and those that come from Sync.
+ */
+data class VisitInfo(
+ val url: String,
+ val title: String?,
+ val visitTime: Long,
+ val visitType: VisitType,
+ val previewImageUrl: String?,
+ var isRemote: Boolean,
+)
+
+/**
+ * Visit type constants as defined by Desktop Firefox.
+ */
+@Suppress("MagicNumber")
+enum class VisitType(val type: Int) {
+
+ // User followed a link.
+ LINK(1),
+
+ // User typed a URL or selected it from the UI (autocomplete results, etc).
+ TYPED(2),
+ BOOKMARK(3),
+ EMBED(4),
+ REDIRECT_PERMANENT(5),
+ REDIRECT_TEMPORARY(6),
+ DOWNLOAD(7),
+ FRAMED_LINK(8),
+ RELOAD(9),
+}
+
+/**
+ * Encapsulates a set of properties which define a result of querying history storage.
+ *
+ * @property id A permanent identifier which might be used for caching or at the UI layer.
+ * @property url A URL of the page.
+ * @property score An unbounded, nonlinear score (larger is more relevant) which is used to rank
+ * this [SearchResult] against others.
+ * @property title An optional title of the page.
+ */
+data class SearchResult(
+ val id: String,
+ val url: String,
+ val score: Int,
+ val title: String? = null,
+)
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/KeyManager.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/KeyManager.kt
new file mode 100644
index 0000000000..cd5b2356a6
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/KeyManager.kt
@@ -0,0 +1,108 @@
+/* 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.concept.storage
+
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlin.IllegalStateException
+
+/**
+ * Knows how to manage (generate, store, validate) keys and recover from their loss.
+ */
+abstract class KeyManager : KeyProvider {
+ // Exists to ensure that key generation/validation/recovery flow is synchronized.
+ private val keyMutex = Mutex()
+
+ /**
+ * @return Generated key.
+ */
+ abstract fun createKey(): String
+
+ /**
+ * Determines if [rawKey] is still valid for a given [canary], or if recovery is necessary.
+ * @return Optional [KeyGenerationReason.RecoveryNeeded] if recovery is necessary.
+ */
+ abstract fun isKeyRecoveryNeeded(rawKey: String, canary: String): KeyGenerationReason.RecoveryNeeded?
+
+ /**
+ * Returns a stored canary, if there's one. A canary is some known string encrypted with the managed key.
+ * @return an optional, stored canary string.
+ */
+ abstract fun getStoredCanary(): String?
+
+ /**
+ * Returns a stored key, if there's one.
+ */
+ abstract fun getStoredKey(): String?
+
+ /**
+ * Stores [key]; using the key, generate and store a canary.
+ */
+ abstract fun storeKeyAndCanary(key: String)
+
+ /**
+ * Recover from key loss that happened due to [reason].
+ * If this KeyManager wraps a storage layer, it should probably remove the now-unreadable data.
+ */
+ abstract suspend fun recoverFromKeyLoss(reason: KeyGenerationReason.RecoveryNeeded)
+
+ override suspend fun getOrGenerateKey(): ManagedKey = keyMutex.withLock {
+ val managedKey = getManagedKey()
+
+ (managedKey.wasGenerated as? KeyGenerationReason.RecoveryNeeded)?.let {
+ recoverFromKeyLoss(managedKey.wasGenerated)
+ }
+ return managedKey
+ }
+
+ /**
+ * Access should be guarded by [keyMutex].
+ */
+ private fun getManagedKey(): ManagedKey {
+ val storedCanaryPhrase = getStoredCanary()
+ val storedKey = getStoredKey()
+
+ return when {
+ // We expected the key to be present, and it is.
+ storedKey != null && storedCanaryPhrase != null -> {
+ // Make sure that the key is valid.
+ when (val recoveryReason = isKeyRecoveryNeeded(storedKey, storedCanaryPhrase)) {
+ is KeyGenerationReason -> ManagedKey(generateAndStoreKey(), recoveryReason)
+ null -> ManagedKey(storedKey)
+ }
+ }
+
+ // The key is present, but we didn't expect it to be there.
+ storedKey != null && storedCanaryPhrase == null -> {
+ // This isn't expected to happen. We can't check this key's validity.
+ ManagedKey(generateAndStoreKey(), KeyGenerationReason.RecoveryNeeded.AbnormalState)
+ }
+
+ // We expected the key to be present, but it's gone missing on us.
+ storedKey == null && storedCanaryPhrase != null -> {
+ // At this point, we're forced to generate a new key to recover and move forward.
+ // However, that means that any data that was previously encrypted is now unreadable.
+ ManagedKey(generateAndStoreKey(), KeyGenerationReason.RecoveryNeeded.Lost)
+ }
+
+ // We didn't expect the key to be present, and it's not.
+ storedKey == null && storedCanaryPhrase == null -> {
+ // Normal case when interacting with this class for the first time.
+ ManagedKey(generateAndStoreKey(), KeyGenerationReason.New)
+ }
+
+ else -> throw IllegalStateException()
+ }
+ }
+
+ /**
+ * Access should be guarded by [keyMutex].
+ */
+ private fun generateAndStoreKey(): String {
+ return createKey().also { newKey ->
+ storeKeyAndCanary(newKey)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/KeyProvider.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/KeyProvider.kt
new file mode 100644
index 0000000000..ba1bede11f
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/KeyProvider.kt
@@ -0,0 +1,58 @@
+/* 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.concept.storage
+
+/**
+ * Knows how to provide a [ManagedKey].
+ */
+interface KeyProvider {
+ /**
+ * Fetches or generates a new encryption key.
+ *
+ * @return [ManagedKey] that wraps the encryption key.
+ */
+ suspend fun getOrGenerateKey(): ManagedKey
+}
+
+/**
+ * An encryption key, with an optional [wasGenerated] field used to indicate if it was freshly
+ * generated. In that case, a [KeyGenerationReason] is supplied, allowing consumers to detect
+ * potential key loss or corruption.
+ * If [wasGenerated] is `null`, that means an existing key was successfully read from the key storage.
+ */
+data class ManagedKey(
+ val key: String,
+ val wasGenerated: KeyGenerationReason? = null,
+)
+
+/**
+ * Describes why a key was generated.
+ */
+sealed class KeyGenerationReason {
+ /**
+ * A new key, not previously present in the store.
+ */
+ object New : KeyGenerationReason()
+
+ /**
+ * Something went wrong with the previously stored key.
+ */
+ sealed class RecoveryNeeded : KeyGenerationReason() {
+ /**
+ * Previously stored key was lost, and a new key was generated as its replacement.
+ */
+ object Lost : RecoveryNeeded()
+
+ /**
+ * Previously stored key was corrupted, and a new key was generated as its replacement.
+ */
+ object Corrupt : RecoveryNeeded()
+
+ /**
+ * Storage layer encountered an abnormal state, which lead to key loss. A new key was generated.
+ */
+ object AbnormalState : RecoveryNeeded()
+ }
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt
new file mode 100644
index 0000000000..47ffc8145b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt
@@ -0,0 +1,277 @@
+/* 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.concept.storage
+
+import kotlinx.coroutines.Deferred
+
+/**
+ * A login stored in the database
+ */
+data class Login(
+ /**
+ * The unique identifier for this login entry.
+ */
+ val guid: String,
+ /**
+ * The username for this login entry.
+ */
+ val username: String,
+ /**
+ * The password for this login entry.
+ */
+ val password: String,
+ /**
+ * The origin this login entry applies to.
+ */
+ val origin: String,
+ /**
+ * The origin this login entry was submitted to.
+ * This only applies to form-based login entries.
+ * It's derived from the action attribute set on the form element.
+ */
+ val formActionOrigin: String? = null,
+ /**
+ * The HTTP realm this login entry was requested for.
+ * This only applies to non-form-based login entries.
+ * It's derived from the WWW-Authenticate header set in a HTTP 401
+ * response, see RFC2617 for details.
+ */
+ val httpRealm: String? = null,
+ /**
+ * HTML field associated with the [username].
+ */
+ val usernameField: String = "",
+ /**
+ * HTML field associated with the [password].
+ */
+ val passwordField: String = "",
+ /**
+ * Number of times this password has been used.
+ */
+ val timesUsed: Long = 0L,
+ /**
+ * Time of creation in milliseconds from the unix epoch.
+ */
+ val timeCreated: Long = 0L,
+ /**
+ * Time of last use in milliseconds from the unix epoch.
+ */
+ val timeLastUsed: Long = 0L,
+ /**
+ * Time of last password change in milliseconds from the unix epoch.
+ */
+ val timePasswordChanged: Long = 0L,
+) {
+ /**
+ * Converts [Login] into a [LoginEntry].
+ */
+ fun toEntry() = LoginEntry(
+ origin = origin,
+ formActionOrigin = formActionOrigin,
+ httpRealm = httpRealm,
+ usernameField = usernameField,
+ passwordField = passwordField,
+ username = username,
+ password = password,
+ )
+}
+
+/**
+ * Login autofill entry
+ *
+ * This contains the data needed to handle autofill but not the data related to
+ * the DB record. [LoginsStorage] methods that save data typically input
+ * [LoginEntry] instances. This allows the storage backend handle
+ * dupe-checking issues like determining which login record should be updated
+ * for a given [LoginEntry]. [LoginEntry] also represents the login data
+ * that's editable in the API.
+ *
+ * All fields have the same meaning as in [Login].
+ */
+data class LoginEntry(
+ val origin: String,
+ val formActionOrigin: String? = null,
+ val httpRealm: String? = null,
+ val usernameField: String = "",
+ val passwordField: String = "",
+ val username: String,
+ val password: String,
+)
+
+/**
+ * Login where the sensitive data is the encrypted.
+ *
+ * This have the same fields as [Login] except username and password is replaced with [secFields]
+ */
+data class EncryptedLogin(
+ val guid: String,
+ val origin: String,
+ val formActionOrigin: String? = null,
+ val httpRealm: String? = null,
+ val usernameField: String = "",
+ val passwordField: String = "",
+ val timesUsed: Long = 0L,
+ val timeCreated: Long = 0L,
+ val timeLastUsed: Long = 0L,
+ val timePasswordChanged: Long = 0L,
+ val secFields: String,
+)
+
+/**
+ * An interface describing a storage layer for logins/passwords.
+ */
+interface LoginsStorage : AutoCloseable {
+ /**
+ * Clears out all local state, bringing us back to the state before the first write (or sync).
+ */
+ suspend fun wipeLocal()
+
+ /**
+ * Deletes the login with the given GUID.
+ *
+ * @return True if the deletion did anything, false otherwise.
+ */
+ suspend fun delete(guid: String): Boolean
+
+ /**
+ * Fetches a password from the underlying storage layer by its GUID
+ *
+ * @param guid Unique identifier for the desired record.
+ * @return [Login] record, or `null` if the record does not exist.
+ */
+ suspend fun get(guid: String): Login?
+
+ /**
+ * Marks that a login has been used
+ *
+ * @param guid Unique identifier for the desired record.
+ */
+ suspend fun touch(guid: String)
+
+ /**
+ * Fetches the full list of logins from the underlying storage layer.
+ *
+ * @return A list of stored [Login] records.
+ */
+ suspend fun list(): List<Login>
+
+ /**
+ * Calculate how we should save a login
+ *
+ * For a [LoginEntry] to save find an existing [Login] to be update (if
+ * any).
+ *
+ * @param entry [LoginEntry] being saved
+ * @return [Login] that should be updated, or null if the login should be added
+ */
+ suspend fun findLoginToUpdate(entry: LoginEntry): Login?
+
+ /**
+ * Inserts the provided login into the database
+
+ * This will return an error result if the provided record is invalid
+ * (missing password, origin, or doesn't have exactly one of formSubmitURL
+ * and httpRealm).
+ *
+ * @param login [LoginEntry] to add.
+ * @return [EncryptedLogin] that was added
+ */
+ suspend fun add(entry: LoginEntry): EncryptedLogin
+
+ /**
+ * Updates an existing login in the database
+ *
+ * This will throw if `guid` does not refer to a record that exists in the
+ * database, or if the provided record is invalid (missing password,
+ * origin, or doesn't have exactly one of formSubmitURL and httpRealm).
+ *
+ * @param guid Unique identifier for the record
+ * @param login [LoginEntry] to add.
+ * @return [EncryptedLogin] that was added
+ */
+ suspend fun update(guid: String, entry: LoginEntry): EncryptedLogin
+
+ /**
+ * Checks if a record exists for a [LoginEntry] and calls either add() or update()
+ *
+ * This will throw if the provided record is invalid (missing password,
+ * origin, or doesn't have exactly one of formSubmitURL and httpRealm).
+ *
+ * @param login [LoginEntry] to add or update.
+ * @return [EncryptedLogin] that was added
+ */
+ suspend fun addOrUpdate(entry: LoginEntry): EncryptedLogin
+
+ /**
+ * Fetch the list of logins for some origin from the underlying storage layer.
+ *
+ * @param origin A host name used to look up logins
+ * @return A list of [Login] objects, representing matching logins.
+ */
+ suspend fun getByBaseDomain(origin: String): List<Login>
+
+ /**
+ * Decrypt an [EncryptedLogin]
+ *
+ * @param login [EncryptedLogin] to decrypt
+ * @return [Login] with decrypted data
+ */
+ suspend fun decryptLogin(login: EncryptedLogin): Login
+}
+
+/**
+ * Validates a [LoginEntry] that will be saved and calculates if saving it
+ * would update an existing [Login] or create a new one.
+ */
+interface LoginValidationDelegate {
+ /**
+ * The result of validating a given [Login] against currently stored [Login]s. This will
+ * include whether it can be created, updated, or neither, along with an explanation of any errors.
+ */
+ sealed class Result {
+ /**
+ * Indicates that the [Login] does not currently exist in the storage, and a new entry
+ * with its information can be made.
+ */
+ object CanBeCreated : Result()
+
+ /**
+ * Indicates that a matching [Login] was found in storage, and the [Login] can be used
+ * to update its information.
+ */
+ data class CanBeUpdated(val foundLogin: Login) : Result()
+ }
+
+ /**
+ *
+ * Checks whether a [login] should be saved or updated.
+ *
+ * @returns a [Result], detailing whether a [login] should be saved or updated.
+ */
+ fun shouldUpdateOrCreateAsync(entry: LoginEntry): Deferred<Result>
+}
+
+/**
+ * Used to handle [Login] storage so that the underlying engine doesn't have to. An instance of
+ * this should be attached to the Gecko runtime in order to be used.
+ */
+interface LoginStorageDelegate {
+ /**
+ * Called after a [login] has been autofilled into web content.
+ */
+ fun onLoginUsed(login: Login)
+
+ /**
+ * Given a [domain], returns the matching [Login]s found in [loginStorage].
+ *
+ * This is called when the engine believes a field should be autofilled.
+ */
+ fun onLoginFetch(domain: String): Deferred<List<Login>>
+
+ /**
+ * Called when a [LogenEntry] should be added or updated.
+ */
+ fun onLoginSave(login: LoginEntry)
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/Storage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/Storage.kt
new file mode 100644
index 0000000000..72c01ebc14
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/Storage.kt
@@ -0,0 +1,20 @@
+/* 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.concept.storage
+
+/**
+ * An interface which provides generic operations for storing browser data like history and bookmarks.
+ */
+interface Storage : Cancellable {
+ /**
+ * Make sure underlying database connections are established.
+ */
+ suspend fun warmUp()
+
+ /**
+ * Runs internal database maintenance tasks
+ */
+ suspend fun runMaintenance(dbSizeLimit: UInt)
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceRegistry.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceRegistry.kt
new file mode 100644
index 0000000000..44feb6e8da
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceRegistry.kt
@@ -0,0 +1,27 @@
+/* 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.concept.storage
+
+/**
+ * An interface which registers and unregisters storage maintenance WorkManager workers
+ * that run maintenance on storages.
+ */
+interface StorageMaintenanceRegistry {
+
+ /**
+ * Registers a storage maintenance worker that prunes database when its size exceeds a size limit.
+ * See also [Storage.runMaintenance].
+ * */
+ fun registerStorageMaintenanceWorker()
+
+ /**
+ * Unregisters the storage maintenance worker that is registered
+ * by [StorageMaintenanceRegistry.registerStorageMaintenanceWorker].
+ * See also [Storage.runMaintenance].
+ *
+ * @param uniqueWorkName Unique name of the work request that needs to be unregistered.
+ * */
+ fun unregisterStorageMaintenanceWorker(uniqueWorkName: String)
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/AddressTest.kt b/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/AddressTest.kt
new file mode 100644
index 0000000000..9d775d417b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/AddressTest.kt
@@ -0,0 +1,92 @@
+/* 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.concept.storage
+
+import mozilla.components.concept.storage.Address.Companion.toOneLineAddress
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class AddressTest {
+
+ @Test
+ fun `WHEN all address properties are present THEN full address present in label`() {
+ val address = generateAddress()
+ val expected =
+ "${address.streetAddress}, ${address.addressLevel3}, ${address.addressLevel2}, " +
+ "${address.organization}, ${address.addressLevel1}, ${address.country}, " +
+ "${address.postalCode}, ${address.tel}, ${address.email}"
+
+ assertEquals(expected, address.addressLabel)
+ }
+
+ @Test
+ fun `WHEN any address properties are missing THEN label only includes only properties that are available`() {
+ val address = generateAddress(
+ addressLevel3 = "",
+ organization = "",
+ email = "",
+ )
+ val expected =
+ "${address.streetAddress}, ${address.addressLevel2}, ${address.addressLevel1}, " +
+ "${address.country}, ${address.postalCode}, ${address.tel}"
+
+ assertEquals(expected, address.addressLabel)
+ }
+
+ @Test
+ fun `WHEN no address properties are present THEN label is the empty string`() {
+ val address = generateAddress(
+ name = "",
+ organization = "",
+ streetAddress = "",
+ addressLevel3 = "",
+ addressLevel2 = "",
+ addressLevel1 = "",
+ postalCode = "",
+ country = "",
+ tel = "",
+ email = "",
+ )
+
+ assertEquals("", address.addressLabel)
+ }
+
+ @Test
+ fun `GIVEN multiline street address WHEN one line address is called THEN an one line address is returned`() {
+ val streetAddress = """
+ line1
+ line2
+ line3
+ """.trimIndent()
+
+ assertEquals("line1 line2 line3", streetAddress.toOneLineAddress())
+ }
+
+ private fun generateAddress(
+ guid: String = "",
+ name: String = "Firefox The Browser",
+ organization: String = "Mozilla",
+ streetAddress: String = "street",
+ addressLevel3: String = "3",
+ addressLevel2: String = "2",
+ addressLevel1: String = "1",
+ postalCode: String = "code",
+ country: String = "country",
+ tel: String = "tel",
+ email: String = "email",
+ ) = Address(
+ guid = guid,
+ name = name,
+ organization = organization,
+ streetAddress = streetAddress,
+ addressLevel3 = addressLevel3,
+ addressLevel2 = addressLevel2,
+ addressLevel1 = addressLevel1,
+ postalCode = postalCode,
+ country = country,
+ tel = tel,
+ email = email,
+ )
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/BookmarkNodeTest.kt b/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/BookmarkNodeTest.kt
new file mode 100644
index 0000000000..087504c32c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/BookmarkNodeTest.kt
@@ -0,0 +1,119 @@
+/* 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.concept.storage
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BookmarkNodeTest {
+
+ private val bookmarkChild1 = testBookmarkItem(
+ url = "http://www.mockurl.com/1",
+ title = "Child 1",
+ )
+ private val bookmarkChild2 = testBookmarkItem(
+ url = "http://www.mockurl.com/2",
+ title = "Child 2",
+ )
+ private val bookmarkChild3 = testBookmarkItem(
+ url = "http://www.mockurl.com/3",
+ title = "Child 3",
+ )
+ private val allChildren = listOf(bookmarkChild1, bookmarkChild2)
+
+ @Test
+ fun `GIVEN a bookmark node with children WHEN subtracting a sub set of children THEN the children subset is removed and rest remains`() {
+ val bookmarkNode = testFolder("parent1", "root", allChildren)
+ val subsetToSubtract = setOf(bookmarkChild1)
+ val expectedRemainingSubset = listOf(bookmarkChild2)
+ val bookmarkNodeSubsetRemoved = bookmarkNode.minus(subsetToSubtract)
+ assertEquals(expectedRemainingSubset, bookmarkNodeSubsetRemoved.children)
+ }
+
+ @Test
+ fun `GIVEN a bookmark node with children WHEN subtracting a set of all children THEN all children are removed and empty list remains`() {
+ val bookmarkNode = testFolder("parent1", "root", allChildren)
+ val setOfAllChildren = setOf(bookmarkChild1, bookmarkChild2)
+ val bookmarkNodeAllChildrenRemoved = bookmarkNode.minus(setOfAllChildren)
+ assertEquals(emptyList<BookmarkNode>(), bookmarkNodeAllChildrenRemoved.children)
+ }
+
+ @Test
+ fun `GIVEN a bookmark node with children WHEN subtracting a set of non-children THEN no children are removed`() {
+ val setOfNonChildren = setOf(bookmarkChild3)
+ val bookmarkNode = testFolder("parent1", "root", allChildren)
+ val bookmarkNodeNonChildrenRemoved = bookmarkNode.minus(setOfNonChildren)
+ assertEquals(allChildren, bookmarkNodeNonChildrenRemoved.children)
+ }
+
+ @Test
+ fun `GIVEN a bookmark node with children WHEN subtracting an empty set THEN no children are removed`() {
+ val bookmarkNode = testFolder("parent1", "root", allChildren)
+ val bookmarkNodeEmptySetRemoved = bookmarkNode.minus(emptySet())
+ assertEquals(allChildren, bookmarkNodeEmptySetRemoved.children)
+ }
+
+ @Test
+ fun `GIVEN a bookmark node with an empty list as children WHEN subtracting a set of non-children from an empty parent THEN an empty list remains`() {
+ val parentWithEmptyList = testFolder("parent1", "root", emptyList())
+ val setOfAllChildren = setOf(bookmarkChild1, bookmarkChild2)
+ val parentWithEmptyListNonChildRemoved = parentWithEmptyList.minus(setOfAllChildren)
+ assertEquals(emptyList<BookmarkNode>(), parentWithEmptyListNonChildRemoved.children)
+ }
+
+ @Test
+ fun `GIVEN a bookmark node with null as children WHEN subtracting a set of non-children from a parent with null children THEN null remains`() {
+ val parentWithNullList = testFolder("parent1", "root", null)
+ val parentWithNullListNonChildRemoved = parentWithNullList.minus(allChildren.toSet())
+ assertEquals(null, parentWithNullListNonChildRemoved.children)
+ }
+
+ @Test
+ fun `GIVEN a bookmark node with children WHEN subtracting a sub-set of children THEN the rest of the parents object should remain the same`() {
+ val bookmarkNode = testFolder("parent1", "root", allChildren)
+ val subsetToSubtract = setOf(bookmarkChild1)
+ val expectedRemainingSubset = listOf(bookmarkChild2)
+ val resultBookmarkNode = bookmarkNode.minus(subsetToSubtract)
+
+ // We're pinning children to the same value so we can compare the rest.
+ val restOfResult = resultBookmarkNode.copy(children = expectedRemainingSubset)
+ val restOfOriginal = bookmarkNode.copy(children = expectedRemainingSubset)
+ assertEquals(restOfResult, restOfOriginal)
+ }
+
+ private fun testBookmarkItem(
+ parentGuid: String = "someFolder",
+ url: String,
+ title: String = "Item for $url",
+ guid: String = "guid#${Math.random() * 1000}",
+ position: UInt = 0u,
+ ) = BookmarkNode(
+ type = BookmarkNodeType.ITEM,
+ dateAdded = 0,
+ children = null,
+ guid = guid,
+ parentGuid = parentGuid,
+ position = position,
+ title = title,
+ url = url,
+ )
+
+ private fun testFolder(
+ guid: String,
+ parentGuid: String? = null,
+ children: List<BookmarkNode>?,
+ title: String = "Folder: $guid",
+ position: UInt = 0u,
+ ) = BookmarkNode(
+ type = BookmarkNodeType.FOLDER,
+ url = null,
+ dateAdded = 0,
+ guid = guid,
+ parentGuid = parentGuid,
+ position = position,
+ title = title,
+ children = children,
+ )
+}
diff --git a/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/CreditCardEntryTest.kt b/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/CreditCardEntryTest.kt
new file mode 100644
index 0000000000..8dd797aef8
--- /dev/null
+++ b/mobile/android/android-components/components/concept/storage/src/test/java/mozilla/components/concept/storage/CreditCardEntryTest.kt
@@ -0,0 +1,94 @@
+/* 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.concept.storage
+
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsesEnd
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsesStart
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsis
+import mozilla.components.support.ktx.kotlin.last4Digits
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+
+class CreditCardEntryTest {
+
+ private val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+
+ @Test
+ fun `WHEN obfuscatedCardNumber getter is called THEN the expected obfuscated card number is returned`() {
+ assertEquals(
+ ellipsesStart +
+ ellipsis + ellipsis + ellipsis + ellipsis +
+ creditCard.number.last4Digits() +
+ ellipsesEnd,
+ creditCard.obfuscatedCardNumber,
+ )
+ }
+
+ @Test
+ fun `WHEN expiryDdate getter is called THEN the expected expiry date string is returned`() {
+ assertEquals("0${creditCard.expiryMonth}/${creditCard.expiryYear}", creditCard.expiryDate)
+ }
+
+ @Test
+ fun `GIVEN empty expiration date strings WHEN a credit card needs to display its full expiration date THEN the an empty string is returned`() {
+ val creditCardWithoutYear = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "",
+ cardType = "amex",
+ )
+ val creditCardWithoutMonth = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+ val creditCardWithoutFullDate = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "",
+ expiryYear = "",
+ cardType = "amex",
+ )
+
+ assertEquals("", creditCardWithoutYear.expiryDate)
+ assertEquals("", creditCardWithoutMonth.expiryDate)
+ assertEquals("", creditCardWithoutFullDate.expiryDate)
+ }
+
+ @Test
+ fun `GIVEN empty number THEN entry is considered invalid`() {
+ val entry = creditCard.copy(number = "")
+
+ assertFalse(entry.isValid)
+ }
+
+ @Test
+ fun `GIVEN empty expiry month THEN entry is considered invalid`() {
+ val entry = creditCard.copy(expiryMonth = "")
+
+ assertFalse(entry.isValid)
+ }
+
+ @Test
+ fun `GIVEN empty expiry year THEN entry is considered invalid`() {
+ val entry = creditCard.copy(expiryYear = "")
+
+ assertFalse(entry.isValid)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/sync/README.md b/mobile/android/android-components/components/concept/sync/README.md
new file mode 100644
index 0000000000..787a6a3af2
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/README.md
@@ -0,0 +1,26 @@
+# [Android Components](../../../README.md) > Concept > Sync
+
+The `concept-sync` component contains interfaces and types that describe various aspects of data synchronization.
+
+This abstraction makes it possible to create different implementations of synchronization backends, without tightly
+coupling concrete implementations of storage, accounts and sync sub-systems.
+
+## Usage
+
+### 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:concept-sync:{latest-version}"
+```
+
+### Integration
+
+TODO
+
+## 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/concept/sync/build.gradle b/mobile/android/android-components/components/concept/sync/build.gradle
new file mode 100644
index 0000000000..31a356155a
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/build.gradle
@@ -0,0 +1,37 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.sync'
+}
+
+dependencies {
+ // Necessary because we use 'suspend'. Fun fact: this module will compile just fine without this
+ // dependency, but it will crash at runtime.
+ // Included via 'api' because this module is unusable without coroutines.
+ api ComponentsDependencies.kotlin_coroutines
+
+ // Observables are part of the public API of this module.
+ api project(':support-base')
+}
+
+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/concept/sync/proguard-rules.pro b/mobile/android/android-components/components/concept/sync/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/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/concept/sync/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/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/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt
new file mode 100644
index 0000000000..fe46cc5b90
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt
@@ -0,0 +1,65 @@
+/* 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.concept.sync
+
+/**
+ * Allows monitoring events targeted at the current account/device.
+ */
+interface AccountEventsObserver {
+ /** The callback called when an account event is received */
+ fun onEvents(events: List<AccountEvent>)
+}
+
+typealias OuterDeviceCommandIncoming = DeviceCommandIncoming
+
+/**
+ * Incoming account events.
+ */
+sealed class AccountEvent {
+ /** An incoming command from another device */
+ data class DeviceCommandIncoming(val command: OuterDeviceCommandIncoming) : AccountEvent()
+
+ /** The account's profile was updated */
+ object ProfileUpdated : AccountEvent()
+
+ /** The authentication state of the account changed - eg, the password changed */
+ object AccountAuthStateChanged : AccountEvent()
+
+ /** The account itself was destroyed */
+ object AccountDestroyed : AccountEvent()
+
+ /** Another device connected to the account */
+ data class DeviceConnected(val deviceName: String) : AccountEvent()
+
+ /** A device (possibly this one) disconnected from the account */
+ data class DeviceDisconnected(val deviceId: String, val isLocalDevice: Boolean) : AccountEvent()
+
+ /** An unknown account event. Should be gracefully ignore */
+ object Unknown : AccountEvent()
+}
+
+/**
+ * Incoming device commands (ie, targeted at the current device.)
+ */
+sealed class DeviceCommandIncoming {
+ /** A command to open a list of tabs on the current device */
+ class TabReceived(val from: Device?, val entries: List<TabData>) : DeviceCommandIncoming()
+}
+
+/**
+ * Outgoing device commands (ie, targeted at other devices.)
+ */
+sealed class DeviceCommandOutgoing {
+ /** A command to open a tab on another device */
+ class SendTab(val title: String, val url: String) : DeviceCommandOutgoing()
+}
+
+/**
+ * Information about a tab sent with tab related commands.
+ */
+data class TabData(
+ val title: String,
+ val url: String,
+)
diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt
new file mode 100644
index 0000000000..94b022ce20
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt
@@ -0,0 +1,175 @@
+/* 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.concept.sync
+
+import android.content.Context
+import androidx.annotation.MainThread
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.support.base.observer.Observable
+
+/**
+ * Represents a result of interacting with a backend service which may return an authentication error.
+ */
+sealed class ServiceResult {
+ /**
+ * All good.
+ */
+ object Ok : ServiceResult()
+
+ /**
+ * Auth error.
+ */
+ object AuthError : ServiceResult()
+
+ /**
+ * Error that isn't auth.
+ */
+ object OtherError : ServiceResult()
+}
+
+/**
+ * Describes available interactions with the current device and other devices associated with an [OAuthAccount].
+ */
+interface DeviceConstellation : Observable<AccountEventsObserver> {
+ /**
+ * Perform actions necessary to finalize device initialization based on [authType].
+ * @param authType Type of an authentication event we're experiencing.
+ * @param config A [DeviceConfig] that describes current device.
+ * @return A boolean success flag.
+ */
+ suspend fun finalizeDevice(authType: AuthType, config: DeviceConfig): ServiceResult
+
+ /**
+ * Current state of the constellation. May be missing if state was never queried.
+ * @return [ConstellationState] describes current and other known devices in the constellation.
+ */
+ fun state(): ConstellationState?
+
+ /**
+ * Allows monitoring state of the device constellation via [DeviceConstellationObserver].
+ * Use this to be notified of changes to the current device or other devices.
+ */
+ @MainThread
+ fun registerDeviceObserver(observer: DeviceConstellationObserver, owner: LifecycleOwner, autoPause: Boolean)
+
+ /**
+ * Set name of the current device.
+ * @param name New device name.
+ * @param context An application context, used for updating internal caches.
+ * @return A boolean success flag.
+ */
+ suspend fun setDeviceName(name: String, context: Context): Boolean
+
+ /**
+ * Set a [DevicePushSubscription] for the current device.
+ * @param subscription A new [DevicePushSubscription].
+ * @return A boolean success flag.
+ */
+ suspend fun setDevicePushSubscription(subscription: DevicePushSubscription): Boolean
+
+ /**
+ * Send a command to a specified device.
+ * @param targetDeviceId A device ID of the recipient.
+ * @param outgoingCommand An event to send.
+ * @return A boolean success flag.
+ */
+ suspend fun sendCommandToDevice(targetDeviceId: String, outgoingCommand: DeviceCommandOutgoing): Boolean
+
+ /**
+ * Process a raw event, obtained via a push message or some other out-of-band mechanism.
+ * @param payload A raw, plaintext payload to be processed.
+ * @return A boolean success flag.
+ */
+ suspend fun processRawEvent(payload: String): Boolean
+
+ /**
+ * Refreshes [ConstellationState]. Registered [DeviceConstellationObserver] observers will be notified.
+ *
+ * @return A boolean success flag.
+ */
+ suspend fun refreshDevices(): Boolean
+
+ /**
+ * Polls for any pending [DeviceCommandIncoming] commands.
+ * In case of new commands, registered [AccountEventsObserver] observers will be notified.
+ *
+ * @return A boolean success flag.
+ */
+ suspend fun pollForCommands(): Boolean
+}
+
+/**
+ * Describes current device and other devices in the constellation.
+ */
+// N.B.: currentDevice should not be nullable.
+// See https://github.com/mozilla-mobile/android-components/issues/8768
+data class ConstellationState(val currentDevice: Device?, val otherDevices: List<Device>)
+
+/**
+ * Allows monitoring constellation state.
+ */
+interface DeviceConstellationObserver {
+ fun onDevicesUpdate(constellation: ConstellationState)
+}
+
+/**
+ * Describes a type of the physical device in the constellation.
+ */
+enum class DeviceType {
+ DESKTOP,
+ MOBILE,
+ TABLET,
+ TV,
+ VR,
+ UNKNOWN,
+}
+
+/**
+ * Describes an Autopush-compatible push channel subscription.
+ */
+data class DevicePushSubscription(
+ val endpoint: String,
+ val publicKey: String,
+ val authKey: String,
+)
+
+/**
+ * Configuration for the current device.
+ *
+ * @property name An initial name to use for the device record which will be created during authentication.
+ * This can be changed later via [DeviceConstellation.setDeviceName].
+ * @property type Type of a device - mobile, desktop - used for displaying identifying icons on other devices.
+ * This cannot be changed once device record is created.
+ * @property capabilities A set of device capabilities, such as SEND_TAB.
+ * @property secureStateAtRest A flag indicating whether or not to use encrypted storage for the persisted account
+ * state.
+ */
+data class DeviceConfig(
+ val name: String,
+ val type: DeviceType,
+ val capabilities: Set<DeviceCapability>,
+ val secureStateAtRest: Boolean = false,
+)
+
+/**
+ * Capabilities that a [Device] may have.
+ */
+enum class DeviceCapability {
+ SEND_TAB,
+}
+
+/**
+ * Describes a device in the [DeviceConstellation].
+ */
+data class Device(
+ val id: String,
+ val displayName: String,
+ val deviceType: DeviceType,
+ val isCurrentDevice: Boolean,
+ val lastAccessTime: Long?,
+ val capabilities: List<DeviceCapability>,
+ val subscriptionExpired: Boolean,
+ val subscription: DevicePushSubscription?,
+)
diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt
new file mode 100644
index 0000000000..7737d4bc36
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt
@@ -0,0 +1,358 @@
+/* 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.concept.sync
+
+import kotlinx.coroutines.Deferred
+
+/**
+ * An object that represents a login flow initiated by [OAuthAccount].
+ * @property state OAuth state parameter, identifying a specific authentication flow.
+ * This string is randomly generated during [OAuthAccount.beginOAuthFlow] and [OAuthAccount.beginPairingFlow].
+ * @property url Url which needs to be loaded to go through the authentication flow identified by [state].
+ */
+data class AuthFlowUrl(val state: String, val url: String)
+
+/**
+ * Represents a specific type of an "in-flight" migration state that could result from intermittent
+ * issues during [OAuthAccount.migrateFromAccount].
+ */
+enum class InFlightMigrationState(val reuseSessionToken: Boolean) {
+ /**
+ * "Copy" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionToken].
+ */
+ COPY_SESSION_TOKEN(false),
+
+ /**
+ * "Reuse" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionToken].
+ */
+ REUSE_SESSION_TOKEN(true),
+}
+
+/**
+ * Data structure describing FxA and Sync credentials necessary to sign-in into an FxA account.
+ */
+data class MigratingAccountInfo(
+ val sessionToken: String,
+ val kSync: String,
+ val kXCS: String,
+)
+
+/**
+ * Representing all the possible entry points into FxA
+ *
+ * These entry points will be reflected in the authentication URL and will be tracked
+ * in server telemetry to allow studying authentication entry points independently.
+ *
+ * If you are introducing a new path to the firefox accounts sign in please add a new entry point
+ * here.
+ */
+interface FxAEntryPoint {
+ val entryName: String
+}
+
+/**
+ * Facilitates testing consumers of FirefoxAccount.
+ */
+interface OAuthAccount : AutoCloseable {
+
+ /**
+ * Constructs a URL used to begin the OAuth flow for the requested scopes and keys.
+ *
+ * @param scopes List of OAuth scopes for which the client wants access
+ * @param entryPoint The UI entryPoint used to start this flow. An arbitrary
+ * string which is recorded in telemetry by the server to help analyze the
+ * most effective touchpoints
+ * @return [AuthFlowUrl] if available, `null` in case of a failure
+ */
+ suspend fun beginOAuthFlow(
+ scopes: Set<String>,
+ entryPoint: FxAEntryPoint,
+ ): AuthFlowUrl?
+
+ /**
+ * Constructs a URL used to begin the pairing flow for the requested scopes and pairingUrl.
+ *
+ * @param pairingUrl URL string for pairing
+ * @param scopes List of OAuth scopes for which the client wants access
+ * @param entryPoint The UI entryPoint used to start this flow. An arbitrary
+ * string which is recorded in telemetry by the server to help analyze the
+ * most effective touchpoints
+ * @return [AuthFlowUrl] if available, `null` in case of a failure
+ */
+ suspend fun beginPairingFlow(
+ pairingUrl: String,
+ scopes: Set<String>,
+ entryPoint: FxAEntryPoint,
+ ): AuthFlowUrl?
+
+ /**
+ * Returns current FxA Device ID for an authenticated account.
+ *
+ * @return Current device's FxA ID, if available. `null` otherwise.
+ */
+ fun getCurrentDeviceId(): String?
+
+ /**
+ * Returns session token for an authenticated account.
+ *
+ * @return Current account's session token, if available. `null` otherwise.
+ */
+ fun getSessionToken(): String?
+
+ /**
+ * Fetches the profile object for the current client either from the existing cached state
+ * or from the server (requires the client to have access to the profile scope).
+ *
+ * @param ignoreCache Fetch the profile information directly from the server
+ * @return Profile (optional, if successfully retrieved) representing the user's basic profile info
+ */
+ suspend fun getProfile(ignoreCache: Boolean = false): Profile?
+
+ /**
+ * Authenticates the current account using the [code] and [state] parameters obtained via the
+ * OAuth flow initiated by [beginOAuthFlow].
+ *
+ * Modifies the FirefoxAccount state.
+ * @param code OAuth code string
+ * @param state state token string
+ * @return Deferred boolean representing success or failure
+ */
+ suspend fun completeOAuthFlow(code: String, state: String): Boolean
+
+ /**
+ * Tries to fetch an access token for the given scope.
+ *
+ * @param singleScope Single OAuth scope (no spaces) for which the client wants access
+ * @return [AccessTokenInfo] that stores the token, along with its scope, key and
+ * expiration timestamp (in seconds) since epoch when complete
+ */
+ suspend fun getAccessToken(singleScope: String): AccessTokenInfo?
+
+ /**
+ * Call this whenever an authentication error was encountered while using an access token
+ * issued by [getAccessToken].
+ */
+ fun authErrorDetected()
+
+ /**
+ * This method should be called when a request made with an OAuth token failed with an
+ * authentication error. It will re-build cached state and perform a connectivity check.
+ *
+ * In time, fxalib will grow a similar method, at which point we'll just relay to it.
+ * See https://github.com/mozilla/application-services/issues/1263
+ *
+ * @param singleScope An oauth scope for which to check authorization state.
+ * @return An optional [Boolean] flag indicating if we're connected, or need to go through
+ * re-authentication. A null result means we were not able to determine state at this time.
+ */
+ suspend fun checkAuthorizationStatus(singleScope: String): Boolean?
+
+ /**
+ * Fetches the token server endpoint, for authentication using the SAML bearer flow.
+ *
+ * @return Token server endpoint URL string, `null` if it couldn't be obtained.
+ */
+ suspend fun getTokenServerEndpointURL(): String?
+
+ /**
+ * Fetches the URL for the user to manage their account
+ *
+ * @param entryPoint A string which will be included as a query param in the URL for metrics.
+ * @return The URL which should be opened in a browser tab.
+ */
+ suspend fun getManageAccountURL(entryPoint: FxAEntryPoint): String?
+
+ /**
+ * Get the pairing URL to navigate to on the Authority side (typically a computer).
+ *
+ * @return The URL to show the pairing user
+ */
+ fun getPairingAuthorityURL(): String
+
+ /**
+ * Registers a callback for when the account state gets persisted
+ *
+ * @param callback the account state persistence callback
+ */
+ fun registerPersistenceCallback(callback: StatePersistenceCallback)
+
+ /**
+ * Returns the device constellation for the current account
+ *
+ * @return Device constellation for the current account
+ */
+ fun deviceConstellation(): DeviceConstellation
+
+ /**
+ * Reset internal account state and destroy current device record.
+ * Use this when device record is no longer relevant, e.g. while logging out. On success, other
+ * devices will no longer see the current device in their device lists.
+ *
+ * @return A [Deferred] that will be resolved with a success flag once operation is complete.
+ * Failure indicates that we may have failed to destroy current device record. Nothing to do for
+ * the consumer; device record will be cleaned up eventually via TTL.
+ */
+ suspend fun disconnect(): Boolean
+
+ /**
+ * Serializes the current account's authentication state as a JSON string, for persistence in
+ * the Android KeyStore/shared preferences. The authentication state can be restored using
+ * [FirefoxAccount.fromJSONString].
+ *
+ * @return String containing the authentication details in JSON format
+ */
+ fun toJSONString(): String
+}
+
+/**
+ * Describes a delegate object that is used by [OAuthAccount] to persist its internal state as it changes.
+ */
+interface StatePersistenceCallback {
+ /**
+ * @param data Account state representation as a string (e.g. as json).
+ */
+ fun persist(data: String)
+}
+
+sealed class AuthType {
+ /**
+ * Account restored from hydrated state on disk.
+ */
+ object Existing : AuthType()
+
+ /**
+ * Account created in response to a sign-in.
+ */
+ object Signin : AuthType()
+
+ /**
+ * Account created in response to a sign-up.
+ */
+ object Signup : AuthType()
+
+ /**
+ * Account created via pairing (similar to sign-in, but without requiring credentials).
+ */
+ object Pairing : AuthType()
+
+ /**
+ * Account was created for an unknown external reason, hopefully identified by [action].
+ */
+ data class OtherExternal(val action: String?) : AuthType()
+
+ /**
+ * Account created via a shared account state from another app via the copy token flow.
+ */
+ object MigratedCopy : AuthType()
+
+ /**
+ * Account created via a shared account state from another app via the reuse token flow.
+ */
+ object MigratedReuse : AuthType()
+
+ /**
+ * Existing account was recovered from an authentication problem.
+ */
+ object Recovered : AuthType()
+}
+
+/**
+ * Different types of errors that may be encountered during authorization.
+ * Intermittent network problems are the most common reason for these errors.
+ */
+enum class AuthFlowError {
+ /**
+ * Couldn't begin authorization, i.e. failed to obtain an authorization URL.
+ */
+ FailedToBeginAuth,
+
+ /**
+ * Couldn't complete authorization after user entered valid credentials/paired correctly.
+ */
+ FailedToCompleteAuth,
+}
+
+/**
+ * Observer interface which lets its users monitor account state changes and major events.
+ * (XXX - there's some tension between this and the
+ * mozilla.components.concept.sync.AccountEvent we should resolve!)
+ */
+interface AccountObserver {
+ /**
+ * Account state has been resolved and can now be queried.
+ *
+ * @param authenticatedAccount Currently resolved authenticated account, if any.
+ */
+ fun onReady(authenticatedAccount: OAuthAccount?) = Unit
+
+ /**
+ * Account just got logged out.
+ */
+ fun onLoggedOut() = Unit
+
+ /**
+ * Account was successfully authenticated.
+ *
+ * @param account An authenticated instance of a [OAuthAccount].
+ * @param authType Describes what kind of authentication event caused this invocation.
+ */
+ fun onAuthenticated(account: OAuthAccount, authType: AuthType) = Unit
+
+ /**
+ * Account's profile is now available.
+ * @param profile A fresh version of account's [Profile].
+ */
+ fun onProfileUpdated(profile: Profile) = Unit
+
+ /**
+ * Account needs to be re-authenticated (e.g. due to a password change).
+ */
+ fun onAuthenticationProblems() = Unit
+
+ /**
+ * Encountered an error during an authentication or migration flow.
+ * @param error Exact error encountered.
+ */
+ fun onFlowError(error: AuthFlowError) = Unit
+}
+
+data class Avatar(
+ val url: String,
+ val isDefault: Boolean,
+)
+
+data class Profile(
+ val uid: String?,
+ val email: String?,
+ val avatar: Avatar?,
+ val displayName: String?,
+)
+
+/**
+ * Scoped key data.
+ *
+ * @property kid The JWK key identifier.
+ * @property k The JWK key data.
+ */
+data class OAuthScopedKey(
+ val kty: String,
+ val scope: String,
+ val kid: String,
+ val k: String,
+)
+
+/**
+ * The result of authentication with FxA via an OAuth flow.
+ *
+ * @property token The access token produced by the flow.
+ * @property key An OAuthScopedKey if present.
+ * @property expiresAt The expiry date timestamp of this token since unix epoch (in seconds).
+ */
+data class AccessTokenInfo(
+ val scope: String,
+ val token: String,
+ val key: OAuthScopedKey?,
+ val expiresAt: Long,
+)
diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt
new file mode 100644
index 0000000000..51b24d0752
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt
@@ -0,0 +1,53 @@
+/* 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.concept.sync
+
+/**
+ * Results of running a sync via [SyncableStore.sync].
+ */
+sealed class SyncStatus {
+ /**
+ * Sync succeeded successfully.
+ */
+ object Ok : SyncStatus()
+
+ /**
+ * Sync completed with an error.
+ */
+ data class Error(val exception: Exception) : SyncStatus()
+}
+
+/**
+ * A Firefox Sync friendly auth object which can be obtained from [OAuthAccount].
+ *
+ * Why is there a Firefox Sync-shaped authentication object at the concept level, you ask?
+ * Mainly because this is what the [SyncableStore] consumes in order to actually perform
+ * synchronization, which is in turn implemented by `places`-backed storage layer.
+ * If this class lived in `services-firefox-accounts`, we'd end up with an ugly dependency situation
+ * between services and storage components.
+ *
+ * Turns out that building a generic description of an authentication/synchronization layer is not
+ * quite the way to go when you only have a single, legacy implementation.
+ *
+ * However, this may actually improve once we retire the tokenserver from the architecture.
+ * We could also consider a heavier use of generics, as well.
+ */
+data class SyncAuthInfo(
+ val kid: String,
+ val fxaAccessToken: String,
+ val fxaAccessTokenExpiresAt: Long,
+ val syncKey: String,
+ val tokenServerUrl: String,
+)
+
+/**
+ * Describes a "sync" entry point for a storage layer.
+ */
+interface SyncableStore {
+ /**
+ * Registers this storage with a sync manager.
+ */
+ fun registerWithSyncManager()
+}
diff --git a/mobile/android/android-components/components/concept/tabstray/README.md b/mobile/android/android-components/components/concept/tabstray/README.md
new file mode 100644
index 0000000000..56b5838a55
--- /dev/null
+++ b/mobile/android/android-components/components/concept/tabstray/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Concept > Tabstray
+
+Abstract definition of a tabs tray component.
+
+## Usage
+
+### 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:concept-tabstray:{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/concept/tabstray/build.gradle b/mobile/android/android-components/components/concept/tabstray/build.gradle
new file mode 100644
index 0000000000..67d5ae9d25
--- /dev/null
+++ b/mobile/android/android-components/components/concept/tabstray/build.gradle
@@ -0,0 +1,33 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.tabstray'
+}
+
+dependencies {
+ api project(':concept-engine')
+
+ implementation project(':support-base')
+}
+
+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/concept/tabstray/proguard-rules.pro b/mobile/android/android-components/components/concept/tabstray/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/tabstray/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/concept/tabstray/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/tabstray/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/tabstray/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/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/Tab.kt b/mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/Tab.kt
new file mode 100644
index 0000000000..4de7bc7fe6
--- /dev/null
+++ b/mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/Tab.kt
@@ -0,0 +1,42 @@
+/* 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.concept.tabstray
+
+import android.graphics.Bitmap
+import mozilla.components.concept.engine.mediasession.MediaSession
+
+/**
+ * Data class representing a tab to be displayed in a [TabsTray].
+ *
+ * @property id Unique ID of the tab.
+ * @property url Current URL of the tab.
+ * @property title Current title of the tab (or an empty [String]]).
+ * @property private whether or not the session is private.
+ * @property icon Current icon of the tab (or null)
+ * @property thumbnail Current thumbnail of the tab (or null)
+ * @property playbackState Current media session playback state for the tab (or null)
+ * @property controller Current media session controller for the tab (or null)
+ * @property lastAccess The last time this tab was selected.
+ * @property createdAt When the tab was first created.
+ * @property searchTerm the last used search term for this tab or from the originating tab, or an
+ * empty string if no search was executed.
+ */
+@Deprecated(
+ "This will be removed in a future release",
+ ReplaceWith("TabSessionState", "mozilla.components.browser.state.state"),
+)
+data class Tab(
+ val id: String,
+ val url: String,
+ val title: String = "",
+ val private: Boolean = false,
+ val icon: Bitmap? = null,
+ val thumbnail: Bitmap? = null,
+ val playbackState: MediaSession.PlaybackState? = null,
+ val controller: MediaSession.Controller? = null,
+ val lastAccess: Long = 0L,
+ val createdAt: Long = 0L,
+ val searchTerm: String = "",
+)
diff --git a/mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/Tabs.kt b/mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/Tabs.kt
new file mode 100644
index 0000000000..a6d83d4297
--- /dev/null
+++ b/mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/Tabs.kt
@@ -0,0 +1,21 @@
+/* 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.concept.tabstray
+
+/**
+ * Aggregate data type keeping a reference to the list of tabs and the index of the selected tab.
+ *
+ * @property list The list of tabs.
+ * @property selectedTabId Id of the selected tab in the list of tabs (or null).
+ */
+@Deprecated(
+ "This will be removed in future versions",
+ ReplaceWith("TabList", "mozilla.components.feature.tabs.tabstray"),
+)
+@Suppress("Deprecation")
+data class Tabs(
+ val list: List<Tab>,
+ val selectedTabId: String?,
+)
diff --git a/mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/TabsTray.kt b/mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/TabsTray.kt
new file mode 100644
index 0000000000..0b85de4f74
--- /dev/null
+++ b/mobile/android/android-components/components/concept/tabstray/src/main/java/mozilla/components/concept/tabstray/TabsTray.kt
@@ -0,0 +1,44 @@
+/* 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.concept.tabstray
+
+import mozilla.components.support.base.observer.Observable
+
+/**
+ * Generic interface for components that provide "tabs tray" functionality.
+ */
+@Deprecated("This will be removed in a future release", ReplaceWith("TabsTray", "mozilla.components.browser.tabstray"))
+@Suppress("Deprecation")
+interface TabsTray : Observable<TabsTray.Observer> {
+ /**
+ * Interface to be implemented by classes that want to observe a tabs tray.
+ */
+ interface Observer {
+ /**
+ * One or many tabs have been added or removed.
+ */
+ fun onTabsUpdated() = Unit
+
+ /**
+ * A new tab has been selected.
+ */
+ fun onTabSelected(tab: Tab)
+
+ /**
+ * A tab has been closed.
+ */
+ fun onTabClosed(tab: Tab)
+ }
+
+ /**
+ * Updates the list of tabs.
+ */
+ fun updateTabs(tabs: Tabs)
+
+ /**
+ * Called when binding a new item to get if it should be shown as selected or not.
+ */
+ fun isTabSelected(tabs: Tabs, position: Int): Boolean
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/README.md b/mobile/android/android-components/components/concept/toolbar/README.md
new file mode 100644
index 0000000000..60e050ce0d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Concept > Toolbar
+
+Abstract definition of a browser toolbar component.
+
+## Usage
+
+### 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:concept-toolbar:{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/concept/toolbar/build.gradle b/mobile/android/android-components/components/concept/toolbar/build.gradle
new file mode 100644
index 0000000000..54e303cd11
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/build.gradle
@@ -0,0 +1,39 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.toolbar'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_core_ktx
+ api project(':support-base')
+ implementation project(':support-ktx')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':support-test')
+}
+
+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/concept/toolbar/proguard-rules.pro b/mobile/android/android-components/components/concept/toolbar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/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/concept/toolbar/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/toolbar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/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/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteDelegate.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteDelegate.kt
new file mode 100644
index 0000000000..ec68a17633
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteDelegate.kt
@@ -0,0 +1,24 @@
+/* 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.concept.toolbar
+
+/**
+ * Describes an object to which a [AutocompleteResult] may be applied.
+ * Usually, this will delegate to a specific text view.
+ */
+interface AutocompleteDelegate {
+ /**
+ * @param result Apply result of autocompletion.
+ * @param onApplied a lambda/callback invoked if (and only if) the result has been
+ * applied. A result may be discarded by implementations because it is stale or
+ * the autocomplete request has been cancelled.
+ */
+ fun applyAutocompleteResult(result: AutocompleteResult, onApplied: () -> Unit = { })
+
+ /**
+ * Autocompletion was invoked and no match was returned.
+ */
+ fun noAutocompleteResult(input: String)
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteProvider.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteProvider.kt
new file mode 100644
index 0000000000..0534bbc007
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteProvider.kt
@@ -0,0 +1,36 @@
+/* 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.concept.toolbar
+
+/**
+ * Object providing autocomplete suggestions for the toolbar.
+ * More such objects can be set for the same toolbar with each getting results from a different source.
+ * If more providers are used the [autocompletePriority] property allows to easily set an order
+ * for the results and the suggestion of which provider should be tried to be applied first.
+ */
+interface AutocompleteProvider : Comparable<AutocompleteProvider> {
+ /**
+ * Retrieves an autocomplete suggestion which best matches [query].
+ *
+ * @param query Segment of text to be autocompleted.
+ *
+ * @return Optional domain URL which best matches the query.
+ */
+ suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult?
+
+ /**
+ * Order in which this provider will be queried for autocomplete suggestions in relation ot others.
+ * - a lower priority means that this provider must be called before others with a higher priority.
+ * - an equal priority offers no ordering guarantees.
+ *
+ * Defaults to `0`.
+ */
+ val autocompletePriority: Int
+ get() = 0
+
+ override fun compareTo(other: AutocompleteProvider): Int {
+ return autocompletePriority - other.autocompletePriority
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteResult.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteResult.kt
new file mode 100644
index 0000000000..145188c4d4
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteResult.kt
@@ -0,0 +1,22 @@
+/* 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.concept.toolbar
+
+/**
+ * Describes an autocompletion result.
+ *
+ * @property input Input for which this AutocompleteResult is being provided.
+ * @property text AutocompleteResult of autocompletion, text to be displayed.
+ * @property url AutocompleteResult of autocompletion, full matching url.
+ * @property source Name of the autocompletion source.
+ * @property totalItems A total number of results also available.
+ */
+data class AutocompleteResult(
+ val input: String,
+ val text: String,
+ val url: String,
+ val source: String,
+ val totalItems: Int,
+)
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/ScrollableToolbar.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/ScrollableToolbar.kt
new file mode 100644
index 0000000000..86af351c26
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/ScrollableToolbar.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.concept.toolbar
+
+/**
+ * Interface to be implemented by components that provide hiding-on-scroll toolbar functionality.
+ */
+interface ScrollableToolbar {
+
+ /**
+ * Enable scrolling of the dynamic toolbar. Restore this functionality after [disableScrolling] stopped it.
+ *
+ * The toolbar may have other intrinsic checks depending on which the toolbar will be animated or not.
+ */
+ fun enableScrolling()
+
+ /**
+ * Completely disable scrolling of the dynamic toolbar.
+ * Use [enableScrolling] to restore the functionality.
+ */
+ fun disableScrolling()
+
+ /**
+ * Force the toolbar to expand.
+ */
+ fun expand()
+
+ /**
+ * Force the toolbar to collapse. Only if dynamic.
+ */
+ fun collapse()
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt
new file mode 100644
index 0000000000..55244b4f6b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt
@@ -0,0 +1,563 @@
+/* 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.concept.toolbar
+
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.view.View.NO_ID
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import androidx.annotation.ColorRes
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.ktx.android.view.setPadding
+import java.lang.ref.WeakReference
+
+/**
+ * Interface to be implemented by components that provide browser toolbar functionality.
+ */
+@Suppress("TooManyFunctions")
+interface Toolbar : ScrollableToolbar {
+ /**
+ * Sets/Gets the title to be displayed on the toolbar.
+ */
+ var title: String
+
+ /**
+ * Sets/Gets the URL to be displayed on the toolbar.
+ */
+ var url: CharSequence
+
+ /**
+ * Sets/gets private mode.
+ *
+ * In private mode the IME should not update any personalized data such as typing history and personalized language
+ * model based on what the user typed.
+ */
+ var private: Boolean
+
+ /**
+ * Sets/Gets the site security to be displayed on the toolbar.
+ */
+ var siteSecure: SiteSecurity
+
+ /**
+ * Sets/Gets the highlight icon to be displayed on the toolbar.
+ */
+ var highlight: Highlight
+
+ /**
+ * Sets/Gets the site tracking protection state to be displayed on the toolbar.
+ */
+ var siteTrackingProtection: SiteTrackingProtection
+
+ /**
+ * Displays the currently used search terms as part of this Toolbar.
+ *
+ * @param searchTerms the search terms used by the current session
+ */
+ fun setSearchTerms(searchTerms: String)
+
+ /**
+ * Displays the given loading progress. Expects values in the range [0, 100].
+ */
+ fun displayProgress(progress: Int)
+
+ /**
+ * Should be called by an activity when the user pressed the back key of the device.
+ *
+ * @return Returns true if the back press event was handled and should not be propagated further.
+ */
+ fun onBackPressed(): Boolean
+
+ /**
+ * Should be called by the host activity when it enters the stop state.
+ */
+ fun onStop()
+
+ /**
+ * Registers the given function to be invoked when the user selected a new URL i.e. is done
+ * editing.
+ *
+ * If the function returns `true` then the toolbar will automatically switch to "display mode". Otherwise it
+ * remains in "edit mode".
+ *
+ * @param listener the listener function
+ */
+ fun setOnUrlCommitListener(listener: (String) -> Boolean)
+
+ /**
+ * Registers the given function to be invoked when users changes text in the toolbar.
+ *
+ * @param filter A function which will perform autocompletion and send results to [AutocompleteDelegate].
+ */
+ fun setAutocompleteListener(filter: suspend (String, AutocompleteDelegate) -> Unit)
+
+ /**
+ * Attempt to restart the autocomplete functionality with the current user input.
+ */
+ fun refreshAutocomplete() = Unit
+
+ /**
+ * Adds an action to be displayed on the right side of the toolbar in display mode.
+ *
+ * Related:
+ * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Browser_action
+ */
+ fun addBrowserAction(action: Action)
+
+ /**
+ * Removes a previously added browser action (see [addBrowserAction]). If the the provided
+ * actions was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ fun removeBrowserAction(action: Action)
+
+ /**
+ * Removes a previously added page action (see [addBrowserAction]). If the the provided
+ * actions was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ fun removePageAction(action: Action)
+
+ /**
+ * Removes a previously added navigation action (see [addNavigationAction]). If the the provided
+ * actions was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ fun removeNavigationAction(action: Action)
+
+ /**
+ * Declare that the actions (navigation actions, browser actions, page actions) have changed and
+ * should be updated if needed.
+ */
+ fun invalidateActions()
+
+ /**
+ * Adds an action to be displayed on the right side of the URL in display mode.
+ *
+ * Related:
+ * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Page_actions
+ */
+ fun addPageAction(action: Action)
+
+ /**
+ * Adds an action to be displayed on the far left side of the URL in display mode.
+ */
+ fun addNavigationAction(action: Action)
+
+ /**
+ * Adds an action to be displayed at the start of the URL in edit mode.
+ */
+ fun addEditActionStart(action: Action)
+
+ /**
+ * Adds an action to be displayed at the end of the URL in edit mode.
+ */
+ fun addEditActionEnd(action: Action)
+
+ /**
+ * Removes an action at the end of the URL in edit mode.
+ */
+ fun removeEditActionEnd(action: Action)
+
+ /**
+ * Hides the menu button in display mode.
+ */
+ fun hideMenuButton()
+
+ /**
+ * Shows the menu button in display mode.
+ */
+ fun showMenuButton()
+
+ /**
+ * Sets the horizontal padding in display mode.
+ */
+ fun setDisplayHorizontalPadding(horizontalPadding: Int)
+
+ /**
+ * Hides the page action separator in display mode.
+ */
+ fun hidePageActionSeparator()
+
+ /**
+ * Shows the page action separator in display mode.
+ */
+ fun showPageActionSeparator()
+
+ /**
+ * Casts this toolbar to an Android View object.
+ */
+ fun asView(): View = this as View
+
+ /**
+ * Registers the given listener to be invoked when the user edits the URL.
+ */
+ fun setOnEditListener(listener: OnEditListener)
+
+ /**
+ * Switches to URL displaying mode (from editing mode) if supported by the toolbar implementation.
+ */
+ fun displayMode()
+
+ /**
+ * Switches to URL editing mode (from display mode) if supported by the toolbar implementation,
+ * and focuses the URL input field based on the cursor selection.
+ *
+ * @param cursorPlacement Where the cursor should be set after focusing on the URL input field.
+ */
+ fun editMode(cursorPlacement: CursorPlacement = CursorPlacement.ALL)
+
+ /**
+ * Dismisses the display toolbar popup menu
+ */
+ fun dismissMenu()
+
+ /**
+ * Listener to be invoked when the user edits the URL.
+ */
+ interface OnEditListener {
+ /**
+ * Fired when the toolbar switches to edit mode.
+ */
+ fun onStartEditing() = Unit
+
+ /**
+ * Fired when the user presses the back button while in edit mode.
+ */
+ fun onCancelEditing(): Boolean = true
+
+ /**
+ * Fired when the toolbar switches back to display mode.
+ */
+ fun onStopEditing() = Unit
+
+ /**
+ * Fired whenever the user changes the text in the address bar.
+ */
+ fun onTextChanged(text: String) = Unit
+
+ /**
+ * Fired when user clears input by tapping the clear input button.
+ */
+ fun onInputCleared() = Unit
+ }
+
+ /**
+ * Generic interface for actions to be added to the toolbar.
+ */
+ interface Action {
+ val visible: () -> Boolean
+ get() = { true }
+
+ val autoHide: () -> Boolean
+ get() = { false }
+
+ val weight: () -> Int
+ get() = { -1 }
+
+ fun createView(parent: ViewGroup): View
+
+ fun bind(view: View)
+ }
+
+ /**
+ * An action button to be added to the toolbar.
+ *
+ * @param imageDrawable The drawable to be shown.
+ * @param contentDescription The content description to use.
+ * @param visible Lambda that returns true or false to indicate whether this button should be shown.
+ * @param autoHide Lambda that returns true or false to indicate whether this button should auto hide.
+ * @param weight Lambda that returns an integer to indicate weight of an action. The lesser the weight,
+ * the closer it is to the url. A default weight -1 indicates, the position is not cared for
+ * and action will be appended at the end.
+ * @param padding A optional custom padding.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ * @param longClickListener Callback that will be invoked whenever the button is long-pressed.
+ * @param listener Callback that will be invoked whenever the button is pressed
+ */
+ open class ActionButton(
+ val imageDrawable: Drawable? = null,
+ val contentDescription: String,
+ override val visible: () -> Boolean = { true },
+ override val autoHide: () -> Boolean = { false },
+ override val weight: () -> Int = { -1 },
+ private val background: Int = 0,
+ private val padding: Padding? = null,
+ @ColorRes val iconTintColorResource: Int = ViewGroup.NO_ID,
+ private val longClickListener: (() -> Unit)? = null,
+ private val listener: () -> Unit,
+ ) : Action {
+ private var view: WeakReference<AppCompatImageButton>? = null
+
+ override fun createView(parent: ViewGroup): View =
+ AppCompatImageButton(parent.context).also { imageButton ->
+ view = WeakReference(imageButton)
+
+ imageButton.setImageDrawable(imageDrawable)
+ imageButton.contentDescription = contentDescription
+ imageButton.setTintResource(iconTintColorResource)
+ imageButton.setOnClickListener { listener.invoke() }
+ imageButton.setOnLongClickListener {
+ longClickListener?.invoke()
+ true
+ }
+ imageButton.isLongClickable = longClickListener != null
+
+ val backgroundResource = if (background == 0) {
+ parent.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless)
+ } else {
+ background
+ }
+
+ imageButton.setBackgroundResource(backgroundResource)
+ padding?.let { imageButton.setPadding(it) }
+ }
+
+ /**
+ * Changes the content description and the tint colour of the view.
+ *
+ * @param contentDescription The content description to use.
+ * @param tintColorResource ID of color resource to tint the icon.
+ */
+ fun updateView(
+ contentDescription: String? = null,
+ @ColorRes tintColorResource: Int = ViewGroup.NO_ID,
+ ) {
+ view?.get()?.let {
+ it.contentDescription = contentDescription
+ it.setTintResource(tintColorResource)
+ }
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ /**
+ * An action button with two states, selected and unselected. When the button is pressed, the
+ * state changes automatically.
+ *
+ * @param imageDrawable The drawable to be shown if the button is in unselected state.
+ * @param imageSelectedDrawable The drawable to be shown if the button is in selected state.
+ * @param contentDescription The content description to use if the button is in unselected state.
+ * @param contentDescriptionSelected The content description to use if the button is in selected state.
+ * @param visible Lambda that returns true or false to indicate whether this button should be shown.
+ * @param weight Lambda that returns an integer to indicate weight of an action. The lesser the weight,
+ * the closer it is to the url. A default weight -1 indicates, the position is not cared for
+ * and action will be appended at the end.
+ * @param selected Sets whether this button should be selected initially.
+ * @param padding A optional custom padding.
+ * @param listener Callback that will be invoked whenever the checked state changes.
+ */
+ open class ActionToggleButton(
+ internal val imageDrawable: Drawable,
+ internal val imageSelectedDrawable: Drawable,
+ private val contentDescription: String,
+ private val contentDescriptionSelected: String,
+ override val visible: () -> Boolean = { true },
+ override val weight: () -> Int = { -1 },
+ private var selected: Boolean = false,
+ @DrawableRes private val background: Int = 0,
+ private val padding: Padding? = null,
+ private val listener: (Boolean) -> Unit,
+ ) : Action {
+ private var view: WeakReference<ImageButton>? = null
+
+ override fun createView(parent: ViewGroup): View = AppCompatImageButton(parent.context).also { imageButton ->
+ view = WeakReference(imageButton)
+
+ imageButton.scaleType = ImageView.ScaleType.CENTER
+ imageButton.setOnClickListener { toggle() }
+ imageButton.isSelected = selected
+
+ updateViewState()
+
+ val backgroundResource = if (background == 0) {
+ parent.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless)
+ } else {
+ background
+ }
+
+ imageButton.setBackgroundResource(backgroundResource)
+ padding?.let { imageButton.setPadding(it) }
+ }
+
+ /**
+ * Changes the selected state of the action to the inverse of its current state.
+ *
+ * @param notifyListener If true (default) the listener will be notified about the state change.
+ */
+ fun toggle(notifyListener: Boolean = true) {
+ setSelected(!selected, notifyListener)
+ }
+
+ /**
+ * Changes the selected state of the action.
+ *
+ * @param selected The new selected state
+ * @param notifyListener If true (default) the listener will be notified about a state change.
+ */
+ fun setSelected(selected: Boolean, notifyListener: Boolean = true) {
+ if (this.selected == selected) {
+ // Nothing to do here.
+ return
+ }
+
+ this.selected = selected
+ updateViewState()
+
+ if (notifyListener) {
+ listener.invoke(selected)
+ }
+ }
+
+ /**
+ * Returns the current selected state of the action.
+ */
+ fun isSelected() = selected
+
+ private fun updateViewState() {
+ view?.get()?.let {
+ it.isSelected = selected
+
+ if (selected) {
+ it.setImageDrawable(imageSelectedDrawable)
+ it.contentDescription = contentDescriptionSelected
+ } else {
+ it.setImageDrawable(imageDrawable)
+ it.contentDescription = contentDescription
+ }
+ }
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ /**
+ * An "empty" action with a desired width to be used as "placeholder".
+ *
+ * @param desiredWidth The desired width in density independent pixels for this action.
+ * @param padding A optional custom padding.
+ */
+ open class ActionSpace(
+ @Dimension(unit = DP) private val desiredWidth: Int,
+ private val padding: Padding? = null,
+ ) : Action {
+ override fun createView(parent: ViewGroup): View = View(parent.context).apply {
+ minimumWidth = desiredWidth
+ padding?.let { this.setPadding(it) }
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ /**
+ * An action that just shows a static, non-clickable image.
+ *
+ * @param imageDrawable The drawable to be shown.
+ * @param contentDescription Optional content description to be used. If no content description
+ * is provided then this view will be treated as not important for
+ * accessibility.
+ * @param padding A optional custom padding.
+ */
+ open class ActionImage(
+ private val imageDrawable: Drawable,
+ private val contentDescription: String? = null,
+ private val padding: Padding? = null,
+ ) : Action {
+
+ override fun createView(parent: ViewGroup): View = AppCompatImageView(parent.context).also { image ->
+ image.minimumWidth = imageDrawable.intrinsicWidth
+ image.setImageDrawable(imageDrawable)
+
+ image.contentDescription = contentDescription
+ image.importantForAccessibility = if (contentDescription.isNullOrEmpty()) {
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ } else {
+ View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ }
+ padding?.let { pd -> image.setPadding(pd) }
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ enum class SiteSecurity {
+ INSECURE,
+ SECURE,
+ }
+
+ /**
+ * Indicates which tracking protection status a site has.
+ */
+ enum class SiteTrackingProtection {
+ /**
+ * The site has tracking protection enabled, but none trackers have been blocked or detected.
+ */
+ ON_NO_TRACKERS_BLOCKED,
+
+ /**
+ * The site has tracking protection enabled, and trackers have been blocked or detected.
+ */
+ ON_TRACKERS_BLOCKED,
+
+ /**
+ * Tracking protection has been disabled for a specific site.
+ */
+ OFF_FOR_A_SITE,
+
+ /**
+ * Tracking protection has been disabled for all sites.
+ */
+ OFF_GLOBALLY,
+ }
+
+ /**
+ * Indicates the reason why a highlight icon is shown or hidden.
+ */
+ enum class Highlight {
+ /**
+ * The site has changed its permissions from their default values.
+ */
+ PERMISSIONS_CHANGED,
+
+ /**
+ * The site does not show a dot indicator.
+ */
+ NONE,
+ }
+
+ /**
+ * Indicates where the cursor should be set after focusing on the URL input field.
+ */
+ enum class CursorPlacement {
+ /**
+ * All of the text in the input field should be selected.
+ */
+ ALL,
+
+ /**
+ * No text should be selected and the cursor should be placed at the end of the text.
+ */
+ END,
+ }
+}
+
+private fun AppCompatImageButton.setTintResource(@ColorRes tintColorResource: Int) {
+ if (tintColorResource != NO_ID) {
+ imageTintList = ContextCompat.getColorStateList(context, tintColorResource)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionButtonTest.kt b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionButtonTest.kt
new file mode 100644
index 0000000000..ddcbccdfe9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionButtonTest.kt
@@ -0,0 +1,76 @@
+/* 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.concept.toolbar
+
+import android.widget.LinearLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ActionButtonTest {
+
+ @Test
+ fun `set padding`() {
+ var button = Toolbar.ActionButton(mock(), "imageResource") {}
+ val linearLayout = LinearLayout(testContext)
+ var view = button.createView(linearLayout)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ button = Toolbar.ActionButton(
+ mock(),
+ "imageResource",
+ padding = Padding(16, 20, 24, 28),
+ ) {}
+
+ view = button.createView(linearLayout)
+ view.paddingLeft
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+
+ @Test
+ fun `constructor with drawables`() {
+ val visibilityListener = { false }
+ val button = Toolbar.ActionButton(
+ mock(),
+ "image",
+ visibilityListener,
+ { false },
+ { -1 },
+ 0,
+ null,
+ ) { }
+ assertNotNull(button.imageDrawable)
+ assertEquals("image", button.contentDescription)
+ assertEquals(visibilityListener, button.visible)
+ assertEquals(Unit, button.bind(mock()))
+
+ val buttonVisibility = Toolbar.ActionButton(mock(), "image") {}
+ assertEquals(true, buttonVisibility.visible())
+ }
+
+ @Test
+ fun `set contentDescription`() {
+ val button = Toolbar.ActionButton(mock(), "image") { }
+ val linearLayout = LinearLayout(testContext)
+ val view = button.createView(linearLayout)
+
+ button.updateView("contentDescription")
+
+ assertEquals("contentDescription", view.contentDescription)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionImageTest.kt b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionImageTest.kt
new file mode 100644
index 0000000000..2992103063
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionImageTest.kt
@@ -0,0 +1,84 @@
+/* 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.concept.toolbar
+
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class ActionImageTest {
+
+ @Test
+ fun `setting minimumWidth`() {
+ val drawable: Drawable = mock()
+ val image = Toolbar.ActionImage(drawable)
+ val emptyImage = Toolbar.ActionImage(mock())
+
+ val viewGroup: ViewGroup = mock()
+ `when`(viewGroup.context).thenReturn(testContext)
+ `when`(drawable.intrinsicWidth).thenReturn(5)
+
+ val emptyImageView = emptyImage.createView(viewGroup)
+ assertEquals(0, emptyImageView.minimumWidth)
+
+ val imageView = image.createView(viewGroup)
+ assertTrue(imageView.minimumWidth != 0)
+ }
+
+ @Test
+ fun `accessibility description provided`() {
+ val image = Toolbar.ActionImage(mock())
+ var imageAccessible = Toolbar.ActionImage(mock(), "image")
+ val viewGroup: ViewGroup = mock()
+ `when`(viewGroup.context).thenReturn(testContext)
+
+ val imageView = image.createView(viewGroup)
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO, imageView.importantForAccessibility)
+
+ var imageViewAccessible = imageAccessible.createView(viewGroup)
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, imageViewAccessible.importantForAccessibility)
+
+ imageAccessible = Toolbar.ActionImage(mock(), "")
+ imageViewAccessible = imageAccessible.createView(viewGroup)
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO, imageViewAccessible.importantForAccessibility)
+ }
+
+ @Test
+ fun `bind is not implemented`() {
+ val button = Toolbar.ActionImage(mock())
+ assertEquals(Unit, button.bind(mock()))
+ }
+
+ @Test
+ fun `padding is set`() {
+ var image = Toolbar.ActionImage(mock())
+ val viewGroup: ViewGroup = mock()
+ `when`(viewGroup.context).thenReturn(testContext)
+ var view = image.createView(viewGroup)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ image = Toolbar.ActionImage(mock(), padding = Padding(16, 20, 24, 28))
+
+ view = image.createView(viewGroup)
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionSpaceTest.kt b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionSpaceTest.kt
new file mode 100644
index 0000000000..6c4d3da1b9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionSpaceTest.kt
@@ -0,0 +1,47 @@
+/* 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.concept.toolbar
+
+import android.widget.LinearLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ActionSpaceTest {
+
+ @Test
+ fun `Toolbar ActionSpace must set padding`() {
+ var space = Toolbar.ActionSpace(0)
+ val linearLayout = LinearLayout(testContext)
+ var view = space.createView(linearLayout)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ space = Toolbar.ActionSpace(
+ 0,
+ padding = Padding(16, 20, 24, 28),
+ )
+
+ view = space.createView(linearLayout)
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+
+ @Test
+ fun `bind is not implemented`() {
+ val button = Toolbar.ActionSpace(0)
+ assertEquals(Unit, button.bind(mock()))
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionToggleButtonTest.kt b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionToggleButtonTest.kt
new file mode 100644
index 0000000000..0c47f626c5
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionToggleButtonTest.kt
@@ -0,0 +1,213 @@
+/* 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.concept.toolbar
+
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.UUID
+
+@RunWith(AndroidJUnit4::class)
+class ActionToggleButtonTest {
+
+ @Test
+ fun `clicking view will toggle state`() {
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {}
+ val view = button.createView(FrameLayout(testContext))
+
+ assertFalse(button.isSelected())
+
+ view.performClick()
+
+ assertTrue(button.isSelected())
+
+ view.performClick()
+
+ assertFalse(button.isSelected())
+ }
+
+ @Test
+ fun `clicking view will invoke listener`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ val view = button.createView(FrameLayout(testContext))
+
+ assertFalse(listenerInvoked)
+
+ view.performClick()
+
+ assertTrue(listenerInvoked)
+ }
+
+ @Test
+ fun `toggle will invoke listener`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(listenerInvoked)
+
+ button.toggle()
+
+ assertTrue(listenerInvoked)
+ }
+
+ @Test
+ fun `toggle will not invoke listener if notifyListener is set to false`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(listenerInvoked)
+
+ button.toggle(notifyListener = false)
+
+ assertFalse(listenerInvoked)
+ }
+
+ @Test
+ fun `setSelected will invoke listener`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(button.isSelected())
+ assertFalse(listenerInvoked)
+
+ button.setSelected(true)
+
+ assertTrue(listenerInvoked)
+ }
+
+ @Test
+ fun `setSelected will not invoke listener if value has not changed`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(button.isSelected())
+ assertFalse(listenerInvoked)
+
+ button.setSelected(false)
+
+ assertFalse(listenerInvoked)
+ }
+
+ @Test
+ fun `setSelected will not invoke listener if notifyListener is set to false`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(button.isSelected())
+ assertFalse(listenerInvoked)
+
+ button.setSelected(true, notifyListener = false)
+
+ assertFalse(listenerInvoked)
+ }
+
+ @Test
+ fun `isSelected will always return correct state`() {
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {}
+ assertFalse(button.isSelected())
+
+ button.toggle()
+ assertTrue(button.isSelected())
+
+ button.setSelected(true)
+ assertTrue(button.isSelected())
+
+ button.setSelected(false)
+ assertFalse(button.isSelected())
+
+ button.setSelected(true, notifyListener = false)
+ assertTrue(button.isSelected())
+
+ button.toggle(notifyListener = false)
+ assertFalse(button.isSelected())
+
+ val view = button.createView(FrameLayout(testContext))
+ view.performClick()
+ assertTrue(button.isSelected())
+ }
+
+ @Test
+ fun `Toolbar ActionToggleButton must set padding`() {
+ var button = Toolbar.ActionToggleButton(mock(), mock(), "imageResource", "") {}
+ val linearLayout = LinearLayout(testContext)
+ var view = button.createView(linearLayout)
+ val padding = Padding(16, 20, 24, 28)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ button = Toolbar.ActionToggleButton(mock(), mock(), "imageResource", "", padding = padding) {}
+
+ view = button.createView(linearLayout)
+ view.paddingLeft
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+
+ @Test
+ fun `default constructor with drawables`() {
+ var selectedValue = false
+ val visibility = { true }
+ val button = Toolbar.ActionToggleButton(mock(), mock(), "image", "selected", visible = visibility) { value ->
+ selectedValue = value
+ }
+ assertEquals(true, button.visible())
+ assertNotNull(button.imageDrawable)
+ assertNotNull(button.imageSelectedDrawable)
+ assertEquals(visibility, button.visible)
+ button.setSelected(true)
+ assertTrue(selectedValue)
+
+ val buttonVisibility = Toolbar.ActionToggleButton(mock(), mock(), "image", "selected", background = 0) { }
+ assertTrue(buttonVisibility.visible())
+ }
+
+ @Test
+ fun `bind is not implemented`() {
+ val button = Toolbar.ActionToggleButton(mock(), mock(), "image", "imageSelected") {}
+ assertEquals(Unit, button.bind(mock()))
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/resources/robolectric.properties b/mobile/android/android-components/components/concept/toolbar/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/accounts-push/README.md b/mobile/android/android-components/components/feature/accounts-push/README.md
new file mode 100644
index 0000000000..385500c232
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/README.md
@@ -0,0 +1,64 @@
+# [Android Components](../../../README.md) > Feature > Accounts-Push
+
+Feature component for sending tabs to other devices with a registered FxA Account.
+
+## Usage
+
+### 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-accounts-push:{latest-version}"
+```
+
+## Usage
+
+In order to make use of the send tab features here, it's required to have an an FxA Account setup.
+See the [service-firefox-accounts](../../service/firefox-accounts/README.md) for more information how to set this up.
+
+```kotlin
+
+val sendTabUseCases = SendTabUseCases(fxaAccountManager)
+
+// Send to a particular device
+sendTabUseCases.sendToDeviceAsync("1234", TabData("Mozilla", "https://mozilla.org"))
+
+// Send to all devices
+sendTabUseCases.sendToAllAsync(TabData("Mozilla", "https://mozilla.org"))
+
+// Send multiple tabs to devices works too..
+sendTabUseCases.sendToDeviceAsync("1234", listof(tab1, tab2))
+sendTabUseCases.sendToAllAsync(listof(tab1, tab2))
+
+```
+
+To receive tabs:
+
+```kotlin
+SendTabFeature(fxaAccountManager) { device, tabs ->
+ // handle tab data here.
+}
+```
+
+### Push Support
+
+Over time, push registration information can become stale on an FxA server in various ways,
+(e.g. registration expires from long intervals of no use).
+
+To counter this, the FxA server can notify us on the next sync to force registration to occur
+in order to fix the problem. Therefore, if FxA and Push are used together,
+include the support feature as well:
+
+```kotlin
+val feature = FxaPushSupportFeature(context, fxaAccountManager, autoPushFeature)
+
+// initialize the feature; this can be done immediately if needed.
+feature.initalize()
+```
+
+## 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/accounts-push/build.gradle b/mobile/android/android-components/components/feature/accounts-push/build.gradle
new file mode 100644
index 0000000000..c87fa7582a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/build.gradle
@@ -0,0 +1,61 @@
+/* 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'
+
+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.accounts.push'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':service-firefox-accounts')
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':concept-push')
+ implementation project(':feature-push')
+
+ implementation ComponentsDependencies.androidx_work_runtime
+ implementation ComponentsDependencies.androidx_lifecycle_process
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/accounts-push/proguard-rules.pro b/mobile/android/android-components/components/feature/accounts-push/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/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/accounts-push/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/accounts-push/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/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/accounts-push/src/main/java/mozilla/components/feature/accounts/push/FxaPushSupportFeature.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/FxaPushSupportFeature.kt
new file mode 100644
index 0000000000..41821619e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/FxaPushSupportFeature.kt
@@ -0,0 +1,392 @@
+/* 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.accounts.push
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.DeviceConstellationObserver
+import mozilla.components.concept.sync.DevicePushSubscription
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.feature.accounts.push.cache.PushScopeProperty
+import mozilla.components.feature.push.AutoPushFeature
+import mozilla.components.feature.push.AutoPushSubscription
+import mozilla.components.feature.push.PushScope
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.ext.withConstellation
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.SharedPreferencesCache
+import org.json.JSONObject
+import mozilla.components.concept.sync.AccountObserver as SyncAccountObserver
+
+internal const val PREFERENCE_NAME = "mozac_feature_accounts_push"
+internal const val PREF_LAST_VERIFIED = "last_verified_push_subscription"
+internal const val PREF_FXA_SCOPE = "fxa_push_scope"
+
+/**
+ * A feature used for supporting FxA and push integration where needed. One of the main functions is when FxA notifies
+ * the device during a sync, that it's unable to reach the device via push messaging; triggering a push
+ * registration renewal.
+ *
+ * @param context The application Android context.
+ * @param accountManager The FxaAccountManager.
+ * @param pushFeature The [AutoPushFeature] if that is setup for observing push events.
+ * @param crashReporter Instance of `CrashReporting` to record unexpected caught exceptions.
+ * @param coroutineScope The scope in which IO work within the feature should be performed on.
+ * @param owner the lifecycle owner for the observer. Defaults to [ProcessLifecycleOwner].
+ * @param autoPause whether to stop notifying the observer during onPause lifecycle events.
+ * Defaults to false so that observers are always notified.
+ */
+class FxaPushSupportFeature(
+ private val context: Context,
+ private val accountManager: FxaAccountManager,
+ private val pushFeature: AutoPushFeature,
+ private val crashReporter: CrashReporting? = null,
+ private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+ private val owner: LifecycleOwner = ProcessLifecycleOwner.get(),
+ private val autoPause: Boolean = false,
+) {
+
+ /**
+ * A unique scope for the FxA push subscription that is generated once and stored in SharedPreferences.
+ *
+ * This scope is randomly generated and unique to the app install.
+ * (why this uuid? Note it is *not* reset on logout!)
+ */
+ private val pushScope = PushScopeProperty(context, coroutineScope)
+
+ /**
+ * Initialize the support feature to launch the appropriate observers.
+ */
+ fun initialize() = coroutineScope.launch {
+ val scopeValue = pushScope.value()
+
+ val autoPushObserver = AutoPushObserver(accountManager, pushFeature, scopeValue)
+
+ val accountObserver = AccountObserver(
+ context,
+ pushFeature,
+ scopeValue,
+ crashReporter,
+ owner,
+ autoPause,
+ )
+
+ coroutineScope.launch(Main) {
+ accountManager.register(accountObserver)
+
+ pushFeature.register(autoPushObserver, owner, autoPause)
+ }
+ }
+
+ companion object {
+ const val PUSH_SCOPE_PREFIX = "fxa_push_scope_"
+ }
+}
+
+/**
+ * An [FxaAccountManager] observer to know when an account has been added, so we can begin observing the device
+ * constellation.
+ */
+internal class AccountObserver(
+ private val context: Context,
+ private val push: AutoPushFeature,
+ private val fxaPushScope: String,
+ private val crashReporter: CrashReporting?,
+ private val lifecycleOwner: LifecycleOwner,
+ private val autoPause: Boolean,
+) : SyncAccountObserver {
+
+ private val logger = Logger(AccountObserver::class.java.simpleName)
+ private val verificationDelegate = VerificationDelegate(context, push.config.disableRateLimit)
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ val constellationObserver = ConstellationObserver(
+ context = context,
+ push = push,
+ scope = fxaPushScope,
+ account = account,
+ verifier = verificationDelegate,
+ crashReporter = crashReporter,
+ )
+
+ // NB: can we just expose registerDeviceObserver on account manager?
+ // registration could happen after onDevicesUpdate has been called, without having to tie this
+ // into the account "auth lifecycle".
+ // See https://github.com/mozilla-mobile/android-components/issues/8766
+ GlobalScope.launch(Main) {
+ account.deviceConstellation().registerDeviceObserver(constellationObserver, lifecycleOwner, autoPause)
+ account.deviceConstellation().refreshDevices()
+ }
+ }
+
+ override fun onLoggedOut() {
+ logger.debug("Un-subscribing for FxA scope $fxaPushScope events.")
+
+ push.unsubscribe(fxaPushScope)
+
+ // Delete cached value of last verified timestamp when we log out.
+ preference(context).edit()
+ .remove(PREF_LAST_VERIFIED)
+ .apply()
+ }
+}
+
+/**
+ * Subscribes to the AutoPushFeature, and updates the FxA device record if necessary.
+ * Note that if the subscription already exists, then this doesn't hit any servers, so
+ * it's OK to call this somewhat frequently.
+ */
+internal fun pushSubscribe(
+ push: AutoPushFeature,
+ account: OAuthAccount,
+ scope: String,
+ crashReporter: CrashReporting?,
+ logContext: String,
+) {
+ val logger = Logger("FxaPushSupportFeature")
+ val currentDevice = account.deviceConstellation().state()?.currentDevice
+ if (currentDevice == null) {
+ logger.warn("Can't subscribe to account push notifications as there's no current device")
+ return
+ }
+ logger.debug("Subscribing for FxaPushScope ($scope) events.")
+ push.subscribe(
+ scope,
+ onSubscribeError = { e ->
+ crashReporter?.recordCrashBreadcrumb(Breadcrumb("Subscribing to FxA push failed."))
+ logger.warn("Subscribing to FxA push failed: $logContext: ", e)
+ },
+ onSubscribe = { subscription ->
+ logger.info("Created a new subscription: $logContext: $subscription")
+ // Apparently `subscriptionExpired` typically means just the FCM token is wrong, so
+ // after getting a new one, our push endpoint will remain the same as it was. So here
+ // we always update the endpoint if `subscriptionExpired` is true, even when the
+ // subscription matches, just to ensure `subscriptionExpired` is reset.
+ if (currentDevice.subscriptionExpired ||
+ currentDevice.subscription?.endpoint != subscription.endpoint
+ ) {
+ logger.info("Updating account with new subscription info.")
+ CoroutineScope(Main).launch {
+ account.deviceConstellation().setDevicePushSubscription(subscription.into())
+ }
+ }
+ },
+ )
+}
+
+/**
+ * A [DeviceConstellation] observer to know when we should notify the push feature to begin the registration renewal
+ * when notified by the FxA server. See [Device.subscriptionExpired].
+ */
+internal class ConstellationObserver(
+ context: Context,
+ private val push: AutoPushFeature,
+ private val scope: String,
+ private val account: OAuthAccount,
+ private val verifier: VerificationDelegate = VerificationDelegate(context),
+ private val crashReporter: CrashReporting?,
+) : DeviceConstellationObserver {
+
+ private val logger = Logger(ConstellationObserver::class.java.simpleName)
+
+ override fun onDevicesUpdate(constellation: ConstellationState) {
+ logger.info("onDevicesUpdate triggered.")
+
+ val currentDevice = constellation.currentDevice ?: return
+ if (currentDevice.subscriptionExpired) {
+ if (verifier.allowedToRenew()) {
+ // This smells wrong - the fact FxaPushSupportFeature needs to detect a
+ // subscription problem implies non-FxA users will never detect this and
+ // remain broken?
+ logger.info("Our push subscription is expired; renewing FCM registration.")
+ push.renewRegistration()
+
+ logger.info("Incrementing verifier")
+ logger.debug(
+ "Verifier state before: timestamp=${verifier.innerTimestamp}, count=${verifier.innerCount}",
+ )
+ verifier.increment()
+ logger.debug(
+ "Verifier state after: timestamp=${verifier.innerTimestamp}, count=${verifier.innerCount}",
+ )
+ } else {
+ logger.info("Short-circuiting onDevicesUpdate: rate-limited")
+ }
+ }
+
+ // And unconditionally subscribe - if our local DB already has a subscription it will
+ // be returned without hitting the server. If some other problem meant our subscription
+ // was dropped or never made, it will hit the server and deliver a new end-point.
+ pushSubscribe(push, account, scope, crashReporter, "onDevicesUpdate")
+ }
+}
+
+/**
+ * An [AutoPushFeature] observer to handle [FxaAccountManager] subscriptions and push events.
+ */
+internal class AutoPushObserver(
+ private val accountManager: FxaAccountManager,
+ private val pushFeature: AutoPushFeature,
+ private val fxaPushScope: String,
+) : AutoPushFeature.Observer {
+ private val logger = Logger(AutoPushObserver::class.java.simpleName)
+
+ override fun onMessageReceived(scope: String, message: ByteArray?) {
+ if (scope != fxaPushScope) {
+ return
+ }
+
+ logger.info("Received new push message for $scope")
+
+ // Ignore push messages that do not have data.
+ val rawEvent = message ?: return
+
+ accountManager.withConstellation {
+ CoroutineScope(Main).launch {
+ processRawEvent(String(rawEvent))
+ }
+ }
+ }
+
+ override fun onSubscriptionChanged(scope: PushScope) {
+ if (scope != fxaPushScope) {
+ return
+ }
+
+ logger.info("Our sync push scope ($scope) has expired. Re-subscribing..")
+
+ val account = accountManager.authenticatedAccount()
+ if (account == null) {
+ logger.info("We don't have any account to pass the push subscription to.")
+ return
+ }
+ pushSubscribe(pushFeature, account, fxaPushScope, null, "onSubscriptionChanged")
+ }
+}
+
+/**
+ * A helper that rate limits how often we should notify our servers to renew push registration. For debugging, we
+ * can override this rate-limit check by enabling the [disableRateLimit] flag.
+ *
+ * Implementation notes: This saves the timestamp of our renewal and the number of times we have renewed our
+ * registration within the [PERIODIC_INTERVAL_MILLISECONDS] interval of time.
+ */
+internal class VerificationDelegate(
+ context: Context,
+ private val disableRateLimit: Boolean = false,
+) : SharedPreferencesCache<VerificationState>(context) {
+ override val logger: Logger = Logger(VerificationDelegate::class.java.simpleName)
+ override val cacheKey: String = PREF_LAST_VERIFIED
+ override val cacheName: String = PREFERENCE_NAME
+
+ override fun VerificationState.toJSON() =
+ JSONObject().apply {
+ put(KEY_TIMESTAMP, timestamp)
+ put(KEY_TOTAL_COUNT, totalCount)
+ }
+
+ override fun fromJSON(obj: JSONObject) =
+ VerificationState(
+ obj.getLong(KEY_TIMESTAMP),
+ obj.getInt(KEY_TOTAL_COUNT),
+ )
+
+ @VisibleForTesting
+ internal var innerCount: Int = 0
+
+ @VisibleForTesting
+ internal var innerTimestamp: Long = System.currentTimeMillis()
+
+ init {
+ getCached()?.let { cache ->
+ innerTimestamp = cache.timestamp
+ innerCount = cache.totalCount
+ }
+ }
+
+ /**
+ * Checks whether we're within our rate limiting constraints.
+ */
+ fun allowedToRenew(): Boolean {
+ logger.info("Allowed to renew?")
+
+ if (disableRateLimit) {
+ logger.info("Rate limit override is enabled - allowed to renew!")
+ return true
+ }
+
+ // within time frame
+ val currentTime = System.currentTimeMillis()
+ if ((currentTime - innerTimestamp) >= PERIODIC_INTERVAL_MILLISECONDS) {
+ logger.info("Resetting. currentTime($currentTime) - $innerTimestamp < $PERIODIC_INTERVAL_MILLISECONDS")
+ reset()
+ } else {
+ logger.info("No need to reset inner timestamp and count.")
+ }
+
+ // within interval counter
+ if (innerCount > MAX_REQUEST_IN_INTERVAL) {
+ logger.info("Not allowed: innerCount($innerCount) > $MAX_REQUEST_IN_INTERVAL")
+ return false
+ }
+
+ logger.info("Allowed to renew!")
+ return true
+ }
+
+ /**
+ * Should be called whenever a successful invocation has taken place and we want to record it.
+ */
+ fun increment() {
+ logger.info("Incrementing verification state.")
+ val count = innerCount + 1
+
+ setToCache(VerificationState(innerTimestamp, count))
+
+ innerCount = count
+ }
+
+ private fun reset() {
+ logger.info("Resetting verification state.")
+ val timestamp = System.currentTimeMillis()
+ innerCount = 0
+ innerTimestamp = timestamp
+
+ setToCache(VerificationState(timestamp, 0))
+ }
+
+ companion object {
+ private const val KEY_TIMESTAMP = "timestamp"
+ private const val KEY_TOTAL_COUNT = "totalCount"
+
+ internal const val PERIODIC_INTERVAL_MILLISECONDS = 24 * 60 * 60 * 1000L // 24 hours
+ internal const val MAX_REQUEST_IN_INTERVAL = 500 // 500 requests in 24 hours
+ }
+}
+
+internal data class VerificationState(val timestamp: Long, val totalCount: Int)
+
+internal fun preference(context: Context) = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
+
+internal fun AutoPushSubscription.into() = DevicePushSubscription(
+ endpoint = this.endpoint,
+ publicKey = this.publicKey,
+ authKey = this.authKey,
+)
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt
new file mode 100644
index 0000000000..4f049a790b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt
@@ -0,0 +1,65 @@
+/* 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.accounts.push
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCommandIncoming
+import mozilla.components.concept.sync.TabData
+import mozilla.components.feature.push.AutoPushFeature
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A feature that uses the [FxaAccountManager] to receive tabs.
+ *
+ * Adding push support with [AutoPushFeature] will allow for the account to be notified immediately.
+ * If the push components are not used, the feature can still function
+ * while tabs would only be received when refreshing the device state.
+ *
+ * See [SendTabUseCases] for the ability to send tabs to other devices.
+ *
+ * @param accountManager Firefox account manager.
+ * @param owner Android lifecycle owner for the observers. Defaults to the [ProcessLifecycleOwner]
+ * so that we can always observe events throughout the application lifecycle.
+ * @param autoPause whether or not the observer should automatically be
+ * paused/resumed with the bound lifecycle.
+ * @param onTabsReceived the callback invoked with new tab(s) are received.
+ */
+class SendTabFeature(
+ accountManager: FxaAccountManager,
+ owner: LifecycleOwner = ProcessLifecycleOwner.get(),
+ autoPause: Boolean = false,
+ onTabsReceived: (Device?, List<TabData>) -> Unit,
+) {
+ init {
+ val observer = EventsObserver(onTabsReceived)
+
+ // Observe the account for all account events, although we'll ignore
+ // non send-tab command events.
+ accountManager.registerForAccountEvents(observer, owner, autoPause)
+ }
+}
+
+internal class EventsObserver(
+ private val onTabsReceived: (Device?, List<TabData>) -> Unit,
+) : AccountEventsObserver {
+ private val logger = Logger("EventsObserver")
+
+ override fun onEvents(events: List<AccountEvent>) {
+ events.asSequence()
+ .filterIsInstance<AccountEvent.DeviceCommandIncoming>()
+ .map { it.command }
+ .filterIsInstance<DeviceCommandIncoming.TabReceived>()
+ .forEach { command ->
+ logger.debug("Showing ${command.entries.size} tab(s) received from deviceID=${command.from?.id}")
+
+ onTabsReceived(command.from, command.entries)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabUseCases.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabUseCases.kt
new file mode 100644
index 0000000000..6f6a869faa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabUseCases.kt
@@ -0,0 +1,193 @@
+/* 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.accounts.push
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.plus
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceCommandOutgoing.SendTab
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.TabData
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.ktx.kotlin.crossProduct
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Contains use cases for sending tabs to devices related to the firefox-accounts.
+ *
+ * See [SendTabFeature] for the ability to receive tabs from other devices.
+ *
+ * @param accountManager The AccountManager on which we want to retrieve our devices.
+ * @param coroutineContext The Coroutine Context on which we want to do the actual sending.
+ * By default, we want to do this on the IO dispatcher since it involves making network requests to
+ * the Sync servers.
+ */
+class SendTabUseCases(
+ accountManager: FxaAccountManager,
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+) {
+ private var job: Job = SupervisorJob()
+ private val scope = CoroutineScope(coroutineContext) + job
+
+ class SendToDeviceUseCase internal constructor(
+ private val accountManager: FxaAccountManager,
+ private val scope: CoroutineScope,
+ ) {
+ /**
+ * Sends the tab to provided deviceId if possible.
+ *
+ * @param deviceId The deviceId to send the tab.
+ * @param tab The tab to send.
+ * @return a deferred boolean if the result was successful or not.
+ */
+ operator fun invoke(deviceId: String, tab: TabData) =
+ scope.async { send(deviceId, tab) }
+
+ /**
+ * Sends the tabs to provided deviceId if possible.
+ *
+ * @param deviceId The deviceId to send the tab.
+ * @param tabs The list of tabs to send.
+ * @return a deferred boolean as true if the combined result was successful or not.
+ */
+ operator fun invoke(deviceId: String, tabs: List<TabData>): Deferred<Boolean> {
+ return scope.async {
+ tabs.map { tab ->
+ send(deviceId, tab)
+ }.fold(true) { acc, result ->
+ acc and result
+ }
+ }
+ }
+
+ private suspend fun send(deviceId: String, tab: TabData): Boolean {
+ // Filter tabs that don't have a send-capable uri
+ if (!isValidTabSchema(tab)) {
+ return false
+ }
+ filterSendTabDevices(accountManager) { constellation, devices ->
+ val device = devices.firstOrNull {
+ it.id == deviceId
+ }
+ device?.let {
+ return constellation.sendCommandToDevice(
+ device.id,
+ SendTab(tab.title, tab.url),
+ )
+ }
+ }
+
+ return false
+ }
+ }
+
+ class SendToAllUseCase internal constructor(
+ private val accountManager: FxaAccountManager,
+ private val scope: CoroutineScope,
+ ) {
+
+ /**
+ * Sends the tab to all send-tab compatible devices.
+ *
+ * @param tab The tab to send.
+ * @return a deferred boolean as true if the combined result was successful or not.
+ */
+ operator fun invoke(tab: TabData): Deferred<Boolean> {
+ return scope.async {
+ sendToAll { devices ->
+ devices.map { device ->
+ device to tab
+ }
+ }
+ }
+ }
+
+ /**
+ * Sends the tabs to all the send-tab compatible devices.
+ *
+ * @param tabs a collection of tabs to send.
+ * @return a deferred boolean as true if the combined result was successful or not.
+ */
+ operator fun invoke(tabs: Collection<TabData>): Deferred<Boolean> {
+ return scope.async {
+ sendToAll { devices ->
+ val filteredTabs = tabs.filter { isValidTabSchema(it) }
+ devices.crossProduct(filteredTabs) { device, tab ->
+ device to tab
+ }
+ }
+ }
+ }
+
+ private suspend inline fun sendToAll(
+ block: (Collection<Device>) -> List<Pair<Device, TabData>>,
+ ): Boolean {
+ // Filter devices to send tab capable ones.
+ filterSendTabDevices(accountManager) { constellation, devices ->
+ // Get a list of device-tab combinations that we want to send.
+ return block(devices).map { (device, tab) ->
+ // Filter tabs that don't have a send-capable uri
+ if (!isValidTabSchema(tab)) {
+ return false
+ }
+ // Send the tab!
+ constellation.sendCommandToDevice(
+ device.id,
+ SendTab(tab.title, tab.url),
+ )
+ }.fold(true) { acc, result ->
+ // Collect the results and reduce them into one final result.
+ acc and result
+ }
+ }
+ return false
+ }
+ }
+
+ val sendToDeviceAsync: SendToDeviceUseCase by lazy {
+ SendToDeviceUseCase(
+ accountManager,
+ scope,
+ )
+ }
+
+ val sendToAllAsync: SendToAllUseCase by lazy {
+ SendToAllUseCase(
+ accountManager,
+ scope,
+ )
+ }
+}
+
+@VisibleForTesting
+internal inline fun filterSendTabDevices(
+ accountManager: FxaAccountManager,
+ block: (DeviceConstellation, Collection<Device>) -> Unit,
+) {
+ val constellation = accountManager.authenticatedAccount()?.deviceConstellation() ?: return
+
+ constellation.state()?.let { state ->
+ state.otherDevices.filter {
+ it.capabilities.contains(DeviceCapability.SEND_TAB)
+ }.let { devices ->
+ block(constellation, devices)
+ }
+ }
+}
+
+@VisibleForTesting
+internal fun isValidTabSchema(tab: TabData): Boolean {
+ // We don't sync certain schemas, about|resource|chrome|file|blob|moz-extension
+ // See https://searchfox.org/mozilla-central/rev/7d379061bd56251df911728686c378c5820513d8/modules/libpref/init/all.js#4356
+ val filteredSchemas = arrayOf("about:", "resource:", "chrome:", "file:", "blob:", "moz-extension:")
+ return filteredSchemas.none({ tab.url.startsWith(it) })
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/PushScopeProperty.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/PushScopeProperty.kt
new file mode 100644
index 0000000000..44d9c43353
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/PushScopeProperty.kt
@@ -0,0 +1,43 @@
+/* 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.accounts.push.cache
+
+import android.content.Context
+import android.content.SharedPreferences
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.withContext
+import mozilla.components.feature.accounts.push.FxaPushSupportFeature
+import mozilla.components.feature.accounts.push.PREF_FXA_SCOPE
+import mozilla.components.feature.accounts.push.preference
+import mozilla.components.feature.push.PushScope
+import java.util.UUID
+
+/**
+ * An implementation of a [ScopeProperty] that generates and stores a scope in [SharedPreferences].
+ */
+internal class PushScopeProperty(
+ private val context: Context,
+ private val coroutineScope: CoroutineScope,
+) : ScopeProperty {
+
+ override suspend fun value(): PushScope = withContext(coroutineScope.coroutineContext) {
+ val prefs = preference(context)
+
+ // Generate a unique scope if one doesn't exist.
+ val randomUuid = UUID.randomUUID().toString().replace("-", "")
+
+ // Return a scope in the format example: "fxa_push_scope_a62d5f27c9d74af4996d057f0e0e9c38"
+ val scope = FxaPushSupportFeature.PUSH_SCOPE_PREFIX + randomUuid
+
+ if (!prefs.contains(PREF_FXA_SCOPE)) {
+ prefs.edit().putString(PREF_FXA_SCOPE, scope).apply()
+
+ return@withContext scope
+ }
+
+ // The default string is non-null, so we can safely cast.
+ prefs.getString(PREF_FXA_SCOPE, scope) as String
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/ScopeProperty.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/ScopeProperty.kt
new file mode 100644
index 0000000000..30e4030888
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/ScopeProperty.kt
@@ -0,0 +1,18 @@
+/* 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.accounts.push.cache
+
+import mozilla.components.feature.push.PushScope
+
+/**
+ * A [ScopeProperty] implementation generates and holds the [PushScope].
+ */
+interface ScopeProperty {
+
+ /**
+ * Returns the [PushScope] value.
+ */
+ suspend fun value(): PushScope
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/ext/String.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/ext/String.kt
new file mode 100644
index 0000000000..6a9fb3d9d4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/ext/String.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.accounts.push.ext
+
+import android.net.Uri
+
+/**
+ * Removes all expect the [lastSegmentToTake] of the [Uri.getLastPathSegment] result.
+ *
+ * Example:
+ * ```
+ * val url = https://mozilla.org/firefox
+ * val redactedUrl = url.redactPartialUri(3, "redacted...")
+ *
+ * assertEquals("https://mozilla.org/redacted...fox", redactedUrl)
+ * ```
+ */
+fun String.redactPartialUri(lastSegmentToTake: Int = 20, shortForm: String = "redacted..."): String {
+ val uri = Uri.parse(this)
+ val end = shortForm + uri.lastPathSegment?.takeLast(lastSegmentToTake)
+ val path = uri
+ .pathSegments
+ .take(uri.pathSegments.size - 1)
+ .joinToString("/")
+
+ return uri
+ .buildUpon()
+ .path(path)
+ .appendPath(end)
+ .build()
+ .toString()
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AccountObserverTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AccountObserverTest.kt
new file mode 100644
index 0000000000..9bd630332d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AccountObserverTest.kt
@@ -0,0 +1,167 @@
+/* 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.accounts.push
+
+import android.os.Looper.getMainLooper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.feature.push.AutoPushFeature
+import mozilla.components.feature.push.PushConfig
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+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.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(RobolectricTestRunner::class)
+class AccountObserverTest {
+
+ private val accountManager: FxaAccountManager = mock()
+ private val pushFeature: AutoPushFeature = mock()
+ private val pushScope: String = "testScope"
+ private val account: OAuthAccount = mock()
+ private val constellation: DeviceConstellation = mock()
+ private val config: PushConfig = mock()
+ private val crashReporter: CrashReporting = mock()
+
+ @Before
+ fun setup() {
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(pushFeature.config).thenReturn(config)
+ }
+
+ @Test
+ fun `register device observer for existing accounts`() {
+ val lifecycle: Lifecycle = mock()
+ val lifecycleOwner: LifecycleOwner = mock()
+ val observer = AccountObserver(
+ testContext,
+ pushFeature,
+ pushScope,
+ crashReporter,
+ lifecycleOwner,
+ false,
+ )
+ `when`(lifecycle.currentState).thenReturn(Lifecycle.State.STARTED)
+ `when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
+
+ observer.onAuthenticated(account, AuthType.Existing)
+ shadowOf(getMainLooper()).idle()
+
+ verify(constellation).registerDeviceObserver(any(), eq(lifecycleOwner), anyBoolean())
+
+ reset(constellation)
+
+ observer.onAuthenticated(account, AuthType.Recovered)
+ shadowOf(getMainLooper()).idle()
+
+ verify(constellation).registerDeviceObserver(any(), eq(lifecycleOwner), anyBoolean())
+ }
+
+ @Test
+ fun `onLoggedOut removes cache`() {
+ val observer = AccountObserver(
+ testContext,
+ pushFeature,
+ pushScope,
+ crashReporter,
+ mock(),
+ false,
+ )
+
+ preference(testContext).edit()
+ .putString(PREF_LAST_VERIFIED, "{\"timestamp\": 100, \"totalCount\": 0}")
+ .putString(PREF_FXA_SCOPE, "12345")
+ .apply()
+
+ assertTrue(preference(testContext).contains(PREF_LAST_VERIFIED))
+
+ observer.onLoggedOut()
+
+ assertFalse(preference(testContext).contains(PREF_LAST_VERIFIED))
+ assertTrue(preference(testContext).contains(PREF_FXA_SCOPE))
+ }
+
+ @Test
+ fun `feature does not subscribe when authenticating`() {
+ val observer = AccountObserver(
+ testContext,
+ pushFeature,
+ pushScope,
+ crashReporter,
+ mock(),
+ false,
+ )
+
+ observer.onAuthenticated(account, AuthType.Existing)
+
+ verify(pushFeature).config
+
+ verifyNoMoreInteractions(pushFeature)
+
+ observer.onAuthenticated(account, AuthType.Recovered)
+
+ verifyNoMoreInteractions(pushFeature)
+
+ observer.onAuthenticated(account, AuthType.Signup)
+
+ verifyNoMoreInteractions(pushFeature)
+
+ observer.onAuthenticated(account, AuthType.Signin)
+
+ verifyNoMoreInteractions(pushFeature)
+ }
+
+ @Test
+ fun `feature and service invoked on logout`() {
+ val observer = AccountObserver(
+ testContext,
+ pushFeature,
+ pushScope,
+ crashReporter,
+ mock(),
+ false,
+ )
+
+ observer.onLoggedOut()
+
+ verify(pushFeature).unsubscribe(eq(pushScope), any(), any())
+ }
+
+ @Test
+ fun `feature and service not invoked for any other callback`() {
+ val observer = AccountObserver(
+ testContext,
+ pushFeature,
+ pushScope,
+ crashReporter,
+ mock(),
+ false,
+ )
+
+ observer.onAuthenticationProblems()
+ observer.onProfileUpdated(mock())
+
+ verify(pushFeature).config
+ verifyNoMoreInteractions(pushFeature)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AutoPushObserverTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AutoPushObserverTest.kt
new file mode 100644
index 0000000000..4fd2002fb1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AutoPushObserverTest.kt
@@ -0,0 +1,134 @@
+/* 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.accounts.push
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.feature.push.AutoPushFeature
+import mozilla.components.feature.push.AutoPushSubscription
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.nullable
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.stubbing.OngoingStubbing
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+class AutoPushObserverTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val manager: FxaAccountManager = mock()
+ private val account: OAuthAccount = mock()
+ private val constellation: DeviceConstellation = mock()
+ private val pushFeature: AutoPushFeature = mock()
+
+ @ExperimentalCoroutinesApi
+ @Test
+ fun `messages are forwarded to account manager`() = runTestOnMain {
+ val observer = AutoPushObserver(manager, mock(), "test")
+
+ `when`(manager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+
+ observer.onMessageReceived("test", "foobar".toByteArray())
+
+ verify(constellation).processRawEvent("foobar")
+ Unit
+ }
+
+ @Test
+ fun `account manager is not invoked if no account is available`() = runTestOnMain {
+ val observer = AutoPushObserver(manager, mock(), "test")
+
+ observer.onMessageReceived("test", "foobar".toByteArray())
+
+ verify(constellation, never()).setDevicePushSubscription(any())
+ verify(constellation, never()).processRawEvent("foobar")
+ Unit
+ }
+
+ @Test
+ fun `messages are not forwarded to account manager if they are for a different scope`() = runTestOnMain {
+ val observer = AutoPushObserver(manager, mock(), "fake")
+
+ observer.onMessageReceived("test", "foobar".toByteArray())
+
+ verify(constellation, never()).processRawEvent(any())
+ Unit
+ }
+
+ @ExperimentalCoroutinesApi
+ @Test
+ fun `subscription changes are forwarded to account manager`() = runTestOnMain {
+ val observer = AutoPushObserver(manager, pushFeature, "test")
+
+ whenSubscribe()
+
+ `when`(manager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ val state: ConstellationState = mock()
+ val device: Device = mock()
+ `when`(constellation.state()).thenReturn(state)
+ `when`(state.currentDevice).thenReturn(device)
+ `when`(device.subscriptionExpired).thenReturn(true)
+
+ observer.onSubscriptionChanged("test")
+
+ verify(constellation).setDevicePushSubscription(any())
+ Unit
+ }
+
+ @Test
+ fun `do nothing if there is no account manager`() = runTestOnMain {
+ val observer = AutoPushObserver(manager, pushFeature, "test")
+
+ whenSubscribe()
+
+ observer.onSubscriptionChanged("test")
+
+ verify(constellation, never()).setDevicePushSubscription(any())
+ Unit
+ }
+
+ @Test
+ fun `subscription changes are not forwarded to account manager if they are for a different scope`() = runTestOnMain {
+ val observer = AutoPushObserver(manager, mock(), "fake")
+
+ `when`(manager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+
+ observer.onSubscriptionChanged("test")
+
+ verify(constellation, never()).setDevicePushSubscription(any())
+ verifyNoInteractions(pushFeature)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun whenSubscribe(): OngoingStubbing<Unit>? {
+ return `when`(pushFeature.subscribe(any(), nullable(), any(), any())).thenAnswer {
+ // Invoke the `onSubscribe` lambda with a fake subscription.
+ (it.arguments[3] as ((AutoPushSubscription) -> Unit)).invoke(
+ AutoPushSubscription(
+ scope = "test",
+ endpoint = "https://foo",
+ publicKey = "p256dh",
+ authKey = "auth",
+ appServerKey = null,
+ ),
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ConstellationObserverTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ConstellationObserverTest.kt
new file mode 100644
index 0000000000..0b1fc0e3f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ConstellationObserverTest.kt
@@ -0,0 +1,185 @@
+/* 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.accounts.push
+
+import android.content.Context
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.DevicePushSubscription
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.feature.push.AutoPushFeature
+import mozilla.components.feature.push.AutoPushSubscription
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.nullable
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.stubbing.OngoingStubbing
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+class ConstellationObserverTest {
+
+ private val push: AutoPushFeature = mock()
+ private val verifier: VerificationDelegate = mock()
+ private val state: ConstellationState = mock()
+ private val device: Device = mock()
+ private val context: Context = mock()
+ private val account: OAuthAccount = mock()
+ private val constellation: DeviceConstellation = mock()
+ private val crashReporter: CrashReporting = mock()
+
+ @Before
+ fun setup() {
+ `when`(state.currentDevice).thenReturn(device)
+ `when`(device.subscriptionExpired).thenReturn(false)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.state()).thenReturn(state)
+ }
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `first subscribe works`() = runTestOnMain {
+ val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
+
+ verifyNoInteractions(push)
+
+ whenSubscribe()
+
+ observer.onDevicesUpdate(state)
+
+ verify(push).subscribe(eq("testScope"), any(), any(), any())
+ verifyNoMoreInteractions(push)
+ // We should have told the constellation of the new subscription.
+ verify(constellation).setDevicePushSubscription(any())
+
+ Unit
+ }
+
+ @Test
+ fun `re-subscribe doesn't update constellation on same endpoint`() = runTestOnMain {
+ val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
+
+ verifyNoInteractions(push)
+
+ whenAlreadySubscribed()
+ whenSubscribe()
+
+ observer.onDevicesUpdate(state)
+
+ verify(push).subscribe(eq("testScope"), any(), any(), any())
+ verifyNoMoreInteractions(push)
+ // We should not have told the constellation of the subscription as it matches
+ verify(constellation).state()
+ verifyNoMoreInteractions(constellation)
+ Unit
+ }
+
+ @Test
+ fun `re-subscribe update constellations on same endpoint if expired`() = runTestOnMain {
+ val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
+
+ verifyNoInteractions(push)
+
+ whenAlreadySubscribed(true)
+ whenSubscribe()
+
+ observer.onDevicesUpdate(state)
+
+ verify(push).subscribe(eq("testScope"), any(), any(), any())
+ verifyNoMoreInteractions(push)
+ // We should have told the constellation of the same end-point subscription to clear the
+ // expired flag on the server.
+ verify(constellation).setDevicePushSubscription(any())
+ Unit
+ }
+
+ // @Test
+ fun `notify crash reporter if subscribe error occurs`() {
+ val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
+
+ whenSubscribeError()
+ observer.onDevicesUpdate(state)
+
+ verify(crashReporter).recordCrashBreadcrumb(any())
+ }
+
+ @Test
+ fun `no FCM renewal if verifier is false`() {
+ val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
+
+ verifyNoInteractions(push)
+
+ `when`(device.subscriptionExpired).thenReturn(true)
+ `when`(verifier.allowedToRenew()).thenReturn(false)
+
+ observer.onDevicesUpdate(state)
+
+ // The verifier prevents us fetching a new FCM token but doesn't prevent
+ // us calling .subscribe() on the push service.
+ verify(push).subscribe(eq("testScope"), any(), any(), any())
+ verifyNoMoreInteractions(push)
+ }
+
+ @Test
+ fun `invoke registration renewal`() {
+ val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
+
+ `when`(device.subscriptionExpired).thenReturn(true)
+ `when`(verifier.allowedToRenew()).thenReturn(true)
+
+ observer.onDevicesUpdate(state)
+
+ verify(push).renewRegistration()
+ verify(verifier).increment()
+ }
+
+ private fun testSubscription() = AutoPushSubscription(
+ scope = "testScope",
+ endpoint = "https://example.com/foobar",
+ publicKey = "",
+ authKey = "",
+ appServerKey = null,
+ )
+
+ @Suppress("UNCHECKED_CAST")
+ private fun whenSubscribe(): OngoingStubbing<Unit>? {
+ return `when`(push.subscribe(any(), nullable(), any(), any())).thenAnswer {
+ // Invoke the `onSubscribe` lambda with a fake subscription.
+ (it.arguments[3] as ((AutoPushSubscription) -> Unit)).invoke(
+ testSubscription(),
+ )
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun whenSubscribeError(): OngoingStubbing<Unit>? {
+ return `when`(push.subscribe(any(), nullable(), any(), any())).thenAnswer {
+ // Invoke the `onSubscribeError` lambda with a fake exception.
+ (it.arguments[2] as ((Exception) -> Unit)).invoke(
+ IllegalStateException("test"),
+ )
+ }
+ }
+
+ private fun whenAlreadySubscribed(expired: Boolean = false) {
+ val subscription: DevicePushSubscription = mock()
+ `when`(device.subscriptionExpired).thenReturn(expired)
+ `when`(device.subscription).thenReturn(subscription)
+ `when`(subscription.endpoint).thenReturn(testSubscription().endpoint)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/EventsObserverTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/EventsObserverTest.kt
new file mode 100644
index 0000000000..6de8ff42f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/EventsObserverTest.kt
@@ -0,0 +1,48 @@
+/* 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.accounts.push
+
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCommandIncoming
+import mozilla.components.concept.sync.TabData
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class EventsObserverTest {
+ @Test
+ fun `events are delivered successfully`() {
+ val callback: (Device?, List<TabData>) -> Unit = mock()
+ val observer = EventsObserver(callback)
+ val events = listOf(AccountEvent.DeviceCommandIncoming(command = DeviceCommandIncoming.TabReceived(mock(), mock())))
+
+ observer.onEvents(events)
+
+ verify(callback).invoke(any(), any())
+
+ observer.onEvents(listOf(AccountEvent.DeviceCommandIncoming(command = DeviceCommandIncoming.TabReceived(null, mock()))))
+
+ verify(callback).invoke(eq(null), any())
+ }
+
+ @Test
+ fun `only TabReceived commands are delivered`() {
+ val callback: (Device?, List<TabData>) -> Unit = mock()
+ val observer = EventsObserver(callback)
+ val events = listOf(
+ AccountEvent.ProfileUpdated,
+ AccountEvent.DeviceCommandIncoming(command = DeviceCommandIncoming.TabReceived(mock(), mock())),
+ AccountEvent.DeviceCommandIncoming(command = DeviceCommandIncoming.TabReceived(mock(), mock())),
+ )
+
+ observer.onEvents(events)
+
+ verify(callback, times(2)).invoke(any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/FxaPushSupportFeatureTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/FxaPushSupportFeatureTest.kt
new file mode 100644
index 0000000000..7ac2e83694
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/FxaPushSupportFeatureTest.kt
@@ -0,0 +1,83 @@
+/* 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.accounts.push
+
+import mozilla.components.feature.accounts.push.FxaPushSupportFeature.Companion.PUSH_SCOPE_PREFIX
+import mozilla.components.feature.push.AutoPushFeature
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+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.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class FxaPushSupportFeatureTest {
+
+ @get:Rule
+ val coroutineTestRule = MainCoroutineRule()
+
+ private val pushFeature: AutoPushFeature = mock()
+ private val accountManager: FxaAccountManager = mock()
+
+ private lateinit var feature: FxaPushSupportFeature
+
+ @Before
+ fun setup() {
+ preference(testContext).edit().remove(PREF_FXA_SCOPE).apply()
+ `when`(pushFeature.config).thenReturn(mock())
+
+ feature = FxaPushSupportFeature(
+ context = testContext,
+ accountManager = accountManager,
+ pushFeature = pushFeature,
+ crashReporter = null,
+ coroutineScope = coroutineTestRule.scope,
+ )
+ }
+
+ @Test
+ fun `account observer registered`() {
+ feature.initialize()
+
+ verify(accountManager).register(any())
+ verify(pushFeature).register(any(), any(), anyBoolean())
+ }
+
+ @Test
+ fun `feature generates and caches a scope`() {
+ feature.initialize()
+
+ assertTrue(preference(testContext).contains(PREF_FXA_SCOPE))
+ }
+
+ @Test
+ fun `feature does not generate a scope if one already exists`() {
+ preference(testContext).edit().putString(PREF_FXA_SCOPE, "testScope").apply()
+
+ feature.initialize()
+
+ val cachedScope = preference(testContext).getString(PREF_FXA_SCOPE, "")
+ assertEquals("testScope", cachedScope!!)
+ }
+
+ @Test
+ fun `feature generates a partially predictable push scope`() {
+ feature.initialize()
+
+ val cachedScope = preference(testContext).getString(PREF_FXA_SCOPE, "")
+
+ assertTrue(cachedScope!!.contains(PUSH_SCOPE_PREFIX))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabFeatureKtTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabFeatureKtTest.kt
new file mode 100644
index 0000000000..83a06ba8cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabFeatureKtTest.kt
@@ -0,0 +1,67 @@
+/* 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.accounts.push
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.ext.withConstellation
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi
+class SendTabFeatureKtTest {
+ @Test
+ fun `feature register all observers`() = runTest {
+ val accountManager: FxaAccountManager = mock()
+
+ SendTabFeature(
+ accountManager = accountManager,
+ onTabsReceived = mock(),
+ )
+
+ verify(accountManager).registerForAccountEvents(any(), any(), anyBoolean())
+ }
+
+ @Test
+ fun `feature registers only the device observer`() {
+ val accountManager: FxaAccountManager = mock()
+
+ SendTabFeature(
+ accountManager = accountManager,
+ onTabsReceived = mock(),
+ )
+
+ verify(accountManager).registerForAccountEvents(any(), any(), anyBoolean())
+
+ verify(accountManager, never()).register(any(), any(), anyBoolean())
+ }
+
+ @Test
+ fun `block is executed only account is available`() {
+ val accountManager: FxaAccountManager = mock()
+ val block: DeviceConstellation.() -> Unit = mock()
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+
+ accountManager.withConstellation(block)
+
+ verify(block, never()).invoke(constellation)
+
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+
+ accountManager.withConstellation(block)
+
+ verify(block).invoke(constellation)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabUseCasesTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabUseCasesTest.kt
new file mode 100644
index 0000000000..d8a1ac52a7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabUseCasesTest.kt
@@ -0,0 +1,331 @@
+/* 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.accounts.push
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.TabData
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import java.util.UUID
+
+@ExperimentalCoroutinesApi
+class SendTabUseCasesTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val manager: FxaAccountManager = mock()
+ private val account: OAuthAccount = mock()
+ private val constellation: DeviceConstellation = mock()
+ private val state: ConstellationState = mock()
+
+ @Before
+ fun setup() {
+ `when`(manager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.state()).thenReturn(state)
+ }
+
+ @Test
+ fun `SendTabUseCase - tab is sent to capable device`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = generateDevice()
+
+ `when`(state.otherDevices).thenReturn(listOf(device))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(true)
+
+ useCases.sendToDeviceAsync(device.id, TabData("Title", "http://example.com"))
+
+ verify(constellation).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabUseCase - tabs are sent to capable device`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = generateDevice()
+ val tab = TabData("Title", "http://example.com")
+
+ `when`(state.otherDevices).thenReturn(listOf(device))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(true)
+
+ useCases.sendToDeviceAsync(device.id, listOf(tab, tab))
+
+ verify(constellation, times(2)).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabUseCase - tabs are NOT sent to incapable devices`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = mock()
+ val tab = TabData("Title", "http://example.com")
+
+ useCases.sendToDeviceAsync("123", listOf(tab, tab))
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+
+ `when`(device.id).thenReturn("123")
+ `when`(state.otherDevices).thenReturn(listOf(device))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(false)
+
+ useCases.sendToDeviceAsync("123", listOf(tab, tab))
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabUseCase - ONLY tabs with valid schema are sent to capable device`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = generateDevice()
+ val tab = TabData("Title", "http://example.com")
+ val tab1 = TabData("PDFFile", "file://path/to/some/pdf")
+ val tab2 = TabData("AboutConfig", "about:config")
+
+ `when`(state.otherDevices).thenReturn(listOf(device))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(true)
+
+ useCases.sendToDeviceAsync(device.id, listOf(tab, tab1, tab2))
+
+ verify(constellation, times(1)).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabUseCase - device id does not match when sending single tab`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = generateDevice("123")
+ val tab = TabData("Title", "http://example.com")
+
+ useCases.sendToDeviceAsync("123", tab)
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+
+ `when`(state.otherDevices).thenReturn(listOf(device))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(false)
+
+ useCases.sendToDeviceAsync("456", tab)
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+
+ useCases.sendToDeviceAsync("123", tab)
+
+ verify(constellation).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabUseCase - device id does not match when sending tabs`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = generateDevice("123")
+ val tab = TabData("Title", "http://example.com")
+
+ useCases.sendToDeviceAsync("123", listOf(tab))
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+
+ `when`(state.otherDevices).thenReturn(listOf(device))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(false)
+
+ useCases.sendToDeviceAsync("456", listOf(tab))
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+
+ useCases.sendToDeviceAsync("123", listOf(tab))
+
+ verify(constellation).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabToAllUseCase - tab is sent to capable devices`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = generateDevice()
+ val device2: Device = generateDevice()
+
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(false)
+
+ val tab = TabData("Mozilla", "https://mozilla.org")
+
+ useCases.sendToAllAsync(tab)
+
+ verify(constellation, times(2)).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabToAllUseCase - tabs is sent to capable devices`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = generateDevice()
+ val device2: Device = generateDevice()
+
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(false)
+
+ val tab = TabData("Mozilla", "https://mozilla.org")
+ val tab2 = TabData("Firefox", "https://firefox.com")
+
+ useCases.sendToAllAsync(listOf(tab, tab2))
+
+ verify(constellation, times(4)).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabToAllUseCase - tab is NOT sent to incapable devices`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager)
+ val tab = TabData("Mozilla", "https://mozilla.org")
+ val device: Device = mock()
+ val device2: Device = mock()
+
+ useCases.sendToAllAsync(tab)
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+
+ `when`(device.id).thenReturn("123")
+ `when`(device2.id).thenReturn("456")
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
+
+ useCases.sendToAllAsync(tab)
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabToAllUseCase - tabs are NOT sent to capable devices`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager)
+ val tab = TabData("Mozilla", "https://mozilla.org")
+ val tab2 = TabData("Firefox", "https://firefox.com")
+ val device: Device = mock()
+ val device2: Device = mock()
+
+ useCases.sendToAllAsync(tab)
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+
+ `when`(device.id).thenReturn("123")
+ `when`(device2.id).thenReturn("456")
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
+
+ useCases.sendToAllAsync(listOf(tab, tab2))
+
+ verify(constellation, never()).sendCommandToDevice(eq("123"), any())
+ verify(constellation, never()).sendCommandToDevice(eq("456"), any())
+ }
+
+ @Test
+ fun `SendTabToAllUseCase - ONLY tabs with valid schema are sent to capable devices`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = generateDevice()
+ val device2: Device = generateDevice()
+
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(false)
+
+ val tab = TabData("Mozilla", "https://mozilla.org")
+ val tab2 = TabData("Firefox", "https://firefox.com")
+ // Invalid url to send
+ val tab3 = TabData("PDFFile", "file://path/to/pdffile")
+ // Invalid url to send
+ val tab4 = TabData("AboutPage", "about:config")
+
+ useCases.sendToAllAsync(listOf(tab, tab2, tab3, tab4))
+
+ verify(constellation, times(4)).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `SendTabUseCase - result is false if any send tab action fails`() = runTestOnMain {
+ val useCases = SendTabUseCases(manager, coroutineContext)
+ val device: Device = mock()
+ val tab = TabData("Title", "http://example.com")
+
+ useCases.sendToDeviceAsync("123", listOf(tab, tab))
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+
+ `when`(device.id).thenReturn("123")
+ `when`(state.otherDevices).thenReturn(listOf(device))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(true)
+ .thenReturn(true)
+
+ val result = useCases.sendToDeviceAsync("123", listOf(tab, tab))
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+ Assert.assertFalse(result.await())
+ }
+
+ @Test
+ fun `filter devices returns capable devices`() = runTestOnMain {
+ var executed = false
+ `when`(state.otherDevices).thenReturn(listOf(generateDevice(), generateDevice()))
+ filterSendTabDevices(manager) { _, _ ->
+ executed = true
+ }
+
+ Assert.assertTrue(executed)
+ }
+
+ @Test
+ fun `filter devices does NOT provide for incapable devices`() = runTestOnMain {
+ val device: Device = mock()
+ val device2: Device = mock()
+
+ `when`(device.id).thenReturn("123")
+ `when`(device2.id).thenReturn("456")
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
+
+ filterSendTabDevices(manager) { _, filteredDevices ->
+ Assert.assertTrue(filteredDevices.isEmpty())
+ }
+
+ val accountManager: FxaAccountManager = mock()
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val state: ConstellationState = mock()
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.state()).thenReturn(state)
+
+ filterSendTabDevices(mock()) { _, _ ->
+ Assert.fail()
+ }
+ }
+
+ private fun generateDevice(id: String = UUID.randomUUID().toString()): Device {
+ return Device(
+ id = id,
+ displayName = id,
+ deviceType = DeviceType.MOBILE,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(DeviceCapability.SEND_TAB),
+ subscriptionExpired = false,
+ subscription = null,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/VerificationDelegateTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/VerificationDelegateTest.kt
new file mode 100644
index 0000000000..dfccb3404a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/VerificationDelegateTest.kt
@@ -0,0 +1,140 @@
+/* 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.accounts.push
+
+import mozilla.components.feature.accounts.push.VerificationDelegate.Companion.MAX_REQUEST_IN_INTERVAL
+import mozilla.components.support.test.robolectric.testContext
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class VerificationDelegateTest {
+
+ @Before
+ fun setup() {
+ preference(testContext).edit().remove(PREF_LAST_VERIFIED).apply()
+ }
+
+ @Test
+ fun `init uses current timestamp`() {
+ val timestamp = System.currentTimeMillis()
+ val verifier = VerificationDelegate(testContext)
+ assertEquals(0, verifier.innerCount)
+ assertTrue(timestamp <= verifier.innerTimestamp && timestamp + 1000 > verifier.innerTimestamp)
+ }
+
+ @Test
+ fun `init uses cached timestamp`() {
+ lastVerifiedPref = Pair(1000, 50)
+
+ val verifier = VerificationDelegate(testContext)
+ assertEquals(50, verifier.innerCount)
+ assertEquals(1000, verifier.innerTimestamp)
+ }
+
+ @Test
+ fun `after interval the counter resets`() {
+ lastVerifiedPref = Pair(System.currentTimeMillis() - VERIFY_NOW_INTERVAL, 50)
+
+ val verifier = VerificationDelegate(testContext)
+
+ assertEquals(50, verifier.innerCount)
+ assertEquals(50, lastVerifiedPref.second)
+
+ val result = verifier.allowedToRenew()
+
+ assertEquals(0, verifier.innerCount)
+
+ assertEquals(0, lastVerifiedPref.second)
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `false if requesting above rate limit`() {
+ val timestamp = System.currentTimeMillis()
+ lastVerifiedPref = Pair(timestamp, 501)
+
+ val verifier = VerificationDelegate(testContext)
+
+ assertEquals(MAX_REQUEST_IN_INTERVAL + 1, verifier.innerCount)
+ assertEquals(MAX_REQUEST_IN_INTERVAL + 1, lastVerifiedPref.second)
+
+ val result = verifier.allowedToRenew()
+
+ assertFalse(result)
+ assertEquals(timestamp, verifier.innerTimestamp)
+ }
+
+ @Test
+ fun `reset when above rate limit and interval`() {
+ lastVerifiedPref = Pair(System.currentTimeMillis() - VERIFY_NOW_INTERVAL, 501)
+
+ val verifier = VerificationDelegate(testContext)
+
+ assertEquals(501, verifier.innerCount)
+ assertEquals(501, lastVerifiedPref.second)
+
+ val result = verifier.allowedToRenew()
+
+ assertEquals(0, verifier.innerCount)
+
+ assertEquals(0, lastVerifiedPref.second)
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `increment updates inner values and cache`() {
+ val verifier = VerificationDelegate(testContext)
+
+ assertEquals(0, verifier.innerCount)
+ assertEquals(0, lastVerifiedPref.second)
+
+ verifier.increment()
+
+ assertEquals(1, verifier.innerCount)
+ assertEquals(1, lastVerifiedPref.second)
+ }
+
+ @Test
+ fun `rate-limiting disabled short circuits check`() {
+ val timestamp = System.currentTimeMillis()
+ lastVerifiedPref = Pair(timestamp, 501)
+
+ val verifier = VerificationDelegate(testContext, true)
+
+ val result = verifier.allowedToRenew()
+
+ assertTrue(result)
+ }
+
+ companion object {
+ var lastVerifiedPref: Pair<Long, Int>
+ get() {
+ val stringResult = requireNotNull(
+ preference(testContext).getString(
+ PREF_LAST_VERIFIED,
+ "{\"timestamp\": ${System.currentTimeMillis()}, \"totalCount\": 0}",
+ ),
+ )
+ val json = JSONObject(stringResult)
+ return Pair(json.getLong("timestamp"), json.getInt("totalCount"))
+ }
+ set(value) {
+ preference(testContext).edit()
+ .putString(PREF_LAST_VERIFIED, "{\"timestamp\": ${value.first}, \"totalCount\": ${value.second}}")
+ .apply()
+ }
+
+ private const val VERIFY_NOW_INTERVAL = 25 * 60 * 60 * 1000L // 25 hours in milliseconds
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ext/StringKtTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ext/StringKtTest.kt
new file mode 100644
index 0000000000..bc9f9ca52b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ext/StringKtTest.kt
@@ -0,0 +1,44 @@
+/* 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.accounts.push.ext
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.kotlin.toNormalizedUrl
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class StringKtTest {
+
+ @Test
+ fun `redacted endpoint contains short form in it`() {
+ val endpoint =
+ "https://updates.push.services.mozilla.com/wpush/v1/gAAAAABfAL4OBMRjP3O66lugUrcHT8kk4ENnJP4SE67US" +
+ "kmH9NdIz__-_3PtC_V79-KwG73Y3mZye1qtnYzoJETaGQidjgbiJdXzB7u0T9BViE2b7O3oqsFJpnwvmO-CiFqKKP14vitH"
+
+ assertTrue(endpoint.redactPartialUri().contains("redacted..."))
+ }
+
+ @Test
+ fun `default schema if http is missing`() {
+ assertEquals("https://www.example.com", "https://www.example.com".toNormalizedUrl())
+ assertEquals("https://www.example.com", "htTPs://www.example.com".toNormalizedUrl())
+ assertEquals("http://www.example.com", "HTTP://www.example.com".toNormalizedUrl())
+ assertEquals("http://www.example.com", "http://www.example.com".toNormalizedUrl())
+ assertEquals("http://www.example.com", "www.example.com".toNormalizedUrl())
+ assertEquals("http://example.com", "example.com".toNormalizedUrl())
+ assertEquals("http://example", "example".toNormalizedUrl())
+ assertEquals("http://example", " example ".toNormalizedUrl())
+
+ assertEquals("ftp://example.com", "ftp://example.com".toNormalizedUrl())
+ assertEquals("ftp://example.com", "FTP://example.com".toNormalizedUrl())
+
+ assertEquals("http://httpexample.com", "httpexample.com".toNormalizedUrl())
+ assertEquals("http://httpsexample.com", "httpsexample.com".toNormalizedUrl())
+ assertEquals("http://httpsexample.com", " httpsexample.com ".toNormalizedUrl())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/accounts-push/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/accounts-push/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/accounts-push/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/accounts-push/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/accounts/.gitignore b/mobile/android/android-components/components/feature/accounts/.gitignore
new file mode 100644
index 0000000000..2ddf5f27b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/.gitignore
@@ -0,0 +1 @@
+manifest.json
diff --git a/mobile/android/android-components/components/feature/accounts/README.md b/mobile/android/android-components/components/feature/accounts/README.md
new file mode 100644
index 0000000000..6f8c7889a4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/README.md
@@ -0,0 +1,18 @@
+# [Android Components](../../../README.md) > Feature > Accounts
+
+A component which ties together an FxaAccountManager with the tabs feature, to
+facilitate OAuth authentication flows managed by the account manager.
+
+### 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-accounts:{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/accounts/build.gradle b/mobile/android/android-components/components/feature/accounts/build.gradle
new file mode 100644
index 0000000000..b508fe6697
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/build.gradle
@@ -0,0 +1,58 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.accounts'
+}
+
+tasks.register("updateBuiltInExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/fxawebchannel')
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_work_runtime
+
+ implementation project(':concept-engine')
+ implementation project(":browser-state")
+ implementation project(':feature-tabs')
+ implementation project(':service-firefox-accounts')
+ implementation project(':support-ktx')
+ implementation project(':support-webextensions')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.mozilla_appservices_full_megazord_forUnitTests
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ testImplementation project(':support-test')
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
+
+preBuild.dependsOn updateBuiltInExtensionVersion
diff --git a/mobile/android/android-components/components/feature/accounts/proguard-rules.pro b/mobile/android/android-components/components/feature/accounts/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/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/accounts/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/accounts/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/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/accounts/src/main/assets/extensions/fxawebchannel/background.js b/mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/background.js
new file mode 100644
index 0000000000..b90f57154a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/background.js
@@ -0,0 +1,22 @@
+/* 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/. */
+
+/*
+Establish communication with native application.
+*/
+const WEB_CHANNEL_BACKGROUND_MESSAGING_ID = "mozacWebchannelBackground";
+let port = browser.runtime.connectNative(WEB_CHANNEL_BACKGROUND_MESSAGING_ID);
+/*
+Handle messages from native application, register content script for specific url.
+*/
+port.onMessage.addListener( event => {
+ if(event.type == "overrideFxAServer"){
+ browser.contentScripts.register({
+ "matches": [ event.url+"/*" ],
+ "js": [{file: "fxawebchannel.js"}],
+ "runAt": "document_start"
+ });
+ port.disconnect();
+ }
+});
diff --git a/mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/fxawebchannel.js b/mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/fxawebchannel.js
new file mode 100644
index 0000000000..2f5934dff1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/fxawebchannel.js
@@ -0,0 +1,25 @@
+/* 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/. */
+
+/*
+Establish communication with native application.
+*/
+let port = browser.runtime.connectNative("mozacWebchannel");
+
+/*
+Handle messages from native application, dispatch them to FxA via an event.
+*/
+port.onMessage.addListener((event) => {
+ window.dispatchEvent(new CustomEvent('WebChannelMessageToContent', {
+ detail: JSON.stringify(event)
+ }));
+});
+
+/*
+Handle messages from FxA. Messages are posted to the native application for processing.
+*/
+window.addEventListener('WebChannelMessageToChrome', function (e) {
+ const detail = JSON.parse(e.detail);
+ port.postMessage(detail);
+});
diff --git a/mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/manifest.template.json b/mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/manifest.template.json
new file mode 100644
index 0000000000..609cb607e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/src/main/assets/extensions/fxawebchannel/manifest.template.json
@@ -0,0 +1,33 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "fxa@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - Firefox Accounts WebChannel",
+ "version": "${version}",
+ "content_scripts": [
+ {
+ "matches": [
+ "https://accounts.firefox.com/*",
+ "https://stable.dev.lcip.org/*",
+ "https://accounts.stage.mozaws.net/*",
+ "https://latest.dev.lcip.org/*",
+ "https://accounts.firefox.com.cn/*"
+ ],
+ "js": ["fxawebchannel.js"],
+ "run_at": "document_start"
+ }
+ ],
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "permissions": [
+ "mozillaAddons",
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent",
+ "<all_urls>"
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeature.kt b/mobile/android/android-components/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeature.kt
new file mode 100644
index 0000000000..60913282b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeature.kt
@@ -0,0 +1,119 @@
+/* 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.accounts
+
+import android.content.Context
+import android.net.Uri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.concept.sync.FxAEntryPoint
+import mozilla.components.service.fxa.FxaAuthData
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.toAuthType
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Ties together an account manager with a session manager/tabs implementation, facilitating an
+ * authentication flow.
+ * @property accountManager [FxaAccountManager].
+ * @property redirectUrl This is the url that will be reached at the final authentication state.
+ * @property coroutineContext The context which will be used to execute network requests necessary
+ * to initiate authentication. Note that [onBeginAuthentication] will be executed using this context.
+ * @property onBeginAuthentication A lambda function that receives the authentication url.
+ * Executed on [coroutineContext].
+ */
+class FirefoxAccountsAuthFeature(
+ private val accountManager: FxaAccountManager,
+ private val redirectUrl: String,
+ private val coroutineContext: CoroutineContext = Dispatchers.IO,
+ private val onBeginAuthentication: (Context, String) -> Unit = { _, _ -> },
+) {
+ /**
+ * Begins Email authentication, launching `onBeginAuthentication` if successful
+ * @param context [Context] The application context
+ * @param entrypoint [FxAEntryPoint] The Firefox Accounts feature/entrypoint that is launching
+ * authentication
+ */
+ fun beginAuthentication(context: Context, entrypoint: FxAEntryPoint) {
+ beginAuthenticationAsync(context) {
+ accountManager.beginAuthentication(entrypoint = entrypoint)
+ }
+ }
+
+ /**
+ * Begins Pairing authentication, launching `onBeginAuthentication` if successful
+ * @param context [Context] The application context
+ * @param pairingUrl [String] The pairing URL retrieved from the QR scanner
+ * @param entrypoint [FxAEntryPoint] The Firefox Accounts feature/entrypoint that is launching
+ * authentication
+ */
+ fun beginPairingAuthentication(
+ context: Context,
+ pairingUrl: String,
+ entrypoint: FxAEntryPoint,
+ ) {
+ beginAuthenticationAsync(context) {
+ accountManager.beginAuthentication(pairingUrl, entrypoint = entrypoint)
+ }
+ }
+
+ private fun beginAuthenticationAsync(context: Context, beginAuthentication: suspend () -> String?) {
+ CoroutineScope(coroutineContext).launch {
+ // FIXME return a fallback URL provided by Config...
+ // https://github.com/mozilla-mobile/android-components/issues/2496
+ val authUrl = beginAuthentication() ?: "https://accounts.firefox.com/signin"
+
+ // TODO
+ // We may fail to obtain an authentication URL, for example due to transient network errors.
+ // If that happens, open up a fallback URL in order to present some kind of a "no network"
+ // UI to the user.
+ // It's possible that the underlying problem will go away by the time the tab actually
+ // loads, resulting in a confusing experience.
+
+ onBeginAuthentication(context, authUrl)
+ }
+ }
+
+ val interceptor = object : RequestInterceptor {
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ if (uri.startsWith(redirectUrl)) {
+ val parsedUri = Uri.parse(uri)
+ val code = parsedUri.getQueryParameter("code")
+
+ if (code != null) {
+ val authType = parsedUri.getQueryParameter("action").toAuthType()
+ val state = parsedUri.getQueryParameter("state") as String
+
+ // Notify the state machine about our success.
+ CoroutineScope(Dispatchers.Main).launch {
+ accountManager.finishAuthentication(
+ FxaAuthData(
+ authType = authType,
+ code = code,
+ state = state,
+ ),
+ )
+ }
+
+ return RequestInterceptor.InterceptionResponse.Url(redirectUrl)
+ }
+ }
+
+ return null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FxaWebChannelFeature.kt b/mobile/android/android-components/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FxaWebChannelFeature.kt
new file mode 100644
index 0000000000..f5554c99e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FxaWebChannelFeature.kt
@@ -0,0 +1,407 @@
+/* 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.accounts
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+import mozilla.appservices.fxaclient.contentUrl
+import mozilla.appservices.fxaclient.isCustom
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.concept.engine.webextension.WebExtensionRuntime
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.service.fxa.FxaAuthData
+import mozilla.components.service.fxa.ServerConfig
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.sync.toSyncEngines
+import mozilla.components.service.fxa.toAuthType
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.isSameOriginAs
+import mozilla.components.support.webextensions.WebExtensionController
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.net.URL
+
+/**
+ * Configurable FxA capabilities.
+ */
+enum class FxaCapability {
+ // Enables "choose what to sync" selection during support auth flows (currently, sign-up).
+ CHOOSE_WHAT_TO_SYNC,
+}
+
+/**
+ * Feature implementation that provides Firefox Accounts WebChannel support.
+ * For more information https://github.com/mozilla/fxa/blob/master/packages/fxa-content-server/docs/relier-communication-protocols/fx-webchannel.md
+ * This feature uses a web extension to communicate with FxA Web Content.
+ *
+ * @property customTabSessionId optional custom tab session ID, if feature is being used with a custom tab.
+ * @property runtime the [WebExtensionRuntime] (e.g the browser engine) to use.
+ * @property store a reference to the application's [BrowserStore].
+ * @property accountManager a reference to application's [FxaAccountManager].
+ * @property fxaCapabilities a set of [FxaCapability] that client supports.
+ */
+class FxaWebChannelFeature(
+ private val customTabSessionId: String?,
+ private val runtime: WebExtensionRuntime,
+ private val store: BrowserStore,
+ private val accountManager: FxaAccountManager,
+ private val serverConfig: ServerConfig,
+ private val fxaCapabilities: Set<FxaCapability> = emptySet(),
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ @VisibleForTesting
+ // This is an internal var to make it mutable for unit testing purposes only
+ internal var extensionController = WebExtensionController(
+ WEB_CHANNEL_EXTENSION_ID,
+ WEB_CHANNEL_EXTENSION_URL,
+ WEB_CHANNEL_MESSAGING_ID,
+ )
+
+ override fun start() {
+ val messageHandler = WebChannelViewBackgroundMessageHandler(serverConfig)
+ extensionController.registerBackgroundMessageHandler(messageHandler, WEB_CHANNEL_BACKGROUND_MESSAGING_ID)
+
+ extensionController.install(runtime)
+
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(customTabSessionId) }
+ .distinctUntilChangedBy { it.engineState.engineSession }
+ .collect {
+ it.engineState.engineSession?.let { engineSession ->
+ registerFxaContentMessageHandler(engineSession)
+ }
+ }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ @Suppress("MaxLineLength", "")
+ /* ktlint-disable no-multi-spaces */
+ /**
+ * Communication channel is established from fxa-web-content to this class via webextension, as follows:
+ * [fxa-web-content] <--js events--> [fxawebchannel.js webextension] <--port messages--> [FxaWebChannelFeature]
+ *
+ * Overall message flow, as implemented by this class, is documented below. For detailed message descriptions, see:
+ * https://github.com/mozilla/fxa/blob/master/packages/fxa-content-server/docs/relier-communication-protocols/fx-webchannel.md
+ *
+ * [fxa-web-channel] [FxaWebChannelFeature] Notes:
+ * loaded ------> | fxa web content loaded
+ * fxa-status ------> | web content requests account status & device capabilities
+ * | <------ fxa-status-response this class responds, based on state of [accountManager]
+ * can-link-account ------> | user submitted credentials, web content verifying if account linking is allowed
+ * | <------ can-link-account-response this class responds, based on state of [accountManager]
+ * oauth-login ------> authentication completed within fxa web content, this class receives OAuth code & state
+ */
+ private class WebChannelViewContentMessageHandler(
+ private val accountManager: FxaAccountManager,
+ private val serverConfig: ServerConfig,
+ private val fxaCapabilities: Set<FxaCapability>,
+ ) : MessageHandler {
+ @SuppressWarnings("ComplexMethod")
+ override fun onPortMessage(message: Any, port: Port) {
+ if (!isCommunicationAllowed(serverConfig, port)) {
+ logger.error("Communication disallowed, ignoring WebChannel message.")
+ return
+ }
+
+ val json = try {
+ message as JSONObject
+ } catch (e: ClassCastException) {
+ logger.error("Received an invalid WebChannel message of type: ${message.javaClass}")
+ // TODO ideally, this should log to Sentry
+ return
+ }
+
+ val payload: JSONObject
+ val command: WebChannelCommand
+ val messageId: String
+
+ try {
+ payload = json.getJSONObject("message")
+ command = payload.getString("command").toWebChannelCommand() ?: throw
+ JSONException("Couldn't get WebChannel command")
+ messageId = payload.optString("messageId", "")
+ } catch (e: JSONException) {
+ // We don't have control over what messages we will get from the webchannel.
+ // If somehow we're receiving mis-constructed messages, it's probably best to not
+ // blow up the host application. This comes at a cost: we might not catch problems
+ // as quickly if we're not crashing (and thus receiving crash logs).
+ // TODO ideally, this should log to Sentry.
+ logger.error("Error while processing WebChannel command", e)
+ return
+ }
+
+ logger.debug("Processing WebChannel command: $command")
+
+ val response = when (command) {
+ WebChannelCommand.CAN_LINK_ACCOUNT -> processCanLinkAccountCommand(messageId)
+ WebChannelCommand.FXA_STATUS -> processFxaStatusCommand(accountManager, messageId, fxaCapabilities)
+ WebChannelCommand.OAUTH_LOGIN -> processOauthLoginCommand(accountManager, payload)
+ }
+ response?.let { port.postMessage(it) }
+ }
+ }
+
+ private fun registerFxaContentMessageHandler(engineSession: EngineSession) {
+ val messageHandler = WebChannelViewContentMessageHandler(accountManager, serverConfig, fxaCapabilities)
+ extensionController.registerContentMessageHandler(engineSession, messageHandler)
+ }
+
+ private class WebChannelViewBackgroundMessageHandler(
+ private val serverConfig: ServerConfig,
+ ) : MessageHandler {
+ override fun onPortConnected(port: Port) {
+ if (serverConfig.server.isCustom()) {
+ port.postMessage(
+ JSONObject()
+ .put("type", "overrideFxAServer")
+ .put("url", serverConfig.server.contentUrl()),
+ )
+ }
+ }
+ }
+
+ @VisibleForTesting
+ companion object {
+ private val logger = Logger("mozac-fxawebchannel")
+
+ internal const val WEB_CHANNEL_EXTENSION_ID = "fxa@mozac.org"
+ internal const val WEB_CHANNEL_MESSAGING_ID = "mozacWebchannel"
+ internal const val WEB_CHANNEL_BACKGROUND_MESSAGING_ID = "mozacWebchannelBackground"
+ internal const val WEB_CHANNEL_EXTENSION_URL = "resource://android/assets/extensions/fxawebchannel/"
+
+ // Constants for incoming messages from the WebExtension.
+ private const val CHANNEL_ID = "account_updates"
+
+ enum class WebChannelCommand {
+ CAN_LINK_ACCOUNT,
+ OAUTH_LOGIN,
+ FXA_STATUS,
+ }
+
+ // For all possible messages and their meaning/payloads, see:
+ // https://github.com/mozilla/fxa/blob/master/packages/fxa-content-server/docs/relier-communication-protocols/fx-webchannel.md
+
+ /**
+ * Gets triggered when user initiates a login within FxA web content.
+ * Expects a response.
+ * On Fx Desktop, this event triggers "a different user was previously signed in on this machine" warning.
+ */
+ private const val COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account"
+
+ /**
+ * Gets triggered when a user successfully authenticates via OAuth.
+ */
+ private const val COMMAND_OAUTH_LOGIN = "fxaccounts:oauth_login"
+
+ /**
+ * Gets triggered on startup to fetch the FxA state from the host application.
+ * Expects a response, which includes application's capabilities and a description of the
+ * current Firefox Account (if present).
+ */
+ private const val COMMAND_STATUS = "fxaccounts:fxa_status"
+
+ /**
+ * Handles the [COMMAND_CAN_LINK_ACCOUNT] event from the web-channel.
+ * Currently this always response with 'ok=true'.
+ * On Fx Desktop, this event prompts a possible "another user was previously logged in on
+ * this device" warning. Currently we don't support propagating this warning to a consuming application.
+ */
+ private fun processCanLinkAccountCommand(messageId: String): JSONObject {
+ // TODO don't allow linking if we're logged in already? This is requested after user
+ // entered their credentials.
+ return JSONObject().also { status ->
+ status.put("id", CHANNEL_ID)
+ status.put(
+ "message",
+ JSONObject().also { message ->
+ message.put("messageId", messageId)
+ message.put("command", COMMAND_CAN_LINK_ACCOUNT)
+ message.put(
+ "data",
+ JSONObject().also { data ->
+ data.put("ok", true)
+ },
+ )
+ },
+ )
+ }
+ }
+
+ /**
+ * Handles the [COMMAND_STATUS] event from the web-channel.
+ * Responds with supported application capabilities and information about currently signed-in Firefox Account.
+ */
+ @Suppress("ComplexMethod")
+ private fun processFxaStatusCommand(
+ accountManager: FxaAccountManager,
+ messageId: String,
+ fxaCapabilities: Set<FxaCapability>,
+ ): JSONObject {
+ val status = JSONObject()
+ status.put("id", CHANNEL_ID)
+ status.put(
+ "message",
+ JSONObject().also { message ->
+ message.put("messageId", messageId)
+ message.put("command", COMMAND_STATUS)
+ message.put(
+ "data",
+ JSONObject().also { data ->
+ data.put(
+ "capabilities",
+ JSONObject().also { capabilities ->
+ capabilities.put(
+ "engines",
+ JSONArray().also { engines ->
+ accountManager.supportedSyncEngines()?.forEach { engine ->
+ engines.put(engine.nativeName)
+ } ?: emptyArray<SyncEngine>()
+ },
+ )
+
+ if (fxaCapabilities.contains(FxaCapability.CHOOSE_WHAT_TO_SYNC)) {
+ capabilities.put("choose_what_to_sync", true)
+ }
+ },
+ )
+ val account = accountManager.authenticatedAccount()
+ if (account == null) {
+ data.put("signedInUser", JSONObject.NULL)
+ } else {
+ data.put(
+ "signedInUser",
+ JSONObject().also { signedInUser ->
+ signedInUser.put(
+ "email",
+ accountManager.accountProfile()?.email ?: JSONObject.NULL,
+ )
+ signedInUser.put(
+ "uid",
+ accountManager.accountProfile()?.uid ?: JSONObject.NULL,
+ )
+ signedInUser.put(
+ "sessionToken",
+ account.getSessionToken() ?: JSONObject.NULL,
+ )
+ // Our account state machine only ever completes authentication for
+ // "verified" accounts, so this is always 'true'.
+ signedInUser.put(
+ "verified",
+ true,
+ )
+ },
+ )
+ }
+ },
+ )
+ },
+ )
+ return status
+ }
+
+ private fun JSONArray.toStringList(): List<String> {
+ val result = mutableListOf<String>()
+ for (i in 0 until this.length()) {
+ this.optString(i, null)?.let { result.add(it) }
+ }
+ return result
+ }
+
+ /**
+ * Handles the [COMMAND_OAUTH_LOGIN] event from the web-channel.
+ */
+ private fun processOauthLoginCommand(accountManager: FxaAccountManager, payload: JSONObject): JSONObject? {
+ val authType: AuthType
+ val code: String
+ val state: String
+ val declinedEngines: List<String>?
+
+ try {
+ val data = payload.getJSONObject("data")
+ authType = data.getString("action").toAuthType()
+ code = data.getString("code")
+ state = data.getString("state")
+ declinedEngines = data.optJSONArray("declinedSyncEngines")?.toStringList()
+ } catch (e: JSONException) {
+ // TODO ideally, this should log to Sentry.
+ logger.error("Error while processing WebChannel oauth-login command", e)
+ return null
+ }
+
+ CoroutineScope(Dispatchers.Main).launch {
+ accountManager.finishAuthentication(
+ FxaAuthData(
+ authType = authType,
+ code = code,
+ state = state,
+ declinedEngines = declinedEngines?.toSyncEngines(),
+ ),
+ )
+ }
+
+ return null
+ }
+
+ private fun String.toWebChannelCommand(): WebChannelCommand? {
+ return when (this) {
+ COMMAND_CAN_LINK_ACCOUNT -> WebChannelCommand.CAN_LINK_ACCOUNT
+ COMMAND_OAUTH_LOGIN -> WebChannelCommand.OAUTH_LOGIN
+ COMMAND_STATUS -> WebChannelCommand.FXA_STATUS
+ else -> {
+ logger.warn("Unrecognized WebChannel command: $this")
+ null
+ }
+ }
+ }
+
+ private fun isCommunicationAllowed(serverConfig: ServerConfig, port: Port): Boolean {
+ val senderOrigin = port.senderUrl()
+ val expectedOrigin = serverConfig.server.contentUrl()
+ return isCommunicationAllowed(senderOrigin, expectedOrigin)
+ }
+
+ @VisibleForTesting
+ internal fun isCommunicationAllowed(senderOrigin: String, expectedOrigin: String): Boolean {
+ if (!isSafeUrl(senderOrigin)) {
+ logger.error("$senderOrigin looks unsafe, aborting.")
+ return false
+ }
+
+ if (!senderOrigin.isSameOriginAs(expectedOrigin)) {
+ logger.error("Host mismatch for WebChannel message. Expected: $expectedOrigin, got: $senderOrigin.")
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Rejects URLs that are deemed "unsafe" (not expected).
+ */
+ private fun isSafeUrl(urlStr: String): Boolean {
+ val url = URL(urlStr)
+ return url.userInfo.isNullOrEmpty() &&
+ (url.protocol == "https" || url.host == "localhost" || url.host == "127.0.0.1")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt b/mobile/android/android-components/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt
new file mode 100644
index 0000000000..a32681e8fe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt
@@ -0,0 +1,304 @@
+/* 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.accounts
+
+import android.content.Context
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.test.runTest
+import mozilla.appservices.fxaclient.FxaServer
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.AuthFlowUrl
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.FxAEntryPoint
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.FxaAuthData
+import mozilla.components.service.fxa.ServerConfig
+import mozilla.components.service.fxa.StorageWrapper
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.base.observer.ObserverRegistry
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import kotlin.coroutines.CoroutineContext
+
+internal class TestableStorageWrapper(
+ manager: FxaAccountManager,
+ accountEventObserverRegistry: ObserverRegistry<AccountEventsObserver>,
+ serverConfig: ServerConfig,
+ private val block: () -> OAuthAccount = {
+ val account: OAuthAccount = mock()
+ `when`(account.deviceConstellation()).thenReturn(mock())
+ account
+ },
+) : StorageWrapper(manager, accountEventObserverRegistry, serverConfig) {
+ override fun obtainAccount(): OAuthAccount = block()
+}
+
+// Same as the actual account manager, except we get to control how FirefoxAccountShaped instances
+// are created. This is necessary because due to some build issues (native dependencies not available
+// within the test environment) we can't use fxaclient supplied implementation of FirefoxAccountShaped.
+// Instead, we express all of our account-related operations over an interface.
+class TestableFxaAccountManager(
+ context: Context,
+ config: ServerConfig,
+ scopes: Set<String>,
+ coroutineContext: CoroutineContext,
+ block: () -> OAuthAccount = { mock() },
+) : FxaAccountManager(context, config, DeviceConfig("test", DeviceType.MOBILE, setOf()), null, scopes, null, coroutineContext) {
+ private val testableStorageWrapper = TestableStorageWrapper(this, accountEventObserverRegistry, serverConfig, block)
+ override fun getStorageWrapper(): StorageWrapper {
+ return testableStorageWrapper
+ }
+}
+
+@RunWith(AndroidJUnit4::class)
+class FirefoxAccountsAuthFeatureTest {
+ val mockEntryPoint: FxAEntryPoint = mock<FxAEntryPoint>().apply {
+ whenever(entryName).thenReturn("home-menu")
+ }
+
+ // Note that tests that involve secure storage specify API=21, because of issues testing secure storage on
+ // 23+ API levels. See https://github.com/mozilla-mobile/android-components/issues/4956
+
+ @Config(sdk = [22])
+ @Test
+ fun `begin authentication`() = runTest {
+ val manager = prepareAccountManagerForSuccessfulAuthentication(
+ this.coroutineContext,
+ )
+ val authUrl = CompletableDeferred<String>()
+ val feature = FirefoxAccountsAuthFeature(
+ manager,
+ "somePath",
+ this.coroutineContext,
+ ) { _, url ->
+ authUrl.complete(url)
+ }
+ feature.beginAuthentication(testContext, mockEntryPoint)
+ authUrl.await()
+ assertEquals("auth://url", authUrl.getCompleted())
+ }
+
+ @Config(sdk = [22])
+ @Test
+ fun `begin pairing authentication`() = runTest {
+ val manager = prepareAccountManagerForSuccessfulAuthentication(
+ this.coroutineContext,
+ )
+ val authUrl = CompletableDeferred<String>()
+ val feature = FirefoxAccountsAuthFeature(
+ manager,
+ "somePath",
+ this.coroutineContext,
+ ) { _, url ->
+ authUrl.complete(url)
+ }
+ feature.beginPairingAuthentication(testContext, "auth://pair", mockEntryPoint)
+ authUrl.await()
+ assertEquals("auth://url", authUrl.getCompleted())
+ }
+
+ @Config(sdk = [22])
+ @Test
+ fun `begin authentication with errors`() = runTest {
+ val manager = prepareAccountManagerForFailedAuthentication(
+ this.coroutineContext,
+ )
+ val authUrl = CompletableDeferred<String>()
+
+ val feature = FirefoxAccountsAuthFeature(
+ manager,
+ "somePath",
+ this.coroutineContext,
+ ) { _, url ->
+ authUrl.complete(url)
+ }
+ feature.beginAuthentication(testContext, mockEntryPoint)
+ authUrl.await()
+ // Fallback url is invoked.
+ assertEquals("https://accounts.firefox.com/signin", authUrl.getCompleted())
+ }
+
+ @Config(sdk = [22])
+ @Test
+ fun `begin pairing authentication with errors`() = runTest {
+ val manager = prepareAccountManagerForFailedAuthentication(
+ this.coroutineContext,
+ )
+ val authUrl = CompletableDeferred<String>()
+
+ val feature = FirefoxAccountsAuthFeature(
+ manager,
+ "somePath",
+ this.coroutineContext,
+ ) { _, url ->
+ authUrl.complete(url)
+ }
+ feature.beginPairingAuthentication(testContext, "auth://pair", mockEntryPoint)
+ authUrl.await()
+ // Fallback url is invoked.
+ assertEquals("https://accounts.firefox.com/signin", authUrl.getCompleted())
+ }
+
+ @Test
+ fun `auth interceptor`() = runTest {
+ val manager = mock<FxaAccountManager>()
+ val redirectUrl = "https://accounts.firefox.com/oauth/success/123"
+ val feature = FirefoxAccountsAuthFeature(
+ manager,
+ redirectUrl,
+ mock(),
+ ) { _, _ -> }
+
+ // Non-final FxA url.
+ assertNull(feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/not/the/right/url", null, false, false, false, false, false))
+ verify(manager, never()).finishAuthentication(any())
+
+ // Non-FxA url.
+ assertNull(feature.interceptor.onLoadRequest(mock(), "https://www.wikipedia.org", null, false, false, false, false, false))
+ verify(manager, never()).finishAuthentication(any())
+
+ // Redirect url, without code/state.
+ assertNull(feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/", null, false, false, false, false, false))
+ verify(manager, never()).finishAuthentication(any())
+
+ // Redirect url, without code/state.
+ assertNull(feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/test", null, false, false, false, false, false))
+ verify(manager, never()).finishAuthentication(any())
+
+ // Code+state, no action.
+ assertEquals(
+ RequestInterceptor.InterceptionResponse.Url(redirectUrl),
+ feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode1&state=testState1", null, false, false, false, false, false),
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(manager).finishAuthentication(
+ FxaAuthData(authType = AuthType.OtherExternal(null), code = "testCode1", state = "testState1"),
+ )
+
+ // Code+state, action=signin.
+ assertEquals(
+ RequestInterceptor.InterceptionResponse.Url(redirectUrl),
+ feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode2&state=testState2&action=signin", null, false, false, false, false, false),
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(manager).finishAuthentication(
+ FxaAuthData(authType = AuthType.Signin, code = "testCode2", state = "testState2"),
+ )
+
+ // Code+state, action=signup.
+ assertEquals(
+ RequestInterceptor.InterceptionResponse.Url(redirectUrl),
+ feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode3&state=testState3&action=signup", null, false, false, false, false, false),
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(manager).finishAuthentication(
+ FxaAuthData(authType = AuthType.Signup, code = "testCode3", state = "testState3"),
+ )
+
+ // Code+state, action=pairing.
+ assertEquals(
+ RequestInterceptor.InterceptionResponse.Url(redirectUrl),
+ feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode4&state=testState4&action=pairing", null, false, false, false, false, false),
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(manager).finishAuthentication(
+ FxaAuthData(authType = AuthType.Pairing, code = "testCode4", state = "testState4"),
+ )
+
+ // Code+state, action is an unknown value.
+ assertEquals(
+ RequestInterceptor.InterceptionResponse.Url(redirectUrl),
+ feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode5&state=testState5&action=someNewActionType", null, false, false, false, false, false),
+ )
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(manager).finishAuthentication(
+ FxaAuthData(authType = AuthType.OtherExternal("someNewActionType"), code = "testCode5", state = "testState5"),
+ )
+ Unit
+ }
+
+ @Config(sdk = [22])
+ private suspend fun prepareAccountManagerForSuccessfulAuthentication(
+ coroutineContext: CoroutineContext,
+ ): TestableFxaAccountManager {
+ val mockAccount: OAuthAccount = mock()
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+
+ `when`(mockAccount.deviceConstellation()).thenReturn(mock())
+ `when`(mockAccount.getProfile(anyBoolean())).thenReturn(profile)
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(AuthFlowUrl("authState", "auth://url"))
+ `when`(mockAccount.beginPairingFlow(anyString(), any(), any())).thenReturn(AuthFlowUrl("authState", "auth://url"))
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ ServerConfig(FxaServer.Release, "dummyId", "bad://url"),
+ setOf("test-scope"),
+ coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ manager.start()
+
+ return manager
+ }
+
+ @Config(sdk = [22])
+ private suspend fun prepareAccountManagerForFailedAuthentication(
+ coroutineContext: CoroutineContext,
+ ): TestableFxaAccountManager {
+ val mockAccount: OAuthAccount = mock()
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+
+ `when`(mockAccount.getProfile(anyBoolean())).thenReturn(profile)
+ `when`(mockAccount.deviceConstellation()).thenReturn(mock())
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(null)
+ `when`(mockAccount.beginPairingFlow(anyString(), any(), any())).thenReturn(null)
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ ServerConfig(FxaServer.Release, "dummyId", "bad://url"),
+ setOf("test-scope"),
+ coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ manager.start()
+
+ return manager
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FxaWebChannelFeatureTest.kt b/mobile/android/android-components/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FxaWebChannelFeatureTest.kt
new file mode 100644
index 0000000000..809ed7a703
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FxaWebChannelFeatureTest.kt
@@ -0,0 +1,830 @@
+/* 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.accounts
+
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.appservices.fxaclient.FxaServer
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.FxaAuthData
+import mozilla.components.service.fxa.ServerConfig
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import mozilla.components.support.webextensions.WebExtensionController
+import org.json.JSONException
+import org.json.JSONObject
+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.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class FxaWebChannelFeatureTest {
+
+ @Before
+ fun setup() {
+ WebExtensionController.installedExtensions.clear()
+ }
+
+ @Test
+ fun `start installs webextension`() {
+ val engine: Engine = mock()
+ val store: BrowserStore = mock()
+ val accountManager: FxaAccountManager = mock()
+ val serverConfig: ServerConfig = mock()
+ val webchannelFeature = FxaWebChannelFeature(null, engine, store, accountManager, serverConfig)
+ webchannelFeature.start()
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_URL),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+
+ onSuccess.value.invoke(mock())
+
+ // Already installed, should not try to install again.
+ webchannelFeature.start()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_URL),
+ any(),
+ any(),
+ )
+ }
+
+ @Test
+ fun `start registers the background message handler`() {
+ val engine: Engine = mock()
+ val store: BrowserStore = mock()
+ val accountManager: FxaAccountManager = mock()
+ val serverConfig: ServerConfig = mock()
+ val controller: WebExtensionController = mock()
+ val webchannelFeature = FxaWebChannelFeature(null, engine, store, accountManager, serverConfig)
+
+ webchannelFeature.extensionController = controller
+
+ webchannelFeature.start()
+
+ verify(controller).registerBackgroundMessageHandler(any(), any())
+ }
+
+ @Test
+ fun `backgroundMessageHandler sends overrideFxAServer`() {
+ val engine: Engine = mock()
+ val store: BrowserStore = mock()
+ val accountManager: FxaAccountManager = mock()
+ val serverConfig: ServerConfig = mock()
+ val controller: WebExtensionController = mock()
+ val webchannelFeature = FxaWebChannelFeature(null, engine, store, accountManager, serverConfig)
+
+ whenever(serverConfig.server).thenReturn(FxaServer.Custom("https://foo.bar"))
+ webchannelFeature.extensionController = controller
+
+ webchannelFeature.start()
+
+ val messageHandler = argumentCaptor<MessageHandler>()
+ verify(controller).registerBackgroundMessageHandler(messageHandler.capture(), any())
+
+ val port: Port = mock()
+ val message = argumentCaptor<JSONObject>()
+ messageHandler.value.onPortConnected(port)
+ verify(port).postMessage(message.capture())
+
+ val overrideUrlMessage = JSONObject().put("type", "overrideFxAServer").put("url", "https://foo.bar")
+ verify(port, times(1)).postMessage(message.capture())
+
+ assertEquals(overrideUrlMessage.toString(), message.value.toString())
+ }
+
+ @Test
+ fun `backgroundMessageHandler should not send overrideFxAServer for predefined Config`() {
+ val engine: Engine = mock()
+ val store: BrowserStore = mock()
+ val accountManager: FxaAccountManager = mock()
+ val serverConfig: ServerConfig = mock()
+ val controller: WebExtensionController = mock()
+ val webchannelFeature = FxaWebChannelFeature(null, engine, store, accountManager, serverConfig)
+
+ whenever(serverConfig.server).thenReturn(FxaServer.Release)
+ webchannelFeature.extensionController = controller
+
+ webchannelFeature.start()
+
+ val messageHandler = argumentCaptor<MessageHandler>()
+ verify(controller).registerBackgroundMessageHandler(messageHandler.capture(), any())
+
+ val port: Port = mock()
+ messageHandler.value.onPortConnected(port)
+
+ verify(port, never()).postMessage(any())
+ }
+
+ @Test
+ fun `start registers content message handler for selected session`() {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ val accountManager: FxaAccountManager = mock()
+ val serverConfig: ServerConfig = mock()
+ val controller: WebExtensionController = mock()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab", engineSession = engineSession)
+ val store = spy(
+ BrowserStore(initialState = BrowserState(tabs = listOf(tab), selectedTabId = tab.id)),
+ )
+
+ val webchannelFeature = FxaWebChannelFeature(null, engine, store, accountManager, serverConfig)
+ webchannelFeature.extensionController = controller
+
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(controller).registerContentMessageHandler(eq(engineSession), any(), any())
+ }
+
+ @Test
+ fun `Ignores messages coming from a different FxA host than configured`() {
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val expectedEngines: Set<SyncEngine> = setOf(SyncEngine.History)
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines)
+ whenever(port.senderUrl()).thenReturn("https://bar.foo/email")
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port, never()).postMessage(any())
+ }
+
+ @Test
+ fun `COMMAND_STATUS configured with CWTS must provide a boolean=true flag to the web-channel`() {
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val expectedEngines: Set<SyncEngine> = setOf(SyncEngine.History)
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val responseToTheWebChannel = argumentCaptor<JSONObject>()
+ val webchannelFeature = prepareFeatureForTest(
+ ext,
+ port,
+ engineSession,
+ expectedEngines,
+ setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC),
+ )
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port).postMessage(responseToTheWebChannel.capture())
+ assertTrue(responseToTheWebChannel.value.getCWTSSupport()!!)
+ }
+
+ // Receiving and responding a fxa-status message if sync is configured with one engine
+ @Test
+ fun `COMMAND_STATUS configured with one engine must be provided to the web-channel`() {
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val expectedEngines: Set<SyncEngine> = setOf(SyncEngine.History)
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val responseToTheWebChannel = argumentCaptor<JSONObject>()
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port).postMessage(responseToTheWebChannel.capture())
+
+ val capabilitiesFromWebChannel = responseToTheWebChannel.value.getSupportedEngines()
+ assertTrue(capabilitiesFromWebChannel.size == 1)
+ assertNull(responseToTheWebChannel.value.getCWTSSupport())
+
+ assertTrue(responseToTheWebChannel.value.isSignedInUserNull())
+ }
+
+ // Receiving and responding a fxa-status message if sync is configured with more than one engine
+ @Test
+ fun `COMMAND_STATUS configured with more than one engine must be provided to the web-channel`() {
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val expectedEngines: Set<SyncEngine> = setOf(SyncEngine.History)
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val responseToTheWebChannel = argumentCaptor<JSONObject>()
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port).postMessage(responseToTheWebChannel.capture())
+
+ val capabilitiesFromWebChannel = responseToTheWebChannel.value.getSupportedEngines()
+ assertTrue(
+ expectedEngines.all {
+ capabilitiesFromWebChannel.contains(it.nativeName)
+ },
+ )
+
+ assertNull(responseToTheWebChannel.value.getCWTSSupport())
+ assertTrue(responseToTheWebChannel.value.isSignedInUserNull())
+ }
+
+ // Receiving and responding a fxa-status message if account manager is logged in
+ @Test
+ fun `COMMAND_STATUS with account manager is logged in with profile`() {
+ val accountManager: FxaAccountManager = mock()
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val expectedEngines: Set<SyncEngine> = setOf(SyncEngine.History)
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val responseToTheWebChannel = argumentCaptor<JSONObject>()
+
+ val account: OAuthAccount = mock()
+ val profile = Profile(uid = "testUID", email = "test@example.com", avatar = null, displayName = null)
+ whenever(account.getSessionToken()).thenReturn("testToken")
+ whenever(accountManager.accountProfile()).thenReturn(profile)
+ whenever(accountManager.authenticatedAccount()).thenReturn(account)
+ whenever(accountManager.supportedSyncEngines()).thenReturn(expectedEngines)
+
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines, emptySet(), accountManager)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port).postMessage(responseToTheWebChannel.capture())
+
+ val capabilitiesFromWebChannel = responseToTheWebChannel.value.getSupportedEngines()
+ assertTrue(
+ expectedEngines.all {
+ capabilitiesFromWebChannel.contains(it.nativeName)
+ },
+ )
+
+ assertNull(responseToTheWebChannel.value.getCWTSSupport())
+
+ val signedInUser = responseToTheWebChannel.value.signedInUser()
+ assertEquals("test@example.com", signedInUser.email)
+ assertEquals("testUID", signedInUser.uid)
+ assertTrue(signedInUser.verified)
+ assertEquals("testToken", signedInUser.sessionToken)
+ }
+
+ @Test
+ fun `COMMAND_STATUS with account manager is logged in without profile`() {
+ val accountManager: FxaAccountManager = mock()
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val expectedEngines: Set<SyncEngine> = setOf(SyncEngine.History)
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val responseToTheWebChannel = argumentCaptor<JSONObject>()
+
+ val account: OAuthAccount = mock()
+ whenever(account.getSessionToken()).thenReturn("testToken")
+ whenever(accountManager.accountProfile()).thenReturn(null)
+ whenever(accountManager.authenticatedAccount()).thenReturn(account)
+ whenever(accountManager.supportedSyncEngines()).thenReturn(expectedEngines)
+
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines, emptySet(), accountManager)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port).postMessage(responseToTheWebChannel.capture())
+
+ val capabilitiesFromWebChannel = responseToTheWebChannel.value.getSupportedEngines()
+ assertTrue(
+ expectedEngines.all {
+ capabilitiesFromWebChannel.contains(it.nativeName)
+ },
+ )
+
+ assertNull(responseToTheWebChannel.value.getCWTSSupport())
+
+ val signedInUser = responseToTheWebChannel.value.signedInUser()
+ assertNull(signedInUser.email)
+ assertNull(signedInUser.uid)
+ assertTrue(signedInUser.verified)
+ assertEquals("testToken", signedInUser.sessionToken)
+ }
+
+ // Receiving and responding a fxa-status message if account manager is logged out
+ @Test
+ fun `COMMAND_STATUS with account manager is logged out`() {
+ val accountManager: FxaAccountManager = mock()
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val expectedEngines = setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords)
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val responseToTheWebChannel = argumentCaptor<JSONObject>()
+
+ whenever(accountManager.accountProfile()).thenReturn(null)
+ whenever(accountManager.supportedSyncEngines()).thenReturn(expectedEngines)
+ WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
+
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines, emptySet(), accountManager)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port).postMessage(responseToTheWebChannel.capture())
+
+ val capabilitiesFromWebChannel = responseToTheWebChannel.value.getSupportedEngines()
+ assertTrue(
+ expectedEngines.all {
+ capabilitiesFromWebChannel.contains(it.nativeName)
+ },
+ )
+
+ assertNull(responseToTheWebChannel.value.getCWTSSupport())
+ assertTrue(responseToTheWebChannel.value.isSignedInUserNull())
+ }
+
+ // Receiving and responding a fxa-status message if account manager when sync is not configured
+ @Test
+ fun `COMMAND_STATUS with account manager when sync is not configured`() {
+ val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val expectedEngines = setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords)
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val responseToTheWebChannel = argumentCaptor<JSONObject>()
+
+ WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
+
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines, emptySet(), accountManager)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port).postMessage(responseToTheWebChannel.capture())
+
+ val capabilitiesFromWebChannel = responseToTheWebChannel.value.getSupportedEngines()
+ assertTrue(
+ expectedEngines.all {
+ capabilitiesFromWebChannel.contains(it.nativeName)
+ },
+ )
+
+ assertNull(responseToTheWebChannel.value.getCWTSSupport())
+ assertTrue(responseToTheWebChannel.value.isSignedInUserNull())
+ }
+
+ @Test
+ fun `COMMAND_STATUS with no capabilities configured must provide an empty list of engines to the web-channel`() {
+ val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val responseToTheWebChannel = argumentCaptor<JSONObject>()
+
+ whenever(accountManager.supportedSyncEngines()).thenReturn(null)
+ WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
+
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, null, emptySet(), accountManager)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val requestFromTheWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:fxa_status",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(requestFromTheWebChannel, port)
+ verify(port).postMessage(responseToTheWebChannel.capture())
+
+ assertNull(responseToTheWebChannel.value.getCWTSSupport())
+ val capabilitiesFromWebChannel = responseToTheWebChannel.value.getSupportedEngines()
+ assertTrue(capabilitiesFromWebChannel.isEmpty())
+ }
+
+ // Receiving an oauth-login message account manager accepts the request
+ @Test
+ fun `COMMAND_OAUTH_LOGIN web-channel must be processed through when the accountManager accepts the request`() = runTest {
+ val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val messageHandler = argumentCaptor<MessageHandler>()
+
+ WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
+
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, null, emptySet(), accountManager)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ // Action: signin
+ verifyOauthLogin("signin", AuthType.Signin, "fffs", "fsdf32", null, messageHandler.value, accountManager)
+ // Signup.
+ verifyOauthLogin("signup", AuthType.Signup, "anotherCode1", "anotherState2", setOf(SyncEngine.Passwords), messageHandler.value, accountManager)
+ // Pairing.
+ verifyOauthLogin("pairing", AuthType.Pairing, "anotherCode2", "anotherState3", null, messageHandler.value, accountManager)
+ // Some other action.
+ verifyOauthLogin("newAction", AuthType.OtherExternal("newAction"), "anotherCode3", "anotherState4", null, messageHandler.value, accountManager)
+ }
+
+ // Receiving an oauth-login message account manager refuses the request
+ @Test
+ fun `COMMAND_OAUTH_LOGIN web-channel must be processed when the accountManager refuses the request`() = runTest {
+ val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val messageHandler = argumentCaptor<MessageHandler>()
+
+ WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
+
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, null, emptySet(), accountManager)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ // Action: signin
+ verifyOauthLogin("signin", AuthType.Signin, "fffs", "fsdf32", setOf(SyncEngine.Passwords, SyncEngine.Bookmarks), messageHandler.value, accountManager)
+ // Signup.
+ verifyOauthLogin("signup", AuthType.Signup, "anotherCode1", "anotherState2", null, messageHandler.value, accountManager)
+ // Pairing.
+ verifyOauthLogin("pairing", AuthType.Pairing, "anotherCode2", "anotherState3", null, messageHandler.value, accountManager)
+ // Some other action.
+ verifyOauthLogin("newAction", AuthType.OtherExternal("newAction"), "anotherCode3", "anotherState4", null, messageHandler.value, accountManager)
+ }
+
+ // Receiving can-link-account returns 'ok=true' message (for now)
+ @Test
+ fun `COMMAND_CAN_LINK_ACCOUNT must provide an OK response to the web-channel`() {
+ val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val port: Port = mock()
+ val jsonFromWebChannel = argumentCaptor<JSONObject>()
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val expectedEngines = setOf(SyncEngine.History, SyncEngine.Bookmarks)
+
+ whenever(accountManager.supportedSyncEngines()).thenReturn(expectedEngines)
+ WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
+
+ val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines, emptySet(), accountManager)
+ webchannelFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(ext).registerContentMessageHandler(
+ eq(engineSession),
+ eq(FxaWebChannelFeature.WEB_CHANNEL_MESSAGING_ID),
+ messageHandler.capture(),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val jsonToWebChannel = JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:can_link_account",
+ "messageId":123
+ }
+ }
+ """.trimIndent(),
+ )
+
+ messageHandler.value.onPortMessage(jsonToWebChannel, port)
+ verify(port).postMessage(jsonFromWebChannel.capture())
+
+ assertTrue(jsonFromWebChannel.value.getOk())
+ }
+
+ @Test
+ fun `isCommunicationAllowed extensive testing`() {
+ // Unsafe URL: not https.
+ assertFalse(FxaWebChannelFeature.isCommunicationAllowed("http://foo.bar", "http://foo.bar"))
+ // Unsafe URL: login in url.
+ assertFalse(FxaWebChannelFeature.isCommunicationAllowed("http://bobo:bobo@foo.bar", "http://foo.bar"))
+ // Origin mismatch.
+ assertFalse(FxaWebChannelFeature.isCommunicationAllowed("https://foo.bar", "https://foo.baz"))
+
+ // Happy cases
+ assertTrue(FxaWebChannelFeature.isCommunicationAllowed("https://foo.bar", "https://foo.bar"))
+ // HTTP is allowed for localhost.
+ assertTrue(FxaWebChannelFeature.isCommunicationAllowed("http://127.0.0.1", "http://127.0.0.1"))
+ assertTrue(FxaWebChannelFeature.isCommunicationAllowed("http://localhost", "http://localhost"))
+ }
+
+ private fun JSONObject.getSupportedEngines(): List<String> {
+ val engines = this.getJSONObject("message")
+ .getJSONObject("data")
+ .getJSONObject("capabilities")
+ .getJSONArray("engines")
+
+ val list = mutableListOf<String>()
+ for (i in 0 until engines.length()) {
+ list.add(engines[i].toString())
+ }
+ return list
+ }
+
+ private fun JSONObject.getCWTSSupport(): Boolean? {
+ return try {
+ this.getJSONObject("message")
+ .getJSONObject("data")
+ .getJSONObject("capabilities")
+ .getBoolean("choose_what_to_sync")
+ } catch (e: JSONException) {
+ null
+ }
+ }
+
+ data class SignedInUser(val email: String?, val uid: String?, val sessionToken: String, val verified: Boolean)
+
+ private fun JSONObject.signedInUser(): SignedInUser {
+ val obj = this.getJSONObject("message")
+ .getJSONObject("data")
+ .getJSONObject("signedInUser")
+
+ val email = if (obj.getString("email") == "null") {
+ null
+ } else {
+ obj.getString("email")
+ }
+ val uid = if (obj.getString("uid") == "null") {
+ null
+ } else {
+ obj.getString("uid")
+ }
+ return SignedInUser(
+ email = email,
+ uid = uid,
+ sessionToken = obj.getString("sessionToken"),
+ verified = obj.getBoolean("verified"),
+ )
+ }
+
+ private fun JSONObject.isSignedInUserNull(): Boolean {
+ return this.getJSONObject("message")
+ .getJSONObject("data")
+ .isNull("signedInUser")
+ }
+
+ private fun JSONObject.getOk(): Boolean {
+ return this.getJSONObject("message")
+ .getJSONObject("data")
+ .getBoolean("ok")
+ }
+
+ private suspend fun verifyOauthLogin(action: String, expectedAuthType: AuthType, code: String, state: String, declined: Set<SyncEngine>?, messageHandler: MessageHandler, accountManager: FxaAccountManager) {
+ val jsonToWebChannel = jsonOauthLogin(action, code, state, declined ?: emptySet())
+ val port = mock<Port>()
+ whenever(port.senderUrl()).thenReturn("https://foo.bar/email")
+ messageHandler.onPortMessage(jsonToWebChannel, port)
+
+ val expectedAuthData = FxaAuthData(
+ authType = expectedAuthType,
+ code = code,
+ state = state,
+ declinedEngines = declined ?: emptySet(),
+ )
+ shadowOf(getMainLooper()).idle()
+
+ verify(accountManager).finishAuthentication(expectedAuthData)
+ }
+
+ private fun jsonOauthLogin(action: String, code: String, state: String, declined: Set<SyncEngine>): JSONObject {
+ return JSONObject(
+ """{
+ "message":{
+ "command": "fxaccounts:oauth_login",
+ "messageId":123,
+ "data":{
+ "action":"$action",
+ "redirect":"urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
+ "code":"$code",
+ "state":"$state",
+ "declinedSyncEngines":${declined.map { "${it.nativeName}," }.filterNotNull()}
+ }
+ }
+ }
+ """.trimIndent(),
+ )
+ }
+
+ private fun prepareFeatureForTest(
+ ext: WebExtension = mock(),
+ port: Port = mock(),
+ engineSession: EngineSession = mock(),
+ expectedEngines: Set<SyncEngine>? = setOf(SyncEngine.History),
+ fxaCapabilities: Set<FxaCapability> = emptySet(),
+ accountManager: FxaAccountManager = mock(),
+ ): FxaWebChannelFeature {
+ val serverConfig: ServerConfig = mock()
+ WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
+
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ id = "test-tab",
+ engineSession = engineSession,
+ )
+ val store = spy(
+ BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab), selectedTabId = tab.id),
+ ),
+ )
+
+ whenever(accountManager.supportedSyncEngines()).thenReturn(expectedEngines)
+ whenever(port.engineSession).thenReturn(engineSession)
+ whenever(port.senderUrl()).thenReturn("https://foo.bar/email")
+ whenever(serverConfig.server).thenReturn(FxaServer.Custom("https://foo.bar"))
+
+ return spy(FxaWebChannelFeature(null, mock(), store, accountManager, serverConfig, fxaCapabilities))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/accounts/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/accounts/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/accounts/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/accounts/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/addons/README.md b/mobile/android/android-components/components/feature/addons/README.md
new file mode 100644
index 0000000000..b669b982cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Add-ons
+
+A feature that provides functionality for managing add-ons.
+
+## Usage
+
+### 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-addons:{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/addons/build.gradle b/mobile/android/android-components/components/feature/addons/build.gradle
new file mode 100644
index 0000000000..b6bd5641cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/build.gradle
@@ -0,0 +1,89 @@
+/* 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: 'kotlin-parcelize'
+
+apply plugin: 'com.google.devtools.ksp'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'mozilla.components.feature.addons'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_cardview
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_recyclerview
+ implementation ComponentsDependencies.androidx_work_runtime
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ implementation project(':ui-colors')
+ implementation project(':ui-icons')
+ implementation project(':ui-widgets')
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':concept-fetch')
+ implementation project(':concept-menu')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-webextensions')
+ implementation project(':support-utils')
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.androidx_work_testing
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/addons/proguard-rules.pro b/mobile/android/android-components/components/feature/addons/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/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/addons/schemas/mozilla.components.feature.addons.update.db.UpdateAttemptsDatabase/1.json b/mobile/android/android-components/components/feature/addons/schemas/mozilla.components.feature.addons.update.db.UpdateAttemptsDatabase/1.json
new file mode 100644
index 0000000000..c1c649e60e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/schemas/mozilla.components.feature.addons.update.db.UpdateAttemptsDatabase/1.json
@@ -0,0 +1,58 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "827e1c60d24034d74c143a305c63a6d9",
+ "entities": [
+ {
+ "tableName": "update_attempts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addon_id` TEXT NOT NULL, `date` INTEGER NOT NULL, `status` INTEGER NOT NULL, `error_message` TEXT NOT NULL, `error_trace` TEXT NOT NULL, PRIMARY KEY(`addon_id`))",
+ "fields": [
+ {
+ "fieldPath": "addonId",
+ "columnName": "addon_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "errorMessage",
+ "columnName": "error_message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "errorTrace",
+ "columnName": "error_trace",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "addon_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '827e1c60d24034d74c143a305c63a6d9')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/addons/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..61826b15cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <application android:supportsRtl="true">
+ <service android:name=".update.DefaultAddonUpdater$NotificationHandlerService"
+ android:exported="false" />
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/Addon.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/Addon.kt
new file mode 100644
index 0000000000..5f0cefcabe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/Addon.kt
@@ -0,0 +1,540 @@
+/* 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.addons
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.os.Parcelable
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import kotlinx.parcelize.Parcelize
+import mozilla.components.concept.engine.webextension.Incognito
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.support.base.log.logger.Logger
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+typealias GeckoIncognito = Incognito
+
+val logger = Logger("Addon")
+
+/**
+ * Represents an add-on based on the AMO store:
+ * https://addons.mozilla.org/en-US/firefox/
+ *
+ * @property id The unique ID of this add-on.
+ * @property author Information about the add-on author.
+ * @property downloadUrl The (absolute) URL to download the latest version of the add-on file.
+ * @property version The add-on version e.g "1.23.0".
+ * @property permissions List of the add-on permissions for this File.
+ * @property optionalPermissions Optional permissions requested or granted to this add-on.
+ * @property optionalOrigins Optional origin permissions requested or granted to this add-on.
+ * @property translatableName A map containing the different translations for the add-on name,
+ * where the key is the language and the value is the actual translated text.
+ * @property translatableDescription A map containing the different translations for the add-on description,
+ * where the key is the language and the value is the actual translated text.
+ * @property translatableSummary A map containing the different translations for the add-on name,
+ * where the key is the language and the value is the actual translated text.
+ * @property iconUrl The URL to icon for the add-on.
+ * @property homepageUrl The add-on homepage.
+ * @property rating The rating information of this add-on.
+ * @property createdAt The date the add-on was created.
+ * @property updatedAt The date of the last time the add-on was updated by its developer(s).
+ * @property icon the icon of the this [Addon], available when the icon is loaded.
+ * @property installedState Holds the state of the installed web extension for this add-on. Null, if
+ * the [Addon] is not installed.
+ * @property defaultLocale Indicates which locale will be always available to display translatable fields.
+ * @property ratingUrl The link to the ratings page (user reviews) for this [Addon].
+ * @property detailUrl The link to the detail page for this [Addon].
+ * @property incognito Indicates how the extension works with private browsing windows.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class Addon(
+ val id: String,
+ val author: Author? = null,
+ val downloadUrl: String = "",
+ val version: String = "",
+ val permissions: List<String> = emptyList(),
+ val optionalPermissions: List<Permission> = emptyList(),
+ val optionalOrigins: List<Permission> = emptyList(),
+ val translatableName: Map<String, String> = emptyMap(),
+ val translatableDescription: Map<String, String> = emptyMap(),
+ val translatableSummary: Map<String, String> = emptyMap(),
+ val iconUrl: String = "",
+ val homepageUrl: String = "",
+ val rating: Rating? = null,
+ val createdAt: String = "",
+ val updatedAt: String = "",
+ val icon: Bitmap? = null,
+ val installedState: InstalledState? = null,
+ val defaultLocale: String = DEFAULT_LOCALE,
+ val ratingUrl: String = "",
+ val detailUrl: String = "",
+ val incognito: Incognito = Incognito.SPANNING,
+) : Parcelable {
+
+ /**
+ * Returns an icon for this [Addon], either from the [Addon] or [installedState].
+ */
+ fun provideIcon(): Bitmap? {
+ return icon ?: installedState?.icon
+ }
+
+ /**
+ * Represents an add-on author.
+ *
+ * @property name The name of the author.
+ * @property url The link to the profile page of the author.
+ */
+ @SuppressLint("ParcelCreator")
+ @Parcelize
+ data class Author(
+ val name: String,
+ val url: String,
+ ) : Parcelable
+
+ /**
+ * Holds all the rating information of this add-on.
+ *
+ * @property average An average score from 1 to 5 of how users scored this add-on.
+ * @property reviews The number of users that has scored this add-on.
+ */
+ @SuppressLint("ParcelCreator")
+ @Parcelize
+ data class Rating(
+ val average: Float,
+ val reviews: Int,
+ ) : Parcelable
+
+ /**
+ * Required or optional permission.
+ *
+ * @property name The name of this permission.
+ * @property granted Indicate if this permission is granted or not.
+ */
+ @SuppressLint("ParcelCreator")
+ @Parcelize
+ data class Permission(
+ val name: String,
+ val granted: Boolean,
+ ) : Parcelable
+
+ /**
+ * Returns a list of id resources per each item on the [Addon.permissions] list.
+ * Holds the state of the installed web extension of this add-on.
+ *
+ * @property id The ID of the installed web extension.
+ * @property version The installed version.
+ * @property enabled Indicates if this [Addon] is enabled to interact with web content or not,
+ * defaults to false.
+ * @property supported Indicates if this [Addon] is supported by the browser or not, defaults
+ * to true.
+ * @property disabledReason Indicates why the [Addon] was disabled.
+ * @property optionsPageUrl the URL of the page displaying the
+ * options page (options_ui in the extension's manifest).
+ * @property allowedInPrivateBrowsing true if this addon should be allowed to run in private
+ * browsing pages, false otherwise.
+ * @property icon the icon of the installed extension.
+ */
+ @SuppressLint("ParcelCreator")
+ @Parcelize
+ data class InstalledState(
+ val id: String,
+ val version: String,
+ val optionsPageUrl: String?,
+ val openOptionsPageInTab: Boolean = false,
+ val enabled: Boolean = false,
+ val supported: Boolean = true,
+ val disabledReason: DisabledReason? = null,
+ val allowedInPrivateBrowsing: Boolean = false,
+ val icon: Bitmap? = null,
+ ) : Parcelable
+
+ /**
+ * Enum containing all reasons why an [Addon] was disabled.
+ */
+ enum class DisabledReason {
+
+ /**
+ * The [Addon] was disabled because it is unsupported.
+ */
+ UNSUPPORTED,
+
+ /**
+ * The [Addon] was disabled because is it is blocklisted.
+ */
+ BLOCKLISTED,
+
+ /**
+ * The [Addon] was disabled by the user.
+ */
+ USER_REQUESTED,
+
+ /**
+ * The [Addon] was disabled because it isn't correctly signed.
+ */
+ NOT_CORRECTLY_SIGNED,
+
+ /**
+ * The [Addon] was disabled because it isn't compatible with the application version.
+ */
+ INCOMPATIBLE,
+ }
+
+ /**
+ * Incognito values that control how an [Addon] works with private browsing windows.
+ */
+ enum class Incognito {
+ /**
+ * The [Addon] will see events from private and non-private windows and tabs.
+ */
+ SPANNING,
+
+ /**
+ * The [Addon] will be split between private and non-private windows.
+ */
+ SPLIT,
+
+ /**
+ * Private tabs and windows are invisible to the [Addon].
+ */
+ NOT_ALLOWED,
+ }
+
+ /**
+ * Returns a list of localized Strings per each item on the [permissions] list.
+ * @param context A context reference.
+ */
+ fun translatePermissions(context: Context): List<String> {
+ return localizePermissions(permissions, context)
+ }
+
+ /**
+ * Returns whether or not this [Addon] is currently installed.
+ */
+ fun isInstalled() = installedState != null
+
+ /**
+ * Returns whether or not this [Addon] is currently enabled.
+ */
+ fun isEnabled() = installedState?.enabled == true
+
+ /**
+ * Returns whether or not this [Addon] is currently supported by the browser.
+ */
+ fun isSupported() = installedState?.supported == true
+
+ /**
+ * Returns whether or not this [Addon] is currently disabled because it is not
+ * supported. This is based on the installed extension state in the engine. An
+ * addon can be disabled as unsupported and later become supported, so
+ * both [isSupported] and [isDisabledAsUnsupported] can be true.
+ */
+ fun isDisabledAsUnsupported() = installedState?.disabledReason == DisabledReason.UNSUPPORTED
+
+ /**
+ * Returns whether or not this [Addon] is currently disabled because it is part of
+ * the blocklist. This is based on the installed extension state in the engine.
+ */
+ fun isDisabledAsBlocklisted() = installedState?.disabledReason == DisabledReason.BLOCKLISTED
+
+ /**
+ * Returns whether this [Addon] is currently disabled because it isn't correctly signed.
+ */
+ fun isDisabledAsNotCorrectlySigned() = installedState?.disabledReason == DisabledReason.NOT_CORRECTLY_SIGNED
+
+ /**
+ * Returns whether this [Addon] is currently disabled because it isn't compatible
+ * with the application version.
+ */
+ fun isDisabledAsIncompatible() = installedState?.disabledReason == DisabledReason.INCOMPATIBLE
+
+ /**
+ * Returns whether or not this [Addon] is allowed in private browsing mode.
+ */
+ fun isAllowedInPrivateBrowsing() = installedState?.allowedInPrivateBrowsing == true
+
+ /**
+ * Returns a copy of this [Addon] containing only translations (description,
+ * name, summary) of the provided locales. All other translations
+ * except the [defaultLocale] will be removed.
+ *
+ * @param locales list of locales to keep.
+ * @return copy of the addon with all other translations removed.
+ */
+ fun filterTranslations(locales: List<String>): Addon {
+ val internalLocales = locales + defaultLocale
+ val descriptions = translatableDescription.filterKeys { internalLocales.contains(it) }
+ val names = translatableName.filterKeys { internalLocales.contains(it) }
+ val summaries = translatableSummary.filterKeys { internalLocales.contains(it) }
+ return copy(translatableName = names, translatableDescription = descriptions, translatableSummary = summaries)
+ }
+
+ companion object {
+ /**
+ * A map of permissions to translation string ids.
+ */
+ @Suppress("MaxLineLength")
+ val permissionToTranslation = mapOf(
+ "privacy" to R.string.mozac_feature_addons_permissions_privacy_description,
+ "<all_urls>" to R.string.mozac_feature_addons_permissions_all_urls_description,
+ "tabs" to R.string.mozac_feature_addons_permissions_tabs_description,
+ "unlimitedStorage" to R.string.mozac_feature_addons_permissions_unlimited_storage_description,
+ "webNavigation" to R.string.mozac_feature_addons_permissions_web_navigation_description,
+ "bookmarks" to R.string.mozac_feature_addons_permissions_bookmarks_description,
+ "browserSettings" to R.string.mozac_feature_addons_permissions_browser_setting_description,
+ "browsingData" to R.string.mozac_feature_addons_permissions_browser_data_description,
+ "clipboardRead" to R.string.mozac_feature_addons_permissions_clipboard_read_description,
+ "clipboardWrite" to R.string.mozac_feature_addons_permissions_clipboard_write_description,
+ "declarativeNetRequest" to R.string.mozac_feature_addons_permissions_declarative_net_request_description,
+ "declarativeNetRequestFeedback" to R.string.mozac_feature_addons_permissions_declarative_net_request_feedback_description,
+ "downloads" to R.string.mozac_feature_addons_permissions_downloads_description,
+ "downloads.open" to R.string.mozac_feature_addons_permissions_downloads_open_description,
+ "find" to R.string.mozac_feature_addons_permissions_find_description,
+ "geolocation" to R.string.mozac_feature_addons_permissions_geolocation_description,
+ "history" to R.string.mozac_feature_addons_permissions_history_description,
+ "management" to R.string.mozac_feature_addons_permissions_management_description,
+ "nativeMessaging" to R.string.mozac_feature_addons_permissions_native_messaging_description,
+ "notifications" to R.string.mozac_feature_addons_permissions_notifications_description,
+ "pkcs11" to R.string.mozac_feature_addons_permissions_pkcs11_description,
+ "proxy" to R.string.mozac_feature_addons_permissions_proxy_description,
+ "sessions" to R.string.mozac_feature_addons_permissions_sessions_description,
+ "tabHide" to R.string.mozac_feature_addons_permissions_tab_hide_description,
+ "topSites" to R.string.mozac_feature_addons_permissions_top_sites_description,
+ "devtools" to R.string.mozac_feature_addons_permissions_devtools_description,
+ )
+
+ /**
+ * Takes a list of [permissions] and returns a list of id resources per each item.
+ * @param permissions The list of permissions to be localized. Valid permissions can be found in
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#API_permissions
+ */
+ fun localizePermissions(permissions: List<String>, context: Context): List<String> {
+ var localizedUrlAccessPermissions = emptyList<String>()
+ val requireAllUrlsAccess = permissions.contains("<all_urls>")
+ val notFoundPermissions = mutableListOf<String>()
+
+ val localizedNormalPermissions = permissions.mapNotNull {
+ val id = permissionToTranslation[it]
+ if (id == null) notFoundPermissions.add(it)
+ id
+ }.map { context.getString(it) }
+
+ if (!requireAllUrlsAccess && notFoundPermissions.isNotEmpty()) {
+ localizedUrlAccessPermissions = localizedURLAccessPermissions(context, notFoundPermissions)
+ }
+
+ return localizedNormalPermissions + localizedUrlAccessPermissions
+ }
+
+ /**
+ * Creates an [Addon] object from a [WebExtension] one. The resulting object might have an installed state when
+ * the second method's argument is used.
+ *
+ * @param extension a WebExtension instance.
+ * @param installedState optional - an installed state.
+ */
+ fun newFromWebExtension(extension: WebExtension, installedState: InstalledState? = null): Addon {
+ val metadata = extension.getMetadata()
+ val name = metadata?.name ?: extension.id
+ val description = metadata?.description ?: extension.id
+ val permissions = metadata?.permissions.orEmpty() +
+ metadata?.hostPermissions.orEmpty()
+ val averageRating = metadata?.averageRating ?: 0f
+ val reviewCount = metadata?.reviewCount ?: 0
+ val homepageUrl = metadata?.homepageUrl.orEmpty()
+ val ratingUrl = metadata?.reviewUrl.orEmpty()
+ val developerName = metadata?.developerName.orEmpty()
+ val author = if (developerName.isNotBlank()) {
+ Author(name = developerName, url = metadata?.developerUrl.orEmpty())
+ } else {
+ null
+ }
+ val detailUrl = metadata?.detailUrl.orEmpty()
+ val incognito = when (metadata?.incognito) {
+ GeckoIncognito.NOT_ALLOWED -> Incognito.NOT_ALLOWED
+ GeckoIncognito.SPLIT -> Incognito.SPLIT
+ else -> Incognito.SPANNING
+ }
+
+ val grantedOptionalPermissions = metadata?.grantedOptionalPermissions ?: emptyList()
+ val grantedOptionalOrigins = metadata?.grantedOptionalOrigins ?: emptyList()
+ val optionalPermissions = metadata?.optionalPermissions?.map { permission ->
+ Permission(
+ name = permission,
+ granted = grantedOptionalPermissions.contains(permission),
+ )
+ } ?: emptyList()
+
+ val optionalOrigins = metadata?.optionalOrigins?.map { origin ->
+ Permission(
+ name = origin,
+ granted = grantedOptionalOrigins.contains(origin),
+ )
+ } ?: emptyList()
+
+ return Addon(
+ id = extension.id,
+ author = author,
+ version = metadata?.version.orEmpty(),
+ permissions = permissions,
+ optionalPermissions = optionalPermissions,
+ optionalOrigins = optionalOrigins,
+ downloadUrl = metadata?.downloadUrl.orEmpty(),
+ rating = Rating(averageRating, reviewCount),
+ homepageUrl = homepageUrl,
+ translatableName = mapOf(DEFAULT_LOCALE to name),
+ translatableDescription = mapOf(DEFAULT_LOCALE to metadata?.fullDescription.orEmpty()),
+ // We don't have a summary when we create an add-on from a WebExtension instance so let's
+ // re-use description...
+ translatableSummary = mapOf(DEFAULT_LOCALE to description),
+ updatedAt = fromMetadataToAddonDate(metadata?.updateDate.orEmpty()),
+ ratingUrl = ratingUrl,
+ detailUrl = detailUrl,
+ incognito = incognito,
+ installedState = installedState,
+ )
+ }
+
+ /**
+ * Returns a new [String] formatted in "yyyy-MM-dd'T'HH:mm:ss'Z'".
+ * [Metadata] uses "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" which is in simplified 8601 format
+ * while [Addon] uses "yyyy-MM-dd'T'HH:mm:ss'Z'"
+ *
+ * @param inputDate The string data to be formatted.
+ */
+ @VisibleForTesting
+ internal fun fromMetadataToAddonDate(inputDate: String): String {
+ val updatedAt: String = try {
+ val zone = TimeZone.getTimeZone("GMT")
+ val metadataFormat =
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply {
+ timeZone = zone
+ }
+ val addonFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
+ timeZone = zone
+ }
+ val formattedDate = metadataFormat.parse(inputDate)
+
+ if (formattedDate !== null) {
+ addonFormat.format(formattedDate)
+ } else {
+ ""
+ }
+ } catch (e: ParseException) {
+ logger.error("Unable to format $inputDate", e)
+ ""
+ }
+ return updatedAt
+ }
+
+ @Suppress("MaxLineLength")
+ internal fun localizedURLAccessPermissions(context: Context, accessPermissions: List<String>): List<String> {
+ val localizedSiteAccessPermissions = mutableListOf<String>()
+ val permissionsToTranslations = mutableMapOf<String, Int>()
+
+ accessPermissions.forEach { permission ->
+ val id = localizeURLAccessPermission(permission)
+ if (id != null) {
+ permissionsToTranslations[permission] = id
+ }
+ }
+
+ if (permissionsToTranslations.values.any { it.isAllURLsPermission() }) {
+ localizedSiteAccessPermissions.add(context.getString(R.string.mozac_feature_addons_permissions_all_urls_description))
+ } else {
+ formatURLAccessPermission(permissionsToTranslations, localizedSiteAccessPermissions, context)
+ }
+ return localizedSiteAccessPermissions
+ }
+
+ @Suppress("MagicNumber", "ComplexMethod")
+ private fun formatURLAccessPermission(
+ permissionsToTranslations: MutableMap<String, Int>,
+ localizedSiteAccessPermissions: MutableList<String>,
+ context: Context,
+ ) {
+ val maxShownPermissionsEntries = 4
+ fun addExtraEntriesIfNeeded(count: Int, oneExtraPermission: Int, multiplePermissions: Int) {
+ val collapsedPermissions = count - maxShownPermissionsEntries
+ if (collapsedPermissions == 1) {
+ localizedSiteAccessPermissions.add(context.getString(oneExtraPermission))
+ } else {
+ localizedSiteAccessPermissions.add(context.getString(multiplePermissions, collapsedPermissions))
+ }
+ }
+
+ var domainCount = 0
+ var siteCount = 0
+
+ loop@ for ((permission, stringId) in permissionsToTranslations) {
+ var host = permission.toUri().host ?: ""
+ when {
+ stringId.isDomainAccessPermission() -> {
+ ++domainCount
+ host = host.removePrefix("*.")
+
+ if (domainCount > maxShownPermissionsEntries) continue@loop
+ }
+
+ stringId.isSiteAccessPermission() -> {
+ ++siteCount
+ if (siteCount > maxShownPermissionsEntries) continue@loop
+ }
+ }
+ localizedSiteAccessPermissions.add(context.getString(stringId, host))
+ }
+
+ // If we have [maxPermissionsEntries] or fewer permissions, display them all, otherwise we
+ // display the first [maxPermissionsEntries] followed by an item that says "...plus N others"
+ if (domainCount > maxShownPermissionsEntries) {
+ val onePermission = R.string.mozac_feature_addons_permissions_one_extra_domain_description
+ val multiplePermissions = R.string.mozac_feature_addons_permissions_extra_domains_description_plural
+ addExtraEntriesIfNeeded(domainCount, onePermission, multiplePermissions)
+ }
+ if (siteCount > maxShownPermissionsEntries) {
+ val onePermission = R.string.mozac_feature_addons_permissions_one_extra_site_description
+ val multiplePermissions = R.string.mozac_feature_addons_permissions_extra_sites_description
+ addExtraEntriesIfNeeded(siteCount, onePermission, multiplePermissions)
+ }
+ }
+
+ private fun Int.isSiteAccessPermission(): Boolean {
+ return this == R.string.mozac_feature_addons_permissions_one_site_description
+ }
+
+ private fun Int.isDomainAccessPermission(): Boolean {
+ return this == R.string.mozac_feature_addons_permissions_sites_in_domain_description
+ }
+
+ private fun Int.isAllURLsPermission(): Boolean {
+ return this == R.string.mozac_feature_addons_permissions_all_urls_description
+ }
+
+ internal fun localizeURLAccessPermission(urlAccess: String): Int? {
+ val uri = urlAccess.toUri()
+ val host = (uri.host ?: "").trim()
+ val path = (uri.path ?: "").trim()
+
+ return when {
+ host == "*" || urlAccess == "<all_urls>" -> {
+ R.string.mozac_feature_addons_permissions_all_urls_description
+ }
+ host.isEmpty() || path.isEmpty() -> null
+ host.startsWith(prefix = "*.") -> R.string.mozac_feature_addons_permissions_sites_in_domain_description
+ else -> R.string.mozac_feature_addons_permissions_one_site_description
+ }
+ }
+
+ /**
+ * The default fallback locale in case the [Addon] does not have its own [Addon.defaultLocale].
+ */
+ const val DEFAULT_LOCALE = "en-us"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/AddonManager.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/AddonManager.kt
new file mode 100644
index 0000000000..d3e12a0171
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/AddonManager.kt
@@ -0,0 +1,528 @@
+/* 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.addons
+
+import android.graphics.Bitmap
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.CancellableOperation
+import mozilla.components.concept.engine.webextension.DisabledFlags
+import mozilla.components.concept.engine.webextension.EnableSource
+import mozilla.components.concept.engine.webextension.InstallationMethod
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionRuntime
+import mozilla.components.concept.engine.webextension.isBlockListed
+import mozilla.components.concept.engine.webextension.isDisabledIncompatible
+import mozilla.components.concept.engine.webextension.isDisabledUnsigned
+import mozilla.components.concept.engine.webextension.isUnsupported
+import mozilla.components.feature.addons.update.AddonUpdater
+import mozilla.components.feature.addons.update.AddonUpdater.Status
+import mozilla.components.support.webextensions.WebExtensionSupport
+import mozilla.components.support.webextensions.WebExtensionSupport.installedExtensions
+import java.util.Collections.newSetFromMap
+import java.util.Locale
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Provides access to installed and recommended [Addon]s and manages their states.
+ *
+ * @property store The application's [BrowserStore].
+ * @property runtime The application's [WebExtensionRuntime] to install and configure extensions.
+ * @property addonsProvider The [AddonsProvider] to query available [Addon]s.
+ * @property addonUpdater The [AddonUpdater] instance to use when checking / triggering
+ * updates.
+ */
+@Suppress("LargeClass")
+class AddonManager(
+ private val store: BrowserStore,
+ private val runtime: WebExtensionRuntime,
+ private val addonsProvider: AddonsProvider,
+ private val addonUpdater: AddonUpdater,
+) {
+
+ @VisibleForTesting
+ internal val pendingAddonActions = newSetFromMap(ConcurrentHashMap<CompletableDeferred<Unit>, Boolean>())
+
+ @VisibleForTesting
+ internal val iconLoadingScope = CoroutineScope(Dispatchers.IO)
+
+ // Acts as an in-memory cache for the fetched addon's icons.
+ @VisibleForTesting
+ internal val iconsCache = ConcurrentHashMap<String, Bitmap>()
+
+ /**
+ * Returns the list of all installed and featured add-ons.
+ *
+ * @param waitForPendingActions whether or not to wait (suspend, but not
+ * block) until all pending add-on actions (install/uninstall/enable/disable)
+ * are completed in either success or failure.
+ * @param allowCache whether or not the result may be provided
+ * from a previously cached response, defaults to true.
+ * @return list of all [Addon]s with up-to-date [Addon.installedState].
+ * @throws AddonManagerException in case of a problem reading from
+ * the [addonsProvider] or querying web extension state from the engine / store.
+ */
+ @Throws(AddonManagerException::class)
+ @Suppress("TooGenericExceptionCaught")
+ suspend fun getAddons(waitForPendingActions: Boolean = true, allowCache: Boolean = true): List<Addon> {
+ try {
+ // Make sure extension support is initialized, i.e. the state of all installed extensions is known.
+ WebExtensionSupport.awaitInitialization()
+
+ // Make sure all pending actions are completed.
+ if (waitForPendingActions) {
+ pendingAddonActions.awaitAll()
+ }
+
+ // Get all the featured add-ons not installed from provider.
+ // NB: We're keeping translations only for the default locale.
+ var featuredAddons = emptyList<Addon>()
+ try {
+ val userLanguage = Locale.getDefault().language
+ val locales = listOf(userLanguage)
+ featuredAddons =
+ addonsProvider.getFeaturedAddons(allowCache, language = userLanguage)
+ .filter { addon -> !installedExtensions.containsKey(addon.id) }
+ .map { addon -> addon.filterTranslations(locales) }
+ } catch (throwable: Throwable) {
+ // Do not throw when we fail to fetch the featured add-ons since there can be installed add-ons.
+ logger.warn("Failed to get the featured add-ons", throwable)
+ }
+
+ // Build a list of installed extensions that are not built-in extensions.
+ val installedAddons = installedExtensions
+ .filterValues { !it.isBuiltIn() }
+ .map {
+ val extension = it.value
+ val installedState = toInstalledState(extension)
+ Addon.newFromWebExtension(extension, installedState)
+ }
+
+ return featuredAddons + installedAddons
+ } catch (throwable: Throwable) {
+ throw AddonManagerException(throwable)
+ }
+ }
+
+ /**
+ * Installs an [Addon] from the provided [url].
+ *
+ * @param url the url pointing to either a resources path for locating the extension
+ * within the APK file (e.g. resource://android/assets/extensions/my_web_ext) or to a
+ * local (e.g. resource://android/assets/extensions/my_web_ext.xpi) or remote
+ * (e.g. https://addons.mozilla.org/firefox/downloads/file/123/some_web_ext.xpi) XPI file.
+ * @param installationMethod (optional) the method used to install a [Addon].
+ * @param onSuccess (optional) callback invoked if the addon was installed successfully,
+ * providing access to the [Addon] object.
+ * @param onError (optional) callback invoked if there was an error installing the addon.
+ */
+ fun installAddon(
+ url: String,
+ installationMethod: InstallationMethod? = null,
+ onSuccess: ((Addon) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { _ -> },
+ ): CancellableOperation {
+ val pendingAction = addPendingAddonAction()
+ return runtime.installWebExtension(
+ url = url,
+ installationMethod = installationMethod,
+ onSuccess = { ext ->
+ onAddonInstalled(ext, pendingAction, onSuccess)
+ },
+ onError = { throwable ->
+ completePendingAddonAction(pendingAction)
+ onError(throwable)
+ },
+ )
+ }
+
+ /**
+ * Uninstalls the provided [Addon].
+ *
+ * @param addon the addon to uninstall.
+ * @param onSuccess (optional) callback invoked if the addon was uninstalled successfully.
+ * @param onError (optional) callback invoked if there was an error uninstalling the addon.
+ */
+ fun uninstallAddon(
+ addon: Addon,
+ onSuccess: (() -> Unit) = { },
+ onError: ((String, Throwable) -> Unit) = { _, _ -> },
+ ) {
+ val extension = addon.installedState?.let { installedExtensions[it.id] }
+ if (extension == null) {
+ onError(addon.id, IllegalStateException("Addon is not installed"))
+ return
+ }
+
+ val pendingAction = addPendingAddonAction()
+ runtime.uninstallWebExtension(
+ extension,
+ onSuccess = {
+ addonUpdater.unregisterForFutureUpdates(addon.id)
+ completePendingAddonAction(pendingAction)
+ onSuccess()
+ },
+ onError = { id, throwable ->
+ completePendingAddonAction(pendingAction)
+ onError(id, throwable)
+ },
+ )
+ }
+
+ /**
+ * Enables the provided [Addon].
+ *
+ * @param addon the [Addon] to enable.
+ * @param source [EnableSource] to indicate why the extension is enabled, default to EnableSource.USER.
+ * @param onSuccess (optional) callback invoked with the enabled [Addon].
+ * @param onError (optional) callback invoked if there was an error enabling
+ */
+ fun enableAddon(
+ addon: Addon,
+ source: EnableSource = EnableSource.USER,
+ onSuccess: ((Addon) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ) {
+ val extension = addon.installedState?.let { installedExtensions[it.id] }
+ if (extension == null) {
+ onError(IllegalStateException("Addon is not installed"))
+ return
+ }
+
+ val pendingAction = addPendingAddonAction()
+ runtime.enableWebExtension(
+ extension,
+ source = source,
+ onSuccess = { ext ->
+ val enabledAddon = addon.copy(installedState = toInstalledState(ext))
+ completePendingAddonAction(pendingAction)
+ onSuccess(enabledAddon)
+ },
+ onError = {
+ completePendingAddonAction(pendingAction)
+ onError(it)
+ },
+ )
+ }
+
+ /**
+ * Add the provided [permissions] and [origins] to [Addon].
+ *
+ * @param addon the [Addon] to add the provided [permissions] and [origins].
+ * @param permissions the permissions to added, pass an empty array for opt-out.
+ * @param origins the origins to added, pass an empty array for opt-out.
+ * @param onSuccess (optional) callback invoked with the added permissions and origins.
+ * @param onError (optional) callback invoked if there was an error adding.
+ */
+ fun addOptionalPermission(
+ addon: Addon,
+ permissions: List<String> = emptyList(),
+ origins: List<String> = emptyList(),
+ onSuccess: ((Addon) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ) {
+ if (permissions.isEmpty() && origins.isEmpty()) {
+ onError(IllegalStateException("Either permissions or origins must not be empty"))
+ return
+ }
+ val extension = addon.installedState?.let { installedExtensions[it.id] }
+ if (extension == null) {
+ onError(IllegalStateException("Addon is not installed"))
+ return
+ }
+
+ val pendingAction = addPendingAddonAction()
+ runtime.addOptionalPermissions(
+ extension.id,
+ permissions = permissions,
+ origins = origins,
+ onSuccess = { ext ->
+ val enabledAddon = addon.copy(installedState = toInstalledState(ext))
+ completePendingAddonAction(pendingAction)
+ onSuccess(enabledAddon)
+ },
+ onError = {
+ completePendingAddonAction(pendingAction)
+ onError(it)
+ },
+ )
+ }
+
+ /**
+ * Remove the provided [permissions] and [origins] from [Addon].
+ *
+ * @param addon the [Addon] to remove the provided [permissions] and [origins].
+ * @param permissions the permissions to be removed, pass an empty array for opt-out.
+ * @param origins the origins to be removed, pass an empty array for opt-out.
+ * @param onSuccess (optional) callback invoked with the removed permissions and origins.
+ * @param onError (optional) callback invoked if there was an error removed.
+ */
+ fun removeOptionalPermission(
+ addon: Addon,
+ permissions: List<String> = emptyList(),
+ origins: List<String> = emptyList(),
+ onSuccess: ((Addon) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ) {
+ if (permissions.isEmpty() && origins.isEmpty()) {
+ onError(IllegalStateException("Either permissions or origins must not be empty"))
+ return
+ }
+
+ val extension = addon.installedState?.let { installedExtensions[it.id] }
+ if (extension == null) {
+ onError(IllegalStateException("Addon is not installed"))
+ return
+ }
+
+ val pendingAction = addPendingAddonAction()
+ runtime.removeOptionalPermissions(
+ extension.id,
+ permissions = permissions,
+ origins = origins,
+ onSuccess = { ext ->
+ val enabledAddon = addon.copy(installedState = toInstalledState(ext))
+ completePendingAddonAction(pendingAction)
+ onSuccess(enabledAddon)
+ },
+ onError = {
+ completePendingAddonAction(pendingAction)
+ onError(it)
+ },
+ )
+ }
+
+ /**
+ * Disables the provided [Addon].
+ *
+ * @param addon the [Addon] to disable.
+ * @param source [EnableSource] to indicate why the addon is disabled, default to EnableSource.USER.
+ * @param onSuccess (optional) callback invoked with the enabled [Addon].
+ * @param onError (optional) callback invoked if there was an error enabling
+ */
+ fun disableAddon(
+ addon: Addon,
+ source: EnableSource = EnableSource.USER,
+ onSuccess: ((Addon) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ) {
+ val extension = addon.installedState?.let { installedExtensions[it.id] }
+ if (extension == null) {
+ onError(IllegalStateException("Addon is not installed"))
+ return
+ }
+
+ val pendingAction = addPendingAddonAction()
+ runtime.disableWebExtension(
+ extension,
+ source,
+ onSuccess = { ext ->
+ val disabledAddon = addon.copy(installedState = toInstalledState(ext))
+ completePendingAddonAction(pendingAction)
+ onSuccess(disabledAddon)
+ },
+ onError = {
+ completePendingAddonAction(pendingAction)
+ onError(it)
+ },
+ )
+ }
+
+ /**
+ * Sets whether to allow/disallow the provided [Addon] in private browsing mode.
+ *
+ * @param addon the [Addon] to allow/disallow.
+ * @param allowed true if allow, false otherwise.
+ * @param onSuccess (optional) callback invoked with the enabled [Addon].
+ * @param onError (optional) callback invoked if there was an error enabling
+ */
+ fun setAddonAllowedInPrivateBrowsing(
+ addon: Addon,
+ allowed: Boolean,
+ onSuccess: ((Addon) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { },
+ ) {
+ val extension = addon.installedState?.let { installedExtensions[it.id] }
+ if (extension == null) {
+ onError(IllegalStateException("Addon is not installed"))
+ return
+ }
+
+ val pendingAction = addPendingAddonAction()
+ runtime.setAllowedInPrivateBrowsing(
+ extension,
+ allowed,
+ onSuccess = { ext ->
+ val modifiedAddon = addon.copy(installedState = toInstalledState(ext))
+ completePendingAddonAction(pendingAction)
+ onSuccess(modifiedAddon)
+ },
+ onError = {
+ completePendingAddonAction(pendingAction)
+ onError(it)
+ },
+ )
+ }
+
+ /**
+ * Updates the addon with the provided [id] if an update is available.
+ *
+ * @param id the ID of the addon
+ * @param onFinish callback invoked with the [Status] of the update once complete.
+ */
+ fun updateAddon(id: String, onFinish: ((Status) -> Unit)) {
+ val extension = installedExtensions[id]
+
+ if (extension == null || extension.isUnsupported()) {
+ onFinish(Status.NotInstalled)
+ return
+ }
+
+ val onSuccess: ((WebExtension?) -> Unit) = { updatedExtension ->
+ val status = if (updatedExtension == null) {
+ Status.NoUpdateAvailable
+ } else {
+ WebExtensionSupport.markExtensionAsUpdated(store, updatedExtension)
+ Status.SuccessfullyUpdated
+ }
+ onFinish(status)
+ }
+
+ val onError: ((String, Throwable) -> Unit) = { message, exception ->
+ onFinish(Status.Error(message, exception))
+ }
+
+ runtime.updateWebExtension(extension, onSuccess, onError)
+ }
+
+ private fun addPendingAddonAction() = CompletableDeferred<Unit>().also {
+ pendingAddonActions.add(it)
+ }
+
+ private fun completePendingAddonAction(action: CompletableDeferred<Unit>) {
+ action.complete(Unit)
+ pendingAddonActions.remove(action)
+ }
+
+ /**
+ * Converts a [WebExtension] to [Addon.InstalledState].
+ */
+ fun toInstalledState(extension: WebExtension): Addon.InstalledState {
+ val metadata = extension.getMetadata()
+ val cachedIcon = iconsCache[extension.id]
+ return Addon.InstalledState(
+ id = extension.id,
+ version = metadata?.version ?: "",
+ optionsPageUrl = metadata?.optionsPageUrl,
+ openOptionsPageInTab = metadata?.openOptionsPageInTab ?: false,
+ enabled = extension.isEnabled(),
+ disabledReason = extension.getDisabledReason(),
+ allowedInPrivateBrowsing = extension.isAllowedInPrivateBrowsing(),
+ icon = cachedIcon ?: loadIcon(extension)?.also {
+ iconsCache[extension.id] = it
+ },
+ )
+ }
+
+ @VisibleForTesting
+ @Suppress("TooGenericExceptionCaught")
+ internal fun loadIcon(extension: WebExtension): Bitmap? {
+ // As we are loading the icon from the xpi file this operation should be quick.
+ // If the operation takes a long time, we proceed to return early,
+ // and load the icon in background.
+ return runBlocking(getIconDispatcher()) {
+ try {
+ val icon = withTimeoutOrNull(ADDON_ICON_RETRIEVE_TIMEOUT) {
+ extension.loadIcon(ADDON_ICON_SIZE)
+ }
+ logger.info("Icon for extension ${extension.id} loaded successfully")
+ icon
+ } catch (e: TimeoutCancellationException) {
+ // If the icon is too big, we delegate the task to be done in background.
+ // Eventually, we load the icon and keep it in cache for sequential loads.
+ tryLoadIconInBackground(extension)
+ logger.error("Failed load icon for extension ${extension.id}", e)
+ null
+ } catch (e: Exception) {
+ null
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun tryLoadIconInBackground(extension: WebExtension) {
+ logger.info("Trying to load icon for extension ${extension.id} in background")
+ iconLoadingScope.launch {
+ withContext(getIconDispatcher()) {
+ extension.loadIcon(ADDON_ICON_SIZE)
+ }?.also {
+ logger.info("Icon for extension ${extension.id} loaded in background successfully")
+ iconsCache[extension.id] = it
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getIconDispatcher(): CoroutineDispatcher {
+ val iconThread = HandlerThread("IconThread").also {
+ it.start()
+ }
+ return Handler(iconThread.looper).asCoroutineDispatcher("WebExtensionIconDispatcher")
+ }
+
+ private fun onAddonInstalled(
+ ext: WebExtension,
+ pendingAction: CompletableDeferred<Unit>,
+ onSuccess: ((Addon) -> Unit),
+ ) {
+ val installedState = toInstalledState(ext)
+ val installedAddon = Addon.newFromWebExtension(ext, installedState)
+
+ addonUpdater.registerForFutureUpdates(installedAddon.id)
+ completePendingAddonAction(pendingAction)
+ onSuccess(installedAddon)
+ }
+
+ companion object {
+ // Size of the icon to load for extensions
+ const val ADDON_ICON_SIZE = 48
+
+ private const val ADDON_ICON_RETRIEVE_TIMEOUT = 1000L
+ }
+}
+
+/**
+ * Wraps exceptions thrown by either the initialization process or an [AddonsProvider].
+ */
+class AddonManagerException(throwable: Throwable) : Exception(throwable)
+
+internal fun WebExtension.getDisabledReason(): Addon.DisabledReason? {
+ return if (isBlockListed()) {
+ Addon.DisabledReason.BLOCKLISTED
+ } else if (isDisabledUnsigned()) {
+ Addon.DisabledReason.NOT_CORRECTLY_SIGNED
+ } else if (isDisabledIncompatible()) {
+ Addon.DisabledReason.INCOMPATIBLE
+ } else if (isUnsupported()) {
+ Addon.DisabledReason.UNSUPPORTED
+ } else if (getMetadata()?.disabledFlags?.contains(DisabledFlags.USER) == true) {
+ Addon.DisabledReason.USER_REQUESTED
+ } else {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/AddonsProvider.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/AddonsProvider.kt
new file mode 100644
index 0000000000..6f23153060
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/AddonsProvider.kt
@@ -0,0 +1,28 @@
+/* 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.addons
+
+/**
+ * A contract that indicate how an add-on provider must behave.
+ */
+interface AddonsProvider {
+
+ /**
+ * Provides a list of all featured add-ons, which are add-ons we list in the add-ons manager UI
+ * by default (e.g. the add-ons that are available for the app or a set of curated add-ons).
+ *
+ * @param allowCache whether or not the result may be provided from a previously cached response,
+ * defaults to true.
+ * @param readTimeoutInSeconds optional timeout in seconds to use when fetching featured add-ons.
+ * @param language indicates in which language the translatable fields should be in, if no
+ * matching language is found then a fallback translation is returned using the default language.
+ * When it is null all translations available will be returned.
+ */
+ suspend fun getFeaturedAddons(
+ allowCache: Boolean = true,
+ readTimeoutInSeconds: Long? = null,
+ language: String? = null,
+ ): List<Addon>
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/amo/AMOAddonsProvider.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/amo/AMOAddonsProvider.kt
new file mode 100644
index 0000000000..e5a49d7b8f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/amo/AMOAddonsProvider.kt
@@ -0,0 +1,489 @@
+/* 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.addons.amo
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.util.AtomicFile
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonsProvider
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.sanitizeFileName
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import mozilla.components.support.ktx.util.readAndDeserialize
+import mozilla.components.support.ktx.util.writeString
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.File
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.TimeUnit
+
+internal const val API_VERSION = "api/v4"
+internal const val DEFAULT_SERVER_URL = "https://services.addons.mozilla.org"
+internal const val DEFAULT_COLLECTION_USER = "mozilla"
+internal const val DEFAULT_COLLECTION_NAME = "7e8d6dc651b54ab385fb8791bf9dac"
+internal const val COLLECTION_FILE_NAME_PREFIX = "mozilla_components_addon_collection"
+internal const val COLLECTION_FILE_NAME = "${COLLECTION_FILE_NAME_PREFIX}_%s.json"
+internal const val COLLECTION_FILE_NAME_WITH_LANGUAGE = "${COLLECTION_FILE_NAME_PREFIX}_%s_%s.json"
+internal const val REGEX_FILE_NAMES = "$COLLECTION_FILE_NAME_PREFIX(_\\w+)?_%s.json"
+internal const val MINUTE_IN_MS = 60 * 1000
+internal const val DEFAULT_READ_TIMEOUT_IN_SECONDS = 20L
+internal const val PAGE_SIZE = 50
+
+/**
+ * Implement an add-ons provider that uses the AMO API.
+ *
+ * @property context A reference to the application context.
+ * @property client A [Client] for interacting with the AMO HTTP api.
+ * @property serverURL The url of the endpoint to interact with e.g production, staging
+ * or testing. Defaults to [DEFAULT_SERVER_URL].
+ * @property collectionUser The id or name of the user owning the collection specified in
+ * [collectionName], defaults to [DEFAULT_COLLECTION_USER]. This is used to retrieve the
+ * featured add-ons.
+ * @property collectionName The name of the collection to access, defaults to
+ * [DEFAULT_COLLECTION_NAME]. This is used to retrieve the featured add-ons.
+ * @property maxCacheAgeInMinutes maximum time (in minutes) the cached featured add-ons
+ * should remain valid before a refresh is attempted. Defaults to -1, meaning no cache
+ * is being used by default
+ */
+class AMOAddonsProvider(
+ private val context: Context,
+ private val client: Client,
+ private val serverURL: String = DEFAULT_SERVER_URL,
+ private val collectionUser: String = DEFAULT_COLLECTION_USER,
+ private val collectionName: String = DEFAULT_COLLECTION_NAME,
+ private val sortOption: SortOption = SortOption.POPULARITY_DESC,
+ private val maxCacheAgeInMinutes: Long = -1,
+) : AddonsProvider {
+
+ private val logger = Logger("AMOAddonsProvider")
+
+ private val diskCacheLock = Any()
+
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ // Acts as an in-memory cache for the fetched addon's icons.
+ @VisibleForTesting
+ internal val iconsCache = ConcurrentHashMap<String, Bitmap>()
+
+ /**
+ * Interacts with the collections endpoint to provide a list of available
+ * add-ons. May return a cached response, if [allowCache] is true, and the
+ * cache is not expired (see [maxCacheAgeInMinutes]) or fetching from AMO
+ * failed.
+ *
+ * See: https://addons-server.readthedocs.io/en/latest/topics/api/collections.html
+ *
+ * @param allowCache whether or not the result may be provided
+ * from a previously cached response, defaults to true. Note that
+ * [maxCacheAgeInMinutes] must be set for the cache to be active.
+ * @param readTimeoutInSeconds optional timeout in seconds to use when fetching
+ * available add-ons from a remote endpoint. If not specified [DEFAULT_READ_TIMEOUT_IN_SECONDS]
+ * will be used.
+ * @param language indicates in which language the translatable fields should be in, if no
+ * matching language is found then a fallback translation is returned using the default language.
+ * When it is null all translations available will be returned.
+ * @throws IOException if the request failed, or could not be executed due to cancellation,
+ * a connectivity problem or a timeout.
+ */
+ @Throws(IOException::class)
+ @Suppress("NestedBlockDepth")
+ override suspend fun getFeaturedAddons(
+ allowCache: Boolean,
+ readTimeoutInSeconds: Long?,
+ language: String?,
+ ): List<Addon> {
+ // We want to make sure we always use useFallbackFile = false here, as it warranties
+ // that we are trying to fetch the latest localized add-ons when the user changes
+ // language from the previous one.
+ val cachedFeaturedAddons = if (allowCache && !cacheExpired(context, language, useFallbackFile = false)) {
+ readFromDiskCache(language, useFallbackFile = false)?.loadIcons()
+ } else {
+ null
+ }
+
+ if (cachedFeaturedAddons != null) {
+ return cachedFeaturedAddons
+ }
+
+ return try {
+ fetchFeaturedAddons(readTimeoutInSeconds, language)
+ } catch (e: IOException) {
+ logger.error("Failed to fetch available add-ons", e)
+ if (allowCache) {
+ val cacheLastUpdated = getCacheLastUpdated(context, language, useFallbackFile = true)
+ if (cacheLastUpdated > -1) {
+ val cache = readFromDiskCache(language, useFallbackFile = true)
+ cache?.let {
+ logger.info(
+ "Falling back to available add-ons cache from ${
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US).format(cacheLastUpdated)
+ }",
+ )
+ return it
+ }
+ }
+ }
+ throw e
+ }
+ }
+
+ private suspend fun fetchFeaturedAddons(readTimeoutInSeconds: Long?, language: String?): List<Addon> {
+ val langParam = if (!language.isNullOrEmpty()) {
+ "&lang=$language"
+ } else {
+ ""
+ }
+ client.fetch(
+ Request(
+ // NB: The trailing slash after addons is important to prevent a redirect and additional request
+ url = "$serverURL/$API_VERSION/accounts/account/$collectionUser/collections/$collectionName/addons/" +
+ "?page_size=$PAGE_SIZE" +
+ "&sort=${sortOption.value}" +
+ langParam,
+ readTimeout = Pair(readTimeoutInSeconds ?: DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+ .use { response ->
+ if (response.isSuccess) {
+ val responseBody = response.body.string(Charsets.UTF_8)
+ return try {
+ JSONObject(responseBody).getAddonsFromCollection(language)
+ .loadIcons()
+ .also {
+ if (maxCacheAgeInMinutes > 0) {
+ writeToDiskCache(responseBody, language)
+ }
+ deleteUnusedCacheFiles(language)
+ }
+ } catch (e: JSONException) {
+ throw IOException(e)
+ }
+ } else {
+ val errorMessage = "Failed to fetch featured add-ons from collection. " +
+ "Status code: ${response.status}"
+ logger.error(errorMessage)
+ throw IOException(errorMessage)
+ }
+ }
+ }
+
+ /**
+ * Asynchronously loads add-on icon for the given [iconUrl] and stores in the cache.
+ */
+ @VisibleForTesting
+ internal fun loadIconAsync(addonId: String, iconUrl: String): Deferred<Bitmap?> = scope.async {
+ val cachedIcon = iconsCache[addonId]
+ if (cachedIcon != null) {
+ logger.info("Icon for $addonId was found in the cache")
+ cachedIcon
+ } else if (iconUrl.isBlank()) {
+ logger.info("Unable to find the icon for $addonId blank iconUrl")
+ null
+ } else {
+ try {
+ logger.info("Trying to fetch the icon for $addonId from the network")
+ client.fetch(Request(url = iconUrl.sanitizeURL(), useCaches = true, conservative = true))
+ .use { response ->
+ if (response.isSuccess) {
+ response.body.useStream {
+ val icon = BitmapFactory.decodeStream(it)
+ logger.info("Icon for $addonId fetched from the network")
+ iconsCache[addonId] = icon
+ icon
+ }
+ } else {
+ // There was an network error and we couldn't fetch the icon.
+ logger.info("Unable to fetch the icon for $addonId HTTP code ${response.status}")
+ null
+ }
+ }
+ } catch (e: IOException) {
+ logger.error("Attempt to fetch the $addonId icon failed", e)
+ null
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal suspend fun List<Addon>.loadIcons(): List<Addon> {
+ this.map {
+ // Instead of loading icons one by one, let's load them async
+ // so we can do multiple request at the time.
+ loadIconAsync(it.id, it.iconUrl)
+ }.awaitAll() // wait until all parallel icon requests finish.
+
+ return this.map { addon ->
+ addon.copy(icon = iconsCache[addon.id])
+ }
+ }
+
+ @VisibleForTesting
+ internal fun writeToDiskCache(collectionResponse: String, language: String?) {
+ synchronized(diskCacheLock) {
+ getCacheFile(context, language, useFallbackFile = false).writeString { collectionResponse }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun readFromDiskCache(language: String?, useFallbackFile: Boolean): List<Addon>? {
+ synchronized(diskCacheLock) {
+ return getCacheFile(context, language, useFallbackFile).readAndDeserialize {
+ JSONObject(it).getAddonsFromCollection(language)
+ }
+ }
+ }
+
+ /**
+ * Deletes cache files from previous (now unused) collections.
+ */
+ @VisibleForTesting
+ internal fun deleteUnusedCacheFiles(language: String?) {
+ val currentCacheFileName = getBaseCacheFile(context, language, useFallbackFile = true).name
+
+ context.filesDir
+ .listFiles {
+ _, s ->
+ s.startsWith(COLLECTION_FILE_NAME_PREFIX) && s != currentCacheFileName
+ }
+ ?.forEach {
+ logger.debug("Deleting unused collection cache: " + it.name)
+ it.delete()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun cacheExpired(context: Context, language: String?, useFallbackFile: Boolean): Boolean {
+ return getCacheLastUpdated(
+ context,
+ language,
+ useFallbackFile,
+ ) < Date().time - maxCacheAgeInMinutes * MINUTE_IN_MS
+ }
+
+ @VisibleForTesting
+ internal fun getCacheLastUpdated(context: Context, language: String?, useFallbackFile: Boolean): Long {
+ val file = getBaseCacheFile(context, language, useFallbackFile)
+ return if (file.exists()) file.lastModified() else -1
+ }
+
+ private fun getCacheFile(context: Context, language: String?, useFallbackFile: Boolean): AtomicFile {
+ return AtomicFile(getBaseCacheFile(context, language, useFallbackFile))
+ }
+
+ @VisibleForTesting
+ internal fun getBaseCacheFile(context: Context, language: String?, useFallbackFile: Boolean): File {
+ var file = File(context.filesDir, getCacheFileName(language))
+ if (!file.exists() && useFallbackFile) {
+ // In situations, where users change languages and we can't retrieve the new one,
+ // we always want to fallback to the previous localized file.
+ // Try to find first available localized file.
+ val regex = Regex(REGEX_FILE_NAMES.format(getCollectionName()))
+ val fallbackFile = context.filesDir.listFiles()?.find { it.name.matches(regex) }
+
+ if (fallbackFile?.exists() == true) {
+ file = fallbackFile
+ }
+ }
+ return file
+ }
+
+ @VisibleForTesting
+ internal fun getCacheFileName(language: String? = ""): String {
+ val collection = getCollectionName()
+
+ val fileName = if (language.isNullOrEmpty()) {
+ COLLECTION_FILE_NAME.format(collection)
+ } else {
+ COLLECTION_FILE_NAME_WITH_LANGUAGE.format(language, collection)
+ }
+ return fileName.sanitizeFileName()
+ }
+
+ @VisibleForTesting
+ internal fun getCollectionName(): String {
+ val collectionUser = collectionUser.sanitizeFileName()
+ val collectionName = collectionName.sanitizeFileName()
+
+ // Prefix with collection user in case it was customized. We don't want
+ // to do this for the default "mozilla" user so we don't break out of
+ // the existing cache when we're introducing this. Plus mozilla is
+ // already in the file name anyway.
+ return if (collectionUser != DEFAULT_COLLECTION_USER) {
+ "${collectionUser}_$collectionName"
+ } else {
+ collectionName
+ }
+ }
+}
+
+/**
+ * Represents possible sort options for the recommended add-ons from
+ * the configured add-on collection.
+ */
+enum class SortOption(val value: String) {
+ POPULARITY("popularity"),
+ POPULARITY_DESC("-popularity"),
+ NAME("name"),
+ NAME_DESC("-name"),
+ DATE_ADDED("added"),
+ DATE_ADDED_DESC("-added"),
+}
+
+internal fun JSONObject.getAddonsFromCollection(language: String? = null): List<Addon> {
+ val addonsJson = getJSONArray("results")
+ // Each result in a collection response has an `addon` key and some (optional) notes.
+ return (0 until addonsJson.length()).map { index ->
+ addonsJson.getJSONObject(index).getJSONObject("addon").toAddon(language)
+ }
+}
+
+internal fun JSONObject.toAddon(language: String? = null): Addon {
+ return with(this) {
+ val safeLanguage = language?.lowercase(Locale.getDefault())
+ val summary = getSafeTranslations("summary", safeLanguage)
+ val isLanguageInTranslations = summary.containsKey(safeLanguage)
+ Addon(
+ id = getSafeString("guid"),
+ author = getAuthor(),
+ createdAt = getSafeString("created"),
+ updatedAt = getCurrentVersionCreated(),
+ downloadUrl = getDownloadUrl(),
+ version = getCurrentVersion(),
+ permissions = getPermissions(),
+ translatableName = getSafeTranslations("name", safeLanguage),
+ translatableDescription = getSafeTranslations("description", safeLanguage),
+ translatableSummary = summary,
+ iconUrl = getSafeString("icon_url"),
+ // This isn't the add-on homepage but the URL to the AMO detail page. On AMO, the homepage is
+ // a translatable field but https://github.com/mozilla/addons-server/issues/21310 prevents us
+ // from retrieving the homepage URL of any add-on reliably.
+ homepageUrl = getSafeString("url"),
+ rating = getRating(),
+ ratingUrl = getSafeString("ratings_url"),
+ detailUrl = getSafeString("url"),
+ defaultLocale = (
+ if (!safeLanguage.isNullOrEmpty() && isLanguageInTranslations) {
+ safeLanguage
+ } else {
+ getSafeString("default_locale").ifEmpty { Addon.DEFAULT_LOCALE }
+ }
+ ).lowercase(Locale.ROOT),
+ )
+ }
+}
+
+internal fun JSONObject.getRating(): Addon.Rating? {
+ val jsonRating = optJSONObject("ratings")
+ return if (jsonRating != null) {
+ Addon.Rating(
+ reviews = jsonRating.optInt("text_count"),
+ average = jsonRating.optDouble("average").toFloat(),
+ )
+ } else {
+ null
+ }
+}
+
+internal fun JSONObject.getPermissions(): List<String> {
+ val permissionsJson = getFile()?.getSafeJSONArray("permissions") ?: JSONArray()
+ return (0 until permissionsJson.length()).map { index ->
+ permissionsJson.getString(index)
+ }
+}
+
+internal fun JSONObject.getCurrentVersion(): String {
+ return getJSONObject("current_version").getSafeString("version")
+}
+
+internal fun JSONObject.getFile(): JSONObject? {
+ return getJSONObject("current_version")
+ .getSafeJSONArray("files")
+ .optJSONObject(0)
+}
+
+internal fun JSONObject.getCurrentVersionCreated(): String {
+ // We want to return: `current_version.files[0].created`.
+ return getFile()?.getSafeString("created").orEmpty()
+}
+
+internal fun JSONObject.getDownloadUrl(): String {
+ return getFile()?.getSafeString("url").orEmpty()
+}
+
+internal fun JSONObject.getAuthor(): Addon.Author? {
+ val authorsJson = getSafeJSONArray("authors")
+ // We only consider the first author in the AMO API response, mainly because Gecko does the same.
+ val authorJson = authorsJson.optJSONObject(0)
+
+ return if (authorJson != null) {
+ Addon.Author(
+ name = authorJson.getSafeString("name"),
+ url = authorJson.getSafeString("url"),
+ )
+ } else {
+ null
+ }
+}
+
+internal fun JSONObject.getSafeString(key: String): String {
+ return if (isNull(key)) {
+ ""
+ } else {
+ getString(key)
+ }
+}
+
+internal fun JSONObject.getSafeJSONArray(key: String): JSONArray {
+ return if (isNull(key)) {
+ JSONArray("[]")
+ } else {
+ getJSONArray(key)
+ }
+}
+
+internal fun JSONObject.getSafeTranslations(valueKey: String, language: String?): Map<String, String> {
+ // We can have two different versions of the JSON structure for translatable fields:
+ // 1) A string with only one language, when we provide a language parameter.
+ // 2) An object containing all the languages available when a language parameter is NOT present.
+ // For this reason, we have to be specific about how we parse the JSON.
+ return if (get(valueKey) is String) {
+ val safeLanguage = (language ?: Addon.DEFAULT_LOCALE).lowercase(Locale.ROOT)
+ mapOf(safeLanguage to getSafeString(valueKey))
+ } else {
+ getSafeMap(valueKey)
+ }
+}
+
+internal fun JSONObject.getSafeMap(valueKey: String): Map<String, String> {
+ return if (isNull(valueKey)) {
+ emptyMap()
+ } else {
+ val map = mutableMapOf<String, String>()
+ val jsonObject = getJSONObject(valueKey)
+
+ jsonObject.keys()
+ .forEach { key ->
+ map[key.lowercase(Locale.ROOT)] = jsonObject.getSafeString(key)
+ }
+ map
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidate.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidate.kt
new file mode 100644
index 0000000000..9be45115da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidate.kt
@@ -0,0 +1,54 @@
+/* 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.addons.menu
+
+import android.content.Context
+import androidx.appcompat.content.res.AppCompatResources.getDrawable
+import androidx.core.graphics.drawable.toDrawable
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.ui.icons.R
+
+/**
+ * Create a browser menu item for displaying a web extension action.
+ *
+ * @param onClick a callback to be invoked when this menu item is clicked.
+ */
+fun Action.createMenuCandidate(
+ context: Context,
+ onClick: () -> Unit = this.onClick,
+): TextMenuCandidate {
+ return TextMenuCandidate(
+ title.orEmpty(),
+ start = loadIcon?.let { loadIcon ->
+ val defaultIcon = getDrawable(context, R.drawable.mozac_ic_web_extension_default_icon)
+ AsyncDrawableMenuIcon(
+ loadDrawable = { _, height ->
+ loadIcon(height)?.toDrawable(context.resources)
+ },
+ loadingDrawable = defaultIcon,
+ fallbackDrawable = defaultIcon,
+ )
+ },
+ end = badgeText?.let { badgeText ->
+ TextMenuIcon(
+ badgeText,
+ backgroundTint = badgeBackgroundColor,
+ textStyle = TextStyle(
+ color = badgeTextColor,
+ ),
+ )
+ },
+ containerStyle = ContainerStyle(
+ isVisible = true,
+ isEnabled = enabled ?: false,
+ ),
+ onClick = onClick,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/menu/WebExtensionNestedMenuCandidate.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/menu/WebExtensionNestedMenuCandidate.kt
new file mode 100644
index 0000000000..4c098c5325
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/menu/WebExtensionNestedMenuCandidate.kt
@@ -0,0 +1,148 @@
+/* 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.addons.menu
+
+import android.content.Context
+import androidx.annotation.ColorInt
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.feature.addons.R
+import mozilla.components.ui.icons.R as iconsR
+
+private fun createBackMenuItem(
+ context: Context,
+ @ColorInt webExtIconTintColor: Int?,
+) = NestedMenuCandidate(
+ id = iconsR.drawable.mozac_ic_back_24,
+ text = context.getString(R.string.mozac_feature_addons_addons),
+ start = DrawableMenuIcon(
+ context,
+ iconsR.drawable.mozac_ic_back_24,
+ tint = webExtIconTintColor,
+ ),
+ subMenuItems = null,
+)
+
+private fun createAddonsManagerItem(
+ context: Context,
+ @ColorInt webExtIconTintColor: Int?,
+ onAddonsManagerTapped: () -> Unit,
+) = TextMenuCandidate(
+ text = context.getString(R.string.mozac_feature_addons_addons_manager),
+ start = DrawableMenuIcon(
+ context,
+ iconsR.drawable.mozac_ic_extension_24,
+ tint = webExtIconTintColor,
+ ),
+ onClick = onAddonsManagerTapped,
+)
+
+private fun createWebExtensionSubMenuItems(
+ context: Context,
+ extensions: Collection<WebExtensionState>,
+ tab: SessionState?,
+ onAddonsItemTapped: (String) -> Unit,
+): List<MenuCandidate> {
+ val menuItems = mutableListOf<MenuCandidate>()
+
+ extensions
+ .filter { it.enabled }
+ .filterNot { !it.allowedInPrivateBrowsing && tab?.content?.private == true }
+ .sortedBy { it.name }
+ .forEach { extension ->
+ val tabExtensionState = tab?.extensionState?.get(extension.id)
+ extension.browserAction?.let { browserAction ->
+ menuItems.add(
+ browserAction.copyWithOverride(tabExtensionState?.browserAction).createMenuCandidate(
+ context,
+ ) {
+ onAddonsItemTapped(extension.id)
+ browserAction.onClick()
+ },
+ )
+ }
+
+ extension.pageAction?.let { pageAction ->
+ menuItems.add(
+ pageAction.copyWithOverride(tabExtensionState?.pageAction).createMenuCandidate(
+ context,
+ ) {
+ onAddonsItemTapped(extension.id)
+ pageAction.onClick()
+ },
+ )
+ }
+ }
+
+ return menuItems
+}
+
+/**
+ * Create a browser menu item for displaying a list of web extensions.
+ *
+ * @param tabId ID of tab used to load tab-specific extension state.
+ * @param webExtIconTintColor Optional color used to tint the icons of back and add-ons manager menu items.
+ * @param appendExtensionSubMenuAt If web extension sub menu should appear at the top (start) of
+ * the menu, or if web extensions should appear at the bottom of the menu (end).
+ * @param onAddonsItemTapped Callback to be invoked when a web extension action item is selected.
+ * Can be used to emit telemetry.
+ * @param onAddonsManagerTapped Callback to be invoked when add-ons manager menu item is selected.
+ */
+fun BrowserState.createWebExtensionMenuCandidate(
+ context: Context,
+ tabId: String? = null,
+ @ColorInt webExtIconTintColor: Int? = null,
+ appendExtensionSubMenuAt: Side = Side.END,
+ onAddonsItemTapped: (String) -> Unit = {},
+ onAddonsManagerTapped: () -> Unit = {},
+): MenuCandidate {
+ val items = createWebExtensionSubMenuItems(
+ context,
+ extensions = extensions.values,
+ tab = findTabOrCustomTabOrSelectedTab(tabId),
+ onAddonsItemTapped = onAddonsItemTapped,
+ )
+
+ val addonsManagerItem = createAddonsManagerItem(
+ context,
+ webExtIconTintColor = webExtIconTintColor,
+ onAddonsManagerTapped = onAddonsManagerTapped,
+ )
+
+ return if (items.isNotEmpty()) {
+ val firstItem: MenuCandidate
+ val lastItem: MenuCandidate
+ when (appendExtensionSubMenuAt) {
+ Side.START -> {
+ firstItem = createBackMenuItem(context, webExtIconTintColor)
+ lastItem = addonsManagerItem
+ }
+ Side.END -> {
+ firstItem = addonsManagerItem
+ lastItem = createBackMenuItem(context, webExtIconTintColor)
+ }
+ }
+
+ NestedMenuCandidate(
+ id = R.string.mozac_feature_addons_addons,
+ text = context.getString(R.string.mozac_feature_addons_addons),
+ start = addonsManagerItem.start,
+ subMenuItems = listOf(firstItem, DividerMenuCandidate()) +
+ items + listOf(DividerMenuCandidate(), lastItem),
+ )
+ } else {
+ addonsManagerItem.copy(
+ text = context.getString(R.string.mozac_feature_addons_addons),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/migration/SupportedAddonsChecker.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/migration/SupportedAddonsChecker.kt
new file mode 100644
index 0000000000..2c35a09813
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/migration/SupportedAddonsChecker.kt
@@ -0,0 +1,144 @@
+/* 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.addons.migration
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
+import mozilla.components.feature.addons.worker.shouldReport
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.worker.Frequency
+import java.util.concurrent.TimeUnit
+
+/**
+ * Contract to define the behavior for a periodic checker for newly supported add-ons.
+ */
+interface SupportedAddonsChecker {
+ /**
+ * Registers for periodic checks for new available add-ons.
+ */
+ fun registerForChecks()
+
+ /**
+ * Unregisters for periodic checks for new available add-ons.
+ */
+ fun unregisterForChecks()
+}
+
+/**
+ * An implementation of [SupportedAddonsChecker] that uses the work manager api for scheduling checks.
+ * @property applicationContext The application context.
+ * @param frequency (Optional) indicates how often checks should be performed, defaults
+ * to once per day.
+ */
+@Suppress("LargeClass")
+class DefaultSupportedAddonsChecker(
+ private val applicationContext: Context,
+ private val frequency: Frequency = Frequency(1, TimeUnit.DAYS),
+) : SupportedAddonsChecker {
+ private val logger = Logger("DefaultSupportedAddonsChecker")
+
+ /**
+ * See [SupportedAddonsChecker.registerForChecks]
+ */
+ override fun registerForChecks() {
+ WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
+ CHECKER_UNIQUE_PERIODIC_WORK_NAME,
+ ExistingPeriodicWorkPolicy.UPDATE,
+ createPeriodicWorkerRequest(),
+ )
+ logger.info("Register check for new supported add-ons")
+ }
+
+ /**
+ * See [SupportedAddonsChecker.unregisterForChecks]
+ */
+ override fun unregisterForChecks() {
+ WorkManager.getInstance(applicationContext)
+ .cancelUniqueWork(CHECKER_UNIQUE_PERIODIC_WORK_NAME)
+ logger.info("Unregister check for new supported add-ons")
+ }
+
+ @VisibleForTesting
+ internal fun createPeriodicWorkerRequest(): PeriodicWorkRequest {
+ return PeriodicWorkRequestBuilder<SupportedAddonsWorker>(
+ frequency.repeatInterval,
+ frequency.repeatIntervalTimeUnit,
+ ).apply {
+ setConstraints(getWorkerConstrains())
+ addTag(WORK_TAG_PERIODIC)
+ }.build()
+ }
+
+ @VisibleForTesting
+ internal fun getWorkerConstrains() = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ companion object {
+ private const val IDENTIFIER_PREFIX = "mozilla.components.feature.addons.migration"
+
+ @VisibleForTesting
+ internal const val CHECKER_UNIQUE_PERIODIC_WORK_NAME =
+ "$IDENTIFIER_PREFIX.DefaultSupportedAddonsChecker.periodicWork"
+
+ /**
+ * Identifies all the workers that periodically check for new add-ons.
+ */
+ @VisibleForTesting
+ internal const val WORK_TAG_PERIODIC =
+ "$IDENTIFIER_PREFIX.DefaultSupportedAddonsChecker.periodicWork"
+ }
+}
+
+/**
+ * A implementation which uses WorkManager APIs to check for newly available supported add-ons.
+ */
+internal class SupportedAddonsWorker(
+ val context: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+ private val logger = Logger("SupportedAddonsWorker")
+
+ @Suppress("TooGenericExceptionCaught")
+ override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
+ try {
+ logger.info("Trying to check for new supported add-ons")
+
+ val addonManager = GlobalAddonDependencyProvider.requireAddonManager()
+ val addonUpdater = GlobalAddonDependencyProvider.requireAddonUpdater()
+
+ addonManager.getAddons().filter { addon ->
+ addon.isSupported() && addon.isDisabledAsUnsupported()
+ }.let { addons ->
+ if (addons.isNotEmpty()) {
+ addons.forEach { addon ->
+ addonUpdater.registerForFutureUpdates(addon.id)
+ }
+ val extIds = addons.joinToString { addon -> addon.id }
+ logger.info("New supported add-ons available $extIds")
+ }
+ }
+ } catch (exception: Exception) {
+ logger.error(
+ "An exception happened trying to check for new supported add-ons, re-schedule ${exception.message}",
+ exception,
+ )
+ if (exception.shouldReport()) {
+ GlobalAddonDependencyProvider.onCrash?.invoke(exception)
+ }
+ }
+ Result.success()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt
new file mode 100644
index 0000000000..ef465781c7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt
@@ -0,0 +1,54 @@
+/* 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.addons.ui
+
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.os.Bundle
+import androidx.annotation.ColorRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AppCompatDialogFragment
+import androidx.appcompat.widget.AppCompatImageView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+@VisibleForTesting
+internal const val KEY_ICON = "KEY_ICON"
+
+/**
+ * A generic [Addon] dialog which has an [Addon]'s icon.
+ */
+open class AddonDialogFragment : AppCompatDialogFragment() {
+ init {
+ arguments = arguments ?: Bundle()
+ }
+
+ @VisibleForTesting
+ internal val safeArguments get() = requireNotNull(arguments)
+
+ internal fun loadIcon(addon: Addon, iconView: AppCompatImageView) {
+ val icon =
+ safeArguments.getParcelableCompat(KEY_ICON, Bitmap::class.java)
+ if (icon != null) {
+ iconView.setImageDrawable(BitmapDrawable(iconView.resources, addon.icon))
+ } else {
+ safeArguments.putParcelable(KEY_ICON, addon.provideIcon())
+ iconView.setIcon(addon)
+ }
+ }
+
+ /**
+ * Styling for the addon installation dialog.
+ */
+ data class PromptsStyling(
+ val gravity: Int,
+ val shouldWidthMatchParent: Boolean = false,
+ @ColorRes
+ val confirmButtonBackgroundColor: Int? = null,
+ @ColorRes
+ val confirmButtonTextColor: Int? = null,
+ val confirmButtonRadius: Float? = null,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonFilePicker.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonFilePicker.kt
new file mode 100644
index 0000000000..3f83780a5c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonFilePicker.kt
@@ -0,0 +1,90 @@
+/* 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.addons.ui
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.activity.result.ActivityResultCaller
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import mozilla.components.concept.engine.webextension.InstallationMethod
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.net.getFileName
+import mozilla.components.support.ktx.android.net.toFileUri
+import java.io.File
+
+private const val XPI_DIR = "XPIs"
+
+/**
+ * Allows to launch a file picker to select an add-on file.
+ * @param context the application context
+ */
+class AddonFilePicker(
+ val context: Context,
+ private val addonManager: AddonManager,
+) {
+ internal lateinit var activityLauncher: ActivityResultLauncher<Array<String>>
+ private val logger = Logger("AddonFilePicker")
+
+ /**
+ * Launch an Android file picker where the user can select an add-on file.
+ * @returns a [Boolean] indicating if the file picker was launched successfully or not.
+ */
+ fun launch(): Boolean {
+ return try {
+ activityLauncher.launch(emptyArray())
+ true
+ } catch (e: ActivityNotFoundException) {
+ logger.error("Unable to find an app to select an XPI file", e)
+ false
+ }
+ }
+
+ /**
+ * Registers a listener for file picker results.
+ * @param resultCaller The [ActivityResultCaller] on which results will be observed.
+ */
+ fun registerForResults(resultCaller: ActivityResultCaller) {
+ activityLauncher =
+ resultCaller.registerForActivityResult(AddonOpenDocument(), ::handleUriSelected)
+ }
+
+ internal fun handleUriSelected(uri: Uri?) {
+ uri?.let { safeUri ->
+ val fileUri = convertToFileUri(safeUri)
+ val onSuccess: ((Addon) -> Unit) = {
+ logger.info("Add-on from $fileUri installed successfully")
+ removeTemporaryFile(safeUri)
+ }
+ val onError: ((Throwable) -> Unit) = { throwable ->
+ logger.error("Unable to install add-on from $fileUri", throwable)
+ removeTemporaryFile(safeUri)
+ }
+ addonManager.installAddon(
+ fileUri,
+ InstallationMethod.FROM_FILE,
+ onSuccess,
+ onError,
+ )
+ }
+ }
+
+ internal fun removeTemporaryFile(fileUri: Uri) {
+ val contentResolver = context.contentResolver
+ File(File(context.cacheDir, XPI_DIR), fileUri.getFileName(contentResolver)).delete()
+ }
+
+ internal fun convertToFileUri(uri: Uri): String = uri.toFileUri(context, XPI_DIR).toString()
+}
+
+internal open class AddonOpenDocument : ActivityResultContracts.OpenDocument() {
+ override fun createIntent(context: Context, input: Array<String>): Intent {
+ return super.createIntent(context, arrayOf("application/x-xpinstall", "application/zip"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragment.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragment.kt
new file mode 100644
index 0000000000..095d6bf301
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragment.kt
@@ -0,0 +1,254 @@
+/* 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.addons.ui
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.GradientDrawable
+import android.os.Bundle
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatCheckBox
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentManager
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.feature.addons.databinding.MozacFeatureAddonsFragmentDialogAddonInstalledBinding
+import mozilla.components.support.ktx.android.content.appName
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+@VisibleForTesting internal const val KEY_INSTALLED_ADDON = "KEY_INSTALLED_ADDON"
+private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY"
+private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT"
+private const val KEY_CONFIRM_BUTTON_BACKGROUND_COLOR = "KEY_CONFIRM_BUTTON_BACKGROUND_COLOR"
+private const val KEY_CONFIRM_BUTTON_TEXT_COLOR = "KEY_CONFIRM_BUTTON_TEXT_COLOR"
+private const val KEY_CONFIRM_BUTTON_RADIUS = "KEY_CONFIRM_BUTTON_RADIUS"
+
+private const val DEFAULT_VALUE = Int.MAX_VALUE
+
+/**
+ * A dialog that shows [Addon] installation confirmation.
+ */
+class AddonInstallationDialogFragment : AddonDialogFragment() {
+ /**
+ * A lambda called when the confirm button is clicked.
+ */
+ var onConfirmButtonClicked: ((Addon, Boolean) -> Unit)? = null
+
+ /**
+ * A lambda called when the dialog is dismissed.
+ */
+ var onDismissed: (() -> Unit)? = null
+
+ internal val addon get() = requireNotNull(safeArguments.getParcelableCompat(KEY_INSTALLED_ADDON, Addon::class.java))
+ private var allowPrivateBrowsing: Boolean = false
+
+ internal val confirmButtonRadius
+ get() =
+ safeArguments.getFloat(KEY_CONFIRM_BUTTON_RADIUS, DEFAULT_VALUE.toFloat())
+
+ internal val dialogGravity: Int
+ get() =
+ safeArguments.getInt(
+ KEY_DIALOG_GRAVITY,
+ DEFAULT_VALUE,
+ )
+ internal val dialogShouldWidthMatchParent: Boolean
+ get() =
+ safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT)
+
+ internal val confirmButtonBackgroundColor
+ get() =
+ safeArguments.getInt(
+ KEY_CONFIRM_BUTTON_BACKGROUND_COLOR,
+ DEFAULT_VALUE,
+ )
+
+ internal val confirmButtonTextColor
+ get() =
+ safeArguments.getInt(
+ KEY_CONFIRM_BUTTON_TEXT_COLOR,
+ DEFAULT_VALUE,
+ )
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ onDismissed?.invoke()
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val sheetDialog = Dialog(requireContext())
+ sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ sheetDialog.setCanceledOnTouchOutside(true)
+
+ val rootView = createContainer()
+
+ sheetDialog.setContainerView(rootView)
+
+ sheetDialog.window?.apply {
+ if (dialogGravity != DEFAULT_VALUE) {
+ setGravity(dialogGravity)
+ }
+
+ if (dialogShouldWidthMatchParent) {
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ // This must be called after addContentView, or it won't fully fill to the edge.
+ setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+ }
+
+ return sheetDialog
+ }
+
+ private fun Dialog.setContainerView(rootView: View) {
+ if (dialogShouldWidthMatchParent) {
+ setContentView(rootView)
+ } else {
+ addContentView(
+ rootView,
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ ),
+ )
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ private fun createContainer(): View {
+ val rootView = LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_feature_addons_fragment_dialog_addon_installed,
+ null,
+ false,
+ )
+
+ val binding = MozacFeatureAddonsFragmentDialogAddonInstalledBinding.bind(rootView)
+
+ val addonName = addon.translateName(requireContext())
+ rootView.findViewById<TextView>(R.id.title).text =
+ requireContext().getString(
+ R.string.mozac_feature_addons_installed_dialog_title,
+ addonName,
+ requireContext().appName,
+ )
+ rootView.findViewById<TextView>(R.id.description).text =
+ requireContext().getString(
+ R.string.mozac_feature_addons_installed_dialog_description_2,
+ addonName,
+ requireContext().appName,
+ )
+
+ loadIcon(addon = addon, iconView = binding.icon)
+
+ val allowedInPrivateBrowsing = rootView.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
+ if (addon.incognito == Addon.Incognito.NOT_ALLOWED) {
+ allowedInPrivateBrowsing.isVisible = false
+ } else {
+ allowedInPrivateBrowsing.setOnCheckedChangeListener { _, isChecked ->
+ allowPrivateBrowsing = isChecked
+ }
+ }
+
+ val confirmButton = rootView.findViewById<Button>(R.id.confirm_button)
+ confirmButton.setOnClickListener {
+ onConfirmButtonClicked?.invoke(addon, allowPrivateBrowsing)
+ dismiss()
+ }
+
+ if (confirmButtonBackgroundColor != DEFAULT_VALUE) {
+ val backgroundTintList =
+ ContextCompat.getColorStateList(requireContext(), confirmButtonBackgroundColor)
+ confirmButton.backgroundTintList = backgroundTintList
+ }
+
+ if (confirmButtonTextColor != DEFAULT_VALUE) {
+ val color = ContextCompat.getColor(requireContext(), confirmButtonTextColor)
+ confirmButton.setTextColor(color)
+ }
+
+ if (confirmButtonRadius != DEFAULT_VALUE.toFloat()) {
+ val shape = GradientDrawable()
+ shape.shape = GradientDrawable.RECTANGLE
+ shape.setColor(
+ ContextCompat.getColor(
+ requireContext(),
+ confirmButtonBackgroundColor,
+ ),
+ )
+ shape.cornerRadius = confirmButtonRadius
+ confirmButton.background = shape
+ }
+
+ return rootView
+ }
+
+ override fun show(manager: FragmentManager, tag: String?) {
+ // This dialog is shown as a result of an async operation (installing
+ // an add-on). Once installation succeeds, the activity may already be
+ // in the process of being destroyed. Since the dialog doesn't have any
+ // state we need to keep, and since it's also fine to not display the
+ // dialog at all in case the user navigates away, we can simply use
+ // commitAllowingStateLoss here to prevent crashing on commit:
+ // https://github.com/mozilla-mobile/android-components/issues/7782
+ val ft = manager.beginTransaction()
+ ft.add(this, tag)
+ ft.commitAllowingStateLoss()
+ }
+
+ companion object {
+ /**
+ * Returns a new instance of [AddonInstallationDialogFragment].
+ * @param addon The addon to show in the dialog.
+ * @param promptsStyling Styling properties for the dialog.
+ * @param onDismissed A lambda called when the dialog is dismissed.
+ * @param onConfirmButtonClicked A lambda called when the confirm button is clicked.
+ */
+ fun newInstance(
+ addon: Addon,
+ promptsStyling: PromptsStyling? = PromptsStyling(
+ gravity = Gravity.BOTTOM,
+ shouldWidthMatchParent = true,
+ ),
+ onDismissed: (() -> Unit)? = null,
+ onConfirmButtonClicked: ((Addon, Boolean) -> Unit)? = null,
+ ): AddonInstallationDialogFragment {
+ val fragment = AddonInstallationDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ arguments.apply {
+ putParcelable(KEY_INSTALLED_ADDON, addon)
+
+ promptsStyling?.gravity?.apply {
+ putInt(KEY_DIALOG_GRAVITY, this)
+ }
+ promptsStyling?.shouldWidthMatchParent?.apply {
+ putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, this)
+ }
+ promptsStyling?.confirmButtonBackgroundColor?.apply {
+ putInt(KEY_CONFIRM_BUTTON_BACKGROUND_COLOR, this)
+ }
+
+ promptsStyling?.confirmButtonTextColor?.apply {
+ putInt(KEY_CONFIRM_BUTTON_TEXT_COLOR, this)
+ }
+ }
+ fragment.onConfirmButtonClicked = onConfirmButtonClicked
+ fragment.onDismissed = onDismissed
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonPermissionsAdapter.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonPermissionsAdapter.kt
new file mode 100644
index 0000000000..c59f94a9ea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonPermissionsAdapter.kt
@@ -0,0 +1,73 @@
+/* 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.addons.ui
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.addons.R
+
+/**
+ * An adapter for displaying the permissions of an add-on.
+ *
+ * @property permissions The list of [mozilla.components.feature.addons.Addon] permissions to display.
+ * @property style Indicates how permission items should look like.
+ */
+class AddonPermissionsAdapter(
+ private val permissions: List<String>,
+ private val style: Style? = null,
+) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PermissionViewHolder {
+ val context = parent.context
+ val inflater = LayoutInflater.from(context)
+ val view = inflater.inflate(R.layout.mozac_feature_addons_permission_item, parent, false)
+ val titleView = view.findViewById<TextView>(R.id.permission)
+ return PermissionViewHolder(
+ view,
+ titleView,
+ )
+ }
+
+ override fun getItemCount() = permissions.size
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ holder as PermissionViewHolder
+ val permission = permissions[position]
+ with(holder.textView) {
+ text = permission
+ contentDescription = context.getString(
+ R.string.mozac_feature_addons_permissions_content_description_item,
+ permission,
+ position + 1,
+ permissions.size,
+ )
+ style?.maybeSetItemTextColor(this)
+ }
+ }
+
+ /**
+ * A view holder for displaying the permissions of an add-on.
+ */
+ class PermissionViewHolder(
+ val view: View,
+ val textView: TextView,
+ ) : RecyclerView.ViewHolder(view)
+
+ /**
+ * Allows to customize how permission items should look like.
+ */
+ data class Style(@ColorRes val itemsTextColor: Int? = null) {
+ internal fun maybeSetItemTextColor(textView: TextView) {
+ itemsTextColor?.let {
+ val color = ContextCompat.getColor(textView.context, it)
+ textView.setTextColor(color)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonsManagerAdapter.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonsManagerAdapter.kt
new file mode 100644
index 0000000000..76ffb0990a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonsManagerAdapter.kt
@@ -0,0 +1,548 @@
+/* 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.addons.ui
+
+import android.annotation.SuppressLint
+import android.graphics.Typeface
+import android.os.Build
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.ImageView
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.annotation.DimenRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import mozilla.components.browser.state.action.ExtensionsProcessAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.feature.addons.ui.CustomViewHolder.AddonViewHolder
+import mozilla.components.feature.addons.ui.CustomViewHolder.FooterViewHolder
+import mozilla.components.feature.addons.ui.CustomViewHolder.HeaderViewHolder
+import mozilla.components.feature.addons.ui.CustomViewHolder.SectionViewHolder
+import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder
+import mozilla.components.support.ktx.android.content.appName
+import mozilla.components.support.ktx.android.content.appVersionName
+
+private const val VIEW_HOLDER_TYPE_SECTION = 0
+private const val VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION = 1
+private const val VIEW_HOLDER_TYPE_ADDON = 2
+private const val VIEW_HOLDER_TYPE_FOOTER = 3
+private const val VIEW_HOLDER_TYPE_HEADER = 4
+
+/**
+ * An adapter for displaying add-on items. This will display information related to the state of
+ * an add-on such as recommended, unsupported or installed. In addition, it will perform actions
+ * such as installing an add-on.
+ *
+ * @property addonsManagerDelegate Delegate that will provides method for handling the add-on items.
+ * @param addons The list of add-ons to display.
+ * @property style Indicates how items should look like.
+ * @property excludedAddonIDs The list of add-on IDs to be excluded from the recommended section.
+ */
+@Suppress("LargeClass")
+class AddonsManagerAdapter(
+ private val addonsManagerDelegate: AddonsManagerAdapterDelegate,
+ addons: List<Addon>,
+ private val style: Style? = null,
+ private val excludedAddonIDs: List<String> = emptyList(),
+ private val store: BrowserStore,
+) : ListAdapter<Any, CustomViewHolder>(DifferCallback) {
+ /**
+ * Represents all the add-ons that will be distributed in multiple headers like
+ * enabled, recommended and unsupported, this help have the data source of the items,
+ * displayed in the UI.
+ */
+ @VisibleForTesting
+ internal var addonsMap: MutableMap<String, Addon> = addons.associateBy({ it.id }, { it }).toMutableMap()
+
+ init {
+ submitList(createListWithSections(addons, excludedAddonIDs))
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
+ return when (viewType) {
+ VIEW_HOLDER_TYPE_ADDON -> createAddonViewHolder(parent)
+ VIEW_HOLDER_TYPE_SECTION -> createSectionViewHolder(parent)
+ VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION -> createUnsupportedSectionViewHolder(parent)
+ VIEW_HOLDER_TYPE_FOOTER -> createFooterSectionViewHolder(parent)
+ VIEW_HOLDER_TYPE_HEADER -> createHeaderSectionViewHolder(parent)
+ else -> throw IllegalArgumentException("Unrecognized viewType")
+ }
+ }
+
+ override fun onViewRecycled(holder: CustomViewHolder) {
+ super.onViewRecycled(holder)
+ when (holder) {
+ is AddonViewHolder -> {
+ // Prevent the previous icon from showing while scrolling.
+ holder.iconView.setImageBitmap(null)
+ }
+
+ else -> {}
+ }
+ }
+
+ private fun createSectionViewHolder(parent: ViewGroup): CustomViewHolder {
+ val context = parent.context
+ val inflater = LayoutInflater.from(context)
+ val view = inflater.inflate(R.layout.mozac_feature_addons_section_item, parent, false)
+ val titleView = view.findViewById<TextView>(R.id.title)
+ val divider = view.findViewById<View>(R.id.divider)
+ return SectionViewHolder(view, titleView, divider)
+ }
+
+ private fun createFooterSectionViewHolder(parent: ViewGroup): CustomViewHolder {
+ val context = parent.context
+ val inflater = LayoutInflater.from(context)
+ val view = inflater.inflate(
+ R.layout.mozac_feature_addons_footer_section_item,
+ parent,
+ false,
+ )
+ return FooterViewHolder(view)
+ }
+
+ private fun createHeaderSectionViewHolder(parent: ViewGroup): CustomViewHolder {
+ val context = parent.context
+ val inflater = LayoutInflater.from(context)
+ val view = inflater.inflate(
+ R.layout.mozac_feature_addons_header_section_item,
+ parent,
+ false,
+ )
+ val restartButton = view.findViewById<TextView>(R.id.restart_button)
+
+ return HeaderViewHolder(view, restartButton)
+ }
+
+ private fun createUnsupportedSectionViewHolder(parent: ViewGroup): CustomViewHolder {
+ val context = parent.context
+ val inflater = LayoutInflater.from(context)
+ val view = inflater.inflate(
+ R.layout.mozac_feature_addons_section_unsupported_section_item,
+ parent,
+ false,
+ )
+ val titleView = view.findViewById<TextView>(R.id.title)
+ val descriptionView = view.findViewById<TextView>(R.id.description)
+
+ return UnsupportedSectionViewHolder(view, titleView, descriptionView)
+ }
+
+ private fun createAddonViewHolder(parent: ViewGroup): AddonViewHolder {
+ val context = parent.context
+ val inflater = LayoutInflater.from(context)
+ val view = inflater.inflate(R.layout.mozac_feature_addons_item, parent, false)
+ val contentWrapperView = view.findViewById<View>(R.id.add_on_content_wrapper)
+ val iconView = view.findViewById<ImageView>(R.id.add_on_icon)
+ val titleView = view.findViewById<TextView>(R.id.add_on_name)
+ val summaryView = view.findViewById<TextView>(R.id.add_on_description)
+ val ratingView = view.findViewById<RatingBar>(R.id.rating)
+ val ratingAccessibleView = view.findViewById<TextView>(R.id.rating_accessibility)
+ val reviewCountView = view.findViewById<TextView>(R.id.review_count)
+ val addButton = view.findViewById<ImageView>(R.id.add_button)
+ val allowedInPrivateBrowsingLabel = view.findViewById<ImageView>(R.id.allowed_in_private_browsing_label)
+ val statusErrorView = view.findViewById<View>(R.id.add_on_status_error)
+ return AddonViewHolder(
+ view,
+ contentWrapperView,
+ iconView,
+ titleView,
+ summaryView,
+ ratingView,
+ ratingAccessibleView,
+ reviewCountView,
+ addButton,
+ allowedInPrivateBrowsingLabel,
+ statusErrorView,
+ )
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return when (getItem(position)) {
+ is Addon -> VIEW_HOLDER_TYPE_ADDON
+ is Section -> VIEW_HOLDER_TYPE_SECTION
+ is NotYetSupportedSection -> VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION
+ is FooterSection -> VIEW_HOLDER_TYPE_FOOTER
+ is HeaderSection -> VIEW_HOLDER_TYPE_HEADER
+ else -> throw IllegalArgumentException("items[position] has unrecognized type")
+ }
+ }
+
+ override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
+ val item = getItem(position)
+
+ // Configure an accessibility delegate for each item.
+ holder.itemView.accessibilityDelegate = object : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ info.collectionItemInfo = AccessibilityNodeInfo.CollectionItemInfo(
+ holder.bindingAdapterPosition,
+ 1,
+ 1,
+ 1,
+ holder is SectionViewHolder,
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ info.collectionItemInfo = AccessibilityNodeInfo.CollectionItemInfo.obtain(
+ holder.bindingAdapterPosition,
+ 1,
+ 1,
+ 1,
+ holder is SectionViewHolder,
+ )
+ }
+ }
+ }
+
+ when (holder) {
+ is SectionViewHolder -> bindSection(holder, item as Section, position)
+ is AddonViewHolder -> bindAddon(holder, item as Addon)
+ is UnsupportedSectionViewHolder -> bindNotYetSupportedSection(
+ holder,
+ item as NotYetSupportedSection,
+ )
+ is FooterViewHolder -> bindFooterButton(holder)
+ is HeaderViewHolder -> bindHeaderButton(holder)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun bindSection(holder: SectionViewHolder, section: Section, position: Int) {
+ holder.titleView.setText(section.title)
+ ViewCompat.setAccessibilityHeading(holder.titleView, true)
+
+ style?.let {
+ holder.divider.isVisible = it.visibleDividers && position != 0
+ it.maybeSetSectionsTextColor(holder.titleView)
+ it.maybeSetSectionsTypeFace(holder.titleView)
+ it.maybeSetSectionsDividerStyle(holder.divider)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun bindNotYetSupportedSection(
+ holder: UnsupportedSectionViewHolder,
+ section: NotYetSupportedSection,
+ ) {
+ val unsupportedAddons = addonsMap.values.filter { it.inUnsupportedSection() }
+ val context = holder.itemView.context
+ holder.titleView.setText(section.title)
+ holder.descriptionView.text =
+ if (unsupportedAddons.size == 1) {
+ context.getString(R.string.mozac_feature_addons_unsupported_caption_2)
+ } else {
+ context.getString(
+ R.string.mozac_feature_addons_unsupported_caption_plural_2,
+ unsupportedAddons.size.toString(),
+ )
+ }
+
+ holder.itemView.setOnClickListener {
+ addonsManagerDelegate.onNotYetSupportedSectionClicked(unsupportedAddons)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun bindFooterButton(holder: FooterViewHolder) {
+ holder.itemView.setOnClickListener {
+ addonsManagerDelegate.onFindMoreAddonsButtonClicked()
+ }
+ }
+
+ internal fun bindHeaderButton(holder: HeaderViewHolder) {
+ holder.restartButton.setOnClickListener {
+ store.dispatch(ExtensionsProcessAction.EnabledAction)
+ // Remove the notification.
+ submitList(currentList.filter { item: Any -> item != HeaderSection })
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ @Suppress("LongMethod")
+ internal fun bindAddon(
+ holder: AddonViewHolder,
+ addon: Addon,
+ appName: String = holder.itemView.context.appName,
+ appVersion: String = holder.itemView.context.appVersionName,
+ ) {
+ val context = holder.itemView.context
+ addon.rating?.let {
+ val reviewCount = context.getString(R.string.mozac_feature_addons_user_rating_count_2)
+ val ratingContentDescription =
+ String.format(
+ context.getString(R.string.mozac_feature_addons_rating_content_description_2),
+ it.average,
+ )
+ holder.ratingView.contentDescription = ratingContentDescription
+ // Android RatingBar is not very accessibility-friendly, we will use non visible TextView
+ // for contentDescription for the TalkBack feature
+ holder.ratingAccessibleView.text = ratingContentDescription
+ holder.ratingView.rating = it.average
+ holder.reviewCountView.text = String.format(reviewCount, getFormattedAmount(it.reviews))
+ }
+
+ val addonName = if (addon.translatableName.isNotEmpty()) {
+ addon.translateName(context)
+ } else {
+ addon.id
+ }
+
+ holder.titleView.text = addonName
+
+ if (addon.translatableSummary.isNotEmpty()) {
+ holder.summaryView.text = addon.translateSummary(context)
+ } else {
+ holder.summaryView.visibility = View.GONE
+ }
+
+ holder.itemView.tag = addon
+ // Attach the on click listener to the content wrapper so that it doesn't overlap with the install button.
+ holder.contentWrapperView.setOnClickListener {
+ addonsManagerDelegate.onAddonItemClicked(addon)
+ }
+
+ holder.addButton.isInvisible = addon.isInstalled()
+ holder.addButton.contentDescription = context.getString(
+ R.string.mozac_feature_addons_install_addon_content_description_2,
+ addonName,
+ )
+ holder.addButton.setOnClickListener {
+ if (!addon.isInstalled()) {
+ addonsManagerDelegate.onInstallAddonButtonClicked(addon)
+ }
+ }
+
+ holder.allowedInPrivateBrowsingLabel.isVisible = addon.isAllowedInPrivateBrowsing()
+ style?.maybeSetPrivateBrowsingLabelDrawable(holder.allowedInPrivateBrowsingLabel)
+
+ holder.iconView.setIcon(addon)
+
+ style?.maybeSetAddonNameTextColor(holder.titleView)
+ style?.maybeSetAddonSummaryTextColor(holder.summaryView)
+
+ val statusErrorMessage = holder.statusErrorView.findViewById<TextView>(R.id.add_on_status_error_message)
+ val statusErrorLearnMoreLink = holder.statusErrorView.findViewById<TextView>(
+ R.id.add_on_status_error_learn_more_link,
+ )
+ if (addon.isDisabledAsBlocklisted()) {
+ statusErrorMessage.text = context.getString(R.string.mozac_feature_addons_status_blocklisted, addonName)
+ statusErrorLearnMoreLink.setOnClickListener {
+ addonsManagerDelegate.onLearnMoreLinkClicked(
+ AddonsManagerAdapterDelegate.LearnMoreLinks.BLOCKLISTED_ADDON,
+ addon,
+ )
+ }
+ holder.statusErrorView.isVisible = true
+ } else if (addon.isDisabledAsNotCorrectlySigned()) {
+ statusErrorMessage.text = context.getString(R.string.mozac_feature_addons_status_unsigned, addonName)
+ statusErrorLearnMoreLink.setOnClickListener {
+ addonsManagerDelegate.onLearnMoreLinkClicked(
+ AddonsManagerAdapterDelegate.LearnMoreLinks.ADDON_NOT_CORRECTLY_SIGNED,
+ addon,
+ )
+ }
+ holder.statusErrorView.isVisible = true
+ } else if (addon.isDisabledAsIncompatible()) {
+ statusErrorMessage.text = context.getString(
+ R.string.mozac_feature_addons_status_incompatible,
+ addonName,
+ appName,
+ appVersion,
+ )
+ holder.statusErrorView.isVisible = true
+ // There is no link when the add-on is disabled because it isn't compatible with the application version.
+ statusErrorLearnMoreLink.isVisible = false
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ @Suppress("ComplexMethod")
+ internal fun createListWithSections(addons: List<Addon>, excludedAddonIDs: List<String> = emptyList()): List<Any> {
+ val itemsWithSections = ArrayList<Any>()
+ val installedAddons = ArrayList<Addon>()
+ val recommendedAddons = ArrayList<Addon>()
+ val disabledAddons = ArrayList<Addon>()
+ val unsupportedAddons = ArrayList<Addon>()
+
+ addons.forEach { addon ->
+ when {
+ addon.inUnsupportedSection() -> unsupportedAddons.add(addon)
+ addon.inRecommendedSection() -> recommendedAddons.add(addon)
+ addon.inInstalledSection() -> installedAddons.add(addon)
+ addon.inDisabledSection() -> disabledAddons.add(addon)
+ }
+ }
+
+ // Calls are safe, except in tests since the store is mocked in most cases.
+ @Suppress("UNNECESSARY_SAFE_CALL")
+ if (store?.state?.extensionsProcessDisabled == true) {
+ itemsWithSections.add(HeaderSection)
+ }
+
+ // Add installed section and addons if available
+ if (installedAddons.isNotEmpty()) {
+ itemsWithSections.add(Section(R.string.mozac_feature_addons_enabled, false))
+ itemsWithSections.addAll(installedAddons)
+ }
+
+ // Add disabled section and addons if available
+ if (disabledAddons.isNotEmpty()) {
+ itemsWithSections.add(Section(R.string.mozac_feature_addons_disabled_section, true))
+ itemsWithSections.addAll(disabledAddons)
+ }
+
+ // Add recommended section and addons if available
+ if (recommendedAddons.isNotEmpty()) {
+ itemsWithSections.add(Section(R.string.mozac_feature_addons_recommended_section, true))
+ val filteredRecommendedAddons = recommendedAddons.filter {
+ it.id !in excludedAddonIDs
+ }
+ itemsWithSections.addAll(filteredRecommendedAddons)
+ }
+
+ // Add unsupported section
+ if (unsupportedAddons.isNotEmpty()) {
+ itemsWithSections.add(NotYetSupportedSection(R.string.mozac_feature_addons_unavailable_section))
+ }
+
+ if (addonsManagerDelegate.shouldShowFindMoreAddonsButton()) {
+ itemsWithSections.add(FooterSection)
+ }
+
+ return itemsWithSections
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal data class Section(@StringRes val title: Int, val visibleDivider: Boolean = true)
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal data class NotYetSupportedSection(@StringRes val title: Int)
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal object FooterSection
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal object HeaderSection
+
+ /**
+ * Allows to customize how items should look like.
+ */
+ data class Style(
+ @ColorRes
+ val sectionsTextColor: Int? = null,
+ @ColorRes
+ val addonNameTextColor: Int? = null,
+ @ColorRes
+ val addonSummaryTextColor: Int? = null,
+ val sectionsTypeFace: Typeface? = null,
+ @DrawableRes
+ val addonAllowPrivateBrowsingLabelDrawableRes: Int? = null,
+ val visibleDividers: Boolean = true,
+ @ColorRes
+ val dividerColor: Int? = null,
+ @DimenRes
+ val dividerHeight: Int? = null,
+ ) {
+ internal fun maybeSetSectionsTextColor(textView: TextView) {
+ sectionsTextColor?.let {
+ val color = ContextCompat.getColor(textView.context, it)
+ textView.setTextColor(color)
+ }
+ }
+
+ internal fun maybeSetSectionsTypeFace(textView: TextView) {
+ sectionsTypeFace?.let {
+ textView.typeface = it
+ }
+ }
+
+ internal fun maybeSetAddonNameTextColor(textView: TextView) {
+ addonNameTextColor?.let {
+ val color = ContextCompat.getColor(textView.context, it)
+ textView.setTextColor(color)
+ }
+ }
+
+ internal fun maybeSetAddonSummaryTextColor(textView: TextView) {
+ addonSummaryTextColor?.let {
+ val color = ContextCompat.getColor(textView.context, it)
+ textView.setTextColor(color)
+ }
+ }
+
+ internal fun maybeSetPrivateBrowsingLabelDrawable(imageView: ImageView) {
+ addonAllowPrivateBrowsingLabelDrawableRes?.let {
+ imageView.setImageDrawable(ContextCompat.getDrawable(imageView.context, it))
+ }
+ }
+
+ internal fun maybeSetSectionsDividerStyle(divider: View) {
+ dividerColor?.let {
+ divider.setBackgroundColor(it)
+ }
+ dividerHeight?.let {
+ divider.layoutParams.height = divider.context.resources.getDimensionPixelOffset(it)
+ }
+ }
+ }
+
+ /**
+ * Update the portion of the list that contains the provided [addon].
+ * @property addon The add-on to be updated.
+ */
+ fun updateAddon(addon: Addon) {
+ addonsMap[addon.id] = addon
+ submitList(createListWithSections(addonsMap.values.toList(), excludedAddonIDs))
+ }
+
+ /**
+ * Updates only the portion of the list that changes between the current list and the new provided [addons].
+ * Be aware that updating a subset of the visible list is not supported, [addons] will replace
+ * the current list, but only the add-ons that have been changed will be updated in the UI.
+ * If you provide a subset it will replace the current list.
+ * @property addons A list of add-on to replace the actual list.
+ */
+ fun updateAddons(addons: List<Addon>) {
+ addonsMap = addons.associateBy({ it.id }, { it }).toMutableMap()
+ submitList(createListWithSections(addons))
+ }
+
+ internal object DifferCallback : DiffUtil.ItemCallback<Any>() {
+ override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
+ return when {
+ oldItem is Addon && newItem is Addon -> oldItem.id == newItem.id
+ oldItem is Section && newItem is Section -> oldItem.title == newItem.title
+ oldItem is NotYetSupportedSection && newItem is NotYetSupportedSection -> oldItem.title == newItem.title
+ else -> false
+ }
+ }
+
+ @SuppressLint("DiffUtilEquals")
+ override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
+ return oldItem == newItem
+ }
+ }
+}
+
+private fun Addon.inUnsupportedSection() = isInstalled() && !isSupported()
+private fun Addon.inRecommendedSection() = !isInstalled()
+private fun Addon.inInstalledSection() = isInstalled() && isSupported() && isEnabled()
+private fun Addon.inDisabledSection() = isInstalled() && isSupported() && !isEnabled()
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterDelegate.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterDelegate.kt
new file mode 100644
index 0000000000..52af64db38
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterDelegate.kt
@@ -0,0 +1,63 @@
+/* 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.addons.ui
+
+import mozilla.components.feature.addons.Addon
+
+/**
+ * Provides methods for handling the add-on items in the add-on manager.
+ */
+interface AddonsManagerAdapterDelegate {
+ /**
+ * Defines the different learn more links a user might click.
+ */
+ enum class LearnMoreLinks {
+ /**
+ * The [Addon] is blocklisted and the learn more link should give more information to the user.
+ */
+ BLOCKLISTED_ADDON,
+
+ /**
+ * The [Addon] is not correctly signed and the learn more link should give more information to the user.
+ */
+ ADDON_NOT_CORRECTLY_SIGNED,
+ }
+
+ /**
+ * Handler for when an add-on item is clicked.
+ *
+ * @param addon The [Addon] that was clicked.
+ */
+ fun onAddonItemClicked(addon: Addon) = Unit
+
+ /**
+ * Handler for when the install add-on button is clicked.
+ *
+ * @param addon The [Addon] to install.
+ */
+ fun onInstallAddonButtonClicked(addon: Addon) = Unit
+
+ /**
+ * Handler for when the not yet supported section is clicked.
+ *
+ * @param unsupportedAddons The list of unsupported [Addon].
+ */
+ fun onNotYetSupportedSectionClicked(unsupportedAddons: List<Addon>) = Unit
+
+ /**
+ * Handler to determine whether to show the "find more add-ons" button in the add-ons manager.
+ */
+ fun shouldShowFindMoreAddonsButton(): Boolean = false
+
+ /**
+ * Handler for when the "find more add-ons" button is clicked.
+ */
+ fun onFindMoreAddonsButtonClicked() = Unit
+
+ /**
+ * Handler for when a "learn more" link on an add-on item is clicked.
+ */
+ fun onLearnMoreLinkClicked(link: LearnMoreLinks, addon: Addon) = Unit
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/CustomViewHolder.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/CustomViewHolder.kt
new file mode 100644
index 0000000000..7268d53d31
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/CustomViewHolder.kt
@@ -0,0 +1,65 @@
+/* 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.addons.ui
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * A base view holder.
+ */
+sealed class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ /**
+ * A view holder for displaying section items.
+ */
+ class SectionViewHolder(
+ view: View,
+ val titleView: TextView,
+ val divider: View,
+ ) : CustomViewHolder(view)
+
+ /**
+ * A view holder for displaying Not yet supported section items.
+ */
+ class UnsupportedSectionViewHolder(
+ view: View,
+ val titleView: TextView,
+ val descriptionView: TextView,
+ ) : CustomViewHolder(view)
+
+ /**
+ * A view holder for displaying add-on items.
+ */
+ @Suppress("LongParameterList")
+ class AddonViewHolder(
+ view: View,
+ val contentWrapperView: View,
+ val iconView: ImageView,
+ val titleView: TextView,
+ val summaryView: TextView,
+ val ratingView: RatingBar,
+ val ratingAccessibleView: TextView,
+ val reviewCountView: TextView,
+ val addButton: ImageView,
+ val allowedInPrivateBrowsingLabel: ImageView,
+ val statusErrorView: View,
+ ) : CustomViewHolder(view)
+
+ /**
+ * A view holder for displaying a section above the list of add-ons.
+ */
+ class HeaderViewHolder(
+ view: View,
+ val restartButton: TextView,
+ ) : CustomViewHolder(view)
+
+ /**
+ * A view holder for displaying a section below the list of add-ons.
+ */
+ class FooterViewHolder(view: View) : CustomViewHolder(view)
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/Extensions.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/Extensions.kt
new file mode 100644
index 0000000000..c1b9871b9d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/Extensions.kt
@@ -0,0 +1,143 @@
+/* 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.addons.ui
+
+import android.content.Context
+import android.graphics.drawable.BitmapDrawable
+import android.widget.ImageView
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.ContextCompat
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.feature.addons.update.AddonUpdater
+import mozilla.components.feature.addons.update.AddonUpdater.Status.Error
+import mozilla.components.feature.addons.update.AddonUpdater.Status.NoUpdateAvailable
+import mozilla.components.feature.addons.update.AddonUpdater.Status.SuccessfullyUpdated
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+import java.text.DateFormat
+import java.text.NumberFormat
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * Used to parse [Addon.createdAt] and [Addon.updatedAt].
+ */
+private val dateParser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
+ timeZone = TimeZone.getTimeZone("GMT")
+}
+
+/**
+ * A shortcut to get the localized name of an add-on.
+ */
+fun Addon.translateName(context: Context): String = translatableName.translate(this, context)
+
+/**
+ * A shortcut to get the localized summary of an add-on.
+ */
+fun Addon.translateSummary(context: Context): String = translatableSummary.translate(this, context)
+
+/**
+ * A shortcut to get the localized description of an add-on.
+ */
+fun Addon.translateDescription(context: Context): String = translatableDescription.translate(this, context)
+
+/**
+ * The date the add-on was created, as a JVM date object.
+ */
+val Addon.createdAtDate: Date get() =
+ // This method never returns null and will throw a ParseException if parsing fails
+ dateParser.parse(createdAt)!!
+
+/**
+ * The date of the last time the add-on was updated by its developer(s),
+ * as a JVM date object.
+ */
+val Addon.updatedAtDate: Date get() = dateParser.parse(updatedAt)!!
+
+/**
+ * Try to find the default language on the map otherwise defaults to [Addon.DEFAULT_LOCALE].
+ */
+internal fun Map<String, String>.translate(addon: Addon, context: Context): String {
+ val lang = Locale.getDefault().language
+ val safeLang = if (!lang.isNullOrEmpty()) lang.lowercase(Locale.getDefault()) else lang
+ return get(safeLang) ?: getOrElse(addon.defaultLocale) {
+ context.getString(R.string.mozac_feature_addons_failed_to_translate, lang, addon.defaultLocale)
+ }
+}
+
+/**
+ * Get the formatted number amount for the current default locale.
+ */
+internal fun getFormattedAmount(amount: Int): String {
+ return NumberFormat.getNumberInstance(Locale.getDefault()).format(amount)
+}
+
+/**
+ * Get the localized string for an [AddonUpdater.UpdateAttempt.status].
+ */
+fun AddonUpdater.Status?.toLocalizedString(context: Context): String {
+ return when (this) {
+ SuccessfullyUpdated -> {
+ context.getString(R.string.mozac_feature_addons_updater_status_successfully_updated)
+ }
+ NoUpdateAvailable -> {
+ context.getString(R.string.mozac_feature_addons_updater_status_no_update_available)
+ }
+ is Error -> {
+ val errorLabel = context.getString(R.string.mozac_feature_addons_updater_status_error)
+ "$errorLabel $exception"
+ }
+ else -> ""
+ }
+}
+
+/**
+ * Shows a dialog containing all the information related to the given [AddonUpdater.UpdateAttempt].
+ */
+fun AddonUpdater.UpdateAttempt.showInformationDialog(context: Context) {
+ AlertDialog.Builder(context)
+ .setTitle(R.string.mozac_feature_addons_updater_dialog_title)
+ .setMessage(getDialogMessage(context))
+ .show()
+ .withCenterAlignedButtons()
+}
+
+private fun AddonUpdater.UpdateAttempt.getDialogMessage(context: Context): String {
+ val statusString = status.toLocalizedString(context)
+ val dateString = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(date)
+ val lastAttemptLabel = context.getString(R.string.mozac_feature_addons_updater_dialog_last_attempt)
+ val statusLabel = context.getString(R.string.mozac_feature_addons_updater_dialog_status)
+ return "$lastAttemptLabel $dateString \n $statusLabel $statusString ".trimMargin()
+}
+
+/**
+ * Set icon to this [ImageView] with from the provided [Addon]'s icon.
+ */
+fun ImageView.setIcon(addon: Addon) {
+ val icon = addon.provideIcon()
+ if (icon != null) {
+ setImageDrawable(BitmapDrawable(resources, icon))
+ } else {
+ setDefaultAddonIcon()
+ }
+}
+
+/**
+ * Set the default addon's icon to this [ImageView].
+ */
+fun ImageView.setDefaultAddonIcon() {
+ val safeContext = context ?: return
+ val att = safeContext.theme.resolveAttribute(android.R.attr.textColorPrimary)
+ setColorFilter(ContextCompat.getColor(safeContext, att))
+ setImageDrawable(
+ ContextCompat.getDrawable(
+ safeContext,
+ mozilla.components.ui.icons.R.drawable.mozac_ic_extension_24,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/PermissionsDialogFragment.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/PermissionsDialogFragment.kt
new file mode 100644
index 0000000000..92e555e722
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/PermissionsDialogFragment.kt
@@ -0,0 +1,281 @@
+/* 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.addons.ui
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.GradientDrawable
+import android.os.Bundle
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.Button
+import android.widget.LinearLayout.LayoutParams
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+internal const val KEY_ADDON = "KEY_ADDON"
+private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY"
+private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT"
+private const val KEY_POSITIVE_BUTTON_BACKGROUND_COLOR = "KEY_POSITIVE_BUTTON_BACKGROUND_COLOR"
+private const val KEY_POSITIVE_BUTTON_TEXT_COLOR = "KEY_POSITIVE_BUTTON_TEXT_COLOR"
+private const val KEY_POSITIVE_BUTTON_RADIUS = "KEY_POSITIVE_BUTTON_RADIUS"
+private const val KEY_FOR_OPTIONAL_PERMISSIONS = "KEY_FOR_OPTIONAL_PERMISSIONS"
+internal const val KEY_OPTIONAL_PERMISSIONS = "KEY_OPTIONAL_PERMISSIONS"
+private const val DEFAULT_VALUE = Int.MAX_VALUE
+
+/**
+ * A dialog that shows a set of permission required by an [Addon].
+ */
+class PermissionsDialogFragment : AddonDialogFragment() {
+
+ /**
+ * A lambda called when the allow button is clicked.
+ */
+ var onPositiveButtonClicked: ((Addon) -> Unit)? = null
+
+ /**
+ * A lambda called when the deny button is clicked.
+ */
+ var onNegativeButtonClicked: (() -> Unit)? = null
+
+ internal val addon get() = requireNotNull(safeArguments.getParcelableCompat(KEY_ADDON, Addon::class.java))
+
+ internal val positiveButtonRadius
+ get() =
+ safeArguments.getFloat(KEY_POSITIVE_BUTTON_RADIUS, DEFAULT_VALUE.toFloat())
+
+ internal val dialogGravity: Int
+ get() =
+ safeArguments.getInt(
+ KEY_DIALOG_GRAVITY,
+ DEFAULT_VALUE,
+ )
+ internal val dialogShouldWidthMatchParent: Boolean
+ get() =
+ safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT)
+
+ internal val positiveButtonBackgroundColor
+ get() =
+ safeArguments.getInt(
+ KEY_POSITIVE_BUTTON_BACKGROUND_COLOR,
+ DEFAULT_VALUE,
+ )
+
+ internal val positiveButtonTextColor
+ get() =
+ safeArguments.getInt(
+ KEY_POSITIVE_BUTTON_TEXT_COLOR,
+ DEFAULT_VALUE,
+ )
+
+ /**
+ * This flag is used to adjust the permissions prompt for optional permissions (instead of asking
+ * users to grant the required permissions at install time, which is the default).
+ */
+ internal val forOptionalPermissions: Boolean
+ get() =
+ safeArguments.getBoolean(KEY_FOR_OPTIONAL_PERMISSIONS)
+
+ internal val optionalPermissions get() = requireNotNull(safeArguments.getStringArray(KEY_OPTIONAL_PERMISSIONS))
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val sheetDialog = Dialog(requireContext())
+ sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ sheetDialog.setCanceledOnTouchOutside(true)
+
+ val rootView = createContainer()
+
+ sheetDialog.setContainerView(rootView)
+
+ sheetDialog.window?.apply {
+ if (dialogGravity != DEFAULT_VALUE) {
+ setGravity(dialogGravity)
+ }
+
+ if (dialogShouldWidthMatchParent) {
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ // This must be called after addContentView, or it won't fully fill to the edge.
+ setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+ }
+
+ return sheetDialog
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ onNegativeButtonClicked?.invoke()
+ }
+
+ private fun Dialog.setContainerView(rootView: View) {
+ if (dialogShouldWidthMatchParent) {
+ setContentView(rootView)
+ } else {
+ addContentView(
+ rootView,
+ LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT,
+ ),
+ )
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ private fun createContainer(): View {
+ val rootView = LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_feature_addons_fragment_dialog_addon_permissions,
+ null,
+ false,
+ )
+
+ loadIcon(addon = addon, iconView = rootView.findViewById(R.id.icon))
+
+ rootView.findViewById<TextView>(R.id.title).text = requireContext().getString(
+ if (forOptionalPermissions) {
+ R.string.mozac_feature_addons_optional_permissions_dialog_title
+ } else {
+ R.string.mozac_feature_addons_permissions_dialog_title
+ },
+ addon.translateName(requireContext()),
+ )
+ rootView.findViewById<TextView>(R.id.optional_or_required_text).text = buildOptionalOrRequiredText()
+
+ val listPermissions = buildPermissionsList()
+ val permissionsRecyclerView = rootView.findViewById<RecyclerView>(R.id.permissions)
+ val positiveButton = rootView.findViewById<Button>(R.id.allow_button)
+ val negativeButton = rootView.findViewById<Button>(R.id.deny_button)
+
+ permissionsRecyclerView.adapter = RequiredPermissionsAdapter(listPermissions)
+ permissionsRecyclerView.layoutManager = LinearLayoutManager(context)
+
+ if (forOptionalPermissions) {
+ positiveButton.text = requireContext().getString(R.string.mozac_feature_addons_permissions_dialog_allow)
+ negativeButton.text = requireContext().getString(R.string.mozac_feature_addons_permissions_dialog_deny)
+ }
+
+ positiveButton.setOnClickListener {
+ onPositiveButtonClicked?.invoke(addon)
+ dismiss()
+ }
+
+ if (positiveButtonBackgroundColor != DEFAULT_VALUE) {
+ val backgroundTintList =
+ ContextCompat.getColorStateList(requireContext(), positiveButtonBackgroundColor)
+ positiveButton.backgroundTintList = backgroundTintList
+ }
+
+ if (positiveButtonTextColor != DEFAULT_VALUE) {
+ val color = ContextCompat.getColor(requireContext(), positiveButtonTextColor)
+ positiveButton.setTextColor(color)
+ }
+
+ if (positiveButtonRadius != DEFAULT_VALUE.toFloat()) {
+ val shape = GradientDrawable()
+ shape.shape = GradientDrawable.RECTANGLE
+ shape.setColor(
+ ContextCompat.getColor(
+ requireContext(),
+ positiveButtonBackgroundColor,
+ ),
+ )
+ shape.cornerRadius = positiveButtonRadius
+ positiveButton.background = shape
+ }
+
+ negativeButton.setOnClickListener {
+ onNegativeButtonClicked?.invoke()
+ dismiss()
+ }
+
+ return rootView
+ }
+
+ @VisibleForTesting
+ internal fun buildPermissionsList(): List<String> {
+ val permissionsList = if (forOptionalPermissions) {
+ Addon.localizePermissions(optionalPermissions.asList(), requireContext())
+ } else {
+ addon.translatePermissions(requireContext())
+ }
+
+ return permissionsList
+ }
+
+ @VisibleForTesting
+ internal fun buildOptionalOrRequiredText(): String {
+ val optionalOrRequiredText = if (forOptionalPermissions) {
+ getString(R.string.mozac_feature_addons_optional_permissions_dialog_subtitle)
+ } else {
+ getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)
+ }
+
+ return optionalOrRequiredText
+ }
+
+ companion object {
+ /**
+ * Returns a new instance of [PermissionsDialogFragment].
+ * @param addon The addon to show in the dialog.
+ * @param forOptionalPermissions Whether to show a permission dialog for optional permissions
+ * requested by the extension.
+ * @param optionalPermissions The optional permissions requested by the extension. Only used
+ * when [forOptionalPermissions] is true.
+ * @param promptsStyling Styling properties for the dialog.
+ * @param onPositiveButtonClicked A lambda called when the allow button is clicked.
+ * @param onNegativeButtonClicked A lambda called when the deny button is clicked.
+ */
+ fun newInstance(
+ addon: Addon,
+ forOptionalPermissions: Boolean = false,
+ optionalPermissions: List<String> = emptyList(),
+ promptsStyling: PromptsStyling? = PromptsStyling(
+ gravity = Gravity.BOTTOM,
+ shouldWidthMatchParent = true,
+ ),
+ onPositiveButtonClicked: ((Addon) -> Unit)? = null,
+ onNegativeButtonClicked: (() -> Unit)? = null,
+ ): PermissionsDialogFragment {
+ val fragment = PermissionsDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ arguments.apply {
+ putParcelable(KEY_ADDON, addon)
+ putBoolean(KEY_FOR_OPTIONAL_PERMISSIONS, forOptionalPermissions)
+ putStringArray(KEY_OPTIONAL_PERMISSIONS, optionalPermissions.toTypedArray())
+
+ promptsStyling?.gravity?.apply {
+ putInt(KEY_DIALOG_GRAVITY, this)
+ }
+ promptsStyling?.shouldWidthMatchParent?.apply {
+ putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, this)
+ }
+ promptsStyling?.confirmButtonBackgroundColor?.apply {
+ putInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, this)
+ }
+
+ promptsStyling?.confirmButtonTextColor?.apply {
+ putInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, this)
+ }
+ }
+ fragment.onPositiveButtonClicked = onPositiveButtonClicked
+ fragment.onNegativeButtonClicked = onNegativeButtonClicked
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/RequiredPermissionsAdapter.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/RequiredPermissionsAdapter.kt
new file mode 100644
index 0000000000..72aa400600
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/RequiredPermissionsAdapter.kt
@@ -0,0 +1,52 @@
+/* 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.addons.ui
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.addons.R
+
+/**
+* An adapter for displaying optional or required permissions before installing an addon.
+*
+* @property permissions The list of the permissions that will be displayed.
+*/
+class RequiredPermissionsAdapter(private val permissions: List<String>) :
+ RecyclerView.Adapter<RequiredPermissionsAdapter.ViewHolder>() {
+
+ /**
+ * Function used to specify where to display each permission.
+ */
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val permissionRequired: TextView
+
+ init {
+ permissionRequired = itemView.findViewById(R.id.permission_required_item)
+ }
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(viewGroup.context)
+ .inflate(R.layout.mozac_feature_addons_permissions_required_item, viewGroup, false)
+
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
+ viewHolder.permissionRequired.text = permissions[position]
+ }
+
+ override fun getItemCount() = permissions.size
+
+ /**
+ * Function used in tests to verify the permissions.
+ */
+ fun getItemAtPosition(position: Int): String {
+ return permissions[position]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapter.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapter.kt
new file mode 100644
index 0000000000..09d20ba4c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapter.kt
@@ -0,0 +1,112 @@
+/* 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.addons.ui
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.feature.addons.R
+
+/**
+ * An adapter for displaying unsupported add-on items.
+ *
+ * @property addonManager Manager of installed and recommended [Addon]s and manages their states.
+ * @property unsupportedAddonsAdapterDelegate Delegate that will provides callbacks for handling
+ * any interactions with the unsupported add-ons to the app to handle.
+ * @property unsupportedAddons The list of unsupported add-ons based on the AMO store.
+ */
+@SuppressLint("NotifyDataSetChanged")
+class UnsupportedAddonsAdapter(
+ private val addonManager: AddonManager,
+ private val unsupportedAddonsAdapterDelegate: UnsupportedAddonsAdapterDelegate,
+ addons: List<Addon>,
+) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+ private val unsupportedAddons = addons.toMutableList()
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var pendingUninstall = false
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ holder as UnsupportedAddonViewHolder
+ val addon = unsupportedAddons[position]
+
+ holder.titleView.text =
+ if (addon.translatableName.isNotEmpty()) {
+ addon.translateName(holder.titleView.context)
+ } else {
+ addon.id
+ }
+
+ bindRemoveButton(holder, addon)
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun bindRemoveButton(holder: UnsupportedAddonViewHolder, addon: Addon) {
+ holder.removeButton.isEnabled = !pendingUninstall
+ if (pendingUninstall) {
+ return
+ }
+
+ holder.removeButton.setOnClickListener {
+ pendingUninstall = true
+ notifyDataSetChanged()
+ addonManager.uninstallAddon(
+ addon,
+ onSuccess = {
+ removeUninstalledAddon(addon)
+ },
+ onError = { addonId, throwable ->
+ pendingUninstall = false
+ notifyDataSetChanged()
+ unsupportedAddonsAdapterDelegate.onUninstallError(addonId, throwable)
+ },
+ )
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun removeUninstalledAddon(addon: Addon) {
+ if (!unsupportedAddons.remove(addon)) {
+ return
+ }
+ pendingUninstall = false
+ notifyDataSetChanged()
+ unsupportedAddonsAdapterDelegate.onUninstallSuccess()
+ }
+
+ override fun getItemCount(): Int {
+ return unsupportedAddons.size
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val context = parent.context
+ val inflater = LayoutInflater.from(context)
+ val view = inflater.inflate(R.layout.mozac_feature_addons_unsupported_item, parent, false)
+
+ val iconView = view.findViewById<ImageView>(R.id.add_on_icon)
+ val titleView = view.findViewById<TextView>(R.id.add_on_name)
+ val removeButton = view.findViewById<ImageButton>(R.id.add_on_remove_button)
+
+ return UnsupportedAddonViewHolder(view, iconView, titleView, removeButton)
+ }
+
+ /**
+ * A view holder for displaying unsupported add-on items.
+ */
+ class UnsupportedAddonViewHolder(
+ view: View,
+ val iconView: ImageView,
+ val titleView: TextView,
+ val removeButton: ImageButton,
+ ) : RecyclerView.ViewHolder(view)
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterDelegate.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterDelegate.kt
new file mode 100644
index 0000000000..3795d92fad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterDelegate.kt
@@ -0,0 +1,24 @@
+/* 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.addons.ui
+
+/**
+ * Provides methods for handling the success and error callbacks from uninstalling an add-on in the
+ * list of unsupported add-on items.
+ */
+interface UnsupportedAddonsAdapterDelegate {
+ /**
+ * Callback invoked if the addon was uninstalled successfully.
+ */
+ fun onUninstallSuccess() = Unit
+
+ /**
+ * Callback invoked if there was an error uninstalling the addon.
+ *
+ * @param addonId The unique id of the [Addon].
+ * @param throwable An exception to log.
+ */
+ fun onUninstallError(addonId: String, throwable: Throwable) = Unit
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/AddonUpdater.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/AddonUpdater.kt
new file mode 100644
index 0000000000..717c1bdef6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/AddonUpdater.kt
@@ -0,0 +1,677 @@
+/* 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.addons.update
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.os.IBinder
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionException
+import mozilla.components.concept.engine.webextension.isUnsupported
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.feature.addons.update.db.UpdateAttemptsDatabase
+import mozilla.components.feature.addons.update.db.toEntity
+import mozilla.components.feature.addons.worker.shouldReport
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.worker.Frequency
+import mozilla.components.support.ktx.android.notification.ChannelData
+import mozilla.components.support.ktx.android.notification.ensureNotificationChannelExists
+import mozilla.components.support.utils.PendingIntentUtils
+import mozilla.components.support.webextensions.WebExtensionSupport
+import java.lang.Exception
+import java.util.Date
+import java.util.concurrent.TimeUnit
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * Contract to define the behavior for updating addons.
+ */
+interface AddonUpdater {
+ /**
+ * Registers the given [addonId] for periodically check for new updates.
+ * @param addonId The unique id of the addon.
+ */
+ fun registerForFutureUpdates(addonId: String)
+
+ /**
+ * Unregisters the given [addonId] for periodically checking for new updates.
+ * @param addonId The unique id of the addon.
+ */
+ fun unregisterForFutureUpdates(addonId: String)
+
+ /**
+ * Try to perform an update on the given [addonId].
+ * @param addonId The unique id of the addon.
+ */
+ fun update(addonId: String)
+
+ /**
+ * Invoked when a web extension has changed its permissions while trying to update to a
+ * new version. This requires user interaction as the updated extension will not be installed,
+ * until the user grants the new permissions.
+ *
+ * @param current The current [WebExtension].
+ * @param updated The updated [WebExtension] that requires extra permissions.
+ * @param newPermissions Contains a list of all the new permissions.
+ * @param onPermissionsGranted A callback to indicate if the new permissions from the [updated] extension
+ * are granted or not.
+ */
+ fun onUpdatePermissionRequest(
+ current: WebExtension,
+ updated: WebExtension,
+ newPermissions: List<String>,
+ onPermissionsGranted: ((Boolean) -> Unit),
+ )
+
+ /**
+ * Registers the [extensions] for periodic updates, if applicable. Built-in and
+ * unsupported extensions will not update automatically.
+ *
+ * @param extensions The extensions to be registered for updates.
+ */
+ fun registerForFutureUpdates(extensions: List<WebExtension>) {
+ extensions
+ .filter { extension ->
+ !extension.isBuiltIn() && !extension.isUnsupported()
+ }
+ .forEach { extension ->
+ registerForFutureUpdates(extension.id)
+ }
+ }
+
+ /**
+ * Indicates the status of a request for updating an addon.
+ */
+ sealed class Status {
+ /**
+ * The addon is not part of the installed list.
+ */
+ object NotInstalled : Status()
+
+ /**
+ * The addon was successfully updated.
+ */
+ object SuccessfullyUpdated : Status()
+
+ /**
+ * The addon has not been updated since the last update.
+ */
+ object NoUpdateAvailable : Status()
+
+ /**
+ * An error has happened while trying to update.
+ * @property message A string message describing what has happened.
+ * @property exception The exception of the error.
+ */
+ data class Error(val message: String, val exception: Throwable) : Status()
+ }
+
+ /**
+ * Represents an attempt to update an add-on.
+ */
+ data class UpdateAttempt(val addonId: String, val date: Date, val status: Status?)
+}
+
+/**
+ * An implementation of [AddonUpdater] that uses the work manager api for scheduling new updates.
+ * @property applicationContext The application context.
+ * @param frequency (Optional) indicates how often updates should be performed, defaults
+ * to one day.
+ */
+@Suppress("LargeClass")
+class DefaultAddonUpdater(
+ private val applicationContext: Context,
+ private val frequency: Frequency = Frequency(1, TimeUnit.DAYS),
+ private val notificationsDelegate: NotificationsDelegate,
+) : AddonUpdater {
+ private val logger = Logger("DefaultAddonUpdater")
+
+ @VisibleForTesting
+ internal var scope = CoroutineScope(Dispatchers.IO)
+
+ @VisibleForTesting
+ internal val updateStatusStorage = UpdateStatusStorage()
+ internal var updateAttempStorage = UpdateAttemptStorage(applicationContext)
+
+ /**
+ * See [AddonUpdater.registerForFutureUpdates]. If an add-on is already registered nothing will happen.
+ */
+ override fun registerForFutureUpdates(addonId: String) {
+ WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
+ getUniquePeriodicWorkName(addonId),
+ ExistingPeriodicWorkPolicy.KEEP,
+ createPeriodicWorkerRequest(addonId),
+ )
+ logger.info("registerForFutureUpdates $addonId")
+ }
+
+ /**
+ * See [AddonUpdater.unregisterForFutureUpdates]
+ */
+ override fun unregisterForFutureUpdates(addonId: String) {
+ WorkManager.getInstance(applicationContext)
+ .cancelUniqueWork(getUniquePeriodicWorkName(addonId))
+ logger.info("unregisterForFutureUpdates $addonId")
+ scope.launch {
+ updateAttempStorage.remove(addonId)
+ }
+ }
+
+ /**
+ * See [AddonUpdater.update]
+ */
+ override fun update(addonId: String) {
+ WorkManager.getInstance(applicationContext).beginUniqueWork(
+ getUniqueImmediateWorkName(addonId),
+ ExistingWorkPolicy.KEEP,
+ createImmediateWorkerRequest(addonId),
+ ).enqueue()
+ logger.info("update $addonId")
+ }
+
+ /**
+ * See [AddonUpdater.onUpdatePermissionRequest]
+ */
+ override fun onUpdatePermissionRequest(
+ current: WebExtension,
+ updated: WebExtension,
+ newPermissions: List<String>,
+ onPermissionsGranted: (Boolean) -> Unit,
+ ) {
+ logger.info("onUpdatePermissionRequest $current")
+
+ val shouldGrantWithoutPrompt = Addon.localizePermissions(newPermissions, applicationContext).isEmpty()
+ val shouldNotPrompt =
+ updateStatusStorage.isPreviouslyAllowed(applicationContext, updated.id) || shouldGrantWithoutPrompt
+
+ // When the extension update doesn't have new permissions that the user should grant with a prompt,
+ // we allow the update to continue.
+ //
+ // Otherwise, the permission request will first be user-cancelled because we return `false` below
+ // but we create an Android notification right after, which is responsible for prompting the user.
+ // When the user allows the new permissions in the Android notification, the extension update is
+ // triggered again and - since the permissions have been previously allowed - there is no new
+ // permissions that the user should grant and so we allow the update to continue. At this point,
+ // the extension is fully updated.
+ onPermissionsGranted(shouldNotPrompt)
+
+ if (shouldNotPrompt) {
+ // Update has been completed at this point.
+ updateStatusStorage.markAsUnallowed(applicationContext, updated.id)
+ } else {
+ // We create the Android notification here.
+ val notificationId = NotificationHandlerService.getNotificationId(applicationContext, updated.id)
+ val notification = createNotification(updated, newPermissions, notificationId)
+ notificationsDelegate.notify(notificationId = notificationId, notification = notification)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createImmediateWorkerRequest(addonId: String): OneTimeWorkRequest {
+ val data = AddonUpdaterWorker.createWorkerData(addonId)
+ val constraints = getWorkerConstrains()
+
+ return OneTimeWorkRequestBuilder<AddonUpdaterWorker>()
+ .setConstraints(constraints)
+ .setInputData(data)
+ .addTag(getUniqueImmediateWorkName(addonId))
+ .addTag(WORK_TAG_IMMEDIATE)
+ .build()
+ }
+
+ @VisibleForTesting
+ internal fun createPeriodicWorkerRequest(addonId: String): PeriodicWorkRequest {
+ val data = AddonUpdaterWorker.createWorkerData(addonId)
+ val constraints = getWorkerConstrains()
+
+ return PeriodicWorkRequestBuilder<AddonUpdaterWorker>(
+ frequency.repeatInterval,
+ frequency.repeatIntervalTimeUnit,
+ )
+ .setConstraints(constraints)
+ .setInputData(data)
+ .addTag(getUniquePeriodicWorkName(addonId))
+ .addTag(WORK_TAG_PERIODIC)
+ .build()
+ }
+
+ @VisibleForTesting
+ internal fun getWorkerConstrains() = Constraints.Builder()
+ .setRequiresStorageNotLow(true)
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ @VisibleForTesting
+ internal fun getUniquePeriodicWorkName(addonId: String) =
+ "$IDENTIFIER_PREFIX$addonId.periodicWork"
+
+ @VisibleForTesting
+ internal fun getUniqueImmediateWorkName(extensionId: String) =
+ "$IDENTIFIER_PREFIX$extensionId.immediateWork"
+
+ @VisibleForTesting
+ internal fun createNotification(
+ extension: WebExtension,
+ newPermissions: List<String>,
+ notificationId: Int,
+ ): Notification {
+ val channel = ChannelData(
+ NOTIFICATION_CHANNEL_ID,
+ R.string.mozac_feature_addons_updater_notification_channel_2,
+ NotificationManagerCompat.IMPORTANCE_LOW,
+ )
+ val channelId = ensureNotificationChannelExists(applicationContext, channel)
+ val text = createContentText(newPermissions)
+
+ logger.info("Created update notification for add-on ${extension.id}")
+ return NotificationCompat.Builder(applicationContext, channelId)
+ .setSmallIcon(iconsR.drawable.mozac_ic_extension_24)
+ .setContentTitle(getNotificationTitle(extension))
+ .setContentText(text)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setStyle(
+ NotificationCompat.BigTextStyle()
+ .bigText(text),
+ )
+ .setContentIntent(createContentIntent())
+ .addAction(createAllowAction(extension, notificationId))
+ .addAction(createDenyAction(extension, notificationId))
+ .setAutoCancel(true)
+ .build()
+ }
+
+ private fun getNotificationTitle(extension: WebExtension): String {
+ return applicationContext.getString(
+ R.string.mozac_feature_addons_updater_notification_title,
+ extension.getMetadata()?.name,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun createContentText(newPermissions: List<String>): String {
+ val validNewPermissions = Addon.localizePermissions(newPermissions, applicationContext)
+
+ val string = if (validNewPermissions.size == 1) {
+ R.string.mozac_feature_addons_updater_notification_content_singular
+ } else {
+ R.string.mozac_feature_addons_updater_notification_content
+ }
+ val contentText = applicationContext.getString(string, validNewPermissions.size)
+ var permissionIndex = 1
+ val permissionsText =
+ validNewPermissions.joinToString(separator = "\n") {
+ "${permissionIndex++}-$it"
+ }
+ return "$contentText:\n $permissionsText"
+ }
+
+ private fun createContentIntent(): PendingIntent {
+ val intent =
+ applicationContext.packageManager.getLaunchIntentForPackage(applicationContext.packageName)?.apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ } ?: throw IllegalStateException("Package has no launcher intent")
+ return PendingIntent.getActivity(
+ applicationContext,
+ 0,
+ intent,
+ PendingIntentUtils.defaultFlags or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun createNotificationIntent(extId: String, actionString: String): Intent {
+ return Intent(applicationContext, NotificationHandlerService::class.java).apply {
+ action = actionString
+ putExtra(NOTIFICATION_EXTRA_ADDON_ID, extId)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createAllowAction(ext: WebExtension, requestCode: Int): NotificationCompat.Action {
+ val allowIntent = createNotificationIntent(ext.id, NOTIFICATION_ACTION_ALLOW)
+ val allowPendingIntent = PendingIntent.getService(
+ applicationContext,
+ requestCode,
+ allowIntent,
+ PendingIntentUtils.defaultFlags,
+ )
+
+ val allowText =
+ applicationContext.getString(R.string.mozac_feature_addons_updater_notification_allow_button)
+
+ return NotificationCompat.Action.Builder(
+ iconsR.drawable.mozac_ic_extension_24,
+ allowText,
+ allowPendingIntent,
+ ).build()
+ }
+
+ @VisibleForTesting
+ internal fun createDenyAction(ext: WebExtension, requestCode: Int): NotificationCompat.Action {
+ val denyIntent = createNotificationIntent(ext.id, NOTIFICATION_ACTION_DENY)
+ val denyPendingIntent = PendingIntent.getService(
+ applicationContext,
+ requestCode,
+ denyIntent,
+ PendingIntentUtils.defaultFlags,
+ )
+
+ val denyText =
+ applicationContext.getString(R.string.mozac_feature_addons_updater_notification_deny_button)
+
+ return NotificationCompat.Action.Builder(
+ iconsR.drawable.mozac_ic_extension_24,
+ denyText,
+ denyPendingIntent,
+ ).build()
+ }
+
+ companion object {
+ private const val NOTIFICATION_CHANNEL_ID =
+ "mozilla.components.feature.addons.update.generic.channel"
+
+ @VisibleForTesting
+ internal const val NOTIFICATION_EXTRA_ADDON_ID =
+ "mozilla.components.feature.addons.update.extra.extensionId"
+
+ @VisibleForTesting
+ internal const val NOTIFICATION_TAG = "mozilla.components.feature.addons.update.addonUpdater"
+
+ @VisibleForTesting
+ internal const val NOTIFICATION_ACTION_DENY =
+ "mozilla.components.feature.addons.update.NOTIFICATION_ACTION_DENY"
+
+ @VisibleForTesting
+ internal const val NOTIFICATION_ACTION_ALLOW =
+ "mozilla.components.feature.addons.update.NOTIFICATION_ACTION_ALLOW"
+ private const val IDENTIFIER_PREFIX = "mozilla.components.feature.addons.update."
+
+ /**
+ * Identifies all the workers that periodically check for new updates.
+ */
+ @VisibleForTesting
+ internal const val WORK_TAG_PERIODIC =
+ "mozilla.components.feature.addons.update.addonUpdater.periodicWork"
+
+ /**
+ * Identifies all the workers that immediately check for new updates.
+ */
+ @VisibleForTesting
+ internal const val WORK_TAG_IMMEDIATE =
+ "mozilla.components.feature.addons.update.addonUpdater.immediateWork"
+ }
+
+ /**
+ * Handles notification actions related to addons that require additional permissions
+ * to be updated.
+ */
+ class NotificationHandlerService : Service() {
+
+ private val logger = Logger("NotificationHandlerService")
+
+ @VisibleForTesting
+ internal var context: Context = this
+
+ internal fun onHandleIntent(intent: Intent?) {
+ val addonId = intent?.getStringExtra(NOTIFICATION_EXTRA_ADDON_ID) ?: return
+
+ when (intent.action) {
+ NOTIFICATION_ACTION_ALLOW -> {
+ handleAllowAction(addonId)
+ }
+ NOTIFICATION_ACTION_DENY -> {
+ removeNotification(addonId)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun removeNotification(addonId: String) {
+ val notificationId = getNotificationId(context, addonId)
+ NotificationManagerCompat.from(context).cancel(notificationId)
+ }
+
+ @VisibleForTesting
+ internal fun handleAllowAction(addonId: String) {
+ val storage = UpdateStatusStorage()
+ logger.info("Addon $addonId permissions were granted")
+ storage.markAsAllowed(context, addonId)
+ GlobalAddonDependencyProvider.requireAddonUpdater().update(addonId)
+ removeNotification(addonId)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ onHandleIntent(intent)
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ companion object {
+ @VisibleForTesting
+ internal fun getNotificationId(context: Context, addonId: String): Int {
+ return SharedIdsHelper.getIdForTag(context, "$NOTIFICATION_TAG.$addonId")
+ }
+ }
+ }
+
+ /**
+ * Stores the status of an addon update.
+ */
+ internal class UpdateStatusStorage {
+
+ fun isPreviouslyAllowed(context: Context, addonId: String) =
+ getData(context).contains(addonId)
+
+ @Synchronized
+ fun markAsAllowed(context: Context, addonId: String) {
+ val allowSet = getData(context)
+ allowSet.add(addonId)
+ setData(context, allowSet)
+ }
+
+ @Synchronized
+ fun markAsUnallowed(context: Context, addonId: String) {
+ val allowSet = getData(context)
+ allowSet.remove(addonId)
+ setData(context, allowSet)
+ }
+
+ fun clear(context: Context) {
+ val settings = getSharedPreferences(context)
+ settings.edit().clear().apply()
+ }
+
+ private fun getSettings(context: Context) = getSharedPreferences(context)
+
+ private fun setData(context: Context, allowSet: MutableSet<String>) {
+ getSettings(context)
+ .edit()
+ .putStringSet(KEY_ALLOWED_SET, allowSet)
+ .apply()
+ }
+
+ private fun getData(context: Context): MutableSet<String> {
+ val settings = getSharedPreferences(context)
+ return requireNotNull(settings.getStringSet(KEY_ALLOWED_SET, mutableSetOf()))
+ }
+
+ private fun getSharedPreferences(context: Context): SharedPreferences {
+ return context.getSharedPreferences(PREFERENCE_FILE, Context.MODE_PRIVATE)
+ }
+
+ companion object {
+ private const val PREFERENCE_FILE =
+ "mozilla.components.feature.addons.update.addons_updates_status_preference"
+ private const val KEY_ALLOWED_SET =
+ "mozilla.components.feature.addons.update.KEY_ALLOWED_SET"
+ }
+ }
+
+ /**
+ * A storage implementation to persist [AddonUpdater.UpdateAttempt]s.
+ */
+ class UpdateAttemptStorage(context: Context) {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var databaseInitializer = {
+ UpdateAttemptsDatabase.get(context)
+ }
+ private val database by lazy { databaseInitializer() }
+
+ /**
+ * Persists the [AddonUpdater.UpdateAttempt] provided as a parameter.
+ * @param updateAttempt the [AddonUpdater.UpdateAttempt] to be stored.
+ */
+ @WorkerThread
+ internal fun saveOrUpdate(updateAttempt: AddonUpdater.UpdateAttempt) {
+ database.updateAttemptDao().insertOrUpdate(updateAttempt.toEntity())
+ }
+
+ /**
+ * Finds the [AddonUpdater.UpdateAttempt] that match the [addonId] otherwise returns null.
+ * @param addonId the id to be used as filter in the search.
+ */
+ @WorkerThread
+ fun findUpdateAttemptBy(addonId: String): AddonUpdater.UpdateAttempt? {
+ return database
+ .updateAttemptDao()
+ .getUpdateAttemptFor(addonId)
+ ?.toUpdateAttempt()
+ }
+
+ /**
+ * Deletes the [AddonUpdater.UpdateAttempt] that match the [addonId] provided as a parameter.
+ * @param addonId the id of the [AddonUpdater.UpdateAttempt] to be deleted from the storage.
+ */
+ @WorkerThread
+ internal fun remove(addonId: String) {
+ database.updateAttemptDao().deleteUpdateAttempt(addonId)
+ }
+ }
+}
+
+/**
+ * A implementation which uses WorkManager APIs to perform addon updates.
+ */
+internal class AddonUpdaterWorker(
+ context: Context,
+ private val params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+ private val logger = Logger("AddonUpdaterWorker")
+ internal var updateAttemptStorage = DefaultAddonUpdater.UpdateAttemptStorage(applicationContext)
+
+ @VisibleForTesting
+ internal var attemptScope = CoroutineScope(Dispatchers.IO)
+
+ @Suppress("TooGenericExceptionCaught", "MaxLineLength")
+ override suspend fun doWork(): Result = withContext(Dispatchers.Main) {
+ val extensionId = params.inputData.getString(KEY_DATA_EXTENSIONS_ID) ?: ""
+ logger.info("Trying to update extension $extensionId")
+ // We need to guarantee that we are not trying to update without all the required state being initialized first.
+ WebExtensionSupport.awaitInitialization()
+
+ return@withContext suspendCoroutine { continuation ->
+ try {
+ val manager = GlobalAddonDependencyProvider.requireAddonManager()
+
+ manager.updateAddon(extensionId) { status ->
+ val result = when (status) {
+ AddonUpdater.Status.NotInstalled -> {
+ logger.error("Not installed extension with id $extensionId removing from the updating queue")
+ Result.failure()
+ }
+ AddonUpdater.Status.NoUpdateAvailable -> {
+ logger.info("There is no new updates for the $extensionId")
+ Result.success()
+ }
+ AddonUpdater.Status.SuccessfullyUpdated -> {
+ logger.info("Extension $extensionId SuccessFullyUpdated")
+ Result.success()
+ }
+ is AddonUpdater.Status.Error -> {
+ logger.error(
+ "Unable to update extension $extensionId, re-schedule ${status.message}",
+ status.exception,
+ )
+ retryIfRecoverable(status.exception)
+ }
+ }
+ saveUpdateAttempt(extensionId, status)
+ continuation.resume(result)
+ }
+ } catch (exception: Exception) {
+ logger.error(
+ "Unable to update extension $extensionId, re-schedule ${exception.message}",
+ exception,
+ )
+ saveUpdateAttempt(extensionId, AddonUpdater.Status.Error(exception.message ?: "", exception))
+ if (exception.shouldReport()) {
+ GlobalAddonDependencyProvider.onCrash?.invoke(exception)
+ }
+ continuation.resume(retryIfRecoverable(exception))
+ }
+ }
+ }
+
+ @VisibleForTesting
+ // We want to ensure, we are only retrying when the throwable isRecoverable,
+ // this could cause side effects as described on:
+ // https://github.com/mozilla-mobile/android-components/issues/8681
+ internal fun retryIfRecoverable(throwable: Throwable): Result {
+ return if (throwable is WebExtensionException && throwable.isRecoverable) {
+ Result.retry()
+ } else {
+ Result.success()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun saveUpdateAttempt(extensionId: String, status: AddonUpdater.Status) {
+ attemptScope.launch {
+ updateAttemptStorage.saveOrUpdate(AddonUpdater.UpdateAttempt(extensionId, Date(), status))
+ }
+ }
+
+ companion object {
+
+ @VisibleForTesting
+ internal const val KEY_DATA_EXTENSIONS_ID =
+ "mozilla.components.feature.addons.update.KEY_DATA_EXTENSIONS_ID"
+
+ @VisibleForTesting
+ internal fun createWorkerData(extensionId: String) = Data.Builder()
+ .putString(KEY_DATA_EXTENSIONS_ID, extensionId)
+ .build()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/GlobalAddonDependencyProvider.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/GlobalAddonDependencyProvider.kt
new file mode 100644
index 0000000000..0329f5434a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/GlobalAddonDependencyProvider.kt
@@ -0,0 +1,43 @@
+/* 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.addons.update
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.feature.addons.AddonManager
+
+/**
+ * Provides global access to the dependencies needed for updating add-ons.
+ */
+/** @suppress */
+object GlobalAddonDependencyProvider {
+ @VisibleForTesting
+ internal var addonManager: AddonManager? = null
+
+ @VisibleForTesting
+ internal var updater: AddonUpdater? = null
+
+ internal var onCrash: ((Throwable) -> Unit)? = null
+
+ /**
+ * Initializes the AddonManager, AddonUpdater and an optional onCrash lambda function.
+ */
+ fun initialize(manager: AddonManager, updater: AddonUpdater, onCrash: ((Throwable) -> Unit)? = null) {
+ this.addonManager = manager
+ this.updater = updater
+ this.onCrash = onCrash
+ }
+
+ internal fun requireAddonManager(): AddonManager {
+ return requireNotNull(addonManager) {
+ "initialize must be called before trying to access the AddonManager"
+ }
+ }
+
+ internal fun requireAddonUpdater(): AddonUpdater {
+ return requireNotNull(updater) {
+ "initialize must be called before trying to access the AddonUpdater"
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptDao.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptDao.kt
new file mode 100644
index 0000000000..f6007c171e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptDao.kt
@@ -0,0 +1,25 @@
+/* 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.addons.update.db
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+
+/**
+ * Internal dao for accessing and modifying add-on update requests in the database.
+ */
+@Dao
+internal interface UpdateAttemptDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertOrUpdate(entity: UpdateAttemptEntity): Long
+
+ @Query("SELECT * FROM update_attempts where addon_id =:addonId")
+ fun getUpdateAttemptFor(addonId: String): UpdateAttemptEntity?
+
+ @Query("DELETE FROM update_attempts where addon_id =:addonId")
+ fun deleteUpdateAttempt(addonId: String)
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptEntity.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptEntity.kt
new file mode 100644
index 0000000000..68669cee70
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptEntity.kt
@@ -0,0 +1,81 @@
+/* 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.addons.update.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.feature.addons.update.AddonUpdater
+import mozilla.components.feature.addons.update.AddonUpdater.Status.Error
+import mozilla.components.feature.addons.update.AddonUpdater.Status.NoUpdateAvailable
+import mozilla.components.feature.addons.update.AddonUpdater.Status.NotInstalled
+import mozilla.components.feature.addons.update.AddonUpdater.Status.SuccessfullyUpdated
+import mozilla.components.feature.addons.update.db.UpdateAttemptEntity.Companion.ERROR_DB
+import mozilla.components.feature.addons.update.db.UpdateAttemptEntity.Companion.NOT_INSTALLED_DB
+import mozilla.components.feature.addons.update.db.UpdateAttemptEntity.Companion.NO_UPDATE_AVAILABLE_DB
+import mozilla.components.feature.addons.update.db.UpdateAttemptEntity.Companion.SUCCESSFULLY_UPDATED_DB
+import java.util.Date
+
+/**
+ * Internal entity representing a [AddonUpdater.UpdateAttempt] as it gets saved to the database.
+ */
+@Entity(tableName = "update_attempts")
+internal data class UpdateAttemptEntity(
+ @PrimaryKey
+ @ColumnInfo(name = "addon_id")
+ var addonId: String,
+
+ @ColumnInfo(name = "date")
+ var date: Long,
+
+ @ColumnInfo(name = "status")
+ var status: Int,
+
+ @ColumnInfo(name = "error_message")
+ var errorMessage: String = "",
+
+ @ColumnInfo(name = "error_trace")
+ var errorTrace: String = "",
+) {
+ internal fun toUpdateAttempt(): AddonUpdater.UpdateAttempt {
+ return AddonUpdater.UpdateAttempt(addonId, Date(date), toStatus())
+ }
+
+ companion object {
+ const val NOT_INSTALLED_DB = 0
+ const val SUCCESSFULLY_UPDATED_DB = 1
+ const val NO_UPDATE_AVAILABLE_DB = 2
+ const val ERROR_DB = 3
+ }
+
+ internal fun toStatus(): AddonUpdater.Status? {
+ return when (status) {
+ NOT_INSTALLED_DB -> NotInstalled
+ SUCCESSFULLY_UPDATED_DB -> SuccessfullyUpdated
+ NO_UPDATE_AVAILABLE_DB -> NoUpdateAvailable
+ ERROR_DB -> Error(errorMessage, Exception(errorTrace))
+ else -> null
+ }
+ }
+}
+
+internal fun AddonUpdater.Status.toDB(): Int {
+ return when (this) {
+ NotInstalled -> NOT_INSTALLED_DB
+ SuccessfullyUpdated -> SUCCESSFULLY_UPDATED_DB
+ NoUpdateAvailable -> NO_UPDATE_AVAILABLE_DB
+ is Error -> ERROR_DB
+ }
+}
+
+internal fun AddonUpdater.UpdateAttempt.toEntity(): UpdateAttemptEntity {
+ var message = ""
+ var errorTrace = ""
+ if (status is Error) {
+ message = this.status.message
+ errorTrace = this.status.exception.stackTrace.first().toString()
+ }
+ return UpdateAttemptEntity(addonId, date.time, status?.toDB() ?: -1, message, errorTrace)
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptsDatabase.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptsDatabase.kt
new file mode 100644
index 0000000000..f028e8a16a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/update/db/UpdateAttemptsDatabase.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.addons.update.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+/**
+ * Internal database for saving site AddonUpdaterRequest.
+ */
+@Database(entities = [UpdateAttemptEntity::class], version = 1)
+internal abstract class UpdateAttemptsDatabase : RoomDatabase() {
+ abstract fun updateAttemptDao(): UpdateAttemptDao
+
+ companion object {
+ @Volatile
+ private var instance: UpdateAttemptsDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): UpdateAttemptsDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ UpdateAttemptsDatabase::class.java,
+ "addons_updater_attempts_database",
+ ).build().also { instance = it }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/worker/Extensions.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/worker/Extensions.kt
new file mode 100644
index 0000000000..d897b9af6f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/worker/Extensions.kt
@@ -0,0 +1,20 @@
+/* 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.addons.worker
+
+import kotlinx.coroutines.CancellationException
+import mozilla.components.concept.engine.webextension.WebExtensionException
+import java.io.IOException
+
+/**
+ * Indicates if an exception should be reported to the crash reporter.
+ */
+internal fun Exception.shouldReport(): Boolean {
+ val isRecoverable = (this as? WebExtensionException)?.isRecoverable ?: true
+ return cause !is IOException &&
+ cause !is CancellationException &&
+ this !is CancellationException &&
+ isRecoverable
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_footer_section_item.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_footer_section_item.xml
new file mode 100644
index 0000000000..1f76ce69d5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_footer_section_item.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:layout_marginStart="12dp"
+ android:layout_marginEnd="12dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="12dp"
+ android:text="@string/mozac_feature_addons_find_more_extensions_button_text"
+ android:textAlignment="center"
+ android:textAllCaps="false"
+ android:textSize="14sp" />
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_fragment_dialog_addon_installed.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_fragment_dialog_addon_installed.xml
new file mode 100644
index 0000000000..3c23c7a844
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_fragment_dialog_addon_installed.xml
@@ -0,0 +1,84 @@
+<!-- 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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:windowBackground"
+ android:orientation="vertical"
+ tools:ignore="Overdraw">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:importantForAccessibility="no"
+ android:scaleType="centerInside"
+ app:srcCompat="@drawable/mozac_ic_extension_24" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="11dp"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ tools:text="@string/mozac_feature_addons_installed_dialog_title"
+ tools:textColor="#000000" />
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title"
+ android:layout_alignStart="@id/title"
+ android:layout_marginTop="16dp"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="@string/mozac_feature_addons_installed_dialog_description_2" />
+
+ <androidx.appcompat.widget.AppCompatCheckBox
+ android:id="@+id/allow_in_private_browsing"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/description"
+ android:layout_alignStart="@id/title"
+ android:layout_marginTop="16dp"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:text="@string/mozac_feature_addons_settings_allow_in_private_browsing"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <Button
+ android:id="@+id/confirm_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/allow_in_private_browsing"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:paddingStart="20dp"
+ android:paddingEnd="20dp"
+ android:text="@string/mozac_feature_addons_installed_dialog_okay_button_2"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_fragment_dialog_addon_permissions.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_fragment_dialog_addon_permissions.xml
new file mode 100644
index 0000000000..efe8f2364b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_fragment_dialog_addon_permissions.xml
@@ -0,0 +1,93 @@
+<!-- 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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:windowBackground"
+ android:orientation="vertical"
+ tools:ignore="Overdraw">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ app:srcCompat="@drawable/mozac_ic_extension_24" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="11dp"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ tools:text="Add Ublock Origin?"
+ tools:textColor="#000000" />
+
+ <TextView
+ android:id="@+id/optional_or_required_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title"
+ android:layout_alignStart="@id/title"
+ android:layout_marginTop="16dp"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="It requires your permission to:" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/permissions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/optional_or_required_text"
+ android:layout_alignStart="@id/optional_or_required_text"
+ android:layout_marginStart="0dp"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:visibility="visible" />
+
+ <Button
+ android:id="@+id/deny_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/permissions"
+ android:layout_marginTop="16dp"
+ android:layout_toStartOf="@id/allow_button"
+ android:text="@string/mozac_feature_addons_permissions_dialog_cancel"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/allow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/permissions"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/mozac_feature_addons_permissions_dialog_add"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_header_section_item.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_header_section_item.xml
new file mode 100644
index 0000000000..28156a1860
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_header_section_item.xml
@@ -0,0 +1,52 @@
+<?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/. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:orientation="vertical"
+ tools:ignore="RtlSymmetry">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical">
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/warning_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:scaleType="center"
+ app:srcCompat="@drawable/mozac_ic_warning_fill_24"
+ app:tint="?android:attr/textColorLink"/>
+ <TextView
+ android:text="@string/mozac_feature_extensions_manager_notification_title_text"
+ android:textSize="16sp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textStyle="bold"
+ android:paddingStart="12dp"
+ android:paddingEnd="0dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+ <TextView
+ android:text="@string/mozac_feature_extensions_manager_notification_content_text"
+ android:textColor="?android:attr/textColorPrimary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="16dp"
+ android:paddingStart="36dp"/>
+ <Button
+ android:id="@+id/restart_button"
+ style="@style/Widget.AppCompat.Button.Borderless.Colored"
+ android:text="@string/mozac_feature_extensions_manager_notification_restart_button"
+ android:textColor="?android:attr/textColorLink"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:paddingVertical="8dp"/>
+</LinearLayout>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_item.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_item.xml
new file mode 100644
index 0000000000..9ed77d46fe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_item.xml
@@ -0,0 +1,156 @@
+<?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/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/add_on_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingStart="0dp"
+ android:paddingEnd="0dp">
+
+ <LinearLayout
+ android:id="@+id/add_on_content_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toStartOf="@+id/add_button"
+ android:orientation="horizontal">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/add_on_icon"
+ style="@style/Mozac.Widgets.Favicon"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="16dp"
+ android:importantForAccessibility="no"
+ app:srcCompat="@android:color/transparent"
+ tools:ignore="RequiredSize" />
+
+ <LinearLayout
+ android:id="@+id/details_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:orientation="vertical"
+ android:paddingStart="8dp"
+ android:paddingTop="8dp"
+ android:paddingEnd="4dp"
+ android:paddingBottom="8dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/add_on_name_container_margin_bottom"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/add_on_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textSize="16sp"
+ tools:text="uBlock Origin" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/allowed_in_private_browsing_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginStart="@dimen/allowed_in_private_browsing_label_margins"
+ android:layout_marginEnd="@dimen/allowed_in_private_browsing_label_margins"
+ android:layout_weight="0"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:visibility="gone" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/add_on_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ tools:text="An efficient blocker: easy on memory and CPU footprint, and yet can load and enforce thousands more filters than other popular blockers out there." />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:orientation="horizontal">
+
+ <RatingBar
+ android:id="@+id/rating"
+ style="@style/Widget.AppCompat.RatingBar.Small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:importantForAccessibility="no"
+ android:isIndicator="true"
+ android:numStars="5" />
+
+ <TextView
+ android:id="@+id/rating_accessibility"
+ android:layout_width="0dp"
+ android:layout_height="0dp" />
+
+ <TextView
+ android:id="@+id/review_count"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginStart="6dp"
+ android:textSize="12sp"
+ tools:text="Reviews: 591,642" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/add_on_status_error"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <TextView
+ android:id="@+id/add_on_status_error_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="@color/mozac_feature_addons_error_text_color"
+ android:textSize="14sp"
+ tools:text="@string/mozac_feature_addons_status_blocklisted" />
+
+ <TextView
+ android:id="@+id/add_on_status_error_learn_more_link"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/mozac_feature_addons_status_learn_more"
+ android:textColor="?android:attr/textColorLink"
+ android:textSize="14sp" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/add_button"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:scaleType="center"
+ android:layout_marginStart="0dp"
+ android:layout_marginEnd="6dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ app:srcCompat="@drawable/mozac_ic_plus_24"
+ app:tint="?android:attr/textColorPrimary" />
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_permission_item.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_permission_item.xml
new file mode 100644
index 0000000000..b5fe6cde18
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_permission_item.xml
@@ -0,0 +1,28 @@
+<?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/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/add_on_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/permission"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:textSize="16sp"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="Access your data for all websites" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/listDivider"
+ android:importantForAccessibility="no" />
+</LinearLayout>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_permissions_required_item.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_permissions_required_item.xml
new file mode 100644
index 0000000000..67f7de87db
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_permissions_required_item.xml
@@ -0,0 +1,28 @@
+<!-- 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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:orientation="horizontal"
+ android:paddingTop="8dp">
+
+ <TextView
+ android:id="@+id/bullet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="5dp"
+ android:text="•"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:ignore="HardcodedText,RtlSymmetry" />
+
+ <TextView
+ android:id="@+id/permission_required_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="Access your data for all websites." />
+
+</LinearLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_section_item.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_section_item.xml
new file mode 100644
index 0000000000..c976c66aa1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_section_item.xml
@@ -0,0 +1,30 @@
+<?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/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:visibility="gone"
+ android:background="?android:attr/listDivider" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingStart="72dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="16dp"
+ android:textStyle="bold"
+ tools:text="Recommended" />
+</LinearLayout>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_section_unsupported_section_item.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_section_unsupported_section_item.xml
new file mode 100644
index 0000000000..bb217ebe3f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_section_unsupported_section_item.xml
@@ -0,0 +1,60 @@
+<?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/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/unsupported_add_on_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/photonGrey30" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:orientation="horizontal">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/mozac_feature_addons_remove"
+ android:layout_gravity="center_vertical"
+ app:tint="@android:color/darker_gray"
+ app:srcCompat="@drawable/mozac_ic_cross_24"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:textSize="18sp"
+ tools:text="Not yet supported" />
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:text="2 add-ons"/>
+ </LinearLayout>
+ </LinearLayout>
+
+
+</LinearLayout>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_unsupported_item.xml b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_unsupported_item.xml
new file mode 100644
index 0000000000..e25f78a84f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/layout/mozac_feature_addons_unsupported_item.xml
@@ -0,0 +1,51 @@
+<?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/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:padding="16dp">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/add_on_icon"
+ style="@style/Mozac.Widgets.Favicon"
+ tools:ignore="RequiredSize"
+ android:importantForAccessibility="no"
+ app:srcCompat="@drawable/mozac_ic_extension_24"
+ app:tint="?android:attr/textColorPrimary" />
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/add_on_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toStartOf="@+id/add_on_remove_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textSize="18sp"
+ tools:text="uBlock Origin" />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/add_on_remove_button"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/mozac_feature_addons_remove"
+ app:tint="?android:attr/textColorPrimary"
+ app:srcCompat="@drawable/mozac_ic_delete_24" />
+ </RelativeLayout>
+</LinearLayout>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..3a9e04b773
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-am/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">የግላዊነት ቅንብሮችን ያንብቡ እና ይቀይሩ</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ለሁሉም ድር ጣቢያዎች የእርስዎን ውሂብ ይድረሱበት</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">የ%1$s ውሂብዎን ይድረሱበት</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">በ%1$s ጎራ ስር ባሉ ድረ-ገፆች ያለ ውሂብዎን ይድረሱበት</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">በሌላ 1 ድረ-ገፅ ላይ የእርስዎን ውሂብ ይድረሱበት</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">በ%1$d ሌሎች ድረ-ገፆች ላይ የእርስዎን ውሂብ ይድረሱበት</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">በሌላ 1 ጎራ ላይ የእርስዎን ውሂብ ይድረሱበት</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">በ%1$d ሌሎች ጎራዎች ላይ የእርስዎን ውሂብ ይድረሱበት</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s፣ %2$d ከ%3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">የአሳሽ ትሮችን ይድረሱ</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ያልተገደበ የተገልጋይ-ጎን ውሂብ ያከማቹ</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">በአሰሳ ጊዜ የአሳሽ እንቅስቃሴን ይድረሱ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">ዕልባቶችን ያንብቡ እና ይቀይሩ</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">የአሳሽ ቅንብሮችን ያንብቡ እና ይቀይሩ</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">የቅርብ ጊዜ የአሰሳ ታሪክን፣ ኩኪዎችን እና ተዛማጅ መረጃዎችን ያጽዱ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ከቅንጥብ ሰሌዳው ውሂብ ያግኙ</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ውሂብ ወደ ቅንጥብ ሰሌዳው ያስገቡ</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">በማንኛውም ገጽ ላይ ይዘትን አግድ</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">የአሰሳ ታሪክዎን ያንብቡ</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ፋይሎችን ያውርዱ እና የአሳሹን የማውረድ ታሪክ ያንብቡ እና ያሻሽሉ</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ወደ መሳሪያዎ የወረዱ ፋይሎችን ይክፈቱ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">የሁሉም ክፍት ትሮች ጽሑፍ ያንብቡ</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">አካባቢዎን ይድረሱበት</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">የአሰሳ ታሪክን ይድረሱ</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">የቅጥያ አጠቃቀምን ይቆጣጠሩ እና ገጽታዎችን ያስተዳድሩ</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ከሌላ መተግበሪያዎች ጋር መልዕክቶችን ይለዋወጡ</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">ማሳወቂያዎችን ለእርስዎ ያሳይ</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ክሪፕቶግራፊክ የማረጋገጫ አገልግሎቶችን ያቅርቡ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">የአሳሽ የእጅ አዙር ቅንብሮችን ይቆጣጠሩ</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">በቅርብ ጊዜ የተዘጉ ትሮችን ይድረሱ</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">የአሳሽ ትሮችን ደብቅ እና አሳይ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">የአሰሳ ታሪክን ይድረሱ</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">በክፍት ትሮች ውስጥ ውሂብዎን ለመድረስ የገንቢ መሳሪያዎችን ዘርጋ</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ስሪት</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">ደራሲ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">ደራሲያን</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">መጨረሻ የተሻሻለው</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">መነሻ ገጽ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ስለ ፈቃዶች ተጨማሪ ይወቁ</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">ደረጃ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">ስለዚህ ተጨማሪ የበለጠ መረጃ</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">ስለዚህ ቅጥያ የበለጠ መረጃ</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ቅንብሮች</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">በርቷል</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">ጠፍቷል</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">በግል አሰሳ ፍቀድ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">በግል አሰሳ ውስጥ አሂድ</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">በግል መስኮቶች ውስጥ አይፈቀድም</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ነቅቷል</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">ተሰናክሏል</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ተጭኗል</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">የሚመከር</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">እስካሁን አልተደገፈም</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">እስካሁን አልተገኘም</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ተሰናክሏል</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">ዝርዝር ማብራሪያዎች</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ፈቃዶች</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">አስወግድ</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">አሳውቅ</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s ይታከል?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ተጨማሪ ፈቃዶችን ይጠይቃል።</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">የሚከተለውን ለማድረግ የእርስዎን ፈቃድ ይፈልጋል፦</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ይፈልጋል፡-</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">አክል</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">ፍቀድ</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">ከልክል</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ተወው</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">ተጨማሪን ጫን</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$sን ጫን</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ተወው</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">ግምገማዎች፡- %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">ደረጃ፡ %1$.02f ከ 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ተጨማሪዎች</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ተጨማሪዎች አስተዳዳሪ</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎች ለጊዜው ተሰናክለዋል</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">ቅጥያዎች ለጊዜው ተሰናክለዋል</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">አንድ ወይም ከዚያ በላይ ተጨማሪዎች መስራት አቁመዋል፣ ይህም ስርዓትዎ ያልተረጋጋ እንዲሆን አድርጎታል።</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">አንድ ወይም ከዚያ በላይ ቅጥያዎች መስራት አቁመዋል፣ ይህም ስርዓትዎ ያልተረጋጋ እንዲሆን አድርጎታል።</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎችን እንደገና ያስጀምሩ</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">ቅጥያዎችን እንደገና ያስጀምሩ</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">የበለጠ ተጨማሪዎችን ያግኙ</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">ተጨማሪ ቅጥያዎችን ያግኙ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ፍቀድ</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ከልክል</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s አዲስ ዝማኔ አለው</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d አዲስ ፈቃዶች ያስፈልጋሉ</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">አዲስ ፈቃድ ያስፈልጋል</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪ ዝመናዎች</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">የቅጥያ ዝማኔዎች</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">የሚደገፉ ተጨማሪዎች አረጋጋጭ</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">አዲስ ተጨማሪ አለ</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">አዲስ ተጨማሪዎች አሉ</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$sን ወደ %2$s ያክሉ</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$sን እና %2$sን ወደ %3$s ያክሉ</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">ወደ %1$s ያክሏቸው</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">የፋየርፎክስ ተጨማሪ ቴክኖሎጂ በመዘመን ላይ ነው። እነዚህ ተጨማሪዎች ከፋየርፎክስ 75 እና ከዚያ በታች ጋር ተኳሃኝ ያልሆኑ ማዕቀፎችን ይጠቀማሉ።</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">በአሁኑ ጊዜ ለሚመከሩ ቅጥያዎች የመጀመሪያ ምርጫ ድጋፍን እየገነባን ነው።</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪን በማውረድ እና በማረጋገጥ ላይ…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">ቅጥያ በማውረድ እና በማረጋገጥ ላይ…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎችን መጠየቅ አልተሳካም!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">ቅጥያዎችን መጠየቅ አልተሳካም!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">ለአካባቢ %1$sም ሆነ ነባሪ ቋንቋ %2$s ፣ትርጉም አልተገኘም</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s በተሳካ ሁኔታ ተጭኗል</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$sን መጫን አልተሳካም</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">ይህን ተጨማሪ መጫን አልተሳካም።</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">ይህን ቅጥያ መጫን አልተሳካም።</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">በግንኙነት ችግር ምክንያት ይህ ተጨማሪ ሊወርድ አልቻለም።</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">በግንኙነት ችግር ምክንያት ይህ ቅጥያ ሊወርድ አልቻለም።</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">ይህ ተጨማሪ የተበላሸ መስሎ ስለታየ ሊጫን አልቻለም።</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">ይህ ቅጥያ የተበላሸ ስለሚመስል መጫን አልተቻለም።</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">ይህ ተጨማሪ ስላልተረጋገጠ ሊጫን አልቻለም።</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">ይህ ቅጥያ ስላልተረጋገጠ ሊጫን አልቻለም።</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s ከ%2$s %3$s ጋር ተኳሃኝ ስላልሆነ ሊጫን አልቻለም።</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s መረጋጋትን ወይም የደህንነት ችግሮችን የመፍጠር ዕድሉ ከፍተኛ ስለሆነ ሊጫን አልቻለም።</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s በተሳካ ሁኔታ ነቅቷል</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$sን ማንቃት አልተሳካም</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s በተሳካ ሁኔታ ተሰናክሏል</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$sን ማሰናከል አልተሳካም</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s በተሳካ ሁኔታ ተራግፏል</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$sን ማራገፍ አልተሳካም</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s በተሳካ ሁኔታ ተወግዷል</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$sን ማስወገድ አልተሳካም</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ይህ ተጨማሪ ከቀድሞው የ%1$s ስሪት የተሸጋገረ ነው</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 ተጨማሪ</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 ቅጥያ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s ተጨማሪዎች</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s ቅጥያዎች</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ተጨማሪ ይወቁ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">በተሳካ ሁኔታ ዘምኗል</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">ምንም ዝማኔ የለም</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ስሕተት</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">የዝማኔ መረጃ</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">የመጨረሻ ሙከራ፡-</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">ሁኔታ፡-</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ወደ %2$s ታክሏል</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">በምናሌው ውስጥ ይክፈቱት</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">ከ%2$s ምናሌ %1$sን ይድረሱ።</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">እሺ ገባኝ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">እሺ</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">ተጨማሪ ይወቁ</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s በደህንነት ወይም በመረጋጋት ችግሮች ምክንያት ተሰናክሏል።</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s ደህንነቱ የተጠበቀ እንደሆነ ሊረጋገጥ አልቻለም እና ተሰናክሏል።</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s ከእርስዎ የ%2$s (የ%3$s ስሪት) ጋር ተኳሃኝ አይደለም።</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..3fd52747b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-an/strings.xml
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leyer y modificar la configuración de privacidat</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a los tuyos datos de totz los puestos web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder a los datos de %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder a los datos web d\'o dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceder a los tuyos datos en unatro puesto</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceder a los tuyos datos en %1$d atros puestos</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceder a los tuyos datos en unatro dominio</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceder a los tuyos datos en %1$d atros dominios</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder a las pestanyas d’o navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Almagazenar una cantidat ilimitada de datos en o costau d’o client</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder a l’actividat d’o navegador entre la navegación</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leyer y modificar marcapachinas</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leyer y modificar achustes d’o navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpiar lo historial de navegación recient, cookies y datos relacionaus</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obtener datos d’o portafuellas</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Introducir datos en o portafuellas</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Descargar fichers y leyer y modificar lo historial de descargas d’o navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Ubrir fichers descargaus en o tuyo equipo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leyer lo texto de totas las pestanyas ubiertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a la tuya ubicación</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder a lo historial de navegación</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorizar l’uso d’extensions y administrar temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercambiar mensaches con atras aplicacions distintas</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Amostrar-te notificacions</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Proporcionar servicios d’autenticación criptografica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar la configuración proxy d’o navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder a las pestanyas zarradas recientment</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Amagar y amostrar pestanyas d’o navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder a lo historial de navegación</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Enamplar las ferramientas pa desembolicadors pa acceder a los tuyos datos en as pestanyas ubiertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Autors</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Zaguer actualización</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pachina d’inicio</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saber mas sobre permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Qualificación</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Achustes</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activau</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Desactivau</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir en navegación privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Executar en navegación privada</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activau</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Desactivau</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalau</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendau</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Encara no ye compatible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">No disponible encara</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desactivau</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalles</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Eliminar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Anyadir %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">S’ameneste lo tuyo permiso pa:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Anyadir</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Instalar complemento</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Avaluacions: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complementos</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denegar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tiene una nueva actualización</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Se requieren %1$d nuevos permisos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Se requiere un nuevo permiso</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Actualizacions de complementos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Comprobador de complementos compatibles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Nuevo complemento disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Nuevos complementos disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Anyadir %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Anyadir %1$s y %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Anyadir-los a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">La tecnolochía d’os complementos de Firefox se ye modernizando. Estes complementos usan frameworks que no son compatibles con Firefox 75 y posteriors.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">En este momento somos construyindo las bases pa una selección inicial d’extensions recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Descargando y verificando lo complemento…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Error en solicitar complementos!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalau correctament</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Fallo en instalar %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s activau correctament</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Fallo en activar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s desactivau correctament</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Fallo en desactivar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s desinstalau correctament</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Fallo en desinstalar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s eliminau correctament</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Fallo en eliminar %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Este complemento se migró dende una versión anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 complemento</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s complementos</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saber mas</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualizau correctament</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No i hai garra actualización disponible</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Información d’actualización</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Zaguer intento:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estau:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ha estau anyadiu a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Ubrir-lo en o menú</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Vale, entendiu</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..a21787cca7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ar/strings.xml
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">قراءة إعدادات الخصوصية وتعديلها</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">الوصول إلى بياناتك في المواقع كافة</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">الوصول إلى بياناتك لموقع %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">الوصول إلى بياناتك للمواقع في النطاق %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">الوصول إلى بياناتك في مواقع أخرى (1)</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">الوصول إلى بياناتك في مواقع أخرى (%1$d)</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">الوصول إلى بياناتك في نطاقات أخرى (1)</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">الوصول إلى بياناتك في نطاقات أخرى (%1$d)</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">الوصول إلى ألسنة المتصفح</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">تخزين بيانات غير محدودة في المتصفح</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">الوصول إلى نشاط المتصفح أثناء التنقل</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">قراءة العلامات وتعديلها</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">قراءة إعدادات المتصفح وتعديلها</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">مسح تأريخ التصفح الحديث و الكعكات و البيانات المتعلقة بها</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">جلب البيانات من الحافظة</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">إدخال البيانات في الحافظة</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">احجب المحتوى في أي صفحة</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">اقرأ تأريخ التصفح</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">تنزيل الملفات و قراءة تأريخ تنزيل المتصفح و تعديله</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">فتح الملفات المنزّلة على الجهاز</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">قراءة نصوص الألسنة المفتوحة</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">معرفة مكانك</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">الوصول إلى تأريخ التصفح</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">إدارة استخدام الامتدادات وإدارة السمات</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">تبادل الرسائل مع تطبيقات أخرى غير هذا</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">عرض التنبيهات</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">توفير خدمات استيثاق معمّاة</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">التحكم بإعدادات الوسيط للمتصفح</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">الوصول إلى الألسنة المغلقة حديثًا</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">إخفاء و إظهار ألسنة المتصفح</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">الوصول إلى تأريخ التصفح</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">توسيع أدوات المطورين للوصول إلى بياناتك في الألسنة المفتوحة</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">النسخة</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">المؤلّفون</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">آخر تحديث</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">الصفحة الرئيسية</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">اطّلع على المزيد عن التصاريح</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">التقييم</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">الإعدادات</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">مفعّلة</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">معطّلة</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">اسمح بها في التصفّح الخاص</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">تعمل في التصفّح الخاص</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">مفعّلة</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">معطّلة</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">منصّبة</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">موصى به</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">غير مدعومة بعد</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">غير متاحة بعد</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">معطّلة</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">التفاصيل</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">التصاريح</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">أزِل</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">أتريد إضافة %1$s؟</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">يحتاج الصلاحيات التالية:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">أضِف</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ألغِ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">تثبيت الإضافة</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ألغِ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">المراجعات: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/‏5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">الإضافات</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">مدير الإضافات</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">اسمح</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ارفض</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">تملك %1$s تحديثًا جديدًا</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">مطلوبة تصاريح جديدة (%1$d)</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">مطلوب تصريح جديد</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">تحديثات الإضافات</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">أداة فحص الإضافات المدعومة</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">تتوفّر إضافة جديدة</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">تتوفّر إضافات جديدة</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">أضِف %1$s إلى %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">أضِف %1$s و%2$s إلى %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">أضِفها إلى %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">تقنية الإضافات في Firefox تنتقل إلى الجيل الحديث. لم تعد هذه الإضافة متوافقة مع فيرفكس 75 فأعلى لاستعمالها أُطر عمل عتيقة.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">نعمل الآن على تقديم الدعم لمجموعة أولية منتقاة من الامتدادات المختارة.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">يُنزّل الإضافة ويتثبّت منها…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">فشل استعلام الإضافات!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">لم تُوجد الترجمة لا للمحلية %1$s ولا للغة المبدئية %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">نجح تثبيت %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">فشل تثبيت %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">نجح تفعيل %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">فشل تفعيل %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">نجح تعطيل %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">فشل تعطيل %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">نجحت إزالة %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">فشلت إزالة %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">نجحت إزالة %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">فشلت إزالة %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">رُحّلت هذه الإضافة من إصدارة سابقة من %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">إضافة واحدة</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s من الإضافات</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">اطّلع على المزيد</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">نجح التحديث</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">لا تحديثات متاحة</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">خطأ</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">معلومات المُحدّث</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">آخر محاولة:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">الحالة:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">أُضيفت %1$s إلى %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">افتح في القائمة</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">حسنًا، فهمت</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..0afc043b12
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ast/strings.xml
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lleer y modificar los axustes de la privacidá</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a los datos de tolos sitios web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder a los datos de %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder a los datos de sitios nel dominiu %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceder a los datos d\'otru sitiu más</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceder a los datos d\'otros %1$d sitios más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceder a los datos d\'otru dominiu más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceder a los datos d\'otros %1$d dominios más</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder a les llingüetes del restolador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Atroxar una cantidá infinita de datos del llau del veceru</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder a la actividá del restolador demientres navegues</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lleer y modificar los marcadores</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lleer y modificar los axustes del restolador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Llimpiar l\'historial recién, les cookies y los datos rellacionaos</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Consiguir datos del cartafueyu</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Introducir datos nel cartafueyu</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquiar el conteníu de cualesquier páxina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lleer l\'historial de restolar</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Baxar ficheros y lleer y modificar l\'historial de descargues del restolador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir los ficheros baxaos al preséu</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lleer el testu de toles llingüetes abiertes</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a la llocalización</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder al historial de restolar</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Supervisar l\'usu d\'estensiones y xestionar estilos</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercambiar mensaxes con otres aplicaciones aparte d\'esta</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Amosate avisos</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Fornir servicios d\'autenticación criptográfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar los axustes del proxy del restolador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder a les llingüetes zarraes apocayá</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Anubrir y amosar les llingüetes del restolador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder al historial de restolar</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Permitir que les ferramientes pa desendolcadores accedan a los datos de les llingüetes abiertes</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">L\'últimu anovamientu</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Páxina d\'aniciu</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saber más tocante a los permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Valoración</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Axustes</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activóse</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Desactivóse</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir nel mou de restolar en privao</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Executar nel mou de restolar en privao</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activóse</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Desactivóse</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalóse</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Aconséyase</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Entá nun ye compatible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Entá nun ta disponible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desactivóse</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalles</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Quitar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿Quies amestar «%1$s»?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Rique\'l to permisu pa:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Amestar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Encaboxar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Instalar el complementu</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Encaboxar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Reseñes: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Xestor de complementos</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Negar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">«%1$s» tien un anovamientu</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Ríquense %1$d permisos nuevos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Ríquese un permisu nuevu</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Anovamientos de complementos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Comprobador de complementos compatibles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Hai un complementu nuevu disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Hai complementos nuevos disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Amestar «%1$s» a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Amestar «%1$s» y «%2$s» a «%3$s»</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Amiéstalos a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">La teunoloxía de complementos pa Firefox ta modernizándose. Estos complementos usen frameworks que nun son compatibles a partir de Firefox 75.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Tamos trabayando pa ufrir una seleición inicial d\'estensiones aconseyaes.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Baxando y verificando\'l complementu…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">¡Hebo un fallu al solicitar los complementos!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Nun s\'atopó la traducción de la locale «%1$s» nin de la llingua predeterminada «%2$s»</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">«%1$s» instalóse correutamente</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Hebo un fallu al instalar «%1$s»</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">«%1$s» activóse correutamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Hebo un fallu al activar «%1$s»</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">«%1$s» desactivóse correutamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Hebo un fallu al desactivar «%1$s»</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">«%1$s» desinstalóse correutamente</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Hebo un fallu al desinstalar «%1$s»</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">«%1$s» quitóse correutamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Hebo un fallu al quitar «%1$s»</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Esti complementu migró dende una versión anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 complementu</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s complementos</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saber más</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Anovóse correutamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nun hai nengún anovamientu disponible</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Información del anovador</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Últimu intentu:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estáu:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">«%1$s» amestóse a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Ábrilu nel menú</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Entendílo</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..5d02709717
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-az/strings.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Məxfilik tənzimləmələrini oxumaq və dəyişdirmək</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Bütün saytlar üçün olan məlumatlarınıza baxmaq</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Səyyah vərəqlərinə baxmaq</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Alıcı tərəfində limitsiz məlumat saxlamaq</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Naviqasiya vaxtı səyyah aktivliyinə baxmaq</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Əlfəcinləri oxumaq və dəyişdirmək</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Səyyah tənzimləmələrini oxumaq və dəyişdirmək</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Son səyahət tarixçəsini, çərəzləri və bağlı məlumatları təmizləmək</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Məlumatı mübadilə buferindən almaq</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Məlumatı mübadilə buferinə daxil etmək</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Faylları endirmək və səyyahın endirmə tarixçəsini oxumaq və dəyişdirmək</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Cihazınıza endirilmiş faylları açmaq</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Bütün açıq vərəqlərin yazılarını oxumaq</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Məkanınızı öyrənmək</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Səyahət tarixçəsinə baxmaq</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Uzantı istifadəsini izləmək və mövzuları idarə etmək</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Bu tətbiq xaricindəkilər ilə mesaj alış-verişi etmək</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Bildirişləri sizə göstərmək</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kriptoqrafik təsdiqləmə xidmətləri təmin etmək</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Səyyah proksi tənzimləmələrini idarə etmək</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Son qapatılan vərəqlərə baxmaq</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Səyyah vərəqlərini gizlətmək və göstərmək</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Səyahət tarixçəsinə baxmaq</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Açıq vərəqlərdəki məlumatlarınız üçün tərtibatçı alətlərini genişlətmək</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Buraxılış</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Müəlliflər</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Son yenilənmə</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Ana səhifə</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">İcazələr haqqında ətraflı öyrənin</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Reytinq</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Tənzimləmələr</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Açıqdır</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Qapalıdır</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Məxfi səyahətdə icazə ver</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Məxfi səyahətdə başlat</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Aktivdir</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Söndürülüb</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Qurulu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Məsləhətli</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Hələlik dəstəklənməyən</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Hələlik mövcud deyil</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Sönülü</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Təfərrüatlar</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">İcazələr</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Sil</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s əlavə edilsin?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Bu icazələri verməyinizi istəyir:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Əlavə et</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Ləğv et</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Əlavəni quraşdır</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Ləğv et</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Əlavələr</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">İcazə ver</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Rədd et</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s üçün yeniləmə var</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d yeni icazə tələb edilir</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Yeni icazə tələb edilir</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Əlavə yeniləmələri</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Dəstəklənən əlavələr yoxlayıcısı</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Yeni əlavə mövcuddur</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Yeni əlavələr mövcuddur</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s əlavəsini %2$s səyyahına əlavə et</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s və %2$s əlavələrini %3$s səyyahına əlavə et</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Onları %1$s səyyahına əlavə et</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s uğurla quraşdırıldı</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s uğurla aktivləşdirildi</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s uğurla söndürüldü</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s uğurla silindi</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s uğurla silindi</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 əlavə</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s əlavə</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Ətraflı öyrən</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">uğurla yeniləndi</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Yeniləmə mövcud deyil</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Xəta</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Vəziyyət:</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Tamam, başa düşdüm</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..42046db546
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-azb/strings.xml
@@ -0,0 +1,267 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">گیزلیلیک تنظیم‌لرینی اوخویون و ایصلاح ائله‌یین</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">بوتون وبسایتلار دیتالاریزا ال چاتما</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s دیتالاریزا ال چاتما</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s دامنه‌سینده‌کی سایتلار اوچون دیتالاریزا ال چاتما</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">دیگر 1 سایتدا دیتالاریزا ال چاتما</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">دیگر %1$d سایتداکی دیتالاریزا ال چاتما</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">باشقا 1 دامنه‌ده دیتالاریزا ال چاتما</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">باشقا %1$d دامنه‌‌لرده دیتالاریزا ال چاتما</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%3$d -دن %2$d و %1$s</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">موروچو تاغلارینا ال چاتما</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ایستمجی طرفینده سینیرسیز میقداردا دیتا ساخلایین</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">گزینمه زامانی موروچو فعالیتینه ال چاتما</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">بوکمارک‌لاری اوخوما و ایصلاح ائتمه</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">مورورچو تنظیم‌لرینی اوخویون و ایصلاح ائله‌یین</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">یاخین زامانداکی گؤزآتما گئچمیشینی، کوکی‌لری و ایلگیلی دیتالاری پوزون</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">کلیپ‌ بوردان دیتا آل</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">کلیپ بوردا دیتا گیر</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">هر صفحه‌ده موحتوانی مسدود ائله</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">گؤز آتما گئچمیشینی اوخویون</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">فایل‌لاری یئندیرین و مورورچو گئچمیشینی اوخویوب ایصلاح ائله‌یین</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">جاهازیزدا ایندیریلمیش فایللاری آچین</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">بوتون آچیق تاغلارین متنینی اوخویون.</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">قونوموزا ال چاتما</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">مورور گئچمیشینه ال چاتما</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">اوزانتی ایشه آلماسینی ایزله‌یین و تم‌لری ایداره ائله‌یین</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ساولاری بوندان باشقا اپ‌لره آلین وئرین.</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">سیزه بیلدیریش‌لری گؤسترمک</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">رمزلشدیرمه کیملیک دوغورلاما خیدمت‌لرین ایرائه‌سی</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">مورورچو پروکسی تنظیم‌لرینی کونترول ائدین</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">یاخین باغلانمیش تاغلارا ال چاتین</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">مورورچو تاغلارینی گیزله و گؤستر</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">مورور گئچمیشینه ال چاتما</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">آچیق تاغلاردا بیلگی‌لریزه ال چاتماق اوچون گلیشدیریجی آلت‌لرینی گئنیشله‌دین.</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">یازیم</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">یازیجی</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">یازیجی‌لار</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">سون گونجلله‌مه</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">آنایارپاق</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ایجازه‌لر حاقیندا داها آرتیق بیلین</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">دگرلندیرمه</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">بو تاخیلان حاقیندا آرتیقراق بیلگی</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">بو اوزانتی اوچون داها چوخ بیلگی</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">تنظیم‌‎لر</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">آچیق</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">باغلی</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">اؤزل مورورچویا ایجازه وئر</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">اؤزل موروردا چالیشدیر</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">گیزلی پنجره‌لرده ایجازه وئریلمیر</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">گوجلنمیش</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">گوجدن سالینمیش</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">قورولدو</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">توصیه اولموش</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">هله‌لیک آرخالانمیر</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">هله‌لیک موجود دئییل</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">گوجدن سالینمیش</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">آیرینتی‌لار</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ایجازه‌لر</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">قالدیر</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">راپورت</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s اکلنسین؟</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s داها آرتیق ایجازه‌لر ایستییر.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">بونون اوچون سیزین ایجازه‌ز لازیم:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ایسته‌نن‌لر:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">اکله</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">ایجازه</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">رد </string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">لغو</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">تاخیلانی قور</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s ـنی قور</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">لغو</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">نظرلر: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">تاخیلان‌لار</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">تاخیلان مودیریتی</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان‌لار گئچیجی اولاراق گوجدن سالینمیش.</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">اوزانتی‌لار گئچیجی اوْلاراق گوُجدن سالینمیش.</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">بیر ویا نئچه تاخیلان دایاندیریلدی، بودا سیستمیزی ثابیتسیز ائله‌دی.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان‌لاری یئنی‌دن باشلات</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">داها آرتیق تاخیلان آختار</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ایجازه</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">رد </string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s -ین یئنی گونجلله‌مه‌سی وار</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d یئنی ایجازه‌لر لازیمدیر</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">بیر یئنی ایجازه لازیمدیر</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان گونجلله‌مه‌لری</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">آرخالانمیش تاخیلان‌لارین چک ائلیه‌نی</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">یئنی تاخیلان الده وار</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">یئنی تاخیلان‌لار الده وار</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s -یه %1$s اکله‌یین</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s و %2$s\'یی %3$s\'ا اکله‌یین</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">بونلاری %1$s\'ا اکله‌ییه</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">فایرفاکس‌‍ین تاخیلان تکنولوژیسی مدرنلشیر. بو تاخیلان‌لار فایرفاکس ۷۵ و سونراکی یازیملارلا توتوشمایان چرچیوه‌لردن ایستیفاده ائدیر.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ایندی توصیه اولونان اوزانتی‌لارین ایلک سئچیمی اوچون ساپورت وئریریق.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان یئندیریلیر و دوغرولانیر…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان‌لاری سورغولاماق آلینمادی.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">یئرلی %1$s اوچون و %2$s وارساییلان دیل اوچون ترجومه تاپیلمادی. </string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">باشاریلی قورولدو %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s قورماسی قیریلدی</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">بو تاخیلانین قورماسی قیریلدی</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">باغلانتی خطاسی سببینه گؤره بو تاخیلان یئندیری‌لنمه‌دی.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">بو تاخیلانی قورماق مومکون اولمادی، چونکی خاراب گؤرونور.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">بو تاخیلانی قورماق مومکون اولمادی، چونکی دوغرولانمیب.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s قورولا بیلمه‌دی، چونکی او، %2$s %3$s ایله اویغون دئییل.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s قورماق اولمادی، چونکی اونون ثابیتلیک و یا امنیت موشکول‌لرینه سبب اولماسینین ریسکی چوخدور.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s باشاریلی گوجلندی.</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s گوجلندیرمه‌سی قیریلدی</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s باشاریلی گوجدن سالیندی.</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s گوجدن سالماسی قیریلدی</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s باشاریلی قالدیریلدی</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s ایشدن سالماسی قیریلدی</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s باشاریلی قالدیریلدی</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s قالدیریلماسی قیریلدی</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">بو تاخیلان %1$s -ین اؤنجه‌کی بیر یازیمیندان داشیندی.</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s تاخیلان</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">آرتیق بیلین</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">باشاریلی گونجللندی.</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">هئچ گونجللمه یوخ</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">خطا</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">گونجل‌لییجی بیلگی‌لری</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">سون چالیشما:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">دوروم:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s، %2$s لیستینه اکلندی.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">منودا آچین</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">تامام، باشا دوشدوم</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">آرتیق بیلین</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">امنیت ویا ثابیتلیک موشکول‌لری سببی ایله %1$s گوجدن سالیندی.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s -نین گوونلی اولدوغو دوغورلانمادی و گوجدن سالیندی.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s سیزین %2$s یازیمیزا (%3$s یازیمی) اویغون دئییل.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..30c37d9ad9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-be/strings.xml
@@ -0,0 +1,271 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Прагляд і рэдагаванне налад прыватнасці</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Доступ да вашых звестак для ўсіх вэб-сайтаў</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Доступ да вашых дадзеных для %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Доступ да звестак для сайтаў у дамене %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Доступ да вашых дадзеных на 1 іншым сайце</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Доступ да вашых дадзеных на %1$d іншых сайтах</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Доступ да вашых дадзеных у 1 іншым дамене</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Доступ да вашых дадзеных у %1$d іншых даменах</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d з %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Доступ да картак браўзера</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Захоўваць неабмежаваную колькасць кліенцкіх дадзеных</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Доступ да дзеянняў браўзера ў час навігацыі</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Прагляд і рэдагаванне закладак</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Прагляд і рэдагаванне налад браўзера</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Выдаленне нядаўняй гісторыі аглядання, кукаў і звязаных з імі дадзеных</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Атрымліваць дадзеныя з буфера абмену</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Змяшчаць дадзеныя ў буфер абмену</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Блакаваць змесціва на любой старонцы</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Бачыць вашу гісторыю аглядання</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Сцягванне файлаў, прагляд і змяненне гісторыі сцягванняў браўзера</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Адкрыць файлы, сцягнутыя на вашу прыладу</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Чытаць тэкст усіх адкрытых картак</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Доступ да вашага месцазнаходжання</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Доступ да гісторыі аглядання</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Маніторынг выкарыстання пашырэнняў і кіраванне тэмамі</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Абмен паведамленнямі з іншымі праграмамі</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Паказваць вам абвесткі</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Забеспячэнне паслуг крыптаграфічнай аўтэнтыфікацыі</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Кіраванне наладамі проксі браўзера</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Доступ да нядаўна закрытых картак</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Хаванне і паказ картак браўзера</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Доступ да гісторыі аглядання</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Дазвол інструментам распрацоўшчыка атрымліваць доступ да вашых дадзеных у адкрытых картках</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Версія</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Аўтар</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Аўтары</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Апошняе абнаўленне</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Хатняя старонка</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Даведацца больш пра дазволы</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Ацэнка</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Падрабязней аб гэтым дадатку</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Налады</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Укл.</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Выкл.</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Дазволена ў прыватным агляданні</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Запуск у прыватным агляданні</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Не дазволена ў прыватных вокнах</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Уключаны</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Выключаны</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Усталявана</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Рэкамендуюцца</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Пакуль не падтрымліваецца</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Яшчэ не даступны</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Выключана</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Падрабязнасці</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Дазволы</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Выдаліць</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Паведаміць</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Дадаць %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s запытвае дадатковыя дазволы.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Гэта патрабуе вашага дазволу на:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Ён хоча атрымаць дазвол на:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Дадаць</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Дазволіць</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Адмовіць</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Адмяніць</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Усталяваць дадатак</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Усталяваць %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Адмяніць</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Водгукі: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Дадаткі</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Менеджар дадаткаў</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Дадаткі часова адключаны</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Адзін або некалькі дадатковых кампанентаў перасталі працаваць, што зрабіла вашу сістэму нестабільнай.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Перазапусціць дадаткі</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Знайсці больш дадаткаў</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Знайсці іншыя пашырэнні</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Дазволіць</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Адмовіць</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s мае абнаўленне</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d патрэбныя новыя дазволы</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Патрэбен новы дазвол</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Абнаўленні дадаткаў</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Абнаўленні пашырэнняў</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Пошук дададкаў, якія падтрымліваюцца.</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Даступны новы дадатак</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Даступныя новыя дадаткі</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Дадаць %1$s да %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Дадаць %1$s ды %2$s да %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Дадаць іх да %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Тэхналогія дадаткаў Firefox мадэрнізуецца. Гэтыя дадаткі выкарыстоўваюць фрэймворкі, якія не сумяшчальныя з Firefox версіі 75 і вышэй.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Зараз мы ствараем падтрымку для пачатковага набору рэкамендаваных пашырэнняў.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Спампоўка і праверка дадатку…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Не атрымалася запытацца на дадаткі</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Пераклад не знойдзены, ні для лакалі %1$s, ні для прадвызначанай мовы %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s паспяхова ўсталяваны</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Не атрымалася ўсталяваць %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Не ўдалося ўсталяваць гэты дадатак.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Немагчыма сцягнуць гэты дадатак, бо злучэнне не ўдалося.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Гэты дадатак не можа быць усталяваны, бо ён выглядае пашкоджаным.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Гэты дадатак не можа быць усталяваны, бо ён не правераны.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s не можа быць усталяваны, бо ён несумяшчальны з %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s не можа быць усталяваны, бо ёсць вялікая рызыка, што ён выкліча праблемы ўстойлівасці або бяспекі.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Паспяхова ўключана %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Не атрымалася ўключыць %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s паспяхова адключаны</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Не атрымалася адключыць %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s паспяхова выдалены</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Не атрымалася дэінсталяваць %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s паспяхова выдалены</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Не атрымалася выдаліць %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Гэты дадатак перанесены з папярэдняй версіі %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 дадатак</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 пашырэнне</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">Дадаткаў: %1$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Даведацца больш</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Паспяхова абноўлена</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Няма даступных абнаўленняў</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Памылка</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Інфармацыя аб абнаўленні</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Апошняя спроба:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Статус:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s было дададзена ў %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Адкрыць яго ў меню</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Добра, зразумела</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Падрабязней</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s адключаны, бо ёсць праблемы сумяшчальнасці або ўстойлівасці.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s не можа быць правераны на бяспеку і быў адключаны.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s несумяшчальны з вашай версіяй %2$s (версія %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..b15d57e551
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-bg/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Четене и промяна на настройките за поверителност</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Достъп до вашите данни от всички страници</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Достъп до вашите данни за %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Достъп до вашите данни за страници от домейна %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Достъп до вашите данни от 1 друга страница</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Достъп до вашите данни от %1$d други страници</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Достъп до вашите данни от 1 друг домейн</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Достъп до вашите данни от %1$d други домейни</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d от %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Достъп до разделите</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Съхранява неограничено данни при клиента</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Достъп до действията на четеца по време на разглеждане</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Четене и промяна на отметки</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Четене и промяна на настройки на четеца</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Изчистване на история, бисквитки и свързани данни</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Взимане данни от системния буфер</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Поставяне на данни в системния буфер</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Спиране на съдържание на всяка страница</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Четене на историята на разглеждане</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Изтегляне на файлове, четене и промяна историята на изтеглянията</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Отваря изтеглените на устройството файлове</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Достъп до текста от всички отворени раздели</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Достъп до местоположението</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Достъп до историята на разглеждане</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Следене използването на разширения и управлението на теми</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Обменяне на съобщения с приложения различни от това</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Показване на известия</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Предоставяне на шифровани услуги за упълномощаване</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Управление на настройките на мрежовия посредник</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Достъп до наскоро затворените раздели</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Скриване и показване на разделите на четеца</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Достъп до историята на разглеждане</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Разширяване на развойните инструменти с достъп до данните ви от отворените раздели</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Издание</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Автор</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Автори</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Последно обновяване</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Страница</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Научете повече за правата</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Оценка</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Повече за добавката</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Повече за разширението</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Настройки</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Включена</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Изключена</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Разрешаване в поверителни раздели</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Работи в поверителни раздели</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Не е разрешено в поверителни прозорци</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Включени</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Изключени</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Инсталирани</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Препоръчани</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Все още не се поддържа</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Все още недостъпно</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Изключени</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Подробности</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Права</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Премахване</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Докладване</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Добавяне на „%1$s“?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">Добавката „%1$s“ иска допълнителни права.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Добавката иска следните права:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Иска права за:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Добавяне</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Разрешаване</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Забраняване</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Отказ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Инсталиране на добавка</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Инсталиране на %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Отказ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Отзиви: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Оценка: %1$.02f от 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Добавки</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Управление на добавки</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Добавките са временно изключени</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Разширенията са временно деактивирани</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Една или повече добавки са спрели да работят, като намаляват стабилността на системата.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Едно или повече разширения са спрели да работят, правейки системата ви нестабилна.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Рестарт на добавките</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Рестартиране на разширенията</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Намерете още добавки</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Още разширения</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Разрешаване</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Забраняване</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">„%1$s“ има обновяване</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Необходими са %1$d нови права</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Необходими са нови права</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Обновявания на добавки</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Обновяване на разширението</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Проверка за съвместими добавки</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Налична е нова добавка</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Налични са нови добавки</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Добавяне на %1$s към %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Добавяне на %1$s и %2$s към %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Добавяне към %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Технологията на Firefox за добавки се модернизира. Тези добавки използват софтуерни рамки, които са несъвместими с Firefox издание 75 и по-ново.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">В момента изграждаме поддръжка на първоначалния набор от препоръчителни разширения.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Изтегляне и проверка на добавката …</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Изтегляне и проверка на разширението…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Неуспешно запитване на добавки!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Неуспешна заявка за разширения!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Не е намерен превод за локализация %1$s, нито език по подразбиране %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Добавката „%1$s“ е инсталирана</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Добавката „%1$s“ не е инсталирана</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Добавката не е инсталирана.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Инсталирането на разширението не бе успешно.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Добавката не може да бъде изтеглена поради неуспешно установена връзка.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Разширението не може да бъде изтеглено поради неуспешна връзка.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Добавката не може да бъде инсталирана, защото изглежда е повредена.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Разширението не може да бъде инсталирано, защото изглежда е повредено.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Добавката не може да бъде инсталирана, защото не е проверена.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Това разширение не може да бъде инсталирано, защото не е проверено.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Добавката %1$s не може да бъде инсталирана, защото е несъвместима с %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Добавката %1$s не може да бъде инсталирана, защото носи висок риск от причиняване на проблеми със стабилността или сигурността.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Добавката „%1$s“ е включена</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Добавката „%1$s“ не е включена</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Добавката „%1$s“ е изключена</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Добавката „%1$s“ не е изключена</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Добавката „%1$s“ е премахната</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Добавката „%1$s“ не е премахната</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Добавката „%1$s“ е премахната</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Добавката „%1$s“ не е премахната</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Добавката е мигрирана от предишно издание на %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 добавка</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 разширение</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s добавки</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s разширения</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Научете повече</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Успешно обновена</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Няма обновяване</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Грешка</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Информация от обновяване</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Последно обновяване:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Състояние:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Добавката „%1$s“ е добавена към %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Отворете я от менюто</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Достъп до %1$s от менюто на %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Да, разбрах</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Добре</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Научете повече</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Добавката %1$s е изключена поради съображения за сигурност или стабилност.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Добавката %1$s не може да бъде проверена и е изключена.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Добавката %1$s е несъвместима с изданието на %2$s (издание %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..6d1edc2c2e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-bn/strings.xml
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">পড়ুন এবং গোপনীয়তা সেটিংস পরিবর্তন করুন</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">সকল ওয়েবসাইটের জন্য আপনার তথ্য ব্যবহার করুন</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s এর জন্য আপনার তথ্য ব্যাবহার করুন</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s ডোমেইনে থাকা সাইটের জন্য আপনার তথ্য ব্যবহার করুন</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">আরো ১টি সাইটে তথ্য ব্যবহারের অনুমতি দিন</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">আরো %1$dটি সাইটে তথ্য ব্যবহারের অনুমতি দিন</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">আরো ১টি ডোমেইনে তথ্য ব্যবহারের অনুমতি দিন</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">আরো %1$dটি ডোমেইন তথ্য ব্যবহারের অনুমতি দিন</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ব্রাউজারের ট্যাবগুলো ব্যবহার করুন</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">সীমাহীন পরিমাণের ক্লায়েন্ট-সাইডের তথ্য সঞ্চয় করুন</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">নেভিগেশনের সময়ে ব্রাউজিং কার্যকলাপে প্রবেশ করুন</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">বুকমার্কগুলি পড়ুন এবং পরিবর্তন করুন</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ব্রাউজারের সেটিংস পড়ুন এবং পরিবর্তন করুন</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">সাম্প্রতিক ব্রাউজিং ইতিহাস, কুকিজ, এবং এই সম্পর্কিত তথ্য মুছে ফেলুন</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ক্লিপবোর্ড থেকে তথ্য নিন</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ক্লিপবোর্ডে তথ্য দিন</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ফাইলগুলি ডাউনলোড করুন এবং ব্রাউজারের ডাউনলোডের ইতিহাস পড়ুন এবং সংশোধন করুন</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">আপনার ডিভাইসে ডাউনলোড হওয়া ফাইলগুলো খুলুন</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">খুলে থাকা সবগুলো ট্যাবের লেখা পড়ুন</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">আপনার অবস্থান ব্যবহার করুন</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ব্রাউজিং ইতিহাসে প্রবেশ করুন</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">এক্সটেনশনের ব্যবহার পর্যবেক্ষণ এবং থিম পরিচালনা করুন</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">এর মতো অন্য অ্যাপ্লিকেশনগুলোর মধ্যে বার্তা বিনিময় করুন</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">আপনাকে নোটিফিকেশন দেখাবে</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ক্রিপ্টোগ্রাফিক প্রমাণীকরণ পরিষেবা সরবরাহ করুন</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ব্রাউজারের প্রক্সি সেটিংস নিয়ন্ত্রণ করুন</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">সম্প্রতি বন্ধ করা ট্যাব ব্যবহার করুন</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ব্রাউজারের ট্যাবগুলি লুকান এবং দেখুন</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ব্রাউজিং ইতিহাসে প্রবেশ করুন</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">খোলা ট্যাবগুলোতে আপনার ডাটা ব্যবহার করতে ডেভেলপার টুলগুলো সম্প্রসারিত করুন</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">সংস্করণ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">লেখকরা</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">সর্বশেষ হালনাগাদকৃত</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">হোমপেজ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">অনুমতি সম্পর্কে আরো জানুন</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">রেটিং</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">সেটিংস</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">চালু</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">বন্ধ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">ব্যক্তিগত ব্রাউজিংয়ে অনুমতি দিন</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">ব্যক্তিগত ব্রাউজিংয়ে চালু করুন</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">সক্রিয় হয়েছে</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">নিষ্ক্রিয় হয়েছে</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ইনস্টল করা হয়েছে</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">সুপারিশকৃত</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">এখনো সমর্থিত নয়</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">এখনো বিদ্যমান না</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">নিষ্ক্রিয় করা হয়েছে</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">বিস্তারিত</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">অনুমতিসমূহ</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">মুছে ফেলুন</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s যোগ করবেন?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">এটিতে আপনার অনুমতি দরকার:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">যোগ করুন</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">বাতিল</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">অ্যাড-অন ইনস্টল করুন</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">বাতিল</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">পর্যালোচনা: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">অ্যাড-অনগুলো</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">অ্যাড-অন ব্যবস্থাপক</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">অনুমতি দিন</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">প্রত্যাখান করুন</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s এর একটি নতুন আপডেট রয়েছে</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d নতুন অনুমতির প্রয়োজন</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">একটি নতুন অনুমতির প্রয়োজন</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">অ্যাড-অনের আপডেট</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">অ্যাড-অন পরীক্ষক সমর্থিত</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">নতুন অ্যাড-অন বিদ্যমান</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">নতুন অ্যাড-অন বিদ্যমান</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s কে %2$s এ যুক্ত করুন</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s এবং %2$s কে %3$s এ যুক্ত করুন</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">এগুলোকে %1$s এ যুক্ত করুন</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox অ্যাড-অন প্রযুক্তি আধুনিকীকরণ করছে। এই অ্যাড-অনগুলি এমন ফ্রেমওয়ার্কগুলি ব্যবহার করে যা Firefox 75 এবং এর বাইরে উপযুক্ত নয়।</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">আমরা বর্তমানে প্রস্তাবিত এক্সটেনশনসমূহের প্রাথমিক বাছাই এর জন্য প্রয়োজনীয় সাপোর্ট নির্মাণ করছি।</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">অ্যাড-অন ডাউনলোড এবং যাচাই করা হচ্ছে …</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">অ্যাড-অন তালিকা পেতে ব্যর্থ!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">লোকেল %1$s কিংবা নির্ধারিত ভাষা %2$s, কোনোটির জন্যই অনুবাদ খুঁজে পাওয়া যায়নি</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">সফলভাবে %1$s ইনস্টল করা হয়েছে</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ইনস্টল করতে ব্যর্থ</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">সফলভাবে %1$s সক্রিয় করা হয়েছে</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s সক্রিয় করতে ব্যর্থ হয়েছে</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">সফলভাবে %1$s নিষ্ক্রিয় করা হয়েছে</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s নিষ্ক্রিয় করতে ব্যর্থ</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s সাফলভাবে আনইনস্টল করা হয়েছে</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s আনইনস্টল করতে ব্যর্থ</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">সফলভাবে %1$s অপসারিত হয়েছে</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s অপসারণ করতে ব্যর্থ</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">এই অ্যাড-অনটি %1$s এর পূর্ববর্তী সংস্করণ থেকে স্থানান্তরিত হয়েছিল</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 অ্যাড-অন</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s অ্যাড-অনগুলো</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">আরো জানুন</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">সফলভাবে আপডেট হয়েছে</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">কোন আপডেট বিদ্যমান নেই</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ত্রুটি</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">আপডেটার তথ্য</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">শেষ চেষ্টা:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">অবস্থা:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%2$sএ %1$s যোগ করা হয়েছে</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">মেনুতে খুলুন</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ঠিক আছে, বুঝতে পেরেছি</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..2fb0506106
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-br/strings.xml
@@ -0,0 +1,277 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lenn ha kemmañ arventennoù ar vuhez prevez</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Haeziñ ho roadennoù evit an holl lec’hiennoù</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Haeziñ ho roadennoù evit %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Haeziñ ho roadennoù evit al lec’hiennoù en domani %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Haeziñ ho roadennoù war 1 lec’hienn all</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Haeziñ ho roadennoù war %1$d lec’hienn all</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Haeziñ ho roadennoù war 1 domani all</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Haeziñ ho roadennoù war %1$d a zomanioù all</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d diwar %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Haeziñ ivinelloù ar merdeer</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Kadaviñ ur c’hementad anvevennek a roadennoù arval</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Haeziñ oberiantiz ar merdeer e-pad ar merdeiñ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lenn ha kemmañ ar sinedoù</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lenn ha kemmañ arventennoù ar merdeer</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Skarzhañ ar roll istor, an toupinoù hag ar roadennoù liammet nevesañ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Kaout roadennoù ar golver</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Enankañ roadennoù er golver</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Stankañ danvezioù war forzh peseurt pajenn</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lenn ho roll istor merdeiñ</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Pellgargañ restroù, lenn ha kemmañ roll istor pellgargadennoù ar merdeer</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Digeriñ ar restroù pellgarget war ho trevnad</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lenn an destenn en holl ivinelloù digor</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Haeziñ ho lec’hiadur</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Lenn ar roll istor merdeiñ</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Evezhiañ arver an askouezhioù hag ardeiñ an neuzioù</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Eskemm kemennadennoù gant arloadoù all</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Skrammañ rebuzadurioù evidoc’h</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kinnig gwazerezhioù dilesa enrineget</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Reoliañ arventennoù proksi ar merdeer</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Haeziñ ivinelloù serret nevez ’zo</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Diskouez ha kuzhat ivinelloù ar merdeer</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Lenn ar roll istor merdeiñ</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Digeriñ an ostilhoù diorren evit haeziñ ho roadennoù en ivinelloù digor</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Handelv</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Aozer</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Aozerien</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Hizivadur diwezhañ</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pennbajenn</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Gouzout hiroc’h a-zivout an aotreoù</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Prizadur</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Gouzout muioc’h diwar-benn an askouezh-mañ</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Gouzout muioc’h diwar-benn an askouezh-mañ</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Arventennoù</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Gweredekaet</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Diweredekaet</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Aotren er Merdeiñ Prevez</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Lañsañ er Merdeiñ Prevez</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">N’eo ket aotreet er prenestroù prevez</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Gweredekaet</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Diweredekaet</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Staliet</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Erbedet</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">N’eo ket skoret c’hoazh</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">N’eo ket hegerz c’hoazh</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Diweredekaet</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Munudoù</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Aotreoù</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Dilemel</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Danevellañ</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Ouzhpennañ %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s a c’houlenn aotreoù ouzhpenn.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Goulenn a ra hoc’h aotre evit:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Fell a ra dezhañ:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Ouzhpennañ</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Aotren</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Nac’hañ</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Nullañ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Staliañ an askouezh</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Staliañ %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Nullañ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Burutelladennoù: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Askouezhioù</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Ardoer an askouezhioù</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Diweredekaet eo an askouezhioù evit ar mare</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Adloc’hañ an askouezhioù</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Adloc’hañ an askouezhioù</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Kavout muioc’h a askouezhioù</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Kavout askouezhioù all</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Aotren</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Nac’hañ</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Un hizivadur nevez a zo evit %1$s</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Aotreoù nevez a zo evit %1$d</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Un aotre nevez a zo goulennet</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Hizivadennoù an askouezh</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Hizivaat an askouezhioù</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Gwiriekaer askouezhioù skoret</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Askouezh nevez hegerz</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Askouezhioù nevez hegerz</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Ouzhpennañ %1$s e %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Ouzhpennañ %1$s ha %2$s e %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Ouzhpennañ anezho e %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Arnevesaet eo bet teknologiezh askouezhioù Firefox. An askouezhioù-se a implij sternioù meziant nʼint ket keverlecʼh gant Firefox 75 hag an handelvoù goude.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Emaomp o sevel peadra da gemer e kont skor un dibab a askouezhioù erbedet.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">O pellgargañ hag o wiriañ an askouezh…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">O pellgargañ hag o wiriañ an askouezh…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Ne cʼhaller ket kaout roll an askouezhioù!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">N’eo ket bet kavet an droidigezh, nag evit ar yezh %1$s nag evit ar yezh dre ziouer %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Staliet %1$s gant berzh</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Cʼhwitadenn war staliadur %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Staliadur an askouezh c’hwitet.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">An askouezh-mañ n’hall ket bezañ pellgarget en abeg d’ur fazi kennask.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">An askouezh-mañ n’hall ket bezañ staliet rak kontronet eo war ar seblant.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">An askouezh-mañ n’hall ket bezañ staliet rak n’eo ket bet gwiriet.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s n’hall ket bezañ staliet rak ne glot ket gant %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s n’hall ket bezañ staliet rak gallout a ra degas ur riskl bras a-fet stabilded pe diogelroez.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Gweredekaet %1$s gant berzh</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Cʼhwitadenn war weredekaat %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Diweredekaet %1$s gant berzh</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Cʼhwitadenn war ziweredekaat %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Distaliet %1$s gant berzh</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Cʼhwitadenn war distaliadur %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Dilamet %1$s gant berzh</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Cʼhwitadenn war zilamadur %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Enporzhiet eo bet an askouezh-mañ diwar un handelv kozh eus %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 askouezh</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 askouezhioù</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s askouezh</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s askouezhioù</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Gouzout hirocʼh</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Hizivaet gant berzh</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Hizivadenn ebet hegerz</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Fazi</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Titouroù an hizivaddenn</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Esae diwezhañ:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stad:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Ouzhpennet eo bet %1$s da %2$s.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Digeriñ anezhañ el lañser</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Mat eo, komprenet am eus</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Mat eo</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Gouzout hiroc’h</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Diweredekaet eo bet %1$s en abeg da gudennoù a-fet surentez pe stabilded.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">N’haller ket gwiriekaat ma’z eo diogel %1$s ha diweredekaet eo bet.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Ne glot ket an askouezh %1$s gant hoc’h handelv %2$s (handelv %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..7eb6bb7c60
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-bs/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Čita i uređuje postavke privatnosti</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Pristup vašim podacima za sve web stranice</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Pristupite vašim podacima za %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Pristupite vašim podacima za web stranice u domeni %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Pristupite vašim podacima na još 1 web stranici</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Pristupite vašim podacima na %1$d drugih web stranica</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Pristup vašim podacima na još 1 domeni</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Pristupite vašim podacima na %1$d drugih domena</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d od %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Pristup tabovima browsera</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Pohrani neograničenu količinu client-side podataka</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Pristup aktivnostima browsera tokom navigacije</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Čita i uređuje zabilješke</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Čita i uređuje postavke browsera</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Obriši skorašnju historiju surfanja, kolačiće, i srodne podatke</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Dobavi podatke sa clipboarda</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Unosi podatke na clipboard</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokirajte sadržaj na bilo kojoj stranici</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Pročitajte svoju historiju pretraživanja</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Preuzima fajlove, čita i uređuje historiju preuzimanja u browseru</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Otvara fajlove preuzete na vaš uređaj</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Čita tekst iz svih otvorenih tabova</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Pristupa vašoj lokaciji</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Pristupa historiji surfanja</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Nadgleda upotrebu ekstenzija i upravlja temama</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Razmjenjuje poruke sa aplikacijama osim s ovom</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Prikazuje vam obavještenja</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Pruža kriptografske usluge autentikacije</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontroliše proxy postavke u browseru</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Pristupa nedavno zatvorenim tabovima</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Sakriva i prikazuje tabove</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Pristupa historiji surfanja</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Omogućite razvojnim alatima pristup vašim podacima u otvorenim tabovima</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Verzija</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autori</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Zadnje ažuriranje</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Početna stranica</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saznajte više o dozvolama</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Ocjena</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Više o ovom dodatku</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Više o ovoj ekstenziji</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Postavke</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Uklj.</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Isklj.</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Dozvoli u privatnom surfanju</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Pokreni u privatnom surfanju</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Nije dozvoljeno u privatnim prozorima</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Omogućen</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Onemogućen</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalirano</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Preporučeno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Još nije podržano</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Još nije dostupno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Onemogućeno</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalji</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Dozvole</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Ukloni</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Prijavi</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Dodaj %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s traži dodatna odobrenja.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Traži vašu dozvolu da:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Želi da:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Dodaj</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Dozvoli</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Odbij</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Otkaži</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instaliraj add-on</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instaliraj %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Otkaži</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recenzije: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Ocjena: %1$.02f od 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-oni</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Menadžer dodataka</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Dodaci su privremeno onemogućeni</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Ekstenzije su privremeno onemogućene</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Jedan ili više dodataka su prestali da rade, čineći vaš sistem nestabilnim.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Jedno ili više ekstenzija je prestalo da radi, što je vaš sistem učinilo nestabilnim.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Ponovo pokrenite dodatke</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Ponovo pokrenite ekstenzije</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Pronađite još dodataka</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Pronađite još ekstenzija</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Dozvoli</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Odbij</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ima novu nadogradnju</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Potrebno je %1$d novih dozvola</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Potrebna je nova dozvola</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Nadogradnje add-ona</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Ažuriranja ekstenzija</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Podržani alat za provjeru add-ona</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Novi add-on dostupan</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Novi add-oni dostupni</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Dodaj %1$s u %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Dodaj %1$s i %2$s u %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Dodaj ih u %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox add-on tehnologija se modernizuje. Ovi add-oni koriste framework-e koji nisu kompatibilni sa Firefoxom 75 i novijim.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Trenutno razvijamo podršku za inicijalni izbor preporučenih ekstenzija.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Preuzimanje i provjera add-ona…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Preuzimanje i provjera ekstenzije…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Ne mogu dobaviti listu</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Nije uspjelo postavljanje upita za ekstenzije!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Prijevod nije pronađen za %1$s jezik, niti za standardni %2$s jezik</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s uspješno instaliran</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Instalacija %1$s nije uspjela</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Instalacija ovog dodatka nije uspjela.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Instalacija ove ekstenzije nije uspjela.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Ovaj dodatak nije moguće preuzeti zbog greške u vezi.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Ovu ekstenziju nije bilo moguće preuzeti zbog neuspjeha veze.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Ovaj dodatak nije instaliran jer se čini da je oštećen.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Ovu ekstenziju nije moguće instalirati jer se čini da je oštećena.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Ovaj dodatak nije instaliran jer nije mogao biti verifikovan.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Ovu ekstenziju nije moguće instalirati jer nije potvrđena.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s nije moguće instalirati jer nije kompatibilan sa %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s nije instaliran zbog visokog rizika izazivanja problema sa stabilnošću ili sigurnošću.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s uspješno omogućen</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Neuspješno omogućavanje %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s uspješno onemogućen</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Neuspješno onemogućavanje %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s uspješno deinstaliran</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Deinstalacija %1$s nije uspjela</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s uspješno uklonjen</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Neuspješno uklanjanje %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ovaj add-on je migriran sa prethodne verzije %1$s-a</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 ekstenzija</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s add-ona</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s ekstenzija</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saznajte više</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Uspješno nadograđeno</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nema dostupnih nadogradnji</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Greška</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informacije o nadogradnji</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Zadnji pokušaj:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s je dodan u %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Otvori ga u meniju</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Pristupite %1$s iz %2$s menija.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK, razumijem</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Saznajte više</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s je onemogućen pošto uzrokuje probleme vezane za sigurnost i stabilnost.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s nije mogao biti potvrđen kao siguran i onemogućen je.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s nije kompatibilan sa vašom verzijom %2$s (verzija %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..9d877a50fb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ca/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Llegir i modificar els paràmetres de privadesa</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Accedir a les dades de tots els llocs web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Accedir a les dades de %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Accedir a les dades web del domini %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Accedir a les vostres dades d’1 altre lloc</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Accedir a les vostres dades de %1$d altres llocs</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Accedir a les vostres dades d’ altre domini</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Accedir a les vostres dades de %1$d altres dominis</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Accedir a les pestanyes del navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Emmagatzemar una quantitat il·limitada de dades del costat del client</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Accedir a l’activitat durant la navegació</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Llegir i modificar les adreces d’interès</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Llegir i modificar els paràmetres del navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Esborrar l’historial de navegació recent, les galetes i dades relacionades</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obtenir les dades del porta-retalls</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Afegir dades al porta-retalls</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blocar el contingut de qualsevol pàgina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Llegir l’historial de navegació</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Baixar els fitxers i modificar l’historial de baixades del navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Obrir els fitxers que heu baixat al dispositiu</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Llegir el text de totes les pestanyes obertes</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Accedir a la vostra ubicació</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Accedir a l’historial de navegació</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Supervisar l’ús d’extensions i gestionar els temes</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercanviar missatges amb altres aplicacions</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Mostrar-vos notificacions</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Proporcionar serveis d’autenticació criptogràfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar els paràmetres de servidor intermediari del navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Accedir a les pestanyes tancades recentment</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Amagar i mostrar pestanyes del navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Accedir a l’historial de navegació</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ampliar les eines per a desenvolupadors per accedir a les vostres dades de les pestanyes obertes</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versió</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autoria</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autors</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Darrera actualització</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pàgina d’inici</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Més informació sobre els permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Valoració</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Més informació sobre aquest complement</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Paràmetres</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activat</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Desactivat</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permet en la navegació privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Executa en la navegació privada</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activat</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Desactivat</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instal·lats</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomanats</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Encara no és compatible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Encara no disponible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desactivats</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalls</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Elimina</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Informa</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Voleu afegir %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s sol·licita permisos addicionals.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Necessita permisos per:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Vol:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Afegeix</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permet</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Denega</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancel·la</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Instal·la el complement</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancel·la</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Ressenyes: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complements</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gestor de complements</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">Els complements s’han desactivat temporalment</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">Un o més complements han deixat de funcionar, i això fa que el vostre sistema sigui inestable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Reinicia els complements</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Cerca més complements</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permet</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denega</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s té una actualització nova</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Es necessiten %1$d permisos nous</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Es necessita un permís nou</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Actualitzacions del complement</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Verificador de complements compatibles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Complement nou disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Complements nous disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Afegeix «%1$s» al %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Afegeix «%1$s» i «%2$s» al %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Afegeix-los al %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">La tecnologia del complement del Firefox s’està modernitzant. Aquests complements utilitzen una tecnologia incompatible amb el Firefox 75 i versions posteriors.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Actualment estem treballant per oferir una selecció inicial d’extensions recomanades.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">S’està baixant i verificant el complement…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">No s’ha pogut fer la consulta dels complements</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">No s’ha trobat la traducció, ni per a la llengua %1$s ni per a la llengua per defecte %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s s’ha instal·lat correctament</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">No s’ha pogut instal·lar %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">No s’ha pogut instal·lar aquest complement.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">No s’ha pogut baixar aquest complement perquè s’ha produït un problema de connexió.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">No es pot instal·lar el complement perquè sembla estar malmès.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">No es pot instal·lar aquest complement perquè no està verificat.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">No s’ha pogut instal·lar el complement «%1$s» perquè no és compatible amb el %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">No s’ha pogut instal·lar el complement %1$s perquè hi ha un alt risc que provoqui problemes d’estabilitat o de seguretat.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s s’ha activat correctament</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">No s’ha pogut activar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s s’ha desactivat correctament</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">No s’ha pogut desactivar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s s’ha desinstal·lat correctament</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">No s’ha pogut desinstal·lar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s s’ha eliminat correctament</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">No s’ha pogut eliminar %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Aquest complement es va migrar des d’una versió anterior del %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 complement</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s complements</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Més informació</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">S’ha actualitzat correctament</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No hi ha cap actualització</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informació de l’actualitzador</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Darrer intent:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estat:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s s’ha afegit al %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Obre-ho des del menú</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Entesos</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Més informació</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">S’ha inhabilitat %1$s per motius de seguretat o d’estabilitat.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">No s’ha pogut verificar que «%1$s» sigui segur i s’ha inhabilitat.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s no és compatible amb la vostra versió del %2$s (versió %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..12c845c568
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-cak/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Tisik\'ïx chuqa\' tijal ri runuk\'ulem ichinanem</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Nok pa ri taq atzij pa ronojel ri ajk\'amaya\'l ruxaq</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Katok pa taq atzij richin %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Katok pa ri taq atzij richin ri taq ruxaq k\'amaya\'l pa %1$s ruk\'ojlem b\'ey</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Katok pan atzij pa 1chik ruxaq</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Katok pan atzij pa %1$d ch\'aqa\' ruxaq</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Katok pan atzij pa 1 chik ajk\'amal</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Katok pan atzij pa %1$d chik ajk\'amal</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d richin %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Tok pa ri taq ruwi\' okik\'amaya\'l</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Tiyak mek\'isel kajilab\'al taq tzij richin ri qawinaq</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Nok pa ri rusamaj okik\'amaya\'l toq okinäq pa k\'amaya\'l</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Kesik\'ïx chuqa\' kek\'ex taq yaketal</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Nisik\'ïx chuqa\' nijal ri runuk\'ulem kanob\'äl</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Tiyuj ri k\'ak\'a\' natab\'äl, taq kuki ch\'aqa\' chik taq rutzij ri okem pa k\'amaya\'l</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Tik\'am tzij pa ri yakwujb\'äl</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Kenim taq tzij pa ri yakwujb\'äl</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Tiq\'at rupam pa xab\'achike ruxaq</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Tasik\'ij ri runatab\'al ri okik\'amaya\'l</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Keqasäx taq yakb\'äl, tisik\'ïx chuqa\' tijalwachïx ri kik\'ulwachinel taq ruqasanik okik\'amaya\'l</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Kejaq ri taq yakb\'äl eqasan pan awokisanel</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Tisik\'ïx ri tz\'ib\'anïk pa ri jaqäl taq ruwi\'</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Katok pan ak\'ojlib\'al</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Okem pa runatab\'al rokem k\'amaya\'l</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Kenik\'öx ri rokisaxik k\'amal chuqa\' ri kinuk\'samajixik taq wachinel</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Kekitaqala\' taq rutzijol chi kikojol juley chik taq chokoy</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Kek\'ut pe ri rutzijol chawäch</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Keya\' chib\'äl juxun taq samaj</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Tichajïx runuk\'ulem ruproxi ri okik\'amaya\'l</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Katok pa ri taq ruwi\' nimakol ketz\'apïx</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ke\'ewäx chuqa\' kek\'ut pe ri taq ruwi\' okem pa k\'amaya\'l</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Okem pa runatab\'al rokem k\'amaya\'l</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Kenimirisäx ri kisamajib\'al b\'anonela\' richin ke\'ok pa taq kitzij pa jaqon taq ruwi\'</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Ruwäch</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">B\'anel</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Taq b\'anel</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ruk\'isib\'äl k\'exoj</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Tikirib\'äl ruxaq</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Tetamäx ch\'aqa\' chik chi kij ri taq ya\'öl q\'ij</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Kejqalem</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Ch\'aqa\' chik chi rij re tz\'aqat re\'</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Taq nuk\'ulem</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Tzijïl</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Chupül</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Tiya\' q\'ij chi re pa ichinan okem pa k\'amaya\'l</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Tisamajïx pa ichinan okem pa k\'amaya\'l</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Man okel ta pa ichinan rutzuwäch</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Tzijon</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Chupun</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Xyak</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Chilab\'en</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">K\'a man ruk\'amon ta ri\'</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">K\'a man wachel ta</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Chupun</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Kib\'anikil</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Taq ya\'oj q\'ij</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Tiyuj</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Tiya\' rutzijol</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿La nitz\'aqatisäx %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s nuk\'utuj rutz\'aqat taq ya\'oj q\'ij.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Najowäx chi naya\' q\'ij richin:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Nrajo\':</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Titz\'aqatisäx</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Tiya\' q\'ij</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Tixutüx</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Tiq\'at</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Tiyak ri Tz\'aqat</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Tiyak %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Tiq\'at</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Taq nik\'oxïk: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Rajlab\'al: %1$.02f richin 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Taq tz\'aqat</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Kinuk\'samajel taq Tz\'aqat</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">Echupun jumej ri taq tz\'aqat</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">Jun o ka\'i\' oxi\' taq tz\'aqat xkiq\'ät kisamaj, ri nub\'än chi man jikil ta ri q\'inoj.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Ketzij chik ri taq tz\'aqat</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Kekanöx ch\'aqa\' chik taq tz\'aqat</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Tiya\' q\'ij</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Tiq\'at</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s k\'o jun k\'ak\'a\' k\'exoj</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Najowäx %1$d k\'ak\'a\' ya\'oj q\'ij</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Najowäx jun k\'ak\'a\' ya\'oj q\'ij</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Kik\'exoj tz\'aqat</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">K\'amonel tojtob\'äl tz\'aqat</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">K\'ak\'a\' tz\'aqat wachel</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">K\'ak\'a\' taq tz\'aqat wachel</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Titz\'aqatisäx %1$s pa %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Ketz\'aqatisäx %1$s chuqa\' %2$s pa %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Ketz\'aqatisäx pa %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Nib\'an k\'ak\'a\' chi re ri runa\'ob\'al rutz\'aqat Firefox. Re taq tz\'aqat re\' yekokisaj taq ruchi\' ri man kik\'amon ta ki\' rik\'in ri Firefox 75 &amp; ch\'aqa\' chik e nima\'q.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Wakami niqanük\' tob\'äl kichin nab\'ey cha\'oj taq K\'amal Echilab\'en.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Niqasäx chuqa\' ninik\'öx tz\'aqat…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">¡Xsach toq xk\'utüx Tz\'aqat!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Man xilitäj ta ri tzalq\'omanïk richin ri %1$s chuqa\' ni xa ta ri %2$s ch\'ab\'äl k\'o</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Ütz xyak ri %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Xsach toq xyak ri %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Xsach toq niyak re tz\'aqat re\'.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">Man xqasäx ta re tz\'aqat re ruma man pa rub\'eyal taq ri rokem.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Man xyak ta kan re tz\'aqat xa ke xa man ütz ta.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Man xyak ta kan re jun rutz\'aqat re\' ruma chi man nik\'on ta.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Man xyak ta ri %1$s ruma man nuk\'äm ta ri\' rik\'in %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s man xyak ta ruma jikil chi xtuya\' k\'ayewal pa ruwi\' rutzil o rujikomal.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Ütz xtzij ri %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Xsach toq xtzij ri %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Ütz xchup ri %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Xsach toq xchup ri %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Ütz xyuj ri %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Xsach toq xyuj ri %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Ütz xyuj ri %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Xsach toq xyuj ri %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Re tz\'aqat re\' xk\'am pe pa jun kan ruwäch %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 tz\'aqat</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s taq tz\'aqat</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Tetamäx ch\'aqa\' chik</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Ütz ruk\'exik</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Majun k\'exoj ruwäch</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Sachoj</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Retamab\'äl K\'exoj</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ruk\'isib\'äl tojtob\'enïk:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Rub\'anikil:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s xtz\'aqatisäx pa %2$s.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Tijaq pa k\'utsamaj</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Tok pa %1$s chupam ri %2$s cholsamaj.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ütz, Xq\'ax pa nuwi\'</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">ÜTZ</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Tetamäx ch\'aqa\' chik</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Xchup ri %1$s ruma k\'ayewal pa ruwi\' jikomal chuqa\' rutzil.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Man xjikib\'äx ri %1$s chi jikil ruma ri\' toq xchup.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Man nuk\'äm ta ri\' ri %1$s rik\'in ri ruwäch %2$s (ruwäch %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..64f067cf7c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Basaha ug usba ang mga privacy setting</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">i-Access imong data sa mga website</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">i-Access imong data sa %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">i-Access imong data sa mga site sa %1$s nga domain</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">i-Access imong data sa 1 pa ka-site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">i-Access imong data sa %1$d ubang mga site</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Susiha imong data sa 1 laing domain</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">i-Access imong data sa %1$d uban pang mga domain</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">i-Access ang mga browser tab</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Pagtipig ug unlimited nga client-side data</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">i-Access and kalihokan sa browser samtang nag-navigate</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Basaha ug usba ang mga bookmark</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Basaha ug usba ang mga setting sa browser</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Panasa ang bag-ong browsing history, cookie, ug iglabot nga data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Kuha ug data gikan sa clipboard</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Butang ug data sa clipboard</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Kuhaa ang mga file ug basaha ug usba ang download history sa browser</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Ablihi ang mga file nga nadownload sa imong device</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Basaha ang text sa tanan abli nga tab</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">i-Access imong dapit</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">i-Access ang browsing history</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">i-Monitor ang gamit sa extension ug i-manage ang mga themes</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Exchange ug mga message tali sa ubang mga app</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Ipakita ang mga pahibalo kanimo</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Hatag ug mga cryptographic authentication service</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">i-Control ang mga browser proxy setting</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">i-Access nasiradong mga tab</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">i-Tago ug i-pakita ang mga browser tab</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">i-Access ang browsing history</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">i-Extend ang developer tools para ma-access imong data sa abli nga tab</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Mga Author</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ulahing pag-update</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Homepage</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Tuon sa ubang mga permission</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Rating</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Mga Setting</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">On</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Off</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Tugotan sa private browsing</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">i-Padagan sa private browsing</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">na-Enable</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">na-Disable</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">na-Install</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">na-Rekomenda</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Dili pa suportado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Dili pa available</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">na-Disable</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Mga Detalye</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Mga Permission</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Remove</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">i-Dugang ang %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Kinahanglan imong pagtugot para:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Idugang</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancel</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Mag-install ug Add-on</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancel</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Mga Review: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Mga Add-on</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Add-ons Manager</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Allow</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Deny</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s naay update</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d kinahanglan bag-ong mga permission</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Kinahanglan ug bag-o nga permission</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Mga Add-on update</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Checker sa suportadong mga add-on</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Naa\'y bag-ong add-on</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Naa\'y bag-ong mga add-on</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">i-Dugang ang %1$s sa %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">i-Dugang ang %1$s ug %2$s sa %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">i-Dugang tanan sa %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Ang Firefox add-on technology pirmi ga-usab. Ang mga add-on gagamit ug framework nga dili bagay sa Firefox 75 ug daan.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Kamulo kami gatukod ug suporta alang sa initial selection sa Recommended Extensions.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Kamulong download ug suta sa add-on…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Pakyas sa pagquery sa Add-ons!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Wala\'y translation, para sa locale nga %1$s ug walay default language %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Malampusong na-install %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Pakyas sa install %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Malampusong na-enable %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Pakyas pag-enable %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Malampusong na-disable %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Pakyas pag-disable %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Malampusong na-uninstall %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Pakyas pag-uninstall %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Malampusong na-remove %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Pakyas pag-remove %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Kini nga add-on na-migrate gikan sa daang version sa %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s mga add-on</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Dugang pagtuon</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Malampusong na-update</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Wala\'y update available</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Updater Information</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ulahing sulay:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s nadugang sa %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Ablihi sa menu</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Okay, Sabtan</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..49b1a1baeb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">ڕێکخستنەکانی تایبەتمەندی بخوێنەوە و دەستکاریبکە</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">چوونەناو زانیاریەکان لە هەموو ماڵپەڕەکان</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">چوونەناو زانیارییەکانت لە ماڵپەڕی %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">چوونەناو زانیارییەکانت بۆ ماڵپەڕەکان لە دۆمەینی %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">چوونەناو زانیارییەکانت لە لە 1 ماڵپەڕی تر </string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">چوونەناو زانیارییەکانت لە %1$d ماڵپەڕی تر </string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">چوونەناو زانیارییەکانت لە 1 دۆمەینی تر</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">چوونەناو زانیارییەکانت لە %1$d دۆمەینی تر</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">بچۆناو بازدەرەکانی وێبگەڕ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">خوێندنەوە و دەستکاریکردنی نیشانکراوەکان</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">خوێندنەوە و دەستکاریکردنی ڕێکخستنەکانی وێبگەڕ</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">کۆتا مێژووی کار، شەکرۆکە، و زانیاری وابەستە پاکبکەرەوە</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">زانیاری بەدەستبێنە لە گرتەتەختەوە(لەبەرگرەوە)</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ئەو پەڕگانە بکەرەوە کە داگیراوە بۆ سەر ئامێرەکەت</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">دەقی هەموو بازدەرە کراوەکان بخوێنەرەوە</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">زانینی شوێنەکەت</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">چوونەناوی مێژووی کارەکەت</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">چاودێری بەکارهێنانی زیادکراوەکان و بەڕێوەبردنی ڕووکارەکان</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">گۆڕینەوەی پەیام لگەڵ بەرنامەکان جگە لەمە</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">پیشاندنای ئاگانامە بۆ تۆ</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">چوونەناو کۆتا بازدەرە داخراوەکان</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">پیشاندان و شاردنەوەی بازدەرەکانی وێبگەڕ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">چوونەناوی مێژووی کارەکەت</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">وەشان</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">نووسەرەکان</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">کۆتاجار نوێکراوەتەوە</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">پەڕەی سەرەکی</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">زیاتر بزانە دەربارەی دەسەڵاتەکان</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">پلەبەندی</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ڕێکخستنەکان</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">کارا</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">ناکارا</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">ڕێگەی پێبدە لە وێبگەڕی تایبەتی</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">جێبەجێکردن لە وێبگەڕی تایبەت</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">چالاکە</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">ناچالاکە</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">دامەزراو</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">پێشنیارکراو</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">هێستا پشتگیری ناکرێت</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">هێشتا بەردەست نیە</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ناچالاکە</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">وردەکاری</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ڕێگەپێدانەکان</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">بیسڕەوە</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">زیادکردنی %1$s ؟</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">داوای ڕێپێدانت دەکات بۆ:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">زیادکردنی</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">پاشگەزبوونەوە</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">دامەزراندنی پێوەکراو</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">پاشگەزبوونەوە</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">پێداچوونەوە: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">پێوەکراوەکان</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">بەڕێوەبەری پێوەکراوەکان</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ڕێگەبدە</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ڕێگەمەدە</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s نوێکارییەکی بۆ هاتووە</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d ڕێگەپێدان داواکراوە</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ڕێگەثێدانێکی نوێ داواکراوە</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">نوێکاری پێوەکراوەکان</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">چێکەری پێوەکراوی پشتگیریکراو</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">پێوەکراوی نوێ بەردەستە</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">پێوەکراوی نوێ بەردەستن</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">زیادکردنی %1$s بۆ %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">زیادکردنی %1$s و %2$s بۆ %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">زیادییان بکە بۆ %1$s</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">داگرتن و دڵنیابوونەوە لە پێوەکراو…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">هەڵە لە بەدەستهێنانی زانیاری پێوەکراو</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">وەرگێڕان نەدۆزرایەوە،نە بۆ زمانی %1$s و بۆ زمانی بنەڕەتیش %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">بەسەرکەوتوویی %1$s دەمەزرا</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">نەتوانرا %1$s دابمەزرێت</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">بەسەرکەوتوویی %1$s چالاک کرا</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">نەتوانرا %1$s چالاک بکرێت</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">بەسەرکەوتوویی %1$s ناچالاک کرا</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">نەتوانرا %1$s ناچالاک بکرێت</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s سەرکەوتووانە سڕایەوە</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">نەتوانرا %1$s بسڕێتەوە</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s سڕایەوە بە سەرکەوتووی</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">نەتوانرا %1$s بسڕێتەوە</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ئەم پێوەکراوە هێنراوە لە وەشانی پێشووی %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">١ پێوەکراو</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s پێوەکراو</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">زیاتر بزانە</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">بەسەرکەوتوویی نوێکرایەوە</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">نوێکردنەوە بەردەست نیە</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">هەڵە</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">زانیاری نوێکەرەوە</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">کۆتا هەوڵدان:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">دۆخ:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s زیادکرا بۆ %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">بیکەرەوە لە پێڕست</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">باشە، تێگەیشتم</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..eca911b76a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-co/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Cunsultà è mudificà e preferenze di vita privata</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Accede à i vostri dati per tutti i siti web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Accede à i vostri dati per %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Accede à i vostri dati per i siti di u duminiu %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Accede à i vostri dati per un altru situ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Accede à i vostri dati per %1$d altri siti</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Accede à i vostri dati per un altru duminiu</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Accede à i vostri dati per %1$d altri duminii</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d nant’à %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Accede à l’unghjette di u navigatore</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Allucà una quantità illimitata di dati da u latu cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Accede à l’attività di u navigatore durante a navigazione</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Cunsultà è mudificà l’indette</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Cunsultà è mudificà e preferenze di u navigatore</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Squassà a cronolugia recente di navigazione, i canistrelli, è i dati assuciati</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Ottene i dati da u preme’papei</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Aghjunghje dati in u preme’papei</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bluccà u cuntenutu nant’à qualsiasi pagina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Leghje a vostra cronolugia di navigazione</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Scaricà schedarii, è leghje è mudificà a cronolugia di i scaricamenti di u navigatore</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Apre i schedarii scaricati nant’à u vostru apparechju</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leghje u testu di tutte l’unghjette aperte</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Accede à a vostra lucalizazione</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Accede à a cronolugia di navigazione</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Cuttighjà l’impiegu di l’estensioni è urganizà i temi</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Scambià messaghji cù d’altre appiecazioni chè quessa</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Affissavvi nutificazioni</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Pruvede servizii d’autenticazione cifrata</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Cuntrollà e preferenze proxy di u navigatore</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Accede à l’unghjette chjose pocu fà</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Affissà o piattà l’unghjette di u navigatore</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Accede à a cronolugia di navigazione</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Stende l’impiegu di l’attrezzi di sviluppu per accede à i vostri dati in l’unghjette aperte</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versione</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autore</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autori</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ultima mudificazione</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pagina d’accolta</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Sapene di più nant’à i permessi</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Apprezzazione</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Sapene di più nant’à stu modulu addiziunale</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Sapene di più nant’à st’estensione</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Preferenze</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Attivatu</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Disattivatu</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permette in navigazione privata</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Lancià in navigazione privata</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Micca permessu in e finestre di navigazione privata</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Attivatu</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Disattivatu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installatu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Ricumandatu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Mancu accettatu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Mancu dispunibule</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Disattivatu</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detaglii</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permessi</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Caccià</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Signalà</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Aghjunghje %1$s ?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s richiede permessi addiziunale.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">U vostru accunsentu hè richiestu per :</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Hà bisognu di :</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Aghjunghje</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permette</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Ricusà</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Abbandunà</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installà u modulu</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installà %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Abbandunà</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Voti : %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Valutazione : %1$.02f nant’à 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Moduli addiziunali</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Ghjestiunariu di moduli addiziunali</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">I moduli addiziunali sò disattivati timpurariamente</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">L’estensioni sò disattivate timpurariamente</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Certi moduli addiziunale anu fermatu di funziunà, rindendu u vostru sistema instabile.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Certe estensioni anu fermatu di funziunà, rindendu u vostru sistema instabile.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Rilancià i moduli addiziunali</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Rilancià l’estensioni</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Scopre d’altri moduli addiziunali</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Scopre d’altre estensioni</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permette</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Ricusà</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Una versione mudificata hè dispunibule per %1$s</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d novi permessi sò richiesti</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Un permessu novu hè richiestu</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Rinnovi di i moduli</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Rinnovi d’estensioni</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Verificazione di i moduli addiziunali accettati</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Novu modulu addiziunale dispunibule</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Novi moduli addiziunali dispunibule</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Aghjunghje %1$s à %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Aghjunghje %1$s è %2$s à %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Aghjunghjeli à %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">A tecnolugia di i moduli addiziunali di Firefox s’ammuderneghja. Sti moduli impieganu una struttura chì ùn hè micca cunciliabile cù Firefox 75 è e so versioni precedente.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Simu in treccia di custruisce una prima selezzione d’estensioni ricumandate.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Scaricamentu è verificazione di u modulu…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Scaricamentu è verificazione di l’estensione…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Impussibule d’ottene a lista di i moduli addiziunali</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Impussibule d’ottene a lista di l’estensioni.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Traduzzione micca trova, nè per a lucale %1$s, nè per a lingua predefinita %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Installazione riesciuta di %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Fiascu di l’installazione di %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Fiascu per installà stu modulu addiziunale.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Fiascu per installà st’estensione.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Stu modulu addiziunale ùn pò micca esse scaricatu per via d’un fiascu di cunnessione.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">St’estensione ùn pò micca esse scaricata per via d’un fiascu di cunnessione.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Stu modulu addiziunale ùn pò micca esse installatu perchè pare deteriuratu.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">St’estensione ùn pò micca esse installata perchè pare deteriurata.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Stu modulu addiziunale ùn pò micca esse installatu perchè ùn hè micca statu verificatu.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">St’estensione ùn pò micca esse installata perchè ùn hè micca stata verificata.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s ùn pò micca esse installatu perchè ùn hè micca cumpatibile cù %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s ùn pò micca esse installatu perchè ci hè un risicu tamantu di cagiunà prublemi di stabilità o di sicurità.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Attivazione riesciuta di %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Impussibule d’attivà %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Disattivazione riesciuta di %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Impussibule di disattivà %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Disinstallazione riesciuta di %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Fiascu di a disinstallazione di %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Cacciatura riesciuta di %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Impussibule di caccià %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Stu modulu hè statu impurtatu da una versione precedente di %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 modulu addiziunale</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 estensione</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s moduli addiziunali</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s estensioni</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Sapene di più</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Mudificazione riesciuta</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Alcunu rinnovu dispunibule</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Sbagliu</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Infurmazione nant’à u rinnovu</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ultima prova :</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Statu :</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s hè statu aghjuntu à %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Apritelu da u listinu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Accidite à %1$s via u listinu di %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Iè, aghju capitu</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Vai</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Sapene di più</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s hè statu disattivatu per via di prublemi di sicurità o di stabilità.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s ùn pò micca esse verificatu cum’è sicuru è hè statu disattivatu.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s ùn hè micca cumpatibile cù a vostra versione di %2$s (versione %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..94d6784356
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-cs/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Číst a upravovat nastavení soukromí</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Přistupovat k vašim datům pro všechny webové stránky</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Přistupovat k vašim datům pro %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Přistupovat k vašim datům pro webové stránky na doméně %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Přistupovat k vašim datům jedné další webové stránky</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Přistupovat k vašim datům %1$d dalších webových stránek</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Přistupovat k vašim datům z jedné další domény</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Přistupovat k vašim datům z %1$d dalších domén</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d z %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Přistupovat k panelům prohlížeče</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Ukládat neomezené množství dat na straně klienta</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Přistupovat k aktivitám prohlížeče během prohlížení</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Číst a upravovat záložky</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Číst a upravovat nastavení prohlížeče</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Mazat nedávnou historii prohlížení, cookies a související data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Získat data ze schránky</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Vkládat data do schránky</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokovat obsah na jakékoli stránce</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Číst vaši historii prohlížení</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Stahovat soubory a číst a upravovat historii stahování prohlížeče</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Otevírat soubory stažené do vašeho zařízení</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Přistupovat k textu všech otevřených panelů</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Přistupovat k informacím o vaší poloze</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Přistupovat k historii prohlížení</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorovat využití rozšíření a spravovat vzhledy</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Vyměňovat si zprávy s jinými aplikacemi</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Zobrazovat vám oznámení</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Poskytovat služby pro kryptografickou autentizaci</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Změnit nastavení proxy</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Přistupovat k nedávno zavřeným panelům</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Skrývat a zobrazovat panely prohlížeče</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Přistupovat k historii prohlížení</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Rozšířit nástroje pro vývojáře a získat přístup k vašim datům v otevřených panelech</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Verze</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autor</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Poslední aktualizace</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Domovská stránka</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Zjistěte více informací o oprávněních</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Hodnocení</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Více o doplňku</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Více o tomto rozšíření</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Nastavení</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Povoleno</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Zakázáno</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Povolit v režimu anonymního prohlížení</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Povoleno v režimu anonymního prohlížení</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Doplněk není povolený v anonymních oknech</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Povoleno</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Zakázáno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Nainstalováno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Doporučené</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Zatím nepodporováno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Zatím nedostupné</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Zakázané</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Podrobnosti</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Oprávnění</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Odebrat</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Nahlásit</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Přidat %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">Doplněk %1$s požaduje dodatečná oprávnění.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Doplněk požaduje následující oprávnění:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Chce:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Přidat</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Povolit</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Zakázat</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Zrušit</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Nainstalovat doplněk</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Nainstalovat %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Zrušit</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recenze: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f z 5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Hodnocení: %1$.02f z 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Doplňky</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Správce doplňků</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Doplňky jsou dočasně zakázány</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Rozšíření jsou dočasně zakázána</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Jeden nebo více doplňků přestalo fungovat a váš systém se tak stal nestabilním.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Jedno nebo více rozšíření přestalo fungovat a váš systém se tak stal nestabilním.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Restartovat doplňky</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Restartovat rozšíření</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Najít další doplňky</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Najít další rozšíření</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Povolit</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Zakázat</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s požaduje nová oprávnění</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Požadováno nových oprávnění: %1$d</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Je požadováno nové oprávnění</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Aktualizace doplňků</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Aktualizace rozšíření</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Kontrola podporovaných doplňků</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Je dostupný nový doplněk</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Jsou dostupné nové doplňky</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Přidat %1$s do aplikace %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Přidat %1$s a %2$s do aplikace %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Přidat všechny do aplikace %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Technologie doplňků pro Firefox prochází modernizací. Tyto doplňky používají technologie, které nejsou kompatibilní s Firefoxem 75 a novějšími.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">V současné době pracujeme na podpoře pro vybraná doporučená rozšíření.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Stahování a ověřování doplňku…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Stahování a ověřování rozšíření…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Nepodařilo se získat seznam doplňků.</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Nepodařilo se získat seznam rozšíření!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Překlad pro region %1$s ani pro výchozí jazyk %2$s nebyl nalezen</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Doplněk %1$s byl úspěšně nainstalován</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Doplněk %1$s se nepodařilo nainstalovat</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Tento doplněk se nepodařilo nainstalovat.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Toto rozšíření se nepodařilo nainstalovat.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Tento doplněk se nepodařilo stáhnout z důvodu selhání připojení.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Toto rozšíření se nepodařilo stáhnout z důvodu selhání připojení.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Tento doplněk nemohl být nainstalován, protože je poškozený.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Toto rozšíření nemohlo být nainstalováno, protože je poškozené.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Tento doplněk nemohl být nainstalován, protože nebyl ověřen.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Toto rozšíření nemohlo být nainstalováno, protože nebylo ověřeno.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Doplněk %1$s nelze nainstalovat, protože není kompatibilní s prohlížečem %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Doplněk %1$s nebylo možné nainstalovat, protože existuje vysoké riziko, že způsobí problémy se stabilitou nebo zabezpečením.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Doplněk %1$s byl úspěšně povolen</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Doplněk %1$s se nepodařilo povolit</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Doplněk %1$s byl úspěšně zakázán</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Doplněk %1$s se nepodařilo zakázat</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Doplněk %1$s byl úspěšně odinstalován</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Doplněk %1$s se nepodařilo odinstalovat</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Doplněk %1$s byl úspěšně odebrán</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Doplněk %1$s se nepodařilo odebrat</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Tento doplněk byl přesunut z předchozí verze aplikace %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Jeden doplněk</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 rozšíření</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">Počet doplňků: %1$s</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s rozšíření</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Zjistit více</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Aktualizace úspěšně dokončena</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">K dispozici nejsou žádné aktualizace</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Chyba</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informace o aktualizacích</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Poslední pokus:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stav:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Doplněk %1$s byl přidán do aplikace %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Otevřete ho v nabídce</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Přístup k doplňku %1$s získáte z nabídky %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, rozumím</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Zjistit více</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Doplněk %1$s byl zakázán z bezpečnostních a výkonových důvodů.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Doplněk %1$s se nepodařilo ověřit jako bezpečný a byl zakázán.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Doplněk %1$s není kompatibilní s vaší verzí aplikace %2$s (verze %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..7e1f76ee0a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-cy/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Darllen a newid y gosodiadau preifatrwydd</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Cael mynediad i’ch data ar gyfer pob gwefan</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Cael mynediad i’ch data o %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Cael mynediad i’ch data i wefannau ym mharth %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Cael mynediad at eich data o 1 gwefan arall</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Cael mynediad at eich data o %1$d gwefan arall</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Cael mynediad at eich data o 1 parth arall</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Cael mynediad at eich data o %1$d parth arall</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d o %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Cael mynediad at dabiau’r porwyr</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Cadw swm diddiwedd o ddata ochr y cleient</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Cael mynediad at weithgaredd porwr wrth lywio</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Darllen a newid nodau tudalen</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Darllen a newid gosodiadau’r porwr</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Clirio’r hanes pori diweddar, cwcis a data cysylltiedig</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Estyn data o’r clipfwrdd</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Mewnbynnu data i’r clipfwrdd</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Rhwystro cynnwys ar unrhyw dudalen</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Darllen eich hanes pori</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Llwytho ffeiliau i lawr a darllen a newid hanes llwytho i lawr y porwr</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Agor ffeiliau a llwythwyd i lawr i’ch cyfrifiadur</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Darllen testun yr holl dabiau agored</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Mynediad i’ch lleoliad</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Mynd i’ch hanes pori</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitro’r defnydd o estyniadau a rheoli themâu</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Cyfnewid negeseuon gydag apiau ar wahân i hwn</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Dangos hysbysiadau i chi</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Darparu gwasanaethau dilysiad cryptograffig</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Rheoli gosodiadau dirprwyol porwr</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Mynediad at y tabiau caewyd yn ddiweddar</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Cuddio a dangos tabiau’r porwr</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Mynd i’ch hanes pori</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Estyn offer datblygwyr i gael mynediad i’ch data mewn tabiau agored</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Fersiwn</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Awdur</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Awduron</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Diweddarwyd diwethaf</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Tudalen Cartref</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Dysgu rhagor am ganiatâd</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Graddio</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Rhagor am yr ychwanegyn hwn</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Rhagor am yr estyniad hwn</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Gosodiadau</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ymlaen</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Diffodd</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Caniatáu yn y pori preifat</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Rhedeg yn y pori preifat</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Heb ei ganiatáu mewn ffenestri preifat</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Galluogwyd</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Analluogwyd</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Gosodwyd</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Cymeradwy</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Heb ei gefnogi eto</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ddim eto ar gael</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Analluogwyd</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Manylion</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Caniatâd</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Tynnu</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Adrodd</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Ychwanegu %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">Mae %1$s yn gofyn am ganiatâd ychwanegol.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Mae angen eich caniatâd i:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Y mae eisiau:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Ychwanegu</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Caniatáu</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Gwrthod</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Diddymu</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Gosod Ychwanegyn</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Gosod %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Diddymu</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Sgôr: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Sgôr: %1$.02f allan o 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Ychwanegion</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Rheolwr Ychwanegion</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Mae ychwanegion wedi’u hanalluogi dros dro</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Mae\'r estyniadau wedi\'u hanalluogi dros dro</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Rhoddodd un neu fwy o ategion y gorau i weithio, gan wneud eich system yn ansefydlog.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Mae un neu fwy o estyniadau wedi peidio â gweithio, gan wneud eich system yn ansefydlog.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Ailgychwyn ychwanegion</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Ailgychwyn estyniadau</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Canfod rhagor o ychwanegion</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Canfod estyniadau eraill</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Caniatáu</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Gwrthod</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Mae gan %1$s ddiweddariad newydd</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Mae angen %1$d caniatâd newydd</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Mae angen caniatâd newydd</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Diweddariadau Ychwanegion</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Diweddariadau estyniadau</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Gwirydd ychwanegion sy’n cael eu cynnal</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Ychwanegion newydd ar gael</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Ychwanegion newydd ar gael</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Ychwanegwch %1$s i %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Ychwanegwch %1$s a %2$s i %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Ychwanegwch nhw i %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Mae technoleg ychwanegiad Firefox yn moderneiddio. Mae’r ychwanegion hyn yn defnyddio fframweithiau nad ydynt yn gydnaws â Firefox 75 a thu hwnt.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Ar hyn o bryd rydym yn adeiladu cefnogaeth ar gyfer detholiad cychwynnol o Estyniadau Cymeradwy.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Llwytho a gwirio ychwanegyn…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Llwytho i lawr a gwirio estyniad…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Wedi methu ymholi Ychwanegion!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Wedi methu chwilio\'r Estyniadau!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Heb ddarganfod cyfieithiad, ar gyfer locale %1$s nac iaith rhagosodedig %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Wedi gosod %1$s yn llwyddiannus</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Wedi methu gosod %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Wedi methu gosod yr ychwanegyn hwn.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Wedi methu gosod yr estyniad hwn.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Ni fu modd llwytho’r ychwanegyn hwn i lawr oherwydd methiant y cysylltiad.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Nid oedd modd llwytho\'r estyniad hwn i lawr oherwydd gwall cysylltiad.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Nid oedd modd gosod yr ychwanegyn am ei fod yn edrych yn wallus.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Nid oedd modd gosod yr estyniad hwn oherwydd ei fod yn ymddangos yn llwgr.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Nid oedd modd gosod yr ychwanegyn am nad yw wedi ei wirio.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Nid oedd modd gosod yr estyniad hwn oherwydd nad yw wedi\'i wirio.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Nid oedd modd gosod %1$s oherwydd nid yw’n gydnaws â %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Nid oedd modd gosod %1$s oherwydd bod risg uchel o achosi problemau sefydlogrwydd neu ddiogelwch.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Wedi galluogi %1$s yn llwyddiannus</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Wedi methu galluogi %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Wedi analluogi %1$s yn llwyddiannus</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Wedi methu analluogi %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Wedi dadosod %1$s yn llwyddiannus</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Wedi methu dadosod %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Wedi tynnu %1$s yn llwyddiannus</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Wedi methu tynnu %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Mudwyd yr ychwanegyn hwn o fersiwn flaenorol o %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 ychwanegyn</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 estyniad</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s ychwanegyn</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s estyniad</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Darllen rhagor</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Wedi’i ddiweddaru’n llwyddiannus</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Dim diweddariadau ar gael</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Gwall</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Gwybodaeth am y diweddarydd</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ymgais diwethaf:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Statws:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Mae %1$s wedi ei ychwanegu at %2$s.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Agorwch ef yn y ddewislen</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Cael %1$s o ddewislen %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Iawn, rwy’n deall</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Iawn</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Darllen rhagor</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Mae %1$s wedi ei analluogi oherwydd problemau diogelwch neu sefydlogrwydd.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Nid oedd modd gwirio %1$s yn ddiogel ac mae wedi ei analluogi.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Nid yw %1$s yn gydnaws â’ch fersiwn chi o %2$s (fersiwn %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..c4af0babe4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-da/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Læse og ændre privatlivs-indstillinger</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Tilgå dine data for alle websteder</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Tilgå dine data for %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Tilgå dine data for websteder på domænet %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Tilgå dine data på 1 andet websted</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Tilgå dine data på %1$d andre websteder</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Tilgå dine data på 1 andet domæne</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Tilgå dine data på %1$d andre domæner</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d af %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Tilgå faneblade</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Gemme ubegrænsede mængder data på klient-siden</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Tilgå browser-aktivitet under navigation</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Læse og ændre bogmærker</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Læse og ændre browser-indstillinger</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Rydde seneste browserhistorik, cookies og relaterede data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Læse data fra udklipsholderen</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Skrive data til udklipsholderen</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokere indhold på enhver side</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Læse din browserhistorik</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Hente filer, samt læse og ændre browserens filhentningshistorik</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Åbne filer hentet ned på din enhed</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Læse tekst i alle åbne faneblade</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Se din position</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Tilgå browserhistorik</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Holde øje med brug af udvidelser og håndtere temaer</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Udveksle beskeder med andre apps end denne</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Vise meddelelser</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Tilbyde tjenester til kryptografisk godkendelse</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontrollere browserens proxy-indstillinger</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Tilgå nyligt lukkede faneblade</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Skjule og vise faneblade</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Tilgå browserhistorik</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Udvide Udviklerværktøj til at have adgang til data i åbne faneblade</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Udvikler</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Udviklere</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Senest opdateret</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Hjemmeside</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Læs mere om tilladelser</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Bedømmelse</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Mere om denne tilføjelse</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Mere om denne udvidelse</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Indstillinger</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Til</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Fra</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Tillad i privat browsing</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Kør i privat browsing</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Ikke tilladt i private vinduer</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Aktiveret</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Deaktiveret</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installeret</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Anbefalet</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ikke understøttet endnu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Endnu ikke tilgængelig</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Deaktiveret</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detaljer</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Tilladelser</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Fjern</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Rapporter</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Tilføj %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s kræver yderligere tilladelser.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Tilføjelsen kræver din tilladelse til at:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Den vil:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Tilføj</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Tillad</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Afvis</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Annuller</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installer tilføjelse</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installer %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Annuller</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Anmeldelser: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Bedømmelse: %1$.02f ud af 5 </string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Tilføjelser</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Håndtering af tilføjelser</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Tilføjelser er midlertidigt deaktiveret</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Udvidelser er midlertidigt deaktiveret</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">En eller flere tilføjelser holdt op med at virke, hvilket gjorde dit system ustabilt.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">En eller flere udvidelser holdt op med at virke, hvilket gjorde dit system ustabilt.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Genstart tilføjelser</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Genstart udvidelser</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Find flere tilføjelser</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Find flere udvidelser</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Tillad</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Afvis</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s har en ny opdatering</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nye tilladelser er påkrævet</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">En ny tilladelse er påkrævet</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Opdatering af tilføjelser</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Opdatering af udvidelser</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Kontrol af understøttede tilføjelser</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Ny tilføjelse tilgængelig</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nye tilføjelser tilgængelige</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Føj %1$s til %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Føj %1$s og %2$s til %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Føj dem til %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Teknologien bag tilføjelser i Firefox moderniseres. Disse tilføjelser bruger strukturer, som ikke er kompatible med Firefox 75 og nyere versioner.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Vi udvikler i øjeblikket understøttelse af et første udvalg af anbefalede udvidelser.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Henter og verificerer tilføjelse…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Henter og verificerer udvidelse…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Forespørgsel om tilføjelser mislykkedes!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Forespørgsel om udvidelser mislykkedes!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Der blev hverken fundet en oversættelse for %1$s eller for standard-sproget %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s blev installeret</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Kunne ikke installere %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Kunne ikke installere denne tilføjelse.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Kunne ikke installere denne udvidelse.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Denne tilføjelse kunne ikke hentes på grund af en forbindelsesfejl.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Denne udvidelse kunne ikke hentes på grund af en forbindelsesfejl.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Denne tilføjelse kunne ikke installeres, fordi den lader til at være ødelagt.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Denne udvidelse kunne ikke installeres, fordi den lader til at være ødelagt.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Denne tilføjelse kunne ikke installeres, fordi den ikke er blevet verificeret.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Denne udvidelse kunne ikke installeres, fordi den ikke er blevet verificeret.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s kunne ikke installeres, fordi den ikke er kompatibel med %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s kunne ikke installeres, fordi der er høj risiko for at tilføjelsen kan forårsage stabilitets- eller sikkerhedsproblemer.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s blev aktiveret</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Kunne ikke aktivere %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s blev deaktiveret</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Kunne ikke deaktivere %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s blev afinstalleret</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Kunne ikke afinstallere %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s blev fjernet</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Kunne ikke fjerne %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Denne tilføjelse blev overført fra en tidligere version af %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 tilføjelse</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 udvidelse</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s tilføjelser</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s udvidelser</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Læs mere</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Opdateret</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ingen opdatering fundet</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Fejl</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Opdateringsinformation</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Seneste forsøg:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s er blevet føjet til %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Åbn den i menuen</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Tilgå %1$s fra %2$s-menuen.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, forstået</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Læs mere</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s er blevet deaktiveret på grund af sikkerheds- eller stabilitetsproblemer.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s kunne ikke verificeres som sikker og er blevet deaktiveret.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s er ikke kompatibel med din version af %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..dfd2fe57a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-de/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Datenschutzeinstellungen lesen und ändern</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Auf Ihre Daten für alle Websites zugreifen</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Auf Ihre Daten für %1$s zugreifen</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Auf Ihre Daten für Websites in der Domain %1$s zugreifen</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Auf Ihre Daten auf einer anderen Website zugreifen</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Auf Ihre Daten auf %1$d anderen Websites zugreifen</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Auf Ihre Daten in einer anderen Domain zugreifen</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Auf Ihre Daten in %1$d anderen Domains zugreifen</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d von %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Auf Browsertabs zugreifen</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Unbegrenzt Daten auf Gerät speichern</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Auf Browseraktivität während Seitenwechsel zugreifen</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lesezeichen lesen und verändern</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Browser-Einstellungen lesen und verändern</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Browser-Chronik, Cookies und verwandte Daten löschen</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Zwischenablage auslesen</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Daten in die Zwischenablage schreiben</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Inhalte auf jeder Seite blockieren</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lesen Sie Ihre Surf-Chronik</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Dateien herunterladen und die Download-Chronik lesen und verändern</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Auf Ihr Gerät heruntergeladene Dateien öffnen</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Auf Texte aller offenen Tabs zugreifen</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Auf Ihren Standort zugreifen</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Auf Chronik zugreifen</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Erweiterungsnutzung überwachen und Themes verwalten</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Nachrichten mit anderen Apps austauschen</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Benachrichtigungen anzeigen</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Dienste zur kryptografischen Authentifizierung anbieten</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Proxy-Einstellungen des Browsers ändern</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Auf kürzlich geschlossene Tabs zugreifen</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Browsertabs ausblenden und anzeigen</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Auf Chronik zugreifen</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Entwicklerwerkzeuge erweitern, sodass Zugriff auf offene Tabs besteht</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autoren</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Zuletzt aktualisiert</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Homepage</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Weitere Informationen zu Berechtigungen</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Bewertung</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Mehr über dieses Add-on</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Mehr über diese Erweiterung</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Einstellungen</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ein</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Aus</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Im privaten Modus erlauben</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Im privaten Modus ausführen</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">In privaten Fenstern nicht erlaubt</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Aktiviert</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Deaktiviert</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installiert</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Empfohlen</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Noch nicht unterstützt</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Noch nicht verfügbar</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Deaktiviert</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Details</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Berechtigungen</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Entfernen</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Melden</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s hinzufügen?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s bittet um zusätzliche Berechtigungen.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Benötigte Berechtigungen:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Angefragte Berechtigungen:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Hinzufügen</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Erlauben</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Ablehnen</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Abbrechen</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Add-on installieren</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s installieren</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Abbrechen</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Bewertungen: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Bewertung: %1$.02f von 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Add-ons-Verwaltung</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons sind temporär deaktiviert</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Erweiterungen sind temporär deaktiviert</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Ein oder mehrere Add-ons funktionieren nicht mehr und machen Ihr System instabil.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Ein oder mehrere Erweiterungen funktionieren nicht mehr und machen Ihr System instabil.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons neu starten</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Erweiterungen neu starten</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Weitere Add-ons ansehen</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Weitere Erweiterungen suchen</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Erlauben</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Ablehnen</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s hat ein neues Update</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d neue Berechtigungen werden benötigt</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Eine neue Berechtigung wird benötigt</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Add-on-Updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Updates für Erweiterungen</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Prüfung für unterstützte Add-ons</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Neues Add-on verfügbar</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Neue Add-ons verfügbar</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s zu %2$s hinzufügen</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s und %2$s zu %3$s hinzufügen</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Zu %1$s hinzufügen</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Die Firefox-Add-on-Technologie wird modernisiert. Diese Add-ons verwenden Frameworks, die nicht mit Firefox 75 und höher kompatibel sind.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Wir entwickeln derzeit Unterstützung für eine erste Auswahl empfohlener Erweiterungen.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Add-on wird heruntergeladen und überprüft…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Erweiterung wird heruntergeladen und überprüft…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Abfrage bei Add-ons fehlgeschlagen!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Abfrage bei Erweiterungen fehlgeschlagen!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Übersetzung nicht gefunden, keine Standardsprache %2$s für Gebietsschema %1$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s erfolgreich installiert</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s konnte nicht installiert werden</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Installation dieses Add-ons fehlgeschlagen.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Installation dieser Erweiterung fehlgeschlagen.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Dieses Add-on konnte auf Grund eines Verbindungsfehlers nicht heruntergeladen werden.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Diese Erweiterung konnte auf Grund eines Verbindungsfehlers nicht heruntergeladen werden.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Dieses Add-on konnte nicht installiert werden, da es beschädigt zu sein scheint.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Diese Erweiterung konnte nicht installiert werden, da sie beschädigt zu sein scheint.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Dieses Add-on konnte nicht installiert werden, da es nicht verifiziert wurde.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Diese Erweiterung konnte nicht installiert werden, da sie nicht verifiziert wurde.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s konnte nicht installiert werden, da es nicht mit %2$s %3$s kompatibel ist.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s konnte nicht installiert werden, da es ein hohes Risiko bezüglich Stabilität und Sicherheit darstellt.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s erfolgreich aktiviert</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s konnte nicht aktiviert werden</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s erfolgreich deaktiviert</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s konnte nicht deaktiviert werden</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s erfolgreich deinstalliert</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s konnte nicht deinstalliert werden</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s erfolgreich entfernt</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s konnte nicht entfernt werden</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Dieses Add-on wurde von einer früheren Version von %1$s migriert</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 Add-on</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 Erweiterung</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s Add-ons</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s Erweiterungen</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Weitere Informationen</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Erfolgreich aktualisiert</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Kein Update verfügbar</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Fehler</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Aktualisierungsinformationen</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Letzter Versuch:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s wurde zu %2$s hinzugefügt</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Im Menü öffnen</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Greifen Sie über das %2$s-Menü auf %1$s zu.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Weitere Informationen</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s wurde aus Sicherheits- und Stabilitätsgründen deaktiviert.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s konnte nicht als sicher verifiziert werden und wurde deaktiviert.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s ist nicht mit Ihrer Version von %2$s (Version %3$s) kompatibel.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..0b4363a3d7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Nastajenja priwatnosći cytaś a změniś</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Mějśo pśistup k swójim datam za wšykne websedła</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Mějśo pśistup k swójim datam za %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Mějśo pśistup na swóje daty za sedła w domenje %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Mějśo pśistup na swóje daty na 1 dalšnem websedle</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Mějśo pśistup na swóje daty na %1$d dalšnych websedłach</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Mějśo pśistup na swóje daty na 1 dalšnej domenje</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Mějśo pśistup na swóje daty na %1$d dalšnych domenach</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d z %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Mějśo pśistup k rejtarikam wobglědowaka</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Njewobgranicowanu licbu klientowych datow składowaś</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Mějśo pśistup na aktiwitu wobglědowaka za nawigaciju</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Cytańske znamjenja cytaś a změniś</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Nastajenja wobglědowaka cytaś a změniś</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Aktualnu pśeglědowańsku historiju, cookieje a pśisłušne daty wulašowaś</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Daty z mjazywótkłada zasajźiś</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Daty do mjazywótkłada kopěrowaś</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Wopśimjeśe na kuždem boku blokěrowaś</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Cytajśo swóju pśeglědowańsku historiju</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Ześěgniśo dataje a cytajśo a změńśo ześěgnjeńsku historiju swójogo wobglědowaka</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Dataje wócyniś, kótarež su se ześěgnuli na wašom rěźe</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Tekst wšych wócynjonych rejtarikow cytaś</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Pśistup k wašomu stojnišćoju</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Mějśo pśistup k pśeglědowańskej historiji</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Wužywanje rozšyrjenjow wobglědowaś a drastwy zastojaś</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Wuměńśo powěsći z drugimi nałoženjami ako toś to</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Powěźeńki na was pokazaś</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kryptografiske awtentificěrowańske słužby wobstaraś</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Proksy-nastajenja wobglědowaka kontrolěrowaś</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Mějśo pśistup k njedawno zacynjonym rejtarikam</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Rejtariki wobglědowaka schowaś a pokazaś</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Mějśo pśistup k pśeglědowańskej historiji</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Rozšyŕśo wuwijarske rědy, aby pśistup k swójim datam we wócynjonych rejtarikach měł</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Wersija</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Awtor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Awtory</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Slědna aktualizacija</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Startowy bok</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Dalšne informacije wó pšawach</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Pógódnośenje</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Wěcej wó toś tom dodanku</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Wěcej wó toś tom rozšyrjenju</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Nastajenja</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Zašaltowany</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Wušaltowany</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">W priwatnem modusu dowóliś</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">W priwatnem modusu wuwjasć</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">W priwatnych woknach njedowólony</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Zmóžnjony</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Znjemóžnjony</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Zainstalěrowane</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Dopórucone</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Hyšće njepódprěte</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Njejo hyšći k dispoziciji</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Znjemóžnjony</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Drobnostki</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Pšawa</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Wótwónoźeś</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">K wěsći daś</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s pśidaś?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s wó pśidatne pšawa pšosy.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Waša dowólnosć jo trěbna za:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Póžedane pšawa:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Pśidaś</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Dowóliś</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Wótpokazaś</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Pśetergnuś</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Dodank instalěrowaś</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s instalěrowaś</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Pśetergnuś</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Pógódnośenja: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Pógódnośenje: %1$.02f z 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Dodanki</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Zastojnik dodankow</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Dodanki su nachylu znjemóžnjone</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Rozšyrjenja su nachylu znjemóžnjone</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Jaden dodank abo někotare dodanki wěcej njefunkcioněruju a waš system jo něnto instabilny.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Jadno rozšyrjenje abo někotare rozšyrjenja wěcej njefunkcioněruju a waš system jo něnto instabilny.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Dodanki znowego startowaś</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Rozšyrjenja znowego startowaś</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Dalšne dodanki pytaś</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Dalšne rozšyrjenja pytaś</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Dowóliś</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Wótpokazaś</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ma nowu aktualizaciju</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Licba nowych trěbnych pšawow: %1$d</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Nowe pšawo jo trěbne</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Aktualizacije dodankow</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Aktualizacije rozšyrjenjow</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Kontrola pódprětych dodankow</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nowy dodank k dispoziciji</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nowe dodanki k dispoziciji</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s %1$s pśidaś</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s %1$s a %2$s pśidaś</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Je %1$s pśidaś</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Dodankowa technologija Firefox se modernizěrujo. Toś te dodanki programěrowańsku pótwaŕ wužywaju, kótarež njejsu z Firefox 75 a nowšymi kompatibelne.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Napórajomy tuchylu pódpěru za zachopny wuběr dopóruconych rozšyrjenjow.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Dodank se ześěgujo a pśeglědujo…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Rozšyrjenje se ześěgujo a pśeglědujo…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Dodanki njejsu dali se napšašowaś!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Rozšyrjenja njejsu dali se napšašowaś!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Pśełožk njejo se namakał, daniž za lokale %1$s daniž za standardnu rěc %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s jo se wuspěšnje instalěrował</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s njedajo se instalěrowaś</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Toś ten dodank njedajo se instalěrowaś.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Toś to rozšyrjenje njedajo se instalěrowaś.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Toś ten dodank njedajo se zwiskoweje zmólki dla ześěgnuś.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Toś to rozšyrjenje njedajo se zwiskoweje zmólki dla ześěgnuś.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Toś ten dodank njedajo se instalěrowaś, dokulaž zda se, až jo wobškóźony.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Toś to rozšyrjenje njedajo se instalěrowaś, dokulaž zda se, až jo wobškóźone.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Toś ten dodank njedajo se instalěrowaś, dokulaž njejo pśeglědany.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Toś to rozšyrjenje njedajo se instalěrowaś, dokulaž njejo pśeglědane.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s njedajo se instalěrowaś, dokulaž njejo z %2$s %3$s kompatibelny.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s njedajo se instalěrowaś, dokulaž jo wjelike riziko, až zawinujo stabilnostne abo wěstotne problemy.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s jo se wuspěšnje zmóžnił</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s njedajo se zmóžniś</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s jo se wuspěšnje znjemóžnił</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s njedajo se znjemóžniś</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s jo se wuspěšnje wótinstalěrował</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s njedajo se wótinstalěrowaś</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s jo se wuspěšnje wótwónoźeł</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s njedajo se wótwónoźeś</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Toś ten dodank jo se pśenjasł wót pjerwjejšneje wersije %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Dodank: 1</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 rozšyrjenje</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">Dodanki: %1$s</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">Rozšyrjenja: %1$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Dalšne informacije</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Wuspěšnje zaktualizěrowany</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Aktualizacija njejo k dispoziciji</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Zmólka</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informacije za aktualizaciju</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Slědny wopyt:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s jo se %2$s pśidał.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">W meniju wócyniś</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Mějśo pśistup k %1$s z menija %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">W pórěźe, som zrozměł</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">W pórěźe</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Dalšne informacije</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s jo se dla problemow wěstoty abo stabilnosći znjemóžnił.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s njedajo se ako wěsty wobkšuśiś a jo se znjemóžnił.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s njejo z wašeju wersiju %2$s (wersija %3$s) kompatibelny.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..88c8482168
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-el/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Ανάγνωση και τροποποίηση ρυθμίσεων απορρήτου</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Πρόσβαση στα δεδομένα σας για κάθε ιστότοπο</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Πρόσβαση στα δεδομένα σας για το %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Πρόσβαση στα δεδομένα σας για ιστοτόπους του τομέα %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Πρόσβαση στα δεδομένα σας σε άλλη 1 σελίδα</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Πρόσβαση στα δεδομένα σας σε %1$d ακόμη ιστοτόπους</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Πρόσβαση στα δεδομένα σας σε 1 ακόμα τομέα</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Πρόσβαση στα δεδομένα σας σε %1$d ακόμα τομείς</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d από %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Πρόσβαση στις καρτέλες προγράμματος περιήγησης</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Αποθήκευση απεριόριστων δεδομένων στον πελάτη</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Πρόσβαση στη δραστηριότητα περιήγησης κατά την πλοήγηση</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Ανάγνωση και τροποποίηση σελιδοδεικτών</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Ανάγνωση και τροποποίηση ρυθμίσεων προγράμματος περιήγησης</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Απαλοιφή πρόσφατου ιστορικού περιήγησης, cookie και σχετικών δεδομένων</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Λήψη δεδομένων από το πρόχειρο</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Εισαγωγή δεδομένων στο πρόχειρο</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Φραγή περιεχομένου σε οποιαδήποτε σελίδα</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Ανάγνωση του ιστορικού περιήγησής σας</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Λήψη αρχείων και ανάγνωση/τροποποίηση ιστορικού λήψεων του προγράμματος περιήγησης</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Άνοιγμα ληφθέντων αρχείων της συσκευής σας</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Ανάγνωση κειμένου από όλες τις ανοικτές καρτέλες</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Πρόσβαση στην τοποθεσία σας</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Πρόσβαση στο ιστορικό περιήγησης</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Εποπτεία χρήσης επεκτάσεων και διαχείριση θεμάτων</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Ανταλλαγή μηνυμάτων με εφαρμογές εκτός αυτής</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Εμφάνιση ειδοποιήσεων σε εσάς</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Παροχή κρυπτογραφικών υπηρεσιών ταυτοποίησης</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Έλεγχος ρυθμίσεων διαμεσολαβητή προγράμματος περιήγησης</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Πρόσβαση στις πρόσφατα κλεισμένες καρτέλες</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Απόκρυψη και εμφανιση καρτελών προγράμματος περιήγησης</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Πρόσβαση στο ιστορικό περιήγησης</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Επέκταση εργαλείων προγραμματιστή για πρόσβαση στα δεδομένα σας στις ανοικτές καρτέλες</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Έκδοση</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Δημιουργός</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Δημιουργοί</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Τελευταία ενημέρωση</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Αρχική σελίδα</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Μάθετε περισσότερα σχετικά με τα δικαιώματα</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Αξιολόγηση</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Περισσότερα για αυτό το πρόσθετο</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Σχετικά με αυτήν την επέκταση</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ρυθμίσεις</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ενεργό</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Ανενεργό</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Να επιτρέπεται στην ιδιωτική περιήγηση</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Εκτέλεση στην ιδιωτική περιήγηση</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Δεν επιτρέπεται στα ιδιωτικά παράθυρα</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ενεργά</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Ανενεργά</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Εγκατεστημένα</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Προτεινόμενα</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Δεν υποστηρίζεται ακόμη</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Δεν διατίθεται ακόμα</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Ανενεργά</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Λεπτομέρειες</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Δικαιώματα</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Αφαίρεση</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Αναφορά</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Προσθήκη του %1$s;</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">Το %1$s απαιτεί επιπρόσθετα δικαιώματα.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Απαιτεί την άδειά σας για:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Θέλει να:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Προσθήκη</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Αποδοχή</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Άρνηση</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Ακύρωση</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Εγκατάσταση προσθέτου</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Εγκατάσταση %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Ακύρωση</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Κριτικές: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Βαθμολογία: %1$.02f από 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Πρόσθετα</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Διαχείριση προσθέτων</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Τα πρόσθετα έχουν απενεργοποιηθεί προσωρινά</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Οι επεκτάσεις έχουν απενεργοποιηθεί προσωρινά</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Ένα ή περισσότερα πρόσθετα σταμάτησαν να λειτουργούν, καθιστώντας το σύστημά σας ασταθές.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Μία ή περισσότερες επεκτάσεις σταμάτησαν να λειτουργούν, καθιστώντας το σύστημά σας ασταθές.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Επανεκκίνηση προσθέτων</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Επαναφορά επεκτάσεων</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Εύρεση περισσότερων προσθέτων</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Εύρεση περισσότερων επεκτάσεων</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Αποδοχή</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Άρνηση</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Το %1$s έχει νέα ενημέρωση</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Απαιτούνται %1$d νέα δικαιώματα</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Απαιτείται ένα νέο δικαίωμα</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Ενημερώσεις προσθέτου</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Ενημερώσεις επεκτάσεων</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Έλεγχος υποστήριξης προσθέτων</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Νέο διαθέσιμο πρόσθετο</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Νέα διαθέσιμα πρόσθετα</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Προσθήκη του %1$s στο %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Προσθήκη των %1$s και %2$s στο %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Προσθήκη στο %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Η τεχνολογία των προσθέτων του Firefox εκσυγχρονίζεται. Αυτά τα πρόσθετα χρησιμοποιούν πλαίσια που δεν είναι συμβατά με το Firefox 75 και νεότερες εκδόσεις.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Αυτή τη στιγμή, προσθέτουμε υποστήριξη για μια αρχική συλλογή προτεινόμενων επεκτάσεων.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Λήψη και επαλήθευση προσθέτου…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Λήψη και επαλήθευση επέκτασης…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Αποτυχία ανάκτησης προσθέτων!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Αποτυχία ανάκτησης επεκτάσεων!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Δεν βρέθηκε μετάφραση για τη γλώσσα «%1$s» ούτε για την προεπιλεγμένη γλώσσα «%2$s»</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Επιτυχής εγκατάσταση του %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Αποτυχία εγκατάστασης του %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Αποτυχία εγκατάστασης αυτού του προσθέτου.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Αποτυχία εγκατάστασης επέκτασης.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Δεν ήταν δυνατή η λήψη αυτού του προσθέτου λόγω μιας αποτυχίας στη σύνδεση.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Δεν ήταν δυνατή η λήψη αυτής της επέκτασης λόγω μιας αποτυχίας στη σύνδεση.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Δεν ήταν δυνατή η εγκατάσταση αυτού του προσθέτου, επειδή φαίνεται να είναι κατεστραμμένο.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Δεν ήταν δυνατή η εγκατάσταση αυτής της επέκτασης, επειδή φαίνεται να είναι κατεστραμμένη.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Δεν ήταν δυνατή η εγκατάσταση αυτού του προσθέτου, επειδή δεν έχει επαληθευτεί.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Δεν ήταν δυνατή η εγκατάσταση αυτής της επέκτασης, επειδή δεν έχει επαληθευτεί.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Δεν ήταν δυνατή η εγκατάσταση του %1$s, επειδή δεν είναι συμβατό με το %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Δεν είναι δυνατή η εγκατάσταση του %1$s, επειδή είναι πολύ πιθανό να προκαλέσει προβλήματα σταθερότητας ή ασφάλειας.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Επιτυχής ενεργοποίηση του %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Αποτυχία ενεργοποίησης του %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Επιτυχής απενεργοποίηση του %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Αποτυχία απενεργοποίησης του %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Επιτυχής απεγκατάσταση του %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Αποτυχία απεγκατάστασης του %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Επιτυχής αφαίρεση του %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Αποτυχία αφαίρεσης του %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Αυτό το πρόσθετο μεταφέρθηκε από προηγούμενη έκδοση του %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 πρόσθετο</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 επέκταση</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s πρόσθετα</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s επεκτάσεις</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Μάθετε περισσότερα</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Επιτυχής ενημέρωση</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Καμία διαθέσιμη ενημέρωση</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Σφάλμα</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Πληροφορίες ενημέρωσης</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Τελευταία προσπάθεια:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Κατάσταση:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Το %1$s έχει προστεθεί στο %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Άνοιγμα στο μενού</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Αποκτήστε πρόσβαση στο %1$s από το μενού του %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Εντάξει</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">ΟΚ</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Μάθετε περισσότερα</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Το %1$s έχει απενεργοποιηθεί λόγω ζητημάτων ασφαλείας ή σταθερότητας.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Το %1$s δεν ήταν δυνατό να επαληθευτεί ως ασφαλές και έχει απενεργοποιηθεί.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Το %1$s δεν είναι συμβατό με την έκδοση του %2$s σας (έκδοση %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..c11cbec900
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Read and modify privacy settings</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Access your data for all websites</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Access your data for %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Access your data for sites in the %1$s domain</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Access your data on 1 other site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Access your data on %1$d other sites</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Access your data on 1 other domain</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Access your data on %1$d other domains</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d of %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Access browser tabs</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Store unlimited amount of client-side data</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Access browser activity during navigation</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Read and modify bookmarks</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Read and modify browser settings</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Clear recent browsing history, cookies, and related data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Get data from the clipboard</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Input data to the clipboard</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Block content on any page</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Read your browsing history</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Download files and read and modify the browser’s download history</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Open files downloaded to your device</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Read the text of all open tabs</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Access your location</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Access browsing history</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitor extension usage and manage themes</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Exchange messages with apps other that this one</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Display notifications to you</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Provide cryptographic authentication services</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Control browser proxy settings</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Access recently closed tabs</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Hide and show browser tabs</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Access browsing history</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Extend developer tools to access your data in open tabs</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Author</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Authors</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Last updated</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Homepage</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Learn more about permissions</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Rating</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">More about this add-on</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">More about this extension</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Settings</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">On</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Off</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Allow in private browsing</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Run in private browsing</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Not allowed in private windows</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Enabled</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Disabled</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installed</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recommended</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Not yet supported</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Not yet available</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Disabled</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Details</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissions</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Remove</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Report</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Add %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s requests additional permissions.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">It requires your permission to:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">It wants to:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Add</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Allow</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Deny</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancel</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Install Add-on</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Install %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancel</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Reviews: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Rating: %1$.02f out of 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Add-ons Manager</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons are temporarily disabled</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Extensions are temporarily disabled</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">One or more add-ons stopped working, making your system unstable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">One or more extensions stopped working, making your system unstable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Restart add-ons</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Restart extensions</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Find more add-ons</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Find more extensions</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Allow</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Deny</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s has a new update</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d new permissions are required</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">A new permission is required</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Add-on updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Extension updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Supported add-ons checker</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">New add-on available</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">New add-ons available</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Add %1$s to %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Add %1$s and %2$s to %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Add them to %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox add-on technology is modernizing. These add-ons use frameworks that are not compatible with Firefox 75 &amp; beyond.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">We‘re currently building support for an initial selection of Recommended Extensions.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Downloading and verifying add-on…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Downloading and verifying extension…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Failed to query Add-ons!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Failed to query Extensions!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Translation not found, for locale %1$s neither default language %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Successfully installed %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Failed to install %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Failed to install this add-on.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Failed to install this extension.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be downloaded because of a connection failure.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">This extension could not be downloaded because of a connection failure.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be installed because it appears to be corrupt.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">This extension could not be installed because it appears to be corrupt.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be installed because it has not been verified.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">This extension could not be installed because it has not been verified.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s could not be installed because it is not compatible with %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s could not be installed because it has a high risk of causing stability or security problems.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Successfully enabled %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Failed to enable %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Successfully disabled %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Failed to disable %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Successfully uninstalled %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Failed to uninstall %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Successfully removed %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Failed to remove %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">This add-on was migrated from a previous version of %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extension</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s add-ons</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensions</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Learn more</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Successfully updated</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No update available</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Updater Information</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Last attempt:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s has been added to %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Open it in the menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Access %1$s from the %2$s menu.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Okay, Got it</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Learn more</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s has been disabled due to security or stability issues.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s could not be verified as secure and has been disabled.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s is not compatible with your version of %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..37d8dd33f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Read and modify privacy settings</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Access your data for all web sites</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Access your data for %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Access your data for sites in the %1$s domain</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Access your data on 1 other site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Access your data on %1$d other sites</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Access your data on 1 other domain</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Access your data on %1$d other domains</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d of %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Access browser tabs</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Store unlimited amount of client-side data</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Access browser activity during navigation</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Read and modify bookmarks</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Read and modify browser settings</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Clear recent browsing history, cookies, and related data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Get data from the clipboard</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Input data to the clipboard</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Block content on any page</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Read your browsing history</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Download files and read and modify the browser’s download history</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Open files downloaded to your device</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Read the text of all open tabs</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Access your location</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Access browsing history</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitor extension usage and manage themes</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Exchange messages with apps other that this one</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Display notifications to you</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Provide cryptographic authentication services</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Control browser proxy settings</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Access recently closed tabs</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Hide and show browser tabs</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Access browsing history</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Extend developer tools to access your data in open tabs</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Author</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Authors</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Last updated</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Homepage</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Learn more about permissions</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Rating</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">More about this add-on</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">More about this extension</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Settings</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">On</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Off</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Allow in private browsing</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Run in private browsing</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Not allowed in private windows</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Enabled</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Disabled</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installed</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recommended</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Not yet supported</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Not yet available</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Disabled</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Details</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissions</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Remove</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Report</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Add %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s requests additional permissions.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">It requires your permission to:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">It wants to:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Add</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Allow</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Deny</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancel</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Install Add-on</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Install %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancel</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Reviews: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Rating: %1$.02f out of 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Add-ons Manager</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons are temporarily disabled</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Extensions are temporarily disabled</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">One or more add-ons stopped working, making your system unstable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">One or more extensions stopped working, making your system unstable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Restart add-ons</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Restart extensions</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Find more add-ons</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Find more extensions</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Allow</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Deny</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s has a new update</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d new permissions are required</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">A new permission is required</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Add-on updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Extension updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Supported add-ons checker</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">New add-on available</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">New add-ons available</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Add %1$s to %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Add %1$s and %2$s to %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Add them to %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox add-on technology is modernising. These add-ons use frameworks that are not compatible with Firefox 75 &amp; beyond.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">We‘re currently building support for an initial selection of Recommended Extensions.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Downloading and verifying add-on…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Downloading and verifying extension…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Failed to query Add-ons!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Failed to query Extensions!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Translation not found, for locale %1$s neither default language %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Successfully installed %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Failed to install %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Failed to install this add-on.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Failed to install this extension.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be downloaded because of a connection failure.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">This extension could not be downloaded because of a connection failure.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be installed because it appears to be corrupt.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">This extension could not be installed because it appears to be corrupt.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be installed because it has not been verified.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">This extension could not be installed because it has not been verified.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s could not be installed because it is not compatible with %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s could not be installed because it has a high risk of causing stability or security problems.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Successfully enabled %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Failed to enable %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Successfully disabled %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Failed to disable %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Successfully uninstalled %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Failed to uninstall %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Successfully removed %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Failed to remove %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">This add-on was migrated from a previous version of %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extension</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s add-ons</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensions</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Learn more</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Successfully updated</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No update available</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Updater Information</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Last attempt:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s has been added to %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Open it in the menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Access %1$s from the %2$s menu.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Okay, Got it</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Learn more</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s has been disabled due to security or stability issues.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s could not be verified as secure and has been disabled.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s is not compatible with your version of %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..19c630e4a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-eo/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Legi kaj modifi privatecajn agordojn</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Aliri viajn datumojn por ĉiuj retejoj</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Aliri viajn datumojn por %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Aliri viajn datumojn por retejoj en la nomregno %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Aliri viajn datumojn por alia retejo</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Aliri viajn datumojn por %1$d aliaj retejoj</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Aliri viajn datumojn de alia nomregno</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Aliri viajn datumojn de %1$d aliaj nomregnoj</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d el %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Aliri retumilajn langetojn</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Senlime konservi datumojn en la kliento</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Aliri la agojn de la retumilo dum retumo</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Legi kaj modifi legosignojn</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Legi kaj modifi retumilajn agordojn</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Viŝi ĵusan retuman historion, kuketojn kaj rilatatajn datumojn</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Ricevi datumojn el la tondujo</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Meti datumojn en la tondujon</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloki enhavon en iu ajn paĝo</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Legi vian retuman historion</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Elŝuti dosierojn, legi kaj modifi la elŝutan historion de la retumilo</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Malfermi dosierojn elŝutitaj al via aparato</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Legi la tekston de ĉiuj malfermitaj langetoj</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Aliri vian pozicion</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Aliri la retuman historion</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Kontroli uzon de etendaĵojn kaj administri etosojn</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Interŝanĝi mesaĝojn kun alia programoj</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Montri sciigojn al vi</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Provizi ĉifritajn aŭtentikigajn servojn</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Regi retperantajn agordoj por retumilo</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Aliri ĵuse fermitajn langetojn</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Kaŝi kaj montri langetojn de retumilo</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Aliri la retuman historion</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Etendi la ilojn por programistoj por povi aliri viajn datumojn en malfermitaj langetoj</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versio</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Aŭtoro</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Aŭtoroj</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Laste ĝisdatigita</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Eka paĝo</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Pli da informo pri permesoj</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Taksado</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Pli da informo pri tiu ĉi aldonaĵo</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Agordoj</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ŝaltita</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Malŝaltita</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permesi en privata retumo</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Ŝalti en privata retumo</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Ne permesata en privataj fenestroj</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Aktiva</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Malaktiva</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalita</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Rekomenditaj</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ankoraŭ ne subtenata</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ankoraŭ ne disponebla</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Malaktiva</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detaloj</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permesoj</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Forigi</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Raporto</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Ĉu aldoni %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s petas aldonajn permesojn.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ĝi postulas vian permeson por:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Ĝi volas:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Aldoni</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permesi</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Rifuzi</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Nuligi</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instali aldonaĵon</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instali %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Nuligi</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Revizioj: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Noto: %1$.02f el 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Aldonaĵoj</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Administrilo de aldonaĵoj</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">La aldonaĵoj estas provizore malaktivigitaj</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">Unu aŭ pli da aldonaĵoj ĉesis funkcii, kaj tio igas vian sistemon nestabila.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Restartigi aldonaĵojn</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Serĉi pli da aldonaĵoj</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permesi</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Rifuzi</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s havas novan ĝisdatigon</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d postulas novajn permesojn</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Nova permeso postulata</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Ĝisdatigoj de aldonaĵoj</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Kontrolo de subtenataj aldonaĵoj</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Nova aldonaĵo disponebla</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Novaj aldonaĵoj disponeblaj</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Aldoni %1$s al %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Aldoni %1$s kaj %2$s al %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Aldoni ilin al %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">La teknologio de aldonaĵoj de Firefox moderniĝas. Tiuj ĉi aldonaĵoj uzas teknologiojn kiuj ne kongruas kun Firefox 75 kaj ĝiaj postaj versioj.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Ni nun kreas la unuan liston kun elektitaj rekomenditaj etendaĵoj.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Aldonaĵo elŝutata kaj kontrolata…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Ne eblis akiri la liston de aldonaĵoj!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Traduko netrovita, nek por la lokaĵaro %1$s nek por la norma lingvo %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s sukcese instalita</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Malsukcesa instalo de %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Malsukcesa instalo de tiu ĉi aldonaĵo.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">La aldonaĵo ne povis esti elŝutita pro eraro en la konekto.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Tiu ĉi aldonaĵo ne povis esti instalita ĉar ĝi aspektas difektite.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Tiu ĉi aldonaĵo ne povis esti instalita ĉar ĝi ne estas kontrolita.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s ne povi esti instalita ĉar ĝi ne kongruas kun %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s ne povis esti instalita ĉar ĝi tre riskas okazigi stabilecajn aŭ sekurecajn problemojn.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s sukcese aktivigita</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Malsukcesa aktivigo de %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s sukcese malaktivigita</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Malsukcesa malaktivigo de %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s sukcese malinstalita</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Malsukcesa malinstalo de %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s sukcese forigita</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Malsukcesa forigo de %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Tiu ĉi aldonaĵo migris el antaŭa versio de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 aldonaĵo</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s aldonaĵoj</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Pli da informo</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Sukcese ĝisdatigita</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Neniu ĝisdatigo disponebla</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Eraro</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informoj pri ĝisdatigo</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Lasta klopodo:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stato:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s estis aldonita al %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Malfermi ĝin en la menuo</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Aliri %1$s el la menuo de %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">En ordo, mi komprenis</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Akcepti</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Pli da informo</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s estis malaktivigita pro sekurecaj kaj stabilecaj problemoj.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Ne eblis konfirmi ke %1$s estas sekura kaj ĝi estis do malaktivigita.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s ne kongruas kun via version de %2$s (versio %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..eda766c8a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leer y modificar configuración de privacidad</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a los datos para todos los sitios web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceso a los datos de %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceso a los datos para los sitios en el dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceso a los datos en otro sitio</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceso a los datos en %1$d otros sitios</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceso a los datos en otro dominio</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceso a los datos en %1$d otros dominios</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder a pestañas del navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Almacenar una cantidad ilimitada de datos del lado del cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder a la actividad del navegador durante la navegación</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leer y modificar marcadores</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leer y modificar los ajustes del navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Borrar el historial de navegación reciente, cookies y datos relacionados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obtener datos del portapapeles</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Ingresar datos al portapapeles</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquear contenido en cualquier página</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Leer historial de navegación</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Descargar archivos y leer o modificar el historial de descargas del navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir archivos descargados en tu dispositivo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leer el texto de todas las pestañas abiertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a tu ubicación</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder al historial de navegación</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorear uso de extensiones y administrar temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercambiar mensajes con aplicaciones diferentes a esta</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Mostrarme notificaciones</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Proporcionar servicios de autenticación criptográfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar la configuración del proxy del navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder a las pestañas cerradas recientemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ocultar y mostrar las pestañas del navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder al historial de navegación</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ampliar las herramientas para desarrolladores para acceder a tus datos en las pestañas abiertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Última actualización</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Página de inicio</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Conocer más sobre permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Clasificación</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Más sobre este complemento</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Más sobre esta extensión</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Configuración</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Desactivada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir en navegación privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Ejecutar en navegación privada</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">No permitido en ventanas privadas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Habilitado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Deshabilitado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Todavía no es compatible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Todavía no está disponible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Deshabilitado</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalles</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Eliminar</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Informar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿Agregar %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s solicita permisos adicionales.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Se necesita permiso para:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Quiere:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Agregar</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitir</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Denegar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instalar complemento</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instalar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Revisiones: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Calificación: %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Administrador de complementos</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Los complementos están deshabilitados temporalmente</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Las extensiones están temporalmente deshabilitadas</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Uno o más complementos dejaron de funcionar, haciendo que el sistema sea inestable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Una o más extensiones dejaron de funcionar, haciendo que el sistema sea inestable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reiniciar complementos</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reiniciar extensiones</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Buscar más complementos</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Buscar mas extensiones</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denegar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tiene una nueva actualización</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Se necesitan %1$d nuevos permisos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Se necesita un nuevo permiso</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Actualizaciones de complementos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Actualizaciones de extensiones</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Verificador de complementos compatibles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nuevo complemento disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nuevos complementos disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Agregar %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Agregar %1$s y %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Agregarlos a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La tecnología de los complementos de Firefox se está modernizando. Estos complementos usan marcos que no son compatibles con Firefox 75 y posteriores.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Ahora estamos creando soporte para una selección inicial de extensiones recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Descargando e instalando complemento…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Descargando y verificando la extensión…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">¡Error al solicitar complementos!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">¡Error al solicitar extensiones!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">No se encontró la traducción para la localización %1$s ni el idioma predeterminado %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalado correctamente</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Falló la instalación de %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Error al instalar este complemento.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Error al instalar esta extensión.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no se pudo descargar por una falla en la conexión.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Esta extensión no se pudo descargar por una falla en la conexión.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no pudo instalarse porque parece estar corrupto.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Esta extensión no se pudo instalar porque parece estar dañada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no pudo ser instalado porque no fue verificado.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Esta extensión no se pudo instalar porque no fue verificada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s no pudo instalarse porque no es compatible con %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s no se pudo instalar porque tiene un riesgo alto de causar problemas de estabilidad o seguridad.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s habilitado exitosamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Falló la habilitación de %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s deshabilitado exitosamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Falló la deshabilitación de %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s desinstalado exitosamente</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Falló la desinstalación de %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s eliminado exitosamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Falló la eliminación de %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Este complemento fue migrado desde una versión anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 complemento</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensión</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s complementos</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s complementos</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Conocer más</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Se actualizó correctamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No hay actualización disponible</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Información de actualización</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Último intento:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estado:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Se agregó %1$s a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Abrirlo en el menú</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Acceder a %1$s desde el menú de %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Listo, entendido</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Aceptar</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Conocer más</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ha sido deshabilitado debido a problemas de seguridad o estabilidad.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s no se pudo verificar como seguro y fue deshabilitado.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s no es compatible con tu versión de %2$s (versión %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..b0526b5eaa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leer y modificar ajustes de privacidad</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a tus datos para todos los sitios</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder a tus datos para %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder a tus datos para sitios en el dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceder a tus datos en 1 sitio más</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceder a tus datos en %1$d sitios más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceder a tus datos en 1 dominio más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceder a tus datos en %1$d dominios más</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder a las pestañas del navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Almacenar una cantidad ilimitada de datos en el lado del cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder a la actividad de navegación durante la navegación</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leer y modificar marcadores</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leer y modificar ajustes del navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpiar historial de navegación reciente, cookies y datos relacionados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obtener datos del portapapeles</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Enviar datos al portapapeles</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquear contenido en cualquier página</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lee tu historial de navegación</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Descargar archivos y leer y modificar el historial de descargas del navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir archivos descargados en tu dispositivo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leer el texto de todas las pestañas abiertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a tu ubicación</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder al historial de navegación</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorea el uso de extensiones y gestiona temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercambiar mensajes con aplicaciones aparte de esta</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Mostrarte notificaciones</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Proporciona servicios de autentificación criptográfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controla los ajustes de proxy del navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Accede a pestañas cerradas recientemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Oculta y muestra las pestañas del navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Accede al historial de navegación</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Extiende las herramientas de desarrollo para acceder a tus datos en las pestañas abiertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Última actualización</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Sitio web</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Aprender más acerca de los permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Clasificación</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Más acerca de este complemento</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Más acerca de esta extensión</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ajustes</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Sí</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">No</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir en navegación privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Ejecutar en navegación privada</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">No permitido en ventanas privadas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Desactivado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Todavía no compatible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Aún no disponible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desactivado</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalles</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Eliminar</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Reportar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿Añadir %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s requiere permisos adicionales.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Requiere tu permiso para:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Quiere:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Añadir</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitir</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Denegar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instalar complemento</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instalar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Reseñas: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Calificación: %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Administrador de complementos</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Los complementos están temporalmente deshabilitados</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Las extensiones están temporalmente deshabilitadas</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Uno o más complementos dejaron de funcionar, lo que hizo que tu sistema fuera inestable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Una o más extensiones dejaron de funcionar, lo que hizo que tu sistema fuera inestable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reiniciar complementos</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reiniciar extensiones</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Encuentra más complementos</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Buscar más extensiones</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denegar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tiene una nueva actualización</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Se requieren %1$d nuevos permisos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Se requiere un nuevo permiso</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Actualizaciones de complementos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Actualizaciones de extensiones</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Comprobador de complementos compatibles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nuevo complemento disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nuevos complementos disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Añadir %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Añadir %1$s y %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Añadirlos a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La tecnología de los complementos de Firefox se está modernizando. Estos complementos usan marcos de trabajo que no son compatibles con Firefox 75 en adelante.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Estamos construyendo las bases para una selección inicial de extensiones recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Descargando y verificando complemento…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Descargando y verificando la extensión…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">¡Error al consultar complementos!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">¡Error al consultar extensiones!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">No se encontró la traducción para la localización %1$s ni para el idioma predeterminado %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalado correctamente</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Falló la instalación de %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Falló la instalación de este complemento.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Falló la instalación de esta extensión.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no pudo ser descargado por una falla en la conexión.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Esta extensión no pudo ser descargada por una falla en la conexión.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no pudo ser instalado porque parece estar corrupto.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Esta extensión no pudo ser instalada porque parece estar corrupta.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no pudo ser instalado porque no ha sido verificado.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Esta extensión no pudo ser instalada porque no ha sido verificada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s no pudo ser instalado porque no es compatible con %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s no pudo ser instalado porque tiene un alto riesgo de causar problemas de estabilidad o seguridad.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s activado correctamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Fallo la activación de %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s desactivado correctamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Falló la desactivación de %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s desinstalado correctamente</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Falló la desinstalación de %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s eliminado correctamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Falló la eliminación de %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Este complemento fue migrado desde una versión anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 complemento</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensión</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s complementos</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensiones</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Aprender más</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualizado exitosamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No hay actualización disponible</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Información de actualización</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Último intento:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estado:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ha sido añadido a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Ábrelo en el menú</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Accede a %1$s desde el menú %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, ¡ya caché!</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Aceptar</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Aprender más</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ha sido deshabilitado debido a problemas de seguridad o estabilidad.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s no pudo ser verificado como seguro y ha sido desactivado.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s no es compatible con tu versión de %2$s (versión %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..d70784d7f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leer y modificar configuración de privacidad</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a tus datos de todos los sitios web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder a tus datos para %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder a tus datos de sitios en el dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceder a tus datos en 1 sitio más</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceder a tus datos en %1$d sitios más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceder a tus datos en 1 dominio más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceder a tus datos en %1$d dominios más</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder a las pestañas del navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Almacenar una cantidad ilimitada de datos en el lado del cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder a la actividad del navegador durante la navegación</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leer y modificar marcadores</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leer y modificar ajustes del navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpiar el historial de navegación reciente, cookies y datos relacionados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obtener datos del portapapeles</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Introducir datos en el portapapeles</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquear contenido en cualquier página</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Leer historial de navegación</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Descargar archivos y leer y modificar el historial de descargas del navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir archivos descargados en tu equipo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leer el texto de todas las pestañas abiertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a tu ubicación</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder al historial de navegación</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorizar el uso de extensiones y administrar temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercambiar mensajes con otras aplicaciones distintas</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Mostrarte notificaciones</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Proporcionar servicios de autenticación criptográfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar la configuración proxy del navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder a las pestañas cerradas recientemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ocultar y mostrar pestañas del navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder al historial de navegación</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ampliar las herramientas para desarrolladores para acceder a tus datos en las pestañas abiertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Última actualización</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Página de inicio</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saber más sobre permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Calificación</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Más acerca de este complemento</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Más acerca de esta extensión</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ajustes</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Desactivado</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir en navegación privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Ejecutar en navegación privada</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">No permitido en ventanas privadas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Desactivado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Todavía no es compatible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">No disponible todavía</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desactivado</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalles</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Eliminar</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Informe</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿Añadir %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s solicita permisos adicionales.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Se necesita tu permiso para:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Quiere:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Añadir</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitir</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Denegar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instalar complemento</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instalar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Revisiones: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Calificación: %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Administrador de complementos</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Los complementos están desactivados temporalmente</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Las extensiones están desactivadas temporalmente</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Uno o más complementos dejaron de funcionar, haciendo que el sistema sea inestable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Una o más extensiones dejaron de funcionar, haciendo que el sistema sea inestable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reiniciar complementos</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reiniciar extensiones</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Buscar más complementos</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Encontrar más extensiones</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denegar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tiene una nueva actualización</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Se requieren %1$d nuevos permisos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Se requiere un nuevo permiso</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Actualizaciones de complementos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Actualizaciones de extensiones</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Comprobador de complementos compatibles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nuevo complemento disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nuevos complementos disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Añadir %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Añadir %1$s y %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Añadirlos a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La tecnología de los complementos de Firefox se está modernizando. Estos complementos usan frameworks que no son compatibles con Firefox 75 y posteriores.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">En este momento estamos construyendo las bases para una selección inicial de extensiones recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Descargando y verificando el complemento…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Descargando y verificando extensión…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">¡Error al solicitar complementos!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">¡Error al consultar extensiones!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">No se ha encontrado la traducción para la localización %1$s ni para el idioma predeterminado %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalado correctamente</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Fallo al instalar %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">No se ha podido instalar este complemento.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Ha fallado la instalación de esta extensión.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no se ha podido descargar debido a un fallo de conexión.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Esta extensión no se ha podido descargar debido a un fallo de conexión.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no ha podido ser instalado porque parece que está dañado.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Esta extensión no ha podido ser instalada porque parece que está dañada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no ha podido ser instalado porque no ha sido verificado.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Esta extensión no ha podido ser instalada porque no ha sido verificada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s no ha podido ser instalado porque no es compatible con %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s no ha podido ser instalado porque tiene un alto riesgo de causar problemas de estabilidad o seguridad.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s activado correctamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Fallo al activar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s desactivado correctamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Fallo al desactivar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s desinstalado correctamente</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Fallo al desinstalar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s eliminado correctamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Fallo al eliminar %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Este complemento se migró desde una versión anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 complemento</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensión</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s complementos</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensiones</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saber más</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualizado correctamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No hay actualización disponible</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Información de actualización</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Último intento:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estado:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ha sido añadido a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Ábrelo en el menú</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Accede a %1$s desde el menú %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Vale, entendido</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Aceptar</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Saber más</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ha sido desactivado debido a problemas de seguridad o estabilidad.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s no ha podido ser verificado para como seguro y ha sido desactivado.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s no es compatible con tu versión de %2$s (versión %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..1a1a2eedc9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leer y modificar ajustes de privacidad</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a tus datos para todos los sitios web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder a tus datos para %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder a tus datos para los sitios del dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceder a tus datos en 1 sitio más</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceder a tus datos en %1$d sitios más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceder a tus datos en 1 dominio más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceder a tus datos en %1$d dominios más</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder a las pestañas del navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Almacenar una cantidad ilimitada de datos en el lado del cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder a la actividad del navegador durante la navegación</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leer y modificar marcadores</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leer y modificar ajustes del navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpiar el historial de navegación reciente, cookies y datos relacionados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obtener datos del portapapeles</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Ingresar datos en el portapapeles</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquear contenido en cualquier página</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lee tu historial de navegación</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Descargar archivos y leer y modificar el historial de descargas del navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir archivos descargados en tu equipo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leer el texto de todas las pestañas abiertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a tu ubicación</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder al historial de navegación</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorear el uso de extensiones y administrar temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercambiar mensajes con otras aplicaciones distintas</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Mostrarte notificaciones</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Proporcionar servicios de autenticación criptográfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar la configuración proxy del navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder a las pestañas cerradas recientemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ocultar y mostrar pestañas del navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder al historial de navegación</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ampliar las herramientas para desarrolladores para acceder a tus datos en las pestañas abiertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Última actualización</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Página de inicio</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saber más sobre permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Clasificación</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Más sobre este complemento</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ajustes</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Desactivado</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir en la navegación privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Ejecutar en navegación privada</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Habilitado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Deshabilitado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalados</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendados</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">No compatibles</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Aún no está disponible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Deshabilitados</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalles</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Eliminar</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Reportar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿Agregar %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s solicita permisos adicionales.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Se requiere tu permiso para:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Quiere:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Agregar</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitir</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Denegar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Instalar complemento</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Reseñas: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Administrador de complementos</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">Los complementos están desactivados temporalmente</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">Uno o más complementos dejaron de funcionar, lo que hizo que tu sistema fuera inestable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Reiniciar complementos</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Encontrar más complementos</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denegar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tiene una actualización nueva</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Se requieren %1$d permisos nuevos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Se requiere un permiso nuevo</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Actualizaciones de complementos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Inspector de complementos compatibles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Complemento nuevo disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Complementos nuevos disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Agregar %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Agregar %1$s y %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Agregarlos a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Se moderniza la tecnología de complementos de Firefox. Estos complementos utilizan marcos no compatibles desde Firefox 75 y más allá.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Ahora estamos creando soporte para una selección inicial de extensiones recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Descargando y verificando complemento…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">¡Error al consultar complementos!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">No se encontró la traducción para la localización %1$s ni en el idioma predeterminado %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalado correctamente</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Fallo al instalar %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">No se pudo instalar este complemento.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">No se pudo descargar este complemento debido a una falla en la conexión.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Este complemento no pudo ser instalado porque parece estar dañado.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Este complemento no pudo ser instalado porque no ha sido verificado.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s no pudo ser instalado porque no es compatible con %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s no pudo ser instalado porque tiene un alto riesgo de causar problemas de estabilidad o seguridad.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s habilitado correctamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Fallo al habilitar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s deshabilitado correctamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Fallo al deshabilitar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s desinstalado correctamente</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Fallo al desinstalar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s eliminado correctamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Fallo al eliminar %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Este complemento se migró desde una versión anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 complemento</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s complementos</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saber más</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualizado correctamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No hay actualización disponible</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Información de actualización</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Último intento:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estado:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ha sido agregado a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Abrir en el menú</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Listo, lo tengo</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Saber más</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ha sido deshabilitado debido a problemas de seguridad o estabilidad.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s no pudo ser verificado como seguro y ha sido desactivado.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s no es compatible con tu versión de %2$s (versión %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..d70784d7f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-es/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leer y modificar configuración de privacidad</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a tus datos de todos los sitios web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder a tus datos para %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder a tus datos de sitios en el dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceder a tus datos en 1 sitio más</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceder a tus datos en %1$d sitios más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceder a tus datos en 1 dominio más</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceder a tus datos en %1$d dominios más</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder a las pestañas del navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Almacenar una cantidad ilimitada de datos en el lado del cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder a la actividad del navegador durante la navegación</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leer y modificar marcadores</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leer y modificar ajustes del navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpiar el historial de navegación reciente, cookies y datos relacionados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obtener datos del portapapeles</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Introducir datos en el portapapeles</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquear contenido en cualquier página</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Leer historial de navegación</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Descargar archivos y leer y modificar el historial de descargas del navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir archivos descargados en tu equipo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leer el texto de todas las pestañas abiertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a tu ubicación</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder al historial de navegación</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorizar el uso de extensiones y administrar temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercambiar mensajes con otras aplicaciones distintas</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Mostrarte notificaciones</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Proporcionar servicios de autenticación criptográfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar la configuración proxy del navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder a las pestañas cerradas recientemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ocultar y mostrar pestañas del navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder al historial de navegación</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ampliar las herramientas para desarrolladores para acceder a tus datos en las pestañas abiertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Última actualización</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Página de inicio</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saber más sobre permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Calificación</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Más acerca de este complemento</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Más acerca de esta extensión</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ajustes</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Desactivado</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir en navegación privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Ejecutar en navegación privada</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">No permitido en ventanas privadas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Desactivado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Todavía no es compatible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">No disponible todavía</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desactivado</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalles</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Eliminar</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Informe</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿Añadir %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s solicita permisos adicionales.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Se necesita tu permiso para:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Quiere:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Añadir</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitir</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Denegar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instalar complemento</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instalar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Revisiones: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Calificación: %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Administrador de complementos</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Los complementos están desactivados temporalmente</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Las extensiones están desactivadas temporalmente</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Uno o más complementos dejaron de funcionar, haciendo que el sistema sea inestable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Una o más extensiones dejaron de funcionar, haciendo que el sistema sea inestable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reiniciar complementos</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reiniciar extensiones</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Buscar más complementos</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Encontrar más extensiones</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denegar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tiene una nueva actualización</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Se requieren %1$d nuevos permisos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Se requiere un nuevo permiso</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Actualizaciones de complementos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Actualizaciones de extensiones</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Comprobador de complementos compatibles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nuevo complemento disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nuevos complementos disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Añadir %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Añadir %1$s y %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Añadirlos a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La tecnología de los complementos de Firefox se está modernizando. Estos complementos usan frameworks que no son compatibles con Firefox 75 y posteriores.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">En este momento estamos construyendo las bases para una selección inicial de extensiones recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Descargando y verificando el complemento…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Descargando y verificando extensión…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">¡Error al solicitar complementos!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">¡Error al consultar extensiones!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">No se ha encontrado la traducción para la localización %1$s ni para el idioma predeterminado %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalado correctamente</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Fallo al instalar %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">No se ha podido instalar este complemento.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Ha fallado la instalación de esta extensión.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no se ha podido descargar debido a un fallo de conexión.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Esta extensión no se ha podido descargar debido a un fallo de conexión.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no ha podido ser instalado porque parece que está dañado.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Esta extensión no ha podido ser instalada porque parece que está dañada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento no ha podido ser instalado porque no ha sido verificado.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Esta extensión no ha podido ser instalada porque no ha sido verificada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s no ha podido ser instalado porque no es compatible con %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s no ha podido ser instalado porque tiene un alto riesgo de causar problemas de estabilidad o seguridad.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s activado correctamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Fallo al activar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s desactivado correctamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Fallo al desactivar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s desinstalado correctamente</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Fallo al desinstalar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s eliminado correctamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Fallo al eliminar %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Este complemento se migró desde una versión anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 complemento</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensión</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s complementos</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensiones</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saber más</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualizado correctamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No hay actualización disponible</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Información de actualización</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Último intento:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estado:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ha sido añadido a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Ábrelo en el menú</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Accede a %1$s desde el menú %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Vale, entendido</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Aceptar</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Saber más</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ha sido desactivado debido a problemas de seguridad o estabilidad.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s no ha podido ser verificado para como seguro y ha sido desactivado.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s no es compatible con tu versión de %2$s (versión %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..75b2b71a59
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-et/strings.xml
@@ -0,0 +1,237 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">privaatsussätete vaatamine ja muutmine</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ligipääs kõigi saitide salvestatud andmetele</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">ligipääs saidi %1$s andmetele</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">ligipääs aadressil %1$s töötavate saitide andmetele</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">ligipääs sinu andmetele ühel teisel saidil</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">ligipääs sinu andmetele %1$d teisel saidil</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">ligipääs sinu andmetele ühel teisel domeenil</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">ligipääs sinu andmetele %1$d teisel domeenil</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ligipääs kaartidele</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">piiramatu hulga andmete kliendi poolel salvestamine</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">ligipääs brauseri tegevusele veebilehitsemise vältel</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">järjehoidjate vaatamine ja muutmine</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">brauseri seadistuste vaatamine ja muutmine</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">hiljutise lehitsemise ajaloo, küpsiste ja seotud andmete kustutamine</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">andmete hankimine vahemälust</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">andmete sisestamine vahemällu</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lehitsemise ajaloo lugemine</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">failide allalaadimine ja allalaaditud failide ajaloo vaatamine ning muutmine</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">allalaaditud failide avamine</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">teksti lugemine kõigist avatud kaartidest</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ligipääs sinu asukohale</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ligipääs veebilehitsemise ajaloole</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">laienduste kasutamise jälgimine ja teemade haldamine</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">sõnumite vahetamine muude äppidega kui see</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">teavituste kuvamine</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">krüptitud autentimisteenuste osutamine</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">brauseri puhverserveri sätete haldamine</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">ligipääs hiljuti suletud kaartidele</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">brauseri kaartide peitmine ja kuvamine</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ligipääs veebilehitsemise ajaloole</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">arendajate tööriistade laiendamine, et pääseda ligi avatud kaartide andmetele</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versioon</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autorid</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Viimati uuendatud</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Koduleht</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Rohkem teavet õiguste kohta</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Hinnang</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Sätted</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Sees</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Väljas</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Lubatakse privaatse veebilehitsemise režiimis</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Käivitatakse privaatse veebilehitsemise režiimis</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Lubatud</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Keelatud</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Paigaldatud</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Soovitatud</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Pole veel toetatud</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Pole veel saadaval</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Keelatud</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Üksikasjad</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Õigused</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Eemalda</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Raporteeri</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Kas paigaldada lisa %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s nõuab täiendavaid õigusi.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">See lisa nõuab järgmisi õigusi:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Nõutavad õigused:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Paigalda</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Luba</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Keeldu</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Loobu</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Paigalda lisa</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Loobu</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Arvustusi: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Lisad</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Lisade haldur</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Avasta veel lisasid</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Luba</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Keela</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Lisale %1$s on uuendus</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Nõutakse %1$d uut õigust</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Nõutakse uut õigust</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Lisade uuendused</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Toetatud lisade kontrollija</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Uus lisa on saadaval</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Uued lisad on saadaval</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Paigalda %2$sile lisa %1$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Paigalda %3$sile lisad %1$s ja %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Paigalda need %1$sile</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefoxi lisade tehnoloogia on moderniseerumas. Need lisad kasutavad raamistikke, mis pole alates Firefoxi versioonist 75 enam ühilduvad.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Me alles loome tuge esialgsele valikule soovitatud lisadest.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Lisa allalaadimine ja verifitseerimine…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Lisade nimekirja hankimine ebaõnnestus!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Tõlget ei leitud, ei keelele %1$s ega ka vaikekeelele %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Lisa %1$s paigaldamine õnnestus</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Lisa %1$s paigaldamine ebaõnnestus</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Lisa paigaldamine ebaõnnestus.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">Lisa polnud ühenduse ebaõnnestumise tõttu võimalik alla laadida.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Lisa polnud võimalik paigaldada, kuna see on vigane.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Lisa polnud võimalik paigaldada, kuna see pole verifitseeritud.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Lisa %1$s polnud võimalik paigaldada, kuna see võib põhjustada tõsiseid stabiilsuse või turvalisuse probleeme.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Lisa %1$s lubamine õnnestus</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Lisa %1$s lubamine ebaõnnestus</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Lisa %1$s keelamine õnnestus</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Lisa %1$s keelamine ebaõnnestus</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Lisa %1$s eemaldamine õnnestus</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Lisa %1$s eemaldamine ebaõnnestus</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Lisa %1$s eemaldamine õnnestus</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Lisa %1$s eemaldamine ebaõnnestus</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">See lisa migreeriti eelmisest %1$si versioonist</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 lisa</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s lisa</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Rohkem teavet</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">edukalt uuendatud</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">uuendusi pole</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">viga</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Uuendaja teave</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Viimane katse:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Olek:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%2$sile paigaldati lisa %1$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Ava menüüs</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Olgu, sain aru</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..44fdf12323
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-eu/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Pribatutasun-ezarpenak irakurri eta aldatzea</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Webgune guztietako zure datuak atzitzea</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s ostalariko zure datuak atzitzea</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s domeinupeko guneetako zure datuak atzitzea</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Beste gune batean zure datuak atzitzea</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Beste %1$d gunetan zure datuak atzitzea</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Beste domeinu batean zure datuak atzitzea</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Beste %1$d domeinutan zure datuak atzitzea</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d of %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Nabigatzailearen fitxak atzitzea</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Bezeroan datu kopuru mugagabea biltegiratzea</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Nabigatu ahala nabigazio-jarduera atzitzea</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Laster-markak irakurri eta aldatzea</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Nabigatzailearen ezarpenak irakurri eta aldatzea</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Azken nabigazio-historia, cookieak eta erlazionatutako datuak ezabatzea</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Arbeletik datuak eskuratzea</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Arbelean datuak idaztea</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokeatu edozein orritako edukia</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Irakurri zure nabigatze-historia</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Fitxategiak deskargatzea eta nabigatzailearen deskarga-historia irakurri eta aldatzea</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Zure ordenagailura deskargatutako fitxategiak irekitzea</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Irekitako fitxa guztietako testua irakurtzea</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Zure kokapena atzitzea</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Nabigatze-historia atzitzea</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Gehigarrien erabilera monitorizatu eta itxurak kudeatzea</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Hau ez beste programekin mezuak trukatzea</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Zuri jakinarazpenak bistaratzea</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Autentifikazio-zerbitzu kriptografikoak eskaintzea</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Nabigatzailearen proxy-ezarpenak kontrolatzea</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Itxitako azken fitxak atzitzea</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Nabigatzaileko fitxak ezkutatu eta erakustea</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Nabigatze-historia atzitzea</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Garatzaile-tresnak hedatu eta irekitako fitxetako zure datuak atzitzea</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Bertsioa</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Egilea</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Egileak</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Azken eguneraketa</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Hasiera-orria</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Baimenei buruzko argibide gehiago</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Puntuazioa</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Gehiago gehigarri honi buruz</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ezarpenak</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Aktibatuta</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Desaktibatuta</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Baimendu nabigatze pribatuan</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Exekutatu nabigatze pribatuan</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Ez da leiho pribatuetan onartzen</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Gaituta</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Desgaituta</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalatuta</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Gomendatua</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ez da onartzen oraindik</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Oraindik ez dago erabilgarri</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desgaituta</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Xehetasunak</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Baimenak</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Kendu</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Jakinarazi</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Gehitu %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s gehigarriak aparteko baimenak eskatzen ditu.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Zure baimena behar du ondorengorako:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Ondorengoa egin nahi du:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Gehitu</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Baimendu</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Ukatu</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Utzi</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instalatu gehigarria</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instalatu %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Utzi</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Balorazioak: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">5/%1$.02f</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Balorazioa: 5etik %1$.02f</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Gehigarriak</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gehigarrien kudeatzailea</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">Gehigarriak aldi baterako desgaitu dira</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">Gehigarri bat edo gehiago matxuratu egin dira, zure sistema desegonkortuz.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Berrabiarazi gehigarriak</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Bilatu gehigarri gehiago</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Baimendu</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Ukatu</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s gehigarriak eguneraketa berri bat du</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d baimen gehiago behar dira</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Baimen berri bat behar da</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Gehigarrien eguneraketak</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Onartutako gehigarrien egiaztatzailea</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Gehigarri berria erabilgarri</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Gehigarri berriak erabilgarri</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Gehitu %1$s %2$s(e)ra</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Gehitu %1$s eta %2$s %3$s(e)ra</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Gehitu hauek %1$s(e)ra</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefoxen gehigarrien teknologia modernizatzen ari da. Ondorengo gehigarriek Firefox 75 eta berriagoekin bateragarriak ez diren framework-ak erabiltzen dituzte.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Gomendatutako hedapenen hasierako hautapena emateko euskarria eraikitzen ari gara une honetan.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Gehigarria deskargatzen eta egiaztatzen…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Ezin izan dira gehigarriak kontsultatu!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Ez da itzulpenik aurkitu, ez %1$s hizkuntzarentzat ez eta %2$s hizkuntza lehenetsiarentzat</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s ongi instalatu da</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Ezin izan da %1$s instalatu</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Ezin izan da gehigarri hau instalatu.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">Ezin izan da gehigarri hau deskargatu konexio-akats bat dela-eta.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Ezin izan da gehigarri hau instalatu hondatuta dagoela dirudielako.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Ezin izan da gehigarri hau instalatu egiaztatu gabea delako.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Ezin da %1$s instalatu ez delako %2$s %3$s bertsioarekin bateragarria.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Ezin izan da %1$s instalatu egonkortasun- eta segurtasun-arazoak eragiteko arrisku handia daukalako.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ongi gaitu da</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Ezin izan da %1$s gaitu</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s ongi desgaitu da</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Ezin izan da %1$s desgaitu</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s ongi desinstalatu da</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Ezin izan da %1$s desinstalatu</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s ongi kendu da</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Ezin izan da %1$s kendu</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Gehigarri hau %1$s(r)en aurreko bertsio batetik migratu da</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">Gehigarri 1</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s gehigarri</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Argibide gehiago</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Ondo eguneratuta</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ez dago eguneraketarik</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Errorea</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Eguneraketaren informazioa</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Azken saiakera:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Egoera:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s hedapena %2$s(e)ra gehitu da</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Ireki hedapena menuan</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Atzitu %1$s %2$s menutik.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ados, ulertuta</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Ados</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Argibide gehiago</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s desgaitu egin da segurtasun- edo egonkortasun-arazoengatik.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Ezin da %1$s gehigarria segurua denik egiaztatu eta desgaitu egin da.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s ez da bateragarria zure %2$s bertsioarekin (%3$s bertsioa).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..00ead80362
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-fa/strings.xml
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">خواندن و تغییر تنظیمات حریم خصوصی</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">دسترسی به تمامی اطلاعات شما برای تمامی پایگاه های اینترنتی</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">دسترسی به اطلاعات شما برای %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">دسترسی به داده‌های پایگاه‌ها در دامنهٔ %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">دسترسی به داده‌های شما روی 1 پایگاه دیگر</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">دسترسی به داده‌های شما روی %1$d پایگاه دیگر</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">دسترسی به داده‌های شما روی 1 دامنهٔ دیگر</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">دسترسی به داده‌های شما روی %1$d دامنهٔ دیگر</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">دسترسی به زبانه‌های مرورگر</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ذخیره مقدار نامحدودی دادهٔ سمت-مشتری</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">دسترسی به فعالیت ها در طی گشتن</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">خواندن و تنظیم نشانک‌ها</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">خواندن و تغییر تنظیمات مرورگر</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">پاک کردن تاریخچهٔ مرور اخیر،‌ کلوچک‌ها و اطلاعات مرتبط</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">دریافت داده از تخته‌گیره</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ورود داده به تخته‌گیره</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">محتوا را در هر صفحه‌ای مسدود کن</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">تاریخچه مرور خود را بخوانید</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">دریافت پرونده‌های تاریخچه دریافت ها و تنظیم و خواندن آن ها</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">بازکردن پرونده‌های دریافت ها بر روی کامپیوتر شما</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">خواندن متن تمام زبانه‌های باز</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">دسترسی به موقعیت مکانی شما</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">دسترسی به تاریخچه مرورکردن</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">پایش استفادهٔ افزونه‌ها و مدیریت زمینه‌ها</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">تبادل پیام با برنامه‌هایی جز این برنامه</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">نمایش هشدار بر روی صفحه برای شما</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">فراهم‌سازی خدمات احراز هویت رمزنگاری شده</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">واپایش تنظیمات پیشکار مرورگر</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">دسترسی به آخرین زبانه‌های بسته شده</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">پنهان کردن و نمایش زبانه‌های مرورگر</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">دسترسی به تاریخچه مرورکردن</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">گسترش ابزارتوسعه‌دهندگان برای دسترسی به داده‌های شما بر روی زبانه‌های باز</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">نگارش</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">نویسندگان</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">آخرین بروزرسانی</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">صفحهٔ خانگی</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">درباره مجوزها بیشتر بدانید</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">رتبه‌بندی</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">تنظیمات</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">روشن</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">خاموش</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">اجازه دادن در حالت مرور خصوصی</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">اجرا کردن در حالت مرور خصوصی</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">فعال شد</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">غیر فعال</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">نصب شد</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">پیشنهاد شده</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">هنوز پشتیبانی نشده</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">هنوز در دسترس نیست</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">غیر فعال</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">جزئیات</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">مجوزها</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">حذف</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">اضافه کردن%1$s؟</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">این نیازمند اجازه شماست برای :</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">افزودن</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">لغو</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">نصب افزونه‌ها</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">لغو</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">بررسی‌های:%1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">افزونه‌ها</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">مدیریت افزونه‌ها</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">اجازه دادن</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">رد کردن</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s دارای بروزرسانی جدید است</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$dمجوزهای جدید لازم است</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">یک مجوز جدید لازم است</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">به‌روزرسانی‌های افزونه</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">بررسی افزونه‌های پشتیبانی شده</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">افزونهٔ جدید موجود است</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">افزونه‌های جدید موجود است</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">افزودن %1$s به %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">افزودن %1$s و %2$s به %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">افزودن آنها به %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">فناوری افزونهٔ Firefox در حال نوین‌سازی است. این افزونه‌ها از چارچوب‌هایی استفاده می‌کنند که با Firefox ۷۵ و بالاتر از آن سازگار نیست.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">در حال حاضر در حال ساختن پشتیبانی برای گزیده ای از افزونه‌های انتخاب شده هستیم.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">در حال بارگیری و تأیید افزونه…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">شکست در پرس‌وجوی افزونه!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">برای محل %1$s و همچینین زبان پیش‌فرض %2$s، ترجمه‌ای یافت نشد</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$sبا موفقیت نصب شد</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">شکست در نصب %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s با موفقیت فعال شد</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">شکست در فعالسازی %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s با موفقیت غیرفعال شد</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">شکست در فعالسازی %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$sبا موفقیت حذف شد</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">شکست در پاک‌کردن %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$sبا موفقیت حذف شد</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">شکست در حذف%1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">این افزونه از نگارش قبلی %1$s منتقل شده است</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">۱ افزونه</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%s افزونه</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">بیشتر بدانید</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">با موفقیت به روز شد</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">هیچ بروزرسانی‌ای موجود نیست</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">خطا</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">بروزرسانی اطلاعات</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">آخرین تلاش</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">وضعیت:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s به %2$s اضافه شده است</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">در منو باز کنید</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">باشه، فهمیدم</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..5dd8d807d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ff/strings.xml
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Janngu tee mbaylaa teelte sutura</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Yetto keɓe maa ngam lowe ɗee fof</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Yetto tabbe wanngorde</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Yetto dille wanngorde saanga banngogol</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Tar mbaylaa maantore</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Tar etee mbaylaa teelte wanngorde</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Momtu aslol peeragol cakkitiingol, kuukiije, e keɓe jokkiiɗe heen</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Awto piille, njanngaa etee mbaylaa aslol wanngorde ndee</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Uddit piille gaawtaaɗe e masiŋel maa</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Tar binndi tabbe udditiiɗe fof</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Yetto nokku maa</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Yetto aslol banngagol</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Reen kuutoragol njokkon, njiilaa kettule</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Wostondir mesasuuji e jaaɓnirɗe goɗɗe ko wonaa ngal ɗoo</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Hollat tintine maa</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Yiil teelte proksi wanngorde</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Naat e tabbe uddaaɗe sakket</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Suuɗ tee hollir tabbe wanngorde ndee</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Yetto aslol banngagol</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Timmitin kuutorɗe topayɓe ngam heɓde keɓe maa e tabbe udditiiɗe</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Yamre</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Winnduɓe</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Hesɗitinaa sakket</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Jaɓɓorgo</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Ɓeydu humpito baɗte jamirooje</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Teelte</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Huɓɓii</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Ñifii</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Yamir e banngogol suturo</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Hurmin e banngogol suturo</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Hurminaama</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Ñifaama</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Aafaama</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Basiyaaɗe</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Tammbaaka tawo</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Heɓotaako tawo</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Ñifaama</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Cariiɗe</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Jamirooje</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Ittu</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Ɓeydu %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ina naamndii yamiroore maa e:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Ɓeydu</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Haaytu</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Aaf Ɓeyditere</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Ɓeyditte</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Topitorde Ɓeyditte</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Yamir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Haɗ</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ina jogii hesɗitinal kesal</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d jamirooje kese ina naamnaa</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Yamiroore hesere ina naamnaa</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Kesɗitine Ɓeyditte</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Kesɗitine kese ina ngoodi</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Ɓeyditte kese ina ngoodi</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Ɓeydu %1$s e %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Ɓeydu %1$s kam e %2$s e %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Ɓeydu ɗe e %1$s</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Aawtogol e ƴeewtogol ɓeyditte…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Horiima naamnaade Ɓeyditte!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Aafaama no haaniri %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Horiima aafde %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Hurminaama no haaniri %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Horiima hurminde %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Daaƴaama no haaniri %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Horiima daaƴde %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Aaftaama no haaniri %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Horiima aaftaade %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Ittaama no haaniri %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Horiima ittude %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ɗe ɓeyditte egginaa ko yamre ɓennunde %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1Ɓeyditte</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s ɓeyditte</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Ɓeydu humpito</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Hesɗitinaama no haaniri</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Alaa kesɗitine ngoodi</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Juumre</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Humpito kesɗitine</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Etogol cakkitiingol:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Ngonka:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ɓeydaama e %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Uddit ɗum e dosol ngol</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Eey, mi faamii</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..0dad31dfed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-fi/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lue ja muokkaa yksityisyysasetuksia</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Pääsy tietoihisi kaikilla sivustoilla</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Pääsy tietoihisi sivustolla %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Pääsy tietoihisi sivustoissa, jotka kuuluvat verkkotunnukseen %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Pääsy tietoihisi 1 muulla sivustolla</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Pääsy tietoihisi %1$d muulla sivustolla</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Pääsy tietoihisi 1 muussa verkkotunnuksessa</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Pääsy tietoihisi %1$d muussa verkkotunnuksessa</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d/%3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Käytä selaimen välilehtiä</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Tallenna rajoittamaton määrä tietoja selaimeen</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Tarkkaile selaimen toimintaa siirryttäessä sivulta toiselle</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lue ja muokkaa kirjanmerkkejä</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lue ja muokkaa selaimen asetuksia</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Tyhjennä selaushistoria, evästeet ja näihin liittyvät tiedot</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Lue tietoja leikepöydältä</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Kirjoita tietoja leikepöydälle</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Estä sisältö millä tahansa sivulla</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lue selaushistoriaasi</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Lataa tiedostoja sekä lue ja muokkaa selaimen lataushistoriaa</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Avaa laitteellesi ladattuja tiedostoja</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lue kaikkien avointen välilehtien tekstiä</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Käytä sijaintiasi</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Käytä selaushistoriaa</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Tarkkaile laajennusten käyttöä ja hallitse teemoja</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Vaihda viestejä muiden sovellusten kanssa</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Näytä ilmoituksia sinulle</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Tarjoa salausteknisiä todennuspalveluja</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Hallitse selaimen välityspalvelinasetuksia</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Käytä viimeksi suljettuja välilehtiä</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Piilota ja näytä selaimen välilehtiä</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Käytä selaushistoriaa</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Laajenna kehitystyökaluja käyttämään avoimissa välilehdissä olevia tietoja</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versio</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Tekijä</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Tekijät</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Viimeksi päivitetty</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Verkkosivusto</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Lue lisää käyttöoikeuksista</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Arvosana</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Lisätietoja tästä lisäosasta</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Lisätietoja tästä laajennuksesta</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Asetukset</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Päällä</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Pois</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Salli yksityisessä selauksessa</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Suorita yksityisessä selauksessa</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Ei sallittu yksityisissä ikkunoissa</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Käytössä</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Pois käytöstä</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Asennettu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Suositeltu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ei vielä tuettu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ei vielä saatavilla</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Pois käytöstä</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Yksityiskohdat</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Oikeudet</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Poista</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Raportoi</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Lisätäänkö %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s pyytää lisäoikeuksia.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Se vaatii seuraavat oikeudet:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Se haluaa:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Lisää</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Salli</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Estä</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Peruuta</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Asenna lisäosa</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Asenna %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Peruuta</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Arvosteluja: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Arvostelu: %1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Lisäosat</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Lisäosien hallinta</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosat on väliaikaisesti poistettu käytöstä</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Laajennukset on väliaikaisesti poistettu käytöstä</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Yksi tai useampi lisäosa lakkasi toimimasta, mikä teki järjestelmästä epävakaan.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Yksi tai useampi laajennus lakkasi toimimasta, mikä teki järjestelmästä epävakaan.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Käynnistä lisäosat uudelleen</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Käynnistä laajennukset uudelleen</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Etsi lisää lisäosia</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Etsi lisää laajennuksia</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Salli</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Estä</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Lisäosalle %1$s on uusi päivitys</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d uutta oikeutta vaaditaan</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Uusi oikeus vaaditaan</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosapäivitykset</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Laajennuspäivitykset</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Tuettujen lisäosien tarkistus</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Uusi lisäosa saatavilla</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Uusia lisäosia saatavilla</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Lisää %1$s %2$siin</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Lisää %1$s ja %2$s %3$siin</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Lisää ne %1$siin</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefoxin lisäosateknologiaa nykyaikaistetaan. Nämä lisäosat käyttävät sovelluskehyksiä, jotka eivät ole yhteensopivia Firefox 75:n ja sitä aiempien versioiden kanssa.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Rakennamme parhaillaan suositeltujen laajennusten valikoimaa.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Ladataan ja vahvistetaan lisäosaa…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Laajennusta ladataan ja vahvistetaan…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosien kysely epäonnistui!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Laajennusten kysely epäonnistui!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Käännöstä ei löytynyt maa-asetustolle %1$s tai oletuskielelle %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Asennettiin onnistuneesti %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Lisäosan %1$s asennus epäonnistui</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Tämän lisäosan asentaminen epäonnistui.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Tämän laajennuksen asentaminen epäonnistui.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Tätä lisäosaa ei voitu ladata yhteyshäiriön vuoksi.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Tätä laajennusta ei voitu ladata yhteyshäiriön vuoksi.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosaa ei voitu asentaa koska se vaikuttaa olevan vaurioitunut.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Tätä laajennusta ei voitu asentaa, koska se vaikuttaa olevan vioittunut.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Tätä lisäosaa ei voitu asentaa koska sitä ei ole varmennettu.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Tätä laajennusta ei voitu asentaa, koska sitä ei ole vahvistettu.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Lisäosaa %1$s ei voitu asentaa, koska se ei ole yhteensopiva %2$sin version %3$s kanssa.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Laajennusta %1$s ei voitu asentaa, koska sen riski aiheuttaa epävakautta tai tietoturvaongelmia on suuri.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Otettiin käyttöön onnistuneesti %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Lisäosan %1$s ottaminen käyttöön epäonnistui</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Lisäosa %1$s poistettiin käytöstä onnistuneesti</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Lisäosan %1$s poistaminen käytöstä epäonnistui</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Lisäosan %1$s asennus poistettiin onnistuneesti</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Lisäosan%1$s asennuksen poistaminen epäonnistui</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Poistettiin onnistuneesti %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Lisäosan %1$s poistaminen epäonnistui</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Tämä lisäosa migratoitiin %1$sin aiemmasta versiosta</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 lisäosa</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 laajennus</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s lisäosaa</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s laajennusta</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Lue lisää</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Päivitetty onnistuneesti</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ei päivitystä saatavilla</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Virhe</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Päivitystiedot</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Viimeisin yritys:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Tila:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s on lisätty %2$siin</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Avaa valikossa</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Käytä lisäosaa %1$s %2$sin valikosta.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Selvä</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Lue lisää</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s on poistettu käytöstä turvallisuus- tai vakausongelmian vuoksi.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Lisäosaa %1$s ei voitu vahvistaa turvalliseksi, ja se on poistettu käytöstä.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Lisäosa %1$s ei ole yhteensopiva %2$sin version %3$s kanssa.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..b74143dc33
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-fr/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Consulter et modifier les paramètres de vie privée</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Accéder à vos données pour tous les sites web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Accéder à vos données pour %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Accéder à vos données pour les sites du domaine %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Accéder à vos données pour un autre site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Accéder à vos données pour %1$d autres sites</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Accéder aux données d’un autre domaine</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Accéder aux données de %1$d autres domaines</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d sur %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Accéder aux onglets du navigateur</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Stocker une quantité illimitée de données côté client</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Accéder à l’activité du navigateur pendant la navigation</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Consulter et modifier les marque-pages</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Consulter et modifier les paramètres du navigateur</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Effacer l’historique de navigation récent, les cookies et les données associées</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Lire les données du presse-papiers</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Ajouter des données dans le presse-papiers</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquer du contenu sur n’importe quelle page</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lire votre historique de navigation</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Télécharger des fichiers, et consulter et modifier l’historique des téléchargements du navigateur</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Ouvrir les fichiers téléchargés sur votre appareil</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lire le texte de tous les onglets ouverts</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Accéder à votre localisation</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Accéder à l’historique de navigation</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Surveiller l’utilisation des extensions et gérer les thèmes</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Échanger des messages avec d’autres applications que celle-ci</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Vous afficher des notifications</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Fournir des services d’authentification chiffrée</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Contrôler les paramètres proxy du navigateur</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Accéder aux onglets récemment fermés</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Afficher ou masquer les onglets du navigateur</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Accéder à l’historique de navigation</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ouvrir les outils de développement afin d’accéder à vos données dans les onglets ouverts</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Auteur</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Auteurs</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Dernière mise à jour</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Page d’accueil</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">En savoir plus à propos des permissions</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Appréciation</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">En savoir plus sur ce module</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Plus d’informations sur cette extension</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Paramètres</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activé</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Désactivé</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Autoriser en navigation privée</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Exécuter en navigation privée</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Non autorisé dans les fenêtres de navigation privée</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activé</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Désactivé</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installés</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recommandés</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Pas encore pris en charge</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Pas encore disponible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Désactivés</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Détails</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissions</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Supprimer</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Signaler</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Ajouter %1$s ?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s demande des permissions supplémentaires.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Votre permission est nécessaire pour :</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">L’extension souhaite :</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Ajouter</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Autoriser</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Refuser</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Annuler</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installer le module</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installer %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Annuler</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Avis : %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Note : %1$.02f sur 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Modules complémentaires</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gestionnaire de modules complémentaires</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Modules complémentaires temporairement désactivés</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Les extensions sont temporairement désactivées</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Un ou plusieurs modules complémentaires ont cessé de fonctionner, rendant votre système instable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Une ou plusieurs extensions ont cessé de fonctionner, rendant votre système instable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Redémarrer les modules</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Redémarrer les extensions</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Découvrir davantage de modules</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Trouver d’autres extensions</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Autoriser</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Refuser</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Une nouvelle mise à jour est disponible pour %1$s</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nouvelles permissions sont nécessaires</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Une nouvelle permission est nécessaire</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Mises à jour des modules</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Mises à jour d’extensions</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Vérification des modules complémentaires pris en charge</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nouveau module complémentaire disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nouveaux modules complémentaires disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Ajouter %1$s à %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Ajouter %1$s et %2$s à %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Les ajouter à %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La technologie des modules complémentaires de Firefox se modernise. Ces modules utilisent des frameworks qui ne sont pas compatibles avec Firefox 75 et ses versions ultérieures.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Nous mettons actuellement en place la prise en charge d’une première sélection d’extensions recommandées.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Téléchargement et vérification du module…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Téléchargement et vérification de l’extension…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Impossible d’obtenir la liste des modules complémentaires.</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Impossible d’obtenir la liste des extensions.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Traduction introuvable, que ce soit pour la locale %1$s ou pour la langue par défaut %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Installation réussie de %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Échec de l’installation de %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Échec de l’installation du module.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Échec de l’installation de cette extension.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Ce module complémentaire n’a pas pu être téléchargé à cause d’un échec de connexion.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Cette extension n’a pas pu être téléchargée à cause d’un échec de connexion.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Ce module complémentaire n’a pas pu être installé car il semble corrompu.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Cette extension n’a pas pu être installée car elle semble corrompue.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Ce module complémentaire n’a pas pu être installé car il n’a pas été vérifié.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Cette extension n’a pas pu être installée car elle n’a pas été vérifiée.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s n’a pas pu être installé car il n’est pas compatible avec %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s n’a pas pu être installé car il risque fortement de provoquer des problèmes de stabilité ou de sécurité.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Activation réussie de %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Impossible d’activer %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s désactivé avec succès</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Impossible de désactiver %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Désinstallation réussie de %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Échec de la désinstallation de %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Suppression réussie de %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Impossible de supprimer %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ce module complémentaire a été importé à partir d’une version précédente de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 module complémentaire</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extension</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s modules complémentaires</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensions</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">En savoir plus</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Mise à jour effectuée</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Aucune mise à jour disponible</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Erreur</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informations de mise à jour</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Dernière tentative :</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">État :</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s a été ajouté à %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Ouvrez-le depuis le menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Accédez à %1$s depuis le menu %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">J’ai compris</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">En savoir plus</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s a été désactivé suite à des problèmes de sécurité ou de stabilité.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Le module %1$s n’a pas pu être vérifié comme sûr et a été désactivé.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Le module %1$s n’est pas compatible avec votre version de %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..7582475d00
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-fur/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lei e modificâ lis impostazions di riservatece</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acedi ai tiei dâts di ducj i sîts web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acedi ai tiei dâts par %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acedi ai tiei dâts pai sîts intal domini %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acedi ai tiei dâts su 1 altri sît</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acedi ai tiei dâts su %1$d altris sîts</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acedi ai tiei dâts su 1 altri domini</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acedi ai tiei dâts su %1$d altris dominis</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d di %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acedi aes schedis dal navigadôr</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Archiviâ cuantitâts ilimitadis di dâts sul to dispositîf</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acedi ae ativitât dal navigadôr dulinvie la navigazion</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lei e modificâ i segnelibris</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lei e modificâ lis impostazions dal navigadôr</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Netâ la cronologjie di navigazion resinte, i cookies e i dâts relatîfs</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Otignî i dâts des notis</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Meti dâts tes notis</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blocâ i contignûts in cualsisei pagjine</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lei la tô cronologjie di navigazion</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Discjariâ files, lei e modificâ la cronologjie dai discjamâts dal navigadôr</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Vierzi i files discjamâts su to dispositîf</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lei il test di dutis lis schedis viertis</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acedi ae tô posizion</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acedi ae cronologjie di navigazion</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorâ l’ûs des estensions e gjestî i temis</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Scambiâ messaçs cun altris aplicazions</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Visualizâ notifichis</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Furnî servizis di autenticazion criptografiche</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlâ lis impostazions dal proxy dal navigadôr</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acedi aes schedis sieradis di resint</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Platâ e mostrâ schedis dal navigadôr</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acedi ae cronologjie di navigazion</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Permeti ai struments dai disvilupadôrs di acedi ai tiei dâts tes schedis viertis</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autôr</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autôrs</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ultin inzornament</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pagjine iniziâl</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Plui informazions sui permès</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Valutazion</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Plui informazions su chest component adizionâl</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Altris informazions</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Impostazions</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ativât</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Disativât</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permet in navigazion privade</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Eseguìs in navigazion privade</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">No consintût in barcons privâts</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Abilitât</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Disabilitât</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalâts</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Conseâts</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">No ancjemò supuartâts</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">No ancjemò disponibii</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Disativâts</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detais</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permès</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Gjave</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Segnale</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Zontâ %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s al domande permès adizionâi.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Al à bisugne dal to permès par:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Al desidere:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Zonte</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permet</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Dinee</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Anule</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instale component adizionâl</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instale %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Anule</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recensions: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Valutazion: %1$.02f su 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Components adizionâi</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gjestôr components adizionâi</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">I components adizionâi a son disativâts in mût temporani</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Lis estensions a son disativadis in mût temporani</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Un o plui components adizionâi a àn fermât di funzionâ, rindint il sisteme instabil.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Une o plui estensions si son fermadis e no funzionin, rindint instabil il sisteme.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Torne invie i components adizionâi</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Torne invie lis estensions</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Cjate altris components adizionâi</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Cjate altris estensions</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permet</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Dinee</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s al à un gnûf inzornament</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d gnûfs permès necessaris</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Un gnûf permès necessari</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Inzornaments dal component adizionâl</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Inzornaments des estensions</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Verificadôr components adizionâi supuartâts</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Gnûf component adizionâl disponibil</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Gnûfs components adizionâi disponibii</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Zonte %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Zonte %1$s e %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Zontiju a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La tecnologjie dai components adizionâi di Firefox si sta modernizant. Chescj components adizionâi a doprin frameworks incompatibii cun Firefox 75 e sucessîfs.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">O stin svilupant il supuart par une selezion iniziâl di estensions conseadis.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Daûra discjamâ e verificâ il component adizionâl…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Daûr a discjariâ e a verificâ la estension…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Impussibil interogâ i components adizionâi!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Impussibil interogâ lis estensions!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Traduzion no cjatade, ni pe localizazion in %1$s ni pe lenghe predefinide %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalât cun sucès</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Impussibil instalâ %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Impussibil instalâ chest component adizionâl.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Nol è stât pussibil instalâ cheste estension.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Impussibil discjariâ chest component adizionâl par vie di un erôr te conession.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Nol è stât pussibil discjariâ cheste estension par vie di un erôr te conession.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Impussibil instalâ chest component adizionâl parcè che al somee ruvinât.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Nol è stât pussibil instalâ cheste estension parcè che e somee ruvinade.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Impussibil instalâ chest component adizionâl parcè che nol è stât verificât.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Nol è stât pussibil instalâ cheste estension parcè che no je stade verificade.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Impussibil instalâ %1$s parcè che nol è compatibil cun %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Impussibil instalâ %1$s parcè che al à un risi elevât di causâ problemis di stabilitât e sigurece.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s abilitât cun sucès</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Impussibil abilitâ %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s disabilitât cun sucès</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Impussibil disabilitâ %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s disinstalât cun sucès</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Impussibil disinstalâ %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s gjavât cun sucès</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Impussibil gjavâ %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Chest component adizionâl al rive de migrazion di une version precedente di %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 component adizionâl</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 estension</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s components adizionâi</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s estensions</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Plui informazions</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Inzornât cun sucès</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nissun inzornament disponibil</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Erôr</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informazion sul inzornament</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ultin tentatîf:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stât:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s al è stât zontât a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Vierzilu tal menù</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Jentre in %1$s dal menù di %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Va ben, capît</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Va ben</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Plui informazions</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s al è stât disativât par vie di problemis di sigurece o di stabilitât.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s al è stât disativât parcè che nol è pussibil verificâlu come sigûr.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s nol è compatibil cu la tô version di %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..9df57d0c5e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Privacyynstellingen lêze en bewurkje</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Jo gegevens foar alle websites benaderje</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Jo gegevens foar %1$s benaderje</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Jo gegevens foar alle websites yn it domein %1$s benaderje</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Jo gegevens op 1 oare website benaderje</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Jo gegevens op %1$d oare websites benaderje</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Jo gegevens op 1 oar domein benaderje</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Jo gegevens op %1$d oar domeinen benaderje</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d fan %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Browserljepblêden benaderje</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Gegevens oan clientside ûnbeheind bewarje</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Browseraktiviteit wylst navigearjen benaderje</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Blêdwizers lêze en oanpasse</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Browserynstellingen lêze en oanpasse</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Resinte browserskiednis, cookies en relatearre gegevens wiskje</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Gegevens fan it klamboerd ophelje</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Gegevens op it klamboerd pleatse</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Ynhâld op elke side blokkearje</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Jo navigaasjeskiednis lêze</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Bestannen downloade en downloadskiednis fan de browser lêze en oanpasse</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Nei jo apparaat downloade bestannen iepenje</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">De tekst fan alle iepen ljepblêden lêze</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Jo lokaasje benaderje</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Browserskiednis benaderje</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Utwreidigsgebrûk kontrolearje en tema’s beheare</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Berjochten mei oare apps as dizze útwikselje</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Notifikaasjes werjaan</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kryptografyske autentikaasjetsjinsten biede</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Browserproxyynstellingen beheare</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Koartlyn sluten ljepblêden benaderje</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Browserljepblêden ferstopje en toane</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Browserskiednis benaderje</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Untwikkelersark útwreidzje om jo gegevens yn iepen ljepblêden te benaderjen</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Ferzje</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Auteur</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Skriuwers</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Lêst bywurke</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Startside</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Mear ynfo oer tastimmingen</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Wurdearring</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Mear oer dizze add-on</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Mear oer dizze útwreiding</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ynstellingen</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Oan</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Ut</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Tastean yn priveenavigaasje</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Utfiere yn priveenavigaasje</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Net tastien yn priveefinsters</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ynskeakele</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Utskeakele</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Ynstallearre</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Oanrekommandearre</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Noch net stipe</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Noch net beskikber</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Utskeakele</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Details</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Tastimmingen</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Fuortsmite</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Rapportearje</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s tafoegje?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s freget oanfoljende tastimmingen.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Dit fereasket jo tastimming foar it folgjende:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">De add-on wol:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Tafoegje</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Tastean</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Wegerje</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Annulearje</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Add-on ynstallearje</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s ynstallearje</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Annulearje</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Beoardielingen: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Wurdearring: %1$.02f fan 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Add-onbehearder</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons binne tydlik útskeakele</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Utwreidingen binne tydlik útskeakele</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Ien of mear add-ons wurkje net mear, wêrtroch jo systeem ynstabyl wurdt.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Ien of mear útwreidingen wurkje net mear, wêrtroch jo systeem ynstabyl wurdt.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons opnij starte</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Utwreidingen opnij starte</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Mear add-ons sykje</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Mear útwreidingen sykje</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Tastean</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Wegerje</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s hat in nije fernijing</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nije machtigingen fereaske</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Der is in nije machtiging fereaske</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Add-on-fernijingen</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Utwreidingsfernijingen</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Kontrôle op stipe add-ons</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nije add-on beskikber</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nije add-ons beskikber</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s tafoegje oan %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s en %2$s tafoegje oan %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Tafoegje oan %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">De technology fan Firefox-add-ons modernisearret. Dizze add-ons brûke frameworks dy’t ynkompatibel binne mei Firefox 75 en heger.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Wy bouwe op dit stuit stipe foar in earste seleksje fan oanrekommandearre útwreidingen.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Add-on downloade en ferifiearje…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Utwreiding downloade en ferifiearje…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Sykjen nei add-ons mislearre!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Sykjen nei útwreidingen mislearre!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Gjin oersetting foar locale %1$s of foar de standerttaal %2$s fûn</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s mei sukses ynstallearre</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Ynstallaasje fan %1$s mislearre</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Ynstallaasje fan dizze add-on is mislearre.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Ynstallaasje fan dizze útwreiding is mislearre.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Dizze add-on koe net download wurde, fanwegen in flater yn de ferbining.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Dizze útwreiding koe net download wurde, fanwegen in flater yn de ferbining.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Dizze add-on koe net ynstallearre wurde, omdat dizze skansearre liket.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Dizze útwreiding koe net ynstallearre wurde, omdat dizze skansearre liket.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Dizze add-on koe net ynstallearre wurde, omdat dizze net ferifiearre is.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Dizze útwreiding koe net ynstallearre wurde, omdat dizze net ferifiearre is.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s koe net ynstallearre wurde, omdat it net kompatibel is mei %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s koe net ynstallearre wurde, omdat it in heech risiko op stabiliteits- of feilichheidsproblemen jout.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s mei sukses ynskeakele</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Ynskeakeljen fan %1$s mislearre</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s mei sukses útskeakele</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Utskeakeljen %1$s mislearre</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s mei sukses deynstallearre</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Deynstallearjen fan %1$s mislearre</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s mei sukses fuortsmiten</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Fuortsmiten fan %1$s mislearre</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Dizze add-on is migrearre fan in eardere ferzje fan %1$s út</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 útwreiding</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s add-ons</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s útwreidingen</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Mear ynfo</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Mei sukses bywurke</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Gjin fernijing beskikber</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Flater</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Ynformaasje oer fernijingen</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Lêste besykjen:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Steat:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s is oan %2$s tafoege</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Yn it menu iepenje</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Benaderje %1$s fan it %2$s-menu út.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Oké, begrepen</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Mear ynfo</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s is útskeakele fanwegen feilichheids- of stabiliteitsproblemen.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s koe net as feilich ferifiearre wurde en is útskeakele.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s is net kompatibel mei jo ferzje fan %2$s (ferzje %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..1ff4de5bba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Socruithe príobháideachais a léamh agus a athrú</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Do chuid sonraí ó shuíomh ar bith a léamh</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Teacht ar chluaisíní an bhrabhsálaí</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Stóráil an oiread sonraí agus is maith leat ar thaobh an chliaint</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Teacht ar ghníomhaíocht an bhrabhsálaí le linn nascleanúna</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leabharmharcanna a léamh agus a athrú</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Socruithe an bhrabhsálaí a léamh agus a athrú</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Stair bhrabhsála, fianáin, agus sonraí gaolmhara a ghlanadh</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Sonraí a fháil ón ghearrthaisce</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Sonraí a shábháil sa ghearrthaisce</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Comhaid a íoslódáil, agus stair na n-íoslódálacha a léamh agus a athrú</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Comhaid a íoslódáladh ar do ghléas a oscailt</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Téacs in aon chluaisín oscailte a léamh</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Do láthair a fheiceáil</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Teacht ar an stair bhrabhsála</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monatóireacht a dhéanamh ar úsáid eisínteachtaí agus téamaí a bhainistiú</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Cumarsáid a dhéanamh le haipeanna eile</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Fógraí a thaispeáint duit</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Seirbhísí fíordheimhnithe cripteagrafacha a sholáthar</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Socruithe seachfhreastalaí a rialú</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Teacht ar chluaisíní a dúnadh le déanaí</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Cluaisíní a chur i bhfolach nó a thaispeáint</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Teacht ar an stair bhrabhsála</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Cead a thabhairt d\'uirlisí forbartha teacht ar shonraí i gcluaisíní oscailte</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Leagan</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Údair</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">An nuashonrú is déanaí</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Leathanach baile</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Tuilleadh eolais faoi cheadanna</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Rátáil</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Socruithe</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ann</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">As</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Cumasaithe</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Díchumasaithe</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Suiteáilte</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Molta</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Gan tacaíocht fós</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Díchumasaithe</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Mionsonraí</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Ceadanna</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Bain</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Suiteáil %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Tá do chead uaidh chuige seo:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Suiteáil</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cealaigh</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Suiteáil an Breiseán</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Breiseáin</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Ceadaigh</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Diúitaigh</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Bhí %1$s nuashonraithe</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d cead nua de dhíth</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Nuashonruithe breiseán</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Tá cruth nua-aimseartha teicneolaíochta ar bhreiseáin Firefox. Úsáideann na breiseáin seo creatlacha nach bhfuil comhoiriúnach le Firefox 75 nó le leaganacha níos déanaí.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Tá an breiseán á íoslódáil agus á fhíorú…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Níorbh fhéidir liosta na mbreiseán a fháil!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">D\'éirigh le suiteáil %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Theip ar shuiteáil %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">D\'éirigh le cumasú %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Theip ar chumasú %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">D\'éirigh le díchumasú %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Theip ar dhíchumasú %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">D\'éirigh le díshuiteáil %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Theip ar dhíshuiteáil %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">D\'éirigh le baint %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Theip ar bhaint %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be FireFox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Aistríodh an breiseán seo ó leagan níos sine de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 bhreiseán</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s breiseán</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Tuilleadh eolais</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..c807ec590f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-gd/strings.xml
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">roghainnean na prìobhaideachd a leughadh is atharrachadh</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">cothrom fhaighinn air an dàta agad airson a h-uile làrach-lìn</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Cothrom fhaighinn air an dàta agad airson %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Cothrom fhaighinn air an dàta air fad agad airson làraichean air an àrainn %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Cothrom fhaighinn air an dàta agad air aon làrach eile</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Cothrom fhaighinn air an dàta agad air làraichean (%1$d) eile</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Cothrom fhaighinn air an dàta agad air aon àrainn eile</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Cothrom fhaighinn air an dàta agad air àrainnean (%1$d) eile</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">cothrom fhaighinn air tabaichean a’ bhrabhsair</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">dàta gun chrìoch a stòradh air taobh a’ chliant</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">cothrom fhaighinn air gnìomhachd a’ bhrabhsair rè seòladaireachd</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">comharran-lìn a leughadh is atharrachadh</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">roghainnean a’ bhrabhsair a leughadh is atharrachadh</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">an eachdraidh brabhsaidh, briosgaidean is dàta co-cheangailte eile fhalamhachadh</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">dàta fhaighinn on stòr-bhòrd</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">dàta a chur air an stòr-bhòrd</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bac susbaint air duilleag sam bith</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Leugh an eachdraidh brabhsaidh agad</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">faidhlichean a luchdadh a-nuas agus eachdraidh nan luchdaidhean a-nuas aig a’ bhrabhsair a leughadh is atharrachadh</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">faidhlichean a chaidh a luchdadh a-nuas dhan uidheam agad fhosgladh</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">an teacsa air gach taba fosgailte a leughadh</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">cothrom fhaighinn air d’ ionad</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">cothrom fhaighinn air an eachdraidh bhrabhsaidh</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">sùil a chumail air caitheamh nan leudachan agus ùrlaran a stiùireadh</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">teachdaireachdan a chur is fhaighinn o aplacaidean eile seach an tè seo</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">brathan a shealltainn dhut</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">seirbheisean dearbhaidh crioptografach a sholar</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">roghainnean progsaidh a’ bhrabhsair a stiùireadh</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">cothrom fhaighinn air tabaichean a dhùin thu o chionn goirid</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">tabaichean a’ bhrabhsair a shealltainn is a chur am falach</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">cothrom fhaighinn air an eachdraidh bhrabhsaidh</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">innealan luchd-leasachaidh a leudachadh ach am faigh iad cothrom air an dàta agad ann an tabaichean fosgailte</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Tionndadh</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Ùghdaran</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">An t-ùrachadh mu dheireadh</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">An duilleag-dhachaidh</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Barrachd fiosrachaidh mu cheadan</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Rangachadh</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Roghainnean</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Air</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Dheth</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Ceadaich brabhsadh prìobhaideach</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Ruith sa bhrabhsadh phrìobhaideach</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">An comas</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">À comas</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Air a stàladh</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Molta</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Gun taic fhathast</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Chan eil seo ri fhaighinn fhathast</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">À comas</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Mion-fhiosrachadh</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Ceadan</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Thoir air falbh</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">A bheil thu airson %1$s a chur ris?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Feumaidh e do chead airson:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Cuir ris</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Sguir dheth</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Stàlaich an tuilleadan</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Sguir dheth</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Lèirmheasan: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Tuilleadain</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Manaidsear nan tuilleadan</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Ceadaich</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Diùlt</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Tha ùrachadh ri fhaighinn airson %1$s</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Tha ceadan ùra (%1$d) a dhìth</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Tha feum air cead ùr</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Ùrachaidhean aplacaidean</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Dearbhair nan tuilleadan ris a bheil taic</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Tha tuilleadan ùr ri fhaighinn</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Tha tuilleadain ùra ri am faighinn</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Cuir %1$s ri %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Cuir %1$s agus %2$s ri %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Cuir iad ri %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Tha an teicneolas am broinn Firefox ga shìor-leasachadh. Tha na tuilleadain seo a’ cleachdadh frèamaichean-obrach nach eil co-chòrdail le Firefox 75 ⁊ tionndaidhean nas ùire.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Tha sinn ag obair air taghadh tòiseachail de leudachain a mholamaid.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">A’ luchdadh a-nuas is a’ dearbhadh an tuilleadain…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Dh’fhàillig ceasnachadh na tuilleadan!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Cha deach eadar-theangachadh (%1$s) no a’ chànan bhunaiteach (%2$s) a lorg</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Chaidh %1$s a stàladh</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Dh’fhairtlich oirnn a stàladh %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Chaidh %1$s a chur an comas</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Dh’fhairtlich oirnn %1$s a chur an comas</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Chaidh %1$s a chur à comas</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Dh’fhairtlich oirnn %1$s a chur à comas</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Chaidh %1$s a dhì-stàladh</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Dh’fhairtlich oirnn %1$s a dhì-stàladh</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Chaidh %1$s a thoirt air falbh</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Dh’fhairtlich oirnn %1$s a thoirt air falbh</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Rinn an tuilleadan seo imrich o thionndadh na bu sine de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 tuilleadan</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">Tuilleadain (%1$s)</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Barrachd fiosrachaidh</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Chaidh ùrachadh</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Chan eil ùrachadh ri fhaighinn</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Mearachd</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Fiosrachadh on ùraichear</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">An oidhirp mu dheireadh:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Staid:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Chaidh %1$s a chur ri %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Fosgail e sa chlàr-taice</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Ceart, tha mi agaibh</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..fc4d64f6eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-gl/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Ler e modificar a configuración da privacidade</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder aos seus datos de todos os sitios web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder aos seus datos de %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder aos seus datos para os sitios no dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceder aos seus datos de 1 sitio máis</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceder aos seus datos doutros %1$d sitios</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceder aos seus datos de 1 dominio máis</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceder aos seus datos doutros %1$d dominios</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder ás lapelas do navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Almacenar unha cantidade ilimitada de datos do lado do cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder á actividade do navegador durante a navegación</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Ler e modificar os marcadores</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Ler e modificar a configuración do navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpar o historial de navegación recente, as cookies e datos relacionados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obter datos do portapapeis</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Introducir datos no portapapeis</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquear o contido en calquera páxina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Ler o seu historial de navegación</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Descargar ficheiros e ler e modificar o historial de descargas do navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir ficheiros descargados no seu dispositivo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Ler o texto de todas as lapelas abertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a súa localización</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder ao historial de navegación</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Supervisar o uso das extensións e xestionar os temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Intercambiar mensaxes con outras aplicacións distintas desta</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Mostrarlle notificacións</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Fornecer servizos de autenticación criptográfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar a configuración do proxy do navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder ás lapelas pechadas recentemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Agochar e amosar as lapelas do navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder ao historial de navegación</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Estender as ferramentas de desenvolvemento para acceder aos seus datos nas lapelas abertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Última actualización</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Páxina de inicio</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saiba máis sobre os permisos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Puntuación</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Máis información sobre este complemento</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Máis información sobre esta extensión</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Configuración</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Desactivado</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir na navegación privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Executar na navegación privada</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Non se admite en xanelas privadas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Desactivado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Aínda non compatíbel</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Aínda non dispoñíbel</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desactivado</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalles</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisos</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Retirar</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Informar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Engadir %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s solicita permisos adicionais.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Require o seu permiso para:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Quere:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Engadir</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitir</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Denegar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instalar complemento</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instalar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Opinións: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Valoración: %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Complementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Xestor de complementos</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Os complementos están desactivados temporalmente</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">As extensións están desactivadas temporalmente</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Un ou máis complementos deixaron de funcionar, facendo que o seu sistema sexa inestable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Unha ou máis extensións deixaron de funcionar, facendo que o teu sistema sexa inestable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reiniciar os complementos</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reiniciar extensións</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Atopar máis complementos</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Atopar máis extensións</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denegar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ten unha actualización nova</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d permisos novos requiridos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Requírese un permiso novo</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Actualizacións do complemento</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Actualizacións de extensións</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Comprobador de complementos compatíbeis</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Hai un complemento novo dispoñíbel</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Hai complementos novos dispoñíbeis</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Engadir %1$s ao %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Engadir %1$s e %2$s ao %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Engadilos ao %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">A tecnoloxía dos complementos do Firefox estase a modernizar. Estes complementos empregan infraestruturas que son incompatíbeis con Firefox 75 e posteriores.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Actualmente estamos a crear o soporte para unha selección inicial de extensións recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Descargando e comprobando o complemento…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Descargando e verificando a extensión…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Produciuse un erro ao consultar os complementos!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Produciuse un erro ao consultar as extensións.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Non se atopou a tradución para a configuración rexional %1$s nin o idioma predeterminado %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Instalouse correctamente %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Produciuse un erro ao instalar %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Produciuse un erro ao instalar este complemento.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Produciuse un erro ao instalar esta extensión.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Non se puido descargar este complemento debido a un erro de conexión.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Non se puido descargar esta extensión debido a un erro de conexión.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento non se puido instalar porque parece estar corrupto.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Non se puido instalar esta extensión porque parece estar corrupta.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Este complemento non se puido instalar porque non se verificou.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Non se puido instalar esta extensión porque non se verificou.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Non se puido instalar %1$s porque non é compatible con %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Non se puido instalar %1$s porque ten un alto risco de causar problemas de estabilidade ou seguridade.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Activouse %1$s correctamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Non foi posíbel activar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Desactivouse %1$s correctamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Produciuse un erro ao desactivar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Desinstalouse correctamente %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Produciuse un erro ao desinstalar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Retirouse %1$s correctamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Produciuse un erro ao retirar %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Este complemento foi migrado desde unha versión anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 complemento</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensión</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s complementos</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensións</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Máis información</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualizado correctamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Sen actualizacións dispoñíbeis</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Erro</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Información do actualizador</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Último intento:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estado:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s foi engadido ao %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Abrir no menú</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Acceder a %1$s desde o menú %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Entendido</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Aceptar</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Máis información</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Desactivouse o %1$s debido a problemas de estabilidade ou seguranza.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s non se puido verificar como seguro e desactivouse.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s non é compatible coa túa versión de %2$s (versión %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..49cfdab336
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-gn/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Emoñe’ẽ ha emoambue ñemboheko ñemigua</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Eike ne mba’ekuaarãme opaite ñanduti rendápe</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s mba’ekuaarãpe jeike</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Eike ne mba’ekuaarãme umi ñanduti %1$s mba’eteévape</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Mba’ekuaarãme jeike ambue tendápe</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Mba’ekuaarãme jeike %1$d ambue tendápe</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Mba’ekuaarãme jeike ambue mba’eteévape</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Mba’ekuaarãme jeike %1$d ambue mba’eteévape</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d %3$d peve</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Eike kundahára rendayképe</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Embyaty heta mba’ekuaarã ñemuhára gotyogua</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Eike kundahára rembiapópe eikundaha aja</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Emone’ẽ ha emoambue techaukaha</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Emone’ẽ ha emoambue kundahára ñemoĩporã</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Emopotĩ kundahára rembiasakue ipyahúva, kookie ha mba’ekuaarã heseguáva</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Egueru mba’ekuaarã kuatiajokoha guive</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Emoinge mba’ekuaarã kuatiajokohápe</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Ejoko tetepy oimeraẽva kuatiaroguépe</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Emoñe’ẽ ne ñeikundaha rapykuere</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Emboguejy marandurenda, emoñe’ẽ ha emoambue kundahára ñemboguejy rembiasakue</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Embojuruja marandurenda emboguejýva ne mohendahápe</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Emoñe’ẽ haipyre opaite tendayke pegua</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Eike nerendaitépe</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Eike kundahára rembiasakuépe</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Ema’ẽag̃ui jepysokue jeporu ha téma ñangareko rehe</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Embojopyru ñe’ẽmondo ambue tembiporu’i ndive</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Ehechauka ñemomarandu</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Eikuave’ẽ mba’eporurã criptografía ñemoneĩ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Eñangareko kundaha proxy ñemboheko rehe</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Eike tendayke oñemboty ramovévape</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Emokañy ha ehechauka kundahára rendayke</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Eike kundahára rembiasakuépe</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Embohetave mboguatahára rembiporu eike hag̃ua ne mba’ekuaarã rendayke ijurujávape</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Peteĩchagua</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Apohára</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Apohára</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Mbohekopyahu ramovéva</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Kuatiarogue ñepyrũha</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Eikuaave ñemoneĩ rehegua</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Jeporavopy</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Eikuaave moĩmbaha rehegua</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Ag̃uive ko jepysokuégui</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ñemboheko</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Hendypyre</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Mboguepyre</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Emoneĩ kundahára ñemíme</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Emomba’apo kundahára ñemíme</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Noñemoneĩri ovetã ñemíme</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Myandypyre</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Pe’apyre</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Mohendapyre</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Jeroviaháva</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ndojokupytýi gueteri</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ndojeporukuaái gueteri</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Jepe’apyre</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Mba’emimi</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Ñemoneĩ</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Mboguete</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Momarandu</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Mbojuaju %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s oikotevẽ ñemoneĩ jo’ávare.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Oikotevẽ ne ñemoneĩ rehe:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Eipotápa:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Mbojuaju</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Moneĩ</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Mbotove</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Heja</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Emohenda moĩmbaha</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Emohenda %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Heja</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Jehechajey: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Ehepyme’ẽ: %1$.02f 5-gui</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Moĩmbaha</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Moĩmbaha ñangarekohára</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Umi moĩmbaha ojejoko sapy’ami</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Umi jepysokue ojejoko sapy’ami</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Peteĩ térã hetave moĩmbaha ndoikói, upévare apopyvusu noĩporãi.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Peteĩ térã hetave jepysokue ndoikói ko’ág̃a, upévare apopyvusu noĩporãi.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Emoñepyrũjey moĩmbaha</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Emoñepyrũjey jepysokue</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Ehekave moĩmbaha</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Ejuhuvéta jepysokue</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Moneĩ</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Mbotove</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s hekopyahujeýma</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d oikotevẽ ñemoneĩ ipyahúva</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Tekotevẽ ñemoneĩ pyahu</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Moĩmbaha mbohekopyahu</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Embohekopyahu jepysokue</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Tembiporu’i ojokupytýva rechajeyha</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Tembiporu’i pyahu ojeporukuaámava</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Tembiporu’i pyahu ojeporukuaámava</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Embojuaju %1$s %2$s rehe</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Embojuaju %1$s ha %2$s %3$s rehe</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Embojuajúke %1$s rehe</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox moĩmbaha rembiporupyahu oñemoĩporãve. Ko’ã moĩmbaha oiporu frameworks ndojokupytýiva Firefox 75 ha ramovegua ndive.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Ko’ág̃a romoheñóima pytyvõrã poravopy ñepyrũrãva Jepysokue ñe’ẽporãmbyre.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Emboguejy ha ehechajey moĩmbaha…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Emboguejy ha ehechajey jepysokue…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Oiko jejavy ejerurekuévo moĩmbaha!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">¡Ojavy eporandúvo jepysokue!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Ndojejuhúi umi ñe’ẽasa %1$s pegua ha avei pe ñe’ẽ ypykuéva %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s oñemohenda hekopete</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ojavy emohendakuévo</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Ndoikói ko tembiporu’i ñemohenda.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Ndoikói ko jepysokue ñemohenda.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Ko moĩmbaha ndaikatúi oñemboguejy oĩ rupi jejavy jeikekatúpe.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Ko jepysokue ndaikatúi oñemboguejy oĩ rupi jejavy jeikekatúpe.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Ko moĩmbaha ndaikatúi oñemohenda noĩporãmbái rupi.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Pe jepysokue ndaikatúi oñemohenda noĩporãmbái rupi.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Ko moĩmbaha ndaikatúi oñemohenda ndojehechajeýi rupi.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Ko jepysokue ndaikatúi oñemohenda ndojehechajeýi rupi.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s ndaikatúi oñemohenda ndojokupytýi rupi %2$s %3$s ndive.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s ndaikatúi oñemohenda omoapañuãi rupi tuichaháicha tekopyta térã tekorosã</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s hendýma hekopete</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s ojavy hendykuévo</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s oñemboguéma hekopete</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s ojavy emboguekuévo</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s ojepe’áma hekopete</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s ojavy ojeipe’akuévo</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s oñemboguéma hekopete</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ojavy emboguekuévo</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ko moĩmbaha ova peteĩchagua itujavéva %1$s peguágui</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 moĩmbaha</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 jepysokue</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s moĩmbaha</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s jepysokue</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Kuaave</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Hekopyahu porã</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ndaipóri tekopyahu</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Javy</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Tekopyahu marandu</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ñeha’ã paha:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Rekotee:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Oñembojuaju %1$s %2$s rehe</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Embojuruja poravorãme</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Eike %1$s-pe %2$s poravorã guive.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Oĩma, aikũmby</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">MONEĨ</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Eikuaave</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s oñemboguéma oreko rupi apañuãi tekorosãrã térã tekopyta.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s ndaikatúi ojehechajey tekorosãramo ha oñemboguéma.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s ndojokupytýi pe reiporúva %2$s ndive (peteĩchagua %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..71c1e9aa0c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">ગોપનીયતા સેટિંગ્સ વાંચો અને તેમાં ફેરફાર કરો</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">તમામ વેબસાઇટ્સ માટે તમારા ડેટાને ઍક્સેસ કરો</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">બ્રાઉઝર ટૅબ્સને ઍક્સેસ કરો</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ક્લાયંટ-બાજુ માહિતીની અમર્યાદિત રકમ સંગ્રહ કરો</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">નેવિગેશન દરમિયાન ઍક્સેસ બ્રાઉઝર પ્રવૃત્તિ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">બુકમાર્ક્સ વાંચો અને ફેરફાર કરો</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">બ્રાઉઝર સેટિંગ્સ વાંચો અને ફેરફાર કરો</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">બ્રાઉઝિંગ ઇતિહાસ, કૂકીઝ અને સંબંધિત માહિતીને સાફ કરો</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ક્લિપબોર્ડ પરથી માહિતી મેળવો</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ક્લિપબોર્ડ પર ઇનપુટ માહિતી</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ફાઇલો ડાઉનલોડ કરો અને બ્રાઉઝરનો ડાઉનલોડ ઇતિહાસ વાંચો અને સંશોધિત કરો</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">તમારા ઉપકરણ પર ડાઉનલોડ કરેલી ફાઇલો ખોલો</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">બધા ખુલ્લા ટેબ્સનાં લખાણ વાંચો</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">તમારાં સ્થાનમાં પ્રવેશો</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">બ્રાઉઝિંગ ઇતિહાસ મેળવો</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">એક્સ્ટેંશનના વપરાશને મોનિટર કરો અને થીમ્સ મેનેજ કરો</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">આના સિવાયના અન્ય એપ્લિકેશનો સાથે સંદેશાઓનું વિનિમય કરો</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">તમને સૂચનાઓ પ્રદર્શિત કરો</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ક્રિપ્ટોગ્રાફિક પ્રમાણીકરણ સેવાઓ પ્રદાન કરો</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">બ્રાઉઝર પ્રોક્સી સેટિંગ્સ નિયંત્રિત કરો</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">તાજેતરમાં બંધ કરેલ ટૅબ્સ મેળવો</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">બ્રાઉઝર ટેબ્સ છુપાવો અને બતાવો</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">બ્રાઉઝિંગ ઇતિહાસને મેળવો</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ખુલ્લા ટૅબ્સમાં તમારા ડેટાને મેળવવા માટે ડેવલોપર સાધનો વિસ્તૃત કરો</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">આવૃત્તિ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">લેખકો</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">છેલ્લે સુધારાયેલ</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">મુખ્યપૃષ્ઠ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">પરવાનગી વિશે વધુ શીખો</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">મૂલ્યાંકન</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">સેટિંગ્સ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ચાલુ કરો</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">બંધ કરો</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">ખાનગી બ્રાઉઝિંગમાં ચલાવવાની મંજૂરી આપો</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">ખાનગી બ્રાઉઝિંગમાં ચલાવો</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">સક્રિય</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">નિષ્ક્રિય</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ઇન્સ્ટોલ કરેલું</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">ભલામણ કરાયેલ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">હજી સપોર્ટેડ નથી</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">હજી સુધી ઉપલબ્ધ નથી</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">નિષ્ક્રિય કરેલ</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">વિગતો</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">પરવાનગીઓ</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">દૂર કરો</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$sઉમેરશો?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">આ માટે તમારી પરવાનગીની જરૂર છે:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">ઉમેરો</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">રદ કરો</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">ઍડ-ઑન સ્થાપિત કરો</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">રદ કરો</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ઍડ-ઓન્સ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">પરવાનગી આપો</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">નામંજૂર કરો</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s પાસે નવું અપડેટ છે</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d નવી પરવાનગી જરૂરી છે</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">નવી પરવાનગી જરૂરી છે</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">એડ-ઓન અપડેટ્સ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">આધારભૂત ઍડ-ઓન્સ તપાસનાર</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">નવું એડ-ઓન ઉપલબ્ધ છે</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">નવાં એડ-ઓન્સ ઉપલબ્ધ છે</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s ને %2$s પર ઉમેરો</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s અને %2$s ને %3$s પર ઉમેરો</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">તેમને %1$s પર ઉમેરો</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox એડ-ઓન્સ તકનીકી આધુનિકીકરણ કરે છે. આ એડ-ઓન્સ એ ફ્રેમવર્કનો ઉપયોગ કરે છે જે Firefox 75 અને તેનાથી આગળના સુસંગત નથી.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">એડ-ઓન્સ ડાઉનલોડ કરી રહ્યું છે અને ચકાસી રહ્યું છે…</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s સફળતાપૂર્વક સ્થાપિત કર્યું</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s સ્થાપિત કરવામાં નિષ્ફળ</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ને સફળતાપૂર્વક સક્ષમ કર્યું</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s ને સક્ષમ કરવામાં નિષ્ફળ</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s ને સફળતાપૂર્વક અક્ષમ કર્યું</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s ને અક્ષમ કરવામાં નિષ્ફળ</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s સફળતાપૂર્વક દૂર કર્યું</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ને દૂર કરવામાં નિષ્ફળ</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">આ એડ-ઓનને %1$s ના પહેલાનાં સંસ્કરણથી સ્થાનાંતરિત કરવામાં આવ્યું હતું</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 એડ-ઓન</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s એડ-ઓન્સ</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">વધુ શીખો</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">સફળતાપૂર્વક સુધારો કર્યો</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">કોઇ સુધારો ઉપલબ્ધ નથી</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ભૂલ</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">અપડેટરની માહિતી</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">છેલ્લો પ્રયાસ:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">પરિસ્થિતિ:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s %2$s માં ઉમેરવામાં આવ્યું છે.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">તેને મેનૂમાં ખોલો</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ઠીક છે, સમજાઇ ગયું</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..d4488fc600
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">गोपनीयता सेटिंग पढ़ें और संशोधित करें</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">सभी वेबसाइटों के लिए अपने डेटा को एक्सेस करें</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s के लिए अपने डेटा को एक्सेस करें</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ब्राउज़र टैब को एक्सेस करें</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ग्राहक-पक्ष डेटा की असीमित राशि को स्टोर करें</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">नेवीगेशन के दौरान ब्राउज़र गतिविधि एक्सेस करें</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">बुकमार्क पढ़ें और संशोधित करें</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ब्राउज़र सेटिंग देखें और संशोधित करें</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">आधुनिक ब्राउज़िंग इतिहास, कूकीज़, और संबंधित डेटा मिटाएं</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">क्लिपबोर्ड से डेटा प्राप्त करें</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">क्लिपबोर्ड में डेटा दर्ज करें</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">फाइलों को डाउनलोड करें और ब्राउज़र के डाउनलोड इतिहास को देखें और संशोधित करें</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">उपकरण पर डाउनलोड की गई फाइलें खोलें</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">सभी खुले टैब के लेख पढ़ें</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">अपनी लोकेशन को एक्सेस करें</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ब्राउज़िंग इतिहास को एक्सेस करें</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">एक्सटेंशन उपयोग पर नजर रखें और थीम प्रबंधित करें</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">इस ऐप के अलावा किसी और के साथ संदेशों का आदान-प्रदान करें</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">आपको अधिसूचना प्रदर्शित करें</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">क्रिप्टोग्राफिक प्रमाणीकरण सेवाएं प्रदान करें</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ब्राउज़र प्रॉक्सी सेटिंग्स नियंत्रित करें</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">हाल ही में बंद किए गए टैबों तक पहुंचें</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ब्राउज़र टैब को छिपाएं और दिखाएं</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ब्राउज़िंग इतिहास एक्सेस करें</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">अपने डेटा को खुले टैबों में उपयोग करने के लिए डेवलपर उपकरण को विस्तृत करें</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">संस्करण</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">निर्माता</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">पिछला अपडेट</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">मुख्य पृष्ठ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">अनुमतियों के बारे में और अधिक जानें</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">रेटिंग</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">सेटिंग</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">चालू</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">बंद</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">निजी ब्राउज़िंग में चलने की अनुमति दे</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">निजी ब्राउज़िंग में चलाएं</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">सक्रिय किया गया</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">निष्क्रिय किया गया</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">स्थापित किया गया</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">अनुशंसित</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">अभी तक समर्थित नहीं है</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">अभी तक उपलब्ध नहीं</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">अक्षम</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">विवरण</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">अनुमति‌</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">हटाएं</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s जोड़ना चाहते हैं?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">इसके लिए आपकी अनुमति की आवश्यकता है:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">जोड़ें</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">रद्द करें</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">ऐड-ऑन स्थापित करें</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">रद्द करें</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">समीक्षाएं: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ऐड-ऑन</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ऐड-ऑन प्रबंधक</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">अनुमति दें</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">अस्वीकार करें</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s को एक नया अपडेट है</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d नई अनुमतियों की आवश्यकता है</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">एक नई अनुमति की जरूरत है</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">ऐड-ऑन अपडेट</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">समर्थित ऐड-ऑन जांचकर्ता</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">नया ऐड-ऑन उपलब्ध</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">नए ऐड-ऑन उपलब्ध</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s को %2$s में जोड़ें</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s और %2$s को %3$s में जोड़ें</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">उन्हें %1$s में जोड़ें</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox का ऐड-ऑन तकनीक आधुनिक बन रहा है। ये ऐड-ऑन ऐसे फ्रेमवर्क का उपयोग करते हैं जो Firefox 75 और उसके बाद के संस्करण के साथ संगत नहीं है।</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">हम फिलहाल अनुसंशित एक्सटेंशंस की प्रारंभिक चयन के लिए समर्थन तैयार कर रहे हैं।</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">ऐड-ऑन को डाउनलोड और सत्यापित किया जा रहा है…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">ऐड-ऑन प्रश्न करने में असफल!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s सफलतापूर्वक स्थापित किया गया</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s को स्थापित करने में विफल</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s को सफलतापूर्वक सक्षम किया गया</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s को सक्षम करने में असफल</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s को सफलतापूर्वक अक्षम किया गया</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s को अक्षम करने में असफल</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s को सफलतापूर्वक अस्थापित किया गया</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s को अस्थापित करने में असफल</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s को सफलतापूर्वक हटा दिया गया</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s को हटाने में विफल</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">यह ऐड-ऑन %1$s के पिछले संस्करण से बदला गया था</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 ऐड-ऑन</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s ऐड-ऑन</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">अधिक जानें</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">सफलतापूर्वक अपडेट हो गया</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">कोई अपडेट उपलब्ध नहीं</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">त्रुटि</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">अपडेटर की जानकारी</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">पिछला प्रयास:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">स्थिति:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s को %2$s में जोड़ दिया गया है।</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">इसे मेन्यू में खोलें</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ठीक है, समझ गया।</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..51e7bc99d7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-hil/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Bersyon</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Manunulat</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Homepage</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Mga setting</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">On</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Off</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Ginarekomendar</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalyado</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Ginkuha</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Dagdagan %1$s?</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Idugang</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Kanselahon</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Kanselahon</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Pahanugtan</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Dugang %1$s sa %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Dugang %1$s kag %2$s sa %3$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Magtuon sang madamo pa</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..50058f592a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-hr/strings.xml
@@ -0,0 +1,255 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Čitaj i promijeni postavke privatnosti</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Pristupi tvojim podacima za sve web-stranice</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Pristup podatcima za %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Pristup podatcima stranica na domeni %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Pristup podatcima na 1 drugoj stranici</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Pristup podatcima na ovom broju drugih stranica: %1$d</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Pristup podatcima na 1 drugoj domeni</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Pristup podatcima na ovom broju drugih domena: %1$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Pristupi karticama preglednika</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Spremi neograničenu količinu podataka klijenta</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Pristupi aktivnostima preglednika tijekom navigacije</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Čitaj i promijeni zabilješke</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Čitaj i promijeni postavke preglednika</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Izbriši nedavnu povijest pregledavanja, kolačiće i povezane podatke</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Dohvati podatke iz međuspremnika</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Unesi podatke u međuspremnik</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokiraj sadržaj na svim stranicama</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Pročitaj svoju povijest pretraživanja</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Preuzmi datoteke i pročitaj i promijeni povijest preuzimanja preglednika</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Otvori preuzete datoteke</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Čitaj tekst svih otvorenih kartica</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Pristupi svojoj lokaciji</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Pristupi povijesti pregledavanja</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Prati upotrebu dodataka i upravljaj temama</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Razmijeni poruke s drugim programima</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Prikaži obavijesti</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Pruži usluge za šifrirane autentikacije</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Upravljaj proxy postavkama preglednika</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Pristupi nedavno zatvorenim karticama</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Sakrij i prikaži kartice preglednika</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Pristupi povijesti pregledavanja</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Proširi alate za razvojne programere za pristup podacima u otvorenim karticama</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Verzija</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="120" tools:ignore="UnusedResources">Autori</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Zadnje aktualiziranje</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Početna stranica</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saznaj više o dozvolama</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Ocjena</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Više o ovom dodatku</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Postavke</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Uključeno</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Isključeno</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Dozvoli u privatnom pregledavanju</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Pokreni u privatnom pregledavanju</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Aktivirano</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Deaktivirano</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalirano</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Preporučeno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Još nije podržano</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Još nije dostupno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Deaktivirano</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalji</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Dozvole</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Ukloni</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Dodati %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s zahtjeva dodatne dozvole.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Zahtijeva tvoju dozvolu za:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Želi:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Dodaj</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Dopusti</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Odbij</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Odustani</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Instaliraj dodatak</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Otkaži</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recenzija: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Dodaci</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Upravitelj dodacima</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">Dodaci su privremeno onemogućeni</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">Jedan ili više dodataka prestalo je raditi, što čini vaš sustav nestabilnim.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Ponovno pokrenite dodatke</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Pronađi još dodataka</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Dozvoli</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Zabrani</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Postoji nova verzija za %1$s</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Potrebne su %1$d nove dozvole</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Potrebna je nova dozvola</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Aktualiziranja dodatka</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Provjeritelj dostupnih dodataka</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Novi dodatak je dostupan</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Novi dodaci su dostupni</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Dodaj %1$s u %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Dodaj %1$s i %2$s u %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Dodaj ih u %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Tehnologija Firefoxovih dodataka se modernizira. Ovi dodaci koriste okvire koji nisu kompatibilni s Firefoxom 75 i novijim.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Trenutno razvijamo podršku za početni izbor preporučenih proširenja.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Preuzimanje i provjeravanje dodatka …</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Neuspjelo traženje dodataka!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Prijevod nije pronađen za %1$s jezik, niti za standardni %2$s jezik</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Dodatak %1$s je uspješno instaliran</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Neuspjelo instaliranje dodatka %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Neuspjela instalacija ovog dodatka.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">Dodatak nije bilo moguće preuzeti zbog greške s povezivanjem.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Dodatak nije bilo moguće instalirati jer je neispravan.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Dodatak nije bilo moguće instalirati jer nije provjeren.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s nije moguće instalirati jer nije kompatibilan s %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s nije bilo moguće instalirati jer postoji visok rizik od uzrokovanja problema sa stabilnošću i sigurnošću.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Dodatak %1$s je uspješno aktiviran</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Neuspjelo aktiviranje dodatka %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Dodatak %1$s je uspješno deaktiviran</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Neuspjelo deaktiviranje dodatka %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Dodatak %1$s je uspješno deinstaliran</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Neuspjelo deinstaliranje dodatka %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Dodatak %1$s je uspješno uklonjen</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Neuspjelo uklanjanje dodatka %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ovaj je dodatak preuzet iz prethodne %1$s verzije</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 dodatak</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s dodatka</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saznaj više</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Uspješno aktualizirano</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ne postoji nova verzija</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Greška</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informacije o aktualiziranjima</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Zadnji pokušaj:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stanje:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Dodatak %1$s je dodan u %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Otvori u izborniku</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">U redu, shvaćam</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Saznajte više</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Dodatak %1$s je blokiran zbog problema sa sigurnosti ili stabilnosti.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s nije mogao biti verificiran kao siguran te je onemogućen.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s nije kompatibilan s vašim %2$s (inačica %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..b04fb7909a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Nastajenja priwatnosće čitać a změnić</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Mějće přistup k swojim datam za wšě websydła</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Mějće přistup k swojim datam za %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Mějće přistup k swojim datam za sydła w domenje %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Mějće přistup k swojim datam na 1 dalšim websydle</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Mějće přistup k swojim datam na %1$d dalšich websydłach</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Mějće přistup k swojim datam na 1 dalšej domenje</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Mějće přistup k swojim datam na %1$d dalšich domenach</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d z %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Mějće přistup k rajtarkam wobhladowaka</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Njewobmjezowanu ličbu klientowych datow składować</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Mějće přistup k aktiwiće wobhladowaka za nawigaciju</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Zapołožki čitać a změnić</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Nastajenja wobhladowaka čitać a změnić</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Aktualnu přehladowansku historiju, placki a přisłušne daty zhašeć</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Daty z mjezyskłada zasadźić</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Daty do mjezyskłada kopěrować</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Wobsah na kóždej stronje blokować</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Čitajće swoju přehladowansku historiju</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Sćehńće dataje a čitajće a změńće sćehnjensku historiju swojeho wobhladowaka</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Dataje wočinić, kotrež su so na wašim graće sćahnyli</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Tekst wšěch wočinjenych rajtarkow čitać</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Přistup k wašemu stejnišću</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Mějće přistup k přehladowanskej historiji</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Wužiwanje rozšěrjenjow wobkedźbować a drasty rjadować</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Wuměńće powěsće z druhimi nałoženjemi hač tute</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Zdźělenki na was pokazać</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kryptografiske awtentifikowanske słužby wobstarać</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Proksynastajenja wobhladowaka kontrolować</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Mějće přistup k njedawno začinjenym rajtarkam</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Rajtarki wobhladowaka schować a pokazać</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Mějće přistup k přehladowanskej historiji</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Rozšěrće wuwiwarske nastroje, zo byšće přistup k swojim datam we wočinjenych rajtarkach měł</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Wersija</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Awtor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Awtorojo</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Poslednja aktualizacija</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Startowa strona</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Dalše informacije wo prawach</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Pohódnoćenje</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Wjace wo tutym přidatku</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Wjace wo tutym rozšěrjenju</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Nastajenja</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Zapinjeny</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Wupinjeny</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">W priwatnym modusu dowolić</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">W priwatnym modusu wuwjesć</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">W priwatnych woknach njedowoleny</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Zmóžnjeny</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Znjemóžnjeny</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalowany</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Doporučene</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Hišće njepodpěrane</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Hišće k dispoziciji njeje</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Znjemóžnjeny</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Podrobnosće</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Prawa</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Wotstronić</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Zdźělić</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s přidać?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s wo přidatne prawa prosy.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Waša dowolnosć je trěbna za:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Požadane prawa:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Přidać</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Dowolić</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Wotpokazać</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Přetorhnyć</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Přidatk instalować</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s instalować</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Přetorhnyć</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Pohódnoćenja: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Pohódnoćenje: %1$.02f z 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Přidatki</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Zrjadowak přidatkow</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Přidatki su nachwilu znjemóžnjene</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Rozšěrjenja su na chwilu znjemóžnjene</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Jedyn přidatk abo wjacore přidatki hižo njefunguja a waš system je nětko instabilny.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Jedne rozšěrjenje abo wjacore rozšěrjenja hižo njefunguja a waš system je nětko instabilny.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Přidatki znowa startować</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Rozšěrjenja znowa startować</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Dalše přidatki pytać</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Dalše rozšěrjenja pytać</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Dowolić</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Wotpokazać</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ma nowu aktualizaciju</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Ličba nowych trěbnych prawow: %1$d</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Nowe prawo je trěbne</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Aktualizacije přidatkow</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Aktualizacije za rozšěrjenja</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Přepruwowanje podpěranych přidatkow</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nowy přidatk k dispoziciji</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nowe přidatki k dispoziciji</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s %1$s přidać</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s %1$s a %2$s přidać</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Je %1$s přidać</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Přidatkowa technologija Firefox so modernizuje. Tute přidatki programowanske rošty wužiwaja, kotrež z Firefox 75 a nowšimi kompatibelne njejsu.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Wutworjamy tuchwilu podpěru za spočatny wuběr doporučenych rozšěrjenjow.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Přidatk so sćahuje a přepruwuje…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Rozšěrjenje so sćahuje a přepruwuje…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Přidatki njedachu so naprašować!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Rozšěrjenja njedachu so naprašować!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Přełožk njeje so namakał, ani za lokale %1$s ani za standardnu rěč %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s je so wuspěšnje instalował</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s njeda so instalować</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Tutón přidatk njeda so instalować.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Tute rozšěrjenje njeda so instalować.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Tutón přidatk njeda so zwiskoweho zmylka dla sćahnyć.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Tute rozšěrjenje njeda so zwiskoweho zmylka dla sćahnyć.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Tutón přidatk njeda so instalować, dokelž zda so, zo je wobškodźeny.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Tute rozšěrjenje njeda so instalować, dokelž zda so, zo je wobškodźene.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Tutón přidatk njeda so instalować, dokelž njeje přepruwowany.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Tute rozšěrjenje njeda so instalować, dokelž njeje přepruwowane.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s njeda so instalować, dokelž z %2$s %3$s kompatibelny njeje.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s njeda so instalować, dokelž je wulke riziko, zo wón stabilnostne abo wěstotne problemy zawinuje.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s je so wuspěšnje zmóžnił</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s njeda so zmóžnić</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s je so wuspěšnje znjemóžnił</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s njeda so znjemóžnić</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s je so wuspěšnje wotinstalował</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s njeda so wotinstalować</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s je so wuspěšnje wotstronił</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s njeda so wotstronić</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Tutón přidatk je so wot přechadneje wersije %1$s přenjesł</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Přidatk: 1</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 rozšěrjenje</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">Přidatki: %1$s</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">Rozšěrjenja: %1$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Dalše informacije</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Wuspěšnje zaktualizowany</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Aktualizacija k dispoziciji njeje</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Zmylk</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informacije wo aktualizaciji</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Posledni pospyt:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s je so %2$s přidał.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">W meniju wočinić</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Mějće přistup k %1$s z menija %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">W porjadku, sym zrozumił</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">W porjadku</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Dalše informacije</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s je so dla problemow wěstoty abo stabilnosće znjemóžnił.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s njeda so jako wěsty wobkrućić a je so znjemóžnił.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s z wašej wersiju %2$s (wersija %3$s) kompatibelny njeje.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..d0e981e4b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-hu/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Adatvédelmi beállítások olvasása és módosítása</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Az adatai elérése az összes webhelyhez</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Az adatai elérése itt: %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Az adatai elérése a(z) %1$s tartományban lévő lapokhoz</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Hozzáférés az adataihoz 1 másik weboldalon</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Hozzáférés az adataihoz %1$d másik weboldalon</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Hozzáférés az adataihoz 1 másik tartományban</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Hozzáférés az adataihoz %1$d másik tartományban</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d / %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Böngészőlapok elérése</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Korlátlan mennyiségű kliensoldali adat tárolása</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Böngészőtevékenység elérése navigáláskor</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Könyvjelzők olvasása és módosítása</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Böngészőbeállítások olvasása és módosítása</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Friss böngészési előzmények, sütik és kapcsolódó adatok törlése</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Vágólap tartalmának lekérése</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Adatok vágólapra helyezése</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bármely oldalon lévő tartalom blokkolása</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Böngészési előzmények olvasása</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Fájlok letöltése, valamint a letöltési előzmények olvasása és módosítása</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Az eszközre letöltött fájlok megnyitása</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Az összes nyitott lap szövegének olvasása</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Földrajzi hely adatainak elérése</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Böngészés előzményeinek elérése</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Kiegészítőhasználat monitorozása és témák kezelése</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Üzenetváltás más programokkal</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Értesítések megjelenítése</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kriptográfiai hitelesítési szolgáltatások biztosítása</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Böngésző proxy beállítások vezérlése</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Nemrég bezárt lapok elérése</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Böngészőlapok elrejtése és megjelenítése</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Böngészés előzményeinek elérése</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Fejlesztőeszközök kinyitása, hogy elérje a nyitott lapokon lévő adatokat</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Verzió</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Szerző</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Szerzők</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Legutóbb frissítve</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Honlap</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">További tudnivalók az engedélyekről</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Értékelés</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">További tudnivalók a kiegészítőről</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">További tudnivalók a kiegészítőről</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Beállítások</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Be</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Ki</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Engedélyezés privát böngészésben</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Futtatás privát böngészésben</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Privát ablakokban nem engedélyezett</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Engedélyezve</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Letiltva</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Telepítve</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Ajánlott</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Még nem támogatott</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Még nem érhető el</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Letiltva</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Részletek</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Engedélyek</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Eltávolítás</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Jelentés</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Hozzáadja: %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">A(z) %1$s további engedélyeket igényel.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">A következő engedélyeket igényli:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Ezeket szeretné:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Hozzáadás</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Engedélyezés</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Tiltás</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Mégse</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Kiegészítő telepítése</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">A(z) %1$s telepítése</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Mégse</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Értékelések: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Értékelés: %1$.02f az 5-ből</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Kiegészítők</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Kiegészítőkezelő</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">A kiegészítők ideiglenesen le vannak tiltva</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">A kiegészítők ideiglenesen le vannak tiltva</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Egy vagy több kiegészítő leállt, ami instabillá teszi a rendszert.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Egy vagy több kiegészítő leállt, ami instabillá teszi a rendszert.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítők újraindítása</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Kiegészítők újraindítása</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Több kiegészítő keresése</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">További kiegészítők keresése</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Engedélyezés</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Tiltás</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Új %1$s frissítés érhető el</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d új engedély szükséges</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Új engedély szükséges</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítő-frissítések</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Kiegészítőfrissítések</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Támogatott kiegészítők ellenőrzője</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Új kiegészítő érhető el</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Új kiegészítők érhetők el</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s hozzáadása ehhez: %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s és %2$s hozzáadása ehhez: %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Hozzáadás ehhez: %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">A Firefox kiegészítő technológiája modernizálódik. Ezek a kiegészítők olyan keretrendszereket használnak, amelyek nem kompatibilisek a Firefox 75 és újabb verziókkal.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Jelenleg is dolgozunk azon, hogy elérhetővé tegyük az első ajánlott kiegészítőket.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítő letöltése és ellenőrzése…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Kiegészítő letöltése és ellenőrzése…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Nem sikerült lekérdezni a kiegészítőket!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Nem sikerült lekérdezni a kiegészítőket!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">A fordítás nem található sem a(z) %1$s nyelv, sem az alapértelmezett %2$s nyelv esetén</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">A(z) %1$s sikeresen telepítve</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">A(z) %1$s telepítése sikertelen</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Ennek a kiegészítőnek a telepítése sikertelen.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Nem sikerült telepíteni a kiegészítőt.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">A kiegészítőt csatlakozási hiba miatt nem lehetett letölteni.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">A kiegészítőt csatlakozási hiba miatt nem lehetett letölteni.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">A kiegészítőt nem lehetett telepíteni, mert úgy tűnik, hogy sérült.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">A kiegészítőt nem lehetett telepíteni, mert úgy tűnik, hogy sérült.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">A kiegészítőt nem lehetett telepíteni, mert nincs ellenőrizve.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">A kiegészítőt nem lehetett telepíteni, mert nincs ellenőrizve.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">A(z) %1$s kiegészítőt nem lehetett telepíteni, mert nem kompatibilis a következővel: %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">A(z) %1$s kiegészítőt nem lehetett telepíteni, mert stabilitási és biztonsági szempontból magas kockázatú.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">A(z) %1$s sikeresen engedélyezve</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">A(z) %1$s engedélyezése sikertelen</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">A(z) %1$s sikeresen letiltva</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">A(z) %1$s letiltása sikertelen</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">A(z) %1$s sikeresen eltávolítva</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">A(z) %1$s eltávolítása sikertelen</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">A(z) %1$s eltávolítása sikertelen</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">A(z) %1$s eltávolítása sikertelen</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ez a kiegészítő a %1$s egy régebbi verziójából lett migrálva</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 kiegészítő</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 kiegészítő</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s kiegészítő</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s kiegészítő</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">További tudnivalók</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Sikeresen frissítve</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nem található frissítés</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Hiba</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Frissítő információk</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Utolsó kísérlet:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Állapot:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">A(z) %1$s hozzá lett adva ehhez: %2$s.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Nyissa meg a menüben</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Érje el a(z) %1$s kiegészítőt a %2$s menüjéből.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Rendben, értem</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Rendben</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">További tudnivalók</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">A(z) %1$s biztonsági vagy stabilitási problémák miatt le lett tiltva.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">A(z) %1$s biztonságossága nem ellenőrizhető programban, és le lett tiltva.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">A(z) %1$s nem kompatibilis a %2$s verziójával (verzió: %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..888e203420
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Կարդալ և փոփոխել գաղտնիության կարգավորումները</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Մատչել ձեր տվյալները բոլոր կայքերի համար</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Մատչել ձեր տվյալներին %1$s-ում</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Մատչել ձեր տվյալներին %1$s տիրույթի կայքերի համար</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Մատչել ձեր տվյալներին 1 այլ կայքում</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Մատչել ձեր տվյալներին %1$d այլ կայքերում</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Մատչել ձեր տվյալներին 1 այլ տիրույթում</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Մատչել ձեր տվյալներին %1$d այլ տիրույթներում</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d՝ %3$d-ից</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Մատչել դիտարկիչի ներդիրները</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Պահել անսահմանափակ քանակությամբ սպասառուի տվյալներ</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Մուտք գործել դիտարկիչի գործունեությունը նավիգացիայի ընթացքում</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Կարդացեք և փոփոխեք էջանիշերը</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Կարդացեք և փոփոխեք դիտարկիչի կարգավորումները</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Մաքրել վերջին դիտարկումները, թխուկները և նման տվյալներ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Ստանալ տվյալը սեղմատախտակից</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Ներածել տվյալը սեղմատախտակ</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Արգելափակել բովանդակությունը ցանկացած էջում</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Կարդալ դիտարկումների պատմությունը</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Ներբեռնեք ֆայլեր և կարդացեք ու փոփոխեք դիտարկիչի ներբեռնումների պատմությունը</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Բացեք ձեր սարքում ներբեռնված ֆայլերը</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Կարդալ բոլոր բաց ներդիրների գրվածքը</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Տեղադրության մատչում</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Դիտարկումների պատմության մատչում</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Հետևել ընդլայնման օգտագործմանը և կառավարել ոճերը</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Հաղորդագրությունների փոխանակում այլ հավելվածների հետ</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Ցուցադրել ծանուցումները</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Տրամադրել գաղտնագրման իսկորոշման ծառայություններ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Կառավարել դիտարկիչի պրոքսի կարգավորումները</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Մատչել վերջերս փակված ներդիրներին</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Թաքցնել և ցուցադրել դիտարկիչի ներդիրները</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Մատչել դիտարկիչի պատմությունը</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Երկարաձգել մշակողի գործիքները՝ մատչելու համար ձեր տվյալները բաց ներդիրներում</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Տարբերակ</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Հեղինակ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Հեղինակներ</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Վերջին թարմացումը</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Տնային էջ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Իմանալ ավելին թույլտվությունների մասին</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Վարկանիշ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Ավելին այս հավելման մասին</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Ավելին այս ընդլայնման մասին</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Կարգավորումներ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Միաց.</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Անջ.</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Թույլատրել Մասնավոր դիտարկմամբ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Աշխատեցնել գաղտնի դիտարկմամբ</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Թույլատրված չէ գաղտնի պատուհաններում</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Միացված</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Անջատված</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Տեղադրված</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Խորհուրդ տրված</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Դեռ չի աջակցվում</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Դեռ մատչելի չէ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Անջատված է</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Մանրամասներ</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Թույլտվություններ</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Հեռացնել</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Հաղորդել</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Ավելացնե՞լ %1$s-ը:</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s-ը պահանջում է լրացուցիչ թույլտվություններ:</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Այն պահանջում է ձեր թույլտվությունը՝</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Այն ցանկանում է՝</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Ավելացնել</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Թույլատրել</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Արգելել</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Չեղարկել</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Տեղադրել հավելումը</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Տեղադրել %1$s-ը</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Չեղարկել</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Կարծիքներ. %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Վարկանիշ՝ %1$.02f-ը 5-ից</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Հավելումներ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Հավելումների կառավարիչ</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումները ժամանակավորապես անջատված են</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Ընդլայնումները ժամանակավորապես անջատված են</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Մեկ կամ ավելի հավելումներ դադարեցին աշխատել՝ դարձնելով Ձեր համակարգն անկայուն:</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Մեկ կամ ավելի ընդլայնումներ դադարեցին աշխատել՝ դարձնելով Ձեր համակարգն անկայուն:</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Վերամեկնարկել հավելումները</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Վերամեկնարկել ընդլայնումները</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Գտնել ավելի շատ հավելումներ</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Գտնել ավելի շատ ընդլայնումներ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Թույլատրել</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Արգելել</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s-ը նոր թարմացում ունի</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d նոր թույլտվություններ են պահանջվում</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Նոր թույլտվություն է պահանջվում</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումների թարմացումներ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Ընդլայնման թարմացումներ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Աջակցվող հավելումների ստուգում</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Նոր հավելում է մատչելի</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Նոր հավելում է մատչելի</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Ավելացնել %1$s-ը %2$s-ում</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Ավելացնել %1$s-ը և %2$s-ը %3$s-ում</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Ավելացնել ոճը %1$s-ում</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox հավելումների տեխնոլոգիան արդիականացվում է: Այս հավելումները օգտագործում են կառուցվածքներ, որոնք համատեղելի չեն Firefox 75-ում և դրանից դուրս:</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Ներկայումս մենք կառուցում ենք աջակցություն՝ առաջարկվող ընդլայնումների նախնական ընտրության համար:</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Ներբեռնում և ստուգում է հավելումը…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Ընդլայնման ներբեռնում և ստուգում…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Չհաջողվեց հարցում կատարել հավելումներին:</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Չհաջողվե՛ց հարցում կատարել ընդլայնումների համար:</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Թարգմանությունը չի գտնվել ոչ %1$s լեզվի համար, ոչ էլ սկզբնադիր %2$s լեզվի համար</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s-ը հաջողությամբ տեղադրվեց</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Չհաջողվեց տեղադրել %1$s-ը</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Չհաջողվեց տեղադրել այս հավելումը:</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Չհաջողվեց տեղադրել այս ընդլայնումը:</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումը հնարավոր չէ ներբեռնել կապի խափանման պատճառով:</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Ընդլայնումը հնարավոր չէ ներբեռնել կապի խափանման պատճառով:</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Այս հավելումը չի կարող տեղադրվել, քանի որ այն վնասված է:</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Այս ընդլայնումը չի կարող տեղադրվել, քանի որ այն վնասված է:</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Այս հավելումը չի կարող տեղադրվել, քանի որ այն ստուգված չէ:</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Այս ընդլայնումը չի կարող տեղադրվել, քանի որ այն ստուգված չէ:</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s-ը չի կարող տեղադրվել, քանի որ համատեղելի չէ %2$s %3$s-ի հետ:</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s-ը չի կարող տեղադրվել, քանի որ ունի կայունության կամ անվտանգության խնդիրներ առաջացնելու մեծ վտանգ;</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s-ը հաջողությամբ միացվեց</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Չհաջողվեց միացնել %1$s-ը</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s-ը հաջողությամբ անջատվեց</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Չհաջողվեց անջատել %1$s-ը</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s-ը հաջողությամբ ապատեղադրվեց</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Չհաջողվեց ապատեղադրել %1$s-ը</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s-ը հաջողությամբ հեռացվեց</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Չհաջողվեց հեռացնել %1$s-ը</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Այս հավելումը տեղափոխվել է %1$s-ի նախորդ տարբերակից</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 հավելում</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 ընդլայնում</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s հավելումներ</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s ընդլայնումներ</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Իմանալ ավելին</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Հաջողությամբ թարմացվեց</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Թարմացումներ չկան</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Սխալ</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Թարմացիչի տեղեկություն</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Վերջին փորձը.</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Վիճակ.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s-ը ավելացվել է %2$s-ում</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Բացել այն ցանկում</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Մատչում %1$s-ին %2$s-ի ցանկից։</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Հասկանալի է</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Լավ</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Իմանալ ավելին</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s-ը անջատվել է` անվտանգության կամ կայունության խնդիրների պատճառով:</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s-ը չէր կարող հաստատվել որպես անվտանգ և անջատվել է:</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s-ը անհամատեղելի է %2$s-ի ձեր տարբերակի հետ (տարբերակ՝ %3$s) :</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..2649a8880c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ia/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leger e modificar le parametros de confidentialitate</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a tu datos pro tote le sitos web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder a tu datos pro %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder a tu datos pro le sitos del dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Accede tu datos sur 1 altere sito</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Accede tu datos sur %1$d altere sitos</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Accede tu datos sur 1 altere dominio</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Accede tu datos sur %1$d altere dominios</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder al schedas del navigator</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Immagazinar un quantitate illimitate de datos in le latere del cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder al activitate del navigator durante le navigation</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leger e modificar le marcapaginas</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leger e modificar le parametros del navigator</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Vacuar le chronologia de navigation recente, le cookies e le datos relative</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obtener datos ab le area de transferentia</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Inserer le datos in le area de transferentia</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloca contento sur ulle pagina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lege tu chronologia de navigation</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Discargar files e leger e modificar le chronologia de discargamentos del navigator</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Aperir le files discargate in tu apparato</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leger le texto de tote le schedas aperite</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a tu geolocalisation</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder al chronologia de navigation</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Surveliar le utilisation del extensiones e gerer le themas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Excambiar messages con apps differente de isto</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Monstrar te le notificationes</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Forni servicios de authentication cryptographic</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar le parametros del proxy del navigator</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder al schedas claudite recentemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Celar e monstrar le schedas del navigator</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder al chronologia de navigation</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Extender le instrumentos de disveloppamento pro acceder a tu datos in le schedas aperte</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ultime actualisation</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pagina initial</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saper plus sur le permissiones</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Valutation</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Altero re iste additivo</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Altero re iste extension</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Parametros</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Active</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Inactive</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitter in navigation private</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Exequer in navigation private</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Non permittite in fenestras private</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activate</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Disactivate</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installate</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recommendate</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Non ancora supportate</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Nondum disponibile</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Disactivate</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalios</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissiones</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Remover</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Reportar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Adder %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s require altere permissiones.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Illo require tu permission pro:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Illo vole:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Adder</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitter</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Denegar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancellar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installar additivo</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancellar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recensiones: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Qualification: %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Additivos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gestor de additivos</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Additivos temporarimente disactivate</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Le extensiones es temporarimente disactivate</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Un o plus additivos cessava de functionar rendente instabile tu systema.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Un o plus extensiones cessava de functionar rendente instabile tu systema.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reinitiar additivos</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reinitiar extensiones</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Trovar plus additivos</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Trovar plus extensiones</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitter</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Denegar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ha un nove actualisation</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nove permissiones requirite</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Un nove permission es requirite</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Actualisationes del additivos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Actualisationes de extension</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Verificator de additivos supportate</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nove additivo disponibile</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nove additivos disponibile</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Adder %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Adder %1$s e %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Adder los a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Le technologia del additivos de Firefox se modernisa. Iste additivos usa structuras que non es compatibile con Firefox 75 &amp; ultra.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Nos face actualmente assistentia a un selection initial de Extensiones recommendate.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Discargamento e verification del additivo…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Discargante e verificante extension…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Impossibile consultar additivos!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Impossibile consultar extensiones!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Traduction non trovate, pro le lingua %1$s ni pro illo predefinite %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s installate con successo</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Impossibile installar %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Impossibile installar iste additivo.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Impossibile installar iste extension.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Iste additivo non pote esser discargate a causa de un falta de connexion.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Iste extension non pote esser discargate a causa de un falta de connexion.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Iste additivo non ha potite esser installate perque pare corrumpite.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Iste extension non ha potite esser installate perque illo appare esser corrumpite.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Iste additivo non ha potite esser installate perque illo non ha essite verificate.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Iste extension non ha potite esser installate perque illo non ha essite verificate.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s non ha potite esser installate perque non es compatibile con %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s non poteva esser installate perque ha un alte risco de causar problemas de stabilitate o de securitate.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s activate con successo</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Impossibile activar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s disactivate con successo</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Impossibile disactivar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s disinstallate con successo</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Impossibile disinstallar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s removite con successo</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Impossibile remover %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Iste additivo ha migrate de un previe version de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 additivo</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extension</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s Additivos</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensiones</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saper plus</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualisate con successo</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Il non ha actualisationes</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informationes del actualisator</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ultime tentativa:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stato:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ha essite addite a %2$s.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Aperi lo in le menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Accede a %1$s ex menu de %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK, io lo comprende</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Pro saper plus</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ha essite disactivate a causa de problemas de securitate o de stabilitate.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s non poteva esser verificate como secur e ha essite inactivate.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s non es compatibile con tu version de %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..c7d0bd61df
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-in/strings.xml
@@ -0,0 +1,270 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Melihat dan mengubah pengaturan privasi</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Akses data Anda untuk semua situs web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Mengakses data Anda pada %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Mengakses data Anda untuk situs pada domain %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Mengakses data Anda pada 1 situs lainnya</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Mengakses data Anda pada %1$d situs lainnya</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Mengakses data Anda pada 1 domain lainnya</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Mengakses data Anda pada %1$d domain lainnya</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d dari %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Akses tab peramban</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Simpan data untuk sisi klien dalam jumlah tak terbatas</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Mengakses aktivitas peramban selama navigasi</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Membaca dan mengubah markah</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Melihat dan mengubah setelan peramban</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Bersihkan riwayat penjelajahan terbaru, kuki, dan data terkait</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Mendapatkan data dari papan klip</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Memasukkan data ke papan klip</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokir konten di laman mana pun</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Baca riwayat penjelajahan Anda</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Unduh berkas dan baca serta ubah riwayat unduhan peramban</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Buka berkas yang sudah diunduh ke perangkat Anda</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Baca teks dari semua tab yang terbuka</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Akses lokasi Anda</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Akses riwayat penjelajahan</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Pantau penggunaan ekstensi dan kelola tema</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Bertukar pesan dengan aplikasi selain yang ini</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Menampilkan notifikasi untuk Anda</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Menyediakan layanan autentikasi kriptografi</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Mengendalikan setelan proksi peramban</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Mengakses tab yang baru saja ditutup</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Sembunyikan dan tampilkan tab peramban</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Mengakses riwayat penjelajahan</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Perpanjang akses alat pengembang ke data Anda di dalam tab terbuka</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versi</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Penyusun</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Pembuat</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Terakhir diperbarui</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Beranda</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Pelajari lebih lanjut tentang perizinan</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Peringkat</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Lebih jauh tentang pengaya ini</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Lebih lanjut tentang ekstensi ini</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Pengaturan</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Aktif</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Nonaktif</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Izinkan di penjelajahan pribadi</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Jalankan di penjelajahan pribadi</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Tidak diizinkan di jendela pribadi</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Aktif</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Nonaktif</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Terpasang</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Disarankan</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Belum didukung</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Belum tersedia</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Nonaktif</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detail</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Hak Akses</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Hapus</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Laporkan</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Tambahkan %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s meminta izin tambahan.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Hal ini membutuhkan izin Anda untuk:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Pengaya ingin:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Tambahkan</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Izinkan</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Tolak</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Batalkan</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Pasang Pengaya</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Pasang %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Batal</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Ulasan: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Peringkat: %1$.02f dari 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-on</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Pengelola Pengaya</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Pengaya dinonaktifkan untuk sementara
+</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Ekstensi dinonaktifkan untuk sementara</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Satu atau lebih pengaya berhenti bekerja, membuat sistem Anda tidak stabil.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Mulai ulang pengaya</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Temukan lebih banyak pengaya</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Izinkan</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Tolak</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s memiliki pembaruan baru</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d memerlukan izin baru</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Izin baru dibutuhkan</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Pembaruan pengaya</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Pemeriksa pengaya yang didukung</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Pengaya baru tersedia</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Pengaya baru tersedia</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Tambahkan %1$s ke %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Tambahkan %1$s dan %2$s ke %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Tambahkan ke %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Teknologi pengaya Firefox sedang dimodernisasi. Pengaya ini menggunakan kerangka kerja yang tidak kompatibel dengan Firefox 75 dan seterusnya.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Saat ini kami sedang membangun dukungan untuk pemilihan awal Ekstensi yang Disarankan.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Mengunduh dan memverifikasi pengaya…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Gagal meminta Pengaya!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Terjemahan tidak ditemukan, baik untuk bahasa %1$s maupun %2$s sebagai bahasa baku</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Berhasil menginstal %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Gagal menginstal %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Gagal menginstal pengaya ini.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Pengaya tidak dapat diunduh karena kegagalan sambungan.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Pengaya ini tidak dapat dipasang karena tampaknya datanya rusak.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Pengaya ini tidak dapat dipasang karena belum diverifikasi.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s tidak dapat dipasang karena tidak kompatibel dengan %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s tidak dapat dipasang karena berisiko tinggi menyebabkan masalah stabilitas atau keamanan.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Berhasil mengaktifkan %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Gagal mengaktifkan %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Berhasil menonaktifkan %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Gagal menonaktifkan %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Berhasil menghapus instalasi %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Gagal menghapus instalasi %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Berhasil menghapus %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Gagal menghapus %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Pengaya ini dimigrasikan dari versi sebelumnya dari %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 pengaya</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s pengaya</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Pelajari lebih lanjut</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Berhasil diperbarui</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Tidak ada pembaruan yang tersedia</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Galat</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informasi Pembaru</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Upaya terakhir:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s telah ditambahkan ke %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Buka ini di menu</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Oke, Paham</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Pelajari lebih lanjut</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s telah dinonaktifkan karena masalah keamanan atau kestabilan.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s tidak dapat diverifikasi sebagai aman dan telah dinonaktifkan.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s tidak kompatibel dengan versi %2$s Anda (versi %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..8dc020a5a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-is/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lesa og breyta friðhelgisstillingum</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Skoða gögnin þín fyrir öll vefsvæði</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Skoða gögnin þín fyrir %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Skoða gögnin þín fyrir vefsvæði á %1$s léninu</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Skoða gögnin þín fyrir 1 annað vefsvæði</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Skoða gögnin þín fyrir %1$d önnur vefsvæði</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Skoða gögnin þín fyrir 1 annað lén</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Skoða gögnin þín fyrir %1$d önnur lén</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d af %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Skoða vafraflipa</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Geyma óendanlega mikið af notanda gögnum</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Skoða vafranotkun á meðan vafrað er</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Skoða og breyta bókamerkjum</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Skoða og breyta vafrastillingum</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Hreinsa feril, vefkökur og tengd gögn</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Ná í gögn af klippispjaldi</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Setja inn gögn á klippispjald</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Lokaðu fyrir efni á hvaða síðu sem er</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lesa vafurferilinn þinn</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Sækja skrár og lesta og breyta niðurhalsferli vafrans</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Opna skrár sem hafa verið sóttar á tækið þitt</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lesa texta á öllum flipum</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Leyfa aðgang að staðsetningu þinni</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Skoða ferilsögu vafra</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Fylgjast með notkun á viðbótum og sýsla með þemu</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Deila skilaboðum með öðrum forritum en þessu</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Birta þér tilkynningar</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Veita dulkóðunar auðkennisþjónustur</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Stjórna stillingum fyrir milliþjóna</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Aðgangur að nýlokuðum flipum</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Fela og sýna flipa</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Skoða ferilsögu vafra</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Veita þróunartólum aðgang að gögnum þínum í opnum flipum</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Útgáfa</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Höfundur</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Höfundar</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Seinast uppfært</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Heimasíða</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Fræðast meira um réttindi</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Einkunn</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Meira um þessa viðbót</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Meira um þennan forritsauka</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Stillingar</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Virk</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Óvirk</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Leyfa í huliðsvafri</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Keyra í huliðsvafri</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Ekki leyfilegt í huliðsgluggum</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Virk</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Óvirk</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Uppsett</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Mælt með</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Stuðningur ekki enn fyrir hendi</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ekki enn tiltækt</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Óvirkar</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Nánar</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Heimildir</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Fjarlægja</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Tilkynna</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Bæta %1$s við?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s biður um auknar heimildir.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Leyfi þitt þarf fyrir:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Það vill:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Bæta við</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Leyfa</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Hafna</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Hætta við</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Setja inn viðbót</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Setja upp %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Hætta við</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Umsagnir: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Einkunn: %1$.02f af 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Viðbætur</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Viðbótastjóri</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Viðbætur eru tímabundið óvirkar</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Forritsaukar eru tímabundið óvirkir</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Einn eða fleiri forritsaukar hættu að virka, sem gerir kerfið þitt óstöðugt.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Einn eða fleiri forritsaukar hættu að virka, sem gerir kerfið þitt óstöðugt.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Endurræsa viðbætur</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Endurræsa forritsauka</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Finna fleiri viðbætur</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Finna fleiri forritsauka</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Leyfa</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Hafna</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s er með nýja uppfærslu</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Nýjar heimildir fyrir %1$d eru nauðsynlegar</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Ný heimild er nauðsynleg</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Uppfærslur fyrir viðbætur</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Uppfærslur forritsauka</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Studdar viðbætur</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Ný viðbót í boði</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nýjar viðbætur í boði</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Bæta %1$s við %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Bæta %1$s og %2$s við %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Bæta þeim við %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Tæknin fyrir viðbætur í Firefox er að þróast. Þessar viðbætur nota undirliggjandi kerfi sem eru ekki samhæf Firefox 75 og nýrri.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Við erum í óða önn að hanna stuðning við upphafsval á ráðlögðum forritsaukum.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Sæki og staðfesti viðbót…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Sæki og staðfesti forritsauka…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Ekki tókst að sækja viðbætur!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Mistókst að fletta í forritsaukum!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Þýðing fannst ekki, hvorki fyrir staðfærsluna %1$s né fyrir sjálfgefna tungumálið %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Það tókst að setja inn %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Ekki tókst að setja inn %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Mistókst að setja upp þessa viðbót.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Mistókst að setja upp þennan forritsauka.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Ekki tókst að sækja þessa viðbót þar sem tenging brást.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Ekki tókst að sækja forritsaukann þar sem tenging slitnaði.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Ekki tókst að setja inn viðbótina þar sem hún virðist vera gölluð.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Ekki tókst að setja inn forritsaukann þar sem hann virðist vera gallaður.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Ekki var hægt að setja inn þessa viðbót því hún hefur ekki verið staðfest.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Ekki tókst að setja inn forritsaukann þar sem hann hefur ekki verið staðfestur.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Ekki tókst að setja inn %1$s þar sem hún er ekki samhæfð við %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Ekki tókst að setja inn %1$s þar sem viðbótin er þekkt fyrir að valda hrun eða öryggisvillum.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s var virkjað á árangursríkan hátt</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Ekki tókst að virkja %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s var óvirkjað á árangursríkan máta</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Ekki tókst að óvirkja %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Það tókst að fjarlægja %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Ekki tókst að fjarlægja %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s var fjarlægt á árangursríkan máta</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Ekki tókst að fjarlægja %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Þessi viðbót var flutt úr fyrri útgáfi af %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 viðbót</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 forritsauki</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s viðbætur</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s forritsaukar</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Fræðast meira</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Uppfærsla tókst</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Engar uppfærslur tiltækar</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Villa</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Upplýsingar um síðustu uppfærslu</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Síðasta tilraun:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Staða:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s hefur verið bætt við %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Opnaðu það í valmyndinni</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Finndu %1$s í %2$s valmyndinni.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Allt í lagi, ég skil</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Í lagi</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Frekari upplýsingar</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s hefur verið gerð óvirk vegna vandamála með öryggi eða stöðugleika.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Ekki var hægt að staðfesta %1$s sem öruggt og hefur það verið gert óvirkt.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s er ekki samhæft við útgáfuna þína af %2$s (útgáfa %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..2c0f38c37d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-it/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leggere e modificare le impostazioni relative alla privacy</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Accedere ai dati di tutti i siti web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Accedere ai dati utente per %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Accedere ai dati dei siti web per il dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Accedere ai dati utente su 1 altro sito</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Accedere ai dati utente su %1$d altri siti</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Accedere ai dati utente su 1 altro dominio</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Accedere ai dati utente su %1$d altri domini</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d di %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Accedere alle schede del browser</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Salvare dati sul dispositivo senza limitazioni di spazio</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Accedere all’attività del browser durante la navigazione</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leggere e modificare i segnalibri</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leggere e modificare le impostazioni del browser</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Eliminare cronologia di navigazione recente, cookie e dati associati</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Leggere dati dagli appunti</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Salvare dati negli appunti</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloccare contenuti in qualsiasi pagina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Leggere la cronologia di navigazione</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Scaricare file, leggere e modificare la cronologia di download del browser</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Aprire i file scaricati sul dispositivo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leggere il testo di tutte le schede aperte</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Accedere alla posizione</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Accedere alla cronologia di navigazione</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorare l’utilizzo delle estensioni e gestire i temi</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Scambiare messaggi con altre app</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Visualizzare notifiche</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Fornire servizi di autenticazione crittografica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controllare le impostazioni relative ai proxy</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Accedere alle schede chiuse di recente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Nascondere e mostrare schede</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Accedere alla cronologia di navigazione</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Consentire agli strumenti di sviluppo di accedere ai dati delle schede</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versione</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autore</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autori</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ultimo aggiornamento</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pagina iniziale</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Ulteriori informazioni sui permessi</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Voto</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Ulteriori informazioni su questo componente aggiuntivo</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Altre informazioni</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Impostazioni</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Attiva</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Disattivata</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Consenti in navigazione anonima</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Esegui in navigazione anonima</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Disattivata in finestre anonime</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Attivi</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Disattivato</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installata</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Consigliati</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Non ancora supportato</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Non ancora disponibile</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Disattivati</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Informazioni</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permessi</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Elimina</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Segnala</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Installare %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">Sono richiesti dei permessi aggiuntivi per %1$s.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Richiede il permesso di:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Permessi richiesti:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Installa</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Consenti</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Nega</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Annulla</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installa componente aggiuntivo</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installa %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Annulla</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recensioni: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Valutazione: %1$.02f su 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Componenti aggiuntivi</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gestione componenti aggiuntivi</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">I componenti aggiuntivi sono temporaneamente disattivati</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Le estensioni sono temporaneamente disattivate</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Uno o più componenti aggiuntivi hanno smesso di funzionare, rendendo il sistema instabile.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Una o più estensioni hanno smesso di funzionare, rendendo il sistema instabile.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Riavvia i componenti aggiuntivi</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Riavvia estensioni</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Trova altri componenti aggiuntivi</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Trova altre estensioni</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Consenti</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Nega</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Nuovo aggiornamento per %1$s</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nuovi permessi richiesti</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">È richiesto un nuovo permesso</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Aggiornamenti del componente aggiuntivo</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Aggiornamenti delle estensioni</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Verifica componenti aggiuntivi supportati</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nuovo componente aggiuntivo disponibile</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nuovi componenti aggiuntivi disponibili</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Aggiungi %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Aggiungi %1$s e %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Aggiungili a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La tecnologia dei componenti aggiuntivi di Firefox è in costante aggiornamento. Questi componenti aggiuntivi utilizzano framework incompatibili con Firefox 75 e versioni successive.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Stiamo sviluppando il supporto per una selezione iniziale di estensioni consigliate.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Download e verifica del componente aggiuntivo in corso…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Download e verifica estensione…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Impossibile eseguire la query dei componenti aggiuntivi.</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Impossibile eseguire la query delle estensioni.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Traduzione non trovata, né per la lingua %1$s né per la lingua predefinita %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s installato correttamente</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Impossibile installare %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Installazione del componente aggiuntivo non riuscita.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Installazione dell’estensione non riuscita.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Impossibile scaricare il componente aggiuntivo a causa di un errore nella connessione.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Impossibile scaricare l’estensione a causa di un errore nella connessione.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Impossibile installare il componente aggiuntivo in quanto risulta danneggiato.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Impossibile installare questa estensione in quanto risulta danneggiata.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Impossibile installare il componente aggiuntivo in quanto non verificato.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Impossibile installare questa estensione in quanto non verificata.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Impossibile installare %1$s in quanto non compatibile con %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Impossibile installare %1$s in quanto comporta un rischio elevato per la stabilità o la sicurezza.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s attivato correttamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Impossibile attivare %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s disattivato correttamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Impossibile disattivare %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s disinstallato correttamente</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Impossibile disinstallare %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s rimosso correttamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Impossibile rimuovere %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Questo componente aggiuntivo proviene dalla migrazione di una versione precedente di %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 componente aggiuntivo</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 estensione</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s componenti aggiuntivi</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s estensioni</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Ulteriori informazioni</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Aggiornato correttamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nessun agg. disponibile</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Errore</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informazioni sull‘aggiornamento</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ultimo tentativo:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stato:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s è stato aggiunto a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Apri nel menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Accedi a %1$s dal menu di %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Ulteriori informazioni</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s è stato disattivato in quanto comporta rischi per la stabilità o la sicurezza.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s è stato disattivato in quanto non è può essere verificato come sicuro.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s non è compatibile con la tua versione di %2$s (versione %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..699c769a09
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-iw/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">קריאה ועריכה של הגדרות פרטיות</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">גישה לנתונים שלך מכל האתרים</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">גישה לנתונים שלך עבור %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">גישה לנתונים שלך עבור אתרים תחת שם המתחם %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">גישה לנתונים שלך באתר אחד נוסף</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">גישה לנתונים שלך ב־%1$d אתרים נוספים</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">גישה לנתונים שלך בשם מתחם אחד נוסף</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">גישה לכל המידע שלך ב־%1$d שמות מתחם נוספים</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d מתוך %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">גישה ללשוניות</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">אחסון נתונים בלתי מוגבלים של צד לקוח</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">גישה לפעילות הדפדפן במהלך הניווט</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">קריאה ועריכת סימניות</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">קריאה ועריכת הגדרות הדפדפן</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">איפוס היסטוריית הגלישה, העוגיות והנתונים הנוספים שצברת לאחרונה</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">קבלת נתונים מלוח העריכה</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">הזנת נתונים בלוח העריכה</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">חסימת תוכן בכל עמוד</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">קריאת היסטוריית הגלישה שלך</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">הורדת קבצים, קריאה ועריכת היסטוריית ההורדות של הדפדפן</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">פתיחת קבצים שהורדו למכשיר שלך</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">קריאת הטקסט של כל הלשוניות הפתוחות</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">גישה לנתוני המיקום שלך</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">גישה להיסטוריית הגלישה</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">ניטור שימוש בהרחבות וניהול ערכות נושא</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">החלפת הודעות עם יישומונים אחרים</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">הצגת התרעות</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">אספקת שירותי אימות מוצפנים</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">שליטה בהגדרות המתווך של הדפדפן</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">גישה ללשוניות שנסגרו לאחרונה</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">הסתרה והצגת לשוניות הדפדפן</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">גישה להיסטוריית הגלישה</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">הרחבת כלי הפיתוח לקבלת גישה לנתונים שלך בלשוניות פתוחות</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">גרסה</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">מחבר</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">יוצרים</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">עדכון אחרון</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">דף הבית</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">מידע נוסף על הרשאות</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">דירוג</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">עוד על תוספת זו</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">עוד על הרחבה זו</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">הגדרות</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">פעיל</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">כבוי</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">לאפשר בגלישה פרטית</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">הפעלה בגלישה פרטית</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">לא מופעלת בחלונות פרטיים</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">מופעל</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">מושבת</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">מותקן</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">מומלצות</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">טרם נתמך</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">אינה זמינה עדיין</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">מושבת</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">פרטים</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">הרשאות</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">הסרה</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">דיווח</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">להוסיף את %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">התוספת %1$s מבקשת הרשאות נוספות.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">ההרשאות הבאות נדרשות:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">היא מבקשת:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">הוספה</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">לאפשר</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">לדחות</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ביטול</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">התקנת תוספת</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">התקנת %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ביטול</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">סקירות: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">דירוג: %1$.02f מתוך 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">תוספות</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">מנהל התוספות</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">התוספות מושבתות באופן זמני</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">ההרחבות מושבתות באופן זמני</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">תוספת אחת או יותר הפסיקו לעבוד, מה שהפך את המערכת שלך לבלתי יציבה.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">הרחבה אחת או יותר הפסיקו לעבוד, מה שהפך את המערכת שלך לבלתי יציבה.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">הפעלה מחדש של התוספות</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">הפעלה מחדש להרחבות</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">חיפוש תוספות נוספות</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">חיפוש הרחבות נוספות</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">לאפשר</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">לדחות</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">לתוספת %1$s יש עדכון חדש</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d הרשאות חדשות נדרשות</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">נדרשת הרשאה חדשה</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">עדכוני תוספות</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">עדכוני הרחבות</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">בודק התוספות הנתמכות</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">תוספת חדשה זמינה</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">תוספות חדשות זמינות</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">הוספת %1$s אל %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">הוספת %1$s ו־%2$s אל %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">להוסיף אותם אל %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">טכנולוגיית התוספות של Firefox מתחדשת. תוספות אלה משתמשות במערכות שאינן נתמכות ב־Firefox 75 ומעלה.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">אנו בונים כעת תמיכה עבור מבחר ראשוני של הרחבות מומלצות.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">בתהליך הורדה ואימות תוספת…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">בתהליך הורדה ואימות הרחבה…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">תשאול התוספות נכשל!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">תשאול ההרחבות נכשל!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">לא נמצא תרגום עבור השפה %1$s או עבור שפת ברירת המחדל %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">התוספת %1$s הותקנה בהצלחה</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">התקנת התוספת %1$s נכשלה</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">התקנת תוספת זו נכשלה.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">התקנת הרחבה זו נכשלה.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">לא ניתן היה להוריד את תוספת זו עקב כשל בחיבור.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">לא ניתן היה להוריד את הרחבה זו עקב כשל בחיבור.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">לא ניתן היה להתקין תוספת זו מכיוון שהיא ככל הנראה פגומה.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">לא ניתן היה להתקין הרחבה זו מכיוון שהיא ככל הנראה פגומה.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">לא ניתן היה להתקין תוספת זו מכיוון שהיא לא אומתה.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">לא ניתן היה להתקין הרחבה זו מכיוון שהיא לא אומתה.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">לא ניתן היה להתקין את %1$s מכיוון שאינה תואמת ל־%2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">לא ניתן היה להתקין את %1$s מכיוון שבתוספת זו סיכון גבוה לגרימת בעיות יציבות או אבטחה.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">התוספת %1$s הופעלה בהצלחה</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">הפעלת התוספת %1$s נכשלה</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">התוספת %1$s הושבתה בהצלחה</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">השבתת התוספת %1$s נכשלה</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">התוספת %1$s הוסרה בהצלחה</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">הסרת ההתקנה של התוספת %1$s נכשלה</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">התוספת %1$s הוסרה בהצלחה</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">הסרת התוספת %1$s נכשלה</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">תוספת זו הועברה מגרסה קודמת של %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">תוספת אחת</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">הרחבה אחת</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s תוספות</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s הרחבות</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">מידע נוסף</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">עודכנה בהצלחה</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">אין עדכון זמין</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">שגיאה</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">פרטי המעדכן</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ניסיון אחרון:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">מצב:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">‏%1$s נוספה אל %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">ניתן לפתוח אותה בתפריט</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">ניתן לגשת ל־%1$s מהתפריט של %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">בסדר, הבנתי</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">אישור</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">מידע נוסף</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">התוספת %1$s נחסמה עקב בעיות אבטחה או יציבות.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">לא ניתן לאמת את %1$s כמאובטחת ולכן תוספת זו הושבתה.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">התוספת %1$s אינה תואמת לגרסה של ה־%2$s שלך (גרסה %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..6a7f52070f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ja/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">プライバシー設定の読み取りと変更</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">すべてのウェブサイトの保存されたデータへのアクセス</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s のユーザーデータへのアクセス</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s ドメインにあるサイトのユーザーデータへのアクセス</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">その他 1 サイト上のデータへのアクセス</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">その他 %1$d サイト上のデータへのアクセス</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">その他 1 ドメイン上のデータへのアクセス</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">その他 %1$d ドメイン上のデータへのアクセス</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s、%2$d / %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ブラウザーのタブへのアクセス</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">クライアント側にデータをサイズ制限なしで格納</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">ナビゲーション中のブラウザーアクティビティへのアクセス</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">ブックマークの読み取りと変更</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ブラウザー設定の読み取りと変更</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">最近の閲覧履歴、Cookie および関連データの消去</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">クリップボードからのデータ取得</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">クリップボードへのデータ入力</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">任意のページのコンテンツをブロックする</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">閲覧履歴の読み取り</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ファイルのダウンロードおよびブラウザーのダウンロード履歴の読み取りと変更</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">端末にダウンロードしたファイルを開く</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">開いているすべてのタブからのテキスト読み取り</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ユーザーの位置情報へのアクセス</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">閲覧履歴へのアクセス</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">拡張機能の使用状況の監視とテーマの管理</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">別のアプリとのメッセージ交換</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">通知の表示</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">暗号認証サービスの提供</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ブラウザーのプロキシー設定の制御</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">最近閉じたタブへのアクセス</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ブラウザーのタブの表示状態の変更</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">閲覧履歴へのアクセス</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">開いているタブのユーザーデータへアクセスするため開発ツールを展開</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">バージョン</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">作者</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">作者</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">最終更新日時</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">ホームページ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">権限の詳細情報</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">評価</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">このアドオンの詳細</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">この拡張機能の詳細</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">設定</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">オン</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">オフ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">プライベートブラウジングモードでの動作を許可する</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">プライベートブラウジングモードで実行する</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">プライベートウィンドウでは許可されていません</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">有効</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">無効</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">インストール済み</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">おすすめ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">まだサポートされていません</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">まだ利用できません</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">無効</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">詳細</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">権限</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">削除</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">報告</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s を追加しますか?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s が追加の許可を必要としています。</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">あなたの許可が必要です:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">追加の許可:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">追加</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">許可</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">拒否</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">キャンセル</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">アドオンをインストール</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s をインストール</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">キャンセル</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">レビュー数: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">評価: 5 点中 %1$.02f</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">アドオン</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">アドオンマネージャー</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">アドオンは一時的に無効化されています</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">拡張機能は一時的に無効化されています</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">システムを不安定にしている 1 つ以上のアドオンが動作を停止しました。 </string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">システムを不安定にしている 1 個以上の拡張機能が動作を停止しました。 </string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">アドオンを再起動</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">拡張機能を再起動</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">アドオンを探す</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">拡張機能を探す</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">許可</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">拒否</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s が更新可能です</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d 個の権限が必要です</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">新しい権限が必要です</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">アドオンの更新</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">拡張機能の更新</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">アドオンチェッカー</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">新しいアドオンが利用可能です</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">新しいアドオンが利用可能です</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s を %2$s に追加しましょう</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s と %2$s を %3$s に追加しましょう</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">これらを %1$s に追加しましょう</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox のアドオン技術は近代化されています。これらのアドオンが使用するフレームワークは Firefox 75 以降と互換性がありません。</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">現在、最初に選ばれるおすすめの拡張機能のサポートを構築しています。</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">アドオンをダウンロードして検証しています…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">拡張機能をダウンロードして検証しています…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">アドオン一覧の取得に失敗しました!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">拡張機能一覧の取得に失敗しました!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">ロケール %1$s および既定の言語 %2$s の翻訳が見つかりませんでした</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s のインストールが完了しました</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s のインストールに失敗しました</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">アドオンのインストールに失敗しました。</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">拡張機能のインストールに失敗しました。</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">接続エラーのため、アドオンをダウンロードできませんでした。</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">接続エラーのため、拡張機能をダウンロードできませんでした。</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">このアドオンは壊れているため、インストールできませんでした。</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">この拡張機能は壊れているため、インストールできませんでした。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">このアドオンは検証されていないため、インストールできませんでした。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">この拡張機能は検証されていないため、インストールできませんでした。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%2$s %3$s と互換性がないため、%1$s をインストールできませんでした。</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">セキュリティまたは安定性に問題があるため、%1$s をインストールできませんでした。</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s を有効にしました</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s の有効化に失敗しました</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s を無効にしました</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s の無効化に失敗しました</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s のアンインストールが完了しました</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s のアンインストールに失敗しました</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s の削除が完了しました</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s の削除に失敗しました</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">このアドオンは以前のバージョン %1$s から移行されました</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 個のアドオン</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">拡張機能 1 個</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s 個のアドオン</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">拡張機能 %1$s 個</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">詳細情報</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">アドオンの更新が完了しました</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">更新可能なアドオンはありません</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">エラー</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">アップデーター情報</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">最終確認日:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">状態:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s が %2$s に追加されました</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">メニューから開いてください</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">%2$s メニューから %1$s にアクセスしてください。</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">詳細情報</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s はセキュリティまたは安定性に問題があるため無効化されています。</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s は安全性が検証できないため無効化されています。</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s は %2$s のバージョン (%3$s) と互換性がありません。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..636c00646a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ka/strings.xml
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">პირადულობის პარამეტრების ნახვა და შეცვლა</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ყველა საიტზე თქვენს მონაცემებთან წვდომა</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">თქვენს მონაცემებთან წვდომა საიტზე %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s მისამართის საიტებზე თქვენს მონაცემებთან წვდომა</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">თქვენს მონაცემებთან წვდომა 1 სხვა საიტზე</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">თქვენს მონაცემებთან წვდომა %1$d სხვა საიტზე</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">თქვენს მონაცემებთან წვდომა 1 სხვა მისამართზე</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">თქვენს მონაცემებთან წვდომა %1$d სხვა მისამართზე</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ბრაუზერის ჩანართებთან წვდომა</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">განუსაზღვრელი მოცულობის მონაცემების შენახვა დისკზე</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">გვერდების მონახულებისას ბრაუზერის მოქმედებებთან წვდომა</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">სანიშნების ნახვა და შეცვლა</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ბრაუზერის პარამეტრების ნახვა და შეცვლა</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">ბოლოს ნანახი გვერდების, ფუნთუშებისა და თანდართული მონაცემების წაშლა</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">წვდომა აღებული ასლის მონაცემებზე</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">აღებულ ასლში მონაცემების ჩამატება</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">შიგთავსის შეზღუდვა ნებისმიერ გვერდზე</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">თქვენ მიერ მონახულებული გვერდების ხილვა</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ფაილების ჩამოტვირთვა, ჩამოტვირთვების ნახვა და შეცვლა</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">თქვენს მოწყობილობაზე ჩამოტვირთული ფაილების გახსნა</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">ტექსტის წაკითხვა ყველა გახსნილი ჩანართიდან</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">მდებარეობის მონაცემებთან წვდომა</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">მონახულებული გვერდების ისტორიასთან წვდომა</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">გაფართოების მოხმარების შესახებ მონაცემების შეგროვება და თემების მართვა</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">შეტყობინებების გაცვლა გარეშე პროგრამებთან</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">შეტყობინებების ჩვენება</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ანგარიშზე დაშიფრულად შესვლის მომსახურებების მოწოდება</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ბრაუზერის პროქსის პარამეტრების მართვა</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">ბოლოს დახურულ ჩანართებთან წვდომა</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ბრაუზერის ჩანართების დამალვა და გამოჩენა</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">მონახულებული გვერდების ისტორიასთან წვდომა</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">შემმუშავებლის ხელსაწყოებით თქვენს მონაცემებთან წვდომა გახსნილ ჩანართებში</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ვერსია</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">შემქმნელი</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="120" tools:ignore="UnusedResources">შემქმნელები</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">ბოლო განახლება</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">მთავარი გვერდი</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ვრცლად ნებართვების შესახებ</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">შეფასება</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">ვრცლად ამ დამატების შესახებ</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">პარამეტრები</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ჩართ.</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">გამორთ.</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">დაშვება პირად რეჟიმში</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">გაშვება პირად რეჟიმში</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ჩართულია</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">ამორთულია</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ჩადგმულია</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">სასურველი</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ჯერ მხარდაუჭერელია</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ჯერ არაა ხელმისაწვდომი</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ამორთული</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">დაწვრილებით</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ნებართვები</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">მოცილება</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">საჩივარი</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">ჩაიდგას %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s საჭიროებს დამატებით ნებართვებს.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">მოთხოვნილი უფლებები:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">მოთხოვნილია:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">ჩადგმა</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">ნებართვა</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">უარყოფა</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">გაუქმება</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">დამატების ჩადგმა</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">გაუქმება</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">მიმოხილვები: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">დამატებები</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">დამატებების მმართველი</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">დამატებები დროებით ამორთულია</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">ერთმა ან რამდენიმე დამატებამ შეწყვიტა მუშაობა, რაც სისტემას არამდგრადობას იწვევს.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">დამატებების კვლავგაშვება</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">სხვა დამატებების მონახვა</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">დაშვება</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">უარყოფა</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ითხოვს განახლებას</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d საჭიროებს ახალ ნებართვებს</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">საჭიროებს ახალ ნებართვას</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">დამატების განახლებები</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">მხარდაჭერილი დამატებების შემომწმებელი</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">ახალი დამატებაა ხელმისაწვდომი</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">ახალი დამატებებია ხელმისაწვდომი</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">დაამატეთ %1$s %2$s-ს</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">დაამატეთ %1$s და %2$s %3$s-ს</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">დაამატეთ %1$s-ს</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox დამატებების ტექნოლოგიები ახლდება. ეს დამატებები იყენებს სამუშაო გარსებს, რომლებიც არა თავსებადი Firefox 75 და მომდევნო ვერსიებთან.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ჩვენ ჯერჯერობით ვმუშაობთ, პირველი სასურველი გაფართოებების შესარჩევად.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">დამატება ჩამოიტვირთება და დამოწმდება…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">დამატებების მოთხოვნა ვერ მოხერხდა!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">თარგმანი არ მოიპოვება არც %1$s და არც ნაგულისხმევი %2$s ენისთვის</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">წარმატებით ჩაიდგა %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">ვერ ჩაიდგა %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">ვერ ჩაიდგა დამატება.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">დამატება ვერ ჩამოიტვირთა კავშირის ხარვეზის გამო.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">ეს დამატება ვერ ჩაიდგმება, ვინაიდან ჩანს, დაზიანებულია.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">ამ დამატების დაყენება ვერ მოხერხდა, რადგან დაუმოწმებელია.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s ვერ ჩაიდგმება, რადგან შეუთავსებელია %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s ვერ ჩაიდგმება, რადგან დიდი ალბათობით საფრთხეს შეუქმნის მდგრადობასა და უსაფრთხოებას.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">წარმატებით ჩაირთო %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">ვერ ჩაირთო %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">წარმატებით გამოირთო %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">ვერ გამოირთო %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">წარმატებით ამოიშალა %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">ვერ ამოიშალა %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">წარმატებით მოცილდა %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">ვერ მოცილდა %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ეს დამატება გადმოვიდა %1$s წინა ვერსიიდან</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 დამატება</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s დამატება</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">იხილეთ ვრცლად</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">წარმატებით განახლდა</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">განახლება მიუწვდომელია</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">შეცდომა</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">გამნახლებლის მონაცემები</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ბოლო მცდელობა:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">მდგომარეობა:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s დაემატა %2$s-ს</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">გახსენით მენიუდან</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">კარგი, გასაგებია</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">ვრცლად</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ამორთულია საფრთხის შემცველობის ან არამდგრადობის გამო.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">ვერ დადასტურდა, რომ %1$s უსაფრთხოა და ამიტომ ამორთულია.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s და თქვენი ვერსიის %2$s შეუთავსებლებია (ვერსია %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..00cac5c3ab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Qupıyalıq sazlawların oqıw hám ózgertiw</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Barlıq veb-saytlar ushın maǵlıwmatlarıńızǵa kiriw</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s ushın maǵlıwmatlarıńızǵa kiriw</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s domenindegi saytlar ushın maǵlıwmatlarıńızǵa kiriw</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Jáne 1 saytta maǵlıwmatlarıńızǵa kiriw</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Jáne %1$d sayttaǵı maǵlıwmatlarıńızǵa kiriw</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Jáne 1 domendegi maǵlıwmatlarıńızǵa kiriw</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Jáne %1$d domenlerdegi maǵlıwmatlarıńızǵa kiriw</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Brauzer betlerine kiriw</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Sheksiz kólemdegi maǵlıwmatlardı paydalanıwshı tárepte saqlaw</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Navigaciya waqtında brauzer iskerligine kiriw</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Saylandılardı oqıw hám ózgertiw</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Brauzer sazlawların oqıw hám ózgertiw</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Jaqın aradaǵı brauzer tariyxın, cookie hám oǵan tiyisli maǵlıwmatlardı tazalaw</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Almasıw buferinen maǵlıwmatlardı alıw</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Almasıw buferine maǵlıwmatlardı kirgiziw</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Qálegen bettegi kontentti bloklaw</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Brauzer tariyxın kórsetiw</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Fayllardı júklew hám brauzer tariyxında júklengenlerdi oqıw hám ózgertiw</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Qurılmańızǵa júklengen fayllardı ashıw</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Barlıq ashıq bettegi tekstlerdi oqıw</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Jaylasqan ornıńızǵa kiriw</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Brauzer tariyxına kiriw</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Qosımshadan paydalanıwdı baqlap barıw hám temalardı basqarıw</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Bunnan basqa baǵdarlamalar menen xabar almasıw</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Sizge xabarlamalardı kórsetiw</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kriptografiyalıq audentifikaciya xızmetleri menen támiyinlew</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Brauzer proksi sazlawların basqarıw</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Jaqında jabılǵan betlerge kiriw</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Brauzer betlerin jasırıw hám kórsetiw</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Brauzer tariyxına kiriw</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ashıq betlerde maǵlıwmatlarıńızǵa kiriw ushın baǵdarlamashı ásbapların keńeytiw</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versiya</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Avtorlar</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Sońǵı jańalanǵan</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Bas bet</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Ruqsatnamalar haqqında tolıǵıraq úyreniw</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Baha</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Sazlawlar</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Qosıwlı</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Óshirilgen</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Jeke bette ruqsat etiw</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Jeke betlerde iske túsiriw</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Iske túsirilgen</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Óshirilgen</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Ornatıldı</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Usınıs etilgen</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Házirshe qollap-quwatlanbaydı</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Házirshe kiriw múmkinshiligi joq</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Óshirilgen</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Tolıq maǵlıwmat</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Ruqsatnamalar</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Alıp taslaw</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s qosılsın ba?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Bul tómendegi ruqsatlardı talap etedi:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Qosıw</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Biykarlaw</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Qosımshalardı ornatıw</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Biykarlaw</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Pikirler: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Qosımshalar</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Qosımshalar menejeri</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Ruqsat beriw</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Biykarlaw</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$sjańa xabarlarǵa iye</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$djańa ruqsatnamalar talap etiledi</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Jańa ruqsatnama talap etiledi</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Qosımsha jańalanıwlar</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Qollap-quwatlanatuǵın qosımshalar tekseriwi</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Jańa qosımsha bar</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Jańa qosımshalar bar</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$snı%2$sǵa qosıw</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s hám %2$slardı %3$sǵa qosıw</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Olardı %1$sǵa qosıw</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox qosımsha texnologiyası qayta ózgermekte. Bul qosmshalar Firefox 75 hám onnan keyingi versiyalarǵa sáykes kelmeytuǵın quram-bóleklerden paydalanadı.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Bizler házir usınıs etilgen qosımshalardıń dáslepki tańlawın qollap-quwatlamaqtamız.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Qosımsha tastıyıqlanıp hám júklenip atır…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Qosımshalar dizimin júklew ámelge aspadı!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Awdarma tabılmadı, %1$s tili ushın da, sistemanıń tiykarǵı %2$s tili ushın da</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s tabıslı ornatıldı</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ornatıw ámelge aspadı</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s tabıslı qosıldı</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s qosıw ámelge aspadı</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s tabıslı óshirildi</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s óshiriw ámelge aspadı</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s tabıslı óshirildi</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s óshiriw ámelge aspadı</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s tabıslı alıp taslandı</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s alıp taslaw ámelge aspadı</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Bul qosımsha aldıńǵı %1$s versiyasınan kóshirilgen</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 qosımsha</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s qosımshalar</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Tolıǵıraq úyreniw</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Áwmetli jańalandı</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Jańalanıw joq</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Qáte</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Jańalanıw haqqında maǵlıwmat</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Sońǵı urınıw:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s %2$s ǵa qosıldı</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Bunı menyude ashıń</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Maqul, túsindim</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..d154ecb99c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-kab/strings.xml
@@ -0,0 +1,267 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Ɣer daɣen beddel iɣewwaṛen n tbaḍnit</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Kcem ɣer isefka-inek deg ismal web meṛṛa</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Kcem ɣer ysefka-inek·inem i %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Kcem ar isefka-inek·inem i yismal deg taɣult %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Kcem ɣer yisefka-inek·nem ɣef yiwen n usmel-nniḍen</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Kcem ɣer yisefka-inek·nem ɣef %1$d n usmel-nniḍen</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Kcem ɣer yisefka-inek·nem ɣef 1 n taɣult-nniḍen</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Kcem ɣer yisefka-inek·nem ɣef %1$d n tɣula-nniḍen</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d n %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Kcem γer icarren n iminig</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Sekles isefka war tilas si tama n umsaɣ</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Kcem ɣer urmud n yiminig mi ara tettinigeḍ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Ɣer sakin beddel ticraḍ n yisebtar</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Γer u snifel iγewwaṛen n iminig</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Sfeḍ amazray aneggaru n tunigin, inagan n tuqqna, akked yisefka icudden ɣur-s</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Awi-d isefka seg tkatut Ɣef afus</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Sekcem isefka ɣer tkatut Ɣef afus</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Sewḥel agbur ɣef yal asebter</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Ɣer amazray n tunigin</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Sader-d ifuyla, ɣeṛ daɣen beddel amazray n usader deg iminig</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Ldi ifuya i d-yudren deg yibenk-ik</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Γer aḍris n waccaren akk yeldin</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Kcem ɣer wadig-ik·im</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Kcem ɣer uzray n yiminig</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Qareɛ aseqdec n usiɣzef akked usefrek n yisental</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Ambaddal n yiznan s yisnasen-nniden yemgaraden d wa</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Ad k-d-isken ilɣa</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Ittak-d imeẓla n usesteb awgelhan</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Senqed iɣewwaṛen n upṛuksi n yiminig</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Kcem ɣer wacarren ittwamedlen melmi kan</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Sken-d neγ ffer iccaren n iminig</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Kcem ɣer uzray n yiminig</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Siγzef anekcum i yifecka n uneflay ɣer isefka-inek/inem deg waccaren yeldin</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Lqem</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Ameskar</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Imeskaren</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Aleqqem aneggaru</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Asebter agejdan</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Ɣer ugar ɣef tsurag</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Tizmilin</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Ugar γef uzegrir-agi</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Iɣewwaṛen</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Yermed</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Yensa</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Sireg di tunigin tusligt</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Selkem di tunigin tusligt</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Irmed</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Yensa</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Ibded</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Yelha</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ur ttwasefrak ara</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ulacèit yakan</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Yensa</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Talqayt</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Tisirag</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Kkes</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Aneqqis</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Rnu %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s yesra tisirag niḍen.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Tesra tasiregt-inek akken:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Ibɣa:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Rnu</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Sireg</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Gdel</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Sefsex</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Asebded n uzegrir</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Sebded %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Sefsex</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Cegger: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/55</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Izegrar</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Amsefrak n yizegrar</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Azegrir yensa i kra n wakud</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Ales asenker n yizegrar</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Aff-d ugar n izegrar</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Af-d ugar n isiɣzaf</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Sireg</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Gdel</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s isεa lqem amaynut</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d ttwasrant tisirag timaynutin</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Tasiregt tamaynut tlaq</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Ileqman n uzegrir</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Amsenqed n izegrar imṣaḍan</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Azegrir amaynut yella</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Izegrar imaynuten llan</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Rnu %1$s ɣer %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Rnu %1$s akked %2$s ɣer %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Rnu-ten ɣer %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Tatiknulujit n izegrar Firefox tettnerni. Izegrar-agi seqdacen frameworks ur nettwasefrak ra akked Firefox 75 &amp; ugar.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Aql-aɣ nbennu Tallelt i taggayt tamezwarut akked isiɣzaf ihullen.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Azdam akked usenqed n uzegrir…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Tuttra n izegrar ur teddi ara!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Ur tettwaf ara tsuqilt, ama i tdigant %1$s ama i tutlayt tamezwert %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Ibded akken iwata %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Ur ibded ara %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Yecceḍ usebded n uzegrir-a.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Uzegrir-a ur yezmir ara ad d-yettusader acku yella wugur deg tuqqna</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Azegrir-a ur yezmir ara ad yebded acku yettban yexṣer.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Azegrir-agi ur yebdid ara acku ur yettwasenqed ara.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s ur yezmir ara ad yebded acku ur imṣaḍa ara d %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s ur yezmir ara ad yebded acku yella wugur meqqren n tɣellist akked urkad.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Irmed akken iwata %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Ur irmid ara %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Insa akken iwata %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Ur insi ara %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Ittwakkes akken iwata %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Ur ittwakkes ara %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Ittwakkes akken iwata %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Ur ittwakkes ara %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Azegrir-a ikka-d seg ulqem yezwaren %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 azegrir</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 usiɣzef</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s n isiγzaf</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s yisiɣzaf</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Issin ugar</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Ilqem akken iwata</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ulac alqem</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Tuccḍa</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Talɣut n uleqqam</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Aɛraḍ aneggaru:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Addaden:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s yettwarna ɣer %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Ldi-t deg wumuɣ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ih, awi-t-id</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">IH</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Issin ugar</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s yensa ssebba n wuguren n tɣellist neɣ n uqeɛɛed.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Azegrir %1$s yegguma ad yettusenqed d aɣellsan, dayen yensa.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Azegrir %1$s ur yemṣada ara akked lqem-ik·im n %2$s (lqem %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..b94576e71c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-kk/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Жекелік баптауларды оқу және өзгерту</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Барлық вебсайттар үшін деректеріңізге қатынау</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s үшін деректеріңізге қатынау</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s доменіндегі сайттар үшін деректеріңізге қатынау</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Басқа 1 сайт үшін деректеріңізге қатынау</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Басқа %1$d сайт үшін деректеріңізге қатынау</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Басқа 1 домен үшін деректеріңізге қатынау</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Басқа %1$d домен үшін деректеріңізге қатынау</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d - %3$d ішінен</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Браузер беттеріне қатынау</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Клиент жақтағы шектеусіз деректерді сақтау</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Навигация кезіндегі браузер белсенділігіне қатынау</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Бетбелгілерді оқу және түзету</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Браузер баптауларын оқу және өзгерту</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Жуырдағы шолу тарихын, cookies және сәйкес деректерін өшіру</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Алмасу буферінен деректерді алу</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Деректерді алмасу буферіне енгізу</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Кез келген беттегі мазмұнды блоктау</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Шолу тарихыңызды оқу</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Файлдарды жүктеп алу және браузердің жүктеп алулар тарихын түзету</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Сіздің құрылғыңызға жүктеліп алынған файлдарды ашу</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Барлық ашық беттердің мәтінін оқу</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Орналасуыңызға қатынау</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Шолу тарихына қатынау</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Кеңейтулер қолданылуын бақылау және темаларды басқару</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Бұл қолданбасынан басқа қолданбалармен хабарламалармен алмасу</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Сізге хабарламаларды көрсету</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Криптографиялық аутентификация қызметтерін ұсыну</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Браузердің прокси баптауларын басқару</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Жуырда жабылған беттерге қатынау</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Браузер беттерін жасыру және көрсету</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Шолу тарихына қатынау</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ашық беттердегі деректеріңізге қатынау үшін әзірлеуші құралдарын кеңейту</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Нұсқасы</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Автор</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Авторлар</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Соңғы жаңартылған</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Үй парағы</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Рұқсаттар туралы көбірек білу</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Рейтингі</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Бұл қосымша туралы толығырақ</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Бұл кеңейту туралы толығырақ</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Баптаулар</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Іске қосулы</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Сөндірулі</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Жекелік шолу режимінде рұқсат ету</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Жекелік шолу режимінде жөнелту</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Жекелік шолу терезелерінде рұқсат етілмеген</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Іске қосылған</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Сөндірілген</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Орнатылған</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Ұсынылатын</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Әзірге қолдау жоқ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Әлі қолжетімсіз</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Сөндірілген</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Ақпараты</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Рұқсаттар</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Өшіру</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Хабарлау</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s қосу керек пе?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s қосымша рұқсаттарды сұрайды.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ол сіздің рұқсатыңызды келесі үшін талап етеді:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Оның талаптары:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Қосу</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Рұқсат ету</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Тыйым салу</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Бас тарту</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Қосымшаны орнату</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s орнату</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Бас тарту</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Пікірлер: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Рейтинг: %1$.02f, 5 ішінен</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Қосымшалар</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Қосымшалар басқарушысы</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшалар уақытша сөндірілген</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Кеңейтулер уақытша сөндірілген</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Бір немесе бірнеше қосымша өз жұмысын тоқтатып, жүйені тұрақсыз етті.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Бір немесе бірнеше кеңейту өз жұмысын тоқтатып, жүйені тұрақсыз етті.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшаларды қайта іске қосу</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Кеңейтулерді қайта іске қосу</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Көбірек қосымшаларды табу</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Көбірек кеңейтулерді табу</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Рұқсат ету</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Тыйым салу</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s жаңа жаңартуы бар</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d жаңа рұқсат қажет</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Жаңа рұқсат қажет</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшалар жаңартулары</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Кеңейту жаңартулары</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Қолдауы бар қосымшаларды тексеру құралы</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Жаңа қосымша қолжетімді</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Жаңа қосымшалар қолжетімді</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s ішіне %1$s қосу</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s ішіне %1$s және %2$s қосу</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Оларды %1$s ішіне қосу</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox қосымшалар технологиясы жаңартылуда. Бұл қосымшалар Firefox 75 және жаңалау нұсқасымен үйлесімсіз фреймворктерді қолданады.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Біз қазіргі уақытта Ұсынылатын Кеңейтулердің бастапқы топтамасына қолдауды жасап жатырмыз.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшаны жүктеп алу және тексеру…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Кеңейтуді жүктеп алу және тексеру…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшаларды сұрау сәтсіз аяқталды!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Кеңейтулерді сұрау сәтсіз аяқталды!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Аударма табылмады, %1$s локалі және %2$s бастапқы тілі үшін.</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s сәтті орнатылды</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s орнату сәтсіз аяқталды</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Бұл қосымшаны орнату сәтсіз аяқталды.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Бұл кеңейтуді орнату сәтсіз аяқталды.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Байланысты орнату сәтсіз аяқталған соң, бұл қосымшаны жүктеп алу мүмкін емес.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Байланысты орнату сәтсіз аяқталған соң, бұл кеңейтуді жүктеп алу мүмкін емес.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Бұл қосымша зақымдалған сияқты, сондықтан оны орнату мүмкін емес.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Бұл кеңейту зақымдалған сияқты, сол үшін оны орнату мүмкін емес.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Бұл қосымша расталмаған, сондықтан оны орнату мүмкін емес.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Бұл кеңейту расталмаған, сол үшін оны орнату мүмкін емес.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s орнату мүмкін емес, өйткені ол %2$s %3$s нұсқасымен үйлеспейді.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s орнату мүмкін емес, өйткені ол тұрақтылық немесе қауіпсіздік мәселелерін туғызудың жоғары тәуекелі бар.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s іске сәтті қосылды</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s іске қосу сәтсіз аяқталды</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s сәтті сөндірілді</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s сөндіру сәтсіз аяқталды</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s сәтті өшірілді</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s өшіру сәтсіз аяқталды</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s сәтті өшірілді</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s өшіру сәтсіз аяқталды</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Бұл қосымша %1$s алдыңғы нұсқасынан көшірілді</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 қосымша</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 кеңейту</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s қосымша</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s кеңейту</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Көбірек білу</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Сәтті жаңартылды</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Жаңартулар жоқ</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Қате</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Жаңартушы ақпараты</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Соңғы талабы:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Қалып-күйі:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s %2$s ішіне қосылды</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Мәзірде ашу</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">%2$s мәзірінен %1$s қосымшасына қол жеткізу.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Жақсы, түсіндім</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">ОК</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Көбірек білу</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s қауіпсіздік немесе тұрақтылық мәселелер салдарынан сөндірілген.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s қауіпсіз екенін растау мүмкін емес, сондықтан ол сөндірілді.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s сіздің %2$s нұсқасымен үйлесімді емес (%3$s нұсқасы).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..2d00399111
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Sazkariyên nihêniyê bixwîne û biguherîne</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Ji bo hemû malperan xwe bigihîne daneyên te</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Ji bo %1$s’ê xwe bigihîne daneyên xwe</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Xwe bigihîne daneyên malperên te yên aîdî domaîna %1$s’ê</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Li ser malpereke din xwe bigihîne daneyên xwe</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Li ser %1$d malperên din xwe bigihîne daneyên xwe</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Li ser domaîneke din xwe bigihîne daneyên xwe</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Li ser %1$d domaînên din xwe bigihîne daneyên xwe</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d ji %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Xwe bigihîne hilpekînên gerokê</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ji daxwazkarê daneyên bêsînor tomar bike</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Di dema gerînê de xwe bigihîne çalakiya gerokê</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Xwendin û guhertina favoriyan</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Xwendin û guhertina sazkariyên gerokê</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Paqijkirina raboriya gerînê, kûkî û daneyên têkildar</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Daneyan ji panoyê bistîne</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Daneyan bişîne panoyê</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Naveroka li ser hemû rûpelan asteng bike</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Raboriya geroka xwe bixwîne</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Dosyeyan daxîne, raboriya gerokê bixwîne û biguherîne</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Vekirina dosyeyên li cîhaza te barkirî</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Xwendina nivîsên hemû hilpekînên vekirî</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Xwegihandina cîgeha te</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Xwegihandina raboriya gerokê</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Temaşekirina bikaranîna pêvekan û birêvebirina temayan</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Şandina/stendina peyaman bi sepanên din ên Ji bilî vê</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Danezanan ji te re nîşan bide</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Dabînkirina xizmetên rastandinê ji bo nasnameya krîptografîkî</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontrolkirina eyarên proxya gerokê</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Xwegihandina hilpekînên herî dawî</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Veşartin û nîşandana hilpekînên gerokê</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Xwegihandina raboriya gerînê</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Firehkirina amûrên pêşvebirinê ji bo xwegihandina daneyên di hilpekînên vekirî de</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Guherto</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Niväskar</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Nivîskar</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Nûvekirina dawî</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Serrûpel</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Derbarê destûran de zêdêtir fêr bibe</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Puan</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Agahiyên zêdetir li ser vê pêvekê</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Sazkarî</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Vekirî</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Girtî</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Di gerîna veşartî de destûrê bide</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Di gerîna veşartî de bixebitîne</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Di pencereyên nepen de destûr nayê dayîn</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Çalak</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Neçalak</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Sazkirî</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Yên pêşniyar</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Hîn nayê piştgirîkirin</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Hîn ne mewcûd e</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Neçalak</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Hûrgilî</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Destûr</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Rake</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Gilî bike</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s’ê tevlî bike?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s destûrên zûdetir dixwaze.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ji bo tevlîkirinê, destûrên li jêrê tên xwestin:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Ew dixwaze:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Tevlî bike</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Destûrê bide</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Red bike</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Betal bike</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Pêvekê saz bike</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2"> %1$s saz bike</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Betal bike</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Nirxandin: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Nirxandin: jin 5an %1$.02f</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Pêvek</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Rêvebera pêvekan</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">Pêvek bi awayekî demkî neçalak in</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">Yek an çend pêvek rawestiyan, ev pergala we bêîstiqrar dike.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Pêvekan ji nû ve bide destpêkirin</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Zêdetir pêvekan bibîne</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Destûrê bide</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Red bike</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Nûvekirineke nû ya %1$s’ê heye</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d destûrên nû hewce ne</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Destûreke nû hewce ye</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Nûvekirinên pêvekê</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Kontrolkera pêvekan a tê piştgirîkirin</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Pêveka nû mewcûd e</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Pêvekên nû mewcûd in</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Pêveka %1$s’ê tevlî %2$s’ê bike</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Pêvekên %1$s û %2$s’ê tevlî %3$s’ê bike</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Pêvekan tevlî %1$s’ê bike</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Teknolojiya pêvekên Firefoxê tê modernîzekirin. Ev pêvek kodên bi Firefox 75’ê û wêdetir re nelihev bi kar tîne.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Em niha ji bo Pêvekên Pêşniyarkirî destekê ava dikin.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Pêvek tê daxistin û rastandin…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Rapirsîna pêvekan bi ser neket!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Werger ne ji bo %1$s ne jî ji bo zimanê %2$s hat dîtin</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s bi serkeftî hate sazkirin</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s nehate sazkirin</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Sazkirina vê pêvekê bi ser neket.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">Ev pêvek ji ber têkçûneke pêwendiyê nehat daxistin.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Ev pêvek nehat saz kirin ji ber ku xuya dike ku xirab e.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Ev pêvek nehate sazkirin ji ber ku nehatiye piştrastkirin.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s nehat sazkirin ji ber ku bi %2$s %3$s re ne lihevhatî ye.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s nehate sazkirin ji ber ku rîska wê ya mezin heye ku bibe sedema pirsgirêkên aramî an ewlehiyê.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s bi serkeftî hate çalakkirin</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s nehate çalakkirin</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s bi serkeftî hate neçalakkirin</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s nehate neçalakkirin</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s bi serkeftî hate rakirin</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s nehate rakirin</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s bi serkeftî hate rakirin</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s nehate rakirin</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ev pêvek ji guhertoyeke kevn a %1$s’ê hatiye anîn</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 pêvek</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s pêvek</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Zêdetir bizane</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Bi serkeftî hate nûvekirin</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ti nûvekirin tune</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Çewtî</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Agahiyên Nûvekerê</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ceribandina dawî:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Rewş:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s li %2$s’ê hate tevlîkirin</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Di menûyê de veke</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Ji menuya %2$s xwe bigihînin %1$s</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Baş e</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Baş e</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Zêdetir bizane</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ji ber pirsgirêkên ewlekarî an îstiqrarê hate neçalakkirin.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s wekî ewledar nehat piştrastkirin û hate neçalakkirin.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s bi guhertoya we ya %2$s re (guhertoya %3$s) hevaheng nîne.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..ba3b4015af
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-kn/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">ಖಾಸಗಿತನದ ಸಿದ್ದತೆಗಳನ್ನು ಓದಿ ಮತ್ತು ಬದಲಿಸಿ</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ಎಲ್ಲಾ ಜಾಲತಾಣಗಳ ನಿಮ್ಮ ದತ್ತಾಂಶವನ್ನು ಪಡೆಯಿರಿ</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ವೀಕ್ಷಕದ ಹಾಳೆಗಳನ್ನು ಪ್ರವೇಶಿಸಿ</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ಬಳಸುವವರ ಕಡೆ ದತ್ತಾಂಶವನ್ನು ಅಪರಿಮಿತ ಪ್ರಮಾಣದಲ್ಲಿ ಸಂಗ್ರಹಿಸಿ</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">ಸಂಚಾರಿಸುವ ಸಮಯದಲ್ಲಿ ವೀಕ್ಷಕ ಚಟುವಟಿಕೆಯನ್ನು ಪ್ರವೇಶಿಸಿ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">ಬುಕ್‍ಮಾರ್ಕ್‌ಗಳನ್ನು ಓದಿ ಮತ್ತು ಬದಲಿಸಿ</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ವೀಕ್ಷಕ ಸಿದ್ದತೆಗಳನ್ನು ಓದಿ ಮತ್ತು ಬದಲಿಸಿ</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">ಇತ್ತೀಚಿನ ವೀಕ್ಷಕ ಇತಿಹಾಸ, ಕುಕ್ಕಿಗಳು ಮತ್ತು ಸಂಭಂದಿತ ದತ್ತಾಂಶವನ್ನು ಅಳಿಸಿರಿ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ಕ್ಲಿಪ್‍ಬೋರ್ಡ್‌ನಿಂದ ದತ್ತಾಂಶವನ್ನು ಪಡೆ</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ಕ್ಲಿಪ್‍ಬೋರ್ಡ್‌ನಿಂದ ದತ್ತಾಂಶವನ್ನು ಹಾಕು</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ಕಡತಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಿ ಮತ್ತು ವೀಕ್ಷಕದ ಡೌನ್ಲೋಡ್ ಇತಿಹಾಸವನ್ನು ಓದಿ ಮತ್ತು ಮಾರ್ಪಡಿಸಿ</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ನಿಮ್ಮ ಸಾಧನಕ್ಕೆ ಡೌನ್‌ಲೋಡ್ ಮಾಡಿದ ಫೈಲ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">ತೆರೆದ ಎಲ್ಲಾ ಹಾಳೆಗಳಲ್ಲಿನ ಪಠ್ಯವನ್ನು ಓದು</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ನಿಮ್ಮ ಸ್ಥಳವನ್ನು ನಿಲುಕಿಸಿಕೊಳ್ಳಿ</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ವೀಕ್ಷಣೆಯ ಇತಿಹಾಸವನ್ನು ಅಳಿಸಿ</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">ವಿಸ್ತರಣೆಯ ಬಳಕೆಯನ್ನು ಮೇಲ್ವಿಚಾರಣೆ ಮಾಡಿ ಮತ್ತು ಥೀಮ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ಇದನ್ನು ಹೊರತುಪಡಿಸಿ ಇತರೆ ತಂತ್ರಾಂಶಗಳೊಂದಿಗೆ ಸಂದೇಶಗಳನ್ನು ವಿನಿಮಯ ಮಾಡಿ</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">ನಿಮಗೆ ಅಧಿಸೂಚನೆಗಳನ್ನು ತೋರಿಸಿ</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ಕ್ರಿಪ್ಟೋಗ್ರಾಫಿಕ್ ದೃಢೀಕರಣ ಸೇವೆಗಳನ್ನು ಒದಗಿಸಿ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ವೀಕ್ಷಕ ಪ್ರಾಕ್ಸಿ ಸಿದ್ದತೆಗಳನ್ನು ನಿಯಂತ್ರಿಸಿ</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">ಇತ್ತೀಚೆಗೆ ಮುಚ್ಚಲಾದ ಟ್ಯಾಬ್‌ಗಳು</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ಬ್ರೌಸರ್ ಟ್ಯಾಬ್‌ಗಳನ್ನು ಮರೆಮಾಡಿ ಮತ್ತು ತೋರಿಸಿ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ವೀಕ್ಷಣೆಯ ಇತಿಹಾಸವನ್ನು ಅಳಿಸಿ</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ತೆರೆದ ಹಾಳೆಗಳಲ್ಲಿ ನಿಮ್ಮ ದತ್ತಾಂಶಕ್ಕಾಗಿ ಪ್ರವೇಶಾವಕಾಶ ನೀಡಲು ಡೆವಲಪರ್ ಪರಿಕರಗಳನ್ನು ವಿಸ್ತರಿಸಿ</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ಆವೃತ್ತಿ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">ಲೇಖಕರು</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">ಕೊನೆಯ ಬಾರಿಗೆ ಅಪ್‌ಡೇಟ್ ಮಾಡಿದ್ದು</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">ನೆಲೆಪುಟ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ಅನುಮತಿಗಳ ಬಗ್ಗೆ ಇನ್ನಷ್ಟು ತಿಳಿಯಿರಿ</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">ಜನಪ್ರಿಯತೆಯ ಅಂದಾಜು</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ಸಿದ್ಧತೆಗಳು</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ಆನ್</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">ಆಫ್</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">ಖಾಸಗಿ ಬ್ರೌಸಿಂಗ್‌ನಲ್ಲಿ ಅನುಮತಿಸಿ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">ಖಾಸಗಿ ಬ್ರೌಸಿಂಗ್‌ನಲ್ಲಿ ರನ್ ಮಾಡಿ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ಸಕ್ರಿಯಗೊಳಿಸು</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">ಅಶಕ್ತಗೊಂಡ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ಅನುಸ್ಥಾಪಿತಗೊಂಡಿದೆ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">ಇದನ್ನು ಸಲಹೆ ಮಾಡಲಾಗುತ್ತದೆ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ಇನ್ನೂ ಬೆಂಬಲಿವಿಲ್ಲ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ಇನ್ನು ಲಭ್ಯವಿಲ್ಲ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ಅಶಕ್ತಗೊಂಡ</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">ವಿವರಗಳು</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ಅನುಮತಿಗಳು</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">ತೆಗೆದು ಹಾಕು</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s ಅನ್ನು ಸೇರಿಸು‍?‍</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">ಇದನ್ನು ಮಾಡುವುದಕ್ಕೆ ನಿಮ್ಮ ಅನುಮತಿಯನ್ನು ಕೋರುತ್ತದೆ:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">ಸೇರಿಸು</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ರದ್ದು ಮಾಡು</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">ಆಡ್-ಆನ್ ಅನ್ನು ಅನುಸ್ಥಾಪಿಸು</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ರದ್ದು ಮಾಡು</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ಆಡ್-ಆನ್‌ಗಳು</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ಅನುಮತಿಸು</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ನಿರಾಕರಿಸು</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ಹೊಸ ನವೀಕರಣವನ್ನು ಹೊಂದಿದೆ</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d ಹೊಸ ಅನುಮತಿಗಳ ಅಗತ್ಯವಿದೆ</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ಹೊಸ ಅನುಮತಿ ಅಗತ್ಯವಿದೆ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">ಆಡ್-ಆನ್ ನವೀಕರಣಗಳು</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">ಬೆಂಬಲಿತ ಆಡ್-ಆನ್ ಗಳನ್ನು ಪರೀಷ್ಕಿರಿಸು</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">ಹೊಸ ಆಡ್-ಆನ್‌ ಲಭ್ಯವಿದೆ</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">ಹೊಸ ಆಡ್-ಆನ್‌ಗಳು ಲಭ್ಯವಿದೆ</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s ಅನ್ನು %2$sಗೆ ಸೇರಿಸಿ</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s ಮತ್ತು %2$s ಅನ್ನು %3$s ಗೆ ಸೇರಿಸಿ</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">ಅವುಗಳನ್ನು %1$sಗೆ ಸೇರಿಸಿ</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">ಫೈರ್‌ಫಾಕ್ಸ್ ಆಡ್-ಆನ್ ತಂತ್ರಜ್ಞಾನವು ಆಧುನೀಕರಣಗೊಳ್ಳುತ್ತಿದೆ. ಈ ಆಡ್-ಆನ್‌ಗಳು ಫೈರ್‌ಫಾಕ್ಸ್ 75 ಮತ್ತು ಅದಕ್ಕಿಂತ ಹೆಚ್ಚಿನದಕ್ಕೆ ಹೊಂದಿಕೆಯಾಗದ ಚೌಕಟ್ಟುಗಳನ್ನು ಬಳಸುತ್ತವೆ.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ಶಿಫಾರಸು ಮಾಡಲಾದ ವಿಸ್ತರಣೆಗಳ ಆರಂಭಿಕ ಆಯ್ಕೆಗಾಗಿ ನಾವು ಪ್ರಸ್ತುತ ಬೆಂಬಲವನ್ನು ನಿರ್ಮಿಸುತ್ತಿದ್ದೇವೆ.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">ಆಡ್-ಆನ್ ಅನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ ಮತ್ತು ಪರಿಶೀಲಿಸಲಾಗುತ್ತಿದೆ…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">ಆಡ್-ಆನ್‌ಗಳನ್ನು ಹುಡುಕಲುಲು ವಿಫಲವಾಗಿದೆ!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s ಅನ್ನು ಯಶಸ್ವಿಯಾಗಿ ಸ್ಥಾಪಿಸಲಾಗಿದೆ</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ಅನ್ನು ಸ್ಥಾಪಿಸಲು ವಿಫಲವಾಗಿದೆ</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ಅನ್ನು ಯಶಸ್ವಿಯಾಗಿ ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s ಅನ್ನು ಯಶಸ್ವಿಯಾಗಿ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s ಅನ್ನು ಯಶಸ್ವಿಯಾಗಿ ಸ್ಥಾಪಿಸಲಾಗಿದೆ</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s ಅನ್ನು ಸ್ಥಾಪಿಸಲು ವಿಫಲವಾಗಿದೆ</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s ಅನ್ನು ಯಶಸ್ವಿಯಾಗಿ ತೆಗೆದುಹಾಕಲಾಗಿದೆ</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ತೆಗೆದುಹಾಕಲು ವಿಫಲವಾಗಿದೆ</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ಈ ಆಡ್-ಆನ್ ಅನ್ನು ಹಿಂದಿನ %1$s ಆವೃತ್ತಿಯಿಂದ ಸ್ಥಳಾಂತರಿಸಲಾಗಿದೆ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 ಆಡ್-ಆನ್</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s ಆಡ್-ಆನ್‌ಗಳು</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ಇನ್ನಷ್ಟು ಅರಿತುಕೊಳ್ಳಿ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">ಯಶಸ್ವಿಯಾಗಿ ನವೀಕರಿಸಲಾಗಿದೆ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">ಯಾವುದೇ ಅಪ್‌ಡೇಟುಗಳು ಇಲ್ಲ</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ದೋಷ</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">ನವೀಕರಣ ಮಾಹಿತಿ</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ಕೊನೆಯ ಪ್ರಯತ್ನ:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">ಸ್ಥಿತಿ:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ಅನ್ನು %2$s ಗೆ ಸೇರಿಸಲಾಗಿದೆ</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">ಅದನ್ನು ಮೆನುವಿನಲ್ಲಿ ತೆರೆಯಿರಿ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ಸರಿ, ಗೊತ್ತಾಯಿತು</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..60f62641ed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ko/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">개인 정보 설정 읽기 및 수정</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">모든 웹 사이트에 대한 사용자 데이터에 접근</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s에 대한 사용자 데이터에 접근</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s 도메인 사이트에 대한 사용자 데이터에 접근</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">다른 사이트 1개에 대한 사용자 데이터에 접근</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">다른 사이트 %1$d개에 대한 사용자 데이터에 접근</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">다른 도메인 1개에 대한 사용자 데이터에 접근</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">다른 도메인 %1$d개에 대한 사용자 데이터에 접근</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d / %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">브라우저 탭에 접근</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">무제한의 클라이언트 데이터 저장</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">탐색중에 브라우저 활동에 접근</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">북마크 읽기 및 수정</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">브라우저 설정 읽기 및 수정</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">최근 방문 기록, 쿠키 및 관련 데이터 지우기</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">클립보드의 데이터 가져오기</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">클립보드에 데이터 넣기</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">모든 페이지에서 콘텐츠 차단</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">방문 기록 읽기</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">파일을 다운로드하고 브라우저의 다운로드 기록을 읽고 수정</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">기기에 다운로드한 파일 열기</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">모든 열린 탭의 텍스트 읽기</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">사용자 위치에 접근</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">방문 기록에 접근</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">확장 기능 사용 모니터링 및 테마 관리</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">이 앱 이외의 앱과 메시지를 주고 받음</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">알림을 표시</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">암호화 인증 서비스 제공</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">브라우저 프록시 설정 제어</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">최근에 닫힌 탭에 접근</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">브라우저 탭 숨기기 및 표시</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">방문 기록에 접근</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">열린 탭에서 데이터에 접근할 수 있도록 개발자 도구를 확장</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">버전</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">제작자</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">제작자</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">마지막 업데이트</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">홈페이지</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">권한에 대해 더 알아보기</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">평가</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">이 부가 기능의 상세 정보</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">이 확장 기능의 상세 정보</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">설정</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">켜짐</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">꺼짐</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">사생활 보호 모드에서 허용</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">사생활 보호 모드에서 실행</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">사생활 보호 창에서 허용 안 됨</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">사용함</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">사용 안 함</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">설치됨</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">추천</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">아직 지원되지 않음</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">아직 사용할 수 없음</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">사용 안 함</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">상세 정보</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">권한</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">제거</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">신고</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s 부가 기능을 추가하시겠습니까?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s 부가 기능이 추가 권한을 요청합니다.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">다음의 권한 필요:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">요청 권한:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">추가</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">허용</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">거부</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">취소</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">부가 기능 설치 </string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s 설치</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">취소</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">리뷰: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">평점: %1$.02f / 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">부가 기능</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">부가 기능 관리자</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능이 일시적으로 비활성화됨</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">확장 기능이 일시적으로 비활성화됨</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">하나 이상의 부가 기능이 작동을 중지하여 시스템이 불안정해졌습니다.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">하나 이상의 확장 기능이 작동을 중지하여 시스템이 불안정해졌습니다.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능 다시 시작</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">확장 기능 다시 시작</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">더 많은 부가 기능 찾기</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">확장 기능 더 찾기</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">허용</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">거부</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s 부가 기능이 새 업데이트가 있습니다</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d개의 새로운 권한이 필요합니다</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">새 권한이 필요합니다</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능 업데이트</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">확장 기능 업데이트</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">지원되는 부가 기능 검사기</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">새 부가 기능 있음</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">새 부가 기능 있음</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s에 %1$s 추가</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s에 %1$s 및 %2$s 추가</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">%1$s에 모두 추가</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox 부가 기능 기술은 현대화되고 있습니다. 이 부가 기능은 Firefox 버전 75 이상부터는 호환되지 않는 프레임워크를 사용하고있습니다.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">현재 초기에 선정된 추천 확장 기능들에 대한 지원을 구축하고 있습니다.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능 다운로드 및 검증 중…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">확장 기능 다운로드 및 검사 중…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능 쿼리에 실패했습니다!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">확장 기능 쿼리에 실패했습니다!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">%1$s 로케일 및 기본 언어 %2$s에 대한 번역을 찾을 수 없습니다.</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s 부가 기능 설치에 성공했습니다</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s 부가 기능 설치에 실패했습니다</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">이 부가 기능 설치에 실패했습니다.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">이 확장 기능 설치에 실패했습니다.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">연결 실패로 이 부가 기능을 다운로드할 수 없습니다.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">연결 실패로 이 확장 기능을 다운로드할 수 없습니다.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">이 부가 기능이 손상된 것 같으므로 설치할 수 없습니다.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">이 확장 기능은 손상된 것 같으므로 설치할 수 없습니다.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">이 부가 기능은 확인되지 않았기 때문에 설치할 수 없습니다.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">이 확장 기능은 확인되지 않았으므로 설치할 수 없습니다.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s 부가 기능은 %2$s %3$s 버전과 호환되지 않기 때문에 설치할 수 없습니다.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s 부가 기능은 안정성 또는 보안 문제를 일으킬 위험이 높기 때문에 설치할 수 없습니다.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s 부가 기능 활성화에 성공했습니다</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s 부가 기능 활성화에 실패했습니다</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s 부가 기능 비활성화에 성공했습니다</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s 부가 기능 비활성화에 실패했습니다</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s 부가 기능 설치 제거에 성공했습니다</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s 부가 기능 설치 제거에 실패했습니다</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s 부가 기능 제거에 성공했습니다</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s 부가 기능 제거에 실패했습니다</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">이 부가 기능은 %1$s의 이전 버전에서 마이그레이션되었습니다</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1개 부가 기능</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">확장 기능 1개</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s개 부가 기능</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">확장 기능 %1$s개</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">더 알아보기</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">성공적으로 업데이트됨</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">업데이트 없음</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">오류</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">업데이터 정보</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">마지막 시도:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">상태:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%2$s에 %1$s 부가 기능이 추가되었습니다</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">메뉴에서 열기</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">%2$s 메뉴에서 %1$s에 액세스하세요.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">확인</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">확인</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">더 알아보기</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s 부가 기능은 보안 또는 안정성의 문제로 사용할 수 없습니다.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s 부가 기능은 안전한 것으로 확인되지 않아 사용할 수 없습니다.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s 부가 기능은 %2$s %3$s 버전과 호환되지 않습니다.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..3502397c80
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-lij/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lezi e cangia e inpostaçioin da privacy</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acedi a-i teu dæti pe tutti i sciti</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acedi a-i feuggi do navegatô</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Sarvâ ina quantitæ ilimitâ de dæti lou client</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acedi a-e ativitæ do navegatô inta navegaçion</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lezi e cangia segnalibbri</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lezi e cangia e inpostaçioin do navegatô</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Scancella a stöia da navegaçion, cookie e dæti corelæ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Piggia dæti da-i aponti</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Manda dæti inti aponti</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Scarega file e lezi e cangia a stöia di descaregamenti</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Arvi i descaregamenti in sciô teu dispoxitivo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lezi o testo di tutti i feuggi averti</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Accesso a-a teu localizaçion</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acedi a-a stöia da navegaçion</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Verifica l\'uzo de estenscioin e gestisci i temi</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Scangia mesaggi con app che no segian sta chi</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Fanni vedde notifiche</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Fornî serviççio de aotenticaçion criptografica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Contròlla e inpostaçioin do proxy</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acedi a-i feuggi særæ urtimamente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ascondi e mostra i feuggi</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acedi a-a stöia da navegaçion</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Permetti a-i strumenti do svilupatô de acede a-i dæti feuggi</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Verscion</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Aotoî</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Urtima agiornamento</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pagina Prinçipâ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Ciù informaçioin in sci permissi</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Clasifica</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Inpostaçioin</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Atîva</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Asmòrtou</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permetti in Navegaçion Privâ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Xeua in Navegaçion Privâ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ativou</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Dizativou</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalou</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Consegiou</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">No ancon soportou</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">No gh\'é ancon</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Dizativou</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalli</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissi</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Scancella</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Azonzi %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Serve o teu permisso pe:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Azonzi</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Scancella</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Installa conponente azonto</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Anulla</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Conponenti azonti</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permetti</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">No permette</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s o l\'à un agiornamento</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d neuvi permissi domandæ</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Serve un neuvo permisso</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Agiornamenti conponenti azonti</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Verifica conponenti azonti soportæ</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Gh\'é un nuovo conponente azonto</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Gh\'é di nuovi conponenti azonti</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Azonzi %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Azonzi %1$s e %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Azonzili a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">A tecnologia di conponenti azonti de Firefox s\'agiorna de longo. Sti conponenti azonti deuvian di framework che no en conpatibili co-o Firefox 75 e-e verscioin dòppo.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Semmo apreuvo a svilupâ o sopòrto pe \'na seleçion iniçiâ de estenscioin consegiæ.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Descaregamento e verifica do conponente azonto…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">No ariescio a fâ a query di conponenti azonti!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalou ben</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Inposcibile instalâ %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ativou ben</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Inposcibile ativâ %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s dizativou</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Inposcibile dizativâ %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s dizinstalou ben</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Inposcibile dizinstalâ %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s scancelou</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Inposcibile scancelâ %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Sto conponente azonto o vegne da-a migraçion de \'na verscion ciù vegia de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 conponente azonto</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s conponenti azonti</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Atre informaçioin</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Agiornou ben</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No gh\'é di agiornamenti</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Erô</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informaçioin in sce l\'agiornamento</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Urtimo tentativo:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stato:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s o l\'é stæto azonto a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Arvi into menû</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Va ben, ò capio</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..8f89a31be0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-lo/strings.xml
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">ອ່ານ ແລະ ແກ້ໄຂການຕັ້ງຄ່າຄວາມເປັນສາວນຕົວ</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ເຂົ້າເຖິງຂໍ້ມູນຂອງທ່ານສຳລັບເວັບໄຊທທັງຫມົດ</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">ເຂົ້າເຖິງຂໍ້ມູນຂອງທ່ານສຳລັບ %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">ເຂົ້າເຖິງຂໍ້ມູນຂອງທ່ານສຳລັບເວັບໄຊທໃນໂດເມນ %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">ເຂົ້າເຖິງຂໍ້ມູນຂອງທ່ານໃນອີກ 1 ເວັບໄຊທ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">ເຂົ້າເຖິງຂໍ້ມູນຂອງທ່ານໃນອີກ %1$d ເວັບໄຊທ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">ເຂົ້າເຖິງຂໍ້ມູນຂອງທ່ານໃນອີກ 1 ໂດເມນ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">ເຂົ້າເຖິງຂໍ້ມູນຂອງທ່ານໃນອີກ %1$d ໂດເມນ</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ເຂົ້າເຖິງແທັບຂອງບຣາວເຊີ</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ເກັບຂໍ້ມູນຂອງເບື້ອງລູກຄ່າຍໄວ້ໂດຍບໍ່ຈຳກັດຈຳນວນ</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">ເຂົ້າເຖິງກິດຈະກຳຂອງບຣາວເຊີໃນລະຫວ່າງການນຳທາງ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">ອ່ານ ແລະ ແກ້ໄຂບຸກມາກ</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ອ່ານ ແລະ ແກ້ໄຂການຕັ້ງຄ່າໃນບຣາວເຊີ</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">ລົບລ້າງປະຫວັດການທ່ອງເວັບ, ຄຸກກີ້ ແລະ ຂໍ້ມູນອື່ນໆທີ່ກ່ຽວຂອ້ງ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ຮັບເອົາຂໍ້ມູນຈາກຄຣິບບອດ</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ປ້ອນຂໍ້ມູນເຂົ້າໄປໃນຄຣິບບອດ</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">ຂັດຂວາງເນື້ອຫາຢູ່ໃນຫນ້າຕ່າງໆ</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">ອ່ານປະຫວັດຜົນການຊອກຫາຂອງທ່ານ</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ດາວໂຫລດໄຟລ ແລະ ອ່ານ ແລະ ແກ້ໄຂປະຫວັດການດາວໂຫລດຂອງບຣາວເຊີ</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ເປີດໄຟລທີ່ດາວໂຫລດມາໃນອຸປະກອນຂອງທ່ານ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">ອ່ານຂໍ້ຄວາມຂອງແທັບທີ່ເປີດທັງໝົດ</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ເຂົ້າເຖິງສະຖານທີ່ຂອງທ່ານ</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ເຂົ້າເຖິງປະຫວັດການທ່ອງເວັບ</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">ຕິດຕາມການນຳໃຊ້ເອັກສເທັນຊັນ ແລະ ຈັດການເທມ</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ແລກປ່ຽນຂໍ້ຄວາມກັບໂປຣແກຣມອື່ນທີ່ນອກເຫນືອຈາກໂປຣແກຣມນີ້</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">ສະແດງການແຈ້ງເຕືອນໃຫ້ທ່ານ</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ໃຫ້ບໍລິການກວດສອບຄວາມຖືກຕ້ອງຂອງການເຂົ້າລະຫັດລັບ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ຄວບຄຸມການຕັ້ງຄ່າພັອກຊີຂອງບຣາວເຊີ</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">ເຂົ້າໄປຫາແທັບທີ່ຫາກໍ່ປິດໄປມື້ກີ້ນີ້</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ເຊື່ອງ ແລະ ສະແດງແທັບຂອງບຣາວເຊີ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ເຂົ້າເຖິງປະຫວັດການທ່ອງເວັບ</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ຂະຫຍາຍເຄື່ອງມືຂອງນັກພັດທະນາເພື່ອເຂົ້າເຖິງຂໍ້ມູນຂອງທ່ານໃນແທັບທີ່ເປີດຢູ່</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ເວີຊັນ</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">ຜູ້ຂຽນ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">ຜູ້ສ້າງ</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">ອັດເດດຫຼ້າສຸດ</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">ຫນ້າທຳອິດ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ຮຽນຮູ້ເພີ່ມເຕີມກ່ຽວກັບສິດທິ</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">ການໃຫ້ຄະແນນ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">ເພີ່ມເຕີມກ່ຽວກັບສ່ວນເສີມນີ້</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ການຕັ້ງຄ່າ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ເປີດ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">ປິດ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">ອະນຸຍາດໃຫ້ຢູ່ໃນໂຫມດການທ່ອງເວັບແບບສ່ວນຕົວ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">ດໍາເນີນການໃນໂຫມດການທ່ອງເວັບແບບສ່ວນຕົວ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ເປີດໃຊ້ງານແລ້ວ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">ປິດໃຊ້ງານແລ້ວ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ຕິດ​ຕັ້ງ​ແລ້ວ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">ແນະນຳ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ຍັງບໍ່ໄດ້ຮັບການສະໜັບສະໜູນເທື່ອ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ຍັງບໍ່ທັນມີໃນຕອນນີ້ເທື່ອ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ປິດໃຊ້ງານແລ້ວ</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">ລາຍລະອຽດ</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ສິດທິ</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">ລຶບ</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">ລາຍງານ</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">ເພີ່ມ %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ຮ້ອງຂໍການອະນຸຍາດເພີ່ມເຕີມ.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">ຕ້ອງໄດ້ຮັບການອະນຸຍາດຈາກທ່ານເພື່ອ:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ສ່ວນຂະຫຍາຍຕ້ອງການ:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">ເພີ່ມ</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">ອະນຸຍາດ</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">ປະຕິເສດ</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ຍົກເລີກ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">ຕິດຕັ້ງ Add-on</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ຍົກເລີກ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">ກວດຄືນ: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ຕົວຈັດການ Add-ons</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">ສ່ວນເສີມຖືກປິດໃຊ້ງານຊົ່ວຄາວ</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">ສ່ວນເສີມໜຶ່ງ ຫຼື ຫຼາຍກວ່ານັ້ນຢຸດເຮັດວຽກ, ເຮັດໃຫ້ລະບົບຂອງທ່ານບໍ່ສະຖຽນ.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">ເລີ່ມສ່ວນເສີມໃໝ່</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">ຊອກຫາສ່ວນເສີມເພີ່ມເຕີມ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ອະນຸຍາດ</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ປະຕິເສດ</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ມີຕົວອັບເດດໃຫມ່</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d ຕ້ອງການສິດໃຫມ່</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ຕ້ອງການສິດໃຫມ່</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">ອັບເດດ Add-on</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">ຮອງຮັບໂຕກວດ Add-on</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">ມີ Add-on ໃໝ່</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">ມີ Add-on ໃໝ່</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">ເພີ່ມ %1$s ເຂົ້າໄປໃນ %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">ເພີ່ມ %1$s ແລະ %2$s ເຂົ້າໄປໃນ %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">ເພີ່ມເຂົ້າໄປໃນ %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox add-on ເປັນເທັກໂນໂລຍີທີ່ທັນສະໄຫມ. Add-on ເຫລົ່ານີ້ໃຊ້ເຟມເວີກທີ່ບໍ່ສາມາດເຮັດວຽກຮ່ວມກັບ Firefox 75 &amp; ເວີຊັນກ່ອນຫນ້ານັ້ນ.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ພວກເຮົາກຳລັງສ້າງໃຫ້ມີການຊັບພອດການເລືອກ Extensions ທີ່ແນະນຳໃນຂັ້ນພື້ນຖານ.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">ກຳລັງດາວໂຫລດ ແລະ ກວດສອບ add-on…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">ບໍ່ສາມາດສອບຖາມ Add-ons ໄດ້!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">ບໍ່ພົບການແປສຳລັບພາສາ %1$s ແລະ ພາສາພື້ນຖານ %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">ສຳເລັດການຕິດຕັ້ງ %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">ການຕິດຕັ້ງ %1$s ລົ້ມເຫລວ</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">ລົ້ມເຫລວໃນການຕິດຕັ້ງ add-on ນີ້.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">ແອດອອນນີ້ບໍ່ສາມາດດາວໂຫຼດໄດ້ເນື່ອງຈາກການເຊື່ອມຕໍ່ລົ້ມເຫລວ.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">ໂປຣແກຣມເສີມນີ້ບໍ່ສາມາດຕິດຕັ້ງໄດ້ເພາະວ່າມັນເກີດມີຂໍ້ຜິດພາດ.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">ໂປຣແກຣມເສີມນີ້ບໍ່ສາມາດຕິດຕັ້ງໄດ້ເພາະວ່າມັນຍັງບໍ່ທັນໄດ້ຖືກກວດສອບ.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">ສຳເລັດການເປີດໃຊ້ງານ %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">ເປີດໃຊ້ງານ %1$s ລົ້ມເຫລວ</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">ສຳເລັດການປິດໃຊ້ງານ %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">ປິດໃຊ້ງານ %1$s ລົ້ມເຫລວ</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">ສຳເລັດຖອນການຕິດຕັ້ງ %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">ຖອນການຕິດຕັ້ງ %1$s ລົ້ມເຫລວ</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">ສຳເລັດການລຶບ %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">ລຶບ %1$s ລົ້ມເຫລວ</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Add-on ນີ້ແມ່ນໄດ້ຮັບການໂອນຍ້າຍມາຈາກເວີຊັນ %1$s ກ່ອນຫນ້ານີ້</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s add-ons</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ຮຽນຮູ້ເພີ່ມເຕີມ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">ອັບເດດສຳເລັດແລ້ວ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">ບໍ່ມີການອັບເດດໃດໆ</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ຂໍ້ຜິດພາດ</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">ຂໍ້ມູນກ່ຽວກັບການອັບເດດ</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ການພະຍາຍາມຄັ້ງຫລ້າສຸດ:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">ສະຖານະ:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ໄດ້ຖືກເພີ່ມເຂົ້າໃນ %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">ເປີດມັນຢູ່ໃນເມນູ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ໂອເຄ, ເຂົ້າໃຈແລ້ວ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..461038b166
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-lt/strings.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Skaityti ir keisti privatumo nuostatas</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Pasiekti jūsų duomenis visose svetainėse</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Pasiekti jūsų duomenis iš %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Pasiekti jūsų duomenis svetainėse, priklausančiose %1$s sričiai</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Pasiekti jūsų duomenis dar 1 svetainėje</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Pasiekti jūsų duomenis dar %1$d svetainėse</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Pasiekti jūsų duomenis dar 1 srityje</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Pasiekti jūsų duomenis dar %1$d srityse</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Pasiekti naršyklės korteles</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Saugoti neribotą kiekį kliento pusės duomenų</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Pasiekti naršyklės veiklą navigacijos metu</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Skaityti ir keisti adresyną</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Skaityti ir keisti naršyklės nuostatas</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Valyti paskiausią naršymo žurnalą, slapukus ir susijusius duomenis</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Gauti duomenis iš iškarpinės</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Įvesti duomenis į iškarpinę</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokuoti turinį bet kuriame puslapyje</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Atsiųsti failus bei skaityti ir keisti naršyklės atsiuntimų žurnalą</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Atverti į jūsų įrenginį atsiųstus failus</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Skaityti visų atvirų kortelių tekstą</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Nustatyti jūsų buvimo vietą</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Pasiekti naršymo žurnalą</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Stebėti priedų naudojimą ir tvarkyti grafinius apvalkalus</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Keistis pranešimais su kitomis programomis negu ši</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Rodyti pranešimus jums</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Teikti kriptografinio tapatumo tikrinimo paslaugas</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Valdyti naršyklės įgaliotojo serverio nuostatas</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Pasiekti paskiausiai užvertas korteles</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Slėpti ir rodyti naršyklės kortelės</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Pasiekti naršymo žurnalą</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Leisti programuotojo priemonėms pasiekti jūsų duomenis atvirose kortelėse</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Laida</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Autoriai</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Atnaujinimo data</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Svetainė</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Sužinokite apie leidimus daugiau</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Įvertinimas</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Nuostatos</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Įjungtas</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Išjungtas</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Leisti naršant privačiai</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Leisti naršant privačiai</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Įjungtas</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Išjungtas</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Įdiegti</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Rekomenduojami</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Dar nepalaikomi</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Dar nėra</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Išjungti</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Išsamiau</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Leidimai</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Pašalinti</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Pridėti „%1$s“?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Reikia jūsų leidimo norint leisti:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Pridėti</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Atsisakyti</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Priedo diegimas</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Atsisakyti</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Įvertinimų: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Priedai</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Priedų tvarkytuvė</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Leisti</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Drausti</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Yra „%1$s“ atnaujinimas</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Reikia %1$d naujų leidimų</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Reikia naujo leidimo</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Priedų atnaujinimai</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Palaikomų priedų tikrinimas</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Išleistas naujas priedas</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Išleisti nauji priedai</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Pridėti „%1$s“ į „%2$s“</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Pridėti „%1$s“ ir „%2$s“ į „%3$s“</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Pridėti juos į %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">„Firefox“ priedų technologija tobulėja. Šie priedai naudoja sistemas, kurios nesuderinamos su „Firefox“ 75-a laida ir vėlesnėmis.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Šiuo metu kuriame pradinį rekomenduojamų priedų sąrašą.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Atsiunčiamas ir patikrinamas priedas…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Nepavyko gauti priedų sąrašo!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Vertimas nerastas nei kalbai %1$s, nei numatytajai kalbai %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Sėkmingai įdiegtas „%1$s“</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Nepavyko įdiegti „%1$s“</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Sėkmingai įjungtas „%1$s“</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Nepavyko įjungti „%1$s“</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Sėkmingai išjungtas „%1$s“</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Nepavyko išjungti „%1$s“</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Sėkmingai pašalintas „%1$s“</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Nepavyko pašalinti „%1$s“</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Sėkmingai pašalintas „%1$s“</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Nepavyko pašalinti „%1$s“</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Šis priedas buvo perkeltas iš ankstesnės „%1$s“ laidos</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 priedas</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s priedai</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Sužinoti daugiau</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Sėkmingai atnaujintas</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Naujinimų nėra</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Klaida</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Naujinimo informacija</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Paskutinis bandymas:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Būsena:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">„%1$s“ buvo pridėtas į „%2$s“</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Atverti jį per meniu</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Gerai, supratau</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..d5aa9255cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-mix/strings.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Kavi tya sama marka</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versión</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Autores</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Tutu xina</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Calificación</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Sama</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Kuna</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Kasi</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Kuna</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Kasi</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Kasi</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Xitaá</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿Chika %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Se necesita permiso para:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Chika</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Kunchatu</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Kunchatu</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f / 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Komplementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Komplementos</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Va’a</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 complemento</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s complementos</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Vaá</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Nixi kaa</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Vaá, Ntsitu niniyu</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..b23fe67b52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ml/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">സ്വകാര്യതാ ക്രമീകരണങ്ങൾ വായിക്കുക പരിഷ്കരിക്കുക</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">എല്ലാ വെബ്സൈറ്റുകൾക്കുമായുള്ള നിങ്ങളുടെ ഡാറ്റ ലഭ്യമാക്കുക</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ബ്രൌസര്‍ ടാബുകള്‍ പരിശോധിക്കുക</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">പരിധിയില്ലാതെ ക്ലൈന്റ്-സൈഡ് ഡാറ്റ സൂക്ഷിക്കുക</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">നാവിഗേഷൻ ചെയ്യുമ്പോൾ ബ്രൗസർ പ്രവർത്തനം ആക്സസ്സുചെയ്യുക</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">അടയാളക്കുറിപ്പുകൾ വായിക്കുക പരിഷ്കരിക്കുക</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ബ്രൗസർ ക്രമീകരണങ്ങൾ വായിക്കുക പരിഷ്കരിക്കുക</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">സമീപകാല ബ്രൗസിംഗ് ചരിത്രം, കുക്കികൾ, അതുമായി ബന്ധപ്പെട്ട ഡാറ്റ എന്നിവ നീക്കം ചെയ്യുക</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ക്ലിപ്ബോര്‍ഡില്‍ നിന്നും ‍ഡാറ്റ എടുക്കുക</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ക്ലിപ്പ്ബോർഡിലേക്ക് ഡാറ്റ നൽകുക</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ഫയലുകൾ ഡൗൺലോഡ്‌ ചെയ്യുകയും ബ്രൗസറിന്റെ ഡൌൺലോഡ് ചരിത്രം വായിക്കുകയും പരിഷ്ക്കരിക്കുകയും ചെയ്യുക</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">നിങ്ങളുടെ ഉപകരണത്തിലേക്ക് ഡൗൺലോഡുചെയ്‌ത ഫയലുകൾ തുറക്കുക</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">തുറന്നിരിക്കുന്ന എല്ലാ ടാബുകളിലേയും കുറിപ്പ്‌ വായിക്കുക</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">നിങ്ങളുടെ സ്ഥലവിവരം ലഭ്യമാക്കുക</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ബ്രൌസിങ്ങ് ചരിത്രം പരിശോധിക്കുക</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">എക്സ്റ്റന്‍ഷന്‍ ഉപയോഗം നിരീക്ഷിക്കുകയും തീമുകള്‍ കൈകാര്യം ചെയ്യുകയും ചെയ്യുക</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">മറ്റ് ആപ്ലിക്കേഷനുകളുമായി സന്ദേശങ്ങള്‍ കൈമാറുക</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">അറിയിപ്പുകൾ പ്രദർശിപ്പിക്കുക</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ക്രിപ്റ്റോഗ്രാഫിക് പ്രമാണീകരണ സേവനങ്ങൾ നൽകുക</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ബ്രൌസറിന്റെ പ്രോക്സി ക്രമീകരണങ്ങള്‍ നിയന്ത്രിക്കുക</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">സമീപകാലത്ത് അടച്ച ടാബുകള്‍ പരിശോധിക്കുക</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ബ്രൗസർ ടാബുകൾ മറയ്ക്കുകയും കാണിക്കുകയും ചെയ്യുക</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ബ്രൌസിങ് നാള്‍വഴി പരിശോധിക്കുക</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ഡെവെലപ്പര്‍ ടൂളുകള്‍ക്ക് തുറന്ന ടാബുകളിലെ ഡാറ്റ എടുക്കാന്‍ അനുമതി നല്‍കുക</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">പതിപ്പു്</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">രചയിതാക്കൾ</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">ഏറ്റവും ഒടുവില്‍ പരിഷ്കരിച്ചതു്</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">പൂമുഖം</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">അനുമതികളെക്കുറിച്ച് കൂടുതലറിയുക</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">റേറ്റിങ്</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ക്രമീകരണങ്ങള്‍</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ഓണ്‍</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">ഓഫ്</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">സ്വകാര്യ ബ്രൗസിംഗിൽ ലഭ്യമാക്കുക</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">സ്വകാര്യ ബ്രൗസിംഗിൽ തുറക്കുക</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">പ്രാപ്തമാക്കി</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">അപ്രാപ്തമാക്കി</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ഇൻസ്റ്റാൾ ചെയ്തു</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">ശുപാർശിതം</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ഇതുവരെ പിന്തുണയ്‌ക്കുന്നില്ല</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ഇതുവരെ ലഭ്യമല്ല</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">പ്രവര്‍ത്തനരഹിതമാക്കി</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">വിശദാംശങ്ങള്‍</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">അനുമതികൾ</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">നീക്കം ചെയ്യുക</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s ചേര്‍ക്കണോ?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">നിങ്ങളുടെ അനുമതി ആവശ്യമാണ്:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">കൂട്ടിച്ചേര്‍ക്കുക</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">റദ്ദാക്കുക</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">ആഡ്-ഓണ്‍ ഇന്‍സ്റ്റോള്‍ ചെയ്യുക</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">റദ്ദാക്കുക</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ആഡ്-ഓണുകള്‍</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">അനുവദിക്കുക</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">നിരസിക്കുക</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ന് ഒരു പുതുക്കൽ ലഭ്യമാണ്</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d പുതിയ അനുമതികൾ ആവശ്യമാണ്</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ഒരു പുതിയ അനുമതി ആവശ്യമാണ്</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">ആഡ്-ഓൺ പുതുക്കലുകൾ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">പിന്തുണയുള്ള ആഡ്-ഓണുകൾക്കായുള്ള പരിശോധകർ</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">പുതിയ ആഡ്-ഓൺ ലഭ്യമാണ്</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">പുതിയ ആഡ്-ഓണുകൾ ലഭ്യമാണ്</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s ലേക്ക് %1$s ചേർക്കുക</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s ലേക്ക് %1$s ഉം %2$s ഉം ചേർക്കുക</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">ഇവയെ %1$s ലേക്ക് ചേർക്കുക</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">ഫയർഫോക്സ് ആഡ്-ഓൺ സാങ്കേതികവിദ്യ നവീകരിക്കപ്പെടുന്നു. ഈ ആഡ്-ഓണുകൾ ഫയർഫോക്സ് 75-ും അതിനു മുകളിലും ഉള്ള പതിപ്പുകളുമായി പൊരുത്തപ്പെടാത്ത ഫ്രെയിംവർക്കുകൾ ഉപയോഗിക്കുന്നു.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ശുപാർശ ചെയ്യപ്പെട്ട വിപുലീകരണങ്ങളുടെ പ്രാരംഭ തിരഞ്ഞെടുപ്പിനായി ഞങ്ങൾ നിലവിൽ പിന്തുണ സജ്ജീകരിക്കുകയാണ്.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">ആഡ്-ഓൺ ഡൗൺലോഡ് ചെയ്യുകയും പരിശോധിക്കുകയും ചെയ്യുന്നു…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">ആഡ്-ഓണുകൾ അന്വേഷിക്കുന്നതിൽ പരാജയപ്പെട്ടു!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s വിജയകരമായി ഇൻസ്റ്റാൾ ചെയ്തു</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ഇൻസ്റ്റാൾ ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s വിജയകരമായി പ്രവർത്തനസജ്ജമാക്കി</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s പ്രവർത്തനസജ്ജമാക്കുന്നതിൽ പരാജയപ്പെട്ടു</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s വിജയകരമായി പ്രവർത്തനരഹിതമാക്കി</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s പ്രവർത്തനരഹിതമാക്കുന്നതിൽ പരാജയപ്പെട്ടു</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s വിജയകരമായി നീക്കം ചെയ്തു</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s നീക്കം ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s വിജയകരമായി നീക്കം ചെയ്‌തു</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s നീക്കം ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">%1$s ന്റെ മുമ്പത്തെ പതിപ്പിൽ നിന്ന് ഈ ആഡ്-ഓൺ മൈഗ്രേറ്റ് ചെയ്തതാണ്</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 ആഡ്-ഓൺ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s ആഡ്-ഓണുകൾ</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">കൂടുതല്‍ അറിയുക</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">വിജയകരമായി പുതുക്കി</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">പുതുക്കലുകൾ ലഭ്യമല്ല</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">പിശക്</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">അപ്‌ഡേറ്റർ‌ വിവരങ്ങൾ‌</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">അവസാന ശ്രമം:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">സ്ഥിതി:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s %2$s ലേക്ക് ചേർത്തു</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">മെനുവിൽ ഇത് തുറക്കുക</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ശരി, മനസ്സിലായി</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..4ccadf9c62
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-mr/strings.xml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">गोपनीयता सेटिंग वाचा व बदला</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">सर्व संकेतस्थळांची आपली माहिती पहा</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s साठी आपली माहिती पहा</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s क्षेत्रातील साईटसाठी आपली माहिती पहा</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">1 इतर साइटवर आपला डेटा पहा</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ब्राउझरचे टॅब पहा</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">क्लायंट कडे अमर्यादित माहिती ठेवा</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">संचारण सुरु असताना ब्राउझर कार्य पहा</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">वाचनखूणा वाचा आणि बदला</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ब्राउझर सेटिंग वाचा आणि बदला</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">अलीकडील ब्राउझिंग इतिहास, कुकीज आणि संबंधित डेटा नष्ट करा</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">क्लिपबोर्ड वरील माहिती घ्या</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">क्लिपबोर्ड वर माहिती भरा</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">फाईल डाउनलोड करा आणि ब्राउझरचा डाउनलोड इतिहास वाचून बदल करा</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">आपल्या डिव्हाइसवर डाउनलोड केलेल्या फाईल उघडा</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">सर्व उघडलेल्या टॅबचे मजकूर वाचा</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">आपले ठिकाण पहा</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ब्राऊझिंग इतिहास पहा</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">एक्स्टेंशनचा वापर मॉनिटर करा व थीम व्यवस्थापित करा</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ह्याव्यतिरिक्त इतर अॅप्स सोबत संदेशांची देवाणघेवाण करा</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">आपल्याला सूचना दर्शवा</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">क्रिप्टोग्राफिक प्रमाणीकरण सेवा प्रदान करा</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ब्राउझर प्रॉक्सी सेटिंग संचालित करा</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">नुकतेच बंद केलेले टॅब पहा</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ब्राउझर टॅब लपवा आणि दाखवा</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ब्राऊझिंग इतिहास पहा</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">खुल्या टॅबमध्ये आपला डेटा वापरण्यासाठी डेव्हलपर साधनांचा विस्तार करा</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">आवृत्ती</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">लेखक</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">शेवटचे अद्ययावत</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">मुख्यपृष्ठ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">परवानग्यांबद्दल अधिक जाणून घ्या</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">मूल्यांकन</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">सेटिंग</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">सुरू</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">बंद</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">खाजगी ब्राउझिंग मध्ये अनुमती द्या</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">खाजगी ब्राउझिंग मध्ये चालवा</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">सक्रिय</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">निष्क्रिय</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">प्रतिष्ठापित झाले</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">शिफारस केलेले</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">अद्याप समर्थित नाही</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">सध्या उपलब्ध नाही</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">निष्क्रिय</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">तपशील</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">परवानग्या</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">काढून टाका</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s जोडायचे?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">यासाठी आपली परवानगी हवी आहे:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">जोडा</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">रद्द करा</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">ॲड-ऑन प्रतिष्ठापीत करा</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">रद्द करा</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ॲड-ऑन</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">अ‍ॅड-ऑन व्यवस्थापक</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">परवानगी द्या</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">नाकारा</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s मध्ये नवीन अद्यतन आहे</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d नवीन परवानग्या आवश्यक आहेत</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">नवीन परवानगी आवश्यक आहे</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">ॲड-ऑन सुधारणा</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">समर्थित ॲड-ऑन तपासक</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">नवीन ॲड-ऑन उपलब्ध</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">नवीन ॲड-ऑन उपलब्ध</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s ला %2$s मध्ये जोडा</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s व %2$s ला %3$s मध्ये जोडा</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">त्यांना %1$s मध्ये जोडा</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox अ‍ॅड-ऑन तंत्रज्ञान आधुनिक होत आहे. हे अ‍ॅड-ऑन फ्रेमवर्क वापरतात जे Firefox 75 आणि त्यानंतरच्या आवृत्तींसोबत सुसंगत नाहीत.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">आम्ही सध्या शिफारस केलेल्या विस्तारांच्या प्रारंभिक निवडीसाठी समर्थन तयार करत आहोत.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">अ‍ॅड-ऑन डाउनलोड आणि सत्यापित करीत आहे…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">अ‍ॅड-ऑनची चौकशी करण्यात अयशस्वी!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s यशस्वीरित्या प्रस्थापित केले</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s प्रस्थापित करण्यात अयशस्वी</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s यशस्वीरित्या सक्रिय</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s सक्रिय करण्यात अयशस्वी</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s यशस्वीरित्या निष्क्रिय</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s निष्क्रिय करण्यात अयशस्वी</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s यशस्वीरित्या विस्थापित</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s विस्थापित करण्यात अयशस्वी</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s यशस्वीरित्या काढले</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s काढण्यात अयशस्वी</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">हे अ‍ॅड-ऑन %1$s च्या मागील आवृत्तीमधून स्थलांतरित केले आहे</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 अ‍ॅड-ऑन</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s अ‍ॅड-ऑन</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">अधिक जाणा</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">यशस्वीरित्या अद्यतनित</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">अद्यतन उपलब्ध नाही</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">त्रुटी</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">अद्यतन माहिती</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">मागील प्रयत्न:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">स्थिती:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s हे %2$s मध्ये जोडले गेले आहे</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">त्यास मेनू मध्ये उघडा</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ठीक आहे, समजले</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..fb6dfe441b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-my/strings.xml
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">ကိုယ်ရေးကာကွယ်မှု အပြင်အဆင်များကို ဖတ်ရှုပြင်ဆင်ရန်</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ဝဘ်ဆိုက်အားလုံးအတွက် သင့်အချက်အလက်များကို ကြည့်မည်</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s အတွက် သင်၏ အချက်အလက် များ ရယူမည်</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s ဒိုမိန်းတွင်ရှိသော ဆိုက်များအတွက် သင်၏အချက်အလက်အား ရယူမည်</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">အခြား ဆိုက် 1 ဆိုက် မှ သင်၏ အချက်အလက်များ ရယူမည်</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">အခြား %1$d ဆိုက် ရှိ သင်၏ အချက်အလက်များ ရယူမည်</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">အခြား ဒိုမိန်း 1 ခု ရှိ သင်၏ အချက်အလက်များ ရယူမည်</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">အခြား ဒိုမိန်း %1$d ခု ရှိ သင်၏ အချက်အလက်များ ရယူမည်</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ဘယောက်ဆာတပ်ဗ်များအားသုံးမည်</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">သုံးစွဲသူအချက်အလက်များကိုအကန့်အသတ်မဲ့သိမ်းပါ။</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">လည်ပတ်နေစဉ် ဘရောင်ဇာဆောင်ရွက်မှုကို ကြည့်ရှုမည်</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">စာမှတ်များကို ဖတ်ရန်နှင့် ပြင်ဆင်ရန်</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ဘရောင်ဇာအပြင်အဆင်များကို ဖတ်ရှုပြင်ဆင်ရန်</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">မကြာသေးမီကကြည့်ရှုခဲ့သည့်မှတ်တမ်း၊ cookies နှင့်ဆက်စပ်သောအချက်အလက်များကိုရှင်းလင်းပါ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">clipboard မှဒေတာကိုရယူပါ</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ကလစ်ဘုတ်ထဲသို့ ဒေတာထည့်ပါ</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ဖိုင်များကို ဆွဲယူရန်၊ ဘရောင်ဇာ၏ ဆွဲယူမှတ်တမ်းကို ဖတ်ရန်နှင့် ပြင်ဆင်ရန်</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">သင့်စက်ထဲသို့ဆွဲထားသောဖိုင်များကိုဖွင့်ပါ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">ဖွင့်ထားသော တပ်ဗ်များမှ စာများကို ဖတ်ပါ</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">သင့်တည်နေရာကိုရယူပါ</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ရှာဖွေကြည့်ရှုခဲ့သည့်မှတ်တမ်းကို အသုံးပြုမည်</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">ထပ်တိုးများ အသုံးပြုမှုကိုစောင့်ကြည့်ရန်နှင့် အပြင်အဆင်များကိုစီမံပါ။</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ဒီအက်ပ်နှင့်အခြားအက်ပ်များ မက်ဆေ့ခ်ျများကိုဖလှယ်ပါ</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">သင့်အတွက် သတိပေးချက်များ ပြပေးမည်</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">စာဝှက် စစ်ဆေးခြင်း ဝန်ဆောင်မှုကို ထောက်ပံ့ပါ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ဘရောင်ဇာ၏ ပရောက်ဇီအပြင်အဆင်များကို ထိန်းချုပ်ပါ</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">မကြာသေးမီကပိတ်ထားသော tabs များကိုရယူပါ</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ဘရောင်ဇာတပ်ဗ်များ ဖျောက်ကွယ်ခြင်းနှင့် ပြသခြင်း</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ရှာဖွေကြည့်ရှုခဲ့သည့်မှတ်တမ်းကို အသုံးပြုမည်</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ဖွင့်ထားသော တပ်ဗ်များတွင် ဒေတာကို အသုံးပြုရန် Developer Tools ကို အသုံးပြုပါ</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ဗားရှင်း</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">စာရေးသူ</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">နောက်ဆုံးအခြေအနေ</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">အဖွင့်စာမျက်နှာ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ခွင့်ပြုချက်များအကြောင်းပိုမိုလေ့လာပါ</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">အဆင့်</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">အပြင်အဆင်များ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ဖွင့်ပါ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">ပိတ်ပါ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">တစ်ကိုယ်ရေသုံးချိန်တွင်ခွင့်ပြုရန်</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">တစ်ကိုယ်ရည်သုံးတွင်ထားရန်</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ဖွင့်ထားသည်</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">ပိတ်ထားသည်</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ထည့်သွင်းထားသည်</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">အကြံပြုထားသော</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">မထောက်ပံ့ရသေး</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">မရရှိနိုင်သေးပါ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ပိတ်ထားတယ်</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">အသေးစိတ်များ</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ခွင့်ပြုချက်များ</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">ဖယ်ရှားပါ</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s ကို ထည့်မည်လား။</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">သင့်ခွင့်ပြုချက် လိုအပ်သည်။</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">ထည့်ရန်</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ပယ်​ဖျက်ပါ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">အတ်အွန်အားတပ်ဆင်ပါ</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">မလုပ်တော့ပါ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">သုံးသပ်ချက်များ %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f / 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">အတ်အွန်များ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">အပို ပရိုဂရမ် မန်နေဂျာ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ခွင့်ပြုပါ</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">တားမြစ်ပါ</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s တွင်အသစ်ရှိသည်</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d ခွင့်ပြုချက်အသစ်လိုအပ်သည်</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ခွင့်ပြုချက်အသစ်လိုအပ်သည်</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">ထပ်တိုးမွမ်းမံမှုများ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">ပံ့ပိုးထားတဲ့အက်အွန်များကိုစစ်ဆေးသူ</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">ရနိုင်သည့်အက်အွန်အသစ်</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">ရနိုင်သည့်အက်အွန်အသစ်များ</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s သို့ %1$s ကိုပေါင်းထည့်ပါ။</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s သို့ %2$s နှင့် %1$s ကို ပေါင်းထည့်ပါ။</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">%1$s သို့ သူတို့ကိုပေါင်းထည့်ပါ။</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox အတ်အွန်နည်းပညာသည် ခေတ်မှီနေပါပြီ။ ယခု သုံးနေသည် အတ်အွန်များသည် Firefox 75 နှင့် အထက်တွင် သုံးဆွဲလို့မရနိုင်ပါ။</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">စတင်သုံး ထပ်ပေါ်းဆော့ဝဲထည့်သွင်းခြင်းအား ထောက်ပံ့ပေးနိုင်ရန် လုပ်ဆောင်လျက်ရှိပါသည်</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">မွမ်းမံချက်များကိုဒေါင်းလုပ် လုပ်၍ စစ်ဆေးခြင်း…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">ထပ်တိုးမှုများကိုရှာဖွေရန်မအောင်မြင်ပါ။</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">ဒေသ %1$s အတွက် ပုံသေ ဘာသာစကား %2$s ဘာသာပြန်ဆိုမှု မတွေ့ရှိပါ။ </string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s အားအောင်မြင်စွာထည့်သွင်းပြီးဖြစ်သည်</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ထည့်သွင်းရန်မအောင်မြင်ပါ</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s အားဖွင့်ထားပြီးပါပြီ</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s ဖွင့်ရန်မအောင်မြင်ပါ</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s အားပိတ်ထားပြီးပါပြီ</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s ပိတ်ရန်မအောင်မြင်ပါ</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s အားအောင်မြင်စွာဖယ်ရှားပြီးဖြစ်သည်</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s အားဖယ်ထုတ်ရန်မအောင်မြင်ပါ</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s အားဖယ်ရှားပြီး</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ဖယ်ရှားရန်မအောင်မြင်ပါ</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ဤအပိုဆောင်းထည့်သွင်းမှုကို %1$s ၏ယခင်ဗားရှင်းမှပြောင်းရွှေ့ခဲ့သည်</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 အက်အွန်</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s ထပ်တိုးများ</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ပိုမိုလေ့လာရန်</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">အောင်မြင်စွာ အဆင့်မြှင့်ပြီး</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">အဆင့်မြင်တင်မှု မရနိုင်သေးပါ</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">အမှား</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">အဆင့်မြှင့်တင်မှပေးသော အချက်အလက်များ</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">နောက်ဆုံးကြိုးပမ်းမှု -</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">အခြေအနေ -</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ကို %2$s သို့ ထည့်သွင်ပြီး</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">၎င်း ကို မီနူး ထဲတွင် ဖွင့်ပါ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">အိုကေ၊ ရပြီ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..1442e8407f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lese og endre personverninnstillinger</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Få tilgang til dine data for alle nettsteder</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Få tilgang til dine data for %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Få tilgang til dine data fra nettsteder under %1$s-domenet</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Få tilgang til dine data på ett annet nettsted</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Få tilgang til dine data på %1$d andre nettsteder</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Få tilgang til dine data på ett annet domene</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Få tilgang til dine data på %1$d andre domener</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d av %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Få tilgang til faner</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Lagre ubegrenset mengde klientsidedata</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Tilgang til nettleseraktivitet under navigasjon</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Les og endre bokmerker</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lese og endre nettleserinnstillinger</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Fjern nylig nettlesingshistorikk, infokapsler og relaterte data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Hente data fra utklippstavlen</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Sette inn data på utklippstavlen</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokker innhold på alle sider</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lese din nettleserhistorikk</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Laste ned filer og lese og endre nettleserens nedlastingslogg</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Åpne filer som er lastet ned til enheten din</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lese teksten i alle åpne faner</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Se plasseringen din</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Tilgang til nettleserhistorikken</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Overvåke bruk av utvidelser og behandle temaer</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Utveksle meldinger med andre apper enn denne</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Vise deg varsler</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Tilby kryptografiske godkjenningstjenester</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontrollere proxy-innstillinger for nettleser</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Tilgang til nylig lukkede faner</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Skjul og vis nettleserfaner</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Tilgang til nettleserhistorikken</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Utvide utviklerverktøy for å få tilgang til dine data i åpne faner</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versjon</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Utvikler</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Utviklere</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Sist oppdatert</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Hjemmeside</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Les mer om tillatelser</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Vurdering</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Mer om denne utvidelsen</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Mer om denne utvidelsen</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Innstillinger</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">På</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Av</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Tillat i privat nettlesing</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Kjør i privat nettlesing</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Ikke tillatt i private vinduer</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Påslått</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Avslått</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installert</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Anbefalt</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ikke støttet ennå</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ikke tilgjengelig enda</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Avslått</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detaljer</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Tillatelser</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Fjern</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Rapporter</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Legg til %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ber om ytterligere tillatelser.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Det krever din tillatelse for å:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Den vil:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Legg til</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Tillat</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Avvis</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Avbryt</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installer utvidelse</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installer %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Avbryt</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Omtaler: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Rangering: %1$.02f av 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Utvidelser</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Tilleggsbehandler</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg er midlertidig deaktivert</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Utvidelser er midlertidig deaktivert</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Ett eller flere tillegg sluttet å virke, noe som gjorde systemet ditt ustabilt.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">En eller flere utvidelser sluttet å virke, noe som gjorde systemet ditt ustabilt.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Start tilleggene på nytt</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Start utvidelser på nytt</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Finn flere tillegg</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Finn flere utvidelser</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Tillat</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Avslå</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s har en ny oppdatering</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nye tillatelser er påkrevd</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">En ny tillatelse er påkrevd</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Tilleggsoppdateringer</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Utvidelsesoppdateringer</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Støttet utvidelse-sjekker</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Ny utvidelse tilgjengelig</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nye utvidelser tilgjengelig</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Legg til %1$s i %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Legg til %1$s og %2$s i %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Legg dem til i %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox-tilleggsteknologi moderniseres. Disse tilleggene bruker rammeverk som ikke er kompatible med Firefox 75 og nyere.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Vi utvikler for øyeblikket støtte for et første utvalg av anbefalte utvidelser.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Laster ned og verifiserer utvidelse…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Laster ned og verifiserer utvidelsen …</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Forespørsel om tilleggsprogrammer mislyktes!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Forespørsel om utvidelser mislyktes!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Oversettelsen ble ikke funnet, for språket %2$s eller for standardspråket %1$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s ble installert</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Kunne ikke installere %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Kunne ikke installere dette tillegget.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Kunne ikke installere denne utvidelsen.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Klarte ikke laste ned tillegget på grunn av en tilkoblingsfeil.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Klarte ikke laste ned utvidelsen på grunn av en tilkoblingsfeil.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Klarte ikke installere dette tillegget fordi den ser ut til å være skadet.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Klarte ikke installere denne utvidelsen fordi den ser ut til å være skadet.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Klarte ikke installere dette tillegget fordi den ikke har blitt bekreftet.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Klarte ikke installere denne utvidelsen fordi den ikke har blitt bekreftet.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Klarte ikke installere %1$s fordi den ikke er kompatibel med %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Kan ikke installere %1$s fordi den har høy risko for å forårsake stabilitets- eller sikkerhetsproblemer.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ble aktivert</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Kunne ikke aktivere %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s ble deaktivert</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Kunne ikke deaktivere %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s ble avinstallert</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Kunne ikke avinstallere %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s ble fjernet</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Kunne ikke fjerne %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Dette tillegget ble overført fra en tidligere versjon av %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 tillegg</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 utvidelse</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s tillegg</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s utvidelser</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Les mer</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Oppdatert</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ingen oppdatering tilgjengelig</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Feil</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Oppdateringsinformasjon</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Siste forsøk:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s er lagt til i %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Åpne det i menyen</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Få tilgang til %1$s fra %2$s-menyen.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, jeg forstår</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Les mer</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s har blitt deaktivert på grunn av sikkerhets- eller stabilitetsproblemer.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s kunne ikke bekreftes som sikker og har blitt deaktivert.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s er ikke kompatibel med din versjon av %2$s (versjon %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..6c6dd9b044
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">पढ्नुहोस् र गोपनीयता सेटिंग्स परिमार्जन गर्नुहोस्</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">सबै वेबसाइटहरूका लागि तपाईंको डाटा पहुँच गर्नुहोस्</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s को लागि तपाईंको डाटा पहुँच गर्नुहोस्</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description"> %1$s डोमेनमा साईटहरूको लागि तपाईंको डाटा पहुँच गर्नुहोस्</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">१ अन्य साइटमा तपाईंको डाटा पहुँच गर्नुहोस्</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate"> %1$d अन्य साइटहरूमा तपाईंको डाटा पहुँच गर्नुहोस्</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate"> 1 अन्य डोमेन मा तपाइँको डाटा पहुँच गर्नुहोस्</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate"> %1$d अन्य डोमेनहरुमा तपाइँको डाटा पहुँच गर्नुहोस्</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ब्राउजर ट्याबहरू पहुँच गर्नुहोस्</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ग्राहक-साइड डाटाको असीमित रकम भण्डार गर्नुहोस्</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">नेभिगेसनको बखत ब्राउजर गतिविधि पहुँच गर्नुहोस्</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">पढ्नुहोस् र बुकमार्कहरू परिमार्जन गर्नुहोस्</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ब्राउजर सेटिङ्गहरु पढ्नुहोस् र परिमार्जन गर्नुहोस्</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">हालका ब्राउजिंग ईतिहास, कुकीहरू र सम्बन्धित डाटा खाली गर्नुहोस्</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">क्लिपबोर्डवाट डाटा लिनुहोस्</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">क्लिपबोर्डमा डाटा इनपुट गर्नुहोस्</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">फाईलहरू डाउनलोड गर्नुहोस् र ब्राउजरको डाउनलोड ईतिहास पढ्नुहोस् र परिमार्जन गर्नुहोस्</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">तपाईंको उपकरणमा डाउनलोड गरिएका फाइलहरू खोल्नुहोस्</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">सबै खोलिएका ट्याबहरूको पाठ पढ्नुहोस्</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">तपाईँको स्थानको पहुँच दिनुहोस्</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ब्राउजिंग इतिहास हेर्नुहोस्</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">बिस्तारहरुको उपयोगको अनुगमन गर्नुहोस् र थीमहरु प्रबन्ध गर्नुहोस्</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">यो भन्दा अन्य एपहरूसँग सन्देश साटासाट गर्नुहोस्</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">तपाईँको लागि सूचना प्रदर्शन गर्नुहोस्</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">क्रिप्टोग्राफिक प्रमाणीकरण सम्बन्धी सेवाहरू प्रदान गर्नुहोस्</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ब्राउजरको प्रोक्सी सेटिङ्गहरू नियन्त्रण गर्नुहोस्</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">हालै बन्द गरिएका ट्याबहरू सम्म पहुँच गर्नुहोस्</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ब्राउजर ट्याबहरु लुकाउनुहोस् र देखाउनुहोस्</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ब्राउजिङ्ग इतिहासमा पहुँच गर्नुहोस्</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">खोलिएका ट्याबहरूमा तपाईँको डाटा पहुँच गर्न विकासकर्ता औजारहरू विस्तार गर्नुहोस्</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">संस्करण</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">लेखकहरू</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">पछिल्लो अद्यावधिक</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">गृहपृष्ठ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">अनुमतिहरुको बारेमा थप जान्नुहोस्</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">रेटिङ्ग</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">सेटिङ्गहरु</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">खोल्नुहोस्</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">बन्द गर्नुहोस्</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">निजी ब्राउजिङ्गमा अनुमति दिनुहोस्</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">निजी ब्राउजिङ्गमा चलाउनुहोस्</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">सक्षम गरियो</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">असक्षम गरियो</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">स्थापित</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">सिफारिस गरिएको</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">अझै असमर्थित</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">अझै अनुपलब्ध</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">असक्षम गरियो</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">बिवरणहरु</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">अनुमतिहरु</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">हटाउनुहोस्</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s थप्न चाहानुहुन्छ ?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">यसलाई निम्न अनुमतिहरु आवश्यक छन्:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">थप्नुहोस्</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">रद्द गर्नुहोस्</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">एड-अन स्थापना गर्नुहोस्</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">रद्द गर्नुहोस्</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">समीक्षाहरु: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f / 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">एड-अनहरु</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">एड-अन प्रबन्धक</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">अनुमति दिनुहोस्</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">अस्वीकार गर्नुहोस्</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s को एउटा नयाँ अद्यावधिक छ</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d नयाँ अनुमतिहरु आवश्यक छन्</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">एउटा नयाँ अनुमति आवश्यक छ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">एड-अन अद्यावधिकहरु</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">समर्थित एड-अनहरु परीक्षक</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">नयाँ एड-अन उपलब्ध छ</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">नयाँ एड-अनहरु उपलब्ध छन्</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s लाई %2$s मा थप्नुहोस्</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s र %2$s लाई %3$s मा थप्नुहोस्</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">तिनीहरुलाई %1$s मा थप्नुहोस्</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox एड-अन प्रबिधी आधुनिकीकरण गर्दैछ। यी एड-अनहरूले फ्रेमवर्कहरू प्रयोग गर्छन् जुन, Firefox 75 र त्यसभन्दा माथिका सङ्ग मिल्दैन।</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">हामी हाल सिफारिस गरिएका विस्तारहरूको प्रारम्भिक छनौटको लागि समर्थन निर्माण गर्दैछौं।</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">एड-अन डाउनलोड र प्रमाणीकरण गरिदै…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">एड-अनहरु सोध्ने गर्न असफल भयो!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">भावानुवाद फेला परेन, स्थानिय %1$s को लागि न त, पूर्वनिर्धारित भाषा %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s सफलतापूर्वक स्थापना गरियो</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s स्थापना गर्ने क्रममा समस्या देखा पर्यो</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s लाई सफलातापूर्वक सक्षम गरियो</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s लाई सक्षम पार्ने क्रममा समस्या देखापर्यो</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s लाई सफलातापूर्वक असक्षम गरियो</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s लाई असक्षम पार्ने क्रममा समस्या देखापर्यो</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s सफलतापूर्वक उपकरणबाट हटाइयो</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s उपकरणबाट हटाउने क्रममा समस्या देखा पर्यो</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s सफलतापूर्वक हटाइयो</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s लाई हटाउने क्रममा समस्या देखापर्यो</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">यो एड-अन %1$s को एउटा पहिलाको सस्करणबाट साभार गरिएको हो </string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 एड-अन</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s एड-अनहरु</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">थप जान्नुहोस्</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">सफलतापूर्वक अद्यावधिक गरियो</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">कुनै अद्यावधिक उपलब्ध छैनन्</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">त्रुटि</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">अद्यावधिककर्ता सूचना</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">अन्तिम प्रयास:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">स्थिति:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s लाई %2$s मा थपिएको छ</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">यसलाई मेनुमा खोल्नुहोस्</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ठीक छ, मैले बुझेँ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..a6a5f42fbd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-nl/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Privacyinstellingen lezen en aanpassen</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Uw gegevens voor alle websites benaderen</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Uw gegevens voor %1$s benaderen</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Uw gegevens voor websites in het domein %1$s benaderen</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Uw gegevens op 1 andere website benaderen</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Uw gegevens op %1$d andere websites benaderen</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Uw gegevens op 1 ander domein benaderen</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Uw gegevens op %1$d andere domeinen benaderen</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d van %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Browsertabbladen benaderen</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Gegevens aan clientzijde onbeperkt opslaan</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Browseractiviteit tijdens navigeren benaderen</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Bladwijzers lezen en aanpassen</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Browserinstellingen lezen en aanpassen</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Recente browsergeschiedenis, cookies en gerelateerde gegevens wissen</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Gegevens van het klembord ophalen</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Gegevens op het klembord plaatsen</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Inhoud op elke pagina blokkeren</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Uw navigatiegeschiedenis lezen</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Bestanden downloaden en downloadgeschiedenis van de browser lezen en aanpassen</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Naar uw apparaat gedownloade bestanden openen</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">De tekst van alle open tabbladen lezen</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Uw locatie benaderen</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Browsergeschiedenis benaderen</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Extensiegebruik bewaken en thema’s beheren</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Berichten met andere apps dan deze uitwisselen</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Notificaties weergeven</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Cryptografische authenticatieservices bieden</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Browserproxyinstellingen beheren</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Onlangs gesloten tabbladen benaderen</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Browsertabbladen verbergen en tonen</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Browsergeschiedenis benaderen</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ontwikkelaarshulpmiddelen uitbreiden om uw gegevens in open tabbladen te benaderen</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versie</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Schrijver</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Auteurs</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Laatst bijgewerkt</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Startpagina</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Meer info over toestemmingen</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Waardering</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Meer over deze add-on</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Meer over deze extensie</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Instellingen</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Aan</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Uit</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Toestaan tijdens privénavigatie</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Uitvoeren tijdens privénavigatie</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Niet toegestaan in privévensters</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ingeschakeld</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Uitgeschakeld</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Geïnstalleerd</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Aanbevolen</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Nog niet ondersteund</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Nog niet beschikbaar</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Uitgeschakeld</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Details</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Toestemmingen</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Verwijderen</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Rapporteren</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s toevoegen?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s vraagt aanvullende toestemmingen.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Dit vereist uw toestemming voor het volgende:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">De add-on wil:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Toevoegen</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Toestaan</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Weigeren</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Annuleren</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Add-on installeren</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s installeren</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Annuleren</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Beoordelingen: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Waardering: %1$.02f van 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Add-onbeheerder</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons zijn tijdelijk uitgeschakeld</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Extensies zijn tijdelijk uitgeschakeld</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Een of meer add-ons werken niet meer, waardoor uw systeem instabiel wordt.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Een of meer extensies werken niet meer, waardoor uw systeem instabiel wordt.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons herstarten</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Extensies herstarten</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Meer add-ons zoeken</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Meer extensies zoeken</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Toestaan</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Weigeren</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s heeft een nieuwe update</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nieuwe machtigingen vereist</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Er is een nieuwe machtiging vereist</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Add-on-updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Extensie-updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Controle op ondersteunde add-ons</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nieuwe add-on beschikbaar</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nieuwe add-ons beschikbaar</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s toevoegen aan %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s en %2$s toevoegen aan %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Toevoegen aan %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">De technologie van Firefox-add-ons moderniseert. Deze add-ons gebruiken frameworks die niet compatibel zijn met Firefox 75 en hoger.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">We bouwen momenteel ondersteuning voor een eerste selectie van aanbevolen extensies.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Add-on downloaden en verifiëren…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Extensie downloaden en verifiëren…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Zoeken naar add-ons mislukt!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Zoeken naar extensies mislukt!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Geen vertaling voor locale %1$s of voor de standaardtaal %2$s gevonden</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s met succes geïnstalleerd</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Installatie van %1$s mislukt</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Installatie van deze add-on mislukt.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Installatie van deze extensie mislukt.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Deze add-on kon niet worden gedownload, vanwege een fout in de verbinding.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Deze extensie kon niet worden gedownload, vanwege een fout in de verbinding.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Deze add-on kon niet worden geïnstalleerd, omdat deze beschadigd lijkt.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Deze extensie kon niet worden geïnstalleerd, omdat deze beschadigd lijkt.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Deze add-on kon niet worden geïnstalleerd, omdat deze niet is geverifieerd.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Deze extensie kon niet worden geïnstalleerd, omdat deze niet is geverifieerd.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s kon niet worden geïnstalleerd, omdat het niet compatibel is met %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s kon niet worden geïnstalleerd, omdat het een hoog risico op stabiliteits- of beveiligingsproblemen geeft.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s met succes ingeschakeld</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Inschakelen van %1$s mislukt</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s met succes uitgeschakeld</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Uitschakelen %1$s mislukt</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s met succes gedeïnstalleerd</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Deïnstalleren van %1$s mislukt</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s met succes verwijderd</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Verwijderen van %1$s mislukt</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Deze add-on is gemigreerd vanuit een eerdere versie van %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensie</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s add-ons</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensies</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Meer info</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Met succes bijgewerkt</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Geen update beschikbaar</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Fout</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informatie over updates</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Laatste poging:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s is aan %2$s toegevoegd</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">In het menu openen</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Benader %1$s vanuit het %2$s-menu.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Oké, begrepen</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Meer info</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s is uitgeschakeld vanwege beveiligings- of stabiliteitsproblemen.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s kon niet worden geverifieerd als veilig en is uitgeschakeld.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s is niet compatibel met uw versie van %2$s (versie %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..0497c14100
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,283 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lese og endre personverninnstillingar</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Få tilgang til dine data for alle nettstadar</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Få tilgang til dine data for %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Få tilgang til dine data frå nettstadar under %1$s-domenet</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Få tilgang til dine data på ein annan nettstad</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Få tilgang til dine data på %1$d andre nettstadar</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Få tilgang til dine data på eitt anna domene</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Få tilgang til dine data på %1$d andre domene</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d av %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Få tilgang til faner</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Lagre uavgrensea mengde klientsidedata</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Tilgang til nettlesaraktivitet under navigasjon</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lese og endre bokmerker</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lese og endre nettlesarinnstillingar</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Fjern nyleg nettlesingshistorikk, infokapslar og relaterte data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Hente data frå utklippstavla</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Setje inn data på utklippstavla</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokker innhald på alle sider</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Les nettlesarhistorikken din</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Laste ned filer og lese og endre nedlastingsloggen til nettlesaren</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Opne filer som er lasta ned til eininga di</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lese teksten i alle opne faner</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Sjå plasseringa di</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Tilgang til nettlesarhistorikken</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Overvake bruk av utvidingar og handsame tema</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Utveksle meldingar med andre appar enn denne</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Vise deg varsel</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Tilby kryptografiske godkjenningstenester</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontrollere proxy-innstillingar for nettlesar</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Tilgang til nyleg attletne faner</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Gøym og vis nettlesarfaner</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Tilgang til nettlesarhistorikken</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Utvide utviklarverktøy for å få tilgang til dine data i opne faner</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versjon</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Utviklar</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Utviklarar</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Sist oppdatert</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Heimeside</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Les meir om løyve</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Vurdering</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Meir om denne utvidinga</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Meir om denne utvidinga</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Innstillingar</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">På</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Av</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Tillat i privat nettlesing</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Køyr i privat nettlesing</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Ikkje tillate i private vindauge</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Påslått</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Avslått</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installert</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Tilrådd</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ikkje støtta enno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ikkje tilgjengeleg enno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Avslått</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detaljear</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Løyve</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Fjern</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Rapporter</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Leggje til %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ber om ytterlegare løyve.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Det krev løyve frå deg for å:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Den vil:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Legg til</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Tillat</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Avvis</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Avbryt</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installer tillegg</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installer %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Avbryt</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Omtalar: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Vurdering: %1$.02f av 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Tillegg</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Utvidingshandsamar</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg er mellombels deaktivert</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Utvidinga er mellombels deaktivert</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Eitt eller fleire tillegg slutta å verke, noko som gjorde systemet ditt ustabilt.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Start tillegga på nytt</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Start utvidingar på nytt</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Finn fleire tillegg</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Finn fleire utvidingar</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Tillat</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Avslå</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s har ei ny oppdatering</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nye løyve påkravd</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Eit nytt løyve er påkravd</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Tilleggsoppdateringear</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Utvidingsoppdateringar</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Støtta tilleggskontroll</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nytt tillegg tilgjengeleg</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nytt tillegg tilgjengeleg</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Legg til %1$s i %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Legg til %1$s og %2$s i %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Legg dei til i %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox-tilleggsteknologi vert modernisert. Desse tillegga brukar rammeverk som ikkje er kompatible med Firefox 75 og nyare.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Vi utviklar for tida støtte for eit første utval av tilrådde utvidingar.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Lastar ned og stadfestar tiillegget…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Mislykka førespurnad om tlleggsprogram!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Fann ikkje omsettinga for språket %2$s, eller for standardspråket %1$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s vart installert</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Klarte ikkje å installere %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Klarte ikkje å installere dette tillegget.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Klarte ikkje å laste ned tillegget på grunn av ein tilkoplingsfeil.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Klarte ikkje å installere dette tillegget fordi det ser ut til å vere skada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Klarte ikkje å installere dette tillegget fordi det ikkje er stadfesta.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Klarte ikkje å installere %1$s fordi den ikkje er kompatibel med %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Klarte ikkje å installere %1$s fordi det er stor risiko for at det kan lage stabilitets og sikkerheitsproblem.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s vart aktivert</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">klarte ikkje å aktivere %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s vart deaktivert</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Klarte ikkje å deaktivere %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s vart avinstallert</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Klarte ikkje å avinstallere %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s vart fjerna</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Klarte ikkje å fjerne %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Dette tillegget vart overført frå ein tidlegare versjon av %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 tillegg</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 utviding</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s tillegg</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s utvidingar</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Les meir</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Oppdatert</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Inga oppdatering tilgjengeleg</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Feil</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Oppdateringsinformasjon</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Siste forsøk:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s er lagt til i %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Opne det i menyen</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Få tilgang til %1$s frå %2$s-menyen.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, eg forstår</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Les meir</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s er slått av på grunn av sikkerheits- eller stabilitetsproblem.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Klarte ikkje å stadfeste %1$s som sikker, og er difor slått av.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s er ikkje kompatibel med din versjon av %2$s (versjon %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..6502e609c1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-oc/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Legir e modificar los paramètres de vida privada</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Accedir a vòstras donadas per totes los sites web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Accedir a vòstras donadas per %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Accedir a vòstras donadas pels sites del domeni %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Accedir a vòstras donadas per 1 autre site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Accedir a vòstras donadas per %1$d autres sites</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Accedir a vòstras donadas per 1 autre domeni</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Accedir a vòstras donadas per %1$d autres domenis</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Accedir als onglets del navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Servar una quantitat illimitada de donadas del costat del client</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Accedir a l’activitat del navegador pendent la navegacion</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Legir e modificar los marcapaginas</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Legir e modificar los paramètres del navegadors</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Escafar l’istoric recent, los cookies e las donadas ligadas</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obténer de donadas del quichapapièrs</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Apondre de donadas al quichapapièrs</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blocar lo contengut sus quina pagina que siá</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Legir l’istoric de navegacion</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Telecargar de fichièrs, consultar e modificar l’istoric dels telecargaments del navigator</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Dobrir los fichièrs telecargats sus vòstre aparelh</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Legir lo tèxte de totes los onglets dobèrts</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Accedir a vòstra localizacion</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Accedir a l’istoric de navigacion</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Susvelhar l’utilizacion de las extensions e administrar los tèmas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Escambiar de messatges amb d’autres programas</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Vos mostrar de notificacions</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Provesir de servicis d’autentificacion chifrats</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Contrarotlar los paramètres proxy del navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Accedir als onglets recentament tancats del navegador</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Amagar e mostrar los onglets del navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Accedir a l’istoric de navegacion</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Dobrir las aisinas de desvolopaments per dire d’accedir a vòstras donadas dins d’onglets dobèrts</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autors</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Darrièra mesa a jorn</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pagina d’acuèlh</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Per ne saber mai sus las permissions</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Apreciacion</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Mai de detalhs sus aqueste modul</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Mai d‘informacions sus aquesta extension</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Paramètres</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activat</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Desactivat</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Autorizar en navegacion privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Executar en navegacion privada</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Non autorizat en fenèstras privadas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activat</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Desactivat</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installats</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomandats</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Pas encara compatible</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Pas encara disponibla</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desactivadas</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalhs</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissions</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Levar</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Senhalar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Apondre %1$s ?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s demanda de permissions suplementàrias.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Cal vòstra permission per :</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Vòl :</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Apondre</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Autorizar</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Refusar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Anullar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installar lo modul</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Anullar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Vejaires : %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Nòta : %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Moduls complementaris</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gestionari de moduls complementaris</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Los moduls son temporàriament desactivats</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Las extensions son temporàriament desactivadas</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Un o mai de moduls complementaris an quitat de foncionar, fa venir vòstre sistèma instable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Uno o mai d’una extension an quitat de foncionar, fasent venir vòstre sistèma instable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reïnicializar los moduls</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Relançar las extensions</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Trobar mai de moduls</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Trobar mai d’extensions</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Autorizar</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Refusar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s a una novèla mesa a jorn</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nòvas permissions necessàrias</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Una nòva permission es necessària</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Mesas a jorn des moduls</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Mesas a jorn d’extensions</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Verificacion dels moduls complementaris preses en carga</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Modul novèl disponible</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Moduls novèls disponibles</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Apondre %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Apondre %1$s e %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Los apondre a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La tecnologia dels moduls complementaris de Firefox se moderniza. Aquestes moduls utilizan de frameworks que son pas mai compatibles amb Firefox 75 e las version ulterioras.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Sèm a trabalhar per ofrir una seleccion iniciala d’extensions recomandadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Telecargament e verificacion del modul…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Telecargament e verificacion de l’extension…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Obtencion de la lista dels moduls complementaris impossibla</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Obtencion de la lista de las extensions impossibla.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Traduccion pas trobada tan per la lenga %1$s coma la lenga per defaut %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s installat corrèctament</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Fracàs de l’installacion de %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Fracàs de l’installacion d’aqueste modul.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Fracàs de l’installacion d’aquesta extension.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Es pas estat possible de telecargar aqueste modul a causa d’un fracàs de la connexion.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Es pas estat possible de telecargar aquesta extension a causa d’un fracàs de la connexion.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Aqueste modul complementari a pas pogut èsser installat perque sembla qu’es corromput.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Aquesta extension a pas pogut èsser installada perque sembla qu\'es corrompuda.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Aqueste modul complementari a pas pogut èsser installat perque sembla qu’es corromput.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Aquesta extension a pas pogut èsser installada perque sembla qu\'es corrompuda.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Èra pas possible d’installar %1$s pr’amor qu’es pas compatible amb %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s a pas pogut èsser installat perque presenta un grand risc de problèmas d’instabilitat o de seguretat.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s activat corrèctament</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Fracàs de l’activacion de %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s desactivat corrèctament</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Fracàs de la desactivacion de %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s desinstallat corrèctament</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Fracàs de la desinstallacion de %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s suprimit corrèctament</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Fracàs de la supression de %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Aqueste modul complementari es estat migrat d’una version anteriora de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 modul complementari</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extension</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s moduls complementaris</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensions</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Ne saber mai</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Mesa a jorn realizada</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Cap de mesa a jorn pas disponibla</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informacions de mesa a jorn</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Darrièra temptativa :</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estat :</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s es estat ajustat a %2$s.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Dorbissètz-lo del menú estant</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Accedissètz a %1$s a partir del menú %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Òc, plan comprés</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">D\'acòrdi</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Ne saber mai</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s es estat desactivat en seguida de problèmas de seguretat o d’estabilitat.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Se podiá pas verificar que %1$s foguèsse segur e foguèt desactivat.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s es pas compatible amb vòstra version de %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..b910cf1191
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-or/strings.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">ଗୋପନୀୟତା ସଂରଚନା ପଢ଼ି ବଦଳାନ୍ତୁ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">ସମସ୍ତ ଖୋଲା ଟ୍ୟାବଗୁଡ଼ିକର ତଥ୍ୟ ପଢ଼ନ୍ତୁ</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ଆପଣଙ୍କର ଅବସ୍ଥାନକୁ ଅଭିଗମ୍ୟ କରନ୍ତୁ</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ବ୍ରାଉଜିଙ୍ଗ ଇତିହାସ ଅଭିଗମ୍ୟ କରନ୍ତୁ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ବ୍ରାଉଜର ପ୍ରକ୍ସି ସେଟିଙ୍ଗକୁ ନିୟନ୍ତ୍ରଣ କରନ୍ତୁ</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">ନିକଟରେ ବନ୍ଦ ହୋଇଥିବା ଟ୍ୟାବଗୁଡ଼ିକୁ ଅଭିଗମ୍ୟ କରନ୍ତୁ</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ଲୁଚାନ୍ତୁ ଏବଂ ବ୍ରାଉଜର ଟ୍ୟାବଗୁଡ଼ିକୁ ଦେଖାନ୍ତୁ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ବ୍ରାଉଜିଙ୍ଗ ଇତିହାସ ଅଭିଗମ୍ୟ କରନ୍ତୁ</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ସଂସ୍କରଣ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">ଲେଖକଗଣ</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">ଅନ୍ତିମ ଅଦ୍ୟତନ</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">ମୂଳ ପୃଷ୍ଠା</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">ରେଟିଙ୍ଗ୍</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ସେଟିଂସମୂହ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ଅନ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">ଅଫ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">ପ୍ରାଇଭେଟ ବ୍ରାଉଜିଂକୁ ଅନୁମତି ଦିଅନ୍ତୁ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">ବ୍ୟକ୍ତିଗତ ବ୍ରାଉଜିଂରେ ଚଲାନ୍ତୁ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ସକ୍ରିୟ ହେଲା</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">ଅକ୍ଷମ କରାଗଲା</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ସ୍ଥାପିତ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">ପରାମର୍ଶିତ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ଏବେ ସୁଦ୍ଧା ସମର୍ଥନ ନାହିଁ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ଏବେ ସୁଦ୍ଧା ଅନୁପଲବ୍ଧ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ନିଷ୍କ୍ରିୟ</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">ବିବରଣୀ</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ଅନୁମତିଗୁଡିକ</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">କାଢ଼ନ୍ତୁ</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s କୁ ଯୋଡ଼ିବେ?</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">ଯୋଡ଼ନ୍ତୁ</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ବାତିଲ କରନ୍ତୁ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">ଏଡ-ଅନକୁ ସ୍ଥାପନ କରନ୍ତୁ</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ବାତିଲ କରନ୍ତୁ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">ସମୀକ୍ଷା: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ଏଡ-ଅନଗୁଡ଼ିକ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ଏଡ-ଅନ ପରିଚାଳକ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ଅନୁମତି ଦିଅନ୍ତୁ</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ଅଗ୍ରାହ୍ୟ</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ପାଇଁ ଗୋଟିଏ ନୂଆ ଅଦ୍ୟତନ ଅଛି</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d ନୂଆ ଅନୁମତି ଆବଶ୍ୟକ</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ଏକ ନୂଆ ଅନୁମତି ଆବଶ୍ୟକ</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s କୁ ସଫଳତାର ସହିତ ଇନଷ୍ଟଲ କରାହେଲା</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s କୁ ଇଂସ୍ଟଲ କରିବାରେ ବିଫଳ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1ଆଡ-ଅନ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s ଆଡ-ଅନ ଗୁଡ଼ିକ</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ଅଧିକ ଶିଖନ୍ତୁ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">ସଫଳତାର ସହିତ ଅଦ୍ୟଟିତ ହେଲା</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">କୌଣସି ଅଦ୍ୟତନ ଉପଲବ୍ଧ ନାହିଁ</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ତ୍ରୁଟି</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ଶେଷ ଚେଷ୍ଟା</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">ସ୍ଥିତି:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s କୁ %2$s ରେ ଯୋଗ କରାଯାଇଛି</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">ମେନୁରେ ଖୋଲନ୍ତୁ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ଠିକ ଅଛି, ବୁଝିଲି</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..53775e6dc3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">ਪਰਦੇਦਾਰੀ ਸੈਟਿੰਗਾਂ ਨੂੰ ਪੜ੍ਹਨ ਤੇ ਸੋਧਣ</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ਸਾਰੀਆਂ ਵੈੱਬਸਾਈਟਾਂ ਲਈ ਤੁਹਾਡੇ ਡਾਟੇ ਵਾਸਤੇ ਪਹੁੰਚ</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s ਲਈ ਤੁਹਾਡੇ ਡਾਟੇ ਲਈ ਪਹੁੰਚ</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s ਡੋਮੇਨ ਵਿੱਚ ਸਾਈਟਾਂ ਲਈ ਤੁਹਾਡੇ ਵਾਸਤੇ ਪਹੁੰਚ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">1 ਹੋਰ ਸਾਈਟ ਉੱਤੇ ਤੁਹਾਡੇ ਡਾਟੇ ਵਾਸਤੇ ਪਹੁੰਚ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">%1$d ਹੋਰ ਸਾਈਟਾਂ ਉੱਤੇ ਤੁਹਾਡੇ ਡਾਟੇ ਵਾਸਤੇ ਪਹੁੰਚ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">1 ਹੋਰ ਡੋਮੇਨ ਉੱਤੇ ਤੁਹਾਡੇ ਡਾਟੇ ਲਈ ਪਹੁੰਚ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">%1$d ਹੋਰ ਡੋਮੇਨ ਉੱਤੇ ਤੁਹਾਡੇ ਡਾਟੇ ਲਈ ਪਹੁੰਚ</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%3$d ਵਿੱਚੋਂ %1$s, %2$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ਬਰਾਊਜ਼ਰ ਟੈਬਾਂ ਲਈ ਪਹੁੰਚ</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ਬੇਅੰਤ ਕਲਾਈਂਟ ਪੱਖੀ ਡਾਟਾ ਨੂੰ ਸੰਭਾਲੋ</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">ਨੇਵੀਗੇਸ਼ਨ ਦੌਰਾਨ ਬਰਾਊਜ਼ਰ ਸਰਗਰਮੀ ਲਈ ਪਹੁੰਚ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">ਬੁੱਕਮਾਰਕ ਪੜਨ ਅਤੇ ਸੋਧਣ</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ਬਰਾਊਜ਼ਰ ਸੈਟਿੰਗਾਂ ਪੜ੍ਹਨ ਤੇ ਸੋਧਣ</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">ਤਾਜ਼ਾ ਬਰਾਊਜ਼ ਕਰਨ ਦਾ ਅਤੀਤ, ਕੂਕੀਜ਼ ਅਤੇ ਸੰਬੰਧਿਤ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ਕਲਿੱਪਬੋਰਡ ਤੋਂ ਡਾਟਾ ਲੈਣ</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ਕਲਿੱਪਬੋਰਡ ਵਿੱਚ ਡਾਟਾ ਇਨਪੁਟ ਕਰੋ</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">ਕਿਸੇ ਵੀ ਸਫ਼ੇ ਤੋਂ ਸਮੱਗਰੀ ਉੱਤੇ ਪਾਬੰਦੀ ਲਾਓ</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">ਤੁਹਾਡੇ ਬਰਾਊਜ਼ ਕਰਨ ਦੇ ਅਤੀਤ ਨੂੰ ਪੜ੍ਹਨ</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ਫ਼ਾਈਲਾਂ ਡਾਊਨਲੋਡ ਕਰਨ ਅਤੇ ਬਰਾਊਜ਼ਰ ਦੇ ਡਾਊਨਲੋਡ ਅਤੀਤ ਨੂੰ ਪੜ੍ਹਨ ਅਤੇ ਸੋਧਣ</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ਆਪਣੇ ਡਿਵਾਈਸ ਉੱਤੇ ਡਾਊਨਲੋਡ ਕੀਤੀਆਂ ਫਾਈਲਾਂ ਖੋਲ੍ਹਣ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">ਸਾਰੀਆਂ ਖੁੱਲ੍ਹੀਆਂ ਟੈਬਾਂ ਦੀ ਲਿਖਤ ਪੜਨ</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ਤੁਹਾਡੇ ਟਿਕਾਣੇ ਲਈ ਪਹੁੰਚ</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ਬਰਾਊਜ਼ਰ ਕਰਨ ਦੇ ਅਤੀਤ ਲਈ ਪਹੁੰਚ</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">ਇਕਸਟੈਨਸ਼ਨ ਵਰਤੋ ਦੀ ਨਿਗਰਾਨੀ ਅਤੇ ਥੀਮਾਂ ਦਾ ਇੰਤਜ਼ਾਮ</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ਹੋਰ ਐਪਾਂ ਵਲੋਂ ਇਸ ਨਾਲ ਤਬਾਦਲਾ ਕੀਤੇ ਸੁਨੇੇਹੇ</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">ਤੁਹਾਡੇ ਲਈ ਦਿਖਾਉਣ ਲਈ ਨੋਟੀਫਿਕੇਸ਼ਨ</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ਕ੍ਰਿਪਟੋਗਰਾਫ਼ਿਕ ਪਰਮਾਣਕਿਤਾ ਸੇਵਾਵਾਂ ਦੇਣ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ਕੰਟਰੋਲਰ ਬਰਾਊਜ਼ਰ ਪਰਾਕਸੀ ਸੈਟਿੰਗਾਂ</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">ਤਾਜ਼ਾ ਬੰਦ ਕੀਤੀਆਂ ਟੈਬਾਂ ਲਈ ਪਹੁੰਚ</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ਬਰਾਊਜ਼ਰ ਟੈਬਾਂ ਲੁਕਾਉਣ ਅਤੇ ਵੇਖਾਉਣ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ਬਰਾਊਜ਼ਰ ਅਤੀਤ ਲਈ ਪਹੁੰਚ</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ਟੈਬਾਂ ਖੋਲ੍ਹਣ ਵਿੱਚ ਤੁਹਾਡੇ ਡਾਟਾ ਲਈ ਡਿਵੈਲਪਰ ਟੂਲਾਂ ਦੀ ਪਹੁੰਚ</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ਵਰਜ਼ਨ</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">ਲੇਖਕ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">ਲੇਖਕ</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">ਆਖਰੀ ਅੱਪਡੇਟ</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">ਮੁੱਖ-ਸਫ਼ਾ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ਇਜਾਜ਼ਤਾਂ ਬਾਰੇ ਹੋਰ ਸਿੱਖੋ</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">ਦਰਜਾਬੰਦੀ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">ਇਸ ਐਡ-ਆਨ ਬਾਰੇ ਹੋਰ</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">ਇਸ ਇਕਸਟੈਨਸ਼ਨ ਬਾਰੇ ਹੋਰ</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ਸੈਟਿੰਗਾਂ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ਚਾਲੂ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">ਬੰਦ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">ਨਿੱਜੀ ਬਰਾਊਜ਼ਿੰਗ ਵਿੱਚ ਮਨਜ਼ੂਰ ਕਰੋ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">ਨਿੱਜੀ ਬਰਾਊਜ਼ਿੰਗ ਚ ਚਲਾਓ</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">ਪ੍ਰਾਈਵੇਟ ਵਿੰਡੋਆਂ ਵਿੱਚ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ਸਮਰੱਥ ਹੈ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">ਅਸਮਰੱਥ ਹੈ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ਇੰਸਟਾਲ ਹੈ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">ਸਿਫਾਰਸ਼ੀ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ਹਾਲੇ ਸਹਾਇਕ ਨਹੀਂ ਹੈ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ਹਾਲੇ ਉਪਲਬਧ ਨਹੀਂ ਹੈ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ਅਸਮਰੱਥ ਹੈ</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">ਵੇਰਵੇ</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ਇਜਾਜ਼ਤਾਂ</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">ਹਟਾਓ</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">ਰਿਪੋਰਟ</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s ਨੂੰ ਜੋੜਨਾ ਹੈ?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ਨੂੰ ਵਧੀਕ ਇਜਾਜ਼ਤਾਂ ਦੀ ਲੋੜ ਹੈ।</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">ਇਸ ਨੂੰ ਤੁਹਾਡੀ ਇਜਾਜ਼ਤ ਚਾਹੀਦੀ ਹੈ:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ਇਹ ਚਾਹੁੰਦਾ ਹੈ:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">ਜੋੜੋ</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">ਮਨਜ਼ੂਰ</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">ਇਨਕਾਰ</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ਰੱਦ ਕਰੋ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">ਐਡ-ਆਨ ਇੰਸਟਾਲ ਕਰੋ</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s ਨੂੰ ਇੰਸਟਾਲ ਕਰੋ</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ਰੱਦ ਕਰੋ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">ਰੀਵਿਊ: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">ਰੇਟਿੰਗ: 5 ਵਿੱਚੋਂ %1$.02f</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ਐਡ-ਆਨ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ਐਡ-ਆਨ ਮੈਨੇਜਰ</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ ਆਰਜ਼ੀ ਤੌਰ ਉੱਤੇ ਅਸਮਰੱਥ ਹਨ</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">ਇਕਸਟੈਨਸ਼ਨਾਂ ਆਰਜ਼ੀ ਤੌਰ ਉੱਤੇ ਅਸਮਰੱਥ ਹਨ</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">ਇੱਕ ਜਾਂ ਜ਼ਿਆਦਾ ਐਡ-ਆਨ ਦੇ ਕੰਮ ਕਰਨਾ ਬੰਦ ਕਰਨ ਨਾਲ ਤੁਹਾਡਾ ਸਿਸਟਮ ਅਸਥਿਰ ਕੀਤਾ ਹੈ।</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">ਇੱਕ ਜਾਂ ਵੱਧ ਇਕਸਟੈਨਸ਼ਨਾਂ ਨੇ ਕੰਮ ਕਰਨਾ ਬੰਦ ਕਰ ਦਿੱਤਾ ਹੈ, ਜਿਸ ਨਾਲ ਤੁਹਾਡਾ ਸਿਸਟਮ ਅਸਥਿਰ ਹੋ ਗਿਆ ਹੈ।</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ ਮੁੜ-ਚਾਲੂ ਕਰੋ</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">ਇਕਸਟੈਨਸ਼ਨਾਂ ਨੂੰ ਮੁੜ-ਚਾਲੂ ਕਰੋ</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">ਹੋਰ ਐਡ-ਆਨ ਲੱਭੋ</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">ਹੋਰ ਇਕਸਟੈਨਸ਼ਨਾਂ ਲੱਭੋ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ਮਨਜ਼ੂਰ</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ਇਨਕਾਰ</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ਲਈ ਨਵਾਂ ਅੱਪਡੇਟ ਹੈ</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d ਨੂੰ ਨਵੀਆਂ ਇਜਾਜ਼ਤਾਂ ਦੀ ਲੋੜ ਹੈ</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ਨਵੀ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ ਅੱਪਡੇਟ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">ਇਕਸਟੈਨਸ਼ਨ ਅੱਪਡੇਟ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">ਸਹਾਇਕ ਐਡ-ਆਨ ਚੈਕਰ</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">ਨਵੀਂ ਐਡ-ਆਨ ਉਪਲੱਬਧ</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">ਨਵੀਆਂ ਐਡ-ਆਨ ਉਪਲਬਧ</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s ਨੂੰ %2$s ਚ ਜੋੜੋ</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s ਅਤੇ %2$s ਨੂੰ %3$s ਵਿੱਚ ਜੋੜੋ</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">ਉਹਨਾਂ ਨੂੰ %1$s ਵਿੱਚ ਜੋੜੋ</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox ਐਡ-ਆਨ ਤਕਨੀਕ ਦਾ ਨਵੀਨੀਕਰਨ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ। ਇਹ ਐਡ-ਆਨ ਅਜਿਹਾ ਢਾਂਚਾ ਵਰਤਦੀਆਂ ਹਨ, ਜੋ ਕਿ Firefox 75 ਅਤੇ ਇਸ ਤੋੰ ਨਵਿਆਂ ਨਾਲ ਮੁਆਫ਼ਕ ਨਹੀਂ ਹੋਵੇਗਾ।</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ਅਸੀਂ ਇਸ ਵੇਲੇ ਸਿਫਾਰਸ਼ੀ ਇਕਸਟੈਨਸ਼ਨਾਂ ਦੀ ਸ਼ੁਰੂਆਤੀ ਚੋਣ ਲਈ ਸਹਾਇਤਾ ਤਿਆਰ ਕਰ ਰਹੇ ਹਾਂ।</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">…ਐਡ-ਆਨ ਡਾਊਨਲੋਡ ਅਤੇ ਤਸਦੀਕ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">…ਇਕਸਟੈਨਸ਼ਨ ਨੂੰ ਡਾਊਨਲੋਡ ਅਤੇ ਤਸਦੀਕ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ ਕਿਊਰੀ ਅਸਫ਼ਲ ਹੋਈ!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">ਇਕਸਟੈਨਸ਼ਨਾਂ ਬਾਰੇ ਜਾਣਕਾਰੀ ਲੈਣ ਲਈ ਫੇਲ੍ਹ!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">ਉਲੱਥਾ ਨਹੀਂ ਲੱਭਿਆ, ਨਾ ਹੀ ਲੋਕੇਲ %1$s ਲਈ, ਨਾ ਹੀ ਮੂਲ ਭਾਸ਼ਾ %2$s ਲਈ</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s ਕਾਮਯਾਬੀ ਨਾਲ ਇੰਸਟਾਲ ਕੀਤੀ</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ਨੂੰ ਇੰਸਟਾਲ ਕਰਨ ਵਿੱਚ ਅਸਫ਼ਲ</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">ਇਹ ਐਡ-ਆਨ ਇੰਸਟਾਲ ਕਰਨ ਲਈ ਅਸਫ਼ਲ ਹੈ।</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">ਇਸ ਇਕਸਟੈਨਸ਼ਨ ਨੂੰ ਇੰਸਟਾਲ ਕਰਨ ਲਈ ਫੇਲ੍ਹ ਹੈ।</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">ਕਨੈਕਸ਼ਨ ਅਸਫ਼ਲ ਹੋਣ ਕਰਕੇ ਇਸ ਐਡ-ਆਨ ਨੂੰ ਡਾਊਨਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">ਇਸ ਇਕਸਟੈਨਸ਼ਨ ਨੂੰ ਕਨੈਕਸ਼ਨ ਅਸਫ਼ਲ ਰਹਿਣ ਕਰਕੇ ਡਾਊਨਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">ਇਸ ਐਡ-ਆਨ ਨੂੰ ਇੰਸਟਾਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ ਕਿਉਂਕਿ ਇਹ ਨਿਕਾਰਾ ਜਾਪਦੀ ਹੈ।</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">ਇਹ ਇਕਸਟੈਨਸ਼ਨ ਨਿਕਾਰਾ ਹੋਣ ਕਰਕੇ ਇੰਸਟਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ ਹੈ।</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">ਇਸ ਐਡ-ਆਨ ਨੂੰ ਇੰਸਟਾਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ ਕਿਉਂਕਿ ਇਸ ਦੀ ਤਸਦੀਕ ਨਹੀਂ ਹੋ ਸਕੀ ਹੈ।</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">ਇਸ ਇਕਸਟੈਨਸ਼ਨ ਦੀ ਤਸਦੀਕ ਨਹੀਂ ਹੋ ਸਕਣ ਕਰਕੇ ਇਸ ਨੂੰ ਇੰਸਟਾਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%2$s %3$s ਨਾਲ ਮੁਆਫ਼ਕ ਨਾ ਹੋਣ ਕਰਕੇ %1$s ਨੂੰ ਇੰਸਟਾਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">ਸਥਿਰਤਾ ਜਾਂ ਸੁਰੱਖਿਆ ਸਮੱਸਿਆਵਾਂ ਖੜ੍ਹੀਆਂ ਕਰਨ ਦੇ ਖ਼ਤਰੇ ਕਰਕੇ %1$s ਨੂੰ ਇੰਸਟਾਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ਕਾਮਯਾਬੀ ਨਾਲ ਸਮਰੱਥ ਕੀਤੀ</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s ਨੂੰ ਸਮਰੱਥ ਕਰਨ ਵਿੱਚ ਅਸਫ਼ਲ</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s ਕਾਮਯਾਬੀ ਨਾਲ ਅਸਮਰੱਥ ਕੀਤੀ</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s ਅਸਮਰੱਥ ਕਰਨ ਲਈ ਅਸਫ਼ਲ</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s ਕਾਮਯਾਬੀ ਨਾਲ ਅਣ-ਇੰਸਟਾਲ ਕੀਤੀ</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s ਅਣ-ਇੰਸਟਾਲ ਕਰਨ ਲਈ ਅਸਫ਼ਲ</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s ਕਾਮਯਾਬੀ ਨਾਲ ਹਟਾਈ</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ਨੂੰ ਹਟਾਉਣ ਵਿੱਚ ਅਸਫਲ ਹੋਏ</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ਇਹ ਐਡ-ਆਨ ਨੂੰ %1$s ਦੇ ਪੁਰਾਣੇ ਵਰਜ਼ਨ ਤੋਂ ਲਿਆਂਦਾ ਗਿਆ ਸੀ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 ਐਡ-ਆਨ</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 ਇਕਸਟੈਨਸ਼ਨ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s ਐਡ-ਆਨ</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s ਇਕਸਟੈਨਸ਼ਨਾਂ</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ਹੋਰ ਜਾਣੋ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">ਕਾਮਯਾਬੀ ਨਾਲ ਅੱਪਡੇਟ ਕੀਤਾ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">ਕੋਈ ਅੱਪਡੇਟ ਮੌਜੂਦ ਨਹੀਂ ਹੈ</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ਗਲਤੀ</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">ਅੱਪਡੇਟਰ ਜਾਣਕਾਰੀ</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ਆਖਰੀ ਕੋਸ਼ਿਸ਼:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">ਹਾਲਤ:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ਨੂੰ %2$s ਵਿੱਚ ਜੋੜਿਆ ਗਿਆ ਹੈ</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">ਇਸਨੂੰ ਮੇਨੂ ਵਿੱਚ ਖੋਲ੍ਹੋ</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">%2$s ਮੇਨੂ ਵਿੱਚੋਂ %1$s ਪਹੁੰਚ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">ਠੀਕ ਹੈ, ਸਮਝ ਗਏ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">ਠੀਕ ਹੈ</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">ਹੋਰ ਜਾਣੋ</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ਨੂੰ ਸੁਰੱਖਿਆ ਜਾਂ ਸਥਿਰਤਾ ਮਸਲਿਆਂ ਕਰਕੇ ਅਸਮਰੱਥ ਕੀਤਾ ਗਿਆ ਹੈ।</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s ਦੇ ਸੁਰੱਖਿਅਤ ਹੋਣ ਨੂੰ ਤਸਦੀਕ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ ਅਤੇ ਅਸਮਰੱਥ ਕੀਤਾ ਗਿਆ ਹੈ।</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s %2$s ਦੇ ਤੁਹਾਡੇ ਵਰਜ਼ਨ (ਵਰਜ਼ਨ %3$s) ਦੇ ਮੁਆਫ਼ਕ ਨਹੀਂ ਹੈ।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..ff2f594300
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">پردیداری سیٹنگاں نوں پڑھن تے بدلݨ</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">سریاں سائٹاں لئی تہاڈے ڈیٹے واسطے پہنچ</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s سائٹ لئی تہاڈے ڈیٹے لئی پہنچ</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s دے وچ ڈیٹے نوں پہنچ جاؤ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">1 ہورناں سائٹ دے وچ ڈیٹے نوں پہنچ جاؤ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">%1$d ہورناں سائٹاں دے وچ ڈیٹے نوں پہنچ جاؤ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">1 ہورناں ڈومین دے وچ ڈیٹے نوں پہنچ جاؤ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">%1$d ہورناں ڈومیناں دے وچ ڈیٹے نوں پہنچ جاݨا</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ایپ دیاں ٹیباں لئی پہنچݨا</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">بے حد دا ڈیٹا رکھݨا</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">ورتدے دوران ایپ دی سرگرمی نوں پہنچݨا</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">اتے پتے پڑھن تے بدلݨ</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ایپ سیٹنگاں پڑھن تے بدلݨ</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">حالیہ ورتوں دی تاریخ، کوکیاں تے متعلقہ ڈیٹا صاف کرو</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">کاپی پیسٹ کرن توں ڈیٹا لیݨا</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">کاپی پیسٹ کرن لئی ڈیٹا پاوݨا</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">کسے وی صفحے توں سمگری تے روک لاوݨا</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">تہاڈی تریخ نوں پڑھن</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">فائلاں ڈاؤں‌لوڈ کرنا، تے اوہدی تاریخ نوں پڑھنا تے بدلݨا</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ڈاؤں‌لوڈ کیتیاں فائلاں کھولھݨا</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">ساریاں کھولھیاں ٹیباں دیاں وچ لکھتاں نوں پڑھنا</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">تہاڈی ستھتی نوں پہنچ جاݨا</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ورتوں دی تاریخ نوں پہنچ جاݨا</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">پھیلاؤ دی ورتوں نوں وکھݨا تے رنگ ڈھنگ انتظام کرنا</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ہورناں ایپاں نوں سنیہے بھیجݨے</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">تینوں اطلاع ویکھݨے</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">تصدیق دیاں سیواواں دیݨیاں</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">پراکسی سیٹنگاں نوں بدلݨا</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">حالیہ بند کیتیاں ٹیباں نوں پہنچ جاݨا</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ٹیباں لکاوݨا تے ویکھاوݨا</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ورتوں دی تاریخ نوں پہنچ جاݨا</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">کھولھویاں ٹیباں چ تہاڈے ڈیٹے پہنچݨ لئی سنداں توں ودھݨا</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ورژن</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">لکھاری</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">لکھاریاں</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">آخری بدلیا</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">مکھ صفحہ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">اجازتاں بارے ہور سکھو</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">درجہ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">ایس وادھے والے دے ہور وروے</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">سیٹنگاں</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">چالو</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">بند</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">نجی ڈھنگ چ اجازتب دیو</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">نجی ورتوں چ چالو ہو گیا</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">چالو اے</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">بند ہو گیا اے</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">اینسٹال کیتی اے</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">سفارشی</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">حالے سہارا نہیں اے</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">حالے اپلبدھ نہیں اے</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">بند ہو گیا اے</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">ویروے</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">اجازتاں</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">ہٹاؤ</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">ریپورٹ کرو</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s رکھ لیو؟</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s نوں ودھیک اجازتاں دی لوڑ اے۔</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">ایس لئی اجازت دی لوڑ اے –</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ایہہ چاہندا اے:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">پتہ پایو</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">اجازت دیو</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">اجازت نہ دیو</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">رد کرو</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">وادھے والا اینسٹال کرو</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">رد کرو</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">پڑتالاں – %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">وادھیاں والے</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">وادھیاں والیاں دیاں سیٹنگاں</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">وادھے والے عارضی طور تے چالو نہیں ہن</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">اک جاں زیادہ وادھے والے دے کم کرنا بند کرن نال تہاڈا سسٹم قائم نہیں کیتا اے۔</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">وادھے والے مڑ چالو کرو</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">ہور وادھے والے لبھو</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">اجازت دیو</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">اجازت نہ دیو</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s سند توں نویں رچنا چڑھی گئی اے</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d نویں اجازتاں دی لوڑ اے</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">نویں اجازت دی لوڑ اے</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">وادھے والیاں توں نویاں رچناواں</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">وادھے والے سہارا</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">نواں وادھے والا دستیاب اے</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">نویں وادھے والے دستیاب اے</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s نوں %1$s پا لاؤ</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s نوں %1$s تے %2$s پا لاؤ</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">%1$s نوں اوہ پا لاؤ</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">فائرفاکس وادھیاں دا نواں کیتا جا رہا اے۔ فائرفاکس ۷۵ تے اوتھوں نویاں نال ایہہ معافی نہیں ہوۓگا۔</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">اسیں ایس ویلے سفارشی وادھیاں دی پہلی چوݨ لئی حمایت تیار کر رہے آں۔</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">وادھے والا ڈاؤن‌لوڈ تے تصدیق کیتی جا رہا اے…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">تلاش اسپھل ہوئی!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">ترجمہ نہیں لبھیا، نہ ہی %1$s لئی، نہ ہی مول بولی %2$s لئی</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s ایپ انسٹال کیتی گئی اے</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ایپ انسٹال کر نہیں سکی</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">ایہہ وادھے والا انسٹال کر نہیں سکیا۔</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">کنیکشن اسپھل ہوݨ کرکے ایس وادھے والے نوں ڈاؤن‌لوڈ نہیں کیتا جا سکیا۔</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">ایس وادھے والے نوں انسٹال نہیں کیتا جا سکیا کیوں‌کہ ایہہ نکارہ جاپدی اے۔</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">ایس وادھے والے نوں انسٹال نہیں کیتا جا سکیا کیوں‌کہ ایس دی تصدیق نہیں ہو سکی اے۔</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%2$s %3$s نال معافق نہ ہوݨ کرکے %1$s نوں انسٹال نہیں کیتا جا سکیا۔</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">ستھرتا جاں سرکھیا سمسیاواں کھڑھیاں کرن دے خطرے کرکے %1$s نوں انسٹال نہیں کیتا جا سکیا۔</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ایپ نوں چالو لا گیا</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s ایپ نوں چالو کر نہیں سکیا گیا</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s نوں چالو الٹایا گیا</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s نوں چالو الٹا نہیں سکیا</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s ایپ اݨ‌انسٹال کیتی گئی</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s ایپ اݨ‌انسٹال کر نہیں سکی</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s ہٹایا گیا</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ہٹا نہیں سکیا</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">%1$s دے پراݨے ورژن توں ایہہ منگ گیا سی</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">۱ وادھے والا</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s وادھے والے</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ہور جاݨو</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">ہݨے نواں کیتا گیا</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">حالیہ کوئی نواں ورژن نہیں اے</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">غلطی</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">نویں کرن والے دی جاݨکاری</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">آخری کوشش:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">ستتھی –</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%2$s نوں %1$s پا لایا</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">نویں مینو چ کھولھو</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ٹھیک اے، سمجھ گئے</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">ہور جاݨو</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s نوں سرکھیا جاں ستھرتا مسئلیاں کرکے اسمرتھ کیتا گیا اے۔</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s دے سرکھیت ہوݨ نوں تصدیق نہیں کیتا جا سکیا تے اسمرتھ کیتا گیا اے۔</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s %2$s دے تہاڈے ورژن (ورژن %3$s) دے معافق نہیں اے۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..c54de1f492
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-pl/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Odczytywanie i zmienianie ustawień prywatności</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Dostęp do danych użytkownika na wszystkich stronach</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Dostęp do danych użytkownika na stronie „%1$s”</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Dostęp do danych użytkownika w domenie „%1$ss”</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Dostęp do danych użytkownika na 1 innej stronie</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Dostęp do danych użytkownika na %1$d innych stronach</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Dostęp do danych użytkownika w 1 innej domenie</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Dostęp do danych użytkownika w %1$d innych domenach</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d z %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Dostęp do kart przeglądarki</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Przechowywanie nieograniczonej ilości danych na urządzeniu</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Dostęp do aktywności przeglądarki podczas nawigacji</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Odczytywanie i zmienianie zakładek</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Odczytywanie i zmienianie ustawień przeglądarki</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Usuwanie ostatniej historii przeglądania, ciasteczek i powiązanych danych</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Odczytywanie danych ze schowka</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Dodawanie danych do schowka</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokowanie treści na dowolnej stronie</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Odczytywanie historii przeglądania</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Pobieranie plików i odczytywanie oraz zmienianie historii pobieranych plików</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Otwieranie plików pobranych na urządzenie</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Odczytywanie tekstów ze wszystkich otwartych kart</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Dostęp do informacji o położeniu</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Dostęp do historii przeglądania</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorowanie wykorzystania rozszerzeń i zarządzanie motywami</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Wymienianie wiadomości z aplikacjami innymi niż ta</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Wyświetlanie powiadomień</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Dostarczanie kryptograficznych usług uwierzytelniania</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontrolowanie ustawień proxy przeglądarki</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Dostęp do ostatnio zamkniętych kart</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ukrywanie i wyświetlanie kart przeglądarki</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Dostęp do historii przeglądania</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Dostęp do danych użytkownika i otwartych kart poprzez rozszerzone narzędzia programistyczne</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Wersja</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autorzy</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ostatnia aktualizacja</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Strona domowa</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Więcej informacji o uprawnieniach</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Ocena</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Więcej o tym dodatku</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Więcej o tym rozszerzeniu</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ustawienia</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Włączony</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Wyłączony</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Zezwalaj w trybie prywatnym</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Zezwalaj w trybie prywatnym</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Niedozwolone w oknach prywatnych</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Włączony</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Wyłączony</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Zainstalowane</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Polecane</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Jeszcze nieobsługiwane</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Jeszcze niedostępne</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Wyłączone</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Informacje</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Uprawnienia</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Usuń</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Zgłoś</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Czy dodać „%1$s”?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">Dodatek „%1$s” prosi o nowe uprawnienia.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ten dodatek wymaga następujących uprawnień:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Ten dodatek prosi o nadanie następujących uprawnień:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Dodaj</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Zezwól</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Odmów</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Anuluj</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Zainstaluj dodatek</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Zainstaluj dodatek „%1$s”</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Anuluj</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recenzje: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Ocena: %1$.02f z 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Dodatki</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Zarządzaj dodatkami</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki zostały tymczasowo wyłączone</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Rozszerzenia zostały tymczasowo wyłączone</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Co najmniej jeden dodatek przestał działać, przez co system stał się niestabilny.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Co najmniej jedno rozszerzenie przestało działać, przez co system stał się niestabilny.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Włącz dodatki z powrotem</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Włącz rozszerzenia z powrotem</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Znajdź więcej dodatków</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Znajdź więcej rozszerzeń</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Zezwól</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Odmów</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ma nową aktualizację</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Wymagane są nowe uprawnienia (%1$d)</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Wymagane jest nowe uprawnienie</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Aktualizacje dodatków</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Aktualizacje rozszerzeń</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Wyszukiwanie obsługiwanych dodatków</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Dostępny jest nowy dodatek</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Dostępne są nowe dodatki</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Dodaj „%1$s” do programu %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Dodaj „%1$s” i „%2$s” do programu %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Dodaj je do programu %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Technologia dodatków do Firefoksa jest unowocześniana. Te dodatki używają mechanizmów niezgodnych z Firefoksem 75 i nowszym.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Obecnie pracujemy nad obsługą pierwszej grupy polecanych rozszerzeń.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Pobieranie i weryfikowanie dodatku…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Pobieranie i weryfikowanie rozszerzenia…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Pobranie listy dodatków się nie powiodło</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Pobranie listy rozszerzeń się nie powiodło</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Nie odnaleziono tłumaczenia dla używanego języka (%1$s) ani domyślnego języka (%2$s)</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Zainstalowano dodatek „%1$s”</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Instalacja dodatku „%1$s” się nie powiodła</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Instalacja tego dodatku się nie powiodła.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Instalacja tego rozszerzenia się nie powiodła.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Nie udało się zainstalować tego dodatku z powodu błędu połączenia.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Nie udało się zainstalować tego rozszerzenia z powodu błędu połączenia.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Ten dodatek nie może zostać zainstalowany, ponieważ wygląda on na uszkodzony.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">To rozszerzenie nie może zostać zainstalowane, ponieważ wygląda ono na uszkodzone.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Ten dodatek nie może zostać zainstalowany, ponieważ nie został zweryfikowany.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">To rozszerzenie nie może zostać zainstalowane, ponieważ nie zostało zweryfikowane.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Dodatek „%1$s” nie może zostać zainstalowany, ponieważ nie jest on zgodny z aplikacją %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Dodatek „%1$s” nie może zostać zainstalowany, ponieważ obarczony jest on wysokim ryzykiem utraty stabilności lub problemów z bezpieczeństwem.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Włączono dodatek „%1$s”</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Włączenie dodatku „%1$s” się nie powiodło</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Wyłączono dodatek „%1$s”</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Wyłączenie dodatku „%1$s” się nie powiodło</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Odinstalowano dodatek „%1$s”</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Odinstalowanie dodatku „%1$s” się nie powiodło</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Usunięto dodatek „%1$s”</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Usunięcie dodatku „%1$s” się nie powiodło</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ten dodatek został przeniesiony z poprzedniej wersji programu %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki: 1</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">Rozszerzenia: 1</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki: %1$s</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">Rozszerzenia: %1$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Więcej informacji</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Pomyślnie zaktualizowano</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Brak dostępnej aktualizacji</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Błąd</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informacje o aktualizacji</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ostatnia próba:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stan:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Dodano „%1$s” do programu %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Otwórz w menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Otwórz dodatek „%1$s” z menu programu %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Więcej informacji</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Dodatek „%1$s” został wyłączony z powodu problemów z bezpieczeństwem lub stabilnością.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Dodatek „%1$s” nie mógł zostać zweryfikowany jako bezpieczny i został wyłączony.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Dodatek „%1$s” nie jest zgodny z używaną wersją aplikacji %2$s (wersja %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..93426cff28
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Ler e modificar configurações de privacidade</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acessar seus dados em todos os sites</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acessar seus dados em %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acessar seus dados em sites no domínio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acessar seus dados em 1 outro site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acessar seus dados em %1$d outros sites</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acessar seus dados em 1 outro domínio</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acessar seus dados em %1$d outros domínios</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acessar abas do navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Armazenar uma quantidade ilimitada de dados no dispositivo</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acessar a atividade do navegador durante a navegação</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Ler e modificar favoritos</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Ler e modificar configurações do navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpar histórico de navegação recente, cookies e dados relacionados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obter dados da área de transferência</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Enviar dados para área de transferência</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquear conteúdo em qualquer página</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Ler seu histórico de navegação</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Baixar arquivos, ler e modificar o histórico de downloads do navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir arquivos baixados no seu dispositivo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Ler o texto de todas as abas abertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acessar sua localização</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acessar o histórico de navegação</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorar o uso de extensões e gerenciar temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Trocar mensagens com outros aplicativos além deste</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Exibir notificações para você</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Fornecer serviços de autenticação com criptografia</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar configurações de proxy do navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acessar abas fechadas recentemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ocultar e exibir abas do navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acessar o histórico de navegação</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Estender as ferramentas do desenvolvedor para acessar seus dados nas abas abertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versão</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Última atualização</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Página da extensão</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saiba mais sobre permissões</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Avaliação</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Mais detalhes sobre esta extensão</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Mais detalhes sobre esta extensão</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Configurações</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ativado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Desativado</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir na navegação privativa</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Ativar na navegação privativa</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Não permitido em janelas privativas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ativado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Desativado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ainda não suportado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Não disponível ainda</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desativado</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalhes</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissões</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Remover</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Denunciar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Adicionar %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s requer permissões adicionais.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Requer sua permissão para:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">A extensão quer:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Adicionar</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitir</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Negar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instalar extensão</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instalar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Avaliações: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Avaliação: %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Extensões</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gerenciador de extensões</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Extensões estão temporariamente desativadas</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Extensões estão temporariamente desativadas</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Uma ou mais extensões pararam de funcionar, tornando o sistema instável.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Uma ou mais extensões pararam de funcionar, tornando seu sistema instável.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reiniciar extensões</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reiniciar extensões</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Encontrar mais extensões</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Encontrar mais extensões</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Negar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tem uma nova atualização</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">São necessárias %1$d novas permissões</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">É necessária uma nova permissão</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Atualizações da extensão</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Atualizações de extensões</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Verificador de extensões suportadas</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nova extensão disponível</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Novas extensões disponíveis</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Adicionar %1$s ao %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Adicionar %1$s e %2$s ao %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Adicionar ao %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">A tecnologia de extensões do Firefox está se modernizando. Essas extensões usam frameworks que não são compatíveis com o Firefox 75 em diante.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">No momento, estamos consolidando o suporte a uma seleção inicial de extensões recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Baixando e verificando a extensão…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Baixando e verificando a extensão…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Falha ao consultar extensões!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Falha ao consultar extensões!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Tradução não encontrada, nem para o idioma local %1$s nem para o idioma padrão %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instalado com sucesso</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Falha ao instalar %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Falha ao instalar esta extensão.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Falha ao instalar esta extensão.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Esta extensão não pôde ser baixada devido a uma falha de conexão.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Esta extensão não pôde ser baixada devido a uma falha de conexão.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Esta extensão não pôde ser instalada porque parece estar corrompida.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Esta extensão não pôde ser instalada porque parece estar corrompida.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Esta extensão não pôde ser instalada porque não foi verificada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Esta extensão não pôde ser instalada porque não foi verificada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s não pôde ser instalado porque não é compatível com o %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s não pôde ser instalado porque tem alto risco de causar problemas de estabilidade ou segurança.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ativado com sucesso</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Falha ao ativar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s desativado com sucesso</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Falha ao desativar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s desinstalado com sucesso</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Falha ao desinstalar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s removido com sucesso</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Falha ao remover %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Esta extensão foi migrada de uma versão anterior do %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 extensão</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensão</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s extensões</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensões</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saiba mais</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Atualizado com sucesso</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nenhuma atualização disponível</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Erro</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informações sobre atualização</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Última tentativa:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s foi adicionado ao %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Abra opções da extensão no menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Acesse %1$s pelo menu do %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK, entendi</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Saiba mais</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s foi desativado devido a problemas de segurança ou estabilidade.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s não pôde ser verificado como seguro e foi desativado.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s não é compatível com sua versão do %2$s (versão %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..e546ad50a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Ler e modificar as definições de privacidade</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Aceder aos seus dados em todos os sites</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Aceder aos seus dados para %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Aceder aos seus dados para sites no domínio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Aceder aos seus dados em 1 outro site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Aceder aos seus dados em %1$d outros sites</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Aceder aos seus dados em 1 outro domínio</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Aceder aos seus dados em %1$d outros domínios</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Aceder aos separadores do navegador</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Armazenar dados no cliente sem limite de espaço</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Aceder à atividade do navegador durante a navegação</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Ler e modificar marcadores</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Ler e modificar as definições do navegador</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpar histórico de navegação recente, cookies e dados relacionados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Obter dados da área de transferência</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Introduzir dados na área de transferência</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloquear conteúdo em qualquer página</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Ler o seu histórico de navegação</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Transferir ficheiros, ler e modificar o histórico de transferências do navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abrir ficheiros transferidos para o seu dispositivo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Ler o texto de todos os separadores abertos</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Aceder à sua localização</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Aceder ao histórico de navegação</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorizar utilização de extensões e gerir temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Trocar mensagens com outras aplicações</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Mostrar notificações</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Fornecer serviços criptográficos de autenticação</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controlar as definições de proxy do navegador</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Aceder aos separadores fechados recentemente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ocultar e mostrar separadores do navegador</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Aceder ao histórico de navegação</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Estender as ferramentas de programador para aceder aos seus dados em separadores abertos</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versão</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autores</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Última atualização</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Página inicial</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Saber mais sobre permissões</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Classificação</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Mais acerca deste extra</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Mais sobre esta extensão</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Definições</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ativo</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Desativado</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permitir na navegação privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Executar na navegação privada</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Não permitido em janelas privadas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ativado</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Desativado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomendado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ainda não é suportado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ainda não disponível</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Desativado</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalhes</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissões</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Remover</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Reportar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Adicionar %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s solicita permissões adicionais.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Este requer a sua permissão para:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Este extra pretende:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Adicionar</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permitir</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Recusar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancelar</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instalar extra</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instalar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancelar</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Avaliações: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Avaliação: %1$.02f de 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Extras</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gestor de extras</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Os extras estão temporariamente desativados</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">As extensões estão temporariamente desativadas</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Um ou mais extras pararam de funcionar, tornando o sistema instável.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Uma ou mais extensões deixaram de funcionar, tornando o sistema instável.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reiniciar os extras</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reiniciar extensões</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Procurar mais extras</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Encontrar mais extensões</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permitir</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Recusar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tem uma nova atualização</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d necessita de novas permissões</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">É necessária uma nova permissão</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Atualizações do extra</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Atualizações da extensão</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Verificador de extras suportados</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Novo extra disponível</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Novos extras disponíveis</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Adicionar %1$s ao %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Adicionar %1$s e %2$s ao %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Adicioná-los ao %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">A tecnologia de extras do Firefox está a ser modernizada. Este extras utilizam estruturas que não são compatíveis com o Firefox 75 ou versões posteriores.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Neste momento, estamos a construir a base para uma seleção inicial de extensões recomendadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">A transferir e a validar o extra…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">A transferir e a verificar a extensão…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">A pesquisa por extras falhou!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Falha ao consultar extensões!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Não encontrada a tradução para o idioma %1$s, ou para o idioma predefinido, o %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s foi instalado com sucesso</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">A instalação de %1$s falhou</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">A instalação deste extra falhou.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">A instalação desta extensão falhou.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Este extra não pôde ser transferido porque a ligação falhou.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Não foi possível transferir esta extensão devido a uma falha de ligação.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Este extra não pôde ser instalado porque aparenta estar corrompido.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Não foi possível instalar esta extensão porque esta parece estar corrompida.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Este extra não pôde ser instalado porque ainda não foi verificado.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Não foi possível instalar esta extensão porque esta ainda não foi verificada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s não pôde ser instalado porque não é compatível com o %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s não pôde ser instalado porque possui um risco elevado de causar problemas de estabilidade ou de segurança.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ativado com sucesso</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">A ativação de %1$s falhou</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s foi desativado com sucesso</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">A desativação de %1$s falhou</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s foi desinstalado com sucesso</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">A desinstalação de %1$s falhou</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s foi removido com sucesso</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">A remoção de %1$s falhou</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Este extra foi migrado de uma versão anterior de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 extra</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensão</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s extras</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensões</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Saber mais</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Atualizado com sucesso</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Sem atualizações disponíveis</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Erro</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informação do atualizador</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Última tentativa:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Estado:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s foi adicionado ao %2$s.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Abrir no menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Aceda a %1$s a partir do menu %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, entendi</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Ok</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Saber mais</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s foi desativado devido a problemas de segurança ou estabilidade.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s não pôde ser verificado como sendo seguro e foi desativado.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s não é compatível com sua versão de %2$s (versão %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..8278456b38
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-rm/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Leger e modifitgar parameters per la sfera privata</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Acceder a tias datas per tut las websites</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Acceder a tias datas per %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Acceder a tias datas per websites en la domena %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Acceder a tias datas per 1 autra website</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Acceder a tias datas per %1$d autras websites</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Acceder a tias datas per 1 autra domena</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Acceder a tias datas per %1$d autras domenas</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d da %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Acceder als tabs dal navigatur</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Memorisar ina quantitad da datas betg limitada sin il computer</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Acceder a l\'activitad dal navigatur durant la navigaziun</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Leger e modifitgar segnapaginas</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Leger e modifitgar ils parameters dal navigatur</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Stizzar la cronologia la pli nova, ils cookies e las datas correspundentas</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Leger las datas en l\'archiv provisoric</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Scriver datas en l\'archiv provisoric</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Bloccar cuntegn sin n\'emporta betg tge pagina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Leger tia cronologia da navigaziun</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Telechargiar datotecas e leger e modifitgar la cronologia da telechargiadas dal navigatur</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Avrir las datotecas telechargiadas sin tes apparat</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Leger il text da tut ils tabs averts</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acceder a tia posiziun</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Acceder a la cronologia da navigaziun</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Survegliar l\'utilisaziun dad extensiuns ed administrar designs</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Barattar messadis cun autras apps</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Ta mussar communicaziuns</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Furnir servetschs d\'autentificaziun criptografica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controllar ils parameters da proxy dal navigatur</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Acceder als tabs serrads dacurt</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Zuppentar e mussar ils tabs dal navigatur</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Acceder a la cronologia da navigaziun</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Extender ils utensils per sviluppaders per acceder a tias datas en ils tabs averts</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versiun</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autur(a)</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Auturs</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ultima actualisaziun</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Website</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Vegnir a savair dapli davart permissiuns</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Valitaziun</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Dapli davart quest supplement</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Dapli davart questa extensiun</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Parameters</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activà</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Deactivà</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permetter en il modus privat</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Exequir en il modus privat</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Betg permess en fanestras privatas</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activà</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Deactivà</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installà</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recumandà</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Anc betg sustegnì</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Anc betg disponibel</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Deactivà</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detagls</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Autorisaziuns</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Allontanar</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Rapportar</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Agiuntar %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s dumonda ulteriuras autorisaziuns.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Basegna l\'autorisaziun per:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">L\'extensiun vul:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Agiuntar</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permetter</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Refusar</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Interrumper</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installar in supplement</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installar %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Interrumper</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recensiuns: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Valitaziun: %1$.02f da 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Supplements</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Administraziun da supplements</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Ils supplements èn deactivads temporarmain</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Las extensiuns èn deactivadas temporarmain</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">In u plirs supplements na funcziunan betg pli e rendan il sistem instabel.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Ina u pliras extensiuns na funcziunan betg pli e rendan il sistem instabel.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reaviar ils supplements</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reaviar las extensiuns</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Chattar ulteriurs supplements</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Tschertgar ulteriuras extensiuns</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permetter</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Refusar</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ha ina nova actualisaziun</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d novas autorisaziuns necessarias</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Ina nova autorisaziun è necessaria</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Actualisaziuns da supplements</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Actualisaziuns dad extensiuns</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Controlla da supplements sustegnids</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">In nov supplement stat a disposiziun</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Novs supplements stattan a disposiziun</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Agiuntar %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Agiuntar %1$s e %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Als agiuntar a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">La tecnologia da supplements da Firefox vegn modernisada. Quests supplements fan diever da frameworks che n\'èn betg cumpatibels cun Firefox 75 e versiuns pli novas.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Nus sviluppain actualmain la basa per sustegnair ina selecziun iniziala dad extensiuns recumandadas.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Telechargiar e verifitgar il supplement…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Telechargiar e verifitgar l’extensiun…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Impussibel dad obtegnair la glista dals supplements!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Betg reussì da retschaiver las extensiuns!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Na chattà nagina translaziun, ni per la lingua %1$s ni per la lingua predefinida %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Installà cun success %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Betg reussì dad installar %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Betg reussì dad installar quest supplement.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Betg reussì dad installar questa extensiun.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Impussibel da telechargiar quest supplement causa ina errur da connexiun.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">I n’è betg reussì da telechargiar questa extensiun causa ina errur da connexiun.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Impussibel dad installar quest supplement. El para dad esser donnegià.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">I n’è betg reussì dad installar questa extensiun. Ella para dad esser donnegiada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Impussibel dad installar quest supplement perquai ch\'el n\'è betg verifitgà.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">I n’è betg reussì dad installar questa extensiun perquai ch’ella n’è betg verifitgada.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Impussibel dad installar %1$s pervia dad incumpatibilitad cun %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Impussibel dad installar %1$s perquai che la ristga è gronda ch\'i dat problems da stabilitad u da segirezza.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Activà cun success %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Betg reussì dad activar %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Deactivà cun success %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Betg reussì da deactivar %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Deinstallà cun success %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Betg reussì da deinstallar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Allontanà cun success %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Betg reussì dad allontanar %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Quest supplement è vegnì importà dad ina versiun precedenta da %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 supplement</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extensiun</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s supplements</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensiuns</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Ulteriuras infurmaziuns</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualisà cun success</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nagina actualisaziun disponibla</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Errur</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Infurmaziuns d\'actualisaziun</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ultima emprova:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s è vegnì agiuntà a %2$s.</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Avrir en il menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Acceda a %1$s ord il menu %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, chapì</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Ulteriuras infurmaziuns</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s è vegnì deactivà pervia da problems da segirezza u da stabilitad.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s n’ha betg pudì vegnir verifitgà sco segir ed è vegnì deactivà.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s n\'è betg cumpatibel cun tia versiun da %2$s (versiun %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..ed4c46d2b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ro/strings.xml
@@ -0,0 +1,205 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Să citească și să modifice setările de confidențialitate</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Să îți acceseze datele pentru toate site-urile web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Să îți acceseze datele pentru %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Să îți acceseze datele pentru site-uri din domeniul %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Să îți acceseze datele pe 1 alt site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Să îți acceseze datele pe %1$d alte site-uri</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Să îți acceseze datele în 1 alt domeniu</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Să îți acceseze datele în %1$d alte domenii</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Să acceseze filele browserului</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Să stocheze un volum nelimitat de date pe partea de client</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Să acceseze activitatea browserului în timpul navigării</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Să citească și să modifice marcaje</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Să citească și să modifice setările browserului</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Să șteargă istoricul recent de navigare, cookie-urile și datele asociate</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Să obțină date din clipboard</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Să introducă date în clipboard</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Să descarce fișiere și să citească sau să modifice istoricul descărcărilor din browser</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Să deschidă fișiere descărcate pe dispozitiv</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Să citească textul din toate filele deschise</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Să îți acceseze locația</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Să acceseze istoricul de navigare</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Să monitorizeze utilizarea de extensii și să gestioneze teme</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Să facă schimb de mesaje cu alte aplicații în afară de aceasta</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Să îți afișeze notificări</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Să furnizeze servicii de autentificare criptografică</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Să controleze setările proxy ale browserului</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Să acceseze filele închise recent</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Să ascundă și să afișeze filele browserului</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Să acceseze istoricul de navigare</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Să extindă instrumentele pentru dezvoltatori pentru a-ți accesa datele în filele deschise</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versiune</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Autori</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ultima actualizare</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pagină de start</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Află mai multe despre permisiuni</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Evaluare</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Setări</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Activat</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Dezactivat</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permite în navigare privată</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Execută în navigare privată</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Activat</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Dezactivat</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalate</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recomandate</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Nesuportate încă</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Indisponibile încă</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Dezactivate</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detalii</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permisiuni</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Elimină</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Adaugi %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Îți cere permisiunea:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Adaugă</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Anulează</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Instalează suplimentul</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Anulează</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Suplimente</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permite</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Refuză</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s are o nouă actualizare</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">sunt solicitate %1$d permisiuni noi</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Se solicită o permisiune nouă</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Actualizările suplimentului</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Verificator de suplimente acceptat</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Supliment nou disponibil</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Suplimente noi disponibile</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Adaugă %1$s la %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Adaugă %1$s și %2$s la %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Adaugă-le la %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Tehnologia suplimentelor Firefox se modernizează. Aceste suplimente folosesc cadre necompatibile cu Firefox 75 și versiunile superioare.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Acum construim suportul pentru o selecție inițială de extensii recomandate.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Se descarcă și se verifică suplimentul…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Eroare la interogarea suplimentelor!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s a fost instalat cu succes</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s nu a fost instalat</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s a fost activat cu succes</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s nu a fost activat</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s a fost dezactivat cu succes</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s nu a fost dezactivat</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s a fost dezinstalat cu succes</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s nu a fost dezinstalat</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s a fost eliminat cu succes</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s nu a fost eliminat</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Acest supliment a migrat de la o versiune anterioară %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 supliment</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s suplimente</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Află mai multe</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Actualizare realizată cu succes</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nicio actualizare disponibilă</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Eroare</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informații de actualizare</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ultima încercare:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stare:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s a fost adăugat la %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Deschide-l în meniu</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">OK, am înțeles</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..d2586e16dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ru/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Чтение и изменение настроек приватности</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Доступ к вашим данным для всех сайтов</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Доступ к вашим данным на %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Доступ к вашим данным для сайтов на домене %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Доступ к вашим данным на ещё одном сайте</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Доступ к вашим данным на ещё %1$d сайтах</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Доступ к вашим данным на ещё одном домене</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Доступ к вашим данным на ещё %1$d доменах</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d из %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Доступ к вкладкам браузера</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Хранение неограниченного объёма данных на стороне клиента</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Доступ к активности браузера во время навигации</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Чтение и изменение закладок</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Чтение и изменение настроек браузера</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Удаление недавней истории просмотров, куков и связанных с ними данных</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Получение данных из буфера обмена</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Помещение данных в буфер обмена</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Блокировку содержимого на любой странице</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Чтение истории браузера</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Загрузку файлов, чтение и изменение истории загрузок браузера</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Открытие файлов, загруженных на ваше устройство</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Чтение текста во всех открытых вкладках</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Доступ к вашему местоположению</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Доступ к истории браузера</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Отслеживание использования расширений и управление темами</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Обмен сообщениями с приложениями, помимо этого</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Показ вам уведомлений</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Предоставление услуг криптографической аутентификации</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Контроль настроек прокси в браузере</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Доступ к недавно закрытым вкладкам</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Скрытие и отображение вкладок браузера</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Доступ к истории браузера</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Расширение доступа инструментов разработчика к вашим данным в открытых вкладках</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Версия</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Автор</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Авторы</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Последнее обновление</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Домашняя страница</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Узнайте больше о разрешениях</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Оценка</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Подробнее об этом дополнении</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Подробнее об этом расширении</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Настройки</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Включено</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Отключено</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Разрешено в приватных окнах</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Запуск в приватных окнах</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Не разрешено в приватных окнах</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Включено</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Отключено</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Установлено</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Рекомендованные</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Пока не поддерживаются</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ещё не доступно</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Отключены</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Подробности</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Разрешения</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Удалить</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Сообщить</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Добавить %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s запрашивает дополнительные разрешения.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Оно запрашивает разрешение на:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Оно хочет разрешение на:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Добавить</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Разрешить</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Отклонить</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Отмена</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Установить дополнение</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Установить %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Отмена</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Отзывов: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Оценка: %1$.02f из 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Дополнения</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Менеджер дополнений</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Дополнения временно отключены</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Расширения временно отключены</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Одно или несколько дополнений перестали работать, что сделало вашу систему нестабильной.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Одно или несколько расширений перестали работать, что сделало вашу систему нестабильной.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Перезапустить дополнения</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Перезапустить расширения</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Найти больше дополнений</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Найти больше расширений</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Предоставить</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Отказать</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">У %1$s вышло обновление</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d требует предоставить новые разрешения</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Требуется предоставить ещё одно разрешение</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Обновления дополнений</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Обновления расширений</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Новые поддерживаемые дополнения</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Доступно новое дополнение</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Доступны новые дополнения</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Добавить %1$s в %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Добавить %1$s и %2$s в %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Добавить их в %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Технология дополнений Firefox модернизируется. Эти дополнения используют компоненты, которые не совместимы с Firefox 75 и выше.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">В данное время мы подготавливаем первоначальную систему выбора предлагаемых расширений.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Загрузка и проверка дополнения…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Загрузка и проверка расширения…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Не удалось загрузить список дополнений!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Не удалось загрузить список расширений!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Перевод не был найден ни для языка %1$s, ни для языка по умолчанию (%2$s)</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s успешно установлено</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Ошибка при установке %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Не удалось установить это дополнение.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Не удалось установить это расширение.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Это дополнение не удалось загрузить из-за сбоя соединения.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Это расширение не может быть загружено из-за ошибки соединения.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Это дополнение не может быть установлено, так как оно, по-видимому, повреждено.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Это расширение не может быть установлено, так как оно, по-видимому, повреждено.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Это дополнение не может быть установлено, так как оно не было проверено.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Это расширение не может быть установлено, так как оно не было проверено.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Не удалось установить %1$s, поскольку оно несовместимо с %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s не может быть установлено, так как есть высокий риск, что оно вызовет проблемы со стабильностью или безопасностью.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s успешно включено</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Ошибка при включении %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s успешно отключено</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Ошибка при отключении %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s успешно удалено</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Ошибка при удалении: %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s успешно удалено</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Ошибка при удалении %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Это дополнение было перенесено из предыдущей версии %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 дополнение</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 расширение</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">Дополнений: %1$s</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s расширений</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Узнать больше</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Успешно обновлено</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Обновления отсутствуют</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Ошибка</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Сведения об обновлении</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Последняя попытка:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Состояние:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s было добавлено в %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Открыть его в меню</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Получить доступ к %1$s из меню %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ок, понятно</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Подробнее</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s отключено из-за проблем с безопасностью или стабильностью.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s не удалось проверить на безопасность и был отключён.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s не совместим с вашей версией %2$s (версия %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..cd810510e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-sat/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">ᱱᱤᱥᱚᱱ ᱥᱟᱡᱟᱣ ᱠᱚ ᱯᱟᱲᱦᱟᱣ ᱢᱮ ᱟᱨ ᱵᱚᱫᱚᱞ ᱢᱮ</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ᱡᱚᱛᱚ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱞᱟᱹᱜᱤᱫ ᱟᱢᱟᱜ ᱰᱟᱴᱟ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s ᱞᱟᱹᱜᱤᱫ ᱟᱢᱟᱜ ᱰᱟᱴᱟ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">ᱟᱢᱟᱜ ᱰᱟᱴᱟ ᱟᱹᱛᱩᱨ ᱢᱮ ᱚᱱᱟ ᱥᱟᱭᱤᱴ ᱠᱚ ᱞᱟᱹᱜᱤᱫ ᱡᱟ %1$s ᱰᱚᱢᱮᱱ ᱨᱮ ᱢᱮᱱᱟᱜᱼᱟ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">ᱟᱢᱟᱜ ᱰᱟᱴᱟ 1 ᱚᱞᱜᱟ ᱥᱟᱭᱤᱴ ᱨᱮ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">ᱟᱢᱟᱜ ᱰᱟᱴᱟ %1$d ᱚᱞᱜᱟ ᱥᱟᱭᱤᱴ ᱠᱚᱨᱮ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">ᱟᱢᱟᱜ ᱰᱟᱴᱟ 1 ᱚᱞᱜᱟ ᱰᱚᱢᱮᱱ ᱨᱮ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">ᱟᱢᱟᱜ ᱰᱟᱴᱟ %1$d ᱚᱞᱜᱟ ᱰᱚᱢᱮᱱ ᱠᱚᱨᱮ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d / %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">ᱵᱽᱨᱟᱣᱩᱡᱚᱨ ᱴᱮᱵᱽ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">ᱟᱸᱱᱞᱭᱴᱮᱰ ᱠᱞᱟᱭᱮᱸᱴ ᱰᱟᱴᱟ ᱫᱚᱦᱚᱭ ᱢᱮ </string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱨᱮᱭᱟᱜ ᱠᱟᱹᱢᱤ ᱟᱹᱛᱩᱨ ᱢᱮ ᱯᱟᱱᱛᱮ ᱜᱷᱚᱰᱤ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">ᱵᱩᱠᱢᱟᱨᱠ ᱠᱚ ᱯᱟᱹᱲᱦᱟᱣ ᱢᱮ ᱟᱨ ᱵᱚᱫᱚᱞ ᱢᱮ</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱥᱟᱡᱚᱣ ᱠᱚ ᱯᱟᱹᱲᱦᱟᱣ ᱢᱮ ᱟᱨ ᱵᱚᱫᱚᱞ ᱢᱮ</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">ᱱᱤᱛᱚᱜᱟ ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱦᱤᱛᱟᱹᱞ, ᱠᱩᱠᱤ ᱠᱚ, ᱟᱨ ᱥᱚᱸᱵᱚᱸᱫᱷ ᱰᱟᱴᱟ ᱯᱷᱟᱨᱪᱟᱭ ᱢᱮ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ᱨᱮᱴᱚᱵᱼᱵᱚᱰ ᱠᱷᱚᱱ ᱰᱟᱴᱟ ᱧᱟᱢ ᱢᱮ</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ᱨᱮᱴᱚᱵᱼᱵᱚᱰ ᱨᱮ ᱰᱟᱴᱟ ᱟᱫᱮᱨ ᱢᱮ</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">ᱡᱟᱦᱟᱸ ᱥᱟᱦᱴᱟ ᱨᱮᱜᱮ ᱡᱤᱱᱤᱥ ᱠᱚ ᱵᱟᱹᱰ ᱢᱮ</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱦᱤᱛᱟᱹᱞ ᱯᱟᱲᱦᱟᱣ ᱢᱮ</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ᱨᱮᱫ ᱰᱟᱣᱱᱞᱚᱰ ᱢᱮ ᱟᱨ ᱯᱟᱲᱦᱟᱣ ᱢᱮ ᱟᱨ ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱰᱟᱣᱱᱞᱚᱰ ᱦᱤᱛᱟᱹᱞ ᱵᱚᱫᱚᱞ ᱢᱮ</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ᱨᱮᱫ ᱠᱚ ᱡᱷᱤᱡᱽ ᱢᱮ ᱡᱟᱦᱟᱸ ᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱨᱮ ᱰᱟᱣᱱᱞᱚᱰ ᱦᱚᱭ ᱠᱟᱱᱟ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">ᱥᱟᱱᱟᱢ ᱡᱷᱤᱡᱽ ᱴᱮᱵᱽ ᱠᱚᱣᱟᱜ ᱚᱱᱚᱞ ᱯᱟᱲᱦᱟᱣ ᱢᱮ</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ᱟᱢᱟᱜ ᱡᱟᱭᱜᱟ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱦᱤᱛᱟᱹᱞ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">ᱯᱟᱥᱱᱟᱣ ᱵᱮᱵᱷᱟᱨ ᱨᱮ ᱱᱚᱡᱚᱨ ᱫᱚᱦᱚ ᱟᱨ ᱩᱭᱦᱟᱹᱨ ᱵᱮᱵᱚᱥᱛᱷᱟ</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ᱱᱟᱶᱟ ᱮᱯ ᱪᱷᱟᱰᱤ ᱠᱟᱛᱮ ᱚᱠᱜᱟ ᱮᱯ ᱠᱚᱛᱮ ᱠᱷᱚᱵᱚᱨ ᱵᱚᱫᱚᱞ ᱢᱮ</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">ᱤᱛᱞᱟᱹᱭ ᱠᱚ ᱩᱫᱩᱜ ᱟᱢᱟᱭ</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ᱠᱨᱭᱯᱴᱚᱜᱨᱟᱯᱷᱭᱠ ᱚᱛᱷᱮᱱᱴᱤᱠᱮᱥᱚᱱ ᱥᱟᱹᱨᱣᱤᱥ ᱠᱚ ᱮᱢᱚᱜᱼᱟᱭ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱯᱨᱚᱠᱥᱤ ᱥᱟᱡᱟᱣ ᱠᱚ ᱥᱟᱢᱲᱟᱣ ᱢᱮ</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">ᱱᱤᱛᱚᱜᱽᱼᱟᱜ ᱵᱚᱸᱫᱚᱼᱟᱜ ᱴᱮᱵᱽ ᱠᱚ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ᱵᱽᱨᱟᱣᱩᱡᱚᱨ ᱴᱮᱵᱽ ᱠᱚ ᱩᱠᱩ ᱟᱨ ᱩᱫᱩᱜ ᱢᱮ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱦᱤᱛᱟᱹᱞ ᱟᱹᱛᱩᱨ ᱢᱮ</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ᱟᱢᱟᱜ ᱰᱟᱴᱟ ᱠᱷᱩᱞᱟᱹ ᱴᱮᱵᱽ ᱠᱚᱨᱮ ᱟᱹᱛᱩᱨ ᱞᱟᱹᱜᱤᱫ ᱛᱮ ᱵᱟᱲᱦᱟᱣᱟᱜ ᱴᱩᱞ ᱚᱥᱟᱨ ᱢᱮ</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ᱟᱨᱩᱯᱷᱮᱨᱟᱣ</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">ᱚᱱᱚᱞᱤᱭᱟᱹ :</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">ᱚᱱᱚᱞᱤᱭᱟᱹ ᱠᱚ</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">ᱢᱩᱪᱟᱹᱫ ᱦᱟᱹᱞᱤᱭᱟᱜ</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">ᱚᱲᱟᱜ ᱥᱟᱦᱴᱟ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ᱪᱷᱟᱹᱲ ᱵᱟᱵᱚᱛ ᱡᱟᱹᱥᱛᱤ ᱵᱟᱰᱟᱭ ᱢᱮ</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">ᱫᱚᱨ ᱴᱷᱟᱹᱣᱠᱟᱹ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">ᱱᱚᱶᱟ ᱮᱰᱼᱚᱱ ᱵᱟᱵᱚᱛ ᱰᱷᱮᱨ ᱵᱟᱲᱟᱭ ᱢᱮ</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">ᱱᱚᱶᱟ ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱵᱟᱵᱚᱛ ᱰᱷᱮᱨ ᱵᱟᱲᱟᱭ ᱢᱮ</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ᱥᱟᱡᱟᱣ ᱠᱚ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ᱪᱚᱞᱩ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱚᱸᱫᱚ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">ᱱᱤᱡᱚᱨᱟᱜ ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱨᱮ ᱪᱟᱹᱞᱩ ᱪᱷᱚᱭ ᱮᱢ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">ᱱᱤᱡᱚᱨᱟᱜ ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱨᱮ ᱪᱟᱹᱞᱩᱭ ᱢᱮ</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">ᱯᱨᱟᱭᱣᱮᱴ ᱣᱤᱱᱰᱳ ᱨᱮ ᱵᱟᱭ ᱜᱚᱱᱚᱜᱼᱟ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ᱮᱢ ᱪᱷᱚ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱚᱸᱫᱚᱭ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ᱵᱚᱦᱟᱞᱮᱱᱟ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">ᱵᱟᱛᱟᱣᱟᱜ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ᱱᱤᱛ ᱦᱟᱹᱵᱤᱡ ᱵᱟᱝ ᱥᱟᱯᱯᱚᱴ ᱟᱠᱟᱱᱟ </string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ᱱᱤᱛ ᱦᱟᱹᱵᱤᱡ ᱵᱟᱹᱱᱩᱜᱼᱟ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ᱵᱟᱝ ᱦᱩᱭ ᱦᱚᱪᱚ</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">ᱵᱤᱵᱨᱚᱬ</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ᱪᱷᱟᱹᱰ ᱠᱚ</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">ᱚᱪᱚᱜ ᱢᱮ</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">ᱠᱷᱚᱵᱚᱨ</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s ᱥᱮᱞᱮᱫᱽ ᱟᱢ?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ᱫᱚ ᱟᱨᱦᱚᱸ ᱦᱚᱠ ᱛᱟᱭ ᱫᱚᱨᱠᱟᱨ ᱾</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">ᱱᱚᱶᱟ ᱨᱮ ᱟᱢᱟᱜ ᱚᱱᱩᱢᱚᱛᱤ ᱫᱚᱨᱠᱟᱨ ᱟ:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ᱫᱚᱠᱟᱨ ᱛᱟᱭ :</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">ᱥᱮᱞᱮᱫᱽ ᱢᱮ</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">ᱦᱮᱥᱟᱨᱤᱭᱟᱹ</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">ᱢᱟᱱᱟ</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ᱵᱟᱹᱰᱨᱟᱹ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">ᱮᱰ-ᱚᱱ ᱵᱚᱦᱟᱞ ᱢᱮ</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s ᱵᱚᱦᱟᱞ ᱢᱮ</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ᱵᱟᱹᱰᱨᱟᱹ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">ᱨᱤᱵᱷᱤᱭᱩᱡ ᱠᱚ: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">ᱫᱚᱨ : 5 ᱠᱷᱚᱱ %1$.02f</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ᱮᱰ-ᱟᱸᱱᱥ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ᱮᱰᱼᱚᱱᱥ ᱵᱮᱵᱚᱥᱛᱷᱟ</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰᱼᱚᱱ ᱫᱚ ᱛᱤᱱᱟᱹᱜ ᱜᱷᱟᱹᱬᱤᱡ ᱞᱟᱹᱜᱤᱫ ᱵᱚᱸᱫ ᱠᱟᱱᱟ</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱫᱚ ᱛᱤᱱᱟᱹᱜ ᱜᱷᱟᱹᱬᱤᱡ ᱞᱟᱹᱜᱤᱫ ᱵᱚᱸᱫ ᱠᱟᱱᱟ</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">ᱢᱤᱫ ᱥᱮ ᱚᱱᱟ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱮᱰ-ᱚᱱ ᱠᱚ ᱠᱟᱹᱢᱤ ᱵᱚᱸᱫ ᱮᱱᱟ, ᱟᱢᱟᱜ ᱥᱤᱥᱴᱚᱢ ᱵᱟᱹᱪᱠᱟᱹᱨ ᱮᱫᱟᱭ ᱾</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">ᱢᱤᱫ ᱥᱮ ᱚᱱᱟ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱠᱚ ᱠᱟᱹᱢᱤ ᱵᱚᱸᱫ ᱮᱱᱟ, ᱟᱢᱟᱜ ᱥᱤᱥᱴᱚᱢ ᱵᱟᱹᱪᱠᱟᱹᱨ ᱮᱫᱟᱭ ᱾</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰ-ᱚᱱ ᱫᱩᱦᱲᱟᱹ ᱮᱛᱦᱚᱵ ᱢᱮ</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">ᱮᱠᱮᱴᱮᱱᱮᱚᱱ ᱫᱩᱦᱲᱟᱹ ᱮᱛᱦᱚᱵ ᱢᱮ</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">ᱟᱨᱦᱚᱸ ᱮᱰ-ᱳᱱ ᱯᱟᱱᱛᱮ ᱧᱟᱢ ᱢᱮ</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">ᱟᱨᱦᱚᱸ ᱰᱷᱮᱨ ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱯᱟᱱᱛᱮ ᱧᱟᱢ ᱢᱮ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ᱦᱮᱥᱟᱨᱤᱭᱟᱹ</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ᱢᱟᱱᱟ</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ᱞᱟᱹᱜᱤᱫ ᱱᱟᱶᱟ ᱦᱟᱹᱞᱤᱭᱟᱜ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d ᱱᱟᱶᱟ ᱪᱷᱟᱹᱲ ᱨᱮᱭᱟᱜ ᱫᱚᱨᱠᱟᱨ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ᱢᱤᱫ ᱴᱟᱝ ᱱᱟᱶᱟ ᱪᱷᱟᱹᱲ ᱨᱮᱭᱟᱜ ᱫᱚᱨᱠᱟᱨ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰ-ᱚᱱ ᱨᱮᱭᱟᱜ ᱦᱟᱹᱞᱤᱭᱟᱜ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱦᱟᱹᱞᱤᱭᱟᱹᱠ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">ᱥᱟᱹᱯᱯᱚᱨᱴᱼᱟᱜ ᱮᱰᱰᱼᱚᱱᱥ ᱧᱮᱞᱤᱡ</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">ᱱᱟᱶᱟ ᱮᱰ-ᱚᱱ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">ᱱᱟᱶᱟ ᱮᱰ-ᱚᱱᱥ ᱠᱚ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s ᱨᱮ %1$s ᱥᱮᱞᱮᱫᱽ ᱢᱮ</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s ᱟᱨ %2$s %3$s ᱨᱮ ᱥᱮᱞᱮᱫᱽ ᱢᱮ</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">%1$s ᱨᱮ ᱥᱮᱞᱮᱫᱽ ᱠᱚᱢ</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox ᱮᱰᱼᱚᱱ ᱴᱮᱠᱱᱚᱞᱚᱡᱭ ᱫᱚ ᱢᱚᱰᱮᱱᱚᱜ ᱠᱟᱱᱟ ᱾ ᱱᱚᱣᱟ ᱮᱰᱼᱚᱱ ᱯᱷᱨᱮᱢᱣᱟᱨᱠ ᱠᱚ ᱵᱮᱵᱷᱟᱨᱟᱭ ᱚᱠᱟ ᱫᱚ Firefox 75 ᱟᱨ ᱚᱱᱟ ᱞᱟᱛᱟᱨ ᱵᱷᱟᱹᱥᱚᱱ ᱥᱟᱞᱟᱜ ᱵᱟᱭ ᱠᱟᱹᱢᱤ ᱟᱭ ᱾ </string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ᱟᱞᱮ ᱫᱚ ᱱᱤᱛᱚᱜ ᱮᱛᱦᱚᱵ ᱫᱚᱭᱚᱱ ᱠᱚ ᱨᱮᱭᱟᱜ ᱢᱮᱚᱠ ᱯᱟᱥᱱᱟᱣ ᱠᱚ ᱞᱟᱹᱜᱤᱫ ᱥᱟᱹᱯᱯᱚᱴ ᱞᱮ ᱛᱚᱭᱟᱨ ᱮᱜᱼᱟ ᱾</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰ-ᱚᱱ ᱰᱟᱣᱱᱞᱚᱰ ᱟᱨ ᱧᱮᱞ ᱢᱮᱲᱟᱣ ᱦᱩᱭᱩᱜ ᱠᱟᱱᱟ …</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱰᱟᱣᱱᱞᱚᱰ ᱟᱨ ᱧᱮᱞ ᱢᱮᱲᱟᱣ ᱦᱩᱭᱩᱜ ᱠᱟᱱᱟ …</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰ-ᱚᱱ ᱠᱚ ᱠᱩᱠᱞᱟᱹᱭ ᱰᱤᱜᱟᱹᱣᱮᱱᱟ!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">ᱮᱠᱥᱴᱮᱱᱥᱚᱱᱠᱚ ᱯᱟᱱᱛᱮ ᱡᱷᱚᱜ ᱵᱷᱩᱞ ᱦᱩᱭᱮᱱᱟ!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">%1$s ᱞᱟᱹᱜᱤᱫ ᱛᱚᱨᱡᱚᱢᱟ ᱥᱟᱶ ᱢᱩᱞ ᱯᱟᱹᱨᱥᱤ %2$s ᱵᱟᱭ ᱧᱟᱢ ᱞᱟᱱᱟ</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s ᱨᱟᱹᱥ ᱞᱮᱠᱟᱛᱮ ᱵᱚᱦᱟᱞᱮᱱᱟ</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ᱵᱚᱦᱟᱞ ᱰᱤᱜᱟᱹᱣᱮᱱᱟ</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">ᱱᱚᱶᱟ ᱮᱰᱼᱚᱱ ᱵᱚᱦᱟᱞ ᱨᱮ ᱵᱷᱩᱞ ᱦᱩᱭᱮᱱᱟ ᱾</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">ᱱᱚᱶᱟ ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱵᱚᱦᱟᱞ ᱨᱮ ᱵᱷᱩᱞ ᱦᱩᱭᱮᱱᱟ ᱾</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">ᱡᱩᱲᱟᱹᱣ ᱰᱤᱜᱟᱹᱣ ᱞᱟᱹᱜᱤᱫ ᱛᱮ ᱱᱚᱶᱟ ᱮᱰᱼᱚᱱ ᱟᱹᱛᱩᱨᱩᱨ ᱟᱬᱜᱚ ᱵᱟᱭ ᱜᱟᱱ ᱞᱮᱱᱟ ᱾</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">ᱡᱩᱲᱟᱹᱣ ᱰᱤᱜᱟᱹᱣ ᱞᱟᱹᱜᱤᱫ ᱛᱮ ᱱᱚᱶᱟ ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱟᱹᱛᱩᱨᱩᱨ ᱟᱬᱜᱚ ᱵᱟᱭ ᱜᱟᱱ ᱞᱮᱱᱟ ᱾</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">ᱱᱚᱣᱟ ᱮᱰ-ᱚᱱ ᱵᱟᱝ ᱵᱚᱦᱟᱞ ᱫᱟᱲᱮᱭᱟᱜ ᱟ ᱪᱮᱫᱟᱜ ᱡᱮ ᱱᱚᱣᱟ ᱨᱟᱹᱯᱩᱫ ᱜᱮ ᱧᱮᱞᱚᱠ ᱠᱟᱱᱟ ᱾</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">ᱱᱚᱣᱟ ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱵᱟᱝ ᱵᱚᱦᱟᱞ ᱫᱟᱲᱮᱭᱟᱜ ᱟ ᱪᱮᱫᱟᱜ ᱡᱮ ᱱᱚᱣᱟ ᱨᱟᱹᱯᱩᱫ ᱜᱮ ᱧᱮᱞᱚᱠ ᱠᱟᱱᱟ ᱾</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">ᱱᱚᱣᱟ ᱮᱰᱼᱚᱱ ᱵᱟᱝ ᱵᱚᱦᱟᱞ ᱫᱟᱲᱮᱭᱟᱜ ᱟ ᱪᱮᱫᱟᱜ ᱡᱮ ᱱᱚᱶᱟ ᱫᱚ ᱵᱟᱝ ᱯᱩᱥᱴᱟᱹᱣ ᱠᱟᱱᱟ ᱾</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">ᱱᱚᱣᱟ ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱵᱟᱝ ᱵᱚᱦᱟᱞ ᱫᱟᱲᱮᱭᱟᱜ ᱟ ᱪᱮᱫᱟᱜ ᱡᱮ ᱱᱚᱶᱟ ᱫᱚ ᱵᱟᱝ ᱯᱩᱥᱴᱟᱹᱣ ᱠᱟᱱᱟ ᱾</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s ᱫᱚ ᱵᱟᱝ ᱵᱚᱦᱟᱞ ᱫᱟᱲᱮ ᱞᱮᱱᱟ ᱪᱮᱫᱟᱜ ᱥᱮ ᱱᱚᱣᱟ ᱫᱚ %2$s %3$s ᱥᱟᱞᱟᱜ ᱢᱮᱞ ᱵᱟᱭ ᱡᱚᱢ ᱞᱮᱫᱼᱟ ᱾</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s ᱫᱚ ᱵᱚᱦᱟᱞ ᱵᱟᱝ ᱦᱩᱭ ᱫᱟᱲᱮᱭᱟᱫᱼᱟ ᱪᱮᱫᱟᱜ ᱥᱮ ᱱᱚᱣᱟ ᱫᱚ ᱥᱴᱮᱵᱤᱞᱤᱴᱤ ᱟᱨᱵᱟᱝ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱦᱩᱰᱟᱹᱜ ᱢᱮᱱᱟᱜᱼᱟ ᱾</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ᱵᱮᱥ ᱛᱮ ᱮᱢ ᱪᱷᱚ</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s ᱮᱢ ᱨᱮ ᱦᱩᱰᱟᱹᱜ</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s ᱨᱟᱹᱥ ᱞᱮᱠᱟᱛᱮ ᱵᱚᱸᱫᱚᱭᱮᱱᱟ</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s ᱵᱚᱸᱫᱚᱭ ᱨᱮ ᱰᱤᱜᱟᱹᱣ</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s ᱨᱟᱹᱥ ᱞᱮᱠᱟᱛᱮ ᱵᱚᱦᱟᱞ ᱚᱪᱚᱜᱮᱱᱟ</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s ᱵᱚᱦᱟᱞ ᱚᱪᱚᱜ ᱰᱤᱜᱟᱹᱣᱮᱱᱟ</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s ᱨᱟᱹᱥ ᱞᱮᱠᱟᱛᱮ ᱚᱪᱚᱜ ᱦᱚᱭᱮᱱᱟ</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ᱚᱪᱚᱜ ᱰᱤᱜᱟᱹᱣᱮᱱᱟ</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ᱱᱚᱶᱟ ᱮᱰᱰᱼᱚᱱ ᱫᱚ ᱢᱟᱲᱟᱝ ᱦᱟᱹᱞᱤᱭᱟᱠ %1$s ᱠᱷᱚᱱ ᱩᱪᱟᱹᱲ ᱠᱟᱱᱟ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 ᱮᱰ-ᱚᱱ</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 ᱮᱠᱥᱴᱮᱱᱥᱚᱱ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s ᱮᱰ-ᱚᱱᱥ</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s ᱮᱠᱥᱴᱮᱱᱥᱚᱱ</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ᱰᱷᱮᱨ ᱥᱮᱬᱟᱭ ᱢᱮ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">ᱨᱟᱹᱥ ᱞᱮᱠᱟᱛᱮ ᱦᱟᱹᱞᱤᱭᱟᱜᱮᱱᱟ</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">ᱦᱟᱹᱞᱤᱭᱟᱜ ᱵᱟᱹᱱᱩᱜ-ᱟ</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ᱵᱷᱩᱞ</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">ᱦᱟᱹᱞᱤᱭᱟᱜ ᱵᱟᱵᱚᱛ</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ᱢᱩᱪᱟᱹᱫ ᱠᱩᱨᱩᱢᱩᱴᱩ:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">ᱫᱚᱥᱟ:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s %2$s ᱨᱮ ᱥᱮᱞᱮᱫᱽ ᱦᱚᱭ ᱱᱟ</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">ᱢᱮᱱᱩ ᱨᱮ ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">%2$s ᱢᱮᱱᱩ ᱠᱷᱚᱱ %1$s ᱟᱫᱮᱨ ᱢᱮ ᱾</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">ᱴᱷᱤᱠ, ᱵᱟᱰᱟᱭ ᱠᱮᱜᱼᱟᱹᱧ</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">ᱴᱷᱤᱠ</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">ᱰᱷᱮᱨ ᱥᱮᱬᱟᱭ ᱢᱮ</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ᱫᱚ ᱡᱟᱹᱯᱛᱤ ᱟᱨ ᱵᱟᱝ ᱛᱤᱸᱜᱩ ᱛᱷᱤᱨ ᱠᱟᱛᱷᱟ ᱠᱚ ᱠᱟᱨᱚᱱ ᱛᱮ ᱵᱟᱝ ᱦᱩᱭ ᱦᱚᱪᱚ ᱟᱠᱟᱱᱟ ᱾</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱵᱟᱭ ᱪᱤᱱᱦᱟᱹᱯ ᱠᱟᱱᱟ ᱟᱨ ᱚᱱᱟᱛᱮ ᱵᱚᱸᱫ ᱠᱟᱱᱟ ᱾</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s ᱫᱚ ᱟᱢᱟᱜ ᱦᱟᱹᱞᱤᱭᱟᱹᱠ %2$s (ᱦᱟᱹᱞᱤᱭᱟᱹᱠ %3$s) ᱥᱟᱞᱟᱜ ᱵᱟᱭ ᱠᱟᱹᱢᱤᱭᱟᱭ ᱾</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..d3dbbca241
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-sc/strings.xml
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Lèghere e modificare sa cunfiguratzione de riservadesa</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Atzèdere a is datos de totu is sitos web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Atzèdere a is datos tuos de %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Atzèdere a is datos web de su domìniu %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Atzèdere a is datos tuos in un’àteru situ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Atzèdere a is datos tuos in àteros %1$d sitos</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Atzèdere a is datos tuos in un’àteru domìniu</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Atzèdere a is datos tuos in àteros %1$d domìnios</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d de %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Atzèdere a is ischedas de su navigadore</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Atzèdere a s’atividade de su navigadore durante sa navigatzione</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lèghere e modificare sinnalibros</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lèghere e modificare sa cunfiguratzione de su navigadore</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Limpiare sa cronologia de navigatzione reghente, is testimòngios (cookies) e is datos acapiados</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Otènnere datos de sa punta de billete</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Agiùnghere datos in punta de billete</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blocare su cuntenutu in cale si siat pàgina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Leghe sa cronologia de navigatzione tua</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Iscarrigare archìvios e lèghere e modificare sa cronologia de iscarrigamentos de su navigadore</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Abèrrere archìvios iscarrigados in su dispositivu tuo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lèghere su testu de totu is ischedas abertas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Atzèdere a sa positzione tua</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Atzèdere a sa cronologia de navigatzione</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Controllare s’impreu de is estensiones e gestire is temas</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Iscambiare messàgios cun àteras aplicatziones</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Ammustrare notìficas</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Frunire servìtzios de autenticatzione critogràfica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controllare sa cunfiguratzione de serbidore intermediàriu</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Atzèdere a is ischedas serradas de reghente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Cuare e ammustrare is ischedas de su navigadore</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Atzèdere a sa cronologia de navigatzione</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Abèrrere is ainas de isvilupu pro atzèdere a is datos tuos in is ischedas abertas</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versione</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autoria</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autoria</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ùrtima atualizatzione</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pàgina printzipale</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Àteras informatziones subra de is permissos</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Valutatzione</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Àteru in pitzus de custu cumplementu</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Cunfiguratzione</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ativu</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Disativadu</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Permite in sa navigatzione privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Esecuta in sa navigatzione privada</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ativadu</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Disativadu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installadu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Cussigiadu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">No est ancora cumpatìbile</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">No est ancora a disponimentu</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Disativadu</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detàllios</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissos</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Boga</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Sinnala</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Boles agiùnghere %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s rechedet permissos agiuntivos.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Rechedet su permissu tuo pro:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Bolet:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Agiunghe</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Permite</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Refuda</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Annulla</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Installa su cumplementu</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Annulla</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Retzensiones: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Cumplementos</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gestore de cumplementos</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">Is cumplementos sunt istados disativados in manera temporànea</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Torra a aviare is cumplementos</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Agata àteros cumplementos</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Permite</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Refuda</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s tenet un’atualizatzione</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d permissos noos rechestos</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Permissu nou rechestu</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Atualizatziones de cumplementos</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Verificadore de cumplementos cumpatìbiles</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Cumplementu nou cumpatìbile</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Cumplementos noos a disponimentu</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Agiunghe %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Agiunghe %1$s e %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Agiunghe·ddos a %1$s</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Iscarrighende e verifichende su cumplementu…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Impossìbile otènnere sa lista de cumplementos.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Nissuna tradutzione agatada, ne pro su %1$s nen pro sa lìngua predefinida %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s installadu</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Faddina in s’installatzione de %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Faddina in s’installatzione de custu cumplementu.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">Impossìbile iscarrigare custu cumplementu pro more de una faddina in sa connessione.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Custu cumplementu non si podet installare ca paret corrùmpidu.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Custu cumplementu non si podet installare ca no est averiguadu.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s non si podet installare ca no est cumpatìbile cun %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s non si podet installare ca tenet un’arriscu artu de causare problemas de istabilidade o de seguresa.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ativadu</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Faddina in s’ativatzione de %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s disativadu</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Faddina in sa disativatzione de %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s disinstalladu</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Faddina in sa disinstallatzione de %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s eliminadu</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Faddina in s’eliminatzione de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 cumplementu</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s cumplementos</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Àteras informatziones</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Atualizatzione curreta</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nissuna atualizatzione a disponimentu</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Faddina</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informatzione de s’atualizadore</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ùrtimu tentativu:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Istadu:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s agiuntu a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Aberi·ddu dae su menù</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">AB, cumprèndidu</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Àteras informatziones</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s est istadu disativadu pro more de problemas de seguresa o de istabilidade.</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..6eb142ef1b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-si/strings.xml
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">රහස්‍යතා සැකසුම් කියවීම හා සංශෝධනය</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">සියළු අඩවි සඳහා ඔබගේ දත්ත වෙත ප්‍රවේශය</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s සඳහා ඔබගේ දත්ත වෙත ප්‍රවේශය</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s වසමෙහි අඩවි සඳහා ඔබගේ දත්ත වෙත ප්‍රවේශය</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">අන් අඩවියකට ඔබගේ දත්ත වෙත ප්‍රවේශය</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">අන් අඩවි %1$d කට ඔබගේ දත්ත වෙත ප්‍රවේශය</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">අන් වසමකට ඔබගේ දත්ත වෙත ප්‍රවේශය</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">අන් වසම් %1$d කට ඔබගේ දත්ත වෙත ප්‍රවේශය</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%3$d න් %1$s, %2$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">අතිරික්සුවේ පටිති වෙත ප්‍රවේශය</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">අනුග්‍රාහක-පාර්ශ්ව දත්ත අසීමිතව ගබඩා කිරීම</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">යාත්‍රණය අතරතුර අතිරික්සුවේ ක්‍රියාකාරකම් වෙත ප්‍රවේශය</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">පොත්යොමු කියවීම හා සංශෝධනය</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">අතිරික්සුවේ සැකසුම් කියවීම හා සංශෝධනය</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">මෑත පිරික්සුම් ඉතිහාසය, දත්තකඩ හා ආශ්‍රිත දත්ත මැකීම</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">පසුරු පුවරුවෙන් දත්ත ගැනීම</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">පසුරු පුවරුවට දත්ත ආදාන කරන්න</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">ඕනෑම පිටුවක අන්තර්ගත අවහිරය</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">පිරික්සුම් ඉතිහාසය කියවීම</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ගොනු බාගැනීම සහ අතිරික්සුවේ බාගැනීම් ඉතිහාසය කියවීම, වෙනස් කිරීම</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ඔබගේ පරිගණකයට බාගත වූ ගොනු ඇරීම</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">සියළුම විවෘත පටිතිවල පෙළ කියවීම</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ඔබගේ ස්ථානයට ප්‍රවේශය</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">පිරික්සුම් ඉතිහාසයට ප්‍රවේශය</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">දිගු භාවිතය නිරීක්‍ෂණය හා තේමා කළමනාකරණය</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">මෙය හැර වෙනත් යෙදුම් සමඟ පණිවිඩ හුවමාරු කිරීම</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">ඔබට දැනුම්දීම් පෙන්වන්න</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">අතිරික්සුවේ ප්‍රතියුක්ත සැකසුම් පාලනය</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">මෑත දී වසා දැමූ පටිති වෙත ප්‍රවේශය</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">අතිරික්සුවේ පටිති සැඟවීම හා පෙන්වීම</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">පිරික්සුම් ඉතිහාසයට ප්‍රවේශය</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">විවෘත පටිතිවල ඔබගේ දත්ත වෙත ප්‍රවේශයට සංවර්ධක මෙවලම් විස්තීරණය</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">අනුවාදය</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">කර්තෘ</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">කතුවරු</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">අවසාන යාවත්කාලය</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">මුල් පිටුව</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">අවසර ගැන තව දැනගන්න</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">ඇගැයුම</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">මෙම එක්කහුව ගැන</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">මෙම දිගුව ගැන</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">සැකසුම්</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">සක්‍රිය</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">අක්‍රිය</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">පෞද්. පිරික්සීමෙහි ඉඩදෙන්න</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">පෞද්. පිරික්සීමෙහි ධාවනය</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">පෞද්. කවුළු තුළ ඉඩ නොදේ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">සබලයි</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">අබලයි</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ස්ථාපිතයි</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">නිර්දේශිත</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">තවමත් සහාය නොදක්වයි</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">තවමත් නොතිබේ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">අබලයි</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">විස්තර</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">අවසර</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">ඉවත් කරන්න</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">වාර්තා කරන්න</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s එක් කරන්නද?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s අතිරේක අවසර ඉල්ලා සිටියි.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">එයට ඔබගේ අවසරය අවශ්‍ය වන්නේ:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">වුවමනා වන්නේ:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">එකතු</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">ඉඩ දෙන්න</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">ප්‍රතික්‍ෂේප</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">අවලංගු</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">එක්කහුව ස්ථාපනය</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s ස්ථාපනය කරන්න</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">අවලංගු</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">සමාලෝචන: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">ඇගැයීම: 5 න් %1$.02f</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">එක්කහු</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">එක්කහු කළමනාකරු</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු තාවකාලිකව අබල කර ඇත</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">දිගු තාවකාලිකව අබල කර ඇත</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">ඔබගේ පද්ධතිය අස්ථායී කරමින් එක්කහුවක් හෝ කිහිපයක් නතර විය.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු යළි අරඹන්න</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">තවත් එක්කහු සොයාගන්න</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ඉඩ දෙන්න</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ප්‍රතික්‍ෂේප</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s සඳහා නව යාවත්කාලයක් තිිබේ</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">නව අවසර %1$d ක් අවශ්‍යයි</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">නව අවසරයක් අවශ්‍යයි</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු යාවත්කාල</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">දිගු යාවත්කාල</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">සහාය දක්වන එක්කහු සෝදිසිකරු</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">නව එක්කහුවක් තිබේ</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">නව එක්කහු තිබේ</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s වෙත %1$s එක් කරන්න</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s වෙත %1$s හා %2$s එක් කරන්න</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">%1$s වෙත තේමාව යොදන්න</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">ෆයර්ෆොක්ස් එක්කහු තාක්‍ෂණය නවීකරණය වෙමින් පවතී. මෙම එක්කහු ෆයර්ෆොක්ස් 75 සහ ඉන් ඔබ්බට නොගැළපෙන රාමු භාවිතා කරයි.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">දැනට නිර්දේශිත දිගුවල ප්‍රාරම්භ තේරීමක් සඳහා සහාය තනමින් සිටියි.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහුව බාගැනෙමින් හා සත්‍යාපනය වෙමින්…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු විමසීමට අසමත් විය!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">%1$s සඳහා හෝ පෙරනිමි භාෂාව %2$s සඳහා පරිවර්තනය හමු නොවිණි</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s සාර්ථකව ස්ථාපිතයි</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s ස්ථාපනයට අසමත් විය</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහුව ස්ථාපනයට අසමත් විය.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">සම්බන්ධතාවයට බාධා වීමක් නිසා එක්කහුව බාගැනීමට නොහැකි විය.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">හානි වූ බව පෙනෙන නිසා මෙම එක්කහුව ස්ථාපනයට නොහැකිය.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">සත්‍යාපනය නොකළ එක්කහුවක් බැවින් ස්ථාපනය කිරීමට නොහැකිය.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%2$s %3$s සමඟ නොගැළපෙන නිසා %1$s ස්ථාපනයට නොහැකිය.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">ඉහළ අවදානමක් සහිත ආරක්‍ෂණ හෝ ස්ථායි ගැටළු තිබෙන නිසා %1$s ස්ථාපනයට නොහැකිය.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s සාර්ථකව සබල විය</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s සබල වීමට අසමත් විය</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s සාර්ථකව අබල විය</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s අබල වීමට අසමත් විය</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s සාර්ථකව අස්ථාපිතයි</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s අස්ථාපනයට අසමත් විය</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s සාර්ථකව ඉවත් කෙරිණි</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ඉවත් කිරීමට අසමත් විය</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">මෙම එක්කහුව %1$s පෙර අනුවාදයකින් සංක්‍රමණය විය</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු 1</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">දිගු 1</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු %1$s</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">දිගු %1$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">තව දැනගන්න</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">සාර්ථකව යාවත්කාල විය</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">යාවත්කාල නැත</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">දෝෂයකි</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">යාවත්කාල තොරතුරු</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">අවසාන උත්සාහය:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">තත්‍වය:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%2$s වෙත %1$s එක් කර ඇත</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">වට්ටෝරුවෙහි එය අරින්න</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">හරි, තේරුණා</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">හරි</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">තව දැනගන්න</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">ආරක්‍ෂණ හෝ ස්ථායිතා දෝෂ නිසා %1$s අබල කර ඇත.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s ආරක්‍ෂිත බව සත්‍යාපනයට නොහැකි වූ බැවින් අබල කර ඇත.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s ඔබගේ %2$s අනුවාදයට (%3$s) නොගැළපේ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..445c656c7a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-sk/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Čítať a upravovať nastavenia súkromia</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Prístup k údajom pre všetky webové stránky</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Prístup k údajom pre %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Prístup k údajom pre webové stránky na doméne %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Pristupovať k vašim údajom 1 ďalších webových stránok</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Pristupovať k vašim údajom %1$d ďalších webových stránok</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Pristupovať k vašim údajom na 1 ďalších doménach</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Pristupovať k vašim údajom na %1$d ďalších doménach</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d z %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Prístup ku kartám prehliadača</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Ukladať neobmedzené množstvo údajov na strane klienta</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Prístup k aktivitám prehliadača v priebehu prehliadania</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Čítať a upravovať záložky</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Čítať a upravovať nastavenia prehliadača</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Čítať nedávnu históriu prehliadania, cookies a súvisiace údaje</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Získať údaje zo schránky</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Vkladať údaje do schránky</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blokovať obsah na ľubovoľnej stránke</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Čítať históriu prehliadania</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Sťahovať súbory a čítať a upravovať históriu stiahnutých súborov</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Otvárať súbory stiahnuté do vášho zariadenia</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Prístup k textu všetkých otvorených kariet</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Prístup k údajom o polohe</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Prístup k histórii prehliadania</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorovať používanie rozšírenia a spravovať témy vzhľadu</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Vymieňať si správy s inými aplikáciami</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Zobrazovať upozornenia</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Poskytovať služby spojené s kryptografickým overením</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontrola nad nastavením proxy</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Prístup k nedávno zavretým kartám</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Skrývať a zobrazovať karty prehliadača</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Prístup k histórii prehliadania</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Rozšíriť vývojárske nástroje a získať prístup k vašim údajom v otvorených kartách</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Verzia</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autori</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Posledná aktualizácia</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Domovská stránka</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Ďalšie informácie o povoleniach</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Hodnotenie</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Ďalšie informácie o tomto doplnku</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Ďalšie informácie o tomto rozšírení</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Nastavenia</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Zapnuté</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Vypnuté</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Povoliť v súkromnom prehliadaní</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Spustiť v súkromnom prehliadaní</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Doplnok nie je povolený v súkromných oknách</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Povolené</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Zakázané</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Nainštalované</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Odporúčané</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Zatiaľ nepodporované</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Zatiaľ nie je k dispozícii</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Zakázané</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Podrobnosti</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Povolenia</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Odstrániť</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Nahlásiť</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Chcete pridať %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">Doplnok %1$s požaduje ďalšie povolenia.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Doplnok vyžaduje tieto povolenia:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Chce:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Pridať</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Povoliť</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Zakázať</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Zrušiť</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Inštalovať doplnok</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Nainštalovať %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Zrušiť</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Počet recenzií: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Hodnotenie: %1$.02f z 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Doplnky</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Správca doplnkov</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Doplnky sú dočasne zakázané</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Rozšírenia sú dočasne zakázané</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Jeden alebo viac doplnkov prestalo fungovať a váš systém je nestabilný.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Jedno alebo viac rozšírení prestalo fungovať, v dôsledku čoho je váš systém nestabilný.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Reštartovať doplnky</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Reštartovať rozšírenia</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Zobraziť ďalšie doplnky</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Vyhľadať ďalšie rozšírenia</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Povoliť</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Zakázať</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Doplnok %1$s má novú aktualizáciu</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Počet nových požadovaných povolení: %1$d</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Vyžaduje sa nové povolenie</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Aktualizácia doplnkov</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Aktualizácie rozšírenia</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Kontrola podporovaných doplnkov</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">K dispozícii je nový doplnok</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">K dispozícii sú nové doplnky</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Pridať %1$s do aplikácie %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Pridať %1$s a %2$s do aplikácie %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Pridať do aplikácie %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Technológia doplnkov pre Firefox prechádza modernizáciou. Tieto doplnky používajú technológie, ktoré nie sú kompatibilné s Firefoxom 75 a novším.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">V súčasnosti pracujeme na podpore pre vybrané odporúčané rozšírenia.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Sťahuje sa a overuje sa doplnok…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Rozšírenie sa sťahuje sa a overuje…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Nepodarilo sa získať zoznam doplnkov.</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Nepodarilo sa získať zoznam rozšírení.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Nenašiel sa preklad ani pre jazyk %1$s ani pre jazyk %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Doplnok %1$s bol úspešne nainštalovaný</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Doplnok %1$s sa nepodarilo nainštalovať</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Tento doplnok sa nepodarilo nainštalovať.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Toto rozšírenie sa nepodarilo nainštalovať.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Tento doplnok nemohol byť stiahnutý kvôli problémom s pripojením.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Toto rozšírenie nemohlo byť stiahnuté kvôli problémom s pripojením.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Tento doplnok nemohol byť nainštalovaný, pretože je zrejme poškodený.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Toto rozšírenie nebolo možné nainštalovať, pretože je zrejme poškodené.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Tento doplnok nemohol byť nainštalovaný, pretože nebol overený.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Toto rozšírenie nebolo možné nainštalovať, pretože nebolo overené.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Doplnok %1$s nemohol byť nainštalovaný, pretože nie je kompatibilný s prehliadačom %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Doplnok %1$s nebolo možné nainštalovať, pretože je veľké riziko, že spôsobí problémy so stabilitou alebo bezpečnosťou prehliadača.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Doplnok %1$s bol úspešne povolený</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Doplnok %1$s sa nepodarilo povoliť</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Doplnok %1$s bol úspešne zakázaný</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Doplnok %1$s sa nepodarilo zakázať</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Doplnok %1$s bol úspešne odinštalovaný</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Doplnok %1$s sa nepodarilo odinštalovať</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Doplnok %1$s bol úspešne odstránený</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Doplnok %1$s sa nepodarilo odstrániť</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Tento doplnok bol presunutý z predchádzajúcej verzie aplikácie %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 doplnok</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 rozšírenie</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">Počet doplnkov: %1$s</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">Rozšírenia: %1$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Ďalšie informácie</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Aktualizácia bola úspešne dokončená</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nie sú dostupné žiadne aktualizácie</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Chyba</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informácie o aktualizáciách</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Posledný pokus:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stav:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Doplnok %1$s bol pridaný do aplikácie %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Nájdete ho v ponuke</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Prístup k doplnku %1$s získate z ponuky %2$su.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, rozumiem</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Ďalšie informácie</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Doplnok %1$s bol zablokovaný kvôli problémom so stabilitou alebo bezpečnosťou.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Doplnok %1$s nebolo možné overiť ako bezpečný a bol preto zakázaný.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Doplnok %1$s nie je kompatibilný s vašou verziou prehliadača %2$s (verzia %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..0a6609aa45
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-skr/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">رازداری ترتیباں پڑھو تے تبدیل کرو</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ساری ویب سائٹس کیتے آپݨے ڈیٹا تائیں رسائی گھنو</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s کیتے آپݨے ڈیٹا تائیں رسائی گھنو۔</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s ڈومین وچ آپݨی سائٹس دے ڈیٹا تے رسائی حاصل کرو</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">1 ٻئی سائٹ تے آپݨے ڈیٹا تائیں رسائی گھنو۔</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">%1$d ٻیاں سائٹاں تے آپݨے ڈیٹا تائیں رسائی گھنو</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">1 ٻئی ڈومین تے آپݨے ڈیٹا تائیں رسائی گھنو۔</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">%1$d ٻئی ڈومیناں تے آپݨے ڈیٹا تے رسائی گھنو</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s، %3$d وچوں %2$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">براؤزر ٹیبز تائیں رسائی حاصل کرو</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">گاہک دی طرفوں بے انت ڈیٹا ذخیرہ کرو</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">نیویگیشݨ دے دوران براوئزر دی سرگرمی تائیں رسائی</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">نشانیاں پڑھو تے ترمیم کرو</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">براؤزر ترتیباں پڑھو تے تبدیل کرو</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">حالیہ براؤزنگ تاریخ۔ کوکیاں تے متعلقہ ڈیٹا صاف کرو</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">کلپ بورڈ کنوں ڈیٹا گھنو</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">کلپ بورڈ وچ ڈیٹا پاؤ</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">کہیں وی ورقے تے مواد بلاک کرو</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">آپݨی براؤزنگ تاریخ پڑھو</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">فائلاں ڈاؤن لوڈ کرو تے براؤزر دی ڈاؤن لوڈ تاریخ پڑھو تے تجدید کرو</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">آپݨے ڈیوائس تے ڈاؤن لوڈ تھیاں فائلاں کھولو</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">سارے کھلے ٹیباں دی عبارت پڑھو</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">آپݨے مقام تائیں اپڑو</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">براؤزنگ تاریخ تے اپڑو</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">ایکسٹنشن ورتاوے دی نگرانی کرو تے تھیم منیج کرو</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ایں ایپ دے علاوہ کہیں ٻئی ایپ نال سنیہاں دا تبادلہ کرو</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">تہاݙے کیتے اطلاع نامیاں دی نمائش تھیوے</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">کریپٹو گرافک تصدیق دیاں خدمات فراہم کرو</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">براؤزر دیاں پراکسی ترتیباں کوں کنٹرول کرو</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">حالیہ بند تھیاں ٹیباں تے اپڑو</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">براؤزر ٹیباں لکاؤ تے ݙکھاؤ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">براؤزنگ تاریخ تے اپڑو</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">کھلے ٹیب وچ آپݨے ڈیٹا تائیں رسائی کیتے ڈویلپر دے اوزار ودھاؤ</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ورشن</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">مصنف</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">مصنف</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">چھیکڑی واری اپ ڈیٹ تھیا</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">مکھ پناں</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">اجازتاں بارے ٻیا سکھو</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">ریٹنگ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">ایں ایڈ آن بارے ٻیا</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">ایں ایکسٹنشن بارے ٻیا</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">ترتیباں</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">چالو</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">بند</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">نجی براؤزنگ وچ اجازت ݙیوو</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">نجی براؤزنگ وچ چلاؤ</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">نجی ونڈوز وچ اجازت کائنی</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">فعال تھیا</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">غیرفعال تھیا</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">انسٹال تھیا</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">سفارش تھئے ہوئے</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">اڄݨ تائیں سہارا تھیا کائنی</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">اڄݨ تائیں دستاب کائنی</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">غیرفعال تھیا</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">تفصیلاں</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">اجازتاں</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">ہٹاؤ</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">رپورٹ کرو</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s شامل کروں؟</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s اضافی اجازتاں دی درخواست کریندا ہے</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">ایں وچ تہاݙی اجازت ضروری ہے:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ایہ چاہندا ہے:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">شامل کرو</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">اجازت ݙیوو</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">انکار کرو</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">منسوخ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">ایڈ ــ آن انسٹال کرو</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s انسٹال کرو</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">منسوخ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">جائزہ:%1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">درجہ بندی: 5 وِچوں %1$.02f</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ایڈ ــ آن</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ایڈ ــ آن منیجر</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ آن عارضی طور تے غیرفعال ہن۔</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">ایکسٹنشناں عارضی طور تے غیرفعال ہن۔</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">ہِک یا وَدھ ایڈ آنس نے تُہاݙے سسٹم کوں غیر مستحکم بݨین٘دے ہوئے کَم کرݨ چھوڑ ݙِتّا۔</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">ہِک یا وَدھ ایکسٹینشن نے تُہاݙے سسٹم کوں غیر مستحکم بݨین٘دے ہوئے کَم کرݨ چھوڑ ݙِتّا۔</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ آن ولدا شروع کرو</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">ایکسٹنشناں ولدا شروع کرو</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">ودھیک ایڈ آنز لبھو</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">ٻیاں ایکسٹنشناں لبھو</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">اجازت ݙیوو</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">انکار کرو</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s کیتے ہک نویں اپ ڈیٹ ہے</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d کوں نویاں اجازتاں دی لوڑ ہے</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">نویں اجازت ضروری ہے</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ ــ آن اپ ڈیٹاں</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">ایکسٹنشن اپ ڈیٹاں</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">سہارا تھئے ایڈ ــ آن پڑتال کار</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">نویں ایڈ ــ آن دستیاب ہے</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">نویں ایڈ ــ آن دستیاب ہے</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s وچ %1$s شامل کرو</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s تے %2$s کوں %3$s وچ جوڑو</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">اُنہاں کوں %1$s وچ شامل کرو</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">فائرفوکس دی ایڈ ــ آن ٹیکنالوجی جدید بݨدی پئی ہے۔ ایہ ایڈ ــ آن اینجھے فریم ورک ورتیندن جہڑے فائرفوکس ٧٥ تے اوں کنوں اڳوں دے ورشن دے موافق کائنی۔ </string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ایکسٹنشناساں فی الحال تجویز تھئے ایکسٹنشناں دے موہری انتخاب کیتے سہارا بݨیندے پئے ہیں۔</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ ــ آن کوں ڈاؤن لوڈ تے تصدیق کریندا پئے۔۔۔</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">اایکسٹنشن ڈاؤن لوڈ تے تصدیق کریندا پئے۔۔۔</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ ــ آن دریافت کرݨ وچ ناکام!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">ایکسٹنشناں دریافت کرݨ وچ ناکام!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate"> %1$s زبان کیتے تے نہ ہی پہلوں مقرر زبان %2$s کیتے ترجمہ لبھے</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s کامیابی نال انسٹال تھی ڳیا</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s انسٹال تھیوݨ وچ ناکام تھیا</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">ایہ ایڈ آن انسٹال کرݨ وچ ناکام تھیا</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">ایہ ایکسٹنشن انسٹال کرݨ وچ ناکام تھیا۔</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">کنکشن دی خرابی دی وجہ توں ایہ ایڈ آن ڈاؤن لوڈ نہیں تھی سڳیا۔</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">کنکشن دی خرابی دی وجہ توں ایہ ایکسٹنشن ڈاؤن لوڈ نہیں تھی سڳی۔</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">ایہ ایڈ آن انسٹال نہیں تھی سڳیا کیوں جو ایویں لڳدا ہے جو ایہ کرپٹ ہے۔</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">ایہ ایکسٹنشن انسٹال نہیں تھی سڳی کیوں جو ایویں لڳدا ہے جو ایہ کرپٹ ہے۔</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">ایہ ایڈ آن انسٹال نہیں تھی سڳیا کیوں جو ایندی تصدیق نہیں تھئی۔</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">ایہ ایکسٹنشن انسٹال نہیں تھی سڳی کیوں جو ایندی تصدیق نہیں تھئی۔</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s کوں انسٹال نِھیں کِیتا ون٘ڄ سڳیا کیوں جو اِیہ %2$s%3$s دے نال میل نئیں رکھین٘دا۔ </string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s کوں انسٹال نہیں کیتا ونڄ سڳیا کیوں جو ایندے وِچ استحکام یا سیکورٹی دے مسئلے پیدا تھیوݨ دا زیادہ بھئو ہے۔</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s کامیابی نال فعال تھی ڳیا</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s فعال کرݨ وچ ناکام تھیا</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s کامیابی نال غیرفعال تھی ڳیا</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s غیرفعال کرݨ وچ ناکام تھیا</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s کامیابی نال اݨ انسٹال تھی ڳیا</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s اݨ انسٹال تھیوݨ وچ ناکام تھیا</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s کامیابی نال ہٹ ڳیا</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s ہٹاوݨ وچ ناکام تھیا</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ایہ ایڈ ــ آن %1$s دے پچھلے ورشن کنوں منتقل کیتا ڳیا ہائی</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources"> 1 ایڈ ــ آن</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 ایکسٹنشن</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s ایڈ ــ آن</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s ایکسٹنشناں</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ٻیا سِکھو</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">کامیابی نال اپ ڈیٹ تھی ڳیا</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">اڄݨ تائیں کوئی اپ ڈیٹ دستاب کائنی</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">نقص</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">اپڈیٹر ڄاݨکاری</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">چھیکڑی کوشش:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">حیثیت:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%2$s وچ %1$s شامل تھی ڳیا ہے</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">ایں کوں مینیو وچ کھولو</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">%2$s مینیو نال %1$s تئیں رسائی حاصل کرو۔</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">ٹھیک ہے، سمجھ آ ڳیا</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">ٹھیک ہے</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">ٻیا سِکھو</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">سیکیورٹی یا استحکام دے مسائل دی وجہ توں %1$s کوں غیر فعال کر ݙتا ڳیا ہے۔</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s کوں محفوظ دے طور تے تصدیق نِھیں کِیتی ون٘ڄ سڳین٘دی اَتے اِیکوں غیر فعال کر ݙِتّا ڳِیا ہِے۔</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s تُہاݙے %2$s (ورژن %3$s) دے ورژن نال میل نِھیں کھان٘دا۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..c61073d1ba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-sl/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">branje in spreminjanje nastavitev zasebnosti</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">dostop do vaših podatkov za vsa spletna mesta</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">dostop do vaših podatkov za %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">dostop do vaših podatkov za spletna mesta v domeni %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">dostop do vaših podatkov za 1 drugo spletno mesto</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">dostop do vaših podatkov za %1$d drugih spletnih mest</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">dostop do vaših podatkov za 1 drugo domeno</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">dostop do vaših podatkov za %1$d drugih domen</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d od %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">dostop do zavihkov brskalnika</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">shranjevanje neomejene količine podatkov odjemalca</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">dostop do dejavnosti brskalnika</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">branje in spreminjanje zaznamkov</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">branje in spreminjanje nastavitev brskalnika</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">brisanje nedavne zgodovine, piškotkov in povezanih podatkov</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">dostop do podatkov z odložišča</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">shranjevanje podatkov na odložišče</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Zavrni vsebino na katerikoli strani</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">branje zgodovine brskanja</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">prenašanje datotek ter branje in spreminjanje zgodovine prenosov</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">odpiranje datotek, prenesenih na vašo napravo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">branje besedila vseh odprtih zavihkov</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">dostop do vaše lokacije</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">dostop do zgodovine brskanja</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">spremljanje rabe razširitev in upravljanje tem</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">izmenjevanje sporočil z drugimi aplikacijami</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">prikazovanje obvestil</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">zagotavljanje kriptografskih storitev za overjanje</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">nadzor nad nastavitvami posrednika</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">dostop do nedavno zaprtih zavihkov</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">skrivanje in prikazovanje zavihkov brskalnika</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">dostop do zgodovine brskanja</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">razširjanje razvojnih orodij za dostop do vaših podatkov v odprtih zavihkih</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Različica</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Avtor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Avtorji</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Zadnja posodobitev</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Domača stran</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Več o dovoljenjih</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Ocena</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Več o tem dodatku</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Več o tej razširitvi</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Nastavitve</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Vklopljen</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Izklopljen</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Dovoli v zasebnem brskanju</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Zaženi v zasebnem brskanju</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Ni dovoljeno v zasebnih oknih</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Omogočeno</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Onemogočeno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Nameščeno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Priporočeno</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Še ni podprto</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Še ni na voljo</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Onemogočeno</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Podrobnosti</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Dovoljenja</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Odstrani</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Prijavi</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Dodaj %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s zahteva dodatna dovoljenja.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ta dodatek zahteva vaša dovoljenja za:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Želi:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Dodaj</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Dovoli</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Zavrni</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Prekliči</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Namesti dodatek</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Namesti %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Prekliči</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Ocen: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Ocena: %1$.02f od 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Dodatki</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Upravitelj dodatkov</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki so začasno onemogočeni</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Razširitve so začasno onemogočene</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Eden ali več dodatkov je prenehalo delovati, zaradi česar je sistem postal nestabilen.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Ena ali več razširitev je prenehalo delovati, zaradi česar je sistem postal nestabilen.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Znova zaženi dodatke</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Znova zaženi razširitve</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Poišči več dodatkov</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Poišči več razširitev</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Dovoli</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Zavrni</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ima novo posodobitev</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d zahteva nova dovoljenja</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Zahtevana so nova dovoljenja</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Posodobitve dodatkov</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Posodobitve razširitev</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Pregledovalnik podprtih dodatkov</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Na voljo je nov dodatek</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Na voljo so novi dodatki</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Dodaj %1$s v %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Dodaj %1$s in %2$s v %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Dodaj jih v %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Tehnologija dodatkov za Firefox se posodablja. Ti dodatki uporabljajo ogrodja, ki niso združljivi s Firefoxom 75 in novejšimi.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Trenutno pripravljamo podporo za začetni izbor priporočenih razširitev.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Prenašanje in preverjanje dodatka …</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Prenašanje in preverjanje razširitve …</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Napaka pri pridobivanju dodatkov!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Napaka pri pridobivanju razširitev!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Prevoda ni bilo mogoče najti, niti za jezik %1$s niti za privzeti jezik %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s je bil uspešno nameščen</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Dodatka %1$s ni bilo mogoče namestiti</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Namestitev tega dodatka ni uspela.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Namestitev te razširitve ni uspela.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Tega dodatka ni bilo mogoče prenesti zaradi prekinitve povezave.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Te razširitve ni bilo mogoče prenesti zaradi prekinitve povezave.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Tega dodatka ni bilo mogoče namestiti, ker je videti poškodovan.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Te razširitve ni mogoče namestiti, ker je verjetno poškodovana.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Tega dodatka ni bilo mogoče namestiti, ker ni potrjen.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Te razširitve ni bilo mogoče namestiti, ker ni potrjena.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Ni bilo mogoče namestiti dodatka %1$s, ker ni združljiv s %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s ni bilo mogoče namestiti, ker predstavlja veliko tveganje za težave z varnostjo ali zanesljivostjo.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s je bil uspešno omogočen</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Dodatka %1$s ni bilo mogoče omogočiti</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s je bil uspešno onemogočen</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Dodatka %1$s ni bilo mogoče onemogočiti</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s je bil uspešno odstranjen</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Dodatka %1$s ni bilo mogoče odstraniti</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s je bil uspešno odstranjen</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Dodatka %1$s ni bilo mogoče odstraniti</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ta dodatek je bil preseljen iz prejšnje različice %1$sa</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 dodatek</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 razširitev</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s dodatkov</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s razširitev</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Več o tem</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Uspešno posodobljeno</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Posodobitev ni na voljo</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Napaka</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Podatki o posodobitvah</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Zadnji poskus:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stanje:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s je bil dodan v %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Odprite ga v meniju</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Dostop do dodatka %1$s imate iz menija %2$sa.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Razumem</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">V redu</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Več o tem</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Dodatek %1$s je onemogočen zaradi težav z varnostjo in zanesljivostjo.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">Dodatka %1$s ni bilo mogoče potrditi kot varnega, zato je onemogočen.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">Dodatek %1$s ni združljiv z vašo različico aplikacije %2$s (%3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..c2b8b85a1c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-sq/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Të lexojë dhe ndryshojë rregullime privatësie</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Të hyjë në të dhënat tuaja për krejt sajtet</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Të hyjë në të dhënat tuaja për %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Të hyjë në të dhënat tuaja për sajte të përkatësisë %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Të hyjë në të dhënat tuaja te 1 sajt tjetër</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Të hyjë në të dhënat tuaja te %1$d sajte të tjerë</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Të hyjë në të dhënat tuaja te 1 përkatësi tjetër</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Të hyjë në të dhënat tuaja te %1$d përkatësi të tjera</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d nga %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Të hyjë në skeda shfletuesi</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Të depozitojë sasi të pakufizuar të dhënash në anë klienti</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Të hyjë në veprimtarinë e shfletuesit gjatë lëvizjeve</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Të lexojë dhe ndryshojë faqerojtës</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Të lexojë dhe ndryshojë rregullime shfletuesi</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Të spastrojë historikun e shfletimeve së fundi, “cookies” dhe të dhënat përkatëse</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Të marrë të dhëna nga e papastra</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Të fusë të dhëna në të papastër</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blloko lëndë në çfarëdo faqe</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Të lexojë historikun tuaj të shfletimit</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Të shkarkojë kartela dhe të lexojë dhe ndryshojë historikun e shkarkimeve të shfletuesit</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Të hapë kartela të shkarkuara në kompjuterin tuaj</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Të lexojë tekstin e krejt skedave të hapura</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Të hyjë në të dhëna mbi vendndodhjen tuaj</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Të hyjë në historik shfletimesh</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Të mbikëqyrë përdorimin e zgjerimeve dhe administrojë tema</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Të shkëmbejë mesazhe me programe të tjerë nga ky</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">T’ju shfaqë njoftime</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Të furnizojë shërbime mirëfilltësimi kriptografik</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Të kontrollojë rregullime ndërmjetësi te shfletuesi</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Të hyjë te skeda të mbyllura së fundi</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Të fshehë dhe shfaqë skeda shfletuesi</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Të hyjë në historik shfletimesh</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Të zgjerojë mjetet e zhvilluesit për hyrje në të dhënat tuaja në skeda të hapura</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Autor</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Autorë</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Përditësuar së fundi më</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Faqe hyrëse</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Mësoni më tepër rreth lejesh</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Vlerësim</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Më tepër rreth kësaj shtese</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Më tepër rreth këtij zgjerimi</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Rregullime</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">On</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Off</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Lejoje në shfletim privat</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Xhiroje në shfletim privat</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Nuk lejohet në dritare private</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">E aktivizuar</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">E çaktivizuar</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Të instaluara</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Të këshilluara</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ende të pambuluara</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ende jo gati</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Të çaktivizuara</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Hollësi</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Leje</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Hiqe</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Raportoje</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Të shtohet %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s lyp leje shtesë.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Lyp lejen tuaj të:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Dëshiron të:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Shtoje</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Lejoje</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Mos e lejo</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Anuloje</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Instaloni Shtesë</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Instaloni %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Anuloje</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Shqyrtime: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Vlerësim: %1$.02f nga 5 gjithsej</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Shtesa</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Përgjegjës Shtesash</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Shtesat janë çaktivizuar përkohësisht</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Zgjerimet janë çaktivizuar përkohësisht</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Një, ose më tepër shtesa reshtën së funksionuari, duke e bërë sistemin tuaj të paqëndrueshëm.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Një, ose më tepër zgjerime reshtën së funksionuari, duke e bërë sistemin tuaj të paqëndrueshëm.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Rinisni shtesa</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Rinise zgjerimin</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Gjeni më tepër shtesa</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Gjeni më tepër zgjerime</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Lejoje</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Mohoje</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ka një përditësim të ri</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Lypsen %1$d leje të reja</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Lypset leje e re</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Përditësime shtese</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Përditësime zgjerimesh</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Kontrollues shtesash të mbuluara</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Ka gati shtesë të re</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Ka gati shtesa të reja</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Shtojeni %1$s te %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Shtojeni %1$s dhe %2$s te %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Shtojini te %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Teknologjia e shtesave Firefox po modernizohet. Këto shtesa përdorin platforma që s’janë të përputhshme me Firefox 75 &amp; më tej.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Aktualisht jemi duke krijuar mbulim për një përzgjedhje fillestare Zgjerimesh të Rekomanduar.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Po shkarkohet dhe verifikohet shtesa…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Po shkarkohet dhe verifikohet zgjerimi…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">S’u arrit të kërkohet te Shtesa!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">S’u arrit të kërkohet te Zgjerimet!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">S’u gjet përkthim, as për vendoren %1$s, as për gjuhën parazgjedhje %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">U instalua me sukses %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">S’u arrit të instalohet %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">S’u arrit të çinstalohet kjo shtesë.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">S’u arrit të instalohet ky zgjerim.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Kjo shtesë s’u shkarkua dot, për shkak të një dështimi në lidhjen.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Ky zgjerim s’u shkarkua dot, për shkak të një dështimi në lidhjen.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Kjo shtesë s’u instalua dot, ngaqë duket të jetë e dëmtuar.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Ky zgjerim s’u instalua dot, ngaqë duket të jetë e dëmtuar.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Kjo shtesë s’u instalua dot, ngaqë s’është verifikuar.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Ky zgjerim s’u instalua dot, ngaqë s’është verifikuar.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s s’u instalua dot, ngaqë s’është e përputhshme me %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s s’u instalua dot, ngaqë paraqet rrezik të madh për krijim problemesh qëndrueshmërie ose sigurie.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">U aktivizua me sukses %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">S’u arrit të aktivizohet %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">U çaktivizua me sukses %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">S’u arrit të çaktivizohet %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">U çinstalua me sukses %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">S’u arrit të çinstalohet %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">U hoq me sukses %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">S’u arrit të hiqet %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Kjo shtesë u migrua prej një versioni të mëparshëm të %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 shtesë</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 zgjerim</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s shtesa</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s zgjerime</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Mësoni më tepër</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">U përditësua me sukses</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">S’ka përditësim</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Gabim</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Hollësi Përditësuesi</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Përpjekja e fundit:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Gjendje:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s u shtua te %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Hape te menuja</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Përdoreni %1$s që nga menuja %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK, E Mora Vesh</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Mësoni më tepër</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s është çaktivizuar për shkak problemesh sigurie ose qëndrueshmërie.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s s’u verifikua dot si e siguruar dhe u çaktivizua.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s s’është i përputhshëm me versionin tuaj të %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..a49a848182
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-sr/strings.xml
@@ -0,0 +1,255 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Прочитајте и измените поставке приватности</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Приступите својим подацима за све веб странице</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Приступ подацима за %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Приступ подацима за сајтове на %1$s домену</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Приступ подацима на 1 другом сајту</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Приступ подацима на %1$d других сајтова</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Приступ подацима на 1 другом домену</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Приступ подацима на %1$d других домена</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Приступите језичцима прегледача</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Складиштите неограничену количину клијентских података</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Приступите радњама прегледача приликом навигације</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Прочитајте и измените ознаке</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Прочитајте и измените подешавања прегледача</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Обришите недавну историју прегледања, колачиће и сродне податке</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Узмите податке из привремене меморије</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Унесите податке у привремену меморију</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Блокирајте садржај на било којој страници</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Прочитајте вашу историју прегледања</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Преузмите датотеке и прочитајте и измените историју преузимања</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Отворите датотеке преузете на ваш уређај</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Прочитајте текст из свих отворених језичака</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Приступите вашој локацији</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Приступите историји прегледања</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Надгледајте употребу проширења и управљајте темама</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Размењујте поруке са другим апликацијама осим ове</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Покажите обавештења</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Пружите криптографске услуге провере аутентичности</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Управљајте прокси поставкама прегледача</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Приступите недавно затвореним језичцима</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Сакријте и прикажите језичке прегледача</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Приступите историји прегледања</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Омогући програмерским алаткама приступ вашим подацима у отвореним језичцима</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Верзија</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Аутор</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="120" tools:ignore="UnusedResources">Аутори</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Последњи пут ажурирано</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Почетна страница</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Сазнајте више о дозволама</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Оцена</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link">Више о овом додатку</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Подешавања</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Укључено</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Искључено</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Дозволи у режиму приватног прегледања</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Покрени у режиму приватног прегледања</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Омогућено</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Онемогућено</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Инсталирано</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Препоручено</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Још није подржано</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Још увек није доступно</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Онемогућено</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Детаљи</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Дозволе</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Уклони</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Додати %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s захтева додатне дозволе.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Захтева дозволе за:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Жели да:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Додај</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Дозволи</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Одбиј</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Откажи</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Инсталирај додатак</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Откажи</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Рецензије: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f од 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Додаци</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Управљач додацима</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text">Додаци су привремено онемогућени</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text">Један или више додатака су престали да раде и ваш систем је постао нестабилан.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button">Поново покрените додатке</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text">Пронађите још додатака</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Дозволи</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Одбиј</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s има ново ажурирање</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Потребне су %1$d нове дозволе</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Потребна је нова дозвола</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Ажурирања додатака</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Провера подржаних додатака</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Доступан је нови додатак</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Доступни су нови додаци</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Додај %1$s у %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Додај %1$s и %2$s у %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Додај их у %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Технологија Firefox додатака се модернизује. Ови додаци користе структуре које нису компатибилне са Firefox 75 и новијим.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Тренутно стварамо подршку за почетни одабир препоручених проширења.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Преузимање и проверавање додатка…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Није могуће добити листу додатака!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Превод није пронађен нит за %1$s језик, нити за задати %2$s језик.</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Инсталација %1$s је успела</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Инсталација %1$s није успела</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Инсталирање овог додатка није успело.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">Додатак не може да буде преузет због проблема са везом.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Додатак не може да буде инсталиран, јер је неисправан.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Додатак не може да буде инсталиран, јер није проверен.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s не може да буде инсталиран, јер није компатибилан са %2$s %3$s верзијом.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s не може да буде инсталиран, јер је велика вероватноћа да ће проузроковати нестабилност или проблеме за безбедношћу.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Успешно омогућен додатак %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Омогућавање додатка %1$s није успело</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Успешно онемогућен додатак %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Дективирање %1$s није успело</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Деинсталација %1$s је успела</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Деинсталација %1$s није успела</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Уклањање %1$s је успело</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Уклањање %1$s није успело</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Овај додатак је пренесен из претходне %1$s верзије</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 додатак</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s додатака</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Сазнај више</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Успешно ажурирано</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Нема ажурирања</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Грешка</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Подаци у ажурирању</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Последњи покушај:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Стање:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s је додан у %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Отворите у менију</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">У реду, разумем</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Сазнајте више</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s је онемогућен због проблема са безбедношћу или стабилношћу.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s није могао бити потврђен као безбедан те је онемогућен.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s није компатибилан са вашом %2$s верзијом (верзија %3$s). </string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..359ceede95
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-su/strings.xml
@@ -0,0 +1,271 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Bca jeung ropéa setélan pripasi</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Aksés data anjeun pikeun sakabéh raramatloka</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Aksés data anjeun pikeun %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Aksés data anjeun pikeun loka dina domain %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Aksés data anjeun dina 1 loka séjén</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Aksés data anjeun dina %1$d loka séjén</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Aksés data anjeun dina 1 domain séjén</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Aksés data anjeun dina %1$d domain séjén</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d ti %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Aksés tab panyungsi</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Nyimpen data jumlah anu henteu terbatas ti sisi klién</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Aksés kagiatan panyungsi salila navigasi</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Baca jeung robah markah</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Baca jeung ropéa setélan panyungsi</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Beresihan jujutan langlangan, réréméh, jeung data nu tumali</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Cokot data ti papan klip</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Asupkeun data ti papan klip</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Peungpeuk kontén dina kaca mana waé</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Baca jujutan langlangan anjeun</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Undeur berkas sarta baca jeung ropéa jujutan undeur panyungsi</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Buka berkas undeuran di perangkat anjeun</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Baca téks ti sadaya tab muka</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Aksés lokasi anjeun</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Aksés jujutan langlangan</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Poncorong pamakéan éksténsi sarta kokolakeun téma</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Kikirim surat maké aplikasi lian ti ieu</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Némbongkeun iber ka anjeun</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Nyadiakeun layanan oténtikasi kriptograpik</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Atur setélan proksi panyungsi</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Aksés tab nu anyar ditutup</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Sumputkeun jeung témbongkeun tab panyungsi</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Aksés jujutan langlangan</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Legaan parabot depeloper pikeun ngaksés data anjeun dina tab anu muka</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Pérsi</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Pamilik</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Pamilik</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Panungtung diropéa</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Tepas</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Leuwih jéntré ngeunaan idin</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Peunteun</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Leuwih lengkep ngeunaan ieu émboh</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Salengkepna perkara éksténsi ieu</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Setélan</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Aktip</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Pareum</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Idinan dina langlangan pribadi</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Jalankeun dina langlangan pribadi</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Teu diijinan dina jandéla privat</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Diaktipkeun</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Dipareuman</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Dipasang</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Disarankeun</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Tacan dirojong</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Tacan sayagi</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Dipareuman</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Rincian</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Idin</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Piceun</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Laporan</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Tambahkeun %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ménta idin tambahan.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Butuh idin anjeun pikeun:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Kahayangna:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Nambah</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Idinan</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Tolak</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Bolay</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Masang Add-on</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Pasang %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Bolay</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Ulasan: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Rating %1$.02f tina 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-on</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Manajer Émboh</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Émboh tumpur saheulaanan</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Éksténsi saheulanaan dipareuman</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Aya émboh nu mugen, ngabalukarkeun sistem anjeun teu stabil.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Aya émboh nu mugen, ngabalukarkeun sistem anjeun teu stabil.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Balikan deui émboh</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Panggihan émboh lianna</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Idinan</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Tolak</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s boga apdét anyar</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d idin anyar dipikabutuh</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Perlu idin anyar</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Apdét add-on</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Pamariksa add-on nu didukung</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Aya add-on anyar</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Aya add-on anyar</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Tambahkeun %1$s ka %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Tambahkeun %1$s jeung %2$s ka %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Tambahkeun kabéhanana ka %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Téhnologi add-on Firefox keur dimodérenkeun. Ieu add-on maké rarangka anu henteu cocog jeung Firefox 75 &amp; sanggeusna.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Kami ayeuna ngawangun dukungan pikeun pilihan awal Éksténsi nu Disarankeun.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Ngeundeur jeung verifikasi add-on…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Gagal pikeun pamundut Add-on!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Tarjamahan teu kapanggih, pikeun daérah %1$s pon kitu ogé basa bawaan %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Laksana masang %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Gagal masang %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Gagal masang ieu émboh.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Ieu émboh teu bisa diundeur alatan gagal nyambung.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Ieu émboh teu bisa dipasang, sigana alatan korup.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Ieu émboh teu bisa dipasang alatan acan dipéripikasi.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s teu bisa dipasang alatan teu kompatibel jeung %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s teu bisa dipasang alatan gedé résikona ngabalukarkeun masalah kastabilan jeung kaamanan.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Laksana ngaktipkeun %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Gagal ngaktipkeun %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Laksana numpurkeun %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Gagal numpurkeun %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Laksana ngabongkar %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Gagal ngabongkar %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Laksana miceun %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Gagal miceun %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ieu add-on dipindahkeun ti pérsi heubeul %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s add-on</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Lenyepan</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Laksana ngapdét</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Euweuh apdéteun</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Galat</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Émbaran panganyar</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Tarékah panungtung:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s geus ditambahkeun ka %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Buka dina menu</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Heug, Ngarti</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Lenyepan</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s geus ditumpurkeun alatan masalah kaamanan atawa stabilitas.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s teu bisa dipéripikasi aman tur geus ditumpurkeun.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s teu cocog jeung pérsi %2$s anjeun (pérsi %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..a6fe89491f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Läsa och ändra sekretessinställningar</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Åtkomst till data för alla webbplatser</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Åtkomst till data för %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Åtkomst till data för webbplatser i domänen %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Åtkomst till data på 1 annan webbplats</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Åtkomst till data på %1$d andra webbplatser</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Åtkomst till data på 1 annan domän</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Åtkomst till data på %1$d andra domäner</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d av %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Åtkomst till webbläsarens flikar</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Lagra obegränsat med data på klientsidan</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Åtkomst till webbläsaraktivitet under navigering</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Läsa och ändra bokmärken</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Läsa och ändra webbläsarens inställningar</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Rensa den senaste webbhistoriken, kakor och relaterad data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Hämta data från urklipp</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Mata in data till urklipp</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blockera innehåll på vilken sida som helst</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Läs din webbhistorik</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Hämta filer, läsa och ändra webbläsarens nedladdningshistorik</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Öppna filer som hämtats till din enhet</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Läsa texten på alla öppna flikar</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Åtkomst till din position</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Åtkomst till webbhistoriken</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Övervaka tilläggsanvändning och hantera teman</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Utbyta meddelanden med andra appar än den här</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Visa meddelanden till dig</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Erbjuda kryptografiska autentiseringstjänster</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontrollera proxyinställningar för webbläsare</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Åtkomst till webbläsarens nyligen stängda flikar</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Dölja och visa webbläsarens flikar</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Åtkomst till webbhistoriken</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Utöka utvecklarverktyg för att komma åt dina data i öppna flikar</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Utvecklare</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Utvecklare</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Senast uppdaterad</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Hemsida</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Läs mer om behörigheter</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Betyg</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Mer om detta tillägg</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Mer om detta tillägg</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Inställningar</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">På</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Av</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Tillåt i privat surfning</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Kör i privat surfning</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Inte tillåtet i privata fönster</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Aktiverad</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Inaktiverad</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installerad</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Rekommenderad</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Stöds ännu inte</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ännu inte tillgänglig</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Inaktiverad</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detaljer</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Behörigheter</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Ta bort</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Rapportera</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Lägg till %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s begär ytterligare behörigheter.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Det kräver ditt tillstånd att:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Den vill:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Lägg till</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Tillåt</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Neka</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Avbryt</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Installera tillägg</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Installera %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Avbryt</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recensioner: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Betyg: %1$.02f av 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Tillägg</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Tilläggshanterare</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Tillägg är tillfälligt inaktiverade</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Tillägg är tillfälligt inaktiverade</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Ett eller flera tillägg slutade fungera, vilket gjorde ditt system instabilt.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Ett eller flera tillägg slutade fungera, vilket gjorde ditt system instabilt.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Starta om tillägg</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Starta om tillägg</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Hitta fler tillägg</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Hitta fler tillägg</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Tillåt</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Neka</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s har en ny uppdatering</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d kräver nya behörigheter</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">En ny behörighet krävs</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Tilläggsuppdateringar</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Tilläggsuppdateringar</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Stödd tilläggskontroll</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Nytt tillägg tillgängligt</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Nya tillägg tillgängliga</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Lägg till %1$s i %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Lägg till %1$s och %2$s i %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Lägg till dem i %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox-tilläggsteknologi moderniseras. Dessa tillägg använder ramverk som inte är kompatibla med Firefox 75 och senare.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Vi bygger för närvarande stöd för ett första urval av rekommenderade tillägg.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Hämtar och verifierar tillägg…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Hämtar och verifierar tillägget…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Det gick inte att hämta tillägg!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Det gick inte att fråga efter tillägg!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Översättningen hittades inte, för språket %1$s inte heller för standardspråket %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s har installerats</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Det gick inte att installera %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Det gick inte att installera detta tillägg.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Det gick inte att installera tillägget.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Det här tillägget kunde inte laddas ned på grund av ett anslutningsfel.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Tillägget kunde inte hämtas på grund av ett anslutningsfel.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Tillägget kunde inte installeras eftersom det verkar vara trasigt.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Tillägget kunde inte installeras eftersom det verkar vara skadat.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Det här tillägget kunde inte installeras eftersom det inte har verifierats.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Tillägget kunde inte installeras eftersom det inte har verifierats.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s kunde inte installeras eftersom det inte är kompatibelt med %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s kunde inte installeras eftersom det finns en stor risk för att stabilitets- eller säkerhetsproblem uppstår.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s har aktiverats</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Det gick inte att aktivera %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s har inaktiverats</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Det gick inte att inaktivera %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s har avinstallerats</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Det gick inte att avinstallera %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s har tagits bort</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Det gick inte att ta bort %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Detta tillägg migrerades från en tidigare version av %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 tillägg</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 tillägg</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s tillägg</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s tillägg</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Läs mer</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Uppdaterad</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Ingen uppdatering tillgänglig</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Fel</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Uppdateringsinformation</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Senaste försöket:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s har lagts till %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Öppna den i menyn</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Öppna %1$s från menyn %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Ok, jag förstår</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Läs mer</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s har inaktiverats på grund av säkerhets- eller stabilitetsproblem.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s kunde inte verifieras som säker och har inaktiverats.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s är inte kompatibel med din version av %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..3b458af520
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-szl/strings.xml
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Czytanie i zmiynianie nasztalowań prywatności</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Dostymp do twojich danych na kożdyj strōnie</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Dostymp do twojich danych do %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Dostymp do twojich danych na strōnach we dōmynie %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Dostymp do twojich danych na 1 inkszyj strōnie</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Dostymp do twojich danych na %1$d inkszych strōnach</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Dostymp do twojich danych na 1 inkszyj dōmynie</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Dostymp do twojich danych na %1$d inkszych dōmynach</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Dostymp do kart przeglōndarki</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Chrōniynie bezlimitowyj wielości danych po strōnie klijynta</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Dostymp do aktywności przeglōndarki przi nawigacyji</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Czytanie i zmiynianie zokłodek</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Czytanie i zmiynianie nasztalowań przeglōndarki</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Pucowanie niydownyj historyje przeglōndanio, cookies i skuplowanych danych</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Dostymp do danych z kamerlika</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Wrażanie danych do kamerlika</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Szperowanie zawartości na kożdyj strōnie</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Czytanie historyje przeglōndanio</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Pobiyranie zbiorōw i czytanie a zmiynianie historyje pobiyranio z tyj przeglōndarki</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Ôtwiyranie zbiorōw pobranych na ta maszina</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Czytanie tekstu ze wszyskich ôtwartych kart</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Dostymp do twojigo placu</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Dostymp do historyje przeglōndanio</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Badanie użycio rozszyrzyń i regiyrowanie motywami</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Wymiana wiadōmości z aplikacyjami inkszymi jak ta</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Pokazowanie ci powiadōmiyń</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Dowanie usug kryptograficznyj autyntyzacyje</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kōntrolowanie nasztalowań proxy przeglōndarki</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Dostymp do niydowno zawartych kart</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Krycie i pokazowanie kart przeglōndarki</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Dostymp do historyje przeglōndanio</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Dostymp do twojich danych w ôtwartych kartach bez rozszyrzōne deweloperskie noczynia</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Wersyjo</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Autōry</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ôstatnio aktualizowane</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Dōmowo strōna</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Przewiydz sie wiyncyj ô zgodach</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Ôcyna</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Sztalōnki</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Załōnczōne</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Wyłōnczōne</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Przizwōl przi prywatnym przeglōndaniu</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Puś we prywatnym przeglōdaniu</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Załōnczōne</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Zastawiōne</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Zainstalowane</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Rykōmyndowane</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Jeszcze niyôbsugowane</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Jeszcze niydostympne</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Zastawiōne</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Detajle</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Zgody</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Skasuj</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Przidać %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Rozszyrzynie potrzebuje twojij zgody na:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Przidej</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Pociep</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Zainstaluj rozszyrzynie</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Pociep</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recynzyje: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Rozszyrzynia</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Regiyrowanie rozszyrzyniami</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Przizwōl</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Zakoż</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s mo nowo aktualizacyjo</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Trza nowych zgōd (%1$d)</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Trza nowyj zgody</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Aktualizacyje rozszyrzyń</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Kōntrola ôbsugowanych rozszyrzyń</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Dostympne je nowe rozszyrzynie</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Dostympne sōm nowe rozszyrzynia</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Przidej %1$s do aplikacyje %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Przidej %1$s a %2$s do aplikacyje %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Przidej je do aplikacyje %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Technologijo rozszyrzyń do Firefoxa je modernizowano. Te rozszyrzynia używajōm technologije, co niy ma kōmpatybilno z wersyjōm 75 Firefoxa i nowszymi.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Budujymy prawie sparcie do sztartowyj grupy rykōmyndowanych rozszyrzyń.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Pobieranie i badanie rozszyrzynio…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Pobranie wykazu rozszyrzyń sie niy podarziło!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Niy szło znojś przekładu do godki: %1$s ani do bazowyj godki: %2$s </string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Rozszyrzynie %1$s je zainstalowane</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Instalacyjo rozszyrzynio %1$s sie niy podarziła</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Rozszyrzynie %1$s je załōnczōne</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Załōnczynie rozszyrzynio %1$s sie niy podarziło</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Roszyrzynie %1$s je zastawiōne</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Zastawiynie rozszyrzynio %1$s sie niy podarziło</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Rozszyrzynie %1$s je ôdinstalowane</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Ôdinstalowanie rozszyrzynio %1$s sie niy podarziło</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Rozszyrzynie %1$s je skasowane</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Skasowanie rozszyrzynio %1$s sie niy podarziło</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Te rozszyrzynie prziszło z ôstatnij wersyje aplikacyje %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 rozszyrzynie</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">Rozszyrzynia: %1$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Przewiydz sie wiyncyj</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Aktualizacyjo sie podarziła</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Niy ma dostympnych aktualizacyji</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Feler</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informacyjo ô aktualizacyjach</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ôstatnio prōba:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Sztatus:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s je przidane do aplikacyje %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Ôdewrzij je w myni</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Rozumia</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..dff5f0019e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ta/strings.xml
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">தனியுரிமை அமைப்புகளைப் படித்து மாற்றவும்</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">அனைத்துத் தளங்களுக்குமான உங்கள் தரவை அணுகுக</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s என்பதற்கான உங்கள் தரவை அணுக</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s களத்தில் உள்ள தளங்களுக்கான உங்கள் தரவை அணுக</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">உங்கள் தரவை வேறு 1 தளத்தில் அணுகு</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">உங்கள் தரவை வேறு %1$d தளங்களில் அணுகு</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">உங்கள் தரவை வேறு 1 டொமைனில் அணுகு</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">உங்கள் தரவை வேறு %1$d டொமைன்களில் அணுகு</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">உலாவியின் கீற்றுகளை அணுகுக</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">கிளையண்ட் தரவை வரம்பற்றுச் சேமி</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">வழிச்செலுத்தலின்போது உலாவியின் செயல்பாட்டை அணுகவும்</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">புத்தகக்குறிகளைப் படித்து மாற்றுக</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">உலாவி அமைவுகளைப் படித்து மாற்றுக</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">அண்மைய உலாவல் வரலாறு, நினைவிகள், தொடர்புடைய தரவைத் துடை</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">ஒட்டுப் பலகையிலிருந்து தரவைப் பெறுக</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ஒட்டுப் பலகையில் தரவை உள்ளிடு</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">கோப்புகளைப் பதிவிறக்க உலாவியின் பதிவிறக்க வரலாற்றைப் படிக்க மாற்ற</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">சாதனத்தில் பதிவிறக்கப்பட்ட கோப்புகளைத் திறக்க</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">எல்லாத் திறந்த கீற்றுகளின் உரையையும் படிக்க</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">உங்கள் இருப்பிடத்தை அணுக</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">உலாவல் வரலாற்றை அணுக</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">நீட்சிகளின் பயனளவைக் கண்காணிக்க கருப்பொருட்களை நிர்வகிக்க</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">இதை விடுத்து பிற செயலிகளுடன் செய்திகளைப் பரிமாறவும்</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">உங்களுக்கு அறிவிப்புகளைக் காட்ட</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">மறைகுறியீட்டுச் சான்றுறுதிச் சேவைகளை வழங்க</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">உலாவி பதிலி அமைப்புகளைக் கட்டுப்படுத்த</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">அண்மையில் மூடப்பட்ட கீற்றுகளை அணுக</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">உலாவிக் கீற்றுகளைக் காட்ட மறைக்க</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">உலாவல் வரலாற்றை அணுக</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">திறந்த கீற்றுகளிலுள்ள உங்கள் தரவை அணுக உருவாக்குநர் கருவிகளை நீட்டிக்க</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">பதிப்பு</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">ஆக்குநர்</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">கடைசிப் புதுப்பிப்பு</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">முகப்புப்பக்கம்</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">அனுமதிகள் பற்றி மேலும் அறிக</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">மதிப்பீடு</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">அமைவுகள்</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">இயக்கு</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">நிறுத்து</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">கமுக்க உலாவலில் அனுமதி</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">கமுக்க உலாவலில் இயக்கு</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">செயற்படுத்தப்பட்டது</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">செயல்நீக்கப்பட்டது</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">நிறுவப்பட்டது</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">பரிந்துரைக்கப்பட்டது</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">இன்னும் ஆதரிக்கப்படவில்லை</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">இன்னும் கிடைக்கவில்லை</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">செயல்நீக்கப்பட்டது</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">விவரங்கள்</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">அனுமதிகள்</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">நீக்கு</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s ஐச் சேர்க்கவா?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">இவற்றிற்கான உங்கள் அனுமதி தேவை:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">சேர்</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">இரத்து</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">மேற்சேர்க்கையை நிறுவு</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">இரத்து</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">மீளாய்வுகள்: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">துணை-நிரல்கள்</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">துணை நிரல் நிர்வாகி</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">அனுமதி</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">மறு</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s புதிய புதுப்பிப்பைக் கொண்டுள்ளது</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d புதிய அனுமதிகள் தேவை</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ஒரு புதிய அனுமதி தேவைப்படுகிறது</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">மேற்சேர்க்கைப் புதுப்பிப்புகள்</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">ஆதரிக்கப்படும் மேற்சேர்க்கைச் சரிபார்ப்பு</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">புதிய மேற்சேர்க்கை கிடைக்கிறது</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">புதிய மேற்சேர்க்கைகள் கிடைக்கின்றன</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s ஐ %2$s இல் சேர்க்கவும்</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s, %2$s மேற்சேர்க்கைகளை %3$s இல் சேர்க்கவும்</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">அவற்றை %1$s இல் சேர்க்கவும்</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">பயர்பாக்சு மேற்சேர்க்கை நுட்பம் நவீனமாகிறது. இந்த மேற்சேர்க்கைகள் பயர்பாக்சு 75 &amp; அதற்குப் பிந்தைய பதிப்புகளுக்குப் பொருந்தாக் கட்டமைப்பைப் பயன்படுத்துகின்றன.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">நாங்கள் தற்போது பரிந்துரைக்கப்பட்ட நீட்டிப்புகளின் தொடக்கத் தேர்வுக்கான ஆதரவைக் கட்டமைத்து வருகிறோம்.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">மேற்சேர்க்கையைப் பதிவிறக்கிச் சரிபார்க்கிறது…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">மேற்சேர்க்கை வினவல் தோல்வி!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">மொழிபெயர்ப்பு %1$s மொழிக்கும், இயல்பு மொழி %2$sக்கும் கிடைக்கவில்லை</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s வெற்றிகரமாக நிறுவப்பட்டது</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s நிறுவுதல் தோல்வி</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s வெற்றிகரமாகச் செயற்படுத்தப்பட்டது</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s செயற்படுத்துதல் தோல்வி</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s வெற்றிகரமாகச் செயல்நீக்கப்பட்டது</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s செயல்நீக்குதல் தோல்வி</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s வெற்றிகரமாக நிறுவல்நீக்கப்பட்டது</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s நிறுவல்நீக்குதல் தோல்வி</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s வெற்றிகரமாக நீக்கப்பட்டது</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s நீக்குதல் தோல்வி</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">இந்த மேற்சேர்க்கை %1$s முந்தைய பதிப்பிலிருந்து இடம்பெயர்க்கப்பட்டது</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 மேற்சேர்க்கை</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s மேற்சேர்க்கைகள்</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">மேலும் அறிய</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">வெற்றிகரமாகப் புதுப்பிக்கப்பட்டது</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">புதுப்பிப்பு கிடைக்கவில்லை</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">பிழை</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">புதுப்பித்தல் தகவல்</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">கடைசி முயற்சி:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">நிலை:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ஆனது %2$s உடன் சேர்க்கப்பட்டது</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">பட்டியில் இதனைத் திற</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">சரி, புரிந்தது</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..5eda3ba3cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-te/strings.xml
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">అంతరంగికత అమరికలను చూడటం, మార్చడం</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">అన్ని వెబ్ సైట్లలో మీ డేటాను చూడటం</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s కొరకు మీ డేటాను చూడటం</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s డొమైను లోని సైట్లకు మీ డేటాను చూడటం</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">1 ఇతర సైటులో మీ డేటాను చూడగలదు</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">%1$d ఇతర సైట్లలో మీ డేటాను చూడగలదు</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">1 ఇతర డొమైనులో మీ డేటాను చూడగలదు</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">%1$d ఇతర డొమైన్లలో మీ డేటాను చూడగలదు</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">విహారిణి ట్యాబులను చూడటం</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">క్లయింటు వైపు అపరిమిత డేటాను నిల్వచేయడం</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">నావిగేషన్ సమయంలో విహారిణి కార్యకలాపాన్ని చూడటం</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">ఇష్టాంశాలను చూడటం, మార్చటం</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">విహారిణి అమరికలను చూడటం, మార్చడం</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">ఇటీవలి విహరణ చరిత్ర, కుకీలు, సంబంధింత డేటాను తుడిచివేయడం</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">క్లిప్‌బోర్డు నుండి డేటాను పొందటం</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">క్లిప్‌బోర్డులో డేటాను పెట్టడం</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ఫైళ్లను దించుకోవడం, విహారిణి దింపుకోలు చరిత్రను చూడడం సవరించడం</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">మీ కంప్యూటరులోని దింపుకున్న దస్త్రాలను తెరవడం</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">తెరిచివున్న ట్యాబులన్నిటి పాఠ్యాన్ని చదవడం</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">మీ స్థానాన్ని చూడటం</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">విహరణ చరిత్రను చూడటం</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">పొడగింతల వాడుకను పర్యవేక్షించడం, అలంకారాలను నిర్వహించడం</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">ఇతర అనువర్తనాలతో సందేశాలను ఇచ్చిపుచ్చుకోవడం</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">మీకు గమనింపులను చూపించడం</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">క్రిప్టోగ్రఫిక్ అధీకరణ సేవలను అందించడం</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">విహారిణి ప్రాక్సీ అమరికలను నియంత్రించడం</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">ఇటీవల మూసివేసిన ట్యాబులను చూడటం</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">విహారిణి ట్యాబులను దాచడం, చూపించడం</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">విహరణ చరిత్రను చూడటం</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">తెరిచివున్న ట్యాబులలో మీ డేటాను చూడగలిగేలా డెవలపర్ పనిముట్లను విస్తరించడం</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">వెర్షన్</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">రచయితలు</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">చివరి మార్పు</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">ముంగిలిపేజీ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">అనుమతుల గురించి మరింత తెలుసుకోండి</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">రేటింగు</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">అమరికలు</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ఆన్</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">ఆఫ్</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">అంతరంగిక విహరణలో అనుమతించు</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">అంతరంగిక విహరణలో నడుస్తుంది</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">చేతనం</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">అచేతనం</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">స్థాపించబడింది</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">సిఫార్సు చేయబడినవి</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ఇంకా తోడ్పాటు లేదు</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ఇంకా అందుబాటులో లేవు</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">అచేతనం</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">వివరాలు</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">అనుమతులు</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">తీసివేయి</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$sను చేర్చాలా?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">వీటికి మీ అనుమతి అవసరం:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">చేర్చు</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">రద్దుచేయి</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">పొడగింతను స్థాపించు</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">రద్దుచేయి</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">సమీక్షలు: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">పొడగింతలు</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">పొడగింతల నిర్వహణ</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">అనుమతించు</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">తిరస్కరించు</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s‌కి కొత్త తాజాకరణ ఉంది</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d కొత్త అనుమతులు అవసరం</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">కొత్త అనుమతి కావాలి</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">పొడగింత తాజాకరణలు</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">తోడ్పాటున్న పొడగింతల చెకర్</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">కొత్త పొడగింత అందుబాటులో ఉంది</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">కొత్త పొడగింతలు అందుబాటులో ఉన్నాయి</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s‌ను %2$s‌కి చేర్చు</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s, %2$s‌లను %3$s‌కి చేర్చు</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">వాటిని %1$sకు చేర్చు</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox పొడగింతల సాంకేతికత ఆధునికీకరించబడుతోంది. ఈ పొడగింతలు Firefox 75 ఆ తర్వాతి వెర్షనలకు అనుగుణంగా లేని ఫ్రేమువర్కులను వాడుతున్నాయి.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ప్రస్తుతం మేము ముందుగా ఎంచుకున్న సిఫార్సుచేయబడ్డ పొడగింతలకు తోడ్పాటును నిర్మిస్తున్నాము.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">పొడగింతను దించుకుంటుంది, తనిఖీ చేస్తుంది…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">పొడగింతలను తేవడం విఫలమైంది!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">%1$s లొకేల్, అప్రమేయ భాష %2$s లలో దేనికీ అనువాదం దొరకలేదు</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s విజయవంతంగా స్థాపించబడింది</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s స్థాపన విఫలమైంది</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s విజయవంతంగా చేతనమైంది</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s చేతనించడం విఫలమైంది</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s విజయవంతంగా అచేతనం చేయబడింది</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s అచేతనించడం విఫలమైంది</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s విజయవంతంగా నిర్మూలించబడింది</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s‌ను నిర్మూలించడం విఫలమైంది</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s విజయవంతంగా తొలగించబడింది</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$sని తొలగించడం విఫలమైంది</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ఈ పొడగింత %1$s యొక్క మునుపటి వెర్షను నుండి తేబడింది</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 పొడగింత</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s పొడగింతలు</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">ఇంకా తెలుసుకోండి</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">విజయవంతంగా తాజాకరించబడింది</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">తాజాకరణ అందుబాటులో లేదు</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">దోషం</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">తాజాకరి సమాచారం</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">చివరి ప్రయత్నం:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">స్థితి:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s %2$s‌కి చేర్చబడింది</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">దాన్ని మెనూలో తెరువు</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">సరే, అర్థమయ్యింది</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..8554b62f21
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-tg/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Хондан ва тағйир додани танзимоти махфият</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Дастрас кардани маълумоти шумо барои ҳамаи сомонаҳо</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Дастрас кардани маълумоти шумо барои %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Дастрас кардани маълумоти шумо барои сомонаҳо дар домени %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Дастрас кардани маълумоти шумо дар 1 сомонаи дигар</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Дастрас кардани маълумоти шумо дар %1$d сомонаи дигар</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Дастрас кардани маълумоти шумо дар 1 домени дигар</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Дастрас кардани маълумоти шумо дар %1$d домени дигар</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d аз %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Дастрас кардани варақаҳои браузер</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Нигоҳ доштани миқдори номаҳдуди маълумоти муштариён</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Дастрас кардани фаъолияти браузер ҳангоми паймоиш</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Хондан ва тағйир додани хатбаракҳо</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Хондан ва тағйир додани танзимоти браузер</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Пок кардани таърихи тамошокунии охирин, кукиҳо ва маълумоти марбут</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Гирифтани маълумот аз ҳофизаи муваққатӣ</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Гузоштани маълумот ба ҳофизаи муваққатӣ</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Муҳтаво дар ҳамаи саҳифаҳо манъ карда мешавад</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Таърихи тамошокунии худро аз назар гузаронед</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Боргирӣ кардани файлҳо, хондан ва тағйир додани таърихи боргириҳои браузер</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Кушодани файлҳое, ки ба дастгоҳи шумо боргирӣ карда шудаанд</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Хондани матн дар ҳамаи варақаҳои кушодашуда</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Дастрас кардани ҷойгиршавии шумо</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Дастрас кардани таърихи тамошо</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Назорат кардани истифодаи васеъшавӣ ва идора кардани мавзуъҳо</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Мубодила кардани паёмҳо бо барномаҳо ба ғайр аз барномаи ҷорӣ</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Намоиш додани огоҳномаҳо ба шумо</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Таъмин намудани хидматҳои санҷиши ҳаққоният бо нақши рамзӣ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Идора кардани танзимоти прокси браузер</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Дастрас кардани варақаҳои ба наздикӣ пӯшидашуда</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Намоиш ва пинҳон кардани варақаҳои браузер</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Дастрас кардани таърихи тамошо</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Васеъкунии абзорҳои барномасозӣ барои дастрас кардани маълумоти шумо дар варақаҳои кушодашуда</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Силсила</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Муаллиф</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Муаллифон</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Санаи навсозии охирин</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Саҳифаи асосӣ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Маълумоти бештар дар бораи иҷозатҳо</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Баҳодиҳӣ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Маълумоти бештар дар бораи ин ҷузъи иловагӣ</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Маълумоти бештар дар бораи ин васеъшавӣ</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Танзимот</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Фаъол</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Ғайрифаъол</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Иҷозат додан дар тамошокунии хусусӣ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Иҷро кардан дар тамошокунии хусусӣ</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Дар равзанаҳои хусусӣ иҷозат дода намешавад</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Фаъол аст</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Ғайрифаъол аст</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Насбшуда</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Тавсияшуда</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ҳанӯз дастгирӣ намешавад</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Ҳанӯз дастрас нест</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Ғайрифаъол аст</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Тафсилот</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Иҷозатҳо</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Тоза кардан</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Гузориш додан</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s-ро илова мекунед?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s иҷозати иловагиро дархост мекунад.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ин иҷозати шуморо барои зерин талаб мекунад:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Дархост барои:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Илова кардан</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Иҷозат додан</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Рад кардан</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Бекор кардан</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Насб кардани ҷузъи иловагӣ</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">«%1$s»-ро насб намоед</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Бекор кардан</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Тақризҳо: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Баҳодиҳӣ: %1$.02f аз 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Ҷузъҳои иловагӣ</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Мудири ҷузъи иловагӣ</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Ҷузъҳои иловагӣ муваққатан ғайрифаъол шудаанд</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Васеъшавиҳо муваққатан ғайрифаъол шудаанд</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Кори як ё якчанд ҷузъи иловагӣ қатъ карда шуд ва кори низоми шуморо ба вазъияти ноустувор гардонид.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Кори як ё якчанд васеъшавӣ қатъ карда шуд ва кори низоми шуморо ба вазъияти ноустувор гардонид.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Аз нав оғоз кардани ҷузъҳои иловагӣ</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Аз нав оғоз кардани васеъшавиҳо</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Ёфтани ҷузъҳои иловагии бештар</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Гирифтани васеъшавиҳои бештар</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Иҷозат додан</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Рад кардан</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s навсозии нав дорад</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d иҷозати нав лозиманд</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Иҷозати нав лозим аст</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Навсозиҳои ҷузъи иловагӣ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Навсозиҳои васеъшавӣ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Санҷиши ҷузъҳои иловагии дастгиришаванда</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Ҷузъи иловагии нав дастрас аст</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Ҷузъҳои иловагии нав дастрасанд</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Илова кардани %1$s ба %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Илова кардани %1$s ва %2$s ба %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Илова кардани онҳо ба %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Технологияи ҷузъҳои иловагии Firefox такмил дода мешавад. Ин ҷузъҳои иловагӣ аз унсурҳое истифода мебаранд, ки бо Firefox 75 ва берун аз он мувофиқат намекунанд.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Мо дар айни замон барои интихоби ибтидоии васеъшавиҳои тавсияшуда низоми дастгириро омода карда истодаем.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Боргирӣ ва тасдиқкунии ҷузъи иловагӣ…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Боргирӣ ва тасдиқкунии васеъшавӣ…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Дархости рӯйхати ҷузъҳои иловагӣ иҷро нашуд!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Дархости рӯйхати васеъшавиҳо иҷро нашуд!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Тарҷума барои маҳаллисозии %1$s ё барои забони %2$s ёфт нашуд</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s бо муваффақият насб карда шуд</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Насбкунии %1$s иҷро нашуд</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Ин ҷузъи иловагӣ насб карда нашуд.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Ин васеъшавӣ насб карда нашуд.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Ин ҷузъи иловагӣ ба сабаби хатои пайвастшавӣ боргирӣ карда нашуд.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Ин васеъшавӣ ба сабаби хатои пайвастшавӣ боргирӣ карда нашуд.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Ин ҷузъи иловагӣ насб карда намешавад, зеро ки он вайрон мебошад.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Ин васеъшавӣ насб карда намешавад, зеро ки он вайрон мебошад.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Ин ҷузъи иловагӣ насб карда намешавад, зеро ки он тасдиқнашуда мебошад.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Ин васеъшавӣ насб карда намешавад, зеро ки он тасдиқнашуда мебошад.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">«%1$s» насб карда намешавад, зеро ки он ба «%2$s %3$s» мувофиқат намекунад.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">«%1$s» насб карда намешавад, зеро ки он барои ба вуҷуд овардани мушкилиҳои устуворӣ ва амниятӣ хатари баланд дорад.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s бо муваффақият фаъол карда шуд</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Фаъолкунии %1$s иҷро нашуд</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s бо муваффақият ғайрифаъол карда шуд</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Ғайрифаъолкунии %1$s иҷро нашуд</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Насби %1$s бо муваффақият лағв карда шуд</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Лағвкунии насби %1$s иҷро нашуд</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s бо муваффақият тоза карда шуд</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Тозакунии %1$s иҷро нашуд</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ин ҷузъи иловагӣ аз силсилаи қаблии %1$s интиқол дода шуд</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 ҷузъи иловагӣ</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 васеъшавӣ</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s ҷузъи иловагӣ</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s васеъшавӣ</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Маълумоти бештар</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Бо муваффақият навсозӣ карда шуд</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Навсозиҳо дастнорасанд</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Хато</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Маълумот дар бораи навсозӣ</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Кӯшиши охирин:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Вазъият:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ба %2$s илова карда шуд</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Онро дар меню кушоед</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Ба «%1$s» аз менюи «%2$s» дастрасӣ пайдо намоед.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Хуб, фаҳмидам</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">ХУБ</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Маълумоти бештар</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">Ба сабабҳои мушкилиҳои амниятӣ ё ноустувории кор, «%1$s» ғайрифаъол карда шуд.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">«%1$s» ҳамчун ҷузъи иловагии бехатар тасдиқ карда нашуд ва ба ин сабаб ғайрифаъол карда шуд.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">«%1$s» ба версияи «%2$s» мувофиқат намекунад (версияи «%3$s»).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..ed045b9db9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-th/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">อ่านและเปลี่ยนแปลงการตั้งค่าความเป็นส่วนตัว</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">เข้าถึงข้อมูลของคุณสำหรับเว็บไซต์ทั้งหมด</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">เข้าถึงข้อมูลของคุณสำหรับ %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">เข้าถึงข้อมูลของคุณสำหรับไซต์ในโดเมน %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">เข้าถึงข้อมูลของคุณใน 1 ไซต์อื่น</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">เข้าถึงข้อมูลของคุณใน %1$d ไซต์อื่นๆ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">เข้าถึงข้อมูลของคุณใน 1 โดเมนอื่น</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">เข้าถึงข้อมูลของคุณใน %1$d โดเมนอื่นๆ</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d จาก %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">เข้าถึงแท็บของเบราว์เซอร์</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">จัดเก็บข้อมูลฝั่งไคลเอ็นต์ไม่จำกัดจำนวน</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">เข้าถึงกิจกรรมของเบราว์เซอร์ระหว่างการนำทาง</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">อ่านและเปลี่ยนแปลงที่คั่นหน้า</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">อ่านและเปลี่ยนแปลงการตั้งค่าเบราว์เซอร์</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">ล้างประวัติการเรียกดู, คุกกี้ และข้อมูลที่เกี่ยวข้องล่าสุด</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">รับข้อมูลจากคลิปบอร์ด</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">ป้อนข้อมูลไปยังคลิปบอร์ด</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">ปิดกั้นเนื้อหาบนหน้าใดๆ</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">อ่านประวัติการเรียกดูของคุณ</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ดาวน์โหลดไฟล์และอ่านและเปลี่ยนแปลงประวัติการดาวน์โหลดของเบราว์เซอร์</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">เปิดไฟล์ที่ดาวน์โหลดไปยังอุปกรณ์ของคุณ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">อ่านข้อความของแท็บที่เปิดอยู่ทั้งหมด</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">เข้าถึงตำแหน่งที่ตั้งของคุณ</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">เข้าถึงประวัติการเรียกดู</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">เฝ้าสังเกตการใช้ส่วนขยายและจัดการชุดตกแต่ง</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">แลกเปลี่ยนข้อความกับแอปอื่นนอกเหนือจากแอปนี้</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">แสดงผลการแจ้งเตือนให้คุณ</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">ให้บริการตรวจสอบความถูกต้องของการเข้ารหัสลับ</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">ควบคุมการตั้งค่าพร็อกซีของเบราว์เซอร์</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">เข้าถึงแท็บที่ปิดล่าสุด</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">ซ่อนและแสดงแท็บของเบราว์เซอร์</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">เข้าถึงประวัติการเรียกดู</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ขยายเครื่องมือนักพัฒนาเพื่อเข้าถึงข้อมูลของคุณในแท็บที่เปิดอยู่</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">รุ่น</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">ผู้สร้าง</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">ผู้สร้าง</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">อัปเดตล่าสุด</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">หน้าแรก</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">เรียนรู้เพิ่มเติมเกี่ยวกับสิทธิอนุญาต</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">การจัดอันดับ</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">เพิ่มเติมเกี่ยวกับส่วนเสริมนี้</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">ข้อมูลเพิ่มเติมเกี่ยวกับส่วนขยายนี้</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">การตั้งค่า</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">เปิด</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">ปิด</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">อนุญาตในการเรียกดูแบบส่วนตัว</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">เรียกใช้ในการเรียกดูแบบส่วนตัว</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">ไม่อนุญาตในหน้าต่างส่วนตัว</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">ถูกเปิดใช้งาน</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">ถูกปิดใช้งาน</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">ติดตั้งแล้ว</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">แนะนำ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ยังไม่รองรับ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ยังไม่มีในตอนนี้</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">ถูกปิดใช้งาน</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">รายละเอียด</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">สิทธิอนุญาต</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">เอาออก</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">รายงาน</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">เพิ่ม %1$s หรือไม่?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ขอสิทธิอนุญาตเพิ่มเติม</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">ส่วนขยายต้องการสิทธิอนุญาตจากคุณเพื่อ:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ส่วนเสริมต้องการ:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">เพิ่ม</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">อนุญาต</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">ปฏิเสธ</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">ยกเลิก</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">ติดตั้งส่วนเสริม</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">ติดตั้ง %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">ยกเลิก</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">บทวิจารณ์: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">คะแนน: %1$.02f จาก 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ส่วนเสริม</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ตัวจัดการส่วนเสริม</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">ส่วนเสริมถูกปิดใช้งานชั่วคราว</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">ส่วนขยายถูกปิดใช้งานชั่วคราว</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">มีส่วนเสริมอย่างน้อยตัวหนึ่งหยุดทำงาน ทำให้ระบบของคุณไม่เสถียร</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">มีส่วนขยายอย่างน้อยตัวหนึ่งหยุดทำงาน ทำให้ระบบของคุณไม่เสถียร</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">เริ่มส่วนเสริมใหม่</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">เริ่มส่วนขยายใหม่</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">ค้นหาส่วนเสริมเพิ่มเติม</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">ค้นหาส่วนขยายเพิ่มเติม</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">อนุญาต</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">ปฏิเสธ</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s มีการอัปเดตใหม่</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">ต้องมี %1$d สิทธิอนุญาตใหม่</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ต้องมีสิทธิอนุญาตใหม่</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">การอัปเดตส่วนเสริม</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">การอัปเดตส่วนขยาย</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">รองรับเครื่องมือตรวจสอบส่วนเสริม</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">ส่วนเสริมใหม่ที่ใช้งานได้</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">ส่วนเสริมใหม่ที่ใช้งานได้</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">เพิ่ม %1$s ไปยัง %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">เพิ่ม %1$s และ %2$s ไปยัง %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">เพิ่มไปยัง %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">เทคโนโลยีส่วนเสริมของ Firefox กำลังถูกปรับปรุงให้ทันสมัย ส่วนเสริมเหล่านี้ใช้เฟรมเวิร์กที่เข้ากันไม่ได้กับ Firefox 75 รวมถึงรุ่นที่ต่ำกว่า</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">เรากำลังสร้างการรองรับสำหรับการเลือกขั้นแรกของส่วนขยายที่แนะนำ</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">กำลังดาวน์โหลดและยืนยันส่วนเสริม…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">กำลังดาวน์โหลดและยืนยันส่วนขยาย…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">ไม่สามารถสอบถามส่วนเสริมได้!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">ไม่สามารถสอบถามส่วนขยายได้!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">ไม่พบคำแปลสำหรับภาษา %1$s และภาษาเริ่มต้น %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">ติดตั้ง %1$s สำเร็จแล้ว</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">ไม่สามารถติดตั้ง %1$s ได้</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">ไม่สามารถติดตั้งส่วนเสริมนี้ได้</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">ไม่สามารถติดตั้งส่วนขยายนี้ได้</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">ไม่สามารถดาวน์โหลดส่วนเสริมนี้เนื่องจากการเชื่อมต่อล้มเหลว</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">ไม่สามารถดาวน์โหลดส่วนขยายนี้เนื่องจากการเชื่อมต่อล้มเหลว</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">ไม่สามารถติดตั้งส่วนเสริมนี้เนื่องจากส่วนเสริมดูเหมือนจะเสียหาย</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">ไม่สามารถติดตั้งส่วนขยายนี้เนื่องจากส่วนขยายดูเหมือนจะเสียหาย</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">ไม่สามารถติดตั้งส่วนเสริมนี้เนื่องจากส่วนเสริมไม่ได้รับการยืนยัน</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">ไม่สามารถติดตั้งส่วนขยายนี้เนื่องจากส่วนขยายไม่ได้รับการยืนยัน</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">ไม่สามารถติดตั้ง %1$s เนื่องจากเข้ากันไม่ได้กับ %2$s %3$s</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">ไม่สามารถติดตั้ง %1$s เนื่องจากมีความเสี่ยงสูงที่จะก่อให้เกิดปัญหาด้านเสถียรภาพหรือความปลอดภัย</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">เปิดใช้งาน %1$s สำเร็จแล้ว</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">ไม่สามารถเปิดใช้งาน %1$s ได้</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">ปิดใช้งาน %1$s สำเร็จแล้ว</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">ไม่สามารถปิดใช้งาน %1$s ได้</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">ถอนการติดตั้ง %1$s สำเร็จแล้ว</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">ไม่สามารถถอนการติดตั้ง %1$s ได้</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">เอา %1$s ออกสำเร็จแล้ว</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">ไม่สามารถเอา %1$s ออกได้</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">ส่วนเสริมนี้ถูกโอนย้ายจาก %1$s รุ่นก่อนหน้า</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 ส่วนเสริม</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 ส่วนขยาย</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s ส่วนเสริม</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s ส่วนขยาย</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">เรียนรู้เพิ่มเติม</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">อัปเดตเรียบร้อย</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">ไม่มีการอัปเดต</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">ข้อผิดพลาด</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">ข้อมูลการอัปเดต</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ความพยายามครั้งล่าสุด:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">สถานะ:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ได้ถูกเพิ่มไปยัง %2$s แล้ว</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">เปิดในเมนู</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">เข้าถึง %1$s ได้จากเมนู %2$s</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">ตกลง เข้าใจแล้ว</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">ตกลง</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">เรียนรู้เพิ่มเติม</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s ถูกปิดใช้งานเนื่องจากปัญหาด้านความปลอดภัยหรือความเสถียร</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s ไม่สามารถยืนยันได้ว่าปลอดภัยและได้ถูกปิดใช้งาน</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s เข้ากันไม่ได้กับ %2$s รุ่นของคุณ (รุ่น %3$s)</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..6c2962f6ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-tl/strings.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Basahin at baguhin ang mga setting ng privacy</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">i-Access ang iyong data para sa lahat ng website</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">i-Access ang iyong data sa %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">i-Access ang iyong data sa mga site na nasa domain na %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">i-Access ang iyong data para sa 1 pang site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">i-Access ang iyong data sa %1$d pang mga site</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">i-Access ang iyong data sa 1 pang domain</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">i-Access ang iyong data sa %1$d pang mga domain</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">i-Access ang mga browser tab</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Mag-imbak ng client-side data na hindi limitado ang bilang</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">i-Access ang aktibidad ng iyong browser habang naglilibot</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Basahin at baguhin ang mga bookmark</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Basahin at baguhin ang mga settings ng browser</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Alisin ang browsing history, mga cookie, at mga kaugnay na data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Kunin ang data mula sa clipboard</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Maglagay ng data sa clipboard</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">I-block ang nilalaman sa anumang pahina</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Mag-download ng mga file at basahin at baguhin ang kasaysayan ng mga download ng browser</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Buksan ang mga file na na-download sa iyong device</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Basahin ang sulat sa lahat ng tab na naka-bukas</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">I-access ang iyong lokasyon</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Tignan ang browsing history</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Subaybayan ang paggamit ng extension at pag-manage sa mga tema</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Makipagpalit ng mga mensahe sa mga app bukod dito sa isang ito</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Ipakita sayo ang mga abiso</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Magbigay ng mga cryptographic authentication service</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kontrolin ang mga browser proxy setting</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">I-access ang mga kasasara lang na mga tab</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Itago at ipakita ang mga browser tab</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">I-access ang kasaysayan ng pag-browse</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">i-Extend ang mga developer tool para ma-access sa iyong data sa mga nakabukas na tab</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Bersyon</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">May-akda</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Huling na-update</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Homepage</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Alamin ang tungkol sa mga permiso</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Marka</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Mga Setting</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Nakabukas</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Nakasara</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Payagan sa private browsing</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Patakbuhin sa private browsing</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Pinagana</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Hindi pinagana</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Naka-install</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Inirerekomenda</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Hindi pa suportado</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Hindi pa pwede</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Hindi pinagagana</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Mga Detalye</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Mga Pahintulot</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Alisin</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Idagdag ang %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Nangangailangan ito ng iyong pahintulot na:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Idagdag</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Kanselahin</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">I-install ang Add-on</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Kanselahin</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Mga Review: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Mga Add-on</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Add-ons Manager</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Payagan</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Tanggihan</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Ang %1$s ay may bagong update</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">May %1$d bagong permisong kinakailangan</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">May bagong permisong kinakailangan</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Mga update ng Add-on</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Tagasuri ng mga suportadong add-on</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">May mga bagong dagdag na add-on</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Mga bagong add-on na maaaring gamitin</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Idagdag ang %1$s sa %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Idagdag ang %1$s at %2$s sa %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Idagdag sila sa %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Nagiging makabago na ang Firefox add-on technology. Ang mga add-on na ito ay gumagamit ng mga framework na hindi na gumagana sa Firefox 75 at higit pa.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Kasalukuyan kaming bumubuo ng suporta para sa piling seleksyon ng Mga Inirerekomendang Extension.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Dina-download at vine-verify ang add-on…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Bigong mag-query ng mga Add-on!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Hindi natagpuan ang pagsalin para sa locale na %1$s o sa wikang %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Matagumpay na na-install ang %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Hindi nag-install ang %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Matagumpay na na-install ang %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Bigong ma-enable ang %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Matagumpay na na-disable ang %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Bigong ma-disable ang %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Matagumpay na na-uninstall ang %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Bigong ma-uninstall ang %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Matagumpay na natanggal ang %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Bigong tanggalin ang %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ang add-on na ito ay na-migrate mula sa lumang bersyon ng %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s add-on</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Alamin</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Matagumpay na na-update</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Walang update na available</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Impormasyon sa updater</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Huling pagtatangka:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Katayuan:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">Naidagdag na ang %1$s sa %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Buksan ito sa menu</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">OK, Nakuha ko</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..bd68734184
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-tr/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Gizlilik ayarlarını okuma ve değiştirme</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Tüm web sitelerine ait verilerinize erişme</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s verilerinize erişme</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s alan adındaki sitelere ait verilerinize erişme</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Diğer 1 sitedeki verilerinize erişme</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Diğer %1$d sitedeki verilerinize erişme</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">1 diğer alan adındaki verilerinize erişme</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">%1$d diğer alan adındaki verilerinize erişme</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d/%3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Tarayıcı sekmelerine erişme</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">İstemci tarafında sınırsız miktarda veri depolama</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Gezinti sırasında tarayıcı etkinliğine erişme</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Yer imlerini okuma ve değiştirme</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Tarayıcı ayarlarını okuma ve değiştirme</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Gezinti geçmişini, çerezleri ve ilgili verileri temizleme</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Panodaki verileri alma</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Panoya veri gönderme</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Herhangi bir sayfadaki içeriği engelleme</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Gezinti geçmişinizi okuma</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Dosya indirme, tarayıcının indirme geçmişini okuma ve değiştirme</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Cihazınıza indirilen dosyaları açma</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Tüm açık sekmelerdeki metinleri okuma</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Konumunuza erişme</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Gezinti geçmişine erişme</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Eklenti kullanımını izleme ve temaları yönetme</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Bu uygulama dışındaki uygulamalarla mesaj alışverişi yapma</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Size bildirim gösterme</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kriptografik kimlik doğrulama hizmetleri sağlama</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Tarayıcının vekil sunucu ayarlarını yönetme</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Son kapatılan sekmelere erişme</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Tarayıcı sekmelerini gizleme ve gösterme</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Gezinti geçmişine erişme</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Açık sekmelerdeki verilere erişmek için geliştirici araçlarını genişletme</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Sürüm</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Geliştiren</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Yazarlar</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Son güncelleme</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Ana sayfa</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">İzinler hakkında daha fazla bilgi alın</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Puan</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Bu eklenti hakkında daha fazla bilgi</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Bu uzantı hakkında daha fazla bilgi</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Ayarlar</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Açık</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Kapalı</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Gizli gezintide izin ver</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Gizli gezintide çalıştır</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Gizli pencerelerde izin verilmiyor</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Etkin</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Devre dışı</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Yüklendi</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Önerilen</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Henüz desteklenmiyor</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Henüz mevcut değil</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Devre dışı</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Ayrıntılar</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">İzinler</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Kaldır</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Şikâyet et</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s eklensin mi?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s ek izinler istiyor.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Aşağıdaki izinleri vermenizi istiyor:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">İstenenler:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Ekle</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">İzin ver</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Reddet</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">İptal</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Eklentiyi yükle</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$s eklentisini yükle</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">İptal</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">İnceleme: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Puan: 5 üzerinden %1$.02f</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Eklentiler</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Eklenti yöneticisi</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Eklentiler geçici olarak devre dışı bırakıldı</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Uzantılar geçici olarak devre dışı bırakıldı</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Bir veya daha fazla eklenti çalışmayı durdurdu ve sisteminizi kararsız duruma getirdi.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Bir veya daha fazla uzantı çalışmayı durdurdu ve sisteminizi kararsız duruma getirdi.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Eklentileri yeniden başlat</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Uzantıları yeniden başlat</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Daha fazla eklenti bul</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Daha fazla uzantı bul</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">İzin ver</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Reddet</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Yeni bir %1$s güncellemesi var</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d yeni izin gerekiyor</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Yeni bir izin gerekiyor</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Eklenti güncellemeleri</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Uzantı güncellemeleri</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Desteklenen eklenti kontrolü</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Yeni eklenti mevcut</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Yeni eklentiler mevcut</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s eklentisini %2$s tarayıcısına ekle</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s ve %2$s eklentilerini %3$s tarayıcısına ekle</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Eklentileri %1$s tarayıcısına ekle</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox eklenti teknolojisi modernleşiyor. Bu eklentiler Firefox 75 ve sonrasıyla uyumlu olmayan kodlar kullanıyor.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Önerdiğimiz eklentilerden bazılarını desteklemek için çalışmalara başladık.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Eklenti indiriliyor ve doğrulanıyor…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Uzantı indiriliyor ve doğrulanıyor…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Eklentiler sorgulanamadı.</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Uzantılar sorgulanamadı.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">%1$s veya %2$s çeviri bulunamadı</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s başarıyla yüklendi</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s yüklenemedi</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Bu eklenti yüklenemedi.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Bu uzantı yüklenemedi.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Bağlantı hatası nedeniyle bu eklenti indirilemedi.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Bağlantı hatası nedeniyle bu uzantı indirilemedi.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Bu eklenti bozuk göründüğü için yüklenemedi.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Bu uzantı yüklenemedi çünkü görünüşe göre uzantı bozuk.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Bu eklenti doğrulanmadığı için yüklenemiyor.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Bu uzantı doğrulanmadığı için yüklenemiyor.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s eklentisi %2$s %3$s ile uyumsuz olduğu için yüklenemedi.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s yüklenemedi çünkü kararsızlık veya güvenlik sorunlarına yol açma riski yüksek.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s başarıyla etkinleştirildi</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s etkinleştirilemedi</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s başarıyla etkisizleştirildi</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s etkisizleştirilemedi</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s başarıyla kaldırıldı</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s kaldırılamadı</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s başarıyla kaldırıldı</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s kaldırılamadı</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Bu eklenti eski bir %1$s sürümünden taşındı</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 eklenti</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 uzantı</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s eklenti</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s uzantı</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Daha fazla bilgi al</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Başarıyla güncellendi</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Hiç güncelleme yok</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Hata</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Güncelleyici Bilgileri</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Son deneme:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Durum:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s %2$s tarayıcınıza eklendi</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Menüden açabilirsiniz</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">%1$s eklentisine %2$s menüsünden erişebilirsiniz.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Tamam</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">Tamam</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Daha fazla bilgi alın</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s güvenlik veya kararlılık gerekçesiyle devre dışı bırakıldı.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s eklentisinin güvenliği doğrulanamadığı için eklenti devre dışı bırakıldı.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s sizin %2$s sürümünüzle (sürüm %3$s) uyumlu değil.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..1787da8bce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-trs/strings.xml
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Gahiā nī nadunā dàj gā riña aché nun huìt</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Gatū riña nej si datôt guendâ daran\’ nej sîtio</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Gatū riña nej si datôt guendâ %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Gatū riña nej si datôt guendâ daran’ nej sitio nū riña %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Gatū riña nej si datôt riña 1 a’ngô sîtio</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Gatū riña nej si datôt riña %1$d a’ngô nej sîtio</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Gatū riña nej si datôt riña 1 a’ngô dominio</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Gatū riña nej si datôt riña %1$d a’ngô nej dominio</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Gatū riña nej rakïj ñanj huā \'iaj sa riñā nana\'uî\'t</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Nà\'nïnj sà\' dàj nìko datô garan\' ruhuât ngà cliente</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Gatū riña sun \'iaj navegador nga aché nunt</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Gahiā nī nādunā dàj gā nej markador</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Gahiā nī nādunā dàj gā riña navegador nīkājt</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Nadure’ nej sa gahuin nga gaché nunt, nej cookies, nī nej dâto</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Gīrì\' nej dato āsìj riña portapapel</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Gachrūn nej dâto riña portapapel</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Nārán riña nej nuguan’ nan riña ahuin mān’an pâjina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Ni’hiāj riña nej hiūj gaché nunt</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Nādunïnj nej archivo nu gāhiāt nī nāgi’iát riña nej sa gahuin ngà nadunïnjt riña navegador</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Nā\'nïn nej archivo nadunïnjt riña si agâ\'t</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Gāhiā nūguan\'àn riña daran\' rakïj ñanj huā nî’nïnj ïn</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Gātū riña nūnt</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Gātū riña sa gahuin nga gaché nunt</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Ni’iāj nī dūgumînt dàj \'iaj sun extensión nī dūgumîn nej tema</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Nādunā dūguì\' nuguan\'an ngà a\'ngô nej aplikasiûn huāa</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Gā rangà\' nej sa huāa gīni\'iājt</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Gā\'uì\' nej servisio nagi\'iaj hīa gù\'nàj kriptografika</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Gīni\'iaj dàj gā si proxy riña sā aché nu\'</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Gātū riña nej rakïj ñanj ārán hìaj nâ’nïnj nakàt</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Gāchrī huì\' nī dīgûn\' rakïj ñanj riñā aché nu\'</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Gātū riña sa gahuin nga gaché nunt</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Nāgi\'iaj nìko nej si rasun desarrollador da\' ga\'ue gātūt riña nej si datôt riña rakïj ñanj huā nî\'nïnj \'iát</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Bersiûn</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Nej sí girirà</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Sa nagi\'iaj nākà rūkù nïn\'t</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pajinâ ayi\'ì\'</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Gāhuin chrūn doj rayi\'î nej sa achín nì\'iô\'</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Danè\' nu man</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Nāgi\'iô\'</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Nāchrūn</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Dūnâ\’àj</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Gā\'nïn riña gāchē nu huìt</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Dūgi\'aj sun man riña aché nu huìt</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ngà \'iaj sunj</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Nitāj si \’iaj sunj</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Ngà gatûj man</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Sā sà\'a huin ânj</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Nitāj si aran\' dugui\'ij ngà nej sa huāa</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Huā nïn\' nitāj si huaj</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Nitāj si \'iaj sunj</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Doj sa huāa</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Sa gāchinj nì\'iô\'</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Nādure\'</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">¿Nutàt %1$s aj?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Gīni’ñan dàj gātājt guendâ:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Nūtà\'</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Duyichin\'</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Gā\'nïnj sa nūtâ\'t riñaj</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Dūyichin\'</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Nuguan’ huā rayi’ij: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Sa gā\'ue nūtò\'</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Sa nīkāj ñu\'ūnj nej sa gā\'ue nūtò\'</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Gā\'nïn</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Sī ga\'nïnjt</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ngà ga\'ue nāhuin nākà ñunj</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Ruhuaj %1$d sa gāchìnj nì\'iaj nākàt</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Ruhuaj \'ngō sa gāchìnj nì\'iaj nākàt</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Nej sa nutâ\'t gā\'ue nāgi\'iaj nākàt</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Sa natsij dàj gāran\' nej sa nutâ\'t</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Ngà huā \'ngō sa nūtò\' nākà doj</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Ngà huā nej sa nūtò\' nākà doj</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Nūtà\' %1$s riña %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Nūtà\' %1$s nī %2$s riña %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Nūtà\' riña %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Nahuin nākà daran\' sa ga\'ue nūtò\' nīkāj Firefox. Nitāj si aran\' nej sa nūtò\' nan ngà nej dūguì\' Firefox 75 nī sa gan\'ānj doj.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Akuan\' nïn nī hìaj ri nej ñunj \'ngō yi\'nïn\' li nej sa nūtò\' ni\'ñānj doj.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Hìaj naduni nī natsi sa nata\' naguit…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Nu ga\'ue natsi nej sa nūtò\'.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Nu nāri’ìj nuguan’ nañû, guendâ nāgi’hiát sa huā riña %1$s nī gunedâ nādunā nânj ngà ‘na’ nīñā %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Ngà huā %1$s gārasunt</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Nu ga\'ue nāñùn %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic">Nu gā\'hue nāñùn sa nata\' nan.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error">Nu nāninj sa nutà\'t nan ruhuât dadin\' nitāj conexión hua.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error">Sa nūtà\'t ruhuât nan nī nu nàtaj dadin\' rû\' huaj si huā a\'nan\' man huaj.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error">Sa nūtàt\' ruhuât nan nī nu gā\'hue nàtaj \'hiaj si hìaj natsij aga\' nan man.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Ngà huā yūgui %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Nu ga\'ue nānùn %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Nitāj si hūaj gi\'iaj sun %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Nu ga\'ue nāránt riñanj %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Ngà gahui %1$s riña si āgâ\'t</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Nu ga\'ue gāhuī %1$s riña si āgâ\'t</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Ngà ganare\' %1$s riña si āgâ\'t</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Nu ga\'ue gānārè\' %1$s riña si āgâ\'t</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Riña sa gō doj %1$s ga\'na\' sa nata\' nan.</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 sa ga\'ue nūtò\'</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s nej sa ga\'ue nūtò\'</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Gāhuin chrūn doj</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Ngà nahuin nākà man</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nitāj si hūaj nāhuin nākàj</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Huā sa gahui a\'nan\'</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Nuguan\' huā rayi\'î sa nāhuin nākà nan</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Sa rūkù ginûn huin:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Dàj hua riñaj:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s ngà nañû riña %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Nā\'nïn riña mēnû</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Hīaj, ngà ga\'ue</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..4ddbddf314
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-tt/strings.xml
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Хосусыйлык көйләүләрен уку һәм үзгәртү</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Барлык сайтлар өчен булган мәгълүматыгызга ирешү</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s сайты өчен булган мәгълүматыгызга ирешү</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s доменындагы сайтлар өчен булган мәгълүматыгызга ирешү</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Башка 1 сайтта булган мәгълүматыгызга ирешү</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Башка %1$d сайтта булган мәгълүматыгызга ирешү</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Башка 1 домендагы мәгълүматыгызга ирешү</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Башка %1$d домендагы мәгълүматыгызга ирешү</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Браузер табларына ирешү</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Клиент ягында чиксез күләмдә мәгълүмат саклау</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Навигация вакытында браузер гамәлләренә ирешү</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Кыстыргычларны уку һәм үзгәртү</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Браузер көйләүләрен уку һәм үзгәртү</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Соңгы гизү тарихын, кукиларны һәм шуңа бәйле мәгълүматны чистарту</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Алмаш буферыннан мәгълүмат алу</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Мәгълүматны алмаш буферына кую</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Теләсә нинди биттәге эчтәлекне блоклау</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Гизү тарихыгызны уку</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Файлларны йөкләп алу һәм браузерның йөкләү тарихын уку һәм үзгәртү</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Җиһазыгызга йөкләп алынган файлларны ачу</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Барлык ачык табларның текстын уку</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Урнашкан урыныгызга ирешү</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Гизү тарихына ирешү</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Киңәйтүләр кулланышын контрольдә тоту һәм темалар белән идарә итү</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Бу кушымтадан тыш башка кушымталар белән хәбәрләр алмашу</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Сезгә искәртүләр күрсәтү</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Криптографик аутентификация хезмәтләрен тәкъдим ителсен</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Браузерның прокси көйләүләре белән идарә итү</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Күптән түгел ябылган табларга ирешү</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Браузер табларын яшерү һәм күрсәтү</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Гизү тарихына ирешү</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ачык таблардагы мәгълүматыгызга ирешү өчен җитештерүче коралларын киңәйтү</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Версия</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Авторлар</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Соңгы яңартылу</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Баш бит</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Рөхсәтләр турында күбрәк белү</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Рейтинг</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Көйләүләр</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Кабынган</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Сүнгән</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Хосусый гизү режимында рөхсәт итү</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Хосусый гизү режимында эшләтү</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Кабызылган</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Cүндерелгән</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Урнаштырылган</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Киңәш ителгән</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Әлегә танылмый</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Әле урнаштырылмаган</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Cүндерелгән</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Нечкәлекләр</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Рөхсәтләр</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Бетерү</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s өстәлсенме?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Түбәндәге гамәлләргә рөхсәт сорый:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Өстәү</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Баш тарту</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Кушымчаны урнаштыру</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Баш тарту</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Фикерләр: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Кушымчалар</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Кушымчалар менеджеры</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Рөхсәт итү</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Кире кагу</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s өчен яңа яңарту бар</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d яңа рөхсәтләр таләп ителә</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Яңа рөхсәт кирәк</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Кушымча яңартулары</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Кулланырга мөмкин кушымчаларны тикшерү коралы</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Яңа кушымча бар</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Яңа кушымчалар бар</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%2$s программасына %1$s кушымчасын өстәү</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%3$s программасына %1$s һәм %2$s кушымчаларын өстәү</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Аларны %1$s программасына өстәү</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Кушымчаны йөкләп алу һәм тикшерү…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Кушымчаларны сорап алып булмады!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s уңышлы урнаштырылды</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s кушымчасын урнаштырып булмады</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s уңышлы кабызылды</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s кушымчасын кабызып булмады</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s уңышлы сүндерелде</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s кушымчасын сүндереп булмады</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s уңышлы алып ташланды</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s кушымчасын бетереп булмады</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s уңышлы бетерелде</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s кушымчасын бетереп булмады</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Бу кушымча %1$s программасының искерәк версиясеннән алынды</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 кушымча</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s кушымчалар</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Күбрәк белү</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Уңышлы яңартылды</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Яңартулар юк</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Хата</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Яңарту турында мәгълүмат</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Соңгы омтылыш:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Статус:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%2$s прораммасына %1$s кушымчасы өстәлде</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Менюда ачу</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Яхшы, аңладым</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..aa91ee70be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Kcem ɣer isefka-nnek i %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Kcem ɣer isefka-nnek/m ɣef 1 usit yaḍen</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Kcem ɣer isefka-nnek/m ɣef %1$d yisiten yaḍenin</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Kcem ɣer isefka-nnek/m ɣef 1 yiger yaḍen</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Kcem ɣer isefka-nnek/m ɣef %1$d yigeran yaḍenin</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Rẓem afaylu ittwagmen ɣer wallal-nnek</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Imgayen</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Tizmilin</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Tisɣal</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Kkes</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Rnu %1$s?</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Rnu</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">ssureg</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ɣer-s aleqqem amaynu</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Rnu %1$s ɣer %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Rnu %1$s d %2$s ɣer %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Rnu-ten ɣer %1$s</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Ur tettwaf tsuɣilt, i tdigant %1$s ula i tutlayt tamezwert %2$s</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Isin uggar</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Azgal</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Addad:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Rẓem-t g wumuɣ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..3eb7b589f3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ug/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">شەخسىيەت قۇرۇلمىسىنى ئوقۇش ۋە ئۆزگەرتىش </string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">ھەممە تور بېكەتلەردىكى سانلىق مەلۇماتلىرىڭىزغا ئېرىشىدۇ</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s ئۈچۈن سانلىق مەلۇماتلىرىڭىزنى زىيارەت قىلىدۇ</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">بۇ %1$s دائىرىدىكى تور بېكەت ئۈچۈن سانلىق مەلۇماتلىرىڭىزنى زىيارەت قىلىدۇ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">باشقا بىر تور بېكەتتىكى سانلىق مەلۇماتلىرىڭىزنى زىيارەت قىلىدۇ</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">باشقا %1$d تور بېكەتتىكى سانلىق مەلۇماتلىرىڭىزنى زىيارەت قىلىدۇ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">باشقا بىر دائىرىدىكى سانلىق مەلۇماتلىرىڭىزنى زىيارەت قىلىدۇ</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">باشقا %1$d دائىرىدىكى سانلىق مەلۇماتلىرىڭىزنى زىيارەت قىلىدۇ</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s، %2$d / %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">توركۆرگۈ بەتكۈچلىرىنى زىيارەت قىلىش</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">چەكسىز خېرىدار تەرەپ سانلىق مەلۇماتىنى ساقلايدۇ</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">زىيارەت جەريانىدا توركۆرگۈ پائالىيەتلىرىگە ئېرىشەلەيدۇ</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">خەتكۈچلەرنى ئوقۇش ۋە ئۆزگەرتىش</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">توركۆرگۈ تەڭشىكىنى ئوقۇش ۋە ئۆزگەرتىش</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">يېقىنقى كۆرۈش تارىخى ، ساقلانمىلار ۋە مۇناسىۋەتلىك سانلىق مەلۇماتلارنى تازىلاش</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">چاپلاش تاختىسىدىن سانلىق مەلۇماتقا ئېرىشىش</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">چاپلاش تاختىسىغا سانلىق مەلۇمات كىرگۈزۈش</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">ھەر قانداق بەتتىكى مەزمۇننى چەكلەش</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">زىيارەت تارىخىڭىزنى ئوقۇيدۇ</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">ھۆججەت چۈشۈرىدۇ ھەمدە توركۆرگۈنىڭ چۈشۈرۈش تارىخىنى ئوقۇپ ۋە ئۆزگەرتىدۇ</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">ئۈسكۈنىڭىزگە چۈشۈرۈلگەن ھۆججەتلەرنى ئېچىش</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">بارلىق ئوچۇق بەتكۈچلەرنىڭ تېكىستىنى ئوقۇش</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">ئورنىڭىزغا ئېرىشىدۇ</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">توركۆرگۈ تارىخىغا ئېرىشىدۇ</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">كېڭەيتىلمە ئىشلىتىلىشىنى كۆزىتىدۇ ۋە ئۆرنەك باشقۇرىدۇ</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">باشقا ئەپ بىلەن ئۇچۇر ئالماشتۇرىدۇ</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">سىزگە ئۇقتۇرۇشنى كۆرسىتىدۇ</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">شىفىرلىق دەلىللەش مۇلازىمىتى بىلەن تەمىنلىسۇن</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">توركۆرگۈ ۋاكالەتچى تەڭشىكىنى باشقۇرۇش</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">يېقىندا يېپىلغان بەتكۈچلەرگە ئېرىشىدۇ</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">توركۆرگۈ بەتكۈچىنى يوشۇرىدۇ ۋە كۆرسىتىدۇ</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">توركۆرگۈ تارىخىغا ئېرىشىدۇ</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">ئىجادكارلار قورالى سىز ئاچقان بەتكۈچتىكى سانلىق مەلۇماتلارنى زىيارەت قىلالايدۇ</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">نەشرى</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">يازغۇچى</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">ئاپتورلار</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">ئاخىرقى قېتىم يېڭىلانغان</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">باشبەت</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">ھوقۇق ھەققىدە تەپسىلاتلار</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">باھا</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">قوشۇلما ھەققىدىكى تەپسىلات</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">كېڭەيتمە ھەققىدىكى تەپسىلات</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">تەڭشەكلەر</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">ئوچۇق</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">تاقاق</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">شەخسىي زىيارەت ھالىتىگە يول قويىدۇ</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">شەخسىي زىيارەت ھالىتىنى قوزغىتىدۇ</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">شەخسىي كۆزنەكتە ئىجرا قىلىشقا يول قويمايدۇ</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">قوزغىتىلدى</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">چەكلەندى</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">قاچىلاندى</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">تەۋسىيەلەندى</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">تېخى قوللىمايدۇ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ھازىرچە ئىشلەتكىلى بولمايدۇ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">چەكلەندى</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">تەپسىلاتى</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">ئىجازەتلەر</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">چىقىرىۋىتىش</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">پاش قىلىش</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title"> %1$s نى قوشامسىز؟</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s قوشۇمچە ئىجازەت تەلەپ قىلىدۇ.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">تۆۋەندىكى ھوقۇقلارنى تەلەپ قىلىدۇ:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">ئۇنىڭ ئۈمىدى:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">قوشۇش</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">يول قوي</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">رەت قىل</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">بىكار قىلىش</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">قىستۇرما قاچىلاش</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">%1$sنى ئورنات</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">بىكار قىلىش</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">باھا: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">باھالاش: %1$.02f (تولۇق 5 نومۇر)</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">قىستۇرمىلار</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">قىستۇرما باشقۇرغۇچ</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">قوشۇلما ۋاقىتلىق چەكلەنگەن</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">كېڭەيتمە ۋاقىتلىق چەكلەنگەن</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">بىر ياكى بىر قانچە قوشۇلما ئىشلەشتىن توختاپ، سىستېمىڭىزنى مۇقىمسىزلاشتۇردى.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">بىر ياكى بىر قانچە كېڭەيتمە ئىشلەشتىن توختاپ، سىستېمىڭىزنى تۇراقسىزلاشتۇردى.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">قوشۇلمىنى قايتا قوزغات</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">كېڭەيتمىنى قايتا قوزغىتىش</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">تېخىمۇ كۆپ قوشۇلما ئىزدە</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">تېخىمۇ كۆپ كېڭەيتمە ئىزدە</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">رۇخسەت قىلىش</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">رەت قىلىش</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title"> %1$sنىڭ يېڭىلانمىسى بار</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate"> %1$dدانە يېڭى ھوقۇق تەلەپ قىلىدۇ</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">يېڭى ئىجازەت تەلەپ قىلىنىدۇ</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">قىستۇرما يېڭىلانمىلىرى</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">كېڭەيتىلمە يېڭىلانمىسى</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">قوللايدىغان قوشۇلمىلارنى تەكشۈرگۈچ</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">يېڭى قىستۇرما بار</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">يېڭى قوشۇلما بار</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s نى %2$s غا قوش</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s ۋە %2$s نى %3$s غا قوش</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">ئۇلارنى%1$s غا قوشىدۇ</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox قوشۇلما تېخنىكىسى زامانىۋىلىشىۋاتىدۇ. بۇ قوشۇلما Firefox 75 ۋە ئۇنىڭدىن يۇقىرى نەشرىگە ماسلاشمايدىغان رامكىلارنى ئىشلىتىدۇ.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">بىز ھازىر تەۋسىيە قىلىنغان كېڭەيتىلمىلەرنىڭ دەسلەپكى تاللىشىنى قوللاشقا تىرىشىۋاتىمىز.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">قىستۇرما چۈشۈرۈپ دەلىللىنىۋاتىدۇ…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">كېڭەيتىلمە چۈشۈرۈپ ۋە دەلىللەۋاتىدۇ…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">قىستۇرما ئىزدەش مەغلۇب بولدى!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">كېڭەيتىلمىنى سۈرۈشتۈرەلمىدى!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">يەرلىك تىل %1$s ياكى كۆڭۈلدىكى تىل %2$s نىڭ تەرجىمە ئۇچۇرلىرى تېپىلمىدى</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed"> %1$s مۇۋەپپەقىيەتلىك قاچىلاندى</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install"> %1$s نى ئورنىتىش مەغلۇپ بولدى</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">بۇ قوشۇلمىنى ئورنىتالمىدى.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">بۇ كېڭەيتىلمىنى ئورنىتالمىدى.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">باغلىنالمىغانلىق سەۋەبىدىن بۇ قوشۇلمىنى چۈشۈرەلمەيدۇ.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">بۇ كېڭەيتىلمىنى چۈشۈرەلمەيدۇ چۈنكى باغلىنالمىدى.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">بۇ قوشۇلمىنى ئورنىتالمايدۇ چۈنكى ئۇ بۇزۇلغاندەك تۇرىدۇ.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">بۇ كېڭەيتىلمىنى ئورنىتالمايدۇ چۈنكى ئۇ بۇزۇلغاندەك قىلىدۇ.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">بۇ قوشۇلمىنى ئورنىتالمايدۇ چۈنكى ئۇ دەلىللەنمىگەن.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">بۇ كېڭەيتىلمىنى ئورنىتالمايدۇ چۈنكى ئۇنى دەلىللىيەلمىدى.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s ئورنىتالمايدۇ چۈنكى ئۇ %2$s %3$s بىلەن ماسلاشمايدۇ.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s ئورنىتالمايدۇ چۈنكى ئۇنىڭ مۇقىملىق ياكى بىخەتەرلىق مەسىلىسى كەلتۈرۈپ چىقىرىش خەۋپى يۇقىرى.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled"> %1$sمۇۋەپپەقىيەتلىك قوزغىتىلدى</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable"> %1$s نى قوزغىتىش مەغلۇپ بولدى</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled"> %1$sمۇۋەپپەقىيەتلىك چەكلەندى</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s نى چەكلىيەلمىدى</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s نى مۇۋەپپەقىيەتلىك ئۆچۈرۈۋەتتى</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s نى ئۆچۈرەلمىدى</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources"> %1$s نى مۇۋەپپەقىيەتلىك چىقىرىۋەتتى</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s نى چىقىرىۋېتەلمىدى</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">بۇ قوشۇلما %1$s نىڭ ئالدىنقى نەشرىدىن كۆچۈرۈلگەن</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 قىستۇرما</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 كېڭەيتمە</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s قىستۇرما</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s كېڭەيتىلمە</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">تەپسىلاتىنى بىلىش</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">مۇۋەپپەقىيەتلىك يېڭىلاندى</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">يېڭىلانما يوق</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">خاتالىق</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">يېڭىلاش ئۇچۇرى</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">ئاخىرقى سىناق:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">ھالىتى:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%2$s غا %1$s قوشۇلدى</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">تىزىملىكتىن ئېچىش</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">%2$s تىزىملىكىدىن %1$s نى زىيارەت قىلىڭ.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">ماقۇل</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">جەزملە</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">تەپسىلاتى</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">بىخەتەرلىك ياكى مۇقىملىق مەسىلىسى سەۋەبىدىن %1$s چەكلەنگەن.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s بىخەتەر دەپ دەلىللەنمىگەچكە چەكلەندى.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">سىز ئىشلىتىۋاتقان نەشرى %2$s (%3$s نەشرى) بىلەن %1$s ماسلاشمايدۇ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..a55634d094
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-uk/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Перегляд і редагування налаштувань приватності</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Доступ до ваших даних для всіх вебсайтів</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Отримувати доступ до ваших даних для %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Отримувати доступ до ваших даних для сайтів у домені %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Отримувати доступ до ваших даних на 1 іншому сайті</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Отримувати доступ до ваших даних на %1$d інших сайтах</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Отримувати доступ до ваших даних на 1 іншому домені</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Отримувати доступ до ваших даних на %1$d інших доменах</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d з %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Доступ до вкладок браузера</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Зберігання необмеженої кількості даних на стороні клієнта</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Доступ до активності браузера під час навігації</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Читання й зміна закладок</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Читання й зміна налаштувань браузера</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Стерти нещодавню історію перегляду, файли cookie та повʼязані дані</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Отримання даних з буфера обміну</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Збереження даних в буфер обміну</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Блокувати вміст на всіх сторінках</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Читати історію перегляду</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Завантаження файлів, а також читання й зміна історії браузера</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Відкриття файлів, завантажених на пристрій</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Читання тексту в усіх відкритих вкладках</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Доступ до вашого розташування</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Доступ до історії перегляду</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Моніторинг використання додатків і керування темами</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Обмін повідомленнями з іншими програмами</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Показ сповіщень</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Надання послуг криптографічної автентифікації</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Керування налаштуваннями проксі браузера</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Доступ до нещодавно закритих вкладок</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Приховування і показ вкладок браузера</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Доступ до історії перегляду</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Розширте інструменти розробника для доступу до своїх даних у відкритих вкладках</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Версія</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Автор</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Автори</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Востаннє оновлено</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Домівка</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Докладніше про дозволи</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Рейтинг</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Докладніше про цей додаток</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Докладніше про це розширення</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Налаштування</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Увімк.</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Вимк.</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Дозволити у приватному перегляді</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Запуск у приватному перегляді</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Не дозволено в приватних вікнах</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Увімкнено</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Вимкнено</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Встановлено</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Рекомендовано</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Ще не підтримується</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Поки що недоступно</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Вимкнено</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Подробиці</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Дозволи</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Вилучити</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Повідомити</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Додати %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s запитує додаткові дозволи.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Потребує вашого дозволу на:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Він хоче отримати дозвіл на:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Додати</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Дозволити</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Відмовити</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Скасувати</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Встановити додаток</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Встановити %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Скасувати</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Відгуків: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Оцінка: %1$.02f з 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Додатки</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Керувати додатками</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Додаток тимчасово вимкнено</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Розширення тимчасово вимкнено</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Один або кілька додатків перестали працювати, через що ваша система працює нестабільно.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Одне або кілька розширень припинили працювати, через що ваша система стала нестабільною.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Перезапустіть додатки</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Перезапустити розширення</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Знайти інші додатки</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Знайти інші розширення</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Дозволити</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Заборонити</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s має оновлення</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">Необхідно %1$d нових дозволів</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Потрібен новий дозвіл</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Оновлення додатків</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Оновлення розширень</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Перевірка підтримуваних додатків</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Доступний новий додаток</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Доступні нові додатки</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Додати %1$s до %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Додати %1$s і %2$s до %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Додати їх до %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Технологія додатків Firefox вдосконалюється. Ці додатки використовують frameworks, що несумісні з Firefox версії 75 та новішими.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Ми зараз розробляємо підтримку для початкового набору рекомендованих розширень.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Завантаження й перевірка додатка…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Завантаження та перевірка розширення…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Не вдалося здійснити запит на додатки!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Не вдалося завантажити список розширень!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Перекладу не знайдено, для локалі %1$s та типової мови %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s успішно встановлено</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Не вдалося встановити %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Не вдалося встановити цей додаток.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Не вдалося встановити це розширення.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Неможливо завантажити цей додаток через збій з’єднання.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Не вдалося завантажити це розширення через помилку зі з’єднанням.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Неможливо встановити цей додаток, тому що він виглядає пошкодженим.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Не вдалося встановити це розширення, оскільки воно, ймовірно, пошкоджене.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Неможливо встановити цей додаток, тому що він не був перевірений.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Не вдалося встановити це розширення, оскільки воно не було перевірене.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Не вдалося встановити %1$s через його несумісність із %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s неможливо встановити через високу ймовірність спричинення проблем безпеки та стабільності.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s успішно увімкнено</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Не вдалося увімкнути %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s успішно вимкнено</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Не вдалося вимкнути %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s успішно видалено</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Не вдалося видалити %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s успішно вилучено</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Не вдалося вилучити %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Цей додаток перенесено з попередньої версії %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Додаток: 1</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 розширення</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">Додатків: %1$s</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s розширень</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Докладніше</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Успішно оновлено</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Оновлень не знайдено</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Помилка</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Інформація про оновлення</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Остання спроба:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Стан:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s додано до %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Відкрити в меню</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Отримайте доступ до %1$s з меню %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Гаразд, зрозуміло</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Докладніше</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s було вимкнено, у зв’язку з проблемами безпеки чи стабільності.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s вимкнено через неможливість підтвердити його безпечність.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s несумісний із вашою версією %2$s (версія %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..371a5cb4e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-ur/strings.xml
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">رازداری کی سیٹنگز پڑھیں اور ترمیم کریں</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">تمام ویب سائٹس کے لئے اپنے کوائف تک رسائی</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s کے لیے اپنے ڈیٹا تک رسائی حاصل کریں۔</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s ڈومین میں آپ کی سائٹس کے کوائف کی رسائی حاصل کریں</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">1 دوسری سائٹ پر اپنے ڈیٹا تک رسائی حاصل کریں۔</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">%1$d دوسری سائٹ پر اپنے ڈیٹا تک رسائی حاصل کریں۔</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">1 دیگر ڈومینز پر اپنے ڈیٹا تک رسائی حاصل کریں۔</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">%1$d دیگر ڈومینز پر اپنے ڈیٹا تک رسائی حاصل کریں۔</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">براؤزر ٹیبز تک رسائی حاصل کریں</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">صارف کی طرف سے لامحدود کوائف محفوظ کریں</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">گشت کاری کے دوران برائزر کی سرگرمی کی رسائی</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">بک مارک پڑھیں اور ترمیم کریں</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">رازداری سیٹکگیں پڑھیں اور ترمیم کریں</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">حالیہ برائوزنگ سابقات، کوکیز اور اس متعلقہ کوائف صاف کریں</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">کلپ بورڈ سے کوائف حاصل کریں</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">کلپ بورڈ میں کوائف ڈالیں</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">مسل کو ڈاؤن لوڈ کریں اور پڑھیں اور براؤزرکے سابقات میں ترمیم کریں‪</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">آپ کے آلے پر ڈاؤن لوڈ کردہ فائلیں کھولیں</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">تمام کھلے ٹیبس کا متن پڑھیں</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">اپنے محل وقوع تک رسائی کریں</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">برائوزنگ سابقات تک رسائی حاصل کریں</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">توسیع کے استعمال کی نگرانی اور موضوعات کا انتظام کریں</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">اس ایپ کے علاوہ کسی دوسرے ایپ کے ساتھ پیغامات کا تبادلہ کریں</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">اطلاعات کی آپ کو نمائش کریں</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">کریپٹو گرافک تصدیق کی خدمات فراہم کریں</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">برائوزر کی پراکسی سیٹنگز کو کنٹرول کریں</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">حالیہ بند کی گئی ٹیبیں تک رسائی</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">براؤزر ٹیبز کو چھپائیں اور دکھائیں</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">براؤزنگ کی سابقات تک رسائی</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">کھلے ٹیب میں اپنے ڈیٹا تک رسائی کیلئے ڈویلپر کے اوزار کو بڑھانے</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">ورژن</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">تخليق کار</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">آخری تازہ کاری</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">ابتدائی صفحہ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">اجازتوں کے بارے میں مزید سیکھیں</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">شرح کاری</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">سیٹنگز</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">چالو</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">بند</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">نجی براؤزنگ میں اجازت دی‏‏ں؟</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">نجی براؤزنگ میں چلائیں</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">فعال شدہ</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">غیر فعال شدہ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">تنصیب شدہ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">تجویز کردہ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">ابھی تک تعاون یافتہ نہیں ہے</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">ابھی تک دستیاب نہیں</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">غیر فعال شدہ</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">تفصیلات</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">اجازتیں</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">ہٹائیں</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s شامل کریں؟</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">اس میں آپ کی اجازت کی ضرورت ہے:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">شامل کریں</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">منسوخ کریں</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">ایڈ اون انسٹاال کریں</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">منسوخ کریں</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">جائزہ:%1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">ایڈ اون</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">ایڈ اون مینیجر</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">اجازت دیں</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">انکار کریں</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s کا ایک نیا اپڈیٹ آیا ہے</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d نئی اجازتوں کی درکار ہے</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">ایک نئی اجازت کی درکار ہے</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">ایڈون کے اپڈیٹس</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">معاونت شدہ ایڈ-آن پڑتال کار</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">نی ایڈ آن دستیاب ہے</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">نئے ایڈ آن دستیاب ہیں</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$s کو %2$s میں جوڑیں</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s اور %2$s کو %3$s میں جوڑیں</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">اُنہیں %1$s میں شامل کریں</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox کی ایڈ-آن ٹیکنالوجی جدید بن رہی ہے۔ یہ ایڈ-آن ایسے فرمیورک کا استعمال کرتے ہیں جو Firefox 75 اور اسکے آگے کے ورژن کے سنگ موافق نہیں ہیں۔</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">ہم فی الحال تجویز کردہ ایکس ٹینشنز کے ابتدائی انتخاب کے لئے حمایت حاصل کر رہے ہیں۔</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">ایڈون کو ڈاؤن لوڈ اور تصدیق کر رہا ہے…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">ایڈ-آن کی دریافت کرنے میں ناکام!</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s کامیابی سے انسٹال ہوا</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s انسٹال ہونے میں ناکام رہا</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s کو کامیابی سے فعال کر دیا گیا</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s کو فعال کرنے میں ناکام رہا</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s کو کامیابی سے غیر فعال کر دیا گیا</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s کو غیر فعال کرنے میں ناکام رہا</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s کو کامیابی سے نا تنصیب کر دیا گیا</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s کو نا تنصیب کرنے میں ناکام رہا</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s کو کامیابی سے ہٹا دیا گیا</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s کو ہٹانے میں ناکام رہا</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">اس ایڈ-آن کو %1$s کے پچھلے ورژن سے منتقل کیا گیا تھا</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 ایڈ اون</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s ایڈ اون</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">مزید سیکھیں</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">کامیابی کے ساتھ اپڈیٹ ہوا</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">کوئی اپڈیٹ موجود نہیں ہے</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">نقص</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">اپڈیٹر کی معلومات</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">آخری کوشش:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">حالت:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s کا %2$s میں اظافہ کر دیا گیا ہے۔</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">اسے مینو میں کھولیں</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">ٹھیک ہے، سمجھ آگیا</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..23e4c581a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-uz/strings.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Maxfiylik sozlamalarini oʻqish va oʻzgartirish</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Barcha saytlarga maʼlumotlaringizdan foydalanishga ruxsat berish</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">%1$s uchun maʼlumotlaringizdan foydalanishga ruxsat berish</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">%1$s domenidagi saytlar uchun maʼlumotlardan foydalanishga ruxsat berish</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">1 ta boshqa qurilmadagi maʼlumotlardan foydalanishga ruxsat berish</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">%1$d ta boshqa qurilmadagi maʼlumotlardan foydalanishga ruxsat berish</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">1 ta boshqa domendagi maʼlumotlardan foydalanishga ruxsat berish</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">%1$d ta boshqa domendagi maʼlumotlardan foydalanishga ruxsat berish</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Brauzer varaqlariga kirish</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Mijozga tegishli cheklanmagan miqdordagi maʼlumotlarni saqlash</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Navigatsiya vaqtida brauzer harakatiga kirish</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Xatchoʻplarni oʻqish va oʻzgartirish</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Brauzer sozlamalarini ochish va oʻzgartirish</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Tarix, kuki va maʼlumotlarni tozalash</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Klipborddan maʼlumotlarni olish</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Klipbordga maʼlumotlarni kiritish</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Har qanday sahifadagi kontentni bloklash</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Fayllarni yuklab olish, oʻqish va brauzerning yuklanmalar tarixini oʻzgartirish</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Qurilmaga yuklab olingan fayllarni ochish</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Barcha ochiq varaqlardagi matnni oʻqish</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Joylashuvga ruxsat</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Brauzer tarixiga ruxsat</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Kengaytmalardan foydalanish mavzularni boshqarishni nazorat qilish</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Bundan boshqa ilovalar bilan xabar almashish</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Bildirishnomalar sizga chiqadi</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Kriptografik autentifikatsiya xizmatlari taqdim etilsin</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Brauzer proksi sozlamalarini boshqarish</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Yaqinda yopilgan varaqlarga ruxsat</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Brauzer varaqlarini yashirish va koʻrsatish</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Brauzer tarixiga kirish</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Ochiq varaqlardagi maʼlumotlaringizga kirish uchun dasturchi asboblariga ruxsat berish</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Versiyasi</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Mualliflar</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Soʻnggi yangilanish</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Bosh sahifa</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Ruxsatlar haqida batafsil</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Reyting</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Sozlamalar</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Yoniq</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Oʻchiq</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Maxfiy koʻrishga ruxsat berish</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Maxfiy koʻrish rejimida ishga tushirish</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Yoniq</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Oʻchiq</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Oʻrnatildi</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Tavsiya qilinadi</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Hozircha qoʻllab-quvvatlanmaydi</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Hozircha mavjud emas</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Oʻchiq</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Tafsilotlar</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Ruxsatlar</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Olib tashlash</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">%1$s qoʻshilsinmi?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ruxsat soʻramoqda:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Qoʻshish</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Bekor qilish</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Qoʻshimcha dasturni oʻrnatish</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Bekor qilish</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Sharhlar: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Qoʻshimcha dasturlar</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Qoʻshimcha dasturlar menejeri</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Ruxsat berish</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Rad qilish</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s uchun yangilanish mavjud</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d yangi ruxsat soʻramoqda</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Yangi ruxsat soʻramoqda</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Qoʻshimcha dastur yangilanishlari</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Qoʻllab-quvvatlanadigan qoʻshimcha dasturlarni tekshirish</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Yangi qoʻshimcha dastur mavjud</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Yangi qoʻshimcha dasturlar mavjud</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">%1$sni %2$sga qoʻshish</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">%1$s va %2$sni %3$sga qoʻshish</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Ularni %1$sga qoʻshish</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Firefox qoʻshimcha texnologiyasi zamonaviylashmoqda. Ushbu qoʻshimchalar Firefox 75 va undan keyingi versiyalariga mos kelmaydigan freymvorklardan foydalanadi.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Tavsiya etilgan kengaytmalarning dastlabki tanlovini qoʻllab-quvvatlamoqdamiz.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Qoʻshimcha yuklab olinmoqda va tekshirilmoqda…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Qoʻshimchalar haqida soʻrov bajarilmadi!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Tarjima topilmadi, %1$s uchun ham, tizimining asosiy %2$s tili uchun ham</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s muvaffaqiyatli oʻrnatildi</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s oʻrnatilmadi</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s muvaffaqiyatli yoqildi</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s yoqilmadi</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s muvaffaqiyatli oʻchirib qoʻyildi</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s oʻchirilmadi</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s muvaffaqiyatli oʻchirib tashlandi</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s oʻchirib tashlanmadi</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s muvaffaqiyatli olib tashlandi</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s olib tashlanmadi</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Ushbu qoʻshimcha avvalgi %1$s versiyasidan koʻchirilgan</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 ta qoʻshimcha dastur</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s ta qoʻshimcha dastur</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Batafsil maʼlumot</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Muvaffaqiyatli yangilandi</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Yangilanish mavjud emas</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Xato</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Yangilash vositasi haqida maʼlumot</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Soʻnggi urinish:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Holati:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s %2$sga qoʻshildi</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Uni menyuda oching</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Ok, tushundim</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..85e7c65da3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-vec/strings.xml
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Ƚexere e modifegare ƚe inpostasioni reƚative a ƚa privacy</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Asedere a i dati de tuti i siti web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Asédare a i dati utente par %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Asédare a i dati de i siti web par el dominio %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Asédare a i dati utente so 1 altro sito</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Asédare a i dati utente so %1$d altri siti</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Asédare a i dati utente so 1 altro dominìo</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Asedare a i dati utente so %1$d altri domini</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Asedere a ƚe schede de el browser</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Salvare dati so el dispoxitivo sensa limitasioni de spasio</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Asedere a ƚa attività de el browser durante ƚa navigasion</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Lèxere e modìfegare i segnaƚibri</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Lèxere e modìfegare ƚe inpostasioni de el browser</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Scanseƚare cronologìa de navigasion resente, cookie e dati asocià</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Lèxere i dati da i scaraboci</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Salvare i dati ne i scaraboci</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Blocare contegnudi en cualsìasi pàxina</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Lèxare ƚa cronoloxìa de navigasion</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Scargare file, ƚexere e modìfegare ƚa cronoƚogìa de download de el browser</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Vèrxere i file scaricà so el dispoxitivo</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Lèxere el testo de tute ƚe schede vèrte</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Acedere a ƚa to poxision</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Asedere a ƚa cronoƚogìa de navigasion</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitorare l’utilixo de ƚe estensioni e gestire i temi</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Scanbiare mesaji co altre app</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Vixualixare notifeghe</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Fornire servisi de autenticasion critografica</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Controƚare ƚe inpostasioni reƚative a i proxy</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Asedere a ƚe schede sarà de resente</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Scondere e mostrare ƚe schede</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Asedi ne ƚa cronoƚogìa</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Consentire a i stromenti de sviƚupo de asedere a i dati de ƚe schede</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Autori</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ùltimo axornamento</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Pàgina inisiaƚe</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Altre informasioni so i parmesi</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Voto</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Inpostasion</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Ativa</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Dixativà</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Parmeti en navigasion privada</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Exegui en navigasion privada</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Ativo</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Dixativà</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Instalà</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Racomandà</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">No gnancora suportà</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Gnancora disponibiłe
+</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Dixativà</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Informasioni</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Pàrmesi</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Rimovi</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Instaƚare %1$s?</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Bisogna avere el parmeso de:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Xonta</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Anuƚa</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Instaƚa conponente da xontare</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Anuƚa</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Recension: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f/5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Conponenti che se pole xontare</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Gestion Estension</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Parmeti</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Nega</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">Novo axornamento par %1$s</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d nove autorixasioni dimandà</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Na nova autorixasion dimandà</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Axornamenti de el conponente xontà</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Verifega componenti xontà suportà</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Novo conponente xontà disponibiƚe</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Novi conponenti xontà disponibiƚi</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Xonta %1$s a %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Xonta %1$s e %2$s a %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Xontali a %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Ƚa tecnologia de i conponenti xontabili de Firefox ƚa xe en costante axornamento. Sti conponenti axuntivi i utilixano framework inconpatibiƚi co Firefox 75 e versioni sucesive.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Semo drio svilupare el suporto par na selesion inisiale de estension consijà.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Download e verifega de el conponente xontà en corso…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Inposibiƚe exeguire ƚa query de i conponenti xontà.</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Tradusion no trovà, né par la ƚengua %1$s né par ƚa ƚengua predefinìa %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s instaƚà coretamente</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Inposibiƚe instaƚare %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s ativà coretamente</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Inposibiƚe ativare %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s dixativà coretamente</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Inposibiƚe dixativare %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s dixinstaƚà coretamente</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Inposibiƚe dixinstaƚare %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s cavà coretamente</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Inposibiƚe rimovere %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Sto conponente xontabiƚe el provien da ƚa migrasion de na version presedente de %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 conponente xontà</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s conponenti xontabiƚi</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Altre informasion</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Axornà coretamente</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Nisun axorn. disponibile</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Erore</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Informasion so l‘axornamento</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ùltemo tentativo:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Stato:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s el xe stà xontà a %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Verxi entel menù</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">OK</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..b633602f89
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-vi/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Đọc và chỉnh sửa các cài đặt riêng tư</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Truy cập dữ liệu của bạn trên mọi trang web</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Truy cập dữ liệu của bạn cho %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Truy cập dữ liệu của bạn cho các trang web trong tên miền %1$s</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Truy cập dữ liệu của bạn trên trang web khác</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Truy cập dữ liệu của bạn trên %1$d trang web khác</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Truy cập dữ liệu của bạn trên tên miền khác</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Truy cập dữ liệu của bạn trên %1$d tên miền khác</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d của %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Truy cập các thẻ trên trình duyệt</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Lưu trữ số lượng không giới hạn dữ liệu phía máy khách</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Truy cập hoạt động của trình duyệt trong khi điều hướng</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Xem và chỉnh sửa trang đánh dấu</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Đọc và chỉnh sửa cài đặt trình duyệt</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Xóa lịch sử duyệt web gần đây, cookie, và dữ liệu liên quan</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Nhận dữ liệu từ bộ nhớ tạm</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Nhập dữ liệu vào bộ nhớ tạm</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Chặn nội dung trên bất kỳ trang nào</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Đọc lịch sử duyệt web của bạn</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Tải về các tập tin và chỉnh sửa lịch sử tải về của trình duyệt</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Mở tập tin đã tải xuống thiết bị của bạn</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Đọc văn bản của tất cả các thẻ đang mở</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Truy cập vị trí của bạn</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Truy cập lịch sử duyệt web</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Theo dõi tần suất sử dụng tiện ích và quản lý các chủ đề</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Trao đổi thông báo với các ứng dụng khác</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Hiển thị thông báo cho bạn</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Cung cấp dịch vụ xác thực mật mã</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Kiểm soát cài đặt proxy của trình duyệt</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Truy cập các thẻ đã đóng gần đây</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Ẩn và hiện các thẻ trên trình duyệt</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Truy cập lịch sử duyệt web</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Mở rộng công cụ dành cho nhà phát triển để truy cập dữ liệu của bạn trong các thẻ đang mở</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Phiên bản</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Tác giả</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Tác giả</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Cập nhật lần cuối</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Trang chủ</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Tìm hiểu thêm về quyền hạn</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Xếp hạng</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">Xem thêm thông tin về tiện ích này</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">Thông tin về tiện ích này</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Cài đặt</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Bật</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Tắt</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Cho phép trong duyệt web riêng tư</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Cho phép trong thẻ riêng tư</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Không được phép trong cửa sổ riêng tư</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Đã bật</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Đã tắt</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Đã cài đặt</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Được đề xuất</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Chưa được hỗ trợ</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Chưa có sẵn</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Đã tắt</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Chi tiết</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Quyền hạn</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Xóa</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Báo cáo</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Thêm %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s yêu cầu quyền bổ sung.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Nó yêu cầu bạn cho phép để:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">Tiện ích này muốn:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Cài đặt</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Cho phép</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Từ chối</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Hủy bỏ</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Cài đặt tiện ích</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Cài đặt %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Hủy bỏ</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Đánh giá: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Xếp hạng: %1$.02f trên 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Tiện ích mở rộng</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Quản lí tiện ích</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Tiện ích tạm thời bị vô hiệu hóa</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Tiện ích mở rộng tạm thời bị vô hiệu hóa</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">Một hoặc nhiều tiện ích ngừng hoạt động khiến hệ thống của bạn không ổn định.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">Một hoặc nhiều tiện ích mở rộng ngừng hoạt động khiến hệ thống của bạn không ổn định.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Khởi động lại tiện ích</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Khởi động lại tiện ích</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Tìm thêm tiện ích</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Tìm thêm tiện ích</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Cho phép</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Từ chối</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s có bản cập nhật mới</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d quyền mới cần được cấp</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">Yêu cầu quyền mới</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Cập nhật tiện ích</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Cập nhật tiện ích</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Trình kiểm tra tiện ích được hỗ trợ</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">Tiện ích mới có sẵn</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">Tiện ích mới có sẵn</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Thêm %1$s vào %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Thêm %1$s và %2$s vào %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Thêm chúng vào %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Công nghệ tiện ích Firefox đang hiện đại hóa. Các tiện ích mở rộng này sử dụng các framework mà nó không tương thích với Firefox 75 và mới hơn.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Chúng tôi hiện đang xây dựng hỗ trợ cho lựa chọn ban đầu của Tiện ích mở rộng được đề xuất.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Đang tải xuống và xác minh tiện ích…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Đang tải xuống và xác minh tiện ích…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Không thể truy vấn tiện ích!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Không thể truy vấn tiện ích!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Không tìm thấy bản dịch, đối với ngôn ngữ %1$s cũng như ngôn ngữ mặc định %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Đã cài đặt thành công %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Không thể cài đặt %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Không thể cài đặt tiện ích này.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Không thể cài đặt tiện ích này.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">Không thể tải xuống tiện ích này do lỗi kết nối.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">Không thể tải xuống tiện ích này do lỗi kết nối.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">Không thể cài đặt tiện ích này vì có vẻ nó đã bị hỏng.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">Không thể cài đặt tiện ích này vì có vẻ như nó bị hỏng.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">Không thể cài đặt tiện ích này vì nó chưa được kiểm định.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">Không thể cài đặt tiện ích này vì nó chưa được xác minh.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">Không thể cài đặt %1$s vì nó không tương thích với %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">Không thể cài đặt %1$s vì nó có nguy cơ cao gây ra sự cố về tính ổn định hoặc bảo mật.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Đã bật thành công %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Không thể bật %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Đã tắt thành công %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Không thể tắt %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Đã gỡ cài đặt thành công %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Không thể gỡ cài đặt %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Đã xóa thành công %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Không thể xóa %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">Tiện ích mở rộng này đã được di chuyển từ phiên bản trước của %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 tiện ích</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 tiện ích</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s tiện ích</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s tiện ích</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Tìm hiểu thêm</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Cập nhật thành công</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Không có bản cập nhật nào</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Lỗi</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Thông tin cập nhật</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Lần thử cuối cùng:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Trạng thái:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s đã được thêm vào %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Mở nó trong menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Truy cập %1$s từ menu %2$s.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">OK, đã hiểu</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Tìm hiểu thêm</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s đã bị vô hiệu hóa do vấn đề bảo mật hoặc độ ổn định.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s không thể xác minh là an toàn và đã bị vô hiệu hóa.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s không tương thích với phiên bản %2$s của bạn (phiên bản %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..602f66709d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-yo/strings.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Kà á, kí o sì ṣe àtuṣe sí ààtò àsírí</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Ní àǹfààní sí data rẹ̀ ní gbogbo ìkànnì</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Ní àǹfààní sí data rẹ ní %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Ní àǹfààní sí data rẹ fún àwọn ìkànnì ní agbegbe %1$s </string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Ní àǹfààní sí data rẹ ni 1 ìkànnì mìíràn </string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Ní àǹfààní sí data rẹ lórí %1$d ìkànnì mìíràn </string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Ní àǹfààní sí data rẹ lórí 1 agbegbe mìíràn</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Ní àǹfàní sí data rẹ ní títàn %1$d agbègbè mìíràn</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Ní àǹfàní sí àwọn abala bíráwúsà</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Tọ́ju iye àìlópin dátà ti apá kan àwọn alábárà</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Ní àǹfàní sí iṣẹ́ tó ń lọ lóri bíráwúsà lákòókò ìwákiri</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Kà á ko sì ṣàtúnṣe búkúmaàkì</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Kà á ko sì ṣàtúnṣe ètò bíráwúsà</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Kó àwọn ìtan tí o lò lóríi bíráwúsà kúrò, kúkìsì, àti àwọn dátà tó jọmọ</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Gba dátà láti inú kílípúbọọ̀dù</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Fi dátà sí inú kílípúbọọ̀dù</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Dìnà àwọn àkóónú lóri èyíkéyìí ojú ìwé</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Ṣé ìgbàsílẹ̀ àwọn fáìlì kí o kà á kí o sì ṣàtúnṣe àwọn ìtàn bíráwúsà </string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Ṣí àwọn fáìlì tí a gbà sílẹ̀ lórí ẹ̀rọ rẹ</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Ka àwọn ọ̀rọ̀ gbogbo táàbù tó wà ní ṣíṣí</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Ní àǹfàní sí ibùdó ti o wà</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Ní àǹfàní sí ìtàn bíráwúsìnìn</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Ṣàmójútó àwọn ìlò àfikún ki o sì ṣàkóso àwọn àkórí</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Ṣe pàṣípàrọ̀ àwọn ọ̀rọ̀ ìfiránṣẹ́ pẹ̀lú àwọn áàpù mìíràn ju èyí lọ </string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Ṣe àfihàn àwọn ìwífúnni sí ọ</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Pèsè ìjẹ́risí àwọn iṣẹ́ kiripitógíráfì</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Ṣàkóso asojú àwọn ètò bíráwúsà</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Ní àǹfàní sí táàbù tó ṣẹ̀ṣẹ̀ padé</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Tọ́jú ko sì sàfihàn àwọn táàbù bíráwúsà</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Ní àǹfàní sí ìtàn bíráwúsìnìn</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Fa irinṣẹ́ àwọn ìdàgbàsókè gùn láti ní àǹfàní sí dátà rẹ nínú àwọn táàbù tó wà ní ṣíṣí</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Ẹ̀dà</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors">Àwọn òǹkọ̀wé</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Ti ṣàfikún kẹ́hìn</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Ojú ìwé ìbẹ̀rẹ̀</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Kọ́ si nípa ìgbaniláàyè</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Ìṣe òdiwọ̀n</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Àwọn ètò</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">Tàn-án</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off">Pa á</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Fàyè gbàá nínú bíráwúsìnìn ìkọ̀kọ̀</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Lò ó nínú bíráwúsìnìn ìkọ̀kọ̀</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Jẹ́ kí ó siṣẹ́</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled">Májẹ̀ kí ó siṣẹ́</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Ìfi sórí ẹ̀rọ </string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Ìgbàníyànjú</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">A kò tíì ṣàtìlẹ́yìn fun</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Kò tíì sí</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Májẹ́ kí ó siṣẹ́</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Àwọn àlàyé</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Ìfàyègbà</string>
+ <!-- This is button that remove (uninstall an add-on). -->
+ <string name="mozac_feature_addons_remove">Yọ ọ́ kúrò</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Fi %1$s? kún</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">Ó pè fún ìfàyègbà rẹ láti:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Fi kún</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Paárẹ́</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description">Ṣafikún sórí ẹ̀rọ</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Paárẹ́</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Àwọn àtúnyẹ̀wò: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description">%1$.02f / 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Ṣe àfikún</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Ṣe Àfikún Àṣàmójútó</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Gbàáláyè</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Kọ̀ sílẹ̀</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s ó ní ìṣọdọ̀tun tuntun</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d wọ́n nílò ìgbàláyè tuntun</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">A nílò ìgbàláyè tuntun</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel">Ṣàfikún ìsọdọ̀tun</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel">Olùyẹwò àwọn àfikún tí ó ṣe àtìlẹ́yìn</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title">Àfikún tuntun ti wà</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural">Àwọn àfikún tuntun ti wà</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Àfikún %1$s sí %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Àfikún %1$s àti %2$s sí %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Ṣàfikún wọn sí %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption">Ọ̀làjú ti ń dé bá àfikún ìmọ̀-ẹ̀rọ Firefox. Àwọn àfikún yìí ń lo àwọn ìlànà tí kò bá Firefox mu 75 &amp; jú bẹ̀ẹ́ lọ.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">Lọ́wọ́lọ́wọ́ yìí à ń ṣe àtìlẹ́yìn fún irúfẹ́ àwọn àfikún tí a kọ́kọ́ yàn tí wọ́n sì ti sọ̀rọ̀ rẹ̀ ní dáada. </string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption">Ṣíṣàgbàálẹ̀ àti ìjẹ́rìísí àfikún…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons">Kùnà láti béèrè àwọn àfikún!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">A kò rí ògbufọ̀, fún agbègbè %1$s tàbí èdè tó ba wá %2$s </string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Ó ní àṣèyọrí ìfi sórí ẹ̀rọ %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Ó kùnà ìfi sórí ẹ̀rọ %1$s</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Ó ní àṣeyọrí ṣíṣiṣẹ́ %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Kùnà láti ṣiṣẹ́ %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Ní àṣeyọrí ìyọkúrò %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Kùnà láti yọkúrò %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Ní àṣeyọrí yíyọkúrò %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Ó kùnà láti yọ kúrò %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">A yọ́ kúrò ní ìrọwọ́rọsẹ̀ %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Kùnà láti yọ kúrò %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">A gbé àfikún yìí láti ẹ̀ya ti tẹ́lẹ̀ %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption">1 ṣàfikún</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural">%1$s àwọn àfikún</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Kọ́ sí i</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Ìsọdọ̀tún ti láṣeyọrí</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">Kò sí ìròyìn ọ̀tun nílẹ̀</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Àṣìṣe </string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Ìròyin Alásọdọ̀tun</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Ìgbìyànjú ìkẹyìn:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Ipò:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s wọ́n ti fi kun %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_installed_dialog_description">Ṣí i nínu àkójọpọ̀ àṣàyàn</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button">Bẹ́ẹ̀ni, Mo ri</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..c043582e0b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">读取和修改隐私设置</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">存取您在所有网站的数据</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">存取您用于 %1$s 的数据</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">存取您用于 %1$s 域名的数据</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">存取您在其他 1 个网站的数据</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">存取您在其他 %1$d 个网站的数据</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">存取您在其他 1 个域名的数据</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">存取您在其他 %1$d 个域名的数据</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s,第 %2$d 项,共 %3$d 项</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">存取浏览器标签页</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">存储无限多的客户端数据</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">获知浏览器导航时的行为状态</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">读取和修改书签</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">读取和修改浏览器设置</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">清除最近的浏览历史、Cookie 及有关数据</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">获取剪贴板数据</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">输入数据到剪贴板</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">拦截任何页面上的内容</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">读取您的浏览历史</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">下载文件和读写浏览器的下载历史</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">打开下载至您设备的文件</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">读取已打开的所有标签页的文本</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">获知您的位置</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">存取浏览历史</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">监控扩展使用情况和管理主题</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">与其他应用交换信息</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">向您显示通知</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">提供密码学身份认证服务</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">控制浏览器的代理设置</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">存取最近关闭的标签页</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">隐藏和显示浏览器标签页</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">存取浏览历史</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">让开发者工具可以存取您打开的标签页中的数据</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">版本</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">作者</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">作者</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">上次更新</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">主页</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">详细了解权限</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">评分</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">关于此附加组件的更多信息</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">此扩展的更多信息</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">设置</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">启用</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">禁用</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">允许运行于隐私浏览模式</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">在隐私浏览中运行</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">不允许在隐私窗口中运行</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">已启用</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">已禁用</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">已安装</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">推荐</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">尚不支持</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">尚不可用</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">已禁用</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">详细信息</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">权限</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">移除</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">举报</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">要添加“%1$s”吗?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">“%1$s”需要额外权限。</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">要求取得下列权限:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">它希望:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">添加</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">允许</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">拒绝</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">取消</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">安装附加组件</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">安装“%1$s”</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">取消</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">评价数:%1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">评分:%1$.02f(满分 5 分)</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">附加组件</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">附加组件管理器</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">附加组件已暂时被禁用</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">扩展已被暂时禁用</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">一个或多个附加组件已停止工作,导致您的系统不稳定。</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">一个或多个扩展已停止工作,导致您的系统不稳定。</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">重启附加组件</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">重启扩展</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">寻找更多附加组件</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">寻找更多扩展</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">允许</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">拒绝</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s 有更新</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">需要 %1$d 个新权限</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">需要新的权限</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">附加组件更新</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">扩展更新</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">新支持附加组件检查器</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">有新的附加组件可用</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">有多个新的附加组件可用</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">将 %1$s 添加至 %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">将 %1$s 和 %2$s 添加至 %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">将它们添加至 %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox 附加组件技术正趋于现代化。这些附加组件使用了与 Firefox 75 及更高版本不兼容的框架。</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">我们目前着重构建对部分“推荐扩展”的支持,请稍后再来。</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">正在下载并验证附加组件…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">正在下载并验证扩展…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">查询附加组件失败。</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">查询扩展失败!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">找不到语言环境 %1$s 或默认语言 %2$s 的翻译信息</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">成功安装 %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">未能安装 %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">安装此附加组件失败。</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">安装此扩展失败。</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">此附加组件未能下载,因为连接失败。</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">连接失败,无法下载此扩展。</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">该附加组件无法安装,因为它似乎已损坏。</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">此扩展似乎已损坏,无法安装。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">此附加组件无法安装,因为它未通过验证。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">此扩展未经验证,无法安装。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s 未能安装,因为它与 %2$s %3$s 不兼容。</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">无法安装“%1$s”,因为它很可能引发稳定性或安全性问题。</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">成功启用 %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">未能启用 %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">成功禁用 %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">未能禁用 %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">成功卸载 %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">未能卸载 %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">成功移除 %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">未能移除 %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">此附加组件是从 %1$s 的早期版本迁移而来</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 个附加组件</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 个扩展</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s 个附加组件</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s 个扩展</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">详细了解</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">已成功更新</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">没有可用的更新</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">错误</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">更新信息</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">上次尝试:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">状态:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s 已添加到 %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">在菜单中打开</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">可在 %2$s 菜单中使用“%1$s”。</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">好的</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">确定</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">详细了解</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s 由于安全或稳定性问题已被禁用。</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s 未通过安全验证,已被禁用。</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">“%1$s”与您当前版本(%3$s)的 %2$s 不兼容。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..10f6108cfb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">讀取或修改隱私設定</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">存取您所有網站中的資料</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">存取您在 %1$s 的資料</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">存取您在 %1$s 網域中的網站資料</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">存取您在另 1 個網站的資料</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">存取您在另 %1$d 個網站中的資料</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">存取您在另 1 個網域中的資料</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">存取您在另 %1$d 個網域中的資料</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s(第 %2$d 組,共 %3$d 組)</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">存取瀏覽器分頁</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">在客戶端儲存無限量資料</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">在上網時了解瀏覽器行為狀態</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">讀取或修改書籤</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">讀取或修改瀏覽器設定</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">清除最近的瀏覽紀錄、Cookie 等相關資料</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">取得剪貼簿中的資料</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">輸入資料到剪貼簿</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">封鎖任何頁面上的內容</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">讀取您的上網紀錄</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">下載檔案、讀取或修改瀏覽器的下載紀錄</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">開啟下載到您裝置上的檔案</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">讀取所有開啟分頁當中的文字內容</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">取得您的所在位置</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">取得瀏覽紀錄</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">監控擴充套件使用情況並管理佈景主題</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">與其他程式交換訊息</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">向您顯示通知</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">提供加密驗證服務</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">控制瀏覽器代理伺服器設定</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">取得最近關閉的分頁</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">隱藏或顯示瀏覽器分頁</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">取得瀏覽紀錄</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">讓開發者工具可存取您在開啟分頁中的資料</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">版本</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">作者</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">作者</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">最近更新</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">首頁</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">了解權限的更多資訊</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">評分</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">關於此附加元件的更多資訊</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">此擴充套件的更多資訊</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">設定</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">開啟</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">關閉</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">允許在隱私瀏覽模式執行</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">在隱私瀏覽模式執行</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">不允許於隱私視窗運作</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">啟用</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">停用</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">已安裝</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">推薦項目</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">尚未支援</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">尚未推出</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">已停用</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">詳細資訊</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">權限</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">移除</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">檢舉</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">要安裝 %1$s 嗎?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s 要求更多權限。</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">本套件要求下列權限:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">它想要:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">新增</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">允許</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">拒絕</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">取消</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">安裝附加元件</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">安裝 %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">取消</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">評價: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">評分:%1$.02f 分,滿分 5 分</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">附加元件</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">附加元件管理員</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">已暫時停用附加元件</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">已暫時停用擴充套件</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">有一套或多套附加元件停止運作,讓您的系統不穩定。</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">有一套或多套擴充套件停止運作,讓您的系統不穩定。</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">重新啟動附加元件</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">重新啟動擴充套件</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">看更多附加元件!</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">尋找更多擴充套件</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">允許</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">拒絕</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s 有更新</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">需要 %1$d 組新權限</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">需要一組新權限</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">附加元件更新</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">擴充套件更新</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">檢查支援的附加元件</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">有新的附加元件可以使用</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">有新的附加元件可以使用</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">將 %1$s 安裝到 %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">將 %1$s 與 %2$s 安裝到 %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">將它們安裝到 %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox 的附加元件技術正在現代化。這些附加元件使用與 Firefox 75 或更新版本不相容的技術框架。</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">我們還在開發初版的附加元件推薦功能,請稍後再回來。</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">正在下載與確認附加元件…</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">正在下載與確認擴充套件…</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">附加元件查詢失敗!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">擴充套件查詢失敗!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">找不到語系 %1$s 或預設語言 %2$s 的翻譯資訊</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">%1$s 安裝成功</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">%1$s 安裝失敗</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">附加元件安裝失敗。</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">擴充套件安裝失敗。</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">因為連線失敗,無法下載此附加元件。</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">因為連線失敗,無法下載此擴充套件。</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">檔案似乎已損毀,無法安裝此附加元件。</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">檔案似乎已損毀,無法安裝此擴充套件。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">因為此附加元件尚未經過驗證,無法安裝。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">因為此擴充套件尚未經過驗證,無法安裝。</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">因為與 %2$s %3$s 不相容,無法安裝 %1$s。</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">無法安裝 %1$s,因為它可能會造成安全性或穩定性問題。</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">%1$s 啟用成功</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">%1$s 啟用失敗</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">%1$s 停用成功</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">%1$s 停用失敗</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">%1$s 移除成功</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">%1$s 移除失敗</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">%1$s 移除成功</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">%1$s 移除失敗</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">此附加元件是從舊版 %1$s 轉移而來</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 套附加元件</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 套擴充套件</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s 套附加元件</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s 套擴充套件</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">了解更多</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">已成功更新</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">沒有可用的更新</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">錯誤</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">更新資訊</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">上次嘗試:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">狀態:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">已安裝 %1$s 至 %2$s。</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">到選單開啟</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">從 %2$s 選單使用 %1$s。</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">好的,知道了</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">確定</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">了解更多</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">因為安全性或穩定性因素,已停用 %1$s。</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">無法確認 %1$s 安全,已被停用。</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s 與您執行的 %2$s(版本 %3$s)不相容。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..024d1d0ff0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values/colors.xml
@@ -0,0 +1,11 @@
+<?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>
+ <!-- Color for the text of the error statuses in the add-ons manager. -->
+ <color name="mozac_feature_addons_error_text_color">#ff0000</color>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values/dimens.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..ef6e14a8cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values/dimens.xml
@@ -0,0 +1,9 @@
+<?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>
+ <dimen name="allowed_in_private_browsing_label_margins">8dp</dimen>
+ <dimen name="add_on_name_container_margin_bottom">2dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/addons/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..8429f6135a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/main/res/values/strings.xml
@@ -0,0 +1,300 @@
+<?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 xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Description for privacy add-on permission. -->
+ <string name="mozac_feature_addons_permissions_privacy_description">Read and modify privacy settings</string>
+ <!-- Description for all_urls add-on permission. -->
+ <string name="mozac_feature_addons_permissions_all_urls_description">Access your data for all websites</string>
+ <!-- Description for giving an add-on access to users's data on one site. %1$s will be replaced by the DNS host name for which a web extension is requesting access (e.g., www.mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_one_site_description">Access your data for %1$s</string>
+ <!-- Description for giving an add-on access to users's data in multiple sites in the domain %1$s. %1$s will be replaced by the DNS domain for which a web extension is requesting access (e.g., mozilla.org). -->
+ <string name="mozac_feature_addons_permissions_sites_in_domain_description">Access your data for sites in the %1$s domain</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 5 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 1 other site". This entry it's for the singular case, when the add-on is only accessing one extra site. -->
+ <string name="mozac_feature_addons_permissions_one_extra_site_description" tools:ignore="PluralsCandidate">Access your data on 1 other site</string>
+ <!-- When an add-on requires access to more than 4 sites, for example the add-on requires access for 6 sites. We will show the first 4 sites in individual entries, as in mozac_feature_addons_permissions_one_site_description,
+ then we will show another collapsed entry saying "Access your data on 2 other sites". This entry it's for the plural case, when the add-on is accessing more than one extra site.
+ %1$d will be replaced by an integer indicating the number of additional hosts for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_sites_description" tools:ignore="PluralsCandidate">Access your data on %1$d other sites</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 5 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 1 other domain". This entry it's for the singular case, when the add-on is only accessing one extra domain. -->
+ <string name="mozac_feature_addons_permissions_one_extra_domain_description" tools:ignore="PluralsCandidate">Access your data on 1 other domain</string>
+ <!-- When an add-on requires access to more than 4 domains, for example the add-on requires access for 6 domains. We will show the first 4 domains in individual entries, as in mozac_feature_addons_permissions_sites_in_domain_description,
+ then we will show another collapsed entry saying "Access your data on 2 other domains". This entry it's for the plural case, when the add-on is accessing more than one extra domain.
+ %1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
+ <string name="mozac_feature_addons_permissions_extra_domains_description_plural" tools:ignore="PluralsCandidate">Access your data on %1$d other domains</string>
+ <!-- Content description for a permission item, for an installed add-on details page. %1$s will be replaced with the permission text name, %2$d will be replaced with the permission position in the list and %3$d will be replaced with the total number of permissions in the list.Taking as an example "Read and modify privacy settings" the whole text will read Read and modify privacy settings, 2 of 6. -->
+ <string name="mozac_feature_addons_permissions_content_description_item">%1$s, %2$d of %3$d</string>
+ <!-- Description for tabs add-on permission. -->
+ <string name="mozac_feature_addons_permissions_tabs_description">Access browser tabs</string>
+ <!-- Description for unlimited_storage permission. -->
+ <string name="mozac_feature_addons_permissions_unlimited_storage_description">Store unlimited amount of client-side data</string>
+ <!-- Description for navigation permission. -->
+ <string name="mozac_feature_addons_permissions_web_navigation_description">Access browser activity during navigation</string>
+ <!-- Description for bookmarks permission. -->
+ <string name="mozac_feature_addons_permissions_bookmarks_description">Read and modify bookmarks</string>
+ <!-- Description for browser_setting permission. -->
+ <string name="mozac_feature_addons_permissions_browser_setting_description">Read and modify browser settings</string>
+ <!-- Description for browser_data permission. -->
+ <string name="mozac_feature_addons_permissions_browser_data_description">Clear recent browsing history, cookies, and related data</string>
+ <!-- Description for clipboard_read permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_read_description">Get data from the clipboard</string>
+ <!-- Description for clipboard_write permission. -->
+ <string name="mozac_feature_addons_permissions_clipboard_write_description">Input data to the clipboard</string>
+ <!-- Description for declarativeNetRequest permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_description">Block content on any page</string>
+ <!-- Description for declarativeNetRequestFeedback permission. -->
+ <string name="mozac_feature_addons_permissions_declarative_net_request_feedback_description">Read your browsing history</string>
+ <!-- Description for downloads permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_description">Download files and read and modify the browser’s download history</string>
+ <!-- Description for downloads_open permission. -->
+ <string name="mozac_feature_addons_permissions_downloads_open_description">Open files downloaded to your device</string>
+ <!-- Description for find permission. -->
+ <string name="mozac_feature_addons_permissions_find_description">Read the text of all open tabs</string>
+ <!-- Description for geolocation permission. -->
+ <string name="mozac_feature_addons_permissions_geolocation_description">Access your location</string>
+ <!-- Description for history permission. -->
+ <string name="mozac_feature_addons_permissions_history_description">Access browsing history</string>
+ <!-- Description for management permission. -->
+ <string name="mozac_feature_addons_permissions_management_description">Monitor extension usage and manage themes</string>
+ <!-- Description for native_messaging permission. -->
+ <string name="mozac_feature_addons_permissions_native_messaging_description">Exchange messages with apps other than this one</string>
+ <!-- Description for notifications permission. -->
+ <string name="mozac_feature_addons_permissions_notifications_description">Display notifications to you</string>
+ <!-- Description for pkcs11 permission. -->
+ <string name="mozac_feature_addons_permissions_pkcs11_description">Provide cryptographic authentication services</string>
+ <!-- Description for proxy permission. -->
+ <string name="mozac_feature_addons_permissions_proxy_description">Control browser proxy settings</string>
+ <!-- Description for sessions permission. -->
+ <string name="mozac_feature_addons_permissions_sessions_description">Access recently closed tabs</string>
+ <!-- Description for tab_hide permission. -->
+ <string name="mozac_feature_addons_permissions_tab_hide_description">Hide and show browser tabs</string>
+ <!-- Description for top_sites permission. -->
+ <string name="mozac_feature_addons_permissions_top_sites_description">Access browsing history</string>
+ <!-- Description for devtools permission. -->
+ <string name="mozac_feature_addons_permissions_devtools_description">Extend developer tools to access your data in open tabs</string>
+ <!-- The version on of add-on. -->
+ <string name="mozac_feature_addons_version">Version</string>
+ <!-- The author of an add-on. -->
+ <string name="mozac_feature_addons_author">Author</string>
+ <!-- The authors of an add-on. -->
+ <string name="mozac_feature_addons_authors" moz:removedIn="123" tools:ignore="UnusedResources">Authors</string>
+ <!-- The last date that the add-on was updated. -->
+ <string name="mozac_feature_addons_last_updated">Last updated</string>
+ <!-- The developer website (Homepage) of the add-on. -->
+ <string name="mozac_feature_addons_home_page">Homepage</string>
+ <!-- A link where users can find more information about add-ons permissions. -->
+ <string name="mozac_feature_addons_learn_more">Learn more about permissions</string>
+ <!-- The rating of the add-on. -->
+ <string name="mozac_feature_addons_rating">Rating</string>
+ <!-- A link that points to the detail page of the add-on. -->
+ <string name="mozac_feature_addons_more_info_link" moz:removedIn="126" tools:ignore="UnusedResources">More about this add-on</string>
+ <!-- A link that points to the detail page of the extension. -->
+ <string name="mozac_feature_addons_more_info_link_2">More about this extension</string>
+ <!-- The settings of the add-on. -->
+ <string name="mozac_feature_addons_settings">Settings</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_settings_on">On</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_settings_off" moz:removedIn="125" tools:ignore="UnusedResources">Off</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_allow_in_private_browsing">Allow in private browsing</string>
+ <!-- Indicates the add-on is allowed in private browsing mode. -->
+ <string name="mozac_feature_addons_settings_run_in_private_browsing">Run in private browsing</string>
+ <!-- This is displayed when the add-on is not allowed to run in private browsing. -->
+ <string name="mozac_feature_addons_not_allowed_in_private_browsing">Not allowed in private windows</string>
+ <!-- Indicates the add-on is enabled. -->
+ <string name="mozac_feature_addons_enabled">Enabled</string>
+ <!-- Indicates the add-on is disabled. -->
+ <string name="mozac_feature_addons_disabled" moz:removedIn="125" tools:ignore="UnusedResources">Disabled</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the installed section. -->
+ <string name="mozac_feature_addons_installed_section">Installed</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the recommended section. -->
+ <string name="mozac_feature_addons_recommended_section">Recommended</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet supported section. -->
+ <string name="mozac_feature_addons_unsupported_section">Not yet supported</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the not yet available section. -->
+ <string name="mozac_feature_addons_unavailable_section">Not yet available</string>
+ <!-- This is displayed in a page where the user can see the installed and recommended(not installed yet) add-ons, this string indicates the disabled section. -->
+ <string name="mozac_feature_addons_disabled_section">Disabled</string>
+ <!-- Displays a page with all the details of an add-on. -->
+ <string name="mozac_feature_addons_details">Details</string>
+ <!-- Displays a page with all the permissions of an add-on. -->
+ <string name="mozac_feature_addons_permissions">Permissions</string>
+ <!-- This is a button to remove (i.e. uninstall) an add-on. -->
+ <string name="mozac_feature_addons_remove">Remove</string>
+ <!-- This is a button to report an add-on. -->
+ <string name="mozac_feature_addons_report">Report</string>
+ <!-- This is the title of a dialog that asks the users if they to install or not an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_permissions_dialog_title">Add %1$s?</string>
+ <!-- This is the title of a dialog that asks the users to grant optional permissions. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_title">%1$s requests additional permissions.</string>
+ <!-- This is the subtitle of a dialog that asks the users if they want to install or not an add-on, the subtitle lists the permissions that this add-on requires, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_permissions_dialog_subtitle">It requires your permission to:</string>
+ <!-- This is the subtitle of a dialog that asks the users to grant optional permissions, the subtitle lists the permissions that this add-on requests, the permissions will dynamically be listed after the ":". -->
+ <string name="mozac_feature_addons_optional_permissions_dialog_subtitle">It wants to:</string>
+ <!-- This is a button to add (install) an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_add">Add</string>
+ <!-- This is a button to allow the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_allow">Allow</string>
+ <!-- This is a button to deny the optional permissions requested by an add-on . -->
+ <string name="mozac_feature_addons_permissions_dialog_deny">Deny</string>
+ <!-- This is a button to cancel the add-on installation . -->
+ <string name="mozac_feature_addons_permissions_dialog_cancel">Cancel</string>
+ <!-- Accessibility content description to install add-on button. -->
+ <string name="mozac_feature_addons_install_addon_content_description" tools:ignore="UnusedResources" moz:removedIn="124">Install Add-on</string>
+ <!-- Accessibility content description to install add-on button. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_install_addon_content_description_2">Install %1$s</string>
+ <!-- This is the label of a button to cancel an ongoing add-on installation. -->
+ <string name="mozac_feature_addons_install_addon_dialog_cancel">Cancel</string>
+ <!-- Indicates how many users have rated an add-on. %1$s will be replaced with number of reviews -->
+ <string name="mozac_feature_addons_user_rating_count_2">Reviews: %1$s</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars and / separator and 5 the maximum number of stars e.g (2/5, 4.5/5 or 5/5) . -->
+ <string name="mozac_feature_addons_rating_content_description" moz:removedIn="124" tools:ignore="UnusedResources">%1$.02f/5</string>
+ <!-- Accessibility content description for the amount of stars that add-on has, where %1$.02f will be the amount of stars. -->
+ <string name="mozac_feature_addons_rating_content_description_2">Rating: %1$.02f out of 5</string>
+ <!-- This is the title of page where all the add-ons are listed-->
+ <string name="mozac_feature_addons_addons">Add-ons</string>
+ <!-- Label for add-ons sub menu item for add-ons manager-->
+ <string name="mozac_feature_addons_addons_manager">Add-ons Manager</string>
+ <!-- The title of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_title_text" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons are temporarily disabled</string>
+ <!-- The title of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_title_text">Extensions are temporarily disabled</string>
+ <!-- The content of the "crash" notification in the add-ons manager -->
+ <string name="mozac_feature_addons_manager_notification_content_text" moz:removedIn="126" tools:ignore="UnusedResources">One or more add-ons stopped working, making your system unstable.</string>
+ <!-- The content of the "crash" notification in the extensions manager -->
+ <string name="mozac_feature_extensions_manager_notification_content_text">One or more extensions stopped working, making your system unstable.</string>
+ <!-- Button to re-enable the add-ons in the "crash" notification -->
+ <string name="mozac_feature_addons_manager_notification_restart_button" moz:removedIn="126" tools:ignore="UnusedResources">Restart add-ons</string>
+ <!-- Button to re-enable the extensions in the "crash" notification -->
+ <string name="mozac_feature_extensions_manager_notification_restart_button">Restart extensions</string>
+ <!-- Button in the add-ons manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_addons_button_text" moz:removedIn="126" tools:ignore="UnusedResources">Find more add-ons</string>
+ <!-- Button in the extensions manager that opens AMO in a tab -->
+ <string name="mozac_feature_addons_find_more_extensions_button_text">Find more extensions</string>
+ <!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->
+ <string name="mozac_feature_addons_updater_notification_allow_button">Allow</string>
+ <!-- The label of the deny button on a notification, this will be shown to the user when an add-on needs new permissions. Indicates the user denies the new permissions and prevents the add-on from be updated-->
+ <string name="mozac_feature_addons_updater_notification_deny_button">Deny</string>
+ <!-- The tile of the notification, this will be shown to the user when an add-on needs new permissions, to be updated. %1$s is the name of the add-ons-->
+ <string name="mozac_feature_addons_updater_notification_title">%1$s has a new update</string>
+ <!-- The content of the notification, this will be shown when an add-on needs new permissions, to be updated. %1$d is the amount of new permissions-->
+ <string name="mozac_feature_addons_updater_notification_content" tools:ignore="PluralsCandidate">%1$d new permissions are required</string>
+ <!-- The content of the notification displayed when an add-on needs a new permission-->
+ <string name="mozac_feature_addons_updater_notification_content_singular">A new permission is required</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an add-on. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel" moz:removedIn="126" tools:ignore="UnusedResources">Add-on updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for updating an extension. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_updater_notification_channel_2">Extension updates</string>
+ <!-- Name of the "notification channel" used for displaying a notification for new supported add-ons. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_addons_supported_checker_notification_channel" tools:ignore="UnusedResources">Supported add-ons checker</string>
+ <!-- The tile of the notification, this will be shown to the user when one newly supported add-on is available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title" tools:ignore="UnusedResources">New add-on available</string>
+ <!-- The tile of the notification, this will be shown to the user when more than one newly supported add-ons are available.-->
+ <string name="mozac_feature_addons_supported_checker_notification_title_plural" tools:ignore="UnusedResources">New add-ons available</string>
+ <!-- The content of the notification, this will be shown to the user when one newly supported add-on is available. %1$s is the add-on name and %2$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_one">Add %1$s to %2$s</string>
+ <!-- The content of the notification, this will be shown to the user when two newly supported add-ons are available. %1$s is the first add-on name. %2$s is the second add-on name. %3$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_two">Add %1$s and %2$s to %3$s</string>
+ <!-- The content of the notification, this will be shown to the user when more than two newly supported add-ons are available. %1$s is the app name (in most cases Firefox). -->
+ <string name="mozac_feature_addons_supported_checker_notification_content_more_than_two">Add them to %1$s</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption" moz:removedIn="126" tools:ignore="UnusedResources">Firefox add-on technology is modernizing. These add-ons use frameworks that are not compatible with Firefox 75 &amp; beyond.</string>
+ <!-- This is the caption for not yet supported screen caption -->
+ <string name="mozac_feature_addons_not_yet_supported_caption2">We‘re currently building support for an initial selection of Recommended Extensions.</string>
+ <!-- This is the caption for the add-on installation progress overlay -->
+ <string name="mozac_add_on_install_progress_caption" moz:removedIn="126" tools:ignore="UnusedResources">Downloading and verifying add-on&#8230;</string>
+ <!-- This is the caption for the extension installation progress overlay -->
+ <string name="mozac_extension_install_progress_caption">Downloading and verifying extension&#8230;</string>
+ <!-- Error shown when something unexpected happened while trying to get the add-on list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_add_ons" moz:removedIn="126" tools:ignore="UnusedResources">Failed to query Add-ons!</string>
+ <!-- Error shown when something unexpected happened while trying to get the extension list from the server -->
+ <string name="mozac_feature_addons_failed_to_query_extensions">Failed to query Extensions!</string>
+ <!-- Error shown when unable to find a translation for an add-on field. %1$s is the locale of the user and %2$s is the default language of the add-on -->
+ <string name="mozac_feature_addons_failed_to_translate">Translation not found, for locale %1$s neither default language %2$s</string>
+ <!-- Text shown after successfully installed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_installed">Successfully installed %1$s</string>
+ <!-- Text shown after failed to install an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_install">Failed to install %1$s</string>
+ <!-- Text shown after failing to install an add-on for which we don't have its name. -->
+ <string name="mozac_feature_addons_failed_to_install_generic" moz:removedIn="126" tools:ignore="UnusedResources">Failed to install this add-on.</string>
+ <!-- Text shown after failing to install an extension for which we don't have its name. -->
+ <string name="mozac_feature_addons_extension_failed_to_install">Failed to install this extension.</string>
+ <!-- Text shown when attempting to install an add-on and a network error happened. -->
+ <string name="mozac_feature_addons_failed_to_install_network_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be downloaded because of a connection failure.</string>
+ <!-- Text shown when attempting to install an extension and a network error happened. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_network_error">This extension could not be downloaded because of a connection failure.</string>
+ <!-- Text shown when attempting to install an add-on and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_failed_to_install_corrupt_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be installed because it appears to be corrupt.</string>
+ <!-- Text shown when attempting to install an extension and the downloaded file is corrupted. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_corrupt_error">This extension could not be installed because it appears to be corrupt.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_failed_to_install_not_signed_error" moz:removedIn="126" tools:ignore="UnusedResources">This add-on could not be installed because it has not been verified.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was not signed. -->
+ <string name="mozac_feature_addons_extension_failed_to_install_not_signed_error">This extension could not be installed because it has not been verified.</string>
+ <!-- Text shown when attempting to install an add-on and an error occurred because the extension was incompatible. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_failed_to_install_incompatible_error">%1$s could not be installed because it is not compatible with %2$s %3$s.</string>
+ <!-- Text shown when attempting to install a blocklisted add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_blocklisted_1">%1$s could not be installed because it has a high risk of causing stability or security problems.</string>
+ <!-- Text shown after successfully enabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_enabled">Successfully enabled %1$s</string>
+ <!-- Text shown after failed to enable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_enable">Failed to enable %1$s</string>
+ <!-- Text shown after successfully disabled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_disabled">Successfully disabled %1$s</string>
+ <!-- Text shown after failed to disable an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_disable">Failed to disable %1$s</string>
+ <!-- Text shown after successfully uninstalled an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_uninstalled">Successfully uninstalled %1$s</string>
+ <!-- Text shown after failed to uninstall an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_uninstall">Failed to uninstall %1$s</string>
+ <!-- Text shown after successfully removed an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_successfully_removed" tools:ignore="UnusedResources">Successfully removed %1$s</string>
+ <!-- Text shown after failed to remove an add-on. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_failed_to_remove" tools:ignore="UnusedResources">Failed to remove %1$s</string>
+ <!-- Label shown to indicate that the add-on was migrated from a previous version of the app. %1$s is the app name most of the case it will be Firefox. -->
+ <string name="mozac_feature_addons_migrated_from_a_previous_version_label" tools:ignore="UnusedResources">This add-on was migrated from a previous version of %1$s</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter. -->
+ <string name="mozac_feature_addons_unsupported_caption" moz:removedIn="126" tools:ignore="UnusedResources">1 add-on</string>
+ <!-- Text shown in not yet supported add-ons section. -->
+ <string name="mozac_feature_addons_unsupported_caption_2">1 extension</string>
+ <!-- Text shown in not yet supported add-ons section in AddonsManagerAdapter - plural. %1$s is the number of unsupported add-ons -->
+ <string name="mozac_feature_addons_unsupported_caption_plural" moz:removedIn="126" tools:ignore="UnusedResources">%1$s add-ons</string>
+ <!-- Text shown in not yet supported add-ons section - plural. %1$s is the number of unsupported extensions. -->
+ <string name="mozac_feature_addons_unsupported_caption_plural_2">%1$s extensions</string>
+ <!-- Text link to a sumo page for learning more about unsupported add-ons. -->
+ <string name="mozac_feature_addons_unsupported_learn_more">Learn more</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated. -->
+ <string name="mozac_feature_addons_updater_status_successfully_updated">Successfully updated</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has no updates available. -->
+ <string name="mozac_feature_addons_updater_status_no_update_available">No update available</string>
+ <!-- Displayed in the "Status" field for the updater when there was an error during the add-on update. -->
+ <string name="mozac_feature_addons_updater_status_error">Error</string>
+ <!-- Text shown in a dialog that displays information about the last attempt to update an add-on. This is the title of the dialog. -->
+ <string name="mozac_feature_addons_updater_dialog_title">Updater Information</string>
+ <!-- Label in the dialog displaying information for the last attempt to update add-ons. It's followed by the date of the last attempt. -->
+ <string name="mozac_feature_addons_updater_dialog_last_attempt">Last attempt:</string>
+ <!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
+ <string name="mozac_feature_addons_updater_dialog_status">Status:</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_title">%1$s has been added to %2$s</string>
+ <!-- Text shown in the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_description" moz:removedIn="124" tools:ignore="UnusedResources">Open it in the menu</string>
+ <!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
+ <string name="mozac_feature_addons_installed_dialog_description_2">Access %1$s from the %2$s menu.</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button" moz:removedIn="124" tools:ignore="UnusedResources">Okay, Got it</string>
+ <!-- Confirmation button text for the dialog when add-on installation is completed. -->
+ <string name="mozac_feature_addons_installed_dialog_okay_button_2">OK</string>
+ <!-- "Learn more" link displayed below an add-on status message. -->
+ <string name="mozac_feature_addons_status_learn_more">Learn more</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on has been blocklisted. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_blocklisted">%1$s has been disabled due to security or stability issues.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on hasn't been signed correctly. %1$s is the add-on name. -->
+ <string name="mozac_feature_addons_status_unsigned">%1$s could not be verified as secure and has been disabled.</string>
+ <!-- Status message below an add-on in the add-ons manager when this add-on isn't compatible with the application version. %1$s is the add-on name, %2$s is the app name and %3$s is the app version. -->
+ <string name="mozac_feature_addons_status_incompatible">%1$s is not compatible with your version of %2$s (version %3$s).</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt
new file mode 100644
index 0000000000..9c30bd8c0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt
@@ -0,0 +1,1001 @@
+/* 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.addons
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.ActionHandler
+import mozilla.components.concept.engine.webextension.DisabledFlags
+import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.APP_SUPPORT
+import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.APP_VERSION
+import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.BLOCKLIST
+import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.SIGNATURE
+import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.USER
+import mozilla.components.concept.engine.webextension.EnableSource
+import mozilla.components.concept.engine.webextension.InstallationMethod
+import mozilla.components.concept.engine.webextension.Metadata
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.feature.addons.update.AddonUpdater.Status
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import mozilla.components.support.webextensions.WebExtensionSupport
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+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.doNothing
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class AddonManagerTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ WebExtensionSupport.installedExtensions.clear()
+ }
+
+ @After
+ fun after() {
+ WebExtensionSupport.installedExtensions.clear()
+ }
+
+ @Test
+ fun `getAddons - queries addons from provider and updates installation state`() = runTestOnMain {
+ // Prepare addons provider
+ // addon1 (ext1) is a featured extension that is already installed.
+ // addon2 (ext2) is a featured extension that is not installed.
+ // addon3 (ext3) is a featured extension that is marked as disabled.
+ // addon4 (ext4) and addon5 (ext5) are not featured extensions but they are installed.
+ val addonsProvider: AddonsProvider = mock()
+
+ whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(Addon(id = "ext1"), Addon(id = "ext2"), Addon(id = "ext3")))
+
+ // Prepare engine
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(emptyList())
+ }
+ val store = BrowserStore(
+ BrowserState(
+ extensions = mapOf(
+ "ext1" to WebExtensionState("ext1", "url"),
+ "ext4" to WebExtensionState("ext4", "url"),
+ "ext5" to WebExtensionState("ext5", "url"),
+ // ext6 is a temporarily loaded extension.
+ "ext6" to WebExtensionState("ext6", "url"),
+ // ext7 is a built-in extension.
+ "ext7" to WebExtensionState("ext7", "url"),
+ ),
+ ),
+ )
+
+ WebExtensionSupport.initialize(engine, store)
+ val ext1: WebExtension = mock()
+ whenever(ext1.id).thenReturn("ext1")
+ whenever(ext1.isEnabled()).thenReturn(true)
+ WebExtensionSupport.installedExtensions["ext1"] = ext1
+
+ // Make `ext3` an extension that is disabled because it wasn't supported.
+ val newlySupportedExtension: WebExtension = mock()
+ val metadata: Metadata = mock()
+ whenever(newlySupportedExtension.isEnabled()).thenReturn(false)
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT))
+ whenever(metadata.optionsPageUrl).thenReturn("http://options-page.moz")
+ whenever(metadata.openOptionsPageInTab).thenReturn(true)
+ whenever(newlySupportedExtension.id).thenReturn("ext3")
+ whenever(newlySupportedExtension.url).thenReturn("site_url")
+ whenever(newlySupportedExtension.getMetadata()).thenReturn(metadata)
+ WebExtensionSupport.installedExtensions["ext3"] = newlySupportedExtension
+
+ val ext4: WebExtension = mock()
+ whenever(ext4.id).thenReturn("ext4")
+ whenever(ext4.isEnabled()).thenReturn(true)
+ val ext4Metadata: Metadata = mock()
+ whenever(ext4Metadata.temporary).thenReturn(false)
+ whenever(ext4.getMetadata()).thenReturn(ext4Metadata)
+ WebExtensionSupport.installedExtensions["ext4"] = ext4
+
+ val ext5: WebExtension = mock()
+ whenever(ext5.id).thenReturn("ext5")
+ whenever(ext5.isEnabled()).thenReturn(true)
+ val ext5Metadata: Metadata = mock()
+ whenever(ext5Metadata.temporary).thenReturn(false)
+ whenever(ext5.getMetadata()).thenReturn(ext5Metadata)
+ WebExtensionSupport.installedExtensions["ext5"] = ext5
+
+ val ext6: WebExtension = mock()
+ whenever(ext6.id).thenReturn("ext6")
+ whenever(ext6.url).thenReturn("some url")
+ whenever(ext6.isEnabled()).thenReturn(true)
+ val ext6Metadata: Metadata = mock()
+ whenever(ext6Metadata.name).thenReturn("temporarily loaded extension - ext6")
+ whenever(ext6Metadata.temporary).thenReturn(true)
+ whenever(ext6.getMetadata()).thenReturn(ext6Metadata)
+ WebExtensionSupport.installedExtensions["ext6"] = ext6
+
+ val ext7: WebExtension = mock()
+ whenever(ext7.id).thenReturn("ext7")
+ whenever(ext7.isEnabled()).thenReturn(true)
+ whenever(ext7.isBuiltIn()).thenReturn(true)
+ WebExtensionSupport.installedExtensions["ext7"] = ext7
+
+ // Verify add-ons were updated with state provided by the engine/store.
+ val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons()
+ assertEquals(6, addons.size)
+
+ // ext1 should be installed.
+ val addon1 = addons.find { it.id == "ext1" }!!
+
+ assertEquals("ext1", addon1.id)
+ assertNotNull(addon1.installedState)
+ assertEquals("ext1", addon1.installedState!!.id)
+ assertTrue(addon1.isEnabled())
+ assertFalse(addon1.isDisabledAsUnsupported())
+ assertNull(addon1.installedState!!.optionsPageUrl)
+ assertFalse(addon1.installedState!!.openOptionsPageInTab)
+
+ // ext2 should not be installed.
+ val addon2 = addons.find { it.id == "ext2" }!!
+ assertEquals("ext2", addon2.id)
+ assertNull(addon2.installedState)
+
+ // ext3 should now be marked as supported but still be disabled as unsupported.
+ val addon3 = addons.find { it.id == "ext3" }!!
+ assertEquals("ext3", addon3.id)
+ assertNotNull(addon3.installedState)
+ assertEquals("ext3", addon3.installedState!!.id)
+ assertTrue(addon3.isSupported())
+ assertFalse(addon3.isEnabled())
+ assertTrue(addon3.isDisabledAsUnsupported())
+ assertEquals("http://options-page.moz", addon3.installedState!!.optionsPageUrl)
+ assertTrue(addon3.installedState!!.openOptionsPageInTab)
+
+ // ext4 should be installed.
+ val addon4 = addons.find { it.id == "ext4" }!!
+ assertEquals("ext4", addon4.id)
+ assertNotNull(addon4.installedState)
+ assertEquals("ext4", addon4.installedState!!.id)
+ assertTrue(addon4.isEnabled())
+ assertFalse(addon4.isDisabledAsUnsupported())
+ assertNull(addon4.installedState!!.optionsPageUrl)
+ assertFalse(addon4.installedState!!.openOptionsPageInTab)
+
+ // ext5 should be installed.
+ val addon5 = addons.find { it.id == "ext5" }!!
+ assertEquals("ext5", addon5.id)
+ assertNotNull(addon5.installedState)
+ assertEquals("ext5", addon5.installedState!!.id)
+ assertTrue(addon5.isEnabled())
+ assertFalse(addon5.isDisabledAsUnsupported())
+ assertNull(addon5.installedState!!.optionsPageUrl)
+ assertFalse(addon5.installedState!!.openOptionsPageInTab)
+
+ // ext6 should be installed.
+ val addon6 = addons.find { it.id == "ext6" }!!
+ assertEquals("ext6", addon6.id)
+ assertNotNull(addon6.installedState)
+ assertEquals("ext6", addon6.installedState!!.id)
+ assertTrue(addon6.isEnabled())
+ assertFalse(addon6.isDisabledAsUnsupported())
+ assertNull(addon6.installedState!!.optionsPageUrl)
+ assertFalse(addon6.installedState!!.openOptionsPageInTab)
+ }
+
+ @Test
+ fun `getAddons - returns temporary add-ons as supported`() = runTestOnMain {
+ val addonsProvider: AddonsProvider = mock()
+ whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf())
+
+ // Prepare engine
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(emptyList())
+ }
+
+ val store = BrowserStore()
+ WebExtensionSupport.initialize(engine, store)
+
+ // Add temporary extension
+ val temporaryExtension: WebExtension = mock()
+ val temporaryExtensionIcon: Bitmap = mock()
+ val temporaryExtensionMetadata: Metadata = mock()
+ whenever(temporaryExtensionMetadata.temporary).thenReturn(true)
+ whenever(temporaryExtensionMetadata.name).thenReturn("name")
+ whenever(temporaryExtension.id).thenReturn("temp_ext")
+ whenever(temporaryExtension.url).thenReturn("site_url")
+ whenever(temporaryExtension.getMetadata()).thenReturn(temporaryExtensionMetadata)
+ WebExtensionSupport.installedExtensions["temp_ext"] = temporaryExtension
+
+ val addonManager = spy(AddonManager(store, mock(), addonsProvider, mock()))
+
+ whenever(addonManager.loadIcon(temporaryExtension)).thenReturn(temporaryExtensionIcon)
+
+ val addons = addonManager.getAddons()
+ assertEquals(1, addons.size)
+
+ // Temporary extension should be returned and marked as supported
+ assertEquals("temp_ext", addons[0].id)
+ assertEquals(1, addons[0].translatableName.size)
+ assertNotNull(addons[0].translatableName[addons[0].defaultLocale])
+ assertTrue(addons[0].translatableName.containsValue("name"))
+ assertNotNull(addons[0].installedState)
+ assertTrue(addons[0].isSupported())
+ assertEquals(temporaryExtensionIcon, addons[0].installedState!!.icon)
+ }
+
+ @Test
+ fun `getAddons - filters unneeded locales on featured add-ons`() = runTestOnMain {
+ val addon = Addon(
+ id = "addon1",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "invalid1" to "Name", "invalid2" to "nombre"),
+ translatableDescription = mapOf(Addon.DEFAULT_LOCALE to "description", "invalid1" to "Beschreibung", "invalid2" to "descripción"),
+ translatableSummary = mapOf(Addon.DEFAULT_LOCALE to "summary", "invalid1" to "Kurzfassung", "invalid2" to "resumen"),
+ )
+
+ val store = BrowserStore()
+
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(emptyList())
+ }
+
+ val addonsProvider: AddonsProvider = mock()
+ whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(addon))
+ WebExtensionSupport.initialize(engine, store)
+
+ val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons()
+ assertEquals(1, addons[0].translatableName.size)
+ assertTrue(addons[0].translatableName.contains(addons[0].defaultLocale))
+ assertEquals(1, addons[0].translatableDescription.size)
+ assertTrue(addons[0].translatableDescription.contains(addons[0].defaultLocale))
+ assertEquals(1, addons[0].translatableSummary.size)
+ assertTrue(addons[0].translatableSummary.contains(addons[0].defaultLocale))
+ }
+
+ @Test
+ fun `getAddons - filters unneeded locales on non-featured installed add-ons`() = runTestOnMain {
+ val addon = Addon(
+ id = "addon1",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "invalid1" to "Name", "invalid2" to "nombre"),
+ translatableDescription = mapOf(Addon.DEFAULT_LOCALE to "description", "invalid1" to "Beschreibung", "invalid2" to "descripción"),
+ translatableSummary = mapOf(Addon.DEFAULT_LOCALE to "summary", "invalid1" to "Kurzfassung", "invalid2" to "resumen"),
+ )
+
+ val store = BrowserStore()
+
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(emptyList())
+ }
+
+ val addonsProvider: AddonsProvider = mock()
+ whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(emptyList())
+ WebExtensionSupport.initialize(engine, store)
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn(addon.id)
+ whenever(extension.isEnabled()).thenReturn(true)
+ whenever(extension.getMetadata()).thenReturn(mock())
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+
+ val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons()
+ assertEquals(1, addons[0].translatableName.size)
+ assertTrue(addons[0].translatableName.contains(addons[0].defaultLocale))
+ assertEquals(1, addons[0].translatableDescription.size)
+ assertTrue(addons[0].translatableDescription.contains(addons[0].defaultLocale))
+ assertEquals(1, addons[0].translatableSummary.size)
+ assertTrue(addons[0].translatableSummary.contains(addons[0].defaultLocale))
+ }
+
+ @Test
+ fun `getAddons - suspends until pending actions are completed`() = runTestOnMain {
+ val addon = Addon(
+ id = "ext1",
+ installedState = Addon.InstalledState("ext1", "1.0", "", true),
+ )
+
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+
+ val store = BrowserStore()
+
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(emptyList())
+ }
+ val addonsProvider: AddonsProvider = mock()
+
+ whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(addon))
+ WebExtensionSupport.initialize(engine, store)
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+
+ val addonManager = AddonManager(store, mock(), addonsProvider, mock())
+ addonManager.installAddon(url = addon.downloadUrl)
+ addonManager.enableAddon(addon)
+ addonManager.disableAddon(addon)
+ addonManager.uninstallAddon(addon)
+ assertEquals(4, addonManager.pendingAddonActions.size)
+
+ var getAddonsResult: List<Addon>? = null
+ val nonSuspendingJob = CoroutineScope(Dispatchers.IO).launch {
+ getAddonsResult = addonManager.getAddons(waitForPendingActions = false)
+ }
+
+ nonSuspendingJob.join()
+ assertNotNull(getAddonsResult)
+
+ getAddonsResult = null
+ val suspendingJob = CoroutineScope(Dispatchers.IO).launch {
+ getAddonsResult = addonManager.getAddons(waitForPendingActions = true)
+ }
+
+ addonManager.pendingAddonActions.forEach { it.complete(Unit) }
+
+ suspendingJob.join()
+ assertNotNull(getAddonsResult)
+ }
+
+ @Test
+ fun `getAddons - passes on allowCache parameter`() = runTestOnMain {
+ val store = BrowserStore()
+
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(emptyList())
+ }
+ WebExtensionSupport.initialize(engine, store)
+
+ val addonsProvider: AddonsProvider = mock()
+ whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(emptyList())
+ val addonsManager = AddonManager(store, mock(), addonsProvider, mock())
+
+ addonsManager.getAddons()
+ verify(addonsProvider).getFeaturedAddons(eq(true), eq(null), language = anyString())
+
+ addonsManager.getAddons(allowCache = false)
+ verify(addonsProvider).getFeaturedAddons(eq(false), eq(null), language = anyString())
+ Unit
+ }
+
+ @Test
+ fun `updateAddon - when a extension is updated successfully`() {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "1", url = "https://www.mozilla.org", engineSession = engineSession),
+ ),
+ extensions = mapOf("extensionId" to mock()),
+ ),
+ ),
+ )
+ val onSuccessCaptor = argumentCaptor<((WebExtension?) -> Unit)>()
+ var updateStatus: Status? = null
+ val manager = AddonManager(store, engine, mock(), mock())
+
+ val updatedExt: WebExtension = mock()
+ whenever(updatedExt.id).thenReturn("extensionId")
+ whenever(updatedExt.url).thenReturn("url")
+ whenever(updatedExt.supportActions).thenReturn(true)
+
+ WebExtensionSupport.installedExtensions["extensionId"] = mock()
+
+ val oldExt = WebExtensionSupport.installedExtensions["extensionId"]
+
+ manager.updateAddon("extensionId") { status ->
+ updateStatus = status
+ }
+
+ val actionHandlerCaptor = argumentCaptor<ActionHandler>()
+ val actionCaptor = argumentCaptor<WebExtensionAction.UpdateWebExtensionAction>()
+
+ // Verifying we returned the right status
+ verify(engine).updateWebExtension(any(), onSuccessCaptor.capture(), any())
+ onSuccessCaptor.value.invoke(updatedExt)
+ assertEquals(Status.SuccessfullyUpdated, updateStatus)
+
+ // Verifying we updated the extension in WebExtensionSupport
+ assertNotEquals(oldExt, WebExtensionSupport.installedExtensions["extensionId"])
+ assertEquals(updatedExt, WebExtensionSupport.installedExtensions["extensionId"])
+
+ // Verifying we updated the extension in the store
+ verify(store).dispatch(actionCaptor.capture())
+ assertEquals(
+ WebExtensionState(updatedExt.id, updatedExt.url, updatedExt.getMetadata()?.name, updatedExt.isEnabled()),
+ actionCaptor.allValues.last().updatedExtension,
+ )
+
+ // Verify that we registered an action handler for all existing sessions on the extension
+ verify(updatedExt).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture())
+ actionHandlerCaptor.value.onBrowserAction(updatedExt, engineSession, mock())
+ }
+
+ @Test
+ fun `updateAddon - when extension is not installed`() {
+ var updateStatus: Status? = null
+
+ val manager = AddonManager(mock(), mock(), mock(), mock())
+
+ manager.updateAddon("extensionId") { status ->
+ updateStatus = status
+ }
+
+ assertEquals(Status.NotInstalled, updateStatus)
+ }
+
+ @Test
+ fun `updateAddon - when extension is not supported`() {
+ var updateStatus: Status? = null
+
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("unsupportedExt")
+
+ val metadata: Metadata = mock()
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT))
+ whenever(extension.getMetadata()).thenReturn(metadata)
+
+ WebExtensionSupport.installedExtensions["extensionId"] = extension
+
+ val manager = AddonManager(mock(), mock(), mock(), mock())
+ manager.updateAddon("extensionId") { status ->
+ updateStatus = status
+ }
+
+ assertEquals(Status.NotInstalled, updateStatus)
+ }
+
+ @Test
+ fun `updateAddon - when an error happens while updating`() {
+ val engine: Engine = mock()
+ val onErrorCaptor = argumentCaptor<((String, Throwable) -> Unit)>()
+ var updateStatus: Status? = null
+ val manager = AddonManager(mock(), engine, mock(), mock())
+
+ WebExtensionSupport.installedExtensions["extensionId"] = mock()
+
+ manager.updateAddon("extensionId") { status ->
+ updateStatus = status
+ }
+
+ // Verifying we returned the right status
+ verify(engine).updateWebExtension(any(), any(), onErrorCaptor.capture())
+ onErrorCaptor.value.invoke("message", Exception())
+ assertTrue(updateStatus is Status.Error)
+ }
+
+ @Test
+ fun `updateAddon - when there is not new updates for the extension`() {
+ val engine: Engine = mock()
+ val onSuccessCaptor = argumentCaptor<((WebExtension?) -> Unit)>()
+ var updateStatus: Status? = null
+ val manager = AddonManager(mock(), engine, mock(), mock())
+
+ WebExtensionSupport.installedExtensions["extensionId"] = mock()
+ manager.updateAddon("extensionId") { status ->
+ updateStatus = status
+ }
+
+ verify(engine).updateWebExtension(any(), onSuccessCaptor.capture(), any())
+ onSuccessCaptor.value.invoke(null)
+ assertEquals(Status.NoUpdateAvailable, updateStatus)
+ }
+
+ @Test
+ fun `installAddon successfully`() {
+ val addon = Addon(id = "ext1")
+ val engine: Engine = mock()
+ val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>()
+
+ var installedAddon: Addon? = null
+ val manager = AddonManager(mock(), engine, mock(), mock())
+ manager.installAddon(
+ url = addon.downloadUrl,
+ installationMethod = InstallationMethod.MANAGER,
+ onSuccess = {
+ installedAddon = it
+ },
+ )
+
+ verify(engine).installWebExtension(
+ any(),
+ eq(InstallationMethod.MANAGER),
+ onSuccessCaptor.capture(),
+ any(),
+ )
+
+ val metadata: Metadata = mock()
+ val extension: WebExtension = mock()
+ whenever(metadata.name).thenReturn("nameFromMetadata")
+ whenever(extension.id).thenReturn("ext1")
+ whenever(extension.getMetadata()).thenReturn(metadata)
+ onSuccessCaptor.value.invoke(extension)
+ assertNotNull(installedAddon)
+ assertEquals(addon.id, installedAddon!!.id)
+ assertEquals("nameFromMetadata", installedAddon!!.translateName(testContext))
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `installAddon failure`() {
+ val addon = Addon(id = "ext1")
+ val engine: Engine = mock()
+ val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>()
+
+ var throwable: Throwable? = null
+ val manager = AddonManager(mock(), engine, mock(), mock())
+ manager.installAddon(
+ url = addon.downloadUrl,
+ installationMethod = InstallationMethod.FROM_FILE,
+ onError = { caught ->
+ throwable = caught
+ },
+ )
+
+ verify(engine).installWebExtension(
+ url = any(),
+ installationMethod = eq(InstallationMethod.FROM_FILE),
+ onSuccess = any(),
+ onError = onErrorCaptor.capture(),
+ )
+
+ onErrorCaptor.value.invoke(IllegalStateException("test"))
+ assertNotNull(throwable!!)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `uninstallAddon successfully`() {
+ val installedAddon = Addon(
+ id = "ext1",
+ installedState = Addon.InstalledState("ext1", "1.0", "", true),
+ )
+
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+ WebExtensionSupport.installedExtensions[installedAddon.id] = extension
+
+ val engine: Engine = mock()
+ val onSuccessCaptor = argumentCaptor<(() -> Unit)>()
+
+ var successCallbackInvoked = false
+ val manager = AddonManager(mock(), engine, mock(), mock())
+ manager.uninstallAddon(
+ installedAddon,
+ onSuccess = {
+ successCallbackInvoked = true
+ },
+ )
+ verify(engine).uninstallWebExtension(eq(extension), onSuccessCaptor.capture(), any())
+
+ onSuccessCaptor.value.invoke()
+ assertTrue(successCallbackInvoked)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `uninstallAddon failure cases`() {
+ val addon = Addon(id = "ext1")
+ val engine: Engine = mock()
+ val onErrorCaptor = argumentCaptor<((String, Throwable) -> Unit)>()
+ var throwable: Throwable? = null
+ var msg: String? = null
+ val errorCallback = { errorMsg: String, caught: Throwable ->
+ throwable = caught
+ msg = errorMsg
+ }
+ val manager = AddonManager(mock(), engine, mock(), mock())
+
+ // Extension is not installed so we're invoking the error callback and never the engine
+ manager.uninstallAddon(addon, onError = errorCallback)
+ verify(engine, never()).uninstallWebExtension(any(), any(), onErrorCaptor.capture())
+ assertNotNull(throwable!!)
+ assertEquals("Addon is not installed", throwable!!.localizedMessage)
+
+ // Install extension and try again
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+ manager.uninstallAddon(addon, onError = errorCallback)
+ verify(engine, never()).uninstallWebExtension(any(), any(), onErrorCaptor.capture())
+
+ // Make sure engine error is forwarded to caller
+ val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true))
+ manager.uninstallAddon(installedAddon, onError = errorCallback)
+ verify(engine).uninstallWebExtension(eq(extension), any(), onErrorCaptor.capture())
+ onErrorCaptor.value.invoke(addon.id, IllegalStateException("test"))
+ assertNotNull(throwable!!)
+ assertEquals("test", throwable!!.localizedMessage)
+ assertEquals(msg, addon.id)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `add optional permissions successfully`() {
+ val permission = listOf("permission1")
+ val origin = listOf("origin")
+ val addon = Addon(
+ id = "ext1",
+ installedState = Addon.InstalledState("ext1", "1.0", "", true),
+ )
+
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+
+ val engine: Engine = mock()
+ val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>()
+
+ var updateAddon: Addon? = null
+ val manager = AddonManager(mock(), engine, mock(), mock())
+ manager.addOptionalPermission(
+ addon,
+ permission,
+ origin,
+ onSuccess = {
+ updateAddon = it
+ },
+ )
+
+ verify(engine).addOptionalPermissions(eq(extension.id), any(), any(), onSuccessCaptor.capture(), any())
+ onSuccessCaptor.value.invoke(extension)
+ assertNotNull(updateAddon)
+ assertEquals(addon.id, updateAddon!!.id)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `add optional with empty permissions and origins`() {
+ var onErrorWasExecuted = false
+ val manager = AddonManager(mock(), mock(), mock(), mock())
+
+ manager.addOptionalPermission(
+ mock(),
+ emptyList(),
+ emptyList(),
+ onError = {
+ onErrorWasExecuted = true
+ },
+ )
+
+ assertTrue(onErrorWasExecuted)
+ }
+
+ @Test
+ fun `remove optional permissions successfully`() {
+ val permission = listOf("permission1")
+ val origins = listOf("origin")
+ val addon = Addon(
+ id = "ext1",
+ installedState = Addon.InstalledState("ext1", "1.0", "", true),
+ )
+
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+
+ val engine: Engine = mock()
+ val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>()
+
+ var updateAddon: Addon? = null
+ val manager = AddonManager(mock(), engine, mock(), mock())
+ manager.removeOptionalPermission(
+ addon,
+ permission,
+ origins,
+ onSuccess = {
+ updateAddon = it
+ },
+ )
+
+ verify(engine).removeOptionalPermissions(eq(extension.id), any(), any(), onSuccessCaptor.capture(), any())
+ onSuccessCaptor.value.invoke(extension)
+ assertNotNull(updateAddon)
+ assertEquals(addon.id, updateAddon!!.id)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `remove optional with empty permissions and origins`() {
+ var onErrorWasExecuted = false
+ val manager = AddonManager(mock(), mock(), mock(), mock())
+
+ manager.removeOptionalPermission(
+ mock(),
+ emptyList(),
+ emptyList(),
+ onError = {
+ onErrorWasExecuted = true
+ },
+ )
+
+ assertTrue(onErrorWasExecuted)
+ }
+
+ @Test
+ fun `enableAddon successfully`() {
+ val addon = Addon(
+ id = "ext1",
+ installedState = Addon.InstalledState("ext1", "1.0", "", true),
+ )
+
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+
+ val engine: Engine = mock()
+ val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>()
+
+ var enabledAddon: Addon? = null
+ val manager = AddonManager(mock(), engine, mock(), mock())
+ manager.enableAddon(
+ addon,
+ onSuccess = {
+ enabledAddon = it
+ },
+ )
+
+ verify(engine).enableWebExtension(eq(extension), any(), onSuccessCaptor.capture(), any())
+ onSuccessCaptor.value.invoke(extension)
+ assertNotNull(enabledAddon)
+ assertEquals(addon.id, enabledAddon!!.id)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `enableAddon failure cases`() {
+ val addon = Addon(id = "ext1")
+ val engine: Engine = mock()
+ val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>()
+ var throwable: Throwable? = null
+ val errorCallback = { caught: Throwable ->
+ throwable = caught
+ }
+ val manager = AddonManager(mock(), engine, mock(), mock())
+
+ // Extension is not installed so we're invoking the error callback and never the engine
+ manager.enableAddon(addon, onError = errorCallback)
+ verify(engine, never()).enableWebExtension(any(), any(), any(), onErrorCaptor.capture())
+ assertNotNull(throwable!!)
+ assertEquals("Addon is not installed", throwable!!.localizedMessage)
+
+ // Install extension and try again
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+ manager.enableAddon(addon, onError = errorCallback)
+ verify(engine, never()).enableWebExtension(any(), any(), any(), onErrorCaptor.capture())
+
+ // Make sure engine error is forwarded to caller
+ val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true))
+ manager.enableAddon(installedAddon, source = EnableSource.APP_SUPPORT, onError = errorCallback)
+ verify(engine).enableWebExtension(eq(extension), eq(EnableSource.APP_SUPPORT), any(), onErrorCaptor.capture())
+ onErrorCaptor.value.invoke(IllegalStateException("test"))
+ assertNotNull(throwable!!)
+ assertEquals("test", throwable!!.localizedMessage)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `disableAddon successfully`() {
+ val addon = Addon(
+ id = "ext1",
+ installedState = Addon.InstalledState("ext1", "1.0", "", true),
+ )
+
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+
+ val engine: Engine = mock()
+ val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>()
+
+ var disabledAddon: Addon? = null
+ val manager = AddonManager(mock(), engine, mock(), mock())
+ manager.disableAddon(
+ addon,
+ source = EnableSource.APP_SUPPORT,
+ onSuccess = {
+ disabledAddon = it
+ },
+ )
+
+ verify(engine).disableWebExtension(eq(extension), eq(EnableSource.APP_SUPPORT), onSuccessCaptor.capture(), any())
+ onSuccessCaptor.value.invoke(extension)
+ assertNotNull(disabledAddon)
+ assertEquals(addon.id, disabledAddon!!.id)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `disableAddon failure cases`() {
+ val addon = Addon(id = "ext1")
+ val engine: Engine = mock()
+ val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>()
+ var throwable: Throwable? = null
+ val errorCallback = { caught: Throwable ->
+ throwable = caught
+ }
+ val manager = AddonManager(mock(), engine, mock(), mock())
+
+ // Extension is not installed so we're invoking the error callback and never the engine
+ manager.disableAddon(addon, onError = errorCallback)
+ verify(engine, never()).disableWebExtension(any(), any(), any(), onErrorCaptor.capture())
+ assertNotNull(throwable!!)
+ assertEquals("Addon is not installed", throwable!!.localizedMessage)
+
+ // Install extension and try again
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("ext1")
+ WebExtensionSupport.installedExtensions[addon.id] = extension
+ manager.disableAddon(addon, onError = errorCallback)
+ verify(engine, never()).disableWebExtension(any(), any(), any(), onErrorCaptor.capture())
+
+ // Make sure engine error is forwarded to caller
+ val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true))
+ manager.disableAddon(installedAddon, onError = errorCallback)
+ verify(engine).disableWebExtension(eq(extension), any(), any(), onErrorCaptor.capture())
+ onErrorCaptor.value.invoke(IllegalStateException("test"))
+ assertNotNull(throwable!!)
+ assertEquals("test", throwable!!.localizedMessage)
+ assertTrue(manager.pendingAddonActions.isEmpty())
+ }
+
+ @Test
+ fun `toInstalledState read from icon cache`() {
+ val extension: WebExtension = mock()
+ val metadata: Metadata = mock()
+
+ val manager = spy(AddonManager(mock(), mock(), mock(), mock()))
+
+ manager.iconsCache["ext1"] = mock()
+ whenever(extension.id).thenReturn("ext1")
+ whenever(extension.getMetadata()).thenReturn(metadata)
+ whenever(extension.isEnabled()).thenReturn(true)
+ whenever(extension.getDisabledReason()).thenReturn(null)
+ whenever(extension.isAllowedInPrivateBrowsing()).thenReturn(true)
+ whenever(metadata.version).thenReturn("version")
+ whenever(metadata.optionsPageUrl).thenReturn("optionsPageUrl")
+ whenever(metadata.openOptionsPageInTab).thenReturn(true)
+
+ val installedExtension = manager.toInstalledState(extension)
+
+ assertEquals(manager.iconsCache["ext1"], installedExtension.icon)
+ assertEquals("version", installedExtension.version)
+ assertEquals("optionsPageUrl", installedExtension.optionsPageUrl)
+ assertNull(installedExtension.disabledReason)
+ assertTrue(installedExtension.openOptionsPageInTab)
+ assertTrue(installedExtension.enabled)
+ assertTrue(installedExtension.allowedInPrivateBrowsing)
+
+ verify(manager, times(0)).loadIcon(eq(extension))
+ }
+
+ @Test
+ fun `toInstalledState load icon when cache is not available`() {
+ val extension: WebExtension = mock()
+ val metadata: Metadata = mock()
+
+ val manager = spy(AddonManager(mock(), mock(), mock(), mock()))
+
+ whenever(extension.id).thenReturn("ext1")
+ whenever(extension.getMetadata()).thenReturn(metadata)
+ whenever(extension.isEnabled()).thenReturn(true)
+ whenever(extension.getDisabledReason()).thenReturn(null)
+ whenever(extension.isAllowedInPrivateBrowsing()).thenReturn(true)
+ whenever(metadata.version).thenReturn("version")
+ whenever(metadata.optionsPageUrl).thenReturn("optionsPageUrl")
+ whenever(metadata.openOptionsPageInTab).thenReturn(true)
+
+ val installedExtension = manager.toInstalledState(extension)
+
+ assertEquals(manager.iconsCache["ext1"], installedExtension.icon)
+ assertEquals("version", installedExtension.version)
+ assertEquals("optionsPageUrl", installedExtension.optionsPageUrl)
+ assertNull(installedExtension.disabledReason)
+ assertTrue(installedExtension.openOptionsPageInTab)
+ assertTrue(installedExtension.enabled)
+ assertTrue(installedExtension.allowedInPrivateBrowsing)
+
+ verify(manager).loadIcon(extension)
+ }
+
+ @Test
+ fun `loadIcon try to load the icon from extension`() = runTestOnMain {
+ val extension: WebExtension = mock()
+
+ val manager = spy(AddonManager(mock(), mock(), mock(), mock()))
+
+ whenever(extension.loadIcon(AddonManager.ADDON_ICON_SIZE)).thenReturn(mock())
+
+ val icon = manager.loadIcon(extension)
+
+ assertNotNull(icon)
+ }
+
+ @Test
+ fun `loadIcon calls tryLoadIconInBackground when TimeoutCancellationException`() =
+ runTestOnMain {
+ val extension: WebExtension = mock()
+
+ val manager = spy(AddonManager(mock(), mock(), mock(), mock()))
+ doNothing().`when`(manager).tryLoadIconInBackground(extension)
+
+ doThrow(mock<TimeoutCancellationException>()).`when`(extension)
+ .loadIcon(AddonManager.ADDON_ICON_SIZE)
+
+ val icon = manager.loadIcon(extension)
+
+ assertNull(icon)
+ verify(manager).loadIcon(extension)
+ }
+
+ @Test
+ fun `getDisabledReason cases`() {
+ val extension: WebExtension = mock()
+ val metadata: Metadata = mock()
+ whenever(extension.getMetadata()).thenReturn(metadata)
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(BLOCKLIST))
+ assertEquals(Addon.DisabledReason.BLOCKLISTED, extension.getDisabledReason())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT))
+ assertEquals(Addon.DisabledReason.UNSUPPORTED, extension.getDisabledReason())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(USER))
+ assertEquals(Addon.DisabledReason.USER_REQUESTED, extension.getDisabledReason())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(SIGNATURE))
+ assertEquals(Addon.DisabledReason.NOT_CORRECTLY_SIGNED, extension.getDisabledReason())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_VERSION))
+ assertEquals(Addon.DisabledReason.INCOMPATIBLE, extension.getDisabledReason())
+
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(0))
+ assertNull(extension.getDisabledReason())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/AddonTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/AddonTest.kt
new file mode 100644
index 0000000000..51d5d4a26c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/AddonTest.kt
@@ -0,0 +1,514 @@
+/* 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.addons
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.webextension.DisabledFlags
+import mozilla.components.concept.engine.webextension.Incognito
+import mozilla.components.concept.engine.webextension.Metadata
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+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.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AddonTest {
+
+ @Test
+ fun `translatePermissions - must return the expected string ids per permission category`() {
+ val addon = Addon(
+ id = "id",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = listOf(
+ "bookmarks",
+ "browserSettings",
+ "browsingData",
+ "clipboardRead",
+ "clipboardWrite",
+ "declarativeNetRequest",
+ "declarativeNetRequestFeedback",
+ "downloads",
+ "downloads.open",
+ "find",
+ "geolocation",
+ "history",
+ "management",
+ "nativeMessaging",
+ "notifications",
+ "pkcs11",
+ "privacy",
+ "proxy",
+ "sessions",
+ "tabHide",
+ "tabs",
+ "topSites",
+ "unlimitedStorage",
+ "webNavigation",
+ "devtools",
+ ),
+ createdAt = "",
+ updatedAt = "",
+ )
+
+ val translatedPermissions = addon.translatePermissions(testContext)
+ val expectedPermissions = listOf(
+ R.string.mozac_feature_addons_permissions_bookmarks_description,
+ R.string.mozac_feature_addons_permissions_browser_setting_description,
+ R.string.mozac_feature_addons_permissions_browser_data_description,
+ R.string.mozac_feature_addons_permissions_clipboard_read_description,
+ R.string.mozac_feature_addons_permissions_clipboard_write_description,
+ R.string.mozac_feature_addons_permissions_declarative_net_request_description,
+ R.string.mozac_feature_addons_permissions_declarative_net_request_feedback_description,
+ R.string.mozac_feature_addons_permissions_downloads_description,
+ R.string.mozac_feature_addons_permissions_downloads_open_description,
+ R.string.mozac_feature_addons_permissions_find_description,
+ R.string.mozac_feature_addons_permissions_geolocation_description,
+ R.string.mozac_feature_addons_permissions_history_description,
+ R.string.mozac_feature_addons_permissions_management_description,
+ R.string.mozac_feature_addons_permissions_native_messaging_description,
+ R.string.mozac_feature_addons_permissions_notifications_description,
+ R.string.mozac_feature_addons_permissions_pkcs11_description,
+ R.string.mozac_feature_addons_permissions_privacy_description,
+ R.string.mozac_feature_addons_permissions_proxy_description,
+ R.string.mozac_feature_addons_permissions_sessions_description,
+ R.string.mozac_feature_addons_permissions_tab_hide_description,
+ R.string.mozac_feature_addons_permissions_tabs_description,
+ R.string.mozac_feature_addons_permissions_top_sites_description,
+ R.string.mozac_feature_addons_permissions_unlimited_storage_description,
+ R.string.mozac_feature_addons_permissions_web_navigation_description,
+ R.string.mozac_feature_addons_permissions_devtools_description,
+ ).map { testContext.getString(it) }
+ assertEquals(expectedPermissions, translatedPermissions)
+ }
+
+ @Test
+ fun `isInstalled - true if installed state present and otherwise false`() {
+ val addon = Addon(
+ id = "id",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ )
+ assertFalse(addon.isInstalled())
+
+ val installedAddon = addon.copy(installedState = Addon.InstalledState("id", "1.0", ""))
+ assertTrue(installedAddon.isInstalled())
+ }
+
+ @Test
+ fun `isEnabled - true if installed state enabled and otherwise false`() {
+ val addon = Addon(
+ id = "id",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ )
+ assertFalse(addon.isEnabled())
+
+ val installedAddon = addon.copy(installedState = Addon.InstalledState("id", "1.0", ""))
+ assertFalse(installedAddon.isEnabled())
+
+ val enabledAddon = addon.copy(installedState = Addon.InstalledState("id", "1.0", "", enabled = true))
+ assertTrue(enabledAddon.isEnabled())
+ }
+
+ @Test
+ fun `filterTranslations - only keeps specified translations`() {
+ val addon = Addon(
+ id = "id",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "de" to "Name", "es" to "nombre"),
+ translatableDescription = mapOf(Addon.DEFAULT_LOCALE to "description", "de" to "Beschreibung", "es" to "descripción"),
+ translatableSummary = mapOf(Addon.DEFAULT_LOCALE to "summary", "de" to "Kurzfassung", "es" to "resumen"),
+ )
+
+ val addonEn = addon.filterTranslations(listOf())
+ assertEquals(1, addonEn.translatableName.size)
+ assertTrue(addonEn.translatableName.contains(addonEn.defaultLocale))
+
+ val addonEs = addon.filterTranslations(listOf("es"))
+ assertEquals(2, addonEs.translatableName.size)
+ assertTrue(addonEs.translatableName.contains(addonEn.defaultLocale))
+ assertTrue(addonEs.translatableName.contains("es"))
+ }
+
+ @Test
+ fun `localizedURLAccessPermissions - must translate all_urls access permission`() {
+ val expectedString = testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)
+ val permissions = listOf(
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ )
+
+ val result = Addon.localizedURLAccessPermissions(testContext, permissions).first()
+
+ assertEquals(expectedString, result)
+ }
+
+ @Test
+ fun `localizedURLAccessPermissions - must translate all urls access permission`() {
+ val expectedString = testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)
+ val permissions = listOf(
+ "webRequest",
+ "webRequestBlocking",
+ "*://*/*",
+ )
+
+ val result = Addon.localizedURLAccessPermissions(testContext, permissions).first()
+
+ assertEquals(expectedString, result)
+ }
+
+ @Test
+ fun `localizedURLAccessPermissions - must translate domain access permissions`() {
+ val expectedString = listOf("tweetdeck.twitter.com", "twitter.com").map {
+ testContext.getString(R.string.mozac_feature_addons_permissions_one_site_description, it)
+ }
+ testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)
+ val permissions = listOf(
+ "webRequest",
+ "webRequestBlocking",
+ "*://tweetdeck.twitter.com/*",
+ "*://twitter.com/*",
+ )
+
+ val result = Addon.localizedURLAccessPermissions(testContext, permissions)
+
+ assertEquals(expectedString, result)
+ }
+
+ @Test
+ fun `localizedURLAccessPermissions - must translate one site access permissions`() {
+ val expectedString = listOf("youtube.com", "youtube-nocookie.com", "vimeo.com").map {
+ testContext.getString(R.string.mozac_feature_addons_permissions_sites_in_domain_description, it)
+ }
+ testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)
+ val permissions = listOf(
+ "webRequest",
+ "*://*.youtube.com/*",
+ "*://*.youtube-nocookie.com/*",
+ "*://*.vimeo.com/*",
+ )
+
+ val result = Addon.localizedURLAccessPermissions(testContext, permissions)
+
+ assertEquals(expectedString, result)
+ }
+
+ @Test
+ fun `localizedURLAccessPermissions - must collapse url access permissions`() {
+ var expectedString = listOf("youtube.com", "youtube-nocookie.com", "vimeo.com", "google.co.ao").map {
+ testContext.getString(R.string.mozac_feature_addons_permissions_sites_in_domain_description, it)
+ } + testContext.getString(R.string.mozac_feature_addons_permissions_one_extra_domain_description)
+
+ var permissions = listOf(
+ "webRequest",
+ "*://*.youtube.com/*",
+ "*://*.youtube-nocookie.com/*",
+ "*://*.vimeo.com/*",
+ "*://*.google.co.ao/*",
+ "*://*.google.com.do/*",
+ )
+
+ var result = Addon.localizedURLAccessPermissions(testContext, permissions)
+
+ // 1 domain permissions must be collapsed
+ assertEquals(expectedString, result)
+
+ expectedString = listOf("youtube.com", "youtube-nocookie.com", "vimeo.com", "google.co.ao").map {
+ testContext.getString(R.string.mozac_feature_addons_permissions_sites_in_domain_description, it)
+ } + testContext.getString(R.string.mozac_feature_addons_permissions_extra_domains_description_plural, 2)
+
+ permissions = listOf(
+ "webRequest",
+ "*://*.youtube.com/*",
+ "*://*.youtube-nocookie.com/*",
+ "*://*.vimeo.com/*",
+ "*://*.google.co.ao/*",
+ "*://*.google.com.do/*",
+ "*://*.google.co.ar/*",
+ )
+
+ result = Addon.localizedURLAccessPermissions(testContext, permissions)
+
+ // 2 domain permissions must be collapsed
+ assertEquals(expectedString, result)
+
+ permissions = listOf(
+ "webRequest",
+ "*://www.youtube.com/*",
+ "*://www.youtube-nocookie.com/*",
+ "*://www.vimeo.com/*",
+ "https://mozilla.org/a/b/c/",
+ "*://www.google.com.do/*",
+ )
+
+ expectedString = listOf("www.youtube.com", "www.youtube-nocookie.com", "www.vimeo.com", "mozilla.org").map {
+ testContext.getString(R.string.mozac_feature_addons_permissions_one_site_description, it)
+ } + testContext.getString(R.string.mozac_feature_addons_permissions_one_extra_site_description)
+
+ result = Addon.localizedURLAccessPermissions(testContext, permissions)
+
+ // 1 site permissions must be Collapsed
+ assertEquals(expectedString, result)
+
+ permissions = listOf(
+ "webRequest",
+ "*://www.youtube.com/*",
+ "*://www.youtube-nocookie.com/*",
+ "*://www.vimeo.com/*",
+ "https://mozilla.org/a/b/c/",
+ "*://www.google.com.do/*",
+ "*://www.google.co.ar/*",
+ )
+
+ expectedString = listOf("www.youtube.com", "www.youtube-nocookie.com", "www.vimeo.com", "mozilla.org").map {
+ testContext.getString(R.string.mozac_feature_addons_permissions_one_site_description, it)
+ } + testContext.getString(R.string.mozac_feature_addons_permissions_extra_sites_description, 2)
+
+ result = Addon.localizedURLAccessPermissions(testContext, permissions)
+
+ // 2 site permissions must be Collapsed
+ assertEquals(expectedString, result)
+
+ permissions = listOf(
+ "webRequest",
+ "*://www.youtube.com/*",
+ "*://www.youtube-nocookie.com/*",
+ "*://www.vimeo.com/*",
+ "https://mozilla.org/a/b/c/",
+ )
+
+ expectedString = listOf("www.youtube.com", "www.youtube-nocookie.com", "www.vimeo.com", "mozilla.org").map {
+ testContext.getString(R.string.mozac_feature_addons_permissions_one_site_description, it)
+ }
+
+ result = Addon.localizedURLAccessPermissions(testContext, permissions)
+
+ // None permissions must be collapsed
+ assertEquals(expectedString, result)
+ }
+
+ @Test
+ fun `localizeURLAccessPermission - must provide the correct localized string`() {
+ val siteAccess = listOf(
+ "*://twitter.com/*",
+ "*://tweetdeck.twitter.com/*",
+ "https://mozilla.org/a/b/c/",
+ "https://www.google.com.ag/*",
+ "https://www.google.co.ck/*",
+ )
+
+ siteAccess.forEach {
+ val stringId = Addon.localizeURLAccessPermission(it)
+ assertEquals(R.string.mozac_feature_addons_permissions_one_site_description, stringId)
+ }
+
+ val domainAccess = listOf(
+ "*://*.youtube.com/*",
+ "*://*.youtube.com/*",
+ "*://*.youtube-nocookie.com/*",
+ "*://*.vimeo.com/*",
+ "*://*.facebookcorewwwi.onion/*",
+ )
+
+ domainAccess.forEach {
+ val stringId = Addon.localizeURLAccessPermission(it)
+ assertEquals(R.string.mozac_feature_addons_permissions_sites_in_domain_description, stringId)
+ }
+
+ val allUrlsAccess = listOf(
+ "*://*/*",
+ "http://*/*",
+ "https://*/*",
+ "file://*/*",
+ "<all_urls>",
+ )
+
+ allUrlsAccess.forEach {
+ val stringId = Addon.localizeURLAccessPermission(it)
+ assertEquals(R.string.mozac_feature_addons_permissions_all_urls_description, stringId)
+ }
+ }
+
+ @Test
+ fun `newFromWebExtension - must return an Addon instance`() {
+ val version = "1.2.3"
+ val permissions = listOf("scripting", "activeTab")
+ val hostPermissions = listOf("https://example.org/")
+ val name = "some name"
+ val description = "some description"
+ val extension: WebExtension = mock()
+ val metadata: Metadata = mock()
+ whenever(extension.id).thenReturn("some-id")
+ whenever(extension.url).thenReturn("some-url")
+ whenever(extension.getMetadata()).thenReturn(metadata)
+ whenever(metadata.version).thenReturn(version)
+ whenever(metadata.permissions).thenReturn(permissions)
+ whenever(metadata.optionalPermissions).thenReturn(listOf("clipboardRead"))
+ whenever(metadata.grantedOptionalPermissions).thenReturn(listOf("clipboardRead"))
+ whenever(metadata.optionalOrigins).thenReturn(listOf("*://*.example.com/*", "*://opt-host-perm.example.com/*"))
+ whenever(metadata.grantedOptionalOrigins).thenReturn(listOf("*://*.example.com/*"))
+ whenever(metadata.hostPermissions).thenReturn(hostPermissions)
+ whenever(metadata.name).thenReturn(name)
+ whenever(metadata.description).thenReturn(description)
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(0))
+ whenever(metadata.baseUrl).thenReturn("some-base-url")
+ whenever(metadata.developerName).thenReturn("developer-name")
+ whenever(metadata.developerUrl).thenReturn("developer-url")
+ whenever(metadata.fullDescription).thenReturn("fullDescription")
+ whenever(metadata.homepageUrl).thenReturn("some-url")
+ whenever(metadata.downloadUrl).thenReturn("some-download-url")
+ whenever(metadata.updateDate).thenReturn("1970-01-01T00:00:00Z")
+ whenever(metadata.reviewUrl).thenReturn("some-review-url")
+ whenever(metadata.reviewCount).thenReturn(0)
+ whenever(metadata.averageRating).thenReturn(0f)
+ whenever(metadata.detailUrl).thenReturn("detail-url")
+ whenever(metadata.incognito).thenReturn(Incognito.NOT_ALLOWED)
+
+ val addon = Addon.newFromWebExtension(extension)
+ assertEquals("some-id", addon.id)
+ assertEquals("some-url", addon.homepageUrl)
+ assertEquals("some-download-url", addon.downloadUrl)
+ assertEquals(permissions + hostPermissions, addon.permissions)
+ assertEquals(
+ listOf(Addon.Permission(name = "clipboardRead", granted = true)),
+ addon.optionalPermissions,
+ )
+ assertEquals(
+ listOf(Addon.Permission(name = "*://*.example.com/*", granted = true), Addon.Permission(name = "*://opt-host-perm.example.com/*", granted = false)),
+ addon.optionalOrigins,
+ )
+ assertEquals("", addon.updatedAt)
+ assertEquals("some name", addon.translatableName[Addon.DEFAULT_LOCALE])
+ assertEquals("fullDescription", addon.translatableDescription[Addon.DEFAULT_LOCALE])
+ assertEquals("some description", addon.translatableSummary[Addon.DEFAULT_LOCALE])
+ assertEquals("developer-name", addon.author?.name)
+ assertEquals("developer-url", addon.author?.url)
+ assertEquals("some-download-url", addon.downloadUrl)
+ assertEquals("some-review-url", addon.ratingUrl)
+ assertEquals(0, addon.rating!!.reviews)
+ assertEquals("detail-url", addon.detailUrl)
+ assertEquals(Addon.Incognito.NOT_ALLOWED, addon.incognito)
+ }
+
+ @Test
+ fun `fromMetadataToAddonDate - must return an valid addon formatted date`() {
+ val expectedDate = "2023-09-28T00:37:43Z"
+ val inputDate = "2023-09-28T00:37:43.983Z"
+
+ val result = Addon.fromMetadataToAddonDate(inputDate)
+
+ assertEquals(expectedDate, result)
+ }
+
+ @Test
+ fun `fromMetadataToAddonDate - must return handle invalid date formats`() {
+ val expectedDate = ""
+ val inputDate = "202xd3-09-28T00:37:43.98993Z"
+
+ val result = Addon.fromMetadataToAddonDate(inputDate)
+
+ assertEquals(expectedDate, result)
+ }
+
+ @Test
+ fun `fromMetadataToAddonDate - must return empty strings`() {
+ val expectedDate = ""
+ val inputDate = ""
+
+ val result = Addon.fromMetadataToAddonDate(inputDate)
+
+ assertEquals(expectedDate, result)
+ }
+
+ @Test
+ fun `isDisabledAsBlocklisted - true if installed state disabled status equals to BLOCKLISTED and otherwise false`() {
+ val addon = Addon(id = "id")
+ val blockListedAddon = addon.copy(
+ installedState = Addon.InstalledState(
+ id = "id",
+ version = "1.0",
+ optionsPageUrl = "",
+ disabledReason = Addon.DisabledReason.BLOCKLISTED,
+ ),
+ )
+
+ assertFalse(addon.isDisabledAsBlocklisted())
+ assertTrue(blockListedAddon.isDisabledAsBlocklisted())
+ }
+
+ @Test
+ fun `isDisabledAsNotCorrectlySigned - true if installed state disabled status equals to NOT_CORRECTLY_SIGNED and otherwise false`() {
+ val addon = Addon(id = "id")
+ val blockListedAddon = addon.copy(
+ installedState = Addon.InstalledState(
+ id = "id",
+ version = "1.0",
+ optionsPageUrl = "",
+ disabledReason = Addon.DisabledReason.NOT_CORRECTLY_SIGNED,
+ ),
+ )
+
+ assertFalse(addon.isDisabledAsNotCorrectlySigned())
+ assertTrue(blockListedAddon.isDisabledAsNotCorrectlySigned())
+ }
+
+ @Test
+ fun `isDisabledAsIncompatible - true if installed state disabled status equals to INCOMPATIBLE and otherwise false`() {
+ val addon = Addon(id = "id")
+ val blockListedAddon = addon.copy(
+ installedState = Addon.InstalledState(
+ id = "id",
+ version = "1.0",
+ optionsPageUrl = "",
+ disabledReason = Addon.DisabledReason.INCOMPATIBLE,
+ ),
+ )
+
+ assertFalse(addon.isDisabledAsIncompatible())
+ assertTrue(blockListedAddon.isDisabledAsIncompatible())
+ }
+
+ @Test
+ fun `provideIcon - should provide the icon from either addon or installedState`() {
+ val addonWithoutIcon = Addon(id = "id")
+
+ assertNull(addonWithoutIcon.icon)
+ assertNull(addonWithoutIcon.installedState?.icon)
+ assertNull(addonWithoutIcon.provideIcon())
+
+ val addonWithIcon = addonWithoutIcon.copy(icon = mock())
+
+ assertNotNull(addonWithIcon.icon)
+ assertNull(addonWithIcon.installedState?.icon)
+ assertNotNull(addonWithIcon.provideIcon())
+
+ val addonWithInstalledStateIcon = addonWithoutIcon.copy(
+ installedState = Addon.InstalledState("id", "1.0", "", icon = mock()),
+ )
+
+ assertNull(addonWithInstalledStateIcon.icon)
+ assertNotNull(addonWithInstalledStateIcon.installedState?.icon)
+ assertNotNull(addonWithInstalledStateIcon.provideIcon())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/amo/AMOAddonsProviderTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/amo/AMOAddonsProviderTest.kt
new file mode 100644
index 0000000000..8f8f7cafd5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/amo/AMOAddonsProviderTest.kt
@@ -0,0 +1,684 @@
+/* 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.addons.amo
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.feature.addons.Addon
+import mozilla.components.support.test.any
+import mozilla.components.support.test.file.loadResourceAsString
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+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.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.util.Date
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+class AMOAddonsProviderTest {
+
+ @Test
+ fun `getFeaturedAddons - with a successful status response must contain add-ons`() = runTest {
+ val mockedClient = prepareClient(loadResourceAsString("/collection.json"))
+ val provider = AMOAddonsProvider(testContext, client = mockedClient)
+ val addons = provider.getFeaturedAddons()
+ val addon = addons.first()
+
+ assertTrue(addons.isNotEmpty())
+ assertAddonIsUBlockOrigin(addon)
+ }
+
+ @Test
+ fun `getFeaturedAddons - with a successful status response must handle empty values`() = runTest {
+ val client = prepareClient()
+ val provider = AMOAddonsProvider(testContext, client = client)
+
+ val addons = provider.getFeaturedAddons()
+ val addon = addons.first()
+
+ assertTrue(addons.isNotEmpty())
+
+ // Add-on
+ assertEquals("", addon.id)
+ assertEquals("", addon.createdAt)
+ assertEquals("", addon.updatedAt)
+ assertEquals("", addon.iconUrl)
+ assertEquals("", addon.homepageUrl)
+ assertEquals("", addon.version)
+ assertEquals("", addon.downloadUrl)
+ assertTrue(addon.permissions.isEmpty())
+ assertTrue(addon.translatableName.isEmpty())
+ assertTrue(addon.translatableSummary.isEmpty())
+ assertEquals("", addon.translatableDescription.getValue("ca"))
+ assertEquals(Addon.DEFAULT_LOCALE, addon.defaultLocale)
+ assertEquals("", addon.detailUrl)
+
+ // Author
+ assertNull(addon.author)
+ verify(client).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "7e8d6dc651b54ab385fb8791bf9dac/addons/?page_size=$PAGE_SIZE&sort=${SortOption.POPULARITY_DESC.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ // Ratings
+ assertNull(addon.rating)
+ }
+
+ @Test
+ fun `getFeaturedAddons - with a language`() = runTest {
+ val client = prepareClient(loadResourceAsString("/localized_collection.json"))
+ val provider = AMOAddonsProvider(testContext, client = client)
+
+ val addons = provider.getFeaturedAddons(language = "en")
+ val addon = addons.first()
+
+ assertTrue(addons.isNotEmpty())
+
+ // Add-on
+ assertEquals("uBlock0@raymondhill.net", addon.id)
+ assertEquals("2015-04-25T07:26:22Z", addon.createdAt)
+ assertEquals("2021-02-01T14:04:16Z", addon.updatedAt)
+ assertEquals(
+ "https://addons.cdn.mozilla.net/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ addon.iconUrl,
+ )
+ assertEquals(
+ "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/",
+ addon.homepageUrl,
+ )
+ assertEquals(
+ "https://addons.mozilla.org/firefox/downloads/file/3719054/ublock_origin-1.33.2-an+fx.xpi",
+ addon.downloadUrl,
+ )
+ assertEquals(
+ "dns",
+ addon.permissions.first(),
+ )
+ assertEquals(
+ "uBlock Origin",
+ addon.translatableName["en"],
+ )
+
+ assertEquals(
+ "Finally, an efficient wide-spectrum content blocker. Easy on CPU and memory.",
+ addon.translatableSummary["en"],
+ )
+
+ assertTrue(addon.translatableDescription.getValue("en").isNotBlank())
+ assertEquals("1.33.2", addon.version)
+ assertEquals("en", addon.defaultLocale)
+
+ // Author
+ assertEquals("Raymond Hill", addon.author?.name)
+ assertEquals(
+ "https://addons.mozilla.org/en-US/firefox/user/11423598/",
+ addon.author?.url,
+ )
+
+ // Ratings
+ assertEquals(4.7003F, addon.rating!!.average, 0.7003F)
+ assertEquals(4433, addon.rating!!.reviews)
+
+ verify(client).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "7e8d6dc651b54ab385fb8791bf9dac/addons/?page_size=$PAGE_SIZE&sort=${SortOption.POPULARITY_DESC.value}&lang=en",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ Unit
+ }
+
+ @Test
+ fun `getFeaturedAddons - read timeout can be configured`() = runTest {
+ val mockedClient = prepareClient()
+
+ val provider = spy(AMOAddonsProvider(testContext, client = mockedClient))
+ provider.getFeaturedAddons(readTimeoutInSeconds = 5)
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "7e8d6dc651b54ab385fb8791bf9dac/addons/?page_size=$PAGE_SIZE&sort=${SortOption.POPULARITY_DESC.value}",
+ readTimeout = Pair(5, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+ Unit
+ }
+
+ @Test(expected = IOException::class)
+ fun `getFeaturedAddons - with unexpected status will throw exception`() = runTest {
+ val mockedClient = prepareClient(status = 500)
+ val provider = AMOAddonsProvider(testContext, client = mockedClient)
+ provider.getFeaturedAddons()
+ Unit
+ }
+
+ @Test
+ fun `getFeaturedAddons - returns cached result if allowed and not expired`() = runTest {
+ val mockedClient = prepareClient(loadResourceAsString("/collection.json"))
+
+ val provider = spy(AMOAddonsProvider(testContext, client = mockedClient))
+ provider.getFeaturedAddons(false)
+ verify(provider, never()).readFromDiskCache(null, useFallbackFile = false)
+
+ whenever(provider.cacheExpired(testContext, null, useFallbackFile = false)).thenReturn(true)
+ provider.getFeaturedAddons(true)
+ verify(provider, never()).readFromDiskCache(null, useFallbackFile = false)
+
+ whenever(provider.cacheExpired(testContext, null, useFallbackFile = false)).thenReturn(false)
+ provider.getFeaturedAddons(true)
+ verify(provider).readFromDiskCache(null, useFallbackFile = false)
+ Unit
+ }
+
+ @Test
+ fun `getFeaturedAddons - returns cached result if allowed and fetch failed`() = runTest {
+ val mockedClient: Client = mock()
+ val exception = IOException("test")
+ val cachedAddons: List<Addon> = emptyList()
+ whenever(mockedClient.fetch(any())).thenThrow(exception)
+
+ val provider = spy(AMOAddonsProvider(testContext, client = mockedClient))
+
+ try {
+ // allowCache = false
+ provider.getFeaturedAddons(allowCache = false)
+ fail("Expected IOException")
+ } catch (e: IOException) {
+ assertSame(exception, e)
+ }
+
+ try {
+ // allowCache = true, but no cache present
+ provider.getFeaturedAddons(allowCache = true)
+ fail("Expected IOException")
+ } catch (e: IOException) {
+ assertSame(exception, e)
+ }
+
+ try {
+ // allowCache = true, cache present, but we fail to read
+ whenever(provider.getCacheLastUpdated(testContext, null, useFallbackFile = false)).thenReturn(Date().time)
+ provider.getFeaturedAddons(allowCache = true)
+ fail("Expected IOException")
+ } catch (e: IOException) {
+ assertSame(exception, e)
+ }
+
+ // allowCache = true, cache present for a fallback file, and reading successfully
+ whenever(provider.getCacheLastUpdated(testContext, null, useFallbackFile = true)).thenReturn(Date().time)
+ whenever(provider.readFromDiskCache(null, useFallbackFile = true)).thenReturn(cachedAddons)
+ assertSame(cachedAddons, provider.getFeaturedAddons(allowCache = true))
+
+ // allowCache = true, cache present, and reading successfully
+ whenever(provider.getCacheLastUpdated(testContext, null, useFallbackFile = false)).thenReturn(Date().time)
+ whenever(provider.cacheExpired(testContext, null, useFallbackFile = false)).thenReturn(false)
+ whenever(provider.readFromDiskCache(null, useFallbackFile = false)).thenReturn(cachedAddons)
+ whenever(provider.readFromDiskCache(null, useFallbackFile = false)).thenReturn(cachedAddons)
+ assertEquals(cachedAddons, provider.getFeaturedAddons(allowCache = true))
+ }
+
+ @Test
+ fun `getFeaturedAddons - writes response to cache if configured`() = runTest {
+ val jsonResponse = loadResourceAsString("/collection.json")
+ val mockedClient = prepareClient(jsonResponse)
+
+ val provider = spy(AMOAddonsProvider(testContext, client = mockedClient))
+ val cachingProvider = spy(AMOAddonsProvider(testContext, client = mockedClient, maxCacheAgeInMinutes = 1))
+
+ provider.getFeaturedAddons()
+ verify(provider, never()).writeToDiskCache(jsonResponse, null)
+
+ cachingProvider.getFeaturedAddons()
+ verify(cachingProvider).writeToDiskCache(jsonResponse, null)
+ }
+
+ @Test
+ fun `getFeaturedAddons - deletes unused cache files`() = runTest {
+ val jsonResponse = loadResourceAsString("/collection.json")
+ val mockedClient = prepareClient(jsonResponse)
+
+ val provider = spy(AMOAddonsProvider(testContext, client = mockedClient, maxCacheAgeInMinutes = 1))
+
+ provider.getFeaturedAddons()
+ verify(provider).deleteUnusedCacheFiles(null)
+ }
+
+ @Test
+ fun `deleteUnusedCacheFiles - only deletes collection cache files`() {
+ val regularFile = File(testContext.filesDir, "test.json")
+ regularFile.createNewFile()
+ assertTrue(regularFile.exists())
+
+ val regularDir = File(testContext.filesDir, "testDir")
+ regularDir.mkdir()
+ assertTrue(regularDir.exists())
+
+ val collectionFile = File(testContext.filesDir, COLLECTION_FILE_NAME.format("testCollection"))
+ collectionFile.createNewFile()
+ assertTrue(collectionFile.exists())
+
+ val provider = AMOAddonsProvider(testContext, client = prepareClient(), maxCacheAgeInMinutes = 1)
+ provider.deleteUnusedCacheFiles(null)
+ assertTrue(regularFile.exists())
+ assertTrue(regularDir.exists())
+ assertFalse(collectionFile.exists())
+ }
+
+ @Test
+ fun `deleteUnusedCacheFiles - will not remove the fallback localized file`() {
+ val regularFile = File(testContext.filesDir, "test.json")
+ regularFile.createNewFile()
+ assertTrue(regularFile.exists())
+
+ val regularDir = File(testContext.filesDir, "testDir")
+ regularDir.mkdir()
+ assertTrue(regularDir.exists())
+
+ val provider = AMOAddonsProvider(testContext, client = prepareClient(), maxCacheAgeInMinutes = 1)
+ val enFile = File(testContext.filesDir, provider.getCacheFileName("en"))
+
+ enFile.createNewFile()
+
+ assertTrue(enFile.exists())
+
+ provider.deleteUnusedCacheFiles("es")
+
+ val file = provider.getBaseCacheFile(testContext, "es", useFallbackFile = true)
+
+ assertTrue(file.name.contains("en"))
+ assertTrue(file.exists())
+ assertEquals(enFile.name, file.name)
+ assertTrue(regularFile.exists())
+ assertTrue(regularDir.exists())
+ assertTrue(enFile.delete())
+ assertFalse(file.exists())
+ assertTrue(regularFile.delete())
+ assertTrue(regularDir.delete())
+ }
+
+ @Test
+ fun `getBaseCacheFile - will return a first localized file WHEN the provided language file is not available`() {
+ val provider = AMOAddonsProvider(testContext, client = prepareClient(), maxCacheAgeInMinutes = 1)
+ val enFile = File(testContext.filesDir, provider.getCacheFileName("en"))
+
+ enFile.createNewFile()
+
+ assertTrue(enFile.exists())
+
+ val file = provider.getBaseCacheFile(testContext, "es", useFallbackFile = true)
+
+ assertTrue(file.name.contains("en"))
+ assertTrue(file.exists())
+ assertEquals(enFile.name, file.name)
+
+ assertTrue(enFile.delete())
+ assertFalse(file.exists())
+ }
+
+ @Test
+ fun `getFeaturedAddons - cache expiration check`() {
+ var provider = spy(AMOAddonsProvider(testContext, client = mock(), maxCacheAgeInMinutes = -1))
+ whenever(provider.getCacheLastUpdated(testContext, null, useFallbackFile = false)).thenReturn(Date().time)
+ assertTrue(provider.cacheExpired(testContext, null, useFallbackFile = false))
+
+ whenever(provider.getCacheLastUpdated(testContext, null, useFallbackFile = false)).thenReturn(-1)
+ assertTrue(provider.cacheExpired(testContext, null, useFallbackFile = false))
+
+ provider = spy(AMOAddonsProvider(testContext, client = mock(), maxCacheAgeInMinutes = 10))
+ whenever(provider.getCacheLastUpdated(testContext, null, useFallbackFile = false)).thenReturn(-1)
+ assertTrue(provider.cacheExpired(testContext, null, useFallbackFile = false))
+
+ whenever(provider.getCacheLastUpdated(testContext, null, useFallbackFile = false)).thenReturn(Date().time - 60 * MINUTE_IN_MS)
+ assertTrue(provider.cacheExpired(testContext, null, useFallbackFile = false))
+
+ whenever(provider.getCacheLastUpdated(testContext, null, useFallbackFile = false)).thenReturn(Date().time + 60 * MINUTE_IN_MS)
+ assertFalse(provider.cacheExpired(testContext, null, useFallbackFile = false))
+ }
+
+ @Test
+ fun `loadIconAsync - with a successful status will return a bitmap`() = runTest {
+ val mockedClient = mock<Client>()
+ val mockedResponse = mock<Response>()
+ val stream: InputStream = javaClass.getResourceAsStream("/png/mozac.png")!!.buffered()
+ val responseBody = Response.Body(stream)
+
+ whenever(mockedResponse.body).thenReturn(responseBody)
+ whenever(mockedResponse.status).thenReturn(200)
+ whenever(mockedClient.fetch(any())).thenReturn(mockedResponse)
+
+ val provider = AMOAddonsProvider(testContext, client = mockedClient)
+
+ val bitmap = provider.loadIconAsync("id", "https://example.com/image.png").await()
+ assertTrue(bitmap is Bitmap)
+ }
+
+ @Test
+ fun `loadIconAsync - will return bitmap from the cache when available`() = runTest {
+ val mockedClient = mock<Client>()
+ val expectedIcon = mock<Bitmap>()
+
+ val provider = AMOAddonsProvider(testContext, client = mockedClient)
+
+ provider.iconsCache["id"] = expectedIcon
+
+ val bitmap = provider.loadIconAsync("id", "https://example.com/image.png").await()
+
+ verify(mockedClient, times(0)).fetch(any())
+ assertEquals(expectedIcon, bitmap)
+ assertTrue(bitmap is Bitmap)
+ }
+
+ @Test
+ fun `loadIconAsync - with an unsuccessful status will return null`() = runTest {
+ val mockedClient = prepareClient(status = 500)
+ val provider = AMOAddonsProvider(testContext, client = mockedClient)
+
+ val bitmap = provider.loadIconAsync("id", "https://example.com/image.png").await()
+ assertNull(bitmap)
+ }
+
+ @Test
+ fun `collection name can be configured`() = runTest {
+ val mockedClient = prepareClient()
+
+ val collectionName = "collection123"
+ val provider = AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionName = collectionName,
+ )
+
+ provider.getFeaturedAddons()
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "$collectionName/addons/?page_size=$PAGE_SIZE&sort=${SortOption.POPULARITY_DESC.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ assertEquals(COLLECTION_FILE_NAME.format(collectionName), provider.getCacheFileName())
+ }
+
+ @Test
+ fun `collection sort option can be specified`() = runTest {
+ val mockedClient = prepareClient()
+
+ val collectionName = "collection123"
+ AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionName = collectionName,
+ sortOption = SortOption.POPULARITY,
+ ).also {
+ it.getFeaturedAddons()
+ }
+
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "$collectionName/addons/?page_size=$PAGE_SIZE&sort=${SortOption.POPULARITY.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionName = collectionName,
+ sortOption = SortOption.POPULARITY_DESC,
+ ).also {
+ it.getFeaturedAddons()
+ }
+
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "$collectionName/addons/?page_size=$PAGE_SIZE&sort=${SortOption.POPULARITY_DESC.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionName = collectionName,
+ sortOption = SortOption.NAME,
+ ).also {
+ it.getFeaturedAddons()
+ }
+
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "$collectionName/addons/?page_size=$PAGE_SIZE&sort=${SortOption.NAME.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionName = collectionName,
+ sortOption = SortOption.NAME_DESC,
+ ).also {
+ it.getFeaturedAddons()
+ }
+
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "$collectionName/addons/?page_size=$PAGE_SIZE&sort=${SortOption.NAME_DESC.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionName = collectionName,
+ sortOption = SortOption.DATE_ADDED,
+ ).also {
+ it.getFeaturedAddons()
+ }
+
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "$collectionName/addons/?page_size=$PAGE_SIZE&sort=${SortOption.DATE_ADDED.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionName = collectionName,
+ sortOption = SortOption.DATE_ADDED_DESC,
+ ).also {
+ it.getFeaturedAddons()
+ }
+
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/" +
+ "$collectionName/addons/?page_size=$PAGE_SIZE&sort=${SortOption.DATE_ADDED_DESC.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ Unit
+ }
+
+ @Test
+ fun `collection user can be configured`() = runTest {
+ val mockedClient = prepareClient()
+ val collectionUser = "user123"
+ val collectionName = "collection123"
+ val provider = AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionUser = collectionUser,
+ collectionName = collectionName,
+ )
+
+ provider.getFeaturedAddons()
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/" +
+ "$collectionUser/collections/$collectionName/addons/" +
+ "?page_size=$PAGE_SIZE" +
+ "&sort=${SortOption.POPULARITY_DESC.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ assertEquals(
+ COLLECTION_FILE_NAME.format("${collectionUser}_$collectionName"),
+ provider.getCacheFileName(),
+ )
+ }
+
+ @Test
+ fun `default collection is used if not configured`() = runTest {
+ val mockedClient = prepareClient()
+
+ val provider = AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ )
+
+ provider.getFeaturedAddons()
+ verify(mockedClient).fetch(
+ Request(
+ url = "https://services.addons.mozilla.org/api/v4/accounts/account/" +
+ "$DEFAULT_COLLECTION_USER/collections/$DEFAULT_COLLECTION_NAME/addons/" +
+ "?page_size=$PAGE_SIZE" +
+ "&sort=${SortOption.POPULARITY_DESC.value}",
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ ),
+ )
+
+ assertEquals(COLLECTION_FILE_NAME.format(DEFAULT_COLLECTION_NAME), provider.getCacheFileName())
+ }
+
+ @Test
+ fun `cache file name is sanitized`() = runTest {
+ val mockedClient = prepareClient()
+ val collectionUser = "../../user"
+ val collectionName = "../collection"
+ val provider = AMOAddonsProvider(
+ testContext,
+ client = mockedClient,
+ collectionUser = collectionUser,
+ collectionName = collectionName,
+ )
+
+ assertEquals(
+ COLLECTION_FILE_NAME.format("user_collection"),
+ provider.getCacheFileName(),
+ )
+ }
+
+ private fun assertAddonIsUBlockOrigin(addon: Addon) {
+ // Add-on details
+ assertEquals("uBlock0@raymondhill.net", addon.id)
+ assertEquals("2015-04-25T07:26:22Z", addon.createdAt)
+ assertEquals("2023-07-19T23:09:25Z", addon.updatedAt)
+ assertEquals(
+ "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ addon.iconUrl,
+ )
+ assertEquals(
+ "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/",
+ addon.homepageUrl,
+ )
+ assertEquals(
+ "https://addons.mozilla.org/firefox/downloads/file/4141256/ublock_origin-1.51.0.xpi",
+ addon.downloadUrl,
+ )
+ assertEquals(
+ "dns",
+ addon.permissions.first(),
+ )
+ assertEquals(
+ "uBlock Origin",
+ addon.translatableName["ca"],
+ )
+ assertEquals(
+ "Finalment, un blocador eficient que utilitza pocs recursos de memòria i processador.",
+ addon.translatableSummary["ca"],
+ )
+ assertTrue(addon.translatableDescription.getValue("ca").isNotBlank())
+ assertEquals("1.51.0", addon.version)
+ assertEquals("en-us", addon.defaultLocale)
+ // Author
+ assertEquals("Raymond Hill", addon.author?.name)
+ assertEquals(
+ "https://addons.mozilla.org/en-US/firefox/user/11423598/",
+ addon.author?.url,
+ )
+ // Ratings
+ assertEquals(4.7825F, addon.rating!!.average, 0.7825F)
+ assertEquals(4101, addon.rating!!.reviews)
+ assertEquals(
+ "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/reviews/",
+ addon.ratingUrl,
+ )
+ assertEquals(
+ "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/",
+ addon.detailUrl,
+ )
+ }
+
+ private fun prepareClient(
+ jsonResponse: String = loadResourceAsString("/collection_with_empty_values.json"),
+ status: Int = 200,
+ ): Client {
+ val mockedClient = mock<Client>()
+ val mockedResponse = mock<Response>()
+ val mockedBody = mock<Response.Body>()
+ whenever(mockedBody.string(any())).thenReturn(jsonResponse)
+ whenever(mockedResponse.body).thenReturn(mockedBody)
+ whenever(mockedResponse.status).thenReturn(status)
+ whenever(mockedClient.fetch(any())).thenReturn(mockedResponse)
+ return mockedClient
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidateTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidateTest.kt
new file mode 100644
index 0000000000..c44a2dcff7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidateTest.kt
@@ -0,0 +1,126 @@
+/* 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.addons.menu
+
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.test.robolectric.testContext
+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.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class WebExtensionActionMenuCandidateTest {
+
+ private val baseAction = Action(
+ title = "action",
+ enabled = false,
+ loadIcon = null,
+ badgeText = "",
+ badgeTextColor = 0,
+ badgeBackgroundColor = 0,
+ onClick = {},
+ )
+
+ @Test
+ fun `create menu candidate from null action`() {
+ val onClick = {}
+ val candidate = Action(
+ title = null,
+ enabled = null,
+ loadIcon = null,
+ badgeText = null,
+ badgeTextColor = null,
+ badgeBackgroundColor = null,
+ onClick = onClick,
+ ).createMenuCandidate(testContext)
+
+ assertEquals("", candidate.text)
+ assertFalse(candidate.containerStyle.isEnabled)
+ assertEquals(onClick, candidate.onClick)
+
+ assertNull(candidate.start)
+ assertNull(candidate.end)
+ }
+
+ @Test
+ fun `create menu candidate with text and no badge`() {
+ val candidate = baseAction
+ .copy(badgeText = null)
+ .createMenuCandidate(testContext)
+
+ assertEquals("action", candidate.text)
+ assertFalse(candidate.containerStyle.isEnabled)
+
+ assertNull(candidate.start)
+ assertNull(candidate.end)
+ }
+
+ @Test
+ fun `create menu candidate with badge`() {
+ val candidate = baseAction
+ .copy(
+ badgeText = "10",
+ badgeTextColor = Color.DKGRAY,
+ badgeBackgroundColor = Color.YELLOW,
+ enabled = true,
+ )
+ .createMenuCandidate(testContext)
+
+ assertEquals("action", candidate.text)
+ assertTrue(candidate.containerStyle.isEnabled)
+
+ assertNull(candidate.start)
+ assertTrue(candidate.end is TextMenuIcon)
+
+ assertEquals(
+ TextMenuIcon(
+ text = "10",
+ backgroundTint = Color.YELLOW,
+ textStyle = TextStyle(color = Color.DKGRAY),
+ ),
+ candidate.end,
+ )
+ }
+
+ @Test
+ fun `create menu candidate with icon`() = runTest {
+ var calledWith: Int = -1
+ val candidate = baseAction
+ .copy(
+ badgeText = null,
+ loadIcon = { height ->
+ calledWith = height
+ null
+ },
+ )
+ .createMenuCandidate(testContext)
+
+ assertEquals("action", candidate.text)
+ assertFalse(candidate.containerStyle.isEnabled)
+
+ assertTrue(candidate.start is AsyncDrawableMenuIcon)
+ assertNull(candidate.end)
+
+ val start = candidate.start as AsyncDrawableMenuIcon
+ assertNotNull(start.loadingDrawable)
+ assertNotNull(start.fallbackDrawable)
+ assertNull(start.effect)
+
+ assertNull(start.loadDrawable(40, 30))
+ assertEquals(30, calledWith)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionNestedMenuCandidateTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionNestedMenuCandidateTest.kt
new file mode 100644
index 0000000000..f3eaf7307b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionNestedMenuCandidateTest.kt
@@ -0,0 +1,182 @@
+/* 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.addons.menu
+
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class WebExtensionNestedMenuCandidateTest {
+
+ private val pageAction = Action(
+ title = "page title",
+ loadIcon = { mock() },
+ enabled = true,
+ badgeText = "pageBadge",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ private val browserAction = Action(
+ title = "browser title",
+ loadIcon = { mock() },
+ enabled = true,
+ badgeText = "browserBadge",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ @Test
+ fun `create nested menu from browser extensions and actions`() {
+ val state = BrowserState(
+ extensions = mapOf(
+ "1" to WebExtensionState(id = "1", browserAction = browserAction, pageAction = pageAction),
+ ),
+ )
+ val candidate = state.createWebExtensionMenuCandidate(
+ testContext,
+ appendExtensionSubMenuAt = Side.END,
+ ) as NestedMenuCandidate
+
+ assertEquals(6, candidate.subMenuItems!!.size)
+
+ assertEquals(
+ "Add-ons Manager",
+ (candidate.subMenuItems!![0] as TextMenuCandidate).text,
+ )
+ assertEquals(
+ DividerMenuCandidate(),
+ candidate.subMenuItems!![1],
+ )
+
+ val ext1 = candidate.subMenuItems!![2] as TextMenuCandidate
+ assertTrue(ext1.containerStyle.isEnabled)
+ assertEquals("browser title", ext1.text)
+ assertTrue(ext1.start is AsyncDrawableMenuIcon)
+ assertEquals(
+ "browserBadge",
+ (ext1.end as TextMenuIcon).text,
+ )
+
+ val ext2 = candidate.subMenuItems!![3] as TextMenuCandidate
+ assertTrue(ext2.containerStyle.isEnabled)
+ assertEquals("page title", ext2.text)
+ assertTrue(ext2.start is AsyncDrawableMenuIcon)
+ assertEquals(
+ "pageBadge",
+ (ext2.end as TextMenuIcon).text,
+ )
+
+ assertEquals(
+ DividerMenuCandidate(),
+ candidate.subMenuItems!![4],
+ )
+ assertEquals(
+ "Add-ons",
+ (candidate.subMenuItems!![5] as NestedMenuCandidate).text,
+ )
+ }
+
+ @Test
+ fun `browser actions can be overridden per tab`() {
+ val pageActionOverride = Action(
+ title = "updatedTitle",
+ loadIcon = null,
+ enabled = true,
+ badgeText = "updatedText",
+ badgeTextColor = Color.RED,
+ badgeBackgroundColor = Color.GREEN,
+ ) {}
+ val browserActionOverride = Action(
+ title = "updatedTitle",
+ loadIcon = null,
+ enabled = false,
+ badgeText = "updatedText",
+ badgeTextColor = Color.RED,
+ badgeBackgroundColor = Color.GREEN,
+ ) {}
+
+ val state = BrowserState(
+ extensions = mapOf(
+ "1" to WebExtensionState(id = "1", browserAction = browserAction, pageAction = pageAction),
+ ),
+ tabs = listOf(
+ createTab(
+ id = "tab-1",
+ url = "https://mozilla.org",
+ extensions = mapOf(
+ "1" to WebExtensionState(
+ id = "1",
+ browserAction = browserActionOverride,
+ pageAction = pageActionOverride,
+ ),
+ ),
+ ),
+ ),
+ )
+ val candidate = state.createWebExtensionMenuCandidate(
+ testContext,
+ tabId = "tab-1",
+ appendExtensionSubMenuAt = Side.START,
+ ) as NestedMenuCandidate
+
+ assertEquals(6, candidate.subMenuItems!!.size)
+
+ assertEquals(
+ "Add-ons",
+ (candidate.subMenuItems!![0] as NestedMenuCandidate).text,
+ )
+ assertNull((candidate.subMenuItems!![0] as NestedMenuCandidate).subMenuItems)
+ assertEquals(
+ DividerMenuCandidate(),
+ candidate.subMenuItems!![1],
+ )
+
+ val ext1 = candidate.subMenuItems!![2] as TextMenuCandidate
+ assertFalse(ext1.containerStyle.isEnabled)
+ assertEquals("updatedTitle", ext1.text)
+ assertEquals(
+ "updatedText",
+ (ext1.end as TextMenuIcon).text,
+ )
+
+ val ext2 = candidate.subMenuItems!![3] as TextMenuCandidate
+ assertTrue(ext2.containerStyle.isEnabled)
+ assertEquals("updatedTitle", ext2.text)
+ assertEquals(
+ "updatedText",
+ (ext2.end as TextMenuIcon).text,
+ )
+
+ assertEquals(
+ DividerMenuCandidate(),
+ candidate.subMenuItems!![4],
+ )
+ assertEquals(
+ "Add-ons Manager",
+ (candidate.subMenuItems!![5] as TextMenuCandidate).text,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/DefaultSupportedAddonCheckerTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/DefaultSupportedAddonCheckerTest.kt
new file mode 100644
index 0000000000..498dc05015
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/DefaultSupportedAddonCheckerTest.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.addons.migration
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.Configuration
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.await
+import androidx.work.testing.WorkManagerTestInitHelper
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker.Companion.CHECKER_UNIQUE_PERIODIC_WORK_NAME
+import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker.Companion.WORK_TAG_PERIODIC
+import mozilla.components.support.base.worker.Frequency
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+class DefaultSupportedAddonCheckerTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ val configuration = Configuration.Builder().build()
+ context = spy(testContext).also {
+ val packageManager: PackageManager = mock()
+ doReturn(Intent()).`when`(packageManager).getLaunchIntentForPackage(
+ ArgumentMatchers.anyString(),
+ )
+ doReturn(packageManager).`when`(it).packageManager
+ }
+
+ // Initialize WorkManager (early) for instrumentation tests.
+ WorkManagerTestInitHelper.initializeTestWorkManager(testContext, configuration)
+ }
+
+ @Test
+ fun `registerForChecks - schedule work for future checks`() = runTestOnMain {
+ val frequency = Frequency(1, TimeUnit.DAYS)
+ val checker = DefaultSupportedAddonsChecker(context, frequency)
+
+ val workId = CHECKER_UNIQUE_PERIODIC_WORK_NAME
+
+ val workManger = WorkManager.getInstance(testContext)
+ val workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertTrue(workData.isEmpty())
+
+ checker.registerForChecks()
+
+ assertExtensionIsRegisteredForChecks()
+ // Cleaning work manager
+ workManger.cancelUniqueWork(workId)
+ }
+
+ @Test
+ fun `unregisterForChecks - will remove scheduled work for future checks`() = runTestOnMain {
+ val frequency = Frequency(1, TimeUnit.DAYS)
+ val checker = DefaultSupportedAddonsChecker(context, frequency)
+
+ val workId = CHECKER_UNIQUE_PERIODIC_WORK_NAME
+
+ val workManger = WorkManager.getInstance(testContext)
+ var workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertTrue(workData.isEmpty())
+
+ checker.registerForChecks()
+
+ assertExtensionIsRegisteredForChecks()
+
+ checker.unregisterForChecks()
+
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertEquals(WorkInfo.State.CANCELLED, workData.first().state)
+ }
+
+ private suspend fun assertExtensionIsRegisteredForChecks() {
+ val workId = CHECKER_UNIQUE_PERIODIC_WORK_NAME
+ val workManger = WorkManager.getInstance(testContext)
+ val workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertFalse(workData.isEmpty())
+
+ val work = workData.first()
+
+ assertEquals(WorkInfo.State.ENQUEUED, work.state)
+ assertTrue(work.tags.contains(workId))
+ assertTrue(work.tags.contains(WORK_TAG_PERIODIC))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/SupportedAddonsWorkerTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/SupportedAddonsWorkerTest.kt
new file mode 100644
index 0000000000..f3595c5b92
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/SupportedAddonsWorkerTest.kt
@@ -0,0 +1,106 @@
+/* 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.addons.migration
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.feature.addons.AddonManagerException
+import mozilla.components.feature.addons.update.AddonUpdater
+import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+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.Mockito.verify
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class SupportedAddonsWorkerTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setUp() {
+ GlobalAddonDependencyProvider.addonManager = null
+ }
+
+ @After
+ fun after() {
+ GlobalAddonDependencyProvider.addonManager = null
+ }
+
+ @Test
+ fun `doWork - will return Result_success and update new supported add-ons when found`() =
+ runTestOnMain {
+ val addonManager = mock<AddonManager>()
+ val addonUpdater = mock<AddonUpdater>()
+ val worker = TestListenableWorkerBuilder<SupportedAddonsWorker>(testContext).build()
+ val unsupportedAddon = mock<Addon> {
+ whenever(translatableName).thenReturn(mapOf(Addon.DEFAULT_LOCALE to "one"))
+ whenever(isSupported()).thenReturn(true)
+ whenever(isDisabledAsUnsupported()).thenReturn(true)
+ whenever(defaultLocale).thenReturn(Addon.DEFAULT_LOCALE)
+ }
+
+ GlobalAddonDependencyProvider.initialize(addonManager, addonUpdater, mock())
+
+ whenever(addonManager.getAddons()).thenReturn(listOf(unsupportedAddon))
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.success(), result)
+
+ verify(addonUpdater).registerForFutureUpdates(unsupportedAddon.id)
+ }
+
+ @Test
+ fun `doWork - will try pass any exceptions to the crashReporter`() = runTestOnMain {
+ val addonManager = mock<AddonManager>()
+ val worker = TestListenableWorkerBuilder<SupportedAddonsWorker>(testContext).build()
+ var crashWasReported = false
+ val crashReporter: ((Throwable) -> Unit) = { _ ->
+ crashWasReported = true
+ }
+
+ GlobalAddonDependencyProvider.initialize(addonManager, mock(), crashReporter)
+ GlobalAddonDependencyProvider.addonManager = null
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.success(), result)
+ assertTrue(crashWasReported)
+ }
+
+ @Test
+ fun `doWork - will NOT pass any IOExceptions to the crashReporter`() = runTestOnMain {
+ val addonManager = mock<AddonManager>()
+ val worker = TestListenableWorkerBuilder<SupportedAddonsWorker>(testContext).build()
+ var crashWasReported = false
+ val crashReporter: ((Throwable) -> Unit) = { _ ->
+ crashWasReported = true
+ }
+
+ GlobalAddonDependencyProvider.initialize(addonManager, mock(), crashReporter)
+
+ whenever(addonManager.getAddons()).thenThrow(AddonManagerException(IOException()))
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.success(), result)
+ assertFalse(crashWasReported)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonDialogFragmentTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonDialogFragmentTest.kt
new file mode 100644
index 0000000000..18828cc982
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonDialogFragmentTest.kt
@@ -0,0 +1,71 @@
+/* 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.addons.mozilla.components.feature.addons.ui
+
+import android.graphics.Bitmap
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.AddonDialogFragment
+import mozilla.components.feature.addons.ui.KEY_ICON
+import mozilla.components.feature.addons.ui.setIcon
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import mozilla.components.support.utils.ext.getParcelableCompat
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AddonDialogFragmentTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `loadIcon the add-on icon successfully`() {
+ val addon = mock<Addon>()
+ val bitmap = mock<Bitmap>()
+ val iconView = mock<AppCompatImageView>()
+ val fragment = createAddonDialogFragment()
+
+ fragment.safeArguments.putParcelable(KEY_ICON, bitmap)
+
+ fragment.loadIcon(addon, iconView)
+
+ verify(iconView).setImageDrawable(Mockito.any())
+ }
+
+ @Test
+ fun `loadIcon the add-on icon with a null result`() {
+ val addon = mock<Addon>()
+ val bitmap = mock<Bitmap>()
+ val iconView = mock<AppCompatImageView>()
+ val fragment = createAddonDialogFragment()
+
+ whenever(addon.provideIcon()).thenReturn(bitmap)
+
+ assertNull(fragment.arguments?.getParcelableCompat(KEY_ICON, Bitmap::class.java))
+
+ fragment.loadIcon(addon, iconView)
+
+ assertNotNull(fragment.arguments?.getParcelableCompat(KEY_ICON, Bitmap::class.java))
+ verify(iconView).setIcon(addon)
+ }
+
+ private fun createAddonDialogFragment(): AddonDialogFragment {
+ val dialog = AddonDialogFragment()
+ return spy(dialog).apply {
+ doNothing().`when`(this).dismiss()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonFilePickerTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonFilePickerTest.kt
new file mode 100644
index 0000000000..836427c50e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonFilePickerTest.kt
@@ -0,0 +1,151 @@
+/* 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.addons.mozilla.components.feature.addons.ui
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import androidx.activity.result.ActivityResultCaller
+import androidx.activity.result.ActivityResultLauncher
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.webextension.InstallationMethod
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.feature.addons.ui.AddonFilePicker
+import mozilla.components.feature.addons.ui.AddonOpenDocument
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AddonFilePickerTest {
+
+ private lateinit var filePicker: AddonFilePicker
+ private lateinit var addonManager: AddonManager
+
+ @Before
+ fun setup() {
+ addonManager = mock()
+ filePicker = spy(
+ AddonFilePicker(testContext, addonManager),
+ )
+ }
+
+ @Test
+ fun `WHEN registerForResults is called THEN register AddonOpenDocument`() {
+ val resultCaller: ActivityResultCaller = mock()
+
+ whenever(resultCaller.registerForActivityResult(any<AddonOpenDocument>(), any())).thenReturn(mock())
+
+ filePicker.registerForResults(resultCaller)
+
+ verify(resultCaller).registerForActivityResult(any<AddonOpenDocument>(), any())
+ }
+
+ @Test
+ fun `WHEN launch is called THEN delegate to the activityLauncher`() {
+ val activityLauncher: ActivityResultLauncher<Array<String>> = mock()
+
+ filePicker.activityLauncher = activityLauncher
+
+ val wasOpened = filePicker.launch()
+
+ verify(activityLauncher).launch(any())
+ assertTrue(wasOpened)
+ }
+
+ @Test
+ fun `GIVEN a file picker is not present WHEN launch is called THEN return false`() {
+ val activityLauncher: ActivityResultLauncher<Array<String>> = mock()
+
+ filePicker.activityLauncher = activityLauncher
+ whenever(activityLauncher.launch(any())).thenThrow(ActivityNotFoundException())
+
+ val wasOpened = filePicker.launch()
+
+ verify(activityLauncher).launch(any())
+ assertFalse(wasOpened)
+ }
+
+ @Test
+ fun `WHEN handleUriSelected is called THEN return false`() {
+ val uri: Uri = mock()
+
+ doReturn("file:///data/data/XPIs/addon.xpi").`when`(filePicker).convertToFileUri(uri)
+
+ filePicker.handleUriSelected(uri)
+
+ verify(addonManager).installAddon(
+ url = any<String>(),
+ installationMethod = eq(InstallationMethod.FROM_FILE),
+ onSuccess = any(),
+ onError = any(),
+ )
+ }
+
+ @Test
+ fun `WHEN creating an intent from a AddonOpenDocument THEN it contains mime types for zips and xpi files`() {
+ val contractResult = AddonOpenDocument()
+
+ val intent = contractResult.createIntent(testContext, arrayOf())
+
+ val mimeTypes = intent.extras!!.getStringArray(Intent.EXTRA_MIME_TYPES)
+ assertArrayEquals(mimeTypes, arrayOf("application/x-xpinstall", "application/zip"))
+ }
+
+ @Test
+ fun `WHEN an addon is installed successfully THEN remove the temporary file`() {
+ val uri: Uri = mock()
+
+ doReturn("file:///data/data/XPIs/addon.xpi").`when`(filePicker).convertToFileUri(uri)
+
+ val onSuccessCallbackCapture = argumentCaptor<((Addon) -> Unit)>()
+ filePicker.handleUriSelected(uri)
+
+ verify(addonManager).installAddon(
+ url = any<String>(),
+ installationMethod = eq(InstallationMethod.FROM_FILE),
+ onSuccess = onSuccessCallbackCapture.capture(),
+ onError = any(),
+ )
+
+ onSuccessCallbackCapture.value.invoke(mock())
+
+ verify(filePicker).removeTemporaryFile(uri)
+ }
+
+ @Test
+ fun `WHEN there is an error during installation THEN remove the temporary file`() {
+ val uri: Uri = mock()
+
+ doReturn("file:///data/data/XPIs/addon.xpi").`when`(filePicker).convertToFileUri(uri)
+
+ val onErrorCapture = argumentCaptor<((Throwable) -> Unit)>()
+ filePicker.handleUriSelected(uri)
+
+ verify(addonManager).installAddon(
+ url = any<String>(),
+ installationMethod = eq(InstallationMethod.FROM_FILE),
+ onSuccess = any(),
+ onError = onErrorCapture.capture(),
+ )
+
+ onErrorCapture.value.invoke(mock())
+
+ verify(filePicker).removeTemporaryFile(uri)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragmentTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragmentTest.kt
new file mode 100644
index 0000000000..66b57f4d89
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragmentTest.kt
@@ -0,0 +1,201 @@
+/* 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.addons.ui
+
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatCheckBox
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.utils.ext.getParcelableCompat
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+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.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class AddonInstallationDialogFragmentTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `build dialog`() {
+ val addon = Addon(
+ "id",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"),
+ permissions = listOf("privacy", "<all_urls>", "tabs"),
+ )
+ val fragment = createAddonInstallationDialogFragment(addon)
+ assertSame(addon, fragment.arguments?.getParcelableCompat(KEY_INSTALLED_ADDON, Addon::class.java))
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ val name = addon.translateName(testContext)
+ val titleTextView = dialog.findViewById<TextView>(R.id.title)
+ val description = dialog.findViewById<TextView>(R.id.description)
+ val allowedInPrivateBrowsing = dialog.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
+
+ assertTrue(titleTextView.text.contains(name))
+ assertTrue(description.text.contains(name))
+ assertTrue(allowedInPrivateBrowsing.isVisible)
+ assertTrue(allowedInPrivateBrowsing.text.contains(testContext.getString(R.string.mozac_feature_addons_settings_allow_in_private_browsing)))
+ }
+
+ @Test
+ fun `clicking on confirm dialog buttons notifies lambda with private browsing boolean`() {
+ val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
+
+ val fragment = createAddonInstallationDialogFragment(addon)
+ var confirmationWasExecuted = false
+ var allowInPrivateBrowsing = false
+
+ fragment.onConfirmButtonClicked = { _, allow ->
+ confirmationWasExecuted = true
+ allowInPrivateBrowsing = allow
+ }
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ val confirmButton = dialog.findViewById<Button>(R.id.confirm_button)
+ val allowedInPrivateBrowsing = dialog.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
+ confirmButton.performClick()
+ assertTrue(confirmationWasExecuted)
+ assertFalse(allowInPrivateBrowsing)
+
+ dialog.show()
+ allowedInPrivateBrowsing.performClick()
+ confirmButton.performClick()
+ assertTrue(confirmationWasExecuted)
+ assertTrue(allowInPrivateBrowsing)
+ }
+
+ @Test
+ fun `dismissing the dialog notifies nothing`() {
+ val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
+ val fragment = createAddonInstallationDialogFragment(addon)
+ var confirmationWasExecuted = false
+
+ fragment.onConfirmButtonClicked = { _, _ ->
+ confirmationWasExecuted = true
+ }
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ doReturn(mockFragmentManager()).`when`(fragment).parentFragmentManager
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ fragment.onDismiss(mock())
+ assertFalse(confirmationWasExecuted)
+ }
+
+ @Test
+ fun `WHEN calling onCancel THEN notifies onDismiss`() {
+ val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
+ val fragment = createAddonInstallationDialogFragment(addon)
+ var onDismissedWasExecuted = false
+
+ fragment.onDismissed = {
+ onDismissedWasExecuted = true
+ }
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ doReturn(mockFragmentManager()).`when`(fragment).parentFragmentManager
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ fragment.onCancel(mock())
+ assertTrue(onDismissedWasExecuted)
+ }
+
+ @Test
+ fun `dialog must have all the styles of the feature promptsStyling object`() {
+ val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
+ val styling = AddonDialogFragment.PromptsStyling(Gravity.TOP, true)
+ val fragment = createAddonInstallationDialogFragment(addon, styling)
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ val dialogAttributes = dialog.window!!.attributes
+
+ assertTrue(dialogAttributes.gravity == Gravity.TOP)
+ assertTrue(dialogAttributes.width == ViewGroup.LayoutParams.MATCH_PARENT)
+ }
+
+ @Test
+ fun `allows state loss when committing`() {
+ val addon = mock<Addon>()
+ val fragment = createAddonInstallationDialogFragment(addon)
+
+ val fragmentManager = mock<FragmentManager>()
+ val fragmentTransaction = mock<FragmentTransaction>()
+ `when`(fragmentManager.beginTransaction()).thenReturn(fragmentTransaction)
+
+ fragment.show(fragmentManager, "test")
+ verify(fragmentTransaction).commitAllowingStateLoss()
+ }
+
+ @Test
+ fun `hide private browsing checkbox when the add-on does not allow running in private windows`() {
+ val addon = Addon(
+ "id",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"),
+ permissions = listOf("privacy", "<all_urls>", "tabs"),
+ incognito = Addon.Incognito.NOT_ALLOWED,
+ )
+ val fragment = createAddonInstallationDialogFragment(addon)
+ assertSame(addon, fragment.arguments?.getParcelableCompat(KEY_INSTALLED_ADDON, Addon::class.java))
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ val name = addon.translateName(testContext)
+ val titleTextView = dialog.findViewById<TextView>(R.id.title)
+ val description = dialog.findViewById<TextView>(R.id.description)
+ val allowedInPrivateBrowsing = dialog.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
+
+ assertTrue(titleTextView.text.contains(name))
+ assertTrue(description.text.contains(name))
+ assertFalse(allowedInPrivateBrowsing.isVisible)
+ }
+
+ private fun createAddonInstallationDialogFragment(
+ addon: Addon,
+ promptsStyling: AddonDialogFragment.PromptsStyling? = null,
+ ): AddonInstallationDialogFragment {
+ return spy(AddonInstallationDialogFragment.newInstance(addon, promptsStyling = promptsStyling)).apply {
+ doNothing().`when`(this).dismiss()
+ }
+ }
+
+ 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/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterTest.kt
new file mode 100644
index 0000000000..9ff8a083e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterTest.kt
@@ -0,0 +1,856 @@
+/* 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.addons.ui
+
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.feature.addons.ui.AddonsManagerAdapter.DifferCallback
+import mozilla.components.feature.addons.ui.AddonsManagerAdapter.NotYetSupportedSection
+import mozilla.components.feature.addons.ui.AddonsManagerAdapter.Section
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+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.Mockito.any
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import java.util.Locale
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class AddonsManagerAdapterTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ // We must pass these variables to `bindAddon()` because looking up the version name
+ // requires package info that we do not have in the test context.
+ private val appName = "Firefox"
+ private val appVersion = "1.2.3"
+
+ @Before
+ fun setUp() {
+ Locale.setDefault(Locale.ENGLISH)
+ }
+
+ @Test
+ fun `createListWithSections`() {
+ val adapter = AddonsManagerAdapter(mock(), emptyList(), mock(), emptyList(), mock())
+
+ val installedAddon: Addon = mock()
+ val recommendedAddon: Addon = mock()
+ val unsupportedAddon: Addon = mock()
+ val disabledAddon: Addon = mock()
+
+ `when`(installedAddon.isInstalled()).thenReturn(true)
+ `when`(installedAddon.isEnabled()).thenReturn(true)
+ `when`(installedAddon.isSupported()).thenReturn(true)
+ `when`(unsupportedAddon.isInstalled()).thenReturn(true)
+ `when`(unsupportedAddon.isSupported()).thenReturn(false)
+ `when`(disabledAddon.isEnabled()).thenReturn(false)
+ `when`(disabledAddon.isInstalled()).thenReturn(true)
+ `when`(disabledAddon.isSupported()).thenReturn(true)
+
+ val addons = listOf(installedAddon, recommendedAddon, unsupportedAddon, disabledAddon)
+
+ assertEquals(0, adapter.itemCount)
+
+ val itemsWithSections = adapter.createListWithSections(addons)
+
+ assertEquals(7, itemsWithSections.size)
+ assertEquals(
+ R.string.mozac_feature_addons_enabled,
+ (itemsWithSections[0] as Section).title,
+ )
+ assertEquals(installedAddon, itemsWithSections[1])
+ assertEquals(
+ R.string.mozac_feature_addons_disabled_section,
+ (itemsWithSections[2] as Section).title,
+ )
+ assertEquals(disabledAddon, itemsWithSections[3])
+ assertEquals(
+ R.string.mozac_feature_addons_recommended_section,
+ (itemsWithSections[4] as Section).title,
+ )
+ assertEquals(recommendedAddon, itemsWithSections[5])
+ assertEquals(
+ R.string.mozac_feature_addons_unavailable_section,
+ (itemsWithSections[6] as NotYetSupportedSection).title,
+ )
+
+ // Test if excluededAddonIDs are excluded from recommended section
+ val excludedAddonIDs = listOf(recommendedAddon.id)
+ val itemsWithSections2 = adapter.createListWithSections(addons, excludedAddonIDs)
+
+ // The only recommended addon should be excluded, so the recommended section should be null
+ // Section size should shrink from 7 to 6
+ assertEquals(6, itemsWithSections2.size)
+ // There should be no section between the titles of Recommended & NotYetSupported
+ assertEquals(
+ R.string.mozac_feature_addons_recommended_section,
+ (itemsWithSections2[4] as Section).title,
+ )
+ assertEquals(
+ R.string.mozac_feature_addons_unavailable_section,
+ (itemsWithSections2[5] as NotYetSupportedSection).title,
+ )
+ }
+
+ @Test
+ fun `bind add-on`() {
+ val contentWrapperView = View(testContext)
+ val titleView: TextView = mock()
+ val summaryView: TextView = mock()
+ val ratingAccessibleView: TextView = mock()
+ val reviewCountView: TextView = mock()
+ val addButton = ImageView(testContext)
+ val view = View(testContext)
+ val allowedInPrivateBrowsingLabel = ImageView(testContext)
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val iconView = mock<ImageView>()
+ whenever(iconView.context).thenReturn(testContext)
+ val addonViewHolder = CustomViewHolder.AddonViewHolder(
+ view = view,
+ contentWrapperView = contentWrapperView,
+ iconView = iconView,
+ titleView = titleView,
+ summaryView = summaryView,
+ ratingView = mock(),
+ ratingAccessibleView = ratingAccessibleView,
+ reviewCountView = reviewCountView,
+ addButton = addButton,
+ allowedInPrivateBrowsingLabel = allowedInPrivateBrowsingLabel,
+ statusErrorView = mock(),
+ )
+ val addon = Addon(
+ id = "id",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ rating = Addon.Rating(4.5f, 1000),
+ createdAt = "",
+ updatedAt = "",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "de" to "Name", "es" to "nombre"),
+ translatableDescription = mapOf(Addon.DEFAULT_LOCALE to "description", "de" to "Beschreibung", "es" to "descripción"),
+ translatableSummary = mapOf(Addon.DEFAULT_LOCALE to "summary", "de" to "Kurzfassung", "es" to "resumen"),
+ )
+
+ whenever(titleView.context).thenReturn(testContext)
+ whenever(summaryView.context).thenReturn(testContext)
+
+ val style = AddonsManagerAdapter.Style(
+ sectionsTextColor = android.R.color.black,
+ addonNameTextColor = android.R.color.transparent,
+ addonSummaryTextColor = android.R.color.white,
+ )
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), style, emptyList(), mock())
+
+ adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
+
+ verify(ratingAccessibleView).setText("Rating: 4.50 out of 5")
+ verify(titleView).setText("name")
+ verify(titleView).setTextColor(ContextCompat.getColor(testContext, style.addonNameTextColor!!))
+ verify(summaryView).setText("summary")
+ verify(summaryView).setTextColor(ContextCompat.getColor(testContext, style.addonSummaryTextColor!!))
+ assertNotNull(addonViewHolder.itemView.tag)
+
+ addonViewHolder.contentWrapperView.performClick()
+ verify(addonsManagerAdapterDelegate).onAddonItemClicked(addon)
+ addButton.performClick()
+ verify(addonsManagerAdapterDelegate).onInstallAddonButtonClicked(addon)
+ }
+
+ @Test
+ fun `bind section`() {
+ val titleView: TextView = mock()
+ val divider: View = mock()
+ val sectionViewHolder = CustomViewHolder.SectionViewHolder(View(testContext), titleView, divider)
+ val position = 0
+
+ whenever(titleView.context).thenReturn(testContext)
+
+ val style = AddonsManagerAdapter.Style(
+ sectionsTextColor = android.R.color.black,
+ sectionsTypeFace = mock(),
+ )
+ val adapter = AddonsManagerAdapter(mock(), emptyList(), style, emptyList(), mock())
+ // Force-add a Section item in the list.
+ adapter.submitList(null)
+ adapter.submitList(listOf(Section(R.string.mozac_feature_addons_disabled_section)))
+ // Make sure we have the Footer item in the list.
+ assertEquals(1, adapter.itemCount)
+
+ // Use the "public" method to bind the section.
+ adapter.bindViewHolder(sectionViewHolder, position)
+
+ verify(titleView).setText(R.string.mozac_feature_addons_disabled_section)
+ verify(titleView).typeface = style.sectionsTypeFace
+ verify(titleView).setTextColor(ContextCompat.getColor(testContext, style.sectionsTextColor!!))
+ verify(divider).isVisible = style.visibleDividers && position != 0
+ assertNotNull(sectionViewHolder.itemView.accessibilityDelegate)
+
+ val nodeInfo: AccessibilityNodeInfo = mock()
+ sectionViewHolder.itemView.accessibilityDelegate.onInitializeAccessibilityNodeInfo(mock(), nodeInfo)
+ verify(nodeInfo).collectionItemInfo = argThat {
+ // We cannot verify `rowIndex` because we're using `bindingAdapterPosition`.
+ @Suppress("DEPRECATION")
+ it.rowSpan == 1 && it.columnIndex == 1 && it.columnSpan == 1 && it.isHeading
+ }
+ }
+
+ @Test
+ fun `default section divider visibility is 'GONE' for position 0`() {
+ val titleView: TextView = mock()
+ val divider: View = mock()
+ val addonViewHolder = CustomViewHolder.SectionViewHolder(View(testContext), titleView, divider)
+ val position = 0
+
+ whenever(titleView.context).thenReturn(testContext)
+
+ val style = AddonsManagerAdapter.Style(
+ sectionsTextColor = android.R.color.black,
+ sectionsTypeFace = mock(),
+ )
+ val adapter = AddonsManagerAdapter(mock(), emptyList(), style, emptyList(), mock())
+
+ adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
+
+ verify(divider).visibility = View.GONE
+ }
+
+ @Test
+ fun `default section divider visibility is 'VISIBLE' for other position than 0`() {
+ val titleView: TextView = mock()
+ val divider: View = mock()
+ val addonViewHolder = CustomViewHolder.SectionViewHolder(View(testContext), titleView, divider)
+ val position = 2
+
+ whenever(titleView.context).thenReturn(testContext)
+
+ val style = AddonsManagerAdapter.Style(
+ sectionsTextColor = android.R.color.black,
+ sectionsTypeFace = mock(),
+ )
+ val adapter = AddonsManagerAdapter(mock(), emptyList(), style, emptyList(), mock())
+
+ adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
+
+ verify(divider).visibility = View.VISIBLE
+ }
+
+ @Test
+ fun `section divider visibility is 'GONE' when set as such in style`() {
+ val titleView: TextView = mock()
+ val divider: View = mock()
+ val addonViewHolder = CustomViewHolder.SectionViewHolder(View(testContext), titleView, divider)
+ val position = 2
+
+ whenever(titleView.context).thenReturn(testContext)
+
+ val style = AddonsManagerAdapter.Style(
+ sectionsTextColor = android.R.color.black,
+ sectionsTypeFace = mock(),
+ visibleDividers = false,
+ )
+ val adapter = AddonsManagerAdapter(mock(), emptyList(), style, emptyList(), mock())
+
+ adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
+
+ verify(divider).visibility = View.GONE
+ }
+
+ @Test
+ fun `section divider style is set when arguments are passed in style`() {
+ val titleView: TextView = mock()
+ val divider: View = mock()
+ val addonViewHolder = CustomViewHolder.SectionViewHolder(View(testContext), titleView, divider)
+ val position = 2
+ val dividerHeight = android.R.dimen.notification_large_icon_height
+ val dividerColor = android.R.color.white
+
+ whenever(titleView.context).thenReturn(testContext)
+ whenever(divider.context).thenReturn(testContext)
+ whenever(divider.layoutParams).thenReturn(mock())
+
+ val style = AddonsManagerAdapter.Style(
+ sectionsTextColor = android.R.color.black,
+ sectionsTypeFace = mock(),
+ visibleDividers = true,
+ dividerColor = dividerColor,
+ dividerHeight = dividerHeight,
+ )
+
+ val adapter = AddonsManagerAdapter(mock(), emptyList(), style, emptyList(), mock())
+
+ adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
+
+ verify(divider).visibility = View.VISIBLE
+ verify(divider).setBackgroundColor(dividerColor)
+ verify(divider).layoutParams
+ assertEquals(testContext.resources.getDimensionPixelOffset(dividerHeight), divider.layoutParams.height)
+ }
+
+ @Test
+ fun `bind add-on with no available translatable name`() {
+ val titleView: TextView = mock()
+ val summaryView: TextView = mock()
+ val view = View(testContext)
+ val allowedInPrivateBrowsingLabel = ImageView(testContext)
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val iconView = mock<ImageView>()
+ whenever(iconView.context).thenReturn(testContext)
+ val addonViewHolder = CustomViewHolder.AddonViewHolder(
+ view = view,
+ contentWrapperView = mock(),
+ iconView = iconView,
+ titleView = titleView,
+ summaryView = summaryView,
+ ratingView = mock(),
+ ratingAccessibleView = mock(),
+ reviewCountView = mock(),
+ addButton = mock(),
+ allowedInPrivateBrowsingLabel = allowedInPrivateBrowsingLabel,
+ statusErrorView = mock(),
+ )
+ val addon = Addon(
+ id = "id",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ )
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
+ verify(titleView).setText("id")
+ verify(summaryView).setVisibility(View.GONE)
+ }
+
+ @Test
+ fun updateAddon() {
+ var addon = Addon(
+ id = "id",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ )
+ val adapter = spy(AddonsManagerAdapter(mock(), listOf(addon), mock(), emptyList(), mock()))
+
+ assertEquals(addon, adapter.addonsMap[addon.id])
+
+ addon = addon.copy(version = "newVersion")
+ adapter.updateAddon(addon)
+
+ assertEquals(addon.version, adapter.addonsMap[addon.id]!!.version)
+ verify(adapter).submitList(any())
+
+ // Once the list is submitted, we should have the right item count on the adapter.
+ // In this case, we have 1 heading and 1 add-on.
+ assertEquals(2, adapter.itemCount)
+ }
+
+ @Test
+ fun updateAddons() {
+ var addon1 = Addon(
+ id = "addon1",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ )
+
+ val addon2 = Addon(
+ id = "addon2",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ )
+ val adapter =
+ spy(AddonsManagerAdapter(mock(), listOf(addon1, addon2), mock(), emptyList(), mock()))
+
+ assertEquals(addon1, adapter.addonsMap[addon1.id])
+ assertEquals(addon2, adapter.addonsMap[addon2.id])
+
+ addon1 = addon1.copy(version = "newVersion")
+ adapter.updateAddons(listOf(addon1, addon2))
+
+ // Only addon1 must be updated
+ assertEquals(addon1.version, adapter.addonsMap[addon1.id]!!.version)
+ assertEquals(addon2, adapter.addonsMap[addon2.id])
+ verify(adapter).submitList(any())
+ }
+
+ @Test
+ fun differCallback() {
+ var addon1 = Addon(
+ id = "addon1",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ )
+
+ var addon2 = Addon(
+ id = "addon1",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ createdAt = "",
+ updatedAt = "",
+ )
+
+ assertTrue(DifferCallback.areItemsTheSame(addon1, addon2))
+
+ addon2 = addon2.copy(id = "addon2")
+
+ assertFalse(DifferCallback.areItemsTheSame(addon1, addon2))
+
+ addon2 = addon2.copy(id = "addon1")
+
+ assertTrue(DifferCallback.areItemsTheSame(addon1, addon2))
+
+ addon1 = addon1.copy(version = "newVersion")
+
+ assertFalse(DifferCallback.areContentsTheSame(addon1, addon2))
+ }
+
+ @Test
+ fun bindNotYetSupportedSection() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val titleView: TextView = mock()
+ val descriptionView: TextView = mock()
+ val view = View(testContext)
+ val unsupportedSectionViewHolder = CustomViewHolder.UnsupportedSectionViewHolder(
+ view,
+ titleView,
+ descriptionView,
+ )
+ val unsupportedAddon = Addon(
+ id = "id",
+ installedState = Addon.InstalledState(
+ id = "id",
+ version = "version",
+ optionsPageUrl = "optionsPageUrl",
+ supported = false,
+ ),
+ )
+ val unsupportedAddonTwo = Addon(
+ id = "id2",
+ installedState = Addon.InstalledState(
+ id = "id2",
+ version = "version2",
+ optionsPageUrl = "optionsPageUrl2",
+ supported = false,
+ ),
+ )
+ val unsupportedAddons = arrayListOf(unsupportedAddon, unsupportedAddonTwo)
+ val adapter = AddonsManagerAdapter(
+ addonsManagerAdapterDelegate,
+ unsupportedAddons,
+ mock(),
+ emptyList(),
+ mock(),
+ )
+
+ adapter.bindNotYetSupportedSection(unsupportedSectionViewHolder, mock())
+ verify(unsupportedSectionViewHolder.descriptionView).setText(
+ testContext.getString(R.string.mozac_feature_addons_unsupported_caption_plural_2, unsupportedAddons.size),
+ )
+
+ unsupportedSectionViewHolder.itemView.performClick()
+ verify(addonsManagerAdapterDelegate).onNotYetSupportedSectionClicked(unsupportedAddons)
+ }
+
+ @Test
+ fun bindFooterButton() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+ val view = View(testContext)
+ val viewHolder = CustomViewHolder.FooterViewHolder(view)
+ // Make sure we have the Footer item in the list.
+ adapter.submitList(null)
+ adapter.submitList(listOf(AddonsManagerAdapter.FooterSection))
+ assertEquals(1, adapter.itemCount)
+
+ // Use the "public" method to bind the footer.
+ adapter.onBindViewHolder(viewHolder, 0)
+
+ viewHolder.itemView.performClick()
+ verify(addonsManagerAdapterDelegate).onFindMoreAddonsButtonClicked()
+ assertNotNull(viewHolder.itemView.accessibilityDelegate)
+
+ val nodeInfo: AccessibilityNodeInfo = mock()
+ viewHolder.itemView.accessibilityDelegate.onInitializeAccessibilityNodeInfo(mock(), nodeInfo)
+ verify(nodeInfo).collectionItemInfo = argThat {
+ // We cannot verify `rowIndex` because we're using `bindingAdapterPosition`.
+ @Suppress("DEPRECATION")
+ it.rowSpan == 1 && it.columnIndex == 1 && it.columnSpan == 1 && !it.isHeading
+ }
+ }
+
+ @Test
+ fun bindHeaderButton() {
+ val store = BrowserStore(initialState = BrowserState(extensionsProcessDisabled = true))
+ val adapter =
+ spy(AddonsManagerAdapter(mock(), emptyList(), mock(), emptyList(), store))
+
+ val restartButton = TextView(testContext)
+ val viewHolder = CustomViewHolder.HeaderViewHolder(View(testContext), restartButton)
+ adapter.bindHeaderButton(viewHolder)
+ assertEquals(1, adapter.currentList.size)
+
+ viewHolder.restartButton.performClick()
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ assertFalse(store.state.extensionsProcessDisabled)
+ verify(adapter).submitList(emptyList())
+ }
+
+ @Test
+ fun testNotificationShownWhenProcessIsDisabled() {
+ val store = BrowserStore(initialState = BrowserState(extensionsProcessDisabled = true))
+ val adapter = AddonsManagerAdapter(mock(), emptyList(), mock(), emptyList(), store)
+
+ val itemsWithSections = adapter.createListWithSections(emptyList())
+ assertEquals(AddonsManagerAdapter.HeaderSection, itemsWithSections.first())
+ }
+
+ @Test
+ fun testNotificationNotShownWhenProcessIsEnabled() {
+ val store = BrowserStore(initialState = BrowserState(extensionsProcessDisabled = false))
+ val adapter = AddonsManagerAdapter(mock(), emptyList(), mock(), emptyList(), store)
+
+ val itemsWithSections = adapter.createListWithSections(emptyList())
+ assertTrue(itemsWithSections.isEmpty())
+ }
+
+ @Test
+ fun testFindMoreAddonsButtonIsHidden() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ whenever(addonsManagerAdapterDelegate.shouldShowFindMoreAddonsButton()).thenReturn(false)
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ val itemsWithSections = adapter.createListWithSections(emptyList())
+ assertTrue(itemsWithSections.isEmpty())
+ }
+
+ @Test
+ fun testFindMoreAddonsButtonIsVisible() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ whenever(addonsManagerAdapterDelegate.shouldShowFindMoreAddonsButton()).thenReturn(true)
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ val itemsWithSections = adapter.createListWithSections(emptyList())
+ assertFalse(itemsWithSections.isEmpty())
+ assertEquals(AddonsManagerAdapter.FooterSection, itemsWithSections.last())
+ }
+
+ @Test
+ fun `bind blocklisted add-on`() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val titleView: TextView = mock()
+ whenever(titleView.context).thenReturn(testContext)
+ val summaryView: TextView = mock()
+ whenever(summaryView.context).thenReturn(testContext)
+ val statusErrorView: View = mock()
+ val messageTextView: TextView = mock()
+ val learnMoreTextView = TextView(testContext)
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_message)).thenReturn(
+ messageTextView,
+ )
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_learn_more_link)).thenReturn(
+ learnMoreTextView,
+ )
+ val iconView = mock<ImageView>()
+ whenever(iconView.context).thenReturn(testContext)
+ val addonViewHolder = CustomViewHolder.AddonViewHolder(
+ view = View(testContext),
+ contentWrapperView = mock(),
+ iconView = iconView,
+ titleView = titleView,
+ summaryView = summaryView,
+ ratingView = mock(),
+ ratingAccessibleView = mock(),
+ reviewCountView = mock(),
+ addButton = mock(),
+ allowedInPrivateBrowsingLabel = mock(),
+ statusErrorView = statusErrorView,
+ )
+ val addonName = "some addon name"
+ val addon = makeDisabledAddon(Addon.DisabledReason.BLOCKLISTED, addonName)
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
+
+ verify(statusErrorView).isVisible = true
+ verify(messageTextView).text = "$addonName has been disabled due to security or stability issues."
+
+ // Verify that a click on the "learn more" link actually does something.
+ learnMoreTextView.performClick()
+ verify(addonsManagerAdapterDelegate).onLearnMoreLinkClicked(
+ AddonsManagerAdapterDelegate.LearnMoreLinks.BLOCKLISTED_ADDON,
+ addon,
+ )
+ }
+
+ @Test
+ fun `bind blocklisted add-on without an add-on name`() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val titleView: TextView = mock()
+ whenever(titleView.context).thenReturn(testContext)
+ val summaryView: TextView = mock()
+ whenever(summaryView.context).thenReturn(testContext)
+ val statusErrorView: View = mock()
+ val messageTextView: TextView = mock()
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_message)).thenReturn(
+ messageTextView,
+ )
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_learn_more_link)).thenReturn(
+ mock(),
+ )
+ val iconView = mock<ImageView>()
+ whenever(iconView.context).thenReturn(testContext)
+ val addonViewHolder = CustomViewHolder.AddonViewHolder(
+ view = View(testContext),
+ contentWrapperView = mock(),
+ iconView = iconView,
+ titleView = titleView,
+ summaryView = summaryView,
+ ratingView = mock(),
+ ratingAccessibleView = mock(),
+ reviewCountView = mock(),
+ addButton = mock(),
+ allowedInPrivateBrowsingLabel = mock(),
+ statusErrorView = statusErrorView,
+ )
+ val addon = makeDisabledAddon(Addon.DisabledReason.BLOCKLISTED)
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
+
+ verify(statusErrorView).isVisible = true
+ verify(messageTextView).text = "${addon.id} has been disabled due to security or stability issues."
+ }
+
+ @Test
+ fun `bind add-on not correctly signed`() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val titleView: TextView = mock()
+ whenever(titleView.context).thenReturn(testContext)
+ val summaryView: TextView = mock()
+ whenever(summaryView.context).thenReturn(testContext)
+ val statusErrorView: View = mock()
+ val messageTextView: TextView = mock()
+ val learnMoreTextView = TextView(testContext)
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_message)).thenReturn(
+ messageTextView,
+ )
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_learn_more_link)).thenReturn(
+ learnMoreTextView,
+ )
+ val iconView = mock<ImageView>()
+ whenever(iconView.context).thenReturn(testContext)
+ val addonViewHolder = CustomViewHolder.AddonViewHolder(
+ view = View(testContext),
+ contentWrapperView = mock(),
+ iconView = iconView,
+ titleView = titleView,
+ summaryView = summaryView,
+ ratingView = mock(),
+ ratingAccessibleView = mock(),
+ reviewCountView = mock(),
+ addButton = mock(),
+ allowedInPrivateBrowsingLabel = mock(),
+ statusErrorView = statusErrorView,
+ )
+ val addonName = "some addon name"
+ val addon = makeDisabledAddon(Addon.DisabledReason.NOT_CORRECTLY_SIGNED, addonName)
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
+
+ verify(statusErrorView).isVisible = true
+ verify(messageTextView).text = "$addonName could not be verified as secure and has been disabled."
+
+ // Verify that a click on the "learn more" link actually does something.
+ learnMoreTextView.performClick()
+ verify(addonsManagerAdapterDelegate).onLearnMoreLinkClicked(
+ AddonsManagerAdapterDelegate.LearnMoreLinks.ADDON_NOT_CORRECTLY_SIGNED,
+ addon,
+ )
+ }
+
+ @Test
+ fun `bind add-on not correctly signed and without a name`() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val titleView: TextView = mock()
+ whenever(titleView.context).thenReturn(testContext)
+ val summaryView: TextView = mock()
+ whenever(summaryView.context).thenReturn(testContext)
+ val statusErrorView: View = mock()
+ val messageTextView: TextView = mock()
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_message)).thenReturn(
+ messageTextView,
+ )
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_learn_more_link)).thenReturn(
+ mock(),
+ )
+
+ val iconView = mock<ImageView>()
+ whenever(iconView.context).thenReturn(testContext)
+
+ val addonViewHolder = CustomViewHolder.AddonViewHolder(
+ view = View(testContext),
+ contentWrapperView = mock(),
+ iconView = iconView,
+ titleView = titleView,
+ summaryView = summaryView,
+ ratingView = mock(),
+ ratingAccessibleView = mock(),
+ reviewCountView = mock(),
+ addButton = mock(),
+ allowedInPrivateBrowsingLabel = mock(),
+ statusErrorView = statusErrorView,
+ )
+ val addon = makeDisabledAddon(Addon.DisabledReason.NOT_CORRECTLY_SIGNED)
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
+
+ verify(statusErrorView).isVisible = true
+ verify(messageTextView).text = "${addon.id} could not be verified as secure and has been disabled."
+ }
+
+ @Test
+ fun `bind incompatible add-on`() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val titleView: TextView = mock()
+ whenever(titleView.context).thenReturn(testContext)
+ val summaryView: TextView = mock()
+ whenever(summaryView.context).thenReturn(testContext)
+ val statusErrorView: View = mock()
+ val messageTextView: TextView = mock()
+ val learnMoreTextView: TextView = mock()
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_message)).thenReturn(
+ messageTextView,
+ )
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_learn_more_link)).thenReturn(
+ learnMoreTextView,
+ )
+
+ val iconView = mock<ImageView>()
+ whenever(iconView.context).thenReturn(testContext)
+
+ val addonViewHolder = CustomViewHolder.AddonViewHolder(
+ view = View(testContext),
+ contentWrapperView = mock(),
+ iconView = iconView,
+ titleView = titleView,
+ summaryView = summaryView,
+ ratingView = mock(),
+ ratingAccessibleView = mock(),
+ reviewCountView = mock(),
+ addButton = mock(),
+ allowedInPrivateBrowsingLabel = mock(),
+ statusErrorView = statusErrorView,
+ )
+ val addonName = "some addon name"
+ val addon = makeDisabledAddon(Addon.DisabledReason.INCOMPATIBLE, addonName)
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
+
+ verify(statusErrorView).isVisible = true
+ verify(messageTextView).text = "$addonName is not compatible with your version of $appName (version $appVersion)."
+ verify(learnMoreTextView).isVisible = false
+ }
+
+ @Test
+ fun `bind incompatible add-on and without a name`() {
+ val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
+ val titleView: TextView = mock()
+ whenever(titleView.context).thenReturn(testContext)
+ val summaryView: TextView = mock()
+ whenever(summaryView.context).thenReturn(testContext)
+ val statusErrorView: View = mock()
+ val messageTextView: TextView = mock()
+ val learnMoreTextView: TextView = mock()
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_message)).thenReturn(
+ messageTextView,
+ )
+ whenever(statusErrorView.findViewById<TextView>(R.id.add_on_status_error_learn_more_link)).thenReturn(
+ learnMoreTextView,
+ )
+ val iconView = mock<ImageView>()
+ whenever(iconView.context).thenReturn(testContext)
+
+ val addonViewHolder = CustomViewHolder.AddonViewHolder(
+ view = View(testContext),
+ contentWrapperView = mock(),
+ iconView = iconView,
+ titleView = titleView,
+ summaryView = summaryView,
+ ratingView = mock(),
+ ratingAccessibleView = mock(),
+ reviewCountView = mock(),
+ addButton = mock(),
+ allowedInPrivateBrowsingLabel = mock(),
+ statusErrorView = statusErrorView,
+ )
+ val addon = makeDisabledAddon(Addon.DisabledReason.INCOMPATIBLE)
+ val adapter = AddonsManagerAdapter(addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
+
+ adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
+
+ verify(statusErrorView).isVisible = true
+ verify(messageTextView).text = "${addon.id} is not compatible with your version of $appName (version $appVersion)."
+ verify(learnMoreTextView).isVisible = false
+ }
+
+ private fun makeDisabledAddon(disabledReason: Addon.DisabledReason, name: String? = null): Addon {
+ val installedState: Addon.InstalledState = mock()
+ whenever(installedState.disabledReason).thenReturn(disabledReason)
+ return Addon(
+ id = "@some-addon-id",
+ translatableName = if (name != null) {
+ mapOf(Addon.DEFAULT_LOCALE to name)
+ } else {
+ emptyMap()
+ },
+ installedState = installedState,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsPermissionsAdapterTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsPermissionsAdapterTest.kt
new file mode 100644
index 0000000000..ef500381c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsPermissionsAdapterTest.kt
@@ -0,0 +1,49 @@
+/* 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.addons.ui
+
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.feature.addons.ui.AddonPermissionsAdapter.PermissionViewHolder
+import mozilla.components.feature.addons.ui.AddonPermissionsAdapter.Style
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import mozilla.components.ui.colors.R
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class AddonsPermissionsAdapterTest {
+
+ @Test
+ fun `bind permissions`() {
+ val textView: TextView = mock()
+ val view = View(testContext)
+ val permissions = listOf("permission")
+ val style = Style(itemsTextColor = R.color.photonBlue40)
+ val viewHolder = PermissionViewHolder(view, textView)
+
+ whenever(textView.context).thenReturn(testContext)
+
+ val adapter = AddonPermissionsAdapter(permissions, style)
+
+ adapter.onBindViewHolder(viewHolder, 0)
+
+ verify(textView).text = "permission"
+ verify(textView).contentDescription = testContext.getString(
+ mozilla.components.feature.addons.R.string.mozac_feature_addons_permissions_content_description_item,
+ "permission",
+ 1,
+ 1,
+ )
+ verify(textView).setTextColor(ContextCompat.getColor(testContext, style.itemsTextColor!!))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/ExtensionsTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/ExtensionsTest.kt
new file mode 100644
index 0000000000..c3f78c1413
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/ExtensionsTest.kt
@@ -0,0 +1,162 @@
+/* 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.addons.ui
+
+import android.graphics.Bitmap
+import android.widget.ImageView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.feature.addons.update.AddonUpdater
+import mozilla.components.feature.addons.update.AddonUpdater.Status.Error
+import mozilla.components.feature.addons.update.AddonUpdater.Status.NoUpdateAvailable
+import mozilla.components.feature.addons.update.AddonUpdater.Status.NotInstalled
+import mozilla.components.feature.addons.update.AddonUpdater.Status.SuccessfullyUpdated
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+import java.util.Calendar.MILLISECOND
+import java.util.Date
+import java.util.GregorianCalendar
+import java.util.Locale
+import java.util.TimeZone
+
+@RunWith(AndroidJUnit4::class)
+class ExtensionsTest {
+
+ @Test
+ fun `add-on translateName`() {
+ val addon = Addon(
+ id = "id",
+ downloadUrl = "downloadUrl",
+ version = "version",
+ permissions = emptyList(),
+ rating = Addon.Rating(4.5f, 1000),
+ createdAt = "",
+ updatedAt = "",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "de" to "Name", "es" to "nombre"),
+ )
+
+ Locale.setDefault(Locale("es"))
+
+ assertEquals("nombre", addon.translateName(testContext))
+
+ Locale.setDefault(Locale.GERMAN)
+
+ assertEquals("Name", addon.translateName(testContext))
+
+ Locale.setDefault(Locale.ENGLISH)
+
+ assertEquals("name", addon.translateName(testContext))
+ }
+
+ @Test
+ fun translate() {
+ val addon = Addon("id")
+ val map = mapOf(addon.defaultLocale to "Hello", "es" to "Hola", "de" to "Hallo")
+
+ Locale.setDefault(Locale("es"))
+
+ assertEquals("Hola", map.translate(addon, testContext))
+
+ Locale.setDefault(Locale.GERMAN)
+
+ assertEquals("Hallo", map.translate(addon, testContext))
+
+ Locale.setDefault(Locale.ITALIAN)
+
+ assertEquals("Hello", map.translate(addon, testContext))
+
+ Locale.setDefault(Locale.CHINESE)
+
+ val locales = mapOf("es" to "Hola", "de" to "Hallo")
+
+ val lang = Locale.getDefault().language
+ val notFoundTranslation = testContext.getString(R.string.mozac_feature_addons_failed_to_translate, lang, addon.defaultLocale)
+
+ assertEquals(notFoundTranslation, locales.translate(addon, testContext))
+ }
+
+ @Test
+ fun createdAtUpdatedAtDate() {
+ val addon = Addon(
+ id = "id",
+ createdAt = "2015-04-25T07:26:22Z",
+ updatedAt = "2020-06-28T12:45:18Z",
+ )
+
+ val expectedCreatedAt = GregorianCalendar(TimeZone.getTimeZone("GMT")).apply {
+ set(2015, 3, 25, 7, 26, 22)
+ set(MILLISECOND, 0)
+ }.time
+ val expectedUpdatedAt = GregorianCalendar(TimeZone.getTimeZone("GMT")).apply {
+ set(2020, 5, 28, 12, 45, 18)
+ set(MILLISECOND, 0)
+ }.time
+ assertEquals(expectedCreatedAt, addon.createdAtDate)
+ assertEquals(expectedUpdatedAt, addon.updatedAtDate)
+
+ Locale.setDefault(Locale.GERMAN)
+ assertEquals(expectedCreatedAt, addon.createdAtDate)
+ assertEquals(expectedUpdatedAt, addon.updatedAtDate)
+
+ Locale.setDefault(Locale.ITALIAN)
+ assertEquals(expectedCreatedAt, addon.createdAtDate)
+ assertEquals(expectedUpdatedAt, addon.updatedAtDate)
+ }
+
+ @Test
+ fun getFormattedAmountTest() {
+ val amount = 1000
+
+ Locale.setDefault(Locale.ENGLISH)
+ assertEquals("1,000", getFormattedAmount(amount))
+
+ Locale.setDefault(Locale.GERMAN)
+ assertEquals("1.000", getFormattedAmount(amount))
+
+ Locale.setDefault(Locale("es"))
+ assertEquals("1.000", getFormattedAmount(amount))
+ }
+
+ @Test
+ fun toLocalizedString() {
+ var request = AddonUpdater.UpdateAttempt("addonId", Date(), SuccessfullyUpdated)
+ var string = testContext.getString(R.string.mozac_feature_addons_updater_status_successfully_updated)
+
+ assertEquals(string, request.status.toLocalizedString(testContext))
+
+ string = testContext.getString(R.string.mozac_feature_addons_updater_status_no_update_available)
+ request = request.copy(status = NoUpdateAvailable)
+
+ assertEquals(string, request.status.toLocalizedString(testContext))
+
+ string = testContext.getString(R.string.mozac_feature_addons_updater_status_error)
+ request = request.copy(status = Error("error", Exception()))
+
+ assertTrue(request.status?.toLocalizedString(testContext)!!.contains(string))
+
+ request = request.copy(status = NotInstalled)
+ assertEquals("", request.status.toLocalizedString(testContext))
+ }
+
+ @Test
+ fun setIcon() {
+ val iconView = mock<ImageView>()
+ val icon = mock<Bitmap>()
+ val addon = mock<Addon>()
+ whenever(addon.provideIcon()).thenReturn(icon)
+
+ iconView.setIcon(addon)
+
+ verify(iconView).setImageDrawable(any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/PermissionsDialogFragmentTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/PermissionsDialogFragmentTest.kt
new file mode 100644
index 0000000000..339c880380
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/PermissionsDialogFragmentTest.kt
@@ -0,0 +1,248 @@
+/* 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.addons.ui
+
+import android.view.Gravity.TOP
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.R
+import mozilla.components.feature.addons.ui.AddonDialogFragment.PromptsStyling
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+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
+
+@RunWith(AndroidJUnit4::class)
+class PermissionsDialogFragmentTest {
+
+ @Test
+ fun `build dialog`() {
+ val addon = Addon(
+ "id",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"),
+ permissions = listOf("privacy", "<all_urls>", "tabs"),
+ )
+ val fragment = createPermissionsDialogFragment(addon)
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val name = addon.translateName(testContext)
+ val titleTextView = dialog.findViewById<TextView>(R.id.title)
+ val optionalOrRequiredTextView = dialog.findViewById<TextView>(R.id.optional_or_required_text)
+ val permissionsRecyclerView = dialog.findViewById<RecyclerView>(R.id.permissions)
+ val recyclerAdapter = permissionsRecyclerView.adapter!! as RequiredPermissionsAdapter
+ val optionalOrRequiredText = fragment.buildOptionalOrRequiredText()
+ val permissionList = fragment.buildPermissionsList()
+
+ assertTrue(titleTextView.text.contains(name))
+ assertTrue(optionalOrRequiredText.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
+ assertTrue(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_privacy_description)))
+ assertTrue(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)))
+ assertTrue(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_tabs_description)))
+
+ assertTrue(optionalOrRequiredTextView.text.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
+ Assert.assertNotNull(recyclerAdapter)
+ assertEquals(3, recyclerAdapter.itemCount)
+ assertTrue(recyclerAdapter.getItemAtPosition(0).contains(testContext.getString(R.string.mozac_feature_addons_permissions_privacy_description)))
+ assertTrue(recyclerAdapter.getItemAtPosition(1).contains(testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)))
+ assertTrue(recyclerAdapter.getItemAtPosition(2).contains(testContext.getString(R.string.mozac_feature_addons_permissions_tabs_description)))
+ }
+
+ @Test
+ fun `clicking on dialog buttons notifies lambdas`() {
+ val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
+
+ val fragment = createPermissionsDialogFragment(addon)
+ var allowedWasExecuted = false
+ var denyWasExecuted = false
+
+ fragment.onPositiveButtonClicked = {
+ allowedWasExecuted = true
+ }
+
+ fragment.onNegativeButtonClicked = {
+ denyWasExecuted = true
+ }
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = dialog.findViewById<Button>(R.id.allow_button)
+ val negativeButton = dialog.findViewById<Button>(R.id.deny_button)
+
+ positiveButton.performClick()
+ negativeButton.performClick()
+
+ assertTrue(allowedWasExecuted)
+ assertTrue(denyWasExecuted)
+ }
+
+ @Test
+ fun `dismissing the dialog notifies deny lambda`() {
+ val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
+
+ val fragment = createPermissionsDialogFragment(addon)
+ var denyWasExecuted = false
+
+ fragment.onNegativeButtonClicked = {
+ denyWasExecuted = true
+ }
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ doReturn(mockFragmentManager()).`when`(fragment).parentFragmentManager
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ fragment.onCancel(mock())
+
+ assertTrue(denyWasExecuted)
+ }
+
+ @Test
+ fun `dialog must have all the styles of the feature promptsStyling object`() {
+ val addon = Addon("id", translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"))
+ val styling = PromptsStyling(TOP, true)
+ val fragment = createPermissionsDialogFragment(addon, styling)
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ val dialogAttributes = dialog.window!!.attributes
+
+ assertTrue(dialogAttributes.gravity == TOP)
+ assertTrue(dialogAttributes.width == ViewGroup.LayoutParams.MATCH_PARENT)
+ }
+
+ @Test
+ fun `handles add-ons without permissions`() {
+ val addon = Addon(
+ "id",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"),
+ permissions = emptyList(),
+ )
+ val fragment = createPermissionsDialogFragment(addon)
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val name = addon.translateName(testContext)
+ val titleTextView = dialog.findViewById<TextView>(R.id.title)
+ val optionalOrRequiredTextView = dialog.findViewById<TextView>(R.id.optional_or_required_text)
+ val permissionsRecyclerView = dialog.findViewById<RecyclerView>(R.id.permissions)
+ val recyclerAdapter = permissionsRecyclerView.adapter!! as RequiredPermissionsAdapter
+ val optionalOrRequiredText = fragment.buildOptionalOrRequiredText()
+ val permissionList = fragment.buildPermissionsList()
+
+ assertTrue(titleTextView.text.contains(name))
+ assertTrue(optionalOrRequiredText.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
+ assertTrue(optionalOrRequiredTextView.text.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
+ assertEquals(0, recyclerAdapter.itemCount)
+ assertFalse(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_privacy_description)))
+ assertFalse(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)))
+ assertFalse(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_tabs_description)))
+ }
+
+ @Test
+ fun `build dialog for optional permissions`() {
+ val addon = Addon(
+ "id",
+ translatableName = mapOf(Addon.DEFAULT_LOCALE to "my_addon"),
+ permissions = listOf("privacy", "https://example.org/", "tabs"),
+ )
+ val fragment = createPermissionsDialogFragment(addon, forOptionalPermissions = true, optionalPermissions = addon.permissions)
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val addonName = addon.translateName(testContext)
+ val titleTextView = dialog.findViewById<TextView>(R.id.title)
+ val optionalOrRequiredTextView = dialog.findViewById<TextView>(R.id.optional_or_required_text)
+ val permissionsRecyclerView = dialog.findViewById<RecyclerView>(R.id.permissions)
+ val recyclerAdapter = permissionsRecyclerView.adapter!! as RequiredPermissionsAdapter
+ val allowButton = dialog.findViewById<Button>(R.id.allow_button)
+ val denyButton = dialog.findViewById<Button>(R.id.deny_button)
+ val optionalOrRequiredText = fragment.buildOptionalOrRequiredText()
+ val permissionsList = fragment.buildPermissionsList()
+
+ assertEquals(
+ titleTextView.text,
+ testContext.getString(R.string.mozac_feature_addons_optional_permissions_dialog_title, addonName),
+ )
+
+ assertTrue(optionalOrRequiredText.contains(testContext.getString(R.string.mozac_feature_addons_optional_permissions_dialog_subtitle)))
+ assertTrue(permissionsList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_privacy_description)))
+ assertTrue(
+ permissionsList.contains(
+ testContext.getString(
+ R.string.mozac_feature_addons_permissions_one_site_description,
+ "example.org",
+ ),
+ ),
+ )
+ assertTrue(permissionsList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_tabs_description)))
+
+ assertTrue(optionalOrRequiredTextView.text.contains(testContext.getString(R.string.mozac_feature_addons_optional_permissions_dialog_subtitle)))
+ assertTrue(recyclerAdapter.getItemAtPosition(0).contains(testContext.getString(R.string.mozac_feature_addons_permissions_privacy_description)))
+ assertTrue(recyclerAdapter.getItemAtPosition(1).contains(testContext.getString(R.string.mozac_feature_addons_permissions_tabs_description)))
+ assertTrue(
+ recyclerAdapter.getItemAtPosition(2).contains(
+ testContext.getString(
+ R.string.mozac_feature_addons_permissions_one_site_description,
+ "example.org",
+ ),
+ ),
+ )
+
+ assertEquals(allowButton.text, testContext.getString(R.string.mozac_feature_addons_permissions_dialog_allow))
+ assertEquals(denyButton.text, testContext.getString(R.string.mozac_feature_addons_permissions_dialog_deny))
+ }
+
+ private fun createPermissionsDialogFragment(
+ addon: Addon,
+ promptsStyling: PromptsStyling? = null,
+ forOptionalPermissions: Boolean = false,
+ optionalPermissions: List<String> = emptyList(),
+ ): PermissionsDialogFragment {
+ return spy(
+ PermissionsDialogFragment.newInstance(
+ addon = addon,
+ promptsStyling = promptsStyling,
+ forOptionalPermissions = forOptionalPermissions,
+ optionalPermissions = optionalPermissions,
+ ),
+ ).apply {
+ doNothing().`when`(this).dismiss()
+ }
+ }
+
+ 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/addons/src/test/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterTest.kt
new file mode 100644
index 0000000000..a39e1435e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterTest.kt
@@ -0,0 +1,123 @@
+/* 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.addons.ui
+
+import android.widget.ImageButton
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class UnsupportedAddonsAdapterTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `removing successfully notifies the adapter item changed`() {
+ val addonManager: AddonManager = mock()
+ val unsupportedAddonsAdapterDelegate: UnsupportedAddonsAdapterDelegate = mock()
+ val addonOne = Addon("id1")
+ val addonTwo = Addon("id2")
+ val unsupportedAddons = listOf(addonOne, addonTwo)
+
+ val adapter = spy(
+ UnsupportedAddonsAdapter(
+ addonManager,
+ unsupportedAddonsAdapterDelegate,
+ unsupportedAddons,
+ ),
+ )
+
+ adapter.removeUninstalledAddon(addonOne)
+ verify(unsupportedAddonsAdapterDelegate, times(1)).onUninstallSuccess()
+ verify(adapter, times(1)).notifyDataSetChanged()
+ assertEquals(1, adapter.itemCount)
+
+ adapter.removeUninstalledAddon(addonTwo)
+ verify(unsupportedAddonsAdapterDelegate, times(2)).onUninstallSuccess()
+ verify(adapter, times(2)).notifyDataSetChanged()
+ assertEquals(0, adapter.itemCount)
+
+ adapter.removeUninstalledAddon(addonTwo)
+ verify(unsupportedAddonsAdapterDelegate, times(2)).onUninstallSuccess()
+ verify(adapter, times(2)).notifyDataSetChanged()
+ }
+
+ @Test
+ fun `uninstalling action disables all remove buttons`() {
+ val removeButtonOne = ImageButton(testContext)
+ val unsupportedViewHolderOne = UnsupportedAddonsAdapter.UnsupportedAddonViewHolder(
+ view = mock(),
+ iconView = mock(),
+ titleView = mock(),
+ removeButton = removeButtonOne,
+ )
+ val removeButtonTwo = ImageButton(testContext)
+ val unsupportedViewHolderTwo = UnsupportedAddonsAdapter.UnsupportedAddonViewHolder(
+ view = mock(),
+ iconView = mock(),
+ titleView = mock(),
+ removeButton = removeButtonTwo,
+ )
+ val addonManager: AddonManager = mock()
+ val addonOne = Addon("id1")
+ val addonTwo = Addon("id2")
+ val unsupportedAddons = mapOf(
+ unsupportedViewHolderOne to addonOne,
+ unsupportedViewHolderTwo to addonTwo,
+ )
+ val adapter = spy(
+ UnsupportedAddonsAdapter(
+ addonManager,
+ mock(),
+ unsupportedAddons.values.toList(),
+ ),
+ )
+
+ // mock the adapter.notifyDataSetChanged() behavior
+ whenever(adapter.notifyDataSetChanged()).thenAnswer {
+ unsupportedAddons.forEach { addonEntry ->
+ val addonPair = addonEntry.toPair()
+ adapter.bindRemoveButton(addonPair.first, addonPair.second)
+ }
+ }
+
+ val onSuccessCaptor = argumentCaptor<(() -> Unit)>()
+ adapter.bindRemoveButton(unsupportedViewHolderOne, addonOne)
+ assertFalse(adapter.pendingUninstall)
+ removeButtonOne.performClick()
+ assertTrue(adapter.pendingUninstall)
+ verify(adapter, times(1)).notifyDataSetChanged()
+ // All the visible remove buttons in the adapter should be disabled
+ assertFalse(removeButtonOne.isEnabled)
+ assertFalse(removeButtonTwo.isEnabled)
+
+ verify(addonManager).uninstallAddon(any(), onSuccessCaptor.capture(), any())
+ onSuccessCaptor.value.invoke()
+ assertFalse(adapter.pendingUninstall)
+ verify(adapter, times(2)).notifyDataSetChanged()
+ // All the visible remove buttons in the adapter should be enabled after uninstall complete
+ assertTrue(removeButtonOne.isEnabled)
+ assertTrue(removeButtonTwo.isEnabled)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/AddonUpdaterWorkerTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/AddonUpdaterWorkerTest.kt
new file mode 100644
index 0000000000..98ea74c426
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/AddonUpdaterWorkerTest.kt
@@ -0,0 +1,225 @@
+/* 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.addons.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertTrue
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionException
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import mozilla.components.support.webextensions.WebExtensionSupport
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AddonUpdaterWorkerTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setUp() {
+ GlobalAddonDependencyProvider.addonManager = null
+
+ initWebExtensionSupport()
+ }
+
+ private fun initWebExtensionSupport() {
+ val store = Mockito.spy(BrowserStore())
+ val engine: Engine = mock()
+ val extension: WebExtension = mock()
+ whenever(extension.id).thenReturn("addonId")
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(listOf(extension))
+ }
+ WebExtensionSupport.initialize(engine, store)
+ }
+
+ @After
+ fun after() {
+ GlobalAddonDependencyProvider.addonManager = null
+ }
+
+ @Test
+ fun `doWork - will return Result_success when SuccessfullyUpdated`() = runTestOnMain {
+ val updateAttemptStorage = mock<DefaultAddonUpdater.UpdateAttemptStorage>()
+ val addonId = "addonId"
+ val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
+ val addonManager = mock<AddonManager>()
+ val worker = spy(
+ TestListenableWorkerBuilder<AddonUpdaterWorker>(testContext)
+ .setInputData(AddonUpdaterWorker.createWorkerData(addonId))
+ .build(),
+ )
+
+ doReturn(updateAttemptStorage).`when`((worker as AddonUpdaterWorker)).updateAttemptStorage
+ GlobalAddonDependencyProvider.initialize(addonManager, mock())
+
+ whenever(addonManager.updateAddon(anyString(), onFinishCaptor.capture())).then {
+ onFinishCaptor.value.invoke(AddonUpdater.Status.SuccessfullyUpdated)
+ }
+
+ doReturn(this).`when`(worker).attemptScope
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.success(), result)
+ verify(worker).saveUpdateAttempt(addonId, AddonUpdater.Status.SuccessfullyUpdated)
+ }
+
+ @Test
+ fun `doWork - will return Result_success when NoUpdateAvailable`() = runTestOnMain {
+ val addonId = "addonId"
+ val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
+ val addonManager = mock<AddonManager>()
+ val worker = TestListenableWorkerBuilder<AddonUpdaterWorker>(testContext)
+ .setInputData(AddonUpdaterWorker.createWorkerData(addonId))
+ .build()
+
+ GlobalAddonDependencyProvider.initialize(addonManager, mock())
+
+ whenever(addonManager.updateAddon(anyString(), onFinishCaptor.capture())).then {
+ onFinishCaptor.value.invoke(AddonUpdater.Status.NoUpdateAvailable)
+ }
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.success(), result)
+ }
+
+ @Test
+ fun `doWork - will return Result_failure when NotInstalled`() = runTestOnMain {
+ val addonId = "addonId"
+ val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
+ val addonManager = mock<AddonManager>()
+ val worker = TestListenableWorkerBuilder<AddonUpdaterWorker>(testContext)
+ .setInputData(AddonUpdaterWorker.createWorkerData(addonId))
+ .build()
+
+ GlobalAddonDependencyProvider.initialize(addonManager, mock())
+
+ whenever(addonManager.updateAddon(anyString(), onFinishCaptor.capture())).then {
+ onFinishCaptor.value.invoke(AddonUpdater.Status.NotInstalled)
+ }
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.failure(), result)
+ }
+
+ @Test
+ fun `doWork - will return Result_retry when an Error happens and is recoverable`() = runTestOnMain {
+ val updateAttemptStorage = mock<DefaultAddonUpdater.UpdateAttemptStorage>()
+ val addonId = "addonId"
+ val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
+ val addonManager = mock<AddonManager>()
+ val worker = TestListenableWorkerBuilder<AddonUpdaterWorker>(testContext)
+ .setInputData(AddonUpdaterWorker.createWorkerData(addonId))
+ .build()
+ val recoverableException = WebExtensionException(Exception(), isRecoverable = true)
+ worker.updateAttemptStorage = updateAttemptStorage
+
+ GlobalAddonDependencyProvider.initialize(addonManager, mock())
+
+ whenever(addonManager.updateAddon(anyString(), onFinishCaptor.capture())).then {
+ onFinishCaptor.value.invoke(AddonUpdater.Status.Error("error", recoverableException))
+ }
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.retry(), result)
+ updateAttemptStorage.saveOrUpdate(any())
+ }
+
+ @Test
+ fun `doWork - will return Result_success when an Error happens and is unrecoverable`() = runTestOnMain {
+ val updateAttemptStorage = mock<DefaultAddonUpdater.UpdateAttemptStorage>()
+ val addonId = "addonId"
+ val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
+ val addonManager = mock<AddonManager>()
+ val worker = TestListenableWorkerBuilder<AddonUpdaterWorker>(testContext)
+ .setInputData(AddonUpdaterWorker.createWorkerData(addonId))
+ .build()
+ val unrecoverableException = WebExtensionException(Exception(), isRecoverable = false)
+ worker.updateAttemptStorage = updateAttemptStorage
+
+ GlobalAddonDependencyProvider.initialize(addonManager, mock())
+
+ whenever(addonManager.updateAddon(anyString(), onFinishCaptor.capture())).then {
+ onFinishCaptor.value.invoke(AddonUpdater.Status.Error("error", unrecoverableException))
+ }
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.success(), result)
+ updateAttemptStorage.saveOrUpdate(any())
+ }
+
+ @Test
+ fun `doWork - will try pass any exceptions to the crashReporter`() = runTestOnMain {
+ val addonId = "addonId"
+ val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
+ val addonManager = mock<AddonManager>()
+ val worker = TestListenableWorkerBuilder<AddonUpdaterWorker>(testContext)
+ .setInputData(AddonUpdaterWorker.createWorkerData(addonId))
+ .build()
+ var crashWasReported = false
+ val crashReporter: ((Throwable) -> Unit) = { _ ->
+ crashWasReported = true
+ }
+
+ GlobalAddonDependencyProvider.initialize(addonManager, mock(), crashReporter)
+ GlobalAddonDependencyProvider.addonManager = null
+
+ whenever(addonManager.updateAddon(anyString(), onFinishCaptor.capture())).then {
+ onFinishCaptor.value.invoke(AddonUpdater.Status.Error("error", Exception()))
+ }
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.success(), result)
+ assertTrue(crashWasReported)
+ }
+
+ @Test
+ fun `retryIfRecoverable must return retry for recoverable exception`() {
+ val recoverableException = WebExtensionException(Exception(), isRecoverable = true)
+ val worker = TestListenableWorkerBuilder<AddonUpdaterWorker>(testContext)
+ .build()
+
+ assertEquals(ListenableWorker.Result.retry(), worker.retryIfRecoverable(recoverableException))
+ }
+
+ @Test
+ fun `retryIfRecoverable must return success for unrecoverable exception`() {
+ val unrecoverableException = WebExtensionException(Exception(), isRecoverable = false)
+ val worker = TestListenableWorkerBuilder<AddonUpdaterWorker>(testContext)
+ .build()
+
+ assertEquals(ListenableWorker.Result.success(), worker.retryIfRecoverable(unrecoverableException))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/DefaultAddonUpdaterTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/DefaultAddonUpdaterTest.kt
new file mode 100644
index 0000000000..79900874c0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/DefaultAddonUpdaterTest.kt
@@ -0,0 +1,430 @@
+/* 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.addons.update
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.core.content.getSystemService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.Configuration
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.await
+import androidx.work.testing.WorkManagerTestInitHelper
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import mozilla.components.concept.engine.webextension.DisabledFlags
+import mozilla.components.concept.engine.webextension.Metadata
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.feature.addons.update.AddonUpdaterWorker.Companion.KEY_DATA_EXTENSIONS_ID
+import mozilla.components.feature.addons.update.DefaultAddonUpdater.Companion.WORK_TAG_IMMEDIATE
+import mozilla.components.feature.addons.update.DefaultAddonUpdater.Companion.WORK_TAG_PERIODIC
+import mozilla.components.feature.addons.update.DefaultAddonUpdater.NotificationHandlerService
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.worker.Frequency
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+class DefaultAddonUpdaterTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setUp() {
+ val configuration = Configuration.Builder().build()
+
+ // Initialize WorkManager (early) for instrumentation tests.
+ WorkManagerTestInitHelper.initializeTestWorkManager(testContext, configuration)
+ }
+
+ @Test
+ fun `registerForFutureUpdates - schedule work for future update`() = runTestOnMain {
+ val frequency = Frequency(1, TimeUnit.DAYS)
+ val updater = DefaultAddonUpdater(testContext, frequency, mock())
+ val addonId = "addonId"
+
+ val workId = updater.getUniquePeriodicWorkName(addonId)
+
+ val workManger = WorkManager.getInstance(testContext)
+ var workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertTrue(workData.isEmpty())
+
+ updater.registerForFutureUpdates(addonId)
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertFalse(workData.isEmpty())
+
+ assertExtensionIsRegisteredFoUpdates(updater, addonId)
+
+ // Cleaning work manager
+ workManger.cancelUniqueWork(workId)
+ }
+
+ @Test
+ fun `update - schedule work for immediate update`() = runTestOnMain {
+ val updater = DefaultAddonUpdater(
+ testContext,
+ notificationsDelegate = mock(),
+ )
+ val addonId = "addonId"
+
+ val workId = updater.getUniqueImmediateWorkName(addonId)
+
+ val workManger = WorkManager.getInstance(testContext)
+ var workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertTrue(workData.isEmpty())
+
+ updater.update(addonId)
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertFalse(workData.isEmpty())
+
+ val work = workData.first()
+
+ assertEquals(WorkInfo.State.ENQUEUED, work.state)
+ assertTrue(work.tags.contains(workId))
+ assertTrue(work.tags.contains(WORK_TAG_IMMEDIATE))
+
+ // Cleaning work manager
+ workManger.cancelUniqueWork(workId)
+ }
+
+ @Test
+ fun `onUpdatePermissionRequest - will create a notification when user has haven't allow new permissions`() {
+ val context = spy(testContext).also {
+ val packageManager: PackageManager = mock()
+ doReturn(Intent()).`when`(packageManager).getLaunchIntentForPackage(
+ ArgumentMatchers.anyString(),
+ )
+ doReturn(packageManager).`when`(it).packageManager
+ }
+
+ val notificationsDelegate: NotificationsDelegate = mock()
+
+ var allowedPreviously = false
+ val updater = spy(
+ DefaultAddonUpdater(
+ context,
+ notificationsDelegate = notificationsDelegate,
+ ),
+ )
+
+ val currentExt: WebExtension = mock()
+ val updatedExt: WebExtension = mock()
+ whenever(currentExt.id).thenReturn("addonId")
+ whenever(updatedExt.id).thenReturn("addonId")
+ val notificationId = NotificationHandlerService.getNotificationId(context, updatedExt.id)
+
+ val notification: Notification = mock()
+ val newPermissions = listOf("privacy")
+
+ doReturn(notification).`when`(updater).createNotification(updatedExt, newPermissions, notificationId)
+
+ updater.updateStatusStorage.clear(context)
+
+ updater.onUpdatePermissionRequest(currentExt, updatedExt, newPermissions) {
+ allowedPreviously = it
+ }
+
+ assertFalse(allowedPreviously)
+
+ verify(notificationsDelegate).notify(
+ null,
+ 10000,
+ notification,
+ )
+
+ updater.updateStatusStorage.clear(context)
+ }
+
+ @Test
+ fun `onUpdatePermissionRequest - should not show a notification for unknown permissions`() {
+ val context = spy(testContext).also {
+ val packageManager: PackageManager = mock()
+ doReturn(Intent()).`when`(packageManager).getLaunchIntentForPackage(
+ ArgumentMatchers.anyString(),
+ )
+ doReturn(packageManager).`when`(it).packageManager
+ }
+
+ var allowedPreviously = false
+ val updater = DefaultAddonUpdater(
+ context,
+ notificationsDelegate = mock(),
+ )
+ val currentExt: WebExtension = mock()
+ val updatedExt: WebExtension = mock()
+ whenever(currentExt.id).thenReturn("addonId")
+ whenever(updatedExt.id).thenReturn("addonId")
+
+ updater.updateStatusStorage.clear(context)
+
+ updater.onUpdatePermissionRequest(currentExt, updatedExt, listOf("normandyAddonStudy")) {
+ allowedPreviously = it
+ }
+
+ assertTrue(allowedPreviously)
+
+ val notificationId = NotificationHandlerService.getNotificationId(context, currentExt.id)
+
+ assertFalse(isNotificationVisible(notificationId))
+ assertFalse(updater.updateStatusStorage.isPreviouslyAllowed(testContext, currentExt.id))
+
+ updater.updateStatusStorage.clear(context)
+ }
+
+ @Test
+ fun `createContentText - notification content must adapt to the amount of valid permissions`() {
+ val updater = DefaultAddonUpdater(
+ testContext,
+ notificationsDelegate = mock(),
+ )
+ val validPermissions = listOf("privacy", "management")
+
+ var content = updater.createContentText(validPermissions).split("\n")
+ assertEquals("2 new permissions are required:", content[0].trim())
+ assertEquals("1-Read and modify privacy settings", content[1].trim())
+ assertEquals("2-Monitor extension usage and manage themes", content[2].trim())
+
+ val validAndInvalidPermissions = listOf("privacy", "invalid")
+ content = updater.createContentText(validAndInvalidPermissions).split("\n")
+
+ assertEquals("A new permission is required:", content[0].trim())
+ assertEquals("1-Read and modify privacy settings", content[1].trim())
+ }
+
+ @Test
+ fun `onUpdatePermissionRequest - will NOT create a notification when permissions were granted by the user`() {
+ val context = spy(testContext).also {
+ val packageManager: PackageManager = mock()
+ doReturn(Intent()).`when`(packageManager).getLaunchIntentForPackage(
+ ArgumentMatchers.anyString(),
+ )
+ doReturn(packageManager).`when`(it).packageManager
+ }
+
+ val updater = DefaultAddonUpdater(
+ context,
+ notificationsDelegate = mock(),
+ )
+ val currentExt: WebExtension = mock()
+ val updatedExt: WebExtension = mock()
+ whenever(currentExt.id).thenReturn("addonId")
+ whenever(updatedExt.id).thenReturn("addonId")
+
+ updater.updateStatusStorage.clear(context)
+
+ var allowedPreviously = false
+
+ updater.updateStatusStorage.markAsAllowed(context, currentExt.id)
+ updater.onUpdatePermissionRequest(currentExt, updatedExt, emptyList()) {
+ allowedPreviously = it
+ }
+
+ assertTrue(allowedPreviously)
+
+ val notificationId = NotificationHandlerService.getNotificationId(context, currentExt.id)
+
+ assertFalse(isNotificationVisible(notificationId))
+ assertFalse(updater.updateStatusStorage.isPreviouslyAllowed(testContext, currentExt.id))
+ updater.updateStatusStorage.clear(context)
+ }
+
+ @Test
+ fun `createAllowAction - will create an intent with the correct addon id and allow action`() {
+ val updater = spy(
+ DefaultAddonUpdater(
+ testContext,
+ notificationsDelegate = mock(),
+ ),
+ )
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("addonId")
+
+ updater.createAllowAction(ext, 1)
+
+ verify(updater).createNotificationIntent(ext.id, DefaultAddonUpdater.NOTIFICATION_ACTION_ALLOW)
+ }
+
+ @Test
+ fun `createDenyAction - will create an intent with the correct addon id and deny action`() {
+ val updater = spy(
+ DefaultAddonUpdater(
+ testContext,
+ notificationsDelegate = mock(),
+ ),
+ )
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("addonId")
+
+ updater.createDenyAction(ext, 1)
+
+ verify(updater).createNotificationIntent(ext.id, DefaultAddonUpdater.NOTIFICATION_ACTION_DENY)
+ }
+
+ @Test
+ fun `createNotificationIntent - will generate an intent with an addonId and an action`() {
+ val updater = DefaultAddonUpdater(
+ testContext,
+ notificationsDelegate = mock(),
+ )
+ val addonId = "addonId"
+ val action = "action"
+
+ val intent = updater.createNotificationIntent(addonId, action)
+
+ assertEquals(addonId, intent.getStringExtra(DefaultAddonUpdater.NOTIFICATION_EXTRA_ADDON_ID))
+ assertEquals(action, intent.action)
+ }
+
+ @Test
+ fun `unregisterForFutureUpdates - will remove scheduled work for future update`() = runTestOnMain {
+ val frequency = Frequency(1, TimeUnit.DAYS)
+ val updater = DefaultAddonUpdater(testContext, frequency, mock())
+ updater.scope = CoroutineScope(Dispatchers.Main)
+
+ val addonId = "addonId"
+
+ updater.updateAttempStorage = mock()
+
+ val workId = updater.getUniquePeriodicWorkName(addonId)
+
+ val workManger = WorkManager.getInstance(testContext)
+ var workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertTrue(workData.isEmpty())
+
+ updater.registerForFutureUpdates(addonId)
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
+
+ assertFalse(workData.isEmpty())
+
+ assertExtensionIsRegisteredFoUpdates(updater, addonId)
+
+ updater.unregisterForFutureUpdates(addonId)
+
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ assertEquals(WorkInfo.State.CANCELLED, workData.first().state)
+ verify(updater.updateAttempStorage).remove(addonId)
+ }
+
+ @Test
+ fun `createPeriodicWorkerRequest - will contains the right parameters`() {
+ val frequency = Frequency(1, TimeUnit.DAYS)
+ val updater = DefaultAddonUpdater(testContext, frequency, mock())
+ val addonId = "addonId"
+
+ val workId = updater.getUniquePeriodicWorkName(addonId)
+
+ val workRequest = updater.createPeriodicWorkerRequest(addonId)
+
+ assertTrue(workRequest.tags.contains(workId))
+ assertTrue(workRequest.tags.contains(WORK_TAG_PERIODIC))
+
+ assertEquals(updater.getWorkerConstrains(), workRequest.workSpec.constraints)
+
+ assertEquals(addonId, workRequest.workSpec.input.getString(KEY_DATA_EXTENSIONS_ID))
+ }
+
+ @Test
+ fun `registerForFutureUpdates - will register only unregistered extensions`() = runTestOnMain {
+ val updater = DefaultAddonUpdater(
+ testContext,
+ notificationsDelegate = mock(),
+ )
+ val registeredExt: WebExtension = mock()
+ val notRegisteredExt: WebExtension = mock()
+ whenever(registeredExt.id).thenReturn("registeredExt")
+ whenever(notRegisteredExt.id).thenReturn("notRegisteredExt")
+
+ updater.registerForFutureUpdates("registeredExt")
+
+ val extensions = listOf(registeredExt, notRegisteredExt)
+
+ assertExtensionIsRegisteredFoUpdates(updater, "registeredExt")
+
+ updater.registerForFutureUpdates(extensions)
+
+ extensions.forEach { ext ->
+ assertExtensionIsRegisteredFoUpdates(updater, ext.id)
+ }
+ }
+
+ @Test
+ fun `registerForFutureUpdates - will not register built-in and unsupported extensions`() = runTestOnMain {
+ val updater = DefaultAddonUpdater(
+ testContext,
+ notificationsDelegate = mock(),
+ )
+
+ val regularExt: WebExtension = mock()
+ whenever(regularExt.id).thenReturn("regularExt")
+
+ val builtInExt: WebExtension = mock()
+ whenever(builtInExt.id).thenReturn("builtInExt")
+ whenever(builtInExt.isBuiltIn()).thenReturn(true)
+
+ val unsupportedExt: WebExtension = mock()
+ whenever(unsupportedExt.id).thenReturn("unsupportedExt")
+ val metadata: Metadata = mock()
+ whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(DisabledFlags.APP_SUPPORT))
+ whenever(unsupportedExt.getMetadata()).thenReturn(metadata)
+
+ val extensions = listOf(regularExt, builtInExt, unsupportedExt)
+ updater.registerForFutureUpdates(extensions)
+
+ assertExtensionIsRegisteredFoUpdates(updater, regularExt.id)
+
+ assertExtensionIsNotRegisteredFoUpdates(updater, builtInExt.id)
+ assertExtensionIsNotRegisteredFoUpdates(updater, unsupportedExt.id)
+ }
+
+ private suspend fun assertExtensionIsRegisteredFoUpdates(updater: DefaultAddonUpdater, extId: String) {
+ val workId = updater.getUniquePeriodicWorkName(extId)
+ val workManger = WorkManager.getInstance(testContext)
+ val workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ val work = workData.first()
+
+ assertEquals(WorkInfo.State.ENQUEUED, work.state)
+ assertTrue(work.tags.contains(workId))
+ assertTrue(work.tags.contains(WORK_TAG_PERIODIC))
+ }
+
+ private suspend fun assertExtensionIsNotRegisteredFoUpdates(updater: DefaultAddonUpdater, extId: String) {
+ val workId = updater.getUniquePeriodicWorkName(extId)
+ val workManger = WorkManager.getInstance(testContext)
+ val workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ assertTrue("$extId should not have been registered for updates", workData.isEmpty())
+ }
+
+ private fun isNotificationVisible(notificationId: Int): Boolean {
+ val manager = testContext.getSystemService<NotificationManager>()!!
+
+ val notifications = manager.activeNotifications
+
+ return notifications.any { it.id == notificationId }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/GlobalAddonDependencyProviderTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/GlobalAddonDependencyProviderTest.kt
new file mode 100644
index 0000000000..3c2370d447
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/GlobalAddonDependencyProviderTest.kt
@@ -0,0 +1,44 @@
+/* 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.addons.update
+
+import junit.framework.TestCase.assertEquals
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+
+class GlobalAddonDependencyProviderTest {
+
+ @Before
+ fun before() {
+ GlobalAddonDependencyProvider.updater = null
+ GlobalAddonDependencyProvider.addonManager = null
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `requireAddonUpdater - without calling initialize`() {
+ GlobalAddonDependencyProvider.requireAddonUpdater()
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `requireAddonManager - without calling initialize`() {
+ GlobalAddonDependencyProvider.requireAddonManager()
+ }
+
+ @Test
+ fun `requireAddonManager - after initialize`() {
+ val manager = mock<AddonManager>()
+ GlobalAddonDependencyProvider.initialize(manager, mock())
+ assertEquals(manager, GlobalAddonDependencyProvider.requireAddonManager())
+ }
+
+ @Test
+ fun `requireAddonUpdater - after initialize`() {
+ val updater = mock<AddonUpdater>()
+ GlobalAddonDependencyProvider.initialize(mock(), updater)
+ assertEquals(updater, GlobalAddonDependencyProvider.requireAddonUpdater())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/NotificationHandlerServiceTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/NotificationHandlerServiceTest.kt
new file mode 100644
index 0000000000..ca999fa581
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/NotificationHandlerServiceTest.kt
@@ -0,0 +1,71 @@
+/* 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.addons.update
+
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import mozilla.components.feature.addons.update.DefaultAddonUpdater.NotificationHandlerService
+import mozilla.components.feature.addons.update.DefaultAddonUpdater.UpdateStatusStorage
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class NotificationHandlerServiceTest {
+
+ @Test
+ fun `onHandleIntent - reacts to the allow action`() {
+ val addonId = "addon_id"
+ val allowIntent = Intent(testContext, NotificationHandlerService::class.java).apply {
+ action = DefaultAddonUpdater.NOTIFICATION_ACTION_ALLOW
+ putExtra(DefaultAddonUpdater.NOTIFICATION_EXTRA_ADDON_ID, addonId)
+ }
+
+ val handler = spy(NotificationHandlerService())
+ val updater = mock<AddonUpdater>()
+ val storage = UpdateStatusStorage()
+
+ handler.context = testContext
+ GlobalAddonDependencyProvider.initialize(mock(), updater)
+
+ handler.onHandleIntent(allowIntent)
+
+ verify(handler).handleAllowAction(addonId)
+ verify(handler).removeNotification(addonId)
+ verify(updater).update(addonId)
+ assertTrue(storage.isPreviouslyAllowed(testContext, addonId))
+
+ storage.clear(testContext)
+ }
+
+ @Test
+ fun `onHandleIntent - reacts to the deny action`() {
+ val addonId = "addon_id"
+ val allowIntent = Intent(testContext, NotificationHandlerService::class.java).apply {
+ action = DefaultAddonUpdater.NOTIFICATION_ACTION_DENY
+ putExtra(DefaultAddonUpdater.NOTIFICATION_EXTRA_ADDON_ID, addonId)
+ }
+
+ val handler = spy(NotificationHandlerService())
+ val updater = mock<AddonUpdater>()
+ val storage = UpdateStatusStorage()
+
+ handler.context = testContext
+ GlobalAddonDependencyProvider.initialize(mock(), updater)
+
+ handler.onHandleIntent(allowIntent)
+
+ verify(handler).removeNotification(addonId)
+ verify(handler, times(0)).handleAllowAction(addonId)
+ verify(updater, times(0)).update("addon_id")
+ assertFalse(storage.isPreviouslyAllowed(testContext, "addon_id"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/UpdateAttemptStorageTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/UpdateAttemptStorageTest.kt
new file mode 100644
index 0000000000..124f7ccc9b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/UpdateAttemptStorageTest.kt
@@ -0,0 +1,72 @@
+/* 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.addons.update
+
+import androidx.room.DatabaseConfiguration
+import androidx.room.InvalidationTracker
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import mozilla.components.feature.addons.update.AddonUpdater.Status.SuccessfullyUpdated
+import mozilla.components.feature.addons.update.DefaultAddonUpdater.UpdateAttemptStorage
+import mozilla.components.feature.addons.update.db.UpdateAttemptDao
+import mozilla.components.feature.addons.update.db.UpdateAttemptsDatabase
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.*
+
+class UpdateAttemptStorageTest {
+
+ private lateinit var mockDAO: UpdateAttemptDao
+ private lateinit var storage: UpdateAttemptStorage
+
+ @Before
+ fun setup() {
+ mockDAO = mock()
+ storage = spy(
+ UpdateAttemptStorage(mock()).apply {
+ databaseInitializer = { mockDatabase(mockDAO) }
+ },
+ )
+ }
+
+ @Test
+ fun `save or update a request`() {
+ storage.saveOrUpdate(createNewRequest())
+
+ verify(mockDAO).insertOrUpdate(any())
+ }
+
+ @Test
+ fun `find a request by addonId`() {
+ storage.findUpdateAttemptBy(addonId = "addonId")
+
+ verify(mockDAO).getUpdateAttemptFor("addonId")
+ }
+
+ @Test
+ fun `remove a request`() {
+ storage.remove("addonId")
+
+ verify(mockDAO).deleteUpdateAttempt(any())
+ }
+
+ private fun createNewRequest(): AddonUpdater.UpdateAttempt {
+ return AddonUpdater.UpdateAttempt(
+ addonId = "mozilla-dev-ext",
+ date = Date(),
+ status = SuccessfullyUpdated,
+ )
+ }
+
+ private fun mockDatabase(dao: UpdateAttemptDao) = object : UpdateAttemptsDatabase() {
+ override fun updateAttemptDao() = dao
+ override fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper = mock()
+ override fun createInvalidationTracker(): InvalidationTracker = mock()
+ override fun clearAllTables() = Unit
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/UpdateStatusStorageTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/UpdateStatusStorageTest.kt
new file mode 100644
index 0000000000..5624049ba8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/UpdateStatusStorageTest.kt
@@ -0,0 +1,61 @@
+/* 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.addons.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import mozilla.components.feature.addons.update.DefaultAddonUpdater.UpdateStatusStorage
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UpdateStatusStorageTest {
+
+ private lateinit var storage: UpdateStatusStorage
+
+ @Before
+ fun setup() {
+ storage = UpdateStatusStorage()
+ storage.clear(testContext)
+ }
+
+ @Test
+ fun `isPreviouslyAllowed - returns the actual status of an addon`() {
+ var allowed = storage.isPreviouslyAllowed(testContext, "addonId")
+
+ assertFalse(allowed)
+
+ storage.markAsAllowed(testContext, "addonId")
+ allowed = storage.isPreviouslyAllowed(testContext, "addonId")
+
+ assertTrue(allowed)
+ }
+
+ @Test
+ fun `markAsUnallowed - deletes only the selected addonId from the storage`() {
+ var allowed = storage.isPreviouslyAllowed(testContext, "addonId")
+
+ assertFalse(allowed)
+
+ storage.markAsAllowed(testContext, "addonId")
+ storage.markAsAllowed(testContext, "another_addonId")
+
+ allowed = storage.isPreviouslyAllowed(testContext, "addonId")
+ assertTrue(allowed)
+
+ allowed = storage.isPreviouslyAllowed(testContext, "another_addonId")
+ assertTrue(allowed)
+
+ storage.markAsUnallowed(testContext, "addonId")
+ allowed = storage.isPreviouslyAllowed(testContext, "addonId")
+ assertFalse(allowed)
+
+ allowed = storage.isPreviouslyAllowed(testContext, "another_addonId")
+ assertTrue(allowed)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/db/UpdateAttemptEntityTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/db/UpdateAttemptEntityTest.kt
new file mode 100644
index 0000000000..54053cde69
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/db/UpdateAttemptEntityTest.kt
@@ -0,0 +1,115 @@
+/* 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.addons.update.db
+
+import mozilla.components.feature.addons.update.AddonUpdater
+import mozilla.components.feature.addons.update.AddonUpdater.Status.Error
+import mozilla.components.feature.addons.update.AddonUpdater.Status.NoUpdateAvailable
+import mozilla.components.feature.addons.update.AddonUpdater.Status.NotInstalled
+import mozilla.components.feature.addons.update.AddonUpdater.Status.SuccessfullyUpdated
+import mozilla.components.feature.addons.update.db.UpdateAttemptEntity.Companion.ERROR_DB
+import mozilla.components.feature.addons.update.db.UpdateAttemptEntity.Companion.NOT_INSTALLED_DB
+import mozilla.components.feature.addons.update.db.UpdateAttemptEntity.Companion.NO_UPDATE_AVAILABLE_DB
+import mozilla.components.feature.addons.update.db.UpdateAttemptEntity.Companion.SUCCESSFULLY_UPDATED_DB
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import java.util.Date
+
+class UpdateAttemptEntityTest {
+
+ @Test
+ fun `convert from db entity to domain class`() {
+ val dbEntity = UpdateAttemptEntity(
+ addonId = "mozilla-dev",
+ date = Date().time,
+ status = SUCCESSFULLY_UPDATED_DB,
+ )
+
+ val domainClass = dbEntity.toUpdateAttempt()
+
+ with(dbEntity) {
+ assertEquals(addonId, domainClass.addonId)
+ assertEquals(date, domainClass.date.time)
+ assertEquals(SuccessfullyUpdated, domainClass.status)
+ }
+ }
+
+ @Test
+ fun `convert from domain class to db entity`() {
+ val domainClass = AddonUpdater.UpdateAttempt(
+ addonId = "mozilla-dev",
+ date = Date(),
+ status = Error("error", Exception()),
+ )
+
+ val dbEntity = domainClass.toEntity()
+
+ with(dbEntity) {
+ assertEquals(addonId, domainClass.addonId)
+ assertEquals(date, domainClass.date.time)
+ assertEquals(Error("error", Exception()).toString(), domainClass.status.toString())
+ assertEquals(errorMessage, "error")
+ }
+ }
+
+ @Test
+ fun `convert from db status to domain status`() {
+ var domainStatus: AddonUpdater.Status? = createDBUpdateAttempt(NOT_INSTALLED_DB).toStatus()
+
+ assertEquals(NotInstalled, domainStatus)
+
+ domainStatus = createDBUpdateAttempt(SUCCESSFULLY_UPDATED_DB).toStatus()
+
+ assertEquals(SuccessfullyUpdated, domainStatus)
+
+ domainStatus = createDBUpdateAttempt(NO_UPDATE_AVAILABLE_DB).toStatus()
+
+ assertEquals(NoUpdateAvailable, domainStatus)
+
+ domainStatus = createDBUpdateAttempt(ERROR_DB).toStatus()
+
+ assertEquals(Error("error_message", Exception("error_trace")).toString(), domainStatus.toString())
+
+ domainStatus = createDBUpdateAttempt(Int.MAX_VALUE).toStatus()
+
+ assertNull(domainStatus)
+ }
+
+ @Test
+ fun `convert from domain status to db status`() {
+ val request = AddonUpdater.UpdateAttempt("id", Date(), status = NotInstalled)
+ var dbStatus: Int = request.toEntity().status
+
+ assertEquals(NOT_INSTALLED_DB, dbStatus)
+
+ dbStatus = request.copy(status = SuccessfullyUpdated)
+ .toEntity()
+ .status
+
+ assertEquals(SUCCESSFULLY_UPDATED_DB, dbStatus)
+
+ dbStatus = request.copy(status = NoUpdateAvailable)
+ .toEntity()
+ .status
+
+ assertEquals(NO_UPDATE_AVAILABLE_DB, dbStatus)
+
+ val exception = Exception("")
+ val dbEntity = AddonUpdater.UpdateAttempt(
+ addonId = "id",
+ date = Date(),
+ status = Error("error message", exception),
+ )
+ .toEntity()
+
+ assertEquals(ERROR_DB, dbEntity.status)
+ assertEquals(exception.stackTrace.first().toString(), dbEntity.errorTrace)
+ }
+
+ private fun createDBUpdateAttempt(status: Int): UpdateAttemptEntity {
+ return UpdateAttemptEntity("id", 1L, status, "error_message", "error_trace")
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/worker/ExtensionsTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/worker/ExtensionsTest.kt
new file mode 100644
index 0000000000..36996fc9ed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/java/mozilla/components/feature/addons/worker/ExtensionsTest.kt
@@ -0,0 +1,40 @@
+/* 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.addons.worker
+
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.CancellationException
+import mozilla.components.concept.engine.webextension.WebExtensionException
+import org.junit.Test
+import java.io.IOException
+
+class ExtensionsTest {
+
+ @Test
+ fun `shouldReport - when cause is an IOException must NOT be reported`() {
+ assertFalse(Exception(IOException()).shouldReport())
+ }
+
+ @Test
+ fun `shouldReport - when cause is a CancellationException must NOT be reported`() {
+ assertFalse(Exception(CancellationException()).shouldReport())
+ }
+
+ @Test
+ fun `shouldReport - when cause the exception is a CancellationException must NOT be reported`() {
+ assertFalse(CancellationException().shouldReport())
+ }
+
+ @Test
+ fun `shouldReport - when the exception isRecoverable must be reported`() {
+ assertTrue(WebExtensionException(java.lang.Exception(), isRecoverable = true).shouldReport())
+ }
+
+ @Test
+ fun `shouldReport - when the exception NOT isRecoverable must NOT be reported`() {
+ assertFalse(WebExtensionException(java.lang.Exception(), isRecoverable = false).shouldReport())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_localized_single_result.json b/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_localized_single_result.json
new file mode 100644
index 0000000000..94aeed1f64
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_localized_single_result.json
@@ -0,0 +1,231 @@
+{
+ "page_size": 25,
+ "page_count": 1,
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 607454,
+ "authors": [
+ {
+ "id": 11423598,
+ "name": "Raymond Hill",
+ "url": "https://addons.mozilla.org/ca/firefox/user/11423598/",
+ "username": "gorhill"
+ }
+ ],
+ "average_daily_users": 6229783,
+ "categories": {
+ "android": [
+ "security-privacy"
+ ],
+ "firefox": [
+ "privacy-security"
+ ]
+ },
+ "contributions_url": "",
+ "created": "2015-04-25T07:26:22Z",
+ "current_version": {
+ "id": 5596914,
+ "compatibility": {
+ "firefox": {
+ "min": "78.0",
+ "max": "*"
+ },
+ "android": {
+ "min": "79.0",
+ "max": "*"
+ }
+ },
+ "edit_url": "https://addons.mozilla.org/ca/developers/addon/ublock-origin/versions/5596914",
+ "is_strict_compatibility_enabled": false,
+ "license": {
+ "id": 6,
+ "is_custom": false,
+ "name": "GNU General Public License v3.0",
+ "url": "http://www.gnu.org/licenses/gpl-3.0.html"
+ },
+ "release_notes": "See complete release notes for <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/57eadd553bcb629be4f7576c9aa9a808cc5e918a6ea1413dff063281d4896978/https%3A//github.com/gorhill/uBlock/releases/tag/1.51.0\" rel=\"nofollow\">1.51.0</a>.\n\n<b>Fixes / changes</b>\n\n<ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/a578674b791ed67b926b31004613b0631d0cc73efc2d263385eb842dc0dff09f/https%3A//github.com/gorhill/uBlock/commit/ee0649329c59\" rel=\"nofollow\">Remove obsolete web<em>accessible</em>resources</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7071379ad6c88fec4e40b3ff8165d4fd3ecdaae2fbf0ea145fcf21eb0ea86e43/https%3A//github.com/gorhill/uBlock/commit/cdf385f5f46e\" rel=\"nofollow\">Add missing (deprecated) method to google ima</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/bf46690e10ed8a086d9d50080177c67380aeb5cc7945d96acdbde3fdf7aa9466/https%3A//github.com/gorhill/uBlock/commit/aa6baf9a29db\" rel=\"nofollow\">Fix regression in handling of experimental <code>header=</code> filter option</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f2328063ec5c7512d5899f4b7db7324d42864cea3678aff50af908219b976094/https%3A//github.com/gorhill/uBlock/commit/0da7e12ea4a0\" rel=\"nofollow\">Only already normalized CSS selectors can be fast path-compiled</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/e271cd306c9578e0ed17c785486d183abc8eed2408c7f3fb22eca3afff3ab478/https%3A//github.com/gorhill/uBlock/commit/ec0698196563\" rel=\"nofollow\">Improve compatibility with AdGuard's scriptlets</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f42373a2522c9739807b48d956a9437aa9c7e6b6b4a613a24d412d309ac8c037/https%3A//github.com/gorhill/uBlock/commit/5ebdbf3e2439\" rel=\"nofollow\">Add static network filter option: <code>permissions</code></a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f502abdb178413a344a43d8d0e57463d6c9a7ef3c01a437097ec6a2fd97e1350/https%3A//github.com/gorhill/uBlock/commit/786d9b2212e9\" rel=\"nofollow\">Add <code>set-attr</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/46449872121aa9a39f8fe5f1499dccec24fc094eb72848f37974606abd562997/https%3A//github.com/gorhill/uBlock/commit/fea6f7f311a5\" rel=\"nofollow\">Do not bail too early when trapping properties in <code>acs</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/76b28688717445a36e5ee48387a0e7392370e22d5e3a2fbb53d2b965f54f1eee/https%3A//github.com/gorhill/uBlock/commit/80b3f3c3c020\" rel=\"nofollow\">Fix regression in cloud storage import of \"Filter lists\" pane</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/eb0f0fdfbd50904d7cd8e260b62d0ca4b233c8f9d8a18f9abb31472026e07b23/https%3A//github.com/gorhill/uBlock/commit/083a318090e3\" rel=\"nofollow\">Add <code>set-session-storage-item</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f125a8290e220a02336532a360ea37a7a4ff1c4402a5f518785b7354a7a491d8/https%3A//github.com/gorhill/uBlock/commit/60b21b142268\" rel=\"nofollow\">Prevent negative position when widget size is greater than viewport size</a><ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/e49ec519e35ce90b1ce42475f48affc9a34e97640a60ca13a6ff3d23b58b0ade/https%3A//github.com/gorhill/uBlock/commit/b44815f0c8f0\" rel=\"nofollow\">Ensure no negative value for <code>top</code> property of floating widget in logger</a></li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/2398a39690c82a85f3fb23710721b490fedd503cf3003fdad3b23a8193fb257d/https%3A//github.com/gorhill/uBlock/commit/622cda2cdf91\" rel=\"nofollow\">Add visual hint when not all sublists are enabled</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f549cc526fa10665161c86930eccf00650b123e9c5f0b9ddc11038d7349600cd/https%3A//github.com/gorhill/uBlock/commit/33b409dd5bae\" rel=\"nofollow\">Add support for AdGuard's noop (<code>_</code>) network filter option</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/c649c1ddb8b1d7f280a0687edeae15b9c7b65d4101901337eff3cb8acb6370f0/https%3A//github.com/gorhill/uBlock/commit/5d6e10318662\" rel=\"nofollow\">Add \"tabless\" filter expression for logger output</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/3f7e7d0a06e918e4c8b3f8bb91d33e700a99ff9c2988069b609f614228e6ccf4/https%3A//github.com/gorhill/uBlock/commit/194354cd5d77\" rel=\"nofollow\">Add support for logical expressions to <code>!#if</code> directive</a><ul><li>Also added support for <code>!#else</code></li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f1e827e5c4e256a0c832f985f6a21bb39743a5dfecd5d4014ae3aa9b09d9e116/https%3A//github.com/gorhill/uBlock/commit/7867c2512807\" rel=\"nofollow\">Add resource aliases for increased compatibility with AdGuard lists</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6f42d070e087002ada8f37b36176d8d40d40dafcc5f5e9a1050c689ea079017f/https%3A//github.com/gorhill/uBlock/commit/fd036a51ee20\" rel=\"nofollow\">Add compatibility with AdGuard's <code>#%#//scriptlet(...)</code> syntax</a><ul><li>Also added support for quoted parameters in <code>##+js(...)</code> syntax</li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/50632bd7b34e7e7185c7642640111d9f2ba5493bea1e3ed1c517702302fd4938/https%3A//github.com/gorhill/uBlock/commit/8b7a5264deb4\" rel=\"nofollow\">Fix syntax highlighter throwing with invalid patterns</a></li><li>...</li></ul>\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7c755863346ab5ff9e77aeee9ffcd4ef47f343f5c7d2f2f143dcd474b8558641/https%3A//github.com/gorhill/uBlock/compare/1.50.0...1.51.0\" rel=\"nofollow\">Commits history since last version</a>.",
+ "reviewed": "2023-07-25T09:58:22Z",
+ "version": "1.51.0",
+ "files": [
+ {
+ "id": 4141256,
+ "created": "2023-07-19T23:09:25Z",
+ "hash": "sha256:8b73468bc233a11dd2895219466381783d19123857dd0b6fd16a01820fca4834",
+ "is_restart_required": false,
+ "is_webextension": true,
+ "is_mozilla_signed_extension": false,
+ "platform": "all",
+ "size": 3538418,
+ "status": "public",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4141256/ublock_origin-1.51.0.xpi",
+ "permissions": [
+ "dns",
+ "menus",
+ "privacy",
+ "storage",
+ "tabs",
+ "unlimitedStorage",
+ "webNavigation",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ "http://*/*",
+ "https://*/*",
+ "file://*/*",
+ "https://easylist.to/*",
+ "https://*.fanboy.co.nz/*",
+ "https://filterlists.com/*",
+ "https://forums.lanik.us/*",
+ "https://github.com/*",
+ "https://*.github.io/*",
+ "https://*.letsblock.it/*"
+ ],
+ "optional_permissions": [],
+ "host_permissions": []
+ }
+ ]
+ },
+ "default_locale": "en-US",
+ "description": "Un bloquejador eficient: el consum de memòria i de processador és baix però, no obstant això, pot carregar i aplicar milers de filtres més que altres bloquejadors coneguts.\n\nGràfic de l'eficiència: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nÚs: El gran botó d'engegada de la finestra emergent serveix per a desactivar/activar permanentment el uBlock per al lloc web actual. No és un botó d'engegada general de l'extensió.\n\n***\n\nFlexible, és més que un \"bloquejador d'anuncis\": també pot llegir i crear filtres a partir de fitxers hosts.\n\nPer defecte, es carreguen i s'apliquen aquestes llistes de filtres:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Dominis de malware\n\nSi voleu, podeu seleccionar altres llistes disponibles:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- I altres\n\nÒbviament, com més filtres activeu, més gran serà el consum de memòria. Però fins i tot després d'afegir dues llistes extra de Fanboy, hpHosts’s Ad and tracking servers, el uBlock₀ encara té un consum de memòria inferior al d'altres bloquejadors coneguts.\n\nTambé heu de ser conscient que seleccionant algunes d'aquestes llistes extra és més probable trobar-se amb llocs webs inservibles -- especialment aquelles llistes que s'utilitzen normalment com a fitxer de hosts.\n\n***\n\nSense les llistes predefinides de filtres, aquesta extensió no és res. Així que, si en cap moment voleu fer una aportació, penseu en les persones que treballen durament per a mantenir les llistes de filtres que utilitzeu, a disposició de tothom de manera gratuïta.\n\n***\n\nLliure.\nCodi obert amb llicència pública (GPLv3)\nPer usuaris per a usuaris.\n\nCol·laboradors a Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nCol·laboradors a Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nAquesta és, en certa manera, una versió primitiva. Tingueu-ho en compte quan en doneu la vostra opinió.\n\nRegistre de canvis del projecte:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "developer_comments": null,
+ "edit_url": "https://addons.mozilla.org/ca/developers/addon/ublock-origin/edit",
+ "guid": "uBlock0@raymondhill.net",
+ "has_eula": false,
+ "has_privacy_policy": true,
+ "homepage": "https://github.com/gorhill/uBlock#ublock-origin",
+ "icon_url": "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "icons": {
+ "32": "https://addons.mozilla.org/user-media/addon_icons/607/607454-32.png?modified=mcrushed",
+ "64": "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "128": "https://addons.mozilla.org/user-media/addon_icons/607/607454-128.png?modified=mcrushed"
+ },
+ "is_disabled": false,
+ "is_experimental": false,
+ "last_updated": "2023-08-07T17:15:41Z",
+ "name": "uBlock Origin",
+ "previews": [
+ {
+ "id": 238546,
+ "caption": "The popup panel: default mode",
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238546.png?modified=1622132421",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238546.jpg?modified=1622132421"
+ },
+ {
+ "id": 238548,
+ "caption": "The dashboard: stock filter lists",
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238548.png?modified=1622132423",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238548.jpg?modified=1622132423"
+ },
+ {
+ "id": 238547,
+ "caption": "The popup panel: default-deny mode",
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238547.png?modified=1622132425",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238547.jpg?modified=1622132425"
+ },
+ {
+ "id": 238549,
+ "caption": "The dashboard: settings",
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238549.png?modified=1622132426",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238549.jpg?modified=1622132426"
+ },
+ {
+ "id": 238552,
+ "caption": "The popup panel in Firefox Preview: default mode with more blocking options revealed",
+ "image_size": [
+ 970,
+ 1800
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238552.png?modified=1622132430",
+ "thumbnail_size": [
+ 216,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238552.jpg?modified=1622132430"
+ },
+ {
+ "id": 230370,
+ "caption": "The unified logger tells you all that uBO is seeing and doing",
+ "image_size": [
+ 800,
+ 600
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/230/230370.png?modified=1622132432",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/230/230370.jpg?modified=1622132432"
+ }
+ ],
+ "promoted": {
+ "apps": [
+ "firefox",
+ "android"
+ ],
+ "category": "recommended"
+ },
+ "ratings": {
+ "average": 4.7825,
+ "bayesian_average": 4.782204826721061,
+ "count": 15799,
+ "text_count": 4101
+ },
+ "ratings_url": "https://addons.mozilla.org/ca/firefox/addon/ublock-origin/reviews/",
+ "requires_payment": false,
+ "review_url": "https://addons.mozilla.org/ca/reviewers/review/607454",
+ "slug": "ublock-origin",
+ "status": "public",
+ "summary": "Finalment, un blocador eficient que utilitza pocs recursos de memòria i processador.",
+ "support_email": null,
+ "support_url": "https://old.reddit.com/r/uBlockOrigin/",
+ "tags": [
+ "ad blocker",
+ "anti malware",
+ "anti tracker",
+ "content blocker",
+ "privacy",
+ "security"
+ ],
+ "type": "extension",
+ "url": "https://addons.mozilla.org/ca/firefox/addon/ublock-origin/",
+ "versions_url": "https://addons.mozilla.org/ca/firefox/addon/ublock-origin/versions/",
+ "weekly_downloads": 143905,
+ "_score": null
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_multiple_results.json b/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_multiple_results.json
new file mode 100644
index 0000000000..8d0b13dad1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_multiple_results.json
@@ -0,0 +1,689 @@
+{
+ "page_size": 25,
+ "page_count": 1,
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 607454,
+ "authors": [
+ {
+ "id": 11423598,
+ "name": "Raymond Hill",
+ "url": "https://addons.mozilla.org/en-US/firefox/user/11423598/",
+ "username": "gorhill"
+ }
+ ],
+ "average_daily_users": 6229783,
+ "categories": {
+ "android": [
+ "security-privacy"
+ ],
+ "firefox": [
+ "privacy-security"
+ ]
+ },
+ "contributions_url": "",
+ "created": "2015-04-25T07:26:22Z",
+ "current_version": {
+ "id": 5596914,
+ "compatibility": {
+ "firefox": {
+ "min": "78.0",
+ "max": "*"
+ },
+ "android": {
+ "min": "79.0",
+ "max": "*"
+ }
+ },
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/versions/5596914",
+ "is_strict_compatibility_enabled": false,
+ "license": {
+ "id": 6,
+ "is_custom": false,
+ "name": {
+ "en-US": "GNU General Public License v3.0"
+ },
+ "url": "http://www.gnu.org/licenses/gpl-3.0.html"
+ },
+ "release_notes": {
+ "en-US": "See complete release notes for <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/57eadd553bcb629be4f7576c9aa9a808cc5e918a6ea1413dff063281d4896978/https%3A//github.com/gorhill/uBlock/releases/tag/1.51.0\" rel=\"nofollow\">1.51.0</a>.\n\n<b>Fixes / changes</b>\n\n<ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/a578674b791ed67b926b31004613b0631d0cc73efc2d263385eb842dc0dff09f/https%3A//github.com/gorhill/uBlock/commit/ee0649329c59\" rel=\"nofollow\">Remove obsolete web<em>accessible</em>resources</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7071379ad6c88fec4e40b3ff8165d4fd3ecdaae2fbf0ea145fcf21eb0ea86e43/https%3A//github.com/gorhill/uBlock/commit/cdf385f5f46e\" rel=\"nofollow\">Add missing (deprecated) method to google ima</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/bf46690e10ed8a086d9d50080177c67380aeb5cc7945d96acdbde3fdf7aa9466/https%3A//github.com/gorhill/uBlock/commit/aa6baf9a29db\" rel=\"nofollow\">Fix regression in handling of experimental <code>header=</code> filter option</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f2328063ec5c7512d5899f4b7db7324d42864cea3678aff50af908219b976094/https%3A//github.com/gorhill/uBlock/commit/0da7e12ea4a0\" rel=\"nofollow\">Only already normalized CSS selectors can be fast path-compiled</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/e271cd306c9578e0ed17c785486d183abc8eed2408c7f3fb22eca3afff3ab478/https%3A//github.com/gorhill/uBlock/commit/ec0698196563\" rel=\"nofollow\">Improve compatibility with AdGuard's scriptlets</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f42373a2522c9739807b48d956a9437aa9c7e6b6b4a613a24d412d309ac8c037/https%3A//github.com/gorhill/uBlock/commit/5ebdbf3e2439\" rel=\"nofollow\">Add static network filter option: <code>permissions</code></a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f502abdb178413a344a43d8d0e57463d6c9a7ef3c01a437097ec6a2fd97e1350/https%3A//github.com/gorhill/uBlock/commit/786d9b2212e9\" rel=\"nofollow\">Add <code>set-attr</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/46449872121aa9a39f8fe5f1499dccec24fc094eb72848f37974606abd562997/https%3A//github.com/gorhill/uBlock/commit/fea6f7f311a5\" rel=\"nofollow\">Do not bail too early when trapping properties in <code>acs</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/76b28688717445a36e5ee48387a0e7392370e22d5e3a2fbb53d2b965f54f1eee/https%3A//github.com/gorhill/uBlock/commit/80b3f3c3c020\" rel=\"nofollow\">Fix regression in cloud storage import of \"Filter lists\" pane</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/eb0f0fdfbd50904d7cd8e260b62d0ca4b233c8f9d8a18f9abb31472026e07b23/https%3A//github.com/gorhill/uBlock/commit/083a318090e3\" rel=\"nofollow\">Add <code>set-session-storage-item</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f125a8290e220a02336532a360ea37a7a4ff1c4402a5f518785b7354a7a491d8/https%3A//github.com/gorhill/uBlock/commit/60b21b142268\" rel=\"nofollow\">Prevent negative position when widget size is greater than viewport size</a><ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/e49ec519e35ce90b1ce42475f48affc9a34e97640a60ca13a6ff3d23b58b0ade/https%3A//github.com/gorhill/uBlock/commit/b44815f0c8f0\" rel=\"nofollow\">Ensure no negative value for <code>top</code> property of floating widget in logger</a></li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/2398a39690c82a85f3fb23710721b490fedd503cf3003fdad3b23a8193fb257d/https%3A//github.com/gorhill/uBlock/commit/622cda2cdf91\" rel=\"nofollow\">Add visual hint when not all sublists are enabled</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f549cc526fa10665161c86930eccf00650b123e9c5f0b9ddc11038d7349600cd/https%3A//github.com/gorhill/uBlock/commit/33b409dd5bae\" rel=\"nofollow\">Add support for AdGuard's noop (<code>_</code>) network filter option</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/c649c1ddb8b1d7f280a0687edeae15b9c7b65d4101901337eff3cb8acb6370f0/https%3A//github.com/gorhill/uBlock/commit/5d6e10318662\" rel=\"nofollow\">Add \"tabless\" filter expression for logger output</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/3f7e7d0a06e918e4c8b3f8bb91d33e700a99ff9c2988069b609f614228e6ccf4/https%3A//github.com/gorhill/uBlock/commit/194354cd5d77\" rel=\"nofollow\">Add support for logical expressions to <code>!#if</code> directive</a><ul><li>Also added support for <code>!#else</code></li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f1e827e5c4e256a0c832f985f6a21bb39743a5dfecd5d4014ae3aa9b09d9e116/https%3A//github.com/gorhill/uBlock/commit/7867c2512807\" rel=\"nofollow\">Add resource aliases for increased compatibility with AdGuard lists</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6f42d070e087002ada8f37b36176d8d40d40dafcc5f5e9a1050c689ea079017f/https%3A//github.com/gorhill/uBlock/commit/fd036a51ee20\" rel=\"nofollow\">Add compatibility with AdGuard's <code>#%#//scriptlet(...)</code> syntax</a><ul><li>Also added support for quoted parameters in <code>##+js(...)</code> syntax</li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/50632bd7b34e7e7185c7642640111d9f2ba5493bea1e3ed1c517702302fd4938/https%3A//github.com/gorhill/uBlock/commit/8b7a5264deb4\" rel=\"nofollow\">Fix syntax highlighter throwing with invalid patterns</a></li><li>...</li></ul>\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7c755863346ab5ff9e77aeee9ffcd4ef47f343f5c7d2f2f143dcd474b8558641/https%3A//github.com/gorhill/uBlock/compare/1.50.0...1.51.0\" rel=\"nofollow\">Commits history since last version</a>."
+ },
+ "reviewed": "2023-07-25T09:58:22Z",
+ "version": "1.51.0",
+ "files": [
+ {
+ "id": 4141256,
+ "created": "2023-07-19T23:09:25Z",
+ "hash": "sha256:8b73468bc233a11dd2895219466381783d19123857dd0b6fd16a01820fca4834",
+ "is_restart_required": false,
+ "is_webextension": true,
+ "is_mozilla_signed_extension": false,
+ "platform": "all",
+ "size": 3538418,
+ "status": "public",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4141256/ublock_origin-1.51.0.xpi",
+ "permissions": [
+ "dns",
+ "menus",
+ "privacy",
+ "storage",
+ "tabs",
+ "unlimitedStorage",
+ "webNavigation",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ "http://*/*",
+ "https://*/*",
+ "file://*/*",
+ "https://easylist.to/*",
+ "https://*.fanboy.co.nz/*",
+ "https://filterlists.com/*",
+ "https://forums.lanik.us/*",
+ "https://github.com/*",
+ "https://*.github.io/*",
+ "https://*.letsblock.it/*"
+ ],
+ "optional_permissions": [],
+ "host_permissions": []
+ }
+ ]
+ },
+ "default_locale": "en-US",
+ "description": {
+ "ar": "مانع إعلانات كفوء: خفيف على الذاكرة و المعالج, على الرغم من قدرته على تحميل و تطبيق الألاف من الفلاتر أكثر من بعض أشهر مانعي الإعلانات.\n\nتوضيح عام لكفاءة الإضافة: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nالإستخدام: زر التشغيل الكبير في النافذة المنبثقة هو لتعطيل أو تشغيل uBlock للموقع الحالي. هو ينطبق على الموقع الحالي فقط، و ليس زر تشغيل عام.\n\n***\n\nمع مرونته، هو أكثر من مجرد \"مانع إعلانات\": بإمكانه أيضا قراءة و إنشاء فلاتر من ملفات الإستقبال.\n\nفلاتر حديثة، هذه القوائم من الفلاتر يتم تحميلها و تطبيقها:\n\n- EasyList\n- قائمة خادم الإعلانات لـPeter Lowe\n- EasyPrivacy\n- نطاقات البرامج الضارة\n\nيوفر لك قوائم أكثر لتختار منها إذا كنت ترغب:\n\nقائم التتبع المحسنة لـFanboy\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- و الكثير من القوائم الأخرى.\n\nطبعا، كلما مكَّنتَ فلاتر أكثر، كلما ازداد أثرها على الذاكرة. حتى مع الرغم من إضافة القوائم الإضافية لـFanboy، و قوائم تتبع إعلان hpHost، ما زال بإمكان uBlock₀ العمل بأدنى أثر على الذاكرة أفضل من بعض أشهر قوائم التتبع.\n\nأيضا، كن على علم أن تحديد بعض من هذه القوائم الإضافية قد يؤدي إلى إمكانية أعلى لتعطيل المواقع -- خصوصا تلك القوائم التي تستخدم عادة كملفات مضيفة.\n\n***\n\nبدون وجود قوائم الفلترات, هذه الإضافة عديمة القيمة. إذن إن كانت لديك الرغبة في المساهمة، فكر في أولئك الذين يعملون بجد لصيانة قوائم الفلترات التي تستخدمها، التي تمت إتاحتها لك لتسخدمها مجَّاناََ.\n\n***\n\nمجاناً.\nمفتوح المصدر مع رخصة (GPLv3)\nللمستخدمين من طرف مستخدمين أخرين.\n\nالمساهمون في Github:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nالمساهمون في Crowdin:\n <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nالإضافة في قيد الإنجاز، خذ هذا في عين الإعتبار عندما تستعرضها.\n\nسجل التغييرات للمشروع:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "bg": "Ефикасен блокер: с малко използване на паметта и процесора, но същевременно способен да зарежда и налага хиляди допълнителни филтри в сравнение с други популярни блокери.\n\nИлюстрация на неговата ефикасност: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nИзползване: Големият бутон за \"Включване\" в изскачащият прозорец служи за трайно включване/изключване на uBlock за текущия сайт. Той се отнася само за текущия сайт, не е глобален бутон за включване.\n\n***\n\nГъвкав, той е повече от \"блокер на реклами\": може да чете и създава филтри от хост файлове.\n\nПри първоначално използване са заредени и наложени следните списъци с филтри:\n\n- EasyList\n- Списък с рекламни сървъри от Peter Lowe\n- EasyPrivacy\n- Вредоносни домейни\n\nAко желаете, на разположение са допълнителни списъци, които да изберете:\n\n- разширен проследяващ списък от Fanboy\n- хост файл от Dan Pollock\n- рекламни и проследяващи сървъри от hpHosts\n- MVPS HOSTS\n- Spam404\n- и много други\n\nРазбира се, колкото повече списъци включите, толкова по-голямо е използването на паметта. Въпреки това, дори и след добавяне на двата допълнителни списъка от Fanboy, рекламните и проследяващи сървъри от hpHosts, uBlock₀ използва по-малко памет в сравнение с други много популярни блокери.\n\nСъщо така, имайте предвид, че избирането на някои от допълнителните списъци може да доведе до по-голяма вероятност от неправилно функциониране на уебсайтове -- особено тези списъци, които по принцип се използват като хост файлове.\n\n***\n\nБез предварително зададените списъци с филтри, това разширение е нищо. Така че, ако някога наистина искате да допринесете с нещо, помислете за хората, работещи усилено по поддържането на списъците с филтри, предоставени ви за безплатно използване от всички.\n\n***\n\nБезплатно.\nОтворен код с публичен лиценз (GPLv3)\nЗа потребители от потребителите.\n\nСътрудници @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nСътрудници @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nТова е доста ранна версия, имайте го предвид, когато я разглеждате.\n\nСписък с промени на проекта:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "bn-BD": "একটি দক্ষ প্রতিরোধক: মেমরি ও CPU-র পদচিহ্নের জন্য সহজ, এবং এখনো অন্যান্য জনপ্রিয় ব্লকার বা অবরোধকারীর থেকে হাজার হাজার অধিক ফিল্টারকে লোড এবং জোরদার করতে পারে।\n\nএটির কার্যকারিতার সচিত্র সংক্ষিপ্ত বিবরণ: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nব্যবহার: পপআপে বড় পাওয়ার বোতাম স্থায়ীভাবে বর্তমান ওয়েব সাইটের জন্য uBlock সক্রিয়/নিষ্ক্রিয় করবে। এটা শুধুমাত্র বর্তমান ওয়েব সাইটে প্রযোজ্য, এটি একটি বৈশ্বিক পাওয়ার বোতাম নয়।\n\n***\n\nমনীয়, এটি একটি \"ad blocker\"-এর ছেয়েও অধিক: এছাড়াও এটি হোস্ট ফাইল থেকে ফিল্টার পড়তে ও তৈরি করতে পারে।\n\nবাক্সের বাইরের, এই তালিকার ফিল্টারগুলি লোড করে এবং তা প্রয়োগ করে:\n\n- সহজ তালিকা\n- পিটার লো'য়ের বিজ্ঞাপন সার্ভারের তালিকা\n- সহজ গোপনীয়তা\n- ম্যালওয়্যার ডোমেইন\n\n আপনি যদি চান আপনি নির্বাচন করার জন্য আরো তালিকা পাবেন:\n\n- ফানবয়ের উন্নত ট্র্যাকিং তালিকা\n- Dan Pollock-এর হোস্ট ফাইল\n- hpHosts-এর বিজ্ঞাপন এবং ট্র্যাকিং সার্ভার\n- MVPS হোস্টসমূহ\n- স্প্যাম৪০৪\n- এবং আরও অনেক কিছু\n\nঅবশ্যই, যতবেশি ফিল্টার সক্রিয় করবেন, মেমরি পদচিহ্ন ততবেশি হবে। এমনকি Fanboy-এর দুটি অতিরিক্ত তালিকা, hpHosts-এর বিজ্ঞাপন এবং ট্র্যাকিং সার্ভার যোগ করার পরেও uব্লক অন্যান্য খুব জনপ্রিয় ব্লকারের থেকে কম মেমরি পদচিহ্ন ব্যবহার করে।\n\nএছাড়াও, এই অতিরিক্ত তালিকার কিছু নির্বাচন ওয়েব সাইট ভাঙ্গনের জন্য উচ্চ সম্ভাবনাময় হয়ে উঠতে পারে তাই সাবধান --- বিশেষকরে এই তালিকাগুলি যা সাধারণত হোস্ট ফাইল হিসেবে ব্যবহার করা হয়।\n\n***\n\nফিল্টারের পূর্বনির্ধারিত তালিকা ছাড়া, এই এক্সটেনশনটি কিছুই নয়। তাই কখনও যদি আপনি সত্যিই কিছু অবদান রাখতে চান, আপনার ব্যবহার করা ফিল্টার তালিকা রক্ষণাবেক্ষণের জন্য কঠোর পরিশ্রম করা সেই সব মানুষের করা কথা চিন্তা করুন যারা এই সব বিনামূল্যে ব্যবহারের জন্য উপলব্ধ করেছেন।\n\n***\n\nবিনামূল্যে।\nপাবলিক লাইসেন্সসহ মুক্ত উৎসের (GPLv3)\nব্যবহারকারীদের দ্বারা ব্যবহারকারীদের জন্য।\n\nঅবদানকারীগণ @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nঅবদানকারীগণ @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nএটি একটি প্রাথমিক সংস্করণ, আপনার পর্যালোচনার সময় তা মনে রাখুন।\n\nপ্রকল্পের পরিবর্তন লগ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ca": "Un bloquejador eficient: el consum de memòria i de processador és baix però, no obstant això, pot carregar i aplicar milers de filtres més que altres bloquejadors coneguts.\n\nGràfic de l'eficiència: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nÚs: El gran botó d'engegada de la finestra emergent serveix per a desactivar/activar permanentment el uBlock per al lloc web actual. No és un botó d'engegada general de l'extensió.\n\n***\n\nFlexible, és més que un \"bloquejador d'anuncis\": també pot llegir i crear filtres a partir de fitxers hosts.\n\nPer defecte, es carreguen i s'apliquen aquestes llistes de filtres:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Dominis de malware\n\nSi voleu, podeu seleccionar altres llistes disponibles:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- I altres\n\nÒbviament, com més filtres activeu, més gran serà el consum de memòria. Però fins i tot després d'afegir dues llistes extra de Fanboy, hpHosts’s Ad and tracking servers, el uBlock₀ encara té un consum de memòria inferior al d'altres bloquejadors coneguts.\n\nTambé heu de ser conscient que seleccionant algunes d'aquestes llistes extra és més probable trobar-se amb llocs webs inservibles -- especialment aquelles llistes que s'utilitzen normalment com a fitxer de hosts.\n\n***\n\nSense les llistes predefinides de filtres, aquesta extensió no és res. Així que, si en cap moment voleu fer una aportació, penseu en les persones que treballen durament per a mantenir les llistes de filtres que utilitzeu, a disposició de tothom de manera gratuïta.\n\n***\n\nLliure.\nCodi obert amb llicència pública (GPLv3)\nPer usuaris per a usuaris.\n\nCol·laboradors a Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nCol·laboradors a Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nAquesta és, en certa manera, una versió primitiva. Tingueu-ho en compte quan en doneu la vostra opinió.\n\nRegistre de canvis del projecte:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "cs": "Efektivní blokovač: nezanechává velké stopy, nezatěžuje paměť a CPU, a přesto může načítat a využívat o několik tisíc filtrů více, než jiné populární blockery.\n\nGrafický přehled jeho účinnosti: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPoužití: Velký vypínač ve vyskakovacím okně trvale povolí/zakáže uBlock pro otevřenou stránku. Funguje pouze pro aktivní webovou stránku, není to obecný vypínač.\n\n***\n\nFlexibilní, více než jen \"blokovač reklam\": umí také číst a vytvářet filtry z hosts souborů.\n\nPo instalaci jsou načteny a použity tyto filtry:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPokud chcete, můžete si vybrat tyto další filtry:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- A mnoho dalších\n\nČím více filtrů je povoleno, tím je samozřejmě větší stopa v paměti. I přesto má ale uBlock₀ i po přidání dvou dalších seznamů od Fanboye a \"hpHosts’s Ad and tracking servers\" menší vliv na paměť než mnohé další velmi populární blockery.\n\nDále mějte na paměti, že vybírání více filtrů zvyšuje šanci chybného zobrazení webů -- především u seznamů, které se normálně používají jako hosts soubory.\n\n***\n\nBez předvolených seznamů filtrů by toto rozšíření bylo k ničemu. Pokud tedy opravdu budete chtít něčím přispět, myslete na lidi, kteří spravují Vámi používané seznamy filtrů a uvolňují je pro všechny zdarma.\n\n***\n\nSvobodný software.\nOpen source s veřejnou licencí (GPLv3)\nOd uživatelů pro uživatele.\n\nPřispěvatelé na Githubu: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPřispěvatelé na Crowdinu: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nJde o poměrně ranou verzi, mějte to na paměti při recenzování.\n\nChange log projektu:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "da": "En effektiv blocker: let på hukommelse og CPU forbrug,. Kan indlæse og anvende tusindvis af flere filtre end andre populære blockere derude.\n\nIllustreret oversigt over effektiviteten: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-compared\n\nAnvendelse: Den Store power knap i pop-up-vinduet kan permanent deaktivere/aktivere uBlock på det aktuelle websted. Dette gælder kun for det aktuelle websted, det er ikke en global afbryderknap.\n\n***\n\nFleksibel, det er mere end en \"ad blocker\": den kan også læse og oprette filtre fra hosts-filer.\n\nFra starten af er disse lister over filtre indlæst og anvendt:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nFlere lister er tilgængelige hvis du ønsker det:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Osv.\n\nSelvfølgelig vil flere aktive filtre betyde højere hukommelsesforbrug. Selv efter tilføjelse af Fanboys to ekstra lister, og hpHosts’s Ad and tracking server, har uBlock₀ stadig et lavere hukommelsesforbrug end andre blockere derude.\n\nVær desuden opmærksom på, at hvis du vælger nogle af disse ekstra lister kan det føre til højere sandsynlighed for, at webstedet bliver vist forkert - især de lister der normalt anvendes som hosts-fil.\n\n***\n\nUden de forudindstillede lister med filtre er denne udvidelse intet. Hvis du nogensinde virkelig ønsker at bidrage med noget, så tænk på de mennesker der arbejder hårdt for at vedligeholde de filterlister du bruger, som alle blev stillet gratis til rådighed for alle.\n\n***\n\nGratis.\nOpen source med offentlig licens (GPLv3)\nFor brugere, af brugere.\n\nBidragydere @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nBidragydere @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDette er en tidlig version. Hav dette i tankerne når du skriver en anmeldelse.\n\nProjekt changelog:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "de": "Ein effizienter Blocker: Geringer Speicherbedarf und niedrige CPU-Belastung - und dennoch werden Tausende an Filtern mehr angewendet als bei anderen populären Blockern.\n\nEin illustrierter Überblick über seine Effizienz: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nBenutzung: Der An-/Ausschaltknopf beim Klicken des Erweiterungssymbols dient zum An-/Ausschalten von uBlock auf der aktuellen Webseite. Dies wirkt sich also nur auf die aktuelle Webseite aus und nicht global.\n\n***\n\nuBlock ist flexibel, denn es ist mehr als ein \"Werbeblocker\": Es verarbeitet auch Filter aus mehreren hosts-Dateien.\n\nStandardmäßig werden folgende Filterlisten geladen und angewandt:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nAuf Wunsch können zusätzliche Listen ausgewählt werden:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- etc.\n\nNatürlich ist der Speicherbedarf umso höher, desto mehr Filter angewandt werden. Aber selbst mit den zwei zusätzlichen Listen von Fanboy und hpHosts’s Ad and tracking servers ist der Speicherbedarf von uBlock₀ geringer als bei anderen sehr populären Blockern.\n\nBedenke allerdings, dass durch die Wahl zusätzlicher Listen die Wahrscheinlichkeit größer wird, dass bestimmte Webseiten nicht richtig geladen werden - vor allem bei Listen, die normalerweise als hosts-Dateien verwendet werden.\n\n***\n\nOhne die vorgegebenen Filterlisten ist diese Erweiterung nichts. Wenn du also etwas beitragen möchtest, dann denke an die Menschen, die hart dafür arbeiten, die von dir benutzten Filterlisten zu pflegen, und diese für uns alle frei verfügbar gemacht haben.\n\n***\n\nKostenlos.\nOpen source mit Public License (GPLv3)\nFür Benutzer von Benutzern.\n\nMitwirkende @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nMitwirkende @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDies ist eine ziemlich frühe Version - bitte denke daran, wenn du sie bewertest.\n\nChange log des Projekts:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "el": "Ένας αποτελεσματικός αναστολέας διαφημίσεων: παρόλο το ελαφρύ του αποτύπωμα στη μνήμη και τον επεξεργαστή μπορεί να εφαρμόσει χιλιάδες περισσότερα φίλτρα σε σχέση με άλλους δημοφιλείς blockers.\n\nΑπεικονιζόμενη επισκόπηση της αποτελεσματικότητάς του: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nΧρήση: Το μεγάλο πλήκτρο απενεργοποίησης/ενεργοποίησης στο αναδυόμενο παράθυρο, χρησιμεύει στην εναλλαγή κατάστασης του uBlock για τον τρέχοντα ιστότοπο. Η εφαρμογή της ρύθμισης αυτής γίνεται μόνο για τον τρέχοντα ιστότοπο και δεν επιβάλλεται καθολικά.\n\n***\n\nΕυέλικτος, είναι πολλά περισσότερα από ένας απλός \"ad blocker\": μπορεί επιπλέον να διαβάζει και να δημιουργεί φίλτρα από αρχεία hosts.\n\nΚατά προεπιλογή, οι λίστες φίλτρων που φορτώνονται και επιβάλλονται είναι οι εξής:\n\n- EasyList\n- Λίστα διακομιστών διαφημίσεων του Peter Lowe\n- EasyPrivacy\n- Κακόβουλοι τομείς\n\nΕπιπλέον λίστες είναι διαθέσιμες για να επιλέξετε εάν το επιθυμείτε:\n\n- Ενισχυμένη Ιχνωσική Λίστα του Fanboy\n- Αρχείο hosts του Dan Pollock\n- Διαφημίσεις και διακομιστές ίχνωσης hpHosts\n- MVPS HOSTS\n- Spam404\n- και πολλές άλλες\n\nΦυσικά, όσο περισσότερα φίλτρα ενεργοποιούνται, τόσο αυξάνεται το αποτύπωμα της μνήμης. Ωστόσο, ακόμη και μετά από την προσθήκη δυο επιπλέον λιστών, του Fanboy και της λίστας διαφημίσεων και διακομιστών ίχνωσης hpHosts, το uBlock₀ συνεχίζει να έχει χαμηλότερο αποτύπωμα μνήμης από άλλους δημοφιλείς αναστολείς.\n\nΕπίσης, έχετε υπ'όψην ότι διαλέγοντας μερικές από τις έξτρα λίστες μπορεί να οδηγήσει σε πιθανό σφάλμα στην ιστοσελίδα -- ειδικά εκείνες που κανονικά χρησιμοποιούνται σαν host αρχεία.\n\n***\n\nΧωρίς τις υπάρχουσες λίστες φίλτρων, αυτή η επέκταση δεν έχει καμία αξία. Εάν ποτέ λοιπόν θελήσετε πραγματικά να συνεισφέρετε κάτι, αναλογιστείτε τους ανθρώπους που εργάζονται σκληρά για να διατηρήσουν τις λίστες φίλτρων που χρησιμοποιείτε, οι οποίες διατέθηκαν προς χρήση σε όλους, δωρεάν.\n\n***\n\nΔωρεάν.\nΑνοιχτού κώδικα με άδεια δημόσιας χρήσης (GPLv3)\nΑπό τους χρήστες για τους χρήστες.\n\nΣυνεισφέροντες @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nΣυνεισφέροντες @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nΕίναι μια αρκετά πρόωρη έκδοση, κρατήστε το υπόψη κατά την αξιολόγηση.\n\nΑρχείο αλλαγών του έργου:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "en-US": "uBlock Origin is <b>not</b> an \"ad blocker\", it's a wide-spectrum content blocker with CPU and memory efficiency as a primary feature.\n\n***\n\nOut of the box, uBO blocks ads, trackers, coin miners, popups, etc. through the following lists of filters, enabled by default:\n\n- EasyList (ads)\n- EasyPrivacy (tracking)\n- Peter Lowe’s Ad server list (ads and tracking)\n- Online Malicious URL Blocklist\n- uBO's own lists\n\nMore lists are available for you to select if you wish:\n\n- EasyList Cookie\n- Fanboy Annoyances\n- AdGuard Annoyances\n- Dan Pollock’s hosts file\n- And many others\n\nAdditionally, you can point-and-click to block JavaScript locally or globally, create your own global or local rules to override entries from filter lists, and many more advanced features.\n\n***\n\nFree.\nOpen source with public license (GPLv3)\nFor users by users.\n\nIf ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\n<ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/788d66e7299bdfb1da05832994551640d0ad441e148a3e29afe8dd0a5a90800c/https%3A//github.com/gorhill/uBlock%23ublock-origin\" rel=\"nofollow\">Documentation</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">Release notes</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/32c3d6819f5263e56c265042e8d34e2da4d974e73a7ad55a81786d8995cf65a9/https%3A//www.reddit.com/r/uBlockOrigin/\" rel=\"nofollow\">Community support @ Reddit</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">Contributors @ GitHub</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">Contributors @ Crowdin</a></li></ul>",
+ "es": "Un bloqueador eficiente: capaz de cargar y aplicar miles más de filtros en comparación con otros populares bloqueadores, manteniendo un mínimo consumo de memoria y CPU.\n\nEjemplo con imágenes ilustrando su eficiencia (en inglés): <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUso: El botón grande de apagado/encendido en la ventana emergente de la extensión, es para deshabilitar/habilitar uBlock₀ permanentemente en el sitio web actual. Aplica solo al sitio web actual, no activa o desactiva la extensión de forma general.\n\n***\n\nFlexible, es más que un \"bloqueador de anuncios\": también puede leer y crear filtros desde archivos hosts.\n\nPor defecto ya trae configuradas las siguientes listas de filtros:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nOtras listas disponibles pueden ser seleccionadas, si se desea:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Y muchas más\n\nPor supuesto, mientras más filtros se activen, mayor será el consumo de memoria. No obstante, incluso después de agregar las dos listas adicionales de \"Fanboy's\" y la \"hpHosts’s Ad and tracking servers\", uBlock₀ consume menos memoria que otros bloqueadores similares.\n\nTambién tenga en cuenta que seleccionar algunas de estas listas adicionales puede conducir a una mayor probabilidad de aparición de problemas al mostrar un sitio web -- especialmente las listas utilizadas normalmente como archivo hosts.\n\n***\n\nSin las listas preestablecidas de filtros, esta extensión no sería nada. Así que si alguna vez realmente quieres aportar algo, piensa en las personas que trabajan duro para mantener estas listas de filtros, disponibles de forma gratuita para todos.\n\n***\n\nLibre.\nCódigo abierto con licencia pública (GPLv3)\nHecho para usuarios por los usuarios.\n\nColaboradores @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nColaboradores @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7bae4395c4e5926bb237c1ef9b0f391cb005dbdbf58f4c9e47298db9bb6d1f57/https%3A//crowdin.com/project/ublock\" rel=\"nofollow\">https://crowdin.com/project/ublock</a>\n\n***\n\nRegistro de cambios del proyecto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "eu": "Blokeatzaile eraginkor bat: Memoria eta PUZ erabileran arina da, eta hala ere beste blokeatzaile ezagun batzuk baino milaka iragazki gehiago kargatu eta ezarri ditzake.\n\nBere eraginkortasunaren adibide grafikoa: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nErabilera: Laster-leihoko pizte botoi handia uBlock uneko gunean behin betiko gaitu edo desgaitzeko da. Uneko guneari dagokio soilik, ez da botoi orokor bat.\n\n***\n\nMalgua, iragarki blokeatzaile bat baino gehiago da, ostalarietako iragazkiak sortu eta irakurri ditzake ere.\n\nLehenetsita, honako iragazki zerrendak kargatu eta ezartzen ditu:\n\n- EasyList\n- Peter Loweren iragarki zerbitzarien zerrenda\n- EasyPrivacy\n- Malware domeinuak\n\nZerrenda gehiago dituzu eskura hautatzeko hala nahiez gero:\n\n- Fanboyren hobetutako jarraipen zerrenda\n- Dan Pollocken ostalari zerrenda\n- hpHostsen iragarki eta jarraipen zerbitzariak\n- MVPS Ostalariak\n- Spam404\n- Eta beste hainbat gehiago\n\nJakina, iragazki gehiago kargatuta memoria erabilera handiagoa da. Hala ere, Fanboyren bi zerrenda gehigarriak eta hpHostsen iragarki eta jarraipen zerbitzariak kargatuta, uBlockek beste blokeatzaile ezagun batzuk baino memoria gutxiago erabiltzen du.\n\nBestalde, kontuan izan zerrenda gehigarri hauetako batzuk gaitzeak guneren bat hausteko aukerak handitzen dituela, batez ere ostalari fitxategi gisa erabili ohi diren zerrendak.\n\n***\n\nLehenetsitako iragazki zerrendarik gabe gehigarri honek ez du ezer egiten. Beraz ezertan lagundu nahi baduzu pentsa ezazu erabiltzen dituzun iragazki zerrendak egunean mantentzeko tinko lanean dabiltzan horietan, guztiek erabiltzeko moduan doan eskuragarri jarri dituztenak.\n\n***\n\nDoan.\nLizentzia libreduna (GPLv3)\nErabiltzaileek erabiltzaileentzat sortua.\n\nParte-hartzaileak @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nParte-hartzaileak @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nNahiko berria da bertsio hau, kontua izan honi buruz idaztean.\n\nProiektuaren aldaketa egunkaria:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fa": "یک بلاکر موثر: نیاز به پردازش حافظه و سی پی یو کمتر و در عین حال اجرای هزاران فیلتر بیشتر از سایر رقبای بلاکر موجود.\n\nبررسی تصویری از کارایی این محصول: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nکاربرد: دکمه ی پاور بزرگ در پنجره برای فعال یا غیر فعال کردن uBlock برای صفحه ی جاری است. فقط برای همین سایت اعمال میشود، دکمه ی پاوری برای تمام سایت ها نیست.\n\n***\n\nانعطاف پذیری آن بیشتر از \"ad blocker\" است: همچنین می تواند فیلتر ها را از هاست میزبان، بخواند و بسازد.\n\nبیرون از جعبه، این لیست فیلترها بارگذاری و اجرا میشوند:\n\n- EasyList\n- لیست سرور تبلیغاتی Peter Lowe\n- EasyPrivacy\n-دامنه های تروجان\n\nاگر میخواهید لیست های بیشتر برای انتخاب شما در دسترس هستند:\n\n- ردیابی لیست پیشرفته ی Fanboy\n- میزبانی فایل Dan Pollock\n- تبلیغ و ردیابی سرور hpHosts\n- هاست های MVPS\n- اسپم 404\n- و بسیاری دیگر\n\nالبته هرچه فیلترهای بیشتری فعال باشند، حافظه ی بیشتری اشغال خواهد شد. با اینحال، حتی پس از اضافه کردن دو لیست اضافی Fanboy و سرور های ردیابی و تبلیغ hpHosts ، میبینیم که uBlock هنوز حافظه پایین تری از دیگر برنامه های مشابه اشغال میکند.\n\nهمچنین، بدانید که انتخاب برخی از این لیست ها ممکن است افزایش احتمال شکستگی وب سایت--به ویژه آنهایی که به طور معمول به عنوان میزبان فایل شناخته میشوند را در پی داشته باشد.\n\n***\n\nبدون فهرست از پیش تعیین شده ی فیلتر، این افزونه هیچ است. پس اگر واقعا می خواهید کمکی کرده باشید، به افرادی فکر کنید که برای حفظ لیست فیلتر مورد استفاده شما سخت کار میکنند که برای استفاده همه به رایگان در دسترس باشد.\n\n***\n\nرایگان.\nمتن باز با مجوز عمومی (GPLv3)\nبرای کاربران توسط کاربران.\n\nمشارکت کنندگان در گیت هاب: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nمشارکت کنندگان در کرادین <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nاین کاملا یک نسخه اولیه است، هنگام بررسی اینرا بخاطر داشته باشید.\n\nتغییرات اخیر پروژه:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fi": "Tehokas mainosten estäjä – käyttää vähän resursseja, mutta silti voit ladata ja pakottaa tuhansia suodatinsääntöjä enemmän kuin muut suositut mainoksia estävät lisäosat.\n\nKuvitettu yleiskatsaus uBlockin tehokkuudesta (englanniksi): <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nKäyttö: Iso virtanappi ponnahdusikkunassa on tarkoitettu pysyvästi estämään/sallimaan uBlock kyseisellä sivulla. Tämä koskee vain nykyistä sivua, ei kaikkia sivuja.\n\n***\n\nJoustava, tämä lisäosa on enemmän kuin perinteinen \"mainosten estäjä\". Voit lukea ja luoda suodattimia myös hosts-tiedostoista.\n\nNämä suodatinlistat ovat automaattisesti ladattuna ja kytketty päälle:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nHalutessasi voit valita käyttöösi lisää listoja:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Ja monia muita\n\nJos sinulla on useita listoja käytössä, keskusmuistia kuluu enemmän. Tästä huolimatta, vaikka lisäisit Fanboyn kaksi lisälistaa ja hpHostsin listan, uBlockilla on silti pienempi muistinkulutus kuin muilla suosituilla mainosten estäjillä.\n\nUseiden listojen lisääminen saattaa aiheuttaa sivujen kaatumisen tai hajoamisen. Etenkin listat, joita käytetään normaalisti hosts-tiedostona, voivat aiheuttaa ongelmia.\n\n***\n\nTämä lisäosa ei tee mitään ilman suodatinlistoja. Jos siis haluat osallistua jotenkin, muistathan kaikki ne ihmiset jotka työskentelevät pitääkseen käyttämäsi suodatinlistat ajan tasalla ja saatavilla ilmaiseksi.\n\n***\n\nIlmainen.\nAvoimen lähdekoodin julkinen lisenssi (GPLv3)\nKäyttäjiltä käyttäjille.\n\nKehittäjät @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKehittäjät @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nOtathan huomioon testatessasi, että käytössäsi on varsin varhainen versio.\n\nProjektin muutosloki:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fr": "uBlock est une extension qui bloque les publicités et les pisteurs, légère en empreinte mémoire et en utilisation du processeur et qui pourtant, est capable d'utiliser et de traiter des milliers de filtres de plus que la plupart des autres bloqueurs.\n\nConsultez cette page en Anglais pour avoir une vue d'ensemble illustrée de son efficacité : <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilisation : Le gros bouton power dans la fenêtre pop-up permet de désactiver/activer en permanence uBlock pour le site Web en cours de consultation. Cela s'applique uniquement au site Web actuel, ce n'est pas un bouton power qui affecte entièrement le fonctionnement de l'extension.\n\n***\n\nFlexible, uBlock ne prend pas en charge que les filtres de type Adblock, elle peut également lire et créer des filtres depuis des fichiers hosts.\n\nPar défaut, ces listes de filtrage sont chargées et traitées :\n\n- EasyList (Liste anti-publicités maintenue fréquemment à jour par la communauté)\n- Peter Lowe’s Ad server list (Liste de serveurs publicitaires maintenue à jour par Peter Lowe)\n- EasyPrivacy (Liste anti-pisteurs maintenue fréquemment à jour par la communauté)\n- Malware domains (Liste de protection contre des domaines malveillants)\n\nDavantage de listes sont disponibles si vous souhaitez renforcer le blocage :\n\n- Fanboy’s Enhanced Tracking List (Liste de protection avancée contre le pistage maintenue à jour par Fanboy)\n- Dan Pollock’s hosts file (Fichier Hosts bloquant publicités, domaines malveillants et autres pisteurs, maintenue fréquemment à jour par Dan Pollock)\n- hpHosts’s Ad and tracking servers (Fichier Hosts bloquant des serveurs publicitaires et des serveurs pistant, maintenue à jour par hpHosts)\n- MVPS HOSTS (Fichier Hosts bloquant publicités, domaines malveillants et autres pisteurs, maintenue à jour par MVPS)\n- Spam404 (Liste de protection contre les spams, maintenue fréquemment à jour par la communauté)\n- Et plein d'autres\n\nBien évidemment, plus vous activez de filtres, plus l'empreinte mémoire augmentera. Pourtant, même après avoir ajouté deux listes supplémentaires crées par Fanboy et le fichier Hosts d'hpHosts, uBlock₀ utilise moins de mémoire vive que tous les autres bloqueurs de pubs populaires.\n\nVeuillez tout de même prendre en compte que le fait de choisir parmi ces listes supplémentaires peut conduire à quelques incompatibilités sur les sites Web que vous visitez, bien que ces listes soient maintenues à jour par leurs auteurs.\n\n***\n\nSans les listes prédéfinies de filtres, cette extension (comme d'autres) ne serait rien. Alors si vous tenez vraiment à contribuer d'une quelconque manière, pensez aux personnes travaillant dur pour maintenir à jour ces listes de filtres que vous utilisez, qui plus est proposées gratuitement à tout le monde.\n\n***\n\nGRATUIT.\nSource libre avec une licence publique GPLv3\nFait par des utilisateurs pour des utilisateurs.\n\nContributeurs @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContributeurs @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nIl s'agit d'une version relativement préliminaire, veuillez garder ça à l'esprit lors de votre évaluation de l'extension.\n\nConsultez ici en Anglais le Journal des changements concernant le projet :\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "he": "חוסם יעיל: חותמת נמוכה של המעבד והזיכרון, ועדיין יכול לטעון ולאפשר אלפי מסננים יותר מאשר חוסמים פופולריים אחרים.\n\nסקירה כוללת על היעילות שלו: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nשימוש: לחצן ההפעלה הגדול בחלון הפופאפ הוא בשביל לבטל/להפעיל את uBlock עבור האתר הנוכחי. הוא חל על האתר הנוכחי בלבד, זהו לא לחצן הפעלה גלובלי.\n\n***\n\nגמיש, יותר מ \"חוסם פרסומות\": הוא יכול גם לקרוא וליצור מסננים מקבצי hosts.\n\nהיישר מהקופסה, רשימות המסננים הללו נטענות ומאופשרות:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nרשימות נוספות אלו זמינות לבחירתך אם תרצה:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- ועוד רבים אחרים\n\nכמובן שככל שכמות מסננים גדולה יותר מופעלת, ככה גם חתימת הזיכרון גדולה יותר. ובכל זאת, אפילו לאחר הוספת שתי הרשימות הנוספות של Fanboy ו hpHosts’s Ad and tracking servers, ל uBlock עדיין יש חתימת זיכרון נמוכה יותר מלחוסמים פופולריים אחרים שם בחוץ.\n\nכמו כן, תהיה מודע שבחירה של חלק מהרשימות הנוספות הללו עלולה להוביל בסבירות גבוהה לשבירה של אתרי אינטרנט -- במיוחד הרשימות אשר בדרך כלל משומשות כקובץ hosts.\n\n***\n\nללא רשימות מסננים מוגדרים מראש, תוסף זה לא שווה כלום. אז אם אי פעם תרצה באמת לתרום משהו, תחשוב על האנשים שעובדים לילות כימים כדי לתחזק את רשימות המסננים שאתה משתמש בהן, אשר הובאו לשימוש על ידי כולם ללא כל תשלום.\n\n***\n\nחינם.\nקוד פתוח עם רשיון ציבורי (GPLv3)\nבשביל המשתמשים על ידי המשתמשים.\n\nתורמים @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nתורמים @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nקח בחשבון שזוהי גרסה מוקדמת בזמן הסקירה שלך.\n\nרשימת השינויים של הפרויקט:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "hu": "Egy hatékony blokkoló: kíméletes a processzorral és a memóriával, mégis képes nagyságrendekkel több szűrő betöltésére és alkalmazására a többi népszerű blokkolóhoz viszonyítva.\n\nÁttekintés a hatékonyságáról: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nHasználat: A nagy bekapcsológomb a kiegészítő tiltására/engedélyezésére szolgál a jelenlegi webhelyen. A gomb kizárólag a jelenlegi webhelyre érvényes, nem egy globális kapcsoló.\n\n***\n\nTöbb mint egy egyszerű reklámblokkoló: képes hosts fájlok bejegyzéseit is értelmezni, és azokból szűrőket létrehozni.\n\nAlapértelmezetten a következő szűrőlisták kerülnek betöltésre és alkalmazásra:\n\n- EasyList\n- Peter Lowe hirdetési szerverlistája\n- EasyPrivacy\n- Kártékony domainek\n\nEgyéb listák is kiválaszthatók a felhasználó igénye szerint:\n\n- Fanboy bővített követők listája\n- Dan Pollock hosts fájlja\n- hpHosts hirdetés és követőszerverek listája\n- MVPS HOSTS\n- Spam404\n- És sok más\n\nTermészetesen, több szűrő használatával a memóriaigény is növekszik. Ennek ellenére Fanboy két extra listája és a hpHosts (reklám és követőszerverek) lista hozzáadásával a uBlock memóriafogyasztása még mindig alacsonyabb, mint a legnépszerűbb blokkolóké.\n\nEmellett, néhány extra lista kiválasztásával megnövekszik az esély arra, hogy a webhelyek használhatatlanná válnak -- főleg azon listákról van szó, melyek normál esetben hosts fájlként használatosak.\n\n***\n\nA szűrőlisták nélkül a kiegészítő nem sokat érne. Tehát, ha valaha is eszedbe jutna támogatást kínálni, akkor előbb gondolj azokra, akik keményen dolgoznak a listák karbantartásával, illetve ingyenesen hozzáférhetővé teszik azokat mindenki számára.\n\n***\n\nIngyenes.\nNyílt forráskódú nyilvános licenccel (GPLv3)\nFelhasználóknak felhasználóktól.\n\nKözreműködők a Github-on: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKözreműködők a Crowdin-en: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEz még egy elég korai verzió, amit illik szem előtt tartani értékeléskor.\n\nVáltozások listája:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "id": "Pemblokir yang efisien: ringan penggunaan memori dan CPU, namun dapat memuat dan menjalankan ribuan filter lain dibanding pemblokir populer lain di luar sana.\n\nRingkasan ilustrasi efisiensi: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-Compared\n\nPenggunaan: Tombol daya yang besar dalam popup berfungsi menonaktifkan/mengaktifkan uBlock secara permanen untuk situs yang sedang dibuka. Berlaku untuk situs yang sedang dibuka saja, bukan tombol daya global.\n\n***\n\nFleksibel, lebih dari sekedar \"pemblokir iklan\": juga dapat membaca dan membuat filter dari berkas host.\n\nLangsung bekerja, daftar filter berikut ini dimuat dan dijalankan:\n\n- EasyList\n- Daftar server iklan Peter Lowe\n- EasyPrivacy\n- Domain malware\n\nJika anda ingin, masih banyak tersedia daftar lain untuk anda pilih:\n\n- Daftar Pelacakan Fanboy yang DItingkatkan\n- Berkas host Dan Pollock\n- Server iklan dan pelacakan hpHosts\n- HOST MVPS\n- Spam404\n- dan banyak lainnya\n\nTentu saja, semakin banyak filter yang diaktifkan, semakin besar penggunaan memori. Namun, bahkan setelah menambahkan 2 daftar ekstra Fanboy, server iklan dan pelacakan hpHosts, penggunaan memori uBlock masih lebih kecil dibanding pemblokir iklan populer lain di luar sana.\n\nPerlu diketahui juga bahwa memilih beberapa daftar ekstra juga berpeluang lebih tinggi menyebabkan kerusakan situs -- terutama daftar yang biasanya digunakan sebagai berkas host.\n\n***\n\nTanpa daftar filter yang ada, ekstensi ini bukanlah apa-apa. Jadi, jika Anda benar-benar ingin berkontribusi sesuatu, berpikirlah tentang orang-orang yang bekerja keras mengelola daftar filter yang anda gunakan, yang dibuat dan tersedia untuk digunakan oleh semua dengan gratis.\n\n***\n\nGratis.\nSumber terbuka dengan lisensi publik (GPLv3)\nUntuk pengguna oleh pengguna.\n\nKontributor @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKontributor @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nMasih dalam versi yang sangat awal, mohon diingat ketika anda membuat ulasan.\n\nCatatan perubahan proyek:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "it": "uBlock è un efficiente ad-blocker: occupa poca memoria e poca CPU, ma può usare migliaia di filtri in più rispetto ad altri software simili.\n\nConsulta questa pagina (in inglese) per verificare la sua efficacia <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUso: il pulsante power nel popup serve per disabilitare/abilitare permanentemente uBlock nel sito che stai visitando. e non serve per disabilitare/abilitare l'estensione.\n\n***\n\nMolto più che un ad-blocker: può anche creare filtri dal file host.\n\nPer default sono attivate queste liste:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPuoi anche attivare moltre altre liste:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Ecc.\n\nOvviamente, più liste attivi, maggiore sarà l'impatto sulla memoria. Anche aggiungendo altre due liste di Fanboy, ad di hpHosts e tracking server, uBlock userà meno memoria di molti altri ad-blocker.\n\nSelezionando alcuni di questi filtri può portare ad una maggiore probabilità di problemi nel visualizzare alcuni siti web.\n\n***\n\nSenza queste liste di filtri, questa estensione non è niente. osì se vuoi contribuire, pensa alle persone che lavorano duramente per mantenere queste liste che stai usando, che sono disponibili gratuitamente.\n\n***\n\nGratuito.\nOpen source with public license (GPLv3)\nFatto dagli utenti per gli utenti.\n\nCollaboratori @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nCollaboratori @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nQuesta è una versione preliminare, ricordalo quando scriverai una recensione.\n\nPer leggere le novità di ogni versione consulta questa pagina (In Inlgese):\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ja": "効率的なブロッカー:コンピュータのメモリとCPUのフットプリントはより少なく\n、別の人気のブロッカーよりも何千ものフィルタをロードし、強制的にブロックができます\n\n他ソフトとの比較は以下のとおり: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n使用法: ポップアップに表示される大きな電源ボタンは、現在のサイトでuBlockの有効/無効を切り替えます。 現在のサイトのみに適用されます、グローバルボタンではありません。\n\n***\n\nただの「広告ブロッカー」より柔軟です:ホストファイルを読み込みフィルターを作成できます。\n\n要するに、以下のフィルターが読み込まれ、適用されます:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nご希望であればさらに多くのリストがご利用できます:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Etc.\n\nもちろん、多くのフィルターを適用すれば使用メモリーは増えます。 ただ、それでも、Fanboy's Special Blocking List、Fanboy's Enhanced Tracking List、hpHost's Ad and tracking serversの三つのリストを追加で適用しても、uBlockは他の人気のブロッカーより少ないメモリー消費を実現しています。\n\nそれと、多くのリストの適用は(特にホストファイルとしてよく使われているリスト)ウェブサイトの崩壊を起こしかねないことに注意してください。\n\n***\n\nこの拡張機能は、あらかじめ設定されているフィルターのリストが無ければ意味を成しません。 ですので、何かしらの形で貢献したいと考えることがあった時は、これらのリストを無料で懸命に更新し続けている方々を思い出してください。\n\n***\n\n無料.\nパブリックライセンス(GPLv3)のオープンソース\nユーザーによって作られた、ユーザーのための物。\n\n貢献者 @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n貢献者 @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nこれは割と初期のバージョンですので、それを念頭にレビューをお願いします。\n\nプロジェクト変更ログ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ka": "რეკლამების შედეგიანი შემზღუდავი: მეხსიერებისა და პროცესორის შემსუბუქებული მოხმარება, რეკლამების სხვა შემზღუდავებთან შედარებით, ათასობით მეტი ფილტრის გამოყენების პირობებშიც კი.\n\nშედეგიანობის მიმოხილვა იხილეთ ბმულზე: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nგამოყენება: ჩართვის დიდი ღილაკით, ჩამოშლილ მენიუში, შესაძლებელია uBlock-ის ჩართვა/გამორთვა მიმდინარე ვებსაიტზე. ეს ღილაკი მოქმედებს მხოლოდ არსებულ საიტზე და არ გამოიყენება ზოგადად ჩართვა/გამორთვისთვის.\n\n***\n\nმეტად მოქნილი, ეს არაა უბრალოდ „რეკლამების შემზღუდავი“: ასევე შესაძლებელია hosts ფაილის წაკითხვა და ფილტრების შექმნა.\n\nგარდა ამისა, ნაგულისხმევად ჩართულია და გამოიყენება შემდეგი გასაფილტრი სიები:\n\n- EasyList\n- Peter Lowe-ის სარეკლამო სერვერების სია\n- EasyPrivacy\n- მავნე დომენები\n\nასევე, ხელმისაწვდომია სიები სურვილისამებრ შესარჩევად:\n\n- Fanboy-ის გაუმჯობესებული წესები თვალყურისმდევნებლების შესაზღუდად\n- Dan Pollock-ის hosts ფაილი\n- hpHosts-ის სარეკლამო და თვალყურისმდევნელი სერვერები\n- MVPS HOSTS\n- Spam404\n- და კიდევ ბევრი\n\nრასაკვირველია, რაც უფრო მეტი ფილტრია ჩართული, მეხსიერების გამოყენება იზრდება. თუმცა, Fanboy-ის გაფართოებული წესების, hpHosts-ის სარეკლამო და თვალყურისმდევნელი სერვერების დამატების შემთხვევაშიც კი, uBlock მაინც ნაკლებ მეხსიერებას იყენებს, ვიდრე ყველა სხვა ცნობილი შემზღუდავი პროგრამები.\n\nამასთან, გაითვალისწინეთ, რომ ზოგიერთი დამატებითი წესების შერჩევის შედეგად, შესაძლოა ვებსაიტები არ გამოჩნდეს გამართულად -- განსაკუთრებით იმ წესების შემთხვევაში, რომელიც ჩვეულებრივ, hosts ფაილად გამოიყენება.\n\n***\n\nწინასწარ შედგენილ წესებს, მნიშვნელოვანი ადგილი უჭირავს ამ გაფართოების შედეგიან მუშაობაში. ასე რომ, თუ ოდესმე გადაწყვეტთ ვინმესთვის შემოწირულობის გაღებას, იფიქრეთ იმ ადამიანებზე, რომლებიც თავდაუზოგავად შრომობენ იმ გასაფილტრი წესების მუდმივ განახლებაზე, რომლითაც სარგებლობთ და რომელიც ხელმისაწვდომია ყველასთვის უფასოდ.\n\n***\n\nუფასო.\nღია წყაროს მქონე საჯარო ლიცენზიით (GPLv3)\nმომხმარებლების მიერ, მომხმარებლებისთვის.\n\nწვლილის შემტანები @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nწვლილის შემტანები @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nცვლილებების ჩამონათვალი:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ko": "효율적인 차단기: 메모리와 CPU에 부담이 적고, 다른 인기있는 차단기에 비해 수 천 가지의 필터를 사용할 수 있습니다.\n\n효율성에 대한 소개: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n사용 방법: 해당 웹사이트에서 팝업의 큰 전원 버튼을 눌러 uBlock을 켜고 끌 수 있습니다. 적용은 현재 웹사이트만 적용되며, 전체적으로 적용되지 않습니다.\n\n***\n\n\"AdBlocker\" 보다 더 유연합니다: 호스트 파일들로부터 필터를 만들고 볼 수 있습니다.\n\n특별한 설치 없이도 아래 목록들을 불러오고 적용할 수 있습니다:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\n당신이 원한다면 더 많은 목록을 선택할 수 있습니다:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 그리고 무수히 많은 다른 목록들\n\n물론, 더 많은 필터를 활성화하면 할수록, 메모리 사용량도 높아집니다. 하지만 Fanboy's two extra lists와 hpHosts’s Ad and tracking servers 필터를 추가한 후에도 uBlock₀은 다른 인기있는 차단기에 비해 메모리 사용량이 적습니다.\n\n또, 이러한 일부 추가 목록(특히 일반적으로 사용되는 호스트 파일) 중 선택시 높은 확률로 웹사이트가 파손될 수 있음을 명심해주시기 바랍니다.\n\n***\n\n필터에 필터 목록이 하나도 없다면, 이 확장기능은 아무 쓸모가 없어집니다. 그래서 만약 당신이 정말 어떤것으로든 기여하고 싶을때는, 당신이 사용중인 필터 리스트를 만들고 유지하기 위해 노력중인 사람들을 생각해주세요. 필터들은 모두 무료로 사용이 가능하게 되어있습니다.\n\n***\n\n완전히 무료입니다.\n오픈소스이며, 공개 라이센스(GPLv3)를 따릅니다.\n사용자를 위해, 사용자에 의해 만들어졌습니다.\n\n기여자 @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n기여자 @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n이것은 완전히 초기 버전입니다, 리뷰할 때 이 점을 명심하세요.\n\n프로젝트 변경사항:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "nl": "Een efficiënte adblocker: gebruikt weinig processorkracht en geheugen. Toch kan het duizenden filters meer laden en toepassen dan andere populaire adblockers.\n\nGeïllustreerde efficiëntievergelijking: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nGebruik: met de grote aan-uitknop in de pop-up kan uBlock voor de huidige website permanent worden in- of uitgeschakeld. Het wordt alleen op de huidige website toegepast; dit is geen algemene aan-uitknop.\n\n***\n\nFlexibel, want het is meer dan een ‘adblocker’: het kan ook filters inlezen en aanmaken vanuit hosts-bestanden.\n\nStandaard worden de volgende filterlijsten geladen en toegepast:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nEr zijn meer lijsten beschikbaar die u kunt inschakelen:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- en nog vele andere...\n\nNatuurlijk wordt het geheugengebruik groter naarmate er meer filters worden ingeschakeld. Maar zelfs na het inschakelen van Fanboy’s twee extra lijsten, hpHosts’s Ad en tracking servers, heeft uBlock een lager geheugengebruik dan andere populaire blockers.\n\nLet op, het gebruik van sommige van deze extra lijsten verhoogt de kans dat websites niet goed worden weergegeven - zeker de lijsten die normaal als hosts-bestand worden gebruikt.\n\n***\n\nZonder de standaard filterlijsten doet deze extensie niets. Als u dus ooit echt een bijdrage wilt leveren, denk dan aan de mensen die hard werken om de filterlijsten die u gebruikt te onderhouden, welke allemaal gratis beschikbaar zijn gemaakt.\n\n***\n\nVrij.\nOpen source met publieke licentie (GPLv3)\nVoor gebruikers, door gebruikers.\n\nMedewerkers @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nMedewerkers @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nOnthoud dat dit een hele vroege versie is wanneer u een beoordeling geeft.\n\nProjectwijzigingenlogboek:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pl": "Skutecznie blokuje reklamy, używa mało pamięci RAM i zasobów procesora, a przy tym może wczytywać i stosować o wiele więcej filtrów niż inne popularne rozszerzenia do blokowania reklam.\n\nIlustrowane porównanie z dodatkiem Adblock Plus: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nSposób użycia: wyświetlany w małym wyskakującym oknie przycisk służy do włączenia/wyłączenia rozszerzenia na bieżącej witrynie internetowej. Działanie przycisku ma zastosowanie tylko na bieżącej witrynie – nie działa globalnie.\n\n***\n\nElastyczny. Jest czymś więcej niż „blokerem reklam”. Może czytać i tworzyć filtry z plików hostów.\n\nPo zainstalowaniu są wczytywane i stosowane następujące listy filtrów:\n\n- EasyList\n- Lista serwerów reklam Petera Lowe'a\n- EasyPrivacy\n- Domeny ze złośliwym oprogramowaniem\n\nMożna wybrać więcej list filtrów:\n\n- Rozszerzona lista śledzenia dla fanboyów\n- Plik hostów Dana Pollocka\n- Serwery reklam i śledzenia hpHosts\n- MVPS HOSTS\n- Spam404\n- I wiele innych\n\nIm więcej filtrów jest włączonych, tym większe jest użycie pamięci RAM. Nawet po dodaniu dwóch dodatkowych list filtrów dla fanboyów – listy serwerów reklamowych i śledzących hpHosts – µBlock₀ używa mniej pamięci RAM niż inne popularne dodatki do blokowania reklam.\n\nNależy pamiętać, że wybranie niektórych dodatkowych list może prowadzić do wzrostu prawdopodobieństwa uszkodzenia witryny internetowej – zwłaszcza tych list, które są zwykle używane jako plik hostów.\n\n***\n\nBez zaprogramowanej listy filtrów, to rozszerzenie jest bezwartościowe. Pomyśl zatem o osobach, które ciężko pracują, tworząc i utrzymując udostępniane za darmo używane przez Ciebie listy filtrów.\n\n***\n\nDarmowe rozszerzenie.\nKod źródłowy udostępniany na otwartej licencji (GPLv3)\nDla użytkowników przez użytkowników.\n\nWspółtwórcy rozszerzenia: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nAutorzy tłumaczeń: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/a71b71f3a5bb24b1e45c14bf40ee736e1e270b279e4e77e849bf3a5b7650d85e/https%3A//crowdin.com/project/ublock/translators\" rel=\"nofollow\">https://crowdin.com/project/ublock/translators</a>\n\n***\n\nOceniając rozszerzenie pamiętaj, że jest to jego wczesna wersja.\n\nDziennik zmian:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pt-BR": "Um bloqueador eficaz: Com baixo consumo de memória e CPU e ainda pode carregar e aplicar milhares de filtros. Mais do que outros bloqueadores populares lá fora.\n\nVisão geral ilustrada de sua eficiência: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-compared\n\nUtilização: Use o botão de energia no pop-up para desativar/ativar o uBlock₀ para o site atual. Isso se aplica apenas ao site atual, não é um botão global.\n\n***\n\nFlexível, é mais do que um \"ad blocker\": também pode ler e criar filtros a partir de arquivos de hosts.\n\nPor padrão, essas listas de filtros são carregadas e aplicadas:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMais listas estão disponíveis para você escolher, se desejar:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- E varios outros\n\nClaro, quanto mais filtros habilitados, maior o consumo de memória. Ainda, mesmo após a adição do Fanboy's duas listas extras, hpHosts’s e servidores de rastreamento, uBlock₀ ainda tem o consumo de memória menor do que outros bloqueadores populares lá fora.\n\nTambém, esteja ciente de que selecionar algumas dessas listas extras pode levar à maior probabilidade de quebra do layout do site, especialmente aquelas listas que são normalmente usadas como arquivo hosts.\n\n***\n\nSem as listas predefinidas de filtros, esta extensão não é nada. Então, se você realmente quiser contribuir com alguma coisa, pense sobre as pessoas que trabalham duro para manter as listas de filtro que você está usando, que estão disponíveis de graça para todos.\n\n***\n\nGratuito\nCódigo aberto com licença pública (GPLv3)\nDe usuários para usuários.\n\nContribuidores no Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuidores no Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEssa é uma versão ainda em desenvolvimento, tenha isso em mente quando você avaliar.\n\nRegistro de alterações do projeto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pt-PT": "Um bloqueador eficiente: leve na memória e CPU e, no entanto, consegue carregar e aplicar milhares de filtros a mais do que outros bloqueadores populares disponíveis.\n\nVisão geral ilustrada da sua eficiência:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilização: O botão grande de energia na janela serve para desativar ou ativar, permanentemente, o uBlock para o sítio web atual. Aplica-se unicamente ao sítio web atual, não sendo um botão de energia global.\n\n***\n\nFlexível, é mais do que um bloqueador de anúncios. Pode também ler e criar filtros a partir de ficheiros de servidores.\n\nPor predefinição, estas listas de filtros são carregadas e aplicadas:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nSe quiser, estão disponíveis mais listas para seleção:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- E muitas mais\n\nObviamente que quanto maior o número de filtros ativos, maior será o consumo de memória. No entanto, mesmo após adicionar as duas listas extra do Fanboy, hpHosts Ad and tracking servers, o uBlock₀ continua a consumir menos memória do que outros bloqueadores populares disponíveis.\n\nEsteja ciente de que se selecionar mais listas extra pode resultar numa probabilidade acrescida de rutura em alguns sítios web -- especialmente nas listas que, normalmente, são utilizadas como ficheiros de servidores.\n\n***\n\nSem as listas de filtros predefinidas, esta extensão não é nada. Se realmente quiser contribuir com algo, pense nas pessoas que trabalham duro para manter as listas de filtros que usa, que foram tornadas disponíveis para uso por todos sem custos.\n\n***\n\nGrátis.\nCódigo aberto com licença pública (GPLv3)\nDe utilizadores para utilizadores.\n\nContribuidores @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuidores @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEsta é uma versão inicial, tenha isso em mente quando avaliar.\n\nRegisto de alterações do projeto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ro": "Un blocant (paravan) eficient: folosește foarte puțin procesorul și memoria și totuși poate încărca și aplica mii de filtre în plus față de alte paravane populare.\n\nO ilustrare a eficienței poate fi observată la:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilizare: Butonul mare de pornire/oprire în fereastra paravanului este pentru a activa/dezactiva uBlock pentru saitul curent. Funcția este valabilă doar pentru saitul curent, nu la nivel global.\n\n***\n\nFlexibil, mai mult decât un „blocant de reclame”: acesta poate citi și crea filtre din fișierele de gazde (hosts).\n\nÎn mod implicit, aceste liste de filtre sunt încărcate și aplicate:\n\n- EasyList\n- Lista serverelor de reclame a lui Peter Lowe\n- EasyPrivacy\n- Domenii malițioase\n\nDe asemenea, mai sunt disponibile și alte liste precum:\n\n- Lista îmbunătățită pentru urmărire a lui Fanboy\n- Lista de gazde a lui Dan Pollock\n- Lista de reclame și urmărire hpHosts\n- Gazdele MVPS\n- Spam404\n- Și multe altele\n\nDesigur, cu cât sunt mai multe filtre active cu atât mai mult este utilizată memoria. Totuși, chiar și după adăugarea în plus a două liste Fanboy și lista de reclame și urmărire hPhosts, uBlock₀ tot folosește mai puțină memorie decât restul paravanelor.\n\nDe ținut minte, că odată cu selectarea în plus a unora dintre liste se poate ajunge la afectarea aspectului saiturilor -- în special listele care sunt în mod normal liste de gazde.\n\n***\n\nFără listele prestabilite de filtre această extensie nu face nimic. Așadar, dacă totuși vreți să contribuiți, gândiți-vă la persoanele care muncesc să întrețină aceste filtre pe care le utilizați, care sunt oferite pentru utilizare gratuită.\n\n***\n\nGratuit.\nCu sursă liberă și licență publică (GPLv3)\nPentru utilizatori de la utilizatori.\n\nContribuitori pe Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuitori pe Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEste încă o aplicație recentă, gândiți-vă la acest lucru când scrieți o recenzie.\n\nLista de schimbări a proiectului:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ru": "µBlock — эффективный блокировщик: он использует меньше оперативной памяти и меньше нагружает ЦП, при этом используя больше фильтров, чем другие популярные блокировщики.\n\nИллюстрированный обзор его эффективности: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nИспользование: нажмите большую кнопку «Включение» в выпадающем окне, чтобы включить или отключить uBlock для текущего сайта. Это действует только для текущего сайта, а не для всех.\n\n***\n\nБудучи гибким, это нечто большее, чем просто «блокировщик рекламы»: он также может считывать и создавать фильтры из хост-файлов.\n\nПо умолчанию следующие списки фильтров будут загружены и применены:\n\n- EasyList\n- Список рекламных серверов Питера Лоу\n- EasyPrivacy\n- Вредоносные домены\n\nТакже на выбор доступны другие списки:\n\n- Фанатский улучшенный список отслеживания\n- Хост-файл Дэна Поллока\n- Рекламные и отслеживающие сервера hpHosts\n- MVPS HOSTS\n- Spam404\n- И т. д.\n\nКонечно, чем больше фильтров, тем выше использование памяти. Тем не менее даже после добавления трёх дополнительных списков uBlock₀ всё ещё потребляет меньше памяти, чем другие популярные блокировщики.\n\nТакже имейте в виду, что некоторые их этих списков имеют высокую вероятность поломать веб-сайт, особенно те, что созданы из хост-файлов.\n\n***\n\nБез предустановленных списков фильтров это расширение — ничто. Так что, если вы действительно хотите внести свой вклад, подумайте о людях, усердно поддерживающих списки фильтров, предоставленные Вам для бесплатного использования.\n\n***\n\nБесплатно.\nОткрытый исходный код, публичная лицензия (GPLv3).\nДля пользователей от пользователей.\n\nУчастники на Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nУчастники на Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nЭто ещё очень ранняя версия, имейте это в виду, оценивая программу.\n\nСписок изменений:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sk": "Účinný blokovač: nezaťažuje CPU a pamäť a dokáže načítať a vynútiť o niekoľko tisíc filtrov viac ako iné populárne blokovače.\n\nIlustrovaný prehľad o jeho účinnosti: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPoužitie: Veľký vypínač vo vyskakovacom okne natrvalo zakáže/povolí uBlock pre aktuálnu webovú stránku. Vzťahuje sa len na aktuálnu webovú stránku, nie na všeobecný vypínač.\n\n***\n\nFlexibilný, je viac než len \"blokovač reklám\": dokáže tiež načítať a vytvárať filtre z hosts súborov.\n\nTieto zoznamy filtrov sú predvolene načítané a vynútené:\n\n- EasyList\n- Zoznam reklamných serverov od Petra Lowesa\n- EasyPrivacy\n- Domény malvéru\n\nĎalšie zoznamy sú k dispozícii pre vás na výber, ak si prajete:\n\n- Rozšírený stopovací zoznam od Fanboya\n- Hosts súbor od Dana Pollocka\n- Reklamné a stopovacie servery od hpHosts\n- MVPS HOSTS\n- Spam404\n- A mnoho ďalších\n\nSamozrejme, čím viac povolených filtrov, tým vyššie nároky na pamäť. Aj po pridaní dvoch ďalších zoznamov od Fanboya, reklamných a stopovacích serverov od hpHost má uBlock stále menšie nároky na pamäť ako mnohé ďalšie veľmi populárne blockovače.\n\nĎalej majte na pamäti, že výber viacerých filtrov zvyšuje šancu chybného zobrazenie webov - predovšetkým u zoznamov, ktoré sa normálne používajú ako hosts súbory.\n\n***\n\nBez predvolených zoznamov filtrov by bolo toto rozšírenie k ničomu. Ak teda naozaj budete chcieť niečím prispieť, myslite na ľudí, ktorí spravujú vami používané zoznamy filtrov a uvoľňujú ich pre všetkých zadarmo.\n\n***\n\nBezplatný.\nOtvorený zdrojový kód s verejnou licenciou (GPLv3)\nPre používateľov od používateľov.\n\nPrispievatelia @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPrispievatelia @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nIde o pomerne skorú verziu, majte to na pamäti pri recenzovaní.\n\nZoznam zmien projektu:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sl": "Efektiven zatiralec oglasov: lahek na pomnilniku in procesorju, in vendar lahko nalaga in uveljavlja tisoče filtrov več kot kakšen drug popularen dodatek za blokiranje oglasov.\n\nIlustrirana efektivnost: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUporaba: Velik gumb za vklop/izklop v pojavnem oknu je namenjen trajnemu izklopu/vklopu uBlock₀ za trenutno spletno stran. Ta uporaba velja samo za trenutno spletno stran, tako da gumb ne predstavlja globalnega vklopa/izklopa.\n\n***\n\nuBlock₀ je fleksibilen - in s tem več kot samo \"blokada oglasom\": lahko bere in ustvarja filtre iz datotek z gostitelji (HOSTS datoteka).\n\nBrez kakršnihkoli dodatnih nastavitev, uBlock₀ uporablja sledeče filtre:\n\n- EasyList\n- Seznam oglaševalskih strežnikov Peter Lowe\n- EasyPrivacy\n- Zlonamerne domene\n\nVeč filtrskih seznamov na razpolago (če to želite):\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- In mnogi drugi\n\nSeveda, več filtrov kot je aktivnih, večji je odtis v pomnilniku. Pa kljub temu - tudi z nalaganjem dveh dodatnih seznamov filtrov (Fanboy in hpHosts) ima uBlock₀ še vedno nižjo mero obremenitve pomnilnika kot ostali zelo popularni dodatki za blokiranje oglasov.\n\nPoleg tega bodite pozorni, da vklop določenih dodatnih seznamov filtrov lahko pripelje do višje verjetnosti za nefunkcionalnost spletne strani - predvsem \"ogrožajoči\" so tisti seznami, ki se jih ponavadi uporablja kot HOSTS datoteko.\n\n***\n\nBrez prednastavljenih seznamov filtrov, da dodatek ni nič. Tako da, če res želite kje pomagati ali komu plačati kavo, pomislite na ljudi, ki trdo delajo, da vzdržujejo te sezname filtrov, ki jih uporabljate, in so jih naredili dosegljive zastonj in za vse.\n\n***\n\nZastonj.\nOdprtokodno pod GPLv3 licenco\nZa uporabnike od uporabnikov.\n\nRazvijalci @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPrevajalci @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nGre še za dokaj sveže različice, prosimo da to upoštevate pri vaši kritiki.\n\nDnevnik sprememb projekta:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sq": "Një bllokues efikas: me impakt të vogël te memorja dhe procesori, por mund të hapë dhe të zbatojë mijëra filtra më shumë sesa bllokuesit e tjerë të njohur.\n\nPërmbledhje e ilustruar e efikasitetit të tij: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPërdorimi: Çelësi i komandimit te dritarja e vogël e bën uBlock përherë joaktiv/aktiv për uebsajtin aktual. Ai vlen vetëm për uebsajtin aktual, nuk është një çelës i përgjithshëm.\n\n***\n\nËshtë fleksibël dhe jo thjesht një \"bllokues reklamash\": mund të lexojë dhe të krijojë filtra nga skedat \"hosts\".\n\nFiltrat e listuar këtu hapen dhe zbatohen pas instalimit:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPo të doni, ka edhe shumë lista të tjera të gatshme:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Dhe shumë të tjera\n\nSigurisht që sa më shumë filtra të aktivizoni, aq më i madh do të jetë impakti te memorja. Edhe me shtimin e dy listave shtesë të Fanboy, hpHosts’s Ad and tracking servers, uBlock përsëri ka impakt më të ulët në memorje sesa bllokuesit e tjerë shumë të njohur.\n\nPor, kujdes, sepse duke përzgjedhur disa prej këtyre listave, gjasat që faqet të shfaqin probleme do të jenë më të mëdha -- sidomos listat që normalisht përdoren si skeda \"hosts\".\n\n***\n\nPa listat e programuara, ky program nuk vlen për asgjë. Prandaj, po të doni të kontribuoni diçka, mendoni pak për njerëzit që punojnë fort për mirëmbajtjen e listave me filtra që po përdorni, të cilat na ofrohen të gjithëve pa pagesë.\n\n***\n\nFalas.\nMaterial i hapur me licencë publike (GPLv3)\nKrijuar nga përdoruesit për përdoruesit.\n\nKontributorët @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKontributorët @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nKur të bëni vlerësimin e programit, mos harroni se ky është një version paraprak.\n\nDitari i projektit:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sv-SE": "En effektiv blockerare: lätt på minne och CPU-fotavtryck, som ändå kan ladda och applicera tusentals fler filter jämfört med andra populära blockerare där ute.\n\nIllustrerad översikt av dess effektivitet:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nAnvändning: Den stora strömbrytarikonen i popuprutan är till för att avaktivera/aktivera uBlock₀ på den aktuella webbplatsen permanent. Detta gäller enbart för den aktuella webbplatsen, det är inte en global strömbrytare.\n\n***\n\nFlexibel. uBlock₀ är inte enbart en \"reklamblockerare\": den kan också läsa och skapa filter från hosts-filer.\n\nSom standard är följande filterlistor laddade och applicerade:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nFler filterlistor finns tillgängliga att använda om du vill:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- med flera\n\nJu fler aktiverade filter, desto högre minnesanvändning. Men även efter att ha lagt till Fanboys två extra filterlistor och hpHosts' Ad and tracking servers så använder uBlock₀ mindre minne än andra väldigt populära blockerare.\n\nTänk på att genom att aktivera vissa av dessa extra filterlistor finns det större risk att webbplatser går sönder - särskilt de listor som i normala fall används som hosts-filer.\n\n***\n\nuBlock₀ vore ingenting utan filterlistorna. Så om du vill bidra med någonting, tänk på människorna som arbetar hårt med att upprätthålla de filterlistor du använder, vilka är fritt tillgängliga för allas användning.\n\n***\n\nGratis.\nÖppen källkod med offentlig licens (GPLv3)\nFör användare, av användare.\n\nBidragsgivare @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nBidragsgivare @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDetta är en ganska tidig version, tänk på detta när du skriver en recension.\n\nProjektets ändringslogg:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "uk": "Ефективний блокувальник реклами: сильно не навантажує пам’ять та процесор і може працювати з набагато більшою кількістю фільтрів ніж інші блокувальники.\n\nІлюстрований огляд ефективності: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nВикористання: Ця велика кнопка живлення у виринаючому вікні дозволяє вимкнути або увімкнути uBlock для поточного веб-сайту. Ефект розповсюджується тільки на поточний веб-сайт. Це не глобальна кнопка живлення.\n\n***\n\nБудучи універсальним, це більш ніж просто \"блокувальник реклами\". Він також може створювати фільтри з файлів hosts.\n\nЗа замовчуванням завантажено та застосовано наступні списки фільтрів:\n\n– EasyList\n– список рекламних серверів Петра Лоу\n– EasyPrivacy\n– шкідливі домени\n\nНаступні списки можна можна увімкнути за бажанням:\n\n– покращений список слідкування від Fanboy\n– файл хостів Дена Полока\n– сервери реклами та слідкування hpHosts\n– MVPS HOSTS\n– Spam404\n– тощо.\n\nЗвичайно ж, чим більше фільтрів ви увімкнете тим більшим буде використання пам’яті. Однак, навіть після додання двох додаткових списків Fanboy, серверів слідкування та реклами phHosts, uBlock споживає менше пам’яті ніж інші популярні блокувальники.\n\nТакож майте на увазі, що задіяння деяких додаткових списків може спричинити збільшення ймовірності пошкодження функціонування сайту. Особливо ті списки, які зазвичай використовуються як hosts-файл.\n\n***\n\nБез встановлених списків фільтрів це розширення – ніщо. Тому, якщо ви дійсно хочете зробити свій внесок, подумайте про людей, які тяжко працюють для підтримки списків фільтрів якими ви користуєтесь безкоштовно.\n\n***\n\nБезкоштовно.\nВідкритий джерельний код та публічна ліцензія (GPLv3)\nДля користувачів від користувачів.\n\nУчасники @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nПерекладачі @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nЦе ще дуже дочасна версія, тому майте на увазі, коли робите огляд.\n\nЖурнал змін проекту:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ur": "ایک زبردست اشتہارات کو روکنے والا سافٹویئر. کم میموری اور cpu استعمال کرتا ہے مگر کام بہترین کرتا ہے.\n\nاس کا بہترین اور پراثر کام کرنے کی تصاویر:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nہدایات: بڑا آن/ آف کا بٹن دبا کر آپ موجودہ ویب سائٹ پر uBlock کو فعال یا غیر فعال کر سکتے ہیں. یہ بٹن صرف موجودہ ویب سائٹ کے لئے ہے، باقی ویب سائٹس کو اس سے کوئی فرق نہیں پڑے گا.\n\n***\n\nFlexible, it's more than an \"ad blocker\": it can also read and create filters from hosts files.\n\nیہ والے فلٹر پہلے سے لاگو ہوں گے:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMore lists are available for you to select if you wish:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- And many others\n\nجتنے زیادہ فلٹر لگائیں گے اتنی زیادہ میموری لے گا. Yet, even after adding Fanboy's two extra lists, hpHosts’s Ad and tracking servers, uBlock still has a lower memory footprint than other very popular blockers out there.\n\nAlso, be aware that selecting some of these extra lists may lead to higher likelihood of web site breakage -- especially those lists which are normally used as hosts file.\n\n***\n\nWithout the preset lists of filters, this extension is nothing. So if ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\nمفت.\nاوپن سورس عوامی لائسنس(جی.پی.ایل ورژن ٣) کے ساتھ\nعوام کے لیے، عوام کا بنایا ہوا.\n\nمعاونین کی فہرست Github پر دیکھیں:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nمترجمین کی فہرست Crowdin پر دیکھیں:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\nپراجیکٹ میں ترقیاتی کام کا ریکارڈ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "vi": "Một công cụ chặn quảng cáo hiệu quả: sử dụng ít bộ nhớ, CPU và có thể nạp, áp dụng hàng ngàn bộ lọc so với những công cụ chặn quảng cáo hiện nay.\n\nMinh hoạ tổng quan về tính hiệu quả của µBlock: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nSử dụng: Nút nguồn lớn trong hộp thoại popup để vô hiệu/kích hoạt vĩnh viễn uBlock cho website hiện tại. Nó chỉ áp dụng cho trang hiện tại, không phải tất cả website.\n\n***\n\nLinh hoạt, hơn cả một \"công cụ chặn quảng cáo\": µBlock có thể đọc và tạo bộ lọc từ tập tin hosts.\n\nNgay lập tức, những bộ lọc này được nạp và áp dụng:\n\n- EasyList\n- Danh sách máy chủ quảng cáo của Peter Lowe\n- EasyPrivacy\n- Malware domains\n\nCó thêm nhiều danh sách để bạn lựa chọn:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- Máy chủ quảng cáo và theo dõi hpHosts\n- MVPS HOSTS\n- Spam404\n- Và nhiều hơn nữa\n\nDĩ nhiên, khi kích hoạt thêm bộ lọc, tiện ích sẽ dùng nhiều bộ nhớ hơn. Tuy vậy, sau khi thêm hai danh sách Fanboy, máy chủ quảng cáo và theo dõi của hpHosts, uBlock vẫn dùng ít bộ nhớ hơn so với những công cụ chặn quảng cáo rất phổ biến khác.\n\nNgoài ra, lưu ý rằng chọn thêm một số danh sách có thể dẫn đến khả năng một số website hiển thị không đúng cách -- đặc biệt là những danh sách thường được dùng như tập tin hosts.\n\n***\n\nKhông có danh sách bộ lọc cài sẵn, tiện ích mở rộng này chẳng là gì cả. Vậy nên nếu bạn thật sự muốn đóng góp gì đó, hãy nghĩ về những người đang chăm chỉ duy trì danh sách bộ lọc hoàn toàn miễn phí mà bạn đang dùng.\n\n***\n\nMiễn phí.\nNguồn mở với giấy phép công cộng (GPLv3)\nLàm vì người dùng bởi người dùng.\n\nNhững người đóng góp @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nNhững người đóng góp @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nĐây là một phiên bản khá mới, hãy ghi nhớ điều này khi bạn đánh giá.\n\nThay đổi của dự án:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "zh-CN": "一款高效的请求过滤工具:占用极低的内存和CPU,和其他常见的过滤工具相比,它能够加载并执行上千条过滤规则。\n\n效率概述说明: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n用法:点击弹出窗口中的电源按钮,uBlock 将对当前网页永久禁用/启用过滤功能。 它只控制当前网页的请求过滤,而不是一个全局开关。 它只控制当前网页的请求过滤,而不是一个全局开关。\n\n***\n\n它不只是一个广告拦截工具,它还可以从 hosts 文件里读取和创建过滤规则。\n\n初始默认加载和执行下列过滤规则列表:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\n这里还有更多的规则列表供你选择:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 等等\n\n当然,启用越多的过滤规则就会产生越高的内存占用。 然而,即使再添加 Fanboy 额外的两个规则列表,如 hpHosts’s Ad 和 tracking servers,uBlock 的内存占用依然比其他常见的过滤工具要低的多。\n\n另外请注意,选择一些额外的列表可能会导致网页破损可能性增高 —— 尤其是那些通常被用作 hosts 文件的列表。\n\n***\n\n没有这些过滤规则列表,这个扩展就没有了意义。 所以如果你真的想做点贡献,想想那些维护过滤规则的人们,是他们让所有人能够免费使用这一切变得可能。\n\n***\n\n免费。\n遵从 GPLv3 公共许可协议开源。\n一切为了用户。\n\n贡献者 @ Github:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n贡献者 @ Crowdin:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n它还是一个相当早期的版本,在您评论的时候请记住这一点。\n\n项目更新日志:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "zh-TW": "一款高效率的廣告攔截工具:只使用超低的記憶體和CPU使用量,和其他常見的廣告攔截工具相比,可以載入並執行上千條過濾規則。\n\n效率概述說明: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/90cf866e9b2e1ea9282c8c93e7a0891c713248d4bf07b8aaefe26d97f8ccde33/https%3A//github.com/gorhill/uBlock/wiki/%25C2%25B5Block-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/%C2%B5Block-vs.-ABP:-efficiency-compared</a>\n\n用法:點選快顯視窗中的電源按鈕,μBlock將會在目前正在瀏覽的網頁永久停用/啟用廣告攔截功能。 它只適用於目前正在瀏覽的網頁,而不是全域按鈕。\n\n***\n\n這不只是一個廣告攔截工具,它還可以非常有彈性的從hosts檔裡讀取和建立過濾規則。\n\n初始預設載入和執行下列過濾規則:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n- Long-lived malware domains\n- Malware Domains List\n\n這裡還擁有更多的過濾規則供你選擇:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 其他\n\n啟用越多的過濾規則就會佔用越多的記憶體。 然而,即使在加入 Fanboy 額外的兩個規則和 hpHosts’s Ad and tracking servers,uBlock₀ 的記憶體佔用依然比其他常見的過濾工具要小的多。\n\n另外,請注意選擇的一些額外的清單可能會導致網頁破損可能性增高 — — 尤其是那些通常用來當作hosts檔案的清單。\n\n***\n\n沒有這些過濾規則清單,這個擴充套件就沒有了意義。 所以如果你真的想要做些貢獻,試著想想那些努力維護廣告過濾規則清單的人們,至少他們讓大家可以免費使用這一切。\n\n***\n\n自由、免費。\n開放原始程式碼與公共許可證 (GPLv3)\n一切都是為了使用者。\n\n貢獻者@ Github: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n貢獻者 @ Crowdin: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n這還只是一個非常初期的版本,當您留下建議的時候請手下留情。\n\n專案更新日誌:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>"
+ },
+ "developer_comments": null,
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/edit",
+ "guid": "uBlock0@raymondhill.net",
+ "has_eula": false,
+ "has_privacy_policy": true,
+ "homepage": {
+ "en-US": "https://github.com/gorhill/uBlock#ublock-origin"
+ },
+ "icon_url": "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "icons": {
+ "32": "https://addons.mozilla.org/user-media/addon_icons/607/607454-32.png?modified=mcrushed",
+ "64": "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "128": "https://addons.mozilla.org/user-media/addon_icons/607/607454-128.png?modified=mcrushed"
+ },
+ "is_disabled": false,
+ "is_experimental": false,
+ "last_updated": "2023-08-07T17:15:41Z",
+ "name": {
+ "ar": "uBlock Origin",
+ "bg": "uBlock Origin",
+ "bn-BD": "uBlock Origin",
+ "ca": "uBlock Origin",
+ "cs": "uBlock Origin",
+ "da": "uBlock Origin",
+ "de": "uBlock Origin",
+ "el": "uBlock Origin",
+ "en-US": "uBlock Origin",
+ "es": "uBlock Origin",
+ "eu": "uBlock Origin",
+ "fa": "uBlock Origin",
+ "fi": "uBlock Origin",
+ "fr": "uBlock Origin",
+ "he": "uBlock Origin",
+ "hu": "uBlock Origin",
+ "id": "uBlock Origin",
+ "it": "uBlock Origin",
+ "ja": "uBlock Origin",
+ "ka": "uBlock Origin",
+ "ko": "uBlock Origin",
+ "nl": "uBlock Origin",
+ "pl": "uBlock Origin",
+ "pt-BR": "uBlock Origin",
+ "pt-PT": "uBlock Origin",
+ "ro": "uBlock Origin",
+ "ru": "uBlock Origin",
+ "sk": "uBlock Origin",
+ "sl": "uBlock Origin",
+ "sq": "uBlock Origin",
+ "sv-SE": "uBlock Origin",
+ "uk": "uBlock Origin",
+ "ur": "uBlock Origin",
+ "vi": "uBlock Origin",
+ "zh-CN": "uBlock Origin",
+ "zh-TW": "uBlock Origin"
+ },
+ "previews": [
+ {
+ "id": 238546,
+ "caption": {
+ "en-US": "The popup panel: default mode"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238546.png?modified=1622132421",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238546.jpg?modified=1622132421"
+ },
+ {
+ "id": 238548,
+ "caption": {
+ "en-US": "The dashboard: stock filter lists"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238548.png?modified=1622132423",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238548.jpg?modified=1622132423"
+ },
+ {
+ "id": 238547,
+ "caption": {
+ "en-US": "The popup panel: default-deny mode"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238547.png?modified=1622132425",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238547.jpg?modified=1622132425"
+ },
+ {
+ "id": 238549,
+ "caption": {
+ "en-US": "The dashboard: settings"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238549.png?modified=1622132426",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238549.jpg?modified=1622132426"
+ },
+ {
+ "id": 238552,
+ "caption": {
+ "en-US": "The popup panel in Firefox Preview: default mode with more blocking options revealed"
+ },
+ "image_size": [
+ 970,
+ 1800
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238552.png?modified=1622132430",
+ "thumbnail_size": [
+ 216,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238552.jpg?modified=1622132430"
+ },
+ {
+ "id": 230370,
+ "caption": {
+ "en-US": "The unified logger tells you all that uBO is seeing and doing"
+ },
+ "image_size": [
+ 800,
+ 600
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/230/230370.png?modified=1622132432",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/230/230370.jpg?modified=1622132432"
+ }
+ ],
+ "promoted": {
+ "apps": [
+ "firefox",
+ "android"
+ ],
+ "category": "recommended"
+ },
+ "ratings": {
+ "average": 4.7825,
+ "bayesian_average": 4.782204826721061,
+ "count": 15799,
+ "text_count": 4101
+ },
+ "ratings_url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/reviews/",
+ "requires_payment": false,
+ "review_url": "https://addons.mozilla.org/en-US/reviewers/review/607454",
+ "slug": "ublock-origin",
+ "status": "public",
+ "summary": {
+ "ar": "وأخيراً, مانع اعلانات كفوء. خفيف على المعالج و الذاكرة",
+ "bg": "Най-накрая, ефективен рекламен блокер с малки изисквания за процесор и памет.",
+ "bn-BD": "অবশেষে, একটি দক্ষ প্রতিরোধক। সিপিইউ এবং মেমরির জন্য সহজ।",
+ "ca": "Finalment, un blocador eficient que utilitza pocs recursos de memòria i processador.",
+ "cs": "Konečně efektivní blokovač, který nezatěžuje CPU a paměť.",
+ "da": "Endelig en effektiv blocker til Chromium-baserede browsere. Lavt CPU- og hukommelsesbrug.",
+ "de": "Endlich ein effizienter Blocker. Prozessor-freundlich und bescheiden beim Speicherbedarf.",
+ "el": "Επιτέλους, ένας αποτελεσματικός blocker. Ελαφρύς για τον επεξεργαστή και τη μνήμη.",
+ "en-US": "Finally, an efficient wide-spectrum content blocker. Easy on CPU and memory.",
+ "es": "Por fin, un bloqueador eficiente con uso mínimo de procesador y memoria.",
+ "eu": "Behingoz, blokeatzaile eraginkor bat. PUZ eta memorian arina.",
+ "fa": "بالاخره، یک بلاکر کارآمد. کم حجم بر روی پردازنده و حافظه",
+ "fi": "Viimeinkin tehokas ja kevyt mainosten estäjä.",
+ "fr": "Un bloqueur de nuisances efficace, qui ménagera votre processeur et votre mémoire vive.",
+ "he": "סוף סוף, חוסם יעיל. קל על המעבד והזיכרון",
+ "hu": "Végre egy hatékony reklám- és követésblokkoló böngészőkhöz, amely kíméletes a processzorral és a memóriával.",
+ "id": "Akhirnya, pemblokir iklan yang efisien. Ringan penggunaan CPU dan memori.",
+ "it": "Finalmente, un blocker efficiente. Leggero sulla CPU e sulla memoria.",
+ "ja": "高効率ブロッカーが遂に登場。CPUとメモリーの負担を抑えます。",
+ "ka": "როგორც იქნა, მძლავრი და შედეგიანი რეკლამების შემზღუდავი. ზოგავს CPU-ს და მეხსიერებას.",
+ "ko": "이 부가 기능은 효율적인 차단기입니다. CPU와 메모리에 주는 부담이 적습니다.",
+ "nl": "Eindelijk, een efficiënte adblocker. Gebruikt weinig processorkracht en geheugen.",
+ "pl": "Nareszcie skuteczne blokowanie reklam, niskie użycie procesora i pamięci.",
+ "pt-BR": "Finalmente, um bloqueador eficiente. Com baixo uso de memória e CPU.",
+ "pt-PT": "Finalmente, um bloqueador eficiente. Leve na CPU e memória.",
+ "ro": "În sfârșit, un blocant eficient. Folosește procesorul și memoria foarte puțin.",
+ "ru": "Наконец-то, быстрый и эффективный блокировщик для браузеров.",
+ "sk": "Konečne efektívny blokovač, ktorý nezaťažuje CPU a pamäť.",
+ "sl": "Končno, učinkovita, procesorju in pomnilniku prijazna razširitev za blokiranje oglasov.",
+ "sq": "Më në fund, një bllokues efikas që nuk e rëndon procesorin dhe memorjen.",
+ "sv-SE": "Äntligen en effektiv blockerare! Snäll mot processor och minne.",
+ "uk": "Ефективний блокувальник реклами таки з’явився. Не навантажує процесор та пам'ять.",
+ "ur": "آخر کار، ایک مؤثر اشتہار کو روکنے والا، یہ کم cpu اور میموری لیتا ہے",
+ "vi": "Cuối cùng, đã có một công cụ chặn quảng cáo hiệu quả, tiêu tốn ít CPU và bộ nhớ.",
+ "zh-CN": "一款高效的网络请求过滤工具,占用极低的内存和 CPU。",
+ "zh-TW": "終於出現了,一個高效率的阻擋器,使用不多的 CPU 及記憶體資源。"
+ },
+ "support_email": null,
+ "support_url": {
+ "en-US": "https://old.reddit.com/r/uBlockOrigin/",
+ "ka": "https://old.reddit.com/r/uBlockOrigin/",
+ "ur": "https://old.reddit.com/r/uBlockOrigin/"
+ },
+ "tags": [
+ "ad blocker",
+ "anti malware",
+ "anti tracker",
+ "content blocker",
+ "privacy",
+ "security"
+ ],
+ "type": "extension",
+ "url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/",
+ "versions_url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/versions/",
+ "weekly_downloads": 143905,
+ "_score": null
+ },
+ {
+ "id": 869140,
+ "authors": [
+ {
+ "id": 13394925,
+ "name": "Thomas Wisniewski",
+ "url": "https://addons.mozilla.org/en-US/firefox/user/13394925/",
+ "username": "wisniewskit"
+ },
+ {
+ "id": 6084813,
+ "name": "Rob W",
+ "url": "https://addons.mozilla.org/en-US/firefox/user/6084813/",
+ "username": "RobW"
+ }
+ ],
+ "average_daily_users": 111124,
+ "categories": {
+ "android": [
+ "other"
+ ],
+ "firefox": [
+ "search-tools"
+ ]
+ },
+ "contributions_url": "",
+ "created": "2017-10-31T15:35:56Z",
+ "current_version": {
+ "id": 5110671,
+ "compatibility": {
+ "android": {
+ "min": "56.0",
+ "max": "*"
+ }
+ },
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/google-search-fixer/versions/5110671",
+ "is_strict_compatibility_enabled": false,
+ "license": {
+ "id": 3338,
+ "is_custom": false,
+ "name": {
+ "en-US": "Mozilla Public License 2.0"
+ },
+ "url": "http://www.mozilla.org/MPL/2.0/"
+ },
+ "release_notes": {
+ "en-US": "This version features improves the performance and reliability of the add-on,\nand allows the Desktop mode to behave as intended."
+ },
+ "reviewed": "2020-10-06T08:39:18Z",
+ "version": "1.6",
+ "files": [
+ {
+ "id": 3655036,
+ "created": "2020-10-05T16:52:49Z",
+ "hash": "sha256:ddc495ab2a764774cf1919f0c946b95a932795c344ce9784827ef818125173ae",
+ "is_restart_required": false,
+ "is_webextension": true,
+ "is_mozilla_signed_extension": false,
+ "platform": "all",
+ "size": 13786,
+ "status": "public",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/3655036/google_search_fixer-1.6.xpi",
+ "permissions": [
+ "webRequest",
+ "webRequestBlocking",
+ "*://*/*",
+ "*://*.google.com/*",
+ "*://*.google.ac/*",
+ "*://*.google.ad/*",
+ "*://*.google.ae/*",
+ "*://*.google.com.af/*",
+ "*://*.google.com.ag/*",
+ "*://*.google.com.ai/*",
+ "*://*.google.al/*",
+ "*://*.google.am/*",
+ "*://*.google.co.ao/*",
+ "*://*.google.com.ar/*",
+ "*://*.google.as/*",
+ "*://*.google.at/*",
+ "*://*.google.com.au/*",
+ "*://*.google.az/*",
+ "*://*.google.ba/*",
+ "*://*.google.com.bd/*",
+ "*://*.google.be/*",
+ "*://*.google.bf/*",
+ "*://*.google.bg/*",
+ "*://*.google.com.bh/*",
+ "*://*.google.bi/*",
+ "*://*.google.bj/*",
+ "*://*.google.com.bn/*",
+ "*://*.google.com.bo/*",
+ "*://*.google.com.br/*",
+ "*://*.google.bs/*",
+ "*://*.google.bt/*",
+ "*://*.google.co.bw/*",
+ "*://*.google.by/*",
+ "*://*.google.com.bz/*",
+ "*://*.google.ca/*",
+ "*://*.google.cd/*",
+ "*://*.google.cf/*",
+ "*://*.google.cg/*",
+ "*://*.google.ch/*",
+ "*://*.google.ci/*",
+ "*://*.google.co.ck/*",
+ "*://*.google.cl/*",
+ "*://*.google.cm/*",
+ "*://*.google.cn/*",
+ "*://*.google.com.co/*",
+ "*://*.google.co.cr/*",
+ "*://*.google.com.cu/*",
+ "*://*.google.cv/*",
+ "*://*.google.com.cy/*",
+ "*://*.google.cz/*",
+ "*://*.google.de/*",
+ "*://*.google.dj/*",
+ "*://*.google.dk/*",
+ "*://*.google.dm/*",
+ "*://*.google.com.do/*",
+ "*://*.google.dz/*",
+ "*://*.google.com.ec/*",
+ "*://*.google.ee/*",
+ "*://*.google.com.eg/*",
+ "*://*.google.es/*",
+ "*://*.google.com.et/*",
+ "*://*.google.fi/*",
+ "*://*.google.com.fj/*",
+ "*://*.google.fm/*",
+ "*://*.google.fr/*",
+ "*://*.google.ga/*",
+ "*://*.google.ge/*",
+ "*://*.google.gg/*",
+ "*://*.google.com.gh/*",
+ "*://*.google.com.gi/*",
+ "*://*.google.gl/*",
+ "*://*.google.gm/*",
+ "*://*.google.gp/*",
+ "*://*.google.gr/*",
+ "*://*.google.com.gt/*",
+ "*://*.google.gy/*",
+ "*://*.google.com.hk/*",
+ "*://*.google.hn/*",
+ "*://*.google.hr/*",
+ "*://*.google.ht/*",
+ "*://*.google.hu/*",
+ "*://*.google.co.id/*",
+ "*://*.google.ie/*",
+ "*://*.google.co.il/*",
+ "*://*.google.im/*",
+ "*://*.google.co.in/*",
+ "*://*.google.iq/*",
+ "*://*.google.is/*",
+ "*://*.google.it/*",
+ "*://*.google.je/*",
+ "*://*.google.com.jm/*",
+ "*://*.google.jo/*",
+ "*://*.google.co.jp/*",
+ "*://*.google.co.ke/*",
+ "*://*.google.com.kh/*",
+ "*://*.google.ki/*",
+ "*://*.google.kg/*",
+ "*://*.google.co.kr/*",
+ "*://*.google.com.kw/*",
+ "*://*.google.kz/*",
+ "*://*.google.la/*",
+ "*://*.google.com.lb/*",
+ "*://*.google.li/*",
+ "*://*.google.lk/*",
+ "*://*.google.co.ls/*",
+ "*://*.google.lt/*",
+ "*://*.google.lu/*",
+ "*://*.google.lv/*",
+ "*://*.google.com.ly/*",
+ "*://*.google.co.ma/*",
+ "*://*.google.md/*",
+ "*://*.google.me/*",
+ "*://*.google.mg/*",
+ "*://*.google.mk/*",
+ "*://*.google.ml/*",
+ "*://*.google.com.mm/*",
+ "*://*.google.mn/*",
+ "*://*.google.ms/*",
+ "*://*.google.com.mt/*",
+ "*://*.google.mu/*",
+ "*://*.google.mv/*",
+ "*://*.google.mw/*",
+ "*://*.google.com.mx/*",
+ "*://*.google.com.my/*",
+ "*://*.google.co.mz/*",
+ "*://*.google.com.na/*",
+ "*://*.google.com.nf/*",
+ "*://*.google.com.ng/*",
+ "*://*.google.com.ni/*",
+ "*://*.google.ne/*",
+ "*://*.google.nl/*",
+ "*://*.google.no/*",
+ "*://*.google.com.np/*",
+ "*://*.google.nr/*",
+ "*://*.google.nu/*",
+ "*://*.google.co.nz/*",
+ "*://*.google.com.om/*",
+ "*://*.google.com.pa/*",
+ "*://*.google.com.pe/*",
+ "*://*.google.com.pg/*",
+ "*://*.google.com.ph/*",
+ "*://*.google.com.pk/*",
+ "*://*.google.pl/*",
+ "*://*.google.pn/*",
+ "*://*.google.com.pr/*",
+ "*://*.google.ps/*",
+ "*://*.google.pt/*",
+ "*://*.google.com.py/*",
+ "*://*.google.com.qa/*",
+ "*://*.google.ro/*",
+ "*://*.google.ru/*",
+ "*://*.google.rw/*",
+ "*://*.google.com.sa/*",
+ "*://*.google.com.sb/*",
+ "*://*.google.sc/*",
+ "*://*.google.se/*",
+ "*://*.google.com.sg/*",
+ "*://*.google.sh/*",
+ "*://*.google.si/*",
+ "*://*.google.sk/*",
+ "*://*.google.com.sl/*",
+ "*://*.google.sn/*",
+ "*://*.google.so/*",
+ "*://*.google.sm/*",
+ "*://*.google.sr/*",
+ "*://*.google.st/*",
+ "*://*.google.com.sv/*",
+ "*://*.google.td/*",
+ "*://*.google.tg/*",
+ "*://*.google.co.th/*",
+ "*://*.google.com.tj/*",
+ "*://*.google.tk/*",
+ "*://*.google.tl/*",
+ "*://*.google.tm/*",
+ "*://*.google.tn/*",
+ "*://*.google.to/*",
+ "*://*.google.com.tr/*",
+ "*://*.google.tt/*",
+ "*://*.google.com.tw/*",
+ "*://*.google.co.tz/*",
+ "*://*.google.com.ua/*",
+ "*://*.google.co.ug/*",
+ "*://*.google.co.uk/*",
+ "*://*.google.com.uy/*",
+ "*://*.google.co.uz/*",
+ "*://*.google.com.vc/*",
+ "*://*.google.co.ve/*",
+ "*://*.google.vg/*",
+ "*://*.google.co.vi/*",
+ "*://*.google.com.vn/*",
+ "*://*.google.vu/*",
+ "*://*.google.ws/*",
+ "*://*.google.rs/*",
+ "*://*.google.co.za/*",
+ "*://*.google.co.zm/*",
+ "*://*.google.co.zw/*",
+ "*://*.google.cat/*",
+ "*://*.google.ng/*"
+ ],
+ "optional_permissions": [],
+ "host_permissions": []
+ }
+ ]
+ },
+ "default_locale": "en-US",
+ "description": {
+ "en-US": "Google's Web Search currently doesn't provide the same search experience to browsers like Firefox for Android as it does for Chrome. However, Firefox is actually capable of showing the more advanced page that Chrome gets (with some issues which should hopefully all be cosmetic).\n\nFirefox engineers are working with Google to fix this situation, but it is unclear how long that process will take. This add-on was made to let users opt into getting that better experience in the meantime, by simply installing the addon (no configuration required).\n\nThis add-on works by spoofing the relevant user-agent information so that Google Search sends the Chrome-specific page to Firefox for Android. It also adds a rider to that information which should hopefully make it clear to those paying attention that users are actually using Firefox, not Chrome.\n\nPlease note that using this addon will make your browser appear to Google to be an \"LG Nexus\" device, regardless of the actual make and model number of your device. As such it will appear that way on your Google \"recently used devices\" listings (<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/d94f5b113aaa934774162d1778a4692252c16d2bd50c3e029ae4a6e9a48587bf/https%3A//myaccount.google.com/device-activity\" rel=\"nofollow\">https://myaccount.google.com/device-activity</a>).\n\n\nNote: Android users seeing \"unavailable on your platform\", go to <a href=\"https://addons.mozilla.org/en-US/android/addon/google-search-fixer/\" rel=\"nofollow\">https://addons.mozilla.org/en-US/android/addon/google-search-fixer/</a>"
+ },
+ "developer_comments": null,
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/google-search-fixer/edit",
+ "guid": "{58c32ac4-0d6c-4d6f-ae2c-96aaf8ffcb66}",
+ "has_eula": false,
+ "has_privacy_policy": false,
+ "homepage": {
+ "en-US": "http://github.com/wisniewskit/google-search-fixer"
+ },
+ "icon_url": "https://addons.mozilla.org/user-media/addon_icons/869/869140-64.png?modified=mcrushed",
+ "icons": {
+ "32": "https://addons.mozilla.org/user-media/addon_icons/869/869140-32.png?modified=mcrushed",
+ "64": "https://addons.mozilla.org/user-media/addon_icons/869/869140-64.png?modified=mcrushed",
+ "128": "https://addons.mozilla.org/user-media/addon_icons/869/869140-128.png?modified=mcrushed"
+ },
+ "is_disabled": false,
+ "is_experimental": false,
+ "last_updated": "2020-10-06T08:39:18Z",
+ "name": {
+ "en-US": "Google Search Fixer"
+ },
+ "previews": [],
+ "promoted": {
+ "apps": [
+ "android"
+ ],
+ "category": "recommended"
+ },
+ "ratings": {
+ "average": 4.4096,
+ "bayesian_average": 4.405012204050444,
+ "count": 1233,
+ "text_count": 335
+ },
+ "ratings_url": "https://addons.mozilla.org/en-US/firefox/addon/google-search-fixer/reviews/",
+ "requires_payment": false,
+ "review_url": "https://addons.mozilla.org/en-US/reviewers/review/869140",
+ "slug": "google-search-fixer",
+ "status": "public",
+ "summary": {
+ "en-US": "Override the user-agent string presented to Google Search pages to receive the search experience shown to Chrome."
+ },
+ "support_email": {
+ "en-US": "wisniewskit@gmail.com"
+ },
+ "support_url": {
+ "en-US": "http://github.com/wisniewskit/google-search-fixer"
+ },
+ "tags": [],
+ "type": "extension",
+ "url": "https://addons.mozilla.org/en-US/firefox/addon/google-search-fixer/",
+ "versions_url": "https://addons.mozilla.org/en-US/firefox/addon/google-search-fixer/versions/",
+ "weekly_downloads": 30,
+ "_score": null
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_single_result.json b/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_single_result.json
new file mode 100644
index 0000000000..b6a3a79892
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/resources/amo_search_single_result.json
@@ -0,0 +1,364 @@
+{
+ "page_size": 25,
+ "page_count": 1,
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 607454,
+ "authors": [
+ {
+ "id": 11423598,
+ "name": "Raymond Hill",
+ "url": "https://addons.mozilla.org/en-US/firefox/user/11423598/",
+ "username": "gorhill"
+ }
+ ],
+ "average_daily_users": 6229783,
+ "categories": {
+ "android": [
+ "security-privacy"
+ ],
+ "firefox": [
+ "privacy-security"
+ ]
+ },
+ "contributions_url": "",
+ "created": "2015-04-25T07:26:22Z",
+ "current_version": {
+ "id": 5596914,
+ "compatibility": {
+ "firefox": {
+ "min": "78.0",
+ "max": "*"
+ },
+ "android": {
+ "min": "79.0",
+ "max": "*"
+ }
+ },
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/versions/5596914",
+ "is_strict_compatibility_enabled": false,
+ "license": {
+ "id": 6,
+ "is_custom": false,
+ "name": {
+ "en-US": "GNU General Public License v3.0"
+ },
+ "url": "http://www.gnu.org/licenses/gpl-3.0.html"
+ },
+ "release_notes": {
+ "en-US": "See complete release notes for <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/57eadd553bcb629be4f7576c9aa9a808cc5e918a6ea1413dff063281d4896978/https%3A//github.com/gorhill/uBlock/releases/tag/1.51.0\" rel=\"nofollow\">1.51.0</a>.\n\n<b>Fixes / changes</b>\n\n<ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/a578674b791ed67b926b31004613b0631d0cc73efc2d263385eb842dc0dff09f/https%3A//github.com/gorhill/uBlock/commit/ee0649329c59\" rel=\"nofollow\">Remove obsolete web<em>accessible</em>resources</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7071379ad6c88fec4e40b3ff8165d4fd3ecdaae2fbf0ea145fcf21eb0ea86e43/https%3A//github.com/gorhill/uBlock/commit/cdf385f5f46e\" rel=\"nofollow\">Add missing (deprecated) method to google ima</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/bf46690e10ed8a086d9d50080177c67380aeb5cc7945d96acdbde3fdf7aa9466/https%3A//github.com/gorhill/uBlock/commit/aa6baf9a29db\" rel=\"nofollow\">Fix regression in handling of experimental <code>header=</code> filter option</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f2328063ec5c7512d5899f4b7db7324d42864cea3678aff50af908219b976094/https%3A//github.com/gorhill/uBlock/commit/0da7e12ea4a0\" rel=\"nofollow\">Only already normalized CSS selectors can be fast path-compiled</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/e271cd306c9578e0ed17c785486d183abc8eed2408c7f3fb22eca3afff3ab478/https%3A//github.com/gorhill/uBlock/commit/ec0698196563\" rel=\"nofollow\">Improve compatibility with AdGuard's scriptlets</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f42373a2522c9739807b48d956a9437aa9c7e6b6b4a613a24d412d309ac8c037/https%3A//github.com/gorhill/uBlock/commit/5ebdbf3e2439\" rel=\"nofollow\">Add static network filter option: <code>permissions</code></a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f502abdb178413a344a43d8d0e57463d6c9a7ef3c01a437097ec6a2fd97e1350/https%3A//github.com/gorhill/uBlock/commit/786d9b2212e9\" rel=\"nofollow\">Add <code>set-attr</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/46449872121aa9a39f8fe5f1499dccec24fc094eb72848f37974606abd562997/https%3A//github.com/gorhill/uBlock/commit/fea6f7f311a5\" rel=\"nofollow\">Do not bail too early when trapping properties in <code>acs</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/76b28688717445a36e5ee48387a0e7392370e22d5e3a2fbb53d2b965f54f1eee/https%3A//github.com/gorhill/uBlock/commit/80b3f3c3c020\" rel=\"nofollow\">Fix regression in cloud storage import of \"Filter lists\" pane</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/eb0f0fdfbd50904d7cd8e260b62d0ca4b233c8f9d8a18f9abb31472026e07b23/https%3A//github.com/gorhill/uBlock/commit/083a318090e3\" rel=\"nofollow\">Add <code>set-session-storage-item</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f125a8290e220a02336532a360ea37a7a4ff1c4402a5f518785b7354a7a491d8/https%3A//github.com/gorhill/uBlock/commit/60b21b142268\" rel=\"nofollow\">Prevent negative position when widget size is greater than viewport size</a><ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/e49ec519e35ce90b1ce42475f48affc9a34e97640a60ca13a6ff3d23b58b0ade/https%3A//github.com/gorhill/uBlock/commit/b44815f0c8f0\" rel=\"nofollow\">Ensure no negative value for <code>top</code> property of floating widget in logger</a></li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/2398a39690c82a85f3fb23710721b490fedd503cf3003fdad3b23a8193fb257d/https%3A//github.com/gorhill/uBlock/commit/622cda2cdf91\" rel=\"nofollow\">Add visual hint when not all sublists are enabled</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f549cc526fa10665161c86930eccf00650b123e9c5f0b9ddc11038d7349600cd/https%3A//github.com/gorhill/uBlock/commit/33b409dd5bae\" rel=\"nofollow\">Add support for AdGuard's noop (<code>_</code>) network filter option</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/c649c1ddb8b1d7f280a0687edeae15b9c7b65d4101901337eff3cb8acb6370f0/https%3A//github.com/gorhill/uBlock/commit/5d6e10318662\" rel=\"nofollow\">Add \"tabless\" filter expression for logger output</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/3f7e7d0a06e918e4c8b3f8bb91d33e700a99ff9c2988069b609f614228e6ccf4/https%3A//github.com/gorhill/uBlock/commit/194354cd5d77\" rel=\"nofollow\">Add support for logical expressions to <code>!#if</code> directive</a><ul><li>Also added support for <code>!#else</code></li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f1e827e5c4e256a0c832f985f6a21bb39743a5dfecd5d4014ae3aa9b09d9e116/https%3A//github.com/gorhill/uBlock/commit/7867c2512807\" rel=\"nofollow\">Add resource aliases for increased compatibility with AdGuard lists</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6f42d070e087002ada8f37b36176d8d40d40dafcc5f5e9a1050c689ea079017f/https%3A//github.com/gorhill/uBlock/commit/fd036a51ee20\" rel=\"nofollow\">Add compatibility with AdGuard's <code>#%#//scriptlet(...)</code> syntax</a><ul><li>Also added support for quoted parameters in <code>##+js(...)</code> syntax</li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/50632bd7b34e7e7185c7642640111d9f2ba5493bea1e3ed1c517702302fd4938/https%3A//github.com/gorhill/uBlock/commit/8b7a5264deb4\" rel=\"nofollow\">Fix syntax highlighter throwing with invalid patterns</a></li><li>...</li></ul>\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7c755863346ab5ff9e77aeee9ffcd4ef47f343f5c7d2f2f143dcd474b8558641/https%3A//github.com/gorhill/uBlock/compare/1.50.0...1.51.0\" rel=\"nofollow\">Commits history since last version</a>."
+ },
+ "reviewed": "2023-07-25T09:58:22Z",
+ "version": "1.51.0",
+ "files": [
+ {
+ "id": 4141256,
+ "created": "2023-07-19T23:09:25Z",
+ "hash": "sha256:8b73468bc233a11dd2895219466381783d19123857dd0b6fd16a01820fca4834",
+ "is_restart_required": false,
+ "is_webextension": true,
+ "is_mozilla_signed_extension": false,
+ "platform": "all",
+ "size": 3538418,
+ "status": "public",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4141256/ublock_origin-1.51.0.xpi",
+ "permissions": [
+ "dns",
+ "menus",
+ "privacy",
+ "storage",
+ "tabs",
+ "unlimitedStorage",
+ "webNavigation",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ "http://*/*",
+ "https://*/*",
+ "file://*/*",
+ "https://easylist.to/*",
+ "https://*.fanboy.co.nz/*",
+ "https://filterlists.com/*",
+ "https://forums.lanik.us/*",
+ "https://github.com/*",
+ "https://*.github.io/*",
+ "https://*.letsblock.it/*"
+ ],
+ "optional_permissions": [],
+ "host_permissions": []
+ }
+ ]
+ },
+ "default_locale": "en-US",
+ "description": {
+ "ar": "مانع إعلانات كفوء: خفيف على الذاكرة و المعالج, على الرغم من قدرته على تحميل و تطبيق الألاف من الفلاتر أكثر من بعض أشهر مانعي الإعلانات.\n\nتوضيح عام لكفاءة الإضافة: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nالإستخدام: زر التشغيل الكبير في النافذة المنبثقة هو لتعطيل أو تشغيل uBlock للموقع الحالي. هو ينطبق على الموقع الحالي فقط، و ليس زر تشغيل عام.\n\n***\n\nمع مرونته، هو أكثر من مجرد \"مانع إعلانات\": بإمكانه أيضا قراءة و إنشاء فلاتر من ملفات الإستقبال.\n\nفلاتر حديثة، هذه القوائم من الفلاتر يتم تحميلها و تطبيقها:\n\n- EasyList\n- قائمة خادم الإعلانات لـPeter Lowe\n- EasyPrivacy\n- نطاقات البرامج الضارة\n\nيوفر لك قوائم أكثر لتختار منها إذا كنت ترغب:\n\nقائم التتبع المحسنة لـFanboy\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- و الكثير من القوائم الأخرى.\n\nطبعا، كلما مكَّنتَ فلاتر أكثر، كلما ازداد أثرها على الذاكرة. حتى مع الرغم من إضافة القوائم الإضافية لـFanboy، و قوائم تتبع إعلان hpHost، ما زال بإمكان uBlock₀ العمل بأدنى أثر على الذاكرة أفضل من بعض أشهر قوائم التتبع.\n\nأيضا، كن على علم أن تحديد بعض من هذه القوائم الإضافية قد يؤدي إلى إمكانية أعلى لتعطيل المواقع -- خصوصا تلك القوائم التي تستخدم عادة كملفات مضيفة.\n\n***\n\nبدون وجود قوائم الفلترات, هذه الإضافة عديمة القيمة. إذن إن كانت لديك الرغبة في المساهمة، فكر في أولئك الذين يعملون بجد لصيانة قوائم الفلترات التي تستخدمها، التي تمت إتاحتها لك لتسخدمها مجَّاناََ.\n\n***\n\nمجاناً.\nمفتوح المصدر مع رخصة (GPLv3)\nللمستخدمين من طرف مستخدمين أخرين.\n\nالمساهمون في Github:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nالمساهمون في Crowdin:\n <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nالإضافة في قيد الإنجاز، خذ هذا في عين الإعتبار عندما تستعرضها.\n\nسجل التغييرات للمشروع:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "bg": "Ефикасен блокер: с малко използване на паметта и процесора, но същевременно способен да зарежда и налага хиляди допълнителни филтри в сравнение с други популярни блокери.\n\nИлюстрация на неговата ефикасност: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nИзползване: Големият бутон за \"Включване\" в изскачащият прозорец служи за трайно включване/изключване на uBlock за текущия сайт. Той се отнася само за текущия сайт, не е глобален бутон за включване.\n\n***\n\nГъвкав, той е повече от \"блокер на реклами\": може да чете и създава филтри от хост файлове.\n\nПри първоначално използване са заредени и наложени следните списъци с филтри:\n\n- EasyList\n- Списък с рекламни сървъри от Peter Lowe\n- EasyPrivacy\n- Вредоносни домейни\n\nAко желаете, на разположение са допълнителни списъци, които да изберете:\n\n- разширен проследяващ списък от Fanboy\n- хост файл от Dan Pollock\n- рекламни и проследяващи сървъри от hpHosts\n- MVPS HOSTS\n- Spam404\n- и много други\n\nРазбира се, колкото повече списъци включите, толкова по-голямо е използването на паметта. Въпреки това, дори и след добавяне на двата допълнителни списъка от Fanboy, рекламните и проследяващи сървъри от hpHosts, uBlock₀ използва по-малко памет в сравнение с други много популярни блокери.\n\nСъщо така, имайте предвид, че избирането на някои от допълнителните списъци може да доведе до по-голяма вероятност от неправилно функциониране на уебсайтове -- особено тези списъци, които по принцип се използват като хост файлове.\n\n***\n\nБез предварително зададените списъци с филтри, това разширение е нищо. Така че, ако някога наистина искате да допринесете с нещо, помислете за хората, работещи усилено по поддържането на списъците с филтри, предоставени ви за безплатно използване от всички.\n\n***\n\nБезплатно.\nОтворен код с публичен лиценз (GPLv3)\nЗа потребители от потребителите.\n\nСътрудници @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nСътрудници @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nТова е доста ранна версия, имайте го предвид, когато я разглеждате.\n\nСписък с промени на проекта:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "bn-BD": "একটি দক্ষ প্রতিরোধক: মেমরি ও CPU-র পদচিহ্নের জন্য সহজ, এবং এখনো অন্যান্য জনপ্রিয় ব্লকার বা অবরোধকারীর থেকে হাজার হাজার অধিক ফিল্টারকে লোড এবং জোরদার করতে পারে।\n\nএটির কার্যকারিতার সচিত্র সংক্ষিপ্ত বিবরণ: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nব্যবহার: পপআপে বড় পাওয়ার বোতাম স্থায়ীভাবে বর্তমান ওয়েব সাইটের জন্য uBlock সক্রিয়/নিষ্ক্রিয় করবে। এটা শুধুমাত্র বর্তমান ওয়েব সাইটে প্রযোজ্য, এটি একটি বৈশ্বিক পাওয়ার বোতাম নয়।\n\n***\n\nমনীয়, এটি একটি \"ad blocker\"-এর ছেয়েও অধিক: এছাড়াও এটি হোস্ট ফাইল থেকে ফিল্টার পড়তে ও তৈরি করতে পারে।\n\nবাক্সের বাইরের, এই তালিকার ফিল্টারগুলি লোড করে এবং তা প্রয়োগ করে:\n\n- সহজ তালিকা\n- পিটার লো'য়ের বিজ্ঞাপন সার্ভারের তালিকা\n- সহজ গোপনীয়তা\n- ম্যালওয়্যার ডোমেইন\n\n আপনি যদি চান আপনি নির্বাচন করার জন্য আরো তালিকা পাবেন:\n\n- ফানবয়ের উন্নত ট্র্যাকিং তালিকা\n- Dan Pollock-এর হোস্ট ফাইল\n- hpHosts-এর বিজ্ঞাপন এবং ট্র্যাকিং সার্ভার\n- MVPS হোস্টসমূহ\n- স্প্যাম৪০৪\n- এবং আরও অনেক কিছু\n\nঅবশ্যই, যতবেশি ফিল্টার সক্রিয় করবেন, মেমরি পদচিহ্ন ততবেশি হবে। এমনকি Fanboy-এর দুটি অতিরিক্ত তালিকা, hpHosts-এর বিজ্ঞাপন এবং ট্র্যাকিং সার্ভার যোগ করার পরেও uব্লক অন্যান্য খুব জনপ্রিয় ব্লকারের থেকে কম মেমরি পদচিহ্ন ব্যবহার করে।\n\nএছাড়াও, এই অতিরিক্ত তালিকার কিছু নির্বাচন ওয়েব সাইট ভাঙ্গনের জন্য উচ্চ সম্ভাবনাময় হয়ে উঠতে পারে তাই সাবধান --- বিশেষকরে এই তালিকাগুলি যা সাধারণত হোস্ট ফাইল হিসেবে ব্যবহার করা হয়।\n\n***\n\nফিল্টারের পূর্বনির্ধারিত তালিকা ছাড়া, এই এক্সটেনশনটি কিছুই নয়। তাই কখনও যদি আপনি সত্যিই কিছু অবদান রাখতে চান, আপনার ব্যবহার করা ফিল্টার তালিকা রক্ষণাবেক্ষণের জন্য কঠোর পরিশ্রম করা সেই সব মানুষের করা কথা চিন্তা করুন যারা এই সব বিনামূল্যে ব্যবহারের জন্য উপলব্ধ করেছেন।\n\n***\n\nবিনামূল্যে।\nপাবলিক লাইসেন্সসহ মুক্ত উৎসের (GPLv3)\nব্যবহারকারীদের দ্বারা ব্যবহারকারীদের জন্য।\n\nঅবদানকারীগণ @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nঅবদানকারীগণ @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nএটি একটি প্রাথমিক সংস্করণ, আপনার পর্যালোচনার সময় তা মনে রাখুন।\n\nপ্রকল্পের পরিবর্তন লগ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ca": "Un bloquejador eficient: el consum de memòria i de processador és baix però, no obstant això, pot carregar i aplicar milers de filtres més que altres bloquejadors coneguts.\n\nGràfic de l'eficiència: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nÚs: El gran botó d'engegada de la finestra emergent serveix per a desactivar/activar permanentment el uBlock per al lloc web actual. No és un botó d'engegada general de l'extensió.\n\n***\n\nFlexible, és més que un \"bloquejador d'anuncis\": també pot llegir i crear filtres a partir de fitxers hosts.\n\nPer defecte, es carreguen i s'apliquen aquestes llistes de filtres:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Dominis de malware\n\nSi voleu, podeu seleccionar altres llistes disponibles:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- I altres\n\nÒbviament, com més filtres activeu, més gran serà el consum de memòria. Però fins i tot després d'afegir dues llistes extra de Fanboy, hpHosts’s Ad and tracking servers, el uBlock₀ encara té un consum de memòria inferior al d'altres bloquejadors coneguts.\n\nTambé heu de ser conscient que seleccionant algunes d'aquestes llistes extra és més probable trobar-se amb llocs webs inservibles -- especialment aquelles llistes que s'utilitzen normalment com a fitxer de hosts.\n\n***\n\nSense les llistes predefinides de filtres, aquesta extensió no és res. Així que, si en cap moment voleu fer una aportació, penseu en les persones que treballen durament per a mantenir les llistes de filtres que utilitzeu, a disposició de tothom de manera gratuïta.\n\n***\n\nLliure.\nCodi obert amb llicència pública (GPLv3)\nPer usuaris per a usuaris.\n\nCol·laboradors a Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nCol·laboradors a Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nAquesta és, en certa manera, una versió primitiva. Tingueu-ho en compte quan en doneu la vostra opinió.\n\nRegistre de canvis del projecte:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "cs": "Efektivní blokovač: nezanechává velké stopy, nezatěžuje paměť a CPU, a přesto může načítat a využívat o několik tisíc filtrů více, než jiné populární blockery.\n\nGrafický přehled jeho účinnosti: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPoužití: Velký vypínač ve vyskakovacím okně trvale povolí/zakáže uBlock pro otevřenou stránku. Funguje pouze pro aktivní webovou stránku, není to obecný vypínač.\n\n***\n\nFlexibilní, více než jen \"blokovač reklam\": umí také číst a vytvářet filtry z hosts souborů.\n\nPo instalaci jsou načteny a použity tyto filtry:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPokud chcete, můžete si vybrat tyto další filtry:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- A mnoho dalších\n\nČím více filtrů je povoleno, tím je samozřejmě větší stopa v paměti. I přesto má ale uBlock₀ i po přidání dvou dalších seznamů od Fanboye a \"hpHosts’s Ad and tracking servers\" menší vliv na paměť než mnohé další velmi populární blockery.\n\nDále mějte na paměti, že vybírání více filtrů zvyšuje šanci chybného zobrazení webů -- především u seznamů, které se normálně používají jako hosts soubory.\n\n***\n\nBez předvolených seznamů filtrů by toto rozšíření bylo k ničemu. Pokud tedy opravdu budete chtít něčím přispět, myslete na lidi, kteří spravují Vámi používané seznamy filtrů a uvolňují je pro všechny zdarma.\n\n***\n\nSvobodný software.\nOpen source s veřejnou licencí (GPLv3)\nOd uživatelů pro uživatele.\n\nPřispěvatelé na Githubu: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPřispěvatelé na Crowdinu: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nJde o poměrně ranou verzi, mějte to na paměti při recenzování.\n\nChange log projektu:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "da": "En effektiv blocker: let på hukommelse og CPU forbrug,. Kan indlæse og anvende tusindvis af flere filtre end andre populære blockere derude.\n\nIllustreret oversigt over effektiviteten: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-compared\n\nAnvendelse: Den Store power knap i pop-up-vinduet kan permanent deaktivere/aktivere uBlock på det aktuelle websted. Dette gælder kun for det aktuelle websted, det er ikke en global afbryderknap.\n\n***\n\nFleksibel, det er mere end en \"ad blocker\": den kan også læse og oprette filtre fra hosts-filer.\n\nFra starten af er disse lister over filtre indlæst og anvendt:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nFlere lister er tilgængelige hvis du ønsker det:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Osv.\n\nSelvfølgelig vil flere aktive filtre betyde højere hukommelsesforbrug. Selv efter tilføjelse af Fanboys to ekstra lister, og hpHosts’s Ad and tracking server, har uBlock₀ stadig et lavere hukommelsesforbrug end andre blockere derude.\n\nVær desuden opmærksom på, at hvis du vælger nogle af disse ekstra lister kan det føre til højere sandsynlighed for, at webstedet bliver vist forkert - især de lister der normalt anvendes som hosts-fil.\n\n***\n\nUden de forudindstillede lister med filtre er denne udvidelse intet. Hvis du nogensinde virkelig ønsker at bidrage med noget, så tænk på de mennesker der arbejder hårdt for at vedligeholde de filterlister du bruger, som alle blev stillet gratis til rådighed for alle.\n\n***\n\nGratis.\nOpen source med offentlig licens (GPLv3)\nFor brugere, af brugere.\n\nBidragydere @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nBidragydere @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDette er en tidlig version. Hav dette i tankerne når du skriver en anmeldelse.\n\nProjekt changelog:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "de": "Ein effizienter Blocker: Geringer Speicherbedarf und niedrige CPU-Belastung - und dennoch werden Tausende an Filtern mehr angewendet als bei anderen populären Blockern.\n\nEin illustrierter Überblick über seine Effizienz: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nBenutzung: Der An-/Ausschaltknopf beim Klicken des Erweiterungssymbols dient zum An-/Ausschalten von uBlock auf der aktuellen Webseite. Dies wirkt sich also nur auf die aktuelle Webseite aus und nicht global.\n\n***\n\nuBlock ist flexibel, denn es ist mehr als ein \"Werbeblocker\": Es verarbeitet auch Filter aus mehreren hosts-Dateien.\n\nStandardmäßig werden folgende Filterlisten geladen und angewandt:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nAuf Wunsch können zusätzliche Listen ausgewählt werden:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- etc.\n\nNatürlich ist der Speicherbedarf umso höher, desto mehr Filter angewandt werden. Aber selbst mit den zwei zusätzlichen Listen von Fanboy und hpHosts’s Ad and tracking servers ist der Speicherbedarf von uBlock₀ geringer als bei anderen sehr populären Blockern.\n\nBedenke allerdings, dass durch die Wahl zusätzlicher Listen die Wahrscheinlichkeit größer wird, dass bestimmte Webseiten nicht richtig geladen werden - vor allem bei Listen, die normalerweise als hosts-Dateien verwendet werden.\n\n***\n\nOhne die vorgegebenen Filterlisten ist diese Erweiterung nichts. Wenn du also etwas beitragen möchtest, dann denke an die Menschen, die hart dafür arbeiten, die von dir benutzten Filterlisten zu pflegen, und diese für uns alle frei verfügbar gemacht haben.\n\n***\n\nKostenlos.\nOpen source mit Public License (GPLv3)\nFür Benutzer von Benutzern.\n\nMitwirkende @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nMitwirkende @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDies ist eine ziemlich frühe Version - bitte denke daran, wenn du sie bewertest.\n\nChange log des Projekts:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "el": "Ένας αποτελεσματικός αναστολέας διαφημίσεων: παρόλο το ελαφρύ του αποτύπωμα στη μνήμη και τον επεξεργαστή μπορεί να εφαρμόσει χιλιάδες περισσότερα φίλτρα σε σχέση με άλλους δημοφιλείς blockers.\n\nΑπεικονιζόμενη επισκόπηση της αποτελεσματικότητάς του: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nΧρήση: Το μεγάλο πλήκτρο απενεργοποίησης/ενεργοποίησης στο αναδυόμενο παράθυρο, χρησιμεύει στην εναλλαγή κατάστασης του uBlock για τον τρέχοντα ιστότοπο. Η εφαρμογή της ρύθμισης αυτής γίνεται μόνο για τον τρέχοντα ιστότοπο και δεν επιβάλλεται καθολικά.\n\n***\n\nΕυέλικτος, είναι πολλά περισσότερα από ένας απλός \"ad blocker\": μπορεί επιπλέον να διαβάζει και να δημιουργεί φίλτρα από αρχεία hosts.\n\nΚατά προεπιλογή, οι λίστες φίλτρων που φορτώνονται και επιβάλλονται είναι οι εξής:\n\n- EasyList\n- Λίστα διακομιστών διαφημίσεων του Peter Lowe\n- EasyPrivacy\n- Κακόβουλοι τομείς\n\nΕπιπλέον λίστες είναι διαθέσιμες για να επιλέξετε εάν το επιθυμείτε:\n\n- Ενισχυμένη Ιχνωσική Λίστα του Fanboy\n- Αρχείο hosts του Dan Pollock\n- Διαφημίσεις και διακομιστές ίχνωσης hpHosts\n- MVPS HOSTS\n- Spam404\n- και πολλές άλλες\n\nΦυσικά, όσο περισσότερα φίλτρα ενεργοποιούνται, τόσο αυξάνεται το αποτύπωμα της μνήμης. Ωστόσο, ακόμη και μετά από την προσθήκη δυο επιπλέον λιστών, του Fanboy και της λίστας διαφημίσεων και διακομιστών ίχνωσης hpHosts, το uBlock₀ συνεχίζει να έχει χαμηλότερο αποτύπωμα μνήμης από άλλους δημοφιλείς αναστολείς.\n\nΕπίσης, έχετε υπ'όψην ότι διαλέγοντας μερικές από τις έξτρα λίστες μπορεί να οδηγήσει σε πιθανό σφάλμα στην ιστοσελίδα -- ειδικά εκείνες που κανονικά χρησιμοποιούνται σαν host αρχεία.\n\n***\n\nΧωρίς τις υπάρχουσες λίστες φίλτρων, αυτή η επέκταση δεν έχει καμία αξία. Εάν ποτέ λοιπόν θελήσετε πραγματικά να συνεισφέρετε κάτι, αναλογιστείτε τους ανθρώπους που εργάζονται σκληρά για να διατηρήσουν τις λίστες φίλτρων που χρησιμοποιείτε, οι οποίες διατέθηκαν προς χρήση σε όλους, δωρεάν.\n\n***\n\nΔωρεάν.\nΑνοιχτού κώδικα με άδεια δημόσιας χρήσης (GPLv3)\nΑπό τους χρήστες για τους χρήστες.\n\nΣυνεισφέροντες @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nΣυνεισφέροντες @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nΕίναι μια αρκετά πρόωρη έκδοση, κρατήστε το υπόψη κατά την αξιολόγηση.\n\nΑρχείο αλλαγών του έργου:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "en-US": "uBlock Origin is <b>not</b> an \"ad blocker\", it's a wide-spectrum content blocker with CPU and memory efficiency as a primary feature.\n\n***\n\nOut of the box, uBO blocks ads, trackers, coin miners, popups, etc. through the following lists of filters, enabled by default:\n\n- EasyList (ads)\n- EasyPrivacy (tracking)\n- Peter Lowe’s Ad server list (ads and tracking)\n- Online Malicious URL Blocklist\n- uBO's own lists\n\nMore lists are available for you to select if you wish:\n\n- EasyList Cookie\n- Fanboy Annoyances\n- AdGuard Annoyances\n- Dan Pollock’s hosts file\n- And many others\n\nAdditionally, you can point-and-click to block JavaScript locally or globally, create your own global or local rules to override entries from filter lists, and many more advanced features.\n\n***\n\nFree.\nOpen source with public license (GPLv3)\nFor users by users.\n\nIf ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\n<ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/788d66e7299bdfb1da05832994551640d0ad441e148a3e29afe8dd0a5a90800c/https%3A//github.com/gorhill/uBlock%23ublock-origin\" rel=\"nofollow\">Documentation</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">Release notes</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/32c3d6819f5263e56c265042e8d34e2da4d974e73a7ad55a81786d8995cf65a9/https%3A//www.reddit.com/r/uBlockOrigin/\" rel=\"nofollow\">Community support @ Reddit</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">Contributors @ GitHub</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">Contributors @ Crowdin</a></li></ul>",
+ "es": "Un bloqueador eficiente: capaz de cargar y aplicar miles más de filtros en comparación con otros populares bloqueadores, manteniendo un mínimo consumo de memoria y CPU.\n\nEjemplo con imágenes ilustrando su eficiencia (en inglés): <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUso: El botón grande de apagado/encendido en la ventana emergente de la extensión, es para deshabilitar/habilitar uBlock₀ permanentemente en el sitio web actual. Aplica solo al sitio web actual, no activa o desactiva la extensión de forma general.\n\n***\n\nFlexible, es más que un \"bloqueador de anuncios\": también puede leer y crear filtros desde archivos hosts.\n\nPor defecto ya trae configuradas las siguientes listas de filtros:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nOtras listas disponibles pueden ser seleccionadas, si se desea:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Y muchas más\n\nPor supuesto, mientras más filtros se activen, mayor será el consumo de memoria. No obstante, incluso después de agregar las dos listas adicionales de \"Fanboy's\" y la \"hpHosts’s Ad and tracking servers\", uBlock₀ consume menos memoria que otros bloqueadores similares.\n\nTambién tenga en cuenta que seleccionar algunas de estas listas adicionales puede conducir a una mayor probabilidad de aparición de problemas al mostrar un sitio web -- especialmente las listas utilizadas normalmente como archivo hosts.\n\n***\n\nSin las listas preestablecidas de filtros, esta extensión no sería nada. Así que si alguna vez realmente quieres aportar algo, piensa en las personas que trabajan duro para mantener estas listas de filtros, disponibles de forma gratuita para todos.\n\n***\n\nLibre.\nCódigo abierto con licencia pública (GPLv3)\nHecho para usuarios por los usuarios.\n\nColaboradores @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nColaboradores @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7bae4395c4e5926bb237c1ef9b0f391cb005dbdbf58f4c9e47298db9bb6d1f57/https%3A//crowdin.com/project/ublock\" rel=\"nofollow\">https://crowdin.com/project/ublock</a>\n\n***\n\nRegistro de cambios del proyecto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "eu": "Blokeatzaile eraginkor bat: Memoria eta PUZ erabileran arina da, eta hala ere beste blokeatzaile ezagun batzuk baino milaka iragazki gehiago kargatu eta ezarri ditzake.\n\nBere eraginkortasunaren adibide grafikoa: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nErabilera: Laster-leihoko pizte botoi handia uBlock uneko gunean behin betiko gaitu edo desgaitzeko da. Uneko guneari dagokio soilik, ez da botoi orokor bat.\n\n***\n\nMalgua, iragarki blokeatzaile bat baino gehiago da, ostalarietako iragazkiak sortu eta irakurri ditzake ere.\n\nLehenetsita, honako iragazki zerrendak kargatu eta ezartzen ditu:\n\n- EasyList\n- Peter Loweren iragarki zerbitzarien zerrenda\n- EasyPrivacy\n- Malware domeinuak\n\nZerrenda gehiago dituzu eskura hautatzeko hala nahiez gero:\n\n- Fanboyren hobetutako jarraipen zerrenda\n- Dan Pollocken ostalari zerrenda\n- hpHostsen iragarki eta jarraipen zerbitzariak\n- MVPS Ostalariak\n- Spam404\n- Eta beste hainbat gehiago\n\nJakina, iragazki gehiago kargatuta memoria erabilera handiagoa da. Hala ere, Fanboyren bi zerrenda gehigarriak eta hpHostsen iragarki eta jarraipen zerbitzariak kargatuta, uBlockek beste blokeatzaile ezagun batzuk baino memoria gutxiago erabiltzen du.\n\nBestalde, kontuan izan zerrenda gehigarri hauetako batzuk gaitzeak guneren bat hausteko aukerak handitzen dituela, batez ere ostalari fitxategi gisa erabili ohi diren zerrendak.\n\n***\n\nLehenetsitako iragazki zerrendarik gabe gehigarri honek ez du ezer egiten. Beraz ezertan lagundu nahi baduzu pentsa ezazu erabiltzen dituzun iragazki zerrendak egunean mantentzeko tinko lanean dabiltzan horietan, guztiek erabiltzeko moduan doan eskuragarri jarri dituztenak.\n\n***\n\nDoan.\nLizentzia libreduna (GPLv3)\nErabiltzaileek erabiltzaileentzat sortua.\n\nParte-hartzaileak @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nParte-hartzaileak @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nNahiko berria da bertsio hau, kontua izan honi buruz idaztean.\n\nProiektuaren aldaketa egunkaria:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fa": "یک بلاکر موثر: نیاز به پردازش حافظه و سی پی یو کمتر و در عین حال اجرای هزاران فیلتر بیشتر از سایر رقبای بلاکر موجود.\n\nبررسی تصویری از کارایی این محصول: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nکاربرد: دکمه ی پاور بزرگ در پنجره برای فعال یا غیر فعال کردن uBlock برای صفحه ی جاری است. فقط برای همین سایت اعمال میشود، دکمه ی پاوری برای تمام سایت ها نیست.\n\n***\n\nانعطاف پذیری آن بیشتر از \"ad blocker\" است: همچنین می تواند فیلتر ها را از هاست میزبان، بخواند و بسازد.\n\nبیرون از جعبه، این لیست فیلترها بارگذاری و اجرا میشوند:\n\n- EasyList\n- لیست سرور تبلیغاتی Peter Lowe\n- EasyPrivacy\n-دامنه های تروجان\n\nاگر میخواهید لیست های بیشتر برای انتخاب شما در دسترس هستند:\n\n- ردیابی لیست پیشرفته ی Fanboy\n- میزبانی فایل Dan Pollock\n- تبلیغ و ردیابی سرور hpHosts\n- هاست های MVPS\n- اسپم 404\n- و بسیاری دیگر\n\nالبته هرچه فیلترهای بیشتری فعال باشند، حافظه ی بیشتری اشغال خواهد شد. با اینحال، حتی پس از اضافه کردن دو لیست اضافی Fanboy و سرور های ردیابی و تبلیغ hpHosts ، میبینیم که uBlock هنوز حافظه پایین تری از دیگر برنامه های مشابه اشغال میکند.\n\nهمچنین، بدانید که انتخاب برخی از این لیست ها ممکن است افزایش احتمال شکستگی وب سایت--به ویژه آنهایی که به طور معمول به عنوان میزبان فایل شناخته میشوند را در پی داشته باشد.\n\n***\n\nبدون فهرست از پیش تعیین شده ی فیلتر، این افزونه هیچ است. پس اگر واقعا می خواهید کمکی کرده باشید، به افرادی فکر کنید که برای حفظ لیست فیلتر مورد استفاده شما سخت کار میکنند که برای استفاده همه به رایگان در دسترس باشد.\n\n***\n\nرایگان.\nمتن باز با مجوز عمومی (GPLv3)\nبرای کاربران توسط کاربران.\n\nمشارکت کنندگان در گیت هاب: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nمشارکت کنندگان در کرادین <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nاین کاملا یک نسخه اولیه است، هنگام بررسی اینرا بخاطر داشته باشید.\n\nتغییرات اخیر پروژه:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fi": "Tehokas mainosten estäjä – käyttää vähän resursseja, mutta silti voit ladata ja pakottaa tuhansia suodatinsääntöjä enemmän kuin muut suositut mainoksia estävät lisäosat.\n\nKuvitettu yleiskatsaus uBlockin tehokkuudesta (englanniksi): <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nKäyttö: Iso virtanappi ponnahdusikkunassa on tarkoitettu pysyvästi estämään/sallimaan uBlock kyseisellä sivulla. Tämä koskee vain nykyistä sivua, ei kaikkia sivuja.\n\n***\n\nJoustava, tämä lisäosa on enemmän kuin perinteinen \"mainosten estäjä\". Voit lukea ja luoda suodattimia myös hosts-tiedostoista.\n\nNämä suodatinlistat ovat automaattisesti ladattuna ja kytketty päälle:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nHalutessasi voit valita käyttöösi lisää listoja:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Ja monia muita\n\nJos sinulla on useita listoja käytössä, keskusmuistia kuluu enemmän. Tästä huolimatta, vaikka lisäisit Fanboyn kaksi lisälistaa ja hpHostsin listan, uBlockilla on silti pienempi muistinkulutus kuin muilla suosituilla mainosten estäjillä.\n\nUseiden listojen lisääminen saattaa aiheuttaa sivujen kaatumisen tai hajoamisen. Etenkin listat, joita käytetään normaalisti hosts-tiedostona, voivat aiheuttaa ongelmia.\n\n***\n\nTämä lisäosa ei tee mitään ilman suodatinlistoja. Jos siis haluat osallistua jotenkin, muistathan kaikki ne ihmiset jotka työskentelevät pitääkseen käyttämäsi suodatinlistat ajan tasalla ja saatavilla ilmaiseksi.\n\n***\n\nIlmainen.\nAvoimen lähdekoodin julkinen lisenssi (GPLv3)\nKäyttäjiltä käyttäjille.\n\nKehittäjät @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKehittäjät @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nOtathan huomioon testatessasi, että käytössäsi on varsin varhainen versio.\n\nProjektin muutosloki:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fr": "uBlock est une extension qui bloque les publicités et les pisteurs, légère en empreinte mémoire et en utilisation du processeur et qui pourtant, est capable d'utiliser et de traiter des milliers de filtres de plus que la plupart des autres bloqueurs.\n\nConsultez cette page en Anglais pour avoir une vue d'ensemble illustrée de son efficacité : <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilisation : Le gros bouton power dans la fenêtre pop-up permet de désactiver/activer en permanence uBlock pour le site Web en cours de consultation. Cela s'applique uniquement au site Web actuel, ce n'est pas un bouton power qui affecte entièrement le fonctionnement de l'extension.\n\n***\n\nFlexible, uBlock ne prend pas en charge que les filtres de type Adblock, elle peut également lire et créer des filtres depuis des fichiers hosts.\n\nPar défaut, ces listes de filtrage sont chargées et traitées :\n\n- EasyList (Liste anti-publicités maintenue fréquemment à jour par la communauté)\n- Peter Lowe’s Ad server list (Liste de serveurs publicitaires maintenue à jour par Peter Lowe)\n- EasyPrivacy (Liste anti-pisteurs maintenue fréquemment à jour par la communauté)\n- Malware domains (Liste de protection contre des domaines malveillants)\n\nDavantage de listes sont disponibles si vous souhaitez renforcer le blocage :\n\n- Fanboy’s Enhanced Tracking List (Liste de protection avancée contre le pistage maintenue à jour par Fanboy)\n- Dan Pollock’s hosts file (Fichier Hosts bloquant publicités, domaines malveillants et autres pisteurs, maintenue fréquemment à jour par Dan Pollock)\n- hpHosts’s Ad and tracking servers (Fichier Hosts bloquant des serveurs publicitaires et des serveurs pistant, maintenue à jour par hpHosts)\n- MVPS HOSTS (Fichier Hosts bloquant publicités, domaines malveillants et autres pisteurs, maintenue à jour par MVPS)\n- Spam404 (Liste de protection contre les spams, maintenue fréquemment à jour par la communauté)\n- Et plein d'autres\n\nBien évidemment, plus vous activez de filtres, plus l'empreinte mémoire augmentera. Pourtant, même après avoir ajouté deux listes supplémentaires crées par Fanboy et le fichier Hosts d'hpHosts, uBlock₀ utilise moins de mémoire vive que tous les autres bloqueurs de pubs populaires.\n\nVeuillez tout de même prendre en compte que le fait de choisir parmi ces listes supplémentaires peut conduire à quelques incompatibilités sur les sites Web que vous visitez, bien que ces listes soient maintenues à jour par leurs auteurs.\n\n***\n\nSans les listes prédéfinies de filtres, cette extension (comme d'autres) ne serait rien. Alors si vous tenez vraiment à contribuer d'une quelconque manière, pensez aux personnes travaillant dur pour maintenir à jour ces listes de filtres que vous utilisez, qui plus est proposées gratuitement à tout le monde.\n\n***\n\nGRATUIT.\nSource libre avec une licence publique GPLv3\nFait par des utilisateurs pour des utilisateurs.\n\nContributeurs @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContributeurs @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nIl s'agit d'une version relativement préliminaire, veuillez garder ça à l'esprit lors de votre évaluation de l'extension.\n\nConsultez ici en Anglais le Journal des changements concernant le projet :\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "he": "חוסם יעיל: חותמת נמוכה של המעבד והזיכרון, ועדיין יכול לטעון ולאפשר אלפי מסננים יותר מאשר חוסמים פופולריים אחרים.\n\nסקירה כוללת על היעילות שלו: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nשימוש: לחצן ההפעלה הגדול בחלון הפופאפ הוא בשביל לבטל/להפעיל את uBlock עבור האתר הנוכחי. הוא חל על האתר הנוכחי בלבד, זהו לא לחצן הפעלה גלובלי.\n\n***\n\nגמיש, יותר מ \"חוסם פרסומות\": הוא יכול גם לקרוא וליצור מסננים מקבצי hosts.\n\nהיישר מהקופסה, רשימות המסננים הללו נטענות ומאופשרות:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nרשימות נוספות אלו זמינות לבחירתך אם תרצה:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- ועוד רבים אחרים\n\nכמובן שככל שכמות מסננים גדולה יותר מופעלת, ככה גם חתימת הזיכרון גדולה יותר. ובכל זאת, אפילו לאחר הוספת שתי הרשימות הנוספות של Fanboy ו hpHosts’s Ad and tracking servers, ל uBlock עדיין יש חתימת זיכרון נמוכה יותר מלחוסמים פופולריים אחרים שם בחוץ.\n\nכמו כן, תהיה מודע שבחירה של חלק מהרשימות הנוספות הללו עלולה להוביל בסבירות גבוהה לשבירה של אתרי אינטרנט -- במיוחד הרשימות אשר בדרך כלל משומשות כקובץ hosts.\n\n***\n\nללא רשימות מסננים מוגדרים מראש, תוסף זה לא שווה כלום. אז אם אי פעם תרצה באמת לתרום משהו, תחשוב על האנשים שעובדים לילות כימים כדי לתחזק את רשימות המסננים שאתה משתמש בהן, אשר הובאו לשימוש על ידי כולם ללא כל תשלום.\n\n***\n\nחינם.\nקוד פתוח עם רשיון ציבורי (GPLv3)\nבשביל המשתמשים על ידי המשתמשים.\n\nתורמים @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nתורמים @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nקח בחשבון שזוהי גרסה מוקדמת בזמן הסקירה שלך.\n\nרשימת השינויים של הפרויקט:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "hu": "Egy hatékony blokkoló: kíméletes a processzorral és a memóriával, mégis képes nagyságrendekkel több szűrő betöltésére és alkalmazására a többi népszerű blokkolóhoz viszonyítva.\n\nÁttekintés a hatékonyságáról: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nHasználat: A nagy bekapcsológomb a kiegészítő tiltására/engedélyezésére szolgál a jelenlegi webhelyen. A gomb kizárólag a jelenlegi webhelyre érvényes, nem egy globális kapcsoló.\n\n***\n\nTöbb mint egy egyszerű reklámblokkoló: képes hosts fájlok bejegyzéseit is értelmezni, és azokból szűrőket létrehozni.\n\nAlapértelmezetten a következő szűrőlisták kerülnek betöltésre és alkalmazásra:\n\n- EasyList\n- Peter Lowe hirdetési szerverlistája\n- EasyPrivacy\n- Kártékony domainek\n\nEgyéb listák is kiválaszthatók a felhasználó igénye szerint:\n\n- Fanboy bővített követők listája\n- Dan Pollock hosts fájlja\n- hpHosts hirdetés és követőszerverek listája\n- MVPS HOSTS\n- Spam404\n- És sok más\n\nTermészetesen, több szűrő használatával a memóriaigény is növekszik. Ennek ellenére Fanboy két extra listája és a hpHosts (reklám és követőszerverek) lista hozzáadásával a uBlock memóriafogyasztása még mindig alacsonyabb, mint a legnépszerűbb blokkolóké.\n\nEmellett, néhány extra lista kiválasztásával megnövekszik az esély arra, hogy a webhelyek használhatatlanná válnak -- főleg azon listákról van szó, melyek normál esetben hosts fájlként használatosak.\n\n***\n\nA szűrőlisták nélkül a kiegészítő nem sokat érne. Tehát, ha valaha is eszedbe jutna támogatást kínálni, akkor előbb gondolj azokra, akik keményen dolgoznak a listák karbantartásával, illetve ingyenesen hozzáférhetővé teszik azokat mindenki számára.\n\n***\n\nIngyenes.\nNyílt forráskódú nyilvános licenccel (GPLv3)\nFelhasználóknak felhasználóktól.\n\nKözreműködők a Github-on: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKözreműködők a Crowdin-en: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEz még egy elég korai verzió, amit illik szem előtt tartani értékeléskor.\n\nVáltozások listája:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "id": "Pemblokir yang efisien: ringan penggunaan memori dan CPU, namun dapat memuat dan menjalankan ribuan filter lain dibanding pemblokir populer lain di luar sana.\n\nRingkasan ilustrasi efisiensi: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-Compared\n\nPenggunaan: Tombol daya yang besar dalam popup berfungsi menonaktifkan/mengaktifkan uBlock secara permanen untuk situs yang sedang dibuka. Berlaku untuk situs yang sedang dibuka saja, bukan tombol daya global.\n\n***\n\nFleksibel, lebih dari sekedar \"pemblokir iklan\": juga dapat membaca dan membuat filter dari berkas host.\n\nLangsung bekerja, daftar filter berikut ini dimuat dan dijalankan:\n\n- EasyList\n- Daftar server iklan Peter Lowe\n- EasyPrivacy\n- Domain malware\n\nJika anda ingin, masih banyak tersedia daftar lain untuk anda pilih:\n\n- Daftar Pelacakan Fanboy yang DItingkatkan\n- Berkas host Dan Pollock\n- Server iklan dan pelacakan hpHosts\n- HOST MVPS\n- Spam404\n- dan banyak lainnya\n\nTentu saja, semakin banyak filter yang diaktifkan, semakin besar penggunaan memori. Namun, bahkan setelah menambahkan 2 daftar ekstra Fanboy, server iklan dan pelacakan hpHosts, penggunaan memori uBlock masih lebih kecil dibanding pemblokir iklan populer lain di luar sana.\n\nPerlu diketahui juga bahwa memilih beberapa daftar ekstra juga berpeluang lebih tinggi menyebabkan kerusakan situs -- terutama daftar yang biasanya digunakan sebagai berkas host.\n\n***\n\nTanpa daftar filter yang ada, ekstensi ini bukanlah apa-apa. Jadi, jika Anda benar-benar ingin berkontribusi sesuatu, berpikirlah tentang orang-orang yang bekerja keras mengelola daftar filter yang anda gunakan, yang dibuat dan tersedia untuk digunakan oleh semua dengan gratis.\n\n***\n\nGratis.\nSumber terbuka dengan lisensi publik (GPLv3)\nUntuk pengguna oleh pengguna.\n\nKontributor @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKontributor @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nMasih dalam versi yang sangat awal, mohon diingat ketika anda membuat ulasan.\n\nCatatan perubahan proyek:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "it": "uBlock è un efficiente ad-blocker: occupa poca memoria e poca CPU, ma può usare migliaia di filtri in più rispetto ad altri software simili.\n\nConsulta questa pagina (in inglese) per verificare la sua efficacia <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUso: il pulsante power nel popup serve per disabilitare/abilitare permanentemente uBlock nel sito che stai visitando. e non serve per disabilitare/abilitare l'estensione.\n\n***\n\nMolto più che un ad-blocker: può anche creare filtri dal file host.\n\nPer default sono attivate queste liste:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPuoi anche attivare moltre altre liste:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Ecc.\n\nOvviamente, più liste attivi, maggiore sarà l'impatto sulla memoria. Anche aggiungendo altre due liste di Fanboy, ad di hpHosts e tracking server, uBlock userà meno memoria di molti altri ad-blocker.\n\nSelezionando alcuni di questi filtri può portare ad una maggiore probabilità di problemi nel visualizzare alcuni siti web.\n\n***\n\nSenza queste liste di filtri, questa estensione non è niente. osì se vuoi contribuire, pensa alle persone che lavorano duramente per mantenere queste liste che stai usando, che sono disponibili gratuitamente.\n\n***\n\nGratuito.\nOpen source with public license (GPLv3)\nFatto dagli utenti per gli utenti.\n\nCollaboratori @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nCollaboratori @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nQuesta è una versione preliminare, ricordalo quando scriverai una recensione.\n\nPer leggere le novità di ogni versione consulta questa pagina (In Inlgese):\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ja": "効率的なブロッカー:コンピュータのメモリとCPUのフットプリントはより少なく\n、別の人気のブロッカーよりも何千ものフィルタをロードし、強制的にブロックができます\n\n他ソフトとの比較は以下のとおり: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n使用法: ポップアップに表示される大きな電源ボタンは、現在のサイトでuBlockの有効/無効を切り替えます。 現在のサイトのみに適用されます、グローバルボタンではありません。\n\n***\n\nただの「広告ブロッカー」より柔軟です:ホストファイルを読み込みフィルターを作成できます。\n\n要するに、以下のフィルターが読み込まれ、適用されます:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nご希望であればさらに多くのリストがご利用できます:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Etc.\n\nもちろん、多くのフィルターを適用すれば使用メモリーは増えます。 ただ、それでも、Fanboy's Special Blocking List、Fanboy's Enhanced Tracking List、hpHost's Ad and tracking serversの三つのリストを追加で適用しても、uBlockは他の人気のブロッカーより少ないメモリー消費を実現しています。\n\nそれと、多くのリストの適用は(特にホストファイルとしてよく使われているリスト)ウェブサイトの崩壊を起こしかねないことに注意してください。\n\n***\n\nこの拡張機能は、あらかじめ設定されているフィルターのリストが無ければ意味を成しません。 ですので、何かしらの形で貢献したいと考えることがあった時は、これらのリストを無料で懸命に更新し続けている方々を思い出してください。\n\n***\n\n無料.\nパブリックライセンス(GPLv3)のオープンソース\nユーザーによって作られた、ユーザーのための物。\n\n貢献者 @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n貢献者 @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nこれは割と初期のバージョンですので、それを念頭にレビューをお願いします。\n\nプロジェクト変更ログ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ka": "რეკლამების შედეგიანი შემზღუდავი: მეხსიერებისა და პროცესორის შემსუბუქებული მოხმარება, რეკლამების სხვა შემზღუდავებთან შედარებით, ათასობით მეტი ფილტრის გამოყენების პირობებშიც კი.\n\nშედეგიანობის მიმოხილვა იხილეთ ბმულზე: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nგამოყენება: ჩართვის დიდი ღილაკით, ჩამოშლილ მენიუში, შესაძლებელია uBlock-ის ჩართვა/გამორთვა მიმდინარე ვებსაიტზე. ეს ღილაკი მოქმედებს მხოლოდ არსებულ საიტზე და არ გამოიყენება ზოგადად ჩართვა/გამორთვისთვის.\n\n***\n\nმეტად მოქნილი, ეს არაა უბრალოდ „რეკლამების შემზღუდავი“: ასევე შესაძლებელია hosts ფაილის წაკითხვა და ფილტრების შექმნა.\n\nგარდა ამისა, ნაგულისხმევად ჩართულია და გამოიყენება შემდეგი გასაფილტრი სიები:\n\n- EasyList\n- Peter Lowe-ის სარეკლამო სერვერების სია\n- EasyPrivacy\n- მავნე დომენები\n\nასევე, ხელმისაწვდომია სიები სურვილისამებრ შესარჩევად:\n\n- Fanboy-ის გაუმჯობესებული წესები თვალყურისმდევნებლების შესაზღუდად\n- Dan Pollock-ის hosts ფაილი\n- hpHosts-ის სარეკლამო და თვალყურისმდევნელი სერვერები\n- MVPS HOSTS\n- Spam404\n- და კიდევ ბევრი\n\nრასაკვირველია, რაც უფრო მეტი ფილტრია ჩართული, მეხსიერების გამოყენება იზრდება. თუმცა, Fanboy-ის გაფართოებული წესების, hpHosts-ის სარეკლამო და თვალყურისმდევნელი სერვერების დამატების შემთხვევაშიც კი, uBlock მაინც ნაკლებ მეხსიერებას იყენებს, ვიდრე ყველა სხვა ცნობილი შემზღუდავი პროგრამები.\n\nამასთან, გაითვალისწინეთ, რომ ზოგიერთი დამატებითი წესების შერჩევის შედეგად, შესაძლოა ვებსაიტები არ გამოჩნდეს გამართულად -- განსაკუთრებით იმ წესების შემთხვევაში, რომელიც ჩვეულებრივ, hosts ფაილად გამოიყენება.\n\n***\n\nწინასწარ შედგენილ წესებს, მნიშვნელოვანი ადგილი უჭირავს ამ გაფართოების შედეგიან მუშაობაში. ასე რომ, თუ ოდესმე გადაწყვეტთ ვინმესთვის შემოწირულობის გაღებას, იფიქრეთ იმ ადამიანებზე, რომლებიც თავდაუზოგავად შრომობენ იმ გასაფილტრი წესების მუდმივ განახლებაზე, რომლითაც სარგებლობთ და რომელიც ხელმისაწვდომია ყველასთვის უფასოდ.\n\n***\n\nუფასო.\nღია წყაროს მქონე საჯარო ლიცენზიით (GPLv3)\nმომხმარებლების მიერ, მომხმარებლებისთვის.\n\nწვლილის შემტანები @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nწვლილის შემტანები @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nცვლილებების ჩამონათვალი:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ko": "효율적인 차단기: 메모리와 CPU에 부담이 적고, 다른 인기있는 차단기에 비해 수 천 가지의 필터를 사용할 수 있습니다.\n\n효율성에 대한 소개: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n사용 방법: 해당 웹사이트에서 팝업의 큰 전원 버튼을 눌러 uBlock을 켜고 끌 수 있습니다. 적용은 현재 웹사이트만 적용되며, 전체적으로 적용되지 않습니다.\n\n***\n\n\"AdBlocker\" 보다 더 유연합니다: 호스트 파일들로부터 필터를 만들고 볼 수 있습니다.\n\n특별한 설치 없이도 아래 목록들을 불러오고 적용할 수 있습니다:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\n당신이 원한다면 더 많은 목록을 선택할 수 있습니다:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 그리고 무수히 많은 다른 목록들\n\n물론, 더 많은 필터를 활성화하면 할수록, 메모리 사용량도 높아집니다. 하지만 Fanboy's two extra lists와 hpHosts’s Ad and tracking servers 필터를 추가한 후에도 uBlock₀은 다른 인기있는 차단기에 비해 메모리 사용량이 적습니다.\n\n또, 이러한 일부 추가 목록(특히 일반적으로 사용되는 호스트 파일) 중 선택시 높은 확률로 웹사이트가 파손될 수 있음을 명심해주시기 바랍니다.\n\n***\n\n필터에 필터 목록이 하나도 없다면, 이 확장기능은 아무 쓸모가 없어집니다. 그래서 만약 당신이 정말 어떤것으로든 기여하고 싶을때는, 당신이 사용중인 필터 리스트를 만들고 유지하기 위해 노력중인 사람들을 생각해주세요. 필터들은 모두 무료로 사용이 가능하게 되어있습니다.\n\n***\n\n완전히 무료입니다.\n오픈소스이며, 공개 라이센스(GPLv3)를 따릅니다.\n사용자를 위해, 사용자에 의해 만들어졌습니다.\n\n기여자 @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n기여자 @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n이것은 완전히 초기 버전입니다, 리뷰할 때 이 점을 명심하세요.\n\n프로젝트 변경사항:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "nl": "Een efficiënte adblocker: gebruikt weinig processorkracht en geheugen. Toch kan het duizenden filters meer laden en toepassen dan andere populaire adblockers.\n\nGeïllustreerde efficiëntievergelijking: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nGebruik: met de grote aan-uitknop in de pop-up kan uBlock voor de huidige website permanent worden in- of uitgeschakeld. Het wordt alleen op de huidige website toegepast; dit is geen algemene aan-uitknop.\n\n***\n\nFlexibel, want het is meer dan een ‘adblocker’: het kan ook filters inlezen en aanmaken vanuit hosts-bestanden.\n\nStandaard worden de volgende filterlijsten geladen en toegepast:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nEr zijn meer lijsten beschikbaar die u kunt inschakelen:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- en nog vele andere...\n\nNatuurlijk wordt het geheugengebruik groter naarmate er meer filters worden ingeschakeld. Maar zelfs na het inschakelen van Fanboy’s twee extra lijsten, hpHosts’s Ad en tracking servers, heeft uBlock een lager geheugengebruik dan andere populaire blockers.\n\nLet op, het gebruik van sommige van deze extra lijsten verhoogt de kans dat websites niet goed worden weergegeven - zeker de lijsten die normaal als hosts-bestand worden gebruikt.\n\n***\n\nZonder de standaard filterlijsten doet deze extensie niets. Als u dus ooit echt een bijdrage wilt leveren, denk dan aan de mensen die hard werken om de filterlijsten die u gebruikt te onderhouden, welke allemaal gratis beschikbaar zijn gemaakt.\n\n***\n\nVrij.\nOpen source met publieke licentie (GPLv3)\nVoor gebruikers, door gebruikers.\n\nMedewerkers @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nMedewerkers @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nOnthoud dat dit een hele vroege versie is wanneer u een beoordeling geeft.\n\nProjectwijzigingenlogboek:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pl": "Skutecznie blokuje reklamy, używa mało pamięci RAM i zasobów procesora, a przy tym może wczytywać i stosować o wiele więcej filtrów niż inne popularne rozszerzenia do blokowania reklam.\n\nIlustrowane porównanie z dodatkiem Adblock Plus: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nSposób użycia: wyświetlany w małym wyskakującym oknie przycisk służy do włączenia/wyłączenia rozszerzenia na bieżącej witrynie internetowej. Działanie przycisku ma zastosowanie tylko na bieżącej witrynie – nie działa globalnie.\n\n***\n\nElastyczny. Jest czymś więcej niż „blokerem reklam”. Może czytać i tworzyć filtry z plików hostów.\n\nPo zainstalowaniu są wczytywane i stosowane następujące listy filtrów:\n\n- EasyList\n- Lista serwerów reklam Petera Lowe'a\n- EasyPrivacy\n- Domeny ze złośliwym oprogramowaniem\n\nMożna wybrać więcej list filtrów:\n\n- Rozszerzona lista śledzenia dla fanboyów\n- Plik hostów Dana Pollocka\n- Serwery reklam i śledzenia hpHosts\n- MVPS HOSTS\n- Spam404\n- I wiele innych\n\nIm więcej filtrów jest włączonych, tym większe jest użycie pamięci RAM. Nawet po dodaniu dwóch dodatkowych list filtrów dla fanboyów – listy serwerów reklamowych i śledzących hpHosts – µBlock₀ używa mniej pamięci RAM niż inne popularne dodatki do blokowania reklam.\n\nNależy pamiętać, że wybranie niektórych dodatkowych list może prowadzić do wzrostu prawdopodobieństwa uszkodzenia witryny internetowej – zwłaszcza tych list, które są zwykle używane jako plik hostów.\n\n***\n\nBez zaprogramowanej listy filtrów, to rozszerzenie jest bezwartościowe. Pomyśl zatem o osobach, które ciężko pracują, tworząc i utrzymując udostępniane za darmo używane przez Ciebie listy filtrów.\n\n***\n\nDarmowe rozszerzenie.\nKod źródłowy udostępniany na otwartej licencji (GPLv3)\nDla użytkowników przez użytkowników.\n\nWspółtwórcy rozszerzenia: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nAutorzy tłumaczeń: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/a71b71f3a5bb24b1e45c14bf40ee736e1e270b279e4e77e849bf3a5b7650d85e/https%3A//crowdin.com/project/ublock/translators\" rel=\"nofollow\">https://crowdin.com/project/ublock/translators</a>\n\n***\n\nOceniając rozszerzenie pamiętaj, że jest to jego wczesna wersja.\n\nDziennik zmian:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pt-BR": "Um bloqueador eficaz: Com baixo consumo de memória e CPU e ainda pode carregar e aplicar milhares de filtros. Mais do que outros bloqueadores populares lá fora.\n\nVisão geral ilustrada de sua eficiência: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-compared\n\nUtilização: Use o botão de energia no pop-up para desativar/ativar o uBlock₀ para o site atual. Isso se aplica apenas ao site atual, não é um botão global.\n\n***\n\nFlexível, é mais do que um \"ad blocker\": também pode ler e criar filtros a partir de arquivos de hosts.\n\nPor padrão, essas listas de filtros são carregadas e aplicadas:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMais listas estão disponíveis para você escolher, se desejar:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- E varios outros\n\nClaro, quanto mais filtros habilitados, maior o consumo de memória. Ainda, mesmo após a adição do Fanboy's duas listas extras, hpHosts’s e servidores de rastreamento, uBlock₀ ainda tem o consumo de memória menor do que outros bloqueadores populares lá fora.\n\nTambém, esteja ciente de que selecionar algumas dessas listas extras pode levar à maior probabilidade de quebra do layout do site, especialmente aquelas listas que são normalmente usadas como arquivo hosts.\n\n***\n\nSem as listas predefinidas de filtros, esta extensão não é nada. Então, se você realmente quiser contribuir com alguma coisa, pense sobre as pessoas que trabalham duro para manter as listas de filtro que você está usando, que estão disponíveis de graça para todos.\n\n***\n\nGratuito\nCódigo aberto com licença pública (GPLv3)\nDe usuários para usuários.\n\nContribuidores no Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuidores no Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEssa é uma versão ainda em desenvolvimento, tenha isso em mente quando você avaliar.\n\nRegistro de alterações do projeto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pt-PT": "Um bloqueador eficiente: leve na memória e CPU e, no entanto, consegue carregar e aplicar milhares de filtros a mais do que outros bloqueadores populares disponíveis.\n\nVisão geral ilustrada da sua eficiência:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilização: O botão grande de energia na janela serve para desativar ou ativar, permanentemente, o uBlock para o sítio web atual. Aplica-se unicamente ao sítio web atual, não sendo um botão de energia global.\n\n***\n\nFlexível, é mais do que um bloqueador de anúncios. Pode também ler e criar filtros a partir de ficheiros de servidores.\n\nPor predefinição, estas listas de filtros são carregadas e aplicadas:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nSe quiser, estão disponíveis mais listas para seleção:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- E muitas mais\n\nObviamente que quanto maior o número de filtros ativos, maior será o consumo de memória. No entanto, mesmo após adicionar as duas listas extra do Fanboy, hpHosts Ad and tracking servers, o uBlock₀ continua a consumir menos memória do que outros bloqueadores populares disponíveis.\n\nEsteja ciente de que se selecionar mais listas extra pode resultar numa probabilidade acrescida de rutura em alguns sítios web -- especialmente nas listas que, normalmente, são utilizadas como ficheiros de servidores.\n\n***\n\nSem as listas de filtros predefinidas, esta extensão não é nada. Se realmente quiser contribuir com algo, pense nas pessoas que trabalham duro para manter as listas de filtros que usa, que foram tornadas disponíveis para uso por todos sem custos.\n\n***\n\nGrátis.\nCódigo aberto com licença pública (GPLv3)\nDe utilizadores para utilizadores.\n\nContribuidores @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuidores @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEsta é uma versão inicial, tenha isso em mente quando avaliar.\n\nRegisto de alterações do projeto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ro": "Un blocant (paravan) eficient: folosește foarte puțin procesorul și memoria și totuși poate încărca și aplica mii de filtre în plus față de alte paravane populare.\n\nO ilustrare a eficienței poate fi observată la:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilizare: Butonul mare de pornire/oprire în fereastra paravanului este pentru a activa/dezactiva uBlock pentru saitul curent. Funcția este valabilă doar pentru saitul curent, nu la nivel global.\n\n***\n\nFlexibil, mai mult decât un „blocant de reclame”: acesta poate citi și crea filtre din fișierele de gazde (hosts).\n\nÎn mod implicit, aceste liste de filtre sunt încărcate și aplicate:\n\n- EasyList\n- Lista serverelor de reclame a lui Peter Lowe\n- EasyPrivacy\n- Domenii malițioase\n\nDe asemenea, mai sunt disponibile și alte liste precum:\n\n- Lista îmbunătățită pentru urmărire a lui Fanboy\n- Lista de gazde a lui Dan Pollock\n- Lista de reclame și urmărire hpHosts\n- Gazdele MVPS\n- Spam404\n- Și multe altele\n\nDesigur, cu cât sunt mai multe filtre active cu atât mai mult este utilizată memoria. Totuși, chiar și după adăugarea în plus a două liste Fanboy și lista de reclame și urmărire hPhosts, uBlock₀ tot folosește mai puțină memorie decât restul paravanelor.\n\nDe ținut minte, că odată cu selectarea în plus a unora dintre liste se poate ajunge la afectarea aspectului saiturilor -- în special listele care sunt în mod normal liste de gazde.\n\n***\n\nFără listele prestabilite de filtre această extensie nu face nimic. Așadar, dacă totuși vreți să contribuiți, gândiți-vă la persoanele care muncesc să întrețină aceste filtre pe care le utilizați, care sunt oferite pentru utilizare gratuită.\n\n***\n\nGratuit.\nCu sursă liberă și licență publică (GPLv3)\nPentru utilizatori de la utilizatori.\n\nContribuitori pe Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuitori pe Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEste încă o aplicație recentă, gândiți-vă la acest lucru când scrieți o recenzie.\n\nLista de schimbări a proiectului:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ru": "µBlock — эффективный блокировщик: он использует меньше оперативной памяти и меньше нагружает ЦП, при этом используя больше фильтров, чем другие популярные блокировщики.\n\nИллюстрированный обзор его эффективности: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nИспользование: нажмите большую кнопку «Включение» в выпадающем окне, чтобы включить или отключить uBlock для текущего сайта. Это действует только для текущего сайта, а не для всех.\n\n***\n\nБудучи гибким, это нечто большее, чем просто «блокировщик рекламы»: он также может считывать и создавать фильтры из хост-файлов.\n\nПо умолчанию следующие списки фильтров будут загружены и применены:\n\n- EasyList\n- Список рекламных серверов Питера Лоу\n- EasyPrivacy\n- Вредоносные домены\n\nТакже на выбор доступны другие списки:\n\n- Фанатский улучшенный список отслеживания\n- Хост-файл Дэна Поллока\n- Рекламные и отслеживающие сервера hpHosts\n- MVPS HOSTS\n- Spam404\n- И т. д.\n\nКонечно, чем больше фильтров, тем выше использование памяти. Тем не менее даже после добавления трёх дополнительных списков uBlock₀ всё ещё потребляет меньше памяти, чем другие популярные блокировщики.\n\nТакже имейте в виду, что некоторые их этих списков имеют высокую вероятность поломать веб-сайт, особенно те, что созданы из хост-файлов.\n\n***\n\nБез предустановленных списков фильтров это расширение — ничто. Так что, если вы действительно хотите внести свой вклад, подумайте о людях, усердно поддерживающих списки фильтров, предоставленные Вам для бесплатного использования.\n\n***\n\nБесплатно.\nОткрытый исходный код, публичная лицензия (GPLv3).\nДля пользователей от пользователей.\n\nУчастники на Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nУчастники на Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nЭто ещё очень ранняя версия, имейте это в виду, оценивая программу.\n\nСписок изменений:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sk": "Účinný blokovač: nezaťažuje CPU a pamäť a dokáže načítať a vynútiť o niekoľko tisíc filtrov viac ako iné populárne blokovače.\n\nIlustrovaný prehľad o jeho účinnosti: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPoužitie: Veľký vypínač vo vyskakovacom okne natrvalo zakáže/povolí uBlock pre aktuálnu webovú stránku. Vzťahuje sa len na aktuálnu webovú stránku, nie na všeobecný vypínač.\n\n***\n\nFlexibilný, je viac než len \"blokovač reklám\": dokáže tiež načítať a vytvárať filtre z hosts súborov.\n\nTieto zoznamy filtrov sú predvolene načítané a vynútené:\n\n- EasyList\n- Zoznam reklamných serverov od Petra Lowesa\n- EasyPrivacy\n- Domény malvéru\n\nĎalšie zoznamy sú k dispozícii pre vás na výber, ak si prajete:\n\n- Rozšírený stopovací zoznam od Fanboya\n- Hosts súbor od Dana Pollocka\n- Reklamné a stopovacie servery od hpHosts\n- MVPS HOSTS\n- Spam404\n- A mnoho ďalších\n\nSamozrejme, čím viac povolených filtrov, tým vyššie nároky na pamäť. Aj po pridaní dvoch ďalších zoznamov od Fanboya, reklamných a stopovacích serverov od hpHost má uBlock stále menšie nároky na pamäť ako mnohé ďalšie veľmi populárne blockovače.\n\nĎalej majte na pamäti, že výber viacerých filtrov zvyšuje šancu chybného zobrazenie webov - predovšetkým u zoznamov, ktoré sa normálne používajú ako hosts súbory.\n\n***\n\nBez predvolených zoznamov filtrov by bolo toto rozšírenie k ničomu. Ak teda naozaj budete chcieť niečím prispieť, myslite na ľudí, ktorí spravujú vami používané zoznamy filtrov a uvoľňujú ich pre všetkých zadarmo.\n\n***\n\nBezplatný.\nOtvorený zdrojový kód s verejnou licenciou (GPLv3)\nPre používateľov od používateľov.\n\nPrispievatelia @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPrispievatelia @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nIde o pomerne skorú verziu, majte to na pamäti pri recenzovaní.\n\nZoznam zmien projektu:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sl": "Efektiven zatiralec oglasov: lahek na pomnilniku in procesorju, in vendar lahko nalaga in uveljavlja tisoče filtrov več kot kakšen drug popularen dodatek za blokiranje oglasov.\n\nIlustrirana efektivnost: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUporaba: Velik gumb za vklop/izklop v pojavnem oknu je namenjen trajnemu izklopu/vklopu uBlock₀ za trenutno spletno stran. Ta uporaba velja samo za trenutno spletno stran, tako da gumb ne predstavlja globalnega vklopa/izklopa.\n\n***\n\nuBlock₀ je fleksibilen - in s tem več kot samo \"blokada oglasom\": lahko bere in ustvarja filtre iz datotek z gostitelji (HOSTS datoteka).\n\nBrez kakršnihkoli dodatnih nastavitev, uBlock₀ uporablja sledeče filtre:\n\n- EasyList\n- Seznam oglaševalskih strežnikov Peter Lowe\n- EasyPrivacy\n- Zlonamerne domene\n\nVeč filtrskih seznamov na razpolago (če to želite):\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- In mnogi drugi\n\nSeveda, več filtrov kot je aktivnih, večji je odtis v pomnilniku. Pa kljub temu - tudi z nalaganjem dveh dodatnih seznamov filtrov (Fanboy in hpHosts) ima uBlock₀ še vedno nižjo mero obremenitve pomnilnika kot ostali zelo popularni dodatki za blokiranje oglasov.\n\nPoleg tega bodite pozorni, da vklop določenih dodatnih seznamov filtrov lahko pripelje do višje verjetnosti za nefunkcionalnost spletne strani - predvsem \"ogrožajoči\" so tisti seznami, ki se jih ponavadi uporablja kot HOSTS datoteko.\n\n***\n\nBrez prednastavljenih seznamov filtrov, da dodatek ni nič. Tako da, če res želite kje pomagati ali komu plačati kavo, pomislite na ljudi, ki trdo delajo, da vzdržujejo te sezname filtrov, ki jih uporabljate, in so jih naredili dosegljive zastonj in za vse.\n\n***\n\nZastonj.\nOdprtokodno pod GPLv3 licenco\nZa uporabnike od uporabnikov.\n\nRazvijalci @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPrevajalci @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nGre še za dokaj sveže različice, prosimo da to upoštevate pri vaši kritiki.\n\nDnevnik sprememb projekta:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sq": "Një bllokues efikas: me impakt të vogël te memorja dhe procesori, por mund të hapë dhe të zbatojë mijëra filtra më shumë sesa bllokuesit e tjerë të njohur.\n\nPërmbledhje e ilustruar e efikasitetit të tij: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPërdorimi: Çelësi i komandimit te dritarja e vogël e bën uBlock përherë joaktiv/aktiv për uebsajtin aktual. Ai vlen vetëm për uebsajtin aktual, nuk është një çelës i përgjithshëm.\n\n***\n\nËshtë fleksibël dhe jo thjesht një \"bllokues reklamash\": mund të lexojë dhe të krijojë filtra nga skedat \"hosts\".\n\nFiltrat e listuar këtu hapen dhe zbatohen pas instalimit:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPo të doni, ka edhe shumë lista të tjera të gatshme:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Dhe shumë të tjera\n\nSigurisht që sa më shumë filtra të aktivizoni, aq më i madh do të jetë impakti te memorja. Edhe me shtimin e dy listave shtesë të Fanboy, hpHosts’s Ad and tracking servers, uBlock përsëri ka impakt më të ulët në memorje sesa bllokuesit e tjerë shumë të njohur.\n\nPor, kujdes, sepse duke përzgjedhur disa prej këtyre listave, gjasat që faqet të shfaqin probleme do të jenë më të mëdha -- sidomos listat që normalisht përdoren si skeda \"hosts\".\n\n***\n\nPa listat e programuara, ky program nuk vlen për asgjë. Prandaj, po të doni të kontribuoni diçka, mendoni pak për njerëzit që punojnë fort për mirëmbajtjen e listave me filtra që po përdorni, të cilat na ofrohen të gjithëve pa pagesë.\n\n***\n\nFalas.\nMaterial i hapur me licencë publike (GPLv3)\nKrijuar nga përdoruesit për përdoruesit.\n\nKontributorët @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKontributorët @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nKur të bëni vlerësimin e programit, mos harroni se ky është një version paraprak.\n\nDitari i projektit:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sv-SE": "En effektiv blockerare: lätt på minne och CPU-fotavtryck, som ändå kan ladda och applicera tusentals fler filter jämfört med andra populära blockerare där ute.\n\nIllustrerad översikt av dess effektivitet:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nAnvändning: Den stora strömbrytarikonen i popuprutan är till för att avaktivera/aktivera uBlock₀ på den aktuella webbplatsen permanent. Detta gäller enbart för den aktuella webbplatsen, det är inte en global strömbrytare.\n\n***\n\nFlexibel. uBlock₀ är inte enbart en \"reklamblockerare\": den kan också läsa och skapa filter från hosts-filer.\n\nSom standard är följande filterlistor laddade och applicerade:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nFler filterlistor finns tillgängliga att använda om du vill:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- med flera\n\nJu fler aktiverade filter, desto högre minnesanvändning. Men även efter att ha lagt till Fanboys två extra filterlistor och hpHosts' Ad and tracking servers så använder uBlock₀ mindre minne än andra väldigt populära blockerare.\n\nTänk på att genom att aktivera vissa av dessa extra filterlistor finns det större risk att webbplatser går sönder - särskilt de listor som i normala fall används som hosts-filer.\n\n***\n\nuBlock₀ vore ingenting utan filterlistorna. Så om du vill bidra med någonting, tänk på människorna som arbetar hårt med att upprätthålla de filterlistor du använder, vilka är fritt tillgängliga för allas användning.\n\n***\n\nGratis.\nÖppen källkod med offentlig licens (GPLv3)\nFör användare, av användare.\n\nBidragsgivare @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nBidragsgivare @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDetta är en ganska tidig version, tänk på detta när du skriver en recension.\n\nProjektets ändringslogg:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "uk": "Ефективний блокувальник реклами: сильно не навантажує пам’ять та процесор і може працювати з набагато більшою кількістю фільтрів ніж інші блокувальники.\n\nІлюстрований огляд ефективності: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nВикористання: Ця велика кнопка живлення у виринаючому вікні дозволяє вимкнути або увімкнути uBlock для поточного веб-сайту. Ефект розповсюджується тільки на поточний веб-сайт. Це не глобальна кнопка живлення.\n\n***\n\nБудучи універсальним, це більш ніж просто \"блокувальник реклами\". Він також може створювати фільтри з файлів hosts.\n\nЗа замовчуванням завантажено та застосовано наступні списки фільтрів:\n\n– EasyList\n– список рекламних серверів Петра Лоу\n– EasyPrivacy\n– шкідливі домени\n\nНаступні списки можна можна увімкнути за бажанням:\n\n– покращений список слідкування від Fanboy\n– файл хостів Дена Полока\n– сервери реклами та слідкування hpHosts\n– MVPS HOSTS\n– Spam404\n– тощо.\n\nЗвичайно ж, чим більше фільтрів ви увімкнете тим більшим буде використання пам’яті. Однак, навіть після додання двох додаткових списків Fanboy, серверів слідкування та реклами phHosts, uBlock споживає менше пам’яті ніж інші популярні блокувальники.\n\nТакож майте на увазі, що задіяння деяких додаткових списків може спричинити збільшення ймовірності пошкодження функціонування сайту. Особливо ті списки, які зазвичай використовуються як hosts-файл.\n\n***\n\nБез встановлених списків фільтрів це розширення – ніщо. Тому, якщо ви дійсно хочете зробити свій внесок, подумайте про людей, які тяжко працюють для підтримки списків фільтрів якими ви користуєтесь безкоштовно.\n\n***\n\nБезкоштовно.\nВідкритий джерельний код та публічна ліцензія (GPLv3)\nДля користувачів від користувачів.\n\nУчасники @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nПерекладачі @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nЦе ще дуже дочасна версія, тому майте на увазі, коли робите огляд.\n\nЖурнал змін проекту:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ur": "ایک زبردست اشتہارات کو روکنے والا سافٹویئر. کم میموری اور cpu استعمال کرتا ہے مگر کام بہترین کرتا ہے.\n\nاس کا بہترین اور پراثر کام کرنے کی تصاویر:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nہدایات: بڑا آن/ آف کا بٹن دبا کر آپ موجودہ ویب سائٹ پر uBlock کو فعال یا غیر فعال کر سکتے ہیں. یہ بٹن صرف موجودہ ویب سائٹ کے لئے ہے، باقی ویب سائٹس کو اس سے کوئی فرق نہیں پڑے گا.\n\n***\n\nFlexible, it's more than an \"ad blocker\": it can also read and create filters from hosts files.\n\nیہ والے فلٹر پہلے سے لاگو ہوں گے:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMore lists are available for you to select if you wish:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- And many others\n\nجتنے زیادہ فلٹر لگائیں گے اتنی زیادہ میموری لے گا. Yet, even after adding Fanboy's two extra lists, hpHosts’s Ad and tracking servers, uBlock still has a lower memory footprint than other very popular blockers out there.\n\nAlso, be aware that selecting some of these extra lists may lead to higher likelihood of web site breakage -- especially those lists which are normally used as hosts file.\n\n***\n\nWithout the preset lists of filters, this extension is nothing. So if ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\nمفت.\nاوپن سورس عوامی لائسنس(جی.پی.ایل ورژن ٣) کے ساتھ\nعوام کے لیے، عوام کا بنایا ہوا.\n\nمعاونین کی فہرست Github پر دیکھیں:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nمترجمین کی فہرست Crowdin پر دیکھیں:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\nپراجیکٹ میں ترقیاتی کام کا ریکارڈ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "vi": "Một công cụ chặn quảng cáo hiệu quả: sử dụng ít bộ nhớ, CPU và có thể nạp, áp dụng hàng ngàn bộ lọc so với những công cụ chặn quảng cáo hiện nay.\n\nMinh hoạ tổng quan về tính hiệu quả của µBlock: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nSử dụng: Nút nguồn lớn trong hộp thoại popup để vô hiệu/kích hoạt vĩnh viễn uBlock cho website hiện tại. Nó chỉ áp dụng cho trang hiện tại, không phải tất cả website.\n\n***\n\nLinh hoạt, hơn cả một \"công cụ chặn quảng cáo\": µBlock có thể đọc và tạo bộ lọc từ tập tin hosts.\n\nNgay lập tức, những bộ lọc này được nạp và áp dụng:\n\n- EasyList\n- Danh sách máy chủ quảng cáo của Peter Lowe\n- EasyPrivacy\n- Malware domains\n\nCó thêm nhiều danh sách để bạn lựa chọn:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- Máy chủ quảng cáo và theo dõi hpHosts\n- MVPS HOSTS\n- Spam404\n- Và nhiều hơn nữa\n\nDĩ nhiên, khi kích hoạt thêm bộ lọc, tiện ích sẽ dùng nhiều bộ nhớ hơn. Tuy vậy, sau khi thêm hai danh sách Fanboy, máy chủ quảng cáo và theo dõi của hpHosts, uBlock vẫn dùng ít bộ nhớ hơn so với những công cụ chặn quảng cáo rất phổ biến khác.\n\nNgoài ra, lưu ý rằng chọn thêm một số danh sách có thể dẫn đến khả năng một số website hiển thị không đúng cách -- đặc biệt là những danh sách thường được dùng như tập tin hosts.\n\n***\n\nKhông có danh sách bộ lọc cài sẵn, tiện ích mở rộng này chẳng là gì cả. Vậy nên nếu bạn thật sự muốn đóng góp gì đó, hãy nghĩ về những người đang chăm chỉ duy trì danh sách bộ lọc hoàn toàn miễn phí mà bạn đang dùng.\n\n***\n\nMiễn phí.\nNguồn mở với giấy phép công cộng (GPLv3)\nLàm vì người dùng bởi người dùng.\n\nNhững người đóng góp @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nNhững người đóng góp @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nĐây là một phiên bản khá mới, hãy ghi nhớ điều này khi bạn đánh giá.\n\nThay đổi của dự án:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "zh-CN": "一款高效的请求过滤工具:占用极低的内存和CPU,和其他常见的过滤工具相比,它能够加载并执行上千条过滤规则。\n\n效率概述说明: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n用法:点击弹出窗口中的电源按钮,uBlock 将对当前网页永久禁用/启用过滤功能。 它只控制当前网页的请求过滤,而不是一个全局开关。 它只控制当前网页的请求过滤,而不是一个全局开关。\n\n***\n\n它不只是一个广告拦截工具,它还可以从 hosts 文件里读取和创建过滤规则。\n\n初始默认加载和执行下列过滤规则列表:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\n这里还有更多的规则列表供你选择:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 等等\n\n当然,启用越多的过滤规则就会产生越高的内存占用。 然而,即使再添加 Fanboy 额外的两个规则列表,如 hpHosts’s Ad 和 tracking servers,uBlock 的内存占用依然比其他常见的过滤工具要低的多。\n\n另外请注意,选择一些额外的列表可能会导致网页破损可能性增高 —— 尤其是那些通常被用作 hosts 文件的列表。\n\n***\n\n没有这些过滤规则列表,这个扩展就没有了意义。 所以如果你真的想做点贡献,想想那些维护过滤规则的人们,是他们让所有人能够免费使用这一切变得可能。\n\n***\n\n免费。\n遵从 GPLv3 公共许可协议开源。\n一切为了用户。\n\n贡献者 @ Github:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n贡献者 @ Crowdin:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n它还是一个相当早期的版本,在您评论的时候请记住这一点。\n\n项目更新日志:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "zh-TW": "一款高效率的廣告攔截工具:只使用超低的記憶體和CPU使用量,和其他常見的廣告攔截工具相比,可以載入並執行上千條過濾規則。\n\n效率概述說明: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/90cf866e9b2e1ea9282c8c93e7a0891c713248d4bf07b8aaefe26d97f8ccde33/https%3A//github.com/gorhill/uBlock/wiki/%25C2%25B5Block-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/%C2%B5Block-vs.-ABP:-efficiency-compared</a>\n\n用法:點選快顯視窗中的電源按鈕,μBlock將會在目前正在瀏覽的網頁永久停用/啟用廣告攔截功能。 它只適用於目前正在瀏覽的網頁,而不是全域按鈕。\n\n***\n\n這不只是一個廣告攔截工具,它還可以非常有彈性的從hosts檔裡讀取和建立過濾規則。\n\n初始預設載入和執行下列過濾規則:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n- Long-lived malware domains\n- Malware Domains List\n\n這裡還擁有更多的過濾規則供你選擇:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 其他\n\n啟用越多的過濾規則就會佔用越多的記憶體。 然而,即使在加入 Fanboy 額外的兩個規則和 hpHosts’s Ad and tracking servers,uBlock₀ 的記憶體佔用依然比其他常見的過濾工具要小的多。\n\n另外,請注意選擇的一些額外的清單可能會導致網頁破損可能性增高 — — 尤其是那些通常用來當作hosts檔案的清單。\n\n***\n\n沒有這些過濾規則清單,這個擴充套件就沒有了意義。 所以如果你真的想要做些貢獻,試著想想那些努力維護廣告過濾規則清單的人們,至少他們讓大家可以免費使用這一切。\n\n***\n\n自由、免費。\n開放原始程式碼與公共許可證 (GPLv3)\n一切都是為了使用者。\n\n貢獻者@ Github: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n貢獻者 @ Crowdin: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n這還只是一個非常初期的版本,當您留下建議的時候請手下留情。\n\n專案更新日誌:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>"
+ },
+ "developer_comments": null,
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/edit",
+ "guid": "uBlock0@raymondhill.net",
+ "has_eula": false,
+ "has_privacy_policy": true,
+ "homepage": {
+ "en-US": "https://github.com/gorhill/uBlock#ublock-origin"
+ },
+ "icon_url": "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "icons": {
+ "32": "https://addons.mozilla.org/user-media/addon_icons/607/607454-32.png?modified=mcrushed",
+ "64": "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "128": "https://addons.mozilla.org/user-media/addon_icons/607/607454-128.png?modified=mcrushed"
+ },
+ "is_disabled": false,
+ "is_experimental": false,
+ "last_updated": "2023-08-07T17:15:41Z",
+ "name": {
+ "ar": "uBlock Origin",
+ "bg": "uBlock Origin",
+ "bn-BD": "uBlock Origin",
+ "ca": "uBlock Origin",
+ "cs": "uBlock Origin",
+ "da": "uBlock Origin",
+ "de": "uBlock Origin",
+ "el": "uBlock Origin",
+ "en-US": "uBlock Origin",
+ "es": "uBlock Origin",
+ "eu": "uBlock Origin",
+ "fa": "uBlock Origin",
+ "fi": "uBlock Origin",
+ "fr": "uBlock Origin",
+ "he": "uBlock Origin",
+ "hu": "uBlock Origin",
+ "id": "uBlock Origin",
+ "it": "uBlock Origin",
+ "ja": "uBlock Origin",
+ "ka": "uBlock Origin",
+ "ko": "uBlock Origin",
+ "nl": "uBlock Origin",
+ "pl": "uBlock Origin",
+ "pt-BR": "uBlock Origin",
+ "pt-PT": "uBlock Origin",
+ "ro": "uBlock Origin",
+ "ru": "uBlock Origin",
+ "sk": "uBlock Origin",
+ "sl": "uBlock Origin",
+ "sq": "uBlock Origin",
+ "sv-SE": "uBlock Origin",
+ "uk": "uBlock Origin",
+ "ur": "uBlock Origin",
+ "vi": "uBlock Origin",
+ "zh-CN": "uBlock Origin",
+ "zh-TW": "uBlock Origin"
+ },
+ "previews": [
+ {
+ "id": 238546,
+ "caption": {
+ "en-US": "The popup panel: default mode"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238546.png?modified=1622132421",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238546.jpg?modified=1622132421"
+ },
+ {
+ "id": 238548,
+ "caption": {
+ "en-US": "The dashboard: stock filter lists"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238548.png?modified=1622132423",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238548.jpg?modified=1622132423"
+ },
+ {
+ "id": 238547,
+ "caption": {
+ "en-US": "The popup panel: default-deny mode"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238547.png?modified=1622132425",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238547.jpg?modified=1622132425"
+ },
+ {
+ "id": 238549,
+ "caption": {
+ "en-US": "The dashboard: settings"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238549.png?modified=1622132426",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238549.jpg?modified=1622132426"
+ },
+ {
+ "id": 238552,
+ "caption": {
+ "en-US": "The popup panel in Firefox Preview: default mode with more blocking options revealed"
+ },
+ "image_size": [
+ 970,
+ 1800
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238552.png?modified=1622132430",
+ "thumbnail_size": [
+ 216,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238552.jpg?modified=1622132430"
+ },
+ {
+ "id": 230370,
+ "caption": {
+ "en-US": "The unified logger tells you all that uBO is seeing and doing"
+ },
+ "image_size": [
+ 800,
+ 600
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/230/230370.png?modified=1622132432",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/230/230370.jpg?modified=1622132432"
+ }
+ ],
+ "promoted": {
+ "apps": [
+ "firefox",
+ "android"
+ ],
+ "category": "recommended"
+ },
+ "ratings": {
+ "average": 4.7825,
+ "bayesian_average": 4.782204826721061,
+ "count": 15799,
+ "text_count": 4101
+ },
+ "ratings_url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/reviews/",
+ "requires_payment": false,
+ "review_url": "https://addons.mozilla.org/en-US/reviewers/review/607454",
+ "slug": "ublock-origin",
+ "status": "public",
+ "summary": {
+ "ar": "وأخيراً, مانع اعلانات كفوء. خفيف على المعالج و الذاكرة",
+ "bg": "Най-накрая, ефективен рекламен блокер с малки изисквания за процесор и памет.",
+ "bn-BD": "অবশেষে, একটি দক্ষ প্রতিরোধক। সিপিইউ এবং মেমরির জন্য সহজ।",
+ "ca": "Finalment, un blocador eficient que utilitza pocs recursos de memòria i processador.",
+ "cs": "Konečně efektivní blokovač, který nezatěžuje CPU a paměť.",
+ "da": "Endelig en effektiv blocker til Chromium-baserede browsere. Lavt CPU- og hukommelsesbrug.",
+ "de": "Endlich ein effizienter Blocker. Prozessor-freundlich und bescheiden beim Speicherbedarf.",
+ "el": "Επιτέλους, ένας αποτελεσματικός blocker. Ελαφρύς για τον επεξεργαστή και τη μνήμη.",
+ "en-US": "Finally, an efficient wide-spectrum content blocker. Easy on CPU and memory.",
+ "es": "Por fin, un bloqueador eficiente con uso mínimo de procesador y memoria.",
+ "eu": "Behingoz, blokeatzaile eraginkor bat. PUZ eta memorian arina.",
+ "fa": "بالاخره، یک بلاکر کارآمد. کم حجم بر روی پردازنده و حافظه",
+ "fi": "Viimeinkin tehokas ja kevyt mainosten estäjä.",
+ "fr": "Un bloqueur de nuisances efficace, qui ménagera votre processeur et votre mémoire vive.",
+ "he": "סוף סוף, חוסם יעיל. קל על המעבד והזיכרון",
+ "hu": "Végre egy hatékony reklám- és követésblokkoló böngészőkhöz, amely kíméletes a processzorral és a memóriával.",
+ "id": "Akhirnya, pemblokir iklan yang efisien. Ringan penggunaan CPU dan memori.",
+ "it": "Finalmente, un blocker efficiente. Leggero sulla CPU e sulla memoria.",
+ "ja": "高効率ブロッカーが遂に登場。CPUとメモリーの負担を抑えます。",
+ "ka": "როგორც იქნა, მძლავრი და შედეგიანი რეკლამების შემზღუდავი. ზოგავს CPU-ს და მეხსიერებას.",
+ "ko": "이 부가 기능은 효율적인 차단기입니다. CPU와 메모리에 주는 부담이 적습니다.",
+ "nl": "Eindelijk, een efficiënte adblocker. Gebruikt weinig processorkracht en geheugen.",
+ "pl": "Nareszcie skuteczne blokowanie reklam, niskie użycie procesora i pamięci.",
+ "pt-BR": "Finalmente, um bloqueador eficiente. Com baixo uso de memória e CPU.",
+ "pt-PT": "Finalmente, um bloqueador eficiente. Leve na CPU e memória.",
+ "ro": "În sfârșit, un blocant eficient. Folosește procesorul și memoria foarte puțin.",
+ "ru": "Наконец-то, быстрый и эффективный блокировщик для браузеров.",
+ "sk": "Konečne efektívny blokovač, ktorý nezaťažuje CPU a pamäť.",
+ "sl": "Končno, učinkovita, procesorju in pomnilniku prijazna razširitev za blokiranje oglasov.",
+ "sq": "Më në fund, një bllokues efikas që nuk e rëndon procesorin dhe memorjen.",
+ "sv-SE": "Äntligen en effektiv blockerare! Snäll mot processor och minne.",
+ "uk": "Ефективний блокувальник реклами таки з’явився. Не навантажує процесор та пам'ять.",
+ "ur": "آخر کار، ایک مؤثر اشتہار کو روکنے والا، یہ کم cpu اور میموری لیتا ہے",
+ "vi": "Cuối cùng, đã có một công cụ chặn quảng cáo hiệu quả, tiêu tốn ít CPU và bộ nhớ.",
+ "zh-CN": "一款高效的网络请求过滤工具,占用极低的内存和 CPU。",
+ "zh-TW": "終於出現了,一個高效率的阻擋器,使用不多的 CPU 及記憶體資源。"
+ },
+ "support_email": null,
+ "support_url": {
+ "en-US": "https://old.reddit.com/r/uBlockOrigin/",
+ "ka": "https://old.reddit.com/r/uBlockOrigin/",
+ "ur": "https://old.reddit.com/r/uBlockOrigin/"
+ },
+ "tags": [
+ "ad blocker",
+ "anti malware",
+ "anti tracker",
+ "content blocker",
+ "privacy",
+ "security"
+ ],
+ "type": "extension",
+ "url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/",
+ "versions_url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/versions/",
+ "weekly_downloads": 143905,
+ "_score": null
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/resources/collection.json b/mobile/android/android-components/components/feature/addons/src/test/resources/collection.json
new file mode 100644
index 0000000000..a337ed148f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/resources/collection.json
@@ -0,0 +1,367 @@
+{
+ "page_size": 25,
+ "page_count": 1,
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "addon": {
+ "id": 607454,
+ "authors": [
+ {
+ "id": 11423598,
+ "name": "Raymond Hill",
+ "url": "https://addons.mozilla.org/en-US/firefox/user/11423598/",
+ "username": "gorhill"
+ }
+ ],
+ "average_daily_users": 6229783,
+ "categories": {
+ "android": [
+ "security-privacy"
+ ],
+ "firefox": [
+ "privacy-security"
+ ]
+ },
+ "contributions_url": "",
+ "created": "2015-04-25T07:26:22Z",
+ "current_version": {
+ "id": 5596914,
+ "compatibility": {
+ "firefox": {
+ "min": "78.0",
+ "max": "*"
+ },
+ "android": {
+ "min": "79.0",
+ "max": "*"
+ }
+ },
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/versions/5596914",
+ "is_strict_compatibility_enabled": false,
+ "license": {
+ "id": 6,
+ "is_custom": false,
+ "name": {
+ "en-US": "GNU General Public License v3.0"
+ },
+ "url": "http://www.gnu.org/licenses/gpl-3.0.html"
+ },
+ "release_notes": {
+ "en-US": "See complete release notes for <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/57eadd553bcb629be4f7576c9aa9a808cc5e918a6ea1413dff063281d4896978/https%3A//github.com/gorhill/uBlock/releases/tag/1.51.0\" rel=\"nofollow\">1.51.0</a>.\n\n<b>Fixes / changes</b>\n\n<ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/a578674b791ed67b926b31004613b0631d0cc73efc2d263385eb842dc0dff09f/https%3A//github.com/gorhill/uBlock/commit/ee0649329c59\" rel=\"nofollow\">Remove obsolete web<em>accessible</em>resources</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7071379ad6c88fec4e40b3ff8165d4fd3ecdaae2fbf0ea145fcf21eb0ea86e43/https%3A//github.com/gorhill/uBlock/commit/cdf385f5f46e\" rel=\"nofollow\">Add missing (deprecated) method to google ima</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/bf46690e10ed8a086d9d50080177c67380aeb5cc7945d96acdbde3fdf7aa9466/https%3A//github.com/gorhill/uBlock/commit/aa6baf9a29db\" rel=\"nofollow\">Fix regression in handling of experimental <code>header=</code> filter option</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f2328063ec5c7512d5899f4b7db7324d42864cea3678aff50af908219b976094/https%3A//github.com/gorhill/uBlock/commit/0da7e12ea4a0\" rel=\"nofollow\">Only already normalized CSS selectors can be fast path-compiled</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/e271cd306c9578e0ed17c785486d183abc8eed2408c7f3fb22eca3afff3ab478/https%3A//github.com/gorhill/uBlock/commit/ec0698196563\" rel=\"nofollow\">Improve compatibility with AdGuard's scriptlets</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f42373a2522c9739807b48d956a9437aa9c7e6b6b4a613a24d412d309ac8c037/https%3A//github.com/gorhill/uBlock/commit/5ebdbf3e2439\" rel=\"nofollow\">Add static network filter option: <code>permissions</code></a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f502abdb178413a344a43d8d0e57463d6c9a7ef3c01a437097ec6a2fd97e1350/https%3A//github.com/gorhill/uBlock/commit/786d9b2212e9\" rel=\"nofollow\">Add <code>set-attr</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/46449872121aa9a39f8fe5f1499dccec24fc094eb72848f37974606abd562997/https%3A//github.com/gorhill/uBlock/commit/fea6f7f311a5\" rel=\"nofollow\">Do not bail too early when trapping properties in <code>acs</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/76b28688717445a36e5ee48387a0e7392370e22d5e3a2fbb53d2b965f54f1eee/https%3A//github.com/gorhill/uBlock/commit/80b3f3c3c020\" rel=\"nofollow\">Fix regression in cloud storage import of \"Filter lists\" pane</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/eb0f0fdfbd50904d7cd8e260b62d0ca4b233c8f9d8a18f9abb31472026e07b23/https%3A//github.com/gorhill/uBlock/commit/083a318090e3\" rel=\"nofollow\">Add <code>set-session-storage-item</code> scriptlet</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f125a8290e220a02336532a360ea37a7a4ff1c4402a5f518785b7354a7a491d8/https%3A//github.com/gorhill/uBlock/commit/60b21b142268\" rel=\"nofollow\">Prevent negative position when widget size is greater than viewport size</a><ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/e49ec519e35ce90b1ce42475f48affc9a34e97640a60ca13a6ff3d23b58b0ade/https%3A//github.com/gorhill/uBlock/commit/b44815f0c8f0\" rel=\"nofollow\">Ensure no negative value for <code>top</code> property of floating widget in logger</a></li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/2398a39690c82a85f3fb23710721b490fedd503cf3003fdad3b23a8193fb257d/https%3A//github.com/gorhill/uBlock/commit/622cda2cdf91\" rel=\"nofollow\">Add visual hint when not all sublists are enabled</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f549cc526fa10665161c86930eccf00650b123e9c5f0b9ddc11038d7349600cd/https%3A//github.com/gorhill/uBlock/commit/33b409dd5bae\" rel=\"nofollow\">Add support for AdGuard's noop (<code>_</code>) network filter option</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/c649c1ddb8b1d7f280a0687edeae15b9c7b65d4101901337eff3cb8acb6370f0/https%3A//github.com/gorhill/uBlock/commit/5d6e10318662\" rel=\"nofollow\">Add \"tabless\" filter expression for logger output</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/3f7e7d0a06e918e4c8b3f8bb91d33e700a99ff9c2988069b609f614228e6ccf4/https%3A//github.com/gorhill/uBlock/commit/194354cd5d77\" rel=\"nofollow\">Add support for logical expressions to <code>!#if</code> directive</a><ul><li>Also added support for <code>!#else</code></li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/f1e827e5c4e256a0c832f985f6a21bb39743a5dfecd5d4014ae3aa9b09d9e116/https%3A//github.com/gorhill/uBlock/commit/7867c2512807\" rel=\"nofollow\">Add resource aliases for increased compatibility with AdGuard lists</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6f42d070e087002ada8f37b36176d8d40d40dafcc5f5e9a1050c689ea079017f/https%3A//github.com/gorhill/uBlock/commit/fd036a51ee20\" rel=\"nofollow\">Add compatibility with AdGuard's <code>#%#//scriptlet(...)</code> syntax</a><ul><li>Also added support for quoted parameters in <code>##+js(...)</code> syntax</li></ul></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/50632bd7b34e7e7185c7642640111d9f2ba5493bea1e3ed1c517702302fd4938/https%3A//github.com/gorhill/uBlock/commit/8b7a5264deb4\" rel=\"nofollow\">Fix syntax highlighter throwing with invalid patterns</a></li><li>...</li></ul>\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7c755863346ab5ff9e77aeee9ffcd4ef47f343f5c7d2f2f143dcd474b8558641/https%3A//github.com/gorhill/uBlock/compare/1.50.0...1.51.0\" rel=\"nofollow\">Commits history since last version</a>."
+ },
+ "reviewed": "2023-07-25T09:58:22Z",
+ "version": "1.51.0",
+ "files": [
+ {
+ "id": 4141256,
+ "created": "2023-07-19T23:09:25Z",
+ "hash": "sha256:8b73468bc233a11dd2895219466381783d19123857dd0b6fd16a01820fca4834",
+ "is_restart_required": false,
+ "is_webextension": true,
+ "is_mozilla_signed_extension": false,
+ "platform": "all",
+ "size": 3538418,
+ "status": "public",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4141256/ublock_origin-1.51.0.xpi",
+ "permissions": [
+ "dns",
+ "menus",
+ "privacy",
+ "storage",
+ "tabs",
+ "unlimitedStorage",
+ "webNavigation",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ "http://*/*",
+ "https://*/*",
+ "file://*/*",
+ "https://easylist.to/*",
+ "https://*.fanboy.co.nz/*",
+ "https://filterlists.com/*",
+ "https://forums.lanik.us/*",
+ "https://github.com/*",
+ "https://*.github.io/*",
+ "https://*.letsblock.it/*"
+ ],
+ "optional_permissions": [],
+ "host_permissions": []
+ }
+ ]
+ },
+ "default_locale": "en-US",
+ "description": {
+ "ar": "مانع إعلانات كفوء: خفيف على الذاكرة و المعالج, على الرغم من قدرته على تحميل و تطبيق الألاف من الفلاتر أكثر من بعض أشهر مانعي الإعلانات.\n\nتوضيح عام لكفاءة الإضافة: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nالإستخدام: زر التشغيل الكبير في النافذة المنبثقة هو لتعطيل أو تشغيل uBlock للموقع الحالي. هو ينطبق على الموقع الحالي فقط، و ليس زر تشغيل عام.\n\n***\n\nمع مرونته، هو أكثر من مجرد \"مانع إعلانات\": بإمكانه أيضا قراءة و إنشاء فلاتر من ملفات الإستقبال.\n\nفلاتر حديثة، هذه القوائم من الفلاتر يتم تحميلها و تطبيقها:\n\n- EasyList\n- قائمة خادم الإعلانات لـPeter Lowe\n- EasyPrivacy\n- نطاقات البرامج الضارة\n\nيوفر لك قوائم أكثر لتختار منها إذا كنت ترغب:\n\nقائم التتبع المحسنة لـFanboy\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- و الكثير من القوائم الأخرى.\n\nطبعا، كلما مكَّنتَ فلاتر أكثر، كلما ازداد أثرها على الذاكرة. حتى مع الرغم من إضافة القوائم الإضافية لـFanboy، و قوائم تتبع إعلان hpHost، ما زال بإمكان uBlock₀ العمل بأدنى أثر على الذاكرة أفضل من بعض أشهر قوائم التتبع.\n\nأيضا، كن على علم أن تحديد بعض من هذه القوائم الإضافية قد يؤدي إلى إمكانية أعلى لتعطيل المواقع -- خصوصا تلك القوائم التي تستخدم عادة كملفات مضيفة.\n\n***\n\nبدون وجود قوائم الفلترات, هذه الإضافة عديمة القيمة. إذن إن كانت لديك الرغبة في المساهمة، فكر في أولئك الذين يعملون بجد لصيانة قوائم الفلترات التي تستخدمها، التي تمت إتاحتها لك لتسخدمها مجَّاناََ.\n\n***\n\nمجاناً.\nمفتوح المصدر مع رخصة (GPLv3)\nللمستخدمين من طرف مستخدمين أخرين.\n\nالمساهمون في Github:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nالمساهمون في Crowdin:\n <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nالإضافة في قيد الإنجاز، خذ هذا في عين الإعتبار عندما تستعرضها.\n\nسجل التغييرات للمشروع:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "bg": "Ефикасен блокер: с малко използване на паметта и процесора, но същевременно способен да зарежда и налага хиляди допълнителни филтри в сравнение с други популярни блокери.\n\nИлюстрация на неговата ефикасност: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nИзползване: Големият бутон за \"Включване\" в изскачащият прозорец служи за трайно включване/изключване на uBlock за текущия сайт. Той се отнася само за текущия сайт, не е глобален бутон за включване.\n\n***\n\nГъвкав, той е повече от \"блокер на реклами\": може да чете и създава филтри от хост файлове.\n\nПри първоначално използване са заредени и наложени следните списъци с филтри:\n\n- EasyList\n- Списък с рекламни сървъри от Peter Lowe\n- EasyPrivacy\n- Вредоносни домейни\n\nAко желаете, на разположение са допълнителни списъци, които да изберете:\n\n- разширен проследяващ списък от Fanboy\n- хост файл от Dan Pollock\n- рекламни и проследяващи сървъри от hpHosts\n- MVPS HOSTS\n- Spam404\n- и много други\n\nРазбира се, колкото повече списъци включите, толкова по-голямо е използването на паметта. Въпреки това, дори и след добавяне на двата допълнителни списъка от Fanboy, рекламните и проследяващи сървъри от hpHosts, uBlock₀ използва по-малко памет в сравнение с други много популярни блокери.\n\nСъщо така, имайте предвид, че избирането на някои от допълнителните списъци може да доведе до по-голяма вероятност от неправилно функциониране на уебсайтове -- особено тези списъци, които по принцип се използват като хост файлове.\n\n***\n\nБез предварително зададените списъци с филтри, това разширение е нищо. Така че, ако някога наистина искате да допринесете с нещо, помислете за хората, работещи усилено по поддържането на списъците с филтри, предоставени ви за безплатно използване от всички.\n\n***\n\nБезплатно.\nОтворен код с публичен лиценз (GPLv3)\nЗа потребители от потребителите.\n\nСътрудници @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nСътрудници @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nТова е доста ранна версия, имайте го предвид, когато я разглеждате.\n\nСписък с промени на проекта:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "bn-BD": "একটি দক্ষ প্রতিরোধক: মেমরি ও CPU-র পদচিহ্নের জন্য সহজ, এবং এখনো অন্যান্য জনপ্রিয় ব্লকার বা অবরোধকারীর থেকে হাজার হাজার অধিক ফিল্টারকে লোড এবং জোরদার করতে পারে।\n\nএটির কার্যকারিতার সচিত্র সংক্ষিপ্ত বিবরণ: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nব্যবহার: পপআপে বড় পাওয়ার বোতাম স্থায়ীভাবে বর্তমান ওয়েব সাইটের জন্য uBlock সক্রিয়/নিষ্ক্রিয় করবে। এটা শুধুমাত্র বর্তমান ওয়েব সাইটে প্রযোজ্য, এটি একটি বৈশ্বিক পাওয়ার বোতাম নয়।\n\n***\n\nমনীয়, এটি একটি \"ad blocker\"-এর ছেয়েও অধিক: এছাড়াও এটি হোস্ট ফাইল থেকে ফিল্টার পড়তে ও তৈরি করতে পারে।\n\nবাক্সের বাইরের, এই তালিকার ফিল্টারগুলি লোড করে এবং তা প্রয়োগ করে:\n\n- সহজ তালিকা\n- পিটার লো'য়ের বিজ্ঞাপন সার্ভারের তালিকা\n- সহজ গোপনীয়তা\n- ম্যালওয়্যার ডোমেইন\n\n আপনি যদি চান আপনি নির্বাচন করার জন্য আরো তালিকা পাবেন:\n\n- ফানবয়ের উন্নত ট্র্যাকিং তালিকা\n- Dan Pollock-এর হোস্ট ফাইল\n- hpHosts-এর বিজ্ঞাপন এবং ট্র্যাকিং সার্ভার\n- MVPS হোস্টসমূহ\n- স্প্যাম৪০৪\n- এবং আরও অনেক কিছু\n\nঅবশ্যই, যতবেশি ফিল্টার সক্রিয় করবেন, মেমরি পদচিহ্ন ততবেশি হবে। এমনকি Fanboy-এর দুটি অতিরিক্ত তালিকা, hpHosts-এর বিজ্ঞাপন এবং ট্র্যাকিং সার্ভার যোগ করার পরেও uব্লক অন্যান্য খুব জনপ্রিয় ব্লকারের থেকে কম মেমরি পদচিহ্ন ব্যবহার করে।\n\nএছাড়াও, এই অতিরিক্ত তালিকার কিছু নির্বাচন ওয়েব সাইট ভাঙ্গনের জন্য উচ্চ সম্ভাবনাময় হয়ে উঠতে পারে তাই সাবধান --- বিশেষকরে এই তালিকাগুলি যা সাধারণত হোস্ট ফাইল হিসেবে ব্যবহার করা হয়।\n\n***\n\nফিল্টারের পূর্বনির্ধারিত তালিকা ছাড়া, এই এক্সটেনশনটি কিছুই নয়। তাই কখনও যদি আপনি সত্যিই কিছু অবদান রাখতে চান, আপনার ব্যবহার করা ফিল্টার তালিকা রক্ষণাবেক্ষণের জন্য কঠোর পরিশ্রম করা সেই সব মানুষের করা কথা চিন্তা করুন যারা এই সব বিনামূল্যে ব্যবহারের জন্য উপলব্ধ করেছেন।\n\n***\n\nবিনামূল্যে।\nপাবলিক লাইসেন্সসহ মুক্ত উৎসের (GPLv3)\nব্যবহারকারীদের দ্বারা ব্যবহারকারীদের জন্য।\n\nঅবদানকারীগণ @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nঅবদানকারীগণ @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nএটি একটি প্রাথমিক সংস্করণ, আপনার পর্যালোচনার সময় তা মনে রাখুন।\n\nপ্রকল্পের পরিবর্তন লগ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ca": "Un bloquejador eficient: el consum de memòria i de processador és baix però, no obstant això, pot carregar i aplicar milers de filtres més que altres bloquejadors coneguts.\n\nGràfic de l'eficiència: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nÚs: El gran botó d'engegada de la finestra emergent serveix per a desactivar/activar permanentment el uBlock per al lloc web actual. No és un botó d'engegada general de l'extensió.\n\n***\n\nFlexible, és més que un \"bloquejador d'anuncis\": també pot llegir i crear filtres a partir de fitxers hosts.\n\nPer defecte, es carreguen i s'apliquen aquestes llistes de filtres:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Dominis de malware\n\nSi voleu, podeu seleccionar altres llistes disponibles:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- I altres\n\nÒbviament, com més filtres activeu, més gran serà el consum de memòria. Però fins i tot després d'afegir dues llistes extra de Fanboy, hpHosts’s Ad and tracking servers, el uBlock₀ encara té un consum de memòria inferior al d'altres bloquejadors coneguts.\n\nTambé heu de ser conscient que seleccionant algunes d'aquestes llistes extra és més probable trobar-se amb llocs webs inservibles -- especialment aquelles llistes que s'utilitzen normalment com a fitxer de hosts.\n\n***\n\nSense les llistes predefinides de filtres, aquesta extensió no és res. Així que, si en cap moment voleu fer una aportació, penseu en les persones que treballen durament per a mantenir les llistes de filtres que utilitzeu, a disposició de tothom de manera gratuïta.\n\n***\n\nLliure.\nCodi obert amb llicència pública (GPLv3)\nPer usuaris per a usuaris.\n\nCol·laboradors a Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nCol·laboradors a Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nAquesta és, en certa manera, una versió primitiva. Tingueu-ho en compte quan en doneu la vostra opinió.\n\nRegistre de canvis del projecte:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "cs": "Efektivní blokovač: nezanechává velké stopy, nezatěžuje paměť a CPU, a přesto může načítat a využívat o několik tisíc filtrů více, než jiné populární blockery.\n\nGrafický přehled jeho účinnosti: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPoužití: Velký vypínač ve vyskakovacím okně trvale povolí/zakáže uBlock pro otevřenou stránku. Funguje pouze pro aktivní webovou stránku, není to obecný vypínač.\n\n***\n\nFlexibilní, více než jen \"blokovač reklam\": umí také číst a vytvářet filtry z hosts souborů.\n\nPo instalaci jsou načteny a použity tyto filtry:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPokud chcete, můžete si vybrat tyto další filtry:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- A mnoho dalších\n\nČím více filtrů je povoleno, tím je samozřejmě větší stopa v paměti. I přesto má ale uBlock₀ i po přidání dvou dalších seznamů od Fanboye a \"hpHosts’s Ad and tracking servers\" menší vliv na paměť než mnohé další velmi populární blockery.\n\nDále mějte na paměti, že vybírání více filtrů zvyšuje šanci chybného zobrazení webů -- především u seznamů, které se normálně používají jako hosts soubory.\n\n***\n\nBez předvolených seznamů filtrů by toto rozšíření bylo k ničemu. Pokud tedy opravdu budete chtít něčím přispět, myslete na lidi, kteří spravují Vámi používané seznamy filtrů a uvolňují je pro všechny zdarma.\n\n***\n\nSvobodný software.\nOpen source s veřejnou licencí (GPLv3)\nOd uživatelů pro uživatele.\n\nPřispěvatelé na Githubu: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPřispěvatelé na Crowdinu: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nJde o poměrně ranou verzi, mějte to na paměti při recenzování.\n\nChange log projektu:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "da": "En effektiv blocker: let på hukommelse og CPU forbrug,. Kan indlæse og anvende tusindvis af flere filtre end andre populære blockere derude.\n\nIllustreret oversigt over effektiviteten: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-compared\n\nAnvendelse: Den Store power knap i pop-up-vinduet kan permanent deaktivere/aktivere uBlock på det aktuelle websted. Dette gælder kun for det aktuelle websted, det er ikke en global afbryderknap.\n\n***\n\nFleksibel, det er mere end en \"ad blocker\": den kan også læse og oprette filtre fra hosts-filer.\n\nFra starten af er disse lister over filtre indlæst og anvendt:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nFlere lister er tilgængelige hvis du ønsker det:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Osv.\n\nSelvfølgelig vil flere aktive filtre betyde højere hukommelsesforbrug. Selv efter tilføjelse af Fanboys to ekstra lister, og hpHosts’s Ad and tracking server, har uBlock₀ stadig et lavere hukommelsesforbrug end andre blockere derude.\n\nVær desuden opmærksom på, at hvis du vælger nogle af disse ekstra lister kan det føre til højere sandsynlighed for, at webstedet bliver vist forkert - især de lister der normalt anvendes som hosts-fil.\n\n***\n\nUden de forudindstillede lister med filtre er denne udvidelse intet. Hvis du nogensinde virkelig ønsker at bidrage med noget, så tænk på de mennesker der arbejder hårdt for at vedligeholde de filterlister du bruger, som alle blev stillet gratis til rådighed for alle.\n\n***\n\nGratis.\nOpen source med offentlig licens (GPLv3)\nFor brugere, af brugere.\n\nBidragydere @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nBidragydere @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDette er en tidlig version. Hav dette i tankerne når du skriver en anmeldelse.\n\nProjekt changelog:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "de": "Ein effizienter Blocker: Geringer Speicherbedarf und niedrige CPU-Belastung - und dennoch werden Tausende an Filtern mehr angewendet als bei anderen populären Blockern.\n\nEin illustrierter Überblick über seine Effizienz: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nBenutzung: Der An-/Ausschaltknopf beim Klicken des Erweiterungssymbols dient zum An-/Ausschalten von uBlock auf der aktuellen Webseite. Dies wirkt sich also nur auf die aktuelle Webseite aus und nicht global.\n\n***\n\nuBlock ist flexibel, denn es ist mehr als ein \"Werbeblocker\": Es verarbeitet auch Filter aus mehreren hosts-Dateien.\n\nStandardmäßig werden folgende Filterlisten geladen und angewandt:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nAuf Wunsch können zusätzliche Listen ausgewählt werden:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- etc.\n\nNatürlich ist der Speicherbedarf umso höher, desto mehr Filter angewandt werden. Aber selbst mit den zwei zusätzlichen Listen von Fanboy und hpHosts’s Ad and tracking servers ist der Speicherbedarf von uBlock₀ geringer als bei anderen sehr populären Blockern.\n\nBedenke allerdings, dass durch die Wahl zusätzlicher Listen die Wahrscheinlichkeit größer wird, dass bestimmte Webseiten nicht richtig geladen werden - vor allem bei Listen, die normalerweise als hosts-Dateien verwendet werden.\n\n***\n\nOhne die vorgegebenen Filterlisten ist diese Erweiterung nichts. Wenn du also etwas beitragen möchtest, dann denke an die Menschen, die hart dafür arbeiten, die von dir benutzten Filterlisten zu pflegen, und diese für uns alle frei verfügbar gemacht haben.\n\n***\n\nKostenlos.\nOpen source mit Public License (GPLv3)\nFür Benutzer von Benutzern.\n\nMitwirkende @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nMitwirkende @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDies ist eine ziemlich frühe Version - bitte denke daran, wenn du sie bewertest.\n\nChange log des Projekts:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "el": "Ένας αποτελεσματικός αναστολέας διαφημίσεων: παρόλο το ελαφρύ του αποτύπωμα στη μνήμη και τον επεξεργαστή μπορεί να εφαρμόσει χιλιάδες περισσότερα φίλτρα σε σχέση με άλλους δημοφιλείς blockers.\n\nΑπεικονιζόμενη επισκόπηση της αποτελεσματικότητάς του: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nΧρήση: Το μεγάλο πλήκτρο απενεργοποίησης/ενεργοποίησης στο αναδυόμενο παράθυρο, χρησιμεύει στην εναλλαγή κατάστασης του uBlock για τον τρέχοντα ιστότοπο. Η εφαρμογή της ρύθμισης αυτής γίνεται μόνο για τον τρέχοντα ιστότοπο και δεν επιβάλλεται καθολικά.\n\n***\n\nΕυέλικτος, είναι πολλά περισσότερα από ένας απλός \"ad blocker\": μπορεί επιπλέον να διαβάζει και να δημιουργεί φίλτρα από αρχεία hosts.\n\nΚατά προεπιλογή, οι λίστες φίλτρων που φορτώνονται και επιβάλλονται είναι οι εξής:\n\n- EasyList\n- Λίστα διακομιστών διαφημίσεων του Peter Lowe\n- EasyPrivacy\n- Κακόβουλοι τομείς\n\nΕπιπλέον λίστες είναι διαθέσιμες για να επιλέξετε εάν το επιθυμείτε:\n\n- Ενισχυμένη Ιχνωσική Λίστα του Fanboy\n- Αρχείο hosts του Dan Pollock\n- Διαφημίσεις και διακομιστές ίχνωσης hpHosts\n- MVPS HOSTS\n- Spam404\n- και πολλές άλλες\n\nΦυσικά, όσο περισσότερα φίλτρα ενεργοποιούνται, τόσο αυξάνεται το αποτύπωμα της μνήμης. Ωστόσο, ακόμη και μετά από την προσθήκη δυο επιπλέον λιστών, του Fanboy και της λίστας διαφημίσεων και διακομιστών ίχνωσης hpHosts, το uBlock₀ συνεχίζει να έχει χαμηλότερο αποτύπωμα μνήμης από άλλους δημοφιλείς αναστολείς.\n\nΕπίσης, έχετε υπ'όψην ότι διαλέγοντας μερικές από τις έξτρα λίστες μπορεί να οδηγήσει σε πιθανό σφάλμα στην ιστοσελίδα -- ειδικά εκείνες που κανονικά χρησιμοποιούνται σαν host αρχεία.\n\n***\n\nΧωρίς τις υπάρχουσες λίστες φίλτρων, αυτή η επέκταση δεν έχει καμία αξία. Εάν ποτέ λοιπόν θελήσετε πραγματικά να συνεισφέρετε κάτι, αναλογιστείτε τους ανθρώπους που εργάζονται σκληρά για να διατηρήσουν τις λίστες φίλτρων που χρησιμοποιείτε, οι οποίες διατέθηκαν προς χρήση σε όλους, δωρεάν.\n\n***\n\nΔωρεάν.\nΑνοιχτού κώδικα με άδεια δημόσιας χρήσης (GPLv3)\nΑπό τους χρήστες για τους χρήστες.\n\nΣυνεισφέροντες @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nΣυνεισφέροντες @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nΕίναι μια αρκετά πρόωρη έκδοση, κρατήστε το υπόψη κατά την αξιολόγηση.\n\nΑρχείο αλλαγών του έργου:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "en-US": "uBlock Origin is <b>not</b> an \"ad blocker\", it's a wide-spectrum content blocker with CPU and memory efficiency as a primary feature.\n\n***\n\nOut of the box, uBO blocks ads, trackers, coin miners, popups, etc. through the following lists of filters, enabled by default:\n\n- EasyList (ads)\n- EasyPrivacy (tracking)\n- Peter Lowe’s Ad server list (ads and tracking)\n- Online Malicious URL Blocklist\n- uBO's own lists\n\nMore lists are available for you to select if you wish:\n\n- EasyList Cookie\n- Fanboy Annoyances\n- AdGuard Annoyances\n- Dan Pollock’s hosts file\n- And many others\n\nAdditionally, you can point-and-click to block JavaScript locally or globally, create your own global or local rules to override entries from filter lists, and many more advanced features.\n\n***\n\nFree.\nOpen source with public license (GPLv3)\nFor users by users.\n\nIf ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\n<ul><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/788d66e7299bdfb1da05832994551640d0ad441e148a3e29afe8dd0a5a90800c/https%3A//github.com/gorhill/uBlock%23ublock-origin\" rel=\"nofollow\">Documentation</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">Release notes</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/32c3d6819f5263e56c265042e8d34e2da4d974e73a7ad55a81786d8995cf65a9/https%3A//www.reddit.com/r/uBlockOrigin/\" rel=\"nofollow\">Community support @ Reddit</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">Contributors @ GitHub</a></li><li><a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">Contributors @ Crowdin</a></li></ul>",
+ "es": "Un bloqueador eficiente: capaz de cargar y aplicar miles más de filtros en comparación con otros populares bloqueadores, manteniendo un mínimo consumo de memoria y CPU.\n\nEjemplo con imágenes ilustrando su eficiencia (en inglés): <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUso: El botón grande de apagado/encendido en la ventana emergente de la extensión, es para deshabilitar/habilitar uBlock₀ permanentemente en el sitio web actual. Aplica solo al sitio web actual, no activa o desactiva la extensión de forma general.\n\n***\n\nFlexible, es más que un \"bloqueador de anuncios\": también puede leer y crear filtros desde archivos hosts.\n\nPor defecto ya trae configuradas las siguientes listas de filtros:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nOtras listas disponibles pueden ser seleccionadas, si se desea:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Y muchas más\n\nPor supuesto, mientras más filtros se activen, mayor será el consumo de memoria. No obstante, incluso después de agregar las dos listas adicionales de \"Fanboy's\" y la \"hpHosts’s Ad and tracking servers\", uBlock₀ consume menos memoria que otros bloqueadores similares.\n\nTambién tenga en cuenta que seleccionar algunas de estas listas adicionales puede conducir a una mayor probabilidad de aparición de problemas al mostrar un sitio web -- especialmente las listas utilizadas normalmente como archivo hosts.\n\n***\n\nSin las listas preestablecidas de filtros, esta extensión no sería nada. Así que si alguna vez realmente quieres aportar algo, piensa en las personas que trabajan duro para mantener estas listas de filtros, disponibles de forma gratuita para todos.\n\n***\n\nLibre.\nCódigo abierto con licencia pública (GPLv3)\nHecho para usuarios por los usuarios.\n\nColaboradores @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nColaboradores @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/7bae4395c4e5926bb237c1ef9b0f391cb005dbdbf58f4c9e47298db9bb6d1f57/https%3A//crowdin.com/project/ublock\" rel=\"nofollow\">https://crowdin.com/project/ublock</a>\n\n***\n\nRegistro de cambios del proyecto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "eu": "Blokeatzaile eraginkor bat: Memoria eta PUZ erabileran arina da, eta hala ere beste blokeatzaile ezagun batzuk baino milaka iragazki gehiago kargatu eta ezarri ditzake.\n\nBere eraginkortasunaren adibide grafikoa: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nErabilera: Laster-leihoko pizte botoi handia uBlock uneko gunean behin betiko gaitu edo desgaitzeko da. Uneko guneari dagokio soilik, ez da botoi orokor bat.\n\n***\n\nMalgua, iragarki blokeatzaile bat baino gehiago da, ostalarietako iragazkiak sortu eta irakurri ditzake ere.\n\nLehenetsita, honako iragazki zerrendak kargatu eta ezartzen ditu:\n\n- EasyList\n- Peter Loweren iragarki zerbitzarien zerrenda\n- EasyPrivacy\n- Malware domeinuak\n\nZerrenda gehiago dituzu eskura hautatzeko hala nahiez gero:\n\n- Fanboyren hobetutako jarraipen zerrenda\n- Dan Pollocken ostalari zerrenda\n- hpHostsen iragarki eta jarraipen zerbitzariak\n- MVPS Ostalariak\n- Spam404\n- Eta beste hainbat gehiago\n\nJakina, iragazki gehiago kargatuta memoria erabilera handiagoa da. Hala ere, Fanboyren bi zerrenda gehigarriak eta hpHostsen iragarki eta jarraipen zerbitzariak kargatuta, uBlockek beste blokeatzaile ezagun batzuk baino memoria gutxiago erabiltzen du.\n\nBestalde, kontuan izan zerrenda gehigarri hauetako batzuk gaitzeak guneren bat hausteko aukerak handitzen dituela, batez ere ostalari fitxategi gisa erabili ohi diren zerrendak.\n\n***\n\nLehenetsitako iragazki zerrendarik gabe gehigarri honek ez du ezer egiten. Beraz ezertan lagundu nahi baduzu pentsa ezazu erabiltzen dituzun iragazki zerrendak egunean mantentzeko tinko lanean dabiltzan horietan, guztiek erabiltzeko moduan doan eskuragarri jarri dituztenak.\n\n***\n\nDoan.\nLizentzia libreduna (GPLv3)\nErabiltzaileek erabiltzaileentzat sortua.\n\nParte-hartzaileak @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nParte-hartzaileak @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nNahiko berria da bertsio hau, kontua izan honi buruz idaztean.\n\nProiektuaren aldaketa egunkaria:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fa": "یک بلاکر موثر: نیاز به پردازش حافظه و سی پی یو کمتر و در عین حال اجرای هزاران فیلتر بیشتر از سایر رقبای بلاکر موجود.\n\nبررسی تصویری از کارایی این محصول: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nکاربرد: دکمه ی پاور بزرگ در پنجره برای فعال یا غیر فعال کردن uBlock برای صفحه ی جاری است. فقط برای همین سایت اعمال میشود، دکمه ی پاوری برای تمام سایت ها نیست.\n\n***\n\nانعطاف پذیری آن بیشتر از \"ad blocker\" است: همچنین می تواند فیلتر ها را از هاست میزبان، بخواند و بسازد.\n\nبیرون از جعبه، این لیست فیلترها بارگذاری و اجرا میشوند:\n\n- EasyList\n- لیست سرور تبلیغاتی Peter Lowe\n- EasyPrivacy\n-دامنه های تروجان\n\nاگر میخواهید لیست های بیشتر برای انتخاب شما در دسترس هستند:\n\n- ردیابی لیست پیشرفته ی Fanboy\n- میزبانی فایل Dan Pollock\n- تبلیغ و ردیابی سرور hpHosts\n- هاست های MVPS\n- اسپم 404\n- و بسیاری دیگر\n\nالبته هرچه فیلترهای بیشتری فعال باشند، حافظه ی بیشتری اشغال خواهد شد. با اینحال، حتی پس از اضافه کردن دو لیست اضافی Fanboy و سرور های ردیابی و تبلیغ hpHosts ، میبینیم که uBlock هنوز حافظه پایین تری از دیگر برنامه های مشابه اشغال میکند.\n\nهمچنین، بدانید که انتخاب برخی از این لیست ها ممکن است افزایش احتمال شکستگی وب سایت--به ویژه آنهایی که به طور معمول به عنوان میزبان فایل شناخته میشوند را در پی داشته باشد.\n\n***\n\nبدون فهرست از پیش تعیین شده ی فیلتر، این افزونه هیچ است. پس اگر واقعا می خواهید کمکی کرده باشید، به افرادی فکر کنید که برای حفظ لیست فیلتر مورد استفاده شما سخت کار میکنند که برای استفاده همه به رایگان در دسترس باشد.\n\n***\n\nرایگان.\nمتن باز با مجوز عمومی (GPLv3)\nبرای کاربران توسط کاربران.\n\nمشارکت کنندگان در گیت هاب: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nمشارکت کنندگان در کرادین <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nاین کاملا یک نسخه اولیه است، هنگام بررسی اینرا بخاطر داشته باشید.\n\nتغییرات اخیر پروژه:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fi": "Tehokas mainosten estäjä – käyttää vähän resursseja, mutta silti voit ladata ja pakottaa tuhansia suodatinsääntöjä enemmän kuin muut suositut mainoksia estävät lisäosat.\n\nKuvitettu yleiskatsaus uBlockin tehokkuudesta (englanniksi): <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nKäyttö: Iso virtanappi ponnahdusikkunassa on tarkoitettu pysyvästi estämään/sallimaan uBlock kyseisellä sivulla. Tämä koskee vain nykyistä sivua, ei kaikkia sivuja.\n\n***\n\nJoustava, tämä lisäosa on enemmän kuin perinteinen \"mainosten estäjä\". Voit lukea ja luoda suodattimia myös hosts-tiedostoista.\n\nNämä suodatinlistat ovat automaattisesti ladattuna ja kytketty päälle:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nHalutessasi voit valita käyttöösi lisää listoja:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Ja monia muita\n\nJos sinulla on useita listoja käytössä, keskusmuistia kuluu enemmän. Tästä huolimatta, vaikka lisäisit Fanboyn kaksi lisälistaa ja hpHostsin listan, uBlockilla on silti pienempi muistinkulutus kuin muilla suosituilla mainosten estäjillä.\n\nUseiden listojen lisääminen saattaa aiheuttaa sivujen kaatumisen tai hajoamisen. Etenkin listat, joita käytetään normaalisti hosts-tiedostona, voivat aiheuttaa ongelmia.\n\n***\n\nTämä lisäosa ei tee mitään ilman suodatinlistoja. Jos siis haluat osallistua jotenkin, muistathan kaikki ne ihmiset jotka työskentelevät pitääkseen käyttämäsi suodatinlistat ajan tasalla ja saatavilla ilmaiseksi.\n\n***\n\nIlmainen.\nAvoimen lähdekoodin julkinen lisenssi (GPLv3)\nKäyttäjiltä käyttäjille.\n\nKehittäjät @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKehittäjät @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nOtathan huomioon testatessasi, että käytössäsi on varsin varhainen versio.\n\nProjektin muutosloki:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fr": "uBlock est une extension qui bloque les publicités et les pisteurs, légère en empreinte mémoire et en utilisation du processeur et qui pourtant, est capable d'utiliser et de traiter des milliers de filtres de plus que la plupart des autres bloqueurs.\n\nConsultez cette page en Anglais pour avoir une vue d'ensemble illustrée de son efficacité : <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilisation : Le gros bouton power dans la fenêtre pop-up permet de désactiver/activer en permanence uBlock pour le site Web en cours de consultation. Cela s'applique uniquement au site Web actuel, ce n'est pas un bouton power qui affecte entièrement le fonctionnement de l'extension.\n\n***\n\nFlexible, uBlock ne prend pas en charge que les filtres de type Adblock, elle peut également lire et créer des filtres depuis des fichiers hosts.\n\nPar défaut, ces listes de filtrage sont chargées et traitées :\n\n- EasyList (Liste anti-publicités maintenue fréquemment à jour par la communauté)\n- Peter Lowe’s Ad server list (Liste de serveurs publicitaires maintenue à jour par Peter Lowe)\n- EasyPrivacy (Liste anti-pisteurs maintenue fréquemment à jour par la communauté)\n- Malware domains (Liste de protection contre des domaines malveillants)\n\nDavantage de listes sont disponibles si vous souhaitez renforcer le blocage :\n\n- Fanboy’s Enhanced Tracking List (Liste de protection avancée contre le pistage maintenue à jour par Fanboy)\n- Dan Pollock’s hosts file (Fichier Hosts bloquant publicités, domaines malveillants et autres pisteurs, maintenue fréquemment à jour par Dan Pollock)\n- hpHosts’s Ad and tracking servers (Fichier Hosts bloquant des serveurs publicitaires et des serveurs pistant, maintenue à jour par hpHosts)\n- MVPS HOSTS (Fichier Hosts bloquant publicités, domaines malveillants et autres pisteurs, maintenue à jour par MVPS)\n- Spam404 (Liste de protection contre les spams, maintenue fréquemment à jour par la communauté)\n- Et plein d'autres\n\nBien évidemment, plus vous activez de filtres, plus l'empreinte mémoire augmentera. Pourtant, même après avoir ajouté deux listes supplémentaires crées par Fanboy et le fichier Hosts d'hpHosts, uBlock₀ utilise moins de mémoire vive que tous les autres bloqueurs de pubs populaires.\n\nVeuillez tout de même prendre en compte que le fait de choisir parmi ces listes supplémentaires peut conduire à quelques incompatibilités sur les sites Web que vous visitez, bien que ces listes soient maintenues à jour par leurs auteurs.\n\n***\n\nSans les listes prédéfinies de filtres, cette extension (comme d'autres) ne serait rien. Alors si vous tenez vraiment à contribuer d'une quelconque manière, pensez aux personnes travaillant dur pour maintenir à jour ces listes de filtres que vous utilisez, qui plus est proposées gratuitement à tout le monde.\n\n***\n\nGRATUIT.\nSource libre avec une licence publique GPLv3\nFait par des utilisateurs pour des utilisateurs.\n\nContributeurs @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContributeurs @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nIl s'agit d'une version relativement préliminaire, veuillez garder ça à l'esprit lors de votre évaluation de l'extension.\n\nConsultez ici en Anglais le Journal des changements concernant le projet :\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "he": "חוסם יעיל: חותמת נמוכה של המעבד והזיכרון, ועדיין יכול לטעון ולאפשר אלפי מסננים יותר מאשר חוסמים פופולריים אחרים.\n\nסקירה כוללת על היעילות שלו: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nשימוש: לחצן ההפעלה הגדול בחלון הפופאפ הוא בשביל לבטל/להפעיל את uBlock עבור האתר הנוכחי. הוא חל על האתר הנוכחי בלבד, זהו לא לחצן הפעלה גלובלי.\n\n***\n\nגמיש, יותר מ \"חוסם פרסומות\": הוא יכול גם לקרוא וליצור מסננים מקבצי hosts.\n\nהיישר מהקופסה, רשימות המסננים הללו נטענות ומאופשרות:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nרשימות נוספות אלו זמינות לבחירתך אם תרצה:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- ועוד רבים אחרים\n\nכמובן שככל שכמות מסננים גדולה יותר מופעלת, ככה גם חתימת הזיכרון גדולה יותר. ובכל זאת, אפילו לאחר הוספת שתי הרשימות הנוספות של Fanboy ו hpHosts’s Ad and tracking servers, ל uBlock עדיין יש חתימת זיכרון נמוכה יותר מלחוסמים פופולריים אחרים שם בחוץ.\n\nכמו כן, תהיה מודע שבחירה של חלק מהרשימות הנוספות הללו עלולה להוביל בסבירות גבוהה לשבירה של אתרי אינטרנט -- במיוחד הרשימות אשר בדרך כלל משומשות כקובץ hosts.\n\n***\n\nללא רשימות מסננים מוגדרים מראש, תוסף זה לא שווה כלום. אז אם אי פעם תרצה באמת לתרום משהו, תחשוב על האנשים שעובדים לילות כימים כדי לתחזק את רשימות המסננים שאתה משתמש בהן, אשר הובאו לשימוש על ידי כולם ללא כל תשלום.\n\n***\n\nחינם.\nקוד פתוח עם רשיון ציבורי (GPLv3)\nבשביל המשתמשים על ידי המשתמשים.\n\nתורמים @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nתורמים @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nקח בחשבון שזוהי גרסה מוקדמת בזמן הסקירה שלך.\n\nרשימת השינויים של הפרויקט:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "hu": "Egy hatékony blokkoló: kíméletes a processzorral és a memóriával, mégis képes nagyságrendekkel több szűrő betöltésére és alkalmazására a többi népszerű blokkolóhoz viszonyítva.\n\nÁttekintés a hatékonyságáról: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nHasználat: A nagy bekapcsológomb a kiegészítő tiltására/engedélyezésére szolgál a jelenlegi webhelyen. A gomb kizárólag a jelenlegi webhelyre érvényes, nem egy globális kapcsoló.\n\n***\n\nTöbb mint egy egyszerű reklámblokkoló: képes hosts fájlok bejegyzéseit is értelmezni, és azokból szűrőket létrehozni.\n\nAlapértelmezetten a következő szűrőlisták kerülnek betöltésre és alkalmazásra:\n\n- EasyList\n- Peter Lowe hirdetési szerverlistája\n- EasyPrivacy\n- Kártékony domainek\n\nEgyéb listák is kiválaszthatók a felhasználó igénye szerint:\n\n- Fanboy bővített követők listája\n- Dan Pollock hosts fájlja\n- hpHosts hirdetés és követőszerverek listája\n- MVPS HOSTS\n- Spam404\n- És sok más\n\nTermészetesen, több szűrő használatával a memóriaigény is növekszik. Ennek ellenére Fanboy két extra listája és a hpHosts (reklám és követőszerverek) lista hozzáadásával a uBlock memóriafogyasztása még mindig alacsonyabb, mint a legnépszerűbb blokkolóké.\n\nEmellett, néhány extra lista kiválasztásával megnövekszik az esély arra, hogy a webhelyek használhatatlanná válnak -- főleg azon listákról van szó, melyek normál esetben hosts fájlként használatosak.\n\n***\n\nA szűrőlisták nélkül a kiegészítő nem sokat érne. Tehát, ha valaha is eszedbe jutna támogatást kínálni, akkor előbb gondolj azokra, akik keményen dolgoznak a listák karbantartásával, illetve ingyenesen hozzáférhetővé teszik azokat mindenki számára.\n\n***\n\nIngyenes.\nNyílt forráskódú nyilvános licenccel (GPLv3)\nFelhasználóknak felhasználóktól.\n\nKözreműködők a Github-on: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKözreműködők a Crowdin-en: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEz még egy elég korai verzió, amit illik szem előtt tartani értékeléskor.\n\nVáltozások listája:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "id": "Pemblokir yang efisien: ringan penggunaan memori dan CPU, namun dapat memuat dan menjalankan ribuan filter lain dibanding pemblokir populer lain di luar sana.\n\nRingkasan ilustrasi efisiensi: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-Compared\n\nPenggunaan: Tombol daya yang besar dalam popup berfungsi menonaktifkan/mengaktifkan uBlock secara permanen untuk situs yang sedang dibuka. Berlaku untuk situs yang sedang dibuka saja, bukan tombol daya global.\n\n***\n\nFleksibel, lebih dari sekedar \"pemblokir iklan\": juga dapat membaca dan membuat filter dari berkas host.\n\nLangsung bekerja, daftar filter berikut ini dimuat dan dijalankan:\n\n- EasyList\n- Daftar server iklan Peter Lowe\n- EasyPrivacy\n- Domain malware\n\nJika anda ingin, masih banyak tersedia daftar lain untuk anda pilih:\n\n- Daftar Pelacakan Fanboy yang DItingkatkan\n- Berkas host Dan Pollock\n- Server iklan dan pelacakan hpHosts\n- HOST MVPS\n- Spam404\n- dan banyak lainnya\n\nTentu saja, semakin banyak filter yang diaktifkan, semakin besar penggunaan memori. Namun, bahkan setelah menambahkan 2 daftar ekstra Fanboy, server iklan dan pelacakan hpHosts, penggunaan memori uBlock masih lebih kecil dibanding pemblokir iklan populer lain di luar sana.\n\nPerlu diketahui juga bahwa memilih beberapa daftar ekstra juga berpeluang lebih tinggi menyebabkan kerusakan situs -- terutama daftar yang biasanya digunakan sebagai berkas host.\n\n***\n\nTanpa daftar filter yang ada, ekstensi ini bukanlah apa-apa. Jadi, jika Anda benar-benar ingin berkontribusi sesuatu, berpikirlah tentang orang-orang yang bekerja keras mengelola daftar filter yang anda gunakan, yang dibuat dan tersedia untuk digunakan oleh semua dengan gratis.\n\n***\n\nGratis.\nSumber terbuka dengan lisensi publik (GPLv3)\nUntuk pengguna oleh pengguna.\n\nKontributor @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKontributor @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nMasih dalam versi yang sangat awal, mohon diingat ketika anda membuat ulasan.\n\nCatatan perubahan proyek:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "it": "uBlock è un efficiente ad-blocker: occupa poca memoria e poca CPU, ma può usare migliaia di filtri in più rispetto ad altri software simili.\n\nConsulta questa pagina (in inglese) per verificare la sua efficacia <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUso: il pulsante power nel popup serve per disabilitare/abilitare permanentemente uBlock nel sito che stai visitando. e non serve per disabilitare/abilitare l'estensione.\n\n***\n\nMolto più che un ad-blocker: può anche creare filtri dal file host.\n\nPer default sono attivate queste liste:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPuoi anche attivare moltre altre liste:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Ecc.\n\nOvviamente, più liste attivi, maggiore sarà l'impatto sulla memoria. Anche aggiungendo altre due liste di Fanboy, ad di hpHosts e tracking server, uBlock userà meno memoria di molti altri ad-blocker.\n\nSelezionando alcuni di questi filtri può portare ad una maggiore probabilità di problemi nel visualizzare alcuni siti web.\n\n***\n\nSenza queste liste di filtri, questa estensione non è niente. osì se vuoi contribuire, pensa alle persone che lavorano duramente per mantenere queste liste che stai usando, che sono disponibili gratuitamente.\n\n***\n\nGratuito.\nOpen source with public license (GPLv3)\nFatto dagli utenti per gli utenti.\n\nCollaboratori @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nCollaboratori @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nQuesta è una versione preliminare, ricordalo quando scriverai una recensione.\n\nPer leggere le novità di ogni versione consulta questa pagina (In Inlgese):\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ja": "効率的なブロッカー:コンピュータのメモリとCPUのフットプリントはより少なく\n、別の人気のブロッカーよりも何千ものフィルタをロードし、強制的にブロックができます\n\n他ソフトとの比較は以下のとおり: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n使用法: ポップアップに表示される大きな電源ボタンは、現在のサイトでuBlockの有効/無効を切り替えます。 現在のサイトのみに適用されます、グローバルボタンではありません。\n\n***\n\nただの「広告ブロッカー」より柔軟です:ホストファイルを読み込みフィルターを作成できます。\n\n要するに、以下のフィルターが読み込まれ、適用されます:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nご希望であればさらに多くのリストがご利用できます:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Etc.\n\nもちろん、多くのフィルターを適用すれば使用メモリーは増えます。 ただ、それでも、Fanboy's Special Blocking List、Fanboy's Enhanced Tracking List、hpHost's Ad and tracking serversの三つのリストを追加で適用しても、uBlockは他の人気のブロッカーより少ないメモリー消費を実現しています。\n\nそれと、多くのリストの適用は(特にホストファイルとしてよく使われているリスト)ウェブサイトの崩壊を起こしかねないことに注意してください。\n\n***\n\nこの拡張機能は、あらかじめ設定されているフィルターのリストが無ければ意味を成しません。 ですので、何かしらの形で貢献したいと考えることがあった時は、これらのリストを無料で懸命に更新し続けている方々を思い出してください。\n\n***\n\n無料.\nパブリックライセンス(GPLv3)のオープンソース\nユーザーによって作られた、ユーザーのための物。\n\n貢献者 @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n貢献者 @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nこれは割と初期のバージョンですので、それを念頭にレビューをお願いします。\n\nプロジェクト変更ログ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ka": "რეკლამების შედეგიანი შემზღუდავი: მეხსიერებისა და პროცესორის შემსუბუქებული მოხმარება, რეკლამების სხვა შემზღუდავებთან შედარებით, ათასობით მეტი ფილტრის გამოყენების პირობებშიც კი.\n\nშედეგიანობის მიმოხილვა იხილეთ ბმულზე: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nგამოყენება: ჩართვის დიდი ღილაკით, ჩამოშლილ მენიუში, შესაძლებელია uBlock-ის ჩართვა/გამორთვა მიმდინარე ვებსაიტზე. ეს ღილაკი მოქმედებს მხოლოდ არსებულ საიტზე და არ გამოიყენება ზოგადად ჩართვა/გამორთვისთვის.\n\n***\n\nმეტად მოქნილი, ეს არაა უბრალოდ „რეკლამების შემზღუდავი“: ასევე შესაძლებელია hosts ფაილის წაკითხვა და ფილტრების შექმნა.\n\nგარდა ამისა, ნაგულისხმევად ჩართულია და გამოიყენება შემდეგი გასაფილტრი სიები:\n\n- EasyList\n- Peter Lowe-ის სარეკლამო სერვერების სია\n- EasyPrivacy\n- მავნე დომენები\n\nასევე, ხელმისაწვდომია სიები სურვილისამებრ შესარჩევად:\n\n- Fanboy-ის გაუმჯობესებული წესები თვალყურისმდევნებლების შესაზღუდად\n- Dan Pollock-ის hosts ფაილი\n- hpHosts-ის სარეკლამო და თვალყურისმდევნელი სერვერები\n- MVPS HOSTS\n- Spam404\n- და კიდევ ბევრი\n\nრასაკვირველია, რაც უფრო მეტი ფილტრია ჩართული, მეხსიერების გამოყენება იზრდება. თუმცა, Fanboy-ის გაფართოებული წესების, hpHosts-ის სარეკლამო და თვალყურისმდევნელი სერვერების დამატების შემთხვევაშიც კი, uBlock მაინც ნაკლებ მეხსიერებას იყენებს, ვიდრე ყველა სხვა ცნობილი შემზღუდავი პროგრამები.\n\nამასთან, გაითვალისწინეთ, რომ ზოგიერთი დამატებითი წესების შერჩევის შედეგად, შესაძლოა ვებსაიტები არ გამოჩნდეს გამართულად -- განსაკუთრებით იმ წესების შემთხვევაში, რომელიც ჩვეულებრივ, hosts ფაილად გამოიყენება.\n\n***\n\nწინასწარ შედგენილ წესებს, მნიშვნელოვანი ადგილი უჭირავს ამ გაფართოების შედეგიან მუშაობაში. ასე რომ, თუ ოდესმე გადაწყვეტთ ვინმესთვის შემოწირულობის გაღებას, იფიქრეთ იმ ადამიანებზე, რომლებიც თავდაუზოგავად შრომობენ იმ გასაფილტრი წესების მუდმივ განახლებაზე, რომლითაც სარგებლობთ და რომელიც ხელმისაწვდომია ყველასთვის უფასოდ.\n\n***\n\nუფასო.\nღია წყაროს მქონე საჯარო ლიცენზიით (GPLv3)\nმომხმარებლების მიერ, მომხმარებლებისთვის.\n\nწვლილის შემტანები @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nწვლილის შემტანები @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nცვლილებების ჩამონათვალი:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ko": "효율적인 차단기: 메모리와 CPU에 부담이 적고, 다른 인기있는 차단기에 비해 수 천 가지의 필터를 사용할 수 있습니다.\n\n효율성에 대한 소개: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n사용 방법: 해당 웹사이트에서 팝업의 큰 전원 버튼을 눌러 uBlock을 켜고 끌 수 있습니다. 적용은 현재 웹사이트만 적용되며, 전체적으로 적용되지 않습니다.\n\n***\n\n\"AdBlocker\" 보다 더 유연합니다: 호스트 파일들로부터 필터를 만들고 볼 수 있습니다.\n\n특별한 설치 없이도 아래 목록들을 불러오고 적용할 수 있습니다:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\n당신이 원한다면 더 많은 목록을 선택할 수 있습니다:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 그리고 무수히 많은 다른 목록들\n\n물론, 더 많은 필터를 활성화하면 할수록, 메모리 사용량도 높아집니다. 하지만 Fanboy's two extra lists와 hpHosts’s Ad and tracking servers 필터를 추가한 후에도 uBlock₀은 다른 인기있는 차단기에 비해 메모리 사용량이 적습니다.\n\n또, 이러한 일부 추가 목록(특히 일반적으로 사용되는 호스트 파일) 중 선택시 높은 확률로 웹사이트가 파손될 수 있음을 명심해주시기 바랍니다.\n\n***\n\n필터에 필터 목록이 하나도 없다면, 이 확장기능은 아무 쓸모가 없어집니다. 그래서 만약 당신이 정말 어떤것으로든 기여하고 싶을때는, 당신이 사용중인 필터 리스트를 만들고 유지하기 위해 노력중인 사람들을 생각해주세요. 필터들은 모두 무료로 사용이 가능하게 되어있습니다.\n\n***\n\n완전히 무료입니다.\n오픈소스이며, 공개 라이센스(GPLv3)를 따릅니다.\n사용자를 위해, 사용자에 의해 만들어졌습니다.\n\n기여자 @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n기여자 @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n이것은 완전히 초기 버전입니다, 리뷰할 때 이 점을 명심하세요.\n\n프로젝트 변경사항:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "nl": "Een efficiënte adblocker: gebruikt weinig processorkracht en geheugen. Toch kan het duizenden filters meer laden en toepassen dan andere populaire adblockers.\n\nGeïllustreerde efficiëntievergelijking: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nGebruik: met de grote aan-uitknop in de pop-up kan uBlock voor de huidige website permanent worden in- of uitgeschakeld. Het wordt alleen op de huidige website toegepast; dit is geen algemene aan-uitknop.\n\n***\n\nFlexibel, want het is meer dan een ‘adblocker’: het kan ook filters inlezen en aanmaken vanuit hosts-bestanden.\n\nStandaard worden de volgende filterlijsten geladen en toegepast:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nEr zijn meer lijsten beschikbaar die u kunt inschakelen:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- en nog vele andere...\n\nNatuurlijk wordt het geheugengebruik groter naarmate er meer filters worden ingeschakeld. Maar zelfs na het inschakelen van Fanboy’s twee extra lijsten, hpHosts’s Ad en tracking servers, heeft uBlock een lager geheugengebruik dan andere populaire blockers.\n\nLet op, het gebruik van sommige van deze extra lijsten verhoogt de kans dat websites niet goed worden weergegeven - zeker de lijsten die normaal als hosts-bestand worden gebruikt.\n\n***\n\nZonder de standaard filterlijsten doet deze extensie niets. Als u dus ooit echt een bijdrage wilt leveren, denk dan aan de mensen die hard werken om de filterlijsten die u gebruikt te onderhouden, welke allemaal gratis beschikbaar zijn gemaakt.\n\n***\n\nVrij.\nOpen source met publieke licentie (GPLv3)\nVoor gebruikers, door gebruikers.\n\nMedewerkers @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nMedewerkers @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nOnthoud dat dit een hele vroege versie is wanneer u een beoordeling geeft.\n\nProjectwijzigingenlogboek:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pl": "Skutecznie blokuje reklamy, używa mało pamięci RAM i zasobów procesora, a przy tym może wczytywać i stosować o wiele więcej filtrów niż inne popularne rozszerzenia do blokowania reklam.\n\nIlustrowane porównanie z dodatkiem Adblock Plus: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nSposób użycia: wyświetlany w małym wyskakującym oknie przycisk służy do włączenia/wyłączenia rozszerzenia na bieżącej witrynie internetowej. Działanie przycisku ma zastosowanie tylko na bieżącej witrynie – nie działa globalnie.\n\n***\n\nElastyczny. Jest czymś więcej niż „blokerem reklam”. Może czytać i tworzyć filtry z plików hostów.\n\nPo zainstalowaniu są wczytywane i stosowane następujące listy filtrów:\n\n- EasyList\n- Lista serwerów reklam Petera Lowe'a\n- EasyPrivacy\n- Domeny ze złośliwym oprogramowaniem\n\nMożna wybrać więcej list filtrów:\n\n- Rozszerzona lista śledzenia dla fanboyów\n- Plik hostów Dana Pollocka\n- Serwery reklam i śledzenia hpHosts\n- MVPS HOSTS\n- Spam404\n- I wiele innych\n\nIm więcej filtrów jest włączonych, tym większe jest użycie pamięci RAM. Nawet po dodaniu dwóch dodatkowych list filtrów dla fanboyów – listy serwerów reklamowych i śledzących hpHosts – µBlock₀ używa mniej pamięci RAM niż inne popularne dodatki do blokowania reklam.\n\nNależy pamiętać, że wybranie niektórych dodatkowych list może prowadzić do wzrostu prawdopodobieństwa uszkodzenia witryny internetowej – zwłaszcza tych list, które są zwykle używane jako plik hostów.\n\n***\n\nBez zaprogramowanej listy filtrów, to rozszerzenie jest bezwartościowe. Pomyśl zatem o osobach, które ciężko pracują, tworząc i utrzymując udostępniane za darmo używane przez Ciebie listy filtrów.\n\n***\n\nDarmowe rozszerzenie.\nKod źródłowy udostępniany na otwartej licencji (GPLv3)\nDla użytkowników przez użytkowników.\n\nWspółtwórcy rozszerzenia: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nAutorzy tłumaczeń: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/a71b71f3a5bb24b1e45c14bf40ee736e1e270b279e4e77e849bf3a5b7650d85e/https%3A//crowdin.com/project/ublock/translators\" rel=\"nofollow\">https://crowdin.com/project/ublock/translators</a>\n\n***\n\nOceniając rozszerzenie pamiętaj, że jest to jego wczesna wersja.\n\nDziennik zmian:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pt-BR": "Um bloqueador eficaz: Com baixo consumo de memória e CPU e ainda pode carregar e aplicar milhares de filtros. Mais do que outros bloqueadores populares lá fora.\n\nVisão geral ilustrada de sua eficiência: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-compared\n\nUtilização: Use o botão de energia no pop-up para desativar/ativar o uBlock₀ para o site atual. Isso se aplica apenas ao site atual, não é um botão global.\n\n***\n\nFlexível, é mais do que um \"ad blocker\": também pode ler e criar filtros a partir de arquivos de hosts.\n\nPor padrão, essas listas de filtros são carregadas e aplicadas:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMais listas estão disponíveis para você escolher, se desejar:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- E varios outros\n\nClaro, quanto mais filtros habilitados, maior o consumo de memória. Ainda, mesmo após a adição do Fanboy's duas listas extras, hpHosts’s e servidores de rastreamento, uBlock₀ ainda tem o consumo de memória menor do que outros bloqueadores populares lá fora.\n\nTambém, esteja ciente de que selecionar algumas dessas listas extras pode levar à maior probabilidade de quebra do layout do site, especialmente aquelas listas que são normalmente usadas como arquivo hosts.\n\n***\n\nSem as listas predefinidas de filtros, esta extensão não é nada. Então, se você realmente quiser contribuir com alguma coisa, pense sobre as pessoas que trabalham duro para manter as listas de filtro que você está usando, que estão disponíveis de graça para todos.\n\n***\n\nGratuito\nCódigo aberto com licença pública (GPLv3)\nDe usuários para usuários.\n\nContribuidores no Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuidores no Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEssa é uma versão ainda em desenvolvimento, tenha isso em mente quando você avaliar.\n\nRegistro de alterações do projeto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pt-PT": "Um bloqueador eficiente: leve na memória e CPU e, no entanto, consegue carregar e aplicar milhares de filtros a mais do que outros bloqueadores populares disponíveis.\n\nVisão geral ilustrada da sua eficiência:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilização: O botão grande de energia na janela serve para desativar ou ativar, permanentemente, o uBlock para o sítio web atual. Aplica-se unicamente ao sítio web atual, não sendo um botão de energia global.\n\n***\n\nFlexível, é mais do que um bloqueador de anúncios. Pode também ler e criar filtros a partir de ficheiros de servidores.\n\nPor predefinição, estas listas de filtros são carregadas e aplicadas:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nSe quiser, estão disponíveis mais listas para seleção:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- E muitas mais\n\nObviamente que quanto maior o número de filtros ativos, maior será o consumo de memória. No entanto, mesmo após adicionar as duas listas extra do Fanboy, hpHosts Ad and tracking servers, o uBlock₀ continua a consumir menos memória do que outros bloqueadores populares disponíveis.\n\nEsteja ciente de que se selecionar mais listas extra pode resultar numa probabilidade acrescida de rutura em alguns sítios web -- especialmente nas listas que, normalmente, são utilizadas como ficheiros de servidores.\n\n***\n\nSem as listas de filtros predefinidas, esta extensão não é nada. Se realmente quiser contribuir com algo, pense nas pessoas que trabalham duro para manter as listas de filtros que usa, que foram tornadas disponíveis para uso por todos sem custos.\n\n***\n\nGrátis.\nCódigo aberto com licença pública (GPLv3)\nDe utilizadores para utilizadores.\n\nContribuidores @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuidores @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEsta é uma versão inicial, tenha isso em mente quando avaliar.\n\nRegisto de alterações do projeto:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ro": "Un blocant (paravan) eficient: folosește foarte puțin procesorul și memoria și totuși poate încărca și aplica mii de filtre în plus față de alte paravane populare.\n\nO ilustrare a eficienței poate fi observată la:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilizare: Butonul mare de pornire/oprire în fereastra paravanului este pentru a activa/dezactiva uBlock pentru saitul curent. Funcția este valabilă doar pentru saitul curent, nu la nivel global.\n\n***\n\nFlexibil, mai mult decât un „blocant de reclame”: acesta poate citi și crea filtre din fișierele de gazde (hosts).\n\nÎn mod implicit, aceste liste de filtre sunt încărcate și aplicate:\n\n- EasyList\n- Lista serverelor de reclame a lui Peter Lowe\n- EasyPrivacy\n- Domenii malițioase\n\nDe asemenea, mai sunt disponibile și alte liste precum:\n\n- Lista îmbunătățită pentru urmărire a lui Fanboy\n- Lista de gazde a lui Dan Pollock\n- Lista de reclame și urmărire hpHosts\n- Gazdele MVPS\n- Spam404\n- Și multe altele\n\nDesigur, cu cât sunt mai multe filtre active cu atât mai mult este utilizată memoria. Totuși, chiar și după adăugarea în plus a două liste Fanboy și lista de reclame și urmărire hPhosts, uBlock₀ tot folosește mai puțină memorie decât restul paravanelor.\n\nDe ținut minte, că odată cu selectarea în plus a unora dintre liste se poate ajunge la afectarea aspectului saiturilor -- în special listele care sunt în mod normal liste de gazde.\n\n***\n\nFără listele prestabilite de filtre această extensie nu face nimic. Așadar, dacă totuși vreți să contribuiți, gândiți-vă la persoanele care muncesc să întrețină aceste filtre pe care le utilizați, care sunt oferite pentru utilizare gratuită.\n\n***\n\nGratuit.\nCu sursă liberă și licență publică (GPLv3)\nPentru utilizatori de la utilizatori.\n\nContribuitori pe Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuitori pe Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEste încă o aplicație recentă, gândiți-vă la acest lucru când scrieți o recenzie.\n\nLista de schimbări a proiectului:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ru": "µBlock — эффективный блокировщик: он использует меньше оперативной памяти и меньше нагружает ЦП, при этом используя больше фильтров, чем другие популярные блокировщики.\n\nИллюстрированный обзор его эффективности: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nИспользование: нажмите большую кнопку «Включение» в выпадающем окне, чтобы включить или отключить uBlock для текущего сайта. Это действует только для текущего сайта, а не для всех.\n\n***\n\nБудучи гибким, это нечто большее, чем просто «блокировщик рекламы»: он также может считывать и создавать фильтры из хост-файлов.\n\nПо умолчанию следующие списки фильтров будут загружены и применены:\n\n- EasyList\n- Список рекламных серверов Питера Лоу\n- EasyPrivacy\n- Вредоносные домены\n\nТакже на выбор доступны другие списки:\n\n- Фанатский улучшенный список отслеживания\n- Хост-файл Дэна Поллока\n- Рекламные и отслеживающие сервера hpHosts\n- MVPS HOSTS\n- Spam404\n- И т. д.\n\nКонечно, чем больше фильтров, тем выше использование памяти. Тем не менее даже после добавления трёх дополнительных списков uBlock₀ всё ещё потребляет меньше памяти, чем другие популярные блокировщики.\n\nТакже имейте в виду, что некоторые их этих списков имеют высокую вероятность поломать веб-сайт, особенно те, что созданы из хост-файлов.\n\n***\n\nБез предустановленных списков фильтров это расширение — ничто. Так что, если вы действительно хотите внести свой вклад, подумайте о людях, усердно поддерживающих списки фильтров, предоставленные Вам для бесплатного использования.\n\n***\n\nБесплатно.\nОткрытый исходный код, публичная лицензия (GPLv3).\nДля пользователей от пользователей.\n\nУчастники на Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nУчастники на Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nЭто ещё очень ранняя версия, имейте это в виду, оценивая программу.\n\nСписок изменений:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sk": "Účinný blokovač: nezaťažuje CPU a pamäť a dokáže načítať a vynútiť o niekoľko tisíc filtrov viac ako iné populárne blokovače.\n\nIlustrovaný prehľad o jeho účinnosti: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPoužitie: Veľký vypínač vo vyskakovacom okne natrvalo zakáže/povolí uBlock pre aktuálnu webovú stránku. Vzťahuje sa len na aktuálnu webovú stránku, nie na všeobecný vypínač.\n\n***\n\nFlexibilný, je viac než len \"blokovač reklám\": dokáže tiež načítať a vytvárať filtre z hosts súborov.\n\nTieto zoznamy filtrov sú predvolene načítané a vynútené:\n\n- EasyList\n- Zoznam reklamných serverov od Petra Lowesa\n- EasyPrivacy\n- Domény malvéru\n\nĎalšie zoznamy sú k dispozícii pre vás na výber, ak si prajete:\n\n- Rozšírený stopovací zoznam od Fanboya\n- Hosts súbor od Dana Pollocka\n- Reklamné a stopovacie servery od hpHosts\n- MVPS HOSTS\n- Spam404\n- A mnoho ďalších\n\nSamozrejme, čím viac povolených filtrov, tým vyššie nároky na pamäť. Aj po pridaní dvoch ďalších zoznamov od Fanboya, reklamných a stopovacích serverov od hpHost má uBlock stále menšie nároky na pamäť ako mnohé ďalšie veľmi populárne blockovače.\n\nĎalej majte na pamäti, že výber viacerých filtrov zvyšuje šancu chybného zobrazenie webov - predovšetkým u zoznamov, ktoré sa normálne používajú ako hosts súbory.\n\n***\n\nBez predvolených zoznamov filtrov by bolo toto rozšírenie k ničomu. Ak teda naozaj budete chcieť niečím prispieť, myslite na ľudí, ktorí spravujú vami používané zoznamy filtrov a uvoľňujú ich pre všetkých zadarmo.\n\n***\n\nBezplatný.\nOtvorený zdrojový kód s verejnou licenciou (GPLv3)\nPre používateľov od používateľov.\n\nPrispievatelia @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPrispievatelia @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nIde o pomerne skorú verziu, majte to na pamäti pri recenzovaní.\n\nZoznam zmien projektu:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sl": "Efektiven zatiralec oglasov: lahek na pomnilniku in procesorju, in vendar lahko nalaga in uveljavlja tisoče filtrov več kot kakšen drug popularen dodatek za blokiranje oglasov.\n\nIlustrirana efektivnost: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUporaba: Velik gumb za vklop/izklop v pojavnem oknu je namenjen trajnemu izklopu/vklopu uBlock₀ za trenutno spletno stran. Ta uporaba velja samo za trenutno spletno stran, tako da gumb ne predstavlja globalnega vklopa/izklopa.\n\n***\n\nuBlock₀ je fleksibilen - in s tem več kot samo \"blokada oglasom\": lahko bere in ustvarja filtre iz datotek z gostitelji (HOSTS datoteka).\n\nBrez kakršnihkoli dodatnih nastavitev, uBlock₀ uporablja sledeče filtre:\n\n- EasyList\n- Seznam oglaševalskih strežnikov Peter Lowe\n- EasyPrivacy\n- Zlonamerne domene\n\nVeč filtrskih seznamov na razpolago (če to želite):\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- In mnogi drugi\n\nSeveda, več filtrov kot je aktivnih, večji je odtis v pomnilniku. Pa kljub temu - tudi z nalaganjem dveh dodatnih seznamov filtrov (Fanboy in hpHosts) ima uBlock₀ še vedno nižjo mero obremenitve pomnilnika kot ostali zelo popularni dodatki za blokiranje oglasov.\n\nPoleg tega bodite pozorni, da vklop določenih dodatnih seznamov filtrov lahko pripelje do višje verjetnosti za nefunkcionalnost spletne strani - predvsem \"ogrožajoči\" so tisti seznami, ki se jih ponavadi uporablja kot HOSTS datoteko.\n\n***\n\nBrez prednastavljenih seznamov filtrov, da dodatek ni nič. Tako da, če res želite kje pomagati ali komu plačati kavo, pomislite na ljudi, ki trdo delajo, da vzdržujejo te sezname filtrov, ki jih uporabljate, in so jih naredili dosegljive zastonj in za vse.\n\n***\n\nZastonj.\nOdprtokodno pod GPLv3 licenco\nZa uporabnike od uporabnikov.\n\nRazvijalci @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPrevajalci @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nGre še za dokaj sveže različice, prosimo da to upoštevate pri vaši kritiki.\n\nDnevnik sprememb projekta:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sq": "Një bllokues efikas: me impakt të vogël te memorja dhe procesori, por mund të hapë dhe të zbatojë mijëra filtra më shumë sesa bllokuesit e tjerë të njohur.\n\nPërmbledhje e ilustruar e efikasitetit të tij: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPërdorimi: Çelësi i komandimit te dritarja e vogël e bën uBlock përherë joaktiv/aktiv për uebsajtin aktual. Ai vlen vetëm për uebsajtin aktual, nuk është një çelës i përgjithshëm.\n\n***\n\nËshtë fleksibël dhe jo thjesht një \"bllokues reklamash\": mund të lexojë dhe të krijojë filtra nga skedat \"hosts\".\n\nFiltrat e listuar këtu hapen dhe zbatohen pas instalimit:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPo të doni, ka edhe shumë lista të tjera të gatshme:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Dhe shumë të tjera\n\nSigurisht që sa më shumë filtra të aktivizoni, aq më i madh do të jetë impakti te memorja. Edhe me shtimin e dy listave shtesë të Fanboy, hpHosts’s Ad and tracking servers, uBlock përsëri ka impakt më të ulët në memorje sesa bllokuesit e tjerë shumë të njohur.\n\nPor, kujdes, sepse duke përzgjedhur disa prej këtyre listave, gjasat që faqet të shfaqin probleme do të jenë më të mëdha -- sidomos listat që normalisht përdoren si skeda \"hosts\".\n\n***\n\nPa listat e programuara, ky program nuk vlen për asgjë. Prandaj, po të doni të kontribuoni diçka, mendoni pak për njerëzit që punojnë fort për mirëmbajtjen e listave me filtra që po përdorni, të cilat na ofrohen të gjithëve pa pagesë.\n\n***\n\nFalas.\nMaterial i hapur me licencë publike (GPLv3)\nKrijuar nga përdoruesit për përdoruesit.\n\nKontributorët @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKontributorët @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nKur të bëni vlerësimin e programit, mos harroni se ky është një version paraprak.\n\nDitari i projektit:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sv-SE": "En effektiv blockerare: lätt på minne och CPU-fotavtryck, som ändå kan ladda och applicera tusentals fler filter jämfört med andra populära blockerare där ute.\n\nIllustrerad översikt av dess effektivitet:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nAnvändning: Den stora strömbrytarikonen i popuprutan är till för att avaktivera/aktivera uBlock₀ på den aktuella webbplatsen permanent. Detta gäller enbart för den aktuella webbplatsen, det är inte en global strömbrytare.\n\n***\n\nFlexibel. uBlock₀ är inte enbart en \"reklamblockerare\": den kan också läsa och skapa filter från hosts-filer.\n\nSom standard är följande filterlistor laddade och applicerade:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nFler filterlistor finns tillgängliga att använda om du vill:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- med flera\n\nJu fler aktiverade filter, desto högre minnesanvändning. Men även efter att ha lagt till Fanboys två extra filterlistor och hpHosts' Ad and tracking servers så använder uBlock₀ mindre minne än andra väldigt populära blockerare.\n\nTänk på att genom att aktivera vissa av dessa extra filterlistor finns det större risk att webbplatser går sönder - särskilt de listor som i normala fall används som hosts-filer.\n\n***\n\nuBlock₀ vore ingenting utan filterlistorna. Så om du vill bidra med någonting, tänk på människorna som arbetar hårt med att upprätthålla de filterlistor du använder, vilka är fritt tillgängliga för allas användning.\n\n***\n\nGratis.\nÖppen källkod med offentlig licens (GPLv3)\nFör användare, av användare.\n\nBidragsgivare @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nBidragsgivare @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDetta är en ganska tidig version, tänk på detta när du skriver en recension.\n\nProjektets ändringslogg:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "uk": "Ефективний блокувальник реклами: сильно не навантажує пам’ять та процесор і може працювати з набагато більшою кількістю фільтрів ніж інші блокувальники.\n\nІлюстрований огляд ефективності: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nВикористання: Ця велика кнопка живлення у виринаючому вікні дозволяє вимкнути або увімкнути uBlock для поточного веб-сайту. Ефект розповсюджується тільки на поточний веб-сайт. Це не глобальна кнопка живлення.\n\n***\n\nБудучи універсальним, це більш ніж просто \"блокувальник реклами\". Він також може створювати фільтри з файлів hosts.\n\nЗа замовчуванням завантажено та застосовано наступні списки фільтрів:\n\n– EasyList\n– список рекламних серверів Петра Лоу\n– EasyPrivacy\n– шкідливі домени\n\nНаступні списки можна можна увімкнути за бажанням:\n\n– покращений список слідкування від Fanboy\n– файл хостів Дена Полока\n– сервери реклами та слідкування hpHosts\n– MVPS HOSTS\n– Spam404\n– тощо.\n\nЗвичайно ж, чим більше фільтрів ви увімкнете тим більшим буде використання пам’яті. Однак, навіть після додання двох додаткових списків Fanboy, серверів слідкування та реклами phHosts, uBlock споживає менше пам’яті ніж інші популярні блокувальники.\n\nТакож майте на увазі, що задіяння деяких додаткових списків може спричинити збільшення ймовірності пошкодження функціонування сайту. Особливо ті списки, які зазвичай використовуються як hosts-файл.\n\n***\n\nБез встановлених списків фільтрів це розширення – ніщо. Тому, якщо ви дійсно хочете зробити свій внесок, подумайте про людей, які тяжко працюють для підтримки списків фільтрів якими ви користуєтесь безкоштовно.\n\n***\n\nБезкоштовно.\nВідкритий джерельний код та публічна ліцензія (GPLv3)\nДля користувачів від користувачів.\n\nУчасники @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nПерекладачі @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nЦе ще дуже дочасна версія, тому майте на увазі, коли робите огляд.\n\nЖурнал змін проекту:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ur": "ایک زبردست اشتہارات کو روکنے والا سافٹویئر. کم میموری اور cpu استعمال کرتا ہے مگر کام بہترین کرتا ہے.\n\nاس کا بہترین اور پراثر کام کرنے کی تصاویر:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nہدایات: بڑا آن/ آف کا بٹن دبا کر آپ موجودہ ویب سائٹ پر uBlock کو فعال یا غیر فعال کر سکتے ہیں. یہ بٹن صرف موجودہ ویب سائٹ کے لئے ہے، باقی ویب سائٹس کو اس سے کوئی فرق نہیں پڑے گا.\n\n***\n\nFlexible, it's more than an \"ad blocker\": it can also read and create filters from hosts files.\n\nیہ والے فلٹر پہلے سے لاگو ہوں گے:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMore lists are available for you to select if you wish:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- And many others\n\nجتنے زیادہ فلٹر لگائیں گے اتنی زیادہ میموری لے گا. Yet, even after adding Fanboy's two extra lists, hpHosts’s Ad and tracking servers, uBlock still has a lower memory footprint than other very popular blockers out there.\n\nAlso, be aware that selecting some of these extra lists may lead to higher likelihood of web site breakage -- especially those lists which are normally used as hosts file.\n\n***\n\nWithout the preset lists of filters, this extension is nothing. So if ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\nمفت.\nاوپن سورس عوامی لائسنس(جی.پی.ایل ورژن ٣) کے ساتھ\nعوام کے لیے، عوام کا بنایا ہوا.\n\nمعاونین کی فہرست Github پر دیکھیں:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nمترجمین کی فہرست Crowdin پر دیکھیں:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\nپراجیکٹ میں ترقیاتی کام کا ریکارڈ:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "vi": "Một công cụ chặn quảng cáo hiệu quả: sử dụng ít bộ nhớ, CPU và có thể nạp, áp dụng hàng ngàn bộ lọc so với những công cụ chặn quảng cáo hiện nay.\n\nMinh hoạ tổng quan về tính hiệu quả của µBlock: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nSử dụng: Nút nguồn lớn trong hộp thoại popup để vô hiệu/kích hoạt vĩnh viễn uBlock cho website hiện tại. Nó chỉ áp dụng cho trang hiện tại, không phải tất cả website.\n\n***\n\nLinh hoạt, hơn cả một \"công cụ chặn quảng cáo\": µBlock có thể đọc và tạo bộ lọc từ tập tin hosts.\n\nNgay lập tức, những bộ lọc này được nạp và áp dụng:\n\n- EasyList\n- Danh sách máy chủ quảng cáo của Peter Lowe\n- EasyPrivacy\n- Malware domains\n\nCó thêm nhiều danh sách để bạn lựa chọn:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- Máy chủ quảng cáo và theo dõi hpHosts\n- MVPS HOSTS\n- Spam404\n- Và nhiều hơn nữa\n\nDĩ nhiên, khi kích hoạt thêm bộ lọc, tiện ích sẽ dùng nhiều bộ nhớ hơn. Tuy vậy, sau khi thêm hai danh sách Fanboy, máy chủ quảng cáo và theo dõi của hpHosts, uBlock vẫn dùng ít bộ nhớ hơn so với những công cụ chặn quảng cáo rất phổ biến khác.\n\nNgoài ra, lưu ý rằng chọn thêm một số danh sách có thể dẫn đến khả năng một số website hiển thị không đúng cách -- đặc biệt là những danh sách thường được dùng như tập tin hosts.\n\n***\n\nKhông có danh sách bộ lọc cài sẵn, tiện ích mở rộng này chẳng là gì cả. Vậy nên nếu bạn thật sự muốn đóng góp gì đó, hãy nghĩ về những người đang chăm chỉ duy trì danh sách bộ lọc hoàn toàn miễn phí mà bạn đang dùng.\n\n***\n\nMiễn phí.\nNguồn mở với giấy phép công cộng (GPLv3)\nLàm vì người dùng bởi người dùng.\n\nNhững người đóng góp @ Github: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nNhững người đóng góp @ Crowdin: <a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nĐây là một phiên bản khá mới, hãy ghi nhớ điều này khi bạn đánh giá.\n\nThay đổi của dự án:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "zh-CN": "一款高效的请求过滤工具:占用极低的内存和CPU,和其他常见的过滤工具相比,它能够加载并执行上千条过滤规则。\n\n效率概述说明: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n用法:点击弹出窗口中的电源按钮,uBlock 将对当前网页永久禁用/启用过滤功能。 它只控制当前网页的请求过滤,而不是一个全局开关。 它只控制当前网页的请求过滤,而不是一个全局开关。\n\n***\n\n它不只是一个广告拦截工具,它还可以从 hosts 文件里读取和创建过滤规则。\n\n初始默认加载和执行下列过滤规则列表:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\n这里还有更多的规则列表供你选择:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 等等\n\n当然,启用越多的过滤规则就会产生越高的内存占用。 然而,即使再添加 Fanboy 额外的两个规则列表,如 hpHosts’s Ad 和 tracking servers,uBlock 的内存占用依然比其他常见的过滤工具要低的多。\n\n另外请注意,选择一些额外的列表可能会导致网页破损可能性增高 —— 尤其是那些通常被用作 hosts 文件的列表。\n\n***\n\n没有这些过滤规则列表,这个扩展就没有了意义。 所以如果你真的想做点贡献,想想那些维护过滤规则的人们,是他们让所有人能够免费使用这一切变得可能。\n\n***\n\n免费。\n遵从 GPLv3 公共许可协议开源。\n一切为了用户。\n\n贡献者 @ Github:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n贡献者 @ Crowdin:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n它还是一个相当早期的版本,在您评论的时候请记住这一点。\n\n项目更新日志:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "zh-TW": "一款高效率的廣告攔截工具:只使用超低的記憶體和CPU使用量,和其他常見的廣告攔截工具相比,可以載入並執行上千條過濾規則。\n\n效率概述說明: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/90cf866e9b2e1ea9282c8c93e7a0891c713248d4bf07b8aaefe26d97f8ccde33/https%3A//github.com/gorhill/uBlock/wiki/%25C2%25B5Block-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/%C2%B5Block-vs.-ABP:-efficiency-compared</a>\n\n用法:點選快顯視窗中的電源按鈕,μBlock將會在目前正在瀏覽的網頁永久停用/啟用廣告攔截功能。 它只適用於目前正在瀏覽的網頁,而不是全域按鈕。\n\n***\n\n這不只是一個廣告攔截工具,它還可以非常有彈性的從hosts檔裡讀取和建立過濾規則。\n\n初始預設載入和執行下列過濾規則:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n- Long-lived malware domains\n- Malware Domains List\n\n這裡還擁有更多的過濾規則供你選擇:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 其他\n\n啟用越多的過濾規則就會佔用越多的記憶體。 然而,即使在加入 Fanboy 額外的兩個規則和 hpHosts’s Ad and tracking servers,uBlock₀ 的記憶體佔用依然比其他常見的過濾工具要小的多。\n\n另外,請注意選擇的一些額外的清單可能會導致網頁破損可能性增高 — — 尤其是那些通常用來當作hosts檔案的清單。\n\n***\n\n沒有這些過濾規則清單,這個擴充套件就沒有了意義。 所以如果你真的想要做些貢獻,試著想想那些努力維護廣告過濾規則清單的人們,至少他們讓大家可以免費使用這一切。\n\n***\n\n自由、免費。\n開放原始程式碼與公共許可證 (GPLv3)\n一切都是為了使用者。\n\n貢獻者@ Github: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n貢獻者 @ Crowdin: \n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n這還只是一個非常初期的版本,當您留下建議的時候請手下留情。\n\n專案更新日誌:\n<a href=\"https://prod.outgoing.prod.webservices.mozgcp.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>"
+ },
+ "developer_comments": null,
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/edit",
+ "guid": "uBlock0@raymondhill.net",
+ "has_eula": false,
+ "has_privacy_policy": true,
+ "homepage": {
+ "en-US": "https://github.com/gorhill/uBlock#ublock-origin"
+ },
+ "icon_url": "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "icons": {
+ "32": "https://addons.mozilla.org/user-media/addon_icons/607/607454-32.png?modified=mcrushed",
+ "64": "https://addons.mozilla.org/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "128": "https://addons.mozilla.org/user-media/addon_icons/607/607454-128.png?modified=mcrushed"
+ },
+ "is_disabled": false,
+ "is_experimental": false,
+ "last_updated": "2023-08-07T17:15:41Z",
+ "name": {
+ "ar": "uBlock Origin",
+ "bg": "uBlock Origin",
+ "bn-BD": "uBlock Origin",
+ "ca": "uBlock Origin",
+ "cs": "uBlock Origin",
+ "da": "uBlock Origin",
+ "de": "uBlock Origin",
+ "el": "uBlock Origin",
+ "en-US": "uBlock Origin",
+ "es": "uBlock Origin",
+ "eu": "uBlock Origin",
+ "fa": "uBlock Origin",
+ "fi": "uBlock Origin",
+ "fr": "uBlock Origin",
+ "he": "uBlock Origin",
+ "hu": "uBlock Origin",
+ "id": "uBlock Origin",
+ "it": "uBlock Origin",
+ "ja": "uBlock Origin",
+ "ka": "uBlock Origin",
+ "ko": "uBlock Origin",
+ "nl": "uBlock Origin",
+ "pl": "uBlock Origin",
+ "pt-BR": "uBlock Origin",
+ "pt-PT": "uBlock Origin",
+ "ro": "uBlock Origin",
+ "ru": "uBlock Origin",
+ "sk": "uBlock Origin",
+ "sl": "uBlock Origin",
+ "sq": "uBlock Origin",
+ "sv-SE": "uBlock Origin",
+ "uk": "uBlock Origin",
+ "ur": "uBlock Origin",
+ "vi": "uBlock Origin",
+ "zh-CN": "uBlock Origin",
+ "zh-TW": "uBlock Origin"
+ },
+ "previews": [
+ {
+ "id": 238546,
+ "caption": {
+ "en-US": "The popup panel: default mode"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238546.png?modified=1622132421",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238546.jpg?modified=1622132421"
+ },
+ {
+ "id": 238548,
+ "caption": {
+ "en-US": "The dashboard: stock filter lists"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238548.png?modified=1622132423",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238548.jpg?modified=1622132423"
+ },
+ {
+ "id": 238547,
+ "caption": {
+ "en-US": "The popup panel: default-deny mode"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238547.png?modified=1622132425",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238547.jpg?modified=1622132425"
+ },
+ {
+ "id": 238549,
+ "caption": {
+ "en-US": "The dashboard: settings"
+ },
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238549.png?modified=1622132426",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238549.jpg?modified=1622132426"
+ },
+ {
+ "id": 238552,
+ "caption": {
+ "en-US": "The popup panel in Firefox Preview: default mode with more blocking options revealed"
+ },
+ "image_size": [
+ 970,
+ 1800
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/238/238552.png?modified=1622132430",
+ "thumbnail_size": [
+ 216,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/238/238552.jpg?modified=1622132430"
+ },
+ {
+ "id": 230370,
+ "caption": {
+ "en-US": "The unified logger tells you all that uBO is seeing and doing"
+ },
+ "image_size": [
+ 800,
+ 600
+ ],
+ "image_url": "https://addons.mozilla.org/user-media/previews/full/230/230370.png?modified=1622132432",
+ "thumbnail_size": [
+ 533,
+ 400
+ ],
+ "thumbnail_url": "https://addons.mozilla.org/user-media/previews/thumbs/230/230370.jpg?modified=1622132432"
+ }
+ ],
+ "promoted": {
+ "apps": [
+ "firefox",
+ "android"
+ ],
+ "category": "recommended"
+ },
+ "ratings": {
+ "average": 4.7825,
+ "bayesian_average": 4.782204826721061,
+ "count": 15799,
+ "text_count": 4101
+ },
+ "ratings_url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/reviews/",
+ "requires_payment": false,
+ "review_url": "https://addons.mozilla.org/en-US/reviewers/review/607454",
+ "slug": "ublock-origin",
+ "status": "public",
+ "summary": {
+ "ar": "وأخيراً, مانع اعلانات كفوء. خفيف على المعالج و الذاكرة",
+ "bg": "Най-накрая, ефективен рекламен блокер с малки изисквания за процесор и памет.",
+ "bn-BD": "অবশেষে, একটি দক্ষ প্রতিরোধক। সিপিইউ এবং মেমরির জন্য সহজ।",
+ "ca": "Finalment, un blocador eficient que utilitza pocs recursos de memòria i processador.",
+ "cs": "Konečně efektivní blokovač, který nezatěžuje CPU a paměť.",
+ "da": "Endelig en effektiv blocker til Chromium-baserede browsere. Lavt CPU- og hukommelsesbrug.",
+ "de": "Endlich ein effizienter Blocker. Prozessor-freundlich und bescheiden beim Speicherbedarf.",
+ "el": "Επιτέλους, ένας αποτελεσματικός blocker. Ελαφρύς για τον επεξεργαστή και τη μνήμη.",
+ "en-US": "Finally, an efficient wide-spectrum content blocker. Easy on CPU and memory.",
+ "es": "Por fin, un bloqueador eficiente con uso mínimo de procesador y memoria.",
+ "eu": "Behingoz, blokeatzaile eraginkor bat. PUZ eta memorian arina.",
+ "fa": "بالاخره، یک بلاکر کارآمد. کم حجم بر روی پردازنده و حافظه",
+ "fi": "Viimeinkin tehokas ja kevyt mainosten estäjä.",
+ "fr": "Un bloqueur de nuisances efficace, qui ménagera votre processeur et votre mémoire vive.",
+ "he": "סוף סוף, חוסם יעיל. קל על המעבד והזיכרון",
+ "hu": "Végre egy hatékony reklám- és követésblokkoló böngészőkhöz, amely kíméletes a processzorral és a memóriával.",
+ "id": "Akhirnya, pemblokir iklan yang efisien. Ringan penggunaan CPU dan memori.",
+ "it": "Finalmente, un blocker efficiente. Leggero sulla CPU e sulla memoria.",
+ "ja": "高効率ブロッカーが遂に登場。CPUとメモリーの負担を抑えます。",
+ "ka": "როგორც იქნა, მძლავრი და შედეგიანი რეკლამების შემზღუდავი. ზოგავს CPU-ს და მეხსიერებას.",
+ "ko": "이 부가 기능은 효율적인 차단기입니다. CPU와 메모리에 주는 부담이 적습니다.",
+ "nl": "Eindelijk, een efficiënte adblocker. Gebruikt weinig processorkracht en geheugen.",
+ "pl": "Nareszcie skuteczne blokowanie reklam, niskie użycie procesora i pamięci.",
+ "pt-BR": "Finalmente, um bloqueador eficiente. Com baixo uso de memória e CPU.",
+ "pt-PT": "Finalmente, um bloqueador eficiente. Leve na CPU e memória.",
+ "ro": "În sfârșit, un blocant eficient. Folosește procesorul și memoria foarte puțin.",
+ "ru": "Наконец-то, быстрый и эффективный блокировщик для браузеров.",
+ "sk": "Konečne efektívny blokovač, ktorý nezaťažuje CPU a pamäť.",
+ "sl": "Končno, učinkovita, procesorju in pomnilniku prijazna razširitev za blokiranje oglasov.",
+ "sq": "Më në fund, një bllokues efikas që nuk e rëndon procesorin dhe memorjen.",
+ "sv-SE": "Äntligen en effektiv blockerare! Snäll mot processor och minne.",
+ "uk": "Ефективний блокувальник реклами таки з’явився. Не навантажує процесор та пам'ять.",
+ "ur": "آخر کار، ایک مؤثر اشتہار کو روکنے والا، یہ کم cpu اور میموری لیتا ہے",
+ "vi": "Cuối cùng, đã có một công cụ chặn quảng cáo hiệu quả, tiêu tốn ít CPU và bộ nhớ.",
+ "zh-CN": "一款高效的网络请求过滤工具,占用极低的内存和 CPU。",
+ "zh-TW": "終於出現了,一個高效率的阻擋器,使用不多的 CPU 及記憶體資源。"
+ },
+ "support_email": null,
+ "support_url": {
+ "en-US": "https://old.reddit.com/r/uBlockOrigin/",
+ "ka": "https://old.reddit.com/r/uBlockOrigin/",
+ "ur": "https://old.reddit.com/r/uBlockOrigin/"
+ },
+ "tags": [
+ "ad blocker",
+ "anti malware",
+ "anti tracker",
+ "content blocker",
+ "privacy",
+ "security"
+ ],
+ "type": "extension",
+ "url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/",
+ "versions_url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/versions/",
+ "weekly_downloads": 143905,
+ "_score": null
+ },
+ "notes": null
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/resources/collection_with_empty_values.json b/mobile/android/android-components/components/feature/addons/src/test/resources/collection_with_empty_values.json
new file mode 100644
index 0000000000..fedd44adec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/resources/collection_with_empty_values.json
@@ -0,0 +1,263 @@
+{
+ "page_size": 25,
+ "page_count": 1,
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "addon": {
+ "id": null,
+ "authors": null,
+ "average_daily_users": 5197439,
+ "categories": null,
+ "contributions_url": "",
+ "created": null,
+ "current_version": {
+ "id": 4884220,
+ "compatibility": {
+ "firefox": {
+ "min": "55.0",
+ "max": "*"
+ },
+ "android": {
+ "min": "55.0",
+ "max": "*"
+ }
+ },
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/versions/4884220",
+ "files": [],
+ "is_strict_compatibility_enabled": false,
+ "license": {
+ "id": 6,
+ "is_custom": false,
+ "name": {
+ "ca": "Llicència GPL (General Public License) de GNU, version 3.0",
+ "cs": "GNU General Public License, verze 3.0",
+ "de": "GNU General Public License, Version 3.0",
+ "el": "GNU General Public License, έκδοση 3.0",
+ "en-US": "GNU General Public License, version 3.0",
+ "es": "Licencia pública GNU, versión 3.0",
+ "eu": "GNU General Public License, 3.0 bertsioa",
+ "fa": "مجوز عمومی کلی گنو، نسخهٔ ۳٫۰",
+ "ga-IE": "GNU General Public License, leagan 3.0",
+ "id": "GNU General Public License, versi 3.0",
+ "it": "Licenza GNU General Public License, versione 3.0",
+ "ja": "GNU General Public License バージョン 3.0",
+ "nl": "GNU General Public License, versie 3.0",
+ "pl": "General Public Licence, wersja 3.0",
+ "pt-PT": "GNU General Public License, versão 3.0",
+ "ru": "GNU General Public License, версия 3.0",
+ "sk": "GNU General Public License, verzia 3.0",
+ "sq": "Leje e Përgjithshme Publike GNU, version 3.0",
+ "uk": "GNU General Public License, версія 3.0",
+ "vi": "Giấy phép Công cộng GNU, phiên bản 3.0",
+ "zh-CN": "GNU 通用公共授权,版本 3.0",
+ "zh-TW": "GNU General Public License,版本 3.0"
+ },
+ "url": "http://www.gnu.org/licenses/gpl-3.0.html"
+ },
+ "release_notes": {
+ "en-US": "See <a href=\"https://outgoing.prod.mozaws.net/v1/9b8a0d256c4a52897c7c3eeda6ff39d2cdca892b4d6703fd2504288de9e48eeb/https%3A//github.com/gorhill/uBlock/releases/tag/1.23.0\" rel=\"nofollow\">release notes</a>.\n\n<b>New:</b>\n\n<b>Static filter option <code>elemhide</code> as per ABP semantic</b>\n\nThe <code>elemhide</code> option is now fully supported, rather than being an alias of <code>generichide</code>. The <code>elemhide</code> option will be internally converted into two filters, <code>generichide</code> and <code>specifichide</code>. There have been cases raised by filter list maintainers where <code>specifichide</code> would be useful. Additionally, the filter options <code>elemhide</code>, <code>generichide</code> and <code>specifichide</code> can be aliased with <code>ehide</code>, <code>ghide</code> and <code>shide</code> respectively. (<code>generichide</code> appears over 1,300 times just in <em>\"uBlock filters\"</em>.)\n\n<b>Closed as fixed:</b>\n\n<ul><li><a href=\"https://outgoing.prod.mozaws.net/v1/809df8c146b215104a77966d14f937376b45852b3df1a028f992d346550abf45/https%3A//github.com/uBlockOrigin/uBlock-issues/issues/717%23issuecomment-527345870\" rel=\"nofollow\">Prevent uBO from being reloaded mid-session</a>\n <ul><li>A new advanced setting -- <a href=\"https://outgoing.prod.mozaws.net/v1/01831d59c2d061e18b9d833cee4ea008383504f7584c004d28cc7e3d08f6ef5e/https%3A//github.com/gorhill/uBlock/wiki/Advanced-settings%23extensionupdateforcereload\" rel=\"nofollow\"><code>extensionUpdateForceReload</code></a> -- can be used to override this new behavior.</li></ul></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/5692b6e58a0dd79c1609d9c231125248b4b8f0a7013e41d36694cbc504461ad3/https%3A//github.com/uBlockOrigin/uBlock-issues/issues/710\" rel=\"nofollow\">Comply with new security requirements for Chrome</a>\n <ul><li>The changes benefit all platforms.</li></ul></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/8fc51363be97982c75cc2875260f3fc368e826c5b32f801f64d38c554b90c1ff/https%3A//github.com/uBlockOrigin/uBlock-issues/issues/663%23issuecomment-509205050\" rel=\"nofollow\">Add advanced setting to control logger popup type</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/b7f7b19208e4fc642fc0f311dfc69e2775ca1f1bd53c1a99e1f82d7a3fd2bfdc/https%3A//github.com/gorhill/uBlock/issues/1493\" rel=\"nofollow\">Ignoring ping filters</a></li></ul>\n<b>Commits with no entry in issue tracker:</b>\n\n<ul><li><a href=\"https://outgoing.prod.mozaws.net/v1/1fbbed41350a01e3835600ed0fadecef0d788e5bfb0a95728dcfeaf6bf4b944f/https%3A//github.com/gorhill/uBlock/commit/f2340bef3cb614ade69fa3b043e3481ef9bc00fa\" rel=\"nofollow\">Fix bad returned value in case of empty URL</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/2e1bc40d61267942599c45c232dfc31da171e56ea901120786c83b1fcbd6164c/https%3A//github.com/gorhill/uBlock/commit/0f19dfde3887233674c5cc07413cb743a2a94319\" rel=\"nofollow\">Avoid or defer writing back to cache storage at launch</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/4e590dbd14d876c8733d8175e6aac94ba9f390cd88ed73c8763827a09ee10af9/https%3A//github.com/gorhill/uBlock/commit/35cb0eb3775ad1c8583b5486d1e8773c3b5a4615\" rel=\"nofollow\">Do not bypass network listener in suspended mode</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/9e83b18e5208b6175e9f4b97dd2edddc3f87060aeea6c89eb59bcecbe3594484/https%3A//github.com/gorhill/uBlock/commit/5a5523c0b53697f816d942a1b3a712980bc77b93\" rel=\"nofollow\">Remove stats button from logger</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/f558c39c68d37abacbc09cecee569813721fa19c3aa168b98419243ba0232692/https%3A//github.com/gorhill/uBlock/commit/bf697f344a55a7adbf5d4f9a201dba4669e339af\" rel=\"nofollow\">Log procedural cosmetic exception filters</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/54d7a099aae91e9cfc91b54be0ded516e5eed411963b8113cc047b2b01cd1a21/https%3A//github.com/gorhill/uBlock/commit/e1d75ee6023ba7cba510d4e3957b41a1e8b64213\" rel=\"nofollow\">Prevent reverse-lookup from finding badfilter-ed filters</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/f490e1772b7c36b66ac20081e22c85743f1053abd36bc81b9c69270a4ac51e29/https%3A//github.com/gorhill/uBlock/commit/e94024d350b066e4e04a772b0a3dbc69daab3fb7\" rel=\"nofollow\">Reduce memory usage in staticExtFilteringEngine.HostnameBasedDB</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/f166b22cc860417aedbfe08d54999e449ac05d8fa0526067e996597d811c4bba/https%3A//github.com/gorhill/uBlock/commit/4bf6503f0a654b298ae773066967653128ab4cb6\" rel=\"nofollow\">Store <code>csp=</code> filters into main data structure</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/5d61cb861c4b59b68cd46ad7b881d2ab97d168095f2fcee7ba804876df30acf0/https%3A//github.com/gorhill/uBlock/commit/59c9a34d34a737f6bb48c4130c65f4fe0fa73806\" rel=\"nofollow\">Add ability to quickly create exceptions in logger</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/627f5502422bd6dd2e7a6a23fc3919494fb86d3daa5f1be3dc2d44e3149e157a/https%3A//github.com/gorhill/uBlock/commit/f204d24bf4a7a5f70419bd8b0c7a87595f0a4181\" rel=\"nofollow\">Match static popup filter against local context</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/f3b6d93df97c0c2f8fb87b0289349f8b4add0124b37afb5e21166a7ad980eecf/https%3A//github.com/gorhill/uBlock/commit/1d2b24c79a44040633a180bd3ad1144c9831ca03\" rel=\"nofollow\">Fix erroneous reports of blocked popups in logger</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/41442383bcbe26bed1d3f39a7e59811900919a592790e10017d9be2234f31887/https%3A//github.com/gorhill/uBlock/commit/22b390eb003df4208097cd8eb98c01c81783e2fa\" rel=\"nofollow\">Fix case of unreported <code>:style</code> filters in logger</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/10cb6fadd5d20a9beb574baefbbbbf219d5fa534c501ec84217240be5638c386/https%3A//github.com/gorhill/uBlock/commit/9f825c30595a2689c48353860461a5dc9049a351\" rel=\"nofollow\">Do not flush blocked-elements cache at webNavigation time</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/d702438a63ed072427b4fd38d3ffe087dd81f6b92235b843f70a33eb20d1d36c/https%3A//github.com/gorhill/uBlock/commit/350e436c08613804f1f7ecdfae1651f9bc5f077f\" rel=\"nofollow\">Remove remnants of <code>chrome</code> references</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/90e3bfe648260dbaef96e54ef8492a2edd662600e0605704f8368564e63bf975/https%3A//github.com/gorhill/uBlock/commit/23c4c80136ba4974a6444488ef8162ba75b0cb84\" rel=\"nofollow\">Add support for <code>elemhide</code> (through <code>specifichide</code>)</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/48a77473358f4915b2912a3ff08936388e7cc04d45e4213015a9d9aac31aa3e5/https%3A//github.com/gorhill/uBlock/commit/87d0e456f1997cddb75168ee8dff6c8afcae1636\" rel=\"nofollow\">Simplify client messaging code</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/d2d121e411e5b13bef78d51bc67d0968255ec57ee5ee766c6a2d19c6d686a97f/https%3A//github.com/gorhill/uBlock/commit/149b5cf59cc760fa98c9753f4b4ec12d4b884d9a\" rel=\"nofollow\">Removing now obsolete Safari code base</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/065c6a5b564fa4746033153329482cddc9d350659311d23a2d223877c1a72fb0/https%3A//github.com/gorhill/uBlock/commit/917f3620e0c08b722bbd4d400bca2735d9f6975f\" rel=\"nofollow\">Revisit element picker arguments code</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/db8a7a83eac33bc2c045e0bab764e9e384d01b0213226f80867c7bb0ed5969bf/https%3A//github.com/gorhill/uBlock/commit/9367a6015b8cbb6b49347b00a105aab8f24df861\" rel=\"nofollow\">Convert new setTimeout-if scriptlet to blocklist approach</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/1d8cb95aaa74c8c70e3e1c4bb1b26db54c22a6df9669fc499d0f4d957c8050e5/https%3A//github.com/gorhill/uBlock/commit/58620fb05150cd8cc06a2cfad64011ca34f93468\" rel=\"nofollow\">Work toward modernizing code base: promisification</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/40837ad8e45d8ea3ceac275ec691eae54d1764ed02418e27e347d3ec9085cc60/https%3A//github.com/gorhill/uBlock/commit/e393a5244250d9384e1837b128319d69dc1919e4\" rel=\"nofollow\">Fix icon title always showing <code>(0)</code> when badge is disabled</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/8d3f56432a85938bde956203be79a070723f2555e49cb46b6d50a9dcbd53768d/https%3A//github.com/gorhill/uBlock/commit/1e7e6f86a6b57ad5e962e7196108e9d6a676bc04\" rel=\"nofollow\">Reuse existing Set/Map when calling scriptletFilteringEngine.retrieve</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/4e26b4ec5cf051d2997115a58a6360af94abee5e4376fc4475bd2123b3c2cea6/https%3A//github.com/gorhill/uBlock/commit/a73dd0a9f26df59cfb4b68e98d1184ac042c90ea\" rel=\"nofollow\">Fix entity-based lookup in html &amp; scriptlet filtering</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/181c7b80be2a8ffb41444d1d6063116d677b8eb39eb025a1ae499371e1adcfac/https%3A//github.com/gorhill/uBlock/commit/4792e0e291221c0f3aa158b759c9bbb16b788785\" rel=\"nofollow\">Coalesce tab reloads in burst \"relax blocking mode\" ops</a></li></ul>\n<a href=\"https://outgoing.prod.mozaws.net/v1/4c13192c5873c48eb61a007dc7a7cc495fdfb9a7ed57faa83e5bef457c478c4d/https%3A//github.com/gorhill/uBlock/compare/1.22.4...1.23.0\" rel=\"nofollow\">Commits history since 1.22.4</a>."
+ },
+ "reviewed": null,
+ "version": null
+ },
+ "default_locale": null,
+ "description": {
+ "ar": "مانع إعلانات كفوء: خفيف على الذاكرة و المعالج, على الرغم من قدرته على تحميل و تطبيق الألاف من الفلاتر أكثر من بعض أشهر مانعي الإعلانات.\n\nتوضيح عام لكفاءة الإضافة: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nالإستخدام: زر التشغيل الكبير في النافذة المنبثقة هو لتعطيل أو تشغيل uBlock للموقع الحالي. هو ينطبق على الموقع الحالي فقط، و ليس زر تشغيل عام.\n\n***\n\nمع مرونته، هو أكثر من مجرد \"مانع إعلانات\": بإمكانه أيضا قراءة و إنشاء فلاتر من ملفات الإستقبال.\n\nفلاتر حديثة، هذه القوائم من الفلاتر يتم تحميلها و تطبيقها:\n\n- EasyList\n- قائمة خادم الإعلانات لـPeter Lowe\n- EasyPrivacy\n- نطاقات البرامج الضارة\n\nيوفر لك قوائم أكثر لتختار منها إذا كنت ترغب:\n\nقائم التتبع المحسنة لـFanboy\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- و الكثير من القوائم الأخرى.\n\nطبعا، كلما مكَّنتَ فلاتر أكثر، كلما ازداد أثرها على الذاكرة. حتى مع الرغم من إضافة القوائم الإضافية لـFanboy، و قوائم تتبع إعلان hpHost، ما زال بإمكان uBlock₀ العمل بأدنى أثر على الذاكرة أفضل من بعض أشهر قوائم التتبع.\n\nأيضا، كن على علم أن تحديد بعض من هذه القوائم الإضافية قد يؤدي إلى إمكانية أعلى لتعطيل المواقع -- خصوصا تلك القوائم التي تستخدم عادة كملفات مضيفة.\n\n***\n\nبدون وجود قوائم الفلترات, هذه الإضافة عديمة القيمة. إذن إن كانت لديك الرغبة في المساهمة، فكر في أولئك الذين يعملون بجد لصيانة قوائم الفلترات التي تستخدمها، التي تمت إتاحتها لك لتسخدمها مجَّاناََ.\n\n***\n\nمجاناً.\nمفتوح المصدر مع رخصة (GPLv3)\nللمستخدمين من طرف مستخدمين أخرين.\n\nالمساهمون في Github:\n<a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nالمساهمون في Crowdin:\n <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nالإضافة في قيد الإنجاز، خذ هذا في عين الإعتبار عندما تستعرضها.\n\nسجل التغييرات للمشروع:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "bg": "Ефикасен блокер: с малко използване на паметта и процесора, но същевременно способен да зарежда и налага хиляди допълнителни филтри в сравнение с други популярни блокери.\n\nИлюстрация на неговата ефикасност: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nИзползване: Големият бутон за \"Включване\" в изскачащият прозорец служи за трайно включване/изключване на uBlock за текущия сайт. Той се отнася само за текущия сайт, не е глобален бутон за включване.\n\n***\n\nГъвкав, той е повече от \"блокер на реклами\": може да чете и създава филтри от хост файлове.\n\nПри първоначално използване са заредени и наложени следните списъци с филтри:\n\n- EasyList\n- Списък с рекламни сървъри от Peter Lowe\n- EasyPrivacy\n- Вредоносни домейни\n\nAко желаете, на разположение са допълнителни списъци, които да изберете:\n\n- разширен проследяващ списък от Fanboy\n- хост файл от Dan Pollock\n- рекламни и проследяващи сървъри от hpHosts\n- MVPS HOSTS\n- Spam404\n- и много други\n\nРазбира се, колкото повече списъци включите, толкова по-голямо е използването на паметта. Въпреки това, дори и след добавяне на двата допълнителни списъка от Fanboy, рекламните и проследяващи сървъри от hpHosts, uBlock₀ използва по-малко памет в сравнение с други много популярни блокери.\n\nСъщо така, имайте предвид, че избирането на някои от допълнителните списъци може да доведе до по-голяма вероятност от неправилно функциониране на уебсайтове -- особено тези списъци, които по принцип се използват като хост файлове.\n\n***\n\nБез предварително зададените списъци с филтри, това разширение е нищо. Така че, ако някога наистина искате да допринесете с нещо, помислете за хората, работещи усилено по поддържането на списъците с филтри, предоставени ви за безплатно използване от всички.\n\n***\n\nБезплатно.\nОтворен код с публичен лиценз (GPLv3)\nЗа потребители от потребителите.\n\nСътрудници @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nСътрудници @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nТова е доста ранна версия, имайте го предвид, когато я разглеждате.\n\nСписък с промени на проекта:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "bn-BD": "একটি দক্ষ প্রতিরোধক: মেমরি ও CPU-র পদচিহ্নের জন্য সহজ, এবং এখনো অন্যান্য জনপ্রিয় ব্লকার বা অবরোধকারীর থেকে হাজার হাজার অধিক ফিল্টারকে লোড এবং জোরদার করতে পারে।\n\nএটির কার্যকারিতার সচিত্র সংক্ষিপ্ত বিবরণ: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nব্যবহার: পপআপে বড় পাওয়ার বোতাম স্থায়ীভাবে বর্তমান ওয়েব সাইটের জন্য uBlock সক্রিয়/নিষ্ক্রিয় করবে। এটা শুধুমাত্র বর্তমান ওয়েব সাইটে প্রযোজ্য, এটি একটি বৈশ্বিক পাওয়ার বোতাম নয়।\n\n***\n\nমনীয়, এটি একটি \"ad blocker\"-এর ছেয়েও অধিক: এছাড়াও এটি হোস্ট ফাইল থেকে ফিল্টার পড়তে ও তৈরি করতে পারে।\n\nবাক্সের বাইরের, এই তালিকার ফিল্টারগুলি লোড করে এবং তা প্রয়োগ করে:\n\n- সহজ তালিকা\n- পিটার লো'য়ের বিজ্ঞাপন সার্ভারের তালিকা\n- সহজ গোপনীয়তা\n- ম্যালওয়্যার ডোমেইন\n\n আপনি যদি চান আপনি নির্বাচন করার জন্য আরো তালিকা পাবেন:\n\n- ফানবয়ের উন্নত ট্র্যাকিং তালিকা\n- Dan Pollock-এর হোস্ট ফাইল\n- hpHosts-এর বিজ্ঞাপন এবং ট্র্যাকিং সার্ভার\n- MVPS হোস্টসমূহ\n- স্প্যাম৪০৪\n- এবং আরও অনেক কিছু\n\nঅবশ্যই, যতবেশি ফিল্টার সক্রিয় করবেন, মেমরি পদচিহ্ন ততবেশি হবে। এমনকি Fanboy-এর দুটি অতিরিক্ত তালিকা, hpHosts-এর বিজ্ঞাপন এবং ট্র্যাকিং সার্ভার যোগ করার পরেও uব্লক অন্যান্য খুব জনপ্রিয় ব্লকারের থেকে কম মেমরি পদচিহ্ন ব্যবহার করে।\n\nএছাড়াও, এই অতিরিক্ত তালিকার কিছু নির্বাচন ওয়েব সাইট ভাঙ্গনের জন্য উচ্চ সম্ভাবনাময় হয়ে উঠতে পারে তাই সাবধান --- বিশেষকরে এই তালিকাগুলি যা সাধারণত হোস্ট ফাইল হিসেবে ব্যবহার করা হয়।\n\n***\n\nফিল্টারের পূর্বনির্ধারিত তালিকা ছাড়া, এই এক্সটেনশনটি কিছুই নয়। তাই কখনও যদি আপনি সত্যিই কিছু অবদান রাখতে চান, আপনার ব্যবহার করা ফিল্টার তালিকা রক্ষণাবেক্ষণের জন্য কঠোর পরিশ্রম করা সেই সব মানুষের করা কথা চিন্তা করুন যারা এই সব বিনামূল্যে ব্যবহারের জন্য উপলব্ধ করেছেন।\n\n***\n\nবিনামূল্যে।\nপাবলিক লাইসেন্সসহ মুক্ত উৎসের (GPLv3)\nব্যবহারকারীদের দ্বারা ব্যবহারকারীদের জন্য।\n\nঅবদানকারীগণ @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nঅবদানকারীগণ @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nএটি একটি প্রাথমিক সংস্করণ, আপনার পর্যালোচনার সময় তা মনে রাখুন।\n\nপ্রকল্পের পরিবর্তন লগ:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ca": null,
+ "cs": "Efektivní blokovač: nezanechává velké stopy, nezatěžuje paměť a CPU, a přesto může načítat a využívat o několik tisíc filtrů více, než jiné populární blockery.\n\nGrafický přehled jeho účinnosti: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPoužití: Velký vypínač ve vyskakovacím okně trvale povolí/zakáže uBlock pro otevřenou stránku. Funguje pouze pro aktivní webovou stránku, není to obecný vypínač.\n\n***\n\nFlexibilní, více než jen \"blokovač reklam\": umí také číst a vytvářet filtry z hosts souborů.\n\nPo instalaci jsou načteny a použity tyto filtry:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPokud chcete, můžete si vybrat tyto další filtry:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- A mnoho dalších\n\nČím více filtrů je povoleno, tím je samozřejmě větší stopa v paměti. I přesto má ale uBlock₀ i po přidání dvou dalších seznamů od Fanboye a \"hpHosts’s Ad and tracking servers\" menší vliv na paměť než mnohé další velmi populární blockery.\n\nDále mějte na paměti, že vybírání více filtrů zvyšuje šanci chybného zobrazení webů -- především u seznamů, které se normálně používají jako hosts soubory.\n\n***\n\nBez předvolených seznamů filtrů by toto rozšíření bylo k ničemu. Pokud tedy opravdu budete chtít něčím přispět, myslete na lidi, kteří spravují Vámi používané seznamy filtrů a uvolňují je pro všechny zdarma.\n\n***\n\nSvobodný software.\nOpen source s veřejnou licencí (GPLv3)\nOd uživatelů pro uživatele.\n\nPřispěvatelé na Githubu: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPřispěvatelé na Crowdinu: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nJde o poměrně ranou verzi, mějte to na paměti při recenzování.\n\nChange log projektu:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "da": "En effektiv blocker: let på hukommelse og CPU forbrug,. Kan indlæse og anvende tusindvis af flere filtre end andre populære blockere derude.\n\nIllustreret oversigt over effektiviteten: <a href=\"https://outgoing.prod.mozaws.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-compared\n\nAnvendelse: Den Store power knap i pop-up-vinduet kan permanent deaktivere/aktivere uBlock på det aktuelle websted. Dette gælder kun for det aktuelle websted, det er ikke en global afbryderknap.\n\n***\n\nFleksibel, det er mere end en \"ad blocker\": den kan også læse og oprette filtre fra hosts-filer.\n\nFra starten af er disse lister over filtre indlæst og anvendt:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nFlere lister er tilgængelige hvis du ønsker det:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Osv.\n\nSelvfølgelig vil flere aktive filtre betyde højere hukommelsesforbrug. Selv efter tilføjelse af Fanboys to ekstra lister, og hpHosts’s Ad and tracking server, har uBlock₀ stadig et lavere hukommelsesforbrug end andre blockere derude.\n\nVær desuden opmærksom på, at hvis du vælger nogle af disse ekstra lister kan det føre til højere sandsynlighed for, at webstedet bliver vist forkert - især de lister der normalt anvendes som hosts-fil.\n\n***\n\nUden de forudindstillede lister med filtre er denne udvidelse intet. Hvis du nogensinde virkelig ønsker at bidrage med noget, så tænk på de mennesker der arbejder hårdt for at vedligeholde de filterlister du bruger, som alle blev stillet gratis til rådighed for alle.\n\n***\n\nGratis.\nOpen source med offentlig licens (GPLv3)\nFor brugere, af brugere.\n\nBidragydere @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nBidragydere @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDette er en tidlig version. Hav dette i tankerne når du skriver en anmeldelse.\n\nProjekt changelog:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "de": "Ein effizienter Blocker: Geringer Speicherbedarf und niedrige CPU-Belastung - und dennoch werden Tausende an Filtern mehr angewendet als bei anderen populären Blockern.\n\nEin illustrierter Überblick über seine Effizienz: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nBenutzung: Der An-/Ausschaltknopf beim Klicken des Erweiterungssymbols dient zum An-/Ausschalten von uBlock auf der aktuellen Webseite. Dies wirkt sich also nur auf die aktuelle Webseite aus und nicht global.\n\n***\n\nuBlock ist flexibel, denn es ist mehr als ein \"Werbeblocker\": Es verarbeitet auch Filter aus mehreren hosts-Dateien.\n\nStandardmäßig werden folgende Filterlisten geladen und angewandt:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nAuf Wunsch können zusätzliche Listen ausgewählt werden:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- etc.\n\nNatürlich ist der Speicherbedarf umso höher, desto mehr Filter angewandt werden. Aber selbst mit den zwei zusätzlichen Listen von Fanboy und hpHosts’s Ad and tracking servers ist der Speicherbedarf von uBlock₀ geringer als bei anderen sehr populären Blockern.\n\nBedenke allerdings, dass durch die Wahl zusätzlicher Listen die Wahrscheinlichkeit größer wird, dass bestimmte Webseiten nicht richtig geladen werden - vor allem bei Listen, die normalerweise als hosts-Dateien verwendet werden.\n\n***\n\nOhne die vorgegebenen Filterlisten ist diese Erweiterung nichts. Wenn du also etwas beitragen möchtest, dann denke an die Menschen, die hart dafür arbeiten, die von dir benutzten Filterlisten zu pflegen, und diese für uns alle frei verfügbar gemacht haben.\n\n***\n\nKostenlos.\nOpen source mit Public License (GPLv3)\nFür Benutzer von Benutzern.\n\nMitwirkende @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nMitwirkende @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDies ist eine ziemlich frühe Version - bitte denke daran, wenn du sie bewertest.\n\nChange log des Projekts:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "el": "Ένας αποτελεσματικός αναστολέας διαφημίσεων: παρόλο το ελαφρύ του αποτύπωμα στη μνήμη και τον επεξεργαστή μπορεί να εφαρμόσει χιλιάδες περισσότερα φίλτρα σε σχέση με άλλους δημοφιλείς blockers.\n\nΑπεικονιζόμενη επισκόπηση της αποτελεσματικότητάς του: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nΧρήση: Το μεγάλο πλήκτρο απενεργοποίησης/ενεργοποίησης στο αναδυόμενο παράθυρο, χρησιμεύει στην εναλλαγή κατάστασης του uBlock για τον τρέχοντα ιστότοπο. Η εφαρμογή της ρύθμισης αυτής γίνεται μόνο για τον τρέχοντα ιστότοπο και δεν επιβάλλεται καθολικά.\n\n***\n\nΕυέλικτος, είναι πολλά περισσότερα από ένας απλός \"ad blocker\": μπορεί επιπλέον να διαβάζει και να δημιουργεί φίλτρα από αρχεία hosts.\n\nΚατά προεπιλογή, οι λίστες φίλτρων που φορτώνονται και επιβάλλονται είναι οι εξής:\n\n- EasyList\n- Λίστα διακομιστών διαφημίσεων του Peter Lowe\n- EasyPrivacy\n- Κακόβουλοι τομείς\n\nΕπιπλέον λίστες είναι διαθέσιμες για να επιλέξετε εάν το επιθυμείτε:\n\n- Ενισχυμένη Ιχνωσική Λίστα του Fanboy\n- Αρχείο hosts του Dan Pollock\n- Διαφημίσεις και διακομιστές ίχνωσης hpHosts\n- MVPS HOSTS\n- Spam404\n- και πολλές άλλες\n\nΦυσικά, όσο περισσότερα φίλτρα ενεργοποιούνται, τόσο αυξάνεται το αποτύπωμα της μνήμης. Ωστόσο, ακόμη και μετά από την προσθήκη δυο επιπλέον λιστών, του Fanboy και της λίστας διαφημίσεων και διακομιστών ίχνωσης hpHosts, το uBlock₀ συνεχίζει να έχει χαμηλότερο αποτύπωμα μνήμης από άλλους δημοφιλείς αναστολείς.\n\nΕπίσης, έχετε υπ'όψην ότι διαλέγοντας μερικές από τις έξτρα λίστες μπορεί να οδηγήσει σε πιθανό σφάλμα στην ιστοσελίδα -- ειδικά εκείνες που κανονικά χρησιμοποιούνται σαν host αρχεία.\n\n***\n\nΧωρίς τις υπάρχουσες λίστες φίλτρων, αυτή η επέκταση δεν έχει καμία αξία. Εάν ποτέ λοιπόν θελήσετε πραγματικά να συνεισφέρετε κάτι, αναλογιστείτε τους ανθρώπους που εργάζονται σκληρά για να διατηρήσουν τις λίστες φίλτρων που χρησιμοποιείτε, οι οποίες διατέθηκαν προς χρήση σε όλους, δωρεάν.\n\n***\n\nΔωρεάν.\nΑνοιχτού κώδικα με άδεια δημόσιας χρήσης (GPLv3)\nΑπό τους χρήστες για τους χρήστες.\n\nΣυνεισφέροντες @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nΣυνεισφέροντες @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nΕίναι μια αρκετά πρόωρη έκδοση, κρατήστε το υπόψη κατά την αξιολόγηση.\n\nΑρχείο αλλαγών του έργου:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "en-US": "An efficient blocker: easy on memory and CPU footprint, and yet can load and enforce thousands more filters than other popular blockers out there.\n\nIllustrated overview of its efficiency: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUsage: The big power button in the popup is to permanently disable/enable uBlock for the current web site. It applies to the current web site only, it is not a global power button.\n\n***\n\nFlexible, it's more than an \"ad blocker\": it can also read and create filters from hosts files.\n\nOut of the box, these lists of filters are loaded and enforced:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMore lists are available for you to select if you wish:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- And many others\n\nOf course, the more filters enabled, the higher the memory footprint. Yet, even after adding Fanboy's two extra lists, hpHosts’s Ad and tracking servers, uBlock Origin still has a lower memory footprint than other very popular blockers out there.\n\nAlso, be aware that selecting some of these extra lists may lead to higher likelihood of web site breakage -- especially those lists which are normally used as hosts file.\n\n***\n\nFree.\nOpen source with public license (GPLv3)\nFor users by users.\n\nIf ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\nDocumentation:\n<a href=\"https://outgoing.prod.mozaws.net/v1/788d66e7299bdfb1da05832994551640d0ad441e148a3e29afe8dd0a5a90800c/https%3A//github.com/gorhill/uBlock%23ublock-origin\" rel=\"nofollow\">https://github.com/gorhill/uBlock#ublock-origin</a>\n\nProject change log:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>\n\nContributors @ Github:\n<a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n\nContributors @ Crowdin:\n<a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>",
+ "es": "Un bloqueador eficiente: capaz de cargar y aplicar miles más de filtros en comparación con otros populares bloqueadores, manteniendo un mínimo consumo de memoria y CPU.\n\nEjemplo con imágenes ilustrando su eficiencia (en inglés): <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUso: El botón grande de apagado/encendido en la ventana emergente de la extensión, es para deshabilitar/habilitar uBlock₀ permanentemente en el sitio web actual. Aplica solo al sitio web actual, no activa o desactiva la extensión de forma general.\n\n***\n\nFlexible, es más que un \"bloqueador de anuncios\": también puede leer y crear filtros desde archivos hosts.\n\nPor defecto ya trae configuradas las siguientes listas de filtros:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nOtras listas disponibles pueden ser seleccionadas, si se desea:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Y muchas más\n\nPor supuesto, mientras más filtros se activen, mayor será el consumo de memoria. No obstante, incluso después de agregar las dos listas adicionales de \"Fanboy's\" y la \"hpHosts’s Ad and tracking servers\", uBlock₀ consume menos memoria que otros bloqueadores similares.\n\nTambién tenga en cuenta que seleccionar algunas de estas listas adicionales puede conducir a una mayor probabilidad de aparición de problemas al mostrar un sitio web -- especialmente las listas utilizadas normalmente como archivo hosts.\n\n***\n\nSin las listas preestablecidas de filtros, esta extensión no sería nada. Así que si alguna vez realmente quieres aportar algo, piensa en las personas que trabajan duro para mantener estas listas de filtros, disponibles de forma gratuita para todos.\n\n***\n\nLibre.\nCódigo abierto con licencia pública (GPLv3)\nHecho para usuarios por los usuarios.\n\nColaboradores @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nColaboradores @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/7bae4395c4e5926bb237c1ef9b0f391cb005dbdbf58f4c9e47298db9bb6d1f57/https%3A//crowdin.com/project/ublock\" rel=\"nofollow\">https://crowdin.com/project/ublock</a>\n\n***\n\nRegistro de cambios del proyecto:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "eu": "Blokeatzaile eraginkor bat: Memoria eta PUZ erabileran arina da, eta hala ere beste blokeatzaile ezagun batzuk baino milaka iragazki gehiago kargatu eta ezarri ditzake.\n\nBere eraginkortasunaren adibide grafikoa: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nErabilera: Laster-leihoko pizte botoi handia uBlock uneko gunean behin betiko gaitu edo desgaitzeko da. Uneko guneari dagokio soilik, ez da botoi orokor bat.\n\n***\n\nMalgua, iragarki blokeatzaile bat baino gehiago da, ostalarietako iragazkiak sortu eta irakurri ditzake ere.\n\nLehenetsita, honako iragazki zerrendak kargatu eta ezartzen ditu:\n\n- EasyList\n- Peter Loweren iragarki zerbitzarien zerrenda\n- EasyPrivacy\n- Malware domeinuak\n\nZerrenda gehiago dituzu eskura hautatzeko hala nahiez gero:\n\n- Fanboyren hobetutako jarraipen zerrenda\n- Dan Pollocken ostalari zerrenda\n- hpHostsen iragarki eta jarraipen zerbitzariak\n- MVPS Ostalariak\n- Spam404\n- Eta beste hainbat gehiago\n\nJakina, iragazki gehiago kargatuta memoria erabilera handiagoa da. Hala ere, Fanboyren bi zerrenda gehigarriak eta hpHostsen iragarki eta jarraipen zerbitzariak kargatuta, uBlockek beste blokeatzaile ezagun batzuk baino memoria gutxiago erabiltzen du.\n\nBestalde, kontuan izan zerrenda gehigarri hauetako batzuk gaitzeak guneren bat hausteko aukerak handitzen dituela, batez ere ostalari fitxategi gisa erabili ohi diren zerrendak.\n\n***\n\nLehenetsitako iragazki zerrendarik gabe gehigarri honek ez du ezer egiten. Beraz ezertan lagundu nahi baduzu pentsa ezazu erabiltzen dituzun iragazki zerrendak egunean mantentzeko tinko lanean dabiltzan horietan, guztiek erabiltzeko moduan doan eskuragarri jarri dituztenak.\n\n***\n\nDoan.\nLizentzia libreduna (GPLv3)\nErabiltzaileek erabiltzaileentzat sortua.\n\nParte-hartzaileak @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nParte-hartzaileak @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nNahiko berria da bertsio hau, kontua izan honi buruz idaztean.\n\nProiektuaren aldaketa egunkaria:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fa": "یک بلاکر موثر: نیاز به پردازش حافظه و سی پی یو کمتر و در عین حال اجرای هزاران فیلتر بیشتر از سایر رقبای بلاکر موجود.\n\nبررسی تصویری از کارایی این محصول: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nکاربرد: دکمه ی پاور بزرگ در پنجره برای فعال یا غیر فعال کردن uBlock برای صفحه ی جاری است. فقط برای همین سایت اعمال میشود، دکمه ی پاوری برای تمام سایت ها نیست.\n\n***\n\nانعطاف پذیری آن بیشتر از \"ad blocker\" است: همچنین می تواند فیلتر ها را از هاست میزبان، بخواند و بسازد.\n\nبیرون از جعبه، این لیست فیلترها بارگذاری و اجرا میشوند:\n\n- EasyList\n- لیست سرور تبلیغاتی Peter Lowe\n- EasyPrivacy\n-دامنه های تروجان\n\nاگر میخواهید لیست های بیشتر برای انتخاب شما در دسترس هستند:\n\n- ردیابی لیست پیشرفته ی Fanboy\n- میزبانی فایل Dan Pollock\n- تبلیغ و ردیابی سرور hpHosts\n- هاست های MVPS\n- اسپم 404\n- و بسیاری دیگر\n\nالبته هرچه فیلترهای بیشتری فعال باشند، حافظه ی بیشتری اشغال خواهد شد. با اینحال، حتی پس از اضافه کردن دو لیست اضافی Fanboy و سرور های ردیابی و تبلیغ hpHosts ، میبینیم که uBlock هنوز حافظه پایین تری از دیگر برنامه های مشابه اشغال میکند.\n\nهمچنین، بدانید که انتخاب برخی از این لیست ها ممکن است افزایش احتمال شکستگی وب سایت--به ویژه آنهایی که به طور معمول به عنوان میزبان فایل شناخته میشوند را در پی داشته باشد.\n\n***\n\nبدون فهرست از پیش تعیین شده ی فیلتر، این افزونه هیچ است. پس اگر واقعا می خواهید کمکی کرده باشید، به افرادی فکر کنید که برای حفظ لیست فیلتر مورد استفاده شما سخت کار میکنند که برای استفاده همه به رایگان در دسترس باشد.\n\n***\n\nرایگان.\nمتن باز با مجوز عمومی (GPLv3)\nبرای کاربران توسط کاربران.\n\nمشارکت کنندگان در گیت هاب: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nمشارکت کنندگان در کرادین <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nاین کاملا یک نسخه اولیه است، هنگام بررسی اینرا بخاطر داشته باشید.\n\nتغییرات اخیر پروژه:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fi": "Tehokas mainosten estäjä – käyttää vähän resursseja, mutta silti voit ladata ja pakottaa tuhansia suodatinsääntöjä enemmän kuin muut suositut mainoksia estävät lisäosat.\n\nKuvitettu yleiskatsaus uBlockin tehokkuudesta (englanniksi): <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nKäyttö: Iso virtanappi ponnahdusikkunassa on tarkoitettu pysyvästi estämään/sallimaan uBlock kyseisellä sivulla. Tämä koskee vain nykyistä sivua, ei kaikkia sivuja.\n\n***\n\nJoustava, tämä lisäosa on enemmän kuin perinteinen \"mainosten estäjä\". Voit lukea ja luoda suodattimia myös hosts-tiedostoista.\n\nNämä suodatinlistat ovat automaattisesti ladattuna ja kytketty päälle:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nHalutessasi voit valita käyttöösi lisää listoja:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Ja monia muita\n\nJos sinulla on useita listoja käytössä, keskusmuistia kuluu enemmän. Tästä huolimatta, vaikka lisäisit Fanboyn kaksi lisälistaa ja hpHostsin listan, uBlockilla on silti pienempi muistinkulutus kuin muilla suosituilla mainosten estäjillä.\n\nUseiden listojen lisääminen saattaa aiheuttaa sivujen kaatumisen tai hajoamisen. Etenkin listat, joita käytetään normaalisti hosts-tiedostona, voivat aiheuttaa ongelmia.\n\n***\n\nTämä lisäosa ei tee mitään ilman suodatinlistoja. Jos siis haluat osallistua jotenkin, muistathan kaikki ne ihmiset jotka työskentelevät pitääkseen käyttämäsi suodatinlistat ajan tasalla ja saatavilla ilmaiseksi.\n\n***\n\nIlmainen.\nAvoimen lähdekoodin julkinen lisenssi (GPLv3)\nKäyttäjiltä käyttäjille.\n\nKehittäjät @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKehittäjät @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nOtathan huomioon testatessasi, että käytössäsi on varsin varhainen versio.\n\nProjektin muutosloki:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "fr": "uBlock est une extension qui bloque les publicités et les pisteurs, légère en empreinte mémoire et en utilisation du processeur et qui pourtant, est capable d'utiliser et de traiter des milliers de filtres de plus que la plupart des autres bloqueurs.\n\nConsultez cette page en Anglais pour avoir une vue d'ensemble illustrée de son efficacité : <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilisation : Le gros bouton power dans la fenêtre pop-up permet de désactiver/activer en permanence uBlock pour le site Web en cours de consultation. Cela s'applique uniquement au site Web actuel, ce n'est pas un bouton power qui affecte entièrement le fonctionnement de l'extension.\n\n***\n\nFlexible, uBlock ne prend pas en charge que les filtres de type Adblock, elle peut également lire et créer des filtres depuis des fichiers hosts.\n\nPar défaut, ces listes de filtrage sont chargées et traitées :\n\n- EasyList (Liste anti-publicités maintenue fréquemment à jour par la communauté)\n- Peter Lowe’s Ad server list (Liste de serveurs publicitaires maintenue à jour par Peter Lowe)\n- EasyPrivacy (Liste anti-pisteurs maintenue fréquemment à jour par la communauté)\n- Malware domains (Liste de protection contre des domaines malveillants)\n\nDavantage de listes sont disponibles si vous souhaitez renforcer le blocage :\n\n- Fanboy’s Enhanced Tracking List (Liste de protection avancée contre le pistage maintenue à jour par Fanboy)\n- Dan Pollock’s hosts file (Fichier Hosts bloquant publicités, domaines malveillants et autres pisteurs, maintenue fréquemment à jour par Dan Pollock)\n- hpHosts’s Ad and tracking servers (Fichier Hosts bloquant des serveurs publicitaires et des serveurs pistant, maintenue à jour par hpHosts)\n- MVPS HOSTS (Fichier Hosts bloquant publicités, domaines malveillants et autres pisteurs, maintenue à jour par MVPS)\n- Spam404 (Liste de protection contre les spams, maintenue fréquemment à jour par la communauté)\n- Et plein d'autres\n\nBien évidemment, plus vous activez de filtres, plus l'empreinte mémoire augmentera. Pourtant, même après avoir ajouté deux listes supplémentaires crées par Fanboy et le fichier Hosts d'hpHosts, uBlock₀ utilise moins de mémoire vive que tous les autres bloqueurs de pubs populaires.\n\nVeuillez tout de même prendre en compte que le fait de choisir parmi ces listes supplémentaires peut conduire à quelques incompatibilités sur les sites Web que vous visitez, bien que ces listes soient maintenues à jour par leurs auteurs.\n\n***\n\nSans les listes prédéfinies de filtres, cette extension (comme d'autres) ne serait rien. Alors si vous tenez vraiment à contribuer d'une quelconque manière, pensez aux personnes travaillant dur pour maintenir à jour ces listes de filtres que vous utilisez, qui plus est proposées gratuitement à tout le monde.\n\n***\n\nGRATUIT.\nSource libre avec une licence publique GPLv3\nFait par des utilisateurs pour des utilisateurs.\n\nContributeurs @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContributeurs @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nIl s'agit d'une version relativement préliminaire, veuillez garder ça à l'esprit lors de votre évaluation de l'extension.\n\nConsultez ici en Anglais le Journal des changements concernant le projet :\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "he": "חוסם יעיל: חותמת נמוכה של המעבד והזיכרון, ועדיין יכול לטעון ולאפשר אלפי מסננים יותר מאשר חוסמים פופולריים אחרים.\n\nסקירה כוללת על היעילות שלו: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nשימוש: לחצן ההפעלה הגדול בחלון הפופאפ הוא בשביל לבטל/להפעיל את uBlock עבור האתר הנוכחי. הוא חל על האתר הנוכחי בלבד, זהו לא לחצן הפעלה גלובלי.\n\n***\n\nגמיש, יותר מ \"חוסם פרסומות\": הוא יכול גם לקרוא וליצור מסננים מקבצי hosts.\n\nהיישר מהקופסה, רשימות המסננים הללו נטענות ומאופשרות:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nרשימות נוספות אלו זמינות לבחירתך אם תרצה:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- ועוד רבים אחרים\n\nכמובן שככל שכמות מסננים גדולה יותר מופעלת, ככה גם חתימת הזיכרון גדולה יותר. ובכל זאת, אפילו לאחר הוספת שתי הרשימות הנוספות של Fanboy ו hpHosts’s Ad and tracking servers, ל uBlock עדיין יש חתימת זיכרון נמוכה יותר מלחוסמים פופולריים אחרים שם בחוץ.\n\nכמו כן, תהיה מודע שבחירה של חלק מהרשימות הנוספות הללו עלולה להוביל בסבירות גבוהה לשבירה של אתרי אינטרנט -- במיוחד הרשימות אשר בדרך כלל משומשות כקובץ hosts.\n\n***\n\nללא רשימות מסננים מוגדרים מראש, תוסף זה לא שווה כלום. אז אם אי פעם תרצה באמת לתרום משהו, תחשוב על האנשים שעובדים לילות כימים כדי לתחזק את רשימות המסננים שאתה משתמש בהן, אשר הובאו לשימוש על ידי כולם ללא כל תשלום.\n\n***\n\nחינם.\nקוד פתוח עם רשיון ציבורי (GPLv3)\nבשביל המשתמשים על ידי המשתמשים.\n\nתורמים @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nתורמים @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nקח בחשבון שזוהי גרסה מוקדמת בזמן הסקירה שלך.\n\nרשימת השינויים של הפרויקט:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "hu": "Egy hatékony blokkoló: kíméletes a processzorral és a memóriával, mégis képes nagyságrendekkel több szűrő betöltésére és alkalmazására a többi népszerű blokkolóhoz viszonyítva.\n\nÁttekintés a hatékonyságáról: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nHasználat: A nagy bekapcsológomb a kiegészítő tiltására/engedélyezésére szolgál a jelenlegi webhelyen. A gomb kizárólag a jelenlegi webhelyre érvényes, nem egy globális kapcsoló.\n\n***\n\nTöbb mint egy egyszerű reklámblokkoló: képes hosts fájlok bejegyzéseit is értelmezni, és azokból szűrőket létrehozni.\n\nAlapértelmezetten a következő szűrőlisták kerülnek betöltésre és alkalmazásra:\n\n- EasyList\n- Peter Lowe hirdetési szerverlistája\n- EasyPrivacy\n- Kártékony domainek\n\nEgyéb listák is kiválaszthatók a felhasználó igénye szerint:\n\n- Fanboy bővített követők listája\n- Dan Pollock hosts fájlja\n- hpHosts hirdetés és követőszerverek listája\n- MVPS HOSTS\n- Spam404\n- És sok más\n\nTermészetesen, több szűrő használatával a memóriaigény is növekszik. Ennek ellenére Fanboy két extra listája és a hpHosts (reklám és követőszerverek) lista hozzáadásával a uBlock memóriafogyasztása még mindig alacsonyabb, mint a legnépszerűbb blokkolóké.\n\nEmellett, néhány extra lista kiválasztásával megnövekszik az esély arra, hogy a webhelyek használhatatlanná válnak -- főleg azon listákról van szó, melyek normál esetben hosts fájlként használatosak.\n\n***\n\nA szűrőlisták nélkül a kiegészítő nem sokat érne. Tehát, ha valaha is eszedbe jutna támogatást kínálni, akkor előbb gondolj azokra, akik keményen dolgoznak a listák karbantartásával, illetve ingyenesen hozzáférhetővé teszik azokat mindenki számára.\n\n***\n\nIngyenes.\nNyílt forráskódú nyilvános licenccel (GPLv3)\nFelhasználóknak felhasználóktól.\n\nKözreműködők a Github-on: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKözreműködők a Crowdin-en: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEz még egy elég korai verzió, amit illik szem előtt tartani értékeléskor.\n\nVáltozások listája:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "id": "Pemblokir yang efisien: ringan penggunaan memori dan CPU, namun dapat memuat dan menjalankan ribuan filter lain dibanding pemblokir populer lain di luar sana.\n\nRingkasan ilustrasi efisiensi: <a href=\"https://outgoing.prod.mozaws.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-Compared\n\nPenggunaan: Tombol daya yang besar dalam popup berfungsi menonaktifkan/mengaktifkan uBlock secara permanen untuk situs yang sedang dibuka. Berlaku untuk situs yang sedang dibuka saja, bukan tombol daya global.\n\n***\n\nFleksibel, lebih dari sekedar \"pemblokir iklan\": juga dapat membaca dan membuat filter dari berkas host.\n\nLangsung bekerja, daftar filter berikut ini dimuat dan dijalankan:\n\n- EasyList\n- Daftar server iklan Peter Lowe\n- EasyPrivacy\n- Domain malware\n\nJika anda ingin, masih banyak tersedia daftar lain untuk anda pilih:\n\n- Daftar Pelacakan Fanboy yang DItingkatkan\n- Berkas host Dan Pollock\n- Server iklan dan pelacakan hpHosts\n- HOST MVPS\n- Spam404\n- dan banyak lainnya\n\nTentu saja, semakin banyak filter yang diaktifkan, semakin besar penggunaan memori. Namun, bahkan setelah menambahkan 2 daftar ekstra Fanboy, server iklan dan pelacakan hpHosts, penggunaan memori uBlock masih lebih kecil dibanding pemblokir iklan populer lain di luar sana.\n\nPerlu diketahui juga bahwa memilih beberapa daftar ekstra juga berpeluang lebih tinggi menyebabkan kerusakan situs -- terutama daftar yang biasanya digunakan sebagai berkas host.\n\n***\n\nTanpa daftar filter yang ada, ekstensi ini bukanlah apa-apa. Jadi, jika Anda benar-benar ingin berkontribusi sesuatu, berpikirlah tentang orang-orang yang bekerja keras mengelola daftar filter yang anda gunakan, yang dibuat dan tersedia untuk digunakan oleh semua dengan gratis.\n\n***\n\nGratis.\nSumber terbuka dengan lisensi publik (GPLv3)\nUntuk pengguna oleh pengguna.\n\nKontributor @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKontributor @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nMasih dalam versi yang sangat awal, mohon diingat ketika anda membuat ulasan.\n\nCatatan perubahan proyek:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "it": "uBlock è un efficiente ad-blocker: occupa poca memoria e poca CPU, ma può usare migliaia di filtri in più rispetto ad altri software simili.\n\nConsulta questa pagina (in inglese) per verificare la sua efficacia <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUso: il pulsante power nel popup serve per disabilitare/abilitare permanentemente uBlock nel sito che stai visitando. e non serve per disabilitare/abilitare l'estensione.\n\n***\n\nMolto più che un ad-blocker: può anche creare filtri dal file host.\n\nPer default sono attivate queste liste:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPuoi anche attivare moltre altre liste:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Ecc.\n\nOvviamente, più liste attivi, maggiore sarà l'impatto sulla memoria. Anche aggiungendo altre due liste di Fanboy, ad di hpHosts e tracking server, uBlock userà meno memoria di molti altri ad-blocker.\n\nSelezionando alcuni di questi filtri può portare ad una maggiore probabilità di problemi nel visualizzare alcuni siti web.\n\n***\n\nSenza queste liste di filtri, questa estensione non è niente. osì se vuoi contribuire, pensa alle persone che lavorano duramente per mantenere queste liste che stai usando, che sono disponibili gratuitamente.\n\n***\n\nGratuito.\nOpen source with public license (GPLv3)\nFatto dagli utenti per gli utenti.\n\nCollaboratori @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nCollaboratori @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nQuesta è una versione preliminare, ricordalo quando scriverai una recensione.\n\nPer leggere le novità di ogni versione consulta questa pagina (In Inlgese):\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ja": "効率的なブロッカー:コンピュータのメモリとCPUのフットプリントはより少なく\n、別の人気のブロッカーよりも何千ものフィルタをロードし、強制的にブロックができます\n\n他ソフトとの比較は以下のとおり: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n使用法: ポップアップに表示される大きな電源ボタンは、現在のサイトでuBlockの有効/無効を切り替えます。 現在のサイトのみに適用されます、グローバルボタンではありません。\n\n***\n\nただの「広告ブロッカー」より柔軟です:ホストファイルを読み込みフィルターを作成できます。\n\n要するに、以下のフィルターが読み込まれ、適用されます:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nご希望であればさらに多くのリストがご利用できます:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Etc.\n\nもちろん、多くのフィルターを適用すれば使用メモリーは増えます。 ただ、それでも、Fanboy's Special Blocking List、Fanboy's Enhanced Tracking List、hpHost's Ad and tracking serversの三つのリストを追加で適用しても、uBlockは他の人気のブロッカーより少ないメモリー消費を実現しています。\n\nそれと、多くのリストの適用は(特にホストファイルとしてよく使われているリスト)ウェブサイトの崩壊を起こしかねないことに注意してください。\n\n***\n\nこの拡張機能は、あらかじめ設定されているフィルターのリストが無ければ意味を成しません。 ですので、何かしらの形で貢献したいと考えることがあった時は、これらのリストを無料で懸命に更新し続けている方々を思い出してください。\n\n***\n\n無料.\nパブリックライセンス(GPLv3)のオープンソース\nユーザーによって作られた、ユーザーのための物。\n\n貢献者 @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n貢献者 @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nこれは割と初期のバージョンですので、それを念頭にレビューをお願いします。\n\nプロジェクト変更ログ:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ka": "რეკლამების შედეგიანი შემზღუდავი: მეხსიერებისა და პროცესორის შემსუბუქებული მოხმარება, რეკლამების სხვა შემზღუდავებთან შედარებით, ათასობით მეტი ფილტრის გამოყენების პირობებშიც კი.\n\nშედეგიანობის მიმოხილვა იხილეთ ბმულზე: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nგამოყენება: ჩართვის დიდი ღილაკით, ჩამოშლილ მენიუში, შესაძლებელია uBlock-ის ჩართვა/გამორთვა მიმდინარე ვებსაიტზე. ეს ღილაკი მოქმედებს მხოლოდ არსებულ საიტზე და არ გამოიყენება ზოგადად ჩართვა/გამორთვისთვის.\n\n***\n\nმეტად მოქნილი, ეს არაა უბრალოდ „რეკლამების შემზღუდავი“: ასევე შესაძლებელია hosts ფაილის წაკითხვა და ფილტრების შექმნა.\n\nგარდა ამისა, ნაგულისხმევად ჩართულია და გამოიყენება შემდეგი გასაფილტრი სიები:\n\n- EasyList\n- Peter Lowe-ის სარეკლამო სერვერების სია\n- EasyPrivacy\n- მავნე დომენები\n\nასევე, ხელმისაწვდომია სიები სურვილისამებრ შესარჩევად:\n\n- Fanboy-ის გაუმჯობესებული წესები თვალყურისმდევნებლების შესაზღუდად\n- Dan Pollock-ის hosts ფაილი\n- hpHosts-ის სარეკლამო და თვალყურისმდევნელი სერვერები\n- MVPS HOSTS\n- Spam404\n- და კიდევ ბევრი\n\nრასაკვირველია, რაც უფრო მეტი ფილტრია ჩართული, მეხსიერების გამოყენება იზრდება. თუმცა, Fanboy-ის გაფართოებული წესების, hpHosts-ის სარეკლამო და თვალყურისმდევნელი სერვერების დამატების შემთხვევაშიც კი, uBlock მაინც ნაკლებ მეხსიერებას იყენებს, ვიდრე ყველა სხვა ცნობილი შემზღუდავი პროგრამები.\n\nამასთან, გაითვალისწინეთ, რომ ზოგიერთი დამატებითი წესების შერჩევის შედეგად, შესაძლოა ვებსაიტები არ გამოჩნდეს გამართულად -- განსაკუთრებით იმ წესების შემთხვევაში, რომელიც ჩვეულებრივ, hosts ფაილად გამოიყენება.\n\n***\n\nწინასწარ შედგენილ წესებს, მნიშვნელოვანი ადგილი უჭირავს ამ გაფართოების შედეგიან მუშაობაში. ასე რომ, თუ ოდესმე გადაწყვეტთ ვინმესთვის შემოწირულობის გაღებას, იფიქრეთ იმ ადამიანებზე, რომლებიც თავდაუზოგავად შრომობენ იმ გასაფილტრი წესების მუდმივ განახლებაზე, რომლითაც სარგებლობთ და რომელიც ხელმისაწვდომია ყველასთვის უფასოდ.\n\n***\n\nუფასო.\nღია წყაროს მქონე საჯარო ლიცენზიით (GPLv3)\nმომხმარებლების მიერ, მომხმარებლებისთვის.\n\nწვლილის შემტანები @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nწვლილის შემტანები @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nცვლილებების ჩამონათვალი:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ko": "효율적인 차단기: 메모리와 CPU에 부담이 적고, 다른 인기있는 차단기에 비해 수 천 가지의 필터를 사용할 수 있습니다.\n\n효율성에 대한 소개: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n사용 방법: 해당 웹사이트에서 팝업의 큰 전원 버튼을 눌러 uBlock을 켜고 끌 수 있습니다. 적용은 현재 웹사이트만 적용되며, 전체적으로 적용되지 않습니다.\n\n***\n\n\"AdBlocker\" 보다 더 유연합니다: 호스트 파일들로부터 필터를 만들고 볼 수 있습니다.\n\n특별한 설치 없이도 아래 목록들을 불러오고 적용할 수 있습니다:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\n당신이 원한다면 더 많은 목록을 선택할 수 있습니다:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 그리고 무수히 많은 다른 목록들\n\n물론, 더 많은 필터를 활성화하면 할수록, 메모리 사용량도 높아집니다. 하지만 Fanboy's two extra lists와 hpHosts’s Ad and tracking servers 필터를 추가한 후에도 uBlock₀은 다른 인기있는 차단기에 비해 메모리 사용량이 적습니다.\n\n또, 이러한 일부 추가 목록(특히 일반적으로 사용되는 호스트 파일) 중 선택시 높은 확률로 웹사이트가 파손될 수 있음을 명심해주시기 바랍니다.\n\n***\n\n필터에 필터 목록이 하나도 없다면, 이 확장기능은 아무 쓸모가 없어집니다. 그래서 만약 당신이 정말 어떤것으로든 기여하고 싶을때는, 당신이 사용중인 필터 리스트를 만들고 유지하기 위해 노력중인 사람들을 생각해주세요. 필터들은 모두 무료로 사용이 가능하게 되어있습니다.\n\n***\n\n완전히 무료입니다.\n오픈소스이며, 공개 라이센스(GPLv3)를 따릅니다.\n사용자를 위해, 사용자에 의해 만들어졌습니다.\n\n기여자 @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n기여자 @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n이것은 완전히 초기 버전입니다, 리뷰할 때 이 점을 명심하세요.\n\n프로젝트 변경사항:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "nl": "Een efficiënte adblocker: gebruikt weinig processorkracht en geheugen. Toch kan het duizenden filters meer laden en toepassen dan andere populaire adblockers.\n\nGeïllustreerde efficiëntievergelijking: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nGebruik: met de grote aan-uitknop in de pop-up kan uBlock voor de huidige website permanent worden in- of uitgeschakeld. Het wordt alleen op de huidige website toegepast; dit is geen algemene aan-uitknop.\n\n***\n\nFlexibel, want het is meer dan een ‘adblocker’: het kan ook filters inlezen en aanmaken vanuit hosts-bestanden.\n\nStandaard worden de volgende filterlijsten geladen en toegepast:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nEr zijn meer lijsten beschikbaar die u kunt inschakelen:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- en nog vele andere...\n\nNatuurlijk wordt het geheugengebruik groter naarmate er meer filters worden ingeschakeld. Maar zelfs na het inschakelen van Fanboy’s twee extra lijsten, hpHosts’s Ad en tracking servers, heeft uBlock een lager geheugengebruik dan andere populaire blockers.\n\nLet op, het gebruik van sommige van deze extra lijsten verhoogt de kans dat websites niet goed worden weergegeven - zeker de lijsten die normaal als hosts-bestand worden gebruikt.\n\n***\n\nZonder de standaard filterlijsten doet deze extensie niets. Als u dus ooit echt een bijdrage wilt leveren, denk dan aan de mensen die hard werken om de filterlijsten die u gebruikt te onderhouden, welke allemaal gratis beschikbaar zijn gemaakt.\n\n***\n\nVrij.\nOpen source met publieke licentie (GPLv3)\nVoor gebruikers, door gebruikers.\n\nMedewerkers @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nMedewerkers @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nOnthoud dat dit een hele vroege versie is wanneer u een beoordeling geeft.\n\nProjectwijzigingenlogboek:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pl": "Skutecznie blokuje reklamy, używa mało pamięci RAM i zasobów procesora, a przy tym może wczytywać i stosować o wiele więcej filtrów niż inne popularne rozszerzenia do blokowania reklam.\n\nIlustrowane porównanie z dodatkiem Adblock Plus: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nSposób użycia: wyświetlany w małym wyskakującym oknie przycisk służy do włączenia/wyłączenia rozszerzenia na bieżącej witrynie internetowej. Działanie przycisku ma zastosowanie tylko na bieżącej witrynie – nie działa globalnie.\n\n***\n\nElastyczny. Jest czymś więcej niż „blokerem reklam”. Może czytać i tworzyć filtry z plików hostów.\n\nPo zainstalowaniu są wczytywane i stosowane następujące listy filtrów:\n\n- EasyList\n- Lista serwerów reklam Petera Lowe'a\n- EasyPrivacy\n- Domeny ze złośliwym oprogramowaniem\n\nMożna wybrać więcej list filtrów:\n\n- Rozszerzona lista śledzenia dla fanboyów\n- Plik hostów Dana Pollocka\n- Serwery reklam i śledzenia hpHosts\n- MVPS HOSTS\n- Spam404\n- I wiele innych\n\nIm więcej filtrów jest włączonych, tym większe jest użycie pamięci RAM. Nawet po dodaniu dwóch dodatkowych list filtrów dla fanboyów – listy serwerów reklamowych i śledzących hpHosts – µBlock₀ używa mniej pamięci RAM niż inne popularne dodatki do blokowania reklam.\n\nNależy pamiętać, że wybranie niektórych dodatkowych list może prowadzić do wzrostu prawdopodobieństwa uszkodzenia witryny internetowej – zwłaszcza tych list, które są zwykle używane jako plik hostów.\n\n***\n\nBez zaprogramowanej listy filtrów, to rozszerzenie jest bezwartościowe. Pomyśl zatem o osobach, które ciężko pracują, tworząc i utrzymując udostępniane za darmo używane przez Ciebie listy filtrów.\n\n***\n\nDarmowe rozszerzenie.\nKod źródłowy udostępniany na otwartej licencji (GPLv3)\nDla użytkowników przez użytkowników.\n\nWspółtwórcy rozszerzenia: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nAutorzy tłumaczeń: <a href=\"https://outgoing.prod.mozaws.net/v1/a71b71f3a5bb24b1e45c14bf40ee736e1e270b279e4e77e849bf3a5b7650d85e/https%3A//crowdin.com/project/ublock/translators\" rel=\"nofollow\">https://crowdin.com/project/ublock/translators</a>\n\n***\n\nOceniając rozszerzenie pamiętaj, że jest to jego wczesna wersja.\n\nDziennik zmian:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pt-BR": "Um bloqueador eficaz: Com baixo consumo de memória e CPU e ainda pode carregar e aplicar milhares de filtros. Mais do que outros bloqueadores populares lá fora.\n\nVisão geral ilustrada de sua eficiência: <a href=\"https://outgoing.prod.mozaws.net/v1/54af2c2b64bc518ff8c7fb93e22ea308d599fca1bc03ab6aa8e08b0ec6566a80/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP</a> :-Efficiency-compared\n\nUtilização: Use o botão de energia no pop-up para desativar/ativar o uBlock₀ para o site atual. Isso se aplica apenas ao site atual, não é um botão global.\n\n***\n\nFlexível, é mais do que um \"ad blocker\": também pode ler e criar filtros a partir de arquivos de hosts.\n\nPor padrão, essas listas de filtros são carregadas e aplicadas:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMais listas estão disponíveis para você escolher, se desejar:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- E varios outros\n\nClaro, quanto mais filtros habilitados, maior o consumo de memória. Ainda, mesmo após a adição do Fanboy's duas listas extras, hpHosts’s e servidores de rastreamento, uBlock₀ ainda tem o consumo de memória menor do que outros bloqueadores populares lá fora.\n\nTambém, esteja ciente de que selecionar algumas dessas listas extras pode levar à maior probabilidade de quebra do layout do site, especialmente aquelas listas que são normalmente usadas como arquivo hosts.\n\n***\n\nSem as listas predefinidas de filtros, esta extensão não é nada. Então, se você realmente quiser contribuir com alguma coisa, pense sobre as pessoas que trabalham duro para manter as listas de filtro que você está usando, que estão disponíveis de graça para todos.\n\n***\n\nGratuito\nCódigo aberto com licença pública (GPLv3)\nDe usuários para usuários.\n\nContribuidores no Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuidores no Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEssa é uma versão ainda em desenvolvimento, tenha isso em mente quando você avaliar.\n\nRegistro de alterações do projeto:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "pt-PT": "Um bloqueador eficiente: leve na memória e CPU e, no entanto, consegue carregar e aplicar milhares de filtros a mais do que outros bloqueadores populares disponíveis.\n\nVisão geral ilustrada da sua eficiência:\n<a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilização: O botão grande de energia na janela serve para desativar ou ativar, permanentemente, o uBlock para o sítio web atual. Aplica-se unicamente ao sítio web atual, não sendo um botão de energia global.\n\n***\n\nFlexível, é mais do que um bloqueador de anúncios. Pode também ler e criar filtros a partir de ficheiros de servidores.\n\nPor predefinição, estas listas de filtros são carregadas e aplicadas:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nSe quiser, estão disponíveis mais listas para seleção:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- E muitas mais\n\nObviamente que quanto maior o número de filtros ativos, maior será o consumo de memória. No entanto, mesmo após adicionar as duas listas extra do Fanboy, hpHosts Ad and tracking servers, o uBlock₀ continua a consumir menos memória do que outros bloqueadores populares disponíveis.\n\nEsteja ciente de que se selecionar mais listas extra pode resultar numa probabilidade acrescida de rutura em alguns sítios web -- especialmente nas listas que, normalmente, são utilizadas como ficheiros de servidores.\n\n***\n\nSem as listas de filtros predefinidas, esta extensão não é nada. Se realmente quiser contribuir com algo, pense nas pessoas que trabalham duro para manter as listas de filtros que usa, que foram tornadas disponíveis para uso por todos sem custos.\n\n***\n\nGrátis.\nCódigo aberto com licença pública (GPLv3)\nDe utilizadores para utilizadores.\n\nContribuidores @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuidores @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEsta é uma versão inicial, tenha isso em mente quando avaliar.\n\nRegisto de alterações do projeto:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ro": "Un blocant (paravan) eficient: folosește foarte puțin procesorul și memoria și totuși poate încărca și aplica mii de filtre în plus față de alte paravane populare.\n\nO ilustrare a eficienței poate fi observată la:\n<a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUtilizare: Butonul mare de pornire/oprire în fereastra paravanului este pentru a activa/dezactiva uBlock pentru saitul curent. Funcția este valabilă doar pentru saitul curent, nu la nivel global.\n\n***\n\nFlexibil, mai mult decât un „blocant de reclame”: acesta poate citi și crea filtre din fișierele de gazde (hosts).\n\nÎn mod implicit, aceste liste de filtre sunt încărcate și aplicate:\n\n- EasyList\n- Lista serverelor de reclame a lui Peter Lowe\n- EasyPrivacy\n- Domenii malițioase\n\nDe asemenea, mai sunt disponibile și alte liste precum:\n\n- Lista îmbunătățită pentru urmărire a lui Fanboy\n- Lista de gazde a lui Dan Pollock\n- Lista de reclame și urmărire hpHosts\n- Gazdele MVPS\n- Spam404\n- Și multe altele\n\nDesigur, cu cât sunt mai multe filtre active cu atât mai mult este utilizată memoria. Totuși, chiar și după adăugarea în plus a două liste Fanboy și lista de reclame și urmărire hPhosts, uBlock₀ tot folosește mai puțină memorie decât restul paravanelor.\n\nDe ținut minte, că odată cu selectarea în plus a unora dintre liste se poate ajunge la afectarea aspectului saiturilor -- în special listele care sunt în mod normal liste de gazde.\n\n***\n\nFără listele prestabilite de filtre această extensie nu face nimic. Așadar, dacă totuși vreți să contribuiți, gândiți-vă la persoanele care muncesc să întrețină aceste filtre pe care le utilizați, care sunt oferite pentru utilizare gratuită.\n\n***\n\nGratuit.\nCu sursă liberă și licență publică (GPLv3)\nPentru utilizatori de la utilizatori.\n\nContribuitori pe Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nContribuitori pe Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nEste încă o aplicație recentă, gândiți-vă la acest lucru când scrieți o recenzie.\n\nLista de schimbări a proiectului:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ru": "µBlock — эффективный блокировщик: он использует меньше оперативной памяти и меньше нагружает ЦП, при этом используя больше фильтров, чем другие популярные блокировщики.\n\nИллюстрированный обзор его эффективности: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nИспользование: нажмите большую кнопку «Включение» в выпадающем окне, чтобы включить или отключить uBlock для текущего сайта. Это действует только для текущего сайта, а не для всех.\n\n***\n\nБудучи гибким, это нечто большее, чем просто «блокировщик рекламы»: он также может считывать и создавать фильтры из хост-файлов.\n\nПо умолчанию следующие списки фильтров будут загружены и применены:\n\n- EasyList\n- Список рекламных серверов Питера Лоу\n- EasyPrivacy\n- Вредоносные домены\n\nТакже на выбор доступны другие списки:\n\n- Фанатский улучшенный список отслеживания\n- Хост-файл Дэна Поллока\n- Рекламные и отслеживающие сервера hpHosts\n- MVPS HOSTS\n- Spam404\n- И т. д.\n\nКонечно, чем больше фильтров, тем выше использование памяти. Тем не менее даже после добавления трёх дополнительных списков uBlock₀ всё ещё потребляет меньше памяти, чем другие популярные блокировщики.\n\nТакже имейте в виду, что некоторые их этих списков имеют высокую вероятность поломать веб-сайт, особенно те, что созданы из хост-файлов.\n\n***\n\nБез предустановленных списков фильтров это расширение — ничто. Так что, если вы действительно хотите внести свой вклад, подумайте о людях, усердно поддерживающих списки фильтров, предоставленные Вам для бесплатного использования.\n\n***\n\nБесплатно.\nОткрытый исходный код, публичная лицензия (GPLv3).\nДля пользователей от пользователей.\n\nУчастники на Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nУчастники на Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nЭто ещё очень ранняя версия, имейте это в виду, оценивая программу.\n\nСписок изменений:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sk": "Účinný blokovač: nezaťažuje CPU a pamäť a dokáže načítať a vynútiť o niekoľko tisíc filtrov viac ako iné populárne blokovače.\n\nIlustrovaný prehľad o jeho účinnosti: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPoužitie: Veľký vypínač vo vyskakovacom okne natrvalo zakáže/povolí uBlock pre aktuálnu webovú stránku. Vzťahuje sa len na aktuálnu webovú stránku, nie na všeobecný vypínač.\n\n***\n\nFlexibilný, je viac než len \"blokovač reklám\": dokáže tiež načítať a vytvárať filtre z hosts súborov.\n\nTieto zoznamy filtrov sú predvolene načítané a vynútené:\n\n- EasyList\n- Zoznam reklamných serverov od Petra Lowesa\n- EasyPrivacy\n- Domény malvéru\n\nĎalšie zoznamy sú k dispozícii pre vás na výber, ak si prajete:\n\n- Rozšírený stopovací zoznam od Fanboya\n- Hosts súbor od Dana Pollocka\n- Reklamné a stopovacie servery od hpHosts\n- MVPS HOSTS\n- Spam404\n- A mnoho ďalších\n\nSamozrejme, čím viac povolených filtrov, tým vyššie nároky na pamäť. Aj po pridaní dvoch ďalších zoznamov od Fanboya, reklamných a stopovacích serverov od hpHost má uBlock stále menšie nároky na pamäť ako mnohé ďalšie veľmi populárne blockovače.\n\nĎalej majte na pamäti, že výber viacerých filtrov zvyšuje šancu chybného zobrazenie webov - predovšetkým u zoznamov, ktoré sa normálne používajú ako hosts súbory.\n\n***\n\nBez predvolených zoznamov filtrov by bolo toto rozšírenie k ničomu. Ak teda naozaj budete chcieť niečím prispieť, myslite na ľudí, ktorí spravujú vami používané zoznamy filtrov a uvoľňujú ich pre všetkých zadarmo.\n\n***\n\nBezplatný.\nOtvorený zdrojový kód s verejnou licenciou (GPLv3)\nPre používateľov od používateľov.\n\nPrispievatelia @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPrispievatelia @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nIde o pomerne skorú verziu, majte to na pamäti pri recenzovaní.\n\nZoznam zmien projektu:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sl": "Efektiven zatiralec oglasov: lahek na pomnilniku in procesorju, in vendar lahko nalaga in uveljavlja tisoče filtrov več kot kakšen drug popularen dodatek za blokiranje oglasov.\n\nIlustrirana efektivnost: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nUporaba: Velik gumb za vklop/izklop v pojavnem oknu je namenjen trajnemu izklopu/vklopu uBlock₀ za trenutno spletno stran. Ta uporaba velja samo za trenutno spletno stran, tako da gumb ne predstavlja globalnega vklopa/izklopa.\n\n***\n\nuBlock₀ je fleksibilen - in s tem več kot samo \"blokada oglasom\": lahko bere in ustvarja filtre iz datotek z gostitelji (HOSTS datoteka).\n\nBrez kakršnihkoli dodatnih nastavitev, uBlock₀ uporablja sledeče filtre:\n\n- EasyList\n- Seznam oglaševalskih strežnikov Peter Lowe\n- EasyPrivacy\n- Zlonamerne domene\n\nVeč filtrskih seznamov na razpolago (če to želite):\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- In mnogi drugi\n\nSeveda, več filtrov kot je aktivnih, večji je odtis v pomnilniku. Pa kljub temu - tudi z nalaganjem dveh dodatnih seznamov filtrov (Fanboy in hpHosts) ima uBlock₀ še vedno nižjo mero obremenitve pomnilnika kot ostali zelo popularni dodatki za blokiranje oglasov.\n\nPoleg tega bodite pozorni, da vklop določenih dodatnih seznamov filtrov lahko pripelje do višje verjetnosti za nefunkcionalnost spletne strani - predvsem \"ogrožajoči\" so tisti seznami, ki se jih ponavadi uporablja kot HOSTS datoteko.\n\n***\n\nBrez prednastavljenih seznamov filtrov, da dodatek ni nič. Tako da, če res želite kje pomagati ali komu plačati kavo, pomislite na ljudi, ki trdo delajo, da vzdržujejo te sezname filtrov, ki jih uporabljate, in so jih naredili dosegljive zastonj in za vse.\n\n***\n\nZastonj.\nOdprtokodno pod GPLv3 licenco\nZa uporabnike od uporabnikov.\n\nRazvijalci @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nPrevajalci @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nGre še za dokaj sveže različice, prosimo da to upoštevate pri vaši kritiki.\n\nDnevnik sprememb projekta:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sq": "Një bllokues efikas: me impakt të vogël te memorja dhe procesori, por mund të hapë dhe të zbatojë mijëra filtra më shumë sesa bllokuesit e tjerë të njohur.\n\nPërmbledhje e ilustruar e efikasitetit të tij: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nPërdorimi: Çelësi i komandimit te dritarja e vogël e bën uBlock përherë joaktiv/aktiv për uebsajtin aktual. Ai vlen vetëm për uebsajtin aktual, nuk është një çelës i përgjithshëm.\n\n***\n\nËshtë fleksibël dhe jo thjesht një \"bllokues reklamash\": mund të lexojë dhe të krijojë filtra nga skedat \"hosts\".\n\nFiltrat e listuar këtu hapen dhe zbatohen pas instalimit:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nPo të doni, ka edhe shumë lista të tjera të gatshme:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- Dhe shumë të tjera\n\nSigurisht që sa më shumë filtra të aktivizoni, aq më i madh do të jetë impakti te memorja. Edhe me shtimin e dy listave shtesë të Fanboy, hpHosts’s Ad and tracking servers, uBlock përsëri ka impakt më të ulët në memorje sesa bllokuesit e tjerë shumë të njohur.\n\nPor, kujdes, sepse duke përzgjedhur disa prej këtyre listave, gjasat që faqet të shfaqin probleme do të jenë më të mëdha -- sidomos listat që normalisht përdoren si skeda \"hosts\".\n\n***\n\nPa listat e programuara, ky program nuk vlen për asgjë. Prandaj, po të doni të kontribuoni diçka, mendoni pak për njerëzit që punojnë fort për mirëmbajtjen e listave me filtra që po përdorni, të cilat na ofrohen të gjithëve pa pagesë.\n\n***\n\nFalas.\nMaterial i hapur me licencë publike (GPLv3)\nKrijuar nga përdoruesit për përdoruesit.\n\nKontributorët @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nKontributorët @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nKur të bëni vlerësimin e programit, mos harroni se ky është një version paraprak.\n\nDitari i projektit:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "sv-SE": "En effektiv blockerare: lätt på minne och CPU-fotavtryck, som ändå kan ladda och applicera tusentals fler filter jämfört med andra populära blockerare där ute.\n\nIllustrerad översikt av dess effektivitet:\n<a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nAnvändning: Den stora strömbrytarikonen i popuprutan är till för att avaktivera/aktivera uBlock₀ på den aktuella webbplatsen permanent. Detta gäller enbart för den aktuella webbplatsen, det är inte en global strömbrytare.\n\n***\n\nFlexibel. uBlock₀ är inte enbart en \"reklamblockerare\": den kan också läsa och skapa filter från hosts-filer.\n\nSom standard är följande filterlistor laddade och applicerade:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nFler filterlistor finns tillgängliga att använda om du vill:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- med flera\n\nJu fler aktiverade filter, desto högre minnesanvändning. Men även efter att ha lagt till Fanboys två extra filterlistor och hpHosts' Ad and tracking servers så använder uBlock₀ mindre minne än andra väldigt populära blockerare.\n\nTänk på att genom att aktivera vissa av dessa extra filterlistor finns det större risk att webbplatser går sönder - särskilt de listor som i normala fall används som hosts-filer.\n\n***\n\nuBlock₀ vore ingenting utan filterlistorna. Så om du vill bidra med någonting, tänk på människorna som arbetar hårt med att upprätthålla de filterlistor du använder, vilka är fritt tillgängliga för allas användning.\n\n***\n\nGratis.\nÖppen källkod med offentlig licens (GPLv3)\nFör användare, av användare.\n\nBidragsgivare @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nBidragsgivare @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nDetta är en ganska tidig version, tänk på detta när du skriver en recension.\n\nProjektets ändringslogg:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "uk": "Ефективний блокувальник реклами: сильно не навантажує пам’ять та процесор і може працювати з набагато більшою кількістю фільтрів ніж інші блокувальники.\n\nІлюстрований огляд ефективності: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nВикористання: Ця велика кнопка живлення у виринаючому вікні дозволяє вимкнути або увімкнути uBlock для поточного веб-сайту. Ефект розповсюджується тільки на поточний веб-сайт. Це не глобальна кнопка живлення.\n\n***\n\nБудучи універсальним, це більш ніж просто \"блокувальник реклами\". Він також може створювати фільтри з файлів hosts.\n\nЗа замовчуванням завантажено та застосовано наступні списки фільтрів:\n\n– EasyList\n– список рекламних серверів Петра Лоу\n– EasyPrivacy\n– шкідливі домени\n\nНаступні списки можна можна увімкнути за бажанням:\n\n– покращений список слідкування від Fanboy\n– файл хостів Дена Полока\n– сервери реклами та слідкування hpHosts\n– MVPS HOSTS\n– Spam404\n– тощо.\n\nЗвичайно ж, чим більше фільтрів ви увімкнете тим більшим буде використання пам’яті. Однак, навіть після додання двох додаткових списків Fanboy, серверів слідкування та реклами phHosts, uBlock споживає менше пам’яті ніж інші популярні блокувальники.\n\nТакож майте на увазі, що задіяння деяких додаткових списків може спричинити збільшення ймовірності пошкодження функціонування сайту. Особливо ті списки, які зазвичай використовуються як hosts-файл.\n\n***\n\nБез встановлених списків фільтрів це розширення – ніщо. Тому, якщо ви дійсно хочете зробити свій внесок, подумайте про людей, які тяжко працюють для підтримки списків фільтрів якими ви користуєтесь безкоштовно.\n\n***\n\nБезкоштовно.\nВідкритий джерельний код та публічна ліцензія (GPLv3)\nДля користувачів від користувачів.\n\nУчасники @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nПерекладачі @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nЦе ще дуже дочасна версія, тому майте на увазі, коли робите огляд.\n\nЖурнал змін проекту:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "ur": "ایک زبردست اشتہارات کو روکنے والا سافٹویئر. کم میموری اور cpu استعمال کرتا ہے مگر کام بہترین کرتا ہے.\n\nاس کا بہترین اور پراثر کام کرنے کی تصاویر:\n<a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nہدایات: بڑا آن/ آف کا بٹن دبا کر آپ موجودہ ویب سائٹ پر uBlock کو فعال یا غیر فعال کر سکتے ہیں. یہ بٹن صرف موجودہ ویب سائٹ کے لئے ہے، باقی ویب سائٹس کو اس سے کوئی فرق نہیں پڑے گا.\n\n***\n\nFlexible, it's more than an \"ad blocker\": it can also read and create filters from hosts files.\n\nیہ والے فلٹر پہلے سے لاگو ہوں گے:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\nMore lists are available for you to select if you wish:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- And many others\n\nجتنے زیادہ فلٹر لگائیں گے اتنی زیادہ میموری لے گا. Yet, even after adding Fanboy's two extra lists, hpHosts’s Ad and tracking servers, uBlock still has a lower memory footprint than other very popular blockers out there.\n\nAlso, be aware that selecting some of these extra lists may lead to higher likelihood of web site breakage -- especially those lists which are normally used as hosts file.\n\n***\n\nWithout the preset lists of filters, this extension is nothing. So if ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\nمفت.\nاوپن سورس عوامی لائسنس(جی.پی.ایل ورژن ٣) کے ساتھ\nعوام کے لیے، عوام کا بنایا ہوا.\n\nمعاونین کی فہرست Github پر دیکھیں:\n<a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nمترجمین کی فہرست Crowdin پر دیکھیں:\n<a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\nپراجیکٹ میں ترقیاتی کام کا ریکارڈ:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "vi": "Một công cụ chặn quảng cáo hiệu quả: sử dụng ít bộ nhớ, CPU và có thể nạp, áp dụng hàng ngàn bộ lọc so với những công cụ chặn quảng cáo hiện nay.\n\nMinh hoạ tổng quan về tính hiệu quả của µBlock: <a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\nSử dụng: Nút nguồn lớn trong hộp thoại popup để vô hiệu/kích hoạt vĩnh viễn uBlock cho website hiện tại. Nó chỉ áp dụng cho trang hiện tại, không phải tất cả website.\n\n***\n\nLinh hoạt, hơn cả một \"công cụ chặn quảng cáo\": µBlock có thể đọc và tạo bộ lọc từ tập tin hosts.\n\nNgay lập tức, những bộ lọc này được nạp và áp dụng:\n\n- EasyList\n- Danh sách máy chủ quảng cáo của Peter Lowe\n- EasyPrivacy\n- Malware domains\n\nCó thêm nhiều danh sách để bạn lựa chọn:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- Máy chủ quảng cáo và theo dõi hpHosts\n- MVPS HOSTS\n- Spam404\n- Và nhiều hơn nữa\n\nDĩ nhiên, khi kích hoạt thêm bộ lọc, tiện ích sẽ dùng nhiều bộ nhớ hơn. Tuy vậy, sau khi thêm hai danh sách Fanboy, máy chủ quảng cáo và theo dõi của hpHosts, uBlock vẫn dùng ít bộ nhớ hơn so với những công cụ chặn quảng cáo rất phổ biến khác.\n\nNgoài ra, lưu ý rằng chọn thêm một số danh sách có thể dẫn đến khả năng một số website hiển thị không đúng cách -- đặc biệt là những danh sách thường được dùng như tập tin hosts.\n\n***\n\nKhông có danh sách bộ lọc cài sẵn, tiện ích mở rộng này chẳng là gì cả. Vậy nên nếu bạn thật sự muốn đóng góp gì đó, hãy nghĩ về những người đang chăm chỉ duy trì danh sách bộ lọc hoàn toàn miễn phí mà bạn đang dùng.\n\n***\n\nMiễn phí.\nNguồn mở với giấy phép công cộng (GPLv3)\nLàm vì người dùng bởi người dùng.\n\nNhững người đóng góp @ Github: <a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\nNhững người đóng góp @ Crowdin: <a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\nĐây là một phiên bản khá mới, hãy ghi nhớ điều này khi bạn đánh giá.\n\nThay đổi của dự án:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "zh-CN": "一款高效的请求过滤工具:占用极低的内存和CPU,和其他常见的过滤工具相比,它能够加载并执行上千条过滤规则。\n\n效率概述说明: \n<a href=\"https://outgoing.prod.mozaws.net/v1/407a22e7e017297705e927653caa7e67ad67aabb741f33b91851c56c0544a0d9/https%3A//github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared</a>\n\n用法:点击弹出窗口中的电源按钮,uBlock 将对当前网页永久禁用/启用过滤功能。 它只控制当前网页的请求过滤,而不是一个全局开关。 它只控制当前网页的请求过滤,而不是一个全局开关。\n\n***\n\n它不只是一个广告拦截工具,它还可以从 hosts 文件里读取和创建过滤规则。\n\n初始默认加载和执行下列过滤规则列表:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n\n这里还有更多的规则列表供你选择:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 等等\n\n当然,启用越多的过滤规则就会产生越高的内存占用。 然而,即使再添加 Fanboy 额外的两个规则列表,如 hpHosts’s Ad 和 tracking servers,uBlock 的内存占用依然比其他常见的过滤工具要低的多。\n\n另外请注意,选择一些额外的列表可能会导致网页破损可能性增高 —— 尤其是那些通常被用作 hosts 文件的列表。\n\n***\n\n没有这些过滤规则列表,这个扩展就没有了意义。 所以如果你真的想做点贡献,想想那些维护过滤规则的人们,是他们让所有人能够免费使用这一切变得可能。\n\n***\n\n免费。\n遵从 GPLv3 公共许可协议开源。\n一切为了用户。\n\n贡献者 @ Github:\n<a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n贡献者 @ Crowdin:\n<a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n它还是一个相当早期的版本,在您评论的时候请记住这一点。\n\n项目更新日志:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>",
+ "zh-TW": "一款高效率的廣告攔截工具:只使用超低的記憶體和CPU使用量,和其他常見的廣告攔截工具相比,可以載入並執行上千條過濾規則。\n\n效率概述說明: \n<a href=\"https://outgoing.prod.mozaws.net/v1/90cf866e9b2e1ea9282c8c93e7a0891c713248d4bf07b8aaefe26d97f8ccde33/https%3A//github.com/gorhill/uBlock/wiki/%25C2%25B5Block-vs.-ABP%3A-efficiency-compared\" rel=\"nofollow\">https://github.com/gorhill/uBlock/wiki/%C2%B5Block-vs.-ABP:-efficiency-compared</a>\n\n用法:點選快顯視窗中的電源按鈕,μBlock將會在目前正在瀏覽的網頁永久停用/啟用廣告攔截功能。 它只適用於目前正在瀏覽的網頁,而不是全域按鈕。\n\n***\n\n這不只是一個廣告攔截工具,它還可以非常有彈性的從hosts檔裡讀取和建立過濾規則。\n\n初始預設載入和執行下列過濾規則:\n\n- EasyList\n- Peter Lowe’s Ad server list\n- EasyPrivacy\n- Malware domains\n- Long-lived malware domains\n- Malware Domains List\n\n這裡還擁有更多的過濾規則供你選擇:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- hpHosts’s Ad and tracking servers\n- MVPS HOSTS\n- Spam404\n- 其他\n\n啟用越多的過濾規則就會佔用越多的記憶體。 然而,即使在加入 Fanboy 額外的兩個規則和 hpHosts’s Ad and tracking servers,uBlock₀ 的記憶體佔用依然比其他常見的過濾工具要小的多。\n\n另外,請注意選擇的一些額外的清單可能會導致網頁破損可能性增高 — — 尤其是那些通常用來當作hosts檔案的清單。\n\n***\n\n沒有這些過濾規則清單,這個擴充套件就沒有了意義。 所以如果你真的想要做些貢獻,試著想想那些努力維護廣告過濾規則清單的人們,至少他們讓大家可以免費使用這一切。\n\n***\n\n自由、免費。\n開放原始程式碼與公共許可證 (GPLv3)\n一切都是為了使用者。\n\n貢獻者@ Github: \n<a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">https://github.com/gorhill/uBlock/graphs/contributors</a>\n貢獻者 @ Crowdin: \n<a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">https://crowdin.net/project/ublock</a>\n\n***\n\n這還只是一個非常初期的版本,當您留下建議的時候請手下留情。\n\n專案更新日誌:\n<a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">https://github.com/gorhill/uBlock/releases</a>"
+ },
+ "developer_comments": null,
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/edit",
+ "guid": null,
+ "has_eula": false,
+ "has_privacy_policy": true,
+ "homepage": {
+ "ar": "",
+ "bg": "",
+ "bn-BD": "",
+ "ca": "",
+ "cs": "",
+ "da": "",
+ "de": "",
+ "el": "",
+ "en-US": "https://github.com/gorhill/uBlock#ublock-origin",
+ "es": "",
+ "eu": "",
+ "fa": "",
+ "fi": "",
+ "fr": "",
+ "he": "",
+ "hu": "",
+ "id": "",
+ "it": "",
+ "ja": "",
+ "ko": "",
+ "nl": "",
+ "pl": "",
+ "pt-BR": "",
+ "pt-PT": "",
+ "ro": "",
+ "ru": "",
+ "sk": "",
+ "sl": "",
+ "sq": "",
+ "sv-SE": "",
+ "uk": "",
+ "vi": "",
+ "zh-CN": "",
+ "zh-TW": ""
+ },
+ "icon_url": null,
+ "icons": {
+ "32": "https://addons.cdn.mozilla.net/user-media/addon_icons/607/607454-32.png?modified=mcrushed",
+ "64": "https://addons.cdn.mozilla.net/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "128": "https://addons.cdn.mozilla.net/user-media/addon_icons/607/607454-128.png?modified=mcrushed"
+ },
+ "is_disabled": false,
+ "is_experimental": false,
+ "is_featured": true,
+ "is_recommended": true,
+ "is_source_public": true,
+ "last_updated": null,
+ "name": null,
+ "previews": [
+ {
+ "id": 157572,
+ "caption": {
+ "en-US": "Default mode"
+ },
+ "image_size": [
+ 640,
+ 480
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/157/157572.png?modified=1543520531",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/157/157572.png?modified=1543520531"
+ },
+ {
+ "id": 157576,
+ "caption": {
+ "en-US": "The dashboard: stock filter lists"
+ },
+ "image_size": [
+ 640,
+ 480
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/157/157576.png?modified=1543520531",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/157/157576.png?modified=1543520531"
+ },
+ {
+ "id": 157592,
+ "caption": {
+ "en-US": "Dynamic filtering allows default-deny mode"
+ },
+ "image_size": [
+ 640,
+ 480
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/157/157592.png?modified=1543520532",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/157/157592.png?modified=1543520532"
+ },
+ {
+ "id": 159634,
+ "caption": {
+ "en-US": "The dashboard: settings"
+ },
+ "image_size": [
+ 640,
+ 480
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/159/159634.png?modified=1543520533",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/159/159634.png?modified=1543520533"
+ },
+ {
+ "id": 158734,
+ "caption": {
+ "en-US": "Unified logger"
+ },
+ "image_size": [
+ 700,
+ 525
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/158/158734.png?modified=1543520534",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/158/158734.png?modified=1543520534"
+ }
+ ],
+ "public_stats": true,
+ "ratings": null,
+ "ratings_url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/reviews/",
+ "requires_payment": false,
+ "review_url": "https://addons.mozilla.org/en-US/reviewers/review/607454",
+ "slug": "ublock-origin",
+ "status": "public",
+ "summary": null,
+ "support_email": null,
+ "support_url": {
+ "en-US": "https://old.reddit.com/r/uBlockOrigin/",
+ "ka": "https://old.reddit.com/r/uBlockOrigin/",
+ "ur": "https://old.reddit.com/r/uBlockOrigin/"
+ },
+ "tags": [],
+ "type": "extension",
+ "url": null,
+ "weekly_downloads": 260708
+ },
+ "notes": null
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/resources/localized_collection.json b/mobile/android/android-components/components/feature/addons/src/test/resources/localized_collection.json
new file mode 100644
index 0000000000..db7a59ad18
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/resources/localized_collection.json
@@ -0,0 +1,224 @@
+{
+ "page_size": 50,
+ "page_count": 1,
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "addon": {
+ "id": 607454,
+ "authors": [
+ {
+ "id": 11423598,
+ "name": "Raymond Hill",
+ "url": "https://addons.mozilla.org/en-US/firefox/user/11423598/",
+ "username": "gorhill",
+ "picture_url": null
+ }
+ ],
+ "average_daily_users": 5060298,
+ "categories": {
+ "android": [
+ "security-privacy"
+ ],
+ "firefox": [
+ "privacy-security"
+ ]
+ },
+ "contributions_url": "",
+ "created": "2015-04-25T07:26:22Z",
+ "current_version": {
+ "id": 5174693,
+ "compatibility": {
+ "firefox": {
+ "min": "57.0",
+ "max": "*"
+ },
+ "android": {
+ "min": "57.0",
+ "max": "*"
+ }
+ },
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/versions/5174693",
+ "files": [
+ {
+ "id": 3719054,
+ "created": "2021-02-01T14:04:16Z",
+ "hash": "sha256:5c3a5ef6f5b5475895053238026360020d6793b05541d20032ea9dd1c9cae451",
+ "is_restart_required": false,
+ "is_webextension": true,
+ "is_mozilla_signed_extension": false,
+ "platform": "all",
+ "size": 2742973,
+ "status": "public",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/3719054/ublock_origin-1.33.2-an+fx.xpi",
+ "permissions": [
+ "dns",
+ "menus",
+ "privacy",
+ "storage",
+ "tabs",
+ "unlimitedStorage",
+ "webNavigation",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ "http://*/*",
+ "https://*/*",
+ "file://*/*",
+ "https://easylist.to/*",
+ "https://*.fanboy.co.nz/*",
+ "https://filterlists.com/*",
+ "https://forums.lanik.us/*",
+ "https://github.com/*",
+ "https://*.github.io/*"
+ ],
+ "optional_permissions": []
+ }
+ ],
+ "is_strict_compatibility_enabled": false,
+ "license": {
+ "id": 6,
+ "is_custom": false,
+ "name": "GNU General Public License, version 3.0",
+ "url": "http://www.gnu.org/licenses/gpl-3.0.html"
+ },
+ "release_notes": "<a href=\"https://outgoing.prod.mozaws.net/v1/a24fd1d9a1598d49cdc2cdc6d3ecd435971254320856ac32db39b0c4d9f5e126/https%3A//github.com/gorhill/uBlock/releases/tag/1.33.2\" rel=\"nofollow\">Complete release notes</a>.\n\n<b>Closed as fixed:</b>\n\n<ul><li><a href=\"https://outgoing.prod.mozaws.net/v1/0e94bcfacc284b39a3164377711e52b9ded21cda0d40494a43fd0052d8816860/https%3A//github.com/uBlockOrigin/uBlock-issues/issues/1480\" rel=\"nofollow\">After downgrading to 1.32.4, uBO is broken</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/88ab9531e629dc0a259dbe61b357b13316fa1a9c2f5c93f9208de5937b84a497/https%3A//github.com/uBlockOrigin/uBlock-issues/issues/1478\" rel=\"nofollow\">Whitelisting a site on Chromium Edge still blocks resources fetched by service worker</a></li></ul>\n<a href=\"https://outgoing.prod.mozaws.net/v1/35c6fcff904629a7392564335b09eb1fe5f661f9cd3a6a30534b2c49612c1b5e/https%3A//github.com/gorhill/uBlock/compare/1.33.0...1.33.2\" rel=\"nofollow\">Commits history since 1.33.0</a>.",
+ "reviewed": null,
+ "version": "1.33.2"
+ },
+ "default_locale": "en-US",
+ "description": "uBlock Origin is <b>not</b> an \"ad blocker\", it's a wide-spectrum content blocker with CPU and memory efficiency as a primary feature.\n\n***\n\nOut of the box, these lists of filters are loaded and enforced:\n\n- EasyList (ads)\n- Peter Lowe’s Ad server list (ads and tracking)\n- EasyPrivacy (tracking)\n- Malware domains\n\nMore lists are available for you to select if you wish:\n\n- Fanboy’s Enhanced Tracking List\n- Dan Pollock’s hosts file\n- MVPS HOSTS\n- Spam404\n- And many others\n\nAdditionally, you can point-and-click to block JavaScript locally or globally, create your own global or local rules to override entries from filter lists, and many more advanced features.\n\n***\n\nFree.\nOpen source with public license (GPLv3)\nFor users by users.\n\nIf ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.\n\n***\n\n<ul><li><a href=\"https://outgoing.prod.mozaws.net/v1/788d66e7299bdfb1da05832994551640d0ad441e148a3e29afe8dd0a5a90800c/https%3A//github.com/gorhill/uBlock%23ublock-origin\" rel=\"nofollow\">Documentation</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/de148deb19b52874eb4c5726859834f3294a6057ed44e873c958acee4c920062/https%3A//github.com/gorhill/uBlock/releases\" rel=\"nofollow\">Release notes</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/32c3d6819f5263e56c265042e8d34e2da4d974e73a7ad55a81786d8995cf65a9/https%3A//www.reddit.com/r/uBlockOrigin/\" rel=\"nofollow\">Community support @ Reddit</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/9bfaba4f3fe3310ae0a3189187f5fbab185097d85398752765e086889775079e/https%3A//github.com/gorhill/uBlock/graphs/contributors\" rel=\"nofollow\">Contributors @ GitHub</a></li><li><a href=\"https://outgoing.prod.mozaws.net/v1/6a44868e1580018df8d4d87adf8e97cc74b71b962a73dd5a64604f99db767287/https%3A//crowdin.net/project/ublock\" rel=\"nofollow\">Contributors @ Crowdin</a></li></ul>",
+ "developer_comments": null,
+ "edit_url": "https://addons.mozilla.org/en-US/developers/addon/ublock-origin/edit",
+ "guid": "uBlock0@raymondhill.net",
+ "has_eula": false,
+ "has_privacy_policy": true,
+ "homepage": "https://github.com/gorhill/uBlock#ublock-origin",
+ "icon_url": "https://addons.cdn.mozilla.net/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "icons": {
+ "32": "https://addons.cdn.mozilla.net/user-media/addon_icons/607/607454-32.png?modified=mcrushed",
+ "64": "https://addons.cdn.mozilla.net/user-media/addon_icons/607/607454-64.png?modified=mcrushed",
+ "128": "https://addons.cdn.mozilla.net/user-media/addon_icons/607/607454-128.png?modified=mcrushed"
+ },
+ "is_disabled": false,
+ "is_experimental": false,
+ "last_updated": "2021-02-04T12:05:14Z",
+ "name": "uBlock Origin",
+ "previews": [
+ {
+ "id": 238546,
+ "caption": "The popup panel: default mode",
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/238/238546.png?modified=1590420038",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/238/238546.png?modified=1590420038"
+ },
+ {
+ "id": 238548,
+ "caption": "The dashboard: stock filter lists",
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/238/238548.png?modified=1590420038",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/238/238548.png?modified=1590420038"
+ },
+ {
+ "id": 238547,
+ "caption": "The popup panel: default-deny mode",
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/238/238547.png?modified=1590420038",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/238/238547.png?modified=1590420038"
+ },
+ {
+ "id": 238549,
+ "caption": "The dashboard: settings",
+ "image_size": [
+ 1011,
+ 758
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/238/238549.png?modified=1590420038",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/238/238549.png?modified=1590420038"
+ },
+ {
+ "id": 238552,
+ "caption": "The popup panel in Firefox Preview: default mode with more blocking options revealed",
+ "image_size": [
+ 970,
+ 1800
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/238/238552.png?modified=1590420044",
+ "thumbnail_size": [
+ 259,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/238/238552.png?modified=1590420044"
+ },
+ {
+ "id": 230370,
+ "caption": "The unified logger tells you all that uBO is seeing and doing",
+ "image_size": [
+ 800,
+ 600
+ ],
+ "image_url": "https://addons.cdn.mozilla.net/user-media/previews/full/230/230370.png?modified=1590420038",
+ "thumbnail_size": [
+ 640,
+ 480
+ ],
+ "thumbnail_url": "https://addons.cdn.mozilla.net/user-media/previews/thumbs/230/230370.png?modified=1590420038"
+ }
+ ],
+ "promoted": {
+ "apps": [
+ "firefox",
+ "android"
+ ],
+ "category": "recommended"
+ },
+ "ratings": {
+ "average": 4.7336,
+ "bayesian_average": 4.7331695325641405,
+ "count": 13324,
+ "text_count": 4433
+ },
+ "ratings_url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/reviews/",
+ "requires_payment": false,
+ "review_url": "https://addons.mozilla.org/en-US/reviewers/review/607454",
+ "slug": "ublock-origin",
+ "status": "public",
+ "summary": "Finally, an efficient wide-spectrum content blocker. Easy on CPU and memory.",
+ "support_email": null,
+ "support_url": "https://old.reddit.com/r/uBlockOrigin/",
+ "tags": [],
+ "type": "extension",
+ "url": "https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/",
+ "weekly_downloads": 125955
+ },
+ "notes": null
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/addons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/addons/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/addons/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/addons/src/test/resources/png/mozac.png b/mobile/android/android-components/components/feature/addons/src/test/resources/png/mozac.png
new file mode 100644
index 0000000000..2a03203476
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/resources/png/mozac.png
Binary files differ
diff --git a/mobile/android/android-components/components/feature/addons/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/addons/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/addons/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
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
diff --git a/mobile/android/android-components/components/feature/autofill/README.md b/mobile/android/android-components/components/feature/autofill/README.md
new file mode 100644
index 0000000000..281bd140f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Autofill
+
+A component that provides support for Android's Autofill framework.
+
+## Usage
+
+### 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-autofill:{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/autofill/build.gradle b/mobile/android/android-components/components/feature/autofill/build.gradle
new file mode 100644
index 0000000000..033eebdfe1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.autofill'
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':concept-fetch')
+ implementation project(':concept-storage')
+ implementation project(':lib-publicsuffixlist')
+ implementation project(':service-digitalassetlinks')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(":support-utils")
+ implementation project(':ui-widgets')
+
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_autofill
+ implementation ComponentsDependencies.androidx_biometric
+ implementation ComponentsDependencies.androidx_fragment
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+ implementation ComponentsDependencies.androidx_recyclerview
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_preferences
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+ testImplementation project(':lib-fetch-okhttp')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_mockwebserver
+}
+
+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/autofill/proguard-rules.pro b/mobile/android/android-components/components/feature/autofill/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/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/autofill/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/autofill/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..80ef4db858
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
+ tools:ignore="QueryAllPackagesPermission" />
+
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AbstractAutofillService.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AbstractAutofillService.kt
new file mode 100644
index 0000000000..1d0fb7f97e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AbstractAutofillService.kt
@@ -0,0 +1,83 @@
+/* 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.autofill
+
+import android.os.Build
+import android.os.CancellationSignal
+import android.service.autofill.AutofillService
+import android.service.autofill.FillCallback
+import android.service.autofill.FillRequest
+import android.service.autofill.SaveCallback
+import android.service.autofill.SaveRequest
+import android.widget.inline.InlinePresentationSpec
+import androidx.annotation.RequiresApi
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import mozilla.components.feature.autofill.handler.FillRequestHandler
+import mozilla.components.feature.autofill.handler.MAX_LOGINS
+import mozilla.components.feature.autofill.structure.toRawStructure
+
+/**
+ * Service responsible for implementing Android's Autofill framework.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+abstract class AbstractAutofillService : AutofillService() {
+ abstract val configuration: AutofillConfiguration
+
+ private val fillHandler by lazy { FillRequestHandler(context = this, configuration) }
+
+ override fun onFillRequest(
+ request: FillRequest,
+ cancellationSignal: CancellationSignal,
+ callback: FillCallback,
+ ) {
+ // We are using GlobalScope here instead of a scope bound to the service since the service
+ // seems to get destroyed before we invoke a method on the callback. So we need a scope that
+ // lives longer than the service.
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(Dispatchers.IO) {
+ // You may be wondering why we translate the AssistStructure into a RawStructure and then
+ // create a FillResponseBuilder that outputs the FillResponse. This is purely for testing.
+ // Neither AssistStructure nor FillResponse can be created by us and they do not let us
+ // inspect their data. So we create these intermediate objects that we can create and
+ // inspect in unit tests.
+ val structure = request.fillContexts.last().structure.toRawStructure()
+ val responseBuilder = fillHandler.handle(
+ structure,
+ maxSuggestionCount = request.getMaxSuggestionCount(),
+ )
+ val response = responseBuilder?.build(
+ this@AbstractAutofillService,
+ configuration,
+ request.getInlinePresentationSpec(),
+ )
+ callback.onSuccess(response)
+ }
+ }
+
+ override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
+ // This callback should not get invoked since we do not indicate that we are interested in
+ // saving any data (yet). If for whatever reason it does get invoked then we pretent that
+ // we handled the request successfully. Calling onFailure() requires to pass in a message
+ // and on Android systems before Q this message may be shown in a toast.
+ callback.onSuccess()
+ }
+}
+
+internal fun FillRequest.getInlinePresentationSpec(): InlinePresentationSpec? {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return inlineSuggestionsRequest?.inlinePresentationSpecs?.last()
+ } else {
+ return null
+ }
+}
+
+internal fun FillRequest.getMaxSuggestionCount() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ (inlineSuggestionsRequest?.maxSuggestionCount ?: 1) - 1 // space for search chip
+} else {
+ MAX_LOGINS
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AutofillConfiguration.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AutofillConfiguration.kt
new file mode 100644
index 0000000000..79b31d81f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AutofillConfiguration.kt
@@ -0,0 +1,45 @@
+/* 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.autofill
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.storage.LoginsStorage
+import mozilla.components.feature.autofill.lock.AutofillLock
+import mozilla.components.feature.autofill.ui.AbstractAutofillConfirmActivity
+import mozilla.components.feature.autofill.ui.AbstractAutofillSearchActivity
+import mozilla.components.feature.autofill.ui.AbstractAutofillUnlockActivity
+import mozilla.components.feature.autofill.verify.CredentialAccessVerifier
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.service.digitalassetlinks.local.StatementApi
+import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
+
+/**
+ * Configuration for the "Autofill" feature.
+ *
+ * @property storage The [LoginsStorage] used for looking up accounts and passwords to autofill.
+ * @property publicSuffixList Global instance of the public suffix list used for matching domains.
+ * @property unlockActivity Activity class that implements [AbstractAutofillUnlockActivity].
+ * @property confirmActivity Activity class that implements [AbstractAutofillConfirmActivity].
+ * @property searchActivity Activity class that implements [AbstractAutofillSearchActivity].
+ * @property applicationName The name of the application that integrates this feature. Used in UI.
+ * @property lock Global [AutofillLock] instance used for unlocking the autofill service.
+ * @property verifier Helper for verifying the connection between a domain and an application.
+ * @property activityRequestCode The request code used for pending intents that launch an activity
+ * on behalf of the autofill service.
+ */
+data class AutofillConfiguration(
+ val storage: LoginsStorage,
+ val publicSuffixList: PublicSuffixList,
+ val unlockActivity: Class<*>,
+ val confirmActivity: Class<*>,
+ val searchActivity: Class<*>,
+ val applicationName: String,
+ val httpClient: Client,
+ val lock: AutofillLock = AutofillLock(),
+ val verifier: CredentialAccessVerifier = CredentialAccessVerifier(
+ StatementRelationChecker(StatementApi(httpClient)),
+ ),
+ val activityRequestCode: Int = 1010,
+)
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AutofillUseCases.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AutofillUseCases.kt
new file mode 100644
index 0000000000..8a4a882caf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AutofillUseCases.kt
@@ -0,0 +1,83 @@
+/* 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.autofill
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import android.view.autofill.AutofillManager
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Use cases for common Android Autofill tasks.
+ */
+@SuppressLint("NewApi") // All API calls are checked properly.
+class AutofillUseCases(
+ @VisibleForTesting sdkVersion: Int = Build.VERSION.SDK_INT,
+) {
+ private val isAutofillAvailable = sdkVersion >= Build.VERSION_CODES.O
+ private val logger = Logger("AutofillUseCases")
+
+ /**
+ * Returns true if Autofill is supported by the current device.
+ */
+ fun isSupported(context: Context): Boolean {
+ if (!isAutofillAvailable) {
+ return false
+ }
+
+ return context.getSystemService(AutofillManager::class.java)
+ .isAutofillSupported
+ }
+
+ /**
+ * Returns true if this application is providing Autofill services for the current user.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ fun isEnabled(context: Context): Boolean {
+ if (!isAutofillAvailable) {
+ return false
+ }
+
+ return try {
+ context.getSystemService(AutofillManager::class.java)
+ .hasEnabledAutofillServices()
+ } catch (e: RuntimeException) {
+ // Without more detail about why the system service has timed out, it's easiest to assume
+ // that the failure will continue and so disable the service for now.
+ logger.error("System service lookup has timed out")
+ false
+ }
+ }
+
+ /**
+ * Opens the system's autofill settings to let the user select an autofill service.
+ */
+ fun enable(context: Context) {
+ if (!isAutofillAvailable) {
+ return
+ }
+
+ val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
+ intent.data = Uri.parse("package:${context.packageName}")
+ context.startActivity(intent)
+ }
+
+ /**
+ * Disables autofill if this application is providing Autofill services for the current user.
+ */
+ fun disable(context: Context) {
+ if (!isAutofillAvailable) {
+ return
+ }
+
+ context.getSystemService(AutofillManager::class.java)
+ .disableAutofillServices()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/Authenticator.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/Authenticator.kt
new file mode 100644
index 0000000000..e1034c634e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/Authenticator.kt
@@ -0,0 +1,61 @@
+/* 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.autofill.authenticator
+
+import android.content.Context
+import androidx.fragment.app.FragmentActivity
+import mozilla.components.feature.autofill.AutofillConfiguration
+
+/**
+ * Shared interface to support multiple authentication methods.
+ */
+internal interface Authenticator {
+ /**
+ * Shows an authentication prompt and will invoke [callback] once authentication succeeded or
+ * failed.
+ */
+ fun prompt(activity: FragmentActivity, callback: Callback)
+
+ /**
+ * For passing an activity launch result to the authenticator.
+ */
+ fun onActivityResult(requestCode: Int, resultCode: Int)
+
+ /**
+ * Callback getting invoked by an [Authenticator] implementation once authentication completed.
+ */
+ interface Callback {
+ /**
+ * Called when a biometric (e.g. fingerprint, face, etc.) is recognized, indicating that the
+ * user has successfully authenticated.
+ */
+ fun onAuthenticationSucceeded()
+
+ /**
+ * Called when a biometric (e.g. fingerprint, face, etc.) is presented but not recognized as
+ * belonging to the user.
+ */
+ fun onAuthenticationFailed()
+
+ /**
+ * Called when an unrecoverable error has been encountered and authentication has stopped.
+ */
+ fun onAuthenticationError()
+ }
+}
+
+/**
+ * Creates an [Authenticator] for the current device setup.
+ */
+internal fun createAuthenticator(
+ context: Context,
+ configuration: AutofillConfiguration,
+): Authenticator? {
+ return when {
+ BiometricAuthenticator.isAvailable(context) -> BiometricAuthenticator(configuration)
+ DeviceCredentialAuthenticator.isAvailable(context) -> DeviceCredentialAuthenticator(configuration)
+ else -> null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/BiometricAuthenticator.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/BiometricAuthenticator.kt
new file mode 100644
index 0000000000..8c82fa3452
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/BiometricAuthenticator.kt
@@ -0,0 +1,81 @@
+/* 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.autofill.authenticator
+
+import android.content.Context
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.R
+
+private const val AUTHENTICATORS =
+ BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.DEVICE_CREDENTIAL
+
+/**
+ * [Authenticator] implementation that uses [BiometricManager] and [BiometricPrompt] to authorize
+ * the user.
+ */
+internal class BiometricAuthenticator(
+ private val configuration: AutofillConfiguration,
+) : Authenticator {
+
+ override fun prompt(activity: FragmentActivity, callback: Authenticator.Callback) {
+ val executor = ContextCompat.getMainExecutor(activity)
+ val biometricPrompt = BiometricPrompt(activity, executor, PromptCallback(callback))
+
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setAllowedAuthenticators(AUTHENTICATORS)
+ .setTitle(
+ activity.getString(
+ R.string.mozac_feature_autofill_popup_unlock_application,
+ configuration.applicationName,
+ ),
+ )
+ .build()
+
+ biometricPrompt.authenticate(promptInfo)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int) = Unit
+
+ companion object {
+ /**
+ * Returns `true` if biometric authentication with [BiometricAuthenticator] is possible.
+ */
+ fun isAvailable(context: Context): Boolean {
+ val biometricManager = BiometricManager.from(context)
+ return biometricManager.canAuthenticate(AUTHENTICATORS) ==
+ BiometricManager.BIOMETRIC_SUCCESS
+ }
+
+ /**
+ * Returns `true` if biometric authentication with [BiometricAuthenticator] is not possible
+ * yet, but the user can enroll and create credentials for it.
+ */
+ fun canEnroll(context: Context): Boolean {
+ val biometricManager = BiometricManager.from(context)
+ return biometricManager.canAuthenticate(AUTHENTICATORS) ==
+ BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
+ }
+ }
+}
+
+private class PromptCallback(
+ private val callback: Authenticator.Callback,
+) : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ callback.onAuthenticationSucceeded()
+ }
+
+ override fun onAuthenticationFailed() {
+ callback.onAuthenticationFailed()
+ }
+
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ callback.onAuthenticationError()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/DeviceCredentialAuthenticator.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/DeviceCredentialAuthenticator.kt
new file mode 100644
index 0000000000..c8780d9d10
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/authenticator/DeviceCredentialAuthenticator.kt
@@ -0,0 +1,51 @@
+/* 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.autofill.authenticator
+
+import android.app.Activity.RESULT_OK
+import android.app.KeyguardManager
+import android.content.Context
+import androidx.fragment.app.FragmentActivity
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.R
+
+/**
+ * [Authenticator] implementation that uses Android's [KeyguardManager] to authenticate the user.
+ */
+internal class DeviceCredentialAuthenticator(
+ private val configuration: AutofillConfiguration,
+) : Authenticator {
+ private var callback: Authenticator.Callback? = null
+
+ @Suppress("Deprecation") // This is only used when BiometricPrompt is unavailable
+ override fun prompt(activity: FragmentActivity, callback: Authenticator.Callback) {
+ this.callback = callback
+
+ val manager = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
+ val intent = manager!!.createConfirmDeviceCredentialIntent(
+ activity.getString(
+ R.string.mozac_feature_autofill_popup_unlock_application,
+ configuration.applicationName,
+ ),
+ "",
+ )
+ activity.startActivityForResult(intent, configuration.activityRequestCode)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int) {
+ if (requestCode == configuration.activityRequestCode && resultCode == RESULT_OK) {
+ callback?.onAuthenticationSucceeded()
+ } else {
+ callback?.onAuthenticationFailed()
+ }
+ }
+
+ companion object {
+ fun isAvailable(context: Context): Boolean {
+ val manager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
+ return manager?.isKeyguardSecure == true
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/facts/AutofillFacts.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/facts/AutofillFacts.kt
new file mode 100644
index 0000000000..51a82dea07
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/facts/AutofillFacts.kt
@@ -0,0 +1,106 @@
+/* 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.autofill.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * [Fact]s emitted by the `feature-autofill` component.
+ */
+class AutofillFacts {
+ /**
+ * Items the `feature-autofill` component emits [Fact]s for.
+ */
+ object Items {
+ const val AUTOFILL_REQUEST = "autofill_request"
+ const val AUTOFILL_CONFIRMATION = "autofill_confirmation"
+ const val AUTOFILL_SEARCH = "autofill_search"
+ const val AUTOFILL_LOCK = "autofill_lock"
+ const val AUTOFILL_LOGIN_PASSWORD_DETECTED = "autofill_login_password_detected"
+ }
+
+ /**
+ * Metadata keys used by some [Fact]s emitted by the `feature-autofill` component.
+ */
+ object Metadata {
+ const val HAS_MATCHING_LOGINS = "has_matching_logins"
+ const val NEEDS_CONFIRMATION = "needs_confirmation"
+ }
+}
+
+internal fun emitLoginPasswordDetectedFact() {
+ Fact(
+ Component.FEATURE_AUTOFILL,
+ Action.INTERACTION,
+ AutofillFacts.Items.AUTOFILL_LOGIN_PASSWORD_DETECTED,
+ metadata = null,
+ ).collect()
+}
+
+internal fun emitAutofillRequestFact(
+ hasLogins: Boolean,
+ needsConfirmation: Boolean? = null,
+) {
+ Fact(
+ Component.FEATURE_AUTOFILL,
+ Action.SYSTEM,
+ AutofillFacts.Items.AUTOFILL_REQUEST,
+ metadata = requestMetadata(hasLogins, needsConfirmation),
+ ).collect()
+}
+
+internal fun emitAutofillConfirmationFact(
+ confirmed: Boolean,
+) {
+ Fact(
+ Component.FEATURE_AUTOFILL,
+ if (confirmed) { Action.CONFIRM } else { Action.CANCEL },
+ AutofillFacts.Items.AUTOFILL_CONFIRMATION,
+ ).collect()
+}
+
+internal fun emitAutofillSearchDisplayedFact() {
+ Fact(
+ Component.FEATURE_AUTOFILL,
+ Action.DISPLAY,
+ AutofillFacts.Items.AUTOFILL_SEARCH,
+ ).collect()
+}
+
+internal fun emitAutofillSearchSelectedFact() {
+ Fact(
+ Component.FEATURE_AUTOFILL,
+ Action.SELECT,
+ AutofillFacts.Items.AUTOFILL_SEARCH,
+ ).collect()
+}
+
+internal fun emitAutofillLock(
+ unlocked: Boolean,
+) {
+ Fact(
+ Component.FEATURE_AUTOFILL,
+ if (unlocked) { Action.CONFIRM } else { Action.CANCEL },
+ AutofillFacts.Items.AUTOFILL_LOCK,
+ ).collect()
+}
+
+private fun requestMetadata(
+ hasLogins: Boolean,
+ needsConfirmation: Boolean? = null,
+): Map<String, Any> {
+ val metadata = mutableMapOf<String, Any>(
+ AutofillFacts.Metadata.HAS_MATCHING_LOGINS to hasLogins,
+ )
+
+ needsConfirmation?.let {
+ metadata[AutofillFacts.Metadata.NEEDS_CONFIRMATION] = needsConfirmation
+ }
+
+ return metadata
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/handler/FillRequestHandler.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/handler/FillRequestHandler.kt
new file mode 100644
index 0000000000..154629f320
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/handler/FillRequestHandler.kt
@@ -0,0 +1,106 @@
+/* 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.autofill.handler
+
+import android.annotation.SuppressLint
+import android.app.assist.AssistStructure
+import android.content.Context
+import android.os.Build
+import android.service.autofill.FillRequest
+import android.service.autofill.FillResponse
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.facts.emitAutofillRequestFact
+import mozilla.components.feature.autofill.response.dataset.DatasetBuilder
+import mozilla.components.feature.autofill.response.dataset.LoginDatasetBuilder
+import mozilla.components.feature.autofill.response.fill.AuthFillResponseBuilder
+import mozilla.components.feature.autofill.response.fill.FillResponseBuilder
+import mozilla.components.feature.autofill.response.fill.LoginFillResponseBuilder
+import mozilla.components.feature.autofill.structure.ParsedStructure
+import mozilla.components.feature.autofill.structure.RawStructure
+import mozilla.components.feature.autofill.structure.getLookupDomain
+import mozilla.components.feature.autofill.structure.parseStructure
+import kotlin.math.min
+
+internal const val EXTRA_LOGIN_ID = "loginId"
+
+// Maximum number of logins we are going to display in the autofill overlay.
+internal const val MAX_LOGINS = 10
+
+/**
+ * Class responsible for handling [FillRequest]s and returning [FillResponse]s.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+internal class FillRequestHandler(
+ private val context: Context,
+ private val configuration: AutofillConfiguration,
+) {
+ /**
+ * Handles a fill request for the given [AssistStructure] and returns a matching [FillResponse]
+ * or `null` if the request could not be handled or the passed in [AssistStructure] is `null`.
+ */
+ @SuppressLint("InlinedApi")
+ @Suppress("ReturnCount")
+ suspend fun handle(
+ structure: RawStructure?,
+ forceUnlock: Boolean = false,
+ maxSuggestionCount: Int = MAX_LOGINS,
+ ): FillResponseBuilder? {
+ if (structure == null) {
+ return null
+ }
+
+ val parsedStructure = parseStructure(context, structure) ?: return null
+ return handle(parsedStructure, forceUnlock, maxSuggestionCount)
+ }
+
+ suspend fun handle(
+ parsedStructure: ParsedStructure,
+ forceUnlock: Boolean = false,
+ maxSuggestionCount: Int = MAX_LOGINS,
+ ): FillResponseBuilder {
+ val lookupDomain = parsedStructure.getLookupDomain(configuration.publicSuffixList)
+ val needsConfirmation = !configuration.verifier.hasCredentialRelationship(
+ context,
+ lookupDomain,
+ parsedStructure.packageName,
+ )
+
+ val logins = configuration.storage
+ .getByBaseDomain(lookupDomain)
+ .take(min(MAX_LOGINS, maxSuggestionCount))
+
+ return if (!configuration.lock.keepUnlocked() && !forceUnlock) {
+ AuthFillResponseBuilder(parsedStructure, maxSuggestionCount)
+ } else {
+ emitAutofillRequestFact(hasLogins = logins.isNotEmpty(), needsConfirmation)
+ LoginFillResponseBuilder(parsedStructure, logins, needsConfirmation)
+ }
+ }
+
+ /**
+ * Handles a fill request for the given [RawStructure] and returns only a [DatasetBuilder] for
+ * the given [loginId] - or `null` if the request could not be handled or the passed in
+ * [RawStructure] is `null`
+ */
+ @Suppress("ReturnCount")
+ suspend fun handleConfirmation(structure: RawStructure?, loginId: String): DatasetBuilder? {
+ if (structure == null) {
+ return null
+ }
+
+ val parsedStructure = parseStructure(context, structure) ?: return null
+ val lookupDomain = parsedStructure.getLookupDomain(configuration.publicSuffixList)
+
+ val logins = configuration.storage.getByBaseDomain(lookupDomain)
+ if (logins.isEmpty()) {
+ return null
+ }
+
+ val login = logins.firstOrNull { login -> login.guid == loginId } ?: return null
+
+ return LoginDatasetBuilder(parsedStructure, login, needsConfirmation = false)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/lock/AutofillLock.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/lock/AutofillLock.kt
new file mode 100644
index 0000000000..406dee8abf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/lock/AutofillLock.kt
@@ -0,0 +1,48 @@
+/* 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.autofill.lock
+
+import mozilla.components.concept.storage.LoginsStorage
+import mozilla.components.support.base.android.Clock
+
+// Time after the last unlock that will require a new unlock
+private const val AUTOLOCK_TIME = 5 * 60 * 1000
+
+/**
+ * Helper for keeping track of the lock/unlock state for autofill. The actual unlocking or
+ * decrypting of the underlying storage is done by the [LoginsStorage] implementation.
+ */
+class AutofillLock {
+ private var lastUnlockTimestmap: Long = 0
+
+ /**
+ * Checks whether the autofill lock is still unlocked and whether autofill options will be shown
+ * without authenticating again.
+ */
+ @Synchronized
+ fun isUnlocked() = lastUnlockTimestmap + AUTOLOCK_TIME >= Clock.elapsedRealtime()
+
+ /**
+ * If the autofill lock is unlocked then this will keep it unlocked by extending the time until
+ * it will automatically get locked again.
+ */
+ @Synchronized
+ fun keepUnlocked(): Boolean {
+ return if (isUnlocked()) {
+ unlock()
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Unlocks the autofill lock.
+ */
+ @Synchronized
+ fun unlock() {
+ lastUnlockTimestmap = Clock.elapsedRealtime()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/preference/AutofillPreference.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/preference/AutofillPreference.kt
new file mode 100644
index 0000000000..0d5d6913c6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/preference/AutofillPreference.kt
@@ -0,0 +1,59 @@
+/* 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.autofill.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.appcompat.widget.SwitchCompat
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import mozilla.components.feature.autofill.AutofillUseCases
+import mozilla.components.feature.autofill.R
+
+/**
+ * Preference showing a switch to enable this app as the preferred autofill service of the user.
+ *
+ * When getting enabled this preference will launch Android's system setting for selecting an
+ * autofill service.
+ */
+class AutofillPreference(
+ context: Context,
+ attrs: AttributeSet? = null,
+) : Preference(context, attrs) {
+ private val useCases = AutofillUseCases()
+ private var switchView: SwitchCompat? = null
+
+ init {
+ widgetLayoutResource = R.layout.mozac_feature_autofill_preference
+ isVisible = useCases.isSupported(context)
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ switchView = holder.findViewById(R.id.switch_widget) as SwitchCompat
+
+ update()
+ }
+
+ override fun onClick() {
+ super.onClick()
+
+ if (switchView?.isChecked == true) {
+ useCases.disable(context)
+ switchView?.isChecked = false
+ } else {
+ useCases.enable(context)
+ }
+ }
+
+ /**
+ * Updates the preference (on/off) based on whether this app is set as the user's autofill
+ * service.
+ */
+ fun update() {
+ switchView?.isChecked = useCases.isEnabled(context)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/DatasetBuilder.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/DatasetBuilder.kt
new file mode 100644
index 0000000000..ff236c6858
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/DatasetBuilder.kt
@@ -0,0 +1,18 @@
+/* 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.autofill.response.dataset
+
+import android.content.Context
+import android.service.autofill.Dataset
+import android.widget.inline.InlinePresentationSpec
+import mozilla.components.feature.autofill.AutofillConfiguration
+
+internal interface DatasetBuilder {
+ fun build(
+ context: Context,
+ configuration: AutofillConfiguration,
+ imeSpec: InlinePresentationSpec? = null,
+ ): Dataset
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/LoginDatasetBuilder.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/LoginDatasetBuilder.kt
new file mode 100644
index 0000000000..24fac38825
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/LoginDatasetBuilder.kt
@@ -0,0 +1,206 @@
+/* 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.autofill.response.dataset
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.graphics.BlendMode
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.service.autofill.Dataset
+import android.service.autofill.Field
+import android.service.autofill.InlinePresentation
+import android.service.autofill.Presentations
+import android.text.TextUtils
+import android.view.autofill.AutofillId
+import android.view.autofill.AutofillValue
+import android.widget.RemoteViews
+import android.widget.inline.InlinePresentationSpec
+import androidx.annotation.RequiresApi
+import androidx.autofill.inline.UiVersions
+import androidx.autofill.inline.v1.InlineSuggestionUi
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.handler.EXTRA_LOGIN_ID
+import mozilla.components.feature.autofill.structure.ParsedStructure
+import mozilla.components.support.utils.PendingIntentUtils
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal data class LoginDatasetBuilder(
+ val parsedStructure: ParsedStructure,
+ val login: Login,
+ val needsConfirmation: Boolean,
+ val requestOffset: Int = 0,
+) : DatasetBuilder {
+
+ @SuppressLint("NewApi")
+ override fun build(
+ context: Context,
+ configuration: AutofillConfiguration,
+ imeSpec: InlinePresentationSpec?,
+ ): Dataset {
+ val dataset = Dataset.Builder()
+
+ val attributionIntent = Intent().apply {
+ `package` = context.packageName
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ attributionIntent,
+ PendingIntentUtils.defaultFlags or PendingIntent.FLAG_CANCEL_CURRENT,
+ )
+
+ val usernameText = login.usernamePresentationOrFallback(context)
+ val passwordText = login.passwordPresentation(context)
+
+ val usernamePresentation = createViewPresentation(context, usernameText)
+ val passwordPresentation = createViewPresentation(context, passwordText)
+
+ val usernameInlinePresentation = createInlinePresentation(pendingIntent, imeSpec, usernameText)
+ val passwordInlinePresentation = createInlinePresentation(pendingIntent, imeSpec, passwordText)
+
+ parsedStructure.usernameId?.let { id ->
+ dataset.setValue(
+ id,
+ if (needsConfirmation) null else AutofillValue.forText(login.username),
+ usernamePresentation,
+ usernameInlinePresentation,
+ )
+ }
+
+ parsedStructure.passwordId?.let { id ->
+ dataset.setValue(
+ id,
+ if (needsConfirmation) null else AutofillValue.forText(login.password),
+ passwordPresentation,
+ passwordInlinePresentation,
+ )
+ }
+
+ if (needsConfirmation) {
+ val confirmIntent = Intent(context, configuration.confirmActivity)
+ confirmIntent.putExtra(EXTRA_LOGIN_ID, login.guid)
+
+ val intentSender: IntentSender = PendingIntent.getActivity(
+ context,
+ configuration.activityRequestCode + requestOffset,
+ confirmIntent,
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT,
+ ).intentSender
+
+ dataset.setAuthentication(intentSender)
+ }
+
+ return dataset.build()
+ }
+}
+
+internal fun Login.usernamePresentationOrFallback(context: Context): String {
+ return username.ifEmpty {
+ context.getString(mozilla.components.feature.autofill.R.string.mozac_feature_autofill_popup_no_username)
+ }
+}
+
+private fun Login.passwordPresentation(context: Context): String {
+ return context.getString(
+ mozilla.components.feature.autofill.R.string.mozac_feature_autofill_popup_password,
+ usernamePresentationOrFallback(context),
+ )
+}
+
+internal fun createViewPresentation(context: Context, title: String): RemoteViews {
+ val viewPresentation = RemoteViews(context.packageName, android.R.layout.simple_list_item_1)
+ viewPresentation.setTextViewText(android.R.id.text1, title)
+
+ return viewPresentation
+}
+
+internal fun createInlinePresentation(
+ pendingIntent: PendingIntent,
+ imeSpec: InlinePresentationSpec?,
+ title: String,
+ icon: Icon? = null,
+): InlinePresentation? {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && imeSpec != null &&
+ canUseInlineSuggestions(imeSpec)
+ ) {
+ return InlinePresentation(
+ createSlice(title, attribution = pendingIntent, startIcon = icon),
+ imeSpec,
+ false,
+ )
+ }
+ return null
+}
+
+@SuppressLint("RestrictedApi")
+@RequiresApi(Build.VERSION_CODES.R)
+internal fun createSlice(
+ title: CharSequence,
+ subtitle: CharSequence = "",
+ startIcon: Icon? = null,
+ endIcon: Icon? = null,
+ contentDescription: CharSequence = "",
+ attribution: PendingIntent,
+): Slice {
+ // Build the content for the v1 UI.
+ val builder = InlineSuggestionUi.newContentBuilder(attribution)
+ .setContentDescription(contentDescription)
+ if (!TextUtils.isEmpty(title)) {
+ builder.setTitle(title)
+ }
+ if (!TextUtils.isEmpty(subtitle)) {
+ builder.setSubtitle(subtitle)
+ }
+ if (startIcon != null) {
+ startIcon.setTintBlendMode(BlendMode.DST)
+ builder.setStartIcon(startIcon)
+ }
+ if (endIcon != null) {
+ builder.setEndIcon(endIcon)
+ }
+ return builder.build().slice
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+internal fun canUseInlineSuggestions(imeSpec: InlinePresentationSpec): Boolean {
+ return UiVersions.getVersions(imeSpec.style).contains(UiVersions.INLINE_UI_VERSION_1)
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun Dataset.Builder.setValue(
+ id: AutofillId,
+ value: AutofillValue?,
+ presentation: RemoteViews,
+ inlinePresentation: InlinePresentation? = null,
+): Dataset.Builder {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val fieldBuilder: Field.Builder = Field.Builder()
+ if (value != null) {
+ fieldBuilder.setValue(value)
+ }
+ val presentationsBuilder = Presentations.Builder()
+ presentationsBuilder.setMenuPresentation(presentation)
+
+ if (inlinePresentation != null) {
+ presentationsBuilder.setInlinePresentation(inlinePresentation)
+ }
+
+ fieldBuilder.setPresentations(presentationsBuilder.build())
+ this.setField(id, fieldBuilder.build())
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && inlinePresentation != null) {
+ @Suppress("DEPRECATION")
+ setValue(id, value, presentation, inlinePresentation)
+ } else {
+ @Suppress("DEPRECATION")
+ setValue(id, value, presentation)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/SearchDatasetBuilder.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/SearchDatasetBuilder.kt
new file mode 100644
index 0000000000..f3eefd165c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/dataset/SearchDatasetBuilder.kt
@@ -0,0 +1,76 @@
+/* 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.autofill.response.dataset
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.os.Build
+import android.service.autofill.Dataset
+import android.widget.inline.InlinePresentationSpec
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.R
+import mozilla.components.feature.autofill.handler.MAX_LOGINS
+import mozilla.components.feature.autofill.structure.ParsedStructure
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal data class SearchDatasetBuilder(
+ val parsedStructure: ParsedStructure,
+) : DatasetBuilder {
+
+ @SuppressLint("NewApi")
+ override fun build(
+ context: Context,
+ configuration: AutofillConfiguration,
+ imeSpec: InlinePresentationSpec?,
+ ): Dataset {
+ val dataset = Dataset.Builder()
+
+ val searchIntent = Intent(context, configuration.searchActivity)
+ val searchPendingIntent = PendingIntent.getActivity(
+ context,
+ configuration.activityRequestCode + MAX_LOGINS,
+ searchIntent,
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT,
+ )
+ val intentSender: IntentSender = searchPendingIntent.intentSender
+
+ val title = context.getString(
+ R.string.mozac_feature_autofill_search_suggestions,
+ configuration.applicationName,
+ )
+
+ val usernamePresentation = createViewPresentation(context, title)
+ val passwordPresentation = createViewPresentation(context, title)
+
+ val usernameInlinePresentation = createInlinePresentation(searchPendingIntent, imeSpec, title)
+ val passwordInlinePresentation = createInlinePresentation(searchPendingIntent, imeSpec, title)
+
+ parsedStructure.usernameId?.let { id ->
+ dataset.setValue(
+ id,
+ null,
+ usernamePresentation,
+ usernameInlinePresentation,
+ )
+ }
+
+ parsedStructure.passwordId?.let { id ->
+ dataset.setValue(
+ id,
+ null,
+ passwordPresentation,
+ passwordInlinePresentation,
+ )
+ }
+
+ dataset.setAuthentication(intentSender)
+
+ return dataset.build()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/AuthFillResponseBuilder.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/AuthFillResponseBuilder.kt
new file mode 100644
index 0000000000..c65ddac63e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/AuthFillResponseBuilder.kt
@@ -0,0 +1,125 @@
+/* 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.autofill.response.fill
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.Parcel
+import android.service.autofill.FillResponse
+import android.service.autofill.InlinePresentation
+import android.service.autofill.Presentations
+import android.view.autofill.AutofillId
+import android.widget.RemoteViews
+import android.widget.inline.InlinePresentationSpec
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.R
+import mozilla.components.feature.autofill.response.dataset.createInlinePresentation
+import mozilla.components.feature.autofill.structure.ParsedStructure
+import mozilla.components.feature.autofill.ui.AbstractAutofillUnlockActivity
+import androidx.biometric.R as biometricR
+
+internal data class AuthFillResponseBuilder(
+ private val parsedStructure: ParsedStructure,
+ private val maxSuggestionCount: Int,
+) : FillResponseBuilder {
+
+ @SuppressLint("NewApi")
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun build(
+ context: Context,
+ configuration: AutofillConfiguration,
+ imeSpec: InlinePresentationSpec?,
+ ): FillResponse {
+ val builder = FillResponse.Builder()
+
+ val autofillIds = listOfNotNull(parsedStructure.usernameId, parsedStructure.passwordId)
+
+ val title = context.getString(
+ R.string.mozac_feature_autofill_popup_unlock_application,
+ configuration.applicationName,
+ )
+
+ val authPresentation = RemoteViews(context.packageName, android.R.layout.simple_list_item_1).apply {
+ setTextViewText(
+ android.R.id.text1,
+ title,
+ )
+ }
+
+ val authIntent = Intent(context, configuration.unlockActivity)
+
+ // Pass `ParsedStructure` as raw bytes to prevent the system throwing a ClassNotFoundException
+ // when updating the PendingIntent and trying to create and remap `ParsedStructure`
+ // from the parcelable extra because of an unknown ClassLoader.
+ with(Parcel.obtain()) {
+ parsedStructure.writeToParcel(this, 0)
+
+ authIntent.putExtra(
+ AbstractAutofillUnlockActivity.EXTRA_PARSED_STRUCTURE,
+ this.marshall(),
+ )
+
+ recycle()
+ }
+
+ authIntent.putExtra(AbstractAutofillUnlockActivity.EXTRA_IME_SPEC, imeSpec)
+ authIntent.putExtra(
+ AbstractAutofillUnlockActivity.EXTRA_MAX_SUGGESTION_COUNT,
+ maxSuggestionCount,
+ )
+ val authPendingIntent = PendingIntent.getActivity(
+ context,
+ configuration.activityRequestCode,
+ authIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE,
+ )
+ val intentSender: IntentSender = authPendingIntent.intentSender
+
+ val icon: Icon = Icon.createWithResource(
+ context,
+ biometricR.drawable.fingerprint_dialog_fp_icon,
+ )
+ val authInlinePresentation = createInlinePresentation(authPendingIntent, imeSpec, title, icon)
+ builder.setAuthentication(
+ autofillIds.toTypedArray(),
+ intentSender,
+ authInlinePresentation,
+ authPresentation,
+ )
+
+ return builder.build()
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun FillResponse.Builder.setAuthentication(
+ ids: Array<AutofillId>,
+ authentication: IntentSender,
+ inlinePresentation: InlinePresentation? = null,
+ presentation: RemoteViews,
+): FillResponse.Builder {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val presentations: Presentations.Builder = Presentations.Builder()
+ presentations.apply {
+ inlinePresentation?.let {
+ setInlinePresentation(it)
+ }
+ setMenuPresentation(presentation)
+ }
+ setAuthentication(ids, authentication, presentations.build())
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ @Suppress("DEPRECATION")
+ setAuthentication(ids, authentication, presentation, inlinePresentation)
+ } else {
+ @Suppress("DEPRECATION")
+ setAuthentication(ids, authentication, presentation)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/FillResponseBuilder.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/FillResponseBuilder.kt
new file mode 100644
index 0000000000..28d28011eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/FillResponseBuilder.kt
@@ -0,0 +1,18 @@
+/* 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.autofill.response.fill
+
+import android.content.Context
+import android.service.autofill.FillResponse
+import android.widget.inline.InlinePresentationSpec
+import mozilla.components.feature.autofill.AutofillConfiguration
+
+internal interface FillResponseBuilder {
+ fun build(
+ context: Context,
+ configuration: AutofillConfiguration,
+ imeSpec: InlinePresentationSpec? = null,
+ ): FillResponse
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/LoginFillResponseBuilder.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/LoginFillResponseBuilder.kt
new file mode 100644
index 0000000000..27730c9622
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/LoginFillResponseBuilder.kt
@@ -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/. */
+
+package mozilla.components.feature.autofill.response.fill
+
+import android.content.Context
+import android.os.Build
+import android.service.autofill.FillResponse
+import android.widget.inline.InlinePresentationSpec
+import androidx.annotation.RequiresApi
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.response.dataset.LoginDatasetBuilder
+import mozilla.components.feature.autofill.response.dataset.SearchDatasetBuilder
+import mozilla.components.feature.autofill.structure.ParsedStructure
+
+/**
+ * [FillResponseBuilder] implementation that creates a [FillResponse] containing logins for
+ * autofilling.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+internal data class LoginFillResponseBuilder(
+ val parsedStructure: ParsedStructure,
+ val logins: List<Login>,
+ val needsConfirmation: Boolean,
+) : FillResponseBuilder {
+ private val searchDatasetBuilder = SearchDatasetBuilder(parsedStructure)
+
+ override fun build(
+ context: Context,
+ configuration: AutofillConfiguration,
+ imeSpec: InlinePresentationSpec?,
+ ): FillResponse {
+ val builder = FillResponse.Builder()
+
+ logins.forEachIndexed { index, login ->
+ val datasetBuilder = LoginDatasetBuilder(
+ parsedStructure,
+ login,
+ needsConfirmation,
+ requestOffset = index,
+ )
+
+ val dataset = datasetBuilder.build(
+ context,
+ configuration,
+ imeSpec,
+ )
+
+ builder.addDataset(dataset)
+ }
+
+ builder.addDataset(
+ searchDatasetBuilder.build(context, configuration, imeSpec),
+ )
+
+ return builder.build()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructure.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructure.kt
new file mode 100644
index 0000000000..ebf30869c8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructure.kt
@@ -0,0 +1,110 @@
+/* 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.autofill.structure
+
+import android.content.Context
+import android.os.Build
+import android.os.Parcel
+import android.os.Parcelable
+import android.os.Parcelable.Creator
+import android.view.autofill.AutofillId
+import androidx.annotation.RequiresApi
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.utils.Browsers
+
+/**
+ * Parsed structure from an autofill request.
+ *
+ * Originally implemented in Lockwise:
+ * https://github.com/mozilla-lockwise/lockwise-android/blob/d3c0511f73c34e8759e1bb597f2d3dc9bcc146f0/app/src/main/java/mozilla/lockbox/autofill/ParsedStructure.kt#L52
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+data class ParsedStructure(
+ val usernameId: AutofillId? = null,
+ val passwordId: AutofillId? = null,
+ val webDomain: String? = null,
+ val packageName: String,
+) : Parcelable {
+ constructor(parcel: Parcel) : this(
+ parcel.readParcelableCompat(AutofillId::class.java),
+ parcel.readParcelableCompat(AutofillId::class.java),
+ parcel.readString(),
+ parcel.readString() ?: "",
+ )
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeParcelable(usernameId, flags)
+ parcel.writeParcelable(passwordId, flags)
+ parcel.writeString(webDomain)
+ parcel.writeString(packageName)
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ /**
+ * Create instances of [ParsedStructure] from a [Parcel].
+ */
+ companion object CREATOR : Creator<ParsedStructure> {
+ override fun createFromParcel(parcel: Parcel): ParsedStructure {
+ return ParsedStructure(parcel)
+ }
+
+ override fun newArray(size: Int): Array<ParsedStructure?> {
+ return arrayOfNulls(size)
+ }
+ }
+}
+
+/**
+ * Try to find a domain in the [ParsedStructure] for looking up logins. This is either a "web domain"
+ * for web content the third-party app is displaying (e.g. in a WebView) or the package name of the
+ * application transformed into a domain. In any case the [publicSuffixList] will be used to turn
+ * the domain into a "base" domain (public suffix + 1) before returning.
+ */
+internal suspend fun ParsedStructure.getLookupDomain(publicSuffixList: PublicSuffixList): String {
+ println("Lookup: webDomain=$webDomain, packageName=$packageName")
+ val domain = if (webDomain != null && Browsers.isBrowser(packageName)) {
+ // If the application we are auto-filling is a known browser and it provided a webDomain
+ // for the content it is displaying then we try to autofill for that.
+ webDomain
+ } else {
+ // We reverse the package name in the hope that this will resemble a domain name. This is
+ // of course fragile. So we want to find better mechanisms in the future (e.g. looking up
+ // what URLs the application registers intent handlers for).
+ packageName.split('.').asReversed().joinToString(".")
+ }
+
+ return publicSuffixList.getPublicSuffixPlusOne(domain).await() ?: domain
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun parseStructure(context: Context, structure: RawStructure): ParsedStructure? {
+ val activityPackageName = structure.activityPackageName
+ if (context.packageName == activityPackageName) {
+ // We do not autofill our own activities. Browser content will be auto-filled by Gecko.
+ return null
+ }
+
+ val nodeNavigator = structure.createNavigator()
+ val parsedStructure = ParsedStructureBuilder(nodeNavigator).build()
+
+ if (parsedStructure.passwordId == null && parsedStructure.usernameId == null) {
+ // If we didn't find any password or username fields then there's nothing to autofill for us.
+ return null
+ }
+
+ return parsedStructure
+}
+
+internal fun <T> Parcel.readParcelableCompat(clazz: Class<T>): T? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ readParcelable(clazz.classLoader, clazz)
+ } else {
+ @Suppress("DEPRECATION")
+ readParcelable(clazz.classLoader)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructureBuilder.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructureBuilder.kt
new file mode 100644
index 0000000000..d6e90f3d05
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructureBuilder.kt
@@ -0,0 +1,229 @@
+/* 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.autofill.structure
+
+import android.os.Build
+import android.view.View
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal class ParsedStructureBuilder<ViewNode, AutofillId>(
+ private val navigator: AutofillNodeNavigator<ViewNode, AutofillId>,
+) {
+ fun build(): ParsedStructure {
+ val formNode = findFocusedForm()
+ val (usernameId, passwordId) = findAutofillIds(formNode)
+ val hostnameClue = usernameId ?: passwordId
+
+ return navigator.build(
+ usernameId,
+ passwordId,
+ getWebDomain(hostnameClue),
+ getPackageName(hostnameClue) ?: navigator.activityPackageName,
+ )
+ }
+
+ private fun findFocusedForm(): ViewNode? {
+ val focusPath = findMatchedNodeAncestors {
+ navigator.isFocused(it)
+ }
+
+ return focusPath?.lastOrNull {
+ navigator.isHtmlForm(it)
+ }
+ }
+
+ private fun findAutofillIds(rootNode: ViewNode?): Pair<AutofillId?, AutofillId?> =
+ checkForAdjacentFields(rootNode) ?: getUsernameId(rootNode) to getPasswordId(rootNode)
+
+ private fun getUsernameId(rootNode: ViewNode?): AutofillId? {
+ // how do we localize the "email" and "username"?
+ return getAutofillIdForKeywords(
+ rootNode,
+ listOf(
+ View.AUTOFILL_HINT_USERNAME,
+ View.AUTOFILL_HINT_EMAIL_ADDRESS,
+ "email",
+ "username",
+ "user name",
+ "identifier",
+ "account_name",
+ ),
+ )
+ }
+
+ private fun getPasswordId(rootNode: ViewNode?): AutofillId? {
+ // similar l10n question for password
+ return getAutofillIdForKeywords(rootNode, listOf(View.AUTOFILL_HINT_PASSWORD, "password"))
+ }
+
+ private fun getAutofillIdForKeywords(rootNode: ViewNode?, keywords: Collection<String>): AutofillId? {
+ return checkForNamedTextField(rootNode, keywords)
+ ?: checkForConsecutiveLabelAndField(rootNode, keywords)
+ ?: checkForNestedLayoutAndField(rootNode, keywords)
+ }
+
+ private fun checkForNamedTextField(rootNode: ViewNode?, keywords: Collection<String>): AutofillId? {
+ return navigator.findFirst(rootNode) { node: ViewNode ->
+ if (isAutoFillableEditText(node, keywords) || isAutoFillableInputField(node, keywords)) {
+ navigator.autofillId(node)
+ } else {
+ null
+ }
+ }
+ }
+
+ private fun checkForConsecutiveLabelAndField(rootNode: ViewNode?, keywords: Collection<String>): AutofillId? {
+ return navigator.findFirst(rootNode) { node: ViewNode ->
+ val childNodes = navigator.childNodes(node)
+ // check for consecutive views with keywords followed by possible fill locations
+ for (i in 1.until(childNodes.size)) {
+ val prevNode = childNodes[i - 1]
+ val currentNode = childNodes[i]
+ val id = navigator.autofillId(currentNode) ?: continue
+ if (
+ (navigator.isEditText(currentNode) || navigator.isHtmlInputField(currentNode)) &&
+ containsKeywords(prevNode, keywords)
+ ) {
+ return@findFirst id
+ }
+ }
+ null
+ }
+ }
+
+ private fun checkForNestedLayoutAndField(rootNode: ViewNode?, keywords: Collection<String>): AutofillId? {
+ return navigator.findFirst(rootNode) { node: ViewNode ->
+ val childNodes = navigator.childNodes(node)
+
+ if (childNodes.size != 1) {
+ return@findFirst null
+ }
+
+ val child = childNodes[0]
+ val id = navigator.autofillId(child) ?: return@findFirst null
+ if (
+ (navigator.isEditText(child) || navigator.isHtmlInputField(child)) &&
+ containsKeywords(node, keywords)
+ ) {
+ return@findFirst id
+ }
+ null
+ }
+ }
+
+ private fun checkForAdjacentFields(rootNode: ViewNode?): Pair<AutofillId?, AutofillId?>? {
+ return navigator.findFirst(rootNode) { node: ViewNode ->
+
+ val childNodes = navigator.childNodes(node)
+ // XXX we only look at the list of edit texts before the first button.
+ // This is because we can see the invisible fields, but not that they are
+ // invisible. https://bugzilla.mozilla.org/show_bug.cgi?id=1592047
+ val firstButtonIndex = childNodes.indexOfFirst { navigator.isButton(it) }
+
+ val firstFewNodes = if (firstButtonIndex >= 0) {
+ childNodes.subList(0, firstButtonIndex)
+ } else {
+ childNodes
+ }
+
+ val inputFields = firstFewNodes.filter {
+ navigator.isEditText(it) && navigator.autofillId(it) != null && navigator.isVisible(it)
+ }
+
+ // we must have a minimum of two EditText boxes in order to have a pair.
+ if (inputFields.size < 2) {
+ return@findFirst null
+ }
+
+ for (i in 1.until(inputFields.size)) {
+ val prevNode = inputFields[i - 1]
+ val currentNode = inputFields[i]
+ if (navigator.isPasswordField(currentNode) && navigator.isPasswordField(prevNode).not()) {
+ return@findFirst navigator.autofillId(prevNode) to navigator.autofillId(currentNode)
+ }
+ }
+
+ null
+ }
+ }
+
+ private fun getWebDomain(nearby: AutofillId?): String? {
+ return nearestFocusedNode(nearby) {
+ navigator.webDomain(it)
+ }
+ }
+
+ private fun getPackageName(nearby: AutofillId?): String? {
+ return nearestFocusedNode(nearby) {
+ navigator.packageName(it)
+ }
+ }
+
+ private fun <T> nearestFocusedNode(nearby: AutofillId?, transform: (ViewNode) -> T?): T? {
+ val id = nearby ?: return null
+ val ancestors = findMatchedNodeAncestors {
+ navigator.autofillId(it) == id
+ }
+ return ancestors?.map(transform)?.firstOrNull { it != null }
+ }
+
+ private fun isAutoFillableEditText(node: ViewNode, keywords: Collection<String>): Boolean {
+ return navigator.isEditText(node) &&
+ containsKeywords(node, keywords) &&
+ navigator.autofillId(node) != null
+ }
+
+ private fun isAutoFillableInputField(node: ViewNode, keywords: Collection<String>): Boolean {
+ return navigator.isHtmlInputField(node) &&
+ containsKeywords(node, keywords) &&
+ navigator.autofillId(node) != null
+ }
+
+ private fun containsKeywords(node: ViewNode, keywords: Collection<String>): Boolean {
+ val hints = navigator.clues(node)
+ keywords.forEach { keyword ->
+ hints.forEach { hint ->
+ if (hint.contains(keyword, true)) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ private fun findMatchedNodeAncestors(matcher: (ViewNode) -> Boolean): Iterable<ViewNode>? {
+ navigator.rootNodes
+ .forEach { node ->
+ findMatchedNodeAncestors(node, matcher)?.let { result ->
+ return result
+ }
+ }
+ return null
+ }
+
+ /**
+ * Depth first search a ViewNode tree. Once a match is found, a list of ancestors all the way to
+ * the top is returned. The first node in the list is the matching node, the last is the root node.
+ * If no match is found, then <code>null</code> is returned.
+ *
+ * @param node the parent node.
+ * @param matcher a closure which returns <code>true</code> if and only if the node is matched.
+ * @return an ordered list of the matched node and all its ancestors starting at the matched node.
+ */
+ private fun findMatchedNodeAncestors(node: ViewNode, matcher: (ViewNode) -> Boolean): Iterable<ViewNode>? {
+ if (matcher(node)) {
+ return listOf(node)
+ }
+
+ navigator.childNodes(node)
+ .forEach { child ->
+ findMatchedNodeAncestors(child, matcher)?.let { list ->
+ return list + node
+ }
+ }
+ return null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/RawStructure.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/RawStructure.kt
new file mode 100644
index 0000000000..1ac3fdfc21
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/RawStructure.kt
@@ -0,0 +1,36 @@
+/* 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.autofill.structure
+
+import android.app.assist.AssistStructure
+import android.os.Build
+import android.view.autofill.AutofillId
+import androidx.annotation.RequiresApi
+
+/**
+ * A raw view structure provided by an application - to be parsed into a [ParsedStructure].
+ */
+internal interface RawStructure {
+ val activityPackageName: String
+
+ fun createNavigator(): AutofillNodeNavigator<*, AutofillId>
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun AssistStructure.toRawStructure(): RawStructure {
+ return AssistStructureWrapper(this)
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+private class AssistStructureWrapper(
+ private val actual: AssistStructure,
+) : RawStructure {
+ override val activityPackageName: String
+ get() = actual.activityComponent.packageName
+
+ override fun createNavigator(): AutofillNodeNavigator<*, AutofillId> {
+ return ViewNodeNavigator(actual, activityPackageName)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ViewNodeNavigator.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ViewNodeNavigator.kt
new file mode 100644
index 0000000000..5ed794e298
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ViewNodeNavigator.kt
@@ -0,0 +1,186 @@
+/* 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.autofill.structure
+
+import android.app.assist.AssistStructure
+import android.app.assist.AssistStructure.ViewNode
+import android.os.Build
+import android.text.InputType
+import android.view.View
+import android.view.autofill.AutofillId
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.structure.AutofillNodeNavigator.Companion.editTextMask
+import java.util.Locale
+
+/**
+ * Helper for navigating autofill nodes.
+ *
+ * Original implementation imported from Lockwise:
+ * https://github.com/mozilla-lockwise/lockwise-android/blob/f303f8aee7cc96dcdf4e7863fef6c19ae874032e/app/src/main/java/mozilla/lockbox/autofill/ViewNodeNavigator.kt#L13
+ */
+internal interface AutofillNodeNavigator<Node, Id> {
+ companion object {
+ val editTextMask = InputType.TYPE_CLASS_TEXT
+ val passwordMask =
+ InputType.TYPE_TEXT_VARIATION_PASSWORD or
+ InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+ }
+
+ val rootNodes: List<Node>
+ val activityPackageName: String
+ fun childNodes(node: Node): List<Node>
+ fun clues(node: Node): Iterable<CharSequence>
+ fun autofillId(node: Node): Id?
+ fun isEditText(node: Node): Boolean
+ fun isHtmlInputField(node: Node): Boolean
+ fun isHtmlForm(node: Node): Boolean
+ fun packageName(node: Node): String?
+ fun webDomain(node: Node): String?
+ fun currentText(node: Node): String?
+ fun inputType(node: Node): Int
+ fun isPasswordField(node: Node): Boolean = (inputType(node) and passwordMask) > 0
+ fun isButton(node: Node): Boolean
+ fun isFocused(node: Node): Boolean
+ fun isVisible(node: Node): Boolean
+ fun build(
+ usernameId: Id?,
+ passwordId: Id?,
+ webDomain: String?,
+ packageName: String,
+ ): ParsedStructure
+
+ private fun <T> findFirstRoots(transform: (Node) -> T?): T? {
+ rootNodes
+ .forEach { node ->
+ findFirst(node, transform)?.let { result ->
+ return result
+ }
+ }
+ return null
+ }
+
+ @Suppress("ReturnCount")
+ fun <T> findFirst(rootNode: Node? = null, transform: (Node) -> T?): T? {
+ val node = rootNode ?: return findFirstRoots(transform)
+
+ transform(node)?.let {
+ return it
+ }
+
+ childNodes(node)
+ .forEach { child ->
+ findFirst(child, transform)?.let { result ->
+ return result
+ }
+ }
+ return null
+ }
+}
+
+/**
+ * Helper for navigating autofill nodes.
+ *
+ * Original implementation imported from Lockwise:
+ * https://github.com/mozilla-lockwise/lockwise-android/blob/f303f8aee7cc96dcdf4e7863fef6c19ae874032e/app/src/main/java/mozilla/lockbox/autofill/ViewNodeNavigator.kt#L72
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+internal class ViewNodeNavigator(
+ private val structure: AssistStructure,
+ override val activityPackageName: String,
+) : AutofillNodeNavigator<ViewNode, AutofillId> {
+ override val rootNodes: List<ViewNode>
+ get() = structure.run { (0 until windowNodeCount).map { getWindowNodeAt(it).rootViewNode } }
+
+ override fun childNodes(node: ViewNode): List<ViewNode> =
+ node.run { (0 until childCount) }.map { node.getChildAt(it) }
+
+ override fun clues(node: ViewNode): Iterable<CharSequence> {
+ val hints = mutableListOf<CharSequence?>(
+ node.text,
+ node.idEntry,
+ node.hint, // This is localized.
+ )
+
+ node.autofillOptions?.let {
+ hints.addAll(it)
+ }
+
+ node.autofillHints?.let {
+ hints.addAll(it)
+ }
+
+ node.htmlInfo?.attributes?.let { attrs ->
+ hints.addAll(attrs.map { it.second })
+ }
+
+ return hints.filterNotNull()
+ }
+
+ override fun autofillId(node: ViewNode): AutofillId? = node.autofillId
+
+ override fun isEditText(node: ViewNode) =
+ inputType(node) and editTextMask > 0
+
+ override fun inputType(node: ViewNode) = node.inputType
+
+ override fun isHtmlInputField(node: ViewNode) =
+ htmlTagName(node) == "input"
+
+ private fun htmlAttr(node: ViewNode, name: String) =
+ node.htmlInfo?.attributes?.find { name == it.first }?.second
+
+ @Suppress("ReturnCount")
+ override fun isButton(node: ViewNode): Boolean {
+ val className = node.className ?: ""
+ when {
+ className.contains("Button") -> return true
+ htmlTagName(node) == "button" -> return true
+ htmlTagName(node) != "input" -> return false
+ }
+
+ return when (htmlAttr(node, "type")) {
+ "submit" -> true
+ "button" -> true
+ else -> false
+ }
+ }
+
+ private fun htmlTagName(node: ViewNode) =
+ // Use English locale, as the HTML tags are all in English.
+ node.htmlInfo?.tag?.lowercase(Locale.ENGLISH)
+
+ override fun isHtmlForm(node: ViewNode) =
+ htmlTagName(node) == "form"
+
+ override fun isVisible(node: ViewNode) = node.visibility == View.VISIBLE
+
+ override fun packageName(node: ViewNode): String? = node.idPackage
+
+ override fun webDomain(node: ViewNode): String? = node.webDomain
+
+ override fun currentText(node: ViewNode): String? {
+ return if (node.autofillValue?.isText == true) {
+ node.autofillValue?.textValue.toString()
+ } else {
+ null
+ }
+ }
+
+ override fun isFocused(node: ViewNode) = node.isFocused
+
+ override fun build(
+ usernameId: AutofillId?,
+ passwordId: AutofillId?,
+ webDomain: String?,
+ packageName: String,
+ ): ParsedStructure {
+ return ParsedStructure(
+ usernameId,
+ passwordId,
+ webDomain,
+ packageName,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillConfirmActivity.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillConfirmActivity.kt
new file mode 100644
index 0000000000..d32e1a8e4e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillConfirmActivity.kt
@@ -0,0 +1,139 @@
+/* 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.autofill.ui
+
+import android.app.Dialog
+import android.app.assist.AssistStructure
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.service.autofill.Dataset
+import android.view.autofill.AutofillManager
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.R
+import mozilla.components.feature.autofill.facts.emitAutofillConfirmationFact
+import mozilla.components.feature.autofill.handler.EXTRA_LOGIN_ID
+import mozilla.components.feature.autofill.handler.FillRequestHandler
+import mozilla.components.feature.autofill.structure.toRawStructure
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+/**
+ * Activity responsible for asking the user to confirm before autofilling a third-party app. It is
+ * shown in situations where the authenticity of an application could not be confirmed automatically
+ * with "Digital Asset Links".
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+abstract class AbstractAutofillConfirmActivity : FragmentActivity() {
+ abstract val configuration: AutofillConfiguration
+
+ private var dataset: Deferred<Dataset?>? = null
+ private val fillHandler by lazy { FillRequestHandler(context = this, configuration) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val structure: AssistStructure? = intent.getParcelableExtraCompat(
+ AutofillManager.EXTRA_ASSIST_STRUCTURE,
+ AssistStructure::class.java,
+ )
+ val loginId = intent.getStringExtra(EXTRA_LOGIN_ID)
+ if (loginId == null) {
+ cancel()
+ return
+ }
+ val imeSpec = intent.getImeSpec()
+ // While the user is asked to confirm, we already try to build the fill response asynchronously.
+ val rawStructure = structure?.toRawStructure()
+ if (rawStructure != null) {
+ dataset = lifecycleScope.async(Dispatchers.IO) {
+ val builder = fillHandler.handleConfirmation(rawStructure, loginId)
+ builder?.build(this@AbstractAutofillConfirmActivity, configuration, imeSpec)
+ }
+ }
+
+ if (savedInstanceState == null) {
+ val fragment = AutofillConfirmFragment()
+ fragment.show(supportFragmentManager, "confirm_fragment")
+ }
+ }
+
+ /**
+ * Confirms the autofill request and returns the credentials to the autofill framework.
+ */
+ internal fun confirm() {
+ val replyIntent = Intent().apply {
+ // At this point it should be safe to block since the fill response should be ready once
+ // the user has authenticated.
+ runBlocking { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset?.await()) }
+ }
+
+ emitAutofillConfirmationFact(confirmed = true)
+
+ setResult(RESULT_OK, replyIntent)
+ finish()
+ }
+
+ /**
+ * Cancels the autofill request.
+ */
+ internal fun cancel() {
+ dataset?.cancel()
+
+ emitAutofillConfirmationFact(confirmed = false)
+
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal class AutofillConfirmFragment : DialogFragment() {
+ private val configuration: AutofillConfiguration
+ get() = getConfirmActivity().configuration
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return AlertDialog.Builder(requireContext())
+ .setTitle(
+ getString(R.string.mozac_feature_autofill_confirmation_title),
+ )
+ .setMessage(
+ getString(R.string.mozac_feature_autofill_confirmation_authenticity, configuration.applicationName),
+ )
+ .setPositiveButton(R.string.mozac_feature_autofill_confirmation_yes) { _, _ -> confirmRequest() }
+ .setNegativeButton(R.string.mozac_feature_autofill_confirmation_no) { _, _ -> cancelRequest() }
+ .create()
+ .withCenterAlignedButtons()
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ super.onDismiss(dialog)
+ cancelRequest()
+ }
+
+ private fun confirmRequest() {
+ getConfirmActivity()
+ .confirm()
+ }
+
+ private fun cancelRequest() {
+ getConfirmActivity()
+ .cancel()
+ }
+
+ private fun getConfirmActivity(): AbstractAutofillConfirmActivity {
+ return requireActivity() as AbstractAutofillConfirmActivity
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillSearchActivity.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillSearchActivity.kt
new file mode 100644
index 0000000000..88f5c461a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillSearchActivity.kt
@@ -0,0 +1,144 @@
+/* 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.autofill.ui
+
+import android.app.assist.AssistStructure
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.view.autofill.AutofillManager
+import android.widget.EditText
+import android.widget.inline.InlinePresentationSpec
+import androidx.annotation.RequiresApi
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.R
+import mozilla.components.feature.autofill.facts.emitAutofillSearchDisplayedFact
+import mozilla.components.feature.autofill.facts.emitAutofillSearchSelectedFact
+import mozilla.components.feature.autofill.facts.emitLoginPasswordDetectedFact
+import mozilla.components.feature.autofill.response.dataset.LoginDatasetBuilder
+import mozilla.components.feature.autofill.structure.ParsedStructure
+import mozilla.components.feature.autofill.structure.parseStructure
+import mozilla.components.feature.autofill.structure.toRawStructure
+import mozilla.components.feature.autofill.ui.search.LoginsAdapter
+import mozilla.components.support.ktx.android.view.showKeyboard
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+
+/**
+ * Activity responsible for letting the user manually search and pick credentials for auto-filling a
+ * third-party app.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+abstract class AbstractAutofillSearchActivity : FragmentActivity() {
+ abstract val configuration: AutofillConfiguration
+
+ private lateinit var parsedStructure: ParsedStructure
+ private lateinit var loginsDeferred: Deferred<List<Login>>
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private val adapter = LoginsAdapter(::onLoginSelected)
+ private var imeSpec: InlinePresentationSpec? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (savedInstanceState == null) {
+ emitAutofillSearchDisplayedFact()
+ }
+
+ val structure: AssistStructure? =
+ intent.getParcelableExtraCompat(AutofillManager.EXTRA_ASSIST_STRUCTURE, AssistStructure::class.java)
+ if (structure == null) {
+ finish()
+ return
+ }
+ imeSpec = intent.getImeSpec()
+
+ val parsedStructure = parseStructure(this, structure.toRawStructure())
+ if (parsedStructure == null) {
+ finish()
+ return
+ }
+
+ this.parsedStructure = parsedStructure
+ this.loginsDeferred = loadAsync()
+
+ setContentView(R.layout.mozac_feature_autofill_search)
+
+ val recyclerView = findViewById<RecyclerView>(R.id.mozac_feature_autofill_list)
+ recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
+ recyclerView.adapter = adapter
+ recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL))
+
+ val searchView = findViewById<EditText>(R.id.mozac_feature_autofill_search)
+ searchView.doOnTextChanged { text, _, _, _ ->
+ if (text != null && text.isNotEmpty()) {
+ performSearch(text)
+ } else {
+ clearResults()
+ }
+ }
+
+ searchView.showKeyboard()
+ }
+
+ private fun onLoginSelected(login: Login) {
+ val builder = LoginDatasetBuilder(parsedStructure, login, needsConfirmation = false)
+ val dataset = builder.build(this, configuration, imeSpec)
+
+ val replyIntent = Intent()
+ replyIntent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset)
+
+ emitAutofillSearchSelectedFact()
+
+ setResult(RESULT_OK, replyIntent)
+ finish()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ scope.cancel()
+ }
+
+ private fun performSearch(text: CharSequence) = scope.launch {
+ val logins = loginsDeferred.await()
+
+ val filteredLogins = logins.filter { login ->
+ login.username.contains(text) ||
+ login.origin.contains(text)
+ }
+
+ if (filteredLogins.isNotEmpty() &&
+ filteredLogins[0].password.isNotEmpty()
+ ) {
+ emitLoginPasswordDetectedFact()
+ }
+
+ withContext(Dispatchers.Main) {
+ adapter.update(filteredLogins)
+ }
+ }
+
+ private fun clearResults() {
+ adapter.clear()
+ }
+
+ private fun loadAsync(): Deferred<List<Login>> {
+ return scope.async {
+ configuration.storage.list()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillUnlockActivity.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillUnlockActivity.kt
new file mode 100644
index 0000000000..33586aac72
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillUnlockActivity.kt
@@ -0,0 +1,125 @@
+/* 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.autofill.ui
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcel
+import android.service.autofill.FillResponse
+import android.view.autofill.AutofillManager
+import android.widget.inline.InlinePresentationSpec
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.authenticator.Authenticator
+import mozilla.components.feature.autofill.authenticator.createAuthenticator
+import mozilla.components.feature.autofill.facts.emitAutofillLock
+import mozilla.components.feature.autofill.handler.FillRequestHandler
+import mozilla.components.feature.autofill.handler.MAX_LOGINS
+import mozilla.components.feature.autofill.structure.ParsedStructure
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+
+/**
+ * Activity responsible for unlocking the autofill service by asking the user to verify with a
+ * fingerprint or alternative device unlocking mechanism.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+abstract class AbstractAutofillUnlockActivity : FragmentActivity() {
+ abstract val configuration: AutofillConfiguration
+
+ private var fillResponse: Deferred<FillResponse?>? = null
+ private val fillHandler by lazy { FillRequestHandler(context = this, configuration) }
+ private val authenticator: Authenticator? by lazy { createAuthenticator(this, configuration) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val parsedStructure = with(Parcel.obtain()) {
+ val rawBytes = intent.getByteArrayExtra(EXTRA_PARSED_STRUCTURE)
+ unmarshall(rawBytes!!, 0, rawBytes.size)
+ setDataPosition(0)
+ ParsedStructure(this).also {
+ recycle()
+ }
+ }
+ val imeSpec = intent.getImeSpec()
+ val maxSuggestionCount = intent.getIntExtra(EXTRA_MAX_SUGGESTION_COUNT, MAX_LOGINS)
+ // While the user is asked to authenticate, we already try to build the fill response asynchronously.
+ fillResponse = lifecycleScope.async(Dispatchers.IO) {
+ val builder = fillHandler.handle(parsedStructure, forceUnlock = true, maxSuggestionCount)
+ val result = builder.build(this@AbstractAutofillUnlockActivity, configuration, imeSpec)
+ result
+ }
+
+ if (authenticator == null) {
+ // If no authenticator is available then we just bail here. Instead we should ask the user to
+ // enroll, or show an error message instead.
+ // https://github.com/mozilla-mobile/android-components/issues/9756
+ setResult(RESULT_CANCELED)
+ finish()
+ } else {
+ authenticator!!.prompt(this, PromptCallback())
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ authenticator?.onActivityResult(requestCode, resultCode)
+ }
+
+ internal inner class PromptCallback : Authenticator.Callback {
+ override fun onAuthenticationError() {
+ fillResponse?.cancel()
+
+ emitAutofillLock(unlocked = false)
+
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+
+ override fun onAuthenticationSucceeded() {
+ configuration.lock.unlock()
+
+ val replyIntent = Intent().apply {
+ // At this point it should be safe to block since the fill response should be ready once
+ // the user has authenticated.
+ runBlocking { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse?.await()) }
+ }
+
+ emitAutofillLock(unlocked = true)
+
+ setResult(RESULT_OK, replyIntent)
+ finish()
+ }
+
+ override fun onAuthenticationFailed() {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+
+ companion object {
+ const val EXTRA_PARSED_STRUCTURE = "parsed_structure"
+ const val EXTRA_IME_SPEC = "ime_spec"
+ const val EXTRA_MAX_SUGGESTION_COUNT = "max_suggestion_count"
+ }
+}
+
+internal fun Intent.getImeSpec(): InlinePresentationSpec? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ getParcelableExtraCompat(
+ AbstractAutofillUnlockActivity.EXTRA_IME_SPEC,
+ InlinePresentationSpec::class.java,
+ )
+ } else {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/search/LoginViewHolder.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/search/LoginViewHolder.kt
new file mode 100644
index 0000000000..5d9ecbf771
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/search/LoginViewHolder.kt
@@ -0,0 +1,30 @@
+/* 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.autofill.ui.search
+
+import android.view.View
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.autofill.R
+import mozilla.components.feature.autofill.response.dataset.usernamePresentationOrFallback
+
+/**
+ * ViewHolder for a login item in the autofill search view.
+ */
+internal class LoginViewHolder(
+ itemView: View,
+ private val onLoginSelected: (Login) -> Unit,
+) : RecyclerView.ViewHolder(itemView) {
+ private val usernameView = itemView.findViewById<TextView>(R.id.mozac_feature_autofill_username)
+ private val originView = itemView.findViewById<TextView>(R.id.mozac_feature_autofill_origin)
+
+ fun bind(login: Login) {
+ usernameView.text = login.usernamePresentationOrFallback(itemView.context)
+ originView.text = login.origin
+
+ itemView.setOnClickListener { onLoginSelected(login) }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/search/LoginsAdapter.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/search/LoginsAdapter.kt
new file mode 100644
index 0000000000..54d65b0e88
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/search/LoginsAdapter.kt
@@ -0,0 +1,47 @@
+/* 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.autofill.ui.search
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.autofill.R
+
+/**
+ * Adapter for showing a list of logins.
+ */
+@SuppressLint("NotifyDataSetChanged")
+internal class LoginsAdapter(
+ private val onLoginSelected: (Login) -> Unit,
+) : RecyclerView.Adapter<LoginViewHolder>() {
+ private var logins: List<Login> = emptyList()
+
+ fun update(logins: List<Login>) {
+ this.logins = logins
+ notifyDataSetChanged()
+ }
+
+ fun clear() {
+ this.logins = emptyList()
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LoginViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ val view = inflater.inflate(R.layout.mozac_feature_autofill_login, parent, false)
+ return LoginViewHolder(view, onLoginSelected)
+ }
+
+ override fun onBindViewHolder(holder: LoginViewHolder, position: Int) {
+ val login = logins[position]
+ holder.bind(login)
+ }
+
+ override fun getItemCount(): Int {
+ return logins.count()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/verify/CredentialAccessVerifier.kt b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/verify/CredentialAccessVerifier.kt
new file mode 100644
index 0000000000..627f0ca72b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/verify/CredentialAccessVerifier.kt
@@ -0,0 +1,50 @@
+/* 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.autofill.verify
+
+import android.content.Context
+import mozilla.components.service.digitalassetlinks.AndroidAssetFinder
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
+
+/**
+ * Helper to verify that a specific application is allowed to receive get the login credentials for
+ * a specific domain.
+ *
+ * The verification is done through Digital Asset Links, which allow a domain to specify associated
+ * apps and their signatures.
+ * - https://developers.google.com/digital-asset-links/v1/getting-started
+ * - https://github.com/google/digitalassetlinks/blob/master/well-known/details.md
+ */
+class CredentialAccessVerifier(
+ private val checker: StatementRelationChecker,
+ private val assetsFinder: AndroidAssetFinder = AndroidAssetFinder(),
+) {
+ /**
+ * Verifies and returns `true` if the application with [packageName] is allowed to receive
+ * credentials for [domain] according to the hosted Digital Assets Links file. Returns `false`
+ * otherwise. This method may also return `false` if a verification could not be performed,
+ * e.g. the device is offline.
+ */
+ fun hasCredentialRelationship(
+ context: Context,
+ domain: String,
+ packageName: String,
+ ): Boolean {
+ val assets = assetsFinder.getAndroidAppAsset(packageName, context.packageManager).toList()
+
+ // I was expecting us to need to verify all signatures here. But If I understand the usage
+ // in `OriginVerifier` and the spec (see link in class comment) correctly then verifying one
+ // certificate is enough to identify an app.
+ val asset = assets.firstOrNull() ?: return false
+
+ return checker.checkRelationship(
+ AssetDescriptor.Web("https://$domain"),
+ Relation.GET_LOGIN_CREDS,
+ asset,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_login.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_login.xml
new file mode 100644
index 0000000000..c20c994cd8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_login.xml
@@ -0,0 +1,23 @@
+<?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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="4dp"
+ tools:ignore="Overdraw"
+ android:background="?android:attr/selectableItemBackground">
+ <TextView
+ android:id="@+id/mozac_feature_autofill_username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="4dp"/>
+ <TextView
+ android:id="@+id/mozac_feature_autofill_origin"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="4dp"/>
+</LinearLayout>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_preference.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_preference.xml
new file mode 100644
index 0000000000..a5ef02eae2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_preference.xml
@@ -0,0 +1,11 @@
+<?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/. -->
+<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/switch_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="center_vertical" />
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_search.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_search.xml
new file mode 100644
index 0000000000..cdefaa8152
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/layout/mozac_feature_autofill_search.xml
@@ -0,0 +1,24 @@
+<?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/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+ <EditText
+ android:id="@+id/mozac_feature_autofill_search"
+ android:background="@android:color/transparent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textUri"
+ android:singleLine="true"
+ android:hint="@string/mozac_feature_autofill_search_hint"
+ android:importantForAutofill="no" />
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/mozac_feature_autofill_list"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+</LinearLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..3f9ad47753
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-am/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$sን ይክፈቱ</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(የተጠቃሚ ስም የለም)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">የይለፍ ቃል ለ%1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">ማረጋገጥ አልተሳካም</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s የመተግበሪያውን ትክክለኛነት ማረጋገጥ አልቻለም። የተመረጡትን ምስክርነቶች በራስ-ሙላ መቀጠል ይፈልጋሉ?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">አዎ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">ይቅር</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s ፈልግ</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">መግቢያዎችን ፈልግ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..2826ba8e9d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ar/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">ألغِ قفل %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(لا اسم مستخدم)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">كلمة سر %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">فشل التحقق</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">لم يتمكّن %1$s من التحقّق من صحّة التطبيق. أتريد مواصلة ملء بيانات الولوج المحدّدة؟</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">نعم</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">لا</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">ابحث عن %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">ابحث في جلسات الولوج</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..07637c3330
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ast/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquiar «%1$s»</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Ensin nome d\'usuariu)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contraseña de: %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">La verificación falló</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">«%1$s» nun pudo verificar l\'autenticidá de l\'aplicación. ¿Quies siguir col rellenu automáticu de los datos seleicionaos?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sí</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Non</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Buscar «%1$s»</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Buscar nes cuentes</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..e231f52438
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-azb/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s کیلیدینی آچ</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">( هئچ قوللانیجی آدی یوخ)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s اوچون رمز</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">دوغرولاما قیریلدی</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s اپلیکیشن‌ین صحتینی تایید ائلینمه‌دی. سئچیلن کیملیک بیلگی‌لرینی اوتوماتیک دولدورماغا دوام ائتمک ایستییرسیز؟</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">هن</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">یوخ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s -دا آختار</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">گیریش‌لرده آختار</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..7ba8d819bb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-be/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Разблакаваць %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Без імя карыстальніка)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Пароль для %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Памылка праверкі</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s не ўдалося пацвердзіць сапраўднасць праграмы. Вы хочаце працягнуць аўтазапаўненне выбраных уліковых дадзеных?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Так</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Не</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Шукаць у %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Шукаць лагіны</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..f80a6023d4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-bg/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Отключете %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(без потребителско име)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Парола за %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Удостоверяването е неуспешно</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s не успя да потвърди автентичността на приложението. Желаете ли потребителските данни да бъдат попълнени автоматично въпреки това?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Да</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Не</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Търсене с %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Търсене на регистрация</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..45f6647fde
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-br/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Dibrennañ %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(anv arveriad ebet)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Ger-tremen evit %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Gwiriadur cʼhwitet</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s nʼen deus ket gellet gwiriañ dilested an arloadoù. Hag e fell deocʼh leuniañ an titouroù kennaskañ diuzet en un doare emgefreek?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ya</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ket</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Klask %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Klask titouroù kennaskañ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..132dde06d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-bs/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Otključaj %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nema korisničkog imena)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Lozinka za %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Provjera nije uspjela</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s nije mogao provjeriti autentičnost aplikacije. Želite li nastaviti s automatskim popunjavanjem odabranih akreditacija?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Da</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ne</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Pretraži %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Pretraži prijave</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..67c5b2fde2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ca/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloca el %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Cap nom d’usuari)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contrasenya per a %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Ha fallat la verificació</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">El %1$s no ha pogut verificar l’autenticitat de l’aplicació. Voleu procedir amb l’emplenament automàtic de les credencials seleccionades?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sí</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Cerca al %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Cerca els inicis de sessió</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..3581e1b4a4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-cak/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Timeq\'at %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Majun rub\'i\' okisanel)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Ewan tzij richin %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Xsach jikib\'anïk</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s man xtikïr ta xnik\'öx ri rujikib\'axik chokoy. ¿La nawajo\' chi ruyon ketz\'aqatisäx ri taq ruwujil echa\'on?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ja\'</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Mani</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Tikanöx %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Kekanöx tikirib\'äl taq molojri\'ïl</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..210f13dcd3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Unlock %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Wala\'y username)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Password sa %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verification napakyas</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">Ang %1$s dili maka-suta sa katinuod sa application. Buot mo ba i-padayon sa pag-autofill ang mga napiling mga credential?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Oo</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Dili</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Pangitaon %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Pangitaon ang logins</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..0048681436
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">کردنەوەی %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">( بێ ناوی بەکارهێنەر)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">وشەی تێپەڕبوون بۆ %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">دڵنیابوونەوە سەرکەوتوو نەبوو</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ناتوانێت دڵنیابێت لە دەسەڵات دان بەم بەرنامەیە. ئایا دەتەوێت بەردەوام بیت لە پێدانی دەسەڵاتی پڕکردنەوەی خۆکار زانیارییەکانت لەم ماڵپەڕە؟</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">بەڵێ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">نەخێر</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">بگەڕێ بۆ %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">بگەڕێ لە ناو چوونەژوورەوەکان</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..55b9e5cbbd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-co/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Spalancà %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nisunu nome d’utilizatore)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Parolla d’intesa per %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verificazione fiascata</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ùn pò micca verificà l’autenticità di l’appiecazione. Vulete cuntinuà à riempie autumaticamente l’identificazioni di cunnessione selezziunate ?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sì</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nò</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Ricercà in %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Ricercà identificazioni di cunnessione</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..d0053761c2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-cs/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Odemknout aplikaci %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Žádné uživatelské jméno)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Heslo pro účet %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Ověření se nezdařilo</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">Aplikace %1$s nemohla ověřit cílovou aplikaci. Opravdu do ní chcete vložit vybrané přihlašovací údaje?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ano</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ne</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Hledat v aplikaci %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Hledat přihlašovací údaje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..9e940fe253
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-cy/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Datgloi %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(dim enw defnyddiwr)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Cyfrinair %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Methodd y dilysu</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">Nid oedd %1$s yn gallu gwirio dilysrwydd y rhaglen. Ydych chi am fwrw ymlaen ag awtolenwi’r tystlythyrau a ddewiswyd?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Iawn</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Na</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Chwilio %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Chwilio mewngofnodion</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..ecdb268534
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-da/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Lås %1$s op</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Intet brugernavn)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Adgangskode for %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Bekræftelse mislykkedes</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s kunne ikke bekræfte autenticiteten af applikationen. Vil du fortsætte med at autofylde de valgte login-informationer?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ja</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nej</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Søg med %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Søg efter logins</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..ffc1c79c77
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-de/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s entsperren</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Kein Benutzername)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Passwort für %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verifizierung fehlgeschlagen</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s konnte die Authentizität der Anwendung nicht überprüfen. Möchten Sie mit der Autovervollständigung der ausgewählten Anmeldeinformationen fortfahren?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ja</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nein</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s durchsuchen</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Zugangsdaten durchsuchen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..c68ae83486
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s wěcej njeblokěrowaś</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Žedno wužywaŕske mě)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Gronidło za %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Pśeglědanje njejo se raźiło</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s njejo mógał awtentiskosć nałoženja pśespytowaś. Cośo z awtomatiskim wupołnjowanim wubranych pśizjawjeńskich datow pókšacowaś?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Jo</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ně</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s pśepytaś</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Pśizjawjenja pytaś</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..3192342b51
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-el/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Ξεκλείδωμα %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Χωρίς όνομα χρήστη)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Κωδικός πρόσβασης για %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Αποτυχία επαλήθευσης</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">Το %1$s δεν μπόρεσε να επαληθεύσει την αυθεντικότητα της εφαρμογής. Θέλετε να γίνει αυτόματη συμπλήρωση των επιλεγμένων διαπιστευτηρίων;</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ναι</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Όχι</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Αναζήτηση %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Αναζήτηση συνδέσεων</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..31b82b38e8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Unlock %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(No username)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Password for %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verification failed</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s could not verify the authenticity of the application. Do you want to proceed with autofilling the selected credentials?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Yes</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Search %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Search logins</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..31b82b38e8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Unlock %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(No username)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Password for %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verification failed</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s could not verify the authenticity of the application. Do you want to proceed with autofilling the selected credentials?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Yes</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Search %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Search logins</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..41e4760af1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-eo/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Malbloki %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(sen nomo de uzanto)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Pasvorto por %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Malsukcesa kontrolo</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ne povis kontroli la aŭtentikecon de la programo. Ĉu vi volas daŭrigi la aŭtomatan plenigadon de la elektitaj legitimiloj?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Jes</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ne</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Serĉi %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Serĉi legitimilojn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..6e6c053344
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquear %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Sin nombre de usuario)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contraseña para %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Falló la verificación</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s no pudo verificar la autenticidad de la aplicación. ¿Desea continuar autocompletando las credenciales seleccionadas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sí</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Buscar en %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Buscar inicios de sesión</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..2a11a588b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquear %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Sin nombre de usuario)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contraseña para %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verificación fallida</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s no pudo verificar la autenticidad de la aplicación. ¿Deseas continuar autocompletando las credenciales seleccionadas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sí</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Buscar %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Buscar credenciales</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..925d071c4a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquear %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Sin nombre de usuario)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contraseña para %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Ha fallado la verificación</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s no ha podido verificar la autenticidad de la aplicación. ¿Quieres continuar autocompletando las credenciales seleccionadas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sí</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Buscar %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Buscar inicios de sesión</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..46eb7b70dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquear %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Sin nombre de usuario)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contraseña para %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verificación fallida</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s no pudo verificar la autenticidad de la aplicación. ¿Deseas continuar autocompletando las credenciales seleccionadas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sí</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Buscar %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Buscar inicios de sesión</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..925d071c4a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-es/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquear %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Sin nombre de usuario)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contraseña para %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Ha fallado la verificación</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s no ha podido verificar la autenticidad de la aplicación. ¿Quieres continuar autocompletando las credenciales seleccionadas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sí</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Buscar %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Buscar inicios de sesión</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..21429a5487
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-et/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Lukusta %1$s lahti</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(kasutajanime pole)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Konto %1$s parool</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verifitseerimine ebaõnnestus</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s polnud võimalik rakenduse autentsust verifitseerida. Kas soovid jätkata valitud kasutajatunnuste automaatse täitmisega?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Jah</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ei</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Otsi %1$sist</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Otsi kasutajatunnuseid</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..0670aa1c98
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-eu/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desblokeatu %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Erabiltzaile-izenik ez)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s kontuaren pasahitza</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Egiaztapenak huts egin du</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s(e)k ezin izan du aplikazioaren autentikotasuna egiaztatu. Hautatutako kredentzialak automatikoki bete nahi dituzu halere?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Bai</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ez</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Bilatu %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Bilatu saio-hasierak</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..c37a4c2e2a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fa/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">بازکردن %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(بدون نام‌کاربری)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">گذرواژه برای %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">تأیید ناموفق بود</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s نتوانست هویت برنامه را تایید کند. آیا می‌خواهید به پُر کردن خودکار اطلاعات ادامه دهید؟</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">بله</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">خیر</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">جست‌وجو در %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">جست‌وجو در واردشده ها</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..db106f3585
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ff/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Alaa innde kuutoro)</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Yiylo %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Yiylo ceŋorɗe</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..58c0fa2414
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fi/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Avaa %1$sin lukitus</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ei käyttäjätunnusta)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Salasana tilille %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Vahvistus epäonnistui</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ei voinut vahvistaa sovelluksen aitoutta. Haluatko jatkaa valittujen kirjautumistietoijen automaattista täydentämistä?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Kyllä</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ei</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Etsi %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Etsi kirjautumistietoja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..240da07fd6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fr/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Déverrouiller %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Aucun nom d’utilisateur)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Mot de passe pour %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Échec de la vérification</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s n’a pas pu vérifier l’authenticité de l’application. Voulez-vous tout de même procéder au remplissage automatique des identifiants sélectionnés ?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Oui</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Non</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Rechercher dans %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Rechercher des identifiants</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..0f2eb2f0d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fur/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Sbloche %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nissun non utent)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Password par %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verifiche falide</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s nol à podût verificâ la autenticitât de aplicazion. Procedi cu la compilazion automatiche doprant lis credenziâls selezionadis?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sì</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Cîr in %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Cîr tes credenziâls</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..7056018333
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s ûntskoattelje</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Gjin brûkersnamme)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Wachtwurd foar %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Ferifikaasje mislearre</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s koe de autentisiteit fan de tapassing net ferifiearje. Wolle jo trochgean mei it automatysk ynfoljen fan de selektearre oanmeldgegevens?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ja</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nee</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s trochsykje</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Oanmeldingen sykje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..ab2a418523
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-gd/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Thoir a’ ghlas far %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Gun ainm-cleachdaiche)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Am facal-faire airson %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Dh’fhàillig leis an dearbhadh</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">Cha deach le %1$s dearbh-aithneachadh a dhèanamh air a aplacaid. A bheil thu airson leantainn air adhart le lìonadh fèin-obrachail an teisteis a thagh thu?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Tha</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Chan eil</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Lorg %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Lorg sna clàraidhean a-steach</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..eadc31c59e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-gl/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquear %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Sen nome de usuario)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contrasinal de %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Fallou a comprobación</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s non puido verificar a autenticidade da aplicación. Quere continuar co enchido automático das credenciais seleccionadas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Si</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Non</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Buscar en %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Buscar nas credenciais</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..47c023866d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-gn/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Emyandyjey %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Poruhára hera’ỹva)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s ñe’ẽñemi</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Ojavy jehechajey</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ndaikatúi ohechajey pe tembiporu’i ha’épa añeteguáva. ¿Emyanyhẽsevépa umi terarenda jeporavopyre?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Héẽ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nahániri</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Eheka %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Eheka tembiapo ñepyrũ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..df50691a99
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s को खोलें</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(कोई उपयोगकर्ता नाम नहीं)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s के लिए पासवर्ड</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">सत्यापन विफल</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">हां</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">नहीं‌</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s खोजें</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">लॉगिन खोजें</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..98eb4327ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hil/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Huo</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Indi</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Pangitaon %1$s</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..cae7d79ac2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hr/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Otključaj %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(nema korisničkog imena)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Lozinka za %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Provjera nije uspjela</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s nije mogao provjeriti autentičnost aplikacije. Želite li nastaviti s automatskim popunjavanjem odabranih vjerodajnica?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Da</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ne</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Pretraži %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Pretraži prijave</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..2576402485
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s hižo njeblokować</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Žane wužiwarske mjeno)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Hesło za %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Přepruwowanje je so nimokuliło</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s njemóžeše awtentiskosć nałoženja přepruwować. Chceće z awtomatiskim wupjelnjenjom wubranych přizjewjenskich datow pokročować?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Haj</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ně</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s přepytać</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Přizjewjenja pytać</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..f71c532838
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hu/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">A %1$s feloldása</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nincs felhasználónév)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Jelszó a következőhöz: %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Az ellenőrzés sikertelen</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">A(z) %1$s nem tudta ellenőrizni az alkalmazás hitelességét. Folytatja a kiválasztott hitelesítő adatok automatikus kitöltését?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Igen</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nem</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s keresés</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Bejelentkezések keresése</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..5e194f3fe6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Ապակողպեл %1$s-ը</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(չկա օգտվողի անուն)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Գաղտնաբառ %1$s-ի համար</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Նույնականացումը ձախողվեց</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s-ը չկարողացավ ստուգել ծրագրի իսկությունը: Ցանկանո՞ւմ եք շարունակել ընտրված հավատարմագրերի ինքնալրացումը:</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Այո</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ոչ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Որոնել %1$s-ում</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Որոնել մուտքանուններ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..c7ce79ab63
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ia/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Disblocar %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nulle nomine de usator)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Contrasigno pro %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verification fallite</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s non pote verificar le authenticitate del application. Vole tu continuar con auto-plenamento del seligite credentiales?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Si</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Cercar in %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Cercar credentiales</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..914a06b96f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-in/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Buka %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Tidak ada nama pengguna)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Sandi untuk %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verifikasi gagal</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s tidak dapat memeriksa keaslian aplikasi tersebut. Ingin melanjutkan dengan mengisi otomatis dengan kredensial terpilih?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ya</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Tidak</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Cari %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Cari log masuk</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..750ea8d274
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-is/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Opna fyrir %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ekkert notandanafn)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Lykilorð fyrir %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Staðfesting mistókst</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s gat ekki staðfest áreiðanleika forritsins. Viltu halda áfram að fylla sjálfvirkt út valin auðkenni?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Já</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nei</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Leita í %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Leita að innskráningu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..0397938e5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-it/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Sblocca %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(nessun nome utente)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Password per %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verifica non riuscita</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s non ha potuto verificare l’autenticità dell’applicazione. Procedere con la compilazione automatica usando le credenziali selezionate?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sì</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Cerca in %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Cerca nelle credenziali</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..baf67c7f81
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-iw/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">שחרור נעילת %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(אין שם משתמש)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">ססמה עבור %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">האימות נכשל</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">‏%1$s לא הצליח לאמת את אמינות היישומון. האם ברצונך להמשיך במילוי אוטומטי של פרטי הכניסה שנבחרו?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">כן</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">לא</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">חיפוש ב־%1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">חיפוש כניסות</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..b3a0989710
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ja/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s のロックを解除</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ユーザー名なし)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s のパスワード</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">検証失敗</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s がアプリケーションの信頼性を検証できませんでした。選択した資格情報の自動入力を続けますか?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">はい</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">いいえ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s を検索</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">ログイン情報を検索</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..fd9b99f9d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ka/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">გაიხსნას %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(სახელის გარეშე)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">პაროლი ანგარიშისთვის %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">დამოწმება ვერ მოხერხდა</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ვერ ადასტურებს აპლიკაციის ნამდვილობას. გსურთ, განაგრძოთ შერჩეული მონაცემებით თვითშევსება?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">დიახ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">არა</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">ძიება - %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">ანგარიშების ძიება</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..d601de91b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s bloktan shıǵarıw</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Paydalanıwshı atı joq)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s ushın parol</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Tastıyıqlaw ámelge aspadı</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s baǵdarlamanıń haqıyqıylıǵın teksere almadı. Tańlanǵan esap maǵlıwmatların avtomat tárizde toltırıwdı qáleysiz be?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Awa</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Yaq</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s izleń</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Loginlerdi zleń</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..45a385c8f1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-kab/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Serreḥ i %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Ulac isem n useqdac)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Awal uffir i %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Asenqed ur yeddi ara</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ur ezmir ara ad isenqed asesteb n usnas. Tebɣiḍ ad tkemmleḍ s tacaṛt tawurmant inekcam yettwafernen?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ih</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ala</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Nadi %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Nadi inekcam</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..8f71c1997a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-kk/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s босату</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Пайдаланушы аты жоқ)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s үшін пароль</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Верификация сәтсіз аяқталды</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s қолданба шынайылығын тексере алмады. Таңдалған тіркелу мәліметтерін автоматты түрде толтырумен жалғастыру керек пе?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Иә</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Жоқ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s ішінен іздеу</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Логиндерден іздеу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..5a91889228
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Kilîda %1$s’ê veke</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Navê bikarhêner tune)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Pêborîna %1$s’ê</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Piştrastkirin têk çû</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s nekariye rayeya serlêdanê piştrast bike. Gelo tu dixwazî zanyariyên hesabê hatî hilbijartin xweber bê dagitin û wisa dewam bikî?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Erê</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Na</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Li %1$s bigere</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Li têketinan bigere</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..c8dc9de112
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ko/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s 잠금 해제</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(사용자 이름 없음)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s 비밀번호</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">확인 실패</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s가 애플리케이션의 신뢰성을 확인할 수 없습니다. 선택한 자격 증명을 자동으로 채우시겠습니까?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">예</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">아니요</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s 검색</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">로그인 검색</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..d5b78f234d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-lo/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">ປົດລັອກ %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ບໍ່ມີຊື່ຜູ້ໃຊ້)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">ລະຫັດຜ່ານສຳລັບ %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">ການຢັ້ງຢືນລົ້ມເຫລວ</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ບໍ່ສາມາດລະບຸຄວາມຖືກຕ້ອງຂອງແອັບພຣິເຄຊັນໄດ້. ທ່ານຕ້ອງການຕື່ມຂໍ້ມູນປະຈຳຕົວຂອງທ່ານທີ່ເລືອກອັດຕະໂນມັດຫລືບໍ?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ຕ້ອງການ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">ບໍ່ຕ້ອງການ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">ຄົ້ນຫາ %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">ຄົ້ນຫາຂໍ້ມູນການລັອກອິນ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..b38c5ee06d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-lt/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Atrakinti „%1$s“</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(nėra naudotojo vardo)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Paskyros „%1$s“ slaptažodis</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Patvirtinimas nepavyko</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">„%1$s“ nepavyko patvirtinti programos autentiškumo. Ar norite automatiškai užpildyti prisijungimo duomenis?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Taip</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ne</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Ieškoti „%1$s“</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Ieškoti prisijungimų</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..ada67769d5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-mix/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Kuna %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Koo sivi)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Tu^un se^e %s</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..3beb79aeec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-my/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s ကို ဖွင့်ပါ</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(အသုံးပြုသူအမည် မရှိ)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s အတွက် စကားဝှက်</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">မှန်ကန်မှုကို စစ်ဆေးအတည်ပြုခြင်း မအောင်မြင်ပါ</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s သည် အပလီကေးရှင်း ၏ စစ်မှန်မှုကို အတည်မပြုနိုင်ပါ။ သင်ရွေးချယ်ထားသော အထောက်အထားများကို အလိုအလျောက်ဖြည့်ခြင်းနှင့် ဆက်လက် လုပ်ဆောင်လိုပါသလား။</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">လုပ်ဆောင်ပါမည်</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">မလုပ်ဆောင်တော့ပါ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s ကို ရှာပါ</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">လော့ဂ်အင် ဝင်ရောက်မှုများကို ရှာပါ </string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..cd3ef4edbc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Lås opp %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Uten brukernavn)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Passord for %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Bekreftelsen mislyktes</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s kunne ikke bekrefte ektheten til applikasjonen. Vil du fortsette med autoutfylling av valgte innloggingsinformasjon?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ja</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nei</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Søk %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Søk innlogginger</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..c193be1445
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s लाई अनलक गर्नुहोस्</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(प्रयोगकर्ताको नाम छैन)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s को लागि पासवर्ड</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">प्रमाणीकरण असफल भयो</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ले यो एपको प्रामाणिकता प्रमाणित गर्न सकेन। के तपाइँ छानिएका प्रमाणहरू स्वत: भरेर अगाडि बढ्न चाहनुहुन्छ?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">हुन्छ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">हुँदैन</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s खोज्नुहोस्</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">लगइनहरु खोज्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..b8c7592da0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-nl/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s ontgrendelen</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Geen gebruikersnaam)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Wachtwoord voor %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verificatie mislukt</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s kon de authenticiteit van de toepassing niet verifiëren. Wilt u doorgaan met het automatisch invullen van de geselecteerde aanmeldgegevens?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ja</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nee</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s doorzoeken</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Aanmeldingen zoeken</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..494754299a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Lås opp %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ikkje noko brukarnamn)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Passord for %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Mislykka stadfesting</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s klarte ikkje å stadfeste autentisiteten til programmet. Vil du fortsetje med å automatiskt fylle ut dei valde opplysningane?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ja</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nei</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Søk %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Søk innloggingar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..13a4bfc6da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-oc/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desblocar %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Cap de nom d’utilizaire)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Senhal per %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verificacion fracassada</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s a pas pogut verificar l’autenticitat de l’aplicacion. Volètz contunhar l’autocompletacion dels identificants seleccionats.</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Òc</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Non</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Cercar dins %s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Recercar d’identificants</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..a189a10c53
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-or/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ଉପଭୋକ୍ତାଙ୍କ ନାମ ନାହିଁ)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s ପାଇଁ ପାସୱାର୍ଡ଼</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">ଯାଞ୍ଚ ବିବରଣୀ ବିଫଳ</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ହଁ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">ନାଁ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..2435dc53f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s ਅਣ-ਲਾਕ ਕਰੋ</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ਕੋਈ ਵਰਤੋਂਕਾਰ ਨਾਂ ਨਹੀਂ)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s ਲਈ ਪਾਸਵਰਡ</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">ਤਸਦੀਕ ਅਸਫ਼ਲ ਰਹੀ</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ਐਪਲੀਕੇਸ਼ਨ ਦੀ ਪਰਮਾਣਿਕਤਾ ਦੀ ਤਸਦੀਕ ਨਹੀਂ ਕਰ ਸਕਿਆ। ਕੀ ਤੁਸੀਂ ਚੁਣੀਆਂ ਸਨਦਾਂ ਨੂੰ ਆਪੇ-ਭਰਨ ਨਾਲ ਜਾਰੀ ਰੱਖਣਾ ਚਾਹੁੰਦੇ ਹੋ?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ਹਾਂ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">ਨਹੀਂ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s ਖੋਜੋ</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">ਲਾਗਇਨ ਖੋਜੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..ac43539016
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s بند کرن نوں الٹاؤ</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(کوئی ورتنوالے دا ناں نہیں جاݨیا)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s ناں نال پاس‌ورڈ</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">تصدیق نال غلطی ہو گئی اے</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ایپ دی تصدیق نہیں کر سکدا۔ کیہ تسیں چݨیاں سنداں نوں آپے بھرن نال جاری رکھݨا چہندے او؟</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ہاں</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">نہیں</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s ایپ چ کھوجو</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">کھاتے چ کھوجو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..2b778b31b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pl/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Odblokuj program %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Bez nazwy użytkownika)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Hasło dla konta %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Weryfikacja się nie powiodła</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s nie może zweryfikować autentyczności aplikacji. Czy kontynuować automatyczne wypełnianie wybranych danych logowania?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Tak</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nie</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Szukaj w programie %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Szukaj danych logowania</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..f6b798fd2a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquear %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(sem nome de usuário)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Senha de %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Falha na verificação</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">O %1$s não conseguiu verificar a autenticidade do aplicativo. Quer prosseguir com o preenchimento automático das credenciais selecionadas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sim</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Não</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Procurar no %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Procurar contas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..05a46a791d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Desbloquear %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Sem nome de utilizador)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Palavra-passe para %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">A confirmação falhou</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s não conseguiu confirmar a autenticidade da aplicação. Quer continuar com o preenchimento automático das credenciais selecionadas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sim</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Não</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Pesquisar no %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Pesquisar credenciais</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..78619908c2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-rm/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Debloccar %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nagin num d\'utilisader)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Pled-clav per %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verificaziun betg reussida</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s n\'ha betg pudì verifitgar l\'autenticitad da l\'applicaziun. Vuls ti cuntinuar e laschar emplenir automaticamain las datas d\'annunzia tschernidas?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Gea</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Na</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Tschertgar en %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Retschertgar las datas d\'annunzia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..cac24165e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ro/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Niciun nume de utilizator)</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Caută în %1$s</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..e92c72cac6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ru/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Разблокировать %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Нет имени пользователя)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Пароль для %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Проверка не удалась</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s не удалось проверить подлинность приложения. Вы хотите произвести автозаполнение выбранных учётных данных?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Да</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Нет</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Искать в %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Поиск логинов</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..70fecdcbca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sat/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹ ᱧᱩᱛᱩᱢ ᱵᱟᱹᱱᱩᱜᱼᱟ)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s ᱞᱟᱹᱜᱤᱫ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">ᱯᱨᱚᱢᱟᱬᱤᱛ ᱰᱤᱜᱟᱹᱣ ᱮᱱᱟ</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ᱫᱚ ᱮᱯᱞᱤᱥᱠᱮᱥᱚᱱ ᱨᱮᱭᱟᱜ ᱥᱟᱹᱨᱤ ᱠᱚ ᱵᱟᱭ ᱯᱩᱥᱴᱟᱹᱣ ᱫᱟᱲᱮᱭᱟᱫᱟᱭ ᱾ ᱟᱢ ᱪᱮᱫ ᱵᱟᱪᱷᱟᱣ ᱟᱠᱟᱱ ᱠᱨᱮᱰᱮᱱᱥᱤᱭᱟᱞᱥ ᱛᱮ ᱟᱡ ᱛᱮ ᱯᱩᱨᱟᱹᱣ ᱪᱷᱚᱣᱟᱜ ᱥᱮᱱᱟᱢ ᱠᱟᱱᱟ ᱥᱮ?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ᱦᱮᱸ</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">ᱵᱟᱝ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%s ᱥᱮᱸᱫᱽᱨᱟᱭ ᱢᱮ</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">ᱞᱚᱜᱤᱱ ᱠᱚ ᱥᱮᱸᱫᱽᱨᱟᱭ ᱢᱮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..e05c371bed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sc/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Isbloca %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nissunu nòmine utente)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Crae pro %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Faddina in sa verìfica</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Eja</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nono</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Chirca %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Chirca credentziales</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..74b25af4f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-si/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s අගුළු හරින්න</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(පරිශීලක නාමය නැත)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s සඳහා මුරපදය</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">සත්‍යාපනයට අසමත් විය!</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s සඳහා යෙදුම සත්‍යාපනයට නොහැකි විය. ඔබට තෝරාගත් අක්තපත්‍ර ස්වයංක්‍රීයව පිරවීමෙන් ඉදිරියට යාමට අවශ්‍යද?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ඔව්</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">නැහැ</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s සොයන්න</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">පිවිසුම් සොයන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..4bc86cbb62
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sk/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Odomknúť %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Žiadne používateľské meno)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Heslo pre účet %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Overenie zlyhalo</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">Prehliadač %1$s nemohol overiť pravosť aplikácie. Chcete pokračovať v automatickom dopĺňaní prihlasovacích údajov?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Áno</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nie</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Hľadať v aplikácii %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Hľadať v prihlasovacích údajoch</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..378959c47c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-skr/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s اݨ لاک کرو</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ورتݨ ناں کوئی کائنی)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s کیتے پاس ورڈ</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">پڑتال ناکام تھی ڳئی</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ایپ دے مستند ہووݨ دی تصدیق کائنی کر سڳا۔ بھلا تساں چݨی اسناد کوں خودکاربھرݨ نال اڳوں تے ودھݨ چاہسو؟</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">جیا</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">کو</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s ڳولو</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">لاگ ان ڳولو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..e31de3c7ff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sl/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Odkleni %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(brez uporabniškega imena)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Geslo za %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Preverjanje ni uspelo</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ni mogel preveriti pristnosti aplikacije. Ali želite nadaljevati s samodejnim izpolnjevanjem izbranih podatkov za prijavo?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Da</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ne</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Išči v %1$su</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Iskanje prijav</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..ae824fa834
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sq/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Shkyçe %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Pa emër përdoruesi)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Fjalëkalim për %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verifikimi dështoi</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s s’verifikoi dot mirëfilltësinë e këtij aplikacioni. Doni të kryhet vetëplotësimi i kredencialeve të përzgjedhura?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Po</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Jo</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Kërko për %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Kërkoni te kredenciale hyrjesh</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..cb3c56e1ed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sr/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Откључај %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(без корисничког имена)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Лозинка за %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Провера није успела</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s није могао да провери аутентичност апликације. Желите ли да наставите с аутоматским попуњавањем изабраних акредитива?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Да</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Не</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Претражи %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Претражи пријаве</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..094162d8a3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-su/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Buka konci %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Taya sandiasma)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Kecap sandi pikeun %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Péripikasi gagal</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s teu bisa muguhkeun oténtisitas aplikasina. Rék diteruskeun ku ngeusi otomatis data nu dipilih?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Enya</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Moal</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Paluruh %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Paluruh login</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..c35fb6ef1c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Lås upp %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Inget användarnamn)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Lösenord för %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verifieringen misslyckades</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s kunde inte verifiera programmets äkthet. Vill du fortsätta med att automatiskt fylla i de valda uppgifterna?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ja</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nej</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Sök efter %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Sök inloggningar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..0b6ad1ab00
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ta/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s ஐ பூட்டவிழ்</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(பயனர்பெயர் இல்லை)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s க்கான கடவுச்சொல்</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">சரிபார்ப்பு தோல்வி</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ஆல் செயலியின் நம்பகத்தன்மையை சரிபார்க்க இயலவில்லை. தேர்ந்தெடுக்கப்பட்ட நற்சான்றிதழ்களை தானாக நிரப்புவதன் மூலம் தொடர விரும்புகிறீர்களா?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ஆம்</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">இல்லை</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">புகுபதிகைகளைத் தேடு</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..79069b6350
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-te/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(వాడుకరి పేరు లేదు)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$sకి సంకేతపదం</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">తనిఖీ విఫలమైంది</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">అవును</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">వద్దు</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..a454579cb1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tg/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Кушодани қулфи %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Номи корбар нест)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Ниҳонвожа барои %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Санҷиши ҳаққоният иҷро нашуд</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ҳаққонияти барномаро тасдиқ карда натавонист. Шумо мехоҳед, ки маълумоти воридшавии интихобшударо ба таври худкор пур карда, идома диҳед?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ҳа</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Не</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Ҷустуҷӯ дар %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Ҷустуҷӯи воридшавиҳо</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..251d185372
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-th/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">ปลดล็อค %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ไม่มีชื่อผู้ใช้)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">รหัสผ่านสำหรับ %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">การตรวจสอบล้มเหลว</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ไม่สามารถตรวจสอบความถูกต้องของแอปพลิเคชันได้ คุณต้องการเติมข้อมูลประจำตัวที่เลือกอัตโนมัติต่อหรือไม่?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ใช่</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">ไม่</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">ค้นหา %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">ค้นหาการเข้าสู่ระบบ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..792e13cf2e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tl/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">i-Unlock ang %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(No username)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Password para sa %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Nabigo ang pag-verify</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">Hindi ma-verify ng %1$s ang pagiging tunay ng application. Nais mo bang magpatuloy sa pag-autofill ng mga napiling kredensyal?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Oo</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Hindi</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Hanapin sa %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Hanapin sa mga login</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..a370d24b26
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tok/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">o open e %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(nimi li lon ala)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">nimi open tawa ni: %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">mi ken ala pona e ilo</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">ilo %1$s li ken ala pona e ilo. sina pana ala pana e nimi sina?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">pana</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">ala</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">o lukin e %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">o lukin e nimi open</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..5292e1edbb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tr/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s kilidini aç</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Kullanıcı adı yok)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s parolası</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Doğrulama başarısız</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s, uygulamanın yetkinliğini doğrulayamadı. Seçili hesap bilgilerini otomatik olarak doldurmaya devam etmek istiyor musunuz?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Evet</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Hayır</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s’ta ara</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Hesaplarda ara</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..8d080ad617
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-trs/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Dūgi\'iaj sun\' %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nitāj si yūgui usuario hua)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Da\’nga\’ huì guendâ %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Nu gā’ue nātsij man</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">Nu gā’hue nātsij %1$s si huā hue’ê aplikasiûn nan. Ruhuât gān’ānjt ne’ ñāa da’ gīsìj nej kredenciâ gida’a raj.</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ga\'ue</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Si ga\'ue</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Nanà\'huì\' %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Nānà\'uì\' nej riña gayi\'ît sēsiûn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..5f37bea3a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tt/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s программасын ачу</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Кулланучы исеме юк)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s өчен парол</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Раслау уңышсыз тәмамланды</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Әйе</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Юк</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s эченнән эзләү</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Логиннардан эзләү</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..a83b882497
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Rẓem %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Walu yism unessemres)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Taguri n uzerray i %1$s</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Yah</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Uhu</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Rzu %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Rzu inekcam</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..129dec6052
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ug/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s قۇلۇپىنى ئاچ</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(ئىشلەتكۈچى ئىسمى يوق)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s ئىمى</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">دەلىللىيەلمىدى</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ئەپنىڭ چىنلىقىنى دەلىللىيەلمىدى. ئۆزلۈكىدىن تولدۇرۇلۇپ تاللانغان تىزىمغا كىرىش ئۇچۇرىنى داۋاملاشتۇرامسىز؟</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">ھەئە</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">ياق</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions"> %1$sدىن ئىزدەش</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">كىرىش خاتىرىسىنى ئىزدەش</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..894bbb4520
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-uk/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Розблокувати %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Без імені користувача)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Пароль для %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Помилка перевірки</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s не вдалося перевірити справжність програми. Продовжити автозаповнення вибраних облікових даних?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Так</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Ні</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Пошук в %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Шукати паролі</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..e5eccda540
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-ur/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s ان لاک کریں</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(صارف نام کا نہیں)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$sکے لئے پاسورڈ</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">توثیق کاری ناکام ہوگئی</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">جی ہاں</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">جی نہیں</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$s تلاش کریں</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">لاگ ان تلاش کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..27abed151a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-uz/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">%1$s qulfini ochish</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Foydalanuvchi nomi yoʻq)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s uchun parol</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Tasdiqlanmadi</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s ilovaning haqiqiyligini tekshira olmadi. Tanlangan hisob maʼlumotlarini avtomatik toʻldirishni davom ettirmoqchimisiz?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Ha</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Yoʻq</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">%1$sni qidirish</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Loginlarni qidirish</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..410e0a27bd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-vec/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Sbloca %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Nisun nòme utente)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Password par %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verifega no riusìa</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s non el ga posudo verifegare l’autentisidà de l’aplicasion. Prosedare con ƚa conpilasion otomatega uxando ƚe credensiaƚi selesionà?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Sì</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Nò</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Cata en %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Cata enteƚe credensiaƚi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..c474dc0979
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-vi/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Mở khóa %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Không có tên người dùng)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Mật khẩu cho %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Xác minh thất bại</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s không thể xác minh tính xác thực của ứng dụng. Bạn có muốn tiếp tục tự động điền thông tin đăng nhập đã chọn không?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Có</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Không</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Tìm kiếm %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Tìm kiếm thông tin đăng nhập</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..283990d696
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-yo/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Ṣi sílẹ̀ %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(Kò sí orúkọ àmúlò)
+</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Pásíwọọ̀dù fún %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Iṣẹ́ ìmúdájú kùnà</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s kò le rí àrídájú fún áàpù náà. Ṣé o fẹ́ tẹ̀síwájú pẹ̀lú yíyàn-aládàáṣe fún àwọn ìwé-ẹ̀rí náà?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Bẹ́ẹ̀ni</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">Bẹ́ẹ̀ kọ́</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Ṣàwárí %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Yẹ àwọn ìwọlé wò</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..5a15f62f28
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">解锁 %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(无用户名)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s 的密码</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">验证失败</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s 无法验证此应用程序的真实性,您确定要自动填充选择的登录信息吗?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">是</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">否</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">搜索保存于 %1$s 的登录信息</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">搜索登录信息</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..4c91ff38bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">解鎖 %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(無使用者名稱)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">%1$s 的密碼</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">驗證失敗</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s 無法驗證此應用程式的真實性,您確定要自動填入選擇的登入資訊嗎?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">要填入</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">不要填入</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">搜尋儲存於 %1$s 的登入資訊</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">搜尋登入資訊</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/autofill/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..1445f98965
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/main/res/values/strings.xml
@@ -0,0 +1,48 @@
+<?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>
+ <!-- Autofill: Text shown in popup in third-party app if the browser app needs to be unlocked before
+ a username or password can be autofilled for the highlighted text field. %1$s will be replaced
+ with the name of the browser application (e.g. Firefox) -->
+ <string name="mozac_feature_autofill_popup_unlock_application">Unlock %1$s</string>
+
+ <!-- Autofill: Text shown in popup in third-party app if we found a matching account, but no
+ username is saved (e.g. we only have a password). This text will be shown in place where otherwise
+ the username would be displayed. -->
+ <string name="mozac_feature_autofill_popup_no_username">(No username)</string>
+
+ <!-- Autofill: Text shown in popup in third-party app to autofill the password for an account.
+ %1$s will be replaced with the login/username of the account. -->
+ <string name="mozac_feature_autofill_popup_password">Password for %1$s</string>
+
+ <!-- Autofill: Title of a dialog asking the user to confirm before autofilling credentials into
+ a third-party app after the authenticity verification failed. -->
+ <string name="mozac_feature_autofill_confirmation_title">Verification failed</string>
+
+ <!-- Autofill: Text shown in dialog asking the user to confirm before autofilling credentials into a
+ third-party app where we could not verify the authenticity (e.g. we determined that this app is
+ a twitter client and we could autofill twitter credentials, but according to the "Digital Asset
+ Links" this application is not the official Twitter application for twitter.com credentials.
+ %1$s will be replaced with the name of the browser application (e.g. Firefox).
+ -->
+ <string name="mozac_feature_autofill_confirmation_authenticity">%1$s could not verify the authenticity of the application. Do you want to proceed with autofilling the selected credentials?</string>
+
+ <!-- Autofill: Positive button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_yes">Yes</string>
+
+ <!-- Autofill: Negative button shown in dialog asking the user to confirm before autofilling
+ credentials in a third-part app (Also see string mozac_feature_autofill_confirmation_authenticity). -->
+ <string name="mozac_feature_autofill_confirmation_no">No</string>
+
+ <!-- Autofill: When showing a list of logins to autofill in a third-party app, then this is the
+ last item in the list. When clicking it a new screen opens which allows the user to search for
+ a specific login. %1$s will be replaced with the name of the application (e.g. "Firefox") -->
+ <string name="mozac_feature_autofill_search_suggestions">Search %1$s</string>
+
+ <!-- Autofill: Hint shown in the text field used to search specific logins. Shown when the field
+ is empty and the user has not entered any text into it yet. -->
+ <string name="mozac_feature_autofill_search_hint">Search logins</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/AutofillUseCasesTest.kt b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/AutofillUseCasesTest.kt
new file mode 100644
index 0000000000..21a96c89b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/AutofillUseCasesTest.kt
@@ -0,0 +1,192 @@
+/* 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.autofill
+
+import android.content.Context
+import android.view.autofill.AutofillManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AutofillUseCasesTest {
+ @Test
+ fun testIsSupported() {
+ val context: Context = mock()
+ val autofillManager: AutofillManager = mock()
+ doReturn(autofillManager).`when`(context).getSystemService(AutofillManager::class.java)
+ doReturn(true).`when`(autofillManager).isAutofillSupported
+
+ assertFalse(AutofillUseCases(sdkVersion = 21).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 22).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 23).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 24).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 25).isSupported(context))
+
+ assertTrue(AutofillUseCases(sdkVersion = 26).isSupported(context))
+ assertTrue(AutofillUseCases(sdkVersion = 27).isSupported(context))
+ assertTrue(AutofillUseCases(sdkVersion = 28).isSupported(context))
+ assertTrue(AutofillUseCases(sdkVersion = 29).isSupported(context))
+ assertTrue(AutofillUseCases(sdkVersion = 30).isSupported(context))
+ }
+
+ @Test
+ fun testIsNotSupported() {
+ val context: Context = mock()
+ val autofillManager: AutofillManager = mock()
+ doReturn(autofillManager).`when`(context).getSystemService(AutofillManager::class.java)
+ doReturn(false).`when`(autofillManager).isAutofillSupported
+
+ assertFalse(AutofillUseCases(sdkVersion = 21).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 22).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 23).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 24).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 25).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 26).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 27).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 28).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 29).isSupported(context))
+ assertFalse(AutofillUseCases(sdkVersion = 30).isSupported(context))
+ }
+
+ @Test
+ fun testIsEnabled() {
+ val context: Context = mock()
+ val autofillManager: AutofillManager = mock()
+ doReturn(autofillManager).`when`(context).getSystemService(AutofillManager::class.java)
+ doReturn(true).`when`(autofillManager).hasEnabledAutofillServices()
+
+ assertFalse(AutofillUseCases(sdkVersion = 21).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 22).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 23).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 24).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 25).isEnabled(context))
+
+ assertTrue(AutofillUseCases(sdkVersion = 26).isEnabled(context))
+ assertTrue(AutofillUseCases(sdkVersion = 27).isEnabled(context))
+ assertTrue(AutofillUseCases(sdkVersion = 28).isEnabled(context))
+ assertTrue(AutofillUseCases(sdkVersion = 29).isEnabled(context))
+ assertTrue(AutofillUseCases(sdkVersion = 30).isEnabled(context))
+ }
+
+ @Test
+ fun testIsNotEnabled() {
+ val context: Context = mock()
+ val autofillManager: AutofillManager = mock()
+ doReturn(autofillManager).`when`(context).getSystemService(AutofillManager::class.java)
+ doReturn(false).`when`(autofillManager).hasEnabledAutofillServices()
+
+ assertFalse(AutofillUseCases(sdkVersion = 21).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 22).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 23).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 24).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 25).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 26).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 27).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 28).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 29).isEnabled(context))
+ assertFalse(AutofillUseCases(sdkVersion = 30).isEnabled(context))
+ }
+
+ @Test
+ fun testEnable() {
+ val context: Context = mock()
+
+ AutofillUseCases(sdkVersion = 21).enable(context)
+ verify(context, never()).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 22).enable(context)
+ verify(context, never()).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 23).enable(context)
+ verify(context, never()).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 24).enable(context)
+ verify(context, never()).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 25).enable(context)
+ verify(context, never()).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 26).enable(context)
+ verify(context).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 27).enable(context)
+ verify(context).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 28).enable(context)
+ verify(context).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 29).enable(context)
+ verify(context).startActivity(any())
+ reset(context)
+
+ AutofillUseCases(sdkVersion = 30).enable(context)
+ verify(context).startActivity(any())
+ reset(context)
+ }
+
+ @Test
+ fun testDisable() {
+ val context: Context = mock()
+ val autofillManager: AutofillManager = mock()
+ doReturn(autofillManager).`when`(context).getSystemService(AutofillManager::class.java)
+
+ AutofillUseCases(sdkVersion = 21).disable(context)
+ verify(autofillManager, never()).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 22).disable(context)
+ verify(autofillManager, never()).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 23).disable(context)
+ verify(autofillManager, never()).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 24).disable(context)
+ verify(autofillManager, never()).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 25).disable(context)
+ verify(autofillManager, never()).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 26).disable(context)
+ verify(autofillManager).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 27).disable(context)
+ verify(autofillManager).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 28).disable(context)
+ verify(autofillManager).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 29).disable(context)
+ verify(autofillManager).disableAutofillServices()
+ reset(autofillManager)
+
+ AutofillUseCases(sdkVersion = 30).disable(context)
+ verify(autofillManager).disableAutofillServices()
+ reset(autofillManager)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/handler/FillRequestHandlerTest.kt b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/handler/FillRequestHandlerTest.kt
new file mode 100644
index 0000000000..32e825b931
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/handler/FillRequestHandlerTest.kt
@@ -0,0 +1,235 @@
+/* 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.autofill.handler
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginsStorage
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.facts.AutofillFacts
+import mozilla.components.feature.autofill.response.fill.FillResponseBuilder
+import mozilla.components.feature.autofill.response.fill.LoginFillResponseBuilder
+import mozilla.components.feature.autofill.test.createMockStructure
+import mozilla.components.feature.autofill.ui.AbstractAutofillConfirmActivity
+import mozilla.components.feature.autofill.ui.AbstractAutofillSearchActivity
+import mozilla.components.feature.autofill.ui.AbstractAutofillUnlockActivity
+import mozilla.components.feature.autofill.verify.CredentialAccessVerifier
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.`when`
+import org.robolectric.RobolectricTestRunner
+import java.util.UUID
+
+@ExperimentalCoroutinesApi // for createTestCase
+@RunWith(RobolectricTestRunner::class)
+internal class FillRequestHandlerTest {
+ @Test
+ fun `App - Twitter - With credentials`() {
+ CollectionProcessor.withFactCollection { facts ->
+ val credentials = generateRandomLoginFor("twitter.com")
+
+ createTestCase<LoginFillResponseBuilder>(
+ filename = "fixtures/app_twitter.xml",
+ packageName = "com.twitter.android",
+ logins = mapOf(credentials),
+ assertThat = { builder ->
+ assertNotNull(builder!!)
+ assertEquals(1, builder.logins.size)
+ assertEquals(credentials.second, builder.logins[0])
+ assertEquals(false, builder.needsConfirmation)
+ },
+ )
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_AUTOFILL, component)
+ assertEquals(Action.SYSTEM, action)
+ assertEquals(AutofillFacts.Items.AUTOFILL_REQUEST, item)
+ assertEquals(2, metadata?.size)
+ assertEquals(true, metadata?.get(AutofillFacts.Metadata.HAS_MATCHING_LOGINS))
+ assertEquals(false, metadata?.get(AutofillFacts.Metadata.NEEDS_CONFIRMATION))
+ }
+ }
+ }
+
+ @Test
+ fun `App - Twitter - Without credentials`() {
+ CollectionProcessor.withFactCollection { facts ->
+ createTestCase<LoginFillResponseBuilder>(
+ filename = "fixtures/app_twitter.xml",
+ packageName = "com.twitter.android",
+ logins = emptyMap(),
+ assertThat = { builder ->
+ assertNotNull(builder!!)
+ assertEquals(0, builder.logins.size)
+ assertEquals(false, builder.needsConfirmation)
+ },
+ )
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_AUTOFILL, component)
+ assertEquals(Action.SYSTEM, action)
+ assertEquals(AutofillFacts.Items.AUTOFILL_REQUEST, item)
+ assertEquals(2, metadata?.size)
+ assertEquals(false, metadata?.get(AutofillFacts.Metadata.HAS_MATCHING_LOGINS))
+ assertEquals(false, metadata?.get(AutofillFacts.Metadata.NEEDS_CONFIRMATION))
+ }
+ }
+ }
+
+ @Test
+ fun `App - Expensify`() {
+ createTestCase<LoginFillResponseBuilder>(
+ filename = "fixtures/app_expensify.xml",
+ packageName = "org.me.mobiexpensifyg",
+ logins = mapOf(
+ generateRandomLoginFor("expensify.com"),
+ ),
+ assertThat = { builder ->
+ // Unfortunately we are not able to link the app and the website yet.
+ assertNull(builder)
+ },
+ )
+ }
+
+ @Test
+ fun `App - Facebook`() {
+ val credentials = generateRandomLoginFor("facebook.com")
+ createTestCase<LoginFillResponseBuilder>(
+ filename = "fixtures/app_facebook.xml",
+ packageName = "com.facebook.katana",
+ logins = mapOf(credentials),
+ assertThat = { builder ->
+ assertNotNull(builder!!)
+ assertEquals(1, builder.logins.size)
+ assertEquals(credentials.second, builder.logins[0])
+ },
+ )
+ }
+
+ @Test
+ fun `App - Facebook Lite`() {
+ val credentials = generateRandomLoginFor("facebook.com")
+ createTestCase<LoginFillResponseBuilder>(
+ filename = "fixtures/app_facebook_lite.xml",
+ packageName = "com.facebook.lite",
+ logins = mapOf(credentials),
+ assertThat = { builder ->
+ assertNotNull(builder!!)
+ assertEquals(1, builder.logins.size)
+ assertEquals(credentials.second, builder.logins[0])
+ },
+ )
+ }
+
+ @Test
+ fun `App - Messenger Lite`() {
+ val credentials = generateRandomLoginFor("facebook.com")
+ createTestCase<LoginFillResponseBuilder>(
+ filename = "fixtures/app_messenger_lite.xml",
+ packageName = "com.facebook.mlite",
+ logins = mapOf(credentials),
+ assertThat = { builder ->
+ assertNotNull(builder!!)
+ assertEquals(1, builder.logins.size)
+ assertEquals(credentials.second, builder.logins[0])
+ },
+ )
+ }
+
+ @Test
+ fun `Browser - Fenix Nightly - amazon-co-uk`() {
+ val credentials = generateRandomLoginFor("amazon.co.uk")
+ createTestCase<LoginFillResponseBuilder>(
+ filename = "fixtures/browser_fenix_amazon.co.uk.xml",
+ packageName = "org.mozilla.fenix",
+ logins = mapOf(credentials),
+ assertThat = { builder ->
+ assertNotNull(builder!!)
+ assertEquals(1, builder.logins.size)
+ assertEquals(credentials.second, builder.logins[0])
+ },
+ )
+ }
+
+ @Test
+ fun `Browser - WebView - gmail`() {
+ val credentials = generateRandomLoginFor("accounts.google.com")
+ createTestCase<LoginFillResponseBuilder>(
+ filename = "fixtures/browser_webview_gmail.xml",
+ packageName = "org.chromium.webview_shell",
+ logins = mapOf(credentials),
+ assertThat = { builder ->
+ assertNotNull(builder!!)
+ assertEquals(0, builder.logins.size)
+ assertEquals(false, builder.needsConfirmation)
+ },
+ )
+ }
+}
+
+@ExperimentalCoroutinesApi
+private fun <B : FillResponseBuilder> FillRequestHandlerTest.createTestCase(
+ filename: String,
+ packageName: String,
+ logins: Map<String, Login>,
+ assertThat: (B?) -> Unit,
+ canVerifyRelationship: Boolean = true,
+) = runTest {
+ val structure = createMockStructure(filename, packageName)
+
+ val storage: LoginsStorage = mock()
+ `when`(storage.getByBaseDomain(anyString())).thenAnswer { invocation ->
+ val origin = invocation.getArgument(0) as String
+ println("MockStorage: Query password for $origin")
+ logins[origin]?.let { listOf(it) } ?: emptyList<Login>()
+ }
+
+ val verifier: CredentialAccessVerifier = mock()
+ doReturn(canVerifyRelationship).`when`(verifier).hasCredentialRelationship(any(), any(), any())
+
+ val configuration = AutofillConfiguration(
+ storage = storage,
+ publicSuffixList = PublicSuffixList(testContext),
+ unlockActivity = AbstractAutofillUnlockActivity::class.java,
+ confirmActivity = AbstractAutofillConfirmActivity::class.java,
+ searchActivity = AbstractAutofillSearchActivity::class.java,
+ applicationName = "Test",
+ httpClient = mock(),
+ verifier = verifier,
+ )
+
+ val handler = FillRequestHandler(
+ testContext,
+ configuration,
+ )
+
+ val builder = handler.handle(structure)
+ @Suppress("UNCHECKED_CAST")
+ assertThat(builder as? B)
+}
+
+private fun generateRandomLoginFor(origin: String): Pair<String, Login> {
+ return origin to Login(
+ guid = UUID.randomUUID().toString(),
+ origin = origin,
+ username = "user" + UUID.randomUUID().toString(),
+ password = "password" + UUID.randomUUID().toString(),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/structure/ParsedStructureTest.kt b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/structure/ParsedStructureTest.kt
new file mode 100644
index 0000000000..c42381e5f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/structure/ParsedStructureTest.kt
@@ -0,0 +1,52 @@
+/* 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.autofill.structure
+
+import android.os.Parcel
+import android.view.autofill.AutofillId
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ParsedStructureTest {
+ @Test
+ fun `Given a ParsedStructure WHEN parcelling and unparcelling it THEN get the same object`() {
+ // AutofillId constructor is private but it can be constructed from a parcel.
+ // Use this route instead of mocking to avoid errors like below:
+ // org.robolectric.shadows.ShadowParcel$UnreliableBehaviorError: Looking for Integer at position 72, found String
+ val usernameIdAutofillIdParcel = Parcel.obtain().apply {
+ writeInt(1) // viewId
+ writeInt(3) // flags
+ writeInt(78) // virtualIntId
+ setDataPosition(0) // be a good citizen
+ }
+ val passwordIdAutofillParcel = Parcel.obtain().apply {
+ writeInt(11) // viewId
+ writeInt(31) // flags
+ writeInt(781) // virtualIntId
+ setDataPosition(0) // be a good citizen
+ }
+ val parsedStructure = ParsedStructure(
+ usernameId = AutofillId.CREATOR.createFromParcel(usernameIdAutofillIdParcel),
+ passwordId = AutofillId.CREATOR.createFromParcel(passwordIdAutofillParcel),
+ packageName = "test",
+ webDomain = "https://mozilla.org",
+ )
+
+ // Write the object in a new Parcel.
+ val parcel = Parcel.obtain()
+ parsedStructure.writeToParcel(parcel, 0)
+
+ // Reset Parcel r/w position to be read from beginning afterwards.
+ parcel.setDataPosition(0)
+
+ // Reconstruct the original object from the Parcel.
+ val result = ParsedStructure(parcel)
+
+ assertEquals(parsedStructure, result)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/DOMNavigator.kt b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/DOMNavigator.kt
new file mode 100644
index 0000000000..86f8517850
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/DOMNavigator.kt
@@ -0,0 +1,133 @@
+/* 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.autofill.test
+
+import android.view.autofill.AutofillId
+import mozilla.components.feature.autofill.structure.AutofillNodeNavigator
+import mozilla.components.feature.autofill.structure.ParsedStructure
+import mozilla.components.support.test.mock
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+import java.io.File
+import java.util.WeakHashMap
+import javax.xml.parsers.DocumentBuilderFactory
+
+/**
+ * Alternative [AutofillNodeNavigator] implementation for reading a captured view structure from an
+ * XML file for testing purposes.
+ */
+internal class DOMNavigator(
+ file: File,
+ override val activityPackageName: String,
+) : AutofillNodeNavigator<Element, AutofillId> {
+
+ override fun currentText(node: Element): String? {
+ return node.getAttribute("autofillValue")
+ }
+
+ private val document: Document
+
+ init {
+ val db = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ file.inputStream().use {
+ document = db.parse(it)
+ }
+ }
+
+ override val rootNodes: List<Element>
+ get() = listOf(document.documentElement)
+
+ override fun childNodes(node: Element): List<Element> {
+ val children = node.childNodes
+ return (0 until children.length)
+ .map { children.item(it) }
+ .filter { it is Element }
+ .map { it as Element }
+ }
+
+ override fun clues(node: Element): Iterable<CharSequence> {
+ val attributes = node.attributes
+ return (0 until attributes.length)
+ .map { attributes.item(it) }
+ .mapNotNull { if (it.nodeName != "hint") it.nodeValue else null }
+ }
+
+ override fun autofillId(node: Element): AutofillId? {
+ val rawId = if (isEditText(node) || isHtmlInputField(node)) {
+ attr(node, "autofillId") ?: clues(node).joinToString("|")
+ } else {
+ null
+ }
+
+ return rawId?.let { getOrCreateAutofillIdMock(it) }
+ }
+
+ override fun isEditText(node: Element): Boolean =
+ tagName(node) == "EditText" || (inputType(node) and AutofillNodeNavigator.editTextMask) > 0
+
+ override fun isHtmlInputField(node: Element) = tagName(node) == "input"
+
+ private fun tagName(node: Element) = node.tagName
+
+ override fun isHtmlForm(node: Element): Boolean = node.tagName == "form"
+
+ fun attr(node: Element, name: String) = node.attributes.getNamedItem(name)?.nodeValue
+
+ override fun isFocused(node: Element) = attr(node, "focus") == "true"
+
+ override fun isVisible(node: Element) = attr(node, "visibility")?.let { it == "0" } ?: true
+
+ override fun packageName(node: Element) = attr(node, "idPackage")
+
+ override fun webDomain(node: Element) = attr(node, "webDomain")
+
+ override fun isButton(node: Element): Boolean {
+ when (node.tagName) {
+ "Button" -> return true
+ "button" -> return true
+ }
+
+ return when (attr(node, "type")) {
+ "submit" -> true
+ "button" -> true
+ else -> false
+ }
+ }
+
+ override fun inputType(node: Element): Int =
+ attr(node, "inputType")?.let {
+ Integer.parseInt(it, 16)
+ } ?: 0
+
+ override fun build(
+ usernameId: AutofillId?,
+ passwordId: AutofillId?,
+ webDomain: String?,
+ packageName: String,
+ ): ParsedStructure {
+ return ParsedStructure(
+ usernameId,
+ passwordId,
+ webDomain,
+ packageName,
+ )
+ }
+}
+
+private val autofillIdMapping = WeakHashMap<AutofillId, String>()
+
+private fun getOrCreateAutofillIdMock(id: String): AutofillId {
+ val existingEntry = autofillIdMapping.entries.firstOrNull { entry -> entry.value == id }
+ if (existingEntry != null) {
+ return existingEntry.key
+ }
+
+ val autofillId: AutofillId = mock()
+ autofillIdMapping[autofillId] = id
+ return autofillId
+}
+
+internal val AutofillId.originalId: String
+ get() = autofillIdMapping[this] ?: throw AssertionError("Unknown AutofillId instance")
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/MockStructure.kt b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/MockStructure.kt
new file mode 100644
index 0000000000..416a07ef67
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/MockStructure.kt
@@ -0,0 +1,32 @@
+/* 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.autofill.test
+
+import android.view.autofill.AutofillId
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.feature.autofill.handler.FillRequestHandlerTest
+import mozilla.components.feature.autofill.structure.AutofillNodeNavigator
+import mozilla.components.feature.autofill.structure.RawStructure
+import java.io.File
+
+@ExperimentalCoroutinesApi
+internal fun FillRequestHandlerTest.createMockStructure(filename: String, packageName: String): RawStructure {
+ val classLoader = javaClass.classLoader ?: throw RuntimeException("No class loader")
+ val resource = classLoader.getResource(filename) ?: throw RuntimeException("Resource not found")
+ val file = File(resource.path)
+
+ return MockStructure(packageName, file)
+}
+
+private class MockStructure(
+ private val packageName: String,
+ private val file: File,
+) : RawStructure {
+ override val activityPackageName: String = packageName
+
+ override fun createNavigator(): AutofillNodeNavigator<*, AutofillId> {
+ return DOMNavigator(file, packageName)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_expensify.xml b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_expensify.xml
new file mode 100644
index 0000000000..0db81b3602
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_expensify.xml
@@ -0,0 +1,46 @@
+<?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/. -->
+<FrameLayout>
+ <LinearLayout idEntry="action_bar_root" idPackage="org.me.mobiexpensifyg" idType="id">
+ <View idEntry="action_mode_bar_stub" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ <RelativeLayout idEntry="content" idPackage="org.me.mobiexpensifyg" idType="id">
+ <ImageSwitcher idEntry="background_image_view" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ <FrameLayout idEntry="form_container" idPackage="org.me.mobiexpensifyg" idType="id">
+ <LinearLayout idEntry="text_input_layout" idPackage="org.me.mobiexpensifyg" idType="id">
+ <EditText idEntry="edit_text" idPackage="org.me.mobiexpensifyg" idType="id" hint="Email" inputType="21"/>
+ </LinearLayout>
+ <View idEntry="underline_view" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ <TextView idEntry="error_text" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ <TextView idEntry="cancel_text" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ <FrameLayout idEntry="submit_button" idPackage="org.me.mobiexpensifyg" idType="id">
+ <FrameLayout idEntry="container" idPackage="org.me.mobiexpensifyg" idType="id">
+ <TextView idEntry="title_text" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ <ProgressBar idEntry="progress_bar" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ </FrameLayout>
+ </FrameLayout>
+ </FrameLayout>
+ <LinearLayout idEntry="navigation_element_container" idPackage="org.me.mobiexpensifyg" idType="id">
+ <TextSwitcher idEntry="navigation_element_switcher" idPackage="org.me.mobiexpensifyg" idType="id">
+ <TextView idEntry="navigation_element_text1" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ <TextView idEntry="navigation_element_text2" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ </TextSwitcher>
+ </LinearLayout>
+ <TextSwitcher idEntry="expensify_motto_switcher" idPackage="org.me.mobiexpensifyg" idType="id">
+ <TextView/>
+ <TextView/>
+ </TextSwitcher>
+ <View idEntry="footer_separator" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ <TextSwitcher idEntry="expensify_ambassador_name_switcher" idPackage="org.me.mobiexpensifyg" idType="id">
+ <TextView/>
+ <TextView/>
+ </TextSwitcher>
+ <TextSwitcher idEntry="expensify_customer_since_switcher" idPackage="org.me.mobiexpensifyg" idType="id">
+ <TextView/>
+ <TextView/>
+ </TextSwitcher>
+ <TextView idEntry="privacy_policy_text" idPackage="org.me.mobiexpensifyg" idType="id"/>
+ </RelativeLayout>
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_facebook.xml b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_facebook.xml
new file mode 100644
index 0000000000..35cb055f52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_facebook.xml
@@ -0,0 +1,14 @@
+<?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/. -->
+<FrameLayout>
+ <LinearLayout idEntry="(name removed)" idPackage="com.facebook.katana" idType="id">
+ <FrameLayout idEntry="(name removed)" idPackage="com.facebook.katana" idType="id">
+ <FrameLayout idEntry="(name removed)" idPackage="com.facebook.katana" idType="id">
+ <EditText inputType="21" autofillId="autofillId-username"/>
+ <EditText inputType="81"/>
+ </FrameLayout>
+ </FrameLayout>
+ </LinearLayout>
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_facebook_lite.xml b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_facebook_lite.xml
new file mode 100644
index 0000000000..ceae242125
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_facebook_lite.xml
@@ -0,0 +1,59 @@
+<?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/. -->
+<FrameLayout>
+ <LinearLayout>
+ <View idEntry="action_mode_bar_stub" idPackage="android" idType="id"/>
+ <FrameLayout idEntry="content" idPackage="android" idType="id">
+ <FrameLayout idEntry="main_layout" idPackage="com.facebook.lite" idType="id">
+ <ImageView idEntry="image_overlay_under_content" idPackage="com.facebook.lite" idType="id"/>
+ <ViewGroup>
+ <ViewGroup/>
+ <ViewGroup/>
+ <MultiAutoCompleteTextView autofillValue="" inputType="80001" autofillId="autofillId-username"/>
+ <ViewGroup/>
+ <MultiAutoCompleteTextView inputType="80081"/>
+ <ViewGroup>
+ <ViewGroup/>
+ </ViewGroup>
+ <ViewGroup>
+ <ViewGroup/>
+ </ViewGroup>
+ <ViewGroup/>
+ <ViewGroup>
+ <ViewGroup/>
+ </ViewGroup>
+ <ViewGroup>
+ <ViewGroup/>
+ </ViewGroup>
+ <ViewGroup/>
+ <ViewGroup/>
+ <ViewGroup>
+ <ViewGroup/>
+ </ViewGroup>
+ <ViewGroup>
+ <ViewGroup/>
+ </ViewGroup>
+ <ViewGroup>
+ <ViewGroup/>
+ </ViewGroup>
+ <View/>
+ <View/>
+ </ViewGroup>
+ <ImageView idEntry="image_overlay_over_content" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="webview_stub" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="inline_textbox_stub" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="floating_textbox_stub" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="screen_transition_loading_view_stub" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="zoomview_stub" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="videoview_stub" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="dummy_surfaceview" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="livestreamingview_stub" idPackage="com.facebook.lite" idType="id"/>
+ <View idEntry="snack_bar_view_stub" idPackage="com.facebook.lite" idType="id"/>
+ </FrameLayout>
+ </FrameLayout>
+ </LinearLayout>
+ <View idEntry="navigationBarBackground" idPackage="android" idType="id"/>
+ <View idEntry="statusBarBackground" idPackage="android" idType="id"/>
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_messenger_lite.xml b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_messenger_lite.xml
new file mode 100644
index 0000000000..946a2bdc0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_messenger_lite.xml
@@ -0,0 +1,29 @@
+<?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/. -->
+<FrameLayout>
+ <LinearLayout idEntry="action_bar_root" idPackage="com.facebook.mlite" idType="id">
+ <View idEntry="action_mode_bar_stub" idPackage="com.facebook.mlite" idType="id"/>
+ <ScrollView idEntry="content_view" idPackage="com.facebook.mlite" idType="id">
+ <LinearLayout idEntry="root_view" idPackage="com.facebook.mlite" idType="id">
+ <LinearLayout idEntry="sso_confirmation_form" idPackage="com.facebook.mlite" idType="id"/>
+ <LinearLayout idEntry="username_password_form" idPackage="com.facebook.mlite" idType="id">
+ <TextView idEntry="login_message" idPackage="com.facebook.mlite" idType="id"/>
+ <LinearLayout idEntry="login_username_text" idPackage="com.facebook.mlite" idType="id">
+ <EditText inputType="1" autofillId="autofillId-username"/>
+ </LinearLayout>
+ <LinearLayout idEntry="login_password_edit_text" idPackage="com.facebook.mlite" idType="id">
+ <EditText inputType="81"/>
+ </LinearLayout>
+ <TextView idEntry="login_login_button" idPackage="com.facebook.mlite" idType="id"/>
+ <TextView idEntry="create_account_button" idPackage="com.facebook.mlite" idType="id"/>
+ <TextView idEntry="bottom_create_account_message" idPackage="com.facebook.mlite" idType="id"/>
+ <TextView idEntry="login_forgot_password_button" idPackage="com.facebook.mlite" idType="id"/>
+ </LinearLayout>
+ <LinearLayout idEntry="login_approval_form" idPackage="com.facebook.mlite" idType="id"/>
+ <FrameLayout idEntry="progress_screen" idPackage="com.facebook.mlite" idType="id"/>
+ </LinearLayout>
+ </ScrollView>
+ </LinearLayout>
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_twitter.xml b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_twitter.xml
new file mode 100644
index 0000000000..7d092fa1d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/app_twitter.xml
@@ -0,0 +1,51 @@
+<?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/. -->
+<FrameLayout>
+ <LinearLayout>
+ <View idEntry="action_mode_bar_stub" idPackage="android" idType="id" />
+ <FrameLayout>
+ <FrameLayout idEntry="action_bar_root" idPackage="com.twitter.android" idType="id">
+ <FrameLayout idEntry="content" idPackage="android" idType="id">
+ <LinearLayout>
+ <LinearLayout idEntry="toolbar_container" idPackage="com.twitter.android" idType="id">
+ <FrameLayout idEntry="status_message_container" idPackage="com.twitter.android" idType="id" />
+ <ViewGroup idEntry="toolbar" idPackage="com.twitter.android" idType="id">
+ <ViewGroup>
+ <ImageView />
+ </ViewGroup>
+ <TextView />
+ <RelativeLayout>
+ <ImageView />
+ <Button idEntry="signup" idPackage="com.twitter.android" idType="id" />
+ </RelativeLayout>
+ </ViewGroup>
+ </LinearLayout>
+ <ScrollView>
+ <LinearLayout>
+ <LinearLayout idEntry="signup_header" idPackage="com.twitter.android" idType="id">
+ <TextView idEntry="header_title" idPackage="com.twitter.android" idType="id" />
+ <TextView idEntry="header_subtitle" idPackage="com.twitter.android" idType="id" />
+ </LinearLayout>
+ <LinearLayout idEntry="login_form" idPackage="com.twitter.android" idType="id">
+ <EditText idEntry="login_identifier" idPackage="com.twitter.android" idType="id" autofillValue="" autofillId="autofillId-username" />
+ <EditText idEntry="login_password" idPackage="com.twitter.android" idType="id" />
+ <TextView idEntry="password_reset" idPackage="com.twitter.android" idType="id" />
+ </LinearLayout>
+ </LinearLayout>
+ </ScrollView>
+ <FrameLayout>
+ <View />
+ <Button idEntry="login_login" idPackage="com.twitter.android" idType="id" />
+ </FrameLayout>
+ <WebView idEntry="js_inst" idPackage="com.twitter.android" idType="id" />
+ </LinearLayout>
+ </FrameLayout>
+ <View idEntry="action_mode_bar_stub" idPackage="com.twitter.android" idType="id" />
+ </FrameLayout>
+ </FrameLayout>
+ </LinearLayout>
+ <View idEntry="navigationBarBackground" idPackage="android" idType="id" />
+ <View idEntry="statusBarBackground" idPackage="android" idType="id" />
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/browser_fenix_amazon.co.uk.xml b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/browser_fenix_amazon.co.uk.xml
new file mode 100644
index 0000000000..0faac1f20a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/browser_fenix_amazon.co.uk.xml
@@ -0,0 +1,68 @@
+<?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/. -->
+<FrameLayout>
+ <LinearLayout idEntry="action_bar_root" idPackage="org.mozilla.firefox" idType="id">
+ <View idEntry="action_mode_bar_stub" idPackage="org.mozilla.firefox" idType="id"/>
+ <FrameLayout idEntry="container" idPackage="org.mozilla.firefox" idType="id">
+ <ViewGroup idEntry="browserLayout" idPackage="org.mozilla.firefox" idType="id">
+ <ViewGroup idEntry="swipeRefresh" idPackage="org.mozilla.firefox" idType="id">
+ <FrameLayout idEntry="engineView" idPackage="org.mozilla.firefox" idType="id">
+ <form webDomain="www.amazon.co.uk" data-enable-mobile-account-js="true" action="https://www.amazon.co.uk/ap/register" method="post" data-fwcim-id="369de96d" novalidate="" id="ap_register_form" name="register" class="ap_ango_default fwcim-form auth-validate-form auth-clearable-form">
+ <input webDomain="www.amazon.co.uk" name="appActionToken" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" name="appAction" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" name="openid.return_to" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" name="prevRID" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" name="workflowState" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" id="auth-country-picker" name="countryCode" type="hidden" class="auth-country-picker" disabled="disabled"/>
+ <input webDomain="www.amazon.co.uk" autofillHints="emailAddress" inputType="21" autocorrect="off" id="ap_email" name="email" type="email" maxlength="64" placeholder="Mobile number or email" autocapitalize="off"/>
+ <input webDomain="www.amazon.co.uk" autofillHints="password" inputType="e1" autocorrect="off" autocomplete="off" id="ap_password" name="password" type="password" maxlength="1024" placeholder="Create a password" autocapitalize="off"/>
+ <input webDomain="www.amazon.co.uk" id="ap_show_password_checked" name="showPasswordChecked" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" id="auth-register-show-password-checkbox" name="" type="checkbox" checked=""/>
+ <input webDomain="www.amazon.co.uk" id="continue" type="submit" class="a-button-input" aria-labelledby="auth-continue-announce"/>
+ </form>
+ <form webDomain="www.amazon.co.uk" action="https://www.amazon.co.uk/ap/signin" method="post" data-fwcim-id="4a0d320d" novalidate="" id="ap_login_form" name="signIn" class="auth-validate-form fwcim-form auth-clearable-form">
+ <input webDomain="www.amazon.co.uk" name="appActionToken" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" name="appAction" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" name="openid.return_to" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" name="prevRID" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" name="workflowState" type="hidden"/>
+ <input webDomain="www.amazon.co.uk" autofillHints="emailAddress" inputType="21" autocorrect="off" id="ap_email_login" name="email" type="email" maxlength="128" placeholder="Email (phone for mobile accounts)" autocapitalize="off" focus="true" autofillId="autofillId-username"/>
+ <input webDomain="www.amazon.co.uk" autofillHints="password" inputType="e1" id="ap-credential-autofill-hint" name="password" type="password" maxlength="1024" autofillId="autofillId-password"/>
+ <input webDomain="www.amazon.co.uk" id="continue" type="submit" class="a-button-input" aria-labelledby="continue-announce"/>
+ </form>
+ </FrameLayout>
+ </ViewGroup>
+ <FrameLayout idEntry="nestedScrollQuickAction" idPackage="org.mozilla.firefox" idType="id">
+ <ViewGroup idEntry="quick_action_sheet" idPackage="org.mozilla.firefox" idType="id">
+ <ViewGroup idEntry="quick_action_sheet" idPackage="org.mozilla.firefox" idType="id">
+ <View idEntry="quick_action_sheet_faded_handle" idPackage="org.mozilla.firefox" idType="id"/>
+ <LinearLayout idEntry="quick_action_buttons_layout" idPackage="org.mozilla.firefox" idType="id">
+ <Button idEntry="quick_action_share" idPackage="org.mozilla.firefox" idType="id"/>
+ <Button idEntry="quick_action_downloads" idPackage="org.mozilla.firefox" idType="id"/>
+ <Button idEntry="quick_action_bookmark" idPackage="org.mozilla.firefox" idType="id"/>
+ <Button idEntry="quick_action_appearance" idPackage="org.mozilla.firefox" idType="id"/>
+ <Button idEntry="quick_action_open_app_link" idPackage="org.mozilla.firefox" idType="id"/>
+ <Button idEntry="quick_action_read" idPackage="org.mozilla.firefox" idType="id"/>
+ </LinearLayout>
+ <View idEntry="quick_action_overlay" idPackage="org.mozilla.firefox" idType="id"/>
+ </ViewGroup>
+ </ViewGroup>
+ </FrameLayout>
+ <View idEntry="stubFindInPage" idPackage="org.mozilla.firefox" idType="id"/>
+ <ViewGroup idEntry="toolbar" idPackage="org.mozilla.firefox" idType="id">
+ <TextView idEntry="mozac_browser_toolbar_title_view" idPackage="org.mozilla.firefox" idType="id"/>
+ <TextView idEntry="mozac_browser_toolbar_url_view" idPackage="org.mozilla.firefox" idType="id" hint="Search or enter address"/>
+ <FrameLayout idEntry="counter_root" idPackage="org.mozilla.firefox" idType="id">
+ <TextView idEntry="counter_text" idPackage="org.mozilla.firefox" idType="id"/>
+ </FrameLayout>
+ <EditText idEntry="mozac_browser_toolbar_edit_url_view" idPackage="org.mozilla.firefox" idType="id" hint="Search or enter address" inputType="11"/>
+ <ImageView idEntry="mozac_browser_toolbar_clear_view" idPackage="org.mozilla.firefox" idType="id"/>
+ </ViewGroup>
+ <ViewGroup idEntry="readerViewControlsBar" idPackage="org.mozilla.firefox" idType="id"/>
+ </ViewGroup>
+ </FrameLayout>
+ <ViewGroup idEntry="navigationToolbar" idPackage="org.mozilla.firefox" idType="id"/>
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/browser_webview_gmail.xml b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/browser_webview_gmail.xml
new file mode 100644
index 0000000000..6fb4318b23
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/resources/fixtures/browser_webview_gmail.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<FrameLayout>
+ <TextView idEntry="title" idPackage="android" idType="id" />
+ <LinearLayout idEntry="container" idPackage="org.chromium.webview_shell" idType="id">
+ <EditText idEntry="url_field" idPackage="org.chromium.webview_shell" idType="id" />
+ <form webDomain="accounts.google.com">
+ <input autofillHints="username" name="identifier" type="email" label="Forgot email?" id="identifierId" autofillId="autofillId-username"/>
+ <input name="hiddenPassword" type="password" label="Forgot email?" />
+ <input autofillHints="off" name="ca" type="text" label="Forgot email?" />
+ </form>
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/autofill/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/autofill/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/autofill/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/autofill/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/autofill/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/autofill/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/awesomebar/README.md b/mobile/android/android-components/components/feature/awesomebar/README.md
new file mode 100644
index 0000000000..66a64860b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Awesomebar
+
+A component that connects a [concept-awesomebar](components/concept/awesomebar/README.md) implementation to a [concept-toolbar](components/concept/toolbar/README.md) implementation and provides implementations of various suggestion providers.
+
+## Usage
+
+### 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-awesomebar:{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/awesomebar/build.gradle b/mobile/android/android-components/components/feature/awesomebar/build.gradle
new file mode 100644
index 0000000000..9476f63a3c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/build.gradle
@@ -0,0 +1,59 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.awesomebar'
+}
+
+dependencies {
+ implementation project(':concept-awesomebar')
+ implementation project(':concept-fetch')
+ implementation project(':concept-engine')
+ implementation project(':concept-toolbar')
+ implementation project(':concept-storage')
+
+ implementation project(':browser-state')
+ implementation project(':browser-storage-sync')
+ implementation project(':browser-icons')
+
+ implementation project(':feature-tabs')
+ implementation project(':feature-session')
+ implementation project(':feature-search')
+
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ testImplementation project(':support-test')
+ testImplementation project(':lib-fetch-httpurlconnection')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/awesomebar/proguard-rules.pro b/mobile/android/android-components/components/feature/awesomebar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/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/awesomebar/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/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/awesomebar/src/main/java/mozilla/components/feature/awesomebar/AwesomeBarFeature.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/AwesomeBarFeature.kt
new file mode 100644
index 0000000000..c2b2c872f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/AwesomeBarFeature.kt
@@ -0,0 +1,257 @@
+/* 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.awesomebar
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.view.View
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
+import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
+import mozilla.components.feature.awesomebar.provider.SearchActionProvider
+import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
+import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+
+/**
+ * Connects an [AwesomeBar] with a [Toolbar] and allows adding multiple [AwesomeBar.SuggestionProvider] implementations.
+ */
+class AwesomeBarFeature(
+ private val awesomeBar: AwesomeBar,
+ private val toolbar: Toolbar,
+ private val engineView: EngineView? = null,
+ private val icons: BrowserIcons? = null,
+ private val indicatorIcon: Drawable? = null,
+ onEditStart: (() -> Unit)? = null,
+ onEditComplete: (() -> Unit)? = null,
+) {
+ init {
+ toolbar.setOnEditListener(
+ ToolbarEditListener(
+ awesomeBar,
+ onEditStart,
+ onEditComplete,
+ ::showAwesomeBar,
+ ::hideAwesomeBar,
+ ),
+ )
+
+ awesomeBar.setOnStopListener { toolbar.displayMode() }
+ awesomeBar.setOnEditSuggestionListener(toolbar::setSearchTerms)
+ }
+
+ /**
+ * Add a [AwesomeBar.SuggestionProvider] for "Open tabs" to the [AwesomeBar].
+ */
+ fun addSessionProvider(
+ resources: Resources,
+ store: BrowserStore,
+ selectTabUseCase: TabsUseCases.SelectTabUseCase,
+ ): AwesomeBarFeature {
+ val provider = SessionSuggestionProvider(resources, store, selectTabUseCase, icons, indicatorIcon)
+ awesomeBar.addProviders(provider)
+ return this
+ }
+
+ /**
+ * Adds a [AwesomeBar.SuggestionProvider] for search engine suggestions to the [AwesomeBar].
+ *
+ * @param searchEngine The search engine to request suggestions from.
+ * @param searchUseCase The use case to invoke for searches.
+ * @param fetchClient The HTTP client for requesting suggestions from the search engine.
+ * @param limit The maximum number of suggestions that should be returned. It needs to be >= 1.
+ * @param mode Whether to return a single search suggestion (with chips) or one suggestion per item.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored search suggestion URL.
+ * @param filterExactMatch If true filters out suggestions that exactly match the entered text.
+ */
+ fun addSearchProvider(
+ searchEngine: SearchEngine,
+ searchUseCase: SearchUseCases.SearchUseCase,
+ fetchClient: Client,
+ limit: Int = 15,
+ mode: SearchSuggestionProvider.Mode = SearchSuggestionProvider.Mode.SINGLE_SUGGESTION,
+ engine: Engine? = null,
+ filterExactMatch: Boolean = false,
+ ): AwesomeBarFeature {
+ awesomeBar.addProviders(
+ SearchSuggestionProvider(
+ searchEngine,
+ searchUseCase,
+ fetchClient,
+ limit,
+ mode,
+ engine,
+ filterExactMatch = filterExactMatch,
+ ),
+ )
+ return this
+ }
+
+ /**
+ * Adds a [AwesomeBar.SuggestionProvider] for search engine suggestions to the [AwesomeBar].
+ * If the default search engine is to be used for fetching search engine suggestions then
+ * this method is preferable over [addSearchProvider], as it will read the search engine from
+ * the provided [BrowserStore].
+ *
+ * @param context the activity or application context, required to load search engines.
+ * @param store The [BrowserStore] to lookup search engines from.
+ * @param searchUseCase The use case to invoke for searches.
+ * @param fetchClient The HTTP client for requesting suggestions from the search engine.
+ * @param limit The maximum number of suggestions that should be returned. It needs to be >= 1.
+ * @param mode Whether to return a single search suggestion (with chips) or one suggestion per item.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored search suggestion URL.
+ * @param filterExactMatch If true filters out suggestions that exactly match the entered text.
+ */
+ fun addSearchProvider(
+ context: Context,
+ store: BrowserStore,
+ searchUseCase: SearchUseCases.SearchUseCase,
+ fetchClient: Client,
+ limit: Int = 15,
+ mode: SearchSuggestionProvider.Mode = SearchSuggestionProvider.Mode.SINGLE_SUGGESTION,
+ engine: Engine? = null,
+ filterExactMatch: Boolean = false,
+ ): AwesomeBarFeature {
+ awesomeBar.addProviders(
+ SearchSuggestionProvider(
+ context,
+ store,
+ searchUseCase,
+ fetchClient,
+ limit,
+ mode,
+ engine,
+ filterExactMatch = filterExactMatch,
+ ),
+ )
+ return this
+ }
+
+ /**
+ * Adds an [AwesomeBar.SuggestionProvider] implementation that always returns a suggestion that
+ * mirrors the entered text and invokes a search with the given [SearchEngine] if clicked.
+ *
+ * @param store The [BrowserStore] to read the default search engine from.
+ * @param searchUseCase The use case to invoke for searches.
+ * @param icon The image to display next to the result. If not specified, the engine icon is used.
+ * @param showDescription whether or not to add the search engine name as description.
+ */
+ fun addSearchActionProvider(
+ store: BrowserStore,
+ searchUseCase: SearchUseCases.SearchUseCase,
+ icon: Bitmap? = null,
+ showDescription: Boolean = false,
+ ): AwesomeBarFeature {
+ awesomeBar.addProviders(
+ SearchActionProvider(
+ store,
+ searchUseCase,
+ icon,
+ showDescription,
+ ),
+ )
+ return this
+ }
+
+ /**
+ * Add a [AwesomeBar.SuggestionProvider] for browsing history to the [AwesomeBar].
+ *
+ * @param historyStorage an instance of the [HistoryStorage] used to query matching history.
+ * @param loadUrlUseCase the use case invoked to load the url when the user clicks on the suggestion.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored suggestion URL.
+ * @param maxNumberOfSuggestions optional parameter to specify the maximum number of returned suggestions.
+ * Zero or a negative value here means the default number of history suggestions will be returned.
+ */
+ fun addHistoryProvider(
+ historyStorage: HistoryStorage,
+ loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ engine: Engine? = null,
+ maxNumberOfSuggestions: Int = -1,
+ ): AwesomeBarFeature {
+ awesomeBar.addProviders(
+ if (maxNumberOfSuggestions <= 0) {
+ HistoryStorageSuggestionProvider(historyStorage, loadUrlUseCase, icons, engine)
+ } else {
+ HistoryStorageSuggestionProvider(historyStorage, loadUrlUseCase, icons, engine, maxNumberOfSuggestions)
+ },
+ )
+ return this
+ }
+
+ /**
+ * Add a [AwesomeBar.SuggestionProvider] for clipboard items to the [AwesomeBar].
+ *
+ * @param context the activity or application context, required to look up the clipboard manager.
+ * @param loadUrlUseCase the use case invoked to load the url when
+ * the user clicks on the suggestion.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored suggestion URL.
+ */
+ fun addClipboardProvider(
+ context: Context,
+ loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ engine: Engine? = null,
+ ): AwesomeBarFeature {
+ awesomeBar.addProviders(ClipboardSuggestionProvider(context, loadUrlUseCase, engine = engine))
+ return this
+ }
+
+ private fun showAwesomeBar() {
+ engineView?.asView()?.visibility = View.GONE
+ awesomeBar.asView().visibility = View.VISIBLE
+ }
+
+ private fun hideAwesomeBar() {
+ awesomeBar.asView().visibility = View.GONE
+ engineView?.asView()?.visibility = View.VISIBLE
+ }
+}
+
+internal class ToolbarEditListener(
+ private val awesomeBar: AwesomeBar,
+ private val onEditStart: (() -> Unit)? = null,
+ private val onEditComplete: (() -> Unit)? = null,
+ private val showAwesomeBar: () -> Unit,
+ private val hideAwesomeBar: () -> Unit,
+) : Toolbar.OnEditListener {
+ private var inputStarted = false
+
+ override fun onTextChanged(text: String) {
+ if (inputStarted) {
+ awesomeBar.onInputChanged(text)
+ }
+ }
+
+ override fun onStartEditing() {
+ onEditStart?.invoke() ?: showAwesomeBar()
+ awesomeBar.onInputStarted()
+ inputStarted = true
+ }
+
+ override fun onStopEditing() {
+ onEditComplete?.invoke() ?: hideAwesomeBar()
+ awesomeBar.onInputCancelled()
+ inputStarted = false
+ }
+
+ override fun onCancelEditing(): Boolean {
+ return true
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/facts/AwesomeBarFacts.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/facts/AwesomeBarFacts.kt
new file mode 100644
index 0000000000..419cb392b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/facts/AwesomeBarFacts.kt
@@ -0,0 +1,92 @@
+/* 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.awesomebar.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the AwesomeBar feature.
+ */
+class AwesomeBarFacts {
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val BOOKMARK_SUGGESTION_CLICKED = "bookmark_suggestion_clicked"
+ const val CLIPBOARD_SUGGESTION_CLICKED = "clipboard_suggestion_clicked"
+ const val HISTORY_SUGGESTION_CLICKED = "history_suggestion_clicked"
+ const val SEARCH_ACTION_CLICKED = "search_action_clicked"
+ const val SEARCH_SUGGESTION_CLICKED = "search_suggestion_clicked"
+ const val OPENED_TAB_SUGGESTION_CLICKED = "opened_tab_suggestion_clicked"
+ const val SEARCH_TERM_SUGGESTION_CLICKED = "search_term_suggestion_clicked"
+ }
+}
+
+private fun emitAwesomebarFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitBookmarkSuggestionClickedFact() {
+ emitAwesomebarFact(
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.BOOKMARK_SUGGESTION_CLICKED,
+ )
+}
+
+internal fun emitClipboardSuggestionClickedFact() {
+ emitAwesomebarFact(
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.CLIPBOARD_SUGGESTION_CLICKED,
+ )
+}
+
+internal fun emitHistorySuggestionClickedFact() {
+ emitAwesomebarFact(
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.HISTORY_SUGGESTION_CLICKED,
+ )
+}
+
+internal fun emitSearchActionClickedFact() {
+ emitAwesomebarFact(
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.SEARCH_ACTION_CLICKED,
+ )
+}
+
+internal fun emitSearchSuggestionClickedFact() {
+ emitAwesomebarFact(
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.SEARCH_SUGGESTION_CLICKED,
+ )
+}
+
+internal fun emitOpenTabSuggestionClickedFact() {
+ emitAwesomebarFact(
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.OPENED_TAB_SUGGESTION_CLICKED,
+ )
+}
+
+internal fun emitSearchTermSuggestionClickedFact() {
+ emitAwesomebarFact(
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.SEARCH_TERM_SUGGESTION_CLICKED,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProvider.kt
new file mode 100644
index 0000000000..73252c1560
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProvider.kt
@@ -0,0 +1,133 @@
+/* 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.awesomebar.provider
+
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.concept.storage.BookmarkNode
+import mozilla.components.concept.storage.BookmarksStorage
+import mozilla.components.feature.awesomebar.facts.emitBookmarkSuggestionClickedFact
+import mozilla.components.feature.session.SessionUseCases
+import java.util.UUID
+
+/**
+ * Return up to 20 bookmarks suggestions by default.
+ */
+@VisibleForTesting
+internal const val BOOKMARKS_SUGGESTION_LIMIT = 20
+
+/**
+ * Default suggestions limit multiplier when needing to filter results by an external url filter.
+ */
+@VisibleForTesting
+internal const val BOOKMARKS_RESULTS_TO_FILTER_SCALE_FACTOR = 10
+
+/**
+ * A [AwesomeBar.SuggestionProvider] implementation that provides suggestions based on the bookmarks
+ * stored in the [BookmarksStorage].
+ *
+ * @property bookmarksStorage and instance of the [BookmarksStorage] used
+ * to query matching bookmarks.
+ * @property loadUrlUseCase the use case invoked to load the url when the
+ * user clicks on the suggestion.
+ * @property icons optional instance of [BrowserIcons] to load fav icons
+ * for bookmarked URLs.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored suggestion URL.
+ * @param showEditSuggestion optional parameter to specify if the suggestion should show the edit button
+ * @param suggestionsHeader optional parameter to specify if the suggestion should have a header
+ * @param resultsUriFilter Optional predicate to filter matching suggestions by URL.
+ */
+class BookmarksStorageSuggestionProvider(
+ @get:VisibleForTesting internal val bookmarksStorage: BookmarksStorage,
+ private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ private val icons: BrowserIcons? = null,
+ private val indicatorIcon: Drawable? = null,
+ private val engine: Engine? = null,
+ @get:VisibleForTesting val showEditSuggestion: Boolean = true,
+ private val suggestionsHeader: String? = null,
+ @get:VisibleForTesting val resultsUriFilter: ((Uri) -> Boolean)? = null,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
+ bookmarksStorage.cancelReads(text)
+
+ if (text.isEmpty()) {
+ return emptyList()
+ }
+
+ val suggestions = when (resultsUriFilter) {
+ null -> getBookmarksSuggestions(text)
+ else -> getFilteredBookmarksSuggestions(text, resultsUriFilter)
+ }
+
+ suggestions.firstOrNull()?.url?.let { url -> engine?.speculativeConnect(url) }
+
+ return suggestions.into()
+ }
+
+ /**
+ * Get up to [BOOKMARKS_SUGGESTION_LIMIT] bookmarks matching [query].
+ *
+ * @param query String to filter bookmarks' title or URL by.
+ */
+ private suspend fun getBookmarksSuggestions(query: String) = bookmarksStorage
+ .searchBookmarks(query, BOOKMARKS_SUGGESTION_LIMIT)
+ .filter { it.url != null }
+ .distinctBy { it.url }
+ .sortedBy { it.guid }
+
+ /**
+ * Get up to [BOOKMARKS_SUGGESTION_LIMIT] bookmarks matching [query] and [filter].
+ *
+ * @param query String to filter bookmarks' title or URL by.
+ * @param filter Predicate to filter the URLs of the bookmarks that match the [query].
+ */
+ private suspend fun getFilteredBookmarksSuggestions(query: String, filter: (Uri) -> Boolean) = bookmarksStorage
+ .searchBookmarks(query, BOOKMARKS_SUGGESTION_LIMIT * BOOKMARKS_RESULTS_TO_FILTER_SCALE_FACTOR)
+ .filter {
+ it.url?.toUri()?.let(filter) ?: true
+ }
+ .distinctBy { it.url }
+ .sortedBy { it.guid }
+ .take(BOOKMARKS_SUGGESTION_LIMIT)
+
+ /**
+ * Expects list of BookmarkNode to be specifically of bookmarks (e.g. nodes with a url).
+ */
+ private suspend fun List<BookmarkNode>.into(): List<AwesomeBar.Suggestion> {
+ val iconRequests = this.map { icons?.loadIcon(IconRequest(url = it.url!!, waitOnNetworkLoad = false)) }
+
+ return this.zip(iconRequests) { result, icon ->
+ AwesomeBar.Suggestion(
+ provider = this@BookmarksStorageSuggestionProvider,
+ id = result.guid,
+ icon = icon?.await()?.bitmap,
+ indicatorIcon = indicatorIcon,
+ flags = setOf(AwesomeBar.Suggestion.Flag.BOOKMARK),
+ title = result.title,
+ description = result.url,
+ editSuggestion = if (showEditSuggestion) result.url else null,
+ onSuggestionClicked = {
+ val flags = LoadUrlFlags.select(LoadUrlFlags.ALLOW_JAVASCRIPT_URL)
+ loadUrlUseCase.invoke(result.url!!, flags = flags)
+ emitBookmarkSuggestionClickedFact()
+ },
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProvider.kt
new file mode 100644
index 0000000000..667a4584f3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProvider.kt
@@ -0,0 +1,128 @@
+/* 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.awesomebar.provider
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.graphics.Bitmap
+import android.os.Build
+import android.view.textclassifier.TextClassifier
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toBitmap
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.awesomebar.facts.emitClipboardSuggestionClickedFact
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.utils.WebURLFinder
+import java.util.UUID
+import mozilla.components.ui.icons.R as iconsR
+
+private const val MIME_TYPE_TEXT_PLAIN = "text/plain"
+private const val MINIMUM_CONFIDENCE_SCORE_FOR_URL = 0.7F
+
+/**
+ * An [AwesomeBar.SuggestionProvider] implementation that returns a suggestions for an URL in the clipboard (if there's
+ * any).
+ *
+ * @property context the activity or application context, required to look up the clipboard manager.
+ * @property loadUrlUseCase the use case invoked to load the url when
+ * the user clicks on the suggestion.
+ * @property icon optional icon used for the [AwesomeBar.Suggestion].
+ * @property title optional title used for the [AwesomeBar.Suggestion].
+ * @property requireEmptyText whether or no the input text must be empty for a
+ * clipboard suggestion to be provided, defaults to true.
+ * @property engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored suggestion URL.
+ */
+class ClipboardSuggestionProvider(
+ private val context: Context,
+ private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ private val icon: Bitmap? = null,
+ private val title: String? = null,
+ private val requireEmptyText: Boolean = true,
+ internal val engine: Engine? = null,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = UUID.randomUUID().toString()
+
+ private val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+ override fun onInputStarted(): List<AwesomeBar.Suggestion> {
+ return createClipboardSuggestion()
+ }
+
+ override suspend fun onInputChanged(text: String) =
+ if ((requireEmptyText && text.isEmpty()) || !requireEmptyText) createClipboardSuggestion() else emptyList()
+
+ private fun createClipboardSuggestion(): List<AwesomeBar.Suggestion> {
+ val clipboardUrl = getUrlFromClipboard(clipboardManager)
+
+ val url = clipboardUrl?.let { findUrl(it) } ?: return emptyList()
+ engine?.speculativeConnect(url)
+
+ return listOf(
+ AwesomeBar.Suggestion(
+ provider = this,
+ id = url,
+ description = url,
+ editSuggestion = url,
+ flags = setOf(AwesomeBar.Suggestion.Flag.CLIPBOARD),
+ icon = icon ?: getSearchIcon(),
+ title = title,
+ onSuggestionClicked = {
+ loadUrlUseCase.invoke(url)
+ emitClipboardSuggestionClickedFact()
+ },
+ ),
+ )
+ }
+
+ private fun getSearchIcon(): Bitmap? {
+ val drawable = iconsR.drawable.mozac_ic_search_24
+ return ContextCompat.getDrawable(context, drawable)?.toBitmap()
+ }
+}
+
+private fun findUrl(text: String): String? {
+ val finder = WebURLFinder(text)
+ return finder.bestWebURL()
+}
+
+private fun getUrlFromClipboard(clipboardManager: ClipboardManager): String? {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val description = clipboardManager.primaryClipDescription
+ // An IllegalStateException may be thrown if the clipboard text is too long.
+ val score =
+ try {
+ description?.getConfidenceScore(TextClassifier.TYPE_URL) ?: 0F
+ } catch (e: IllegalStateException) {
+ 0F
+ }
+ return if (score >= MINIMUM_CONFIDENCE_SCORE_FOR_URL) {
+ getTextFromClipboard(clipboardManager)
+ } else {
+ null
+ }
+ } else {
+ return getTextFromClipboard(clipboardManager)
+ }
+}
+
+private fun getTextFromClipboard(clipboardManager: ClipboardManager): String? {
+ val primaryClip = clipboardManager.primaryClip
+
+ if (primaryClip?.itemCount == 0 || !clipboardManager.isPrimaryClipPlainText()) {
+ // We only care about a primary clip with type "text/plain"
+ return null
+ }
+
+ return primaryClip?.firstClipItemText
+}
+
+private fun ClipboardManager.isPrimaryClipPlainText() =
+ primaryClipDescription?.hasMimeType(MIME_TYPE_TEXT_PLAIN) ?: false
+
+private val ClipData.firstClipItemText: String
+ get() = getItemAt(0).text.toString()
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProvider.kt
new file mode 100644
index 0000000000..74b347e9b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProvider.kt
@@ -0,0 +1,191 @@
+/* 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.awesomebar.provider
+
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.concept.storage.HistoryMetadataStorage
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.feature.session.SessionUseCases
+import java.util.UUID
+
+/**
+ * Return 5 history suggestions by default.
+ */
+const val DEFAULT_COMBINED_SUGGESTION_LIMIT = 5
+
+/**
+ * Default suggestions limit multiplier when needing to filter results by an external url filter.
+ */
+@VisibleForTesting
+internal const val COMBINED_HISTORY_RESULTS_TO_FILTER_SCALE_FACTOR = 10
+
+/**
+ * A [AwesomeBar.SuggestionProvider] implementation that combines suggestions from
+ * [HistoryMetadataSuggestionProvider] and [HistoryStorageSuggestionProvider].
+ * It will return suggestions using [HistoryMetadataSuggestionProvider] first,
+ * followed by suggestion from [HistoryStorageSuggestionProvider] up to the provided
+ * [maxNumberOfSuggestions].
+ *
+ * @param historyStorage an instance of the [HistoryStorage] used
+ * to query matching metadata records.
+ * @param historyMetadataStorage an instance of the [HistoryStorage] used
+ * to query matching metadata records.
+ * @param loadUrlUseCase the use case invoked to load the url when the
+ * user clicks on the suggestion.
+ * @param icons optional instance of [BrowserIcons] to load fav icons
+ * for [HistoryMetadata] URLs.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored suggestion URL.
+ * @param maxNumberOfSuggestions optional parameter to specify the maximum number of returned suggestions,
+ * defaults to [DEFAULT_COMBINED_SUGGESTION_LIMIT].
+ * @param showEditSuggestion optional parameter to specify if the suggestion should show the edit button
+ * @param suggestionsHeader optional parameter to specify if the suggestion should have a header
+ * @param resultsUriFilter Optional predicate to filter matching suggestions by URL.
+ */
+class CombinedHistorySuggestionProvider(
+ private val historyStorage: HistoryStorage,
+ private val historyMetadataStorage: HistoryMetadataStorage,
+ private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ private val icons: BrowserIcons? = null,
+ internal val engine: Engine? = null,
+ internal var maxNumberOfSuggestions: Int = DEFAULT_COMBINED_SUGGESTION_LIMIT,
+ @get:VisibleForTesting val showEditSuggestion: Boolean = true,
+ private val suggestionsHeader: String? = null,
+ @get:VisibleForTesting val resultsUriFilter: ((Uri) -> Boolean)? = null,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> = coroutineScope {
+ historyStorage.cancelReads(text)
+ historyMetadataStorage.cancelReads(text)
+
+ if (text.isBlank()) {
+ return@coroutineScope emptyList()
+ }
+
+ val metadataSuggestionsAsync = async {
+ when (resultsUriFilter) {
+ null -> getMetadataSuggestions(text)
+ else -> getFilteredMetadataSuggestions(text, resultsUriFilter)
+ }
+ }
+
+ val historySuggestionsAsync = async {
+ when (resultsUriFilter) {
+ null -> getHistorySuggestions(text)
+ else -> getFilteredHistorySuggestions(text, resultsUriFilter)
+ }
+ }
+
+ val metadataSuggestions = metadataSuggestionsAsync.await()
+ val historySuggestions = historySuggestionsAsync.await()
+ val historyTopScore = historySuggestions.firstOrNull()?.score
+ val updatedMetadataSuggestions = if (historyTopScore != null) {
+ // Make sure history metadata suggestions have a higher score than regular history
+ // suggestions but otherwise retain their relative order.
+ val size = metadataSuggestions.size
+ metadataSuggestions.mapIndexed { index, suggestion ->
+ suggestion.copy(score = (size - index) + historyTopScore)
+ }
+ } else {
+ metadataSuggestions
+ }
+
+ val combinedSuggestions = (updatedMetadataSuggestions + historySuggestions)
+ .distinctBy { it.description }
+ .take(maxNumberOfSuggestions)
+
+ combinedSuggestions.firstOrNull()?.description?.let { url -> engine?.speculativeConnect(url) }
+
+ return@coroutineScope combinedSuggestions
+ }
+
+ /**
+ * Set maximum number of suggestions.
+ */
+ fun setMaxNumberOfSuggestions(maxNumber: Int) {
+ if (maxNumber <= 0) {
+ return
+ }
+
+ maxNumberOfSuggestions = maxNumber
+ }
+
+ /**
+ * Get the maximum number of suggestions that will be provided.
+ */
+ @VisibleForTesting
+ fun getMaxNumberOfSuggestions() = maxNumberOfSuggestions
+
+ /**
+ * Reset maximum number of suggestions to default.
+ */
+ fun resetToDefaultMaxSuggestions() {
+ maxNumberOfSuggestions = DEFAULT_COMBINED_SUGGESTION_LIMIT
+ }
+
+ /**
+ * Get up to [maxNumberOfSuggestions] history metadata suggestions matching [query].
+ *
+ * @param query String to filter bookmarks' title or URL by.
+ */
+ private suspend fun getMetadataSuggestions(query: String) = historyMetadataStorage
+ .queryHistoryMetadata(query, maxNumberOfSuggestions)
+ .filter { it.totalViewTime > 0 }
+ .into(this@CombinedHistorySuggestionProvider, icons, loadUrlUseCase, showEditSuggestion)
+
+ /**
+ * Get up to [maxNumberOfSuggestions] history metadata suggestions matching [query] and [filter].
+ *
+ * @param query String to filter history entry's title or URL by.
+ * @param filter Predicate to filter the URLs of the history entries that match the [query].
+ */
+ private suspend fun getFilteredMetadataSuggestions(query: String, filter: (Uri) -> Boolean) = historyMetadataStorage
+ .queryHistoryMetadata(query, maxNumberOfSuggestions * COMBINED_HISTORY_RESULTS_TO_FILTER_SCALE_FACTOR)
+ .filter {
+ it.totalViewTime > 0 && filter(it.key.url.toUri())
+ }
+ .take(maxNumberOfSuggestions)
+ .into(this@CombinedHistorySuggestionProvider, icons, loadUrlUseCase, showEditSuggestion)
+
+ /**
+ * Get up to [maxNumberOfSuggestions] history suggestions matching [query].
+ *
+ * @param query String to filter history entry's title or URL by.
+ */
+ private suspend fun getHistorySuggestions(query: String) = historyStorage
+ .getSuggestions(query, maxNumberOfSuggestions)
+ .sortedByDescending { it.score }
+ .distinctBy { it.id }
+ .into(this@CombinedHistorySuggestionProvider, icons, loadUrlUseCase, showEditSuggestion)
+
+ /**
+ * Get up to [maxNumberOfSuggestions] history metadata suggestions matching [query] and [filter].
+ *
+ * @param query String to filter history entry's title or URL by.
+ * @param filter Predicate to filter the URLs of the history entries that match the [query].
+ */
+ private suspend fun getFilteredHistorySuggestions(query: String, filter: (Uri) -> Boolean) = historyStorage
+ .getSuggestions(query, maxNumberOfSuggestions * COMBINED_HISTORY_RESULTS_TO_FILTER_SCALE_FACTOR)
+ .distinctBy { it.id }
+ .sortedByDescending { it.score }
+ .filter {
+ filter(it.url.toUri())
+ }
+ .take(maxNumberOfSuggestions)
+ .into(this@CombinedHistorySuggestionProvider, icons, loadUrlUseCase, showEditSuggestion)
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProvider.kt
new file mode 100644
index 0000000000..ad1d7590b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProvider.kt
@@ -0,0 +1,127 @@
+/* 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.awesomebar.provider
+
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.concept.storage.HistoryMetadataStorage
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.feature.awesomebar.facts.emitHistorySuggestionClickedFact
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.ktx.android.net.sameHostWithoutMobileSubdomainAs
+import java.util.UUID
+
+/**
+ * Return 5 history suggestions by default.
+ */
+const val DEFAULT_METADATA_SUGGESTION_LIMIT = 5
+
+/**
+ * Default suggestions limit multiplier when needing to filter results by an external url filter.
+ */
+@VisibleForTesting
+internal const val HISTORY_METADATA_RESULTS_TO_FILTER_SCALE_FACTOR = 10
+
+/**
+ * A [AwesomeBar.SuggestionProvider] implementation that provides suggestions based on [HistoryMetadata].
+ *
+ * @param historyStorage an instance of the [HistoryStorage] used
+ * to query matching metadata records.
+ * @param loadUrlUseCase the use case invoked to load the url when the
+ * user clicks on the suggestion.
+ * @param icons optional instance of [BrowserIcons] to load fav icons
+ * for [HistoryMetadata] URLs.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored suggestion URL.
+ * @param maxNumberOfSuggestions optional parameter to specify the maximum number of returned suggestions,
+ * defaults to [DEFAULT_METADATA_SUGGESTION_LIMIT].
+ * @param showEditSuggestion optional parameter to specify if the suggestion should show the edit button
+ * @param suggestionsHeader optional parameter to specify if the suggestion should have a header
+ * @param resultsUriFilter Optional filter for the url of the suggestions to show.
+ */
+class HistoryMetadataSuggestionProvider(
+ @get:VisibleForTesting internal val historyStorage: HistoryMetadataStorage,
+ private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ private val icons: BrowserIcons? = null,
+ internal val engine: Engine? = null,
+ @get:VisibleForTesting internal val maxNumberOfSuggestions: Int = DEFAULT_METADATA_SUGGESTION_LIMIT,
+ private val showEditSuggestion: Boolean = true,
+ private val suggestionsHeader: String? = null,
+ @get:VisibleForTesting val resultsUriFilter: Uri? = null,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
+ historyStorage.cancelReads(text)
+
+ if (text.isNullOrBlank()) {
+ return emptyList()
+ }
+
+ val suggestions = when (resultsUriFilter) {
+ null -> getHistorySuggestions(text)
+ else -> getHistorySuggestionsFromHost(resultsUriFilter, text)
+ }
+
+ suggestions.firstOrNull()?.key?.url?.let { url -> engine?.speculativeConnect(url) }
+ return suggestions.into(this, icons, loadUrlUseCase, showEditSuggestion)
+ }
+
+ /**
+ * Get up to [maxNumberOfSuggestions] history metadata suggestions matching [query].
+ *
+ * @param query String to filter history entry's title or URL by.
+ */
+ private suspend fun getHistorySuggestions(query: String) = historyStorage
+ .queryHistoryMetadata(query, maxNumberOfSuggestions)
+ .filter { it.totalViewTime > 0 }
+
+ /**
+ * Get up to [maxNumberOfSuggestions] history metadata suggestions matching [query] from the indicated [url].
+ *
+ * @param query String to filter history entry's title or URL by.
+ * @param url URL host to filter all history entry's URL host by.
+ */
+ private suspend fun getHistorySuggestionsFromHost(url: Uri, query: String) = historyStorage
+ .queryHistoryMetadata(query, maxNumberOfSuggestions * HISTORY_METADATA_RESULTS_TO_FILTER_SCALE_FACTOR)
+ .filter {
+ it.totalViewTime > 0 &&
+ it.key.url.toUri().sameHostWithoutMobileSubdomainAs(url)
+ }
+ .take(maxNumberOfSuggestions)
+}
+
+internal suspend fun Iterable<HistoryMetadata>.into(
+ provider: AwesomeBar.SuggestionProvider,
+ icons: BrowserIcons?,
+ loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ showEditSuggestion: Boolean = true,
+): List<AwesomeBar.Suggestion> {
+ val iconRequests = this.map { icons?.loadIcon(IconRequest(url = it.key.url, waitOnNetworkLoad = false)) }
+ return this.zip(iconRequests) { result, icon ->
+ AwesomeBar.Suggestion(
+ provider = provider,
+ icon = icon?.await()?.bitmap,
+ flags = setOf(AwesomeBar.Suggestion.Flag.HISTORY),
+ title = result.title,
+ description = result.key.url,
+ editSuggestion = if (showEditSuggestion) result.key.url else null,
+ onSuggestionClicked = {
+ loadUrlUseCase.invoke(result.key.url)
+ emitHistorySuggestionClickedFact()
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProvider.kt
new file mode 100644
index 0000000000..e9d56bede3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProvider.kt
@@ -0,0 +1,158 @@
+/* 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.awesomebar.provider
+
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.concept.storage.SearchResult
+import mozilla.components.feature.awesomebar.facts.emitHistorySuggestionClickedFact
+import mozilla.components.feature.session.SessionUseCases
+import java.util.UUID
+
+/**
+ * Return 20 history suggestions by default.
+ */
+const val DEFAULT_HISTORY_SUGGESTION_LIMIT = 20
+
+/**
+ * Default suggestions limit multiplier when needing to filter results by an external url filter.
+ */
+@VisibleForTesting
+internal const val HISTORY_RESULTS_TO_FILTER_SCALE_FACTOR = 10
+
+/**
+ * A [AwesomeBar.SuggestionProvider] implementation that provides suggestions based on the browsing
+ * history stored in the [HistoryStorage].
+ *
+ * @param historyStorage and instance of the [HistoryStorage] used
+ * to query matching history records.
+ * @param loadUrlUseCase the use case invoked to load the url when the
+ * user clicks on the suggestion.
+ * @param icons optional instance of [BrowserIcons] to load fav icons
+ * for history URLs.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored suggestion URL.
+ * @param maxNumberOfSuggestions optional parameter to specify the maximum number of returned suggestions,
+ * defaults to [DEFAULT_HISTORY_SUGGESTION_LIMIT]
+ * @param showEditSuggestion optional parameter to specify if the suggestion should show the edit button
+ * @param suggestionsHeader optional parameter to specify if the suggestion should have a header
+ * @param resultsUriFilter Optional predicate to filter matching suggestions by URL.
+ */
+class HistoryStorageSuggestionProvider(
+ @get:VisibleForTesting internal val historyStorage: HistoryStorage,
+ private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ private val icons: BrowserIcons? = null,
+ internal val engine: Engine? = null,
+ @get:VisibleForTesting internal var maxNumberOfSuggestions: Int = DEFAULT_HISTORY_SUGGESTION_LIMIT,
+ @get:VisibleForTesting val showEditSuggestion: Boolean = true,
+ private val suggestionsHeader: String? = null,
+ @get:VisibleForTesting val resultsUriFilter: ((Uri) -> Boolean)? = null,
+) : AwesomeBar.SuggestionProvider {
+
+ override val id: String = UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
+ historyStorage.cancelReads(text)
+
+ if (text.isEmpty()) {
+ return emptyList()
+ }
+
+ val suggestions = when (resultsUriFilter) {
+ null -> getHistorySuggestions(text)
+ else -> getFilteredHistorySuggestions(text, resultsUriFilter)
+ }
+
+ suggestions.firstOrNull()?.url?.let { url -> engine?.speculativeConnect(url) }
+
+ return suggestions.into(this, icons, loadUrlUseCase, showEditSuggestion)
+ }
+
+ /**
+ * Set maximum number of suggestions.
+ */
+ fun setMaxNumberOfSuggestions(maxNumber: Int) {
+ if (maxNumber <= 0) {
+ return
+ }
+
+ maxNumberOfSuggestions = maxNumber
+ }
+
+ /**
+ * Get the maximum number of suggestions that will be provided.
+ */
+ @VisibleForTesting
+ fun getMaxNumberOfSuggestions() = maxNumberOfSuggestions
+
+ /**
+ * Reset maximum number of suggestions to default.
+ */
+ fun resetToDefaultMaxSuggestions() {
+ maxNumberOfSuggestions = DEFAULT_HISTORY_SUGGESTION_LIMIT
+ }
+
+ /**
+ * Get up to [maxNumberOfSuggestions] history suggestions matching [query]].
+ *
+ * @param query String to filter history entry's title or URL by.
+ */
+ private fun getHistorySuggestions(query: String) = historyStorage
+ .getSuggestions(query, maxNumberOfSuggestions)
+ // In case of duplicates we want to pick the suggestion with the highest score.
+ // See: https://github.com/mozilla/application-services/issues/970
+ .sortedByDescending { it.score }
+ .distinctBy { it.id }
+ .take(maxNumberOfSuggestions)
+
+ /**
+ * Get up to [maxNumberOfSuggestions] history suggestions matching [query] and [filter].
+ *
+ * @param query String to filter history entry's title or URL by.
+ * @param filter Predicate to filter the URLs of the history entries that match the [query].
+ */
+ private fun getFilteredHistorySuggestions(query: String, filter: (Uri) -> Boolean) = historyStorage
+ .getSuggestions(query, maxNumberOfSuggestions * HISTORY_RESULTS_TO_FILTER_SCALE_FACTOR)
+ .sortedByDescending { it.score }
+ .distinctBy { it.id }
+ .filter {
+ filter(it.url.toUri())
+ }
+ .take(maxNumberOfSuggestions)
+}
+
+internal suspend fun Iterable<SearchResult>.into(
+ provider: AwesomeBar.SuggestionProvider,
+ icons: BrowserIcons?,
+ loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ showEditSuggestion: Boolean = true,
+): List<AwesomeBar.Suggestion> {
+ val iconRequests = this.map { icons?.loadIcon(IconRequest(url = it.url, waitOnNetworkLoad = false)) }
+ return this.zip(iconRequests) { result, icon ->
+ AwesomeBar.Suggestion(
+ provider = provider,
+ id = result.id,
+ icon = icon?.await()?.bitmap,
+ title = result.title,
+ description = result.url,
+ editSuggestion = if (showEditSuggestion) result.url else null,
+ score = result.score,
+ onSuggestionClicked = {
+ loadUrlUseCase.invoke(result.url)
+ emitHistorySuggestionClickedFact()
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchActionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchActionProvider.kt
new file mode 100644
index 0000000000..47759b236a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchActionProvider.kt
@@ -0,0 +1,59 @@
+/* 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.awesomebar.provider
+
+import android.graphics.Bitmap
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.feature.awesomebar.facts.emitSearchActionClickedFact
+import mozilla.components.feature.search.SearchUseCases
+
+private const val FIXED_ID = "@@@search.action.provider.fixed.id@@"
+
+/**
+ * An [AwesomeBar.SuggestionProvider] implementation that returns a suggestion that mirrors the
+ * entered text and invokes a search with the given [SearchEngine] if clicked.
+ */
+class SearchActionProvider(
+ private val store: BrowserStore,
+ private val searchUseCase: SearchUseCases.SearchUseCase,
+ private val icon: Bitmap? = null,
+ private val showDescription: Boolean = true,
+ private val searchEngine: SearchEngine? = null,
+ private val suggestionsHeader: String? = null,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = java.util.UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
+ if (text.isBlank()) {
+ return emptyList()
+ }
+
+ val searchEngine = searchEngine ?: store.state.search.selectedOrDefaultSearchEngine
+ ?: return emptyList()
+
+ return listOf(
+ AwesomeBar.Suggestion(
+ provider = this,
+ // We always use the same ID for the entered text so that this suggestion gets replaced "in place".
+ id = FIXED_ID,
+ title = text,
+ description = if (showDescription) searchEngine.name else null,
+ icon = icon ?: searchEngine.icon,
+ score = Int.MAX_VALUE - 1,
+ onSuggestionClicked = {
+ searchUseCase.invoke(text)
+ emitSearchActionClickedFact()
+ },
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProvider.kt
new file mode 100644
index 0000000000..f7767f6350
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProvider.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.awesomebar.provider
+
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.annotation.StringRes
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.concept.awesomebar.AwesomeBar
+import java.lang.Integer.MAX_VALUE
+import java.util.UUID
+
+/**
+ * [AwesomeBar.SuggestionProvider] implementation that provides suggestions based on the search engine list.
+ *
+ * @property searchEnginesList a search engine list used to search
+ * @property selectShortcutEngine the use case invoked to temporarily change engine used for search
+ * @property title String resource for the title to be shown for the suggestion(s), it
+ * includes a placeholder for engine name
+ * @property description the description to be shown for the suggestion(s), same description for all
+ * @property searchIcon the icon to be shown for the suggestion(s), same icon for all
+ * @property maxSuggestions the maximum number of suggestions to be provided
+ * @property charactersThreshold the minimum typed characters used to match to a search engine name
+ */
+class SearchEngineSuggestionProvider(
+ private val context: Context,
+ private val searchEnginesList: List<SearchEngine>,
+ private val selectShortcutEngine: (engine: SearchEngine) -> Unit,
+ @StringRes
+ private val title: Int,
+ private val description: String?,
+ private val searchIcon: Bitmap?,
+ internal val maxSuggestions: Int = DEFAULT_MAX_SUGGESTIONS,
+ internal val charactersThreshold: Int = DEFAULT_CHARACTERS_THRESHOLD,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = UUID.randomUUID().toString()
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
+ if (text.isEmpty() || text.length < charactersThreshold) {
+ return emptyList()
+ }
+
+ val suggestions = searchEnginesList
+ .filter { it.name.startsWith(text, true) }.take(maxSuggestions)
+
+ return if (suggestions.isNotEmpty()) {
+ suggestions.into()
+ } else {
+ return emptyList()
+ }
+ }
+
+ /**
+ * Generates a list of [AwesomeBar.Suggestion] from a [SearchEngine] list
+ */
+ private fun List<SearchEngine>.into(): List<AwesomeBar.Suggestion> {
+ return this.map {
+ AwesomeBar.Suggestion(
+ provider = this@SearchEngineSuggestionProvider,
+ id = it.id,
+ icon = searchIcon,
+ flags = setOf(AwesomeBar.Suggestion.Flag.BOOKMARK),
+ title = context.getString(title, it.name),
+ description = description,
+ onSuggestionClicked = { selectShortcutEngine(it) },
+ score = MAX_VALUE,
+ )
+ }
+ }
+
+ companion object {
+ internal const val DEFAULT_MAX_SUGGESTIONS = 1
+ internal const val DEFAULT_CHARACTERS_THRESHOLD = 2
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProvider.kt
new file mode 100644
index 0000000000..23336d9468
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProvider.kt
@@ -0,0 +1,285 @@
+/* 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.awesomebar.provider
+
+import android.content.Context
+import android.graphics.Bitmap
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.feature.awesomebar.facts.emitSearchSuggestionClickedFact
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.search.ext.buildSearchUrl
+import mozilla.components.feature.search.suggestions.SearchSuggestionClient
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import java.io.IOException
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+
+/**
+ * A [AwesomeBar.SuggestionProvider] implementation that provides a suggestion containing search engine suggestions (as
+ * chips) from the passed in [SearchEngine].
+ */
+class SearchSuggestionProvider private constructor(
+ internal val client: SearchSuggestionClient,
+ private val searchUseCase: SearchUseCases.SearchUseCase,
+ private val limit: Int = 15,
+ private val mode: Mode = Mode.SINGLE_SUGGESTION,
+ internal val engine: Engine? = null,
+ private val icon: Bitmap? = null,
+ private val showDescription: Boolean = true,
+ private val filterExactMatch: Boolean = false,
+ private val suggestionsHeader: String? = null,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = UUID.randomUUID().toString()
+
+ init {
+ require(limit >= 1) { "limit needs to be >= 1" }
+ }
+
+ /**
+ * Creates a [SearchSuggestionProvider] for the provided [SearchEngine].
+ *
+ * @param searchEngine The search engine to request suggestions from.
+ * @param searchUseCase The use case to invoke for searches.
+ * @param fetchClient The HTTP client for requesting suggestions from the search engine.
+ * @param limit The maximum number of suggestions that should be returned. It needs to be >= 1.
+ * @param mode Whether to return a single search suggestion (with chips) or one suggestion per item.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored search suggestion URL.
+ * @param icon The image to display next to the result. If not specified, the engine icon is used.
+ * @param showDescription whether or not to add the search engine name as description.
+ * @param filterExactMatch If true filters out suggestions that exactly match the entered text.
+ * @param private When set to `true` then all requests to search engines will be made in private
+ * mode.
+ */
+ constructor(
+ searchEngine: SearchEngine,
+ searchUseCase: SearchUseCases.SearchUseCase,
+ fetchClient: Client,
+ limit: Int = 15,
+ mode: Mode = Mode.SINGLE_SUGGESTION,
+ engine: Engine? = null,
+ icon: Bitmap? = null,
+ showDescription: Boolean = true,
+ filterExactMatch: Boolean = false,
+ private: Boolean = false,
+ ) : this (
+ SearchSuggestionClient(searchEngine) { url -> fetch(fetchClient, url, private) },
+ searchUseCase,
+ limit,
+ mode,
+ engine,
+ icon,
+ showDescription,
+ filterExactMatch,
+ )
+
+ /**
+ * Creates a [SearchSuggestionProvider] using the default engine as provided by the given
+ * [BrowserStore].
+ *
+ * @param context the activity or application context, required to load search engines.
+ * @param store The [BrowserStore] to look up search engines.
+ * @param searchUseCase The use case to invoke for searches.
+ * @param fetchClient The HTTP client for requesting suggestions from the search engine.
+ * @param limit The maximum number of suggestions that should be returned. It needs to be >= 1.
+ * @param mode Whether to return a single search suggestion (with chips) or one suggestion per item.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored search suggestion URL.
+ * @param icon The image to display next to the result. If not specified, the engine icon is used.
+ * @param showDescription whether or not to add the search engine name as description.
+ * @param filterExactMatch If true filters out suggestions that exactly match the entered text.
+ * @param private When set to `true` then all requests to search engines will be made in private
+ * mode.
+ * @param suggestionsHeader Optional suggestions header to display.
+ */
+ constructor(
+ context: Context,
+ store: BrowserStore,
+ searchUseCase: SearchUseCases.SearchUseCase,
+ fetchClient: Client,
+ limit: Int = 15,
+ mode: Mode = Mode.SINGLE_SUGGESTION,
+ engine: Engine? = null,
+ icon: Bitmap? = null,
+ showDescription: Boolean = true,
+ filterExactMatch: Boolean = false,
+ private: Boolean = false,
+ suggestionsHeader: String? = null,
+ ) : this (
+ SearchSuggestionClient(context, store) { url -> fetch(fetchClient, url, private) },
+ searchUseCase,
+ limit,
+ mode,
+ engine,
+ icon,
+ showDescription,
+ filterExactMatch,
+ suggestionsHeader,
+ )
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ @Suppress("ReturnCount")
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
+ if (text.isEmpty()) {
+ return emptyList()
+ }
+
+ val suggestions = fetchSuggestions(text)
+
+ return when (mode) {
+ Mode.MULTIPLE_SUGGESTIONS -> createMultipleSuggestions(text, suggestions).also {
+ // Call speculativeConnect for URL of first (highest scored) suggestion
+ it.firstOrNull()?.title?.let { searchTerms -> maybeCallSpeculativeConnect(searchTerms) }
+ }
+ Mode.SINGLE_SUGGESTION -> createSingleSearchSuggestion(text, suggestions).also {
+ // Call speculativeConnect for URL of first (highest scored) chip
+ it.firstOrNull()?.chips?.firstOrNull()?.let { chip -> maybeCallSpeculativeConnect(chip.title) }
+ }
+ }
+ }
+
+ private fun maybeCallSpeculativeConnect(searchTerms: String) {
+ client.searchEngine?.let { searchEngine ->
+ engine?.speculativeConnect(searchEngine.buildSearchUrl(searchTerms))
+ }
+ }
+
+ private suspend fun fetchSuggestions(text: String): List<String>? {
+ return try {
+ client.getSuggestions(text)
+ } catch (e: SearchSuggestionClient.FetchException) {
+ Logger.info("Could not fetch search suggestions from search engine", e)
+ // If we can't fetch search suggestions then just continue with a single suggestion for the entered text
+ emptyList()
+ } catch (e: SearchSuggestionClient.ResponseParserException) {
+ Logger.warn("Could not parse search suggestions from search engine", e)
+ // If parsing failed then just continue with a single suggestion for the entered text
+ emptyList()
+ }
+ }
+
+ @Suppress("ComplexMethod")
+ private fun createMultipleSuggestions(text: String, result: List<String>?): List<AwesomeBar.Suggestion> {
+ val suggestions = mutableListOf<AwesomeBar.Suggestion>()
+
+ val list = (result ?: listOf(text)).toMutableList()
+ if (!list.contains(text) && !filterExactMatch) {
+ list.add(0, text)
+ }
+
+ if (filterExactMatch && list.contains(text)) {
+ list.remove(text)
+ }
+
+ val description = if (showDescription) {
+ client.searchEngine?.name
+ } else {
+ null
+ }
+
+ list.distinct().take(limit).forEachIndexed { index, item ->
+ suggestions.add(
+ AwesomeBar.Suggestion(
+ provider = this,
+ // We always use the same ID for the entered text so that this suggestion gets replaced "in place".
+ id = if (item == text) ID_OF_ENTERED_TEXT else item,
+ title = item,
+ description = description,
+ // Don't show an autocomplete arrow for the entered text
+ editSuggestion = if (item == text) null else item,
+ icon = icon ?: client.searchEngine?.icon,
+ // Reducing MAX_VALUE to allow SearchActionProvider and SearchTermSuggestionsProvider
+ // to go above for which they need additional spots above available.
+ score = Int.MAX_VALUE - (index + SEARCH_TERMS_MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT + 2),
+ onSuggestionClicked = {
+ searchUseCase.invoke(item)
+ emitSearchSuggestionClickedFact()
+ },
+ ),
+ )
+ }
+
+ return suggestions
+ }
+
+ @Suppress("ComplexCondition")
+ private fun createSingleSearchSuggestion(text: String, result: List<String>?): List<AwesomeBar.Suggestion> {
+ val chips = mutableListOf<AwesomeBar.Suggestion.Chip>()
+
+ if ((result == null || result.isEmpty() || !result.contains(text)) && !filterExactMatch) {
+ // Add the entered text as first suggestion if needed
+ chips.add(AwesomeBar.Suggestion.Chip(text))
+ }
+
+ result?.take(limit - chips.size)?. forEach { title ->
+ if (!filterExactMatch || title != text) {
+ chips.add(AwesomeBar.Suggestion.Chip(title))
+ }
+ }
+
+ return listOf(
+ AwesomeBar.Suggestion(
+ provider = this,
+ id = text,
+ title = client.searchEngine?.name,
+ chips = chips,
+ score = Int.MAX_VALUE,
+ icon = icon ?: client.searchEngine?.icon,
+ onChipClicked = { chip ->
+ searchUseCase.invoke(chip.title)
+ emitSearchSuggestionClickedFact()
+ },
+ ),
+ )
+ }
+
+ enum class Mode {
+ SINGLE_SUGGESTION,
+ MULTIPLE_SUGGESTIONS,
+ }
+
+ companion object {
+ private const val READ_TIMEOUT_IN_MS = 2000L
+ private const val CONNECT_TIMEOUT_IN_MS = 1000L
+ private const val ID_OF_ENTERED_TEXT = "<@@@entered_text_id@@@>"
+
+ @Suppress("ReturnCount", "TooGenericExceptionCaught")
+ private fun fetch(fetchClient: Client, url: String, private: Boolean): String? {
+ try {
+ val request = Request(
+ url = url.sanitizeURL(),
+ readTimeout = Pair(READ_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS),
+ connectTimeout = Pair(CONNECT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS),
+ private = private,
+ )
+
+ val response = fetchClient.fetch(request)
+ if (!response.isSuccess) {
+ response.close()
+ return null
+ }
+
+ return response.use { it.body.string() }
+ } catch (e: IOException) {
+ return null
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ // On some devices we are seeing an ArrayIndexOutOfBoundsException being thrown
+ // somewhere inside AOSP/okhttp.
+ // See: https://github.com/mozilla-mobile/android-components/issues/964
+ return null
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchTermSuggestionsProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchTermSuggestionsProvider.kt
new file mode 100644
index 0000000000..62eedfef2e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SearchTermSuggestionsProvider.kt
@@ -0,0 +1,136 @@
+/* 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.awesomebar.provider
+
+import android.graphics.Bitmap
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.feature.awesomebar.facts.emitSearchTermSuggestionClickedFact
+import mozilla.components.feature.search.SearchUseCases.SearchUseCase
+import mozilla.components.feature.search.ext.buildSearchUrl
+import java.util.UUID
+
+/**
+ * Return 2 search term suggestions by default. Same as on desktop.
+ */
+private const val DEFAULT_SUGGESTION_LIMIT = 2
+
+/**
+ * A too big limit but which help ensure the SearchSuggestionProvider' suggestions which should be placed
+ * below the ones from this provider will appear correctly.
+ */
+const val SEARCH_TERMS_MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT: Int = 1000
+
+/**
+ * Error message if clients are requesting for a too big number of suggestions.
+ */
+private const val MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT_REACHED =
+ "Cannot show more than $SEARCH_TERMS_MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT suggestions."
+
+/**
+ * A [AwesomeBar.SuggestionProvider] implementation that will show past searches done with the
+ * specified [searchEngine] allowing to easily redo a search or continue with a lightly modified search.
+ *
+ * @param historyStorage an instance of the [PlacesHistoryStorage] used
+ * to query matching metadata records.
+ * @param searchUseCase the use case invoked to do a new search with the suggested search term.
+ * @param searchEngine the current search engine used for speculative connects with the first result.
+ * @param maxNumberOfSuggestions optional parameter to specify the maximum number of returned suggestions.
+ * Defaults to `2`.
+ * @param icon optional [Bitmap] to he shown as the suggestions header.
+ * Defaults to `null` in which case the [searchEngine]'s icon will be used.
+ * @param engine optional [Engine] instance to call [Engine.speculativeConnect] for the
+ * highest scored suggestion URL.
+ * @param showEditSuggestion optional parameter to specify if the suggestion should show the edit button.
+ * @param suggestionsHeader optional parameter to specify if the suggestion should have a header
+ */
+class SearchTermSuggestionsProvider(
+ private val historyStorage: PlacesHistoryStorage,
+ private val searchUseCase: SearchUseCase,
+ private val searchEngine: SearchEngine?,
+ @androidx.annotation.IntRange(from = 0, to = SEARCH_TERMS_MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT.toLong())
+ private val maxNumberOfSuggestions: Int = DEFAULT_SUGGESTION_LIMIT,
+ private val icon: Bitmap? = null,
+ private val engine: Engine? = null,
+ private val showEditSuggestion: Boolean = true,
+ private val suggestionsHeader: String? = null,
+) : AwesomeBar.SuggestionProvider {
+ init {
+ if (maxNumberOfSuggestions > SEARCH_TERMS_MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT) {
+ throw IllegalArgumentException(MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT_REACHED)
+ }
+ }
+
+ override val id: String = UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> = coroutineScope {
+ historyStorage.cancelReads(text)
+
+ if (text.isBlank()) {
+ return@coroutineScope emptyList()
+ }
+
+ val suggestions = withContext(this.coroutineContext) {
+ historyStorage.getHistoryMetadataSince(Long.MIN_VALUE)
+ .asSequence()
+ .filter { it.totalViewTime > 0 }
+ .filter { it.key.searchTerm?.startsWith(text) ?: false }
+ .distinctBy { it.key.searchTerm }
+ .sortedByDescending { it.createdAt }
+ .take(maxNumberOfSuggestions)
+ .toList()
+ }
+
+ searchEngine?.let {
+ suggestions.firstOrNull()?.key?.searchTerm?.let { searchTerm ->
+ engine?.speculativeConnect(it.buildSearchUrl(searchTerm))
+ }
+ }
+
+ return@coroutineScope suggestions.into(
+ provider = this@SearchTermSuggestionsProvider,
+ searchEngine = searchEngine,
+ icon = icon,
+ searchUseCase = searchUseCase,
+ showEditSuggestion = showEditSuggestion,
+ )
+ }
+}
+
+private fun Iterable<HistoryMetadata>.into(
+ provider: AwesomeBar.SuggestionProvider,
+ searchEngine: SearchEngine?,
+ icon: Bitmap?,
+ searchUseCase: SearchUseCase,
+ showEditSuggestion: Boolean = true,
+): List<AwesomeBar.Suggestion> {
+ return this.mapIndexedNotNull { index, result ->
+ val safeSearchTerm = result.key.searchTerm ?: return@mapIndexedNotNull null
+
+ AwesomeBar.Suggestion(
+ provider = provider,
+ icon = icon ?: searchEngine?.icon,
+ title = result.key.searchTerm,
+ description = null,
+ editSuggestion = if (showEditSuggestion) safeSearchTerm else null,
+ // Reducing MAX_VALUE by 2: To allow SearchActionProvider to go above and
+ // still have one additional spot above available.
+ score = Int.MAX_VALUE - (index + 2),
+ onSuggestionClicked = {
+ searchUseCase.invoke(safeSearchTerm)
+ emitSearchTermSuggestionClickedFact()
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SessionAutocompleteProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SessionAutocompleteProvider.kt
new file mode 100644
index 0000000000..e044586ac0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SessionAutocompleteProvider.kt
@@ -0,0 +1,55 @@
+/* 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.awesomebar.provider
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.concept.toolbar.AutocompleteResult
+import mozilla.components.support.utils.doesUrlStartsWithText
+import mozilla.components.support.utils.segmentAwareDomainMatch
+
+@VisibleForTesting
+internal const val LOCAL_TABS_AUTOCOMPLETE_SOURCE_NAME = "localTabs"
+
+/**
+ * Provide autocomplete suggestions from the currently opened tabs.
+ *
+ * @param store [BrowserStore] containing the information about the currently open tabs.
+ * @param autocompletePriority Order in which this provider will be queried for autocomplete suggestions
+ * in relation ot others.
+ * - a lower priority means that this provider must be called before others with a higher priority.
+ * - an equal priority offers no ordering guarantees.
+ *
+ * Defaults to `0`.
+ */
+class SessionAutocompleteProvider(
+ private val store: BrowserStore,
+ override val autocompletePriority: Int = 0,
+) : AutocompleteProvider {
+ override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
+ if (query.isEmpty()) {
+ return null
+ }
+
+ val tabUrl = store.state.tabs
+ .firstOrNull {
+ !it.content.private && doesUrlStartsWithText(it.content.url, query)
+ }
+ ?.content?.url
+ ?: return null
+
+ val resultText = segmentAwareDomainMatch(query, arrayListOf(tabUrl))
+ return resultText?.let {
+ AutocompleteResult(
+ input = query,
+ text = it.matchedSegment,
+ url = it.url,
+ source = LOCAL_TABS_AUTOCOMPLETE_SOURCE_NAME,
+ totalItems = 1,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProvider.kt b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProvider.kt
new file mode 100644
index 0000000000..d94e94153e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProvider.kt
@@ -0,0 +1,98 @@
+/* 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.awesomebar.provider
+
+import android.content.res.Resources
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import kotlinx.coroutines.Deferred
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.feature.awesomebar.R
+import mozilla.components.feature.awesomebar.facts.emitOpenTabSuggestionClickedFact
+import mozilla.components.feature.tabs.TabsUseCases
+import java.util.UUID
+
+/**
+ * A [AwesomeBar.SuggestionProvider] implementation that provides suggestions based on the sessions in the
+ * [SessionManager] (Open tabs).
+ */
+class SessionSuggestionProvider(
+ private val resources: Resources,
+ private val store: BrowserStore,
+ private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
+ private val icons: BrowserIcons? = null,
+ private val indicatorIcon: Drawable? = null,
+ private val excludeSelectedSession: Boolean = false,
+ private val suggestionsHeader: String? = null,
+ @get:VisibleForTesting val resultsUriFilter: ((Uri) -> Boolean)? = null,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ @Suppress("ComplexCondition")
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
+ val searchText = text.trim()
+ if (searchText.isEmpty()) {
+ return emptyList()
+ }
+
+ val state = store.state
+ val distinctTabs = state.tabs.distinctBy { it.content.url }
+
+ val suggestions = mutableListOf<AwesomeBar.Suggestion>()
+ val iconRequests: List<Deferred<Icon>?> = distinctTabs.map {
+ icons?.loadIcon(IconRequest(url = it.content.url, waitOnNetworkLoad = false))
+ }
+
+ val searchWords = searchText.split(" ")
+ distinctTabs.zip(iconRequests) { result, icon ->
+ if (
+ resultsUriFilter?.invoke(result.content.url.toUri()) != false &&
+ searchWords.all { result.contains(it) } &&
+ !result.content.private &&
+ shouldIncludeSelectedTab(state, result)
+ ) {
+ suggestions.add(
+ AwesomeBar.Suggestion(
+ provider = this,
+ id = result.id,
+ title = result.content.title.ifBlank { result.content.url },
+ description = resources.getString(R.string.switch_to_tab_description),
+ flags = setOf(AwesomeBar.Suggestion.Flag.OPEN_TAB),
+ icon = icon?.await()?.bitmap,
+ indicatorIcon = indicatorIcon,
+ onSuggestionClicked = {
+ selectTabUseCase(result.id)
+ emitOpenTabSuggestionClickedFact()
+ },
+ ),
+ )
+ }
+ }
+ return suggestions
+ }
+
+ private fun TabSessionState.contains(text: String) =
+ (content.url.contains(text, ignoreCase = true) || content.title.contains(text, ignoreCase = true))
+
+ private fun shouldIncludeSelectedTab(state: BrowserState, tab: TabSessionState): Boolean {
+ return if (excludeSelectedSession) {
+ tab.id != state.selectedTabId
+ } else {
+ true
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..57dbc0764a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-am/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ወደ ትር ቀይር</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..fba83a14a4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-an/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar a la pestanya</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..f259b0e7d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ar/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">انتقل إلى اللسان</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..91a762adcd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ast/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar a la llingüeta</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..6d2c625018
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-az/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Vərəqə keç</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..441313eef0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-azb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">تاغاگئچ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..fba5253729
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ban/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Gingsirang ka tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..a912a278fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-be/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Перайсці ў картку</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..f7774ae1ba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Превключване към раздел</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..baa6d8d0b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ট্যাবে সুইচ করুন</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..29739d7e2a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-br/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Mont d’an ivinell</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..3dbc774758
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-bs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Prebaci se na tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..96d58c8268
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ca/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Canvia a la pestanya</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..7b023f17a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cak/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Tiq\'asäx pa ri ruwi\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..8bdb5911d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Balhin sa tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..0f0e6dc8be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">بچۆ بۆ بازدەر</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..3168163d47
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-co/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Passà à l’unghjetta</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..1fc6913e26
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Přepnout na panel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..2b86ffef21
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-cy/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Newid i dab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..c26328510a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-da/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Skift til faneblad</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..54117ee1c2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-de/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Zum Tab wechseln</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..20f9d4f227
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">K rejtarikoju pśěpěś</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..9f77248fef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-el/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Εναλλαγή σε καρτέλα</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..279f8de89a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Switch to tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..279f8de89a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Switch to tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..41d6cb8be7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-eo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Iri al langeto</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..ac3f4fbcff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar a pestaña</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..9fc6b5708d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar a pestañas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..45f3320849
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar a la pestaña</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..9fc6b5708d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar a pestañas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..45f3320849
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-es/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar a la pestaña</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..3195eef4c7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-et/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Lülitu kaardile</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..4c11113224
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-eu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Aldatu fitxara</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..63e8366078
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">پرش به زبانه</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..12c31c17de
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ff/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Faatu e tabbere</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..48880f893a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Siirry välilehteen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..d466b76f20
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Aller à l’onglet</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..5b8f1d6ecb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Passe ae schede</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..fcdfb6ea4f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Wikselje nei ljepblêd</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..251257f66c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Téigh go cluaisín</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..be41a8dfb2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gd/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Geàrr leum gun taba</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..e151a1c3c4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar á lapela</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..754a48763d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Emoambue tendayképe</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..9dd2996bee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ટૅબ બદલો</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..0a37664e7d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">टैब पर जाएँ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..0114ae7239
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Prijeđi na karticu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..44af2381af
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">K rajtarkej přepinać</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..cf16261534
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Váltás erre a lapra</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..c815e52444
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Անցնել ներդիրին</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..bee98da7a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ia/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Cambiar al scheda</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..925745e961
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-in/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Pindah ke tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..987f6b05b0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-is/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Fara í flipa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..d7c6fc9067
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-it/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Passa alla scheda</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..c3dfd30fa4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-iw/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">מעבר ללשונית</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..4bda6a6be1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ja/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">タブを表示</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..b09863f379
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ka/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ჩანართზე გადასვლა</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..2a22ba999c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Betke ótiw</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..eaa7caa208
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kab/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Ddu ɣer yiccer</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..88933003b5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Бетке ауысу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..230f5332ad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Derbasî hilpekînê bibe</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..e496049175
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-kn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ಟ್ಯಾಬ್‌ಗೆ ಬದಲಾಯಿಸು</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..cd6b78a23d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ko/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">탭 전환</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..96cb97ca24
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lij/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Vanni a-o feuggio</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..982350196a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ປ່ຽນໄປຫາແທັບ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..f4d684fbab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-lt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Pereiti į kortelę</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..82d42aa364
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-mix/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Sama xikua</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..9ad794f510
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ml/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ടാബിലേക്ക് മാറുക</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..27e5148870
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-mr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">टॅबवर जा</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..39f6775b14
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-my/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">မျက်နှာစာ ပြောင်းကြည့်ပါ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..10e6b39340
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Bytt til fane</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..cd250d76c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ट्याबमा स्विच गर्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..f818fb05a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Wisselen naar tabblad</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..41ccb1a92d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Byt til fane</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..05ea755471
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-oc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Anar a l’onglet</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..ed1e59e7c4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-or/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ଆଉ ଏକ ଟ୍ୟାବକୁ ଯାଆନ୍ତୁ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..169b7ad140
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ਟੈਬ ਉੱਤੇ ਜਾਓ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..44288b3ee9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ٹیب تے جاؤ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..eb7ae55f31
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Przełącz na kartę</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..7eb7da507d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Mudar para a aba</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..0069216f34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Mudar para o separador</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..64f9aed7fb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-rm/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Midar al tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..568407ac92
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ro/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Comută la filă</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..93a3e8351f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ru/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Перейти на вкладку</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..4b60ce6ea0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sat/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ᱴᱮᱵᱽ ᱨᱮ ᱩᱪᱟᱹᱲᱚᱜ ᱢᱮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..0846d12f00
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Passa a s’ischeda</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..6986c5a40e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-si/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">පටිත්තට මාරුවන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..8a0af92c8f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Prejsť na kartu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..31dc0ce010
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ٹیب تے ون٘ڄو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..58f718695b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Preklopi na zavihek</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..787da36ff2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sq/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Kalo te skeda</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..0467ae7ff9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Пређи на језичак</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..0616506f0d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-su/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Pindah ka tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..b8609c83c1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Växla till flik</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..6d10437864
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ta/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">கீற்றிற்குத் தாவுக</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..2a6ab3d0cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-te/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ట్యాబుకు మారు</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..68f567e0a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Ба варақа гузаштан</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..ca0971cc47
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-th/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">สลับไปยังแท็บ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..111a2ee175
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Lumipat ng tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..5bf63305a1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Sekmeye geç</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..e50102e42e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-trs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Nādūnā riña \'ngō rakïj ñanj</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..8d613b4601
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-tt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Табка күчү</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..1aef03688b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ug/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">بەتكۈچكە ئالماشتۇرۇش</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..93a3e8351f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-uk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Перейти на вкладку</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..ff94dbf36f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-ur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">ٹیب پر جائیں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..f1514bc684
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-uz/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Varaqqa oʻtish</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..213bb8f517
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-vec/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Pasa a ƚa scheda</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..6cbde73e03
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-vi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Chuyển sang thẻ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..61d92cbee1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-yo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Ìyípadà sí táàbù</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..211ae6eb6b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">切换到标签页</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..6fea6aebc2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">切換至該分頁</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..7c8a232900
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+<?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 description for a suggestion that represents an opened tab.
+ Used to distinguish between History search suggestions from your
+ browsing history and your open tabs -->
+ <string name="switch_to_tab_description">Switch to tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/AwesomeBarFeatureTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/AwesomeBarFeatureTest.kt
new file mode 100644
index 0000000000..935b8f93d6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/AwesomeBarFeatureTest.kt
@@ -0,0 +1,305 @@
+/* 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.awesomebar
+
+import android.content.res.Resources
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
+import mozilla.components.feature.awesomebar.provider.DEFAULT_HISTORY_SUGGESTION_LIMIT
+import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
+import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class AwesomeBarFeatureTest {
+
+ @Test
+ fun `Feature connects toolbar with awesome bar`() {
+ val toolbar: Toolbar = mock()
+ val awesomeBar: AwesomeBar = mock()
+ doReturn(View(testContext)).`when`(awesomeBar).asView()
+
+ var listener: Toolbar.OnEditListener? = null
+
+ `when`(toolbar.setOnEditListener(any())).thenAnswer { invocation ->
+ listener = invocation.getArgument<Toolbar.OnEditListener>(0)
+ Unit
+ }
+
+ AwesomeBarFeature(awesomeBar, toolbar)
+
+ assertNotNull(listener)
+
+ listener!!.onStartEditing()
+
+ verify(awesomeBar).onInputStarted()
+
+ listener!!.onTextChanged("Hello")
+
+ verify(awesomeBar).onInputChanged("Hello")
+
+ listener!!.onStopEditing()
+
+ verify(awesomeBar).onInputCancelled()
+ }
+
+ @Test
+ fun `Feature connects awesome bar with toolbar`() {
+ val toolbar: Toolbar = mock()
+ val awesomeBar: AwesomeBar = mock()
+
+ var stopListener: (() -> Unit)? = null
+
+ `when`(awesomeBar.setOnStopListener(any())).thenAnswer { invocation ->
+ stopListener = invocation.getArgument<() -> Unit>(0)
+ Unit
+ }
+
+ AwesomeBarFeature(awesomeBar, toolbar)
+
+ assertNotNull(stopListener)
+
+ stopListener!!.invoke()
+
+ verify(toolbar).displayMode()
+ }
+
+ @Test
+ fun `addSessionProvider adds provider`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+ val resources: Resources = mock()
+ `when`(resources.getString(Mockito.anyInt())).thenReturn("Switch to tab")
+
+ verify(awesomeBar, never()).addProviders(any())
+
+ feature.addSessionProvider(resources, mock(), mock())
+
+ verify(awesomeBar).addProviders(any())
+ }
+
+ @Test
+ fun `addSearchProvider adds provider with specified search engine`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+
+ verify(awesomeBar, never()).addProviders(any())
+
+ val searchEngine: SearchEngine = mock()
+ feature.addSearchProvider(searchEngine, mock(), mock())
+
+ val provider = argumentCaptor<SearchSuggestionProvider>()
+ verify(awesomeBar).addProviders(provider.capture())
+ assertSame(searchEngine, provider.value.client.searchEngine)
+ }
+
+ @Test
+ fun `addSearchProvider adds provider for default search engine`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+
+ verify(awesomeBar, never()).addProviders(any())
+
+ val store: BrowserStore = mock()
+ feature.addSearchProvider(testContext, store = store, searchUseCase = mock(), fetchClient = mock())
+
+ val provider = argumentCaptor<SearchSuggestionProvider>()
+ verify(awesomeBar).addProviders(provider.capture())
+ assertSame(store, provider.value.client.store)
+ assertNull(provider.value.client.searchEngine)
+ }
+
+ @Test
+ fun `addSearchProvider adds browser engine to suggestion provider`() {
+ val engine: Engine = mock()
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+ feature.addSearchProvider(testContext, mock(), mock(), mock(), engine = engine)
+
+ val provider = argumentCaptor<SearchSuggestionProvider>()
+ verify(awesomeBar).addProviders(provider.capture())
+ assertSame(engine, provider.value.engine)
+
+ feature.addSearchProvider(mock(), mock(), mock(), engine = engine)
+ verify(awesomeBar, times(2)).addProviders(provider.capture())
+ assertSame(engine, provider.allValues.last().engine)
+ }
+
+ @Test
+ fun `addHistoryProvider adds provider`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+
+ verify(awesomeBar, never()).addProviders(any())
+
+ feature.addHistoryProvider(mock(), mock())
+
+ verify(awesomeBar).addProviders(any())
+ }
+
+ @Test
+ fun `addHistoryProvider adds browser engine to suggestion provider`() {
+ val engine: Engine = mock()
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+ feature.addHistoryProvider(mock(), mock(), engine = engine)
+
+ val provider = argumentCaptor<HistoryStorageSuggestionProvider>()
+ verify(awesomeBar).addProviders(provider.capture())
+ assertSame(engine, provider.value.engine)
+ }
+
+ @Test
+ fun `addHistoryProvider adds the limit of suggestions to be returned to suggestion provider if positive`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+ feature.addHistoryProvider(
+ historyStorage = mock(),
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 42,
+ )
+
+ val provider = argumentCaptor<HistoryStorageSuggestionProvider>()
+ verify(awesomeBar).addProviders(provider.capture())
+ assertSame(42, provider.value.maxNumberOfSuggestions)
+ }
+
+ @Test
+ fun `addHistoryProvider does not add the limit of suggestions to be returned to suggestion provider if negative`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+ feature.addHistoryProvider(
+ historyStorage = mock(),
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = -1,
+ )
+
+ val provider = argumentCaptor<HistoryStorageSuggestionProvider>()
+ verify(awesomeBar).addProviders(provider.capture())
+ assertSame(DEFAULT_HISTORY_SUGGESTION_LIMIT, provider.value.maxNumberOfSuggestions)
+ }
+
+ @Test
+ fun `addHistoryProvider does not add the limit of suggestions to be returned to suggestion provider if 0`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+ feature.addHistoryProvider(
+ historyStorage = mock(),
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 0,
+ )
+
+ val provider = argumentCaptor<HistoryStorageSuggestionProvider>()
+ verify(awesomeBar).addProviders(provider.capture())
+ assertSame(DEFAULT_HISTORY_SUGGESTION_LIMIT, provider.value.maxNumberOfSuggestions)
+ }
+
+ @Test
+ fun `addClipboardProvider adds provider`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+
+ verify(awesomeBar, never()).addProviders(any())
+
+ feature.addClipboardProvider(testContext, mock())
+
+ verify(awesomeBar).addProviders(any())
+ }
+
+ @Test
+ fun `addClipboardProvider adds browser engine to suggestion provider`() {
+ val engine: Engine = mock()
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+ feature.addClipboardProvider(testContext, mock(), engine = engine)
+
+ val provider = argumentCaptor<ClipboardSuggestionProvider>()
+ verify(awesomeBar).addProviders(provider.capture())
+ assertSame(engine, provider.value.engine)
+ }
+
+ @Test
+ fun `addSearchActionProvider adds provider`() {
+ val awesomeBar: AwesomeBar = mock()
+
+ val feature = AwesomeBarFeature(awesomeBar, mock())
+
+ verify(awesomeBar, never()).addProviders(any())
+
+ feature.addSearchActionProvider(mock(), mock())
+
+ verify(awesomeBar).addProviders(any())
+ }
+
+ @Test
+ fun `Feature invokes custom start and complete hooks`() {
+ val toolbar: Toolbar = mock()
+ val awesomeBar: AwesomeBar = mock()
+
+ var startInvoked = false
+ var completeInvoked = false
+
+ var listener: Toolbar.OnEditListener? = null
+
+ `when`(toolbar.setOnEditListener(any())).thenAnswer { invocation ->
+ listener = invocation.getArgument<Toolbar.OnEditListener>(0)
+ Unit
+ }
+
+ AwesomeBarFeature(
+ awesomeBar,
+ toolbar,
+ onEditStart = { startInvoked = true },
+ onEditComplete = { completeInvoked = true },
+ )
+
+ assertFalse(startInvoked)
+ assertFalse(completeInvoked)
+
+ listener!!.onStartEditing()
+
+ assertTrue(startInvoked)
+ assertFalse(completeInvoked)
+ startInvoked = false
+
+ listener!!.onStopEditing()
+
+ assertFalse(startInvoked)
+ assertTrue(completeInvoked)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/facts/AwesomeBarFactsTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/facts/AwesomeBarFactsTest.kt
new file mode 100644
index 0000000000..9eb6589691
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/facts/AwesomeBarFactsTest.kt
@@ -0,0 +1,79 @@
+/* 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.awesomebar.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class AwesomeBarFactsTest {
+
+ @Test
+ fun `Emits facts for current state`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitBookmarkSuggestionClickedFact()
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.BOOKMARK_SUGGESTION_CLICKED, item)
+ }
+
+ emitClipboardSuggestionClickedFact()
+
+ assertEquals(2, facts.size)
+ facts[1].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.CLIPBOARD_SUGGESTION_CLICKED, item)
+ }
+
+ emitHistorySuggestionClickedFact()
+
+ assertEquals(3, facts.size)
+ facts[2].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.HISTORY_SUGGESTION_CLICKED, item)
+ }
+
+ emitSearchActionClickedFact()
+ assertEquals(4, facts.size)
+ facts[3].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.SEARCH_ACTION_CLICKED, item)
+ }
+
+ emitSearchSuggestionClickedFact()
+ assertEquals(5, facts.size)
+ facts[4].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.SEARCH_SUGGESTION_CLICKED, item)
+ }
+
+ emitOpenTabSuggestionClickedFact()
+ assertEquals(6, facts.size)
+ facts[5].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.OPENED_TAB_SUGGESTION_CLICKED, item)
+ }
+
+ emitSearchTermSuggestionClickedFact()
+ assertEquals(7, facts.size)
+ facts[6].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.SEARCH_TERM_SUGGESTION_CLICKED, item)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProviderTest.kt
new file mode 100644
index 0000000000..a3b82d2872
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProviderTest.kt
@@ -0,0 +1,335 @@
+/* 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.awesomebar.provider
+
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.storage.BookmarkInfo
+import mozilla.components.concept.storage.BookmarkNode
+import mozilla.components.concept.storage.BookmarkNodeType
+import mozilla.components.concept.storage.BookmarksStorage
+import mozilla.components.support.ktx.android.net.sameHostWithoutMobileSubdomainAs
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.utils.StorageUtils.levenshteinDistance
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.UUID
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class BookmarksStorageSuggestionProviderTest {
+
+ private val bookmarks = TestableBookmarksStorage()
+
+ private val newItem = BookmarkNode(
+ BookmarkNodeType.ITEM,
+ "123",
+ "456",
+ null,
+ "Mozilla",
+ "http://www.mozilla.org",
+ 0,
+ null,
+ )
+
+ @Test
+ fun `Provider returns empty list when text is empty`() = runTest {
+ val provider = BookmarksStorageSuggestionProvider(mock(), mock())
+
+ val suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `Provider cleanups all previous read operations when text is empty`() = runTest {
+ val provider = BookmarksStorageSuggestionProvider(mock(), mock())
+
+ provider.onInputChanged("")
+
+ verify(provider.bookmarksStorage, never()).cancelReads()
+ verify(provider.bookmarksStorage).cancelReads("")
+ }
+
+ @Test
+ fun `Provider cleanups all previous read operations when text is not empty`() = runTest {
+ val storage = spy(bookmarks)
+ val provider = BookmarksStorageSuggestionProvider(storage, mock())
+ storage.addItem("Mobile", newItem.url!!, newItem.title!!, null)
+ val orderVerifier = inOrder(storage)
+
+ provider.onInputChanged("moz")
+
+ orderVerifier.verify(provider.bookmarksStorage, never()).cancelReads()
+ orderVerifier.verify(provider.bookmarksStorage).cancelReads("moz")
+ orderVerifier.verify(provider.bookmarksStorage).searchBookmarks(eq("moz"), anyInt())
+ }
+
+ @Test
+ fun `Provider returns suggestions from configured bookmarks storage`() = runTest {
+ val provider = BookmarksStorageSuggestionProvider(bookmarks, mock())
+
+ val id = bookmarks.addItem("Mobile", newItem.url!!, newItem.title!!, null)
+
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ assertEquals(id, suggestions[0].id)
+
+ suggestions = provider.onInputChanged("mozi")
+ assertEquals(1, suggestions.size)
+ assertEquals(id, suggestions[0].id)
+
+ assertEquals("http://www.mozilla.org", suggestions[0].description)
+ }
+
+ @Test
+ fun `Provider does not return duplicate suggestions`() = runTest {
+ val provider = BookmarksStorageSuggestionProvider(bookmarks, mock())
+
+ for (i in 1..20) {
+ bookmarks.addItem("Mobile", newItem.url!!, newItem.title!!, null)
+ }
+
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ }
+
+ @Test
+ fun `Provider limits number of returned unique suggestions`() = runTest {
+ val provider = BookmarksStorageSuggestionProvider(bookmarks, mock())
+
+ for (i in 1..100) {
+ bookmarks.addItem(
+ "Mobile",
+ "${newItem.url!!} + $i",
+ newItem.title!!,
+ null,
+ )
+ }
+
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(20, suggestions.size)
+ }
+
+ @Test
+ fun `provider calls speculative connect for URL of first suggestion`() = runTest {
+ val engine: Engine = mock()
+ val provider = BookmarksStorageSuggestionProvider(bookmarks, mock(), engine = engine)
+
+ var suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ verify(engine, never()).speculativeConnect(anyString())
+
+ val id = bookmarks.addItem("Mobile", newItem.url!!, newItem.title!!, null)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ assertEquals(id, suggestions[0].id)
+ assertEquals("http://www.mozilla.org", suggestions[0].description)
+ verify(engine, times(1)).speculativeConnect(eq(suggestions[0].description!!))
+ }
+
+ @Test
+ fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runTest {
+ val engine: Engine = mock()
+ val provider = BookmarksStorageSuggestionProvider(bookmarks, mock(), engine = engine, showEditSuggestion = false)
+
+ var suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ verify(engine, never()).speculativeConnect(anyString())
+
+ val id = bookmarks.addItem("Mobile", newItem.url!!, newItem.title!!, null)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ assertEquals(id, suggestions[0].id)
+ assertNull(suggestions[0].editSuggestion)
+ assertEquals("http://www.mozilla.org", suggestions[0].description)
+ verify(engine, times(1)).speculativeConnect(eq(suggestions[0].description!!))
+ }
+
+ @Test
+ fun `GIVEN no external filter WHEN querying bookmarks THEN query a low number of results`() = runTest {
+ val bookmarksSpy = spy(bookmarks)
+ val provider = BookmarksStorageSuggestionProvider(
+ bookmarksStorage = bookmarksSpy,
+ loadUrlUseCase = mock(),
+ )
+
+ provider.onInputChanged("moz")
+
+ verify(bookmarksSpy).searchBookmarks("moz", BOOKMARKS_SUGGESTION_LIMIT)
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying bookmarks THEN query more than the usual default results for the host url`() = runTest {
+ val bookmarksSpy = spy(bookmarks)
+ val provider = BookmarksStorageSuggestionProvider(
+ bookmarksStorage = bookmarksSpy,
+ loadUrlUseCase = mock(),
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("https://www.test.com".toUri())
+ },
+ )
+
+ provider.onInputChanged("moz")
+
+ verify(bookmarksSpy).searchBookmarks(
+ "moz",
+ BOOKMARKS_SUGGESTION_LIMIT * BOOKMARKS_RESULTS_TO_FILTER_SCALE_FACTOR,
+ )
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying bookmarks THEN return only the results that pass through the filter`() = runTest {
+ val bookmarksSpy = spy(bookmarks)
+ val provider = BookmarksStorageSuggestionProvider(
+ bookmarksStorage = bookmarksSpy,
+ loadUrlUseCase = mock(),
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("https://mozilla.com".toUri())
+ },
+ )
+
+ bookmarks.addItem("Other", "https://mozilla.com/firefox", newItem.title!!, null)
+ bookmarks.addItem("Test", "https://mozilla.com/focus", newItem.title!!, null)
+ bookmarks.addItem("Mozilla", "https://mozilla.org/firefox", newItem.title!!, null)
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(2, suggestions.size)
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/focus"))
+ }
+
+ @SuppressWarnings
+ class TestableBookmarksStorage : BookmarksStorage {
+ val bookmarkMap: HashMap<String, BookmarkNode> = hashMapOf()
+
+ override suspend fun warmUp() {
+ throw NotImplementedError()
+ }
+
+ override suspend fun getTree(guid: String, recursive: Boolean): BookmarkNode? {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun getBookmark(guid: String): BookmarkNode? {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun getBookmarksWithUrl(url: String): List<BookmarkNode> {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun getRecentBookmarks(limit: Int, maxAge: Long?, currentTime: Long): List<BookmarkNode> {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun searchBookmarks(query: String, limit: Int): List<BookmarkNode> =
+ synchronized(bookmarkMap) {
+ data class Hit(val key: String, val score: Int)
+
+ val urlMatches = bookmarkMap.asSequence().map {
+ Hit(it.value.guid, levenshteinDistance(it.value.url!!, query))
+ }
+ val titleMatches = bookmarkMap.asSequence().map {
+ Hit(it.value.guid, levenshteinDistance(it.value.title ?: "", query))
+ }
+ val matchedUrls = mutableMapOf<String, Int>()
+ urlMatches.plus(titleMatches).forEach {
+ if (matchedUrls.containsKey(it.key) && matchedUrls[it.key]!! < it.score) {
+ matchedUrls[it.key] = it.score
+ } else {
+ matchedUrls[it.key] = it.score
+ }
+ }
+ // Calculate maxScore so that we can invert our scoring.
+ // Lower Levenshtein distance should produce a higher score.
+ urlMatches.maxByOrNull { it.score }?.score
+ ?: return@synchronized listOf()
+
+ // TODO exclude non-matching results entirely? Score that implies complete mismatch.
+ matchedUrls.asSequence().sortedBy { it.value }.map {
+ bookmarkMap[it.key]!!
+ }.take(limit).toList()
+ }
+
+ override suspend fun countBookmarksInTrees(guids: List<String>): UInt {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun addItem(
+ parentGuid: String,
+ url: String,
+ title: String,
+ position: UInt?,
+ ): String {
+ val id = UUID.randomUUID().toString()
+ bookmarkMap[id] =
+ BookmarkNode(BookmarkNodeType.ITEM, id, parentGuid, position, title, url, 0, null)
+ return id
+ }
+
+ override suspend fun addFolder(parentGuid: String, title: String, position: UInt?): String {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun addSeparator(parentGuid: String, position: UInt?): String {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun updateNode(guid: String, info: BookmarkInfo) {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun deleteNode(guid: String): Boolean {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override suspend fun runMaintenance(dbSizeLimit: UInt) {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override fun cleanup() {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override fun cancelWrites() {
+ // "Not needed for the test"
+ throw NotImplementedError()
+ }
+
+ override fun cancelReads() {
+ // no-op
+ }
+
+ override fun cancelReads(nextQuery: String) {
+ // no-op
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProviderTest.kt
new file mode 100644
index 0000000000..1a93858d19
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProviderTest.kt
@@ -0,0 +1,245 @@
+/* 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.awesomebar.provider
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class ClipboardSuggestionProviderTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val clipboardManager: ClipboardManager
+ get() = testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+ @Test
+ fun `provider returns empty list by default`() = runTestOnMain {
+ clipboardManager.clearPrimaryClip()
+
+ val provider = ClipboardSuggestionProvider(testContext, mock())
+
+ provider.onInputStarted()
+ val suggestions = provider.onInputChanged("Hello")
+
+ assertEquals(0, suggestions.size)
+ }
+
+ @Test
+ fun `provider returns empty list for non plain text clip`() = runTestOnMain {
+ clipboardManager.setPrimaryClip(
+ ClipData.newHtmlText(
+ "Label",
+ "Hello mozilla.org",
+ "<b>This is HTML on mozilla.org</b>",
+ ),
+ )
+
+ assertNull(getSuggestion())
+ }
+
+ @Test
+ fun `provider should return suggestion if clipboard contains url`() = runTestOnMain {
+ assertClipboardYieldsUrl(
+ "https://www.mozilla.org",
+ "https://www.mozilla.org",
+ )
+
+ assertClipboardYieldsUrl(
+ "https : //mozilla.org is a broken firefox.com URL",
+ "mozilla.org",
+ )
+
+ assertClipboardYieldsUrl(
+ """
+ This is a longer
+ text over multiple lines
+ and it https://www.mozilla.org contains
+ URLs as well. https://www.firefox.com
+ """,
+ "https://www.mozilla.org",
+ )
+
+ assertClipboardYieldsUrl(
+ """
+ This is a longer
+ text over multiple lines
+ and it www.mozilla.org contains
+ URLs as well. https://www.firefox.com
+ """,
+ "https://www.firefox.com",
+ )
+
+ assertClipboardYieldsUrl(
+ """
+ mozilla.org
+ firefox.com
+ mozilla.org/en-US/firefox/developer/
+ """,
+ "mozilla.org",
+ )
+
+ // Note that the new, less-lenient URL detection process (Issue #5594) allows the dot
+ // at the end of the IP address to be part of the URL. Gecko handles this.
+ assertClipboardYieldsUrl("My IP is 192.168.0.1.", "192.168.0.1.")
+ }
+
+ @Test
+ fun `provider return suggestion on input start`() = runTestOnMain {
+ clipboardManager.setPrimaryClip(ClipData.newPlainText("Test label", "https://www.mozilla.org"))
+
+ val provider = ClipboardSuggestionProvider(testContext, mock())
+ val suggestions = provider.onInputStarted()
+
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions.firstOrNull()
+ assertNotNull(suggestion!!)
+
+ assertEquals("https://www.mozilla.org", suggestion.description)
+ }
+
+ @Test
+ fun `provider should return no suggestions if clipboard does not contain a url`() = runTestOnMain {
+ assertClipboardYieldsNothing("Hello World")
+
+ assertClipboardYieldsNothing("Is this mozilla org")
+ }
+
+ @Test
+ fun `provider should allow customization of title and icon on suggestion`() = runTestOnMain {
+ clipboardManager.setPrimaryClip(ClipData.newPlainText("Test label", "http://mozilla.org"))
+ val bitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888)
+ val provider = ClipboardSuggestionProvider(
+ testContext,
+ mock(),
+ title = "My test title",
+ icon = bitmap,
+ requireEmptyText = false,
+ )
+
+ val suggestion = run {
+ provider.onInputStarted()
+ val suggestions = provider.onInputChanged("Hello")
+
+ suggestions.firstOrNull()
+ }
+
+ assertEquals(bitmap, suggestion?.icon)
+ assertEquals("My test title", suggestion?.title)
+ }
+
+ @Test
+ fun `clicking suggestion loads url`() = runTestOnMain {
+ clipboardManager.setPrimaryClip(
+ ClipData.newPlainText(
+ "Label",
+ "Hello Mozilla, https://www.mozilla.org",
+ ),
+ )
+
+ val useCase: SessionUseCases.LoadUrlUseCase = mock()
+
+ val provider = ClipboardSuggestionProvider(testContext, useCase, requireEmptyText = false)
+
+ provider.onInputStarted()
+ val suggestions = provider.onInputChanged("Hello")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions.first()
+
+ verify(useCase, never()).invoke(any(), any(), any())
+
+ assertNotNull(suggestion.onSuggestionClicked)
+ suggestion.onSuggestionClicked!!.invoke()
+
+ verify(useCase).invoke(eq("https://www.mozilla.org"), any(), any())
+ }
+
+ @Test
+ fun `provider returns empty list for non-empty text if empty text required`() = runTestOnMain {
+ clipboardManager.setPrimaryClip(
+ ClipData.newPlainText(
+ "Label",
+ "Hello Mozilla, https://www.mozilla.org",
+ ),
+ )
+
+ val provider = ClipboardSuggestionProvider(testContext, mock(), requireEmptyText = true)
+ val suggestions = provider.onInputChanged("Hello")
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `provider calls speculative connect for URL of suggestion`() = runTestOnMain {
+ val engine: Engine = mock()
+ val provider = ClipboardSuggestionProvider(testContext, mock(), engine = engine)
+ var suggestions = provider.onInputStarted()
+ assertTrue(suggestions.isEmpty())
+ verify(engine, never()).speculativeConnect(anyString())
+
+ clipboardManager.setPrimaryClip(ClipData.newPlainText("Test label", "https://www.mozilla.org"))
+ suggestions = provider.onInputStarted()
+ assertEquals(1, suggestions.size)
+ verify(engine, times(1)).speculativeConnect(eq("https://www.mozilla.org"))
+
+ val suggestion = suggestions.firstOrNull()
+ assertNotNull(suggestion!!)
+ assertEquals("https://www.mozilla.org", suggestion.description)
+ }
+
+ private suspend fun assertClipboardYieldsUrl(text: String, url: String) {
+ val suggestion = getSuggestionWithClipboard(text)
+
+ assertNotNull(suggestion)
+
+ assertEquals(url, suggestion!!.description)
+ }
+
+ private suspend fun assertClipboardYieldsNothing(text: String) {
+ val suggestion = getSuggestionWithClipboard(text)
+ assertNull(suggestion)
+ }
+
+ private suspend fun getSuggestionWithClipboard(text: String): AwesomeBar.Suggestion? {
+ clipboardManager.setPrimaryClip(ClipData.newPlainText("Test label", text))
+ return getSuggestion()
+ }
+
+ private suspend fun getSuggestion(): AwesomeBar.Suggestion? {
+ val provider = ClipboardSuggestionProvider(testContext, mock(), requireEmptyText = false)
+
+ provider.onInputStarted()
+ val suggestions = provider.onInputChanged("Hello")
+
+ return suggestions.firstOrNull()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProviderTest.kt
new file mode 100644
index 0000000000..d6da3eb95d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProviderTest.kt
@@ -0,0 +1,378 @@
+/* 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.awesomebar.provider
+
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.storage.DocumentType
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.concept.storage.HistoryMetadataStorage
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.concept.storage.SearchResult
+import mozilla.components.support.ktx.android.net.sameHostWithoutMobileSubdomainAs
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class CombinedHistorySuggestionProviderTest {
+
+ private val historyEntry = HistoryMetadata(
+ key = HistoryMetadataKey("http://www.mozilla.com", null, null),
+ title = "mozilla",
+ createdAt = System.currentTimeMillis(),
+ updatedAt = System.currentTimeMillis(),
+ totalViewTime = 10,
+ documentType = DocumentType.Regular,
+ previewImageUrl = null,
+ )
+
+ @Test
+ fun `GIVEN history items exists WHEN onInputChanged is called with empty text THEN return empty suggestions list`() = runTest {
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(listOf(historyEntry)).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ val history: HistoryStorage = mock()
+ doReturn(listOf(SearchResult("id", "http://www.mozilla.com", 10))).`when`(history).getSuggestions(eq("moz"), anyInt())
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock())
+
+ assertTrue(provider.onInputChanged("").isEmpty())
+ assertTrue(provider.onInputChanged(" ").isEmpty())
+ }
+
+ @Test
+ fun `WHEN onInputChanged is called with empty text THEN cancel all previous read operations`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock())
+
+ provider.onInputChanged("")
+
+ verify(history, never()).cancelReads()
+ verify(metadata, never()).cancelReads()
+ verify(history).cancelReads("")
+ verify(metadata).cancelReads("")
+ }
+
+ @Test
+ fun `GIVEN more suggestions asked than metadata items exist WHEN user changes input THEN return a combined list of suggestions`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(listOf(historyEntry)).`when`(storage).queryHistoryMetadata(eq("moz"), anyInt())
+ val history: HistoryStorage = mock()
+ doReturn(listOf(SearchResult("id", "http://www.mozilla.com/firefox/", 10))).`when`(history).getSuggestions(eq("moz"), anyInt())
+ val provider = CombinedHistorySuggestionProvider(history, storage, mock())
+
+ val result = provider.onInputChanged("moz")
+
+ assertEquals(2, result.size)
+ assertEquals("http://www.mozilla.com", result[0].description)
+ assertEquals("http://www.mozilla.com/firefox/", result[1].description)
+ }
+
+ @Test
+ fun `WHEN onInputChanged is called with non empty text THEN cancel all previous read operations`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(listOf(historyEntry)).`when`(storage).queryHistoryMetadata(eq("moz"), anyInt())
+ val history: HistoryStorage = mock()
+ doReturn(emptyList<SearchResult>()).`when`(history).getSuggestions(eq("moz"), anyInt())
+ val provider = CombinedHistorySuggestionProvider(history, storage, mock())
+ val orderVerifier = Mockito.inOrder(storage, history)
+
+ provider.onInputChanged("moz")
+
+ orderVerifier.verify(history, never()).cancelReads()
+ orderVerifier.verify(storage, never()).cancelReads()
+ orderVerifier.verify(history).cancelReads("moz")
+ orderVerifier.verify(storage).cancelReads("moz")
+ orderVerifier.verify(storage).queryHistoryMetadata(eq("moz"), anyInt())
+ orderVerifier.verify(history).getSuggestions(eq("moz"), anyInt())
+ }
+
+ @Test
+ fun `GIVEN fewer suggestions asked than metadata items exist WHEN user changes input THEN return suggestions only based on metadata items`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(listOf(historyEntry)).`when`(storage).queryHistoryMetadata(eq("moz"), anyInt())
+ val history: HistoryStorage = mock()
+ doReturn(listOf(SearchResult("id", "http://www.mozilla.com/firefox/", 10))).`when`(history).getSuggestions(eq("moz"), anyInt())
+ val provider = CombinedHistorySuggestionProvider(history, storage, mock(), maxNumberOfSuggestions = 1)
+
+ val result = provider.onInputChanged("moz")
+
+ assertEquals(1, result.size)
+ assertEquals("http://www.mozilla.com", result[0].description)
+ }
+
+ @Test
+ fun `GIVEN only storage history items exist WHEN user changes input THEN return suggestions only based on storage items`() = runTest {
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ val history: HistoryStorage = mock()
+ doReturn(listOf(SearchResult("id", "http://www.mozilla.com/firefox/", 10))).`when`(history).getSuggestions(eq("moz"), anyInt())
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock(), maxNumberOfSuggestions = 1)
+
+ val result = provider.onInputChanged("moz")
+
+ assertEquals(1, result.size)
+ assertEquals("http://www.mozilla.com/firefox/", result[0].description)
+ }
+
+ @Test
+ fun `GIVEN duplicated metadata and storage entries WHEN user changes input THEN return distinct suggestions`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(listOf(historyEntry)).`when`(storage).queryHistoryMetadata(eq("moz"), anyInt())
+ val history: HistoryStorage = mock()
+ doReturn(listOf(SearchResult("id", "http://www.mozilla.com", 10))).`when`(history).getSuggestions(eq("moz"), anyInt())
+ val provider = CombinedHistorySuggestionProvider(history, storage, mock())
+
+ val result = provider.onInputChanged("moz")
+
+ assertEquals(1, result.size)
+ assertEquals("http://www.mozilla.com", result[0].description)
+ }
+
+ @Test
+ fun `GIVEN a combined list of suggestions WHEN history results exist THEN urls are deduped and scores are adjusted`() = runTest {
+ val metadataEntry1 = HistoryMetadata(
+ key = HistoryMetadataKey("https://www.mozilla.com", null, null),
+ title = "mozilla",
+ createdAt = System.currentTimeMillis(),
+ updatedAt = System.currentTimeMillis(),
+ totalViewTime = 10,
+ documentType = DocumentType.Regular,
+ previewImageUrl = null,
+ )
+
+ val metadataEntry2 = HistoryMetadata(
+ key = HistoryMetadataKey("https://www.mozilla.com/firefox", null, null),
+ title = "firefox",
+ createdAt = System.currentTimeMillis(),
+ updatedAt = System.currentTimeMillis(),
+ totalViewTime = 20,
+ documentType = DocumentType.Regular,
+ previewImageUrl = null,
+ )
+
+ val searchResult1 = SearchResult(
+ id = "1",
+ url = "https://www.mozilla.com",
+ title = "mozilla",
+ score = 1,
+ )
+
+ val searchResult2 = SearchResult(
+ id = "2",
+ url = "https://www.mozilla.com/pocket",
+ title = "pocket",
+ score = 2,
+ )
+
+ val metadataStorage: HistoryMetadataStorage = mock()
+ val historyStorage: HistoryStorage = mock()
+ doReturn(listOf(metadataEntry2, metadataEntry1)).`when`(metadataStorage).queryHistoryMetadata(eq("moz"), anyInt())
+ doReturn(listOf(searchResult1, searchResult2)).`when`(historyStorage).getSuggestions(eq("moz"), anyInt())
+
+ val provider = CombinedHistorySuggestionProvider(historyStorage, metadataStorage, mock())
+
+ val result = provider.onInputChanged("moz")
+
+ assertEquals(3, result.size)
+ assertEquals("https://www.mozilla.com/firefox", result[0].description)
+ assertEquals(4, result[0].score)
+
+ assertEquals("https://www.mozilla.com", result[1].description)
+ assertEquals(3, result[1].score)
+
+ assertEquals("https://www.mozilla.com/pocket", result[2].description)
+ assertEquals(2, result[2].score)
+ }
+
+ @Test
+ fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runTest {
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ val history: HistoryStorage = mock()
+ doReturn(listOf(SearchResult("id", "http://www.mozilla.com/firefox/", 10))).`when`(history).getSuggestions(eq("moz"), anyInt())
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock(), maxNumberOfSuggestions = 1, showEditSuggestion = false)
+
+ val result = provider.onInputChanged("moz")
+
+ assertEquals(1, result.size)
+ assertEquals("http://www.mozilla.com/firefox/", result[0].description)
+ assertNull(result[0].editSuggestion)
+ }
+
+ @Test
+ fun `WHEN provider max number of suggestions is changed THEN the number of return suggestions is updated`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ },
+ ).`when`(history).getSuggestions(eq("moz"), anyInt())
+
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock(), showEditSuggestion = false)
+
+ provider.setMaxNumberOfSuggestions(2)
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(2, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(22)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(22, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(0)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(22, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(45)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+ }
+
+ @Test
+ fun `WHEN reset provider max number of suggestions THEN the number of return suggestions is reset to default`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ },
+ ).`when`(history).getSuggestions(eq("moz"), anyInt())
+
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock(), showEditSuggestion = false)
+
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(5, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(45)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+
+ provider.resetToDefaultMaxSuggestions()
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(5, suggestions.size)
+ }
+
+ @Test
+ fun `GIVEN no external filter WHEN querying history THEN query a low number of results`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ doReturn(emptyList<SearchResult>()).`when`(history).getSuggestions(eq("moz"), anyInt())
+
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock(), showEditSuggestion = false)
+
+ provider.onInputChanged("moz")
+
+ verify(history).getSuggestions("moz", DEFAULT_COMBINED_SUGGESTION_LIMIT)
+ verify(metadata).queryHistoryMetadata("moz", DEFAULT_COMBINED_SUGGESTION_LIMIT)
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN query more than the usual default results for the host url`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ doReturn(emptyList<SearchResult>()).`when`(history).getSuggestions(eq("moz"), anyInt())
+ val expectedQueryCount = DEFAULT_COMBINED_SUGGESTION_LIMIT * COMBINED_HISTORY_RESULTS_TO_FILTER_SCALE_FACTOR
+
+ val provider = CombinedHistorySuggestionProvider(
+ historyStorage = history,
+ historyMetadataStorage = metadata,
+ loadUrlUseCase = mock(),
+ showEditSuggestion = false,
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("test".toUri())
+ },
+ )
+
+ provider.onInputChanged("moz")
+
+ verify(history).getSuggestions("moz", expectedQueryCount)
+ verify(metadata).queryHistoryMetadata("moz", expectedQueryCount)
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN return only the results that pass through the filter`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(metadata).queryHistoryMetadata(anyString(), anyInt())
+ doReturn(
+ listOf(
+ SearchResult("3", "https://mozilla.com/firefox", 10),
+ SearchResult("5", "http://firefox.com/mozilla", 10),
+ SearchResult("2", "http://allizom.com/focus/", 10),
+ SearchResult("4", "https://mozilla.com/thunderbird", 10),
+ SearchResult("16", "http://www.mozilla.com/firefox", 22),
+ ),
+ ).`when`(history).getSuggestions(anyString(), anyInt())
+
+ val provider = CombinedHistorySuggestionProvider(
+ historyStorage = history,
+ historyMetadataStorage = metadata,
+ loadUrlUseCase = mock(),
+ showEditSuggestion = false,
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("https://mozilla.com".toUri())
+ },
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(3, suggestions.size)
+ assertTrue(suggestions.map { it.description }.contains("http://www.mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/thunderbird"))
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN return the results containing mobile domains`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(metadata).queryHistoryMetadata(anyString(), anyInt())
+ doReturn(
+ listOf(
+ SearchResult("3", "https://m.mozilla.com/firefox", 10),
+ SearchResult("5", "http://firefox.com/mozilla", 10),
+ SearchResult("2", "http://allizom.com/focus/", 10),
+ SearchResult("4", "https://mozilla.com/thunderbird", 10),
+ SearchResult("16", "http://www.mobile.mozilla.com/firefox", 22),
+ ),
+ ).`when`(history).getSuggestions(anyString(), anyInt())
+
+ val provider = CombinedHistorySuggestionProvider(
+ historyStorage = history,
+ historyMetadataStorage = metadata,
+ loadUrlUseCase = mock(),
+ showEditSuggestion = false,
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("https://mozilla.com".toUri())
+ },
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(3, suggestions.size)
+ assertTrue(suggestions.map { it.description }.contains("http://www.mobile.mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://m.mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/thunderbird"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProviderTest.kt
new file mode 100644
index 0000000000..91a3389cae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProviderTest.kt
@@ -0,0 +1,322 @@
+/* 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.awesomebar.provider
+
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.storage.DocumentType
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.concept.storage.HistoryMetadataStorage
+import mozilla.components.feature.awesomebar.facts.AwesomeBarFacts
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+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.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class HistoryMetadataSuggestionProviderTest {
+ private val historyEntry = HistoryMetadata(
+ key = HistoryMetadataKey("http://www.mozilla.com", null, null),
+ title = "mozilla",
+ createdAt = System.currentTimeMillis(),
+ updatedAt = System.currentTimeMillis(),
+ totalViewTime = 10,
+ documentType = DocumentType.Regular,
+ previewImageUrl = null,
+ )
+
+ @Before
+ fun setup() {
+ Facts.clearProcessors()
+ }
+
+ @Test
+ fun `provider returns empty list when text is empty`() = runTest {
+ val provider = HistoryMetadataSuggestionProvider(mock(), mock())
+
+ val suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `provider cleanups all previous read operations when text is empty`() = runTest {
+ val provider = HistoryMetadataSuggestionProvider(mock(), mock())
+
+ provider.onInputChanged("")
+
+ verify(provider.historyStorage, never()).cancelReads()
+ verify(provider.historyStorage).cancelReads("")
+ }
+
+ @Test
+ fun `provider cleanups all previous read operations when text is not empty`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(listOf(historyEntry)).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
+ val provider = HistoryMetadataSuggestionProvider(storage, mock())
+ val orderVerifier = inOrder(storage)
+
+ provider.onInputChanged("moz")
+
+ orderVerifier.verify(provider.historyStorage, never()).cancelReads()
+ orderVerifier.verify(provider.historyStorage).cancelReads("moz")
+ orderVerifier.verify(provider.historyStorage).queryHistoryMetadata(eq("moz"), anyInt())
+ }
+
+ @Test
+ fun `provider returns suggestions from configured history storage`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+
+ whenever(storage.queryHistoryMetadata("moz", DEFAULT_METADATA_SUGGESTION_LIMIT)).thenReturn(listOf(historyEntry))
+
+ val provider = HistoryMetadataSuggestionProvider(storage, mock())
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ assertEquals(historyEntry.key.url, suggestions[0].description)
+ assertEquals(historyEntry.title, suggestions[0].title)
+ }
+
+ @Test
+ fun `provider limits number of returned suggestions to 5 by default`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
+ val provider = HistoryMetadataSuggestionProvider(storage, mock())
+
+ provider.onInputChanged("moz")
+
+ verify(storage).queryHistoryMetadata("moz", 5)
+ Unit
+ }
+
+ @Test
+ fun `provider allows lowering the number of returned suggestions beneath the default`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
+ val provider = HistoryMetadataSuggestionProvider(
+ historyStorage = storage,
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 2,
+ )
+
+ provider.onInputChanged("moz")
+
+ verify(storage).queryHistoryMetadata("moz", 2)
+ Unit
+ }
+
+ @Test
+ fun `provider allows increasing the number of returned suggestions above the default`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
+ val provider = HistoryMetadataSuggestionProvider(
+ historyStorage = storage,
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 8,
+ )
+
+ provider.onInputChanged("moz")
+
+ verify(storage).queryHistoryMetadata("moz", 8)
+ Unit
+ }
+
+ @Test
+ fun `provider only as suggestions pages on which users actually spent some time`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ val historyEntries = mutableListOf<HistoryMetadata>().apply {
+ add(historyEntry)
+ add(historyEntry.copy(totalViewTime = 0))
+ }
+ whenever(storage.queryHistoryMetadata("moz", DEFAULT_METADATA_SUGGESTION_LIMIT)).thenReturn(historyEntries)
+ val provider = HistoryMetadataSuggestionProvider(storage, mock())
+
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ }
+
+ @Test
+ fun `provider calls speculative connect for URL of highest scored suggestion`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ val engine: Engine = mock()
+ val provider = HistoryMetadataSuggestionProvider(storage, mock(), engine = engine)
+
+ var suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ verify(engine, never()).speculativeConnect(anyString())
+
+ whenever(storage.queryHistoryMetadata("moz", DEFAULT_METADATA_SUGGESTION_LIMIT)).thenReturn(listOf(historyEntry))
+
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ verify(engine, times(1)).speculativeConnect(historyEntry.key.url)
+ }
+
+ @Test
+ fun `fact is emitted when suggestion is clicked`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ val engine: Engine = mock()
+ val provider = HistoryMetadataSuggestionProvider(storage, mock(), engine = engine)
+
+ var suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ verify(engine, never()).speculativeConnect(anyString())
+
+ whenever(storage.queryHistoryMetadata("moz", DEFAULT_METADATA_SUGGESTION_LIMIT)).thenReturn(listOf(historyEntry))
+
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+
+ val emittedFacts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ emittedFacts.add(fact)
+ }
+ },
+ )
+
+ suggestions[0].onSuggestionClicked?.invoke()
+ assertTrue(emittedFacts.isNotEmpty())
+ assertEquals(
+ Fact(
+ Component.FEATURE_AWESOMEBAR,
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.HISTORY_SUGGESTION_CLICKED,
+ ),
+ emittedFacts.first(),
+ )
+ }
+
+ @Test
+ fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+
+ whenever(storage.queryHistoryMetadata("moz", DEFAULT_METADATA_SUGGESTION_LIMIT)).thenReturn(listOf(historyEntry))
+
+ val provider = HistoryMetadataSuggestionProvider(storage, mock(), showEditSuggestion = false)
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ assertEquals(historyEntry.key.url, suggestions[0].description)
+ assertEquals(historyEntry.title, suggestions[0].title)
+ assertNull(suggestions[0].editSuggestion)
+ }
+
+ @Test
+ fun `GIVEN no external filter WHEN querying history THEN query a low number of results`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
+ val provider = HistoryMetadataSuggestionProvider(storage, mock())
+
+ provider.onInputChanged("moz")
+
+ verify(storage).queryHistoryMetadata("moz", DEFAULT_METADATA_SUGGESTION_LIMIT)
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN query more than the usual default results for the host url`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ doReturn(emptyList<HistoryMetadata>()).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
+ val expectedQueryCount = 2 * HISTORY_METADATA_RESULTS_TO_FILTER_SCALE_FACTOR
+
+ val provider = HistoryMetadataSuggestionProvider(
+ historyStorage = storage,
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 2,
+ resultsUriFilter = "test".toUri(),
+ )
+
+ provider.onInputChanged("moz")
+
+ verify(storage).queryHistoryMetadata("moz", expectedQueryCount)
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN return only the results that pass through the filter`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ val metadataKey2 = HistoryMetadataKey("https://mozilla.com/firefox", null, null)
+ val historyEntry2 = HistoryMetadata(
+ key = metadataKey2,
+ title = "mozilla",
+ createdAt = System.currentTimeMillis(),
+ updatedAt = System.currentTimeMillis(),
+ totalViewTime = 10,
+ documentType = DocumentType.Regular,
+ previewImageUrl = null,
+ )
+ doReturn(listOf(historyEntry, historyEntry2)).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
+
+ val provider = HistoryMetadataSuggestionProvider(
+ historyStorage = storage,
+ loadUrlUseCase = mock(),
+ resultsUriFilter = "https://mozilla.com".toUri(),
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(2, suggestions.size)
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("http://www.mozilla.com"))
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN return results containing mobile subdomains`() = runTest {
+ val storage: HistoryMetadataStorage = mock()
+ val metadataKey1 = HistoryMetadataKey("https://m.mozilla.com/firefox", null, null)
+ val historyEntry1 = HistoryMetadata(
+ key = metadataKey1,
+ title = "mozilla",
+ createdAt = System.currentTimeMillis(),
+ updatedAt = System.currentTimeMillis(),
+ totalViewTime = 10,
+ documentType = DocumentType.Regular,
+ previewImageUrl = null,
+ )
+
+ val metadataKey2 = HistoryMetadataKey("http://www.mobile.mozilla.com/firefox", null, null)
+ val historyEntry2 = HistoryMetadata(
+ key = metadataKey2,
+ title = "mozilla",
+ createdAt = System.currentTimeMillis(),
+ updatedAt = System.currentTimeMillis(),
+ totalViewTime = 10,
+ documentType = DocumentType.Regular,
+ previewImageUrl = null,
+ )
+ doReturn(listOf(historyEntry1, historyEntry2)).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
+
+ val provider = HistoryMetadataSuggestionProvider(
+ historyStorage = storage,
+ loadUrlUseCase = mock(),
+ resultsUriFilter = "https://mozilla.com".toUri(),
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(2, suggestions.size)
+ assertTrue(suggestions.map { it.description }.contains("http://www.mobile.mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://m.mozilla.com/firefox"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProviderTest.kt
new file mode 100644
index 0000000000..b4d8eb5afd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProviderTest.kt
@@ -0,0 +1,396 @@
+/* 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.awesomebar.provider
+
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.concept.storage.SearchResult
+import mozilla.components.feature.awesomebar.facts.AwesomeBarFacts
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.ktx.android.net.sameHostWithoutMobileSubdomainAs
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+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.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class HistoryStorageSuggestionProviderTest {
+
+ @Before
+ fun setup() {
+ Facts.clearProcessors()
+ }
+
+ @Test
+ fun `Provider returns empty list when text is empty`() = runTest {
+ val provider = HistoryStorageSuggestionProvider(mock(), mock())
+
+ val suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `provider cleanups all previous read operations when text is empty`() = runTest {
+ val provider = HistoryStorageSuggestionProvider(mock(), mock())
+
+ provider.onInputChanged("")
+
+ verify(provider.historyStorage, never()).cancelReads()
+ verify(provider.historyStorage).cancelReads("")
+ }
+
+ @Test
+ fun `provider cleanups all previous read operations when text is not empty`() = runTest {
+ val history: HistoryStorage = mock()
+ doReturn(listOf(SearchResult("id", "http://www.mozilla.com/", 10)))
+ .`when`(history).getSuggestions(anyString(), anyInt())
+ val provider = HistoryStorageSuggestionProvider(history, mock())
+ val orderVerifier = inOrder(history)
+
+ provider.onInputChanged("moz")
+
+ orderVerifier.verify(provider.historyStorage, never()).cancelReads()
+ orderVerifier.verify(provider.historyStorage).cancelReads("moz")
+ orderVerifier.verify(provider.historyStorage).getSuggestions(eq("moz"), anyInt())
+ }
+
+ @Test
+ fun `Provider returns suggestions from configured history storage`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(listOf(SearchResult("id", "http://www.mozilla.com/", 10))).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+ val provider = HistoryStorageSuggestionProvider(history, mock())
+
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ assertEquals("http://www.mozilla.com/", suggestions[0].description)
+ }
+
+ @Test
+ fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(listOf(SearchResult("id", "http://www.mozilla.com/", 10))).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+ val provider = HistoryStorageSuggestionProvider(history, mock(), showEditSuggestion = false)
+
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ assertEquals("http://www.mozilla.com/", suggestions[0].description)
+ assertNull(suggestions[0].editSuggestion)
+ }
+
+ @Test
+ fun `Provider limits number of returned suggestions to a max of 20 by default`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(
+ (1..100).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ },
+ ).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(history, mock())
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(20, suggestions.size)
+ }
+
+ @Test
+ fun `Provider allows lowering the number of returned suggestions beneath the default`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ },
+ ).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history,
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 2,
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(2, suggestions.size)
+ }
+
+ @Test
+ fun `Provider allows increasing the number of returned suggestions above the default`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ },
+ ).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history,
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 22,
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+ assertEquals(22, suggestions.size)
+ }
+
+ @Test
+ fun `WHEN provider max number of suggestions is changed THEN the number of return suggestions is updated`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ },
+ ).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history,
+ loadUrlUseCase = mock(),
+ )
+
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(20, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(2)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(2, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(22)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(22, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(45)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(0)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+ }
+
+ @Test
+ fun `WHEN reset provider max number of suggestions THEN the number of return suggestions is reset to default`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ },
+ ).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history,
+ loadUrlUseCase = mock(),
+ )
+
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(20, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(45)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+
+ provider.resetToDefaultMaxSuggestions()
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(20, suggestions.size)
+ }
+
+ @Test
+ fun `Provider dedupes suggestions`() = runTest {
+ val storage: HistoryStorage = mock()
+
+ val provider = HistoryStorageSuggestionProvider(storage, mock())
+
+ val mozSuggestions = listOf(
+ SearchResult(id = "http://www.mozilla.com/", url = "http://www.mozilla.com/", score = 1),
+ SearchResult(id = "http://www.mozilla.com/", url = "http://www.mozilla.com/", score = 2),
+ SearchResult(id = "http://www.mozilla.com/", url = "http://www.mozilla.com/", score = 3),
+ )
+
+ val pocketSuggestions = listOf(
+ SearchResult(id = "http://www.getpocket.com/", url = "http://www.getpocket.com/", score = 5),
+ )
+
+ val exampleSuggestions = listOf(
+ SearchResult(id = "http://www.example.com", url = "http://www.example.com/", score = 2),
+ )
+
+ `when`(storage.getSuggestions(eq("moz"), eq(DEFAULT_HISTORY_SUGGESTION_LIMIT))).thenReturn(mozSuggestions)
+ `when`(storage.getSuggestions(eq("pocket"), eq(DEFAULT_HISTORY_SUGGESTION_LIMIT))).thenReturn(pocketSuggestions)
+ `when`(storage.getSuggestions(eq("www"), eq(DEFAULT_HISTORY_SUGGESTION_LIMIT))).thenReturn(pocketSuggestions + mozSuggestions + exampleSuggestions)
+
+ var results = provider.onInputChanged("moz")
+ assertEquals(1, results.size)
+ assertEquals(3, results[0].score)
+
+ results = provider.onInputChanged("pocket")
+ assertEquals(1, results.size)
+ assertEquals(5, results[0].score)
+
+ results = provider.onInputChanged("www")
+ assertEquals(3, results.size)
+ assertEquals(5, results[0].score)
+ assertEquals(3, results[1].score)
+ assertEquals(2, results[2].score)
+ }
+
+ @Test
+ fun `provider calls speculative connect for URL of highest scored suggestion`() = runTest {
+ val history: HistoryStorage = mock()
+ val engine: Engine = mock()
+ val provider = HistoryStorageSuggestionProvider(history, mock(), engine = engine)
+
+ var suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ verify(engine, never()).speculativeConnect(anyString())
+
+ Mockito.doReturn(listOf(SearchResult("id", "http://www.mozilla.com/", 10))).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+ assertEquals("http://www.mozilla.com/", suggestions[0].description)
+ verify(engine, times(1)).speculativeConnect(suggestions[0].description!!)
+ }
+
+ @Test
+ fun `fact is emitted when suggestion is clicked`() = runTest {
+ val history: HistoryStorage = mock()
+ val engine: Engine = mock()
+ val provider = HistoryStorageSuggestionProvider(history, mock(), engine = engine)
+
+ var suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ verify(engine, never()).speculativeConnect(anyString())
+
+ Mockito.doReturn(listOf(SearchResult("id", "http://www.mozilla.com/", 10))).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(1, suggestions.size)
+
+ val emittedFacts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ emittedFacts.add(fact)
+ }
+ },
+ )
+
+ suggestions[0].onSuggestionClicked?.invoke()
+ assertTrue(emittedFacts.isNotEmpty())
+ assertEquals(
+ Fact(
+ Component.FEATURE_AWESOMEBAR,
+ Action.INTERACTION,
+ AwesomeBarFacts.Items.HISTORY_SUGGESTION_CLICKED,
+ ),
+ emittedFacts.first(),
+ )
+ }
+
+ @Test
+ fun `GIVEN no external filter WHEN querying history THEN query the provided number of max results`() = runTest {
+ val history: HistoryStorage = mock()
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history,
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 13,
+ )
+
+ provider.onInputChanged("moz")
+
+ verify(history).getSuggestions("moz", 13)
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN query more than the usual default results for the host url`() = runTest {
+ val history: HistoryStorage = mock()
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history,
+ loadUrlUseCase = mock(),
+ maxNumberOfSuggestions = 13,
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("test".toUri())
+ },
+ )
+ val expectedQueryCount = 13 * HISTORY_RESULTS_TO_FILTER_SCALE_FACTOR
+
+ provider.onInputChanged("moz")
+
+ verify(history).getSuggestions("moz", expectedQueryCount)
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN return only the results that pass through the filter`() = runTest {
+ val history: HistoryStorage = mock()
+ doReturn(
+ listOf(
+ SearchResult("3", "https://mozilla.com/firefox", 10),
+ SearchResult("5", "http://firefox.com/mozilla", 10),
+ SearchResult("2", "http://allizom.com/focus/", 10),
+ SearchResult("4", "https://mozilla.com/thunderbird", 10),
+ SearchResult("16", "http://www.mozilla.com/firefox", 22),
+ ),
+ ).`when`(history).getSuggestions(anyString(), anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history,
+ loadUrlUseCase = mock(),
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("https://mozilla.com".toUri())
+ },
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(3, suggestions.size)
+ assertTrue(suggestions.map { it.description }.contains("http://www.mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/thunderbird"))
+ }
+
+ @Test
+ fun `GIVEN a results host filter WHEN querying history THEN return mobile domain results`() = runTest {
+ val history: HistoryStorage = mock()
+ doReturn(
+ listOf(
+ SearchResult("1", "https://m.mozilla.com/firefox", 10),
+ SearchResult("2", "https://mozilla.com/thunderbird", 10),
+ SearchResult("3", "http://www.mobile.mozilla.com/firefox", 22),
+ ),
+ ).`when`(history).getSuggestions(anyString(), anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history,
+ loadUrlUseCase = mock(),
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("https://mozilla.com".toUri())
+ },
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(3, suggestions.size)
+ assertTrue(suggestions.map { it.description }.contains("https://m.mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("http://www.mobile.mozilla.com/firefox"))
+ assertTrue(suggestions.map { it.description }.contains("https://mozilla.com/thunderbird"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchActionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchActionProviderTest.kt
new file mode 100644
index 0000000000..b00cf33edb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchActionProviderTest.kt
@@ -0,0 +1,46 @@
+/* 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.awesomebar.provider
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@ExperimentalCoroutinesApi // for runTest
+class SearchActionProviderTest {
+ @Test
+ fun `provider returns no suggestion for empty text`() = runTest {
+ val provider = SearchActionProvider(mock(), mock())
+ val suggestions = provider.onInputChanged("")
+
+ assertEquals(0, suggestions.size)
+ }
+
+ @Test
+ fun `provider returns no suggestion for blank text`() = runTest {
+ val provider = SearchActionProvider(mock(), mock())
+ val suggestions = provider.onInputChanged(" ")
+
+ assertEquals(0, suggestions.size)
+ }
+
+ @Test
+ fun `provider returns suggestion matching input`() = runTest {
+ val provider = SearchActionProvider(
+ store = mock(),
+ searchEngine = mock(),
+ searchUseCase = mock(),
+ )
+ val suggestions = provider.onInputChanged("firefox")
+
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+
+ assertEquals("firefox", suggestion.title)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProviderTest.kt
new file mode 100644
index 0000000000..effb4a2505
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProviderTest.kt
@@ -0,0 +1,122 @@
+/* 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.awesomebar.provider
+
+import android.content.Context
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.feature.search.ext.createSearchEngine
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi // for runTest
+
+class SearchEngineSuggestionProviderTest {
+ private lateinit var defaultProvider: SearchEngineSuggestionProvider
+ private val engineList = listOf(
+ createSearchEngine("amazon", "https://www.amazon.org/?q={searchTerms}", mock()),
+ createSearchEngine("bing", "https://www.bing.com/?q={searchTerms}", mock()),
+ createSearchEngine("bingo", "https://www.bingo.com/?q={searchTerms}", mock()),
+ )
+ private val testContext: Context = mock()
+
+ @Before
+ fun setup() {
+ defaultProvider = SearchEngineSuggestionProvider(
+ testContext,
+ engineList,
+ mock(),
+ 1,
+ "description",
+ mock(),
+ maxSuggestions = 1,
+ charactersThreshold = 1,
+ )
+
+ whenever(testContext.getString(1, "amazon")).thenReturn("Search amazon")
+ whenever(testContext.getString(1, "bing")).thenReturn("Search bing")
+ whenever(testContext.getString(1, "bingo")).thenReturn("Search bingo")
+ }
+
+ @Test
+ fun `Provider returns empty list when text is empty`() = runTest {
+ val suggestions = defaultProvider.onInputChanged("")
+
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `Provider returns empty list when text is blank`() = runTest {
+ val suggestions = defaultProvider.onInputChanged(" ")
+
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `Provider returns empty list when text is shorter than charactersThreshold`() = runTest {
+ val provider = SearchEngineSuggestionProvider(
+ testContext,
+ engineList,
+ mock(),
+ 1,
+ "description",
+ mock(),
+ charactersThreshold = 3,
+ )
+
+ val suggestions = provider.onInputChanged("am")
+
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `Provider returns empty list when list does not contain engines with typed text`() = runTest {
+ val suggestions = defaultProvider.onInputChanged("x")
+
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `WHEN input matches the beginning of the engine name THEN return the corresponding engine`() = runTest {
+ val suggestions = defaultProvider.onInputChanged("am")
+
+ assertEquals("Search amazon", suggestions[0].title)
+ }
+
+ @Test
+ fun `WHEN input matches not the beginning of the engine name THEN return nothing`() = runTest {
+ val suggestions = defaultProvider.onInputChanged("ma")
+
+ assertEquals(0, suggestions.size)
+ }
+
+ @Test
+ fun `Provider returns empty list when the engine list is empty`() = runTest {
+ val providerEmpty = SearchEngineSuggestionProvider(
+ testContext,
+ emptyList(),
+ mock(),
+ 1,
+ "description",
+ mock(),
+ )
+
+ val suggestions = providerEmpty.onInputChanged("a")
+
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `Provider limits number of returned suggestions to maxSuggestions`() = runTest {
+ // this should match to both engines in list
+ val suggestions = defaultProvider.onInputChanged("bi")
+
+ assertEquals(defaultProvider.maxSuggestions, suggestions.size)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProviderTest.kt
new file mode 100644
index 0000000000..dbb0328da4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProviderTest.kt
@@ -0,0 +1,667 @@
+/* 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.awesomebar.provider
+
+import android.graphics.Bitmap
+import androidx.core.graphics.drawable.toBitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.feature.awesomebar.facts.AwesomeBarFacts
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.search.ext.createSearchEngine
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.io.IOException
+import mozilla.components.ui.icons.R as iconsR
+
+private const val GOOGLE_MOCK_RESPONSE = "[\"firefox\",[\"firefox\",\"firefox for mac\",\"firefox quantum\",\"firefox update\",\"firefox esr\",\"firefox focus\",\"firefox addons\",\"firefox extensions\",\"firefox nightly\",\"firefox clear cache\"]]"
+private const val GOOGLE_MOCK_RESPONSE_WITH_DUPLICATES = "[\"firefox\",[\"firefox\",\"firefox\",\"firefox for mac\",\"firefox quantum\",\"firefox update\",\"firefox esr\",\"firefox esr\",\"firefox focus\",\"firefox addons\",\"firefox extensions\",\"firefox nightly\",\"firefox clear cache\"]]"
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class SearchSuggestionProviderTest {
+ @Test
+ fun `Provider returns suggestion with chips based on search engine suggestion`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider =
+ SearchSuggestionProvider(searchEngine, useCase, HttpURLConnectionClient())
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+ assertEquals(11, suggestion.chips.size)
+
+ assertEquals("fire", suggestion.chips[0].title)
+ assertEquals("firefox", suggestion.chips[1].title)
+ assertEquals("firefox for mac", suggestion.chips[2].title)
+ assertEquals("firefox quantum", suggestion.chips[3].title)
+ assertEquals("firefox update", suggestion.chips[4].title)
+ assertEquals("firefox esr", suggestion.chips[5].title)
+ assertEquals("firefox focus", suggestion.chips[6].title)
+ assertEquals("firefox addons", suggestion.chips[7].title)
+ assertEquals("firefox extensions", suggestion.chips[8].title)
+ assertEquals("firefox nightly", suggestion.chips[9].title)
+ assertEquals("firefox clear cache", suggestion.chips[10].title)
+
+ verify(useCase, never()).invoke(anyString(), any(), any())
+
+ // Chips should be shown at the top of the awesomebar suggestions
+ assertNull(suggestions.firstOrNull { it.score != Int.MAX_VALUE })
+
+ CollectionProcessor.withFactCollection { facts ->
+ suggestion.onChipClicked!!.invoke(suggestion.chips[6])
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.SEARCH_SUGGESTION_CLICKED, item)
+ }
+ }
+
+ verify(useCase).invoke(eq("firefox focus"), any(), any())
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider returns multiple suggestions in MULTIPLE mode`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ useCase,
+ HttpURLConnectionClient(),
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+
+ println(suggestions)
+
+ assertEquals(11, suggestions.size)
+
+ assertEquals("fire", suggestions[0].title)
+ assertEquals("firefox", suggestions[1].title)
+ assertEquals("firefox for mac", suggestions[2].title)
+ assertEquals("firefox quantum", suggestions[3].title)
+ assertEquals("firefox update", suggestions[4].title)
+ assertEquals("firefox esr", suggestions[5].title)
+ assertEquals("firefox focus", suggestions[6].title)
+ assertEquals("firefox addons", suggestions[7].title)
+ assertEquals("firefox extensions", suggestions[8].title)
+ assertEquals("firefox nightly", suggestions[9].title)
+ assertEquals("firefox clear cache", suggestions[10].title)
+
+ verify(useCase, never()).invoke(anyString(), any(), any())
+
+ // Search suggestions should leave room for other providers' suggestions above
+ assertNull(
+ suggestions.firstOrNull {
+ it.score > Int.MAX_VALUE - (SEARCH_TERMS_MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT + 2)
+ },
+ )
+
+ CollectionProcessor.withFactCollection { facts ->
+ suggestions[6].onSuggestionClicked!!.invoke()
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.SEARCH_SUGGESTION_CLICKED, item)
+ }
+ }
+
+ verify(useCase).invoke(eq("firefox focus"), any(), any())
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider returns multiple suggestions with limit`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ useCase,
+ HttpURLConnectionClient(),
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ limit = 5,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+
+ println(suggestions)
+
+ assertEquals(5, suggestions.size)
+
+ assertEquals("fire", suggestions[0].title)
+ assertEquals("firefox", suggestions[1].title)
+ assertEquals("firefox for mac", suggestions[2].title)
+ assertEquals("firefox quantum", suggestions[3].title)
+ assertEquals("firefox update", suggestions[4].title)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider returns chips with limit`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider =
+ SearchSuggestionProvider(searchEngine, useCase, HttpURLConnectionClient(), limit = 5)
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+ assertEquals(5, suggestion.chips.size)
+
+ assertEquals("fire", suggestion.chips[0].title)
+ assertEquals("firefox", suggestion.chips[1].title)
+ assertEquals("firefox for mac", suggestion.chips[2].title)
+ assertEquals("firefox quantum", suggestion.chips[3].title)
+ assertEquals("firefox update", suggestion.chips[4].title)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ private fun getDeviceDesktopIcon(): Bitmap {
+ val drawable = iconsR.drawable.mozac_ic_device_desktop_24
+ return testContext.getDrawable(drawable)!!.toBitmap()
+ }
+
+ private fun getSearchIcon(): Bitmap {
+ val drawable = iconsR.drawable.mozac_ic_search_24
+ return testContext.getDrawable(drawable)!!.toBitmap()
+ }
+
+ @Test
+ fun `Provider should use engine icon by default`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val engineIcon = getDeviceDesktopIcon()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = engineIcon,
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val provider = SearchSuggestionProvider(searchEngine, mock(), HttpURLConnectionClient())
+ try {
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(1, suggestions.size)
+ assertTrue(suggestions[0].icon?.sameAs(engineIcon)!!)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider should use icon parameter when available`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val engineIcon = getDeviceDesktopIcon()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = engineIcon,
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val paramIcon = getSearchIcon()
+
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ mock(),
+ HttpURLConnectionClient(),
+ icon = paramIcon,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(1, suggestions.size)
+ assertTrue(suggestions[0].icon?.sameAs(paramIcon)!!)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider returns empty list if text is empty`() = runTest {
+ val provider = SearchSuggestionProvider(mock(), mock(), mock())
+
+ val suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `Provider should return default suggestion for search engine that cannot provide suggestion`() =
+ runTest {
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = "https://localhost/?q={searchTerms}",
+ icon = mock(),
+ )
+ val provider = SearchSuggestionProvider(searchEngine, mock(), mock())
+
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+ assertEquals(1, suggestion.chips.size)
+
+ assertEquals("fire", suggestion.chips[0].title)
+ }
+
+ @Test
+ fun `Provider doesn't fail if fetch returns HTTP error`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setResponseCode(404).setBody("error"))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider =
+ SearchSuggestionProvider(searchEngine, useCase, HttpURLConnectionClient())
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+ assertEquals(1, suggestion.chips.size)
+ assertEquals("fire", suggestion.chips[0].title)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider doesn't fail if fetch throws exception`() {
+ runTest {
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = "https://localhost/?q={searchTerms}",
+ icon = mock(),
+ suggestUrl = "https://localhost/suggestions",
+ )
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val client = object : Client() {
+ override fun fetch(request: Request): Response {
+ throw IOException()
+ }
+ }
+
+ val provider =
+ SearchSuggestionProvider(searchEngine, useCase, client)
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+ assertEquals(1, suggestion.chips.size)
+ assertEquals("fire", suggestion.chips[0].title)
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `Constructor throws if limit is less than 1`() {
+ SearchSuggestionProvider(mock(), mock(), mock(), limit = 0)
+ }
+
+ @Test
+ fun `Provider returns distinct multiple suggestions`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE_WITH_DUPLICATES))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ useCase,
+ HttpURLConnectionClient(),
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(11, suggestions.size)
+ assertEquals("fire", suggestions[0].title)
+ assertEquals("firefox", suggestions[1].title)
+ assertEquals("firefox for mac", suggestions[2].title)
+ assertEquals("firefox quantum", suggestions[3].title)
+ assertEquals("firefox update", suggestions[4].title)
+ assertEquals("firefox esr", suggestions[5].title)
+ assertEquals("firefox focus", suggestions[6].title)
+ assertEquals("firefox addons", suggestions[7].title)
+ assertEquals("firefox extensions", suggestions[8].title)
+ assertEquals("firefox nightly", suggestions[9].title)
+ assertEquals("firefox clear cache", suggestions[10].title)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider returns multiple suggestions with limit and no description`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ useCase,
+ HttpURLConnectionClient(),
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ limit = 3,
+ showDescription = false,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+
+ println(suggestions)
+
+ assertEquals(3, suggestions.size)
+
+ assertEquals("fire", suggestions[0].title)
+ assertEquals("firefox", suggestions[1].title)
+ assertEquals("firefox for mac", suggestions[2].title)
+ assertNull(suggestions[0].description)
+ assertNull(suggestions[1].description)
+ assertNull(suggestions[2].description)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider calls speculativeConnect for URL of highest scored suggestion in MULTIPLE mode`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val engine: Engine = mock()
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ mock(),
+ HttpURLConnectionClient(),
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ engine = engine,
+ limit = 3,
+ showDescription = false,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(3, suggestions.size)
+ assertEquals("fire", suggestions[0].title)
+ verify(engine, times(1))
+ .speculativeConnect(server.url("/search?q=fire").toString())
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider calls speculativeConnect for URL of highest scored chip in SINGLE mode`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val engine: Engine = mock()
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ mock(),
+ HttpURLConnectionClient(),
+ engine = engine,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("fire")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+ assertEquals(11, suggestion.chips.size)
+ assertEquals("fire", suggestion.chips[0].title)
+ verify(engine, times(1))
+ .speculativeConnect(server.url("/search?q=fire").toString())
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider filters exact match from multiple suggestions`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE_WITH_DUPLICATES))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ useCase,
+ HttpURLConnectionClient(),
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ filterExactMatch = true,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("firefox")
+ assertEquals(10, suggestions.size)
+ assertEquals("firefox for mac", suggestions[1].title)
+ assertEquals("firefox quantum", suggestions[2].title)
+ assertEquals("firefox update", suggestions[3].title)
+ assertEquals("firefox esr", suggestions[4].title)
+ assertEquals("firefox focus", suggestions[5].title)
+ assertEquals("firefox addons", suggestions[6].title)
+ assertEquals("firefox extensions", suggestions[7].title)
+ assertEquals("firefox nightly", suggestions[8].title)
+ assertEquals("firefox clear cache", suggestions[9].title)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+
+ @Test
+ fun `Provider filters chips with exact match`() {
+ runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
+ server.start()
+
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = server.url("/search?q={searchTerms}").toString(),
+ icon = mock(),
+ suggestUrl = server.url("/").toString(),
+ )
+
+ val useCase: SearchUseCases.SearchUseCase = mock()
+
+ val provider = SearchSuggestionProvider(
+ searchEngine,
+ useCase,
+ HttpURLConnectionClient(),
+ filterExactMatch = true,
+ )
+
+ try {
+ val suggestions = provider.onInputChanged("firefox")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+ assertEquals(9, suggestion.chips.size)
+
+ assertEquals("firefox for mac", suggestion.chips[0].title)
+ assertEquals("firefox quantum", suggestion.chips[1].title)
+ assertEquals("firefox update", suggestion.chips[2].title)
+ assertEquals("firefox esr", suggestion.chips[3].title)
+ assertEquals("firefox focus", suggestion.chips[4].title)
+ assertEquals("firefox addons", suggestion.chips[5].title)
+ assertEquals("firefox extensions", suggestion.chips[6].title)
+ assertEquals("firefox nightly", suggestion.chips[7].title)
+ assertEquals("firefox clear cache", suggestion.chips[8].title)
+ } finally {
+ server.shutdown()
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchTermSuggestionsProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchTermSuggestionsProviderTest.kt
new file mode 100644
index 0000000000..bddc5602b0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchTermSuggestionsProviderTest.kt
@@ -0,0 +1,334 @@
+/* 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.awesomebar.provider
+
+import android.graphics.Bitmap
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.search.OS_SEARCH_ENGINE_TERMS_PARAM
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.storage.DocumentType.Media
+import mozilla.components.concept.storage.DocumentType.Regular
+import mozilla.components.concept.storage.HistoryMetadata
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.feature.awesomebar.facts.AwesomeBarFacts
+import mozilla.components.feature.search.SearchUseCases.SearchUseCase
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+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.anyString
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class SearchTermSuggestionsProviderTest {
+ private val historyEntry = HistoryMetadata(
+ key = HistoryMetadataKey("http://www.mozilla.com/search?q=firefox", "fire", null),
+ title = "Firefox",
+ createdAt = System.currentTimeMillis(),
+ updatedAt = System.currentTimeMillis(),
+ totalViewTime = 10,
+ documentType = Regular,
+ previewImageUrl = null,
+ )
+ private var searchEngine: SearchEngine = mock()
+ private val storage: PlacesHistoryStorage = mock()
+
+ @Before
+ fun setup() = runTest {
+ doReturn("http://www.mozilla.com/search?q=firefox".toUri()).`when`(searchEngine).resultsUrl
+ doReturn(listOf(historyEntry)).`when`(storage).getHistoryMetadataSince(Long.MIN_VALUE)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `GIVEN a too large number of suggestions WHEN constructing the provider THEN throw an exception`() {
+ SearchTermSuggestionsProvider(
+ historyStorage = mock(),
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ maxNumberOfSuggestions = SEARCH_TERMS_MAXIMUM_ALLOWED_SUGGESTIONS_LIMIT + 1,
+ )
+ }
+
+ @Test
+ fun `GIVEN an empty input WHEN querying suggestions THEN return an empty list`() = runTest {
+ val provider = SearchTermSuggestionsProvider(mock(), mock(), searchEngine)
+
+ val suggestions = provider.onInputChanged("")
+
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN an empty input WHEN querying suggestions THEN cleanup all read operations for the current query`() = runTest {
+ val provider = SearchTermSuggestionsProvider(storage, mock(), searchEngine)
+
+ provider.onInputChanged("")
+
+ verify(storage, never()).cancelReads()
+ verify(storage).cancelReads("")
+ }
+
+ @Test
+ fun `GIVEN a valid input WHEN querying suggestions THEN cleanup all read operations for the current query`() = runTest {
+ val provider = SearchTermSuggestionsProvider(storage, mock(), searchEngine)
+ val orderVerifier = inOrder(storage)
+
+ provider.onInputChanged("fir")
+
+ orderVerifier.verify(storage, never()).cancelReads()
+ orderVerifier.verify(storage).cancelReads("fir")
+ orderVerifier.verify(storage).getHistoryMetadataSince(Long.MIN_VALUE)
+ }
+
+ @Test
+ fun `GIVEN a valid input WHEN querying suggestions THEN return suggestions from configured history storage`() = runTest {
+ val searchEngineIcon: Bitmap = mock()
+ val provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ icon = searchEngineIcon,
+ showEditSuggestion = true,
+ )
+
+ val suggestions = provider.onInputChanged("fir")
+
+ assertEquals(1, suggestions.size)
+ assertEquals(provider, suggestions[0].provider)
+ assertEquals(historyEntry.key.searchTerm, suggestions[0].title)
+ assertNull(suggestions[0].description)
+ assertEquals(historyEntry.key.searchTerm, suggestions[0].editSuggestion)
+ assertEquals(searchEngineIcon, suggestions[0].icon)
+ assertNull(suggestions[0].indicatorIcon)
+ assertTrue(suggestions[0].chips.isEmpty())
+ assertTrue(suggestions[0].flags.isEmpty())
+ assertNotNull(suggestions[0].onSuggestionClicked)
+ assertNull(suggestions[0].onChipClicked)
+ assertEquals(Int.MAX_VALUE - 2, suggestions[0].score)
+ }
+
+ @Test
+ fun `GIVEN a valid input and no provided icon WHEN querying suggestions THEN return suggestions showing the search engine icon`() = runTest {
+ val searchEngineIcon: Bitmap = mock()
+
+ // Test with a provided icon.
+ var provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ icon = searchEngineIcon,
+ )
+ var suggestions = provider.onInputChanged("fir")
+ assertEquals(1, suggestions.size)
+ assertEquals(searchEngineIcon, suggestions[0].icon)
+
+ // Test with no provided icon.
+ provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ icon = null,
+ )
+ suggestions = provider.onInputChanged("fir")
+ assertEquals(1, suggestions.size)
+ assertEquals(searchEngine.icon, suggestions[0].icon)
+ }
+
+ @Test
+ fun `GIVEN a valid input and editing suggestions disabled WHEN querying suggestions THEN return suggestions with no string for editing the suggestion`() = runTest {
+ var provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ showEditSuggestion = true,
+ )
+ var suggestions = provider.onInputChanged("fir")
+ assertEquals(1, suggestions.size)
+ assertEquals(historyEntry.key.searchTerm, suggestions[0].editSuggestion)
+
+ provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ showEditSuggestion = false,
+ )
+ suggestions = provider.onInputChanged("fir")
+ assertEquals(1, suggestions.size)
+ assertEquals(null, suggestions[0].editSuggestion)
+ }
+
+ @Test
+ fun `GIVEN a valid input WHEN querying suggestions THEN return suggestions configured to do a new search when clicked`() = runTest {
+ val searchUseCase: SearchUseCase = mock()
+ doReturn(listOf(historyEntry)).`when`(storage).getHistoryMetadataSince(Long.MIN_VALUE)
+ val provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = searchUseCase,
+ searchEngine = searchEngine,
+ )
+
+ val suggestions = provider.onInputChanged("fir")
+ assertEquals(1, suggestions.size)
+ suggestions[0].onSuggestionClicked?.invoke()
+
+ verify(searchUseCase).invoke(historyEntry.key.searchTerm!!, null, null)
+ }
+
+ @Test
+ fun `GIVEN a valid input WHEN querying suggestions THEN return suggestions configured to emit a telemetry fact when clicked`() = runTest {
+ val searchUseCase: SearchUseCase = mock()
+ doReturn(listOf(historyEntry)).`when`(storage).getHistoryMetadataSince(Long.MIN_VALUE)
+ val provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = searchUseCase,
+ searchEngine = searchEngine,
+ )
+
+ val suggestions = provider.onInputChanged("fir")
+ assertEquals(1, suggestions.size)
+ CollectionProcessor.withFactCollection { facts ->
+ suggestions[0].onSuggestionClicked?.invoke()
+
+ assertEquals(1, facts.size)
+ with(facts[0]) {
+ assertEquals(Component.FEATURE_AWESOMEBAR, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AwesomeBarFacts.Items.SEARCH_TERM_SUGGESTION_CLICKED, item)
+ }
+ }
+ }
+
+ @Test
+ fun `GIVEN a valid input WHEN querying suggestions THEN return by default 2 suggestions`() = runTest {
+ val historyEntries = listOf(
+ historyEntry.copy(
+ key = historyEntry.key.copy(searchTerm = "fir"),
+ createdAt = 1,
+ ),
+ historyEntry.copy(
+ key = historyEntry.key.copy(searchTerm = "fire"),
+ createdAt = 2,
+ ),
+ historyEntry.copy(
+ key = historyEntry.key.copy(searchTerm = "firefox"),
+ createdAt = 3,
+ ),
+ )
+ doReturn(historyEntries).`when`(storage).getHistoryMetadataSince(Long.MIN_VALUE)
+ val provider = SearchTermSuggestionsProvider(storage, mock(), searchEngine)
+
+ val suggestions = provider.onInputChanged("fir")
+
+ assertEquals(2, suggestions.size)
+ assertEquals(historyEntries[2].key.searchTerm, suggestions[0].title)
+ assertEquals(Int.MAX_VALUE - 2, suggestions[0].score)
+ assertEquals(historyEntries[1].key.searchTerm, suggestions[1].title)
+ assertEquals(Int.MAX_VALUE - 3, suggestions[1].score)
+ }
+
+ @Test
+ fun `GIVEN a valid input and a different number of suggestions required WHEN querying suggestions THEN return the require number of suggestions if available`() = runTest {
+ val historyEntries = listOf(
+ historyEntry.copy(
+ key = historyEntry.key.copy(searchTerm = "fir"),
+ createdAt = 1,
+ ),
+ historyEntry.copy(
+ key = historyEntry.key.copy(searchTerm = "fire"),
+ createdAt = 2,
+ ),
+ historyEntry.copy(
+ key = historyEntry.key.copy(searchTerm = "firef"),
+ createdAt = 3,
+ ),
+ )
+ doReturn(historyEntries).`when`(storage).getHistoryMetadataSince(Long.MIN_VALUE)
+
+ // Test with asking for more suggestions.
+ var provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ maxNumberOfSuggestions = 3,
+ )
+ var suggestions = provider.onInputChanged("fir")
+ assertEquals(3, suggestions.size)
+ assertEquals(historyEntries[2].key.searchTerm, suggestions[0].title)
+ assertEquals(historyEntries[1].key.searchTerm, suggestions[1].title)
+ assertEquals(historyEntries[0].key.searchTerm, suggestions[2].title)
+
+ // Test with asking for fewer suggestions.
+ provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ maxNumberOfSuggestions = 1,
+ )
+ suggestions = provider.onInputChanged("fir")
+ assertEquals(1, suggestions.size)
+ assertEquals(historyEntries[2].key.searchTerm, suggestions[0].title)
+ }
+
+ @Test
+ fun `GIVEN a valid input WHEN querying suggestions THEN return suggestions with different search terms`() = runTest {
+ val historyEntries = listOf(
+ historyEntry,
+ historyEntry.copy(
+ createdAt = 1,
+ documentType = Media,
+ ),
+ )
+ doReturn(historyEntries).`when`(storage).getHistoryMetadataSince(Long.MIN_VALUE)
+ val provider = SearchTermSuggestionsProvider(storage, mock(), searchEngine)
+
+ val suggestions = provider.onInputChanged("fir")
+
+ assertEquals(1, suggestions.size)
+ assertEquals(historyEntries[0].key.searchTerm, suggestions[0].title)
+ }
+
+ @Test
+ fun `GIVEN a new user input WHEN building search term suggestions THEN do a speculative connect to the url for the first search term`() = runTest {
+ val engine: Engine = mock()
+ val searcheEngineSearchUrl = "https://test/q=$OS_SEARCH_ENGINE_TERMS_PARAM"
+ doReturn(listOf(searcheEngineSearchUrl)).`when`(searchEngine).resultUrls
+ val provider = SearchTermSuggestionsProvider(
+ historyStorage = storage,
+ searchUseCase = mock(),
+ searchEngine = searchEngine,
+ engine = engine,
+ )
+
+ var suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ verify(engine, never()).speculativeConnect(anyString())
+
+ doReturn(listOf(historyEntry)).`when`(storage).getHistoryMetadataSince(Long.MIN_VALUE)
+ suggestions = provider.onInputChanged("fir")
+ assertEquals(1, suggestions.size)
+ assertEquals("fire", suggestions[0].title)
+ assertNull(suggestions[0].description)
+ verify(engine, times(1)).speculativeConnect(
+ searcheEngineSearchUrl.replace(OS_SEARCH_ENGINE_TERMS_PARAM, suggestions[0].title!!),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionAutocompleteProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionAutocompleteProviderTest.kt
new file mode 100644
index 0000000000..6569710edd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionAutocompleteProviderTest.kt
@@ -0,0 +1,82 @@
+/* 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.awesomebar.provider
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class SessionAutocompleteProviderTest {
+ @Test
+ fun `GIVEN open tabs exist WHEN asked for autocomplete suggestions THEN return the first matching tab`() = runTest {
+ val tab1 = createTab("https://allizom.org")
+ val tab2 = createTab("https://getpocket.com")
+ val tab3 = createTab("https://www.firefox.com")
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2, tab3),
+ ),
+ )
+ val provider = SessionAutocompleteProvider(store)
+
+ var suggestion = provider.getAutocompleteSuggestion("mozilla")
+ assertNull(suggestion)
+
+ suggestion = provider.getAutocompleteSuggestion("all")
+ assertNotNull(suggestion)
+ assertEquals("all", suggestion?.input)
+ assertEquals("allizom.org", suggestion?.text)
+ assertEquals("https://allizom.org", suggestion?.url)
+ assertEquals(LOCAL_TABS_AUTOCOMPLETE_SOURCE_NAME, suggestion?.source)
+ assertEquals(1, suggestion?.totalItems)
+
+ suggestion = provider.getAutocompleteSuggestion("www")
+ assertNotNull(suggestion)
+ assertEquals("www", suggestion?.input)
+ assertEquals("www.firefox.com", suggestion?.text)
+ assertEquals("https://www.firefox.com", suggestion?.url)
+ assertEquals(LOCAL_TABS_AUTOCOMPLETE_SOURCE_NAME, suggestion?.source)
+ assertEquals(1, suggestion?.totalItems)
+ }
+
+ @Test
+ fun `GIVEN open tabs exist WHEN asked for autocomplete suggestions and only private tabs match THEN return null`() = runTest {
+ val tab1 = createTab(url = "https://allizom.org", private = true)
+ val tab2 = createTab(url = "https://getpocket.com")
+ val tab3 = createTab(url = "https://www.firefox.com", private = true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2, tab3),
+ ),
+ )
+ val provider = SessionAutocompleteProvider(store)
+
+ var suggestion = provider.getAutocompleteSuggestion("mozilla")
+ assertNull(suggestion)
+
+ suggestion = provider.getAutocompleteSuggestion("all")
+ assertNull(suggestion)
+
+ suggestion = provider.getAutocompleteSuggestion("www")
+ assertNull(suggestion)
+ }
+
+ @Test
+ fun `GIVEN no open tabs exist WHEN asked for autocomplete suggestions THEN return null`() = runTest {
+ val provider = SessionAutocompleteProvider(BrowserStore())
+
+ assertNull(provider.getAutocompleteSuggestion("test"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProviderTest.kt
new file mode 100644
index 0000000000..f412dc74a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProviderTest.kt
@@ -0,0 +1,449 @@
+/* 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.awesomebar.provider
+
+import android.content.res.Resources
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.ktx.android.net.sameHostWithoutMobileSubdomainAs
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class SessionSuggestionProviderTest {
+ @Test
+ fun `Provider returns empty list when text is empty`() = runTest {
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val provider = SessionSuggestionProvider(resources, mock(), mock())
+
+ val suggestions = provider.onInputChanged("")
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun `Provider returns Sessions with matching URLs`() = runTest {
+ val store = BrowserStore()
+
+ val tab1 = createTab("https://www.mozilla.org")
+ val tab2 = createTab("https://example.com")
+ val tab3 = createTab("https://firefox.com")
+ val tab4 = createTab("https://example.org/")
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val provider = SessionSuggestionProvider(resources, store, mock())
+
+ run {
+ val suggestions = provider.onInputChanged("Example")
+ assertTrue(suggestions.isEmpty())
+ }
+
+ store.dispatch(TabListAction.AddTabAction(tab1)).join()
+ store.dispatch(TabListAction.AddTabAction(tab2)).join()
+ store.dispatch(TabListAction.AddTabAction(tab3)).join()
+
+ run {
+ val suggestions = provider.onInputChanged("Example")
+ assertEquals(1, suggestions.size)
+
+ assertEquals(tab2.id, suggestions[0].id)
+ assertEquals("Switch to tab", suggestions[0].description)
+ }
+
+ store.dispatch(TabListAction.AddTabAction(tab4)).join()
+
+ run {
+ val suggestions = provider.onInputChanged("Example")
+ assertEquals(2, suggestions.size)
+
+ assertEquals(tab2.id, suggestions[0].id)
+ assertEquals(tab4.id, suggestions[1].id)
+ assertEquals("Switch to tab", suggestions[0].description)
+ assertEquals("Switch to tab", suggestions[1].description)
+ }
+ }
+
+ @Test
+ fun `WHEN input text has trailing space THEN Provider returns Sessions with matching URLs`() =
+ runTest {
+ val store = BrowserStore()
+
+ val tab1 = createTab("https://www.mozilla.org")
+ val tab2 = createTab("https://www.mozilla.org/example/of/content")
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val provider = SessionSuggestionProvider(resources, store, mock())
+ store.dispatch(TabListAction.AddTabAction(tab1)).join()
+ store.dispatch(TabListAction.AddTabAction(tab2)).join()
+
+ run {
+ val suggestions = provider.onInputChanged("mozilla ")
+ assertEquals(2, suggestions.size)
+
+ assertEquals(tab1.id, suggestions[0].id)
+ assertEquals("Switch to tab", suggestions[0].description)
+
+ assertEquals(tab2.id, suggestions[1].id)
+ assertEquals("Switch to tab", suggestions[1].description)
+ }
+ }
+
+ @Test
+ fun `WHEN input text has leading space THEN Provider returns Sessions with matching URLs`() =
+ runTest {
+ val store = BrowserStore()
+
+ val tab1 = createTab("https://www.mozilla.org")
+ val tab2 = createTab("https://www.mozilla.org/example/of/content")
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val provider = SessionSuggestionProvider(resources, store, mock())
+ store.dispatch(TabListAction.AddTabAction(tab1)).join()
+ store.dispatch(TabListAction.AddTabAction(tab2)).join()
+
+ run {
+ val suggestions = provider.onInputChanged(" mozilla")
+ assertEquals(2, suggestions.size)
+
+ assertEquals(tab1.id, suggestions[0].id)
+ assertEquals("Switch to tab", suggestions[0].description)
+
+ assertEquals(tab2.id, suggestions[1].id)
+ assertEquals("Switch to tab", suggestions[1].description)
+ }
+ }
+
+ @Test
+ fun `GIVEN input text has multiple matching words WHEN all match THEN Provider returns Sessions with matching URLs`() =
+ runTest {
+ val store = BrowserStore()
+
+ val tab1 = createTab("https://www.mozilla.org/example/of/content")
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val provider = SessionSuggestionProvider(resources, store, mock())
+ store.dispatch(TabListAction.AddTabAction(tab1)).join()
+
+ run {
+ val suggestions = provider.onInputChanged("mozilla example content")
+ assertEquals(1, suggestions.size)
+
+ assertEquals(tab1.id, suggestions[0].id)
+ assertEquals("Switch to tab", suggestions[0].description)
+ }
+ }
+
+ @Test
+ fun `GIVEN input text has multiple matching words WHEN some match THEN Provider returns an empty list`() =
+ runTest {
+ val store = BrowserStore()
+
+ val tab1 = createTab("https://www.mozilla.org/example/of/content")
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val provider = SessionSuggestionProvider(resources, store, mock())
+ store.dispatch(TabListAction.AddTabAction(tab1)).join()
+
+ run {
+ val suggestions = provider.onInputChanged("mozilla example test")
+ assertTrue(suggestions.isEmpty())
+ }
+ }
+
+ @Test
+ fun `Provider returns Sessions with matching titles`() = runTest {
+ val tab1 = createTab("https://allizom.org", title = "Internet for people, not profit — Mozilla")
+ val tab2 = createTab("https://getpocket.com", title = "Pocket: My List")
+ val tab3 = createTab("https://firefox.com", title = "Download Firefox — Free Web Browser")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2, tab3),
+ ),
+ )
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val provider = SessionSuggestionProvider(resources, store, mock())
+
+ run {
+ val suggestions = provider.onInputChanged("Browser")
+ assertEquals(1, suggestions.size)
+
+ assertEquals(suggestions.first().id, tab3.id)
+ assertEquals("Switch to tab", suggestions.first().description)
+ assertEquals("Download Firefox — Free Web Browser", suggestions[0].title)
+ }
+
+ run {
+ val suggestions = provider.onInputChanged("Mozilla")
+ assertEquals(1, suggestions.size)
+
+ assertEquals(tab1.id, suggestions.first().id)
+ assertEquals("Switch to tab", suggestions.first().description)
+ assertEquals("Internet for people, not profit — Mozilla", suggestions[0].title)
+ }
+ }
+
+ @Test
+ fun `Provider only returns non-private Sessions`() = runTest {
+ val tab = createTab("https://www.mozilla.org")
+ val privateTab1 = createTab("https://mozilla.org/firefox", private = true)
+ val privateTab2 = createTab("https://mozilla.org/projects", private = true)
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab, privateTab1, privateTab2),
+ ),
+ )
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val useCase: TabsUseCases.SelectTabUseCase = mock()
+
+ val provider = SessionSuggestionProvider(resources, store, useCase)
+ val suggestions = provider.onInputChanged("mozilla")
+
+ assertEquals(1, suggestions.size)
+ }
+
+ @Test
+ fun `Clicking suggestion invokes SelectTabUseCase`() = runTest {
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val tab = createTab("https://www.mozilla.org")
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ val useCase: TabsUseCases.SelectTabUseCase = mock()
+
+ val provider = SessionSuggestionProvider(resources, store, useCase)
+ val suggestions = provider.onInputChanged("mozilla")
+ assertEquals(1, suggestions.size)
+
+ val suggestion = suggestions[0]
+
+ verify(useCase, never()).invoke(tab.id)
+
+ suggestion.onSuggestionClicked!!.invoke()
+
+ verify(useCase).invoke(tab.id)
+ }
+
+ @Test
+ fun `When excludeSelectedSession is true provider should not include the selected session`() = runTest {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://wikipedia.org"),
+ createTab(id = "b", url = "https://www.mozilla.org"),
+ ),
+ selectedTabId = "b",
+ ),
+ )
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val useCase: TabsUseCases.SelectTabUseCase = mock()
+
+ val provider = SessionSuggestionProvider(resources, store, useCase, excludeSelectedSession = true)
+ val suggestions = provider.onInputChanged("org")
+
+ assertEquals(1, suggestions.size)
+ assertEquals("a", suggestions.first().id)
+ assertEquals("Switch to tab", suggestions.first().description)
+ }
+
+ @Test
+ fun `When excludeSelectedSession is false provider should include the selected session`() = runTest {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://wikipedia.org"),
+ createTab(id = "b", url = "https://www.mozilla.org"),
+ ),
+ selectedTabId = "b",
+ ),
+ )
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val useCase: TabsUseCases.SelectTabUseCase = mock()
+
+ val provider = SessionSuggestionProvider(resources, store, useCase, excludeSelectedSession = false)
+ val suggestions = provider.onInputChanged("mozilla")
+
+ assertEquals(1, suggestions.size)
+ assertEquals("b", suggestions.first().id)
+ assertEquals("Switch to tab", suggestions.first().description)
+ }
+
+ @Test
+ fun `Uses title for chip title when available, but falls back to URL`() = runTest {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://wikipedia.org", title = "Wikipedia"),
+ createTab(id = "b", url = "https://www.mozilla.org", title = ""),
+ ),
+ selectedTabId = "b",
+ ),
+ )
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val useCase: TabsUseCases.SelectTabUseCase = mock()
+
+ val provider = SessionSuggestionProvider(resources, store, useCase, excludeSelectedSession = false)
+ var suggestions = provider.onInputChanged("mozilla")
+
+ assertEquals(1, suggestions.size)
+ assertEquals("b", suggestions.first().id)
+ assertEquals("https://www.mozilla.org", suggestions.first().title)
+ assertEquals("Switch to tab", suggestions.first().description)
+
+ suggestions = provider.onInputChanged("wiki")
+ assertEquals(1, suggestions.size)
+ assertEquals("a", suggestions.first().id)
+ assertEquals("Wikipedia", suggestions.first().title)
+ assertEquals("Switch to tab", suggestions.first().description)
+ }
+
+ @Test
+ fun `GIVEN a results uri filter WHEN querying tabs THEN return only the results that pass through the filter`() = runTest {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://wikipedia.org"),
+ createTab(id = "b", url = "https://mozilla.org/firefox"),
+ createTab(id = "c", url = "https://mozilla.org/focus"),
+ createTab(id = "d", url = "https://www.mozilla.org/vpn"),
+ ),
+ selectedTabId = "d",
+ ),
+ )
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+ val provider = SessionSuggestionProvider(
+ resources = resources,
+ store = store,
+ selectTabUseCase = mock(),
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs("https://mozilla.org".toUri())
+ },
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(3, suggestions.size)
+ assertTrue(suggestions.map { it.title }.contains("https://mozilla.org/firefox"))
+ assertTrue(suggestions.map { it.title }.contains("https://mozilla.org/firefox"))
+ assertTrue(suggestions.map { it.title }.contains("https://www.mozilla.org/vpn"))
+ }
+
+ @Test
+ fun `GIVEN a results uri filter WHEN querying tabs THEN return results containing mobile subdomains`() = runTest {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "a", url = "https://wikipedia.org"),
+ createTab(id = "b", url = "https://mozilla.org/firefox"),
+ createTab(id = "c", url = "https://mozilla.org/focus"),
+ createTab(id = "d", url = "https://www.mozilla.org/vpn"),
+ createTab(id = "e", url = "https://www.m.mozilla.org"),
+ createTab(id = "f", url = "http://www.mobile.mozilla.org"),
+ ),
+ selectedTabId = "d",
+ ),
+ )
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+ val uriFilter = Uri.parse("https://mozilla.org")
+ val provider = SessionSuggestionProvider(
+ resources = resources,
+ store = store,
+ selectTabUseCase = mock(),
+ resultsUriFilter = {
+ it.sameHostWithoutMobileSubdomainAs(uriFilter)
+ },
+ )
+
+ val suggestions = provider.onInputChanged("moz")
+
+ assertEquals(5, suggestions.size)
+ assertTrue(suggestions.map { it.title }.contains("https://mozilla.org/firefox"))
+ assertTrue(suggestions.map { it.title }.contains("https://mozilla.org/focus"))
+ assertTrue(suggestions.map { it.title }.contains("https://www.m.mozilla.org"))
+ assertTrue(suggestions.map { it.title }.contains("http://www.mobile.mozilla.org"))
+ assertTrue(suggestions.map { it.title }.contains("https://www.mozilla.org/vpn"))
+ }
+
+ @Test
+ fun `GIVEN multiple tabs have the same url WHEN user inputs the same url THEN provider returns a single suggestion for the matching input`() = runTest {
+ val store = BrowserStore()
+
+ val url = "https://www.mozilla.org"
+ val tab1 = createTab(url)
+ val tab2 = createTab(url)
+ val tab3 = createTab(url)
+
+ val resources: Resources = mock()
+ `when`(resources.getString(anyInt())).thenReturn("Switch to tab")
+
+ val provider = SessionSuggestionProvider(resources, store, mock())
+
+ run {
+ val suggestions = provider.onInputChanged("Mozilla")
+ assertTrue(suggestions.isEmpty())
+ }
+
+ store.dispatch(TabListAction.AddTabAction(tab1)).join()
+ store.dispatch(TabListAction.AddTabAction(tab2)).join()
+ store.dispatch(TabListAction.AddTabAction(tab3)).join()
+
+ run {
+ val suggestions = provider.onInputChanged("Mozilla")
+ assertEquals(1, suggestions.size)
+ assertEquals(tab1.id, suggestions[0].id)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/awesomebar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/awesomebar/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/awesomebar/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/awesomebar/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/awesomebar/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/awesomebar/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/containers/README.md b/mobile/android/android-components/components/feature/containers/README.md
new file mode 100644
index 0000000000..aea3596f2d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Containers
+
+Feature component for working with contextual identities also known as containers.
+
+## Usage
+
+### 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-containers:{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/containers/build.gradle b/mobile/android/android-components/components/feature/containers/build.gradle
new file mode 100644
index 0000000000..9ebb3289db
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/build.gradle
@@ -0,0 +1,80 @@
+/* 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/. */
+
+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"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ resources {
+ excludes += ['META-INF/proguard/androidx-annotations.pro']
+ }
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.containers'
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_paging
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_arch_core_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.testing_coroutines
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+
+ 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/containers/proguard-rules.pro b/mobile/android/android-components/components/feature/containers/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/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/containers/schemas/mozilla.components.feature.containers.db.ContainerDatabase/1.json b/mobile/android/android-components/components/feature/containers/schemas/mozilla.components.feature.containers.db.ContainerDatabase/1.json
new file mode 100644
index 0000000000..4a8eb5e60b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/schemas/mozilla.components.feature.containers.db.ContainerDatabase/1.json
@@ -0,0 +1,52 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "77d1905ab2c154b7ed655e58bd578a84",
+ "entities": [
+ {
+ "tableName": "containers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`context_id` TEXT NOT NULL, `name` TEXT NOT NULL, `color` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`context_id`))",
+ "fields": [
+ {
+ "fieldPath": "contextId",
+ "columnName": "context_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "color",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "context_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '77d1905ab2c154b7ed655e58bd578a84')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt b/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt
new file mode 100644
index 0000000000..199034309c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt
@@ -0,0 +1,107 @@
+/* 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.containers
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.Container
+import mozilla.components.browser.state.state.ContainerState.Color
+import mozilla.components.browser.state.state.ContainerState.Icon
+import mozilla.components.feature.containers.db.ContainerDatabase
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+@Suppress("LargeClass")
+class ContainerStorageTest {
+ private lateinit var context: Context
+ private lateinit var storage: ContainerStorage
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ val database = Room.inMemoryDatabaseBuilder(context, ContainerDatabase::class.java).build()
+
+ storage = ContainerStorage(context)
+ storage.database = lazy { database }
+ }
+
+ @Test
+ fun testAddingContainer() = runTest {
+ storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
+ storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
+
+ val containers = getAllContainers()
+
+ assertEquals(2, containers.size)
+
+ assertEquals("1", containers[0].contextId)
+ assertEquals("Personal", containers[0].name)
+ assertEquals(Color.RED, containers[0].color)
+ assertEquals(Icon.FINGERPRINT, containers[0].icon)
+ assertEquals("2", containers[1].contextId)
+ assertEquals("Shopping", containers[1].name)
+ assertEquals(Color.BLUE, containers[1].color)
+ assertEquals(Icon.CART, containers[1].icon)
+ }
+
+ @Test
+ fun testRemovingContainers() = runTest {
+ storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
+ storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
+
+ getAllContainers().let { containers ->
+ assertEquals(2, containers.size)
+
+ storage.removeContainer(containers[0])
+ }
+
+ getAllContainers().let { containers ->
+ assertEquals(1, containers.size)
+
+ assertEquals("2", containers[0].contextId)
+ assertEquals("Shopping", containers[0].name)
+ assertEquals(Color.BLUE, containers[0].color)
+ assertEquals(Icon.CART, containers[0].icon)
+ }
+ }
+
+ @Test
+ fun testGettingContainers() = runTest {
+ storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
+ storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
+
+ val containers = storage.getContainers().first()
+
+ assertNotNull(containers)
+ assertEquals(2, containers.size)
+
+ with(containers[0]) {
+ assertEquals("1", contextId)
+ assertEquals("Personal", name)
+ assertEquals(Color.RED, color)
+ assertEquals(Icon.FINGERPRINT, icon)
+ }
+
+ with(containers[1]) {
+ assertEquals("2", contextId)
+ assertEquals("Shopping", name)
+ assertEquals(Color.BLUE, color)
+ assertEquals(Icon.CART, icon)
+ }
+ }
+
+ private suspend fun getAllContainers(): List<Container> {
+ return storage.containerDao.getContainersList().map { containerEntity ->
+ containerEntity.toContainer()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt b/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt
new file mode 100644
index 0000000000..344bb5d956
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt
@@ -0,0 +1,92 @@
+/* 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.containers.db
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.ContainerState.Color
+import mozilla.components.browser.state.state.ContainerState.Icon
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.UUID
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+@ExperimentalCoroutinesApi
+class ContainerDaoTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: ContainerDatabase
+ private lateinit var containerDao: ContainerDao
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, ContainerDatabase::class.java).build()
+ containerDao = database.containerDao()
+ executor = Executors.newSingleThreadExecutor()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+
+ @Test
+ fun testAddingContainer() = runTest {
+ val container =
+ ContainerEntity(
+ contextId = UUID.randomUUID().toString(),
+ name = "Personal",
+ color = Color.RED,
+ icon = Icon.FINGERPRINT,
+ )
+ containerDao.insertContainer(container)
+
+ val pagedList = containerDao.getContainersList()
+
+ assertEquals(1, pagedList.size)
+ assertEquals(container, pagedList[0])
+ }
+
+ @Test
+ fun testRemovingContainer() = runTest {
+ val container1 =
+ ContainerEntity(
+ contextId = UUID.randomUUID().toString(),
+ name = "Personal",
+ color = Color.RED,
+ icon = Icon.FINGERPRINT,
+ )
+ val container2 =
+ ContainerEntity(
+ contextId = UUID.randomUUID().toString(),
+ name = "Shopping",
+ color = Color.BLUE,
+ icon = Icon.CART,
+ )
+
+ containerDao.insertContainer(container1)
+ containerDao.insertContainer(container2)
+ containerDao.deleteContainer(container1)
+
+ val pagedList = containerDao.getContainersList()
+
+ assertEquals(1, pagedList.size)
+ assertEquals(container2, pagedList[0])
+ }
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/containers/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/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/containers/src/main/java/mozilla/components/feature/containers/ContainerMiddleware.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerMiddleware.kt
new file mode 100644
index 0000000000..772e16ee52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerMiddleware.kt
@@ -0,0 +1,106 @@
+/* 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.containers
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContainerAction
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.Container
+import mozilla.components.browser.state.state.ContainerState
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+import java.util.UUID
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * [Middleware] implementation for handling [ContainerAction] and syncing the containers in
+ * [BrowserState.containers] with the [ContainerStorage].
+ */
+class ContainerMiddleware(
+ applicationContext: Context,
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+ private val containerStorage: Storage = ContainerStorage(applicationContext),
+) : Middleware<BrowserState, BrowserAction> {
+
+ private var scope = CoroutineScope(coroutineContext)
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is InitAction -> initializeContainers(context.store)
+ is ContainerAction.AddContainerAction -> addContainer(action)
+ is ContainerAction.RemoveContainerAction -> removeContainer(context.store, action)
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+ }
+
+ private fun initializeContainers(
+ store: Store<BrowserState, BrowserAction>,
+ ) = scope.launch {
+ containerStorage.getContainers().collect { containers ->
+ store.dispatch(ContainerAction.AddContainersAction(containers))
+ }
+ }
+
+ private fun addContainer(
+ action: ContainerAction.AddContainerAction,
+ ) = scope.launch {
+ containerStorage.addContainer(
+ contextId = action.container.contextId,
+ name = action.container.name,
+ color = action.container.color,
+ icon = action.container.icon,
+ )
+ }
+
+ private fun removeContainer(
+ store: Store<BrowserState, BrowserAction>,
+ action: ContainerAction.RemoveContainerAction,
+ ) = scope.launch {
+ store.state.containers[action.contextId]?.let {
+ containerStorage.removeContainer(it)
+ }
+ }
+
+ /**
+ * Interface for a storage to be passed to the middleware.
+ */
+ interface Storage {
+ /**
+ * Returns a [Flow] list of all the [Container] instances.
+ */
+ fun getContainers(): Flow<List<Container>>
+
+ /**
+ * Adds a new [Container].
+ */
+ suspend fun addContainer(
+ contextId: String = UUID.randomUUID().toString(),
+ name: String,
+ color: ContainerState.Color,
+ icon: ContainerState.Icon,
+ )
+
+ /**
+ * Removes the given [Container].
+ */
+ suspend fun removeContainer(container: Container)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerStorage.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerStorage.kt
new file mode 100644
index 0000000000..1219923b58
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerStorage.kt
@@ -0,0 +1,72 @@
+/* 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.containers
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.paging.DataSource
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.state.Container
+import mozilla.components.browser.state.state.ContainerState.Color
+import mozilla.components.browser.state.state.ContainerState.Icon
+import mozilla.components.feature.containers.db.ContainerDatabase
+import mozilla.components.feature.containers.db.ContainerEntity
+import mozilla.components.feature.containers.db.toContainerEntity
+
+/**
+ * A storage implementation for organizing containers (contextual identities).
+ */
+internal class ContainerStorage(context: Context) : ContainerMiddleware.Storage {
+
+ @VisibleForTesting
+ internal var database: Lazy<ContainerDatabase> =
+ lazy { ContainerDatabase.get(context) }
+ val containerDao by lazy { database.value.containerDao() }
+
+ /**
+ * Adds a new [Container].
+ */
+ override suspend fun addContainer(
+ contextId: String,
+ name: String,
+ color: Color,
+ icon: Icon,
+ ) {
+ containerDao.insertContainer(
+ ContainerEntity(
+ contextId = contextId,
+ name = name,
+ color = color,
+ icon = icon,
+ ),
+ )
+ }
+
+ /**
+ * Returns a [Flow] list of all the [Container] instances.
+ */
+ override fun getContainers(): Flow<List<Container>> {
+ return containerDao.getContainers().map { list ->
+ list.map { entity -> entity.toContainer() }
+ }
+ }
+
+ /**
+ * Returns all saved [Container] instances as a [DataSource.Factory].
+ */
+ fun getContainersPaged(): DataSource.Factory<Int, Container> = containerDao
+ .getContainersPaged()
+ .map { entity ->
+ entity.toContainer()
+ }
+
+ /**
+ * Removes the given [Container].
+ */
+ override suspend fun removeContainer(container: Container) {
+ containerDao.deleteContainer(container.toContainerEntity())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDao.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDao.kt
new file mode 100644
index 0000000000..40e9f5cd41
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDao.kt
@@ -0,0 +1,36 @@
+/* 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.containers.db
+
+import androidx.paging.DataSource
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Internal DAO for accessing [ContainerEntity] instances.
+ */
+@Dao
+internal interface ContainerDao {
+ @Insert
+ suspend fun insertContainer(container: ContainerEntity): Long
+
+ @Delete
+ suspend fun deleteContainer(identity: ContainerEntity)
+
+ @Transaction
+ @Query("SELECT * FROM containers")
+ fun getContainers(): Flow<List<ContainerEntity>>
+
+ @Query("SELECT * FROM containers")
+ suspend fun getContainersList(): List<ContainerEntity>
+
+ @Transaction
+ @Query("SELECT * FROM containers")
+ fun getContainersPaged(): DataSource.Factory<Int, ContainerEntity>
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDatabase.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDatabase.kt
new file mode 100644
index 0000000000..e6a7f55d87
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDatabase.kt
@@ -0,0 +1,63 @@
+/* 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.containers.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import mozilla.components.browser.state.state.ContainerState.Color
+import mozilla.components.browser.state.state.ContainerState.Icon
+
+/**
+ * Internal database for storing containers (contextual identities).
+ */
+@Database(entities = [ContainerEntity::class], version = 1)
+@TypeConverters(Converter::class)
+internal abstract class ContainerDatabase : RoomDatabase() {
+ abstract fun containerDao(): ContainerDao
+
+ companion object {
+ @Volatile
+ private var instance: ContainerDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): ContainerDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ ContainerDatabase::class.java,
+ "containers",
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
+
+internal class Converter {
+ @TypeConverter
+ fun toColorString(color: Color): String {
+ return color.color
+ }
+
+ @TypeConverter
+ fun toColor(color: String): Color? {
+ return Color.values().find { it.color == color }
+ }
+
+ @TypeConverter
+ fun toIconString(icon: Icon): String {
+ return icon.icon
+ }
+
+ @TypeConverter
+ fun toIcon(icon: String): Icon? {
+ return Icon.values().find { it.icon == icon }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerEntity.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerEntity.kt
new file mode 100644
index 0000000000..2532789d43
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerEntity.kt
@@ -0,0 +1,49 @@
+/* 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.containers.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.browser.state.state.Container
+import mozilla.components.browser.state.state.Container.Color
+import mozilla.components.browser.state.state.Container.Icon
+
+/**
+ * Internal entity representing a container (contextual identity).
+ */
+@Entity(tableName = "containers")
+internal data class ContainerEntity(
+ @PrimaryKey
+ @ColumnInfo(name = "context_id")
+ var contextId: String,
+
+ @ColumnInfo(name = "name")
+ var name: String,
+
+ @ColumnInfo(name = "color")
+ var color: Color,
+
+ @ColumnInfo(name = "icon")
+ var icon: Icon,
+) {
+ internal fun toContainer(): Container {
+ return Container(
+ contextId,
+ name,
+ color,
+ icon,
+ )
+ }
+}
+
+internal fun Container.toContainerEntity(): ContainerEntity {
+ return ContainerEntity(
+ contextId,
+ name,
+ color,
+ icon,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt b/mobile/android/android-components/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt
new file mode 100644
index 0000000000..2cef4eacbc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt
@@ -0,0 +1,115 @@
+/* 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.containers
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flow
+import mozilla.components.browser.state.action.ContainerAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContainerState
+import mozilla.components.browser.state.store.BrowserStore
+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.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class ContainerMiddlewareTest {
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
+ // Test container
+ private val container = ContainerState(
+ contextId = "contextId",
+ name = "Personal",
+ color = ContainerState.Color.GREEN,
+ icon = ContainerState.Icon.CART,
+ )
+
+ @Test
+ fun `container storage stores the provided container on add container action`() =
+ runTestOnMain {
+ val storage = mockStorage()
+ val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage)
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(middleware),
+ )
+
+ store.waitUntilIdle() // wait to consume InitAction
+ store.waitUntilIdle() // wait to consume AddContainersAction
+
+ store.dispatch(ContainerAction.AddContainerAction(container)).joinBlocking()
+
+ verify(storage).addContainer(
+ container.contextId,
+ container.name,
+ container.color,
+ container.icon,
+ )
+ }
+
+ @Test
+ fun `fetch the containers from the container storage and load into browser state on initialize container state action`() =
+ runTestOnMain {
+ val storage = mockStorage(listOf(container))
+ val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage)
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(middleware),
+ )
+
+ store.waitUntilIdle() // wait to consume InitAction
+ store.waitUntilIdle() // wait to consume AddContainersAction
+
+ verify(storage).getContainers()
+ assertEquals(container, store.state.containers["contextId"])
+ }
+
+ @Test
+ fun `container storage removes the provided container on remove container action`() =
+ runTestOnMain {
+ val storage = mockStorage()
+ val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage)
+ val store = BrowserStore(
+ initialState = BrowserState(
+ containers = mapOf(
+ container.contextId to container,
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.waitUntilIdle() // wait to consume InitAction
+ store.waitUntilIdle() // wait to consume AddContainersAction
+
+ store.dispatch(ContainerAction.RemoveContainerAction(container.contextId))
+ .joinBlocking()
+
+ verify(storage).removeContainer(container)
+ }
+
+ private fun mockStorage(
+ containers: List<ContainerState> = emptyList(),
+ ): ContainerStorage {
+ val storage: ContainerStorage = mock()
+ whenever(storage.getContainers()).thenReturn(
+ flow {
+ emit(containers)
+ },
+ )
+ return storage
+ }
+}
diff --git a/mobile/android/android-components/components/feature/containers/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/containers/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/containers/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/containers/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/containers/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/containers/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/contextmenu/README.md b/mobile/android/android-components/components/feature/contextmenu/README.md
new file mode 100644
index 0000000000..6c1983139c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/README.md
@@ -0,0 +1,92 @@
+# [Android Components](../../../README.md) > Feature > Context Menu
+
+A component for displaying context menus when *long-pressing* web content.
+
+## Usage
+
+### 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-contextmenu:{latest-version}"
+```
+
+### Integration
+
+`ContextMenuFeature` subscribes to the selected `Session` automatically and displays context menus when web content is `long-pressed`.
+
+Initializing the feature in a [Fragment](https://developer.android.com/reference/androidx/fragment/app/Fragment) (`onViewCreated`) or in an [Activity](https://developer.android.com/reference/android/app/Activity) (`onCreate`):
+
+```Kotlin
+contextMenuFeature = ContextMenuFeature(
+ fragmentManager,
+ sessionManager,
+
+ // Use default context menu items:
+ ContextMenuCandidate.defaultCandidates(context, tabsUseCases, snackbarParentView)
+)
+```
+
+### Forwarding lifecycle events
+
+Start/Stop events need to be forwarded to the feature:
+
+```Kotlin
+// From onStart():
+feature.start()
+
+// From onStop():
+feature.stop()
+```
+
+### Customizing context menu items
+
+When initializing the feature a list of `ContextMenuCandidate` objects need to be passed to the feature. Instead of using the default list (`ContextMenuCandidate.defaultCandidates()`) a customized list can be passed to the feature.
+
+For every observed `HitResult` (`Session.Observer.onLongPress()`) the feature will query all candidates (`ContextMenuCandidate.showFor()`) in order to determine which candidates want to show up in the context menu. If a context menu item was selected by the user the feature will invoke the `ContextMenuCandidate.action()` method of the related candidate.
+
+`ContextMenuCandidate` contains methods (`create*()`) for creating a variety of standard context menu items that can be used when customizing the list.
+
+```Kotlin
+val customCandidates = listOf(
+ // Item from the list of standard items
+ ContextMenuCandidate.createOpenInNewTabCandidate(context, tabsUseCases),
+
+ // Custom item
+ object : ContextMenuCandidate(
+ id = "org.mozilla.custom.contextmenu.toast",
+ label = "Show a toast",
+ showFor = { session, hitResult -> hitResult.src.isNotEmpty() },
+ action = { session, hitResult ->
+ Toast.makeText(context, hitResult.src, Toast.LENGTH_SHORT).show()
+ }
+ )
+)
+
+contextMenuFeature = ContextMenuFeature(
+ fragmentManager,
+ sessionManager,
+ customCandidates)
+```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Extras | Description |
+|--------|---------|----------------|------------------------------------------|
+| CLICK | item | `itemExtras` | The user clicked on a context menu item. |
+
+
+#### `itemExtras`
+
+| Key | Type | Value |
+|--------------|---------|--------------------------------------------|
+| item | String | The `id` of the menu item that was clicked |
+
+## 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/contextmenu/build.gradle b/mobile/android/android-components/components/feature/contextmenu/build.gradle
new file mode 100644
index 0000000000..f1ab6e0b64
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/build.gradle
@@ -0,0 +1,56 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.contextmenu'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(":browser-state")
+ implementation project(':concept-engine')
+ implementation project(':feature-tabs')
+ implementation project(':feature-app-links')
+ implementation project(':browser-state')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+ implementation project(':feature-search')
+ implementation project(':ui-widgets')
+
+ implementation ComponentsDependencies.google_material
+ implementation ComponentsDependencies.androidx_constraintlayout
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+}
+
+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/contextmenu/proguard-rules.pro b/mobile/android/android-components/components/feature/contextmenu/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/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/contextmenu/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/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/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt
new file mode 100644
index 0000000000..47e3b9c037
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt
@@ -0,0 +1,689 @@
+/* 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.contextmenu
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.google.android.material.snackbar.Snackbar
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.feature.app.links.AppLinksUseCases
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.MAX_TITLE_LENGTH
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.ktx.android.content.addContact
+import mozilla.components.support.ktx.android.content.createChooserExcludingCurrentApp
+import mozilla.components.support.ktx.android.content.share
+import mozilla.components.support.ktx.kotlin.stripMailToProtocol
+import mozilla.components.support.ktx.kotlin.takeOrReplace
+import mozilla.components.ui.widgets.DefaultSnackbarDelegate
+import mozilla.components.ui.widgets.SnackbarDelegate
+
+/**
+ * A candidate for an item to be displayed in the context menu.
+ *
+ * @property id A unique ID that will be used to uniquely identify the candidate that the user selected.
+ * @property label The label that will be displayed in the context menu
+ * @property showFor If this lambda returns true for a given [SessionState] and [HitResult] then it
+ * will be displayed in the context menu.
+ * @property action The action to be invoked once the user selects this item.
+ */
+data class ContextMenuCandidate(
+ val id: String,
+ val label: String,
+ val showFor: (SessionState, HitResult) -> Boolean,
+ val action: (SessionState, HitResult) -> Unit,
+) {
+ companion object {
+ // This is used for limiting image title, in order to prevent crashes caused by base64 encoded image
+ // https://github.com/mozilla-mobile/android-components/issues/8298
+ const val MAX_TITLE_LENGTH = 2500
+
+ /**
+ * Returns the default list of context menu candidates.
+ *
+ * Use this list if you do not intend to customize the context menu items to be displayed.
+ */
+ fun defaultCandidates(
+ context: Context,
+ tabsUseCases: TabsUseCases,
+ contextMenuUseCases: ContextMenuUseCases,
+ snackBarParentView: View,
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ ): List<ContextMenuCandidate> = listOf(
+ createOpenInNewTabCandidate(
+ context,
+ tabsUseCases,
+ snackBarParentView,
+ snackbarDelegate,
+ ),
+ createOpenInPrivateTabCandidate(
+ context,
+ tabsUseCases,
+ snackBarParentView,
+ snackbarDelegate,
+ ),
+ createCopyLinkCandidate(context, snackBarParentView, snackbarDelegate),
+ createDownloadLinkCandidate(context, contextMenuUseCases),
+ createShareLinkCandidate(context),
+ createShareImageCandidate(context, contextMenuUseCases),
+ createOpenImageInNewTabCandidate(
+ context,
+ tabsUseCases,
+ snackBarParentView,
+ snackbarDelegate,
+ ),
+ createCopyImageCandidate(
+ context,
+ contextMenuUseCases,
+ ),
+ createSaveImageCandidate(context, contextMenuUseCases),
+ createSaveVideoAudioCandidate(context, contextMenuUseCases),
+ createCopyImageLocationCandidate(context, snackBarParentView, snackbarDelegate),
+ createAddContactCandidate(context),
+ createShareEmailAddressCandidate(context),
+ createCopyEmailAddressCandidate(context, snackBarParentView, snackbarDelegate),
+ )
+
+ /**
+ * Context Menu item: "Open Link in New Tab".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param tabsUseCases [TabsUseCases] used for adding new tabs.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createOpenInNewTabCandidate(
+ context: Context,
+ tabsUseCases: TabsUseCases,
+ snackBarParentView: View,
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.open_in_new_tab",
+ label = context.getString(R.string.mozac_feature_contextmenu_open_link_in_new_tab),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isHttpLink() &&
+ !tab.content.private &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { parent, hitResult ->
+ val tab = tabsUseCases.addTab(
+ hitResult.getLink(),
+ selectTab = false,
+ startLoading = true,
+ parentId = parent.id,
+ contextId = parent.contextId,
+ )
+
+ snackbarDelegate.show(
+ snackBarParentView = snackBarParentView,
+ text = R.string.mozac_feature_contextmenu_snackbar_new_tab_opened,
+ duration = Snackbar.LENGTH_LONG,
+ action = R.string.mozac_feature_contextmenu_snackbar_action_switch,
+ ) {
+ tabsUseCases.selectTab(tab)
+ }
+ },
+ )
+
+ /**
+ * Context Menu item: "Open Link in Private Tab".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param tabsUseCases [TabsUseCases] used for adding new tabs.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed. */
+ fun createOpenInPrivateTabCandidate(
+ context: Context,
+ tabsUseCases: TabsUseCases,
+ snackBarParentView: View,
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.open_in_private_tab",
+ label = context.getString(R.string.mozac_feature_contextmenu_open_link_in_private_tab),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isHttpLink() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { parent, hitResult ->
+ val tab = tabsUseCases.addTab(
+ hitResult.getLink(),
+ selectTab = false,
+ startLoading = true,
+ parentId = parent.id,
+ private = true,
+ )
+
+ snackbarDelegate.show(
+ snackBarParentView,
+ R.string.mozac_feature_contextmenu_snackbar_new_private_tab_opened,
+ Snackbar.LENGTH_LONG,
+ R.string.mozac_feature_contextmenu_snackbar_action_switch,
+ ) {
+ tabsUseCases.selectTab(tab)
+ }
+ },
+ )
+
+ /**
+ * Context Menu item: "Open Link in external App".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param appLinksUseCases [AppLinksUseCases] used to interact with urls that can be opened in 3rd party apps.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createOpenInExternalAppCandidate(
+ context: Context,
+ appLinksUseCases: AppLinksUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.open_in_external_app",
+ label = context.getString(R.string.mozac_feature_contextmenu_open_link_in_external_app),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.canOpenInExternalApp(appLinksUseCases) &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { _, hitResult ->
+ val link = hitResult.getLink()
+ val redirect = appLinksUseCases.appLinkRedirectIncludeInstall(link)
+ val appIntent = redirect.appIntent
+ val marketPlaceIntent = redirect.marketplaceIntent
+ if (appIntent != null) {
+ appLinksUseCases.openAppLink(appIntent)
+ } else if (marketPlaceIntent != null) {
+ appLinksUseCases.openAppLink(marketPlaceIntent)
+ }
+ },
+ )
+
+ /**
+ * Context Menu item: "Add to contact".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createAddContactCandidate(
+ context: Context,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.add_to_contact",
+ label = context.getString(R.string.mozac_feature_contextmenu_add_to_contact),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isMailto() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { _, hitResult -> context.addContact(hitResult.getLink().stripMailToProtocol()) },
+ )
+
+ /**
+ * Context Menu item: "Share email address".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createShareEmailAddressCandidate(
+ context: Context,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.share_email",
+ label = context.getString(R.string.mozac_feature_contextmenu_share_email_address),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isMailto() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { _, hitResult -> context.share(hitResult.getLink().stripMailToProtocol()) },
+ )
+
+ /**
+ * Context Menu item: "Copy email address".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createCopyEmailAddressCandidate(
+ context: Context,
+ snackBarParentView: View,
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.copy_email_address",
+ label = context.getString(R.string.mozac_feature_contextmenu_copy_email_address),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isMailto() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { _, hitResult ->
+ val email = hitResult.getLink().stripMailToProtocol()
+ clipPlainText(
+ context,
+ email,
+ email,
+ R.string.mozac_feature_contextmenu_snackbar_email_address_copied,
+ snackBarParentView,
+ snackbarDelegate,
+ )
+ },
+ )
+
+ /**
+ * Context Menu item: "Open Image in New Tab".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param tabsUseCases [TabsUseCases] used for adding new tabs.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createOpenImageInNewTabCandidate(
+ context: Context,
+ tabsUseCases: TabsUseCases,
+ snackBarParentView: View,
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.open_image_in_new_tab",
+ label = context.getString(R.string.mozac_feature_contextmenu_open_image_in_new_tab),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { parent, hitResult ->
+ val tab = tabsUseCases.addTab(
+ hitResult.src,
+ selectTab = false,
+ startLoading = true,
+ parentId = parent.id,
+ contextId = parent.contextId,
+ private = parent.content.private,
+ )
+
+ snackbarDelegate.show(
+ snackBarParentView = snackBarParentView,
+ text = R.string.mozac_feature_contextmenu_snackbar_new_tab_opened,
+ duration = Snackbar.LENGTH_LONG,
+ action = R.string.mozac_feature_contextmenu_snackbar_action_switch,
+ ) {
+ tabsUseCases.selectTab(tab)
+ }
+ },
+ )
+
+ /**
+ * Context Menu item: "Save image".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createSaveImageCandidate(
+ context: Context,
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.save_image",
+ label = context.getString(R.string.mozac_feature_contextmenu_save_image),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { tab, hitResult ->
+ contextMenuUseCases.injectDownload(
+ tab.id,
+ DownloadState(
+ hitResult.src,
+ skipConfirmation = true,
+ private = tab.content.private,
+ referrerUrl = tab.content.url,
+ ),
+ )
+ },
+ )
+
+ /**
+ * Context Menu item: "Copy image".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createCopyImageCandidate(
+ context: Context,
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.copy_image",
+ label = context.getString(R.string.mozac_feature_contextmenu_copy_image),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { tab, hitResult ->
+ contextMenuUseCases.injectCopyFromInternet(
+ tab.id,
+ ShareInternetResourceState(
+ url = hitResult.src,
+ private = tab.content.private,
+ referrerUrl = tab.content.url,
+ ),
+ )
+ },
+ )
+
+ /**
+ * Context Menu item: "Save video".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createSaveVideoAudioCandidate(
+ context: Context,
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.save_video",
+ label = context.getString(R.string.mozac_feature_contextmenu_save_file_to_device),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isVideoAudio() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { tab, hitResult ->
+ contextMenuUseCases.injectDownload(
+ tab.id,
+ DownloadState(
+ hitResult.src,
+ skipConfirmation = true,
+ private = tab.content.private,
+ referrerUrl = tab.content.url,
+ ),
+ )
+ },
+ )
+
+ /**
+ * Context Menu item: "Save link".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createDownloadLinkCandidate(
+ context: Context,
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.download_link",
+ label = context.getString(R.string.mozac_feature_contextmenu_download_link),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isLinkForOtherThanWebpage() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { tab, hitResult ->
+ contextMenuUseCases.injectDownload(
+ tab.id,
+ DownloadState(
+ hitResult.getLink(),
+ skipConfirmation = true,
+ private = tab.content.private,
+ referrerUrl = tab.content.url,
+ ),
+ )
+ },
+ )
+
+ /**
+ * Context Menu item: "Share Link".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createShareLinkCandidate(
+ context: Context,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.share_link",
+ label = context.getString(R.string.mozac_feature_contextmenu_share_link),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ (hitResult.isUri() || hitResult.isImage() || hitResult.isVideoAudio()) &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { _, hitResult ->
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ putExtra(Intent.EXTRA_TEXT, hitResult.getLink())
+ }
+ context.startActivity(
+ intent.createChooserExcludingCurrentApp(
+ context,
+ context.getString(R.string.mozac_feature_contextmenu_share_link),
+ ),
+ )
+ },
+ )
+
+ /**
+ * Context Menu item: "Share image"
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createShareImageCandidate(
+ context: Context,
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.share_image",
+ label = context.getString(R.string.mozac_feature_contextmenu_share_image),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { tab, hitResult ->
+ contextMenuUseCases.injectShareFromInternet(
+ tab.id,
+ ShareInternetResourceState(
+ url = hitResult.src,
+ private = tab.content.private,
+ referrerUrl = tab.content.url,
+ ),
+ )
+ },
+ )
+
+ /**
+ * Context Menu item: "Copy Link".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createCopyLinkCandidate(
+ context: Context,
+ snackBarParentView: View,
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.copy_link",
+ label = context.getString(R.string.mozac_feature_contextmenu_copy_link),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ (hitResult.isUri() || hitResult.isImage() || hitResult.isVideoAudio()) &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { _, hitResult ->
+ clipPlainText(
+ context,
+ hitResult.getLink(),
+ hitResult.getLink(),
+ R.string.mozac_feature_contextmenu_snackbar_link_copied,
+ snackBarParentView,
+ snackbarDelegate,
+ )
+ },
+ )
+
+ /**
+ * Context Menu item: "Copy Image Location".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
+ */
+ fun createCopyImageLocationCandidate(
+ context: Context,
+ snackBarParentView: View,
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
+ ) = ContextMenuCandidate(
+ id = "mozac.feature.contextmenu.copy_image_location",
+ label = context.getString(R.string.mozac_feature_contextmenu_copy_image_location),
+ showFor = { tab, hitResult ->
+ tab.isUrlSchemeAllowed(hitResult.getLink()) &&
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
+ },
+ action = { _, hitResult ->
+ clipPlainText(
+ context,
+ hitResult.getLink(),
+ hitResult.src,
+ R.string.mozac_feature_contextmenu_snackbar_link_copied,
+ snackBarParentView,
+ snackbarDelegate,
+ )
+ },
+ )
+
+ private fun clipPlainText(
+ context: Context,
+ label: String,
+ plainText: String,
+ displayTextId: Int,
+ snackBarParentView: View,
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ ) {
+ val clipboardManager =
+ context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText(label, plainText)
+ clipboardManager.setPrimaryClip(clip)
+
+ snackbarDelegate.show(
+ snackBarParentView = snackBarParentView,
+ text = displayTextId,
+ duration = Snackbar.LENGTH_SHORT,
+ )
+ }
+ }
+}
+
+// Some helper methods to work with HitResult. We may want to improve the API of HitResult and remove some of the
+// helpers eventually: https://github.com/mozilla-mobile/android-components/issues/1443
+
+private fun HitResult.isImage(): Boolean =
+ (this is HitResult.IMAGE || this is HitResult.IMAGE_SRC) && src.isNotEmpty()
+
+private fun HitResult.isVideoAudio(): Boolean =
+ (this is HitResult.VIDEO || this is HitResult.AUDIO) && src.isNotEmpty()
+
+private fun HitResult.isUri(): Boolean =
+ ((this is HitResult.UNKNOWN && src.isNotEmpty()) || this is HitResult.IMAGE_SRC)
+
+private fun HitResult.isHttpLink(): Boolean =
+ isUri() && getLink().startsWith("http")
+
+private fun HitResult.isLinkForOtherThanWebpage(): Boolean {
+ val link = getLink()
+ val isHtml = link.endsWith("html") || link.endsWith("htm")
+ return isHttpLink() && !isHtml
+}
+
+private fun HitResult.isIntent(): Boolean =
+ (
+ this is HitResult.UNKNOWN && src.isNotEmpty() &&
+ getLink().startsWith("intent:")
+ )
+
+private fun HitResult.isMailto(): Boolean =
+ (this is HitResult.UNKNOWN && src.isNotEmpty()) &&
+ getLink().startsWith("mailto:")
+
+private fun HitResult.canOpenInExternalApp(appLinksUseCases: AppLinksUseCases): Boolean {
+ if (isHttpLink() || isIntent() || isVideoAudio()) {
+ val redirect = appLinksUseCases.appLinkRedirectIncludeInstall(getLink())
+ return redirect.hasExternalApp() || redirect.hasMarketplaceIntent()
+ }
+ return false
+}
+
+internal fun HitResult.getLink(): String = when (this) {
+ is HitResult.UNKNOWN -> src
+ is HitResult.IMAGE_SRC -> uri
+ is HitResult.IMAGE ->
+ if (title.isNullOrBlank()) {
+ src.takeOrReplace(MAX_TITLE_LENGTH, "image")
+ } else {
+ title.toString()
+ }
+ is HitResult.VIDEO ->
+ if (title.isNullOrBlank()) src else title.toString()
+ is HitResult.AUDIO ->
+ if (title.isNullOrBlank()) src else title.toString()
+ else -> "about:blank"
+}
+
+@VisibleForTesting
+internal fun SessionState.isUrlSchemeAllowed(url: String): Boolean {
+ return when (val engineSession = engineState.engineSession) {
+ null -> true
+ else -> {
+ val urlScheme = Uri.parse(url).normalizeScheme().scheme
+ !engineSession.getBlockedSchemes().contains(urlScheme)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuFeature.kt b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuFeature.kt
new file mode 100644
index 0000000000..5c391463d4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuFeature.kt
@@ -0,0 +1,140 @@
+/* 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.contextmenu
+
+import android.view.HapticFeedbackConstants
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.fragment.app.FragmentManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+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.EngineView
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.feature.contextmenu.facts.emitCancelMenuFact
+import mozilla.components.feature.contextmenu.facts.emitClickFact
+import mozilla.components.feature.contextmenu.facts.emitDisplayFact
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val FRAGMENT_TAG = "mozac_feature_contextmenu_dialog"
+
+/**
+ * Feature for displaying a context menu after long-pressing web content.
+ *
+ * This feature will subscribe to the currently selected tab and display a context menu based on
+ * the [HitResult] in its `ContentState`. Once the context menu is closed or the user selects an
+ * item from the context menu the related [HitResult] will be consumed.
+ *
+ * @property fragmentManager The [FragmentManager] to be used when displaying a context menu (fragment).
+ * @property store The [BrowserStore] this feature should subscribe to.
+ * @property candidates A list of [ContextMenuCandidate] objects. For every observed [HitResult] this feature will query
+ * all candidates ([ContextMenuCandidate.showFor]) in order to determine which candidates want to show up in the context
+ * menu. If a context menu item was selected by the user the feature will invoke the [ContextMenuCandidate.action]
+ * method of the related candidate.
+ * @property engineView The [EngineView]] this feature component should show context menus for.
+ * @param tabId Optional id of a tab. Instead of showing context menus for the currently selected tab this feature will
+ * show only context menus for this tab if an id is provided.
+ * @param additionalNote which it will be attached to the bottom of context menu but for a specific [HitResult]
+ */
+class ContextMenuFeature(
+ private val fragmentManager: FragmentManager,
+ private val store: BrowserStore,
+ private val candidates: List<ContextMenuCandidate>,
+ private val engineView: EngineView,
+ private val useCases: ContextMenuUseCases,
+ private val tabId: String? = null,
+ private val additionalNote: (HitResult) -> String? = { null },
+) : LifecycleAwareFeature {
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Start observing the selected session and when needed show a context menu.
+ */
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .distinctUntilChangedBy { it?.content?.hitResult }
+ .collect { state ->
+ val hitResult = state?.content?.hitResult
+ if (hitResult != null) {
+ showContextMenu(state, hitResult)
+ } else {
+ hideContextMenu()
+ }
+ }
+ }
+ }
+
+ /**
+ * Stop observing the selected session and do not show any context menus anymore.
+ */
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun showContextMenu(tab: SessionState, hitResult: HitResult) {
+ fragmentManager.findFragmentByTag(FRAGMENT_TAG)?.let { fragment ->
+ // There's already a ContextMenuFragment being displayed. Let's only make sure it has
+ // a reference to this feature instance.
+ (fragment as ContextMenuFragment).feature = this
+ return
+ }
+
+ val (ids, labels) = candidates
+ .filter { candidate -> candidate.showFor(tab, hitResult) }
+ .fold(Pair(mutableListOf<String>(), mutableListOf<String>())) { items, candidate ->
+ items.first.add(candidate.id)
+ items.second.add(candidate.label)
+ items
+ }
+
+ // We have no context menu items to show for this HitResult. Let's consume it to remove it from the Session.
+ if (ids.isEmpty()) {
+ useCases.consumeHitResult(tab.id)
+ return
+ }
+
+ // We know that we are going to show a context menu. Now is the time to perform the haptic feedback.
+ engineView.asView().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+
+ val fragment = ContextMenuFragment.create(tab, hitResult.getLink(), ids, labels, additionalNote(hitResult))
+ fragment.feature = this
+ emitDisplayFact(labels.joinToString())
+ fragment.show(fragmentManager, FRAGMENT_TAG)
+ }
+
+ private fun hideContextMenu() {
+ emitCancelMenuFact()
+ fragmentManager.findFragmentByTag(FRAGMENT_TAG)?.let { fragment ->
+ fragmentManager.beginTransaction()
+ .remove(fragment)
+ .commitAllowingStateLoss()
+ }
+ }
+
+ internal fun onMenuItemSelected(tabId: String, itemId: String) {
+ val tab = store.state.findTabOrCustomTab(tabId) ?: return
+ val candidate = candidates.find { it.id == itemId } ?: return
+
+ useCases.consumeHitResult(tab.id)
+
+ tab.content.hitResult?.let { hitResult ->
+ candidate.action.invoke(tab, hitResult)
+ emitClickFact(candidate)
+ }
+ }
+
+ internal fun onMenuCancelled(tabId: String) {
+ useCases.consumeHitResult(tabId)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuFragment.kt b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuFragment.kt
new file mode 100644
index 0000000000..e585aa2c4f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuFragment.kt
@@ -0,0 +1,173 @@
+/* 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.contextmenu
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Build
+import android.os.Bundle
+import android.text.Html
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.text.HtmlCompat
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.textview.MaterialTextView
+import mozilla.components.browser.state.state.SessionState
+
+private const val EXPANDED_TITLE_MAX_LINES = 15
+private const val KEY_TITLE = "title"
+private const val KEY_SESSION_ID = "session_id"
+private const val KEY_IDS = "ids"
+private const val KEY_LABELS = "labels"
+private const val KEY_ADDITIONAL_NOTE = "additional_note"
+
+/**
+ * [DialogFragment] implementation to display the actual context menu dialog.
+ */
+class ContextMenuFragment : DialogFragment() {
+ internal var feature: ContextMenuFeature? = null
+
+ @VisibleForTesting internal val itemIds: List<String> by lazy {
+ requireArguments().getStringArrayList(KEY_IDS)!!
+ }
+
+ @VisibleForTesting internal val itemLabels: List<String> by lazy {
+ requireArguments().getStringArrayList(KEY_LABELS)!!
+ }
+
+ @VisibleForTesting internal val sessionId: String by lazy {
+ requireArguments().getString(KEY_SESSION_ID)!!
+ }
+
+ @VisibleForTesting internal val title: String by lazy {
+ requireArguments().getString(KEY_TITLE)!!
+ }
+
+ @VisibleForTesting internal val additionalNote: String? by lazy {
+ requireArguments().getString(KEY_ADDITIONAL_NOTE)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ @SuppressLint("UseGetLayoutInflater")
+ val inflater = LayoutInflater.from(requireContext())
+
+ val builder = AlertDialog.Builder(requireContext())
+ .setCustomTitle(createDialogTitleView(inflater))
+ .setView(createDialogContentView(inflater))
+
+ return builder.create()
+ }
+
+ @SuppressLint("InflateParams")
+ internal fun createDialogTitleView(inflater: LayoutInflater): View {
+ return inflater.inflate(
+ R.layout.mozac_feature_contextmenu_title,
+ null,
+ ).findViewById<AppCompatTextView>(
+ R.id.titleView,
+ ).apply {
+ text = title
+
+ setOnClickListener {
+ maxLines = EXPANDED_TITLE_MAX_LINES
+ }
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ internal fun createDialogContentView(inflater: LayoutInflater): View {
+ val view = inflater.inflate(R.layout.mozac_feature_contextmenu_dialog, null)
+
+ view.findViewById<RecyclerView>(R.id.recyclerView).apply {
+ layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ adapter = ContextMenuAdapter(this@ContextMenuFragment, inflater)
+ }
+
+ additionalNote?.let { value ->
+ val additionalNoteView = view.findViewById<MaterialTextView>(R.id.additional_note)
+ additionalNoteView.visibility = View.VISIBLE
+ additionalNoteView.text = getSpannedValueOfString(value)
+ }
+
+ return view
+ }
+
+ private fun getSpannedValueOfString(value: String) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ Html.fromHtml(value, HtmlCompat.FROM_HTML_MODE_LEGACY)
+ } else {
+ @Suppress("Deprecation")
+ Html.fromHtml(value)
+ }
+
+ internal fun onItemSelected(position: Int) {
+ feature?.onMenuItemSelected(sessionId, itemIds[position])
+
+ dismiss()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ feature?.onMenuCancelled(sessionId)
+ }
+
+ companion object {
+ /**
+ * Create a new [ContextMenuFragment].
+ */
+ fun create(
+ tab: SessionState,
+ title: String,
+ ids: List<String>,
+ labels: List<String>,
+ additionalNote: String?,
+ ): ContextMenuFragment {
+ val arguments = Bundle()
+ arguments.putString(KEY_TITLE, title)
+ arguments.putStringArrayList(KEY_IDS, ArrayList(ids))
+ arguments.putStringArrayList(KEY_LABELS, ArrayList(labels))
+ arguments.putString(KEY_SESSION_ID, tab.id)
+ arguments.putString(KEY_ADDITIONAL_NOTE, additionalNote)
+
+ val fragment = ContextMenuFragment()
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
+
+/**
+ * RecyclerView adapter for displaying the context menu.
+ */
+internal class ContextMenuAdapter(
+ private val fragment: ContextMenuFragment,
+ private val inflater: LayoutInflater,
+) : RecyclerView.Adapter<ContextMenuViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, position: Int) = ContextMenuViewHolder(
+ inflater.inflate(R.layout.mozac_feature_contextmenu_item, parent, false),
+ )
+
+ override fun getItemCount(): Int = fragment.itemIds.size
+
+ override fun onBindViewHolder(holder: ContextMenuViewHolder, position: Int) {
+ val label = fragment.itemLabels[position]
+ holder.labelView.text = label
+
+ holder.itemView.setOnClickListener { fragment.onItemSelected(position) }
+ }
+}
+
+/**
+ * View holder for a context menu item.
+ */
+internal class ContextMenuViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal val labelView = itemView.findViewById<TextView>(R.id.labelView)
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuUseCases.kt b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuUseCases.kt
new file mode 100644
index 0000000000..a523917221
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuUseCases.kt
@@ -0,0 +1,85 @@
+/* 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.contextmenu
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CopyInternetResourceAction
+import mozilla.components.browser.state.action.ShareInternetResourceAction
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.HitResult
+
+/**
+ * Contains use cases related to the context menu feature.
+ *
+ * @param store the application's [BrowserStore].
+ */
+class ContextMenuUseCases(
+ store: BrowserStore,
+) {
+ class ConsumeHitResultUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Consumes the [HitResult] from the [BrowserStore] with the given [tabId].
+ */
+ operator fun invoke(tabId: String) {
+ store.dispatch(ContentAction.ConsumeHitResultAction(tabId))
+ }
+ }
+
+ class InjectDownloadUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Adds a [DownloadState] to the [BrowserStore] with the given [tabId].
+ *
+ * This is a hacky workaround. After we have migrated everything from browser-session to
+ * browser-state we should revisits this and find a better solution.
+ */
+ operator fun invoke(tabId: String, download: DownloadState) {
+ store.dispatch(
+ ContentAction.UpdateDownloadAction(
+ tabId,
+ download,
+ ),
+ )
+ }
+ }
+
+ /**
+ * Usecase allowing adding a new 'share' [ShareInternetResourceState] to the [BrowserStore]
+ */
+ class InjectShareInternetResourceUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Adds a specific [ShareInternetResourceState] to be shared to the [BrowserStore].
+ */
+ operator fun invoke(tabId: String, internetResource: ShareInternetResourceState) {
+ store.dispatch(ShareInternetResourceAction.AddShareAction(tabId, internetResource))
+ }
+ }
+
+ /**
+ * Use case allowing adding a new 'copy' [ShareInternetResourceState] to the [BrowserStore]
+ */
+ class InjectCopyInternetResourceUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Adds a specific [ShareInternetResourceState] to be copied to the [BrowserStore].
+ */
+ operator fun invoke(tabId: String, internetResource: ShareInternetResourceState) {
+ store.dispatch(CopyInternetResourceAction.AddCopyAction(tabId, internetResource))
+ }
+ }
+
+ val consumeHitResult = ConsumeHitResultUseCase(store)
+ val injectDownload = InjectDownloadUseCase(store)
+ val injectShareFromInternet = InjectShareInternetResourceUseCase(store)
+ val injectCopyFromInternet = InjectCopyInternetResourceUseCase(store)
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/DefaultSelectionActionDelegate.kt b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/DefaultSelectionActionDelegate.kt
new file mode 100644
index 0000000000..9380c8ca47
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/DefaultSelectionActionDelegate.kt
@@ -0,0 +1,119 @@
+/* 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.contextmenu
+
+import android.content.res.Resources
+import android.util.Patterns
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import mozilla.components.feature.contextmenu.facts.emitTextSelectionClickFact
+import mozilla.components.feature.search.SearchAdapter
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val SEARCH = "CUSTOM_CONTEXT_MENU_SEARCH"
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val SEARCH_PRIVATELY = "CUSTOM_CONTEXT_MENU_SEARCH_PRIVATELY"
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val SHARE = "CUSTOM_CONTEXT_MENU_SHARE"
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val EMAIL = "CUSTOM_CONTEXT_MENU_EMAIL"
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val CALL = "CUSTOM_CONTEXT_MENU_CALL"
+
+private val customActions = arrayOf(CALL, EMAIL, SEARCH, SEARCH_PRIVATELY, SHARE)
+
+/**
+ * Adds normal and private search buttons to text selection context menus.
+ * Also adds share, email, and call actions which are optionally displayed.
+ */
+class DefaultSelectionActionDelegate(
+ private val searchAdapter: SearchAdapter,
+ resources: Resources,
+ private val shareTextClicked: ((String) -> Unit)? = null,
+ private val emailTextClicked: ((String) -> Unit)? = null,
+ private val callTextClicked: ((String) -> Unit)? = null,
+ private val actionSorter: ((Array<String>) -> Array<String>)? = null,
+) : SelectionActionDelegate {
+
+ private val normalSearchText =
+ resources.getString(R.string.mozac_selection_context_menu_search_2)
+ private val privateSearchText =
+ resources.getString(R.string.mozac_selection_context_menu_search_privately_2)
+ private val shareText = resources.getString(R.string.mozac_selection_context_menu_share)
+ private val emailText = resources.getString(R.string.mozac_selection_context_menu_email)
+ private val callText = resources.getString(R.string.mozac_selection_context_menu_call)
+
+ override fun getAllActions(): Array<String> = customActions
+
+ @SuppressWarnings("ComplexMethod")
+ override fun isActionAvailable(id: String, selectedText: String): Boolean {
+ val isPrivate = searchAdapter.isPrivateSession()
+ return (id == SHARE && shareTextClicked != null) ||
+ (
+ id == EMAIL && emailTextClicked != null &&
+ Patterns.EMAIL_ADDRESS.matcher(selectedText.trim()).matches()
+ ) ||
+ (
+ id == CALL &&
+ callTextClicked != null && Patterns.PHONE.matcher(selectedText.trim()).matches()
+ ) ||
+ (id == SEARCH && !isPrivate) ||
+ (id == SEARCH_PRIVATELY && isPrivate)
+ }
+
+ override fun getActionTitle(id: String): CharSequence? = when (id) {
+ SEARCH -> normalSearchText
+ SEARCH_PRIVATELY -> privateSearchText
+ SHARE -> shareText
+ EMAIL -> emailText
+ CALL -> callText
+ else -> null
+ }
+
+ override fun performAction(id: String, selectedText: String): Boolean {
+ emitTextSelectionClickFact(id)
+ return when (id) {
+ SEARCH -> {
+ searchAdapter.sendSearch(false, selectedText)
+ true
+ }
+ SEARCH_PRIVATELY -> {
+ searchAdapter.sendSearch(true, selectedText)
+ true
+ }
+ SHARE -> {
+ shareTextClicked?.invoke(selectedText)
+ true
+ }
+ EMAIL -> {
+ emailTextClicked?.invoke(selectedText.trim())
+ true
+ }
+ CALL -> {
+ callTextClicked?.invoke(selectedText.trim())
+ true
+ }
+ else -> {
+ false
+ }
+ }
+ }
+
+ /**
+ * Takes in a list of actions and sorts them.
+ * @returns the sorted list.
+ */
+ override fun sortedActions(actions: Array<String>): Array<String> {
+ return if (actionSorter != null) {
+ actionSorter.invoke(actions)
+ } else {
+ actions
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ext/DefaultSelectionActionDelegate.kt b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ext/DefaultSelectionActionDelegate.kt
new file mode 100644
index 0000000000..b9de9f2d0d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ext/DefaultSelectionActionDelegate.kt
@@ -0,0 +1,32 @@
+/* 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.contextmenu.ext
+
+import android.content.Context
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
+import mozilla.components.feature.search.BrowserStoreSearchAdapter
+import mozilla.components.support.ktx.android.content.call
+import mozilla.components.support.ktx.android.content.email
+import mozilla.components.support.ktx.android.content.share
+
+/**
+ * More convenient secondary constructor for creating a [DefaultSelectionActionDelegate].
+ */
+@Suppress("FunctionName")
+fun DefaultSelectionActionDelegate(
+ store: BrowserStore,
+ context: Context,
+ shareTextClicked: ((String) -> Unit)? = { context.share(it) },
+ emailTextClicked: ((String) -> Unit)? = { context.email(it) },
+ callTextClicked: ((String) -> Unit)? = { context.call(it) },
+) =
+ DefaultSelectionActionDelegate(
+ BrowserStoreSearchAdapter(store),
+ context.resources,
+ shareTextClicked,
+ emailTextClicked,
+ callTextClicked,
+ )
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/facts/ContextMenuFacts.kt b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/facts/ContextMenuFacts.kt
new file mode 100644
index 0000000000..55c694ddab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/facts/ContextMenuFacts.kt
@@ -0,0 +1,58 @@
+/* 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.contextmenu.facts
+
+import mozilla.components.feature.contextmenu.ContextMenuCandidate
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [ContextMenuFeature]
+ */
+class ContextMenuFacts {
+ /**
+ * Items that specify which portion of the [ContextMenuFeature] was interacted with
+ */
+ object Items {
+ const val MENU = "menu"
+ const val ITEM = "item"
+ const val TEXT_SELECTION_OPTION = "textSelectionOption"
+ }
+}
+
+private fun emitContextMenuFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_CONTEXTMENU,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitClickFact(candidate: ContextMenuCandidate) {
+ val metadata = mapOf("item" to candidate.id)
+ emitContextMenuFact(Action.CLICK, ContextMenuFacts.Items.ITEM, metadata = metadata)
+}
+
+internal fun emitDisplayFact(labels: String) {
+ emitContextMenuFact(Action.DISPLAY, ContextMenuFacts.Items.MENU, labels)
+}
+
+internal fun emitCancelMenuFact() {
+ emitContextMenuFact(Action.CANCEL, ContextMenuFacts.Items.MENU)
+}
+
+internal fun emitTextSelectionClickFact(optionId: String) {
+ val metadata = mapOf("textSelectionOption" to optionId)
+ emitContextMenuFact(Action.CLICK, ContextMenuFacts.Items.TEXT_SELECTION_OPTION, metadata = metadata)
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_dialog.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_dialog.xml
new file mode 100644
index 0000000000..9918f93fe3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_dialog.xml
@@ -0,0 +1,39 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recyclerView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:scrollbars="vertical"
+ android:paddingBottom="16dp"
+ android:clipToPadding="false"
+ app:layout_constraintBottom_toTopOf="@+id/additional_note"
+ app:layout_constraintHeight_default="wrap"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/additional_note"
+ style="@style/Mozac.Dialog.AdditionalNote"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="24dp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/recyclerView" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_item.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_item.xml
new file mode 100644
index 0000000000..47b6e14706
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_item.xml
@@ -0,0 +1,18 @@
+<?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/. -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/labelView"
+ style="@android:style/TextAppearance.Material.Menu"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:ellipsize="end"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:lines="1"
+ android:paddingEnd="16dp"
+ android:paddingStart="16dp"
+ android:textSize="16sp" />
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_title.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_title.xml
new file mode 100644
index 0000000000..57319ac75c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/layout/mozac_feature_contextmenu_title.xml
@@ -0,0 +1,23 @@
+<?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/. -->
+<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/titleView"
+ style="@style/Base.DialogWindowTitle.AppCompat"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="2"
+ android:paddingStart="16dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="8dp"
+ android:paddingBottom="16dp"
+ app:autoSizeMaxTextSize="20sp"
+ app:autoSizeMinTextSize="12sp"
+ app:autoSizeStepGranularity="2sp"
+ app:autoSizeTextType="uniform"
+ tools:text="http://www.mozilla.org" />
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..3e3debaac9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-am/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">አገናኙን በአዲስ ትር ውስጥ ክፈት</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">አገናኙን በግል ትር ውስጥ ክፈት</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">ምስሉን በአዲስ ትር ውስጥ ክፈት</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">አገናኙን አውርድ</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">አገናኝ አጋራ</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ምስል አጋራ</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">አገናኝ ቅዳ</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">የምስሉን መገኛ አገናኝ ቅዳ</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ምስል አስቀምጥ</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">ምስል ቅዳ</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ፋይሉን ወደ መሳሪያ አስቀምጥ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">አዲስ ትር ተከፍቷል</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">አዲስ የግል ትር ተከፍቷል</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">አገናኝ ወደ ቅንጥብ ሰሌዳ ተቀድቷል</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ቀይር</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">አገናኙን በሌላ መተግበሪያ ውስጥ ክፈት</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">የኢሜይል አድራሻ አጋራ</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">የኢሜል አድራሻ ቅዳ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">የኢሜል አድራሻ ወደ ቅንጥብ ሰሌዳ ተቀድቷል</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">ወደ እውቂያ አክል</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ፈልግ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">የግል ፍለጋ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">አጋራ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ኢሜይል</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">ደውል</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..c9be3f65a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-an/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Ubrir lo vinclo en una nueva pestanya</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Ubrir lo vinclo en una pestanya privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Ubrir la imachen en una nueva pestanya</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Descargar lo vinclo</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir lo vinclo</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir la imachen</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar lo vinclo</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar l’adreza d’a imachen</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Alzar la imachen</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Alzar la imachen en dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">S’ha ubierto una nueva pestanya</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">S’ha ubierto una nueva pestanya privada</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">S’ha copiau lo vinclo en o portafuellas</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Cambiar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Ubrir lo vinclo en una aplicación externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir adreza de correu-e</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar adreza de correu-e</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Adreza de correu copiada ta lo portafuellas</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Anyadir a contacto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Buscar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Busqueda privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Correu-e</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Gritar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..7f770f9725
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ar/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">افتح الرابط في لسان جديد</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">افتح الرابط في لسان خاص</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">افتح الصورة في لسان جديد</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">نزّل الرابط</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">شارِك الرابط</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">شارك الصورة</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">انسخ الرابط</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">انسخ مكان الصورة</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">احفظ الصورة</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">انسخ الصورة</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">احفظ الملف في الجهاز</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">فُتِح لسان جديد</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">فُتِح لسان خاص جديد</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">نُسخ الرابط إلى الحافظة</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">انتقل</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">افتح الرابط في تطبيق خارجي</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">شارِك عنوان البريد</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">انسخ عنوان البريد الإلكتروني</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">نُسخ عنوان البريد إلى الحافظة</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">أضِف إلى المتراسلين</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ابحث</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">بحث خاص</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">شارِك</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">أبرِد</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">اتصل به</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..0bcbede6fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ast/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir l\'enllaz nuna llingüeta nueva</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir l\'enllaz nuna llingüeta privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir la imaxe nuna llingüeta nueva</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Baxar l\'enllaz</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir l\'enllaz</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir la imaxe</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar l\'enllaz</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar la llocalización de la imaxe</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Guardar la imaxe</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar la imaxe</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Guardar el ficheru nel preséu</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Abrióse una llingüeta nueva</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Abrióse una llingüeta privada nueva</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">L\'enllaz copióse al cartafueyu</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Cambiar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir l\'enllaz nuna aplicación esterna</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir la direición de corréu electrónicu</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar la direición de corréu electrónicu</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">La direición de corréu electrónicu copióse al cartafueyu</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Amestar a un contautu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Buscar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Busca privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Unviar per corréu electrónicu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Llamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..ace251e709
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-az/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Keçidi yeni vərəqdə aç</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Keçidi məxfi vərəqdə aç</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Şəkli yeni vərəqdə aç</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Endirmə keçidi</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Keçidi paylaş</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Şəkli paylaş</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Keçidi köçür</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Şəkil ünvanını köçür</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Şəkli saxla</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Şəkli cihaza saxla</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Yeni vərəq açıldı</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Yeni məxfi vərəq açıldı</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Keçid buferə köçürüldü</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Keç</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Keçidi başqa tətbiqdə aç</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">E-poçt ünvanını paylaş</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">E-poçt ünvanını köçür</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-poçt ünvanı mübadilə buferinə köçürüldü</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Əlaqələrə əlavə et</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Axtar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Məxfi Axtarış</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Paylaş</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-poçt</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Zəng et</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..97b092eb5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-azb/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">باغلانتی‌نی یئنی تاغدا آچ</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">باغلانتی‌نی گیزلی تاغدا آچ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">عکسی یئنی تاغدا آچ</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">باغلانتی‌نی یئندیر</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">باغلانتی‌نی پایلاش</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">عکسی پایلاش</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">باغلانتی‌نی کوپی ائله</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">عکس یئرینی کوپی ائله</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">عکسی ساخلا</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">عکسی کوپی ائله</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">فایلی جهازدا ساخلا</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">یئنی تاغ آچیلدی</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">یئنی گیزلی تاغ آچیلدی</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">باغلانتی کلیپ‌بوردا کوپی اولدو</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">بونا گئچ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">باغلانتی‌نی ائشیکده‌کی اپ‌ده آچ</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ایمیل آدرسی پایلاش</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ایمیل آدرسی کوپی ائله</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ایمیل آدرسی کلیپ‌بوردا کوپی اولدو</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">موخاطب‌لره آرتیر</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">آختاریش</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">گیزلی آختاریش</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">پایلاش</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ایمیل</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">تماس</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..a478cc786c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ban/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Unduh tautan</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Ngbagiang tautan</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Ngbagiang gambar</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Raksa gambar</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Raksa berkas ka piranti</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Tambah ka kontak</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Rereh</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Bagiang</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Rerepél</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..06aed70eef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-be/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Адкрыць спасылку ў новай картцы</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Адкрыць спасылку ў прыватнай картцы</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Адкрыць выяву ў новай картцы</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Сцягнуць спасылку</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Падзяліцца спасылкай</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Падзяліцца выявай</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Скапіраваць спасылку</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Капіяваць адрас выявы</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Захаваць выяву</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Капіяваць відарыс</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Захавайце файл на прыладзе</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Адкрыта новая картка</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Адкрыта новая прыватная картка</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Спасылка скапіявана ў буфер абмену</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Перайсці</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Адкрыць спасылку ў знешняй праграме</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Падзяліцца адрасам эл.пошты</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Капіяваць адрас эл.пошты</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Адрас пошты скапіяваны ў буфер абмену</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Дадаць у кантакт</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Шукаць</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Прыватны пошук</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Падзяліцца</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Эл.пошта</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Выклік</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..816956615f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bg/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Отваряне в раздел</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Отваряне в поверителен раздел</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Отваряне в раздел</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Изтегляне на препратката</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Споделяне на препратка</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Споделяне на изображение</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Копиране на препратка</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Копиране адреса на изображение</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Запазване на изображение</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Копиране на изображението</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Запазване на файла на устройството</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Отворен е нов раздел</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Отворен е нов поверителен раздел</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Препратката е копирана в буфера</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Отваряне</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Отваряне на препратка в приложение</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Споделяне на електронен адрес</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Копиране на електронен адрес</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Имейлът е копиран в буфера</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Добавяне към контакт</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Търсене</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Поверително търсене</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Споделяне</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Имейл</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Позвъняване</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..c5f3a8c7a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bn/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">একটি নতুন ট্যাবে লিংক খুলুন</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">লিংকটি ব্যক্তিগত ট্যাবে খুলুন</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">একটি নতুন ট্যাবে ছবিটি খুলুন</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ডাউনলোড লিঙ্ক</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">লিংক শেয়ার করুন</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ছবি শেয়ার করুন</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">লিংক কপি করুন</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">ছবির অবস্থান কপি করুন</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ছবি সংরক্ষণ করুন</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ডিভাইসে ফাইল সংরক্ষণ করুন</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">নতুন ট্যাব খোলা হয়েছে</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">নতুন ব্যক্তিগত ট্যাব খোলা হয়েছে</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">ক্লিপবোর্ডে লিংক কপি করা হয়েছে</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">পরিবর্তন</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">বাইরের অ্যাপে লিংক খুলুন</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ইমেইল ঠিকানা শেয়ার করো</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ইমেইল ঠিকানা অনুলিপি</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ইমেইল ঠিকানা ক্লিপবোর্ডে অনুলিপি করা হয়েছে</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">কন্টাক্টে যোগ করুন</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">অনুসন্ধান</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">ব্যক্তিগত অনুসন্ধান</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">শেয়ার</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ইমেইল</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">কল</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..92f48be6db
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-br/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Digeriñ an ere e-barzh un ivinell nevez</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Digeriñ an ere en un ivinell brevez</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Digeriñ ar skeudenn e-barzh un ivinell nevez</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Pellgargañ an ere</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Rannañ an ere</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Rannañ ar skeudenn</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Eilañ an ere</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Eilañ lecʼhiadur ar skeudenn</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Enrollañ ar skeudenn</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Eilañ ar skeudenn</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Enrollañ ar restr en trevnad</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Digor eo an ivinell nevez</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Ivinell brevez nevez bet digoret</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ere eilet er golver</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Mont</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Digeriñ an ere en un arload diavaez</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Rannañ ar chomlec’h postel</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Eilañ ar chomlec’h postel</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Chomlec’h postel eilet er golver</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Ouzhpennañ en darempredoù</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Klask</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Klask prevez</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Rannañ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Postel</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Gervel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..1a19e0fb69
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-bs/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Otvori link u novom tabu</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Otvori link u privatnom tabu</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Otvori sliku u novom tabu</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Preuzmi link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Podijeli link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Podijeli sliku</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopiraj link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopiraj lokaciju slike</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Spasi sliku</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopiraj sliku</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Spasi fajl na uređaj</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Otvoren novi tab</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Otvoren novi privatni tab</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link kopiran na clipboard</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Prebaci</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Otvori link u vanjskoj aplikaciji</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Podijeli email adresu</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopiraj email adresu</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Email adresa kopirana u clipboard</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Dodaj u kontakt</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Traži</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privatna pretraga</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Podijeli</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Nazovi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..d035c774d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ca/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Obre l’enllaç en una pestanya nova</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Obre l’enllaç en una pestanya privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Obre la imatge en una pestanya nova</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Baixa l’enllaç</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Comparteix l’enllaç</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Comparteix la imatge</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copia l’enllaç</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copia la ubicació de la imatge</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Desa la imatge</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copia la imatge</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Desa el fitxer al dispositiu</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">S’ha obert una pestanya nova</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">S’ha obert una pestanya privada nova</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">S’ha copiat l’enllaç al porta-retalls</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Vés-hi</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Obre l’enllaç en una aplicació externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Comparteix l’adreça electrònica</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copia l’adreça electrònica</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">S’ha copiat l’adreça al porta-retalls</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Afegeix a un contacte</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Cerca</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Cerca privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Comparteix</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Correu electrònic</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Truca</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..4b0ed88240
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cak/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Tijaq ri ximonel pa jun k\'ak\'a\' ruwi\'</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Tijaq ximöy pa jun ichinan ruwi\'</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Tijaq wachib\'äl pa jun k\'ak\'a\' ruwi\'</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Niqasäx ximonel</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Tikomonïx ri ximöy</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Tikomonïx wachib\'äl</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Tiwachib\'ëx ximonel</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Tiwachib\'ëx rub\'ey wachib\'äl</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Tiyak wachib\'äl</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Tiwachib\'ëx wachib\'äl</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Tityak yakb\'äl pan okisab\'äl</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Xjaq k\'ak\'a\' ruwi\'</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Xjaq k\'ak\'a\' ichinan ruwi\'</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Xwachib\'ëx ri ximonel molwuj</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Tijalwachïx</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Tijaq ximonel pa jun chik chokoy</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Tikomonïx rochochib\'al taqoya\'l</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Tiwachib\'ëx rochochib\'al taqoya\'l</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Taqoya\'l wachib\'en pa molwuj</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Titz\'aqatisäx pa rub\'i\' achib\'il</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Tikanöx</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Ichinan Kanoxïk</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Tikomonïx</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Taqoya\'l</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Toyon</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..ff3d7d4286
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">i-Open ang link sa bag-ong tab</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">i-Open ang link sa pribadong tab</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">i-Open ang image sa bag-ong tab</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">i-Download ang link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">i-Share ang link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">i-Share ang image</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopyaha ang link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopyaha ang image location</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">i-Save ang image</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">i-Save ang file sa device</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Bag-ong tab na-open</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Bag-ong private tab na-open</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ang link na-kopya sa clipboard</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Balhin</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">i-Open ang link sa external nga app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">i-Share ang email address</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopyaha ang email address</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Ang email address nakopya na sa clipboard</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Idugang sa contact</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Search</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Private Search</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Share</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Call</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..1876064ce0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">بەستەر بکەرەوە لە بازدەری نوێ</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">بەستەر بکەرەوە لە بازدەری تایبەت</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">وێنە بکەرەوە لە بازدەری نوێ</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">بەستەری داگرتن</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">بەستەر بڵاوبکەرەوە</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">وێنە بڵاوبکەرەوە</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">بەستەر لەبەربگرەوە</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">شوێنی وێنە لەبەربگرەوە</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">وێنە پاشەکەوت بکە</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">پەڕگە پاشەکەوتبکە لە ئامێر</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">بازدەری نوێ کرایەوە</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">بازدەری تایبەتی نوێ کرایەوە</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">بەستەر لەبەرگیراوە بۆ گرتەتەختە</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">گۆڕین</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">بەستەر لە بەرنامەی دەرەکی بکەرەوە</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">پۆستی ئەلیکترۆنی بڵابکەرەوە</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">پۆستی ئەلیکترۆنی لەبەربگرەوە</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">پۆستی ئەلیکترۆنی لەبەرگیرایەوە لە گرتەتەختە</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">زیادی بکە بۆ پەیوەندیکەران</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">گەڕان</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">گەڕانی تایبەت</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">بڵاوکردنەوە</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">پۆستی ئەلکترۆنی</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">پەیوەندی</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..9a04c02cc0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-co/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Apre u liame in una nova unghjetta</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Apre u liame in una nova unghjetta privata</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Apre a fiura in una nova unghjetta</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Scaricà a destinazione di u liame</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Sparte u liame</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Sparte a fiura</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Cupià u liame</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Cupià l’indirizzu di a fiura</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Arregistrà a fiura</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Cupià a fiura</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Arregistrà u schedariu nant’à l’apparechju</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nova unghjetta aperta</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nova unghjetta privata aperta</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Liame cupiatu in u preme’papei</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Affissà</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Apre u liame in un’appiecazione esterna</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Sparte l’indirizzu elettronicu</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Cupià l’indirizzu elettronicu</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">L’indirizzu elettronicu hè statu cupiatu in u preme’papei</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Aghjunghje à un cuntattu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Ricercà</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Ricerca privata</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Sparte</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Mandà un messaghju</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Chjamà</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..65b3397a18
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cs/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Otevřít odkaz v novém panelu</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Otevřít odkaz v anonymním panelu</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Otevřít obrázek v novém panelu</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Stáhnout</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Sdílet odkaz</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Sdílet obrázek</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopírovat odkaz</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopírovat adresu obrázku</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Uložit obrázek</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopírovat obrázek</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Uložit soubor do zařízení</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nový panel otevřen</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nový anonymní panel otevřen</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Odkaz zkopírován do schránky</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Přepnout</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Otevřít odkaz v externí aplikaci</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Sdílet e-mailovou adresu</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopírovat e-mailovou adresu</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-mailová adresa byla zkopírována do schránky</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Přidat kontakt</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Vyhledat</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Vyhledat v anonymním okně</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Sdílet</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Poslat e-mailem</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Zavolat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..82df2a9d9b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-cy/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Agor dolen mewn tab newydd</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Agor dolen mewn tab preifat</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Agor delwedd mewn tab newydd</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Dolen llwytho i lawr</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Rhannu dolen</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Rhannu delwedd</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copïo dolen</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copi lleoliad delwedd</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Cadw delwedd</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copïo delwedd</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Cadw ffeil i’r ddyfais</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Tab newydd wedi ei agor</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Tab preifat newydd wedi ei agor</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Copïwyd dolen i’r clipfwrdd</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Newid</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Agorwch y ddolen mewn ap allanol</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Rhannu cyfeiriad e-bost</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copïo cyfeiriad e-bost</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Cyfeiriad e-bost wedi ei gopïo i’r clipfwrdd</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Ychwanegu i gyswllt</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Chwilio</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Chwilio Preifat</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Rhannu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-bost</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Galw</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..fa95f01c68
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-da/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Åbn link i nyt faneblad</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Åbn link i privat faneblad</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Åbn billede i nyt faneblad</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Hent link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Del link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Del billede</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopier link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopier billedadresse</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Gem billede</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopier billede</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Gem fil på enheden</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nyt faneblad er åbnet</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Et nyt privat faneblad blev åbnet</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link kopieret til udklipsholder</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Skift</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Åbn link i en ekstern app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Del mailadresse</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopier mailadresse</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Mailadresse kopieret til udklipsholder</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Føj til kontakt</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Søg</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privat søgning</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Del</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Send mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Ring</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..63f2e62097
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-de/strings.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Link in neuem Tab öffnen</string>
+
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Link in privatem Tab öffnen</string>
+
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Grafik in neuem Tab öffnen</string>
+
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Link herunterladen</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Link teilen</string>
+
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Grafik teilen</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Link kopieren</string>
+
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Grafikadresse kopieren</string>
+
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Grafik speichern</string>
+
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Grafik kopieren</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Datei auf Gerät speichern</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Neuer Tab geöffnet</string>
+
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Neuer privater Tab geöffnet</string>
+
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link in Zwischenablage kopiert</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Wechseln</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Link in externer App öffnen</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">E-Mail-Adresse teilen</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">E-Mail-Adresse kopieren</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-Mail-Adresse in Zwischenablage kopiert</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Zu Kontakt hinzufügen</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Suchen</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Private Suche</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Teilen</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Per E-Mail versenden</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Anrufen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..3ab0968145
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Wótkaz w nowem rejtariku wócyniś</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Wótkaz w priwatnem rejtariku wócyniś</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Wobraz w nowem rejtariku wócyniś</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Ześěgnjeński wótkaz</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Wótkaz źěliś</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Wobraz źěliś</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Wótkaz kopěrowaś</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Wobrazowu adresu kopěrowaś</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Wobraz składowaś</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Wobraz kopěrowaś</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Na rěźe składowaś</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nowy rejtarik jo se wócynił</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nowy priwatny rejtarik jo se wócynił</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Wótkaz jo se do mjazywótkłada kopěrował</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Pśešaltowaś</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Wótkaz w eksternem nałoženju wócyniś</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">E-mailowu adresu źěliś</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">E-mailowu adresu kopěrowaś</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-mailowa adresa jo se kopěrowała do mjazywótkłada</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Kontaktoju pśidaś</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Pytaś</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Priwatne pytanje</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Źěliś</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Wołaś</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..5a6f77c3ba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-el/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Άνοιγμα συνδέσμου σε νέα καρτέλα</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Άνοιγμα συνδέσμου σε ιδιωτική καρτέλα</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Άνοιγμα εικόνας σε νέα καρτέλα</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Λήψη συνδέσμου</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Κοινή χρήση συνδέσμου</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Κοινή χρήση εικόνας</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Αντιγραφή συνδέσμου</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Αντιγραφή τοποθεσίας εικόνας</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Αποθήκευση εικόνας</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Αντιγραφή εικόνας</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Αποθήκευση αρχείου στη συσκευή</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Άνοιξε νέα καρτέλα</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Άνοιξε νέα ιδιωτική καρτέλα</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ο σύνδεσμος αντιγράφτηκε στο πρόχειρο</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Εναλλαγή</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Άνοιγμα συνδέσμου σε εξωτερική εφαρμογή</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Κοινή χρήση διεύθυνσης email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Αντιγραφή διεύθυνσης email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Η διεύθυνση email αντιγράφτηκε στο πρόχειρο</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Προσθήκη σε επαφή</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Αναζήτηση</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Ιδιωτική αναζήτηση</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Κοινή χρήση</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Κλήση</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..bdd087d978
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Open link in new tab</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Open link in private tab</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Open image in new tab</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Download link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Share link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Share image</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copy link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copy image location</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Save image</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copy image</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Save file to device</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">New tab opened</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">New private tab opened</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link copied to clipboard</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Switch</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Open link in external app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Share email address</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copy email address</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Email address copied to clipboard</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Add to contact</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Search</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Private Search</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Share</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Call</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..bdd087d978
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Open link in new tab</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Open link in private tab</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Open image in new tab</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Download link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Share link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Share image</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copy link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copy image location</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Save image</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copy image</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Save file to device</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">New tab opened</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">New private tab opened</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link copied to clipboard</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Switch</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Open link in external app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Share email address</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copy email address</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Email address copied to clipboard</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Add to contact</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Search</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Private Search</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Share</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Call</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..7a19b601d0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-eo/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Malfermi ligilon en nova langeto</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Malfermi ligilon en privata langeto</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Malfermi bildon en nova langeto</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Elŝuta ligilo</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Dividi ligilon</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Dividi bildon</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopii ligilon</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopii adreson de bildo</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Konservi bildon</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopii bildon</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Konservi dosieron en la aparato</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nova langeto malfermita</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nova privata langeto malfermita</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ligilo kopiita al la tondujo</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Ŝanĝi</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Malfermi ligilon en ekstera programo</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Dividi retpoŝtan adreson</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopii retpoŝtan adreson</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Retpoŝta adreso kopiita al tondujo</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Aldoni al kontakto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Serĉi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privata serĉo</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Dividi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Retpoŝto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Voki</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..ff7a8b5fa1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir enlace en una pestaña nueva</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir enlace en una pestaña privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir la imagen en una pestaña nueva</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Descargar enlace</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir enlace</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir imagen</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar enlace</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar ubicación de la imagen</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Guardar imagen</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar imagen</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Guardar el archivo en el dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Se abrió una pestaña nueva</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Se abrió una nueva pestaña privada</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Enlace copiado al portapapeles</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Intercambiar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir enlace en aplicación externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir dirección de correo electrónico</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar dirección de correo electrónico</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Dirección de correo electrónico copiada al portapapeles</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Agregar a contactos</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Buscar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Búsqueda privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Correo electrónico</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Llamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..be8d68c8a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir enlace en una pestaña nueva</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir enlace en una pestaña privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir imagen en una pestaña nueva</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Descargar enlace</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir enlace</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir imagen</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar enlace</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar dirección de imagen</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Guardar imagen</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar imagen</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Guardar archivo en el dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nueva pestaña abierta</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nueva pestaña privada abierta</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Enlace copiado al portapapeles</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Cambiar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir enlace en una aplicación externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Dirección de email copiada al portapapeles</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Añadir a contacto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Buscar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Búsqueda privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Correo</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Llamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..5dafec4216
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir enlace en una pestaña nueva</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir enlace en una pestaña privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir imagen en una pestaña nueva</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Descargar enlace</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir enlace</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir imagen</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar enlace</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar ubicación de la imagen</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Guardar imagen</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar imagen</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Guardar el archivo en el dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Se abrió una pestaña nueva</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Se abrió una nueva pestaña privada</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Enlace copiado al portapapeles</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Cambiar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir enlace en aplicación externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir la dirección de correo electrónico</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar dirección de correo electrónico</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Dirección de correo electrónico copiada al portapapeles</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Añadir a los contactos</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Buscar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Búsqueda privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Correo electrónico</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Llamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..a26ed46bf6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir enlace en una pestaña nueva</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir enlace en una pestaña privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir imagen en una pestaña nueva</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Descargar enlace</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir enlace</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir imagen</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar enlace</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar ubicación de la imagen</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Guardar imagen</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar imagen</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Guardar el archivo en el dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nueva pestaña abierta</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nueva pestaña privada abierta</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Enlace copiado al portapapeles</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Cambiar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir enlace en aplicación externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir dirección de correo electrónico</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar dirección de correo electrónico</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Dirección de correo copiada al portapeles</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Agregar al contacto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Buscar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Búsqueda privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Correo electrónico</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Llamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..5dafec4216
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-es/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir enlace en una pestaña nueva</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir enlace en una pestaña privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir imagen en una pestaña nueva</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Descargar enlace</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir enlace</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir imagen</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar enlace</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar ubicación de la imagen</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Guardar imagen</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar imagen</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Guardar el archivo en el dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Se abrió una pestaña nueva</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Se abrió una nueva pestaña privada</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Enlace copiado al portapapeles</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Cambiar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir enlace en aplicación externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir la dirección de correo electrónico</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar dirección de correo electrónico</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Dirección de correo electrónico copiada al portapapeles</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Añadir a los contactos</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Buscar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Búsqueda privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Correo electrónico</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Llamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..7f6c38aac1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-et/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Ava link uuel kaardil</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Ava link uuel privaatsel kaardil</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Ava pilt uuel kaardil</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Laadi lingil olev sisu alla</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Jaga linki</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Jaga pilti</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopeeri link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopeeri pildi asukoht</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Salvesta pilt</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopeeri pilt</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Salvesta fail seadmesse</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Avati uus kaart</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Avati uus privaatne kaart</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link kopeeriti vahemällu</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Lülitu</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Ava link välises äpis</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Jaga e-posti aadressi</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopeeri e-posti aadress</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-posti aadress kopeeriti vahemällu</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Lisa kontaktile</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Otsi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privaatne otsing</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Jaga</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Saada e-kiri</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Helista</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..34ded4679b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-eu/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Ireki lotura fitxa berrian</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Ireki lotura fitxa pribatuan</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Ireki irudia fitxa berrian</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Deskargatu lotura</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Partekatu lotura</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Partekatu irudia</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopiatu lotura</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopiatu irudiaren helbidea</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Gorde irudia</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopiatu irudia</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Gorde fitxategia gailuan</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Fitxa berria ireki da</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Fitxa pribatu berria ireki da</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Lotura arbelean kopiatuta</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Aldatu</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Ireki lotura kanpoko aplikazioan</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Partekatu helbide elektronikoa</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopiatu helbide elektronikoa</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Helbide elektronikoa arbelean kopiatu da</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Gehitu kontaktura</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Bilatu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Bilatu modu pribatuan</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Partekatu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Helbide elektronikoa</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Deitu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..99e12ffb48
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fa/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">بازکردن پیوند در زبانه جدید</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">باز کردن پیوند در زبانه خصوصی</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">باز کردن تصویر در زبانه جدید</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">بارگیری پیوند</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">هم‌رسانی پیوند</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">هم‌رسانی تصویر</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">رونوشت از پیوند</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">رونوشت از مکان تصویر</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ذخیره تصویر</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">رونوشت از تصویر</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ذخیرهٔ پرونده در افزاره</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">زبانهٔ جدید باز شد</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">زبانهٔ خصوصی جدید باز شد</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">پیوند به تخته‌گیره رونوشت شد</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">تعویض</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">گشودن پیوند در کاره‌ای دیگر</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">هم‌رسانی نشانی رایانامه</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">رونوشت از نشانی رایانامه</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">نشانی رایانامه به تخته‌گیره رونوشت شد</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">افزودن به مخاطبین</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">جست‌وجو</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">جست‌وجوی ناشناس</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">هم‌رسانی</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">رایانامه</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">تماس</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..18fbe75b1b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ff/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Uddit jokkol e tabbere hesere</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Uddit jokkol e tabbere suuriinde</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Uddit natal e tabbere hesere</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Aawto jokkol</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Lollin jokkol</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Natto jokkol</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Natto nokku natal</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Danndu natal</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Danndu fiilde e masiŋel</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Tabbere hesere udditaama</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Tabbere suturo hesere udditaama</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Jokkorde nattaama e ɗakkitorde</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Yah toon</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Uddit jokkol e jaaɓnirgal gonngal boowal</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Ñiiɓirde iimeel nattaama e ɗakkitorde</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Yiylo</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Njiilaw Suturo</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Lollin</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Noddu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..648994b98b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fi/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Avaa linkki uuteen välilehteen</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Avaa linkki uuteen yksityiseen välilehteen</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Avaa kuva uuteen välilehteen</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Lataa linkki</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Jaa linkki</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Jaa kuva</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopioi linkki</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopioi kuvan sijainti</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Tallenna kuva</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopioi kuva</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Tallenna tiedosto laitteelle</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Uusi välilehti avattu</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Uusi yksityinen välilehti avattu</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Linkki kopioitu leikepöydälle</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Vaihda</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Avaa linkki ulkoisessa sovelluksessa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Jaa sähköpostiosoite</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopioi sähköpostiosoite</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Sähköpostiosoite kopioitu leikepöydälle</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Lisää yhteystietoon</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Haku</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Yksityinen haku</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Jaa</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Sähköposti</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Puhelu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..6d41216d61
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fr/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Ouvrir le lien dans un nouvel onglet</string>
+
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Ouvrir le lien dans un nouvel onglet privé</string>
+
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Ouvrir l’image dans un nouvel onglet</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Télécharger la cible du lien</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Partager le lien</string>
+
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Partager l’image</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copier le lien</string>
+
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copier l’adresse de l’image</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Enregistrer l’image</string>
+
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copier l’image</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Enregistrer le fichier sur l’appareil</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nouvel onglet ouvert</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nouvel onglet privé ouvert</string>
+
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Lien copié dans le presse-papiers</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Afficher</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Ouvrir le lien dans une application externe</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Partager l’adresse e-mail</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copier l’adresse e-mail</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">L’adresse e-mail a été copiée dans le presse-papiers</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Ajouter à un contact</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Rechercher</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Recherche privée</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Partager</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Envoyer un e-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Appeler</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..72f3c44469
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fur/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Vierç link intune gnove schede</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Vierç link intune schede privade</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Vierç imagjin intune gnove schede</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Discjame link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Condivît link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Condivît imagjin</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copie link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copie posizion imagjin</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Salve imagjin</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copie imagjin</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Salve file sul dispositîf</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Vierte gnove schede</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Vierte gnove schede privade</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link copiât intes notis</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Passe a</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Vierç link intune aplicazion esterne</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Condivît direzion e-mail</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copie direzion e-mail</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Direzion e-mail copiade intes notis</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Zonte a un contat</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Cîr</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Ricercje privade</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Condivît</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Mande e-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Clame</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..2f10e83864
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Keppeling iepenje yn nij ljepblêd</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Keppeling iepenje yn priveeljepblêd</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Ofbylding iepenje yn nij ljepblêd</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Keppeling downloade</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Keppeling diele</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Ofbylding diele</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Keppeling kopiearje</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Ofbyldingslokaasje kopiearje</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Ofbylding bewarje</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Ofbylding kopiearje</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Bestân op apparaat bewarje</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nij ljepblêd iepene</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nij priveeljepblêd iepene</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Koppeling nei klamboerd kopiearre</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Wikselje</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Keppeling yn eksterne app iepenje</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">E-mailadres diele</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">E-mailadres kopiearje</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-mailadres nei klamboerd kopiearre</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Oan kontakt tafoegje</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Sykje</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privee sykje</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Diele</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-maile</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Belje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..e99740765f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Oscail an nasc i gcluaisín nua</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Oscail an nasc i gcluaisín príobháideach</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Oscail an íomhá i gcluaisín nua</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Comhroinn an nasc</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Cóipeáil an nasc</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Cóipeáil suíomh na híomhá</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Sábháil an íomhá</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Osclaíodh cluaisín nua</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Osclaíodh cluaisín nua príobháideach</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Cóipeáladh an nasc go dtí an ghearrthaisce</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Malartaigh</string>
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Oscail an nasc in aip eile</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..50d1ca1ab8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gd/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Fosgail an ceangal ann an taba ùr</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Fosgail an ceangal le taba prìobhaideach</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Fosgail an dealbh ann an taba ùr</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Luchdaich a-nuas an ceangal</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Co-roinn an ceangal</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Co-roinn an dealbh</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Dèan lethbhreac dhen cheangal</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Dèan lethbhreac de sheòladh an deilbh</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Sàbhail an dealbh</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Dèan lethbhreac dhen dealbh</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Sàbhail am faidhle air uidheam</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Chaidh taba ùr fhosgladh</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Chaidh taba prìobhaideach ùr fhosgladh</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Chaidh lethbhreac dhen cheangal a chur air an stòr-bhòrd</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Geàrr leum</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Fosgail an ceangal le aplacaid eile</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Co-roinn an seòladh puist-d</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Dèan lethbhreac de sheòladh a’ phuist-d</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Chaidh lethbhreac dhen t-seòladh phuist-d a chur air an stòr-bhòrd</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Cuir ris an neach-aithne</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Lorg</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Lorg prìobhaideach</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Co-roinn</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Post-d</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Cuir fòn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..d5fed3e112
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gl/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir a ligazón nunha lapela nova</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir a ligazón nunha lapela privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir a ligazón nunha lapela nova</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Descargar ligazón</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir ligazón</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir imaxe</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar ligazón</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar a localización da imaxe</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Gardar imaxe</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar a imaxe</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Gardar o ficheiro no dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Abriuse unha lapela nova</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Abriuse unha lapela privada nova</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Copiouse a ligazón ao portapapeis</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Cambiar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir ligazón nunha aplicación externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir correo electrónico</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar correo electrónico</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Copiouse o enderezo de correo electrónico ao portapapeis</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Engadir a contacto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Buscar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Busca privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Correo electrónico</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Chamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..bcba32fe9e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gn/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Embojuruja juajuha tendayke pyahúpe</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Embojuruja juajuha tendayke ñemíme</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Embojuruja ta’ãnga tendayke pyahúpe</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Juajuha mboguejyrã</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Emoherakuã juajuha</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Emoherakuã ta’ãnga</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Emonguatia juajuha</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Emonguatia ta’ãnga rendaite</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Eñongatu ta’ãnga</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Embokuatia ta’ãnga</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Eñongatu marandurenda mba’e’okápe</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Ijuruja tendayke pyahu</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Ijuruja tendayke pyahu ñemigua</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Juajuha monguatiapyre kuatiajokohápe</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Moambue</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Embojuruja juajuha tembiporu’i okayguápe</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Emoherakuã email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Emonguatia email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Ñanduti veve kundaharape ohasáva kuatiajokohápe</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Embojuaju terarenda</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Heka</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Ñemigua jeheka</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Moherakuã</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Ñanduti veve</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Henói</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..688f6d28b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">નવા ટેબમાં લિંક ખોલો</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ખાનગી ટેબમાં લિંક ખોલો</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">નવા ટેબમાં છબી ખોલો</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">લિંક ડાઉનલોડ કરો</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">લિંક શેર કરો</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">છબી શેર કરો</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">લિંક કૉપિ કરો</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">છબી સ્થાન કૉપિ કરો</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">છબી સાચવો</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ઉપકરણ પર ફાઇલ સાચવો</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">નવી ટૅબ ખૂલી ગઇ છે</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">નવું ખાનગી ટેબ ખોલ્યું</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">ક્લિપબોર્ડ પર લિંક કૉપિ કરી</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">બદલો</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">અન્ય એપ્લિકેશનમાં લિંક ખોલો</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">સંપર્કમાં ઉમેરો</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">શોધો</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">ખાનગી શોધ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">શેર કરો</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ઈમેલ કરો</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">કૉલ કરો</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..d7e2ee8db7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">लिंक को नए टैब में खोलें</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">लिंक को निजी टैब में खोलें</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">चित्र को नए टैब में खोलें</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">लिंक डाउनलोड करें</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">लिंक साझा करें</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">चित्र साझा करें</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">लिंक कॉपी करें</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">चित्र पता कॉपी करें</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">चित्र सहेजें</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">डिवाइस पर फ़ाइल सहेजें</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">नया टैब खुल गया</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">नया निजी टैब खुल गया</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">लिंक क्लिपबोर्ड में कॉपी हो गई</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">बदलें</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">लिंक को बाहरी ऐप में खोलें</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ईमेल पता साझा करें</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ईमेल पता कॉपी करें</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ईमेल पता क्लिपबोर्ड में कॉपी हो गया</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">संपर्क में जोड़ें</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">खोजें</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">निजी खोज</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">साझा करें</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ईमेल</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">कॉल करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..4a8b863d23
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hil/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Pangitaon</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Tawagan</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..121a76a730
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hr/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Otvori poveznicu u novoj kartici</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Otvori poveznicu u privatnoj kartici</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Otvori sliku u novoj kartici</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Preuzmi poveznicu</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Dijeli poveznicu</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Podijeli sliku</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopiraj poveznicu</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopiraj lokaciju slike</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Spremi sliku</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopiraj sliku</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Spremi datoteku na uređaj</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nova kartica otvorena</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nova privatna kartica otvorena</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Poveznica kopirana u međuspremnik</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Prebaci</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Otvori poveznicu u vanjskoj aplikaciji</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Podijeli adresu e-pošte</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopiraj adresu e-pošte</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Adresa e-pošte kopirana je u međuspremnik</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Dodaj kontaktu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Traži</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privatna pretraga</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Dijeli</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-pošta</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Poziv</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..a14df59663
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Wotkaz w nowym rajtarku wočinić</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Wotkaz w priwatnym rajtarku wočinić</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Wobraz w nowym rajtarku wočinić</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Sćehnjenski wotkaz</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Wotkaz dźělić</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Wobraz dźělić</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Wotkaz kopěrować</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Wobrazowu adresu kopěrować</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Wobraz składować</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Wobraz kopěrować</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Dataju na graće składować</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nowy rajtark je so wočinił</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nowy priwatny rajtark je so wočinił</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Wotkaz je so do mjezyskłada kopěrował</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Přepinać</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Wotkaz w eksternym nałoženju wočinić</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">E-mejlowu adresu dźělić</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">E-mejlowu adresu kopěrować</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-mejlowa adresa je so do mjezyskłada kopěrowała</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Kontaktej přidać</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Pytać</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Priwatne pytanje</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Dźělić</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-mejl</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Wołać</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..135f895f5e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hu/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Hivatkozás megnyitása új lapon</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Hivatkozás megnyitása privát lapon</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Kép megnyitása új lapon</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Hivatkozás letöltése</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Hivatkozás megosztása</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Kép megosztása</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Hivatkozás másolása</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kép címének másolása</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Kép mentése</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kép másolása</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Fájl mentése az eszközre</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Új lap nyílt meg</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Új privát lap nyílt meg</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Hivatkozás vágólapra másolva</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Kapcsolja át</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">A hivatkozás megnyitása egy külső alkalmazásban</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">E-mail cím megosztása</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">E-mail cím másolása</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Az e-mail cím vágólapra másolva</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Hozzáadás a névjegyhez</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Keresés</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privát keresés</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Megosztás</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Hívás</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..608338dc61
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Բացել հղումը նոր ներդիրում</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Բացել հղումը Մասնավոր ներդիրում</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Բացել պատկերը նոր ներդիրում</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Ներբեռնել հղումը</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Տարածել հղումը</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Տարածել նկարը</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Պատճենել հղումը</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Պատճենել պատկերի հասցեն</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Պահպանել պատկերը</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Պատճենել պատկերը</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Պահպանել ֆայլը սարքում</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Նոր ներդիր է բացվել</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Նոր գաղտնի ներդիրը բացվեց</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Հղումը պատճենվել է սեղմատախտակին</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Անցնել</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Բացել հղումը արտաքին հավելվածում</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Տարածել էլ. հասցեն</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Պատճենել էլ. փոստը</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Էլ. փոստը պատճեմված է</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Ավելացնել կոնտակտներում</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Որոնում</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Մասնավոր որոնում</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Տարածել</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Էլ. փոստ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Զանգ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..4731db1b2f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ia/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Aperir le ligamine in un nove scheda</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Aperir ligamine in scheda private</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Aperir imagine in nove scheda</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Discargar le ligamine</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartir le ligamine</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartir le imagine</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar ligamine</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar le ubication del imagine</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Salvar le imagine</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar imagine</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Salvar file a in apparato</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nove scheda aperite</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nove scheda private aperite</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ligamine copiate al area de transferentia</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Commutar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Aperir le ligamine in un application externe</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartir le adresse email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar le adresse email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Adresse email copiate al area de transferentia</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Adder al contactos</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Cercar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Recerca private</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartir</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Appellar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..f2ce06941c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-in/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Buka tautan di tab baru</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Buka tautan di tab pribadi</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Buka gambar di tab baru</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Tautan unduhan</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Bagikan tautan</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Bagikan gambar</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Salin tautan</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Salin lokasi gambar</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Simpan gambar</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Salin gambar</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Simpan berkas ke perangkat</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Tab baru dibuka</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Tab pribadi baru dibuka</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Tautan disalin ke papan klip</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Ganti</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Buka tautan di aplikasi eksternal</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Bagikan alamat surel</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Salin alamat surel</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Alamat surel disalin ke papan klip</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Tambah ke kontak</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Cari</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Pencarian Pribadi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Bagikan</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Surel</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Panggil</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..b36cca0a26
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-is/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Opna tengil í nýjum flipa</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Opna tengil í huliðsflipa</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Opna mynd í nýjum flipa</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Niðurhalstengill</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Deila tengli</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Deila mynd</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Afrita tengil</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Afrita staðsetningu myndar</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Vista mynd</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Afrita mynd</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Vista skjal í tæki</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nýr flipi opnaður</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nýr huliðsflipi opnaður</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Tengill afritaður á klippispjald</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Skipta</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Opna tengil með ytra smáforriti</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Deila tölvupóstfangi</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Afrita tölvupóstfang</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Tölvupóstfang afritað á klippispjald</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Bæta við tengilið</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Leita</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Einkaleit</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Deila</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Tölvupóstur</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Hringja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..8c9e243176
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-it/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Apri link in nuova scheda</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Apri in scheda anonima</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Apri immagine in nuova finestra</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Scarica link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Condividi link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Condividi immagine</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copia link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copia indirizzo immagine</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Salva immagine</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copia immagine</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Salva file sul dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Aperta nuova scheda</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Aperta nuova scheda anonima</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link copiato negli appunti</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Passa a</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Apri link in un’app esterna</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Condividi indirizzo email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copia indirizzo email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Indirizzo email copiato negli appunti</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Aggiungi a un contatto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Cerca</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Ricerca anonima</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Condividi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Invia email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Chiama</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..36705f2d42
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-iw/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">פתיחת קישור בלשונית חדשה</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">פתיחת קישור בלשונית פרטית</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">פתיחת תמונה בלשונית חדשה</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">הורדת קישור</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">שיתוף קישור</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">שיתוף תמונה</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">העתקת קישור</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">העתקת מיקום תמונה</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">שמירת תמונה</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">העתקת תמונה</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">שמירת קובץ למכשיר</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">לשונית חדשה נפתחה</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">לשונית פרטית חדשה נפתחה</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">הקישור הועתק ללוח</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">מעבר</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">פתיחת קישור ביישומון חיצוני</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">שיתוף כתובת דוא״ל</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">העתקת כתובת דוא״ל</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">כתובת הדוא״ל הועתקה ללוח</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">הוספת איש קשר</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">חיפוש</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">חיפוש פרטי</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">שיתוף</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">דוא״ל</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">חיוג</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..e549b76d3b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ja/strings.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">リンクを新しいタブで開く</string>
+
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">リンクをプライベートタブで開く</string>
+
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">画像を新しいタブで開く</string>
+
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">リンク先をダウンロード</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">リンクを共有</string>
+
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">画像を共有</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">リンクをコピー</string>
+
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">画像の URL をコピー</string>
+
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">画像を保存</string>
+
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">画像をコピー</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ファイルを端末に保存</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">新しいタブを開きました</string>
+
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">プライベートタブを開きました</string>
+
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">リンクをクリップボードにコピーしました</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">切り替え</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">リンクを外部アプリで開く</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">メールアドレスを共有</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">メールアドレスをコピー</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">メールアドレスをクリップボードにコピーしました</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">連絡先に追加</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">検索</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">プライベート検索</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">共有</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">メール</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">通話</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..c660f2a49a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ka/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">ბმულის გახსნა ახალ ჩანართში</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ბმულის პირად ჩანართში გახსნა</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">სურათის ახალ ჩანართში გახსნა</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ბმულის ჩამოტვირთვა</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">ბმულის გაზიარება</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">სურათის გაზიარება</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">ბმულის ასლი</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">სურათის მისამართის ასლი</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">სურათის შენახვა</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">სურათის ასლი</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">შენახვა მოწყობილობაში</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">ახალი ჩანართი გაიხსნა</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">ახალი პირადი ჩანართი გაიხსნა</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">ბმულის ასლი აღებულია</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">გადასვლა</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">ბმულის გარეშე პროგრამით გახსნა</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ელფოსტის გაზიარება</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ელფოსტის მისამართის ასლი</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ელფოსტის ასლი აღებულია</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">პირის დამატება</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ძიება</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">პირადი ძიება</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">გაზიარება</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ელფოსტა</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">დარეკვა</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..e9a5035982
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Siltemeni jańa bette ashıw</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Siltemeni jeke bette ashıw</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Súwretti taza bette ashıw</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Siltemeni júklep alıw</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Siltemeni bólisiw</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Súwretti bólisiw</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Siltemeni kóshirip alıw</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Súwrettıń siltemesin kóshirip alıw</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Súwretti saqlaw</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Súwretti kóshirip alıw</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Fayldı qurılmaǵa saqlaw</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Jańa bet ashıldı</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Jańa jeke bet ashıldı</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Silteme almasıw buferine kóshirip alındı</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Ótiw</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Siltemeni sırtqı baǵdarlamada ashıw</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Elektron pochta mánzilin bólisiw</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Elektron pochta mánzilin kóshirip alıw</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Elektron pochta mánzili almasıw buferine kóshirip alındı</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Kontaktke qosıw</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Izlew</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Jeke izlew</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Bólisiw</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Qońıraw qılıw</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..74cb77ae12
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kab/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Ldi aseɣwen deg iccer amaynut</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Ldi aseɣwen deg iccer uslig</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Ldi-t tugna deg yiccer amaynut</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Aseɣwen n usader</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Bḍu aseɣwen</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Bḍu tugna</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Nɣel aseɣwen</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Nɣel tansa n tugna</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Sekles tugna</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Nɣel tugna</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Sekles afaylu ɣer ibenk</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Iccer amaynut yeldi</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Iccer uslig amaynut yelldi</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Aseɣen yenɣel ɣef aus</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Nṭew</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Ldi aseɣwen deg usnas azɣaray</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Bḍu tansa imayl</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Nɣel tansa imayl</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Tansa imayl tettwanɣel ɣer afus</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Rnu ɣer unermis</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Nadi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Anadi uslig</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Bḍu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Imayl</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Siwel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..5d52bb2ba6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kk/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Сілтемені жаңа бетте ашу</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Сілтемені жекелік бетінде ашу</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Суретті жаңа бетте ашу</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Сілтемені жүктеп алу</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Сілтемемен бөлісу</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Суретпен бөлісу</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Сілтемені көшіріп алу</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Суреттің сілтемесін көшіру</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Суретті сақтау</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Суретті көшіру</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Файлды құрылғыға сақтау</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Жаңа бет ашылды</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Жаңа жекелік беті ашылды</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Сілтеме алмасу буферіне көшірілді</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Ауысу</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Сілтемені сыртқы қолданбада ашу</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Эл. пошта адресімен бөлісу</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Эл. пошта адресін көшіріп алу</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Эл. пошта адресі алмасу буферіне көшірілді</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Контакттарға қосу</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Іздеу</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Жеке іздеу</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Бөлісу</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Эл. пошта</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Қоңырау</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..61fc1a07b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Girêdanê di hilpekîna nû de veke</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Girêdanê di hilpekîna veşartî de veke</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Wêneyê di hilpekîna nû de veke</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Girêdana daxistinê</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Girêdanê parve bike</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Wêneyê parve bike</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Girêdanê kopî bike</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Cîgeha wêneyê kopî bike</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Wêneyê tomar bike</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Wêneyî kopî bike</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Dosyeyê li cîhazê tomar bike</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Hilpekîna nû vebû</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Hilpekîna veşartî ya nû vebû</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Girêdan li panoyê hate kopîkirin</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Derbas bibe</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Girêdanê di sepaneke din de veke</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Navnîşana emaîlê parve bike</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Navnîşana emaîlê kopî bike</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Navnîşana emaîlê li panoyê hate kopîkirin</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Tevlî kesî bike</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Lê bigere</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Lêgerîna veşartî</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Parve bike</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Emaîl</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Bigere</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..d757feff46
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-kn/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">ಹೊಸ ಟ್ಯಾಬ್‌ನಲ್ಲಿ ಕೊಂಡಿಯನ್ನು ತೆರೆ</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ಕೊಂಡಿಯನ್ನು ಖಾಸಗಿ ಹಾಳೆಯಲ್ಲಿ ತೆರೆ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">ಹೊಸ ಟ್ಯಾಬ್‌ನಲ್ಲಿ ಚಿತ್ರವನ್ನು ತೆರೆ</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ಡೌನ್‌ಲೋಡ್ ಕೊಂಡಿ</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">ಕೊಂಡಿಯನ್ನು ಹಂಚಿಕೊಳ್ಳಿ</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಿ</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">ಕೊಂಡಿ ನಕಲಿಸು‍</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">ಚಿತ್ರದ ತಾಣವನ್ನು ಕಾಪಿ ಮಾಡು</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ಚಿತ್ರವನ್ನು ಉಳಿಸು</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ಫೈಲ್ ಅನ್ನು ಸಾಧನಕ್ಕೆ ಉಳಿಸಿ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">ಹೊಸ ಹಾಳೆ ತೆರೆಯಲಾಗಿದೆ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">ಹೊಸ ಖಾಸಗಿ ಹಾಳೆ ತೆಗೆಯಲಾಗಿದೆ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">ಲಿಂಕ್ ಅನ್ನು ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಲಾಗಿದೆ</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ಬದಲಾಯಿಸು</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">ಬಾಹ್ಯ ಅಪ್ಲಿಕೇಶನ್‌ನಲ್ಲಿ ಲಿಂಕ್ ತೆರೆಯಿರಿ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ಹುಡುಕು</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">ಖಾಸಗಿ ಹುಡುಕಾಟ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">ಹಂಚು</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ಇಮೇಲ್</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">ಕರೆ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..07f7eb291b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ko/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">링크를 새 탭에서 열기</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">사생활 보호 탭에 링크 열기</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">이미지를 새 탭에서 열기</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">다운로드 링크</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">링크 공유</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">이미지 공유</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">링크 복사</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">이미지 주소 복사</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">이미지 저장</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">이미지 복사</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">파일을 기기에 저장</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">새 탭 열림</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">새 사생활 보호 탭 열림</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">링크가 클립보드에 복사됨</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">전환</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">외부 앱에서 링크 열기</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">이메일 주소 공유</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">이메일 주소 복사</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">이메일 주소가 클립보드에 복사됨</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">연락처에 추가</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">검색</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">사생활 보호 검색</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">공유</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">이메일</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">통화</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..a511fa71a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lij/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Arvi link in atro feuggio</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Arvi link in feuggio privou</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Arvi inmagine in atro feuggio</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Scarega link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Condividdi link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Condividdi inmagine</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Còpia link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Còpia indirisso inmagine</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Sarva inmagine</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Sarva schedaio into dispoxitivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Neuvo feuggio averto</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Neuvo feuggio privou averto</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link copiou in sci aponti</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Passa a</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Arvi link in app esterna</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Condividdi</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..49067eabb5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lo/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">ເປີດລີ້ງໃນແທັບໃຫມ່</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ເປີດລີ້ງໃນແທັບສ່ວນຕົວ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">ເປີດຮູບພາບໃນແທັບໃຫມ່</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ລິ້ງດາວໂຫລດ</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">ແບ່ງປັນລີ້ງ</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ແບ່ງປັນຮູບພາບ</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">ສຳເນົາລີ້ງ</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">ສຳເນົາທີ່ຢູ່ຮູບພາບ</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ບັນທຶກຮູບພາບ</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">ສຳເນົາຮູບ</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ບັນທຶກເອກະສານເຂົ້າໃນອຸປະກອນ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">ເປີດແທັບໃຫມ່ແລ້ວ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">ເປີດແທັບສ່ວນຕົວໃຫມ່ແລ້ວ </string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">ສຳເນົາລີ້ງໄປໄວ້ໃນຄຣິບບອດແລ້ວ</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ສັບປ່ຽນ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">ເປີດລິ້ງນີ້ໃນ app ພາຍນອກ</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ແບ່ງປັນທີ່ຢູ່ອີເມລ</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ສຳເນົາທີ່ຢູ່ອີເມລ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ທີ່ຢູ່ອີເມລໄດ້ຖືກສຳເນົາໄປໄວ້ໃນຄຣິບບອດແລ້ວ</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">ເພີ່ມເຂົ້າໄປໃນລາຍຊື່ຕິດຕໍ່</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ຄົ້ນຫາ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">ຄົ້ນຫາແບບສ່ວນຕົວ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">ແບ່ງປັນ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ອີເມລ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">ໂທ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..57f86d0d57
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-lt/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Atverti naujoje kortelėje</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Atverti privačiojoje kortelėje</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Atverti paveikslą naujoje kortelėje</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Atsiųsti saitą</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Dalintis saitu</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Dalintis paveikslu</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopijuoti saitą</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopijuoti paveikslo adresą</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Įrašyti paveikslą</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Įrašyti failą į įrenginį</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Atverta nauja kortelė</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Atverta nauja privačioji kortelė</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Saitas nukopijuotas į iškarpinę</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Pereiti</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Atverti saitą išorinėje programoje?</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Dalintis el. pašto adresu</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopijuoti el. pašto adresą</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">El. pašto adresas nukopijuotas į iškarpinę</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Pridėti prie adresato</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Ieškoti</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privačioji paieška</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Dalintis</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Siųsti el. laišką</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Skambinti</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..cf671dd295
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-mix/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Kuna ña kunu kuncheu nu inka xikua</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Kuna ña kunu kuncheu nu inka xikua se´e</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Kuna tutu ndatavana nu inka xikua</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Ndatava link</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Stucha</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Korreo</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Kana</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..51434f5290
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ml/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">കണ്ണി പുതിയ റ്റാബില്‍ തുറക്കുക</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">കണ്ണി സ്വകാര്യ ടാബിൽ തുറക്കുക</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">ചിത്രം പുതിയ ടാബിൽ തുറക്കുക</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">കണ്ണി ഡൗൺലോഡ് ചെയ്യുക</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">കണ്ണി പങ്കിടുക</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ചിത്രം പങ്കിടുക</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">കണ്ണി പകർത്തുക</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">ചിത്രത്തിന്റെ സ്ഥാനം പകർത്തുക</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ചിത്രം സൂക്ഷിക്കുക</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ഉപകരണത്തിലേക്ക് ഫയൽ സൂക്ഷിക്കുക</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">പുതിയ ടാബ് തുറന്നു</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">പുതിയ സ്വകാര്യ ടാബ് തുറന്നു</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">കണ്ണി ക്ലിപ്പ്ബോർഡിലേക്ക് പകർത്തിയിരിക്കുന്നു</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">മാറുക</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">ബാഹ്യ ആപ്പിൽ കണ്ണി തുറക്കുക</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">പങ്കിടുക</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..a944fb2483
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-mr/strings.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">दुवा नवीन टॅबमध्ये उघडा</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">दुवा खाजगी टॅबमध्ये उघडा</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">नवीन टॅबमध्ये प्रतिमा उघडा</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">डाउनलोड दुवा</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">दुवा शेअर करा</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">प्रतिमा सामयिक करा</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">दुव्याची प्रत बनवा</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">प्रतिमा ठिकाणाची प्रत बनवा</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">प्रतिमा साठवा</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">डिव्हाइसमध्ये फाईल जतन करा</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">नवीन टॅब उघडला</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">नवीन खाजगी टॅब उघडला</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">क्लिपबोर्डवर दुव्याची प्रत बनवली</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">बदला</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">बाहेरील अॅपमध्ये दुवा उघडा</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ईमेल पत्ता शेअर करा</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ईमेल पत्त्याची प्रत बनवा</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">संपर्कांत समावेश करा</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">शोधा</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">खाजगी शोध</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">शेअर करा</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ईमेल</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">कॉल करा</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..297550261d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-my/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">လင့်ခ်ကို တပ်ဗ်အသစ်တွင် ဖွင့်ပါ</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">လင်ခ့်ကို ကိုယ်ပိုင်သီးသန့်သုံးတပ်ဗ်တွင်ဖွင့်ပါ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">တပ်ဗ်အသစ်တွင် ပုံဖွင့်ပါ</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ဒေါင်းလုဒ်လင်ခ့်</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">လင်ခ့်အားမျှဝေပါ</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ပုံ မျှဝေရန်</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">လင့်ခ်ကူးပါ</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">ပုံတည်နေရာကူးယူပါ</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ရုပ်ပုံ သိမ်းဆည်းပါ</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ဖိုင်ကိုသင့်ထဲကိုသိမ်းပါ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">တပ်ဗ်အသစ် ဖွင့်ထားသည်</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">သီးသန့်သုံး တပ်ဗ်တစ်ခုဖွင့်ထားပြီး</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">လင့်ခ်ကို ကလစ်ဘုတ်သို့ ကူးယူပြီး</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ပြောင်းပါ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">ပြင်ပအက်ပ်တွင်လင့်ခ်ဖွင့်ပါ</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">အီးမေလ်းလိပ်စာ မျှဝေမည်</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">အီးမေလ်းအား ကူးမည်</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ကလစ်ဘုတ်သို့ အီးမေလ်းကိုကူးထားပြီး</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">အဆက်အသွယ်ထဲသို့ထည့်ပါ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ရှာရန်</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">သီးသန့် ရှာရန်</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">မျှ​ဝေရန်</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">အီးမေလ်း</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">ခေါ်ရန်</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..ecd8377ef7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Åpne lenke i ny fane</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Åpne lenke i privat fane</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Åpne bilde i ny fane</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Last ned lenke</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Del lenke</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Del bilde</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopier lenke</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopier bildeplassering</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Lagre bilde</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopier bilde</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Lagre fil på enheten</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Ny fane åpnet</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Ny privat fane åpnet</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Lenke kopiert til utklippstavlen</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Bytt</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Åpne lenken i ekstern app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Del e-postadresse</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopier e-postadresse</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-postadresse kopiert til utklippstavlen</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Legg til i kontakt</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Søk</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privat søk</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Del</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-post</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Ring</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..82faabe7b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">लिङ्कलाई एउटा नयाँ ट्याबमा खोल्नुहोस्</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">निजी ट्याबमा लिङ्क खोल्नुहोस्</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">तस्वीरलाई नयाँ ट्याबमा खोल्नुहोस्</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">डाउनलोड लिङ्क</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">सेयर लिङ्क</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">सेयर तस्वीर</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">लिङ्कलाई कपि गर्नुहोस्</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">तस्वीरको स्थान कपि गर्नुहोस्</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">तस्वीर सेभ गर्नुहोस्</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">उपकरणमा फाइल सेभ गर्नुहोस्</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">नयाँ ट्याब खोलिएको छ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">नयाँ निजी ट्याब खोलिएको छ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">क्लिपबोर्डमा लिङ्क कपि गरिएको छ</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">स्वीच</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">बाहिरी एपमा लिङ्क खोल्नुहोस्</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">इमेल ठेगाना सेयर गर्नुहोस्</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">इमेल ठेगाना कपि गर्नुहोस्</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">क्लिपबोर्डमा इमेल ठेगाना कपि गरियो</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">सम्पर्कमा थप्नुहोस्</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">खोज्नुहोस्</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">निजी खोज गर्नुहोस्</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">सेयर गर्नुहोस्</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">इमेल</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">कल गर्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..1c8f33e11b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nl/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Koppeling openen in nieuw tabblad</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Koppeling openen in privétabblad</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Afbeelding openen in nieuw tabblad</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Koppeling downloaden</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Koppeling delen</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Afbeelding delen</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Koppeling kopiëren</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Afbeeldingslocatie kopiëren</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Afbeelding opslaan</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Afbeelding kopiëren</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Bestand op apparaat opslaan</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nieuw tabblad geopend</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nieuw privétabblad geopend</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Koppeling naar klembord gekopieerd</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Wisselen</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Koppeling in externe app openen</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">E-mailadres delen</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">E-mailadres kopiëren</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-mailadres naar klembord gekopieerd</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Aan contact toevoegen</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Zoeken</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privé zoeken</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Delen</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-mailen</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Bellen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..0491aa129c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Opne lenke i ny fane</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Opne lenke i privat fane</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Opne bilde i ny fane</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Last ned lenke</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Del lenke</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Del bildet</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopier lenke</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopier bildeplassering</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Lagre bilde</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopier bilde</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Lagre fil på eininga</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Ny fane opna</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Ny privat fane opna</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Lenke kopiert til utklippstavla</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Byt</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Opne lenka i ekstern app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Del e-postadresse</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopier e-postadresse</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-postadresse kopiert til utklippstavla</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Legg til i kontakt</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Søk</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privat søk</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Del</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-post</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Ring</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..936b64acc5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-oc/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Dobrir dins un onglet novèl</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Dobrir en navigacion privada</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Dobrir l’imatge dins un onglet novèl</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Telecargar lo ligam</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Partejar lo ligam</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Partejar l’imatge</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar lo ligam</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar l’adreça de l’imatge</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Enregistrar l’imatge</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar l’imatge</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Enregistrar lo fichièr sul periferic</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Onglet novèl dobèrt</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Onglet novèl privat dobèrt</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Copiat dins lo quichapapièrs</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Bascular</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Dobrir lo ligam dins una aplicacion extèrna</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Partejar l’adreça electronica</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar l’adreça electronica</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Adreça copiada al quichapapièrs</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Apondre als contactes</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Cercar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Recèrca privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Partejar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Adreça electronica</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Sonar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..794cf992e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-or/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">ନୂଆ ଟ୍ୟାବରେ ଲିଙ୍କ ଖୋଲନ୍ତୁ</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ଲିଙ୍କକୁ ବ୍ୟକ୍ତିଗତ ଟ୍ୟାବରେ ଖୋଲନ୍ତୁ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">ନୂଆ ଟ୍ୟାବରେ ଛବି ଖୋଲନ୍ତୁ</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ଲିଙ୍କ ଡାଉନଲୋଡ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">ଲିଙ୍କ ବିତରଣ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ପ୍ରତିଛବିକୁ ସହଭାଗ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">ଲିଙ୍କ ନକଲ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">ପ୍ରତିଛବି ଅବସ୍ଥିତି ନକଲ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ପ୍ରତିଛବି ସଂରକ୍ଷଣ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ଫାଇଲକୁ ଉପକରଣରେ ସଞ୍ଚୟ କରନ୍ତୁ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">ନୂଆ ଟ୍ୟାବ ଖୋଲିଛି</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">ନୂଆ ବ୍ୟକ୍ତିଗତ ଟ୍ୟାବ ଖୋଲାଗଲା</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">କ୍ଲିପବୋର୍ଡରେ ନକଲ କରାହୋଇଛି</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ପରିବର୍ତ୍ତନ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ଇମେଲ ଠିକଣାକୁ ସହଭାଗ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ଇମେଲ ଠିକଣାକୁ ନକଲ କରନ୍ତୁ</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">ସମ୍ପର୍କ ତାଲିକାରେ ଯୋଗ କରନ୍ତୁ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ଖୋଜନ୍ତୁ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">ବିତରଣ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ଇମେଲ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">କଲ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..ac1eaa6277
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">ਨਵੀਂ ਟੈਬ ‘ਚ ਲਿੰਕ ਖੋਲ੍ਹੋ</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ਲਿੰਕ ਨਿੱਜੀ ਟੈਬ ‘ਚ ਖੋਲ੍ਹੋ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">ਚਿੱਤਰ ਨਵੀਂ ਟੈਬ ‘ਚ ਖੋਲ੍ਹੋ</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ਡਾਊਨਲੋਡ ਲਿੰਕ</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">ਲਿੰਕ ਸਾਂਝਾ ਕਰੋ</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ਚਿੱਤਰ ਸਾਂਝਾ ਕਰੋ</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">ਲਿੰਕ ਕਾਪੀ ਕਰੋ</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">ਚਿੱਤਰ ਟਿਕਾਣਾ ਕਾਪੀ ਕਰੋ</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ਚਿੱਤਰ ਸੰਭਾਲੋ</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">ਚਿੱਤਰ ਨੂੰ ਕਾਪੀ ਕਰੋ</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ਫਾਇਲ ਡਿਵਾਈਸ ਉੱਤੇ ਸੰਭਾਲੋ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">ਨਵੀਂ ਟੈਬ ਖੋਲ੍ਹੀ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">ਨਵੀਂ ਪ੍ਰਾਈਵੈਟ ਟੈਬ ਖੋਲ੍ਹੀ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">ਲਿੰਕ ਕਲਿੱਪਬੋਰਡ ਲਈ ਕਾਪੀ ਕੀਤਾ</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ਬਦਲੋ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">ਬਾਹਰੀ ਐਪ ਵਿੱਚ ਲਿੰਕ ਖੋਲ੍ਹੋ</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ਈਮੇਲ ਸਿਰਨਾਵੇ ਨੂੰ ਸਾਂਝਾ ਕਰੋ</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ਈਮੇਲ ਸਿਰਨਾਵੇ ਨੂੰ ਕਾਪੀ ਕਰੋ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ਈਮੇਲ ਸਿਰਨਾਵਾਂ ਕਲਿੱਪਬੋਰਡ ਵਿੱਚ ਕਾਪੀ ਕੀਤਾ</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">ਸੰਪਰਕ ਵਿੱਚ ਜੋੜੋ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ਖੋਜੋ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">ਨਿੱਜੀ ਖੋਜ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">ਸਾਂਝਾ ਕਰੋ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ਈਮੇਲ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">ਕਾਲ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..f732b4ba98
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">نویں ٹیب چ کھولھو</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">نجی ٹیب چ کھولھو</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">نویں ٹیب چ تصویر کھولھو</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ایس پتے توں ڈاؤں‌لوڈ کرو</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">پتہ سانجھا کرو</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">تصویر سانجھا کرو</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">پتہ کاپی کرو</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">تصویر دا پتہ کاپی کرو</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">تصویر ڈاؤں‌لوڈ کرو</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">تصویر کاپی کرو</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">فائل ڈاؤں‌لوڈ کرو</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">نویں ٹیب کھولھی گئی</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">نجی ٹیب کھولھی گئی</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">پتہ کاپی کیتا گیا</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ہورناں نوں جاؤ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">باہری ایپ چ پتے نوں جاؤ</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ای‌میل دا پتہ سانجھا کرو</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ای‌میل دا پتہ کاپی کرو</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ای‌میل دا پتہ کاپی کیتا گیا</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">رابطے وچ شامل کرو</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">کھوجو</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">نجی کھوجو</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">سانجھا کرو</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ای‌میل بھیجو</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">فون کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..637a77f4de
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pl/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Otwórz odnośnik w nowej karcie</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Otwórz odnośnik w prywatnej karcie</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Otwórz obraz w nowej karcie</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Pobierz odnośnik</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Udostępnij odnośnik</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Udostępnij obraz</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopiuj odnośnik</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopiuj adres obrazu</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Zapisz obraz</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopiuj obraz</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Zapisz plik na urządzeniu</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Otwarto nową kartę</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Otwarto nową kartę prywatną</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Skopiowano odnośnik do schowka</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Przejdź</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Otwórz odnośnik w zewnętrznej aplikacji</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Udostępnij adres e-mail</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopiuj adres e-mail</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Skopiowano adres e-mail do schowka</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Dodaj do kontaktu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Wyszukaj</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Wyszukaj prywatnie</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Udostępnij</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Wyślij e-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Zadzwoń</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..43d49e945c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir link em nova aba</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir em aba privativa</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir imagem em nova aba</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Baixar link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Compartilhar link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Compartilhar imagem</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar endereço da imagem</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Salvar imagem</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar imagem</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Salvar arquivo no dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nova aba aberta</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nova aba privativa aberta</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link copiado para área de transferência</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Mostrar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir link em app externo</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Compartilhar endereço de email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar endereço de email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Endereço de email copiado para área de transferência</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Adicionar em um contato</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Pesquisar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Pesquisa privativa</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Compartilhar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Chamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..4f2d3fefde
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Abrir ligação num novo separador</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Abrir ligação num separador privado</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Abrir imagem num novo separador</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Transferir ligação</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Partilhar ligação</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Partilhar imagem</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar ligação</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar endereço da imagem</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Guardar imagem</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar imagem</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Guardar ficheiro no dispositivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Novo separador aberto</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Novo separador privado aberto</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ligação copiada para a área de transferência</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Alternar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Abrir ligação numa aplicação externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Partilhar endereço de e-mail</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar endereço de e-mail</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Endereço de e-mail copiado para a área de transferência</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Adicionar ao contacto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Procurar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Pesquisa privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Partilhar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Chamar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..d3eda1fce9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-rm/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Avrir la colliaziun en in nov tab</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Avrir la colliaziun en in nov tab privat</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Avrir la grafica en in nov tab</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Telechargiar la colliaziun</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Cundivider la colliaziun</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Cundivider la grafica</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiar la colliaziun</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiar l\'adressa da la grafica</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Memorisar la grafica</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copiar il maletg</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Memorisar la datoteca sin l\'apparat</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Avert in nov tab</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Avert in nov tab privat</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Copià la colliaziun en l\'archiv provisoric</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Midar</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Avrir la colliaziun en ina app externa</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Cundivider l\'adressa d\'e-mail</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiar l\'adressa d\'e-mail</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Copià l\'adressa d\'e-mail en l\'archiv provisoric</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Agiuntar ad in contact</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Tschertgar</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Tschertga privata</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Cundivider</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Telefonar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..7ad904493d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ro/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Deschide linkul într-o filă nouă</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Deschide linkul într-o filă privată</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Deschide imaginea într-o filă nouă</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Descarcă linkul</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Partajează linkul</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Partajează imaginea</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copiază linkul</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copiază locația imaginii</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Salvează imaginea</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Salvează fișierul pe dispozitiv</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">S-a deschis o filă nouă</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Filă privată nouă deschisă</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Linkul a fost copiat în clipboard</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Comută</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Deschide linkul într-o aplicație externă</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Partajează adresa de e-mail</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copiază adresa de e-mail</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Adresă de e-mail copiată în clipboard</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Adaugă la contact</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Căutare</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Căutare privată</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Partajează</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Sună</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..dcee952da3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ru/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Открыть ссылку в новой вкладке</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Открыть в приватной вкладке</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Открыть изображение в новой вкладке</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Загрузить по ссылке</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Поделиться ссылкой</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Поделиться изображением</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Копировать ссылку</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Копировать ссылку на изображение</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Сохранить изображение</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Копировать изображение</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Сохранить файл на устройстве</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Открыта новая вкладка</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Открыта новая приватная вкладка</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ссылка скопирована в буфер обмена</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Перейти</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Открыть ссылку в другом приложении</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Поделиться адресом эл. почты</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Копировать адрес эл. почты</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Адрес эл. почты скопирован в буфер обмена</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Добавить в контакты</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Поиск</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Приватный поиск</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Поделиться</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Эл. почта</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Позвонить</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..1e102cf7fb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sat/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">ᱱᱟᱶᱟ ᱴᱮᱵᱽ ᱨᱮ ᱞᱤᱝᱠ ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ᱯᱨᱭᱣᱮᱴ ᱴᱮᱵᱽ ᱨᱮ ᱞᱤᱝᱠ ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">ᱱᱟᱶᱟ ᱴᱮᱵᱽ ᱨᱮ ᱪᱤᱛᱟᱹᱨ ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ᱰᱟᱣᱱᱞᱚᱰ ᱞᱤᱝᱠ</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">ᱞᱤᱝᱠ ᱦᱟᱹᱴᱤᱧ ᱢᱮ</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">ᱪᱤᱛᱟᱹᱨ ᱦᱟᱹᱴᱤᱧ ᱢᱮ</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">ᱞᱤᱝᱠ ᱱᱚᱠᱚᱞ ᱢᱮ</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">ᱪᱤᱛᱟᱹᱨ ᱨᱮᱭᱟᱜ ᱡᱟᱭᱜᱟ ᱱᱚᱠᱚᱞ ᱢᱮ</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">ᱪᱤᱛᱟᱹᱨ ᱥᱟᱺᱪᱟᱣ ᱢᱮ</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">ᱪᱤᱛᱟᱹᱨ ᱱᱚᱠᱚᱞ ᱢᱮ</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ᱨᱮᱫ ᱥᱟᱫᱷᱚᱱ ᱨᱮ ᱥᱟᱺᱪᱟᱣ ᱢᱮ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">ᱱᱟᱶᱟ ᱴᱮᱵᱽ ᱠᱷᱩᱞᱟᱹᱭ ᱮᱱᱟ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">ᱱᱟᱶᱟ ᱱᱤᱡᱚᱨᱟᱜ ᱴᱮᱵᱽ ᱠᱷᱩᱞᱟᱹᱭ ᱮᱱᱟ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">ᱞᱤᱝᱠ ᱨᱮᱴᱚᱵᱼᱵᱚᱰ ᱨᱮ ᱱᱚᱠᱚᱞᱮᱱᱟ</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ᱵᱚᱫᱚᱞ ᱢᱮ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">ᱞᱤᱝᱠ ᱵᱟᱦᱨᱮ ᱮᱯ ᱨᱮ ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ᱤᱼᱢᱮᱞ ᱴᱷᱤᱠᱬᱟᱹ ᱠᱚ ᱦᱟᱹᱴᱤᱧ ᱢᱮ</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ᱤᱼᱢᱮᱞ ᱴᱷᱤᱠᱬᱟᱹ ᱱᱚᱠᱚᱞ ᱢᱮ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ᱤᱼᱢᱮᱞ ᱴᱷᱤᱠᱬᱟᱹ ᱨᱮᱴᱚᱵᱼᱵᱚᱰ ᱨᱮ ᱱᱚᱠᱚᱞᱮᱱᱟ</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">ᱥᱚᱢᱯᱚᱨᱠ ᱨᱮ ᱥᱮᱞᱮᱫᱽ ᱢᱮ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ᱥᱮᱸᱫᱽᱨᱟ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">ᱱᱤᱡᱮᱨᱟᱜ ᱥᱮᱸᱫᱽᱨᱟ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">ᱦᱟᱹᱴᱤᱧ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ᱤᱼᱢᱮᱞ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">ᱠᱚᱞ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..580e3a1d74
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sc/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Aberi su ligòngiu in un’ischeda noa</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Aberi su ligòngiu in un’ischeda privada noa</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Aberi s’immàgine in un’ischeda noa</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Iscàrriga su ligòngiu</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Cumpartzi su ligòngiu</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Cumpartzi s’immàgine</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Còpia su ligòngiu</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Còpia sa positzione de s’immàgine</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Sarva s’immàgine</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Còpia s’immàgine</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Sarva s’immàgine in su dispositivu</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Ischeda noa aberta</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Ischeda privada noa aberta</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ligòngiu copiadu in punta de billete</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Bae</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Aberi su ligòngiu in s’aplicatzione esterna</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Cumpartzi s’indiritzu de posta eletrònica</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Còpia s’indiritzu</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Indiritzu copiadu in punta de billete</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Agiunghe a su cuntatu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Chirca</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Chirca privada</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Cumpartzi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Indiritzu de posta eletrònica</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Muti</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..3c2deaffb8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-si/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">නව පටිත්තක සබැඳිය අරින්න</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">පෞද්. පටිත්තක සබැඳිය අරින්න</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">නව පටිත්තකින් රූපය බලන්න</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">බාගැනීමේ සබැඳිය</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">සබැඳිය බෙදාගන්න</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">රූපය බෙදාගන්න</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">සබැඳියේ පිටපතක්</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">රූපයේ ස්ථානයෙහි පිටපතක්</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">රූපය සුරකින්න</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">රූපයේ පිටපතක්</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">උපාංගයට ගොනුව සුරකින්න</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">නව පටිත්තක් විවෘතයි</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">නව පෞද්. පටිත්තක් විවෘතයි</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">සබැඳිය පසුරු පුවරුවට පිටපත් විය</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">මාරු වන්න</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">බාහිර යෙදුමකින් සබැඳිය අරින්න</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">වි-තැපෑල බෙදාගන්න</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">වි-තැපෑලෙහි පිටපතක්</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">වි-තැපෑල පසුරු පුවරුවට පිටපත් විය</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">සබඳතාවයට යොදන්න</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">සොයන්න</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">පෞද්. සෙවුම</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">බෙදාගන්න</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">වි-තැපෑල</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">අමතන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..3b20611d30
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sk/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Otvoriť odkaz na novej karte</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Otvoriť odkaz na súkromnej karte</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Otvoriť obrázok na novej karte</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Stiahnuť odkaz</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Zdieľať odkaz</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Zdieľať obrázok</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopírovať odkaz</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopírovať adresu obrázka</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Uložiť obrázok</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopírovať obrázok</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Uložiť súbor do zariadenia</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Bola otvorená nová karta</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Bola otvorená nová súkromná karta</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Odkaz bol skopírovaný do schránky</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Prepnúť</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Otvoriť odkaz v inej aplikácii</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Zdieľať e‑mailovú adresu</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopírovať e‑mailovú adresu</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E‑mailová adresa bola skopírovaná do schránky</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Pridať medzi kontakty</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Hľadať</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Súkromne vyhľadať</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Zdieľať</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Poslať e‑mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Zavolať</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..dd26c0311a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-skr/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">نویں ٹیب وچ لنک کھولو</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">نجی ٹیب وچ لنک کھولو</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">نویں ٹیب وچ تصویر کھولو</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">لنک ڈاؤن لوڈ کرو</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">لنک شیئر کرو</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">تصویر شیئر کرو</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">لنک نقل کرو</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">تصویر مقام نقل کرو</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">تصویر محفوظ کرو</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">تصویر کاپی کرو</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">فائل ڈیوائس وچ محفوظ کرو</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">نواں ٹیب کھل ڳیا</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">نواں نجی ٹیب کھُل ڳیا</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">لنک کلپ بورڈ تے نقل تھی ڳیا</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">سوئچ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">لنک ٻاہرلی ایپ وچ کھولو</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ای میل پتہ شیئر کرو</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ای میل پتہ نقل کرو</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ای میل پتہ کلپ بورڈ تے نقل تھی ڳیا</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">رابطہ وچ شامل کرو</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ڳولو</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">نجی ڳولݨ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">شیئر</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ای میل</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">فون کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..7eedd1d51f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sl/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Odpri povezavo v novem zavihku</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Odpri povezavo v zasebnem zavihku</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Odpri sliko v novem zavihku</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Prenesi povezavo</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Deli povezavo</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Deli sliko</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopiraj povezavo</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopiraj mesto slike</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Shrani sliko</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopiraj sliko</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Shrani datoteko na napravo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Odprt nov zavihek</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Odprt nov zaseben zavihek</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Povezava kopirana v odložišče</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Preklopi</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Odpri povezavo v zunanji aplikaciji</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Deli e-poštni naslov</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopiraj e-poštni naslov</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-poštni naslov kopiran v odložišče</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Dodaj k stiku</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Išči</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Zasebno iskanje</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Deli</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-pošta</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Pokliči</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..44c5483f05
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sq/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Hape lidhjen në skedë të re</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Hape lidhjen në skedë private</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Hape figurën në skedë të re</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Lidhje shkarkimi</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Ndani lidhje me të tjerët</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Ndani figurë me të tjerët</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopjoje lidhjen</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopjo vendndodhje figure</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Ruaje figurën</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopjo figurën</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Ruajeni kartelën te pajisje</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">U hap skedë e re</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">U hap skedë e re private</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Lidhja u kopjua në të papastër</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Kaloni në të</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Hape lidhjen në aplikacion të jashtëm</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Jepe adresën email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopjoje adresën email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Adresa email u kopjua në të papastër</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Shtoje te kontaktet</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Kërko</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Kërkim Privat</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Ndajeni me të tjerë</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Thirrje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..1794929708
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sr/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Отвори везу у новом језичку</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Отвори везу у приватном језичку</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Отвори слику у новом језичку</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Преузми везу</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Подели везу</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Подели слику</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Копирај везу</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Копирај локацију слике</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Сачувај слику</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Копирај слику</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Сачувај датотеку на уређај</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Нови језичак је отворен</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Нови приватни језичак је отворен</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Веза је копирана у привремену меморију</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Пребаци</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Отвори везу у спољној апликацији</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Подели адресу е-поште</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Копирај адресу е-поште</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Адреса е-поште је копирана у привремену меморију</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Додај у контакте</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Претрага</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Приватна претрага</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Дели</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Е-пошта</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Позови</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..c60cc95393
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-su/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Buka tutumbu di tab anyar</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Buka dina tab nyamuni</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Buka gambar di tab anyar</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Undeur tutumbu</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Bagikeun tutumbu</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Bagikeun gambar</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Tiron tutumbu</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Tiron pernah gambar</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Teundeun gambar</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Tiron gambar</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Teundeun berkas ka piranti</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Tab anyar dibuka</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Tab nyamuni anyar dibuka</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Tutumbu ditiron kana papan klip</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Gilir</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Buka tutumbu di aplikasi luar</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Bagikeun alamat surélék</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Tiron alamat surélék</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Alamat surélék geus ditiron kana papan klip</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Tambahkeun ka kontak</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Sungsi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Sungsi Nyamuni</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Bagikeun</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Surélék</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Gero</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..ce908a7552
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Öppna länk i ny flik</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Öppna länk i privat flik</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Öppna bild i ny flik</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Hämta länk</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Dela länk</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Dela bild</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopiera länk</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Kopiera bildadress</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Spara bild</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Kopiera bild</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Spara fil till enhet</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Ny flik öppnad</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Ny privat flik öppnad</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Länk kopierad till urklipp</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Växla</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Öppna länk i extern app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Dela e-postadress</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopiera e-postadress</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-postadress kopierad till urklipp</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Lägg till kontakt</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Sök</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Privat sökning</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Dela</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-post</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Ring</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..b6a998ad59
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ta/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">இணைப்பைப் புதிய கீற்றில் திற</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">இணைப்பைக் கமுக்கக் கீற்றில் திற</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">படத்தைப் புதிய கீற்றில் திற</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">இணைப்பைத் பதிவிறக்கு</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">தொடுப்பைப் பகிர்</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">படத்தைப் பகிர்</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">இணைப்பை நகலெடுக்கவும்</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">பட இருப்பிடத்தை நகலெடுக்கவும்</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">படத்தைச் சேமி</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">கோப்பைக் கருவியில் சேமி</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">புதிய கீற்று திறந்தது</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">புதிய கமுக்கக் கீற்று திறந்தது</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">இணைப்பு ஒட்டுப்பலகைக்கு நகலெடுக்கப்பட்டது</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">தாவு</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">இணைப்பை வேறு செயலியில் திற</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">மின்னஞ்சல் முகவரியைப் பகிரவும்</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">மின்னஞ்சல் முகவரியை நகலெடு</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">மின்னஞ்சல் முகவரி ஒட்டுப்பலகைக்கு நகலெடுக்கப்பட்டது</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">தொடர்பில் சேர்</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">தேடு:</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">கமுக்கத் தேடல்</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">பகிர்</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">மின்னஞ்சல்</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">அழை</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..92564bd098
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-te/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">లంకెను కొత్త ట్యాబులో తెరువు</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">లంకెను అంతరంగిక ట్యాబులో తెరువు</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">బొమ్మను కొత్త ట్యాబులో తెరువు</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">లంకెను దించుకో</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">లంకెను పంచుకోండి</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">బొమ్మని పంచుకోండి</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">లంకెను కాపీచేయి</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">బొమ్మ స్థానాన్ని కాపీచేయి</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">బొమ్మను భద్రపరుచు</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ఫైలును పరికరంలో భద్రపరచు</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">కొత్త ట్యాబు తెరుచుకుంది</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">కొత్త అంతరంగిక ట్యాబు తెరుచుకుంది</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">లంకె క్లిప్‌బోర్డుకి కాపీ అయ్యింది</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">మారు</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">లంకెని బయటి అనువర్తనంలో తెరువు</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ఈమెయిలు చిరునామాను పంచుకో</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ఈమెయిలు చిరునామాను కాపీచేయి</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ఈమెయిలు చిరునామా క్లిప్‌బోర్డుకి కాపీ అయ్యింది</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">పరిచయాలకు చేర్చు</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">వెతకండి</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">అంతరంగికంగా వెతకండి</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">పంచుకోండి</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ఈమెయిలు</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">ఫోనుచెయ్యండి</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..3ab9e7d1e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tg/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Кушодани пайванд дар варақаи нав</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Кушодани пайванд дар варақаи хусусӣ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Кушодани тасвир дар варақаи нав</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Пайванди боргирӣ</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Мубодила кардани пайванд</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Мубодила кардани тасвир</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Нусха бардоштани пайванд</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Нусха бардоштани ҷойгиршавии тасвир</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Нигоҳ доштани тасвир</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Нусха бардоштани тасвир</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Нигоҳ доштани файл дар дастгоҳ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Варақаи нав кушода шуд</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Варақаи хусусии нав кушода шуд</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Пайванд ба ҳофизаи муваққатӣ нусха бардошта шуд</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Гузариш</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Кушодани пайванд дар барномаи берунӣ</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Мубодила кардани нишонии почтаи электронӣ</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Нусха бардоштани нишонии почтаи электронӣ</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Нишонии почтаи электронӣ ба ҳофизаи муваққатӣ нусха бардошта шуд</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Илова кардан ба тамос</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Ҷустуҷӯ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Ҷустуҷӯи хусусӣ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Мубодила кардан</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Фиристодани паёми электронӣ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Занг задан</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..f3b91aec36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-th/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">เปิดลิงก์ในแท็บใหม่</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">เปิดลิงก์ในแท็บส่วนตัว</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">เปิดภาพในแท็บใหม่</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ดาวน์โหลดลิงก์</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">แบ่งปันลิงก์</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">แบ่งปันภาพ</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">คัดลอกลิงก์</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">คัดลอกตำแหน่งที่ตั้งภาพ</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">บันทึกภาพ</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">คัดลอกภาพ</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">บันทึกไฟล์ไปยังอุปกรณ์</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">เปิดแท็บใหม่แล้ว</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">เปิดแท็บส่วนตัวใหม่แล้ว</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">คัดลอกลิงก์ไปยังคลิปบอร์ดแล้ว</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">สลับ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">เปิดลิงก์ในแอปภายนอก</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">แบ่งปันที่อยู่อีเมล</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">คัดลอกที่อยู่อีเมล</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">คัดลอกที่อยู่อีเมลไปยังคลิปบอร์ดแล้ว</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">เพิ่มไปยังผู้ติดต่อ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ค้นหา</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">การค้นหาแบบส่วนตัว</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">แบ่งปัน</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">อีเมล</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">โทร</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..32f0ca2003
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tl/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Buksan ang link sa panibagong tab</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Buksan ang link sa pribadong tab</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Buksan ang larawan sa bagong tab</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">i-Download ang link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Ibahagi ang link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Ibahagi ang larawan</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Kopyahin ang link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Koyahin ang lokasyon ng larawan</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">I-save ang imahe</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">i-Save ang file sa device</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Nagbukas ng bagong tab</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Nagbukas ng bagong pribadong tab</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Nakopya na ang teksto sa clipboard</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Lumipat</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Buksan ang link sa hiwalay na app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Ibahagi ang mga email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Kopyahin ang email address</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Nakopya na ang email address sa clipboard</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Idagdag sa contact</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Hanapin</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Pribadong Paghanap</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Ibahagi</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Mag-email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Tawagan</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..7ec5f13dc7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tr/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Bağlantıyı yeni sekmede aç</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Bağlantıyı gizli sekmede aç</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Resmi yeni sekmede aç</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Bağlantıyı indir</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Bağlantıyı paylaş</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Resmi paylaş</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Bağlantıyı kopyala</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Resim konumunu kopyala</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Resmi kaydet</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Resmi kopyala</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Dosyayı cihaza kaydet</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Yeni sekme açıldı</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Yeni gizli sekme açıldı</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Bağlantı panoya kopyalandı</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Buna geç</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Bağlantıyı başka uygulamada aç</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">E-posta adresini paylaş</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">E-posta adresini kopyala</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">E-posta adresi panoya kopyalandı</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Kişiye ekle</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Ara</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Gizli arama</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Paylaş</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-posta</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Çağrı yap</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..4cdfe223d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-trs/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Nā\'nïn lînk riña a\'ngô rakïj ñanj nakàa</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Nā\'nïn lînk riña \'ngō rakïj ñanj huìi</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Nā\'nïn ñadu\'ua riña a\'ngô rakïj ñanj nakàa</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Lînk riñā nadunïnjt</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Dūyinga\' lînk</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Dūyinga\' ñadu\'ua</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Gūxūn nī nāchrūnt a\'ngô hiūj u lînk</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Gūxūn nī nāchrūnt a\'ngô hiūj u riña \'na\' ñadu\'ua</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Nā\'nïnj sà\' ñadu\'ua</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Gīda’a ñanj dū’hua</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Nā\'nïnj sà\' riña si ārchibô aga\' nan</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Ngà nayî\'nïnj a\'ngô rakïj ñanj nākàa</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Ngà nayî\'nïnj a\'ngô rakïj ñanj nākà huìi</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Ngà nañû enlace riña portapapel</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Nādūnā</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Nā\'nïn lînk riña a\'ngô aplikasiûn</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Dūyinga’ si direksiun korreo</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Gūxun\' dirección korreo da\' nāchrūn\' a\'ngô hiūj u</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Ganatûj si direksiun korreo riña portapapel</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Gūnutò\’ man riña kontakto</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Nānà\'uì\'</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Nānà\'uì\' huì</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Dūyingô\'</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Korrêo</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Gā\’mīn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..10bae29cfd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tt/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Сылтаманы яңа биттә ачу</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Сылтаманы xосусый табта ачу</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Рәсемне яңа табта ачу</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Сылтаманы йөкләп алу</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Сылтаманы уртаклашу</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Рәсемне уртаклашу</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Сылтаманы күчереп алу</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Рәсемнең сылтамасын күчереп алу</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Рәсемне саклау</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Рәсемне копияләү</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Файлны җиһазга саклау</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Яңа таб ачылды</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Яңа хосусый таб ачылды</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Сылтама алмашу буферына күчермәләнде</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Күчү</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Сылтаманы тышкы кушымтада ачу</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Эл. почта адресын уртаклашу</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Эл. почта адресын күчереп алу</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Эл. почта адресы алмашу буферына күчермәләнде</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Контактларга өстәү</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Эзләү</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Хосусый эзләү</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Уртаклашу</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Эл. почта</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Шалтырату</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..4e55701fc4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Rẓem asɣen g useksel amaynu</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Rẓem asɣen g useksel uslig</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Rẓem tawlaft g useksel amaynu</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Agem asɣen</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Bḍu asɣen</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Bḍu tawlaft</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Nɣel asɣun</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Agem tawlaft</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Agem afaylu ɣer wallal</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">irzem useksel amaynu</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">irzem useksel uslig amaynu</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Bḍu tansa imayl</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Nɣel tansa imayl</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Rnu ɣer wassaɣ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Rzu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Arezzu Uslig</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Bḍu</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Imayl</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Ɣer</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..b2eab24fa2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ug/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">ئۇلانمىنى يېڭى بەتكۈچتە ئېچىش</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ئۇلانمىنى شەخسىي بەتكۈچتە ئاچ</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">رەسىمنى يېڭى بەتكۈچتە ئېچىش</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">چۈشۈرۈش ئۇلانمىسى</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">ئۇلانمىنى ھەمبەھىرلەش</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">رەسىمنى ھەمبەھىرلەش</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">ئۇلانمىنى كۆچۈرۈش</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">سۈرەت ئورنىنى كۆچۈر</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">رەسىمنى ساقلاش</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">سۈرەت كۆچۈر</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">ھۆججەتنى ئۈسكۈنىگە ساقلا</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">يېڭى بەتكۈچ ئېچىلدى</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">يېڭى شەخسىي بەتكۈچ ئېچىلدى</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">ئۇلانما چاپلاش تاختىسىغا كۆچۈرۈلدى</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">ئاتلاش</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">ئۇلانمىنى سىرتقى ئەپتە ئېچىش</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ئېلخەت ئادرېسىنى ھەمبەھىرلەش</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ئېلخەت ئادرېسىنى كۆچۈرۈش</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ئېلخەت ئادرېسى چاپلاش تاختىسىغا كۆچۈرۈلدى</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">ئالاقەداشقا قوش</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">ئىزدەش</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">شەخسىي ئىزدە</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">ھەمبەھىرلەش</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ئېلخەت</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">چاقىرىش</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..fbf3fb6a47
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-uk/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Відкрити посилання в новій вкладці</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Відкрити посилання у приватній вкладці</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Відкрити зображення в новій вкладці</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Завантажити посилання</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Поділитись посиланням</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Поділитися зображенням</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Копіювати посилання</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Копіювати адресу зображення</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Зберегти зображення</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Копіювати зображення</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Зберегти файл на пристрій</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Відкрито нову вкладку</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Відкрита нова приватна вкладка</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Посилання скопійовано до буфера</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Перемкнути</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Відкрити посилання у зовнішній програмі</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Поділитись адресою е-пошти</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Копіювати адресу е-пошти</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Адресу е-пошти скопійовано до буферу обміну</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Додати до контакту</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Шукати</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Приватний пошук</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Поділитися</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Ел. поштою</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Виклик</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..dfc8c1094d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-ur/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">ربط نئی ٹیب میں کھولیں</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">ربط نجی ٹیب میں کھولیں</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">تصویر نئی ٹیب میں کھولیں</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">ڈاؤن لوڈ ربط</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">ربط شیئر کریں</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">تصویر شیئر کریں</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">ربط نقل کریں</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">نقش محل وقوع نقل کریں</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">نقش محفوظ کریں</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">فائل کو آلہ میں محفوظ کریں</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">نیا ٹیب کھلا</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">نیا نجی ٹیب کھل گیا</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">لنک کاپی کلپ بورڈ میں کیا گیا</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">سوئچ</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">لنک کو باهری ایپ میں کھولیں</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">ای میل پتہ شیئر کریں</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">ای میل پتہ نقل کریں</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">ای میل پتہ کلپ بورڈ میں نقل کیا گیا</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">رابطہ افراد میں ڈالیں</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">تلاش کریں</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">نجی تلاش</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">شیئر</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">ای میل</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">کال کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..223715070f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-uz/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Havolani yangi varaqda ochish</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Havolani maxfiy varaqda ochish</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Rasmni yangi varaqda ochish</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Havola orqali yuklab olish</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Havolani ulashish</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Rasmni ulashish</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Havoladan nusxa olish</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Rasm manzilidan nusxa olish</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Rasmni saqlash</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Faylni qurilmaga saqlash</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Yangi varaq ochildi</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Yangi maxfiy varaq ochildi</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Havoladan klipboardga nusxa olindi</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Oʻtish</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Havolani tashqi ilovada ochish</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Email manzillarini boʻlishish</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Email manzilidan nusxa olish</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Email manzili vaqtinchalik xotiraga nusxalandi</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Kontaktga qoʻshish</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Izlash</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Maxfiy qidiruv</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Ulashish</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Qoʻngʻiroq qilish</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..9a2f0ab546
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-vec/strings.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Vèrxi link en na nova scheda</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Vèrxi en na scheda anonima</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Vèrxi imagine en na nova fenèstra</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Scarega link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Condividi link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Condividi imaxene</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Còpia link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Còpia indiriso imagine</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Salva imagine</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Còpia imaxene</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Salva el file so el dispoxitivo</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Vèrxi nova scheda</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Vèrta nova scheda anonima</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link copià ne i scarabòci</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Pasa a</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Vèrxi en na app esterna</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Condividi indiriso email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Còpia indiriso email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Indiriso email copià intel i scaraboci</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Xonta a on contato</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Serca</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Reserca anonema</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Condividi</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..68d8102afd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-vi/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Mở liên kết trong thẻ mới</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Mở liên kết trong thẻ riêng tư mới</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Mở ảnh trong thẻ mới</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Tải xuống liên kết</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Chia sẻ liên kết</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Chia sẻ ảnh</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Sao chép liên kết</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Sao chép liên kết ảnh</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Lưu ảnh</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Sao chép ảnh</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Lưu tập tin vào thiết bị</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Thẻ mới đã mở</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Thẻ riêng tư mới đã mở</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Đã sao chép liên kết vào bộ nhớ tạm</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Chuyển</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Mở liên kết đến ứng dụng bên ngoài</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Chia sẻ địa chỉ email</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Sao chép địa chỉ email</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Đã sao chép địa chỉ email vào bộ nhớ tạm</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Thêm vào liên lạc</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Tìm kiếm</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Tìm kiếm riêng tư</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Chia sẻ</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">E-mail</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Gọi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..acc9af30f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-yo/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Ṣí líǹkì nínú táàbù tuntun</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Ṣí líǹkì nínú táàbù ìkọ̀kọ̀</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Ṣí àwòrán nínú táàbù tuntun</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Ṣe ìgbàsílẹ̀ líǹkì</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Pín líǹkì</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Pín àwòrán</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Ṣe àdàkọ líǹkì</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Ṣe ìdàkọ ipò àwòrán</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Fi àwòrán pamọ́</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Fi fáìlì pamọ́ sórí ẹ̀rọ</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">Táàbù tuntún wà ní ṣíṣí</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">Táàbù tuntun oníkọ̀kọ̀ wà ní ṣíṣí</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Líǹkì ti wà ní ìdàkọ sórí àtẹ-fọ́nrán</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Ṣe àyípadà</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Ṣí líǹkì nínú áàpù ìta</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Pín àdírẹ́sì ímeèlì</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Ṣe àdàkọ àdírẹ́sì ímeèlì</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Àdírẹ́sì ímeèlì ti wà ní àdàkọ sórí àtẹ-fọ́nrán</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Fikún àwọn olùbásọ̀rọ̀</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Ṣe àwárí</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Àwárí ìkọ̀kọ̀</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Pín</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Ímeèlì</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Ìpè</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..7b2cc2b8ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">新建标签页打开链接</string>
+
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">新建隐私标签页打开链接</string>
+
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">新建标签页打开图像</string>
+
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">下载链接</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">分享链接</string>
+
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">分享图像</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">复制链接</string>
+
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">复制图像地址</string>
+
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">保存图像</string>
+
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">复制图像</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">将文件保存到设备中</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">已打开新标签页</string>
+
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">已新建隐私标签页</string>
+
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">链接已复制到剪贴板</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">切换</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">在外部应用中打开链接</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">分享电子邮件地址</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">复制电子邮件地址</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">电子邮件地址已复制到剪贴板</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">添加到联系人</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">搜索</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">隐私搜索</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">分享</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">电邮</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">呼叫</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..f942f7b9e6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">用新分頁開啟鏈結</string>
+
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">用隱私分頁開啟鏈結</string>
+
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">用新分頁開啟圖片</string>
+
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">下載鏈結</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">分享鏈結</string>
+
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">分享圖片</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">複製鏈結</string>
+
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">複製圖片網址</string>
+
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">儲存圖片</string>
+
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">複製圖片</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">將檔案儲存到裝置上</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">已開啟新分頁</string>
+
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">已開啟新隱私分頁</string>
+
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">已將鏈結複製至剪貼簿</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">切換</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">用外部應用程式開啟鏈結</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">分享電子郵件地址</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">複製電子郵件地址</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">已將電子郵件地址複製至剪貼簿</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">新增為聯絡人</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">搜尋</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">隱私搜尋</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">分享</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">發送郵件</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">撥號</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..c82374b18e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+<?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>
+ <color name="mozac_additional_note_background">#1D1133</color>
+ <color name="mozac_additional_note_text_color">#BFBFC9</color>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..32c120724f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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>
+ <!-- Text for context menu item to open the link in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_new_tab">Open link in new tab</string>
+ <!-- Text for context menu item to open the link in a private tab. -->
+ <string name="mozac_feature_contextmenu_open_link_in_private_tab">Open link in private tab</string>
+ <!-- Text for context menu item to open the image in a new tab. -->
+ <string name="mozac_feature_contextmenu_open_image_in_new_tab">Open image in new tab</string>
+ <!-- Text for context menu item to save / download the link. -->
+ <string name="mozac_feature_contextmenu_download_link">Download link</string>
+ <!-- Text for context menu item to share the link with an other app. -->
+ <string name="mozac_feature_contextmenu_share_link">Share link</string>
+ <!-- Text for context menu item to share the image with an other app. -->
+ <string name="mozac_feature_contextmenu_share_image">Share image</string>
+ <!-- Text for context menu item to copy the link to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_link">Copy link</string>
+ <!-- Text for context menu item to copy the URL pointing to the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image_location">Copy image location</string>
+ <!-- Text for context menu item to save / download the image. -->
+ <string name="mozac_feature_contextmenu_save_image">Save image</string>
+ <!-- Text for context menu item to copy the image to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_image">Copy image</string>
+ <!-- Text for context menu item to save / download the file. -->
+ <string name="mozac_feature_contextmenu_save_file_to_device">Save file to device</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_tab_opened">New tab opened</string>
+ <!-- Text for confirmation "snackbar" shown after opening a link in a new private tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_new_private_tab_opened">New private tab opened</string>
+ <!-- Text for confirmation "snackbar" shown after copying a link or image URL to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_link_copied">Link copied to clipboard</string>
+ <!-- Action shown in a "snacbkar" after opening a new/private tab. Clicking this action will switch to the newly opened tab. -->
+ <string name="mozac_feature_contextmenu_snackbar_action_switch">Switch</string>
+ <!-- Text for context menu item to open the link in an external app. -->
+ <string name="mozac_feature_contextmenu_open_link_in_external_app">Open link in external app</string>
+ <!-- Text for context menu item to share the email with another app. -->
+ <string name="mozac_feature_contextmenu_share_email_address">Share email address</string>
+ <!-- Text for context menu item to copy the email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_copy_email_address">Copy email address</string>
+ <!-- Text for confirmation "snackbar" shown after copying a email address to the clipboard. -->
+ <string name="mozac_feature_contextmenu_snackbar_email_address_copied">Email address copied to clipboard</string>
+ <!-- Text for context menu item to add to a contact. -->
+ <string name="mozac_feature_contextmenu_add_to_contact">Add to contact</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search using the selected text.-->
+ <string name="mozac_selection_context_menu_search_2">Search</string>
+ <!-- Action shown in a text selection context menu. This will prompt a search in a private tab using the selected text-->
+ <string name="mozac_selection_context_menu_search_privately_2">Private Search</string>
+ <!-- Action shown in a text selection context menu. This will prompt a share of the selected text. -->
+ <string name="mozac_selection_context_menu_share">Share</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new email from the selected text. -->
+ <string name="mozac_selection_context_menu_email">Email</string>
+ <!-- Action shown in a text selection context menu. This will prompt a new call from the selected text. -->
+ <string name="mozac_selection_context_menu_call">Call</string>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/style.xml b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/style.xml
new file mode 100644
index 0000000000..e89670a3fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/main/res/values/style.xml
@@ -0,0 +1,14 @@
+<?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>
+ <style name="Mozac.Dialog.AdditionalNote" parent="Widget.MaterialComponents.TextView">
+ <item name="android:layout_width">48dp</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:background">@color/mozac_additional_note_background</item>
+ <item name="android:textColor">@color/mozac_additional_note_text_color</item>
+ <item name="android:lineSpacingExtra">3dp</item>
+ </style>
+</resources>
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt b/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt
new file mode 100644
index 0000000000..867966194c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt
@@ -0,0 +1,2063 @@
+/* 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.contextmenu
+
+import android.content.ClipboardManager
+import android.content.Context
+import android.view.View
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.MainScope
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.feature.app.links.AppLinkRedirect
+import mozilla.components.feature.app.links.AppLinksUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.widgets.SnackbarDelegate
+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.anyBoolean
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ContextMenuCandidateTest {
+
+ private lateinit var snackbarDelegate: TestSnackbarDelegate
+
+ @Before
+ fun setUp() {
+ snackbarDelegate = TestSnackbarDelegate()
+ }
+
+ @Test
+ fun `Default candidates sanity check`() {
+ val candidates = ContextMenuCandidate.defaultCandidates(testContext, mock(), mock(), mock())
+ // Just a sanity check: When changing the list of default candidates be aware that this will affect all
+ // consumers of this component using the default list.
+ assertEquals(14, candidates.size)
+ }
+
+ @Test
+ fun `Candidate 'Open Link in New Tab' showFor displayed in correct cases`() {
+ val store = BrowserStore()
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+ val openInNewTab = ContextMenuCandidate.createOpenInNewTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertTrue(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Open Link in New Tab' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val openInNewTab = ContextMenuCandidate.createOpenInNewTabCandidate(
+ testContext,
+ mock(),
+ mock(),
+ snackbarDelegate,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Open Link in New Tab' action properly executes for session with a contextId`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", contextId = "1"),
+ ),
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+ val openInNewTab = ContextMenuCandidate.createOpenInNewTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("1", store.state.tabs.first().contextId)
+
+ openInNewTab.action.invoke(store.state.tabs.first(), HitResult.UNKNOWN("https://firefox.com"))
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://firefox.com", store.state.tabs.last().content.url)
+ assertEquals("1", store.state.tabs.last().contextId)
+ }
+
+ @Test
+ fun `Candidate 'Open Link in New Tab' action properly executes and shows snackbar`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org"),
+ ),
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+ val openInNewTab = ContextMenuCandidate.createOpenInNewTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+ assertFalse(snackbarDelegate.hasShownSnackbar)
+
+ openInNewTab.action.invoke(store.state.tabs.first(), HitResult.UNKNOWN("https://firefox.com"))
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertTrue(snackbarDelegate.hasShownSnackbar)
+ assertNotNull(snackbarDelegate.lastActionListener)
+ assertEquals("https://firefox.com", store.state.tabs.last().content.url)
+ }
+
+ @Test
+ fun `Candidate 'Open Link in New Tab' snackbar action works`() {
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = mock(),
+ scope = MainScope(),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+
+ val openInNewTab = ContextMenuCandidate.createOpenInNewTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+ assertFalse(snackbarDelegate.hasShownSnackbar)
+
+ openInNewTab.action.invoke(store.state.tabs.first(), HitResult.UNKNOWN("https://firefox.com"))
+ store.waitUntilIdle()
+
+ assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
+
+ snackbarDelegate.lastActionListener!!.invoke(mock())
+ store.waitUntilIdle()
+
+ assertEquals("https://firefox.com", store.state.selectedTab!!.content.url)
+ }
+
+ @Test
+ fun `Candidate 'Open Link in New Tab' action properly handles link with an image`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+
+ val openInNewTab = ContextMenuCandidate.createOpenInNewTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+
+ openInNewTab.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://www.mozilla_src.org", "https://www.mozilla_uri.org"),
+ )
+ store.waitUntilIdle()
+
+ assertEquals("https://www.mozilla_uri.org", store.state.tabs.last().content.url)
+ }
+
+ @Test
+ fun `Candidate 'Open Link in Private Tab' showFor displayed in correct cases`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+ val openInPrivateTab = ContextMenuCandidate.createOpenInPrivateTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertTrue(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Open Link in Private Tab' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val openInPrivateTab = ContextMenuCandidate.createOpenInPrivateTabCandidate(
+ testContext,
+ mock(),
+ mock(),
+ snackbarDelegate,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Open Link in Private Tab' action properly executes and shows snackbar`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+ val openInPrivateTab = ContextMenuCandidate.createOpenInPrivateTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+ assertFalse(snackbarDelegate.hasShownSnackbar)
+
+ openInPrivateTab.action.invoke(store.state.tabs.first(), HitResult.UNKNOWN("https://firefox.com"))
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertTrue(snackbarDelegate.hasShownSnackbar)
+ assertNotNull(snackbarDelegate.lastActionListener)
+ assertEquals("https://firefox.com", store.state.tabs.last().content.url)
+ }
+
+ @Test
+ fun `Candidate 'Open Link in Private Tab' snackbar action works`() {
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = mock(),
+ scope = MainScope(),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+ val openInPrivateTab = ContextMenuCandidate.createOpenInPrivateTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+ assertFalse(snackbarDelegate.hasShownSnackbar)
+
+ openInPrivateTab.action.invoke(store.state.tabs.first(), HitResult.UNKNOWN("https://firefox.com"))
+ store.waitUntilIdle()
+
+ assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
+ assertEquals(2, store.state.tabs.size)
+
+ snackbarDelegate.lastActionListener!!.invoke(mock())
+ store.waitUntilIdle()
+
+ assertEquals("https://firefox.com", store.state.selectedTab!!.content.url)
+ }
+
+ @Test
+ fun `Candidate 'Open Link in Private Tab' action properly handles link with an image`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+ val openInPrivateTab = ContextMenuCandidate.createOpenInPrivateTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+ openInPrivateTab.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://www.mozilla_src.org", "https://www.mozilla_uri.org"),
+ )
+ store.waitUntilIdle()
+ assertEquals("https://www.mozilla_uri.org", store.state.tabs.last().content.url)
+ }
+
+ @Test
+ fun `Candidate 'Open Image in New Tab'`() {
+ val store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = mock(),
+ scope = MainScope(),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+
+ val openImageInTab = ContextMenuCandidate.createOpenImageInNewTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ // showFor
+
+ assertFalse(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+
+ // action
+
+ assertEquals(1, store.state.tabs.size)
+ assertFalse(snackbarDelegate.hasShownSnackbar)
+
+ openImageInTab.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertFalse(store.state.tabs.last().content.private)
+ assertEquals("https://firefox.com", store.state.tabs.last().content.url)
+ assertTrue(snackbarDelegate.hasShownSnackbar)
+ assertNotNull(snackbarDelegate.lastActionListener)
+
+ // Snackbar action
+
+ assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
+
+ snackbarDelegate.lastActionListener!!.invoke(mock())
+ store.waitUntilIdle()
+
+ assertEquals("https://firefox.com", store.state.selectedTab!!.content.url)
+ }
+
+ @Test
+ fun `Candidate 'Open Image in New Tab' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val openImageInTab = ContextMenuCandidate.createOpenImageInNewTabCandidate(
+ testContext,
+ mock(),
+ mock(),
+ snackbarDelegate,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Open Image in New Tab' opens in private tab if session is private`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+
+ val openImageInTab = ContextMenuCandidate.createOpenImageInNewTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+
+ openImageInTab.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com"),
+ )
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertTrue(store.state.tabs.last().content.private)
+ assertEquals("https://firefox.com", store.state.tabs.last().content.url)
+ }
+
+ @Test
+ fun `Candidate 'Open Image in New Tab' opens with the session's contextId`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", contextId = "1"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val tabsUseCases = TabsUseCases(store)
+ val parentView = CoordinatorLayout(testContext)
+
+ val openImageInTab = ContextMenuCandidate.createOpenImageInNewTabCandidate(
+ testContext,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ )
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("1", store.state.tabs.first().contextId)
+
+ openImageInTab.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com"),
+ )
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://firefox.com", store.state.tabs.last().content.url)
+ assertEquals("1", store.state.tabs.last().contextId)
+ }
+
+ @Test
+ fun `Candidate 'Save image'`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val saveImage = ContextMenuCandidate.createSaveImageCandidate(
+ testContext,
+ ContextMenuUseCases(store),
+ )
+
+ // showFor
+
+ assertFalse(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+
+ // action
+
+ assertNull(store.state.tabs.first().content.download)
+
+ saveImage.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC(
+ "https://www.mozilla.org/media/img/logos/firefox/logo-quantum.9c5e96634f92.png",
+ "https://firefox.com",
+ ),
+ )
+
+ store.waitUntilIdle()
+
+ assertNotNull(store.state.tabs.first().content.download)
+ assertEquals(
+ "https://www.mozilla.org/media/img/logos/firefox/logo-quantum.9c5e96634f92.png",
+ store.state.tabs.first().content.download!!.url,
+ )
+ assertTrue(
+ store.state.tabs.first().content.download!!.skipConfirmation,
+ )
+ assertTrue(
+ store.state.tabs.first().content.download!!.private,
+ )
+ }
+
+ @Test
+ fun `Candidate 'Save image' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val saveImage = ContextMenuCandidate.createSaveImageCandidate(
+ testContext,
+ mock(),
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Save video and audio'`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val saveVideoAudio = ContextMenuCandidate.createSaveVideoAudioCandidate(
+ testContext,
+ ContextMenuUseCases(store),
+ )
+
+ // showFor
+
+ assertFalse(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.AUDIO("https://www.mozilla.org"),
+ ),
+ )
+
+ // action
+
+ assertNull(store.state.tabs.first().content.download)
+
+ saveVideoAudio.action.invoke(
+ store.state.tabs.first(),
+ HitResult.AUDIO("https://developer.mozilla.org/media/examples/t-rex-roar.mp3"),
+ )
+
+ store.waitUntilIdle()
+
+ assertNotNull(store.state.tabs.first().content.download)
+ assertEquals(
+ "https://developer.mozilla.org/media/examples/t-rex-roar.mp3",
+ store.state.tabs.first().content.download!!.url,
+ )
+ assertTrue(
+ store.state.tabs.first().content.download!!.skipConfirmation,
+ )
+
+ assertTrue(
+ store.state.tabs.first().content.download!!.private,
+ )
+ }
+
+ @Test
+ fun `Candidate 'Save video and audio' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val saveVideoAudio = ContextMenuCandidate.createSaveVideoAudioCandidate(
+ testContext,
+ mock(),
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.AUDIO("https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'download link'`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ val downloadLink = ContextMenuCandidate.createDownloadLinkCandidate(
+ testContext,
+ ContextMenuUseCases(store),
+ )
+
+ // showFor
+
+ assertTrue(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.PHONE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.EMAIL("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.GEO("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org/firefox/products.html"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org/firefox/products.htm"),
+ ),
+ )
+
+ // action
+
+ assertNull(store.state.tabs.first().content.download)
+
+ downloadLink.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC(
+ "https://www.mozilla.org/media/img/logos/firefox/logo-quantum.9c5e96634f92.png",
+ "https://www.mozilla.org/en-US/privacy-policy.pdf",
+ ),
+ )
+
+ store.waitUntilIdle()
+
+ assertNotNull(store.state.tabs.first().content.download)
+ assertEquals(
+ "https://www.mozilla.org/en-US/privacy-policy.pdf",
+ store.state.tabs.first().content.download!!.url,
+ )
+ assertTrue(
+ store.state.tabs.first().content.download!!.skipConfirmation,
+ )
+
+ assertTrue(store.state.tabs.first().content.download!!.private)
+ }
+
+ @Test
+ fun `Candidate 'download link' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val downloadLink = ContextMenuCandidate.createDownloadLinkCandidate(
+ testContext,
+ mock(),
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Get link for image, video, audio gets title if title is set`() {
+ val titleString = "test title"
+
+ val hitResultImage = HitResult.IMAGE("https://www.mozilla.org", titleString)
+ var title = hitResultImage.getLink()
+ assertEquals(titleString, title)
+
+ val hitResultVideo = HitResult.VIDEO("https://www.mozilla.org", titleString)
+ title = hitResultVideo.getLink()
+ assertEquals(titleString, title)
+
+ val hitResultAudio = HitResult.AUDIO("https://www.mozilla.org", titleString)
+ title = hitResultAudio.getLink()
+ assertEquals(titleString, title)
+ }
+
+ @Test
+ fun `Get link for image, video, audio gets URL if title is blank`() {
+ val titleString = " "
+ val url = "https://www.mozilla.org"
+
+ val hitResultImage = HitResult.IMAGE(url, titleString)
+ var title = hitResultImage.getLink()
+ assertEquals(url, title)
+
+ val hitResultVideo = HitResult.VIDEO(url, titleString)
+ title = hitResultVideo.getLink()
+ assertEquals(url, title)
+
+ val hitResultAudio = HitResult.AUDIO(url, titleString)
+ title = hitResultAudio.getLink()
+ assertEquals(url, title)
+ }
+
+ @Test
+ fun `Get link for image, video, audio gets URL if title is null`() {
+ val titleString = null
+ val url = "https://www.mozilla.org"
+
+ val hitResultImage = HitResult.IMAGE(url, titleString)
+ var title = hitResultImage.getLink()
+ assertEquals(url, title)
+
+ val hitResultVideo = HitResult.VIDEO(url, titleString)
+ title = hitResultVideo.getLink()
+ assertEquals(url, title)
+
+ val hitResultAudio = HitResult.AUDIO(url, titleString)
+ title = hitResultAudio.getLink()
+ assertEquals(url, title)
+ }
+
+ @Test
+ fun `Get link for image gets 'image' title if title is null and URL is longer than 2500 characters`() {
+ val titleString = null
+ val replacementString = "image"
+ val url = "1".repeat(ContextMenuCandidate.MAX_TITLE_LENGTH + 1)
+
+ val hitResultImage = HitResult.IMAGE(url, titleString)
+ val title = hitResultImage.getLink()
+ assertEquals(replacementString, title)
+ }
+
+ @Test
+ fun `Get link for image gets URL if title is null and URL is not longer than 2500 characters`() {
+ val titleString = null
+ val url = "1".repeat(ContextMenuCandidate.MAX_TITLE_LENGTH)
+
+ val hitResultImage = HitResult.IMAGE(url, titleString)
+ val title = hitResultImage.getLink()
+ assertEquals(url, title)
+ }
+
+ @Test
+ fun `Candidate 'Share Link'`() {
+ val context = spy(testContext)
+
+ val shareLink = ContextMenuCandidate.createShareLinkCandidate(context)
+
+ // showFor
+
+ assertTrue(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ shareLink.showFor(
+ createTab("test://www.mozilla.org"),
+ HitResult.UNKNOWN("test://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+
+ // action
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ shareLink.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com"),
+ )
+
+ verify(context).startActivity(any())
+ }
+
+ @Test
+ fun `Candidate 'Share Link' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val shareLink = ContextMenuCandidate.createShareLinkCandidate(
+ testContext,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("test://www.mozilla.org"),
+ HitResult.UNKNOWN("test://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Share image'`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(TabSessionState("123", ContentState(url = "https://www.mozilla.org"))),
+ ),
+ )
+ val context = spy(testContext)
+
+ val usecases = spy(ContextMenuUseCases(store))
+ val shareUsecase: ContextMenuUseCases.InjectShareInternetResourceUseCase = mock()
+ doReturn(shareUsecase).`when`(usecases).injectShareFromInternet
+ val shareImage = ContextMenuCandidate.createShareImageCandidate(context, usecases)
+ val shareStateCaptor = argumentCaptor<ShareInternetResourceState>()
+ // showFor
+
+ assertTrue(
+ shareImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ shareImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ shareImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.AUDIO("https://www.mozilla.org"),
+ ),
+ )
+
+ // action
+
+ shareImage.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com"),
+ )
+
+ verify(shareUsecase).invoke(eq("123"), shareStateCaptor.capture())
+ assertEquals("https://firefox.com", shareStateCaptor.value.url)
+ assertEquals(store.state.tabs.first().content.private, shareStateCaptor.value.private)
+ }
+
+ @Test
+ fun `Candidate 'Share image' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val shareImage = ContextMenuCandidate.createShareImageCandidate(
+ testContext,
+ mock(),
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ shareImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ shareImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Copy image'`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(TabSessionState("123", ContentState(url = "https://www.mozilla.org"))),
+ ),
+ )
+ val context = spy(testContext)
+
+ val useCases = spy(ContextMenuUseCases(store))
+ val copyUseCase: ContextMenuUseCases.InjectCopyInternetResourceUseCase = mock()
+ doReturn(copyUseCase).`when`(useCases).injectCopyFromInternet
+ val copyImage = ContextMenuCandidate.createCopyImageCandidate(context, useCases)
+ val shareStateCaptor = argumentCaptor<ShareInternetResourceState>()
+
+ // showFor
+
+ assertTrue(
+ copyImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ copyImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.AUDIO("https://www.mozilla.org"),
+ ),
+ )
+
+ // action
+
+ copyImage.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com"),
+ )
+
+ verify(copyUseCase).invoke(eq("123"), shareStateCaptor.capture())
+ assertEquals("https://firefox.com", shareStateCaptor.value.url)
+ assertEquals(store.state.tabs.first().content.private, shareStateCaptor.value.private)
+ }
+
+ @Test
+ fun `Candidate 'Copy image' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val copyImage = ContextMenuCandidate.createCopyImageCandidate(
+ testContext,
+ mock(),
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ copyImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Copy Link'`() {
+ val parentView = CoordinatorLayout(testContext)
+
+ val copyLink = ContextMenuCandidate.createCopyLinkCandidate(
+ testContext,
+ parentView,
+ snackbarDelegate,
+ )
+
+ // showFor
+
+ assertTrue(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ copyLink.showFor(
+ createTab("test://www.mozilla.org"),
+ HitResult.UNKNOWN("test://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+
+ // action
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ copyLink.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com"),
+ )
+
+ assertTrue(snackbarDelegate.hasShownSnackbar)
+
+ val clipboardManager = testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ assertEquals(
+ "https://getpocket.com",
+ clipboardManager.primaryClip!!.getItemAt(0).text,
+ )
+ }
+
+ @Test
+ fun `Candidate 'Copy Link' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val copyLink = ContextMenuCandidate.createCopyLinkCandidate(
+ testContext,
+ mock(),
+ snackbarDelegate,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("test://www.mozilla.org"),
+ HitResult.UNKNOWN("test://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Copy Image Location'`() {
+ val parentView = CoordinatorLayout(testContext)
+
+ val copyImageLocation = ContextMenuCandidate.createCopyImageLocationCandidate(
+ testContext,
+ parentView,
+ snackbarDelegate,
+ )
+
+ // showFor
+
+ assertFalse(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertTrue(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org"),
+ ),
+ )
+
+ // action
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ copyImageLocation.action.invoke(
+ store.state.tabs.first(),
+ HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com"),
+ )
+
+ assertTrue(snackbarDelegate.hasShownSnackbar)
+
+ val clipboardManager = testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ assertEquals(
+ "https://firefox.com",
+ clipboardManager.primaryClip!!.getItemAt(0).text,
+ )
+ }
+
+ @Test
+ fun `Candidate 'Copy Image Location' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val copyImageLocation = ContextMenuCandidate.createCopyImageLocationCandidate(
+ testContext,
+ mock(),
+ snackbarDelegate,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Open in external app'`() {
+ val tab = createTab("https://www.mozilla.org")
+ val getAppLinkRedirectMock: AppLinksUseCases.GetAppLinkRedirect = mock()
+
+ doReturn(
+ AppLinkRedirect(mock(), null, null),
+ ).`when`(getAppLinkRedirectMock).invoke(eq("https://www.example.com"))
+
+ doReturn(
+ AppLinkRedirect(null, null, mock()),
+ ).`when`(getAppLinkRedirectMock).invoke(eq("intent:www.example.com#Intent;scheme=https;package=org.mozilla.fenix;end"))
+
+ doReturn(
+ AppLinkRedirect(null, null, null),
+ ).`when`(getAppLinkRedirectMock).invoke(eq("https://www.otherexample.com"))
+
+ // This mock exists only to verify that it was called
+ val openAppLinkRedirectMock: AppLinksUseCases.OpenAppLinkRedirect = mock()
+
+ val appLinksUseCasesMock: AppLinksUseCases = mock()
+ doReturn(getAppLinkRedirectMock).`when`(appLinksUseCasesMock).appLinkRedirectIncludeInstall
+ doReturn(openAppLinkRedirectMock).`when`(appLinksUseCasesMock).openAppLink
+
+ val openLinkInExternalApp = ContextMenuCandidate.createOpenInExternalAppCandidate(
+ testContext,
+ appLinksUseCasesMock,
+ )
+
+ // showFor
+
+ assertTrue(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.UNKNOWN("https://www.example.com"),
+ ),
+ )
+
+ assertTrue(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.UNKNOWN("intent:www.example.com#Intent;scheme=https;package=org.mozilla.fenix;end"),
+ ),
+ )
+
+ assertTrue(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.VIDEO("https://www.example.com"),
+ ),
+ )
+
+ assertTrue(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.AUDIO("https://www.example.com"),
+ ),
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.UNKNOWN("https://www.otherexample.com"),
+ ),
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.VIDEO("https://www.otherexample.com"),
+ ),
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.AUDIO("https://www.otherexample.com"),
+ ),
+ )
+
+ // action
+
+ openLinkInExternalApp.action.invoke(
+ tab,
+ HitResult.UNKNOWN("https://www.example.com"),
+ )
+
+ openLinkInExternalApp.action.invoke(
+ tab,
+ HitResult.UNKNOWN("intent:www.example.com#Intent;scheme=https;package=org.mozilla.fenix;end"),
+ )
+
+ openLinkInExternalApp.action.invoke(
+ tab,
+ HitResult.UNKNOWN("https://www.otherexample.com"),
+ )
+
+ verify(openAppLinkRedirectMock, times(2)).invoke(any(), anyBoolean(), any())
+ }
+
+ @Test
+ fun `Candidate 'Open in external app' allows for an additional validation for it to be shown`() {
+ val tab = createTab("https://www.mozilla.org")
+ val getAppLinkRedirectMock: AppLinksUseCases.GetAppLinkRedirect = mock()
+ doReturn(
+ AppLinkRedirect(mock(), null, null),
+ ).`when`(getAppLinkRedirectMock).invoke(eq("https://www.example.com"))
+ doReturn(
+ AppLinkRedirect(null, null, mock()),
+ ).`when`(getAppLinkRedirectMock).invoke(eq("intent:www.example.com#Intent;scheme=https;package=org.mozilla.fenix;end"))
+ val openAppLinkRedirectMock: AppLinksUseCases.OpenAppLinkRedirect = mock()
+ val appLinksUseCasesMock: AppLinksUseCases = mock()
+ doReturn(getAppLinkRedirectMock).`when`(appLinksUseCasesMock).appLinkRedirectIncludeInstall
+ doReturn(openAppLinkRedirectMock).`when`(appLinksUseCasesMock).openAppLink
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val openLinkInExternalApp = ContextMenuCandidate.createOpenInExternalAppCandidate(
+ testContext,
+ appLinksUseCasesMock,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.UNKNOWN("https://www.example.com"),
+ ),
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.UNKNOWN("intent:www.example.com#Intent;scheme=https;package=org.mozilla.fenix;end"),
+ ),
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.VIDEO("https://www.example.com"),
+ ),
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.AUDIO("https://www.example.com"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Copy email address'`() {
+ val parentView = CoordinatorLayout(testContext)
+
+ val copyEmailAddress = ContextMenuCandidate.createCopyEmailAddressCandidate(
+ testContext,
+ parentView,
+ snackbarDelegate,
+ )
+
+ // showFor
+
+ assertTrue(
+ copyEmailAddress.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ ),
+ )
+
+ assertTrue(
+ copyEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com"),
+ ),
+ )
+
+ assertFalse(
+ copyEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ copyEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("example@example.com"),
+ ),
+ )
+
+ // action
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ copyEmailAddress.action.invoke(
+ store.state.tabs.first(),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ )
+
+ assertTrue(snackbarDelegate.hasShownSnackbar)
+
+ val clipboardManager = testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ assertEquals(
+ "example@example.com",
+ clipboardManager.primaryClip!!.getItemAt(0).text,
+ )
+ }
+
+ @Test
+ fun `Candidate 'Copy email address' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val copyEmailAddress = ContextMenuCandidate.createCopyEmailAddressCandidate(
+ testContext,
+ mock(),
+ snackbarDelegate,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ copyEmailAddress.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ ),
+ )
+
+ assertFalse(
+ copyEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Share email address'`() {
+ val context = spy(testContext)
+
+ val shareEmailAddress = ContextMenuCandidate.createShareEmailAddressCandidate(context)
+
+ // showFor
+
+ assertTrue(
+ shareEmailAddress.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ ),
+ )
+
+ assertTrue(
+ shareEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com"),
+ ),
+ )
+
+ assertFalse(
+ shareEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ shareEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("example@example.com"),
+ ),
+ )
+
+ // action
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ shareEmailAddress.action.invoke(
+ store.state.tabs.first(),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ )
+
+ verify(context).startActivity(any())
+ }
+
+ @Test
+ fun `Candidate 'Share email address' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val shareEmailAddress = ContextMenuCandidate.createShareEmailAddressCandidate(
+ testContext,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ shareEmailAddress.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ ),
+ )
+
+ assertFalse(
+ shareEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Candidate 'Add to contacts'`() {
+ val context = spy(testContext)
+
+ val addToContacts = ContextMenuCandidate.createAddContactCandidate(context)
+
+ // showFor
+
+ assertTrue(
+ addToContacts.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ ),
+ )
+
+ assertTrue(
+ addToContacts.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com"),
+ ),
+ )
+
+ assertFalse(
+ addToContacts.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ )
+
+ assertFalse(
+ addToContacts.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("example@example.com"),
+ ),
+ )
+
+ // action
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla", private = true),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ addToContacts.action.invoke(
+ store.state.tabs.first(),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ )
+
+ verify(context).startActivity(any())
+ }
+
+ @Test
+ fun `Candidate 'Add to contacts' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val addToContacts = ContextMenuCandidate.createAddContactCandidate(
+ testContext,
+ additionalValidation,
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ addToContacts.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com"),
+ ),
+ )
+
+ assertFalse(
+ addToContacts.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com"),
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN SessionState with null EngineSession WHEN isUrlSchemeAllowed is called THEN it returns true`() {
+ val sessionState = TabSessionState(
+ content = mock(),
+ engineState = EngineState(engineSession = null),
+ )
+
+ assertTrue(sessionState.isUrlSchemeAllowed("http://mozilla.org"))
+ }
+
+ @Test
+ fun `GIVEN SessionState with no blocked url schemes WHEN isUrlSchemeAllowed is called THEN it returns true`() {
+ val noBlockedUrlSchemesEngineSession = Mockito.mock(EngineSession::class.java)
+ doReturn(emptyList<String>()).`when`(noBlockedUrlSchemesEngineSession).getBlockedSchemes()
+ val sessionState = TabSessionState(
+ content = mock(),
+ engineState = EngineState(engineSession = noBlockedUrlSchemesEngineSession),
+ )
+
+ assertTrue(sessionState.isUrlSchemeAllowed("http://mozilla.org"))
+ }
+
+ @Test
+ fun `GIVEN SessionState with blocked url schemes WHEN isUrlSchemeAllowed is called THEN it returns false if the url has that scheme`() {
+ val engineSessionWithBlockedUrlScheme = Mockito.mock(EngineSession::class.java)
+ doReturn(listOf("http")).`when`(engineSessionWithBlockedUrlScheme).getBlockedSchemes()
+ val sessionState = TabSessionState(
+ content = mock(),
+ engineState = EngineState(engineSession = engineSessionWithBlockedUrlScheme),
+ )
+
+ assertFalse(sessionState.isUrlSchemeAllowed("http://mozilla.org"))
+ assertFalse(sessionState.isUrlSchemeAllowed("hTtP://mozilla.org"))
+ assertFalse(sessionState.isUrlSchemeAllowed("HttP://www.mozilla.org"))
+ assertTrue(sessionState.isUrlSchemeAllowed("www.mozilla.org"))
+ assertTrue(sessionState.isUrlSchemeAllowed("https://mozilla.org"))
+ assertTrue(sessionState.isUrlSchemeAllowed("mozilla.org"))
+ assertTrue(sessionState.isUrlSchemeAllowed("/mozilla.org"))
+ assertTrue(sessionState.isUrlSchemeAllowed("content://http://mozilla.org"))
+ }
+
+ @Test
+ fun `GIVEN SessionState with blocked url schemes WHEN isUrlSchemeAllowed is called THEN it returns true if the url does not have that scheme`() {
+ val engineSessionWithBlockedUrlScheme = Mockito.mock(EngineSession::class.java)
+ doReturn(listOf("http")).`when`(engineSessionWithBlockedUrlScheme).getBlockedSchemes()
+ val sessionState = TabSessionState(
+ content = mock(),
+ engineState = EngineState(engineSession = engineSessionWithBlockedUrlScheme),
+ )
+
+ assertTrue(sessionState.isUrlSchemeAllowed("https://mozilla.org"))
+ }
+}
+
+private class TestSnackbarDelegate : SnackbarDelegate {
+ var hasShownSnackbar = false
+ var lastActionListener: ((v: View) -> Unit)? = null
+
+ override fun show(snackBarParentView: View, text: Int, duration: Int, action: Int, listener: ((v: View) -> Unit)?) {
+ hasShownSnackbar = true
+ lastActionListener = listener
+ }
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFeatureTest.kt b/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFeatureTest.kt
new file mode 100644
index 0000000000..057d020108
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFeatureTest.kt
@@ -0,0 +1,403 @@
+/* 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.contextmenu
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+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.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.any
+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.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+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.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class ContextMenuFeatureTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setUp() {
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ selectedTabId = "test-tab",
+ ),
+ )
+ }
+
+ @Test
+ fun `New HitResult for selected session will cause fragment transaction`() {
+ val fragmentManager = mockFragmentManager()
+
+ val (engineView, view) = mockEngineView()
+
+ val feature = ContextMenuFeature(
+ fragmentManager,
+ store,
+ ContextMenuCandidate.defaultCandidates(testContext, mock(), mock(), mock()),
+ engineView,
+ mock(),
+ )
+
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(
+ "test-tab",
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(fragmentManager).beginTransaction()
+ verify(view).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+
+ @Test
+ fun `New HitResult for selected session will not cause fragment transaction if feature is stopped`() {
+ val fragmentManager = mockFragmentManager()
+
+ val (engineView, view) = mockEngineView()
+
+ val feature = ContextMenuFeature(
+ fragmentManager,
+ store,
+ ContextMenuCandidate.defaultCandidates(testContext, mock(), mock(), mock()),
+ engineView,
+ mock(),
+ )
+
+ feature.start()
+ feature.stop()
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(
+ "test-tab",
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(fragmentManager, never()).beginTransaction()
+ verify(view, never()).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+
+ @Test
+ fun `Feature will re-attach to already existing fragment`() {
+ val fragment: ContextMenuFragment = mock()
+ doReturn("test-tab").`when`(fragment).sessionId
+
+ val fragmentManager: FragmentManager = mock()
+ doReturn(fragment).`when`(fragmentManager).findFragmentByTag(any())
+
+ val (engineView, view) = mockEngineView()
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(
+ "test-tab",
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+
+ val feature = ContextMenuFeature(
+ fragmentManager,
+ store,
+ ContextMenuCandidate.defaultCandidates(testContext, mock(), mock(), mock()),
+ engineView,
+ mock(),
+ )
+
+ feature.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(fragment).feature = feature
+ verify(view, never()).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+
+ @Test
+ fun `Already existing fragment will be removed if session has no HitResult set anymore`() {
+ val fragment: ContextMenuFragment = mock()
+ doReturn("test-tab").`when`(fragment).sessionId
+
+ val transaction: FragmentTransaction = mock()
+
+ val fragmentManager: FragmentManager = mock()
+ doReturn(fragment).`when`(fragmentManager).findFragmentByTag(any())
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+ doReturn(transaction).`when`(transaction).remove(fragment)
+
+ val (engineView, view) = mockEngineView()
+
+ val feature = ContextMenuFeature(
+ fragmentManager,
+ store,
+ ContextMenuCandidate.defaultCandidates(testContext, mock(), mock(), mock()),
+ engineView,
+ mock(),
+ )
+
+ feature.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(fragmentManager).beginTransaction()
+ verify(transaction).remove(fragment)
+
+ verify(view, never()).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+
+ fun `Already existing fragment will be removed if session does not exist anymore`() {
+ val fragment: ContextMenuFragment = mock()
+ doReturn("test-tab").`when`(fragment).sessionId
+
+ val transaction: FragmentTransaction = mock()
+
+ val fragmentManager: FragmentManager = mock()
+ doReturn(fragment).`when`(fragmentManager).findFragmentByTag(any())
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+ doReturn(transaction).`when`(transaction).remove(fragment)
+
+ val (engineView, view) = mockEngineView()
+
+ val feature = ContextMenuFeature(
+ fragmentManager,
+ store,
+ ContextMenuCandidate.defaultCandidates(testContext, mock(), mock(), mock()),
+ engineView,
+ mock(),
+ )
+
+ store.dispatch(TabListAction.RemoveTabAction("test-tab"))
+ .joinBlocking()
+
+ feature.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(fragmentManager).beginTransaction()
+ verify(transaction).remove(fragment)
+
+ verify(view, never()).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+
+ @Test
+ fun `No dialog will be shown if no item wants to be shown`() {
+ val fragmentManager = mockFragmentManager()
+
+ val candidate = ContextMenuCandidate(
+ id = "test-id",
+ label = "Test Item",
+ showFor = { _, _ -> false },
+ action = { _, _ -> Unit },
+ )
+
+ val (engineView, view) = mockEngineView()
+
+ val feature = ContextMenuFeature(
+ fragmentManager,
+ store,
+ listOf(candidate),
+ engineView,
+ ContextMenuUseCases(mock()),
+ )
+
+ feature.showContextMenu(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ )
+
+ verify(fragmentManager, never()).beginTransaction()
+ verify(view, never()).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+
+ @Test
+ fun `Cancelling context menu item will consume HitResult`() {
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(
+ "test-tab",
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+
+ val (engineView, _) = mockEngineView()
+
+ val feature = ContextMenuFeature(
+ mockFragmentManager(),
+ store,
+ ContextMenuCandidate.defaultCandidates(testContext, mock(), mock(), mock()),
+ engineView,
+ ContextMenuUseCases(store),
+ )
+
+ assertNotNull(store.state.findTab("test-tab")!!.content.hitResult)
+
+ feature.onMenuCancelled("test-tab")
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTab("test-tab")!!.content.hitResult)
+ }
+
+ @Test
+ fun `Selecting context menu item will invoke action of candidate and consume HitResult`() {
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(
+ "test-tab",
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+
+ val (engineView, view) = mockEngineView()
+ var actionInvoked = false
+
+ val candidate = ContextMenuCandidate(
+ id = "test-id",
+ label = "Test Item",
+ showFor = { _, _ -> true },
+ action = { _, _ -> actionInvoked = true },
+ )
+
+ val feature = ContextMenuFeature(
+ mockFragmentManager(),
+ store,
+ listOf(candidate),
+ engineView,
+ ContextMenuUseCases(store),
+ )
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNotNull(store.state.findTab("test-tab")!!.content.hitResult)
+ assertFalse(actionInvoked)
+
+ feature.onMenuItemSelected("test-tab", "test-id")
+
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNull(store.state.findTab("test-tab")!!.content.hitResult)
+ assertTrue(actionInvoked)
+ verify(view, never()).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+
+ @Test
+ fun `Selecting context menu item will emit a click fact`() {
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ ContentAction.UpdateHitResultAction(
+ "test-tab",
+ HitResult.UNKNOWN("https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+
+ val (engineView, _) = mockEngineView()
+ val candidate = ContextMenuCandidate(
+ id = "test-id",
+ label = "Test Item",
+ showFor = { _, _ -> true },
+ action = { _, _ -> }, // noop
+ )
+
+ val feature = ContextMenuFeature(
+ mockFragmentManager(),
+ store,
+ listOf(candidate),
+ engineView,
+ ContextMenuUseCases(store),
+ )
+
+ CollectionProcessor.withFactCollection { facts ->
+ feature.onMenuItemSelected("test-tab", candidate.id)
+
+ assertEquals(1, facts.size)
+
+ val fact = facts[0]
+ assertEquals(Component.FEATURE_CONTEXTMENU, fact.component)
+ assertEquals(Action.CLICK, fact.action)
+ assertEquals("item", fact.item)
+ assertEquals("test-id", fact.metadata?.get("item"))
+ }
+ }
+
+ private fun mockFragmentManager(): FragmentManager {
+ val fragmentManager: FragmentManager = mock()
+
+ val transaction: FragmentTransaction = mock()
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+
+ return fragmentManager
+ }
+
+ private fun mockEngineView(): Pair<EngineView, View> {
+ val actualView: View = mock()
+
+ val engineView = mock<EngineView>().also {
+ `when`(it.asView()).thenReturn(actualView)
+ }
+
+ return Pair(engineView, actualView)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFragmentTest.kt b/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFragmentTest.kt
new file mode 100644
index 0000000000..b3e312c4c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFragmentTest.kt
@@ -0,0 +1,347 @@
+/* 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.contextmenu
+
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.HitResult
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+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.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ContextMenuFragmentTest {
+ @Test
+ fun `Build dialog`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val title = "Hello World"
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ assertNotNull(dialog)
+
+ verify(fragment).createDialogTitleView(any())
+ verify(fragment).createDialogContentView(any())
+ }
+
+ @Test
+ fun `Dialog title view`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val title = "Hello World"
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ val inflater = LayoutInflater.from(testContext)
+ val view = fragment.createDialogTitleView(inflater)
+
+ assertEquals(
+ "Hello World",
+ view.findViewById<TextView>(R.id.titleView).text,
+ )
+ }
+
+ @Test
+ fun `CLicking title view expands title`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val title = "Hello World"
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ val inflater = LayoutInflater.from(testContext)
+ val view = fragment.createDialogTitleView(inflater)
+ val titleView = view.findViewById<TextView>(R.id.titleView)
+
+ titleView.performClick()
+
+ assertEquals(
+ 15,
+ titleView.maxLines,
+ )
+ }
+
+ @Test
+ fun `Dialog content view`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val title = "Hello World"
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val inflater = LayoutInflater.from(testContext)
+ val view = fragment.createDialogContentView(inflater)
+
+ val adapter = view.findViewById<RecyclerView>(R.id.recyclerView).adapter as ContextMenuAdapter
+
+ assertEquals(3, adapter.itemCount)
+
+ val parent = LinearLayout(testContext)
+
+ val holder = adapter.onCreateViewHolder(parent, 0)
+
+ adapter.bindViewHolder(holder, 0)
+ assertEquals("Item A", holder.labelView.text)
+
+ adapter.bindViewHolder(holder, 1)
+ assertEquals("Item B", holder.labelView.text)
+
+ adapter.bindViewHolder(holder, 2)
+ assertEquals("Item C", holder.labelView.text)
+ }
+
+ @Test
+ fun `Clicking context menu item notifies fragment`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val title = "Hello World"
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+ doReturn(testContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val inflater = LayoutInflater.from(testContext)
+ val view = fragment.createDialogContentView(inflater)
+
+ val adapter = view.findViewById<RecyclerView>(R.id.recyclerView).adapter as ContextMenuAdapter
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), 0)
+ adapter.bindViewHolder(holder, 0)
+
+ holder.labelView.performClick()
+
+ verify(fragment).onItemSelected(0)
+ }
+
+ @Test
+ fun `On selection fragment notifies feature and dismisses dialog`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val title = "Hello World"
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val feature: ContextMenuFeature = mock()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+ fragment.feature = feature
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.onItemSelected(0)
+
+ verify(feature).onMenuItemSelected(tab.id, "A")
+ verify(fragment).dismiss()
+ }
+
+ @Test
+ fun `Fragment shows correct title for IMAGE HitResult`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val titleString = "test title"
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.IMAGE("https://www.mozilla.org", titleString)
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals(titleString, fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows src as title for IMAGE HitResult with blank title`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val titleString = " "
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.IMAGE("https://www.mozilla.org", titleString)
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("https://www.mozilla.org", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows src as title for IMAGE HitResult with null title`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.IMAGE("https://www.mozilla.org")
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("https://www.mozilla.org", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows uri as title for IMAGE_SRC HitResult`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.IMAGE_SRC("https://www.mozilla.org", "https://another.com")
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("https://another.com", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows src as title for UNKNOWN HitResult`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.UNKNOWN("https://www.mozilla.org")
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("https://www.mozilla.org", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows src as title for AUDIO HitResult with blank title`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val titleString = " "
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.AUDIO("https://www.mozilla.org", titleString)
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("https://www.mozilla.org", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows src as title for AUDIO HitResult with null title`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.AUDIO("https://www.mozilla.org")
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("https://www.mozilla.org", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows src as title for VIDEO HitResult with blank title`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val titleString = " "
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.VIDEO("https://www.mozilla.org", titleString)
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("https://www.mozilla.org", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows src as title for VIDEO HitResult with null title`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.VIDEO("https://www.mozilla.org")
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("https://www.mozilla.org", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows about blank as title for EMAIL HitResult`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.EMAIL("https://www.mozilla.org")
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("about:blank", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows about blank as title for GEO HitResult`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.GEO("https://www.mozilla.org")
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("about:blank", fragment.title)
+ }
+
+ @Test
+ fun `Fragment shows about blank as title for PHONE HitResult`() {
+ val ids = listOf("A", "B", "C")
+ val labels = listOf("Item A", "Item B", "Item C")
+ val tab = createTab("https://www.mozilla.org")
+ val additionalNote = "Additional note"
+
+ val hitResult = HitResult.PHONE("https://www.mozilla.org")
+ val title = hitResult.getLink()
+
+ val fragment = spy(ContextMenuFragment.create(tab, title, ids, labels, additionalNote))
+
+ assertEquals("about:blank", fragment.title)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/DefaultSelectionActionDelegateTest.kt b/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/DefaultSelectionActionDelegateTest.kt
new file mode 100644
index 0000000000..7f034889e7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/DefaultSelectionActionDelegateTest.kt
@@ -0,0 +1,267 @@
+/* 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.contextmenu
+
+import android.content.res.Resources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.search.SearchAdapter
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+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.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class DefaultSelectionActionDelegateTest {
+
+ val selectedRegularText = "mozilla"
+ val selectedEmailText = "test@mozilla.org"
+ val selectedPhoneText = "555-5555"
+ var lambdaValue: String? = null
+ val shareClicked: (String) -> Unit = { lambdaValue = it }
+ val emailClicked: (String) -> Unit = { lambdaValue = it }
+ val phoneClicked: (String) -> Unit = { lambdaValue = it }
+
+ @Before
+ fun setup() {
+ lambdaValue = null
+ }
+
+ @Test
+ fun `are non-private regular actions available`() {
+ val searchAdapter = mock<SearchAdapter> {
+ whenever(isPrivateSession()).thenReturn(false)
+ }
+ val delegate = DefaultSelectionActionDelegate(
+ searchAdapter,
+ getTestResources(),
+ shareClicked,
+ emailClicked,
+ phoneClicked,
+ )
+
+ assertTrue(delegate.isActionAvailable(SEARCH, selectedRegularText))
+ assertTrue(delegate.isActionAvailable(SHARE, selectedRegularText))
+ assertFalse(delegate.isActionAvailable(SEARCH_PRIVATELY, selectedRegularText))
+ assertFalse(delegate.isActionAvailable(EMAIL, selectedRegularText))
+ assertFalse(delegate.isActionAvailable(CALL, selectedRegularText))
+ }
+
+ @Test
+ fun `are non-private non-share actions available`() {
+ val searchAdapter = mock<SearchAdapter> {
+ whenever(isPrivateSession()).thenReturn(false)
+ }
+ val delegate = DefaultSelectionActionDelegate(
+ searchAdapter,
+ getTestResources(),
+ )
+
+ assertTrue(delegate.isActionAvailable(SEARCH, selectedRegularText))
+ assertFalse(delegate.isActionAvailable(SHARE, selectedRegularText))
+ assertFalse(delegate.isActionAvailable(SEARCH_PRIVATELY, selectedRegularText))
+ }
+
+ @Test
+ fun `is email available when passed in and email text selected`() {
+ val searchAdapter = mock<SearchAdapter> {
+ whenever(isPrivateSession()).thenReturn(false)
+ }
+ val delegate = DefaultSelectionActionDelegate(
+ searchAdapter,
+ getTestResources(),
+ emailTextClicked = emailClicked,
+ )
+
+ assertTrue(delegate.isActionAvailable(EMAIL, selectedEmailText))
+ assertTrue(delegate.isActionAvailable(EMAIL, " $selectedEmailText "))
+ assertFalse(delegate.isActionAvailable(EMAIL, selectedRegularText))
+ assertFalse(delegate.isActionAvailable(EMAIL, selectedPhoneText))
+ assertFalse(delegate.isActionAvailable(EMAIL, " $selectedPhoneText "))
+ }
+
+ @Test
+ fun `is call available when passed in and call text selected`() {
+ val searchAdapter = mock<SearchAdapter> {
+ whenever(isPrivateSession()).thenReturn(false)
+ }
+ val delegate = DefaultSelectionActionDelegate(
+ searchAdapter,
+ getTestResources(),
+ callTextClicked = phoneClicked,
+ )
+
+ assertTrue(delegate.isActionAvailable(CALL, selectedPhoneText))
+ assertFalse(delegate.isActionAvailable(CALL, selectedRegularText))
+ assertFalse(delegate.isActionAvailable(CALL, selectedEmailText))
+ }
+
+ @Test
+ fun `are private actions available`() {
+ val searchAdapter = mock<SearchAdapter> {
+ whenever(isPrivateSession()).thenReturn(true)
+ }
+ val delegate = DefaultSelectionActionDelegate(
+ searchAdapter,
+ getTestResources(),
+ shareClicked,
+ )
+
+ assertTrue(delegate.isActionAvailable(SEARCH_PRIVATELY, selectedRegularText))
+ assertTrue(delegate.isActionAvailable(SHARE, selectedRegularText))
+ assertFalse(delegate.isActionAvailable(SEARCH, selectedRegularText))
+ }
+
+ @Test
+ fun `when share ID is passed to perform action it should invoke the lambda`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), shareClicked)
+
+ delegate.performAction(SHARE, "some selected text")
+
+ assertEquals(lambdaValue, "some selected text")
+ }
+
+ @Test
+ fun `when email ID is passed to perform action it should invoke the lambda`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), emailTextClicked = emailClicked)
+
+ delegate.performAction(EMAIL, selectedEmailText)
+
+ assertEquals(lambdaValue, selectedEmailText)
+
+ delegate.performAction(EMAIL, " $selectedEmailText ")
+
+ assertEquals(lambdaValue, selectedEmailText)
+ }
+
+ @Test
+ fun `when call ID is passed to perform action it should invoke the lambda`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), callTextClicked = phoneClicked)
+
+ delegate.performAction(CALL, selectedPhoneText)
+
+ assertEquals(lambdaValue, selectedPhoneText)
+
+ delegate.performAction(CALL, " $selectedPhoneText ")
+
+ assertEquals(lambdaValue, selectedPhoneText)
+ }
+
+ @Test
+ fun `when unknown ID is passed to performAction it should not perform a search`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), shareClicked)
+
+ delegate.performAction("unrecognized string", "some selected text")
+
+ verify(adapter, times(0)).sendSearch(anyBoolean(), anyString())
+ }
+
+ @Test
+ fun `when unknown ID is passed to performAction it not consume the action`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), shareClicked)
+
+ val result = delegate.performAction("unrecognized string", "some selected text")
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `when search ID is passed to performAction it should perform a search`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), shareClicked)
+
+ delegate.performAction(SEARCH, "some selected text")
+
+ verify(adapter, times(1)).sendSearch(false, "some selected text")
+ }
+
+ @Test
+ fun `when search ID is passed to performAction it should consume the action`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), shareClicked)
+
+ val result = delegate.performAction(SEARCH, "some selected text")
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `when private search ID is passed to performAction it should perform a private normal search`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), shareClicked)
+
+ delegate.performAction(SEARCH_PRIVATELY, "some selected text")
+
+ verify(adapter, times(1)).sendSearch(true, "some selected text")
+ }
+
+ @Test
+ fun `when private search ID is passed to performAction it should consume the action`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), shareClicked)
+
+ val result = delegate.performAction(SEARCH_PRIVATELY, "some selected text")
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `when calling performAction check that Facts are emitted`() {
+ val adapter = mock<SearchAdapter>()
+ val delegate =
+ DefaultSelectionActionDelegate(adapter, getTestResources(), shareClicked)
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ assertEquals(0, facts.size)
+
+ delegate.performAction(SEARCH_PRIVATELY, "some selected text")
+
+ assertEquals(1, facts.size)
+
+ delegate.performAction(CALL, selectedPhoneText)
+
+ assertEquals(2, facts.size)
+ }
+}
+
+fun getTestResources() = mock<Resources> {
+ whenever(getString(R.string.mozac_selection_context_menu_search_2)).thenReturn("Search")
+ whenever(getString(R.string.mozac_selection_context_menu_search_privately_2))
+ .thenReturn("search privately")
+ whenever(getString(R.string.mozac_selection_context_menu_share)).thenReturn("share")
+ whenever(getString(R.string.mozac_selection_context_menu_email)).thenReturn("email")
+ whenever(getString(R.string.mozac_selection_context_menu_call)).thenReturn("call")
+}
diff --git a/mobile/android/android-components/components/feature/contextmenu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/contextmenu/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/contextmenu/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/contextmenu/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/contextmenu/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/contextmenu/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/customtabs/README.md b/mobile/android/android-components/components/feature/customtabs/README.md
new file mode 100644
index 0000000000..0638777429
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/README.md
@@ -0,0 +1,30 @@
+# [Android Components](../../../README.md) > Feature > Custom Tabs
+
+ A component for providing [Custom Tabs](https://developer.chrome.com/multidevice/android/customtabs) functionality in browsers.
+
+## Usage
+
+### 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-customtabs:{latest-version}"
+```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Description |
+|--------|-------------------|------------------------------------------|
+| CLICK | close | The user clicked on the close button |
+| CLICK | action_button | The user clicked on an action button |
+
+In addition to the facts emitted above this feature will inject an addition extra (`customTab: true`) into the `BrowserMenuBuilder` passed to the `BrowserToolbar` (see [browser-menu](../../browser/menu/README.md)).
+
+## 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/customtabs/build.gradle b/mobile/android/android-components/components/feature/customtabs/build.gradle
new file mode 100644
index 0000000000..bd29893247
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/build.gradle
@@ -0,0 +1,64 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.customtabs'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-menu')
+ implementation project(':browser-state')
+ implementation project(':browser-toolbar')
+ implementation project(':concept-engine')
+ implementation project(':concept-fetch')
+ implementation project(':concept-menu')
+ implementation project(':feature-session')
+ implementation project(':feature-intent')
+ implementation project(':feature-tabs')
+ implementation project(':service-digitalassetlinks')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ api ComponentsDependencies.androidx_browser
+
+ 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/customtabs/proguard-rules.pro b/mobile/android/android-components/components/feature/customtabs/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/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/customtabs/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/customtabs/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/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/customtabs/src/main/java/mozilla/components/feature/customtabs/AbstractCustomTabsService.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/AbstractCustomTabsService.kt
new file mode 100644
index 0000000000..7a96b72cfc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/AbstractCustomTabsService.kt
@@ -0,0 +1,135 @@
+/* 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.customtabs
+
+import android.app.Service
+import android.net.Uri
+import android.os.Binder
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import androidx.browser.customtabs.CustomTabsService
+import androidx.browser.customtabs.CustomTabsSessionToken
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.customtabs.feature.OriginVerifierFeature
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.customtabs.store.SaveCreatorPackageNameAction
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+/**
+ * Maximum number of speculative connections we will open when an app calls into
+ * [AbstractCustomTabsService.mayLaunchUrl] with a list of URLs.
+ */
+private const val MAX_SPECULATIVE_URLS = 50
+
+/**
+ * [Service] providing Custom Tabs related functionality.
+ */
+abstract class AbstractCustomTabsService : CustomTabsService() {
+ private val logger = Logger("CustomTabsService")
+ private val scope = MainScope()
+
+ abstract val engine: Engine
+ abstract val customTabsServiceStore: CustomTabsServiceStore
+ open val relationChecker: RelationChecker? = null
+
+ @VisibleForTesting
+ internal val verifier by lazy {
+ relationChecker?.let { checker ->
+ OriginVerifierFeature(packageManager, checker) { customTabsServiceStore.dispatch(it) }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ scope.cancel()
+ }
+
+ override fun warmup(flags: Long): Boolean {
+ // We need to run this on the main thread since that's where GeckoRuntime expects to get initialized (if needed)
+ return runBlocking(Main) {
+ engine.warmUp()
+ true
+ }
+ }
+
+ override fun requestPostMessageChannel(sessionToken: CustomTabsSessionToken, postMessageOrigin: Uri): Boolean {
+ return false
+ }
+
+ /**
+ * Saves the package name of the app creating the custom tab when a new session is started.
+ */
+ override fun newSession(sessionToken: CustomTabsSessionToken): Boolean {
+ // Extract the process UID of the app creating the custom tab.
+ val uid = Binder.getCallingUid()
+ // Only save the package if exactly one package name maps to the process UID.
+ val packageName = packageManager.getPackagesForUid(uid)?.singleOrNull()
+
+ if (!packageName.isNullOrEmpty()) {
+ customTabsServiceStore.dispatch(SaveCreatorPackageNameAction(sessionToken, packageName))
+ }
+ return true
+ }
+
+ override fun extraCommand(commandName: String, args: Bundle?): Bundle? = null
+
+ override fun mayLaunchUrl(
+ sessionToken: CustomTabsSessionToken,
+ url: Uri?,
+ extras: Bundle?,
+ otherLikelyBundles: List<Bundle>?,
+ ): Boolean {
+ logger.debug("Opening speculative connections")
+
+ // Most likely URL for a future navigation: Open a speculative connection.
+ url?.let { engine.speculativeConnect(it.toString()) }
+
+ // A list of other likely URLs. Let's open a speculative connection for them up to a limit.
+ otherLikelyBundles?.take(MAX_SPECULATIVE_URLS)?.forEach { bundle ->
+ bundle.getParcelableCompat(KEY_URL, Uri::class.java)?.let { uri ->
+ engine.speculativeConnect(uri.toString())
+ }
+ }
+
+ return true
+ }
+
+ override fun postMessage(sessionToken: CustomTabsSessionToken, message: String, extras: Bundle?) =
+ RESULT_FAILURE_DISALLOWED
+
+ override fun validateRelationship(
+ sessionToken: CustomTabsSessionToken,
+ @Relation relation: Int,
+ origin: Uri,
+ extras: Bundle?,
+ ): Boolean {
+ val verifier = verifier
+ val state = customTabsServiceStore.state.tabs[sessionToken]
+ return if (verifier != null && state != null) {
+ scope.launch(Main) {
+ val result = verifier.verify(state, sessionToken, relation, origin)
+ sessionToken.callback?.onRelationshipValidationResult(relation, origin, result, extras)
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ override fun updateVisuals(sessionToken: CustomTabsSessionToken, bundle: Bundle?): Boolean {
+ return false
+ }
+
+ override fun receiveFile(sessionToken: CustomTabsSessionToken, uri: Uri, purpose: Int, extras: Bundle?): Boolean {
+ return false
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt
new file mode 100644
index 0000000000..2d5fe96e9a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt
@@ -0,0 +1,280 @@
+/* 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.customtabs
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.browser.customtabs.CustomTabColorSchemeParams
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.browser.customtabs.CustomTabsIntent.ColorScheme
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_CLOSE_BUTTON_ICON
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_COLOR_SCHEME
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_COLOR_SCHEME_PARAMS
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_ENABLE_URLBAR_HIDING
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_MENU_ITEMS
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_NAVIGATION_BAR_COLOR
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_NAVIGATION_BAR_DIVIDER_COLOR
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SHARE_STATE
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TINT_ACTION_BUTTON
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TOOLBAR_COLOR
+import androidx.browser.customtabs.CustomTabsIntent.KEY_DESCRIPTION
+import androidx.browser.customtabs.CustomTabsIntent.KEY_ICON
+import androidx.browser.customtabs.CustomTabsIntent.KEY_ID
+import androidx.browser.customtabs.CustomTabsIntent.KEY_MENU_ITEM_TITLE
+import androidx.browser.customtabs.CustomTabsIntent.KEY_PENDING_INTENT
+import androidx.browser.customtabs.CustomTabsIntent.NO_TITLE
+import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_DEFAULT
+import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON
+import androidx.browser.customtabs.CustomTabsIntent.SHOW_PAGE_TITLE
+import androidx.browser.customtabs.CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID
+import androidx.browser.customtabs.CustomTabsSessionToken
+import androidx.browser.customtabs.TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY
+import mozilla.components.browser.state.state.ColorSchemeParams
+import mozilla.components.browser.state.state.ColorSchemes
+import mozilla.components.browser.state.state.CustomTabActionButtonConfig
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabMenuItem
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.utils.toSafeBundle
+import mozilla.components.support.utils.toSafeIntent
+import kotlin.math.max
+
+/**
+ * Checks if the provided intent is a custom tab intent.
+ *
+ * @param intent the intent to check.
+ * @return true if the intent is a custom tab intent, otherwise false.
+ */
+fun isCustomTabIntent(intent: Intent) = isCustomTabIntent(intent.toSafeIntent())
+
+/**
+ * Checks if the provided intent is a custom tab intent.
+ *
+ * @param safeIntent the intent to check, wrapped as a SafeIntent.
+ * @return true if the intent is a custom tab intent, otherwise false.
+ */
+fun isCustomTabIntent(safeIntent: SafeIntent) = safeIntent.hasExtra(EXTRA_SESSION)
+
+/**
+ * Checks if the provided intent is a trusted web activity intent.
+ *
+ * @param intent the intent to check.
+ * @return true if the intent is a trusted web activity intent, otherwise false.
+ */
+fun isTrustedWebActivityIntent(intent: Intent) = isTrustedWebActivityIntent(intent.toSafeIntent())
+
+/**
+ * Checks if the provided intent is a trusted web activity intent.
+ *
+ * @param safeIntent the intent to check, wrapped as a SafeIntent.
+ * @return true if the intent is a trusted web activity intent, otherwise false.
+ */
+fun isTrustedWebActivityIntent(safeIntent: SafeIntent) = isCustomTabIntent(safeIntent) &&
+ safeIntent.getBooleanExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false)
+
+/**
+ * Creates a [CustomTabConfig] instance based on the provided [Intent].
+ *
+ * @param intent The [Intent] wrapped as a [SafeIntent], which is processed to extract configuration data.
+ * @param resources Optional [Resources] to verify that only icons of a max size are provided.
+ *
+ * @return the configured [CustomTabConfig].
+ */
+fun createCustomTabConfigFromIntent(intent: Intent, resources: Resources?): CustomTabConfig {
+ val safeIntent = intent.toSafeIntent()
+
+ return CustomTabConfig(
+ colorScheme = safeIntent.getColorExtra(EXTRA_COLOR_SCHEME),
+ colorSchemes = getColorSchemes(safeIntent),
+ closeButtonIcon = getCloseButtonIcon(safeIntent, resources),
+ enableUrlbarHiding = safeIntent.getBooleanExtra(EXTRA_ENABLE_URLBAR_HIDING, false),
+ actionButtonConfig = getActionButtonConfig(safeIntent),
+ showShareMenuItem = (safeIntent.getIntExtra(EXTRA_SHARE_STATE, SHARE_STATE_DEFAULT) == SHARE_STATE_ON),
+ menuItems = getMenuItems(safeIntent),
+ exitAnimations = safeIntent.getBundleExtra(EXTRA_EXIT_ANIMATION_BUNDLE)?.unsafe,
+ titleVisible = safeIntent.getIntExtra(EXTRA_TITLE_VISIBILITY_STATE, NO_TITLE) == SHOW_PAGE_TITLE,
+ sessionToken = if (intent.extras != null) {
+ // getSessionTokenFromIntent throws if extras is null
+ CustomTabsSessionToken.getSessionTokenFromIntent(intent)
+ } else {
+ null
+ },
+ externalAppType = ExternalAppType.CUSTOM_TAB,
+ )
+}
+
+@ColorInt
+private fun SafeIntent.getColorExtra(name: String): Int? =
+ if (hasExtra(name)) getIntExtra(name, 0) else null
+
+private fun getCloseButtonIcon(intent: SafeIntent, resources: Resources?): Bitmap? {
+ val icon = try {
+ intent.getParcelableExtra(EXTRA_CLOSE_BUTTON_ICON, Bitmap::class.java)
+ } catch (e: ClassCastException) {
+ null
+ }
+ val maxSize = resources?.getDimension(R.dimen.mozac_feature_customtabs_max_close_button_size) ?: Float.MAX_VALUE
+
+ return if (icon != null && max(icon.width, icon.height) <= maxSize) {
+ icon
+ } else {
+ null
+ }
+}
+
+private fun getColorSchemes(safeIntent: SafeIntent): ColorSchemes? {
+ val defaultColorSchemeParams = getDefaultSchemeColorParams(safeIntent)
+ val lightColorSchemeParams = getLightColorSchemeParams(safeIntent)
+ val darkColorSchemeParams = getDarkColorSchemeParams(safeIntent)
+
+ return if (allNull(defaultColorSchemeParams, lightColorSchemeParams, darkColorSchemeParams)) {
+ null
+ } else {
+ ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ lightColorSchemeParams = lightColorSchemeParams,
+ darkColorSchemeParams = darkColorSchemeParams,
+ )
+ }
+}
+
+/**
+ * Processes the given [SafeIntent] to extract possible default [CustomTabColorSchemeParams]
+ * properties.
+ *
+ * @param safeIntent the [SafeIntent] to process.
+ *
+ * @return the derived [ColorSchemeParams] or null if the [SafeIntent] had no default
+ * [CustomTabColorSchemeParams] properties.
+ *
+ * @see [CustomTabsIntent.Builder.setDefaultColorSchemeParams].
+ */
+private fun getDefaultSchemeColorParams(safeIntent: SafeIntent): ColorSchemeParams? {
+ val toolbarColor = safeIntent.getColorExtra(EXTRA_TOOLBAR_COLOR)
+ val secondaryToolbarColor = safeIntent.getColorExtra(EXTRA_SECONDARY_TOOLBAR_COLOR)
+ val navigationBarColor = safeIntent.getColorExtra(EXTRA_NAVIGATION_BAR_COLOR)
+ val navigationBarDividerColor = safeIntent.getColorExtra(EXTRA_NAVIGATION_BAR_DIVIDER_COLOR)
+
+ return if (allNull(
+ toolbarColor,
+ secondaryToolbarColor,
+ navigationBarColor,
+ navigationBarDividerColor,
+ )
+ ) {
+ null
+ } else {
+ ColorSchemeParams(
+ toolbarColor = toolbarColor,
+ secondaryToolbarColor = secondaryToolbarColor,
+ navigationBarColor = navigationBarColor,
+ navigationBarDividerColor = navigationBarDividerColor,
+ )
+ }
+}
+
+private fun getLightColorSchemeParams(safeIntent: SafeIntent) =
+ getColorSchemeParams(safeIntent, CustomTabsIntent.COLOR_SCHEME_LIGHT)
+
+private fun getDarkColorSchemeParams(safeIntent: SafeIntent) =
+ getColorSchemeParams(safeIntent, CustomTabsIntent.COLOR_SCHEME_DARK)
+
+/**
+ * Processes the given [SafeIntent] to extract possible [CustomTabColorSchemeParams] properties for
+ * the given [colorScheme].
+ *
+ * @param safeIntent The [SafeIntent] to process.
+ * @param colorScheme The [ColorScheme] to get the [ColorSchemeParams] for.
+ *
+ * @return the derived [ColorSchemeParams] for the given [ColorScheme], or null if the [SafeIntent]
+ * had no [CustomTabColorSchemeParams] properties for the [ColorScheme].
+ *
+ * @see [CustomTabsIntent.Builder.setColorSchemeParams].
+ */
+private fun getColorSchemeParams(safeIntent: SafeIntent, @ColorScheme colorScheme: Int): ColorSchemeParams? {
+ val bundle = safeIntent.getColorSchemeParamsBundle()?.get(colorScheme)
+
+ val toolbarColor = bundle?.getNullableSafeValue(EXTRA_TOOLBAR_COLOR)
+ val secondaryToolbarColor = bundle?.getNullableSafeValue(EXTRA_SECONDARY_TOOLBAR_COLOR)
+ val navigationBarColor = bundle?.getNullableSafeValue(EXTRA_NAVIGATION_BAR_COLOR)
+ val navigationBarDividerColor = bundle?.getNullableSafeValue(EXTRA_NAVIGATION_BAR_DIVIDER_COLOR)
+
+ return if (allNull(toolbarColor, secondaryToolbarColor, navigationBarColor, navigationBarDividerColor)) {
+ null
+ } else {
+ ColorSchemeParams(
+ toolbarColor = toolbarColor,
+ secondaryToolbarColor = secondaryToolbarColor,
+ navigationBarColor = navigationBarColor,
+ navigationBarDividerColor = navigationBarDividerColor,
+ )
+ }
+}
+
+private fun <T> allNull(vararg value: T?) = value.toList().all { it == null }
+
+@VisibleForTesting
+internal fun SafeIntent.getColorSchemeParamsBundle() = extras?.let {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ @Suppress("DEPRECATION")
+ it.getSparseParcelableArray(EXTRA_COLOR_SCHEME_PARAMS)
+ } else {
+ it.getSparseParcelableArray(EXTRA_COLOR_SCHEME_PARAMS, Bundle::class.java)
+ }
+}
+
+private fun Bundle.getNullableSafeValue(key: String) =
+ if (containsKey(key)) toSafeBundle().getInt(key) else null
+
+private fun getActionButtonConfig(intent: SafeIntent): CustomTabActionButtonConfig? {
+ val actionButtonBundle = intent.getBundleExtra(EXTRA_ACTION_BUTTON_BUNDLE) ?: return null
+ val description = actionButtonBundle.getString(KEY_DESCRIPTION)
+ val icon = actionButtonBundle.getParcelable(KEY_ICON, Bitmap::class.java)
+ val pendingIntent = actionButtonBundle.getParcelable(KEY_PENDING_INTENT, PendingIntent::class.java)
+ val id = actionButtonBundle.getInt(KEY_ID, TOOLBAR_ACTION_BUTTON_ID)
+ val tint = intent.getBooleanExtra(EXTRA_TINT_ACTION_BUTTON, false)
+
+ return if (description != null && icon != null && pendingIntent != null) {
+ CustomTabActionButtonConfig(
+ id = id,
+ description = description,
+ icon = icon,
+ pendingIntent = pendingIntent,
+ tint = tint,
+ )
+ } else {
+ null
+ }
+}
+
+private fun getMenuItems(intent: SafeIntent): List<CustomTabMenuItem> =
+ intent.getParcelableArrayListExtra(EXTRA_MENU_ITEMS, Parcelable::class.java).orEmpty()
+ .mapNotNull { menuItemBundle ->
+ val bundle = (menuItemBundle as? Bundle)?.toSafeBundle()
+ val name = bundle?.getString(KEY_MENU_ITEM_TITLE)
+ val pendingIntent = bundle?.getParcelable(KEY_PENDING_INTENT, PendingIntent::class.java)
+
+ if (name != null && pendingIntent != null) {
+ CustomTabMenuItem(
+ name = name,
+ pendingIntent = pendingIntent,
+ )
+ } else {
+ null
+ }
+ }
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt
new file mode 100644
index 0000000000..8bd8ee2179
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt
@@ -0,0 +1,74 @@
+/* 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.customtabs
+
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.content.res.Resources
+import android.provider.Browser
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.externalPackage
+import mozilla.components.feature.intent.ext.putSessionId
+import mozilla.components.feature.intent.processing.IntentProcessor
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.utils.toSafeIntent
+
+/**
+ * Processor for intents which trigger actions related to custom tabs.
+ */
+class CustomTabIntentProcessor(
+ private val addCustomTabUseCase: CustomTabsUseCases.AddCustomTabUseCase,
+ private val resources: Resources,
+ private val isPrivate: Boolean = false,
+) : IntentProcessor {
+
+ private fun matches(intent: Intent): Boolean {
+ val safeIntent = intent.toSafeIntent()
+ return safeIntent.action == ACTION_VIEW && isCustomTabIntent(safeIntent)
+ }
+
+ @VisibleForTesting
+ internal fun getAdditionalHeaders(intent: SafeIntent): Map<String, String>? {
+ val pairs = intent.getBundleExtra(Browser.EXTRA_HEADERS)
+ val headers = mutableMapOf<String, String>()
+ pairs?.keySet()?.forEach { key ->
+ val header = pairs.getString(key)
+ if (header != null) {
+ headers[key] = header
+ } else {
+ throw IllegalArgumentException("getAdditionalHeaders() intent bundle contains wrong key value pair")
+ }
+ }
+ return if (headers.isEmpty()) {
+ null
+ } else {
+ headers
+ }
+ }
+
+ override fun process(intent: Intent): Boolean {
+ val safeIntent = SafeIntent(intent)
+ val url = safeIntent.dataString
+
+ return if (!url.isNullOrEmpty() && matches(intent)) {
+ val config = createCustomTabConfigFromIntent(intent, resources)
+ val caller = safeIntent.externalPackage()
+ val customTabId = addCustomTabUseCase(
+ url,
+ config,
+ isPrivate,
+ getAdditionalHeaders(safeIntent),
+ source = SessionState.Source.External.CustomTab(caller),
+ )
+ intent.putSessionId(customTabId)
+
+ true
+ } else {
+ false
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabWindowFeature.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabWindowFeature.kt
new file mode 100644
index 0000000000..77aa6f0999
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabWindowFeature.kt
@@ -0,0 +1,107 @@
+/* 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.customtabs
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.browser.customtabs.CustomTabColorSchemeParams
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.core.net.toUri
+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.findCustomTab
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+const val SHORTCUT_CATEGORY = "mozilla.components.pwa.category.SHORTCUT"
+
+/**
+ * Feature implementation for handling window requests by opening custom tabs.
+ */
+class CustomTabWindowFeature(
+ private val activity: Activity,
+ private val store: BrowserStore,
+ private val sessionId: String,
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Transform a [CustomTabConfig] into a [CustomTabsIntent] that creates a
+ * new custom tab with the same styling and layout
+ */
+ @Suppress("ComplexMethod")
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun configToIntent(config: CustomTabConfig?): CustomTabsIntent {
+ val intent = CustomTabsIntent.Builder().apply {
+ setInstantAppsEnabled(false)
+
+ val customTabColorSchemeBuilder = CustomTabColorSchemeParams.Builder()
+ config?.colorSchemes?.defaultColorSchemeParams?.toolbarColor?.let {
+ customTabColorSchemeBuilder.setToolbarColor(it)
+ }
+ config?.colorSchemes?.defaultColorSchemeParams?.navigationBarColor?.let {
+ customTabColorSchemeBuilder.setNavigationBarColor(it)
+ }
+ setDefaultColorSchemeParams(customTabColorSchemeBuilder.build())
+
+ if (config?.enableUrlbarHiding == true) setUrlBarHidingEnabled(true)
+ config?.closeButtonIcon?.let { setCloseButtonIcon(it) }
+ if (config?.showShareMenuItem == true) setShareState(CustomTabsIntent.SHARE_STATE_ON)
+ config?.titleVisible?.let { setShowTitle(it) }
+ config?.actionButtonConfig?.apply { setActionButton(icon, description, pendingIntent, tint) }
+ config?.menuItems?.forEach { addMenuItem(it.name, it.pendingIntent) }
+ }.build()
+
+ intent.intent.`package` = activity.packageName
+ intent.intent.addCategory(SHORTCUT_CATEGORY)
+
+ return intent
+ }
+
+ /**
+ * Starts observing the configured session to listen for window requests.
+ */
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.findCustomTab(sessionId) }
+ .distinctUntilChangedBy {
+ it.content.windowRequest
+ }
+ .collect { state ->
+ val windowRequest = state.content.windowRequest
+ if (windowRequest?.type == WindowRequest.Type.OPEN) {
+ val intent = configToIntent(state.config)
+ val uri = windowRequest.url.toUri()
+ // This could only fail if the above intent is for our application
+ // and we are not registered to handle its schemes.
+ try {
+ intent.launchUrl(activity, uri)
+ } catch (e: ActivityNotFoundException) {
+ // Workaround for unsupported schemes
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1878704
+ state.engineState.engineSession?.loadUrl(windowRequest.url)
+ }
+ store.dispatch(ContentAction.ConsumeWindowRequestAction(sessionId))
+ }
+ }
+ }
+ }
+
+ /**
+ * Stops observing the configured session for incoming window requests.
+ */
+ override fun stop() {
+ scope?.cancel()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsFacts.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsFacts.kt
new file mode 100644
index 0000000000..6be0cb9610
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsFacts.kt
@@ -0,0 +1,37 @@
+/* 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.customtabs
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [CustomTabsToolbarFeature]
+ */
+class CustomTabsFacts {
+ /**
+ * Items that specify which portion of the [CustomTabsToolbarFeature] was interacted with
+ */
+ object Items {
+ const val CLOSE = "close"
+ const val ACTION_BUTTON = "action_button"
+ }
+}
+
+private fun emitCustomTabsFact(
+ action: Action,
+ item: String,
+) {
+ Fact(
+ Component.FEATURE_CUSTOMTABS,
+ action,
+ item,
+ ).collect()
+}
+
+internal fun emitCloseFact() = emitCustomTabsFact(Action.CLICK, CustomTabsFacts.Items.CLOSE)
+internal fun emitActionButtonFact() = emitCustomTabsFact(Action.CLICK, CustomTabsFacts.Items.ACTION_BUTTON)
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeature.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeature.kt
new file mode 100644
index 0000000000..f437a6874d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeature.kt
@@ -0,0 +1,425 @@
+/* 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.customtabs
+
+import android.app.PendingIntent
+import android.app.UiModeManager.MODE_NIGHT_YES
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.util.Size
+import android.view.Window
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
+import androidx.appcompat.app.AppCompatDelegate.NightMode
+import androidx.appcompat.content.res.AppCompatResources.getDrawable
+import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK
+import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT
+import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_SYSTEM
+import androidx.browser.customtabs.CustomTabsIntent.ColorScheme
+import androidx.core.content.ContextCompat.getColor
+import androidx.core.graphics.drawable.toDrawable
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.state.ColorSchemeParams
+import mozilla.components.browser.state.state.ColorSchemes
+import mozilla.components.browser.state.state.CustomTabActionButtonConfig
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabMenuItem
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.customtabs.feature.CustomTabSessionTitleObserver
+import mozilla.components.feature.customtabs.menu.sendWithUrl
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.ktx.android.content.share
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.ktx.android.view.setNavigationBarTheme
+import mozilla.components.support.ktx.android.view.setStatusBarTheme
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
+import mozilla.components.support.utils.ColorUtils.getReadableTextColor
+import mozilla.components.support.utils.ext.resizeMaintainingAspectRatio
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * Initializes and resets the [BrowserToolbar] for a Custom Tab based on the [CustomTabConfig].
+ *
+ * @property store The given [BrowserStore] to use.
+ * @property toolbar Reference to the [BrowserToolbar], so that the color and menu items can be set.
+ * @property sessionId ID of the custom tab session. No-op if null or invalid.
+ * @property useCases The given [CustomTabsUseCases] to use.
+ * @property menuBuilder [BrowserMenuBuilder] reference to pull menu options from.
+ * @property menuItemIndex Location to insert any custom menu options into the predefined menu list.
+ * @property window Reference to the [Window] so the navigation bar color can be set.
+ * @property updateTheme Whether or not the toolbar and system bar colors should be changed.
+ * @property appNightMode The [NightMode] used in the app. Defaults to [MODE_NIGHT_FOLLOW_SYSTEM].
+ * @property forceActionButtonTinting When set to true the [toolbar] action button will always be tinted
+ * based on the [toolbar] background, ignoring the value of [CustomTabActionButtonConfig.tint].
+ * @property isNavBarEnabled Whether or not the navigation bar is enabled.
+ * @property shareListener Invoked when the share button is pressed.
+ * @property closeListener Invoked when the close button is pressed.
+ */
+@Suppress("LargeClass")
+class CustomTabsToolbarFeature(
+ private val store: BrowserStore,
+ private val toolbar: BrowserToolbar,
+ private val sessionId: String? = null,
+ private val useCases: CustomTabsUseCases,
+ private val menuBuilder: BrowserMenuBuilder? = null,
+ private val menuItemIndex: Int = menuBuilder?.items?.size ?: 0,
+ private val window: Window? = null,
+ private val updateTheme: Boolean = true,
+ @NightMode private val appNightMode: Int = MODE_NIGHT_FOLLOW_SYSTEM,
+ private val forceActionButtonTinting: Boolean = false,
+ private val isNavBarEnabled: Boolean = false,
+ private val shareListener: (() -> Unit)? = null,
+ private val closeListener: () -> Unit,
+) : LifecycleAwareFeature, UserInteractionHandler {
+ private var initialized: Boolean = false
+ private val titleObserver = CustomTabSessionTitleObserver(toolbar)
+ private val context get() = toolbar.context
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Gets the current custom tab session.
+ */
+ private val session: CustomTabSessionState?
+ get() = sessionId?.let { store.state.findCustomTab(it) }
+
+ /**
+ * Initializes the feature and registers the [CustomTabSessionTitleObserver].
+ */
+ override fun start() {
+ val tabId = sessionId ?: return
+ val tab = store.state.findCustomTab(tabId) ?: return
+
+ scope = store.flowScoped { flow ->
+ flow
+ .mapNotNull { state -> state.findCustomTab(tabId) }
+ .ifAnyChanged { tab -> arrayOf(tab.content.title, tab.content.url) }
+ .collect { tab -> titleObserver.onTab(tab) }
+ }
+
+ if (!initialized) {
+ initialized = true
+ init(tab.config)
+ }
+ }
+
+ /**
+ * Unregisters the [CustomTabSessionTitleObserver].
+ */
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ @VisibleForTesting
+ internal fun init(config: CustomTabConfig) {
+ // Don't allow clickable toolbar so a custom tab can't switch to edit mode.
+ toolbar.display.onUrlClicked = { false }
+ toolbar.display.hidePageActionSeparator()
+
+ // Use the intent provided color scheme or fallback to the app night mode preference.
+ val nightMode = config.colorScheme?.toNightMode() ?: appNightMode
+
+ val colorSchemeParams = config.colorSchemes?.getConfiguredColorSchemeParams(
+ nightMode = nightMode,
+ isDarkMode = context.isDarkMode(),
+ )
+
+ val readableColor = if (updateTheme) {
+ colorSchemeParams?.toolbarColor?.let { getReadableTextColor(it) }
+ ?: toolbar.display.colors.menu
+ } else {
+ // It's private mode, the readable color needs match the app.
+ // Note: The main app is configuring the private theme, Custom Tabs is adding the
+ // additional theming for the dynamic UI elements e.g. action & share buttons.
+ val colorResId = context.theme.resolveAttribute(android.R.attr.textColorPrimary)
+ getColor(context, colorResId)
+ }
+
+ if (updateTheme) {
+ colorSchemeParams.let {
+ updateTheme(
+ toolbarColor = it?.toolbarColor,
+ navigationBarColor = it?.navigationBarColor ?: it?.toolbarColor,
+ navigationBarDividerColor = it?.navigationBarDividerColor,
+ readableColor = readableColor,
+ )
+ }
+ }
+
+ // Add navigation close action
+ if (config.showCloseButton) {
+ addCloseButton(readableColor, config.closeButtonIcon)
+ }
+
+ // Add action button
+ addActionButton(readableColor, config.actionButtonConfig)
+
+ // Show share button
+ if (config.showShareMenuItem) {
+ addShareButton(readableColor)
+ }
+
+ // Add menu items
+ if (config.menuItems.isNotEmpty() || menuBuilder?.items?.isNotEmpty() == true) {
+ addMenuItems(config.menuItems, menuItemIndex)
+ }
+
+ if (isNavBarEnabled) {
+ toolbar.display.hideMenuButton()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun updateTheme(
+ @ColorInt toolbarColor: Int? = null,
+ @ColorInt navigationBarColor: Int? = null,
+ @ColorInt navigationBarDividerColor: Int? = null,
+ @ColorInt readableColor: Int,
+ ) {
+ toolbarColor?.let {
+ toolbar.setBackgroundColor(it)
+
+ toolbar.display.colors = toolbar.display.colors.copy(
+ text = readableColor,
+ title = readableColor,
+ securityIconSecure = readableColor,
+ securityIconInsecure = readableColor,
+ trackingProtection = readableColor,
+ menu = readableColor,
+ )
+
+ window?.setStatusBarTheme(it)
+ }
+
+ if (navigationBarColor != null || navigationBarDividerColor != null) {
+ window?.setNavigationBarTheme(navigationBarColor, navigationBarDividerColor)
+ }
+ }
+
+ /**
+ * Display a close button at the start of the toolbar.
+ * When clicked, it calls [closeListener].
+ */
+ @VisibleForTesting
+ internal fun addCloseButton(@ColorInt readableColor: Int, bitmap: Bitmap?) {
+ val drawableIcon = bitmap?.toDrawable(context.resources)
+ ?: getDrawable(context, iconsR.drawable.mozac_ic_cross_24)!!.mutate()
+
+ drawableIcon.setTint(readableColor)
+
+ val button = Toolbar.ActionButton(
+ drawableIcon,
+ context.getString(R.string.mozac_feature_customtabs_exit_button),
+ ) {
+ emitCloseFact()
+ session?.let {
+ useCases.remove(it.id)
+ }
+ closeListener.invoke()
+ }
+ toolbar.addNavigationAction(button)
+ }
+
+ /**
+ * Display an action button from the custom tab config on the toolbar.
+ * When clicked, it activates the corresponding [PendingIntent].
+ */
+ @VisibleForTesting
+ internal fun addActionButton(
+ @ColorInt readableColor: Int,
+ buttonConfig: CustomTabActionButtonConfig?,
+ ) {
+ buttonConfig?.let { config ->
+ val icon = config.icon
+ val scaledIconSize = icon.resizeMaintainingAspectRatio(ACTION_BUTTON_MAX_DRAWABLE_DP_SIZE)
+ val drawableIcon = Bitmap.createScaledBitmap(
+ icon,
+ scaledIconSize.width.dpToPx(context.resources.displayMetrics),
+ scaledIconSize.height.dpToPx(context.resources.displayMetrics),
+ true,
+ ).toDrawable(context.resources)
+
+ if (config.tint || forceActionButtonTinting) {
+ drawableIcon.setTint(readableColor)
+ }
+
+ val button = Toolbar.ActionButton(
+ drawableIcon,
+ config.description,
+ ) {
+ emitActionButtonFact()
+ session?.let {
+ config.pendingIntent.sendWithUrl(context, it.content.url)
+ }
+ }
+
+ toolbar.addBrowserAction(button)
+ }
+ }
+
+ /**
+ * Display a share button as a button on the toolbar.
+ * When clicked, it activates [shareListener] and defaults to the [share] KTX helper.
+ */
+ @VisibleForTesting
+ internal fun addShareButton(@ColorInt readableColor: Int) {
+ val drawableIcon = getDrawable(context, iconsR.drawable.mozac_ic_share_android_24)!!
+ drawableIcon.setTint(readableColor)
+
+ val button = Toolbar.ActionButton(
+ drawableIcon,
+ context.getString(R.string.mozac_feature_customtabs_share_link),
+ ) {
+ val listener = shareListener ?: {
+ session?.let {
+ context.share(it.content.url)
+ }
+ }
+ emitActionButtonFact()
+ listener.invoke()
+ }
+
+ toolbar.addBrowserAction(button)
+ }
+
+ /**
+ * Build the menu items displayed when the 3-dot overflow menu is opened.
+ */
+ @VisibleForTesting
+ internal fun addMenuItems(
+ menuItems: List<CustomTabMenuItem>,
+ index: Int,
+ ) {
+ menuItems.map { item ->
+ SimpleBrowserMenuItem(item.name) {
+ session?.let {
+ item.pendingIntent.sendWithUrl(context, it.content.url)
+ }
+ }
+ }.also { items ->
+ val combinedItems = menuBuilder?.let { builder ->
+ val newMenuItemList = mutableListOf<BrowserMenuItem>()
+ val insertIndex = index.coerceIn(0, builder.items.size)
+
+ newMenuItemList.apply {
+ addAll(builder.items)
+ addAll(insertIndex, items)
+ }
+ } ?: items
+
+ val combinedExtras = menuBuilder?.let { builder ->
+ builder.extras + Pair("customTab", true)
+ }
+
+ toolbar.display.menuBuilder = BrowserMenuBuilder(combinedItems, combinedExtras.orEmpty())
+ }
+ }
+
+ /**
+ * When the back button is pressed if not initialized returns false,
+ * when initialized removes the current Custom Tabs session and returns true.
+ * Should be called when the back button is pressed.
+ */
+ override fun onBackPressed(): Boolean {
+ return if (!initialized) {
+ false
+ } else {
+ if (sessionId != null && useCases.remove(sessionId)) {
+ closeListener.invoke()
+ true
+ } else {
+ false
+ }
+ }
+ }
+
+ companion object {
+ private val ACTION_BUTTON_MAX_DRAWABLE_DP_SIZE = Size(48, 24)
+ }
+}
+
+@VisibleForTesting
+internal fun ColorSchemes.getConfiguredColorSchemeParams(
+ @NightMode nightMode: Int? = null,
+ isDarkMode: Boolean = false,
+) = when {
+ noColorSchemeParamsSet() -> null
+
+ defaultColorSchemeParamsOnly() -> defaultColorSchemeParams
+
+ // Try to follow specified color scheme.
+ nightMode == MODE_NIGHT_FOLLOW_SYSTEM -> {
+ if (isDarkMode) {
+ darkColorSchemeParams?.withDefault(defaultColorSchemeParams)
+ ?: defaultColorSchemeParams
+ } else {
+ lightColorSchemeParams?.withDefault(defaultColorSchemeParams)
+ ?: defaultColorSchemeParams
+ }
+ }
+
+ nightMode == MODE_NIGHT_NO -> lightColorSchemeParams?.withDefault(
+ defaultColorSchemeParams,
+ ) ?: defaultColorSchemeParams
+
+ nightMode == MODE_NIGHT_YES -> darkColorSchemeParams?.withDefault(
+ defaultColorSchemeParams,
+ ) ?: defaultColorSchemeParams
+
+ // No color scheme set, try to use default.
+ else -> defaultColorSchemeParams
+}
+
+/**
+ * Try to convert the given [ColorScheme] to [NightMode].
+ */
+@VisibleForTesting
+@NightMode
+internal fun Int.toNightMode() = when (this) {
+ COLOR_SCHEME_SYSTEM -> MODE_NIGHT_FOLLOW_SYSTEM
+ COLOR_SCHEME_LIGHT -> MODE_NIGHT_NO
+ COLOR_SCHEME_DARK -> MODE_NIGHT_YES
+ else -> null
+}
+
+private fun ColorSchemes.noColorSchemeParamsSet() =
+ defaultColorSchemeParams == null && lightColorSchemeParams == null && darkColorSchemeParams == null
+
+private fun ColorSchemes.defaultColorSchemeParamsOnly() =
+ defaultColorSchemeParams != null && lightColorSchemeParams == null && darkColorSchemeParams == null
+
+private fun Context.isDarkMode() =
+ resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+
+/**
+ * Try to create a [ColorSchemeParams] using the given [defaultColorSchemeParam] as a fallback if
+ * there are missing properties.
+ */
+@VisibleForTesting
+internal fun ColorSchemeParams.withDefault(defaultColorSchemeParam: ColorSchemeParams?) = ColorSchemeParams(
+ toolbarColor = toolbarColor
+ ?: defaultColorSchemeParam?.toolbarColor,
+ secondaryToolbarColor = secondaryToolbarColor
+ ?: defaultColorSchemeParam?.secondaryToolbarColor,
+ navigationBarColor = navigationBarColor
+ ?: defaultColorSchemeParam?.navigationBarColor,
+ navigationBarDividerColor = navigationBarDividerColor
+ ?: defaultColorSchemeParam?.navigationBarDividerColor,
+)
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserver.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserver.kt
new file mode 100644
index 0000000000..446542030a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserver.kt
@@ -0,0 +1,50 @@
+/* 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.customtabs.feature
+
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.concept.toolbar.Toolbar
+
+/**
+ * Sets the title of the custom tab toolbar based on the session title and URL.
+ */
+class CustomTabSessionTitleObserver(
+ private val toolbar: Toolbar,
+) {
+ private var url: String? = null
+ private var title: String? = null
+ private var showedTitle = false
+
+ internal fun onTab(tab: CustomTabSessionState) {
+ if (tab.content.title != title) {
+ onTitleChanged(tab)
+ title = tab.content.title
+ }
+
+ if (tab.content.url != url) {
+ onUrlChanged(tab)
+ url = tab.content.url
+ }
+ }
+
+ private fun onUrlChanged(tab: CustomTabSessionState) {
+ // If we showed a title once in a custom tab then we are going to continue displaying
+ // a title (to avoid the layout bouncing around). However if no title is available then
+ // we just use the URL.
+ if (showedTitle && tab.content.title.isEmpty()) {
+ toolbar.title = tab.content.url
+ }
+ }
+
+ private fun onTitleChanged(tab: CustomTabSessionState) {
+ if (tab.content.title.isNotEmpty()) {
+ toolbar.title = tab.content.title
+ showedTitle = true
+ } else if (showedTitle) {
+ // See comment in OnUrlChanged().
+ toolbar.title = tab.content.url
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeature.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeature.kt
new file mode 100644
index 0000000000..d92c26cda0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeature.kt
@@ -0,0 +1,67 @@
+/* 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.customtabs.feature
+
+import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.browser.customtabs.CustomTabsService.Relation
+import androidx.browser.customtabs.CustomTabsSessionToken
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.CustomTabsAction
+import mozilla.components.feature.customtabs.store.OriginRelationPair
+import mozilla.components.feature.customtabs.store.ValidateRelationshipAction
+import mozilla.components.feature.customtabs.store.VerificationStatus.FAILURE
+import mozilla.components.feature.customtabs.store.VerificationStatus.PENDING
+import mozilla.components.feature.customtabs.store.VerificationStatus.SUCCESS
+import mozilla.components.feature.customtabs.verify.OriginVerifier
+import mozilla.components.service.digitalassetlinks.RelationChecker
+
+class OriginVerifierFeature(
+ private val packageManager: PackageManager,
+ private val relationChecker: RelationChecker,
+ private val dispatch: (CustomTabsAction) -> Unit,
+) {
+
+ private var cachedVerifier: Triple<String, Int, OriginVerifier>? = null
+
+ suspend fun verify(
+ state: CustomTabState,
+ token: CustomTabsSessionToken,
+ @Relation relation: Int,
+ origin: Uri,
+ ): Boolean {
+ val packageName = state.creatorPackageName ?: return false
+
+ val existingRelation = state.relationships[OriginRelationPair(origin, relation)]
+ return if (existingRelation == SUCCESS || existingRelation == FAILURE) {
+ // Return if relation is already success or failure
+ existingRelation == SUCCESS
+ } else {
+ val verifier = getVerifier(packageName, relation)
+ dispatch(ValidateRelationshipAction(token, relation, origin, PENDING))
+
+ val result = verifier.verifyOrigin(origin)
+ val status = if (result) SUCCESS else FAILURE
+
+ dispatch(ValidateRelationshipAction(token, relation, origin, status))
+ result
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getVerifier(packageName: String, @Relation relation: Int): OriginVerifier {
+ cachedVerifier?.let {
+ val (cachedPackage, cachedRelation, verifier) = it
+ if (cachedPackage == packageName && cachedRelation == relation) {
+ return verifier
+ }
+ }
+
+ return OriginVerifier(packageName, relation, packageManager, relationChecker).also {
+ cachedVerifier = Triple(packageName, relation, it)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidates.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidates.kt
new file mode 100644
index 0000000000..07bf60b119
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidates.kt
@@ -0,0 +1,32 @@
+/* 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.customtabs.menu
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+/**
+ * Build menu items displayed when the 3-dot overflow menu is opened.
+ * These items are provided by the app that creates the custom tab,
+ * and should be inserted alongside menu items created by the browser.
+ */
+fun CustomTabSessionState.createCustomTabMenuCandidates(context: Context) =
+ config.menuItems.map { item ->
+ TextMenuCandidate(
+ text = item.name,
+ ) {
+ item.pendingIntent.sendWithUrl(context, content.url)
+ }
+ }
+
+internal fun PendingIntent.sendWithUrl(context: Context, url: String) = send(
+ context,
+ 0,
+ Intent(null, url.toUri()),
+)
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsAction.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsAction.kt
new file mode 100644
index 0000000000..5c9e6941e6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsAction.kt
@@ -0,0 +1,40 @@
+/* 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.customtabs.store
+
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsService.Relation
+import androidx.browser.customtabs.CustomTabsSessionToken
+import mozilla.components.lib.state.Action
+
+sealed class CustomTabsAction : Action {
+ abstract val token: CustomTabsSessionToken
+}
+
+/**
+ * Saves the package name corresponding to a custom tab token.
+ *
+ * @property token Token of the custom tab.
+ * @property packageName Package name of the app that created the custom tab.
+ */
+data class SaveCreatorPackageNameAction(
+ override val token: CustomTabsSessionToken,
+ val packageName: String,
+) : CustomTabsAction()
+
+/**
+ * Marks the state of a custom tabs [Relation] verification.
+ *
+ * @property token Token of the custom tab to verify.
+ * @property relation Relationship type to verify.
+ * @property origin Origin to verify.
+ * @property status State of the verification process.
+ */
+data class ValidateRelationshipAction(
+ override val token: CustomTabsSessionToken,
+ @Relation val relation: Int,
+ val origin: Uri,
+ val status: VerificationStatus,
+) : CustomTabsAction()
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceState.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceState.kt
new file mode 100644
index 0000000000..e95b4262ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceState.kt
@@ -0,0 +1,68 @@
+/* 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.customtabs.store
+
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsService
+import androidx.browser.customtabs.CustomTabsSessionToken
+import mozilla.components.lib.state.State
+
+/**
+ * Value type that represents the custom tabs state
+ * accessible from both the service and activity.
+ */
+data class CustomTabsServiceState(
+ val tabs: Map<CustomTabsSessionToken, CustomTabState> = emptyMap(),
+) : State
+
+/**
+ * Value type that represents the state of a single custom tab
+ * accessible from both the service and activity.
+ *
+ * This data is meant to supplement [mozilla.components.browser.session.tab.CustomTabConfig],
+ * not replace it. It only contains data that the service also needs to work with.
+ *
+ * @property creatorPackageName Package name of the app that created the custom tab.
+ * @property relationships Map of origin and relationship type to current verification state.
+ */
+data class CustomTabState(
+ val creatorPackageName: String? = null,
+ val relationships: Map<OriginRelationPair, VerificationStatus> = emptyMap(),
+)
+
+/**
+ * Pair of origin and relation type used as key in [CustomTabState.relationships].
+ *
+ * @property origin URL that contains only the scheme, host, and port.
+ * https://html.spec.whatwg.org/multipage/origin.html#concept-origin
+ * @property relation Enum that indicates the relation type.
+ */
+data class OriginRelationPair(
+ val origin: Uri,
+ @CustomTabsService.Relation val relation: Int,
+)
+
+/**
+ * Different states of Digital Asset Link verification.
+ */
+enum class VerificationStatus {
+ /**
+ * Indicates verification has started and hasn't returned yet.
+ *
+ * To avoid flashing the toolbar, we choose to hide it when a Digital Asset Link is being verified.
+ * We only show the toolbar when the verification fails, or an origin never requested to be verified.
+ */
+ PENDING,
+
+ /**
+ * Indicates that verification has completed and the link was verified.
+ */
+ SUCCESS,
+
+ /**
+ * Indicates that verification has completed and the link was invalid.
+ */
+ FAILURE,
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducer.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducer.kt
new file mode 100644
index 0000000000..13ee59c934
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducer.kt
@@ -0,0 +1,28 @@
+/* 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.customtabs.store
+
+internal object CustomTabsServiceStateReducer {
+
+ fun reduce(state: CustomTabsServiceState, action: CustomTabsAction): CustomTabsServiceState {
+ val tabState = state.tabs.getOrElse(action.token) { CustomTabState() }
+ val newTabState = reduceTab(tabState, action)
+ return state.copy(tabs = state.tabs + Pair(action.token, newTabState))
+ }
+
+ private fun reduceTab(state: CustomTabState, action: CustomTabsAction): CustomTabState {
+ return when (action) {
+ is SaveCreatorPackageNameAction ->
+ state.copy(creatorPackageName = action.packageName)
+ is ValidateRelationshipAction ->
+ state.copy(
+ relationships = state.relationships + Pair(
+ OriginRelationPair(action.origin, action.relation),
+ action.status,
+ ),
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStore.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStore.kt
new file mode 100644
index 0000000000..e0b9ba5b86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStore.kt
@@ -0,0 +1,15 @@
+/* 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.customtabs.store
+
+import mozilla.components.lib.state.Store
+
+class CustomTabsServiceStore(
+ initialState: CustomTabsServiceState = CustomTabsServiceState(),
+) : Store<CustomTabsServiceState, CustomTabsAction>(
+ initialState,
+ CustomTabsServiceStateReducer::reduce,
+ threadNamePrefix = "CustomTabsService",
+)
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/verify/OriginVerifier.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/verify/OriginVerifier.kt
new file mode 100644
index 0000000000..bcf4f34541
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/verify/OriginVerifier.kt
@@ -0,0 +1,76 @@
+/* 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.customtabs.verify
+
+import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN
+import androidx.browser.customtabs.CustomTabsService.Relation
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.digitalassetlinks.AndroidAssetFinder
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation.HANDLE_ALL_URLS
+import mozilla.components.service.digitalassetlinks.Relation.USE_AS_ORIGIN
+import mozilla.components.service.digitalassetlinks.RelationChecker
+
+/**
+ * Used to verify postMessage origin for a designated package name.
+ *
+ * Uses Digital Asset Links to confirm that the given origin is associated with the package name.
+ * It caches any origin that has been verified during the current application
+ * lifecycle and reuses that without making any new network requests.
+ */
+class OriginVerifier(
+ private val packageName: String,
+ @Relation private val relation: Int,
+ packageManager: PackageManager,
+ private val relationChecker: RelationChecker,
+) {
+
+ @VisibleForTesting
+ internal val androidAsset by lazy {
+ AndroidAssetFinder().getAndroidAppAsset(packageName, packageManager).firstOrNull()
+ }
+
+ /**
+ * Verify the claimed origin for the cached package name asynchronously. This will end up
+ * making a network request for non-cached origins with a HTTP [Client].
+ *
+ * @param origin The postMessage origin the application is claiming to have. Can't be null.
+ */
+ suspend fun verifyOrigin(origin: Uri) = withContext(IO) { verifyOriginInternal(origin) }
+
+ @Suppress("ReturnCount")
+ private fun verifyOriginInternal(origin: Uri): Boolean {
+ val cachedOrigin = cachedOriginMap[packageName]
+ if (cachedOrigin == origin) return true
+
+ if (origin.scheme != "https") return false
+ val relationship = when (relation) {
+ RELATION_USE_AS_ORIGIN -> USE_AS_ORIGIN
+ RELATION_HANDLE_ALL_URLS -> HANDLE_ALL_URLS
+ else -> return false
+ }
+
+ val originVerified = relationChecker.checkRelationship(
+ source = AssetDescriptor.Web(site = origin.toString()),
+ target = androidAsset ?: return false,
+ relation = relationship,
+ )
+
+ if (originVerified && packageName !in cachedOriginMap) {
+ cachedOriginMap[packageName] = origin
+ }
+ return originVerified
+ }
+
+ companion object {
+ private val cachedOriginMap = mutableMapOf<String, Uri>()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..c7214823df
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">ወደ ቀዳሚው መተግበሪያ ተመለስ</string>
+ <string name="mozac_feature_customtabs_share_link">አገናኝ አጋራ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..e0e79c0ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-an/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Tornar ta l’aplicación anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir lo vinclo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..6815ac4db3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ar/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">ارجع للتطبيق السابق</string>
+ <string name="mozac_feature_customtabs_share_link">شارِك الرابط</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..20a0e7e9fe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Volver a l\'aplicación anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir l\'enllaz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..b225d20e41
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-az/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Əvvəlki tətbiqə qayıt</string>
+ <string name="mozac_feature_customtabs_share_link">Keçidi paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..deac72bfb9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">اؤنجه‌کی اَپَه دؤن</string>
+ <string name="mozac_feature_customtabs_share_link">باغلانتی‌نی پایلاش</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..b990cefbab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Uliang ka aplikasi sadurungnyane</string>
+ <string name="mozac_feature_customtabs_share_link">Ngbagiang tautan</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..fee7305095
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Вярнуцца ў папярэднюю праграму</string>
+ <string name="mozac_feature_customtabs_share_link">Падзяліцца спасылкай</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..967d209d75
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Връщане към предишното приложение</string>
+ <string name="mozac_feature_customtabs_share_link">Споделяне на препратка</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..4676782818
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">আগের অ্যাপে ফিরে যান</string>
+ <string name="mozac_feature_customtabs_share_link">লিংক শেয়ার করুন</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..7e77e03600
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Distreiñ dʼan arload kent</string>
+ <string name="mozac_feature_customtabs_share_link">Rannañ an ere</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..f9624f2eaa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Povratak na prethodnu aplikaciju</string>
+ <string name="mozac_feature_customtabs_share_link">Podijeli link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..e66e0ab37e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Torna a l’aplicació anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Comparteix l’enllaç</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..47a7f2d06b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Titzolin pa ri jun chokoy</string>
+ <string name="mozac_feature_customtabs_share_link">Tikomonïx ri ximonel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..3a63483dab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Balik sa previous nga app</string>
+ <string name="mozac_feature_customtabs_share_link">i-Share ang link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..78ded2fc7b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">بگەڕێوە بۆ بەرنامەی پێشوو</string>
+ <string name="mozac_feature_customtabs_share_link">بەستەر بڵاوبکەرەوە</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..912e8712fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Rivene à l’appiecazione precedente</string>
+ <string name="mozac_feature_customtabs_share_link">Sparte u liame</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..0a6027a0ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Návrat do předchozí aplikace</string>
+ <string name="mozac_feature_customtabs_share_link">Sdílet odkaz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..04e4257505
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Nôl i’r ap blaenorol</string>
+ <string name="mozac_feature_customtabs_share_link">Rhannu dolen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..52264f69a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Tilbage til forrige app</string>
+ <string name="mozac_feature_customtabs_share_link">Del link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..a061bc7ba2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Zurück zur vorherigen App</string>
+ <string name="mozac_feature_customtabs_share_link">Link teilen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..b6c295e334
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Slědk k pjerwjejšnemu nałoženjeju</string>
+ <string name="mozac_feature_customtabs_share_link">Wótkaz źěliś</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..4f7ef1cc74
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Επιστροφή στην προηγούμενη εφαρμογή</string>
+ <string name="mozac_feature_customtabs_share_link">Κοινή χρήση συνδέσμου</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..7d001b2872
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Return to previous app</string>
+ <string name="mozac_feature_customtabs_share_link">Share link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..7d001b2872
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Return to previous app</string>
+ <string name="mozac_feature_customtabs_share_link">Share link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..867203597e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Reen al antaŭa programo</string>
+ <string name="mozac_feature_customtabs_share_link">Kundividi ligilon</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..ec4b3f66e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Volver a la aplicación anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir enlace</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..8eda46d9e3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Regresar a la aplicación anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir enlace</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..ec4b3f66e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Volver a la aplicación anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir enlace</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..ec4b3f66e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Volver a la aplicación anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir enlace</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..ec4b3f66e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Volver a la aplicación anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir enlace</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..4b62fbc1be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Tagasi eelmise äpi juurde</string>
+ <string name="mozac_feature_customtabs_share_link">Jaga linki</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..0494ad16ad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Itzuli aurreko aplikaziora</string>
+ <string name="mozac_feature_customtabs_share_link">Partekatu lotura</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..6a4589a70a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">بازگشت به برنامهٔ قبل</string>
+ <string name="mozac_feature_customtabs_share_link">اشتراک‌گذاری پیوند</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..17aaa5ca70
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ff/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Rutto e jaaɓnirgal ɓennungal</string>
+ <string name="mozac_feature_customtabs_share_link">Lollin jokkol</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..9eb780ce76
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Palaa edelliseen sovellukseen</string>
+ <string name="mozac_feature_customtabs_share_link">Jaa linkki</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..71e5ba49b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Revenir à l’application précédente</string>
+ <string name="mozac_feature_customtabs_share_link">Partager le lien</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..d0c99e5949
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Torne ae aplicazion precedente</string>
+ <string name="mozac_feature_customtabs_share_link">Condivît link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..a34d8bc45f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Tebek nei foarige app</string>
+ <string name="mozac_feature_customtabs_share_link">Keppeling diele</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..26d689932b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Fill ar an aip roimhe seo</string>
+ <string name="mozac_feature_customtabs_share_link">Comhroinn an nasc</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..f194676ccb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Till gun aplacaid roimhe</string>
+ <string name="mozac_feature_customtabs_share_link">Co-roinn an ceangal</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..f3044f9fb5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Volver á aplicación anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir ligazón</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..1e65bfac50
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Ejevyjey tembiporu’i mboyveguávape</string>
+ <string name="mozac_feature_customtabs_share_link">Emoherakuã juajuha</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..030d775962
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">જુના એપમાં પાંછા જાઓ</string>
+ <string name="mozac_feature_customtabs_share_link">લિંક શેર કરો</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..13a4031103
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">पिछले ऐप पर वापस जाएं</string>
+ <string name="mozac_feature_customtabs_share_link">लिंक साझा करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..b975cab3ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Vrati se na prethodnu aplikaciju</string>
+ <string name="mozac_feature_customtabs_share_link">Dijeli poveznicu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..f4d842a363
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Wróćo k předchadnemu nałoženju</string>
+ <string name="mozac_feature_customtabs_share_link">Wotkaz dźělić</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..ffee0390d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Vissza az előző apphoz</string>
+ <string name="mozac_feature_customtabs_share_link">Hivatkozás megosztása</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..11e13e8549
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Վերադառնալ նախորդ հավելվածին</string>
+ <string name="mozac_feature_customtabs_share_link">Տարածել հղումը</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..025c1f6fea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Retornar al app previe</string>
+ <string name="mozac_feature_customtabs_share_link">Compartir le ligamine</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..ba8bbc8887
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Kembali ke aplikasi sebelumnya</string>
+ <string name="mozac_feature_customtabs_share_link">Bagikan tautan</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..8436de9ccc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Farðu aftur í fyrra smáforrit</string>
+ <string name="mozac_feature_customtabs_share_link">Deila tengli</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..bbb8597fab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Torna all’applicazione precedente</string>
+ <string name="mozac_feature_customtabs_share_link">Condividi link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..bcb8ba7e7b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">חזרה ליישומון הקודם</string>
+ <string name="mozac_feature_customtabs_share_link">שיתוף קישור</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..c032d9fca5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">前のアプリへ戻る</string>
+ <string name="mozac_feature_customtabs_share_link">リンクを共有</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..d1635e7825
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">წინა პროგრამაზე დაბრუნება</string>
+ <string name="mozac_feature_customtabs_share_link">ბმულის გაზიარება</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..3282c0c294
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Aldınǵı baǵdarlamaǵa qaytıw</string>
+ <string name="mozac_feature_customtabs_share_link">Siltemeni bólisiw</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..152805abb6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Uɣal ar usnas izrin</string>
+ <string name="mozac_feature_customtabs_share_link">Bḍu aseɣwen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..34c58c3238
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Алдыңғы қолданбаға оралу</string>
+ <string name="mozac_feature_customtabs_share_link">Сілтемемен бөлісу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..25aca1dea7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Vegere sepana berê</string>
+ <string name="mozac_feature_customtabs_share_link">Girêdanê parve bike</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..8a851a3309
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">ಹಿಂದಿನ ಅನ್ವಯಕ್ಕೆ ಮರಳಿ</string>
+ <string name="mozac_feature_customtabs_share_link">ಕೊಂಡಿಯನ್ನು ಹಂಚಿಕೊಳ್ಳಿ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..b41a66886d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">이전 앱으로 돌아가기</string>
+ <string name="mozac_feature_customtabs_share_link">링크 공유</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..dec5eaf455
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lij/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Vanni a l\'app de primma</string>
+ <string name="mozac_feature_customtabs_share_link">Condividdi link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..5de508f665
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">ກັບໄປຫາແອັບກ່ອນຫນ້ານີ້</string>
+ <string name="mozac_feature_customtabs_share_link">ແບ່ງປັນລີ້ງ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..61de1b3ecf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Grįžti į ankstesnę programą</string>
+ <string name="mozac_feature_customtabs_share_link">Dalintis saitu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..a1ee4e969f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ml/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">മുമ്പത്തെ ആപ്പിലേക്ക് മടങ്ങുക</string>
+ <string name="mozac_feature_customtabs_share_link">കണ്ണി പങ്കിടുക</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..4bacf2546d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-mr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">मागील अॅप वर परत या</string>
+ <string name="mozac_feature_customtabs_share_link">दुवा शेअर करा</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..1084e61f08
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-my/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">အရင်ကအက်ပ်သို့ သွားပါ</string>
+ <string name="mozac_feature_customtabs_share_link">လင့်ခ်ကို မျှဝေရန်</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..d319f8b77a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Gå tilbake til forrige app</string>
+ <string name="mozac_feature_customtabs_share_link">Del lenke</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..9630785ed2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">पहिलाको एपमा फर्किनुहोस्</string>
+ <string name="mozac_feature_customtabs_share_link">लिङ्क सेयर गर्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..6b0f757532
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Terug naar vorige app</string>
+ <string name="mozac_feature_customtabs_share_link">Koppeling delen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..a815c4d553
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Gå tilbake til førre app</string>
+ <string name="mozac_feature_customtabs_share_link">Del lenke</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..01a0d4c645
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Tornar a l’aplicacion precedenta</string>
+ <string name="mozac_feature_customtabs_share_link">Partejar lo ligam</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..1a4925a2c1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-or/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_share_link">ଲିଙ୍କ ବିତରଣ କରନ୍ତୁ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..9a2a887cab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">ਪਿਛਲੀ ਐਪ ‘ਤੇ ਜਾਓ</string>
+ <string name="mozac_feature_customtabs_share_link">ਲਿੰਕ ਸਾਂਝਾ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..431f8fa03f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">پچھلی ایپ نوں واپس جاؤ</string>
+ <string name="mozac_feature_customtabs_share_link">پتہ سانجھا کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..1cee15d909
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Wróć do poprzedniej aplikacji</string>
+ <string name="mozac_feature_customtabs_share_link">Udostępnij odnośnik</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..2838396787
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Retornar ao aplicativo anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Compartilhar link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..94edd3d2a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Voltar à aplicação anterior</string>
+ <string name="mozac_feature_customtabs_share_link">Partilhar ligação</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..5c8624edf4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Turnar a la app precedenta</string>
+ <string name="mozac_feature_customtabs_share_link">Cundivider la colliaziun</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..f1bd593246
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ro/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Revenire la aplicația anterioară</string>
+ <string name="mozac_feature_customtabs_share_link">Partajează linkul</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..bfdac1f591
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Вернуться к предыдущему приложению</string>
+ <string name="mozac_feature_customtabs_share_link">Поделиться ссылкой</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..519b799596
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">ᱞᱟᱦᱟ ᱛᱮᱭᱟᱜ ᱮᱯ ᱨᱮ ᱨᱩᱣᱟᱹᱲᱚᱜ ᱢᱮ</string>
+ <string name="mozac_feature_customtabs_share_link">ᱞᱤᱝᱠ ᱦᱟᱹᱴᱤᱧ ᱢᱮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..33b8dad9e6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Torra a s’aplicatzione pretzedente</string>
+ <string name="mozac_feature_customtabs_share_link">Cumpartzi su ligòngiu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..f62e75a275
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">කලින් යෙදුමට ආපසු</string>
+ <string name="mozac_feature_customtabs_share_link">සබැඳිය බෙදාගන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..db1a9c2fef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Návrat do predchádzajúcej aplikácie</string>
+ <string name="mozac_feature_customtabs_share_link">Zdieľať odkaz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..1336257ab3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">پچھلی ایپ تے واپس ون٘ڄو</string>
+ <string name="mozac_feature_customtabs_share_link">لنک شیئر کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..e71763b1ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Nazaj na prejšnjo aplikacijo</string>
+ <string name="mozac_feature_customtabs_share_link">Deli povezavo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..f616f9fee4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Kthehu te aplikacioni i mëparshëm</string>
+ <string name="mozac_feature_customtabs_share_link">Ndani lidhje me të tjerët</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..6fd3ffc4e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Врати се на претходну апликацију</string>
+ <string name="mozac_feature_customtabs_share_link">Подели везу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..6baec852bf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Balik deui ka aplikasi saméméhna</string>
+ <string name="mozac_feature_customtabs_share_link">Bagikeun tutumbu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..ac83bc4426
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Återgå till föregående app</string>
+ <string name="mozac_feature_customtabs_share_link">Dela länk</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..1935e8b52e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ta/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">முந்தைய செயலிக்குத் திரும்பு</string>
+ <string name="mozac_feature_customtabs_share_link">தொடுப்பைப் பகிர்</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..cc2cfcd80b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-te/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">మునుపటి అనువర్తనానికి తిరిగి వెళ్ళు</string>
+ <string name="mozac_feature_customtabs_share_link">లంకెను పంచుకోండి</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..bc9afc0c8e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Бозгашт ба барномаи қаблӣ</string>
+ <string name="mozac_feature_customtabs_share_link">Мубодила кардани пайванд</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..177b490040
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">กลับไปที่แอปก่อนหน้า</string>
+ <string name="mozac_feature_customtabs_share_link">แบ่งปันลิงก์</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..3d37950f8c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Bumalik sa nakaraang app</string>
+ <string name="mozac_feature_customtabs_share_link">Ibahagi ang link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..ec2c84f263
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tok/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">o tawa ilo pini</string>
+ <string name="mozac_feature_customtabs_share_link">o pana e nimi nasin</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..ec428ff885
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Önceki uygulamaya dön</string>
+ <string name="mozac_feature_customtabs_share_link">Bağlantıyı paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..ffea9b71c2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Nānīkāj riña aplikasiûn garâjsunt akuan\'</string>
+ <string name="mozac_feature_customtabs_share_link">Dūyingô\' lînk</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..68c4066531
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Соңгы кушымтага кире кайту</string>
+ <string name="mozac_feature_customtabs_share_link">Сылтаманы уртаклашу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..cece299be4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_share_link">Bḍu asɣen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..6091e4f4e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">ئالدىنقى ئەپكە قايت</string>
+ <string name="mozac_feature_customtabs_share_link">ئۇلانمىنى ئورتاقلاش</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..1b927f06ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Повернутись до попередньої програми</string>
+ <string name="mozac_feature_customtabs_share_link">Поділитись посиланням</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..f7ffc60127
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">پچھلی ایپلیکیشن میں واپس جائیں</string>
+ <string name="mozac_feature_customtabs_share_link">ربط شیئر کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..76890b9883
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Oldingi ilovaga qaytish</string>
+ <string name="mozac_feature_customtabs_share_link">Havolani ulashish</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..ee9fe88ef3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vec/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Torna indrio a ƚ’aplicasione presedente</string>
+ <string name="mozac_feature_customtabs_share_link">Condividi link</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..91dabe207a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Quay lại ứng dụng trước</string>
+ <string name="mozac_feature_customtabs_share_link">Chia sẻ liên kết</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..793f44af04
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">Padà sí áàpù ti tẹ́lẹ̀</string>
+ <string name="mozac_feature_customtabs_share_link">Pín líǹkì</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..9bc538909f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">返回之前的应用</string>
+ <string name="mozac_feature_customtabs_share_link">分享链接</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..fd1477504f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mozac_feature_customtabs_exit_button">回到先前的應用程式</string>
+ <string name="mozac_feature_customtabs_share_link">分享鏈結</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values/dimens.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..7ec404f03f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+<?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>
+ <dimen name="mozac_feature_customtabs_max_close_button_size">24dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..f10d13b6ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+<?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>
+ <string name="mozac_feature_customtabs_exit_button">Return to previous app</string>
+ <string name="mozac_feature_customtabs_share_link">Share link</string>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt
new file mode 100644
index 0000000000..3b143f8721
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt
@@ -0,0 +1,130 @@
+/* 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.customtabs
+
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.IBinder
+import android.support.customtabs.ICustomTabsCallback
+import android.support.customtabs.ICustomTabsService
+import androidx.browser.customtabs.CustomTabsService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+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.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AbstractCustomTabsServiceTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun customTabService() {
+ val customTabsService = object : MockCustomTabsService() {
+ override val customTabsServiceStore = CustomTabsServiceStore()
+ override fun getPackageManager(): PackageManager = mock()
+ }
+
+ val customTabsServiceStub = customTabsService.onBind(mock())
+ assertNotNull(customTabsServiceStub)
+
+ val stub = customTabsServiceStub as ICustomTabsService.Stub
+
+ val callback = mock<ICustomTabsCallback>()
+ doReturn(mock<IBinder>()).`when`(callback).asBinder()
+
+ assertTrue(stub.warmup(123))
+ assertTrue(stub.newSession(callback))
+ assertNull(stub.extraCommand("", mock()))
+ assertFalse(stub.updateVisuals(mock(), mock()))
+ assertFalse(stub.requestPostMessageChannel(mock(), mock()))
+ assertEquals(
+ CustomTabsService.RESULT_FAILURE_DISALLOWED,
+ stub.postMessage(mock(), "", mock()),
+ )
+ assertFalse(
+ stub.validateRelationship(
+ mock(),
+ 0,
+ mock(),
+ mock(),
+ ),
+ )
+ assertTrue(
+ stub.mayLaunchUrl(
+ mock(),
+ mock(),
+ mock(),
+ emptyList<Bundle>(),
+ ),
+ )
+ }
+
+ @Test
+ fun `Warmup will access engine instance`() {
+ var engineAccessed = false
+
+ val customTabsService = object : MockCustomTabsService() {
+ override val engine: Engine
+ get() {
+ engineAccessed = true
+ return mock()
+ }
+ }
+
+ val stub = customTabsService.onBind(mock()) as ICustomTabsService.Stub
+
+ assertTrue(stub.warmup(42))
+
+ assertTrue(engineAccessed)
+ }
+
+ @Test
+ fun `mayLaunchUrl opens a speculative connection for most likely URL`() {
+ val engine: Engine = mock()
+
+ val customTabsService = object : MockCustomTabsService() {
+ override val engine: Engine = engine
+ }
+
+ val stub = customTabsService.onBind(mock()) as ICustomTabsService.Stub
+
+ assertTrue(stub.mayLaunchUrl(mock(), Uri.parse("https://www.mozilla.org"), Bundle(), listOf()))
+
+ verify(engine).speculativeConnect("https://www.mozilla.org")
+ }
+
+ @Test
+ fun `verifier is only created when store and client are provided`() {
+ val basic = MockCustomTabsService()
+ assertNull(basic.verifier)
+
+ val both = object : MockCustomTabsService() {
+ override val relationChecker: RelationChecker = mock()
+
+ override fun getPackageManager(): PackageManager = mock()
+ }
+ assertNotNull(both.verifier)
+ }
+
+ private open class MockCustomTabsService : AbstractCustomTabsService() {
+ override val engine: Engine = mock()
+ override val customTabsServiceStore: CustomTabsServiceStore = mock()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt
new file mode 100644
index 0000000000..097d95796a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt
@@ -0,0 +1,416 @@
+/* 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.customtabs
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.os.Binder
+import android.os.Build
+import android.os.Bundle
+import android.util.SparseArray
+import androidx.browser.customtabs.CustomTabColorSchemeParams
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.browser.customtabs.TrustedWebUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.ColorSchemeParams
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.utils.toSafeIntent
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class CustomTabConfigHelperTest {
+
+ private lateinit var resources: Resources
+
+ @Before
+ fun setup() {
+ resources = spy(testContext.resources)
+ doReturn(24f).`when`(resources).getDimension(R.dimen.mozac_feature_customtabs_max_close_button_size)
+ }
+
+ @Test
+ fun isCustomTabIntent() {
+ val customTabsIntent = CustomTabsIntent.Builder().build()
+ assertTrue(isCustomTabIntent(customTabsIntent.intent))
+ assertFalse(isCustomTabIntent(mock<Intent>()))
+ }
+
+ @Test
+ fun isTrustedWebActivityIntent() {
+ val customTabsIntent = CustomTabsIntent.Builder().build().intent
+ val trustedWebActivityIntent = Intent(customTabsIntent)
+ .putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
+ assertTrue(isTrustedWebActivityIntent(trustedWebActivityIntent))
+ assertFalse(isTrustedWebActivityIntent(customTabsIntent))
+ assertFalse(isTrustedWebActivityIntent(mock<Intent>()))
+ assertFalse(
+ isTrustedWebActivityIntent(
+ Intent().putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true),
+ ),
+ )
+ }
+
+ @Test
+ fun createFromIntentNoColorScheme() {
+ val customTabsIntent = CustomTabsIntent.Builder().build()
+
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(null, result.colorScheme)
+ }
+
+ @Test
+ fun createFromIntentWithColorScheme() {
+ val colorScheme = CustomTabsIntent.COLOR_SCHEME_SYSTEM
+ val customTabsIntent = CustomTabsIntent.Builder().setColorScheme(colorScheme).build()
+
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(colorScheme, result.colorScheme)
+ }
+
+ @Test
+ fun createFromIntentNoColorSchemeParams() {
+ val customTabsIntent = CustomTabsIntent.Builder().build()
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(null, result.colorSchemes)
+ }
+
+ @Test
+ fun createFromIntentWithDefaultColorSchemeParams() {
+ val colorSchemeParams = createColorSchemeParams()
+ val customTabsIntent = CustomTabsIntent.Builder().setDefaultColorSchemeParams(
+ createCustomTabColorSchemeParamsFrom(colorSchemeParams),
+ ).build()
+
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(colorSchemeParams, result.colorSchemes!!.defaultColorSchemeParams)
+ }
+
+ @Test
+ fun createFromIntentWithDefaultColorSchemeParamsWithNoProperties() {
+ val customTabsIntent = CustomTabsIntent.Builder().setDefaultColorSchemeParams(
+ CustomTabColorSchemeParams.Builder().build(),
+ ).build()
+
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(null, result.colorSchemes?.defaultColorSchemeParams)
+ }
+
+ @Test
+ fun createFromIntentWithLightColorSchemeParams() {
+ val colorSchemeParams = createColorSchemeParams()
+ val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams(
+ CustomTabsIntent.COLOR_SCHEME_LIGHT,
+ createCustomTabColorSchemeParamsFrom(colorSchemeParams),
+ ).build()
+
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(colorSchemeParams, result.colorSchemes!!.lightColorSchemeParams)
+ }
+
+ @Test
+ fun createFromIntentWithLightColorSchemeParamsWithNoProperties() {
+ val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams(
+ CustomTabsIntent.COLOR_SCHEME_LIGHT,
+ CustomTabColorSchemeParams.Builder().build(),
+ ).build()
+
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(null, result.colorSchemes?.lightColorSchemeParams)
+ }
+
+ @Test
+ fun createFromIntentWithDarkColorSchemeParams() {
+ val colorSchemeParams = createColorSchemeParams()
+ val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams(
+ CustomTabsIntent.COLOR_SCHEME_DARK,
+ createCustomTabColorSchemeParamsFrom(colorSchemeParams),
+ ).build()
+
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(colorSchemeParams, result.colorSchemes!!.darkColorSchemeParams)
+ }
+
+ @Test
+ fun createFromIntentWithDarkColorSchemeParamsWithNoProperties() {
+ val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams(
+ CustomTabsIntent.COLOR_SCHEME_DARK,
+ CustomTabColorSchemeParams.Builder().build(),
+ ).build()
+
+ val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertEquals(null, result.colorSchemes?.lightColorSchemeParams)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.TIRAMISU])
+ fun getColorSchemeParamsBundleOnAndroidVersionTiramisu() {
+ val colorScheme = CustomTabsIntent.COLOR_SCHEME_DARK
+ val colorSchemeParams = createColorSchemeParams()
+ val customTabColorScheme = createCustomTabColorSchemeParamsFrom(colorSchemeParams)
+ val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams(
+ colorScheme,
+ customTabColorScheme,
+ ).build()
+
+ val result = customTabsIntent.intent.toSafeIntent().getColorSchemeParamsBundle()!!
+ val expected = SparseArray<Bundle>()
+ expected.put(colorScheme, createBundleFrom(customTabColorScheme))
+
+ result[colorScheme].assertEquals(expected[colorScheme])
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.S_V2])
+ fun getColorSchemeParamsBundlePreAndroidVersionTiramisu() {
+ val colorScheme = CustomTabsIntent.COLOR_SCHEME_DARK
+ val colorSchemeParams = createColorSchemeParams()
+ val customTabColorScheme = createCustomTabColorSchemeParamsFrom(colorSchemeParams)
+ val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams(
+ colorScheme,
+ customTabColorScheme,
+ ).build()
+
+ val result = customTabsIntent.intent.toSafeIntent().getColorSchemeParamsBundle()!!
+ val expected = SparseArray<Bundle>()
+ expected.put(colorScheme, createBundleFrom(customTabColorScheme))
+
+ result[colorScheme].assertEquals(expected[colorScheme])
+ }
+
+ @Test
+ fun createFromIntentWithCloseButton() {
+ val size = 24
+ val builder = CustomTabsIntent.Builder()
+ val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888)
+ builder.setCloseButtonIcon(closeButtonIcon)
+
+ val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources)
+ assertEquals(closeButtonIcon, customTabConfig.closeButtonIcon)
+ assertEquals(size, customTabConfig.closeButtonIcon?.width)
+ assertEquals(size, customTabConfig.closeButtonIcon?.height)
+
+ val customTabConfigNoResources = createCustomTabConfigFromIntent(builder.build().intent, null)
+ assertEquals(closeButtonIcon, customTabConfigNoResources.closeButtonIcon)
+ assertEquals(size, customTabConfigNoResources.closeButtonIcon?.width)
+ assertEquals(size, customTabConfigNoResources.closeButtonIcon?.height)
+ }
+
+ @Test
+ fun createFromIntentWithMaxOversizedCloseButton() {
+ val size = 64
+ val builder = CustomTabsIntent.Builder()
+ val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888)
+ builder.setCloseButtonIcon(closeButtonIcon)
+
+ val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources)
+ assertNull(customTabConfig.closeButtonIcon)
+
+ val customTabConfigNoResources = createCustomTabConfigFromIntent(builder.build().intent, null)
+ assertEquals(closeButtonIcon, customTabConfigNoResources.closeButtonIcon)
+ }
+
+ @Test
+ fun createFromIntentUsingDisplayMetricsForCloseButton() {
+ val size = 64
+ val builder = CustomTabsIntent.Builder()
+ val resources: Resources = mock()
+ val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888)
+ builder.setCloseButtonIcon(closeButtonIcon)
+
+ `when`(resources.getDimension(R.dimen.mozac_feature_customtabs_max_close_button_size)).thenReturn(64f)
+
+ val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, resources)
+ assertEquals(closeButtonIcon, customTabConfig.closeButtonIcon)
+ }
+
+ @Test
+ fun createFromIntentWithInvalidCloseButton() {
+ val customTabsIntent = CustomTabsIntent.Builder().build()
+ // Intent is a parcelable but not a Bitmap
+ customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_CLOSE_BUTTON_ICON, Intent())
+
+ val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+ assertNull(customTabConfig.closeButtonIcon)
+ }
+
+ @Test
+ fun createFromIntentWithUrlbarHiding() {
+ val builder = CustomTabsIntent.Builder()
+ builder.setUrlBarHidingEnabled(true)
+
+ val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources)
+ assertTrue(customTabConfig.enableUrlbarHiding)
+ }
+
+ @Test
+ fun createFromIntentWithShareMenuItem() {
+ val builder = CustomTabsIntent.Builder()
+ builder.setShareState(CustomTabsIntent.SHARE_STATE_ON)
+
+ val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources)
+ assertTrue(customTabConfig.showShareMenuItem)
+ }
+
+ @Test
+ fun createFromIntentWithShareState() {
+ val builder = CustomTabsIntent.Builder()
+ builder.setShareState(CustomTabsIntent.SHARE_STATE_ON)
+
+ val extraShareState = builder.build().intent.getIntExtra(CustomTabsIntent.EXTRA_SHARE_STATE, 5)
+ assertEquals(CustomTabsIntent.SHARE_STATE_ON, extraShareState)
+ }
+
+ @Test
+ fun createFromIntentWithCustomizedMenu() {
+ val builder = CustomTabsIntent.Builder()
+ val pendingIntent = PendingIntent.getActivity(null, 0, null, 0)
+ builder.addMenuItem("menuitem1", pendingIntent)
+ builder.addMenuItem("menuitem2", pendingIntent)
+
+ val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources)
+ assertEquals(2, customTabConfig.menuItems.size)
+ assertEquals("menuitem1", customTabConfig.menuItems[0].name)
+ assertSame(pendingIntent, customTabConfig.menuItems[0].pendingIntent)
+ assertEquals("menuitem2", customTabConfig.menuItems[1].name)
+ assertSame(pendingIntent, customTabConfig.menuItems[1].pendingIntent)
+ }
+
+ @Test
+ fun createFromIntentWithActionButton() {
+ val builder = CustomTabsIntent.Builder()
+
+ val bitmap = mock<Bitmap>()
+ val intent = PendingIntent.getActivity(testContext, 0, Intent("testAction"), 0)
+ builder.setActionButton(bitmap, "desc", intent)
+
+ val customTabsIntent = builder.build()
+ val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertNotNull(customTabConfig.actionButtonConfig)
+ assertEquals("desc", customTabConfig.actionButtonConfig?.description)
+ assertEquals(intent, customTabConfig.actionButtonConfig?.pendingIntent)
+ assertEquals(bitmap, customTabConfig.actionButtonConfig?.icon)
+ assertFalse(customTabConfig.actionButtonConfig!!.tint)
+ }
+
+ @Test
+ fun createFromIntentWithInvalidActionButton() {
+ val customTabsIntent = CustomTabsIntent.Builder().build()
+
+ val invalid = Bundle()
+ customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, invalid)
+ val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+
+ assertNull(customTabConfig.actionButtonConfig)
+ }
+
+ @Test
+ fun createFromIntentWithInvalidExtras() {
+ val customTabsIntent = CustomTabsIntent.Builder().build()
+
+ val extrasField = Intent::class.java.getDeclaredField("mExtras")
+ extrasField.isAccessible = true
+ extrasField.set(customTabsIntent.intent, null)
+ extrasField.isAccessible = false
+
+ assertFalse(isCustomTabIntent(customTabsIntent.intent))
+
+ // Make sure we're not failing
+ val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+ assertNotNull(customTabConfig)
+ assertNull(customTabConfig.actionButtonConfig)
+ }
+
+ @Test
+ fun createFromIntentWithExitAnimationOption() {
+ val customTabsIntent = CustomTabsIntent.Builder().build()
+ val bundle = Bundle()
+ customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE, bundle)
+
+ val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+ assertEquals(bundle, customTabConfig.exitAnimations)
+ }
+
+ @Test
+ fun createFromIntentWithPageTitleOption() {
+ val customTabsIntent = CustomTabsIntent.Builder().build()
+ customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.SHOW_PAGE_TITLE)
+
+ val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources)
+ assertTrue(customTabConfig.titleVisible)
+ }
+
+ @Test
+ fun createFromIntentWithSessionToken() {
+ val customTabsIntent: Intent = mock()
+ val bundle: Bundle = mock()
+ val binder: Binder = mock()
+ `when`(customTabsIntent.extras).thenReturn(bundle)
+ `when`(bundle.getBinder(CustomTabsIntent.EXTRA_SESSION)).thenReturn(binder)
+
+ val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent, testContext.resources)
+ assertNotNull(customTabConfig.sessionToken)
+ }
+
+ private fun createColorSchemeParams() = ColorSchemeParams(
+ toolbarColor = Color.BLACK,
+ secondaryToolbarColor = Color.RED,
+ navigationBarColor = Color.BLUE,
+ navigationBarDividerColor = Color.YELLOW,
+ )
+
+ private fun createCustomTabColorSchemeParamsFrom(colorSchemeParams: ColorSchemeParams): CustomTabColorSchemeParams {
+ val customTabColorSchemeBuilder = CustomTabColorSchemeParams.Builder()
+ customTabColorSchemeBuilder.setToolbarColor(colorSchemeParams.toolbarColor!!)
+ customTabColorSchemeBuilder.setSecondaryToolbarColor(colorSchemeParams.secondaryToolbarColor!!)
+ customTabColorSchemeBuilder.setNavigationBarColor(colorSchemeParams.navigationBarColor!!)
+ customTabColorSchemeBuilder.setNavigationBarDividerColor(colorSchemeParams.navigationBarDividerColor!!)
+ return customTabColorSchemeBuilder.build()
+ }
+
+ private fun createBundleFrom(customTabColorScheme: CustomTabColorSchemeParams): Bundle {
+ val expectedBundle = Bundle()
+ expectedBundle.putInt(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, customTabColorScheme.toolbarColor!!)
+ expectedBundle.putInt(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR, customTabColorScheme.secondaryToolbarColor!!)
+ expectedBundle.putInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_COLOR, customTabColorScheme.navigationBarColor!!)
+ expectedBundle.putInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_DIVIDER_COLOR, customTabColorScheme.navigationBarDividerColor!!)
+ return expectedBundle
+ }
+
+ /**
+ * As Bundle does not implement Equals, assert the values individually.
+ */
+ private fun Bundle.assertEquals(bundle: Bundle) {
+ assertEquals(bundle.getInt(CustomTabsIntent.EXTRA_TOOLBAR_COLOR), getInt(CustomTabsIntent.EXTRA_TOOLBAR_COLOR))
+ assertEquals(bundle.getInt(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR), getInt(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR))
+ assertEquals(bundle.getInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_COLOR), getInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_COLOR))
+ assertEquals(bundle.getInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_DIVIDER_COLOR), getInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_DIVIDER_COLOR))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt
new file mode 100644
index 0000000000..521726f404
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt
@@ -0,0 +1,175 @@
+/* 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.customtabs
+
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Browser
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState.Source
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.test.any
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import mozilla.components.support.utils.toSafeIntent
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class CustomTabIntentProcessorTest {
+ @Test
+ fun processCustomTabIntentWithDefaultHandlers() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(middleware = listOf(middleware))
+ val useCases = SessionUseCases(store)
+ val customTabsUseCases = CustomTabsUseCases(store, useCases.loadUrl)
+
+ val handler =
+ CustomTabIntentProcessor(customTabsUseCases.add, testContext.resources)
+
+ val intent = mock<Intent>()
+ whenever(intent.action).thenReturn(Intent.ACTION_VIEW)
+ whenever(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)).thenReturn(true)
+ whenever(intent.dataString).thenReturn("http://mozilla.org")
+ whenever(intent.putExtra(any<String>(), any<String>())).thenReturn(intent)
+
+ handler.process(intent)
+
+ store.waitUntilIdle()
+
+ var customTabId: String? = null
+
+ middleware.assertFirstAction(CustomTabListAction.AddCustomTabAction::class) { action ->
+ customTabId = action.tab.id
+ }
+
+ middleware.assertFirstAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals(customTabId, action.tabId)
+ assertEquals("http://mozilla.org", action.url)
+ assertEquals(LoadUrlFlags.external(), action.flags)
+ }
+
+ verify(intent).putExtra(eq(EXTRA_SESSION_ID), any<String>())
+
+ val customTab = store.state.findCustomTab(customTabId!!)
+ assertNotNull(customTab!!)
+ assertEquals("http://mozilla.org", customTab.content.url)
+ assertTrue(customTab.source is Source.External.CustomTab)
+ assertNotNull(customTab.config)
+ assertFalse(customTab.content.private)
+ }
+
+ @Test
+ fun processCustomTabIntentWithAdditionalHeaders() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(middleware = listOf(middleware))
+ val useCases = SessionUseCases(store)
+ val customTabsUseCases = CustomTabsUseCases(store, useCases.loadUrl)
+
+ val handler =
+ CustomTabIntentProcessor(customTabsUseCases.add, testContext.resources)
+
+ val intent = mock<Intent>()
+ whenever(intent.action).thenReturn(Intent.ACTION_VIEW)
+ whenever(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)).thenReturn(true)
+ whenever(intent.dataString).thenReturn("http://mozilla.org")
+ whenever(intent.putExtra(any<String>(), any<String>())).thenReturn(intent)
+
+ val headersBundle = Bundle().apply {
+ putString("X-Extra-Header", "true")
+ }
+ whenever(intent.getBundleExtra(Browser.EXTRA_HEADERS)).thenReturn(headersBundle)
+ val headers = handler.getAdditionalHeaders(intent.toSafeIntent())
+
+ handler.process(intent)
+
+ store.waitUntilIdle()
+
+ var customTabId: String? = null
+
+ middleware.assertFirstAction(CustomTabListAction.AddCustomTabAction::class) { action ->
+ customTabId = action.tab.id
+ }
+
+ middleware.assertFirstAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals(customTabId, action.tabId)
+ assertEquals("http://mozilla.org", action.url)
+ assertEquals(LoadUrlFlags.external(), action.flags)
+ assertEquals(headers, action.additionalHeaders)
+ }
+
+ verify(intent).putExtra(eq(EXTRA_SESSION_ID), any<String>())
+
+ val customTab = store.state.findCustomTab(customTabId!!)
+ assertNotNull(customTab!!)
+ assertEquals("http://mozilla.org", customTab.content.url)
+ assertTrue(customTab.source is Source.External.CustomTab)
+ assertNotNull(customTab.config)
+ assertFalse(customTab.content.private)
+ }
+
+ @Test
+ fun processPrivateCustomTabIntentWithDefaultHandlers() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = BrowserStore(middleware = listOf(middleware))
+ val useCases = SessionUseCases(store)
+ val customTabsUseCases = CustomTabsUseCases(store, useCases.loadUrl)
+
+ val handler =
+ CustomTabIntentProcessor(customTabsUseCases.add, testContext.resources, true)
+
+ val intent = mock<Intent>()
+ whenever(intent.action).thenReturn(Intent.ACTION_VIEW)
+ whenever(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)).thenReturn(true)
+ whenever(intent.dataString).thenReturn("http://mozilla.org")
+ whenever(intent.putExtra(any<String>(), any<String>())).thenReturn(intent)
+
+ handler.process(intent)
+
+ store.waitUntilIdle()
+
+ var customTabId: String? = null
+
+ middleware.assertFirstAction(CustomTabListAction.AddCustomTabAction::class) { action ->
+ customTabId = action.tab.id
+ }
+
+ middleware.assertFirstAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals(customTabId, action.tabId)
+ assertEquals("http://mozilla.org", action.url)
+ assertEquals(LoadUrlFlags.external(), action.flags)
+ }
+
+ verify(intent).putExtra(eq(EXTRA_SESSION_ID), any<String>())
+
+ val customTab = store.state.findCustomTab(customTabId!!)
+ assertNotNull(customTab!!)
+ assertEquals("http://mozilla.org", customTab.content.url)
+ assertTrue(customTab.source is Source.External.CustomTab)
+ assertNotNull(customTab.config)
+ assertTrue(customTab.content.private)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabWindowFeatureTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabWindowFeatureTest.kt
new file mode 100644
index 0000000000..8be4a6edab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabWindowFeatureTest.kt
@@ -0,0 +1,164 @@
+/* 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.customtabs
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ColorSchemeParams
+import mozilla.components.browser.state.state.ColorSchemes
+import mozilla.components.browser.state.state.CustomTabActionButtonConfig
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabMenuItem
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class CustomTabWindowFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var store: BrowserStore
+ private val sessionId = "session-uuid"
+ private lateinit var activity: Activity
+ private lateinit var engineSession: EngineSession
+
+ @Before
+ fun setup() {
+ activity = mock()
+ engineSession = mock()
+
+ store = spy(
+ BrowserStore(
+ BrowserState(
+ customTabs = listOf(
+ createCustomTab(
+ id = sessionId,
+ url = "https://www.mozilla.org",
+ engineSession = engineSession,
+ ),
+ ),
+ ),
+ ),
+ )
+
+ whenever(activity.packageName).thenReturn("org.mozilla.firefox")
+ }
+
+ @Test
+ fun `given a request to open window, when the url can be handled, then the activity should start`() {
+ val feature = spy(CustomTabWindowFeature(activity, store, sessionId))
+ val windowRequest: WindowRequest = mock()
+
+ feature.start()
+ whenever(windowRequest.type).thenReturn(WindowRequest.Type.OPEN)
+ whenever(windowRequest.url).thenReturn("https://www.firefox.com")
+ store.dispatch(ContentAction.UpdateWindowRequestAction(sessionId, windowRequest)).joinBlocking()
+
+ verify(activity).startActivity(any(), any())
+ verify(store).dispatch(ContentAction.ConsumeWindowRequestAction(sessionId))
+ }
+
+ @Test
+ fun `given a request to open window, when the url can't be handled, then handleError should be called`() {
+ val exception = ActivityNotFoundException()
+ val feature = spy(CustomTabWindowFeature(activity, store, sessionId))
+ val windowRequest: WindowRequest = mock()
+
+ feature.start()
+ whenever(windowRequest.type).thenReturn(WindowRequest.Type.OPEN)
+ whenever(windowRequest.url).thenReturn("blob:https://www.firefox.com")
+ whenever(activity.startActivity(any(), any())).thenThrow(exception)
+ store.dispatch(ContentAction.UpdateWindowRequestAction(sessionId, windowRequest)).joinBlocking()
+ verify(engineSession).loadUrl("blob:https://www.firefox.com")
+ }
+
+ @Test
+ fun `creates intent based on default custom tab config`() {
+ val feature = CustomTabWindowFeature(activity, store, sessionId)
+ val config = CustomTabConfig()
+ val intent = feature.configToIntent(config)
+
+ val newConfig = createCustomTabConfigFromIntent(intent.intent, null)
+ assertEquals("org.mozilla.firefox", intent.intent.`package`)
+ assertEquals(config, newConfig)
+ }
+
+ @Test
+ fun `creates intent based on custom tab config`() {
+ val feature = CustomTabWindowFeature(activity, store, sessionId)
+ val config = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = ColorSchemeParams(
+ toolbarColor = Color.RED,
+ navigationBarColor = Color.BLUE,
+ ),
+ ),
+ enableUrlbarHiding = true,
+ showShareMenuItem = true,
+ titleVisible = true,
+ )
+ val intent = feature.configToIntent(config)
+
+ val newConfig = createCustomTabConfigFromIntent(intent.intent, null)
+ assertEquals("org.mozilla.firefox", intent.intent.`package`)
+ assertEquals(config, newConfig)
+ }
+
+ @Test
+ fun `creates intent with same menu items`() {
+ val feature = CustomTabWindowFeature(activity, store, sessionId)
+ val config = CustomTabConfig(
+ actionButtonConfig = CustomTabActionButtonConfig(
+ description = "button",
+ icon = mock(),
+ pendingIntent = mock(),
+ ),
+ menuItems = listOf(
+ CustomTabMenuItem("Item A", mock()),
+ CustomTabMenuItem("Item B", mock()),
+ CustomTabMenuItem("Item C", mock()),
+ ),
+ )
+ val intent = feature.configToIntent(config)
+
+ val newConfig = createCustomTabConfigFromIntent(intent.intent, null)
+ assertEquals("org.mozilla.firefox", intent.intent.`package`)
+ assertEquals(config, newConfig)
+ }
+
+ @Test
+ fun `handles no requests when stopped`() {
+ val feature = CustomTabWindowFeature(activity, store, sessionId)
+ feature.start()
+ feature.stop()
+
+ val windowRequest: WindowRequest = mock()
+ whenever(windowRequest.type).thenReturn(WindowRequest.Type.OPEN)
+ whenever(windowRequest.url).thenReturn("https://www.firefox.com")
+ store.dispatch(ContentAction.UpdateWindowRequestAction(sessionId, windowRequest)).joinBlocking()
+ verify(activity, never()).startActivity(any(), any())
+ verify(store, never()).dispatch(ContentAction.ConsumeWindowRequestAction(sessionId))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt
new file mode 100644
index 0000000000..4a97ba985a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt
@@ -0,0 +1,1582 @@
+/* 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.customtabs
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.core.content.ContextCompat.getColor
+import androidx.core.view.forEach
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ColorSchemeParams
+import mozilla.components.browser.state.state.ColorSchemes
+import mozilla.components.browser.state.state.CustomTabActionButtonConfig
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabMenuItem
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doNothing
+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 CustomTabsToolbarFeatureTest {
+ @Test
+ fun `start without sessionId invokes nothing`() {
+ val store = BrowserStore()
+ val toolbar: BrowserToolbar = mock()
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = null, useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature, never()).init(any())
+ }
+
+ @Test
+ fun `start calls initialize with the sessionId`() {
+ val tab = createCustomTab("https://www.mozilla.org", id = "mozilla")
+
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = BrowserToolbar(testContext)
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature).init(tab.config)
+
+ // Calling start again should NOT call init again
+
+ feature.start()
+
+ verify(feature, times(1)).init(tab.config)
+ }
+
+ @Test
+ fun `initialize updates toolbar`() {
+ val tab = createCustomTab("https://www.mozilla.org", id = "mozilla")
+
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = BrowserToolbar(testContext)
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}
+
+ feature.init(tab.config)
+
+ assertFalse(toolbar.display.onUrlClicked.invoke())
+ }
+
+ @Test
+ fun `initialize updates toolbar, window and text color`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = ColorSchemeParams(
+ toolbarColor = Color.RED,
+ navigationBarColor = Color.BLUE,
+ ),
+ ),
+ ),
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val window: Window = mock()
+ `when`(window.decorView).thenReturn(mock())
+ val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases, window = window) {}
+
+ feature.init(tab.config)
+
+ verify(toolbar).setBackgroundColor(Color.RED)
+ verify(window).statusBarColor = Color.RED
+ verify(window).navigationBarColor = Color.BLUE
+
+ assertEquals(Color.WHITE, toolbar.display.colors.title)
+ assertEquals(Color.WHITE, toolbar.display.colors.text)
+ }
+
+ @Test
+ fun `initialize does not update toolbar background if flag is set`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = ColorSchemeParams(toolbarColor = Color.RED),
+ ),
+ ),
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val window: Window = mock()
+ `when`(window.decorView).thenReturn(mock())
+
+ run {
+ val feature = CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ window = window,
+ updateTheme = false,
+ ) {}
+
+ feature.init(tab.config)
+
+ verify(toolbar, never()).setBackgroundColor(Color.RED)
+ }
+
+ run {
+ val feature = CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ window = window,
+ updateTheme = true,
+ ) {}
+
+ feature.init(tab.config)
+
+ verify(toolbar).setBackgroundColor(Color.RED)
+ }
+ }
+
+ @Test
+ fun `adds close button`() {
+ val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig())
+
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}
+
+ feature.start()
+
+ verify(toolbar).addNavigationAction(any())
+ }
+
+ @Test
+ fun `doesn't add close button if the button should be hidden`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ showCloseButton = false,
+ ),
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}
+
+ feature.start()
+
+ verify(toolbar, never()).addNavigationAction(any())
+ }
+
+ @Test
+ fun `close button invokes callback and removes session`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ initialState = BrowserState(
+ customTabs = listOf(
+ createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()),
+ ),
+ ),
+ )
+
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ var closeClicked = false
+ val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {
+ closeClicked = true
+ }
+
+ feature.start()
+
+ verify(toolbar).addNavigationAction(any())
+
+ val button = extractActionView(toolbar, testContext.getString(R.string.mozac_feature_customtabs_exit_button))
+
+ middleware.assertNotDispatched(CustomTabListAction.RemoveCustomTabAction::class)
+
+ button?.performClick()
+
+ assertTrue(closeClicked)
+
+ middleware.assertLastAction(CustomTabListAction.RemoveCustomTabAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ }
+ }
+
+ @Test
+ fun `does not add share button by default`() {
+ val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig())
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature, never()).addShareButton(anyInt())
+ verify(toolbar, never()).addBrowserAction(any())
+ }
+
+ @Test
+ fun `adds share button`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ showShareMenuItem = true,
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature).addShareButton(anyInt())
+ verify(toolbar).addBrowserAction(any())
+ }
+
+ @Test
+ fun `share button uses custom share listener`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ showShareMenuItem = true,
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ var clicked = false
+ val feature = CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ shareListener = { clicked = true },
+ ) {}
+
+ feature.start()
+
+ val captor = argumentCaptor<Toolbar.ActionButton>()
+ verify(toolbar).addBrowserAction(captor.capture())
+
+ val button = captor.value.createView(FrameLayout(testContext))
+ button.performClick()
+ assertTrue(clicked)
+ }
+
+ @Test
+ fun `initialize calls addActionButton`() {
+ val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig())
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature).addActionButton(anyInt(), any())
+ }
+
+ @Test
+ fun `GIVEN a square icon larger than the max drawable size WHEN adding action button to toolbar THEN the icon is scaled to fit`() {
+ val captor = argumentCaptor<Toolbar.ActionButton>()
+ val size = 48
+ val pendingIntent: PendingIntent = mock()
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ actionButtonConfig = CustomTabActionButtonConfig(
+ description = "Button",
+ icon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888),
+ pendingIntent = pendingIntent,
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature).addActionButton(anyInt(), any())
+ verify(toolbar).addBrowserAction(captor.capture())
+
+ val button = captor.value.createView(FrameLayout(testContext))
+ assertEquals(24, (button as ImageButton).drawable.intrinsicHeight)
+ assertEquals(24, button.drawable.intrinsicWidth)
+ }
+
+ @Test
+ fun `GIVEN a wide icon larger than the max drawable size WHEN adding action button to toolbar THEN the icon is scaled to fit`() {
+ val captor = argumentCaptor<Toolbar.ActionButton>()
+ val width = 96
+ val height = 48
+ val pendingIntent: PendingIntent = mock()
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ actionButtonConfig = CustomTabActionButtonConfig(
+ description = "Button",
+ icon = Bitmap.createBitmap(IntArray(width * height), width, height, Bitmap.Config.ARGB_8888),
+ pendingIntent = pendingIntent,
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature).addActionButton(anyInt(), any())
+ verify(toolbar).addBrowserAction(captor.capture())
+
+ val button = captor.value.createView(FrameLayout(testContext))
+ assertEquals(24, (button as ImageButton).drawable.intrinsicHeight)
+ assertEquals(48, button.drawable.intrinsicWidth)
+ }
+
+ @Test
+ fun `GIVEN a tall icon larger than the max drawable size WHEN adding action button to toolbar THEN the icon is scaled to fit`() {
+ val captor = argumentCaptor<Toolbar.ActionButton>()
+ val width = 24
+ val height = 48
+ val pendingIntent: PendingIntent = mock()
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ actionButtonConfig = CustomTabActionButtonConfig(
+ description = "Button",
+ icon = Bitmap.createBitmap(IntArray(width * height), width, height, Bitmap.Config.ARGB_8888),
+ pendingIntent = pendingIntent,
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature).addActionButton(anyInt(), any())
+ verify(toolbar).addBrowserAction(captor.capture())
+
+ val button = captor.value.createView(FrameLayout(testContext))
+ assertEquals(24, (button as ImageButton).drawable.intrinsicHeight)
+ assertEquals(12, button.drawable.intrinsicWidth)
+ }
+
+ @Test
+ fun `action button uses updated url`() {
+ val size = 48
+ val pendingIntent: PendingIntent = mock()
+ val captor = argumentCaptor<Toolbar.ActionButton>()
+ val intentCaptor = argumentCaptor<Intent>()
+
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ actionButtonConfig = CustomTabActionButtonConfig(
+ description = "Button",
+ icon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888),
+ pendingIntent = pendingIntent,
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(
+ "mozilla",
+ "https://github.com/mozilla-mobile/android-components",
+ ),
+ ).joinBlocking()
+
+ verify(feature).addActionButton(anyInt(), any())
+ verify(toolbar).addBrowserAction(captor.capture())
+
+ doNothing().`when`(pendingIntent).send(any(), anyInt(), any())
+
+ val button = captor.value.createView(FrameLayout(testContext))
+ button.performClick()
+
+ verify(pendingIntent).send(any(), anyInt(), intentCaptor.capture())
+ assertEquals("https://github.com/mozilla-mobile/android-components", intentCaptor.value.dataString)
+ }
+
+ @Test
+ fun `initialize calls addMenuItems when config has items`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem("Share", mock()),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ verify(feature).addMenuItems(anyList(), anyInt())
+ }
+
+ @Test
+ fun `initialize calls addMenuItems when menuBuilder has items`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem("Share", mock()),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ ) {},
+ )
+
+ feature.start()
+
+ verify(feature).addMenuItems(anyList(), anyInt())
+ }
+
+ @Test
+ fun `menu items added WITHOUT current items`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem("Share", mock()),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ ) {},
+ )
+
+ feature.start()
+
+ val menuBuilder = toolbar.display.menuBuilder
+ assertEquals(1, menuBuilder!!.items.size)
+ }
+
+ @Test
+ fun `menu items added WITH current items`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem("Share", mock()),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ ) {},
+ )
+
+ feature.start()
+
+ val menuBuilder = toolbar.display.menuBuilder
+ assertEquals(3, menuBuilder!!.items.size)
+ }
+
+ @Test
+ fun `menu item added at specified index`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem("Share", mock()),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 1,
+ ) {},
+ )
+
+ feature.start()
+
+ val menuBuilder = toolbar.display.menuBuilder!!
+
+ assertEquals(3, menuBuilder.items.size)
+ assertTrue(menuBuilder.items[1] is SimpleBrowserMenuItem)
+ }
+
+ @Test
+ fun `menu item added appended if index too large`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem("Share", mock()),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {},
+ )
+
+ feature.start()
+
+ val menuBuilder = toolbar.display.menuBuilder!!
+
+ assertEquals(3, menuBuilder.items.size)
+ assertTrue(menuBuilder.items[2] is SimpleBrowserMenuItem)
+ }
+
+ @Test
+ fun `menu item added appended if index too small`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem("Share", mock()),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = -4,
+ ) {},
+ )
+
+ feature.start()
+
+ val menuBuilder = toolbar.display.menuBuilder!!
+
+ assertEquals(3, menuBuilder.items.size)
+ assertTrue(menuBuilder.items[0] is SimpleBrowserMenuItem)
+ }
+
+ @Test
+ fun `menu item uses updated url`() {
+ val pendingIntent: PendingIntent = mock()
+ val intentCaptor = argumentCaptor<Intent>()
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem("Share", pendingIntent),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {})
+
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(
+ "mozilla",
+ "https://github.com/mozilla-mobile/android-components",
+ ),
+ ).joinBlocking()
+
+ val menuBuilder = toolbar.display.menuBuilder!!
+
+ val item = menuBuilder.items[0]
+
+ val menu: BrowserMenu = mock()
+ val view = TextView(testContext)
+
+ item.bind(menu, view)
+
+ view.performClick()
+
+ doNothing().`when`(pendingIntent).send(any(), anyInt(), any())
+
+ verify(pendingIntent).send(any(), anyInt(), intentCaptor.capture())
+ assertEquals("https://github.com/mozilla-mobile/android-components", intentCaptor.value.dataString)
+ }
+
+ @Test
+ fun `onBackPressed removes initialized session`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ customTabs = listOf(
+ createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()),
+ ),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ var closeExecuted = false
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {
+ closeExecuted = true
+ },
+ )
+
+ feature.start()
+
+ val result = feature.onBackPressed()
+
+ assertTrue(result)
+ assertTrue(closeExecuted)
+ }
+
+ @Test
+ fun `onBackPressed without a session does nothing`() {
+ val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig())
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ var closeExecuted = false
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = null,
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {
+ closeExecuted = true
+ },
+ )
+
+ feature.start()
+
+ val result = feature.onBackPressed()
+
+ assertFalse(result)
+ assertFalse(closeExecuted)
+ }
+
+ @Test
+ fun `onBackPressed with uninitialized feature returns false`() {
+ val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig())
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ var closeExecuted = false
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = null,
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {
+ closeExecuted = true
+ },
+ )
+
+ val result = feature.onBackPressed()
+
+ assertFalse(result)
+ assertFalse(closeExecuted)
+ }
+
+ @Test
+ fun `WHEN config toolbar color is dark THEN readableColor is white`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = ColorSchemeParams(toolbarColor = Color.BLACK),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {},
+ )
+
+ feature.start()
+
+ verify(feature).updateTheme(
+ tab.config.colorSchemes!!.defaultColorSchemeParams!!.toolbarColor,
+ tab.config.colorSchemes!!.defaultColorSchemeParams!!.toolbarColor,
+ tab.config.colorSchemes!!.defaultColorSchemeParams!!.navigationBarDividerColor,
+ Color.WHITE,
+ )
+ verify(feature).addCloseButton(Color.WHITE, tab.config.closeButtonIcon)
+ verify(feature).addActionButton(Color.WHITE, tab.config.actionButtonConfig)
+ assertEquals(Color.WHITE, toolbar.display.colors.text)
+ }
+
+ @Test
+ fun `WHEN config toolbar color is not dark THEN readableColor is black`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = ColorSchemeParams(toolbarColor = Color.WHITE),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {},
+ )
+
+ feature.start()
+
+ verify(feature).updateTheme(
+ tab.config.colorSchemes!!.defaultColorSchemeParams!!.toolbarColor,
+ tab.config.colorSchemes!!.defaultColorSchemeParams!!.toolbarColor,
+ tab.config.colorSchemes!!.defaultColorSchemeParams!!.navigationBarDividerColor,
+ Color.BLACK,
+ )
+ verify(feature).addCloseButton(Color.BLACK, tab.config.closeButtonIcon)
+ verify(feature).addActionButton(Color.BLACK, tab.config.actionButtonConfig)
+ }
+
+ @Test
+ fun `WHEN config toolbar has no colour set THEN readableColor uses the toolbar display menu colour`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {},
+ )
+
+ feature.start()
+
+ verify(feature).updateTheme(
+ tab.config.colorSchemes?.defaultColorSchemeParams?.toolbarColor,
+ tab.config.colorSchemes?.defaultColorSchemeParams?.toolbarColor,
+ tab.config.colorSchemes?.defaultColorSchemeParams?.navigationBarDividerColor,
+ toolbar.display.colors.menu,
+ )
+ verify(feature).addCloseButton(toolbar.display.colors.menu, tab.config.closeButtonIcon)
+ verify(feature).addActionButton(toolbar.display.colors.menu, tab.config.actionButtonConfig)
+ assertEquals(Color.WHITE, toolbar.display.colors.menu)
+ }
+
+ @Test
+ fun `WHEN tab is private THEN readableColor is the default private color`() {
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(showShareMenuItem = true),
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store = store,
+ toolbar = toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ updateTheme = false,
+ ) {},
+ )
+
+ feature.start()
+
+ val colorResId = testContext.theme.resolveAttribute(android.R.attr.textColorPrimary)
+ val privateColor = getColor(testContext, colorResId)
+ verify(feature).addCloseButton(privateColor, tab.config.closeButtonIcon)
+ verify(feature).addActionButton(privateColor, tab.config.actionButtonConfig)
+ verify(feature).addShareButton(privateColor)
+ }
+
+ @Test
+ fun `WHEN COLOR_SCHEME_SYSTEM THEN toNightMode returns MODE_NIGHT_FOLLOW_SYSTEM`() {
+ assertEquals(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, CustomTabsIntent.COLOR_SCHEME_SYSTEM.toNightMode())
+ }
+
+ @Test
+ fun `WHEN COLOR_SCHEME_LIGHT THEN toNightMode returns MODE_NIGHT_NO`() {
+ assertEquals(AppCompatDelegate.MODE_NIGHT_NO, CustomTabsIntent.COLOR_SCHEME_LIGHT.toNightMode())
+ }
+
+ @Test
+ fun `WHEN COLOR_SCHEME_DARK THEN toNightMode returns MODE_NIGHT_YES`() {
+ assertEquals(AppCompatDelegate.MODE_NIGHT_YES, CustomTabsIntent.COLOR_SCHEME_DARK.toNightMode())
+ }
+
+ @Test
+ fun `WHEN unknown color scheme THEN toNightMode returns null`() {
+ assertEquals(null, 100.toNightMode())
+ }
+
+ @Test
+ fun `WHEN no color scheme params set THEN getConfiguredColorSchemeParams returns null `() {
+ val customTabConfig = CustomTabConfig()
+ assertEquals(null, customTabConfig.colorSchemes?.getConfiguredColorSchemeParams())
+ }
+
+ @Test
+ fun `WHEN only default color scheme params set THEN getConfiguredColorSchemeParams returns default `() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ defaultColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode follow system and is light mode THEN getConfiguredColorSchemeParams returns light color scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ lightColorSchemeParams = lightColorSchemeParams,
+ darkColorSchemeParams = darkColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ lightColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(
+ nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode follow system, is light mode no light color scheme THEN getConfiguredColorSchemeParams returns default scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ darkColorSchemeParams = darkColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ defaultColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(
+ nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode follow system and is dark mode THEN getConfiguredColorSchemeParams returns dark color scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ lightColorSchemeParams = lightColorSchemeParams,
+ darkColorSchemeParams = darkColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ darkColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(
+ nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
+ isDarkMode = true,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode follow system, is dark mode no dark color scheme THEN getConfiguredColorSchemeParams returns default scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ lightColorSchemeParams = lightColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ defaultColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(
+ nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
+ isDarkMode = true,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode no THEN getConfiguredColorSchemeParams returns light color scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ lightColorSchemeParams = lightColorSchemeParams,
+ darkColorSchemeParams = darkColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ lightColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(
+ nightMode = AppCompatDelegate.MODE_NIGHT_NO,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode no & no light color params THEN getConfiguredColorSchemeParams returns default color scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ darkColorSchemeParams = darkColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ defaultColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(
+ nightMode = AppCompatDelegate.MODE_NIGHT_NO,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode yes THEN getConfiguredColorSchemeParams returns dark color scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ lightColorSchemeParams = lightColorSchemeParams,
+ darkColorSchemeParams = darkColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ darkColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(
+ nightMode = AppCompatDelegate.MODE_NIGHT_YES,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode yes & no dark color params THEN getConfiguredColorSchemeParams returns default color scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ lightColorSchemeParams = lightColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ defaultColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(
+ nightMode = AppCompatDelegate.MODE_NIGHT_YES,
+ ),
+ )
+ }
+
+ @Test
+ fun `WHEN night mode not set THEN getConfiguredColorSchemeParams returns default color scheme`() {
+ val customTabConfig = CustomTabConfig(
+ colorSchemes = ColorSchemes(
+ defaultColorSchemeParams = defaultColorSchemeParams,
+ lightColorSchemeParams = lightColorSchemeParams,
+ darkColorSchemeParams = darkColorSchemeParams,
+ ),
+ )
+
+ assertEquals(
+ defaultColorSchemeParams,
+ customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(),
+ )
+ }
+
+ @Test
+ fun `WHEN ColorSchemeParams has all properties THEN withDefault returns the same ColorSchemeParams`() {
+ val result = lightColorSchemeParams.withDefault(defaultColorSchemeParams)
+
+ assertEquals(lightColorSchemeParams, result)
+ }
+
+ @Test
+ fun `WHEN ColorSchemeParams has some properties THEN withDefault uses default for the missing properties`() {
+ val colorSchemeParams = ColorSchemeParams(
+ toolbarColor = Color.BLACK,
+ navigationBarDividerColor = Color.YELLOW,
+ )
+
+ val expected = ColorSchemeParams(
+ toolbarColor = colorSchemeParams.toolbarColor,
+ secondaryToolbarColor = defaultColorSchemeParams.secondaryToolbarColor,
+ navigationBarColor = defaultColorSchemeParams.navigationBarColor,
+ navigationBarDividerColor = colorSchemeParams.navigationBarDividerColor,
+ )
+
+ val result = colorSchemeParams.withDefault(defaultColorSchemeParams)
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN ColorSchemeParams has no properties THEN withDefault returns all default ColorSchemeParams`() {
+ val result = ColorSchemeParams().withDefault(defaultColorSchemeParams)
+
+ assertEquals(defaultColorSchemeParams, result)
+ }
+
+ @Test
+ fun `show title only if not empty`() {
+ val dispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(),
+ title = "",
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {},
+ )
+
+ feature.start()
+
+ assertEquals("", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateTitleAction(
+ "mozilla",
+ "Internet for people, not profit - Mozilla",
+ ),
+ ).joinBlocking()
+
+ assertEquals("Internet for people, not profit - Mozilla", toolbar.title)
+
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `Will use URL as title if title was shown once and is now empty`() {
+ val dispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+
+ val tab = createCustomTab(
+ "https://www.mozilla.org",
+ id = "mozilla",
+ config = CustomTabConfig(),
+ title = "",
+ )
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(tab),
+ ),
+ )
+ val toolbar = spy(BrowserToolbar(testContext))
+ val useCases = CustomTabsUseCases(
+ store = store,
+ loadUrlUseCase = SessionUseCases(store).loadUrl,
+ )
+ val feature = spy(
+ CustomTabsToolbarFeature(
+ store,
+ toolbar,
+ sessionId = "mozilla",
+ useCases = useCases,
+ menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())),
+ menuItemIndex = 4,
+ ) {},
+ )
+
+ feature.start()
+
+ feature.start()
+
+ assertEquals("", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction("mozilla", "https://www.mozilla.org/en-US/firefox/"),
+ ).joinBlocking()
+
+ assertEquals("", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateTitleAction(
+ "mozilla",
+ "Firefox - Protect your life online with privacy-first products",
+ ),
+ ).joinBlocking()
+
+ assertEquals("Firefox - Protect your life online with privacy-first products", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction("mozilla", "https://github.com/mozilla-mobile/android-components"),
+ ).joinBlocking()
+
+ assertEquals("https://github.com/mozilla-mobile/android-components", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateTitleAction("mozilla", "Le GitHub"),
+ ).joinBlocking()
+
+ assertEquals("Le GitHub", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction("mozilla", "https://github.com/mozilla-mobile/fenix"),
+ ).joinBlocking()
+
+ assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateTitleAction("mozilla", ""),
+ ).joinBlocking()
+
+ assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateTitleAction(
+ "mozilla",
+ "A collection of Android libraries to build browsers or browser-like applications.",
+ ),
+ ).joinBlocking()
+
+ assertEquals("A collection of Android libraries to build browsers or browser-like applications.", toolbar.title)
+
+ store.dispatch(
+ ContentAction.UpdateTitleAction("mozilla", ""),
+ ).joinBlocking()
+
+ assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title)
+ }
+
+ private fun extractActionView(
+ browserToolbar: BrowserToolbar,
+ contentDescription: String,
+ ): ImageButton? {
+ var actionView: ImageButton? = null
+
+ browserToolbar.forEach { group ->
+ val viewGroup = group as ViewGroup
+
+ viewGroup.forEach inner@{ subGroup ->
+ if (subGroup is ViewGroup) {
+ subGroup.forEach {
+ if (it is ImageButton && it.contentDescription == contentDescription) {
+ actionView = it
+ return@inner
+ }
+ }
+ }
+ }
+ }
+
+ return actionView
+ }
+
+ private val defaultColorSchemeParams = ColorSchemeParams(
+ toolbarColor = Color.CYAN,
+ secondaryToolbarColor = Color.GREEN,
+ navigationBarColor = Color.WHITE,
+ navigationBarDividerColor = Color.MAGENTA,
+ )
+
+ private val lightColorSchemeParams = ColorSchemeParams(
+ toolbarColor = Color.BLACK,
+ secondaryToolbarColor = Color.RED,
+ navigationBarColor = Color.BLUE,
+ navigationBarDividerColor = Color.YELLOW,
+ )
+
+ private val darkColorSchemeParams = ColorSchemeParams(
+ toolbarColor = Color.DKGRAY,
+ secondaryToolbarColor = Color.LTGRAY,
+ navigationBarColor = Color.GRAY,
+ navigationBarDividerColor = Color.WHITE,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt
new file mode 100644
index 0000000000..c918442593
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.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.customtabs.feature
+
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.concept.toolbar.AutocompleteDelegate
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.test.ThrowProperty
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class CustomTabSessionTitleObserverTest {
+
+ @Test
+ fun `show title only if not empty`() {
+ val toolbar: Toolbar = mock()
+ val observer = CustomTabSessionTitleObserver(toolbar)
+ val url = "https://www.mozilla.org"
+ val title = "Internet for people, not profit - Mozilla"
+
+ observer.onTab(createCustomTab(url, title = ""))
+ verify(toolbar, never()).title = ""
+
+ observer.onTab(createCustomTab(url, title = title))
+ verify(toolbar).title = title
+ }
+
+ @Test
+ fun `Will use URL as title if title was shown once and is now empty`() {
+ val toolbar = MockToolbar()
+ var tab = createCustomTab("https://mozilla.org")
+ val observer = CustomTabSessionTitleObserver(toolbar)
+
+ observer.onTab(tab)
+ assertEquals("", toolbar.title)
+
+ tab = tab.withUrl("https://www.mozilla.org/en-US/firefox/")
+ observer.onTab(tab)
+ assertEquals("", toolbar.title)
+
+ tab = tab.withTitle("Firefox - Protect your life online with privacy-first products")
+ observer.onTab(tab)
+ assertEquals("Firefox - Protect your life online with privacy-first products", toolbar.title)
+
+ tab = tab.withUrl("https://github.com/mozilla-mobile/android-components")
+ observer.onTab(tab)
+ assertEquals("Firefox - Protect your life online with privacy-first products", toolbar.title)
+
+ tab = tab.withTitle("")
+ observer.onTab(tab)
+ assertEquals("https://github.com/mozilla-mobile/android-components", toolbar.title)
+
+ tab = tab.withTitle("A collection of Android libraries to build browsers or browser-like applications.")
+ observer.onTab(tab)
+ assertEquals("A collection of Android libraries to build browsers or browser-like applications.", toolbar.title)
+
+ tab = tab.withTitle("")
+ observer.onTab(tab)
+ assertEquals("https://github.com/mozilla-mobile/android-components", toolbar.title)
+ }
+
+ private class MockToolbar : Toolbar {
+ override var title: String = ""
+ override var highlight: Toolbar.Highlight = Toolbar.Highlight.NONE
+ override var url: CharSequence by ThrowProperty()
+ override var private: Boolean by ThrowProperty()
+ override var siteSecure: Toolbar.SiteSecurity by ThrowProperty()
+ override var siteTrackingProtection: Toolbar.SiteTrackingProtection by ThrowProperty()
+ override fun setSearchTerms(searchTerms: String) = Unit
+ override fun displayProgress(progress: Int) = Unit
+ override fun onBackPressed(): Boolean = false
+ override fun onStop() = Unit
+ override fun setOnUrlCommitListener(listener: (String) -> Boolean) = Unit
+ override fun setAutocompleteListener(filter: suspend (String, AutocompleteDelegate) -> Unit) = Unit
+ override fun addBrowserAction(action: Toolbar.Action) = Unit
+ override fun removeBrowserAction(action: Toolbar.Action) = Unit
+ override fun invalidateActions() = Unit
+ override fun addPageAction(action: Toolbar.Action) = Unit
+ override fun removePageAction(action: Toolbar.Action) = Unit
+ override fun addNavigationAction(action: Toolbar.Action) = Unit
+ override fun removeNavigationAction(action: Toolbar.Action) = Unit
+ override fun addEditActionStart(action: Toolbar.Action) = Unit
+ override fun addEditActionEnd(action: Toolbar.Action) = Unit
+ override fun removeEditActionEnd(action: Toolbar.Action) = Unit
+ override fun hideMenuButton() = Unit
+ override fun showMenuButton() = Unit
+ override fun setDisplayHorizontalPadding(horizontalPadding: Int) = Unit
+ override fun hidePageActionSeparator() = Unit
+ override fun showPageActionSeparator() = Unit
+ override fun setOnEditListener(listener: Toolbar.OnEditListener) = Unit
+ override fun displayMode() = Unit
+ override fun editMode(cursorPlacement: Toolbar.CursorPlacement) = Unit
+ override fun dismissMenu() = Unit
+ override fun enableScrolling() = Unit
+ override fun disableScrolling() = Unit
+ override fun collapse() = Unit
+ override fun expand() = Unit
+ }
+}
+
+private fun CustomTabSessionState.withTitle(title: String) = copy(
+ content = content.copy(title = title),
+)
+
+private fun CustomTabSessionState.withUrl(url: String) = copy(
+ content = content.copy(url = url),
+)
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt
new file mode 100644
index 0000000000..e035349879
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt
@@ -0,0 +1,82 @@
+/* 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.customtabs.feature
+
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN
+import androidx.browser.customtabs.CustomTabsSessionToken
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.customtabs.store.OriginRelationPair
+import mozilla.components.feature.customtabs.store.ValidateRelationshipAction
+import mozilla.components.feature.customtabs.store.VerificationStatus.FAILURE
+import mozilla.components.feature.customtabs.store.VerificationStatus.PENDING
+import mozilla.components.feature.customtabs.store.VerificationStatus.SUCCESS
+import mozilla.components.feature.customtabs.verify.OriginVerifier
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class OriginVerifierFeatureTest {
+
+ @Test
+ fun `verify fails if no creatorPackageName is saved`() = runTest {
+ val feature = OriginVerifierFeature(mock(), mock(), mock())
+
+ assertFalse(feature.verify(CustomTabState(), mock(), RELATION_HANDLE_ALL_URLS, mock()))
+ }
+
+ @Test
+ fun `verify returns existing relationship`() = runTest {
+ val feature = OriginVerifierFeature(mock(), mock(), mock())
+ val origin = "https://example.com".toUri()
+ val state = CustomTabState(
+ creatorPackageName = "com.example.twa",
+ relationships = mapOf(
+ OriginRelationPair(origin, RELATION_HANDLE_ALL_URLS) to SUCCESS,
+ OriginRelationPair(origin, RELATION_USE_AS_ORIGIN) to FAILURE,
+ OriginRelationPair("https://sample.com".toUri(), RELATION_HANDLE_ALL_URLS) to PENDING,
+ ),
+ )
+
+ assertTrue(feature.verify(state, mock(), RELATION_HANDLE_ALL_URLS, origin))
+ assertFalse(feature.verify(state, mock(), RELATION_USE_AS_ORIGIN, origin))
+ }
+
+ @Test
+ fun `verify checks new relationships`() = runTest {
+ val store: CustomTabsServiceStore = mock()
+ val verifier: OriginVerifier = mock()
+ val feature = spy(OriginVerifierFeature(mock(), mock()) { store.dispatch(it) })
+ doReturn(verifier).`when`(feature).getVerifier(anyString(), anyInt())
+ doReturn(true).`when`(verifier).verifyOrigin(any())
+
+ val token: CustomTabsSessionToken = mock()
+ val origin = "https://sample.com".toUri()
+ val state = CustomTabState(creatorPackageName = "com.example.twa")
+ assertNotNull(state)
+
+ assertTrue(feature.verify(state, token, RELATION_HANDLE_ALL_URLS, origin))
+
+ verify(verifier).verifyOrigin(origin)
+ verify(store).dispatch(ValidateRelationshipAction(token, RELATION_HANDLE_ALL_URLS, origin, PENDING))
+ verify(store).dispatch(ValidateRelationshipAction(token, RELATION_HANDLE_ALL_URLS, origin, SUCCESS))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidatesTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidatesTest.kt
new file mode 100644
index 0000000000..1f2e42ec1a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidatesTest.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.customtabs.menu
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabMenuItem
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class CustomTabMenuCandidatesTest {
+
+ @Test
+ fun `return an empty list if there are no menu items`() {
+ val customTabSessionState = createCustomTab(
+ url = "https://mozilla.org",
+ config = CustomTabConfig(menuItems = emptyList()),
+ )
+
+ assertEquals(
+ emptyList<MenuCandidate>(),
+ customTabSessionState.createCustomTabMenuCandidates(mock()),
+ )
+ }
+
+ @Test
+ fun `create a candidate for each menu item`() {
+ val pendingIntent1 = mock<PendingIntent>()
+ val pendingIntent2 = mock<PendingIntent>()
+ val customTabSessionState = createCustomTab(
+ url = "https://mozilla.org",
+ config = CustomTabConfig(
+ menuItems = listOf(
+ CustomTabMenuItem(
+ name = "item1",
+ pendingIntent = pendingIntent1,
+ ),
+ CustomTabMenuItem(
+ name = "item2",
+ pendingIntent = pendingIntent2,
+ ),
+ ),
+ ),
+ )
+
+ val context = mock<Context>()
+ val intent = argumentCaptor<Intent>()
+ val menuCandidates = customTabSessionState.createCustomTabMenuCandidates(context)
+
+ assertEquals(2, menuCandidates.size)
+ assertEquals("item1", menuCandidates[0].text)
+ assertEquals("item2", menuCandidates[1].text)
+
+ menuCandidates[0].onClick()
+ verify(pendingIntent1).send(eq(context), anyInt(), intent.capture())
+ assertEquals("https://mozilla.org".toUri(), intent.value.data)
+
+ menuCandidates[1].onClick()
+ verify(pendingIntent2).send(eq(context), anyInt(), intent.capture())
+ assertEquals("https://mozilla.org".toUri(), intent.value.data)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducerTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducerTest.kt
new file mode 100644
index 0000000000..bfee3db851
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducerTest.kt
@@ -0,0 +1,174 @@
+/* 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.customtabs.store
+
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN
+import androidx.browser.customtabs.CustomTabsSessionToken
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CustomTabsServiceStateReducerTest {
+
+ @Test
+ fun `reduce adds new tab to map`() {
+ val token: CustomTabsSessionToken = mock()
+ val initialState = CustomTabsServiceState()
+ val action = SaveCreatorPackageNameAction(token, "com.example.twa")
+
+ assertEquals(
+ CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(creatorPackageName = "com.example.twa"),
+ ),
+ ),
+ CustomTabsServiceStateReducer.reduce(initialState, action),
+ )
+ }
+
+ @Test
+ fun `reduce replaces existing tab in map`() {
+ val token: CustomTabsSessionToken = mock()
+ val initialState = CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(creatorPackageName = "com.example.twa"),
+ ),
+ )
+ val action = SaveCreatorPackageNameAction(token, "com.example.trusted.web.app")
+
+ assertEquals(
+ CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(creatorPackageName = "com.example.trusted.web.app"),
+ ),
+ ),
+ CustomTabsServiceStateReducer.reduce(initialState, action),
+ )
+ }
+
+ @Test
+ fun `reduce adds new relationship`() {
+ val token: CustomTabsSessionToken = mock()
+ val initialState = CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(creatorPackageName = "com.example.twa"),
+ ),
+ )
+ val action = ValidateRelationshipAction(
+ token,
+ RELATION_HANDLE_ALL_URLS,
+ "https://example.com".toUri(),
+ VerificationStatus.PENDING,
+ )
+
+ assertEquals(
+ CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(
+ creatorPackageName = "com.example.twa",
+ relationships = mapOf(
+ Pair(
+ OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS),
+ VerificationStatus.PENDING,
+ ),
+ ),
+ ),
+ ),
+ ),
+ CustomTabsServiceStateReducer.reduce(initialState, action),
+ )
+ }
+
+ @Test
+ fun `reduce adds new relationship of different type`() {
+ val token: CustomTabsSessionToken = mock()
+ val initialState = CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(
+ creatorPackageName = "com.example.twa",
+ relationships = mapOf(
+ Pair(
+ OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS),
+ VerificationStatus.FAILURE,
+ ),
+ ),
+ ),
+ ),
+ )
+ val action = ValidateRelationshipAction(
+ token,
+ RELATION_USE_AS_ORIGIN,
+ "https://example.com".toUri(),
+ VerificationStatus.PENDING,
+ )
+
+ assertEquals(
+ CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(
+ creatorPackageName = "com.example.twa",
+ relationships = mapOf(
+ Pair(
+ OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS),
+ VerificationStatus.FAILURE,
+ ),
+ Pair(
+ OriginRelationPair("https://example.com".toUri(), RELATION_USE_AS_ORIGIN),
+ VerificationStatus.PENDING,
+ ),
+ ),
+ ),
+ ),
+ ),
+ CustomTabsServiceStateReducer.reduce(initialState, action),
+ )
+ }
+
+ @Test
+ fun `reduce replaces existing relationship`() {
+ val token: CustomTabsSessionToken = mock()
+ val initialState = CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(
+ creatorPackageName = "com.example.twa",
+ relationships = mapOf(
+ Pair(
+ OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS),
+ VerificationStatus.PENDING,
+ ),
+ ),
+ ),
+ ),
+ )
+ val action = ValidateRelationshipAction(
+ token,
+ RELATION_HANDLE_ALL_URLS,
+ "https://example.com".toUri(),
+ VerificationStatus.SUCCESS,
+ )
+
+ assertEquals(
+ CustomTabsServiceState(
+ tabs = mapOf(
+ token to CustomTabState(
+ creatorPackageName = "com.example.twa",
+ relationships = mapOf(
+ Pair(
+ OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS),
+ VerificationStatus.SUCCESS,
+ ),
+ ),
+ ),
+ ),
+ ),
+ CustomTabsServiceStateReducer.reduce(initialState, action),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt
new file mode 100644
index 0000000000..5e6e384af4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt
@@ -0,0 +1,85 @@
+/* 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.customtabs.verify
+
+import android.content.pm.PackageManager
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Response
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.MockitoAnnotations.openMocks
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class OriginVerifierTest {
+
+ private val androidAsset = AssetDescriptor.Android(
+ packageName = "com.app.name",
+ sha256CertFingerprint = "AA:BB:CC:10:20:30:01:02",
+ )
+
+ @Mock private lateinit var packageManager: PackageManager
+
+ @Mock private lateinit var response: Response
+
+ @Mock private lateinit var body: Response.Body
+
+ @Mock private lateinit var checker: RelationChecker
+
+ @Suppress("Deprecation")
+ @Before
+ fun setup() {
+ openMocks(this)
+
+ doReturn(body).`when`(response).body
+ doReturn(200).`when`(response).status
+ doReturn("{\"linked\":true}").`when`(body).string()
+ }
+
+ @Test
+ fun `only HTTPS allowed`() = runTest {
+ val verifier = buildVerifier(RELATION_HANDLE_ALL_URLS)
+ assertFalse(verifier.verifyOrigin("LOL".toUri()))
+ assertFalse(verifier.verifyOrigin("http://www.android.com".toUri()))
+ }
+
+ @Test
+ fun verifyOrigin() = runTest {
+ val verifier = buildVerifier(RELATION_USE_AS_ORIGIN)
+ doReturn(true).`when`(checker).checkRelationship(
+ AssetDescriptor.Web("https://www.example.com"),
+ Relation.USE_AS_ORIGIN,
+ androidAsset,
+ )
+ assertTrue(verifier.verifyOrigin("https://www.example.com".toUri()))
+ }
+
+ private fun buildVerifier(relation: Int): OriginVerifier {
+ val verifier = spy(
+ OriginVerifier(
+ "com.app.name",
+ relation,
+ packageManager,
+ checker,
+ ),
+ )
+ doReturn(androidAsset).`when`(verifier).androidAsset
+ return verifier
+ }
+}
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/customtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..49324d83c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,3 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
+
diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/customtabs/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/customtabs/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/downloads/README.md b/mobile/android/android-components/components/feature/downloads/README.md
new file mode 100644
index 0000000000..a6e938389c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/README.md
@@ -0,0 +1,183 @@
+# [Android Components](../../../README.md) > Feature > Downloads
+
+Feature implementation for apps that want to use [Android downloads manager](https://developer.android.com/reference/android/app/DownloadManager).
+
+## Usage
+
+### 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-downloads:{latest-version}"
+```
+
+The `AbstractFetchDownloadService` also requires extra permissions needed to post notifications and to start downloads
+- `android.permission.POST_NOTIFICATIONS`
+- `android.permission.FOREGROUND_SERVICE`
+- `android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK`
+
+The implementing service in the client app also needs to declare `dataSync` as `foregroundServiceType` in the manifest for
+Android 14 compatibility. Adding the `FOREGROUND_SERVICE_DATA_SYNC` permission in the app manifest is not needed since it is declared in feature-downloads module.
+
+### DownloadsFeature
+Feature implementation for proving download functionality for the selected session.
+
+```kotlin
+
+
+//This will be called before starting each download in case you don't have the right permissions
+// or if the user removed your permissions.
+val onNeedToRequestPermissions = {
+ session: Session, download: Download ->
+ //request permission e.g (WRITE_EXTERNAL_STORAGE)
+
+ // After you get granted the permissions, remember to call downloadsFeature.onPermissionsGranted()
+ // to start downloading the pending download.
+}
+
+//This will be called after every download is completed.
+val onDownloadCompleted = {
+ download: Download, downloadId: Long ->
+ //Show some UI to let user know the download was completed.
+}
+
+val downloadsFeature =
+ DownloadsFeature(context,
+ onNeedToRequestPermissions /*Optional*/,
+ onDownloadCompleted /*Optional*/,
+ fragmentManager /*Optional, if it is provided, before every download a dialog will be shown*/,
+ dialog /*Optional, if it is not provided a simple dialog will be shown before every download, with a positive button and negative button.*/,
+ sessionManager = sessionManager)
+
+//Starts observing the selected session for new downloads and forward it to
+// the download manager
+downloadsFeature.start()
+
+//Stop observing the selected session
+downloadsFeature.stop()
+
+```
+
+### DownloadDialogFragment
+ This is general representation of a dialog meant to be used in collaboration with `DownloadsFeature`
+ to show a dialog before a download is triggered. If `SimpleDownloadDialogFragment` is not flexible enough for your use case you should inherit for this class.
+
+```kotlin
+class FocusDialogDownloadFragment : DownloadDialogFragment() {
+
+ /*Creating a customized the dialog*/
+ override fun onCreateDialog(bundle: Bundle?): AlertDialog {
+ //DownloadsFeature will add these metadata before calling show() on the dialog.
+ val fileName = arguments?.getString(KEY_FILE_NAME)
+ //Not used, just for the sake you can use this metadata
+ val url = arguments?.getString(KEY_URL)
+ val contentLength = arguments?.getString(KEY_CONTENT_LENGTH)
+
+ val builder = AlertDialog.Builder(requireContext())
+ builder.setCancelable(true)
+ builder.setTitle(getString(R.string.download_dialog_title))
+
+ val inflater = activity!!.layoutInflater
+ val dialogView = inflater.inflate(R.layout.download_dialog, null)
+ builder.setView(dialogView)
+
+ dialogView.download_dialog_icon.setImageResource(R.drawable.ic_download)
+ dialogView.download_dialog_file_name.text = fileName
+ dialogView.download_dialog_cancel.text = getString(R.string.download_dialog_action_cancel)
+ dialogView.download_dialog_download.text =
+ getString(R.string.download_dialog_action_download)
+
+ dialogView.download_dialog_warning.text = getString(R.string.download_dialog_warning)
+
+ setCancelButton(dialogView.download_dialog_cancel)
+ setDownloadButton(dialogView.download_dialog_download)
+
+ return builder.create()
+ }
+
+ private fun setDownloadButton(button: Button) {
+ button.setOnClickListener {
+ //Letting know DownloadFeature that can proceed with the download
+ onStartDownload()
+ TelemetryWrapper.downloadDialogDownloadEvent(true)
+ dismiss()
+ }
+ }
+
+ private fun setCancelButton(button: Button) {
+ button.setOnClickListener {
+ TelemetryWrapper.downloadDialogDownloadEvent(false)
+ dismiss()
+ }
+ }
+}
+
+//Adding our dialog to DownloadsFeature
+val downloadsFeature = DownloadsFeature(
+ context(),
+ sessionManager = sessionManager,
+ fragmentManager = fragmentManager,
+ dialog = FocusDialogDownloadFragment()
+ )
+
+downloadsFeature.start()
+```
+
+### SimpleDownloadDialogFragment
+
+A confirmation dialog to be called before a download is triggered.
+
+SimpleDownloadDialogFragment is the default dialog if you don't provide a value to DownloadsFeature.
+It is composed by a title, a negative and a positive bottoms. When the positive button is clicked the download is triggered.
+
+```kotlin
+//To use the default behavior, just provide a fragmentManager/childFragmentManager.
+ downloadsFeature = DownloadsFeature(
+ requireContext(),
+ sessionManager = components.sessionManager,
+ fragmentManager = fragmentManager /*If you're inside a Fragment use childFragmentManager '*/
+ )
+
+ downloadsFeature.start()
+```
+Customizing SimpleDownloadDialogFragment.
+
+```kotlin
+ val dialog = SimpleDownloadDialogFragment.newInstance(
+ dialogTitleText = R.string.dialog_title,
+ positiveButtonText = R.string.download,
+ negativeButtonText = R.string.cancel,
+ cancelable = true,
+ themeResId = R.style.your_theme
+ )
+
+ downloadsFeature = DownloadsFeature(
+ requireContext(),
+ sessionManager = components.sessionManager,
+ fragmentManager = fragmentManager,
+ dialog = dialog
+ )
+
+ downloadsFeature.start()
+ ```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Description |
+|-----------|--------------|---------------------------------------------------|
+| RESUME | notification | The user resumes a download. |
+| PAUSE | notification | The user pauses a download. |
+| CANCEL | notification | The user cancels a download. |
+| TRY_AGAIN | notification | The user taps on try again when a download fails. |
+| OPEN | notification | The user opens a downloaded file. |
+| DISPLAY | prompt | A download prompt was shown. |
+| CANCEL | prompt | A download prompt was canceled. |
+
+## 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/downloads/build.gradle b/mobile/android/android-components/components/feature/downloads/build.gradle
new file mode 100644
index 0000000000..1072c051b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/build.gradle
@@ -0,0 +1,90 @@
+/* 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: 'kotlin-parcelize'
+
+apply plugin: 'com.google.devtools.ksp'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'mozilla.components.feature.downloads'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':concept-fetch')
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':support-utils')
+ implementation project(':ui-icons')
+ implementation project(':ui-widgets')
+
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+ implementation ComponentsDependencies.androidx_localbroadcastmanager
+ implementation ComponentsDependencies.androidx_paging
+ implementation ComponentsDependencies.androidx_recyclerview
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':concept-engine')
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_arch_core_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.testing_coroutines
+
+}
+
+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/downloads/proguard-rules.pro b/mobile/android/android-components/components/feature/downloads/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/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/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/1.json b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/1.json
new file mode 100644
index 0000000000..dce503c5d4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/1.json
@@ -0,0 +1,76 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "342d0e5d0a0fcde72b88ac4585caf842",
+ "entities": [
+ {
+ "tableName": "downloads",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fileName",
+ "columnName": "file_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "contentType",
+ "columnName": "content_type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "contentLength",
+ "columnName": "content_length",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "destinationDirectory",
+ "columnName": "destination_directory",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '342d0e5d0a0fcde72b88ac4585caf842')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/2.json b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/2.json
new file mode 100644
index 0000000000..43c2d3dda9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/2.json
@@ -0,0 +1,82 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "1c1abe6e744766f8b4f8e4b402b2f099",
+ "entities": [
+ {
+ "tableName": "downloads",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `is_private` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fileName",
+ "columnName": "file_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "contentType",
+ "columnName": "content_type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "contentLength",
+ "columnName": "content_length",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "destinationDirectory",
+ "columnName": "destination_directory",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrivate",
+ "columnName": "is_private",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1c1abe6e744766f8b4f8e4b402b2f099')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/3.json b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/3.json
new file mode 100644
index 0000000000..3cc682cc1b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/3.json
@@ -0,0 +1,76 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "342d0e5d0a0fcde72b88ac4585caf842",
+ "entities": [
+ {
+ "tableName": "downloads",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fileName",
+ "columnName": "file_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "contentType",
+ "columnName": "content_type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "contentLength",
+ "columnName": "content_length",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "destinationDirectory",
+ "columnName": "destination_directory",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '342d0e5d0a0fcde72b88ac4585caf842')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/4.json b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/4.json
new file mode 100644
index 0000000000..8a66ffc3a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/4.json
@@ -0,0 +1,76 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "342d0e5d0a0fcde72b88ac4585caf842",
+ "entities": [
+ {
+ "tableName": "downloads",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fileName",
+ "columnName": "file_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "contentType",
+ "columnName": "content_type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "contentLength",
+ "columnName": "content_length",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "destinationDirectory",
+ "columnName": "destination_directory",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '342d0e5d0a0fcde72b88ac4585caf842')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt b/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt
new file mode 100644
index 0000000000..e7c689b4f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt
@@ -0,0 +1,281 @@
+/* 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.downloads
+
+import android.content.Context
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.feature.downloads.db.DownloadsDatabase
+import mozilla.components.feature.downloads.db.Migrations
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+private const val MIGRATION_TEST_DB = "migration-test"
+
+@ExperimentalCoroutinesApi
+class OnDeviceDownloadStorageTest {
+ private lateinit var context: Context
+ private lateinit var storage: DownloadStorage
+ private lateinit var database: DownloadsDatabase
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ DownloadsDatabase::class.java,
+ )
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ database = Room.inMemoryDatabaseBuilder(context, DownloadsDatabase::class.java).build()
+
+ storage = DownloadStorage(context)
+ storage.database = lazy { database }
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ }
+
+ @Test
+ fun migrate1to2() {
+ helper.createDatabase(MIGRATION_TEST_DB, 1).apply {
+ query("SELECT * FROM downloads").use { cursor ->
+ assertEquals(-1, cursor.columnNames.indexOf("is_private"))
+ }
+ execSQL(
+ "INSERT INTO " +
+ "downloads " +
+ "(id, url, file_name, content_type,content_length,status,destination_directory,created_at) " +
+ "VALUES " +
+ "(1,'url','file_name','content_type',1,1,'destination_directory',1)",
+ )
+ }
+
+ val dbVersion2 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 2, true, Migrations.migration_1_2)
+
+ dbVersion2.query("SELECT * FROM downloads").use { cursor ->
+ assertTrue(cursor.columnNames.contains("is_private"))
+
+ cursor.moveToFirst()
+ assertEquals(0, cursor.getInt(cursor.getColumnIndexOrThrow("is_private")))
+ }
+ }
+
+ @Test
+ fun migrate2to3() {
+ helper.createDatabase(MIGRATION_TEST_DB, 2).apply {
+ query("SELECT * FROM downloads").use { cursor ->
+ assertTrue(cursor.columnNames.contains("is_private"))
+ }
+ // A private download
+ execSQL(
+ "INSERT INTO " +
+ "downloads " +
+ "(id, url, file_name, content_type,content_length,status,destination_directory,created_at,is_private) " +
+ "VALUES " +
+ "(1,'url','file_name','content_type',1,1,'destination_directory',1,1)",
+ )
+
+ // A normal download
+ execSQL(
+ "INSERT INTO " +
+ "downloads " +
+ "(id, url, file_name, content_type,content_length,status,destination_directory,created_at,is_private) " +
+ "VALUES " +
+ "(2,'url','file_name','content_type',1,1,'destination_directory',1,0)",
+ )
+ }
+
+ val dbVersion2 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 3, true, Migrations.migration_2_3)
+
+ dbVersion2.query("SELECT * FROM downloads").use { cursor ->
+ assertFalse(cursor.columnNames.contains("is_private"))
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ // Only non private downloads should be in the db.
+ assertEquals(2, cursor.getInt(cursor.getColumnIndexOrThrow("id")))
+ }
+ }
+
+ @Test
+ fun migrate3to4() {
+ helper.createDatabase(MIGRATION_TEST_DB, 3).apply {
+ // A data url download
+ execSQL(
+ "INSERT INTO " +
+ "downloads " +
+ "(id, url, file_name, content_type,content_length,status,destination_directory,created_at) " +
+ "VALUES " +
+ "(1,'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==','file_name','content_type',1,1,'destination_directory',1)",
+ )
+ // A normal url download
+ execSQL(
+ "INSERT INTO " +
+ "downloads " +
+ "(id, url, file_name, content_type,content_length,status,destination_directory,created_at) " +
+ "VALUES " +
+ "(2,'url','file_name','content_type',1,1,'destination_directory',1)",
+ )
+ }
+
+ val dbVersion4 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 4, true, Migrations.migration_3_4)
+
+ dbVersion4.query("SELECT * FROM downloads").use { cursor ->
+ assertEquals(2, cursor.count)
+
+ cursor.moveToFirst()
+ // Data url must be removed from download 1.
+ assertEquals("", cursor.getString(cursor.getColumnIndexOrThrow("url")))
+
+ cursor.moveToNext()
+
+ // The download 1 must keep its url.
+ assertEquals("url", cursor.getString(cursor.getColumnIndexOrThrow("url")))
+ }
+ }
+
+ @Test
+ fun testAddingDownload() = runTest {
+ val download1 = createMockDownload("1", "url1")
+ val download2 = createMockDownload("2", "url2")
+ val download3 = createMockDownload("3", "url3")
+
+ storage.add(download1)
+ storage.add(download2)
+ storage.add(download3)
+
+ val downloads = getDownloadsPagedList()
+
+ assertEquals(3, downloads.size)
+
+ assertTrue(DownloadStorage.isSameDownload(download1, downloads.first()))
+ assertTrue(DownloadStorage.isSameDownload(download2, downloads[1]))
+ assertTrue(DownloadStorage.isSameDownload(download3, downloads[2]))
+ }
+
+ @Test
+ fun testAddingDataURLDownload() = runTest {
+ val download1 = createMockDownload("1", "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")
+ val download2 = createMockDownload("2", "url2")
+
+ storage.add(download1)
+ storage.add(download2)
+
+ val downloads = getDownloadsPagedList()
+
+ assertEquals(2, downloads.size)
+
+ assertTrue(DownloadStorage.isSameDownload(download1.copy(url = ""), downloads.first()))
+ assertTrue(DownloadStorage.isSameDownload(download2, downloads[1]))
+ }
+
+ @Test
+ fun testUpdatingDataURLDownload() = runTest {
+ val download1 = createMockDownload("1", "url1")
+ val download2 = createMockDownload("2", "url2")
+
+ storage.add(download1)
+ storage.add(download2)
+
+ var downloads = getDownloadsPagedList()
+
+ assertEquals(2, downloads.size)
+
+ assertTrue(DownloadStorage.isSameDownload(download1, downloads.first()))
+ assertTrue(DownloadStorage.isSameDownload(download2, downloads[1]))
+
+ val updatedDownload1 = createMockDownload("1", "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")
+ val updatedDownload2 = createMockDownload("2", "updated_url2")
+
+ storage.update(updatedDownload1)
+ storage.update(updatedDownload2)
+
+ downloads = getDownloadsPagedList()
+
+ assertTrue(DownloadStorage.isSameDownload(updatedDownload1.copy(url = ""), downloads.first()))
+ assertTrue(DownloadStorage.isSameDownload(updatedDownload2, downloads[1]))
+ }
+
+ @Test
+ fun testRemovingDownload() = runTest {
+ val download1 = createMockDownload("1", "url1")
+ val download2 = createMockDownload("2", "url2")
+
+ storage.add(download1)
+ storage.add(download2)
+
+ assertEquals(2, getDownloadsPagedList().size)
+
+ storage.remove(download1)
+
+ val downloads = getDownloadsPagedList()
+ val downloadFromDB = downloads.first()
+
+ assertEquals(1, downloads.size)
+ assertTrue(DownloadStorage.isSameDownload(download2, downloadFromDB))
+ }
+
+ @Test
+ fun testGettingDownloads() = runTest {
+ val download1 = createMockDownload("1", "url1")
+ val download2 = createMockDownload("2", "url2")
+
+ storage.add(download1)
+ storage.add(download2)
+
+ val downloads = getDownloadsPagedList()
+
+ assertEquals(2, downloads.size)
+
+ assertTrue(DownloadStorage.isSameDownload(download1, downloads.first()))
+ assertTrue(DownloadStorage.isSameDownload(download2, downloads[1]))
+ }
+
+ @Test
+ fun testRemovingDownloads() = runTest {
+ for (index in 1..2) {
+ storage.add(createMockDownload(index.toString(), "url1"))
+ }
+
+ var pagedList = getDownloadsPagedList()
+
+ assertEquals(2, pagedList.size)
+
+ pagedList.forEach { download ->
+ storage.remove(download)
+ }
+
+ pagedList = getDownloadsPagedList()
+
+ assertTrue(pagedList.isEmpty())
+ }
+
+ private fun createMockDownload(id: String, url: String): DownloadState {
+ return DownloadState(
+ id = id,
+ url = url,
+ contentType = "application/zip",
+ contentLength = 5242880,
+ userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36",
+ )
+ }
+
+ private suspend fun getDownloadsPagedList(): List<DownloadState> {
+ return storage.getDownloadsList()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt b/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt
new file mode 100644
index 0000000000..ac7e4da2fb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt
@@ -0,0 +1,114 @@
+/* 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.downloads.db
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.feature.downloads.DownloadStorage
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+class DownloadDaoTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: DownloadsDatabase
+ private lateinit var dao: DownloadDao
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, DownloadsDatabase::class.java).build()
+ dao = database.downloadDao()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ }
+
+ @Test
+ fun testInsertingAndReadingDownloads() = runTest {
+ val download = insertMockDownload("1", "https://www.mozilla.org/file1.txt")
+ val pagedList = getDownloadsPagedList()
+
+ assertEquals(1, pagedList.size)
+ assertTrue(DownloadStorage.isSameDownload(download, pagedList[0].toDownloadState()))
+ }
+
+ @Test
+ fun testRemoveAllDownloads() = runTest {
+ for (index in 1..4) {
+ insertMockDownload(index.toString(), "https://www.mozilla.org/file1.txt")
+ }
+
+ var pagedList = getDownloadsPagedList()
+
+ assertEquals(4, pagedList.size)
+ dao.deleteAllDownloads()
+
+ pagedList = getDownloadsPagedList()
+
+ assertTrue(pagedList.isEmpty())
+ }
+
+ @Test
+ fun testRemovingDownloads() = runTest {
+ for (index in 1..2) {
+ insertMockDownload(index.toString(), "https://www.mozilla.org/file1.txt")
+ }
+
+ var pagedList = getDownloadsPagedList()
+
+ assertEquals(2, pagedList.size)
+
+ pagedList.forEach {
+ dao.delete(it)
+ }
+
+ pagedList = getDownloadsPagedList()
+
+ assertTrue(pagedList.isEmpty())
+ }
+
+ @Test
+ fun testUpdateDownload() = runTest {
+ insertMockDownload("1", "https://www.mozilla.org/file1.txt")
+
+ var pagedList = getDownloadsPagedList()
+
+ assertEquals(1, pagedList.size)
+
+ val download = pagedList.first()
+
+ val updatedDownload = download.toDownloadState().copy("new_url")
+
+ dao.update(updatedDownload.toDownloadEntity())
+ pagedList = getDownloadsPagedList()
+
+ assertEquals("new_url", pagedList.first().url)
+ }
+
+ private suspend fun getDownloadsPagedList(): List<DownloadEntity> {
+ return dao.getDownloadsList()
+ }
+
+ private suspend fun insertMockDownload(id: String, url: String): DownloadState {
+ val download = DownloadState(
+ id = id,
+ url = url,
+ contentType = "application/zip",
+ contentLength = 5242880,
+ userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36",
+ )
+ dao.insert(download.toDownloadEntity())
+ return download
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/downloads/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..0b82a27a0a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?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/.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
+
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage"
+ android:maxSdkVersion="28"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
+
+ <!-- Needed to prompt the user to give permission to install a downloaded apk -->
+ <uses-permission-sdk-23 android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+
+ <!-- Needed to receive broadcasts from AC code-->
+ <uses-permission android:name="${applicationId}.permission.RECEIVE_DOWNLOAD_BROADCAST" />
+
+ <permission
+ android:name="${applicationId}.permission.RECEIVE_DOWNLOAD_BROADCAST"
+ android:protectionLevel="signature" />
+
+ <application android:supportsRtl="true">
+ <provider
+ android:name="mozilla.components.feature.downloads.provider.FileProvider"
+ android:authorities="${applicationId}.feature.downloads.fileprovider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/feature_downloads_file_paths" />
+ </provider>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt
new file mode 100644
index 0000000000..6b03d9a059
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt
@@ -0,0 +1,1120 @@
+/* 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.downloads
+
+import android.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
+import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
+import android.app.Notification
+import android.app.Service
+import android.content.ActivityNotFoundException
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.content.IntentFilter
+import android.net.Uri
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Bundle
+import android.os.Environment
+import android.os.IBinder
+import android.os.ParcelFileDescriptor
+import android.provider.MediaStore
+import android.provider.MediaStore.setIncludePending
+import android.webkit.MimeTypeMap
+import android.widget.Toast
+import androidx.annotation.ColorRes
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import androidx.core.net.toUri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status
+import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED
+import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED
+import mozilla.components.browser.state.state.content.DownloadState.Status.DOWNLOADING
+import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED
+import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED
+import mozilla.components.browser.state.state.content.DownloadState.Status.PAUSED
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_RANGE
+import mozilla.components.concept.fetch.Headers.Names.RANGE
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.feature.downloads.DownloadNotification.NOTIFICATION_DOWNLOAD_GROUP_ID
+import mozilla.components.feature.downloads.ext.addCompletedDownload
+import mozilla.components.feature.downloads.ext.isScheme
+import mozilla.components.feature.downloads.ext.withResponse
+import mozilla.components.feature.downloads.facts.emitNotificationCancelFact
+import mozilla.components.feature.downloads.facts.emitNotificationOpenFact
+import mozilla.components.feature.downloads.facts.emitNotificationPauseFact
+import mozilla.components.feature.downloads.facts.emitNotificationResumeFact
+import mozilla.components.feature.downloads.facts.emitNotificationTryAgainFact
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.ifNullOrEmpty
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import mozilla.components.support.ktx.kotlinx.coroutines.throttleLatest
+import mozilla.components.support.utils.DownloadUtils
+import mozilla.components.support.utils.ext.registerReceiverCompat
+import mozilla.components.support.utils.ext.stopForegroundCompat
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import kotlin.random.Random
+
+/**
+ * Service that performs downloads through a fetch [Client] rather than through the native
+ * Android download manager.
+ *
+ * To use this service, you must create a subclass in your application and add it to the manifest.
+ */
+@Suppress("TooManyFunctions", "LargeClass", "ComplexMethod")
+abstract class AbstractFetchDownloadService : Service() {
+ protected abstract val store: BrowserStore
+ protected abstract val notificationsDelegate: NotificationsDelegate
+
+ private val notificationUpdateScope = MainScope()
+
+ protected abstract val httpClient: Client
+
+ protected open val style: Style = Style()
+
+ @VisibleForTesting
+ internal val context: Context get() = this
+
+ @VisibleForTesting
+ internal var compatForegroundNotificationId: Int = COMPAT_DEFAULT_FOREGROUND_ID
+ private val logger = Logger("AbstractFetchDownloadService")
+
+ internal var downloadJobs = mutableMapOf<String, DownloadJobState>()
+
+ // TODO Move this to browser store and make immutable:
+ // https://github.com/mozilla-mobile/android-components/issues/7050
+ internal data class DownloadJobState(
+ var job: Job? = null,
+ @Volatile var state: DownloadState,
+ var currentBytesCopied: Long = 0,
+ @GuardedBy("context") var status: Status,
+ var foregroundServiceId: Int = 0,
+ var downloadDeleted: Boolean = false,
+ var notifiedStopped: Boolean = false,
+ var lastNotificationUpdate: Long = 0L,
+ var createdTime: Long = System.currentTimeMillis(),
+ ) {
+ internal fun canUpdateNotification(): Boolean {
+ return isUnderNotificationUpdateLimit() && !notifiedStopped
+ }
+
+ /**
+ * Android imposes a limit on of how often we can send updates for a notification.
+ * The limit is one second per update.
+ * See https://developer.android.com/training/notify-user/build-notification.html#Updating
+ * This function indicates if we are under that limit.
+ */
+ internal fun isUnderNotificationUpdateLimit(): Boolean {
+ return getSecondsSinceTheLastNotificationUpdate() >= 1
+ }
+
+ @Suppress("MagicNumber")
+ internal fun getSecondsSinceTheLastNotificationUpdate(): Long {
+ return (System.currentTimeMillis() - lastNotificationUpdate) / 1000
+ }
+ }
+
+ internal fun setDownloadJobStatus(downloadJobState: DownloadJobState, status: Status) {
+ synchronized(context) {
+ if (status == DOWNLOADING) {
+ downloadJobState.notifiedStopped = false
+ }
+ downloadJobState.status = status
+ updateDownloadState(downloadJobState.state.copy(status = status))
+ }
+ }
+
+ internal fun getDownloadJobStatus(downloadJobState: DownloadJobState): Status {
+ synchronized(context) {
+ return downloadJobState.status
+ }
+ }
+
+ internal val broadcastReceiver by lazy {
+ object : BroadcastReceiver() {
+ @Suppress("LongMethod")
+ override fun onReceive(context: Context, intent: Intent?) {
+ val downloadId =
+ intent?.extras?.getString(DownloadNotification.EXTRA_DOWNLOAD_ID) ?: return
+ val currentDownloadJobState = downloadJobs[downloadId] ?: return
+
+ when (intent.action) {
+ ACTION_PAUSE -> {
+ setDownloadJobStatus(currentDownloadJobState, PAUSED)
+ currentDownloadJobState.job?.cancel()
+ emitNotificationPauseFact()
+ logger.debug("ACTION_PAUSE for ${currentDownloadJobState.state.id}")
+ }
+
+ ACTION_RESUME -> {
+ setDownloadJobStatus(currentDownloadJobState, DOWNLOADING)
+
+ currentDownloadJobState.job = CoroutineScope(IO).launch {
+ startDownloadJob(currentDownloadJobState)
+ }
+
+ emitNotificationResumeFact()
+ logger.debug("ACTION_RESUME for ${currentDownloadJobState.state.id}")
+ }
+
+ ACTION_CANCEL -> {
+ cancelDownloadJob(currentDownloadJobState)
+ removeDownloadJob(currentDownloadJobState)
+ emitNotificationCancelFact()
+ logger.debug("ACTION_CANCEL for ${currentDownloadJobState.state.id}")
+ }
+
+ ACTION_TRY_AGAIN -> {
+ removeNotification(context, currentDownloadJobState)
+ currentDownloadJobState.lastNotificationUpdate = System.currentTimeMillis()
+ setDownloadJobStatus(currentDownloadJobState, DOWNLOADING)
+
+ currentDownloadJobState.job = CoroutineScope(IO).launch {
+ startDownloadJob(currentDownloadJobState)
+ }
+
+ emitNotificationTryAgainFact()
+ logger.debug("ACTION_TRY_AGAIN for ${currentDownloadJobState.state.id}")
+ }
+
+ ACTION_DISMISS -> {
+ removeDownloadJob(currentDownloadJobState)
+ logger.debug("ACTION_DISMISS for ${currentDownloadJobState.state.id}")
+ }
+
+ ACTION_OPEN -> {
+ if (!openFile(context, currentDownloadJobState.state)) {
+ val fileExt = MimeTypeMap.getFileExtensionFromUrl(
+ currentDownloadJobState.state.filePath.toString(),
+ )
+ val errorMessage = applicationContext.getString(
+ R.string.mozac_feature_downloads_open_not_supported1,
+ fileExt,
+ )
+
+ Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
+ logger.debug("ACTION_OPEN errorMessage for ${currentDownloadJobState.state.id} ")
+ }
+
+ emitNotificationOpenFact()
+ logger.debug("ACTION_OPEN for ${currentDownloadJobState.state.id}")
+ }
+ }
+ }
+ }
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ registerNotificationActionsReceiver()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val download = intent?.getStringExtra(EXTRA_DOWNLOAD_ID)?.let {
+ store.state.downloads[it]
+ } ?: return START_REDELIVER_INTENT
+
+ when (intent.action) {
+ ACTION_REMOVE_PRIVATE_DOWNLOAD -> {
+ handleRemovePrivateDownloadIntent(download)
+ }
+ ACTION_TRY_AGAIN -> {
+ val newDownloadState = download.copy(status = DOWNLOADING)
+ store.dispatch(
+ DownloadAction.UpdateDownloadAction(
+ newDownloadState,
+ ),
+ )
+ handleDownloadIntent(newDownloadState)
+ }
+
+ else -> {
+ handleDownloadIntent(download)
+ }
+ }
+
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ @VisibleForTesting
+ internal fun handleRemovePrivateDownloadIntent(download: DownloadState) {
+ if (download.private) {
+ downloadJobs[download.id]?.let {
+ // Do not cancel already completed downloads.
+ if (it.status != COMPLETED) {
+ cancelDownloadJob(it)
+ }
+ removeDownloadJob(it)
+ }
+ store.dispatch(DownloadAction.RemoveDownloadAction(download.id))
+ }
+ }
+
+ @VisibleForTesting
+ internal fun handleDownloadIntent(download: DownloadState) {
+ // If the job already exists, then don't create a new ID. This can happen when calling tryAgain
+ val foregroundServiceId = downloadJobs[download.id]?.foregroundServiceId ?: Random.nextInt()
+
+ val actualStatus = if (download.status == INITIATED) DOWNLOADING else download.status
+
+ // Create a new job and add it, with its downloadState to the map
+ val downloadJobState = DownloadJobState(
+ state = download.copy(status = actualStatus, notificationId = foregroundServiceId),
+ foregroundServiceId = foregroundServiceId,
+ status = actualStatus,
+ )
+
+ store.dispatch(DownloadAction.UpdateDownloadAction(downloadJobState.state))
+
+ if (actualStatus == DOWNLOADING) {
+ downloadJobState.job = CoroutineScope(IO).launch {
+ startDownloadJob(downloadJobState)
+ }
+ }
+
+ downloadJobs[download.id] = downloadJobState
+
+ setForegroundNotification(downloadJobState)
+
+ notificationUpdateScope.launch {
+ while (isActive) {
+ delay(PROGRESS_UPDATE_INTERVAL)
+ updateDownloadNotification()
+ if (downloadJobs.isEmpty()) cancel()
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun cancelDownloadJob(
+ currentDownloadJobState: DownloadJobState,
+ ) {
+ currentDownloadJobState.lastNotificationUpdate = System.currentTimeMillis()
+ setDownloadJobStatus(
+ currentDownloadJobState,
+ CANCELLED,
+ )
+ currentDownloadJobState.job?.cancel()
+ currentDownloadJobState.job = CoroutineScope(IO).launch {
+ deleteDownloadingFile(currentDownloadJobState.state)
+ currentDownloadJobState.downloadDeleted =
+ true
+ }
+ }
+
+ /**
+ * Android rate limits notifications being sent, so we must send them on a delay so that
+ * notifications are not dropped
+ */
+ @Suppress("ComplexMethod")
+ private fun updateDownloadNotification() {
+ for (download in downloadJobs.values) {
+ if (!download.canUpdateNotification()) { continue }
+ /*
+ * We want to keep a consistent state in the UI, download.status can be changed from
+ * another thread while we are posting updates to the UI, causing inconsistent UIs.
+ * For this reason, we ONLY use the latest status during an UI update, new changes
+ * will be posted in subsequent updates.
+ */
+ val uiStatus = getDownloadJobStatus(download)
+
+ updateForegroundNotificationIfNeeded(download)
+
+ // Dispatch the corresponding notification based on the current status
+ updateDownloadNotification(uiStatus, download)
+
+ if (uiStatus != DOWNLOADING) {
+ sendDownloadStopped(download)
+ }
+ }
+ }
+
+ /**
+ * Data class for styling download notifications.
+ * @param notificationAccentColor accent color for all download notifications.
+ */
+ data class Style(
+ @ColorRes
+ val notificationAccentColor: Int = R.color.mozac_feature_downloads_notification,
+ )
+
+ /**
+ * Updates the notification state with the passed [download] data.
+ * Be aware that you need to pass [latestUIStatus] as [DownloadJobState.status] can be modified
+ * from another thread, causing inconsistencies in the ui.
+ */
+ @VisibleForTesting
+ internal fun updateDownloadNotification(
+ latestUIStatus: Status,
+ download: DownloadJobState,
+ scope: CoroutineScope = CoroutineScope(IO),
+ ) {
+ val notification = when (latestUIStatus) {
+ DOWNLOADING -> DownloadNotification.createOngoingDownloadNotification(
+ context,
+ download,
+ style.notificationAccentColor,
+ )
+ PAUSED -> DownloadNotification.createPausedDownloadNotification(
+ context,
+ download,
+ style.notificationAccentColor,
+ )
+ FAILED -> DownloadNotification.createDownloadFailedNotification(
+ context,
+ download,
+ style.notificationAccentColor,
+ )
+ COMPLETED -> {
+ addToDownloadSystemDatabaseCompat(download.state, scope)
+ DownloadNotification.createDownloadCompletedNotification(
+ context,
+ download,
+ style.notificationAccentColor,
+ )
+ }
+ CANCELLED -> {
+ removeNotification(context, download)
+ download.lastNotificationUpdate = System.currentTimeMillis()
+ null
+ }
+ INITIATED -> null
+ }
+
+ notification?.let {
+ notificationsDelegate.notify(
+ notificationId = download.foregroundServiceId,
+ notification = it,
+ )
+ download.lastNotificationUpdate = System.currentTimeMillis()
+ }
+ }
+
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ stopSelf()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ clearAllDownloadsNotificationsAndJobs()
+ unregisterNotificationActionsReceiver()
+ }
+
+ // Cancels all running jobs and remove all notifications.
+ // Also cleans any resources that we were holding like broadcastReceivers
+ internal fun clearAllDownloadsNotificationsAndJobs() {
+ val notificationManager = NotificationManagerCompat.from(context)
+
+ stopForegroundCompat(true)
+ compatForegroundNotificationId = COMPAT_DEFAULT_FOREGROUND_ID
+
+ // Before doing any cleaning, we have to stop the notification updater scope.
+ // To ensure we are not recreating the notifications.
+ notificationUpdateScope.cancel()
+
+ downloadJobs.values.forEach { state ->
+ notificationManager.cancel(state.foregroundServiceId)
+ state.job?.cancel()
+ }
+ if (SDK_INT >= Build.VERSION_CODES.N) {
+ notificationManager.cancel(NOTIFICATION_DOWNLOAD_GROUP_ID)
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ internal fun startDownloadJob(currentDownloadJobState: DownloadJobState) {
+ logger.debug("Starting download for ${currentDownloadJobState.state.id} ")
+ try {
+ performDownload(currentDownloadJobState)
+ } catch (e: Exception) {
+ logger.error("Unable to complete download for ${currentDownloadJobState.state.id} marked as FAILED", e)
+ setDownloadJobStatus(currentDownloadJobState, FAILED)
+ }
+ }
+
+ internal fun deleteDownloadingFile(downloadState: DownloadState) {
+ val downloadedFile = File(downloadState.filePath)
+ downloadedFile.delete()
+ }
+
+ /**
+ * Adds a file to the downloads database system, so it could appear in Downloads App
+ * (and thus become eligible for management by the Downloads App) only for compatible devices
+ * otherwise nothing will happen.
+ */
+ @VisibleForTesting
+ internal fun addToDownloadSystemDatabaseCompat(
+ download: DownloadState,
+ scope: CoroutineScope = CoroutineScope(IO),
+ ) {
+ if (!shouldUseScopedStorage()) {
+ val fileName = download.fileName
+ ?: throw IllegalStateException("A fileName for a download is required")
+ val file = File(download.filePath)
+ // addCompletedDownload can't handle any non http(s) urls
+ scope.launch {
+ addCompletedDownload(
+ title = fileName,
+ description = fileName,
+ isMediaScannerScannable = true,
+ mimeType = getSafeContentType(context, download.filePath, download.contentType),
+ path = file.absolutePath,
+ length = download.contentLength ?: file.length(),
+ // Only show notifications if our channel is blocked
+ showNotification = !DownloadNotification.isChannelEnabled(context),
+ download,
+ )
+ }
+ }
+ }
+
+ @VisibleForTesting
+ @Suppress("LongParameterList")
+ internal fun addCompletedDownload(
+ title: String,
+ description: String,
+ isMediaScannerScannable: Boolean,
+ mimeType: String,
+ path: String,
+ length: Long,
+ showNotification: Boolean,
+ download: DownloadState,
+ ) {
+ try {
+ val url = if (!download.isScheme(listOf("http", "https"))) null else download.url.toUri()
+ context.addCompletedDownload(
+ title = title,
+ description = description,
+ isMediaScannerScannable = isMediaScannerScannable,
+ mimeType = mimeType,
+ path = path,
+ length = length,
+ // Only show notifications if our channel is blocked
+ showNotification = showNotification,
+ uri = url,
+ referer = download.referrerUrl?.toUri(),
+ )
+ } catch (e: IllegalArgumentException) {
+ logger.error("Unable add the download to the system database", e)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun registerNotificationActionsReceiver() {
+ val filter = IntentFilter().apply {
+ addAction(ACTION_PAUSE)
+ addAction(ACTION_RESUME)
+ addAction(ACTION_CANCEL)
+ addAction(ACTION_DISMISS)
+ addAction(ACTION_TRY_AGAIN)
+ addAction(ACTION_OPEN)
+ }
+
+ context.registerReceiverCompat(
+ broadcastReceiver,
+ filter,
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun unregisterNotificationActionsReceiver() {
+ context.unregisterReceiver(broadcastReceiver)
+ }
+
+ @VisibleForTesting
+ internal fun removeDownloadJob(downloadJobState: DownloadJobState) {
+ downloadJobs.remove(downloadJobState.state.id)
+ if (downloadJobs.isEmpty()) {
+ stopSelf()
+ } else {
+ updateForegroundNotificationIfNeeded(downloadJobState)
+ removeNotification(context, downloadJobState)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun removeNotification(context: Context, currentDownloadJobState: DownloadJobState) {
+ NotificationManagerCompat.from(context).cancel(currentDownloadJobState.foregroundServiceId)
+ }
+
+ /**
+ * Refresh the notification group content only for devices that support it,
+ * otherwise nothing will happen.
+ */
+ @VisibleForTesting
+ internal fun updateNotificationGroup(): Notification? {
+ return if (SDK_INT >= Build.VERSION_CODES.N) {
+ val downloadList = downloadJobs.values.toList()
+ val notificationGroup =
+ DownloadNotification.createDownloadGroupNotification(
+ context,
+ downloadList,
+ style.notificationAccentColor,
+ )
+
+ notificationsDelegate.notify(
+ notificationId = NOTIFICATION_DOWNLOAD_GROUP_ID,
+ notification = notificationGroup,
+ )
+ notificationGroup
+ } else {
+ null
+ }
+ }
+
+ internal fun createCompactForegroundNotification(downloadJobState: DownloadJobState): Notification {
+ val notification =
+ DownloadNotification.createOngoingDownloadNotification(
+ context,
+ downloadJobState,
+ style.notificationAccentColor,
+ )
+ compatForegroundNotificationId = downloadJobState.foregroundServiceId
+
+ notificationsDelegate.notify(
+ notificationId = compatForegroundNotificationId,
+ notification = notification,
+ )
+
+ downloadJobState.lastNotificationUpdate = System.currentTimeMillis()
+
+ return notification
+ }
+
+ @VisibleForTesting
+ internal fun getForegroundId(): Int {
+ return if (SDK_INT >= Build.VERSION_CODES.N) {
+ NOTIFICATION_DOWNLOAD_GROUP_ID
+ } else {
+ compatForegroundNotificationId
+ }
+ }
+
+ /**
+ * We have two different behaviours as notification groups are not supported in all devices.
+ * For devices that support it, we create a separate notification which will be the foreground
+ * notification, it will be always present until we don't have more active downloads.
+ * For devices that doesn't support notification groups, we set the latest active notification as
+ * the foreground notification and we keep changing it to the latest active download.
+ */
+ @VisibleForTesting
+ internal fun setForegroundNotification(downloadJobState: DownloadJobState) {
+ var previousDownload: DownloadJobState? = null
+
+ val (notificationId, notification) = if (SDK_INT >= Build.VERSION_CODES.N) {
+ NOTIFICATION_DOWNLOAD_GROUP_ID to updateNotificationGroup()
+ } else {
+ previousDownload = downloadJobs.values.firstOrNull {
+ it.foregroundServiceId == compatForegroundNotificationId
+ }
+ downloadJobState.foregroundServiceId to createCompactForegroundNotification(
+ downloadJobState,
+ )
+ }
+
+ startForeground(notificationId, notification)
+ /**
+ * In devices that doesn't use notification groups, every new download becomes the new foreground one,
+ * unfortunately, when we call startForeground it removes the previous foreground notification
+ * when it's not an ongoing one, for this reason, we have to recreate the deleted notification.
+ * By the way ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
+ * doesn't work neither calling stopForeground(false) and then calling startForeground,
+ * it always deletes the previous notification :(
+ */
+ previousDownload?.let {
+ updateDownloadNotification(previousDownload.status, it)
+ }
+ }
+
+ /**
+ * Indicates the status of a download has changed and maybe the foreground notification needs,
+ * to be updated. For devices that support group notifications, we update the overview
+ * notification. For devices that don't support group notifications, we try to find a new
+ * active download and selected it as the new foreground notification.
+ */
+ internal fun updateForegroundNotificationIfNeeded(download: DownloadJobState) {
+ if (SDK_INT < Build.VERSION_CODES.N) {
+ /**
+ * For devices that don't support notification groups, we have to keep updating
+ * the foreground notification id, when the previous one gets a state that
+ * is likely to be dismissed.
+ */
+ val status = download.status
+ val foregroundId = download.foregroundServiceId
+ val isSelectedForegroundId = compatForegroundNotificationId == foregroundId
+ val needNewForegroundNotification = when (status) {
+ COMPLETED, FAILED, CANCELLED -> true
+ else -> false
+ }
+
+ if (isSelectedForegroundId && needNewForegroundNotification) {
+ // We need to deselect the actual foreground notification, because while it is
+ // selected the user will not be able to dismiss it.
+ stopForegroundCompat(false)
+
+ // Now we need to find a new foreground notification, if needed.
+ val newSelectedForegroundDownload = downloadJobs.values.firstOrNull { it.status == DOWNLOADING }
+ newSelectedForegroundDownload?.let {
+ setForegroundNotification(it)
+ }
+ }
+ } else {
+ // This device supports notification groups, we just need to update the summary notification
+ updateNotificationGroup()
+ }
+ // If all downloads have been completed we don't need the status of
+ // foreground service anymore, we can call stopForeground and let the user
+ // swipe the foreground notification.
+ val finishedDownloading = downloadJobs.values.toList().all { it.status == COMPLETED }
+ if (finishedDownloading) {
+ stopForegroundCompat(false)
+ }
+ }
+
+ @Suppress("ComplexCondition", "ComplexMethod")
+ internal fun performDownload(currentDownloadJobState: DownloadJobState, useHttpClient: Boolean = false) {
+ val download = currentDownloadJobState.state
+ val isResumingDownload = currentDownloadJobState.currentBytesCopied > 0L
+ val headers = MutableHeaders()
+
+ if (isResumingDownload) {
+ headers.append(RANGE, "bytes=${currentDownloadJobState.currentBytesCopied}-")
+ }
+
+ var isUsingHttpClient = false
+ val request = Request(
+ download.url.sanitizeURL(),
+ headers = headers,
+ private = download.private,
+ referrerUrl = download.referrerUrl,
+ )
+ // When resuming a download we need to use the httpClient as
+ // download.response doesn't support adding headers.
+ val response = if (isResumingDownload || useHttpClient || download.response == null) {
+ isUsingHttpClient = true
+ httpClient.fetch(request)
+ } else {
+ requireNotNull(download.response)
+ }
+ logger.debug("Fetching download for ${currentDownloadJobState.state.id} ")
+
+ // If we are resuming a download and the response does not contain a CONTENT_RANGE
+ // we cannot be sure that the request will properly be handled
+ if (response.status != PARTIAL_CONTENT_STATUS && response.status != OK_STATUS ||
+ (isResumingDownload && !response.headers.contains(CONTENT_RANGE))
+ ) {
+ response.close()
+ // We experienced a problem trying to fetch the file, send a failure notification
+ currentDownloadJobState.currentBytesCopied = 0
+ currentDownloadJobState.state = currentDownloadJobState.state.copy(currentBytesCopied = 0)
+ setDownloadJobStatus(currentDownloadJobState, FAILED)
+ logger.debug("Unable to fetching Download for ${currentDownloadJobState.state.id} status FAILED")
+ return
+ }
+
+ response.body.useStream { inStream ->
+ var copyInChuckStatus: CopyInChuckStatus? = null
+ val newDownloadState = download.withResponse(response.headers, inStream)
+ currentDownloadJobState.state = newDownloadState
+
+ useFileStream(newDownloadState, isResumingDownload) { outStream ->
+ copyInChuckStatus = copyInChunks(currentDownloadJobState, inStream, outStream, isUsingHttpClient)
+ }
+
+ if (copyInChuckStatus != CopyInChuckStatus.ERROR_IN_STREAM_CLOSED) {
+ verifyDownload(currentDownloadJobState)
+ }
+ }
+ }
+
+ /**
+ * Updates the status of an ACTIVE download to completed or failed based on bytes copied
+ */
+ internal fun verifyDownload(download: DownloadJobState) {
+ if (getDownloadJobStatus(download) == DOWNLOADING &&
+ download.currentBytesCopied < download.state.contentLength ?: 0
+ ) {
+ setDownloadJobStatus(download, FAILED)
+ logger.error("verifyDownload for ${download.state.id} FAILED")
+ } else if (getDownloadJobStatus(download) == DOWNLOADING) {
+ setDownloadJobStatus(download, COMPLETED)
+ /**
+ * In cases when we don't get the file size provided initially, we have to
+ * use downloadState.currentBytesCopied as a fallback.
+ */
+ val fileSizeNotFound = download.state.contentLength == null || download.state.contentLength == 0L
+ if (fileSizeNotFound) {
+ val newState = download.state.copy(contentLength = download.currentBytesCopied)
+ updateDownloadState(newState)
+ }
+ logger.debug("verifyDownload for ${download.state.id} ${download.status}")
+ }
+ }
+
+ @VisibleForTesting
+ internal enum class CopyInChuckStatus {
+ COMPLETED, ERROR_IN_STREAM_CLOSED
+ }
+
+ @VisibleForTesting
+ internal fun copyInChunks(
+ downloadJobState: DownloadJobState,
+ inStream: InputStream,
+ outStream: OutputStream,
+ downloadWithHttpClient: Boolean = false,
+ ): CopyInChuckStatus {
+ val data = ByteArray(CHUNK_SIZE)
+ logger.debug(
+ "starting copyInChunks ${downloadJobState.state.id}" +
+ " currentBytesCopied ${downloadJobState.state.currentBytesCopied}",
+ )
+
+ val throttleUpdateDownload = throttleLatest<Long>(
+ PROGRESS_UPDATE_INTERVAL,
+ coroutineScope = CoroutineScope(IO),
+ ) { copiedBytes ->
+ val newState = downloadJobState.state.copy(currentBytesCopied = copiedBytes)
+ updateDownloadState(newState)
+ }
+
+ var isInStreamClosed = false
+ // To ensure that we copy all files (even ones that don't have fileSize, we must NOT check < fileSize
+ while (getDownloadJobStatus(downloadJobState) == DOWNLOADING) {
+ var bytesRead: Int
+ try {
+ bytesRead = inStream.read(data)
+ } catch (e: IOException) {
+ if (downloadWithHttpClient) {
+ throw e
+ }
+ isInStreamClosed = true
+ break
+ }
+ // If bytesRead is -1, there's no data left to read from the stream
+ if (bytesRead == -1) { break }
+ downloadJobState.currentBytesCopied += bytesRead
+
+ throttleUpdateDownload(downloadJobState.currentBytesCopied)
+
+ outStream.write(data, 0, bytesRead)
+ }
+ if (isInStreamClosed) {
+ // In cases where [download.response] is available and users with slow
+ // networks start a download but quickly press pause and then resume
+ // [isResumingDownload] will be false as there will be not enough time
+ // for bytes to be copied, but the stream in [download.response] will be closed,
+ // we have to fallback to [httpClient]
+ performDownload(downloadJobState, useHttpClient = true)
+ return CopyInChuckStatus.ERROR_IN_STREAM_CLOSED
+ }
+ logger.debug(
+ "Finishing copyInChunks ${downloadJobState.state.id} " +
+ "currentBytesCopied ${downloadJobState.currentBytesCopied}",
+ )
+ return CopyInChuckStatus.COMPLETED
+ }
+
+ /**
+ * Informs [mozilla.components.feature.downloads.manager.FetchDownloadManager] that a download
+ * is no longer in progress due to being paused, completed, or failed
+ */
+ private fun sendDownloadStopped(downloadState: DownloadJobState) {
+ downloadState.notifiedStopped = true
+
+ val intent = Intent(ACTION_DOWNLOAD_COMPLETE)
+ intent.putExtra(EXTRA_DOWNLOAD_STATUS, getDownloadJobStatus(downloadState))
+ intent.putExtra(EXTRA_DOWNLOAD_ID, downloadState.state.id)
+ intent.setPackage(context.packageName)
+
+ context.sendBroadcast(intent, "${context.packageName}.permission.RECEIVE_DOWNLOAD_BROADCAST")
+ }
+
+ /**
+ * Creates an output stream on the local filesystem, then informs the system that a download
+ * is complete after [block] is run.
+ *
+ * Encapsulates different behaviour depending on the SDK version.
+ */
+ @SuppressLint("NewApi")
+ internal fun useFileStream(
+ download: DownloadState,
+ append: Boolean,
+ block: (OutputStream) -> Unit,
+ ) {
+ val downloadWithUniqueFileName = makeUniqueFileNameIfNecessary(download, append)
+ updateDownloadState(downloadWithUniqueFileName)
+
+ if (shouldUseScopedStorage()) {
+ useFileStreamScopedStorage(downloadWithUniqueFileName, block)
+ } else {
+ useFileStreamLegacy(downloadWithUniqueFileName, append, block)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun shouldUseScopedStorage() = getSdkVersion() >= Build.VERSION_CODES.Q
+
+ /**
+ * Gets the SDK version from the system.
+ * Used for testing since current robolectric version doesn't allow mocking API 29, remove after
+ * update
+ */
+ @VisibleForTesting
+ internal fun getSdkVersion(): Int = SDK_INT
+
+ /**
+ * Updates the given [updatedDownload] in the store and in the [downloadJobs].
+ */
+ @VisibleForTesting
+ internal fun updateDownloadState(updatedDownload: DownloadState) {
+ downloadJobs[updatedDownload.id]?.state = updatedDownload
+ store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload))
+ }
+
+ /**
+ * Returns an updated [DownloadState] with a unique fileName if the file is not being appended
+ */
+ @Suppress("Deprecation")
+ internal fun makeUniqueFileNameIfNecessary(
+ download: DownloadState,
+ append: Boolean,
+ ): DownloadState {
+ if (append) {
+ return download
+ }
+
+ return download.fileName?.let {
+ download.copy(
+ fileName = DownloadUtils.uniqueFileName(
+ Environment.getExternalStoragePublicDirectory(download.destinationDirectory),
+ it,
+ ),
+ )
+ } ?: download
+ }
+
+ @TargetApi(Build.VERSION_CODES.Q)
+ @VisibleForTesting
+ internal fun useFileStreamScopedStorage(download: DownloadState, block: (OutputStream) -> Unit) {
+ val values = ContentValues().apply {
+ put(MediaStore.Downloads.DISPLAY_NAME, download.fileName)
+ put(
+ MediaStore.Downloads.MIME_TYPE,
+ getSafeContentType(context, download.filePath, download.contentType),
+ )
+ put(MediaStore.Downloads.SIZE, download.contentLength)
+ put(MediaStore.Downloads.IS_PENDING, 1)
+ }
+
+ val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+ val resolver = applicationContext.contentResolver
+ var downloadUri = queryDownloadMediaStore(applicationContext, download)
+
+ if (downloadUri == null) {
+ downloadUri = resolver.insert(collection, values)
+ }
+
+ downloadUri?.let {
+ val pfd = resolver.openFileDescriptor(it, "w")
+ ParcelFileDescriptor.AutoCloseOutputStream(pfd).use(block)
+
+ values.clear()
+ values.put(MediaStore.Downloads.IS_PENDING, 0)
+ resolver.update(it, values, null, null)
+ } ?: throw IOException("Failed to register download with content resolver")
+ }
+
+ @TargetApi(Build.VERSION_CODES.P)
+ @Suppress("Deprecation")
+ @VisibleForTesting
+ internal fun useFileStreamLegacy(download: DownloadState, append: Boolean, block: (OutputStream) -> Unit) {
+ createDirectoryIfNeeded(download)
+ FileOutputStream(File(download.filePath), append).use(block)
+ }
+
+ @VisibleForTesting
+ internal fun createDirectoryIfNeeded(download: DownloadState) {
+ val directory = File(download.directoryPath)
+ if (!directory.exists()) {
+ directory.mkdir()
+ }
+ }
+
+ companion object {
+ /**
+ * Launches an intent to open the given file, returns whether or not the file could be opened
+ */
+ fun openFile(
+ applicationContext: Context,
+ download: DownloadState,
+ ): Boolean {
+ val newIntent = createOpenFileIntent(applicationContext, download)
+
+ return try {
+ applicationContext.startActivity(newIntent)
+ true
+ } catch (error: ActivityNotFoundException) {
+ false
+ }
+ }
+
+ /**
+ * Creates an Intent which can then be used to open the file specified.
+ * @param context the current Android *Context*
+ * @param download contains the details of the downloaded file to be opened.
+ */
+ fun createOpenFileIntent(
+ context: Context,
+ download: DownloadState,
+ ): Intent {
+ val filePath = download.filePath
+ val contentType = download.contentType
+
+ // For devices that support the scoped storage we can query the directly the download
+ // media store otherwise we have to construct the uri based on the file path.
+ val fileUri: Uri =
+ if (SDK_INT >= Build.VERSION_CODES.Q) {
+ queryDownloadMediaStore(context, download)
+ ?: getFilePathUri(context, filePath)
+ } else {
+ // Create a new file with the location of the saved file to extract the correct path
+ // `file` has the wrong path, so we must construct it based on the `fileName` and `dir.path`s
+ getFilePathUri(context, filePath)
+ }
+
+ val newIntent =
+ Intent(ACTION_VIEW).apply {
+ setDataAndType(fileUri, getSafeContentType(context, fileUri, contentType))
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ }
+
+ return newIntent
+ }
+
+ @TargetApi(Build.VERSION_CODES.Q)
+ @VisibleForTesting
+ internal fun queryDownloadMediaStore(applicationContext: Context, download: DownloadState): Uri? {
+ val resolver = applicationContext.contentResolver
+ val queryProjection = arrayOf(MediaStore.Downloads._ID)
+ val querySelection = "${MediaStore.Downloads.DISPLAY_NAME} = ?"
+ val querySelectionArgs = arrayOf("${download.fileName}")
+
+ val queryBundle = Bundle().apply {
+ putString(ContentResolver.QUERY_ARG_SQL_SELECTION, querySelection)
+ putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, querySelectionArgs)
+ }
+
+ // Query if we have a pending download with the same name. This can happen
+ // if a download was interrupted, failed or cancelled before the file was
+ // written to disk. Our logic above will have generated a unique file name
+ // based on existing files on the device, but we might already have a row
+ // for the download in the content resolver.
+
+ val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+ val queryCollection =
+ if (SDK_INT >= Build.VERSION_CODES.R) {
+ queryBundle.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE)
+ collection
+ } else {
+ @Suppress("DEPRECATION")
+ setIncludePending(collection)
+ }
+
+ var downloadUri: Uri? = null
+ resolver.query(
+ queryCollection,
+ queryProjection,
+ queryBundle,
+ null,
+ )?.use {
+ if (it.count > 0) {
+ val idColumnIndex = it.getColumnIndex(MediaStore.Downloads._ID)
+ it.moveToFirst()
+ downloadUri = ContentUris.withAppendedId(collection, it.getLong(idColumnIndex))
+ }
+ }
+
+ return downloadUri
+ }
+
+ @VisibleForTesting
+ internal fun getSafeContentType(context: Context, constructedFilePath: Uri, contentType: String?): String {
+ val contentTypeFromFile = context.contentResolver.getType(constructedFilePath)
+ val resultContentType = if (!contentTypeFromFile.isNullOrEmpty()) {
+ contentTypeFromFile
+ } else {
+ contentType.ifNullOrEmpty { "*/*" }
+ }
+ return DownloadUtils.sanitizeMimeType(resultContentType).ifNullOrEmpty { "*/*" }
+ }
+
+ @VisibleForTesting
+ internal fun getSafeContentType(context: Context, filePath: String, contentType: String?): String {
+ return getSafeContentType(context, getFilePathUri(context, filePath), contentType)
+ }
+
+ @VisibleForTesting
+ internal fun getFilePathUri(context: Context, filePath: String): Uri {
+ return FileProvider.getUriForFile(
+ context,
+ context.packageName + FILE_PROVIDER_EXTENSION,
+ File(filePath),
+ )
+ }
+
+ private const val FILE_PROVIDER_EXTENSION = ".feature.downloads.fileprovider"
+ private const val CHUNK_SIZE = 32 * 1024
+ private const val PARTIAL_CONTENT_STATUS = 206
+ private const val OK_STATUS = 200
+
+ /**
+ * This interval was decided on by balancing the limit of the system (200ms) and allowing
+ * users to press buttons on the notification. If a new notification is presented while a
+ * user is tapping a button, their press will be cancelled.
+ */
+ internal const val PROGRESS_UPDATE_INTERVAL = 750L
+
+ const val EXTRA_DOWNLOAD_STATUS = "mozilla.components.feature.downloads.extras.DOWNLOAD_STATUS"
+ const val ACTION_OPEN = "mozilla.components.feature.downloads.OPEN"
+ const val ACTION_PAUSE = "mozilla.components.feature.downloads.PAUSE"
+ const val ACTION_RESUME = "mozilla.components.feature.downloads.RESUME"
+ const val ACTION_CANCEL = "mozilla.components.feature.downloads.CANCEL"
+ const val ACTION_DISMISS = "mozilla.components.feature.downloads.DISMISS"
+ const val ACTION_REMOVE_PRIVATE_DOWNLOAD = "mozilla.components.feature.downloads.ACTION_REMOVE_PRIVATE_DOWNLOAD"
+ const val ACTION_TRY_AGAIN = "mozilla.components.feature.downloads.TRY_AGAIN"
+ const val COMPAT_DEFAULT_FOREGROUND_ID = -1
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
new file mode 100644
index 0000000000..8450318929
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
@@ -0,0 +1,93 @@
+/* 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.downloads
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatDialogFragment
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.BYTES_TO_MB_LIMIT
+import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KILOBYTE
+import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE
+import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
+
+/**
+ * This is a general representation of a dialog meant to be used in collaboration with [DownloadsFeature]
+ * to show a dialog before a download is triggered.
+ * If [SimpleDownloadDialogFragment] is not flexible enough for your use case you should inherit for this class.
+ * Be mindful to call [onStartDownload] when you want to start the download.
+ */
+abstract class DownloadDialogFragment : AppCompatDialogFragment() {
+
+ /**
+ * A callback to trigger a download, call it when you are ready to start a download. For instance,
+ * a valid use case can be in confirmation dialog, after the positive button is clicked,
+ * this callback must be called.
+ */
+ var onStartDownload: () -> Unit = {}
+
+ var onCancelDownload: () -> Unit = {}
+
+ /**
+ * Add the metadata of this download object to the arguments of this fragment.
+ */
+ fun setDownload(download: DownloadState) {
+ val args = arguments ?: Bundle()
+ args.putString(KEY_FILE_NAME, download.realFilenameOrGuessed)
+ args.putString(KEY_URL, download.url)
+ args.putLong(KEY_CONTENT_LENGTH, download.contentLength ?: 0)
+ arguments = args
+ }
+
+ companion object {
+ /**
+ * Key for finding the file name in the arguments.
+ */
+ const val KEY_FILE_NAME = "KEY_FILE_NAME"
+
+ /**
+ * Key for finding the content length in the arguments.
+ */
+ const val KEY_CONTENT_LENGTH = "KEY_CONTENT_LENGTH"
+
+ /**
+ * Key for finding the url in the arguments.
+ */
+ const val KEY_URL = "KEY_URL"
+
+ const val FRAGMENT_TAG = "SHOULD_DOWNLOAD_PROMPT_DIALOG"
+
+ const val MEGABYTE = 1024.0 * 1024.0
+
+ const val KILOBYTE = 1024.0
+
+ const val BYTES_TO_MB_LIMIT = 0.01
+ }
+}
+
+/**
+ * Converts the bytes to megabytes with two decimal places and returns a formatted string
+ */
+fun Long.toMegabyteString(): String {
+ return String.format("%.2f MB", this / MEGABYTE)
+}
+
+/**
+ * Converts the bytes to kilobytes with two decimal places and returns a formatted string
+ */
+fun Long.toKilobyteString(): String {
+ return String.format("%.2f KB", this / KILOBYTE)
+}
+
+/**
+ * Converts the bytes to megabytes or kilobytes( if size smaller than 0.01 MB)
+ * with two decimal places and returns a formatted string
+ */
+fun Long.toMegabyteOrKilobyteString(): String {
+ return if (this / MEGABYTE < BYTES_TO_MB_LIMIT) {
+ this.toKilobyteString()
+ } else {
+ this.toMegabyteString()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadMiddleware.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadMiddleware.kt
new file mode 100644
index 0000000000..c7e480e038
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadMiddleware.kt
@@ -0,0 +1,186 @@
+/* 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.downloads
+
+import android.app.DownloadManager
+import android.content.Context
+import android.content.Intent
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED
+import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED
+import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_REMOVE_PRIVATE_DOWNLOAD
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+import mozilla.components.support.base.log.logger.Logger
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * [Middleware] implementation for managing downloads via the provided download service. Its
+ * purpose is to react to global download state changes (e.g. of [BrowserState.downloads])
+ * and notify the download service, as needed.
+ */
+@Suppress("ComplexMethod")
+class DownloadMiddleware(
+ private val applicationContext: Context,
+ private val downloadServiceClass: Class<*>,
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+ @get:VisibleForTesting
+ internal val downloadStorage: DownloadStorage = DownloadStorage(applicationContext),
+) : Middleware<BrowserState, BrowserAction> {
+ private val logger = Logger("DownloadMiddleware")
+
+ private var scope = CoroutineScope(coroutineContext)
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is DownloadAction.RemoveDownloadAction -> removeDownload(action.downloadId, context.store)
+ is DownloadAction.RemoveAllDownloadsAction -> removeDownloads()
+ is DownloadAction.UpdateDownloadAction -> updateDownload(action.download, context)
+ is DownloadAction.RestoreDownloadsStateAction -> restoreDownloads(context.store)
+ is ContentAction.CancelDownloadAction -> closeDownloadResponse(context.store, action.sessionId)
+ is DownloadAction.AddDownloadAction -> {
+ if (!action.download.private && !saveDownload(context.store, action.download)) {
+ // The download was already added before, so we are ignoring this request.
+ logger.debug(
+ "Ignored add action for ${action.download.id} " +
+ "download already in store.downloads",
+ )
+ return
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+
+ when (action) {
+ is TabListAction.RemoveAllTabsAction,
+ is TabListAction.RemoveAllPrivateTabsAction,
+ -> removePrivateNotifications(context.store)
+ is TabListAction.RemoveTabsAction,
+ is TabListAction.RemoveTabAction,
+ -> {
+ val privateTabs = context.store.state.getNormalOrPrivateTabs(private = true)
+ if (privateTabs.isEmpty()) {
+ removePrivateNotifications(context.store)
+ }
+ }
+ is DownloadAction.AddDownloadAction -> sendDownloadIntent(action.download)
+ is DownloadAction.RestoreDownloadStateAction -> sendDownloadIntent(action.download)
+ else -> {
+ // no-op
+ }
+ }
+ }
+
+ private fun removeDownload(
+ downloadId: String,
+ store: Store<BrowserState, BrowserAction>,
+ ) = scope.launch {
+ store.state.downloads[downloadId]?.let {
+ downloadStorage.remove(it)
+ logger.debug("Removed download ${it.fileName} from the storage")
+ }
+ }
+
+ private fun removeDownloads() = scope.launch {
+ downloadStorage.removeAllDownloads()
+ }
+
+ private fun updateDownload(updated: DownloadState, context: MiddlewareContext<BrowserState, BrowserAction>) {
+ if (updated.private) return
+ context.state.downloads[updated.id]?.let { old ->
+ // To not overwhelm the storage, we only send updates that are relevant,
+ // we only care about properties, that we are stored on the storage.
+ if (!DownloadStorage.isSameDownload(old, updated)) {
+ scope.launch {
+ downloadStorage.update(updated)
+ }
+ logger.debug("Updated download ${updated.fileName} on the storage")
+ }
+ }
+ }
+
+ private fun restoreDownloads(store: Store<BrowserState, BrowserAction>) = scope.launch {
+ downloadStorage.getDownloadsList().forEach { download ->
+ if (!store.state.downloads.containsKey(download.id) && !download.private) {
+ store.dispatch(DownloadAction.RestoreDownloadStateAction(download))
+ logger.debug("Download restored from the storage ${download.fileName}")
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun saveDownload(store: Store<BrowserState, BrowserAction>, download: DownloadState): Boolean {
+ return if (!store.state.downloads.containsKey(download.id) && !download.private) {
+ scope.launch {
+ downloadStorage.add(download)
+ logger.debug("Added download ${download.fileName} to the storage")
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ @VisibleForTesting
+ internal fun closeDownloadResponse(store: Store<BrowserState, BrowserAction>, tabId: String) {
+ store.state.findTabOrCustomTab(tabId)?.let {
+ it.content.download?.response?.close()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun sendDownloadIntent(download: DownloadState) {
+ if (download.status !in arrayOf(COMPLETED, CANCELLED, FAILED)) {
+ val intent = Intent(applicationContext, downloadServiceClass)
+ intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, download.id)
+ startForegroundService(intent)
+ logger.debug("Sending download intent ${download.fileName}")
+ }
+ }
+
+ @VisibleForTesting
+ internal fun startForegroundService(intent: Intent) {
+ ContextCompat.startForegroundService(applicationContext, intent)
+ }
+
+ @VisibleForTesting
+ internal fun removeStatusBarNotification(store: Store<BrowserState, BrowserAction>, download: DownloadState) {
+ download.notificationId?.let {
+ val intent = Intent(applicationContext, downloadServiceClass)
+ intent.action = ACTION_REMOVE_PRIVATE_DOWNLOAD
+ intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, download.id)
+ applicationContext.startService(intent)
+ store.dispatch(DownloadAction.DismissDownloadNotificationAction(download.id))
+ }
+ }
+
+ @VisibleForTesting
+ internal fun removePrivateNotifications(store: Store<BrowserState, BrowserAction>) {
+ val privateDownloads = store.state.downloads.filterValues { it.private }
+ privateDownloads.forEach { removeStatusBarNotification(store, it.value) }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt
new file mode 100644
index 0000000000..9b9c9aa34c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt
@@ -0,0 +1,366 @@
+/* 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.downloads
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.NotificationManagerCompat.IMPORTANCE_NONE
+import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED
+import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED
+import mozilla.components.browser.state.state.content.DownloadState.Status.DOWNLOADING
+import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED
+import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED
+import mozilla.components.browser.state.state.content.DownloadState.Status.PAUSED
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_CANCEL
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_DISMISS
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_PAUSE
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_RESUME
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_TRY_AGAIN
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState
+import mozilla.components.support.utils.PendingIntentUtils
+import kotlin.random.Random
+
+@Suppress("LargeClass")
+internal object DownloadNotification {
+
+ private const val NOTIFICATION_CHANNEL_ID = "mozac.feature.downloads.generic"
+ private const val NOTIFICATION_GROUP_KEY = "mozac.feature.downloads.group"
+ internal const val NOTIFICATION_DOWNLOAD_GROUP_ID = 100
+ private const val LEGACY_NOTIFICATION_CHANNEL_ID = "Downloads"
+ internal const val PERCENTAGE_MULTIPLIER = 100
+
+ internal const val EXTRA_DOWNLOAD_ID = "downloadId"
+
+ @VisibleForTesting
+ internal fun createDownloadGroupNotification(
+ context: Context,
+ notifications: List<DownloadJobState>,
+ notificationAccentColor: Int,
+ ): Notification {
+ val allDownloadsHaveFinished = notifications.all { it.status != DOWNLOADING }
+ val icon = if (allDownloadsHaveFinished) {
+ R.drawable.mozac_feature_download_ic_download_complete
+ } else {
+ R.drawable.mozac_feature_download_ic_ongoing_download
+ }
+ val summaryList = getSummaryList(context, notifications)
+ val summaryLine1 = summaryList.first()
+ val summaryLine2 = if (summaryList.size == 2) summaryList[1] else ""
+
+ return NotificationCompat.Builder(context, ensureChannelExists(context))
+ .setSmallIcon(icon)
+ .setColor(ContextCompat.getColor(context, notificationAccentColor))
+ .setContentTitle(
+ context.applicationContext.getString(R.string.mozac_feature_downloads_notification_channel),
+ )
+ .setContentText(summaryList.joinToString("\n"))
+ .setStyle(NotificationCompat.InboxStyle().addLine(summaryLine1).addLine(summaryLine2))
+ .setGroup(NOTIFICATION_GROUP_KEY)
+ .setGroupSummary(true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .build()
+ }
+
+ /**
+ * Build the notification to be displayed while the download service is active.
+ */
+ fun createOngoingDownloadNotification(
+ context: Context,
+ downloadJobState: DownloadJobState,
+ notificationAccentColor: Int,
+ ): Notification {
+ val downloadState = downloadJobState.state
+ val channelId = ensureChannelExists(context)
+ val isIndeterminate = downloadJobState.isIndeterminate()
+ val percentCopied = downloadJobState.getPercent() ?: -1
+
+ return NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.drawable.mozac_feature_download_ic_ongoing_download)
+ .setContentTitle(downloadState.fileName)
+ .setContentText(downloadJobState.getProgress())
+ .setColor(ContextCompat.getColor(context, notificationAccentColor))
+ .setCategory(NotificationCompat.CATEGORY_PROGRESS)
+ .setProgress(DownloadNotification.PERCENTAGE_MULTIPLIER, percentCopied, isIndeterminate)
+ .setOngoing(true)
+ .setWhen(downloadJobState.createdTime)
+ .setOnlyAlertOnce(true)
+ .addAction(getPauseAction(context, downloadState.id))
+ .addAction(getCancelAction(context, downloadState.id))
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setCompatGroup(NOTIFICATION_GROUP_KEY)
+ .build()
+ }
+
+ /**
+ * Build the notification to be displayed while the download service is paused.
+ */
+ fun createPausedDownloadNotification(
+ context: Context,
+ downloadJobState: DownloadJobState,
+ notificationAccentColor: Int,
+ ): Notification {
+ val channelId = ensureChannelExists(context)
+
+ val downloadState = downloadJobState.state
+ return NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.drawable.mozac_feature_download_ic_download)
+ .setContentTitle(downloadState.fileName)
+ .setContentText(
+ context.applicationContext.getString(R.string.mozac_feature_downloads_paused_notification_text),
+ )
+ .setColor(ContextCompat.getColor(context, notificationAccentColor))
+ .setCategory(NotificationCompat.CATEGORY_PROGRESS)
+ .setOngoing(true)
+ .setWhen(downloadJobState.createdTime)
+ .setOnlyAlertOnce(true)
+ .addAction(getResumeAction(context, downloadState.id))
+ .addAction(getCancelAction(context, downloadState.id))
+ .setDeleteIntent(createDismissPendingIntent(context, downloadState.id))
+ .setCompatGroup(NOTIFICATION_GROUP_KEY)
+ .build()
+ }
+
+ /**
+ * Build the notification to be displayed when a download finishes.
+ */
+ fun createDownloadCompletedNotification(
+ context: Context,
+ downloadJobState: DownloadJobState,
+ notificationAccentColor: Int,
+ contentIntent: PendingIntent = createOpenFilePendingIntent(context, downloadJobState.state),
+ ): Notification {
+ val channelId = ensureChannelExists(context)
+ val downloadState = downloadJobState.state
+
+ return NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.drawable.mozac_feature_download_ic_download_complete)
+ .setContentTitle(downloadState.fileName)
+ .setWhen(downloadJobState.createdTime)
+ .setOnlyAlertOnce(true)
+ .setContentText(
+ context.applicationContext.getString(R.string.mozac_feature_downloads_completed_notification_text2),
+ )
+ .setColor(ContextCompat.getColor(context, notificationAccentColor))
+ .setContentIntent(contentIntent)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setDeleteIntent(createDismissPendingIntent(context, downloadState.id))
+ .setCompatGroup(NOTIFICATION_GROUP_KEY)
+ .build()
+ }
+
+ /**
+ * Build the notification to be displayed when a download fails to finish.
+ */
+ fun createDownloadFailedNotification(
+ context: Context,
+ downloadJobState: DownloadJobState,
+ notificationAccentColor: Int,
+ ): Notification {
+ val channelId = ensureChannelExists(context)
+ val downloadState = downloadJobState.state
+
+ return NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.drawable.mozac_feature_download_ic_download_failed)
+ .setContentTitle(downloadState.fileName)
+ .setContentText(
+ context.applicationContext.getString(R.string.mozac_feature_downloads_failed_notification_text2),
+ )
+ .setColor(ContextCompat.getColor(context, notificationAccentColor))
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+ .addAction(getTryAgainAction(context, downloadState.id))
+ .addAction(getCancelAction(context, downloadState.id))
+ .setWhen(downloadJobState.createdTime)
+ .setOnlyAlertOnce(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setDeleteIntent(createDismissPendingIntent(context, downloadState.id))
+ .setCompatGroup(NOTIFICATION_GROUP_KEY)
+ .build()
+ }
+
+ @VisibleForTesting
+ internal fun getSummaryList(context: Context, notifications: List<DownloadJobState>): List<String> {
+ return notifications.take(2).map { downloadState ->
+ "${downloadState.state.fileName} ${downloadState.getStatusDescription(context)}"
+ }
+ }
+
+ /**
+ * Check if notifications from the download channel are enabled.
+ * Verifies that app notifications, channel notifications, and group notifications are enabled.
+ */
+ fun isChannelEnabled(context: Context): Boolean {
+ return if (SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager: NotificationManager = context.getSystemService()!!
+ if (!notificationManager.areNotificationsEnabled()) return false
+
+ val channelId = ensureChannelExists(context)
+ val channel = notificationManager.getNotificationChannel(channelId)
+ if (channel.importance == IMPORTANCE_NONE) return false
+
+ true
+ } else {
+ NotificationManagerCompat.from(context).areNotificationsEnabled()
+ }
+ }
+
+ /**
+ * Make sure a notification channel for download notification exists.
+ *
+ * Returns the channel id to be used for download notifications.
+ */
+ private fun ensureChannelExists(context: Context): String {
+ if (SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager: NotificationManager = context.getSystemService()!!
+
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ context.applicationContext.getString(R.string.mozac_feature_downloads_notification_channel),
+ NotificationManager.IMPORTANCE_LOW,
+ )
+
+ notificationManager.createNotificationChannel(channel)
+
+ notificationManager.deleteNotificationChannel(LEGACY_NOTIFICATION_CHANNEL_ID)
+ }
+
+ return NOTIFICATION_CHANNEL_ID
+ }
+
+ private fun createOpenFilePendingIntent(context: Context, downloadState: DownloadState) =
+ PendingIntent.getActivity(
+ context,
+ 0,
+ AbstractFetchDownloadService.createOpenFileIntent(context, downloadState),
+ PendingIntentUtils.defaultFlags,
+ )
+
+ private fun getPauseAction(context: Context, downloadStateId: String): NotificationCompat.Action {
+ val pauseIntent = createPendingIntent(context, ACTION_PAUSE, downloadStateId)
+
+ return NotificationCompat.Action.Builder(
+ 0,
+ context.applicationContext.getString(R.string.mozac_feature_downloads_button_pause),
+ pauseIntent,
+ ).build()
+ }
+
+ private fun getResumeAction(context: Context, downloadStateId: String): NotificationCompat.Action {
+ val resumeIntent = createPendingIntent(context, ACTION_RESUME, downloadStateId)
+
+ return NotificationCompat.Action.Builder(
+ 0,
+ context.applicationContext.getString(R.string.mozac_feature_downloads_button_resume),
+ resumeIntent,
+ ).build()
+ }
+
+ private fun getCancelAction(context: Context, downloadStateId: String): NotificationCompat.Action {
+ val cancelIntent = createPendingIntent(context, ACTION_CANCEL, downloadStateId)
+
+ return NotificationCompat.Action.Builder(
+ 0,
+ context.applicationContext.getString(R.string.mozac_feature_downloads_button_cancel),
+ cancelIntent,
+ ).build()
+ }
+
+ private fun getTryAgainAction(context: Context, downloadStateId: String): NotificationCompat.Action {
+ val tryAgainIntent = createPendingIntent(context, ACTION_TRY_AGAIN, downloadStateId)
+
+ return NotificationCompat.Action.Builder(
+ 0,
+ context.applicationContext.getString(R.string.mozac_feature_downloads_button_try_again),
+ tryAgainIntent,
+ ).build()
+ }
+
+ private fun createDismissPendingIntent(context: Context, downloadStateId: String): PendingIntent {
+ return createPendingIntent(context, ACTION_DISMISS, downloadStateId)
+ }
+
+ private fun createPendingIntent(context: Context, action: String, downloadStateId: String): PendingIntent {
+ val intent = Intent(action)
+ intent.setPackage(context.applicationContext.packageName)
+ intent.putExtra(EXTRA_DOWNLOAD_ID, downloadStateId)
+
+ // We generate a random requestCode in order to generate a distinct PendingIntent:
+ // https://developer.android.com/reference/android/app/PendingIntent.html
+ return PendingIntent.getBroadcast(
+ context.applicationContext,
+ Random.nextInt(),
+ intent,
+ PendingIntentUtils.defaultFlags,
+ )
+ }
+}
+
+@VisibleForTesting
+internal fun NotificationCompat.Builder.setCompatGroup(groupKey: String): NotificationCompat.Builder {
+ return if (SDK_INT >= Build.VERSION_CODES.N) {
+ setGroup(groupKey)
+ } else {
+ this
+ }
+}
+
+private fun DownloadJobState.getPercent(): Int? {
+ val bytesCopied = currentBytesCopied
+ val contentLength = state.contentLength
+ return if (contentLength == null || contentLength == 0L) {
+ null
+ } else {
+ (DownloadNotification.PERCENTAGE_MULTIPLIER * bytesCopied / contentLength).toInt()
+ }
+}
+
+@VisibleForTesting
+internal fun DownloadJobState.getProgress(): String {
+ val bytesCopied = currentBytesCopied
+ return if (isIndeterminate()) {
+ ""
+ } else {
+ "${DownloadNotification.PERCENTAGE_MULTIPLIER * bytesCopied / state.contentLength!!}%"
+ }
+}
+
+private fun DownloadJobState.isIndeterminate(): Boolean {
+ val bytesCopied = currentBytesCopied
+ return state.contentLength == null || bytesCopied == 0L || state.contentLength == 0L
+}
+
+@VisibleForTesting
+internal fun DownloadJobState.getStatusDescription(context: Context): String {
+ return when (this.status) {
+ DOWNLOADING -> {
+ getProgress()
+ }
+
+ PAUSED -> {
+ context.applicationContext.getString(R.string.mozac_feature_downloads_paused_notification_text)
+ }
+
+ COMPLETED -> {
+ context.applicationContext.getString(R.string.mozac_feature_downloads_completed_notification_text2)
+ }
+
+ FAILED -> {
+ context.applicationContext.getString(R.string.mozac_feature_downloads_failed_notification_text2)
+ }
+
+ CANCELLED, INITIATED -> ""
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadStorage.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadStorage.kt
new file mode 100644
index 0000000000..f1ee939204
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadStorage.kt
@@ -0,0 +1,98 @@
+/* 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.downloads
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.paging.DataSource
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.feature.downloads.db.DownloadsDatabase
+import mozilla.components.feature.downloads.db.toDownloadEntity
+
+/**
+ * A storage implementation for organizing download.
+ */
+class DownloadStorage(context: Context) {
+
+ @VisibleForTesting
+ internal var database: Lazy<DownloadsDatabase> = lazy { DownloadsDatabase.get(context) }
+
+ private val downloadDao by lazy { database.value.downloadDao() }
+
+ /**
+ * Adds a new [download].
+ */
+ suspend fun add(download: DownloadState) {
+ downloadDao.insert(download.toDownloadEntity())
+ }
+
+ /**
+ * Returns a [Flow] list of all the [DownloadState] instances.
+ */
+ fun getDownloads(): Flow<List<DownloadState>> {
+ return downloadDao.getDownloads().map { list ->
+ list.map { entity -> entity.toDownloadState() }
+ }
+ }
+
+ /**
+ * Returns a [List] of all the [DownloadState] instances.
+ */
+ suspend fun getDownloadsList(): List<DownloadState> {
+ return downloadDao.getDownloadsList().map { entity ->
+ entity.toDownloadState()
+ }
+ }
+
+ /**
+ * Returns all saved [DownloadState] instances as a [DataSource.Factory].
+ */
+ fun getDownloadsPaged(): DataSource.Factory<Int, DownloadState> = downloadDao
+ .getDownloadsPaged()
+ .map { entity ->
+ entity.toDownloadState()
+ }
+
+ /**
+ * Removes the given [download].
+ */
+ suspend fun remove(download: DownloadState) {
+ downloadDao.delete(download.toDownloadEntity())
+ }
+
+ /**
+ * Update the given [download].
+ */
+ suspend fun update(download: DownloadState) {
+ downloadDao.update(download.toDownloadEntity())
+ }
+
+ /**
+ * Removes all the downloads.
+ */
+ suspend fun removeAllDownloads() {
+ downloadDao.deleteAllDownloads()
+ }
+
+ companion object {
+ /**
+ * Takes two [DownloadState] objects and the determine if they are the same, be aware this
+ * only takes into considerations fields that are being stored,
+ * not all the field on [DownloadState] are stored.
+ */
+ fun isSameDownload(first: DownloadState, second: DownloadState): Boolean {
+ return first.id == second.id &&
+ first.fileName == second.fileName &&
+ first.url == second.url &&
+ first.contentType == second.contentType &&
+ first.contentLength == second.contentLength &&
+ first.status == second.status &&
+ first.destinationDirectory == second.destinationDirectory &&
+ first.createdTime == second.createdTime
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
new file mode 100644
index 0000000000..a5b94c0545
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
@@ -0,0 +1,499 @@
+/* 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.downloads
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ResolveInfo
+import android.widget.Toast
+import androidx.annotation.ColorRes
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.core.net.toUri
+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.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.FRAGMENT_TAG
+import mozilla.components.feature.downloads.dialog.DeniedPermissionDialogFragment
+import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
+import mozilla.components.feature.downloads.facts.emitPromptDismissedFact
+import mozilla.components.feature.downloads.facts.emitPromptDisplayedFact
+import mozilla.components.feature.downloads.manager.AndroidDownloadManager
+import mozilla.components.feature.downloads.manager.DownloadManager
+import mozilla.components.feature.downloads.manager.noop
+import mozilla.components.feature.downloads.manager.onDownloadStopped
+import mozilla.components.feature.downloads.ui.DownloadAppChooserDialog
+import mozilla.components.feature.downloads.ui.DownloaderApp
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.OnNeedToRequestPermissions
+import mozilla.components.support.base.feature.PermissionsFeature
+import mozilla.components.support.ktx.android.content.appName
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+import mozilla.components.support.ktx.kotlin.isSameOriginAs
+import mozilla.components.support.utils.Browsers
+
+/**
+ * The name of the file to be downloaded.
+ */
+@JvmInline
+value class Filename(val value: String)
+
+/**
+ * The size of the file to be downloaded expressed as the number of `bytes`.
+ * The value will be `0` if the size is unknown.
+ */
+@JvmInline
+value class ContentSize(val value: Long)
+
+/**
+ * The list of all applications that can perform a download, including this application.
+ */
+@JvmInline
+value class ThirdPartyDownloaderApps(val value: List<DownloaderApp>)
+
+/**
+ * Callback for when the user picked a certain application with which to download the current file.
+ */
+@JvmInline
+value class ThirdPartyDownloaderAppChosenCallback(val value: (DownloaderApp) -> Unit)
+
+/**
+ * Callback for when the positive button of a download dialog was tapped.
+ */
+@JvmInline
+value class PositiveActionCallback(val value: () -> Unit)
+
+/**
+ * Callback for when the negative button of a download dialog was tapped.
+ */
+@JvmInline
+value class NegativeActionCallback(val value: () -> Unit)
+
+/**
+ * Feature implementation to provide download functionality for the selected
+ * session. The feature will subscribe to the selected session and listen
+ * for downloads.
+ *
+ * @property applicationContext a reference to the application context.
+ * @property onNeedToRequestPermissions a callback invoked when permissions
+ * need to be requested before a download can be performed. Once the request
+ * is completed, [onPermissionsResult] needs to be invoked.
+ * @property onDownloadStopped a callback invoked when a download is paused or completed.
+ * @property downloadManager a reference to the [DownloadManager] which is
+ * responsible for performing the downloads.
+ * @property store a reference to the application's [BrowserStore].
+ * @property useCases [DownloadsUseCases] instance for consuming processed downloads.
+ * @property fragmentManager a reference to a [FragmentManager]. If a fragment
+ * manager is provided, a dialog will be shown before every download.
+ * @property promptsStyling styling properties for the dialog.
+ * @property shouldForwardToThirdParties Indicates if downloads should be forward to third party apps,
+ * if there are multiple apps a chooser dialog will shown.
+ * @property customFirstPartyDownloadDialog An optional delegate for showing a dialog for a download
+ * that will be processed by the current application.
+ * @property customThirdPartyDownloadDialog An optional delegate for showing a dialog for a download
+ * that can be processed by multiple installed applications including the current one.
+ */
+@Suppress("LargeClass")
+class DownloadsFeature(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ @get:VisibleForTesting(otherwise = PRIVATE)
+ internal val useCases: DownloadsUseCases,
+ override var onNeedToRequestPermissions: OnNeedToRequestPermissions = { },
+ onDownloadStopped: onDownloadStopped = noop,
+ private val downloadManager: DownloadManager = AndroidDownloadManager(applicationContext, store),
+ private val tabId: String? = null,
+ private val fragmentManager: FragmentManager? = null,
+ private val promptsStyling: PromptsStyling? = null,
+ private val shouldForwardToThirdParties: () -> Boolean = { false },
+ private val customFirstPartyDownloadDialog:
+ ((Filename, ContentSize, PositiveActionCallback, NegativeActionCallback) -> Unit)? = null,
+ private val customThirdPartyDownloadDialog:
+ ((ThirdPartyDownloaderApps, ThirdPartyDownloaderAppChosenCallback, NegativeActionCallback) -> Unit)? = null,
+) : LifecycleAwareFeature, PermissionsFeature {
+
+ var onDownloadStopped: onDownloadStopped
+ get() = downloadManager.onDownloadStopped
+ set(value) { downloadManager.onDownloadStopped = value }
+
+ init {
+ this.onDownloadStopped = onDownloadStopped
+ }
+
+ private var scope: CoroutineScope? = null
+
+ @VisibleForTesting
+ internal var dismissPromptScope: CoroutineScope? = null
+
+ @VisibleForTesting
+ internal var previousTab: SessionState? = null
+
+ /**
+ * Starts observing downloads on the selected session and sends them to the [DownloadManager]
+ * to be processed.
+ */
+ @Suppress("Deprecation")
+ override fun start() {
+ // Dismiss the previous prompts when the user navigates to another site.
+ // This prevents prompts from the previous page from covering content.
+ dismissPromptScope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .distinctUntilChangedBy { it.content.url }
+ .collect {
+ val currentHost = previousTab?.content?.url
+ val newHost = it.content.url
+
+ // The user is navigating to another site
+ if (currentHost?.isSameOriginAs(newHost) == false) {
+ previousTab?.let { tab ->
+ // We have an old download request.
+ tab.content.download?.let { download ->
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ dismissAllDownloadDialogs()
+ previousTab = null
+ }
+ }
+ }
+ }
+ }
+
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .distinctUntilChangedBy { it.content.download }
+ .collect { state ->
+ state.content.download?.let { downloadState ->
+ previousTab = state
+ processDownload(state, downloadState)
+ }
+ }
+ }
+ }
+
+ /**
+ * Calls the tryAgain function of the corresponding [DownloadManager]
+ */
+ @Suppress("Unused")
+ fun tryAgain(id: String) {
+ downloadManager.tryAgain(id)
+ }
+
+ /**
+ * Stops observing downloads on the selected session.
+ */
+ override fun stop() {
+ scope?.cancel()
+ dismissPromptScope?.cancel()
+ downloadManager.unregisterListeners()
+ }
+
+ /**
+ * Notifies the [DownloadManager] that a new download must be processed.
+ */
+ @VisibleForTesting
+ internal fun processDownload(tab: SessionState, download: DownloadState): Boolean {
+ val apps = getDownloaderApps(applicationContext, download)
+ // We only show the dialog If we have multiple apps that can handle the download.
+ val shouldShowAppDownloaderDialog = shouldForwardToThirdParties() && apps.size > 1
+
+ return if (shouldShowAppDownloaderDialog) {
+ when (customThirdPartyDownloadDialog) {
+ null -> showAppDownloaderDialog(tab, download, apps)
+ else -> customThirdPartyDownloadDialog.invoke(
+ ThirdPartyDownloaderApps(apps),
+ ThirdPartyDownloaderAppChosenCallback {
+ onDownloaderAppSelected(it, tab, download)
+ },
+ NegativeActionCallback {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ },
+ )
+ }
+
+ false
+ } else {
+ if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
+ when {
+ customFirstPartyDownloadDialog != null && !download.skipConfirmation -> {
+ customFirstPartyDownloadDialog.invoke(
+ Filename(download.realFilenameOrGuessed),
+ ContentSize(download.contentLength ?: 0),
+ PositiveActionCallback {
+ startDownload(download)
+ useCases.consumeDownload.invoke(tab.id, download.id)
+ },
+ NegativeActionCallback {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ },
+ )
+ false
+ }
+
+ fragmentManager != null && !download.skipConfirmation -> {
+ showDownloadDialog(tab, download)
+ false
+ }
+
+ else -> {
+ useCases.consumeDownload(tab.id, download.id)
+ startDownload(download)
+ }
+ }
+ } else {
+ onNeedToRequestPermissions(downloadManager.permissions)
+ false
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun startDownload(download: DownloadState): Boolean {
+ val id = downloadManager.download(download)
+ return if (id != null) {
+ true
+ } else {
+ showDownloadNotSupportedError()
+ false
+ }
+ }
+
+ /**
+ * Notifies the feature that the permissions request was completed. It will then
+ * either trigger or clear the pending download.
+ */
+ override fun onPermissionsResult(permissions: Array<String>, grantResults: IntArray) {
+ if (permissions.isEmpty()) {
+ // If we are requesting permissions while a permission prompt is already being displayed
+ // then Android seems to call `onPermissionsResult` immediately with an empty permissions
+ // list. In this case just ignore it.
+ return
+ }
+
+ withActiveDownload { (tab, download) ->
+ if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
+ if (shouldForwardToThirdParties()) {
+ startDownload(download)
+ useCases.consumeDownload(tab.id, download.id)
+ } else {
+ processDownload(tab, download)
+ }
+ } else {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ showPermissionDeniedDialog()
+ }
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun showDownloadNotSupportedError() {
+ Toast.makeText(
+ applicationContext,
+ applicationContext.getString(
+ R.string.mozac_feature_downloads_file_not_supported2,
+ applicationContext.appName,
+ ),
+ Toast.LENGTH_LONG,
+ ).show()
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun showDownloadDialog(
+ tab: SessionState,
+ download: DownloadState,
+ dialog: DownloadDialogFragment = getDownloadDialog(),
+ ) {
+ dialog.setDownload(download)
+
+ dialog.onStartDownload = {
+ startDownload(download)
+ useCases.consumeDownload.invoke(tab.id, download.id)
+ }
+
+ dialog.onCancelDownload = {
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ }
+
+ if (!isAlreadyADownloadDialog() && fragmentManager != null && !fragmentManager.isDestroyed) {
+ emitPromptDisplayedFact()
+ dialog.showNow(fragmentManager, FRAGMENT_TAG)
+ }
+ }
+
+ private fun getDownloadDialog(): DownloadDialogFragment {
+ return findPreviousDownloadDialogFragment() ?: SimpleDownloadDialogFragment.newInstance(
+ promptsStyling = promptsStyling,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun showAppDownloaderDialog(
+ tab: SessionState,
+ download: DownloadState,
+ apps: List<DownloaderApp>,
+ appChooserDialog: DownloadAppChooserDialog = getAppDownloaderDialog(),
+ ) {
+ appChooserDialog.setApps(apps)
+ appChooserDialog.onAppSelected = { app ->
+ onDownloaderAppSelected(app, tab, download)
+ }
+
+ appChooserDialog.onDismiss = {
+ emitPromptDismissedFact()
+ useCases.cancelDownloadRequest.invoke(tab.id, download.id)
+ }
+
+ if (!isAlreadyAppDownloaderDialog() && fragmentManager != null && !fragmentManager.isDestroyed) {
+ emitPromptDisplayedFact()
+ appChooserDialog.showNow(fragmentManager, DownloadAppChooserDialog.FRAGMENT_TAG)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun onDownloaderAppSelected(app: DownloaderApp, tab: SessionState, download: DownloadState) {
+ if (app.packageName == applicationContext.packageName) {
+ if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) {
+ startDownload(download)
+ useCases.consumeDownload(tab.id, download.id)
+ } else {
+ onNeedToRequestPermissions(downloadManager.permissions)
+ }
+ } else {
+ try {
+ applicationContext.startActivity(app.toIntent())
+ } catch (error: ActivityNotFoundException) {
+ val errorMessage = applicationContext.getString(
+ R.string.mozac_feature_downloads_unable_to_open_third_party_app,
+ app.name,
+ )
+ Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show()
+ }
+ useCases.consumeDownload(tab.id, download.id)
+ }
+ }
+
+ private fun getAppDownloaderDialog() = findPreviousAppDownloaderDialogFragment()
+ ?: DownloadAppChooserDialog.newInstance(
+ promptsStyling?.gravity,
+ promptsStyling?.shouldWidthMatchParent,
+ )
+
+ @VisibleForTesting
+ internal fun isAlreadyAppDownloaderDialog(): Boolean {
+ return findPreviousAppDownloaderDialogFragment() != null
+ }
+
+ private fun findPreviousAppDownloaderDialogFragment(): DownloadAppChooserDialog? {
+ return fragmentManager?.findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG) as? DownloadAppChooserDialog
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun isAlreadyADownloadDialog(): Boolean {
+ return findPreviousDownloadDialogFragment() != null
+ }
+
+ private fun findPreviousDownloadDialogFragment(): DownloadDialogFragment? {
+ return fragmentManager?.findFragmentByTag(FRAGMENT_TAG) as? DownloadDialogFragment
+ }
+
+ private fun withActiveDownload(block: (Pair<SessionState, DownloadState>) -> Unit) {
+ val state = store.state.findTabOrCustomTabOrSelectedTab(tabId) ?: return
+ val download = state.content.download ?: return
+ block(Pair(state, download))
+ }
+
+ /**
+ * Find all apps that can perform a download, including this app.
+ */
+ @VisibleForTesting
+ internal fun getDownloaderApps(context: Context, download: DownloadState): List<DownloaderApp> {
+ val packageManager = context.packageManager
+
+ val browsers = Browsers.findResolvers(context, packageManager, includeThisApp = true)
+ .associateBy { it.activityInfo.identifier }
+
+ val thisApp = browsers.values
+ .firstOrNull { it.activityInfo.packageName == context.packageName }
+ ?.toDownloaderApp(context, download)
+
+ // Check for data URL that can cause a TransactionTooLargeException when querying for apps
+ // See https://github.com/mozilla-mobile/android-components/issues/9665
+ if (download.url.startsWith("data:")) {
+ return listOfNotNull(thisApp)
+ }
+
+ val apps = Browsers.findResolvers(
+ context,
+ packageManager,
+ includeThisApp = false,
+ url = download.url,
+ contentType = download.contentType,
+ )
+ // Remove browsers and returns only the apps that can perform a download plus this app.
+ return apps.filter { !browsers.contains(it.activityInfo.identifier) }
+ .map { it.toDownloaderApp(context, download) } + listOfNotNull(thisApp)
+ }
+
+ @VisibleForTesting
+ internal fun dismissAllDownloadDialogs() {
+ findPreviousDownloadDialogFragment()?.dismiss()
+ findPreviousAppDownloaderDialogFragment()?.dismiss()
+ }
+
+ private val ActivityInfo.identifier: String get() = packageName + name
+
+ @VisibleForTesting
+ internal fun DownloaderApp.toIntent(): Intent {
+ return Intent(Intent.ACTION_VIEW).apply {
+ setDataAndTypeAndNormalize(url.toUri(), contentType)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ setClassName(packageName, activityName)
+ addCategory(Intent.CATEGORY_BROWSABLE)
+ }
+ }
+
+ /**
+ * Styling for the download dialog prompt
+ */
+ data class PromptsStyling(
+ val gravity: Int,
+ val shouldWidthMatchParent: Boolean = false,
+ @ColorRes
+ val positiveButtonBackgroundColor: Int? = null,
+ @ColorRes
+ val positiveButtonTextColor: Int? = null,
+ val positiveButtonRadius: Float? = null,
+ val fileNameEndMargin: Int? = null,
+ )
+
+ @VisibleForTesting
+ internal fun showPermissionDeniedDialog() {
+ fragmentManager?.let {
+ val dialog = DeniedPermissionDialogFragment.newInstance(
+ R.string.mozac_feature_downloads_write_external_storage_permissions_needed_message,
+ )
+ dialog.showNow(fragmentManager, DeniedPermissionDialogFragment.FRAGMENT_TAG)
+ }
+ }
+}
+
+@VisibleForTesting
+internal fun ResolveInfo.toDownloaderApp(context: Context, download: DownloadState): DownloaderApp {
+ return DownloaderApp(
+ loadLabel(context.packageManager).toString(),
+ this,
+ activityInfo.packageName,
+ activityInfo.name,
+ download.url,
+ download.contentType,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsUseCases.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsUseCases.kt
new file mode 100644
index 0000000000..343fdfd65f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsUseCases.kt
@@ -0,0 +1,92 @@
+/* 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.downloads
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.store.BrowserStore
+
+/**
+ * Contains use cases related to the downloads feature.
+ *
+ * @param store the application's [BrowserStore].
+ */
+class DownloadsUseCases(
+ store: BrowserStore,
+) {
+
+ /**
+ * Use case that cancels the download request from a tab.
+ */
+ class CancelDownloadRequestUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Cancels the download request the session with the given [tabId].
+ */
+ operator fun invoke(tabId: String, downloadId: String) {
+ store.dispatch(ContentAction.CancelDownloadAction(tabId, downloadId))
+ }
+ }
+
+ class ConsumeDownloadUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Consumes the download with the given [downloadId] from the session with the given
+ * [tabId].
+ */
+ operator fun invoke(tabId: String, downloadId: String) {
+ store.dispatch(
+ ContentAction.ConsumeDownloadAction(
+ tabId,
+ downloadId,
+ ),
+ )
+ }
+ }
+
+ /**
+ * Use case that allows to restore downloads from the storage.
+ */
+ class RestoreDownloadsUseCase(private val store: BrowserStore) {
+ /**
+ * Restores downloads from the storage.
+ */
+ operator fun invoke() {
+ store.dispatch(DownloadAction.RestoreDownloadsStateAction)
+ }
+ }
+
+ /**
+ * Use case that allows to remove a download.
+ */
+ class RemoveDownloadUseCase(private val store: BrowserStore) {
+ /**
+ * Removes the download with the given [downloadId].
+ */
+ operator fun invoke(downloadId: String) {
+ store.dispatch(DownloadAction.RemoveDownloadAction(downloadId))
+ }
+ }
+
+ /**
+ * Use case that allows to remove all downloads.
+ */
+ class RemoveAllDownloadsUseCase(private val store: BrowserStore) {
+ /**
+ * Removes all downloads.
+ */
+ operator fun invoke() {
+ store.dispatch(DownloadAction.RemoveAllDownloadsAction)
+ }
+ }
+
+ val cancelDownloadRequest = CancelDownloadRequestUseCase(store)
+ val consumeDownload = ConsumeDownloadUseCase(store)
+ val restoreDownloads = RestoreDownloadsUseCase(store)
+ val removeDownload = RemoveDownloadUseCase(store)
+ val removeAllDownloads = RemoveAllDownloadsUseCase(store)
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragment.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragment.kt
new file mode 100644
index 0000000000..66695d4a5d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragment.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.downloads
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.GradientDrawable
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import androidx.annotation.StringRes
+import androidx.annotation.StyleRes
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.core.view.marginBottom
+import androidx.core.view.marginStart
+import androidx.core.view.marginTop
+import mozilla.components.feature.downloads.databinding.MozacDownloadsPromptBinding
+
+/**
+ * A confirmation dialog to be called before a download is triggered.
+ * Meant to be used in collaboration with [DownloadsFeature]
+ *
+ * [SimpleDownloadDialogFragment] is the default dialog used by DownloadsFeature if you don't provide a value.
+ * It is composed by a title, a negative and a positive bottoms. When the positive button is clicked
+ * the download is triggered.
+ *
+ */
+class SimpleDownloadDialogFragment : DownloadDialogFragment() {
+
+ private val safeArguments get() = requireNotNull(arguments)
+
+ @VisibleForTesting
+ internal var testingContext: Context? = null
+
+ internal val dialogGravity: Int get() =
+ safeArguments.getInt(KEY_DIALOG_GRAVITY, DEFAULT_VALUE)
+ internal val dialogShouldWidthMatchParent: Boolean get() =
+ safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT)
+
+ internal val positiveButtonBackgroundColor get() =
+ safeArguments.getInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, DEFAULT_VALUE)
+ internal val positiveButtonTextColor get() =
+ safeArguments.getInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, DEFAULT_VALUE)
+ internal val positiveButtonRadius get() =
+ safeArguments.getFloat(KEY_POSITIVE_BUTTON_RADIUS, DEFAULT_VALUE.toFloat())
+ internal val fileNameEndMargin get() =
+ safeArguments.getInt(KEY_FILE_NAME_END_MARGIN, DEFAULT_VALUE)
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val sheetDialog = Dialog(requireContext())
+ sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ sheetDialog.setCanceledOnTouchOutside(false)
+
+ val rootView = createContainer()
+ sheetDialog.setContainerView(rootView)
+ sheetDialog.window?.apply {
+ if (dialogGravity != DEFAULT_VALUE) {
+ setGravity(dialogGravity)
+ }
+
+ if (dialogShouldWidthMatchParent) {
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ // This must be called after addContentView, or it won't fully fill to the edge.
+ setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+ }
+ return sheetDialog
+ }
+
+ @SuppressLint("InflateParams")
+ private fun createContainer(): View {
+ val rootView = LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_downloads_prompt,
+ null,
+ false,
+ )
+
+ val binding = MozacDownloadsPromptBinding.bind(rootView)
+
+ with(requireBundle()) {
+ binding.title.text = if (getLong(KEY_CONTENT_LENGTH) <= 0L) {
+ getString(R.string.mozac_feature_downloads_dialog_download)
+ } else {
+ val contentSize = getLong(KEY_CONTENT_LENGTH).toMegabyteOrKilobyteString()
+ getString(getInt(KEY_TITLE_TEXT, R.string.mozac_feature_downloads_dialog_title2), contentSize)
+ }
+
+ if (positiveButtonBackgroundColor != DEFAULT_VALUE) {
+ val backgroundTintList = ContextCompat.getColorStateList(
+ requireContext(),
+ positiveButtonBackgroundColor,
+ )
+ binding.downloadButton.backgroundTintList = backgroundTintList
+ }
+
+ if (positiveButtonTextColor != DEFAULT_VALUE) {
+ val color = ContextCompat.getColor(requireContext(), positiveButtonTextColor)
+ binding.downloadButton.setTextColor(color)
+ }
+
+ if (positiveButtonRadius != DEFAULT_VALUE.toFloat()) {
+ val shape = GradientDrawable()
+ shape.shape = GradientDrawable.RECTANGLE
+ shape.setColor(
+ ContextCompat.getColor(
+ requireContext(),
+ positiveButtonBackgroundColor,
+ ),
+ )
+ shape.cornerRadius = positiveButtonRadius
+ binding.downloadButton.background = shape
+ }
+
+ if (fileNameEndMargin != DEFAULT_VALUE) {
+ binding.filename.layoutParams = RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
+ ).apply {
+ marginEnd = fileNameEndMargin
+ marginStart = binding.filename.marginStart
+ topMargin = binding.filename.marginTop
+ bottomMargin = binding.filename.marginBottom
+ addRule(RelativeLayout.BELOW, R.id.title)
+ addRule(RelativeLayout.END_OF, R.id.icon)
+ addRule(RelativeLayout.ALIGN_BASELINE, R.id.icon)
+ }
+ }
+
+ binding.filename.text = getString(KEY_FILE_NAME, "")
+ binding.filename.movementMethod = ScrollingMovementMethod()
+
+ binding.downloadButton.text = getString(
+ getInt(KEY_DOWNLOAD_TEXT, R.string.mozac_feature_downloads_dialog_download),
+ )
+
+ binding.closeButton.setOnClickListener {
+ onCancelDownload()
+ dismiss()
+ }
+
+ binding.downloadButton.setOnClickListener {
+ onStartDownload()
+ dismiss()
+ }
+ }
+
+ return rootView
+ }
+
+ private fun Dialog.setContainerView(rootView: View) {
+ if (dialogShouldWidthMatchParent) {
+ setContentView(rootView)
+ } else {
+ addContentView(
+ rootView,
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ ),
+ )
+ }
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [SimpleDownloadDialogFragment]
+ */
+ fun newInstance(
+ @StringRes dialogTitleText: Int = R.string.mozac_feature_downloads_dialog_title2,
+ @StringRes downloadButtonText: Int = R.string.mozac_feature_downloads_dialog_download,
+ @StyleRes themeResId: Int = 0,
+ promptsStyling: DownloadsFeature.PromptsStyling? = null,
+ ): SimpleDownloadDialogFragment {
+ val fragment = SimpleDownloadDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putInt(KEY_DOWNLOAD_TEXT, downloadButtonText)
+ putInt(KEY_THEME_ID, themeResId)
+ putInt(KEY_TITLE_TEXT, dialogTitleText)
+
+ promptsStyling?.apply {
+ putInt(KEY_DIALOG_GRAVITY, gravity)
+ putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, shouldWidthMatchParent)
+
+ positiveButtonBackgroundColor?.let {
+ putInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, it)
+ }
+
+ positiveButtonTextColor?.let {
+ putInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, it)
+ }
+
+ positiveButtonRadius?.let {
+ putFloat(KEY_POSITIVE_BUTTON_RADIUS, it)
+ }
+
+ fileNameEndMargin?.let {
+ putInt(KEY_FILE_NAME_END_MARGIN, it)
+ }
+ }
+ }
+
+ fragment.arguments = arguments
+
+ return fragment
+ }
+
+ const val KEY_DOWNLOAD_TEXT = "KEY_DOWNLOAD_TEXT"
+
+ // WARNING: If KEY_CONTENT_LENGTH is <= 0, this will be overriden with the default string "Download"
+ const val KEY_TITLE_TEXT = "KEY_TITLE_TEXT"
+ const val KEY_THEME_ID = "KEY_THEME_ID"
+
+ private const val KEY_POSITIVE_BUTTON_BACKGROUND_COLOR = "KEY_POSITIVE_BUTTON_BACKGROUND_COLOR"
+ private const val KEY_POSITIVE_BUTTON_TEXT_COLOR = "KEY_POSITIVE_BUTTON_TEXT_COLOR"
+ private const val KEY_POSITIVE_BUTTON_RADIUS = "KEY_POSITIVE_BUTTON_RADIUS"
+ private const val KEY_FILE_NAME_END_MARGIN = "KEY_FILE_NAME_END_MARGIN"
+ private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY"
+ private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT"
+ private const val DEFAULT_VALUE = Int.MAX_VALUE
+ }
+
+ private fun requireBundle(): Bundle {
+ return arguments ?: throw IllegalStateException("Fragment $this arguments is not set.")
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadDao.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadDao.kt
new file mode 100644
index 0000000000..edd31debd2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadDao.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.downloads.db
+
+import androidx.paging.DataSource
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Internal dao for accessing and modifying Downloads in the database.
+ */
+@Dao
+internal interface DownloadDao {
+
+ @Insert
+ suspend fun insert(entity: DownloadEntity): Long
+
+ @Update
+ suspend fun update(entity: DownloadEntity)
+
+ @Query("SELECT * FROM downloads ORDER BY created_at DESC")
+ fun getDownloads(): Flow<List<DownloadEntity>>
+
+ @Query("SELECT * FROM downloads ORDER BY created_at DESC")
+ suspend fun getDownloadsList(): List<DownloadEntity>
+
+ @Delete
+ suspend fun delete(entity: DownloadEntity)
+
+ @Query("DELETE FROM downloads")
+ suspend fun deleteAllDownloads()
+
+ @Query("SELECT * FROM downloads ORDER BY created_at DESC")
+ fun getDownloadsPaged(): DataSource.Factory<Int, DownloadEntity>
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadEntity.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadEntity.kt
new file mode 100644
index 0000000000..be913e51eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadEntity.kt
@@ -0,0 +1,86 @@
+/* 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.downloads.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.browser.state.state.content.DownloadState
+
+/**
+ * Internal entity representing a download as it gets saved to the database.
+ */
+@Entity(tableName = "downloads")
+internal data class DownloadEntity(
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ var id: String,
+
+ @ColumnInfo(name = "url")
+ var url: String,
+
+ @ColumnInfo(name = "file_name")
+ var fileName: String?,
+
+ @ColumnInfo(name = "content_type")
+ var contentType: String?,
+
+ @ColumnInfo(name = "content_length")
+ var contentLength: Long?,
+
+ @ColumnInfo(name = "status")
+ var status: DownloadState.Status,
+
+ @ColumnInfo(name = "destination_directory")
+ var destinationDirectory: String,
+
+ @ColumnInfo(name = "created_at")
+ var createdAt: Long,
+
+) {
+
+ internal fun toDownloadState(): DownloadState {
+ return DownloadState(
+ url,
+ fileName,
+ contentType,
+ contentLength,
+ currentBytesCopied = 0,
+ status = status,
+ userAgent = null,
+ destinationDirectory = destinationDirectory,
+ referrerUrl = null,
+ skipConfirmation = false,
+ id = id,
+ sessionId = null,
+ createdTime = createdAt,
+ )
+ }
+}
+
+internal fun DownloadState.toDownloadEntity(): DownloadEntity {
+ /**
+ * Data URLs cause problems when restoring the values from the db,
+ * as the string could be so long that it could break the maximum allowed size for a cursor,
+ * causing SQLiteBIobTooBigException when restoring downloads from the DB.
+ */
+ val isDataURL = url.startsWith("data:")
+ val sanitizedURL = if (isDataURL) {
+ ""
+ } else {
+ url
+ }
+
+ return DownloadEntity(
+ id,
+ sanitizedURL,
+ fileName,
+ contentType,
+ contentLength,
+ status = status,
+ destinationDirectory = destinationDirectory,
+ createdAt = createdTime,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadsDatabase.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadsDatabase.kt
new file mode 100644
index 0000000000..9adabfe9cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadsDatabase.kt
@@ -0,0 +1,91 @@
+/* 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.downloads.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import mozilla.components.browser.state.state.content.DownloadState
+
+/**
+ * Internal database for saving downloads.
+ */
+@Database(entities = [DownloadEntity::class], version = 4)
+@TypeConverters(StatusConverter::class)
+internal abstract class DownloadsDatabase : RoomDatabase() {
+ abstract fun downloadDao(): DownloadDao
+
+ companion object {
+ @Volatile
+ private var instance: DownloadsDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): DownloadsDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ DownloadsDatabase::class.java,
+ "mozac_downloads_database",
+ ).addMigrations(
+ Migrations.migration_1_2,
+ Migrations.migration_2_3,
+ Migrations.migration_3_4,
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
+
+@Suppress("MaxLineLength", "MagicNumber")
+internal object Migrations {
+ val migration_1_2 = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL(
+ "ALTER TABLE downloads ADD COLUMN is_private INTEGER NOT NULL DEFAULT 0",
+ )
+ }
+ }
+ val migration_2_3 = object : Migration(2, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Create a temporal table
+ db.execSQL("CREATE TABLE temp_downloads (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))")
+ // Copy the data
+ db.execSQL("INSERT INTO temp_downloads (id,url,file_name,content_type,content_length,status,destination_directory,created_at) SELECT id,url,file_name,content_type,content_length,status,destination_directory,created_at FROM downloads where is_private = 0")
+ // Remove the old table
+ db.execSQL("DROP TABLE downloads")
+ // Rename the table name to the correct one
+ db.execSQL("ALTER TABLE temp_downloads RENAME TO downloads")
+ }
+ }
+
+ val migration_3_4 = object : Migration(3, 4) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Clear any data urls.
+ db.execSQL("UPDATE downloads SET url='' WHERE url LIKE 'data:%' ")
+ }
+ }
+}
+
+@Suppress("unused")
+internal class StatusConverter {
+ private val statusArray = DownloadState.Status.values()
+
+ @TypeConverter
+ fun toInt(status: DownloadState.Status): Int {
+ return status.id
+ }
+
+ @TypeConverter
+ fun toStatus(index: Int): DownloadState.Status? {
+ return statusArray.find { it.id == index }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragment.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragment.kt
new file mode 100644
index 0000000000..1278bd37fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragment.kt
@@ -0,0 +1,74 @@
+/* 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.downloads.dialog
+
+import android.app.Dialog
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.provider.Settings
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import mozilla.components.support.base.R
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+internal const val KEY_MESSAGE = "KEY_MESSAGE"
+
+/**
+ * A dialog to be displayed when the Android permission is denied,
+ * users should be notified and offered a way activate it on the app settings.
+ * The dialog will have two buttons: One "Go to settings" and another for "Dismissing".
+ */
+class DeniedPermissionDialogFragment : DialogFragment() {
+ internal val message: Int by lazy { safeArguments.getInt(KEY_MESSAGE) }
+ val safeArguments get() = requireNotNull(arguments)
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(requireContext())
+ .setMessage(message)
+ .setCancelable(true)
+ .setNegativeButton(R.string.mozac_support_base_permissions_needed_negative_button) { _, _ ->
+ dismiss()
+ }
+ .setPositiveButton(R.string.mozac_support_base_permissions_needed_positive_button) { _, _ ->
+ openSettingsPage()
+ }
+ return builder.create().withCenterAlignedButtons()
+ }
+
+ @VisibleForTesting
+ internal fun openSettingsPage() {
+ dismiss()
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ val uri = Uri.fromParts("package", requireContext().packageName, null)
+ intent.data = uri
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ requireContext().startActivity(intent)
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [DeniedPermissionDialogFragment]
+ * @param message the message of the dialog.
+ **/
+ fun newInstance(
+ @StringRes message: Int,
+ ): DeniedPermissionDialogFragment {
+ val fragment = DeniedPermissionDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putInt(KEY_MESSAGE, message)
+ }
+
+ fragment.arguments = arguments
+ return fragment
+ }
+
+ const val FRAGMENT_TAG = "DENIED_DOWNLOAD_PERMISSION_PROMPT_DIALOG"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/Context.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/Context.kt
new file mode 100644
index 0000000000..7e7b28c68c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/Context.kt
@@ -0,0 +1,56 @@
+/* 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.downloads.ext
+
+import android.annotation.TargetApi
+import android.app.DownloadManager
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import androidx.core.content.getSystemService
+
+/**
+ * Wraps around [DownloadManager.addCompletedDownload] and calls the correct
+ * method depending on the SDK version.
+ *
+ * Deprecated in Android Q, use MediaStore on that version.
+ */
+@TargetApi(Build.VERSION_CODES.P)
+@Suppress("Deprecation", "LongParameterList")
+internal fun Context.addCompletedDownload(
+ title: String,
+ description: String,
+ isMediaScannerScannable: Boolean,
+ mimeType: String,
+ path: String,
+ length: Long,
+ showNotification: Boolean,
+ uri: Uri?,
+ referer: Uri?,
+) = getSystemService<DownloadManager>()!!.run {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ addCompletedDownload(
+ title,
+ description,
+ isMediaScannerScannable,
+ mimeType,
+ path,
+ length,
+ showNotification,
+ uri,
+ referer,
+ )
+ } else {
+ addCompletedDownload(
+ title,
+ description,
+ isMediaScannerScannable,
+ mimeType,
+ path,
+ length,
+ showNotification,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt
new file mode 100644
index 0000000000..460b834dce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt
@@ -0,0 +1,52 @@
+/* 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.downloads.ext
+
+import androidx.core.net.toUri
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_DISPOSITION
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_LENGTH
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.support.ktx.kotlin.sanitizeFileName
+import mozilla.components.support.utils.DownloadUtils
+import java.io.InputStream
+import java.net.URLConnection
+
+internal fun DownloadState.isScheme(protocols: Iterable<String>): Boolean {
+ val scheme = url.trim().toUri().scheme ?: return false
+ return protocols.contains(scheme)
+}
+
+/**
+ * Returns a copy of the download with some fields filled in based on values from a response.
+ *
+ * @param headers Headers from the response.
+ * @param stream Stream of the response body.
+ */
+internal fun DownloadState.withResponse(headers: Headers, stream: InputStream?): DownloadState {
+ val contentDisposition = headers[CONTENT_DISPOSITION]
+ var contentType = this.contentType
+ if (contentType == null && stream != null) {
+ contentType = URLConnection.guessContentTypeFromStream(stream)
+ }
+ if (contentType == null) {
+ contentType = headers[CONTENT_TYPE]
+ }
+
+ val newFileName = if (fileName.isNullOrBlank()) {
+ DownloadUtils.guessFileName(contentDisposition, destinationDirectory, url, contentType)
+ } else {
+ fileName
+ }
+ return copy(
+ fileName = newFileName?.sanitizeFileName(),
+ contentType = contentType,
+ contentLength = contentLength ?: headers[CONTENT_LENGTH]?.toLongOrNull(),
+ )
+}
+
+internal val DownloadState.realFilenameOrGuessed
+ get() = fileName ?: DownloadUtils.guessFileName(null, destinationDirectory, url, contentType)
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/facts/DownloadsFacts.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/facts/DownloadsFacts.kt
new file mode 100644
index 0000000000..cdbf443a8d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/facts/DownloadsFacts.kt
@@ -0,0 +1,43 @@
+/* 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.downloads.facts
+
+import mozilla.components.feature.downloads.facts.DownloadsFacts.Items.PROMPT
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [DownloadsFeature]
+ */
+class DownloadsFacts {
+ /**
+ * Items that specify which portion of the [DownloadsFeature] was interacted with
+ */
+ object Items {
+ const val NOTIFICATION = "notification"
+ const val PROMPT = "prompt"
+ }
+}
+
+internal fun emitNotificationResumeFact() = emitFact(Action.RESUME)
+internal fun emitNotificationPauseFact() = emitFact(Action.PAUSE)
+internal fun emitNotificationCancelFact() = emitFact(Action.CANCEL)
+internal fun emitNotificationTryAgainFact() = emitFact(Action.TRY_AGAIN)
+internal fun emitNotificationOpenFact() = emitFact(Action.OPEN)
+internal fun emitPromptDisplayedFact() = emitFact(Action.DISPLAY, item = PROMPT)
+internal fun emitPromptDismissedFact() = emitFact(Action.CANCEL, item = PROMPT)
+
+private fun emitFact(
+ action: Action,
+ item: String = DownloadsFacts.Items.NOTIFICATION,
+) {
+ Fact(
+ Component.FEATURE_DOWNLOADS,
+ action,
+ item,
+ ).collect()
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/AndroidDownloadManager.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/AndroidDownloadManager.kt
new file mode 100644
index 0000000000..173409553d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/AndroidDownloadManager.kt
@@ -0,0 +1,165 @@
+/* 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.downloads.manager
+
+import android.Manifest.permission.INTERNET
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
+import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
+import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.util.LongSparseArray
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
+import androidx.core.net.toUri
+import androidx.core.util.set
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Headers.Names.COOKIE
+import mozilla.components.concept.fetch.Headers.Names.REFERRER
+import mozilla.components.concept.fetch.Headers.Names.USER_AGENT
+import mozilla.components.feature.downloads.AbstractFetchDownloadService
+import mozilla.components.feature.downloads.ext.isScheme
+import mozilla.components.support.utils.DownloadUtils
+import mozilla.components.support.utils.ext.getSerializableExtraCompat
+import mozilla.components.support.utils.ext.registerReceiverCompat
+
+typealias SystemDownloadManager = android.app.DownloadManager
+typealias SystemRequest = android.app.DownloadManager.Request
+
+/**
+ * Handles the interactions with the [AndroidDownloadManager].
+ *
+ * @property applicationContext a reference to [Context] applicationContext.
+ */
+class AndroidDownloadManager(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ override var onDownloadStopped: onDownloadStopped = noop,
+) : BroadcastReceiver(), DownloadManager {
+
+ private val downloadRequests = LongSparseArray<SystemRequest>()
+ private var isSubscribedReceiver = false
+
+ // Do not require WRITE_EXTERNAL_STORAGE permission on API 29 and above (using scoped storage)
+ override val permissions
+ get() = if (getSDKVersion() >= Build.VERSION_CODES.Q) {
+ arrayOf(INTERNET)
+ } else {
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)
+ }
+
+ @VisibleForTesting
+ internal fun getSDKVersion() = SDK_INT
+
+ /**
+ * Schedules a download through the [AndroidDownloadManager].
+ * @param download metadata related to the download.
+ * @param cookie any additional cookie to add as part of the download request.
+ * @return the id reference of the scheduled download.
+ */
+ override fun download(download: DownloadState, cookie: String): String? {
+ val androidDownloadManager: SystemDownloadManager = applicationContext.getSystemService()!!
+
+ if (!download.isScheme(listOf("http", "https"))) {
+ // We are ignoring everything that is not http or https. This is a limitation of
+ // Android's download manager. There's no reason to show a download dialog for
+ // something we can't download anyways.
+ return null
+ }
+
+ validatePermissionGranted(applicationContext)
+
+ val request = download.toAndroidRequest(cookie)
+ val downloadID = androidDownloadManager.enqueue(request)
+ store.dispatch(DownloadAction.AddDownloadAction(download.copy(id = downloadID.toString())))
+ downloadRequests[downloadID] = request
+ registerBroadcastReceiver()
+ return downloadID.toString()
+ }
+
+ override fun tryAgain(downloadId: String) {
+ val androidDownloadManager: SystemDownloadManager = applicationContext.getSystemService()!!
+ androidDownloadManager.enqueue(downloadRequests[downloadId.toLong()])
+ }
+
+ /**
+ * Remove all the listeners.
+ */
+ override fun unregisterListeners() {
+ if (isSubscribedReceiver) {
+ applicationContext.unregisterReceiver(this)
+ isSubscribedReceiver = false
+ downloadRequests.clear()
+ }
+ }
+
+ private fun registerBroadcastReceiver() {
+ if (!isSubscribedReceiver) {
+ val filter = IntentFilter(ACTION_DOWNLOAD_COMPLETE)
+
+ applicationContext.registerReceiverCompat(
+ this,
+ filter,
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+
+ isSubscribedReceiver = true
+ }
+ }
+
+ /**
+ * Invoked when a download is complete. Notifies [onDownloadStopped] and removes the queued
+ * download if it's complete.
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ val downloadID = intent.getStringExtra(EXTRA_DOWNLOAD_ID) ?: ""
+ val download = store.state.downloads[downloadID]
+ val downloadStatus =
+ intent.getSerializableExtraCompat(AbstractFetchDownloadService.EXTRA_DOWNLOAD_STATUS, Status::class.java)
+ as Status
+
+ if (download != null) {
+ onDownloadStopped(download, downloadID, downloadStatus)
+ }
+ }
+}
+
+private fun DownloadState.toAndroidRequest(cookie: String): SystemRequest {
+ val request = SystemRequest(url.toUri())
+ .setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
+
+ if (!contentType.isNullOrEmpty()) {
+ request.setMimeType(contentType)
+ }
+
+ with(request) {
+ addRequestHeaderSafely(USER_AGENT, userAgent)
+ addRequestHeaderSafely(COOKIE, cookie)
+ addRequestHeaderSafely(REFERRER, referrerUrl)
+ }
+
+ val fileName = if (fileName.isNullOrBlank()) {
+ DownloadUtils.guessFileName(null, destinationDirectory, url, contentType)
+ } else {
+ fileName
+ }
+ request.setDestinationInExternalPublicDir(destinationDirectory, fileName)
+
+ return request
+}
+
+internal fun SystemRequest.addRequestHeaderSafely(name: String, value: String?) {
+ if (value.isNullOrEmpty()) return
+ addRequestHeader(name, value)
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/DownloadManager.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/DownloadManager.kt
new file mode 100644
index 0000000000..0bd71e11c6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/DownloadManager.kt
@@ -0,0 +1,48 @@
+/* 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.downloads.manager
+
+import android.content.Context
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+
+typealias onDownloadStopped = (DownloadState, String, Status) -> Unit
+
+interface DownloadManager {
+
+ val permissions: Array<String>
+
+ var onDownloadStopped: onDownloadStopped
+
+ /**
+ * Schedules a download through the [DownloadManager].
+ * @param download metadata related to the download.
+ * @param cookie any additional cookie to add as part of the download request.
+ * @return the id reference of the scheduled download.
+ */
+ fun download(
+ download: DownloadState,
+ cookie: String = "",
+ ): String?
+
+ /**
+ * Schedules another attempt at downloading the given download.
+ * @param downloadId the id of the previously attempted download
+ */
+ fun tryAgain(
+ downloadId: String,
+ )
+
+ fun unregisterListeners() = Unit
+}
+
+fun DownloadManager.validatePermissionGranted(context: Context) {
+ if (!context.isPermissionGranted(permissions.asIterable())) {
+ throw SecurityException("You must be granted ${permissions.joinToString()}")
+ }
+}
+
+internal val noop: onDownloadStopped = { _, _, _ -> }
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/FetchDownloadManager.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/FetchDownloadManager.kt
new file mode 100644
index 0000000000..dc8266c003
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/FetchDownloadManager.kt
@@ -0,0 +1,137 @@
+/* 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.downloads.manager
+
+import android.Manifest.permission.FOREGROUND_SERVICE
+import android.Manifest.permission.INTERNET
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.annotation.SuppressLint
+import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
+import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.P
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.downloads.AbstractFetchDownloadService
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.EXTRA_DOWNLOAD_STATUS
+import mozilla.components.feature.downloads.ext.isScheme
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.utils.ext.getSerializableExtraCompat
+import mozilla.components.support.utils.ext.registerReceiverCompat
+import kotlin.reflect.KClass
+
+/**
+ * Handles the interactions with [AbstractFetchDownloadService].
+ *
+ * @property applicationContext a reference to [Context] applicationContext.
+ * @property service The subclass of [AbstractFetchDownloadService] to use.
+ */
+class FetchDownloadManager<T : AbstractFetchDownloadService>(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val service: KClass<T>,
+ override var onDownloadStopped: onDownloadStopped = noop,
+ private val notificationsDelegate: NotificationsDelegate,
+) : BroadcastReceiver(), DownloadManager {
+
+ private var isSubscribedReceiver = false
+
+ // Do not require WRITE_EXTERNAL_STORAGE permission on API 29 and above (using scoped storage)
+ override val permissions
+ @SuppressLint("InlinedApi")
+ get() = if (getSDKVersion() >= Build.VERSION_CODES.Q) {
+ arrayOf(INTERNET, FOREGROUND_SERVICE)
+ } else if (getSDKVersion() >= P) {
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE, FOREGROUND_SERVICE)
+ } else {
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)
+ }
+
+ @VisibleForTesting
+ internal fun getSDKVersion() = SDK_INT
+
+ /**
+ * Schedules a download through the [AbstractFetchDownloadService].
+ * @param download metadata related to the download.
+ * @param cookie any additional cookie to add as part of the download request.
+ * @return the id reference of the scheduled download.
+ */
+ override fun download(download: DownloadState, cookie: String): String? {
+ if (!download.isScheme(listOf("http", "https", "data", "blob", "moz-extension"))) {
+ return null
+ }
+ validatePermissionGranted(applicationContext)
+
+ if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ notificationsDelegate.requestNotificationPermission()
+ }
+
+ // The middleware will notify the service to start the download
+ // once this action is processed.
+ store.dispatch(DownloadAction.AddDownloadAction(download))
+
+ registerBroadcastReceiver()
+ return download.id
+ }
+
+ override fun tryAgain(downloadId: String) {
+ val download = store.state.downloads[downloadId] ?: return
+
+ val intent = Intent(applicationContext, service.java)
+ intent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+ intent.action = AbstractFetchDownloadService.ACTION_TRY_AGAIN
+ applicationContext.startService(intent)
+
+ registerBroadcastReceiver()
+ }
+
+ /**
+ * Remove all the listeners.
+ */
+ override fun unregisterListeners() {
+ if (isSubscribedReceiver) {
+ applicationContext.unregisterReceiver(this)
+ isSubscribedReceiver = false
+ }
+ }
+
+ private fun registerBroadcastReceiver() {
+ if (!isSubscribedReceiver) {
+ val filter = IntentFilter(ACTION_DOWNLOAD_COMPLETE)
+
+ applicationContext.registerReceiverCompat(
+ this,
+ filter,
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+
+ isSubscribedReceiver = true
+ }
+ }
+
+ /**
+ * Invoked when a download is complete. Notifies [onDownloadStopped] and removes the queued
+ * download if it's complete.
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ val downloadID = intent.getStringExtra(EXTRA_DOWNLOAD_ID) ?: ""
+ val download = store.state.downloads[downloadID]
+ val downloadStatus = intent.getSerializableExtraCompat(EXTRA_DOWNLOAD_STATUS, Status::class.java)
+ as Status?
+
+ if (download != null && downloadStatus != null) {
+ onDownloadStopped(download, downloadID, downloadStatus)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/provider/FileProvider.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/provider/FileProvider.kt
new file mode 100644
index 0000000000..9aa89519a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/provider/FileProvider.kt
@@ -0,0 +1,18 @@
+/* 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.downloads.provider
+
+/**
+ * A file provider to provide functionality for the feature downloads component.
+ *
+ * We need this class to create a fully qualified class name that doesn't clash with other
+ * file providers in other components see https://stackoverflow.com/a/43444164/5533820.
+ *
+ * Be aware, when creating new file resources avoid using common names like "@xml/file_paths",
+ * as other file providers could be using the same names and this could case unexpected behaviors.
+ * As a convention try to use unique names like using the name of the component as a prefix of the
+ * name of the file, like component_xxx_file_paths.xml.
+ */
+/** @suppress */
+class FileProvider : androidx.core.content.FileProvider()
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeature.kt
new file mode 100644
index 0000000000..d02e36c307
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeature.kt
@@ -0,0 +1,98 @@
+/* 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.downloads.temporary
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.CopyInternetResourceAction
+import mozilla.components.browser.state.action.ShareInternetResourceAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Client
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.android.content.copyImage
+import java.util.concurrent.TimeUnit
+
+/**
+ * [LifecycleAwareFeature] implementation for copying online resources.
+ *
+ * This will intercept only [ShareInternetResourceAction] [BrowserAction]s.
+ *
+ * Following which it will transparently
+ * - download internet resources while respecting the private mode related to cookies handling
+ * - temporarily cache the downloaded resources
+ * - copy the resource to the device clipboard.
+ *
+ * with a 1 second timeout to ensure a smooth UX.
+ *
+ * To finish the process in this small timeframe the feature is recommended to be used only for images.
+ *
+ * @property context Android context used for various platform interactions.
+ * @property store a reference to the application's [BrowserStore].
+ * @property tabId ID of the tab session, or null if the selected session should be used.
+ * @property onCopyConfirmation The confirmation action of copying an image.
+ * @param httpClient Client used for downloading internet resources.
+ * @param cleanupCacheCoroutineDispatcher Coroutine dispatcher used for the cleanup of old
+ * cached files. Defaults to IO.
+ */
+class CopyDownloadFeature(
+ private val context: Context,
+ private val store: BrowserStore,
+ private val tabId: String?,
+ private val onCopyConfirmation: () -> Unit,
+ httpClient: Client,
+ cleanupCacheCoroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : TemporaryDownloadFeature(
+ context,
+ httpClient,
+ cleanupCacheCoroutineDispatcher,
+) {
+
+ /**
+ * At most time to allow for the file to be downloaded.
+ */
+ private val operationTimeoutMs by lazy { TimeUnit.MINUTES.toMinutes(1) }
+
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .distinctUntilChangedBy { it.content.copy }
+ .collect { state ->
+ state.content.copy?.let { copyState ->
+ logger.debug("Starting the copying process")
+ startCopy(copyState)
+
+ // This is a fire and forget action, not something that we want lingering the tab state.
+ store.dispatch(CopyInternetResourceAction.ConsumeCopyAction(state.id))
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun startCopy(internetResource: ShareInternetResourceState) {
+ val coroutineExceptionHandler = coroutineExceptionHandler("Copy")
+
+ scope?.launch(coroutineExceptionHandler) {
+ withTimeout(operationTimeoutMs) {
+ val download = download(internetResource)
+ copy(download.canonicalPath, onCopyConfirmation)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun copy(filePath: String, onCopyConfirmation: () -> Unit) =
+ context.copyImage(filePath, onCopyConfirmation)
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeature.kt
new file mode 100644
index 0000000000..ac6b0023b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeature.kt
@@ -0,0 +1,97 @@
+/* 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.downloads.temporary
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ShareInternetResourceAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Client
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.android.content.shareMedia
+
+/**
+ * At most time to allow for the file to be downloaded and action to be performed.
+ */
+private const val OPERATION_TIMEOUT_MS: Long = 1000L
+
+/**
+ * [LifecycleAwareFeature] implementation for sharing online resources.
+ *
+ * This will intercept only [ShareInternetResourceAction] [BrowserAction]s.
+ *
+ * Following which it will transparently
+ * - download internet resources while respecting the private mode related to cookies handling
+ * - temporarily cache the downloaded resources
+ * - automatically open the platform app chooser to share the cached files with other installed Android apps.
+ *
+ * with a 1 second timeout to ensure a smooth UX.
+ *
+ * To finish the process in this small timeframe the feature is recommended to be used only for images.
+ *
+ * @property context Android context used for various platform interactions
+ * @property store a reference to the application's [BrowserStore]
+ * @property tabId ID of the tab session, or null if the selected session should be used.
+ * @param httpClient Client used for downloading internet resources
+ * @param cleanupCacheCoroutineDispatcher Coroutine dispatcher used for the cleanup of old
+ * cached files. Defaults to IO.
+ */
+class ShareDownloadFeature(
+ private val context: Context,
+ private val store: BrowserStore,
+ private val tabId: String?,
+ httpClient: Client,
+ cleanupCacheCoroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : TemporaryDownloadFeature(context, httpClient, cleanupCacheCoroutineDispatcher) {
+
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .distinctUntilChangedBy { it.content.share }
+ .collect { state ->
+ state.content.share?.let { shareState ->
+ logger.debug("Starting the sharing process")
+ startSharing(shareState)
+
+ // This is a fire and forget action, not something that we want lingering the tab state.
+ store.dispatch(ShareInternetResourceAction.ConsumeShareAction(state.id))
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun startSharing(internetResource: ShareInternetResourceState) {
+ val coroutineExceptionHandler = coroutineExceptionHandler("Share")
+
+ scope?.launch(coroutineExceptionHandler) {
+ withTimeout(OPERATION_TIMEOUT_MS) {
+ val download = download(internetResource)
+ share(
+ contentType = internetResource.contentType,
+ filePath = download.canonicalPath,
+ )
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun share(
+ filePath: String,
+ contentType: String?,
+ subject: String? = null,
+ message: String? = null,
+ ) = context.shareMedia(filePath, contentType, subject, message)
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/TemporaryDownloadFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/TemporaryDownloadFeature.kt
new file mode 100644
index 0000000000..3a4119315c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/TemporaryDownloadFeature.kt
@@ -0,0 +1,156 @@
+/* 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.downloads.temporary
+
+import android.content.Context
+import android.webkit.MimeTypeMap
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.ifNullOrEmpty
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.net.URLConnection
+import kotlin.math.absoluteValue
+import kotlin.random.Random
+
+/**
+ * Default mime type for base64 images data URLs not containing the media type.
+ */
+@VisibleForTesting
+internal const val DEFAULT_IMAGE_EXTENSION = "jpg"
+
+/**
+ * Subdirectory of Context.getCacheDir() where the resources to be shared are stored.
+ *
+ * Location must be kept in sync with the paths our FileProvider can share from.
+ */
+@VisibleForTesting
+internal var cacheDirName = "mozac_share_cache"
+
+/**
+ * Base class for downloading resources from the internet and storing them in a temporary cache.
+ *
+ * @property context Android context used for various platform interactions.
+ * @property httpClient Client used for downloading internet resources.
+ * @param cleanupCacheCoroutineDispatcher Coroutine dispatcher used for the cleanup of old
+ * cached files. Defaults to IO.
+ */
+abstract class TemporaryDownloadFeature(
+ private val context: Context,
+ private val httpClient: Client,
+ cleanupCacheCoroutineDispatcher: CoroutineDispatcher = IO,
+) : LifecycleAwareFeature {
+
+ val logger = Logger("TemporaryDownloadFeature")
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ var scope: CoroutineScope? = null
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ init {
+ CoroutineScope(cleanupCacheCoroutineDispatcher).launch {
+ cleanupCache()
+ }
+ }
+
+ @WorkerThread
+ @VisibleForTesting
+ internal fun download(internetResource: ShareInternetResourceState): File {
+ val request = Request(
+ internetResource.url.sanitizeURL(),
+ private = internetResource.private,
+ referrerUrl = internetResource.referrerUrl,
+ )
+ val response = if (internetResource.response == null) {
+ httpClient.fetch(request)
+ } else {
+ requireNotNull(internetResource.response)
+ }
+
+ if (response.status != Response.SUCCESS) {
+ response.close()
+ // We experienced a problem trying to fetch the file, nothing more we can do.
+ throw (RuntimeException("Resource is not available to download"))
+ }
+
+ var tempFile: File? = null
+ response.body.useStream { input ->
+ val fileExtension = '.' + getFileExtension(response.headers, input)
+ tempFile = getTempFile(fileExtension)
+ FileOutputStream(tempFile).use { output -> input.copyTo(output) }
+ }
+
+ return tempFile!!
+ }
+
+ @VisibleForTesting
+ internal fun getFilename(fileExtension: String) =
+ Random.nextInt().absoluteValue.toString() + fileExtension
+
+ @VisibleForTesting
+ internal fun getTempFile(fileExtension: String) =
+ File(getMediaShareCacheDirectory(), getFilename(fileExtension))
+
+ @VisibleForTesting
+ internal fun getCacheDirectory() = File(context.cacheDir, cacheDirName)
+
+ @VisibleForTesting
+ internal fun getFileExtension(responseHeaders: Headers, responseStream: InputStream): String {
+ val mimeType = URLConnection.guessContentTypeFromStream(responseStream) ?: responseHeaders[CONTENT_TYPE]
+
+ return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType).ifNullOrEmpty { DEFAULT_IMAGE_EXTENSION }
+ }
+
+ @VisibleForTesting
+ internal fun getMediaShareCacheDirectory(): File {
+ val mediaShareCacheDir = getCacheDirectory()
+ if (!mediaShareCacheDir.exists()) {
+ mediaShareCacheDir.mkdirs()
+ }
+ return mediaShareCacheDir
+ }
+
+ @VisibleForTesting
+ internal fun cleanupCache() {
+ logger.debug("Deleting previous cache of shared files")
+ getCacheDirectory().listFiles()?.forEach { it.delete() }
+ }
+
+ protected fun coroutineExceptionHandler(action: String) =
+ CoroutineExceptionHandler { _, throwable ->
+ when (throwable) {
+ is InterruptedException -> {
+ logger.warn("$action failed: operation timeout reached")
+ }
+
+ is IOException,
+ is RuntimeException,
+ is NullPointerException,
+ -> {
+ logger.warn("$action failed: $throwable")
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialog.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialog.kt
new file mode 100644
index 0000000000..73cad1e2e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialog.kt
@@ -0,0 +1,136 @@
+/* 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.downloads.ui
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.LinearLayout
+import androidx.appcompat.app.AppCompatDialogFragment
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.downloads.R
+import mozilla.components.support.utils.ext.getParcelableArrayListCompat
+import java.util.ArrayList
+
+/**
+ * A dialog where an user can select with which app a download must be performed.
+ */
+internal class DownloadAppChooserDialog : AppCompatDialogFragment() {
+ private val safeArguments get() = requireNotNull(arguments)
+ internal val appsList: ArrayList<DownloaderApp>
+ get() =
+ safeArguments.getParcelableArrayListCompat(KEY_APP_LIST, DownloaderApp::class.java)
+ ?: arrayListOf()
+
+ internal val dialogGravity: Int get() =
+ safeArguments.getInt(KEY_DIALOG_GRAVITY, DEFAULT_VALUE)
+ internal val dialogShouldWidthMatchParent: Boolean get() =
+ safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT)
+
+ /**
+ * Indicates the user has selected an application to perform the download
+ */
+ internal var onAppSelected: ((DownloaderApp) -> Unit) = {}
+
+ internal var onDismiss: () -> Unit = {}
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val sheetDialog = Dialog(requireContext())
+ sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ sheetDialog.setCanceledOnTouchOutside(false)
+
+ val rootView = createContainer()
+ sheetDialog.setContainerView(rootView)
+ sheetDialog.window?.apply {
+ if (dialogGravity != DEFAULT_VALUE) {
+ setGravity(dialogGravity)
+ }
+
+ if (dialogShouldWidthMatchParent) {
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ // This must be called after addContentView, or it won't fully fill to the edge.
+ setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+ }
+ return sheetDialog
+ }
+
+ @SuppressLint("InflateParams")
+ private fun createContainer(): View {
+ val rootView = LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_downloader_chooser_prompt,
+ null,
+ false,
+ )
+
+ val recyclerView = rootView.findViewById<RecyclerView>(R.id.apps_list)
+ recyclerView.adapter = DownloaderAppAdapter(rootView.context, appsList) { app ->
+ onAppSelected(app)
+ dismiss()
+ }
+
+ rootView.findViewById<AppCompatImageButton>(R.id.close_button).setOnClickListener {
+ dismiss()
+ onDismiss()
+ }
+
+ return rootView
+ }
+
+ fun setApps(apps: List<DownloaderApp>) {
+ val args = arguments ?: Bundle()
+ args.putParcelableArrayList(KEY_APP_LIST, ArrayList(apps))
+ arguments = args
+ }
+
+ private fun Dialog.setContainerView(rootView: View) {
+ if (dialogShouldWidthMatchParent) {
+ setContentView(rootView)
+ } else {
+ addContentView(
+ rootView,
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ ),
+ )
+ }
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [DownloadAppChooserDialog]
+ */
+ fun newInstance(
+ gravity: Int? = DEFAULT_VALUE,
+ dialogShouldWidthMatchParent: Boolean? = false,
+ ): DownloadAppChooserDialog {
+ val fragment = DownloadAppChooserDialog()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ gravity?.let { putInt(KEY_DIALOG_GRAVITY, it) }
+ dialogShouldWidthMatchParent?.let { putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, it) }
+ }
+
+ fragment.arguments = arguments
+
+ return fragment
+ }
+
+ private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY"
+ private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT"
+ private const val DEFAULT_VALUE = Int.MAX_VALUE
+
+ private const val KEY_APP_LIST = "KEY_APP_LIST"
+ internal const val FRAGMENT_TAG = "SHOULD_APP_DOWNLOAD_PROMPT_DIALOG"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt
new file mode 100644
index 0000000000..75abeef01f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt
@@ -0,0 +1,219 @@
+/* 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.downloads.ui
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.GradientDrawable
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.LinearLayout
+import androidx.annotation.ColorRes
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AppCompatDialogFragment
+import androidx.core.content.ContextCompat
+import kotlinx.parcelize.Parcelize
+import mozilla.components.feature.downloads.R
+import mozilla.components.feature.downloads.databinding.MozacDownloadCancelBinding
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+/**
+ * The dialog warns the user that closing last private tab leads to cancellation of active private
+ * downloads.
+ */
+class DownloadCancelDialogFragment : AppCompatDialogFragment() {
+
+ var onAcceptClicked: ((tabId: String?, source: String?) -> Unit)? = null
+ var onDenyClicked: (() -> Unit)? = null
+
+ private val safeArguments get() = requireNotNull(arguments)
+ private val downloadCount by lazy { safeArguments.getInt(KEY_DOWNLOAD_COUNT) }
+ private val tabId by lazy { safeArguments.getString(KEY_TAB_ID) }
+ private val source by lazy { safeArguments.getString(KEY_SOURCE) }
+ private val promptStyling by lazy {
+ safeArguments.getParcelableCompat(KEY_STYLE, PromptStyling::class.java) ?: PromptStyling()
+ }
+ private val promptText by lazy {
+ safeArguments.getParcelableCompat(KEY_TEXT, PromptText::class.java) ?: PromptText()
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return Dialog(requireContext()).apply {
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+ setCanceledOnTouchOutside(true)
+
+ setContainerView(promptStyling.shouldWidthMatchParent, createContainer())
+
+ window?.apply {
+ setGravity(promptStyling.gravity)
+
+ if (promptStyling.shouldWidthMatchParent) {
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ // This must be called after addContentView, or it won't fully fill to the edge.
+ setLayout(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ )
+ }
+ }
+ }
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ super.onDismiss(dialog)
+ onDenyClicked?.invoke()
+ }
+
+ @Suppress("NestedBlockDepth")
+ private fun Dialog.setContainerView(dialogShouldWidthMatchParent: Boolean, rootView: View) {
+ if (dialogShouldWidthMatchParent) {
+ setContentView(rootView)
+ } else {
+ addContentView(
+ rootView,
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ ),
+ )
+ }
+ }
+
+ @Suppress("InflateParams", "NestedBlockDepth")
+ private fun createContainer() = LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_download_cancel,
+ null,
+ false,
+ ).apply {
+ with(MozacDownloadCancelBinding.bind(this)) {
+ acceptButton.setOnClickListener {
+ onAcceptClicked?.invoke(tabId, source)
+ dismiss()
+ }
+
+ denyButton.setOnClickListener {
+ onDenyClicked?.invoke()
+ dismiss()
+ }
+
+ with(promptText) {
+ title.text = getString(titleText)
+ body.text = buildWarningText(downloadCount, bodyText)
+ acceptButton.text = getString(acceptText)
+ denyButton.text = getString(denyText)
+ }
+
+ with(promptStyling) {
+ positiveButtonBackgroundColor?.let {
+ val backgroundTintList = ContextCompat.getColorStateList(requireContext(), it)
+ acceptButton.backgroundTintList = backgroundTintList
+
+ // It appears there is not guaranteed way to get background color of a button,
+ // there are always nullable types, hence the code changing the positiveButtonRadius
+ // executes only if positiveButtonBackgroundColor is provided
+ positiveButtonRadius?.let {
+ val shape = GradientDrawable()
+ shape.shape = GradientDrawable.RECTANGLE
+ shape.setColor(
+ ContextCompat.getColor(
+ requireContext(),
+ positiveButtonBackgroundColor,
+ ),
+ )
+ shape.cornerRadius = positiveButtonRadius
+ acceptButton.background = shape
+ }
+ }
+
+ positiveButtonTextColor?.let {
+ val color = ContextCompat.getColor(requireContext(), it)
+ acceptButton.setTextColor(color)
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun buildWarningText(downloadCount: Int, @StringRes stringId: Int) = String.format(
+ getString(stringId),
+ downloadCount,
+ )
+
+ companion object {
+ private const val KEY_DOWNLOAD_COUNT = "KEY_DOWNLOAD_COUNT"
+ private const val KEY_TAB_ID = "KEY_TAB_ID"
+ private const val KEY_SOURCE = "KEY_SOURCE"
+ private const val KEY_STYLE = "KEY_STYLE"
+ private const val KEY_TEXT = "KEY_TEXT"
+
+ /**
+ * Returns a new instance of [DownloadCancelDialogFragment].
+ * @param downloadCount The number of currently active downloads.
+ * @param promptStyling Styling properties for the dialog.
+ * @param onPositiveButtonClicked A lambda called when the allow button is clicked.
+ * @param onNegativeButtonClicked A lambda called when the deny button is clicked.
+ */
+ fun newInstance(
+ downloadCount: Int,
+ tabId: String? = null,
+ source: String? = null,
+ promptText: PromptText? = null,
+ promptStyling: PromptStyling? = null,
+ onPositiveButtonClicked: ((tabId: String?, source: String?) -> Unit)? = null,
+ onNegativeButtonClicked: (() -> Unit)? = null,
+ ): DownloadCancelDialogFragment {
+ return DownloadCancelDialogFragment().apply {
+ this.arguments = Bundle().apply {
+ putInt(KEY_DOWNLOAD_COUNT, downloadCount)
+ tabId?.let { putString(KEY_TAB_ID, it) }
+ source?.let { putString(KEY_SOURCE, it) }
+ promptText?.let { putParcelable(KEY_TEXT, it) }
+ promptStyling?.let { putParcelable(KEY_STYLE, it) }
+ }
+ this.onAcceptClicked = onPositiveButtonClicked
+ this.onDenyClicked = onNegativeButtonClicked
+ }
+ }
+ }
+
+ /**
+ * Styling for the downloads cancellation dialog.
+ * Note that for [positiveButtonRadius] to be applied,
+ * specifying [positiveButtonBackgroundColor] is necessary.
+ */
+ @Parcelize
+ data class PromptStyling(
+ val gravity: Int = Gravity.BOTTOM,
+ val shouldWidthMatchParent: Boolean = true,
+ @ColorRes
+ val positiveButtonBackgroundColor: Int? = null,
+ @ColorRes
+ val positiveButtonTextColor: Int? = null,
+ val positiveButtonRadius: Float? = null,
+ ) : Parcelable
+
+ /**
+ * The class gives an option to override string resources used by [DownloadCancelDialogFragment].
+ */
+ @Parcelize
+ data class PromptText(
+ @StringRes
+ val titleText: Int = R.string.mozac_feature_downloads_cancel_active_downloads_warning_content_title,
+ @StringRes
+ val bodyText: Int = R.string.mozac_feature_downloads_cancel_active_private_downloads_warning_content_body,
+ @StringRes
+ val acceptText: Int = R.string.mozac_feature_downloads_cancel_active_downloads_accept,
+ @StringRes
+ val denyText: Int = R.string.mozac_feature_downloads_cancel_active_private_downloads_deny,
+ ) : Parcelable
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderApp.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderApp.kt
new file mode 100644
index 0000000000..2871429ee4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderApp.kt
@@ -0,0 +1,31 @@
+/* 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.downloads.ui
+
+import android.annotation.SuppressLint
+import android.content.pm.ResolveInfo
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Represents an app that can perform downloads.
+ *
+ * @property name Name of the app.
+ * @property resolver The [ResolveInfo] for this app.
+ * @property packageName Package of the app.
+ * @property activityName Activity that will be shared to.
+ * @property url The full url to the content that should be downloaded.
+ * @property contentType Content type (MIME type) to indicate the media type of the download.
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class DownloaderApp(
+ val name: String,
+ val resolver: ResolveInfo,
+ val packageName: String,
+ val activityName: String,
+ val url: String,
+ val contentType: String?,
+) : Parcelable
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt
new file mode 100644
index 0000000000..5be32f71b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt
@@ -0,0 +1,72 @@
+/* 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.downloads.ui
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.downloads.R
+
+/**
+ * An adapter for displaying the applications that can perform downloads.
+ */
+class DownloaderAppAdapter(
+ context: Context,
+ private val apps: List<DownloaderApp>,
+ val onAppSelected: ((DownloaderApp) -> Unit),
+) : RecyclerView.Adapter<DownloaderAppViewHolder>() {
+
+ private val inflater = LayoutInflater.from(context)
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloaderAppViewHolder {
+ val view = inflater.inflate(R.layout.mozac_download_app_list_item, parent, false)
+
+ val nameLabel = view.findViewById<TextView>(R.id.app_name)
+ val iconImage = view.findViewById<ImageView>(R.id.app_icon)
+
+ return DownloaderAppViewHolder(view, nameLabel, iconImage)
+ }
+
+ override fun getItemCount(): Int = apps.size
+
+ override fun onBindViewHolder(holder: DownloaderAppViewHolder, position: Int) {
+ val app = apps[position]
+ val context = holder.itemView.context
+ with(app) {
+ holder.nameLabel.text = name
+ holder.iconImage.setImageDrawable(app.resolver.loadIcon(context.packageManager))
+ holder.bind(app, onAppSelected)
+ }
+ }
+}
+
+/**
+ * View holder for a [DownloaderApp] item.
+ */
+class DownloaderAppViewHolder(
+ itemView: View,
+ val nameLabel: TextView,
+ val iconImage: ImageView,
+) : RecyclerView.ViewHolder(itemView) {
+ /**
+ * Show a certain downloader application in the current View.
+ */
+ fun bind(app: DownloaderApp, onAppSelected: ((DownloaderApp) -> Unit)) {
+ itemView.app = app
+ itemView.setOnClickListener {
+ onAppSelected(it.app)
+ }
+ }
+
+ internal var View.app: DownloaderApp
+ get() = tag as DownloaderApp
+ set(value) {
+ tag = value
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download.xml
new file mode 100644
index 0000000000..f85368e457
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/>
+ <path
+ android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z"
+ android:fillColor="#FFF"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim0.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim0.xml
new file mode 100644
index 0000000000..fe77c7f45a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim0.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/>
+ <path
+ android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z"
+ android:fillColor="#3FFF"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim1.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim1.xml
new file mode 100644
index 0000000000..b14f73537b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim1.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/>
+ <path
+ android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z"
+ android:fillColor="#3FFF"/>
+ <path
+ android:name="animated_fill"
+ android:pathData="M 0 0 L 24 0 L 24 4 L 0 4 Z"
+ android:fillColor="#FFF"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim2.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim2.xml
new file mode 100644
index 0000000000..77353bc0b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim2.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/>
+ <path
+ android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z"
+ android:fillColor="#3FFF"/>
+ <path
+ android:name="animated_fill"
+ android:pathData="M 0 0 L 24 0 L 24 6 L 0 6 Z"
+ android:fillColor="#FFF"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim3.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim3.xml
new file mode 100644
index 0000000000..f326e0d24a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim3.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/>
+ <path
+ android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z"
+ android:fillColor="#3FFF"/>
+ <path
+ android:name="animated_fill"
+ android:pathData="M 0 0 L 24 0 L 24 8 L 0 8 Z"
+ android:fillColor="#FFF"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim4.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim4.xml
new file mode 100644
index 0000000000..0f9feaf19f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim4.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/>
+ <path
+ android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z"
+ android:fillColor="#3FFF"/>
+ <path
+ android:name="animated_fill"
+ android:pathData="M 0 0 L 24 0 L 24 10 L 0 10 Z"
+ android:fillColor="#FFF"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim5.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim5.xml
new file mode 100644
index 0000000000..8102a93b58
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim5.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/>
+ <path
+ android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z"
+ android:fillColor="#3FFF"/>
+ <path
+ android:name="animated_fill"
+ android:pathData="M 0 0 L 24 0 L 24 20 L 0 20 Z"
+ android:fillColor="#FFF"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_complete.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_complete.xml
new file mode 100644
index 0000000000..83225e9e42
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_complete.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="m6,20h12c0.5523,0 1,0.4477 1,1 0,0.5128 -0.386,0.9355 -0.8834,0.9933l-0.1166,0.0067h-12c-0.5523,0 -1,-0.4477 -1,-1 0,-0.5128 0.386,-0.9355 0.8834,-0.9933l0.1166,-0.0067h12zM18.7041,7.2899c0.3922,0.3889 0.3948,1.022 0.0059,1.4142l-8,8.0675c-0.3923,0.3956 -1.0324,0.3943 -1.4231,-0.003l-4,-4.0675c-0.3872,-0.3938 -0.382,-1.0269 0.0118,-1.4142 0.3938,-0.3872 1.0269,-0.382 1.4142,0.0118l3.29,3.3455 7.287,-7.3484c0.3889,-0.3922 1.022,-0.3948 1.4142,-0.0059z"
+ android:fillColor="#FFF"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_failed.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_failed.xml
new file mode 100644
index 0000000000..5d52532588
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_failed.xml
@@ -0,0 +1,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/. -->
+
+<vector android:height="24dp" android:viewportHeight="16"
+ android:viewportWidth="16" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FFF" android:pathData="M14.742,12.106L9.789,2.2a2,2 0,0 0,-3.578 0l-4.953,9.91A2,2 0,0 0,3.047 15h9.905a2,2 0,0 0,1.79 -2.894zM7,5a1,1 0,0 1,2 0v4a1,1 0,0 1,-2 0zM8,13.25A1.25,1.25 0,1 1,9.25 12,1.25 1.25,0 0,1 8,13.25z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_ongoing_download.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_ongoing_download.xml
new file mode 100644
index 0000000000..298fcac943
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_ongoing_download.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false">
+ <item android:drawable="@drawable/mozac_feature_download_ic_download_anim0" android:duration="200" />
+ <item android:drawable="@drawable/mozac_feature_download_ic_download_anim1" android:duration="200" />
+ <item android:drawable="@drawable/mozac_feature_download_ic_download_anim2" android:duration="200" />
+ <item android:drawable="@drawable/mozac_feature_download_ic_download_anim3" android:duration="200" />
+ <item android:drawable="@drawable/mozac_feature_download_ic_download_anim4" android:duration="200" />
+ <item android:drawable="@drawable/mozac_feature_download_ic_download_anim5" android:duration="200" />
+ <item android:drawable="@drawable/mozac_feature_download_ic_download" android:duration="200" />
+</animation-list>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_app_list_item.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_app_list_item.xml
new file mode 100644
index 0000000000..5233427097
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_app_list_item.xml
@@ -0,0 +1,44 @@
+<?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/. -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="76dp"
+ android:layout_height="80dp"
+ android:background="?selectableItemBackground"
+ tools:ignore="Overdraw">
+
+ <ImageView
+ android:id="@+id/app_icon"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginTop="8dp"
+ android:importantForAccessibility="no"
+ app:layout_constraintBottom_toTopOf="@id/app_name"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:srcCompat="@tools:sample/avatars" />
+
+ <TextView
+ android:id="@+id/app_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="5dp"
+ android:ellipsize="end"
+ android:gravity="center|top"
+ android:lines="2"
+ android:textAlignment="gravity"
+ android:textSize="12sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/app_icon"
+ tools:text="Copy to clipboard" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml
new file mode 100644
index 0000000000..4f034e5a70
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml
@@ -0,0 +1,78 @@
+<!-- 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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:windowBackground"
+ tools:ignore="Overdraw">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ app:srcCompat="@drawable/mozac_ic_information_24"
+ app:tint="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@+id/icon"
+ android:layout_alignBottom="@+id/icon"
+ android:layout_toEndOf="@id/icon"
+ android:gravity="center_vertical"
+ android:paddingStart="4dp"
+ android:paddingEnd="8dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ tools:text="@string/mozac_feature_downloads_cancel_active_downloads_warning_content_title"
+ tools:textColor="#000000" />
+
+ <TextView
+ android:id="@+id/body"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title"
+ android:layout_alignStart="@id/title"
+ android:layout_marginTop="8dp"
+ android:paddingStart="4dp"
+ android:paddingEnd="8dp"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="@string/mozac_feature_downloads_cancel_active_private_downloads_warning_content_body" />
+
+ <Button
+ android:id="@+id/deny_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/body"
+ android:layout_marginTop="16dp"
+ android:layout_toStartOf="@id/accept_button"
+ android:textAlignment="center"
+ android:textAllCaps="false"
+ tools:text="@string/mozac_feature_downloads_cancel_active_private_downloads_deny" />
+
+ <Button
+ android:id="@+id/accept_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/body"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:textAlignment="center"
+ android:textAllCaps="false"
+ tools:text="@string/mozac_feature_downloads_cancel_active_downloads_accept" />
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloader_chooser_prompt.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloader_chooser_prompt.xml
new file mode 100644
index 0000000000..2509ab8a6b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloader_chooser_prompt.xml
@@ -0,0 +1,75 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/relativeLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:windowBackground"
+ android:orientation="vertical"
+ tools:ignore="Overdraw">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_feature_download_ic_download"
+ app:tint="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:text="@string/mozac_feature_downloads_third_party_app_chooser_dialog_title"
+ android:textColor="?android:attr/textColorPrimary"
+ app:layout_constraintEnd_toStartOf="@+id/close_button"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toEndOf="@id/icon"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:textColor="#000000" />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="3dp"
+ android:layout_marginEnd="12dp"
+ android:layout_marginTop="16dp"
+ android:background="@null"
+ android:contentDescription="@string/mozac_feature_downloads_button_close"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_cross_24"
+ app:tint="?android:attr/textColorPrimary"
+ tools:textColor="#000000" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/apps_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:paddingBottom="16dp"
+ android:orientation="horizontal"
+ android:clipToPadding="false"
+ app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/icon"
+ tools:listitem="@layout/mozac_download_app_list_item" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloads_prompt.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloads_prompt.xml
new file mode 100644
index 0000000000..485533ce1b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloads_prompt.xml
@@ -0,0 +1,92 @@
+<?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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:background="?android:windowBackground"
+ android:orientation="vertical"
+ tools:ignore="Overdraw">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ app:srcCompat="@drawable/mozac_feature_download_ic_download"
+ app:tint="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="11dp"
+ android:layout_toStartOf="@id/close_button"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="Download (85.7 MB)"
+ tools:textColor="#000000" />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/close_button"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="3dp"
+ android:scaleType="centerInside"
+ android:background="@null"
+ android:contentDescription="@string/mozac_feature_downloads_button_close"
+ app:srcCompat="@drawable/mozac_ic_cross_24"
+ app:tint="?android:attr/textColorPrimary"
+ tools:textColor="#000000" />
+
+ <TextView
+ android:id="@+id/filename"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxHeight="160dp"
+ android:layout_below="@id/title"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="16dp"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:scrollbars="vertical"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="@tools:sample/lorem/random"
+ tools:textColor="#000000" />
+
+ <Button
+ android:id="@+id/download_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/filename"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:text="@string/mozac_feature_downloads_dialog_download"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..3fb72a4502
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-am/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">የወረዱ</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ማውረድ ባለበት ቆሟል</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ማውረድ ተጠናቅቋል</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ማውረድ አልተሳካም</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">(%1$s)ን አውርድ </string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">አውርድ</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ተወው</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ይህን የፋይል አይነት ማውረድ አይችልም</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ፋይል መክፈት አልተቻለም</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ፋይሎችን የሚከፍት ምንም መተግበሪያ አልተገኘም</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">ባለበት አቁም</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ካቆመበት ቀጥል</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ተወው</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ክፈት</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">እንደገና ሞክር</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">ዝጋ</string>
+
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">በመጠቀም አከናውን</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$sን መክፈት አልተቻለም</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ፋይሎችን ለማውረድ የፋይሎች እና የሚዲያ መዳረሻ ፍቃድ ያስፈልጋል። ወደ አንድሮይድ ቅንብሮች ይሂዱ፣ ፈቃዶችን ይንኩ እና ፍቀድን ይንኩ።</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">የግል ውርዶች ይሰረዙ?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ሁሉንም የግል ትሮችን አሁን ከዘጉ፣ %1$sን ማውረድ ይሰረዛል። እርግጠኛ ነዎት ከግል አሰሳ መውጣት ይፈልጋሉ?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ማውረዶችን አቋርጥ </string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">በግል አሰሳ ውስጥ ይቆዩ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..2d6bfdc42a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-an/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descargas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">S’ha pausau la descarga</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Ha rematau la descarga</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Ha fallau la descarga</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Descargar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar esta mena de fichero</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No s’ha puesto ubrir lo fichero</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No s’ha trobau garra aplicación pa ubrir los fichers %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausar</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Continar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ubrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Tornar-lo a prebar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zarrar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar l\'acción con</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se puede ubrir %1$s</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..76de9b97a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ar/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">التنزيلات</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">أُلبث التنزيل</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">اكتمل التنزيل</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">فشل التنزيل</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">نزّل (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">نزّل</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ألغِ</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">لا يقدر %1$s على تنزيل هذا النوع من الملفات</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">لا يمكن فتح الملف</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">لم يوجد أي تطبيق يفتح ملفات %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">ألبِث</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">استأنف</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ألغِ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">افتح</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">أعِد المحاولة</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">أغلِق</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">أكمل الإجراء باستخدام</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">تعذر فتح %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">تصريح الوصول إلى الملفات والوسائط مطلوب لتنزيل الملفات. انتقل إلى إعدادات أندرويد، وانقر ”الأذونات“ ثم انقر ”سماح“.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">أتريد إلغاء التنزيلات الخاصة؟</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">إن أغلقت كل الألسنة الخاصة الآن، فسيُلغى تنزيل %1$s. هل أنت متأكد أنك تريد مغادرة التصفح الخاص؟</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ألغِ التنزيلات</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">ابقَ في التصفح الخاص</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..5f9d21334a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ast/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descargues</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descarga en posa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Completóse la descarga</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">La descarga falló</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Baxar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Baxar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Encaboxar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s nun pue baxar esti tipu de ficheru</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Nun se pudo abrir el ficheru</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nun s\'atopó nenguna aplicación p\'abrir ficheros «%1$s»</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Posar</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Siguir</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Encaboxar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Retentar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zarrar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar l\'aición con:</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nun ye posible abrir «%1$s»</string>
+
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tienes de permitir l\'accesu a los ficheros ya al conteníu multimedia pa baxar ficheros. Vete a la configuración d\'Android, dempués a los permisos ya toca Permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Quies encaboxar les descargues privaes?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si zarres agora toles llingüetes privaes, va encaboxase la descarga de «%1$s». ¿De xuru que quies colar del mou de restolar en privao?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Encaboxales</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Quedar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..fd2a8b0a86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-az/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Endirmələr</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Endirməyə fasilə verildi</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Endirmə bitdi</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Endirmə uğursuz oldu</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Endir (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Endir</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Ləğv et</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s bu fayl növünü endirə bilmir</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Faylı açmaq mümkün olmadı</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s fayllarını açmaq üçün tətbiq tapılmadı</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Fasilə ver</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Davam etdir</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Ləğv et</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Aç</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Təkrar Yoxla</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Qapat</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Əməliyyatı bununla tamamla</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ilə açıla bilmir</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..8f630f592b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-azb/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">یئندیریلن‌لر</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">یئندیرمه دایاندیریلدی</string>
+
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">یئندیرمه کامیل اولدو</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">یئندیرمه باشاریسیز</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">یئندیر (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">یئندیر</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">لغو</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">فایل آچیلانمادی.</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s فایلی آچماق اوچون هئچ بیر اپ تاپیلمادی.</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">دایاندیر</string>
+
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">سوردور</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">لغو</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">آچ</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">یئنی‌دن چالیش</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">باغلا</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ایشه آلاراق حرکتی تماملایین</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s آچیلمیر</string>
+
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">فایل‌لاری یئندیرمک اوچون فایل‌لار و مدیا ایجازه‌سی ایسته‌نیلیر. اندروید تنظیم‌لرینه گئچین، ایجازه‌لره توخونون و ایجازه وئرین.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">گیزلی یئندیرمه‌لر لغو ائدیلسین؟</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ایندی بوتون گیزلی تاغلاری باغلاساز %1$s یئندیرمه‌سی لغو ائدیله‌جک. گیزلی موروچودان آیریلماق ایسته‌دیگیزه اطمینانیز وار؟</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">یئندیرمه‌لری لغو ائله</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">گیزلی موروچودا قالین</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..a3dda8db06
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-be/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Сцягванні</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Сцягванне прыпынена</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Сцягванне скончана</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Няўдача сцягвання</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Сцягванне (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Сцягнуць</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Адмяніць</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s не можа сцягнуць гэты тып файла</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Немагчыма адкрыць файл</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Не знойдзена праграма, каб адкрыць файлы %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Прыпыніць</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Працягнуць</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Адмяніць</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Адкрыць</string>
+
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Паспрабаваць зноў</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Закрыць</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Завяршыць дзеянне з дапамогаю</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Не ўдалося адкрыць %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Для сцягвання файлаў патрэбен доступ да файлаў і мультымедыя. Перайдзіце ў налады Android, націсніце «Дазволы», затым «Дазволіць».</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Скасаваць прыватныя сцягванні?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Калі вы зараз закрыеце ўсе прыватныя карткі, сцягванне %1$s будзе скасавана. Вы сапраўды жадаеце выйсці з прыватнага аглядання?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Скасаваць сцягванні</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Застацца ў прыватным агляданні</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..d49e0557ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bg/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Изтегляния</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Изтеглянето е поставено на пауза</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Изтеглянето е завършено</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Изтеглянето е неуспешно</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Изтегляне (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Изтегляне</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Отказ</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s не може да изтегли този вид файл</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Отварянето на файла е невъзможно</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Не е открито приложение, което да отваря файлове от вида %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Пауза</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Продължаване</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Отказ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Отваряне</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Повторен опит</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Затваряне</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Завършване на действието използвайки</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Не успя да се отвори %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">За сваляне на файлове е необходимо за достъп до медия и файлове. За да разрешите, влезте в настройки на Android, след което докоснете разрешения и позволяване.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Отмяна на поверителните изтегляния?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ако затворите всички поверителни раздели, %1$s изтегляния ще бъдат прекъснати. Сигурни ли сте, че искате да напуснете поверителното разглеждане?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Прекъсване</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Оставане в поверително разглеждане</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..9a6fc87ea9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bn/strings.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ডাউনলোডগুলো</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ডাউনলোডে বিরতি দেওয়া হয়েছে</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ডাউনলোড সম্পন্ন হয়েছে</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ডাউনলোড ব্যর্থ হয়েছে</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ডাউনলোড (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ডাউনলোড</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">বাতিল</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s এই ধরণের ফাইল ডাউনলোড করতে পারবেন না</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ফাইল খোলা যায়নি</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ফাইলটি খুলতে কোনো অ্যাপ পাওয়া যায়নি</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">বিরতি দিন</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">পুনরায় শুরু করুন</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">বাতিল</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">খুলুন</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">পুনরায় চেষ্টা করুন</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">বন্ধ</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s খুলতে ব্যর্থ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..4521b68b21
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-br/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Pellgargadurioù</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Pellgargañ ehanet</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Pellgargadur echu</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Cʼhwitadenn war ar pellgargadur</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Pellgargañ (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Pellgargañ</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Nullañ</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ne cʼhall ket pellgargañ ar rizh restr-mañ</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Ne cʼhaller ket digeriñ ar restr</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Arload ebet evit digeriñ restroù %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Ehanañ</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Kendercʼhel</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Nullañ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Digeriñ</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Klask en-dro</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Serriñ</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Kenderc’hel gant</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">N’haller ket digeriñ %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Aotreoù haeziñ ar restroù ha mediaoù a zo ret kaout a-benn pellgargañ restroù. Mont da arventennoù Android, pouezit war an aotreoù ha pouezit war aotren.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Nullañ ar pellgargadennoù prevez?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ma serrit ho holl ivinelloù prevez bremañ e vo paouezet gant pellgargadenn %1$s.Sur hoc’h e gell deoc’h kuitaat ar merdeiñ prevez?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Nullañ ar bellgargañ</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Chom er Merdeiñ Prevez</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..28ef89bf9a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bs/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Preuzimanja</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Preuzimanje pauzirano</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Preuzimanje završeno</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Neuspjelo preuzimanje</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Preuzimanje (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Preuzmi</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Otkaži</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ne može preuzeti ovaj tip fajla</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Ne mogu otvoriti fajl.</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nije pronađena aplikacija za otvaranje %1$s fajlova</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pauziraj</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Nastavi</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Otkaži</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Otvori</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Pokušaj ponovo</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zatvori</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Završi radnju pomoću</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ne mogu otvoriti %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Pristup datotekama i medijima je potreban za preuzimanje datoteka. Idite na postavke Androida, dodirnite dozvole i dodirnite dozvoli.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Otkazati privatna preuzimanja?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ako sada zatvorite sve privatne tabove, %1$s preuzimanja će biti otkazano. Jeste li sigurni da želite napustiti privatno pretraživanje?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Otkaži preuzimanja</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ostani u privatnom pretraživanju</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..fdfe1510ed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ca/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Baixades</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Baixada en pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Ha acabat la baixada</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Ha fallat la baixada</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Baixada (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Baixa</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancel·la</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">El %1$s no pot baixar aquest tipus de fitxer</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No s’ha pogut obrir el fitxer</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No s’ha trobat cap aplicació per a obrir els fitxers %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Reprèn</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancel·la</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Obre</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Torna-ho a provar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Tanca</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completa l’acció mitjançant</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">No s’ha pogut obrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Cal permís d’accés als fitxers i contingut multimèdia per baixar fitxers. Aneu als paràmetres de l’Android, aneu als permisos i trieu «Permet».</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Voleu cancel·lar les baixades privades?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si tanqueu totes les pestanyes privades ara, la baixada %1$s es cancel·larà. Esteu segur que voleu deixar la navegació privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancel·la les baixades</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Continua la navegació privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..a2fb97d855
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cak/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Taq qasanïk</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Xq\'at qasanïk</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Xtz\'aqät qasanïk</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Xsach qasanïk</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Qasanïk (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Tiqasäx</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Tiq\'at</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Man nitikïr ta nuqasaj re ruwäch yakb\'äl re\' ri %1$s</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Man nijaqatäj ta ri yakb\'äl</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Majun chokoy xilitäj richin nijaq %1$s taq yakb\'äl</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Tuxlan</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Titikïr chik el</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Tiq\'at</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Tijaq</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Titojtob\'ëx chik</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Titz\'apïx</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Titz\'aqatisäïx rub\'anik rik\'in</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Man tikirel ta nijaq %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">K\'o chi niya\' q\'ij richin ye\'ok chi kipam ri taq yakb\'äl chuqa\' taq k\'ïy k\'oxom richin yeqasäx taq yakb\'äl. Tib\'an b\'enam pa runuk\'ulem Android, tapitz\'a\' ya\'oj q\'ij chuqa\' tipitz\' ri tiya\' q\'ij.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿La ye\'aq\'ät ri ichinan taq qasanïk?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">¿We xke\'atz\'apij wakami ronojel ri ichinan taq ruwi\', xkeq\'at %1$s qasanïk. ¿La at jikïl chi nawajo\' yatel pa ri ichinan okem pa k\'amaya\'l?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Keq\'at qasanïk</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Tik\'oje\' pa ichinan okem pa k\'amaya\'l</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..20b253dcaa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Mga Download</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Download na-pause</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Download human na</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Download na-pakyas</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Download</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancel</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s dili kadownload ani nga file type</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Dili maka-abli sa file</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Wala\'y app nakita nga maka-abli sa %1$s mga file</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Resume</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancel</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Open</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Try Again</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Close</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Kumpletoha ang action gamit ang</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Dili ka-abli sa %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Permission access sa files ug media kinahanglan para madownload ang mga file. Adto sa Android settings, tap ang permissions, ug tap allow.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">I-cancel ang mga private downloads?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">I-cancel ang mga downloads</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Magpabilin sa private browsing</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..9b8597cfb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">داگرتنەکان</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">داگرتن وەستێنرا</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">داوگرتن تەواو بوو</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">داگرتن سەرکەوتوو نەبوو</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">داگرتن (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">داگرتن</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">پاشگەزبوونەوە</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ناتوانێت ئەم جۆری پەڕگەیە دابگرێت</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ناتوانرێت پەڕگە بکرێتەوە</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">هیچ بەرنامەیەک نەدۆزرایەوە بتوانێت پەڕگەی %1$s بکاتەوە</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">وچان</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">بەردەوامبوونەوە</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">پاشگەزبوونەوە</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">کردنەوە</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">دووبارە هەوڵ بدەرەوە</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">داخستن</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">کردار تەواوبکە بەبەکارهێنانی</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">نەتوانرا %1$s بکرێتەوە</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..1114f1fe53
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-co/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Scaricamenti</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Scaricamentu messu in pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Scaricamentu compiu</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Fiascu di u scaricamentu</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Scaricamentu (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Scaricà</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Abbandunà</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ùn pò micca scaricà stu tipu di schedariu</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Impussibule d’apre u schedariu</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Alcuna appiecazione trova per apre i schedarii %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Cuntinuà</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Abbandunà</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Apre</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Pruvà torna</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Chjode</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Cumplettà l’azzione cù</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impussibule d’apre %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">I diritti d’accessu à i schedarii è i medià sò richiesti per scaricà schedarii. Accidite à e preferenze d’Android, picchichjate Permessi, è dopu Permette.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Abbandunà i scaricamenti in a navigazione privata ?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">A chjusura di tutte l’unghjette private subitu pianterà u scaricamentu di %1$s. Vulete veramente piantà a navigazione privata ?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Abbandunà i scaricamenti</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stà in navigazione privata</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..826fa7ab25
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cs/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Stahování</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Stahování pozastaveno</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Stahování dokončeno</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Stahování selhalo</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Stáhnout soubor %1$s</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Stáhnout</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Zrušit</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s nemůže stáhnout tento typ souboru</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Soubor se nepodařilo otevřít</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Pro otevření souborů typu %1$s nebyla nalezena žádná aplikace</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pozastavit</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Pokračovat</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Zrušit</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Otevřít</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Zkusit znovu</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zavřít</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dokončit akci pomocí</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Aplikaci %1$s se nepodařilo otevřít</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Pro stahování souborů je potřeba oprávnění pro přístup k souborům a médiím. Otevřete nastavení systému Android, klepněte na nastavení oprávnění a vyberte Povolit.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Zrušit stahování z anonymního prohlížení?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Pokud zavřete všechny anonymní panely, bude také zrušeno stahování souboru %1$s. Opravdu chcete ukončit anonymní prohlížení?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Zrušit stahování</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Zůstat v anonymním prohlížení</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..ba05a7e824
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cy/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Llwythi</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Oedi’r llwythi</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Llwythi cyflawn</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Methodd y llwytho</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Llwytho (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Llwytho i Lawr</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Diddymu</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Nid yw %1$s yn gallu llwytho’r math hwn o ffeil</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Methu agor y ffeil</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Heb ganfod unrhyw ap sy’n agor ffeiliau %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Oedi</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Ailgychwyn</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Diddymu</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Agor</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Ceisiwch Eto</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Cau</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Cwblhau’r weithred gyda</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Methu agor %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Mae angen caniatâd mynedid i ffeiliau a chyfryngau i lwytho ffeiliau i lawr. Ewch i osodiadau Android, tapiwch caniatâd, a thapio caniatáu.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Diddymu llwythi preifat?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Os ydych yn cau pob ffenestr Pori Preifat nawr, bydd %1$s llwyth yn cael ei ddiddymu. Ydych chi’n siŵr eich bod am adael y modd Pori Preifat?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Diddymu pob llwyth</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Aros yn y modd pori preifat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..bad7aae935
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-da/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Filhentninger</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Hentning sat på pause</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Hentning fuldført</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Hentning mislykkedes</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Hent (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Hent</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Annuller</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s kan ikke hente denne filtype</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Kunne ikke åbne filen</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ingen app fundet til at åbne %1$s-filer</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Genoptag</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Annuller</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Åbn</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Prøv igen</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Luk</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Udfør handlingen med</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kunne ikke åbne %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tilladelse til at få adgang til filer og medier er nødvendig for at hente filer. Gå til Indstillinger i Android, tryk på Tilladelser, og tryk så på Tillad.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Annuller private filhentninger?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Hvis du lukker alle private faneblade nu, vil hentning af %1$s blive annulleret. Er du sikker på, at du vil forlade privat browsing-tilstand?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Annuller filhentninger</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Forbliv i privat browsing-tilstand</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..4f1478259e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-de/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Download pausiert</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Download abgeschlossen</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Download fehlgeschlagen</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Herunterladen (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Herunterladen</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Abbrechen</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s kann diesen Dateityp nicht herunterladen</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Datei konnte nicht geöffnet werden</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Keine App zum Öffnen von %1$s-Dateien gefunden</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausieren</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Fortsetzen</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Abbrechen</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Öffnen</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Erneut versuchen</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Schließen</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Aktion abschließen mittels</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s kann nicht geöffnet werden</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Berechtigung für Datei- und Medienzugriff erforderlich, um Dateien herunterzuladen. Öffnen Sie die Android-Einstellungen, tippen Sie auf Berechtigungen und tippen Sie auf Erlauben.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Private Downloads abbrechen?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Wenn Sie jetzt alle privaten Tabs schließen, wird der Download von %1$s abgebrochen. Soll der Private Modus wirklich verlassen werden?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Downloads abbrechen</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Im Privaten Modus bleiben</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..6efee27722
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Ześěgnjenja</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Ześěgnjenje jo se zastajiło</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Ześěgnjenje jo se dokóńcyło</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Ześěgnjenje njejo se raźiło</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Ześěgnuś (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Ześěgnuś</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Pśetergnuś</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s njamóžo toś ten datajowy typ ześěgnuś</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Dataja njedajo se wócyniś</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Žedno nałoženje za wócynjanje datajow typa %1$s namakane</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Zastajiś</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Pókšacowaś</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Pśetergnuś</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Wócyniś</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Hyšći raz wopytaś</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zacyniś</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Akciju skóńcyś z pomocu</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s njedajo se wócyniś</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Pśistup k datajam a medijam jo trjebny, aby dataje ześěgnuł. Pśejźćo k nastajenjam Android, pótusniśo pšawa a pón Dowóliś.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Priwatne ześěgnjenja pśetergnuś?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jolic zacynijośo něnto wšykne priwatne rejtariki, se %1$s ześěgnjenje pśetergnjo. Cośo priwatny modus napšawdu spušćiś?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Ześěgnjenja pśetergnuś</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">W priwatnem modusu wóstaś</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..8b27d69322
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-el/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Λήψεις</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Λήψη σε παύση</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Η λήψη ολοκληρώθηκε</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Αποτυχία λήψης</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Λήψη (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Λήψη</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Ακύρωση</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Το %1$s δεν μπορεί να κάνει λήψη αυτού του τύπου αρχείου</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Αδυναμία ανοίγματος αρχείου</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Δεν βρέθηκε εφαρμογή για άνοιγμα αρχείων %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Παύση</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Συνέχιση</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Ακύρωση</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Άνοιγμα</string>
+
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Δοκιμή ξανά</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Κλείσιμο</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Ολοκλήρωση ενέργειας με</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Αδυναμία ανοίγματος του %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Απαιτείται άδεια για πρόσβαση σε αρχεία και πολυμέσα για τη λήψη αρχείων. Μεταβείτε στις Ρυθμίσεις Android, πατήστε «Δικαιώματα» και επιλέξτε «Να επιτρέπεται».</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Ακύρωση ιδιωτικών λήψεων;</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Αν κλείσετε τώρα όλες τις ιδιωτικές καρτέλες, θα ακυρωθεί %1$s λήψη. Θέλετε σίγουρα να αποχωρήσετε από την ιδιωτική περιήγηση;</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Ακύρωση λήψεων</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Παραμονή σε ιδιωτική περιήγηση</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..fb702f10cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Download paused</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Download completed</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Download failed</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Download</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancel</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s can’t download this file type</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Could not open file</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No app found to open %1$s files</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Resume</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancel</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Open</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Try Again</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Close</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Complete action using</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Unable to open %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Files and media permission access needed to download files. Go to Android settings, tap permissions, and tap allow.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancel private downloads?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">If you close all Private tabs now, %1$s download will be cancelled. Are you sure you want to leave Private Browsing?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancel downloads</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stay in private browsing</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..fb702f10cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Download paused</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Download completed</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Download failed</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Download</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancel</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s can’t download this file type</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Could not open file</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No app found to open %1$s files</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Resume</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancel</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Open</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Try Again</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Close</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Complete action using</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Unable to open %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Files and media permission access needed to download files. Go to Android settings, tap permissions, and tap allow.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancel private downloads?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">If you close all Private tabs now, %1$s download will be cancelled. Are you sure you want to leave Private Browsing?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancel downloads</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stay in private browsing</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..dc04266501
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-eo/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Elŝutoj</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Elŝuto paŭzinta</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Elŝuto kompleta</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Elŝuto malsukcesa</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Elŝuto (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Elŝuti</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Nuligi</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ne povas elŝuti tiun ĉi tipon de dosiero</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Ne eblis malfermi la dosieron</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Neniu programo trovita por malfermi dosierojn %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Paŭzigi</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Daŭrigi</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Nuligi</string>
+
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Malfermi</string>
+
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Klopodi denove</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Fermi</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Kompletigi agon per</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ne eblis malfermi %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">La permeso aliri la konservejon de dosieroj kaj aŭdvidaĵojn estas bezonata por elŝuti dosierojn. Iru al la agordoj de Android, tuŝetu Permesoj kaj poste Permesi.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Ĉu nuligi privatajn elŝutojn?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se vi fermas ĉiujn viajn langetojn de privata retumo nun, %1$s elŝutoj estos nuligitaj. Ĉu vi certe volas forlasi la privatan retumon?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Nuligi elŝutojn</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Resti en privata retumo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..a35f9710bd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descargas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descarga pausada</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Descarga completa</string>
+
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Falló la descarga</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Descarga (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Descargar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No se pudo abrir el archivo</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No se encontró ninguna aplicación para abrir archivos %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausar</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Reanudar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Volver a intentar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Cerrar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se puede abrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ir a la configuración de Android, tocar permisos y tocar permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cerrás todas las pestañas privadas ahora, se cancelará(n) %1$s descarga(s). ¿Estás seguro de querer dejar la navegación privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descargas</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Seguir en la navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..9103fb0e84
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descargas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descarga pausada</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Descarga completada</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Descarga fallida</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Bajar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Bajar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No se pudo abrir el archivo</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No se encontró ninguna aplicación para abrir archivos %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausar</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Continuar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Volver a intentarlo</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Cerrar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se pudo abrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ve a la configuración de Android, toca permisos y toca permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cierra todas las pestañas privadas ahora, %1$s descargas serán canceladas. ¿De verdad quieres dejar la navegación privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descarga</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Permanecer en navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..759558d337
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descargas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descarga en pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Descarga completa</string>
+
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Descarga fallida</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Descargar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No se ha podido abrir el archivo</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No se ha encontrado ninguna aplicación para abrir archivos %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Continuar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Reintentar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Cerrar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se puede abrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ve a los ajustes de Android, toca permisos y toca permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cierra todas las pestañas privadas ahora, la descarga de %1$s se cancelará. ¿Estás seguro de que quieres abandonar la navegación privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descargas</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Seguir en navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..6b02c90772
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descargas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descarga pausada</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Descarga completa</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Descarga fallada</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Descargar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No se pudo abrir el archivo</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No se encontró ninguna aplicación para abrir archivos %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausar</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Reanudar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Reintentar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Cerrar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se pudo abrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ve a la configuración de Android, toca permisos y toca permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cierras todas las pestañas privadas ahora, se cancelará la descarga de %1$s. ¿Estás seguro de que deseas salir de la navegación privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descargas</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Permanecer en la navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..759558d337
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descargas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descarga en pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Descarga completa</string>
+
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Descarga fallida</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Descargar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No se ha podido abrir el archivo</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No se ha encontrado ninguna aplicación para abrir archivos %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Continuar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Reintentar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Cerrar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se puede abrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ve a los ajustes de Android, toca permisos y toca permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cierra todas las pestañas privadas ahora, la descarga de %1$s se cancelará. ¿Estás seguro de que quieres abandonar la navegación privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descargas</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Seguir en navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..be4a1a152c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-et/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Allalaadimised</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Allalaadimine on pausil</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Allalaadimine lõpetati</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Allalaadimine ebaõnnestus</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Kas soovid faili %1$s alla laadida?</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Laadi alla</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Loobu</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$sil pole võimalik seda tüüpi faile alla laadida</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Faili avamine ebaõnnestus.</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s failide avamiseks puudub äpp</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Paus</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Jätka</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Katkesta</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ava</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Proovi uuesti</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Sulge</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Lõpeta tegevus kasutades äppi</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Pole võimalik avada äppi %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Failide allalaadimiseks on vajalik failide ja meedia ligipääsu õigus. Mine Androidi sätetesse, vali õigused ning puuduta lubamise valikut.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Kas katkestada privaatsed allalaadimised?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Kui sa sulged praegu kõik privaatsed kaardid, siis faili %1$s allalaadimine katkestatakse. Kas oled kindel, et soovid privaatsest veebilehitsemisest väljuda?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Katkesta allalaadimised</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Jää privaatse veebilehitsemise režiimi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..362cef474e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-eu/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Deskargak</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Deskarga pausatuta</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Deskarga burututa</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Deskargak huts egin du</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Deskargatu (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Deskargatu</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Utzi</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s(e)k ezin du fitxategi mota hau deskargatu</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Ezin da fitxategia ireki</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ez da aplikaziorik aurkitu %1$s motako fitxategiak irekitzeko</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausatu</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Berrekin</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Utzi</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ireki</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Saiatu berriro</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Itxi</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Burutu ekintza honekin</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ezin da %1$s ireki</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Fitxategiak deskargatzeko, fitxategi eta multimediaren sarbide-baimenak behar dira. Zoaz Android-en ezarpenetara, sakatu baimenak eta sakatu baimendu.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Utzi deskarga pribatuak?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Fitxa pribatu guztiak orain itxiz gero, %1$s fitxategiaren deskarga bertan behera utziko da. Ziur zaude nabigatze pribatua utzi nahi duzula?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Utzi deskargak</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Mantendu nabigatze pribatuan</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..6d11b9cb08
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fa/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">بارگیری‌ها</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">بارگیری مکث شد</string>
+
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">بارگیری کامل شد</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">بارگیری‌ شکست خورد</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">بارگیری (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">بارگیری</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">لغو</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s نمی‌تواند این نوع پرونده را بارگیری کند</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">پرونده باز نشد</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">هیچ کاره‌ای برای گشودن پرونده‌های %1$s یافت نشد</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">مکث</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ازسرگیری</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">لغو</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">باز کردن</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">تلاش مجدد</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">بستن</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">تکمیل عملکرد با استفاده از</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">نتوانست %1$s را باز کند</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">برای بارگیری پرونده‌ها، اجازهٔ دسترسی به پرونده‌ها و رسانه لازم است. به تنطیمات اندروید بروید، روی اجازه‌ها بزنید و سپس اجازه دادن را بزنید.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">بارگیری‌های خصوصی لغو شوند؟</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">اگر اکنون همهٔ زبانه‌های خصوصی را ببندید، بارگیری %1$s لغو می‌شود. آیا مطمئنید که می‌خواهید از مرور خصوصی خارج شوید؟</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">لغو بارگیری‌ها</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">باقی ماندن در مرور خصوصی</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..3da43915d6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ff/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Gaawte</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Aawtogol dartinaama</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Aawtagol gasii</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Aawtagol woorii</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Aawtogol (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Aawto</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Haaytu</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s waawaa aawtaade sifaa ndee fiilde</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Horiima udditde fiilde</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Alaa jaaɓngal yiytaa ngam udditde piille %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Sabbo</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Fuɗɗito</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Haaytu</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Uddit</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Eto goɗngol</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Uddu</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Horiima uddide %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Yamiroore keɓgol piille e mejaaje ena coklaa ngam aawtaade piille. Yah to teelte Android, tappu e jamirooje, tappaa yamir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Haaytin gaawte cuuriiɗe?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Haaytin gaawte</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Heddo e banngogol suturo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..216806c75a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fi/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Lataukset</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Lataus keskeytetty</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Lataus valmis</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Lataus epäonnistui</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Lataa (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Lataa</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Peruuta</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ei voi ladata tätä tiedostotyyppiä</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Tiedostoa ei voitu avata</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">&quot;%1$s&quot;-tiedostojen avaamiseen ei löytynyt sovellusta</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Keskeytä</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Jatka</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Peruuta</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Avaa</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Yritä uudelleen</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Sulje</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Suorita toiminto sovelluksella</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ei voitu avata %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tiedostojen lataamiseksi tarvitaan tiedostojen ja median käyttöoikeudet. Siirry Androidin asetuksiin, napauta käyttöoikeudet ja napauta salli.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Perutetaanko yksityiset lataukset?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jos suljet kaikki yksityiset välilehdet nyt, tiedoston %1$s lataus perutaan. Haluatko varmasti poistua yksityisestä selaamisesta?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Peruuta lataukset</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Pysy yksityisessä selaamisessa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..886d0d1e85
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fr/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Téléchargements</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Téléchargement mis en pause</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Téléchargement terminé</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Le téléchargement a échoué</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Télécharger (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Télécharger</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Annuler</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ne peut pas télécharger ce type de fichier</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Impossible d’ouvrir le fichier</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Aucune application trouvée pour ouvrir les fichiers %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Reprendre</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Annuler</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ouvrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Réessayer</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Fermer</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Continuer avec</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impossible d’ouvrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Les autorisations d’accès au stockage de fichiers et de médias sont nécessaires pour télécharger des fichiers. Rendez-vous dans les paramètres d’Android, sélectionnez Autorisations puis Autoriser.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Annuler les téléchargements privés ?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si vous fermez tous les onglets de navigation privée maintenant, le téléchargement de %1$s sera annulé. Voulez-vous vraiment quitter la navigation privée ?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Annuler les téléchargements</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Rester en mode de navigation privée</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..609b7ca086
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fur/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Discjamâts</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Discjariament in pause</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Discjariament completât</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Discjariament falît</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Discjame (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Discjame</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Anule</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s nol pues discjamâ chest gjenar di file</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Impussibil vierzi il file</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nissune aplicazion cjatade par vierzi i files %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Ripie</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Anule</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Vierç</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Torne prove</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Siere</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Complete azion doprant</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impussibil vierzi %1$s</string>
+
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Par discjamâ i files al è necessari il permès di acedi a files e contignûts multimediâi. Va su lis impostazions di Android, tocje Autorizazions e dopo permet.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Anulâ i discjariaments in modalitât privade?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se tu sieris dutis lis schedis privadis cumò, al vignarà anulât il discjariament di %1$s. Lassâ pardabon la navigazion privade?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Anule i discjariaments</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Reste te navigazion privade</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..c4a8844052
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Downloaden pauzearre</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Downloaden foltôge</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Downloaden mislearre</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Downloade (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Downloade</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Annulearje</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s kin dit bestânstype net downloade</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Kin bestân net iepenje</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Gjin app fûn om %1$s-bestannen mei te iepenjen</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pauzearje</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Ferfetsje</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Annulearje</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Iepenje</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Opnij probearje</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Slute</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Hanneling foltôgje mei</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kin %1$s net iepenje</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tagong ta bestannen en media fereaske om bestannen te downloaden. Gean nei Android-ynstellingen, tik op machtigingen en tik op tastean.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Priveedownloads annulearje?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">As jo no alle priveeljepblêden slute, sil %1$s download annulearre wurde. Binne jo wis dat jo Priveenavigaasje ferlitte wolle?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Downloads annulearje</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Yn priveenavigaasje bliuwe</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..d39b15c1a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Íoslódálacha</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Cuireadh an íoslódáil ar shos</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Íoslódáil críochnaithe</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Theip ar íoslódáil</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Íoslódáil (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Íoslódáil</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cealaigh</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Ní féidir le %1$s comhad den chineál seo a íoslódáil</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Níorbh fhéidir an comhad a oscailt</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Sos</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Lean</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cealaigh</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Oscail</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Bain Triail Eile As</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Dún</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..e38a01f7e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gd/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Luchdaidhean a-nuas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Tha an luchdadh a-nuas na stad</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Chaidh a luchdadh a-nuas</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Dh’fhàillig an luchdadh a-nuas</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Luchdadh a-nuas (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Luchdaich a-nuas</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Sguir dheth</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Chan urrainn dha %1$s a leithid seo a dh’fhaidhle a luchdadh a-nuas</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Cha b’ urrainn dhuinn am faidhle fhosgladh</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Cha deach aplacaid a lorg a dh‘fhosgladh faidhlichean %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Cuir na stad</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Lean air</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Sguir dheth</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Fosgail</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Feuch ris a-rithist</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Dùin</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Coilean seo le</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Cha ghabh %1$s fhosgladh</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tha feum air cead inntrigidh do dh’fhaidhlichean is meadhanan mus luchdaich thu a-nuas faidhle. Tadhail air roghainnean Android is thoir gnogag air a’ chead.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">A bheil thu airson sgur de gach luchdadh a-nuas prìobhaideach?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ma dhùineas tu gach taba brabhsaidh prìobhaideach an-dràsta, thèid crìoch a chur air luchdadh a-nuas an fhaidhle “%1$s”. A bheil thu cinnteach gu bheil thu airson am brabhsadh prìobhaideach fhàgail?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Sguir de gach luchdadh a-nuas</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Fuirich sa bhrabhsadh phrìobhaideach</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..f5bf3cac41
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gl/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descargas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descarga en pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Rematou a descarga</string>
+
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Produciuse un fallo ao descargar</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Descargar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s non pode descargar este tipo de ficheiro</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Non foi posíbel abrir o ficheiro</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Non se atopou ningunha aplicación para abrir ficheiros %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Retomar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Tentar de novo</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Pechar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Non foi posíbel abrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Precísase permiso de acceso a ficheiros e recursos multimedia para descargar ficheiros. Vaia á configuración de Android, toque nos permisos e toque en permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancelar as descargas privadas?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se pecha agora todos os separadores privados, cancelarase a descarga de %1$s. Seguro que quere saír da navegación privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar as descargas</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ficar na navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..4353487f58
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gn/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Ñemboguejy</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Ñemboguejy opyta</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Oguejypáma</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Ñemboguejy ojavy</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Emboguejy (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Mboguejy</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Heja</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ndaikatúi omboguejy koichagua marandurenda</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Ndaikatúi eike marandurendápe</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ndojejuhúi tembiporu’i embojuruja hag̃ua %1$s marandurenda</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Mombyta</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Ku’ejey</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Heja</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Mbojuruja</string>
+
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Eha’ãjey</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Mboty</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Emoĩmba tembiapo rupi</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ndaikatúi ijuruja %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Oñeikotevẽ marandurendápe jeike ha ñemoneĩ emboguejy hag̃ua marandurenda. Eho Android ñembohekópe, eikutu ñemoneĩ ha eikutu moneĩ.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Ehejase ñemboguejy ñemigua?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Embotýramo umi kundahára ñemigua rovetã ko’ág̃a, ojehejáta ñemboguejy %1$s. ¿Añetéhápe rehejase ñeikundaha ñemigua?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Eheja ñemboguejy</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Epyta kundaha ñemiguápe</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..d3b6f95bfd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ડાઉનલોડ્સ</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ડાઉનલોડ અટકાવ્યુ</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ડાઉનલોડ પૂરુ થયું</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ડાઉનલોડ નિષ્ફળ થયું</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">(%1$s) ડાઉનલોડ કરો</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ડાઉનલોડ કરો</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">રદ કરો</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s આ પ્રકારની ફાઇલને ડાઉનલોડ કરી શકતા નથી</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ફાઇલ ખોલી શકાતી નથી</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ફાઇલો ખોલવા માટે કોઈ એપ્લિકેશન મળી નથી</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">અટકાવો</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ફરી શરૂ કરો</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">રદ કરો</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ખોલો</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ફરીથી પ્રયત્ન કરો</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">બંધ કરો</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">આની મદદથી ક્રિયા પૂર્ણ કરો</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ખોલવામાં અસમર્થ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..2ce36bf6ee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">डाउनलोड</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">डाउनलोड रोका गया</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">डाउनलोड संपन्न</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">डाउनलोड विफल</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">(%1$s) डाउनलोड करें</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">डाउनलोड करें</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">रद्द करें</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s इस प्रकार के फाइल को डाउनलोड नहीं कर सकता</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">फ़ाइल खोला नहीं जा सका</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s फाइलें खोलने के लिए कोई ऐप नहीं मिला</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">रोकें</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">पुनः चलाएं</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">रद्द करें</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">खोलें</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">पुनः प्रयास करें</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">बंद करें</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">इसके उपयोग से कार्य पूरा करें</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s को खोलने में असमर्थ रहा</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..39955ba66e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hil/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string>
+
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Padayun</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Pagabuksan</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Sirado</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..15da0f5495
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hr/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Preuzimanja</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Preuzimanje je pauzirano</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Preuzimanje završeno</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Preuzimanje neuspjelo</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Preuzmi (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Preuzmi</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Odustani</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ne može preuzeti ovu vrstu datoteka</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Nije bilo moguće otvoriti datoteku</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nije pronađena aplikacija za otvaranje %1$s datoteka</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pauziraj</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Nastavi</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Odustani</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Otvori</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Pokušaj ponovo</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zatvori</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Završi radnju koristeći</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nije moguće otvoriti %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Za preuzimanje datoteka potrebna je dozvola za pristup datotekama i medijima. Idi u postavke sustava Android, odaberi dozvole i odaberi &quot;Dopusti&quot;.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Prekinuti privatna preuzimanja?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ukoliko zatvoriš sve prozore privatnog pretraživanja, prekinut će se preuzimanje datoteke %1$s. Stvarno želiš prekinuti privatno pretraživanje?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Prekini preuzimanja</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ostani u privatnom pretraživanju</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..6224c59482
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Sćehnjenja</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Sćehnjenje je zastajene</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Sćehnjenje dokónčene</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Sćehnjenje njeje so poradźiło</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Sćahnyć (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Sćahnyć</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Přetorhnyć</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s njemóže tutón datajowy typ sćahnyć</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Dataja njeda so wočinić</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Žane nałoženje za wočinjenje datajow typa %1$s namakane</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Zastajić</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Pokročować</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Přetorhnyć</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Wočinić</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Hišće raz spytać</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Začinić</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Akciju skónčić z pomocu</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s njeda so wočinić</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Přistup k datajam a medijam je trěbny, zo byšće dataje sćahnył. Přeńdźće k nastajenjam Android, podótkńće so prawow a potom Dowolić.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Priwatne sćehnjenja přetorhnyć?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jeli nětko wšě priwatne rajtarki začiniće, so sćehnjenje dataje %1$s přetorhnje. Chceće priwatny modus woprawdźe wopušćić?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Sćehnjenja přetorhnyć</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">W priwatnym modusu wostać</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..5bd9b4214d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hu/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Letöltések</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">A letöltés szüneteltetve</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">A letöltés befejeződött</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">A letöltés sikertelen</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Letöltés (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Letöltés</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Mégse</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">A %1$s nem tudja letölteni ezt a fájltípust</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">A fájl megnyitása sikertelen</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nem található alkalmazás, amely megnyitná a(z) %1$s fájlokat</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Szünet</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Folytatás</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Mégse</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Megnyitás</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Próbálja újra</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Bezárás</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Művelet befejezése ezzel:</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">A(z) %1$s nem nyitható meg</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">A fájlok letöltéséhez fájl és média engedély hozzáférés szükséges. Nyissa meg az Android beállításokat, koppintson az engedélyekre, és koppintson az engedélyezésre.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Megszakítja a privát letöltéseket?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ha most bezárja az összes privát lapot, akkor %1$s letöltés megszakad. Biztos, hogy ki akar lépni a privát böngészésből?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Letöltések megszakítása</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Maradok privát böngészésben</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..5bdc00e926
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Ներբեռնումներ</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Ներբեռնումը դադարեցված է</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Ներբեռնվեց</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Ներբեռնումը ձախողվեց</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Ներբեռնել (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Ներբեռնել</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Չեղարկել</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s-ը չի կարող ներբեռնել այս տեսակի ֆայլը</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Հնարավոր չէ բացել ֆայլը</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ֆայլերը բացելու համար որևէ հավելված չի գտնվել</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Դադարեցնել</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Վերսկսել</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Չեղարկել</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Բացել</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Կրկին փորձել</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Փակել</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Ավարտել գործողությունը հետևյալով՝</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Անհնար է բացել %1$s-ը</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Ֆայլեր ներբեռնելու համար անհրաժեշտ է ֆայլերի և մեդիայի թույլտվություն: Անցեք Android-ի կարգավորումներին, հպեք թույլտվություններին և հպեք թույլատրել:</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Չեղարկե՞լ անձնական ներբեռնումները:</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Եթե հիմա փակեք բոլոր Մասնավոր ներդիրները, %1$s ներբեռնում կչեղարկվի: Փակե՞լ:</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Չեղարկել ներբեռնումները</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Շարունակել Մասնավոր դիտարկումը</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..2f69b7435d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ia/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Discargamentos</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Discargamento pausate</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Discargamento completate</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Discargamento fallite</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Discargar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Discargar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancellar</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s non pote discargar iste typo de file</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Impossibile aperir file.</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nulle app trovate pro aperir %1$s files</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausar</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Reprender</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancellar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Aperir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Retentar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Clauder</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar le action per</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impossibile aperir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Le permission de accesso a files e multimedia es necessari pro discargar files. Vade al parametros de Android, tocca Permissiones e tocca Permitter.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancellar le discargamentos private?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si tu claude tote le schedas private ora, %1$s discargamento essera cancellate. Desira tu vermente abandonar le Navigation private?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancellar le discargamentos</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Restar in navigation private</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..442a1b812b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-in/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Unduhan</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Unduhan ditunda</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Unduhan selesai</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Unduhan gagal</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Unduh (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Unduh</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Batal</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s tidak dapat mengunduh jenis berkas ini</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Tidak dapat membuka berkas</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Tidak ditemukan aplikasi untuk membuka berkas %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Tunda</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Lanjutkan</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Batalkan</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Buka</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Coba Lagi</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Tutup</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Selesaikan aksi menggunakan</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Tidak dapat membuka %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Akses izin berkas dan media diperlukan untuk mengunduh berkas. Buka pengaturan Android, ketuk perizinan, dan ketuk izinkan.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Batalkan unduhan pribadi?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jika Anda menutup semua tab pada Penjelajahan Pribadi sekarang, %1$s unduhan akan dibatalkan. Yakin akan meninggalkan Penjelajahan Pribadi?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Batalkan unduhan</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Tetap dalam penjelajahan pribadi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..2ce32b7cfe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-is/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Sóttar skrár</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Hlé gert á niðurhali</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Niðurhali lokið</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Niðurhal mistókst</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Niðurhal (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Sækja</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Hætta við</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s getur ekki sótt þessa skráartegund</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Ekki var hægt að opna skrá</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ekkert forrit fannst sem getur opnað %1$s skrár</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Í bið</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Halda áfram</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Hætta við</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Opna</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Reyna aftur</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Loka</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Ljúka aðgerð með því að nota</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ekki tókst að opna %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Heimildir fyrir aðgang að skrám og miðlum þarf til að sækja skrár. Farðu í Android-stillingar, ýttu á heimildir og ýttu á að leyfa.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Hætta við einkaniðurhal?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ef þú lokar öllum huliðsgluggum, þá verður hætt við %1$s niðurhal. Ertu viss um að þú viljir hætta í huliðsvafri?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Hætta við niðurhal</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Vera áfram í huliðsvafri</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..e73298c2ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-it/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Download</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Download in pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Download completato</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Download non riuscito</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Scarica</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Annulla</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Non è possibile scaricare questo tipo di file in %1$s</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Impossibile aprire il file</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nessuna app trovata per aprire file di tipo %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Riprendi</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Annulla</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Apri</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Riprova</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Chiudi</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completa azione con</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impossibile aprire %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Per scaricare i file è richiesto il permesso di accedere a file e contenuti multimediali. Vai alle impostazioni di Android, tocca Autorizzazioni e successivamente Consenti.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Annullare i download in modalità Navigazione anonima?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Chiudendo tutte le schede in modalità anonima verrà annullato il download di %1$s. Abbandonare la modalità Navigazione anonima?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Annulla i download</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Rimani in modalità Navigazione anonima</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..6b0ef451d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-iw/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">הורדות</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ההורדה הושהתה</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ההורדה הושלמה</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ההורדה נכשלה</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">הורדה (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">הורדה</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ביטול</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">אין באפשרות %1$s להוריד את סוג הקובץ הזה</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">אין אפשרות לפתוח את הקובץ</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">לא נמצא יישומון לפתיחת קובצי %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">השהייה</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">המשך</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ביטול</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">פתיחה</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ניסיון חוזר</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">סגירה</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">השלמת הפעולה באמצעות</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">לא ניתן לפתוח את %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">יש צורך בגישה להרשאה לקבצים ומדיה על מנת להוריד קבצים. יש לעבור אל ההגדרות של Android, להקיש על הרשאות ולהקיש על ״לאפשר״.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">לבטל את ההורדות הפרטיות?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">אם כל הלשוניות הפרטיות ייסגרו כעת, ההורדה של %1$s תבוטל. האם ברצונך לצאת ממצב גלישה פרטית?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ביטול ההורדות</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">להישאר במצב גלישה פרטית</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..59844154cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ja/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ダウンロード一覧</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ダウンロードを一時停止しました</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ダウンロードが完了しました</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ダウンロードに失敗しました</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ダウンロード (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ダウンロード</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">キャンセル</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s はこのファイルの種類をダウンロードできません</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ファイルを開けませんでした</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ファイルを開けるアプリが見つかりません</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">一時停止</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">再開</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">キャンセル</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">開く</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">再試行</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">閉じる</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">使用するアプリを選択</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s を開けません</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ファイルをダウンロードするにはファイルとメディアへのアクセス許可が必要です。Android のアプリの設定を開き、[権限] をタップし、[許可] をタップしてください。</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">プライベートダウンロードをキャンセルしますか?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">すべてのプライベートウィンドウを今すぐ閉じると、%1$s ファイルのダウンロードがキャンセルされます。プライベートブラウジングモードを終了してもよろしいですか?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ダウンロードをキャンセル</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">プライベートブラウジングを継続</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..ec729a5e36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ka/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ჩამოტვირთვები</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ჩამოტვირთვა შეჩერებულია</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ჩამოტვირთვა დასრულდა</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ჩამოტვირთვა ვერ მოხერხდა</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ჩამოტვირთვა (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ჩამოტვირთვა</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">გაუქმება</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ვერ ჩამოტვირთავს ამ სახის ფაილს</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ფაილის გახსნა ვერ ხერხდება</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">არ მოიძებნა აპი, რომლითაც გაიხსნება %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">შეჩერება</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">გაგრძელება</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">გაუქმება</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">გახსნა</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ხელახლა</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">დახურვა</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">მოქმედების შესასრულებლად გამოიყენება</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">ვერ გაიხსნა %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ფაილების ნებართვებთან წვდომაა საჭირო, ჩამოსატვირთად. გადადით Android-ის პარამეტრებში, შეეხეთ ნებართვებს და შემდეგ დაშვებას.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">გაუქმდეს ყველა პირადი ჩამოტვირთვა?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">თუ პირადი დათვალიერების ყველა ფანჯარას დახურავთ, %1$s ჩამოტვირთვა გაუქმდება. ნამდვილად გსურთ პირადი დათვალიერების დატოვება?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ჩამოტვირთვების გაუქმება</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">პირად ფანჯარაში დარჩენა</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..0be25ed38f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Júklengenler</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Júklew pauzalandı</string>
+
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Júklew ámelge asırıldı</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Júklew sátsiz boldı</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Júklew (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Júklew</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Biykarlaw</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s bul fayl túrin júkley almadı</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Fayldı ashıw múmkin bolmadı</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s faylların ashatuǵın baǵdarlama tabılmadı</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pauza</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Qayta baslaw</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Biykarlaw</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ashıw</string>
+
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Qayta urınıp kóriw</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Jabıw</string>
+
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Háreketti tómendegiden paydalanıp juwmaqlaw</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ashıw múmkin emes</string>
+
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Fayllardı júklep alıw ushın fayllar hám medialarǵa kiriwge ruqsat kerek. Android sazlawlarına ótiń, ruqsatlar bólimin tańlań hám ruqsat beriw túymesin basıń.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Jeke júklemelerdi biykarlayıq pa?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Eger házir barlıq jeke betlerdi jawsańız, %1$sdı júklew biykarlanadı. Jeke kóriwden shıǵıwǵa isenimińiz kámil me?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Júklemelerdi biykarlaw</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Jeke kóriwde qalıw</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..6d7c286752
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kab/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Isidar</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Asader yesteɛfa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Asader yemmed</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Ifuyla ittwazedmen</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Asader (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Sader</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Sefsex</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ur izmir ara ad d-isader anaw-a n yifuyla</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Ur yezmir ara ad yeldi afaylu</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ulac asnas yettwafen i twaledyawt n yifuyla %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Seṛǧu</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Kemmel</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Sefsex</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ldi</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Ɛreḍ tikelt nniḍen</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Mdel</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Fak tigawt s uqeqdec</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ur yizmir ara ad yeldi %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tisirag n unekcum ɣer yifuyla d yimidyaten ttusrant i usader n yufuyla. Rzu ɣer yiɣewwaren n Android, fren tisirag syen sit ɣef sireg.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Sefsex isidar usligen?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ma tmedleḍ akk accaren usligen tura, %1$s n uzdam ad iţwasefsex. Tebɣiḍ aţefɣeḍ si tunigin tusligt?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Sefsex isadaren</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Qqim deg tunigin tusligt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..a7557a17cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kk/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Жүктемелер</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Жүктеп алу аялдатылды</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Жүктеп алу аяқталды</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Жүктеп алу сәтсіз аяқталды</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Жүктеп алу (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Жүктеп алу</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Бас тарту</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s бұл файл түрін жүктей алмайды</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Файлды ашу мүмкін емес</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s файлдарын ашатын қолданба табылмады</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Аялдату</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Жалғастыру</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Бас тарту</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ашу</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Қайтадан көру</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Жабу</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Әрекетті келесіні қолданып аяқтау</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ашу мүмкін емес</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Файлдарды жүктеп алу үшін файлдар мен медиа рұқсаты қажет. Android баптауларына өтіп, рұқсаттарды шертіңіз және рұқсат етуді таңдаңыз.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Жекелік жүктемелерден бас тарту керек пе?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Егер қазір барлық жекелік шолу беттерін жапсаңыз, %1$s жүктемеден бас тартылады. Жекелік шолу режимінен шығуды шынымен қалайсыз ба?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Жүктемелерден бас тарту</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Жекелік шолу режимінде қалу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..26bcf428f1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Daxistinên te</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Daxistin hate sekinandin</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Daxistin qediya</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Daxistin bi ser neket</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Daxîne (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Daxîne</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Betal bike</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s nikare vê cureya dosyeyê daxîne</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Dosye nehate vekirin</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Sepana ku karibe dosyeyên %1$s’ê veke, nehate dîtin</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Bisekinîne</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Bidomîne</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Betal bike</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Veke</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Dîsa biceribîne</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Bigire</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Çalakiyê bi vê temam bike</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s nayê vekirin</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Ji bo jêbarkirina pelan pêdivî bi destûra pelan û ya medyayê heye. Here beşa sazkariyên Androîdê, li ser destûran bitikîne û destûrdanê bitikîne.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Daxistinên taybet betal bikî?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Heke tu hemû hilpekînên taybet niha bigirî, %1$s jêbarkirin dê bên betalkirin. Tu ji dil dixwazî ji gera taybet derkevî?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Jêbarkirinan betal bike</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Di gera taybet de bimîne</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..03ae4d90ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kn/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ಡೌನ್‌ಲೋಡ್‌ಗಳು</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ಡೌನ್‌ಲೋಡ್ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ಡೌನ್‌ಲೋಡ್‌ ಪೂರ್ಣಗೊಂಡಿದೆ</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ಡೌನ್‌ಲೋಡ್ ವಿಫಲವಾಗಿದೆ</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ಡೌನ್‌ಲೋಡ್</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ರದ್ದು ಮಾಡು</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ಈ ಫೈಲ್ ಪ್ರಕಾರವನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ಫೈಲ್ ತೆರೆಯಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ಫೈಲ್‌ಗಳನ್ನು ತೆರೆಯಲು ಯಾವುದೇ ಅಪ್ಲಿಕೇಶನ್ ಕಂಡುಬಂದಿಲ್ಲ</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">ವಿರಾಮ</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ಪುನರಾರಂಭಿಸು</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ರದ್ದು ಮಾಡು</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ತೆರೆ</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ಇನ್ನೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸಿ</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">ಮುಚ್ಚು</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..84b62db1fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ko/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">다운로드</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">다운로드 일시 중지됨</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">다운로드 완료</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">다운로드 실패</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">다운로드 (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">다운로드</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">취소</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s는 이 파일 형식은 다운로드 할 수 없습니다</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">파일을 열 수 없습니다</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s 파일을 열 앱이 없습니다</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">일시 중지</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">계속</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">취소</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">열기</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">다시 시도</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">닫기</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">다음 앱을 사용하여 작업 완료</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s 앱을 열 수 없음</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">파일을 다운로드하려면 파일 및 미디어 권한 액세스가 필요합니다. Android 설정으로 이동하여 권한을 누르고 허용을 누르세요.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">사생활 보호 다운로드를 취소하시겠습니까?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">모든 사생활 보호 탭을 닫으면, %1$s개의 다운로드가 취소됩니다. 정말 사생활 보호 모드에서 나가시겠습니까?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">다운로드 취소</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">사생활 보호 모드 계속하기</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..17882b46bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lij/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descaregamenti</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descaregamento in pösa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Descaregamento finio</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Descaregamento falio</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Descaregamento (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Descarega</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Anulla</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">No l\'é poscibile scaregâ sto tipo de schedaio in %1$s</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No pòsso arvî o schedaio</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nisciunn-a app pe arvî schedai do tipo %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pösa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Repiggia</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Anulla</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Arvi</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Preuva torna</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Særa</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..7cf44e4f42
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lo/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ດາວໂຫລດ</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ການດາວໂຫລດໄດ້ຢຸດຊົ່ວຄາວແລ້ວ</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ການດາວໂຫລດສຳເລັດແລ້ວ</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ການດາວໂຫລດລົ້ມເຫລວ</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ດາວໂຫລດ (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ດາວໂຫລດ</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ຍົກເລີກ</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ບໍ່ສາມາດດາວໂຫລດເອກະສານປະເພດນີ້ໄດ້</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ບໍ່ສາມາດເປີດໄຟລໄດ້</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">ບໍ່ພົບແອັບທີ່ຈະໃຊ້ເປີດໄຟລ %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">ຢຸດ</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ດຳເນີນການຕໍ່</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ຍົກເລີກ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ເປີດ</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ຫລອງໃຫມ່ອີກຄັ້ງ</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">ປິດ</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ດຳເນີນການໂດຍໃຊ້</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">ບໍ່ສາມາດເປີດ %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ຈຳເປັນຕ້ອງມີສິດການເຂົ້າໄປຫາໄຟລ ແລະ ມີເດຍ ເພື່ອດາວໂຫລດໄຟລເຫລົ້ານັ້ນ. ໄປ​ທີ່​ການ​ຕັ້ງ​ຄ່າ Android​, ແຕະໃສ່ສິດອະນຸຍາດ, ແລະ​ ແຕະໃສ່​ອະ​ນຸ​ຍາດ​​.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">ຍົກເລີກການດາວໂຫຼດສ່ວນຕົວບໍ?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ຖ້າທ່ານປິດແທັບສ່ວນຕົວທັງໝົດດຽວນີ້, ການດາວໂຫຼດ %1$s ຈະຖືກຍົກເລີກ. ທ່ານແນ່ໃຈແລ້ວບໍ່ວ່າຕ້ອງການອອກຈາກການທ່ອງເວັບແບບສ່ວນຕົວ?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ຍົກເລີກການດາວໂຫຼດ</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">ຢູ່ໃນໂຫມດການທອງເວັບແບບສ່ວນຕົວ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..b564aff148
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lt/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Atsiuntimai</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Atsiuntimas pristabdytas</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Atsiuntimas baigtas</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Atsiųsti nepavyko</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Atsiuntimas („%1$s“)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Atsiųsti</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Atsisakyti</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">„%1$s“ negali atsiųsti šio tipo failų</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Nepavyko atverti failo</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nerasta programų, galinčių atverti %1$s tipo failus</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pristabdyti</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Tęsti</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Atsisakyti</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Atverti</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Bandyti dar kartą</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Užverti</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Užbaigti veiksmą naudojant</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nepavyko atverti „%1$s“</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Norint parsiųsti failus, reikia leidimo prie failų ir medijos. Eikite į „Android“ nustatymus, bakstelėkite leidimus, ir bakstelėkite leisti.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Atsisakyti privačių atsiuntimų?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jei dabar užversite visas privačiojo naršymo korteles, bus nutrauktas %1$s atsiuntimas. Ar tikrai norite išeiti iš privačiojo naršymo seanso?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Atsisakyti atsiuntimų</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Tęsti privatųjį naršymą</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..ccf2f57281
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-mix/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Snuù</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Snuù (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Snuù</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Kunchatu</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Kuna</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Kasi</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..6f5347ef00
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ml/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ഡൗണ്‍ലോഡുകള്‍</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ഡൗൺലോഡ് താൽക്കാലികമായി നിർത്തി</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ഡൗണ്‍ലോഡ് പൂര്‍ത്തിയായിരിക്കുന്നു</string>
+
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ഡൗൺലോഡ് പരാജയപ്പെട്ടു</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ഡൗൺലോഡ് ചെയ്യുക (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ഡൗണ്‍ലോഡ്</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">റദ്ദാക്കുക</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s - ഈ ഫയൽ തരം ഡൗൺലോഡ് ചെയ്യാൻ സാധ്യമല്ല</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ഫയൽ തുറക്കാൻ സാധിച്ചില്ല</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ഫയലുകൾ തുറക്കുന്നതിന് ആപ്പുകളൊന്നും കണ്ടെത്തിയില്ല</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">തൽക്കാലം നിര്‍ത്തുക</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">തുടരുക</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">റദ്ദാക്കുക</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">തുറക്കുക</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">വീണ്ടും ശ്രമിയ്ക്കുക</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">അടയ്ക്കുക</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..6baf608c96
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-mr/strings.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">डाउनलोड</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">डाउनलोड स्थगित</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">डाउनलोड पूर्ण</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">डाउनलोड अयशस्वी</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">डाउनलोड (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">डाउनलोड</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">रद्द</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ह्या प्रकारच्या फाईल डाउनलोड करू शकत नाही</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">दस्तावेज उघडू शकले नाही</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s फाईल उघडण्यासाठी कुठलेही अॅप सापडले नाही</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">विराम</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">पुन्हा सुरू करा</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">रद्द करा</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">उघडा</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">पुन्हा प्रयत्न करा</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">बंद करा</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">वापरून कृती पूर्ण करा</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s उघडण्यात अक्षम</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..88db33e437
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-my/strings.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ဆွဲချချက်များ</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ဒေါင်းလုပ်ခေတ္တရပ်နားသည်</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ဆွဲချချက်များ ပြီးသွားပြီ</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ဆွဲယူမှု မအောင်မြင်ပါ</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ဆွဲယူပါ (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ဆွဲယူပါ</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ပယ်​ဖျက်ပါ</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s သည်ဒီဖိုင်အမျိုးအစားကို မဆွဲယူနိုင်ပါ။</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ဖိုင်ဖွင့်၍ မရပါ</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1"> %1$s ဖိုင် ဖွင့်ရန် အက်ပ် မရှိပါ။</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">ခေတ္တရပ်တန့်ပါ</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ဆက်လက်ဆောင်ရွက်ပါ</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ပယ်​ဖျက်ပါ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ဖွင့်ပါ</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ထပ်ကြိုးစားပါ</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">ပိတ်ပါ</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ယခုတာဝန်အား</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s အားဖွင့်လို့မရပါ</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ဖိုင်များကို ဆွဲချက်ချရန် ဖိုင်များနှင့် မီဒီယာ တို့၏ ခွင့်ပြုချက် လိုအပ်သည်။ Android ဆက်တင်များ သို့သွားပါ။ ခွင့်ပြုချက်များကို နှိပ်ပါ။ ပြီးနောက် ခွင့်ပြုပါ ကိုနှိပ်ပါ။</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..ae08db228c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Nedlastinger</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Nedlasting satt på pause</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Nedlasting fullført</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Nedlasting feilet</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Last ned (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Last ned</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Avbryt</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s kan ikke laste ned denne filtypen</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Klarte ikke åpne filen</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ingen app funnet for å åpne %1$s-filer</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Fortsett</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Avbryt</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Åpne</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Prøv på nytt</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Lukk</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Fullfør handling med</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kan ikke å åpne %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tilgang til filer og medier er nødvendig for å laste ned filer. Gå til Android-innstillinger, trykk på tillatelser og trykk på tillat.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Avbryte private nedlastinger?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Hvis du lukker alle privat nettlesing-vinduene nå, vil %1$s-nedlastingen bli avbrutt. Er du sikker på at du vil gå ut av privat nettlesing?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Avbryt nedlastinger</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Fortsett med privat nettlesing</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..08361e125f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">डाउनलोडहरु</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">डाउनलोड रोकियो</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">डाउनलोड पूरा भयो</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">डाउनलोड असफल भयो</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">डाउनलोड (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">डाउनलोड</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">रद्द गर्नुहोस्</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ले यो फाइलको प्रकार डाउनलोड गर्न सक्दैन</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">यो फाइल खोल्न सकिएन</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s फाइलहरू खोल्न कुनै एप फेला परेनन्</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">रोक्नुहोस्</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">पुनः सुचारु गर्नुहोस्</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">रद्द गर्नुहोस्</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">खोल्नुहोस्</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">पुनः प्रयास गर्नुहोस्</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">बन्द गर्नुहोस्</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">प्रयोग गरेर कार्य सम्पन्न गर्नुहोस्</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s खोल्न सकिएन</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">फाइलहरू डाउनलोड गर्न फाइलहरू र मिडिया अनुमति पहुँच आवश्यक छ। एण्ड्रोइड सेटिङ्गहरूमा जानुहोस्, अनुमति ट्याप गर्नुहोस्, र अनुमति ट्याप गर्नुहोस्।</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">निजी डाउनलोडहरू रद्द गर्ने हो?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">यदि तपाईंले अहिले सबै निजी ट्याबहरू बन्द गर्नुभयो भने, %1$s डाउनलोड रद्द हुनेछ। के तपाइँ निजी ब्राउजिङ छोड्न निश्चित हुनुहुन्छ?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">डाउनलोडहरू रद्द गर्नुहोस्</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">निजी ब्राउजिङ्गमै बस्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..48fe33f965
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nl/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Downloaden gepauzeerd</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Downloaden voltooid</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Downloaden mislukt</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Downloaden (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Downloaden</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Annuleren</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s kan dit bestandstype niet downloaden</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Kan bestand niet openen</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Geen app gevonden om %1$s-bestanden mee te openen</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pauzeren</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Hervatten</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Annuleren</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Openen</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Opnieuw proberen</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Sluiten</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Handeling voltooien met</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kan %1$s niet openen</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Toegang tot bestanden en media vereist om bestanden te downloaden. Ga naar Android-instellingen, tik op machtigingen en tik op toestaan.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Privédownloads annuleren?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Als u nu alle privétabbladen sluit, zal %1$s download worden geannuleerd. Weet u zeker dat u Privénavigatie wilt verlaten?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Downloads annuleren</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">In privénavigatie blijven</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..2083b1535c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Nedlastingar</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Nedlasting sett på pause</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Nedlasting fullført</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Nedlasting feila</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Last ned (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Last ned</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Avbryt</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s kan ikkje laste ned denne filtypen</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Klarte ikkje å opne fila</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ingen app funnen for å opne %1$s-filer</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Fortset</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Avbryt</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Opne</string>
+
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Prøv på nytt</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Lat att</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Fullfør handlinga med</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Klarte ikkje å opne %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tilgang til filer og medium er nødvendig for å laste ned filer. Gå til Android-innstillingar, trykk på løyve (tillatelser) og trykk på tillat.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Avbryte private nedlastingar?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Om du lèt att alle private faner no, blir %1$s-nedlastinga avbroten. Er du sikker på at du vil gå ut av privat nettlesing?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Avbryt nedlastingar</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Fortset med privat nettlesing</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..07aa0b35b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-oc/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Telecargaments</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Lo telecargament es en pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Telecargament acabat</string>
+
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Lo telecargament a fracassat</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Telecargar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Telecargar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Anullar</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s pòt pas telecargar aqueste tipe de fichièr</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Dubertura impossibla del fichièr</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Cap d’aplicacions per dobrir los fichièrs %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Reprendre</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Anullar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Dobrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Tornar ensajar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Tampar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Contunhar l‘accion amb</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Dobertura impossibla %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Las autorizacion d’accès a l’emmagazinatge de fichièrs e dels mèdias son necessàrias per telecargar de fichièrs. Anatz als paramètres d’Android, seleccionatz Autorizacions puèi Autorizar.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Anullar los telecargaments privats ?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se sortissètz ara del mòde de navegacion privada, anullarà %1$s telecargaments. Volètz vertadièrament sortir del mòde de navegacion privada ?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Anullar los telecargaments</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Demorar dins lo mòde de navegacion privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..58b4eedd5b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-or/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ଡାଉନଲୋଡ଼ସମୂହ</string>
+
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ଆହରଣ ସମ୍ପୂର୍ଣ୍ଣ ହୋଇଛି</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ଆହରଣ ବିଫଳ ହେଲା</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ଡାଉନଲୋଡ଼(%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ଡାଉନଲୋଡ଼</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ବାତିଲ କରନ୍ତୁ</string>
+
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ବାତିଲ କରନ୍ତୁ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ଖୋଲନ୍ତୁ</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">ବନ୍ଦ କରନ୍ତୁ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..3ca780189f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ਡਾਊਨਲੋਡ</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ਡਾਉਨਲੋਡ ਰੁਕ ਗਿਆ</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ਡਾਊਨਲੋਡ ਪੂਰਾ ਹੋਇਆ</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ਡਾਊਨਲੋਡ ਅਸਫ਼ਲ ਹੈ</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">(%1$s) ਡਾਉਨਲੋਡ</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ਡਾਊਨਲੋਡ ਕਰੋ</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ਰੱਦ ਕਰੋ</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ਇਹ ਫਾਈਲ ਕਿਸਮ ਡਾਊਨਲੋਡ ਨਹੀਂ ਕਰ ਸਕਦਾ ਹੈ</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ਫ਼ਾਈਲ ਨੂੰ ਖੋਲ੍ਹਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ਫਾਇਲਾਂ ਖੋਲ੍ਹਣ ਲਈ ਕੋਈ ਐਪ ਨਹੀਂ ਲੱਭੀ</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">ਥੰਮੋ</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ਮੁੜ-ਪ੍ਰਾਪਤ</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ਰੱਦ ਕਰੋ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ਖੋਲ੍ਹੋ</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">ਬੰਦ ਕਰੋ</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ਇਸ ਨਾਲ ਕਾਰਵਾਈ ਪੂਰੀ ਕਰੋ</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ਨੂੰ ਖੋਲ੍ਹਣ ਵਿੱਚ ਅਸਮਰੱਥ</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ਫਾਇਲਾਂ ਡਾਊਨਲੋਡ ਕਰਨ ਲਈ ਫਾਇਲਾਂ ਤੇ ਮੀਡੀਆ ਇਜਾਜ਼ਤ ਪਹੁੰਚ ਦੀ ਲੋੜ ਹੈ। Android ਸੈਟਿੰਗਾਂ ਉੱਤੇ ਜਾਓ, ਇਜਾਜ਼ਤਾਂ ਨੂੰ ਛੂਹੋ ਅਤੇ ਮਨਜ਼ੂਰੀ ਨੂੰ ਛੂਹੋ।</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">ਨਿੱਜੀ ਡਾਊਨਲੋਡਾਂ ਨੂੰ ਰੱਦ ਕਰਨਾ ਹੈ?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ਜੇ ਤੁਸੀਂ ਹੁਣੇ ਸਾਰੀਆਂ ਨਿੱਜੀ ਟੈਬਾਂ ਨੂੰ ਬੰਦ ਕੀਤਾ ਤਾਂ %1$s ਡਾਊਨਲੋਡ ਨੂੰ ਰੱਦ ਕੀਤਾ ਜਾਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਨਿੱਜੀ ਬਰਾਊਜ਼ਿੰਗ ਨੂੰ ਛੱਡਣਾ ਚਾਹੁੰਦੇ ਹੋ?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ਡਾਊਨਲੋਡਾਂ ਨੂੰ ਰੱਦ ਕਰੋ</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">ਨਿੱਜੀ ਬਰਾਊਜ਼ਿੰਗ ਵਿੱਚ ਰਹੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..ef11d19c67
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ڈاؤں‌لوڈ</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ڈاؤں‌لوڈ رک گیا</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ڈاؤں‌لوڈ پورا ہویا</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ڈاؤں‌لوڈ نال غلطی ہو گئی اے</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ڈاؤں‌لوڈ کرن (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">آہو، ڈاؤں‌لوڈ کرو</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">رد کرو</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">ایہہ فائل دی قسم %1$s ڈاؤں‌لوڈ کر نہیں سکدی</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">فائل کھولھ نہیں سکدی</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s فائلاں کھولھݨ لئی کوئی ایپ نہیں لبھی</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">روکو</string>
+
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">پھر جاری کرو</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">رد کرو</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">کھولھو</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">فیر کرو</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">بند کرو</string>
+
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">کیہنوں نال کم کرو</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ایپ کھولھ نہیں سکدی</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ڈاؤں‌لوڈ کرن لئی فائلاں دی اجازت لوڑدی اے۔ فون دیاں سیٹنگاں نوں جاؤ تے اجازت دیݨ دی چوݨ لاؤ۔</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">نجی ڈاؤں‌لوڈاں نوں رد کرو؟</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">جے ساریاں ٹیباں کیتیاں %1$s فائل دا ڈاؤں‌لوڈ رد کر جاؤگے۔ تسیں پکے او؟</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">رد کرو</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">نجی ورتوں چ رہو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..663b43a376
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pl/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Pobieranie plików</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Wstrzymano pobieranie</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Ukończono pobieranie</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Pobranie się nie powiodło</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Pobieranie (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Pobierz</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Anuluj</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s nie może pobrać pliku tego typu</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Nie można otworzyć pliku</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nie odnaleziono aplikacji zdolnej do otwierania plików %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Wstrzymaj</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Wznów</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Anuluj</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Otwórz</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Spróbuj ponownie</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zamknij</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dokończ za pomocą aplikacji</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nie można otworzyć aplikacji %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Do pobierania plików wymagany jest dostęp do plików i multimediów. Przejdź do ustawień Androida, stuknij „Uprawnienia” i stuknij „Zezwól”.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Czy anulować pobieranie plików?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Zamknięcie wszystkich prywatnych kart teraz spowoduje przerwanie pobierania pliku „%1$s”. Czy na pewno opuścić tryb prywatny?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Anuluj pobieranie</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Pozostań w trybie prywatnym</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..bf941eb32f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Download pausado</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Download concluído</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Download falhou</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Baixar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Baixar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">O %1$s não pode baixar este tipo de arquivo</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Não foi possível abrir o arquivo</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nenhum aplicativo encontrado para abrir arquivos %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausar</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Continuar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Tentar novamente</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Fechar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar ação usando</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Não foi possível abrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">É necessário ter permissão de acesso a arquivos e mídia para baixar arquivos. Abra a configuração do Android, toque em permissões e toque em permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancelar downloads privativos?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se fechar agora todas as abas privativas, %1$s download será cancelado. Tem certeza que quer sair do modo de navegação privativa?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar downloads</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Permanecer na navegação privativa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..0dc5869bb6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Transferências</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Transferência em pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Transferência concluída</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Transferência falhada</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Transferência (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Transferir</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">O %1$s não pode transferir este tipo de ficheiro</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Não foi possível abrir o ficheiro</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Não foi encontrada nenhuma aplicação para abrir ficheiros %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Retomar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancelar</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Abrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Tentar novamente</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Fechar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Concluir ação utilizando</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Não foi possível abrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">A permissão de acesso a ficheiros e media é necessária para transferir ficheiros. Aceda às configurações do Android, toque em permissões e toque em permitir.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancelar transferências privadas?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se fechar todos os separadores Privados agora, %1$s transferências serão canceladas. Tem a certeza de que pretende sair da navegação privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar transferências</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Manter a navegação privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..1990f9a0f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-rm/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Telechargiadas</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Telechargia ruaussa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Telechargiada cumplettada</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Telechargiada betg reussida</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Telechargiar (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Telechargiar</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Interrumper</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s na po betg telechargiar quest tip da datoteca</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Impussibel dad avrir la datoteca</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Chattà nagina app per avrir datotecas %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Cuntinuar</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Interrumper</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Avrir</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Reempruvar</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Serrar</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Cumplettar l\'acziun cun</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impussibel d\'avrir %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Per pudair telechargiar datotecas è la permissiun da pudair acceder a datotecas e medias necessaria. Va als parameters dad Android, smatga sin permissiuns e lura sin permetter.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Interrumper las telechargiadas privatas?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Sche ti serras ussa tut ils tabs privats vegn la telechargiada da %1$s interrutta. Vuls ti propi bandunar il modus privat?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Interrumper las telechargiadas</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Restar en il modus privat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..c6a98c2471
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ro/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Descărcări</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Descărcare trecută în pauză</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Descărcare finalizată</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Descărcare eșuată</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Descărcare (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Descarcă</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Renunță</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s nu poate descărca acest tip de fișier</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Fișierul nu poate fi deschis</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nu s-a găsit nicio aplicație care să deschidă fișierele %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pauză</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Continuă</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Renunță</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Deschide</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Încearcă din nou</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Închide</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Finalizează acțiunea folosind</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s nu poate fi deschis</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..378f1328a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ru/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Загрузки</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Загрузка приостановлена</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Загрузка завершена</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Загрузка не удалась</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Загрузить (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Загрузить</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Отмена</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s не может загрузить этот тип файла</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Не удалось открыть файл</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Не найдено приложений, умеющих открывать файлы %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Приостановить</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Возобновить</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Отменить</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Открыть</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Попробовать снова</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Закрыть</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Завершить действие используя</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Не удалось открыть %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Для загрузки файлов требуется разрешение на доступ к файлам и мультимедиа. Перейдите в системные настройки Android, выберите «Разрешения», затем выберите «Разрешить».</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Отменить приватные загрузки?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Если вы сейчас закроете все приватные вкладки, загрузка %1$s будет отменена. Вы уверены, что хотите выйти из Приватного просмотра?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Отменить загрузки</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Остаться в приватном просмотре</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..88bca32b6e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sat/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ᱰᱟᱣᱱᱞᱚᱰ ᱠᱚ</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ᱰᱟᱣᱱᱞᱚᱰ ᱛᱷᱩᱠᱩᱢ ᱟᱠᱟᱱᱟ</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ᱰᱟᱣᱱᱞᱚᱰ ᱯᱩᱨᱟᱹᱣᱮᱱᱟ</string>
+
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ᱰᱟᱣᱱᱞᱚᱰ ᱰᱤᱜᱟᱹᱣᱮᱱᱟ</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ᱰᱟᱣᱱᱞᱚᱰ (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ᱰᱟᱣᱱᱞᱚᱰ</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ᱵᱟᱹᱰᱨᱟᱹ</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ᱱᱚᱶᱟ ᱞᱮᱠᱟᱱᱟ ᱨᱮᱫ ᱵᱟᱭ ᱰᱟᱣᱱᱞᱚᱰ ᱫᱟᱲᱮᱜ ᱠᱟᱱᱟᱭ</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ᱨᱮᱫ ᱵᱟᱝ ᱠᱷᱩᱞᱟᱹ ᱫᱟᱲᱮᱞᱟᱱᱟ</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ᱨᱮᱫ ᱠᱚ ᱠᱷᱩᱞᱟᱹ ᱞᱟᱹᱜᱤᱫ ᱪᱮᱫ ᱦᱚᱸ ᱮᱯ ᱵᱟᱝ ᱧᱟᱢ ᱞᱟᱱᱟ</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">ᱛᱷᱩᱠᱩᱢ ᱢᱮ</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ᱫᱩᱦᱲᱟᱹ ᱮᱦᱚᱵᱽ</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ᱵᱟᱹᱰᱨᱟᱹ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ᱡᱷᱚᱡᱽ ᱢᱮ</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ᱫᱚᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">ᱵᱚᱸᱫᱚᱭ ᱢᱮ</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ᱠᱟᱹᱢᱤ ᱠᱚ ᱪᱟᱵᱟᱭ ᱢᱮ</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ᱵᱟᱝ ᱠᱷᱩᱞᱟᱹ ᱫᱟᱲᱮᱞᱟᱱᱟ</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ᱨᱮᱫ ᱰᱟᱣᱱᱞᱚᱰ ᱞᱟᱹᱜᱤᱫ ᱨᱮᱫ ᱟᱨ ᱢᱮᱰᱤᱭᱟ ᱯᱟᱹᱨᱢᱤᱥᱚᱱ ᱟᱫᱮᱨ ᱫᱚᱨᱠᱟᱨ ᱾ ᱮᱱᱰᱨᱚᱭᱮᱰ ᱥᱟᱡᱟᱣ ᱛᱮ ᱪᱟᱞᱟᱜ ᱢᱮ, ᱯᱟᱹᱨᱢᱤᱥᱚᱱ ᱨᱮ ᱴᱤᱯᱟᱹᱣ ᱢᱮ, ᱟᱨ ᱮᱢ ᱪᱷᱚ ᱨᱮ ᱴᱤᱯᱟᱹᱣ ᱢᱮ ᱾</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">ᱱᱤᱡᱚᱨᱟᱜ ᱰᱟᱣᱱᱞᱚᱰ ᱠᱚ ᱵᱟᱹᱰᱨᱟᱹᱭᱟᱢ ᱥᱮ?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ᱡᱤᱫᱤ ᱟᱢ ᱱᱤᱛ ᱨᱮ ᱡᱷᱚᱛᱚ ᱴᱮᱵᱽ ᱠᱚ ᱵᱚᱸᱫᱚᱭᱟᱢ, %1$s ᱰᱟᱣᱱᱞᱚᱰ ᱠᱚ ᱵᱟᱹᱛᱤᱞᱚᱜᱼᱟ ᱾ ᱟᱢ ᱪᱮᱫ ᱡᱷᱚᱛᱚ ᱞᱮᱠᱷᱟᱛᱮ ᱯᱨᱟᱭᱣᱮᱴ ᱵᱽᱨᱟᱣᱡᱤᱝ ᱟᱲᱟᱜ ᱥᱮᱱᱟᱢ ᱠᱟᱱᱟ ᱥᱮ?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ᱰᱟᱣᱱᱞᱚᱰ ᱠᱚ ᱵᱟᱹᱰᱨᱟᱹᱭ ᱢᱮ</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">ᱯᱨᱟᱭᱣᱮᱴ ᱵᱽᱨᱟᱣᱡᱤᱝ ᱨᱮ ᱛᱟᱦᱮᱸᱱ ᱢᱮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..e4f9e4f188
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sc/strings.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Iscarrigamentos</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Pàusa in s’iscarrigamentu</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Iscarrigamentu cumpletu</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Faddina in s’iscarrigamentu</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Iscarrigamentu (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Iscàrriga</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Annulla</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s non podet iscarrigare custa genia de archìviu</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Impossìbile abèrrere s’archìviu</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nissuna aplicatzione agatada pro abèrrere is archìvios %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pàusa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Avia</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Annulla</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Aberi</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Torra a proare</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Serra</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Cumpleta s’atzione cun</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impossìbile abèrrere %1$s</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Boles annullare totu is iscarrigamentos privados?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si serras immoe totu is ischedas de navigatzione privada, %1$s iscarrigamentu at a èssere annulladu. Seguru chi boles lassare sa navigatzione privada?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Annulla s’iscarrigamentu</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Abarra in sa modalidade de navigatzione privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..2a786c68fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-si/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">බාගැනීම්</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">බාගැනීමේ විරාමයකි</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">බාගැනීම සම්පූර්ණයි</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">බාගැනීමට අසමත්!</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">(%1$s) බාගන්න</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">බාගන්න</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">අවලංගු</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s සඳහා මෙම ගොනු වර්ගය බාගැනීමට නොහැකිය</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ගොනුව ඇරීමට නොහැකිය</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ගොනු විවෘත කිරීමට යෙදුමක් හමු නොවිණි</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">විරාමයක්</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">නැවතත්</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">අවලංගු</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">අරින්න</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">නැවත</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">වසන්න</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">මෙය භාවිතයෙන් සිදු කරන්න</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s විවෘත කළ නොහැකිය</string>
+
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ගොනු බාගැනීම සඳහා ගොනු සහ මාධ්‍ය වෙත ප්‍රවේශ අවසරය වුවමනාය. ඇන්ඩ්‍රොයිඩ් සැකසුම් වෙත ගොස්, අවසර -&gt; ඉඩ දෙන්න තට්ටු කරන්න.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">පෞද්. බාගැනීම් අවලංගු කරන්නද?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ඔබ දැන් සියළුම පෞද්. පටිති වසා දැමුවහොත්, %1$s බාගැනීම අවලංගු වනු ඇත. ඔබට මෙය හැරයාමට අවශ්‍ය බව විශ්වාසද?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">බාගැනීම අවලංගු</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">පෞද්. පිරික්සීමෙහි රැඳෙන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..ea55916a75
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sk/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Stiahnuté súbory</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Sťahovanie je pozastavené</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Sťahovanie je dokončené</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Sťahovanie zlyhalo</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Stiahnuť (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Stiahnuť</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Zrušiť</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Aplikácia %1$s nedokáže stiahnuť tento typ súboru</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Nepodarilo sa otvoriť súbor</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nenašla sa žiadna aplikácia, ktorá by dokázala otvoriť súbor %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pozastaviť</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Pokračovať</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Zrušiť</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Otvoriť</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Skúsiť znova</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zavrieť</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dokončiť akciu použitím</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nepodarilo sa otvoriť %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Na stiahnutie súborov je potrebné povolenie prístupu k súborom a médiám. Ak ho chcete povoliť, prejdite do sekcie Povolenia v nastaveniach systému Android.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Zrušiť súkromné sťahovanie?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ak teraz zatvoríte všetky súkromné karty, sťahovanie súboru %1$s sa zruší. Naozaj chcete opustiť súkromné prehliadanie?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Zrušiť sťahovanie</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Zostať v režime Súkromné prehliadanie</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..ff8515d020
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-skr/strings.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ڈاؤن لوڈاں</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ڈاؤن لوڈ رک ڳیا</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ڈاؤن لوڈ مکمل تھی ڳیا</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ڈاؤن لوڈ ناکام تھیا</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">ڈاؤن لوڈ(%1$s) </string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ڈاؤن لوڈ</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">منسوخ</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ایہ فائل قسم ڈاؤن لوڈ کائنی کر سڳدا</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">فائل کائنی کھول سڳا</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s فائلاں کھولݨ کیتے کوئی ایپ کائنی لبھی</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">ذرا روکو</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ولدا جاری کرو</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">منسوخ</string>
+
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">کھولو</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ولدا کوشش کرو</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">بند کرو</string>
+
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ایں کوں ورتݨ نال عمل پورا کرو</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s کھولݨ وچ ناکام ریہا</string>
+
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">فائلاں کوں ڈاؤن لوڈ کرݨ کیتے فائلاں تے میڈیا دی اجازت تائیں رسائی دی لوڑ ہے۔ اینڈرائیڈ ترتیباں تے ون٘ڄو، اجازتاں تے انگل پھیرو تے اجازت ݙیوو تے انگل پھیرو۔</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">نجی ڈاؤن لوڈاں منسوخ کروں؟</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">جے تساں ہݨ ساریاں نجی ٹیباں بند کریندے ہو، %1$s ڈوان لوڈ منسوخ کر ݙتی ویسی۔ بھل ا تہاکوں پک ہے جو تساں نجی براؤزنگ چھوڑݨ چاہندے ہو؟</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ڈاؤن لوڈ منسوخ کرو</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">نجی براؤزنگ وچ راہوو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..2477f39b91
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sl/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Prenosi</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Prenos zaustavljen</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Prenos dokončan</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Prenos neuspešen</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Prenesi (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Prenesi</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Prekliči</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ne more prenesti te vrste datoteke</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Datoteke ni bilo mogoče odpreti</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ni aplikacije za odpiranje datotek %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Premor</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Nadaljuj</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Prekliči</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Odpri</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Poskusi znova</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Zapri</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dokončaj dejanje z</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ni mogoče odpreti %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Za prenašanje datotek so potrebna dovoljenja za dostop do datotek in predstavnosti. V nastavitvah sistema Android tapnite Dovoljenja in Dovoli.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Prekliči zasebne prenose?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Če zdaj zaprete vse zasebne zavihke, bo preklican prenos %1$s. Ste prepričani, da želite zapustiti zasebno brskanje?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Prekliči prenose</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ostani v zasebnem brskanju</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..fc96e4b812
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sq/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Shkarkime</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Shkarkim i ndalur</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Shkarkim i plotësuar</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Shkarkim i dështuar</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Shkarkim (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Shkarkoje</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Anuloje</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s s’mund ta shkarkojë këtë lloj kartelash</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">S’hapet dot kartelë</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">S’u gjet aplikacion për hapje kartelash %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Ndale</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Vazhdoje</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Anuloje</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Hape</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Riprovo</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Mbylle</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Plotësoje veprimin duke përdorur</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">S’arrihet të hapet %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Leje mbi kartela dhe media të nevojshme për të shkarkuar kartela. Kaloni te rregullimet e Android-it, prekni Leje dhe prekni Lejoje.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Të anulohen shkarkimet private?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Nëse i mbyllni tani krejt skedat Private, do të anulohet shkarkimi %1$s. Jeni i sigurt se doni të dilni nga mënyra e Shfletimit Privat?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Anuloji shkarkimet</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Qëndro në shfletim privat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..7cd3c3ee6a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sr/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Преузимања</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Преузимање паузирано</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Преузимање завршено</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Преузимање неуспешно</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Преузми (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Преузми</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Откажи</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s не може преузети ову врсту датотеке</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Није могуће отворити датотеку</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Није пронађена апликација за отварање %1$s датотека</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Паузирај</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Настави</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Откажи</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Отвори</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Покушај поново</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Затвори</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Доврши акцију користећи</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Није могуће отворити %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">За преузимање датотека потребан је приступ датотекама и медијима. Идите на Android подешавања, изаберите Дозволе, а затим Дозволи.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Отказати приватна преузимања?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ако затворите све приватне језичке сада, преузимања (укупно %1$s) биће отказана. Да ли сигурно желите напустити приватно прегледање?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Откажи преузимања</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Остани у приватном прегледању</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..73528dccc5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-su/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Undeuran</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Ngundeur direureuhkeun</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Ngundeur anggeus</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Ngundeur gagal</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Undeur (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Undeur</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Bolay</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s teu bisa ngundeur tipe berkas kieu</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Teu bisa muka berkas</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Teu manggih aplikasi pikeun muka berkas %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Reureuh</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Teruskeun</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Bolay</string>
+
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Buka</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Pecakan Deui</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Tutup</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Anggeuskeun peta maké</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Teu bisa muka %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Butuh aksés idin berkas jeung média pikeun ngundeur berkas. Buka setélan Android, toél idin, laju pilih idinan.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Bedokeun ngundeur nyamuni?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Upama anjeun nutup sakabéh tab Nyamuni ayeuna, %1$s undeuran bakal bedo. Yakin rék ninggalkeun Pamaluruhan Nyamuni?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Bolaykeun ngundeur</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Angger dina langlangan nyamuni</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..44bb37134c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Hämtningar</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Hämtning pausad</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Hämtning slutförd</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Hämtningen misslyckades</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Hämta (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Hämta</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Avbryt</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s kan inte ladda ner den här filtypen</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Det gick inte att öppna filen</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ingen app hittades för att öppna %1$s-filer</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pausa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Återuppta</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Avbryt</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Öppna</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Försök igen</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Stäng</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Slutför åtgärden med</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kunde inte öppna %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Åtkomst till filer och media krävs för att ladda ner filer. Gå till Android-inställningar, tryck på behörigheter och tryck på tillåt.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Vill du avbryta privata nedladdningar?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Om du stänger alla privata flikar nu avbryts nedladdningen av %1$s. Är du säker på att du vill lämna privat surfning?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Avbryt nedladdningar</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stanna i privat surfning</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..76a5bfbe6e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ta/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">பதிவிறக்கங்கள்</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">பதிவிறக்கம் இடைநிறுத்தப்பட்டது</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">பதிவிறக்கம் முடிந்தது</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">பதிவிறக்கம் தோல்வியடைந்தது</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">பதிவிறக்கவா (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">பதிவிறக்கு</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">இரத்து செய்</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s இக்கோப்பு வகையைப் பதிவிறக்க இயலவில்லை</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">கோப்பைத் திறக்க முடியவில்லை</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s கோப்புகளைத் திறப்பதற்கான செயலிகள் இல்லை</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">இடைநிறுத்து</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">தொடர்க</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ரத்துசெய்</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">திற</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">மீண்டும் முயற்சிக்கவும்</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">மூடு</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">பயன்படுத்தி செயற்பாட்டை முடி</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s திறக்க முடியவில்லை</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..f335a2e3e8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-te/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">దింపుకోళ్ళు</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">దింపుకోలు నిలిపివేయబడింది</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">దింపుకోలు పూర్తయ్యింది</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">దింపుకోలు విఫలమైంది</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">దింపుకోలు (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">దించుకో</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">రద్దుచేయి</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">ఈ రకపు ఫైలును %1$s దించుకోలేదు</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ఫైలును తెరవలేకపోయాం</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ఫైళ్ళను తెరవడానికి తగ్గ అనువర్తనం కనబడలేదు</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">నిలిపివేయి</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">కొనసాగించు</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">రద్దుచేయి</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">తెరువు</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">మళ్ళీ ప్రయత్నించు</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">మూసివేయి</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">దీన్ని వాడి చర్యను పూర్తిచేయి</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s‌ను తెరవలేకున్నాం</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">అంతరంగిక దింపుకోళ్ళను రద్దుచేయాలా?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">దింపుకోళ్ళను రద్దుచేయి</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">అంతరంగిక విహారణలోనే ఉండు</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..0ba8cc1054
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tg/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Боргириҳо</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Боргирӣ таваққуф карда шуд</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Боргирӣ анҷом ёфт</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Боргирӣ иҷро нашуд</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Боргирӣ (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Боргирӣ кардан</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Бекор кардан</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ин навъи файлро боргирӣ карда наметавонад</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Файлро кушода натавонист</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Ягон барномае барои кушодани файлҳои %1$s ёфт нашуд</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Таваққуф кардан</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Давом додан</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Бекор кардан</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Кушодан</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Аз нав кӯшиш кардан</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Пӯшидан</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Иҷро кардани амал ба воситаи</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s кушода намешавад</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Барои боргирӣ кардани файлҳо иҷозати дастрасӣ ба файлҳо ва расонаҳо лозим аст. Ба танзимоти Android гузаред, иҷозатҳоро ламс кунед ва иҷозат диҳед.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Боргириҳои хусусиро бекор мекунед?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Агар шумо ҳозир ҳамаи равзанаҳои хусусиро пӯшед, боргирии %1$s бекор карда мешавад. Шумо мутмаин ҳастед, ки мехоҳед тамошокунии хусусиро тарк кунед?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Бекор кардани боргириҳо</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Истодан дар тамошокунии хусусӣ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..70f626e69f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-th/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">การดาวน์โหลด</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">การดาวน์โหลดถูกหยุดชั่วคราว</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">การดาวน์โหลดเสร็จสมบูรณ์</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">การดาวน์โหลดล้มเหลว</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">การดาวน์โหลด (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ดาวน์โหลด</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">ยกเลิก</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s ไม่สามารถดาวน์โหลดไฟล์ชนิดนี้</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ไม่สามารถเปิดไฟล์</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">ไม่พบแอปที่เปิดไฟล์ %1$s ได้</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">หยุดชั่วคราว</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">ดาวน์โหลดต่อ</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">ยกเลิก</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">เปิด</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">ลองอีกครั้ง</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">ปิด</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ดำเนินการโดยใช้</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">ไม่สามารถเปิด %1$s ได้</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">จำเป็นต้องมีสิทธิ์เข้าถึงไฟล์และสื่อเพื่อดาวน์โหลดไฟล์ ไปยังการตั้งค่า Android แตะสิทธิอนุญาต แล้วแตะอนุญาต</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">ยกเลิกการดาวน์โหลดส่วนตัว?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">หากคุณปิดแท็บท่องเว็บแบบส่วนตัวทั้งหมดตอนนี้ %1$s การดาวน์โหลดจะถูกยกเลิก คุณแน่ใจหรือไม่ว่าต้องการออกจากการท่องเว็บแบบส่วนตัว?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">ยกเลิกการดาวน์โหลด</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">คงอยู่ในการท่องเว็บแบบส่วนตัว</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..b08a9a75ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tl/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Mga Download</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Naka-pause ang pag-download</string>
+
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Kumpleto na ang download</string>
+
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Nabigo ang pag-download</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Mag-download (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">i-Download</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Kanselahin</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Hindi ma-download ng %1$s ang uri ng file na ito</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Hindi mabuksan ang file</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Walang nahanap na app upang buksan ang %1$s na mga file</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">i-Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Ipagpatuloy</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Kanselahin</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Buksan</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Subukan Muli</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Isara</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Kumpletuhin ang pagkilos gamit ang</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Hindi mabuksan ang %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Kailangan ng pahintulot sa pag-access ng mga file at media upang ma-download ang mga file. Pumunta sa Android settings, pindutin ang permissions, at pindutin ang allow.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">I kansela ang pag download sa Private?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">I kansela and download</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Manatili sa Pribadong pag Browse</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..98e4da0d6f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tr/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">İndirilenler</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">İndirme duraklatıldı</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">İndirme tamamlandı</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">İndirme başarısız oldu</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">İndir (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">İndir</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">İptal</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s bu dosya türünü indiremiyor</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Dosya açılamadı</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s dosyalarını açacak uygulama bulunamadı</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Duraklat</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Sürdür</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">İptal</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Aç</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Yeniden dene</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Kapat</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Eylemi şununla tamamla</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s açılamıyor</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Dosya indirmek için dosyalar ve medya erişimine izin vermeniz gerekiyor. Android ayarlarından izinlere girerek izin verin.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Gizli indirmeler iptal edilsin mi?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Gizli sekmeleri kapatırsanız %1$s indirme işlemi iptal edilecek. Gizli gezintiden çıkmak istediğinize emin misiniz?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">İndirmeleri iptal et</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Gizli gezintiyi sürdür</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..ec98213be7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-trs/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Nej sa nadunïnjt</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Giyi\'chin\' riña sa nadunïnjt</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Ngà gisîj nahuij nadunin</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Nu ga\'ue nādunin</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Nādūnïnj (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Nādunïnj</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Dūyichin\'</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s na\'uēj nādūnïnj archibô huā dānanj</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Nu ga\'ue nāyī\'nin archîbo</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Nitāj aplikasiûn nikājt da\' nā\'nïn riña nej archibô %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Dūnikïn\' akuan\'</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Nāyì\'ì ñû</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Dūyichin\'</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Nā\'nīn</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Ginùn huin ñû</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Nārán</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dūnahuij sa \'iát ngà sa gu\'nàj</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nu ga\’ue nāyi’nïn riña %1$s</string>
+
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Achín nī’hia da’ gātū riña nej ārchîbo nī gā’hue nādunïn nej man. Guīj riña gā’hue nāgi’hiát sa màn riña Android, guru’man ra’a riña tāj sa achín nì’hiaj nī gūru’man ra’a gā’huēj.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Dūyichîn’t nej sa nadunïnj hùi raj.</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Sisī nāránt riña daran’ nej rakïj ñanj riña aché nun huît hìaj nī, nārè sa nadunïnjt riña %1$s. Ruhuâ yāngà’ gāhuīt riña aché nu huìt nan anj.</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Dūyichin’ nej sa nadunï̄njt</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ginu ngè riña aché nun huìt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..c48ffd0286
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tt/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Йөкләп алулар</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Йөкләп алу туктатылды</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Йөкләп алу тәмамланды</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Йөкләп алу уңышсыз тәмамланды</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Йөкләп алу (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Йөкләп алу</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Баш тарту</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s бу файл төрен йөкли алмый</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Файлны ачып булмады</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s файлларын ачу өчен кушымта табылмады</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Туктату</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Дәвам итү</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Баш тарту</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ачу</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Янәдән тырышып карау</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Ябу</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Түбәндәгене кулланып гамәлне тәмамлау</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s кушымтасын ачу мөмкин түгел</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Файлларны йөкләп алу өчен файллардан һәм медиадан файдалану рөхсәте кирәк. Android көйләүләренә кереп, &quot;рөхсәтләр&quot; дигән битне ачып, &quot;рөхсәт итү&quot; дигәнгә басыгыз.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Шәхси иңдерүләрдән баш тартырга телисезме?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Йөкләүдән баш тарту</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Хосусый гизү режимында калу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..d690959857
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Agamen</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Agam ibedden</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Agem (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Agem</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">ur izmir %1$s ad d-yagem anaw n ufaylu-a</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Ur izmir ad irẓem afaylu</string>
+
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Rẓem</string>
+
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Arm daɣ</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Mḍel</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ur izmir ad irẓem %1$s</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..62a2546c82
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ug/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">چۈشۈرۈلمىلەر</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">چۈشۈرۈش توختىتىلدى</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">چۈشۈرۈش تاماملاندى</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">چۈشۈرۈش مەغلۇپ بولدى</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">چۈشۈرۈش(%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">چۈشۈرۈش</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">بىكار قىلىش</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s بۇ تىپتىكى ھۆججەتنى چۈشۈرەلمەيدۇ</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">ھۆججەتنى ئاچالمىدى</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$sھۆججىتىنى ئاچىدىغان ئەپ تېپىلمىدى</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">توختىتىش</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">داۋاملاشتۇرۇش</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">بىكار قىلىش</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">ئېچىش</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">قايتا سىناڭ</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">تاقاش</string>
+
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">تۆۋەندىكى ئەپتە مەشغۇلاتنى تاماملايدۇ</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app"> %1$sنى ئاچالمىدى</string>
+
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ھۆججەت چۈشۈرۈشتە ھۆججەت ۋە ۋاسىتە زىيارەت قىلىش ئىجازىتىگە ئېھتىياجلىق. ئاندىرويىد تەڭشەككە يۆتكىلىپ، ھوقۇقنى چېكىپ، يول قوينى چېكىڭ.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">شەخسىي چۈشۈرۈشتىن ۋاز كېچەمدۇ؟</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ئەگەر ھازىر ھەممە شەخسىيەت بەتكۈچنى تاقىسىڭىز، %1$s چۈشۈرۈشتىن ۋاز كېچىدۇ. راستىنلا شەخسىي زىيارەت ھالىتىدىن چېكىنەمسىز؟</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">چۈشۈرۈشنى بىكار قىلىش</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">شەخسىي زىيارەت ھالىتىدە قالدۇر</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..7424f8a852
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-uk/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Завантаження</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Завантаження призупинено</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Завантаження завершено</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Завантаження невдале</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Завантажити (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Завантажити</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Скасувати</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s не може завантажити цей тип файлу</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Не вдається відкрити файл</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Не знайдено програми, щоб відкрити файли %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Призупинити</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Продовжити</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Скасувати</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Відкрити</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Спробувати знову</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Закрити</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Завершити дію за допомогою</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Не вдалося відкрити %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Доступ до файлів та медіафайлів необхідний для завантаження файлів. Перейдіть до налаштувань Android, торкніться дозволів і торкніться дозволити.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Скасувати приватні завантаження?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Якщо ви закриєте всі приватні вкладки, завантаження %1$s буде скасовано. Ви дійсно хочете вийти з режиму приватного перегляду?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Скасувати завантаження</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Залишитись в режимі приватного перегляду</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..2c9abb21b0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ur/strings.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">ڈاؤن لوڈز</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">ڈاؤن لوڈ موقوف ہوا</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">ڈاؤن لوڈ مکمل</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">ڈاؤن لوڈ ناکام</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">(%1$s) ڈاؤن لوڈ کریں</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">ڈاؤن لوڈ</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">منسوخ کریں</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s اس قسم کی فائل کو ڈاؤن لوڈ نہیں کرسکتے ہیں</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">فائل نہیں کھول سکتے</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s فائلوں کو کھولنے کے لئے کوئی ایپ نہیں ملا</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">توقف کریں</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">پھر جاری کریں</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">منسوخ کریں</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">کھولیں</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">دوبارہ کوشش کریں</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">بند کریں</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">عمل مکمل کریں بمع</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s کھولنے میں ناکام رہا</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">نجی ڈاؤن لوڈز کو منسوخ کریں؟</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..97274f3ac5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-uz/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Yuklanmalar</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Yuklanma pauza qilindi</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Yuklab olindi</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Yuklab olinmadi</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Yuklab olish (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Yuklab olish</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Bekor qilish</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Bu turdagi faylni %1$s yuklab olmaydi</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Fayl ochilmadi</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">%1$s ta faylni ochish uchun hech qanday ilova topilmadi</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pauza</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Davom etish</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Bekor qilish</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ochish</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Yana urinish</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Yopish</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Harakatni quyidagidan foydalanib tugatish</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ochib boʻlmadi</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Fayllarni yuklab olish uchun fayllar va mediaga kirish ruxsati kerak. Android sozlamalariga oʻting, ruxsatnomalar va ruxsat berish tugmasini bosing.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Maxfiy yuklanmalar bekor qilinsinmi?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Barcha maxfiy varaqlarni yopsangiz, %1$s ta yuklanma bekor qilinadi. Maxfiy rejimdan chiqishni istaysizmi?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Yuklanmalarni bekor qilish</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Maxfiy rejimda qolaman</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..7dc177ba61
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-vec/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Download en pausa</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Download conpletà</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Download falìo</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Download</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Anuƚa</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">Non xe posibiƚe scargare sto tipo de file en %1$s</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">No xe posibiƚe vèrxere el file</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pauxa</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Ricomisìa</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Anuƚa</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Vèrxi</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Prova ancora</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Sara su</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..ca208ff304
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-vi/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Tải xuống</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Đã tạm dừng tải xuống</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Đã hoàn tất tải xuống</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Có lỗi khi tải xuống</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Tải xuống (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Tải xuống</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Hủy bỏ</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s không thể tải xuống loại tập tin này</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Không thể mở tập tin</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Không tìm thấy ứng dụng nào để mở %1$s</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Tạm dừng</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Tiếp tục</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Hủy bỏ</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Mở</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Thử lại</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Đóng</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Hoàn thành hành động bằng</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Không thể mở %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Cần có quyền truy cập vào tập tin và phương tiện để tải tập tin xuống. Đi tới cài đặt Android, nhấn vào quyền và nhấn cho phép.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Hủy các tải xuống riêng tư?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Nếu bạn đóng tất cả các thẻ riêng tư ngay bây giờ, quá trình tải xuống %1$s sẽ bị hủy. Bạn có chắc chắn muốn thoát khỏi duyệt web riêng tư không?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Hủy tải xuống</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Vẫn ở lại chế độ duyệt web riêng tư</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..dc2831d64f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-yo/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Àwọn ìgbàsílẹ̀</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Ìdádúró ráńpẹ́ fún ìgbàsílẹ̀</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Ìgbàsílẹ̀ ti parí</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Ìgbàsílẹ̀ kùnà</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Ìgbàsílẹ̀ (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Ṣe ìgbàsílẹ̀</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Parẹ́</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s kò le ṣe ìgbàsílẹ̀ fún irúfẹ́́ fáìlì yí</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Kò le ṣí fáìlì</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">Kò ṣàwárí áàpù tí a lè fi ṣí %1$s fáìlì</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Ìdádúró ráńpẹ́</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Tún bẹ̀rẹ̀</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Parẹ́</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Ṣí</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Gbìyànjú Si </string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Padé</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Parí ìgbésẹ̀ pẹ̀lú lílo</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kò ṣe é sí %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Ó nílò ìgbàláàyè fáìlì àti ìkànnì láti ṣe ìgbàsílẹ̀ àwọn fáìlì. Lọ sí orí àwọn ètò Áńdíróìdì, tẹ àwọn ìgbàláàyè, ko sì tẹ gbà láàyè.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Pa àwọn ìgbàsílẹ̀ ìkọ̀kọ̀ rẹ́?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Tí o bá pa gbogbo àwọn táàbù ìkọ̀kọ̀ báyìí, %1$s ìgbàsílẹ̀ yóó paarẹ́. Ṣe ó dá ọ lójú pé o fẹ́ fi àyẹ̀wò ìkọ̀kọ̀ re kalẹ̀?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Pa àwọn ìgbàsílẹ̀ rẹ́</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Dúró sí àyẹ̀wò ìkọ̀kọ̀</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..fd5ce5ba85
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">下载</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">下载已暂停</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">下载完毕</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">下载失败</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">下载(%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">下载</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">取消</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s 无法下载此种文件类型</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">无法打开文件</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">找不到可用于打开 %1$s 文件的应用程序</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">暂停</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">继续</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">取消</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">打开</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">重试</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">关闭</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">使用下列应用完成操作</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">无法打开 %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">下载文件需要文件和媒体访问权限。请前往 Android 设置,点按“权限”,并选择“允许”。</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">要取消隐私下载吗?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">若您现在关闭所有隐私浏览标签页,%1$s 个下载将被取消。确定要离开隐私浏览模式吗?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">取消下载</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">留在隐私浏览模式</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..ce88358dfd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">下載項目</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">已暫停下載</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">下載完成</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">下載失敗</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">下載(%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">下載</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">取消</string>
+
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s 無法下載此類檔案</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">無法開啟檔案</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">找不到可用來開啟 %1$s 檔案的應用程式</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">暫停</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">繼續</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">取消</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">開啟</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">再試一次</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">關閉</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">使用下列軟體完成操作</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--&gt;
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">無法開啟 %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">需要有檔案與媒體存取權限才能下載檔案。請到 Android 設定當中的「權限」點擊「允許」。</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">要取消隱私下載項目嗎?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">如果您現在關閉所有隱私瀏覽分頁,將會取消 %1$s 項下載工作,確定要離開隱私瀏覽模式嗎?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">取消下載</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">留在隱私瀏覽模式</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..2c92281f2f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+<?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>
+ <color name="mozac_feature_downloads_notification">#607D8B</color>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..8018b4abee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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>
+ <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_downloads_notification_channel">Downloads</string>
+
+ <!-- Text shown on the second row of a paused download notification. -->
+ <string name="mozac_feature_downloads_paused_notification_text">Download paused</string>
+ <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_completed_notification_text2">Download completed</string>
+ <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. -->
+ <string name="mozac_feature_downloads_failed_notification_text2">Download failed</string>
+
+ <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string>
+ <!-- Alert dialog confirmation before download a file, this is the positive action. -->
+ <string name="mozac_feature_downloads_dialog_download">Download</string>
+ <!-- Alert dialog confirmation before download a file, this is the negative action. -->
+ <string name="mozac_feature_downloads_dialog_cancel">Cancel</string>
+ <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. -->
+ <string name="mozac_feature_downloads_file_not_supported2">%1$s can’t download this file type</string>
+
+ <!-- Message that appears when the downloaded file could not be opened-->
+ <string name="mozac_feature_downloads_could_not_open_file">Could not open file</string>
+
+ <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening-->
+ <string name="mozac_feature_downloads_open_not_supported1">No app found to open %1$s files</string>
+
+ <!-- Button that pauses the download when pressed -->
+ <string name="mozac_feature_downloads_button_pause">Pause</string>
+ <!-- Button that resumes the download when pressed -->
+ <string name="mozac_feature_downloads_button_resume">Resume</string>
+ <!-- Button that cancels the download when pressed -->
+ <string name="mozac_feature_downloads_button_cancel">Cancel</string>
+ <!-- Button that opens the downloaded file when pressed -->
+ <string name="mozac_feature_downloads_button_open">Open</string>
+ <!-- Button that restarts the download after a failed attempt -->
+ <string name="mozac_feature_downloads_button_try_again">Try Again</string>
+
+ <!-- Content description for close button -->
+ <string name="mozac_feature_downloads_button_close">Close</string>
+ <!-- Title for the third party download app chooser dialog -->
+ <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Complete action using</string>
+ <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->-->
+ <string name="mozac_feature_downloads_unable_to_open_third_party_app">Unable to open %1$s</string>
+ <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. -->
+ <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Files and media permission access needed to download files. Go to Android settings, tap permissions, and tap allow.</string>
+
+ <!-- Alert dialog confirmation before cancelling downloads, this is the title -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancel private downloads?</string>
+ <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">If you close all Private tabs now, %1$s download will be canceled. Are you sure you want to leave Private Browsing?</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. -->
+ <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancel downloads</string>
+ <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing -->
+ <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stay in private browsing</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/xml/feature_downloads_file_paths.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/xml/feature_downloads_file_paths.xml
new file mode 100644
index 0000000000..85507b5f6d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/main/res/xml/feature_downloads_file_paths.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<paths>
+ <external-path name="Download" path="." />
+
+ <!-- Offer access only to files under Context.getCacheDir()/media_share_cache/ -->
+ <cache-path name="mediaShareCache" path="mozac_share_cache"/>
+</paths>
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt
new file mode 100644
index 0000000000..f95c4438ad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt
@@ -0,0 +1,2155 @@
+/* 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.downloads
+
+import android.app.DownloadManager
+import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.Service
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.FileProvider
+import androidx.core.content.getSystemService
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED
+import mozilla.components.browser.state.state.content.DownloadState.Status.DOWNLOADING
+import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED
+import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_CANCEL
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_PAUSE
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_REMOVE_PRIVATE_DOWNLOAD
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_RESUME
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_TRY_AGAIN
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.PROGRESS_UPDATE_INTERVAL
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.CopyInChuckStatus.ERROR_IN_STREAM_CLOSED
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState
+import mozilla.components.feature.downloads.DownloadNotification.NOTIFICATION_DOWNLOAD_GROUP_ID
+import mozilla.components.feature.downloads.facts.DownloadsFacts.Items.NOTIFICATION
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+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.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.utils.ext.stopForegroundCompat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doCallRealMethod
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.Implementation
+import org.robolectric.annotation.Implements
+import org.robolectric.shadows.ShadowNotificationManager
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import kotlin.random.Random
+
+@RunWith(AndroidJUnit4::class)
+@Config(shadows = [ShadowFileProvider::class])
+class AbstractFetchDownloadServiceTest {
+
+ @Rule @JvmField
+ val folder = TemporaryFolder()
+
+ // We need different scopes and schedulers because:
+ // - the service will continuously try to update the download notification using MainScope()
+ // - if using the same scope in tests the test won't end
+ // - need a way to advance main dispatcher used by the service.
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val mainDispatcher = coroutinesTestRule.testDispatcher
+ private val testsDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler())
+
+ @Mock private lateinit var client: Client
+ private lateinit var browserStore: BrowserStore
+ private lateinit var notificationManagerCompat: NotificationManagerCompat
+
+ private lateinit var notificationsDelegate: NotificationsDelegate
+
+ private lateinit var service: AbstractFetchDownloadService
+
+ private lateinit var shadowNotificationService: ShadowNotificationManager
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ browserStore = BrowserStore()
+
+ notificationManagerCompat = spy(NotificationManagerCompat.from(testContext))
+ notificationsDelegate = NotificationsDelegate(notificationManagerCompat)
+ service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ doReturn(testContext).`when`(service).context
+ doNothing().`when`(service).useFileStream(any(), anyBoolean(), any())
+ doReturn(true).`when`(notificationManagerCompat).areNotificationsEnabled()
+
+ shadowNotificationService =
+ shadowOf(testContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
+ }
+
+ @Test
+ fun `begins download when started`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ assertEquals(download.url, providedDownload.value.state.url)
+ assertEquals(download.fileName, providedDownload.value.state.fileName)
+
+ // Ensure the job is properly added to the map
+ assertEquals(1, service.downloadJobs.count())
+ assertNotNull(service.downloadJobs[providedDownload.value.state.id])
+ }
+
+ @Test
+ fun `WHEN a download intent is received THEN handleDownloadIntent must be called`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+
+ doNothing().`when`(service).handleRemovePrivateDownloadIntent(any())
+ doNothing().`when`(service).handleDownloadIntent(any())
+
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ verify(service).handleDownloadIntent(download)
+ verify(service, never()).handleRemovePrivateDownloadIntent(download)
+ }
+
+ @Test
+ fun `WHEN an intent does not provide an action THEN handleDownloadIntent must be called`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val downloadIntent = Intent()
+
+ doNothing().`when`(service).handleRemovePrivateDownloadIntent(any())
+ doNothing().`when`(service).handleDownloadIntent(any())
+
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ verify(service).handleDownloadIntent(download)
+ verify(service, never()).handleRemovePrivateDownloadIntent(download)
+ }
+
+ @Test
+ fun `WHEN a try again intent is received THEN handleDownloadIntent must be called`() =
+ runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val downloadIntent = Intent(ACTION_TRY_AGAIN)
+
+ doNothing().`when`(service).handleRemovePrivateDownloadIntent(any())
+ doNothing().`when`(service).handleDownloadIntent(any())
+
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+ val newDownloadState = download.copy(status = DOWNLOADING)
+ browserStore.dispatch(DownloadAction.AddDownloadAction(newDownloadState)).joinBlocking()
+
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ verify(service).handleDownloadIntent(newDownloadState)
+ assertEquals(newDownloadState.status, DOWNLOADING)
+ verify(service, never()).handleRemovePrivateDownloadIntent(newDownloadState)
+ }
+
+ @Test
+ fun `WHEN a remove download intent is received THEN handleRemoveDownloadIntent must be called`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val downloadIntent = Intent(ACTION_REMOVE_PRIVATE_DOWNLOAD)
+
+ doNothing().`when`(service).handleRemovePrivateDownloadIntent(any())
+ doNothing().`when`(service).handleDownloadIntent(any())
+
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ verify(service).handleRemovePrivateDownloadIntent(download)
+ verify(service, never()).handleDownloadIntent(download)
+ }
+
+ @Test
+ fun `WHEN handleRemovePrivateDownloadIntent with a private download is called THEN removeDownloadJob must be called`() {
+ val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = true)
+ val downloadJobState = DownloadJobState(state = downloadState, status = COMPLETED)
+ val browserStore = mock<BrowserStore>()
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ doAnswer { }.`when`(service).removeDownloadJob(any())
+
+ service.downloadJobs[downloadState.id] = downloadJobState
+
+ service.handleRemovePrivateDownloadIntent(downloadState)
+
+ verify(service, times(0)).cancelDownloadJob(downloadJobState)
+ verify(service).removeDownloadJob(downloadJobState)
+ verify(browserStore).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id))
+ }
+
+ @Test
+ fun `WHEN handleRemovePrivateDownloadIntent is called with a private download AND not COMPLETED status THEN removeDownloadJob and cancelDownloadJob must be called`() {
+ val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = true)
+ val downloadJobState = DownloadJobState(state = downloadState, status = DOWNLOADING)
+ val browserStore = mock<BrowserStore>()
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ doAnswer { }.`when`(service).removeDownloadJob(any())
+
+ service.downloadJobs[downloadState.id] = downloadJobState
+
+ service.handleRemovePrivateDownloadIntent(downloadState)
+
+ verify(service).cancelDownloadJob(downloadJobState)
+ verify(service).removeDownloadJob(downloadJobState)
+ verify(browserStore).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id))
+ }
+
+ @Test
+ fun `WHEN handleRemovePrivateDownloadIntent is called with with a non-private (or regular) download THEN removeDownloadJob must not be called`() {
+ val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = false)
+ val downloadJobState = DownloadJobState(state = downloadState, status = COMPLETED)
+ val browserStore = mock<BrowserStore>()
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ doAnswer { }.`when`(service).removeDownloadJob(any())
+
+ service.downloadJobs[downloadState.id] = downloadJobState
+
+ service.handleRemovePrivateDownloadIntent(downloadState)
+
+ verify(service, never()).removeDownloadJob(downloadJobState)
+ verify(browserStore, never()).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id))
+ }
+
+ @Test
+ fun `service redelivers if no download extra is passed `() = runTest(testsDispatcher) {
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+
+ val intentCode = service.onStartCommand(downloadIntent, 0, 0)
+
+ assertEquals(Service.START_REDELIVER_INTENT, intentCode)
+ }
+
+ @Test
+ fun `verifyDownload sets the download to failed if it is not complete`() = runTest(testsDispatcher) {
+ val downloadState = DownloadState(
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 50L,
+ currentBytesCopied = 5,
+ status = DOWNLOADING,
+ )
+
+ val downloadJobState = DownloadJobState(
+ job = null,
+ state = downloadState,
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 5,
+ status = DOWNLOADING,
+ )
+
+ service.verifyDownload(downloadJobState)
+
+ assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
+ verify(service).setDownloadJobStatus(downloadJobState, FAILED)
+ verify(service).updateDownloadState(downloadState.copy(status = FAILED))
+ }
+
+ @Test
+ fun `verifyDownload does NOT set the download to failed if it is paused`() = runTest(testsDispatcher) {
+ val downloadState = DownloadState(
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 50L,
+ currentBytesCopied = 5,
+ status = DownloadState.Status.PAUSED,
+ )
+
+ val downloadJobState = DownloadJobState(
+ job = null,
+ state = downloadState,
+ currentBytesCopied = 5,
+ status = DownloadState.Status.PAUSED,
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ )
+
+ service.verifyDownload(downloadJobState)
+
+ assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState))
+ verify(service, times(0)).setDownloadJobStatus(downloadJobState, DownloadState.Status.FAILED)
+ verify(service, times(0)).updateDownloadState(downloadState.copy(status = DownloadState.Status.FAILED))
+ }
+
+ @Test
+ fun `verifyDownload does NOT set the download to failed if it is complete`() = runTest(testsDispatcher) {
+ val downloadState = DownloadState(
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 50L,
+ currentBytesCopied = 50,
+ status = DOWNLOADING,
+ )
+
+ val downloadJobState = DownloadJobState(
+ job = null,
+ state = downloadState,
+ currentBytesCopied = 50,
+ status = DOWNLOADING,
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ )
+
+ service.verifyDownload(downloadJobState)
+
+ assertNotEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
+ verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED)
+ verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED))
+ }
+
+ @Test
+ fun `verifyDownload does NOT set the download to failed if it is cancelled`() = runTest(testsDispatcher) {
+ val downloadState = DownloadState(
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 50L,
+ currentBytesCopied = 50,
+ status = DownloadState.Status.CANCELLED,
+ )
+
+ val downloadJobState = DownloadJobState(
+ job = null,
+ state = downloadState,
+ currentBytesCopied = 50,
+ status = DownloadState.Status.CANCELLED,
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ )
+
+ service.verifyDownload(downloadJobState)
+
+ assertNotEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
+ verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED)
+ verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED))
+ }
+
+ @Test
+ fun `verifyDownload does NOT set the download to failed if it is status COMPLETED`() = runTest(testsDispatcher) {
+ val downloadState = DownloadState(
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 50L,
+ currentBytesCopied = 50,
+ status = DownloadState.Status.COMPLETED,
+ )
+
+ val downloadJobState = DownloadJobState(
+ job = null,
+ state = downloadState,
+ currentBytesCopied = 50,
+ status = DownloadState.Status.COMPLETED,
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ )
+
+ service.verifyDownload(downloadJobState)
+
+ verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED)
+ verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED))
+ }
+
+ @Test
+ fun `verify that a COMPLETED download contains a file size`() {
+ val downloadState = DownloadState(
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 0L,
+ currentBytesCopied = 50,
+ status = DOWNLOADING,
+ )
+ val downloadJobState = DownloadJobState(
+ job = null,
+ state = downloadState,
+ currentBytesCopied = 50,
+ status = DOWNLOADING,
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ )
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(downloadState)).joinBlocking()
+ service.downloadJobs[downloadJobState.state.id] = downloadJobState
+ service.verifyDownload(downloadJobState)
+ browserStore.waitUntilIdle()
+
+ assertEquals(downloadJobState.state.contentLength, service.downloadJobs[downloadJobState.state.id]!!.state.contentLength)
+ assertEquals(downloadJobState.state.contentLength, browserStore.state.downloads.values.first().contentLength)
+ }
+
+ @Test
+ fun `broadcastReceiver handles ACTION_PAUSE`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ val pauseIntent = Intent(ACTION_PAUSE).apply {
+ setPackage(testContext.applicationContext.packageName)
+ putExtra(DownloadNotification.EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
+ }
+
+ CollectionProcessor.withFactCollection { facts ->
+ service.broadcastReceiver.onReceive(testContext, pauseIntent)
+
+ val pauseFact = facts[0]
+ assertEquals(Action.PAUSE, pauseFact.action)
+ assertEquals(NOTIFICATION, pauseFact.item)
+ }
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState))
+ }
+
+ @Test
+ fun `broadcastReceiver handles ACTION_CANCEL`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ val cancelIntent = Intent(ACTION_CANCEL).apply {
+ setPackage(testContext.applicationContext.packageName)
+ putExtra(DownloadNotification.EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
+ }
+
+ assertFalse(service.downloadJobs[providedDownload.value.state.id]!!.downloadDeleted)
+
+ CollectionProcessor.withFactCollection { facts ->
+ service.broadcastReceiver.onReceive(testContext, cancelIntent)
+
+ val cancelFact = facts[0]
+ assertEquals(Action.CANCEL, cancelFact.action)
+ assertEquals(NOTIFICATION, cancelFact.item)
+ }
+ }
+
+ @Test
+ fun `broadcastReceiver handles ACTION_RESUME`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+
+ val downloadResponse = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ val resumeResponse = Response(
+ "https://example.com/file.txt",
+ 206,
+ MutableHeaders("Content-Range" to "1-67589/67589"),
+ Response.Body(mock()),
+ )
+ doReturn(downloadResponse).`when`(client)
+ .fetch(Request("https://example.com/file.txt"))
+ doReturn(resumeResponse).`when`(client)
+ .fetch(Request("https://example.com/file.txt", headers = MutableHeaders("Range" to "bytes=1-")))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ // Simulate a pause
+ var downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ downloadJobState.currentBytesCopied = 1
+ service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)
+
+ service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)
+ service.downloadJobs[providedDownload.value.state.id]?.job?.cancel()
+
+ val resumeIntent = Intent(ACTION_RESUME).apply {
+ setPackage(testContext.applicationContext.packageName)
+ putExtra(DownloadNotification.EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
+ }
+
+ CollectionProcessor.withFactCollection { facts ->
+ service.broadcastReceiver.onReceive(testContext, resumeIntent)
+
+ val resumeFact = facts[0]
+ assertEquals(Action.RESUME, resumeFact.action)
+ assertEquals(NOTIFICATION, resumeFact.item)
+ }
+
+ downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState))
+
+ // Make sure the download job is completed (break out of copyInChunks)
+ service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+
+ verify(service).startDownloadJob(providedDownload.value)
+ }
+
+ @Test
+ fun `broadcastReceiver handles ACTION_TRY_AGAIN`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+
+ // Simulate a failure
+ var downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ service.setDownloadJobStatus(downloadJobState, FAILED)
+ service.downloadJobs[providedDownload.value.state.id]?.job?.cancel()
+
+ val tryAgainIntent = Intent(ACTION_TRY_AGAIN).apply {
+ setPackage(testContext.applicationContext.packageName)
+ putExtra(DownloadNotification.EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
+ }
+
+ CollectionProcessor.withFactCollection { facts ->
+ service.broadcastReceiver.onReceive(testContext, tryAgainIntent)
+
+ val tryAgainFact = facts[0]
+ assertEquals(Action.TRY_AGAIN, tryAgainFact.action)
+ assertEquals(NOTIFICATION, tryAgainFact.item)
+ }
+
+ downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState))
+
+ // Make sure the download job is completed (break out of copyInChunks)
+ service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+
+ verify(service).startDownloadJob(providedDownload.value)
+ }
+
+ @Test
+ fun `download fails on a bad network response`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 400,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
+ }
+
+ @Test
+ fun `makeUniqueFileNameIfNecessary transforms fileName when appending FALSE`() {
+ folder.newFile("example.apk")
+
+ val download = DownloadState(
+ url = "mozilla.org",
+ fileName = "example.apk",
+ destinationDirectory = folder.root.path,
+ )
+ val transformedDownload = service.makeUniqueFileNameIfNecessary(download, false)
+
+ assertNotEquals(download.fileName, transformedDownload.fileName)
+ }
+
+ @Test
+ fun `makeUniqueFileNameIfNecessary does NOT transform fileName when appending TRUE`() {
+ folder.newFile("example.apk")
+
+ val download = DownloadState(
+ url = "mozilla.org",
+ fileName = "example.apk",
+ destinationDirectory = folder.root.path,
+ )
+ val transformedDownload = service.makeUniqueFileNameIfNecessary(download, true)
+
+ assertEquals(download, transformedDownload)
+ }
+
+ @Test
+ fun `notification is shown when download status is ACTIVE`() = runBlocking {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ service.setDownloadJobStatus(downloadJobState, DOWNLOADING)
+ assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState))
+
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+
+ // The additional notification is the summary one (the notification group).
+ assertEquals(2, shadowNotificationService.size())
+ }
+
+ @Test
+ fun `onStartCommand must change status of INITIATED downloads to DOWNLOADING`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt", status = INITIATED)
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ doNothing().`when`(service).performDownload(any(), anyBoolean())
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.first().job!!.joinBlocking()
+
+ verify(service).startDownloadJob(any())
+ assertEquals(DOWNLOADING, service.downloadJobs.values.first().status)
+ }
+
+ @Test
+ fun `onStartCommand must change the status only for INITIATED downloads`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt", status = FAILED)
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ verify(service, never()).startDownloadJob(any())
+ assertEquals(FAILED, service.downloadJobs.values.first().status)
+ }
+
+ @Test
+ fun `onStartCommand sets the notification foreground`() = runTest(testsDispatcher) {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ doNothing().`when`(service).performDownload(any(), anyBoolean())
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ verify(service).setForegroundNotification(any())
+ }
+
+ @Test
+ fun `sets the notification foreground in devices that support notification group`() = runTest(testsDispatcher) {
+ val download = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ )
+ val downloadState = DownloadJobState(
+ state = download,
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+ val notification = mock<Notification>()
+
+ doReturn(notification).`when`(service).updateNotificationGroup()
+
+ service.downloadJobs["1"] = downloadState
+
+ service.setForegroundNotification(downloadState)
+
+ verify(service).startForeground(NOTIFICATION_DOWNLOAD_GROUP_ID, notification)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `sets the notification foreground in devices that DO NOT support notification group`() {
+ val download = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ )
+ val downloadState = DownloadJobState(
+ state = download,
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+ val notification = mock<Notification>()
+
+ doReturn(notification).`when`(service).createCompactForegroundNotification(downloadState)
+
+ service.downloadJobs["1"] = downloadState
+
+ service.setForegroundNotification(downloadState)
+
+ verify(service).startForeground(downloadState.foregroundServiceId, notification)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun createCompactForegroundNotification() {
+ val download = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ )
+ val downloadState = DownloadJobState(
+ state = download,
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ assertEquals(0, shadowNotificationService.size())
+
+ val notification = service.createCompactForegroundNotification(downloadState)
+
+ service.downloadJobs["1"] = downloadState
+
+ service.setForegroundNotification(downloadState)
+
+ assertNull(notification.group)
+ assertEquals(1, shadowNotificationService.size())
+ assertNotNull(shadowNotificationService.getNotification(downloadState.foregroundServiceId))
+ }
+
+ @Test
+ fun `getForegroundId in devices that support notification group will return NOTIFICATION_DOWNLOAD_GROUP_ID`() {
+ val download = DownloadState(id = "1", url = "https://example.com/file.txt", fileName = "file.txt")
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ doNothing().`when`(service).performDownload(any(), anyBoolean())
+
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ assertEquals(NOTIFICATION_DOWNLOAD_GROUP_ID, service.getForegroundId())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `getForegroundId in devices that support DO NOT notification group will return the latest active download`() {
+ val download = DownloadState(id = "1", url = "https://example.com/file.txt", fileName = "file.txt")
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ doNothing().`when`(service).performDownload(any(), anyBoolean())
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ val foregroundId = service.downloadJobs.values.first().foregroundServiceId
+ assertEquals(foregroundId, service.getForegroundId())
+ assertEquals(foregroundId, service.compatForegroundNotificationId)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `updateNotificationGroup will do nothing on devices that do not support notificaiton groups`() = runTest(testsDispatcher) {
+ val download = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ )
+ val downloadState = DownloadJobState(
+ state = download,
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ service.downloadJobs["1"] = downloadState
+
+ val notificationGroup = service.updateNotificationGroup()
+
+ assertNull(notificationGroup)
+ assertEquals(0, shadowNotificationService.size())
+ }
+
+ @Test
+ fun `removeDownloadJob will update the background notification if there are other pending downloads`() {
+ val download = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ )
+ val downloadState = DownloadJobState(
+ state = download,
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ service.downloadJobs["1"] = downloadState
+ service.downloadJobs["2"] = mock()
+
+ doNothing().`when`(service).updateForegroundNotificationIfNeeded(downloadState)
+
+ service.removeDownloadJob(downloadJobState = downloadState)
+
+ assertEquals(1, service.downloadJobs.size)
+ verify(service).updateForegroundNotificationIfNeeded(downloadState)
+ verify(service).removeNotification(testContext, downloadState)
+ }
+
+ @Test
+ fun `WHEN all downloads are completed stopForeground must be called`() {
+ val download1 = DownloadState(
+ id = "1",
+ url = "https://example.com/file1.txt",
+ fileName = "file1.txt",
+ status = COMPLETED,
+ )
+ val download2 = DownloadState(
+ id = "2",
+ url = "https://example.com/file2.txt",
+ fileName = "file2.txt",
+ status = COMPLETED,
+ )
+ val downloadState1 = DownloadJobState(
+ state = download1,
+ status = COMPLETED,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ val downloadState2 = DownloadJobState(
+ state = download2,
+ status = COMPLETED,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ service.downloadJobs["1"] = downloadState1
+ service.downloadJobs["2"] = downloadState2
+
+ service.updateForegroundNotificationIfNeeded(downloadState1)
+
+ verify(service).stopForegroundCompat(false)
+ }
+
+ @Test
+ fun `Until all downloads are NOT completed stopForeground must NOT be called`() {
+ val download1 = DownloadState(
+ id = "1",
+ url = "https://example.com/file1.txt",
+ fileName = "file1.txt",
+ status = COMPLETED,
+ )
+ val download2 = DownloadState(
+ id = "2",
+ url = "https://example.com/file2.txt",
+ fileName = "file2.txt",
+ status = DOWNLOADING,
+ )
+ val downloadState1 = DownloadJobState(
+ state = download1,
+ status = COMPLETED,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ val downloadState2 = DownloadJobState(
+ state = download2,
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ service.downloadJobs["1"] = downloadState1
+ service.downloadJobs["2"] = downloadState2
+
+ service.updateForegroundNotificationIfNeeded(downloadState1)
+
+ verify(service, never()).stopForeground(Service.STOP_FOREGROUND_DETACH)
+ }
+
+ @Test
+ fun `removeDownloadJob will stop the service if there are none pending downloads`() {
+ val download = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ )
+ val downloadState = DownloadJobState(
+ state = download,
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ doNothing().`when`(service).stopForeground(Service.STOP_FOREGROUND_DETACH)
+ doNothing().`when`(service).clearAllDownloadsNotificationsAndJobs()
+ doNothing().`when`(service).stopSelf()
+
+ service.downloadJobs["1"] = downloadState
+
+ service.removeDownloadJob(downloadJobState = downloadState)
+
+ assertTrue(service.downloadJobs.isEmpty())
+ verify(service).stopSelf()
+ verify(service, times(0)).updateForegroundNotificationIfNeeded(downloadState)
+ }
+
+ @Test
+ fun `updateForegroundNotification will update the notification group for devices that support it`() {
+ doReturn(null).`when`(service).updateNotificationGroup()
+
+ service.updateForegroundNotificationIfNeeded(mock())
+
+ verify(service).updateNotificationGroup()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `updateForegroundNotification will select a new foreground notification`() {
+ val downloadState1 = DownloadJobState(
+ state = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DownloadState.Status.COMPLETED,
+ ),
+ status = DownloadState.Status.COMPLETED,
+ foregroundServiceId = Random.nextInt(),
+ )
+ val downloadState2 = DownloadJobState(
+ state = DownloadState(
+ id = "2",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ ),
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ service.compatForegroundNotificationId = downloadState1.foregroundServiceId
+
+ service.downloadJobs["1"] = downloadState1
+ service.downloadJobs["2"] = downloadState2
+
+ service.updateForegroundNotificationIfNeeded(downloadState1)
+
+ verify(service).setForegroundNotification(downloadState2)
+ assertEquals(downloadState2.foregroundServiceId, service.compatForegroundNotificationId)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `updateForegroundNotification will NOT select a new foreground notification`() {
+ val downloadState1 = DownloadJobState(
+ state = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ ),
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+ val downloadState2 = DownloadJobState(
+ state = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ ),
+ status = DOWNLOADING,
+ foregroundServiceId = Random.nextInt(),
+ )
+
+ service.compatForegroundNotificationId = downloadState1.foregroundServiceId
+
+ service.downloadJobs["1"] = downloadState1
+ service.downloadJobs["2"] = downloadState2
+
+ service.updateForegroundNotificationIfNeeded(downloadState1)
+
+ verify(service, times(0)).setForegroundNotification(downloadState2)
+ verify(service, times(0)).updateNotificationGroup()
+ assertEquals(downloadState1.foregroundServiceId, service.compatForegroundNotificationId)
+ }
+
+ @Test
+ fun `notification is shown when download status is PAUSED`() = runBlocking {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)
+ assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState))
+
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+
+ // one of the notifications it is the group notification only for devices the support it
+ assertEquals(2, shadowNotificationService.size())
+ }
+
+ @Test
+ fun `notification is shown when download status is COMPLETED`() = runBlocking {
+ performSuccessfulCompleteDownload()
+
+ assertEquals(2, shadowNotificationService.size())
+ }
+
+ @Test
+ fun `completed download notification avoids notification trampoline restrictions by using an activity based PendingIntent to open the file`() = runBlocking {
+ val downloadJobState = performSuccessfulCompleteDownload()
+
+ val notification = shadowNotificationService.getNotification(downloadJobState.foregroundServiceId)
+ val shadowNotificationContentPendingIntent = shadowOf(notification.contentIntent)
+ assertTrue(shadowNotificationContentPendingIntent.isActivity)
+ }
+
+ private suspend fun performSuccessfulCompleteDownload(): DownloadJobState {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ service.setDownloadJobStatus(downloadJobState, COMPLETED)
+ assertEquals(COMPLETED, service.getDownloadJobStatus(downloadJobState))
+
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+ return downloadJobState
+ }
+
+ @Test
+ fun `notification is shown when download status is FAILED`() = runBlocking {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ service.setDownloadJobStatus(downloadJobState, FAILED)
+ assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
+
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+
+ // one of the notifications it is the group notification only for devices the support it
+ assertEquals(2, shadowNotificationService.size())
+ }
+
+ @Test
+ fun `notification is not shown when download status is CANCELLED`() = runBlocking {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ service.setDownloadJobStatus(downloadJobState, DownloadState.Status.CANCELLED)
+ assertEquals(DownloadState.Status.CANCELLED, service.getDownloadJobStatus(downloadJobState))
+
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+
+ // The additional notification is the summary one (the notification group).
+ assertEquals(1, shadowNotificationService.size())
+ }
+
+ @Test
+ fun `job status is set to failed when an Exception is thrown while performDownload`() = runTest(testsDispatcher) {
+ doThrow(IOException()).`when`(client).fetch(any())
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+ assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
+ }
+
+ @Test
+ fun `WHEN a download is from a private session the request must be private`() = runTest(testsDispatcher) {
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+ val download = DownloadState("https://example.com/file.txt", "file.txt", private = true)
+ val downloadJob = DownloadJobState(state = download, status = DOWNLOADING)
+ val providedRequest = argumentCaptor<Request>()
+
+ service.performDownload(downloadJob)
+ verify(client).fetch(providedRequest.capture())
+ assertTrue(providedRequest.value.private)
+
+ downloadJob.state = download.copy(private = false)
+ service.performDownload(downloadJob)
+
+ verify(client, times(2)).fetch(providedRequest.capture())
+
+ assertFalse(providedRequest.value.private)
+ }
+
+ @Test
+ fun `performDownload - use the download response when available`() {
+ val responseFromDownloadState = mock<Response>()
+ val responseFromClient = mock<Response>()
+ val download = DownloadState("https://example.com/file.txt", "file.txt", response = responseFromDownloadState)
+ val downloadJob = DownloadJobState(state = download, status = DOWNLOADING)
+
+ doReturn(404).`when`(responseFromDownloadState).status
+ doReturn(responseFromClient).`when`(client).fetch(any())
+
+ service.performDownload(downloadJob)
+
+ verify(responseFromDownloadState, atLeastOnce()).status
+ verifyNoInteractions(client)
+ }
+
+ @Test
+ fun `performDownload - use the client response when the download response NOT available`() {
+ val responseFromClient = mock<Response>()
+ val download = spy(DownloadState("https://example.com/file.txt", "file.txt", response = null))
+ val downloadJob = DownloadJobState(state = download, status = DOWNLOADING)
+
+ doReturn(404).`when`(responseFromClient).status
+ doReturn(responseFromClient).`when`(client).fetch(any())
+
+ service.performDownload(downloadJob)
+
+ verify(responseFromClient, atLeastOnce()).status
+ }
+
+ @Test
+ fun `performDownload - use the client response when resuming a download`() {
+ val responseFromDownloadState = mock<Response>()
+ val responseFromClient = mock<Response>()
+ val download = spy(DownloadState("https://example.com/file.txt", "file.txt", response = responseFromDownloadState))
+ val downloadJob = DownloadJobState(currentBytesCopied = 100, state = download, status = DOWNLOADING)
+
+ doReturn(404).`when`(responseFromClient).status
+ doReturn(responseFromClient).`when`(client).fetch(any())
+
+ service.performDownload(downloadJob)
+
+ verify(responseFromClient, atLeastOnce()).status
+ verifyNoInteractions(responseFromDownloadState)
+ }
+
+ @Test
+ fun `onDestroy cancels all running jobs`() = runBlocking {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ // Simulate a long running reading operation by sleeping for 5 seconds.
+ Response.Body(
+ object : InputStream() {
+ override fun read(): Int {
+ Thread.sleep(5000)
+ return 0
+ }
+ },
+ ),
+ )
+ // Call the real method to force the reading of the response's body.
+ doCallRealMethod().`when`(service).useFileStream(any(), anyBoolean(), any())
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.registerNotificationActionsReceiver()
+ service.onStartCommand(downloadIntent, 0, 0)
+
+ service.downloadJobs.values.forEach { assertTrue(it.job!!.isActive) }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ // Advance the clock so that the puller posts a notification.
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+ // One of the notifications it is the group notification only for devices the support it
+ assertEquals(2, shadowNotificationService.size())
+
+ // Now destroy
+ service.onDestroy()
+
+ // Assert that jobs were cancelled rather than completed.
+ service.downloadJobs.values.forEach {
+ assertTrue(it.job!!.isCancelled)
+ assertFalse(it.job!!.isCompleted)
+ }
+
+ // Assert that all currently shown notifications are gone.
+ assertEquals(0, shadowNotificationService.size())
+ }
+
+ @Test
+ fun `updateDownloadState must update the download state in the store and in the downloadJobs`() {
+ val download = DownloadState(
+ "https://example.com/file.txt",
+ "file1.txt",
+ status = DOWNLOADING,
+ )
+ val downloadJob = DownloadJobState(state = mock(), status = DOWNLOADING)
+ val mockStore = mock<BrowserStore>()
+ val mockNotificationsDelegate = mock<NotificationsDelegate>()
+
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = mockStore
+ override val notificationsDelegate = mockNotificationsDelegate
+ },
+ )
+
+ service.downloadJobs[download.id] = downloadJob
+
+ service.updateDownloadState(download)
+
+ assertEquals(download, service.downloadJobs[download.id]!!.state)
+ verify(mockStore).dispatch(DownloadAction.UpdateDownloadAction(download))
+ }
+
+ @Test
+ fun `onTaskRemoved cancels all notifications on the shadow notification manager`() = runBlocking {
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.registerNotificationActionsReceiver()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+
+ service.setDownloadJobStatus(service.downloadJobs[download.id]!!, DownloadState.Status.PAUSED)
+
+ // Advance the clock so that the poller posts a notification.
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+ assertEquals(2, shadowNotificationService.size())
+
+ // Now simulate onTaskRemoved.
+ service.onTaskRemoved(null)
+
+ verify(service).stopSelf()
+ }
+
+ @Test
+ fun `clearAllDownloadsNotificationsAndJobs cancels all running jobs and remove all notifications`() = runTest(testsDispatcher) {
+ val download = DownloadState(
+ id = "1",
+ url = "https://example.com/file.txt",
+ fileName = "file.txt",
+ status = DOWNLOADING,
+ )
+ val downloadState = DownloadJobState(
+ state = download,
+ foregroundServiceId = Random.nextInt(),
+ status = DOWNLOADING,
+ job = CoroutineScope(IO).launch {
+ @Suppress("ControlFlowWithEmptyBody")
+ while (true) { }
+ },
+ )
+
+ service.registerNotificationActionsReceiver()
+ service.downloadJobs[download.id] = downloadState
+
+ val notificationStyle = AbstractFetchDownloadService.Style()
+ val notification = DownloadNotification.createOngoingDownloadNotification(
+ testContext,
+ downloadState,
+ notificationStyle.notificationAccentColor,
+ )
+
+ NotificationManagerCompat.from(testContext).notify(downloadState.foregroundServiceId, notification)
+
+ // We have a pending notification
+ assertEquals(1, shadowNotificationService.size())
+
+ service.clearAllDownloadsNotificationsAndJobs()
+
+ // Assert that all currently shown notifications are gone.
+ assertEquals(0, shadowNotificationService.size())
+
+ // Assert that jobs were cancelled rather than completed.
+ service.downloadJobs.values.forEach {
+ assertTrue(it.job!!.isCancelled)
+ assertFalse(it.job!!.isCompleted)
+ }
+ }
+
+ @Test
+ fun `onDestroy will remove all download notifications, jobs and will call unregisterNotificationActionsReceiver`() = runTest(testsDispatcher) {
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ doReturn(testContext).`when`(service).context
+
+ service.registerNotificationActionsReceiver()
+
+ service.onDestroy()
+
+ verify(service).clearAllDownloadsNotificationsAndJobs()
+ verify(service).unregisterNotificationActionsReceiver()
+ }
+
+ @Test
+ fun `register and unregister notification actions receiver`() {
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ doReturn(testContext).`when`(service).context
+
+ service.onCreate()
+
+ verify(service).registerNotificationActionsReceiver()
+
+ service.onDestroy()
+
+ verify(service).unregisterNotificationActionsReceiver()
+ }
+
+ @Test
+ fun `WHEN a download is completed and the scoped storage is not used it MUST be added manually to the download system database`() = runTest(testsDispatcher) {
+ val download = DownloadState(
+ url = "http://www.mozilla.org",
+ fileName = "example.apk",
+ destinationDirectory = folder.root.path,
+ status = DownloadState.Status.COMPLETED,
+ )
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ val downloadJobState = DownloadJobState(state = download, status = DownloadState.Status.COMPLETED)
+
+ doReturn(testContext).`when`(service).context
+ service.updateDownloadNotification(DownloadState.Status.COMPLETED, downloadJobState, this)
+
+ verify(service).addCompletedDownload(
+ title = any(),
+ description = any(),
+ isMediaScannerScannable = eq(true),
+ mimeType = any(),
+ path = any(),
+ length = anyLong(),
+ showNotification = anyBoolean(),
+ download = any(),
+ )
+ }
+
+ @Test
+ fun `WHEN a download is completed and the scoped storage is used addToDownloadSystemDatabaseCompat MUST NOT be called`() = runTest(testsDispatcher) {
+ val download = DownloadState(
+ url = "http://www.mozilla.org",
+ fileName = "example.apk",
+ destinationDirectory = folder.root.path,
+ status = DownloadState.Status.COMPLETED,
+ )
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ val downloadJobState = DownloadJobState(state = download, status = DownloadState.Status.COMPLETED)
+
+ doReturn(testContext).`when`(service).context
+ doNothing().`when`(service).addCompletedDownload(
+ title = any(),
+ description = any(),
+ isMediaScannerScannable = eq(true),
+ mimeType = any(),
+ path = any(),
+ length = anyLong(),
+ showNotification = anyBoolean(),
+ download = any(),
+ )
+ doReturn(true).`when`(service).shouldUseScopedStorage()
+
+ service.updateDownloadNotification(DownloadState.Status.COMPLETED, downloadJobState, this)
+
+ verify(service, never()).addCompletedDownload(
+ title = any(),
+ description = any(),
+ isMediaScannerScannable = eq(true),
+ mimeType = any(),
+ path = any(),
+ length = anyLong(),
+ showNotification = anyBoolean(),
+ download = any(),
+ )
+ }
+
+ @Test
+ fun `WHEN we download on devices with version higher than Q THEN we use scoped storage`() {
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+ val uniqueFile: DownloadState = mock()
+ val qSdkVersion = 29
+ doReturn(uniqueFile).`when`(service).makeUniqueFileNameIfNecessary(any(), anyBoolean())
+ doNothing().`when`(service).updateDownloadState(uniqueFile)
+ doNothing().`when`(service).useFileStreamScopedStorage(eq(uniqueFile), any())
+ doReturn(qSdkVersion).`when`(service).getSdkVersion()
+
+ service.useFileStream(mock(), true) {}
+
+ verify(service).useFileStreamScopedStorage(eq(uniqueFile), any())
+ }
+
+ @Test
+ fun `WHEN we download on devices with version lower than Q THEN we use legacy file stream`() {
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+ val uniqueFile: DownloadState = mock()
+ val qSdkVersion = 27
+ doReturn(uniqueFile).`when`(service).makeUniqueFileNameIfNecessary(any(), anyBoolean())
+ doNothing().`when`(service).updateDownloadState(uniqueFile)
+ doNothing().`when`(service).useFileStreamLegacy(eq(uniqueFile), anyBoolean(), any())
+ doReturn(qSdkVersion).`when`(service).getSdkVersion()
+
+ service.useFileStream(mock(), true) {}
+
+ verify(service).useFileStreamLegacy(eq(uniqueFile), anyBoolean(), any())
+ }
+
+ @Test
+ @Suppress("Deprecation")
+ fun `do not pass non-http(s) url to addCompletedDownload`() = runTest(testsDispatcher) {
+ val download = DownloadState(
+ url = "blob:moz-extension://d5ea9baa-64c9-4c3d-bb38-49308c47997c/",
+ fileName = "example.apk",
+ destinationDirectory = folder.root.path,
+ )
+
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ val spyContext = spy(testContext)
+ val downloadManager: DownloadManager = mock()
+
+ doReturn(spyContext).`when`(service).context
+ doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
+
+ service.addToDownloadSystemDatabaseCompat(download, this)
+ verify(downloadManager).addCompletedDownload(anyString(), anyString(), anyBoolean(), anyString(), anyString(), anyLong(), anyBoolean(), isNull(), any())
+ }
+
+ @Test
+ @Suppress("Deprecation")
+ fun `GIVEN a download that throws an exception WHEN adding to the system database THEN handle the exception`() =
+ runTest(testsDispatcher) {
+ val download = DownloadState(
+ url = "url",
+ fileName = "example.apk",
+ destinationDirectory = folder.root.path,
+ )
+
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ val spyContext = spy(testContext)
+ val downloadManager: DownloadManager = mock()
+
+ doReturn(spyContext).`when`(service).context
+ doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
+
+ doAnswer { throw IllegalArgumentException() }.`when`(downloadManager)
+ .addCompletedDownload(
+ anyString(), anyString(), anyBoolean(), anyString(),
+ anyString(), anyLong(), anyBoolean(), isNull(), any(),
+ )
+
+ try {
+ service.addToDownloadSystemDatabaseCompat(download, this)
+ } catch (e: IOException) {
+ fail()
+ }
+ }
+
+ @Test
+ @Suppress("Deprecation")
+ fun `pass http(s) url to addCompletedDownload`() = runTest(testsDispatcher) {
+ val download = DownloadState(
+ url = "https://mozilla.com",
+ fileName = "example.apk",
+ destinationDirectory = folder.root.path,
+ )
+
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+
+ val spyContext = spy(testContext)
+ val downloadManager: DownloadManager = mock()
+
+ doReturn(spyContext).`when`(service).context
+ doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
+
+ service.addToDownloadSystemDatabaseCompat(download, this)
+ verify(downloadManager).addCompletedDownload(anyString(), anyString(), anyBoolean(), anyString(), anyString(), anyLong(), anyBoolean(), any(), any())
+ }
+
+ @Test
+ @Suppress("Deprecation")
+ fun `always call addCompletedDownload with a not empty or null mimeType`() = runTest(testsDispatcher) {
+ val service = spy(
+ object : AbstractFetchDownloadService() {
+ override val httpClient = client
+ override val store = browserStore
+ override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
+ },
+ )
+ val spyContext = spy(testContext)
+ var downloadManager: DownloadManager = mock()
+ doReturn(spyContext).`when`(service).context
+ doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
+ val downloadWithNullMimeType = DownloadState(
+ url = "blob:moz-extension://d5ea9baa-64c9-4c3d-bb38-49308c47997c/",
+ fileName = "example.apk",
+ destinationDirectory = folder.root.path,
+ contentType = null,
+ )
+ val downloadWithEmptyMimeType = downloadWithNullMimeType.copy(contentType = "")
+ val defaultMimeType = "*/*"
+
+ service.addToDownloadSystemDatabaseCompat(downloadWithNullMimeType, this)
+ verify(downloadManager).addCompletedDownload(
+ anyString(), anyString(), anyBoolean(), eq(defaultMimeType),
+ anyString(), anyLong(), anyBoolean(), isNull(), any(),
+ )
+
+ downloadManager = mock()
+ doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
+ service.addToDownloadSystemDatabaseCompat(downloadWithEmptyMimeType, this)
+ verify(downloadManager).addCompletedDownload(
+ anyString(), anyString(), anyBoolean(), eq(defaultMimeType),
+ anyString(), anyLong(), anyBoolean(), isNull(), any(),
+ )
+ }
+
+ @Test
+ fun `cancelled download does not prevent other notifications`() = runBlocking {
+ val cancelledDownload = DownloadState("https://example.com/file.txt", "file.txt")
+ val response = Response(
+ "https://example.com/file.txt",
+ 200,
+ MutableHeaders(),
+ Response.Body(mock()),
+ )
+
+ doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
+ val cancelledDownloadIntent = Intent("ACTION_DOWNLOAD")
+ cancelledDownloadIntent.putExtra(EXTRA_DOWNLOAD_ID, cancelledDownload.id)
+
+ browserStore.dispatch(DownloadAction.AddDownloadAction(cancelledDownload)).joinBlocking()
+ service.onStartCommand(cancelledDownloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+
+ val providedDownload = argumentCaptor<DownloadJobState>()
+
+ verify(service).performDownload(providedDownload.capture(), anyBoolean())
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+
+ val cancelledDownloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+
+ service.setDownloadJobStatus(cancelledDownloadJobState, DownloadState.Status.CANCELLED)
+ assertEquals(DownloadState.Status.CANCELLED, service.getDownloadJobStatus(cancelledDownloadJobState))
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+ // The additional notification is the summary one (the notification group).
+ assertEquals(1, shadowNotificationService.size())
+
+ val download = DownloadState("https://example.com/file.txt", "file.txt")
+ val downloadIntent = Intent("ACTION_DOWNLOAD")
+ downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
+
+ // Start another download to ensure its notifications are presented
+ browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+ service.onStartCommand(downloadIntent, 0, 0)
+ service.downloadJobs.values.forEach { it.job?.join() }
+ verify(service, times(2)).performDownload(providedDownload.capture(), anyBoolean())
+ service.downloadJobs[providedDownload.value.state.id]?.job?.join()
+
+ val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
+
+ service.setDownloadJobStatus(downloadJobState, DownloadState.Status.COMPLETED)
+ assertEquals(DownloadState.Status.COMPLETED, service.getDownloadJobStatus(downloadJobState))
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
+ // one of the notifications it is the group notification only for devices the support it
+ assertEquals(2, shadowNotificationService.size())
+ }
+
+ @Test
+ fun `createDirectoryIfNeeded - MUST create directory when it does not exists`() = runTest(testsDispatcher) {
+ val download = DownloadState(destinationDirectory = Environment.DIRECTORY_DOWNLOADS, url = "")
+
+ val file = File(download.directoryPath)
+ file.delete()
+
+ assertFalse(file.exists())
+
+ service.createDirectoryIfNeeded(download)
+
+ assertTrue(file.exists())
+ }
+
+ @Test
+ fun `keeps track of how many seconds have passed since the last update to a notification`() = runBlocking {
+ val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
+ val oneSecond = 1000L
+
+ downloadJobState.lastNotificationUpdate = System.currentTimeMillis()
+
+ delay(oneSecond)
+
+ var seconds = downloadJobState.getSecondsSinceTheLastNotificationUpdate()
+
+ assertEquals(1, seconds)
+
+ delay(oneSecond)
+
+ seconds = downloadJobState.getSecondsSinceTheLastNotificationUpdate()
+
+ assertEquals(2, seconds)
+ }
+
+ @Test
+ fun `is a notification under the time limit for updates`() = runBlocking {
+ val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
+ val oneSecond = 1000L
+
+ downloadJobState.lastNotificationUpdate = System.currentTimeMillis()
+
+ assertFalse(downloadJobState.isUnderNotificationUpdateLimit())
+
+ delay(oneSecond)
+
+ assertTrue(downloadJobState.isUnderNotificationUpdateLimit())
+ }
+
+ @Test
+ fun `try to update a notification`() = runBlocking {
+ val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
+ val oneSecond = 1000L
+
+ downloadJobState.lastNotificationUpdate = System.currentTimeMillis()
+
+ // It's over the notification limit
+ assertFalse(downloadJobState.canUpdateNotification())
+
+ delay(oneSecond)
+
+ // It's under the notification limit
+ assertTrue(downloadJobState.canUpdateNotification())
+
+ downloadJobState.notifiedStopped = true
+
+ assertFalse(downloadJobState.canUpdateNotification())
+
+ downloadJobState.notifiedStopped = false
+
+ assertTrue(downloadJobState.canUpdateNotification())
+ }
+
+ @Test
+ fun `copyInChunks must alter download currentBytesCopied`() = runTest(testsDispatcher) {
+ val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
+ val inputStream = mock<InputStream>()
+
+ assertEquals(0, downloadJobState.currentBytesCopied)
+
+ doReturn(15, -1).`when`(inputStream).read(any())
+ doNothing().`when`(service).updateDownloadState(any())
+
+ service.copyInChunks(downloadJobState, inputStream, mock())
+
+ assertEquals(15, downloadJobState.currentBytesCopied)
+ }
+
+ @Test
+ fun `copyInChunks - must return ERROR_IN_STREAM_CLOSED when inStream is closed`() = runTest(testsDispatcher) {
+ val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
+ val inputStream = mock<InputStream>()
+
+ assertEquals(0, downloadJobState.currentBytesCopied)
+
+ doAnswer { throw IOException() }.`when`(inputStream).read(any())
+ doNothing().`when`(service).updateDownloadState(any())
+ doNothing().`when`(service).performDownload(any(), anyBoolean())
+
+ val status = service.copyInChunks(downloadJobState, inputStream, mock())
+
+ verify(service).performDownload(downloadJobState, true)
+ assertEquals(ERROR_IN_STREAM_CLOSED, status)
+ }
+
+ @Test
+ fun `copyInChunks - must throw when inStream is closed and download was performed using http client`() = runTest(testsDispatcher) {
+ val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
+ val inputStream = mock<InputStream>()
+ var exceptionWasThrown = false
+
+ assertEquals(0, downloadJobState.currentBytesCopied)
+
+ doAnswer { throw IOException() }.`when`(inputStream).read(any())
+ doNothing().`when`(service).updateDownloadState(any())
+ doNothing().`when`(service).performDownload(any(), anyBoolean())
+
+ try {
+ service.copyInChunks(downloadJobState, inputStream, mock(), true)
+ } catch (e: IOException) {
+ exceptionWasThrown = true
+ }
+
+ verify(service, times(0)).performDownload(downloadJobState, true)
+ assertTrue(exceptionWasThrown)
+ }
+
+ @Test
+ fun `copyInChunks - must return COMPLETED when finish copying bytes`() = runTest(testsDispatcher) {
+ val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
+ val inputStream = mock<InputStream>()
+
+ assertEquals(0, downloadJobState.currentBytesCopied)
+
+ doReturn(15, -1).`when`(inputStream).read(any())
+ doNothing().`when`(service).updateDownloadState(any())
+
+ val status = service.copyInChunks(downloadJobState, inputStream, mock())
+
+ verify(service, never()).performDownload(any(), anyBoolean())
+
+ assertEquals(15, downloadJobState.currentBytesCopied)
+ assertEquals(AbstractFetchDownloadService.CopyInChuckStatus.COMPLETED, status)
+ }
+
+ @Test
+ fun `getSafeContentType - WHEN the file content type is available THEN use it`() {
+ val contentTypeFromFile = "application/pdf; qs=0.001"
+ val spyContext = spy(testContext)
+ val contentResolver = mock<ContentResolver>()
+
+ doReturn(contentTypeFromFile).`when`(contentResolver).getType(any())
+ doReturn(contentResolver).`when`(spyContext).contentResolver
+
+ val result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), "any")
+
+ assertEquals("application/pdf", result)
+ }
+
+ @Test
+ fun `getSafeContentType - WHEN the file content type is not available THEN use the provided content type`() {
+ val contentType = " application/pdf "
+ val spyContext = spy(testContext)
+ val contentResolver = mock<ContentResolver>()
+ doReturn(contentResolver).`when`(spyContext).contentResolver
+
+ doReturn(null).`when`(contentResolver).getType(any())
+ var result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), contentType)
+ assertEquals("application/pdf", result)
+
+ doReturn("").`when`(contentResolver).getType(any())
+ result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), contentType)
+ assertEquals("application/pdf", result)
+ }
+
+ @Test
+ fun `getSafeContentType - WHEN none of the provided content types are available THEN return a generic content type`() {
+ val spyContext = spy(testContext)
+ val contentResolver = mock<ContentResolver>()
+ doReturn(contentResolver).`when`(spyContext).contentResolver
+
+ doReturn(null).`when`(contentResolver).getType(any())
+ var result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), null)
+ assertEquals("*/*", result)
+
+ doReturn("").`when`(contentResolver).getType(any())
+ result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), null)
+ assertEquals("*/*", result)
+ }
+
+ // Following 3 tests use the String version of #getSafeContentType while the above 3 tested the Uri version
+ // The String version just overloads and delegates the Uri one but being in a companion object we cannot
+ // verify the delegation so we are left to verify the result to prevent any regressions.
+ @Test
+ fun `getSafeContentType2 - WHEN the file content type is available THEN use it`() {
+ val contentTypeFromFile = "application/pdf; qs=0.001"
+ val spyContext = spy(testContext)
+ val contentResolver = mock<ContentResolver>()
+
+ doReturn(contentTypeFromFile).`when`(contentResolver).getType(any())
+ doReturn(contentResolver).`when`(spyContext).contentResolver
+
+ val result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", "any")
+
+ assertEquals("application/pdf", result)
+ }
+
+ @Test
+ fun `getSafeContentType2 - WHEN the file content type is not available THEN use the provided content type`() {
+ val contentType = " application/pdf "
+ val spyContext = spy(testContext)
+ val contentResolver = mock<ContentResolver>()
+ doReturn(contentResolver).`when`(spyContext).contentResolver
+
+ doReturn(null).`when`(contentResolver).getType(any())
+ var result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", contentType)
+ assertEquals("application/pdf", result)
+
+ doReturn("").`when`(contentResolver).getType(any())
+ result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", contentType)
+ assertEquals("application/pdf", result)
+ }
+
+ @Test
+ fun `getSafeContentType2 - WHEN none of the provided content types are available THEN return a generic content type`() {
+ val spyContext = spy(testContext)
+ val contentResolver = mock<ContentResolver>()
+ doReturn(contentResolver).`when`(spyContext).contentResolver
+
+ doReturn(null).`when`(contentResolver).getType(any())
+ var result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", null)
+ assertEquals("*/*", result)
+
+ doReturn("").`when`(contentResolver).getType(any())
+ result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", null)
+ assertEquals("*/*", result)
+ }
+
+ // Hard to test #getFilePathUri since it only returns the result of a certain Android api call.
+ // But let's try.
+ @Test
+ @Config(shadows = [DefaultFileProvider::class]) // use default implementation just for this test
+ fun `getFilePathUri - WHEN called without a registered provider THEN exception is thrown`() {
+ // There is no app registered provider that could expose a file from the filesystem of the machine running this test.
+ // Peeking into the exception would indicate whether the code really called "FileProvider.getUriForFile" as expected.
+ var exception: IllegalArgumentException? = null
+ try {
+ AbstractFetchDownloadService.getFilePathUri(testContext, "test.txt")
+ } catch (e: IllegalArgumentException) {
+ exception = e
+ }
+
+ assertTrue(exception!!.stackTrace[0].fileName.contains("FileProvider"))
+ assertTrue(exception.stackTrace[0].methodName == "getUriForFile")
+ }
+
+ @Test
+ fun `getFilePathUri - WHEN called THEN return a file provider path for the filePath`() {
+ // Test that the String filePath is passed to the provider from which we expect a Uri path
+ val result = AbstractFetchDownloadService.getFilePathUri(testContext, "location/test.txt")
+
+ assertTrue(result.toString().endsWith("location/test.txt"))
+ }
+}
+
+@Implements(FileProvider::class)
+object ShadowFileProvider {
+ @Implementation
+ @JvmStatic
+ @Suppress("UNUSED_PARAMETER")
+ fun getUriForFile(
+ context: Context?,
+ authority: String?,
+ file: File,
+ ) = "content://authority/random/location/${file.name}".toUri()
+}
+
+@Implements(FileProvider::class)
+object DefaultFileProvider
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadCancelDialogFragmentTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadCancelDialogFragmentTest.kt
new file mode 100644
index 0000000000..0322416b68
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadCancelDialogFragmentTest.kt
@@ -0,0 +1,150 @@
+/* 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.downloads
+
+import android.graphics.drawable.GradientDrawable
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.downloads.ui.DownloadCancelDialogFragment
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(application = TestApplication::class)
+class DownloadCancelDialogFragmentTest {
+
+ @Test
+ fun `WHEN accept button is clicked THEN onAcceptClicked must be called`() {
+ spy(DownloadCancelDialogFragment.newInstance(2)).apply {
+ doReturn(testContext).`when`(this).requireContext()
+ doReturn(mockFragmentManager()).`when`(this).parentFragmentManager
+ var wasAcceptClicked = false
+ onAcceptClicked = { _, _ -> wasAcceptClicked = true }
+
+ with(onCreateDialog(null)) {
+ findViewById<Button>(R.id.accept_button).apply { performClick() }
+ }
+
+ assertTrue(wasAcceptClicked)
+ }
+ }
+
+ @Test
+ fun `WHEN deny button is clicked THEN onDenyClicked must be called`() {
+ spy(DownloadCancelDialogFragment.newInstance(2)).apply {
+ doReturn(testContext).`when`(this).requireContext()
+ doReturn(mockFragmentManager()).`when`(this).parentFragmentManager
+ var wasDenyCalled = false
+ onDenyClicked = { wasDenyCalled = true }
+
+ with(onCreateDialog(null)) {
+ findViewById<Button>(R.id.deny_button).apply { performClick() }
+ }
+
+ assertTrue(wasDenyCalled)
+ }
+ }
+
+ @Test
+ fun `WHEN overriding strings are provided to the prompt, THEN they are used by the prompt`() {
+ val testText = DownloadCancelDialogFragment.PromptText(
+ titleText = R.string.mozac_feature_downloads_cancel_active_private_downloads_warning_content_body,
+ bodyText = R.string.mozac_feature_downloads_cancel_active_downloads_warning_content_title,
+ acceptText = R.string.mozac_feature_downloads_cancel_active_private_downloads_deny,
+ denyText = R.string.mozac_feature_downloads_cancel_active_downloads_accept,
+ )
+ spy(
+ DownloadCancelDialogFragment.newInstance(
+ 0,
+ promptText = testText,
+ ),
+ ).apply {
+ doReturn(testContext).`when`(this).requireContext()
+
+ with(onCreateDialog(null)) {
+ findViewById<TextView>(R.id.title).apply {
+ Assert.assertEquals(text, testContext.getString(testText.titleText))
+ }
+ findViewById<TextView>(R.id.body).apply {
+ Assert.assertEquals(text, testContext.getString(testText.bodyText))
+ }
+ findViewById<Button>(R.id.accept_button).apply {
+ Assert.assertEquals(text, testContext.getString(testText.acceptText))
+ }
+ findViewById<Button>(R.id.deny_button).apply {
+ Assert.assertEquals(text, testContext.getString(testText.denyText))
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `WHEN styling is provided to the prompt, THEN it's used by the prompt`() {
+ val testStyling = DownloadCancelDialogFragment.PromptStyling(
+ gravity = Gravity.TOP,
+ shouldWidthMatchParent = false,
+ positiveButtonBackgroundColor = android.R.color.white,
+ positiveButtonTextColor = android.R.color.black,
+ positiveButtonRadius = 4f,
+ )
+
+ spy(
+ DownloadCancelDialogFragment.newInstance(
+ 0,
+ promptStyling = testStyling,
+ ),
+ ).apply {
+ doReturn(testContext).`when`(this).requireContext()
+
+ with(onCreateDialog(null)) {
+ with(window!!.attributes) {
+ Assert.assertTrue(gravity == Gravity.TOP)
+ Assert.assertTrue(width == ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+
+ with(findViewById<Button>(R.id.accept_button)) {
+ Assert.assertEquals(
+ ContextCompat.getColor(
+ testContext,
+ testStyling.positiveButtonBackgroundColor!!,
+ ),
+ (background as GradientDrawable).color?.defaultColor,
+ )
+ Assert.assertEquals(
+ testStyling.positiveButtonRadius!!,
+ (background as GradientDrawable).cornerRadius,
+ )
+ Assert.assertEquals(
+ ContextCompat.getColor(
+ testContext,
+ testStyling.positiveButtonTextColor!!,
+ ),
+ textColors.defaultColor,
+ )
+ }
+ }
+ }
+ }
+
+ 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/downloads/src/test/java/mozilla/components/feature/downloads/DownloadDialogFragmentTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadDialogFragmentTest.kt
new file mode 100644
index 0000000000..d7685c9017
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadDialogFragmentTest.kt
@@ -0,0 +1,65 @@
+/* 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.downloads
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.BYTES_TO_MB_LIMIT
+import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KEY_FILE_NAME
+import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KILOBYTE
+import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.math.roundToLong
+
+@RunWith(AndroidJUnit4::class)
+class DownloadDialogFragmentTest {
+
+ private lateinit var dialog: DownloadDialogFragment
+ private lateinit var download: DownloadState
+
+ @Before
+ fun setup() {
+ dialog = object : DownloadDialogFragment() {}
+ download = DownloadState(
+ "http://ipv4.download.thinkbroadband.com/5MB.zip",
+ "5MB.zip",
+ "application/zip",
+ 5242880,
+ userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36",
+ )
+ }
+
+ @Test
+ fun `when setDownload must set download metadata`() {
+ dialog.setDownload(download)
+
+ assertNotNull(dialog.arguments)
+ val fileName = dialog.arguments!!.getString(KEY_FILE_NAME)
+ val url = dialog.arguments!!.getString(DownloadDialogFragment.KEY_URL)
+ val contentLength = dialog.arguments!!.getLong(DownloadDialogFragment.KEY_CONTENT_LENGTH)
+
+ assertEquals(fileName, download.fileName)
+ assertEquals(url, download.url)
+ assertEquals(contentLength, download.contentLength)
+ }
+
+ @Test
+ fun `extension function 'toMegabyteOrKilobyteString' returns MB string when size is or equal or bigger than the limit `() {
+ val size = (BYTES_TO_MB_LIMIT * MEGABYTE).roundToLong()
+ val expectedString = String.format("%.2f MB", size / MEGABYTE)
+ assertEquals(expectedString, size.toMegabyteOrKilobyteString())
+ }
+
+ @Test
+ fun `extension function 'toMegabyteOrKilobyteString' returns KB string when size is smaller than the limit `() {
+ val size = (BYTES_TO_MB_LIMIT * MEGABYTE).roundToLong() - 1
+ val expectedString = String.format("%.2f KB", size / KILOBYTE)
+ assertEquals(expectedString, size.toMegabyteOrKilobyteString())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt
new file mode 100644
index 0000000000..c7f38b5e16
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt
@@ -0,0 +1,607 @@
+/* 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.downloads
+
+import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
+import android.content.Context
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED
+import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED
+import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED
+import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+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 mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class DownloadMiddlewareTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun `service is started when download is queued`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val download = DownloadState("https://mozilla.org/download", destinationDirectory = "")
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(downloadMiddleware).startForegroundService(intentCaptor.capture())
+ assertEquals(download.id, intentCaptor.value.getStringExtra(EXTRA_DOWNLOAD_ID))
+
+ reset(downloadMiddleware)
+
+ // We don't store private downloads in the storage.
+ val privateDownload = download.copy(id = "newId", private = true)
+
+ store.dispatch(DownloadAction.AddDownloadAction(privateDownload)).joinBlocking()
+
+ verify(downloadMiddleware, never()).saveDownload(any(), any())
+ verify(downloadMiddleware.downloadStorage, never()).add(privateDownload)
+ verify(downloadMiddleware).startForegroundService(intentCaptor.capture())
+ assertEquals(privateDownload.id, intentCaptor.value.getStringExtra(EXTRA_DOWNLOAD_ID))
+ }
+
+ @Test
+ fun `saveDownload do not store private downloads`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val privateDownload = DownloadState("https://mozilla.org/download", private = true)
+
+ store.dispatch(DownloadAction.AddDownloadAction(privateDownload)).joinBlocking()
+
+ verify(downloadMiddleware.downloadStorage, never()).add(privateDownload)
+ }
+
+ @Test
+ fun `restarted downloads MUST not be passed to the downloadStorage`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadStorage: DownloadStorage = mock()
+ val downloadMiddleware = DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ downloadStorage = downloadStorage,
+ coroutineContext = dispatcher,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ var download = DownloadState("https://mozilla.org/download", destinationDirectory = "")
+ store.dispatch(DownloadAction.RestoreDownloadStateAction(download)).joinBlocking()
+
+ verify(downloadStorage, never()).add(download)
+
+ download = DownloadState("https://mozilla.org/download", destinationDirectory = "")
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ verify(downloadStorage).add(download)
+ }
+
+ @Test
+ fun `previously added downloads MUST be ignored`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadStorage: DownloadStorage = mock()
+ val download = DownloadState("https://mozilla.org/download")
+ val downloadMiddleware = DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ downloadStorage = downloadStorage,
+ coroutineContext = dispatcher,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ downloads = mapOf(download.id to download),
+ ),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ verify(downloadStorage, never()).add(download)
+ }
+
+ @Test
+ fun `RemoveDownloadAction MUST remove from the storage`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadStorage: DownloadStorage = mock()
+ val downloadMiddleware = DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ downloadStorage = downloadStorage,
+ coroutineContext = dispatcher,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val download = DownloadState("https://mozilla.org/download", destinationDirectory = "")
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ store.dispatch(DownloadAction.RemoveDownloadAction(download.id)).joinBlocking()
+
+ verify(downloadStorage).remove(download)
+ }
+
+ @Test
+ fun `RemoveAllDownloadsAction MUST remove all downloads from the storage`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadStorage: DownloadStorage = mock()
+ val downloadMiddleware = DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ downloadStorage = downloadStorage,
+ coroutineContext = dispatcher,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val download = DownloadState("https://mozilla.org/download", destinationDirectory = "")
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ store.dispatch(DownloadAction.RemoveAllDownloadsAction).joinBlocking()
+
+ verify(downloadStorage).removeAllDownloads()
+ }
+
+ @Test
+ fun `UpdateDownloadAction MUST update the storage when changes are needed`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadStorage: DownloadStorage = mock()
+ val downloadMiddleware = DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ downloadStorage = downloadStorage,
+ coroutineContext = dispatcher,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val download = DownloadState("https://mozilla.org/download", status = INITIATED)
+ store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking()
+
+ val downloadInTheStore = store.state.downloads.getValue(download.id)
+
+ assertEquals(download, downloadInTheStore)
+
+ var updatedDownload = download.copy(status = COMPLETED, skipConfirmation = true)
+ store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload)).joinBlocking()
+
+ verify(downloadStorage).update(updatedDownload)
+
+ // skipConfirmation is value that we are not storing in the storage,
+ // changes on it shouldn't trigger an update on the storage.
+ updatedDownload = updatedDownload.copy(skipConfirmation = false)
+ store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload)).joinBlocking()
+
+ verify(downloadStorage, times(1)).update(any())
+
+ // Private downloads are not updated in the storage.
+ updatedDownload = updatedDownload.copy(private = true)
+
+ store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload)).joinBlocking()
+ verify(downloadStorage, times(1)).update(any())
+ }
+
+ @Test
+ fun `RestoreDownloadsState MUST populate the store with items in the storage`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadStorage: DownloadStorage = mock()
+ val downloadMiddleware = DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ downloadStorage = downloadStorage,
+ coroutineContext = dispatcher,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val download = DownloadState("https://mozilla.org/download")
+ whenever(downloadStorage.getDownloadsList()).thenReturn(listOf(download))
+
+ assertTrue(store.state.downloads.isEmpty())
+
+ store.dispatch(DownloadAction.RestoreDownloadsStateAction).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ assertEquals(download, store.state.downloads.values.first())
+ }
+
+ @Test
+ fun `private downloads MUST NOT be restored`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadStorage: DownloadStorage = mock()
+ val downloadMiddleware = DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ downloadStorage = downloadStorage,
+ coroutineContext = dispatcher,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val download = DownloadState("https://mozilla.org/download", private = true)
+ whenever(downloadStorage.getDownloadsList()).thenReturn(listOf(download))
+
+ assertTrue(store.state.downloads.isEmpty())
+
+ store.dispatch(DownloadAction.RestoreDownloadsStateAction).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ assertTrue(store.state.downloads.isEmpty())
+ }
+
+ @Test
+ fun `sendDownloadIntent MUST call startForegroundService WHEN downloads are NOT COMPLETED, CANCELLED and FAILED`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ ),
+ )
+
+ val ignoredStatus = listOf(COMPLETED, CANCELLED, FAILED)
+ ignoredStatus.forEach { status ->
+ val download = DownloadState("https://mozilla.org/download", status = status)
+ downloadMiddleware.sendDownloadIntent(download)
+ verify(downloadMiddleware, times(0)).startForegroundService(any())
+ }
+
+ reset(downloadMiddleware)
+
+ val allowedStatus = DownloadState.Status.values().filter { it !in ignoredStatus }
+
+ allowedStatus.forEachIndexed { index, status ->
+ val download = DownloadState("https://mozilla.org/download", status = status)
+ downloadMiddleware.sendDownloadIntent(download)
+ verify(downloadMiddleware, times(index + 1)).startForegroundService(any())
+ }
+ }
+
+ @Test
+ fun `WHEN RemoveAllTabsAction and RemoveAllPrivateTabsAction are received THEN removePrivateNotifications must be called`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val actions = listOf(TabListAction.RemoveAllTabsAction(), TabListAction.RemoveAllPrivateTabsAction)
+
+ actions.forEach {
+ store.dispatch(it).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(downloadMiddleware, times(1)).removePrivateNotifications(any())
+ reset(downloadMiddleware)
+ }
+ }
+
+ @Test
+ fun `WHEN RemoveTabsAction is received AND there is no private tabs THEN removePrivateNotifications MUST be called`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ createTab("https://www.wikipedia.com", private = true, id = "test-tab3"),
+ ),
+ ),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf("test-tab1", "test-tab3"))).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(downloadMiddleware, times(1)).removePrivateNotifications(any())
+ reset(downloadMiddleware)
+ }
+
+ @Test
+ fun `WHEN RemoveTabsAction is received AND there is a private tab THEN removePrivateNotifications MUST NOT be called`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ createTab("https://www.wikipedia.com", private = true, id = "test-tab3"),
+ ),
+ ),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf("test-tab1", "test-tab2"))).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(downloadMiddleware, times(0)).removePrivateNotifications(any())
+ reset(downloadMiddleware)
+ }
+
+ @Test
+ fun `WHEN RemoveTabAction is received AND there is no private tabs THEN removePrivateNotifications MUST be called`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", id = "test-tab2"),
+ createTab("https://www.wikipedia.com", private = true, id = "test-tab3"),
+ ),
+ ),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ store.dispatch(TabListAction.RemoveTabAction("test-tab3")).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(downloadMiddleware, times(1)).removePrivateNotifications(any())
+ }
+
+ @Test
+ fun `WHEN RemoveTabAction is received AND there is a private tab THEN removePrivateNotifications MUST NOT be called`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab1"),
+ createTab("https://www.firefox.com", private = true, id = "test-tab2"),
+ createTab("https://www.wikipedia.com", private = true, id = "test-tab3"),
+ ),
+ ),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ store.dispatch(TabListAction.RemoveTabAction("test-tab3")).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(downloadMiddleware, times(0)).removePrivateNotifications(any())
+ }
+
+ @Test
+ fun `WHEN removeStatusBarNotification is called THEN an ACTION_REMOVE_PRIVATE_DOWNLOAD intent must be created`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val download = DownloadState("https://mozilla.org/download", notificationId = 100)
+ val store = mock<BrowserStore>()
+
+ downloadMiddleware.removeStatusBarNotification(store, download)
+
+ verify(store, times(1)).dispatch(DownloadAction.DismissDownloadNotificationAction(download.id))
+ verify(applicationContext, times(1)).startService(any())
+ }
+
+ @Test
+ fun `WHEN removePrivateNotifications is called THEN removeStatusBarNotification will be called only for private download`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val download = DownloadState("https://mozilla.org/download", notificationId = 100)
+ val privateDownload = DownloadState("https://mozilla.org/download", notificationId = 100, private = true)
+ val store = BrowserStore(
+ initialState = BrowserState(
+ downloads = mapOf(download.id to download, privateDownload.id to privateDownload),
+ ),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ downloadMiddleware.removePrivateNotifications(store)
+
+ verify(downloadMiddleware, times(1)).removeStatusBarNotification(store, privateDownload)
+ }
+
+ @Test
+ fun `WHEN removePrivateNotifications is called THEN removeStatusBarNotification will be called for all private downloads`() = runTestOnMain {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val download = DownloadState("https://mozilla.org/download", notificationId = 100, sessionId = "tab1")
+ val privateDownload = DownloadState("https://mozilla.org/download", notificationId = 100, private = true, sessionId = "tab2")
+ val anotherPrivateDownload = DownloadState("https://mozilla.org/download", notificationId = 100, private = true, sessionId = "tab3")
+ val store = BrowserStore(
+ initialState = BrowserState(
+ downloads = mapOf(download.id to download, privateDownload.id to privateDownload, anotherPrivateDownload.id to anotherPrivateDownload),
+ ),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ downloadMiddleware.removePrivateNotifications(store)
+
+ verify(downloadMiddleware, times(2)).removeStatusBarNotification(any(), any())
+ }
+
+ @Test
+ fun `WHEN an action for canceling a download response is received THEN a download response must be canceled`() = runTestOnMain {
+ val response = mock<Response>()
+ val download = DownloadState(id = "downloadID", url = "example.com/5MB.zip", response = response)
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val tab = createTab("https://www.mozilla.org")
+
+ store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking()
+ store.dispatch(ContentAction.UpdateDownloadAction(tab.id, download = download)).joinBlocking()
+ store.dispatch(ContentAction.CancelDownloadAction(tab.id, download.id)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(downloadMiddleware, times(1)).closeDownloadResponse(any(), any())
+ verify(response).close()
+ }
+
+ @Test
+ fun `WHEN closing a download response THEN the response object must be closed`() {
+ val applicationContext: Context = mock()
+ val downloadMiddleware = spy(
+ DownloadMiddleware(
+ applicationContext,
+ AbstractFetchDownloadService::class.java,
+ coroutineContext = dispatcher,
+ downloadStorage = mock(),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(downloadMiddleware),
+ )
+
+ val tab = createTab("https://www.mozilla.org")
+ val response = mock<Response>()
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = tab.id, response = response)
+
+ store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking()
+ store.dispatch(ContentAction.UpdateDownloadAction(tab.id, download = download)).joinBlocking()
+
+ downloadMiddleware.closeDownloadResponse(store, tab.id)
+ verify(response).close()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
new file mode 100644
index 0000000000..c7b1abf3ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
@@ -0,0 +1,414 @@
+/* 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.downloads
+
+import android.app.PendingIntent
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.EXTRA_PROGRESS
+import androidx.core.app.NotificationCompat.EXTRA_PROGRESS_INDETERMINATE
+import androidx.core.app.NotificationCompat.EXTRA_PROGRESS_MAX
+import androidx.core.content.ContextCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class DownloadNotificationTest {
+
+ @Test
+ fun getProgress() {
+ val downloadJobState = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ )
+
+ assertEquals("10%", downloadJobState.getProgress())
+
+ val newDownload = downloadJobState.copy(state = downloadJobState.state.copy(contentLength = null))
+
+ assertEquals("", newDownload.getProgress())
+
+ val downloadWithNoSize = downloadJobState.copy(state = downloadJobState.state.copy(contentLength = 0))
+
+ assertEquals("", downloadWithNoSize.getProgress())
+
+ val downloadWithNullSize = downloadJobState.copy(state = downloadJobState.state.copy(contentLength = null))
+
+ assertEquals("", downloadWithNullSize.getProgress())
+ }
+
+ @Test
+ fun setCompatGroup() {
+ val notificationBuilder = NotificationCompat.Builder(testContext, "")
+ .setCompatGroup("myGroup").build()
+
+ assertEquals("myGroup", notificationBuilder.group)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `setCompatGroup will not set the group`() {
+ val notificationBuilder = NotificationCompat.Builder(testContext, "")
+ .setCompatGroup("myGroup").build()
+
+ assertNotEquals("myGroup", notificationBuilder.group)
+ }
+
+ @Test
+ fun getStatusDescription() {
+ val pausedText = testContext.getString(R.string.mozac_feature_downloads_paused_notification_text)
+ val completedText = testContext.getString(R.string.mozac_feature_downloads_completed_notification_text2)
+ val failedText = testContext.getString(R.string.mozac_feature_downloads_failed_notification_text2)
+ var downloadJobState = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ status = DownloadState.Status.DOWNLOADING,
+ currentBytesCopied = 10,
+ )
+
+ assertEquals(downloadJobState.getProgress(), downloadJobState.getStatusDescription(testContext))
+
+ downloadJobState = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.PAUSED,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ status = DownloadState.Status.PAUSED,
+ )
+
+ assertEquals(pausedText, downloadJobState.getStatusDescription(testContext))
+
+ downloadJobState = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.COMPLETED,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ status = DownloadState.Status.COMPLETED,
+ )
+
+ assertEquals(completedText, downloadJobState.getStatusDescription(testContext))
+
+ downloadJobState = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.FAILED,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ status = DownloadState.Status.FAILED,
+ )
+
+ assertEquals(failedText, downloadJobState.getStatusDescription(testContext))
+
+ downloadJobState = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.CANCELLED,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ status = DownloadState.Status.CANCELLED,
+ )
+
+ assertEquals("", downloadJobState.getStatusDescription(testContext))
+ }
+
+ @Test
+ fun getDownloadSummary() {
+ val download1 = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ )
+ val download2 = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla2.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 20,
+ status = DownloadState.Status.DOWNLOADING,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 20,
+ status = DownloadState.Status.DOWNLOADING,
+ )
+
+ val summary = DownloadNotification.getSummaryList(testContext, listOf(download1, download2))
+ assertEquals(listOf("mozilla.txt 10%", "mozilla2.txt 20%"), summary)
+ }
+
+ @Test
+ fun `createOngoingDownloadNotification progress does not overflow`() {
+ val size = 3 * 1024L * 1024L * 1024L
+ val copiedSize = size / 2
+ val downloadJobState = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = size,
+ currentBytesCopied = copiedSize,
+ status = DownloadState.Status.DOWNLOADING,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = copiedSize,
+ status = DownloadState.Status.DOWNLOADING,
+ )
+
+ val style = AbstractFetchDownloadService.Style()
+
+ val notification = DownloadNotification.createOngoingDownloadNotification(
+ testContext,
+ downloadJobState,
+ notificationAccentColor = style.notificationAccentColor,
+ )
+
+ assertEquals(
+ 50L,
+ 100L * notification.extras.getInt(EXTRA_PROGRESS) / notification.extras.getInt(EXTRA_PROGRESS_MAX),
+ )
+
+ assertEquals(false, notification.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE))
+
+ val notificationNewDownload = DownloadNotification.createOngoingDownloadNotification(
+ testContext,
+ downloadJobState.copy(state = downloadJobState.state.copy(contentLength = null)),
+ notificationAccentColor = style.notificationAccentColor,
+ )
+
+ assertEquals(true, notificationNewDownload.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE))
+
+ val notificationDownloadWithNoSize = DownloadNotification.createOngoingDownloadNotification(
+ testContext,
+ downloadJobState.copy(state = downloadJobState.state.copy(contentLength = 0)),
+ notificationAccentColor = style.notificationAccentColor,
+ )
+
+ assertEquals(true, notificationDownloadWithNoSize.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE))
+ }
+
+ @Test
+ fun getOngoingNotificationAccentColor() {
+ val download = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ )
+
+ val style = AbstractFetchDownloadService.Style()
+
+ val notification = DownloadNotification.createOngoingDownloadNotification(
+ testContext,
+ download,
+ notificationAccentColor = style.notificationAccentColor,
+ )
+
+ val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor)
+
+ assertEquals(accentColor, notification.color)
+ }
+
+ @Test
+ fun getPausedNotificationAccentColor() {
+ val download = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.PAUSED,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.PAUSED,
+ )
+
+ val style = AbstractFetchDownloadService.Style()
+
+ val notification = DownloadNotification.createPausedDownloadNotification(
+ testContext,
+ download,
+ notificationAccentColor = style.notificationAccentColor,
+ )
+
+ val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor)
+
+ assertEquals(accentColor, notification.color)
+ }
+
+ @Test
+ fun getCompletedNotificationAccentColor() {
+ val download = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.COMPLETED,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.COMPLETED,
+ )
+
+ val style = AbstractFetchDownloadService.Style()
+
+ val notification = DownloadNotification.createDownloadCompletedNotification(
+ testContext,
+ download,
+ notificationAccentColor = style.notificationAccentColor,
+ mock(PendingIntent::class.java),
+ )
+
+ val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor)
+
+ assertEquals(accentColor, notification.color)
+ }
+
+ @Test
+ fun getFailedNotificationAccentColor() {
+ val download = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.FAILED,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.FAILED,
+ )
+
+ val style = AbstractFetchDownloadService.Style()
+
+ val notification = DownloadNotification.createDownloadFailedNotification(
+ testContext,
+ download,
+ notificationAccentColor = style.notificationAccentColor,
+ )
+
+ val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor)
+
+ assertEquals(accentColor, notification.color)
+ }
+
+ @Test
+ fun getGroupNotificationAccentColor() {
+ val download1 = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ )
+
+ val download2 = DownloadJobState(
+ job = null,
+ state = DownloadState(
+ fileName = "mozilla.txt",
+ url = "mozilla.org/mozilla.txt",
+ contentLength = 100L,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ ),
+ foregroundServiceId = 1,
+ downloadDeleted = false,
+ currentBytesCopied = 10,
+ status = DownloadState.Status.DOWNLOADING,
+ )
+
+ val style = AbstractFetchDownloadService.Style()
+
+ val notification = DownloadNotification.createDownloadGroupNotification(
+ testContext,
+ listOf(download1, download2),
+ notificationAccentColor = style.notificationAccentColor,
+ )
+
+ val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor)
+
+ assertEquals(accentColor, notification.color)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadStorageTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadStorageTest.kt
new file mode 100644
index 0000000000..ddbfd380f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadStorageTest.kt
@@ -0,0 +1,36 @@
+/* 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.downloads
+
+import android.os.Environment
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DownloadStorageTest {
+ @Test
+ fun isSameDownload() {
+ val download = DownloadState(
+ id = "1",
+ url = "url",
+ contentType = "application/zip",
+ contentLength = 5242880,
+ status = DownloadState.Status.DOWNLOADING,
+ destinationDirectory = Environment.DIRECTORY_MUSIC,
+ )
+
+ assertTrue(DownloadStorage.isSameDownload(download, download))
+ assertFalse(DownloadStorage.isSameDownload(download, download.copy(id = "2")))
+ assertFalse(DownloadStorage.isSameDownload(download, download.copy(url = "newUrl")))
+ assertFalse(DownloadStorage.isSameDownload(download, download.copy(contentType = "contentType")))
+ assertFalse(DownloadStorage.isSameDownload(download, download.copy(contentLength = 0)))
+ assertFalse(DownloadStorage.isSameDownload(download, download.copy(status = DownloadState.Status.COMPLETED)))
+ assertFalse(DownloadStorage.isSameDownload(download, download.copy(destinationDirectory = Environment.DIRECTORY_DOWNLOADS)))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadUseCasesTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadUseCasesTest.kt
new file mode 100644
index 0000000000..feee7fd022
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadUseCasesTest.kt
@@ -0,0 +1,51 @@
+/* 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.downloads
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.DownloadAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class DownloadUseCasesTest {
+
+ @Test
+ fun consumeDownloadUseCase() {
+ val store: BrowserStore = mock()
+ val useCases = DownloadsUseCases(store)
+
+ useCases.consumeDownload("tabId", "downloadId")
+ verify(store).dispatch(ContentAction.ConsumeDownloadAction("tabId", "downloadId"))
+ }
+
+ @Test
+ fun restoreDownloadsUseCase() {
+ val store: BrowserStore = mock()
+ val useCases = DownloadsUseCases(store)
+
+ useCases.restoreDownloads()
+ verify(store).dispatch(DownloadAction.RestoreDownloadsStateAction)
+ }
+
+ @Test
+ fun removeDownloadUseCase() {
+ val store: BrowserStore = mock()
+ val useCases = DownloadsUseCases(store)
+
+ useCases.removeDownload("downloadId")
+ verify(store).dispatch(DownloadAction.RemoveDownloadAction("downloadId"))
+ }
+
+ @Test
+ fun removeAllDownloadsUseCase() {
+ val store: BrowserStore = mock()
+ val useCases = DownloadsUseCases(store)
+
+ useCases.removeAllDownloads()
+ verify(store).dispatch(DownloadAction.RemoveAllDownloadsAction)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
new file mode 100644
index 0000000000..0b956c55be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
@@ -0,0 +1,1300 @@
+/* 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.downloads
+
+import android.Manifest.permission.INTERNET
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+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.BrowserState
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.downloads.DownloadsUseCases.CancelDownloadRequestUseCase
+import mozilla.components.feature.downloads.DownloadsUseCases.ConsumeDownloadUseCase
+import mozilla.components.feature.downloads.manager.DownloadManager
+import mozilla.components.feature.downloads.ui.DownloadAppChooserDialog
+import mozilla.components.feature.downloads.ui.DownloaderApp
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+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.robolectric.grantPermission
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+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.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.shadows.ShadowToast
+
+@RunWith(AndroidJUnit4::class)
+class DownloadsFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setUp() {
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "test-tab")),
+ selectedTabId = "test-tab",
+ ),
+ )
+ }
+
+ @Test
+ fun `Adding a download object will request permissions if needed`() {
+ val fragmentManager: FragmentManager = mock()
+
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+
+ var requestedPermissions = false
+
+ val feature = DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ onNeedToRequestPermissions = { requestedPermissions = true },
+ fragmentManager = mockFragmentManager(),
+ )
+
+ feature.start()
+
+ assertFalse(requestedPermissions)
+
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertTrue(requestedPermissions)
+ verify(fragmentManager, never()).beginTransaction()
+ }
+
+ @Test
+ fun `Adding a download when permissions are granted will show dialog`() {
+ val fragmentManager: FragmentManager = mockFragmentManager()
+
+ grantPermissions()
+
+ val feature = DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ fragmentManager = fragmentManager,
+ )
+
+ feature.start()
+
+ verify(fragmentManager, never()).beginTransaction()
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(fragmentManager).beginTransaction()
+ }
+
+ @Test
+ fun `Try again calls download manager`() {
+ val fragmentManager: FragmentManager = mockFragmentManager()
+
+ val downloadManager: DownloadManager = mock()
+
+ grantPermissions()
+
+ val feature = DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ fragmentManager = fragmentManager,
+ downloadManager = downloadManager,
+ )
+
+ feature.start()
+ feature.tryAgain("0")
+
+ verify(downloadManager).tryAgain("0")
+ }
+
+ @Test
+ fun `Adding a download without a fragment manager will start download immediately`() {
+ grantPermissions()
+
+ val downloadManager: DownloadManager = mock()
+ doReturn(
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE),
+ ).`when`(downloadManager).permissions
+
+ val feature = DownloadsFeature(
+ testContext,
+ store,
+ useCases = DownloadsUseCases(store),
+ downloadManager = downloadManager,
+ )
+
+ feature.start()
+
+ verify(downloadManager, never()).download(any(), anyString())
+
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+ doReturn("id").`when`(downloadManager).download(download)
+
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(downloadManager).download(eq(download), anyString())
+ }
+
+ @Test
+ fun `Adding a Download with skipConfirmation flag will start download immediately`() {
+ val fragmentManager: FragmentManager = mockFragmentManager()
+
+ grantPermissions()
+
+ val downloadManager: DownloadManager = mock()
+ doReturn(
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE),
+ ).`when`(downloadManager).permissions
+
+ val feature = DownloadsFeature(
+ testContext,
+ store,
+ useCases = DownloadsUseCases(store),
+ fragmentManager = fragmentManager,
+ downloadManager = downloadManager,
+ )
+
+ feature.start()
+
+ verify(fragmentManager, never()).beginTransaction()
+
+ val download = DownloadState(
+ url = "https://www.mozilla.org",
+ skipConfirmation = true,
+ sessionId = "test-tab",
+ )
+
+ doReturn("id").`when`(downloadManager).download(eq(download), anyString())
+
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(fragmentManager, never()).beginTransaction()
+ verify(downloadManager).download(eq(download), anyString())
+
+ assertNull(store.state.findTab("test-tab")!!.content.download)
+ }
+
+ @Test
+ fun `When starting a download an existing dialog is reused`() {
+ grantPermissions()
+
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
+ .joinBlocking()
+
+ val dialogFragment: DownloadDialogFragment = mock()
+ val fragmentManager: FragmentManager = mock()
+ doReturn(dialogFragment).`when`(fragmentManager).findFragmentByTag(DownloadDialogFragment.FRAGMENT_TAG)
+
+ val downloadManager: DownloadManager = mock()
+ doReturn(
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE),
+ ).`when`(downloadManager).permissions
+
+ val feature = DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ downloadManager = downloadManager,
+ fragmentManager = fragmentManager,
+ )
+
+ val tab = store.state.findTab("test-tab")
+ feature.showDownloadDialog(tab!!, download)
+
+ verify(dialogFragment).onStartDownload = any()
+ verify(dialogFragment).onCancelDownload = any()
+ verify(dialogFragment).setDownload(download)
+ verify(dialogFragment, never()).showNow(any(), any())
+ }
+
+ @Test
+ fun `WHEN dismissing a download dialog THEN the download stream should be closed`() {
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val closeDownloadResponseUseCase = mock<CancelDownloadRequestUseCase>()
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+ val dialogFragment = spy(object : DownloadDialogFragment() {})
+ val fragmentManager: FragmentManager = mock()
+
+ doReturn(dialogFragment).`when`(fragmentManager).findFragmentByTag(DownloadDialogFragment.FRAGMENT_TAG)
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
+ .joinBlocking()
+ doReturn(closeDownloadResponseUseCase).`when`(downloadsUseCases).cancelDownloadRequest
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = downloadsUseCases,
+ downloadManager = mock(),
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ val tab = store.state.findTab("test-tab")
+
+ feature.showDownloadDialog(tab!!, download)
+
+ dialogFragment.onCancelDownload()
+ verify(closeDownloadResponseUseCase).invoke(anyString(), anyString())
+ }
+
+ @Test
+ fun `onPermissionsResult will start download if permissions were granted and thirdParty enabled`() {
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>()
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+ val downloadManager: DownloadManager = mock()
+ val permissionsArray = arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)
+ val grantedPermissionsArray = arrayOf(PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED).toIntArray()
+
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+ .joinBlocking()
+
+ doReturn(permissionsArray).`when`(downloadManager).permissions
+ doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = downloadsUseCases,
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { true },
+ ),
+ )
+
+ doReturn(false).`when`(feature).startDownload(any())
+
+ grantPermissions()
+
+ feature.onPermissionsResult(permissionsArray, grantedPermissionsArray)
+
+ verify(feature).startDownload(download)
+ verify(feature, never()).processDownload(any(), eq(download))
+ verify(consumeDownloadUseCase).invoke(anyString(), anyString())
+ }
+
+ @Test
+ fun `onPermissionsResult will process download if permissions were granted and thirdParty disabled`() {
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>()
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+ val downloadManager: DownloadManager = mock()
+ val permissionsArray = arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)
+ val grantedPermissionsArray = arrayOf(PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED).toIntArray()
+
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+ .joinBlocking()
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = downloadsUseCases,
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { false },
+ ),
+ )
+
+ doReturn(permissionsArray).`when`(downloadManager).permissions
+ doReturn(false).`when`(feature).processDownload(any(), any())
+
+ grantPermissions()
+
+ feature.onPermissionsResult(permissionsArray, grantedPermissionsArray)
+
+ verify(feature).processDownload(any(), eq(download))
+ verify(feature, never()).startDownload(download)
+ verify(consumeDownloadUseCase, never()).invoke(anyString(), anyString())
+ }
+
+ @Test
+ fun `onPermissionsResult will cancel the download if permissions were not granted`() {
+ val closeDownloadResponseUseCase = mock<CancelDownloadRequestUseCase>()
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ selectedTabId = "test-tab",
+ ),
+ )
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+
+ doReturn(closeDownloadResponseUseCase).`when`(downloadsUseCases).cancelDownloadRequest
+
+ store.dispatch(
+ ContentAction.UpdateDownloadAction(
+ "test-tab",
+ DownloadState("https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+
+ val downloadManager: DownloadManager = mock()
+ doReturn(
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE),
+ ).`when`(downloadManager).permissions
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = downloadsUseCases,
+ downloadManager = downloadManager,
+ ),
+ )
+
+ feature.start()
+
+ feature.onPermissionsResult(
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE),
+ arrayOf(PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_DENIED).toIntArray(),
+ )
+
+ store.waitUntilIdle()
+
+ verify(downloadManager, never()).download(any(), anyString())
+ verify(closeDownloadResponseUseCase).invoke(anyString(), anyString())
+ verify(feature).showPermissionDeniedDialog()
+ }
+
+ @Test
+ fun `Calling stop() will unregister listeners from download manager`() {
+ val downloadManager: DownloadManager = mock()
+
+ val feature = DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ downloadManager = downloadManager,
+ )
+
+ feature.start()
+
+ verify(downloadManager, never()).unregisterListeners()
+
+ feature.stop()
+
+ verify(downloadManager).unregisterListeners()
+ }
+
+ @Test
+ fun `DownloadManager failing to start download will cause error toast to be displayed`() {
+ grantPermissions()
+
+ val downloadManager: DownloadManager = mock()
+ doReturn(
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE),
+ ).`when`(downloadManager).permissions
+
+ doReturn(null).`when`(downloadManager).download(any(), anyString())
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = DownloadsUseCases(store),
+ downloadManager = downloadManager,
+ ),
+ )
+
+ doNothing().`when`(feature).showDownloadNotSupportedError()
+
+ feature.start()
+
+ verify(downloadManager, never()).download(any(), anyString())
+ verify(feature, never()).showDownloadNotSupportedError()
+
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(downloadManager).download(eq(download), anyString())
+ verify(feature).showDownloadNotSupportedError()
+ }
+
+ @Test
+ fun `showDownloadNotSupportedError shows toast`() {
+ grantPermissions()
+
+ val downloadManager: DownloadManager = mock()
+ doReturn(
+ arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE),
+ ).`when`(downloadManager).permissions
+
+ doReturn(null).`when`(downloadManager).download(any(), anyString())
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ downloadManager = downloadManager,
+ ),
+ )
+
+ feature.showDownloadNotSupportedError()
+
+ val toast = ShadowToast.getTextOfLatestToast()
+ assertNotNull(toast)
+ assertTrue(toast.contains("can’t download this file type"))
+ }
+
+ @Test
+ fun `download dialog must be added once`() {
+ val fragmentManager = mockFragmentManager()
+ val dialog = mock<DownloadDialogFragment>()
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ downloadManager = mock(),
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ feature.showDownloadDialog(mock(), mock(), dialog)
+
+ verify(dialog).showNow(fragmentManager, DownloadDialogFragment.FRAGMENT_TAG)
+ doReturn(true).`when`(feature).isAlreadyADownloadDialog()
+
+ feature.showDownloadDialog(mock(), mock(), dialog)
+ verify(dialog, times(1)).showNow(fragmentManager, DownloadDialogFragment.FRAGMENT_TAG)
+ }
+
+ @Test
+ fun `download dialog must NOT be shown WHEN the fragmentManager isDestroyed`() {
+ val fragmentManager = mockFragmentManager()
+ val dialog = mock<DownloadDialogFragment>()
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ downloadManager = mock(),
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ doReturn(false).`when`(feature).isAlreadyADownloadDialog()
+ doReturn(true).`when`(fragmentManager).isDestroyed
+
+ feature.showDownloadDialog(mock(), mock(), dialog)
+
+ verify(dialog, never()).showNow(fragmentManager, DownloadDialogFragment.FRAGMENT_TAG)
+ }
+
+ @Test
+ fun `app downloader dialog must NOT be shown WHEN the fragmentManager isDestroyed`() {
+ val fragmentManager = mockFragmentManager()
+ val dialog = mock<DownloadAppChooserDialog>()
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = mock(),
+ downloadManager = mock(),
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ doReturn(false).`when`(feature).isAlreadyADownloadDialog()
+ doReturn(true).`when`(fragmentManager).isDestroyed
+
+ feature.showAppDownloaderDialog(mock(), mock(), emptyList(), dialog)
+
+ verify(dialog, never()).showNow(fragmentManager, DownloadDialogFragment.FRAGMENT_TAG)
+ }
+
+ @Test
+ fun `processDownload only forward downloads when shouldForwardToThirdParties is true`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val downloadManager: DownloadManager = mock()
+
+ grantPermissions()
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ DownloadsUseCases(store),
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { false },
+ ),
+ )
+
+ doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions
+ doReturn(false).`when`(feature).startDownload(download)
+
+ feature.processDownload(tab, download)
+
+ verify(feature, never()).showAppDownloaderDialog(any(), any(), any(), any())
+ }
+
+ @Test
+ fun `processDownload must not forward downloads to third party apps when we are the only app that can handle the download`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = mock<DownloaderApp>()
+
+ grantPermissions()
+
+ val downloadManager: DownloadManager = mock()
+ doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ DownloadsUseCases(store),
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { true },
+ ),
+ )
+
+ doReturn(false).`when`(feature).startDownload(download)
+ doReturn(listOf(ourApp)).`when`(feature).getDownloaderApps(testContext, download)
+
+ feature.processDownload(tab, download)
+
+ verify(feature, times(0)).showAppDownloaderDialog(any(), any(), any(), any())
+ }
+
+ @Test
+ fun `processDownload MUST forward downloads to third party apps when there are multiple apps that can handle the download`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = mock<DownloaderApp>()
+ val anotherApp = mock<DownloaderApp>()
+
+ grantPermissions()
+
+ val downloadManager: DownloadManager = mock()
+ doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ DownloadsUseCases(store),
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { true },
+ ),
+ )
+
+ doReturn(false).`when`(feature).startDownload(download)
+ doNothing().`when`(feature).showAppDownloaderDialog(any(), any(), any(), any())
+ doReturn(listOf(ourApp, anotherApp)).`when`(feature).getDownloaderApps(testContext, download)
+
+ feature.processDownload(tab, download)
+
+ verify(feature).showAppDownloaderDialog(any(), any(), any(), any())
+ }
+
+ @Test
+ fun `GIVEN download should not be forwarded to third party apps but to a custom delegate WHEN processing a download request THEN forward it to the delegate`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
+ val usecases: DownloadsUseCases = mock()
+ val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
+ val cancelDownloadUseCase: CancelDownloadRequestUseCase = mock()
+ doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
+ doReturn(cancelDownloadUseCase).`when`(usecases).cancelDownloadRequest
+ val downloadManager: DownloadManager = mock()
+ var delegateFilename = ""
+ var delegateContentSize: Long = -1
+ var delegatePositiveActionCallback: (() -> Unit)? = null
+ var delegateNegativeActionCallback: (() -> Unit)? = null
+ grantPermissions()
+ doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions
+ val feature = spy(
+ DownloadsFeature(
+ applicationContext = testContext,
+ store = mock(),
+ useCases = usecases,
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { true },
+ customFirstPartyDownloadDialog = { filename, contentSize, positiveActionCallback, negativeActionCallback ->
+ delegateFilename = filename.value
+ delegateContentSize = contentSize.value
+ delegatePositiveActionCallback = positiveActionCallback.value
+ delegateNegativeActionCallback = negativeActionCallback.value
+ },
+ ),
+ )
+
+ feature.processDownload(tab, download)
+
+ assertEquals("file.txt", delegateFilename)
+ assertEquals(0, delegateContentSize)
+ assertNotNull(delegatePositiveActionCallback)
+ delegatePositiveActionCallback?.invoke()
+ verify(consumeDownloadUseCase).invoke(tab.id, download.id)
+ assertNotNull(delegateNegativeActionCallback)
+ delegateNegativeActionCallback?.invoke()
+ verify(cancelDownloadUseCase).invoke(tab.id, download.id)
+ }
+
+ @Test
+ fun `GIVEN download should be forwarded to third party apps and a custom delegate is set WHEN processing a download request THEN forward it to the delegate`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
+ val usecases: DownloadsUseCases = mock()
+ val cancelDownloadUseCase: CancelDownloadRequestUseCase = mock()
+ doReturn(cancelDownloadUseCase).`when`(usecases).cancelDownloadRequest
+ val downloadManager: DownloadManager = mock()
+ var delegateDownloaderApps: List<DownloaderApp> = emptyList()
+ var delegateChosenAppCallback: ((DownloaderApp) -> Unit)? = null
+ var delegateNegativeActionCallback: (() -> Unit)? = null
+ val ourApp = mock<DownloaderApp>()
+ val anotherApp = mock<DownloaderApp>()
+ grantPermissions()
+ doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions
+ val feature = spy(
+ DownloadsFeature(
+ applicationContext = testContext,
+ store = mock(),
+ useCases = usecases,
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { true },
+ customThirdPartyDownloadDialog = { apps, chosenAppCallback, dismissCallback ->
+ delegateDownloaderApps = apps.value
+ delegateChosenAppCallback = chosenAppCallback.value
+ delegateNegativeActionCallback = dismissCallback.value
+ },
+ ),
+ )
+ doReturn(listOf(ourApp, anotherApp)).`when`(feature).getDownloaderApps(testContext, download)
+ doNothing().`when`(feature).onDownloaderAppSelected(anotherApp, tab, download)
+
+ feature.processDownload(tab, download)
+
+ assertEquals(listOf(ourApp, anotherApp), delegateDownloaderApps)
+ assertNotNull(delegateChosenAppCallback)
+ delegateChosenAppCallback?.invoke(anotherApp)
+ verify(feature).onDownloaderAppSelected(anotherApp, tab, download)
+ assertNotNull(delegateNegativeActionCallback)
+ delegateNegativeActionCallback?.invoke()
+ verify(cancelDownloadUseCase).invoke(tab.id, download.id)
+ }
+
+ @Test
+ fun `when url is data url return only our app as downloader app`() {
+ val context = mock<Context>()
+ val download = DownloadState(url = "data:", sessionId = "test-tab")
+ val app = mock<ResolveInfo>()
+
+ val activityInfo = mock<ActivityInfo>()
+ app.activityInfo = activityInfo
+ val nonLocalizedLabel = "nonLocalizedLabel"
+ val packageName = "packageName"
+ val appName = "Fenix"
+
+ activityInfo.packageName = packageName
+ activityInfo.name = appName
+ activityInfo.exported = true
+
+ val packageManager = mock<PackageManager>()
+ whenever(context.packageManager).thenReturn(packageManager)
+ whenever(context.packageName).thenReturn(packageName)
+ whenever(app.loadLabel(packageManager)).thenReturn(nonLocalizedLabel)
+
+ val ourApp = DownloaderApp(
+ nonLocalizedLabel,
+ app,
+ packageName,
+ appName,
+ download.url,
+ download.contentType,
+ )
+
+ val mockList = listOf(app)
+ @Suppress("DEPRECATION")
+ whenever(packageManager.queryIntentActivities(any(), anyInt())).thenReturn(mockList)
+
+ val downloadManager: DownloadManager = mock()
+
+ val feature = DownloadsFeature(
+ context,
+ store,
+ DownloadsUseCases(store),
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { true },
+ )
+
+ val appList = feature.getDownloaderApps(context, download)
+
+ assertTrue(download.url.startsWith("data:"))
+ assertEquals(1, appList.size)
+ assertEquals(ourApp, appList[0])
+ }
+
+ @Test
+ fun `showAppDownloaderDialog MUST setup and show the dialog`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = mock<DownloaderApp>()
+ val anotherApp = mock<DownloaderApp>()
+ val apps = listOf(ourApp, anotherApp)
+ val dialog = mock<DownloadAppChooserDialog>()
+ val fragmentManager: FragmentManager = mockFragmentManager()
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ DownloadsUseCases(store),
+ downloadManager = mock(),
+ shouldForwardToThirdParties = { true },
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ feature.showAppDownloaderDialog(tab, download, apps, dialog)
+
+ verify(dialog).setApps(apps)
+ verify(dialog).onAppSelected = any()
+ verify(dialog).onDismiss = any()
+ verify(dialog).showNow(fragmentManager, DownloadAppChooserDialog.FRAGMENT_TAG)
+ }
+
+ @Test
+ fun `WHEN dismissing a downloader app dialog THEN the download should be canceled`() {
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>()
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = mock<DownloaderApp>()
+ val anotherApp = mock<DownloaderApp>()
+ val apps = listOf(ourApp, anotherApp)
+ val dialog = spy(DownloadAppChooserDialog())
+ val fragmentManager: FragmentManager = mockFragmentManager()
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ downloadsUseCases,
+ downloadManager = mock(),
+ shouldForwardToThirdParties = { true },
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest
+
+ feature.showAppDownloaderDialog(tab, download, apps, dialog)
+ dialog.onDismiss()
+
+ verify(cancelDownloadRequestUseCase).invoke(anyString(), anyString())
+ }
+
+ @Test
+ fun `when isAlreadyAppDownloaderDialog we must NOT show the appChooserDialog`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = mock<DownloaderApp>()
+ val anotherApp = mock<DownloaderApp>()
+ val apps = listOf(ourApp, anotherApp)
+ val dialog = mock<DownloadAppChooserDialog>()
+ val fragmentManager: FragmentManager = mockFragmentManager()
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ DownloadsUseCases(store),
+ downloadManager = mock(),
+ shouldForwardToThirdParties = { true },
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG)
+ doReturn(true).`when`(feature).isAlreadyAppDownloaderDialog()
+
+ feature.showAppDownloaderDialog(tab, download, apps)
+
+ verify(dialog).setApps(apps)
+ verify(dialog).onAppSelected = any()
+ verify(dialog).onDismiss = any()
+ verify(dialog, times(0)).showNow(fragmentManager, DownloadAppChooserDialog.FRAGMENT_TAG)
+ }
+
+ @Test
+ fun `when our app is selected for downloading and permission granted then we should perform the download`() {
+ val spyContext = spy(testContext)
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>()
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
+ val anotherApp = mock<DownloaderApp>()
+ val apps = listOf(ourApp, anotherApp)
+ val dialog = DownloadAppChooserDialog()
+ val fragmentManager: FragmentManager = mockFragmentManager()
+ val downloadManager: DownloadManager = mock()
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ downloadsUseCases,
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { true },
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ grantPermissions()
+
+ doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG)
+ doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload
+ doReturn(false).`when`(feature).startDownload(any())
+ doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions
+
+ feature.showAppDownloaderDialog(tab, download, apps)
+ dialog.onAppSelected(ourApp)
+
+ verify(feature).startDownload(any())
+ verify(consumeDownloadUseCase).invoke(anyString(), anyString())
+ verify(spyContext, times(0)).startActivity(any())
+ }
+
+ @Test
+ fun `GIVEN permissions are granted WHEN our app is selected for download THEN perform the download`() {
+ val spyContext = spy(testContext)
+ val usecases: DownloadsUseCases = mock()
+ val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
+ doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
+ val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
+ var wasPermissionsRequested = false
+ val feature = spy(
+ DownloadsFeature(
+ applicationContext = testContext,
+ store = mock(),
+ useCases = usecases,
+ onNeedToRequestPermissions = { wasPermissionsRequested = true },
+ ),
+ )
+ doReturn(false).`when`(feature).startDownload(any())
+
+ grantPermissions()
+ feature.onDownloaderAppSelected(ourApp, tab, download)
+
+ verify(feature).startDownload(download)
+ verify(consumeDownloadUseCase).invoke(tab.id, download.id)
+ assertFalse(wasPermissionsRequested)
+ verify(spyContext, never()).startActivity(any())
+ }
+
+ @Test
+ fun `GIVEN permissions are not granted WHEN our app is selected for download THEN request the needed permissions`() {
+ val spyContext = spy(testContext)
+ val usecases: DownloadsUseCases = mock()
+ val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
+ doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
+ val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
+ var wasPermissionsRequested = false
+ val feature = spy(
+ DownloadsFeature(
+ applicationContext = testContext,
+ store = mock(),
+ useCases = usecases,
+ onNeedToRequestPermissions = { wasPermissionsRequested = true },
+ ),
+ )
+
+ feature.onDownloaderAppSelected(ourApp, tab, download)
+
+ verify(feature, never()).startDownload(any())
+ verify(consumeDownloadUseCase, never()).invoke(anyString(), anyString())
+ assertTrue(wasPermissionsRequested)
+ verify(spyContext, never()).startActivity(any())
+ }
+
+ @Test
+ fun `GIVEN a download WHEN a 3rd party app is selected THEN delegate download to it`() {
+ val spyContext = spy(testContext)
+ val usecases: DownloadsUseCases = mock()
+ val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
+ doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
+ val anotherApp = DownloaderApp(
+ name = "app",
+ packageName = "test",
+ resolver = mock(),
+ activityName = "",
+ url = download.url,
+ contentType = null,
+ )
+ val feature = spy(
+ DownloadsFeature(
+ applicationContext = spyContext,
+ store = mock(),
+ useCases = usecases,
+ ),
+ )
+ val intentArgumentCaptor = argumentCaptor<Intent>()
+ val expectedIntent = with(feature) { anotherApp.toIntent() }
+
+ feature.onDownloaderAppSelected(anotherApp, tab, download)
+
+ verify(spyContext).startActivity(intentArgumentCaptor.capture())
+ assertEquals(expectedIntent.toUri(0), intentArgumentCaptor.value.toUri(0))
+ verify(consumeDownloadUseCase).invoke(tab.id, download.id)
+ verify(feature, never()).startDownload(any())
+ assertNull(ShadowToast.getTextOfLatestToast())
+ }
+
+ @Test
+ fun `GIVEN a download WHEN a 3rd party app is selected and the download fails THEN show a warning toast and consume the download`() {
+ val spyContext = spy(testContext)
+ val usecases: DownloadsUseCases = mock()
+ val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
+ doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
+ val anotherApp = DownloaderApp(
+ name = "app",
+ packageName = "test",
+ resolver = mock(),
+ activityName = "",
+ url = download.url,
+ contentType = null,
+ )
+ val feature = spy(
+ DownloadsFeature(
+ applicationContext = spyContext,
+ store = mock(),
+ useCases = usecases,
+ ),
+ )
+ val expectedWarningText = testContext.getString(
+ R.string.mozac_feature_downloads_unable_to_open_third_party_app,
+ anotherApp.name,
+ )
+ val intentArgumentCaptor = argumentCaptor<Intent>()
+ val expectedIntent = with(feature) { anotherApp.toIntent() }
+ doThrow(ActivityNotFoundException()).`when`(spyContext).startActivity(any())
+
+ feature.onDownloaderAppSelected(anotherApp, tab, download)
+
+ verify(spyContext).startActivity(intentArgumentCaptor.capture())
+ assertEquals(expectedIntent.toUri(0), intentArgumentCaptor.value.toUri(0))
+ verify(consumeDownloadUseCase).invoke(tab.id, download.id)
+ verify(feature, never()).startDownload(any())
+ assertEquals(expectedWarningText, ShadowToast.getTextOfLatestToast())
+ }
+
+ @Test
+ fun `when an app third party is selected for downloading we MUST forward the download`() {
+ val spyContext = spy(testContext)
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>()
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = DownloaderApp(name = "app", packageName = "thridparty.app", resolver = mock(), activityName = "", url = "", contentType = null)
+ val anotherApp = mock<DownloaderApp>()
+ val apps = listOf(ourApp, anotherApp)
+ val dialog = DownloadAppChooserDialog()
+ val fragmentManager: FragmentManager = mockFragmentManager()
+ val feature = spy(
+ DownloadsFeature(
+ spyContext,
+ store,
+ downloadsUseCases,
+ downloadManager = mock(),
+ shouldForwardToThirdParties = { true },
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ doReturn(false).`when`(feature).startDownload(any())
+ doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG)
+ doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload
+
+ feature.showAppDownloaderDialog(tab, download, apps)
+ dialog.onAppSelected(ourApp)
+
+ verify(feature, times(0)).startDownload(any())
+ verify(consumeDownloadUseCase).invoke(anyString(), anyString())
+ verify(spyContext).startActivity(any())
+ }
+
+ @Test
+ fun `None exception is thrown when unable to open an app third party for downloading`() {
+ val spyContext = spy(testContext)
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>()
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = DownloaderApp(name = "app", packageName = "thridparty.app", resolver = mock(), activityName = "", url = "", contentType = null)
+ val anotherApp = mock<DownloaderApp>()
+ val apps = listOf(ourApp, anotherApp)
+ val dialog = DownloadAppChooserDialog()
+ val fragmentManager: FragmentManager = mockFragmentManager()
+ val feature = spy(
+ DownloadsFeature(
+ spyContext,
+ store,
+ downloadsUseCases,
+ downloadManager = mock(),
+ shouldForwardToThirdParties = { true },
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ doThrow(ActivityNotFoundException()).`when`(spyContext).startActivity(any())
+ doReturn(false).`when`(feature).startDownload(any())
+ doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG)
+ doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload
+
+ feature.showAppDownloaderDialog(tab, download, apps)
+ dialog.onAppSelected(ourApp)
+
+ verify(feature, times(0)).startDownload(any())
+ verify(consumeDownloadUseCase).invoke(anyString(), anyString())
+ verify(spyContext).startActivity(any())
+ }
+
+ @Test
+ fun `when the appChooserDialog is dismissed THEN the download must be canceled`() {
+ val spyContext = spy(testContext)
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>()
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = mock<DownloaderApp>()
+ val anotherApp = mock<DownloaderApp>()
+ val apps = listOf(ourApp, anotherApp)
+ val dialog = DownloadAppChooserDialog()
+ val fragmentManager: FragmentManager = mockFragmentManager()
+ val feature = spy(
+ DownloadsFeature(
+ spyContext,
+ store,
+ downloadsUseCases,
+ downloadManager = mock(),
+ shouldForwardToThirdParties = { true },
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ doReturn(false).`when`(feature).startDownload(any())
+ doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG)
+ doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest
+
+ feature.showAppDownloaderDialog(tab, download, apps)
+ dialog.onDismiss()
+
+ verify(cancelDownloadRequestUseCase).invoke(anyString(), anyString())
+ }
+
+ @Test
+ fun `ResolveInfo to DownloaderApps`() {
+ val spyContext = spy(testContext)
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val info = ActivityInfo().apply {
+ packageName = "thridparty.app"
+ name = "activityName"
+ icon = android.R.drawable.btn_default
+ }
+ val resolveInfo = ResolveInfo().apply {
+ labelRes = android.R.string.ok
+ activityInfo = info
+ nonLocalizedLabel = "app"
+ }
+
+ val expectedApp = DownloaderApp(name = "app", packageName = "thridparty.app", resolver = resolveInfo, activityName = "activityName", url = download.url, contentType = download.contentType)
+
+ val app = resolveInfo.toDownloaderApp(spyContext, download)
+ assertEquals(expectedApp, app)
+ }
+
+ @Test
+ fun `previous dialogs MUST be dismissed when navigating to another website`() {
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>()
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+ .joinBlocking()
+
+ doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = downloadsUseCases,
+ downloadManager = mock(),
+ ),
+ )
+
+ doNothing().`when`(feature).dismissAllDownloadDialogs()
+ doReturn(true).`when`(feature).processDownload(any(), any())
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+ .joinBlocking()
+
+ grantPermissions()
+
+ val tab = createTab("https://www.firefox.com")
+ store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking()
+
+ verify(feature).dismissAllDownloadDialogs()
+ verify(downloadsUseCases).cancelDownloadRequest
+ assertNull(feature.previousTab)
+ }
+
+ @Test
+ fun `previous dialogs must NOT be dismissed when navigating on the same website`() {
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>()
+ val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+ .joinBlocking()
+
+ doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = downloadsUseCases,
+ downloadManager = mock(),
+ ),
+ )
+
+ doNothing().`when`(feature).dismissAllDownloadDialogs()
+ doReturn(true).`when`(feature).processDownload(any(), any())
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+ .joinBlocking()
+
+ grantPermissions()
+
+ val tab = createTab("https://www.mozilla.org/example")
+ store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking()
+
+ verify(feature, never()).dismissAllDownloadDialogs()
+ verify(downloadsUseCases, never()).cancelDownloadRequest
+ assertNotNull(feature.previousTab)
+ }
+
+ @Test
+ fun `when our app is selected for downloading and permission not granted then we should ask for permission`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
+ val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
+ val anotherApp = mock<DownloaderApp>()
+ val apps = listOf(ourApp, anotherApp)
+ val downloadManager: DownloadManager = mock()
+ var permissionsRequested = false
+ val dialog = DownloadAppChooserDialog()
+ val downloadsUseCases = spy(DownloadsUseCases(store))
+ val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>()
+ val fragmentManager: FragmentManager = mockFragmentManager()
+
+ val feature = spy(
+ DownloadsFeature(
+ testContext,
+ store,
+ useCases = downloadsUseCases,
+ downloadManager = downloadManager,
+ shouldForwardToThirdParties = { true },
+ onNeedToRequestPermissions = { permissionsRequested = true },
+ fragmentManager = fragmentManager,
+ ),
+ )
+
+ doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions
+ doReturn(testContext.packageName).`when`(spy(ourApp)).packageName
+ doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG)
+ doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload
+
+ assertFalse(permissionsRequested)
+
+ feature.showAppDownloaderDialog(tab, download, apps, dialog)
+ dialog.onAppSelected(ourApp)
+
+ assertTrue(permissionsRequested)
+
+ verify(feature, never()).startDownload(any())
+ verify(spy(testContext), never()).startActivity(any())
+ verify(consumeDownloadUseCase, never()).invoke(anyString(), anyString())
+ }
+}
+
+private fun grantPermissions() {
+ grantPermission(INTERNET, WRITE_EXTERNAL_STORAGE)
+}
+
+private fun mockFragmentManager(): FragmentManager {
+ val fragmentManager: FragmentManager = mock()
+ doReturn(mock<FragmentTransaction>()).`when`(fragmentManager).beginTransaction()
+ return fragmentManager
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragmentTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragmentTest.kt
new file mode 100644
index 0000000000..f552c9b0af
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragmentTest.kt
@@ -0,0 +1,151 @@
+/* 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.downloads
+
+import android.app.Application
+import android.graphics.drawable.GradientDrawable
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.view.marginEnd
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.robolectric.annotation.Config
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+@Config(application = TestApplication::class)
+class SimpleDownloadDialogFragmentTest {
+
+ private lateinit var dialog: SimpleDownloadDialogFragment
+ private lateinit var download: DownloadState
+ private lateinit var mockFragmentManager: FragmentManager
+
+ @Before
+ fun setup() {
+ mockFragmentManager = mock()
+ download = DownloadState(
+ "http://ipv4.download.thinkbroadband.com/5MB.zip",
+ "5MB.zip",
+ "application/zip",
+ 5242880,
+ userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36",
+ )
+ dialog = SimpleDownloadDialogFragment.newInstance()
+ }
+
+ @Test
+ fun `when the positive button is clicked onStartDownload must be called`() {
+ var isOnStartDownloadCalled = false
+
+ val onStartDownload = {
+ isOnStartDownloadCalled = true
+ }
+
+ val fragment = Mockito.spy(SimpleDownloadDialogFragment.newInstance())
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.onStartDownload = onStartDownload
+ fragment.testingContext = testContext
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val downloadDialog = fragment.onCreateDialog(null)
+ downloadDialog.show()
+
+ val positiveButton = downloadDialog.findViewById<Button>(R.id.download_button)
+ positiveButton.performClick()
+
+ assertTrue(isOnStartDownloadCalled)
+ }
+
+ @Test
+ fun `when the cancel button is clicked onCancelDownload must be called`() {
+ var isDownloadCancelledCalled = false
+
+ val onCancelDownload = {
+ isDownloadCancelledCalled = true
+ }
+
+ val fragment = Mockito.spy(SimpleDownloadDialogFragment.newInstance())
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.onCancelDownload = onCancelDownload
+ fragment.testingContext = testContext
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val downloadDialog = fragment.onCreateDialog(null)
+ downloadDialog.show()
+
+ val closeButton = downloadDialog.findViewById<ImageButton>(R.id.close_button)
+ closeButton.performClick()
+
+ assertTrue(isDownloadCancelledCalled)
+ }
+
+ @Test
+ fun `dialog must adhere to promptsStyling`() {
+ val promptsStyling = DownloadsFeature.PromptsStyling(
+ gravity = Gravity.TOP,
+ shouldWidthMatchParent = true,
+ positiveButtonBackgroundColor = android.R.color.white,
+ positiveButtonTextColor = android.R.color.black,
+ positiveButtonRadius = 4f,
+ fileNameEndMargin = 56,
+ )
+
+ val fragment = Mockito.spy(
+ SimpleDownloadDialogFragment.newInstance(
+ R.string.mozac_feature_downloads_dialog_title2,
+ R.string.mozac_feature_downloads_dialog_download,
+ 0,
+ promptsStyling,
+ ),
+ )
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ val dialogAttributes = dialog.window!!.attributes
+ val positiveButton = dialog.findViewById<Button>(R.id.download_button)
+ val filename = dialog.findViewById<TextView>(R.id.filename)
+
+ assertEquals(ContextCompat.getColor(testContext, promptsStyling.positiveButtonBackgroundColor!!), (positiveButton.background as GradientDrawable).color?.defaultColor)
+ assertEquals(promptsStyling.positiveButtonRadius!!, (positiveButton.background as GradientDrawable).cornerRadius)
+ assertEquals(ContextCompat.getColor(testContext, promptsStyling.positiveButtonTextColor!!), positiveButton.textColors.defaultColor)
+ assertTrue(dialogAttributes.gravity == Gravity.TOP)
+ assertTrue(dialogAttributes.width == ViewGroup.LayoutParams.MATCH_PARENT)
+ assertTrue(filename.marginEnd == 56)
+ }
+
+ private fun mockFragmentManager(): FragmentManager {
+ val fragmentManager: FragmentManager = mock()
+ val transaction: FragmentTransaction = mock()
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+ return fragmentManager
+ }
+}
+
+class TestApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ setTheme(appcompatR.style.Theme_AppCompat)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/db/DownloadEntityTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/db/DownloadEntityTest.kt
new file mode 100644
index 0000000000..84fe5a5310
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/db/DownloadEntityTest.kt
@@ -0,0 +1,94 @@
+/* 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.downloads.db
+
+import android.os.Environment
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DownloadEntityTest {
+
+ @Test
+ fun `convert a DownloadEntity to a DownloadState`() {
+ val downloadEntity = DownloadEntity(
+ id = "1",
+ url = "url",
+ fileName = "fileName",
+ contentType = "application/zip",
+ contentLength = 5242880,
+ status = DownloadState.Status.DOWNLOADING,
+ destinationDirectory = Environment.DIRECTORY_MUSIC,
+ createdAt = 33,
+ )
+
+ val downloadState = downloadEntity.toDownloadState()
+
+ assertEquals(downloadEntity.id, downloadState.id)
+ assertEquals(downloadEntity.url, downloadState.url)
+ assertEquals(downloadEntity.fileName, downloadState.fileName)
+ assertEquals(downloadEntity.contentType, downloadState.contentType)
+ assertEquals(downloadEntity.contentLength, downloadState.contentLength)
+ assertEquals(downloadEntity.status, downloadState.status)
+ assertEquals(downloadEntity.destinationDirectory, downloadState.destinationDirectory)
+ assertEquals(downloadEntity.createdAt, downloadState.createdTime)
+ }
+
+ @Test
+ fun `convert a DownloadState to DownloadEntity`() {
+ val downloadState = DownloadState(
+ id = "1",
+ url = "url",
+ fileName = "fileName",
+ contentType = "application/zip",
+ contentLength = 5242880,
+ status = DownloadState.Status.DOWNLOADING,
+ destinationDirectory = Environment.DIRECTORY_MUSIC,
+ private = true,
+ createdTime = 33,
+ )
+
+ val downloadEntity = downloadState.toDownloadEntity()
+
+ assertEquals(downloadState.id, downloadEntity.id)
+ assertEquals(downloadState.url, downloadEntity.url)
+ assertEquals(downloadState.fileName, downloadEntity.fileName)
+ assertEquals(downloadState.contentType, downloadEntity.contentType)
+ assertEquals(downloadState.contentLength, downloadEntity.contentLength)
+ assertEquals(downloadState.status, downloadEntity.status)
+ assertEquals(downloadState.destinationDirectory, downloadEntity.destinationDirectory)
+ assertEquals(downloadState.createdTime, downloadEntity.createdAt)
+ }
+
+ @Test
+ fun `GIVEN a download with data URL WHEN converting a DownloadState to DownloadEntity THEN data url is removed`() {
+ val downloadState = DownloadState(
+ id = "1",
+ url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==",
+ )
+
+ val downloadEntity = downloadState.toDownloadEntity()
+
+ assertEquals(downloadState.id, downloadEntity.id)
+ assertTrue(downloadEntity.url.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN a download with no data URL WHEN converting a DownloadState to DownloadEntity THEN data url is not removed`() {
+ val downloadState = DownloadState(
+ id = "1",
+ url = "url",
+ )
+
+ val downloadEntity = downloadState.toDownloadEntity()
+
+ assertEquals(downloadState.id, downloadEntity.id)
+ assertEquals(downloadState.url, downloadEntity.url)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragmentTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragmentTest.kt
new file mode 100644
index 0000000000..60c6580979
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragmentTest.kt
@@ -0,0 +1,67 @@
+/* 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.downloads.dialog
+
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.os.Looper.getMainLooper
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.R
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+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.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class DeniedPermissionDialogFragmentTest {
+
+ @Test
+ fun `WHEN showing the dialog THEN it has the provided message`() {
+ val messageId = R.string.mozac_support_base_permissions_needed_negative_button
+ val fragment = spy(
+ DeniedPermissionDialogFragment.newInstance(messageId),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val messageTextView = dialog.findViewById<TextView>(android.R.id.message)
+
+ assertEquals(fragment.message, messageId)
+ assertEquals(messageTextView.text.toString(), testContext.getString(messageId))
+ }
+
+ @Test
+ fun `WHEN clicking the positive button THEN the settings page will show`() {
+ val messageId = R.string.mozac_support_base_permissions_needed_negative_button
+
+ val fragment = spy(
+ DeniedPermissionDialogFragment.newInstance(messageId),
+ )
+
+ doNothing().`when`(fragment).dismiss()
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(fragment).openSettingsPage()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ext/DownloadStateKtTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ext/DownloadStateKtTest.kt
new file mode 100644
index 0000000000..10364fbcdd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ext/DownloadStateKtTest.kt
@@ -0,0 +1,48 @@
+/* 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.downloads.ext
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.support.utils.DownloadUtils
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DownloadStateKtTest {
+ @Test
+ fun `GIVEN a download filename is unkwnown WHEN requested with a guessing fallback THEN return a guessed canonical filename`() {
+ val download = DownloadState(
+ url = "url",
+ fileName = null,
+ )
+ val expectedName = with(download) {
+ DownloadUtils.guessFileName(null, destinationDirectory, url, contentType)
+ }
+
+ val result = download.realFilenameOrGuessed
+
+ assertEquals(expectedName, result)
+ }
+
+ @Test
+ fun `GIVEN a download filename is available WHEN requested with a guessing fallback THEN return the available filename`() {
+ val download = DownloadState(
+ url = "http://example.com/file.jpg",
+ fileName = "test",
+ contentType = "image/jpeg",
+ )
+ val guessedName = with(download) {
+ DownloadUtils.guessFileName(null, destinationDirectory, url, contentType)
+ }
+
+ val result = download.realFilenameOrGuessed
+
+ assertEquals("test", result)
+ assertNotEquals(guessedName, result)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/AndroidDownloadManagerTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/AndroidDownloadManagerTest.kt
new file mode 100644
index 0000000000..8265d376d7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/AndroidDownloadManagerTest.kt
@@ -0,0 +1,212 @@
+/* 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.downloads.manager
+
+import android.Manifest.permission.INTERNET
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
+import android.app.DownloadManager.Request
+import android.content.Intent
+import android.os.Build
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.EXTRA_DOWNLOAD_STATUS
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.grantPermission
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+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.anyString
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class AndroidDownloadManagerTest {
+
+ private lateinit var store: BrowserStore
+ private lateinit var download: DownloadState
+ private lateinit var downloadManager: AndroidDownloadManager
+
+ @Before
+ fun setup() {
+ download = DownloadState(
+ "http://ipv4.download.thinkbroadband.com/5MB.zip",
+ "",
+ "application/zip",
+ 5242880,
+ userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36",
+ )
+ store = BrowserStore()
+ downloadManager = AndroidDownloadManager(testContext, store)
+ }
+
+ @Test(expected = SecurityException::class)
+ fun `calling download without the right permission must throw an exception`() {
+ downloadManager.download(download)
+ }
+
+ @Test
+ fun `calling download must download the file`() {
+ var downloadCompleted = false
+
+ downloadManager.onDownloadStopped = { _, _, _ -> downloadCompleted = true }
+
+ grantPermissions()
+
+ assertTrue(store.state.downloads.isEmpty())
+ val id = downloadManager.download(download)!!
+ store.waitUntilIdle()
+ assertEquals(download.copy(id = id), store.state.downloads[id])
+
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+ assertTrue(downloadCompleted)
+ }
+
+ @Test
+ fun `calling tryAgain starts the download again`() {
+ var downloadStopped = false
+
+ downloadManager.onDownloadStopped = { _, _, _ -> downloadStopped = true }
+ grantPermissions()
+
+ val id = downloadManager.download(download)!!
+ store.waitUntilIdle()
+ notifyDownloadFailed(id)
+ shadowOf(getMainLooper()).idle()
+ assertTrue(downloadStopped)
+
+ downloadStopped = false
+ downloadManager.tryAgain(id)
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+ assertTrue(downloadStopped)
+ }
+
+ @Test
+ fun `trying to download a file with invalid protocol must NOT triggered a download`() {
+ val invalidDownload = download.copy(url = "ftp://ipv4.download.thinkbroadband.com/5MB.zip")
+ grantPermissions()
+
+ val id = downloadManager.download(invalidDownload)
+ assertNull(id)
+ }
+
+ @Test
+ fun `GIVEN a device that supports scoped storage THEN permissions must not included file access`() {
+ val downloadManager = spy(AndroidDownloadManager(testContext, store))
+
+ doReturn(Build.VERSION_CODES.Q).`when`(downloadManager).getSDKVersion()
+ println(downloadManager.permissions.joinToString { it })
+ assertTrue(WRITE_EXTERNAL_STORAGE !in downloadManager.permissions)
+ }
+
+ @Test
+ fun `GIVEN a device does not supports scoped storage THEN permissions must be included file access`() {
+ val downloadManager = spy(AndroidDownloadManager(testContext, store))
+
+ doReturn(Build.VERSION_CODES.P).`when`(downloadManager).getSDKVersion()
+
+ assertTrue(WRITE_EXTERNAL_STORAGE in downloadManager.permissions)
+
+ doReturn(Build.VERSION_CODES.O_MR1).`when`(downloadManager).getSDKVersion()
+
+ assertTrue(WRITE_EXTERNAL_STORAGE in downloadManager.permissions)
+ }
+
+ @Test
+ fun `sendBroadcast with valid downloadID must call onDownloadStopped after download`() {
+ var downloadCompleted = false
+ var downloadStatus: DownloadState.Status? = null
+ val downloadWithFileName = download.copy(fileName = "5MB.zip")
+
+ grantPermissions()
+
+ val id = downloadManager.download(
+ downloadWithFileName,
+ cookie = "yummy_cookie=choco",
+ )!!
+
+ downloadManager.onDownloadStopped = { _, _, status ->
+ downloadStatus = status
+ downloadCompleted = true
+ }
+
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(downloadCompleted)
+ assertEquals(DownloadState.Status.COMPLETED, downloadStatus)
+ }
+
+ @Test
+ fun `sendBroadcast with completed download`() {
+ var downloadStatus: DownloadState.Status? = null
+ val downloadWithFileName = download.copy(fileName = "5MB.zip")
+ grantPermissions()
+
+ downloadManager.onDownloadStopped = { _, _, status ->
+ downloadStatus = status
+ }
+
+ val id = downloadManager.download(
+ downloadWithFileName,
+ cookie = "yummy_cookie=choco",
+ )!!
+ store.waitUntilIdle()
+ assertEquals(downloadWithFileName.copy(id = id), store.state.downloads[id])
+
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+ assertEquals(DownloadState.Status.COMPLETED, downloadStatus)
+ }
+
+ @Test
+ fun `no null or empty headers can be added to the DownloadManager`() {
+ val mockRequest: Request = mock()
+
+ mockRequest.addRequestHeaderSafely("User-Agent", "")
+
+ verifyNoInteractions(mockRequest)
+
+ mockRequest.addRequestHeaderSafely("User-Agent", null)
+
+ verifyNoInteractions(mockRequest)
+
+ val fireFox = "Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1"
+
+ mockRequest.addRequestHeaderSafely("User-Agent", fireFox)
+
+ verify(mockRequest).addRequestHeader(anyString(), anyString())
+ }
+
+ private fun notifyDownloadFailed(id: String) {
+ val intent = Intent(ACTION_DOWNLOAD_COMPLETE)
+ intent.putExtra(android.app.DownloadManager.EXTRA_DOWNLOAD_ID, id)
+ intent.putExtra(EXTRA_DOWNLOAD_STATUS, DownloadState.Status.FAILED)
+ testContext.sendBroadcast(intent)
+ }
+
+ private fun notifyDownloadCompleted(id: String) {
+ val intent = Intent(ACTION_DOWNLOAD_COMPLETE)
+ intent.putExtra(android.app.DownloadManager.EXTRA_DOWNLOAD_ID, id)
+ intent.putExtra(EXTRA_DOWNLOAD_STATUS, DownloadState.Status.COMPLETED)
+ testContext.sendBroadcast(intent)
+ }
+
+ private fun grantPermissions() {
+ grantPermission(INTERNET, WRITE_EXTERNAL_STORAGE)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/FetchDownloadManagerTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/FetchDownloadManagerTest.kt
new file mode 100644
index 0000000000..e72061db98
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/FetchDownloadManagerTest.kt
@@ -0,0 +1,322 @@
+/* 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.downloads.manager
+
+import android.Manifest.permission.FOREGROUND_SERVICE
+import android.Manifest.permission.INTERNET
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
+import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Looper.getMainLooper
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Client
+import mozilla.components.feature.downloads.AbstractFetchDownloadService
+import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.EXTRA_DOWNLOAD_STATUS
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.grantPermission
+import mozilla.components.support.test.robolectric.testContext
+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.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class FetchDownloadManagerTest {
+
+ private lateinit var broadcastManager: LocalBroadcastManager
+ private lateinit var service: MockDownloadService
+ private lateinit var download: DownloadState
+ private lateinit var downloadManager: FetchDownloadManager<MockDownloadService>
+ private lateinit var store: BrowserStore
+ private lateinit var notificationsDelegate: NotificationsDelegate
+
+ @Before
+ fun setup() {
+ broadcastManager = LocalBroadcastManager.getInstance(testContext)
+ service = MockDownloadService()
+ store = BrowserStore()
+ notificationsDelegate = mock()
+ download = DownloadState(
+ "http://ipv4.download.thinkbroadband.com/5MB.zip",
+ "",
+ "application/zip",
+ 5242880,
+ userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36",
+ )
+ downloadManager = FetchDownloadManager(
+ testContext,
+ store,
+ MockDownloadService::class,
+ notificationsDelegate = notificationsDelegate,
+ )
+ }
+
+ @Test(expected = SecurityException::class)
+ fun `calling download without the right permission must throw an exception`() {
+ downloadManager.download(download)
+ }
+
+ @Test
+ fun `calling download must queue the download`() {
+ var downloadStopped = false
+
+ downloadManager.onDownloadStopped = { _, _, _ -> downloadStopped = true }
+
+ grantPermissions()
+
+ assertTrue(store.state.downloads.isEmpty())
+ val id = downloadManager.download(download)!!
+ store.waitUntilIdle()
+ assertEquals(download, store.state.downloads[download.id])
+
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(downloadStopped)
+ }
+
+ @Test
+ fun `sending an ACTION_DOWNLOAD_COMPLETE intent without an EXTRA_DOWNLOAD_STATUS should not crash`() {
+ var downloadStopped = false
+
+ downloadManager.onDownloadStopped = { _, _, _ -> downloadStopped = true }
+
+ grantPermissions()
+
+ assertTrue(store.state.downloads.isEmpty())
+ val id = downloadManager.download(download)!!
+ store.waitUntilIdle()
+ assertEquals(download, store.state.downloads[download.id])
+
+ // Excluding the EXTRA_DOWNLOAD_STATUS
+ val intent = Intent(ACTION_DOWNLOAD_COMPLETE)
+ intent.putExtra(EXTRA_DOWNLOAD_ID, id)
+
+ testContext.sendBroadcast(intent)
+
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(downloadStopped)
+ }
+
+ @Test
+ fun `calling tryAgain starts the download again`() {
+ val context = spy(testContext)
+ downloadManager = FetchDownloadManager(
+ context,
+ store,
+ MockDownloadService::class,
+ notificationsDelegate = notificationsDelegate,
+ )
+ var downloadStopped = false
+
+ downloadManager.onDownloadStopped = { _, _, _ -> downloadStopped = true }
+
+ grantPermissions()
+
+ val id = downloadManager.download(download)!!
+ store.waitUntilIdle()
+ notifyDownloadFailed(id)
+ shadowOf(getMainLooper()).idle()
+ assertTrue(downloadStopped)
+
+ downloadStopped = false
+ downloadManager.tryAgain(id)
+ verify(context).startService(any())
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(downloadStopped)
+ }
+
+ @Test
+ fun `GIVEN a device that supports scoped storage THEN permissions must not included file access`() {
+ val downloadManagerSpy = spy(downloadManager)
+
+ doReturn(Build.VERSION_CODES.Q).`when`(downloadManagerSpy).getSDKVersion()
+
+ assertTrue(WRITE_EXTERNAL_STORAGE !in downloadManagerSpy.permissions)
+ }
+
+ @Test
+ fun `GIVEN a device does not supports scoped storage THEN permissions must be included file access`() {
+ val downloadManagerSpy = spy(downloadManager)
+
+ doReturn(Build.VERSION_CODES.P).`when`(downloadManagerSpy).getSDKVersion()
+
+ assertTrue(WRITE_EXTERNAL_STORAGE in downloadManagerSpy.permissions)
+
+ doReturn(Build.VERSION_CODES.O_MR1).`when`(downloadManagerSpy).getSDKVersion()
+
+ assertTrue(WRITE_EXTERNAL_STORAGE in downloadManagerSpy.permissions)
+ }
+
+ @Test
+ fun `try again should not crash when download does not exist`() {
+ val context: Context = mock()
+ downloadManager = FetchDownloadManager(
+ context,
+ store,
+ MockDownloadService::class,
+ notificationsDelegate = notificationsDelegate,
+ )
+ grantPermissions()
+ val id = downloadManager.download(download)!!
+
+ downloadManager.tryAgain(id + 1)
+ verify(context, never()).startService(any())
+ }
+
+ @Test
+ fun `trying to download a file with invalid protocol must NOT triggered a download`() {
+ val invalidDownload = download.copy(url = "ftp://ipv4.download.thinkbroadband.com/5MB.zip")
+ grantPermissions()
+
+ val id = downloadManager.download(invalidDownload)
+ assertNull(id)
+ }
+
+ @Test
+ fun `trying to download a file with a blob scheme should trigger a download`() {
+ val validBlobDownload =
+ download.copy(url = "blob:https://ipv4.download.thinkbroadband.com/5MB.zip")
+ grantPermissions()
+
+ val id = downloadManager.download(validBlobDownload)!!
+ assertNotNull(id)
+ }
+
+ @Test
+ fun `trying to download a file with a moz-extension scheme should trigger a download`() {
+ val validBlobDownload =
+ download.copy(url = "moz-extension://db84fb8b-909c-4270-8567-0e947ffe379f/readerview.html?id=1&url=https%3A%2F%2Fmozilla.org")
+ grantPermissions()
+
+ val id = downloadManager.download(validBlobDownload)!!
+ assertNotNull(id)
+ }
+
+ @Test
+ fun `sendBroadcast with valid downloadID must call onDownloadStopped after download`() {
+ var downloadStopped = false
+ var downloadStatus: DownloadState.Status? = null
+ val downloadWithFileName = download.copy(fileName = "5MB.zip")
+
+ grantPermissions()
+
+ downloadManager.onDownloadStopped = { _, _, status ->
+ downloadStatus = status
+ downloadStopped = true
+ }
+
+ val id = downloadManager.download(
+ downloadWithFileName,
+ cookie = "yummy_cookie=choco",
+ )!!
+ store.waitUntilIdle()
+
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(downloadStopped)
+ assertEquals(DownloadState.Status.COMPLETED, downloadStatus)
+ }
+
+ @Test
+ fun `sendBroadcast with completed download`() {
+ var downloadStatus: DownloadState.Status? = null
+ val downloadWithFileName = download.copy(fileName = "5MB.zip")
+ grantPermissions()
+
+ downloadManager.onDownloadStopped = { _, _, status ->
+ downloadStatus = status
+ }
+
+ val id = downloadManager.download(
+ downloadWithFileName,
+ cookie = "yummy_cookie=choco",
+ )!!
+ store.waitUntilIdle()
+ assertEquals(downloadWithFileName, store.state.downloads[downloadWithFileName.id])
+
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+
+ store.waitUntilIdle()
+ assertEquals(DownloadState.Status.COMPLETED, downloadStatus)
+ }
+
+ @Test
+ fun `onReceive properly gets download object form sendBroadcast`() {
+ var downloadStopped = false
+ var downloadStatus: DownloadState.Status? = null
+ var downloadName = ""
+ var downloadSize = 0L
+ val downloadWithFileName = download.copy(fileName = "5MB.zip", contentLength = 5L)
+
+ grantPermissions()
+
+ downloadManager.onDownloadStopped = { download, _, status ->
+ downloadStatus = status
+ downloadStopped = true
+ downloadName = download.fileName ?: ""
+ downloadSize = download.contentLength ?: 0
+ }
+
+ val id = downloadManager.download(downloadWithFileName)!!
+ store.waitUntilIdle()
+ notifyDownloadCompleted(id)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(downloadStopped)
+ assertEquals("5MB.zip", downloadName)
+ assertEquals(5L, downloadSize)
+ assertEquals(DownloadState.Status.COMPLETED, downloadStatus)
+ }
+
+ private fun notifyDownloadFailed(id: String) {
+ val intent = Intent(ACTION_DOWNLOAD_COMPLETE)
+ intent.putExtra(EXTRA_DOWNLOAD_ID, id)
+ intent.putExtra(EXTRA_DOWNLOAD_STATUS, DownloadState.Status.FAILED)
+
+ testContext.sendBroadcast(intent)
+ }
+
+ private fun notifyDownloadCompleted(id: String) {
+ val intent = Intent(ACTION_DOWNLOAD_COMPLETE)
+ intent.putExtra(EXTRA_DOWNLOAD_ID, id)
+ intent.putExtra(EXTRA_DOWNLOAD_STATUS, DownloadState.Status.COMPLETED)
+
+ testContext.sendBroadcast(intent)
+ }
+
+ private fun grantPermissions() {
+ grantPermission(INTERNET, WRITE_EXTERNAL_STORAGE, FOREGROUND_SERVICE)
+ }
+
+ class MockDownloadService : AbstractFetchDownloadService() {
+ override val httpClient: Client = mock()
+ override val store: BrowserStore = mock()
+ override val notificationsDelegate: NotificationsDelegate = mock()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeatureTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeatureTest.kt
new file mode 100644
index 0000000000..db89a7cdda
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeatureTest.kt
@@ -0,0 +1,340 @@
+/* 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.downloads.temporary
+
+import android.content.Context
+import android.webkit.MimeTypeMap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.CopyInternetResourceAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+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.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+import java.io.File
+import java.nio.charset.StandardCharsets
+
+/**
+ * The 89a gif header as seen on https://www.w3.org/Graphics/GIF/spec-gif89a.txt
+ */
+private const val GIF_HEADER = "GIF89a"
+
+@RunWith(AndroidJUnit4::class)
+class CopyDownloadFeatureTest {
+ // When writing new tests initialize CopyDownloadFeature with this class' context property
+ // When creating new directories use class' context property#cacheDir as a parent
+ // This will ensure the effectiveness of @After. Otherwise leftover files may be left on the machine running tests.
+
+ private lateinit var context: Context
+ private val testCacheDirName = "testCacheDir"
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setup() {
+ // Effectively reset context mock
+ context = spy(testContext)
+ doReturn(File(testCacheDirName)).`when`(context).cacheDir
+ }
+
+ @After
+ fun cleanup() {
+ context.cacheDir.deleteRecursively()
+ }
+
+ @Test
+ fun `cleanupCache should automatically be called when this class is initialized`() = runTest {
+ val cacheDir = File(context.cacheDir, cacheDirName).also { dir ->
+ dir.mkdirs()
+ File(dir, "leftoverFile").also { file -> file.createNewFile() }
+ }
+
+ assertTrue(cacheDir.listFiles()!!.isNotEmpty())
+
+ CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)
+
+ assertTrue(cacheDir.listFiles()!!.isEmpty())
+ }
+
+ @Test
+ fun `CopyFeature starts the copy process for AddCopyAction which is immediately consumed`() {
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ "123",
+ ContentState(url = "https://www.mozilla.org"),
+ ),
+ ),
+ ),
+ ),
+ )
+ val copyFeature =
+ spy(CopyDownloadFeature(context, store, "123", mock(), mock(), dispatcher))
+ doNothing().`when`(copyFeature).startCopy(any())
+ val download = ShareInternetResourceState(url = "testDownload")
+ val action = CopyInternetResourceAction.AddCopyAction("123", download)
+ copyFeature.start()
+
+ store.dispatch(action).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(copyFeature).startCopy(download)
+ verify(store).dispatch(CopyInternetResourceAction.ConsumeCopyAction("123"))
+ }
+
+ @Test
+ fun `cleanupCache should delete all files from the cache directory`() = runTest {
+ val copyFeature =
+ spy(CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher))
+ val testDir = File(context.cacheDir, cacheDirName).also { dir ->
+ dir.mkdirs()
+ File(dir, "testFile").also { file -> file.createNewFile() }
+ }
+
+ doReturn(testDir).`when`(copyFeature).getCacheDirectory()
+ assertTrue(testDir.listFiles()!!.isNotEmpty())
+
+ copyFeature.cleanupCache()
+
+ assertTrue(testDir.listFiles()!!.isEmpty())
+ }
+
+ @Test
+ fun `startCopy() will download and then copy the selected download`() = runTest {
+ val confirmationAction = mock<() -> Unit>()
+ val copyFeature =
+ spy(
+ CopyDownloadFeature(
+ context,
+ mock(),
+ null,
+ confirmationAction,
+ mock(),
+ dispatcher,
+ ),
+ )
+ val shareState = ShareInternetResourceState(url = "testUrl", contentType = "contentType")
+ val downloadedFile = File("filePath")
+ doReturn(downloadedFile).`when`(copyFeature).download(any())
+ copyFeature.scope = scope
+
+ copyFeature.startCopy(shareState)
+
+ verify(copyFeature).download(shareState)
+ verify(copyFeature).copy(downloadedFile.canonicalPath, confirmationAction)
+ }
+
+ @Test
+ fun `download() will persist in cache the response#body() if available`() {
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)
+ val inputStream = "test".byteInputStream(StandardCharsets.UTF_8)
+ val responseFromShareState = mock<Response>()
+ doReturn(Response.Body(inputStream)).`when`(responseFromShareState).body
+ val shareState =
+ ShareInternetResourceState("randomUrl.jpg", response = responseFromShareState)
+ doReturn(Response.SUCCESS).`when`(responseFromShareState).status
+ doReturn(MutableHeaders()).`when`(responseFromShareState).headers
+
+ val result = copyFeature.download(shareState)
+
+ assertTrue(result.exists())
+ assertTrue(result.name.endsWith(".$DEFAULT_IMAGE_EXTENSION"))
+ assertEquals(cacheDirName, result.parentFile!!.name)
+ assertEquals("test", result.inputStream().bufferedReader().use { it.readText() })
+ }
+
+ @Test(expected = RuntimeException::class)
+ fun `download() will throw an error if the request is not successful`() {
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)
+ val inputStream = "test".byteInputStream(StandardCharsets.UTF_8)
+ val responseFromShareState = mock<Response>()
+ doReturn(Response.Body(inputStream)).`when`(responseFromShareState).body
+ val shareState =
+ ShareInternetResourceState("randomUrl.jpg", response = responseFromShareState)
+ doReturn(500).`when`(responseFromShareState).status
+
+ copyFeature.download(shareState)
+ }
+
+ @Test
+ fun `download() will download from the provided url the response#body() if is unavailable`() {
+ val client: Client = mock()
+ val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8)
+ doAnswer { Response("randomUrl", 200, MutableHeaders(), Response.Body(inputStream)) }
+ .`when`(client).fetch(any())
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), client, dispatcher)
+ val shareState = ShareInternetResourceState("randomUrl")
+
+ val result = copyFeature.download(shareState)
+
+ assertTrue(result.exists())
+ assertTrue(result.name.endsWith(".$DEFAULT_IMAGE_EXTENSION"))
+ assertEquals(cacheDirName, result.parentFile!!.name)
+ assertEquals("clientTest", result.inputStream().bufferedReader().use { it.readText() })
+ }
+
+ @Test
+ fun `download() will create a not private Request if not in private mode`() {
+ val client: Client = mock()
+ val requestCaptor = argumentCaptor<Request>()
+ val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8)
+ doAnswer {
+ Response(
+ "randomUrl.png",
+ 200,
+ MutableHeaders(),
+ Response.Body(inputStream),
+ )
+ }
+ .`when`(client).fetch(requestCaptor.capture())
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), client, dispatcher)
+ val shareState = ShareInternetResourceState("randomUrl.png", private = false)
+
+ copyFeature.download(shareState)
+
+ assertFalse(requestCaptor.value.private)
+ }
+
+ @Test
+ fun `download() will create a private Request if in private mode`() {
+ val client: Client = mock()
+ val requestCaptor = argumentCaptor<Request>()
+ val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8)
+ doAnswer {
+ Response(
+ "randomUrl.png",
+ 200,
+ MutableHeaders(),
+ Response.Body(inputStream),
+ )
+ }
+ .`when`(client).fetch(requestCaptor.capture())
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), client, dispatcher)
+ val shareState = ShareInternetResourceState("randomUrl.png", private = true)
+
+ copyFeature.download(shareState)
+
+ assertTrue(requestCaptor.value.private)
+ }
+
+ @Test
+ fun `getFilename(extension) will return a String with the extension suffix`() {
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)
+ val testExtension = "testExtension"
+
+ val result = copyFeature.getFilename(testExtension)
+
+ assertTrue(result.endsWith(testExtension))
+ assertTrue(result.length > testExtension.length)
+ }
+
+ @Test
+ fun `getTempFile(extension) will return a File from the cache dir and with name ending in extension`() {
+ val copyFeature =
+ spy(CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher))
+ val testExtension = "testExtension"
+
+ val result = copyFeature.getTempFile(testExtension)
+
+ assertTrue(result.name.endsWith(testExtension))
+ assertEquals(copyFeature.getCacheDirectory().toString(), result.parent)
+ }
+
+ @Test
+ fun `getCacheDirectory() will return a new directory in the app's cache`() {
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)
+
+ val result = copyFeature.getCacheDirectory()
+
+ assertEquals(testCacheDirName, result.parent)
+ assertEquals(cacheDirName, result.name)
+ }
+
+ @Test
+ fun `getMediaShareCacheDirectory creates the needed files if they don't exist`() {
+ val copyFeature =
+ spy(CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher))
+ assertFalse(context.cacheDir.exists())
+
+ val result = copyFeature.getMediaShareCacheDirectory()
+
+ assertEquals(cacheDirName, result.name)
+ assertTrue(result.exists())
+ }
+
+ @Test
+ fun `getFileExtension returns a default extension if one cannot be extracted`() {
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)
+
+ val result = copyFeature.getFileExtension(mock(), mock())
+
+ assertEquals(DEFAULT_IMAGE_EXTENSION, result)
+ }
+
+ @Test
+ fun `getFileExtension returns an extension based on the media type inferred from the stream`() {
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)
+ val gifStream = (GIF_HEADER + "testImage").byteInputStream(StandardCharsets.UTF_8)
+ // Add the gif mapping to a by default empty shadow of MimeTypeMap.
+ shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("gif", "image/gif")
+
+ val result = copyFeature.getFileExtension(mock(), gifStream)
+
+ assertEquals("gif", result)
+ }
+
+ @Test
+ fun `getFileExtension returns an extension based on the response headers`() {
+ val copyFeature =
+ CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)
+ val gifHeaders = MutableHeaders().apply { set(CONTENT_TYPE, "image/gif") }
+ // Add the gif mapping to a by default empty shadow of MimeTypeMap.
+ shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("gif", "image/gif")
+
+ val result = copyFeature.getFileExtension(gifHeaders, mock())
+
+ assertEquals("gif", result)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeatureTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeatureTest.kt
new file mode 100644
index 0000000000..e49ec42285
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeatureTest.kt
@@ -0,0 +1,301 @@
+/* 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.downloads.temporary
+
+import android.content.Context
+import android.webkit.MimeTypeMap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ShareInternetResourceAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.content.ShareInternetResourceState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+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.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+import java.io.File
+import java.nio.charset.StandardCharsets
+
+/**
+ * The 89a gif header as seen on https://www.w3.org/Graphics/GIF/spec-gif89a.txt
+ */
+private const val GIF_HEADER = "GIF89a"
+
+@RunWith(AndroidJUnit4::class)
+class ShareDownloadFeatureTest {
+ // When writing new tests initialize ShareDownloadFeature with this class' context property
+ // When creating new directories use class' context property#cacheDir as a parent
+ // This will ensure the effectiveness of @After. Otherwise leftover files may be left on the machine running tests.
+
+ private lateinit var context: Context
+ private val testCacheDirName = "testCacheDir"
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setup() {
+ // Effectively reset context mock
+ context = spy(testContext)
+ doReturn(File(testCacheDirName)).`when`(context).cacheDir
+ }
+
+ @After
+ fun cleanup() {
+ context.cacheDir.deleteRecursively()
+ }
+
+ @Test
+ fun `cleanupCache should automatically be called when this class is initialized`() = runTestOnMain {
+ val cacheDir = File(context.cacheDir, cacheDirName).also { dir ->
+ dir.mkdirs()
+ File(dir, "leftoverFile").also { file ->
+ file.createNewFile()
+ }
+ }
+
+ assertTrue(cacheDir.listFiles()!!.isNotEmpty())
+
+ ShareDownloadFeature(context, mock(), null, mock(), dispatcher)
+
+ assertTrue(cacheDir.listFiles()!!.isEmpty())
+ }
+
+ @Test
+ fun `ShareFeature starts the share process for AddShareAction which is immediately consumed`() {
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(TabSessionState("123", ContentState(url = "https://www.mozilla.org"))),
+ ),
+ ),
+ )
+ val shareFeature = spy(ShareDownloadFeature(context, store, "123", mock(), dispatcher))
+ doNothing().`when`(shareFeature).startSharing(any())
+ val download = ShareInternetResourceState(url = "testDownload")
+ val action = ShareInternetResourceAction.AddShareAction("123", download)
+ shareFeature.start()
+
+ store.dispatch(action).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(shareFeature).startSharing(download)
+ verify(store).dispatch(ShareInternetResourceAction.ConsumeShareAction("123"))
+ }
+
+ @Test
+ fun `cleanupCache should delete all files from the cache directory`() = runTestOnMain {
+ val shareFeature = spy(ShareDownloadFeature(context, mock(), null, mock(), dispatcher))
+ val testDir = File(context.cacheDir, cacheDirName).also { dir ->
+ dir.mkdirs()
+ File(dir, "testFile").also { file ->
+ file.createNewFile()
+ }
+ }
+
+ doReturn(testDir).`when`(shareFeature).getCacheDirectory()
+ assertTrue(testDir.listFiles()!!.isNotEmpty())
+
+ shareFeature.cleanupCache()
+
+ assertTrue(testDir.listFiles()!!.isEmpty())
+ }
+
+ @Test
+ fun `startSharing() will download and then share the selected download`() = runTestOnMain {
+ val shareFeature = spy(ShareDownloadFeature(context, mock(), null, mock(), dispatcher))
+ val shareState = ShareInternetResourceState(url = "testUrl", contentType = "contentType")
+ val downloadedFile = File("filePath")
+ doReturn(downloadedFile).`when`(shareFeature).download(any())
+ shareFeature.scope = scope
+
+ shareFeature.startSharing(shareState)
+
+ verify(shareFeature).download(shareState)
+ verify(shareFeature).share(downloadedFile.canonicalPath, "contentType", null, null)
+ }
+
+ @Test
+ fun `download() will persist in cache the response#body() if available`() {
+ val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher)
+ val inputStream = "test".byteInputStream(StandardCharsets.UTF_8)
+ val responseFromShareState = mock<Response>()
+ doReturn(Response.Body(inputStream)).`when`(responseFromShareState).body
+ val shareState = ShareInternetResourceState("randomUrl.jpg", response = responseFromShareState)
+ doReturn(Response.SUCCESS).`when`(responseFromShareState).status
+ doReturn(MutableHeaders()).`when`(responseFromShareState).headers
+
+ val result = shareFeature.download(shareState)
+
+ assertTrue(result.exists())
+ assertTrue(result.name.endsWith(".$DEFAULT_IMAGE_EXTENSION"))
+ assertEquals(cacheDirName, result.parentFile!!.name)
+ assertEquals("test", result.inputStream().bufferedReader().use { it.readText() })
+ }
+
+ @Test(expected = RuntimeException::class)
+ fun `download() will throw an error if the request is not successful`() {
+ val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher)
+ val inputStream = "test".byteInputStream(StandardCharsets.UTF_8)
+ val responseFromShareState = mock<Response>()
+ doReturn(Response.Body(inputStream)).`when`(responseFromShareState).body
+ val shareState =
+ ShareInternetResourceState("randomUrl.jpg", response = responseFromShareState)
+ doReturn(500).`when`(responseFromShareState).status
+
+ shareFeature.download(shareState)
+ }
+
+ @Test
+ fun `download() will download from the provided url the response#body() if is unavailable`() {
+ val client: Client = mock()
+ val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8)
+ doAnswer { Response("randomUrl", 200, MutableHeaders(), Response.Body(inputStream)) }
+ .`when`(client).fetch(any())
+ val shareFeature = ShareDownloadFeature(context, mock(), null, client, dispatcher)
+ val shareState = ShareInternetResourceState("randomUrl")
+
+ val result = shareFeature.download(shareState)
+
+ assertTrue(result.exists())
+ assertTrue(result.name.endsWith(".$DEFAULT_IMAGE_EXTENSION"))
+ assertEquals(cacheDirName, result.parentFile!!.name)
+ assertEquals("clientTest", result.inputStream().bufferedReader().use { it.readText() })
+ }
+
+ @Test
+ fun `download() will create a not private Request if not in private mode`() {
+ val client: Client = mock()
+ val requestCaptor = argumentCaptor<Request>()
+ val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8)
+ doAnswer { Response("randomUrl.png", 200, MutableHeaders(), Response.Body(inputStream)) }
+ .`when`(client).fetch(requestCaptor.capture())
+ val shareFeature = ShareDownloadFeature(context, mock(), null, client, dispatcher)
+ val shareState = ShareInternetResourceState("randomUrl.png", private = false)
+
+ shareFeature.download(shareState)
+
+ assertFalse(requestCaptor.value.private)
+ }
+
+ @Test
+ fun `download() will create a private Request if in private mode`() {
+ val client: Client = mock()
+ val requestCaptor = argumentCaptor<Request>()
+ val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8)
+ doAnswer { Response("randomUrl.png", 200, MutableHeaders(), Response.Body(inputStream)) }
+ .`when`(client).fetch(requestCaptor.capture())
+ val shareFeature = ShareDownloadFeature(context, mock(), null, client, dispatcher)
+ val shareState = ShareInternetResourceState("randomUrl.png", private = true)
+
+ shareFeature.download(shareState)
+
+ assertTrue(requestCaptor.value.private)
+ }
+
+ @Test
+ fun `getFilename(extension) will return a String with the extension suffix`() {
+ val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher)
+ val testExtension = "testExtension"
+
+ val result = shareFeature.getFilename(testExtension)
+
+ assertTrue(result.endsWith(testExtension))
+ assertTrue(result.length > testExtension.length)
+ }
+
+ @Test
+ fun `getTempFile(extension) will return a File from the cache dir and with name ending in extension`() {
+ val shareFeature = spy(ShareDownloadFeature(context, mock(), null, mock(), dispatcher))
+ val testExtension = "testExtension"
+
+ val result = shareFeature.getTempFile(testExtension)
+
+ assertTrue(result.name.endsWith(testExtension))
+ assertEquals(shareFeature.getCacheDirectory().toString(), result.parent)
+ }
+
+ @Test
+ fun `getCacheDirectory() will return a new directory in the app's cache`() {
+ val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher)
+
+ val result = shareFeature.getCacheDirectory()
+
+ assertEquals(testCacheDirName, result.parent)
+ assertEquals(cacheDirName, result.name)
+ }
+
+ @Test
+ fun `getMediaShareCacheDirectory creates the needed files if they don't exist`() {
+ val shareFeature = spy(ShareDownloadFeature(context, mock(), null, mock(), dispatcher))
+ assertFalse(context.cacheDir.exists())
+
+ val result = shareFeature.getMediaShareCacheDirectory()
+
+ assertEquals(cacheDirName, result.name)
+ assertTrue(result.exists())
+ }
+
+ @Test
+ fun `getFileExtension returns a default extension if one cannot be extracted`() {
+ val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher)
+
+ val result = shareFeature.getFileExtension(mock(), mock())
+
+ assertEquals(DEFAULT_IMAGE_EXTENSION, result)
+ }
+
+ @Test
+ fun `getFileExtension returns an extension based on the media type inferred from the stream`() {
+ val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher)
+ val gifStream = (GIF_HEADER + "testImage").byteInputStream(StandardCharsets.UTF_8)
+ // Add the gif mapping to a by default empty shadow of MimeTypeMap.
+ shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("gif", "image/gif")
+
+ val result = shareFeature.getFileExtension(mock(), gifStream)
+
+ assertEquals("gif", result)
+ }
+
+ @Test
+ fun `getFileExtension returns an extension based on the response headers`() {
+ val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher)
+ val gifHeaders = MutableHeaders().apply {
+ set(CONTENT_TYPE, "image/gif")
+ }
+ // Add the gif mapping to a by default empty shadow of MimeTypeMap.
+ shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("gif", "image/gif")
+
+ val result = shareFeature.getFileExtension(gifHeaders, mock())
+
+ assertEquals("gif", result)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialogTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialogTest.kt
new file mode 100644
index 0000000000..d47fa56571
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialogTest.kt
@@ -0,0 +1,130 @@
+/* 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.downloads.ui
+
+import android.app.Application
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.DownloadState
+import mozilla.components.feature.downloads.R
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertTrue
+import org.junit.Before
+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.annotation.Config
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+@Config(application = TestApplication::class)
+class DownloadAppChooserDialogTest {
+
+ private lateinit var dialog: DownloadAppChooserDialog
+ private lateinit var download: DownloadState
+ private lateinit var mockFragmentManager: FragmentManager
+
+ @Before
+ fun setup() {
+ mockFragmentManager = mock()
+ download = DownloadState(
+ "http://ipv4.download.thinkbroadband.com/5MB.zip",
+ "5MB.zip",
+ "application/zip",
+ 5242880,
+ userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36",
+ )
+ dialog = DownloadAppChooserDialog.newInstance()
+ }
+
+ @Test
+ fun `when an app is selected onAppSelected must be called`() {
+ var onAppSelectedWasCalled = false
+ val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
+ val apps = listOf(ourApp)
+
+ val onAppSelected: ((DownloaderApp) -> Unit) = {
+ onAppSelectedWasCalled = true
+ }
+
+ val fragment = spy(DownloadAppChooserDialog.newInstance())
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.setApps(apps)
+ fragment.onAppSelected = onAppSelected
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val downloadDialog = fragment.onCreateDialog(null)
+ downloadDialog.show()
+
+ val adapter = downloadDialog.findViewById<RecyclerView>(R.id.apps_list).adapter
+ val holder = adapter!!.onCreateViewHolder(LinearLayout(testContext), 0)
+ adapter.bindViewHolder(holder, 0)
+
+ holder.itemView.performClick()
+
+ assertTrue(onAppSelectedWasCalled)
+ }
+
+ @Test
+ fun `when the cancel button is clicked onDismiss must be called`() {
+ var isDismissCalled = false
+
+ val onDismiss = {
+ isDismissCalled = true
+ }
+
+ val fragment = spy(DownloadAppChooserDialog.newInstance())
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.onDismiss = onDismiss
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val downloadDialog = fragment.onCreateDialog(null)
+ downloadDialog.show()
+
+ val closeButton = downloadDialog.findViewById<AppCompatImageButton>(R.id.close_button)
+ closeButton.performClick()
+
+ assertTrue(isDismissCalled)
+ }
+
+ @Test
+ fun `dialog must adhere to promptsStyling`() {
+ val fragment = spy(DownloadAppChooserDialog.newInstance(Gravity.TOP, true))
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ val dialogAttributes = dialog.window!!.attributes
+
+ assertTrue(dialogAttributes.gravity == Gravity.TOP)
+ assertTrue(dialogAttributes.width == ViewGroup.LayoutParams.MATCH_PARENT)
+ }
+
+ private fun mockFragmentManager(): FragmentManager {
+ val fragmentManager: FragmentManager = mock()
+ val transaction: FragmentTransaction = mock()
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+ return fragmentManager
+ }
+}
+
+class TestApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ setTheme(appcompatR.style.Theme_AppCompat)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapterTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapterTest.kt
new file mode 100644
index 0000000000..2ea423b202
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapterTest.kt
@@ -0,0 +1,48 @@
+/* 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.downloads.ui
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertTrue
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(application = TestApplication::class)
+class DownloaderAppAdapterTest {
+
+ @Test
+ fun `bind apps`() {
+ val nameLabel = spy(TextView(testContext))
+ val iconImage = spy(ImageView(testContext))
+ val ourApp = DownloaderApp(name = "app", packageName = "thridparty.app", resolver = mock(), activityName = "", url = "", contentType = null)
+ val apps = listOf(ourApp)
+ val view = View(testContext)
+ var appSelected = false
+ val viewHolder = DownloaderAppViewHolder(view, nameLabel, iconImage)
+
+ val adapter = DownloaderAppAdapter(testContext, apps) {
+ appSelected = true
+ }
+
+ adapter.onBindViewHolder(viewHolder, 0)
+ view.performClick()
+
+ verify(nameLabel).text = ourApp.name
+ verify(iconImage).setImageDrawable(any())
+ assertTrue(appSelected)
+ assertEquals(ourApp, view.tag)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/downloads/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/downloads/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/downloads/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/downloads/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/downloads/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/findinpage/README.md b/mobile/android/android-components/components/feature/findinpage/README.md
new file mode 100644
index 0000000000..b80b81b073
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/README.md
@@ -0,0 +1,84 @@
+# [Android Components](../../../README.md) > Feature > Find In Page
+
+A feature that provides [Find in Page functionality](https://support.mozilla.org/en-US/kb/search-contents-current-page-text-or-links).
+
+## Usage
+
+### 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-findinpage:{latest-version}"
+```
+
+### Adding feature to application
+
+To use this feature you have to do two things:
+
+**1. Add the `FindInPageBar` widget to you layout:**
+
+```xml
+<mozilla.components.feature.findinpage.view.FindInPageBar
+ android:id="@+id/find_in_page"
+ android:layout_width="match_parent"
+ android:background="#FFFFFFFF"
+ android:elevation="10dp"
+ android:layout_height="56dp"
+ android:padding="4dp" />
+```
+
+These are the properties that you can customize of this widget.
+```xml
+<attr name="findInPageQueryTextColor" format="reference|color"/>
+<attr name="findInPageQueryHintTextColor" format="reference|color"/>
+<attr name="findInPageQueryTextSize" format="dimension"/>
+<attr name="findInPageResultCountTextColor" format="reference|color"/>
+<attr name="findInPageResultCountTextSize" format="dimension"/>
+<attr name="findInPageButtonsTint" format="reference|color"/>
+<attr name="findInPageNoMatchesTextColor" format="reference|color"/>
+```
+
+**2. Add the `FindInPageFeature` to your activity/fragment:**
+
+```kotlin
+val findInPageBar = layout.findViewById<FindInPageBar>(R.id.find_in_page)
+
+val findInPageFeature = FindInPageFeature(
+ sessionManager,
+ findInPageView
+) {
+ // Optional: Handle clicking of "close" button.
+}
+
+lifecycle.addObservers(findInPageFeature)
+
+// To show "Find in Page" results for a `Session`:
+findInPageFeature.bind(session)
+```
+
+🦊 A practical example of using feature find in page can be found in [Sample Browser](https://github.com/mozilla-mobile/android-components/tree/main/samples/browser).
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Extras | Description |
+|--------|----------|---------------|-----------------------------------------------------|
+| CLICK | previous | | The user clicked the previous result button. |
+| CLICK | next | | The user clicked the next result button. |
+| CLICK | close | | The user clicked the close button. |
+| COMMIT | input | `inputExtras` | The user committed a query to be found on the page. |
+
+
+#### `inputExtras`
+
+| Key | Type | Value |
+|-------|--------|-----------------------------|
+| value | String | The query that was searched |
+
+## 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/findinpage/build.gradle b/mobile/android/android-components/components/feature/findinpage/build.gradle
new file mode 100644
index 0000000000..7abc2177d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/build.gradle
@@ -0,0 +1,48 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.findinpage'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':support-ktx')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_constraintlayout
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':support-test')
+}
+
+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/findinpage/proguard-rules.pro b/mobile/android/android-components/components/feature/findinpage/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/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/findinpage/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/findinpage/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/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/findinpage/src/main/java/mozilla/components/feature/findinpage/FindInPageFeature.kt b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/FindInPageFeature.kt
new file mode 100644
index 0000000000..707748db8c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/FindInPageFeature.kt
@@ -0,0 +1,75 @@
+/* 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.findinpage
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.findinpage.internal.FindInPageInteractor
+import mozilla.components.feature.findinpage.internal.FindInPagePresenter
+import mozilla.components.feature.findinpage.view.FindInPageView
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+
+/**
+ * Feature implementation that will keep a [FindInPageView] in sync with a bound [SessionState].
+ */
+class FindInPageFeature(
+ store: BrowserStore,
+ view: FindInPageView,
+ engineView: EngineView,
+ private val onClose: (() -> Unit)? = null,
+) : LifecycleAwareFeature, UserInteractionHandler {
+ @VisibleForTesting internal var presenter = FindInPagePresenter(store, view)
+
+ @VisibleForTesting internal var interactor = FindInPageInteractor(this, view, engineView)
+
+ private var session: SessionState? = null
+
+ override fun start() {
+ presenter.start()
+ interactor.start()
+ }
+
+ override fun stop() {
+ presenter.stop()
+ interactor.stop()
+ }
+
+ /**
+ * Binds this feature to the given [SessionState]. Until unbound the [FindInPageView] will be
+ * updated presenting the current "Find in Page" state.
+ */
+ fun bind(session: SessionState) {
+ this.session = session
+
+ presenter.bind(session)
+ interactor.bind(session)
+ }
+
+ /**
+ * Returns true if the back button press was handled and the feature unbound from a session.
+ */
+ override fun onBackPressed(): Boolean {
+ return if (session != null) {
+ unbind()
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Unbinds the feature from a previously bound [SessionState]. The [FindInPageView] will be
+ * cleared and not be updated to present the "Find in Page" state anymore.
+ */
+ fun unbind() {
+ session = null
+ presenter.unbind()
+ interactor.unbind()
+ onClose?.invoke()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/facts/FindInPageFacts.kt b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/facts/FindInPageFacts.kt
new file mode 100644
index 0000000000..9865ebdc3c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/facts/FindInPageFacts.kt
@@ -0,0 +1,46 @@
+/* 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.findinpage.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [FindInPageFeature]
+ */
+class FindInPageFacts {
+ /**
+ * Items that specify which portion of the [FindInPageFeature] was interacted with
+ */
+ object Items {
+ const val PREVIOUS = "previous"
+ const val NEXT = "next"
+ const val CLOSE = "close"
+ const val INPUT = "input"
+ }
+}
+
+private fun emitFindInPageFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_FINDINPAGE,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitCloseFact() = emitFindInPageFact(Action.CLICK, FindInPageFacts.Items.CLOSE)
+internal fun emitNextFact() = emitFindInPageFact(Action.CLICK, FindInPageFacts.Items.NEXT)
+internal fun emitPreviousFact() = emitFindInPageFact(Action.CLICK, FindInPageFacts.Items.PREVIOUS)
+internal fun emitCommitFact(value: String) =
+ emitFindInPageFact(Action.COMMIT, FindInPageFacts.Items.INPUT, value)
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/internal/FindInPageInteractor.kt b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/internal/FindInPageInteractor.kt
new file mode 100644
index 0000000000..b89b1c1baf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/internal/FindInPageInteractor.kt
@@ -0,0 +1,75 @@
+/* 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.findinpage.internal
+
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.findinpage.FindInPageFeature
+import mozilla.components.feature.findinpage.facts.emitCloseFact
+import mozilla.components.feature.findinpage.facts.emitCommitFact
+import mozilla.components.feature.findinpage.facts.emitNextFact
+import mozilla.components.feature.findinpage.facts.emitPreviousFact
+import mozilla.components.feature.findinpage.view.FindInPageView
+import mozilla.components.support.ktx.android.view.hideKeyboard
+
+/**
+ * Interactor that implements [FindInPageView.Listener] and notifies the engine or feature about actions the user
+ * performed (e.g. "find next result").
+ */
+internal class FindInPageInteractor(
+ private val feature: FindInPageFeature,
+ private val view: FindInPageView,
+ private val engineView: EngineView?,
+) : FindInPageView.Listener {
+ private var engineSession: EngineSession? = null
+
+ fun start() {
+ view.listener = this
+ }
+
+ fun stop() {
+ view.listener = null
+ }
+
+ fun bind(session: SessionState) {
+ engineSession = session.engineState.engineSession
+ }
+
+ override fun onPreviousResult() {
+ engineSession?.findNext(forward = false)
+ engineView?.asView()?.clearFocus()
+ view.asView().hideKeyboard()
+ emitPreviousFact()
+ }
+
+ override fun onNextResult() {
+ engineSession?.findNext(forward = true)
+ engineView?.asView()?.clearFocus()
+ view.asView().hideKeyboard()
+ emitNextFact()
+ }
+
+ override fun onClose() {
+ // We pass this event up to the feature. The feature is responsible for unbinding its sub components and
+ // potentially notifying other dependencies.
+ feature.unbind()
+ emitCloseFact()
+ }
+
+ fun unbind() {
+ engineSession?.clearFindMatches()
+ engineSession = null
+ }
+
+ override fun onFindAll(query: String) {
+ engineSession?.findAll(query)
+ emitCommitFact(query)
+ }
+
+ override fun onClearMatches() {
+ engineSession?.clearFindMatches()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/internal/FindInPagePresenter.kt b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/internal/FindInPagePresenter.kt
new file mode 100644
index 0000000000..04d76f8d9b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/internal/FindInPagePresenter.kt
@@ -0,0 +1,57 @@
+/* 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.findinpage.internal
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.findinpage.view.FindInPageView
+import mozilla.components.lib.state.ext.flowScoped
+
+/**
+ * Presenter that will observe [SessionState] changes and update the view whenever
+ * a find result was added.
+ */
+internal class FindInPagePresenter(
+ private val store: BrowserStore,
+ private val view: FindInPageView,
+) {
+ @Volatile
+ internal var session: SessionState? = null
+
+ private var scope: CoroutineScope? = null
+
+ fun start() {
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> session?.let { state.findTabOrCustomTab(it.id) } }
+ .distinctUntilChangedBy { it.content.findResults }
+ .collect {
+ val results = it.content.findResults
+ if (results.isNotEmpty()) {
+ view.displayResult(results.last())
+ }
+ }
+ }
+ }
+
+ fun stop() {
+ scope?.cancel()
+ }
+
+ fun bind(session: SessionState) {
+ this.session = session
+ view.private = session.content.private
+ view.focus()
+ }
+
+ fun unbind() {
+ view.clear()
+ this.session = null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/view/FindInPageBar.kt b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/view/FindInPageBar.kt
new file mode 100644
index 0000000000..5b82cb4c83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/view/FindInPageBar.kt
@@ -0,0 +1,260 @@
+/* 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.findinpage.view
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.text.Editable
+import android.text.TextWatcher
+import android.util.AttributeSet
+import android.util.TypedValue.COMPLEX_UNIT_PX
+import android.widget.EditText
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.inputmethod.EditorInfoCompat
+import mozilla.components.browser.state.state.content.FindResultState
+import mozilla.components.feature.findinpage.R
+import mozilla.components.support.ktx.android.view.hideKeyboard
+import mozilla.components.support.ktx.android.view.showKeyboard
+
+private const val DEFAULT_VALUE = 0
+
+/**
+ * A customizable "Find in page" bar implementing [FindInPageView].
+ */
+class FindInPageBar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr), FindInPageView {
+ private val styling: FindInPageBarStyling = createStyling(context, attrs, defStyleAttr)
+
+ @VisibleForTesting
+ internal val queryEditText: EditText
+
+ @VisibleForTesting
+ internal val resultsCountTextView: TextView
+
+ @VisibleForTesting
+ internal val resultFormat: String =
+ context.getString(R.string.mozac_feature_findindpage_result)
+
+ @VisibleForTesting
+ internal val accessibilityFormat: String =
+ context.getString(R.string.mozac_feature_findindpage_accessibility_result)
+
+ override var listener: FindInPageView.Listener? = null
+
+ /**
+ * Sets/gets private mode.
+ *
+ * In private mode the IME should not update any personalized data such as typing history and personalized language
+ * model based on what the user typed.
+ */
+ override var private: Boolean
+ get() = (queryEditText.imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) != 0
+ set(value) {
+ queryEditText.imeOptions = if (value) {
+ queryEditText.imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
+ } else {
+ queryEditText.imeOptions and (EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv())
+ }
+ }
+
+ init {
+ inflate(getContext(), R.layout.mozac_feature_findinpage_view, this)
+
+ queryEditText = findViewById(R.id.find_in_page_query_text)
+ resultsCountTextView = findViewById(R.id.find_in_page_result_text)
+
+ bindQueryEditText()
+ bindResultsCountView()
+ bindPreviousButton()
+ bindNextButton()
+ bindCloseButton()
+ }
+
+ internal fun onQueryChange(newQuery: String) {
+ if (newQuery.isNotBlank()) {
+ listener?.onFindAll(newQuery)
+ } else {
+ resultsCountTextView.text = ""
+ listener?.onClearMatches()
+ }
+ }
+
+ override fun focus() {
+ queryEditText.showKeyboard()
+ }
+
+ override fun clear() {
+ queryEditText.text = null
+ queryEditText.clearFocus()
+ resultsCountTextView.text = null
+ resultsCountTextView.contentDescription = null
+ }
+
+ override fun displayResult(result: FindResultState) {
+ with(result) {
+ val ordinal = if (numberOfMatches > 0) activeMatchOrdinal + 1 else activeMatchOrdinal
+ resultsCountTextView.text = String.format(resultFormat, ordinal, numberOfMatches)
+ resultsCountTextView.setTextColorIfNotDefaultValue(
+ if (numberOfMatches > 0) styling.resultCountTextColor else styling.resultNoMatchesTextColor,
+ )
+ val accessibilityLabel = String.format(accessibilityFormat, ordinal, numberOfMatches)
+ resultsCountTextView.contentDescription = accessibilityLabel
+ announceForAccessibility(accessibilityLabel)
+ }
+ }
+
+ private fun createStyling(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ ): FindInPageBarStyling {
+ val attr = context.obtainStyledAttributes(attrs, R.styleable.FindInPageBar, defStyleAttr, 0)
+
+ with(attr) {
+ return FindInPageBarStyling(
+ getColor(
+ R.styleable.FindInPageBar_findInPageQueryTextColor,
+ DEFAULT_VALUE,
+ ),
+ getColor(
+ R.styleable.FindInPageBar_findInPageQueryHintTextColor,
+ DEFAULT_VALUE,
+ ),
+ getDimensionPixelSize(
+ R.styleable.FindInPageBar_findInPageQueryTextSize,
+ DEFAULT_VALUE,
+ ),
+ getColor(
+ R.styleable.FindInPageBar_findInPageResultCountTextColor,
+ DEFAULT_VALUE,
+ ),
+ getColor(
+ R.styleable.FindInPageBar_findInPageNoMatchesTextColor,
+ DEFAULT_VALUE,
+ ),
+ getDimensionPixelSize(
+ R.styleable.FindInPageBar_findInPageResultCountTextSize,
+ DEFAULT_VALUE,
+ ),
+ getColorStateList(R.styleable.FindInPageBar_findInPageButtonsTint),
+ ).also { recycle() }
+ }
+ }
+
+ private fun bindNextButton() {
+ val nextButton = findViewById<AppCompatImageButton>(R.id.find_in_page_next_btn)
+ nextButton.setIconTintIfNotDefaultValue(styling.buttonsTint)
+ nextButton.setOnClickListener {
+ if (queryEditText.text.isNotEmpty()) {
+ listener?.onNextResult()
+ }
+ }
+ }
+
+ private fun bindPreviousButton() {
+ val previousButton = findViewById<AppCompatImageButton>(R.id.find_in_page_prev_btn)
+ previousButton.setIconTintIfNotDefaultValue(styling.buttonsTint)
+ previousButton.setOnClickListener {
+ if (queryEditText.text.isNotEmpty()) {
+ listener?.onPreviousResult()
+ }
+ }
+ }
+
+ private fun bindCloseButton() {
+ val closeButton = findViewById<AppCompatImageButton>(R.id.find_in_page_close_btn)
+ closeButton.setIconTintIfNotDefaultValue(styling.buttonsTint)
+ closeButton.setOnClickListener {
+ clear()
+ listener?.onClose()
+ }
+ }
+
+ private fun bindResultsCountView() {
+ resultsCountTextView.setTextSizeIfNotDefaultValue(styling.resultCountTextSize)
+ resultsCountTextView.setTextColorIfNotDefaultValue(styling.resultCountTextColor)
+ }
+
+ @VisibleForTesting
+ internal fun bindQueryEditText() {
+ with(queryEditText) {
+ setTextSizeIfNotDefaultValue(styling.queryTextSize)
+ setTextColorIfNotDefaultValue(styling.queryTextColor)
+ setHintTextColorIfNotDefaultValue(styling.queryHintTextColor)
+
+ addTextChangedListener(
+ object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) = Unit
+ override fun beforeTextChanged(
+ s: CharSequence?,
+ start: Int,
+ count: Int,
+ after: Int,
+ ) = Unit
+
+ override fun onTextChanged(
+ newCharacter: CharSequence?,
+ start: Int,
+ before: Int,
+ count: Int,
+ ) {
+ val newQuery = newCharacter?.toString() ?: return
+ onQueryChange(newQuery)
+ }
+ },
+ )
+
+ onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
+ if (!hasFocus) {
+ this@FindInPageBar.hideKeyboard()
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun hideKeyboard() {
+ queryEditText.hideKeyboard()
+ }
+}
+
+internal data class FindInPageBarStyling(
+ val queryTextColor: Int,
+ val queryHintTextColor: Int,
+ val queryTextSize: Int,
+ val resultCountTextColor: Int,
+ val resultNoMatchesTextColor: Int,
+ val resultCountTextSize: Int,
+ val buttonsTint: ColorStateList?,
+)
+
+private fun TextView.setTextSizeIfNotDefaultValue(newValue: Int) {
+ if (newValue != DEFAULT_VALUE) {
+ setTextSize(COMPLEX_UNIT_PX, newValue.toFloat())
+ }
+}
+
+private fun TextView.setTextColorIfNotDefaultValue(newValue: Int) {
+ if (newValue != DEFAULT_VALUE) {
+ setTextColor(newValue)
+ }
+}
+
+private fun TextView.setHintTextColorIfNotDefaultValue(newValue: Int) {
+ if (newValue != DEFAULT_VALUE) {
+ setHintTextColor(newValue)
+ }
+}
+
+private fun AppCompatImageButton.setIconTintIfNotDefaultValue(newValue: ColorStateList?) {
+ val safeValue = newValue ?: return
+ imageTintList = safeValue
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/view/FindInPageView.kt b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/view/FindInPageView.kt
new file mode 100644
index 0000000000..d95ad753ea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/java/mozilla/components/feature/findinpage/view/FindInPageView.kt
@@ -0,0 +1,54 @@
+/* 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.findinpage.view
+
+import android.view.View
+import mozilla.components.browser.state.state.content.FindResultState
+
+/**
+ * An interface for views that can display "find in page" results and related UI controls.
+ */
+interface FindInPageView {
+ /**
+ * Listener to be invoked after the user performs certain actions (e.g. "find next result").
+ */
+ var listener: Listener?
+
+ /**
+ * Sets/gets private mode.
+ *
+ * In private mode the IME should not update any personalized data such as typing history and personalized language
+ * model based on what the user typed.
+ */
+ var private: Boolean
+
+ /**
+ * Displays the given [FindResultState] state in the view.
+ */
+ fun displayResult(result: FindResultState)
+
+ /**
+ * Requests focus for the input element the user can type their query into.
+ */
+ fun focus()
+
+ /**
+ * Clears the UI state.
+ */
+ fun clear()
+
+ /**
+ * Casts this [FindInPageView] interface to an actual Android [View] object.
+ */
+ fun asView(): View = (this as View)
+
+ interface Listener {
+ fun onPreviousResult()
+ fun onNextResult()
+ fun onClose()
+ fun onFindAll(query: String)
+ fun onClearMatches()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/layout/mozac_feature_findinpage_view.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/layout/mozac_feature_findinpage_view.xml
new file mode 100644
index 0000000000..83ff8d43ef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/layout/mozac_feature_findinpage_view.xml
@@ -0,0 +1,83 @@
+<?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/. -->
+
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:focusableInTouchMode="false"
+ tools:background="#ffffffff"
+ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+ <EditText
+ android:id="@+id/find_in_page_query_text"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/mozac_feature_findinpage_query_marginStart"
+ android:background="#00000000"
+ android:ems="10"
+ android:gravity="center_vertical"
+ android:hint="@string/mozac_feature_findindpage_input"
+ android:accessibilityHeading="true"
+ android:importantForAccessibility="yes"
+ android:clickable="true"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:inputType="textNoSuggestions"
+ android:lines="1"
+ android:maxLines="1"
+ android:textSize="@dimen/mozac_feature_findinpage_query_text_size"
+ app:layout_constraintEnd_toStartOf="@id/find_in_page_result_text"
+ app:layout_constraintStart_toStartOf="parent"
+ android:importantForAutofill="no"
+ tools:ignore="UnusedAttribute"/>
+
+ <TextView
+ android:id="@+id/find_in_page_result_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/mozac_feature_findinpage_result_count_margin_end"
+ android:layout_marginStart="@dimen/mozac_feature_findinpage_result_count_margin_start"
+ android:textSize="@dimen/mozac_feature_findinpage_result_count_text_size"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/find_in_page_prev_btn"
+ app:layout_constraintStart_toEndOf="@+id/find_in_page_query_text"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="10/20"/>
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/find_in_page_prev_btn"
+ style="@style/Mozac.Feature.FindInPage.Buttons"
+ android:contentDescription="@string/mozac_feature_findindpage_previous_result"
+ app:srcCompat="@drawable/mozac_ic_chevron_up_24"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/find_in_page_next_btn"
+ app:layout_constraintStart_toEndOf="@+id/find_in_page_result_text"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/find_in_page_next_btn"
+ style="@style/Mozac.Feature.FindInPage.Buttons"
+ android:contentDescription="@string/mozac_feature_findindpage_next_result"
+ app:srcCompat="@drawable/mozac_ic_chevron_down_24"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/find_in_page_close_btn"
+ app:layout_constraintStart_toEndOf="@+id/find_in_page_prev_btn"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/find_in_page_close_btn"
+ style="@style/Mozac.Feature.FindInPage.Buttons"
+ android:contentDescription="@string/mozac_feature_findindpage_dismiss"
+ app:srcCompat="@drawable/mozac_ic_cross_24"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/find_in_page_next_btn"
+ app:layout_constraintTop_toTopOf="parent"/>
+</merge>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..5750feb0f3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-am/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">በገጽ ውስጥ ያግኙ</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d ከ%2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">የሚቀጥለውን ውጤት ያግኙ</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ያለፈውን ውጤት ያግኙ</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">በገጽ ውስጥ ማግኘትን አሰናብት</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..960c77e7aa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-an/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Mirar en a pachina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Trobar lo resultau siguient</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Trobar lo resultau anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Deixar de mirar en a pachina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..62ebe1f4b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ar/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ابحث في الصفحة</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/‏%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d من أصل %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">ابحث عن النتيجة التالية</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ابحث عن النتيجة السابقة</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">أخفِ لوحة البحث في الصفحة</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..e305c362d7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ast/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Atopar…</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Atopar el resultáu siguiente</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Atopar el resultáu anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Dexar d\'atopar</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..498b313e36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-az/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Səhifədə tap</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d nəticədən %1$d dənəsi</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Sonrakı nəticəni tap</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Əvvəlki nəticəni tap</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Səhifədə axtarışı qapat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..2657899814
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-azb/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">صفحه‌ده تاپین</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d سونوچدان %1$d سونوچ</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">سونراکی سونوچو تاپ</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">اؤنجه‌کی سونوچو تاپ</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">صفحه‌ده تاپماغی باغلا</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..d39c00e59a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-be/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Знайсці на старонцы</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d з %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Знайсці наступны вынік</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Знайсці папярэдні вынік</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Прыбраць пошук на старонцы</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..c44b85be8b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bg/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Търсене в страницата</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d от %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Отиване към следващото съвпадение</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Отиване към предишното съвпадение</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Затваряне на търсенето в страницата</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..df6d0d1192
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">পাতায় অনুসন্ধান করুন</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d এর মধ্যে %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">পরবর্তী ফলাফল খুঁজুন</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">পূর্ববর্তী ফলাফল খুঁজুন</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">পাতায় অনুসন্ধান বাদ দিন</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..070b7974cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-br/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Kavout er bajennad</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d eus %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Klask an disocʼh da-heul</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Klask an disocʼh kent</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Argas “Klask er bajenn”</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..de75977111
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-bs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Pronađi na stranici</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d od %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Pronađi sljedeći rezultat</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Pronađi prethodni rezultat</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Odbaci pronalazak na stranici</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..c3f09aa5a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ca/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Cerca a la pàgina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Cerca el següent resultat</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Cerca el resultat anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Tanca la cerca a la pàgina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..c36c2623ba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cak/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Tikanöx pa ruxaq</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d richin %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Tikanöx ri jun chik nilitäj</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Tikanöx ri jun ilitajnäq kan</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Titz\'apïx nikanöx pa ruxaq</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..f8e6ed2c5e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Pangita-a sa page</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d gawas sa %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Pangita-a ang sunod nga resulta</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Pangitaa ang niaging resulta</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Undanga ang pagpangita sulod sa page</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..3418614502
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">لە ناو پەڕگە بگەڕێ</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d لە %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">ئەنجامی داهاتوو بدۆزرەوە</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ئەنجامی پێشوو بدۆزرەوە</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">گەڕان لە پەڕە پشتگوێبخە</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..d82fe936e3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-co/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Circà in a pagina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d nant’à %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Truvà u risultatu seguente</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Truvà u risultatu precedente</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Chjode a ricerca in a pagina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..f03ea22250
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Najít na stránce</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d z %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Najít další</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Najít předchozí</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Ukončit hledání na stránce</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..7015c6119f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-cy/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Canfod ar y dudalen</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d o %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Canfod y canlyniad nesaf</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Canfod y canlyniad blaenorol</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Cau’r canfod ar y dudalen</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..736d31828f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-da/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Find på siden</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d af %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Find næste</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Find forrige</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Deaktiver find på siden</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..0fda394ed8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-de/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Seite durchsuchen</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d von %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Nächstes Ergebnis suchen</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Vorheriges Ergebnis suchen</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">&quot;Seite durchsuchen&quot; deaktivieren</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..5033e7e4cc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Na boku pytaś</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d z %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Pśiducy wuslědk namakaś</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Pjerwjejšny wuslědk namakaś</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Na boku pytaś znjemóžniś</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..0db1373049
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-el/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Εύρεση στη σελίδα</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d από %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Εύρεση επόμενου αποτελέσματος</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Εύρεση προηγούμενου αποτελέσματος</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Απόρριψη εύρεσης στη σελίδα</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..d1ab7e1e4a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Find in page</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d out of %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Find next result</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Find previous result</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Dismiss find in page</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..d1ab7e1e4a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Find in page</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d out of %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Find next result</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Find previous result</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Dismiss find in page</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..7ebfac0962
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-eo/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Serĉi en paĝo</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d el %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Trovi venontan rezulton</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Trovi antaŭan rezulton</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Fermi serĉon en paĝo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..77b5b926a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Buscar en la página</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Buscar resultado siguiente</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Buscar resultado anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Descartar la búsqueda en la página</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..dd4e856d62
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Buscar en la página</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Buscar el siguiente resultado</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Buscar el resultado anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Cerrar búsqueda en la página</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..92a062a88f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Buscar en la página</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Buscar resultado siguiente</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Buscar resultado anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Descartar buscar en página</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..92a062a88f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Buscar en la página</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Buscar resultado siguiente</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Buscar resultado anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Descartar buscar en página</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..92a062a88f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-es/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Buscar en la página</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Buscar resultado siguiente</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Buscar resultado anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Descartar buscar en página</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..5f1cefee0a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-et/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Otsi lehelt</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">tulemus %1$d, kokku %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Leia järgmine tulemus</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Leia eelmine tulemus</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Katkesta otsimine</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..99aa6f857a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-eu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Bilatu orrian</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%2$d/%1$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d(e)tik %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Bilatu hurrengo emaitza</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Bilatu aurreko emaitza</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Baztertu orrian bilatzea</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..0d677e2196
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fa/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">پیدا کردن در صفحه</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d از %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">پیدا کردن نتیجه بعدی</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">پیدا کردن نتیجه قبلی</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">رد کردن پیدا کردن در این صفحه</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..63914ba92d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ff/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Yiytu e Hello</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d e nder %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Yiytu njeñtudi paandi</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Yiytu njeñtudi njawtundi</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..c5df85a447
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Etsi sivulta</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">Osuma %1$d yhteensä %2$d osumasta</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Etsi seuraava osuma</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Etsi edellinen osuma</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Sulje Etsi sivulta -toiminto</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..5532d185ab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Rechercher dans la page</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d sur %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Trouver le résultat suivant</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Trouver le résultat précédent</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Fermer la recherche dans la page</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..519f93b5a7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fur/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Cjate te pagjine</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d di %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Cjate risultât sucessîf</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Cjate risultât precedent</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Siere cjate te pagjine</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..097876432b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Sykje op side</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d fan %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Folgjende resultaat fine</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Foarige resultaat fine</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Sykje op side slute</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..a603113b5a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Aimsigh sa leathanach</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d as %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">An chéad toradh eile</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">An toradh roimhe seo</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Dún “Aimsigh sa leathanach”</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..25f126ee4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gd/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Lorg air an duilleag</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d à %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Lorg an ath-thoradh</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Lorg an toradh roimhe</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Leig seachad an gleus “Lorg san duilleag”</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..7765be1901
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Atopar na páxina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Atopar o resultado seguinte</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Atopar o resultado anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Pechar a busca</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..ca9df85c78
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Eheka kuatiaroguépe</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d %2$d rehegua</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Eheka ambue oikopyréva</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Eheka oikopyre mboyveguáva</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Eheja kuatiaroguépe jeheka</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..b688a3d0a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">પૃષ્ઠમાં શોધો</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d માંથી %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">આગામી પરિણામ શોધો</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">અગાઉના પરિણામ શોધો</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">પૃષ્ઠમાં શોધો કાઢી નાખો</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..bd186e7442
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">पृष्ठ मे ढूंढें</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d में से %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">अगला परिणाम खोजें</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">पिछला परिणाम खोजें</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">पृष्ठ में ढूंढें हटाएं</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..2a9cfda09d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Pronađi na stranici</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d od %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Pronađi sljedeći rezultat</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Pronađi prethodni rezultat</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Odbaci „pronađi na stranici”</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..94253a5a04
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Na stronje pytać</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d z %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Přichodny wuslědk namakać</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Předchadny wuslědk namakać</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Na stronje pytać znjemóžnić</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..f67cab9870
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Keresés az oldalon</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d ennyiből: %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Következő találat</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Előző találat</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Kereső elrejtése</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..fbafa4d274
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Գտնել էջում</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d-ը %2$d-ից</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Գտնել հաջորդ արդյունքը</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Գտնել նախորդ արդյունքը</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Բաց թողնել գտնելը էջում</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..065c0e9b5d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ia/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Cercar in le pagina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Trovar le resultato successive</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Trovar le resultato previe</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Dimitter le recerca in le pagina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..e410fae139
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-in/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Temukan di laman</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d dari %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Temukan hasil selanjutnya</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Temukan hasil sebelumnya</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Tutup pencarian di laman</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..9bfda06840
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-is/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Finna á síðu</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d af %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Finna næstu niðurstöðu</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Finna fyrri niðurstöðu</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Slepptu leit á síðu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..33160a15f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-it/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Trova nella pagina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d di %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Risultato successivo</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Risultato precedente</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Chiudi trova nella pagina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..013ad1eb80
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-iw/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">חיפוש בדף</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d מתוך %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">חיפוש התוצאה הבאה</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">חיפוש התוצאה הקודמת</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">סגירת החיפוש בדף</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..aa0a233005
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ja/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ページ内検索</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d 件中 %1$d 件目</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">次の結果を検索</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">前の結果を検索</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">ページ内検索を閉じる</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..0284f99e79
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ka/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">პოვნა გვერდზე</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d შედეგი %2$d-დან</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">მომდევნო შედეგი</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">წინა შედეგი</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">ძიების გაუქმება</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..b527b8ae2d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Betten tabıw</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d dan %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Keyingi nátiyjeni tabıw</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Aldınǵı nátiyjeni tabıw</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Betten izlewdi alıp taslaw</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..9350559d46
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kab/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Nadi deg usebter</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d ɣef %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Af-d agmuḍ d-iteddun</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">af-d agmuḍ yezrin</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Mdel anadi deg usebter</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..4e8b1c5016
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Беттен табу</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d ішінен %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Келесі нәтижені табу</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Алдыңғы нәтижені табу</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Беттен іздеуді алып тастау</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..98973f1386
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Di rûpelê de bibîne</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">Ji %2$d encaman %1$d encam</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Encama pêş bibîne</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Encama paş bibîne</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">’Di rûpelê de bibîne’yê bigire</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..02bfacacad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-kn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ಪುಟದಲ್ಲಿ ಹುಡುಕು</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d ರಲ್ಲಿ %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">ಮುಂದಿನ ಫಲಿತಾಂಶವನ್ನು ಹುಡುಕಿ</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ಹಿಂದಿನ ಫಲಿತಾಂಶವನ್ನು ಹುಡುಕಿ</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">ಪುಟದಲ್ಲಿನ ಹುಡುಕು ವಜಾಗೊಳಿಸಿ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..346b282a5e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ko/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">페이지에서 찾기</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d 중 %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">다음 결과 찾기</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">이전 결과 찾기</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">페이지에서 찾기 닫기</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..3229463a47
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lij/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Treuva inta pagina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Pròscimo exito</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Exito primma</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Særa treuva inta pagina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..3a5b2a31fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lo/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ຄົ້ນຫາໃນຫນ້ານີ້</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d ບໍ່ຢູ່ໃນ %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">ຄົ້ນຫາຜົນລັບຕໍ່ໄປ</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ຄົ້ນຫາຜົນລັບກ່ອນໜ້ານີ້</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">ຍົກເລີກການຄົ້ນຫາໃນໜ້າ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..a5c87dda81
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-lt/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Rasti tinklalapyje</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d iš %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Rasti tolesnį</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Rasti ankstesnį</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Paslėpti radimo funkcijas</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..10b36bbe05
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-mix/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..069c01fa4a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ml/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">പേജിൽ കണ്ടെത്തുക</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d -ൽ %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">അടുത്ത ഫലം കണ്ടെത്തുക</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">മുമ്പത്തെ ഫലം കണ്ടെത്തുക</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">പേജിലെ തിരച്ചില്‍ നിര്‍ത്തുക</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..c39d158501
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-mr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">पृष्ठामध्ये शोधा</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d पैकी %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">पुढील परिणाम शोधा</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">मागील परिणाम शोधा</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">पृष्ठामध्ये शोधणे रद्द करा</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..e7fb0b7bcf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-my/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">စာမျက်နှာထဲတွင် ရှာပါ</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d / %2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d ရဲ့ %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">နောက်ထပ်ရလဒ် ရှာပါ</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ယခင်ရလဒ်ကိုရှာပါ</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">စာမျက်နှာတွင် ရှာဖွေမှု ဖယ်ထုတ်ပါ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..9c042e6c44
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Søk på siden</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d av %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Søk etter neste</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Søk etter forrige</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Lukk søk på siden</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..1952db3089
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">यो पृष्ठमा खोज्नुहोस्</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d मध्ये %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">अर्को परिणाम खोज्नुहोस्</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">पहिलाको परिणाम खोज्नुहोस्</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">पृष्ठमा फेला पार्नुहोस् लाई खारेज गर्नुहोस्</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..ae5167e85a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Zoeken op pagina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d van %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Volgende resultaat vinden</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Vorige resultaat vinden</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Zoeken op pagina sluiten</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..444d4bec05
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Søk på sida</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d av %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Søk etter neste</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Søk etter førre</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Lat att søk på sida</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..2278f50a02
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-oc/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Recercar dins la pagina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d sus %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Trobar lo resultat seguent</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Trobar lo resultat precedent</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Quitar la recèrca dins la pagina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..64b9dc5446
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-or/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ପୃଷ୍ଠାରେ ଖୋଜି ପାଆନ୍ତୁ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..f3a34b18fe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ਸਫ਼ੇ ‘ਚ ਲੱਭੋ</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d ‘ਚੋਂ %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">ਅਗਲਾ ਨਤੀਜਾ ਲੱਭੋ</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ਪਿਛਲਾ ਨਤੀਜਾ ਲੱਭੋ</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">ਸਫ਼ੇ ‘ਚ ਲੱਭਣ ਨੂੰ ਖ਼ਾਰਜ ਕਰੋ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..fec0a430f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">صفحے چ لبھو</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d چوں %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">اگلا نتیجہ لبھو</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">پچھلا نتیجہ لبھو</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">لبھݨ والے نوں بند کرو</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..752d58673b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Znajdź na stronie</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d z %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Znajdź następny wynik</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Znajdź poprzedni wynik</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Zamknij wyszukiwanie na stronie</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..0aef22c5ea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Procurar na página</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Próximo resultado</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Resultado anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Descartar procurar na página</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..2682a96e90
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Localizar na página</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Localizar próximo resultado</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Localizar resultado anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Dispensar localizar na página</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..a601d0c651
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-rm/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Tschertgar en la pagina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d da %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Tschertgar il proxim resultat</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Tschertgar il resultat precedent</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Serrar la tschertga en la pagina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..75f54041ea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ro/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Caută în pagină</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d din %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Mergi la rezultatul următor</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Mergi la rezultatul anterior</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Înlătură căutarea în pagină</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..20ffd0c271
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ru/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Найти на странице</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d из %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Найти следующее совпадение</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Найти предыдущее совпадение</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Убрать поиск на странице</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..0f52aca21e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sat/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ᱥᱟᱦᱴᱟ ᱨᱮ ᱯᱟᱱᱛᱮ ᱢᱮ</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d ᱠᱷᱚᱱ %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">ᱤᱱᱟᱹ ᱛᱟᱭᱚᱢᱟᱜ ᱠᱩᱲᱟᱹᱭ ᱧᱟᱢ ᱢᱮ</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ᱞᱟᱦᱟ ᱛᱮᱭᱟᱜ ᱠᱩᱲᱟᱹᱭ ᱧᱟᱢ ᱢᱮ</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">ᱥᱟᱦᱴᱟ ᱨᱮ ᱧᱟᱢ ᱚᱰᱚᱠ ᱵᱟᱫ ᱢᱮ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..f468db0d23
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sc/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Chirca in sa pàgina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Agata su resurtadu imbeniente</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Agata su resurtadu pretzedente</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Serra sa chirca in sa pàgina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..a98e3e906d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-si/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">පිටුවේ සොයන්න</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d / %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">ඊළඟ ප්‍රතිඵලය සොයන්න</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">කලින් ප්‍රතිඵලය සොයන්න</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">පිටුවේ සෙවීම ඉවතලන්න</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..78f0cb8334
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Hľadať na stránke</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d z %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Nájsť ďalší výsledok</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Nájsť predchádzajúci výsledok</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Zavrieť hľadanie na stránke</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..8a7e68969c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-skr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ورقے وچ لبھو</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d وچوں %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">اڳلا نتیجہ لبھو</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">پچھلا نتیجہ لبھو</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">ورقے وچ لبھݨ کوں فارغ کرو</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..1613c99504
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Najdi na strani</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d od %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Najdi naslednji zadetek</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Najdi prejšnji zadetek</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Končaj iskanje na strani</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..01fc4117bb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sq/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Gjej në faqe</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d nga %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Gjej përfundimin pasues</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Gjej përfundimin e mëparshëm</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Hidhe tej gjetjen në faqe</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..16e39af338
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Нађи на страници</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d од %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Нађи следећи резултат</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Нађи претходни резултат</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Одбаци претрагу на страници</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..04334577ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-su/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Téangan dina Kaca</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d ti %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Téang hasil séjén</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Téang hasil saméméhna</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Tutup téangan dina kaca</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..164444ffd9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Hitta på sidan</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d av %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Hitta nästa resultat</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Hitta föregående resultat</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Ignorera hitta på sidan</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..1871ba84d7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ta/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">பக்கத்தில் தேடுக</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d இல் %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">அடுத்த முடிவைக் கண்டறியவும்</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">முந்தைய முடிவைக் கண்டறியவும்</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">பக்கத்தில் கண்டுபிடியை இரத்து செய்</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..20b03731fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-te/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">పేజీలో వెతకండి</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$dలో %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">తదుపరి ఫలితాన్ని కనుగొనండి</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">మునుపటి ఫలితాన్ని కనుగొనండి</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">పేజీలో వెతకడాన్ని రద్దుచేయి</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..2f688579da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tg/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Ҷустуҷӯ дар саҳифа</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d аз %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Ҷустуҷӯи натиҷаи навбатӣ</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Ҷустуҷӯи натиҷаи қаблӣ</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Қатъ кардани ҷустуҷӯ дар саҳифа</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..3271f76250
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-th/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">ค้นหาในหน้า</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d จาก %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">ค้นหาผลลัพธ์ถัดไป</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ค้นหาผลลัพธ์ก่อนหน้า</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">ยกเลิกการค้นหาในหน้า</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..5e34f0396f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Hanapin sa pahina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d ng %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Susunod na resulta</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Nakaraang resulta</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Iwaksi ang hanapin sa pahina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..5bb0565cc0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Sayfada bul</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d sonuçtan %1$d sonuç</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Sonraki sonucu bul</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Önceki sonucu bul</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Sayfa bulmayı kapat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..77fb837bdc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-trs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Nānà\'uì\' riña ñanj</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d sa %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Nānà\'uì\' riña sa \'na\'a</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Nānà\'uì\' riña sa gâchin</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Sī nana\'uî\'t riña pâjina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..7e92ed1736
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tt/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Биттән табу</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%2$d нәтиҗәдән %1$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Алдагы нәтиҗәне табу</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Арттагы нәтиҗәне табу</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Биттән эзләүне тәмамлау</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..f978dd7a9f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Af g tesna</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d nger %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Af tanaṭṭuft-dd yuckan</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Ttu arezzu g tasna</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..ccb8fc98ef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ug/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">بەت ئىچىدىن ئىزدەش</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">كېيىنكى نەتىجىنى ئىزدە</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">ئالدىنقى نەتىجىنى ئىزدە</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">بەتتىن ئىزدەشنى ياپ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..c44a6b3707
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-uk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Знайти на сторінці</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d з %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Знайти наступний результат</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Знайти попередній результат</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Відхилити пошук на сторінці</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..ff2a687815
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-ur/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">صفحہ میں ڈھونڈیں</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d میں سے %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">اگلا نتیجہ تلاش کریں</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">پچھلا نتیجہ تلاش کریں</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">صفحہ میں تلاش کوبرخاست کریں</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..b325a8669b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-uz/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Sahifadan topish</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Keyingi natijani topish</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Oldingi natijani topish</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Sahifadan topishni olib tashlash</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..5fd86f6c75
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-vec/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Cata ne ƚa pàgina</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d out de %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Risultato seguente</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Risultato presedente</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Sara su cata ne ƚa pàgina</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..a4a42e5a86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-vi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Tìm trong trang</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d trong số %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Tìm kiếm kết quả tiếp theo</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Tìm kiếm kết quả trước đó</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Bỏ qua tìm kiếm trong trang</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..1437377ae9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-yo/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Wa ní orí ojú-ìwé</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d//%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d nínú un %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Wá àbájáde tó kàn</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Wá àbájáde tó sáájú</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Kọ àwárí inú ojú-ìwé</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..0e1ea6c31f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">在页面中查找</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">第 %1$d 项,共 %2$d 项</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">查找下一个结果</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">查找上一个结果</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">关闭页内查找</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..ec4301ba78
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">在頁面中搜尋</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">第 %1$d 筆,共 %2$d 筆</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">尋找下一筆</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">尋找上一筆</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">關閉頁面搜尋</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values/attrs.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000..874658ab63
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values/attrs.xml
@@ -0,0 +1,15 @@
+<?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>
+ <declare-styleable name="FindInPageBar">
+ <attr name="findInPageQueryTextColor" format="reference|color"/>
+ <attr name="findInPageQueryHintTextColor" format="reference|color"/>
+ <attr name="findInPageQueryTextSize" format="dimension"/>
+ <attr name="findInPageResultCountTextColor" format="reference|color"/>
+ <attr name="findInPageNoMatchesTextColor" format="reference|color"/>
+ <attr name="findInPageResultCountTextSize" format="dimension"/>
+ <attr name="findInPageButtonsTint" format="reference|color"/>
+ </declare-styleable>
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values/dimens.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..de7128169a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values/dimens.xml
@@ -0,0 +1,12 @@
+<?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>
+ <dimen name="mozac_feature_findinpage_query_text_size">16sp</dimen>
+ <dimen name="mozac_feature_findinpage_query_marginStart">16dp</dimen>
+
+ <dimen name="mozac_feature_findinpage_result_count_text_size">12sp</dimen>
+ <dimen name="mozac_feature_findinpage_result_count_margin_end">16dp</dimen>
+ <dimen name="mozac_feature_findinpage_result_count_margin_start">16dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..a3444ffe05
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values/strings.xml
@@ -0,0 +1,27 @@
+<?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 xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Watermark/Hint for the find in page input field. -->
+ <string name="mozac_feature_findindpage_input">Find in page</string>
+
+ <!-- String to show the number of results found in the page and the
+ position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_result">%1$d/%2$d</string>
+
+ <!-- String to be read by the accessibility service presenting the number of results found in the page
+ and the position the user is at. The first argument is the position, the second argument is the total. -->
+ <string name="mozac_feature_findindpage_accessibility_result" tools:ignore="PluralsCandidate">%1$d out of %2$d</string>
+
+ <!-- String to be read by the accessibility service when focusing the next result button. -->
+ <string name="mozac_feature_findindpage_next_result">Find next result</string>
+
+ <!-- String to be read by the accessibility service when focusing the previous result button. -->
+ <string name="mozac_feature_findindpage_previous_result">Find previous result</string>
+
+ <!-- String to be read by the accessibility service when focusing the dismiss button in the "find in page" UI. -->
+ <string name="mozac_feature_findindpage_dismiss">Dismiss find in page</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/main/res/values/style.xml b/mobile/android/android-components/components/feature/findinpage/src/main/res/values/style.xml
new file mode 100644
index 0000000000..13a2fe1f9a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/main/res/values/style.xml
@@ -0,0 +1,12 @@
+<?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>
+ <style name="Mozac.Feature.FindInPage.Buttons" parent="">
+ <item name="android:layout_width">48dp</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
+ <item name="tint">#ff000000</item>
+ </style>
+</resources>
diff --git a/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/FindInPageFeatureTest.kt b/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/FindInPageFeatureTest.kt
new file mode 100644
index 0000000000..ab6c82bb2f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/FindInPageFeatureTest.kt
@@ -0,0 +1,130 @@
+/* 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.findinpage
+
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.feature.findinpage.internal.FindInPageInteractor
+import mozilla.components.feature.findinpage.internal.FindInPagePresenter
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class FindInPageFeatureTest {
+
+ @Test
+ fun `start is forwarded to presenter and interactor`() {
+ val presenter: FindInPagePresenter = mock()
+ val interactor: FindInPageInteractor = mock()
+
+ val feature = FindInPageFeature(mock(), mock(), mock())
+ feature.presenter = presenter
+ feature.interactor = interactor
+
+ feature.start()
+
+ verify(presenter).start()
+ verify(interactor).start()
+ }
+
+ @Test
+ fun `stop is forwarded to presenter and interactor`() {
+ val presenter: FindInPagePresenter = mock()
+ val interactor: FindInPageInteractor = mock()
+
+ val feature = FindInPageFeature(mock(), mock(), mock())
+ feature.presenter = presenter
+ feature.interactor = interactor
+
+ feature.stop()
+
+ verify(presenter).stop()
+ verify(interactor).stop()
+ }
+
+ @Test
+ fun `bind is forwarded to presenter and interactor`() {
+ val presenter: FindInPagePresenter = mock()
+ val interactor: FindInPageInteractor = mock()
+
+ val feature = FindInPageFeature(mock(), mock(), mock())
+ feature.presenter = presenter
+ feature.interactor = interactor
+
+ val session: SessionState = mock()
+ feature.bind(session)
+
+ verify(presenter).bind(session)
+ verify(interactor).bind(session)
+ }
+
+ @Test
+ fun `onBackPressed unbinds if bound to session`() {
+ val presenter: FindInPagePresenter = mock()
+ val interactor: FindInPageInteractor = mock()
+
+ val feature = spy(FindInPageFeature(mock(), mock(), mock()))
+ feature.presenter = presenter
+ feature.interactor = interactor
+
+ val session: SessionState = mock()
+ feature.bind(session)
+
+ assertTrue(feature.onBackPressed())
+
+ verify(feature).unbind()
+ }
+
+ @Test
+ fun `onBackPressed returns false if not bound`() {
+ val presenter: FindInPagePresenter = mock()
+ val interactor: FindInPageInteractor = mock()
+
+ val feature = spy(FindInPageFeature(mock(), mock(), mock()))
+ feature.presenter = presenter
+ feature.interactor = interactor
+
+ assertFalse(feature.onBackPressed())
+
+ verify(feature, never()).unbind()
+ }
+
+ @Test
+ fun `unbind is forwarded to presenter and interactor`() {
+ val presenter: FindInPagePresenter = mock()
+ val interactor: FindInPageInteractor = mock()
+
+ val feature = FindInPageFeature(mock(), mock(), mock())
+ feature.presenter = presenter
+ feature.interactor = interactor
+
+ feature.unbind()
+
+ verify(presenter).unbind()
+ verify(interactor).unbind()
+ }
+
+ @Test
+ fun `unbind invokes close lambda`() {
+ val presenter: FindInPagePresenter = mock()
+ val interactor: FindInPageInteractor = mock()
+
+ var lambdaInvoked = false
+
+ val feature = FindInPageFeature(mock(), mock(), mock()) {
+ lambdaInvoked = true
+ }
+
+ feature.presenter = presenter
+ feature.interactor = interactor
+
+ feature.unbind()
+
+ assertTrue(lambdaInvoked)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPageInteractorTest.kt b/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPageInteractorTest.kt
new file mode 100644
index 0000000000..d51bf4d89b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPageInteractorTest.kt
@@ -0,0 +1,211 @@
+/* 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.findinpage.internal
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.findinpage.FindInPageFeature
+import mozilla.components.feature.findinpage.view.FindInPageView
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class FindInPageInteractorTest {
+
+ @Test
+ fun `Start registers interactor as listener on view`() {
+ val view: FindInPageView = mock()
+ val interactor = FindInPageInteractor(mock(), view, mock())
+
+ verify(view, never()).listener = interactor
+
+ interactor.start()
+
+ verify(view).listener = interactor
+ }
+
+ @Test
+ fun `Stop unregisters interactor as listener on view`() {
+ val view: FindInPageView = mock()
+ val interactor = FindInPageInteractor(mock(), view, mock())
+
+ interactor.start()
+ interactor.stop()
+
+ verify(view).listener = null
+ }
+
+ @Test
+ fun `FindInPageView-Listener implementation can get invoked without binding to session`() {
+ val view: FindInPageView = mock()
+ `when`(view.asView()).thenReturn(View(testContext))
+
+ val interactor = FindInPageInteractor(mock(), view, mock())
+
+ // Nothing should throw here if we haven't bound the interactor to a session
+ interactor.onPreviousResult()
+ interactor.onNextResult()
+ interactor.onClose()
+ interactor.onFindAll("example")
+ interactor.onClearMatches()
+ }
+
+ @Test
+ fun `OnPreviousResult updates engine session`() {
+ val view: FindInPageView = mock()
+ `when`(view.asView()).thenReturn(View(testContext))
+
+ val sessionState: SessionState = mock()
+ val engineState: EngineState = mock()
+ val engineSession: EngineSession = mock()
+ `when`(engineState.engineSession).thenReturn(engineSession)
+ `when`(sessionState.engineState).thenReturn(engineState)
+
+ val interactor = FindInPageInteractor(mock(), view, mock())
+ interactor.bind(sessionState)
+ interactor.onPreviousResult()
+
+ verify(engineSession).findNext(false)
+ }
+
+ @Test
+ fun `onNextResult updates engine session`() {
+ val view: FindInPageView = mock()
+ `when`(view.asView()).thenReturn(View(testContext))
+
+ val sessionState: SessionState = mock()
+ val engineState: EngineState = mock()
+ val engineSession: EngineSession = mock()
+ `when`(engineState.engineSession).thenReturn(engineSession)
+ `when`(sessionState.engineState).thenReturn(engineState)
+
+ val interactor = FindInPageInteractor(mock(), view, mock())
+ interactor.bind(sessionState)
+ interactor.onNextResult()
+
+ verify(engineSession).findNext(true)
+ }
+
+ @Test
+ fun `onNextResult blurs focused engine view`() {
+ val view: FindInPageView = mock()
+ `when`(view.asView()).thenReturn(View(testContext))
+
+ val actualEngineView: View = mock()
+ val engineView: EngineView = mock()
+ `when`(engineView.asView()).thenReturn(actualEngineView)
+
+ val sessionState: SessionState = mock()
+ val engineState: EngineState = mock()
+ val engineSession: EngineSession = mock()
+ `when`(engineState.engineSession).thenReturn(engineSession)
+ `when`(sessionState.engineState).thenReturn(engineState)
+
+ val interactor = FindInPageInteractor(mock(), view, engineView)
+
+ interactor.bind(sessionState)
+ interactor.onNextResult()
+ verify(actualEngineView).clearFocus()
+ }
+
+ @Test
+ fun `onClose notifies feature`() {
+ val feature: FindInPageFeature = mock()
+
+ val interactor = FindInPageInteractor(feature, mock(), mock())
+ interactor.onClose()
+
+ verify(feature).unbind()
+ }
+
+ @Test
+ fun `unbind clears matches`() {
+ val view: FindInPageView = mock()
+ val sessionState: SessionState = mock()
+ val engineState: EngineState = mock()
+ val engineSession: EngineSession = mock()
+ `when`(engineState.engineSession).thenReturn(engineSession)
+ `when`(sessionState.engineState).thenReturn(engineState)
+
+ val interactor = FindInPageInteractor(mock(), view, mock())
+ interactor.bind(sessionState)
+ verify(engineSession, never()).clearFindMatches()
+
+ interactor.unbind()
+ verify(engineSession).clearFindMatches()
+ }
+
+ @Test
+ fun `onFindAll updates engine session`() {
+ val sessionState: SessionState = mock()
+ val engineState: EngineState = mock()
+ val engineSession: EngineSession = mock()
+ `when`(engineState.engineSession).thenReturn(engineSession)
+ `when`(sessionState.engineState).thenReturn(engineState)
+
+ val interactor = FindInPageInteractor(mock(), mock(), mock())
+
+ interactor.bind(sessionState)
+ interactor.onFindAll("example")
+
+ verify(engineSession).findAll("example")
+ }
+
+ @Test
+ fun `onClearMatches updates engine session`() {
+ val sessionState: SessionState = mock()
+ val engineState: EngineState = mock()
+ val engineSession: EngineSession = mock()
+ `when`(engineState.engineSession).thenReturn(engineSession)
+ `when`(sessionState.engineState).thenReturn(engineState)
+
+ val interactor = FindInPageInteractor(mock(), mock(), mock())
+ interactor.bind(sessionState)
+ interactor.onClearMatches()
+
+ verify(engineSession).clearFindMatches()
+ }
+
+ @Test
+ fun `interactor emits the facts`() {
+ CollectionProcessor.withFactCollection { facts ->
+ val view: FindInPageView = mock()
+ `when`(view.asView()).thenReturn(View(testContext))
+
+ val actualEngineView: View = mock()
+ val engineView: EngineView = mock()
+ `when`(engineView.asView()).thenReturn(actualEngineView)
+
+ val interactor = FindInPageInteractor(mock(), view, engineView)
+ interactor.onClose()
+ interactor.onFindAll("Mozilla")
+ interactor.onNextResult()
+ interactor.onPreviousResult()
+
+ val closeFact = facts[0]
+ val commitFact = facts[1]
+ val nextFact = facts[2]
+ val previousFact = facts[3]
+
+ Assert.assertEquals("close", closeFact.item)
+ Assert.assertEquals("input", commitFact.item)
+ Assert.assertEquals(Action.COMMIT, commitFact.action)
+ Assert.assertEquals("next", nextFact.item)
+ Assert.assertEquals("previous", previousFact.item)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPagePresenterTest.kt b/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPagePresenterTest.kt
new file mode 100644
index 0000000000..3f7a8bfe43
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPagePresenterTest.kt
@@ -0,0 +1,117 @@
+/* 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.findinpage.internal
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.content.FindResultState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.findinpage.view.FindInPageView
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class FindInPagePresenterTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ private lateinit var store: BrowserStore
+
+ @Before
+ @ExperimentalCoroutinesApi
+ fun setUp() {
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "test-tab"),
+ ),
+ selectedTabId = "test-tab",
+ ),
+ )
+ }
+
+ @Test
+ fun `view is updated to display latest find result`() {
+ val view: FindInPageView = mock()
+ val presenter = FindInPagePresenter(store, view)
+ presenter.start()
+
+ val result = FindResultState(0, 2, false)
+ store.dispatch(ContentAction.AddFindResultAction("test-tab", result)).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ verify(view, never()).displayResult(result)
+
+ presenter.bind(store.state.selectedTab!!)
+ store.dispatch(ContentAction.AddFindResultAction("test-tab", result)).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ verify(view).displayResult(result)
+
+ val result2 = FindResultState(1, 2, true)
+ store.dispatch(ContentAction.AddFindResultAction("test-tab", result2)).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ verify(view).displayResult(result2)
+ }
+
+ @Test
+ fun `no find results are observed after stop has been called`() {
+ val view: FindInPageView = mock()
+ val presenter = FindInPagePresenter(store, view)
+ presenter.start()
+
+ presenter.bind(store.state.selectedTab!!)
+ store.dispatch(ContentAction.AddFindResultAction("test-tab", mock())).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ verify(view, times(1)).displayResult(any())
+
+ presenter.stop()
+ store.dispatch(ContentAction.AddFindResultAction("test-tab", mock())).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ verify(view, times(1)).displayResult(any())
+ }
+
+ @Test
+ fun `bind updates session and focuses view`() {
+ val view: FindInPageView = mock()
+
+ val presenter = FindInPagePresenter(mock(), view)
+ val session = Mockito.mock(SessionState::class.java, Mockito.RETURNS_DEEP_STUBS)
+ `when`(session.content.private).thenReturn(false)
+ presenter.bind(session)
+
+ assertEquals(presenter.session, session)
+ verify(view).focus()
+ }
+
+ @Test
+ fun `unbind clears session and view`() {
+ val view: FindInPageView = mock()
+
+ val presenter = FindInPagePresenter(mock(), view)
+ val session = Mockito.mock(SessionState::class.java, Mockito.RETURNS_DEEP_STUBS)
+ `when`(session.content.private).thenReturn(false)
+ presenter.bind(session)
+ presenter.unbind()
+
+ assertNull(presenter.session)
+ verify(view).clear()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/view/FindInPageBarTest.kt b/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/view/FindInPageBarTest.kt
new file mode 100644
index 0000000000..a16c9d897e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/view/FindInPageBarTest.kt
@@ -0,0 +1,170 @@
+/* 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.findinpage.view
+
+import android.widget.EditText
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.core.view.inputmethod.EditorInfoCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.content.FindResultState
+import mozilla.components.feature.findinpage.R
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.shadows.ShadowLooper
+
+@RunWith(AndroidJUnit4::class)
+class FindInPageBarTest {
+
+ @Test
+ fun `Clicking close button invokes onClose method of listener`() {
+ val listener: FindInPageView.Listener = mock()
+
+ val view = FindInPageBar(testContext)
+ view.listener = listener
+
+ view.findViewById<AppCompatImageButton>(R.id.find_in_page_close_btn)
+ .performClick()
+
+ verify(listener).onClose()
+ }
+
+ @Test
+ fun `Clicking next button invokes onNextResult method of listener`() {
+ val listener: FindInPageView.Listener = mock()
+
+ val view = FindInPageBar(testContext)
+ view.listener = listener
+
+ view.findViewById<EditText>(R.id.find_in_page_query_text).setText("Non empty query")
+ view.findViewById<AppCompatImageButton>(R.id.find_in_page_next_btn)
+ .performClick()
+
+ verify(listener).onNextResult()
+ }
+
+ @Test
+ fun `Clicking previous button invokes onPreviousResult method of listener`() {
+ val listener: FindInPageView.Listener = mock()
+
+ val view = FindInPageBar(testContext)
+ view.listener = listener
+
+ view.findViewById<EditText>(R.id.find_in_page_query_text).setText("Non empty query")
+ view.findViewById<AppCompatImageButton>(R.id.find_in_page_prev_btn)
+ .performClick()
+
+ verify(listener).onPreviousResult()
+ }
+
+ @Test
+ fun `Entering text invokes onFindAll method of listener`() {
+ val listener: FindInPageView.Listener = mock()
+
+ val view = FindInPageBar(testContext)
+ view.listener = listener
+
+ view.findViewById<EditText>(R.id.find_in_page_query_text)
+ .setText("Hello World")
+
+ verify(listener).onFindAll("Hello World")
+ }
+
+ @Test
+ fun `Clearing text invokes onClearMatches method of listener`() {
+ val listener: FindInPageView.Listener = mock()
+
+ val view = FindInPageBar(testContext)
+ view.listener = listener
+
+ view.findViewById<EditText>(R.id.find_in_page_query_text)
+ .setText("")
+
+ verify(listener).onClearMatches()
+ }
+
+ @Test
+ fun `displayResult with matches will update views`() {
+ val view = spy(FindInPageBar(testContext))
+
+ view.displayResult(FindResultState(0, 100, false))
+
+ val textCorrectValue = view.resultFormat.format(1, 100)
+ val contentDesCorrectValue = view.accessibilityFormat.format(1, 100)
+
+ assertEquals(textCorrectValue, view.resultsCountTextView.text)
+ assertEquals(contentDesCorrectValue, view.resultsCountTextView.contentDescription)
+ verify(view).announceForAccessibility(contentDesCorrectValue)
+ }
+
+ @Test
+ fun `displayResult with no matches will update views`() {
+ val view = spy(FindInPageBar(testContext))
+
+ view.displayResult(FindResultState(0, 0, false))
+
+ val textCorrectValue = view.resultFormat.format(0, 0)
+ val contentDesCorrectValue = view.accessibilityFormat.format(0, 0)
+
+ assertEquals(textCorrectValue, view.resultsCountTextView.text)
+ assertEquals(contentDesCorrectValue, view.resultsCountTextView.contentDescription)
+ verify(view).announceForAccessibility(contentDesCorrectValue)
+ }
+
+ @Test
+ fun `private flag sets IME_FLAG_NO_PERSONALIZED_LEARNING on find in page bar`() {
+ val findInPageBar = spy(FindInPageBar(testContext))
+ val edit = findInPageBar.queryEditText
+
+ // By default "private mode" is off.
+ assertEquals(
+ 0,
+ edit.imeOptions and
+ EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING,
+ )
+ assertEquals(false, findInPageBar.private)
+
+ // Turning on private mode sets flag
+ findInPageBar.private = true
+ assertNotEquals(
+ 0,
+ edit.imeOptions and
+ EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING,
+ )
+ assertTrue(findInPageBar.private)
+
+ // Turning private mode off again - should remove flag
+ findInPageBar.private = false
+ assertEquals(
+ 0,
+ edit.imeOptions and
+ EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING,
+ )
+ assertEquals(false, findInPageBar.private)
+ }
+
+ @Test
+ fun `clearing the focus of the find in page bar hides the keyboard`() {
+ val findInPageBar = spy(FindInPageBar(testContext))
+
+ // re-initialize the listener to use the spy
+ findInPageBar.bindQueryEditText()
+
+ // Focus the find in page bar first
+ findInPageBar.focus()
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
+
+ // clearing the focus should hide the keyboard
+ findInPageBar.clear()
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
+ verify(findInPageBar).hideKeyboard()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/findinpage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/findinpage/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/findinpage/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/findinpage/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/findinpage/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/findinpage/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/fxsuggest/README.md b/mobile/android/android-components/components/feature/fxsuggest/README.md
new file mode 100644
index 0000000000..1c1099dd6f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/README.md
@@ -0,0 +1,49 @@
+# [Android Components](../../../README.md) > Feature > Firefox Suggest
+
+A component for accessing Firefox Suggest search suggestions.
+
+[Firefox Suggest](https://support.mozilla.org/en-US/kb/firefox-suggest-faq) provides suggestions for sponsored and web content in the address bar. Suggestions are downloaded, stored, and matched on-device.
+
+## Usage
+
+### 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-fxsuggest:{latest-version}"
+```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Extras | Description |
+|---------------|----------------------------------|-------------------------------|---------------------------------------------------------------------------------------------------------------|
+| `INTERACTION` | `amp_suggestion_clicked` | `suggestion_clicked_extras` | The user clicked on a Firefox Suggestion from adMarketplace. |
+| `DISPLAY` | `amp_suggestion_impressed` | `suggestion_impressed_extras` | A Firefox Suggestion from adMarketplace was visible when the user finished interacting with the awesomebar. |
+| `INTERACTION` | `wikipedia_suggestion_clicked` | `suggestion_clicked_extras` | The user clicked on a Firefox Suggestion for a Wikipedia page. |
+| `DISPLAY` | `wikipedia_suggestion_impressed` | `suggestion_impressed_extras` | A Firefox Suggestion for a Wikipedia page was visible when the user finished interacting with the awesomebar. |
+
+#### `suggestion_clicked_extras`
+
+| Key | Type | Value |
+|--------------------|----------------------------|------------------------------------------------------------|
+| `interaction_info` | `FxSuggestInteractionInfo` | Type-specific information to record for this suggestion. |
+| `position` | `Long` | The 1-based position of this suggestion in the awesomebar. |
+
+
+#### `suggestion_impressed_extras`
+
+| Key | Type | Value |
+|------------------------|----------------------------|----------------------------------------------------------------------------------------------------------------|
+| `interaction_info` | `FxSuggestInteractionInfo` | Type-specific information to record for this suggestion. |
+| `position` | `Long` | The 1-based position of this suggestion in the awesomebar. |
+| `is_clicked` | `Boolean` | Whether the user clicked on this suggestion after it was shown. |
+| `engagement_abandoned` | `Boolean` | Whether the user dismissed the awesomebar without navigating to a destination after this suggestion was shown. |
+
+## 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/fxsuggest/build.gradle b/mobile/android/android-components/components/feature/fxsuggest/build.gradle
new file mode 100644
index 0000000000..2acd8a99c7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/build.gradle
@@ -0,0 +1,87 @@
+/* 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
+
+buildscript {
+ repositories {
+ maven {
+ url "https://maven.mozilla.org/maven2"
+ }
+ }
+
+ dependencies {
+ classpath "${ApplicationServicesConfig.groupId}:tooling-nimbus-gradle:${ApplicationServicesConfig.version}"
+ }
+}
+
+plugins {
+ id "com.jetbrains.python.envs" version "$python_envs_plugin"
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.feature.fxsuggest'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += '-opt-in=kotlin.ExperimentalUnsignedTypes'
+}
+
+dependencies {
+ api ComponentsDependencies.mozilla_appservices_suggest
+
+ implementation project(':browser-state')
+ implementation project(':concept-awesomebar')
+ implementation project(':concept-engine')
+ implementation project(':feature-session')
+ implementation project(':service-nimbus')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+
+ implementation ComponentsDependencies.androidx_work_runtime
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.androidx_work_testing
+ testImplementation ComponentsDependencies.mozilla_appservices_full_megazord_forUnitTests
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
+nimbus {
+ // The path to the Nimbus feature manifest file
+ manifestFile = "fxsuggest.fml.yaml"
+
+ channels = [
+ debug: "debug",
+ release: "release",
+ ]
+
+ applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir')
+ ? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null
+}
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/feature/fxsuggest/fxsuggest.fml.yaml b/mobile/android/android-components/components/feature/fxsuggest/fxsuggest.fml.yaml
new file mode 100644
index 0000000000..e55a8e78eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/fxsuggest.fml.yaml
@@ -0,0 +1,36 @@
+# 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/.
+
+about:
+ description: Nimbus Feature Manifest for the Firefox Suggest feature.
+ android:
+ package: mozilla.components.feature.fxsuggest
+ class: .FxSuggestNimbus
+channels:
+ - debug
+ - release
+features:
+ awesomebar-suggestion-provider:
+ description: Configuration for the Firefox Suggest awesomebar suggestion provider.
+ variables:
+ available-suggestion-types:
+ description: >
+ A map of suggestion types to booleans that indicate whether or not the provider should
+ return suggestions of those types.
+ type: Map<SuggestionType, Boolean>
+ default: {
+ "amp": false,
+ "ampMobile": false,
+ "wikipedia": true,
+ }
+enums:
+ SuggestionType:
+ description: The type of a Firefox Suggest search suggestion.
+ variants:
+ amp:
+ description: A Firefox Suggestion from adMarketplace.
+ ampMobile:
+ description: A firefox Suggestion from adMarketplace specifically for mobile.
+ wikipedia:
+ description: A Firefox Suggestion for a Wikipedia page.
diff --git a/mobile/android/android-components/components/feature/fxsuggest/proguard-rules.pro b/mobile/android/android-components/components/feature/fxsuggest/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/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/fxsuggest/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/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/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionScheduler.kt b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionScheduler.kt
new file mode 100644
index 0000000000..c32ed771b5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionScheduler.kt
@@ -0,0 +1,68 @@
+/* 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.fxsuggest
+
+import android.content.Context
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.worker.Frequency
+import java.util.concurrent.TimeUnit
+
+/**
+ * Schedules a periodic background task to incrementally download and persist new Firefox Suggest
+ * search suggestions.
+ *
+ * @property context The Android application context.
+ * @property frequency The optional interval period for the background task. Defaults to 1 day.
+ */
+class FxSuggestIngestionScheduler(
+ private val context: Context,
+ private val frequency: Frequency = Frequency(repeatInterval = 1, repeatIntervalTimeUnit = TimeUnit.DAYS),
+) {
+ private val logger = Logger("FxSuggestIngestionScheduler")
+
+ /**
+ * Schedules a periodic background task to ingest new suggestions. Does nothing if the task is
+ * already scheduled.
+ */
+ fun startPeriodicIngestion() {
+ logger.info("Scheduling periodic ingestion for new suggestions")
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(
+ FxSuggestIngestionWorker.WORK_TAG,
+ ExistingPeriodicWorkPolicy.KEEP,
+ createPeriodicIngestionWorkerRequest(),
+ )
+ }
+
+ /**
+ * Cancels a scheduled background task to ingest new suggestions.
+ */
+ fun stopPeriodicIngestion() {
+ logger.info("Canceling periodic ingestion for new suggestions")
+ WorkManager.getInstance(context).cancelAllWorkByTag(FxSuggestIngestionWorker.WORK_TAG)
+ }
+
+ internal fun createPeriodicIngestionWorkerRequest(): PeriodicWorkRequest {
+ val constraints = getWorkerConstrains()
+ return PeriodicWorkRequestBuilder<FxSuggestIngestionWorker>(
+ this.frequency.repeatInterval,
+ this.frequency.repeatIntervalTimeUnit,
+ ).apply {
+ setConstraints(constraints)
+ addTag(FxSuggestIngestionWorker.WORK_TAG)
+ }.build()
+ }
+
+ internal fun getWorkerConstrains() = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.UNMETERED)
+ .setRequiresBatteryNotLow(true)
+ .setRequiresStorageNotLow(true)
+ .build()
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionWorker.kt b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionWorker.kt
new file mode 100644
index 0000000000..b30f8b0f4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionWorker.kt
@@ -0,0 +1,40 @@
+/* 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.fxsuggest
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A [CoroutineWorker] that downloads and persists new Firefox Suggest search suggestions.
+ *
+ * @param context The Android application context.
+ * @param params Parameters for this worker's internal state.
+ */
+internal class FxSuggestIngestionWorker(
+ context: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+ private val logger = Logger("FxSuggestIngestionWorker")
+
+ override suspend fun doWork(): Result {
+ logger.info("Ingesting new suggestions")
+ val storage = GlobalFxSuggestDependencyProvider.requireStorage()
+ val success = storage.ingest()
+ return if (success) {
+ logger.info("Successfully ingested new suggestions")
+ Result.success()
+ } else {
+ logger.error("Failed to ingest new suggestions")
+ Result.retry()
+ }
+ }
+
+ internal companion object {
+ const val WORK_TAG = "mozilla.components.feature.fxsuggest.ingest.work.tag"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestStorage.kt b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestStorage.kt
new file mode 100644
index 0000000000..902ac035eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestStorage.kt
@@ -0,0 +1,119 @@
+/* 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.fxsuggest
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.withContext
+import mozilla.appservices.suggest.SuggestApiException
+import mozilla.appservices.suggest.SuggestIngestionConstraints
+import mozilla.appservices.suggest.SuggestStore
+import mozilla.appservices.suggest.SuggestStoreBuilder
+import mozilla.appservices.suggest.Suggestion
+import mozilla.appservices.suggest.SuggestionQuery
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.support.base.log.logger.Logger
+import java.io.File
+
+/**
+ * A coroutine-aware wrapper around the synchronous [SuggestStore] interface.
+ *
+ * @param context The Android application context.
+ * @param crashReporter An optional [CrashReporting] instance for reporting unexpected caught
+ * exceptions.
+ */
+class FxSuggestStorage(context: Context) {
+ // Lazily initializes the store on first use. `cacheDir` and using the `File` constructor
+ // does I/O, so `store.value` should only be accessed from the read or write scope.
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val store: Lazy<SuggestStore> = lazy {
+ SuggestStoreBuilder()
+ .cachePath(File(context.cacheDir, CACHE_DATABASE_NAME).absolutePath)
+ .dataPath(context.getDatabasePath(DATABASE_NAME).absolutePath)
+ .build()
+ }
+
+ // We expect almost all Suggest storage operations to be reads, with infrequent writes. The
+ // I/O dispatcher supports both workloads, and using separate scopes lets us cancel reads
+ // without affecting writes.
+ private val readScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
+ private val writeScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
+
+ private val logger = Logger("FxSuggestStorage")
+
+ /**
+ * Queries the store for suggestions.
+ *
+ * @param query The input and suggestion types to match.
+ * @return A list of matching suggestions.
+ */
+ suspend fun query(query: SuggestionQuery): List<Suggestion> =
+ withContext(readScope.coroutineContext) {
+ handleSuggestExceptions("query", emptyList()) {
+ store.value.query(query)
+ }
+ }
+
+ /**
+ * Downloads and persists new Firefox Suggest search suggestions.
+ *
+ * @param constraints Optional limits on suggestions to ingest.
+ * @return `true` if ingestion succeeded; `false` if ingestion failed and should be retried.
+ */
+ suspend fun ingest(constraints: SuggestIngestionConstraints = SuggestIngestionConstraints()): Boolean =
+ withContext(writeScope.coroutineContext) {
+ handleSuggestExceptions("ingest", false) {
+ store.value.ingest(constraints)
+ true
+ }
+ }
+
+ /**
+ * Interrupts any ongoing queries for suggestions.
+ */
+ fun cancelReads() {
+ if (store.isInitialized()) {
+ store.value.interrupt()
+ readScope.coroutineContext.cancelChildren()
+ }
+ }
+
+ /**
+ * Runs an [operation] with the given [name], ignoring and logging any non-fatal exceptions.
+ * Returns either the result of the [operation], or the provided [default] value if the
+ * [operation] throws an exception.
+ *
+ * @param name The name of the operation to run.
+ * @param default The default value to return if the operation fails.
+ * @param operation The operation to run.
+ */
+ private inline fun <T> handleSuggestExceptions(
+ name: String,
+ default: T,
+ operation: () -> T,
+ ): T {
+ return try {
+ operation()
+ } catch (e: SuggestApiException) {
+ logger.warn("Ignoring exception from `$name`", e)
+ default
+ }
+ }
+
+ internal companion object {
+ /**
+ * The database file name for cached data.
+ */
+ const val CACHE_DATABASE_NAME = "suggest.sqlite"
+
+ /**
+ * The database file name for permanent data.
+ */
+ const val DATABASE_NAME = "suggest_data.sqlite"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestSuggestionProvider.kt b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestSuggestionProvider.kt
new file mode 100644
index 0000000000..3e51e46f3c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/FxSuggestSuggestionProvider.kt
@@ -0,0 +1,185 @@
+/* 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.fxsuggest
+
+import android.content.res.Resources
+import mozilla.appservices.suggest.Suggestion
+import mozilla.appservices.suggest.SuggestionProvider
+import mozilla.appservices.suggest.SuggestionQuery
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.ktx.kotlin.toBitmap
+import java.util.UUID
+
+private const val MAX_NUM_OF_FIREFOX_SUGGESTIONS = 1
+
+/**
+ * An [AwesomeBar.SuggestionProvider] that returns Firefox Suggest search suggestions.
+ *
+ * @param resources Your application's [Resources] instance.
+ * @param loadUrlUseCase A use case that loads a suggestion's URL when clicked.
+ * @param includeSponsoredSuggestions Whether to return suggestions for sponsored content.
+ * @param includeNonSponsoredSuggestions Whether to return suggestions for web content.
+ * @param suggestionsHeader An optional header title for grouping the returned suggestions.
+ * @param contextId The contextual services user identifier, used for telemetry.
+ */
+class FxSuggestSuggestionProvider(
+ private val resources: Resources,
+ private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ private val includeSponsoredSuggestions: Boolean,
+ private val includeNonSponsoredSuggestions: Boolean,
+ private val suggestionsHeader: String? = null,
+ private val contextId: String? = null,
+) : AwesomeBar.SuggestionProvider {
+ /**
+ * [AwesomeBar.Suggestion.metadata] keys for this provider's suggestions.
+ */
+ object MetadataKeys {
+ const val CLICK_INFO = "click_info"
+ const val IMPRESSION_INFO = "impression_info"
+ }
+
+ override val id: String = UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? = suggestionsHeader
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> =
+ if (text.isEmpty()) {
+ emptyList()
+ } else {
+ val providers = buildList() {
+ val availableSuggestionTypes = FxSuggestNimbus.features
+ .awesomebarSuggestionProvider
+ .value()
+ .availableSuggestionTypes
+ if (includeSponsoredSuggestions && availableSuggestionTypes[SuggestionType.AMP] == true) {
+ add(SuggestionProvider.AMP)
+ }
+ if (includeSponsoredSuggestions && availableSuggestionTypes[SuggestionType.AMP_MOBILE] == true) {
+ add(SuggestionProvider.AMP_MOBILE)
+ }
+ if (includeNonSponsoredSuggestions && availableSuggestionTypes[SuggestionType.WIKIPEDIA] == true) {
+ add(SuggestionProvider.WIKIPEDIA)
+ }
+ }
+ GlobalFxSuggestDependencyProvider.requireStorage().query(
+ SuggestionQuery(
+ keyword = text,
+ providers = providers,
+ limit = MAX_NUM_OF_FIREFOX_SUGGESTIONS,
+ ),
+ ).into()
+ }
+
+ override fun onInputCancelled() {
+ GlobalFxSuggestDependencyProvider.requireStorage().cancelReads()
+ }
+
+ private suspend fun List<Suggestion>.into(): List<AwesomeBar.Suggestion> =
+ mapNotNull { suggestion ->
+ val details = when (suggestion) {
+ is Suggestion.Amp -> SuggestionDetails(
+ title = suggestion.title,
+ url = suggestion.url,
+ fullKeyword = suggestion.fullKeyword,
+ isSponsored = true,
+ icon = suggestion.icon,
+ clickInfo = contextId?.let {
+ FxSuggestInteractionInfo.Amp(
+ blockId = suggestion.blockId,
+ advertiser = suggestion.advertiser.lowercase(),
+ reportingUrl = suggestion.clickUrl,
+ iabCategory = suggestion.iabCategory,
+ contextId = it,
+ )
+ },
+ impressionInfo = contextId?.let {
+ FxSuggestInteractionInfo.Amp(
+ blockId = suggestion.blockId,
+ advertiser = suggestion.advertiser.lowercase(),
+ reportingUrl = suggestion.impressionUrl,
+ iabCategory = suggestion.iabCategory,
+ contextId = it,
+ )
+ },
+ )
+ is Suggestion.Wikipedia -> {
+ val interactionInfo = contextId?.let {
+ FxSuggestInteractionInfo.Wikipedia(contextId = it)
+ }
+ SuggestionDetails(
+ title = suggestion.title,
+ url = suggestion.url,
+ fullKeyword = suggestion.fullKeyword,
+ isSponsored = false,
+ icon = suggestion.icon,
+ clickInfo = interactionInfo,
+ impressionInfo = interactionInfo,
+ )
+ }
+ else -> return@mapNotNull null
+ }
+ AwesomeBar.Suggestion(
+ provider = this@FxSuggestSuggestionProvider,
+ icon = details.icon?.toUByteArray()?.asByteArray()?.toBitmap(),
+ title = details.title,
+ description = if (details.isSponsored) {
+ resources.getString(R.string.sponsored_suggestion_description)
+ } else {
+ null
+ },
+ onSuggestionClicked = {
+ loadUrlUseCase.invoke(details.url)
+ },
+ score = Int.MIN_VALUE,
+ metadata = buildMap {
+ details.clickInfo?.let { put(MetadataKeys.CLICK_INFO, it) }
+ details.impressionInfo?.let { put(MetadataKeys.IMPRESSION_INFO, it) }
+ },
+ )
+ }
+}
+
+internal data class SuggestionDetails(
+ val title: String,
+ val url: String,
+ val fullKeyword: String,
+ val isSponsored: Boolean,
+ val icon: List<UByte>?,
+ val clickInfo: FxSuggestInteractionInfo? = null,
+ val impressionInfo: FxSuggestInteractionInfo? = null,
+)
+
+/**
+ * Additional information about a Firefox Suggest [AwesomeBar.Suggestion] to record in telemetry when the user
+ * interacts with the suggestion.
+ */
+sealed interface FxSuggestInteractionInfo {
+ /**
+ * Interaction information for a sponsored Firefox Suggest search suggestion from AMP.
+ *
+ * @param blockId A unique identifier for the suggestion.
+ * @param advertiser The name of the advertiser providing the sponsored suggestion.
+ * @param reportingUrl The url to report the click or impression to.
+ * @param iabCategory The categorization of the suggestion.
+ * @param contextId The contextual services user identifier.
+ */
+ data class Amp(
+ val blockId: Long,
+ val advertiser: String,
+ val reportingUrl: String,
+ val iabCategory: String,
+ val contextId: String,
+ ) : FxSuggestInteractionInfo
+
+ /**
+ * Interaction information for a Firefox Suggest search suggestion from Wikipedia.
+ *
+ * @param contextId The contextual services user identifier.
+ */
+ data class Wikipedia(
+ val contextId: String,
+ ) : FxSuggestInteractionInfo
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/GlobalFxSuggestDependencyProvider.kt b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/GlobalFxSuggestDependencyProvider.kt
new file mode 100644
index 0000000000..faddb798f8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/GlobalFxSuggestDependencyProvider.kt
@@ -0,0 +1,30 @@
+/* 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.fxsuggest
+
+/**
+ * Provides global access to the dependencies needed to access Firefox Suggest search suggestions.
+ */
+object GlobalFxSuggestDependencyProvider {
+ internal var storage: FxSuggestStorage? = null
+
+ /**
+ * Initializes this provider with a wrapped Suggest store.
+ *
+ * Your application's [onCreate][android.app.Application.onCreate] method should call this
+ * method once.
+ *
+ * @param storage The wrapped Suggest store.
+ */
+ fun initialize(storage: FxSuggestStorage) {
+ this.storage = storage
+ }
+
+ internal fun requireStorage(): FxSuggestStorage {
+ return requireNotNull(storage) {
+ "`GlobalFxSuggestDependencyProvider.initialize` must be called before accessing `storage`"
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/facts/FxSuggestFacts.kt b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/facts/FxSuggestFacts.kt
new file mode 100644
index 0000000000..21493ff3f1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/facts/FxSuggestFacts.kt
@@ -0,0 +1,89 @@
+/* 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.fxsuggest.facts
+
+import mozilla.components.feature.fxsuggest.FxSuggestInteractionInfo
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the Firefox Suggest feature.
+ */
+class FxSuggestFacts {
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val AMP_SUGGESTION_CLICKED = "amp_suggestion_clicked"
+ const val AMP_SUGGESTION_IMPRESSED = "amp_suggestion_impressed"
+ const val WIKIPEDIA_SUGGESTION_CLICKED = "wikipedia_suggestion_clicked"
+ const val WIKIPEDIA_SUGGESTION_IMPRESSED = "wikipedia_suggestion_impressed"
+ }
+
+ /**
+ * Keys used in the metadata map.
+ */
+ object MetadataKeys {
+ const val INTERACTION_INFO = "interaction_info"
+ const val POSITION = "position"
+ const val IS_CLICKED = "is_clicked"
+ const val ENGAGEMENT_ABANDONED = "engagement_abandoned"
+ }
+}
+
+private fun emitFxSuggestFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_FXSUGGEST,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitSuggestionClickedFact(
+ interactionInfo: FxSuggestInteractionInfo,
+ positionInAwesomeBar: Long,
+) {
+ emitFxSuggestFact(
+ Action.INTERACTION,
+ when (interactionInfo) {
+ is FxSuggestInteractionInfo.Amp -> FxSuggestFacts.Items.AMP_SUGGESTION_CLICKED
+ is FxSuggestInteractionInfo.Wikipedia -> FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_CLICKED
+ },
+ metadata = mapOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO to interactionInfo,
+ FxSuggestFacts.MetadataKeys.POSITION to positionInAwesomeBar,
+ ),
+ )
+}
+
+internal fun emitSuggestionImpressedFact(
+ interactionInfo: FxSuggestInteractionInfo,
+ positionInAwesomeBar: Long,
+ isClicked: Boolean,
+ engagementAbandoned: Boolean,
+) {
+ emitFxSuggestFact(
+ Action.DISPLAY,
+ when (interactionInfo) {
+ is FxSuggestInteractionInfo.Amp -> FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED
+ is FxSuggestInteractionInfo.Wikipedia -> FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_IMPRESSED
+ },
+ metadata = mapOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO to interactionInfo,
+ FxSuggestFacts.MetadataKeys.POSITION to positionInAwesomeBar,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED to isClicked,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED to engagementAbandoned,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/facts/FxSuggestFactsMiddleware.kt b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/facts/FxSuggestFactsMiddleware.kt
new file mode 100644
index 0000000000..622a5459b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/java/mozilla/components/feature/fxsuggest/facts/FxSuggestFactsMiddleware.kt
@@ -0,0 +1,84 @@
+/* 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.fxsuggest.facts
+
+import mozilla.components.browser.state.action.AwesomeBarAction
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.state.AwesomeBarState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.feature.fxsuggest.FxSuggestInteractionInfo
+import mozilla.components.feature.fxsuggest.FxSuggestSuggestionProvider
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.base.facts.Fact
+
+/**
+ * Reports [Fact]s for interactions with Firefox Suggest [AwesomeBar.Suggestion]s.
+ *
+ * We report two kinds of interactions: impressions and clicks. We report impressions for any Firefox Suggest
+ * search suggestions that are visible when the user finishes interacting with the [AwesomeBar].
+ * If the user taps on one of those visible Firefox Suggest suggestions, we'll also report a click for that suggestion.
+ *
+ * Each impression's [Fact.metadata] contains a [FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED] key, whose value is
+ * `false` if the user navigated to a destination (like a URL, a search results page, or a suggestion), or
+ * `true` if the user dismissed the [AwesomeBar] without navigating to a destination.
+ *
+ * We _don't_ report impressions for any suggestions that the user sees as they're still typing.
+ */
+class FxSuggestFactsMiddleware : Middleware<BrowserState, BrowserAction> {
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ handleAction(context, action)
+ next(action)
+ }
+
+ private fun handleAction(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ action: BrowserAction,
+ ) = when (action) {
+ is AwesomeBarAction.EngagementFinished -> emitSuggestionFacts(
+ awesomeBarState = context.state.awesomeBarState,
+ engagementAbandoned = action.abandoned,
+ )
+ else -> Unit
+ }
+
+ private fun emitSuggestionFacts(awesomeBarState: AwesomeBarState, engagementAbandoned: Boolean) {
+ val visibilityState = awesomeBarState.visibilityState
+ val clickedSuggestion = awesomeBarState.clickedSuggestion
+ visibilityState.visibleProviderGroups.entries.forEachIndexed { groupIndex, (_, suggestions) ->
+ suggestions.forEachIndexed { suggestionIndex, suggestion ->
+ val positionInGroup = suggestionIndex.toLong() + 1
+ val positionInAwesomeBar = groupIndex.toLong() + positionInGroup
+ val isClicked = clickedSuggestion == suggestion
+
+ val impressionInfo = suggestion.metadata?.get(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO,
+ ) as? FxSuggestInteractionInfo
+ impressionInfo?.let {
+ emitSuggestionImpressedFact(
+ interactionInfo = it,
+ positionInAwesomeBar = positionInAwesomeBar,
+ isClicked = isClicked,
+ engagementAbandoned = engagementAbandoned,
+ )
+ }
+
+ if (isClicked) {
+ val clickInfo = suggestion.metadata?.get(
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO,
+ ) as? FxSuggestInteractionInfo
+ clickInfo?.let {
+ emitSuggestionClickedFact(it, positionInAwesomeBar)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..8043abe0ab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">ስፖንሰር የተደረገ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..303e6433d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">اسپانسرلی</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..f0718da649
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Спансавана</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..17e62c6132
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Спонсорирано</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..20e84c0035
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Paeroniet</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..c887bcccfb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponzorisano</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..c704133c46
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..3ce9b4753f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">To\'on</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..a6b504fa5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Spunsurizatu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..88b9606ddd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponzorováno</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..7a2515dec8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Noddwyd</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..e76f79f71e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsoreret</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..4fbf31858d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Gesponsert</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..64b039149a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponserowany</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..aeaedde977
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Χορηγία</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..051e478a4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsored</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..051e478a4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsored</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..a64643096e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patronita</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..fee5548ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinado</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..fee5548ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinado</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..fee5548ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinado</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..fee5548ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinado</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..fee5548ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinado</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..7c4597fbad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsitud</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..32c5e58b2b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Babesleak hornituta</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..1cdf22b8c3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsoroitu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..dbbfc41eef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsorisé</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..af5f8d967c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsorizât</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..5f1de443e7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsore</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..fee5548ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinado</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..e91d55ed37
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Pytyvõpyréva</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..a23ead351e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponzorirano</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..64b039149a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponserowany</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..84394b33bd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Szponzorált</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..72f889cff6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Հովանավորված</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..c22878de50
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsorisate</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..963220372f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Disponsori</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..87c9467877
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Kostað</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..584b2fec52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsorizzato</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..7401f742ed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">ממומן</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..745ec27f5d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">広告</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..d414242cd2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">დაფინანსებული</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..717e16f556
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">S lmendad</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..84f3004bbb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Демеуленген</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..514caeb64d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsorkirî</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..7f5e020d8a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">스폰서</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..89f8e79b05
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">ໄດ້ຮັບການສະຫນັບສະຫນູນ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..48f8371abc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponset</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..9731cdb595
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Gesponsord</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..dd05ba69fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..b84ef29b0c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Esponsorizat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..84743460d5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">ਸਪੌਂਸਰ ਕੀਤੇ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..bf6e67a534
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">سفارش کیتی</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..b1b39ff2ee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsorowane</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..fee5548ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinado</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..fee5548ca1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrocinado</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..d1e7419cf6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsurisà</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..015c2263ee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Спонсировано</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..645c2876fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">ᱠᱟᱹᱢᱤᱼᱤᱭᱟᱹ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..da82caab16
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Patrotzinadu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..a4d3f1ffe4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">අනුග්‍රහය ලද</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..2d35d68fe6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponzorované</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..e7fcf63035
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">سپانسر تھئے</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..a23ead351e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponzorirano</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..043ff9e5d5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">E sponsorizuar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..8043a90ba3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Спонзорисано</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..4da34a1fac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Disponsoran</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..cd309dd7a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsrad</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..51fc73aafc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Сарпарастӣ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..8c928ba7bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">ได้รับการสนับสนุน</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..b8000e6734
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsorlu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..1329b916cc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">قوللىغان</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..71da9dc8e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Спонсоровано</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..e55cc5105f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Được tài trợ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..17dbc15473
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">赞助推广</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..535187d30a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">贊助項目</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..96cb8941a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?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 description for a sponsored suggestion from Firefox Suggest. -->
+ <string name="sponsored_suggestion_description">Sponsored</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestFactsMiddlewareTest.kt b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestFactsMiddlewareTest.kt
new file mode 100644
index 0000000000..11a6ae6373
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestFactsMiddlewareTest.kt
@@ -0,0 +1,914 @@
+/* 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.fxsuggest
+
+import mozilla.components.browser.state.action.AwesomeBarAction
+import mozilla.components.browser.state.state.AwesomeBarState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.feature.fxsuggest.facts.FxSuggestFacts
+import mozilla.components.feature.fxsuggest.facts.FxSuggestFactsMiddleware
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+class FxSuggestFactsMiddlewareTest {
+ private lateinit var processor: CollectionProcessor
+
+ @Before
+ fun setUp() {
+ processor = CollectionProcessor()
+ Facts.registerProcessor(processor)
+ }
+
+ @After
+ fun tearDown() {
+ Facts.clearProcessors()
+ }
+
+ @Test
+ fun `GIVEN no suggestions are visible WHEN the engagement is completed THEN no facts are collected`() {
+ val store = BrowserStore(
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertTrue(processor.facts.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN 2 non-AMP suggestions are visible WHEN the engagement is completed THEN no facts are collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(provider),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ clickedSuggestion = providerGroupSuggestions[1],
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertTrue(processor.facts.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN 1 AMP suggestion is visible WHEN the engagement is abandoned THEN 1 impression fact is collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/impression",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/click",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = true)).joinBlocking()
+
+ assertEquals(1, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(123, impressionInfo.blockId)
+ assertEquals("mozilla", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression", impressionInfo.reportingUrl)
+ assertEquals("22 - Shopping", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertTrue(engagementAbandoned)
+ }
+ }
+
+ @Test
+ fun `GIVEN 1 AMP suggestion is visible WHEN the engagement is completed THEN 1 impression fact is collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/impression",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/click",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertEquals(1, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(123, impressionInfo.blockId)
+ assertEquals("mozilla", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression", impressionInfo.reportingUrl)
+ assertEquals("22 - Shopping", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ }
+
+ @Test
+ fun `GIVEN 1 AMP suggestion is visible and a non-AMP suggestion is clicked WHEN the engagement is completed THEN 1 impression fact is collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/impression",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/click",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ clickedSuggestion = providerGroupSuggestions[0],
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertEquals(1, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(123, impressionInfo.blockId)
+ assertEquals("mozilla", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression", impressionInfo.reportingUrl)
+ assertEquals("22 - Shopping", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ }
+
+ @Test
+ fun `GIVEN 1 AMP suggestion is visible and clicked WHEN the engagement is completed THEN 1 impression fact and 1 click fact are collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/impression",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/click",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ clickedSuggestion = providerGroupSuggestions[1],
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertEquals(2, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(123, impressionInfo.blockId)
+ assertEquals("mozilla", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression", impressionInfo.reportingUrl)
+ assertEquals("22 - Shopping", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertTrue(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ processor.facts[1].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_CLICKED, item)
+
+ assertEquals(setOf(FxSuggestFacts.MetadataKeys.INTERACTION_INFO, FxSuggestFacts.MetadataKeys.POSITION), metadata?.keys)
+
+ val clickInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(123, clickInfo.blockId)
+ assertEquals("mozilla", clickInfo.advertiser)
+ assertEquals("https://example.com/click", clickInfo.reportingUrl)
+ assertEquals("22 - Shopping", clickInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", clickInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+ }
+ }
+
+ @Test
+ fun `GIVEN 2 AMP suggestions are visible WHEN the engagement is completed THEN 2 impression facts are collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/impression-1",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/click-1",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 456,
+ advertiser = "good place eats",
+ reportingUrl = "https://example.com/impression-2",
+ iabCategory = "8 - Food & Drink",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 456,
+ advertiser = "good place eats",
+ reportingUrl = "https://example.com/click-2",
+ iabCategory = "8 - Food & Drink",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertEquals(2, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(123, impressionInfo.blockId)
+ assertEquals("mozilla", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression-1", impressionInfo.reportingUrl)
+ assertEquals("22 - Shopping", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ processor.facts[1].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(456, impressionInfo.blockId)
+ assertEquals("good place eats", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression-2", impressionInfo.reportingUrl)
+ assertEquals("8 - Food & Drink", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(4, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ }
+
+ @Test
+ fun `GIVEN 2 AMP suggestions are visible and a non-AMP suggestion is clicked WHEN the engagement is completed THEN 2 impression facts are collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/impression-1",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/click-1",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 456,
+ advertiser = "good place eats",
+ reportingUrl = "https://example.com/impression-2",
+ iabCategory = "8 - Food & Drink",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 456,
+ advertiser = "good place eats",
+ reportingUrl = "https://example.com/click-2",
+ iabCategory = "8 - Food & Drink",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ clickedSuggestion = providerGroupSuggestions[2],
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertEquals(2, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(123, impressionInfo.blockId)
+ assertEquals("mozilla", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression-1", impressionInfo.reportingUrl)
+ assertEquals("22 - Shopping", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ processor.facts[1].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(456, impressionInfo.blockId)
+ assertEquals("good place eats", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression-2", impressionInfo.reportingUrl)
+ assertEquals("8 - Food & Drink", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(4, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ }
+
+ @Test
+ fun `GIVEN 2 AMP suggestions are visible and an AMP suggestion is clicked WHEN the engagement is completed THEN 2 impression facts and 1 click fact are collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/impression-1",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/click-1",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 456,
+ advertiser = "good place eats",
+ reportingUrl = "https://example.com/impression-2",
+ iabCategory = "8 - Food & Drink",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Amp(
+ blockId = 456,
+ advertiser = "good place eats",
+ reportingUrl = "https://example.com/click-2",
+ iabCategory = "8 - Food & Drink",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ clickedSuggestion = providerGroupSuggestions[3],
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertEquals(3, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(123, impressionInfo.blockId)
+ assertEquals("mozilla", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression-1", impressionInfo.reportingUrl)
+ assertEquals("22 - Shopping", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ processor.facts[1].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(456, impressionInfo.blockId)
+ assertEquals("good place eats", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression-2", impressionInfo.reportingUrl)
+ assertEquals("8 - Food & Drink", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(4, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertTrue(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ processor.facts[2].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_CLICKED, item)
+
+ assertEquals(setOf(FxSuggestFacts.MetadataKeys.INTERACTION_INFO, FxSuggestFacts.MetadataKeys.POSITION), metadata?.keys)
+
+ val clickInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(456, clickInfo.blockId)
+ assertEquals("good place eats", clickInfo.advertiser)
+ assertEquals("https://example.com/click-2", clickInfo.reportingUrl)
+ assertEquals("8 - Food & Drink", clickInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", clickInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(4, position)
+ }
+ }
+
+ @Test
+ fun `GIVEN 1 Wikipedia suggestion is visible WHEN the engagement is completed THEN 1 impression fact is collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Wikipedia(
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Wikipedia(
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertEquals(1, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Wikipedia)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertFalse(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ }
+
+ @Test
+ fun `GIVEN 1 Wikipedia suggestion is visible and clicked WHEN the engagement is completed THEN 1 impression fact and 1 click fact are collected`() {
+ val provider: AwesomeBar.SuggestionProvider = mock()
+ val providerGroup = AwesomeBar.SuggestionProviderGroup(listOf(provider))
+ val providerGroupSuggestions = listOf(
+ AwesomeBar.Suggestion(provider),
+ AwesomeBar.Suggestion(
+ provider = provider,
+ metadata = mapOf(
+ FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO to FxSuggestInteractionInfo.Wikipedia(
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO to FxSuggestInteractionInfo.Wikipedia(
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ awesomeBarState = AwesomeBarState(
+ visibilityState = AwesomeBar.VisibilityState(
+ visibleProviderGroups = mapOf(providerGroup to providerGroupSuggestions),
+ ),
+ clickedSuggestion = providerGroupSuggestions[1],
+ ),
+ ),
+ middleware = listOf(FxSuggestFactsMiddleware()),
+ )
+
+ store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)).joinBlocking()
+
+ assertEquals(2, processor.facts.size)
+ processor.facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Wikipedia)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertTrue(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ processor.facts[1].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_CLICKED, item)
+
+ assertEquals(setOf(FxSuggestFacts.MetadataKeys.INTERACTION_INFO, FxSuggestFacts.MetadataKeys.POSITION), metadata?.keys)
+
+ val clickInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Wikipedia)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", clickInfo.contextId)
+
+ val position = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(2, position)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestFactsTest.kt b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestFactsTest.kt
new file mode 100644
index 0000000000..a6274d743d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestFactsTest.kt
@@ -0,0 +1,191 @@
+/* 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.fxsuggest
+
+import mozilla.components.feature.fxsuggest.facts.FxSuggestFacts
+import mozilla.components.feature.fxsuggest.facts.emitSuggestionClickedFact
+import mozilla.components.feature.fxsuggest.facts.emitSuggestionImpressedFact
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class FxSuggestFactsTest {
+
+ @Test
+ fun `GIVEN interaction information for an AMP suggestion WHEN emitting a click fact THEN 1 click fact is collected`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSuggestionClickedFact(
+ FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/reporting",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ positionInAwesomeBar = 0,
+ )
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_CLICKED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ ),
+ metadata?.keys,
+ )
+
+ val clickInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(clickInfo.blockId, 123)
+ assertEquals(clickInfo.advertiser, "mozilla")
+ assertEquals(clickInfo.reportingUrl, "https://example.com/reporting")
+ assertEquals(clickInfo.iabCategory, "22 - Shopping")
+ assertEquals(clickInfo.contextId, "c303282d-f2e6-46ca-a04a-35d3d873712d")
+
+ val positionInAwesomebar = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(0, positionInAwesomebar)
+ }
+ }
+ }
+
+ @Test
+ fun `GIVEN interaction information for an AMP suggestion WHEN emitting an impression fact THEN 1 impression fact is collected`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSuggestionImpressedFact(
+ FxSuggestInteractionInfo.Amp(
+ blockId = 123,
+ advertiser = "mozilla",
+ reportingUrl = "https://example.com/reporting",
+ iabCategory = "22 - Shopping",
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ positionInAwesomeBar = 0,
+ isClicked = true,
+ engagementAbandoned = false,
+ )
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Amp)
+ assertEquals(impressionInfo.blockId, 123)
+ assertEquals(impressionInfo.advertiser, "mozilla")
+ assertEquals(impressionInfo.reportingUrl, "https://example.com/reporting")
+ assertEquals(impressionInfo.iabCategory, "22 - Shopping")
+ assertEquals(impressionInfo.contextId, "c303282d-f2e6-46ca-a04a-35d3d873712d")
+
+ val positionInAwesomebar = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(0, positionInAwesomebar)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertTrue(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ }
+ }
+
+ @Test
+ fun `GIVEN interaction information for a Wikipedia suggestion WHEN emitting a click fact THEN 1 click fact is collected`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSuggestionClickedFact(
+ FxSuggestInteractionInfo.Wikipedia(
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ positionInAwesomeBar = 0,
+ )
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_CLICKED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ ),
+ metadata?.keys,
+ )
+
+ val clickInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Wikipedia)
+ assertEquals(clickInfo.contextId, "c303282d-f2e6-46ca-a04a-35d3d873712d")
+
+ val positionInAwesomebar = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(0, positionInAwesomebar)
+ }
+ }
+ }
+
+ @Test
+ fun `GIVEN interaction information for a Wikipedia suggestion WHEN emitting an impression fact THEN 1 impression fact is collected`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSuggestionImpressedFact(
+ FxSuggestInteractionInfo.Wikipedia(
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ ),
+ positionInAwesomeBar = 0,
+ isClicked = true,
+ engagementAbandoned = false,
+ )
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_FXSUGGEST, component)
+ assertEquals(Action.DISPLAY, action)
+ assertEquals(FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_IMPRESSED, item)
+
+ assertEquals(
+ setOf(
+ FxSuggestFacts.MetadataKeys.INTERACTION_INFO,
+ FxSuggestFacts.MetadataKeys.POSITION,
+ FxSuggestFacts.MetadataKeys.IS_CLICKED,
+ FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED,
+ ),
+ metadata?.keys,
+ )
+
+ val impressionInfo = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO) as? FxSuggestInteractionInfo.Wikipedia)
+ assertEquals(impressionInfo.contextId, "c303282d-f2e6-46ca-a04a-35d3d873712d")
+
+ val positionInAwesomebar = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)
+ assertEquals(0, positionInAwesomebar)
+
+ val isClicked = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)
+ assertTrue(isClicked)
+
+ val engagementAbandoned = requireNotNull(metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean)
+ assertFalse(engagementAbandoned)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionSchedulerTest.kt b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionSchedulerTest.kt
new file mode 100644
index 0000000000..375a0f0d3a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionSchedulerTest.kt
@@ -0,0 +1,80 @@
+/* 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.fxsuggest
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.Configuration
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.await
+import androidx.work.testing.WorkManagerTestInitHelper
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FxSuggestIngestionSchedulerTest {
+ private lateinit var storage: FxSuggestStorage
+ private lateinit var workManager: WorkManager
+
+ @Before
+ fun setUp() {
+ storage = mock()
+ GlobalFxSuggestDependencyProvider.storage = storage
+
+ WorkManagerTestInitHelper.initializeTestWorkManager(
+ testContext,
+ Configuration.Builder().build(),
+ )
+ workManager = WorkManager.getInstance(testContext)
+ }
+
+ @After
+ fun tearDown() {
+ workManager.cancelAllWork()
+ GlobalFxSuggestDependencyProvider.storage = null
+ }
+
+ @Test
+ fun startPeriodicIngestion() = runTest {
+ val scheduler = FxSuggestIngestionScheduler(testContext)
+
+ scheduler.startPeriodicIngestion()
+
+ val workInfos = workManager.getWorkInfosForUniqueWork(FxSuggestIngestionWorker.WORK_TAG).await()
+ assertEquals(1, workInfos.size)
+ assertEquals(WorkInfo.State.ENQUEUED, workInfos.first().state)
+ }
+
+ @Test
+ fun stopPeriodicIngestion() = runTest {
+ val scheduler = FxSuggestIngestionScheduler(testContext)
+ scheduler.startPeriodicIngestion()
+
+ scheduler.stopPeriodicIngestion()
+
+ val workInfos = workManager.getWorkInfosForUniqueWork(FxSuggestIngestionWorker.WORK_TAG).await()
+ assertEquals(1, workInfos.size)
+ assertEquals(WorkInfo.State.CANCELLED, workInfos.first().state)
+ }
+
+ @Test
+ fun createPeriodicIngestionWorkerRequest() = runTest {
+ val scheduler = FxSuggestIngestionScheduler(testContext)
+
+ val workRequest = scheduler.createPeriodicIngestionWorkerRequest()
+
+ assertTrue(workRequest.workSpec.isPeriodic)
+ assertTrue(workRequest.tags.contains(FxSuggestIngestionWorker.WORK_TAG))
+ assertEquals(1 * 24 * 60 * 60 * 1000, workRequest.workSpec.intervalDuration)
+ assertEquals(scheduler.getWorkerConstrains(), workRequest.workSpec.constraints)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionWorkerTest.kt b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionWorkerTest.kt
new file mode 100644
index 0000000000..84df98f78e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestIngestionWorkerTest.kt
@@ -0,0 +1,61 @@
+/* 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.fxsuggest
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class FxSuggestIngestionWorkerTest {
+ private lateinit var storage: FxSuggestStorage
+
+ @Before
+ fun setUp() {
+ storage = mock()
+ GlobalFxSuggestDependencyProvider.storage = storage
+ }
+
+ @After
+ fun tearDown() {
+ GlobalFxSuggestDependencyProvider.storage = null
+ }
+
+ @Test
+ fun workSucceeds() = runTest {
+ whenever(storage.ingest(any())).thenReturn(true)
+
+ val worker = TestListenableWorkerBuilder<FxSuggestIngestionWorker>(testContext).build()
+
+ val result = worker.startWork().await()
+
+ verify(storage).ingest(any())
+ assertEquals(ListenableWorker.Result.success(), result)
+ }
+
+ @Test
+ fun workShouldRetry() = runTest {
+ whenever(storage.ingest(any())).thenReturn(false)
+
+ val worker = TestListenableWorkerBuilder<FxSuggestIngestionWorker>(testContext).build()
+
+ val result = worker.startWork().await()
+
+ verify(storage).ingest(any())
+ assertEquals(ListenableWorker.Result.retry(), result)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestSuggestionProviderTest.kt
new file mode 100644
index 0000000000..93d106fa5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/test/java/mozilla/components/feature/fxsuggest/FxSuggestSuggestionProviderTest.kt
@@ -0,0 +1,394 @@
+/* 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.fxsuggest
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.appservices.suggest.Suggestion
+import mozilla.appservices.suggest.SuggestionProvider
+import mozilla.appservices.suggest.SuggestionQuery
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.After
+import org.junit.Assert.assertEquals
+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.Mockito.doNothing
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class FxSuggestSuggestionProviderTest {
+ private lateinit var storage: FxSuggestStorage
+
+ @Before
+ fun setUp() {
+ storage = mock()
+ val suggestionProviderConfig = AwesomebarSuggestionProvider(
+ availableSuggestionTypes = mapOf(
+ SuggestionType.AMP to true,
+ SuggestionType.AMP_MOBILE to false,
+ SuggestionType.WIKIPEDIA to true,
+ ),
+ )
+ FxSuggestNimbus.features.awesomebarSuggestionProvider.withCachedValue(suggestionProviderConfig)
+ GlobalFxSuggestDependencyProvider.storage = storage
+ }
+
+ @After
+ fun tearDown() {
+ FxSuggestNimbus.features.awesomebarSuggestionProvider.withCachedValue(null)
+ GlobalFxSuggestDependencyProvider.storage = null
+ }
+
+ @Test
+ fun inputEmpty() = runTest {
+ whenever(storage.query(any())).thenReturn(
+ listOf(
+ Suggestion.Wikipedia(
+ title = "Las Vegas",
+ url = "https://wikipedia.org/wiki/Las_Vegas",
+ icon = null,
+ iconMimetype = null,
+ fullKeyword = "las",
+ ),
+ ),
+ )
+
+ val provider = FxSuggestSuggestionProvider(
+ resources = testContext.resources,
+ loadUrlUseCase = mock(),
+ includeNonSponsoredSuggestions = true,
+ includeSponsoredSuggestions = true,
+ )
+
+ val suggestions = provider.onInputChanged("")
+
+ verify(storage, never()).query(any())
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun inputNotEmpty() = runTest {
+ whenever(storage.query(any())).thenReturn(
+ listOf(
+ Suggestion.Amp(
+ title = "Lasagna Come Out Tomorrow",
+ url = "https://www.lasagna.restaurant",
+ rawUrl = "https://www.lasagna.restaurant",
+ icon = listOf(
+ 137u, 80u, 78u, 71u, 13u, 10u, 26u, 10u, 0u, 0u, 0u, 13u, 73u, 72u, 68u, 82u,
+ 0u, 0u, 0u, 1u, 0u, 0u, 0u, 1u, 1u, 3u, 0u, 0u, 0u, 37u, 219u, 86u, 202u, 0u,
+ 0u, 0u, 3u, 80u, 76u, 84u, 69u, 0u, 0u, 0u, 167u, 122u, 61u, 218u, 0u, 0u, 0u,
+ 1u, 116u, 82u, 78u, 83u, 0u, 64u, 230u, 216u, 102u, 0u, 0u, 0u, 10u, 73u, 68u,
+ 65u, 84u, 8u, 215u, 99u, 96u, 0u, 0u, 0u, 2u, 0u, 1u, 226u, 33u, 188u, 51u, 0u,
+ 0u, 0u, 0u, 73u, 69u, 78u, 68u, 174u, 66u, 96u, 130u,
+ ),
+ iconMimetype = null,
+ fullKeyword = "lasagna",
+ blockId = 0,
+ advertiser = "Good Place Eats",
+ iabCategory = "8 - Food & Drink",
+ impressionUrl = "https://example.com/impression_url",
+ clickUrl = "https://example.com/click_url",
+ rawClickUrl = "https://example.com/click_url",
+ score = 0.3,
+ ),
+ ),
+ )
+
+ val provider = FxSuggestSuggestionProvider(
+ resources = testContext.resources,
+ loadUrlUseCase = mock(),
+ includeNonSponsoredSuggestions = true,
+ includeSponsoredSuggestions = true,
+ )
+
+ val suggestions = provider.onInputChanged("la")
+
+ verify(storage).query(
+ eq(
+ SuggestionQuery(
+ keyword = "la",
+ providers = listOf(SuggestionProvider.AMP, SuggestionProvider.WIKIPEDIA),
+ limit = 1,
+ ),
+ ),
+ )
+ assertEquals(1, suggestions.size)
+ assertEquals("Lasagna Come Out Tomorrow", suggestions[0].title)
+ assertEquals(testContext.resources.getString(R.string.sponsored_suggestion_description), suggestions[0].description)
+ assertNotNull(suggestions[0].icon)
+ assertEquals(Int.MIN_VALUE, suggestions[0].score)
+ assertTrue(suggestions[0].metadata.isNullOrEmpty())
+ }
+
+ @Test
+ fun inputCancelled() = runTest {
+ doNothing().`when`(storage).cancelReads()
+
+ val provider = FxSuggestSuggestionProvider(
+ resources = testContext.resources,
+ loadUrlUseCase = mock(),
+ includeNonSponsoredSuggestions = true,
+ includeSponsoredSuggestions = true,
+ )
+
+ provider.onInputCancelled()
+
+ verify(storage).cancelReads()
+ }
+
+ @Test
+ fun includeNonSponsoredSuggestionsOnly() = runTest {
+ whenever(storage.query(any())).thenReturn(
+ listOf(
+ Suggestion.Wikipedia(
+ title = "Las Vegas",
+ url = "https://wikipedia.org/wiki/Las_Vegas",
+ icon = null,
+ iconMimetype = null,
+ fullKeyword = "las",
+ ),
+ ),
+ )
+
+ val provider = FxSuggestSuggestionProvider(
+ resources = testContext.resources,
+ loadUrlUseCase = mock(),
+ includeNonSponsoredSuggestions = true,
+ includeSponsoredSuggestions = false,
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ )
+
+ val suggestions = provider.onInputChanged("la")
+
+ verify(storage).query(
+ eq(
+ SuggestionQuery(
+ keyword = "la",
+ providers = listOf(SuggestionProvider.WIKIPEDIA),
+ limit = 1,
+ ),
+ ),
+ )
+ assertEquals(1, suggestions.size)
+ assertEquals("Las Vegas", suggestions.first().title)
+ assertNull(suggestions.first().description)
+ assertNull(suggestions.first().icon)
+ assertEquals(Int.MIN_VALUE, suggestions.first().score)
+ suggestions.first().metadata?.let {
+ assertEquals(setOf(FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO, FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO), it.keys)
+
+ val clickInfo = requireNotNull(it[FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO] as? FxSuggestInteractionInfo.Wikipedia)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", clickInfo.contextId)
+
+ val impressionInfo = requireNotNull(it[FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO] as? FxSuggestInteractionInfo.Wikipedia)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+ }
+ }
+
+ @Test
+ fun includeSponsoredSuggestionsOnly() = runTest {
+ whenever(storage.query(any())).thenReturn(
+ listOf(
+ Suggestion.Amp(
+ title = "Lasagna Come Out Tomorrow",
+ url = "https://www.lasagna.restaurant",
+ rawUrl = "https://www.lasagna.restaurant",
+ icon = null,
+ iconMimetype = null,
+ fullKeyword = "lasagna",
+ blockId = 0,
+ advertiser = "Good Place Eats",
+ iabCategory = "8 - Food & Drink",
+ impressionUrl = "https://example.com/impression_url",
+ clickUrl = "https://example.com/click_url",
+ rawClickUrl = "https://example.com/click_url",
+ score = 0.3,
+ ),
+ ),
+ )
+
+ val provider = FxSuggestSuggestionProvider(
+ resources = testContext.resources,
+ loadUrlUseCase = mock(),
+ includeNonSponsoredSuggestions = false,
+ includeSponsoredSuggestions = true,
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ )
+
+ val suggestions = provider.onInputChanged("la")
+
+ verify(storage).query(
+ eq(
+ SuggestionQuery(
+ keyword = "la",
+ providers = listOf(SuggestionProvider.AMP),
+ limit = 1,
+ ),
+ ),
+ )
+ assertEquals(1, suggestions.size)
+ assertEquals("Lasagna Come Out Tomorrow", suggestions.first().title)
+ assertEquals(testContext.resources.getString(R.string.sponsored_suggestion_description), suggestions.first().description)
+ assertEquals(Int.MIN_VALUE, suggestions.first().score)
+ suggestions.first().metadata?.let {
+ assertEquals(setOf(FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO, FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO), it.keys)
+
+ val clickInfo = requireNotNull(it[FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO] as? FxSuggestInteractionInfo.Amp)
+ assertEquals(0, clickInfo.blockId)
+ assertEquals("good place eats", clickInfo.advertiser)
+ assertEquals("https://example.com/click_url", clickInfo.reportingUrl)
+ assertEquals("8 - Food & Drink", clickInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", clickInfo.contextId)
+
+ val impressionInfo = requireNotNull(it[FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO] as? FxSuggestInteractionInfo.Amp)
+ assertEquals(0, impressionInfo.blockId)
+ assertEquals("good place eats", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression_url", impressionInfo.reportingUrl)
+ assertEquals("8 - Food & Drink", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+ }
+ }
+
+ @Test
+ fun includeMobileSponsoredSuggestionsOnly() = runTest {
+ FxSuggestNimbus.features.awesomebarSuggestionProvider.withCachedValue(
+ AwesomebarSuggestionProvider(
+ availableSuggestionTypes = mapOf(
+ SuggestionType.AMP to false,
+ SuggestionType.AMP_MOBILE to true,
+ SuggestionType.WIKIPEDIA to true,
+ ),
+ ),
+ )
+ whenever(storage.query(any())).thenReturn(
+ listOf(
+ Suggestion.Amp(
+ title = "Mobile - Lasagna Come Out Tomorrow",
+ url = "https://www.lasagna.restaurant",
+ rawUrl = "https://www.lasagna.restaurant",
+ icon = null,
+ iconMimetype = null,
+ fullKeyword = "lasagna",
+ blockId = 0,
+ advertiser = "Good Place Eats",
+ iabCategory = "8 - Food & Drink",
+ impressionUrl = "https://example.com/impression_url",
+ clickUrl = "https://example.com/click_url",
+ rawClickUrl = "https://example.com/click_url",
+ score = 0.3,
+ ),
+ ),
+ )
+
+ val provider = FxSuggestSuggestionProvider(
+ resources = testContext.resources,
+ loadUrlUseCase = mock(),
+ includeNonSponsoredSuggestions = false,
+ includeSponsoredSuggestions = true,
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ )
+
+ val suggestions = provider.onInputChanged("la")
+
+ verify(storage).query(
+ eq(
+ SuggestionQuery(
+ keyword = "la",
+ providers = listOf(SuggestionProvider.AMP_MOBILE),
+ limit = 1,
+ ),
+ ),
+ )
+ assertEquals(1, suggestions.size)
+ assertEquals("Mobile - Lasagna Come Out Tomorrow", suggestions.first().title)
+ assertEquals(testContext.resources.getString(R.string.sponsored_suggestion_description), suggestions.first().description)
+ assertEquals(Int.MIN_VALUE, suggestions.first().score)
+ suggestions.first().metadata?.let {
+ assertEquals(setOf(FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO, FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO), it.keys)
+
+ val clickInfo = requireNotNull(it[FxSuggestSuggestionProvider.MetadataKeys.CLICK_INFO] as? FxSuggestInteractionInfo.Amp)
+ assertEquals(0, clickInfo.blockId)
+ assertEquals("good place eats", clickInfo.advertiser)
+ assertEquals("https://example.com/click_url", clickInfo.reportingUrl)
+ assertEquals("8 - Food & Drink", clickInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", clickInfo.contextId)
+
+ val impressionInfo = requireNotNull(it[FxSuggestSuggestionProvider.MetadataKeys.IMPRESSION_INFO] as? FxSuggestInteractionInfo.Amp)
+ assertEquals(0, impressionInfo.blockId)
+ assertEquals("good place eats", impressionInfo.advertiser)
+ assertEquals("https://example.com/impression_url", impressionInfo.reportingUrl)
+ assertEquals("8 - Food & Drink", impressionInfo.iabCategory)
+ assertEquals("c303282d-f2e6-46ca-a04a-35d3d873712d", impressionInfo.contextId)
+ }
+ }
+
+ @Test
+ fun includeSponsoredSuggestionsOnlyWhenAmpUnavailable() = runTest {
+ FxSuggestNimbus.features.awesomebarSuggestionProvider.withCachedValue(
+ AwesomebarSuggestionProvider(availableSuggestionTypes = emptyMap()),
+ )
+ whenever(storage.query(any())).thenReturn(emptyList())
+
+ val provider = FxSuggestSuggestionProvider(
+ resources = testContext.resources,
+ loadUrlUseCase = mock(),
+ includeNonSponsoredSuggestions = false,
+ includeSponsoredSuggestions = true,
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ )
+
+ val suggestions = provider.onInputChanged("la")
+
+ verify(storage).query(
+ eq(
+ SuggestionQuery(
+ keyword = "la",
+ providers = emptyList(),
+ limit = 1,
+ ),
+ ),
+ )
+ assertTrue(suggestions.isEmpty())
+ }
+
+ @Test
+ fun includeNonSponsoredSuggestionsOnlyWhenWikipediaUnavailable() = runTest {
+ FxSuggestNimbus.features.awesomebarSuggestionProvider.withCachedValue(
+ AwesomebarSuggestionProvider(availableSuggestionTypes = emptyMap()),
+ )
+ whenever(storage.query(any())).thenReturn(emptyList())
+
+ val provider = FxSuggestSuggestionProvider(
+ resources = testContext.resources,
+ loadUrlUseCase = mock(),
+ includeNonSponsoredSuggestions = true,
+ includeSponsoredSuggestions = false,
+ contextId = "c303282d-f2e6-46ca-a04a-35d3d873712d",
+ )
+
+ val suggestions = provider.onInputChanged("la")
+
+ verify(storage).query(
+ eq(
+ SuggestionQuery(
+ keyword = "la",
+ providers = emptyList(),
+ limit = 1,
+ ),
+ ),
+ )
+ assertTrue(suggestions.isEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/fxsuggest/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/fxsuggest/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/fxsuggest/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/fxsuggest/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/fxsuggest/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/fxsuggest/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/intent/README.md b/mobile/android/android-components/components/feature/intent/README.md
new file mode 100644
index 0000000000..e3e5ba90a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Intent
+
+A component that provides intent processing functionality by combining various other feature modules.
+
+## Usage
+
+### 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-intent:{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/intent/build.gradle b/mobile/android/android-components/components/feature/intent/build.gradle
new file mode 100644
index 0000000000..022636abb5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/build.gradle
@@ -0,0 +1,46 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.intent'
+}
+
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':browser-state')
+ implementation project(':feature-search')
+ implementation project(':feature-session')
+ implementation project(':feature-tabs')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+ testImplementation ComponentsDependencies.androidx_browser
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.kotlin_coroutines
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/intent/proguard-rules.pro b/mobile/android/android-components/components/feature/intent/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/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/intent/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/intent/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/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/intent/src/main/java/mozilla/components/feature/intent/ext/IntentExtensions.kt b/mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/ext/IntentExtensions.kt
new file mode 100644
index 0000000000..b2336b48bb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/ext/IntentExtensions.kt
@@ -0,0 +1,63 @@
+/* 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.intent.ext
+
+import android.content.Intent
+import android.os.BadParcelableException
+import mozilla.components.support.utils.SafeIntent
+
+const val EXTRA_SESSION_ID = "activeSessionId"
+
+/**
+ * Retrieves [mozilla.components.browser.session.Session] ID from the intent.
+ *
+ * @return The session ID previously added with [putSessionId],
+ * or null if no ID was found.
+ */
+fun Intent.getSessionId(): String? = getStringExtra(EXTRA_SESSION_ID)
+
+/**
+ * Retrieves [mozilla.components.browser.session.Session] ID from the intent.
+ *
+ * @return The session ID previously added with [putSessionId],
+ * or null if no ID was found.
+ */
+fun SafeIntent.getSessionId(): String? = getStringExtra(EXTRA_SESSION_ID)
+
+/**
+ * Add [mozilla.components.browser.session.Session] ID to the intent.
+ *
+ * @param sessionId The session ID data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see [getSessionId]
+ */
+fun Intent.putSessionId(sessionId: String?): Intent {
+ return putExtra(EXTRA_SESSION_ID, sessionId)
+}
+
+/**
+ * Sanitizes the intent. If the intent cannot be unparcelled, all extras are removed.
+ * https://developer.android.com/guide/components/activities/parcelables-and-bundles
+ *
+ * @return Returns the sanitized Intent object.
+ */
+@Suppress("TooGenericExceptionCaught")
+fun Intent.sanitize(): Intent {
+ try {
+ this.getBooleanExtra("TriggerUnparcel", false)
+ return this
+ } catch (e: BadParcelableException) {
+ return this.replaceExtras(null)
+ } catch (e: RuntimeException) {
+ if (e.cause is ClassNotFoundException) {
+ return this.replaceExtras(null)
+ }
+
+ throw e
+ }
+}
diff --git a/mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/IntentProcessor.kt b/mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/IntentProcessor.kt
new file mode 100644
index 0000000000..fa252c415d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/IntentProcessor.kt
@@ -0,0 +1,20 @@
+/* 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.intent.processing
+
+import android.content.Intent
+
+/**
+ * Processor for Android intents which should trigger session-related actions.
+ */
+interface IntentProcessor {
+ /**
+ * Processes the given [Intent].
+ *
+ * @param intent The intent to process.
+ * @return True if the intent was processed, otherwise false.
+ */
+ fun process(intent: Intent): Boolean
+}
diff --git a/mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/TabIntentProcessor.kt b/mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/TabIntentProcessor.kt
new file mode 100644
index 0000000000..508f1aeb75
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/TabIntentProcessor.kt
@@ -0,0 +1,121 @@
+/* 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.intent.processing
+
+import android.app.SearchManager
+import android.content.Intent
+import android.content.Intent.ACTION_MAIN
+import android.content.Intent.ACTION_SEARCH
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_VIEW
+import android.content.Intent.ACTION_WEB_SEARCH
+import android.content.Intent.EXTRA_TEXT
+import android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.externalPackage
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.ktx.kotlin.isUrl
+import mozilla.components.support.ktx.kotlin.toNormalizedUrl
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.utils.WebURLFinder
+
+/**
+ * Processor for intents which should trigger session-related actions.
+ *
+ * @property tabsUseCases An instance of [TabsUseCases] used to open new tabs.
+ * @property newTabSearchUseCase A reference to [SearchUseCases.NewTabSearchUseCase] to be used for
+ * ACTION_SEND intents if the provided text is not a URL.
+ * @property isPrivate Whether a processed intent should open a new tab as private
+ */
+class TabIntentProcessor(
+ private val tabsUseCases: TabsUseCases,
+ private val newTabSearchUseCase: SearchUseCases.NewTabSearchUseCase,
+ private val isPrivate: Boolean = false,
+) : IntentProcessor {
+
+ /**
+ * Loads a URL from a view intent in a new session.
+ */
+ private fun processViewIntent(intent: SafeIntent): Boolean {
+ val url = intent.dataString
+
+ return if (url.isNullOrEmpty()) {
+ false
+ } else {
+ val caller = intent.externalPackage()
+ tabsUseCases.selectOrAddTab(
+ url.toNormalizedUrl(),
+ private = isPrivate,
+ source = SessionState.Source.External.ActionView(caller),
+ flags = LoadUrlFlags.external(),
+ )
+ true
+ }
+ }
+
+ /**
+ * Processes a send intent and tries to load [EXTRA_TEXT] as a URL.
+ * If it's not a URL, a search is run instead.
+ */
+ private fun processSendIntent(intent: SafeIntent): Boolean {
+ val extraText = intent.getStringExtra(EXTRA_TEXT)
+
+ return if (extraText.isNullOrBlank()) {
+ false
+ } else {
+ val url = WebURLFinder(extraText).bestWebURL()
+ val source = SessionState.Source.External.ActionSend(intent.externalPackage())
+ if (url != null) {
+ addNewTab(url, source)
+ } else {
+ newTabSearchUseCase(extraText, source)
+ }
+ true
+ }
+ }
+
+ private fun processSearchIntent(intent: SafeIntent): Boolean {
+ val searchQuery = intent.getStringExtra(SearchManager.QUERY)
+
+ return if (searchQuery.isNullOrBlank()) {
+ false
+ } else {
+ val source = SessionState.Source.External.ActionSearch(intent.externalPackage())
+ if (searchQuery.isUrl()) {
+ addNewTab(searchQuery, source)
+ } else {
+ newTabSearchUseCase(searchQuery, source)
+ }
+ true
+ }
+ }
+
+ private fun addNewTab(url: String, source: SessionState.Source) {
+ tabsUseCases.addTab(
+ url.toNormalizedUrl(),
+ source = source,
+ flags = LoadUrlFlags.external(),
+ private = isPrivate,
+ )
+ }
+
+ /**
+ * Processes the given intent by invoking the registered handler.
+ *
+ * @param intent the intent to process
+ * @return true if the intent was processed, otherwise false.
+ */
+ override fun process(intent: Intent): Boolean {
+ val safeIntent = SafeIntent(intent)
+ return when (safeIntent.action) {
+ ACTION_VIEW, ACTION_MAIN, ACTION_NDEF_DISCOVERED -> processViewIntent(safeIntent)
+ ACTION_SEND -> processSendIntent(safeIntent)
+ ACTION_SEARCH, ACTION_WEB_SEARCH -> processSearchIntent(safeIntent)
+ else -> false
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/intent/src/test/java/mozilla/components/feature/intent/ext/IntentExtensionsTest.kt b/mobile/android/android-components/components/feature/intent/src/test/java/mozilla/components/feature/intent/ext/IntentExtensionsTest.kt
new file mode 100644
index 0000000000..9d37aba370
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/src/test/java/mozilla/components/feature/intent/ext/IntentExtensionsTest.kt
@@ -0,0 +1,83 @@
+/* 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.intent.ext
+
+import android.content.Intent
+import android.os.BadParcelableException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.utils.toSafeIntent
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class IntentExtensionsTest {
+
+ @Test
+ fun `getSessionId should call getStringExtra`() {
+ val id = "mock-session-id"
+ val intent: Intent = mock()
+ val safeIntent: SafeIntent = mock()
+
+ `when`(intent.getStringExtra(EXTRA_SESSION_ID)).thenReturn(id)
+ `when`(safeIntent.getStringExtra(EXTRA_SESSION_ID)).thenReturn(id)
+
+ assertEquals(id, intent.getSessionId())
+ assertEquals(id, safeIntent.getSessionId())
+ }
+
+ @Test
+ fun `putSessionId should put string extra`() {
+ val id = "mock-session-id"
+ val intent = Intent()
+
+ assertEquals(intent, intent.putSessionId(id))
+
+ assertEquals(id, intent.getSessionId())
+ assertEquals(id, intent.toSafeIntent().getSessionId())
+ }
+
+ @Test
+ fun `WHEN unparcel successful THEN extras are not removed`() {
+ val intent: Intent = mock()
+ `when`(intent.getBooleanExtra("TriggerUnparcel", false)).thenReturn(false)
+
+ intent.sanitize()
+ verify(intent, never()).replaceExtras(null)
+ }
+
+ @Test
+ fun `WHEN unparcel fails with BadParcelableException THEN extras are cleared`() {
+ val intent: Intent = mock()
+ `when`(intent.getBooleanExtra("TriggerUnparcel", false)).thenThrow(BadParcelableException("test"))
+ `when`(intent.replaceExtras(null)).thenReturn(intent)
+
+ intent.sanitize()
+ verify(intent).replaceExtras(null)
+ }
+
+ @Test
+ fun `WHEN unparcel fails with RuntimeException and ClassNotFoundException cause THEN extras are cleared`() {
+ val intent: Intent = mock()
+ `when`(intent.getBooleanExtra("TriggerUnparcel", false)).thenThrow(RuntimeException("test", ClassNotFoundException("test")))
+ `when`(intent.replaceExtras(null)).thenReturn(intent)
+
+ intent.sanitize()
+ verify(intent).replaceExtras(null)
+ }
+
+ @Test(expected = RuntimeException::class)
+ fun `WHEN unparcel fails with RuntimeException THEN extras are cleared`() {
+ val intent: Intent = mock()
+ `when`(intent.getBooleanExtra("TriggerUnparcel", false)).thenThrow(RuntimeException("test"))
+
+ intent.sanitize()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt b/mobile/android/android-components/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt
new file mode 100644
index 0000000000..125c691625
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt
@@ -0,0 +1,408 @@
+/* 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.intent.processing
+
+import android.app.SearchManager
+import android.content.Intent
+import android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.selector.findNormalOrPrivateTabByUrl
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.search.ext.createSearchEngine
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+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.Mockito.anyBoolean
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.doReturn
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class TabIntentProcessorTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ private lateinit var middleware: CaptureActionsMiddleware<BrowserState, BrowserAction>
+
+ private lateinit var searchEngine: SearchEngine
+ private lateinit var store: BrowserStore
+ private lateinit var engine: Engine
+ private lateinit var engineSession: EngineSession
+
+ private lateinit var sessionUseCases: SessionUseCases
+ private lateinit var tabsUseCases: TabsUseCases
+ private lateinit var searchUseCases: SearchUseCases
+
+ @Before
+ fun setup() {
+ searchEngine = createSearchEngine(
+ name = "Test",
+ url = "https://localhost/?q={searchTerms}",
+ icon = mock(),
+ )
+
+ engine = mock()
+ engineSession = mock()
+ doReturn(engineSession).`when`(engine).createSession(anyBoolean(), anyString())
+
+ middleware = CaptureActionsMiddleware()
+
+ store = BrowserStore(
+ BrowserState(
+ search = SearchState(regionSearchEngines = listOf(searchEngine)),
+ ),
+ middleware = EngineMiddleware.create(
+ engine = mock(),
+ scope = scope,
+ ) + listOf(middleware),
+ )
+
+ sessionUseCases = SessionUseCases(store)
+ tabsUseCases = TabsUseCases(store)
+ searchUseCases = SearchUseCases(store, tabsUseCases, sessionUseCases)
+ }
+
+ @Test
+ fun `open or select tab on ACTION_VIEW intent`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_VIEW)
+ whenever(intent.dataString).thenReturn("http://mozilla.org")
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionView)
+
+ val tab = store.state.findNormalOrPrivateTabByUrl("http://mozilla.org", private = false)
+ assertNotNull(tab)
+
+ val otherTab = createTab("https://firefox.com")
+ store.dispatch(TabListAction.AddTabAction(otherTab, select = true)).joinBlocking()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(otherTab, store.state.selectedTab)
+ assertTrue(store.state.tabs[1].source is SessionState.Source.Internal.None)
+
+ // processing the same intent again doesn't add an additional tab
+ handler.process(intent)
+ // processing a similar intent which produces the same url doesn't add an additional tab
+ whenever(intent.dataString).thenReturn("mozilla.org")
+ handler.process(intent)
+
+ store.waitUntilIdle()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(tab, store.state.selectedTab)
+ // sources of existing tabs weren't affected
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionView)
+ assertTrue(store.state.tabs[1].source is SessionState.Source.Internal.None)
+
+ // Intent with a url that's missing a scheme
+ whenever(intent.dataString).thenReturn("example.com")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(3, store.state.tabs.size)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionView)
+ assertNotNull(store.state.findNormalOrPrivateTabByUrl("http://example.com", private = false))
+ }
+
+ @Test
+ fun `open or select tab on ACTION_MAIN intent`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_MAIN)
+ whenever(intent.dataString).thenReturn("https://mozilla.org")
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionView)
+
+ val tab = store.state.findNormalOrPrivateTabByUrl("https://mozilla.org", false)
+ assertNotNull(tab)
+
+ val otherTab = createTab("https://firefox.com")
+ store.dispatch(TabListAction.AddTabAction(otherTab, select = true)).joinBlocking()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(otherTab, store.state.selectedTab)
+
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(tab, store.state.selectedTab)
+
+ // Intent with a url that's missing a scheme
+ whenever(intent.dataString).thenReturn("example.com")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(3, store.state.tabs.size)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionView)
+ assertNotNull(store.state.findNormalOrPrivateTabByUrl("http://example.com", private = false))
+ }
+
+ @Test
+ fun `open or select tab on ACTION_NDEF_DISCOVERED intent`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(ACTION_NDEF_DISCOVERED)
+ whenever(intent.dataString).thenReturn("https://mozilla.org")
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+
+ val tab = store.state.findNormalOrPrivateTabByUrl("https://mozilla.org", false)
+ assertNotNull(tab)
+
+ val otherTab = createTab("https://firefox.com")
+ store.dispatch(TabListAction.AddTabAction(otherTab, select = true)).joinBlocking()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(otherTab, store.state.selectedTab)
+
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(tab, store.state.selectedTab)
+
+ // Intent with a url that's missing a scheme
+ whenever(intent.dataString).thenReturn("example.com")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(3, store.state.tabs.size)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionView)
+ assertNotNull(store.state.findNormalOrPrivateTabByUrl("http://example.com", private = false))
+ }
+
+ @Test
+ fun `open tab on ACTION_SEND intent`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_SEND)
+ whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("https://mozilla.org")
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://mozilla.org", store.state.tabs[0].content.url)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionSend)
+
+ whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("see https://getpocket.com")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.tabs[1].content.url)
+ assertTrue(store.state.tabs[1].source is SessionState.Source.External.ActionSend)
+
+ whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("see https://firefox.com and https://mozilla.org")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://firefox.com", store.state.tabs[2].content.url)
+ assertTrue(store.state.tabs[2].source is SessionState.Source.External.ActionSend)
+
+ whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("checkout the Tweet: https://tweets.mozilla.com")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(4, store.state.tabs.size)
+ assertEquals("https://tweets.mozilla.com", store.state.tabs[3].content.url)
+ assertTrue(store.state.tabs[3].source is SessionState.Source.External.ActionSend)
+
+ whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("checkout the Tweet: HTTPS://tweets.mozilla.org")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(5, store.state.tabs.size)
+ assertEquals("https://tweets.mozilla.org", store.state.tabs[4].content.url)
+ assertTrue(store.state.tabs[4].source is SessionState.Source.External.ActionSend)
+
+ // Intent with a url that's missing a scheme
+ whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("example.com")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(6, store.state.tabs.size)
+ assertTrue(store.state.tabs[5].source is SessionState.Source.External.ActionSend)
+ assertNotNull(store.state.findNormalOrPrivateTabByUrl("http://example.com", private = false))
+ }
+
+ @Test
+ fun `open tab and trigger search on ACTION_SEND if text is not a URL`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val searchTerms = "mozilla android"
+ val searchUrl = "https://localhost/?q=mozilla%20android"
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_SEND)
+ whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn(searchTerms)
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(searchUrl, store.state.tabs[0].content.url)
+ assertEquals(searchTerms, store.state.tabs[0].content.searchTerms)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionSend)
+ }
+
+ @Test
+ fun `nothing happens on ACTION_SEND if no text is provided`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_SEND)
+ whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn(" ")
+
+ val processed = handler.process(intent)
+ assertFalse(processed)
+ }
+
+ @Test
+ fun `nothing happens on ACTION_SEARCH if text is empty`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_SEARCH)
+ whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn(" ")
+
+ val processed = handler.process(intent)
+ assertFalse(processed)
+ }
+
+ @Test
+ fun `open tab on ACTION_SEARCH intent`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_SEARCH)
+ whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn("http://mozilla.org")
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("http://mozilla.org", store.state.tabs[0].content.url)
+ assertEquals("", store.state.tabs[0].content.searchTerms)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionSearch)
+
+ // Intent with a url that's missing a scheme
+ whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn("example.com")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(2, store.state.tabs.size)
+ assertTrue(store.state.tabs[1].source is SessionState.Source.External.ActionSearch)
+ assertNotNull(store.state.findNormalOrPrivateTabByUrl("http://example.com", private = false))
+ }
+
+ @Test
+ fun `open tab and trigger search on ACTION_SEARCH intent if text is not a URL`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val searchTerms = "mozilla android"
+ val searchUrl = "https://localhost/?q=mozilla%20android"
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_SEARCH)
+ whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn(searchTerms)
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(searchUrl, store.state.tabs[0].content.url)
+ assertEquals(searchTerms, store.state.tabs[0].content.searchTerms)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionSearch)
+ }
+
+ @Test
+ fun `nothing happens on ACTION_WEB_SEARCH if text is empty`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_WEB_SEARCH)
+ whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn(" ")
+
+ val processed = handler.process(intent)
+ assertFalse(processed)
+ }
+
+ @Test
+ fun `open tab on ACTION_WEB_SEARCH intent`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_WEB_SEARCH)
+ whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn("http://mozilla.org")
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("http://mozilla.org", store.state.tabs[0].content.url)
+ assertEquals("", store.state.tabs[0].content.searchTerms)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionSearch)
+
+ // Intent with a url that's missing a scheme
+ whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn("example.com")
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(2, store.state.tabs.size)
+ assertTrue(store.state.tabs[1].source is SessionState.Source.External.ActionSearch)
+ assertNotNull(store.state.findNormalOrPrivateTabByUrl("http://example.com", private = false))
+ }
+
+ @Test
+ fun `open tab and trigger search on ACTION_WEB_SEARCH intent if text is not a URL`() {
+ val handler = TabIntentProcessor(TabsUseCases(store), searchUseCases.newTabSearch)
+
+ val searchTerms = "mozilla android"
+ val searchUrl = "https://localhost/?q=mozilla%20android"
+
+ val intent: Intent = mock()
+ whenever(intent.action).thenReturn(Intent.ACTION_SEARCH)
+ whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn(searchTerms)
+
+ assertEquals(0, store.state.tabs.size)
+ handler.process(intent)
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(searchUrl, store.state.tabs[0].content.url)
+ assertEquals(searchTerms, store.state.tabs[0].content.searchTerms)
+ assertTrue(store.state.tabs[0].source is SessionState.Source.External.ActionSearch)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/intent/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/intent/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/intent/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/intent/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/intent/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/intent/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/logins/README.md b/mobile/android/android-components/components/feature/logins/README.md
new file mode 100644
index 0000000000..662567e3cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Logins
+
+Feature component with features related to logins.
+
+## Usage
+
+### 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-logins:{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/logins/build.gradle b/mobile/android/android-components/components/feature/logins/build.gradle
new file mode 100644
index 0000000000..fd4febbe32
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/build.gradle
@@ -0,0 +1,73 @@
+/* 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/. */
+
+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"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ resources {
+ excludes += ['META-INF/proguard/androidx-annotations.pro']
+ }
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.logins'
+}
+
+dependencies {
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':feature-prompts')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_paging
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.kotlin_coroutines
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_arch_core_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+}
+
+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/logins/proguard-rules.pro b/mobile/android/android-components/components/feature/logins/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/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/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json b/mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json
new file mode 100644
index 0000000000..e136e78b0b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json
@@ -0,0 +1,40 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "bbf39f381a14e0e0a5544f54f0e1cedc",
+ "entities": [
+ {
+ "tableName": "logins_exceptions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `origin` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bbf39f381a14e0e0a5544f54f0e1cedc')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt
new file mode 100644
index 0000000000..749ee0b82f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt
@@ -0,0 +1,139 @@
+/* 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.logins
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import mozilla.components.feature.logins.exceptions.LoginException
+import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
+import mozilla.components.feature.logins.exceptions.adapter.LoginExceptionAdapter
+import mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+@Suppress("LargeClass")
+class LoginExceptionStorageTest {
+ private lateinit var context: Context
+ private lateinit var storage: LoginExceptionStorage
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ LoginExceptionDatabase::class.java,
+ )
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+
+ context = ApplicationProvider.getApplicationContext()
+ val database =
+ Room.inMemoryDatabaseBuilder(context, LoginExceptionDatabase::class.java).build()
+
+ storage =
+ LoginExceptionStorage(
+ context,
+ )
+ storage.database = lazy { database }
+ }
+
+ @After
+ fun tearDown() {
+ executor.shutdown()
+ }
+
+ @Test
+ fun testAddingExceptions() {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ val exceptions = getAllExceptions()
+
+ assertEquals(2, exceptions.size)
+
+ assertEquals("mozilla.org", exceptions[0].origin)
+ assertEquals("firefox.com", exceptions[1].origin)
+ }
+
+ @Test
+ fun testRemovingExceptions() {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ getAllExceptions().let { exceptions ->
+ assertEquals(2, exceptions.size)
+ storage.removeLoginException(exceptions[0])
+ }
+
+ getAllExceptions().let { exceptions ->
+ assertEquals(1, exceptions.size)
+ assertEquals("firefox.com", exceptions[0].origin)
+ }
+ }
+
+ @Test
+ fun testGettingExceptions() = runBlocking {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ val exceptions = storage.getLoginExceptions().first()
+
+ assertNotNull(exceptions)
+ assertEquals(2, exceptions.size)
+
+ with(exceptions[0]) {
+ assertEquals("mozilla.org", origin)
+ }
+
+ with(exceptions[1]) {
+ assertEquals("firefox.com", origin)
+ }
+ }
+
+ @Test
+ fun testGettingExceptionsByOrigin() = runBlocking {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ val exception = storage.findExceptionByOrigin("mozilla.org")
+
+ assertNotNull(exception)
+ assertEquals("mozilla.org", exception!!.origin)
+ }
+
+ @Test
+ fun testGettingNoExceptionsByOrigin() = runBlocking {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ val exception = storage.findExceptionByOrigin("testsite.org")
+
+ assertNull(exception)
+ }
+
+ private fun getAllExceptions(): List<LoginException> {
+ return storage.database.value.loginExceptionDao().getLoginExceptionsList()
+ .map { loginExceptionEntity ->
+ LoginExceptionAdapter(loginExceptionEntity)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt
new file mode 100644
index 0000000000..1354a7f15d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt
@@ -0,0 +1,78 @@
+/* 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.logins.exceptions.db
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class LoginExceptionDaoTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: LoginExceptionDatabase
+ private lateinit var loginExceptionDao: LoginExceptionDao
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, LoginExceptionDatabase::class.java).build()
+ loginExceptionDao = database.loginExceptionDao()
+ executor = Executors.newSingleThreadExecutor()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+
+ @Test
+ fun testAddingLoginException() {
+ val exception = LoginExceptionEntity(
+ origin = "mozilla.org",
+ ).also {
+ it.id = loginExceptionDao.insertLoginException(it)
+ }
+
+ val loginExceptionsList = loginExceptionDao.getLoginExceptionsList()
+
+ assertEquals(1, loginExceptionsList.size)
+ assertEquals(exception, loginExceptionsList[0])
+ }
+
+ @Test
+ fun testRemovingLoginException() {
+ val exception1 = LoginExceptionEntity(
+ origin = "mozilla.org",
+ ).also {
+ it.id = loginExceptionDao.insertLoginException(it)
+ }
+
+ val exception2 = LoginExceptionEntity(
+ origin = "firefox.com",
+ ).also {
+ it.id = loginExceptionDao.insertLoginException(it)
+ }
+
+ loginExceptionDao.deleteLoginException(exception1)
+
+ val loginExceptionsList = loginExceptionDao.getLoginExceptionsList()
+
+ assertEquals(1, loginExceptionsList.size)
+ assertEquals(exception2, loginExceptionsList[0])
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/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/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt
new file mode 100644
index 0000000000..bd4c348987
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt
@@ -0,0 +1,20 @@
+/* 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.logins.exceptions
+
+/**
+ * A login exception.
+ */
+interface LoginException {
+ /**
+ * Unique ID of this login exception.
+ */
+ val id: Long
+
+ /**
+ * The origin of the login exception site.
+ */
+ val origin: String
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt
new file mode 100644
index 0000000000..4b86ebb48b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt
@@ -0,0 +1,85 @@
+/* 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.logins.exceptions
+
+import android.content.Context
+import androidx.paging.DataSource
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import mozilla.components.feature.logins.exceptions.adapter.LoginExceptionAdapter
+import mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase
+import mozilla.components.feature.logins.exceptions.db.LoginExceptionEntity
+import mozilla.components.feature.prompts.login.LoginExceptions
+
+/**
+ * A storage implementation for organizing login exceptions.
+ */
+class LoginExceptionStorage(
+ context: Context,
+) : LoginExceptions {
+ internal var database: Lazy<LoginExceptionDatabase> =
+ lazy { LoginExceptionDatabase.get(context) }
+
+ /**
+ * Adds a new [LoginException].
+ *
+ * @param origin The origin.
+ */
+ override fun addLoginException(origin: String) {
+ LoginExceptionEntity(
+ origin = origin,
+ ).also { entity ->
+ entity.id = database.value.loginExceptionDao().insertLoginException(entity)
+ }
+ }
+
+ /**
+ * Returns a [Flow] list of all the [LoginException] instances.
+ */
+ fun getLoginExceptions(): Flow<List<LoginException>> {
+ return database.value.loginExceptionDao().getLoginExceptions().map { list ->
+ list.map { entity -> LoginExceptionAdapter(entity) }
+ }
+ }
+
+ /**
+ * Returns all [LoginException]s as a [DataSource.Factory].
+ */
+ fun getLoginExceptionsPaged(): DataSource.Factory<Int, LoginException> = database.value
+ .loginExceptionDao()
+ .getLoginExceptionsPaged()
+ .map { entity -> LoginExceptionAdapter(entity) }
+
+ /**
+ * Removes the given [LoginException].
+ */
+ fun removeLoginException(site: LoginException) {
+ val exceptionEntity = (site as LoginExceptionAdapter).entity
+ database.value.loginExceptionDao().deleteLoginException(exceptionEntity)
+ }
+
+ override fun isLoginExceptionByOrigin(origin: String): Boolean {
+ return findExceptionByOrigin(origin) != null
+ }
+
+ /**
+ * Finds a [LoginException] by origin.
+ */
+ fun findExceptionByOrigin(origin: String): LoginException? {
+ val exception = database.value.loginExceptionDao().findExceptionByOrigin(origin)
+ return exception?.let {
+ LoginExceptionAdapter(
+ it,
+ )
+ }
+ }
+
+ /**
+ * Removes all [LoginException]s.
+ */
+ fun deleteAllLoginExceptions() {
+ database.value.loginExceptionDao().deleteAllLoginExceptions()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt
new file mode 100644
index 0000000000..9c0a35a8aa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt
@@ -0,0 +1,30 @@
+/* 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.logins.exceptions.adapter
+
+import mozilla.components.feature.logins.exceptions.LoginException
+import mozilla.components.feature.logins.exceptions.db.LoginExceptionEntity
+
+internal class LoginExceptionAdapter(
+ internal val entity: LoginExceptionEntity,
+) : LoginException {
+ override val id: Long
+ get() = entity.id!!
+
+ override val origin: String
+ get() = entity.origin
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is LoginExceptionAdapter) {
+ return false
+ }
+
+ return entity == other.entity
+ }
+
+ override fun hashCode(): Int {
+ return entity.hashCode()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt
new file mode 100644
index 0000000000..e7bac42abc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt
@@ -0,0 +1,43 @@
+/* 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.logins.exceptions.db
+
+import androidx.paging.DataSource
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Internal DAO for accessing [LoginExceptionEntity] instances.
+ */
+@Dao
+internal interface LoginExceptionDao {
+ @Insert
+ fun insertLoginException(exception: LoginExceptionEntity): Long
+
+ @Delete
+ fun deleteLoginException(exception: LoginExceptionEntity)
+
+ @Transaction
+ @Query("SELECT * FROM logins_exceptions")
+ fun getLoginExceptions(): Flow<List<LoginExceptionEntity>>
+
+ @Transaction
+ @Query("SELECT * FROM logins_exceptions")
+ fun getLoginExceptionsList(): List<LoginExceptionEntity>
+
+ @Query("DELETE FROM logins_exceptions")
+ fun deleteAllLoginExceptions()
+
+ @Query("SELECT * FROM logins_exceptions WHERE origin = :origin")
+ fun findExceptionByOrigin(origin: String): LoginExceptionEntity?
+
+ @Transaction
+ @Query("SELECT * FROM logins_exceptions")
+ fun getLoginExceptionsPaged(): DataSource.Factory<Int, LoginExceptionEntity>
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt
new file mode 100644
index 0000000000..e1125aa44f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt
@@ -0,0 +1,36 @@
+/* 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.logins.exceptions.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+/**
+ * Internal database for storing login exceptions.
+ */
+@Database(entities = [LoginExceptionEntity::class], version = 1)
+internal abstract class LoginExceptionDatabase : RoomDatabase() {
+ abstract fun loginExceptionDao(): LoginExceptionDao
+
+ companion object {
+ @Volatile
+ private var instance: LoginExceptionDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): LoginExceptionDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ LoginExceptionDatabase::class.java,
+ "login_exceptions",
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt
new file mode 100644
index 0000000000..fee109f835
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt
@@ -0,0 +1,22 @@
+/* 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.logins.exceptions.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * Internal entity representing a login exception.
+ */
+@Entity(tableName = "logins_exceptions")
+internal data class LoginExceptionEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var id: Long? = null,
+
+ @ColumnInfo(name = "origin")
+ var origin: String,
+)
diff --git a/mobile/android/android-components/components/feature/logins/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/logins/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/logins/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/media/README.md b/mobile/android/android-components/components/feature/media/README.md
new file mode 100644
index 0000000000..fae0df7fe3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/README.md
@@ -0,0 +1,67 @@
+# [Android Components](../../../README.md) > Feature > Media
+
+Feature component for website media related features.
+
+## Usage
+
+Add the push service for controlling the media session:
+
+```kotlin
+class MediaSessionService(
+ override val store: BrowserStore,
+ override val crashReporter: CrashReporting
+) : AbstractMediaSessionService()
+```
+
+Expose the service in the `AndroidManifest.xml`:
+```xml
+<service android:name=".media.MediaSessionService"
+ android:foregroundServiceType="mediaPlayback"
+ android:exported="false" />
+```
+
+The `AbstractMediaSessionService` also requires extra permissions needed to post notification updates on media changes:
+- `android.permission.POST_NOTIFICATIONS`
+- `android.permission.FOREGROUND_SERVICE`
+- `android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK`
+
+### 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-media:{latest-version}"
+```
+
+### Notification: Recording devices
+
+`RecordingDevicesMiddleware` can be used to show an ongoing notification when a recording device (camera,
+microphone) is used by web content. Notifications will be shown in the "Media" notification channel.
+
+This feature should only be initialized once globally:
+
+```kotlin
+BrowserStore(
+ middleware = listOf(
+ RecordingDevicesMiddleware(applicationContext, notificationsDelegate)
+ )
+)
+```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Description |
+|--------|-----------------|-------------------------------------------|
+| PLAY | state | Media started playing. |
+| PAUSE | state | Media playback was paused. |
+| STOP | state | Media playback has ended. |
+| PLAY | notification | Play action of notification was invoked |
+| PAUSE | notification | Pause action of notification was invoked |
+
+## 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/media/build.gradle b/mobile/android/android-components/components/feature/media/build.gradle
new file mode 100644
index 0000000000..650288778f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/build.gradle
@@ -0,0 +1,62 @@
+/* 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'
+
+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.media'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':browser-state')
+ implementation project(':ui-icons')
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_media
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.kotlin_reflect
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/media/proguard-rules.pro b/mobile/android/android-components/components/feature/media/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/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/media/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/media/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..ba25f83356
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+ <application />
+
+</manifest>
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/MediaSessionFeature.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/MediaSessionFeature.kt
new file mode 100644
index 0000000000..dcc4013fda
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/MediaSessionFeature.kt
@@ -0,0 +1,108 @@
+/* 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.media
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState.PAUSED
+import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState.PLAYING
+import mozilla.components.feature.media.ext.findActiveMediaTab
+import mozilla.components.feature.media.service.MediaServiceBinder
+import mozilla.components.feature.media.service.MediaSessionDelegate
+import mozilla.components.lib.state.ext.flowScoped
+
+/**
+ * Feature implementation that handles MediaSession state changes and controls showing a notification
+ * reflecting the media states.
+ *
+ * @param applicationContext the application's [Context].
+ * @param mediaServiceClass the media service class will handle the media playback state
+ * @param store Reference to the browser store where tab state is located.
+ */
+class MediaSessionFeature(
+ val applicationContext: Context,
+ val mediaServiceClass: Class<*>,
+ val store: BrowserStore,
+) {
+ @VisibleForTesting
+ internal var scope: CoroutineScope? = null
+
+ @VisibleForTesting
+ internal var mediaService: MediaSessionDelegate? = null
+
+ @VisibleForTesting
+ internal val mediaServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName, binder: IBinder) {
+ val serviceBinder = binder as MediaServiceBinder
+ mediaService = serviceBinder.getMediaService()
+ // The service is bound when media starts playing. Ensure in-progress media status is correctly shown.
+ store.state.findActiveMediaTab()?.let {
+ showMediaStatus(it)
+ }
+ }
+
+ override fun onServiceDisconnected(className: ComponentName?) {
+ mediaService = null
+ }
+ }
+
+ /**
+ * Starts the feature.
+ */
+ fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map { state -> state.findActiveMediaTab() }
+ .distinctUntilChangedBy { tab -> tab?.mediaSessionState }
+ .collect { state -> showMediaStatus(state) }
+ }
+ }
+
+ /**
+ * Stops the feature.
+ */
+ fun stop() {
+ scope?.cancel()
+ scope = null
+ }
+
+ @VisibleForTesting
+ internal fun showMediaStatus(sessionState: SessionState?) {
+ if (sessionState == null) {
+ mediaService?.let {
+ it.handleNoMedia()
+ applicationContext.unbindService(mediaServiceConnection)
+ mediaService = null
+ }
+
+ return
+ }
+
+ when (sessionState.mediaSessionState?.playbackState) {
+ PLAYING -> {
+ if (mediaService == null) {
+ applicationContext.bindService(
+ Intent(applicationContext, mediaServiceClass),
+ mediaServiceConnection,
+ Context.BIND_AUTO_CREATE,
+ )
+ }
+
+ mediaService?.handleMediaPlaying(sessionState)
+ }
+ PAUSED -> mediaService?.handleMediaPaused(sessionState)
+ else -> mediaService?.handleMediaStopped(sessionState)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/ext/MediaSessionState.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/ext/MediaSessionState.kt
new file mode 100644
index 0000000000..fe0825a648
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/ext/MediaSessionState.kt
@@ -0,0 +1,47 @@
+/* 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.media.ext
+
+import android.support.v4.media.session.PlaybackStateCompat
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.concept.engine.mediasession.MediaSession
+
+/**
+ * Turns the [MediaSessionState] into a [PlaybackStateCompat] to be used with a `MediaSession`.
+ */
+internal fun MediaSessionState.toPlaybackState() =
+ PlaybackStateCompat.Builder()
+ .setActions(
+ PlaybackStateCompat.ACTION_PLAY_PAUSE or
+ PlaybackStateCompat.ACTION_PLAY or
+ PlaybackStateCompat.ACTION_PAUSE,
+ )
+ .setState(
+ when (playbackState) {
+ MediaSession.PlaybackState.PLAYING -> PlaybackStateCompat.STATE_PLAYING
+ MediaSession.PlaybackState.PAUSED -> PlaybackStateCompat.STATE_PAUSED
+ else -> PlaybackStateCompat.STATE_NONE
+ },
+ // Time state not exposed yet:
+ // https://github.com/mozilla-mobile/android-components/issues/2458
+ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN,
+ when (playbackState) {
+ // The actual playback speed is not exposed yet:
+ // https://github.com/mozilla-mobile/android-components/issues/2459
+ MediaSession.PlaybackState.PLAYING -> 1.0f
+ else -> 0.0f
+ },
+ )
+ .build()
+
+/**
+ * If this state is [MediaSession.PlaybackState.PLAYING] then return true, else return false.
+ */
+fun MediaSessionState.playing(): Boolean {
+ return when (playbackState) {
+ MediaSession.PlaybackState.PLAYING -> true
+ else -> false
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/ext/SessionState.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/ext/SessionState.kt
new file mode 100644
index 0000000000..4ee0099417
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/ext/SessionState.kt
@@ -0,0 +1,49 @@
+/* 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.media.ext
+
+import android.content.Context
+import android.graphics.Bitmap
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.feature.media.R
+
+internal fun SessionState?.getTitleOrUrl(context: Context, title: String? = null): String = when {
+ this == null -> context.getString(R.string.mozac_feature_media_notification_private_mode)
+ content.private -> context.getString(R.string.mozac_feature_media_notification_private_mode)
+ title != null -> title
+ content.title.isNotEmpty() -> content.title
+ else -> content.url
+}
+
+internal fun SessionState?.getArtistOrUrl(artist: String? = null): String = when {
+ this == null || content.private -> ""
+ artist != null -> artist
+ else -> content.url
+}
+
+@Suppress("TooGenericExceptionCaught")
+internal suspend fun SessionState?.getNonPrivateIcon(
+ getArtwork: (suspend () -> Bitmap?)?,
+): Bitmap? = when {
+ this == null -> null
+ content.private -> null
+ getArtwork != null -> getArtwork() ?: content.icon
+ else -> content.icon
+}
+
+/**
+ * Finds the [SessionState] (tab or custom tab) that has an active media session. Returns `null` if
+ * no tab has a media session attached.
+ */
+fun BrowserState.findActiveMediaTab(): SessionState? {
+ return (tabs.asSequence() + customTabs.asSequence()).filter { tab ->
+ tab.mediaSessionState != null &&
+ tab.mediaSessionState!!.playbackState != MediaSession.PlaybackState.UNKNOWN
+ }.sortedByDescending { tab ->
+ tab.mediaSessionState
+ }.firstOrNull()
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/facts/MediaFacts.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/facts/MediaFacts.kt
new file mode 100644
index 0000000000..f70253d9d6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/facts/MediaFacts.kt
@@ -0,0 +1,50 @@
+/* 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.media.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [MediaFeature]
+ */
+class MediaFacts {
+ /**
+ * Items that specify which portion of the [MediaFeature] was interacted with
+ */
+ object Items {
+ const val NOTIFICATION = "notification"
+ const val STATE = "state"
+ }
+}
+
+internal fun emitNotificationPlayFact() = emitNotificationFact(Action.PLAY)
+internal fun emitNotificationPauseFact() = emitNotificationFact(Action.PAUSE)
+
+internal fun emitStatePlayFact() = emitStateFact(Action.PLAY)
+internal fun emitStatePauseFact() = emitStateFact(Action.PAUSE)
+internal fun emitStateStopFact() = emitStateFact(Action.STOP)
+
+private fun emitStateFact(
+ action: Action,
+) {
+ Fact(
+ Component.FEATURE_MEDIA,
+ action,
+ MediaFacts.Items.STATE,
+ ).collect()
+}
+
+private fun emitNotificationFact(
+ action: Action,
+) {
+ Fact(
+ Component.FEATURE_MEDIA,
+ action,
+ MediaFacts.Items.NOTIFICATION,
+ ).collect()
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocus.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocus.kt
new file mode 100644
index 0000000000..cb232d8909
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocus.kt
@@ -0,0 +1,116 @@
+/* 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.media.focus
+
+import android.media.AudioManager
+import android.os.Build
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Class responsible for request audio focus and reacting to audio focus changes.
+ *
+ * https://developer.android.com/guide/topics/media-apps/audio-focus
+ */
+internal class AudioFocus(
+ audioManager: AudioManager,
+ val store: BrowserStore,
+) : AudioManager.OnAudioFocusChangeListener {
+ private val logger = Logger("AudioFocus")
+ private var playDelayed = false
+ private var resumeOnFocusGain = false
+ private var sessionId: String? = null
+
+ private val audioFocusController = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ AudioFocusControllerV26(audioManager, this)
+ } else {
+ AudioFocusControllerV21(audioManager, this)
+ }
+
+ @Synchronized
+ fun request(tabId: String?) {
+ sessionId = tabId
+ val result = audioFocusController.request()
+ processAudioFocusResult(result)
+ }
+
+ @Synchronized
+ fun abandon() {
+ audioFocusController.abandon()
+ sessionId = null
+ playDelayed = false
+ resumeOnFocusGain = false
+ }
+
+ private fun processAudioFocusResult(result: Int) {
+ logger.debug("processAudioFocusResult($result)")
+ val sessionState = sessionId?.let {
+ store.state.findTabOrCustomTab(it)
+ }
+
+ when (result) {
+ AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
+ // Granted: Gecko already started playing media.
+ playDelayed = false
+ resumeOnFocusGain = false
+ }
+ AudioManager.AUDIOFOCUS_REQUEST_FAILED -> {
+ // Failed: Pause media since we didn't get audio focus.
+ sessionState?.mediaSessionState?.controller?.pause()
+ playDelayed = false
+ resumeOnFocusGain = false
+ }
+ AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
+ // Delayed: Pause media until we gain focus via callback
+ sessionState?.mediaSessionState?.controller?.pause()
+ playDelayed = true
+ resumeOnFocusGain = false
+ }
+ else -> throw IllegalStateException("Unknown audio focus request response: $result")
+ }
+ }
+
+ @Synchronized
+ @Suppress("ComplexMethod")
+ override fun onAudioFocusChange(focusChange: Int) {
+ logger.debug("onAudioFocusChange($focusChange)")
+ val sessionState = sessionId?.let {
+ store.state.findTabOrCustomTab(it)
+ }
+
+ when (focusChange) {
+ AudioManager.AUDIOFOCUS_GAIN -> {
+ if (playDelayed || resumeOnFocusGain) {
+ sessionState?.mediaSessionState?.controller?.play()
+ playDelayed = false
+ resumeOnFocusGain = false
+ }
+ }
+
+ AudioManager.AUDIOFOCUS_LOSS -> {
+ sessionState?.mediaSessionState?.controller?.pause()
+ resumeOnFocusGain = false
+ playDelayed = false
+ }
+
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
+ sessionState?.mediaSessionState?.controller?.pause()
+ resumeOnFocusGain = sessionState?.mediaSessionState?.playbackState == MediaSession.PlaybackState.PLAYING
+
+ playDelayed = false
+ }
+
+ else -> {
+ logger.debug("Unhandled focus change: $focusChange")
+ }
+
+ // We do not handle any ducking related focus change here. On API 26+ the system should
+ // duck and restore the volume automatically
+ // https://github.com/mozilla-mobile/android-components/issues/3936
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusController.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusController.kt
new file mode 100644
index 0000000000..d8da73b916
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusController.kt
@@ -0,0 +1,13 @@
+/* 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.media.focus
+
+/**
+ * A controller that knows how to request and abandon audio focus.
+ */
+internal interface AudioFocusController {
+ fun request(): Int
+ fun abandon()
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusControllerV21.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusControllerV21.kt
new file mode 100644
index 0000000000..b409dd9bb4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusControllerV21.kt
@@ -0,0 +1,28 @@
+/* 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.media.focus
+
+import android.media.AudioManager
+
+/**
+ * [AudioFocusController] implementation for Android API 21+.
+ */
+@Suppress("DEPRECATION")
+internal class AudioFocusControllerV21(
+ private val audioManager: AudioManager,
+ private val listener: AudioManager.OnAudioFocusChangeListener,
+) : AudioFocusController {
+ override fun request(): Int {
+ return audioManager.requestAudioFocus(
+ listener,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN,
+ )
+ }
+
+ override fun abandon() {
+ audioManager.abandonAudioFocus(listener)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusControllerV26.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusControllerV26.kt
new file mode 100644
index 0000000000..814cfb0857
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/focus/AudioFocusControllerV26.kt
@@ -0,0 +1,39 @@
+/* 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.media.focus
+
+import android.annotation.TargetApi
+import android.media.AudioAttributes
+import android.media.AudioFocusRequest
+import android.media.AudioManager
+import android.os.Build
+
+/**
+ * [AudioFocusController] implementation for Android API 26+.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+internal class AudioFocusControllerV26(
+ private val audioManager: AudioManager,
+ listener: AudioManager.OnAudioFocusChangeListener,
+) : AudioFocusController {
+ private val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+ .setAudioAttributes(
+ AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .build(),
+ )
+ .setWillPauseWhenDucked(false)
+ .setOnAudioFocusChangeListener(listener)
+ .build()
+
+ override fun request(): Int {
+ return audioManager.requestAudioFocus(request)
+ }
+
+ override fun abandon() {
+ audioManager.abandonAudioFocusRequest(request)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeature.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeature.kt
new file mode 100644
index 0000000000..01bff9dfb5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeature.kt
@@ -0,0 +1,99 @@
+/* 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.media.fullscreen
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import android.os.Build
+import android.view.WindowManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Feature that will auto-rotate the device to the correct orientation for the media aspect ratio.
+ */
+class MediaSessionFullscreenFeature(
+ private val activity: Activity,
+ private val store: BrowserStore,
+ private val tabId: String?,
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map {
+ it.tabs + it.customTabs
+ }.map { tab ->
+ tab.firstOrNull { it.mediaSessionState?.fullscreen == true }
+ }.distinctUntilChanged { old, new ->
+ old.hasSameOrientationInformationAs(new)
+ }.collect { state ->
+ // There should only be one fullscreen session.
+ if (state == null) {
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ activity.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ return@collect
+ }
+
+ if (store.state.findCustomTabOrSelectedTab(tabId)?.id == state.id) {
+ setOrientationForTabState(state)
+ }
+ setDeviceSleepModeForTabState(state)
+ }
+ }
+ }
+
+ @Suppress("SourceLockedOrientationActivity") // We deliberately want to lock the orientation here.
+ private fun setOrientationForTabState(activeTabState: SessionState) {
+ when (activeTabState.mediaSessionState?.elementMetadata?.portrait) {
+ true ->
+ activity.requestedOrientation =
+ ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+
+ false ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInPictureInPictureMode) {
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ } else {
+ activity.requestedOrientation =
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+ }
+
+ null -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ }
+ }
+
+ private fun setDeviceSleepModeForTabState(activeTabState: SessionState) {
+ activeTabState.mediaSessionState?.let {
+ when (activeTabState.mediaSessionState?.playbackState) {
+ MediaSession.PlaybackState.PLAYING -> {
+ activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+
+ else -> {
+ activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+ }
+ }
+ }
+
+ private fun SessionState?.hasSameOrientationInformationAs(other: SessionState?): Boolean =
+ this?.mediaSessionState?.fullscreen == other?.mediaSessionState?.fullscreen &&
+ this?.mediaSessionState?.playbackState == other?.mediaSessionState?.playbackState &&
+ this?.mediaSessionState?.elementMetadata == other?.mediaSessionState?.elementMetadata &&
+ this?.content?.pictureInPictureEnabled == other?.content?.pictureInPictureEnabled
+
+ override fun stop() {
+ scope?.cancel()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/middleware/LastMediaAccessMiddleware.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/middleware/LastMediaAccessMiddleware.kt
new file mode 100644
index 0000000000..16eda03bde
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/middleware/LastMediaAccessMiddleware.kt
@@ -0,0 +1,38 @@
+/* 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.media.middleware
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.LastAccessAction
+import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] that updates [TabSessionState.lastMediaAccessState] everytime the user starts playing media or
+ * the [MediaSession] gets deactivated as when the user navigates to other URL or starts playing media
+ * in another tab.
+ */
+class LastMediaAccessMiddleware : Middleware<BrowserState, BrowserAction> {
+ @Suppress("ComplexCondition")
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ next(action)
+
+ if (action is MediaSessionAction.UpdateMediaPlaybackStateAction &&
+ action.playbackState == MediaSession.PlaybackState.PLAYING
+ ) {
+ context.dispatch(LastAccessAction.UpdateLastMediaAccessAction(action.tabId))
+ } else if (action is MediaSessionAction.DeactivatedMediaSessionAction) {
+ context.dispatch(LastAccessAction.ResetLastMediaSessionAction(action.tabId))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/middleware/RecordingDevicesMiddleware.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/middleware/RecordingDevicesMiddleware.kt
new file mode 100644
index 0000000000..67b7922b24
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/middleware/RecordingDevicesMiddleware.kt
@@ -0,0 +1,253 @@
+/* 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.media.middleware
+
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Binder
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.concept.engine.media.RecordingDevice
+import mozilla.components.feature.media.R
+import mozilla.components.feature.media.notification.MediaNotificationChannel
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.android.OnPermissionGranted
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.utils.PendingIntentUtils
+import mozilla.components.support.utils.ThreadUtils
+import mozilla.components.support.utils.ext.registerReceiverCompat
+import mozilla.components.ui.icons.R as iconsR
+
+private const val NOTIFICATION_TAG = "mozac.feature.media.recordingDevices"
+private const val NOTIFICATION_ID = 1
+private const val PENDING_INTENT_TAG = "mozac.feature.media.pendingintent"
+private const val ACTION_RECORDING_DEVICES_NOTIFICATION_DISMISSED =
+ "mozac.feature.media.recordingDevices.notificationDismissed"
+private const val NOTIFICATION_REMINDER_DELAY_MS = 5 * 60 * 1000L // 5 minutes
+
+/**
+ * Middleware for displaying an ongoing notification while recording devices (camera, microphone)
+ * are used by web content.
+ */
+class RecordingDevicesMiddleware(
+ private val context: Context,
+ private val notificationsDelegate: NotificationsDelegate,
+) : Middleware<BrowserState, BrowserAction> {
+ private var isShowingNotification: Boolean = false
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ next(action)
+
+ // Whenever the recording devices of a tab change or tabs get added/removed then process
+ // the current list and show/hide the notification.
+ if (
+ action is ContentAction.SetRecordingDevices ||
+ action is TabListAction ||
+ action is CustomTabListAction
+ ) {
+ process(context, false)
+ }
+ }
+
+ private fun process(
+ middlewareContext: MiddlewareContext<BrowserState, BrowserAction>,
+ isReminder: Boolean,
+ ) {
+ val devices = middlewareContext.state.tabs
+ .map { tab -> tab.content.recordingDevices }
+ .flatten()
+ .filter { device -> device.status == RecordingDevice.Status.RECORDING }
+ .distinctBy { device -> device.type }
+
+ val isUsingCamera = devices.find { it.type == RecordingDevice.Type.CAMERA } != null
+ val isUsingMicrophone = devices.find { it.type == RecordingDevice.Type.MICROPHONE } != null
+
+ val recordingState = when {
+ isUsingCamera && isUsingMicrophone -> RecordingState.CameraAndMicrophone
+ isUsingCamera -> RecordingState.Camera
+ isUsingMicrophone -> RecordingState.Microphone
+ else -> RecordingState.None
+ }
+
+ updateNotification(
+ recordingState,
+ isReminder,
+ processRecordingState = {
+ isShowingNotification = false
+ process(middlewareContext, true)
+ },
+ )
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun updateNotification(
+ recordingState: RecordingState,
+ isReminder: Boolean = false,
+ processRecordingState: () -> Unit = {},
+ ) {
+ if (recordingState.isRecording && !isShowingNotification) {
+ showNotification(
+ context,
+ recordingState,
+ notificationsDelegate,
+ isReminder,
+ processRecordingState,
+ ) {
+ isShowingNotification = true
+ }
+ } else if (!recordingState.isRecording && isShowingNotification) {
+ hideNotification()
+ isShowingNotification = false
+ }
+ }
+
+ private fun hideNotification() {
+ NotificationManagerCompat.from(context)
+ .cancel(NOTIFICATION_TAG, NOTIFICATION_ID)
+ }
+
+ private fun showNotification(
+ context: Context,
+ recordingState: RecordingState,
+ notificationsDelegate: NotificationsDelegate,
+ isReminder: Boolean = false,
+ processRecordingState: () -> Unit,
+ onPermissionGranted: OnPermissionGranted,
+ ) {
+ val channelId = MediaNotificationChannel.ensureChannelExists(context)
+
+ val intent =
+ context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ } ?: throw IllegalStateException("Package has no launcher intent")
+
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ SharedIdsHelper.getIdForTag(context, PENDING_INTENT_TAG),
+ intent,
+ PendingIntentUtils.defaultFlags or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+
+ val dismissPendingIntent = PendingIntent.getBroadcast(
+ context,
+ 0,
+ Intent(ACTION_RECORDING_DEVICES_NOTIFICATION_DISMISSED),
+ PendingIntentUtils.defaultFlags,
+ )
+
+ val broadcastReceiver = NotificationDismissedReceiver(processRecordingState)
+
+ context.registerReceiverCompat(
+ broadcastReceiver,
+ IntentFilter(ACTION_RECORDING_DEVICES_NOTIFICATION_DISMISSED),
+ ContextCompat.RECEIVER_EXPORTED,
+ )
+
+ val textResource = if (isReminder) {
+ context.getString(
+ recordingState.reminderTextResource,
+ context.packageManager.getApplicationLabel(context.applicationInfo).toString(),
+ )
+ } else {
+ context.getString(recordingState.textResource)
+ }
+
+ val notification = NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(recordingState.iconResource)
+ .setContentTitle(context.getString(recordingState.titleResource))
+ .setContentText(textResource)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .setDeleteIntent(dismissPendingIntent)
+ .build()
+
+ notificationsDelegate.notify(
+ NOTIFICATION_TAG,
+ NOTIFICATION_ID,
+ notification,
+ onPermissionGranted = onPermissionGranted,
+ )
+ }
+
+ internal class NotificationDismissedReceiver(
+ private val processRecordingState: () -> Unit,
+ ) : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val callingAppInfo = context.packageManager.getNameForUid(Binder.getCallingUid())
+ if (callingAppInfo.equals(context.packageName)) {
+ ThreadUtils.postToMainThreadDelayed(
+ {
+ processRecordingState.invoke()
+ },
+ NOTIFICATION_REMINDER_DELAY_MS,
+ )
+ }
+ }
+ }
+}
+
+internal sealed class RecordingState {
+ abstract val iconResource: Int
+ abstract val titleResource: Int
+ abstract val textResource: Int
+ abstract val reminderTextResource: Int
+
+ val isRecording
+ get() = this !is None
+
+ object CameraAndMicrophone : RecordingState() {
+ override val iconResource = iconsR.drawable.mozac_ic_camera_24
+ override val titleResource = R.string.mozac_feature_media_sharing_camera_and_microphone
+ override val textResource = R.string.mozac_feature_media_sharing_camera_and_microphone_text
+ override val reminderTextResource =
+ R.string.mozac_feature_media_sharing_camera_and_microphone_reminder_text_2
+ }
+
+ object Camera : RecordingState() {
+ override val iconResource = iconsR.drawable.mozac_ic_camera_24
+ override val titleResource = R.string.mozac_feature_media_sharing_camera
+ override val textResource = R.string.mozac_feature_media_sharing_camera_text
+ override val reminderTextResource =
+ R.string.mozac_feature_media_sharing_camera_reminder_text
+ }
+
+ object Microphone : RecordingState() {
+ override val iconResource = iconsR.drawable.mozac_ic_microphone_24
+ override val titleResource = R.string.mozac_feature_media_sharing_microphone
+ override val textResource = R.string.mozac_feature_media_sharing_microphone_text
+ override val reminderTextResource =
+ R.string.mozac_feature_media_sharing_microphone_reminder_text_2
+ }
+
+ object None : RecordingState() {
+ override val iconResource: Int
+ get() = throw UnsupportedOperationException()
+
+ override val titleResource: Int
+ get() = throw UnsupportedOperationException()
+ override val textResource: Int
+ get() = throw UnsupportedOperationException()
+ override val reminderTextResource: Int
+ get() = throw UnsupportedOperationException()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/notification/MediaNotification.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/notification/MediaNotification.kt
new file mode 100644
index 0000000000..9f678df609
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/notification/MediaNotification.kt
@@ -0,0 +1,155 @@
+/* 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.media.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.content.Context
+import android.graphics.Bitmap
+import android.os.Build
+import android.support.v4.media.session.MediaSessionCompat
+import androidx.annotation.DrawableRes
+import androidx.core.app.NotificationCompat
+import androidx.media.app.NotificationCompat.MediaStyle
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.feature.media.R
+import mozilla.components.feature.media.ext.getArtistOrUrl
+import mozilla.components.feature.media.ext.getNonPrivateIcon
+import mozilla.components.feature.media.ext.getTitleOrUrl
+import mozilla.components.feature.media.service.AbstractMediaSessionService
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.utils.PendingIntentUtils
+import java.util.Locale
+
+/**
+ * Helper to display a notification for web content playing media.
+ */
+internal class MediaNotification(
+ private val context: Context,
+ private val cls: Class<*>,
+) {
+ /**
+ * Creates a new [Notification] for the given [sessionState].
+ */
+ suspend fun create(sessionState: SessionState?, mediaSessionCompat: MediaSessionCompat): Notification {
+ val data = sessionState?.toNotificationData(context, cls) ?: NotificationData()
+
+ return buildNotification(data, mediaSessionCompat, sessionState !is CustomTabSessionState)
+ }
+
+ private fun buildNotification(
+ data: NotificationData,
+ mediaSession: MediaSessionCompat,
+ isCustomTab: Boolean,
+ ): Notification {
+ val channel = MediaNotificationChannel.ensureChannelExists(context)
+ val style = MediaStyle().setMediaSession(mediaSession.sessionToken)
+ val builder = NotificationCompat.Builder(context, channel)
+ .setSmallIcon(data.icon)
+ .setContentTitle(data.title)
+ .setContentText(data.description)
+ .setLargeIcon(data.largeIcon)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+
+ if (data.action != null) {
+ builder.addAction(data.action)
+ style.setShowActionsInCompactView(0)
+ }
+
+ // There is a known OEM crash with Huawei Devices on lollipop with setting a style
+ // see https://github.com/mozilla-mobile/android-components/issues/7468 and
+ // https://issuetracker.google.com/issues/37078372
+ val huaweiOnLollipop =
+ Build.MANUFACTURER.lowercase(Locale.getDefault()).contains("huawei") &&
+ Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1
+ if (!huaweiOnLollipop) {
+ builder.setStyle(style)
+ }
+
+ if (isCustomTab) {
+ // We only set a content intent if this media notification is not for an "external app"
+ // like a custom tab. Currently we can't route the user to that particular activity:
+ // https://github.com/mozilla-mobile/android-components/issues/3986
+ builder.setContentIntent(data.contentIntent)
+ }
+
+ return builder.build()
+ }
+}
+
+private suspend fun SessionState.toNotificationData(
+ context: Context,
+ cls: Class<*>,
+): NotificationData {
+ val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.also {
+ it.action = AbstractMediaSessionService.ACTION_SWITCH_TAB
+ }
+
+ return when (mediaSessionState?.playbackState) {
+ MediaSession.PlaybackState.PLAYING -> NotificationData(
+ title = getTitleOrUrl(context, mediaSessionState?.metadata?.title),
+ description = getArtistOrUrl(mediaSessionState?.metadata?.artist),
+ icon = R.drawable.mozac_feature_media_playing,
+ largeIcon = getNonPrivateIcon(mediaSessionState?.metadata?.getArtwork),
+ action = NotificationCompat.Action.Builder(
+ R.drawable.mozac_feature_media_action_pause,
+ context.getString(R.string.mozac_feature_media_notification_action_pause),
+ PendingIntent.getService(
+ context,
+ 0,
+ AbstractMediaSessionService.pauseIntent(context, cls),
+ getNotificationFlag(),
+ ),
+ ).build(),
+ contentIntent = PendingIntent.getActivity(
+ context,
+ SharedIdsHelper.getIdForTag(context, AbstractMediaSessionService.PENDING_INTENT_TAG),
+ intent?.apply { putExtra(AbstractMediaSessionService.EXTRA_TAB_ID, id) },
+ getUpdateNotificationFlag(),
+ ),
+ )
+ MediaSession.PlaybackState.PAUSED -> NotificationData(
+ title = getTitleOrUrl(context, mediaSessionState?.metadata?.title),
+ description = getArtistOrUrl(mediaSessionState?.metadata?.artist),
+ icon = R.drawable.mozac_feature_media_paused,
+ largeIcon = getNonPrivateIcon(mediaSessionState?.metadata?.getArtwork),
+ action = NotificationCompat.Action.Builder(
+ R.drawable.mozac_feature_media_action_play,
+ context.getString(R.string.mozac_feature_media_notification_action_play),
+ PendingIntent.getService(
+ context,
+ 0,
+ AbstractMediaSessionService.playIntent(context, cls),
+ getNotificationFlag(),
+ ),
+ ).build(),
+ contentIntent = PendingIntent.getActivity(
+ context,
+ SharedIdsHelper.getIdForTag(context, AbstractMediaSessionService.PENDING_INTENT_TAG),
+ intent?.apply { putExtra(AbstractMediaSessionService.EXTRA_TAB_ID, id) },
+ getUpdateNotificationFlag(),
+ ),
+ )
+ // Dummy notification used of all other media states.
+ else -> NotificationData()
+ }
+}
+
+private data class NotificationData(
+ val title: String = "",
+ val description: String = "",
+ @DrawableRes val icon: Int = R.drawable.mozac_feature_media_playing,
+ val largeIcon: Bitmap? = null,
+ val action: NotificationCompat.Action? = null,
+ val contentIntent: PendingIntent? = null,
+)
+
+private fun getNotificationFlag() = PendingIntentUtils.defaultFlags
+
+private fun getUpdateNotificationFlag() = PendingIntentUtils.defaultFlags or FLAG_UPDATE_CURRENT
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/notification/MediaNotificationChannel.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/notification/MediaNotificationChannel.kt
new file mode 100644
index 0000000000..bd07ed2d2a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/notification/MediaNotificationChannel.kt
@@ -0,0 +1,45 @@
+/* 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.media.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import mozilla.components.feature.media.R
+
+private const val NOTIFICATION_CHANNEL_ID = "mozac.feature.media.generic"
+private const val LEGACY_NOTIFICATION_CHANNEL_ID = "Media"
+
+internal object MediaNotificationChannel {
+ /**
+ * Make sure a notification channel for media notification exists.
+ *
+ * Returns the channel id to be used for media notifications.
+ */
+ fun ensureChannelExists(context: Context): String {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager: NotificationManager = context.getSystemService(
+ Context.NOTIFICATION_SERVICE,
+ ) as NotificationManager
+
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.mozac_feature_media_notification_channel),
+ NotificationManager.IMPORTANCE_LOW,
+ )
+ channel.setShowBadge(false)
+ channel.lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
+
+ notificationManager.createNotificationChannel(channel)
+
+ // We can't just change a channel. So we had to re-create the channel with a new name.
+ notificationManager.deleteNotificationChannel(LEGACY_NOTIFICATION_CHANNEL_ID)
+ }
+
+ return NOTIFICATION_CHANNEL_ID
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/AbstractMediaSessionService.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/AbstractMediaSessionService.kt
new file mode 100644
index 0000000000..6299ea039c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/AbstractMediaSessionService.kt
@@ -0,0 +1,103 @@
+/* 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.media.service
+
+import android.app.Service
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.support.base.android.NotificationsDelegate
+import java.lang.ref.WeakReference
+
+/**
+ * [Binder] offering access to this service and all operations it can perform.
+ */
+internal class MediaServiceBinder(delegate: MediaSessionDelegate) : Binder() {
+ // Necessary to prevent the delegate leaking Context when the MediaService is destroyed.
+ @get:VisibleForTesting internal val service = WeakReference(delegate)
+
+ /**
+ * Get an instance of [MediaSessionDelegate] which supports showing/hiding and updating
+ * a media notification based on the passed in [SessionState].
+ */
+ fun getMediaService(): MediaSessionDelegate? = service.get()
+}
+
+/**
+ * A foreground service that will keep the process alive while we are playing media (with the app possibly in the
+ * background) and shows an ongoing notification indicating the current media playing status.
+ */
+abstract class AbstractMediaSessionService : Service() {
+ protected abstract val store: BrowserStore
+ protected abstract val crashReporter: CrashReporting?
+ protected abstract val notificationsDelegate: NotificationsDelegate
+
+ @VisibleForTesting
+ internal var binder: MediaServiceBinder? = null
+
+ @VisibleForTesting
+ internal var delegate: MediaSessionServiceDelegate? = null
+
+ override fun onCreate() {
+ super.onCreate()
+
+ delegate = MediaSessionServiceDelegate(
+ context = this,
+ service = this,
+ store = store,
+ crashReporter = crashReporter,
+ notificationsDelegate = notificationsDelegate,
+ ).also {
+ binder = MediaServiceBinder(it)
+ }
+
+ delegate?.onCreate()
+ }
+
+ override fun onDestroy() {
+ delegate?.onDestroy()
+ binder = null
+ delegate = null
+
+ super.onDestroy()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ delegate?.onStartCommand(intent)
+ return START_NOT_STICKY
+ }
+
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ delegate?.onTaskRemoved()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return binder
+ }
+
+ companion object {
+ internal const val ACTION_PLAY = "mozac.feature.mediasession.service.PLAY"
+ internal const val ACTION_PAUSE = "mozac.feature.mediasession.service.PAUSE"
+
+ const val NOTIFICATION_TAG = "mozac.feature.mediasession.foreground-service"
+ const val PENDING_INTENT_TAG = "mozac.feature.mediasession.pendingintent"
+ const val ACTION_SWITCH_TAB = "mozac.feature.mediasession.SWITCH_TAB"
+ const val EXTRA_TAB_ID = "mozac.feature.mediasession.TAB_ID"
+
+ internal fun playIntent(context: Context, cls: Class<*>): Intent = Intent(ACTION_PLAY).apply {
+ component = ComponentName(context, cls)
+ }
+
+ internal fun pauseIntent(context: Context, cls: Class<*>): Intent = Intent(ACTION_PAUSE).apply {
+ component = ComponentName(context, cls)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionDelegate.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionDelegate.kt
new file mode 100644
index 0000000000..0a26fd4a07
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionDelegate.kt
@@ -0,0 +1,32 @@
+/* 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.media.service
+
+import mozilla.components.browser.state.state.SessionState
+
+/**
+ * A delegate for handling all possible media states of a [SessionState].
+ */
+interface MediaSessionDelegate {
+ /**
+ * Handle media playing in the passed in [sessionState].
+ */
+ fun handleMediaPlaying(sessionState: SessionState)
+
+ /**
+ * Handle media being paused in the passed in [sessionState].
+ */
+ fun handleMediaPaused(sessionState: SessionState)
+
+ /**
+ * Handle media being stopped in the passed in [sessionState].
+ */
+ fun handleMediaStopped(sessionState: SessionState)
+
+ /**
+ * Handle no media available.
+ */
+ fun handleNoMedia()
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionServiceDelegate.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionServiceDelegate.kt
new file mode 100644
index 0000000000..f2e4323f01
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionServiceDelegate.kt
@@ -0,0 +1,298 @@
+/* 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.media.service
+
+import android.app.ForegroundServiceStartNotAllowedException
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioManager
+import android.os.Build
+import android.support.v4.media.MediaMetadataCompat
+import android.support.v4.media.session.MediaSessionCompat
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.feature.media.ext.getArtistOrUrl
+import mozilla.components.feature.media.ext.getNonPrivateIcon
+import mozilla.components.feature.media.ext.getTitleOrUrl
+import mozilla.components.feature.media.ext.toPlaybackState
+import mozilla.components.feature.media.facts.emitNotificationPauseFact
+import mozilla.components.feature.media.facts.emitNotificationPlayFact
+import mozilla.components.feature.media.facts.emitStatePauseFact
+import mozilla.components.feature.media.facts.emitStatePlayFact
+import mozilla.components.feature.media.facts.emitStateStopFact
+import mozilla.components.feature.media.focus.AudioFocus
+import mozilla.components.feature.media.notification.MediaNotification
+import mozilla.components.feature.media.session.MediaSessionCallback
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.ext.registerReceiverCompat
+import mozilla.components.support.utils.ext.stopForegroundCompat
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+@VisibleForTesting
+internal class BecomingNoisyReceiver(private val controller: MediaSession.Controller?) : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) {
+ if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
+ controller?.pause()
+ }
+ }
+
+ @VisibleForTesting
+ fun deviceIsBecomingNoisy(context: Context) {
+ val becomingNoisyIntent = Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
+ onReceive(context, becomingNoisyIntent)
+ }
+}
+
+/**
+ * Delegate handling callbacks from an [AbstractMediaSessionService].
+ *
+ * The implementation was moved from [AbstractMediaSessionService] to this delegate for better testability.
+ */
+internal class MediaSessionServiceDelegate(
+ @get:VisibleForTesting internal var context: Context,
+ @get:VisibleForTesting internal val service: AbstractMediaSessionService,
+ @get:VisibleForTesting internal val store: BrowserStore,
+ @get:VisibleForTesting internal val crashReporter: CrashReporting?,
+ @get:VisibleForTesting internal val notificationsDelegate: NotificationsDelegate,
+) : MediaSessionDelegate {
+ private val logger = Logger("MediaSessionService")
+
+ @VisibleForTesting
+ internal var notificationHelper = MediaNotification(context, service::class.java)
+
+ @VisibleForTesting
+ internal var mediaSession = MediaSessionCompat(context, "MozacMediaSession")
+
+ @VisibleForTesting
+ internal var audioFocus = AudioFocus(context.getSystemService(Context.AUDIO_SERVICE) as AudioManager, store)
+
+ @VisibleForTesting
+ internal val notificationId by lazy {
+ SharedIdsHelper.getIdForTag(context, AbstractMediaSessionService.NOTIFICATION_TAG)
+ }
+
+ @VisibleForTesting
+ internal var controller: MediaSession.Controller? = null
+
+ @VisibleForTesting
+ internal var notificationScope: CoroutineScope? = null
+
+ @VisibleForTesting
+ internal val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
+
+ @VisibleForTesting
+ internal var noisyAudioStreamReceiver: BecomingNoisyReceiver? = null
+
+ @VisibleForTesting
+ internal var isForegroundService: Boolean = false
+
+ fun onCreate() {
+ logger.debug("Service created")
+ mediaSession.setCallback(MediaSessionCallback(store))
+ notificationScope = MainScope()
+ }
+
+ fun onDestroy() {
+ notificationScope?.cancel()
+ notificationScope = null
+ audioFocus.abandon()
+ logger.debug("Service destroyed")
+ }
+
+ fun onStartCommand(intent: Intent?) {
+ logger.debug("Command received: ${intent?.action}")
+
+ when (intent?.action) {
+ AbstractMediaSessionService.ACTION_PLAY -> {
+ controller?.play()
+ emitNotificationPlayFact()
+ }
+ AbstractMediaSessionService.ACTION_PAUSE -> {
+ controller?.pause()
+ emitNotificationPauseFact()
+ }
+ else -> logger.debug("Can't process action: ${intent?.action}")
+ }
+ }
+
+ fun onTaskRemoved() {
+ /* no need to do this for custom tabs */
+ store.state.tabs.forEach {
+ it.mediaSessionState?.controller?.stop()
+ }
+
+ shutdown()
+ }
+
+ override fun handleMediaPlaying(sessionState: SessionState) {
+ emitStatePlayFact()
+
+ updateMediaSession(sessionState)
+ registerBecomingNoisyListenerIfNeeded(sessionState)
+ audioFocus.request(sessionState.id)
+ controller = sessionState.mediaSessionState?.controller
+
+ if (isForegroundService) {
+ updateNotification(sessionState)
+ } else {
+ startForeground(sessionState)
+ }
+ }
+
+ override fun handleMediaPaused(sessionState: SessionState) {
+ emitStatePauseFact()
+
+ updateMediaSession(sessionState)
+ unregisterBecomingNoisyListenerIfNeeded()
+ stopForeground()
+
+ updateNotification(sessionState)
+ }
+
+ override fun handleMediaStopped(sessionState: SessionState) {
+ emitStateStopFact()
+
+ updateMediaSession(sessionState)
+ unregisterBecomingNoisyListenerIfNeeded()
+ stopForeground()
+
+ updateNotification(sessionState)
+ }
+
+ override fun handleNoMedia() {
+ shutdown()
+ }
+
+ @VisibleForTesting
+ internal fun updateNotification(sessionState: SessionState) {
+ notificationScope?.launch {
+ val notification = notificationHelper.create(sessionState, mediaSession)
+ notificationsDelegate.notify(
+ notificationId = notificationId,
+ notification = notification,
+ )
+ }
+ }
+
+ @VisibleForTesting
+ @Suppress("TooGenericExceptionCaught")
+ internal fun startForeground(
+ sessionState: SessionState,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ ) {
+ notificationScope?.launch(coroutineContext) {
+ val notification = notificationHelper.create(sessionState, mediaSession)
+ try {
+ service.startForeground(notificationId, notification)
+ } catch (e: Exception) {
+ if (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+ e is ForegroundServiceStartNotAllowedException
+ ) {
+ // We should not encounter this exception if `android:foregroundServiceType="mediaPlayback"`
+ // is added to the service. The crash reporter loses the stack trace for this
+ // exception so we want to be able to track this crash independently to ensure
+ // this case is fixed and be able to determine if there are other cases where we
+ // might be trying to start foreground services from the background.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1802620
+ crashReporter?.submitCaughtException(e)
+ } else {
+ throw e
+ }
+ }
+
+ isForegroundService = true
+ }
+ }
+
+ @VisibleForTesting
+ internal fun updateMediaSession(sessionState: SessionState) {
+ mediaSession.setPlaybackState(sessionState.mediaSessionState?.toPlaybackState())
+ mediaSession.isActive = true
+ notificationScope?.launch {
+ mediaSession.setMetadata(
+ MediaMetadataCompat.Builder()
+ .putString(
+ MediaMetadataCompat.METADATA_KEY_TITLE,
+ sessionState.getTitleOrUrl(context, sessionState.mediaSessionState?.metadata?.title),
+ )
+ .putString(
+ MediaMetadataCompat.METADATA_KEY_ARTIST,
+ sessionState.getArtistOrUrl(sessionState.mediaSessionState?.metadata?.artist),
+ )
+ .putBitmap(
+ MediaMetadataCompat.METADATA_KEY_ART,
+ sessionState.getNonPrivateIcon(sessionState.mediaSessionState?.metadata?.getArtwork),
+ )
+ .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1)
+ .build(),
+ )
+ }
+ }
+
+ @VisibleForTesting
+ internal fun stopForeground() {
+ service.stopForegroundCompat(false)
+ isForegroundService = false
+ }
+
+ @VisibleForTesting
+ internal fun registerBecomingNoisyListenerIfNeeded(state: SessionState) {
+ if (noisyAudioStreamReceiver != null) {
+ return
+ }
+
+ noisyAudioStreamReceiver = BecomingNoisyReceiver(state.mediaSessionState?.controller)
+ noisyAudioStreamReceiver?.let {
+ registerBecomingNoisyListener(it)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun registerBecomingNoisyListener(broadcastReceiver: BroadcastReceiver) {
+ context.registerReceiverCompat(
+ broadcastReceiver,
+ intentFilter,
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun unregisterBecomingNoisyListenerIfNeeded() {
+ noisyAudioStreamReceiver?.let {
+ context.unregisterReceiver(noisyAudioStreamReceiver)
+ noisyAudioStreamReceiver = null
+ }
+ }
+
+ @VisibleForTesting
+ internal fun shutdown() {
+ mediaSession.release()
+ // Explicitly cancel media notification.
+ // Otherwise, when media is paused, with [STOP_FOREGROUND_DETACH] notification behavior,
+ // the notification will persist even after service is stopped and destroyed.
+ notificationsDelegate.notificationManagerCompat.cancel(notificationId)
+ unregisterBecomingNoisyListenerIfNeeded()
+ service.stopSelf()
+ }
+
+ @VisibleForTesting
+ internal fun deviceBecomingNoisy(context: Context) {
+ noisyAudioStreamReceiver?.deviceIsBecomingNoisy(context)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/session/MediaSessionCallback.kt b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/session/MediaSessionCallback.kt
new file mode 100644
index 0000000000..b7cc67d5a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/java/mozilla/components/feature/media/session/MediaSessionCallback.kt
@@ -0,0 +1,28 @@
+/* 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.media.session
+
+import android.support.v4.media.session.MediaSessionCompat
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.media.ext.findActiveMediaTab
+import mozilla.components.support.base.log.logger.Logger
+
+internal class MediaSessionCallback(
+ private val store: BrowserStore,
+) : MediaSessionCompat.Callback() {
+ private val logger = Logger("MediaSessionCallback")
+
+ override fun onPlay() {
+ logger.debug("play()")
+
+ store.state.findActiveMediaTab()?.mediaSessionState?.controller?.play()
+ }
+
+ override fun onPause() {
+ logger.debug("pause()")
+
+ store.state.findActiveMediaTab()?.mediaSessionState?.controller?.pause()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_action_pause.xml b/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_action_pause.xml
new file mode 100644
index 0000000000..b7dd2e228a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_action_pause.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_action_play.xml b/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_action_play.xml
new file mode 100644
index 0000000000..45efe8d2a1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_action_play.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M8,5v14l11,-7z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_paused.xml b/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_paused.xml
new file mode 100644
index 0000000000..c60abeb67f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_paused.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M7,9v6h4l5,5V4l-5,5H7z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_playing.xml b/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_playing.xml
new file mode 100644
index 0000000000..c8bedcd89f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/drawable/mozac_feature_media_playing.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..2a38313c9e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-am/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ሚዲያ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">ካሜራ በርቷል</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ማይክሮፎን በርቷል</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">ካሜራ እና ማይክሮፎን በርተዋል</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">ካሜራዎን የሚጠቀመውን ትር ለመክፈት መታ ያድርጉ።</string>
+
+
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">ማይክሮፎንዎን የሚጠቀመውን ትር ለመክፈት መታ ያድርጉ።</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">የእርስዎን ማይክሮፎን እና ካሜራ የሚጠቀመውን ትር ለመክፈት መታ ያድርጉ።</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">ማስታወሻ፦ %1$s አሁንም ካሜራዎን እየተጠቀመ ነው። ትሩን ለመክፈት መታ ያድርጉ።</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">ማስታወሻ፦ %1$s አሁንም ማይክሮፎንዎን እየተጠቀመ ነው። ትሩን ለመክፈት መታ ያድርጉ</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">ማስታወሻ፦ %1$s አሁንም የእርስዎን ማይክሮፎን እየተጠቀመ ነው። ትሩን ለመክፈት መታ ያድርጉ።</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">ማስታወሻ፦ %1$s አሁንም የእርስዎን ማይክሮፎን እና ካሜራ እየተጠቀመ ነው። ትሩን ለመክፈት መታ ያድርጉ</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">ማስታወሻ፦ %1$s አሁንም የእርስዎን ማይክሮፎን እና ካሜራ እየተጠቀመ ነው። ትሩን ለመክፈት መታ ያድርጉ።</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">አጫውት</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">ባለበት አቁም</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">አንድ ድረ-ገፅ ሚዲያ እየተጫወተ ነው።</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..adb8660128
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-an/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medios</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La camara ye activa</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Lo microfono ye activo</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La camara y lo microfono son activos</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un puesto ye reproducindo mosica</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..74685ebf0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ar/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">الوسائط</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">الكمرة مُشغّلة</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">الميكروفون مُشغّل</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">الكمرة و الميكروفون مُشغّلان</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">شغّل</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">ألبِث</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">أحد المواقع يشغّل الوسائط</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..1a41a186b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ast/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Conteníu multimedia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La cámara ta activada</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">El micrófonu ta activáu</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La cámara ya\'l micrófonu tán activaos</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Posar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sitiu ta reproduciendo conteníu multimedia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..0db58f34e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-az/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera açıqdır</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon açıqdır</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera və mikrofon açıqdır</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Oynat</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Fasilə</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Sayt media oynadır</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..341d39582a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-azb/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">مدیا</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">کامئرا آچیق</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">میکروفون آچیق</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">کامئرا و میکروفون آچیق</string>
+
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">کامئرازدان ایستیفاده ائدن تاغی آچماق اوچون توخونون.</string>
+
+
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">میکروفونوزدان ایستیفاده ائدن تاغی آچماق اوچون توخونون.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">کامئرا و میکروفونوزدان ایستیفاده ائدن تاغی آچماق اوچون توخونون.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">یادا سالما: %1$s هله‌ده کامئرانیزدان ایستیفاده ائدیر. تاغی آچماق اوچون توخونون.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">یادا سالما: %1$s هله‌ده میکروفونوزدان ایستیفاده ائدیر. تاغی آچماق اوچون توخونون.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">یادا سالما: %1$s هله‌ده میکروفونوزدان ایستیفاده ائدیر. تاغی آچماق اوچون توخونون.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">یادا سالما: %1$s هله‌ده کامئرا و میکروفونوزدان ایستیفاده ائدیر. تاغی آچماق اوچون توخونون</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">یادا سالما: %1$s هله‌ده کامئرا و میکروفونوزدان ایستیفاده ائدیر. تاغی آچماق اوچون توخونون.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">اوینات</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">دایاندیر</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">بیر سایت مدیا اوینادیر</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..1a501ddef1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-be/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Медыя</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Камера ўключана</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Мікрафон уключаны</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Камера і мікрафон уключаны</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Прайграць</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Прыпыніць</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Сайт прайгравае медыя</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..b319aa2333
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-bg/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Медия</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Камерата работи</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Микрофонът работи</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Камерата и микрофонът работят</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Докоснете, за да отворите раздела, който използва вашата камера.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Докоснете, за да отворите раздела, който използва вашия микрофон.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Докоснете, за да отворите раздела, който използва вашите микрофон и камера.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Напомняне: %1$s все още използва камерата ви. Докоснете, за да отворите раздела.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Напомняне: %1$s все още използва микрофона ви. Докоснете, за да отворите раздела</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Напомняне: %1$s все още използва микрофона ви. Докоснете, за да отворите раздела.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Напомняне: %1$s все още използва микрофона и камерата ви. Докоснете, за да отворите раздела</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Напомняне: %1$s все още използва микрофона и камерата ви. Докоснете, за да отворите раздела.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Изпълняване</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Пауза</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Сайтът възпроизвежда медия</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..84e460c77a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-bn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">মিডিয়া</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">ক্যামেরা চালু আছে</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">মাইক্রোফোন চালু আছে</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">ক্যামেরা এবং মাইক্রোফোন চালু রয়েছে</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">চালু করুন</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">বিরতি দিন</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">একটি সাইটে মিডিয়া চলছে</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..58beef7ee1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-br/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Gweredekaet eo ar cʼhamera</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Gweredekaet eo ar glevell</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Gweredekaet eo ar cʼhamera hag ar glevell</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Lenn</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Ehan</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Emañ ul lecʼhienn o lenn ur pezh media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..25f1b10a39
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-bs/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Mediji</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera je uključena</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon je uključen</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera i mikrofon su uključeni</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Dodirnite da otvorite tab koji koristi vašu kameru.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Dodirnite da otvorite tab koji koristi vaš mikrofon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Dodirnite da otvorite tab koji koristi vaš mikrofon i kameru.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Podsjetnik: %1$s još uvijek koristi vašu kameru. Dodirnite da otvorite tab.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Podsjetnik: %1$s još uvijek koristi vaš mikrofon. Dodirnite da otvorite tab</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Podsjetnik: %1$s još uvijek koristi vaš mikrofon. Dodirnite da otvorite tab.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Podsjetnik: %1$s još uvijek koristi vaš mikrofon i kameru. Dodirnite da otvorite tab</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Podsjetnik: %1$s još uvijek koristi vaš mikrofon i kameru. Dodirnite da otvorite tab.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Pokreni</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pauza</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Stranica reprodukuje medije</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..dc962103cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ca/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimèdia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La càmera està activada</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">El micròfon està activat</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La càmera i el micròfon estan activats</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toqueu per a obrir la pestanya que usa la càmera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toqueu per a obrir la pestanya que usa el micròfon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toqueu per a obrir la pestanya que usa el micròfon i la càmera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Recordatori: %1$s encara usa la càmera. Toqueu per a obrir la pestanya.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatori: %1$s encara usa el micròfon. Toqueu per a obrir la pestanya</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Recordatori: %1$s encara usa el micròfon. Toqueu per a obrir la pestanya.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatori: %1$s encara usa el micròfon i la càmera. Toqueu per a obrir la pestanya</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Recordatori: %1$s encara usa el micròfon i la càmera. Toqueu per a obrir la pestanya.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reprodueix</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un lloc està reproduint multimèdia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..1fd18fea2c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-cak/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">K\'ïy k\'oxom</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Tzijïl ri elesäy wachib\'äl</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Tzijïl ri q\'asäy ch\'ab\'äl</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">E tzijïl ri elesäy wachib\'äl chuqa\' ri q\'asäy ch\'ab\'äl</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tachapa\' richin najäq ri ruwi\' nrokisaj ri elesäy awachib\'äl.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tachapa\' richin nijaq ri ruwi\' nrokisaj ri q\'asäy ach\'ab\'äl.</string>
+
+
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tachapa\' richin najäq ri ruwi\' nrokisaj ri q\'asäy ach\'ab\'äl chuqa\' ri elesäy awachib\'al.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Runataxik: %1$s tajin nrokisaj na ri elesäy awachib\'al. Tachapa\' richin najäq ri ruwi\'.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Runataxik: %1$s tajin nrokisaj na ri q\'asäy ach\'ab\'äl. Tachapa\' richin najäq ri ruwi\'.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Runataxik: %1$s tajin nrokisaj na ri q\'asäy ach\'ab\'äl. Tachapa\' richin najäq ri ruwi\'.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Runataxik: %1$s tajin nrokisaj na ri q\'asäy ach\'ab\'äl chuqa\' elesäy awachib\'al. Tachapa\' richin najäq ri ruwi\'</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Runataxik: %1$s tajin nrokisaj na ri q\'asäy ach\'ab\'äl chuqa\' elesäy awachib\'al. Tachapa\' richin najäq ri ruwi\'.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Titzij</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Tuxlan</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Jun ruxaq yerutzij k\'ïy taq k\'oxom</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..6e1c048651
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Camera ga-andar</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microphone ga-andar</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Camera ug microphone ga-andar</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Play</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Usa ka-site nagpatukar ug media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..dd39756271
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">میدیا</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">کامێرا کارایە</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">مایکرۆفۆن کارایە</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">کامێرا و مایکرۆفۆن کاران</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">لێدان</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">وچان</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ماڵپەڕی میدیا لێدەدات</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..cffda386cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-co/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimedià</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">L’apparechju-fotò hè attivatu</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">U microfonu hè attivatu</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">L’apparechju-fotò è u microfonu sò attivati</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Picchichjà per apre l’unghjetta chì impiegheghja u vostru apparechju-fotò.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Picchichjà per apre l’unghjetta chì impiegheghja u vostru microfonu.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Picchichjà per apre l’unghjetta chì impiegheghja i vostri microfonu è apparechju-fotò.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Ramentu : %1$s impiegheghja sempre u vostru apparechju-fotò. Picchichjà per apre l’unghjetta.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Ramentu : %1$s impiegheghja sempre u vostru microfonu. Picchichjà per apre l’unghjetta</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Ramentu : %1$s impiegheghja sempre u vostru microfonu. Picchichjà per apre l’unghjetta.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Ramentu : %1$s impiegheghja sempre i vostri microfonu è apparechju-fotò. Picchichjà per apre l’unghjetta</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Ramentu : %1$s impiegheghja sempre i vostri microfonu è apparechju-fotò. Picchichjà per apre l’unghjetta.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Lettura</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un situ leghje un elementu multimedià</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..97c59e828d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-cs/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Média</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera je zapnuta</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon je zapnut</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera a mikrofon jsou zapnuty</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Klepnutím otevřete panel, který používá fotoaparát.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Klepnutím otevřete panel, který používá mikrofon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Klepnutím otevřete panel, který používá váš mikrofon a fotoaparát.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Připomínka: %1$s stále používá váš fotoaparát. Klepnutím otevřete panel.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Připomínka: %1$s stále používá váš mikrofon. Klepnutím otevřete panel</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Připomínka: %1$s stále používá váš mikrofon. Klepnutím otevřete panel.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Připomínka: %1$s stále používá váš mikrofon a kameru. Klepnutím otevřete panel</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Připomínka: %1$s stále používá váš mikrofon a kameru. Klepnutím otevřete panel.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Přehrát</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pozastavit</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Stránka přehrává média</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..f8095ccf66
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-cy/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Cyfrwng</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Camera ymlaen</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microffon ymlaen</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Camera a meicroffon ymlaen</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tapiwch i agor y tab sy\'n defnyddio\'ch camera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tapiwch i agor y tab sy\'n defnyddio\'ch meicroffon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tapiwch i agor y tab sy\'n defnyddio\'ch meicroffon a\'ch camera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Nodyn atgoffa: Mae %1$s yn dal i ddefnyddio\'ch camera. Tapiwch i agor y tab.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Nodyn atgoffa: Mae %1$s yn dal i ddefnyddio\'ch meicroffon. Tapiwch i agor y tab</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Nodyn atgoffa: Mae %1$s yn dal i ddefnyddio\'ch meicroffon. Tapiwch i agor y tab.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Nodyn atgoffa: Mae %1$s yn dal i ddefnyddio\'ch meicroffon a\'ch camera. Tapiwch i agor y tab</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Nodyn atgoffa: Mae %1$s yn dal i ddefnyddio\'ch meicroffon a\'ch camera. Tapiwch i agor y tab.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Chwarae</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Oedi</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Mae gwefan yn chwarae cyfryngau</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..28a62989b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-da/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medieindhold</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kameraet er tændt</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofonen er tændt</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera og mikrofon er tændte</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tryk for at åbne fanebladet, der bruger dit kamera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tryk for at åbne fanebladet, der bruger din mikrofon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tryk for at åbne fanebladet, der bruger din mikrofon og dit kamera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Påmindelse: %1$s bruger stadig dit kamera. Tryk for at åbne fanebladet.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Påmindelse: %1$s bruger stadig din mikrofon. Tryk for at åbne fanebladet.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Påmindelse: %1$s bruger stadig din mikrofon. Tryk for at åbne fanebladet.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Påmindelse: %1$s bruger stadig din mikrofon og dit kamera. Tryk for at åbne fanebladet</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Påmindelse: %1$s bruger stadig din mikrofon og dit kamera. Tryk for at åbne fanebladet.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Afspil</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Et websted afspiller mediefiler</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..6d14a70d33
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-de/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medien</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera ist aktiv</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon ist aktiv</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera und Mikrofon sind aktiv</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Antippen, um den Tab zu öffnen, der Ihre Kamera verwendet.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Antippen, um den Tab zu öffnen, der Ihr Mikrofon verwendet.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Antippen, um den Tab zu öffnen, der Ihr Mikrofon und Ihre Kamera verwendet.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Erinnerung: %1$s verwendet noch Ihre Kamera. Antippen, um den Tab zu öffnen.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Erinnerung: %1$s verwendet noch Ihr Mikrofon. Antippen, um den Tab zu öffnen</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Erinnerung: %1$s verwendet weiterhin Ihr Mikrofon. Antippen, um den Tab zu öffnen.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Erinnerung: %1$s verwendet noch Ihr Mikrofon und Ihre Kamera. Antippen, um den Tab zu öffnen</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Erinnerung: %1$s verwendet weiterhin Ihr Mikrofon und Ihre Kamera. Antippen, um den Tab zu öffnen.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Wiedergeben</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Eine Website spielt Medien ab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..5cc1b64ec4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medije</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera jo zašaltowana</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon jo zašaltowany</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera a mikrofon stej zašaltowanej</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Pótusniśo, aby rejtarik wócynił, kótaryž wašu kameru wužywa. </string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Pótusniśo, aby rejtarik wócynił, kótaryž waš mikrofon wužywa. </string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Pótusniśo, aby rejtarik wócynił, kótaryž waš mikrofon a wašu kameru wužywa. </string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Glědajśo: %1$s wašu kameru južo wužywa. Pótusniśo, aby rejtarik wócynił.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Glědajśo: %1$s waš mikrofon južo wužywa. Pótusniśo, aby rejtarik wócynił</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Glědajśo: %1$s waš mikrofon južo wužywa. Pótusniśo, aby rejtarik wócynił.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Glědajśo: %1$s waš mikrofon a wašu kameru južo wužywa. Pótusniśo, aby rejtarik wócynił</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Glědajśo: %1$s waš mikrofon a wašu kameru južo wužywa. Pótusniśo, aby rejtarik wócynił.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Wótgraś</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Zastajiś</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Sedło medije wótgrawa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..9d9c1eb674
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-el/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Πολυμέσα</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Η κάμερα είναι ενεργή</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Το μικρόφωνο είναι ενεργό</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Η κάμερα και το μικρόφωνο είναι ενεργά</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Πατήστε για άνοιγμα της καρτέλας που χρησιμοποιεί την κάμερά σας.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Πατήστε για άνοιγμα της καρτέλας που χρησιμοποιεί το μικρόφωνό σας.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Πατήστε για άνοιγμα της καρτέλας που χρησιμοποιεί το μικρόφωνο και την κάμερά σας.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Υπενθύμιση: Το %1$s χρησιμοποιεί ακόμα την κάμερά σας. Πατήστε για άνοιγμα της καρτέλας.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Υπενθύμιση: Το %1$s χρησιμοποιεί ακόμα το μικρόφωνό σας. Πατήστε για άνοιγμα της καρτέλας</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Υπενθύμιση: Το %1$s χρησιμοποιεί ακόμα το μικρόφωνό σας. Πατήστε για άνοιγμα της καρτέλας.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Υπενθύμιση: Το %1$s χρησιμοποιεί ακόμα το μικρόφωνο και την κάμερά σας. Πατήστε για άνοιγμα της καρτέλας</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Υπενθύμιση: Το %1$s χρησιμοποιεί ακόμα το μικρόφωνο και την κάμερά σας. Πατήστε για άνοιγμα της καρτέλας.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Αναπαραγωγή</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Παύση</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Ένας ιστότοπος αναπαράγει πολυμέσα</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..1961f7bdbe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Camera is on</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microphone is on</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Camera and microphone are on</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tap to open the tab that’s using your camera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tap to open the tab that’s using your microphone.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tap to open the tab that’s using your microphone and camera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Reminder: %1$s is still using your camera. Tap to open the tab.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Reminder: %1$s is still using your microphone. Tap to open the tab</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Reminder: %1$s is still using your microphone. Tap to open the tab.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Reminder: %1$s is still using your microphone and camera. Tap to open the tab</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Reminder: %1$s is still using your microphone and camera. Tap to open the tab.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Play</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">A site is playing media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..c794d4a237
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Camera is on</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microphone is on</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Camera and microphone are on</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tap to open the tab that’s using your camera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tap to open the tab that’s using your microphone.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tap to open the tab that’s using your microphone and camera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Reminder: %1$s is still using your camera. Tap to open the tab.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Reminder: %1$s is still using your microphone. Tap to open the tab</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Reminder: %1$s is still using your microphone. Tap to open the tab.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Reminder: %1$s is still using your microphone and camera. Tap to open the tab</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Reminder: %1$s is still using your microphone and camera. Tap to open the tab.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Play</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">A site is playing media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..7c98f55eec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-eo/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Aŭdvidaĵo</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Filmilo ŝaltita</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofono ŝaltita</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Filmilo kaj mikrofono ŝaltitaj</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tuŝetu por malfermi la langeton kiu uzas vian filmilon.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tuŝetu por malfermi la langeton kiu uzas vian mikrofonon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tuŝetu por malfermi la langeton kiu uzas vian filmilon kaj mikrofonon.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Memorigo: %1$s ankoraŭ uzas vian filmilon. Tuŝetu por malfermi la langeton.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Memorigo: %1$s ankoraŭ uzas vian mikrofonon. Tuŝetu por malfermi la langeton.</string>
+
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Memorigo: %1$s ankoraŭ uzas vian mikrofonon. Tuŝetu por malfermi la langeton.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Memorigo: %1$s ankoraŭ uzas vian mikrofonon kaj filmilon. Tuŝetu por malfermi la langeton.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Memorigo: %1$s ankoraŭ uzas vian mikrofonon kaj filmilon. Tuŝetu por malfermi la langeton.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Ludi</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Paŭzigi</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Aŭdvidaĵo ludata de retejo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..e89920b721
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medios</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La cámara está encendida</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">El micrófono está encendido</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La cámara y el micrófono están encendidos</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tocá para abrir la pestaña que está usando tu cámara.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tocá para abrir la pestaña que está usando tu micrófono.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tocá para abrir la pestaña que está usando tu micrófono y tu cámara.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Recordatorio: %1$s todavía está usando tu cámara. Tocá para abrir la pestaña.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s todavía está usando tu micrófono. Tocá para abrir la pestaña</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Recordatorio: %1$s todavía está usando tu micrófono. Tocá para abrir la pestaña.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s todavía está usando tu micrófono y tu cámara. Tocá para abrir la pestaña</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Recordatorio: %1$s todavía está usando tu micrófono y tu cámara. Tocá para abrir la pestaña.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sitio está reproduciendo medios</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..cb20dbf5c7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medios</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Cámara encendida</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Micrófono encendido</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Cámara y micrófono encendidos</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toca para abrir la pestaña que está usando tu cámara.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toca para abrir la pestaña que está usando tu micrófono.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toca para abrir la pestaña que está usando tu micrófono y cámara.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Recordatorio: %1$s todavía está usando tu cámara. Toca para abrir la pestaña.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s todavía está usando tu micrófono. Toca para abrir la pestaña</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Recordatorio: %1$s todavía está usando tu micrófono. Toca para abrir la pestaña.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s todavía está usando tu micrófono y cámara. Toca para abrir la pestaña</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Recordatorio: %1$s todavía está usando tu micrófono y cámara. Toca para abrir la pestaña.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sitio está reproduciendo medios</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..71bef26954
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimedia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La cámara está activada</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">El micrófono está activado</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La cámara y el micrófono están activados</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toca para abrir la pestaña que está usando tu cámara.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toca para abrir la pestaña que está usando tu micrófono.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toca para abrir la pestaña que está usando tu micrófono y tu cámara.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Recordatorio: %1$s todavía está usando tu cámara. Toca para abrir la pestaña.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s todavía está usando tu micrófono. Toca para abrir la pestaña</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Recordatorio: %1$s todavía está usando tu micrófono. Toca para abrir la pestaña.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s todavía está usando tu micrófono y tu cámara. Toca para abrir la pestaña</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Recordatorio: %1$s todavía está usando tu micrófono y tu cámara. Toca para abrir la pestaña.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sitio está reproduciendo contenido multimedia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..b7f5e8358e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimedia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La cámara está activada</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Micrófono encendido</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Cámara y micrófono encendidos</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sitio está reproduciendo contenido multimedia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..71bef26954
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-es/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimedia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La cámara está activada</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">El micrófono está activado</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La cámara y el micrófono están activados</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toca para abrir la pestaña que está usando tu cámara.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toca para abrir la pestaña que está usando tu micrófono.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toca para abrir la pestaña que está usando tu micrófono y tu cámara.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Recordatorio: %1$s todavía está usando tu cámara. Toca para abrir la pestaña.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s todavía está usando tu micrófono. Toca para abrir la pestaña</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Recordatorio: %1$s todavía está usando tu micrófono. Toca para abrir la pestaña.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s todavía está usando tu micrófono y tu cámara. Toca para abrir la pestaña</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Recordatorio: %1$s todavía está usando tu micrófono y tu cámara. Toca para abrir la pestaña.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sitio está reproduciendo contenido multimedia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..de103560f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-et/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Meedia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kaamera on sisse lülitatud</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon on sisse lülitatud</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kaamera ja mikrofon on sisse lülitatud</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Puuduta kaamerat kasutava kaardi avamiseks.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Puuduta mikrofoni kasutava kaardi avamiseks.</string>
+
+
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Puuduta mikrofoni ja kaamerat kasutava kaardi avamiseks.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Meeldetuletus: %1$s kasutab endiselt kaamerat. Puuduta kaardi avamiseks.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Meeldetuletus: %1$s kasutab endiselt mikrofoni. Puuduta kaardi avamiseks.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Meeldetuletus: %1$s kasutab endiselt mikrofoni. Puuduta kaardi avamiseks.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Meeldetuletus: %1$s kasutab endiselt mikrofoni ja kaamerat. Puuduta kaardi avamiseks.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Meeldetuletus: %1$s kasutab endiselt mikrofoni ja kaamerat. Puuduta kaardi avamiseks.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Esita</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Paus</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Sait esitab meediat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..12d9fb39a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-eu/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera piztuta dago</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofonoa piztuta dago</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera eta mikrofonoa piztuta daude</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Sakatu zure kamera erabiltzen ari den fitxa irekitzeko.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Sakatu zure mikrofonoa erabiltzen ari den fitxa irekitzeko.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Sakatu zure mikrofono eta kamera erabiltzen ari den fitxa irekitzeko.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Gogorarazlea: %1$s zure kamera ari da erabiltzen oraindik. Sakatu fitxa irekitzeko.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Gogorarazlea: %1$s zure mikrofonoa ari da erabiltzen oraindik. Sakatu fitxa irekitzeko</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Gogorarazlea: %1$s zure mikrofonoa ari da erabiltzen oraindik. Sakatu fitxa irekitzeko.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Gogorarazlea: %1$s zure mikrofono eta kamera ari da erabiltzen oraindik. Sakatu fitxa irekitzeko</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Gogorarazlea: %1$s zure mikrofono eta kamera ari da erabiltzen oraindik. Sakatu fitxa irekitzeko.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Erreproduzitu</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausatu</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Gune bat multimedia erreproduzitzen ari da</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..6529864cad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-fa/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">رسانه</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">دوربین روشن است</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">صدابَر وصل است</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">دوربین و صدابَر وصل هستند</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">پخش</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">مکث</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">پایگاهی در حال پخش رسانه است</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..9b3cabee15
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ff/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Mejaaje</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kameraa ena huɓɓi</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikkoroo ena huɓɓi</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kameraa e mikkoroo ena kuɓɓi</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Tar</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Sabbo</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Lowre ina tara mejaaje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..713dcededf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-fi/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera on päällä</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofoni on päällä</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera ja mikrofoni ovat päällä</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Napauta avataksesi välilehden, joka käyttää kameraa.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Napauta avataksesi välilehden, joka käyttää mikrofonia.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Napauta avataksesi välilehden, joka käyttää mikrofonia ja kameraa.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Muistutus: %1$s käyttää edelleen kameraasi. Avaa välilehti napauttamalla.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Muistutus: %1$s käyttää edelleen mikrofoniasi. Avaa välilehti napauttamalla</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Muistutus: %1$s käyttää edelleen mikrofoniasi. Avaa välilehti napauttamalla.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Muistutus: %1$s käyttää edelleen mikrofoniasi ja kameraasi. Avaa välilehti napauttamalla</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Muistutus: %1$s käyttää edelleen mikrofoniasi ja kameraasi. Avaa välilehti napauttamalla.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Toista</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Tauko</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Sivusto toistaa mediaa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..6f02a1be94
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-fr/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimédia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La caméra est activée</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Le microphone est activé</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La caméra et le microphone sont activés</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Appuyez pour ouvrir l’onglet qui utilise votre appareil photo.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Appuyez pour ouvrir l’onglet qui utilise votre microphone.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Appuyez pour ouvrir l’onglet qui utilise votre microphone et votre appareil photo.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Rappel : %1$s utilise toujours votre appareil photo. Appuyez pour ouvrir l’onglet.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Rappel : %1$s utilise toujours votre microphone. Appuyez pour ouvrir l’onglet</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Rappel : %1$s utilise toujours votre microphone. Appuyez pour ouvrir l’onglet.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Rappel : %1$s utilise toujours votre microphone et votre appareil photo. Appuyez pour ouvrir l’onglet</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Rappel : %1$s utilise toujours votre microphone et votre appareil photo. Appuyez pour ouvrir l’onglet.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Lecture</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un site lit un élément multimédia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..898cc3292c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-fur/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimedia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La fotocjamare e je ative</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Il microfon al è atîf</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La fotocjamare e il microfon a son atîfs</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tocje par vierzi la schede che e sta doprant la fotocjamare.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tocje par vierzi la schede che e sta doprant il microfon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tocje par vierzi la schede che e sta doprant il microfon e la fotocjamare.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Promemoria: %1$s al sta ancjemò doprant la fotocjamare. Tocje parvierzi la schede.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Promemoria: %1$s al sta ancjemò doprant il microfon. Tocje par vierzi la schede</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Pro memoria: %1$s al sta ancjemò doprant il to microfon. Tocje par vierzi la schede.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Promemoria: %1$s al sta ancjemò doprant il microfon e la fotocjamare. Tocje par vierzi la schede</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Pro memoria: %1$s al sta ancjemò doprant il microfon e la fotocjamare. Tocje par vierzi la schede.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Riprodûs</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Met in pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sît al sta riprodusint contignûts multimediâi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..a04d2a4bd6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera is oan</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofoan is oan</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera en mikrofoan binne oan</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tik om it ljepblêd dat jo kamera brûkt te iepenjen.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tik om it ljepblêd dat jo mikrofoan brûkt te iepenjen.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tik om it ljepblêd dat jo mikrofoan en kamera brûkt te iepenjen.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Oantinken: %1$s brûkt jo kamera noch hieltyd. Tik om it ljepblêd te iepenjen.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Oantinken: %1$s brûkt jo mikrofoan noch hieltyd. Tik om it ljepblêd te iepenjen.</string>
+
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Oantinken: %1$s brûkt jo mikrofoan noch hieltyd. Tik om it ljepblêd te iepenjen.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Oantinken: %1$s brûkt jo mikrofoan en kamera noch hieltyd. Tik om it ljepblêd te iepenjen.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Oantinken: %1$s brûkt jo mikrofoan en kamera noch hieltyd. Tik om it ljepblêd te iepenjen.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Ofspylje</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pauzearje</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">In website spilet media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..329388b6df
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Meáin</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Tá an ceamara ar siúl</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Tá an micreafón ar siúl</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Tá an ceamara agus an micreafón ar siúl</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Seinn</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Sos</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Tá suíomh ag seinm meáin</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..ba6e495c30
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-gd/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Meadhanan</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Tha an camara air</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Tha am micreofon air</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Tha an camara ’s am micreofon air</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Cluich</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Cuir na stad</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Tha làrach a’ cluich meadhanan</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..c313ca9711
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-gl/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimedia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">A cámara está activada</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">O micrófono está activado</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">A cámara e o micrófono están activados</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toque para abrir a lapela que está a usar a súa cámara.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toque para abrir a lapela que está a usar o seu micrófono.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toque para abrir a lapela que está a usar o seu micrófono e a súa cámara.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Recordatorio: %1$s aínda está usando a súa cámara. Toque para abrir a pestana.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s aínda está usando o seu micrófono. Toque para abrir a pestana</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Recordatorio: %1$s aínda está usando o seu micrófono. Toque para abrir a pestana.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Recordatorio: %1$s aínda está usando o seu micrófono e a súa cámara. Toque para abrir a pestana</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Recordatorio: %1$s aínda está usando o seu micrófono e a súa cámara. Toque para abrir a pestana.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sitio está a reproducir multimedia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..5db9c58387
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-gn/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Momaranduha</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Ta’ãnganohẽha hendy</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Ñe’ẽatãha hendy</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Ta’ãnganohẽha ha ñe’ẽatãha hendy</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Eikutu embojuruja hag̃ua tendayke oiporúva ne ta’ãnganohẽha.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Eikutu embojuruja hag̃ua tendayke oiporúva ne ñe’ẽatãha.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Eikutu embojuruja hag̃ua tendayke oiporúva ne ñe’ẽatãha ha ta’ãnganohẽha.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Mandu’arã: %1$s oiporu gueteri ne ta’ãnganohẽha. Eikutu embojuruja hag̃ua tendayke.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Mandu’arã: %1$s oiporu gueteri ne ñe’ẽatãha. Eikutu embojuruja hag̃ua tendayke.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Mandu’arã: %1$s oiporu gueteri ne ñe’ẽatãha. Eikutu embojuruja hag̃ua tendayke.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Mandu’arã: %1$s oiporu gueteri ne ñe’ẽatãha ha ta’ãnganohẽha. Eikutu embojuruja hag̃ua tendayke.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Mandu’arã: %1$s oiporu gueteri ne ñe’ẽatãha ha ta’ãnganohẽha. Eikutu embojuruja hag̃ua tendayke.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Mboheta</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Mombyta</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Ko tenda omboheta momaranduha retepy</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..cdcc023586
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">મીડિયા</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">કૅમેરા ચાલુ છે</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">માઇક્રોફોન ચાલુ છે</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">કૅમેરા અને માઇક્રોફોન ચાલુ છે</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">શરુ કરો</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">અટકાવો</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">એક સાઇટ મીડિયા ચલાવી રહી છે</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..c949bc34ba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">मीडिया</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">कैमरा चालू है</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">माइक्रोफोन चालू है</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">कैमरा और माइक्रोफोन चालू हैं</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">चलाएं</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">रोकें</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">एक साइट मीडिया चला रहा है</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..371dc144a3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-hil/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medya</string>
+
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..25d6fa1646
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-hr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Mediji</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera je uključena</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon je uključen</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera i mikrofon su uključeni</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Pokreni</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pauziraj</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Jedna web-stranica reproducira medije</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..b623f57322
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medije</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera je zapinjena</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon je zapinjeny</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera a mikrofon stej zapinjenej</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Podótkńće so, zo byšće rajtark wočinił, kotryž wašu kameru wužiwa. </string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Podótkńće so, zo byšće rajtark wočinił, kotryž waš mikrofon wužiwa. </string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Podótkńće so, zo byšće rajtark wočinił, kotryž waš mikrofon a wašu kameru wužiwa. </string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Kedźbu: %1$s wašu kameru hižo wužiwa. Podótkńće so, zo byšće rajtark wočinił.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Kedźbu: %1$s waš mikrofon hižo wužiwa. Podótkńće so, zo byšće rajtark wočinił</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Kedźbu: %1$s waš mikrofon hižo wužiwa. Podótkńće so, zo byšće rajtark wočinił.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Kedźbu: %1$s waš mikrofon a wašu kameru hižo wužiwa. Podótkńće so, zo byšće rajtark wočinił</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Kedźbu: %1$s waš mikrofon a wašu kameru hižo wužiwa. Podótkńće so, zo byšće rajtark wočinił.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Wothrać</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Přestawka</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Sydło medije wothrawa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..81de265e5b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-hu/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Média</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera be</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon be</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera és mikrofon be</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Koppintson a kamerát használó lap megnyitásához.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Koppintson a mikrofont használó lap megnyitásához.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Koppintson a mikrofont és kamerát használó lap megnyitásához.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Emlékeztető: A %1$s még mindig használja a kameráját. Koppintson a lap megnyitásához.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Emlékeztető: A %1$s még mindig használja a mikrofonját. Koppintson a lap megnyitásához.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Emlékeztető: A %1$s még mindig használja a mikrofonját. Koppintson a lap megnyitásához.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Emlékeztető: A %1$s még mindig használja a mikrofonját és kameráját. Koppintson a lap megnyitásához.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Emlékeztető: A %1$s még mindig használja a mikrofonját és kameráját. Koppintson a lap megnyitásához.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Lejátszás</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Szünet</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Egy webhely médialejátszást folytat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..67280dbc3d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Մեդիա</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Տեսախցիկը միացված է</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Խոսափողը միացված է</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Տեսախցիկը և խոսափողը միացված են</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Հպեք՝ Ձեր տեսախցիկից օգտվող ներդիրը բացելու համար:</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Հպեք՝ Ձեր խոսափողից օգտվող ներդիրը բացելու համար:</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Հպեք՝ Ձեր խոսափողից և տեսախցիկից օգտվող ներդիրը բացելու համար:</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Հիշեցում. %1$s-ը դեռ օգտվում է Ձեր տեսախցիկից: Հպեք՝ ներդիրը բացելու համար:</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Հիշեցում. %1$s-ը դեռ օգտվում է Ձեր խոսափողից: Հպեք՝ ներդիրը բացելու համար:</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Հիշեցում. %1$s-ը դեռ օգտվում է Ձեր խոսափողից: Հպեք՝ ներդիրը բացելու համար:</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Հիշեցում. %1$s-ը դեռ օգտվում է Ձեր խոսափողից և տեսախցիկից: Հպեք՝ ներդիրը բացելու համար:</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Հիշեցում. %1$s-ը դեռ օգտվում է Ձեր խոսափողից և տեսախցիկից: Հպեք՝ ներդիրը բացելու համար:</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Նվագարկել</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Դադարեցնել</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Կայքը նվագարկում է մեդիա</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..8103d9133c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ia/strings.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medios</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Le camera es activate</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Le microphono es activate</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Camera e microphono es activate</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tocca pro aperir le scheda que usa tu camera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tocca pro aperir le scheda que usa tu microphono.</string>
+
+
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tocca pro aperir le scheda que usa tu microphono e tu camera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Memento: %1$s ancora usa tu camera. Tocca pro aperir le scheda.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Memento: %1$s ancora usa tu microphono. Tocca pro aperir le scheda.</string>
+
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Memento: %1$s ancora usa tu microphono. Tocca pro aperir le scheda.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Memento: %1$s ancora usa tu microphono e camera. Tocca pro aperir le scheda.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Memento: %1$s ancora usa tu microphono e camera. Tocca pro aperir le scheda.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproducer</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sito reproduce contentos multimedial </string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..ed5de8a69d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-in/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera aktif</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon aktif</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera dan mikrofon aktif</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Mainkan</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Jeda</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Situs sedang memutar media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..5d54639a35
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-is/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Miðill</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kveikt er á myndavél</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Kveikt er á hljóðnema</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kveikt er á myndavél og hljóðnema</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Ýttu á til að opna flipann sem notar myndavélina þína.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Ýttu á til að opna flipann sem notar hljóðnemann þinn.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Ýttu á til að opna flipann sem notar hljóðnemann og myndavélina.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Áminning: %1$s er enn að nota myndavélina þína. Ýttu á til að opna flipann.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Áminning: %1$s er enn að nota hljóðnemann þinn. Ýttu á til að opna flipann</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Áminning: %1$s er enn að nota hljóðnemann þinn. Ýttu á til að opna flipann.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Áminning: %1$s er enn að nota hljóðnemann og myndavélina. Ýttu á til að opna flipann</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Áminning: %1$s er enn að nota hljóðnemann og myndavélina. Ýttu á til að opna flipann.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Spila</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Setja í bið</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Vefsvæði er að spila miðil</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..3a2f06de81
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-it/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La fotocamera è attiva</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Il microfono è attivo</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La fotocamera e il microfono sono attivi</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tocca per aprire la scheda che sta utilizzando la fotocamera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tocca per aprire la scheda che sta utilizzando il microfono.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tocca per aprire la scheda che sta utilizzando il microfono e la fotocamera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Promemoria: %1$s sta ancora utilizzando la fotocamera. Tocca per aprire la scheda.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Promemoria: %1$s sta ancora utilizzando il microfono. Tocca per aprire la scheda</string>
+
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Promemoria: %1$s sta ancora utilizzando il microfono. Tocca per aprire la scheda.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Promemoria: %1$s sta ancora utilizzando il microfono e la fotocamera. Tocca per aprire la scheda</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Promemoria: %1$s sta ancora utilizzando il microfono e la fotocamera. Tocca per aprire la scheda.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Riproduci</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Metti in pausa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un sito sta riproducendo contenuti multimediali</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..b8d89869a3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-iw/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">מדיה</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">המצלמה פעילה</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">המיקרופון פעיל</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">המצלמה והמיקרופון פעילים</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">יש להקיש כדי לפתוח את הלשונית שמשתמשת במצלמה שלך.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">יש להקיש כדי לפתוח את הלשונית שמשתמשת במיקרופון שלך.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">יש להקיש כדי לפתוח את הלשונית שמשתמשת במיקרופון ובמצלמה שלך.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">תזכורת: %1$s עדיין משתמש במצלמה שלך. יש להקיש כדי לפתוח את הלשונית.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">תזכורת: %1$s עדיין משתמש במיקרופון שלך. יש להקיש כדי לפתוח את הלשונית.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">תזכורת: %1$s עדיין משתמש במיקרופון שלך. יש להקיש כדי לפתוח את הלשונית.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">תזכורת: %1$s עדיין משתמש במיקרופון ובמצלמה שלך. יש להקיש כדי לפתוח את הלשונית</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">תזכורת: %1$s עדיין משתמש במיקרופון ובמצלמה שלך. יש להקיש כדי לפתוח את הלשונית.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">ניגון</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">השהייה</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">אתר מנגן מדיה</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..c1cfbc4c7d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ja/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">メディア</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">カメラを共有中</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">マイクを共有中</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">カメラとマイクを共有中</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">タップしてカメラを使用しているタブを開いてください。</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">タップしてマイクを使用しているタブを開いてください。</string>
+
+
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">タップしてマイクとカメラを使用しているタブを開いてください。</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">通知: %1$s がまだカメラを使用しています。タップしてタブを開いてください。</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">通知: %1$s がまだマイクを使用しています。タップしてタブを開いてください。</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">通知: %1$s がまだマイクを使用しています。タップしてタブを開いてください。</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">通知: %1$s がまだマイクとカメラを使用しています。タップしてタブを開いてください。</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">通知: %1$s がまだマイクとカメラを使用しています。タップしてタブを開いてください。</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">再生</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">一時停止</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">サイトがメディアを再生しています</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..792398b0f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ka/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ფაილები</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">კამერა ჩართულია</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">მიკროფონი ჩართულია</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">კამერა და მიკროფონი ჩართულია</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">გაშვება</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">შეჩერება</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">საიტზე გაშვებულია ფაილი</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..3b164f0b99
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera qosılǵan</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon qosılǵan</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera hám mikrofon qosılǵan</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Baslaw</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pauza</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Saytta media qoyılıp tur</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..a823f58c8a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-kab/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Allalen n teywalt</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Takamirat tetteddu</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Asawaḍ itteddu</string>
+
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Takamiṛat akked usawaḍ tteddun</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Urar</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Asteɛfu</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Asmel yaqqaṛ aferdis aget midya</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..03ebec011a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-kk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Медиа</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Камера іске қосулы тұр</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Микрофон іске қосулы тұр</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Камера және микрофон іске қосулы тұр</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Камераңызды пайдаланып тұрған бетті ашу үшін шертіңіз.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Микрофоныңызды пайдаланып тұрған бетті ашу үшін шертіңіз.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Микрофоныңыз бер камераңызды пайдаланып тұрған бетті ашу үшін шертіңіз.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Еске салу: %1$s әлі де камераңызды пайдалануда. Бетті ашу үшін шертіңіз.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Еске салу: %1$s әлі де микрофоныңызды пайдалануда. Бетті ашу үшін шертіңіз</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Еске салу: %1$s әлі де микрофоныңызды пайдалануда. Бетті ашу үшін шертіңіз.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Еске салу: %1$s әлі де микрофоныңыз бен камераңызды пайдалануда. Бетті ашу үшін шертіңіз</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Еске салу: %1$s әлі де микрофоныңыз бен камераңызды пайдалануда. Бетті ашу үшін шертіңіз.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Ойнату</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Аялдату</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Сайт медианы ойнап жатыр</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..65c7577f45
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medya</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera vekirî ye</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mîkrofon vekirî ye</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera û mîkrofon vekirî ne</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Ji bo vekirina hilpekîna ku kameraya te bi kar tîne, bitikîne.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Ji bo vekirina hilpekîna ku mîkrofona te bi kar tîne, bitikîne.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Ji bo vekirina hilpekîna ku mîkrofon û kameraya te bi kar tîne, bitikîne.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Bibîrxistin: %1$s hê jî kameraya te bi kar tîne. Ji bo vekirina hilpekînê bitikîne.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Bibîrxistin: %1$s hê jî mîkrofona te bi kar tîne. Ji bo vekirina hilpekînê bitikîne</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Bibîrxistin: %1$s hê jî mîkrofona te bi kar tîne. Ji bo vekirina hilpekînê bitikîne.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Bibîrxistin: %1$s hê jî mîkrofon û kameraya te bi kar tîne. Ji bo vekirina hilpekînê bitikîne</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Bibîrxistin: %1$s hê jî mîkrofon kameraya te bi kar tîne. Ji bo vekirina hilpekînê bitikîne.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Lêde</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Bisekinîne</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Malperek li medyayê dide</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..431fea19d4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-kn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ಮಾಧ್ಯಮ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">ಕ್ಯಾಮೆರಾ ಚಾಲನೆಗೊಂಡಿದೆ</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ಮೈಕ್ರೊಫೋನ್ ಚಾಲನೆಯಲ್ಲಿದೆ</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">ಕ್ಯಾಮೆರಾ ಮತ್ತು ಮೈಕ್ರೊಫೋನ್ ಚಾಲನೆಯಲ್ಲಿವೆ</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">ಪ್ಲೇ ಮಾಡು</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">ವಿರಾಮ</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ಒಂದು ಸೈಟ್ ಮಾಧ್ಯಮವನ್ನು ಆಡುತ್ತಿದೆ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..7c97ebc40b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ko/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">미디어</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">카메라가 켜저 있음</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">마이크가 켜져 있음</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">카메라와 마이크가 켜저 있음</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">카메라를 사용하고 있는 탭을 열려면 누르세요.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">마이크를 사용하고 있는 탭을 열려면 누르세요.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">마이크와 카메라를 사용하고 있는 탭을 열려면 누르세요.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">미리 알림: %1$s가 아직 카메라를 사용하고 있습니다. 탭을 열려면 누르세요.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">미리 알림: %1$s가 아직 마이크를 사용하고 있습니다. 탭을 열려면 누르세요.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">미리 알림: %1$s가 아직 마이크를 사용하고 있습니다. 탭을 열려면 누르세요.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">미리 알림: %1$s가 아직 마이크와 카메라를 사용하고 있습니다. 탭을 열려면 누르세요.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">미리 알림: %1$s가 아직 마이크와 카메라를 사용하고 있습니다. 탭을 열려면 누르세요.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">재생</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">중지</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">사이트가 미디어를 재생하고 있습니다</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..31b5a789e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-lij/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Fòtocamera averta</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Micròfono açeizo</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Fòtocamera e micròdfono açeixi</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Riproduçion</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pösa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un scito o fâ anâ di media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..be8ef3d643
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-lo/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ມີເດຍ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">ກ້ອງເປີດຢູ່</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ໄມໂຄຣໂຟນເປີດຢູ່</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">ກ້ອງ ແລະ ໄມໂຄຣໂຟນເປີດຢູ່</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">ຫຼິ້ນ</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">ຢຸດ</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ເວັບໄຊທກຳລັງເປີດມີເດຍຢູ່</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..843481b530
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-lt/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medija</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Naudojama kamera</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Naudojamas mikrofonas</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Naudojama kamera ir mikrofonas</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Groti</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pristabdyti</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Svetainėje groja medija</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..b2e7807f07
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-mix/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Kunchatu</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..71c1789ca9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ml/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">മീഡിയ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">ക്യാമറ ഓണാണ്</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">മൈക്രോഫോൺ ഓണാണ്</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">ക്യാമറയും മൈക്രോഫോണും ഓണാണ്</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">പ്ലേ ചെയ്യുക</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">തല്‍ക്കാലത്തേക്ക് നിര്‍ത്തുക</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ഒരു സൈറ്റ് മീഡിയ പ്ലേ ചെയ്യുന്നുണ്ട്</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..365495f4a4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-mr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">मिडीया</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">कॅमेरा चालू आहे</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">मायक्रोफोन चालू आहे</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">कॅमेरा आणि मायक्रोफोन चालू आहेत</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">चालवा</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">थांबवा</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">एक साईट मिडिया चालवत आहे</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..ebf3b77f22
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-my/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">မီဒီယာ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">ကင်မရာဖွင့်ထားသည်</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">မိုက်ခရိုဖုန်းဖွင့်နေသည်</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">ကင်မရာနှင့်မိုက်ကရိုဖုန်းဖွင့်ထားသည်</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">ဖွင့်ပါ</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">ခေတ္တရပ်တန့်ပါ</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ဆိုဘ် တစ်ခုကမီဒီယာကိုဖွင့်နေသည်</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..0ec9fd943a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medier</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera er på</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon er på</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera og mikrofon er på</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Trykk for å åpne fanen som bruker kameraet ditt.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Trykk for å åpne fanen som bruker mikrofonen din.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Trykk for å åpne fanen som bruker mikrofonen og kameraet ditt.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Påminnelse: %1$s bruker fortsatt kameraet ditt. Trykk for å åpne fanen.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Påminnelse: %1$s bruker fortsatt mikrofonen din. Trykk for å åpne fanen</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Påminnelse: %1$s bruker fortsatt mikrofonen din. Trykk for å åpne fanen.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Påminnelse: %1$s bruker fortsatt mikrofonen og kameraet ditt. Trykk for å åpne fanen</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Påminnelse: %1$s bruker fortsatt mikrofonen og kameraet ditt. Trykk for å åpne fanen.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Spill av</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Et nettsted spiller av medium</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..fa0b126376
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">मिडिया</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">क्यामरा खुल्ला छ</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">माइक्रोफोन खुल्ला छ</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">क्यामरा र माइक्रोफोन दुबै खुल्ला छन्</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">बजाउनुहोस्</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">रोक्नुहोस्</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">एउटा साइटले मिडिया बजाइरहेको छ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..69af62dea2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-nl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Camera is aan</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microfoon is aan</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Camera en microfoon zijn aan</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tik om het tabblad dat uw camera gebruikt te openen.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tik om het tabblad dat uw microfoon gebruikt te openen.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tik om het tabblad dat uw microfoon en camera gebruikt te openen.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Herinnering: %1$s gebruikt uw camera nog steeds. Tik om het tabblad te openen.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Herinnering: %1$s gebruikt uw microfoon nog steeds. Tik om het tabblad te openen.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Herinnering: %1$s gebruikt uw microfoon nog steeds. Tik om het tabblad te openen.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Herinnering: %1$s gebruikt uw microfoon en camera nog steeds. Tik om het tabblad te openen.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Herinnering: %1$s gebruikt uw microfoon en camera nog steeds. Tik om het tabblad te openen.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Afspelen</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pauzeren</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Een website speelt media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..54a2f5bb49
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medium</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kameraet er på</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofonen er på</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera og mikrofon er på</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Trykk for å opne fana som brukar kameraet ditt.</string>
+
+
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Trykk for å opne fana som brukar mikrofonen din.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Trykk for å opne fana som brukar mikrofonen og kameraet ditt.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Påminning: %1$s brukar framleis kameraet ditt. Trykk for å opne fana.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Påminning: %1$s brukar framleis mikrofonen din. Trykk for å opne fana</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Påminning: %1$s brukar framleis mikrofonen din. Trykk for å opne fana.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Spel av</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Ein nettstad spelar av medium</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..4d177d34e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-oc/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Mèdias</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La camèra es activa</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Lo microfòn es actiu</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La camèra e lo microfòn son actius</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tocatz per dobrir l’onglet qu’es a utilizar la camèra.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tocatz per dobrir l’onglet qu’es a utilizar lo microfòn.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tocatz per dobrir l’onglet qu’es a utilizar la camèra e lo microfòn.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Rapèl : %1$s utiliza encara la camèra. Tocatz per dobrir l’onglet.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Rapèl : %1$s utiliza encara lo microfòn. Tocatz per dobrir l’onglet.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Rapèl : %1$s utiliza encara lo microfòn. Tocatz per dobrir l’onglet.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Rapèl : %1$s utiliza encara la camèra e lo microfòn. Tocatz per dobrir l’onglet.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Rapèl : %1$s utiliza encara la camèra e lo microfòn. Tocatz per dobrir l’onglet.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Lectura</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un site jòga un element multimèdia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..3df7c6ed41
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-or/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ମିଡ଼ିଆ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">କ୍ୟାମେରା ଅନ ଅଛି</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ମାଇକ୍ରୋଫୋନ ଅନ ଅଛି</string>
+
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..530ee56bc4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ਮੀਡਿਆ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">ਕੈਮਰਾ ਚਾਲੂ ਹੈ</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ਮਾਈਕਰੋਫੋਨ ਚਾਲੂ ਹੈ</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">ਕੈਮਰਾ ਤੇ ਮਾਈਕਰੋਫ਼ੋਨ ਚਾਲ ਹਨ</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">ਤੁਹਾਡੇ ਕੈਮਰੇ ਨੂੰ ਵਰਤਣ ਵਾਲੀ ਟੈਬ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">ਤੁਹਾਡੇ ਮਾਈਕਰੋਫ਼ੋਨ ਨੂੰ ਵਰਤਣ ਵਾਲੀ ਟੈਬ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">ਤੁਹਾਡੇ ਮਾਈਕਰੋਫ਼ੋਨ ਅਤੇ ਕੈਮਰੇ ਨੂੰ ਵਰਤਣ ਵਾਲੀ ਟੈਬ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">ਰੀਮਾਈਂਡਰ: %1$s ਹਾਲੇ ਵੀ ਤੁਹਾਡੇ ਕੈਮਰੇ ਨੂੰ ਵਰਤ ਰਹੀ ਹੈ। ਟੈਬ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">ਰੀਮਾਈਂਡਰ: %1$s ਹਾਲੇ ਵੀ ਤੁਹਾਡੇ ਮਾਈਕਰੋਫ਼ੋਨ ਨੂੰ ਵਰਤ ਰਹੀ ਹੈ। ਟੈਬ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">ਰੀਮਾਈਂਡਰ: %1$s ਹਾਲੇ ਵੀ ਤੁਹਾਡੇ ਮਾਈਕਰੋਫ਼ੋਨ ਨੂੰ ਵਰਤ ਰਹੀ ਹੈ। ਟੈਬ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">ਰੀਮਾਈਂਡਰ: %1$s ਹਾਲੇ ਵੀ ਤੁਹਾਡੇ ਮਾਈਕਰੋਫ਼ੋਨ ਅਤੇ ਕੈਮਰੇ ਨੂੰ ਵਰਤ ਰਹੀ ਹੈ। ਟੈਬ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">ਰੀਮਾਈਂਡਰ: %1$s ਹਾਲੇ ਵੀ ਤੁਹਾਡੇ ਮਾਈਕਰੋਫ਼ੋਨ ਅਤੇ ਕੈਮਰੇ ਨੂੰ ਵਰਤ ਰਹੀ ਹੈ। ਟੈਬ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">ਚਲਾਓ</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">ਵਿਰਾਮ</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ਸਾਈਟ ਮੀਡਿਆ ਚਲਾ ਰਹੀ ਹੈ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..a1f77acea8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">میڈیا</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">کیمرہ چالو اے</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">مائیکروفون چالو اے</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">کیمرہ تے مائیکروفون چالو ہن</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">چلاؤ</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">روکو</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">اک سائٹ میڈیا چلا رہی اے</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..9c22f9a90b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-pl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimedia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Aparat jest włączony</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon jest włączony</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Aparat i mikrofon są włączone</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Stuknij, aby otworzyć kartę korzystającą z aparatu.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Stuknij, aby otworzyć kartę korzystającą z mikrofonu.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Stuknij, aby otworzyć kartę korzystającą z mikrofonu i aparatu.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Przypomnienie: %1$s nadal korzysta z aparatu. Stuknij, aby otworzyć kartę.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Przypomnienie: %1$s nadal korzysta z mikrofonu. Stuknij, aby otworzyć kartę</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Przypomnienie: %1$s nadal korzysta z mikrofonu. Stuknij, aby otworzyć kartę.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Przypomnienie: %1$s nadal korzysta z mikrofonu i aparatu. Stuknij, aby otworzyć kartę</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Przypomnienie: %1$s nadal korzysta z mikrofonu i aparatu. Stuknij, aby otworzyć kartę.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Odtwórz</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Wstrzymaj</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Witryna odtwarza multimedia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..90ba3f4b26
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Mídia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Câmera ligada</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microfone ligado</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Câmera e microfone ligados</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toque para abrir a aba que está usando sua câmera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toque para abrir a aba que está usando seu microfone.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toque para abrir a aba que usa seu microfone e câmera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Lembrete: O %1$s ainda está usando sua câmera. Toque para abrir a aba.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Lembrete: O %1$s ainda está usando seu microfone. Toque para abrir a aba</string>
+
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Lembrete: O %1$s ainda está usando seu microfone. Toque para abrir a aba.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Lembrete: O %1$s ainda está usando seu microfone e câmera. Toque para abrir a aba</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Lembrete: O %1$s ainda está usando seu microfone e câmera. Toque para abrir a aba.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproduzir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausar</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Um site está reproduzindo mídia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..86658e844c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Multimédia</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Câmara ligada</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microfone ligado</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Câmara e microfone ligados</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toque para abrir o separador que está a utilizar a sua câmara.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toque para abrir o separador que está a utilizar o seu microfone.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toque para abrir o separador que está a utilizar o seu microfone e câmara.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Lembrete: %1$s ainda está a utilizar a sua câmara. Toque para abrir o separador.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Lembrete: %1$s ainda está a utilizar o seu microfone. Toque para abrir o separador</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Lembrete: %1$s ainda está a utilizar o seu microfone. Toque para abrir o separador.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Lembrete: %1$s ainda está a utilizar o seu microfone e câmara. Toque para abrir o separador</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Lembrete: %1$s ainda está a utilizar o seu microfone e câmara. Toque para abrir o separador.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reproduzir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Um site está a reproduzir multimédia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..5564d78211
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-rm/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Medias</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">La camera è activa</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Il microfon è activ</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">La camera ed il microfon èn activs</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tutgar per avrir il tab che utilisescha tia camera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tutgar per avrir il tab che utilisescha tes microfon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tutgar per avrir il tab che utilisescha tes microfon e tia camera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Promemoria: %1$s utilisescha anc adina tia camera. Tutgar per avrir il tab.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Promemoria: %1$s utilisescha anc adina tes microfon. Tutgar per avrir il tab</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Promemoria: %1$s utilisescha anc adina tes microfon. Tutgar per avrir il tab.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Promemoria: %1$s utilisescha anc adina tes microfon e tia camera. Tutgar per avrir il tab</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Promemoria: %1$s utilisescha anc adina tes microfon e tia camera. Tutgar per avrir il tab.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Far ir</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Ina website fa ir multimedia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..1d2be5f664
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ro/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Conținut media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Camera este pornită</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microfonul este pornit</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Camera și microfonul sunt pornite</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Redare</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pauză</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Un site redă conținut media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..3c45e590bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ru/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Медиа</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Камера включена</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Микрофон включён</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Камера и микрофон включены</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Нажмите, чтобы открыть вкладку, использующую вашу камеру.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Нажмите, чтобы открыть вкладку, использующую ваш микрофон.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Нажмите, чтобы открыть вкладку, использующую ваш микрофон и камеру.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Напоминание: %1$s всё ещё использует вашу камеру. Нажмите, чтобы открыть вкладку.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Напоминание: %1$s всё ещё использует ваш микрофон. Нажмите, чтобы открыть вкладку</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Напоминание: %1$s всё ещё использует ваш микрофон. Нажмите, чтобы открыть вкладку.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Напоминание: %1$s всё ещё использует ваш микрофон и камеру. Нажмите, чтобы открыть вкладку</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Напоминание: %1$s всё ещё использует ваш микрофон и камеру. Нажмите, чтобы открыть вкладку.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Воспроизвести</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Приостановить</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Сайт воспроизводит звук и/или видео</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..25449c1766
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-sat/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ᱢᱤᱰᱤᱭᱟ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">ᱠᱮᱢᱨᱟ ᱪᱟᱹᱞᱩ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ᱢᱟᱭᱠᱨᱚᱯᱷᱚᱱ ᱪᱟᱹᱞᱩ ᱢᱮᱱᱟᱜ-ᱟ</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">ᱠᱮᱢᱨᱟ ᱟᱨ ᱢᱟᱭᱠᱨᱚᱯᱷᱚᱱ ᱚᱱ ᱢᱮᱱᱟᱜ-ᱟ</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">ᱟᱢᱟᱜ ᱠᱟᱢᱨᱟ ᱵᱮᱵᱷᱟᱨᱮᱫ ᱴᱮᱵᱽ ᱠᱷᱩᱞᱟᱹ ᱞᱟᱹᱜᱤᱫ ᱴᱤᱯᱟᱹᱣ ᱢᱮ ᱾</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">ᱟᱢᱟᱜ ᱢᱟᱭᱤᱠ ᱵᱮᱵᱷᱟᱨᱮᱫ ᱴᱮᱵᱽ ᱠᱷᱩᱞᱟᱹ ᱞᱟᱹᱜᱤᱫ ᱴᱤᱯᱟᱹᱣ ᱢᱮ ᱾</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">ᱟᱢᱟᱜ ᱢᱟᱭᱤᱠ ᱟᱨ ᱠᱮᱢᱨᱟ ᱵᱮᱵᱷᱟᱨᱮᱫ ᱴᱮᱵᱽ ᱠᱷᱩᱞᱟᱹ ᱞᱟᱹᱜᱤᱫ ᱴᱤᱯᱟᱹᱣ ᱢᱮ ᱾</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">ᱩᱭᱦᱟ.ᱨ ᱚᱪᱚ : %1$s ᱱᱤᱛᱦᱚᱸ ᱟᱢᱟᱜ ᱠᱮᱢᱨᱟ ᱵᱮᱵᱷᱟᱨ ᱮᱫᱟᱭ ᱾ ᱴᱮᱵᱽ ᱡᱷᱤᱡᱽ ᱞᱟᱹᱜᱤᱫ ᱴᱮᱵᱽ ᱨᱮ ᱚᱛᱟᱭ ᱢᱮ ᱾</string>
+
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">ᱩᱭᱦᱟ.ᱨ ᱚᱪᱚ : %1$s ᱱᱤᱛᱦᱚᱸ ᱟᱢᱟᱜ ᱢᱟᱭᱤᱠ ᱵᱮᱵᱷᱟᱨ ᱮᱫᱟᱭ ᱾ ᱴᱮᱵᱽ ᱡᱷᱤᱡᱽ ᱞᱟᱹᱜᱤᱫ ᱴᱮᱵᱽ ᱨᱮ ᱚᱛᱟᱭ ᱢᱮ ᱾</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">ᱩᱭᱦᱟ.ᱨ ᱚᱪᱚ : %1$s ᱱᱤᱛᱦᱚᱸ ᱟᱢᱟᱜ ᱢᱟᱭᱤᱠ ᱵᱮᱵᱷᱟᱨ ᱮᱫᱟᱭ ᱾ ᱴᱮᱵᱽ ᱡᱷᱤᱡᱽ ᱞᱟᱹᱜᱤᱫ ᱴᱮᱵᱽ ᱨᱮ ᱚᱛᱟᱭ ᱢᱮ ᱾</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">ᱩᱭᱦᱟ.ᱨ ᱚᱪᱚ : %1$s ᱱᱤᱛᱦᱚᱸ ᱟᱢᱟᱜ ᱢᱟᱭᱤᱠ ᱟᱨ ᱠᱮᱢᱨᱟ ᱵᱮᱵᱷᱟᱨ ᱮᱫᱟᱭ ᱾ ᱴᱮᱵᱽ ᱡᱷᱤᱡᱽ ᱞᱟᱹᱜᱤᱫ ᱴᱮᱵᱽ ᱨᱮ ᱚᱛᱟᱭ ᱢᱮ ᱾</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">ᱩᱭᱦᱟ.ᱨ ᱚᱪᱚ : %1$s ᱱᱤᱛᱦᱚᱸ ᱟᱢᱟᱜ ᱢᱟᱭᱤᱠ ᱟᱨ ᱠᱮᱢᱨᱟ ᱵᱮᱵᱷᱟᱨ ᱮᱫᱟᱭ ᱾ ᱴᱮᱵᱽ ᱡᱷᱤᱡᱽ ᱞᱟᱹᱜᱤᱫ ᱴᱮᱵᱽ ᱨᱮ ᱚᱛᱟᱭ ᱢᱮ ᱾</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">ᱮᱱᱮᱡ ᱢᱮ</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">ᱛᱷᱩᱠᱩᱢ ᱢᱮ</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ᱢᱤᱫᱴᱟᱝ ᱥᱟᱭᱤᱴ ᱢᱤᱰᱤᱭᱟ ᱮᱱᱮᱡ ᱫᱟᱭ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..6bce4ac7b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-sc/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Cuntenutos multimediales</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Càmera ativa</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Micròfonu ativu</string>
+
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Sa càmera e su micròfonu sunt ativos</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toca pro abèrrere s’ischeda chi est impreende sa càmera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toca pro abèrrere s’ischeda chi est impreende su micròfonu.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toca pro abèrrere s’ischeda chi est impreende sa càmera e su micròfonu.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Regorda: %1$s est ancora impreende sa càmera. Toca pro abèrrere s’ischeda.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Regorda: %1$s est ancora impreende su micròfonu. Toca pro abèrrere s’ischeda</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Regorda: %1$s est ancora impreende su micròfonu. Toca pro abèrrere s’ischeda.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Regorda: %1$s est ancora impreende sa càmera e su micròfonu. Toca pro abèrrere s’ischeda</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Regorda: %1$s est ancora impreende sa càmera e su micròfonu. Toca pro abèrrere s’ischeda.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Reprodue</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pàusa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Unu situ est riproduente cuntenutu multimediale</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..41edbc1ea9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-si/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">මාධ්‍ය</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">රූගතය සක්‍රියයි</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ශබ්දවාහිනිය සක්‍රියයි</string>
+
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">රූගතය හා ශබ්දවාහිනිය සක්‍රියයි</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">ඔබගේ රූගතය භාවිතා කරන පටිත්ත ඇරීමට තට්ටු කරන්න.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">ඔබගේ ශබ්දවාහිනිය භාවිතා කරන පටිත්ත ඇරීමට තට්ටු කරන්න.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">ඔබගේ ශබ්දවාහිනිය හා රූගතය භාවිතා කරන පටිත්ත ඇරීමට තට්ටු කරන්න.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">සටහන: %1$s තවමත් ඔබගේ රූගතය භාවිතා කරයි. පටිත්ත ඇරීමට තට්ටු කරන්න.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">සටහන: %1$s තවමත් ඔබගේ ශබ්දවාහිනිය භාවිතා කරයි. පටිත්ත ඇරීමට තට්ටු කරන්න</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">සටහන: %1$s තවමත් ඔබගේ ශබ්දවාහිනිය භාවිතා කරයි. පටිත්ත ඇරීමට තට්ටු කරන්න.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">සටහන: %1$s තවමත් ඔබගේ ශබ්දවාහිනිය හා රූගතය භාවිතා කරයි. පටිත්ත ඇරීමට තට්ටු කරන්න</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">සටහන: %1$s තවමත් ඔබගේ ශබ්දවාහිනිය හා රූගතය භාවිතා කරයි. පටිත්ත ඇරීමට තට්ටු කරන්න.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">වාදනය</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">විරාමයක්</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">අඩවියක් මාධ්‍ය වාදනය කරයි</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..a895411f72
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-sk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Médiá</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera je zapnutá</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofón je zapnutý</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera a mikrofón sú zapnuté</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Ťuknutím otvoríte kartu, ktorá používa vašu kameru.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Ťuknutím otvoríte kartu, ktorá používa váš mikrofón.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Ťuknutím otvoríte kartu, ktorá používa váš mikrofón a kameru.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Pripomenutie: %1$s stále používa vašu kameru. Ťuknutím otvoríte kartu.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Pripomenutie: %1$s stále používa váš mikrofón. Ťuknutím otvoríte kartu.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Pripomenutie: %1$s stále používa váš mikrofón. Ťuknutím otvoríte kartu.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Pripomenutie: %1$s stále používa váš mikrofón a kameru. Ťuknutím otvoríte kartu.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Pripomenutie: %1$s stále používa váš mikrofón a kameru. Ťuknutím otvoríte kartu.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Prehrať</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pozastaviť</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Stránka prehráva médiá</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..727fd7df6b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-skr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">میڈیا</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">کیمرہ چالو ہے</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">مائیکروفون چالو ہے</string>
+
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">کیمرہ تے مائیکروفون چالو ہن</string>
+
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">تُہاݙے کیمرے کوں استعمال کرݨ آلے ٹیب کوں کھولݨ کِیتے دباؤ۔</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">تُہاݙے مائیکرو فون کوں استعمال کرݨ آلے ٹیب کوں کھولݨ کِیتے دباؤ۔</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">تُہاݙے مائیکرو فون اَتے کیمرے کوں استعمال کرݨ آلے ٹیب کوں کھولݨ کِیتے دباؤ۔</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">یاد دہانی: %1$s ہالی وی تُہاݙا کیمرہ استعمال کرین٘دا پِیا ہِے۔ ٹیب کھولݨ کِیتے دباؤ۔</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">یاد دہانی: %1$s ہالی وی تُہاݙا مائیکرو فون استعمال کرین٘دا پِیا ہِے۔ ٹیب کھولݨ کِیتے دباؤ۔</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">یاد دہانی: %1$s ہالی وی تُہاݙا مائیکرو فون استعمال کرین٘دا پِیا ہِے۔ ٹیب کھولݨ کِیتے دباؤ۔</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">یاد دہانی: %1$s ہالی وی تُہاݙا مائیکرو فون اَتے کیمرہ استعمال کرین٘دا پِیا ہِے۔ ٹیب کھولݨ کِیتے دباؤ۔</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">یاد دہانی: %1$s ہالی وی تُہاݙا مائیکرو فون اَتے کیمرہ استعمال کرین٘دا پِیا ہِے۔ ٹیب کھولݨ کِیتے دباؤ۔</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">چلاؤ</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">ذرا روکو</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">سائٹ میڈیا چلیندی پئی ہے۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..8482100ccc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-sl/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Predstavnost</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera je vključena</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon je vključen</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera in mikrofon sta vključena</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tapnite, da odprete zavihek, ki uporablja kamero.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tapnite, da odprete zavihek, ki uporablja mikrofon.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tapnite, da odprete zavihek, ki uporablja mikrofon in kamero.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Opomnik: %1$s še vedno uporablja kamero. Tapnite, da odprete zavihek.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Opomnik: %1$s še vedno uporablja mikrofon. Tapnite, da odprete zavihek</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Opomnik: %1$s še vedno uporablja mikrofon. Tapnite, da odprete zavihek.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Opomnik: %1$s še vedno uporablja mikrofon in kamero. Tapnite, da odprete zavihek</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Opomnik: %1$s še vedno uporablja mikrofon in kamero. Tapnite, da odprete zavihek.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Predvajaj</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Premor</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Stran predvaja predstavnost</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..dc685d267b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-sq/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera është e hapur</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofoni është i hapur</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera dhe mikrofoni janë të hapur</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Prekeni, që të hapet skeda që po përdor kamerën tuaj.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Prekeni, që të hapet skeda që po përdor mikrofonin tuaj.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Prekeni, që të hapet skeda që po përdor mikrofonin dhe kamerën tuaj.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Kujtues: %1$s ende po përdor kamerën tuaj. Prekeni që të hapet skeda.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Kujtues: %1$s ende po përdor mikrofonin tuaj. Prekeni që të hapet skeda</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Kujtues: %1$s ende po përdor mikrofonin tuaj. Prekeni që të hapet skeda.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Kujtues: %1$s ende po përdor mikrofonin dhe skedën tuaj. Prekeni që të hapet skeda</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Kujtues: %1$s ende po përdor mikrofonin dhe skedën tuaj. Prekeni që të hapet skeda.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Luaje</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Ndalesë</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Një sajt po luan media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..e38818ee75
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-sr/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Мултимедија</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Камера је укључена</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Микрофон је укључен</string>
+
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Камера и микрофон су укључени</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Репродукуј</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Паузирај</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Страница репродукује мултимедију</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..36d6141e58
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-su/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Média</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kaméra hurung</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikropon hurung</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kaméra jeung mikropon hurung</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Toél pikeun muka tab anu maké kaméra anjeun.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Toél pikeun muka tab anu maké mikropon anjeun.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Toél pikeun muka tab anu maké mikropon jeung kaméra anjeun.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Panginget: %1$s maké kénéh kaméra anjeun. Toél pikeun muka tabna.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text">Panginget: %1$s maké kénéh mikropon anjeun. Toél pikeun muka tabna</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text">Panginget: %1$s maké kénéh mikropon jeung kaméra anjeun. Toél pikeun muka tabna</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Ulinkeun</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Reureuh</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Loka nuju maén média</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..e629afb3f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kameran är på</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofonen är på</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kameran och mikrofonen är på</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tryck för att öppna fliken som använder kameran.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tryck för att öppna fliken som använder mikrofonen.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tryck för att öppna fliken som använder din mikrofon och kamera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Påminnelse: %1$s använder fortfarande din kamera. Tryck här för att öppna fliken.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Påminnelse: %1$s använder fortfarande din mikrofon. Tryck här för att öppna fliken</string>
+
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Påminnelse: %1$s använder fortfarande din mikrofon. Tryck här för att öppna fliken.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Påminnelse: %1$s använder fortfarande din mikrofon och kamera. Tryck här för att öppna fliken</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Påminnelse: %1$s använder fortfarande din mikrofon och kamera. Tryck här för att öppna fliken.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Spela</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pausa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">En webbplats spelar media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..eeb18cf012
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ta/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ஊடகம்</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">படக்கருவி செயல்பாட்டில் உள்ளது</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ஒலிவாங்கி செயல்பாட்டில் உள்ளது</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">படக்கருவி மற்றும் ஒலிவாங்கி பயன்பாட்டில் உள்ளது</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">இயக்கு</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">இடைநிறுத்து</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">தளம் ஊடகத்தை இயக்கிக் கொண்டிருக்கிறது</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..caaa153ff5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-te/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">మాధ్యమాలు</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">కెమెరా నడుస్తోంది</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">మైక్రోఫోను నడుస్తోంది</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">కెమెరా, మైక్రోఫోను నడుస్తున్నాయి</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">ఆడించు</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">నిలుపు</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ఒక సైటు మాధ్యమాన్ని ఆడిస్తోంది</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..a725b8155e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-tg/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Расона</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Камера фаъол аст</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Микрофон фаъол аст</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Камера ва микрофон фаъол мебошанд</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Барои кушодани варақае, ки аз камераи шумо истифода мебарад, дар ин ҷой зер кунед.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Барои кушодани варақае, ки аз микрофони шумо истифода мебарад, дар ин ҷой зер кунед.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Барои кушодани варақае, ки аз микрофон ва камераи шумо истифода мебарад, дар ин ҷой зер кунед.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Ёдоварӣ: %1$s то ҳол аз камераи шумо истифода мебарад. Барои кушодани варақа дар ин ҷой зер кунед.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Ёдоварӣ: %1$s то ҳол аз микрофони шумо истифода мебарад. Барои кушодани варақа дар ин ҷой зер кунед.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Ёдоварӣ: %1$s то ҳол аз микрофони шумо истифода мебарад. Барои кушодани варақа дар ин ҷой зер кунед.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Ёдоварӣ: %1$s то ҳол аз микрофон ва камераи шумо истифода мебарад. Барои кушодани варақа дар ин ҷой зер кунед.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Ёдоварӣ: %1$s то ҳол аз микрофон ва камераи шумо истифода мебарад. Барои кушодани варақа дар ин ҷой зер кунед.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Пахш кардан</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Таваққуф кардан</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Сомона расонаро пахш карда истодааст</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..bae10ac0c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-th/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">สื่อ</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">กล้องเปิดอยู่</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">ไมโครโฟนเปิดอยู่</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">กล้องและไมโครโฟนเปิดอยู่</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">แตะเพื่อเปิดแท็บที่กำลังใช้กล้องของคุณ</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">แตะเพื่อเปิดแท็บที่กำลังใช้ไมโครโฟนของคุณ</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">แตะเพื่อเปิดแท็บที่กำลังใช้ไมโครโฟนและกล้องของคุณ</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">คำเตือน: %1$s ยังใช้กล้องของคุณอยู่ แตะเพื่อเปิดแท็บ</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">คำเตือน: %1$s ยังใช้ไมโครโฟนของคุณอยู่ แตะเพื่อเปิดแท็บ</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">คำเตือน: %1$s ยังใช้ไมโครโฟนของคุณอยู่ แตะเพื่อเปิดแท็บ</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">คำเตือน: %1$s ยังใช้ไมโครโฟนและกล้องของคุณอยู่ แตะเพื่อเปิดแท็บ</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">คำเตือน: %1$s ยังใช้ไมโครโฟนและกล้องของคุณอยู่ แตะเพื่อเปิดแท็บ</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">เล่น</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">หยุดชั่วคราว</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">ไซต์กำลังเล่นสื่อ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..947e1323ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-tl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Nakabukas ang camera</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Nakabukas ang mikropono</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Nakabukas ang camera at mikropono</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Paandarin</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">i-Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">May site na nagpapaandar ng media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..b4c583a831
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-tr/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Ortam</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera açık</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon açık</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera ve mikrofon açık</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Kameranızı kullanan sekmeyi açmak için dokunun.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Mikrofonunuzu kullanan sekmeyi açmak için dokunun.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Mikrofonunuzu ve kameranızı kullanan sekmeyi açmak için dokunun.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Anımsatma: %1$s hâlâ kameranızı kullanıyor. Sekmeyi açmak için dokunun.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Anımsatma: %1$s hâlâ mikrofonunuzu kullanıyor. Sekmeyi açmak için dokunun.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Anımsatma: %1$s hâlâ mikrofonunuzu kullanıyor. Sekmeyi açmak için dokunun.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Anımsatma: %1$s hâlâ mikrofonunuzu ve kameranızı kullanıyor. Sekmeyi açmak için dokunun.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Anımsatma: %1$s hâlâ mikrofonunuzu ve kameranızı kullanıyor. Sekmeyi açmak için dokunun.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Oynat</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Duraklat</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Bir site medya oynatıyor</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..8914665e6a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-trs/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Sa gīni\'io\' gā\'min\'</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Ngà nanûn sa nari ñanj dū\'uô\'</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Nga nanûn aga\' uxun nanèe</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Ngà nanûn sa nari ñanj dū\'uô\' ngà aga\' uxun nanèe</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Dūguachrá</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Dūnikïn\' akuan\'</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Huā \'ngō sitiô nari sa ni\'io\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..607ca4f88f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-tt/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Медиа</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Камера кабызылган</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Микрофон кабызылган</string>
+
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Камера һәм микрофон кабынган</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Уйнату</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Туктату</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Сайтта бер медиа уйный</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..ce30606e0c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Amidya</string>
+
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Yuɣ umikṛu</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Ɣer</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..87924ebdc3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ug/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">ۋاسىتە</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">كامېرا ئېچىلدى</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">مىكروفون ئېچىلدى</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">كامېرا ۋە مىكروفون ئوچۇق</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">چېكىلسە كامېرانى ئىشلىتىۋاتقان بەتكۈچنى ئاچىدۇ.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">چېكىلسە مىكروفوننى ئىشلىتىۋاتقان بەتكۈچنى ئاچىدۇ.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">چېكىلسە مىكروفون ۋە كامېرانى ئىشلىتىۋاتقان بەتكۈچنى ئاچىدۇ.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">ئەسكەرتىش: %1$s كامېرايىڭىزنى ئىشلىتىۋاتىدۇ. چېكىلسە بەتكۈچنى ئاچىدۇ.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">ئەسكەرتىش: %1$s مېكروفونىڭىزنى ئىشلىتىۋاتىدۇ. چېكىلسە بەتكۈچنى ئاچىدۇ</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">ئەسكەرتىش: %1$s مېكروفونىڭىزنى ئىشلىتىۋاتىدۇ. چېكىلسە بەتكۈچنى ئاچىدۇ.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">ئەسكەرتىش: %1$s مېكروفون ۋە كامېرايىڭىزنى ئىشلىتىۋاتىدۇ. چېكىلسە بەتكۈچنى ئاچىدۇ</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">ئەسكەرتىش: %1$s مېكروفون ۋە كامېرايىڭىزنى ئىشلىتىۋاتىدۇ. چېكىلسە بەتكۈچنى ئاچىدۇ.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">باشلاش</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">توختىتىش</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">بېكەت ۋاسىتە قويۇۋاتىدۇ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..b2b982a78e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-uk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Медіа</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Камера увімкнена</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Мікрофон увімкнений</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Камера та мікрофон увімкнені</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Торкніться, щоб відкрити вкладку, яка використовує вашу камеру.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Торкніться, щоб відкрити вкладку, яка використовує ваш мікрофон.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Торкніться, щоб відкрити вкладку, яка використовує ваші мікрофон і камеру.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Нагадування: %1$s досі використовує вашу камеру. Торкніться, щоб відкрити вкладку.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Нагадування: %1$s досі використовує ваш мікрофон. Торкніться, щоб відкрити вкладку.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Нагадування: %1$s досі використовує ваш мікрофон. Торкніться, щоб відкрити вкладку.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Нагадування: %1$s досі використовує ваші мікрофон і камеру. Торкніться, щоб відкрити вкладку.</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Нагадування: %1$s досі використовує ваші мікрофон і камеру. Торкніться, щоб відкрити вкладку.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Відтворити</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Призупинити</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Сайт відтворює медіа</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..d0b64b45f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-ur/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">میڈیا</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">کیمرہ چالو ہے</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">مائیکروفون چالو ہے</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">کیمرہ اور مائیکروفون چالو ہیں</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">چلائیں</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">توقف کریں</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">کوئی سائٹ میڈیا چلا رہی ہے</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..75a455e474
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-uz/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Kamera yoniq</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Mikrofon yoniq</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Kamera va mikrofon yoniq</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Boshlash</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pauza</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Saytda media qoʻyilmoqda</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..2918f0285b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-vec/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Ƚa machineta fotografica ƚa xe ativa</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">El microfono el xe ativo</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Ƚa teƚecamera e el microfono ƚa xe ativa</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Riproduxi</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Meti en pauxa</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">On sito el xe drio riprodure contenudi multimediaƚi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..7fa420995f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-vi/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Đa phương tiện</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Đã bật máy ảnh</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Đã bật micrô</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Đã bật máy ảnh và micrô</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Nhấn để mở thẻ đang sử dụng máy ảnh của bạn.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Nhấn để mở thẻ đang sử dụng micrô của bạn.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Nhấn để mở thẻ đang sử dụng micrô và máy ảnh của bạn.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Lời nhắc: %1$s vẫn đang sử dụng máy ảnh của bạn. Nhấn để mở thẻ.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Nhắc nhở: %1$s vẫn đang sử dụng micrô của bạn. Nhấn để mở thẻ</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Nhắc nhở: %1$s vẫn đang sử dụng micrô của bạn. Chạm để mở thẻ.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Nhắc nhở: %1$s vẫn đang sử dụng micrô và máy ảnh của bạn. Nhấn để mở thẻ</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Nhăc nhở: %1$s vẫn đang sử dụng micrô và máy ảnh của bạn. Chạm để mở thẻ.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Phát</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Tạm dừng</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Một trang web đang phát phương tiện</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..addb0ebe59
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-yo/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Ìkànnì</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Ẹ̀rọ̀-ìyàwọ̀rán wà ní títàn</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Ẹ̀rọ̀-ìgbóhùn wà ní títàn</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Ẹ̀rọ-ìyàwọ̀rán àti ẹ̀rọ-ìgbóhùn wà ní títàn</string>
+
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Bẹ̀rẹ̀</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Dúró-díẹ̀</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">Sáìtì kán tan ìkànnì</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..954fa795a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">媒体</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">摄像头已开</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">麦克风已开</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">摄像头和麦克风已开</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">点按可打开正在使用您相机的标签页。</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">点按可打开正在使用您麦克风的标签页。</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">点按可打开正在使用您麦克风和相机的标签页。</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">提醒:%1$s 仍在使用您的相机。点按可打开相关标签页。</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">提醒:%1$s 仍在使用您的麦克风。点按可打开相关标签页。</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">提醒:%1$s 仍在使用您的麦克风。点按可打开相关标签页。</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">提醒:%1$s 仍在使用您的麦克风和相机。点按可打开相关标签页。</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">提醒:%1$s 仍在使用您的麦克风和相机。点按可打开相关标签页。</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">播放</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">暂停</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">网站正在播放媒体</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..f2cc374801
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">媒體</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">已開啟攝影機</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">已開啟麥克風</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">已開啟攝影機與麥克風</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">點擊此處即可開啟正在使用您攝影機的分頁。</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">點擊此處即可開啟正在使用您麥克風的分頁。</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">點擊此處即可開啟正在使用您麥克風與攝影機的分頁。</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">提醒:%1$s 正在使用您的攝影機,點擊此處即可開啟該分頁。</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">提醒:%1$s 正在使用您的麥克風,點擊此處即可開啟該分頁。</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">提醒:%1$s 正在使用您的麥克風,點擊此處即可開啟該分頁。</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">提醒:%1$s 正在使用您的麥克風與攝影機,點擊此處即可開啟該分頁。</string>
+
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">提醒:%1$s 正在使用您的麥克風與攝影機,點擊此處即可開啟該分頁。</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">播放</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">暫停</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">有網站正在播放媒體內容</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/media/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..0e94f2df7a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/main/res/values/strings.xml
@@ -0,0 +1,43 @@
+<?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 xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Name of the "notification channel" used for displaying media notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_media_notification_channel">Media</string>
+
+ <!-- Title of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera">Camera is on</string>
+ <!-- Title of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone">Microphone is on</string>
+ <!-- Title of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone">Camera and microphone are on</string>
+
+ <!-- Text of notification shown when the device's camera is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_text">Tap to open the tab that’s using your camera.</string>
+ <!-- Text of notification shown when the device's microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_microphone_text">Tap to open the tab that’s using your microphone.</string>
+ <!-- Text of notification shown when the device's camera and microphone is shared with a website (WebRTC) -->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_text">Tap to open the tab that’s using your microphone and camera.</string>
+
+
+ <!-- Text of reminder notification shown when the device's camera is shared with a website (WebRTC). %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_reminder_text">Reminder: %1$s is still using your camera. Tap to open the tab.</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Reminder: %1$s is still using your microphone. Tap to open the tab</string>
+ <!-- Text of reminder notification shown when the device's microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_microphone_reminder_text_2">Reminder: %1$s is still using your microphone. Tap to open the tab.</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text" moz:RemovedIn="124" tools:ignore="UnusedResources">Reminder: %1$s is still using your microphone and camera. Tap to open the tab</string>
+ <!-- Text of reminder notification shown when the device's camera and microphone is shared with a website (WebRTC) %1$s is a placeholder that will be replaced by the app name-->
+ <string name="mozac_feature_media_sharing_camera_and_microphone_reminder_text_2">Reminder: %1$s is still using your microphone and camera. Tap to open the tab.</string>
+
+ <!--This is the title of the "play" action media shown in the media notification while web content is playing media. Clicking it will resume playing paused media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_play">Play</string>
+
+ <!--This is the title of the "pause" action shown in the media notification while web content is playing media. Clicking it will pause currently playing media. On most modern Android system only an icon is visible. But screen readers may read this title. -->
+ <string name="mozac_feature_media_notification_action_pause">Pause</string>
+
+ <!-- Neutral title of the media notification for when a website that is open in private mode. -->
+ <string name="mozac_feature_media_notification_private_mode">A site is playing media</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/MediaSessionFeatureTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/MediaSessionFeatureTest.kt
new file mode 100644
index 0000000000..f42ff3b5e6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/MediaSessionFeatureTest.kt
@@ -0,0 +1,358 @@
+/* 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.media
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.MediaSessionState
+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.mediasession.MediaSession.PlaybackState
+import mozilla.components.feature.media.service.MediaServiceBinder
+import mozilla.components.feature.media.service.MediaSessionDelegate
+import mozilla.components.feature.media.service.MediaSessionServiceDelegate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MediaSessionFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `WHEN the feature starts THEN it starts observing the store`() {
+ val store: BrowserStore = mock()
+ val feature = MediaSessionFeature(
+ mock(),
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ assertNull(feature.scope)
+
+ feature.start()
+
+ assertNotNull(feature.scope)
+ }
+
+ @Test
+ fun `GIVEN a started feature WHEN it is stopped THEN the store is not observed for updates anymore`() {
+ val feature = MediaSessionFeature(
+ mock(),
+ MediaSessionServiceDelegate::class.java,
+ mock(),
+ )
+ feature.scope = CoroutineScope(Dispatchers.Default)
+
+ feature.stop()
+
+ assertNull(feature.scope)
+ }
+
+ @Test
+ fun `WHEN the media service is bound THEN store this and show the current media playing status`() {
+ val mediaTab = getMediaTab(PlaybackState.PLAYING)
+ val initialState = BrowserState(tabs = listOf(mediaTab))
+ val store = BrowserStore(initialState)
+ val mediaServiceClass = MediaSessionServiceDelegate::class.java
+ val feature = MediaSessionFeature(
+ mock(),
+ mediaServiceClass,
+ store,
+ )
+ val mediaService: MediaSessionServiceDelegate = mock()
+ val binder = MediaServiceBinder(mediaService)
+
+ feature.mediaServiceConnection.onServiceConnected(mock(), binder)
+
+ assertEquals(mediaService, feature.mediaService)
+ verify(feature.mediaService)!!.handleMediaPlaying(mediaTab)
+ }
+
+ @Test
+ fun `GIVEN media service is bound WHEN the service is disconnected THEN cleanup local properties`() {
+ val mediaService: ComponentName = mock()
+ val feature = MediaSessionFeature(mock(), mediaService.javaClass, mock())
+ feature.mediaService = mock()
+
+ feature.mediaServiceConnection.onServiceDisconnected(mediaService)
+
+ assertNull(feature.mediaService)
+ }
+
+ @Test
+ fun `GIVEN feature is started but media service is not WHEN media starts playing THEN bind to the media service and show the playing status`() {
+ val mockApplicationContext: Context = mock()
+ val mediaTab = getMediaTab(PlaybackState.PLAYING)
+ val initialState = BrowserState(tabs = listOf(mediaTab))
+ val store = BrowserStore(initialState)
+ val mediaServiceClass = MediaSessionServiceDelegate::class.java
+ val feature = MediaSessionFeature(
+ mockApplicationContext,
+ mediaServiceClass,
+ store,
+ )
+ doReturn(true).`when`(mockApplicationContext).bindService(
+ any<Intent>(),
+ any(),
+ anyInt(),
+ )
+ val mediaServiceIntentCaptor = argumentCaptor<Intent>()
+
+ feature.start()
+
+ verify(mockApplicationContext).bindService(
+ mediaServiceIntentCaptor.capture(),
+ any(),
+ eq(Context.BIND_AUTO_CREATE),
+ )
+ assertEquals(mediaServiceClass.name, mediaServiceIntentCaptor.value.component!!.className)
+ }
+
+ @Test
+ fun `GIVEN feature and media service are started WHEN media starts playing in a normal tab THEN handle showing the new playing status`() {
+ val mediaTab = getMediaTab(PlaybackState.PLAYING)
+ val initialState = BrowserState(
+ tabs = listOf(mediaTab),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mock(),
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ feature.mediaService = mock()
+
+ feature.start()
+
+ verify(feature.mediaService)!!.handleMediaPlaying(mediaTab)
+ }
+
+ @Test
+ fun `GIVEN feature and media service are started WHEN media starts playing in a custom tab THEN handle showing the new playing status`() {
+ val mediaTab = getCustomTabWithMedia(PlaybackState.PLAYING)
+ val initialState = BrowserState(
+ customTabs = listOf(mediaTab),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mock(),
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ feature.mediaService = mock()
+
+ feature.start()
+
+ verify(feature.mediaService)!!.handleMediaPlaying(mediaTab)
+ }
+
+ @Test
+ fun `GIVEN feature and media service are started WHEN media is paused in a normal tab THEN handle showing the new playing status`() {
+ val mediaTab = getMediaTab(PlaybackState.PAUSED)
+ val initialState = BrowserState(
+ tabs = listOf(mediaTab),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mock(),
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ feature.mediaService = mock()
+
+ feature.start()
+
+ verify(feature.mediaService)!!.handleMediaPaused(mediaTab)
+ }
+
+ @Test
+ fun `GIVEN feature and media service are started WHEN media is paused in a custom tab THEN handle showing the new playing status`() {
+ val mediaTab = getCustomTabWithMedia(PlaybackState.PAUSED)
+ val initialState = BrowserState(
+ customTabs = listOf(mediaTab),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mock(),
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ feature.mediaService = mock()
+
+ feature.start()
+
+ verify(feature.mediaService)!!.handleMediaPaused(mediaTab)
+ }
+
+ @Test
+ fun `GIVEN feature and media service are started WHEN media is stopped in a normal tab THEN handle showing the new playing status`() {
+ val mediaTab = getMediaTab(PlaybackState.STOPPED)
+ val initialState = BrowserState(
+ tabs = listOf(mediaTab),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mock(),
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ feature.mediaService = mock()
+
+ feature.start()
+
+ verify(feature.mediaService)!!.handleMediaStopped(mediaTab)
+ }
+
+ @Test
+ fun `GIVEN feature and media service are started WHEN media is stopped in a custom tab THEN handle showing the new playing status`() {
+ val mediaTab = getCustomTabWithMedia(PlaybackState.STOPPED)
+ val initialState = BrowserState(
+ customTabs = listOf(mediaTab),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mock(),
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ feature.mediaService = mock()
+
+ feature.start()
+
+ verify(feature.mediaService)!!.handleMediaStopped(mediaTab)
+ }
+
+ @Test
+ fun `GIVEN feature and media service are started WHEN the media status is unknown THEN disconnect the media service and cleanup`() {
+ val mockApplicationContext: Context = mock()
+ val mediaTab = getMediaTab(PlaybackState.UNKNOWN)
+ val initialState = BrowserState(
+ tabs = listOf(mediaTab),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mockApplicationContext,
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ val mediaService: MediaSessionDelegate = mock()
+ feature.mediaService = mediaService
+
+ feature.start()
+
+ verify(mediaService).handleNoMedia()
+ verify(mockApplicationContext).unbindService(feature.mediaServiceConnection)
+ assertNull(feature.mediaService)
+ }
+
+ @Test
+ fun `GIVEN feature and media service are started WHEN there is no media tab THEN stop the media service and cleanup`() {
+ val mockApplicationContext: Context = mock()
+ val initialState = BrowserState()
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mockApplicationContext,
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ val mediaService: MediaSessionDelegate = mock()
+ feature.mediaService = mediaService
+
+ feature.start()
+
+ verify(mediaService).handleNoMedia()
+ verify(mockApplicationContext).unbindService(feature.mediaServiceConnection)
+ assertNull(feature.mediaService)
+ }
+
+ @Test
+ fun `GIVEN a normal tab is playing media WHEN media is deactivated THEN stop the media service and cleanup`() {
+ val mockApplicationContext: Context = mock()
+ val mediaTab = getMediaTab(PlaybackState.PLAYING)
+ val initialState = BrowserState(
+ tabs = listOf(mediaTab),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mockApplicationContext,
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ val mediaService: MediaSessionDelegate = mock()
+ feature.mediaService = mediaService
+ feature.start()
+
+ store.dispatch(MediaSessionAction.DeactivatedMediaSessionAction(store.state.tabs[0].id))
+ store.waitUntilIdle()
+
+ verify(mediaService).handleNoMedia()
+ verify(mockApplicationContext).unbindService(feature.mediaServiceConnection)
+ assertNull(feature.mediaService)
+ }
+
+ @Test
+ fun `GIVEN a custom tab is playing media WHEN media is deactivated THEN stop the media service and cleanup`() {
+ val mockApplicationContext: Context = mock()
+ val mediaTab = getMediaTab(PlaybackState.UNKNOWN)
+ val customTabWithMedia = getCustomTabWithMedia(PlaybackState.PLAYING)
+ val initialState = BrowserState(
+ tabs = listOf(mediaTab),
+ customTabs = listOf(customTabWithMedia),
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFeature(
+ mockApplicationContext,
+ MediaSessionServiceDelegate::class.java,
+ store,
+ )
+ val mediaService: MediaSessionDelegate = mock()
+ feature.mediaService = mediaService
+ feature.start()
+
+ store.dispatch(MediaSessionAction.DeactivatedMediaSessionAction(store.state.customTabs[0].id))
+ store.waitUntilIdle()
+
+ verify(mediaService).handleNoMedia()
+ verify(mockApplicationContext).unbindService(feature.mediaServiceConnection)
+ assertNull(feature.mediaService)
+ }
+
+ private fun getMediaTab(playbackState: PlaybackState = PlaybackState.PLAYING) = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ playbackState = playbackState,
+ ),
+ )
+
+ private fun getCustomTabWithMedia(playbackState: PlaybackState = PlaybackState.PLAYING) = createCustomTab(
+ "https://www.mozilla.org",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ playbackState = playbackState,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/ext/SessionStateKtTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/ext/SessionStateKtTest.kt
new file mode 100644
index 0000000000..34ecf97c0b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/ext/SessionStateKtTest.kt
@@ -0,0 +1,188 @@
+/* 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.media.ext
+
+import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SessionStateKtTest {
+ private val bitmap: Bitmap = mock()
+ private val getArtwork: (suspend () -> Bitmap?) = { bitmap }
+ private val getArtworkNull: (suspend () -> Bitmap?) = { null }
+
+ @Test
+ fun `getNonPrivateIcon returns null when in private mode`() = runTest {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(true)
+
+ val result = sessionState.getNonPrivateIcon(getArtwork)
+
+ assertEquals(result, null)
+ }
+
+ @Test
+ fun `getNonPrivateIcon returns bitmap when not in private mode`() = runTest {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(false)
+
+ val result = sessionState.getNonPrivateIcon(getArtwork)
+
+ assertEquals(result, bitmap)
+ }
+
+ @Test
+ fun `getNonPrivateIcon returns content icon when not in private mode`() = runTest {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+ val icon: Bitmap = mock()
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(false)
+ whenever(contentState.icon).thenReturn(icon)
+
+ val result = sessionState.getNonPrivateIcon(null)
+
+ assertEquals(result, icon)
+ }
+
+ @Test
+ fun `getNonPrivateIcon returns content icon when getArtwork return null`() = runTest {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+ val icon: Bitmap = mock()
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(false)
+ whenever(contentState.icon).thenReturn(icon)
+
+ val result = sessionState.getNonPrivateIcon(getArtworkNull)
+
+ assertEquals(result, icon)
+ }
+
+ @Test
+ fun `getTitleOrUrl returns null when in private mode`() {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(true)
+
+ val result = sessionState.getTitleOrUrl(testContext, "test")
+
+ assertEquals(result, "A site is playing media")
+ }
+
+ @Test
+ fun `getTitleOrUrl returns metadata title when not in private mode`() {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(false)
+
+ val result = sessionState.getTitleOrUrl(testContext, "test")
+
+ assertEquals(result, "test")
+ }
+
+ @Test
+ fun `getTitleOrUrl returns title when not in private mode`() {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+ val contentTitle = "content title"
+ val contentUrl = "content url"
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(false)
+ whenever(contentState.title).thenReturn(contentTitle)
+ whenever(contentState.url).thenReturn(contentUrl)
+
+ val result = sessionState.getTitleOrUrl(testContext, null)
+
+ assertEquals(result, contentTitle)
+ }
+
+ @Test
+ fun `getTitleOrUrl returns url when not in private mode`() {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+ val contentTitle = ""
+ val contentUrl = "content url"
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(false)
+ whenever(contentState.title).thenReturn(contentTitle)
+ whenever(contentState.url).thenReturn(contentUrl)
+
+ val result = sessionState.getTitleOrUrl(testContext, null)
+
+ assertEquals(result, contentUrl)
+ }
+
+ @Test
+ fun `getArtistOrUrl returns artist when not in private mode`() {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+ val artist = "test artist"
+ val contentUrl = "content url"
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(false)
+ whenever(contentState.url).thenReturn(contentUrl)
+
+ val result = sessionState.getArtistOrUrl(artist)
+
+ assertEquals(result, artist)
+ }
+
+ @Test
+ fun `getArtistOrUrl returns null when in private mode`() {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+ val artist = "test artist"
+ val contentUrl = "content url"
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(true)
+ whenever(contentState.url).thenReturn(contentUrl)
+
+ val result = sessionState.getArtistOrUrl(artist)
+
+ assertEquals(result, "")
+ }
+
+ @Test
+ fun `getArtistOrUrl returns url when not in private mode and no artist`() {
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+ val artist = null
+ val contentUrl = "content url"
+
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(contentState.private).thenReturn(false)
+ whenever(contentState.url).thenReturn(contentUrl)
+
+ val result = sessionState.getArtistOrUrl(artist)
+
+ assertEquals(result, contentUrl)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/focus/AudioFocusTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/focus/AudioFocusTest.kt
new file mode 100644
index 0000000000..c206d99520
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/focus/AudioFocusTest.kt
@@ -0,0 +1,363 @@
+/* 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.media.focus
+
+import android.media.AudioManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.feature.media.service.AbstractMediaSessionService
+import mozilla.components.feature.media.service.MediaSessionServiceDelegate
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(AndroidJUnit4::class)
+class AudioFocusTest {
+ private lateinit var audioManager: AudioManager
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setUp() {
+ audioManager = mock()
+ }
+
+ @Test
+ fun `Successful request will not change media session in state`() {
+ doReturn(AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
+ .`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+
+ verify(audioManager).requestAudioFocus(any())
+ verifyNoMoreInteractions(mediaSessionState.controller)
+ }
+
+ @Test
+ fun `Failed request will pause media session`() {
+ doReturn(AudioManager.AUDIOFOCUS_REQUEST_FAILED)
+ .`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+
+ verify(audioManager).requestAudioFocus(any())
+ verify(mediaSessionState.controller).pause()
+ }
+
+ @Test
+ fun `Delayed request will pause media`() {
+ doReturn(AudioManager.AUDIOFOCUS_REQUEST_DELAYED)
+ .`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+
+ verify(audioManager).requestAudioFocus(any())
+ verify(mediaSessionState.controller).pause()
+ }
+
+ @Test
+ fun `Will pause and resume playing media on and after transient focus loss`() {
+ doReturn(AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
+ .`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+
+ verify(audioManager).requestAudioFocus(any())
+ verifyNoMoreInteractions(mediaSessionState.controller)
+
+ audioFocus.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
+
+ verify(mediaSessionState.controller).pause()
+ verifyNoMoreInteractions(mediaSessionState.controller)
+
+ audioFocus.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN)
+
+ verify(mediaSessionState.controller).play()
+ verifyNoMoreInteractions(mediaSessionState.controller)
+ }
+
+ @Test
+ fun `Will not resume paused media after transient focus loss`() {
+ doReturn(AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
+ .`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PAUSED,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+
+ verify(audioManager).requestAudioFocus(any())
+ verifyNoMoreInteractions(mediaSessionState.controller)
+
+ audioFocus.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
+
+ verify(mediaSessionState.controller).pause()
+ verifyNoMoreInteractions(mediaSessionState.controller)
+
+ audioFocus.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN)
+
+ verify(mediaSessionState.controller, never()).play()
+ verifyNoMoreInteractions(mediaSessionState.controller)
+ }
+
+ @Test
+ fun `Will resume media sessio nplayback when gaining focus after being delayed`() {
+ doReturn(AudioManager.AUDIOFOCUS_REQUEST_DELAYED)
+ .`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+
+ verify(audioManager).requestAudioFocus(any())
+ verify(mediaSessionState.controller).pause()
+
+ audioFocus.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN)
+
+ verify(mediaSessionState.controller).play()
+ verifyNoMoreInteractions(mediaSessionState.controller)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `An unknown audio focus response will throw an exception`() {
+ doReturn(-1).`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+ }
+
+ @Test
+ fun `An unknown focus change event will be ignored`() {
+ doReturn(AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
+ .`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+
+ verify(audioManager).requestAudioFocus(any())
+ verifyNoMoreInteractions(mediaSessionState.controller)
+
+ audioFocus.onAudioFocusChange(999)
+ verifyNoMoreInteractions(mediaSessionState.controller)
+ }
+
+ @Test
+ fun `An audio focus loss will pause media and regain will not resume automatically`() {
+ doReturn(AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
+ .`when`(audioManager).requestAudioFocus(any())
+
+ val controller: MediaSession.Controller = mock()
+ val mediaSessionState = MediaSessionState(
+ controller,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ )
+ val tabSession = createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = mediaSessionState,
+ )
+ val initialState = BrowserState(
+ tabs = listOf(tabSession),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, notificationsDelegate)
+
+ delegate.onCreate()
+
+ val audioFocus = AudioFocus(audioManager, store)
+ audioFocus.request(tabSession.id)
+
+ verify(audioManager).requestAudioFocus(any())
+ verifyNoMoreInteractions(mediaSessionState.controller)
+
+ audioFocus.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS)
+
+ verify(mediaSessionState.controller).pause()
+ verifyNoMoreInteractions(mediaSessionState.controller)
+
+ audioFocus.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN)
+ verify(mediaSessionState.controller, never()).play()
+ verifyNoMoreInteractions(mediaSessionState.controller)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeatureTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeatureTest.kt
new file mode 100644
index 0000000000..b6f055af7d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeatureTest.kt
@@ -0,0 +1,479 @@
+/* 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.media.fullscreen
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import android.os.Build
+import android.view.Window
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.MediaSessionState
+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.mediasession.MediaSession
+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 mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class MediaSessionFullscreenFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN the currently selected tab is not in fullscreen WHEN the feature is running THEN orientation is set to default`() {
+ val activity: Activity = mock()
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = false,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+
+ feature.start()
+
+ verify(activity).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER)
+ }
+
+ @Test
+ fun `GIVEN the currently selected tab plays portrait media WHEN the feature is running THEN orientation is set to portrait`() {
+ val activity: Activity = mock()
+ val window: Window = mock()
+ whenever(activity.window).thenReturn(window)
+
+ val elementMetadata = MediaSession.ElementMetadata(width = 360, height = 640)
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+
+ feature.start()
+
+ verify(activity).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT)
+ }
+
+ @Test
+ fun `GIVEN the currently selected tab plays landscape media WHEN it enters fullscreen THEN set orientation to landscape`() {
+ val activity: Activity = mock()
+ val window: Window = mock()
+ whenever(activity.window).thenReturn(window)
+
+ val elementMetadata = MediaSession.ElementMetadata(width = 640, height = 360)
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+
+ feature.start()
+
+ verify(activity).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `GIVEN the currently selected tab plays landscape media WHEN it enters pip mode THEN set orientation to unspecified`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+
+ feature.start()
+ activity.enterPictureInPictureMode()
+ store.waitUntilIdle()
+
+ assertTrue(activity.isInPictureInPictureMode)
+ store.dispatch(ContentAction.PictureInPictureChangedAction("tab1", true))
+ store.waitUntilIdle()
+
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `GIVEN the currently selected tab is in pip mode WHEN an external intent arrives THEN set orientation to default`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+
+ feature.start()
+ activity.enterPictureInPictureMode()
+ store.waitUntilIdle()
+ store.dispatch(ContentAction.PictureInPictureChangedAction("tab1", true))
+ store.waitUntilIdle()
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation)
+
+ val tab2 = createTab(
+ url = "https://firefox.com",
+ id = "tab2",
+ )
+ store.dispatch(TabListAction.AddTabAction(tab2, select = true))
+ store.dispatch(
+ MediaSessionAction.UpdateMediaFullscreenAction(
+ store.state.tabs[0].id,
+ false,
+ MediaSession.ElementMetadata(),
+ ),
+ )
+ store.waitUntilIdle()
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_USER, activity.requestedOrientation)
+ assertEquals(tab2.id, store.state.selectedTabId)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `GIVEN the currently selected tab is in pip mode WHEN it exits pip mode THEN set orientation to default`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+
+ feature.start()
+ activity.enterPictureInPictureMode()
+ store.waitUntilIdle()
+
+ assertTrue(activity.isInPictureInPictureMode)
+ store.dispatch(ContentAction.PictureInPictureChangedAction("tab1", true))
+ store.waitUntilIdle()
+
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation)
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaFullscreenAction(
+ store.state.tabs[0].id,
+ false,
+ MediaSession.ElementMetadata(),
+ ),
+ )
+ store.waitUntilIdle()
+
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_USER, activity.requestedOrientation)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `GIVEN the currently selected tab is in pip mode WHEN a custom tab loads THEN display custom tab in device's current orientation`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+
+ feature.start()
+ activity.enterPictureInPictureMode()
+ store.waitUntilIdle()
+
+ store.dispatch(ContentAction.PictureInPictureChangedAction("tab1", true))
+ store.waitUntilIdle()
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation)
+
+ val customTab = createCustomTab(
+ "https://www.mozilla.org",
+ source = SessionState.Source.Internal.CustomTab,
+ id = "tab2",
+ )
+ store.dispatch(CustomTabListAction.AddCustomTabAction(customTab)).joinBlocking()
+ val externalActivity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ assertEquals(1, store.state.customTabs.size)
+ store.waitUntilIdle()
+ val featureForExternalAppBrowser = MediaSessionFullscreenFeature(
+ externalActivity,
+ store,
+ "tab2",
+ )
+ featureForExternalAppBrowser.start()
+
+ assertNotEquals(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, externalActivity.requestedOrientation)
+ }
+
+ @Test
+ fun `GIVEN the selected tab in fullscreen mode WHEN the media is paused or stopped THEN release the wake lock of the device`() {
+ val activity: Activity = mock()
+ val window: Window = mock()
+
+ whenever(activity.window).thenReturn(window)
+
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+ feature.start()
+ verify(activity.window).addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+ store.dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction("tab1", MediaSession.PlaybackState.PAUSED))
+ store.waitUntilIdle()
+ verify(activity.window).clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+ clearInvocations(activity.window)
+
+ store.dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction("tab1", MediaSession.PlaybackState.PLAYING))
+ store.waitUntilIdle()
+ verify(activity.window).addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+ store.dispatch(MediaSessionAction.DeactivatedMediaSessionAction("tab1"))
+ store.waitUntilIdle()
+ verify(activity.window).clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+
+ @Test
+ fun `GIVEN the selected tab is not in fullscreen mode WHEN it enters fullscreen THEN lock the wake lock of the device`() {
+ val activity: Activity = mock()
+ val window: Window = mock()
+
+ whenever(activity.window).thenReturn(window)
+
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = false,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+ feature.start()
+ verify(activity.window, never()).addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+ store.dispatch(MediaSessionAction.UpdateMediaFullscreenAction("tab1", true, elementMetadata))
+ store.waitUntilIdle()
+ verify(activity.window).addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+ clearInvocations(activity.window)
+
+ store.dispatch(MediaSessionAction.UpdateMediaFullscreenAction("tab1", false, elementMetadata))
+ store.waitUntilIdle()
+ verify(activity.window).clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+
+ @Test
+ fun `GIVEN the selected tab in fullscreen mode WHEN the active tab is changed to no media tab THEN release the wake lock of the device`() {
+ val activity: Activity = mock()
+ val window: Window = mock()
+
+ whenever(activity.window).thenReturn(window)
+
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ )
+ val store = BrowserStore(initialState)
+
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store,
+ null,
+ )
+ feature.start()
+ verify(activity.window).addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+ val tab2 = createTab(
+ url = "https://firefox.com",
+ id = "tab2",
+ )
+ clearInvocations(activity.window)
+ store.dispatch(TabListAction.AddTabAction(tab2, select = true))
+ store.dispatch(MediaSessionAction.UpdateMediaFullscreenAction(store.state.tabs[0].id, false, elementMetadata))
+ store.waitUntilIdle()
+ verify(activity.window).clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ assertEquals(tab2.id, store.state.selectedTabId)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/middleware/LastMediaAccessMiddlewareTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/middleware/LastMediaAccessMiddlewareTest.kt
new file mode 100644
index 0000000000..347ad5a6db
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/middleware/LastMediaAccessMiddlewareTest.kt
@@ -0,0 +1,287 @@
+/* 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.media.middleware
+
+import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.LastMediaAccessState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LastMediaAccessMiddlewareTest {
+
+ @Test
+ fun `GIVEN a normal tab WHEN media started playing THEN then lastMediaAccess is updated`() {
+ val mediaTabId = "42"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(content = ContentState("https://mozilla.org/1", private = true)),
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = false),
+ id = mediaTabId,
+ ),
+ TabSessionState(content = ContentState("https://mozilla.org/3", private = false)),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction(mediaTabId, MediaSession.PlaybackState.PLAYING))
+ .joinBlocking()
+
+ val updatedMediaState = store.state.tabs[1].lastMediaAccessState
+ assertTrue(
+ "expected lastMediaAccess (${updatedMediaState.lastMediaAccess}) > 0",
+ updatedMediaState.lastMediaAccess > 0,
+ )
+ assertEquals(mediaTabUrl, updatedMediaState.lastMediaUrl)
+ }
+
+ @Test
+ fun `GIVEN a private tab WHEN media started playing THEN then lastMediaAccess is updated`() {
+ val mediaTabId = "43"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(content = ContentState("https://mozilla.org/1", private = true)),
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = true),
+ id = mediaTabId,
+ ),
+ TabSessionState(content = ContentState("https://mozilla.org/3", private = false)),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction(mediaTabId, MediaSession.PlaybackState.PLAYING))
+ .joinBlocking()
+
+ val updatedMediaState = store.state.tabs[1].lastMediaAccessState
+ assertTrue(
+ "expected lastMediaAccess (${updatedMediaState.lastMediaAccess}) > 0",
+ updatedMediaState.lastMediaAccess > 0,
+ )
+ assertEquals(mediaTabUrl, updatedMediaState.lastMediaUrl)
+ }
+
+ @Test
+ fun `GIVEN a normal tab WHEN media is paused THEN then lastMediaAccess is not changed`() {
+ val mediaTabId = "42"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = false),
+ id = mediaTabId,
+ lastMediaAccessState = LastMediaAccessState(mediaTabUrl, 222),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction(mediaTabId, MediaSession.PlaybackState.PAUSED))
+ .joinBlocking()
+
+ assertEquals(222, store.state.tabs[0].lastMediaAccessState.lastMediaAccess)
+ }
+
+ @Test
+ fun `GIVEN a private tab WHEN media is paused THEN then lastMediaAccess is not changed`() {
+ val mediaTabId = "43"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = true),
+ id = mediaTabId,
+ lastMediaAccessState = LastMediaAccessState(mediaTabUrl, 333),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction(mediaTabId, MediaSession.PlaybackState.PAUSED))
+ .joinBlocking()
+
+ assertEquals(333, store.state.tabs[0].lastMediaAccessState.lastMediaAccess)
+ }
+
+ @Test
+ fun `GIVEN a normal tab WHEN media is stopped THEN then lastMediaAccess is not changed`() {
+ val mediaTabId = "42"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = false),
+ id = mediaTabId,
+ lastMediaAccessState = LastMediaAccessState(mediaTabUrl, 222),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction(mediaTabId, MediaSession.PlaybackState.STOPPED))
+ .joinBlocking()
+
+ assertEquals(222, store.state.tabs[0].lastMediaAccessState.lastMediaAccess)
+ }
+
+ @Test
+ fun `GIVEN a private tab WHEN media is stopped THEN then lastMediaAccess is not changed`() {
+ val mediaTabId = "43"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = true),
+ id = mediaTabId,
+ lastMediaAccessState = LastMediaAccessState(mediaTabUrl, 333),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction(mediaTabId, MediaSession.PlaybackState.STOPPED))
+ .joinBlocking()
+
+ assertEquals(333, store.state.tabs[0].lastMediaAccessState.lastMediaAccess)
+ }
+
+ @Test
+ fun `GIVEN a normal tab WHEN media status is unknown THEN then lastMediaAccess is not changed`() {
+ val mediaTabId = "42"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = false),
+ id = mediaTabId,
+ lastMediaAccessState = LastMediaAccessState(mediaTabUrl, 222),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction(mediaTabId, MediaSession.PlaybackState.UNKNOWN))
+ .joinBlocking()
+
+ assertEquals(222, store.state.tabs[0].lastMediaAccessState.lastMediaAccess)
+ }
+
+ @Test
+ fun `GIVEN a private tab WHEN media status is unknown THEN then lastMediaAccess is not changed`() {
+ val mediaTabId = "43"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = true),
+ id = mediaTabId,
+ lastMediaAccessState = LastMediaAccessState(mediaTabUrl, 333),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.UpdateMediaPlaybackStateAction(mediaTabId, MediaSession.PlaybackState.UNKNOWN))
+ .joinBlocking()
+
+ assertEquals(333, store.state.tabs[0].lastMediaAccessState.lastMediaAccess)
+ }
+
+ @Test
+ fun `GIVEN lastMediaAccess is set for a normal tab WHEN media session is deactivated THEN reset mediaSessionActive to false`() {
+ val mediaTabId = "42"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = false),
+ id = mediaTabId,
+ lastMediaAccessState = LastMediaAccessState(mediaTabUrl, 222, true),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.DeactivatedMediaSessionAction(mediaTabId))
+ .joinBlocking()
+
+ assertEquals(mediaTabUrl, store.state.tabs[0].lastMediaAccessState.lastMediaUrl)
+ assertEquals(222, store.state.tabs[0].lastMediaAccessState.lastMediaAccess)
+ assertFalse(store.state.tabs[0].lastMediaAccessState.mediaSessionActive)
+ }
+
+ @Test
+ fun `GIVEN lastMediaAccess is set for a private tab WHEN media session is deactivated THEN reset lastMediaAccess to 0`() {
+ val mediaTabId = "43"
+ val mediaTabUrl = "https://mozilla.org/2"
+ val browserState = BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ content = ContentState(mediaTabUrl, private = true),
+ id = mediaTabId,
+ lastMediaAccessState = LastMediaAccessState(mediaTabUrl, 333, true),
+ ),
+ ),
+ )
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(LastMediaAccessMiddleware()),
+ )
+
+ store
+ .dispatch(MediaSessionAction.DeactivatedMediaSessionAction(mediaTabId))
+ .joinBlocking()
+
+ assertEquals(mediaTabUrl, store.state.tabs[0].lastMediaAccessState.lastMediaUrl)
+ assertEquals(333, store.state.tabs[0].lastMediaAccessState.lastMediaAccess)
+ assertFalse(store.state.tabs[0].lastMediaAccessState.mediaSessionActive)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/middleware/RecordingDevicesMiddlewareTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/middleware/RecordingDevicesMiddlewareTest.kt
new file mode 100644
index 0000000000..36916a49b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/middleware/RecordingDevicesMiddlewareTest.kt
@@ -0,0 +1,138 @@
+/* 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.media.middleware
+
+import android.app.NotificationManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.media.RecordingDevice
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.robolectric.Shadows
+
+@RunWith(AndroidJUnit4::class)
+class RecordingDevicesMiddlewareTest {
+ private lateinit var notificationsDelegate: NotificationsDelegate
+
+ @Before
+ fun setup() {
+ // Prepare the PackageManager to answer getLaunchIntentForPackage call.
+ val applicationManager = Shadows.shadowOf(testContext.packageManager)
+
+ val activityComponent = ComponentName(testContext.packageName, "Test")
+ applicationManager.addActivityIfNotPresent(activityComponent)
+
+ applicationManager.addIntentFilterForActivity(
+ activityComponent,
+ IntentFilter(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_INFO) },
+ )
+
+ val notificationManagerCompat: NotificationManagerCompat = Mockito.spy(
+ NotificationManagerCompat.from(
+ testContext,
+ ),
+ )
+
+ notificationsDelegate = NotificationsDelegate(notificationManagerCompat)
+
+ whenever(notificationManagerCompat.areNotificationsEnabled()).thenReturn(true)
+ }
+
+ @Test
+ fun `updateNotification should show notification once when recording`() {
+ val realNotificationManager = testContext.getSystemService(Context.NOTIFICATION_SERVICE)
+ as NotificationManager
+ val notificationManager = Shadows.shadowOf(realNotificationManager)
+
+ assertEquals(0, notificationManager.size())
+
+ val middleware = RecordingDevicesMiddleware(testContext, notificationsDelegate)
+
+ middleware.updateNotification(RecordingState.Camera)
+
+ assertEquals(1, notificationManager.size())
+
+ // Another update with the same state to ensure that the notification is shown once.
+ middleware.updateNotification(RecordingState.CameraAndMicrophone)
+
+ assertEquals(1, notificationManager.size())
+ }
+
+ @Test
+ fun `updateNotification hides notification when it has shown notification`() {
+ val realNotificationManager = testContext.getSystemService(Context.NOTIFICATION_SERVICE)
+ as NotificationManager
+ val notificationManager = Shadows.shadowOf(realNotificationManager)
+
+ assertEquals(0, notificationManager.size())
+
+ val middleware = RecordingDevicesMiddleware(testContext, notificationsDelegate)
+
+ middleware.updateNotification(RecordingState.Camera)
+
+ assertEquals(1, notificationManager.size())
+
+ middleware.updateNotification(RecordingState.None)
+
+ assertEquals(0, notificationManager.size())
+ }
+
+ @Test
+ fun `middleware shows notification when tab has a recording device then hides when recording devices become inactive`() {
+ val realNotificationManager = testContext.getSystemService(Context.NOTIFICATION_SERVICE)
+ as NotificationManager
+ val notificationManager = Shadows.shadowOf(realNotificationManager)
+
+ val middleware = RecordingDevicesMiddleware(testContext, notificationsDelegate)
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(0, notificationManager.size())
+
+ store.dispatch(
+ ContentAction.SetRecordingDevices(
+ sessionId = "mozilla",
+ devices = listOf(
+ RecordingDevice(RecordingDevice.Type.CAMERA, RecordingDevice.Status.RECORDING),
+ ),
+ ),
+ ).joinBlocking()
+
+ assertEquals(1, notificationManager.size())
+
+ store.dispatch(
+ ContentAction.SetRecordingDevices(
+ sessionId = "mozilla",
+ devices = emptyList(),
+ ),
+ ).joinBlocking()
+
+ assertEquals(0, notificationManager.size())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/notification/MediaNotificationTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/notification/MediaNotificationTest.kt
new file mode 100644
index 0000000000..09715c7e8f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/notification/MediaNotificationTest.kt
@@ -0,0 +1,208 @@
+/* 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.media.notification
+
+import android.app.Notification
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.core.app.NotificationCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.feature.media.R
+import mozilla.components.feature.media.service.AbstractMediaSessionService
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+
+@RunWith(AndroidJUnit4::class)
+class MediaNotificationTest {
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = spy(testContext).also {
+ val packageManager: PackageManager = mock()
+ doReturn(Intent()).`when`(packageManager).getLaunchIntentForPackage(ArgumentMatchers.anyString())
+ doReturn(packageManager).`when`(it).packageManager
+ }
+ }
+
+ @Test
+ fun `media session notification for playing state`() = runTest {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "test-tab",
+ title = "Mozilla",
+ mediaSessionState = MediaSessionState(mock(), playbackState = MediaSession.PlaybackState.PLAYING),
+ ),
+ ),
+ )
+
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
+
+ assertEquals("https://www.mozilla.org", notification.text)
+ assertEquals("Mozilla", notification.title)
+ assertEquals(R.drawable.mozac_feature_media_playing, notification.iconResource)
+ }
+
+ @Test
+ fun `media session notification for paused state`() = runTest {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "test-tab",
+ title = "Mozilla",
+ mediaSessionState = MediaSessionState(mock(), playbackState = MediaSession.PlaybackState.PAUSED),
+ ),
+ ),
+ )
+
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
+
+ assertEquals("https://www.mozilla.org", notification.text)
+ assertEquals("Mozilla", notification.title)
+ assertEquals(R.drawable.mozac_feature_media_paused, notification.iconResource)
+ }
+
+ @Test
+ fun `media session notification for stopped state`() = runTest {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "test-tab",
+ title = "Mozilla",
+ mediaSessionState = MediaSessionState(mock(), playbackState = MediaSession.PlaybackState.STOPPED),
+ ),
+ ),
+ )
+
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
+
+ assertEquals("", notification.text)
+ assertEquals("", notification.title)
+ }
+
+ @Test
+ fun `media session notification for playing state in private mode`() = runTest {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "test-tab",
+ title = "Mozilla",
+ private = true,
+ mediaSessionState = MediaSessionState(mock(), playbackState = MediaSession.PlaybackState.PLAYING),
+ ),
+ ),
+ )
+
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
+
+ assertEquals("", notification.text)
+ assertEquals("A site is playing media", notification.title)
+ assertEquals(R.drawable.mozac_feature_media_playing, notification.iconResource)
+ }
+
+ @Test
+ fun `media session notification for paused state in private mode`() = runTest {
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "test-tab",
+ title = "Mozilla",
+ private = true,
+ mediaSessionState = MediaSessionState(mock(), playbackState = MediaSession.PlaybackState.PAUSED),
+ ),
+ ),
+ )
+
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
+
+ assertEquals("", notification.text)
+ assertEquals("A site is playing media", notification.title)
+ assertEquals(R.drawable.mozac_feature_media_paused, notification.iconResource)
+ }
+
+ @Test
+ fun `media session notification with metadata in non private mode`() = runTest {
+ val mediaSessionState: MediaSessionState = mock()
+ val metadata: MediaSession.Metadata = mock()
+ whenever(mediaSessionState.metadata).thenReturn(metadata)
+ whenever(mediaSessionState.playbackState).thenReturn(MediaSession.PlaybackState.PAUSED)
+ whenever(metadata.title).thenReturn("test title")
+
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "test-tab",
+ title = "Mozilla",
+ private = false,
+ mediaSessionState = mediaSessionState,
+ ),
+ ),
+ )
+
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
+
+ assertEquals("https://www.mozilla.org", notification.text)
+ assertEquals("test title", notification.title)
+ assertEquals(R.drawable.mozac_feature_media_paused, notification.iconResource)
+ }
+
+ @Test
+ fun `media session notification with metadata in private mode`() = runTest {
+ val mediaSessionState: MediaSessionState = mock()
+ val metadata: MediaSession.Metadata = mock()
+ whenever(mediaSessionState.metadata).thenReturn(metadata)
+ whenever(mediaSessionState.playbackState).thenReturn(MediaSession.PlaybackState.PAUSED)
+ whenever(metadata.title).thenReturn("test title")
+
+ val state = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ id = "test-tab",
+ title = "Mozilla",
+ private = true,
+ mediaSessionState = mediaSessionState,
+ ),
+ ),
+ )
+
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
+
+ assertEquals("", notification.text)
+ assertEquals("A site is playing media", notification.title)
+ assertEquals(R.drawable.mozac_feature_media_paused, notification.iconResource)
+ }
+}
+
+private val Notification.text: String?
+ get() = extras.getString(NotificationCompat.EXTRA_TEXT)
+
+private val Notification.title: String?
+ get() = extras.getString(NotificationCompat.EXTRA_TITLE)
+
+private val Notification.iconResource: Int
+ @Suppress("DEPRECATION")
+ get() = icon
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/AbstractMediaSessionServiceTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/AbstractMediaSessionServiceTest.kt
new file mode 100644
index 0000000000..a89d852f2f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/AbstractMediaSessionServiceTest.kt
@@ -0,0 +1,100 @@
+/* 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.media.service
+
+import android.app.Service.START_NOT_STICKY
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+
+@RunWith(AndroidJUnit4::class)
+class AbstractMediaSessionServiceTest {
+ private val service = Robolectric.buildService(FakeMediaService::class.java).get()
+
+ @Test
+ fun `WHEN the service is created THEN it initialize all needed properties`() {
+ service.onCreate()
+
+ assertNotNull(service.binder)
+ assertEquals(MediaSessionServiceDelegate::class.java, service.delegate!!.javaClass)
+ assertEquals(service, service.delegate!!.context)
+ assertEquals(service, service.delegate!!.service)
+ assertEquals(service.store, service.delegate!!.store)
+ }
+
+ @Test
+ fun `WHEN the service is destroyed THEN clean all internal properties`() {
+ service.delegate = mock()
+ service.binder = mock()
+
+ service.onDestroy()
+
+ assertNull(service.delegate)
+ assertNull(service.binder)
+ }
+
+ @Test
+ fun `WHEN the service receives a new Intent THEN send it to the delegate and set the service to not be automatically restarted`() {
+ service.delegate = mock()
+ val intent: Intent = mock()
+
+ val result = service.onStartCommand(intent, 33, 22)
+
+ verify(service.delegate)!!.onStartCommand(intent)
+ assertEquals(START_NOT_STICKY, result)
+ }
+
+ @Test
+ fun `GIVEN the service is running WHEN a task in the application is removed THEN inform the delegate`() {
+ service.delegate = mock()
+
+ service.onTaskRemoved(null)
+
+ verify(service.delegate)!!.onTaskRemoved()
+ }
+
+ @Test
+ fun `WHEN the service is bound THEN return the current binder instance`() {
+ service.binder = mock()
+
+ val result = service.onBind(null)
+
+ assertEquals(service.binder, result)
+ }
+
+ @Test
+ fun `WHEN the play intent is asked for THEN it is set with an action to play media`() {
+ val intent = AbstractMediaSessionService.playIntent(testContext, FakeMediaService::class.java)
+
+ assertEquals(AbstractMediaSessionService.ACTION_PLAY, intent.action)
+ assertEquals(ComponentName(testContext, FakeMediaService::class.java), intent.component)
+ }
+
+ @Test
+ fun `WHEN the play intent is asked for THEN it is set with an action to pause media`() {
+ val intent = AbstractMediaSessionService.pauseIntent(testContext, FakeMediaService::class.java)
+
+ assertEquals(AbstractMediaSessionService.ACTION_PAUSE, intent.action)
+ assertEquals(ComponentName(testContext, FakeMediaService::class.java), intent.component)
+ }
+}
+
+class FakeMediaService : AbstractMediaSessionService() {
+ public override val store: BrowserStore = mock()
+ public override val crashReporter: CrashReporting = mock()
+ public override val notificationsDelegate: NotificationsDelegate = mock()
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaServiceBinderTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaServiceBinderTest.kt
new file mode 100644
index 0000000000..5da01da91d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaServiceBinderTest.kt
@@ -0,0 +1,30 @@
+/* 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.media.service
+
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class MediaServiceBinderTest {
+ @Test
+ fun `GIVEN a constructed instance THEN the internal service is kept as a weak reference`() {
+ val delegate: MediaSessionDelegate = mock()
+
+ val binder = MediaServiceBinder(delegate)
+
+ assertSame(delegate, binder.service.get())
+ }
+
+ @Test
+ fun `GIVEN a constructed instance WHEN the media service is asked for THEN return the initially provided instance`() {
+ val delegate: MediaSessionDelegate = mock()
+ val binder = MediaServiceBinder(delegate)
+
+ val result = binder.getMediaService()
+
+ assertSame(delegate, result)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaSessionServiceDelegateTest.kt b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaSessionServiceDelegateTest.kt
new file mode 100644
index 0000000000..8880f60296
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaSessionServiceDelegateTest.kt
@@ -0,0 +1,581 @@
+/* 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.media.service
+
+import android.app.ForegroundServiceStartNotAllowedException
+import android.app.Notification
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.Build
+import android.support.v4.media.MediaMetadataCompat
+import android.support.v4.media.session.MediaSessionCompat
+import android.support.v4.media.session.PlaybackStateCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineExceptionHandler
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.concept.engine.mediasession.MediaSession.Metadata
+import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState
+import mozilla.components.feature.media.ext.toPlaybackState
+import mozilla.components.feature.media.facts.MediaFacts
+import mozilla.components.feature.media.notification.MediaNotification
+import mozilla.components.feature.media.session.MediaSessionCallback
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.coMock
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import mozilla.components.support.utils.ext.stopForegroundCompat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.util.ReflectionHelpers.setStaticField
+import kotlin.reflect.jvm.javaField
+import android.media.session.PlaybackState as AndroidPlaybackState
+
+@RunWith(AndroidJUnit4::class)
+class MediaSessionServiceDelegateTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val notificationId = SharedIdsHelper.getIdForTag(testContext, AbstractMediaSessionService.NOTIFICATION_TAG)
+
+ @Test
+ fun `WHEN the service is created THEN create a new notification scope audio focus manager`() {
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.mediaSession = mock()
+ val mediaCallbackCaptor = argumentCaptor<MediaSessionCallback>()
+
+ delegate.onCreate()
+
+ verify(delegate.mediaSession).setCallback(mediaCallbackCaptor.capture())
+ assertNotNull(delegate.notificationScope)
+ }
+
+ @Test
+ fun `WHEN the service is destroyed THEN stop notification updates and abandon audio focus`() {
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.audioFocus = mock()
+
+ delegate.onDestroy()
+
+ verify(delegate.audioFocus)!!.abandon()
+ verify(delegate.service, never()).stopSelf()
+ assertNull(delegate.notificationScope)
+ }
+
+ @Test
+ fun `GIVEN media playing started WHEN a new play command is received THEN resume media and emit telemetry`() {
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.controller = mock() // simulate media already started playing
+
+ CollectionProcessor.withFactCollection { facts ->
+ delegate.onStartCommand(Intent(AbstractMediaSessionService.ACTION_PLAY))
+
+ verify(delegate.controller)!!.play()
+ assertEquals(1, facts.size)
+ with(facts[0]) {
+ assertEquals(Component.FEATURE_MEDIA, component)
+ assertEquals(Action.PLAY, action)
+ assertEquals(MediaFacts.Items.NOTIFICATION, item)
+ }
+ }
+ }
+
+ @Test
+ fun `GIVEN media playing started WHEN a new pause command is received THEN pause media and emit telemetry`() {
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.controller = mock() // simulate media already started playing
+
+ CollectionProcessor.withFactCollection { facts ->
+ delegate.onStartCommand(Intent(AbstractMediaSessionService.ACTION_PAUSE))
+
+ verify(delegate.controller)!!.pause()
+ assertEquals(1, facts.size)
+ with(facts[0]) {
+ assertEquals(Component.FEATURE_MEDIA, component)
+ assertEquals(Action.PAUSE, action)
+ assertEquals(MediaFacts.Items.NOTIFICATION, item)
+ }
+ }
+ }
+
+ @Test
+ fun `WHEN the task is removed THEN stop media in all tabs and shutdown`() {
+ val notificationManagerCompat: NotificationManagerCompat = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ whenever(notificationsDelegate.notificationManagerCompat).thenReturn(notificationManagerCompat)
+
+ val mediaTab1 = getMediaTab()
+ val mediaTab2 = getMediaTab(PlaybackState.PAUSED)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(mediaTab1, mediaTab2),
+ ),
+ )
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), store, mock(), notificationsDelegate)
+ delegate.mediaSession = mock()
+
+ delegate.onTaskRemoved()
+
+ verify(mediaTab1.mediaSessionState!!.controller).stop()
+ verify(mediaTab2.mediaSessionState!!.controller).stop()
+ verify(delegate.mediaSession).release()
+ verify(delegate.service).stopSelf()
+ }
+
+ @Test
+ fun `WHEN handling playing media THEN emit telemetry`() {
+ val mediaTab = getMediaTab()
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.audioFocus = mock()
+
+ CollectionProcessor.withFactCollection { facts ->
+ delegate.handleMediaPlaying(mediaTab)
+
+ assertEquals(1, facts.size)
+ with(facts[0]) {
+ assertEquals(Component.FEATURE_MEDIA, component)
+ assertEquals(Action.PLAY, action)
+ assertEquals(MediaFacts.Items.STATE, item)
+ }
+ }
+ }
+
+ @Test
+ fun `WHEN handling playing media THEN setup internal properties`() {
+ val mediaTab = getMediaTab()
+ val delegate = spy(MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock()))
+ delegate.audioFocus = mock()
+
+ delegate.handleMediaPlaying(mediaTab)
+
+ verify(delegate).updateMediaSession(mediaTab)
+ verify(delegate).registerBecomingNoisyListenerIfNeeded(mediaTab)
+ assertSame(mediaTab.mediaSessionState!!.controller, delegate.controller)
+ }
+
+ @Test
+ fun `GIVEN the service is already in foreground WHEN handling playing media THEN setup internal properties`() = runTestOnMain {
+ val mediaTab = getMediaTab()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), notificationsDelegate)
+ delegate.onCreate()
+ delegate.audioFocus = mock()
+ delegate.isForegroundService = true
+
+ delegate.handleMediaPlaying(mediaTab)
+
+ verify(notificationsDelegate).notify(any(), eq(delegate.notificationId), any(), any(), any(), eq(false))
+ }
+
+ @Test
+ fun `GIVEN the service is not in foreground WHEN handling playing media THEN start the media service as foreground`() {
+ val mediaTab = getMediaTab()
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.onCreate()
+ delegate.audioFocus = mock()
+ delegate.isForegroundService = false
+
+ delegate.handleMediaPlaying(mediaTab)
+
+ verify(delegate.service).startForeground(eq(delegate.notificationId), any())
+ assertTrue(delegate.isForegroundService)
+ }
+
+ @Test
+ fun `WHEN updating the notification for a new media state THEN post a new notification`() = runTestOnMain {
+ val mediaTab = getMediaTab()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), notificationsDelegate)
+ delegate.onCreate()
+ val notification: Notification = mock()
+ delegate.notificationHelper = coMock {
+ doReturn(notification).`when`(this).create(mediaTab, delegate.mediaSession)
+ }
+
+ delegate.updateNotification(mediaTab)
+
+ verify(notificationsDelegate).notify(any(), eq(delegate.notificationId), eq(notification), any(), any(), eq(false))
+ }
+
+ @Test
+ fun `WHEN starting the service as foreground THEN use start with a new notification for the current media state`() = runTestOnMain {
+ val mediaTab = getMediaTab()
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.onCreate()
+ val notification: Notification = mock()
+ delegate.notificationHelper = coMock {
+ doReturn(notification).`when`(this).create(mediaTab, delegate.mediaSession)
+ }
+
+ delegate.startForeground(mediaTab)
+
+ verify(delegate.service).startForeground(eq(delegate.notificationId), eq(notification))
+ assertTrue(delegate.isForegroundService)
+ }
+
+ @Test
+ fun `GIVEN media is paused WHEN media is handling resuming media THEN resume the right session`() {
+ val mediaTab1 = getMediaTab()
+ val mediaTab2 = getMediaTab(PlaybackState.PAUSED)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(mediaTab1, mediaTab2),
+ ),
+ )
+ val service: AbstractMediaSessionService = mock()
+ val crashReporter: CrashReporting = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, crashReporter, mock())
+ val mediaSessionCallback = MediaSessionCallback(store)
+ delegate.onCreate()
+
+ mediaSessionCallback.onPause()
+ verify(mediaTab1.mediaSessionState!!.controller).pause()
+
+ mediaSessionCallback.onPlay()
+ verify(mediaTab1.mediaSessionState!!.controller).play()
+ }
+
+ @Test
+ fun `WHEN handling paused media THEN emit telemetry`() {
+ val mediaTab = getMediaTab(PlaybackState.PAUSED)
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+
+ CollectionProcessor.withFactCollection { facts ->
+ delegate.handleMediaPaused(mediaTab)
+
+ assertEquals(1, facts.size)
+ with(facts[0]) {
+ assertEquals(Component.FEATURE_MEDIA, component)
+ assertEquals(Action.PAUSE, action)
+ assertEquals(MediaFacts.Items.STATE, item)
+ }
+ }
+ }
+
+ @Test
+ fun `WHEN handling paused media THEN update internal state and notification and stop the service`() = runTestOnMain {
+ val mediaTab = getMediaTab(PlaybackState.PAUSED)
+ val notificationManagerCompat = spy(NotificationManagerCompat.from(testContext))
+ val notificationsDelegate = spy(NotificationsDelegate(notificationManagerCompat))
+ doReturn(true).`when`(notificationManagerCompat).areNotificationsEnabled()
+
+ val notificationHelper: MediaNotification = mock()
+ val notification: Notification = mock()
+ val mediaSession: MediaSessionCompat = mock()
+ val notificationId = SharedIdsHelper.getIdForTag(testContext, AbstractMediaSessionService.NOTIFICATION_TAG)
+
+ val delegate = spy(MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), notificationsDelegate))
+ delegate.isForegroundService = true
+ delegate.mediaSession = mediaSession
+ delegate.notificationHelper = notificationHelper
+
+ doReturn(notification).`when`(notificationHelper).create(mediaTab, mediaSession)
+
+ delegate.onCreate()
+
+ delegate.handleMediaPaused(mediaTab)
+
+ verify(delegate).updateMediaSession(mediaTab)
+ verify(delegate).unregisterBecomingNoisyListenerIfNeeded()
+ verify(delegate.service).stopForegroundCompat(false)
+ verify(notificationsDelegate).notify(null, notificationId, notification)
+ assertFalse(delegate.isForegroundService)
+ }
+
+ @Test
+ fun `WHEN handling stopped media THEN emit telemetry`() {
+ val mediaTab = getMediaTab(PlaybackState.STOPPED)
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+
+ CollectionProcessor.withFactCollection { facts ->
+ delegate.handleMediaStopped(mediaTab)
+
+ assertEquals(1, facts.size)
+ with(facts[0]) {
+ assertEquals(Component.FEATURE_MEDIA, component)
+ assertEquals(Action.STOP, action)
+ assertEquals(MediaFacts.Items.STATE, item)
+ }
+ }
+ }
+
+ @Test
+ fun `WHEN handling stopped media THEN update internal state and notification and stop the service`() = runTestOnMain {
+ val mediaTab = getMediaTab(PlaybackState.STOPPED)
+ val notificationsDelegate: NotificationsDelegate = mock()
+
+ val delegate = spy(MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), notificationsDelegate))
+ delegate.isForegroundService = true
+ delegate.onCreate()
+
+ delegate.handleMediaStopped(mediaTab)
+
+ verify(delegate).updateMediaSession(mediaTab)
+ verify(delegate).unregisterBecomingNoisyListenerIfNeeded()
+ verify(delegate.service).stopForegroundCompat(false)
+ verify(notificationsDelegate).notify(any(), eq(notificationId), any(), any(), any(), eq(false))
+ assertFalse(delegate.isForegroundService)
+ }
+
+ @Test
+ fun `WHEN there is no media playing THEN stop the media service`() {
+ val notificationManagerCompat: NotificationManagerCompat = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ whenever(notificationsDelegate.notificationManagerCompat).thenReturn(notificationManagerCompat)
+
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), notificationsDelegate)
+ delegate.audioFocus = mock()
+ delegate.mediaSession = mock()
+
+ delegate.handleNoMedia()
+
+ verify(delegate.mediaSession).release()
+ verify(delegate.service).stopSelf()
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `WHEN updating the media session THEN use the values from the current media session`() {
+ val bitmap: Bitmap = mock()
+ val getArtwork: (suspend () -> Bitmap?) = { bitmap }
+ val metadata = Metadata("title", "artist", "album", getArtwork)
+
+ val mediaTab = createTab(
+ title = "Mozilla",
+ url = "https://www.mozilla.org",
+ mediaSessionState = MediaSessionState(mock(), metadata = metadata),
+ )
+
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.mediaSession = mock()
+ delegate.onCreate()
+ val metadataCaptor = argumentCaptor<MediaMetadataCompat>()
+ // Need to capture method arguments and manually check for equality
+ val playbackStateCaptor = argumentCaptor<PlaybackStateCompat>()
+ val expectedPlaybackState = mediaTab.mediaSessionState!!.toPlaybackState()
+
+ delegate.updateMediaSession(mediaTab)
+
+ verify(delegate.mediaSession).isActive = true
+ verify(delegate.mediaSession).setPlaybackState(playbackStateCaptor.capture())
+ assertEquals(expectedPlaybackState.state, playbackStateCaptor.value.state)
+ assertEquals(
+ (expectedPlaybackState.playbackState as AndroidPlaybackState).state,
+ (playbackStateCaptor.value.playbackState as AndroidPlaybackState).state,
+ )
+ assertEquals(
+ (expectedPlaybackState.playbackState as AndroidPlaybackState).position,
+ (playbackStateCaptor.value.playbackState as AndroidPlaybackState).position,
+ )
+ assertEquals(
+ (expectedPlaybackState.playbackState as AndroidPlaybackState).playbackSpeed,
+ (playbackStateCaptor.value.playbackState as AndroidPlaybackState).playbackSpeed,
+ )
+ assertEquals(
+ (expectedPlaybackState.playbackState as AndroidPlaybackState).actions,
+ (playbackStateCaptor.value.playbackState as AndroidPlaybackState).actions,
+ )
+ assertEquals(
+ (expectedPlaybackState.playbackState as AndroidPlaybackState).customActions,
+ (playbackStateCaptor.value.playbackState as AndroidPlaybackState).customActions,
+ )
+ assertEquals(expectedPlaybackState.playbackSpeed, playbackStateCaptor.value.playbackSpeed)
+ assertEquals(expectedPlaybackState.actions, playbackStateCaptor.value.actions)
+ assertEquals(expectedPlaybackState.position, playbackStateCaptor.value.position)
+ verify(delegate.mediaSession).setMetadata(metadataCaptor.capture())
+ assertEquals(metadata.title, metadataCaptor.value.bundle.getString(MediaMetadataCompat.METADATA_KEY_TITLE))
+ assertEquals(metadata.artist, metadataCaptor.value.bundle.getString(MediaMetadataCompat.METADATA_KEY_ARTIST))
+ assertEquals(bitmap, metadataCaptor.value.bundle.getParcelable(MediaMetadataCompat.METADATA_KEY_ART))
+ assertEquals(-1L, metadataCaptor.value.bundle.getLong(MediaMetadataCompat.METADATA_KEY_DURATION))
+ }
+
+ @Test
+ fun `WHEN stopping running in foreground THEN stop the foreground service`() {
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock())
+ delegate.isForegroundService = true
+
+ delegate.stopForeground()
+
+ verify(delegate.service).stopForegroundCompat(false)
+ assertFalse(delegate.isForegroundService)
+ }
+
+ @Test
+ fun `GIVEN a audio noisy receiver is already registered WHEN trying to register a new one THEN return early`() {
+ val context = spy(testContext)
+ val delegate = MediaSessionServiceDelegate(context, mock(), mock(), mock(), mock())
+ delegate.noisyAudioStreamReceiver = mock()
+
+ delegate.registerBecomingNoisyListenerIfNeeded(mock())
+
+ verify(context, never()).registerReceiver(any(), any(), eq(Context.RECEIVER_NOT_EXPORTED))
+ }
+
+ @Test
+ fun `GIVEN a audio noisy receiver is not already registered WHEN trying to register a new one THEN register it`() {
+ val delegate = spy(MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), mock()))
+ val receiverCaptor = argumentCaptor<BroadcastReceiver>()
+
+ delegate.registerBecomingNoisyListenerIfNeeded(mock())
+
+ verify(delegate).registerBecomingNoisyListener(receiverCaptor.capture())
+ assertEquals(BecomingNoisyReceiver::class.java, receiverCaptor.value.javaClass)
+ }
+
+ @Test
+ fun `GIVEN a audio noisy receiver is already registered WHEN trying to unregister one THEN unregister it`() {
+ val context = spy(testContext)
+ val delegate = MediaSessionServiceDelegate(context, mock(), mock(), mock(), mock())
+ delegate.noisyAudioStreamReceiver = mock()
+ context.registerReceiver(
+ delegate.noisyAudioStreamReceiver,
+ delegate.intentFilter,
+ Context.RECEIVER_NOT_EXPORTED,
+ )
+ val receiverCaptor = argumentCaptor<BroadcastReceiver>()
+
+ delegate.unregisterBecomingNoisyListenerIfNeeded()
+
+ verify(context).unregisterReceiver(receiverCaptor.capture())
+ assertEquals(BecomingNoisyReceiver::class.java, receiverCaptor.value.javaClass)
+ assertNull(delegate.noisyAudioStreamReceiver)
+ }
+
+ @Test
+ fun `GIVEN a audio noisy receiver is not already registered WHEN trying to unregister one THEN return early`() {
+ val context = spy(testContext)
+ val delegate = MediaSessionServiceDelegate(context, mock(), mock(), mock(), mock())
+
+ delegate.unregisterBecomingNoisyListenerIfNeeded()
+
+ verify(context, never()).unregisterReceiver(any())
+ }
+
+ @Test
+ fun `WHEN the delegate is shutdown THEN cleanup resources and stop the media service`() {
+ val notificationManagerCompat: NotificationManagerCompat = mock()
+ val notificationsDelegate: NotificationsDelegate = mock()
+ whenever(notificationsDelegate.notificationManagerCompat).thenReturn(notificationManagerCompat)
+
+ val delegate = MediaSessionServiceDelegate(testContext, mock(), mock(), mock(), notificationsDelegate)
+ delegate.mediaSession = mock()
+
+ delegate.shutdown()
+
+ verify(delegate.mediaSession).release()
+ verify(delegate.service).stopSelf()
+ assertNull(delegate.noisyAudioStreamReceiver)
+ }
+
+ @Test
+ fun `when device is becoming noisy, playback is paused`() {
+ val controller: MediaSession.Controller = mock()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org",
+ mediaSessionState = MediaSessionState(controller, playbackState = PlaybackState.PLAYING),
+ ),
+ ),
+ )
+ val store = BrowserStore(initialState)
+ val service: AbstractMediaSessionService = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, store, mock(), mock())
+ delegate.onCreate()
+ delegate.handleMediaPlaying(initialState.tabs[0])
+
+ delegate.deviceBecomingNoisy(testContext)
+
+ verify(controller).pause()
+ }
+
+ @Test
+ fun `GIVEN device is at least API level 31 WHEN startForeground throws an exception THEN catch and pass the exception to the crash reporter`() = runTestOnMain {
+ val crashReporter: CrashReporting = mock()
+ val service: AbstractMediaSessionService = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, mock(), crashReporter, mock())
+ delegate.onCreate()
+ val notification: Notification = mock()
+ delegate.notificationHelper = coMock {
+ doReturn(notification).`when`(this).create(mock(), delegate.mediaSession)
+ }
+
+ val exception = ForegroundServiceStartNotAllowedException("Test thrown exception")
+ doThrow(exception).`when`(service).startForeground(anyInt(), any())
+ setSdkInt(31)
+
+ delegate.startForeground(mock())
+
+ verify(crashReporter).submitCaughtException(exception)
+ }
+
+ @Test(expected = ForegroundServiceStartNotAllowedException::class)
+ fun `GIVEN device is less than 31 WHEN startForeground throws an exception THEN rethrow the exception`() {
+ var throwable: Throwable? = null
+ val exceptionHandler = CoroutineExceptionHandler { _, t ->
+ throwable = t
+ }
+
+ runTestOnMain {
+ val crashReporter: CrashReporting = mock()
+ val service: AbstractMediaSessionService = mock()
+ val delegate = MediaSessionServiceDelegate(testContext, service, mock(), crashReporter, mock())
+ delegate.onCreate()
+ val notification: Notification = mock()
+ delegate.notificationHelper = coMock {
+ doReturn(notification).`when`(this).create(mock(), delegate.mediaSession)
+ }
+
+ val exception = ForegroundServiceStartNotAllowedException("Test thrown exception")
+ doThrow(exception).`when`(service).startForeground(anyInt(), any())
+ setSdkInt(30)
+
+ delegate.startForeground(mock(), exceptionHandler)
+ }
+
+ throwable?.let { throw it }
+ }
+
+ private fun getMediaTab(playbackState: PlaybackState = PlaybackState.PLAYING) = createTab(
+ title = "Mozilla",
+ url = "https://www.mozilla.org",
+ mediaSessionState = MediaSessionState(mock(), playbackState = playbackState),
+ )
+
+ private fun setSdkInt(sdkVersion: Int) {
+ setStaticField(Build.VERSION::SDK_INT.javaField, sdkVersion)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/media/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/media/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/media/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/media/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/media/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/media/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/privatemode/README.md b/mobile/android/android-components/components/feature/privatemode/README.md
new file mode 100644
index 0000000000..dd4deb60ff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Private Mode
+
+Features used to enhance private browsing mode.
+
+## Usage
+
+### 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-privatemode:{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/privatemode/build.gradle b/mobile/android/android-components/components/feature/privatemode/build.gradle
new file mode 100644
index 0000000000..1743d9f642
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/build.gradle
@@ -0,0 +1,46 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.privatemode'
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ 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/privatemode/proguard-rules.pro b/mobile/android/android-components/components/feature/privatemode/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/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/privatemode/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/privatemode/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..5fba0d9762
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/AndroidManifest.xml
@@ -0,0 +1,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/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
+</manifest>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/feature/SecureWindowFeature.kt b/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/feature/SecureWindowFeature.kt
new file mode 100644
index 0000000000..388fd30388
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/feature/SecureWindowFeature.kt
@@ -0,0 +1,58 @@
+/* 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.privatemode.feature
+
+import android.view.Window
+import android.view.WindowManager.LayoutParams.FLAG_SECURE
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Prevents screenshots and screen recordings in private tabs.
+ *
+ * @param isSecure Returns true if the session should have [FLAG_SECURE] set.
+ * @param clearFlagOnStop Used to keep [FLAG_SECURE] enabled or not when calling [stop].
+ * Can be overriden to customize when the secure flag is set.
+ */
+class SecureWindowFeature(
+ private val window: Window,
+ private val store: BrowserStore,
+ private val customTabId: String? = null,
+ private val isSecure: (SessionState) -> Boolean = { it.content.private },
+ private val clearFlagOnStop: Boolean = true,
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(customTabId) }
+ .map { isSecure(it) }
+ .distinctUntilChanged()
+ .collect { isSecure ->
+ if (isSecure) {
+ window.addFlags(FLAG_SECURE)
+ } else {
+ window.clearFlags(FLAG_SECURE)
+ }
+ }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ if (clearFlagOnStop) {
+ window.clearFlags(FLAG_SECURE)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationService.kt b/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationService.kt
new file mode 100644
index 0000000000..638c36356e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationService.kt
@@ -0,0 +1,261 @@
+/* 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.privatemode.notification
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_ONE_SHOT
+import android.app.Service
+import android.content.Intent
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.IBinder
+import androidx.annotation.CallSuper
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.VISIBILITY_SECRET
+import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.privateTabs
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.privatemode.R
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.ktx.android.notification.ChannelData
+import mozilla.components.support.ktx.android.notification.ensureNotificationChannelExists
+import mozilla.components.support.utils.PendingIntentUtils
+import mozilla.components.support.utils.ext.stopForegroundCompat
+import java.util.Locale
+
+/**
+ * Manages notifications for private tabs.
+ *
+ * Private tab notifications solve two problems:
+ * 1. They allow users to interact with the browser from outside of the app
+ * (example: by closing all private tabs).
+ * 2. The notification will keep the process alive, allowing the browser to
+ * keep private tabs in memory.
+ *
+ * As long as a private tab is open this service will keep its notification alive.
+ */
+@Suppress("TooManyFunctions")
+abstract class AbstractPrivateNotificationService(
+ private val notificationScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+) : Service() {
+ private var privateTabsScope: CoroutineScope? = null
+ private var localeScope: CoroutineScope? = null
+
+ abstract val store: BrowserStore
+ abstract val notificationsDelegate: NotificationsDelegate
+
+ /**
+ * Customizes the private browsing notification.
+ */
+ abstract fun NotificationCompat.Builder.buildNotification()
+
+ /**
+ * Customize the notification response when the [Locale] has been changed.
+ */
+ abstract fun notifyLocaleChanged()
+
+ /**
+ * Erases all private tabs in reaction to the user tapping the notification.
+ */
+ @CallSuper
+ protected open fun erasePrivateTabs() {
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction)
+ }
+
+ /**
+ * Retrieves the notification id based on the tag.
+ */
+ protected fun getNotificationId(): Int {
+ return SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG)
+ }
+
+ /**
+ * Retrieves the channel id based on the channel data.
+ */
+ protected fun getChannelId(): String {
+ return ensureNotificationChannelExists(
+ this,
+ NOTIFICATION_CHANNEL,
+ onSetupChannel = {
+ if (SDK_INT >= Build.VERSION_CODES.O) {
+ enableLights(false)
+ enableVibration(false)
+ setShowBadge(false)
+ }
+ },
+ )
+ }
+
+ /**
+ * Re-build and notify an existing notification.
+ */
+ protected fun refreshNotification() {
+ notificationScope.launch {
+ val notificationId = getNotificationId()
+ val channelId = getChannelId()
+
+ val notification = createNotification(channelId)
+ withContext(Dispatchers.Main) {
+ notificationsDelegate.notify(notificationId = notificationId, notification = notification)
+ }
+ }
+ }
+
+ /**
+ * Create the private browsing notification and
+ * add a listener to stop the service once all private tabs are closed.
+ *
+ * The service should be started only if private tabs are open.
+ */
+ final override fun onCreate() {
+ notificationScope.launch {
+ val notificationId = getNotificationId()
+ val channelId = getChannelId()
+ val notification = createNotification(channelId)
+
+ if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ notificationsDelegate.requestNotificationPermission(
+ onPermissionGranted = { refreshNotification() },
+ )
+ }
+
+ withContext(Dispatchers.Main) {
+ startForeground(notificationId, notification)
+ }
+ }
+
+ privateTabsScope = store.flowScoped { flow ->
+ flow.map { state -> state.privateTabs.isEmpty() }
+ .distinctUntilChanged()
+ .collect { noPrivateTabs ->
+ if (noPrivateTabs) stopService()
+ }
+ }
+
+ localeScope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.locale }
+ .distinctUntilChanged()
+ .collect {
+ notifyLocaleChanged()
+ }
+ }
+ }
+
+ /**
+ * Builds a notification based on the specified channel id.
+ *
+ * @param channelId The channel id for the [Notification]
+ */
+ private fun createNotification(channelId: String): Notification {
+ val eraseIntent = Intent(ACTION_ERASE).let { intent ->
+ intent.setClass(this, this::class.java)
+ PendingIntent.getService(
+ this,
+ 0,
+ intent,
+ PendingIntentUtils.defaultFlags or FLAG_ONE_SHOT,
+ )
+ }
+
+ return NotificationCompat.Builder(this, channelId)
+ .setOngoing(true)
+ .setVisibility(VISIBILITY_SECRET)
+ .setShowWhen(false)
+ .setLocalOnly(true)
+ .setContentIntent(eraseIntent)
+ .apply {
+ if (SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ setDeleteIntent(eraseIntent)
+ }
+
+ buildNotification()
+ }
+ .build()
+ }
+
+ final override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ if (intent.action == ACTION_ERASE) {
+ erasePrivateTabs()
+ }
+
+ return START_NOT_STICKY
+ }
+
+ final override fun onDestroy() {
+ privateTabsScope?.cancel()
+ localeScope?.cancel()
+ }
+
+ final override fun onBind(intent: Intent?): IBinder? = null
+
+ final override fun onTaskRemoved(rootIntent: Intent) {
+ if (rootIntent.action in defaultIgnoreTaskActions ||
+ rootIntent.action in ignoreTaskActions() ||
+ rootIntent.component?.className in ignoreTaskComponentClasses()
+ ) {
+ // The app may have multiple tasks (e.g. for PWAs). If tasks get removed that are not
+ // the main browser task then we do not want to remove all private tabs here.
+ // I am not sure whether we can reliably identify the main task since it can be launched
+ // from multiple entry points (e.g. the launcher, VIEW intents, ..).
+ // So instead we ignore tasks with root intents that we absolutely do not want to handle
+ // here (e.g. PWAs) and then extend the list if needed.
+ return
+ }
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction)
+ stopService()
+ }
+
+ private fun stopService() {
+ stopForegroundCompat(true)
+ stopSelf()
+ }
+
+ /**
+ * Builds a list of Intent actions that will get ignored
+ * when they are in the root intent that gets passed to onTaskRemoved().
+ *
+ */
+ abstract fun ignoreTaskActions(): List<String>
+
+ /**
+ * Builds a list of Intent components' qualified class name that will get ignored
+ * when they are in the root intent that gets passed to onTaskRemoved().
+ *
+ */
+ abstract fun ignoreTaskComponentClasses(): List<String>
+
+ companion object {
+ private const val NOTIFICATION_TAG =
+ "mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService"
+ const val ACTION_ERASE = "mozilla.components.feature.privatemode.action.ERASE"
+
+ val NOTIFICATION_CHANNEL = ChannelData(
+ id = "browsing-session",
+ name = R.string.mozac_feature_privatemode_notification_channel_name,
+ importance = IMPORTANCE_LOW,
+ )
+
+ // List of default Intent actions that will get ignored
+ // when they are in the root intent that gets passed to onTaskRemoved().
+ @VisibleForTesting
+ internal val defaultIgnoreTaskActions = listOf(
+ "mozilla.components.feature.pwa.VIEW_PWA",
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeature.kt b/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeature.kt
new file mode 100644
index 0000000000..2dc3086fd5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeature.kt
@@ -0,0 +1,49 @@
+/* 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.privatemode.notification
+
+import android.content.Context
+import android.content.Intent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.selector.privateTabs
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import kotlin.reflect.KClass
+
+/**
+ * Starts up a [AbstractPrivateNotificationService] once a private tab is opened.
+ *
+ * @param store Browser store reference used to observe the number of private tabs.
+ * @param notificationServiceClass The service sub-class that should be started by this feature.
+ */
+class PrivateNotificationFeature<T : AbstractPrivateNotificationService>(
+ context: Context,
+ private val store: BrowserStore,
+ private val notificationServiceClass: KClass<T>,
+) : LifecycleAwareFeature {
+
+ private val applicationContext = context.applicationContext
+ private var scope: CoroutineScope? = null
+
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map { state -> state.privateTabs.isNotEmpty() }
+ .distinctUntilChanged()
+ .collect { hasPrivateTabs ->
+ if (hasPrivateTabs) {
+ applicationContext.startService(Intent(applicationContext, notificationServiceClass.java))
+ }
+ }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..187c4d8cfa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">የግል አሰሳ ክፍለ ጊዜ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..d775e46430
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-an/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesión de navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..d2f4886a0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ar/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">جلسة تصفح خاصة</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..457d635755
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesión de restolar en privao</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..55f7e0792b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-az/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Məxfi səyahət sessiyası</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..ec6fc61308
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">گیزلی مورور اوتورومو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..fa07631f1a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesi parerehan pribadi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..b8139d2113
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Сеанс прыватнага аглядання</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..ef4231310e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Поверителна сесия</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..202a4f48c4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">ব্যক্তিগত ব্রাউজিং সেশন</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..7f5fbe5d6d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Estez merdeiñ prevez</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..257e347741
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesija privatnog surfanja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..a8bba4c5eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sessió de navegació privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..345b325348
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Ichinan okem pa k\'amaya\'l</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..02d5db9319
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Pribado nga browsing session</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..afdd152d2d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">دانیشتنی گەڕانی تایبەت</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..f0b94247ed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sessione di navigazione privata</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..01b363ad88
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Relace anonymního prohlížení</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..3a32633f6e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesiwn pori preifat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..d3e9ad0c56
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Privat browsing-session</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..6962068c23
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Private Surf-Sitzung</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..6e32fee374
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Priwatne pósejźenje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..44c884a333
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Συνεδρία ιδιωτικής περιήγησης</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..587e57a1dc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Private browsing session</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..587e57a1dc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Private browsing session</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..24e839434f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Seanco de privata retumo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..d775e46430
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesión de navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..d775e46430
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesión de navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..d775e46430
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesión de navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..d775e46430
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesión de navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..d775e46430
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesión de navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..818868f7b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Privaatse veebilehitsemise seanss</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..ce1cc77225
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Nabigatze pribatuko saioa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..1b0ab6cdc2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">نشستِ مرور ناشناس</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..ad96bb66cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Yksityinen selausistunto</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..712fb939b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Session de navigation privée</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..d24e91d19b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Session di navigazion privade</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..0bf34311e7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Priveenavigaasjesesje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..8064f0b6e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Seisean brabhsaidh prìobhaideach</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..d775e46430
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesión de navegación privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..eaaef34716
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Tembiapo kundaha ñemíme</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..bc1ed90389
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">ખાનગી બ્રાઉઝિંગ સત્ર</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..0cf2bf22be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">निजी ब्राउज़िंग सत्र</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..17a3aa688e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesija privatnog pretraživanja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..78f71ed53b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Priwatne posedźenje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..b58ef5be53
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Privát böngészési munkamenet</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..1c0211b44d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Գանղտնի դիտարկման աշխատաշրջան</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..b299198ddc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Session de navigation private</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..fd7bcd9b30
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesi penjelajahan pribadi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..b85189898b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Huliðsvafurlota</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..449c79bace
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sessione di navigazione anonima</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..1ac361ffc5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">הפעלה בגלישה פרטית</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..e57c06a447
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">プライベートブラウジングセッション</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..99846f0b5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">პირადი დათვალიერების სეანსი</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..04f25b68ba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Jeke kóriw seansı</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..cddb5621a7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Tiɣimit n tunigin tusligt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..d8545dba12
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Жекелік шолу сессиясы</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..f49d779df7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Rûniştina gerîna veşartî</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..66fb25b0e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-kn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">ಖಾಸಗಿ ಬ್ರೌಸಿಂಗ್ ಸೆಷನ್</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..58fc1a45ca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">사생활 보호 모드 세션</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..97397e1dc8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">ເຊສຊັນການທ່ອງເວັບແບບສ່ວນຕົວ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..fa2849bd50
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Privačiojo naršymo seansas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..2501d4d94e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-mr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">खाजगी ब्राऊझिंग सत्र</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..1b1da5b38c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-my/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">သီးသန့်ကြည့်ရှုခြင်းပုံစံ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..7159bbe9bf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Privat nettlesingsøkt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..82a848084f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">निजी ब्राउजिङ्ग सत्र</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..c51408466e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Privénavigatiesessie</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..7159bbe9bf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Privat nettlesingsøkt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..9214c4fa23
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Session de navegacion privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..9d636d882c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">ਨਿੱਜੀ ਬਰਾਊਜ਼ ਕਰਨ ਦਾ ਸ਼ੈਸ਼ਨ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..3d7e304a13
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">نجی ورتوں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..786b051c52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesja trybu prywatnego</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..2febb65f6a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sessão de navegação privativa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..da6a1e8dc3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sessão de navegação privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..9371c80689
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesida da navigaziun privata</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..a2965d814a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ro/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesiune de navigare privată</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..5971b2bb61
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Приватный просмотр</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..3d20f4bd6f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">ᱯᱨᱭᱣᱮᱴ ᱵᱽᱨᱟᱣᱩᱡᱤᱸᱜ ᱠᱟᱹᱢᱤ </string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..7db20cd453
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sessione de navigatzione privada</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..ebd851ac52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">පෞද්. පිරික්සුම් වාරය</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..9f430d7176
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Súkromné prehliadanie</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..971f535406
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">نجی براؤزنگ سیشن</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..8ef95bda09
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Seja zasebnega brskanja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..ff918d3e51
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sesion shfletimi privat</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..95acbc3bb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Сесија за приватно прегледање</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..a85f313c02
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Rintakan pamaluruhan nyamuni</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..497edd6bac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Privat surfsession</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..9521d4a790
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ta/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">கமுக்க உலாவல் அமர்வு</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..93bbc2f4b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-te/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">అంతరంగిక విహరణ సెషను</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..db6d7f325b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Ҷаласаи тамошокунии хусусӣ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..001877e436
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">วาระการท่องเว็บแบบส่วนตัว</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..587e57a1dc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Private browsing session</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..4bf45da6c7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Gizli gezinti oturumu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..1b699c60a1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Sēsiûn riña gāchē nu huìt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..683fb8376c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Хосусый гизү утырышы</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..94e8d8ba72
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">شەخسىي زىيارەت سۆزلەشكۈ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..8145544044
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Сеанс приватного перегляду</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..971f535406
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-ur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">نجی براؤزنگ سیشن</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..268840ae3f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Maxfiy koʻrish seansi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..894f23f484
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Phiên duyệt web riêng tư</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..0bf23890c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Àkókò bíráwúsìnìn ìkọ̀kọ̀</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..c30e937dda
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">隐私浏览会话</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..badb8f5119
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">隱私瀏覽階段</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/privatemode/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..82a3bbd38e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?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 user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
+ <string name="mozac_feature_privatemode_notification_channel_name">Private browsing session</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/feature/SecureWindowFeatureTest.kt b/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/feature/SecureWindowFeatureTest.kt
new file mode 100644
index 0000000000..d35709751a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/feature/SecureWindowFeatureTest.kt
@@ -0,0 +1,90 @@
+/* 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.privatemode.feature
+
+import android.view.Window
+import android.view.WindowManager.LayoutParams.FLAG_SECURE
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+class SecureWindowFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var window: Window
+ private val tabId = "test-tab"
+
+ @Before
+ fun setup() {
+ window = mock()
+ }
+
+ @Test
+ fun `no-op if no sessions`() {
+ val store = BrowserStore(BrowserState(tabs = emptyList()))
+ val feature = SecureWindowFeature(window, store)
+
+ feature.start()
+
+ verify(window, never()).addFlags(FLAG_SECURE)
+ verify(window, never()).clearFlags(FLAG_SECURE)
+ }
+
+ @Test
+ fun `add flags to private session`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = tabId, private = true),
+ ),
+ selectedTabId = tabId,
+ ),
+ )
+ val feature = SecureWindowFeature(window, store)
+
+ feature.start()
+
+ verify(window).addFlags(FLAG_SECURE)
+ }
+
+ @Test
+ fun `remove flags from normal session`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = tabId, private = false),
+ ),
+ selectedTabId = tabId,
+ ),
+ )
+ val feature = SecureWindowFeature(window, store)
+
+ feature.start()
+
+ verify(window).clearFlags(FLAG_SECURE)
+ }
+
+ @Test
+ fun `remove flags on stop`() {
+ val store = BrowserStore()
+ val feature = SecureWindowFeature(window, store, clearFlagOnStop = true)
+
+ feature.start()
+ feature.stop()
+
+ verify(window).clearFlags(FLAG_SECURE)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationServiceTest.kt b/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationServiceTest.kt
new file mode 100644
index 0000000000..615caf4123
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationServiceTest.kt
@@ -0,0 +1,195 @@
+/* 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.privatemode.notification
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.Service
+import android.content.ComponentName
+import android.content.Intent
+import android.content.SharedPreferences
+import androidx.core.app.NotificationCompat
+import androidx.core.content.getSystemService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import mozilla.components.browser.state.action.LocaleAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService.Companion.ACTION_ERASE
+import mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService.Companion.defaultIgnoreTaskActions
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import mozilla.components.support.utils.ext.stopForegroundCompat
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.Locale
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class AbstractPrivateNotificationServiceTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ private lateinit var preferences: SharedPreferences
+ private lateinit var notificationManager: NotificationManager
+
+ @Before
+ fun setup() {
+ preferences = mock()
+ notificationManager = mock()
+ val editor = mock<SharedPreferences.Editor>()
+
+ whenever(preferences.edit()).thenReturn(editor)
+ whenever(editor.putLong(anyString(), anyLong())).thenReturn(editor)
+ }
+
+ @Test
+ fun `WHEN the service is created THEN start foreground is called`() = runTestOnMain {
+ val service = spy(
+ object : MockService(scope = this@runTestOnMain) {
+ override fun NotificationCompat.Builder.buildNotification() {
+ setCategory(Notification.CATEGORY_STATUS)
+ }
+ override fun notifyLocaleChanged() {
+ // NOOP
+ }
+ },
+ )
+ attachContext(service)
+
+ val notification = argumentCaptor<Notification>()
+ service.onCreate()
+ advanceUntilIdle()
+
+ verify(service).startForeground(anyInt(), notification.capture())
+ assertEquals(Notification.CATEGORY_STATUS, notification.value.category)
+ }
+
+ @Test
+ fun `GIVEN an erase intent is received THEN remove all private tabs`() {
+ val service = MockService()
+ val result = service.onStartCommand(Intent(ACTION_ERASE), 0, 0)
+
+ verify(service.store).dispatch(TabListAction.RemoveAllPrivateTabsAction)
+ assertEquals(Service.START_NOT_STICKY, result)
+ }
+
+ @Test
+ fun `WHEN task is removed THEN all private tabs are removed`() {
+ val service = spy(MockService())
+ service.onTaskRemoved(mock())
+
+ verify(service.store).dispatch(TabListAction.RemoveAllPrivateTabsAction)
+ verify(service).stopForegroundCompat(true)
+ verify(service).stopSelf()
+ }
+
+ @Test
+ fun `WHEN task is removed with ignored intents THEN do nothing`() {
+ val service = spy(MockService())
+
+ val mockTaskActions = listOf("action1", "action2")
+ whenever(service.ignoreTaskActions()).then { mockTaskActions }
+
+ (mockTaskActions + defaultIgnoreTaskActions).forEach { it ->
+ service.onTaskRemoved(Intent(it))
+
+ verify(service.store, never()).dispatch(TabListAction.RemoveAllPrivateTabsAction)
+ verify(service, never()).stopForegroundCompat(true)
+ verify(service, never()).stopSelf()
+ }
+
+ val mockTaskCompoentClasses = listOf(
+ "org.mozilla.fenix.IntentReceiverActivity",
+ "org.mozilla.fenix.customtabs.ExternalAppBrowserActivity",
+ "comp1",
+ "comp2",
+ )
+ whenever(service.ignoreTaskComponentClasses()).then { mockTaskCompoentClasses }
+
+ mockTaskCompoentClasses.forEach { it ->
+ service.onTaskRemoved(Intent().setComponent(ComponentName(testContext, it)))
+
+ verify(service.store, never()).dispatch(TabListAction.RemoveAllPrivateTabsAction)
+ verify(service, never()).stopForegroundCompat(true)
+ verify(service, never()).stopSelf()
+ }
+ }
+
+ @Test
+ fun `WHEN a locale change is made in the browser store THEN the service should notify`() {
+ val service = spy(MockServiceWithStore())
+ attachContext(service)
+ service.onCreate()
+
+ val mockLocale = Locale("English")
+ service.store.dispatch(LocaleAction.UpdateLocaleAction(mockLocale)).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(service).notifyLocaleChanged()
+ }
+
+ private open class MockService(scope: CoroutineScope = TestScope()) :
+ AbstractPrivateNotificationService(scope) {
+ override val store: BrowserStore = spy(BrowserStore())
+ override val notificationsDelegate: NotificationsDelegate = mock()
+
+ override fun NotificationCompat.Builder.buildNotification() = Unit
+ override fun notifyLocaleChanged() {
+ // NOOP
+ }
+
+ override fun ignoreTaskActions(): List<String> = mock()
+ override fun ignoreTaskComponentClasses(): List<String> = mock()
+ }
+
+ private open class MockServiceWithStore : AbstractPrivateNotificationService() {
+ override val store = BrowserStore(
+ BrowserState(
+ locale = null,
+ ),
+ )
+ override val notificationsDelegate: NotificationsDelegate = mock()
+
+ override fun NotificationCompat.Builder.buildNotification() = Unit
+ override fun notifyLocaleChanged() {
+ // NOOP
+ }
+
+ override fun ignoreTaskActions(): List<String> = mock()
+ override fun ignoreTaskComponentClasses(): List<String> = mock()
+ }
+
+ private fun attachContext(service: Service) {
+ Mockito.doReturn(preferences).`when`(service).getSharedPreferences(anyString(), anyInt())
+ Mockito.doReturn(notificationManager).`when`(service).getSystemService<NotificationManager>()
+ Mockito.doReturn("").`when`(service).getString(anyInt())
+ Mockito.doReturn("").`when`(service).packageName
+ Mockito.doReturn(testContext.resources).`when`(service).resources
+ Mockito.doReturn(testContext.applicationInfo).`when`(service).applicationInfo
+ }
+}
diff --git a/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeatureTest.kt b/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeatureTest.kt
new file mode 100644
index 0000000000..8fa4015870
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeatureTest.kt
@@ -0,0 +1,133 @@
+/* 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.privatemode.notification
+
+import android.content.Context
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+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.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class PrivateNotificationFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var context: Context
+ private lateinit var store: BrowserStore
+ private lateinit var feature: PrivateNotificationFeature<AbstractPrivateNotificationService>
+
+ @Before
+ fun setup() {
+ context = mock()
+ whenever(context.applicationContext).thenReturn(context)
+ whenever(context.packageName).thenReturn(testContext.packageName)
+
+ store = BrowserStore()
+
+ feature = PrivateNotificationFeature(context, store, AbstractPrivateNotificationService::class)
+ }
+
+ @Test
+ fun `service should be started if pre-existing private session is present`() = runTest(StandardTestDispatcher()) {
+ val privateSession = createTab("https://firefox.com", private = true)
+ val intent = argumentCaptor<Intent>()
+
+ store.dispatch(TabListAction.AddTabAction(privateSession)).join()
+
+ feature.start()
+ runCurrent()
+ verify(context, times(1)).startService(intent.capture())
+
+ val expected = Intent(testContext, AbstractPrivateNotificationService::class.java)
+ assertEquals(expected.component, intent.value.component)
+ assertTrue(expected.filterEquals(intent.value))
+ }
+
+ @Test
+ fun `service should be started when private session is added`() = runTestOnMain {
+ val privateSession = createTab("https://firefox.com", private = true)
+
+ feature.start()
+ verify(context, never()).startService(any())
+
+ store.dispatch(TabListAction.AddTabAction(privateSession)).join()
+ verify(context, times(1)).startService(any())
+ Unit
+ }
+
+ @Test
+ fun `service should not be started multiple times`() = runTestOnMain {
+ val privateSession1 = createTab("https://firefox.com", private = true)
+ val privateSession2 = createTab("https://mozilla.org", private = true)
+
+ feature.start()
+
+ store.dispatch(TabListAction.AddTabAction(privateSession1)).join()
+ store.dispatch(TabListAction.AddTabAction(privateSession2)).join()
+
+ verify(context, times(1)).startService(any())
+ Unit
+ }
+
+ @Test
+ fun `notification service should not be started when normal sessions are added`() = runTestOnMain {
+ val normalSession = createTab("https://firefox.com")
+ val customSession = createCustomTab("https://firefox.com")
+
+ feature.start()
+ verify(context, never()).startService(any())
+
+ store.dispatch(TabListAction.AddTabAction(normalSession)).join()
+ verify(context, never()).startService(any())
+
+ store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
+ verify(context, never()).startService(any())
+ Unit
+ }
+
+ @Test
+ fun `notification service should not be started when custom sessions are added`() = runTestOnMain {
+ val privateCustomSession = createCustomTab("https://firefox.com").let {
+ it.copy(content = it.content.copy(private = true))
+ }
+ val customSession = createCustomTab("https://firefox.com")
+
+ feature.start()
+ verify(context, never()).startService(any())
+
+ store.dispatch(CustomTabListAction.AddCustomTabAction(privateCustomSession)).join()
+ verify(context, never()).startService(any())
+
+ store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
+ verify(context, never()).startService(any())
+ Unit
+ }
+}
diff --git a/mobile/android/android-components/components/feature/privatemode/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/privatemode/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/privatemode/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/privatemode/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/privatemode/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/privatemode/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/prompts/README.md b/mobile/android/android-components/components/feature/prompts/README.md
new file mode 100644
index 0000000000..704f34b36e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/README.md
@@ -0,0 +1,76 @@
+# [Android Components](../../../README.md) > Feature > Prompts
+
+A feature for displaying native dialogs. It will subscribe to the selected session and will handle all the common prompt dialogs from web content like input type
+date, file, time, color, option, menu, authentication, confirmation and alerts.
+
+## Usage
+
+### 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-prompts:{latest-version}"
+```
+
+### PromptFeature
+
+ ```kotlin
+ val onNeedToRequestPermissions : (Array<String>) -> Unit = { permissions ->
+ /* You are in charge of triggering the request for the permissions needed,
+ * this way you can control, when you request the permissions,
+ * in case that you want to show an informative dialog,
+ * to clarify the use of these permissions.
+ */
+ this.requestPermissions(permissions, MY_PROMPT_PERMISSION_REQUEST_CODE)
+ }
+
+ val promptFeature = PromptFeature(fragment = this,
+ fragment = fragment,
+ store = store,
+ fragmentManager= fragmentManager,
+ onNeedToRequestPermissions = onNeedToRequestPermissions
+ )
+
+ // It will start listing for new prompt requests from web content.
+ promptFeature.start()
+
+ // It will stop listing for prompt requests from web content.
+ promptFeature.stop()
+
+ /* There are some requests that are not handled with dialogs, instead they are delegated to other apps
+ * to perform the request e.g a file picker request, which delegates to the OS file picker.
+ * For this reason, you have to forward the results of these requests to the prompt feature by overriding,
+ * onActivityResult in your Activity or Fragment and forward its calls to promptFeature.onActivityResult.
+ */
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ promptFeature.onActivityResult(requestCode, resultCode, data)
+ }
+
+ /* Additionally, there are requests that need to have some runtime permission before they can be performed,
+ * like file pickers request that need access to read the selected files. You need to override
+ * onRequestPermissionsResult in your Activity or Fragment and forward the results to
+ * promptFeature.PermissionsResult.
+ */
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
+ when (requestCode) {
+ MY_PROMPT_PERMISSION_REQUEST_CODE -> promptFeature.onPermissionsResult(permissions, grantResults)
+ }
+ }
+ ```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Description |
+|-----------|--------------|-------------------------|
+| DISPLAY | prompt | A prompt was shown. |
+| CANCEL | prompt | A prompt was canceled. |
+| CONFIRM | prompt | A prompt was confirmed. |
+
+
+## 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/prompts/build.gradle b/mobile/android/android-components/components/feature/prompts/build.gradle
new file mode 100644
index 0000000000..0212157d95
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/build.gradle
@@ -0,0 +1,79 @@
+/* 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'
+
+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'
+ }
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ namespace 'mozilla.components.feature.prompts'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':feature-session')
+ implementation project(':feature-tabs')
+ implementation project(':lib-state')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+ implementation project(':ui-icons')
+ implementation project(':ui-widgets')
+ implementation project(':ui-colors')
+
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.google_material
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+
+ debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':feature-session')
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+
+ androidTestImplementation project(':support-android-test')
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+}
+
+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/prompts/proguard-rules.pro b/mobile/android/android-components/components/feature/prompts/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/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/prompts/src/androidTest/java/mozilla/components/feature/prompts/file/OnDeviceFilePickerTest.kt b/mobile/android/android-components/components/feature/prompts/src/androidTest/java/mozilla/components/feature/prompts/file/OnDeviceFilePickerTest.kt
new file mode 100644
index 0000000000..5aef30b6d6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/androidTest/java/mozilla/components/feature/prompts/file/OnDeviceFilePickerTest.kt
@@ -0,0 +1,155 @@
+/* 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.prompts.file
+
+import android.content.ClipData
+import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
+import androidx.test.core.app.ApplicationProvider
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.feature.prompts.PromptContainer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class OnDeviceFilePickerTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private val badUri1 = "file:///data/user/0/${context.packageName}/any_directory/file.text".toUri()
+ private val badUri2 = "file:///data/data/${context.packageName}/any_directory/file.text".toUri()
+ private val goodUri = "file:///data/directory/${context.packageName}/any_directory/file.text".toUri()
+
+ @Test
+ fun removeUrisUnderPrivateAppDir() {
+ val uris = arrayOf(badUri1, badUri2, goodUri)
+
+ val filterUris = uris.removeUrisUnderPrivateAppDir(context)
+
+ assertEquals(1, filterUris.size)
+ assertTrue(filterUris.contains(goodUri))
+ }
+
+ @Test
+ fun unsafeUrisWillNotBeSelected() {
+ val promptContainer = PromptContainer.TestPromptContainer(context)
+ val fileUploadsDirCleaner = FileUploadsDirCleaner { context.cacheDir }
+ val filePicker = FilePicker(
+ container = promptContainer,
+ fileUploadsDirCleaner = fileUploadsDirCleaner,
+ store = BrowserStore(),
+ ) { }
+ var onDismissWasExecuted = false
+ var onSingleFileSelectedWasExecuted = false
+ var onMultipleFilesSelectedWasExecuted = false
+
+ val filePickerRequest = PromptRequest.File(
+ arrayOf(""),
+ isMultipleFilesSelection = true,
+ onDismiss = { onDismissWasExecuted = true },
+ onSingleFileSelected = { _, _ -> onSingleFileSelectedWasExecuted = true },
+ onMultipleFilesSelected = { _, _ -> onMultipleFilesSelectedWasExecuted = true },
+ )
+
+ val intent = Intent()
+ intent.clipData = ClipData("", arrayOf(), ClipData.Item(badUri1))
+ filePicker.handleFilePickerIntentResult(intent, filePickerRequest)
+
+ assertTrue(onDismissWasExecuted)
+ assertFalse(onSingleFileSelectedWasExecuted)
+ assertFalse(onMultipleFilesSelectedWasExecuted)
+ }
+
+ @Test
+ fun safeUrisWillBeSelected() {
+ val promptContainer = PromptContainer.TestPromptContainer(context)
+ val fileUploadsDirCleaner = FileUploadsDirCleaner { context.cacheDir }
+ val filePicker = FilePicker(
+ container = promptContainer,
+ fileUploadsDirCleaner = fileUploadsDirCleaner,
+ store = BrowserStore(),
+ ) { }
+ var urisWereSelected = false
+ var onDismissWasExecuted = false
+ var onSingleFileSelectedWasExecuted = false
+
+ val filePickerRequest = PromptRequest.File(
+ arrayOf(""),
+ isMultipleFilesSelection = true,
+ onDismiss = { onDismissWasExecuted = true },
+ onSingleFileSelected = { _, _ -> onSingleFileSelectedWasExecuted = true },
+ onMultipleFilesSelected = { _, uris -> urisWereSelected = uris.isNotEmpty() },
+ )
+
+ val intent = Intent()
+ intent.clipData = ClipData("", arrayOf(), ClipData.Item(goodUri))
+ filePicker.handleFilePickerIntentResult(intent, filePickerRequest)
+
+ assertTrue(urisWereSelected)
+ assertFalse(onDismissWasExecuted)
+ assertFalse(onSingleFileSelectedWasExecuted)
+ }
+
+ @Test
+ fun unsafeUriWillNotBeSelected() {
+ val promptContainer = PromptContainer.TestPromptContainer(context)
+ val fileUploadsDirCleaner = FileUploadsDirCleaner { context.cacheDir }
+ val filePicker = FilePicker(
+ container = promptContainer,
+ fileUploadsDirCleaner = fileUploadsDirCleaner,
+ store = BrowserStore(),
+ ) { }
+ var onDismissWasExecuted = false
+ var onSingleFileSelectedWasExecuted = false
+ var onMultipleFilesSelectedWasExecuted = false
+
+ val filePickerRequest = PromptRequest.File(
+ arrayOf(""),
+ onDismiss = { onDismissWasExecuted = true },
+ onSingleFileSelected = { _, _ -> onSingleFileSelectedWasExecuted = true },
+ onMultipleFilesSelected = { _, _ -> onMultipleFilesSelectedWasExecuted = true },
+ )
+
+ val intent = Intent()
+ intent.data = badUri1
+ filePicker.handleFilePickerIntentResult(intent, filePickerRequest)
+
+ assertTrue(onDismissWasExecuted)
+ assertFalse(onSingleFileSelectedWasExecuted)
+ assertFalse(onMultipleFilesSelectedWasExecuted)
+ }
+
+ @Test
+ fun safeUriWillBeSelected() {
+ val promptContainer = PromptContainer.TestPromptContainer(context)
+ val fileUploadsDirCleaner = FileUploadsDirCleaner { context.cacheDir }
+ val filePicker = FilePicker(
+ container = promptContainer,
+ fileUploadsDirCleaner = fileUploadsDirCleaner,
+ store = BrowserStore(),
+ ) { }
+ var onDismissWasExecuted = false
+ var onSingleFileSelectedWasExecuted = false
+ var onMultipleFilesSelectedWasExecuted = false
+
+ val filePickerRequest = PromptRequest.File(
+ arrayOf(""),
+ onDismiss = { onDismissWasExecuted = true },
+ onSingleFileSelected = { _, _ -> onSingleFileSelectedWasExecuted = true },
+ onMultipleFilesSelected = { _, _ -> onMultipleFilesSelectedWasExecuted = true },
+ )
+
+ val intent = Intent()
+ intent.data = goodUri
+ filePicker.handleFilePickerIntentResult(intent, filePickerRequest)
+
+ assertTrue(onSingleFileSelectedWasExecuted)
+ assertFalse(onMultipleFilesSelectedWasExecuted)
+ assertFalse(onDismissWasExecuted)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/prompts/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..ee81dd1486
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Needed for uploading media files on devices with Android 13 and later. -->
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+
+ <!-- Used for requesting partial video/image files access on devices with Android 14 and later. -->
+ <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
+
+ <application android:supportsRtl="true">
+ <provider
+ android:name="mozilla.components.feature.prompts.provider.FileProvider"
+ android:authorities="${applicationId}.feature.prompts.fileprovider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/feature_prompts_file_paths" />
+ </provider>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptContainer.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptContainer.kt
new file mode 100644
index 0000000000..fb02e45d2e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptContainer.kt
@@ -0,0 +1,63 @@
+/* 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.prompts
+
+import android.content.Context
+import android.content.Intent
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Wrapper to hold shared functionality between activities and fragments for [PromptFeature].
+ */
+internal sealed class PromptContainer {
+
+ /**
+ * Getter for [Context].
+ */
+ abstract val context: Context
+
+ /**
+ * Launches an activity for which you would like a result when it finished.
+ */
+ abstract fun startActivityForResult(intent: Intent, code: Int)
+
+ /**
+ * Returns a localized string.
+ */
+ abstract fun getString(@StringRes res: Int, vararg objects: Any): String
+
+ internal class Activity(
+ private val activity: android.app.Activity,
+ ) : PromptContainer() {
+
+ override val context get() = activity
+
+ override fun startActivityForResult(intent: Intent, code: Int) =
+ activity.startActivityForResult(intent, code)
+
+ override fun getString(res: Int, vararg objects: Any) = activity.getString(res, *objects)
+ }
+
+ internal class Fragment(
+ private val fragment: androidx.fragment.app.Fragment,
+ ) : PromptContainer() {
+
+ override val context get() = fragment.requireContext()
+
+ @Suppress("DEPRECATION")
+ // https://github.com/mozilla-mobile/android-components/issues/10357
+ override fun startActivityForResult(intent: Intent, code: Int) =
+ fragment.startActivityForResult(intent, code)
+
+ override fun getString(res: Int, vararg objects: Any) = fragment.getString(res, *objects)
+ }
+
+ @VisibleForTesting
+ internal class TestPromptContainer(override val context: Context) : PromptContainer() {
+ override fun startActivityForResult(intent: Intent, code: Int) = Unit
+ override fun getString(res: Int, vararg objects: Any) = context.getString(res, *objects)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
new file mode 100644
index 0000000000..481b0e5ce3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
@@ -0,0 +1,1220 @@
+/* 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.prompts
+
+import android.app.Activity
+import android.content.Intent
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+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.prompt.Choice
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.prompt.PromptRequest.Alert
+import mozilla.components.concept.engine.prompt.PromptRequest.Authentication
+import mozilla.components.concept.engine.prompt.PromptRequest.BeforeUnload
+import mozilla.components.concept.engine.prompt.PromptRequest.Color
+import mozilla.components.concept.engine.prompt.PromptRequest.Confirm
+import mozilla.components.concept.engine.prompt.PromptRequest.Dismissible
+import mozilla.components.concept.engine.prompt.PromptRequest.File
+import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.Popup
+import mozilla.components.concept.engine.prompt.PromptRequest.Repost
+import mozilla.components.concept.engine.prompt.PromptRequest.SaveCreditCard
+import mozilla.components.concept.engine.prompt.PromptRequest.SaveLoginPrompt
+import mozilla.components.concept.engine.prompt.PromptRequest.SelectAddress
+import mozilla.components.concept.engine.prompt.PromptRequest.SelectCreditCard
+import mozilla.components.concept.engine.prompt.PromptRequest.SelectLoginPrompt
+import mozilla.components.concept.engine.prompt.PromptRequest.Share
+import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
+import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection
+import mozilla.components.concept.identitycredential.Account
+import mozilla.components.concept.identitycredential.Provider
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardValidationDelegate
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.concept.storage.LoginValidationDelegate
+import mozilla.components.feature.prompts.address.AddressDelegate
+import mozilla.components.feature.prompts.address.AddressPicker
+import mozilla.components.feature.prompts.address.DefaultAddressDelegate
+import mozilla.components.feature.prompts.creditcard.CreditCardDelegate
+import mozilla.components.feature.prompts.creditcard.CreditCardPicker
+import mozilla.components.feature.prompts.creditcard.CreditCardSaveDialogFragment
+import mozilla.components.feature.prompts.dialog.AlertDialogFragment
+import mozilla.components.feature.prompts.dialog.AuthenticationDialogFragment
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.MENU_CHOICE_DIALOG_TYPE
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.MULTIPLE_CHOICE_DIALOG_TYPE
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.SINGLE_CHOICE_DIALOG_TYPE
+import mozilla.components.feature.prompts.dialog.ColorPickerDialogFragment
+import mozilla.components.feature.prompts.dialog.ConfirmDialogFragment
+import mozilla.components.feature.prompts.dialog.MultiButtonDialogFragment
+import mozilla.components.feature.prompts.dialog.PromptAbuserDetector
+import mozilla.components.feature.prompts.dialog.PromptDialogFragment
+import mozilla.components.feature.prompts.dialog.Prompter
+import mozilla.components.feature.prompts.dialog.SaveLoginDialogFragment
+import mozilla.components.feature.prompts.dialog.TextPromptDialogFragment
+import mozilla.components.feature.prompts.dialog.TimePickerDialogFragment
+import mozilla.components.feature.prompts.ext.executeIfWindowedPrompt
+import mozilla.components.feature.prompts.facts.emitCreditCardSaveShownFact
+import mozilla.components.feature.prompts.facts.emitPromptConfirmedFact
+import mozilla.components.feature.prompts.facts.emitPromptDismissedFact
+import mozilla.components.feature.prompts.facts.emitPromptDisplayedFact
+import mozilla.components.feature.prompts.facts.emitSuccessfulAddressAutofillFormDetectedFact
+import mozilla.components.feature.prompts.facts.emitSuccessfulCreditCardAutofillFormDetectedFact
+import mozilla.components.feature.prompts.file.FilePicker
+import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
+import mozilla.components.feature.prompts.identitycredential.DialogColors
+import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider
+import mozilla.components.feature.prompts.identitycredential.PrivacyPolicyDialogFragment
+import mozilla.components.feature.prompts.identitycredential.SelectAccountDialogFragment
+import mozilla.components.feature.prompts.identitycredential.SelectProviderDialogFragment
+import mozilla.components.feature.prompts.login.LoginDelegate
+import mozilla.components.feature.prompts.login.LoginExceptions
+import mozilla.components.feature.prompts.login.LoginPicker
+import mozilla.components.feature.prompts.login.StrongPasswordPromptViewListener
+import mozilla.components.feature.prompts.login.SuggestStrongPasswordDelegate
+import mozilla.components.feature.prompts.share.DefaultShareDelegate
+import mozilla.components.feature.prompts.share.ShareDelegate
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.session.SessionUseCases.ExitFullScreenUseCase
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.ActivityResultHandler
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.OnNeedToRequestPermissions
+import mozilla.components.support.base.feature.PermissionsFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.ifNullOrEmpty
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
+import java.lang.ref.WeakReference
+import java.security.InvalidParameterException
+import java.util.Collections
+import java.util.Date
+import java.util.WeakHashMap
+
+@VisibleForTesting(otherwise = PRIVATE)
+internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
+
+/**
+ * Feature for displaying native dialogs for html elements like: input type
+ * date, file, time, color, option, menu, authentication, confirmation and alerts.
+ *
+ * There are some requests that are handled with intents instead of dialogs,
+ * like file choosers and others. For this reason, you have to keep the feature
+ * aware of the flow of requesting data from other apps, overriding
+ * onActivityResult in your [Activity] or [Fragment] and forward its calls
+ * to [onActivityResult].
+ *
+ * This feature will subscribe to the currently selected session and display
+ * a suitable native dialog based on [Session.Observer.onPromptRequested] events.
+ * Once the dialog is closed or the user selects an item from the dialog
+ * the related [PromptRequest] will be consumed.
+ *
+ * @property container The [Activity] or [Fragment] which hosts this feature.
+ * @property store The [BrowserStore] this feature should subscribe to.
+ * @property customTabId Optional id of a custom tab. Instead of showing context
+ * menus for the currently selected tab this feature will show only context menus
+ * for this custom tab if an id is provided.
+ * @property fragmentManager The [FragmentManager] to be used when displaying
+ * a dialog (fragment).
+ * @property shareDelegate Delegate used to display share sheet.
+ * @property exitFullscreenUsecase Usecase allowing to exit browser tabs' fullscreen mode.
+ * @property isSaveLoginEnabled A callback invoked when a login prompt is triggered. If false,
+ * 'save login' prompts will not be shown.
+ * @property isCreditCardAutofillEnabled A callback invoked when credit card fields are detected in the webpage.
+ * If this resolves to `true` a prompt allowing the user to select the credit card details to be autocompleted
+ * will be shown.
+ * @property isAddressAutofillEnabled A callback invoked when address fields are detected in the webpage.
+ * If this resolves to `true` a prompt allowing the user to select the address details to be autocompleted
+ * will be shown.
+ * @property loginExceptionStorage An implementation of [LoginExceptions] that saves and checks origins
+ * the user does not want to see a save login dialog for.
+ * @property loginDelegate Delegate for login picker.
+ * @property suggestStrongPasswordDelegate Delegate for strong password generator.
+ * @property isSuggestStrongPasswordEnabled Feature flag denoting whether the suggest strong password
+ * feature is enabled or not. If this resolves to 'false', the feature will be hidden.
+ * @property onSaveLoginWithStrongPassword A callback invoked to save a new login that uses the
+ * generated strong password
+ * @property creditCardDelegate Delegate for credit card picker.
+ * @property addressDelegate Delegate for address picker.
+ * @property fileUploadsDirCleaner a [FileUploadsDirCleaner] to clean up temporary file uploads.
+ * @property onNeedToRequestPermissions A callback invoked when permissions
+ * need to be requested before a prompt (e.g. a file picker) can be displayed.
+ * Once the request is completed, [onPermissionsResult] needs to be invoked.
+ */
+@Suppress("LargeClass", "LongParameterList")
+class PromptFeature private constructor(
+ private val container: PromptContainer,
+ private val store: BrowserStore,
+ private var customTabId: String?,
+ private val fragmentManager: FragmentManager,
+ private val identityCredentialColorsProvider: DialogColorsProvider = DialogColorsProvider {
+ DialogColors.default()
+ },
+ private val tabsUseCases: TabsUseCases,
+ private val shareDelegate: ShareDelegate,
+ private val exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
+ override val creditCardValidationDelegate: CreditCardValidationDelegate? = null,
+ override val loginValidationDelegate: LoginValidationDelegate? = null,
+ private val isSaveLoginEnabled: () -> Boolean = { false },
+ private val isCreditCardAutofillEnabled: () -> Boolean = { false },
+ private val isAddressAutofillEnabled: () -> Boolean = { false },
+ override val loginExceptionStorage: LoginExceptions? = null,
+ private val loginDelegate: LoginDelegate = object : LoginDelegate {},
+ private val suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
+ SuggestStrongPasswordDelegate {},
+ private val isSuggestStrongPasswordEnabled: Boolean = false,
+ private val onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> },
+ private val creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
+ private val addressDelegate: AddressDelegate = DefaultAddressDelegate(),
+ private val fileUploadsDirCleaner: FileUploadsDirCleaner,
+ onNeedToRequestPermissions: OnNeedToRequestPermissions,
+) : LifecycleAwareFeature,
+ PermissionsFeature,
+ Prompter,
+ ActivityResultHandler,
+ UserInteractionHandler {
+ // These three scopes have identical lifetimes. We do not yet have a way of combining scopes
+ private var handlePromptScope: CoroutineScope? = null
+ private var dismissPromptScope: CoroutineScope? = null
+
+ @VisibleForTesting
+ var activePromptRequest: PromptRequest? = null
+
+ internal val promptAbuserDetector = PromptAbuserDetector()
+ private val logger = Logger("PromptFeature")
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var activePrompt: WeakReference<PromptDialogFragment>? = null
+
+ // This set of weak references of fragments is only used for dismissing all prompts on navigation.
+ // For all other code only `activePrompt` is tracked for now.
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal val activePromptsToDismiss =
+ Collections.newSetFromMap(WeakHashMap<PromptDialogFragment, Boolean>())
+
+ constructor(
+ activity: Activity,
+ store: BrowserStore,
+ customTabId: String? = null,
+ fragmentManager: FragmentManager,
+ tabsUseCases: TabsUseCases,
+ identityCredentialColorsProvider: DialogColorsProvider = DialogColorsProvider { DialogColors.default() },
+ shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
+ creditCardValidationDelegate: CreditCardValidationDelegate? = null,
+ loginValidationDelegate: LoginValidationDelegate? = null,
+ isSaveLoginEnabled: () -> Boolean = { false },
+ isCreditCardAutofillEnabled: () -> Boolean = { false },
+ isAddressAutofillEnabled: () -> Boolean = { false },
+ loginExceptionStorage: LoginExceptions? = null,
+ loginDelegate: LoginDelegate = object : LoginDelegate {},
+ suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
+ SuggestStrongPasswordDelegate {},
+ isSuggestStrongPasswordEnabled: Boolean = false,
+ onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> },
+ creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
+ addressDelegate: AddressDelegate = DefaultAddressDelegate(),
+ fileUploadsDirCleaner: FileUploadsDirCleaner,
+ onNeedToRequestPermissions: OnNeedToRequestPermissions,
+ ) : this(
+ container = PromptContainer.Activity(activity),
+ store = store,
+ customTabId = customTabId,
+ fragmentManager = fragmentManager,
+ tabsUseCases = tabsUseCases,
+ identityCredentialColorsProvider = identityCredentialColorsProvider,
+ shareDelegate = shareDelegate,
+ exitFullscreenUsecase = exitFullscreenUsecase,
+ creditCardValidationDelegate = creditCardValidationDelegate,
+ loginValidationDelegate = loginValidationDelegate,
+ isSaveLoginEnabled = isSaveLoginEnabled,
+ isCreditCardAutofillEnabled = isCreditCardAutofillEnabled,
+ isAddressAutofillEnabled = isAddressAutofillEnabled,
+ loginExceptionStorage = loginExceptionStorage,
+ fileUploadsDirCleaner = fileUploadsDirCleaner,
+ onNeedToRequestPermissions = onNeedToRequestPermissions,
+ loginDelegate = loginDelegate,
+ suggestStrongPasswordDelegate = suggestStrongPasswordDelegate,
+ isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled,
+ onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword,
+ creditCardDelegate = creditCardDelegate,
+ addressDelegate = addressDelegate,
+ )
+
+ constructor(
+ fragment: Fragment,
+ store: BrowserStore,
+ customTabId: String? = null,
+ fragmentManager: FragmentManager,
+ tabsUseCases: TabsUseCases,
+ shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ exitFullscreenUsecase: ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
+ creditCardValidationDelegate: CreditCardValidationDelegate? = null,
+ loginValidationDelegate: LoginValidationDelegate? = null,
+ isSaveLoginEnabled: () -> Boolean = { false },
+ isCreditCardAutofillEnabled: () -> Boolean = { false },
+ isAddressAutofillEnabled: () -> Boolean = { false },
+ loginExceptionStorage: LoginExceptions? = null,
+ loginDelegate: LoginDelegate = object : LoginDelegate {},
+ suggestStrongPasswordDelegate: SuggestStrongPasswordDelegate = object :
+ SuggestStrongPasswordDelegate {},
+ isSuggestStrongPasswordEnabled: Boolean = false,
+ onSaveLoginWithStrongPassword: (String, String) -> Unit = { _, _ -> },
+ creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
+ addressDelegate: AddressDelegate = DefaultAddressDelegate(),
+ fileUploadsDirCleaner: FileUploadsDirCleaner,
+ onNeedToRequestPermissions: OnNeedToRequestPermissions,
+ ) : this(
+ container = PromptContainer.Fragment(fragment),
+ store = store,
+ customTabId = customTabId,
+ fragmentManager = fragmentManager,
+ tabsUseCases = tabsUseCases,
+ shareDelegate = shareDelegate,
+ exitFullscreenUsecase = exitFullscreenUsecase,
+ creditCardValidationDelegate = creditCardValidationDelegate,
+ loginValidationDelegate = loginValidationDelegate,
+ isSaveLoginEnabled = isSaveLoginEnabled,
+ isCreditCardAutofillEnabled = isCreditCardAutofillEnabled,
+ isAddressAutofillEnabled = isAddressAutofillEnabled,
+ loginExceptionStorage = loginExceptionStorage,
+ fileUploadsDirCleaner = fileUploadsDirCleaner,
+ onNeedToRequestPermissions = onNeedToRequestPermissions,
+ loginDelegate = loginDelegate,
+ suggestStrongPasswordDelegate = suggestStrongPasswordDelegate,
+ isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled,
+ onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword,
+ creditCardDelegate = creditCardDelegate,
+ addressDelegate = addressDelegate,
+ )
+
+ private val filePicker =
+ FilePicker(container, store, customTabId, fileUploadsDirCleaner, onNeedToRequestPermissions)
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var loginPicker =
+ with(loginDelegate) {
+ loginPickerView?.let {
+ LoginPicker(store, it, onManageLogins, customTabId)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var strongPasswordPromptViewListener =
+ with(suggestStrongPasswordDelegate) {
+ strongPasswordPromptViewListenerView?.let {
+ StrongPasswordPromptViewListener(store, it, customTabId)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var creditCardPicker =
+ with(creditCardDelegate) {
+ creditCardPickerView?.let {
+ CreditCardPicker(
+ store = store,
+ creditCardSelectBar = it,
+ manageCreditCardsCallback = onManageCreditCards,
+ selectCreditCardCallback = onSelectCreditCard,
+ sessionId = customTabId,
+ )
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var addressPicker =
+ with(addressDelegate) {
+ addressPickerView?.let {
+ AddressPicker(
+ store = store,
+ addressSelectBar = it,
+ onManageAddresses = onManageAddresses,
+ sessionId = customTabId,
+ )
+ }
+ }
+
+ override val onNeedToRequestPermissions
+ get() = filePicker.onNeedToRequestPermissions
+
+ override fun onOpenLink(url: String) {
+ tabsUseCases.addTab(
+ url = url,
+ )
+ }
+
+ /**
+ * Starts observing the selected session to listen for prompt requests
+ * and displays a dialog when needed.
+ */
+ @Suppress("ComplexMethod", "LongMethod")
+ override fun start() {
+ promptAbuserDetector.resetJSAlertAbuseState()
+
+ handlePromptScope = store.flowScoped { flow ->
+ flow.map { state -> state.findTabOrCustomTabOrSelectedTab(customTabId) }
+ .ifAnyChanged {
+ arrayOf(it?.content?.promptRequests, it?.content?.loading)
+ }
+ .collect { state ->
+ state?.content?.let { content ->
+ if (content.promptRequests.lastOrNull() != activePromptRequest) {
+ // Dismiss any active select login or credit card prompt if it does
+ // not match the current prompt request for the session.
+ when (activePromptRequest) {
+ is SelectLoginPrompt -> {
+ loginPicker?.dismissCurrentLoginSelect(activePromptRequest as SelectLoginPrompt)
+ strongPasswordPromptViewListener?.dismissCurrentSuggestStrongPassword(
+ activePromptRequest as SelectLoginPrompt,
+ )
+ }
+
+ is SaveLoginPrompt -> {
+ (activePrompt?.get() as? SaveLoginDialogFragment)?.dismissAllowingStateLoss()
+ }
+
+ is SaveCreditCard -> {
+ (activePrompt?.get() as? CreditCardSaveDialogFragment)?.dismissAllowingStateLoss()
+ }
+
+ is SelectCreditCard -> {
+ creditCardPicker?.dismissSelectCreditCardRequest(
+ activePromptRequest as SelectCreditCard,
+ )
+ }
+
+ is SelectAddress -> {
+ addressPicker?.dismissSelectAddressRequest(
+ activePromptRequest as SelectAddress,
+ )
+ }
+
+ is SingleChoice,
+ is MultipleChoice,
+ is MenuChoice,
+ -> {
+ (activePrompt?.get() as? ChoiceDialogFragment)?.let { dialog ->
+ if (dialog.isAdded) {
+ dialog.dismissAllowingStateLoss()
+ } else {
+ activePromptsToDismiss.remove(dialog)
+ activePrompt?.clear()
+ }
+ }
+ }
+
+ else -> {
+ // no-op
+ }
+ }
+
+ onPromptRequested(state)
+ } else if (!content.loading) {
+ promptAbuserDetector.resetJSAlertAbuseState()
+ } else if (content.loading) {
+ dismissSelectPrompts()
+ }
+
+ activePromptRequest = content.promptRequests.lastOrNull()
+ }
+ }
+ }
+
+ // Dismiss all prompts when page URL or session id changes. See Fenix#5326
+ dismissPromptScope = store.flowScoped { flow ->
+ flow.ifAnyChanged { state ->
+ arrayOf(
+ state.selectedTabId,
+ state.findTabOrCustomTabOrSelectedTab(customTabId)?.content?.url,
+ )
+ }.collect {
+ dismissSelectPrompts()
+
+ val prompt = activePrompt?.get()
+
+ store.consumeAllSessionPrompts(
+ sessionId = prompt?.sessionId,
+ activePrompt,
+ predicate = { it.shouldDismissOnLoad },
+ consume = { prompt?.dismiss() },
+ )
+
+ // Let's make sure we do not leave anything behind..
+ activePromptsToDismiss.forEach { fragment -> fragment.dismiss() }
+ }
+ }
+
+ fragmentManager.findFragmentByTag(FRAGMENT_TAG)?.let { fragment ->
+ // There's still a [PromptDialogFragment] visible from the last time. Re-attach this feature so that the
+ // fragment can invoke the callback on this feature once the user makes a selection. This can happen when
+ // the app was in the background and on resume the activity and fragments get recreated.
+ reattachFragment(fragment as PromptDialogFragment)
+ }
+ }
+
+ override fun stop() {
+ // Stops observing the selected session for incoming prompt requests.
+ handlePromptScope?.cancel()
+ dismissPromptScope?.cancel()
+
+ // Dismisses the logins prompt so that it can appear on another tab
+ dismissSelectPrompts()
+ }
+
+ override fun onBackPressed(): Boolean {
+ return dismissSelectPrompts()
+ }
+
+ /**
+ * Notifies the feature of intent results for prompt requests handled by
+ * other apps like credit card and file chooser requests.
+ *
+ * @param requestCode The code of the app that requested the intent.
+ * @param data The result of the request.
+ * @param resultCode The code of the result.
+ */
+ override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
+ if (requestCode == PIN_REQUEST) {
+ if (resultCode == Activity.RESULT_OK) {
+ creditCardPicker?.onAuthSuccess()
+ } else {
+ creditCardPicker?.onAuthFailure()
+ }
+
+ return true
+ }
+
+ return filePicker.onActivityResult(requestCode, resultCode, data)
+ }
+
+ /**
+ * Notifies the feature that the biometric authentication was completed. It will then
+ * either process or dismiss the prompt request.
+ *
+ * @param isAuthenticated True if the user is authenticated successfully from the biometric
+ * authentication prompt or false otherwise.
+ */
+ fun onBiometricResult(isAuthenticated: Boolean) {
+ if (isAuthenticated) {
+ creditCardPicker?.onAuthSuccess()
+ } else {
+ creditCardPicker?.onAuthFailure()
+ }
+ }
+
+ /**
+ * Notifies the feature that the permissions request was completed. It will then
+ * either process or dismiss the prompt request.
+ *
+ * @param permissions List of permission requested.
+ * @param grantResults The grant results for the corresponding permissions
+ * @see [onNeedToRequestPermissions].
+ */
+ override fun onPermissionsResult(permissions: Array<String>, grantResults: IntArray) {
+ filePicker.onPermissionsResult(permissions, grantResults)
+ }
+
+ /**
+ * Invoked when a native dialog needs to be shown.
+ *
+ * @param session The session which requested the dialog.
+ */
+ @Suppress("NestedBlockDepth")
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun onPromptRequested(session: SessionState) {
+ // Some requests are handle with intents
+ session.content.promptRequests.lastOrNull()?.let { promptRequest ->
+ store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.let {
+ promptRequest.executeIfWindowedPrompt { exitFullscreenUsecase(it.id) }
+ }
+
+ when (promptRequest) {
+ is File -> {
+ emitPromptDisplayedFact(promptName = "FilePrompt")
+ filePicker.handleFileRequest(promptRequest)
+ }
+
+ is Share -> handleShareRequest(promptRequest, session)
+ is SelectCreditCard -> {
+ emitSuccessfulCreditCardAutofillFormDetectedFact()
+ if (isCreditCardAutofillEnabled() && promptRequest.creditCards.isNotEmpty()) {
+ creditCardPicker?.handleSelectCreditCardRequest(promptRequest)
+ }
+ }
+
+ is SelectLoginPrompt -> {
+ if (promptRequest.logins.isEmpty()) {
+ if (isSuggestStrongPasswordEnabled) {
+ val currentUrl =
+ store.state.findTabOrCustomTabOrSelectedTab(customTabId)?.content?.url
+ if (currentUrl != null) {
+ strongPasswordPromptViewListener?.handleSuggestStrongPasswordRequest(
+ promptRequest,
+ currentUrl,
+ onSaveLoginWithStrongPassword,
+ )
+ }
+ }
+ } else {
+ loginPicker?.handleSelectLoginRequest(promptRequest)
+ }
+ emitPromptDisplayedFact(promptName = "SelectLoginPrompt")
+ }
+
+ is SelectAddress -> {
+ emitSuccessfulAddressAutofillFormDetectedFact()
+ if (isAddressAutofillEnabled() && promptRequest.addresses.isNotEmpty()) {
+ addressPicker?.handleSelectAddressRequest(promptRequest)
+ }
+ }
+
+ else -> handleDialogsRequest(promptRequest, session)
+ }
+ }
+ }
+
+ /**
+ * Invoked when a dialog is dismissed. This consumes the [PromptFeature]
+ * value from the session indicated by [sessionId].
+ *
+ * @param sessionId this is the id of the session which requested the prompt.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog was shown.
+ * @param value an optional value provided by the dialog as a result of canceling the action.
+ */
+ override fun onCancel(sessionId: String, promptRequestUID: String, value: Any?) {
+ store.consumePromptFrom(sessionId, promptRequestUID, activePrompt) {
+ emitPromptDismissedFact(promptName = it::class.simpleName.ifNullOrEmpty { "" })
+ when (it) {
+ is BeforeUnload -> it.onStay()
+ is Popup -> {
+ val shouldNotShowMoreDialogs = value as Boolean
+ promptAbuserDetector.userWantsMoreDialogs(!shouldNotShowMoreDialogs)
+ it.onDeny()
+ }
+
+ is Dismissible -> it.onDismiss()
+ else -> {
+ // no-op
+ }
+ }
+ }
+ }
+
+ /**
+ * Invoked when the user confirms the action on the dialog. This consumes
+ * the [PromptFeature] value from the [SessionState] indicated by [sessionId].
+ *
+ * @param sessionId that requested to show the dialog.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog was shown.
+ * @param value an optional value provided by the dialog as a result of confirming the action.
+ */
+ @Suppress("UNCHECKED_CAST", "ComplexMethod")
+ override fun onConfirm(sessionId: String, promptRequestUID: String, value: Any?) {
+ store.consumePromptFrom(sessionId, promptRequestUID, activePrompt) {
+ when (it) {
+ is TimeSelection -> it.onConfirm(value as Date)
+ is Color -> it.onConfirm(value as String)
+ is Alert -> {
+ val shouldNotShowMoreDialogs = value as Boolean
+ promptAbuserDetector.userWantsMoreDialogs(!shouldNotShowMoreDialogs)
+ it.onConfirm(!shouldNotShowMoreDialogs)
+ }
+
+ is SingleChoice -> it.onConfirm(value as Choice)
+ is MenuChoice -> it.onConfirm(value as Choice)
+ is BeforeUnload -> it.onLeave()
+ is Popup -> {
+ val shouldNotShowMoreDialogs = value as Boolean
+ promptAbuserDetector.userWantsMoreDialogs(!shouldNotShowMoreDialogs)
+ it.onAllow()
+ }
+
+ is MultipleChoice -> it.onConfirm(value as Array<Choice>)
+
+ is Authentication -> {
+ val (user, password) = value as Pair<String, String>
+ it.onConfirm(user, password)
+ }
+
+ is TextPrompt -> {
+ val (shouldNotShowMoreDialogs, text) = value as Pair<Boolean, String>
+
+ promptAbuserDetector.userWantsMoreDialogs(!shouldNotShowMoreDialogs)
+ it.onConfirm(!shouldNotShowMoreDialogs, text)
+ }
+
+ is Share -> it.onSuccess()
+
+ is SaveCreditCard -> it.onConfirm(value as CreditCardEntry)
+ is SaveLoginPrompt -> it.onConfirm(value as LoginEntry)
+
+ is Confirm -> {
+ val (isCheckBoxChecked, buttonType) =
+ value as Pair<Boolean, MultiButtonDialogFragment.ButtonType>
+ promptAbuserDetector.userWantsMoreDialogs(!isCheckBoxChecked)
+ when (buttonType) {
+ MultiButtonDialogFragment.ButtonType.POSITIVE ->
+ it.onConfirmPositiveButton(!isCheckBoxChecked)
+
+ MultiButtonDialogFragment.ButtonType.NEGATIVE ->
+ it.onConfirmNegativeButton(!isCheckBoxChecked)
+
+ MultiButtonDialogFragment.ButtonType.NEUTRAL ->
+ it.onConfirmNeutralButton(!isCheckBoxChecked)
+ }
+ }
+
+ is Repost -> it.onConfirm()
+ is PromptRequest.IdentityCredential.SelectProvider -> it.onConfirm(value as Provider)
+ is PromptRequest.IdentityCredential.SelectAccount -> it.onConfirm(value as Account)
+ is PromptRequest.IdentityCredential.PrivacyPolicy -> it.onConfirm(value as Boolean)
+ else -> {
+ // no-op
+ }
+ }
+ emitPromptConfirmedFact(it::class.simpleName.ifNullOrEmpty { "" })
+ }
+ }
+
+ /**
+ * Invoked when the user is requesting to clear the selected value from the dialog.
+ * This consumes the [PromptFeature] value from the [SessionState] indicated by [sessionId].
+ *
+ * @param sessionId that requested to show the dialog.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog was shown.
+ */
+ override fun onClear(sessionId: String, promptRequestUID: String) {
+ store.consumePromptFrom(sessionId, promptRequestUID, activePrompt) {
+ when (it) {
+ is TimeSelection -> it.onClear()
+ else -> {
+ // no-op
+ }
+ }
+ }
+ }
+
+ /**
+ * Re-attaches a fragment that is still visible but not linked to this feature anymore.
+ */
+ private fun reattachFragment(fragment: PromptDialogFragment) {
+ val session = store.state.findTabOrCustomTab(fragment.sessionId)
+ if (session?.content?.promptRequests?.isEmpty() != false) {
+ fragmentManager.beginTransaction()
+ .remove(fragment)
+ .commitAllowingStateLoss()
+ return
+ }
+ // Re-assign the feature instance so that the fragment can invoke us once the user makes a selection or cancels
+ // the dialog.
+ fragment.feature = this
+ }
+
+ private fun handleShareRequest(promptRequest: Share, session: SessionState) {
+ emitPromptDisplayedFact(promptName = "ShareSheet")
+ shareDelegate.showShareSheet(
+ context = container.context,
+ shareData = promptRequest.data,
+ onDismiss = {
+ emitPromptDismissedFact(promptName = "ShareSheet")
+ onCancel(session.id, promptRequest.uid)
+ },
+ onSuccess = { onConfirm(session.id, promptRequest.uid, null) },
+ )
+ }
+
+ /**
+ * Called from on [onPromptRequested] to handle requests for showing native dialogs.
+ */
+ @Suppress("ComplexMethod", "LongMethod")
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun handleDialogsRequest(
+ promptRequest: PromptRequest,
+ session: SessionState,
+ ) {
+ // Requests that are handled with dialogs
+ val dialog = when (promptRequest) {
+ is SaveCreditCard -> {
+ if (!isCreditCardAutofillEnabled.invoke() || creditCardValidationDelegate == null ||
+ !promptRequest.creditCard.isValid
+ ) {
+ dismissDialogRequest(promptRequest, session)
+
+ if (creditCardValidationDelegate == null) {
+ logger.debug(
+ "Ignoring received SaveCreditCard because PromptFeature." +
+ "creditCardValidationDelegate is null. If you are trying to autofill " +
+ "credit cards, try attaching a CreditCardValidationDelegate to PromptFeature",
+ )
+ }
+
+ return
+ }
+
+ emitCreditCardSaveShownFact()
+
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequestUID = promptRequest.uid,
+ shouldDismissOnLoad = false,
+ creditCard = promptRequest.creditCard,
+ )
+ }
+
+ is SaveLoginPrompt -> {
+ if (!isSaveLoginEnabled.invoke() || loginValidationDelegate == null) {
+ dismissDialogRequest(promptRequest, session)
+
+ if (loginValidationDelegate == null) {
+ logger.debug(
+ "Ignoring received SaveLoginPrompt because PromptFeature." +
+ "loginValidationDelegate is null. If you are trying to autofill logins, " +
+ "try attaching a LoginValidationDelegate to PromptFeature",
+ )
+ }
+
+ return
+ }
+
+ SaveLoginDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequestUID = promptRequest.uid,
+ shouldDismissOnLoad = false,
+ hint = promptRequest.hint,
+ // For v1, we only handle a single login and drop all others on the floor
+ entry = promptRequest.logins[0],
+ icon = session.content.icon,
+ )
+ }
+
+ is SingleChoice -> ChoiceDialogFragment.newInstance(
+ promptRequest.choices,
+ session.id,
+ promptRequest.uid,
+ true,
+ SINGLE_CHOICE_DIALOG_TYPE,
+ )
+
+ is MultipleChoice -> ChoiceDialogFragment.newInstance(
+ promptRequest.choices,
+ session.id,
+ promptRequest.uid,
+ true,
+ MULTIPLE_CHOICE_DIALOG_TYPE,
+ )
+
+ is MenuChoice -> ChoiceDialogFragment.newInstance(
+ promptRequest.choices,
+ session.id,
+ promptRequest.uid,
+ true,
+ MENU_CHOICE_DIALOG_TYPE,
+ )
+
+ is Alert -> {
+ with(promptRequest) {
+ AlertDialogFragment.newInstance(
+ session.id,
+ promptRequest.uid,
+ true,
+ title,
+ message,
+ promptAbuserDetector.areDialogsBeingAbused(),
+ )
+ }
+ }
+
+ is TimeSelection -> {
+ val selectionType = when (promptRequest.type) {
+ TimeSelection.Type.DATE -> TimePickerDialogFragment.SELECTION_TYPE_DATE
+ TimeSelection.Type.DATE_AND_TIME -> TimePickerDialogFragment.SELECTION_TYPE_DATE_AND_TIME
+ TimeSelection.Type.TIME -> TimePickerDialogFragment.SELECTION_TYPE_TIME
+ TimeSelection.Type.MONTH -> TimePickerDialogFragment.SELECTION_TYPE_MONTH
+ }
+
+ with(promptRequest) {
+ TimePickerDialogFragment.newInstance(
+ session.id,
+ promptRequest.uid,
+ true,
+ initialDate,
+ minimumDate,
+ maximumDate,
+ selectionType,
+ stepValue,
+ )
+ }
+ }
+
+ is TextPrompt -> {
+ with(promptRequest) {
+ TextPromptDialogFragment.newInstance(
+ session.id,
+ promptRequest.uid,
+ true,
+ title,
+ inputLabel,
+ inputValue,
+ promptAbuserDetector.areDialogsBeingAbused(),
+ )
+ }
+ }
+
+ is Authentication -> {
+ with(promptRequest) {
+ AuthenticationDialogFragment.newInstance(
+ session.id,
+ promptRequest.uid,
+ true,
+ title,
+ message,
+ userName,
+ password,
+ onlyShowPassword,
+ uri,
+ )
+ }
+ }
+
+ is Color -> ColorPickerDialogFragment.newInstance(
+ session.id,
+ promptRequest.uid,
+ true,
+ promptRequest.defaultColor,
+ )
+
+ is Popup -> {
+ val title = container.getString(R.string.mozac_feature_prompts_popup_dialog_title)
+ val positiveLabel = container.getString(R.string.mozac_feature_prompts_allow)
+ val negativeLabel = container.getString(R.string.mozac_feature_prompts_deny)
+
+ ConfirmDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequest.uid,
+ title = title,
+ message = promptRequest.targetUri,
+ positiveButtonText = positiveLabel,
+ negativeButtonText = negativeLabel,
+ hasShownManyDialogs = promptAbuserDetector.areDialogsBeingAbused(),
+ shouldDismissOnLoad = true,
+ )
+ }
+
+ is BeforeUnload -> {
+ val title =
+ container.getString(R.string.mozac_feature_prompt_before_unload_dialog_title)
+ val body =
+ container.getString(R.string.mozac_feature_prompt_before_unload_dialog_body)
+ val leaveLabel =
+ container.getString(R.string.mozac_feature_prompts_before_unload_leave)
+ val stayLabel =
+ container.getString(R.string.mozac_feature_prompts_before_unload_stay)
+
+ ConfirmDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequest.uid,
+ title = title,
+ message = body,
+ positiveButtonText = leaveLabel,
+ negativeButtonText = stayLabel,
+ shouldDismissOnLoad = true,
+ )
+ }
+
+ is Confirm -> {
+ with(promptRequest) {
+ val positiveButton = positiveButtonTitle.ifEmpty {
+ container.getString(R.string.mozac_feature_prompts_ok)
+ }
+ val negativeButton = negativeButtonTitle.ifEmpty {
+ container.getString(R.string.mozac_feature_prompts_cancel)
+ }
+
+ MultiButtonDialogFragment.newInstance(
+ session.id,
+ promptRequest.uid,
+ title,
+ message,
+ promptAbuserDetector.areDialogsBeingAbused(),
+ false,
+ positiveButton,
+ negativeButton,
+ neutralButtonTitle,
+ )
+ }
+ }
+
+ is Repost -> {
+ val title = container.context.getString(R.string.mozac_feature_prompt_repost_title)
+ val message =
+ container.context.getString(R.string.mozac_feature_prompt_repost_message)
+ val positiveAction =
+ container.context.getString(R.string.mozac_feature_prompt_repost_positive_button_text)
+ val negativeAction =
+ container.context.getString(R.string.mozac_feature_prompt_repost_negative_button_text)
+
+ ConfirmDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequestUID = promptRequest.uid,
+ shouldDismissOnLoad = true,
+ title = title,
+ message = message,
+ positiveButtonText = positiveAction,
+ negativeButtonText = negativeAction,
+ )
+ }
+
+ is PromptRequest.IdentityCredential.SelectProvider -> {
+ SelectProviderDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequestUID = promptRequest.uid,
+ shouldDismissOnLoad = true,
+ providers = promptRequest.providers,
+ colorsProvider = identityCredentialColorsProvider,
+ )
+ }
+
+ is PromptRequest.IdentityCredential.SelectAccount -> {
+ SelectAccountDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequestUID = promptRequest.uid,
+ shouldDismissOnLoad = true,
+ accounts = promptRequest.accounts,
+ provider = promptRequest.provider,
+ colorsProvider = identityCredentialColorsProvider,
+ )
+ }
+
+ is PromptRequest.IdentityCredential.PrivacyPolicy -> {
+ val title =
+ container.getString(
+ R.string.mozac_feature_prompts_identity_credentials_privacy_policy_title,
+ promptRequest.providerDomain,
+ )
+ val message =
+ container.getString(
+ R.string.mozac_feature_prompts_identity_credentials_privacy_policy_description,
+ promptRequest.host,
+ promptRequest.providerDomain,
+ promptRequest.privacyPolicyUrl,
+ promptRequest.termsOfServiceUrl,
+ )
+ PrivacyPolicyDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequestUID = promptRequest.uid,
+ shouldDismissOnLoad = true,
+ title = title,
+ message = message,
+ icon = promptRequest.icon,
+ )
+ }
+
+ else -> throw InvalidParameterException("Not valid prompt request type $promptRequest")
+ }
+
+ dialog.feature = this
+
+ if (canShowThisPrompt(promptRequest)) {
+ // If the ChoiceDialogFragment's choices data were updated,
+ // we need to dismiss the previous dialog
+ activePrompt?.get()?.let { promptDialog ->
+ // ChoiceDialogFragment could update their choices data,
+ // and we need to dismiss the previous UI dialog,
+ // without consuming the engine callbacks, and allow to create a new dialog with the
+ // updated data.
+ if (promptDialog is ChoiceDialogFragment &&
+ !session.content.promptRequests.any { it.uid == promptDialog.promptRequestUID }
+ ) {
+ // We want to avoid consuming the engine callbacks and allow a new dialog
+ // to be created with the updated data.
+ promptDialog.feature = null
+ promptDialog.dismiss()
+ }
+ }
+
+ emitPromptDisplayedFact(promptName = dialog::class.simpleName.ifNullOrEmpty { "" })
+ dialog.show(fragmentManager, FRAGMENT_TAG)
+ activePrompt = WeakReference(dialog)
+
+ if (promptRequest.shouldDismissOnLoad) {
+ activePromptsToDismiss.add(dialog)
+ }
+ } else {
+ dismissDialogRequest(promptRequest, session)
+ }
+ promptAbuserDetector.updateJSDialogAbusedState()
+ }
+
+ /**
+ * Dismiss and consume the given prompt request for the session.
+ */
+ @VisibleForTesting
+ internal fun dismissDialogRequest(promptRequest: PromptRequest, session: SessionState) {
+ (promptRequest as Dismissible).onDismiss()
+ store.dispatch(ContentAction.ConsumePromptRequestAction(session.id, promptRequest))
+ emitPromptDismissedFact(promptName = promptRequest::class.simpleName.ifNullOrEmpty { "" })
+ }
+
+ private fun canShowThisPrompt(promptRequest: PromptRequest): Boolean {
+ return when (promptRequest) {
+ is SingleChoice,
+ is MultipleChoice,
+ is MenuChoice,
+ is TimeSelection,
+ is File,
+ is Color,
+ is Authentication,
+ is BeforeUnload,
+ is SaveLoginPrompt,
+ is SelectLoginPrompt,
+ is SelectCreditCard,
+ is SaveCreditCard,
+ is SelectAddress,
+ is Share,
+ is PromptRequest.IdentityCredential.SelectProvider,
+ is PromptRequest.IdentityCredential.SelectAccount,
+ is PromptRequest.IdentityCredential.PrivacyPolicy,
+ -> true
+
+ is Alert, is TextPrompt, is Confirm, is Repost, is Popup -> promptAbuserDetector.shouldShowMoreDialogs
+ }
+ }
+
+ /**
+ * Dismisses the select prompts if they are active and visible.
+ *
+ * @returns true if a select prompt was dismissed, otherwise false.
+ */
+ @VisibleForTesting
+ fun dismissSelectPrompts(): Boolean {
+ var result = false
+
+ (activePromptRequest as? SelectLoginPrompt)?.let { selectLoginPrompt ->
+ loginPicker?.let { loginPicker ->
+ if (loginDelegate.loginPickerView?.asView()?.isVisible == true) {
+ loginPicker.dismissCurrentLoginSelect(selectLoginPrompt)
+ result = true
+ }
+ }
+ }
+
+ (activePromptRequest as? SelectCreditCard)?.let { selectCreditCardPrompt ->
+ creditCardPicker?.let { creditCardPicker ->
+ if (creditCardDelegate.creditCardPickerView?.asView()?.isVisible == true) {
+ creditCardPicker.dismissSelectCreditCardRequest(selectCreditCardPrompt)
+ result = true
+ }
+ }
+ }
+
+ (activePromptRequest as? SelectAddress)?.let { selectAddressPrompt ->
+ addressPicker?.let { addressPicker ->
+ if (addressDelegate.addressPickerView?.asView()?.isVisible == true) {
+ addressPicker.dismissSelectAddressRequest(selectAddressPrompt)
+ result = true
+ }
+ }
+ }
+
+ return result
+ }
+
+ companion object {
+ // The PIN request code
+ const val PIN_REQUEST = 303
+ }
+}
+
+/**
+ * Removes the [PromptRequest] indicated by [promptRequestUID] from the current Session if it it exists
+ * and offers a [consume] callback for other optional side effects.
+ *
+ * @param sessionId Session id of the tab or custom tab in which to try consuming [PromptRequests].
+ * If the id is not provided or a tab with that id is not found the method will act on the current tab.
+ * @param promptRequestUID Id of the [PromptRequest] to be consumed.
+ * @param activePrompt The current active Prompt if known. If provided it will always be cleared,
+ * irrespective of if [PromptRequest] indicated by [promptRequestUID] is found and removed or not.
+ * @param consume callback with the [PromptRequest] if found, before being removed from the Session.
+ */
+internal fun BrowserStore.consumePromptFrom(
+ sessionId: String?,
+ promptRequestUID: String,
+ activePrompt: WeakReference<PromptDialogFragment>? = null,
+ consume: (PromptRequest) -> Unit,
+) {
+ state.findTabOrCustomTabOrSelectedTab(sessionId)?.let { tab ->
+ activePrompt?.clear()
+ tab.content.promptRequests.firstOrNull { it.uid == promptRequestUID }?.let {
+ consume(it)
+ dispatch(ContentAction.ConsumePromptRequestAction(tab.id, it))
+ }
+ }
+}
+
+/**
+ * Removes the most recent [PromptRequest] of type [P] from the current Session if it it exists
+ * and offers a [consume] callback for other optional side effects.
+ *
+ * @param sessionId Session id of the tab or custom tab in which to try consuming [PromptRequests].
+ * If the id is not provided or a tab with that id is not found the method will act on the current tab.
+ * @param activePrompt The current active Prompt if known. If provided it will always be cleared,
+ * irrespective of if [PromptRequest] indicated by [promptRequestUID] is found and removed or not.
+ * @param consume callback with the [PromptRequest] if found, before being removed from the Session.
+ */
+internal inline fun <reified P : PromptRequest> BrowserStore.consumePromptFrom(
+ sessionId: String?,
+ activePrompt: WeakReference<PromptDialogFragment>? = null,
+ consume: (P) -> Unit,
+) {
+ state.findTabOrCustomTabOrSelectedTab(sessionId)?.let { tab ->
+ activePrompt?.clear()
+ tab.content.promptRequests.lastOrNull { it is P }?.let {
+ consume(it as P)
+ dispatch(ContentAction.ConsumePromptRequestAction(tab.id, it))
+ }
+ }
+}
+
+/**
+ * Filters and removes all [PromptRequest]s from the current Session if it it exists
+ * and offers a [consume] callback for other optional side effects on each filtered [PromptRequest].
+ *
+ * @param sessionId Session id of the tab or custom tab in which to try consuming [PromptRequests].
+ * If the id is not provided or a tab with that id is not found the method will act on the current tab.
+ * @param activePrompt The current active Prompt if known. If provided it will always be cleared,
+ * irrespective of if [PromptRequest] indicated by [promptRequestUID] is found and removed or not.
+ * @param predicate function allowing matching only specific [PromptRequest]s from all contained in the Session.
+ * @param consume callback with the [PromptRequest] if found, before being removed from the Session.
+ */
+internal fun BrowserStore.consumeAllSessionPrompts(
+ sessionId: String?,
+ activePrompt: WeakReference<PromptDialogFragment>? = null,
+ predicate: (PromptRequest) -> Boolean,
+ consume: (PromptRequest) -> Unit = { },
+) {
+ state.findTabOrCustomTabOrSelectedTab(sessionId)?.let { tab ->
+ activePrompt?.clear()
+ tab.content.promptRequests
+ .filter { predicate(it) }
+ .forEach {
+ consume(it)
+ dispatch(ContentAction.ConsumePromptRequestAction(tab.id, it))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptMiddleware.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptMiddleware.kt
new file mode 100644
index 0000000000..5c0d3fe6cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptMiddleware.kt
@@ -0,0 +1,59 @@
+/* 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.prompts
+
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] implementation for managing [PromptRequest]s.
+ */
+class PromptMiddleware : Middleware<BrowserState, BrowserAction> {
+
+ private val scope = MainScope()
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is ContentAction.UpdatePromptRequestAction -> {
+ if (shouldBlockPrompt(action, context)) {
+ return
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+ }
+
+ private fun shouldBlockPrompt(
+ action: ContentAction.UpdatePromptRequestAction,
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ ): Boolean {
+ if (action.promptRequest is PromptRequest.Popup) {
+ context.state.findTab(action.sessionId)?.let {
+ if (it.content.promptRequests.lastOrNull { prompt -> prompt is PromptRequest.Popup } != null) {
+ scope.launch {
+ (action.promptRequest as PromptRequest.Popup).onDeny()
+ }
+ return true
+ }
+ }
+ }
+ return false
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt
new file mode 100644
index 0000000000..055d231d1d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt
@@ -0,0 +1,68 @@
+/* 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.prompts.address
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.R
+
+@VisibleForTesting
+internal object AddressDiffCallback : DiffUtil.ItemCallback<Address>() {
+ override fun areItemsTheSame(oldItem: Address, newItem: Address) =
+ oldItem.guid == newItem.guid
+
+ override fun areContentsTheSame(oldItem: Address, newItem: Address) =
+ oldItem == newItem
+}
+
+/**
+ * RecyclerView adapter for displaying address items.
+ */
+internal class AddressAdapter(
+ private val onAddressSelected: (Address) -> Unit,
+) : ListAdapter<Address, AddressViewHolder>(AddressDiffCallback) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddressViewHolder {
+ val view = LayoutInflater
+ .from(parent.context)
+ .inflate(R.layout.mozac_feature_prompts_address_list_item, parent, false)
+ return AddressViewHolder(view, onAddressSelected)
+ }
+
+ override fun onBindViewHolder(holder: AddressViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+/**
+ * View holder for a address item.
+ */
+@VisibleForTesting
+internal class AddressViewHolder(
+ itemView: View,
+ private val onAddressSelected: (Address) -> Unit,
+) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
+ @VisibleForTesting
+ lateinit var address: Address
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ fun bind(address: Address) {
+ this.address = address
+ itemView.findViewById<TextView>(R.id.address_name)?.text = address.addressLabel
+ }
+
+ override fun onClick(v: View?) {
+ onAddressSelected(address)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt
new file mode 100644
index 0000000000..00838b8965
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt
@@ -0,0 +1,33 @@
+/* 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.prompts.address
+
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+
+/**
+ * Delegate for address picker
+ */
+interface AddressDelegate {
+ /**
+ * The [SelectablePromptView] used for [AddressPicker] to display a
+ * selectable prompt list of address options.
+ */
+ val addressPickerView: SelectablePromptView<Address>?
+
+ /**
+ * Callback invoked when the user clicks "Manage addresses" from
+ * select address prompt.
+ */
+ val onManageAddresses: () -> Unit
+}
+
+/**
+ * Default implementation for address picker delegate
+ */
+class DefaultAddressDelegate(
+ override val addressPickerView: SelectablePromptView<Address>? = null,
+ override val onManageAddresses: () -> Unit = {},
+) : AddressDelegate
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt
new file mode 100644
index 0000000000..b1be8ac212
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt
@@ -0,0 +1,89 @@
+/* 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.prompts.address
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.consumePromptFrom
+import mozilla.components.feature.prompts.facts.emitAddressAutofillDismissedFact
+import mozilla.components.feature.prompts.facts.emitAddressAutofillShownFact
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Interactor that implements [SelectablePromptView.Listener] and notifies the feature about actions
+ * the user performed in the address picker.
+ *
+ * @property store The [BrowserStore] this feature should subscribe to.
+ * @property addressSelectBar The [SelectablePromptView] view into which the select address
+ * prompt will be inflated.
+ * @property onManageAddresses Callback invoked when user clicks on "Manage adresses" button from
+ * select address prompt.
+ * @property sessionId The session ID which requested the prompt.
+ */
+class AddressPicker(
+ private val store: BrowserStore,
+ private val addressSelectBar: SelectablePromptView<Address>,
+ private val onManageAddresses: () -> Unit = {},
+ private var sessionId: String? = null,
+) : SelectablePromptView.Listener<Address> {
+
+ init {
+ addressSelectBar.listener = this
+ }
+
+ /**
+ * Shows the select address prompt in response to the [PromptRequest] event.
+ *
+ * @param request The [PromptRequest] containing the the address request data to be shown.
+ */
+ internal fun handleSelectAddressRequest(request: PromptRequest.SelectAddress) {
+ emitAddressAutofillShownFact()
+ addressSelectBar.showPrompt(request.addresses)
+ }
+
+ /**
+ * Dismisses the active [PromptRequest.SelectAddress] request.
+ *
+ * @param promptRequest The current active [PromptRequest.SelectAddress] or null
+ * otherwise.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ fun dismissSelectAddressRequest(promptRequest: PromptRequest.SelectAddress? = null) {
+ emitAddressAutofillDismissedFact()
+ addressSelectBar.hidePrompt()
+
+ try {
+ if (promptRequest != null) {
+ promptRequest.onDismiss()
+ sessionId?.let {
+ store.dispatch(ContentAction.ConsumePromptRequestAction(it, promptRequest))
+ }
+ return
+ }
+
+ store.consumePromptFrom<PromptRequest.SelectAddress>(sessionId) {
+ it.onDismiss()
+ }
+ } catch (e: RuntimeException) {
+ Logger.error("Can't dismiss select address prompt", e)
+ }
+ }
+
+ override fun onOptionSelect(option: Address) {
+ store.consumePromptFrom<PromptRequest.SelectAddress>(sessionId) {
+ it.onConfirm(option)
+ }
+
+ addressSelectBar.hidePrompt()
+ }
+
+ override fun onManageOptions() {
+ onManageAddresses.invoke()
+ dismissSelectAddressRequest()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt
new file mode 100644
index 0000000000..693b2cde34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt
@@ -0,0 +1,151 @@
+/* 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.prompts.address
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.withStyledAttributes
+import androidx.core.view.isVisible
+import androidx.core.widget.ImageViewCompat
+import androidx.core.widget.TextViewCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.facts.emitAddressAutofillExpandedFact
+import mozilla.components.feature.prompts.facts.emitSuccessfulAddressAutofillSuccessFact
+import mozilla.components.support.ktx.android.view.hideKeyboard
+
+/**
+ * A customizable "Select addresses" bar implementing [SelectablePromptView].
+ */
+class AddressSelectBar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr), SelectablePromptView<Address> {
+
+ private var view: View? = null
+ private var recyclerView: RecyclerView? = null
+ private var headerView: AppCompatTextView? = null
+ private var expanderView: AppCompatImageView? = null
+ private var manageAddressesView: AppCompatTextView? = null
+ private var headerTextStyle: Int? = null
+
+ private val listAdapter = AddressAdapter { address ->
+ listener?.apply {
+ onOptionSelect(address)
+ emitSuccessfulAddressAutofillSuccessFact()
+ }
+ }
+
+ override var listener: SelectablePromptView.Listener<Address>? = null
+
+ init {
+ context.withStyledAttributes(
+ attrs,
+ R.styleable.AddressSelectBar,
+ defStyleAttr,
+ 0,
+ ) {
+ val textStyle =
+ getResourceId(
+ R.styleable.AddressSelectBar_mozacSelectAddressHeaderTextStyle,
+ 0,
+ )
+
+ if (textStyle > 0) {
+ headerTextStyle = textStyle
+ }
+ }
+ }
+
+ override fun hidePrompt() {
+ this.isVisible = false
+ recyclerView?.isVisible = false
+ manageAddressesView?.isVisible = false
+
+ listAdapter.submitList(null)
+
+ toggleSelectAddressHeader(shouldExpand = false)
+ }
+
+ override fun showPrompt(options: List<Address>) {
+ if (view == null) {
+ view = View.inflate(context, LAYOUT_ID, this)
+ bindViews()
+ }
+
+ listAdapter.submitList(options)
+ view?.isVisible = true
+ }
+
+ private fun bindViews() {
+ recyclerView = findViewById<RecyclerView>(R.id.address_list).apply {
+ layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ adapter = listAdapter
+ }
+
+ headerView = findViewById<AppCompatTextView>(R.id.select_address_header).apply {
+ setOnClickListener {
+ toggleSelectAddressHeader(shouldExpand = recyclerView?.isVisible != true)
+ }
+
+ headerTextStyle?.let { appearance ->
+ TextViewCompat.setTextAppearance(this, appearance)
+ currentTextColor.let { color ->
+ TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(color))
+ }
+ }
+ }
+
+ expanderView =
+ findViewById<AppCompatImageView>(R.id.mozac_feature_address_expander).apply {
+ headerView?.currentTextColor?.let {
+ ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(it))
+ }
+ }
+
+ manageAddressesView = findViewById<AppCompatTextView>(R.id.manage_addresses).apply {
+ setOnClickListener {
+ listener?.onManageOptions()
+ }
+ }
+ }
+
+ /**
+ * Toggles the visibility of the list of address items in the prompt.
+ *
+ * @param shouldExpand True if the list of addresses should be displayed, false otherwise.
+ */
+ private fun toggleSelectAddressHeader(shouldExpand: Boolean) {
+ recyclerView?.isVisible = shouldExpand
+ manageAddressesView?.isVisible = shouldExpand
+
+ if (shouldExpand) {
+ emitAddressAutofillExpandedFact()
+ view?.hideKeyboard()
+ expanderView?.rotation = ROTATE_180
+ headerView?.contentDescription =
+ context.getString(R.string.mozac_feature_prompts_collapse_address_content_description_2)
+ } else {
+ expanderView?.rotation = 0F
+ headerView?.contentDescription =
+ context.getString(R.string.mozac_feature_prompts_expand_address_content_description_2)
+ }
+ }
+
+ companion object {
+ val LAYOUT_ID = R.layout.mozac_feature_prompts_address_select_prompt
+
+ private const val ROTATE_180 = 180F
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/concept/PasswordPromptView.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/concept/PasswordPromptView.kt
new file mode 100644
index 0000000000..71dd678d09
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/concept/PasswordPromptView.kt
@@ -0,0 +1,42 @@
+/* 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.prompts.concept
+
+/**
+ * An interface for views that can display a generated strong password prompt.
+ */
+interface PasswordPromptView {
+
+ var listener: Listener?
+
+ /**
+ * Shows a simple prompt with the given [generatedPassword].
+ */
+ fun showPrompt(
+ generatedPassword: String,
+ url: String,
+ onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
+ )
+
+ /**
+ * Hides the prompt.
+ */
+ fun hidePrompt()
+
+ /**
+ * Interface to allow a class to listen to generated strong password event events.
+ */
+ interface Listener {
+ /**
+ * Called when a user wants to use a strong generated password.
+ *
+ */
+ fun onUseGeneratedPassword(
+ generatedPassword: String,
+ url: String,
+ onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/concept/SelectablePromptView.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/concept/SelectablePromptView.kt
new file mode 100644
index 0000000000..0db9a256a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/concept/SelectablePromptView.kt
@@ -0,0 +1,49 @@
+/* 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.prompts.concept
+
+import android.view.View
+
+/**
+ * An interface for views that can display an option selection prompt.
+ */
+interface SelectablePromptView<T> {
+
+ var listener: Listener<T>?
+
+ /**
+ * Shows an option selection prompt with the provided options.
+ *
+ * @param options A list of options to display in the prompt.
+ */
+ fun showPrompt(options: List<T>)
+
+ /**
+ * Hides the option selection prompt.
+ */
+ fun hidePrompt()
+
+ /**
+ * Casts this [SelectablePromptView] interface to an Android [View] object.
+ */
+ fun asView(): View = (this as View)
+
+ /**
+ * Interface to allow a class to listen to the option selection prompt events.
+ */
+ interface Listener<in T> {
+ /**
+ * Called when an user selects an options from the prompt.
+ *
+ * @param option The selected option.
+ */
+ fun onOptionSelect(option: T)
+
+ /**
+ * Called when the user invokes the option to manage the list of options.
+ */
+ fun onManageOptions()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardDelegate.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardDelegate.kt
new file mode 100644
index 0000000000..9723d0d521
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardDelegate.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.prompts.creditcard
+
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+
+/**
+ * Delegate for credit card picker and related callbacks
+ */
+interface CreditCardDelegate {
+ /**
+ * The [SelectablePromptView] used for [CreditCardPicker] to display a
+ * selectable prompt list of credit cards.
+ */
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry>?
+ get() = null
+
+ /**
+ * Callback invoked when a user selects "Manage credit cards"
+ * from the select credit card prompt.
+ */
+ val onManageCreditCards: () -> Unit
+ get() = {}
+
+ /**
+ * Callback invoked when a user selects a credit card option
+ * from the select credit card prompt
+ */
+ val onSelectCreditCard: () -> Unit
+ get() = {}
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt
new file mode 100644
index 0000000000..0c79c37086
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt
@@ -0,0 +1,48 @@
+/* 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.prompts.creditcard
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.utils.creditCardIssuerNetwork
+
+/**
+ * View holder for displaying a credit card item.
+ *
+ * @param onCreditCardSelected Callback invoked when a credit card item is selected.
+ */
+class CreditCardItemViewHolder(
+ view: View,
+ private val onCreditCardSelected: (CreditCardEntry) -> Unit,
+) : RecyclerView.ViewHolder(view) {
+
+ /**
+ * Binds the view with the provided [CreditCardEntry].
+ *
+ * @param creditCard The [CreditCardEntry] to display.
+ */
+ fun bind(creditCard: CreditCardEntry) {
+ itemView.findViewById<ImageView>(R.id.credit_card_logo)
+ .setImageResource(creditCard.cardType.creditCardIssuerNetwork().icon)
+
+ itemView.findViewById<TextView>(R.id.credit_card_number).text =
+ creditCard.obfuscatedCardNumber
+
+ itemView.findViewById<TextView>(R.id.credit_card_expiration_date).text =
+ creditCard.expiryDate
+
+ itemView.setOnClickListener {
+ onCreditCardSelected(creditCard)
+ }
+ }
+
+ companion object {
+ val LAYOUT_ID = R.layout.mozac_feature_prompts_credit_card_list_item
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt
new file mode 100644
index 0000000000..f6fd58d17c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt
@@ -0,0 +1,119 @@
+/* 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.prompts.creditcard
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.consumePromptFrom
+import mozilla.components.feature.prompts.facts.emitCreditCardAutofillDismissedFact
+import mozilla.components.feature.prompts.facts.emitCreditCardAutofillShownFact
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Interactor that implements [SelectablePromptView.Listener] and notifies the feature about actions
+ * the user performed in the credit card picker.
+ *
+ * @property store The [BrowserStore] this feature should subscribe to.
+ * @property creditCardSelectBar The [SelectablePromptView] view into which the select credit card
+ * prompt will be inflated.
+ * @property manageCreditCardsCallback A callback invoked when a user selects "Manage credit cards"
+ * from the select credit card prompt.
+ * @property selectCreditCardCallback A callback invoked when a user selects a credit card option
+ * from the select credit card prompt
+ * @property sessionId The session ID which requested the prompt.
+ */
+class CreditCardPicker(
+ private val store: BrowserStore,
+ private val creditCardSelectBar: SelectablePromptView<CreditCardEntry>,
+ private val manageCreditCardsCallback: () -> Unit = {},
+ private val selectCreditCardCallback: () -> Unit = {},
+ private var sessionId: String? = null,
+) : SelectablePromptView.Listener<CreditCardEntry> {
+
+ init {
+ creditCardSelectBar.listener = this
+ }
+
+ // The selected credit card option to confirm.
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var selectedCreditCard: CreditCardEntry? = null
+
+ override fun onManageOptions() {
+ manageCreditCardsCallback.invoke()
+ dismissSelectCreditCardRequest()
+ }
+
+ override fun onOptionSelect(option: CreditCardEntry) {
+ selectedCreditCard = option
+ creditCardSelectBar.hidePrompt()
+ selectCreditCardCallback.invoke()
+ }
+
+ /**
+ * Called on a successful authentication to confirm the selected credit card option.
+ */
+ fun onAuthSuccess() {
+ store.consumePromptFrom<PromptRequest.SelectCreditCard>(sessionId) {
+ selectedCreditCard?.let { creditCard ->
+ it.onConfirm(creditCard)
+ }
+
+ selectedCreditCard = null
+ }
+ }
+
+ /**
+ * Called on a failed authentication to dismiss the current select credit card prompt request.
+ */
+ fun onAuthFailure() {
+ selectedCreditCard = null
+
+ store.consumePromptFrom<PromptRequest.SelectCreditCard>(sessionId) {
+ it.onDismiss()
+ }
+ }
+
+ /**
+ * Dismisses the active select credit card request.
+ *
+ * @param promptRequest The current active [PromptRequest.SelectCreditCard] or null
+ * otherwise.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ fun dismissSelectCreditCardRequest(promptRequest: PromptRequest.SelectCreditCard? = null) {
+ emitCreditCardAutofillDismissedFact()
+ creditCardSelectBar.hidePrompt()
+
+ try {
+ if (promptRequest != null) {
+ promptRequest.onDismiss()
+ sessionId?.let {
+ store.dispatch(ContentAction.ConsumePromptRequestAction(it, promptRequest))
+ }
+ return
+ }
+
+ store.consumePromptFrom<PromptRequest.SelectCreditCard>(sessionId) {
+ it.onDismiss()
+ }
+ } catch (e: RuntimeException) {
+ Logger.error("Can't dismiss this select credit card prompt", e)
+ }
+ }
+
+ /**
+ * Shows the select credit card prompt in response to the [PromptRequest] event.
+ *
+ * @param request The [PromptRequest] containing the the credit card request data to be shown.
+ */
+ internal fun handleSelectCreditCardRequest(request: PromptRequest.SelectCreditCard) {
+ emitCreditCardAutofillShownFact()
+ creditCardSelectBar.showPrompt(request.creditCards)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragment.kt
new file mode 100644
index 0000000000..d5c5b64518
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragment.kt
@@ -0,0 +1,210 @@
+/* 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.prompts.creditcard
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardValidationDelegate.Result
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.dialog.KEY_PROMPT_UID
+import mozilla.components.feature.prompts.dialog.KEY_SESSION_ID
+import mozilla.components.feature.prompts.dialog.KEY_SHOULD_DISMISS_ON_LOAD
+import mozilla.components.feature.prompts.dialog.PromptDialogFragment
+import mozilla.components.feature.prompts.facts.emitCreditCardAutofillCreatedFact
+import mozilla.components.feature.prompts.facts.emitCreditCardAutofillUpdatedFact
+import mozilla.components.support.ktx.android.content.appName
+import mozilla.components.support.ktx.android.view.toScope
+import mozilla.components.support.utils.creditCardIssuerNetwork
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+private const val KEY_CREDIT_CARD = "KEY_CREDIT_CARD"
+
+/**
+ * [android.support.v4.app.DialogFragment] implementation to display a dialog that allows
+ * user to save a new credit card or update an existing credit card.
+ */
+internal class CreditCardSaveDialogFragment : PromptDialogFragment() {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val creditCard by lazy {
+ safeArguments.getParcelableCompat(KEY_CREDIT_CARD, CreditCardEntry::class.java)!!
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var confirmResult: Result = Result.CanBeCreated
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return BottomSheetDialog(requireContext(), R.style.MozDialogStyle).apply {
+ setCancelable(true)
+ setOnShowListener {
+ val bottomSheet =
+ findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout
+ val behavior = BottomSheetBehavior.from(bottomSheet)
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ return LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_feature_prompt_save_credit_card_prompt,
+ container,
+ false,
+ )
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ view.findViewById<ImageView>(R.id.credit_card_logo)
+ .setImageResource(creditCard.cardType.creditCardIssuerNetwork().icon)
+
+ view.findViewById<TextView>(R.id.save_credit_card_message).text =
+ getString(
+ R.string.mozac_feature_prompts_save_credit_card_prompt_body_2,
+ context?.appName,
+ )
+
+ view.findViewById<TextView>(R.id.credit_card_number).text = creditCard.obfuscatedCardNumber
+ view.findViewById<TextView>(R.id.credit_card_expiration_date).text = creditCard.expiryDate
+
+ view.findViewById<Button>(R.id.save_confirm).setOnClickListener {
+ feature?.onConfirm(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ value = creditCard,
+ )
+ dismiss()
+ emitSaveUpdateFact()
+ }
+
+ view.findViewById<Button>(R.id.save_cancel).setOnClickListener {
+ feature?.onCancel(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ )
+ dismiss()
+ }
+
+ updateUI(view)
+ }
+
+ /**
+ * Emit the save or update fact based on the confirm action for the credit card.
+ */
+ @VisibleForTesting
+ internal fun emitSaveUpdateFact() {
+ when (confirmResult) {
+ is Result.CanBeCreated -> {
+ emitCreditCardAutofillCreatedFact()
+ }
+ is Result.CanBeUpdated -> {
+ emitCreditCardAutofillUpdatedFact()
+ }
+ }
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ )
+ }
+
+ /**
+ * Updates the dialog based on whether a save or update credit card state should be displayed.
+ */
+ private fun updateUI(view: View) = view.toScope().launch(IO) {
+ val validationDelegate = feature?.creditCardValidationDelegate ?: return@launch
+ confirmResult = validationDelegate.shouldCreateOrUpdate(creditCard)
+
+ withContext(Main) {
+ when (confirmResult) {
+ is Result.CanBeCreated -> setViewText(
+ view = view,
+ header = requireContext().getString(R.string.mozac_feature_prompts_save_credit_card_prompt_title),
+ cancelButtonText = requireContext().getString(R.string.mozac_feature_prompt_not_now),
+ confirmButtonText = requireContext().getString(R.string.mozac_feature_prompt_save_confirmation),
+ )
+ is Result.CanBeUpdated -> setViewText(
+ view = view,
+ header = requireContext().getString(R.string.mozac_feature_prompts_update_credit_card_prompt_title),
+ cancelButtonText = requireContext().getString(R.string.mozac_feature_prompts_cancel),
+ confirmButtonText = requireContext().getString(R.string.mozac_feature_prompt_update_confirmation),
+ showMessageBody = false,
+ )
+ }
+ }
+ }
+
+ /**
+ * Updates the header and button text in the dialog.
+ *
+ * @param view The view associated with the dialog.
+ * @param header The header text to be displayed.
+ * @param cancelButtonText The cancel button text to be displayed.
+ * @param confirmButtonText The confirm button text to be displayed.
+ * @param showMessageBody Whether or not to show the dialog message body text.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun setViewText(
+ view: View,
+ header: String,
+ cancelButtonText: String,
+ confirmButtonText: String,
+ showMessageBody: Boolean = true,
+ ) {
+ view.findViewById<AppCompatTextView>(R.id.save_credit_card_message).isVisible =
+ showMessageBody
+ view.findViewById<AppCompatTextView>(R.id.save_credit_card_header).text = header
+ view.findViewById<Button>(R.id.save_cancel).text = cancelButtonText
+ view.findViewById<Button>(R.id.save_confirm).text = confirmButtonText
+ }
+
+ companion object {
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ creditCard: CreditCardEntry,
+ ): CreditCardSaveDialogFragment {
+ val fragment = CreditCardSaveDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putParcelable(KEY_CREDIT_CARD, creditCard)
+ }
+
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBar.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBar.kt
new file mode 100644
index 0000000000..93fcdeeca0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBar.kt
@@ -0,0 +1,152 @@
+/* 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.prompts.creditcard
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.withStyledAttributes
+import androidx.core.view.isVisible
+import androidx.core.widget.ImageViewCompat
+import androidx.core.widget.TextViewCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.facts.emitCreditCardAutofillExpandedFact
+import mozilla.components.feature.prompts.facts.emitSuccessfulCreditCardAutofillSuccessFact
+import mozilla.components.support.ktx.android.view.hideKeyboard
+
+/**
+ * A customizable "Select credit card" bar implementing [SelectablePromptView].
+ */
+class CreditCardSelectBar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr), SelectablePromptView<CreditCardEntry> {
+
+ private var view: View? = null
+ private var recyclerView: RecyclerView? = null
+ private var headerView: AppCompatTextView? = null
+ private var expanderView: AppCompatImageView? = null
+ private var manageCreditCardsButtonView: AppCompatTextView? = null
+ private var headerTextStyle: Int? = null
+
+ private val listAdapter = CreditCardsAdapter { creditCard ->
+ listener?.apply {
+ onOptionSelect(creditCard)
+ emitSuccessfulCreditCardAutofillSuccessFact()
+ }
+ }
+
+ override var listener: SelectablePromptView.Listener<CreditCardEntry>? = null
+
+ init {
+ context.withStyledAttributes(
+ attrs,
+ R.styleable.CreditCardSelectBar,
+ defStyleAttr,
+ 0,
+ ) {
+ val textStyle =
+ getResourceId(
+ R.styleable.CreditCardSelectBar_mozacSelectCreditCardHeaderTextStyle,
+ 0,
+ )
+
+ if (textStyle > 0) {
+ headerTextStyle = textStyle
+ }
+ }
+ }
+
+ override fun hidePrompt() {
+ this.isVisible = false
+ recyclerView?.isVisible = false
+ manageCreditCardsButtonView?.isVisible = false
+
+ listAdapter.submitList(null)
+
+ toggleSelectCreditCardHeader(shouldExpand = false)
+ }
+
+ override fun showPrompt(options: List<CreditCardEntry>) {
+ if (view == null) {
+ view = View.inflate(context, LAYOUT_ID, this)
+ bindViews()
+ }
+
+ listAdapter.submitList(options)
+ view?.isVisible = true
+ }
+
+ private fun bindViews() {
+ recyclerView = findViewById<RecyclerView>(R.id.credit_cards_list).apply {
+ layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ adapter = listAdapter
+ }
+
+ headerView = findViewById<AppCompatTextView>(R.id.select_credit_card_header).apply {
+ setOnClickListener {
+ toggleSelectCreditCardHeader(shouldExpand = recyclerView?.isVisible != true)
+ }
+
+ headerTextStyle?.let {
+ TextViewCompat.setTextAppearance(this, it)
+ currentTextColor.let {
+ TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(it))
+ }
+ }
+ }
+
+ expanderView =
+ findViewById<AppCompatImageView>(R.id.mozac_feature_credit_cards_expander).apply {
+ headerView?.currentTextColor?.let {
+ ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(it))
+ }
+ }
+
+ manageCreditCardsButtonView =
+ findViewById<AppCompatTextView>(R.id.manage_credit_cards).apply {
+ setOnClickListener {
+ listener?.onManageOptions()
+ }
+ }
+ }
+
+ /**
+ * Toggles the visibility of the list of credit cards in the prompt.
+ *
+ * @param shouldExpand True if the list of credit cards should be displayed, false otherwise.
+ */
+ private fun toggleSelectCreditCardHeader(shouldExpand: Boolean) {
+ recyclerView?.isVisible = shouldExpand
+ manageCreditCardsButtonView?.isVisible = shouldExpand
+
+ if (shouldExpand) {
+ view?.hideKeyboard()
+ expanderView?.rotation = ROTATE_180
+ headerView?.contentDescription =
+ context.getString(R.string.mozac_feature_prompts_collapse_credit_cards_content_description_2)
+ emitCreditCardAutofillExpandedFact()
+ } else {
+ expanderView?.rotation = 0F
+ headerView?.contentDescription =
+ context.getString(R.string.mozac_feature_prompts_expand_credit_cards_content_description_2)
+ }
+ }
+
+ companion object {
+ val LAYOUT_ID = R.layout.mozac_feature_prompts_credit_card_select_prompt
+
+ private const val ROTATE_180 = 180F
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapter.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapter.kt
new file mode 100644
index 0000000000..cfa7f4d265
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapter.kt
@@ -0,0 +1,39 @@
+/* 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.prompts.creditcard
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import mozilla.components.concept.storage.CreditCardEntry
+
+/**
+ * Adapter for a list of credit cards to be displayed.
+ *
+ * @param onCreditCardSelected Callback invoked when a credit card item is selected.
+ */
+class CreditCardsAdapter(
+ private val onCreditCardSelected: (CreditCardEntry) -> Unit,
+) : ListAdapter<CreditCardEntry, CreditCardItemViewHolder>(DiffCallback) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CreditCardItemViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(CreditCardItemViewHolder.LAYOUT_ID, parent, false)
+ return CreditCardItemViewHolder(view, onCreditCardSelected)
+ }
+
+ override fun onBindViewHolder(holder: CreditCardItemViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ internal object DiffCallback : DiffUtil.ItemCallback<CreditCardEntry>() {
+ override fun areItemsTheSame(oldItem: CreditCardEntry, newItem: CreditCardEntry) =
+ oldItem.guid == newItem.guid
+
+ override fun areContentsTheSame(oldItem: CreditCardEntry, newItem: CreditCardEntry) =
+ oldItem == newItem
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AbstractPromptTextDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AbstractPromptTextDialogFragment.kt
new file mode 100644
index 0000000000..b1ee54fa98
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AbstractPromptTextDialogFragment.kt
@@ -0,0 +1,72 @@
+/* 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.prompts.dialog
+
+import android.annotation.SuppressLint
+import android.text.method.ScrollingMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.annotation.IdRes
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import mozilla.components.feature.prompts.R
+
+internal const val KEY_MANY_ALERTS = "KEY_MANY_ALERTS"
+internal const val KEY_USER_CHECK_BOX = "KEY_USER_CHECK_BOX"
+
+/**
+ * An abstract alert for showing a text message plus a checkbox for handling [hasShownManyDialogs].
+ */
+internal abstract class AbstractPromptTextDialogFragment : PromptDialogFragment() {
+
+ /**
+ * Tells if a checkbox should be shown for preventing this [sessionId] from showing more dialogs.
+ */
+ internal val hasShownManyDialogs: Boolean by lazy { safeArguments.getBoolean(KEY_MANY_ALERTS) }
+
+ /**
+ * Stores the user's decision from the checkbox
+ * for preventing this [sessionId] from showing more dialogs.
+ */
+ internal var userSelectionNoMoreDialogs: Boolean
+ get() = safeArguments.getBoolean(KEY_USER_CHECK_BOX)
+ set(value) {
+ safeArguments.putBoolean(KEY_USER_CHECK_BOX, value)
+ }
+
+ /**
+ * Creates custom view that adds a [TextView] + [CheckBox] and attach the corresponding
+ * events for handling [hasShownManyDialogs].
+ */
+ @SuppressLint("InflateParams")
+ internal fun setCustomMessageView(builder: AlertDialog.Builder): AlertDialog.Builder {
+ val inflater = LayoutInflater.from(requireContext())
+ val view = inflater.inflate(R.layout.mozac_feature_prompt_with_check_box, null)
+ val textView = view.findViewById<TextView>(R.id.message)
+ textView.text = message
+ textView.movementMethod = ScrollingMovementMethod()
+
+ addCheckBoxIfNeeded(view)
+
+ builder.setView(view)
+
+ return builder
+ }
+
+ internal fun addCheckBoxIfNeeded(
+ view: View,
+ @IdRes id: Int = R.id.mozac_feature_prompts_no_more_dialogs_check_box,
+ ) {
+ if ((hasShownManyDialogs)) {
+ val checkBox = view.findViewById<CheckBox>(id)
+ checkBox.isVisible = true
+ checkBox.setOnCheckedChangeListener { _, isChecked ->
+ userSelectionNoMoreDialogs = isChecked
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AlertDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AlertDialogFragment.kt
new file mode 100644
index 0000000000..d14bb23047
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AlertDialogFragment.kt
@@ -0,0 +1,80 @@
+/* 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.prompts.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.appcompat.app.AlertDialog
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+/**
+ * [android.support.v4.app.DialogFragment] implementation to display web Alerts with native dialogs.
+ */
+internal class AlertDialogFragment : AbstractPromptTextDialogFragment() {
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(requireContext())
+ .setTitle(title)
+ .setCancelable(true)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ onPositiveClickAction()
+ }
+ return setCustomMessageView(builder)
+ .create()
+ .withCenterAlignedButtons()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+
+ private fun onPositiveClickAction() {
+ if (!userSelectionNoMoreDialogs) {
+ feature?.onCancel(sessionId, promptRequestUID)
+ } else {
+ feature?.onConfirm(sessionId, promptRequestUID, userSelectionNoMoreDialogs)
+ }
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [AlertDialogFragment]
+ * @param sessionId to create the dialog.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog is shown.
+ * @param shouldDismissOnLoad whether or not the dialog should automatically be dismissed
+ * when a new page is loaded.
+ * @param title the title of the dialog.
+ * @param message the message of the dialog.
+ * @param hasShownManyDialogs tells if this [sessionId] has shown many dialogs
+ * in a short period of time, if is true a checkbox will be part of the dialog, for the user
+ * to choose if wants to prevent this [sessionId] continuing showing dialogs.
+ */
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ title: String,
+ message: String,
+ hasShownManyDialogs: Boolean,
+ ): AlertDialogFragment {
+ val fragment = AlertDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putString(KEY_TITLE, title)
+ putString(KEY_MESSAGE, message)
+ putBoolean(KEY_MANY_ALERTS, hasShownManyDialogs)
+ }
+
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragment.kt
new file mode 100644
index 0000000000..af9ccd3a9b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragment.kt
@@ -0,0 +1,189 @@
+/* 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.prompts.dialog
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.appcompat.app.AlertDialog
+import mozilla.components.feature.prompts.R
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+private const val KEY_USERNAME_EDIT_TEXT = "KEY_USERNAME_EDIT_TEXT"
+private const val KEY_PASSWORD_EDIT_TEXT = "KEY_PASSWORD_EDIT_TEXT"
+private const val KEY_ONLY_SHOW_PASSWORD = "KEY_ONLY_SHOW_PASSWORD"
+private const val KEY_URL = "KEY_SESSION_URL"
+
+/**
+ * [android.support.v4.app.DialogFragment] implementation to display a
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">authentication</a>
+ * dialog with native dialogs.
+ */
+internal class AuthenticationDialogFragment : PromptDialogFragment() {
+
+ internal val onlyShowPassword: Boolean by lazy { safeArguments.getBoolean(KEY_ONLY_SHOW_PASSWORD) }
+
+ private var url: String?
+ get() = safeArguments.getString(KEY_URL, null)
+ set(value) {
+ safeArguments.putString(KEY_URL, value)
+ }
+
+ internal var username: String
+ get() = safeArguments.getString(KEY_USERNAME_EDIT_TEXT, "")
+ set(value) {
+ safeArguments.putString(KEY_USERNAME_EDIT_TEXT, value)
+ }
+
+ internal var password: String
+ get() = safeArguments.getString(KEY_PASSWORD_EDIT_TEXT, "")
+ set(value) {
+ safeArguments.putString(KEY_PASSWORD_EDIT_TEXT, value)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(requireContext())
+ .setupTitle()
+ .setMessage(message)
+ .setCancelable(true)
+ .setNegativeButton(R.string.mozac_feature_prompts_cancel) { _, _ ->
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ onPositiveClickAction()
+ }
+ return addLayout(builder).create().withCenterAlignedButtons()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+
+ private fun onPositiveClickAction() {
+ feature?.onConfirm(sessionId, promptRequestUID, username to password)
+ }
+
+ @SuppressLint("InflateParams")
+ private fun addLayout(builder: AlertDialog.Builder): AlertDialog.Builder {
+ val inflater = LayoutInflater.from(requireContext())
+ val view = inflater.inflate(R.layout.mozac_feature_prompt_auth_prompt, null)
+
+ bindUsername(view)
+ bindPassword(view)
+
+ return builder.setView(view)
+ }
+
+ private fun bindUsername(view: View) {
+ // Username field uses the AutofillEditText so if the user focus is here, the autofill
+ // application can get the web domain info without searching through the view tree.
+ val usernameEditText = view.findViewById<AutofillEditText>(R.id.username)
+ usernameEditText.url = url
+
+ if (onlyShowPassword) {
+ usernameEditText.visibility = GONE
+ } else {
+ usernameEditText.setText(username)
+ usernameEditText.addTextChangedListener(
+ object : TextWatcher {
+ override fun afterTextChanged(editable: Editable) {
+ username = editable.toString()
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
+ },
+ )
+ }
+ }
+
+ private fun bindPassword(view: View) {
+ // Password field uses the AutofillEditText so if the user focus is here, the autofill
+ // application can get the web domain info without searching through the view tree.
+ val passwordEditText = view.findViewById<AutofillEditText>(R.id.password)
+ passwordEditText.url = url
+
+ passwordEditText.setText(password)
+ passwordEditText.addTextChangedListener(
+ object : TextWatcher {
+ override fun afterTextChanged(editable: Editable) {
+ password = editable.toString()
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
+ },
+ )
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [AuthenticationDialogFragment]
+ * @param sessionId the id of the session for which this dialog will be created.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog is shown.
+ * @param shouldDismissOnLoad whether or not the dialog should automatically be dismissed
+ * when a new page is loaded.
+ * @param title the title of the dialog.
+ * @param message the text that will go below title.
+ * @param username the default value of the username text field.
+ * @param password the default value of the password text field.
+ * @param onlyShowPassword indicates if the dialog should include an username text field.
+ */
+ @Suppress("LongParameterList")
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ title: String,
+ message: String,
+ username: String,
+ password: String,
+ onlyShowPassword: Boolean,
+ url: String?,
+ ): AuthenticationDialogFragment {
+ val fragment = AuthenticationDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putString(KEY_TITLE, title)
+ putString(KEY_MESSAGE, message)
+ putBoolean(KEY_ONLY_SHOW_PASSWORD, onlyShowPassword)
+ putString(KEY_USERNAME_EDIT_TEXT, username)
+ putString(KEY_PASSWORD_EDIT_TEXT, password)
+ putString(KEY_URL, url)
+ }
+
+ fragment.arguments = arguments
+ return fragment
+ }
+
+ @StringRes
+ internal val DEFAULT_TITLE = R.string.mozac_feature_prompt_sign_in
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun AlertDialog.Builder.setupTitle(): AlertDialog.Builder {
+ return if (title.isEmpty()) {
+ setTitle(DEFAULT_TITLE)
+ } else {
+ setTitle(title)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AutofillEditText.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AutofillEditText.kt
new file mode 100644
index 0000000000..c094256f15
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/AutofillEditText.kt
@@ -0,0 +1,31 @@
+/* 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.prompts.dialog
+
+import android.content.Context
+import android.os.Build
+import android.util.AttributeSet
+import android.view.ViewStructure
+import androidx.appcompat.widget.AppCompatEditText
+
+/**
+ * [androidx.appcompat.widget.AppCompatEditText] implementation to add WebDomain information which
+ * allows autofill applications to detect which URL is requesting the authentication info.
+ */
+internal class AutofillEditText : AppCompatEditText {
+ internal var url: String? = null
+
+ constructor (context: Context) : super(context)
+
+ constructor (context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor (context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ override fun onProvideAutofillStructure(structure: ViewStructure?, flags: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && url != null) {
+ structure?.setWebDomain(url)
+ }
+ super.onProvideAutofillStructure(structure, flags)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/BasicColorAdapter.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/BasicColorAdapter.kt
new file mode 100644
index 0000000000..736ffde257
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/BasicColorAdapter.kt
@@ -0,0 +1,131 @@
+/* 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.prompts.dialog
+
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat
+import androidx.core.graphics.BlendModeCompat
+import androidx.core.graphics.BlendModeCompat.SRC_IN
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.utils.ColorUtils
+
+/**
+ * Represents an item in the [BasicColorAdapter] list.
+ *
+ * @property color color int that this item corresponds to.
+ * @property contentDescription accessibility description of this color.
+ * @property selected if true, this is the color that will be set when the dialog is closed.
+ */
+data class ColorItem(
+ @ColorInt val color: Int,
+ val contentDescription: String,
+ val selected: Boolean = false,
+)
+
+private object ColorItemDiffCallback : DiffUtil.ItemCallback<ColorItem>() {
+ override fun areItemsTheSame(oldItem: ColorItem, newItem: ColorItem) =
+ oldItem.color == newItem.color
+
+ override fun areContentsTheSame(oldItem: ColorItem, newItem: ColorItem) =
+ oldItem == newItem
+}
+
+/**
+ * RecyclerView adapter for displaying color items.
+ */
+internal class BasicColorAdapter(
+ private val onColorSelected: (Int) -> Unit,
+) : ListAdapter<ColorItem, ColorViewHolder>(ColorItemDiffCallback) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ColorViewHolder {
+ val view = LayoutInflater
+ .from(parent.context)
+ .inflate(R.layout.mozac_feature_prompts_color_item, parent, false)
+ return ColorViewHolder(view, onColorSelected)
+ }
+
+ override fun onBindViewHolder(holder: ColorViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+/**
+ * View holder for a color item.
+ */
+internal class ColorViewHolder(
+ itemView: View,
+ private val onColorSelected: (Int) -> Unit,
+) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
+ @VisibleForTesting
+ @ColorInt
+ internal var color: Int = Color.BLACK
+
+ private val checkDrawable: Drawable? by lazy {
+ // Get the height of the row
+ val typedValue = TypedValue()
+ itemView.context.theme.resolveAttribute(
+ android.R.attr.listPreferredItemHeight,
+ typedValue,
+ true,
+ )
+ var height = typedValue.getDimension(itemView.context.resources.displayMetrics).toInt()
+
+ // Remove padding for the shadow
+ val backgroundPadding = Rect()
+ ContextCompat.getDrawable(itemView.context, R.drawable.color_picker_row_bg)?.getPadding(backgroundPadding)
+ height -= backgroundPadding.top + backgroundPadding.bottom
+
+ ContextCompat.getDrawable(itemView.context, R.drawable.color_picker_checkmark)?.apply {
+ setBounds(0, 0, height, height)
+ }
+ }
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ fun bind(colorItem: ColorItem) {
+ // Save the color for the onClick callback
+ color = colorItem.color
+
+ // Set the background to look like this item's color
+ itemView.background = itemView.background.apply {
+ colorFilter = createBlendModeColorFilterCompat(
+ colorItem.color,
+ BlendModeCompat.MODULATE,
+ )
+ }
+ itemView.contentDescription = colorItem.contentDescription
+
+ // Display the check mark
+ val check = if (colorItem.selected) {
+ checkDrawable?.apply {
+ val readableColor = ColorUtils.getReadableTextColor(color)
+ colorFilter = createBlendModeColorFilterCompat(readableColor, SRC_IN)
+ }
+ } else {
+ null
+ }
+ itemView.isActivated = colorItem.selected
+ (itemView as TextView).setCompoundDrawablesRelative(check, null, null, null)
+ }
+
+ override fun onClick(v: View?) {
+ onColorSelected(color)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ChoiceAdapter.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ChoiceAdapter.kt
new file mode 100644
index 0000000000..9f65ac6ab3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ChoiceAdapter.kt
@@ -0,0 +1,217 @@
+/* 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.prompts.dialog
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckedTextView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.engine.prompt.Choice
+import mozilla.components.feature.prompts.R
+
+/**
+ * RecyclerView adapter for displaying choice items.
+ */
+internal class ChoiceAdapter(
+ private val fragment: ChoiceDialogFragment,
+ private val inflater: LayoutInflater,
+) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+
+ companion object {
+ internal const val TYPE_MULTIPLE = 1
+ internal const val TYPE_SINGLE = 2
+ internal const val TYPE_GROUP = 3
+ internal const val TYPE_MENU = 4
+ internal const val TYPE_MENU_SEPARATOR = 5
+ }
+
+ private val choices = mutableListOf<Choice>()
+
+ init {
+ addItems(fragment.choices)
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ val item = choices[position]
+ return when {
+ fragment.isSingleChoice and item.isGroupType -> TYPE_GROUP
+ fragment.isSingleChoice -> TYPE_SINGLE
+ fragment.isMenuChoice -> if (item.isASeparator) TYPE_MENU_SEPARATOR else TYPE_MENU
+ item.isGroupType -> TYPE_GROUP
+ else -> TYPE_MULTIPLE
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, type: Int): RecyclerView.ViewHolder {
+ val layoutId = getLayoutId(type)
+ val view = inflater.inflate(layoutId, parent, false)
+
+ return when (type) {
+ TYPE_GROUP -> GroupViewHolder(view)
+
+ TYPE_MENU -> MenuViewHolder(view)
+
+ TYPE_MENU_SEPARATOR -> MenuSeparatorViewHolder(view)
+
+ TYPE_SINGLE -> SingleViewHolder(view)
+
+ TYPE_MULTIPLE -> MultipleViewHolder(view)
+
+ else -> throw IllegalArgumentException(" $type is not a valid layout type")
+ }
+ }
+
+ override fun getItemCount(): Int = choices.size
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val choice = choices[position]
+ when (holder) {
+ is MenuSeparatorViewHolder -> return
+
+ is GroupViewHolder -> {
+ holder.bind(choice)
+ }
+
+ is SingleViewHolder -> {
+ holder.bind(choice, fragment)
+ }
+
+ is MultipleViewHolder -> {
+ holder.bind(choice, fragment)
+ }
+
+ is MenuViewHolder -> {
+ holder.bind(choice, fragment)
+ }
+ }
+ }
+
+ private fun getLayoutId(itemType: Int): Int {
+ return when (itemType) {
+ TYPE_GROUP -> R.layout.mozac_feature_choice_group_item
+ TYPE_MULTIPLE -> R.layout.mozac_feature_multiple_choice_item
+ TYPE_SINGLE -> R.layout.mozac_feature_single_choice_item
+ TYPE_MENU -> R.layout.mozac_feature_menu_choice_item
+ TYPE_MENU_SEPARATOR -> R.layout.mozac_feature_menu_separator_choice_item
+ else -> throw IllegalArgumentException(" $itemType is not a valid layout dialog type")
+ }
+ }
+
+ /**
+ * View holder for a single choice item.
+ */
+ internal class SingleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal val labelView = itemView.findViewById<CheckedTextView>(R.id.labelView)
+
+ fun bind(choice: Choice, fragment: ChoiceDialogFragment) {
+ labelView.choice = choice
+ labelView.isChecked = choice.selected
+
+ if (choice.enable) {
+ itemView.setOnClickListener {
+ val actualChoice = labelView.choice
+ fragment.onSelect(actualChoice)
+ labelView.toggle()
+ }
+ } else {
+ itemView.isClickable = false
+ }
+ }
+ }
+
+ /**
+ * View holder for a Multiple choice item.
+ */
+ internal class MultipleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal val labelView = itemView.findViewById<CheckedTextView>(R.id.labelView)
+
+ fun bind(choice: Choice, fragment: ChoiceDialogFragment) {
+ labelView.choice = choice
+ labelView.isChecked = choice in fragment.mapSelectChoice
+
+ if (choice.enable) {
+ itemView.setOnClickListener {
+ val actualChoice = labelView.choice
+ with(fragment.mapSelectChoice) {
+ if (actualChoice in this) {
+ this -= actualChoice
+ } else {
+ this[actualChoice] = actualChoice
+ }
+ }
+ labelView.toggle()
+ }
+ } else {
+ itemView.isClickable = false
+ }
+ }
+ }
+
+ /**
+ * View holder for a Menu choice item.
+ */
+ internal class MenuViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal val labelView = itemView.findViewById<TextView>(R.id.labelView)
+
+ fun bind(choice: Choice, fragment: ChoiceDialogFragment) {
+ labelView.choice = choice
+
+ if (choice.enable) {
+ itemView.setOnClickListener {
+ val actualChoice = labelView.choice
+ fragment.onSelect(actualChoice)
+ }
+ } else {
+ itemView.isClickable = false
+ }
+ }
+ }
+
+ /**
+ * View holder for a menu separator choice item.
+ */
+ internal class MenuSeparatorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
+
+ /**
+ * View holder for a group choice item.
+ */
+ internal class GroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal val labelView = itemView.findViewById<TextView>(R.id.labelView)
+
+ fun bind(choice: Choice) {
+ labelView.choice = choice
+ labelView.isEnabled = false
+ }
+ }
+
+ private fun addItems(items: Array<Choice>, indent: String? = null) {
+ for (choice in items) {
+ if (indent != null && !choice.isGroupType) {
+ choice.label = indent + choice.label
+ }
+
+ choices.add(choice)
+
+ if (choice.isGroupType) {
+ val newIndent = if (indent != null) indent + '\t' else "\t"
+ addItems(requireNotNull(choice.children), newIndent)
+ }
+
+ if (choice.selected) {
+ fragment.mapSelectChoice[choice] = choice
+ }
+ }
+ }
+}
+
+internal var TextView.choice: Choice
+ get() = tag as Choice
+ set(value) {
+ this.text = value.label
+ this.isEnabled = value.enable
+ tag = value
+ }
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragment.kt
new file mode 100644
index 0000000000..a15e09bf09
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragment.kt
@@ -0,0 +1,145 @@
+/* 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.prompts.dialog
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.appcompat.app.AlertDialog
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.engine.prompt.Choice
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.utils.ext.getParcelableArrayCompat
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+private const val KEY_CHOICES = "KEY_CHOICES"
+private const val KEY_DIALOG_TYPE = "KEY_DIALOG_TYPE"
+
+/**
+ * [android.support.v4.app.DialogFragment] implementation to display choice(options,optgroup and menu)
+ * web content in native dialogs.
+ */
+internal class ChoiceDialogFragment : PromptDialogFragment() {
+
+ internal val choices: Array<Choice> by lazy {
+ safeArguments.getParcelableArrayCompat(KEY_CHOICES, Choice::class.java) ?: emptyArray()
+ }
+
+ @VisibleForTesting
+ internal val dialogType: Int by lazy { safeArguments.getInt(KEY_DIALOG_TYPE) }
+
+ internal val isSingleChoice get() = dialogType == SINGLE_CHOICE_DIALOG_TYPE
+
+ internal val isMenuChoice get() = dialogType == MENU_CHOICE_DIALOG_TYPE
+
+ internal val mapSelectChoice by lazy { HashMap<Choice, Choice>() }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return when (dialogType) {
+ SINGLE_CHOICE_DIALOG_TYPE -> createSingleChoiceDialog()
+ MULTIPLE_CHOICE_DIALOG_TYPE -> createMultipleChoiceDialog()
+ MENU_CHOICE_DIALOG_TYPE -> createSingleChoiceDialog()
+ else -> throw IllegalArgumentException(" $dialogType is not a valid choice dialog type")
+ }
+ }
+
+ companion object {
+ fun newInstance(
+ choices: Array<Choice>,
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ dialogType: Int,
+ ): ChoiceDialogFragment {
+ val fragment = ChoiceDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putParcelableArray(KEY_CHOICES, choices)
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putInt(KEY_DIALOG_TYPE, dialogType)
+ }
+
+ fragment.arguments = arguments
+
+ return fragment
+ }
+
+ const val SINGLE_CHOICE_DIALOG_TYPE = 0
+ const val MULTIPLE_CHOICE_DIALOG_TYPE = 1
+ const val MENU_CHOICE_DIALOG_TYPE = 2
+ }
+
+ @SuppressLint("InflateParams")
+ internal fun createDialogContentView(inflater: LayoutInflater): View {
+ val index = choices.indexOfFirst { it.selected }
+ val view = inflater.inflate(R.layout.mozac_feature_choice_dialogs, null)
+ view.findViewById<RecyclerView>(R.id.recyclerView).apply {
+ layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false).also {
+ it.scrollToPosition(index)
+ }
+ adapter = ChoiceAdapter(this@ChoiceDialogFragment, inflater)
+ }
+ return view
+ }
+
+ fun onSelect(selectedChoice: Choice) {
+ feature?.onConfirm(sessionId, promptRequestUID, selectedChoice)
+ dismiss()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+
+ private fun createSingleChoiceDialog(): AlertDialog {
+ val builder = AlertDialog.Builder(requireContext())
+ val inflater = LayoutInflater.from(requireContext())
+ val view = createDialogContentView(inflater)
+
+ return builder.setView(view)
+ .setOnDismissListener {
+ feature?.onCancel(sessionId, promptRequestUID)
+ }.create()
+ }
+
+ private fun createMultipleChoiceDialog(): AlertDialog {
+ val builder = AlertDialog.Builder(requireContext())
+ val inflater = LayoutInflater.from(requireContext())
+ val view = createDialogContentView(inflater)
+
+ return builder.setView(view)
+ .setNegativeButton(R.string.mozac_feature_prompts_cancel) { _, _ ->
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+ .setPositiveButton(R.string.mozac_feature_prompts_ok) { _, _ ->
+ feature?.onConfirm(sessionId, promptRequestUID, mapSelectChoice.keys.toTypedArray())
+ }.setOnDismissListener {
+ feature?.onCancel(sessionId, promptRequestUID)
+ }.create().withCenterAlignedButtons()
+ }
+}
+
+@Suppress("UNCHECKED_CAST")
+@VisibleForTesting(otherwise = PRIVATE)
+internal fun Array<Parcelable>.toArrayOfChoices(): Array<Choice> {
+ return if (this.isArrayOf<Choice>()) {
+ this as Array<Choice>
+ } else {
+ Array(this.size) { index ->
+ this[index] as Choice
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragment.kt
new file mode 100644
index 0000000000..83b849991e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragment.kt
@@ -0,0 +1,164 @@
+/* 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.prompts.dialog
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.graphics.Color
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.prompts.R
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+private const val KEY_SELECTED_COLOR = "KEY_SELECTED_COLOR"
+
+private const val RGB_BIT_MASK = 0xffffff
+
+/**
+ * [androidx.fragment.app.DialogFragment] implementation for a color picker dialog.
+ */
+internal class ColorPickerDialogFragment : PromptDialogFragment(), DialogInterface.OnClickListener {
+
+ @ColorInt
+ private var initiallySelectedCustomColor: Int? = null
+ private lateinit var defaultColors: List<ColorItem>
+ private lateinit var listAdapter: BasicColorAdapter
+
+ @VisibleForTesting
+ internal var selectedColor: Int
+ get() = safeArguments.getInt(KEY_SELECTED_COLOR)
+ set(value) {
+ safeArguments.putInt(KEY_SELECTED_COLOR, value)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
+ AlertDialog.Builder(requireContext())
+ .setCancelable(true)
+ .setTitle(R.string.mozac_feature_prompts_choose_a_color)
+ .setNegativeButton(R.string.mozac_feature_prompts_cancel, this)
+ .setPositiveButton(R.string.mozac_feature_prompts_set_date, this)
+ .setView(createDialogContentView())
+ .create()
+ .withCenterAlignedButtons()
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ onClick(dialog, DialogInterface.BUTTON_NEGATIVE)
+ }
+
+ override fun onClick(dialog: DialogInterface?, which: Int) {
+ when (which) {
+ DialogInterface.BUTTON_POSITIVE ->
+ feature?.onConfirm(sessionId, promptRequestUID, selectedColor.toHexColor())
+ DialogInterface.BUTTON_NEGATIVE -> feature?.onCancel(sessionId, promptRequestUID)
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ internal fun createDialogContentView(): View {
+ val view = LayoutInflater
+ .from(requireContext())
+ .inflate(R.layout.mozac_feature_prompts_color_picker_dialogs, null)
+
+ // Save the color selected when this dialog opened to show at the end
+ initiallySelectedCustomColor = selectedColor
+
+ // Load list of colors from resources
+ val typedArray = resources.obtainTypedArray(R.array.mozac_feature_prompts_default_colors)
+
+ defaultColors = List(typedArray.length()) { i ->
+ val color = typedArray.getColor(i, Color.BLACK)
+ if (color == initiallySelectedCustomColor) {
+ // No need to save the initial color, its already in the list
+ initiallySelectedCustomColor = null
+ }
+
+ color.toColorItem()
+ }
+ typedArray.recycle()
+
+ setupRecyclerView(view)
+ onColorChange(selectedColor)
+ return view
+ }
+
+ private fun setupRecyclerView(view: View) {
+ listAdapter = BasicColorAdapter(this::onColorChange)
+ view.findViewById<RecyclerView>(R.id.recyclerView).apply {
+ layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false).apply {
+ stackFromEnd = true
+ }
+ adapter = listAdapter
+ setHasFixedSize(true)
+ itemAnimator = null
+ }
+ }
+
+ /**
+ * Called when a new color is selected by the user.
+ */
+ @VisibleForTesting
+ internal fun onColorChange(newColor: Int) {
+ selectedColor = newColor
+
+ val colorItems = defaultColors.toMutableList()
+ val index = colorItems.indexOfFirst { it.color == newColor }
+ val lastColor = if (index > -1) {
+ colorItems[index] = colorItems[index].copy(selected = true)
+ initiallySelectedCustomColor
+ } else {
+ newColor
+ }
+ if (lastColor != null) {
+ colorItems.add(lastColor.toColorItem(selected = lastColor == newColor))
+ }
+
+ listAdapter.submitList(colorItems)
+ }
+
+ companion object {
+
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ defaultColor: String,
+ ) = ColorPickerDialogFragment().apply {
+ arguments = (arguments ?: Bundle()).apply {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putInt(KEY_SELECTED_COLOR, defaultColor.toColor())
+ }
+ }
+ }
+}
+
+internal fun Int.toColorItem(selected: Boolean = false): ColorItem {
+ return ColorItem(
+ color = this,
+ contentDescription = toHexColor(),
+ selected = selected,
+ )
+}
+
+internal fun String.toColor(): Int {
+ return try {
+ Color.parseColor(this)
+ } catch (e: IllegalArgumentException) {
+ Color.BLACK
+ }
+}
+
+internal fun Int.toHexColor(): String {
+ return String.format("#%06x", RGB_BIT_MASK and this)
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragment.kt
new file mode 100644
index 0000000000..9d04f7d26b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragment.kt
@@ -0,0 +1,84 @@
+/* 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.prompts.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+internal const val KEY_POSITIVE_BUTTON = "KEY_POSITIVE_BUTTON"
+internal const val KEY_NEGATIVE_BUTTON = "KEY_NEGATIVE_BUTTON"
+
+/**
+ * [android.support.v4.app.DialogFragment] implementation for a confirm dialog.
+ * The user has two possible options, allow the request or
+ * deny it (Positive and Negative buttons]. When the positive button is pressed the
+ * feature.onConfirm function will be called otherwise the feature.onCancel function will be called.
+ */
+internal class ConfirmDialogFragment : AbstractPromptTextDialogFragment() {
+
+ @VisibleForTesting
+ internal val positiveButtonText: String by lazy { safeArguments.getString(KEY_POSITIVE_BUTTON)!! }
+
+ @VisibleForTesting
+ internal val negativeButtonText: String by lazy { safeArguments.getString(KEY_NEGATIVE_BUTTON)!! }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(requireContext())
+ .setCancelable(false)
+ .setTitle(title)
+ .setNegativeButton(negativeButtonText) { _, _ ->
+ feature?.onCancel(sessionId, promptRequestUID, userSelectionNoMoreDialogs)
+ }
+ .setPositiveButton(positiveButtonText) { _, _ ->
+ onPositiveClickAction()
+ }
+ return setCustomMessageView(builder)
+ .create()
+ .withCenterAlignedButtons()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID, userSelectionNoMoreDialogs)
+ }
+
+ private fun onPositiveClickAction() {
+ feature?.onConfirm(sessionId, promptRequestUID, userSelectionNoMoreDialogs)
+ }
+
+ companion object {
+ fun newInstance(
+ sessionId: String? = null,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ title: String,
+ message: String,
+ positiveButtonText: String,
+ negativeButtonText: String,
+ hasShownManyDialogs: Boolean = false,
+ ): ConfirmDialogFragment {
+ val fragment = ConfirmDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putString(KEY_TITLE, title)
+ putString(KEY_MESSAGE, message)
+ putString(KEY_POSITIVE_BUTTON, positiveButtonText)
+ putString(KEY_NEGATIVE_BUTTON, negativeButtonText)
+ putBoolean(KEY_MANY_ALERTS, hasShownManyDialogs)
+ }
+
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/FullScreenNotificationDialog.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/FullScreenNotificationDialog.kt
new file mode 100644
index 0000000000..3c403f3734
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/FullScreenNotificationDialog.kt
@@ -0,0 +1,70 @@
+/* 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.prompts.dialog
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.Gravity
+import android.view.WindowManager
+import androidx.annotation.LayoutRes
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+private const val TAG = "mozac_feature_prompts_full_screen_notification_dialog"
+private const val SNACKBAR_DURATION_LONG_MS = 3000L
+
+/**
+ * UI to show a 'full screen mode' notification.
+ */
+interface FullScreenNotification {
+ /**
+ * Show the notification.
+ *
+ * @param fragmentManager the [FragmentManager] to add this notification to.
+ */
+ fun show(fragmentManager: FragmentManager)
+}
+
+/**
+ * [DialogFragment] that is configured to match the style and behaviour of a Snackbar.
+ *
+ * @property layout the layout to use for the dialog.
+ */
+class FullScreenNotificationDialog(@LayoutRes val layout: Int) :
+ DialogFragment(), FullScreenNotification {
+ override fun show(fragmentManager: FragmentManager) = super.show(fragmentManager, TAG)
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = requireActivity().let {
+ val view = layoutInflater.inflate(layout, null)
+ AlertDialog.Builder(it).setView(view).create()
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ dialog?.let { dialog ->
+ dialog.window?.let { window ->
+ // Prevent any user input from key or other button events to it.
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ )
+
+ window.setGravity(Gravity.BOTTOM)
+ window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ }
+ }
+
+ // Attempt to automatically dismiss the dialog after the given duration.
+ lifecycleScope.launch {
+ delay(SNACKBAR_DURATION_LONG_MS)
+ dialog?.dismiss()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/LoginDialogFacts.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/LoginDialogFacts.kt
new file mode 100644
index 0000000000..6d9a985aa8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/LoginDialogFacts.kt
@@ -0,0 +1,45 @@
+/* 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.prompts.dialog
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [LoginDialogFragment]
+ */
+class LoginDialogFacts {
+ /**
+ * Items that specify how the [LoginDialogFragment] was interacted with
+ */
+ object Items {
+ const val DISPLAY = "display"
+ const val SAVE = "save"
+ const val NEVER_SAVE = "never_save"
+ const val CANCEL = "cancel"
+ }
+}
+
+private fun emitLoginDialogFacts(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_PROMPTS,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitDisplayFact() = emitLoginDialogFacts(Action.CLICK, LoginDialogFacts.Items.DISPLAY)
+internal fun emitNeverSaveFact() = emitLoginDialogFacts(Action.CLICK, LoginDialogFacts.Items.NEVER_SAVE)
+internal fun emitSaveFact() = emitLoginDialogFacts(Action.CLICK, LoginDialogFacts.Items.SAVE)
+internal fun emitCancelFact() = emitLoginDialogFacts(Action.CLICK, LoginDialogFacts.Items.CANCEL)
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragment.kt
new file mode 100644
index 0000000000..0ace6d2366
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragment.kt
@@ -0,0 +1,99 @@
+/* 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.prompts.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.appcompat.app.AlertDialog
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+private const val KEY_POSITIVE_BUTTON_TITLE = "KEY_POSITIVE_BUTTON_TITLE"
+private const val KEY_NEGATIVE_BUTTON_TITLE = "KEY_NEGATIVE_BUTTON_TITLE"
+private const val KEY_NEUTRAL_BUTTON_TITLE = "KEY_NEUTRAL_BUTTON_TITLE"
+
+/**
+ * [android.support.v4.app.DialogFragment] implementation to display a confirm dialog,
+ * it can have up to three buttons, they could be positive, negative or neutral.
+ */
+internal class MultiButtonDialogFragment : AbstractPromptTextDialogFragment() {
+
+ internal val positiveButtonTitle: String? by lazy { safeArguments.getString(KEY_POSITIVE_BUTTON_TITLE) }
+
+ internal val negativeButtonTitle: String? by lazy { safeArguments.getString(KEY_NEGATIVE_BUTTON_TITLE) }
+
+ internal val neutralButtonTitle: String? by lazy { safeArguments.getString(KEY_NEUTRAL_BUTTON_TITLE) }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(requireContext())
+ .setTitle(title)
+ .setCancelable(true)
+ .setupButtons()
+ return setCustomMessageView(builder)
+ .create()
+ .withCenterAlignedButtons()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+
+ private fun AlertDialog.Builder.setupButtons(): AlertDialog.Builder {
+ if (!positiveButtonTitle.isNullOrBlank()) {
+ setPositiveButton(positiveButtonTitle) { _, _ ->
+ feature?.onConfirm(sessionId, promptRequestUID, userSelectionNoMoreDialogs to ButtonType.POSITIVE)
+ }
+ }
+ if (!negativeButtonTitle.isNullOrBlank()) {
+ setNegativeButton(negativeButtonTitle) { _, _ ->
+ feature?.onConfirm(sessionId, promptRequestUID, userSelectionNoMoreDialogs to ButtonType.NEGATIVE)
+ }
+ }
+ if (!neutralButtonTitle.isNullOrBlank()) {
+ setNeutralButton(neutralButtonTitle) { _, _ ->
+ feature?.onConfirm(sessionId, promptRequestUID, userSelectionNoMoreDialogs to ButtonType.NEUTRAL)
+ }
+ }
+ return this
+ }
+
+ companion object {
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ title: String,
+ message: String,
+ hasShownManyDialogs: Boolean,
+ shouldDismissOnLoad: Boolean,
+ positiveButton: String = "",
+ negativeButton: String = "",
+ neutralButton: String = "",
+ ): MultiButtonDialogFragment {
+ val fragment = MultiButtonDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putString(KEY_TITLE, title)
+ putString(KEY_MESSAGE, message)
+ putBoolean(KEY_MANY_ALERTS, hasShownManyDialogs)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putString(KEY_POSITIVE_BUTTON_TITLE, positiveButton)
+ putString(KEY_NEGATIVE_BUTTON_TITLE, negativeButton)
+ putString(KEY_NEUTRAL_BUTTON_TITLE, neutralButton)
+ }
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+
+ enum class ButtonType {
+ POSITIVE,
+ NEGATIVE,
+ NEUTRAL,
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptAbuserDetector.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptAbuserDetector.kt
new file mode 100644
index 0000000000..ac92f5e726
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptAbuserDetector.kt
@@ -0,0 +1,64 @@
+/* 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.prompts.dialog
+
+import java.util.Date
+
+/**
+ * Helper class to identify if a website has shown many dialogs.
+ */
+internal class PromptAbuserDetector {
+
+ internal var jsAlertCount = 0
+ internal var lastDialogShownAt = Date()
+ var shouldShowMoreDialogs = true
+ private set
+
+ fun resetJSAlertAbuseState() {
+ jsAlertCount = 0
+ shouldShowMoreDialogs = true
+ }
+
+ fun updateJSDialogAbusedState() {
+ if (!areDialogsAbusedByTime()) {
+ jsAlertCount = 0
+ }
+ ++jsAlertCount
+ lastDialogShownAt = Date()
+ }
+
+ fun userWantsMoreDialogs(checkBox: Boolean) {
+ shouldShowMoreDialogs = checkBox
+ }
+
+ fun areDialogsBeingAbused(): Boolean {
+ return areDialogsAbusedByTime() || areDialogsAbusedByCount()
+ }
+
+ internal fun areDialogsAbusedByTime(): Boolean {
+ return if (jsAlertCount == 0) {
+ false
+ } else {
+ val now = Date()
+ val diffInSeconds = (now.time - lastDialogShownAt.time) / SECOND_MS
+ diffInSeconds < MAX_SUCCESSIVE_DIALOG_SECONDS_LIMIT
+ }
+ }
+
+ internal fun areDialogsAbusedByCount(): Boolean {
+ return jsAlertCount > MAX_SUCCESSIVE_DIALOG_COUNT
+ }
+
+ companion object {
+ // Maximum number of successive dialogs before we prompt users to disable dialogs.
+ internal const val MAX_SUCCESSIVE_DIALOG_COUNT: Int = 2
+
+ // Minimum time required between dialogs in seconds before enabling the stop dialog.
+ internal const val MAX_SUCCESSIVE_DIALOG_SECONDS_LIMIT: Int = 3
+
+ // Number of milliseconds in 1 second.
+ internal const val SECOND_MS: Int = 1000
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt
new file mode 100644
index 0000000000..093c389cbf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt
@@ -0,0 +1,98 @@
+/* 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.prompts.dialog
+
+import androidx.fragment.app.DialogFragment
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.CreditCardValidationDelegate
+import mozilla.components.concept.storage.LoginValidationDelegate
+import mozilla.components.feature.prompts.login.LoginExceptions
+
+internal const val KEY_SESSION_ID = "KEY_SESSION_ID"
+internal const val KEY_TITLE = "KEY_TITLE"
+internal const val KEY_MESSAGE = "KEY_MESSAGE"
+internal const val KEY_PROMPT_UID = "KEY_PROMPT_UID"
+internal const val KEY_SHOULD_DISMISS_ON_LOAD = "KEY_SHOULD_DISMISS_ON_LOAD"
+
+/**
+ * An abstract representation for all different types of prompt dialogs.
+ * for handling [PromptFeature] dialogs.
+ */
+internal abstract class PromptDialogFragment : DialogFragment() {
+ var feature: Prompter? = null
+
+ internal val sessionId: String by lazy { requireNotNull(arguments).getString(KEY_SESSION_ID)!! }
+
+ internal val promptRequestUID: String by lazy { requireNotNull(arguments).getString(KEY_PROMPT_UID)!! }
+
+ /**
+ * Whether or not the dialog should automatically be dismissed when a new page is loaded.
+ */
+ internal val shouldDismissOnLoad: Boolean by lazy {
+ safeArguments.getBoolean(KEY_SHOULD_DISMISS_ON_LOAD, true)
+ }
+
+ internal val title: String by lazy { safeArguments.getString(KEY_TITLE)!! }
+
+ internal val message: String by lazy { safeArguments.getString(KEY_MESSAGE)!! }
+
+ val safeArguments get() = requireNotNull(arguments)
+}
+
+internal interface Prompter {
+
+ /**
+ * Validates whether or not a given [CreditCard] may be stored.
+ */
+ val creditCardValidationDelegate: CreditCardValidationDelegate?
+
+ /**
+ * Validates whether or not a given Login may be stored.
+ *
+ * Logging in will not prompt a save dialog if this is left null.
+ */
+ val loginValidationDelegate: LoginValidationDelegate?
+
+ /**
+ * Stores whether a site should never be prompted for logins saving.
+ */
+ val loginExceptionStorage: LoginExceptions?
+
+ /**
+ * Invoked when a dialog is dismissed. This consumes the [PromptRequest] indicated by [promptRequestUID]
+ * from the session indicated by [sessionId].
+ *
+ * @param sessionId this is the id of the session which requested the prompt.
+ * @param promptRequestUID id of the [PromptRequest] for which this dialog was shown.
+ * @param value an optional value provided by the dialog as a result of cancelling the action.
+ */
+ fun onCancel(sessionId: String, promptRequestUID: String, value: Any? = null)
+
+ /**
+ * Invoked when the user confirms the action on the dialog. This consumes the [PromptRequest] indicated
+ * by [promptRequestUID] from the session indicated by [sessionId].
+ *
+ * @param sessionId that requested to show the dialog.
+ * @param promptRequestUID id of the [PromptRequest] for which this dialog was shown.
+ * @param value an optional value provided by the dialog as a result of confirming the action.
+ */
+ fun onConfirm(sessionId: String, promptRequestUID: String, value: Any?)
+
+ /**
+ * Invoked when the user is requesting to clear the selected value from the dialog.
+ * This consumes the [PromptFeature] value from the session indicated by [sessionId].
+ *
+ * @param sessionId that requested to show the dialog.
+ * @param promptRequestUID id of the [PromptRequest] for which this dialog was shown.
+ */
+ fun onClear(sessionId: String, promptRequestUID: String)
+
+ /**
+ * Invoked when the user is requesting to open a website from the dialog.
+ *
+ * @param url The url to be opened.
+ */
+ fun onOpenLink(url: String)
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragment.kt
new file mode 100644
index 0000000000..bcc36d12f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragment.kt
@@ -0,0 +1,428 @@
+/* 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.prompts.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.content.res.ColorStateList
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.content.ContextCompat
+import androidx.core.widget.ImageViewCompat
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.concept.storage.LoginValidationDelegate.Result
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.ext.onDone
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.ktx.android.view.hideKeyboard
+import mozilla.components.support.ktx.android.view.toScope
+import mozilla.components.support.utils.ext.getParcelableCompat
+import kotlin.reflect.KProperty
+import com.google.android.material.R as MaterialR
+
+private const val KEY_LOGIN_HINT = "KEY_LOGIN_HINT"
+private const val KEY_LOGIN_USERNAME = "KEY_LOGIN_USERNAME"
+private const val KEY_LOGIN_PASSWORD = "KEY_LOGIN_PASSWORD"
+private const val KEY_LOGIN_ORIGIN = "KEY_LOGIN_ORIGIN"
+private const val KEY_LOGIN_FORM_ACTION_ORIGIN = "KEY_LOGIN_FORM_ACTION_ORIGIN"
+private const val KEY_LOGIN_HTTP_REALM = "KEY_LOGIN_HTTP_REALM"
+
+@VisibleForTesting internal const val KEY_LOGIN_ICON = "KEY_LOGIN_ICON"
+
+/**
+ * [android.support.v4.app.DialogFragment] implementation to display a
+ * dialog that allows users to save/update usernames and passwords for a given domain.
+ */
+@Suppress("LargeClass")
+internal class SaveLoginDialogFragment : PromptDialogFragment() {
+
+ private inner class SafeArgString(private val key: String) {
+ operator fun getValue(frag: SaveLoginDialogFragment, prop: KProperty<*>): String =
+ safeArguments.getString(key)!!
+
+ operator fun setValue(frag: SaveLoginDialogFragment, prop: KProperty<*>, value: String) {
+ safeArguments.putString(key, value)
+ }
+ }
+
+ private val origin by lazy { safeArguments.getString(KEY_LOGIN_ORIGIN)!! }
+ private val formActionOrigin by lazy { safeArguments.getString(KEY_LOGIN_FORM_ACTION_ORIGIN) }
+ private val httpRealm by lazy { safeArguments.getString(KEY_LOGIN_HTTP_REALM) }
+
+ @VisibleForTesting
+ internal val icon by lazy { safeArguments.getParcelableCompat(KEY_LOGIN_ICON, Bitmap::class.java) }
+
+ @VisibleForTesting
+ internal var username by SafeArgString(KEY_LOGIN_USERNAME)
+
+ @VisibleForTesting
+ internal var password by SafeArgString(KEY_LOGIN_PASSWORD)
+
+ @Volatile
+ private var loginValid = false
+ private var validateStateUpdate: Job? = null
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return BottomSheetDialog(requireContext(), R.style.MozDialogStyle).apply {
+ setCancelable(true)
+ setOnShowListener {
+ /*
+ Note: we must include a short delay before expanding the bottom sheet.
+ This is because the keyboard is still in the process of hiding when `onShowListener` is triggered.
+ Because of this, we'll only be given a small portion of the screen to draw on which will set the bottom
+ anchor of this view incorrectly to somewhere in the center of the view. If we delay a small amount we
+ are given the correct amount of space and are properly anchored.
+ */
+ CoroutineScope(IO).launch {
+ delay(KEYBOARD_HIDING_DELAY)
+ launch(Main) {
+ val bottomSheet =
+ findViewById<View>(MaterialR.id.design_bottom_sheet) as FrameLayout
+ val behavior = BottomSheetBehavior.from(bottomSheet)
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ /*
+ * If an implementation of [LoginExceptions] is hooked up to [PromptFeature], we will not
+ * show this save login dialog for any origin saved as an exception.
+ */
+ CoroutineScope(IO).launch {
+ if (feature?.loginExceptionStorage?.isLoginExceptionByOrigin(origin) == true) {
+ feature?.onCancel(sessionId, promptRequestUID)
+ dismiss()
+ }
+ }
+
+ return setupRootView(container)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ view.findViewById<AppCompatTextView>(R.id.host_name).text = origin
+
+ view.findViewById<AppCompatTextView>(R.id.save_message).text =
+ getString(R.string.mozac_feature_prompt_login_save_headline_2)
+
+ view.findViewById<Button>(R.id.save_confirm).setOnClickListener {
+ onPositiveClickAction()
+ }
+
+ view.findViewById<Button>(R.id.save_cancel).apply {
+ setOnClickListener {
+ if (this.text == context?.getString(R.string.mozac_feature_prompt_never_save)) {
+ emitNeverSaveFact()
+ CoroutineScope(IO).launch {
+ feature?.loginExceptionStorage?.addLoginException(origin)
+ }
+ }
+ feature?.onCancel(sessionId, promptRequestUID)
+ dismiss()
+ }
+ }
+
+ emitDisplayFact()
+ update()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID)
+ emitCancelFact()
+ }
+
+ private fun onPositiveClickAction() {
+ feature?.onConfirm(
+ sessionId,
+ promptRequestUID,
+ LoginEntry(
+ origin = origin,
+ formActionOrigin = formActionOrigin,
+ httpRealm = httpRealm,
+ username = username,
+ password = password,
+ ),
+ )
+ emitSaveFact()
+ dismiss()
+ }
+
+ @VisibleForTesting
+ internal fun setupRootView(container: ViewGroup? = null): View {
+ val rootView = inflateRootView(container)
+ bindUsername(rootView)
+ bindPassword(rootView)
+ bindIcon(rootView)
+ return rootView
+ }
+
+ @VisibleForTesting
+ internal fun inflateRootView(container: ViewGroup? = null): View {
+ return LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_feature_prompt_save_login_prompt,
+ container,
+ false,
+ )
+ }
+
+ private fun bindUsername(view: View) {
+ val usernameEditText = view.findViewById<TextInputEditText>(R.id.username_field)
+
+ usernameEditText.setText(username)
+ usernameEditText.addTextChangedListener(
+ object : TextWatcher {
+ override fun afterTextChanged(editable: Editable) {
+ username = editable.toString()
+ // Update accesses member state, so it must be called after username is set
+ update()
+ }
+
+ override fun beforeTextChanged(
+ s: CharSequence?,
+ start: Int,
+ count: Int,
+ after: Int,
+ ) = Unit
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) =
+ Unit
+ },
+ )
+
+ with(usernameEditText) {
+ onDone(false) {
+ hideKeyboard()
+ clearFocus()
+ }
+ }
+ }
+
+ private fun bindPassword(view: View) {
+ val passwordEditText = view.findViewById<TextInputEditText>(R.id.password_field)
+
+ passwordEditText.addTextChangedListener(
+ object : TextWatcher {
+ override fun afterTextChanged(editable: Editable) {
+ // Note that password is accessed by `fun update`
+ password = editable.toString()
+ if (password.isEmpty()) {
+ setViewState(
+ loginValid = false,
+ passwordErrorText =
+ context?.getString(R.string.mozac_feature_prompt_error_empty_password),
+ )
+ } else {
+ setViewState(
+ loginValid = true,
+ passwordErrorText = "",
+ )
+ }
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) =
+ Unit
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
+ },
+ )
+ passwordEditText.setText(password)
+
+ with(passwordEditText) {
+ onDone(false) {
+ hideKeyboard()
+ clearFocus()
+ }
+ }
+ }
+
+ private fun bindIcon(view: View) {
+ val iconView = view.findViewById<ImageView>(R.id.host_icon)
+ if (icon != null) {
+ iconView.setImageBitmap(icon)
+ } else {
+ setImageViewTint(iconView)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun setImageViewTint(imageView: ImageView) {
+ val tintColor = ContextCompat.getColor(
+ requireContext(),
+ requireContext().theme.resolveAttribute(android.R.attr.textColorPrimary),
+ )
+ ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor))
+ }
+
+ /**
+ * Check current state then update view state to match.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ fun update() = view?.toScope()?.launch(IO) {
+ val entry = LoginEntry(
+ origin = origin,
+ formActionOrigin = formActionOrigin,
+ httpRealm = httpRealm,
+ username = username,
+ password = password,
+ )
+
+ try {
+ validateStateUpdate?.cancelAndJoin()
+ } catch (cancellationException: CancellationException) {
+ Logger.error("Failed to cancel job", cancellationException)
+ }
+
+ var validateDeferred: Deferred<Result>?
+ validateStateUpdate = launch validate@{
+ if (!loginValid) {
+ // Don't run the validation logic if we know the login is invalid
+ return@validate
+ }
+ val validationDelegate =
+ feature?.loginValidationDelegate ?: return@validate
+ validateDeferred = validationDelegate.shouldUpdateOrCreateAsync(entry)
+ val result = validateDeferred?.await()
+ withContext(Main) {
+ when (result) {
+ Result.CanBeCreated -> {
+ setViewState(
+ headline = context?.getString(R.string.mozac_feature_prompt_login_save_headline_2),
+ negativeText = context?.getString(R.string.mozac_feature_prompt_never_save),
+ confirmText = context?.getString(R.string.mozac_feature_prompt_save_confirmation),
+ )
+ }
+ is Result.CanBeUpdated -> {
+ setViewState(
+ headline = if (result.foundLogin.username.isEmpty()) {
+ context?.getString(
+ R.string.mozac_feature_prompt_login_add_username_headline,
+ )
+ } else {
+ context?.getString(R.string.mozac_feature_prompt_login_update_headline_2)
+ },
+ negativeText = context?.getString(R.string.mozac_feature_prompt_dont_update),
+ confirmText =
+ context?.getString(R.string.mozac_feature_prompt_update_confirmation),
+ )
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+ validateStateUpdate?.invokeOnCompletion {
+ if (it is CancellationException) {
+ validateDeferred?.cancel()
+ }
+ }
+ }
+ }
+
+ private fun setViewState(
+ headline: String? = null,
+ negativeText: String? = null,
+ confirmText: String? = null,
+ loginValid: Boolean? = null,
+ passwordErrorText: String? = null,
+ ) {
+ if (headline != null) {
+ view?.findViewById<AppCompatTextView>(R.id.save_message)?.text = headline
+ }
+
+ if (negativeText != null) {
+ view?.findViewById<MaterialButton>(R.id.save_cancel)?.text = negativeText
+ }
+
+ val confirmButton = view?.findViewById<Button>(R.id.save_confirm)
+ if (confirmText != null) {
+ confirmButton?.text = confirmText
+ }
+
+ if (loginValid != null) {
+ this.loginValid = loginValid
+ confirmButton?.isEnabled = loginValid
+ }
+
+ if (passwordErrorText != null) {
+ view?.findViewById<TextInputLayout>(R.id.password_text_input_layout)?.error =
+ passwordErrorText
+ }
+ }
+
+ companion object {
+ private const val KEYBOARD_HIDING_DELAY = 100L
+
+ /**
+ * A builder method for creating a [SaveLoginDialogFragment]
+ * @param sessionId the id of the session for which this dialog will be created.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog is shown.
+ * @param shouldDismissOnLoad whether or not the dialog should automatically be dismissed
+ * when a new page is loaded.
+ * @param hint a value that helps to determine the appropriate prompting behavior.
+ * @param login represents login information on a given domain.
+ * */
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ hint: Int,
+ entry: LoginEntry,
+ icon: Bitmap? = null,
+ ): SaveLoginDialogFragment {
+ val fragment = SaveLoginDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putInt(KEY_LOGIN_HINT, hint)
+ putString(KEY_LOGIN_USERNAME, entry.username)
+ putString(KEY_LOGIN_PASSWORD, entry.password)
+ putString(KEY_LOGIN_ORIGIN, entry.origin)
+ putString(KEY_LOGIN_FORM_ACTION_ORIGIN, entry.formActionOrigin)
+ putString(KEY_LOGIN_HTTP_REALM, entry.httpRealm)
+ putParcelable(KEY_LOGIN_ICON, icon)
+ }
+
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragment.kt
new file mode 100644
index 0000000000..01492e96e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragment.kt
@@ -0,0 +1,130 @@
+/* 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.prompts.dialog
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.widget.EditText
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import mozilla.components.feature.prompts.R
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+
+private const val KEY_USER_EDIT_TEXT = "KEY_USER_EDIT_TEXT"
+private const val KEY_LABEL_INPUT = "KEY_LABEL_INPUT"
+private const val KEY_DEFAULT_INPUT_VALUE = "KEY_DEFAULT_INPUT_VALUE"
+
+/**
+ * [androidx.fragment.app.DialogFragment] implementation to display a
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt">Window.prompt()</a> with native dialogs.
+ */
+internal class TextPromptDialogFragment : AbstractPromptTextDialogFragment(), TextWatcher {
+ /**
+ * Contains the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt#Parameters">default()</a>
+ * value provided by this [sessionId].
+ */
+ internal val defaultInputValue: String? by lazy { safeArguments.getString(KEY_DEFAULT_INPUT_VALUE) }
+
+ /**
+ * Contains the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt#Parameters">message</a>
+ * value provided by this [sessionId].
+ */
+ internal val labelInput: String? by lazy { safeArguments.getString(KEY_LABEL_INPUT) }
+
+ private var userSelectionEditText: String
+ get() = safeArguments.getString(KEY_USER_EDIT_TEXT, defaultInputValue)
+ set(value) {
+ safeArguments.putString(KEY_USER_EDIT_TEXT, value)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(requireContext())
+ .setTitle(title)
+ .setCancelable(true)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ onPositiveClickAction()
+ }
+ return addLayout(builder).create().withCenterAlignedButtons()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+
+ private fun onPositiveClickAction() {
+ feature?.onConfirm(sessionId, promptRequestUID, userSelectionNoMoreDialogs to userSelectionEditText)
+ }
+
+ @SuppressLint("InflateParams")
+ private fun addLayout(builder: AlertDialog.Builder): AlertDialog.Builder {
+ val inflater = LayoutInflater.from(requireContext())
+ val view = inflater.inflate(R.layout.mozac_feature_text_prompt, null)
+
+ val label = view.findViewById<TextView>(R.id.input_label)
+ val editText = view.findViewById<EditText>(R.id.input_value)
+
+ label.text = labelInput
+ editText.setText(defaultInputValue)
+ editText.addTextChangedListener(this)
+
+ addCheckBoxIfNeeded(view)
+
+ return builder.setView(view)
+ }
+
+ override fun afterTextChanged(editable: Editable) {
+ userSelectionEditText = editable.toString()
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
+
+ companion object {
+ /**
+ * A builder method for creating a [TextPromptDialogFragment]
+ * @param sessionId to create the dialog.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog is shown.
+ * @param shouldDismissOnLoad whether or not the dialog should automatically be dismissed
+ * when a new page is loaded.
+ * @param title the title of the dialog.
+ * @param inputLabel
+ * @param defaultInputValue
+ * @param hasShownManyDialogs tells if this [sessionId] has shown many dialogs
+ * in a short period of time, if is true a checkbox will be part of the dialog, for the user
+ * to choose if wants to prevent this [sessionId] continuing showing dialogs.
+ */
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ title: String,
+ inputLabel: String,
+ defaultInputValue: String,
+ hasShownManyDialogs: Boolean,
+ ): TextPromptDialogFragment {
+ val fragment = TextPromptDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putString(KEY_TITLE, title)
+ putString(KEY_LABEL_INPUT, inputLabel)
+ putString(KEY_DEFAULT_INPUT_VALUE, defaultInputValue)
+ putBoolean(KEY_MANY_ALERTS, hasShownManyDialogs)
+ }
+
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragment.kt
new file mode 100644
index 0000000000..60f1f3e415
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragment.kt
@@ -0,0 +1,342 @@
+/* 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.prompts.dialog
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.app.DatePickerDialog
+import android.app.Dialog
+import android.app.TimePickerDialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.DialogInterface.BUTTON_NEGATIVE
+import android.content.DialogInterface.BUTTON_NEUTRAL
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.os.Build
+import android.os.Build.VERSION_CODES.M
+import android.os.Bundle
+import android.text.format.DateFormat
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.DatePicker
+import android.widget.TimePicker
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.ext.day
+import mozilla.components.feature.prompts.ext.hour
+import mozilla.components.feature.prompts.ext.millisecond
+import mozilla.components.feature.prompts.ext.minute
+import mozilla.components.feature.prompts.ext.month
+import mozilla.components.feature.prompts.ext.second
+import mozilla.components.feature.prompts.ext.toCalendar
+import mozilla.components.feature.prompts.ext.year
+import mozilla.components.feature.prompts.widget.MonthAndYearPicker
+import mozilla.components.feature.prompts.widget.TimePrecisionPicker
+import mozilla.components.support.utils.TimePicker.shouldShowSecondsPicker
+import mozilla.components.support.utils.ext.getSerializableCompat
+import mozilla.components.ui.widgets.withCenterAlignedButtons
+import java.util.Calendar
+import java.util.Date
+
+private const val KEY_INITIAL_DATE = "KEY_INITIAL_DATE"
+private const val KEY_MIN_DATE = "KEY_MIN_DATE"
+private const val KEY_MAX_DATE = "KEY_MAX_DATE"
+private const val KEY_SELECTED_DATE = "KEY_SELECTED_DATE"
+private const val KEY_SELECTION_TYPE = "KEY_SELECTION_TYPE"
+private const val KEY_STEP_VALUE = "KEY_STEP_VALUE"
+
+/**
+ * [DialogFragment][androidx.fragment.app.DialogFragment] implementation to display date picker with a native dialog.
+ */
+internal class TimePickerDialogFragment :
+ PromptDialogFragment(),
+ DatePicker.OnDateChangedListener,
+ TimePicker.OnTimeChangedListener,
+ TimePickerDialog.OnTimeSetListener,
+ DatePickerDialog.OnDateSetListener,
+ DialogInterface.OnClickListener,
+ MonthAndYearPicker.OnDateSetListener,
+ TimePrecisionPicker.OnTimeSetListener {
+ private val initialDate: Date by lazy {
+ safeArguments.getSerializableCompat(KEY_INITIAL_DATE, Date::class.java) as Date
+ }
+ private val minimumDate: Date? by lazy {
+ safeArguments.getSerializableCompat(KEY_MIN_DATE, Date::class.java) as? Date
+ }
+ private val maximumDate: Date? by lazy {
+ safeArguments.getSerializableCompat(KEY_MAX_DATE, Date::class.java) as? Date
+ }
+ private val selectionType: Int by lazy { safeArguments.getInt(KEY_SELECTION_TYPE) }
+ private val stepSize: String? by lazy { safeArguments.getString(KEY_STEP_VALUE) }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var selectedDate: Date
+ get() = safeArguments.getSerializableCompat(KEY_SELECTED_DATE, Date::class.java) as Date
+ set(value) {
+ safeArguments.putSerializable(KEY_SELECTED_DATE, value)
+ }
+
+ @Suppress("ComplexMethod")
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val context = requireContext()
+ val dialog = when (selectionType) {
+ SELECTION_TYPE_TIME -> createTimePickerDialog(context)
+ SELECTION_TYPE_DATE -> initialDate.toCalendar().let { cal ->
+ DatePickerDialog(
+ context,
+ this@TimePickerDialogFragment,
+ cal.year,
+ cal.month,
+ cal.day,
+ ).apply { setMinMaxDate(datePicker) }
+ }
+ SELECTION_TYPE_DATE_AND_TIME -> AlertDialog.Builder(context)
+ .setView(inflateDateTimePicker(LayoutInflater.from(context)))
+ .create()
+ .also {
+ it.setButton(BUTTON_POSITIVE, context.getString(R.string.mozac_feature_prompts_set_date), this)
+ it.setButton(BUTTON_NEGATIVE, context.getString(R.string.mozac_feature_prompts_cancel), this)
+ }
+ SELECTION_TYPE_MONTH -> AlertDialog.Builder(context)
+ .setTitle(R.string.mozac_feature_prompts_set_month)
+ .setView(inflateDateMonthPicker())
+ .create()
+ .also {
+ it.setButton(BUTTON_POSITIVE, context.getString(R.string.mozac_feature_prompts_set_date), this)
+ it.setButton(BUTTON_NEGATIVE, context.getString(R.string.mozac_feature_prompts_cancel), this)
+ }
+ else -> throw IllegalArgumentException()
+ }
+
+ dialog.also {
+ it.setCancelable(true)
+ it.setButton(BUTTON_NEUTRAL, context.getString(R.string.mozac_feature_prompts_clear), this)
+ }
+
+ return dialog
+ }
+
+ /**
+ * Called when the user touches outside of the dialog.
+ */
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ onClick(dialog, BUTTON_NEGATIVE)
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ val alertDialog = dialog
+ if (alertDialog is AlertDialog) {
+ // We want to call the extension function after the show() call on the dialog,
+ // and the DialogFragment does that call during onStart().
+ alertDialog.withCenterAlignedButtons()
+ }
+ }
+
+ // Create the appropriate time picker dialog for the given step value.
+ private fun createTimePickerDialog(context: Context): AlertDialog {
+ // Create the Android time picker dialog
+ fun createTimePickerDialog(): AlertDialog {
+ return initialDate.toCalendar().let { cal ->
+ TimePickerDialog(
+ context,
+ this,
+ cal.hour,
+ cal.minute,
+ DateFormat.is24HourFormat(context),
+ )
+ }
+ }
+
+ // Create the custom time picker dialog
+ fun createTimeStepPickerDialog(stepValue: Float): AlertDialog {
+ return AlertDialog.Builder(context)
+ .setTitle(R.string.mozac_feature_prompts_set_time)
+ .setView(
+ TimePrecisionPicker(
+ context = requireContext(),
+ selectedTime = initialDate.toCalendar(),
+ maxTime = maximumDate?.toCalendar()
+ ?: TimePrecisionPicker.getDefaultMaxTime(),
+ minTime = minimumDate?.toCalendar()
+ ?: TimePrecisionPicker.getDefaultMinTime(),
+ stepValue = stepValue,
+ timeSetListener = this,
+ ),
+ )
+ .create()
+ .also {
+ it.setButton(
+ BUTTON_POSITIVE,
+ context.getString(R.string.mozac_feature_prompts_set_date),
+ this,
+ )
+ it.setButton(
+ BUTTON_NEGATIVE,
+ context.getString(R.string.mozac_feature_prompts_cancel),
+ this,
+ )
+ }
+ }
+
+ return if (!shouldShowSecondsPicker(stepSize?.toFloat())) {
+ createTimePickerDialog()
+ } else {
+ stepSize?.let {
+ createTimeStepPickerDialog(it.toFloat())
+ } ?: createTimePickerDialog()
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ private fun inflateDateTimePicker(inflater: LayoutInflater): View {
+ val view = inflater.inflate(R.layout.mozac_feature_prompts_date_time_picker, null)
+ val datePicker = view.findViewById<DatePicker>(R.id.date_picker)
+ val dateTimePicker = view.findViewById<TimePicker>(R.id.datetime_picker)
+ val cal = initialDate.toCalendar()
+
+ // Bind date picker
+ setMinMaxDate(datePicker)
+ datePicker.init(cal.year, cal.month, cal.day, this)
+ initTimePicker(dateTimePicker, cal)
+
+ return view
+ }
+
+ private fun inflateDateMonthPicker(): View {
+ return MonthAndYearPicker(
+ context = requireContext(),
+ selectedDate = initialDate.toCalendar(),
+ maxDate = maximumDate?.toCalendar() ?: MonthAndYearPicker.getDefaultMaxDate(),
+ minDate = minimumDate?.toCalendar() ?: MonthAndYearPicker.getDefaultMinDate(),
+ dateSetListener = this,
+ )
+ }
+
+ @Suppress("DEPRECATION")
+ private fun initTimePicker(picker: TimePicker, cal: Calendar) {
+ if (Build.VERSION.SDK_INT >= M) {
+ picker.hour = cal.hour
+ picker.minute = cal.minute
+ } else {
+ picker.currentHour = cal.hour
+ picker.currentMinute = cal.minute
+ }
+ picker.setIs24HourView(DateFormat.is24HourFormat(requireContext()))
+ picker.setOnTimeChangedListener(this)
+ }
+
+ private fun setMinMaxDate(datePicker: DatePicker) {
+ minimumDate?.let {
+ datePicker.minDate = it.time
+ }
+ maximumDate?.let {
+ datePicker.maxDate = it.time
+ }
+ }
+
+ override fun onTimeSet(
+ picker: TimePrecisionPicker,
+ hour: Int,
+ minute: Int,
+ second: Int,
+ millisecond: Int,
+ ) {
+ val calendar = selectedDate.toCalendar()
+ calendar.hour = hour
+ calendar.minute = minute
+ calendar.second = second
+ calendar.millisecond = millisecond
+ selectedDate = calendar.time
+ }
+
+ override fun onDateChanged(view: DatePicker?, year: Int, monthOfYear: Int, dayOfMonth: Int) {
+ val calendar = Calendar.getInstance()
+ calendar.set(year, monthOfYear, dayOfMonth)
+ selectedDate = calendar.time
+ }
+
+ override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) {
+ onDateChanged(view, year, month, dayOfMonth)
+ onClick(null, BUTTON_POSITIVE)
+ }
+
+ override fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int) {
+ onDateChanged(null, year, month, 0)
+ }
+
+ override fun onTimeChanged(picker: TimePicker?, hourOfDay: Int, minute: Int) {
+ val calendar = selectedDate.toCalendar()
+ calendar.set(Calendar.HOUR_OF_DAY, hourOfDay)
+ calendar.set(Calendar.MINUTE, minute)
+ selectedDate = calendar.time
+ }
+
+ override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) {
+ onTimeChanged(view, hourOfDay, minute)
+ onClick(null, BUTTON_POSITIVE)
+ }
+
+ override fun onClick(dialog: DialogInterface?, which: Int) {
+ when (which) {
+ BUTTON_POSITIVE -> feature?.onConfirm(sessionId, promptRequestUID, selectedDate)
+ BUTTON_NEGATIVE -> feature?.onCancel(sessionId, promptRequestUID)
+ BUTTON_NEUTRAL -> feature?.onClear(sessionId, promptRequestUID)
+ }
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [TimePickerDialogFragment]
+ * @param sessionId to create the dialog.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog is shown.
+ * @param shouldDismissOnLoad whether or not the dialog should automatically be dismissed
+ * when a new page is loaded.
+ * @param title of the dialog.
+ * @param initialDate date that will be selected by default.
+ * @param minDate the minimumDate date that will be allowed to be selected.
+ * @param maxDate the maximumDate date that will be allowed to be selected.
+ * @param selectionType indicate which type of time should be selected, valid values are
+ * ([TimePickerDialogFragment.SELECTION_TYPE_DATE], [TimePickerDialogFragment.SELECTION_TYPE_DATE_AND_TIME],
+ * and [TimePickerDialogFragment.SELECTION_TYPE_TIME])
+ * @param stepValue value of time jumped whenever the time is incremented/decremented.
+ *
+ * @return a new instance of [TimePickerDialogFragment]
+ */
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ initialDate: Date,
+ minDate: Date?,
+ maxDate: Date?,
+ selectionType: Int = SELECTION_TYPE_DATE,
+ stepValue: String? = null,
+ ): TimePickerDialogFragment {
+ val fragment = TimePickerDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+ fragment.arguments = arguments
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putSerializable(KEY_INITIAL_DATE, initialDate)
+ putSerializable(KEY_MIN_DATE, minDate)
+ putSerializable(KEY_MAX_DATE, maxDate)
+ putString(KEY_STEP_VALUE, stepValue)
+ putInt(KEY_SELECTION_TYPE, selectionType)
+ }
+ fragment.selectedDate = initialDate
+ return fragment
+ }
+
+ const val SELECTION_TYPE_DATE = 1
+ const val SELECTION_TYPE_DATE_AND_TIME = 2
+ const val SELECTION_TYPE_TIME = 3
+ const val SELECTION_TYPE_MONTH = 4
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/Calendar.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/Calendar.kt
new file mode 100644
index 0000000000..ac50247251
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/Calendar.kt
@@ -0,0 +1,65 @@
+/* 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/. */
+
+@file:Suppress("TooManyFunctions")
+
+package mozilla.components.feature.prompts.ext
+
+import java.util.Calendar
+import java.util.Date
+
+internal fun Date.toCalendar() = Calendar.getInstance().also { it.time = this }
+
+internal var Calendar.millisecond: Int
+ get() = get(Calendar.MILLISECOND)
+ set(value) {
+ set(Calendar.MILLISECOND, value)
+ }
+internal var Calendar.second: Int
+ get() = get(Calendar.SECOND)
+ set(value) {
+ set(Calendar.SECOND, value)
+ }
+internal var Calendar.minute: Int
+ get() = get(Calendar.MINUTE)
+ set(value) {
+ set(Calendar.MINUTE, value)
+ }
+internal var Calendar.hour: Int
+ get() = get(Calendar.HOUR_OF_DAY)
+ set(value) {
+ set(Calendar.HOUR_OF_DAY, value)
+ }
+internal var Calendar.day: Int
+ get() = get(Calendar.DAY_OF_MONTH)
+ set(value) {
+ set(Calendar.DAY_OF_MONTH, value)
+ }
+internal var Calendar.year: Int
+ get() = get(Calendar.YEAR)
+ set(value) {
+ set(Calendar.YEAR, value)
+ }
+
+internal var Calendar.month: Int
+ get() = get(Calendar.MONTH)
+ set(value) {
+ set(Calendar.MONTH, value)
+ }
+
+internal fun Calendar.minMillisecond(): Int = getActualMinimum(Calendar.MILLISECOND)
+internal fun Calendar.maxMillisecond(): Int = getActualMaximum(Calendar.MILLISECOND)
+internal fun Calendar.minSecond(): Int = getActualMinimum(Calendar.SECOND)
+internal fun Calendar.maxSecond(): Int = getActualMaximum(Calendar.SECOND)
+internal fun Calendar.minMinute(): Int = getActualMinimum(Calendar.MINUTE)
+internal fun Calendar.maxMinute(): Int = getActualMaximum(Calendar.MINUTE)
+internal fun Calendar.minHour(): Int = getActualMinimum(Calendar.HOUR_OF_DAY)
+internal fun Calendar.maxHour(): Int = getActualMaximum(Calendar.HOUR_OF_DAY)
+internal fun Calendar.minMonth(): Int = getMinimum(Calendar.MONTH)
+internal fun Calendar.maxMonth(): Int = getActualMaximum(Calendar.MONTH)
+internal fun Calendar.minDay(): Int = getMinimum(Calendar.DAY_OF_MONTH)
+internal fun Calendar.maxDay(): Int = getActualMaximum(Calendar.DAY_OF_MONTH)
+internal fun Calendar.minYear(): Int = getMinimum(Calendar.YEAR)
+internal fun Calendar.maxYear(): Int = getActualMaximum(Calendar.YEAR)
+internal fun now() = Calendar.getInstance()
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/EditText.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/EditText.kt
new file mode 100644
index 0000000000..370037df6a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/EditText.kt
@@ -0,0 +1,25 @@
+/* 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.prompts.ext
+
+import android.view.inputmethod.EditorInfo
+import android.widget.EditText
+
+/**
+ * Extension function to handle keyboard Done action
+ * @param actionConsumed true if you have consumed the action, else false.
+ * @param onDonePressed callback to execute when Done key is pressed
+ */
+internal fun EditText.onDone(actionConsumed: Boolean, onDonePressed: () -> Unit) {
+ setOnEditorActionListener { _, actionId, _ ->
+ when (actionId) {
+ EditorInfo.IME_ACTION_DONE -> {
+ onDonePressed()
+ actionConsumed
+ }
+ else -> false
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/PromptRequest.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/PromptRequest.kt
new file mode 100644
index 0000000000..17db873e40
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/PromptRequest.kt
@@ -0,0 +1,36 @@
+/* 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.prompts.ext
+
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.prompt.PromptRequest.Alert
+import mozilla.components.concept.engine.prompt.PromptRequest.Confirm
+import mozilla.components.concept.engine.prompt.PromptRequest.Popup
+import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
+import kotlin.reflect.KClass
+
+/**
+ * List of all prompts who are not to be shown in fullscreen.
+ */
+@PublishedApi
+internal val PROMPTS_TO_EXIT_FULLSCREEN_FOR = listOf<KClass<out PromptRequest>>(
+ Alert::class,
+ TextPrompt::class,
+ Confirm::class,
+ Popup::class,
+)
+
+/**
+ * Convenience method for executing code if the current [PromptRequest] is one that
+ * should not be shown in fullscreen tabs.
+ */
+internal inline fun <reified T> T.executeIfWindowedPrompt(
+ block: () -> Unit,
+) where T : PromptRequest {
+ PROMPTS_TO_EXIT_FULLSCREEN_FOR
+ .firstOrNull {
+ this::class == it
+ }?.let { block.invoke() }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/AddressAutofillDialogFacts.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/AddressAutofillDialogFacts.kt
new file mode 100644
index 0000000000..c7142a12d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/AddressAutofillDialogFacts.kt
@@ -0,0 +1,76 @@
+/* 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.prompts.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the Autofill prompt feature for addresses.
+ */
+class AddressAutofillDialogFacts {
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val AUTOFILL_ADDRESS_FORM_DETECTED = "autofill_address_form_detected"
+ const val AUTOFILL_ADDRESS_SUCCESS = "autofill_address_success"
+ const val AUTOFILL_ADDRESS_PROMPT_SHOWN = "autofill_address_prompt_shown"
+ const val AUTOFILL_ADDRESS_PROMPT_EXPANDED = "autofill_address_prompt_expanded"
+ const val AUTOFILL_ADDRESS_PROMPT_DISMISSED = "autofill_address_prompt_dismissed"
+ }
+}
+
+private fun emitAddressAutofillDialogFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_PROMPTS,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitSuccessfulAddressAutofillFormDetectedFact() {
+ emitAddressAutofillDialogFact(
+ Action.INTERACTION,
+ AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_FORM_DETECTED,
+ )
+}
+
+internal fun emitSuccessfulAddressAutofillSuccessFact() {
+ emitAddressAutofillDialogFact(
+ Action.INTERACTION,
+ AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_SUCCESS,
+ )
+}
+
+internal fun emitAddressAutofillShownFact() {
+ emitAddressAutofillDialogFact(
+ Action.INTERACTION,
+ AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_SHOWN,
+ )
+}
+
+internal fun emitAddressAutofillExpandedFact() {
+ emitAddressAutofillDialogFact(
+ Action.INTERACTION,
+ AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_EXPANDED,
+ )
+}
+
+internal fun emitAddressAutofillDismissedFact() {
+ emitAddressAutofillDialogFact(
+ Action.INTERACTION,
+ AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_DISMISSED,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFacts.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFacts.kt
new file mode 100644
index 0000000000..6160c82f52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFacts.kt
@@ -0,0 +1,100 @@
+/* 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.prompts.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the Autofill prompt feature for credit cards.
+ */
+class CreditCardAutofillDialogFacts {
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val AUTOFILL_CREDIT_CARD_FORM_DETECTED = "autofill_credit_card_form_detected"
+ const val AUTOFILL_CREDIT_CARD_SUCCESS = "autofill_credit_card_success"
+ const val AUTOFILL_CREDIT_CARD_PROMPT_SHOWN = "autofill_credit_card_prompt_shown"
+ const val AUTOFILL_CREDIT_CARD_PROMPT_EXPANDED = "autofill_credit_card_prompt_expanded"
+ const val AUTOFILL_CREDIT_CARD_PROMPT_DISMISSED = "autofill_credit_card_prompt_dismissed"
+ const val AUTOFILL_CREDIT_CARD_CREATED = "autofill_credit_card_created"
+ const val AUTOFILL_CREDIT_CARD_UPDATED = "autofill_credit_card_updated"
+ const val AUTOFILL_CREDIT_CARD_SAVE_PROMPT_SHOWN = "autofill_credit_card_save_prompt_shown"
+ }
+}
+
+private fun emitCreditCardAutofillDialogFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_PROMPTS,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitSuccessfulCreditCardAutofillFormDetectedFact() {
+ emitCreditCardAutofillDialogFact(
+ Action.INTERACTION,
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_FORM_DETECTED,
+ )
+}
+
+internal fun emitSuccessfulCreditCardAutofillSuccessFact() {
+ emitCreditCardAutofillDialogFact(
+ Action.INTERACTION,
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SUCCESS,
+ )
+}
+
+internal fun emitCreditCardAutofillShownFact() {
+ emitCreditCardAutofillDialogFact(
+ Action.INTERACTION,
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_SHOWN,
+ )
+}
+
+internal fun emitCreditCardAutofillExpandedFact() {
+ emitCreditCardAutofillDialogFact(
+ Action.INTERACTION,
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_EXPANDED,
+ )
+}
+
+internal fun emitCreditCardAutofillDismissedFact() {
+ emitCreditCardAutofillDialogFact(
+ Action.INTERACTION,
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_DISMISSED,
+ )
+}
+
+internal fun emitCreditCardAutofillCreatedFact() {
+ emitCreditCardAutofillDialogFact(
+ Action.CONFIRM,
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_CREATED,
+ )
+}
+
+internal fun emitCreditCardAutofillUpdatedFact() {
+ emitCreditCardAutofillDialogFact(
+ Action.CONFIRM,
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_UPDATED,
+ )
+}
+
+internal fun emitCreditCardSaveShownFact() {
+ emitCreditCardAutofillDialogFact(
+ Action.DISPLAY,
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SAVE_PROMPT_SHOWN,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/LoginAutofillDialogFacts.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/LoginAutofillDialogFacts.kt
new file mode 100644
index 0000000000..0098598137
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/LoginAutofillDialogFacts.kt
@@ -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/. */
+
+package mozilla.components.feature.prompts.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the Autofill prompt feature for logins.
+ */
+class LoginAutofillDialogFacts {
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val AUTOFILL_LOGIN_PROMPT_SHOWN = "autofill_login_prompt_shown"
+ const val AUTOFILL_LOGIN_PROMPT_DISMISSED = "autofill_login_prompt_dismissed"
+ const val AUTOFILL_LOGIN_PERFORMED = "autofill_login_performed"
+ }
+}
+
+private fun emitLoginAutofillDialogFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_PROMPTS,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitLoginAutofillShownFact() {
+ emitLoginAutofillDialogFact(
+ Action.INTERACTION,
+ LoginAutofillDialogFacts.Items.AUTOFILL_LOGIN_PROMPT_SHOWN,
+ )
+}
+
+internal fun emitLoginAutofillPerformedFact() {
+ emitLoginAutofillDialogFact(
+ Action.INTERACTION,
+ LoginAutofillDialogFacts.Items.AUTOFILL_LOGIN_PERFORMED,
+ )
+}
+
+internal fun emitLoginAutofillDismissedFact() {
+ emitLoginAutofillDialogFact(
+ Action.INTERACTION,
+ LoginAutofillDialogFacts.Items.AUTOFILL_LOGIN_PROMPT_DISMISSED,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/PromptFacts.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/PromptFacts.kt
new file mode 100644
index 0000000000..c5374fe5aa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/facts/PromptFacts.kt
@@ -0,0 +1,61 @@
+/* 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.prompts.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for different events related to the prompt feature.
+ */
+class PromptFacts {
+ /**
+ * Different events emitted by prompts.
+ */
+ object Items {
+ const val PROMPT = "PROMPT"
+ }
+}
+
+private fun emitFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_PROMPTS,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitPromptDisplayedFact(promptName: String) {
+ emitFact(
+ Action.DISPLAY,
+ PromptFacts.Items.PROMPT,
+ value = promptName,
+ )
+}
+
+internal fun emitPromptDismissedFact(promptName: String) {
+ emitFact(
+ Action.CANCEL,
+ PromptFacts.Items.PROMPT,
+ value = promptName,
+ )
+}
+
+internal fun emitPromptConfirmedFact(promptName: String) {
+ emitFact(
+ Action.CONFIRM,
+ PromptFacts.Items.PROMPT,
+ value = promptName,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FilePicker.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FilePicker.kt
new file mode 100644
index 0000000000..a07fcab997
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FilePicker.kt
@@ -0,0 +1,247 @@
+/* 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.prompts.file
+
+import android.app.Activity
+import android.app.Activity.RESULT_OK
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.EXTRA_INITIAL_INTENTS
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.net.Uri
+import android.provider.MediaStore.EXTRA_OUTPUT
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.fragment.app.Fragment
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.prompt.PromptRequest.File
+import mozilla.components.feature.prompts.PromptContainer
+import mozilla.components.feature.prompts.consumePromptFrom
+import mozilla.components.support.base.feature.OnNeedToRequestPermissions
+import mozilla.components.support.base.feature.PermissionsFeature
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+import mozilla.components.support.ktx.android.net.getFileName
+import mozilla.components.support.ktx.android.net.isUnderPrivateAppDirectory
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+
+/**
+ * The image capture intent doesn't return the URI where the image is saved,
+ * so we track it here.
+ *
+ * Top-level scoped to survive activity recreation in the "Don't keep activities" scenario.
+ */
+@VisibleForTesting
+internal var captureUri: Uri? = null
+
+/**
+ * @property container The [Activity] or [Fragment] which hosts the file picker.
+ * @property store The [BrowserStore] this feature should subscribe to.
+ * @property fileUploadsDirCleaner a [FileUploadsDirCleaner] to clean up temporary file uploads.
+ * @property onNeedToRequestPermissions a callback invoked when permissions
+ * need to be requested before a prompt (e.g. a file picker) can be displayed.
+ * Once the request is completed, [onPermissionsResult] needs to be invoked.
+ */
+internal class FilePicker(
+ private val container: PromptContainer,
+ private val store: BrowserStore,
+ private var sessionId: String? = null,
+ private var fileUploadsDirCleaner: FileUploadsDirCleaner,
+ override val onNeedToRequestPermissions: OnNeedToRequestPermissions,
+) : PermissionsFeature {
+
+ private val logger = Logger("FilePicker")
+
+ /**
+ * Cache of the current request to be used after permission is granted.
+ */
+ @VisibleForTesting
+ internal var currentRequest: PromptRequest? = null
+
+ @Suppress("ComplexMethod")
+ fun handleFileRequest(promptRequest: File, requestPermissions: Boolean = true) {
+ // Track which permissions are needed.
+ val neededPermissions = mutableSetOf<String>()
+ // Build a list of intents for capturing media and opening the file picker to combine later.
+ val intents = mutableListOf<Intent>()
+ captureUri = null
+
+ // Compare the accepted values against image/*, video/*, and audio/*
+ for (type in MimeType.values()) {
+ val hasPermission = container.context.isPermissionGranted(type.permission)
+ // The captureMode attribute can be used if the accepted types are exactly for
+ // image/*, video/*, or audio/*.
+ if (hasPermission && type.shouldCapture(
+ promptRequest.mimeTypes,
+ promptRequest.captureMode,
+ )
+ ) {
+ type.buildIntent(container.context, promptRequest)?.also {
+ saveCaptureUriIfPresent(it)
+ container.startActivityForResult(it, FILE_PICKER_ACTIVITY_REQUEST_CODE)
+ return
+ }
+ }
+ // Otherwise, build the intent and create a chooser later
+ if (type.matches(promptRequest.mimeTypes)) {
+ if (hasPermission) {
+ type.buildIntent(container.context, promptRequest)?.also {
+ saveCaptureUriIfPresent(it)
+ intents.add(it)
+ }
+ } else {
+ neededPermissions.addAll(type.permission)
+ }
+ }
+ }
+
+ val canSkipPermissionRequest = !requestPermissions && intents.isNotEmpty()
+
+ if (neededPermissions.isEmpty() || canSkipPermissionRequest) {
+ // Combine the intents together using a chooser.
+ val lastIntent = intents.removeAt(intents.lastIndex)
+ val chooser = Intent.createChooser(lastIntent, null).apply {
+ putExtra(EXTRA_INITIAL_INTENTS, intents.toTypedArray())
+ }
+
+ container.startActivityForResult(chooser, FILE_PICKER_ACTIVITY_REQUEST_CODE)
+ } else {
+ askAndroidPermissionsForRequest(neededPermissions, promptRequest)
+ }
+ }
+
+ /**
+ * Notifies the feature of intent results for prompt requests handled by
+ * other apps like file chooser requests.
+ *
+ * @param requestCode The code of the app that requested the intent.
+ * @param intent The result of the request.
+ */
+ fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean {
+ var resultHandled = false
+ val request = getActivePromptRequest() ?: return false
+ if (requestCode == FILE_PICKER_ACTIVITY_REQUEST_CODE && request is File) {
+ store.consumePromptFrom(sessionId, request.uid) {
+ if (resultCode == RESULT_OK) {
+ handleFilePickerIntentResult(intent, request)
+ } else {
+ request.onDismiss()
+ }
+ }
+ resultHandled = true
+ }
+ if (request !is File) {
+ logger.error("Invalid PromptRequest expected File but $request was provided")
+ }
+
+ return resultHandled
+ }
+
+ private fun getActivePromptRequest(): PromptRequest? =
+ store.state.findCustomTabOrSelectedTab(sessionId)?.content?.promptRequests?.lastOrNull { prompt ->
+ prompt is File
+ }
+
+ /**
+ * Notifies the feature that the permissions request was completed. It will then
+ * either process or dismiss the prompt request.
+ *
+ * @param permissions List of permission requested.
+ * @param grantResults The grant results for the corresponding permissions
+ * @see [onNeedToRequestPermissions].
+ */
+ override fun onPermissionsResult(permissions: Array<String>, grantResults: IntArray) {
+ if (grantResults.isNotEmpty() && grantResults.all { it == PERMISSION_GRANTED }) {
+ onPermissionsGranted()
+ } else {
+ onPermissionsDenied()
+ }
+ currentRequest = null
+ }
+
+ /**
+ * Used in conjunction with [onNeedToRequestPermissions], to notify the feature
+ * that all the required permissions have been granted, and the pending [PromptRequest]
+ * can be performed.
+ *
+ * If the required permission has not been granted
+ * [onNeedToRequestPermissions] will be called.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun onPermissionsGranted() {
+ // Try again handling the original request which we've cached before.
+ // Actually consuming it will be done in onActivityResult once the user returns from the file picker.
+ handleFileRequest(currentRequest as File, requestPermissions = false)
+ }
+
+ /**
+ * Used in conjunction with [onNeedToRequestPermissions] to notify the feature that one
+ * or more required permissions have been denied.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun onPermissionsDenied() {
+ // Nothing left to do. Consume / cleanup the requests.
+ store.consumePromptFrom<File>(sessionId) { request ->
+ request.onDismiss()
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun handleFilePickerIntentResult(intent: Intent?, request: File) {
+ if (intent?.clipData != null && request.isMultipleFilesSelection) {
+ intent.clipData?.run {
+ val uris = Array<Uri>(itemCount) { index -> getItemAt(index).uri }
+ // We want to verify that we are not exposing any private data
+ val sanitizedUris = uris.removeUrisUnderPrivateAppDir(container.context)
+ if (sanitizedUris.isEmpty()) {
+ request.onDismiss()
+ } else {
+ sanitizedUris.map {
+ enqueueForCleanup(container.context, it)
+ }
+ request.onMultipleFilesSelected(container.context, sanitizedUris)
+ }
+ }
+ } else {
+ val uri = intent?.data ?: captureUri
+ uri?.let {
+ // We want to verify that we are not exposing any private data
+ if (!it.isUnderPrivateAppDirectory(container.context)) {
+ enqueueForCleanup(container.context, it)
+ request.onSingleFileSelected(container.context, it)
+ } else {
+ request.onDismiss()
+ }
+ } ?: request.onDismiss()
+ }
+
+ captureUri = null
+ }
+
+ private fun saveCaptureUriIfPresent(intent: Intent) =
+ intent.getParcelableExtraCompat(EXTRA_OUTPUT, Uri::class.java)?.let { captureUri = it }
+
+ @VisibleForTesting
+ fun askAndroidPermissionsForRequest(permissions: Set<String>, request: File) {
+ currentRequest = request
+ onNeedToRequestPermissions(permissions.toTypedArray())
+ }
+
+ private fun enqueueForCleanup(context: Context, uri: Uri) {
+ val contentResolver = context.contentResolver
+ val fileName = uri.getFileName(contentResolver)
+ fileUploadsDirCleaner.enqueueForCleanup(fileName)
+ }
+
+ companion object {
+ const val FILE_PICKER_ACTIVITY_REQUEST_CODE = 7113
+ }
+}
+
+internal fun Array<Uri>.removeUrisUnderPrivateAppDir(context: Context): Array<Uri> {
+ return this.filter { !it.isUnderPrivateAppDirectory(context) }.toTypedArray()
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FileUploadsDirCleaner.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FileUploadsDirCleaner.kt
new file mode 100644
index 0000000000..5a87b6ab00
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FileUploadsDirCleaner.kt
@@ -0,0 +1,86 @@
+/* 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.prompts.file
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.engine.prompt.PromptRequest.File.Companion.DEFAULT_UPLOADS_DIR_NAME
+import mozilla.components.support.base.log.logger.Logger
+import java.io.File
+import java.io.IOException
+
+/**
+ * A storage implementation for organizing temporal uploads metadata to be clean up.
+ */
+@OptIn(DelicateCoroutinesApi::class)
+class FileUploadsDirCleaner(
+ private val scope: CoroutineScope = GlobalScope,
+ private val cacheDirectory: () -> File,
+) {
+ private val logger = Logger("FileUploadsDirCleaner")
+
+ private val cacheDir by lazy { cacheDirectory() }
+
+ @VisibleForTesting
+ internal var fileNamesToBeDeleted: List<String> = emptyList()
+
+ @VisibleForTesting
+ internal var dispatcher: CoroutineDispatcher = IO
+
+ /**
+ * Enqueue the [fileName] for future clean up.
+ */
+ internal fun enqueueForCleanup(fileName: String) {
+ fileNamesToBeDeleted += (fileName)
+ logger.info("File $fileName added to the upload cleaning queue.")
+ }
+
+ /**
+ * Remove all the temporary file uploads.
+ */
+ internal fun cleanRecentUploads() {
+ scope.launch(dispatcher) {
+ val cacheUploadDirectory = File(getCacheDir(), DEFAULT_UPLOADS_DIR_NAME)
+ fileNamesToBeDeleted = fileNamesToBeDeleted.filter { fileName ->
+ try {
+ File(cacheUploadDirectory, fileName).delete()
+ logger.info("Temporal file $fileName deleted")
+ false
+ } catch (e: IOException) {
+ logger.error("Unable to delete the temporal file $fileName", e)
+ true
+ }
+ }
+ }
+ }
+
+ /**
+ * Remove the file uploads directory if exists.
+ */
+ suspend fun cleanUploadsDirectory() {
+ withContext(dispatcher) {
+ val cacheUploadDirectory = File(getCacheDir(), DEFAULT_UPLOADS_DIR_NAME)
+ if (cacheUploadDirectory.exists()) {
+ // To not collide with users uploading while, we are cleaning
+ // previous files, lets rename the directory.
+ val cacheUploadDirectoryToDelete = File(getCacheDir(), "/uploads_to_be_deleted")
+
+ cacheUploadDirectory.renameTo(cacheUploadDirectoryToDelete)
+ cacheUploadDirectoryToDelete.deleteRecursively()
+ logger.info("Removed the temporal files under /uploads")
+ }
+ }
+ }
+
+ private suspend fun getCacheDir(): File = withContext(dispatcher) {
+ cacheDir
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerMiddleware.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerMiddleware.kt
new file mode 100644
index 0000000000..41fb56ea59
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerMiddleware.kt
@@ -0,0 +1,40 @@
+/* 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.prompts.file
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] that observe when a user navigates away from a site and clean up,
+ * temporary file uploads.
+ */
+class FileUploadsDirCleanerMiddleware(
+ private val fileUploadsDirCleaner: FileUploadsDirCleaner,
+) : Middleware<BrowserState, BrowserAction> {
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is TabListAction.SelectTabAction,
+ is ContentAction.UpdateUrlAction,
+ is ContentAction.UpdateLoadRequestAction,
+ -> {
+ fileUploadsDirCleaner.cleanRecentUploads()
+ }
+ else -> {
+ // no-op
+ }
+ }
+ next(action)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/MimeType.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/MimeType.kt
new file mode 100644
index 0000000000..4f40081ebb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/file/MimeType.kt
@@ -0,0 +1,187 @@
+/* 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.prompts.file
+
+import android.Manifest.permission.CAMERA
+import android.Manifest.permission.READ_EXTERNAL_STORAGE
+import android.Manifest.permission.READ_MEDIA_AUDIO
+import android.Manifest.permission.READ_MEDIA_IMAGES
+import android.Manifest.permission.READ_MEDIA_VIDEO
+import android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
+import android.Manifest.permission.RECORD_AUDIO
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_GET_CONTENT
+import android.content.Intent.CATEGORY_OPENABLE
+import android.content.Intent.EXTRA_ALLOW_MULTIPLE
+import android.content.Intent.EXTRA_LOCAL_ONLY
+import android.content.Intent.EXTRA_MIME_TYPES
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore.ACTION_IMAGE_CAPTURE
+import android.provider.MediaStore.ACTION_VIDEO_CAPTURE
+import android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION
+import android.provider.MediaStore.EXTRA_OUTPUT
+import android.webkit.MimeTypeMap
+import androidx.core.content.FileProvider.getUriForFile
+import mozilla.components.concept.engine.prompt.PromptRequest.File
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale.US
+
+internal sealed class MimeType(
+ private val type: String,
+ val permission: List<String>,
+) {
+
+ data class Image(
+ private val getUri: (Context, String, java.io.File) -> Uri = { context, authority, file ->
+ getUriForFile(context, authority, file)
+ },
+ ) : MimeType(
+ "image/",
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ listOf(CAMERA, READ_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ listOf(CAMERA, READ_MEDIA_IMAGES)
+ } else {
+ listOf(CAMERA)
+ },
+ ) {
+ /**
+ * Build an image capture intent using the application FileProvider.
+ * A FileProvider must be defined in your AndroidManifest.xml, see
+ * https://developer.android.com/training/camera/photobasics#TaskPath
+ */
+ override fun buildIntent(context: Context, request: File): Intent? {
+ val intent = Intent(ACTION_IMAGE_CAPTURE).withDeviceSupport(context) ?: return null
+
+ val photoFile = try {
+ val filename = SimpleDateFormat("yyyy-MM-ddHH.mm.ss", US).format(Date())
+ java.io.File.createTempFile(filename, ".jpg", context.cacheDir)
+ } catch (e: IOException) {
+ return null
+ }
+
+ val photoUri = getUri(context, "${context.packageName}.feature.prompts.fileprovider", photoFile)
+
+ return intent.apply { putExtra(EXTRA_OUTPUT, photoUri) }.addCaptureHint(request.captureMode)
+ }
+ }
+
+ object Video : MimeType(
+ "video/",
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ listOf(CAMERA, READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ listOf(CAMERA, READ_MEDIA_VIDEO)
+ } else {
+ listOf(CAMERA)
+ },
+ ) {
+ override fun buildIntent(context: Context, request: File) =
+ Intent(ACTION_VIDEO_CAPTURE).withDeviceSupport(context)?.addCaptureHint(request.captureMode)
+ }
+
+ object Audio : MimeType(
+ "audio/",
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ listOf(RECORD_AUDIO, READ_MEDIA_AUDIO)
+ } else {
+ listOf(RECORD_AUDIO)
+ },
+ ) {
+ override fun buildIntent(context: Context, request: File) =
+ Intent(RECORD_SOUND_ACTION).withDeviceSupport(context)
+ }
+
+ object Wildcard : MimeType(
+ "*/",
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ listOf(READ_MEDIA_IMAGES, READ_MEDIA_AUDIO, READ_MEDIA_VIDEO)
+ } else {
+ listOf(READ_EXTERNAL_STORAGE)
+ },
+ ) {
+ private val mimeTypeMap = MimeTypeMap.getSingleton()
+
+ override fun matches(mimeTypes: Array<out String>) = true
+
+ override fun shouldCapture(mimeTypes: Array<out String>, capture: File.FacingMode) = false
+
+ override fun buildIntent(context: Context, request: File) =
+ Intent(ACTION_GET_CONTENT).apply {
+ type = "*/*"
+ addCategory(CATEGORY_OPENABLE)
+ putExtra(EXTRA_LOCAL_ONLY, true)
+ if (request.mimeTypes.isNotEmpty()) {
+ val types = request.mimeTypes
+ .map {
+ if (it.contains("/")) {
+ it
+ } else {
+ mimeTypeMap.getMimeTypeFromExtension(it) ?: "*/*"
+ }
+ }
+ .toTypedArray()
+ putExtra(EXTRA_MIME_TYPES, types)
+ }
+ putExtra(EXTRA_ALLOW_MULTIPLE, request.isMultipleFilesSelection)
+ }
+ }
+
+ /**
+ * True if any of the given mime values match this type. If no values are specified, then
+ * there will not be a match.
+ */
+ open fun matches(mimeTypes: Array<out String>) =
+ mimeTypes.isNotEmpty() && mimeTypes.any { it.startsWith(type) }
+
+ open fun shouldCapture(mimeTypes: Array<out String>, capture: File.FacingMode) =
+ capture != File.FacingMode.NONE &&
+ mimeTypes.isNotEmpty() &&
+ mimeTypes.all { it.startsWith(type) }
+
+ abstract fun buildIntent(context: Context, request: File): Intent?
+
+ companion object {
+ /**
+ * List of all MimeTypes that can be iterated
+ */
+ fun values() = listOf(Image(), Video, Audio, Wildcard)
+
+ const val CAMERA_FACING = "android.intent.extras.CAMERA_FACING"
+ const val LENS_FACING_FRONT = "android.intent.extras.LENS_FACING_FRONT"
+ const val USE_FRONT_CAMERA = "android.intent.extra.USE_FRONT_CAMERA"
+ const val LENS_FACING_BACK = "android.intent.extras.LENS_FACING_BACK"
+ const val USE_BACK_CAMERA = "android.intent.extra.USE_BACK_CAMERA"
+ }
+}
+
+/**
+ * Return the intent only if its type has any corresponding apps on the device.
+ */
+@SuppressLint("QueryPermissionsNeeded") // We expect our browsers to have the QUERY_ALL_PACKAGES permission
+private fun Intent.withDeviceSupport(context: Context) =
+ if (resolveActivity(context.packageManager) != null) this else null
+
+/**
+ * Hacky request for specific camera orientation
+ * https://stackoverflow.com/questions/43841738
+ */
+private fun Intent.addCaptureHint(capture: File.FacingMode): Intent? {
+ if (capture == File.FacingMode.FRONT_CAMERA) {
+ putExtra(MimeType.CAMERA_FACING, 1)
+ putExtra(MimeType.LENS_FACING_FRONT, 1)
+ putExtra(MimeType.USE_FRONT_CAMERA, true)
+ } else if (capture == File.FacingMode.BACK_CAMERA) {
+ putExtra(MimeType.CAMERA_FACING, 0)
+ putExtra(MimeType.LENS_FACING_BACK, 1)
+ putExtra(MimeType.USE_BACK_CAMERA, true)
+ }
+ return this
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/DialogColors.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/DialogColors.kt
new file mode 100644
index 0000000000..246d8b2cb0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/DialogColors.kt
@@ -0,0 +1,57 @@
+/* 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.prompts.identitycredential
+
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Represents the colors used by the dialogs.
+ */
+data class DialogColors(
+ val title: Color,
+ val description: Color,
+) {
+
+ companion object {
+
+ /**
+ * Creates an [DialogColors] that represents the default colors used in an
+ * IdentityCredential dialog.
+ *
+ * @param title The text color for the title of a suggestion.
+ * @param description The text color for the description of a suggestion.
+ */
+ @Composable
+ fun default(
+ title: Color = MaterialTheme.colors.onBackground,
+ description: Color = MaterialTheme.colors.onBackground.copy(
+ alpha = ContentAlpha.medium,
+ ),
+ ) = DialogColors(
+ title,
+ description,
+ )
+
+ /**
+ * Creates a provider that provides the default [DialogColors]
+ */
+ fun defaultProvider() = DialogColorsProvider { default() }
+ }
+}
+
+/**
+ * An [DialogColorsProvider] implementation can provide an [DialogColors]
+ */
+fun interface DialogColorsProvider {
+
+ /**
+ * Provides [DialogColors]
+ */
+ @Composable
+ fun provideColors(): DialogColors
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/IdentityCredentialItem.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/IdentityCredentialItem.kt
new file mode 100644
index 0000000000..e89ce54259
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/IdentityCredentialItem.kt
@@ -0,0 +1,106 @@
+/* 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.prompts.identitycredential
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import mozilla.components.feature.prompts.identitycredential.previews.DialogPreviewMaterialTheme
+import mozilla.components.feature.prompts.identitycredential.previews.LightDarkPreview
+
+/**
+ * List item used to display an IdentityCredential item that supports clicks
+ *
+ * @param title the Title of the item
+ * @param description The Description of the item.
+ * @param modifier The modifier to apply to this layout.
+ * @param onClick Invoked when the item is clicked.
+ * @param beforeItemContent An optional layout to display before the item.
+ *
+ */
+@Composable
+internal fun IdentityCredentialItem(
+ title: String,
+ description: String,
+ modifier: Modifier = Modifier,
+ colors: DialogColors = DialogColors.default(),
+ onClick: () -> Unit,
+ beforeItemContent: (@Composable () -> Unit)? = null,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(horizontal = 16.dp, vertical = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ beforeItemContent?.invoke()
+
+ Column {
+ Text(
+ text = title,
+ style = TextStyle(
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ color = colors.title,
+ letterSpacing = 0.15.sp,
+ ),
+ maxLines = 1,
+ )
+
+ Text(
+ text = description,
+ style = TextStyle(
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ color = colors.description,
+ letterSpacing = 0.25.sp,
+ ),
+ maxLines = 1,
+ )
+ }
+ }
+}
+
+@Composable
+@LightDarkPreview
+private fun ProviderItemPreview() {
+ DialogPreviewMaterialTheme {
+ IdentityCredentialItem(
+ modifier = Modifier.background(MaterialTheme.colors.background),
+ title = "Title",
+ description = "Description",
+ onClick = {},
+ )
+ }
+}
+
+@Composable
+@Preview(name = "Provider with a start-spacer")
+private fun ProviderItemPreviewWithSpacer() {
+ IdentityCredentialItem(
+ modifier = Modifier.background(Color.White),
+ title = "Title",
+ description = "Description",
+ onClick = {},
+ ) {
+ Spacer(modifier = Modifier.size(24.dp))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/PrivacyPolicyDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/PrivacyPolicyDialogFragment.kt
new file mode 100644
index 0000000000..60db3c282d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/PrivacyPolicyDialogFragment.kt
@@ -0,0 +1,130 @@
+/* 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.prompts.identitycredential
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.text.style.URLSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.core.text.HtmlCompat
+import androidx.core.text.getSpans
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.dialog.AbstractPromptTextDialogFragment
+import mozilla.components.feature.prompts.dialog.KEY_MESSAGE
+import mozilla.components.feature.prompts.dialog.KEY_PROMPT_UID
+import mozilla.components.feature.prompts.dialog.KEY_SESSION_ID
+import mozilla.components.feature.prompts.dialog.KEY_SHOULD_DISMISS_ON_LOAD
+import mozilla.components.feature.prompts.dialog.KEY_TITLE
+
+internal const val KEY_ICON = "KEY_ICON"
+
+/**
+ * [ A Federated Credential Management dialog for showing a privacy policy.
+ */
+internal class PrivacyPolicyDialogFragment : AbstractPromptTextDialogFragment() {
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(requireContext())
+ .setTitle(title)
+ .setCancelable(true)
+ .setPositiveButton(R.string.mozac_feature_prompts_identity_credentials_continue) { _, _ ->
+ onConfirmAction(true)
+ }
+ .setNegativeButton(R.string.mozac_feature_prompts_identity_credentials_cancel) { _, _ ->
+ onConfirmAction(false)
+ }
+
+ return setMessage(builder)
+ .create()
+ }
+
+ internal fun setMessage(builder: AlertDialog.Builder): AlertDialog.Builder {
+ val inflater = LayoutInflater.from(requireContext())
+ val view = inflater.inflate(R.layout.mozac_feature_prompt_simple_text, null)
+ val textView = view.findViewById<TextView>(R.id.labelView)
+ val text = HtmlCompat.fromHtml(message, HtmlCompat.FROM_HTML_MODE_COMPACT)
+
+ val spannableStringBuilder = SpannableStringBuilder(text)
+ spannableStringBuilder.getSpans<URLSpan>().forEach { link ->
+ addActionToLinks(spannableStringBuilder, link)
+ }
+ textView.text = spannableStringBuilder
+ textView.movementMethod = LinkMovementMethod.getInstance()
+
+ builder.setView(view)
+
+ return builder
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+
+ private fun addActionToLinks(
+ spannableStringBuilder: SpannableStringBuilder,
+ link: URLSpan,
+ ) {
+ val start = spannableStringBuilder.getSpanStart(link)
+ val end = spannableStringBuilder.getSpanEnd(link)
+ val flags = spannableStringBuilder.getSpanFlags(link)
+ val clickable: ClickableSpan = object : ClickableSpan() {
+ override fun onClick(view: View) {
+ view.setOnClickListener {
+ dismiss()
+ feature?.onOpenLink(link.url)
+ }
+ }
+ }
+ spannableStringBuilder.setSpan(clickable, start, end, flags)
+ spannableStringBuilder.removeSpan(link)
+ }
+
+ private fun onConfirmAction(confirmed: Boolean) {
+ feature?.onConfirm(sessionId, promptRequestUID, confirmed)
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [PrivacyPolicyDialogFragment]
+ * @param sessionId to create the dialog.
+ * @param promptRequestUID identifier of the [PromptRequest] for which this dialog is shown.
+ * @param shouldDismissOnLoad whether or not the dialog should automatically be dismissed
+ * when a new page is loaded.
+ * @param title the title of the dialog.
+ * @param message the message of the dialog.
+ * @param icon an icon of the provider.
+ */
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ shouldDismissOnLoad: Boolean,
+ title: String,
+ message: String,
+ icon: String?,
+ ): PrivacyPolicyDialogFragment {
+ val fragment = PrivacyPolicyDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putString(KEY_TITLE, title)
+ putString(KEY_MESSAGE, message)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putString(KEY_ICON, icon)
+ }
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectAccountDialog.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectAccountDialog.kt
new file mode 100644
index 0000000000..2054053b49
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectAccountDialog.kt
@@ -0,0 +1,172 @@
+/* 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.prompts.identitycredential
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import mozilla.components.concept.identitycredential.Account
+import mozilla.components.concept.identitycredential.Provider
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.identitycredential.previews.DialogPreviewMaterialTheme
+import mozilla.components.feature.prompts.identitycredential.previews.LightDarkPreview
+import mozilla.components.support.ktx.kotlin.base64ToBitmap
+
+/**
+ * A Federated Credential Management dialog for selecting an account.
+ *
+ * @param provider The [Provider] on which the user is logging in.
+ * @param colors The colors of the dialog.
+ * @param accounts The list of available accounts for this provider.
+ * @param modifier [Modifier] to be applied to the layout.
+ * @param onAccountClick Invoked when the user clicks on an item.
+ */
+@Composable
+fun SelectAccountDialog(
+ provider: Provider,
+ accounts: List<Account>,
+ modifier: Modifier = Modifier,
+ colors: DialogColors = DialogColors.default(),
+ onAccountClick: (Account) -> Unit,
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(16.dp),
+ ) {
+ provider.icon?.base64ToBitmap()?.asImageBitmap()?.let {
+ Image(
+ bitmap = it,
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier
+ .size(16.dp),
+ )
+
+ Spacer(Modifier.width(4.dp))
+ }
+
+ Text(
+ text = stringResource(
+ id = R.string.mozac_feature_prompts_identity_credentials_choose_account_for_provider,
+ provider.name,
+ ),
+ style = TextStyle(
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ color = colors.title,
+ letterSpacing = 0.15.sp,
+ ),
+ )
+ }
+
+ accounts.forEach { account ->
+ AccountItem(account = account, colors = colors, onClick = onAccountClick)
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+}
+
+@Composable
+private fun AccountItem(
+ account: Account,
+ modifier: Modifier = Modifier,
+ colors: DialogColors = DialogColors.default(),
+ onClick: (Account) -> Unit,
+) {
+ IdentityCredentialItem(
+ title = account.name,
+ description = account.email,
+ colors = colors,
+ modifier = modifier,
+ onClick = { onClick(account) },
+ ) {
+ account.icon?.base64ToBitmap()?.asImageBitmap()?.let { bitmap ->
+ Image(
+ bitmap = bitmap,
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .size(32.dp),
+ )
+ } ?: Spacer(
+ Modifier
+ .padding(horizontal = 16.dp)
+ .width(32.dp),
+ )
+ }
+}
+
+@Composable
+@Preview(name = "Provider with no favicon")
+private fun AccountItemPreview() {
+ AccountItem(
+ modifier = Modifier.background(Color.White),
+ account = Account(
+ 0,
+ "user@mozilla.com",
+ "User",
+ USER_PICTURE,
+ ),
+ onClick = {},
+ )
+}
+
+@Composable
+@LightDarkPreview
+private fun SelectAccountDialogPreview() {
+ DialogPreviewMaterialTheme {
+ SelectAccountDialog(
+ provider = Provider(0, GOOGLE_FAVICON, "Google", "google.com"),
+ accounts = listOf(
+ Account(
+ 0,
+ "user@mozilla.com",
+ "User",
+ USER_PICTURE,
+ ),
+ Account(
+ 1,
+ "user2@mozilla.com",
+ "Google",
+ null,
+ ),
+ ),
+ modifier = Modifier.background(Color.White),
+ onAccountClick = { },
+ )
+ }
+}
+
+@Suppress("MaxLineLength")
+private const val GOOGLE_FAVICON =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAARCAYAAADUryzEAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAH+SURBVHgBhVO/b9NQEL5z7Bi1lWJUCYRYHBaWDh6SKhspAYnRoVO3SqxIbiUGEEIRQ1kbxB9AM7VISJiNoSUeI6DCYmSpt1ZUak2U0GLXuZ7dOnUsp73l+e593/343hkhZYePKqp/EhhAoLOrRkEEmwgd/mrd3PpmJvGYdPbvl1cJYQkuMQI085KwfP1Lxwl9Mb74Uyu/J4BFuMIEoKp/HCixHyXYf1BqEF2QuS13gPQ2P1OyQt//ta0CYgOBFApg7ob13R5ij9rX1P67uzuDv/k45ASBMHfLOmsxabvVipqOo78pNZls/Nu8Df7vAgRBrphFHmfhCPeEggdT8zvg2dNrk8/2Rsi1N12dx1OyyIjghgnUOCBrB5/TIAJhNYkZuSNwBT4vsiNlVrrEFJHf3UE6q7DtTfO5N4Jg5W3u1Um0pPBza+eeE4nIH8aHozvQ7M+4J3JQtOumO65kbaU33BfWwOK9IPN5dzYkRy1JntAYR3640tOSy8YatKJVLm3Mt/moJrAWEL7+sfDRCp3qJ13peYIxsft0SezPxjo5X19OFaMEGgOk/7mflK12OM5QXPlAB/mwDjjA+tarSXP4M1XWdXVCLrS7Xi8ryUhCsV9a7jx5sRbpkL4trz9eJBQMnlBLExncypHU7CxsOHEQx5WJ5j4WoyQiiE6SlLRTu5e/Z+DrlXsAAAAASUVORK5CYII="
+
+@Suppress("MaxLineLength")
+private const val USER_PICTURE =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAtfSURBVHgBnVdbbFv1Gf/Z52If28c+dnyLndgnl15D2iRNadrSNoUxAkgjTCBg08T6hrYHyrQXNE2UbdK0p5WnsT3Rt0p74DJQkabSZlxWCr3RdM2N1Ekcx3cf34/tc4732WiVmDpAi2Qlsezzff/v+93+JnzHn9nZWYnTq7O6YRxrqo0xk4mRg8F+yTDaaDYbisMhxSx2x3WfLzDXQuOtU6dOKd/luaZv+8DM9JTMW/gXXc6en8LMSI1mHWq9DrQN+Hx9YFm20wDMZgYw84hGBmG022ibzW9YbeZXX3755dj/1cDs9LRUN+qvaK3WSYblEQiGIPb4UFNrKBZyYM1m+AMRWK02qNUyqoUkBI6FzR2EwQjwB4NgWAuaevu0JoivnnrpxD0nYr538SlZNzWv8Qx7stNjm95rtFpgrQ44Pb2wii7oZhNcHh9EpwtGQ4GoZ6CXNwFlDVbeDLWpo6RWYZiNk1yrfu3U7/8k36sW899v/OxHT495fcFzkscrO0QnTVqnT7GwO50wTG3YnG7wdGrCALzeIDjOAqa8ilx6i1ahQ6moiAYlpDQWhpWBYLXAzFkk3dBmD91/eO7DDz9I/s8GZqbGZKvDeY7nGNnh8cLEcmgaBn2KAW+hBzEcbA4PgoEQ7DYRvGCnJnxobFzCRjwLtWGg0WzC0Ktw9IRRbrSgai3ksxlwbUj5XHbG5w2+vbKycHcd7H/+mJ4ek9SqcWFifK/cpkMvxTOwOkSIHgb52CLsNPZQWAZnFZGi025trEIe3oUjE8NYOVeCUmyCZXR0+q1Wq9gXFmGRp2CihxlNDRwdIpNKyt6A78IzzzwxfuLEV5i42wCqxisWwSZfuXoF49u9eGhPCDfWCHAtwOHyYWjbKNzuHsTia9hMrMMheVEvl6DGL0OnUzPmNvRWZ1gManU6gaFBDofgcDhQq9VgF2wYHh5CrVKSTTBeoYov3V3B1NhO2WQ2neU5Di3DBJvkRyaTwfcO7UR/wNM9XZBOrxTLuLO2BDeNXXK5EXTbYMlcRTpfpoJm1MoNWEkX7E4bJg5M4p0338TNG1dxZ2URiXiMfi/gy6UF5ArKlFsSz8zPzyvdBsLR6B9FURrroJ2hPavgqYAL6VQSOyIeHD2wC5OyFVG/gEy+BJNWR1Qy40CfgWuXPkYLApIZ0gdFJSoKNB0HgqEeCK4gPpi7iEq9SnQVsHTrChbmr2Bp8RbSWxumza30+8w08V3Xcdbl9qFKAlMoKSgoReJ8LwJ0Qq1agFsU0COaEe4N4Mj+/ZgeC2Hfdg8qW/OY+/gaYUXCnfUCWrTrjjKabAxabRajI8NYIHB2GNRPApXL5dDUDNRpZevJzM4yTH9mdV2f5fgOVfguvZLpTdRVAtQtKyZGZuCylBHqj0Bga2hXU2jVV9FRw1w6geRWGuFIH67djqParMDrFlBp6JjaF0CiUMJQr9jVBCcBuEp4qdVIsOhVVMoolUsSB32WtYnSMaI3iQ3tzuUhcfGj1+bs0q5S03Hg0afgMNN4N28CvB1NmlA2X8BmqoxclYdTcqPNV8HyTdhEBv1hK8qpLzE2KGMg2ofJPbtRUFkUiIolRUGrpRNd65g8PIFAKHiMfeDYzNjy8jzamgYrIdUpEvWcEiqVCoxWg0YGlLKrKKXpy80E0aaOIjVWbwCC04eA6McPiB0bGxu48sUKgd9As1LG9sB9xH0Vh/dsx7ufLGJ1dQEKTUGnOirpg6OjIR7PGOshxQOBoF6t0O7z1GELxVIZ5WIaIwNTqObTSN34HAyBq5hcIHES0DIJqGom0gQelVIBZpMJQ+Ee0MSJ6ylEfHQIdwCVxE2EXINIx26iWiyQqhpdBdV0DXMffATh0mWZXVmal5Jb67QDeqDF2pWEdCZB/DRgI5VYu/we4mtxOB08NHK8ldvLyClV+L1eEikvzBZijULqqhFWiLIj2/sw2OclCzHDYu9BKrZKwpWEw07PJnxUyjVomk56UCMdqUisXaCh1io0BB0i0wOfP0hfJi1gqNtcEklzGW6ngFI+gfNXV/HpigKWHM+9uowBXxxyrws8b4KVtHYjHoeFZDjsD8A7OAzOvxvZW/+g6dnIJ7SOpXSx1iYr76yqTWJlvm/bbqUvHCWV89ODBKptQl84QibD4pObC9g+so8ewON2LIv5LRqP1YWwy4L+HpF2rSC2vo5cJglVraNJrxYdJJ+Oo6VsUkEGh48ch42jE9dJrKi4QWsQSeL7w30YHxtXWLVciR2ePDR24fJH3WChtZoEQg9UwYWnH5mCvHs/gbFJjV3HzOQwMUNFu1FEs14CRzqRLRZpYla4nA5IPcNgmgVkie+h4jptgSgYlHF4YhTnb6ySXrjh9bUQ9Pq7IPRJnhjzwNEHD+4e2jam6QZWCKktKuYk/f7xE4/gwPgY1MImSpuLqFSb0CnpmIwqMSRLKcEgqvLw+0VEdoyibh9C77ZJcLT7jeWr8NpbCO08AsY9gB2DUSgN8griEEcOayO9iYT8GJZD77Ora3cuSoL9+am9E1iiBjL5DJ6aOYJtfh5K4l+o0P+KboFKWs/RnvcceZhKU9hQcsSALLjGJoK7D8IeHkfAG0DquooW7bdAvtDZu7VRgoOoHSB1zDts2LuTGg30wE/pSlObc8zIyM7YnbWtFyLhPuuAvA11SwBOYmZZyYCRImiLvVhYJaWrlLBj+ocwu0NdtjjDQ3CRXPOuQHevZvL9RrlAehFHYWuRdKUBv4uHKxilqGhFYW0BZ989T4ewkXNyuL2yCs7hPtHNhA89+OjpaLj/RZ3Q+bdz76Bf3olf/vq3IMcihyuS3daQWJknbDioWIPyIIki0S5J1iwQi+yip5sDGAqoxcQK2tkvoNfzCLhE3H//fgw9/iuiXAEv/PwX+Oh2gr5jQTK5+YZSUk903XBHdHjBYM0n+/oHEd+IYTASAjEPhloEQwU5E/k7acDHc3O49vln5Acl2GwcWHpfsNm7GeArerW7oZUnBWyS4Xg8bvT4I3CFd8PmCeHw3p04MOCGxDRwaXHtSVXVvrLjldiKsue+UbemN6dG9+7D+p1lpMm/K9kEaiQy5cwGFuevIyjxOHRwEgMDMiyUHSw8S/RqIpVMUgxnUFV1AirgpDim1XLknkECNWUxT5BknidtaZAy6tg3Mvjab/7y3tmvZUKf33uJxOhZXdek0fFJNIopCIzW3WUniDhFQq4c7Y46m02hohTI2VTyemogW0AimUeR1A2MBSwF1VAoAq3DfVaCpV0j2jEwtRSoZSVmZqrP/eHMRbVT92v3gunpGRnt5oVIdFiW5QHafQWRvih6JB/OvfdXVGis8a0kzESjo0cPIbX4z65rGkTh9fVNckSB3FFCb9AHod3C/bsG0CtZ4bAYBM5VygXumDscPd57/KXYPVNxjFbhc3vnElvxmeWFm1JHWjWDpVMSpRpqd8ciaUQmm8Xg2DHy+DxRqQKny0HaUAf1QVmiTvgQEB6eQKR/gDBA4+fbKNWNmJKvPTn63O8W8E33gkQykdw1IL9NCWVWtNukdDqFpeUFDG0fIRY48dj3H6fg4YGJlNJJ2WGLQGul7G+zW5BO5ij1ZAmUpBlWJzkixXeOTM3KxBgrc3z/T75e/J4NdCcRjytev/dMW2cEXrBMuaig1U45r7cPbl8veoJ9ECUP3Y5YLN++TVige4DNSoCkSFcg2zWx2P/Aw90swfHsaz67+Ny2h08k71XrWy+nszPPyg6v8xTHm563251d2nUuombBiYA8jE///ibUGtGS10gfdHz2yWViB6ccnH70jGESTr/++unYNz3/Wxu42whdz3NpZZbuiNNk/nt9kV3y1EOPSXfmP0OjXlUa5XTM7XZcv3Tx/Fy5xbwVi8W+0/X83zizJdeTv85mAAAAAElFTkSuQmCC"
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectAccountDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectAccountDialogFragment.kt
new file mode 100644
index 0000000000..24395337f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectAccountDialogFragment.kt
@@ -0,0 +1,118 @@
+/* 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.prompts.identitycredential
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.ui.platform.ComposeView
+import mozilla.components.concept.identitycredential.Account
+import mozilla.components.concept.identitycredential.Provider
+import mozilla.components.feature.prompts.dialog.KEY_PROMPT_UID
+import mozilla.components.feature.prompts.dialog.KEY_SESSION_ID
+import mozilla.components.feature.prompts.dialog.KEY_SHOULD_DISMISS_ON_LOAD
+import mozilla.components.feature.prompts.dialog.PromptDialogFragment
+import mozilla.components.support.utils.ext.getParcelableArrayListCompat
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+private const val KEY_ACCOUNTS = "KEY_ACCOUNTS"
+private const val KEY_PROVIDER = "KEY_PROVIDER"
+
+/**
+ * A Federated Credential Management dialog for selecting an account.
+ */
+internal class SelectAccountDialogFragment : PromptDialogFragment() {
+
+ internal val accounts: List<Account> by lazy {
+ safeArguments.getParcelableArrayListCompat(KEY_ACCOUNTS, Account::class.java) ?: emptyList()
+ }
+
+ private var colorsProvider: DialogColorsProvider = DialogColors.defaultProvider()
+
+ internal val provider: Provider by lazy {
+ requireNotNull(
+ safeArguments.getParcelableCompat(
+ KEY_PROVIDER,
+ Provider::class.java,
+ ),
+ )
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
+ AlertDialog.Builder(requireContext())
+ .setCancelable(true)
+ .setView(createDialogContentView())
+ .create()
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+
+ @SuppressLint("InflateParams")
+ internal fun createDialogContentView(): View {
+ return ComposeView(requireContext()).apply {
+ setContent {
+ val colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
+ MaterialTheme(colors) {
+ SelectAccountDialog(
+ provider = provider,
+ accounts = accounts,
+ colors = colorsProvider.provideColors(),
+ onAccountClick = ::onAccountChange,
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when a new [Provider] is selected by the user.
+ */
+ @VisibleForTesting
+ internal fun onAccountChange(account: Account) {
+ feature?.onConfirm(sessionId, promptRequestUID, account)
+ dismiss()
+ }
+
+ companion object {
+
+ /**
+ * A builder method for creating a [SelectAccountDialogFragment]
+ * @param sessionId The id of the session for which this dialog will be created.
+ * @param promptRequestUID Identifier of the [PromptRequest] for which this dialog is shown.
+ * @param accounts The list of available accounts.
+ * @param provider The provider on which the user is logging in.
+ * @param shouldDismissOnLoad Whether or not the dialog should automatically be dismissed
+ * when a new page is loaded.
+ * @param colorsProvider Provides [DialogColors] that define the colors in the Dialog
+ */
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ accounts: List<Account>,
+ provider: Provider,
+ shouldDismissOnLoad: Boolean,
+ colorsProvider: DialogColorsProvider,
+ ) = SelectAccountDialogFragment().apply {
+ arguments = (arguments ?: Bundle()).apply {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putParcelableArrayList(KEY_ACCOUNTS, ArrayList(accounts))
+ putParcelable(KEY_PROVIDER, provider)
+ }
+ this.colorsProvider = colorsProvider
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectProviderDialog.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectProviderDialog.kt
new file mode 100644
index 0000000000..ed23b6b9f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectProviderDialog.kt
@@ -0,0 +1,144 @@
+/* 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.prompts.identitycredential
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import mozilla.components.concept.identitycredential.Provider
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.identitycredential.previews.DialogPreviewMaterialTheme
+import mozilla.components.feature.prompts.identitycredential.previews.LightDarkPreview
+import mozilla.components.support.ktx.kotlin.base64ToBitmap
+
+/**
+ * A Federated Credential Management dialog for selecting a provider.
+ * @param providers The list of available providers.
+ * @param colors The colors of the dialog.
+ * @param modifier [Modifier] to be applied to the layout.
+ * @param onProviderClick Called when the user clicks on an item.
+ */
+@Composable
+fun SelectProviderDialog(
+ providers: List<Provider>,
+ modifier: Modifier = Modifier,
+ colors: DialogColors = DialogColors.default(),
+ onProviderClick: (Provider) -> Unit,
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = stringResource(id = R.string.mozac_feature_prompts_identity_credentials_choose_provider),
+ style = TextStyle(
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ color = colors.title,
+ letterSpacing = 0.15.sp,
+ fontWeight = FontWeight.Bold,
+ ),
+ modifier = Modifier.padding(16.dp),
+ )
+
+ providers.forEach { provider ->
+ ProviderItem(provider = provider, onClick = onProviderClick, colors = colors)
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+}
+
+@Composable
+private fun ProviderItem(
+ provider: Provider,
+ modifier: Modifier = Modifier,
+ colors: DialogColors = DialogColors.default(),
+ onClick: (Provider) -> Unit,
+) {
+ IdentityCredentialItem(
+ title = provider.name,
+ description = provider.domain,
+ modifier = modifier,
+ colors = colors,
+ onClick = { onClick(provider) },
+ ) {
+ provider.icon?.base64ToBitmap()?.asImageBitmap()?.let { bitmap ->
+ Image(
+ bitmap = bitmap,
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .size(24.dp),
+ )
+ } ?: Spacer(
+ Modifier
+ .padding(horizontal = 16.dp)
+ .width(24.dp),
+ )
+ }
+}
+
+@Composable
+@Preview(name = "Provider with no favicon")
+private fun ProviderItemPreview() {
+ ProviderItem(
+ modifier = Modifier.background(Color.White),
+ provider = Provider(
+ 0,
+ null,
+ "Title",
+ "Description",
+ ),
+ onClick = {},
+ )
+}
+
+@Composable
+@LightDarkPreview
+private fun SelectProviderDialogPreview() {
+ DialogPreviewMaterialTheme {
+ SelectProviderDialog(
+ providers = listOf(
+ Provider(
+ 0,
+ null,
+ "Title",
+ "Description",
+ ),
+ Provider(
+ 0,
+ GOOGLE_FAVICON,
+ "Google",
+ "google.com",
+ ),
+ ),
+ modifier = Modifier.background(MaterialTheme.colors.background),
+ ) { }
+ }
+}
+
+@Suppress("MaxLineLength")
+private const val GOOGLE_FAVICON =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAARCAYAAADUryzEAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAH+SURBVHgBhVO/b9NQEL5z7Bi1lWJUCYRYHBaWDh6SKhspAYnRoVO3SqxIbiUGEEIRQ1kbxB9AM7VISJiNoSUeI6DCYmSpt1ZUak2U0GLXuZ7dOnUsp73l+e593/343hkhZYePKqp/EhhAoLOrRkEEmwgd/mrd3PpmJvGYdPbvl1cJYQkuMQI085KwfP1Lxwl9Mb74Uyu/J4BFuMIEoKp/HCixHyXYf1BqEF2QuS13gPQ2P1OyQt//ta0CYgOBFApg7ob13R5ij9rX1P67uzuDv/k45ASBMHfLOmsxabvVipqOo78pNZls/Nu8Df7vAgRBrphFHmfhCPeEggdT8zvg2dNrk8/2Rsi1N12dx1OyyIjghgnUOCBrB5/TIAJhNYkZuSNwBT4vsiNlVrrEFJHf3UE6q7DtTfO5N4Jg5W3u1Um0pPBza+eeE4nIH8aHozvQ7M+4J3JQtOumO65kbaU33BfWwOK9IPN5dzYkRy1JntAYR3640tOSy8YatKJVLm3Mt/moJrAWEL7+sfDRCp3qJ13peYIxsft0SezPxjo5X19OFaMEGgOk/7mflK12OM5QXPlAB/mwDjjA+tarSXP4M1XWdXVCLrS7Xi8ryUhCsV9a7jx5sRbpkL4trz9eJBQMnlBLExncypHU7CxsOHEQx5WJ5j4WoyQiiE6SlLRTu5e/Z+DrlXsAAAAASUVORK5CYII="
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectProviderDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectProviderDialogFragment.kt
new file mode 100644
index 0000000000..db68fea6fe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/SelectProviderDialogFragment.kt
@@ -0,0 +1,103 @@
+/* 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.prompts.identitycredential
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.ui.platform.ComposeView
+import mozilla.components.concept.identitycredential.Provider
+import mozilla.components.feature.prompts.dialog.KEY_PROMPT_UID
+import mozilla.components.feature.prompts.dialog.KEY_SESSION_ID
+import mozilla.components.feature.prompts.dialog.KEY_SHOULD_DISMISS_ON_LOAD
+import mozilla.components.feature.prompts.dialog.PromptDialogFragment
+import mozilla.components.support.utils.ext.getParcelableArrayListCompat
+
+private const val KEY_PROVIDERS = "KEY_PROVIDERS"
+
+/**
+ * A Federated Credential Management dialog for selecting a provider.
+ */
+internal class SelectProviderDialogFragment : PromptDialogFragment() {
+
+ private val providers: List<Provider> by lazy {
+ safeArguments.getParcelableArrayListCompat(KEY_PROVIDERS, Provider::class.java)
+ ?: emptyList()
+ }
+
+ private var colorsProvider: DialogColorsProvider = DialogColors.defaultProvider()
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
+ AlertDialog.Builder(requireContext())
+ .setCancelable(true)
+ .setView(createDialogContentView())
+ .create()
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ feature?.onCancel(sessionId, promptRequestUID)
+ }
+
+ @SuppressLint("InflateParams")
+ internal fun createDialogContentView(): View {
+ return ComposeView(requireContext()).apply {
+ setContent {
+ val colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
+ MaterialTheme(colors) {
+ SelectProviderDialog(
+ providers = providers,
+ onProviderClick = ::onProviderChange,
+ colors = DialogColors.default(),
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when a new [Provider] is selected by the user.
+ */
+ @VisibleForTesting
+ internal fun onProviderChange(provider: Provider) {
+ feature?.onConfirm(sessionId, promptRequestUID, provider)
+ dismiss()
+ }
+
+ companion object {
+
+ /**
+ * A builder method for creating a [SelectAccountDialogFragment]
+ * @param sessionId The id of the session for which this dialog will be created.
+ * @param promptRequestUID Identifier of the [PromptRequest] for which this dialog is shown.
+ * @param providers The list of available providers.
+ * @param shouldDismissOnLoad Whether or not the dialog should automatically be dismissed
+ * when a new page is loaded.
+ * @param colorsProvider Provides [DialogColors] that define the colors in the Dialog
+ */
+ fun newInstance(
+ sessionId: String,
+ promptRequestUID: String,
+ providers: List<Provider>,
+ shouldDismissOnLoad: Boolean,
+ colorsProvider: DialogColorsProvider,
+ ) = SelectProviderDialogFragment().apply {
+ arguments = (arguments ?: Bundle()).apply {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_PROMPT_UID, promptRequestUID)
+ putBoolean(KEY_SHOULD_DISMISS_ON_LOAD, shouldDismissOnLoad)
+ putParcelableArrayList(KEY_PROVIDERS, ArrayList(providers))
+ }
+ this.colorsProvider = colorsProvider
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/previews/DialogPreviewMaterialTheme.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/previews/DialogPreviewMaterialTheme.kt
new file mode 100644
index 0000000000..afae915e0d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/previews/DialogPreviewMaterialTheme.kt
@@ -0,0 +1,24 @@
+/* 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.prompts.identitycredential.previews
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+import mozilla.components.ui.colors.PhotonColors
+
+@Composable
+internal fun DialogPreviewMaterialTheme(content: @Composable () -> Unit) {
+ val colors = if (!isSystemInDarkTheme()) {
+ lightColors()
+ } else {
+ darkColors(background = PhotonColors.DarkGrey30)
+ }
+ MaterialTheme(colors = colors) {
+ content()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/previews/LightDarkPreview.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/previews/LightDarkPreview.kt
new file mode 100644
index 0000000000..83c80f9448
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/identitycredential/previews/LightDarkPreview.kt
@@ -0,0 +1,16 @@
+/* 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.prompts.identitycredential.previews
+
+import android.content.res.Configuration
+import androidx.compose.ui.tooling.preview.Preview
+
+/**
+ * A wrapper annotation for the two uiMode that are commonly used
+ * in Compose preview functions.
+ */
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
+annotation class LightDarkPreview
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/BasicLoginAdapter.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/BasicLoginAdapter.kt
new file mode 100644
index 0000000000..b765a9352a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/BasicLoginAdapter.kt
@@ -0,0 +1,67 @@
+/* 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.prompts.login
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.prompts.R
+
+private object LoginItemDiffCallback : DiffUtil.ItemCallback<Login>() {
+ override fun areItemsTheSame(oldItem: Login, newItem: Login) =
+ oldItem.guid == newItem.guid
+
+ override fun areContentsTheSame(oldItem: Login, newItem: Login) =
+ oldItem == newItem
+}
+
+/**
+ * RecyclerView adapter for displaying login items.
+ */
+internal class BasicLoginAdapter(
+ private val onLoginSelected: (Login) -> Unit,
+) : ListAdapter<Login, LoginViewHolder>(LoginItemDiffCallback) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LoginViewHolder {
+ val view = LayoutInflater
+ .from(parent.context)
+ .inflate(R.layout.login_selection_list_item, parent, false)
+ return LoginViewHolder(view, onLoginSelected)
+ }
+
+ override fun onBindViewHolder(holder: LoginViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+/**
+ * View holder for a login item.
+ */
+internal class LoginViewHolder(
+ itemView: View,
+ private val onLoginSelected: (Login) -> Unit,
+) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
+ @VisibleForTesting
+ lateinit var login: Login
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ fun bind(login: Login) {
+ this.login = login
+ itemView.findViewById<TextView>(R.id.username)?.text = login.username
+ itemView.findViewById<TextView>(R.id.password)?.text = login.password
+ }
+
+ override fun onClick(v: View?) {
+ onLoginSelected(login)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginDelegate.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginDelegate.kt
new file mode 100644
index 0000000000..80f4d33297
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginDelegate.kt
@@ -0,0 +1,27 @@
+/* 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.prompts.login
+
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+
+/**
+ * Delegate to display the login select prompt and related callbacks
+ */
+interface LoginDelegate {
+ /**
+ * The [SelectablePromptView] used for [LoginPicker] to display a
+ * selectable prompt list of logins.
+ */
+ val loginPickerView: SelectablePromptView<Login>?
+ get() = null
+
+ /**
+ * Callback invoked when a user selects "Manage logins"
+ * from the select login prompt.
+ */
+ val onManageLogins: () -> Unit
+ get() = {}
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginExceptions.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginExceptions.kt
new file mode 100644
index 0000000000..6364f28a99
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginExceptions.kt
@@ -0,0 +1,22 @@
+/* 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.prompts.login
+
+/**
+ * Interface to be implemented by a storage layer to exclude the save logins prompt from showing.
+ */
+interface LoginExceptions {
+ /**
+ * Checks if a specific origin should show a save logins prompt or if it is an exception.
+ * @param origin The origin to search exceptions list for.
+ */
+ fun isLoginExceptionByOrigin(origin: String): Boolean
+
+ /**
+ * Adds a new origin to the exceptions list implementation.
+ * @param origin The origin to add to the list of exceptions.
+ */
+ fun addLoginException(origin: String)
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginPicker.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginPicker.kt
new file mode 100644
index 0000000000..224f4e770e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginPicker.kt
@@ -0,0 +1,80 @@
+/* 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.prompts.login
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.consumePromptFrom
+import mozilla.components.feature.prompts.facts.emitLoginAutofillDismissedFact
+import mozilla.components.feature.prompts.facts.emitLoginAutofillPerformedFact
+import mozilla.components.feature.prompts.facts.emitLoginAutofillShownFact
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * The [LoginPicker] displays a list of possible logins in a [SelectablePromptView] for a site after
+ * receiving a [PromptRequest.SelectLoginPrompt] when a user clicks into a login field and we have
+ * matching logins. It allows the user to select which one of these logins they would like to fill,
+ * or select an option to manage their logins.
+ *
+ * @property store The [BrowserStore] this feature should subscribe to.
+ * @property loginSelectBar The [SelectablePromptView] view into which the select login "prompt" will be inflated.
+ * @property manageLoginsCallback A callback invoked when a user selects "manage logins" from the
+ * select login prompt.
+ * @property sessionId This is the id of the session which requested the prompt.
+ */
+internal class LoginPicker(
+ private val store: BrowserStore,
+ private val loginSelectBar: SelectablePromptView<Login>,
+ private val manageLoginsCallback: () -> Unit = {},
+ private var sessionId: String? = null,
+) : SelectablePromptView.Listener<Login> {
+
+ init {
+ loginSelectBar.listener = this
+ }
+
+ internal fun handleSelectLoginRequest(request: PromptRequest.SelectLoginPrompt) {
+ emitLoginAutofillShownFact()
+ loginSelectBar.showPrompt(request.logins)
+ }
+
+ override fun onOptionSelect(option: Login) {
+ store.consumePromptFrom<PromptRequest.SelectLoginPrompt>(sessionId) {
+ it.onConfirm(option)
+ }
+ emitLoginAutofillPerformedFact()
+ loginSelectBar.hidePrompt()
+ }
+
+ override fun onManageOptions() {
+ manageLoginsCallback.invoke()
+ dismissCurrentLoginSelect()
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ fun dismissCurrentLoginSelect(promptRequest: PromptRequest.SelectLoginPrompt? = null) {
+ try {
+ if (promptRequest != null) {
+ promptRequest.onDismiss()
+ sessionId?.let {
+ store.dispatch(ContentAction.ConsumePromptRequestAction(it, promptRequest))
+ }
+ loginSelectBar.hidePrompt()
+ return
+ }
+
+ store.consumePromptFrom<PromptRequest.SelectLoginPrompt>(sessionId) {
+ it.onDismiss()
+ }
+ } catch (e: RuntimeException) {
+ Logger.error("Can't dismiss this login select prompt", e)
+ }
+ emitLoginAutofillDismissedFact()
+ loginSelectBar.hidePrompt()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginSelectBar.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginSelectBar.kt
new file mode 100644
index 0000000000..1fc79d79e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/LoginSelectBar.kt
@@ -0,0 +1,140 @@
+/* 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.prompts.login
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.withStyledAttributes
+import androidx.core.view.isVisible
+import androidx.core.widget.ImageViewCompat
+import androidx.core.widget.TextViewCompat
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.support.ktx.android.view.hideKeyboard
+
+/**
+ * A customizable multiple login selection bar implementing [SelectablePromptView].
+ */
+class LoginSelectBar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr), SelectablePromptView<Login> {
+
+ var headerTextStyle: Int? = null
+
+ init {
+ context.withStyledAttributes(
+ attrs,
+ R.styleable.LoginSelectBar,
+ defStyleAttr,
+ 0,
+ ) {
+ val textStyle =
+ getResourceId(R.styleable.LoginSelectBar_mozacLoginSelectHeaderTextStyle, 0)
+ if (textStyle > 0) {
+ headerTextStyle = textStyle
+ }
+ }
+ }
+
+ override var listener: SelectablePromptView.Listener<Login>? = null
+
+ override fun showPrompt(options: List<Login>) {
+ if (loginPickerView == null) {
+ loginPickerView =
+ View.inflate(context, R.layout.mozac_feature_login_multiselect_view, this)
+ bindViews()
+ }
+
+ listAdapter.submitList(options)
+ loginPickerView?.isVisible = true
+ }
+
+ override fun hidePrompt() {
+ this.isVisible = false
+ loginsList?.isVisible = false
+ listAdapter.submitList(mutableListOf())
+ manageLoginsButton?.isVisible = false
+ toggleSavedLoginsHeader(shouldExpand = false)
+ }
+
+ override fun asView(): View {
+ return super.asView()
+ }
+
+ private var loginPickerView: View? = null
+ private var loginsList: RecyclerView? = null
+ private var manageLoginsButton: AppCompatTextView? = null
+ private var savedLoginsHeader: AppCompatTextView? = null
+ private var expandArrowHead: AppCompatImageView? = null
+
+ private var listAdapter = BasicLoginAdapter {
+ listener?.onOptionSelect(it)
+ }
+
+ private fun bindViews() {
+ manageLoginsButton = findViewById<AppCompatTextView>(R.id.manage_logins).apply {
+ setOnClickListener {
+ listener?.onManageOptions()
+ }
+ }
+ loginsList = findViewById(R.id.logins_list)
+ savedLoginsHeader = findViewById<AppCompatTextView>(R.id.saved_logins_header).apply {
+ headerTextStyle?.let {
+ TextViewCompat.setTextAppearance(this, it)
+ currentTextColor.let {
+ TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(it))
+ }
+ }
+ setOnClickListener {
+ toggleSavedLoginsHeader(shouldExpand = loginsList?.isVisible != true)
+ }
+ }
+ expandArrowHead =
+ findViewById<AppCompatImageView>(R.id.mozac_feature_login_multiselect_expand).apply {
+ savedLoginsHeader?.currentTextColor?.let {
+ ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(it))
+ }
+ }
+ loginsList?.apply {
+ layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false).also {
+ val dividerItemDecoration = DividerItemDecoration(context, it.orientation)
+ addItemDecoration(dividerItemDecoration)
+ }
+ adapter = listAdapter
+ }
+ }
+
+ private fun toggleSavedLoginsHeader(shouldExpand: Boolean) {
+ if (shouldExpand) {
+ loginsList?.isVisible = true
+ manageLoginsButton?.isVisible = true
+ loginPickerView?.hideKeyboard()
+ expandArrowHead?.rotation = ROTATE_180
+ savedLoginsHeader?.contentDescription =
+ context.getString(R.string.mozac_feature_prompts_collapse_logins_content_description)
+ } else {
+ expandArrowHead?.rotation = 0F
+ loginsList?.isVisible = false
+ manageLoginsButton?.isVisible = false
+ savedLoginsHeader?.contentDescription =
+ context.getString(R.string.mozac_feature_prompts_expand_logins_content_description_2)
+ }
+ }
+
+ companion object {
+ private const val ROTATE_180 = 180F
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/StrongPasswordPromptViewListener.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/StrongPasswordPromptViewListener.kt
new file mode 100644
index 0000000000..69a1abf3a7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/StrongPasswordPromptViewListener.kt
@@ -0,0 +1,92 @@
+/* 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.prompts.login
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.prompts.concept.PasswordPromptView
+import mozilla.components.feature.prompts.consumePromptFrom
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Displays a [PasswordPromptView] for a site after receiving a [PromptRequest.SelectLoginPrompt]
+ * when a user clicks into a login field and we don't have any matching logins. The user can receive
+ * a suggestion for a strong password that can be used for filling in the password field.
+ *
+ * @property browserStore The [BrowserStore] this feature should subscribe to.
+ * @property suggestStrongPasswordBar The view where the suggest strong password "prompt" will be inflated.
+ * @property sessionId This is the id of the session which requested the prompt.
+ */
+internal class StrongPasswordPromptViewListener(
+ private val browserStore: BrowserStore,
+ private val suggestStrongPasswordBar: PasswordPromptView,
+ private var sessionId: String? = null,
+) : PasswordPromptView.Listener {
+
+ init {
+ suggestStrongPasswordBar.listener = this
+ }
+
+ internal fun handleSuggestStrongPasswordRequest(
+ request: PromptRequest.SelectLoginPrompt,
+ currentUrl: String,
+ onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
+ ) {
+ request.generatedPassword?.let {
+ suggestStrongPasswordBar.showPrompt(
+ it,
+ currentUrl,
+ onSaveLoginWithStrongPassword,
+ )
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ fun dismissCurrentSuggestStrongPassword(promptRequest: PromptRequest.SelectLoginPrompt? = null) {
+ try {
+ if (promptRequest != null) {
+ promptRequest.onDismiss()
+ sessionId?.let {
+ browserStore.dispatch(
+ ContentAction.ConsumePromptRequestAction(
+ it,
+ promptRequest,
+ ),
+ )
+ }
+ suggestStrongPasswordBar.hidePrompt()
+ return
+ }
+
+ browserStore.consumePromptFrom<PromptRequest.SelectLoginPrompt>(sessionId) {
+ it.onDismiss()
+ }
+ } catch (e: RuntimeException) {
+ Logger.error("Can't dismiss this prompt", e)
+ }
+ suggestStrongPasswordBar.hidePrompt()
+ }
+
+ override fun onUseGeneratedPassword(
+ generatedPassword: String,
+ url: String,
+ onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
+ ) {
+ browserStore.consumePromptFrom<PromptRequest.SelectLoginPrompt>(sessionId) {
+ // Create complete login entry: https://bugzilla.mozilla.org/show_bug.cgi?id=1869575
+ val createdLoginEntryWithPassword = Login(
+ guid = "",
+ origin = url,
+ username = "",
+ password = generatedPassword,
+ )
+ it.onConfirm(createdLoginEntryWithPassword)
+ }
+ onSaveLoginWithStrongPassword.invoke(url, generatedPassword)
+ suggestStrongPasswordBar.hidePrompt()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordBar.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordBar.kt
new file mode 100644
index 0000000000..d18d027644
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordBar.kt
@@ -0,0 +1,110 @@
+/* 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.prompts.login
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.View
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.withStyledAttributes
+import androidx.core.view.isVisible
+import androidx.core.widget.TextViewCompat
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.PasswordPromptView
+
+/**
+ * A prompt bar implementing [PasswordPromptView] to display the strong generated password.
+ */
+class SuggestStrongPasswordBar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr), PasswordPromptView {
+
+ private var headerTextStyle: Int? = null
+ private var suggestStrongPasswordView: View? = null
+ private var suggestStrongPasswordHeader: AppCompatTextView? = null
+ private var useStrongPasswordTitle: AppCompatTextView? = null
+
+ override var listener: PasswordPromptView.Listener? = null
+
+ init {
+ context.withStyledAttributes(
+ attrs,
+ R.styleable.LoginSelectBar,
+ defStyleAttr,
+ 0,
+ ) {
+ val textStyle =
+ getResourceId(R.styleable.LoginSelectBar_mozacLoginSelectHeaderTextStyle, 0)
+ if (textStyle > 0) {
+ headerTextStyle = textStyle
+ }
+ }
+ }
+
+ override fun showPrompt(
+ generatedPassword: String,
+ url: String,
+ onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
+ ) {
+ if (suggestStrongPasswordView == null) {
+ suggestStrongPasswordView =
+ View.inflate(context, R.layout.mozac_feature_suggest_strong_password_view, this)
+ bindViews(generatedPassword, url, onSaveLoginWithStrongPassword)
+ }
+ suggestStrongPasswordView?.isVisible = true
+ useStrongPasswordTitle?.isVisible = false
+ }
+
+ override fun hidePrompt() {
+ isVisible = false
+ }
+
+ private fun bindViews(
+ strongPassword: String,
+ url: String,
+ onSaveLoginWithStrongPassword: (url: String, password: String) -> Unit,
+ ) {
+ suggestStrongPasswordHeader =
+ findViewById<AppCompatTextView>(R.id.suggest_strong_password_header).apply {
+ headerTextStyle?.let {
+ TextViewCompat.setTextAppearance(this, it)
+ currentTextColor.let { textColor ->
+ TextViewCompat.setCompoundDrawableTintList(
+ this,
+ ColorStateList.valueOf(textColor),
+ )
+ }
+ }
+ setOnClickListener {
+ useStrongPasswordTitle?.let {
+ it.visibility = if (it.isVisible) {
+ GONE
+ } else {
+ VISIBLE
+ }
+ }
+ }
+ }
+
+ useStrongPasswordTitle = findViewById<AppCompatTextView>(R.id.use_strong_password).apply {
+ text = context.getString(
+ R.string.mozac_feature_prompts_suggest_strong_password_message,
+ strongPassword,
+ )
+ visibility = GONE
+ setOnClickListener {
+ listener?.onUseGeneratedPassword(
+ generatedPassword = strongPassword,
+ url = url,
+ onSaveLoginWithStrongPassword = onSaveLoginWithStrongPassword,
+ )
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordDelegate.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordDelegate.kt
new file mode 100644
index 0000000000..b77e544000
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordDelegate.kt
@@ -0,0 +1,19 @@
+/* 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.prompts.login
+
+import mozilla.components.feature.prompts.concept.PasswordPromptView
+
+/**
+ * Delegate to display the suggest strong password prompt.
+ */
+interface SuggestStrongPasswordDelegate {
+
+ /**
+ * The [PasswordPromptView] used for [StrongPasswordPromptViewListener] to display a simple prompt.
+ */
+ val strongPasswordPromptViewListenerView: PasswordPromptView?
+ get() = null
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/provider/FileProvider.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/provider/FileProvider.kt
new file mode 100644
index 0000000000..512dc3026c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/provider/FileProvider.kt
@@ -0,0 +1,18 @@
+/* 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.prompts.provider
+
+/**
+ * A file provider to provide functionality for the feature prompts component.
+ *
+ * We need this class to create a fully qualified class name that doesn't clash with other
+ * file providers in other components see https://stackoverflow.com/a/43444164/5533820.
+ *
+ * Be aware, when creating new file resources avoid using common names like "@xml/file_paths",
+ * as other file providers could be using the same names and this could case unexpected behaviors.
+ * As a convention try to use unique names like using the name of the component as a prefix of the
+ * name of the file, like component_xxx_file_paths.xml.
+ */
+/** @suppress */
+class FileProvider : androidx.core.content.FileProvider()
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/share/ShareDelegate.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/share/ShareDelegate.kt
new file mode 100644
index 0000000000..96d7bebaea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/share/ShareDelegate.kt
@@ -0,0 +1,51 @@
+/* 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.prompts.share
+
+import android.content.Context
+import mozilla.components.concept.engine.prompt.ShareData
+import mozilla.components.support.ktx.android.content.share
+
+/**
+ * Delegate to display a share prompt.
+ */
+interface ShareDelegate {
+
+ /**
+ * Displays a share sheet for the given [ShareData].
+ *
+ * @param context Reference to context.
+ * @param shareData Data to share.
+ * @param onDismiss Callback to be invoked if the share sheet is dismissed and nothing
+ * is selected, or if it fails to load.
+ * @param onSuccess Callback to be invoked if the data is successfully shared.
+ */
+ fun showShareSheet(
+ context: Context,
+ shareData: ShareData,
+ onDismiss: () -> Unit,
+ onSuccess: () -> Unit,
+ )
+}
+
+/**
+ * Default [ShareDelegate] implementation that displays the native share sheet.
+ */
+class DefaultShareDelegate : ShareDelegate {
+
+ override fun showShareSheet(
+ context: Context,
+ shareData: ShareData,
+ onDismiss: () -> Unit,
+ onSuccess: () -> Unit,
+ ) {
+ val shareSucceeded = context.share(
+ text = listOfNotNull(shareData.url, shareData.text).joinToString(" "),
+ subject = shareData.title.orEmpty(),
+ )
+
+ if (shareSucceeded) onSuccess() else onDismiss()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/LoginPanelTextInputLayout.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/LoginPanelTextInputLayout.kt
new file mode 100644
index 0000000000..1cb7733079
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/LoginPanelTextInputLayout.kt
@@ -0,0 +1,55 @@
+/* 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.prompts.widget
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.annotation.StyleableRes
+import androidx.core.content.ContextCompat
+import androidx.core.content.withStyledAttributes
+import com.google.android.material.textfield.TextInputLayout
+import mozilla.components.feature.prompts.R
+
+internal class LoginPanelTextInputLayout(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : TextInputLayout(context, attrs, defStyleAttr) {
+ constructor(context: Context) : this(context, null, 0)
+
+ constructor(context: Context, attrs: AttributeSet? = null) : this(context, attrs, 0)
+
+ init {
+ context.withStyledAttributes(
+ attrs,
+ R.styleable.LoginPanelTextInputLayout,
+ defStyleAttr,
+ 0,
+ ) {
+
+ defaultHintTextColor = ColorStateList.valueOf(
+ ContextCompat.getColor(
+ context,
+ R.color.mozacBoxStrokeColor,
+ ),
+ )
+
+ getColorOrNull(R.styleable.LoginPanelTextInputLayout_mozacInputLayoutErrorTextColor)?.let { color ->
+ setErrorTextColor(ColorStateList.valueOf(color))
+ }
+
+ getColorOrNull(R.styleable.LoginPanelTextInputLayout_mozacInputLayoutErrorIconColor)?.let { color ->
+ setErrorIconTintList(ColorStateList.valueOf(color))
+ }
+ }
+ }
+
+ private fun TypedArray.getColorOrNull(@StyleableRes styleableRes: Int): Int? {
+ val resourceId = this.getResourceId(styleableRes, 0)
+ return if (resourceId > 0) ContextCompat.getColor(context, resourceId) else null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/MonthAndYearPicker.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/MonthAndYearPicker.kt
new file mode 100644
index 0000000000..2b6d334a72
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/MonthAndYearPicker.kt
@@ -0,0 +1,178 @@
+/* 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.prompts.widget
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.widget.NumberPicker
+import android.widget.ScrollView
+import androidx.annotation.VisibleForTesting
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.ext.month
+import mozilla.components.feature.prompts.ext.now
+import mozilla.components.feature.prompts.ext.year
+import java.util.Calendar
+
+/**
+ * UI widget that allows to select a month and a year.
+ */
+@SuppressLint("ViewConstructor") // This view is only instantiated in code
+internal class MonthAndYearPicker @JvmOverloads constructor(
+ context: Context,
+ private val selectedDate: Calendar = now(),
+ private val maxDate: Calendar = getDefaultMaxDate(),
+ private val minDate: Calendar = getDefaultMinDate(),
+ internal var dateSetListener: OnDateSetListener? = null,
+) : ScrollView(context), NumberPicker.OnValueChangeListener {
+
+ @VisibleForTesting
+ internal val monthView: NumberPicker
+
+ @VisibleForTesting
+ internal val yearView: NumberPicker
+ private val monthsLabels: Array<out String>
+
+ init {
+ inflate(context, R.layout.mozac_feature_promps_widget_month_picker, this)
+
+ adjustMinMaxDateIfAreInIllogicalRange()
+ adjustIfSelectedDateIsInIllogicalRange()
+
+ monthsLabels = context.resources.getStringArray(R.array.mozac_feature_prompts_months)
+
+ monthView = findViewById(R.id.month_chooser)
+ yearView = findViewById(R.id.year_chooser)
+
+ iniMonthView()
+ iniYearView()
+ }
+
+ override fun onValueChange(view: NumberPicker, oldVal: Int, newVal: Int) {
+ var month = 0
+ var year = 0
+ when (view.id) {
+ R.id.month_chooser -> {
+ month = newVal
+ // Wrapping months to update greater fields
+ if (oldVal == view.maxValue && newVal == view.minValue) {
+ yearView.value += 1
+ if (!yearView.value.isMinYear()) {
+ month = Calendar.JANUARY
+ }
+ } else if (oldVal == view.minValue && newVal == view.maxValue) {
+ yearView.value -= 1
+ if (!yearView.value.isMaxYear()) {
+ month = Calendar.DECEMBER
+ }
+ }
+ year = yearView.value
+ }
+ R.id.year_chooser -> {
+ month = monthView.value
+ year = newVal
+ }
+ }
+
+ selectedDate.month = month
+ selectedDate.year = year
+ updateMonthView(month)
+ dateSetListener?.onDateSet(this, month + 1, year) // Month is zero based
+ }
+
+ private fun Int.isMinYear() = minDate.year == this
+ private fun Int.isMaxYear() = maxDate.year == this
+
+ private fun iniMonthView() {
+ monthView.setOnValueChangedListener(this)
+ monthView.setOnLongPressUpdateInterval(SPEED_MONTH_SPINNER)
+ updateMonthView(selectedDate.month)
+ }
+
+ private fun iniYearView() {
+ val year = selectedDate.year
+ val max = maxDate.year
+ val min = minDate.year
+
+ yearView.init(year, min, max)
+ yearView.wrapSelectorWheel = false
+ yearView.setOnLongPressUpdateInterval(SPEED_YEAR_SPINNER)
+ }
+
+ private fun updateMonthView(month: Int) {
+ var min = Calendar.JANUARY
+ var max = Calendar.DECEMBER
+
+ if (selectedDate.year.isMinYear()) {
+ min = minDate.month
+ }
+
+ if (selectedDate.year.isMaxYear()) {
+ max = maxDate.month
+ }
+
+ monthView.apply {
+ displayedValues = null
+ minValue = min
+ maxValue = max
+ displayedValues = monthsLabels.copyOfRange(monthView.minValue, monthView.maxValue + 1)
+ value = month
+ wrapSelectorWheel = true
+ }
+ }
+
+ private fun adjustMinMaxDateIfAreInIllogicalRange() {
+ // If the input date range is illogical/garbage, we should not restrict the input range (i.e. allow the
+ // user to select any date). If we try to make any assumptions based on the illogical min/max date we could
+ // potentially prevent the user from selecting dates that are in the developers intended range, so it's best
+ // to allow anything.
+ if (maxDate.before(minDate)) {
+ minDate.timeInMillis = getDefaultMinDate().timeInMillis
+ maxDate.timeInMillis = getDefaultMaxDate().timeInMillis
+ }
+ }
+
+ private fun adjustIfSelectedDateIsInIllogicalRange() {
+ if (selectedDate.before(minDate) || selectedDate.after(maxDate)) {
+ selectedDate.timeInMillis = minDate.timeInMillis
+ }
+ }
+
+ private fun NumberPicker.init(currentValue: Int, min: Int, max: Int) {
+ minValue = min
+ maxValue = max
+ value = currentValue
+ setOnValueChangedListener(this@MonthAndYearPicker)
+ }
+
+ interface OnDateSetListener {
+ fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int)
+ }
+
+ companion object {
+
+ private const val SPEED_MONTH_SPINNER = 200L
+ private const val SPEED_YEAR_SPINNER = 100L
+
+ @VisibleForTesting
+ internal const val DEFAULT_MAX_YEAR = 9999
+
+ @VisibleForTesting
+ internal const val DEFAULT_MIN_YEAR = 1
+
+ internal fun getDefaultMinDate(): Calendar {
+ return now().apply {
+ month = Calendar.JANUARY
+ year = DEFAULT_MIN_YEAR
+ }
+ }
+
+ internal fun getDefaultMaxDate(): Calendar {
+ return now().apply {
+ month = Calendar.DECEMBER
+ year = DEFAULT_MAX_YEAR
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/TimePrecisionPicker.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/TimePrecisionPicker.kt
new file mode 100644
index 0000000000..f1855abdba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/TimePrecisionPicker.kt
@@ -0,0 +1,231 @@
+/* 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.prompts.widget
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.View
+import android.widget.NumberPicker
+import android.widget.ScrollView
+import androidx.annotation.VisibleForTesting
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.ext.hour
+import mozilla.components.feature.prompts.ext.maxHour
+import mozilla.components.feature.prompts.ext.maxMillisecond
+import mozilla.components.feature.prompts.ext.maxMinute
+import mozilla.components.feature.prompts.ext.maxSecond
+import mozilla.components.feature.prompts.ext.millisecond
+import mozilla.components.feature.prompts.ext.minHour
+import mozilla.components.feature.prompts.ext.minMillisecond
+import mozilla.components.feature.prompts.ext.minMinute
+import mozilla.components.feature.prompts.ext.minSecond
+import mozilla.components.feature.prompts.ext.minute
+import mozilla.components.feature.prompts.ext.now
+import mozilla.components.feature.prompts.ext.second
+import mozilla.components.support.utils.TimePicker.shouldShowMillisecondsPicker
+import java.util.Calendar
+
+/**
+ * UI widget that allows to select a time with precision of seconds or milliseconds.
+ */
+@SuppressLint("ViewConstructor") // This view is only instantiated in code
+internal class TimePrecisionPicker @JvmOverloads constructor(
+ context: Context,
+ private val selectedTime: Calendar = now(),
+ private val minTime: Calendar = getDefaultMinTime(),
+ private val maxTime: Calendar = getDefaultMaxTime(),
+ stepValue: Float,
+ private var timeSetListener: OnTimeSetListener? = null,
+) : ScrollView(context), NumberPicker.OnValueChangeListener {
+
+ @VisibleForTesting
+ internal val hourView: NumberPicker
+
+ @VisibleForTesting
+ internal val minuteView: NumberPicker
+
+ @VisibleForTesting
+ internal val secondView: NumberPicker
+
+ @VisibleForTesting
+ internal val millisecondView: NumberPicker
+
+ init {
+ inflate(context, R.layout.mozac_feature_prompts_time_picker, this)
+
+ adjustMinMaxTimeIfInIllogicalRange()
+
+ hourView = findViewById(R.id.hour_picker)
+ minuteView = findViewById(R.id.minute_picker)
+ secondView = findViewById(R.id.second_picker)
+ millisecondView = findViewById(R.id.millisecond_picker)
+
+ // Hide the millisecond picker if this is not the desired precision defined by the step
+ if (!shouldShowMillisecondsPicker(stepValue)) {
+ millisecondView.visibility = GONE
+ findViewById<View>(R.id.millisecond_separator).visibility = GONE
+ }
+
+ initHourView()
+ updateMinuteView()
+ minuteView.setOnValueChangedListener(this)
+ minuteView.setOnLongPressUpdateInterval(SPEED_MINUTE_SPINNER)
+ secondView.init(
+ selectedTime.second,
+ selectedTime.minSecond(),
+ selectedTime.maxSecond(),
+ )
+ millisecondView.init(
+ selectedTime.millisecond,
+ selectedTime.minMillisecond(),
+ selectedTime.maxMillisecond(),
+ )
+ }
+
+ override fun onValueChange(view: NumberPicker, oldVal: Int, newVal: Int) {
+ var hour = selectedTime.hour
+ var minute = selectedTime.minute
+ var second = selectedTime.second
+ var millisecond = selectedTime.millisecond
+
+ when (view.id) {
+ R.id.hour_picker -> {
+ hour = newVal
+ selectedTime.hour = hour
+ updateMinuteView()
+ }
+ R.id.minute_picker -> {
+ minute = newVal
+ selectedTime.minute = minute
+ }
+ R.id.second_picker -> {
+ second = newVal
+ selectedTime.set(Calendar.SECOND, second)
+ }
+ R.id.millisecond_picker -> {
+ millisecond = newVal
+ selectedTime.set(Calendar.MILLISECOND, millisecond)
+ }
+ }
+
+ // Update the selected time with the latest values.
+ timeSetListener?.onTimeSet(this, hour, minute, second, millisecond)
+ }
+
+ private fun adjustMinMaxTimeIfInIllogicalRange() {
+ // If the input time range is illogical, we should not restrict the input range.
+ if (maxTime.before(minTime)) {
+ minTime.timeInMillis = getDefaultMinTime().timeInMillis
+ maxTime.timeInMillis = getDefaultMaxTime().timeInMillis
+ }
+ }
+
+ // Initialize the hour view with the min and max values.
+ private fun initHourView() {
+ val min = minTime.hour
+ val max = maxTime.hour
+
+ if (selectedTime.hour < min || selectedTime.hour > max) {
+ selectedTime.hour = min
+ timeSetListener?.onTimeSet(
+ this,
+ selectedTime.hour,
+ selectedTime.minute,
+ selectedTime.second,
+ selectedTime.millisecond,
+ )
+ }
+
+ hourView.apply {
+ minValue = min
+ maxValue = max
+ displayedValues = ((min..max).map { it.toString() }).toTypedArray()
+ value = selectedTime.hour
+ wrapSelectorWheel = true
+ setOnValueChangedListener(this@TimePrecisionPicker)
+ setOnLongPressUpdateInterval(SPEED_HOUR_SPINNER)
+ }
+ }
+
+ // Update the minute view.
+ private fun updateMinuteView() {
+ val min: Int = if (selectedTime.hour == minTime.hour) {
+ minTime.minute
+ } else {
+ selectedTime.minMinute()
+ }
+ val max: Int = if (selectedTime.hour == maxTime.hour) {
+ maxTime.minute
+ } else {
+ selectedTime.maxMinute()
+ }
+ // If the hour is set to min/max value, then constraint the minute to a valid value.
+ val minute = if (selectedTime.minute < min || selectedTime.minute > max) {
+ timeSetListener?.onTimeSet(
+ this,
+ selectedTime.hour,
+ min,
+ selectedTime.second,
+ selectedTime.millisecond,
+ )
+ min
+ } else {
+ selectedTime.minute
+ }
+
+ minuteView.apply {
+ displayedValues = null
+ minValue = min
+ maxValue = max
+ displayedValues = ((min..max).map { it.toString() }).toTypedArray()
+ value = minute
+ wrapSelectorWheel = true
+ }
+ }
+
+ // Initialize the [NumberPicker].
+ private fun NumberPicker.init(currentValue: Int, min: Int, max: Int) {
+ minValue = min
+ maxValue = max
+ value = currentValue
+ displayedValues = ((min..max).map { it.toString() }).toTypedArray()
+ setOnValueChangedListener(this@TimePrecisionPicker)
+ setOnLongPressUpdateInterval(SPEED_MINUTE_SPINNER)
+ }
+
+ // Interface used to set the selected time with seconds/milliseconds precision
+ interface OnTimeSetListener {
+ fun onTimeSet(
+ picker: TimePrecisionPicker,
+ hour: Int,
+ minute: Int,
+ second: Int,
+ millisecond: Int,
+ )
+ }
+
+ companion object {
+ private const val SPEED_HOUR_SPINNER = 200L
+ private const val SPEED_MINUTE_SPINNER = 100L
+
+ internal fun getDefaultMinTime(): Calendar {
+ return now().apply {
+ hour = minHour()
+ minute = minMinute()
+ second = minSecond()
+ millisecond = minMillisecond()
+ }
+ }
+
+ internal fun getDefaultMaxTime(): Calendar {
+ return now().apply {
+ hour = maxHour()
+ minute = maxMinute()
+ second = maxSecond()
+ millisecond = maxMillisecond()
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/color/button_state_list.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/color/button_state_list.xml
new file mode 100644
index 0000000000..ce7a07e0d5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/color/button_state_list.xml
@@ -0,0 +1,8 @@
+<?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/. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false" android:alpha="0.50" android:color="?android:colorEdgeEffect" />
+ <item android:color="?android:colorEdgeEffect"/>
+</selector>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-hdpi/color_picker_row_bg.9.png b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-hdpi/color_picker_row_bg.9.png
new file mode 100644
index 0000000000..68ac5ef730
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-hdpi/color_picker_row_bg.9.png
Binary files differ
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-mdpi/color_picker_row_bg.9.png b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-mdpi/color_picker_row_bg.9.png
new file mode 100644
index 0000000000..5d72bdd255
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-mdpi/color_picker_row_bg.9.png
Binary files differ
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-xhdpi/color_picker_row_bg.9.png b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-xhdpi/color_picker_row_bg.9.png
new file mode 100644
index 0000000000..f5f9283b4a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-xhdpi/color_picker_row_bg.9.png
Binary files differ
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-xxhdpi/color_picker_row_bg.9.png b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-xxhdpi/color_picker_row_bg.9.png
new file mode 100644
index 0000000000..1d69dc96eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable-xxhdpi/color_picker_row_bg.9.png
Binary files differ
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/drawable/color_picker_checkmark.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable/color_picker_checkmark.xml
new file mode 100644
index 0000000000..f162dba5d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable/color_picker_checkmark.xml
@@ -0,0 +1,11 @@
+<?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/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="ring"
+ android:innerRadius="15dip"
+ android:thickness="4dip"
+ android:useLevel="false">
+ <solid android:color="@android:color/white"/>
+</shape>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/drawable/mozac_ic_password_reveal_two_state.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable/mozac_ic_password_reveal_two_state.xml
new file mode 100644
index 0000000000..79760e5aa8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/drawable/mozac_ic_password_reveal_two_state.xml
@@ -0,0 +1,8 @@
+<?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/. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/mozac_ic_eye_24" android:state_checked="false" />
+ <item android:drawable="@drawable/mozac_ic_eye_slash_24" />
+</selector>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/login_selection_list_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/login_selection_list_item.xml
new file mode 100644
index 0000000000..e754c7b50b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/login_selection_list_item.xml
@@ -0,0 +1,50 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/login_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingStart="63dp"
+ android:paddingTop="8dp"
+ android:paddingEnd="8dp"
+ android:paddingBottom="8dp">
+
+ <TextView
+ android:id="@+id/username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAutofill="no"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:textIsSelectable="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Username" />
+
+ <TextView
+ android:id="@+id/password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAutofill="no"
+ android:inputType="textPassword"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textIsSelectable="false"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/username"
+ tools:ignore="TextViewEdits"
+ tools:text="password" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_choice_dialogs.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_choice_dialogs.xml
new file mode 100644
index 0000000000..ef5562819c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_choice_dialogs.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<androidx.recyclerview.widget.RecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/recyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"/> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_choice_group_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_choice_group_item.xml
new file mode 100644
index 0000000000..7a166ded04
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_choice_group_item.xml
@@ -0,0 +1,7 @@
+<?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/. -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="?android:attr/listSeparatorTextViewStyle"
+ android:id="@+id/labelView"/> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_login_multiselect_view.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_login_multiselect_view.xml
new file mode 100644
index 0000000000..116eae6cb2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_login_multiselect_view.xml
@@ -0,0 +1,86 @@
+<?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/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+ <ScrollView
+ android:id="@+id/login_scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/scroll_child"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/saved_logins_header"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/mozac_feature_prompts_expand_logins_content_description_2"
+ android:drawablePadding="24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="56dp"
+ android:text="@string/mozac_feature_prompts_saved_logins_2"
+ android:textColor="?android:colorEdgeEffect"
+ android:textSize="16sp"
+ app:drawableStartCompat="@drawable/mozac_ic_login_24"
+ app:drawableTint="?android:colorEdgeEffect"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/mozac_feature_login_multiselect_expand"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ android:padding="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_chevron_down_24"
+ app:tint="?android:colorEdgeEffect" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/logins_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_constraintTop_toBottomOf="@id/mozac_feature_login_multiselect_expand"
+ tools:listitem="@layout/login_selection_list_item" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/manage_logins"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:background="?android:selectableItemBackground"
+ android:drawablePadding="24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="0dp"
+ android:text="@string/mozac_feature_prompts_manage_logins_2"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp"
+ android:visibility="gone"
+ app:drawableStartCompat="@drawable/mozac_ic_settings_24"
+ app:drawableTint="?android:textColorPrimary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/logins_list" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </ScrollView>
+</merge>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_menu_choice_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_menu_choice_item.xml
new file mode 100644
index 0000000000..c491949485
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_menu_choice_item.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/labelView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"/>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_menu_separator_choice_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_menu_separator_choice_item.xml
new file mode 100644
index 0000000000..f3dcfa28a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_menu_separator_choice_item.xml
@@ -0,0 +1,8 @@
+<?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/. -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:background="?android:attr/listDivider"/>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_multiple_choice_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_multiple_choice_item.xml
new file mode 100644
index 0000000000..89a672cc21
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_multiple_choice_item.xml
@@ -0,0 +1,23 @@
+<?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/. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:background="?android:attr/selectableItemBackground">
+
+ <CheckedTextView
+ android:id="@+id/labelView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:checkMark="?android:attr/listChoiceIndicatorMultiple"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
+</LinearLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_promps_widget_month_picker.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_promps_widget_month_picker.xml
new file mode 100644
index 0000000000..4d00492351
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_promps_widget_month_picker.xml
@@ -0,0 +1,40 @@
+<?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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:parentTag="android.widget.ScrollView">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <android.widget.NumberPicker
+ android:id="@+id/month_chooser"
+ android:layout_width="60dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="1dp"
+ android:layout_marginEnd="1dp"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ <android.widget.NumberPicker
+ android:id="@+id/year_chooser"
+ android:layout_width="75dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="1dp"
+ android:layout_marginEnd="1dp"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ </LinearLayout>
+</merge>
+
+
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_auth_prompt.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_auth_prompt.xml
new file mode 100644
index 0000000000..170da9e56e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_auth_prompt.xml
@@ -0,0 +1,54 @@
+<?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/. -->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingLeft">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/username_text_input_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <mozilla.components.feature.prompts.dialog.AutofillEditText
+ android:id="@+id/username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/mozac_feature_prompt_username_hint"
+ android:inputType="text"
+ android:autofillHints="username"
+ tools:ignore="UnusedAttribute"/>
+
+ </com.google.android.material.textfield.TextInputLayout >
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/password_text_input_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:passwordToggleEnabled="true">
+
+ <mozilla.components.feature.prompts.dialog.AutofillEditText
+ android:id="@+id/password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/mozac_feature_prompt_password_hint"
+ android:inputType="textPassword"
+ android:autofillHints="password"
+ tools:ignore="UnusedAttribute"/>
+
+ </com.google.android.material.textfield.TextInputLayout >
+
+ </LinearLayout>
+</ScrollView> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_save_credit_card_prompt.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_save_credit_card_prompt.xml
new file mode 100644
index 0000000000..03bb4d9a83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_save_credit_card_prompt.xml
@@ -0,0 +1,140 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:windowBackground"
+ android:paddingStart="16dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="16dp"
+ android:paddingBottom="16dp"
+ tools:ignore="Overdraw">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/lock_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_lock_24"
+ app:tint="?android:attr/textColorPrimary"
+ android:importantForAccessibility="no" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/save_credit_card_header"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:gravity="center_vertical"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ android:layout_marginStart="12dp"
+ android:layout_marginEnd="12dp"
+ app:layout_constraintStart_toEndOf="@id/lock_icon"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Securely save this card?" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/save_credit_card_message"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:text="@string/mozac_feature_prompts_save_credit_card_prompt_body"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="14sp"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="@id/save_credit_card_header"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/save_credit_card_header"
+ app:layout_goneMarginTop="20dp"
+ tools:text="Card number will be encrypted. Security coded won\'t be saved."
+ tools:visibility="visible" />
+
+ <ImageView
+ android:id="@+id/credit_card_logo"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginTop="16dp"
+ android:scaleType="fitCenter"
+ android:importantForAccessibility="no"
+ app:layout_constraintStart_toStartOf="@id/save_credit_card_header"
+ app:layout_constraintTop_toBottomOf="@id/save_credit_card_message" />
+
+ <TextView
+ android:id="@+id/credit_card_number"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="48dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAutofill="no"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:textIsSelectable="false"
+ app:layout_constraintBottom_toTopOf="@id/credit_card_expiration_date"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/credit_card_logo"
+ app:layout_constraintTop_toBottomOf="@id/save_credit_card_message"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="Card 0000000000" />
+
+ <TextView
+ android:id="@+id/credit_card_expiration_date"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="48dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAutofill="no"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textIsSelectable="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/credit_card_logo"
+ app:layout_constraintTop_toBottomOf="@id/credit_card_number"
+ tools:text="01/2022" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/save_cancel"
+ style="@style/Widget.MaterialComponents.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="12dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:letterSpacing="0"
+ android:text="@string/mozac_feature_prompt_not_now"
+ android:textAlignment="center"
+ android:textAllCaps="false"
+ android:textColor="@color/button_state_list"
+ android:textSize="14sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/save_confirm"
+ app:layout_constraintTop_toBottomOf="@id/credit_card_logo"
+ app:rippleColor="?android:textColorSecondary" />
+
+ <Button
+ android:id="@+id/save_confirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_marginTop="16dp"
+ android:text="@string/mozac_feature_prompt_save_confirmation"
+ android:textAlignment="center"
+ android:textAllCaps="false"
+ android:textColor="?android:windowBackground"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ app:backgroundTint="@color/button_state_list"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/credit_card_logo"
+ app:rippleColor="?android:textColorSecondary" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_save_login_prompt.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_save_login_prompt.xml
new file mode 100644
index 0000000000..6deb56ffff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_save_login_prompt.xml
@@ -0,0 +1,144 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/feature_prompt_login_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:windowBackground"
+ android:paddingStart="16dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="16dp"
+ android:paddingBottom="16dp"
+ tools:ignore="Overdraw">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/host_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_globe_24"
+ android:importantForAccessibility="no" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/host_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:gravity="center_vertical"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp"
+ android:layout_marginStart="12dp"
+ android:layout_marginEnd="12dp"
+ app:layout_constraintStart_toEndOf="@+id/host_icon"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="host.com" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/save_message"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:drawablePadding="16dp"
+ android:gravity="center_vertical"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp"
+ app:drawableStartCompat="@drawable/mozac_ic_login_24"
+ app:drawableTint="?android:textColorPrimary"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/host_name"
+ app:layout_goneMarginTop="8dp"
+ tools:text="@string/mozac_feature_prompt_login_save_headline_2" />
+
+ <mozilla.components.feature.prompts.widget.LoginPanelTextInputLayout
+ android:id="@+id/userNameLayout"
+ style="@style/MozTextInputLayout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="30dp"
+ android:layout_marginTop="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/save_message">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/username_field"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/mozac_feature_prompt_username_hint"
+ android:imeOptions="actionDone"
+ android:singleLine="true"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp" />
+ </mozilla.components.feature.prompts.widget.LoginPanelTextInputLayout>
+
+ <mozilla.components.feature.prompts.widget.LoginPanelTextInputLayout
+ android:id="@+id/password_text_input_layout"
+ style="@style/MozTextInputLayout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="30dp"
+ android:layout_marginTop="12dp"
+ app:errorEnabled="true"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/userNameLayout"
+ app:passwordToggleDrawable="@drawable/mozac_ic_password_reveal_two_state"
+ app:passwordToggleEnabled="true"
+ app:passwordToggleTint="?android:textColorPrimary">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/password_field"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/mozac_feature_prompt_password_hint"
+ android:imeOptions="actionDone"
+ android:inputType="textPassword"
+ android:singleLine="true"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp" />
+
+ </mozilla.components.feature.prompts.widget.LoginPanelTextInputLayout>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/save_cancel"
+ style="@style/Widget.MaterialComponents.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="12dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:letterSpacing="0"
+ android:text="@string/mozac_feature_prompt_never_save"
+ android:textAlignment="center"
+ android:textAllCaps="false"
+ android:textColor="@color/button_state_list"
+ android:textSize="14sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/save_confirm"
+ app:layout_constraintTop_toBottomOf="@+id/password_text_input_layout"
+ app:rippleColor="?android:textColorSecondary" />
+
+ <Button
+ android:id="@+id/save_confirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_marginTop="16dp"
+ android:text="@string/mozac_feature_prompt_save_confirmation"
+ android:textAlignment="center"
+ android:textAllCaps="false"
+ android:textColor="?android:windowBackground"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ app:backgroundTint="@color/button_state_list"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/password_text_input_layout"
+ app:rippleColor="?android:textColorSecondary" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_simple_text.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_simple_text.xml
new file mode 100644
index 0000000000..62c9bc86ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_simple_text.xml
@@ -0,0 +1,14 @@
+<?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/. -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/labelView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingStart="24dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="24dp"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall" />
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_with_check_box.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_with_check_box.xml
new file mode 100644
index 0000000000..f094fe9c43
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompt_with_check_box.xml
@@ -0,0 +1,47 @@
+<?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/. -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingLeft">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingLeft">
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:maxLines="30"
+ android:scrollbars="vertical"
+ android:textColor="?android:attr/textColorPrimary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Message" />
+
+ <CheckBox
+ android:id="@id/mozac_feature_prompts_no_more_dialogs_check_box"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/mozac_feature_prompts_no_more_dialogs"
+ android:textColor="?android:attr/textColorPrimary"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/message"
+ tools:visibility="visible" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</ScrollView>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_list_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_list_item.xml
new file mode 100644
index 0000000000..3a63084b7e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_list_item.xml
@@ -0,0 +1,31 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/address_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+ <TextView
+ android:id="@+id/address_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAutofill="no"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:textIsSelectable="false"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="1230 Main St, Los Angeles, CA 90237" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_select_prompt.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_select_prompt.xml
new file mode 100644
index 0000000000..80a4660703
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_select_prompt.xml
@@ -0,0 +1,84 @@
+<?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/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+ <ScrollView
+ android:id="@+id/address_scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/scroll_child"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/select_address_header"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/mozac_feature_prompts_expand_address_content_description_2"
+ android:drawablePadding="24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="56dp"
+ android:text="@string/mozac_feature_prompts_select_address_2"
+ android:textColor="?android:colorEdgeEffect"
+ android:textSize="16sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/mozac_feature_address_expander"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ android:padding="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_chevron_down_24"
+ app:tint="?android:colorEdgeEffect" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/address_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_constraintTop_toBottomOf="@id/mozac_feature_address_expander"
+ tools:listitem="@layout/mozac_feature_prompts_address_list_item" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/manage_addresses"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:background="?android:selectableItemBackground"
+ android:drawablePadding="24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="24dp"
+ android:paddingEnd="0dp"
+ android:text="@string/mozac_feature_prompts_manage_address"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp"
+ android:visibility="gone"
+ app:drawableStartCompat="@drawable/mozac_ic_settings_24"
+ app:drawableTint="?android:textColorPrimary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/address_list" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </ScrollView>
+</merge>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_color_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_color_item.xml
new file mode 100644
index 0000000000..15f07d2dc6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_color_item.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/color_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:gravity="center_vertical"
+ android:background="@drawable/color_picker_row_bg"
+ android:minHeight="?android:attr/listPreferredItemHeight" />
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_color_picker_dialogs.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_color_picker_dialogs.xml
new file mode 100644
index 0000000000..7c7e329fb8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_color_picker_dialogs.xml
@@ -0,0 +1,12 @@
+<?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/. -->
+<androidx.recyclerview.widget.RecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/recyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ tools:listitem="@layout/mozac_feature_prompts_color_item" />
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_credit_card_list_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_credit_card_list_item.xml
new file mode 100644
index 0000000000..34bc24b7ef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_credit_card_list_item.xml
@@ -0,0 +1,59 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/credit_card_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+ <ImageView
+ android:id="@+id/credit_card_logo"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginStart="16dp"
+ android:scaleType="fitCenter"
+ android:importantForAccessibility="no"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+ <TextView
+ android:id="@+id/credit_card_number"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="48dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAutofill="no"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:textIsSelectable="false"
+ app:layout_constraintBottom_toTopOf="@id/credit_card_expiration_date"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/credit_card_logo"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="Card 0000000000" />
+
+ <TextView
+ android:id="@+id/credit_card_expiration_date"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="48dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAutofill="no"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textIsSelectable="false"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/credit_card_logo"
+ app:layout_constraintTop_toBottomOf="@id/credit_card_number"
+ tools:text="01/2022" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_credit_card_select_prompt.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_credit_card_select_prompt.xml
new file mode 100644
index 0000000000..fad767dafd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_credit_card_select_prompt.xml
@@ -0,0 +1,84 @@
+<?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/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+ <ScrollView
+ android:id="@+id/credit_card_scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/scroll_child"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/select_credit_card_header"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/mozac_feature_prompts_expand_credit_cards_content_description_2"
+ android:drawablePadding="24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="56dp"
+ android:text="@string/mozac_feature_prompts_select_credit_card_2"
+ android:textColor="?android:colorEdgeEffect"
+ android:textSize="16sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/mozac_feature_credit_cards_expander"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ android:padding="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/mozac_ic_chevron_down_24"
+ app:tint="?android:colorEdgeEffect" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/credit_cards_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_constraintTop_toBottomOf="@id/mozac_feature_credit_cards_expander"
+ tools:listitem="@layout/mozac_feature_prompts_credit_card_list_item" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/manage_credit_cards"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:background="?android:selectableItemBackground"
+ android:drawablePadding="24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="24dp"
+ android:paddingEnd="0dp"
+ android:text="@string/mozac_feature_prompts_manage_credit_cards_2"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp"
+ android:visibility="gone"
+ app:drawableStartCompat="@drawable/mozac_ic_settings_24"
+ app:drawableTint="?android:textColorPrimary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/credit_cards_list" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </ScrollView>
+</merge>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_date_time_picker.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_date_time_picker.xml
new file mode 100644
index 0000000000..e33d882417
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_date_time_picker.xml
@@ -0,0 +1,26 @@
+<?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/. -->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <DatePicker
+ android:id="@+id/date_picker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TimePicker
+ android:id="@+id/datetime_picker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ </LinearLayout>
+</ScrollView>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_time_picker.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_time_picker.xml
new file mode 100644
index 0000000000..68323699f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_time_picker.xml
@@ -0,0 +1,66 @@
+<?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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:parentTag="android.widget.ScrollView">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <android.widget.NumberPicker
+ android:id="@+id/hour_picker"
+ android:layout_width="60dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="1dp"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/mozac_feature_prompts_second_separator" />
+
+ <android.widget.NumberPicker
+ android:id="@+id/minute_picker"
+ android:layout_width="75dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="1dp"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/mozac_feature_prompts_second_separator" />
+
+ <android.widget.NumberPicker
+ android:id="@+id/second_picker"
+ android:layout_width="75dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="1dp"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ <TextView
+ android:id="@+id/millisecond_separator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/mozac_feature_prompts_millisecond_separator" />
+
+ <android.widget.NumberPicker
+ android:id="@+id/millisecond_picker"
+ android:layout_width="75dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="1dp"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ </LinearLayout>
+</merge> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_single_choice_item.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_single_choice_item.xml
new file mode 100644
index 0000000000..d0373b3133
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_single_choice_item.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/. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:background="?android:attr/selectableItemBackground">
+
+ <CheckedTextView
+ android:id="@+id/labelView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:checkMark="?android:attr/listChoiceIndicatorSingle"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
+</LinearLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_suggest_strong_password_view.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_suggest_strong_password_view.xml
new file mode 100644
index 0000000000..47ce784257
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_suggest_strong_password_view.xml
@@ -0,0 +1,52 @@
+<?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/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/suggest_strong_password_header"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/mozac_feature_prompts_suggest_strong_password_content_description"
+ android:drawablePadding="24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="56dp"
+ android:text="@string/mozac_feature_prompts_suggest_strong_password"
+ android:textColor="?android:colorEdgeEffect"
+ android:textSize="16sp"
+ app:drawableStartCompat="@drawable/mozac_ic_login_24"
+ app:drawableTint="?android:colorEdgeEffect"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/use_strong_password"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:background="?android:selectableItemBackground"
+ android:drawablePadding="24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="0dp"
+ android:text="@string/mozac_feature_prompts_suggest_strong_password_message"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="16sp"
+ android:visibility="gone"
+ app:drawableStartCompat="@drawable/mozac_ic_lock_24"
+ app:drawableTint="?android:textColorPrimary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/suggest_strong_password_header" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</merge>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_text_prompt.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_text_prompt.xml
new file mode 100644
index 0000000000..c3c9cdc776
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/layout/mozac_feature_text_prompt.xml
@@ -0,0 +1,45 @@
+<?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/. -->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingLeft">
+
+ <TextView
+ android:id="@+id/input_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:text="Please enter your name"
+ android:labelFor="@+id/input_value"
+ android:contentDescription="@string/mozac_feature_prompts_content_description_input_label"/>
+
+ <EditText
+ android:id="@+id/input_value"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:ignore="Autofill"
+ android:inputType="text"/>
+
+ <CheckBox
+ android:id="@id/mozac_feature_prompts_no_more_dialogs_check_box"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/mozac_feature_prompts_no_more_dialogs"
+ android:visibility="gone"
+ tools:visibility="visible"
+ android:textColor="#aaa"/>
+
+ </LinearLayout>
+</ScrollView>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..a0d6b1b708
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-am/strings.xml
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">እሺ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ተወው</string>
+
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ይህ ገጽ ተጨማሪ መገናኛዎችን እንዳይፈጥር ይከለክሉት</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">አቀናብር</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">አጽዳ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ይግቡ</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">የተጠቃሚ ስም</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">የይለፍ ቃል</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">አታስቀምጥ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">አሁን አይሆንም</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">በጭራሽ አታስቀምጥ</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">አሁን አይሆንም</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">አስቀምጥ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">አታዘምን</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">አሁን አይሆንም</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">አዘምን</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">የይለፍ ቃል ባዶ መሆን የለበትም</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">የይለፍ ቃል ያስገቡ</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">መግባትን ማስቀመጥ አልተቻለም</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">የይለፍ ቃል ማስቀመጥ አልተቻለም</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ይህ መግቢያ ይቀመጥ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">የይለፍ ቃል ይቀመጥ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ይህ መግቢያ ይዘምን?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">የይለፍ ቃል ይዘምን?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">የተጠቃሚ ስም በተቀመጠው ይለፍ ቃል ላይ ይታከል?</string>
+
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">የጽሑፍ ግብዓት መስክ ለማስገባት መለያ</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ቀለም ይምረጡ</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ፍቀድ</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ከልክል</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">እርግጠኛ ነዎት?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">ከዚህ ድረ-ገፅ መውጣት ይፈልጋሉ? ያስገቡት ውሂብ ላይቀመጥ ይችላል</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ቆይ</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ውጣ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">አንድ ወር ይምረጡ</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ጥር</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">የካ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">መጋ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ሚያ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">ግን</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ሰኔ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ሐም</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ነሐ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">መስ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ጥቅ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ሕዳ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ታሕ</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">ስዓት ይሙሉ</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">መግቢያዎችን ያስተዳድሩ</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">የይለፍ ቃሎችን አስተዳድር</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">የተጠቆሙ መግቢያዎችን ዘርጋ</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">የተቀመጡ የይለፍ ቃሎችን ዘርጋ</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">የተጠቆሙ መግቢያዎችን ሰብስብ</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">የተቀመጡ የይለፍ ቃሎችን ሰብስብ</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">የተጠቆሙ መግቢያዎች</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">የተቀመጡ የይለፍ ቃሎች</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">ጠንካራ የይለፍ ቃል ጥቆማ</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">ጠንካራ የይለፍ ቃል ጥቆማ</string>
+
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">ጠንካራ የይለፍ ቃል ይጠቀሙ:- %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ውሂብ ወደዚህ ድረ-ገፅ እንደገና ይላክ?</string>
+
+ <string name="mozac_feature_prompt_repost_message">ይህን ገጽ ማደስ እንደ ክፍያ መላክ ወይም አስተያየት መለጠፍ ያሉ የቅርብ ጊዜ ድርጊቶችን ሁለት ጊዜ ማባዛት ይችላል።</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ውሂብ እንደገና ላክ</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ተወው</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">ክሬዲት ካርድ ይምረጡ</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">የተቀመጠ ካርድ ተጠቀም</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">የተጠቆሙ ክሬዲት ካርዶችን ዘርጋ</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">የተቀመጡ ካርዶችን ዘርጋ</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">የተጠቆሙ ክሬዲት ካርዶችን ሰብስብ</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">የተቀመጡ ካርዶችን ሰብስብ</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">ክሬዲት ካርዶችን ያስተዳድሩ</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">ካርዶችን ያስተዳድሩ</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">ይህን ካርድ ደህንነቱ በተጠበቀ ሁኔታ ይቀመጥ?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">የካርድ ማብቂያ ቀን ይዘምን?</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">የካርድ ቁጥር ይሰወራል ። የደህንነት የሚስጥር ፅሑፍ አይቀመጥም።</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s የካርድ ቁጥርዎን ያመስጥራል። የደህንነት ኮድዎ አይቀመጥም።</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">አድራሻ ይምረጡ</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">የተጠቆሙ አድራሻዎችን ዘርጋ</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">የተቀመጡ አድራሻዎችን ዘርጋ</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">የተጠቆሙ አድራሻዎችን ሰብስብ</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">የተቀመጡ አድራሻዎችን ሰብስብ</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">አድራሻዎችን ያስተዳድሩ</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">የመለያ ምስል</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">መለያ አቅራቢ ይምረጡ</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">በ%1$s መለያ ይግቡ</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$sን እንደ መለያ አቅራቢ ይጠቀሙ</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ በ%2$s መለያ ወደ %1$s መግባት በ<a href="%3$s">የግላዊነት ፖሊሲ</a> እና <a href="%4$s">የአገልግሎት ውል</a> ተገዢ ነው]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">ቀጥል</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">ሰርዝ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..7385652c19
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-an/strings.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Vale</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Privar que ista pachina creye dialogos adicionals</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Aplicar</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Borrar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Iniciar sesión</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nombre d’usuario</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Clau</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No alzar-lo</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">No alzar nunca</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Alzar-lo</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No actualizar-lo</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Lo campo Clau no puede estar vuedo</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No se puede alzar l’inicio de sesión</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Alzar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Actualizar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Anyadir nombre de usuario a la clau alzada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta pa escribir un campo de dentrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Triar una color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Refusar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">En yes seguro?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Quiers salir d’este puesto? Los datos que has escrito no s’alzarán</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">No salir</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Salir</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Triar un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Chi</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Chun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Chul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Avi</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Chestiona os inicios de sesión</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expande los inicios de sesión sucherius</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Reduce los inicios de sesión sucherius</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Inicios de sesión sucherius</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..a7a196cfa7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ar/strings.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">حسنا</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ألغِ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">امنع هذه الصفحة من إنشاء نوافذ حوار إضافية</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">حدد</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">امسح</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">لِج</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">اسم المستخدم</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">كلمة السر</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">لا تحفظ</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">لا تحفظ أبدًا</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ليس الآن</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">احفظ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">لا تُحدّث</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">حدّث</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">لا يمكن أن يكون حقل كلمة السر فارغًا</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">تعذّر حفظ جلسة الولوج</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">أنحفظ هذا الولوج؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">أنحدّث هذا الولوج؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">أنُضيف اسم المستخدم إلى كلمة المرور المحفوظة؟</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">تسمية لإدخال نص في حقل نصوص</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">اختر لونًا</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">اسمح</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ارفض</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">أمتأكّد أنت؟</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">أتريد مغادرة هذا الموقع؟ قد لا تُحظ البيانات المُدخلة</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">سأبقى</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">غادِر</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">اختر شهرا</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">يناير</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">فبراير</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">مارس</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">أبريل</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">مايو</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">يونيو</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">يوليو</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">أغسطس</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">سبتمبر</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">أكتوبر</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">نوفمبر</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ديسمبر</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">اضبط الوقت</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">أدِر جلسات الولوج</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">وسّع جلسات الولوج المقترحة</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">اطوِ جلسات الولوج المقترحة</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">جلسات الولوج المقترحة</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">أنُعيد إرسال البيانات إلى هذا الموقع؟</string>
+ <string name="mozac_feature_prompt_repost_message">بإنعاش هذه الصفحة قد تُكرّر آخر الإجراءات مثل إرسال النقود أو نشر تعليق.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">أعِد إرسال البيانات</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ألغِ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">اختر بطاقة ائتمان</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">وسّع بطاقات الائتمان المقترحة</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">اطوِ بطاقات الائتمان المقترحة</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">أدِر بطاقات الائتمان</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">أأحفظ هذه البطاقة بأمان؟</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">أتريد تحديث تاريخ انتهاء البطاقة؟</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">سيُعمّى رقم البطاقة. لن يُحفظ رمز الأمان.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">اختر العنوان</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">وسِّع العناوين المقترحة</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">اطوِ العناوين المقترحة</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">أدر العناوين</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">صورة الحساب</string>
+ <!-- Title of the Identity Credential provider dialog choose. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">اختر مزود ولوج</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">استخدم %1$s كمزود ولوج</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..812ae26724
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ast/strings.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">D\'acuerdu</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Encaboxar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Evitar qu\'esta páxina cree diálogos adicionales</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Afitar</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Borrar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Aniciu de sesión</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nome d\'usuariu</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contraseña</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Nun guardar</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nun guardar enxamás</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Agora non</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Guardar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Nun anovar</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Anovar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">El campu «Contraseña» nun ha tar baleru</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Nun ye posible guardar la cuenta</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">¿Quies guardar esta cuenta?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">¿Quies anovar esta cuenta?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">¿Quies amestar el nome d\'usuariu a la contraseña guardada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta pa introducir un campu d\'entrada de testu</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Escoyeta d\'un color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Negar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">¿De xuru?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">¿Quies colar d\'esti sitiu? Ye posible que nun se guarden los datos introducíos</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Quedar</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Colar</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Escoyeta d\'un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Xin</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Xun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Xnt</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Och</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Pay</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Avi</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Xestión de cuentes</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Espander les cuentes suxeríes</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Recoyer les cuentes suxeríes</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Cuentes suxeríes</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">¿Quies volver unviar los datos a esti sitiu?</string>
+ <string name="mozac_feature_prompt_repost_message">Anovar esta páxina podría duplicar les aiciones de recién, como unviar un pagu o espublizar dos vegaes un artículu.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Volver unviar</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Encaboxar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleiciona una tarxeta de creitu</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Espander les tarxetes de creitu</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Recoyer les tarxetes de creitu</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Xestión de tarxetes de creitu</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">¿Quies guardar esta tarxeta?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">¿Quies anovar la data de caducidá de la tarxeta?</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Cífrase\'l númberu de la tarxeta, mas nun se guarda\'l códigu de seguranza.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleición d\'unes señes</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Espander les señes suxeríes</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Recoyer les señes suxeríes</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Xestionar les señes</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..b292f9a227
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-az/strings.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Tamam</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Ləğv et</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Bu səhifənin əlavə dialoqlar yaratmasını əngəllə</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Qur</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Təmizlə</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Daxil ol</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">İstifadəçi adı</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Parol</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Saxlama</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Heç vaxt saxlama</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Saxla</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Yeniləmə</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Yenilə</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Parol sahəsi boş olmamalıdır</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Daxil olmanı yadda saxlamaq mümkün olmadı</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Hesab yadda saxlansın?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Hesab yenilənsin?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Saxlanılmış parola istifadəçi adı əlavə edilsin?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Mətn girişi sahəsi üçün etiket</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Rəng seç</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">İcazə ver</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Rədd et</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Əminsiniz?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Saytı tərk etmək istəyirsiniz? Daxil etdiyiniz məlumatlar saxlanmamış ola bilərlər</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Qal</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Tərk et</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Ay seç</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Yan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fev</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">İyn</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">İyl</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Avq</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sen</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Noy</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dek</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Hesabları idarə et</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Məsləhət görülən hesabları genişlət</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Məsləhət görülən hesabları daralt</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Məsləhət görülən hesablar</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Məlumat bu sayta təkrar göndərilsin?</string>
+ <string name="mozac_feature_prompt_repost_message">Bu səhifəni yeniləmə ödəniş və ya şərh yazma kimi son əməliyyatları təkrarlaya bilər.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Təkrar göndər</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Ləğv et</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..c76429897f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-azb/strings.xml
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">تامام</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">لغو</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">بو صفحه‌نین ایضافی دیالوق‌لار یاراتماسی‌نین قارشیسینی آلین</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">تنظیم ‌ائله</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">پوز</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">گیریش</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">قوللانیجی آدی</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">رمز</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">ساخلاما</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">ایندی یوخ</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">هئچ زامان ساخلاما</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ایندی یوخ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">ساخلا</string>
+
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">گونجل‌ ائله‌مه</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">ایندی یوخ</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">گونجلله</string>
+
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">رمز یئرلیکی بوش اولمالیدیر</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">بیر رمز وئر</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">گیریشی ساخلاماق مومکون اولمادی</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">رمزی یاددا ساخلاماق مومکون دئییل</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">بو گیریش یاددا ساخلانسین؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">رمز یاددا ساخلانسین؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">بو گیریش گونجللنسین؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">رمز گونجللنسین؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">قوللانیجی آدی یاددا ساخلانمیش رمزه اکلنسین؟</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">متن گیریش یئرلیکینه گیرمک اتیکتی</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">بیر رنگ سئچ</string>
+
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ایجازه وئر</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">رد </string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">سیز آرخایینسیز؟</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">بو سایتدان آییرلماق ایستییرسیز؟ گیردیگینیز دیتالار یاددا ساخلانیلمایا بیلر.</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">قال</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">آیریل</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">بیر آی سئج</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ژانویه</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">فوریه</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">مارس</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">آپریل</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">می</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">جون</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">جولای</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">آقوست</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">سپتامبر</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">اوکتوبر</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">نووامبر</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">دسامبر</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">چاغ تنظیمی</string>
+
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">اوتوروم‌لاری ایداره ائله</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">رمزلری ایداره ائله</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">توصیه اولونان گیریش‌لری گئنیش‌لت</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">ساخلانمیش رمزلری گئنیش‌لت</string>
+
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">توصیه اولونان گیریش‌لری یئغ</string>
+
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">ساخلانمیش رمزلری یئغ</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">توصیه اولونان گیریش‌لر</string>
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">ساخلانمیش رمزلر</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">گوجلو رمز تکلیف ائدین</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">گوجلو رمز تکلیف ائدین</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">گوجلو رمز ایستیفاده ائدین: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">دیتا بو سایتا یئنی‌دن گؤندریلسین؟</string>
+
+ <string name="mozac_feature_prompt_repost_message">بو صفحه‌نین رفرشی اؤده‌مه گؤندرمک ویا بیر باخیشی ایکی دفعه یایینلاماق کیمی سون حرکت‌لرین تیکرار اولماسینا باعیث اولار.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">دیتانی گئنه گؤندر</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">لغو</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">اعتباری کارتینی سئچین</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">ساخلانمیش کارتدان ایستیفاده ائدین</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">تکلیف اولونان اعتباری کارت‌لاری گئنیش‌لت</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">ساخلانمیش کارت‌لاری گئنیش‌لت</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">تکلیف اولونان اعتباری کارت‌لاری یئغ</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">ساخلانمیش کارت‌لاری یئغ</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">اعتباری کارت‌لاری ایداره ائله</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">کارت‌لاری ایداره ائله</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">بو کارت گوونلی شکیلده یاددا ساخلانسین؟</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">کارتین ایستیفاده موددیتی یئنی‌لنسین؟</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">کارت نومره‌سی رمزلشدیریله‌جک. گوونلیک کودو یاددا ساخلانمیاجاق.</string>
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s کارت نومره‌زی رمزله‌ییر. گوونلیک کودو یاددا ساخلانمیاجاق</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">آدرس سئچین</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">تکلیف اولونان آدرس‌لری گئنیش‌لت</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">ساخلانمیش آدرس‌لری گئنیش‌لت</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">تکلیف اولونان آدرس‌لری یئغ</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">ساخلانمیش آدرس‌لری یئغ</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">آدرس‌لری ایداره ائله</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">حساب عکسی</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">حساب ایرائه‌ وئره‌نی سئچین</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s حسابی ایله گیرین</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s اوتورم آچما ایرائه وئره‌نی کیمی ایشه آلین</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[%2$s حسابی ایله %1$s حسابینا داخیل اولماق اونلارین <a href="%3$s">گیزلیلیک سیاستینه</a> و <a href="%4$s">خیدمت شرطلرینه باخیر. </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">ایدامه وئر</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">لغو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..cc094372e7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ban/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">CUMPU</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Sét</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Puyung</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ten karaksa</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ten mangkin</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Raksa</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Anyarin</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Pilih warna</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Péb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Méi</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Agu</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sép</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dés</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..26e60b96c3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-be/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Добра</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Адмяніць</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Забараніць гэтай старонцы ствараць дадатковыя дыялогі</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Устанавіць</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Ачысціць</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Увайсці</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Імя карыстальніка</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Пароль</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Не захоўваць</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Не зараз</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ніколі не захоўваць</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Не зараз</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Захаваць</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Не абнаўляць</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Не зараз</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Абнавіць</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Поле пароля не павінна быць пустым</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Увядзіце пароль</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Немагчыма захаваць лагін</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Немагчыма захаваць пароль</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Захаваць гэты лагін?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Захаваць пароль?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Абнавіць гэты лагін?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Абнавіць пароль?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Дадаць імя карыстальніка да захаванага пароля?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Метка для тэкставага поля ўводу</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Выберыце колер</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Дазволіць</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Забараніць</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Вы ўпэўнены?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ці хочаце пакінуць гэты сайт? Дадзеныя, якія вы ўвялі, могуць не захавацца</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Застацца</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Выйсці</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Выбраць месяц</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Сту</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Лют</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Сак</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Кра</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Май</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Чэр</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Ліп</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Жні</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Вер</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Кас</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Ліс</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Сне</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Усталяваць час</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Кіраваць лагінамі</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Кіраваць паролямі</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Разгарнуць прапанаваныя лагіны</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Разгарнуць захаваныя паролі</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Згарнуць прапанаваныя лагіны</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Згарнуць захаваныя паролі</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Прапанаваныя лагіны</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Захаваныя паролі</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Прапанаваць надзейны пароль</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Прапанаваць надзейны пароль</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Выкарыстаць надзейны пароль: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Адправіць дадзеныя на гэты сайт паўторна?</string>
+ <string name="mozac_feature_prompt_repost_message">Абнаўленне гэтай старонкі можа паўтарыць апошнія дзеянні, напрыклад, адправіць плацеж або размясціць каментарый двойчы.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Паўторна адправіць дадзеныя</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Адмяніць</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Выберыце крэдытную карту</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Выкарыстаць захаваную карту</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Разгарнуць прапанаваныя крэдытныя карты</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">Разгарнуць захаваныя карты</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Згарнуць прапанаваныя крэдытныя карты</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">Згарнуць захаваныя карты</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Кіраванне крэдытнымі картамі</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Кіраваць картамі</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Захаваць надзейна гэту карту?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Абнавіць тэрмін дзеяння карты?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Нумар карты будзе зашыфраваны. Код бяспекі не будзе захаваны.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s шыфруе нумар вашай карты. Ваш код бяспекі не будзе захаваны.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Выбраць адрас</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Разгарнуць прапанаваныя адрасы</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Разгарнуць захаваныя адрасы</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Згарнуць прапанаваныя адрасы</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Згарнуць захаваныя адрасы</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Кіраваць адрасамі</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Відарыс уліковага запісу</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Выберыце правайдара ўваходу</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Увайсці з уліковым запісам %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Выкарыстоўваць %1$s у якасці правайдара ўваходу</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Уваход у %1$s з уліковым запісам %2$s рэгулюецца іх <a href="%3$s">палітыкай прыватнасці</a> і <a href="%4$s">умовамі выкарыстання</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Працягнуць</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Скасаваць</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..60cb450032
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-bg/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Добре</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Отказ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Забраняване на страницата да създава нови диалогови прозорци</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Задаване</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Изчистване</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Вписване</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Потребителско име</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Парола</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Без запазване</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Не сега</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Без запазване</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Не сега</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Запазване</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Без обновяване</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Не сега</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Обновяване</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Полето за парола не трябва да е празно</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Въведете парола</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Данните за вход не могат да бъдат запазени</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Паролата не може да бъде запазена</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Запазване на регистрацията?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Запазване на паролата?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Обновяване на регистрацията?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Актуализиране на паролата?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Добавяне на потребител към запазената парола?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Етикет към поле изискващо въвеждане</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Избиране на цвят</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Разрешаване</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Забраняване</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Сигурни ли сте?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Желаете ли да напуснете страницата? Въведените данни може да не са запазени</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Оставане</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Напускане</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Избор на месец</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ян</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">февр</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">март</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">апр</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">май</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">юни</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">юли</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">авг</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">септ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">окт</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ноем</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">дек</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Задаване на време</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Управление на регистрации</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Управление на пароли</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Разгъване на предложени регистрации</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Разгъване на запазените пароли</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Сгъване на предложени регистрации</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Сгъване на запазените пароли</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Предложени регистрации</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Запазени пароли</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Предлагане на силна парола</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Предлагане на силна парола</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Използвайте силна парола: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Повторно изпращане на данни до страницата?</string>
+ <string name="mozac_feature_prompt_repost_message">Презареждането на страницата може да дублира скорошни действия, като заплащане или публикуване на коментар два пъти.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Повторно изпращане на данни</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Отказ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Изберете банкова карта</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Използване на запазена карта</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Разгъва предложения списък с банкови карти</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">Разгъване на запазените карти</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Сгъва предложения списък с банкови карти</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">Сгъване на запазените карти</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Управление на банкови карти</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Управление на карти</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Защитено запазване на картата?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Обновяване на валидността на картата?</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Номерът на карата ще бъде шифрован. Кодът за сигурност няма да бъде запазен.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s шифрова номера на картата ви. Кодът ви за сигурност няма да бъде запазен.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Изберете адрес</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Разгъване на предложени адреси</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Разгъване на запазените адреси</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Свиване на предложени адреси</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Сгъване на запазените адреси</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Управление на адреси</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Профилна снимка</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Изберете доставчик на вход</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Вход в профила на %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Използвайте %1$s като доставчик на вход</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Входът в профила на %1$s с/ъс %2$s е предмет на тяхната <a href="%3$s">Политика за личните данни</a> и техните <a href="%4$s">Общи условия</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Продължаване</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Отказ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..16a8f63c2b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-bn/strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ঠিক আছে</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">বাতিল</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">অতিরিক্ত ডায়ালগ তৈরি করা থেকে এই পাতাটিকে বিরত রাখুন</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">নির্ধারণ</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">পরিষ্কার</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">সাইন ইন</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ব্যবহারকারীর নাম</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">পাসওয়ার্ড</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">সংরক্ষণ করবেন না</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">কখনও সংরক্ষণ করবেন না</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">সংরক্ষণ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">হালনাগাদ করবেন না</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">আপডেট</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">পাসওয়ার্ডের জায়গাটি খালি রাখা যাবে না</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">লগইন সংরক্ষণ করা যায়নি</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">এই লগইন সংরক্ষণ করবেন?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">এই লগইন হালনাগাদ করবেন?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">সংরক্ষিত পাসওয়ার্ডে ব্যবহারকারীর নাম যুক্ত করবেন?</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">একটি রঙ নির্বাচন করুন</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">অনুমতি দিন</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">প্রত্যাখান করুন</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">আপনি কি নিশ্চিত?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">আপনি কি এই সাইটটি ছেড়ে যেতে চান? আপনার প্রবেশ করা ডাটা সংরক্ষণ নাও হতে পারে</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">থাকুন</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ত্যাগ করুন</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">একটি মাস বাছাই করুন</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">লগইন ব্যবস্থাপনা করুন</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">প্রস্তাবিত লগইনগুলি সম্প্রসারিত করুন</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">প্রস্তাবিত লগইনগুলি সংকুচিত করুন</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">প্রস্তাবিত লগইনগুলি</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">সাইটে আবার তথ্য পাঠাবে?</string>
+ <string name="mozac_feature_prompt_repost_message">এই পৃষ্ঠাটি রিফ্রেশ করলে সাম্প্রতিক কাজ আবার হতে পারে যেমন কোনও অর্থ প্রদান বা দুইবার মন্তব্য পোস্ট করা।</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">তথ্য আবার পাঠাও</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">বাতিল</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..ee06036029
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-br/strings.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Mat eo</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Nullañ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Mirout ar bajennad-mañ ouzh krouiñ boestadoù emziviz ouzhpenn</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Arventennañ</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Skarzhañ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Kennaskañ</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Anv arveriad</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Ger-tremen</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Na enrollañ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Diwezhatoc’h</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Na enrollañ biken</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ket bremañ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Enrollañ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Na hizivaat</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Diwezhatoc’h</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Hizivaat</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Ar vaezienn ger-tremen a rank bezañ leuniet</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Enankit ur ger-tremen</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Ne cʼhaller ket enrollañ an titour kennaskañ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">N’haller ket enrollañ ar ger-tremen</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Enrollañ an titour kennaskañ-mañ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Enrollañ ar ger-tremen?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Hizivaat an titour kennaskañ-mañ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Hizivaat ar ger-tremen?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Ouzhpennañ an anv arveriad dʼar gerioù-tremen enrollet?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Tikedenn evit ur vaezienn destenn.</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Dibab ul liv</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Aotren</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Nacʼhañ</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Sur ocʼh?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Fellout a ra deocʼh kuitaat al lecʼhienn-mañ? Ne vo ket enrollet ar roadennoù bet enanket ganeocʼh</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Chom</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Kuitaat</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Dibab ur miz</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Gen</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Cʼhw</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Meu</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Ebr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mae</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Mez</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Gou</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Eos</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Gwe</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Her</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Du</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Ker</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Dibab an eur</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Ardoer titouroù kennaskañ</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Merañ ar gerioù-tremen</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Brasaat an titouroù kennaskañ aliet</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Dispakañ ar gerioù-tremen enrollet</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Berraat an titouroù kennaskañ aliet</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Kuzhat ar gerioù-tremen enrollet</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Titouroù kennaskañ aliet</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Gerioù-tremen enrollet</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Kas roadennoù en-dro d’al lec’hienn?</string>
+ <string name="mozac_feature_prompt_repost_message">Azbevaat ar bajenn a c’hallfe eilañ ar gweredoù nevez, evel kas ur paeamant pe embann un evezhiadenn div wech.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Adkas roadennoù</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Nullañ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Diuzañ ur gartenn gred</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Astenn ar cʼhartennoù kred kinniget</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Dispakañ ar c’hartennoù enrollet</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Bihanaat ar cʼhartennoù kred kinniget</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Kuzhat ar c’hartennoù enrollet</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Merañ ar cʼhartennoù kred</string>
+
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Merañ ar c’hartennoù</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Enrollañ ar gartenn-mañ en surentez?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Hizivaat deiziad termen ar gartenn?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Enrigenet e vo niverenn ar gartenn. Ne vo ket enrollet ar c’hod surentez.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Dibab ur chomlec’h</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Displegañ ar chomlec’hioù kinniget</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Dispakañ ar chomlec’hioù enrollet</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Plegañ ar chomlec’hioù kinniget</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Kuzhat ar chomlec’hioù enrollet</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Merañ ar chomlec’hioù</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Skeudenn ar gont</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Dibab ur pourchaser dilesa</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Kennaskañ gant ur gont %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Ober gant %1$s evel pourchaser dilesa</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Kennaskañ ouzh %1$s gant ur gont %2$s a zo reolennet gant o <a href="%3$s">Reolenn a-fet buhez prevez</a> ha <a href="%4$s">divizoù arver</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Kenderc’hel</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Nullañ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..d43b77b4a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-bs/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Otkaži</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Spriječi ovu stranicu da kreira dodatne dijaloge</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Postavi</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Očisti</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Prijava</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Korisničko ime</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Lozinka</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Nemoj spasiti</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ne sada</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nikad ne spašavaj</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ne sada</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Spasi</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Nemoj ažurirati</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ne sada</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Ažuriraj</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Polje za lozinku ne smije biti prazno</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Unesite lozinku</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Ne mogu spasiti prijavu</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Nije moguće sačuvati lozinku</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Spasi ovu prijavu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Sačuvati lozinku?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Ažurirati ovu prijavu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Ažurirati lozinku?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Dodaj korisničko ime uz sačuvanu lozinku?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Oznaka za unos u tekstualno polje</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Izaberite boju</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Dozvoli</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Odbij</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Da li ste sigurni?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Želite li napustiti ovu stranicu? Podaci koje ste unijeli možda neće biti sačuvani</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Ostani</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Napusti</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Izaberite mjesec</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Postavi vrijeme</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Upravljanje prijavama</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Upravljajte lozinkama</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Proširi predložene prijave</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Proširi sačuvane lozinke</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Sažmi predložene prijave</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Sažmi sačuvane lozinke</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Predložene prijave</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Sačuvane lozinke</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Predloži jaku lozinku</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Predloži jaku lozinku</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Koristite jaku lozinku: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Ponovo poslati podatke na ovu stranicu?</string>
+ <string name="mozac_feature_prompt_repost_message">Osvježavanje ove stranice moglo bi ponoviti nedavne radnje, kao što su dvostruko plaćanje ili komentarisanje.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Ponovo pošalji podatke</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Otkaži</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Odaberite kreditnu karticu</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Koristi sačuvanu karticu</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Proširite predložene kreditne kartice</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Proširi sačuvane kartice</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Sažmi predložene kreditne kartice</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Sažmi sačuvane kartice</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Upravljaj kreditnim karticama</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Upravljajte karticama</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Sigurno sačuvati ovu karticu?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Ažuriraj datum isteka kartice?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Broj kartice će biti šifrovan. Sigurnosni kod neće biti sačuvan.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s šifruje broj vaše kartice. Vaš sigurnosni kod neće biti sačuvan.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Odaberi adresu</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Proširite predložene adrese</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Proširi sačuvane adrese</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Sažmi predložene adrese</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Sažmi sačuvane adrese</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Upravljaj adresama</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Slika računa</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Odaberi provajdera za prijavu</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Prijavite se sa %1$s računom</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Koristite %1$s kao provajdera za prijavu</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Prijava na %1$s sa %2$s računom podliježe njihovoj <a href="%3$s">Politici privatnosti</a> i <a href="%4$s">Uslovima korištenja usluge </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Nastavi</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Otkaži</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..63d52c09b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ca/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">D’acord</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancel·la</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Evita que aquesta pàgina creï més diàlegs</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Defineix</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Esborra</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Inicia la sessió</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nom d’usuari</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contrasenya</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No desis</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ara no</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">No desis mai</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ara no</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Desa</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No actualitzis</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ara no</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualitza</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">El camp de la contrasenya no pot estar buit</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Escriviu una contrasenya</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No es pot desar l’inici de sessió</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">No s\'ha pogut desar la contrasenya</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Voleu desar aquest inici de sessió?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Voleu desar la contrasenya?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Voleu actualitzar aquest inici de sessió?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Voleu actualitzar la contrasenya?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Voleu afegir el nom d’usuari a la contrasenya desada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta per introduir un camp d’entrada de text</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Trieu un color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permet</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Denega</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Segur?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Voleu sortir d’aquest lloc? És possible que no es desin les dades que hàgiu introduït</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">No surtis</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Surt</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Trieu un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">gen.</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">febr.</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">març</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">abr.</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">maig</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">juny</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">jul.</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ag.</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">set.</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">oct.</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nov.</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">des.</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Defineix l’hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gestiona els inicis de sessió</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gestiona les contrasenyes</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Amplia els inicis de sessió suggerits</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Amplia les contrasenyes desades</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Redueix els inicis de sessió suggerits</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Redueix les contrasenyes desades</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Inicis de sessió suggerits</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Contrasenyes desades</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggereix una contrasenya segura</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggereix una contrasenya segura</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Useu una contrasenya segura: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Voleu tornar a enviar les dades a aquest lloc?</string>
+ <string name="mozac_feature_prompt_repost_message">Actualitzar aquesta pàgina pot provocar la repetició de les accions recents, com ara enviar un pagament o publicar un comentari dues vegades.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Torna a enviar les dades</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancel·la</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleccioneu una targeta de crèdit</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Usa una targeta desada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Amplia les targetes de crèdit suggerides</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Amplia les targetes desades</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Redueix les targetes de crèdit suggerides</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Redueix les targetes desades</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gestiona les targetes de crèdit</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gestiona les targetes</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Voleu desar aquesta targeta de forma segura?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Voleu actualitzar la data de caducitat de la targeta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">El número de targeta es xifrarà. El codi de seguretat no es desarà.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s xifra el número de la targeta. El codi de seguretat no es desarà.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Trieu l’adreça</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Amplia les adreces suggerides</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Amplia les adreces desades</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Redueix les adreces suggerides</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Redueix les adreces desades</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gestiona les adreces</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imatge del compte</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Trieu un proveïdor d\'inici de sessió</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Inicia la sessió amb un compte de %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Usa %1$s com a proveïdor d\'inici de sessió</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[L\'inici de sessió a %1$s amb un compte de %2$s està subjecte a la seva <a href="%3$s">Política de privadesa</a> i a les <a href="%4$s">Condicions del servei </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continua</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancel·la</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..aae206423e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-cak/strings.xml
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ÜTZ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Tiq\'at</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Man tiya\' q\'ij chi re ruxaq re\' yerunük\' kitz\'aqat taq tzijonem</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Tib\'an runuk\'ulem</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Tijosq\'ïx</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Titikirisäx molojri\'ïl</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Rub\'i\' winäq</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Ewan tzij</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Man tiyak</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Wakami mani</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Majub\'ey tiyak</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Wakami mani</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Tiyak</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Man tik\'ex ruwäch</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Wakami mani</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Tik\'ex</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Man tikirel ta kowöl ri ruk\'ojlem ewan tzij</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Titz\'ib\'äx jun ewan tzij</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Man xtikïr ta xyak rutikirib\'al molojri\'ïl</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Man tikirel ta niyak ri ewan tzij</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">¿La niyak rutikirib\'al re moloj re\'?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">¿La niyak ewan tzij?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">¿La nik\'ex rutikirib\'al re moloj re\'?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">¿La nik\'ex ri ewan tzij?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">¿La nitz\'aqatisäx rub\'i\' winäq pa ri ewan tzij yakon?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etal richin ninim jun ruk\'ojlem rokem tz\'ib\'anïk</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Ticha\' jun b\'onil</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Tiya\' q\'ij</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Tiq\'at</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">¿La kan at jikïl?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">¿La nawajo\' yatel chupam re ruxaq re\'? Rik\'in jub\'a\' man xkeyak ta ri taq tzij xawokisaj</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Tik\'oje\' na</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Tel</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Tacha\' jun ik\'</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ju\'ik\'</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Ka\'ik\'</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Oxik\'</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Kajik\'</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Ro\' ik\'</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Waqik\'</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Wuqik\'</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Waqxaqik\'</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">B\'elejik\'</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Lajik\'</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Julajik\'</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Kab\'lajik\'</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Tib\'an ruk\'ojlem wakami</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Tinuk\'samajïx rutikirib\'al molojri\'ïl</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Kenuk\'samajïx ewan taq tzij</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Kerik\' ri chilab\'en tikirib\'äl taq molojri\'ïl</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Kerik\' kij ri yakon ewan taq tzij</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Kek\'ol ri chilab\'en tikirib\'äl taq molojri\'ïl</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Kek\'ol ri yakon ewan taq tzij</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Chilab\'en taq molojri\'ïl</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Xeyak ewan taq tzij</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Tichilab\'ëx ütz ewan tzij</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Tichilab\'ëx ütz ewan tzij</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Ke\'awokisaj ütz ewan taq tzij: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">¿La nitaq chik tzij chi re re ruxaq?</string>
+ <string name="mozac_feature_prompt_repost_message">Nisamajïx chik re ruxaq rik\'in jub\'a\' nukamuluj samaj k\'a jub\'a\' b\'anon, achi\'el nitaq jun tojïk o kamul nrelesaj rutzijol jun na\'oj.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Titaq chik tzij</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Tiq\'at</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Ticha\' rutarjeta\' kre\'ito\'</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Tokisäx yakon tarjeta\'</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Kerik\' ri chilab\'en rutarjeta\' kre\ito\'</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">Kerik yakon taq tarjeta\'</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Kek\'ol ri chilab\'en rutarjeta\' kre\ito\'</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">Kek\'ol yakon taq tarjeta\'</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Kenuk\'samajïx kikre\'ito\' taq tarjeta\'</string>
+
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Kenuk\'samajïx taq tarjeta\'</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">¿Jikïl tayaka\' re tarjeta\' re\'?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">¿Tik\'ex ruq\'ijul nik\'is tarjeta\'?</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Ri rajilab\'al tarjeta\' ewan rusik\'ixik. Man xtiyake\' ta ri rub\'itz\'ib\' jikomal.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s nrewaj rusik\'ixij ri rajilab\'al atarjeta\'. Ri jikon rub\'itz\'ib\' man xtiyake\' ta kan.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Ticha\' ochochib\'äl</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Kerik\' ri chilab\'en ochochib\'äl</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Kerik\' yakon taq ochochib\'äl</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Kek\'ol ri chilab\'en ochochib\'äl</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Kek\'ol yakon taq ochochib\'äl</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Kenuk\'samajïx taq ochochib\'äl</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Ruwachib\'al rub\'i\' taqoya\'l</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Ticha\' jun ruya\'oj rutikirib\'al moloj</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Titikirisäx moloj rik\'in jun rub\'i\' rutaqoya\'l %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Tokisäx %1$s achi\'el ya\'öl rutikirib\'al moloj</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Tatikirisaj moloj pa %1$s rik\'in jun rub\'i\' rutaq\'oya\'l %2$s ruximon ri\' rik\'in ri <a href="%3$s">Runa\'ojil ichinanem</a> chuqa\' <a href="%4$s">Rutzijol Samak</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Titikïr chik el</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Tiq\'at</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..86e88d54d6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancel</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Pug-ngi ni nga page magbuhat ug dugang mga dialog</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Set</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Clear</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Sign in</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Username</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ayaw i-save</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Dili mag-save</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Save</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ayaw i-update</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Update</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Ang Password field dapat naa\'y sulod</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Dili makasave sa login</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">i-Save ni nga login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">i-Update ni nga login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">i-Dugang ang na-save nga password?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label sa pag-butang ug text input field</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Pili ug color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Allow</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Deny</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Sigurado ka?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Buot mo bang mohawa ani nga site? Ang data nga imong gi-butang basin dili ma-save</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Stay</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Leave</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Pili ug bulan</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">i-Manage ang mga login</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">i-Expand ang nasugyot nga mga login</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">i-Collapse ang nasugyot nga mga login</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Nasugyot nga mga login</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">i-Padala usab ang mga data dinhi nga site?</string>
+ <string name="mozac_feature_prompt_repost_message">Pag-refresh ani nga page posibleng maka-kopya sa bag-o nga mga aksyon, sama sa pagpadala ug bayad o pag-post sa usa ka komento makaduha. </string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Resend data</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancel</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Pili ug credit card</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">i-Expand ang nasugyot nga mga credit card</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">i-Collapse ang nasugyot nga mga credit card</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings -->
+ <string name="mozac_feature_prompts_manage_credit_cards">i-Manage ang mga credit card</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..20a7b4aa1d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">باشە</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">پاشگەزبوونەوە</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ڕێگە مەدە ئەم پەڕەیە داواکردنی زیاتر بکاتەوە</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">دیاریبکە</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">پاککردنەوە</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">بچۆژوورەوە</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ناوی بەکارهێنەر</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">وشەی تێپەڕبوون</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">پاشەکەوتی مەکە</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">هەرگیز پاشەکەوت مەکە</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">پاشەکەوتکردن</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">نوێی مەکەرەوە</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">نوێکردنەوە</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">خانەی وشەی تێپەڕبوون نابێت بەتاڵ بێت</string>
+
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">نەتوانرا چوونەژوورەوە پاشەکەوت بکرێت</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ئەم چوونەژوورەوەیە پاشەکەوت دەکەیت؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ئەم چوونەژوورەوە نوێدەکەیتەوە؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">زیادکردنی ناوی بەکارهێنەر بۆ وشەی تێپەڕی هەڵگیراو؟</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ڕەنگێک هەڵبژێرە</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ڕێگەبدە</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ڕێگەمەدە</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">ئایا تۆ دڵنیایت؟</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">دەتەویت ئەم ماڵپەڕە بەجێبهێڵیت؟ ئەو زانیارییانەی نووسیوتن لێرە لەوانەیە هەڵنەگیرێن.</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">بمێنەرەوە</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">جێیبهێلە</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">مانگێک هەڵبژێرە</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">کانوونی دووەم</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">شوبات</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">ئازار</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">نیسان</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">ئایار</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">حوزەیران</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">تەمموز</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ئاب</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ئەیلوول</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">تشرینی یەکەم</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">تشرینی دووەم</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">کانوونی یەکەم</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">بەڕێوەبردنی چوونەژوورەوەکان</string>
+
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">چوونەژوورەوە پێشنیارکراوەکان</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">زانیاری دەنێریتەوە بۆ ئەم ماڵپەڕە؟</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ناردنەوەی زانیاری</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">پاشگەزبوونەوە</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..7afbd91b65
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-co/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Vai</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Abbandunà</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Impedisce sta pagina d’apre dialoghi addiziunali</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Definisce</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Squassà</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Cunnettesi</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nome d’utilizatore</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Parolla d’intesa</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ùn arregistrà micca</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Micca subitu</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ùn arregistrà mai</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Micca subitu</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Arregistrà</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ùn micca rinnovà</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Micca subitu</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Piglià in contu</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">U campu di a parolla d’intesa ùn deve micca esse viotu</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Stampittate una parolla d’intesa</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Impussibule d’arregistrà l’identificazione di cunnessione</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Ùn si pò micca arregistrà a parolla d’intesa</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Arregistrà st’identificazioni di cunnessione</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Arregistrà a parolla d’intesa ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Mudificà st’identificazione di cunnessione ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Mudificà a parolla d’intesa ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Aghjunghje un nome d’utilizatore à a parolla d’intesa arregistrata ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Discrizzione per a creazione d’un campu di scrittura di testu</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Sceglie un culore</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permette</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Ricusà</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Cunfirmazione</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vulete abbandunà stu situ ? Certi dati chì vo avete stampittati puderianu esse persi</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Stà</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Abbandunà</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Sceglie un mese</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ghje.</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ferr.</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">marzu</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">apri.</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">magh.</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ghju.</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">lugl.</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">aostu</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">sitt.</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">utto.</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nuve.</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">dice.</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Sceglie l’ora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Urganizà l’identificazioni di cunnessione</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Amministrà e parolle d’intesa</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Spiegà l’identificazioni di cunnessione suggerite</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Spiegà e parolle d’intesa arregistrate</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Ripiegà l’identificazioni di cunnessione suggerite</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Ripiegà e parolle d’intesa arregistrate</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Identificazioni di cunnessione suggerite</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Parolle d’intesa arregistrate</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggerisce una parolla d’intesa forte</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggerisce una parolla d’intesa forte</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Impiegà a parolla d’intesa forte : %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Rimandà i dati à stu situ ?</string>
+ <string name="mozac_feature_prompt_repost_message">L’attualizazione di sta pagina puderia ripete azzioni recente, cum’è l’aviu d’un pagamentu o a publicazione d’un cummentu in doppiu.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Rimandà i dati</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Abbandunà</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Selezziunà una carta bancaria</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Impiegà una carta arregistrata</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Spiegà e carte bancarie suggerite</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Spiegà e carte arregistrate</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Ripiegà e carte bancarie suggerite</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Ripiegà e carte arregistrate</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Urganizà e carte bancarie</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Amministrà e carte</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Arregistrà sta carta di manera sicura ?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Mudificà a data di scadenza di a carta ?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">U numeru di a carta serà cifratu. U codice di sicurità ùn serà micca arregistratu.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s cifra u vostru numeru di carta. U vostru codice di sicurità ùn serà micca arregistratu.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Selezziunà un indirizzu</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Spiegà l’indirizzi suggeriti</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Spiegà l’indirizzi arregistrati</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Ripiegà l’indirizzi suggeriti</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Ripiegà l’indirizzi arregistrati</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Urganizà l’indirizzi</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Fiura per u contu</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Sceglie un furnidore d’accessu</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Cunnittitevi cù un contu %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Impiegà %1$s cum’è furnidore di cunnessione</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Cunnettesi à %1$s cù un contu %2$s hè sottumessu à a so <a href="%3$s">pulitica di cunfidenzialità</a> è à e so <a href="%4$s">cundizioni d’utilizazione</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Cuntinuà</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Abbandunà</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..2a2beceeb8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-cs/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Zrušit</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Zabránit stránce ve vytváření dalších dialogů</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Nastavit</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Vymazat</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Přihlášení</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Uživatelské jméno</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Heslo</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Neukládat</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Teď ne</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nikdy neukládat</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Teď ne</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Uložit</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Neaktualizovat</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Teď ne</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Aktualizovat</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Heslo nesmí být prázdné</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Zadejte heslo</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Přihlašovací údaje nelze uložit</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Heslo není možné uložit</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Uložit tyto přihlašovací údaje?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Uložit heslo?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Aktualizovat tyto přihlašovací údaje?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Aktualizovat heslo?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Přidat k uloženému heslu uživatelské jméno?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Štítek vstupního pole pro zadání textu</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Vyberte barvu</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Povolit</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Zakázat</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Opravdu?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Opravdu chcete tuto stránku opustit? Zadané údaje se nemusí uložit</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Zůstat</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Opustit</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Vyberte měsíc</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Led</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Úno</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Bře</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Dub</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Kvě</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Čvn</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Čvc</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Srp</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Zář</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Říj</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Lis</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Pro</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Nastavit čas</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Správa přihlašovacích údajů</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Správa přihlašovacích údajů</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Zobrazit navrhované přihlašovací údaje</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Rozbalit uložená hesla</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Skýt navrhované přihlašovací údaje</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Sbalit uložená hesla</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Navrhované přihlašovací údaje</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Uložená hesla</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Navrhnout silné heslo</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Navrhnout silné heslo</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Použít silné heslo: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Chcete znovu odeslat data tomuto serveru?</string>
+ <string name="mozac_feature_prompt_repost_message">Opětovné načtení této stránky může zopakovat vaši nedávnou akci, například druhé odeslání stejné platby nebo komentáře.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Odeslat</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Zrušit</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Vyberte platební kartu</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Použít uloženou kartu</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Zobrazit návrhy platebních karet</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">Rozbalit uložené karty</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Skrýt návrhy platebních karet</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">Sbalit uložené karty</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Správa platebních karet</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Spravovat karty</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Bezpečně uložit tuto kartu?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Aktualizovat platnost karty?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Číslo karty bude uložené šifrované. Bezpečnostní kód uložen nebude.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s zašifruje číslo vaší karty. Váš bezpečnostní kód nebude uložen.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Vyberte adresu</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Zobrazit navrhované adresy</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Rozbalit uložené adresy</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Skrýt navrhované adresy</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Sbalit uložené adresy</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Správa adres</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Obrázek účtu</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Výběr poskytovatele přihlášení</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Přihlášení pomocí účtu %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Použití %1$s jako poskytovatele přihlášení</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Přihlášení k %1$s pomocí účtu %2$s podléhá jejich <a href="%3$s">zásadám ochrany osobních údajů</a> a <a href="%4$s">podmínkám poskytování služby</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Pokračovat</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Zrušit</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..d6e70ad31a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-cy/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Iawn</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Diddymu</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Atal y dudalen hon rhag creu deialogau ychwanegol</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Gosod</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Clirio</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Mewngofnodi</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Enw Defnyddiwr</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Cyfrinair</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Peidio â chadw</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Nid nawr</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Byth cadw</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Nid nawr</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Cadw</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Peidio â diweddaru</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Nid nawr</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Diweddaru</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Rhaid i faes cyfrinair beidio â bod yn wag</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Rhowch gyfrinair</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Methu cadw mewngofnod</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Methu cadw cyfrinair</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Cadw’r mewngofnod hwn?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Cadw cyfrinair?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Diweddaru’r mewngofnod hwn?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Diweddaru cyfrinair?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Ychwanegu enw defnyddiwr i gyfrinair wedi’i gadw?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label ar gyfer mynd i faes mewnbwn testun</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Dewis lliw</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Caniatáu</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Gwrthod</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ydych chi’n siŵr?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ydych chi eisiau gadael y wefan hon? Efallai na fydd data rydych chi wedi’i roi’n cael ei gadw</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Aros</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Gadael</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Dewis mis</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ion</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Chw</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Maw</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Ebr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Meh</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Gor</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Awst</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Med</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Hyd</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Tach</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Rhag</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Gosod yr amser</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Rheoli mewngofnodion</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Rheoli cyfrineiriau</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Ehangu’r mewngofnodion</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Ehangu cyfrineiriau sydd wedi\'u cadw</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Lleihau’r mewngofnodion</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Cau cyfrineiriau sydd wedi\'u cadw</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Mewngofnodion</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Cyfrineiriau wedi\'u cadw</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Awgrym o gyfrinair cryf</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Awgrym o gyfrinair cryf</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Defnyddiwch gyfrinair cryf: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Ail-anfon data i’r wefan hon?</string>
+ <string name="mozac_feature_prompt_repost_message">Gall adnewyddu’r dudalen hon ddyblygu gweithredoedd diweddar, fel anfon taliad neu gofnodi sylw ddwywaith.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Ail-anfon data</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Diddymu</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Dewiswch gerdyn credyd</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Defnyddio cerdyn sydd wedi\'i gadw</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Ehangu awgrymiad y cardiau credyd</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Ehangu cardiau sydd wedi\'u cadw</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Lleihau awgrymiad y cardiau credyd</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Cau cardiau sydd wedi\'u cadw</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Rheoli cardiau credyd</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Rheoli cardiau</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Cadw’r cerdyn hwn yn ddiogel?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Diweddaru dyddiad dod i ben cerdyn?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Bydd rhif y cerdyn yn cael ei amgryptio. Ni fydd y cod diogelwch yn cael ei gadw.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">Mae %s yn amgryptio rhif eich cerdyn. Ni fydd eich cod diogelwch yn cael ei gadw.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Dewiswch gyfeiriad</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Ehangu awgrymiadau cyfeiriadau</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Ehangu cyfeiriadau sydd wedi\'u cadw</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Lleihau awgrymiadau cyfeiriadau</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Cau cyfeiriadau sydd wedi\'u cadw</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Rheoli cyfeiriadau</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Llun cyfrif</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Dewiswch ddarparwr mewngofnodi</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Mewngofnodwch gyda chyfrif %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Defnyddiwch %1$s fel darparwr mewngofnodi</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Mae mewngofnodi i %1$s gyda chyfrif %2$s yn amodol ar eu <a href="%3$s">Polisi Preifatrwydd</a> a <a href="%4$s">Thelerau Gwasanaeth </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Parhau</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Diddymu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..df13afc653
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-da/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Annuller</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Fjern denne sides mulighed for at oprette flere dialogbokse</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Indstil</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Ryd</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Log ind</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Brugernavn</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Adgangskode</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Gem ikke</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ikke nu</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Gem aldrig</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ikke nu</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Gem</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Opdater ikke</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ikke nu</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Opdater</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Feltet Adgangskode må ikke være tomt</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Indtast en adgangskode</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Kunne ikke gemme login</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Kan ikke gemme adgangskode</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Gem dette login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Gem adgangskode?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Opdater dette login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Opdater adgangskode?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Føj brugernavn til gemt adgangskode?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiket for indtastning af tekst i et input-felt</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Vælg en farve</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Tillad</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Afvis</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Er du sikker?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vil du forlade dette websted? Data, du har indtastet, gemmes muligvis ikke</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Bliv</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Forlad</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Vælg en måned</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Vælg tidspunkt</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Håndter logins</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Håndter adgangskoder</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Udvid foreslåede logins</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Udvid gemte adgangskoder</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Sammenfold foreslåede logins</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Sammenfold gemte adgangskoder</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Foreslåede logins</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Gemte adgangskoder</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Foreslå stærk adgangskode</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Foreslå stærk adgangskode</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Brug stærk adgangskode: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Send data igen til dette websted?</string>
+ <string name="mozac_feature_prompt_repost_message">Genindlæsning af denne side kan gentage nylige handlinger, fx sådan at en betaling udføres igen eller en kommentar sendes to gange.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Send data igen</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Annuller</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Vælg betalingskort</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Brug gemt kort</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Udvid foreslåede betalingskort</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Udvid gemte kort</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Sammenfold foreslåede betalingskort</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Sammenfold gemte kort</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Håndter betalingskort</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Håndter kort</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Gem dette kort sikkert?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Opdater kortets udløbsdato?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Kortnummeret vil blive krypteret. Sikkerhedskoden vil ikke blive gemt.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s krypterer dit kortnummer. Din sikkerhedskode bliver ikke gemt.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Vælg adresse</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Udvid foreslåede adresser</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Udvid gemte adresser</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Sammenfold foreslåede adresser</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Sammenfold gemte adresser</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Håndter adresser</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Kontobillede</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Vælg en login-udbyder</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Log ind med en %1$s-konto</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Brug %1$s som login-udbyder</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Indlogning på %1$s med en %2$s-konto er underlagt deres <a href="%3$s">privatlivspolitik</a> og <a href="%4$s">tjenestevilkår</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Fortsæt</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Annuller</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..de995ecc07
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-de/strings.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Abbrechen</string>
+
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Diese Seite daran hindern, weitere Dialoge zu öffnen</string>
+
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Übernehmen</string>
+
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Leeren</string>
+
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Anmelden</string>
+
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Benutzername</string>
+
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Passwort</string>
+
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Nicht speichern</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Nicht jetzt</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nie speichern</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Nicht jetzt</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Speichern</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Nicht aktualisieren</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Nicht jetzt</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Aktualisieren</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Das Passwortfeld darf nicht leer bleiben</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Passwort eingeben</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Zugangsdaten konnten nicht gespeichert werden</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Passwort kann nicht gespeichert werden</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Diese Zugangsdaten speichern?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Passwort speichern?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Diese Zugangsdaten aktualisieren?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Passwort aktualisieren?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Benutzernamen zum gespeicherten Passwort hinzufügen?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Beschriftung für ein Texteingabefeld</string>
+
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Wählen Sie eine Farbe</string>
+
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Erlauben</string>
+
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Ablehnen</string>
+
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Sind Sie sicher?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Möchten Sie diese Seite verlassen? Von Ihnen eingegebene Daten werden möglicherweise nicht gespeichert</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Bleiben</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Verlassen</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Monat auswählen</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mär</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dez</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Zeit einstellen</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Zugangsdaten verwalten</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Passwörter verwalten</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Vorgeschlagene Zugangsdaten ausklappen</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Gespeicherte Passwörter ausklappen</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Vorgeschlagene Zugangsdaten einklappen</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Gespeicherte Passwörter einklappen</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Vorgeschlagene Zugangsdaten</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Gespeicherte Passwörter</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Starkes Passwort vorschlagen</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Starkes Passwort vorschlagen</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Verwenden Sie ein starkes Passwort: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Daten erneut an diese Website senden?</string>
+ <string name="mozac_feature_prompt_repost_message">Durch das Aktualisieren dieser Seite können die letzten Aktionen doppelt ausgeführt werden, z.&amp;thinsp;B. das Senden einer Zahlung oder das zweimalige Posten eines Kommentars.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Erneut senden</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Abbrechen</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Kreditkarte auswählen</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Eine gespeicherte Karte verwenden</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Vorgeschlagene Kreditkarten ausklappen</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Gespeicherte Karten ausklappen</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Vorgeschlagene Kreditkarten einklappen</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Gespeicherte Karten einklappen</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Kreditkarten verwalten</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Karten verwalten</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Soll diese Karte sicher gespeichert werden?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Ablaufdatum der Karte aktualisieren?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Die Kartennummer wird verschlüsselt. Der Sicherheitscode wird nicht gespeichert.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s verschlüsselt Ihre Kartennummer. Ihr Sicherheitscode wird nicht gespeichert.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Adresse auswählen</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Vorgeschlagene Adressen ausklappen</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Gespeicherte Adressen ausklappen</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Vorgeschlagene Adressen einklappen</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Gespeicherte Adressen einklappen</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Adressen verwalten</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Kontobild</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Wählen Sie einen Login-Anbieter</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Melden Sie sich mit einem %1$s-Konto an</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s als Login-Anbieter verwenden</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Die Anmeldung bei %1$s mit einem %2$s-Konto unterliegt deren <a href="%3$s">Datenschutzerklärung</a> und <a href="%4$s">Nutzungsbedingungen </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Weiter</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Abbrechen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..36188bb0bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">W pórěźe</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Pśetergnuś</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Toś tomu bokoju napóranje pśidatnych dialogow zawoboraś</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Nastajiś</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Wuprozniś</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Pśizjawiś</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Wužywarske mě</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Gronidło</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Njeskładowaś</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Nic něnto</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nigda njeskładowaś</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Nic něnto</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Składowaś</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Njeaktualizěrowaś</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Nic něnto</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Aktualizěrowaś</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Gronidłowe pólo njesmějo prozne byś</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Gronidło zapódaś</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Pśizjawjenje njedajo se składowaś</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Gronidło njedajo se składowaś</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Toś to pśizjawjenje składowaś?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Gronidło składowaś?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Toś to pśizjawjenje aktualizěrowaś?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Gronidło aktualizěrowaś?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Wužywaŕske mě skłaźonemu gronidłoju pśidaś?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Pópisanje za zapódaśe do tekstowego póla</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Wubjeŕśo barwu</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Dowóliś</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Wótpokazaś</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Sćo wěsty?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Cośo toś to sedło spušćiś? Zapódane daty njebudu se snaź składowaś</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Wóstaś</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Spušćiś</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Mjasec wubraś</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Měr</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Awg</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Now</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Cas nastajiś</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Pśizjawjenja zastojaś</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gronidła zastojaś</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Naraźone pśizjawjenja pokazaś</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Skłaźone gronidła pokazaś</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Naraźone pśizjawjenja schowaś</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Skłaźone gronidła schowaś</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Naraźone pśizjawjenja</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Skłaźone gronidła</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Mócne gronidło naraźiś</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Mócne gronidło naraźiś</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Mócne gronidło wužywaś: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Daty k toś tomu sedłoju znowego pósłaś?</string>
+ <string name="mozac_feature_prompt_repost_message">Aktualizěrowanje toś togo boka mógło nejnowše akcije pódwojś, na pśikład słanje płaśenja abo dwójne wótpósćełanje komentara.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Daty znowego pósłaś</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Pśetergnuś</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Kreditowu kórtu wubraś</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Skłaźonu kórtu wužywaś</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Naraźone kreditowe kórty pokazaś</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Skłaźone kórty pokazaś</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Naraźone kreditowe kórty schowaś</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Skłaźone kórty schowaś</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Kreditowe kórty zastojaś</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Kórty zastojaś</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Toś tu kórtu wěsće składowaś?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Datum spadnjenja kórty aktualizěrowaś?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Numer kórty buźo se koděrowaś. Wěstotny kod njebuźo se składowaś.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s waš kórtowy numer koděrujo. Waš wěstotny kod njebuźo se składowaś.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Adresu wubraś</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Naraźone adrese pokazaś</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Skłaźone adrese pokazaś</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Naraźone adrese schowaś</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Skłaźone adrese schowaś</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Adrese zastojaś</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Kontowy wobraz</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Wubjeŕśo pśizjawjeńskego póbitowarja</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Se z kontom %1$s pśizjawiś</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s ako pśizjawjeńskego póbitowarja wužywaś</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Pśizjawjenje pla %1$s z kontom %2$s jogo <a href="%3$s">pšawidłam priwatnosći</a> a <a href="%4$s">wužywańskim wuměnjenjam</a> pódlažy]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Dalej</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Pśetergnuś</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..44f632fa99
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-el/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Ακύρωση</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Να απαγορεύεται σε αυτή τη σελίδα να δημιουργεί επιπρόσθετους διαλόγους</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ορισμός</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Απαλοιφή</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Σύνδεση</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Όνομα χρήστη</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Κωδικός πρόσβασης</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Χωρίς αποθήκευση</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Όχι τώρα</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ποτέ αποθήκευση</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Όχι τώρα</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Αποθήκευση</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Να μη γίνει ενημέρωση</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Όχι τώρα</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Ενημέρωση</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Το πεδίο κωδικού πρόσβασης δεν πρέπει να είναι κενό</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Εισαγάγετε έναν κωδικό πρόσβασης</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Αποτυχία αποθήκευσης σύνδεσης</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Αδυναμία αποθήκευσης κωδικού πρόσβασης</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Αποθήκευση σύνδεσης;</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Αποθήκευση κωδικού πρόσβασης;</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Ενημέρωση σύνδεσης;</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Ενημέρωση κωδικού πρόσβασης;</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Προσθήκη ονόματος χρήστη στον αποθηκευμένο κωδικό πρόσβασης;</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Ετικέτα για εισαγωγή πεδίου κειμένου</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Επιλέξτε χρώμα</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Αποδοχή</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Άρνηση</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Είστε σίγουροι;</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Θέλετε να αποχωρήσετε από τον ιστότοπο; Τα καταχωρημένα δεδομένα ενδέχεται να μην αποθηκευτούν</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Παραμονή</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Αποχώρηση</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Επιλέξτε μήνα</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ιαν</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Φεβ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Μάρ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Απρ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Μάι</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Ιούν</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Ιούλ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Αύγ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Σεπ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Οκτ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Νοέ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Δεκ</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Ορισμός ώρας</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Διαχείριση συνδέσεων</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Διαχείριση κωδικών πρόσβασης</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Ανάπτυξη προτεινόμενων συνδέσεων</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Ανάπτυξη των αποθηκευμένων κωδικών πρόσβασης</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Σύμπτυξη προτεινόμενων συνδέσεων</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Σύμπτυξη αποθηκευμένων κωδικών πρόσβασης</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Προτεινόμενες συνδέσεις</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Αποθηκευμένοι κωδικοί πρόσβασης</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Πρόταση ισχυρού κωδικού πρόσβασης</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Πρόταση ισχυρού κωδικού πρόσβασης</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Χρησιμοποιήστε ισχυρό κωδικό: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Επαναποστολή δεδομένων στον ιστότοπο;</string>
+ <string name="mozac_feature_prompt_repost_message">Η ανανέωση αυτής της σελίδας ίσως επαναλάβει τις πρόσφατες ενέργειες, όπως αποστολή πληρωμής ή δημοσίευση σχολίου δύο φορές.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Επαναποστολή δεδομένων</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Ακύρωση</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Επιλογή πιστωτικής κάρτας</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Χρήση αποθηκευμένης κάρτας</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Ανάπτυξη προτεινόμενων πιστωτικών καρτών</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Ανάπτυξη αποθηκευμένων καρτών</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Σύμπτυξη προτεινόμενων πιστωτικών καρτών</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Σύμπτυξη αποθηκευμένων καρτών</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Διαχείριση πιστωτικών καρτών</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Διαχείριση καρτών</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Ασφαλής αποθήκευση κάρτας;</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Ενημέρωση ημερομηνίας λήξης κάρτας;</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Ο αριθμός της κάρτας θα κρυπτογραφηθεί. Ο κωδικός ασφαλείας δεν θα αποθηκευτεί.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">Το %s κρυπτογραφεί τον αριθμό της κάρτας σας. Ο κωδικός ασφαλείας σας δεν θα αποθηκευτεί.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Επιλογή διεύθυνσης</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Ανάπτυξη προτεινόμενων διευθύνσεων</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Ανάπτυξη αποθηκευμένων διευθύνσεων</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Σύμπτυξη προτεινόμενων διευθύνσεων</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Σύμπτυξη αποθηκευμένων διευθύνσεων</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Διαχείριση διευθύνσεων</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Εικόνα λογαριασμού</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Επιλογή παρόχου σύνδεσης</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Σύνδεση με λογαριασμό %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Χρήση %1$s ως παρόχου σύνδεσης</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Η σύνδεση στο %1$s με λογαριασμό %2$s υπόκειται στην <a href="%3$s">Πολιτική απορρήτου</a> και τους <a href="%4$s">Όρους υπηρεσίας</a> του]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Συνέχεια</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Ακύρωση</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..e3e8443fe9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancel</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Prevent this page from creating additional dialogs</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Set</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Clear</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Sign in</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Username</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Don’t save</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Not now</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Never save</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Not now</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Save</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Don’t update</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Not now</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Update</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Password field must not be empty</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Enter a password</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Unable to save login</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Can’t save password</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Save this login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Save password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Update this login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Update password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Add username to saved password?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label for entering a text input field</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Choose a colour</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Allow</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Deny</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Are you sure?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Do you want to leave this site? Data you have entered may not be saved</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Stay</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Leave</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Pick a month</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Set time</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Manage logins</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Manage passwords</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Expand suggested logins</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Expand saved passwords</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Collapse suggested logins</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Collapse saved passwords</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Suggested logins</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Saved passwords</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggest strong password</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggest strong password</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Use strong password: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Resend data to this site?</string>
+ <string name="mozac_feature_prompt_repost_message">Refreshing this page could duplicate recent actions, such as sending a payment or posting a comment twice.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Resend data</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancel</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Select credit card</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Use saved card</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Expand suggested credit cards</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">Expand saved cards</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Collapse suggested credit cards</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">Collapse saved cards</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Manage credit cards</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Manage cards</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Securely save this card?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Update card expiration date?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Card number will be encrypted. Security code won’t be saved.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s encrypts your card number. Your security code won’t be saved.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Select address</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Expand suggested addresses</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Expand saved addresses</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Collapse suggested addresses</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Collapse saved addresses</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Manage addresses</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Account picture</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Choose a login provider</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Sign in with a %1$s account</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Use %1$s as a login provider</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Logging in to %1$s with a %2$s account is subject to their <a href="%3$s">Privacy Policy</a> and <a href="%4$s">Terms of Service</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continue</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..b4cf291d22
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancel</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Prevent this page from creating additional dialogues</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Set</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Clear</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Sign in</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Username</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Don’t save</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Not now</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Never save</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Not now</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Save</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Don’t update</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Not now</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Update</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Password field must not be empty</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Enter a password</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Unable to save login</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Can’t save password</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Save this login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Save password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Update this login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Update password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Add username to saved password?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label for entering a text input field</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Choose a colour</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Allow</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Deny</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Are you sure?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Do you want to leave this site? Data you have entered may not be saved</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Stay</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Leave</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Pick a month</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Set time</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Manage logins</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Manage passwords</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expand suggested logins</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expand saved passwords</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Collapse suggested logins</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Collapse saved passwords</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Suggested logins</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Saved passwords</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggest strong password</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggest strong password</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Use strong password: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Resend data to this site?</string>
+ <string name="mozac_feature_prompt_repost_message">Refreshing this page could duplicate recent actions, such as sending a payment or posting a comment twice.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Resend data</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancel</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Select credit card</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Use saved card</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expand suggested credit cards</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expand saved cards</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Collapse suggested credit cards</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Collapse saved cards</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Manage credit cards</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Manage cards</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Securely save this card?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Update card expiration date?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Card number will be encrypted. Security code won’t be saved.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s encrypts your card number. Your security code won’t be saved.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Select address</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expand suggested addresses</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expand saved addresses</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Collapse suggested addresses</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Collapse saved addresses</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Manage addresses</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Account picture</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Choose a login provider</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Sign in with a %1$s account</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Use %1$s as a login provider</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Logging in to %1$s with a %2$s account is subject to their <a href="%3$s">Privacy Policy</a> and <a href="%4$s">Terms of Service</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continue</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..a6cc86761b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-eo/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Akcepti</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Nuligi</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Ne permesi al tiu ĉi paĝo la kreadon de novaj dialogoj</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Akcepti</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Viŝi</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Komenci seancon</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nomo de uzanto</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Pasvorto</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ne konservi</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ne nun</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Neniam konservi</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ne nun</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Konservi</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ne ĝisdatigi</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ne nun</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Ĝisdatigi</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">La pasvorto ne povas esti malplena</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Tajpu pasvorton</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Ne eblas konservi legitimilon</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Ne eblas konservi la pasvorton</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Ĉu konservi tiun ĉi legitimilon?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Ĉu konservi pasvorton?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Ĉu ĝisdatigi tiun ĉi akreditilon?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Ĉu ĝisdatigi pasvorton?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Ĉu aldoni nomon de uzanto al la konservita pasvorto?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etikedo por eniga teksta kampo</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Elekti koloron</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permesi</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Rifuzi</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ĉu vi certas?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ĉu vi volas foriri el tiu ĉi retejo? Enigitaj datumoj eble ne estos konservitaj.</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Resti</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Foriri</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Elekti monaton</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aŭg</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Difini horon</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Administri legitimilojn</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Administri pasvortojn</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Malfaldi sugestitajn legitimilojn</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Malfaldi konservitajn pasvortojn</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Faldi sugestitajn legitimilojn</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Faldi konservitajn pasvortojn</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Sugestitaj legitimiloj</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Konservitaj pasvortoj</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugesti fortan pasvorton</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugesti fortan pasvorton</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Uzi fortan pasvorton: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Ĉu resendi datumojn al tiu ĉi retejo?</string>
+ <string name="mozac_feature_prompt_repost_message">Reŝargo de tiu paĝo povus ripeti ĵusajn agojn, ekzemple resendon de pago aŭ komento.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Resendi datumojn</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Nuligi</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Elekti kreditkarton</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Uzi konservitan karton</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Malfaldi sugestitajn kreditkartojn</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Elporti konservitajn pasvortojn</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Faldi sugestitajn kreditkartojn</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Faldi konservitajn kartojn</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Administri kreditkartojn</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Administri kartojn</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Ĉu sekure konservi tiun ĉi kreditkarton?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Ĉu ĝisdatigi la daton de senvalidiĝo de kreditkarto?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">La numero de kreditkaro estos ĉifrita. La sekureca kodo ne estos konservita.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s ĉifras vian numeron de karto. Via sekureca kodo ne estos konservita.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Elekti adresojn</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Malfaldi sugestitajn adresojn</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Malfaldi konservitajn adresojn</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Faldi sugestitajn adresojn</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Faldi konservitajn adresojn</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Administri adresojn</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Bildo de profilo</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Elektu provanton de legitimo por uzantoj</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Komencu seancon kun konto de %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Uzi %1$s kiel provizanton de legitimilo</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Komenco de seanco en %1$s per konto de %2$s estas regata de ilia <a href="%3$s">politiko pri privateco</a> kaj <a href="%4$s">kondiĉoj de uzo</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Daŭrigi</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Nuligi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..b16d50b15a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Aceptar</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Impedir que esta página cree diálogos adicionales</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Establecer</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Eliminar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Iniciar sesión</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nombre de usuario</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contraseña</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No guardar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">No ahora</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">No guardar nunca</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">No ahora</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Guardar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No actualizar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">No ahora</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">El campo de contraseña no puede estar vacío</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Ingresar una contraseña</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No se puede guardar el inicio de sesión</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">No se puede guardar la contraseña</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">¿Guardar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">¿Guardar contraseña?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">¿Actualizar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">¿Actualizar contraseña?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">¿Agregar nombre de usuario a la contraseña guardada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta para ingresar un campo de entrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Elija un color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Denegar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">¿Estás seguro?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">¿Querés salir de este sitio? Los datos que ingresaste pueden no guardarse</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Permanecer</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Salir</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Elegí un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">En</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mayo</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dic</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Establecer hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Administrar inicios de sesión</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Administrar contraseñas</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expandir los inicios de sesión sugeridos</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expandir las contraseñas guardadas</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Contraer los inicios de sesión sugeridos</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Contraer contraseñas guardadas</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Inicios de sesión sugeridos</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Contraseñas guardadas</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugerir contraseña segura</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugerir contraseña segura</string>
+
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Usar contraseña segura: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">¿Reenviar datos a este sitio?</string>
+ <string name="mozac_feature_prompt_repost_message">Recargar esta página podría duplicar acciones recientes, como enviar un pago o publicar un comentario dos veces.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reenviar datos</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancelar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleccionar tarjeta de crédito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Usar tarjeta guardada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Ampliar las tarjetas de crédito sugeridas</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expandir tarjetas guardadas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Contraer tarjetas de crédito sugeridas</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Contraer tarjetas guardadas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Administrar tarjetas de crédito</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Administrar tarjetas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">¿Guardar esta tarjeta de forma segura?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">¿Actualizar la fecha de vencimiento de la tarjeta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">El número de tarjeta será cifrado. El código de seguridad no se guardará.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s cifra tu número de tarjeta. El código de seguridad no se guardará.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleccionar dirección</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expandir direcciones sugeridas</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expandir las direcciones guardadas</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Contraer direcciones sugeridas</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Contraer direcciones guardadas</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Administrar direcciones</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Foto de la cuenta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Elegir un proveedor de inicio de sesión</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Iniciar sesión con una cuenta de %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Usar %1$s como proveedor de inicio de sesión</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Iniciar sesión en %1$s con una cuenta %2$s está sujeto a la <a href="%3$s">Política de privacidad</a> y <a href="%4$s">Términos de servicio</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..ed1d9dd56f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Aceptar</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Evitar que esta página cree diálogos adicionales</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ajustar</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Limpiar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Conectarse</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nombre de usuario</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contraseña</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No guardar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ahora no</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nunca guardar</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ahora no</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Guardar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No actualizar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ahora no</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">El campo de contraseña no puede estar vacío</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Ingresar una contraseña</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No se pudo guardar la credencial</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">No se puede guardar la contraseña</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">¿Guardar esta credencial?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">¿Guardar contraseña?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">¿Actualizar esta credencial?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">¿Actualizar contraseña?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">¿Añadir nombre de usuario a la contraseña guardada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta para ingresar un campo de entrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Elige un color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Denegar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">¿De verdad quieres proceder?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">¿Quieres dejar este sitio? Los datos que ha ingresado podrían no estar guardados</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Mantenerse</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Salir</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Elige un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ene</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dic</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Ajustar hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Administrar credenciales</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gestionar contraseñas</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expandir credenciales sugeridas</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expandir contraseñas guardadas</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Contraer credenciales sugeridas</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Contraer contraseñas guardadas</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Credenciales sugeridas</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Contraseñas guardadas</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugerir contraseña segura</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugerir contraseña segura</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Usar contraseña segura: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">¿Reenviar datos a este sitio?</string>
+ <string name="mozac_feature_prompt_repost_message">Recargar esta página podría duplicar acciones recientes, como enviar un pago o publicar un comentario dos veces.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reenviar datos</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancelar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleccionar tarjeta de crédito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Usar tarjeta guardada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expandir tarjetas de crédito sugeridas</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expandir tarjetas guardadas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Ocultar tarjetas de crédito sugeridas</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Contraer tarjetas guardadas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gestionar tarjetas de crédito</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gestionar tarjetas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">¿Guardar esta tarjeta de forma segura?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">¿Actualizar la fecha de vencimiento de la tarjeta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">El número de la tarjeta será encriptado. El código de seguridad no será guardo.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s cifra tu número de tarjeta. Tu código de seguridad no será guardado.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleccionar dirección</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expandir direcciones sugeridas</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expandir direcciones guardadas</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Ocultar direcciones sugeridas</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Contraer direcciones guardadas</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gestionar direcciones</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imagen de la cuenta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Elige un proveedor de inicio de sesión</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Conéctate con una cuenta de %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Usar %1$s como proveedor de inicio de sesión</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Conectarse a %1$s con una cuenta de %2$s está sujeto a su <a href="%3$s">Política de privacidad</a> y <a href="%4$s">Términos de servicio</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..2457b86e36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Vale</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Evitar que esta página cree diálogos adicionales</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Establecer</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Limpiar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Iniciar sesión</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nombre de usuario</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contraseña</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No guardar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ahora no</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">No guardar nunca</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ahora no</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Guardar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No actualizar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ahora no</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">El campo de contraseña no puede estar vacío</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Introduce una contraseña</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No se puede guardar el inicio de sesión</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">No se ha podido guardar la contraseña</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">¿Guardar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">¿Guardar contraseña?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">¿Actualizar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">¿Actualizar contraseña?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">¿Añadir nombre de usuario a la contraseña guardada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta para ingresar un campo de entrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Elegir un color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Denegar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">¿Estás seguro?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">¿Quieres salir de este sitio? Los datos que has introducido puede que no estén guardados</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Permanecer</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Salir</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Elige un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Enero</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Febrero</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Marzo</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abril</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mayo</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Junio</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Julio</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Agosto</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sept</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dic</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Establecer hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Administrar inicios de sesión</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Administrar contraseñas</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expandir inicios de sesión sugeridos</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expandir las contraseñas guardadas</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Contraer inicios de sesión sugeridos</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Contraer las contraseñas guardadas</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Inicios de sesión sugeridos</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Contraseñas guardadas</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugerir contraseña segura</string>
+
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugerir contraseña segura</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Usar contraseña segura: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">¿Reenviar los datos a este sitio?</string>
+ <string name="mozac_feature_prompt_repost_message">Actualizar esta página podría duplicar acciones recientes, como hacer un pago o publicar un comentario dos veces.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reenviar los datos</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancelar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleccionar tarjeta de crédito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Usar tarjeta guardada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expandir la lista de tarjetas de crédito sugeridas</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expandir tarjetas guardadas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Contraer la lista de tarjetas de crédito sugeridas</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Contraer tarjetas guardadas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Administrar tarjetas de crédito</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Administrar tarjetas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">¿Guardar esta tarjeta de forma segura?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">¿Actualizar la fecha de caducidad de la tarjeta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">El número de tarjeta será cifrado. El código de seguridad no se guardará.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s cifra tu número de tarjeta. Tu código de seguridad no se guardará.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleccionar dirección</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expandir direcciones sugeridas</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expandir direcciones guardadas</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Contraer direcciones sugeridas</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Contraer direcciones guardadas</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Administrar direcciones</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imagen de la cuenta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Elegir un proveedor de inicio de sesión</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Conéctate con una cuenta de %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Usar %1$s como proveedor de inicio de sesión</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Iniciar sesión en %1$s con una cuenta %2$s está sujeto a la <a href="%3$s">Política de privacidad</a> y a los <a href="%4$s">Términos de servicio</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..d38f4e62a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Aceptar</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Prevenir esta página desde la creación de cuadros de diálogo adicionales</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Establecer</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Limpiar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Iniciar sesión</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nombre de usuario</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contraseña</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No guardar</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nunca guardar</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ahora no</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Guardar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No actualizar</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">El campo de contraseña no puede estar vacío</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No se puede guardar el inicio de sesión</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">¿Guardar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">¿Actualizar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">¿Agregar nombre de usuario a la contraseña guardada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta para ingresar un campo de entrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Elegir un color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Denegar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">¿Estás seguro?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">¿Deseas abandonar este sitio? Los datos que has ingresado podrían perderse</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Permanecer en el sitio</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Abandonar el sitio</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Eligir un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ene</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dic</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Establecer hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Administrar inicios de sesión</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expandir los inicios de sesión sugeridos</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Contraer los inicios de sesión sugeridos</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Inicios de sesión sugeridos</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">¿Reenviar datos a este sitio?</string>
+ <string name="mozac_feature_prompt_repost_message">Actualizar esta página podría duplicar acciones recientes, como enviar un pago o publicar un comentario dos veces.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reenviar datos</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancelar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleccionar tarjeta de crédito</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expandir sección de tarjetas de crédito sugeridas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Ocultar las tarjetas de crédito sugeridas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Administrar tarjetas de crédito</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">¿Guardar esta tarjeta de forma segura?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">¿Actualizar la fecha de vencimiento de la tarjeta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">El número de tarjeta se encriptará. El código de seguridad no se guardará.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleccionar dirección</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expandir direcciones sugeridas</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Ocultar direcciones sugeridas</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Administrar direcciones</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imagen de la cuenta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Elige un proveedor de inicio de sesión</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Conéctate con una cuenta de %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Usar %1$s como proveedor de inicio de sesión</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Iniciar sesión en %1$s con una cuenta de %2$s está sujeto a su <a href="%3$s">Política de Privacidad</a> y <a href="%4$s">Términos de Servicio.</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..2457b86e36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-es/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Vale</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Evitar que esta página cree diálogos adicionales</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Establecer</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Limpiar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Iniciar sesión</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nombre de usuario</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contraseña</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No guardar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ahora no</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">No guardar nunca</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ahora no</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Guardar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No actualizar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ahora no</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">El campo de contraseña no puede estar vacío</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Introduce una contraseña</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No se puede guardar el inicio de sesión</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">No se ha podido guardar la contraseña</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">¿Guardar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">¿Guardar contraseña?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">¿Actualizar este inicio de sesión?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">¿Actualizar contraseña?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">¿Añadir nombre de usuario a la contraseña guardada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta para ingresar un campo de entrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Elegir un color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Denegar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">¿Estás seguro?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">¿Quieres salir de este sitio? Los datos que has introducido puede que no estén guardados</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Permanecer</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Salir</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Elige un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Enero</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Febrero</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Marzo</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abril</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mayo</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Junio</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Julio</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Agosto</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sept</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dic</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Establecer hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Administrar inicios de sesión</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Administrar contraseñas</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expandir inicios de sesión sugeridos</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expandir las contraseñas guardadas</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Contraer inicios de sesión sugeridos</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Contraer las contraseñas guardadas</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Inicios de sesión sugeridos</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Contraseñas guardadas</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugerir contraseña segura</string>
+
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugerir contraseña segura</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Usar contraseña segura: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">¿Reenviar los datos a este sitio?</string>
+ <string name="mozac_feature_prompt_repost_message">Actualizar esta página podría duplicar acciones recientes, como hacer un pago o publicar un comentario dos veces.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reenviar los datos</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancelar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleccionar tarjeta de crédito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Usar tarjeta guardada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expandir la lista de tarjetas de crédito sugeridas</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expandir tarjetas guardadas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Contraer la lista de tarjetas de crédito sugeridas</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Contraer tarjetas guardadas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Administrar tarjetas de crédito</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Administrar tarjetas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">¿Guardar esta tarjeta de forma segura?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">¿Actualizar la fecha de caducidad de la tarjeta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">El número de tarjeta será cifrado. El código de seguridad no se guardará.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s cifra tu número de tarjeta. Tu código de seguridad no se guardará.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleccionar dirección</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expandir direcciones sugeridas</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expandir direcciones guardadas</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Contraer direcciones sugeridas</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Contraer direcciones guardadas</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Administrar direcciones</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imagen de la cuenta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Elegir un proveedor de inicio de sesión</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Conéctate con una cuenta de %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Usar %1$s como proveedor de inicio de sesión</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Iniciar sesión en %1$s con una cuenta %2$s está sujeto a la <a href="%3$s">Política de privacidad</a> y a los <a href="%4$s">Términos de servicio</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..5d0f71851a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-et/strings.xml
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Loobu</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Sellel lehel keelatakse lisanduvate dialoogide loomine</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Vali</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Tühjenda</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Logi sisse</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Kasutajanimi</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Parool</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ära salvesta</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Mitte praegu</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ära salvesta kunagi</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Mitte praegu</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Salvesta</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ära uuenda</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Mitte praegu</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Uuenda</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Parooli väli ei tohi olla tühi</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Sisesta parool</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Kasutajatunnuste salvestamine pole võimalik</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Parooli ei saa salvestada</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Kas salvestada need kasutajatunnused?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Salvesta parool?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Kas uuendada kasutajatunnused?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Uuenda parool?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Kas lisada salvestatud paroolile kasutajanimi?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Nimetus sisestuskasti tekstiväljale</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Värvi valimine</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Luba</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Keela</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Kas oled kindel?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Kas soovid sellelt saidilt lahkuda? Sisestatud andmeid ei pruugita salvestada</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Jää</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Lahku</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Vali kuu</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">jaan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">veebr</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">mär</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">juun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">juul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">sept</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">dets</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Aja määramine</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Halda kasutajakontosid</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Halda paroole</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Laienda soovitatud kasutajakontosid</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Laienda salvestatud paroolid</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Ahenda soovitatud kasutajakontod</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Ahenda salvestatud paroolid</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Soovitatud kasutajakontod</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Salvestatud paroolid</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Soovita tugevat parooli</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Soovita tugevat parooli</string>
+
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Kasuta tugevat parooli %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Kas saata andmed sellele saidile uuesti?</string>
+ <string name="mozac_feature_prompt_repost_message">Selle lehe värskendamine võib dubleerida hiljutisi toiminguid, nagu makse saatmine või kommentaari postitamine.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Saada andmed uuesti</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Loobu</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Vali krediitkaart</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Kasuta salvestatud kaarti</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Laienda soovitatud krediitkaarte</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Laienda salvestatud kaardid</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Ahenda soovitatud krediitkaarte</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Ahenda salvestatud kaardid</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Halda krediitkaarte</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Halda kaarte</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Kas salvestada see kaart turvaliselt?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Kas uuendada kaardi aegumiskuupäeva?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Kaardi number krüptitakse. Turvakoodi ei salvestata.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s krüpteerib kaardi numbri. Turvakoodi ei salvestata.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Vali aadress</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Laienda soovitatud aadressid</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Laienda salvestatud aadressid</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Ahenda soovitatud aadressid</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Ahenda salvestatud aadressid</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Halda aadresse</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Konto pilt</string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Jätka</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Loobu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..3782fee0a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-eu/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Ados</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Utzi</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Eragotzi orri honi elkarrizketa-koadro gehiago sortzea</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ezarri</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Garbitu</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Hasi saioa</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Erabiltzaile-izena</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Pasahitza</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ez gorde</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Une honetan ez</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ez gorde inoiz</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Une honetan ez</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Gorde</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ez eguneratu</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Une honetan ez</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Eguneratu</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Pasahitzaren eremuak ezin du hutsik egon</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Idatzi pasahitz bat</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Ezin da saio-hasiera gorde</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Ezin da pasahitza gorde</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Gorde saio-hasiera hau?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Gorde pasahitza?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Eguneratu saio-hasiera hau?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Eguneratu pasahitza?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Gehitu erabiltzaile-izena gordetako pasahitzari?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Testua idazteko eremua sartzeko etiketa</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Aukeratu kolore bat</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Baimendu</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Ukatu</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ziur zaude?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Gune hau utzi nahi duzu? Sartu dituzun datuak gal litezke</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Jarraitu hemen</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Utzi</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Hautatu hilabetea</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Urt</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Ots</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Api</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Eka</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Uzt</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Abu</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Ira</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Urr</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Aza</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Abe</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Ezarri denbora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Kudeatu saio-hasierak</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Kudeatu pasahitzak</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Zabaldu iradokitako saio-hasierak</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Zabaldu gordetako pasahitzak</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Tolestu iradokitako saio-hasierak</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Tolestu gordetako pasahitzak</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Iradokitako saio-hasierak</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Gordetako pasahitzak</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Gomendatu pasahitz sendoa</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Gomendatu pasahitz sendoa</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Erabili pasahitz sendoa: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Birbidali datuak gune honetara?</string>
+ <string name="mozac_feature_prompt_repost_message">Orri hau berritzeak azken ekintzak bikoiztea eragin lezake, adibidez ordainketa bat egitea edo iruzkin bat birritan bidaltzea.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Birbidali datuak</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Utzi</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Hautatu kreditu-txartela</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Erabili gordetako txartela</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Zabaldu iradokitako kreditu-txartelak</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Zabaldu gordetako txartelak</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Tolestu iradokitako kreditu-txartelak</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Tolestu gordetako txartelak</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Kudeatu kreditu-txartelak</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Kudeatu txartelak</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Gorde txartela modu seguruan?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Eguneratu txartelaren iraungitze-data?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Txartel-zenbakia zifratu egingo da. Segurtasun-kodea ez da gordeko.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s(e)k zure txartel-zenbakia zifratzen du. Zure segurtasun-kodea ez da gordeko.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Hautatu helbidea</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Zabaldu iradokitako helbideak</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Zabaldu gordetako helbideak</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Tolestu iradokitako helbideak</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Tolestu gordetako helbideak</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Kudeatu helbideak</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Kontuaren argazkia</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Aukeratu saio-hasiera hornitzailea</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Hasi saioa %1$s kontuarekin</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Erabili %1$s saio-hasiera hornitzaile gisa</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[%1$s hornitzailean %2$s kontuarekin saioa hastea bere <a href="%3$s">pribatutasun-politika</a> eta <a href="%4$s">zerbitzuaren baldintzen</a> menpe dago]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Jarraitu</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Utzi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..d827812d36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fa/strings.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">تأیید</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">لغو</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">از ایجاد پنجره‌های جدید توسط این صفحه جلوگیری شود.</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">تنظیم</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">پاک کردن</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ورود</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">نام کاربری</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">گذرواژه</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">ذخیره نشود</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">هرگز ذخیره نکن</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">اکنون نه</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">ذخیره</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">بروزرسانی نکن</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">بروزرسانی</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">خانهٔ گذرواژه نباید خالی باشد</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">ذخیره ورود امکان پذیر نیست</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ذخیره این ورود؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">بروزرسانی این ورود؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">افزودن نام‌کاربری به گذرواژه ذخیره شده؟</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">برچسب برای وارد کردن یک خانهٔ ورودی متنی</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">یک رنگ انتخاب کنید</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">اجازه دادن</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">رد کردن</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">آیا اطمینان دارید؟</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">می خواهید این پایگاه را ترک کنید؟ اطلاعاتی که وارد کردید ممکن است ذخیره نشود</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ماندن</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ترک کردن</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">یک ماه انتخاب کنید</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ژانویه</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">فوریه</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">مارس</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">آوریل</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">مه</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ژوئن</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ژوئیه</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">اوت</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">سپتامبر</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">اکتبر</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">نوامبر</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">دسامبر</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">تنظیم زمان</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">مدیریت ورودها</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">ورودهای پیشنهادی را گسترش دهید</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">ورودهای پیشنهادی را گسترش دهید</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">ورود‌های پیشنهاد شده</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ارسال دوباره داده به این پایگاه؟</string>
+ <string name="mozac_feature_prompt_repost_message">بازخوانی این صفحه می‌تواند کنش‌های اخیر مانند پرداخت یا ارسال نظر را دوباره تکرار کند.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ارسال دوبارهٔ داده‌ها</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">لغو</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">انتخاب کارت اعتباری</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">کارت‌های اعتباری بیشتر</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">کارت‌های اعتباری کم‌تر</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">مدیریت کارت‌های اعتباری</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">این کارت به صورت ایمن ذخیره شود؟</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">تاریخ انقضای کارت به‌روز شود؟</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">شماره کارت رمزگذاری خواهد شد. رمز امنیتی ذخیره نخواهد شد.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">گزینش نشانی</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">گسترش نشانی‌های پیشنهادی</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">جمع کردن نشانی‌های پیشنهادی</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">مدیریت نشانی‌ها</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">تصویر حساب</string>
+ <!-- Title of the Identity Credential provider dialog choose. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">انتخاب یک فراهم‌کنندهٔ ورود</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">استفاده از %1$s به عنوان یک فراهم‌کننده</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ورود به %1$s با یک حساب %2$s تحت <a href="%3$s">سیاست حفظ محرمانگی</a> و <a href="%4$s">شرایط ارائهٔ خدمات</a> آن‌هاست]]></string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..1316ca2667
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ff/strings.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Moƴƴii</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Haaytu</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Haɗ ngoo hello sosde kaalde goɗɗe</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Teelto</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Momtu</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Seŋo</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Innde kuutoro</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Finnde</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Hoto danndu</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Hoto danndu abada</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Danndu</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Hesɗitin</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Gallol finnde fotaani wonde mehol</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Horiima danndude seŋorde</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Ɓeydu innde kuutoro e finnde danndaade?</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Yamir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Haɗ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Suɓo lewru</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Siilo</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Colte</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mbooy</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Duujal</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Seeɗto</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Korse</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Morso</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Juko</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Siilto</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Yarkomaa</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Jolal</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Bowte</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Laɓo kartal banke</string>
+
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Uddit karte banke basiyaaɗe</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Uddu karte banke basiyaaɗe</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Toppito karte banke</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Danndu ngal kartal e kisnal?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Hesɗitin ñalngu kiiɗtugol kartal?</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..c0c777120e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fi/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Peruuta</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Estä tätä sivua luomasta lisäikkunoita</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Aseta</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Tyhjennä</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Kirjaudu sisään</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Käyttäjätunnus</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Salasana</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Älä tallenna</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ei nyt</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Älä tallenna koskaan</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ei nyt</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Tallenna</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Älä päivitä</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ei nyt</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Päivitä</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Salasanakenttä ei saa olla tyhjä</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Kirjoita salasana</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Kirjautumistietojen tallennus epäonnistui</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Salasanaa ei voi tallentaa</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Tallennetaanko tämä kirjautumistieto?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Tallennetaanko salasana?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Päivitetäänkö tämä kirjautumistieto?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Päivitetäänkö salasana?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Lisätäänkö käyttäjänimi tallennettuun salasanaan?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Selite tekstin kirjoittamiselle tekstikenttään</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Valitse väri</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Salli</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Estä</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Oletko varma?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Haluatko poistua tältä sivustolta? Kirjoittamasi tiedot eivät välttämättä tallennu</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Pysy</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Poistu</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Valitse kuukausi</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">tammi</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">helmi</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">maalis</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">huhti</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">touko</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">kesä</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">heinä</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">elo</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">syys</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">loka</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">marras</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">joulu</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Aseta aika</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Hallitse kirjautumistietoja</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Hallitse salasanoja</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Laajenna ehdotetut kirjautumistiedot</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Laajenna tallennetut salasanat</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Supista ehdotetut kirjautumistiedot</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Supista tallennetut salasanat</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Ehdotetut kirjautumistiedot</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Tallennetut salasanat</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Ehdota vahvaa salasanaa</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Ehdota vahvaa salasanaa</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Käytä vahvaa salasanaa: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Lähetetäänkö tiedot uudelleen tälle sivustolle?</string>
+ <string name="mozac_feature_prompt_repost_message">Tämän sivun päivittäminen saattaa kahdentaa viimeisimmät toiminnot, kuten maksun suorittamisen tai kommentin lähettämisen kahdesti.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Lähetä tiedot uudelleen</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Peruuta</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Valitse luottokortti</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Käytä tallennettua korttia</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Laajenna ehdotetut luottokortit</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Laajenna tallennetut kortit</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Supista ehdotetut luottokortit</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Supista tallennetut kortit</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Hallitse luottokortteja</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Hallitse kortteja</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Tallennetaanko tämä kortti turvallisesti?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Päivitetäänkö kortin viimeinen voimassaolopäivä?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Kortin numero salataan. Suojakoodia ei tallenneta.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s salaa korttisi numeron. Turvakoodiasi ei tallenneta.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Valitse osoite</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Laajenna ehdotetut osoitteet</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Laajenna tallennetut osoitteet</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Supista ehdotetut osoitteet</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Supista tallennetut osoitteet</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Hallitse osoitteita</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Tilin kuva</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Valitse kirjautumispalvelu</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Kirjaudu sisään %1$s-tilillä</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Käytä palvelua %1$s kirjautumiseen</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Kirjautuminen palvelun %1$s tilillä %2$s on kyseisen palvelun <a href="%3$s">tietosuojakäytännön</a> ja <a href="%4$s">käyttöehtojen</a> alaista]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Jatka</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Peruuta</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..4dfffcad53
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fr/strings.xml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Annuler</string>
+
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Empêcher cette page d’ouvrir des dialogues supplémentaires</string>
+
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Valider</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Effacer</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Se connecter</string>
+
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nom d’utilisateur</string>
+
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Mot de passe</string>
+
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ne pas enregistrer</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Plus tard</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ne jamais enregistrer</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Pas pour cette fois</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Enregistrer</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ne pas mettre à jour</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Plus tard</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Mettre à jour</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Le champ mot de passe ne doit pas être vide</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Saisissez un mot de passe</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Impossible d’enregistrer l’identifiant</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Impossible d’enregistrer le mot de passe</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Enregistrer ces identifiants ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Enregistrer le mot de passe ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Mettre à jour cet identifiant ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Mettre à jour le mot de passe ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Ajouter un nom d’utilisateur au mot de passe enregistré ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Libellé pour la création d’un champ de saisie de texte</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Choisir une couleur</string>
+
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Autoriser</string>
+
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Refuser</string>
+
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Confirmation</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Voulez-vous quitter ce site ? Des données saisies peuvent être perdues</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Rester</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Quitter</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Choisir un mois</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">janv.</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">févr.</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">mars</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">avr.</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">juin</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">juil.</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">août</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">sept.</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">oct.</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nov.</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">déc.</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Choisir l’heure</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gérer les identifiants</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gérer les mots de passe</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Développer les identifiants suggérés</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Développer les mots de passe enregistrés</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Réduire les identifiants suggérés</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Réduire les mots de passe enregistrés</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Identifiants suggérés</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Mots de passe enregistrés</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggérer un mot de passe fort</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggérer un mot de passe fort</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Utiliser un mot de passe fort : %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Renvoyer les données à ce site ?</string>
+ <string name="mozac_feature_prompt_repost_message">Actualiser cette page pourrait répéter des actions récentes, telles que l’envoi d’un paiement ou la publication d’un commentaire.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Renvoyer les données</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Annuler</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Sélectionner une carte bancaire</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Utiliser une carte enregistrée</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Développer les cartes bancaires suggérées</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Développer les cartes enregistrées</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Réduire les cartes bancaires suggérées</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Réduire les cartes enregistrées</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gérer les cartes bancaires</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gérer les cartes</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Enregistrer cette carte en toute sécurité ?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Mettre à jour la date d’expiration de la carte ?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Le numéro de carte sera chiffré. Le code de sécurité ne sera pas enregistré.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s chiffre votre numéro de carte. Votre code de sécurité ne sera pas enregistré.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Sélectionner une adresse</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Développer les adresses suggérées</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Développer les adresses enregistrées</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Réduire les adresses suggérées</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Réduire les adresses enregistrées</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gérer les adresses</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Photo du profil</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Choisir un fournisseur de connexion</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Connectez-vous avec un compte %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Utiliser %1$s comme fournisseur de connexion</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Se connecter à %1$s avec un compte %2$s est soumis à la <a href="%3$s">politique de confidentialité</a> et aux <a href="%4$s">conditions d’utilisation</a> de ce dernier.]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuer</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Annuler</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..0e81b65ce8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fur/strings.xml
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Va ben</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Anule</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Impedìs a cheste pagjine di creâ altris dialics</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Stabilìs</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Nete</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Jentre</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Non utent</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No sta salvâ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">No cumò</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">No sta salvâ mai</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">No cumò</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Salve</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No sta inzornâ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">No cumò</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Inzorne</string>
+
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Il cjamp de password nol à di sei vueit</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Inserìs une password</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Impussibil salvâ lis credenziâls</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Impussibil salvâ la password</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Salvâ cheste credenziâl?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Salvâ la password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Inzornâ cheste credenziâl?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Inzornâ la password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Zontâ il non utent ae password salvade?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etichete associade a un cjamp pal inseriment di test</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Sielç un colôr</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permet</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Dinee</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Bandonâ pardabon?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Desideristu bandonâ chest sît? I dâts inserîts a podaressin lâ pierdûts</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Reste</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Bandone</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Selezione un mês</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Zen</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fev</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Avr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jug</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Lui</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Avo</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Otu</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dic</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Stabilìs ore</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gjestìs credenziâls</string>
+
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gjestìs passwords</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Slargje lis credenziâls sugjeridis</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Slargje lis passwords salvadis</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Comprim lis credenziâls sugjeridis</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Strenç lis passwords salvadis</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Credenziâls sugjeridis</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Passwords salvadis</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugjerìs password complesse</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugjerìs password complesse</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Dopre password complesse: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Tornâ a inviâ i dâts a chest sît?</string>
+ <string name="mozac_feature_prompt_repost_message">Se tu tornis a cjariâ cheste pagjine tu podaressis causâ la ripetizion des azions resintis, come l’inviament di un paiament o publicâ un coment dôs voltis.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Torne invie i dâts</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Anule</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Selezione cjarte di credit</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Dopre cjarte salvade</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Slargje la liste des cjartis di credit sugjeridis</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Slargje lis cjartis salvadis</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Comprim la liste des cjartis di credit sugjeridis</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Strenç lis cjartis salvadis</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gjestìs cjartis di credit</string>
+
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gjestìs cjartis</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Salvâ cheste cjarte in maniere sigure?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Inzornâ la date di scjadince de cjarte?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Il numar de cjarte al sarà cifrât. Il codiç di sigurece nol vignarà salvât.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s al cifre il numar de tô cjarte. Il codiç di sigurece nol vignarà salvât.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Selezione recapit</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Slargje i recapits sugjerîts</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Slargje lis direzions salvadis</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Comprim recapits sugjerîts</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Strenç lis direzions salvadis</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gjestìs recapits</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imagjin pal account</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Sielç un furnidôr di acès</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Jentre cuntun account %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Dopre %1$s come furnidôr di acès</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[L’acès a %1$s cuntun account %2$s al sotstà ae <a href="%3$s">Informative su la riservatece</a> e ai <a href="%4$s">Tiermins dal servizi</a> di chest ultin]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continue</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Anule</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..648aa71c19
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Annulearje</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Foarkomme dat dizze side ekstra dialoochfinsters makket</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ynstelle</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Wiskje</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Oanmelde</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Brûkersnamme</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Wachtwurd</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Net bewarje</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">No net</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nea bewarje</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">No net</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Bewarje</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Net bywurkje</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">No net</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Bywurkje</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Wachtwurdfjild mei net leech wêze</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Folje in wachtwurd yn</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Kin oanmelding net bewarje</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Kin wachtwurd net bewarje</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Dizze oanmelding bewarje?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Wachtwurd bewarje?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Dizze oanmelding bywurkje?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Wachtwurd bywurkje?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Brûkersnamme oan bewarre wachtwurd tafoegje?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label foar it ynfieren fan in tekstynfierfjild</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Kies in kleur</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Tastean</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Wegerje</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Binne jo wis?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Wolle jo dizze website ferlitte? Ynfierde gegevens wurde mooglik net bewarre</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Bliuwe</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Ferlitte</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Kies in moanne</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">mrt</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">maa</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">des</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Tiid ynstelle</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Oanmeldingen beheare</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Wachtwurden beheare</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Foarstelde oanmeldingen útklappe</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Bewarre wachtwurden te útklappe</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Foarstelde oanmeldingen ynklappe</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Bewarre wachtwurden ynklappe</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Foarstelde oanmeldingen</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Bewarre wachtwurden</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sterk wachtwurd foarstelle</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sterk wachtwurd foarstelle</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Sterk wachtwurd brûke: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Gegevens opnij nei dizze website ferstjoere?</string>
+ <string name="mozac_feature_prompt_repost_message">It opnij laden fan dizze side kin resinte aksjes duplisearje, lykas it ferstjoeren fan in betelling of it twa kear pleatsen fan in berjocht.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Gegevens opnij ferstjoere</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Annulearje</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Selektearje creditcard</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Bewarre kaart brûke</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Foarstelde creditcards útklappe</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Bewarre kaarten te útklappe</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Foarstelde creditcards ynklappe</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Bewarre kaarten ynklappe</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Creditcards beheare</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Kaarten beheare</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Dizze kaart feilich bewarje?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Ferrindatum kaart bywurkje?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">It kaartnûmer sil fersifere wurde. De befeiligingskoade wurdt net bewarre.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s fersiferet jo kaartnûmer. Jo befeiligingskoade wurdt net bewarre.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Adres selektearje</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Foarstelde adressen útklappe</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Bewarre adressen útklappe</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Foarstelde adressen ynklappe</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Bewarre adressen ynklappe</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Adressen beheare</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Accountôfbylding</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Kies in oanmeldprovider</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Meld jo oan mei in %1$s-account</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s as oanmeldprovider brûke</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Oanmelding by %1$s mei in %2$s-account falt ûnder harren <a href="%3$s">Privacybelied</a> en <a href="%4$s">Tsjinstbetingsten</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Trochgean</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Annulearje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..ce1fcab220
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cealaigh</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Ná lig don leathanach seo tuilleadh dialóg a chruthú</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Socraigh</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Glan</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Logáil isteach</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Ainm úsáideora</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Focal Faire</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ná sábháil</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Sábháil</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Nuashonraigh</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Ní cheadaítear focal faire folamh</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Níorbh fhéidir an focal faire a shábháil</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Lipéad do réimse ionchurtha téacs</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Roghnaigh dath</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Ceadaigh</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Diúltaigh</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Roghnaigh mí</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ean</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fea</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Már</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Aib</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Bea</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Mei</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Iúil</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Lún</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">MFó</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">DFó</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Samh</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Nol</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..5ad7782f05
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-gd/strings.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Ceart ma-thà</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Sguir dheth</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Na leig leis an duilleag seo còmhraidhean eile a chruthachadh</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Suidhich</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Falamhaich</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Clàraich a-steach</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Ainm-cleachdaiche</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Facal-faire</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Na sàbhail</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Na sàbhail idir</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Chan ann an-dràsta</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Sàbhail</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Na ùraich</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Ùraich</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Chan fhaod raon an fhacail-fhaire a bhith bàn</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Cha ghabh an clàradh a-steach a shàbhaladh</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">A bheil thu airson an clàradh a-steach seo a shàbhaladh?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">A bheil thu airson an clàradh a-steach seo ùrachadh?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">A bheil thu airson an t-ainm-cleachdaiche seo a chur ris an fhacal-fhaire a shàbhail thu?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Leubail airson raon ion-chur teacsa a chur a-steach</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Tagh dath</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Ceadaich</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Diùlt</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">A bheil thu cinnteach?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">A bheil thu airson an làrach seo fhàgail? Dh‘fhaoidte nach deach dàta a chuir thu a-steach a shàbhaladh fhathast</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Fuirich</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Fàg an-seo</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Tagh mìos</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Faoi</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Gearr</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Màrt</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Gibl</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Cèit</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Ògmh</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Iuch</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Lùna</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sult</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Dàmh</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Samh</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dùbh</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Suidhich àm</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Stiùirich na clàraidhean a-steach</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Leudaich na mholar de chlàraidhean a-steach</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Co-theannaich na mholar de chlàraidhean a-steach</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Clàraidhean a-steach a mholamaid</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">A bheil thu airson an dàta a chur gun làrach seo a-rithist?</string>
+ <string name="mozac_feature_prompt_repost_message">Ma nì thu ath-nuadhachadh air an duilleag seo, dh’fhaoidte gun dèid gnìomhan a rinn thu o chionn goirid, can pàigheadh a chur thu no beachd a phostaich thu, a dhèanamh a-rithist.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Cuir an dàta a-rithist</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Sguir dheth</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Tagh cairt-chreideis</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Leudaich na cairtean-creideis a mholamaid</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Co-theannaich na cairtean-creideis a mholamaid</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Stiùirich na cairtean-creideis</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">A bheil thu airson a’ chairt seo a shàbhaladh air dòigh thèarainte?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">A bheil thu airson an latha a dh’fhalbhas an ùine air a’ chairt ùrachadh?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Thèid àireamh na cairte a chrioptachadh. Cha tèid an còd tèarainteachd a shàbhaladh.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Tagh seòladh</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Leudaich na seòlaidhean a tha gam moladh</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Co-theannaich na seòlaidhean a tha gam moladh</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Stiùirich na seòlaidhean</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..785542bd46
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-gl/strings.xml
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Aceptar</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Evitar que esta páxina cree diálogos adicionais</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Estabelecer</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Limpar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Acceder</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nome de usuario</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contrasinal</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Non gardar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Agora non</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Non gardar nunca</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Agora non</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Gardar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Non actualizar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Agora non</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">O campo de contrasinal non debe estar baleiro</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Introduza un contrasinal</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Non foi posíbel gardar o acceso</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Non se pode gardar o contrasinal</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Gardar esta identificación?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Gardar o contrasinal?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Actualizar esta identificación?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Actualizar o contrasinal?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Engadir nome de usuario ao contrasinal gardado?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta para introducir un campo de entrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Escolla unha cor</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Denegar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Confirma?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Desexa abandonar este sitio? Pode que os datos introducidos non se garden</string>
+
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Permanecer</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Abandonar</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Escolla un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Xan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maio</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Xuñ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Xul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Out</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Establecer a hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Xestionar as identificacións</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Xestionar os contrasinais</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Expandir os inicios de sesión suxeridos</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Expandir os contrasinais gardados</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Contraer os inicios de sesión suxeridos</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Contraer os contrasinais gardados</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Inicios de sesión suxeridos</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Contrasinais gardados</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suxerir contrasinais fortes</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suxerir contrasinais fortes</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Usar un contrasinal forte: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Reenviar os datos a este sitio?</string>
+ <string name="mozac_feature_prompt_repost_message">Actualizar esta páxina pode duplicar accións recentes, como enviar un pago ou publicar un comentario dúas veces.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reenviar datos</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancelar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Seleccionar a tarxeta de crédito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Usar unha tarxeta gardada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Ampliar as tarxetas de crédito suxeridas</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">Expandir as tarxetas gardadas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Contraer as tarxetas de crédito suxeridas</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">Contraer as tarxetas gardadas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Xestionar as tarxetas de crédito</string>
+
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Xestionar tarxetas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Gardar esta tarxeta de forma segura?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Actualizar a data de caducidade da tarxeta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">O número de tarxeta cifrarase. O código de seguridade non se gardará.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s cifra o seu número de tarxeta. O seu código de seguranza non se gardará.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleccione o enderezo</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Expandir os enderezos suxeridos</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Amplíe os enderezos gardados</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Contraer os enderezos suxeridos</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Contraer os enderezos gardados</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Xestionar enderezos</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imaxe da conta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Escolla un provedor de inicio de sesión</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Iniciar sesión cunha conta %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Use %1$s como provedor de inicio de sesión</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Iniciar sesión en %1$s cunha conta de %2$s está suxeito á súa <a href="%3$s">Política de privacidade</a> e ás súas <a href="%4$s">Condicións de servizo </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..ed14962d6b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-gn/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">MONEĨ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Heja</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Ani emoneĩ ko kuatiaroguépe omoheñóivo ñomongeta</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Mboheko</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Mopotĩ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Eñepyrũ tembiapo</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Poruhára réra</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Ñe’ẽñemi</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Ani eñongatu</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Ani ko’ág̃a</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Aníke eñongatu</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ani ko’ág̃a</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Ñongatu</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Ani embohekopyahu</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Ani ko’ág̃a</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Mbohekopyahu</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Pe ñe’ẽñemi kora ndopytaiva’erã nandi</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Emoinge ñe’ẽñemi</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Nereñongatukuaái tembiapo ñepyrũ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Noñeñongatúi ñe’ẽñemi</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">¿Tembiapo ñepyrũ ñongatu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">¿Eñongatu ñe’ẽñemi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">¿Tembiapo ñepyrũ mbohekopyahu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">¿Embohekopyahu ñe’ẽñemi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">¿Embojuaju poruhára réra ñe’ẽñemi ñongatupyrépe?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Teramoĩ eike hag̃ua moñe’ẽrã jeikeha korápe</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Eiporavo sa’y</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Moneĩ</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Mbotove</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">¿Eikuaaporãpa?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">¿Esẽse ko tendágui? Umi mba’ekuaarã emoingéva oñeñongatu’ỹva</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Pyta</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Ñesẽ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Eiporavo peteĩ jasy</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jasyteĩ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Jasykõi</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Jasyapy</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Jasyrundy</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Jasypo</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jasypoteĩ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jasypokõi</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Jasypoapy</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Jasyporundy</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Jasypa</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Jasypateĩ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Jasypakõi</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Emoĩ aravo</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Tembiapo ñepyrũ ñangarekohára</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Eñangareko ñe’ẽñemíre</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Emoasãi tembiapo ñepyrũ je’epyre</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Emyasãi ñe’ẽñemi ñongatupyre</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Emomichĩ tembiapo ñepyrũ je’epyre</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Emoñynỹi ñe’ẽñemi ñongatupyre</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Tembiapo ñepyrũ je’epyre</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Ñe’ẽñemi ñongatupyre</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Eikuave’ẽ ñe’ẽñemi hekorosãva</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Eikuave’ẽ ñe’ẽñemi hekorosãva</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Eiporu ñe’ẽñemi hekorosãva: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">¿Emondojey mba’ekuaarã ko tendápe?</string>
+ <string name="mozac_feature_prompt_repost_message">Emyanyhẽjeývo ko kuatiarogue ombohetakuaa ejaporamóva, omondokuaa jehepyme’ẽ térã omoherakuãjo’a nde jehaipy.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Emondojey mba’ekuaarã</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Heja</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Eiporavo kuatia’atã ñemurã</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Eiporu kuatia’atã ñongatupyre</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Emoasãi kuatia’atã ñemurã rysýi je’epyre</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">Emyasãi kuatia’atã ñongatupyre</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Eñomi kuatia’atã ñemurã je’epyre</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">Emoñynỹi kuatia’atã ñongatupyre</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Eñangareko kuatia’atã ñemurãre</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Eñangareko kuatia’atã</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">¿Eñongatu ko kuatia’atã oĩ porã hag̃uáme?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">¿Embohekopyahu kuatia’atã arange paha?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Kuatia’atã papapy ipe’ahañemíta. Pe’ahañemi noñeñongatumo’ãi.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s ombopapapy nde kuatia’atã. Nde rekorosãrã ayvu noñeñongatumo’ãi.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Eiporavo kundaharape</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Emoasãi kundaharape je’epyréva</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Emyasãi kundaharape ñongatupyre</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Emomichĩ kundaharape je’epyréva</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Emoñynỹi kundaharape ñongatupyre</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Eñangareko kundaharapére</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Mba’ete ra’ãnga</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Eiporavo tembiapo ñepyrũ me’ẽhára</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Eñepyrũ tembiapo %1$s mba’etépe</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Eiporu %1$s tembiapo ñepyrũ me’ẽhárõ</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Emba’apo %1$s peteĩ mba’ete %2$s ndive ojokupytýva <a href="%3$s">Porureko ñemigua</a> ha avei <a href="%4$s">Mba’epytyvõrã ñemboguata</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Eku’ejey</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Heja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..fd199216f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">રદ કરો</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">આ પૃષ્ઠને અતિરિક્ત સંવાદો બનાવવાથી રોકો</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">ગોઠવો</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">સાફ કરો</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">સાઇન ઇન કરો</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">વપરાશકર્તા નામ</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">પાસવર્ડ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">સાચવો નહીં</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">સાચવો</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">અપડેટ કરશો નહીં</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">અપડેટ કરો</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">પાસવર્ડની જગ્યા ખાલી ન હોવો જોઈએ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">લાૅગ-ઇન સાચવવામાં અસમર્થ</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">તમે આ લૉગિનને સાચવવા માંગો છો?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">તમે આ લૉગિનને અપડેટ કરવા માંગો છો?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">શું તમે સાચવેલા પાસવર્ડમાં વપરાશકર્તા નામ ઉમેરવા માંગો છો?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">ટેક્સ્ટ ઇનપુટ ક્ષેત્ર દાખલ કરવા માટેનું લેબલ</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">રંગ પસંદ કરો</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">પરવાનગી આપો</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">નકારો</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">શું તમે ચોક્કસ છો?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">શું તમે આ સાઇટ છોડવા માંગો છો? તમે દાખલ કરેલો ડેટા સાચવવામાં આવશે નહીં</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">રહો</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">છોડો</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">મહિનો પસંદ કરો</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">જાન્યુઆરી</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ફેબ્રુઆરી</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">માર્ચ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">એપ્રિલ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">મે</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">જૂન</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">જુલાઈ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ઑગસ્ટ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">સપ્ટેમ્બર</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ઑક્ટોબર</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">નવેમ્બર</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ડિસેમ્બર</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..c4c9f6ae09
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ठीक है</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">रद्द करें</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">इस पृष्ठ को अतरिक्त सूचना प्रदान करने से रोकें</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">सेट करें</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">मिटाएं</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">साइन इन करें</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">उपयोगकर्ता नाम</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">पासवर्ड</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">मत सहेजें‌</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">कभी नहीं सहेजें</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">सहेजें</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">अपडेट न करें</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">अद्यतित करें</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">पासवर्ड क्षेत्र रिक्त नहीं होनी चाहिए</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">लॉगिन जानकारी सजेहने में असफल</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">इस लॉगिन को सहेजना चाहते हैं?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">इस लॉगिन को अपडेट करना चाहते हैं?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">सहेजे गए पासवर्ड में उपयोगकर्ता नाम जोड़ना चाहते हैं?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">पाठ इनपुट क्षेत्र दर्ज करने के लिए लेबल</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">एक रंग चुनें</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">अनुमति दें</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">अस्वीकार करें</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">क्या आपको यकीन है?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">क्या आप इस साइट को छोड़ना चाहते हैं? आपके द्वारा दर्ज किया गया डेटा सहेजा नहीं जा सकता है</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">रुकें</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">छोड़ें</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">एक महीना चुनें</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">जनवरी</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">फरवरी</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">मार्च</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">अप्रैल</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">मई</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">जून</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">जुलाई</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">अगस्त</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">सितंबर</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">अक्टूबर</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">नवंबर</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">दिसंबर</string>
+
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">लॉगिन प्रबंधित करें</string>
+
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">सुझाए गए लॉगिन का विस्तार करें</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">सुझाए गए लॉगिन संक्षिप्त करें</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">सुझाए गए लॉगिन</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">इस साइट पर डेटा फिर से भेजें?</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">डेटा फिर से भेजें</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">रद्द करें</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">क्रेडिट कार्ड चुनें</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">सुझाए गए क्रेडिट कार्ड का विस्तार करें</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings -->
+ <string name="mozac_feature_prompts_manage_credit_cards">क्रेडिट कार्ड प्रबंधित करें</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..2647f4d9b5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hil/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Sige</string>
+
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Save</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Magapabilin</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Magpili sang bulan</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Enero</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Pebrero</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Marso</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abril</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mayo</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Hunyo</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Hulyo</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Agosto</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Setiyembre</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oktubre</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nobiyembre</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Disiyembre</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..b23b6aba5b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hr/strings.xml
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">U redu</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Odustani</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Onemogući ovoj stranici stvaranje dodatnih dijaloga</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Postavi</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Izbriši</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Prijavi se</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Korisničko ime</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Lozinka</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Nemoj spremiti</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nikad ne spremaj</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ne sada</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Spremi</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Nemoj aktualizirati</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Aktualiziraj</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Polje lozinke ne smije biti prazno</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Nije moguće spremiti prijavu</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Spremiti ovu prijavu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Aktualizirati ovu prijavu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Dodati korisničko ime spremljenoj lozinki?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Oznaka za unašanje polja za unos teksta</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Odaberi boju</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Dozvoli</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Zabrani</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Sigurno?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Želiš napustiti ovu stranicu? Upisani podaci se možda neće spremiti</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Ostani</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Napusti</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Odaberi mjesec</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Sij</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Velj</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Ožu</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Tra</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Svi</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Lip</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Srp</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Kol</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Ruj</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Lis</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Stu</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Pro</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Postavi vrijeme</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Upravljaj prijavama</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Proširi predložene prijave</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Sažmi predložene prijave</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Predložene prijave</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Ponovno poslati podatke ovoj stranici?</string>
+ <string name="mozac_feature_prompt_repost_message">Osvježavanje ove stranice moglo bi ponoviti nedavne radnje, kao što su dvostruko plaćanje ili komentiranje.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Ponovno pošalji podatke</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Odustani</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Odaberi kreditnu karticu</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Proširi predložene kreditne kartice</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Sažmi predložene kreditne kartice</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Upravljaj kreditnim karticama</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Sigurno spremi ovu karticu?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Ažuriraj datum isteka kartice?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Broj kartice će biti šifriran. Sigurnosni kod neće biti spremljen.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Odaberite adresu</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Proširi predložene adrese</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Sažmi predložene adrese</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Upravljanje adresama</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Slika računa</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Odaberite davatelja usluge prijave</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Prijavite se s računom %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Koristite %1$s kao davatelja usluge prijave</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Prijava u %1$s s %2$s računom podliježe njihovoj <a href="%3$s">politici privatnosti</a> i <a href="%4$s">Uvjetima usluge</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Nastavi</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Odustani</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..23d8daba55
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">W porjadku</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Přetorhnyć</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Tutu stronu při wutworjenju přidatnych dialogow haćić</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Nastajić</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Zhašeć</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Přizjewić</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Wužiwarske mjeno</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Hesło</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Njeskładować</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Nic nětko</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ženje njeskładować</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Nic nětko</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Składować</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Njeaktualizować</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Nic nětko</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Aktualizować</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Hesłowe polo njesmě prózdne być</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Hesło zapodać</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Přizjewjenje njeda so składować</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Hesło njeda so składować</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Tute přizjewjenje składować?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Hesło składować?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Tute přizjewjenje aktualizować?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Hesło aktualizować?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Wužiwarske mjeno składowanemu hesłu přidać?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Popis za zapodaće do tekstoweho pola</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Wubjerće barbu</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Dowolić</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Wotpokazać</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Sće wěsty?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Chceće tute sydło wopušćić? Zapodate daty njebudu so snano składować</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Wostać</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Wopušćić</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Měsac wubrać</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Měr</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mej</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Awg</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Now</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Čas nastajić</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Přizjewjenja rjadować</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Hesła rjadować</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Namjetowane přizjewjenja pokazać</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Składowane hesła pokazać</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Namjetowane přizjewjenja schować</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Składowane hesła schować</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Namjetowane přizjewjenja</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Składowane hesła</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sylne hesło namjetować</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sylne hesło namjetować</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Sylne hesło wužiwać: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Daty k tutomu sydłu znowa pósłać?</string>
+ <string name="mozac_feature_prompt_repost_message">Aktualizowanje tuteje strony móhło najnowše akcije podwojić, na přikład słanje płaćenja abo dwójne wotesyłanje komentara.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Daty znowa pósłać</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Přetorhnyć</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Kreditnu kartu wubrać</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Składowanu kartu wužiwać</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Namjetowane kreditne karty pokazać</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Składowane karty pokazać</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Namjetowane kreditne karty schować</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Składowane karty schować</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Kreditne karty rjadować</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Karty rjadować</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Tutu kartu wěsće składować?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Datum spadnjenja karty aktualizować?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Čisło karty budźe so zaklučować. Wěstotny kod njebudźe so składować.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s waše kartowe čisło zaklučuje. Waš wěstotny kod njebudźe so składować.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Adresu wubrać</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Namjetowane adresy pokazać</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Składowane adresy pokazać</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Namjetowane adresy schować</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Składowane adresy schować</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Adresy rjadować</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Kontowy wobraz</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Wubjerće přizjewjenskeho poskićowarja</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">So z kontom %1$s přizjewić</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s jako přizjewjenskeho poskićowarja wužiwać</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Přizjewjenje pola %1$s z kontom %2$s jeho <a href="%3$s">prawidłam priwatnosće</a> a <a href="%4$s">wužiwanskim wuměnjenjam</a> podleži]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Dale</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Přetorhnyć</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..a480fe2bdd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hu/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Rendben</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Mégse</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Az oldal ne hozhasson létre további párbeszédablakokat</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Beállítás</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Törlés</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Bejelentkezés</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Felhasználónév</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Jelszó</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ne mentse</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Most nem</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Sose mentse</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Most nem</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Mentés</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ne frissítse</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Most nem</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Frissítés</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">A jelszómező nem lehet üres</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Adjon meg egy jelszót</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">A bejelentkezés nem menthető</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">A jelszó nem menthető</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Menti ezt a bejelentkezést?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Menti a jelszót?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Frissíti ezt a bejelentkezést?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Jelszó frissítése?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Hozzáadja a felhasználónevet a mentett jelszóhoz?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Címke a szövegbeviteli mezőhöz</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Válasszon színt</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Engedélyezés</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Tiltás</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Biztos benne?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Elhagyja ezt az oldalt? A bevitt adatok lehet, hogy nem lettek elmentve.</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Maradás</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Távozás</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Válasszon hónapot</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">jan.</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">febr.</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">márc.</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ápr.</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">máj.</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">jún.</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">júl.</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">aug.</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">szept.</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">okt.</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nov.</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">dec.</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Idő beállítása</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Bejelentkezések kezelése</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Jelszavak kezelése</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Javasolt bejelentkezések kibontása</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Mentett jelszavak kibontása</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Javasolt bejelentkezések összecsukása</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Mentett jelszavak összecsukása</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Javasolt bejelentkezések</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Mentett jelszavak</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Erős jelszó javaslata</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Erős jelszó javaslata</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Erős jelszó használata: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Újraküldi az adatokat ennek a webhelynek?</string>
+ <string name="mozac_feature_prompt_repost_message">Az oldal frissítése megismételheti a legutóbbi műveleteket, például újra fizethet vagy még egyszer elküldheti ugyanazt a hozzászólást.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Adatok újraküldése</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Mégse</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Válasszon bankkártyát</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Mentett kártya használata</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Javasolt bankkártyák kibontása</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Mentett kártyák kibontása</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Javasolt bankkártyák összecsukása</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Mentett kártyák összecsukása</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Bankkártyák kezelése</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Kártyák kezelése</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Elmenti biztonságosan ezt a kártyát?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Frissíti a kártya lejárati dátumát?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">A kártyaszám titkosítva lesz. A biztonsági kód nem kerül mentésre.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">A %s titkosítja a kártyaszámát. A biztonsági kód nem lesz mentve.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Cím kiválasztása</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Javasolt címek kibontása</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Mentett címek kibontása</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Javasolt címek összecsukása</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Mentett címek összecsukása</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Címek kezelése</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Fiók képe</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Válasszon bejelentkezési szolgáltatót</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Jelentkezzen be a %1$s-fiókjába</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">A(z) %1$s használata bejelentkezési szolgáltatóként</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[A(z) %2$s fiókkal a(z) %1$s szolgáltatásba való bejelentkezésre az <a href="%3$s">Adatvédelmi irányelvek</a> és a <a href="%4$s">Szolgáltatási feltételei</a> vonatkoznak]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Folytatás</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Mégse</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..8190f8d45b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Լավ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Չեղարկել</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Կասեցնել այս էջը հավելյալ երկխոսություններ ստեղծելուց</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Կայել</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Մաքրել</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Մուտք գործել</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Օգտվողի անուն</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Գաղտնաբառ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Չպահպանել</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ոչ հիմա</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Երբեք չպահպանել</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ոչ հիմա</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Պահպանել</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Չթարմացնել</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ոչ հիմա</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Թարմացնել</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Գաղտնաբառի դաշտը չպետք է դատարկ լինի</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Մուտքագրեք գաղտնաբառ</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Անհնար է պահել մուտքանունը</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Հնարավոր չէ պահել գաղտնաբառը</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Պահպանե՞լ մուտքանունը</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Պահե՞լ գաղտնաբառը</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Թարմացնե՞լ մուտքանունը:</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Թարմացնե՞լ գաղտնաբառը:</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Ավելացնե՞լ օգտվողի անունը գաղտնաբառին:</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Պիտակ`գրվածքի մուտքի դաշտ մուտքագրելու համար</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Ընտրեք գույնը</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Թույլատրել</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Արգելել</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Համոզվա՞ծ եք</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ցանկանո՞ւմ եք լքել այս կայքը: Ձեր մուտքագրած տվյալները, հնարավոր է, չպահպանվեն:</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Մնալ</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Լքել</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Ընտրեք ամիսը</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Հուն</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Փետ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Մարտ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Ապր</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Մայիս</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Հուն</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Հուլ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Օգս</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Սեպ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Հոկ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Նոյ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Դեկ</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Կայել ժամանակ</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Կառավարել մուտքանունները</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Կառավարել գաղտնաբառերը</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Ընդլայնել առաջարկվող մուտքանունները</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Ընդարձակել պահված գաղտնաբառերը</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Կոծկել առաջարկվող մուտքանունները</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Կոծկել պահված գաղտնաբառերը</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Առաջարկվող մուտքանուններ</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Պահված գաղտնաբառեր</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Առաջարկել ուժեղ գաղտնաբառ</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Առաջարկել ուժեղ գաղտնաբառ</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Օգտագործեք ուժեղ գաղտնաբառ՝ %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Կրկին ուղարկել տվյալները այս կայքի համար:</string>
+ <string name="mozac_feature_prompt_repost_message">Տվյալ էջի թարմացումը կարող է կրկնօրինակել վերջին գործողությունները, ինչպես օրինակ՝ վճարումը կամ մեկնաբանության կրկնակի հրապարակումը:</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Կրկին ուղարկել</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Չեղարկել</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Ընտրեք բանկային քարտ</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Օգտագործել պահված քարտը</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Ընդարձակել առաջարկվող բանկային քարտերը</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Ընդարձակել պահված քարտերը</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Կոծկել առաջարկվող բանկային քարտերը</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Կոծկել պահված քարտերը</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Կառավարել բանկային քարտերը</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Կառավարել քարտերը</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Ապահով պահե՞լ այս քարտը:</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Թարմացնե՞լ քարտի գործողության ժամկետը:</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Քարտի համարը կկոդավորվի: Անվտանգության կոդը չի պահվի:</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s-ը գաղտնագրում է ձեր քարտի համարը: Անվտանգության ձեր կոդը չի պահպանվի:</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Ընտրեք հասցե</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Ընդլայնել առաջարկվող հասցեները</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Ընդարձակել պահված հասցեները</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Կոծկել առաջարկվող հասցեները</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Կոծկել պահված հասցեները</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Կառավարել հասցեները</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Հաշվի նկար</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Ընտրեք մուտքի մատակարար</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Մուտք գործեք %1$s հաշիվով</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Օգտագործեք %1$s որպես մուտքի մատակարար</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[%1$s մուտք գործելը %2$s հաշվով ենթակա է իրենց <a href="%3$s">Գաղտնիության դրույթներին</a> և <a href="%4$s">Ծառայության պայմաններին</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Շարունակել</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Չեղարկել</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..d81b6b52e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ia/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancellar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Impedir iste pagina de crear altere dialogos</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Definir</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Vacuar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Aperir session</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nomine de usator</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Contrasigno</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Non salvar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Non ora</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Non salvar mais</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Non ora</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Salvar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Non actualisar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Non ora</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualisar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Le campo del contrasigno non debe esser vacue</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Insere un contrasigno</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Impossibile salvar le credentiales</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Impossibile salvar le contrasigno</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Salvar iste credentiales?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Salvar le contrasigno?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Actualisar iste credentiales?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Actualisar le contrasigno?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Adder le nomine de usator al contrasigno salvate?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiquetta pro introduction de un texto de campo de entrata</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Eliger un color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitter</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Denegar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Es tu secur?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vole tu lassar iste sito? Le datos que tu ha inserite non pote esser salvate</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Remaner</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Quitar</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Elige un mense</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maio</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Adjustar le hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gerer credentiales</string>
+
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gerer contrasignos</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expander le credentiales suggerite</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expander le contrasignos salvate</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Collaber le credentiales suggerite</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Comprimer le contrasignos salvate</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Credentiales suggerite</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Contrasignos salvate</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggerer contrasigno complexe</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggerer contrasigno complexe</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Usa contrasigno forte: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Reinviar le datos a iste sito?</string>
+ <string name="mozac_feature_prompt_repost_message">Actualisar iste pagina pote duplicar activitates recente, tal como inviar un pagamento o un commento duo vices.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reinviar datos</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancellar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Eliger le carta de credito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Usar un carta salvate</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expander le cartas de credito suggerite</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expander le cartas salvate</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Collaber le cartas de credito suggerite</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Comprimer le cartas salvate</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gerer le cartas de credito</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gerer le cartas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Securmente salveguardar iste carta?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Actualisar le data de expiration del carta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Le numero de carta sera cryptate. Le codice de securitate non sera salvate.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s crypta tu numero de carta. Tu codice de securitate non sera salvate.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seliger adress</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expander adresses suggerite</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expander adresses salvate</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Collaber adresses suggerite</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Collaber adresses salvate</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gerer adresses</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imagine del conto</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Selige un fornitor de accesso</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Aperir session con un conto %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Usar %1$s como fornitor de accesso</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Le accesso a %1$s con un conto %2$s es subjecte a lor <a href="%3$s">Politica de confidentialitate</a> e <a href="%4$s">Terminos de servicio</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancellar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..7f0fdf319c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-in/strings.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Oke</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Batal</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Cegah laman ini membuat dialog lainnya</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Setel</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Hapus</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Masuk</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nama Pengguna</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Sandi</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Jangan simpan</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Jangan pernah simpan</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Tidak sekarang</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Simpan</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Jangan perbarui</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Perbarui</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Bidang kata sandi tidak boleh kosong</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Gagal menyimpan info masuk</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Simpan info masuk ini?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Perbarui info masuk ini?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Tambahkan nama pengguna ke kata sandi yang disimpan?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label untuk memasukkan input teks</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Pilih warna</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Izinkan</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Tolak</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Yakin?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Apakah Anda ingin meninggalkan situs ini? Data yang Anda masukkan mungkin tidak disimpan</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Tetap di sini</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Tinggalkan</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Pilih bulan</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mei</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Agu</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Des</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Atur waktu</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Kelola info masuk</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Perlihatkan log masuk yang disarankan</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Ciutkan log masuk yang disarankan</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Log masuk yang disarankan</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Kirim ulang data ke situs ini?</string>
+ <string name="mozac_feature_prompt_repost_message">Menyegarkan halaman ini dapat mengulang tindakan yang baru saja dilakukan, seperti melakukan pembayaran atau mengirim komentar dua kali</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Kirim ulang data</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Batal</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Pilih kartu kredit</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Perluas kartu kredit yang disarankan</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Ciutkan kartu kredit yang disarankan</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Kelola kartu kredit</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Simpan kartu ini dengan aman?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Perbarui tanggal kedaluwarsa kartu?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Nomor kartu akan dienkripsi. Kode keamanan tidak akan disimpan.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Pilih alamat</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Bentangkan alamat yang disarankan</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Ciutkan alamat yang disarankan</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Kelola alamat</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Gambar akun</string>
+ <!-- Title of the Identity Credential provider dialog choose. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Pilih penyedia log masuk</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Pilih %1$s sebagai penyedia info masuk</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Masuk ke %1$s dengan akun %2$s tunduk pada <a href="%3$s">Kebijakan Privasi</a> dan <a href="%4$s">Ketentuan Layanan</a>]]></string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..790b620f4d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-is/strings.xml
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Í lagi</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Hætta við</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Koma í veg fyrir að þessi síða búi til fleiri glugga</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Stilla</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Hreinsa</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Innskráning</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Notendanafn</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Lykilorð</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ekki vista</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ekki núna</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Aldrei vista</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ekki núna</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Vista</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ekki uppfæra</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ekki núna</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Uppfæra</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Lykilorðareiturinn má ekki vera tómur</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Settu inn lykilorð</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Gat ekki vistað innskráningarupplýsingar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Get ekki vistað lykilorð</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Vista þessa innskráningu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Vista lykilorð?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Uppfæra þessa innskráningu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Uppfæra lykilorð?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Bæta notandanafni við vistað lykilorð?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Svæði til þess að setja heiti á innsláttartexta svæði</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Velja lit</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Leyfa</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Hafna</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ertu viss?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Viltu yfirgefa þessa síðu? Ekki er víst að gögnin sem þú hefur slegið inn séu vistuð</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Vera áfram</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Yfirgefa</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Veldu mánuð</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maí</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jún</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Júl</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ágú</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nóv</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Des</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Stilla tíma</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Sýsla með innskráningar</string>
+
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Sýsla með lykilorð</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Fletta út tillögum að innskráningu</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Fletta út vistuð lykilorð</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Fella saman tillögur að innskráningu</string>
+
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Fella saman vistuð lykilorð</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Tillögur að innskráningu</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Vistuð lykilorð</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Stinga upp á sterku lykilorði</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Stinga upp á sterku lykilorði</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Notaðu sterkt lykilorð: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Senda gögn til baka á þetta vefsvæði?</string>
+ <string name="mozac_feature_prompt_repost_message">Ef þú endurlest þessa síðu gæti það endurtekið nýlegar aðgerðir, eins og að senda greiðslu eða setja inn athugasemd tvisvar.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Endursenda gögn</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Hætta við</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Veldu greiðslukort</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Nota vistað kort</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Fletta út tillögum að greiðslukortum</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Fletta út vistuð greiðslukort</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Fella saman tillögur að greiðslukortum</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Fella saman vistuð greiðslukort</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Sýsla með greiðslukort</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Sýsla með greiðslukort</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Vista þetta kort á öruggan hátt?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Uppfæra gildistíma korts?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Kortanúmer verður dulritað. Öryggiskóði verður ekki vistaður.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s dulkóðar kortanúmerið þitt. Öryggiskóðinn þinn verður ekki vistaður.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Veldu póstfang</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Fletta út tillögum að póstföngum</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Fletta út vistuðum heimilisföngum</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Fella saman tillögur að póstföngum</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Fella saman vistuð heimilisföng</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Sýsla með póstföng</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Mynd fyrir notandaaðgang</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Veldu innskráningarveitu</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Skráðu þig inn með %1$s reikningi</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Nota %1$s sem innskráningarveitu</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Innskráning á %1$s með %2$s reikningi er háð <a href="%3$s">persónuverndarstefnu</a> og <a href="%4$s">þjónustuskilmálum þeirra </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Halda áfram</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Hætta við</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..55f0558393
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-it/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Annulla</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Impedisci a questa pagina di aprire ulteriori finestre di dialogo</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Imposta</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Annulla</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Accedi</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nome utente</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Non salvare</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Non adesso</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Non salvare mai</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Non adesso</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Salva</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Non aggiornare</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Non adesso</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Aggiorna</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">È necessario inserire una password</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Inserisci una password</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Impossibile salvare le credenziali</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Impossibile salvare la password</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Salvare queste credenziali?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Salvare la password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Aggiornare queste credenziali?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Aggiornare la password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Aggiungere il nome utente alle credenziali salvate?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etichetta associata a un campo per l’inserimento di testo</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Scegli un colore</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permetti</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Nega</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Abbandonare la pagina?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vuoi abbandonare questo sito? I dati inseriti potrebbero non essere stati salvati</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Non abbandonare</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Abbandona</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Seleziona mese</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Gen</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mag</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Giu</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Lug</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Ott</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dic</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Imposta ora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gestione credenziali</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gestisci password</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Espandi le credenziali suggerite</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Espandi le password salvate</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Comprimi le credenziali suggerite</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Comprimi le password salvate</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Credenziali suggerite</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Password salvate</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggerisci password complessa</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggerisci password complessa</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Utilizza password complessa: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Inviare nuovamente i dati a questo sito?</string>
+ <string name="mozac_feature_prompt_repost_message">Il ricaricamento può causare la ripetizione di azioni recenti svolte sulla pagina, generando, per esempio, pagamenti o commenti duplicati.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reinvia i dati</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Annulla</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleziona carta di credito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Utilizza carta salvata</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Espandi l’elenco delle carte di credito suggerite</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Espandi le carte salvate</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Comprimi l’elenco delle carte di credito suggerite</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Comprimi le carte salvate</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gestisci carte di credito</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gestisci carte</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Salvare questa carta in modo sicuro?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Aggiornare la data di scadenza della carta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Il numero della carta sarà crittato. Il codice di sicurezza non verrà salvato.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s critta il numero della tua carta. Il codice di sicurezza non verrà salvato.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleziona indirizzo</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Espandi gli indirizzi suggeriti</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Espandi gli indirizzi salvati</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Comprimi gli indirizzi suggeriti</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Comprimi gli indirizzi salvati</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gestisci indirizzi</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Immagine per l’account</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Scegli un provider di accesso</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Accedi con un account %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Utilizza %1$s come provider di accesso</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[L’accesso a %1$s con un account %2$s è soggetto all’<a href="%3$s">Informativa sulla privacy</a> e alle <a href="%4$s">Condizioni di utilizzo del servizio</a> di quest’ultimo.]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continua</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Annulla</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..879dd4dcc2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-iw/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">אישור</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ביטול</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">למנוע מדף זה ליצור תיבות דו־שיח נוספות</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">הגדרה</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">ניקוי</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">כניסה</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">שם משתמש</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">ססמה</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">לא לשמור</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">לא כעת</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">לעולם לא לשמור</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">לא כעת</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">לשמור</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">לא לעדכן</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">לא כעת</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">עדכון</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">שדה הססמה לא יכול להישאר ריק</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">נא להכניס ססמה</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">לא ניתן לשמור את הכניסה</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">לא ניתן לשמור את הססמה</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">לשמור כניסה זו?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">לשמור את הססמה?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">לעדכן כניסה זו?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">לעדכן את הססמה?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">להוסיף שם משתמש לססמה השמורה?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">תווית להזנת שדה קלט טקסט</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">בחירת צבע</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">לאפשר</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">לדחות</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">להמשיך?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">האם ברצונך לעזוב את האתר הזה? ייתכן שנתונים שהזנת לא יישמרו</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">להישאר</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">לעזוב</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">בחירת חודש</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ינו׳</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">פבר׳</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">מרץ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">אפר׳</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">מאי</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">יונ׳</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">יול׳</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">אוג׳</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ספט׳</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">אוק׳</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">נוב׳</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">דצמ׳</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">הגדרת זמן</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">ניהול כניסות</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">ניהול ססמאות</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">הרחבת הכניסות המוצעות</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">הרחבת ססמאות שמורות</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">צמצום הכניסות המוצעות</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">צמצום ססמאות שמורות</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">כניסות מוצעות</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">ססמאות שמורות</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">קבלת הצעה לססמה חזקה</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">קבלת הצעה לססמה חזקה</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">שימוש בססמה חזקה: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">לשלוח את הנתונים לאתר הזה שוב?</string>
+ <string name="mozac_feature_prompt_repost_message">רענון העמוד הזה עשוי להוביל לשכפול הפעולות האחרונות, כגון ביצוע תשלום או פרסום תגובה פעמיים.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">שליחת נתונים מחדש</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ביטול</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">בחירת כרטיס אשראי</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">שימוש בכרטיס השמור</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">הרחבת כרטיסי האשראי המוצעים</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">הרחבת כרטיסים שמורים</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">צמצום כרטיסי האשראי המוצעים</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">צמצום כרטיסים שמורים</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">ניהול כרטיסי אשראי</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">ניהול כרטיסים</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">לשמור את הכרטיס הזה באופן מאובטח?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">לעדכן את תאריך התפוגה של הכרטיס?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">מספר הכרטיס יוצפן. קוד האבטחה לא יישמר.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">‏%s מצפין את מספר הכרטיס שלך. קוד האבטחה שלך לא יישמר.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">בחירת כתובת</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">הרחבת הכתובות המוצעות</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">הרחבת כתובות דוא״ל</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">צמצום הכתובות המוצעות</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">צמצום כתובות דוא״ל</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">ניהול כתובות</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">תמונת חשבון</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">בחירת ספק התחברות</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">כניסה עם חשבון %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">שימוש ב־%1$s כספק התחברות</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[הכניסה ל־%1$s עם חשבון %2$s כפופה ל<a href="%3$s">מדיניות הפרטיות</a> ול<a href="%4$s">תנאי השימוש</a> שלהם]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">המשך</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">ביטול</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..9b0ba18b32
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ja/strings.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">キャンセル</string>
+
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">このページによる追加のダイアログ表示を抑止する</string>
+
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">設定</string>
+
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">消去</string>
+
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ログイン</string>
+
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ユーザー名</string>
+
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">パスワード</string>
+
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">保存しない</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">後で</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">保存しない</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">今はしない</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">保存する</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">更新しない</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">後で</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">更新</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">パスワードを入力してください</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">パスワードを入力してください</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">ログイン情報を保存できません</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">パスワードを保存できません</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">このログイン情報を保存しますか?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">パスワードを保存しますか?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">このログイン情報を更新しますか?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">パスワードを更新しますか?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">保存されたパスワードにユーザー名を追加しますか?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">テキスト入力フィールドに入力するためのラベル</string>
+
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">色を選択</string>
+
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">許可</string>
+
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">拒否</string>
+
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">本当によろしいですか?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">このサイトを離れますか? 入力したデータが保存されない可能性があります</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">留まる</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">移動する</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">月を選択してください</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">1月</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">2月</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">3月</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">4月</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">5月</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">6月</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">7月</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">8月</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">9月</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">10月</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">11月</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">12月</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">時刻の設定</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">ログイン情報の管理</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">パスワードを管理</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">提案されたログイン情報を展開</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">保存したパスワードを展開</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">提案されたログイン情報を折りたたむ</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">保存したパスワードを折りたたむ</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">提案されたログイン情報</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">保存されたパスワード</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">強固なパスワードを提案する</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">強固なパスワードを提案する</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">強固なパスワードを使用してください: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">このサイトにデータを再送信しますか?</string>
+ <string name="mozac_feature_prompt_repost_message">このページを更新すると、支払いの送信やコメントの投稿が 2 回行われるなど、直前の操作が重複する可能性があります。</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">データを再送信</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">キャンセル</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">クレジットカードを選択</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">保存したカード情報を使用</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">提案されたクレジットカード情報を展開する</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">保存したカード情報を展開</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">提案されたクレジットカード情報を折りたたむ</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">保存したカード情報を折りたたむ</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">クレジットカードを管理</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">カード情報を管理</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">このカードの情報を安全に保存しますか?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">カードの有効期限を更新しますか?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">カード番号は暗号化されます。セキュリティコードは保存されません。</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s がカード番号を暗号化します。セキュリティコードは保存しません。</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">アドレスの選択</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">提案されたアドレス情報を展開する</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">保存したアドレス情報を展開</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">提案されたアドレス情報を折りたたむ</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">保存したアドレス情報を折りたたむ</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">アドレスの管理</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">アカウント写真</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">ログインプロバイダーを選択してください</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s アカウントでログイン</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">ログインプロバイダーとして %1$s を使用する</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[%2$s アカウントで %1$s にログインすると、その <a href="%3$s">プライバシー ポリシー</a> と <a href="%4$s">サービス利用規約</a> に同意したものとみなされます]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">続ける</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">キャンセル</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..4f60984371
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ka/strings.xml
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">კარგი</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">გაუქმება</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">მომდევნო ამომხტომი სარკმლების შეზღუდვა</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">დაყენება</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">გასუფთავება</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">შესვლა</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">მომხმარებლის სახელი</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">პაროლი</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">შენახვის გარეშე</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">არასოდეს შეინახოს</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ახლა არა</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">შენახვა</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">არ განახლდეს</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">განახლება</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">პაროლის ველი ვერ იქნება ცარიელი</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">მონაცემების შენახვა ვერ მოხერხდა</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">შეინახოს ეს მონაცემები?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">განახლდეს ეს მონაცემები?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">დაემატოს სახელი შენახულ პაროლს?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">წარწერა ტექსტის შესაყვანი ველისთვის</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ფერის შერჩევა</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">დაშვება</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">უარყოფა</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">დარწმუნებული ხართ?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">გსურთ დატოვოთ ეს საიტი? თქვენ მიერ შეყვანილი მონაცემები არ შეინახება</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">დარჩენა</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">დატოვება</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">თვის შერჩევა</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">იან</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">თებ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">მარ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">აპრ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">მაი</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ივნ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ივლ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">აგვ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">სექ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ოქტ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ნოე</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">დეკ</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">დროის მითითება</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">ანგარიშების მართვა</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">შემოთავაზებების გაშლა</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">შემოთავაზებების აკეცვა</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">შემოთავაზებული ანგარიშები</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ხელახლა გაეგზავნოს მონაცემები ამ საიტს?</string>
+ <string name="mozac_feature_prompt_repost_message">გვერდის გაახლებით, შესაძლოა გამეორდეს ბოლო მოქმედება, მაგალითად თანხის ჩამოჭრა ან კომენტარის დატოვება.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">კვლავ გაგზავნა</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">გაუქმება</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">საკრედიტო ბარათის არჩევა</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">შემოთავაზებული საკრედიტო ბარათების ჩამოშლა</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">შემოთავაზებული საკრედიტო ბარათების აკეცვა</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">საკრედიტო ბარათების მართვა</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">შეინახოს ეს ბარათი უსაფრთხოდ?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">განახლდეს ბარათის ვადის თარიღი?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">ბარათის ნომერი დაიშიფრება. უსაფრთხოების კოდი არ შეინახება.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">მისამართის შერჩევა</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">შემოთავაზებების გაშლა</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">შემოთავაზებების აკეცვა</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">მისამართების მართვა</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">ანგარიშის სურათი</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">აირჩიეთ შესვლის უზრუნველმყოფი</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">შესვლისთვის გამოიყენეთ %1$s-ანგარიში</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">გამოიყენეთ %1$s ანგარიშის უზრუნველმყოფად</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[შესვლისთვის როცა %1$s გამოიყენება %2$s ანგარიშით, ექვემდებარება მათს <a href="%3$s">პირადულობის დებულებასა</a> და <a href="%4$s">მომსახურების პირობებს</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">განაგრძეთ</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">გაუქმება</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..4c58d61726
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">YAQSHI</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Biykarlaw</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Bul bette qosımsha dialog aynalar jaratılıwına tıyım salıw</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ornatıw</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Tazalaw</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Kiriw</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Paydalanıwshı atı</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Parol</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Saqlamaw</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Heshqashan saqlamaw</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Házir emes</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Saqlaw</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Jańalanbasın</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Jańalaw</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Parol qatarı bos bolmawı kerek</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Logindi saqlaw múmkin emes</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Logindi saqlayıq pa?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Bul login jańalansın ba?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Saqlaǵan parolge paydalanıwshı atı qosılsın ba?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Tekst kirgiziw maydanı ushın belgi</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Reńdi tańlań</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Ruqsat beriw</string>
+
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Biykarlaw</string>
+
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Isenimińiz kámil me?</string>
+
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Bul sayttan shıǵıwdı qáleysiz be? Siz kirgizgen maǵlıwmat saqlanbawı múmkin</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Qalıw</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Shıǵıp ketiw</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Aydı tańlań</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Dáliw</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Hút</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Hamal</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Sáwir</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Jawza</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Saratan</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Áset</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Súmbile</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Miyzan</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Aqırap</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Qawıs</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Jeddi</string>
+
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Waqıttı ornatıw</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Loginlerdi basqarıw</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Usınıs etilgen loginlerdi keńeytiw</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Usınıs etilgen loginlerdi qısqartıw</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Usınıs etilgen loginler</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Bul saytqa maǵlıwmatlar qayta jiberilsin be?</string>
+ <string name="mozac_feature_prompt_repost_message">Bul betti jańalaw nátiyjesinde pikir jazıw yamasa tólemdi jiberiw sıyaqlı sóńǵı háreketler qaytalanıwı múmkin.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Maǵlıwmatlardı qayta jiberiw</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Biykarlaw</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Bank kartasın tańlań</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Usınıs etilgen bank kartaların keńeytiw</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Usınıs etilgen bank kartaların qısqartıw</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Bank kartaların basqarıw</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Bul karta qáwipsiz saqlansın ba?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Karta ámel qılıw múddeti jańalansın ba?</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Karta nomeri shifrlenedi. Qupıyalıq kodı saqlanbaydı.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Mánzildi tańlań</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Usınıs etilgen mánzillerdi keńeytiriw</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Usınıs etilgen mánzillerdi qısqartıw</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Mánzillerdi basqarıw</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..dc9067b13a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kab/strings.xml
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">IH</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Sefsex</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Sewḥel asebter-agi seg ulday n tnaka n udiwenni nniḍen</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Sbadu</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Sfeḍ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Kcem</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Isem n useqdac</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Awal uffir</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Ur seklas ara</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Mačči tura</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Urǧin sekles</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Mačči tura</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Sekles</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Ur leqqem ara</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Mačči tura</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Leqqem</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Urti n wawal uffir ur ilaq ara ad yili d ilem</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Sekcem awal uffir</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Ur yezmir ara ad isekles anekcum</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Ur sseklas ara awal uffir</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Sekles anekcum-agi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Sekles awal uffir?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Leqqem anekcum-agi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Leqqem awal uffir?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Rnu isem n useqdac ɣer wawal uffir yettwaskelsen?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Tacreḍt i tmerna n wurti n tira n uḍris</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Fren ini</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Sireg</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Gdel</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">S tidet?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Teffɣeḍ ad teffɣeḍ seg usmel-a? Isefka i teskecmeḍ yezmer ur ttwaseklasen ara</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Qqim</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Ffeɣ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Fren aggur</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Yen</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fuṛ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Meɣ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Yeb</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maggu</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Yun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Yul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ɣuc</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Cta</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Tub</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Wam</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Duj</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Sbadu akud</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Sefrek inekcam</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Sefrek awalen uffiren</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Sken-d inekcam isumar</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Ffer inekcam isumar</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Inekcam yettwasumren</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Awalen uffiren yettwakelsen</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">SuƔer awal uffir iǧehden</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sumer awal uffir iǧehden</string>
+
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Seqdec awan n uεeddi iǧehden: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Ales tuzna n yisefka ɣer usmel-a?</string>
+ <string name="mozac_feature_prompt_repost_message">Asmiren n usebter-a yezmer ad d-yerr tigawin n melmi kan, am tuzna n lexlaṣ neɣ asuffeɣ n uwennit snat tikkal.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Ales tuzna n yisefka</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Sefsex</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Fren takarḍa n usmad</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Seqdec tkarḍa yettwaskelsen</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Semɣer tikerḍiwin n usmad i d-yettusumren</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Fneẓ tikerḍiwin n usmad i d-yettusumren</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Sefrektikarḍiwin n usmad</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Sefrek tikarḍiwin</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Asekles n tkarḍa-a s wudem aɣellsan?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Leqqem azemz n keffu n tkarḍa?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Uṭṭun n tkarḍa ad yettwawgelhen. Tangalt n tɣellist ur tettwaseklas ara.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Fren tansa</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Snefli tansiwin d-yettwasumren</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Fneẓ tansiwin d-yettwasumren</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Fneẓ tansiwin i yettwaskelsen</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Sefrek tansiwin</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Tugna n umiḍan</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Fren asaǧǧăw n unekcum</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Qqen s umidan %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Seqdec %1$s am usaǧǧaw n tuqqna</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Tuqqna ɣer %1$s s umiḍan %2$s ad yili ddaw <a href="%3$s">Tsertit tabaḍnit</a> d <a href="%4$s">Tewtilin n useqdec</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Kemmel</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Sefsex</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..2dc4c617a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kk/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ОК</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Бас тарту</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Бұл параққа қосымша сұхбаттарды жасауға тыйым салу</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Орнату</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Тазарту</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Кіру</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Пайдаланушы аты</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Пароль</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Сақтамау</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Қазір емес</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ешқашан сақтамау</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Қазір емес</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Сақтау</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Жаңартпау</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Қазір емес</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Жаңарту</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Пароль өрісі бос болмауы тиіс</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Парольді енгізіңіз</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Логинді сақтау мүмкін емес</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Парольді сақтау мүмкін емес</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Бұл логинді сақтау керек пе?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Парольді сақтау керек пе?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Бұл логинді жаңарту керек пе?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Парольді жаңарту керек пе?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Пайдаланушы атын сақталған парольге қосу керек пе?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Мәтінді енгізу өрісінің белгісі</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Түсті таңдау</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Рұқсат ету</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Тыйым салу</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Сіз сенімдісіз бе?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Осы сайттан кеткіңіз келе ме? Сіз енгізген деректер сақталмауы мүмкін</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Қалу</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Кету</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Айды таңдау</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Қаң</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Ақп</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Нау</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Сәу</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Мам</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Мау</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Шіл</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Там</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Қыр</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Қаз</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Қар</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Жел</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Уақытты орнату</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Логиндерді басқару</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Парольдерді басқару</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Ұсынылған логиндерді жазық қылу</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Сақталған парольдерді жазық қылу</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Ұсынылған логиндерді қайыру</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Сақталған парольдерді бүктеу</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Ұсынылған логиндер</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Сақталған парольдер</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Мықты парольді ұсыну</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Мықты парольді ұсыну</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Мықты парольді қолдану: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Осы сайтқа деректерді қайта жіберу керек пе?</string>
+ <string name="mozac_feature_prompt_repost_message">Бұл парақты жаңарту төлемдерді жіберу немесе пікірді екі рет жіберу сияқты соңғы әрекеттерді қайталауы мүмкін.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Деректерді қайта жіберу</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Бас тарту</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Несиелік картаны таңдау</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Сақталған картаны пайдалану</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Ұсынылған несиелік карталарды жаю</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Сақталған карталарды жазық қылу</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Ұсынылған несиелік карталарды жию</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Сақталған карталарды бүктеу</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Несиелік карталарды басқару</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Карталарды басқару</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Бұл картаны қауіпсіз сақтау керек пе?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Картаның жарамдылық мерзімін жаңарту керек пе?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Карта нөмірі шифрленеді. Қауіпсіздік коды сақталмайды.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%sкартаңыздың нөмірін шифрлейді. Қауіпсіздік кодыңыз сақталмайтын болады.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Адресті таңдау</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Ұсынылған адрестерді жазық қылу</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Сақталған адрестерді жазық қылу</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Ұсынылған адрестерді бүктеу</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Сақталған адрестерді бүктеу</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Адрестерді басқару</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Тіркелгі суреті</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Жүйеге кіру провайдерін таңдау</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s тіркелгісімен кіру</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Жүйеге кіру провайдері ретінде %1$s қолдану</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ %2$s тіркелгісімен %1$s ішіне кіру олардың <a href="%3$s">Жекелік саясаты</a> және <a href="%4$s">Қолдану шарттарымен</a> реттеледі]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Жалғастыру</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Болдырмау</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..f8ee9e3735
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Baş e</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Betal bike</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Nehêle ku ev malper diyalogên ekstra çêbike</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Saz bike</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Paqij bike</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Têkeve</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Navê bikarhêner</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Pêborîn</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Tomar neke</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Ne niha</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Qet tomar neke</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ne niha</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Tomar bike</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Nûve neke</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Ne niha</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Nûve bike</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Divê qada pêborînê vala nemîne</string>
+
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Şîfreyekê binivîse</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Hesab nehate tomarkirin</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Şîfre nayê tomarkirin</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Vî hesabî tomar bike?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Şîfreyê tomar bike?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Vî hesabî nûve bike?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Şîfreyê venû bike?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Navê bikarhêner tevlî pêborîna tomarkirî bike?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etîketa qada têketina metinê</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Rengekî hilbijêre</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Destûrê bide</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Red bike</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Bi rastî jî?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Tu dixwazî ji vê malperê derkevî? Dibe ku daneyên te têxistine neyên tomarkirin</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Lê bimîne</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Derkeve</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Mehekê hilbijêre</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Rêb</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Sib</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Ada</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Nîs</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Gul</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Hez</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Tîr</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Teb</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Îlo</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Cot</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Mij</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Ber</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Demê eyar bike</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Hesaban birêve bibe</string>
+
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Şîfreyan bi krê ve bibe</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Hesabên pêşniyarkirî fireh bike</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Şîfreyên tomarkirî fireh bike</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Hesabên pêşniyarkirî teng bike</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Şîfreyên tomarkirî teng bike</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Hesabên pêşniyarkirî</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Şîdreyên tomarkirî</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Şîfreyên xurt pêşniyar bike</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Daneyan dîsa ji vê malperê re bişîne?</string>
+ <string name="mozac_feature_prompt_repost_message">Nûkirina vê rûpelê, dibe ku kirinên vê dawiyê ducar bike. (wekî; şandina pereyan an jî şandina şîroveyekê)</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Daneyan dîsa bişîne</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Betal bike</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Karta krediyê hilbijêre</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Kartên krediyê yên pêşniyarkirî fireh bike</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Kartên krediyê yên pêşniyarkirî teng bike</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Kartên krediyê bi rê ve bibe</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Vê kardê bi awayekî ewle hilînî?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Dîroka dawî ya kardê venû bikin?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Hejmara kardê dê bê şîfrekirin. Koda ewlekariyê dê neyê hilanîn.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Navnîşanê hilbijêre</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Navnîşanên pêşniyarkirî berfireh bike</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Hesabên pêşniyarkirî teng bike</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Navnîşanan bi rê ve bibe</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Wêneyê hesabî</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Peydakera têketinê hilbijêre</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Bi hesabekî %1$s têkevê</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$sê wekî peydakera têketinê bi kar bîne</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Têketina %1$s bi hesabekî %2$s ve girêdayî <a href="%3$s">Siyaseta nepenîtiyê</a> û <a href="%4$s">Şertên karûbarê wan e. </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Bidomîne</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Betal bike</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..57aab051b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-kn/strings.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ರದ್ದು ಮಾಡು</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ಹೆಚ್ಚುವರಿ ಸಂವಾದ ಚೌಕಗಳನ್ನು ರಚಿಸದಂತೆ ಈ ಪುಟವನ್ನು ತಡೆ</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">ನಿಶ್ಚಯಿಸು</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">ಅಳಿಸು</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ಸೈನ್ ಇನ್</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ಬಳಕೆದಾರನ ಹೆಸರು</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">ಗುಪ್ತಪದ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">ಉಳಿಸಬೇಡ</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ಎಂದಿಗೂ ಉಳಿಸಬೇಡ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">ಉಳಿಸು</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">ಪರಿಷ್ಕರಿಸಬೇಡ</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">ಪರಿಷ್ಕರಿಸು</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">ಪಾಸ್ವರ್ಡ್ ಕ್ಷೇತ್ರವು ಖಾಲಿಯಾಗಿರಬಾರದು</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">ಚಿತ್ರವನ್ನು ಉಳಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ಈ ಲಾಗಿನ್ ಅನ್ನು ಉಳಿಸುವುದೇ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ಈ ಲಾಗಿನ್ ಅನ್ನು ನವೀಕರಿಸುವುದೇ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">ಉಳಿಸಿದ ಪಾಸ್‌ವರ್ಡ್‌ಗೆ ಬಳಕೆದಾರಹೆಸರನ್ನು ಸೇರಿಸುವುದೇ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">ಪಠ್ಯ ಇನ್ಪುಟ್ ಕ್ಷೇತ್ರವನ್ನು ನಮೂದಿಸಲು ಲೇಬಲ್</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ಒಂದು ಬಣ್ಣವನ್ನು ಆರಿಸಿ</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ಅನುಮತಿಸು</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ನಿರಾಕರಿಸು</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">ನೀವು ಖಚಿತವೆ?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">ನೀವು ಈ ಸೈಟ್ ಅನ್ನು ಬಿಡಲು ಬಯಸುವಿರಾ? ನೀವು ನಮೂದಿಸಿದ ಡೇಟಾವನ್ನು ಉಳಿಸಲಾಗುವುದಿಲ್ಲ</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ಉಳಿಯಿರಿ</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ಹೊರನೆಡೆ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ತಿಂಗಳೊಂದನ್ನು ಆರಿಸಿ</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ಜನವರಿ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ಫೆಬ್ರವರಿ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">ಮಾರ್ಚ್</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ಎಪ್ರಿಲ್</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">ಮೇ</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ಜೂನ್</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ಜುಲೈ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ಆಗಸ್ಟ್</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ಸೆಪ್ಟೆಂಬರ್</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ಅಕ್ಟೋಬರ್</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ನವೆಂಬರ್</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ಡಿಸೆಂಬರ್</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..69b227b800
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ko/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">확인</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">취소</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">이 페이지에서 추가 대화 상자 생성 막기</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">설정</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">지우기</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">로그인</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">사용자 이름</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">비밀번호</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">저장 안 함</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">나중에</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">저장 안 함</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">나중에</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">저장</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">업데이트 안 함</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">나중에</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">업데이트</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">비밀번호 필드는 비워 둘 수 없습니다</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">비밀번호 입력</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">로그인을 저장할 수 없음</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">비밀번호를 저장할 수 없음</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">이 로그인을 저장하시겠습니까?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">비밀번호를 저장하시겠습니까?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">이 로그인을 업데이트하시겠습니까?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">비밀번호를 업데이트하시겠습니까?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">저장된 비밀번호에 사용자 이름을 추가하시겠습니까?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">텍스트 입력을 위한 라벨</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">색상 선택</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">허용</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">거부</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">계속하시겠습니까?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">이 사이트를 나가시겠습니까? 입력한 데이터가 저장되지 않을 수 있습니다</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">유지</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">나가기</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">월 선택</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">1월</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">2월</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">3월</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">4월</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">5월</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">6월</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">7월</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">8월</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">9월</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">10월</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">11월</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">12월</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">시간 설정</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">로그인 관리</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">비밀번호 관리</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">제안된 로그인 펼치기</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">저장된 비밀번호 펼치기</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">제안된 로그인 접기</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">저장된 비밀번호 접기</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">제안된 로그인</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">저장된 비밀번호</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">강력한 비밀번호 제안</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">강력한 비밀번호 제안</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">강력한 비밀번호 사용: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">이 사이트로 데이터를 다시 보내시겠습니까?</string>
+ <string name="mozac_feature_prompt_repost_message">이 페이지를 새로 고침하면 결제를 두 번 보내거나 댓글을 두 번 게시하는 등 최근 작업이 중복될 수 있습니다.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">데이터 다시 보내기</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">취소</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">신용 카드 선택</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">저장된 카드 사용</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">제안된 신용 카드 펼치기</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">저장된 비밀번호 펼치기</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">제안된 신용 카드 접기</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">저장된 비밀번호 접기</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">신용 카드 관리</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">카드 관리</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">이 카드를 안전하게 저장하시겠습니까?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">카드 유효 기간을 업데이트하시겠습니까?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">카드 번호는 암호화됩니다. 보안 코드는 저장되지 않습니다.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s는 카드 번호를 암호화합니다. 보안 코드는 저장되지 않습니다.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">주소 선택</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">제안된 주소 펼치기</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">저장된 주소 펼치기</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">제안된 주소 접기</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">저장된 주소 접기</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">주소 관리</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">계정 사진</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">로그인 공급자 선택</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s 계정으로 로그인</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">로그인 공급자로 %1$s 사용</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ %2$s 계정으로 %1$s에 로그인하면 해당 계정의 <a href="%3$s">개인정보처리방침</a> 및 <a href="%4$s">서비스 약관</a>이 적용됩니다.]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">계속</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">취소</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..7b1c8bb127
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-lij/strings.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Va ben</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Anulla</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">No fâ arvî atri barcoin de dialogo a sta pagina</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Inpòsta</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Scancella</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Intra</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nomme utente</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Poula segreta</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No sarvâ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Sarva</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No agiornâ</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Agiorna</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Ti devi scrive \'na poula segreta</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No pòsso sarvâ l\'acesso</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Sarvâ st\'acesso?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Agiornâ st\'acesso?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Azonze sto nomme utente a-e poule segrete sarvæ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etichetta asociâ a un canpo de testo</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Çerni un cô</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permetti</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">No permette</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">T\'ê seguo?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ti veu anâ via da-o scito? I dæti inserii porievan no ese sarvæ</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Stanni chi</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Vanni via</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Çerni un meize</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Zen</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fre</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Arv</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maz</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Zug</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Lug</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Agó</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Òtô</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dex</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..69dc7e9f93
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-lo/strings.xml
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ຕົກ​ລົງ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ຍົກເລີກ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ປ້ອງກັນຫນ້ານີ້ບໍ່ໃຫ້ສະແດງໄດອະລັອກເພີ່ມອີກ</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">ຕັ້ງຄ່າ</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">ລົບລ້າງ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ເຂົ້າສູ່ລະບົບ</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ຊື່ຜູ້ໃຊ້</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">ລະຫັດຜ່ານ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">ບໍ່ບັນທຶກ</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ບໍ່ຕ້ອງບັນທຶກ</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ບໍ່​ແມ່ນ​ຕອນ​ນີ້</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">ບັນທຶກ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">ບໍ່ຕ້ອງອັບເດດ</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">ອັບເດດ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">ປ່ອງປ້ອນລະຫັດຜ່ານຈະຕ້ອງບໍ່ຫວ່າງເປົ່າ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">ບໍ່ສາມາດບັກທຶກການເຂົ້າສູ່ລະບົບໄດ້</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ບັນທຶກການເຂົ້າສູ່ລະບົບນີ້ໄວ້ບໍ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ອັບເດດການເຂົ້າສູ່ລະບົບນີ້ບໍ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">ເພີ່ມຊື່ຜູ້ໃຊ້ເພື່ອບັນທຶກລະຫັດຜ່ານໄວ້ບໍ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">ປ້າຍກຳກັບສຳລັບການປ້ອນຂໍ້ມູນໃສ່ໃນຊ່ອງຂໍ້ຄວາມ</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ເລືອກສີ</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ອະນຸຍາດ</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ປະຕິເສດ</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">ທ່ານແນ່ໃຈແລ້ວບໍ່?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">ທ່ານຕ້ອງການອອກຈາກເວັບໄຊທນີ້ບໍ? ຂໍ້ມູນທີ່ທ່ານໄດ້ປ້ອນໃສ່ອາດຈະບໍ່ໄດ້ຮັບການບັນທຶກ</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ຢູ່</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ອອກ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ເລືອກເດືອນ</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ມັງກອນ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">​ກຸມພາ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">​ມີນາ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">​ເມສາ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">ພຶດສະພາ</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">​ມິຖຸນາ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ກໍລະ​ກົດ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ສິງຫາ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ກັນຍາ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ຕຸລາ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ພະຈິກ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ທັນວາ</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">ກໍານົດເວລາ</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">ຈັດການການເຂົ້າສູ່ລະບົບ</string>
+
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">ຂະຫຍາຍການເຂົ້າສູ່ລະບົບທີ່ແນະນຳ</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">ຍຸບການເຂົ້າສູ່ລະບົບທີ່ແນະນຳ</string>
+
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">ການລັອກອິນທີ່ໄດ້ຮັບການແນະນຳ</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ສົ່ງຂໍ້ມູນຄືນສູ່ເວັບໄຊທນີ້ອີກບໍ?</string>
+ <string name="mozac_feature_prompt_repost_message">ການລີເຟສຫນ້ານີ້ອາດຈະເຮັດໃຫ້ການກະທຳກ່ອນຫນ້ານີ້ຊໍ້າກັນເຊັ່ນວ່າ: ການສົ່ງລາຍການຈ່າຍເງິນ ຫລື ການໂພສຄອມເມັ້ນ 2 ຄັ້ງ.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ສົ່ງຂໍ້ມູນຄືນໃຫມ່</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ຍົກເລີກ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">ເລືອກບັດເຄດິດ</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">ຂະຫຍາຍການແນະນຳຂອງບັດເຄດິດ</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">ຍຸບການແນະນຳຂອງບັດເຄດິດ</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">ຈັດການບັດເຄດິດ</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">ຕ້ອງການບັນທຶກບັດນີ້ຢ່າງປອດໄພຫລືບໍ?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">ຕ້ອງການອັບເດດວັນໝົດອາຍຸຂອງບັດບໍ?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">ໝາຍເລກບັດຈະຖືກເຂົ້າລະຫັດ. ລະຫັດຄວາມປອດໄພຈະບໍ່ຖືກບັນທຶກໄວ້.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">ເລືອກທີ່ຢູ່</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">ຂະຫຍາຍທີ່ຢູ່ທີ່ແນະນຳ</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">ຫຍໍ້ທີ່ຢູ່ທີ່ແນະນຳ</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">ຈັດການທີ່ຢູ່</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">ຮູບບັນຊີ</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">ເລືອກຜູ້ໃຫ້ບໍລິການເຂົ້າສູ່ລະບົບ</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">ເຂົ້າສູ່ລະບົບດ້ວຍບັນຊີ %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">ໃຊ້ %1$s ເປັນຜູ້ໃຫ້ບໍລິການເຂົ້າສູ່ລະບົບ</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ການເຂົ້າສູ່ລະບົບ %1$s ດ້ວຍບັນຊີ %2$s ແມ່ນຢູ່ພາຍໃຕ້ <a href="%3$s">ນະໂຍບາຍຄວາມເປັນສ່ວນຕົວ</a> ແລະ <a href="%4$s">ເງື່ອນໄຂການໃຫ້ບໍລິການ </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">ສືບຕໍ່</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">ຍົກເລີກ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..dfba6170a3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-lt/strings.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Gerai</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Atsisakyti</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Nebeleisti šiam tinklalapiui kurti naujų dialogo langų</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Nustatyti</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Išvalyti</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Prisijunkite</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Naudotojo vardas</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Slaptažodis</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Neįsiminti</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Niekada neįrašyti</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ne dabar</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Įsiminti</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Neatnaujinti</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Atnaujinti</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Slaptažodžio laukas negali būti tuščias</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Nepavyko įrašyti prisijungimo</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Įrašyti šį prisijungimą?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Atnaujinti šį prisijungimą?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Pridėti naudotojo vardą prie įrašyto slaptažodžio?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Teksto įvesties lauko pavadinimas</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Pasirinkite spalvą</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Leisti</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Drausti</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ar tikrai to norite?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ar tikrai norite užverti šią svetainę? Jūsų įvesti duomenys gali būti neįrašyti</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Likti</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Išeiti</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Nurodykite mėnesį</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Sau</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Vas</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Kov</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Bal</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Geg</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Bir</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Lie</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Rgp</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Rgs</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Spa</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Lap</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Grd</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Nustatyti laiką</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Tvarkyti prisijungimus</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Išskleisti siūlomus prisijungimus</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Suskleisti siūlomus prisijungimus</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Siūlomi prisijungimai</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Dar kartą siųsti duomenis į šią svetainę?</string>
+ <string name="mozac_feature_prompt_repost_message">Šio tinklalapio įkėlimas iš naujo gali pakartoti paskiausius veiksmus, tokius kaip mokėjimo nusiuntimas ar komentaro parašymas.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Persiųsti duomenis</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Atsisakyti</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Pasirinkite mokėjimo kortelę</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Išskleisti siūlomas mokėjimo korteles</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Suskleisti siūlomas mokėjimo korteles</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Tvarkyti mokėjimo korteles</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Saugiai išsaugoti šią kortelę?</string>
+
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Atnaujinti kortelės galiojimo datą?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Kortelės numeris bus užšifruotas. Saugos kodas nebus išsaugotas.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Pasirinkite adresą</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Išskleisti siūlomus adresus</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Sutraukti siūlomus adresus</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Tvarkyti adresus</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..b81c10ce38
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-mix/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Vaá</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Kunchatu</string>
+
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Stòo</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Tu un seè</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Chika vaá</string>
+
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Yoo u un</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..3f1aae6abc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ml/strings.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ശരി</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">റദ്ദാക്കുക</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">അധിക ഡയലോഗുകൾ സൃഷ്ടിക്കുന്നതിൽ നിന്ന് ഈ പേജിനെ തടയുക</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">ക്രമീകരിക്കുക</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">മായ്ക്കുക</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">പ്രവേശിക്കുക</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ഉപയോക്തൃനാമം</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">രഹസ്യവാക്ക്</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">സൂക്ഷിക്കേണ്ട</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">സൂക്ഷിക്കുക</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">പുതുക്കേണ്ടതില്ല</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">പുതുക്കുക</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">രഹസ്യവാക്ക് ശൂന്യമായിരിക്കരുത്</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">ലോഗിൻ സൂക്ഷിക്കുവാനായില്ല</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ഈ ലോഗിൻ സംരക്ഷിക്കണോ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ഈ ലോഗിൻ പുതുക്കണോ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">സംരക്ഷിച്ച രഹസ്യവാക്കിലേക്ക് ഉപയോക്തൃനാമം ചേർക്കണോ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">ഒരു വാചക ഇൻപുട്ട് ഫീൽഡ് നൽകുന്നതിനുള്ള ലേബൽ</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ഒരു നിറം തിരഞ്ഞെടുക്കുക</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">അനുവദിക്കുക</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">നിരസിക്കുക</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">താങ്കൾക്ക് ഉറപ്പാണോ?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">നിങ്ങൾക്ക് ഈ സൈറ്റ് വിടണോ? നിങ്ങൾ നൽകിയ ഡാറ്റ സംരക്ഷിച്ചേക്കില്ല</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">തുടരുക</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">വിടുക</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ഒരു മാസം തിരഞ്ഞെടുക്കുക</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ജനു</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ഫെബ്രു</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">മാർ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ഏപ്രി</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">മെയ്</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ജൂൺ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ജൂലൈ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ഓഗ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">സെപ്തം</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ഒക്ടോ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">നവം</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ഡിസം</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..984e1ad59f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-mr/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ठीक आहे</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">रद्द करा</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">या पृष्ठास अतिरिक्त संवाद तयार करण्यापासून प्रतिबंधित करा</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">निश्चित करा</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">पुसा</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">साइन इन करा</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">वापरकर्ता नाव</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">पासवर्ड</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">जतन करू नका</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">कधीही जतन करू नका</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">जतन करा</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">अद्यावत करू नका</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">अद्ययावत करा</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">पासवर्ड क्षेत्र रिक्त नसावे</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">लॉगिन जतन करण्यात अक्षम</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">हे लॉगिन जतन करायचे?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">हे लॉगिन अद्यतन करायचे?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">जतन केलेल्या संकेतशब्दामध्ये वापरकर्तानाव जोडायचे?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">मजकूर इनपुट फील्ड प्रविष्ट करण्यासाठी लेबल</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">एक रंग निवडा</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">परवानगी द्या</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">नकारा</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">आपणास खात्री आहे का?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">आपण ही साइट सोडू इच्छिता? आपण प्रविष्ट केलेला डेटा कदाचित जतन केला जाणार नाही</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">थांबा</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">सोडा</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">महिना निवडा</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">जाने</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">फेब्रु</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">मार्च</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">एप्रिल</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">मे</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">जून</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">जुलै</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ऑगस्ट</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">सप्टें</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ऑक्टो</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">नोव्हें</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">डिसें</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">लॉगिन व्यवस्थापित करा</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">सूचित लॉगिन विस्तृत करा</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">सूचित लॉगिन संकुचित करा</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">सूचित लॉगिन</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">या साइटवर डेटा पुन्हा पाठवायचा?</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">डेटा पुन्हा पाठवा</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">रद्द करा</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..bc07bc46a3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-my/strings.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">အိုကေ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ပယ်​ဖျက်ပါ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ဤစာမျက်နှာအား နောက်ထပ် ဒိုင်ယာလော့ဂ်များ ဖန်တီးခြင်းမှ ပိတ်ထားမည်</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">သတ်မှတ်ရန်</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">ရှင်းလင်းပါ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">၀င်ပါ</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">သုံးစွဲသူ အမည်</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">စကားဝှက်</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">မသိမ်းပါနဲ့</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ဘယ်တော့မျှ မသိမ်းပါ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">သိမ်းပါ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">မပြင်ဆင် ပါနှင့်</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">ပြင်ဆင်ပါ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">လျှို့ဝှက်နံပါတ်ဖြည့်စွက်ပေးပါ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">လော့အင်ကိုမသိမ်းဆည်းနိုင်ပါ</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ဤ ဝင်ရောက်မှု ကို သိမ်းမည်လား။</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ဤ ဝင်ရောက်မှု ကို ပြင်ဆင်မည်လား?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">အခု သုံးဆွဲသူအား သိမ်းထားမည်လား?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">စာသားထည့်သွင်းရန်ပြထားသောလမ်းညွှန်စာသား</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">အရောင်ရွေးပါ</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ခွင့်ပြုပါ</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">တားမြစ်ပါ</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">သင် သေချာ သလား?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">ဒီ ဆိုက် မှ ထွက်လို သလား? သင် ထည့်သွင်းသော အချက်အလက်များ မသိမ်းရသေးပါ။</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">နေမည်</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ထွက်ခွာမည်</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">လ အားရွေးပါ</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ဇန်နဝါရီ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ဖေဖော်ဝါရီ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">မတ်</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ဧပြီ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">မေ</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ဇွန်</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ဇူလိုင်</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">သြဂုတ်</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">စက်တင်ဘာ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">အောက်တိုဘာ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">နိုဝင်ဘာ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ဒီဇင်ဘာ</string>
+
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">လော့အင် များ စီမံပါ</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">အကြံပြု လော့အင် များ ကို ဖြန့်ချပါ</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">အကြံပြု လော့အင်များ ကို ပြန်သိမ်းပါ</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">အကြံပြုထားသော လော့အင်များ</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ဤ ဆိုက် သို့ အချက်အလက်များ ပြန်ပို့မည်လား။</string>
+ <string name="mozac_feature_prompt_repost_message">ဤ စာမျက်နှာအား ပြန်လည်စတင်ခြင်းသည် ငွေပေးချေခြင်း သို့မဟုတ် မှက်ချက်များတင်ခြင်းကဲ့သို့သော လက်တလောလုပ်ဆောင်ချက်များအား နှစ်ကြိမ်ဖြစ်စေနိုင်သည်။</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">အချက်အလက် ပြန်ပို့ ရန်</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ပယ်​ဖျက်ပါ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">အကြွေးဝယ်ကဒ် ကိုရွေးပါ။</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">အကြုံပြုထားသော အကြွေးဝယ်ကတ်များကို ဖြန့်ချပါ</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">အကြုံပြုထားသော အကြွေးဝယ်ကတ်များကို ပြန်သိမ်းပါ</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">အကြွေးဝယ်ကတ်များကို စီမံပါ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..a35beb81bf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Avbryt</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Forhindre dette nettstedet fra å lage flere dialoger</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Angi</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Tøm</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Logg inn</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Brukernavn</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Passord</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ikke lagre</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ikke nå</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Lagre aldri</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ikke nå</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Lagre</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ikke oppdater</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ikke nå</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Oppdater</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Passordfeltet kan ikke stå tomt</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Skriv inn et passord</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Klarte ikke å lagre innloggingen</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Kan ikke lagre passordet</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Lagre denne innloggingen?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Lagre passord?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Vil du oppdatere denne innloggingen?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Oppdatere passord?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Vil du legge til brukernavn til lagret passord?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etikett for utfylling av et skrivefelt</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Velg en farge</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Tillat</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Avvis</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Er du sikker?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vil du forlate dette nettstedet? Data du har lagt inn, blir kanskje ikke lagret</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Bli</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Forlat</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Velg en måned</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Des</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Angi tid</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Behandle innlogginger</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Behandle passord</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Utvid foreslåtte innlogginger</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Utvid lagrede passord</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Slå sammen foreslåtte innlogginger</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Skjul lagrede passord</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Foreslåtte innlogginger</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Lagrede passord</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Foreslå sterkt passord</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Foreslå sterkt passord</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Bruk sterkt passord: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Send data på nytt til dette nettstedet?</string>
+ <string name="mozac_feature_prompt_repost_message">Oppdatering av denne siden kan duplisere nylige handlinger, for eksempel å sende en betaling eller legge igjen en kommentar to ganger.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Send data på nytt</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Avbryt</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Velg betalingskort</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Bruk lagret kort</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Utvid foreslåtte betalingskort</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Utvid lagrede kort</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Slå sammen foreslåtte betalingskort</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Skjul lagrede kort</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Behandle betalingskort</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Behandle kort</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Lagre dette kortet trygt?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Oppdatere kortets utløpsdato?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Kortnummer vil bli kryptert. Sikkerhetskoden blir ikke lagret.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s krypterer kortnummeret ditt. Sikkerhetskoden din blir ikke lagret.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Velg adresse</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Utvid foreslåtte adresser</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Utvid lagrede adresser</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Slå sammen foreslåtte adresser</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Skjul lagrede adresser</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Behandle adresser</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Kontobilde</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Velg en innloggingsleverandør</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Logg på med en %1$s-konto</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Bruk %1$s som innloggingsleverandør</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Å logge på %1$s med en %2$s-konto er underlagt deres <a href="%3$s">personvernbestemmelser</a> og <a href="%4$s">tjenestevilkår</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Fortsett</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Avbryt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..b47563813a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">टीक छ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">रद्द गर्नुहोस्</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">यस पृष्ठलाई थप संवादहरू सिर्जना हुनबाट रोक्नुहोस्</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">सेट गर्नुहोस्</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">खाली गर्नुहोस्</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">साइन इन</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">प्रयोगकर्ताको नाम</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">पासवर्ड</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">सेभ नगर्नुहोस्</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">कहिल्यै सेभ नगर्नुहोस्</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">सेभ</string>
+
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">अद्यावधिक नगर्नुहोस्</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">अद्यावधिक</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">पासवर्ड लेख्ने ठाउँ खाली राख्न हुँदैन</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">लगइन सेभ गर्न सकिएन</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">यो लगइन सेभ गर्न चाहानुहुन्छ ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">यो लगइन अद्यावधिक गर्न चाहानुहुन्छ ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">सेभ गरिएको पासवर्डमा प्रयोगकर्ता नाम थप्न चाहानुहुन्छ ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">अक्षर आगत क्षेत्र प्रविष्ट गर्नको लागि लेबल</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">एउटा रङ्ग छान्नुहोस्</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">अनुमति दिनुहोस्</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">अस्वीकार गर्नुहोस्</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">के तपाईँ निश्चित हुनुहुन्छ ?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">के तपाईं यो साइट छोड्न चाहनुहुन्छ? तपाईंले प्रविष्ट गर्नुभएको डाटा सेभ नहुन पनि सक्छ</string>
+
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">यहि रहनुहोस्</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">छोड्नुहोस्</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">कुनै एउटा महिना छान्नुहोस्</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">जनवरी</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">फेब्रुअरी</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">मार्च</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">अप्रिल</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">मे</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">जुन</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">जुलाई</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">अगस्ट</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">सेप्टेम्बर</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">अक्टोबर</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">नोभेम्बर</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">डिसेम्बर</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">लगइनहरु प्रबन्ध गर्नुहोस्</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">सुझाब गरिएका लगइनहरू विस्तार गर्नुहोस्</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">सुझाब गरिएका लगइनहरू संक्षिप्त गर्नुहोस्</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">सुझाब गरिएका लगइनहरु</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">यस साइटमा डाटा पुन: पठाउन चाहानुहुन्छ?</string>
+ <string name="mozac_feature_prompt_repost_message">यो पृष्ठ ताजा गर्दा अहिलेका कार्यहरू नक्कल हुन सक्छ, जस्तै भुक्तानी पठाउने वा दुई पटक टिप्पणी पोष्ट गर्ने।</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">डाटा पुन: पठाउनुहोस्</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">रद्द गर्नुहोस्</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">क्रेडिट कार्ड छान्नुहोस्</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">सुझाव गरिएका क्रेडिट कार्डहरू विस्तार गर्नुहोस्</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">सुझाव गरिएका क्रेडिट कार्डहरू संक्षिप्त गर्नुहोस्</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings -->
+ <string name="mozac_feature_prompts_manage_credit_cards">क्रेडिट कार्डहरू ब्यवस्थापन गर्नुहोस्</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..8f4e475773
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-nl/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Annuleren</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Voorkomen dat deze pagina extra dialoogvensters maakt</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Instellen</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Wissen</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Aanmelden</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Gebruikersnaam</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Wachtwoord</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Niet opslaan</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Niet nu</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nooit opslaan</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Niet nu</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Opslaan</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Niet bijwerken</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Niet nu</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Bijwerken</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Wachtwoordveld mag niet leeg zijn</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Vul een wachtwoord in</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Kan aanmelding niet opslaan</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Kan wachtwoord niet opslaan</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Deze aanmelding opslaan?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Wachtwoord opslaan?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Deze aanmelding bijwerken?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Wachtwoord bijwerken?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Gebruikersnaam aan opgeslagen wachtwoord toevoegen?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label voor het invullen van een tekstinvoerveld</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Kies een kleur</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Toestaan</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Weigeren</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Weet u het zeker?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Wilt u deze website verlaten? Ingevoerde gegevens worden mogelijk niet opgeslagen</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Blijven</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Verlaten</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Kies een maand</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">mrt</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">mei</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Tijd instellen</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Aanmeldingen beheren</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Wachtwoorden beheren</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Voorgestelde aanmeldingen uitvouwen</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Opgeslagen wachtwoorden uitvouwen</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Voorgestelde aanmeldingen inklappen</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Opgeslagen wachtwoorden inklappen</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Voorgestelde aanmeldingen</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Opgeslagen wachtwoorden</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sterk wachtwoord voorstellen</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sterk wachtwoord voorstellen</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Sterk wachtwoord gebruiken: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Gegevens opnieuw naar deze website verzenden?</string>
+ <string name="mozac_feature_prompt_repost_message">Het opnieuw laden van deze pagina kan recente acties dupliceren, zoals het verzenden van een betaling of het tweemaal plaatsen van een bericht.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Gegevens opnieuw verzenden</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Annuleren</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Selecteer creditcard</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Opgeslagen kaart gebruiken</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Voorgestelde creditcards uitbreiden</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Opgeslagen kaarten uitvouwen</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Voorgestelde creditcards inklappen</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Opgeslagen kaarten inklappen</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Creditcards beheren</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Kaarten beheren</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Deze kaart veilig opslaan?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Vervaldatum kaart bijwerken?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Het kaartnummer wordt versleuteld. De beveiligingscode wordt niet opgeslagen.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s versleutelt uw kaartnummer. Uw beveiligingscode wordt niet opgeslagen.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Adres selecteren</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Voorgestelde adressen uitvouwen</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Opgeslagen adressen uitvouwen</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Voorgestelde adressen inklappen</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Opgeslagen adressen inklappen</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Adressen beheren</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Accountafbeelding</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Kies een aanmeldprovider</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Aanmelden met een %1$s-account</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s als aanmeldprovider gebruiken</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Aanmelding bij %1$s met een %2$s-account valt onder hun <a href="%3$s">Privacybeleid</a> en <a href="%4$s">Servicevoorwaarden</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Doorgaan</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Annuleren</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..6266243205
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Avbryt</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Hindre denne nettsaden frå å lage fleire dialogar</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Spesifiser</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Tøm</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Logg inn</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Brukarnamn</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Passord</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Ikkje lagre</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Ikkje no</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Lagre aldri</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ikkje no</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Lagre</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Ikkje oppdater</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Ikkje no</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Oppdater</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Passordfeltet kan ikkje stå tomt</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Skriv inn passord</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Klarte ikkje å lagre innlogginga</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Lagre denne innlogginga?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Lagre passord?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Vil du oppdatere denne innlogginga?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Oppdatere passord?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Leggje brukarnamn til lagra passord?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etikett for utfylling av eit skrivefelt</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Vel ein farge</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Tillat</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Avvis</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Er du sikker?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vil du forlate denne nettstaden Data du har lagt inn, vert kanskje ikkje lagra</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Bli</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Forlat</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Vel ein månad</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Des</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Oppgi tid</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Handsam innloggingar</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Handsam passord</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Utvid føreslåtte innloggingar</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Utvid lagra passord</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Slå saman føreslåtte innloggingar</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Føreslåtte innloggingar</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Lagra passord</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Føreslå sterkt passord</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Føreslå sterkt passord</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Bruk sterkt passord: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Sende data på nytt til denne nettstaden?</string>
+ <string name="mozac_feature_prompt_repost_message">Oppdatering av denne sida kan duplisere nylege handlingar, til dømes å sende ei betaling eller leggje igjen ein kommentar to gongar.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Send data på nytt</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Avbryt</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Vel betalingskort</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Bruk lagra kort</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Utvid føreslått betalingskort</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Minimer føreslått betalingskort</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Handsam betalingskort</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Handsam kort</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Lagre dette kortet trygt?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Oppdatere siste bruksdato for kortet?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Kortnummeret vil bli kryptert. Tryggingskoden vert ikkje lagra.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Vel adresse</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Utvid føreslåtte adresser</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Utvid lagra adresser</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Slå saman føreslåtte adresser</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Handsam adresser</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Kontobilde</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Vel ein innloggingsleverandør</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Log inn med ein %1$s-konto</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Bruk %1$s som innloggingsleverandør</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Å logge på %1$s med ein %2$s-konto er underlagt <a href="%3$s">personvernerklæringa</a> og <a href="%4$s">tenestevilkåra</a> deira]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Fortset</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Avbryt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..f835c12ef6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-oc/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">D’acòrdi</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Anullar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Empachar aquesta pagina de dobrir de dialògs suplementaris</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Definir</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Escafar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Se connectar</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nom d’utilizaire</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Senhal</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Enregistrar pas</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Pas ara</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Enregistrar pas jamai</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Pas ara</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Enregistrar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Metre pas a jorn</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Pas ara</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Metre a jorn</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Lo camp senhal pòt pas èsser void</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Picatz un senhal</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Impossible d’enregistrar l’identificant</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Enregistrament de senhal impossible</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Salvar aqueste identificant ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Salvar lo senhal ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Metre a jorn aqueste identificant ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Actualizar lo senhal ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Apondre un nom d’utilizaire al senhal salvat ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta per la creacion d’un camp de picada de tèxt</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Causir una color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Autorizar</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Refusar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">O confirmatz ?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Volètz quitar aqueste site ? Las donadas qu’ajatz picadas seràn benlèu pas enregistradas.</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Demorar</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Quitar</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Causir un mes</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Gen</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Març</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abril</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Junh</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Julh</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Causida del temps</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gerir los identificants</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gerir los senhals</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Espandir los identificants suggerits</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Desplegar los senhals salvats</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Plegar los identificants suggerits</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Plegar los senhals salvats</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Identificants recomandats</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Senhals salvats</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggerir un senhal fòrt</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggerir un senhal fòrt</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Utilizar un senhal fòrt : %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Tornar enviar las donadas a aqueste site ?</string>
+ <string name="mozac_feature_prompt_repost_message">L’actualizacion d’aquesta pagina poiriá menar a la duplicacion d’accions recentas coma enviar un pagament o publicar un comentari dos còps.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Tornar enviar</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Anullar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seleccionar carta de crèdit</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Utilizar una carta enregistrada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Espandir las cartas de crèdit suggeridas</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Desplegar las cartas enregistradas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Plegar las cartas de crèdit suggeridas</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Plegar las cartas enregistradas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gerir las cartas de crèdit</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gerir las cartas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Salvar d’un biais segur aquesta carta ?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Actualizar la data d’expiracion de la carta ?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Los numèros de carta son chifrats. Se gardarà pas lo còdi de seguretat.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s chifra lo numèro de carta. Lo còdi de seguretat s’enregistrarà pas.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seleccion d’adreça</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Espandir las adreças suggeridas</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Desplegar las adreças enregistradas</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Plegar las adreças suggeridas</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Plegar las adreças enregistradas</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gestion de las adreças</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imatge del compte</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Causir un provesidor d’identificants</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Se connectar amb un compte %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Causir %1$s coma provesidor d’identificants</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[La connexion a %1$s amb un compte %2$s es somesa a la <a href="%3$s">politica de confidencialitat</a> e a las <a href="%4$s">condicions d’utilizacion</a> d’aqueste.]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Contunhar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Anullar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..37034a6663
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-or/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ଠିକ୍ ଅଛି</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ବାତିଲ କରନ୍ତୁ</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ଉପଭୋକ୍ତାଙ୍କ ନାମ</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">ପାସ୍‍ୱାର୍ଡ଼</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">ପାସ୍‍ୱାର୍ଡ଼ ଖାଲି ଛଡ଼ାଯିବା ଉଚିତ୍ ନୁହେଁ</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">ସଞ୍ଚିତ ପାସ୍‍ୱାର୍ଡ଼ ପାଇଁ ଉପଭୋକ୍ତାଙ୍କ ନାମ ଯୋଡ଼ିବେ?</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ଏକ ରଙ୍ଗ ବାଛନ୍ତୁ</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">ଆପଣ ନିଶ୍ଚିତ ଅଛନ୍ତି ତ?</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ରୁହନ୍ତୁ</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ଛାଡ଼ନ୍ତୁ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ମାସ ବାଛନ୍ତୁ</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ଜାନୁ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ଫେବୃ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">ମାର୍ଚ୍ଚ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ଅପ୍ରେ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">ମଇ</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ଜୁନ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ଜୁଲା</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ଅଗଷ୍ଟ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ସେପ୍ଟେ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ଅକ୍ଟୋ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ନଭେ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ଡିସେ</string>
+
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ବାତିଲ କରନ୍ତୁ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..8cc587826d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ਠੀਕ ਹੈ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ਰੱਦ ਕਰੋ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ਇਸ ਸਫ਼ੇ ਨੂੰ ਹੋਰ ਡਾਈਲਾਗ ਬਣਾਉਣ ਤੋਂ ਰੋਕੋ</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">ਸੈੱਟ ਕਰੋ</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">ਸਾਫ਼ ਕਰੋ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ਸਾਈਨ ਇਨ</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ਵਰਤੋਂਕਾਰ ਨਾਂ</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">ਪਾਸਵਰਡ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">ਨਾ ਸੰਭਾਲੋ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">ਹੁਣੇ ਨਹੀਂ</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ਕਦੇ ਨਾ ਸੰਭਾਲੋ</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ਹੁਣੇ ਨਹੀਂ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">ਸੰਭਾਲੋ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">ਅੱਪਡੇਟ ਨਾ ਕਰੋ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">ਹੁਣੇ ਨਹੀਂ</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">ਅੱਪਡੇਟ ਕਰੋ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">ਪਾਸਵਰਡ ਖੇਤਰ ਖਾਲੀ ਨਹੀਂ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">ਪਾਸਵਰਡ ਦਿਓ</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">ਲਾਗਇਨ ਸੰਭਾਲਣ ਲਈ ਅਸਮਰੱਥ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">ਪਾਸਵਰਡ ਸੰਭਾਲਿਆ ਨਹੀਂ ਜਾ ਸਕਦਾ ਹੈ</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ਇਹ ਲਾਗਇਨ ਸੰਭਾਲਣਾ ਹੈ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">ਪਾਸਵਰਡ ਸੰਭਾਲਣਾ ਹੈ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ਇਹ ਲਾਗਇਨ ਅੱਪਡੇਟ ਕਰਨਾ ਹੈ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">ਪਾਸਵਰਡ ਅੱਪਡੇਟ ਕਰਨਾ ਹੈ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">ਵਰਤੋਂਕਾਰ-ਨਾਂ ਨੂੰ ਸੰਭਾਲੇ ਪਾਸਵਰਡ ਵਿੱਚ ਜੋੜਨਾ ਹੈ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">ਲਿਖਣ ਵਾਲੇ ਖਾਨੇ ਵਿੱਚ ਜਾਣ ਲਈ ਲੇਬਲ</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ਰੰਗ ਚੁਣੋ</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ਆਗਿਆ ਦਿਓ</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ਇਨਕਾਰ ਕਰੋ</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">ਕੀ ਤੁਸੀਂ ਚਾਹੁੰਦੇ ਹੋ?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">ਕੀ ਤੁਸੀਂ ਇਹ ਸਾਈਟ ਛੱਡਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਤੁਹਾਡੇ ਵਲੋਂ ਦਿੱਤਾ ਡਾਟਾ ਸੰਭਾਲਿਆ ਨਹੀਂ ਵੀ ਗਿਆ ਹੋ ਸਕਦਾ ਹੈ</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ਇੱਥੇ ਹੀ ਰਹੋ</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ਛੱਡੋ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ਮਹੀਨਾ ਚੁਣੋ</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ਜਨ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ਫਰ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">ਮਾਰਚ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ਅਪ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">ਮਈ</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ਜੂਨ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ਜੁਲ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ਅਗ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ਸਤੰ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ਅਕਤੂ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ਨਵੰ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ਦਸੰ</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">ਸਮਾਂ ਸੈੱਟ ਕਰੋ</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">ਲਾਗਇਨਾਂ ਦਾ ਇੰਤਜ਼ਾਮ</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">ਪਾਸਵਰਡਾਂ ਦਾ ਇੰਤਜ਼ਾਮ</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">ਸੁਝਾਏ ਲਾਗਇਨ ਫੈਲਾਓ</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">ਸੰਭਾਲੇ ਹੋਏ ਪਾਸਵਰਡਾਂ ਨੂੰ ਫੈਲਾਓ</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">ਸੁਝਾਏ ਲਾਗਇਨ ਸਮੇਟੋ</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">ਸੰਭਾਲੇ ਹੋਏ ਪਾਸਵਰਡਾਂ ਨੂੰ ਸਮੇਟੋ</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">ਸੁਝਾਏ ਗਏ ਲਾਗਇਨ</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">ਸੰਭਾਲੇ ਹੋਏ ਪਾਸਵਰਡ</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">ਮਜ਼ਬੂਤ ਪਾਸਵਰਡ ਲਈ ਸੁਝਾਅ</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">ਮਜ਼ਬੂਤ ਪਾਸਵਰਡ ਲਈ ਸੁਝਾਅ</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">ਮਜ਼ਬੂਤ ਪਾਸਵਰਡ ਨੂੰ ਵਰਤੋਂ: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ਇਸ ਸਾਈਟ ਲਈ ਡਾਟਾ ਮੁੜ-ਭੇਜਣਾ ਹੈ?</string>
+ <string name="mozac_feature_prompt_repost_message">ਇਸ ਸਫ਼ੇ ਨੂੰ ਮੁੜ-ਤਾਜ਼ਾ ਕਰਨ ਨਾਲ ਤਾਜ਼ਾ ਕਾਰਵਾਈਆਂ ਨੂੰ ਦੁਹਰਾਇਆ ਜਾਵੇਗਾ
+ਜਿਵੇਂ ਕਿ ਭੁਗਤਾਨ ਭੇਜਣ ਜਾਂ ਟਿੱਪਣੀ ਨੂੰ ਦੋ ਵਾਰ ਪੋਸਟ ਕੀਤਾ ਜਾਵੇਗਾ।</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ਡਾਟਾ ਮੁੜ-ਭੇਜੋ</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ਰੱਦ ਕਰੋ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">ਕਰੈਡਿਟ ਕਾਰਡ ਚੁਣੋ</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">ਸੰਭਾਲੇ ਕਾਰਡ ਨੂੰ ਵਰਤੋਂ</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">ਸੁਝਾਏ ਗਏ ਕਰੈਡਿਟ ਕਾਰਡ ਨੂੰ ਫੈਲਾਓ</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">ਸੰਭਾਲੇ ਹੋਏ ਕਾਰਡਾਂ ਨੂੰ ਫੈਲਾਓ</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">ਸੁਝਾਏ ਗਏ ਕਰੈਡਿਟ ਕਾਰਡ ਨੂੰ ਸਮੇਟੋ</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">ਸੰਭਾਲੇ ਹੋਏ ਕਾਰਡਾਂ ਨੂੰ ਸਮੇਟੋ</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">ਕਰੈਡਿਟ ਕਾਰਡਾਂ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰੋ</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">ਕਾਰਡਾਂ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰੋ</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">ਇਹ ਕਾਰਡ ਨੂੰ ਸੁਰੱਖਿਅਤ ਢੰਗ ਨਾਲ ਸੰਭਾਲਣਾ ਹੈ?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">ਕਾਰਡ ਦੀ ਮਿਆਦ ਪੁੱਗਣ ਦੀ ਤਾਰੀਖ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨਾ ਹੈ?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">ਕਾਰਡ ਨੰਬਰ ਨੂੰ ਇੰਕ੍ਰਿਪਟ ਕੀਤਾ ਜਾਵੇਗਾ। ਸੁਰੱਖਿਆ ਕੋਡ ਸੰਭਾਲਿਆ ਨਹੀਂ ਜਾਵੇਗਾ।</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s ਤੁਹਾਡੇ ਕਾਰਡ ਨੂੰ ਇੰਕ੍ਰਿਪਟ ਕਰਦਾ ਹੈ। ਤੁਹਾਡੇ ਸੁਰੱਖਿਆ ਕੋਡ ਨੂੰ ਸੰਭਾਲਿਆ ਨਹੀਂ ਜਾਵੇਗਾ।</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">ਸਿਰਨਾਵਾਂ ਚੁਣੋ</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">ਸੁਝਾਏ ਸਿਰਨਾਵਿਆਂ ਨੂੰ ਫੈਲਾਓ</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">ਸੰਭਾਲੇ ਹੋਏ ਸਿਰਨਾਵਿਆਂ ਨੂੰ ਫੈਲਾਓ</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">ਸੁਝਾਏ ਸਿਰਨਾਵਿਆਂ ਨੂੰ ਸਮੇਟੋ</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">ਸੰਭਾਲੇ ਹੋਏ ਸਿਰਨਾਵਿਆਂ ਨੂੰ ਸਮੇਟੋ</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">ਸਿਰਨਾਵਿਆਂ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰੋ</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">ਖਾਤੇ ਦੀ ਤਸਵੀਰ</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">ਲਾਗਇਨ ਪੂਰਕ ਚੁਣੋ</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s ਖਾਤੇ ਨਾਲ ਸਾਈਨ ਇਨ ਕਰੋ</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s ਨੂੰ ਲਾਗਇਨ ਪੂਰਕ ਵਜੋਂ ਵਰਤੋਂ</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[%1$s ਵਿੱਚ %2$s ਖਾਤੇ ਨਾਲ ਲਾਗਇਨ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ, ਜੋ ਕਿ <a href="%3$s">ਪਰਦੇਦਾਰੀ ਨੀਤੀ</a> ਅਤੇ <a href="%4$s">ਸੇਵਾ ਦੀਆਂ ਸ਼ਰਤਾਂ</a> ਦੇ ਅਧੀਨ ਹੈ।]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">ਜਾਰੀ ਰੱਖੋ</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">ਰੱਦ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..f3e24af90d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ٹھیک اے</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">رد کرو</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ایس صفحے توں کوئی ہور سوال روکو</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">پا لاؤ</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">صاف کرو</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">لوگ این کرو</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ورتنوالے دا ناں</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">پاس‌ورڈ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">ایہہ نہ رکھو</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">کدے نہ رکھو</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ہݨے نہیں</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">سانجھا کرو</string>
+
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">ایہنوں نہ نواں کرو</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">نواں کرو</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">پاس‌ورڈ نہیں پایا</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">پاس‌ورڈ رکھ نہیں سکدا</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">پاس‌ورڈ رکھیو؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ایہنوں نواں کریو؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">پاس‌ورڈ نال ورتنوالے دا ناں وی رکھیو؟</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">لکھت دی تھاں لئی تفصیل</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">رنگ چݨو</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">اجازت دیو</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">اجازت نہ دیو</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">تسیں پکے او؟</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">تسیں پکے او، ایس سائٹ توں چھڈݨا چاہیدے؟ کوئی کجھ پاۓ گئے ڈیٹے نہیں رکھےگا۔</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ایتھوں ای رہو</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">چڈو</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">مہینہ چݨو</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">جنوری</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">فیوری</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">مارچ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">اپریل</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">مئی</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">جون</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">جولائی</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">اگست</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">سرمبر</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">اکتوبر</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">نومبر</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">دسمبر</string>
+
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">ویلے نوں بدلو</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">پاس‌ورڈاں دا انتظام</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">تجویز دے ویروے ویکھو</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">تجویز لُکاؤ</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">تجویز دے ویروے</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">مضبوط پاس‌ورڈ لئی تجویز کرو</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">مضبوط پاس‌ورڈ لئی تجویز کرو</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">مضبوط پاس‌ورڈ نوں ورتوں: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">فیر بھیجیو؟</string>
+ <string name="mozac_feature_prompt_repost_message">ایس صفحے نوں مڑ تازہ کرن نال تاشہ کاروائیاں نوں دہرایا جاۓگا؛ جیویں کہ بھگتان بھیجݨ یاں ٹپݨی نوں دو وار پا لایا جاۓگا۔</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">آہو، فیر بھیجو</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">رد کرو</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">کریڈٹ کارڈ چݨو</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">تجویز دے کارڈ ویکھو</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">تجویز دے کارڈ لکاؤ</string>
+
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">کریڈٹ کارڈاں دا انتظام کرو</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">سرکھیت نال کارڈ دا نمبر رکھو؟</string>
+
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">تسیں کارڈ دی میاد پگݨ دی تاریخ نوں بدلݨا چاہیدے او؟</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">کارڈ نمبر نوں اوہلا پا لا جاۓگا۔ سرکھیا کوڈ رکھ نہیں جاۓگا۔</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">پتہ چݨو</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">ہور پتے ویکھو</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">تجویز دے پتے لکاؤ</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">پتیاں دا انتظام</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">کھاتے دی تصویر</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">لاگ ان دیݨ والا چݨو</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s کھاتے نال لاگ ان کرو</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s نوں لاگ ان دیݨ والے وجوں ورتوں</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[%1$s وچ %2$s کھاتے نال لاگ ان کیتا جا رہا اے، جو کہ <a href="%3$s">پردے داری نیتی</a> تے <a href="%4$s">سیوا دیاں شرطاں</a> دے ادھین اے]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">جاری رکھو</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">رد کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..92107e968f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pl/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Anuluj</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Zabroń tej stronie otwierać kolejne okna dialogowe</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ustaw</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Wyczyść</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Logowanie</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nazwa użytkownika</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Hasło</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Nie zachowuj</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Nie teraz</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nigdy nie zachowuj</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Nie teraz</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Zachowaj</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Nie aktualizuj</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Nie teraz</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Aktualizuj</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Pole hasła nie może być puste</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Wpisz hasło</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Nie można zachować danych logowania</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Nie można zachować hasła</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Czy zachować te dane logowania?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Czy zachować hasło?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Czy zaktualizować te dane logowania?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Czy zaktualizować hasło?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Czy dodać nazwę użytkownika do zachowanego hasła?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etykieta przechodzenia do pola wprowadzania tekstu</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Wybierz kolor</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Zezwól</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Zabroń</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Potwierdzenie</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Czy chcesz opuścić tę stronę? Wprowadzone dane mogły nie zostać zapisane.</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Zostań</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Opuść</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Wybierz miesiąc</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">sty</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">lut</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">kwi</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">cze</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">lip</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">sie</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">wrz</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">paź</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">lis</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">gru</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Ustaw czas</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Zarządzaj danymi logowania</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Zarządzaj hasłami</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Rozwiń podpowiadane dane logowania</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Rozwiń zachowane hasła</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Zwiń podpowiadane dane logowania</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Zwiń zachowane hasła</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Podpowiadane dane logowania</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Zachowane hasła</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Zaproponuj silne hasło</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Zaproponuj silne hasło</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Użyj silnego hasła: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Czy ponownie przesłać dane do tej witryny?</string>
+ <string name="mozac_feature_prompt_repost_message">Odświeżenie tej strony może spowodować powtórzenie ostatnich działań, na przykład jeszcze raz wysłać płatność lub opublikować komentarz dwa razy.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Prześlij ponownie</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Anuluj</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Wybierz kartę płatniczą</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Użyj zachowanej karty</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Rozwiń podpowiadane karty płatnicze</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Rozwiń zachowane karty</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Zwiń podpowiadane karty płatnicze</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Zwiń zachowane karty</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Zarządzaj kartami płatniczymi</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Zarządzaj kartami</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Czy bezpiecznie zachować tę kartę?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Czy zaktualizować datę ważności karty?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Numer karty zostanie zaszyfrowany. Kod zabezpieczający nie zostanie zachowany.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s szyfruje numer karty. Kod zabezpieczający nie zostanie zachowany.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Wybierz adres</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Rozwiń podpowiadane adresy</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Rozwiń zachowane adresy</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Zwiń podpowiadane adresy</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Zwiń zachowane adresy</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Zarządzaj adresami</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Obraz konta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Wybierz dostawcę logowania</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Zaloguj się za pomocą konta %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Używaj konta %1$s jako dostawcę logowania</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Logowanie na %1$s za pomocą konta %2$s podlega <a href="%3$s">zasadom ochrony prywatności</a> i <a href="%4$s">regulaminowi usługi</a> danego konta]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Kontynuuj</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Anuluj</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..de8cab92f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Impedir que esta página crie diálogos adicionais</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Definir</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Limpar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Entrar</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nome de usuário</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Senha</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Não salvar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Agora não</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nunca salvar</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Agora não</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Salvar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Não atualizar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Agora não</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Atualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">O campo da senha não deve ficar vazio</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Digite uma senha</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Não foi possível salvar a conta</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Não foi possível salvar a senha</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Salvar esta conta?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Salvar senha?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Atualizar esta conta?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Atualizar senha?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Adicionar nome de usuário à senha salva?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta para inserir um campo de entrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Escolha uma cor</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Negar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Tem certeza?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Quer sair deste site? Os dados que você digitou podem não ser salvos</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Ficar</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Sair</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Escolha um mês</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fev</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Out</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dez</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Ajustar hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gerenciar contas</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gerenciar senhas</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expandir sugestão de contas</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expandir senhas salvas</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Recolher sugestão de contas</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Recolher senhas salvas</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Sugestão de contas</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Senhas salvas</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugerir senha forte</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugerir senha forte</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Usar senha forte: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Reenviar dados para este site?</string>
+ <string name="mozac_feature_prompt_repost_message">Atualizar esta página pode duplicar ações recentes, como enviar um pagamento ou publicar um comentário duas vezes.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reenviar dados</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancelar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Selecionar cartão de crédito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Usar cartão salvo</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expandir sugestões de cartão de crédito</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expandir cartões salvos</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Recolher sugestões de cartão de crédito</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Recolher cartões salvos</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gerenciar cartões de crédito</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gerenciar cartões</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Salvar este cartão com segurança?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Atualizar data de validade do cartão?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">O número do cartão será criptografado. O código de segurança não será salvo.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">O %s criptografa o número do seu cartão. O código de segurança não é salvo.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Selecionar endereço</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expandir endereços sugeridos</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expandir endereços salvos</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Recolher endereços sugeridos</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Recolher endereços salvos</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gerenciar endereços</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imagem da conta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Escolha um provedor de autenticação</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Entre com uma conta %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Usar %1$s como um provedor acesso a contas</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Entrar em %1$s com uma conta %2$s está sujeito à sua <a href="%3$s">política de privacidade</a> e seus <a href="%4$s">termos do serviço</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Avançar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..42564fdfe1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancelar</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Impedir esta página de criar novas janelas</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Definir</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Limpar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Iniciar sessão</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nome de utilizador</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Palavra-passe</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Não guardar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Agora não</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nunca guardar</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Agora não</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Guardar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Não atualizar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Agora não</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Atualizar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">O campo de palavra-passe não deve estar vazio</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Introduza uma palavra-passe</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Não foi possível guardar a credencial</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Não foi possível guardar a palavra-passe</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Guardar esta credencial?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Guardar palavra-passe?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Atualizar esta credencial?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Atualizar palavra-passe?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Adicionar nome de utilizador à palavra-passe guardada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiqueta para introduzir um campo de entrada de texto</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Escolha uma cor</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permitir</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Recusar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Tem a certeza?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Deseja sair deste site? Os dados que introduziu poderão não ser guardados</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Ficar</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Sair</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Selecione um mês</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fev</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Out</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dez</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Definir hora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gerir credenciais</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gerir palavras-passes</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expandir credenciais sugeridas</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expandir palavras-passe guardadas</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Colapsar credenciais sugeridas</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Colapsar palavras-passe guardadas</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Credenciais sugeridas</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Palavras-passe guardadas</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugerir palavra-passe forte</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugerir palavra-passe forte</string>
+
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Utilizar palavra-passe forte: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Reenviar dados para este site?</string>
+ <string name="mozac_feature_prompt_repost_message">Atualizar esta página pode duplicar ações recentes, tais como enviar um pagamento ou publicar novamente um comentário. </string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Reenviar dados</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancelar</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Selecionar cartão de crédito</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Utilizar cartão guardado</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expandir os cartões de créditos sugeridos</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expandir cartões guardados</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Colapsar os cartões de crédito sugeridos</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Colapsar cartões guardados</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gerir cartões de crédito</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gerir cartões</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Guardar este cartão com segurança?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Atualizar a data de validade do cartão?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">O número do cartão será encriptado. O código de segurança não será guardado.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">O %s encripta o número do seu cartão. O seu código de segurança não será guardado.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Selecionar endereço</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expandir endereços sugeridos</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expandir endereços guardados</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Colapsar endereços sugeridas</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Colapsar endereços guardados</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gerir endereços</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Imagem da conta</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Escolha um fornecedor de autenticação</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Iniciar sessão com uma conta %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Utilizar %1$s como fornecedor de início de sessão</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Ao iniciar sessão em %1$s com uma conta %2$s está sujeito à sua <a href="%3$s">Política de Privacidade</a> e <a href="%4$s">Termos de Serviço</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancelar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..2b7976fc10
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-rm/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Interrumper</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Impedir che questa pagina creeschia ulteriurs dialogs</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Definir</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Svidar</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">S\'annunziar</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Num d\'utilisader</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Pled-clav</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Betg memorisar</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Betg ussa</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Mai memorisar</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Betg ussa</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Memorisar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Betg actualisar</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Betg ussa</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualisar</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Il champ dal pled-clav na dastga betg esser vid</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Endatar in pled-clav</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Impussibel da memorisar las datas d\'annunzia</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Impussibel da memorisar il pled-clav</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Memorisar questa infurmaziun d\'annunzia?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Memorisar il pled-clav?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Actualisar questa infurmaziun d\'annunzia?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Actualisar il pled-clav?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Agiuntar in num d\'utilisader al pled-clav memorisà?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Legenda dad in champ per inserir text</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Tscherner ina colur</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permetter</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Refusar</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Es ti segir?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vuls ti propi bandunar questa pagina? Las datas che ti has endatà n\'èn forsa betg memorisadas</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Restar</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Bandunar</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Tscherner in mais</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">schan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">favr</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">mars</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">avr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">matg</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">zercl</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">fan</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">avu</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">sett</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Drizzar l\'ura</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Administrar las datas d\'annunzia</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Administrar ils pleds-clav</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expander las datas d\'annunzia proponidas</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Extender ils pleds-clav memorisads</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Reducir las datas d\'annunzia proponidas</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Reducir ils pleds-clav memorisads</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Datas d\'annunzia proponidas</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Pleds-clav memorisads</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Proponer in ferm pled-clav</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Proponer in ferm pled-clav</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Utilisar in ferm pled-clav: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Anc ina giada trametter las datas a questa website?</string>
+ <string name="mozac_feature_prompt_repost_message">Cun actualisar questa pagina vegnan eventualmain duplitgadas acziuns recentas (sco far in pajament u publitgar duas giadas in commentari).</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Trametter anc ina giada</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Interrumper</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Tscherner ina carta da credit</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Utilisar ina carta memorisada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expander las cartas da credit proponidas</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Extender las cartas memorisadas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Reducir las cartas da credit proponidas</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Reducir las cartas memorisadas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Administrar las cartas da credit</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Administrar las cartas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Memorisar questa carta a moda segira?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Actualisar la data da scadenza da la carta?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Il numer da la carta vegn criptà. Il code da segirezza na vegn betg memorisà.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s criptescha il numer da tia carta. Tes code da segirezza na vegn betg memorisà.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Tscherner l\'adressa</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expander las adressas proponidas</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Extender las adressas proponidas</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Cumprimer las adressas proponidas</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Reducir las adressas memorisadas</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Administrar las adressas</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Maletg da conto</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Tscherner in purschider per l\'annunzia</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">T\'annunzia cun in conto %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Utilisar %1$s sco purschider d\'annunzia</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[L\'annunzia tar %1$s cun in conto da %2$s è suttamessa a las <a href="%3$s">Directivas per la protecziun da datas</a> e las <a href="%4$s">Cundiziuns d\'utilisaziun</a> correspundentas]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Cuntinuar</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Interrumper</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..2459648434
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ro/strings.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Anulare</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Împiedică acestă pagină să creeze dialoguri adiționale</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Setează</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Şterge</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Autentificare</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nume de utilizator</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Parolă</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Nu salva</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nu salva niciodată</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Salvează</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Nu actualiza</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Actualizează</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Câmpul de parolă nu trebuie să fie gol</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Datele de autentificare nu au putut fi salvate</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Salvezi aceste date de autentificare?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Actualizezi aceste date de autentificare?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Adaugi numele de utilizator la parola salvată?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etichetă pentru crearea unui câmp de introducere text</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Alege o culoare</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permite</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Refuză</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ești sigur(ă)?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vrei să ieși de pe acest site? Datele pe care le-ai introdus nu vor fi salvate</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Rămâi</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Ieși</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Alege o lună</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ian</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Iun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Iul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Extinde datele de autentificare sugerate</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Restrânge datele de autentificare sugerate</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Date de autentificare sugerate</string>
+
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Retrimite datele</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Anulează</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..ca198e6cd5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ru/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ОК</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Отмена</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Не давать этой странице создавать дополнительные диалоговые окна</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Установить</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Очистить</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Войти</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Имя пользователя</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Пароль</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Не сохранять</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Не сейчас</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Никогда не сохранять</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Не сейчас</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Сохранить</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Не обновлять</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Не сейчас</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Обновить</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Поле пароля не может быть пустым</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Введите пароль</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Не удалось сохранить логин</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Не удалось сохранить пароль</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Сохранить этот логин?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Сохранить пароль?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Обновить этот логин?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Обновить пароль?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Добавить имя пользователя к сохранённому паролю?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Метка для текстового поля ввода</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Выберите цвет</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Разрешить</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Не разрешать</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Вы уверены?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Вы действительно хотите покинуть этот сайт? Все введённые данные могут быть потеряны</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Остаться</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Уйти</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Выберите месяц</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Янв</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Фев</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Мар</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Апр</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Май</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Июн</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Июл</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Авг</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Сен</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Окт</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Ноя</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Дек</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Установка времени</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Управление паролями</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Управление паролями</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Развернуть предлагаемые пароли</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Развернуть сохранённые пароли</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Свернуть предлагаемые пароли</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Свернуть сохранённые пароли</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Предлагаемые пароли</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Сохранённые пароли</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Предложить надежный пароль</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Предложить надежный пароль</string>
+
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Используйте надежный пароль: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Отправить данные на этот сайт снова?</string>
+ <string name="mozac_feature_prompt_repost_message">Обновление этой страницы может повторить последние выполненные действия, например, снова отправив платёж или комментарий.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Отправить данные снова</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Отмена</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Выберите банковскую карту</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Использовать сохранённую карту</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Развернуть предлагаемые банковские карты</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Развернуть сохранённые карты</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Свернуть предлагаемые банковские карты</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Свернуть сохранённые карты</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Управление банковскими картами</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Управление картами</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Сохранить надёжно эту карту?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Обновить срок действия карты?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Номер карты будет зашифрован. Код безопасности не будет сохранён.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s шифрует номер вашей карты. Ваш код безопасности не будет сохранён.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Выберите адрес</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Развернуть предлагаемые адреса</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Развернуть сохранённые адреса</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Свернуть предлагаемые адреса</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Свернуть сохранённые адреса</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Управление адресами</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Фото аккаунта</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Выберите провайдера входа</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Войти с аккаунтом %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Использовать %1$s в качестве провайдера входа</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Вход в %1$s с учётной записью %2$s регулируется их <a href="%3$s">Политикой конфиденциальности</a> и <a href="%4$s">Условиями использования</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Продолжить</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Отменить</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..b915c490ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sat/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ᱴᱷᱤᱠ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ᱵᱟᱹᱰᱨᱟᱹ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ᱱᱚᱶᱟ ᱥᱟᱦᱴᱟ ᱟᱠᱚᱴᱮᱢ ᱵᱟᱹᱲᱛᱤ ᱠᱟᱛᱷᱟ ᱵᱮᱱᱟᱣ ᱠᱷᱚᱱ</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">ᱥᱟᱡᱟᱣ ᱢᱮ</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">ᱯᱷᱟᱨᱪᱟ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ᱵᱚᱞᱚᱱ ᱥᱩᱦᱤ</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹᱜ ᱧᱩᱛᱩᱢ</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">ᱟᱞᱚᱢ ᱥᱟᱺᱪᱟᱣᱜ-ᱟ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">ᱱᱤᱛᱚᱜ ᱫᱚ ᱵᱟᱝᱟ</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ᱛᱤᱥ ᱦᱚᱸ ᱵᱟᱝ ᱥᱟᱺᱪᱟᱣᱜ-ᱟ</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ᱱᱤᱛᱚᱜ ᱫᱚ ᱵᱟᱝᱟ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">ᱥᱟᱺᱪᱟᱣ ᱢᱮ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">ᱟᱞᱚᱢ ᱦᱟᱹᱞᱤᱭᱟᱜ-ᱟ</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">ᱱᱤᱛᱚᱜ ᱫᱚ ᱵᱟᱝᱟ</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">ᱦᱟᱹᱞᱤᱭᱟᱹᱜ ᱢᱮ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">ᱥᱟᱵᱟᱫᱽ ᱜᱟᱫᱮᱞ ᱠᱷᱟᱹᱞᱤ ᱟᱞᱚ ᱛᱟᱦᱮᱸᱱ ᱢᱟ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">ᱢᱤᱫᱴᱟᱹᱝ ᱥᱟᱵᱟᱫ ᱟᱫᱮᱨ ᱢᱮ</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">ᱞᱚᱜᱤᱱ ᱵᱟᱭ ᱥᱟᱺᱪᱟᱣ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫ ᱥᱟᱧᱪᱟᱣ ᱵᱟᱭ ᱜᱟᱱ ᱞᱮᱱᱟ</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">ᱱᱚᱶᱟ ᱞᱚᱜᱤᱱ ᱥᱟᱺᱪᱟᱣ ᱢᱮ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱠᱚ ᱥᱟᱺᱪᱟᱣᱟ ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">ᱱᱚᱶᱟ ᱞᱚᱜᱤᱱ ᱦᱟᱹᱞᱤᱭᱟᱜ ᱢᱮ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱦᱟᱹᱞᱤᱭᱟᱹᱠ ᱟ ?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">ᱥᱟᱺᱪᱟᱣᱠᱟᱱ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱨᱮ ᱵᱮᱵᱷᱟᱨᱤᱡ ᱧᱩᱛᱩᱢ ᱥᱮᱞᱮᱫᱽ ᱟᱢ?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">ᱚᱱᱚᱞ ᱟᱫᱮᱨ ᱡᱟᱭᱜᱟ ᱨᱮᱭᱟᱜ ᱞᱮᱵᱮᱞ</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ᱨᱚᱝ ᱵᱟᱪᱷᱟᱣ ᱛᱟᱞᱟᱝ ᱢᱮ</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ᱦᱮᱥᱟᱨᱤᱭᱟᱹ</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ᱵᱟᱝ ᱦᱮᱥᱟᱨᱭ</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">ᱟᱢ ᱜᱚᱴᱟ ᱛᱮ ᱢᱮᱱᱟᱢ-ᱟ?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">ᱪᱮᱫ ᱟᱢ ᱱᱚᱶᱟ ᱥᱟᱭᱤᱴ ᱵᱟᱹᱜᱤ ᱥᱟᱱᱟᱢ ᱠᱟᱱᱟ? ᱰᱟᱴᱟ ᱡᱟᱦᱸᱟ ᱟᱢ ᱟᱫᱮᱨ ᱞᱮᱫᱟᱢ ᱚᱱᱟ ᱥᱟᱺᱪᱟᱣ ᱵᱟᱝ ᱦᱩᱭᱩᱜ-ᱟ</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ᱛᱟᱦᱮᱸᱱ ᱢᱮ</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ᱵᱟᱹᱜᱤ ᱢᱮ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ᱪᱟᱸᱫᱚ ᱵᱟᱪᱷᱟᱣ ᱢᱮ</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ᱯᱩᱥ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ᱢᱟᱜ</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">ᱯᱷᱟ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ᱪᱟᱹᱛ</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">ᱵᱟᱹᱭ</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ᱡᱷᱮ</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ᱟᱥᱟ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ᱥᱟᱱ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ᱵᱷᱟ</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ᱟᱥᱤ</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ᱠᱟᱨ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ᱟᱜᱷ</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">ᱚᱠᱛᱚ ᱥᱮᱴ ᱢᱮ</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">ᱞᱚᱜᱤᱱ ᱵᱮᱵᱚᱥᱛᱷᱟ ᱠᱚ</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫ ᱢᱮᱱᱮᱡᱽ ᱢᱮ</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱟᱛᱟᱣᱟᱠᱟᱱ ᱞᱚᱜᱤᱱ ᱠᱚ ᱴᱟᱨᱟᱝ ᱪᱷᱚᱭ ᱢᱮ</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">ᱥᱟᱺᱪᱟᱣ ᱟᱠᱟᱱ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱠᱚ ᱯᱟᱥᱱᱟᱣ ᱢᱮ</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱟᱛᱟᱣᱟᱠᱟᱱ ᱞᱚᱜᱤᱱ ᱠᱚ ᱠᱟᱹᱴᱤᱡ ᱪᱷᱚᱭ ᱢᱮ</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">ᱥᱟᱺᱪᱟᱣᱟᱜ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱠᱚ ᱫᱟᱥᱟᱣ ᱢᱮ</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱟᱛᱟᱣᱟᱠᱟᱱ ᱞᱚᱜᱤᱱ ᱠᱚ</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">ᱥᱟᱧᱪᱟᱣ ᱠᱟᱱ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱠᱚ</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">ᱟᱸᱴ ᱫᱟᱫᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱵᱟᱛᱟᱣ ᱢᱮ …</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">ᱟᱸᱴ ᱫᱟᱫᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱵᱟᱛᱟᱣ ᱢᱮ …</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">ᱟᱹᱴ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱵᱮᱵᱷᱟᱨ ᱢᱮ ᱺ %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ᱱᱚᱶᱟ ᱥᱟᱭᱤᱴ ᱞᱟᱹᱜᱤᱫ ᱰᱟᱴᱟ ᱫᱩᱦᱲᱟᱹ ᱵᱷᱮᱡᱟᱭ ᱟᱢ‌ ᱥᱮ?</string>
+ <string name="mozac_feature_prompt_repost_message">ᱥᱟᱦᱴᱟ ᱨᱤᱯᱷᱨᱮᱥ ᱞᱮᱠᱷᱟᱱ ᱱᱤᱛᱚᱜᱟᱜ ᱠᱟᱹᱢᱤ ᱠᱚ ᱰᱩᱯᱞᱤᱠᱮᱴ ᱫᱟᱲᱮᱭᱟᱜᱼᱟᱭ, ᱡᱮᱢᱚᱱ ᱯᱩᱭᱥᱟᱹ ᱵᱷᱮᱡᱟ ᱠᱚ ᱟᱨ ᱵᱟᱝ ᱠᱚᱢᱮᱴ ᱠᱚ ᱵᱟᱨ ᱡᱮᱠᱷᱟ ᱵᱷᱮᱡᱟ ᱠᱚ ᱾</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ᱰᱟᱴᱟ ᱫᱩᱦᱲᱟ ᱵᱷᱮᱡᱟᱭᱢᱮ</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ᱵᱟᱹᱰᱨᱟᱹ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">ᱠᱨᱮᱰᱤᱴ ᱠᱟᱰ ᱵᱟᱪᱷᱟᱣ ᱢᱮ</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">ᱥᱟᱧᱪᱟᱣ ᱠᱟᱱ ᱠᱟᱰ ᱵᱮᱵᱷᱟᱨ ᱢᱮ</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱟᱛᱟᱣ ᱟᱠᱟᱱ ᱠᱨᱮᱰᱤᱴ ᱠᱟᱰ ᱡᱷᱟᱹᱞ ᱪᱷᱚᱭ ᱢᱮ</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">ᱥᱟᱺᱪᱟᱣ ᱟᱠᱟᱱ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱠᱚ ᱯᱟᱥᱱᱟᱣ ᱢᱮ</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱟᱛᱟᱣ ᱟᱠᱟᱱ ᱠᱨᱮᱰᱤᱴ ᱠᱟᱰ ᱦᱚᱯᱚᱱ ᱪᱷᱚᱭ ᱢᱮ</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">ᱥᱟᱧᱪᱟᱣ ᱠᱟᱱ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽᱠᱚ ᱫᱟᱥᱟᱣ ᱢᱮ</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">ᱠᱨᱮᱰᱤᱴ ᱠᱟᱰ ᱠᱚ ᱢᱮᱱᱮᱡᱽ ᱢᱮ</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">ᱠᱟᱰ ᱢᱮᱱᱮᱡᱽ ᱢᱮ</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">ᱱᱚᱶᱟ ᱠᱟᱰ ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱥᱟᱹᱦᱤᱡ ᱫᱚᱦᱚᱭ ᱟᱢ ᱥᱮ ?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">ᱠᱟᱰ ᱪᱟᱵᱟ ᱢᱟᱦᱟᱸ ᱦᱟᱹᱞᱤᱭᱟᱹᱭᱟᱹᱠ ᱟᱢ ᱥᱮ ?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">ᱠᱟᱰ ᱱᱚᱢᱵᱚᱨ ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹᱜᱼᱟ ᱾ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱠᱳᱰ ᱫᱚ ᱵᱟᱝ ᱥᱟᱧᱪᱟᱣᱜᱼᱟ ᱾</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s ᱫᱚ ᱟᱢᱟᱜ ᱠᱟᱰ ᱱᱚᱢᱵᱚᱨ ᱮ ᱮᱱᱠᱨᱤᱯᱴ ᱟ ᱾ ᱟᱢᱟᱜ ᱥᱤᱠᱭᱚᱨᱤᱴᱤ ᱠᱳᱰ ᱵᱟᱭ ᱥᱟᱧᱪᱟᱣᱜ ᱟ ᱾</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">ᱴᱷᱤᱠᱬᱟᱹ ᱢᱮᱴᱟᱣ ᱢᱮ</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱟᱛᱟᱣᱟᱠᱟᱱ ᱞᱚᱜᱤᱱ ᱠᱚ ᱯᱟᱥᱱᱟᱣ ᱪᱷᱚᱭ ᱢᱮ</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">ᱥᱟᱧᱪᱟᱣ ᱟᱠᱟᱱ ᱴᱷᱤᱠᱬᱟᱹ ᱠᱚ ᱯᱟᱥᱱᱟᱣ ᱢᱮ</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">ᱵᱟᱛᱟᱣᱟᱠᱟᱱ ᱞᱚᱜᱤᱱ ᱠᱚ ᱦᱚᱯᱚᱱ ᱪᱷᱚᱭ ᱢᱮ</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">ᱥᱟᱧᱪᱟᱣᱟᱜ ᱴᱷᱤᱠᱬᱟᱹ ᱠᱚ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">ᱴᱷᱤᱠᱬᱟᱹᱤᱭᱟᱹ ᱡᱚᱛᱚᱱ ᱮᱢ</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">ᱠᱷᱟᱛᱟ ᱪᱤᱛᱟᱹᱨ</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">ᱢᱤᱫᱴᱟᱝ ᱵᱚᱞᱚ ᱮᱢᱚᱜᱤᱡ ᱵᱟᱪᱷᱟᱣ ᱢᱮ</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s ᱠᱷᱟᱛᱟ ᱵᱷᱵᱷᱟᱨ ᱟᱛᱮᱫ ᱥᱩᱦᱤ ᱮᱢ ᱢᱮ</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s ᱫᱚ ᱢᱤᱫᱴᱟᱝ ᱵᱚᱞᱚ ᱮᱢᱤᱡ ᱞᱮᱠᱷᱟ ᱵᱮᱵᱷᱟᱨ ᱢᱮ</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[%1$s ᱨᱮ %2$s ᱠᱷᱟᱛᱟ ᱛᱮ ᱵᱚᱞᱚ ᱠᱟᱛᱮ ᱚᱱᱟ ᱠᱚᱣᱟᱜ <a href="%3$s">ᱯᱨᱟᱭᱵᱷᱮᱥᱤ ᱱᱤᱛᱤ</a> ᱟᱨ <a href="%4$s">ᱠᱟᱹᱢᱤ ᱨᱮᱭᱟᱜ ᱥᱚᱨᱛᱚ</a> ᱨᱮᱭᱟᱜ ᱚᱫᱷᱤᱱ ᱨᱮ ᱢᱮᱱᱟᱜ ᱠᱟᱫᱟ ᱾]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">ᱞᱮᱛᱟᱲ</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">ᱵᱟᱹᱰᱨᱟᱹ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..e4dd91d8c6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sc/strings.xml
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">AB</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Annulla</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Èvita chi custa pàgina creet àteros diàlogos</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Cunfigura</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Isbòida</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Identìfica·ti</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nòmine utente</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Crae</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Non sarves</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Immoe nono</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Non sarves mai</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Immoe nono</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Sarva</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">No atualizes</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Immoe nono</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Atualiza</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Sa crae non podet èssere bòida</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Inserta una crae</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Impossìbile sarvare is credentziales</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Impossìbile sarvare sa crae</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Boles sarvare custa credentziale?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Boles sarvare sa crae?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Boles atualizare custa credentziale?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Boles atualizare sa crae?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Boles agiùnghere su nòmine de utente a sa crae sarvada?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Eticheta pro introduire unu campu de intrada de testu</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Sèbera unu colore</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Permite</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Refuda</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Seguru?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Boles lassare custu situ? Podet dare chi is datos insertados non bèngiant sarvados</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Abarra</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Lassa</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Piga unu mese</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ghe</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fre</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Làm</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Arg</string>
+
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aus</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Cab</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Làd</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">StA</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Ida</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Cunfigura s’ora</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gesti is credentziales</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Gesti is craes</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Ismànnia is credentziales cussigiadas</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Mìnima is credentziales cussigiadas</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Credentziales cussigiadas</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Craes sarvadas</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Cussìgia una crae segura</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Cussìgia una crae segura</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Imprea una crae segura: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Boles torrare a inviare is datos a su situ?</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Torra a imbiare is datos</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Annulla</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Seletziona una carta de crèditu</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Imprea una carta sarvada</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Ismànnia is cartas de crèditu cussigiadas</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Mìnima is cartas de crèditu cussigiadas</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Gesti is cartas de crèditu</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Gesti is cartas</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Boles sarvare custa carta cun seguresa?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Boles atualizare sa data de iscadèntzia de sa carta?</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Su nùmeru de carta at a èssere tzifradu. Su còdighe de seguresa no at a èssere sarvadu.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s tzifrat su nùmeru de sa carta tua. Su còdighe de seguresa no at a èssere sarvadu.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Seletziona un’indiritzu</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Ismànnia is indiritzos cussigiados</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Mìnima is indiritzos cussigiados</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Gesti is indiritzos</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Immàgine de su contu</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Sèbera unu frunidore de atzessu</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Identìfica·ti cun unu contu de %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Imprea %1$s comente frunidore de atzessu</string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Sighi</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Annulla</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..841bbc61d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-si/strings.xml
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">හරි</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">අවලංගු</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">මෙම පිටුව අමතර කවුළු සෑදීමෙන් වළක්වන්න</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">සකසන්න</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">මකන්න</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">පිවිසෙන්න</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">පරිශීලක නාමය</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">මුරපදය</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">සුරකින්න එපා</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">දැන් නොවේ</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">කිසිවිට නොසුරකින්න</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">දැන් නොවේ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">සුරකින්න</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">යාවත්කාල නොකරන්න</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">දැන් නොවේ</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">යාවත්කාල</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">මුරපද ක්‍ෂේත්‍රය හිස් නොවිය යුතුය</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">මුරපදය යොදන්න</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">පිවිසුම සුරැකීමට නොහැකිය</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">මුරපදය සුරැකීමට නොහැකිය</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">මෙම පිවිසුම සුරකින්නද?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">මුරපදය සුරකින්නද?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">පිවිසුම සංශෝධනයක්ද?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">මුරපදය යාවත්කාල කරන්නද?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">සුරැකි මුරපදයට පරි. නාමය එක් කරන්නද?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">පෙළ ආදාන ක්‍ෂේත්‍රයක් ඇතුල් කිරීමට නම්පත</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">වර්ණයක් තෝරන්න</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ඉඩ දෙන්න</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ප්‍රතික්‍ෂේප</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">ඔබට විශ්වාස ද?</string>
+
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">ඔබට මෙම අඩවිය හැර යාමට අවශ්‍යද? ඔබ ඇතුල් කළ දත්ත නොසුරැකෙනු ඇත</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">රැඳෙන්න</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">හැරයන්න</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">මාසයක් තෝරන්න</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">දුරුතු</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">නවම්</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">මැදින්</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">බක්</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">වෙසක්</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">පොසොන්</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ඇසළ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">නිකිණි</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">බිනර</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">වප්</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">ඉල්</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">උඳු</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">කාලය සකසන්න</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">පිවිසුම් කළමනාකරණය</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">මුරපද කළමනාකරණය</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">යෝජිත පිවිසුම් විහිදන්න</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">සුරැකි මුරපද විහිදන්න</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">යෝජිත පිවිසුම් හකුලන්න</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">සුරැකි මුරපද හකුළන්න</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">යෝජිත පිවිසුම්</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">සුරැකි මුරපද</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">ශක්තිමත් මුරපදයක් යෝජනා කරන්න</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">ශක්තිමත් මුරපදයක් යෝජනා කරන්න</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">ශක්තිමත් මුරපදයක් භාවිතා කරන්න: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">අඩවියට දත්ත යළි යවන්නද?</string>
+ <string name="mozac_feature_prompt_repost_message">මෙම පිටුව නැවුම් කිරීමෙන් ගෙවීමක් යැවීම හෝ අදහසක් පළ කිරීම වැනි මෑත ක්‍රියාමාර්ග දෙවරක් අනුපිටපත් විය හැකිය.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">දත්ත යළි යවන්න</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">අවලංගු</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">ණයපතක් තෝරන්න</string>
+
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">සුරැකි පත යොදාගන්න</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">යෝජිත ණයපත් විහිදන්න</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">සුරැකි පත් විහිදන්න</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">යෝජිත ණයපත් හකුලන්න</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">සුරැකි පත් හකුළන්න</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">ණයපත් කළමනාකරණය</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">පත් කළමනාකරණය</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">මෙම පත ආරක්‍ෂිතව සුරකින්නද?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">පත ඉකුත්වන දිනය යාවත්කාල කරන්නද?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">පතෙහි අංකය සංකේතනය වනු ඇත. ආරක්‍ෂණ කේතය සුරැකෙන්නේ නැත.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s ඔබගේ පතෙහි අංකය සංකේතනය කරයි. ඔබගේ ආරක්‍ෂණ කේතය සුරැකෙන්නේ නැත.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">ලිපිනය තෝරන්න</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">යෝජිත ලිපින විහිදන්න</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">සුරැකි ලිපින විහිදන්න</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">යෝජිත ලිපින හකුලන්න</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">සුරැකි ලිපින හකුළන්න</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">ලිපින කළමනාකරණය</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">ගිණුමේ ඡායාරූපය</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">පිවිසුම් ප්‍රතිපාදකයක් තෝරන්න</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s ගිණුමකින් පිවිසෙන්න</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">පිවිසුම් ප්‍රතිපාදකයක් ලෙස %1$s යොදාගන්න</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ %2$s ගිණුමක් සමඟින් %1$s වෙත පිවිසීමෙන් ඔවුන්ගේ <a href="%4$s">සේවාවේ නියම</a> සහ <a href="%3$s">රහස්‍යතා ප්‍රතිපත්තියට</a> යටත් වේ]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">ඉදිරියට</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">අවලංගු</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..b0d1818daa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sk/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Zrušiť</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Zabrániť tejto stránke otvárať ďalšie okná</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Nastaviť</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Vymazať</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Prihlásiť sa</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Používateľské meno</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Heslo</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Neuložiť</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Teraz nie</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nikdy neukladať</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Teraz nie</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Uložiť</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Neaktualizovať</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Teraz nie</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Aktualizovať</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Pole s heslom nesmie byť prázdne</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Zadajte heslo</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Prihlasovacie údaje sa nepodarilo uložiť</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Heslo nie je možné uložiť</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Chcete uložiť tieto prihlasovacie údaje?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Uložiť heslo?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Chcete aktualizovať tieto prihlasovacie údaje?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Aktualizovať heslo?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Chcete k uloženému heslu pridať používateľské meno?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Štítok na zadanie poľa pre zadávanie textu</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Vyberte farbu</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Povoliť</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Zakázať</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Naozaj?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Chcete zostať na tejto stránke? Zadané údaje nemusia byť uložené</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Zostať</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Odísť</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Výber mesiaca</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Máj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jún</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Júl</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Nastaviť čas</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Spravovať prihlasovacie údaje</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Spravovať heslá</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Rozbaliť navrhované prihlasovacie údaje</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Rozbaliť uložené heslá</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Zbaliť navrhované prihlasovacie údaje</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Zbaliť uložené heslá</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Navrhované prihlasovacie údaje</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Uložené heslá</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Navrhnúť silné heslo</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Navrhnúť silné heslo</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Použiť silné heslo: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Znova odoslať údaje tejto stránke?</string>
+ <string name="mozac_feature_prompt_repost_message">Obnovením tejto stránky môžete zopakovať posledné akcie, ako je odosielanie platby alebo opätovné uverejnenie komentára.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Znova odoslať údaje</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Zrušiť</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Vyberte platobnú kartu</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Použiť uloženú kartu</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Rozbaliť navrhované platobné karty</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Rozbaliť uložené karty</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Zbaliť navrhované platobné karty</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Zbaliť uložené karty</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Spravovať platobné karty</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Spravovať karty</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Bezpečne uložiť túto kartu?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Aktualizovať dátum vypršania platnosti karty?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Číslo karty bude zašifrované. Bezpečnostný kód sa neuloží.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s zašifruje číslo vašej karty. Váš bezpečnostný kód sa neuloží.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Vyberte adresu</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Rozbaliť navrhované adresy</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Rozbaliť uložené adresy</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Zbaliť navrhované adresy</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Zbaliť uložené adresy</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Spravovať adresy</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Obrázok účtu</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Vyberte poskytovateľa prihlásenia</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Prihláste sa pomocou účtu %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Použite %1$s ako poskytovateľa prihlásenia</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Na prihlásenie do %1$s pomocou účtu %2$s sa vzťahujú <a href="%3$s">Pravidlá ochrany osobných údajov</a> a <a href="%4$s">Zmluvné podmienky</a> daného účtu]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Pokračovať</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Zrušiť</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..66973e343c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-skr/strings.xml
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ٹھیک ہے</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">منسوخ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ایں ورقے کوں وادھوں ڈائیلاگ بݨاوݨ کنوں روکو</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">ٹھیک کرو</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">صاف کرو</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">سائن ان</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ورتݨ ناں</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">پاس ورڈ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">محفوظ نہ کرو</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">ہݨ کائناں</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">کݙاہیں وی محفوظ نہ کرو</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ہݨ کائناں</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">محفوظ کرو</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">اپ ڈیٹ نہ کرو</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">ہݨ کائناں</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">اپ ڈیٹ کرو</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">پاس ورڈ خانہ خالی کائنی ہووݨاں چاہیدا</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">پاس ورڈ درج کرو</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">لاگ ان محفوظ کرݨ کنوں قاصر</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">پاس ورڈ محفوظ کائنی کر سڳدا</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">ایہ لاگ ان محفوظ کروں؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">پاس ورڈ محفوظ کروں؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">ایہ لاگ ان اپ ڈیٹ کروں؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">پاس ورڈ اپ ڈیٹ کروں؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">محفوظ تھئے پاس ورڈ وچ ورتݨ ناں شامل کروں؟</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">عبارت ان پُٹ خانے وچ درج کرݨ کیتے لیبل</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">رنگ چُݨو</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">اجازت ݙیوو</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">انکار کرو</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">بھلا تہاکوں پک ہے؟</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">بھلا تساں ایہ سائٹ چھوڑݨ چاہندے ہو؟ تھی سڳدے تہاݙا درج تھیا ڈیٹا محفوظ نہ تھیوے</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">راہوو</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">چھوڑو</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ہک مہینہ چُݨو</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">جنورى</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">فرورى</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">مارچ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">اپريل</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">مئی</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">جون</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">جولائى</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">اگست</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ستمبر</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">اکتوبر</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">نومبر</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">دسمبر</string>
+
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">وقت ٹھیک کرو</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">لاگ ان منیج کرو</string>
+
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">پاس ورڈز دا بندوبست کرو</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">تجویز تھئے لاگ اناں کوں ودھاؤ</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">محفوظ تھئے پاس ورڈ کھنڈاؤ</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">تجویز تھئے لاگ اناں کوں کٹھا کرو</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">محفوظ تھئے پاس ورڈ ولھیٹو</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">تجویز تھئے لاگ ان</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">محفوظ تھئے پاس ورڈ</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">تکڑا پاس ورڈ تجویز کرو</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">تکڑا پاس ورڈ تجویز کرو</string>
+
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">تَکڑا پاس ورڈ وَرتو: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ایں سائٹ تے ڈیٹا ولدا بھیڄوں؟</string>
+ <string name="mozac_feature_prompt_repost_message">ایں ورقے کوں تازہ کرݨ نال حالیہ عملاں دی ݙوجھی نقل بݨ سڳدی ہے۔ جیویں جو پیسیاں دی ادائیگی پٹھݨ یا ݙو واری تبصرہ کرݨ۔</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ڈیٹا ولدا پٹھو</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">منسوخ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">کریڈٹ کارڈ چݨو</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">محفوظ تھیا کارڈ ورتو</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">تجویز تھئے کریڈٹ کارڈاں کوں ودھاؤ</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">محفوظ تھئے کارڈ کھنڈاؤ</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">تجویز تھئے کریڈٹ کارڈاں کوں کٹھا کرو</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">محفوظ تھئے کارڈ ولھیٹو</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">کریڈیٹ کارڈ منیج کرو</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">کارڈز منیج کرو</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">ایہ کارڈ حفاظت نال محفوظ کروں؟</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">کارڈ مُکݨ تریخ اپ ڈیٹ کروں؟</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">کارڈ نمبر دی خفیہ کاری کیتی ویسی۔ حفاظتی کوڈ محفوظ کائناں کیتا ویسی۔</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s تُہاݙے کارڈ نمبر کوں انکرپٹ کرین٘دا ہِے۔ تُہاݙا سیکیورٹی کوڈ محفوظ کائناں تھیسی۔</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">پتہ چُݨو</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">تجویز تھئے پتیاں کوں ودھاؤ</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">محفوظ تھئے پتے کھنڈاؤ</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">تجویز تھئے پتیاں کوں کٹھا کرو</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">محفوظ تھئے پتے ولھیٹو</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">پتے منیج کرو</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">کھاتہ تصویر</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">لاگ ان مہیا کار چݨو</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s کھاتے نال سائن ان تھیوو</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">%1$s کوں لاگ ان مہیاکار دے طور تے ورتو</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ہِک %2$s اکاؤنٹ دے نال %1$s وِچ لاگ اِن کرݨ اِنّھاں دے <a href="%3$s">رازداری پالیسی</a> اَتے <a href="%4$s">خدمت دیاں شرطاں</a>دے تابع ہِے۔]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">جاری</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">منسوخ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..dec932c1c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sl/strings.xml
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">V redu</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Prekliči</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Tej strani prepreči ustvarjanje novih pogovornih oken</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Nastavi</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Počisti</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Prijava</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Uporabniško ime</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Geslo</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ne shrani</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ne zdaj</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nikoli ne shranjuj</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ne zdaj</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Shrani</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ne posodobi</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ne zdaj</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Posodobi</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Polje za geslo ne sme biti prazno</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Vnesite geslo</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Ni mogoče shraniti povezave</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Gesla ni mogoče shraniti</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Shranim to prijavo?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Posodobim to prijavo?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Dodaj shranjenemu geslu uporabniško ime?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Oznaka polja za vnos besedila</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Izberite barvo</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Dovoli</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Zavrni</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ali ste prepričani?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ali res želite zapustiti to spletno mesto? Podatki, ki ste jih vnesli, morda ne bodo shranjeni</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Ostani na strani</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Zapusti stran</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Izberi mesec</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Avg</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Nastavi čas</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Upravljanje prijav</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Upravljanje gesel</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Razširi predlagane prijave</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Prikaži shranjena gesla</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Strni predlagane prijave</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Skrij shranjena gesla</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Predlagane prijave</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Shranjena gesla</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Predlagaj močno geslo</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Predlagaj močno geslo</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Uporabi močno geslo: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Ponovno pošlji podatke spletnemu mestu?</string>
+ <string name="mozac_feature_prompt_repost_message">Osvežitev te strani lahko podvoji nedavna dejanja, kot je pošiljanje plačila ali objava komentarja.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Znova pošlji podatke</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Prekliči</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Izberite kreditno kartico</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Uporabi shranjeno kartico</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Razširi predlagane kreditne kartice</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Prikaži shranjene kartice</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Strni predlagane kreditne kartice</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Skrij shranjene kartice</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Upravljanje kreditnih kartic</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Upravljanje kartic</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Želite varno shraniti to kartico?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Posodobi datum poteka veljavnosti kartice?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Številka kartice bo šifrirana. Varnostna koda ne bo shranjena.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s šifrira številko vaše kartice. Varnostna koda se ne bo shranila.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Izbira naslova</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Razširi predlagane naslove</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Prikaži shranjene naslove</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Strni predlagane naslove</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Skrij shranjene naslove</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Upravljanje naslovov</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Slika računa</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Izberite ponudnika prijave</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Prijava z računom %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Uporabi %1$s kot ponudnika prijave</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Pri prijavi v %1$s z računom %2$s veljajo njihovi <a href="%4$s">pogoji uporabe</a> in <a href="%3$s">pravilnik o zasebnosti</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Nadaljuj</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Prekliči</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..8a5a8bcaff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sq/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Anuloje</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Pengoja kësaj faqeje krijimin e dialogëve shtesë</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Vëre</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Spastroje</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Hyni</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Emër përdoruesi</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Fjalëkalim</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Mos e ruaj</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Jo tani</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Mos e ruaj kurrë</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Jo tani</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Ruaje</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Mos e përditëso</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Jo tani</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Përditësoje</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Fusha e fjalëkalimit s’duhet të jetë e zbrazët</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Jepni një fjalëkalim</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">S’arrihet të ruhen kredenciale hyrjesh</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">S’ruhet dot fjalëkalimi</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Të ruhen këto kredenciale hyrjesh?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Të ruhet fjalëkalimi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Të përditësohen këto kredenciale hyrjesh?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Të përditësohet fjalëkalimi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Të shtohet emri i përdoruesit te fjalëkalimi i ruajtur?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etiketë për dhënie te një fushë futjeje tekstesh</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Zgjidhni një ngjyrë</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Lejoje</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Mohoje</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Jeni i sigurt?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Doni ta braktisni këtë sajt? Të dhënat që keni dhënë mund të mos ruhen</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Qëndro</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Braktise</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Zgjidhni një muaj</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Shk</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Pri</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Qer</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Kor</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Gus</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sht</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Tet</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nën</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dhj</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Ujdisni kohën</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Administroni kredenciale hyrjesh</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Administroni fjalëkalime</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Zgjeroji kredencialet e sugjeruara të hyrjeve</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Zgjero fjalëkalimet e ruajtur</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Tkurri kredencialet e sugjeruara të hyrjeve</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Tkurri fjalëkalimet e ruajtur</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Kredenciale të sugjeruara hyrjesh</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Fjalëkalime të ruajtur</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Sugjero fjalëkalim të fuqishëm</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Sugjero fjalëkalim të fuqishëm</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Përdor fjalëkalim të fuqishëm: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Të ridërgohen të dhëna te ky sajt?</string>
+ <string name="mozac_feature_prompt_repost_message">Rifreskimi i kësaj faqeje mund të përsëdytëte veprime tani së fundi, të tilla si dërgimi i një pagese apo postimi i një komenti dy herë.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Ridërgoji të dhënat</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Anuloje</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Përzgjidhni kartë krediti</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Përdor kartë të ruajtur</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Zgjero karta kreditit të sugjeruara</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Zgjero karta të ruajtura</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Tkurri kartat e kreditit të sugjeruara</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Tkurri kartat e ruajtura</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Administroni karta krediti</string>
+
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Administroni karta</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Të ruhet në mënyrë të sigurt kjo kartë?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Të përditësohet data e skadimit të kartës?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Numri i kartës do të fshehtëzohet. Kodi i sigurisë s’do të ruhet.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s-i e fshehtëzon numrin e kartës tuaj. Kodi juaj i sigurisë s’do të ruhet.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Përzgjidhni adresë</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Zgjeroji adresat e sugjeruara</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Zgjeroji adresat e ruajtura</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Tkurri adresat e sugjeruara</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Tkurri adresat e ruajtura</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Administroni adresa</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Foto llogarie</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Zgjidhni një shërbim hyrjesh</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Bëni hyrjen me një llogari %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Përdor %1$s si shërbim hyrjesh</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Hyrja te %1$s me një llogari %2$s është subjekt i <a href="%3$s">Rregullave të Privatësisë</a> dhe <a href="%4$s">Kushteve të Shërbimit</a> të tyre]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Vazhdo</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Anuloje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..a231fed4e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sr/strings.xml
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ОК</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Откажи</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Онемогући овој страници да ствара додатне дијалоге</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Постави</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Обриши</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Пријави се</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Корисничко име</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Лозинка</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Немој сачувати</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Никада не чувај</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Не сада</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Сачувај</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Немој ажурирати</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Ажурирај</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Поље за лозинку не сме бити празно</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Није могуће сачувати пријаву</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Сачувати ову пријаву?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Ажурирати ову пријаву?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Додати корисничко име у сачувану лозинку?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Ознака за попуњавање поља за унос текста</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Изаберите боју</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Дозволи</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Забрани</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Да ли сте сигурни?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Да ли желите да напустите ову страницу? Подаци које сте унели се можда неће сачувати</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Остани</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Напусти</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Изаберите месец</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Јан</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Феб</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Мар</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Апр</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Мај</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Јун</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Јул</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Авг</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Сеп</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Окт</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Нов</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Дец</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Постави време</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Управљај пријавама</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Прошири предложене пријаве</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Скупи предложене пријаве</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Предложене пријаве</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Желите ли поново послати податке на ову страницу?</string>
+ <string name="mozac_feature_prompt_repost_message">Освежавање ове странице може резултовати дуплирањем недавних радњи, као што је дупло слање уплате или постављање коментара.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Поново пошаљи податке</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Откажи</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Изабери кредитну картицу</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Рашири препоручене кредитне картице</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Скупи препоручене кредитне картице</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Управљај кредитним картицама</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Безбедно сачувати ову картицу?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Ажурирати датум истека картице?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Број картице биће шифрован. Безбедносни код неће бити сачуван.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Изабери адресу</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Рашири предложене адресе</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Скупи предложене адресе</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Управљај адресама</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Слика налога</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Изаберите добављача за пријаву</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Пријавите се преко %1$s налога</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Користите %1$s као добављач пријављивања</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Пријављивање на %1$s преко %2$s налога подлеже њиховој <a href="%3$s">политици приватности</a> и <a href="%4$s">условима коришћења</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Настави</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Откажи</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..8069cf1b66
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-su/strings.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">HEUG</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Bolay</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Nyegah ieu kaca tina ngadamel dialog anu sanés</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Setél</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Beresihan</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Asup</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Sandiasma</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Kecap sandi</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Ulah diteundeun</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ulah diteundeun</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Engké deui</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Teundeun</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Ulah ngapdét</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Apdét</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Widang kecap sandi henteu kaci kosong</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Teu bisa neundeun login</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Teundeun ieu login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Apdét ieu login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Tambahkeun sandiasma kana kecap sandi anu diteundeun?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label pikeun nuliskeun widang input téks</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Pilih kelir</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Idinan</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Tolak</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Anjeun yakin?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Arék ninggalkeun ieu loka? Data anu geus diasupkeun bisa leungit</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Cicing</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Tinggalkeun</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Pilih sasih</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Péb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Méi</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Agu</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sép</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nop</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dés</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Setél wanci</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Kokolakeun login</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Legaan saran login</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Leutikan saran login</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Saran login</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Usulkeun kecap sandi anu wedel</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Usulkeun kecap sandi anu wedel</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Paké kecap sandi anu wedel: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Kirimkeun deui data ka ieu loka?</string>
+ <string name="mozac_feature_prompt_repost_message">Nyegerkeun ieu kaca bisa ngaduplikasi peta panganyarna, contona mayar atawa ngirim koméntar dua kali.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Kirimkeun deui data</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Bolay</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Pilih kartu kiridit</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Legaan kartu kiridit anu disarankeun</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Tilep saran kartu kiridit</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Kokolakeun kartu kiridit</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Teundeun ieu kartu sacara aman?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Mutahirkeun titimangsa kadaluwarsa kartu?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Nomer kartu bakal diénkrip. Kode kaamanan moal diteundeun.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Pilih alamat</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Legaan saran alamat</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Leutikan saran alamat</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Kokolakeun alamat</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Gambar akun</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Pilih panyadia login</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Asup maké akun %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Paké %1$s salaku panyadia login</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Asup log ka %1$s maké akun %2$s nurut kana <a href="%3$s">Kawijakan Pripasi</a> jeung <a href="%4$s">Katangtuan Layanan.</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Tuluykeun</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Bolay</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..a4c4cb0a83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Avbryt</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Förhindra att den här sidan skapar fler dialogrutor</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ange</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Rensa</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Logga in</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Användarnamn</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Lösenord</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Spara inte</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Inte nu</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Spara aldrig</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Inte nu</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Spara</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Uppdatera inte</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Inte nu</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Uppdatera</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Lösenordsfältet får inte vara tomt</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Ange ett lösenord</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Kunde inte spara inloggningsuppgifter</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Det går inte att spara lösenordet</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Spara den här inloggningen?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Spara lösenord?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Vill du uppdatera den här inloggningen?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Uppdatera lösenord?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Lägg till användarnamn till det sparade lösenordet?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Etikett för att ange ett textinmatningsfält</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Välj en färg</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Tillåt</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Neka</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Är du säker?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Vill du lämna den här webbplatsen? Data du har angett kanske inte sparas</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Stanna</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Lämna</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Välj en månad</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Maj</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Ange tid</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Hantera inloggningar</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Hantera lösenord</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Expandera föreslagna inloggningar</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Expandera sparade lösenord</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Komprimera föreslagna inloggningar</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Komprimera sparade lösenord</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Föreslagna inloggningar</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Sparade lösenord</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Föreslå ett starkt lösenord</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Föreslå ett starkt lösenord</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Använd starkt lösenord: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Skicka data igen till den här webbplatsen?</string>
+ <string name="mozac_feature_prompt_repost_message">Uppdatering av den här sidan kan duplicera senaste åtgärder, till exempel att skicka en betalning eller lägga upp en kommentar två gånger.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Skicka data igen</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Avbryt</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Välj kreditkort</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Använd sparat kort</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Expandera föreslagna kreditkort</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Expandera sparade kort</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Komprimera föreslagna kreditkort</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Komprimera sparade kort</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Hantera kreditkort</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Hantera kort</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Vill du spara det här kortet säkert?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Uppdatera kortets utgångsdatum?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Kortnummer kommer att krypteras. Säkerhetskoden kommer inte att sparas.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s krypterar ditt kortnummer. Din säkerhetskod kommer inte att sparas.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Välj adress</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Expandera föreslagna adresser</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Expandera sparade adresser</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Komprimera föreslagna adresser</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Komprimera sparade adresser</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Hantera adresser</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Kontobild</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Välj en inloggningsleverantör</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Logga in med ett %1$s-konto</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Använd %1$s som inloggningsleverantör</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Att logga in på %1$s med ett %2$s-konto omfattas av deras <a href="%3$s">sekretesspolicy</a> och <a href="%4$s">användarvillkor </a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Fortsätt</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Avbryt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..c65df4f77b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ta/strings.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">சரி</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">இரத்து</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">கூடுதல் உரையாடல்களை உருவாக்குவதிலிருந்து இந்தப் பக்கத்தைத் தடுக்கவும்</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">அமை</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">துடை</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">உள்நுழை</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">பயனர்பெயர்</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">கடவுச்சொல்</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">சேமிக்காதே</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ஒருபோதும் சேமிக்காதே</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">சேமி</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">புதுப்பிக்காதே</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">புதுப்பி</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">கடவுச்சொல் களம் காலியாக இருக்கக்கூடாது</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">புகுபதிகையைச் சேமிக்க இயலவில்லை</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">இப்புகுபதிகையைச் சேமிக்கவா?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">இப்புகுபதிகையைப் புதுப்பிக்கவா?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">சேமிக்கப்பட்ட கடவுச்சொல்லிற்குப் பயனர்பெயரைச் சேர்க்கவா?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">உரை உள்ளீட்டுக் களத்தில் உள்ளிடுவதற்கான அடையாளம்</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">வண்ணத்தைத் தேர்வுசெய்க</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">அனுமதி</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">நிராகரி</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">உறுதியாக?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">இத்தளத்தை விட்டு வெளியேற விரும்புகிறீர்களா? நீங்கள் உள்ளிட்ட தரவு சேமிக்கப்படாமல் போகலாம்</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">இரு</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">வெளியேறு</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ஒரு மாதத்தை தேர்ந்தெடு</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ஜன</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">பிப்</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">மார்</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ஏப்</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">மே</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">யூன்</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">யூலை</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ஆக</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">செப்</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">அக்</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">நவ</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">டிச</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">உள்நுழைவுகளை நிர்வகி</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">பரிந்துரைத்த உள்நுழைவுகளை விரிவாக்குங்கள்</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">பரிந்துரைத்த உள்நுழைவுகளை விரிவாக்கு</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">பரிந்துரைத்த உள்நுழைவுகள்</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">இத்தளத்திற்குத் தரவை மீண்டும் அனுப்பவா?</string>
+ <string name="mozac_feature_prompt_repost_message">இந்தப் பக்கத்தைப் புதுப்பிப்பது அண்மையச் செயல்களை திரும்பச் செய்யக்கூடும், இருமுறை பணம் அனுப்புதல் அல்லது இருமுறை கருத்திடல் போன்றவை.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">தரவை மீண்டும் அனுப்பு</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">இரத்துசெய்</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..292fec853a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-te/strings.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">సరే</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">రద్దుచేయి</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">అదనపు డైలాగులు సృష్టించకుండా ఈ పేజీని నివారించు</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">అమర్చు</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">తుడిచివేయి</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ప్రవేశించు</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">వాడుకరి పేరు</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">సంకేతపదం</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">భద్రపరచవద్దు</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ఎప్పుడూ భద్రపరచవద్దు</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ఇప్పుడు కాదు</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">భద్రపరుచు</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">తాజాకరించవద్దు</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">తాజాకరించు</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">సంకేతపదం ఖాళీగా ఉండకూడదు</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">ప్రవేశ వివరాలను భద్రపరచలేకపోతున్నాం</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">ఈ ప్రవేశాన్ని భద్రపరచాలా?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">ఈ ప్రవేశాన్ని తాజాకరించాలా?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">భద్రపరచిన సంకేతపదానికి వాడుకరి పేరును చేర్చాలా?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">పాఠ్య ఖాళీని పూరించడానికి లేబుల్</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">ఒక రంగును ఎంచుకోండి</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">అనుమతించు</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">తిరస్కరించు</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">మీరు నిశ్చితమేనా?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">ఈ సైటును వదిలి వెళ్లాలనుకుంటున్నారా? మీరు నమోదు చేసిన డేటా భద్రపరచబడకపోవచ్చు</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ఉండు</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">వదలివెళ్ళు</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ఒక నెల ఎంచుకోండి</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">జన</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ఫిబ్ర</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">మార్చి</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ఏప్రి</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">మే</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">జూన్</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">జూలై</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ఆగ</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">సెప్టెం</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">అక్టో</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">నవం</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">డిసెం</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">ప్రవేశాలను నిర్వహించండి</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">సూచించిన ప్రవేశాలను విస్తరించు</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">సూచించిన ప్రవేశాలను కుదించు</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">సూచించిన ప్రవేశాలు</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ఈ సైటుకి డేటాని మళ్ళీ పంపాలా?</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">డేటాను మళ్ళీ పంపించు</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">రద్దుచేయి</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..58703d132c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tg/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ХУБ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Бекор кардан</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Ин саҳифаро аз эҷоди равзанаҳои гуфтугӯии иловагӣ пешгирӣ намоед</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Танзим кардан</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Пок кардан</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Ворид шудан</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Номи корбар</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Ниҳонвожа</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Нигоҳ дошта нашавад</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Ҳоло не</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ҳеҷ гоҳ нигоҳ дошта нашавад</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Ҳоло не</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Нигоҳ доштан</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Навсозӣ карда нашавад</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Ҳоло не</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Навсозӣ кардан</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Ҷойи ниҳонвожа бояд холӣ набошад</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Ниҳонвожаеро ворид намоед</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Нигоҳ доштани маълумоти воридшавӣ ғайриимкон аст</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Ниҳонвожа нигоҳ дошта нашуд</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Маълумоти воридшавии ҷориро нигоҳ медоред?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Ниҳонвожаро нигоҳ медоред?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Маълумоти воридшавии ҷориро аз нав нигоҳ медоред?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Ниҳонвожаро аз нав нигоҳ медоред?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Номи корбарро ба ниҳонвожаи нигоҳдошташуда илова мекунед?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Нишона барои ворид кардани майдони воридкунии матн</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Рангеро интихоб кунед</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Иҷозат додан</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Рад кардан</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Шумо мутмаин ҳастед?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Шумо мехоҳед, ки ин сомонаро тарк намоед? Маълумоте, ки шумо ворид кардед, метавонад нигоҳ дошта нашавад</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Истодан</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Тарк кардан</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Моҳеро интихоб намоед</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Янв</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Фев</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Мар</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Апр</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Май</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Июн</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Июл</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Авг</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Сен</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Окт</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Ноя</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Дек</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Танзими вақт</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Идоракунии воридшавӣ</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Идоракунии ниҳонвожаҳо</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Намоиш додани воридшавиҳои пешниҳодшуда</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Баркушодани ниҳонвожаҳои нигоҳдошташуда</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Пинҳон кардани воридшавиҳои пешниҳодшуда</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Пинҳон кардани ниҳонвожаҳои нигоҳдошташуда</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Воридшавиҳои пешниҳодшуда</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Ниҳонвожаҳои нигоҳдошташуда</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Пешниҳод кардани ниҳонвожаи боқувват</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Пешниҳод кардани ниҳонвожаи боқувват</string>
+
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Аз ниҳонвожаи қавӣ истифода баред: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Маълумотро ба ин сомона аз нав мефиристонед?</string>
+ <string name="mozac_feature_prompt_repost_message">Амали навсозии ин саҳифа метавонад амалҳои охиринро такрор намояд, масалан, амали пардохт ё интишори шарҳ метавонад дубора иҷро карда шавад.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Аз нав фиристодани маълумот</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Бекор кардан</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Корти кредитиро интихоб кунед</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Истифодаи корти нигоҳдошташуда</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Кортҳои кредитии пешниҳодшударо нишон диҳед</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Баркушодани кортҳои нигоҳдошташуда</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Кортҳои кредитии пешниҳодшударо пинҳон кунед</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Пинҳон кардани кортҳои нигоҳдошташуда</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Идоракунии кортҳои кредитӣ</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Идоракунии кортҳо</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Ин кортро ба таври бехатар нигоҳ медоред?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Санаи анҷоми муҳлати кори кортро нав мекунед?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Рақами корт рамзгузорӣ карда мешавад. Рамзи амниятӣ нигоҳ дошта намешавад.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">«%s» рақами корти шуморо рамзгузорӣ мекунад. Рамзи амниятии шумо нигоҳ дошта намешавад.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Интихоб кардани нишонӣ</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Намоиш додани нишониҳои пешниҳодшуда</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Баркушодани нишониҳои нигоҳдошташуда</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Пинҳон кардани нишониҳои пешниҳодшуда</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Пинҳон кардани нишониҳои нигоҳдошташуда</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Идоракунии нишониҳо</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Акси ҳисоб</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Интихоби таъминкунандаи воридшавӣ</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Бо ҳисоби «%1$s» ворид шавед</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Истифода бурдани «%1$s» ҳамчун таъминкунандаи воридшавӣ</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Вақте ки шумо ба «%1$s» бо ҳисоби «%2$s» ворид мешавед, <a href="%3$s">Сиёсати махфият</a> ва <a href="%4$s">Шартҳои хизматрасонии</a> марбут ба он татбиқ карда мешавад]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Идома додан</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Бекор кардан</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..b8bf2bd77c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-th/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ตกลง</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ยกเลิก</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">ป้องกันไม่ให้หน้านี้แสดงกล่องโต้ตอบเพิ่มอีก</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">ตั้ง</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">ล้าง</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">ลงชื่อเข้า</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ชื่อผู้ใช้</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">รหัสผ่าน</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">ไม่บันทึก</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">ยังไม่ทำตอนนี้</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ไม่บันทึกเสมอ</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ไม่ใช่ตอนนี้</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">บันทึก</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">ไม่อัปเดต</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">ยังไม่ทำตอนนี้</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">อัปเดต</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">ช่องป้อนรหัสผ่านจะต้องไม่ว่างเปล่า</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">ป้อนรหัสผ่าน</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">ไม่สามารถบันทึกการเข้าสู่ระบบ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">ไม่สามารถบันทึกรหัสผ่านได้</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">บันทึกการเข้าสู่ระบบนี้?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">บันทึกรหัสผ่านหรือไม่?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">อัปเดตการเข้าสู่ระบบนี้?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">ปรับปรุงรหัสผ่านหรือไม่?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">เพิ่มชื่อผู้ใช้ในรหัสผ่านที่บันทึกไว้?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">ป้ายกำกับสำหรับช่องป้อนข้อความ</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">เลือกสี</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">อนุญาต</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">ปฏิเสธ</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">คุณแน่ใจหรือไม่?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">คุณต้องการออกจากไซต์นี้หรือไม่? ข้อมูลที่คุณใส่อาจไม่ถูกบันทึก</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">อยู่ต่อ</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ออก</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">เลือกเดือน</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">ม.ค.</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">ก.พ.</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">มี.ค.</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">เม.ย.</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">พ.ค.</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">มิ.ย.</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ก.ค.</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ส.ค.</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ก.ย.</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ต.ค.</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">พ.ย.</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">ธ.ค.</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">ตั้งเวลา</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">จัดการการเข้าสู่ระบบ</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">จัดการรหัสผ่าน</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">ขยายการเข้าสู่ระบบที่เสนอแนะ</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">ขยายรหัสผ่านที่บันทึกไว้</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">ยุบการเข้าสู่ระบบที่เสนอแนะ</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">ยุบรหัสผ่านที่บันทึกไว้</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">การเข้าสู่ระบบที่เสนอแนะ</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">รหัสผ่านที่บันทึกไว้</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">แนะนำรหัสผ่านที่คาดเดายาก</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">แนะนำรหัสผ่านที่คาดเดายาก</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">ใช้รหัสผ่านที่คาดเดายาก: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">ส่งข้อมูลไปยังไซต์นี้อีกครั้งไหม</string>
+ <string name="mozac_feature_prompt_repost_message">การเรียกหน้านี้ใหม่อาจทำให้เกิดการกระทำซ้ำเช่นการส่งการชำระเงินหรือโพสต์ความคิดเห็นซ้ำ</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ส่งข้อมูลอีกครั้ง</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ยกเลิก</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">เลือกบัตรเครดิต</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">ใช้บัตรที่บันทึกไว้</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">ขยายบัตรเครดิตที่เสนอแนะ</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">ขยายบัตรที่บันทึกไว้</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">ยุบบัตรเครดิตที่เสนอแนะ</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">ยุบบัตรที่บันทึกไว้</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">จัดการบัตรเครดิต</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">จัดการบัตร</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">ต้องการบันทึกบัตรนี้อย่างปลอดภัยหรือไม่?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">ต้องการปรับปรุงวันหมดอายุบัตรหรือไม่?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">หมายเลขบัตรจะถูกเข้ารหัส รหัสความปลอดภัยจะไม่ถูกบันทึก</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s จะเข้ารหัสหมายเลขบัตรของคุณ รหัสความปลอดภัยของคุณจะไม่ถูกบันทึก</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">เลือกที่อยู่</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">ขยายที่อยู่ที่เสนอแนะ</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">ขยายที่อยู่ที่บันทึกไว้</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">ยุบที่อยู่ที่เสนอแนะ</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">ยุบที่อยู่ที่บันทึกไว้</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">จัดการที่อยู่</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">รูปภาพบัญชี</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">เลือกผู้ให้บริการเข้าสู่ระบบ</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">ลงชื่อเข้าด้วยบัญชี %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">ใช้ %1$s เป็นผู้ให้บริการเข้าสู่ระบบ</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[การลงชื่อเข้าใช้ %1$s ด้วยบัญชี %2$s อยู่ภายใต้<a href="%3$s">นโยบายความเป็นส่วนตัว</a>และ<a href="%4$s">ข้อกำหนดในการให้บริการ</a>ของผู้ให้บริการ]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">ดำเนินการต่อ</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">ยกเลิก</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..ac77bdfebf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tl/strings.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Kanselahin</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Pigilan ang pahinang ito sa pagpapakita ng mga karagdagang dialog</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Itakda</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Alisin</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Mag-sign in</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Username</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Huwag i-save</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Huwag kailanman i-save</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Hindi Ngayon</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">i-Save</string>
+
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Huwag i-update</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">I-update</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Hindi dapat blangko ang password field</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Hindi mai-save ang login</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">i-Save ang login na ito?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">i-Update ang pag-login na ito?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Idagdag ang username sa naka-save na password?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label para sa paglagay sa text input field</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Pumili ng kulay</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Payagan</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Tanggihan</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Sigurado ka ba?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Gusto mo bang umalis sa site na ito? Hindi mase-save ang data na naipasok mo na</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Manatili</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Umalis na</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Pumili ng buwan</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ene</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Peb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Abr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Hun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Hul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nob</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dis</string>
+
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">i-Manage ang mga login</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Palawakin ang mga minumungkahing login</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Itago ang mga minumungkahing login</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Mga minumungkahing login</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Muling ipadala ang data sa site na ito?</string>
+ <string name="mozac_feature_prompt_repost_message">Ang pag-refresh sa pahinang ito ay maaaring magpadoble ng mga aksyon, gaya ng pagpapadala ng bayad o mag-post ng komento nang dalawang beses.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Ipadala muli ang data</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Kanselahin</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Pumili ng credit card</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Palawakin ang naka-mungkahing mga credit card</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Itago ang mga minungkahing credit card</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Pamahalaan ang mga credit card</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">I save and Card</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">I update and expiration ng card?</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Mamili ng address</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Pamahalaan ang mga address</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..888800ee55
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tok/strings.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">pona</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">ala</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">o ken ala e lipu lili sin tan lipu ni</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">o weka</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">o kama</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">nimi</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">nimi open</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">o awen ala</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ala la o awen</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">o awen</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">o sin ala</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">o sin</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">o pana e nimi open</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">mi ken ala awen e nimi open</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">o awen ala awen e nimi open ni?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">o sin ala sin e nimi open ni?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">sina pana ala pana e nimi tawa nimi open ni?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">nimi pi poki sitelen</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">o jo e kule</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">o ken</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">o ken ala</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">sina wile ala wile?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">sina weka ala weka tan lipu ni? sona sina li ken awen ala.</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ala</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">weka</string>
+
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">o ante e nimi open</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">o lukin mute e nimi open</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">o lukin lili e nimi open mute</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">nimi open pi lipu ni</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">o pana ala pana sin e sona tawa lipu ni?</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">pana</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">ala</string>
+
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">o lukin mute e lipu mani</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">o lukin lili e lipu mani</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings -->
+ <string name="mozac_feature_prompts_manage_credit_cards">o ante e lipu mani</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..868387a034
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tr/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Tamam</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">İptal</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Bu sayfanın ek iletişim kutuları oluşturmasının önle</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ayarlandı</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Temizle</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Giriş yap</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Kullanıcı adı</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Parola</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Kaydetme</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Şimdi değil</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Asla kaydetme</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Şimdi değil</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Kaydet</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Güncelleme</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Şimdi değil</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Güncelle</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Parola alanı boş olmamalıdır</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Parola girin</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Hesap kaydedilemedi</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Parola kaydedilemedi</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Bu hesap kaydedilsin mi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Parola kaydedilsin mi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Bu hesap güncellensin mi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Parola güncellensin mi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Kayıtlı parolaya kullanıcı adı eklensin mi?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Metin giriş alanı etiketi</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Bir renk seçin</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">İzin ver</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Reddet</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Emin misiniz?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Bu siteden çıkmak istiyor musunuz? Girdiğiniz veriler kaydedilmeyebilir</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Sitede kal</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Çık</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Bir ay seçin</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Oca</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Şub</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Nis</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Haz</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Tem</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ağu</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Eyl</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Eki</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Kas</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Ara</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Zamanı ayarla</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Hesapları yönet</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Parolaları yönet</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Önerilen hesapları genişlet</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Kayıtlı parolaları genişlet</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Önerilen hesapları daralt</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Kayıtlı parolaları daralt</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Önerilen hesaplar</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Kayıtlı parolalar</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Güçlü parola öner</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Güçlü parola öner</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Güçlü parola kullan: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Veriler siteye yeniden gönderilsin mi?</string>
+ <string name="mozac_feature_prompt_repost_message">Bu sayfayı tazelemek, son yaptığınız eylemleri (örn. ödeme yapma veya yorum gönderme) tekrarlayabilir.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Verileri yeniden gönder</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">İptal</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Kredi kartı seç</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Kayıtlı kartı kullan</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Önerilen kredi kartlarını genişlet</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Kayıtlı kartları genişlet</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Önerilen kredi kartlarını daralt</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Kayıtlı kartları daralt</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Kredi kartlarını yönet</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Kartları yönet</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Bu kart güvenli bir şekilde kaydedilsin mi?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Kartın son kullanma tarihi güncellensin mi?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Kart numarası şifrelenecektir. Güvenlik kodu kaydedilmeyecektir.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s kart numaranızı şifreler. Güvenlik kodunuz kaydedilmez.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Adres seçin</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Önerilen adresleri genişlet</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Kayıtlı adresleri genişlet</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Önerilen adresleri daralt</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Kayıtlı adresleri daralt</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Adresleri yönet</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Hesap resmi</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Oturum açma sağlayıcısı seç</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s hesabıyla oturum aç</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Oturum açma sağlayıcısı olarak %1$s kullan</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[%2$s hesabıyla %1$s üzerinde oturum açtığınızda bu sağlayıcının <a href="%3$s">Gizlilik Politikası</a> ve <a href="%4$s">Hizmet Koşulları</a>’na tabi olursunuz]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">İleri</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Vazgeç</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..3656a7f581
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-trs/strings.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Gā\'ue</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Dūyichin\'</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Nāgi\'iaj da\' si giri pajinâ nan a\'ngô sa gīrij</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Nāgi\'iaj yītïnj</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Nā\'nïn\'</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Gāyi\'ì sēsiûn</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Si yūguî rè\'</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Da\'nga\' huìi</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Sī na\'nïn sà\'t</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Nitāj āmān nā\'nïnj sà\'t</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Sī ga\'hue akuan\' nïn</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Nā\'nïnj sà\'</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Sī nagi\'iaj nākàt</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Nāgi\'iaj nākà</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Nitāj si da\'ui gūnàj gātsì riña huāj da\'nga\' huìi</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Na\'ue nā\'nïnj sà\'aj riña gayì\'ìt sēsiûn</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Nā\'nïnj sà\'t riña gayì\'ìt sēsiûn nan anj.</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Nāgi\'iaj nākat riña gayì\'ìt sēsiûn nan anj.</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Nūtà\'t si yūguît riña da\'nga\' huì na\'nïn sà\' raj.</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Gāchrūn \'ngō etikêta si ruhuât gātūy hiūj dan</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Nāguī \'ngō kōlô</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Gā\'nïn</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Sī ga\'nïnjt</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Huā nīkā ruhuâ raj.</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ruhuât gāhuīt riña sitiô nan anj. Ga\'ue nārè\' nej nuguan\' ngà duguatûjt</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Gūnàj gīnut</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Gāhuī riña sîtio</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Nāguī \'ngō ahuii</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ahui yi\'î hio\'o</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Ahui gūdukuu</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Ahui unu\'</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Ahui ñan\'ānj du\'ui</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Ahui diû huaan</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Ahui numân gumàan</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Ahui hiej</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ahui yi\'naa</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Ahui umin rikî\'</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Ahui nîma</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Ahui Sāndrisìi</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Ahui hio\' yi</string>
+
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Gānïn ‘ngō diû</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Gīni’hiāj rayi’î riña ayi’ìt sēsiûn</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Nāgi’hiaj nìko nej riña ayi’ìt sesiûn huaa</string>
+
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Gāchrī huì nej riña ayi’ìt sesiûn huaa</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Nej riña ayi’ìt sesiûn huaa</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Nā’nïnjt nej dato riña sitiô nan anj.</string>
+ <string name="mozac_feature_prompt_repost_message">Sisī nāgi’hiaj nākàt pajinâ nan nī gā’hue nāhuin huà’ nej sa gi’hiaj nākàt, dàj rû’ gā’nïnjt ‘ngō san’ānj an asi huà’ gāhuī ‘ngō nuguan’ gā’nïnjt.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Nā’nïnj ñû gān’ānj nej dâto</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Dūyichin\'</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Nāguī tarjeta san’ānj an</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Nāgi’hiaj da’ gatū doj nej tarjeta san’ānj an</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Gāchrī huì nej tarjeta san’ānj huāa</string>
+
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Ni’hiāj dàj gā nej tarjeta san’ānj an</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Nā’nïnj sà’ hue’êt tarjeta nan anj.</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Nāgi’hiāj nākàt diû gisìj gīrè’ tarjeta nan anj</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Nāruguì’ da’ga’ nīkāj si tarjetât. Si nanun sà’ da’nga’ gāhui rayi’ij.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Nāguī ‘ngō dīreksiûn</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Nāgi’hiaj nìko nej direksiûn huaa</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Gāchrī huì nej direksiûn huaa</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Ni’hiāj dàj gā nej dīreksiûn</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Ñadu’hua ginù riña kuênda</string>
+ <!-- Title of the Identity Credential provider dialog choose. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Nāna’huì’ ‘ngō sa rūgûñu’ūnj gāyi’ì’ sēsiûn</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Gārasun %1$s da’ rūgûñu’ūnj man gāyi’ìt sēsiûn</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Gāyi’ì sēsiûn riña %1$s ngà ‘ngō %2$s kuendâ nīkò’ rukû <a href="%3$s">Nuguan’ guendâ gā huì gāchē nunt</a> nī <a href="%4$s">Sa da’huît gīni’înt da’ gā’hue gārasunt</a>]]></string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..497b129f7f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tt/strings.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ОК</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Баш тарту</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Бу битне өстәмә диалог тәрәзәләрен ачудан тый</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Урнаштыру</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Чистарту</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Керү</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Кулланучы исеме</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Парол</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Сакламау</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Беркайчан да cакламау</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Хәзер түгел</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Саклау</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Яңартмау</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Яңарту</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Серсүз кыры буш булырга тиеш түгел</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Логинны саклап булмый</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Бу логин саклансынмы?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Бу логин яңартылсынмы?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Сакланган серсүз янына кулланучы исеме өстәлсенме?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Текст кертү кырының тамгасы</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Төс сайлау</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Рөхсәт итү</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Кире кагу</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Моны раслыйсызмы?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Сез бу сайттан чыгарга телисезме? Сез керткән мәгълүмат сакланмаган булырга мөмкин</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Калу</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Чыгу</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Айны сайлау</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Гый</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Фев</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Мар</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Апр</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Май</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Июн</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Июл</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Авг</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Сен</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Окт</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Ноя</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Дек</string>
+
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Вакытны билгеләү</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Логиннар белән идарә итү</string>
+
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Тәкъдим ителгән логиннарны киңәйтү</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Тәкъдим ителгән логиннарны төрү</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Тәкъдим ителгән логиннар</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Бу сайтка мәгълүматны яңадан җибәрергәме?</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Мәгълүматны яңадан җибәрү</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Баш тарту</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Кредит картасын сайлау</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Тәкъдим ителгән кредит карталарын җәю</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Тәкъдим ителгән кредит карталарын төрү</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Кредит карталары белән идарә итү</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Бу картаны хәвефсез рәвештә саклансынмы?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Картаның вакыты чыгу датасы яңартылсынмы?</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Адрес сайлау</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Тәкъдим ителгән адресларны ачып салу</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Тәкъдим ителгән адресларны төрү</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Адреслар белән идарә итү</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..f1f4000754
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">WAXXA</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Sfeḍ</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Kcem</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Isem n unessemres</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Taguri n uzerray</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Sti yan uklu</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">ssureg</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">S tidet?</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Qqim</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Ffeɣ</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Fren yan uyyur</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Yen</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Bṛa</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Maṛ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Ibr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Yun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Yul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ɣuc</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Cut</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Kṭu</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nwa</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Duj</string>
+
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Ssuter ifeska</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..e044589822
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ug/strings.xml
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ماقۇل</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">بىكار قىلىش</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">بۇ بەتنىڭ تېخىمۇ كۆپ سۆزلەشكۈ قۇرۇشىنىڭ ئالدىنى ئالىدۇ</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">تەڭشەك</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">تازىلاش</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">كىرىش</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">ئىشلەتكۈچى</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">پارول</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">ساقلىما</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">ھازىر ئەمەس</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">ھەرگىز ساقلىما</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">ھازىر ئەمەس</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">ساقلا</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">يېڭىلانمىسۇن</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">ھازىر ئەمەس</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">يېڭىلاش</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">ئىم بۆلىكى بوش قالمايدۇ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">ئىم كىرگۈزۈلىدۇ</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">كىرىش ئۇچۇرىنى ساقلىيالمايدۇ</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">ئىم ساقلىيالمايدۇ</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">بۇ كىرىشنى ساقلامدۇ؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">ئىم ساقلامدۇ؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">بۇ كىرىشنى يېڭىلامدۇ؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">ئىم يېڭىلامدۇ؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">ساقلانغان ئىمغا ئىشلەتكۈچى ئاتىنى قوشامدۇ؟</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">تېكىست كىرگۈزۈش بۆلىكىنىڭ بەلگىسى</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">رەڭ تاللىنىدۇ</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">رۇخسەت قىلىش</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">رەت قىلىش</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">جەزملەشتۈرەمسىز؟</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">بۇ تور بېكەتتىن ئايرىلامسىز؟ سىز كىرگۈزگەن سانلىق مەلۇماتلار ساقلانماسلىقى مۇمكىن</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">قېپقال</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">ئايرىل</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ئاي تاللاڭ</string>
+
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">يانۋار</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">فېۋرال</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">مارت</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">ئاپرېل</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">ماي</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">ئىيۇن</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">ئىيۇل</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">ئاۋغۇست</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">سېنتەبر</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">ئۆكتەبر</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">نويابر</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">دېكابر</string>
+
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">ۋاقىت تەڭشىكى</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">كىرىشنى باشقۇرۇش</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">ئىم باشقۇرۇش</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">تەۋسىيە كىرىشنى كېڭەيتىدۇ</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">ساقلىغان ئىمنى ياي</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">تەۋسىيە كىرىشنى يىغ</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">ساقلىغان ئىمنى يىغ</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">تەۋسىيە كىرىش</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">ساقلانغان ئىم</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">كۈچلۈك ئىم تەۋسىيە قىلىنىدۇ</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">كۈچلۈك ئىم تەۋسىيە قىلىنىدۇ</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">كۈچلۈك ئىم ئىشلىتىش: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">بۇ بېكەتكە سانلىق مەلۇماتنى قايتا يوللامدۇ؟</string>
+ <string name="mozac_feature_prompt_repost_message">بۇ بەت يېڭىلانسا چىقىم قىلىش ياكى ئىنكاسنى ئىككى قېتىم يوللاشقا ئوخشاش يېقىنقى مەشغۇلاتلارنى كۆپەيتىدۇ.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">سانلىق مەلۇماتنى قايتا يوللايدۇ</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">بىكار قىلىش</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">ئىناۋەتلىك كارتا تاللىنىدۇ</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">ساقلانغان كارتىنى ئىشلەت</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">تەۋسىيە قىلىنغان ئىناۋەتلىك كارتىنى كېڭەيتىدۇ</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">ساقلانغان كارتىنى ياي</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">تەۋسىيە قىلىنغان ئىناۋەتلىك كارتىنى يىغىدۇ</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">ساقلانغان كارتىنى يىغ</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">ئىناۋەتلىك كارتا باشقۇرۇش</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">كارتا باشقۇرۇش</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">بۇ كارتىنى بىخەتەر ساقلامدۇ؟</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">كارتىنىڭ مۇددىتى توشۇش قەرەلىنى يېڭىلامدۇ؟</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">كارتا نومۇرى شىفىرلىنىدۇ. بىخەتەرلىك كودى ساقلانمايدۇ.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s كارتا نومۇرىڭىزنى شىفىرلايدۇ. بىخەتەرلىك كودىڭىز ساقلانمايدۇ.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">ئادرېس تاللىنىدۇ</string>
+
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">تەۋسىيە ئادرېسنى كېڭەيتىدۇ</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">ساقلانغان ئادرېسنى ياي</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">تەۋسىيە ئادرېسنى يىغىدۇ</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">ساقلانغان ئادرېسنى يىغ</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">ئادرېس باشقۇرۇش</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">ھېسابات رەسىمى</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">تىزىمغا كىرىشنى تەمىنلىگۈچى تاللىنىدۇ</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">%1$s ھېساباتىدا تىزىمغا كىرىدۇ</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">تىزىمغا كىرىشنى تەمىنلىگۈچىگە %1$s نى ئىشلىتىدۇ</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[بىر %2$s ھېساباتىدا %1$s غا تىزىمغا كىرىشتە ئۇلارنىڭ <a href="%3$s">شەخسىيەت تۈزۈمى</a> ۋە <a href="%4$s">مۇلازىمەت ماددىلىرى</a>غا بويسۇنىدۇ]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">داۋاملاشتۇر</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">ۋاز كەچ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..53c48170b5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-uk/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Скасувати</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Заборонити цій сторінці створювати додаткові діалогові вікна</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Встановити</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Очистити</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Увійти</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Ім’я користувача</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Пароль</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Не зберігати</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Не зараз</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Ніколи не зберігати</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Не зараз</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Зберегти</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Не оновлювати</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Не зараз</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Оновити</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Поле пароля не повинно бути порожнім</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Введіть пароль</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Не вдається зберегти запис</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Не вдається зберегти пароль</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Зберегти цей пароль?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Зберегти пароль?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Оновити цей пароль?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Оновити пароль?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Додати ім’я користувача до збереженого пароля?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Мітка для введення поля введення тексту</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Оберіть колір</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Дозволити</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Заборонити</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ви впевнені?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ви хочете піти з цього сайту? Введені вами дані можуть не зберегтися</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Залишитись</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Піти</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Оберіть місяць</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Січ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Лют</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Бер</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Кві</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Тра</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Чер</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Лип</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Сер</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Вер</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Жов</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Лис</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Гру</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Налаштувати час</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Керувати паролями</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Керувати паролями</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Розгорнути запропоновані паролі</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Розгорнути збережені паролі</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Згорнути запропоновані паролі</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Згорнути збережені паролі</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Пропоновані паролі</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Збережені паролі</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Запропонувати надійний пароль</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Запропонувати надійний пароль</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Використати надійний пароль: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Надіслати дані на цей сайт повторно?</string>
+ <string name="mozac_feature_prompt_repost_message">Оновлення цієї сторінки може повторити нещодавні дії, наприклад, надіслати платіж або опублікувати коментар двічі.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Повторно надіслати дані</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Скасувати</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Виберіть кредитну картку</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Використати збережену картку</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Розгорнути запропоновані кредитні картки</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Розгорнути збережені картки</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Згорнути запропоновані кредитні картки</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Згорнути збережені картки</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Керувати кредитними картками</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Керувати картками</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Зберегти надійно цю картку?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Оновити термін дії картки?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Номер картки буде зашифровано. Код безпеки не буде збережено.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s шифрує номер вашої картки. Ваш код безпеки не буде збережено.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Вибрати адресу</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Розгорнути пропоновані адреси</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Розгорнути збережені адреси</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Згорнути пропоновані адреси</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Згорнути збережені адреси</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Керувати адресами</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Зображення облікового запису</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Оберіть постачальника послуг входу</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Увійдіть з обліковим записом %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Використовувати %1$s як постачальника авторизації</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Вхід до %1$s з використанням облікового запису %2$s регулюється <a href="%3$s">Політикою приватності</a> та <a href="%4$s">Умовами надання послуг</a> постачальника послуг авторизації]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Продовжити</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Скасувати</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..3e5299b140
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-ur/strings.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ٹھیک ہے</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">منسوخ کریں</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">اس صفحہ کو اور مکالمے بنانے سے روکیں</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">سیٹ کریں</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">صاف کریں</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">سائن ان کریں</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">صارف کا نام</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">پاس ورڈ</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">محفوظ مت کریں</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">کبھی بھی محفوظ نہ کریں</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">محفوظ کریں</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">تازہ کاری نا کریں</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">تازہ کاری کریں</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">پاسورڈ فیلڈ خالی نہیں ہونا چاہئے</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">لاگ ان کو محفوظ کرنے سے قاصر</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">اس لاگ ان کو محفوظ کریں؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">اس لاگ ان کو تازہ کاری کریں؟</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">صارف نام کو محفوظ کردہ پاسورڈ میں رکھیں؟</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">ایک متنی انپُٹ فیلڈ داخل کرنے کے لئے لیبل</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">رنگ کا انتخاب کریں</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">اجازت دیں</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">انکار کریں</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">کیا آپ کو یقین ہے؟</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">کیا آپ اس سائٹ کو چھوڑنا چاہتے ہیں؟ ہوسکتا ہے کہ آپ نے جو ڈیٹا داخل کیا وہ محفوظ نہ ہو</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">ٹھہریں</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">چھوڑيں</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">ایک مہینہ منتخب کریں</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">جنورى</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">فرورى</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">مارچ</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">اپريل</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">مئی</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">جون</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">جولائى</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">اگست</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">ستمبر</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">اکتوبر</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">نومبر</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">دسمبر</string>
+
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">لاگ ان بندوبست کریں</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">تجویز کردہ لاگ ان کو وسعت دیں</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">تجویز کردہ لاگ ان کو ختم کریں</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">تجویز شدہ لاگ ان</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">اس سائٹ پر ڈیٹا دوبارہ ارسال کریں؟</string>
+ <string name="mozac_feature_prompt_repost_message">اس صفحہ کو ریفریش کرنے سے حالیہ کارروائیوں کی نقل بن سکتی ہے، جیسے کہ پیسوں کی ادائیگی بھیجنا یا دو بار تبصرہ پوسٹ کرنا۔</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">ڈیٹا کو دوبارہ ارسال کریں</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">منسوخ کریں</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">کریڈٹ کارڈ منتخب کریں</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">تجویز کردہ کریڈٹ کارڈز کو وسعت دیں</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">تجویز کردہ کریڈٹ کارڈز کو سکیڑیں</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings -->
+ <string name="mozac_feature_prompts_manage_credit_cards">کریڈٹ کارڈز کا منظم کریں</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..8198f89c0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-uz/strings.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Bekor qilish</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Bu sahifada qoʻshimcha oynalar yaratishni toʻxtatish</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Oʻrnatish</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Tozalash</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Kirish</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Foydalanuvchi</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Parol</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Saqlanmasin</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Hech qachon saqlanmasin</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Hozir emas</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Saqlash</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Yangilanmasin</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Yangilash</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Parol maydoni boʻsh qolmasligi kerak</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Login saqlanmadi</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Bu login saqlansinmi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Bu login yangilansinmi?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Saqlangan parolga foydalanuvchi nomi qoʻshilsinmi?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Matn kiritish maydoni uchun yorliq</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Rangni tanlang</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Ruxsat berish</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Rad qilish</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ishonchingiz komilmi?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Bu saytdan chiqishni xohlaysizmi? Siz kiritgan maʼlumot saqlanmasligi mumkin</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Qolaman</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Ketaman</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Oyni tanlang</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Yan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Fev</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Iyun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Iyul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Avg</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sen</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Okt</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Noy</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dek</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Vaqtni kiritish</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Loginlarni boshqarish</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Tavsiya etilgan loginlarni kengaytirish</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Tavsiya etilgan loginlarni yigʻish</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Tavsiya etilgan loginlar</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Bu saytga maʼlumotlar qayta yuborilsinmi?</string>
+ <string name="mozac_feature_prompt_repost_message">Bu sahifani yangilash toʻlovni yuborish yoki ikki marta sharh qoldirish kabi soʻnggi amallarni takrorlashi mumkin.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Maʼlumotlarni qayta yuborish</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Bekor qilish</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Kredit kartani tanlash</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Tavsiya etilgan kredit kartalarni kengaytirish</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Tavsiya etiladigan kredit kartalarni yigʻish</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Kredit kartalarni boshqarish</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Bu karta xavfsiz saqlansinmi?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Karta amal qilish muddati yangilansinmi?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Karta raqami shifrlanadi. Xavfsizlik kodi saqlanmaydi.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Manzilni tanlang</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Tavsiya etilgan manzillarni kengaytirish</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Tavsiya etilgan manzillarni yigʻish</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Manzillarni boshqarish</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..3fbe68316e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-vec/strings.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Anuƚa</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Inpedisi a sta pàgina de vèrxere altre fenèstre de diaƚogo</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Inposta</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Pulisi</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Va rento</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Nòme utente</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">No stà salvare</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Salva</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Axorna</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Bixogna meter rento na password</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">No xe mìa posibiƚe salvare ƚa password</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Eticheta par meter rento el testo en on canpo</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Seji a coƚore</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Parmeti</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Nega</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Seƚesiona on mexe</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Xan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Avr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Mai</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Xun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Luj</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ago</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Set</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oto</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dex</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..c3ade7b30b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-vi/strings.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Hủy bỏ</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Ngăn trang này tạo ra các hộp thoại bổ sung</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Thiết lập</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Xóa</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Đăng nhập</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Tên đăng nhập</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Mật khẩu</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Không lưu</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">Không phải bây giờ</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Không bao giờ lưu</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Không phải bây giờ</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Lưu</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Đừng cập nhật</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">Không phải bây giờ</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Cập nhật</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Trường mật khẩu không được để trống</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">Nhập mật khẩu</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Không thể lưu thông tin đăng nhập</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">Không thể lưu mật khẩu</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Lưu thông tin đăng nhập này?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">Lưu mật khẩu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Cập nhật thông tin đăng nhập này?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">Cập nhật lại mật khẩu?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Thêm tên người dùng vào mật khẩu đã lưu?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Nhãn để nhập trường văn bản</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Chọn một màu</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Cho phép</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Từ chối</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Bạn có chắc không?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Bạn có muốn rời khỏi trang web này? Dữ liệu bạn đã nhập có thể không được lưu</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Ở lại</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Rời khỏi</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Chọn tháng</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Thg01</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Thg02</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Thg03</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Thg04</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Thg05</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Thg06</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Thg07</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Thg08</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Thg09</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Thg10</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Thg11</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Thg12</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Cài đặt thời gian</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Quản lý đăng nhập</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">Quản lý mật khẩu</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Mở rộng thông tin đăng nhập được đề xuất</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">Mở rộng mật khẩu đã lưu</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Thu gọn thông tin đăng nhập được đề xuất</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">Thu gọn mật khẩu đã lưu</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Thông tin đăng nhập được đề xuất</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">Mật khẩu đã lưu</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Đề xuất mật khẩu mạnh</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Đề xuất mật khẩu mạnh</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Sử dụng mật khẩu mạnh: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Gửi lại dữ liệu cho trang web này?</string>
+ <string name="mozac_feature_prompt_repost_message">Việc làm mới trang này có thể trùng lặp với các hành động gần đây, chẳng hạn như gửi thanh toán hoặc đăng nhận xét hai lần.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Gửi lại dữ liệu</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Huỷ bỏ</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Chọn thẻ tín dụng</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">Sử dụng thẻ đã lưu</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Mở rộng thẻ tín dụng được đề xuất</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">Mở rộng thẻ đã lưu</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Thu gọn thẻ tín dụng được đề xuất</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">Thu gọn thẻ đã lưu</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Quản lý thẻ tín dụng</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">Quản lý thẻ tín dụng</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Lưu thẻ này một cách an toàn?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Cập nhật ngày hết hạn thẻ?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">Số thẻ sẽ được mã hóa. Mã bảo mật sẽ không được lưu.</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s mã hóa số thẻ của bạn. Mã bảo mật của bạn sẽ không được lưu.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Chọn địa chỉ</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Mở rộng các địa chỉ được đề xuất</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">Mở rộng địa chỉ đã lưu</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Thu gọn các địa chỉ được đề xuất</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">Thu gọn địa chỉ đã lưu</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Quản lý địa chỉ</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Ảnh tài khoản</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Chọn một nhà cung cấp đăng nhập</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Đăng nhập bằng tài khoản %1$s</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Sử dụng %1$s làm nhà cung cấp thông tin đăng nhập</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[ Đăng nhập vào %1$s với tài khoản %2$s phải tuân theo <a href="%3$s">chính sách bảo mật</a> và <a href="%4$s">điều khoản dịch vụ</a> của họ]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Tiếp tục</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Hủy bỏ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..ec27325d62
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-yo/strings.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">Ó DÁA</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Parẹ́</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Ṣe ìdíwọ́ fún ojú-ìwé yìí láti ṣẹ̀dá ìsọ̀rọ̀ńgbèsì mìíràn</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Ṣètò</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Parẹ́</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Wọlé</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Orúkọ aṣàmúlò</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Pásíwọọ̀dù</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">Má fi pamọ́</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Má fi pamọ̀ láéláé</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Kìí ṣe báyìí</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Fipamọ́</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">Má sẹ ìsọdituntun</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Ìsọdituntun</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">Àye pásíwọọ̀dù ò gbọdọ̀ gbófo</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">Kò le fi ohun-ìwọlé pamọ́</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">Fi ohun-ìwọlé yìí pamọ́?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">Ṣe ìsọdituntun fún ohun-ìwọlé yìí?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Ṣe àfikún orúkọ-aṣàmúlò mọ́ ọ̀rọ̀-ìṣínà tó wà ní ìpamọ́?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Lébẹ́ẹ̀lì fún títẹ ọ̀rọ̀ sí</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Yan àwọ̀ kan</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Gbà láàyè</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Kọ̀</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Ṣé ó dá ọ lójú?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Ṣé o fẹ́ fi sáìtì yí sílẹ̀? Dátà tí o tẹ̀ lè má wá sí ní ìpamọ́</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Dúró</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Fi kalẹ̀</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Mú oṣù kan</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Ṣẹrẹ</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Èrèlé</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Ẹrẹ́nà</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Igbe</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">Èbìbí</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Okúdù</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Agẹmọ</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Ògún</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Ọwẹ́wẹ̀</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Ọ̀wàwà</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Béélú</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Ọpẹ́</string>
+
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">Ṣàkóso àwọn ohun-ìwọlé</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">Ṣe ìpọ̀si fún àbá àwọn ohun-ìwọlé</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">Pa àbá àwọn ohun-ìwọlé rẹ́</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">Àbá àwọn ohun-ìwọlé</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Tún dátà ráńṣẹ́ sí sáìtì yí?</string>
+ <string name="mozac_feature_prompt_repost_message">Dídá ojú-ìwé yìí padà lé è sọ àwọn ìṣẹ̀lẹ̀ àìpẹ́ di méjì, gẹ́gẹ́ bíi ka máa fi owó ránṣé tàbí fífi ọ̀rọ̀ àsọyé ránṣẹ́ lẹ́ẹ̀mejì.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Fi dátà ráńṣẹ́ lẹ́ẹ̀kan si</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Parẹ́</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">Yan káàdì ìyáwó</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">Fẹ àwọn káàdì ìyáwó tí a dábàá</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">Wó àwọn káàdì ìyáwó tí a dábàá</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">Sàkóso àwọn káàdì ìyáwó</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Fí káàdì yí pamọ́ dáada?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title"> Ṣe ìsọdituntun fún ọjọ́ òpin-ìlò káàdì?</string>
+
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">A ó sọ nọ́ḿbà káàdì di kóòdù. A ò ní ṣe ìfipamọ́ kóòdù ààbò.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Yan àdírẹ́ẹ̀sì</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">Fẹ àwọn adírẹ́sì tí a dábàá</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">Wó àwọn àdírẹ́sì tí a dábàá</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Ṣàkóso àwọn àdírẹ́sì</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-zam/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-zam/strings.xml
new file mode 100644
index 0000000000..e8273d25d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-zam/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">ăɁ</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">B-láɁ=y</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Tòmbî</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">-taɁ lélù lèɁn</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Chó lèl</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..4287c42133
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">确定</string>
+
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">取消</string>
+
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">阻止此页面创建更多对话框</string>
+
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">设置</string>
+
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">清除</string>
+
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">登录</string>
+
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">用户名</string>
+
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">密码</string>
+
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">不保存</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">暂时不要</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">永不保存</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">暂时不要</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">保存</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">不更新</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">暂时不要</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">更新</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">密码不能为空</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">请输入密码</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">无法保存登录信息</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">无法保存密码</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">要保存此登录信息吗?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">要保存密码吗?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">要更新此登录信息吗?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">要更新密码吗?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">要将用户名添加到已存密码吗?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">文本输入栏的标签</string>
+
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">选择颜色</string>
+
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">允许</string>
+
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">拒绝</string>
+
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">您确定吗?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">您确定要离开此网站吗?您所输入的数据可能尚未保存</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">留下</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">离开</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">选择月份</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">1 月</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">2 月</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">3 月</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">4 月</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">5 月</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">6 月</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">7 月</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">8 月</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">9 月</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">10 月</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">11 月</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">12 月</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">设置时间</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">管理登录信息</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">管理密码</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">展开推荐的登录信息</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">展开保存的密码</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">折叠推荐的登录信息</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">折叠保存的密码</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">推荐的登录信息</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">保存的密码</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">建议高强度密码</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">建议高强度密码</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">使用高强度密码:%1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">重新发送数据至此网站?</string>
+ <string name="mozac_feature_prompt_repost_message">刷新页面可能会再次执行最近的操作,例如重复付款或发表评论。</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">重新发送数据</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">取消</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">选择信用卡</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">使用保存的信用卡</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">展开建议的信用卡</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">展开保存的信用卡</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">折叠建议的信用卡</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">折叠保存的信用卡</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">管理信用卡</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">管理信用卡</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">安全地保存此卡片?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">是否要更新卡片有效期?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">卡号将被加密,且不会保存安全码。</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s 会将卡号加密保存。安全码不会被保存。</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">选择地址</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">展开建议的地址</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">展开保存的地址</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">折叠建议的地址</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">折叠保存的地址</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">管理地址</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">头像</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">选择一个登录方式</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">使用 %1$s 账户登录</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">使用 %1$s 登录</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[使用 %2$s 账户登录 %1$s 须遵守其<a href="%3$s">隐私政策</a>和<a href="%4$s">服务条款</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">继续</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">取消</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..d435190102
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">確定</string>
+
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">取消</string>
+
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">避免此頁面產生更多對話框</string>
+
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">設定</string>
+
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">清除</string>
+
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">登入</string>
+
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">使用者名稱</string>
+
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">密碼</string>
+
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save">不要儲存</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2" tools:ignore="UnusedResources">現在不要</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">永不儲存</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">現在不要</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">儲存</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update">不要更新</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2" tools:ignore="UnusedResources">現在不要</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">更新</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password">密碼不得為空白</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2" tools:ignore="UnusedResources">輸入密碼</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause">無法儲存登入資訊</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2" tools:ignore="UnusedResources">無法儲存密碼</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline">要儲存這筆登入資訊嗎?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2" tools:ignore="UnusedResources">要儲存密碼嗎?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline">要更新這筆登入資訊嗎?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2" tools:ignore="UnusedResources">要更新密碼嗎?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">要將使用者名稱加進儲存的密碼資訊嗎?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">文字輸入欄位的標籤</string>
+
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">選擇一種色彩</string>
+
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">允許</string>
+
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">拒絕</string>
+
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">您確定嗎?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">您確定要離開此網站嗎?您所輸入的資料可能還沒儲存</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">留下來</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">離開</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">挑選月份</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">1 月</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">2 月</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">3 月</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">4 月</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">5 月</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">6 月</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">7 月</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">8 月</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">9 月</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">10 月</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">11 月</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">12 月</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">設定時間</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins">管理登入密碼</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2" tools:ignore="UnusedResources">管理密碼</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description">展開建議的登入資訊</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2" tools:ignore="UnusedResources">展開儲存的密碼</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description">摺疊建議的登入資訊</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2" tools:ignore="UnusedResources">摺疊儲存的密碼</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins">建議的登入資訊</string>
+
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2" tools:ignore="UnusedResources">已存密碼</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">建議安全的密碼</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">建議安全的密碼</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">使用安全的密碼:%1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">要重新發送資料到這個網站嗎?</string>
+ <string name="mozac_feature_prompt_repost_message">重新整理頁面可能會再次執行最近的操作,例如重複付款或張貼留言。</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">重送資料</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">取消</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card">選擇信用卡</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2" tools:ignore="UnusedResources">使用儲存的卡片資訊</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description">展開建議的信用卡</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2" tools:ignore="UnusedResources">展開儲存的卡片資訊</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description">摺疊建議的信用卡</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2" tools:ignore="UnusedResources">摺疊儲存的卡片資訊</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards">管理信用卡</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2" tools:ignore="UnusedResources">管理卡片</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">安全地儲存這張卡的資料?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">是否要更新卡片效期?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body">將加密卡號,也不會儲存安全碼。</string>
+
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2" tools:ignore="UnusedResources">%s 會加密您的卡號,且不會儲存安全碼。</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">選擇地址</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description">展開建議的地址</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2" tools:ignore="UnusedResources">展開儲存的地址資訊</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description">摺疊建議的地址</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2" tools:ignore="UnusedResources">摺疊儲存的地址資訊</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">管理已存地址</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">帳號圖片</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">選擇登入資訊服務提供者</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">使用 %1$s 帳號登入</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">使用 %1$s 登入</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[使用 %2$s 帳號登入 %1$s 須遵守該登入服務的<a href="%3$s">隱私權保護政策</a>及<a href="%4$s">服務條款</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">繼續</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">取消</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values/attrs.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000..de216bfa8b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values/attrs.xml
@@ -0,0 +1,23 @@
+<?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>
+ <declare-styleable name="LoginPanelTextInputLayout">
+ <attr name="mozacInputLayoutErrorTextColor" format="reference"/>
+ <attr name="mozacInputLayoutErrorIconColor" format="reference"/>
+ <attr name="mozacPromptLoginEditTextCursorColor" format="reference"/>
+ </declare-styleable>
+
+ <declare-styleable name="LoginSelectBar">
+ <attr name="mozacLoginSelectHeaderTextStyle" format="reference"/>
+ </declare-styleable>
+
+ <declare-styleable name="CreditCardSelectBar">
+ <attr name="mozacSelectCreditCardHeaderTextStyle" format="reference"/>
+ </declare-styleable>
+
+ <declare-styleable name="AddressSelectBar">
+ <attr name="mozacSelectAddressHeaderTextStyle" format="reference"/>
+ </declare-styleable>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..0cae21eca4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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 xmlns:tools="http://schemas.android.com/tools">
+ <array name="mozac_feature_prompts_default_colors">
+ <item>#D73920</item>
+ <item>#FF8605</item>
+ <item>#FFCB13</item>
+ <item>#5FAD47</item>
+ <item>#21A1DE</item>
+ <item>#102457</item>
+ <item>#5B2067</item>
+ <item>#D4DDE4</item>
+ <item>#FFFFFF</item>
+ </array>
+ <color name="mozacBoxStrokeColor">#828282</color>
+ <color tools:override="true" tools:ignore="UnusedResources"
+ name="mtrl_textinput_default_box_stroke_color">@color/mozacBoxStrokeColor</color>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values/ids.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values/ids.xml
new file mode 100644
index 0000000000..9ba0f9c8e8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values/ids.xml
@@ -0,0 +1,7 @@
+<?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>
+ <item name="mozac_feature_prompts_no_more_dialogs_check_box" type="id"/>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values/quarantined_strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values/quarantined_strings.xml
new file mode 100644
index 0000000000..5943e14165
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values/quarantined_strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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/. -->
+<!-- Strings in this file are not yet ready for localization. -->
+<resources>
+ <!-- Text of the title of a dialog when a page is requesting to open a new window. -->
+ <string name="mozac_feature_prompts_popup_dialog_title">Prevent this site from opening a pop-up window?</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values/strings-no-translatable.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values/strings-no-translatable.xml
new file mode 100644
index 0000000000..b30bbc6b64
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values/strings-no-translatable.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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>
+ <!-- Months of the years, used on the month chooser dialog. -->
+ <string-array name="mozac_feature_prompts_months">
+ <item>@string/mozac_feature_prompts_jan</item>
+ <item>@string/mozac_feature_prompts_feb</item>
+ <item>@string/mozac_feature_prompts_mar</item>
+ <item>@string/mozac_feature_prompts_apr</item>
+ <item>@string/mozac_feature_prompts_may</item>
+ <item>@string/mozac_feature_prompts_jun</item>
+ <item>@string/mozac_feature_prompts_jul</item>
+ <item>@string/mozac_feature_prompts_aug</item>
+ <item>@string/mozac_feature_prompts_sep</item>
+ <item>@string/mozac_feature_prompts_oct</item>
+ <item>@string/mozac_feature_prompts_nov</item>
+ <item>@string/mozac_feature_prompts_dec</item>
+ </string-array>
+ <!-- Text used for the millisecond separator in the time picker. -->
+ <string name="mozac_feature_prompts_millisecond_separator">.</string>
+ <!-- Text used for the second separator in the time picker. -->
+ <string name="mozac_feature_prompts_second_separator">:</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..1f5415543c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values/strings.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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 xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools">
+ <!-- Text for confirmation for a positive action in dialog -->
+ <string name="mozac_feature_prompts_ok">OK</string>
+ <!-- Text for confirmation for a negative action in dialog. -->
+ <string name="mozac_feature_prompts_cancel">Cancel</string>
+ <!-- When a page shows many dialogs, this checkbox will appear for letting the user choose to prevent showing more dialogs. -->
+ <string name="mozac_feature_prompts_no_more_dialogs">Prevent this page from creating additional dialogs</string>
+ <!-- Text for a positive button, when an user selects a date in date/time picker. -->
+ <string name="mozac_feature_prompts_set_date">Set</string>
+ <!-- Text for a button that clears the selected input in the date/time picker. -->
+ <string name="mozac_feature_prompts_clear">Clear</string>
+ <!-- Text for the title of an authentication dialog. -->
+ <string name="mozac_feature_prompt_sign_in">Sign in</string>
+ <!-- Text for username field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_username_hint">Username</string>
+ <!-- Text for password field in an authentication dialog. -->
+ <string name="mozac_feature_prompt_password_hint">Password</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save" moz:removedIn="125" tools:ignore="UnusedResources">Don’t save</string>
+ <!-- Negative confirmation that we should not save the new or updated login -->
+ <string name="mozac_feature_prompt_dont_save_2">Not now</string>
+ <!-- Negative confirmation that we should never save a login for this site -->
+ <string name="mozac_feature_prompt_never_save">Never save</string>
+ <!-- Negative confirmation that we should not save a credit card for this site -->
+ <string name="mozac_feature_prompt_not_now">Not now</string>
+ <!-- Positive confirmation that we should save the new or updated login -->
+ <string name="mozac_feature_prompt_save_confirmation">Save</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update" moz:removedIn="125" tools:ignore="UnusedResources">Don’t update</string>
+ <!-- Negative confirmation that we should not save the updated login -->
+ <string name="mozac_feature_prompt_dont_update_2">Not now</string>
+ <!-- Positive confirmation that we should save the updated login -->
+ <string name="mozac_feature_prompt_update_confirmation">Update</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password" moz:removedIn="125" tools:ignore="UnusedResources">Password field must not be empty</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_empty_password_2">Enter a password</string>
+ <!-- Error text displayed underneath the login field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause" moz:removedIn="125" tools:ignore="UnusedResources">Unable to save login</string>
+ <!-- Error text displayed underneath the password field when it is in an error case -->
+ <string name="mozac_feature_prompt_error_unknown_cause_2">Can’t save password</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new login. -->
+ <string name="mozac_feature_prompt_login_save_headline" moz:removedIn="125" tools:ignore="UnusedResources">Save this login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new username and password and user decides if app should save the new password. -->
+ <string name="mozac_feature_prompt_login_save_headline_2">Save password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_update_headline" moz:removedIn="125" tools:ignore="UnusedResources">Update this login?</string>
+ <!-- Prompt message displayed when app detects a user has entered a new password for an existing login and user decides if app should update the password. -->
+ <string name="mozac_feature_prompt_login_update_headline_2">Update password?</string>
+ <!-- Prompt message displayed when app detects a user has entered a username for an existing login without a username and user decides if app should update the login. -->
+ <string name="mozac_feature_prompt_login_add_username_headline">Add username to saved password?</string>
+ <!-- Text for a label for the field when prompt requesting a text is shown. -->
+ <!-- For more info take a look here https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt -->
+ <string name="mozac_feature_prompts_content_description_input_label">Label for entering a text input field</string>
+ <!-- Title of a color picker dialog, this text is shown above a color picker. -->
+ <string name="mozac_feature_prompts_choose_a_color">Choose a color</string>
+ <!-- Text of a confirm button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_allow">Allow</string>
+ <!-- Text of a negative button in dialog requesting to open a new window. -->
+ <string name="mozac_feature_prompts_deny">Deny</string>
+ <!-- Title of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_title">Are you sure?</string>
+ <!-- Body text of the dialog shown when a user is leaving a website and there is still data not saved yet. -->
+ <string name="mozac_feature_prompt_before_unload_dialog_body">Do you want to leave this site? Data you have entered may not be saved</string>
+ <!-- Stay button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to stay in the website. -->
+ <string name="mozac_feature_prompts_before_unload_stay">Stay</string>
+ <!-- Leave button of the dialog shown when a user is leaving a website and there is still data not saved yet, this indicates that the user wants to leave in the website. -->
+ <string name="mozac_feature_prompts_before_unload_leave">Leave</string>
+ <!-- Title of the month chooser dialog. -->
+ <string name="mozac_feature_prompts_set_month">Pick a month</string>
+ <!-- January (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jan">Jan</string>
+ <!-- February month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_feb">Feb</string>
+ <!-- March month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_mar">Mar</string>
+ <!-- April month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_apr">Apr</string>
+ <!-- May month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_may">May</string>
+ <!-- June month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jun">Jun</string>
+ <!-- July month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_jul">Jul</string>
+ <!-- August month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_aug">Aug</string>
+ <!-- September month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_sep">Sep</string>
+ <!-- October month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_oct">Oct</string>
+ <!-- November month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_nov">Nov</string>
+ <!-- December month of the year (short description), used on the month chooser dialog. -->
+ <string name="mozac_feature_prompts_dec">Dec</string>
+ <!-- Title of the time picker dialog. -->
+ <string name="mozac_feature_prompts_set_time">Set time</string>
+ <!-- Option in expanded select login prompt that links to login settings -->
+ <string name="mozac_feature_prompts_manage_logins" moz:removedIn="125" tools:ignore="UnusedResources">Manage logins</string>
+ <!-- Option in expanded select password prompt that links to password settings -->
+ <string name="mozac_feature_prompts_manage_logins_2">Manage passwords</string>
+ <!-- Content description for expanding the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Expand suggested logins</string>
+ <!-- Content description for expanding the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_expand_logins_content_description_2">Expand saved passwords</string>
+ <!-- Content description for collapsing the saved logins options in the select login prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Collapse suggested logins</string>
+ <!-- Content description for collapsing the saved passwords options in the select password prompt -->
+ <string name="mozac_feature_prompts_collapse_logins_content_description_2">Collapse saved passwords</string>
+ <!-- Header for the select login prompt to allow users to fill a form with a saved login -->
+ <string name="mozac_feature_prompts_saved_logins" moz:removedIn="125" tools:ignore="UnusedResources">Suggested logins</string>
+ <!-- Header for the select password prompt to allow users to fill a form with a saved password -->
+ <string name="mozac_feature_prompts_saved_logins_2">Saved passwords</string>
+
+ <!-- Content description for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_content_description">Suggest strong password</string>
+ <!-- Header for the suggest strong password prompt to allow users to fill a form with a suggested strong password -->
+ <string name="mozac_feature_prompts_suggest_strong_password">Suggest strong password</string>
+ <!-- Title for using the suggest strong password confirmation dialog. %1$s will be replaced with the generated password -->
+ <string name="mozac_feature_prompts_suggest_strong_password_message">Use strong password: %1$s</string>
+
+ <!-- Strings shown in a dialog that appear when users try to refresh a certain kind of webpages -->
+ <string name="mozac_feature_prompt_repost_title">Resend data to this site?</string>
+ <string name="mozac_feature_prompt_repost_message">Refreshing this page could duplicate recent actions, such as sending a payment or posting a comment twice.</string>
+ <!-- Pressing this will dismiss the dialog and reload the page sending again the previous data -->
+ <string name="mozac_feature_prompt_repost_positive_button_text">Resend data</string>
+ <!-- Pressing this will dismiss the dialog and not refresh the webpage -->
+ <string name="mozac_feature_prompt_repost_negative_button_text">Cancel</string>
+
+ <!-- Credit Card Autofill -->
+ <!-- Header for the select credit card prompt to allow users to fill a form with a saved credit card. -->
+ <string name="mozac_feature_prompts_select_credit_card" moz:removedIn="125" tools:ignore="UnusedResources">Select credit card</string>
+ <!-- Header for the select card prompt to allow users to fill a form with a saved card. -->
+ <string name="mozac_feature_prompts_select_credit_card_2">Use saved card</string>
+ <!-- Content description for expanding the select credit card options in the select credit card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Expand suggested credit cards</string>
+ <!-- Content description for expanding the saved card options in the select card prompt. -->
+ <string name="mozac_feature_prompts_expand_credit_cards_content_description_2">Expand saved cards</string>
+ <!-- Content description for collapsing the select credit card options in the select credit prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Collapse suggested credit cards</string>
+ <!-- Content description for collapsing the saved card options in the select prompt. -->
+ <string name="mozac_feature_prompts_collapse_credit_cards_content_description_2">Collapse saved cards</string>
+ <!-- Option in the expanded select credit card prompt that links to credit cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards" moz:removedIn="125" tools:ignore="UnusedResources">Manage credit cards</string>
+ <!-- Option in the expanded select card prompt that links to cards settings. -->
+ <string name="mozac_feature_prompts_manage_credit_cards_2">Manage cards</string>
+ <!-- Text for the title of a save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_title">Securely save this card?</string>
+ <!-- Text for the title of an update credit card dialog. -->
+ <string name="mozac_feature_prompts_update_credit_card_prompt_title">Update card expiration date?</string>
+ <!-- Subtitle text displayed under the title of the save credit card dialog. -->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body" moz:removedIn="125" tools:ignore="UnusedResources">Card number will be encrypted. Security code won’t be saved.</string>
+ <!-- Subtitle text displayed under the title of the saved card dialog. Parameter will be replaced by app name-->
+ <string name="mozac_feature_prompts_save_credit_card_prompt_body_2">%s encrypts your card number. Your security code won’t be saved.</string>
+
+ <!-- Address Autofill -->
+ <!-- Header for the select address prompt to allow users to fill a form with a saved address. -->
+ <string name="mozac_feature_prompts_select_address_2">Select address</string>
+ <!-- Content description for expanding the select addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Expand suggested addresses</string>
+ <!-- Content description for expanding the saved addresses options in the select address prompt. -->
+ <string name="mozac_feature_prompts_expand_address_content_description_2">Expand saved addresses</string>
+ <!-- Content description for collapsing the select address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description" moz:removedIn="125" tools:ignore="UnusedResources">Collapse suggested addresses</string>
+ <!-- Content description for collapsing the saved address options in the select address prompt. -->
+ <string name="mozac_feature_prompts_collapse_address_content_description_2">Collapse saved addresses</string>
+ <!-- Text for the manage addresses button. -->
+ <string name="mozac_feature_prompts_manage_address">Manage addresses</string>
+
+ <!-- Federated Credential Management prompts -->
+ <!--Content description for the Account picture in the Select Account FedCM prompt -->
+ <string name="mozac_feature_prompts_account_picture">Account picture</string>
+ <!-- Title of the Identity Credential provider dialog chooser. -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_provider">Choose a login provider</string>
+ <!-- Title of an account picker dialog for identity credentials. The %1$s will be replaced with the name of the provider -->
+ <string name="mozac_feature_prompts_identity_credentials_choose_account_for_provider">Sign in with a %1$s account</string>
+ <!-- Title of the Identity Credential privacy policy dialog title. The %1$s will be replaced with the name of the provider. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_title">Use %1$s as a login provider</string>
+ <!-- Title of the Identity Credential privacy policy dialog description. The %1$s will be replaced with the name of the provider, %2$s will be replaced with the account, %3$s will be replaced with the privacy policy url and %4$s will be replaced with the terms of service. -->
+ <string name="mozac_feature_prompts_identity_credentials_privacy_policy_description"><![CDATA[Logging in to %1$s with a %2$s account is subject to their <a href="%3$s">Privacy Policy</a> and <a href="%4$s">Terms of Service</a>]]></string>
+ <!-- Text for the positive button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_continue">Continue</string>
+ <!-- Text for the cancel button of the Identity Credential dialogs. -->
+ <string name="mozac_feature_prompts_identity_credentials_cancel">Cancel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/values/styles.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..b883675d27
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?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>
+ <style name="MozTextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+ <item name="boxStrokeColor">@color/mozacBoxStrokeColor</item>
+ <item name="boxStrokeWidth">2dp</item>
+ <item name="android:theme">@style/MozacPromptLoginTextInputLayoutCursorAppearance</item>
+ </style>
+ <style name="MozDialogStyle" parent="Theme.Design.Light.BottomSheetDialog">
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:windowSoftInputMode">adjustResize</item>
+ </style>
+ <style name="MozacPromptLoginTextInputLayoutCursorAppearance" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">
+ <item name="colorControlActivated">?mozacPromptLoginEditTextCursorColor</item>
+ </style>
+</resources>
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/res/xml/feature_prompts_file_paths.xml b/mobile/android/android-components/components/feature/prompts/src/main/res/xml/feature_prompts_file_paths.xml
new file mode 100644
index 0000000000..9ec37fe478
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/main/res/xml/feature_prompts_file_paths.xml
@@ -0,0 +1,7 @@
+<?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/. -->
+<paths>
+ <cache-path name="feature-prompts-images" path="." />
+</paths>
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptContainerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptContainerTest.kt
new file mode 100644
index 0000000000..5da08da439
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptContainerTest.kt
@@ -0,0 +1,76 @@
+/* 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.prompts
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.fragment.app.Fragment
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+
+class PromptContainerTest {
+
+ @Mock private lateinit var activity: Activity
+
+ @Mock private lateinit var fragment: Fragment
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `get context from activity`() {
+ val container: PromptContainer = PromptContainer.Activity(activity)
+ assertEquals(activity, container.context)
+ }
+
+ @Test
+ fun `get context from fragment`() {
+ val mockContext: Context = mock()
+ val container = PromptContainer.Fragment(fragment)
+ doReturn(mockContext).`when`(fragment).requireContext()
+
+ assertEquals(mockContext, container.context)
+ }
+
+ @Suppress("DEPRECATION")
+ // https://github.com/mozilla-mobile/android-components/issues/10357
+ @Test
+ fun `startActivityForResult must delegate its calls either to an activity or a fragment`() {
+ val intent: Intent = mock()
+ val code = 1
+
+ var container: PromptContainer = PromptContainer.Activity(activity)
+ container.startActivityForResult(intent, code)
+ verify(activity).startActivityForResult(intent, code)
+
+ container = PromptContainer.Fragment(fragment)
+ container.startActivityForResult(intent, code)
+ verify(fragment).startActivityForResult(intent, code)
+ }
+
+ @Test
+ fun `getString must delegate its calls either to an activity or a fragment`() {
+ doReturn("").`when`(activity).getString(anyInt(), *emptyArray())
+ doReturn("").`when`(fragment).getString(anyInt(), *emptyArray())
+
+ var container: PromptContainer = PromptContainer.Activity(activity)
+ container.getString(R.string.mozac_feature_prompts_ok)
+ verify(activity).getString(R.string.mozac_feature_prompts_ok, *emptyArray())
+
+ container = PromptContainer.Fragment(fragment)
+ container.getString(R.string.mozac_feature_prompts_ok)
+ verify(fragment).getString(R.string.mozac_feature_prompts_ok, *emptyArray())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt
new file mode 100644
index 0000000000..3c1cfba67c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt
@@ -0,0 +1,2850 @@
+/* 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.prompts
+
+import android.app.Activity
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
+import android.content.ClipData
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.TabSessionState
+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.prompt.Choice
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.prompt.PromptRequest.Alert
+import mozilla.components.concept.engine.prompt.PromptRequest.Authentication
+import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Level.NONE
+import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Method.HOST
+import mozilla.components.concept.engine.prompt.PromptRequest.Color
+import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
+import mozilla.components.concept.engine.prompt.ShareData
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.feature.prompts.address.AddressDelegate
+import mozilla.components.feature.prompts.address.AddressPicker
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.creditcard.CreditCardDelegate
+import mozilla.components.feature.prompts.creditcard.CreditCardPicker
+import mozilla.components.feature.prompts.creditcard.CreditCardSaveDialogFragment
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment
+import mozilla.components.feature.prompts.dialog.ConfirmDialogFragment
+import mozilla.components.feature.prompts.dialog.MultiButtonDialogFragment
+import mozilla.components.feature.prompts.dialog.PromptDialogFragment
+import mozilla.components.feature.prompts.dialog.SaveLoginDialogFragment
+import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
+import mozilla.components.feature.prompts.file.FilePicker.Companion.FILE_PICKER_ACTIVITY_REQUEST_CODE
+import mozilla.components.feature.prompts.login.LoginDelegate
+import mozilla.components.feature.prompts.login.LoginPicker
+import mozilla.components.feature.prompts.share.ShareDelegate
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+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.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+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.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`
+import org.robolectric.Robolectric
+import java.lang.ref.WeakReference
+import java.security.InvalidParameterException
+import java.util.Date
+
+@RunWith(AndroidJUnit4::class)
+class PromptFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var store: BrowserStore
+ private lateinit var fragmentManager: FragmentManager
+ private lateinit var loginPicker: LoginPicker
+ private lateinit var creditCardPicker: CreditCardPicker
+ private lateinit var addressPicker: AddressPicker
+
+ private val tabId = "test-tab"
+ private fun tab(): TabSessionState? {
+ return store.state.tabs.find { it.id == tabId }
+ }
+
+ @Before
+ @ExperimentalCoroutinesApi
+ fun setUp() {
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = tabId),
+ ),
+ customTabs = listOf(
+ createCustomTab("https://www.mozilla.org", id = "custom-tab"),
+ ),
+ selectedTabId = tabId,
+ ),
+ )
+ loginPicker = mock()
+ creditCardPicker = mock()
+ addressPicker = mock()
+ fragmentManager = mockFragmentManager()
+ }
+
+ @Test
+ fun `PromptFeature acts on the selected session by default`() {
+ val feature = spy(
+ PromptFeature(
+ fragment = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ ) { },
+ )
+ feature.start()
+
+ val promptRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ verify(feature).onPromptRequested(store.state.tabs.first())
+ }
+
+ @Test
+ fun `PromptFeature acts on a given custom tab session`() {
+ val feature = spy(
+ PromptFeature(
+ fragment = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ customTabId = "custom-tab",
+ fragmentManager = fragmentManager,
+ ) { },
+ )
+ feature.start()
+
+ val promptRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", promptRequest))
+ .joinBlocking()
+ verify(feature).onPromptRequested(store.state.customTabs.first())
+ }
+
+ @Test
+ fun `PromptFeature acts on the selected session if there is no custom tab ID`() {
+ val feature = spy(
+ PromptFeature(
+ fragment = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ customTabId = tabId,
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ ) { },
+ )
+
+ val promptRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.start()
+ verify(feature).onPromptRequested(store.state.tabs.first())
+ }
+
+ @Test
+ fun `New promptRequests for selected session will cause fragment transaction`() {
+ val feature =
+ PromptFeature(
+ fragment = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ ) { }
+ feature.start()
+
+ val singleChoiceRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, singleChoiceRequest))
+ .joinBlocking()
+ verify(fragmentManager).beginTransaction()
+ }
+
+ @Test
+ fun `New promptRequests for selected session will not cause fragment transaction if feature is stopped`() {
+ val feature =
+ PromptFeature(
+ fragment = mock(),
+ tabsUseCases = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ ) { }
+ feature.start()
+ feature.stop()
+
+ val singleChoiceRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, singleChoiceRequest))
+ .joinBlocking()
+ verify(fragmentManager, never()).beginTransaction()
+ }
+
+ @Test
+ fun `Feature will re-attach to already existing fragment`() {
+ val fragment: ChoiceDialogFragment = mock()
+ doReturn(tabId).`when`(fragment).sessionId
+ doReturn(fragment).`when`(fragmentManager).findFragmentByTag(FRAGMENT_TAG)
+
+ val singleChoiceRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, singleChoiceRequest))
+ .joinBlocking()
+
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ tabsUseCases = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ ) { }
+ feature.start()
+ verify(fragment).feature = feature
+ }
+
+ @Test
+ fun `Existing fragment will be removed if session has no prompt request`() {
+ val fragment: ChoiceDialogFragment = mock()
+ doReturn(tabId).`when`(fragment).sessionId
+ doReturn(fragment).`when`(fragmentManager).findFragmentByTag(FRAGMENT_TAG)
+
+ val transaction: FragmentTransaction = mock()
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+ doReturn(transaction).`when`(transaction).remove(any())
+
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ ) { }
+ feature.start()
+
+ verify(fragment, never()).feature = feature
+ verify(fragmentManager).beginTransaction()
+ verify(transaction).remove(fragment)
+ }
+
+ @Test
+ fun `Existing fragment will be removed if session does not exist anymore`() {
+ val fragment: ChoiceDialogFragment = mock()
+ doReturn("invalid-tab").`when`(fragment).sessionId
+ doReturn(fragment).`when`(fragmentManager).findFragmentByTag(FRAGMENT_TAG)
+
+ val singleChoiceRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction("invalid-tab", singleChoiceRequest))
+ .joinBlocking()
+
+ val transaction: FragmentTransaction = mock()
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+ doReturn(transaction).`when`(transaction).remove(any())
+
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ ) { }
+ feature.start()
+
+ verify(fragment, never()).feature = feature
+ verify(fragmentManager).beginTransaction()
+ verify(transaction).remove(fragment)
+ }
+
+ @Test
+ fun `Calling onStop will attempt to dismiss the select prompts`() {
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ ) { },
+ )
+
+ feature.stop()
+
+ verify(feature).dismissSelectPrompts()
+ }
+
+ @Test
+ fun `GIVEN loginPickerView is visible WHEN dismissSelectPrompts THEN dismissCurrentLoginSelect called and true returned`() {
+ // given
+ val loginPickerView: SelectablePromptView<Login> = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ loginDelegate = object : LoginDelegate {
+ override val loginPickerView = loginPickerView
+ override val onManageLogins = {}
+ },
+ ) { },
+ )
+ val selectLoginPrompt = mock<PromptRequest.SelectLoginPrompt>()
+ whenever(loginPickerView.asView()).thenReturn(mock())
+ whenever(loginPickerView.asView().visibility).thenReturn(View.VISIBLE)
+ feature.loginPicker = loginPicker
+ feature.activePromptRequest = selectLoginPrompt
+
+ // when
+ val result = feature.dismissSelectPrompts()
+
+ // then
+ verify(feature.loginPicker!!).dismissCurrentLoginSelect(selectLoginPrompt)
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `GIVEN saveLoginPrompt is visible WHEN prompt is removed from state THEN dismiss saveLoginPrompt`() {
+ // given
+ val loginUsername = "username"
+ val loginPassword = "password"
+ val entry: LoginEntry = mock()
+ `when`(entry.username).thenReturn(loginUsername)
+ `when`(entry.password).thenReturn(loginPassword)
+ val promptRequest = PromptRequest.SaveLoginPrompt(2, listOf(entry), { }, { })
+ val saveLoginPrompt: SaveLoginDialogFragment = mock()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest))
+ .joinBlocking()
+ store.waitUntilIdle()
+
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ exitFullscreenUsecase = mock(),
+ isSaveLoginEnabled = { true },
+ loginValidationDelegate = mock(),
+ ) { },
+ )
+
+ feature.start()
+ feature.activePrompt = WeakReference(saveLoginPrompt)
+ feature.activePromptRequest = promptRequest
+
+ // when
+ store.dispatch(ContentAction.ConsumePromptRequestAction(tabId, promptRequest))
+ .joinBlocking()
+
+ // then
+ verify(saveLoginPrompt).dismissAllowingStateLoss()
+ }
+
+ @Test
+ fun `GIVEN isSaveLoginEnabled is false WHEN saveLoginPrompt request is handled THEN dismiss saveLoginPrompt`() {
+ val promptRequest = spy(
+ PromptRequest.SaveLoginPrompt(
+ hint = 2,
+ logins = emptyList(),
+ onConfirm = {},
+ onDismiss = {},
+ ),
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ isSaveLoginEnabled = { false },
+ ) {},
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
+ @Test
+ fun `GIVEN loginValidationDelegate is null WHEN saveLoginPrompt request is handled THEN dismiss saveLoginPrompt`() {
+ val promptRequest = spy(
+ PromptRequest.SaveLoginPrompt(
+ hint = 2,
+ logins = emptyList(),
+ onConfirm = {},
+ onDismiss = {},
+ ),
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ isSaveLoginEnabled = { true },
+ ) {},
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
+ @Test
+ fun `WHEN dismissDialogRequest is called THEN dismiss and consume the prompt request`() {
+ val tab = createTab("https://www.mozilla.org", id = tabId)
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ customTabs = listOf(
+ createCustomTab("https://www.mozilla.org", id = "custom-tab"),
+ ),
+ selectedTabId = tabId,
+ ),
+ ),
+ )
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ ) {}
+
+ var onDismissWasCalled = false
+ val promptRequest = PromptRequest.SaveLoginPrompt(
+ hint = 2,
+ logins = emptyList(),
+ onConfirm = {},
+ onDismiss = { onDismissWasCalled = true },
+ )
+
+ feature.dismissDialogRequest(promptRequest, tab)
+
+ store.waitUntilIdle()
+
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(tab.id, promptRequest))
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `GIVEN loginPickerView is not visible WHEN dismissSelectPrompts THEN dismissCurrentLoginSelect called and false returned`() {
+ // given
+ val loginPickerView: SelectablePromptView<Login> = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ loginDelegate = object : LoginDelegate {
+ override val loginPickerView = loginPickerView
+ override val onManageLogins = {}
+ },
+ ) { },
+ )
+ val selectLoginPrompt = mock<PromptRequest.SelectLoginPrompt>()
+ whenever(loginPickerView.asView()).thenReturn(mock())
+ whenever(loginPickerView.asView().visibility).thenReturn(View.GONE)
+ feature.loginPicker = loginPicker
+ feature.activePromptRequest = selectLoginPrompt
+
+ // when
+ val result = feature.dismissSelectPrompts()
+
+ // then
+ assertEquals(false, result)
+ }
+
+ @Test
+ fun `GIVEN PromptFeature WHEN onBackPressed THEN dismissSelectPrompts is called`() {
+ // given
+ val loginPickerView: SelectablePromptView<Login> = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ loginDelegate = object : LoginDelegate {
+ override val loginPickerView = loginPickerView
+ override val onManageLogins = {}
+ },
+ ) { },
+ )
+ val selectLoginPrompt = mock<PromptRequest.SelectLoginPrompt>()
+ whenever(loginPickerView.asView()).thenReturn(mock())
+ whenever(loginPickerView.asView().visibility).thenReturn(View.VISIBLE)
+ feature.loginPicker = loginPicker
+ feature.activePromptRequest = selectLoginPrompt
+
+ // when
+ val result = feature.onBackPressed()
+
+ // then
+ verify(feature).dismissSelectPrompts()
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `Calling dismissSelectPrompts should dismiss the login picker if the login prompt is active`() {
+ val loginPickerView: SelectablePromptView<Login> = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ loginDelegate = object : LoginDelegate {
+ override val loginPickerView = loginPickerView
+ override val onManageLogins = {}
+ },
+ ) { },
+ )
+ val selectLoginPrompt = mock<PromptRequest.SelectLoginPrompt>()
+ whenever(loginPickerView.asView()).thenReturn(mock())
+ whenever(loginPickerView.asView().visibility).thenReturn(View.VISIBLE)
+
+ feature.loginPicker = loginPicker
+ feature.activePromptRequest = mock<SingleChoice>()
+ feature.dismissSelectPrompts()
+ verify(feature.loginPicker!!, never()).dismissCurrentLoginSelect(any())
+
+ feature.loginPicker = loginPicker
+ feature.activePromptRequest = selectLoginPrompt
+ feature.dismissSelectPrompts()
+ verify(feature.loginPicker!!).dismissCurrentLoginSelect(selectLoginPrompt)
+ }
+
+ @Test
+ fun `GIVEN creditCardPickerView is visible WHEN dismissSelectPrompts is called THEN dismissSelectCreditCardRequest returns true`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ ) { },
+ )
+ val selectCreditCardRequest = mock<PromptRequest.SelectCreditCard>()
+ feature.creditCardPicker = creditCardPicker
+ feature.activePromptRequest = selectCreditCardRequest
+
+ whenever(creditCardPickerView.asView()).thenReturn(mock())
+ whenever(creditCardPickerView.asView().visibility).thenReturn(View.VISIBLE)
+
+ val result = feature.dismissSelectPrompts()
+
+ verify(feature.creditCardPicker!!).dismissSelectCreditCardRequest(selectCreditCardRequest)
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `GIVEN creditCardPickerView is not visible WHEN dismissSelectPrompts is called THEN dismissSelectPrompt returns false`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ ) { },
+ )
+ val selectCreditCardRequest = mock<PromptRequest.SelectCreditCard>()
+ feature.creditCardPicker = creditCardPicker
+ feature.activePromptRequest = selectCreditCardRequest
+
+ whenever(creditCardPickerView.asView()).thenReturn(mock())
+ whenever(creditCardPickerView.asView().visibility).thenReturn(View.GONE)
+
+ val result = feature.dismissSelectPrompts()
+
+ assertEquals(false, result)
+ }
+
+ @Test
+ fun `GIVEN an active select credit card request WHEN onBackPressed is called THEN dismissSelectPrompts is called`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ ) { },
+ )
+ val selectCreditCardRequest = mock<PromptRequest.SelectCreditCard>()
+ feature.creditCardPicker = creditCardPicker
+ feature.activePromptRequest = selectCreditCardRequest
+
+ whenever(creditCardPickerView.asView()).thenReturn(mock())
+ whenever(creditCardPickerView.asView().visibility).thenReturn(View.VISIBLE)
+
+ val result = feature.onBackPressed()
+
+ verify(feature).dismissSelectPrompts()
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `WHEN dismissSelectPrompts is called THEN the active credit card picker should be dismissed`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ ) { },
+ )
+ feature.creditCardPicker = creditCardPicker
+ feature.activePromptRequest = mock<SingleChoice>()
+
+ whenever(creditCardPickerView.asView()).thenReturn(mock())
+ whenever(creditCardPickerView.asView().visibility).thenReturn(View.VISIBLE)
+
+ feature.dismissSelectPrompts()
+ verify(feature.creditCardPicker!!, never()).dismissSelectCreditCardRequest(any())
+
+ val selectCreditCardRequest = mock<PromptRequest.SelectCreditCard>()
+ feature.activePromptRequest = selectCreditCardRequest
+
+ feature.dismissSelectPrompts()
+
+ verify(feature.creditCardPicker!!).dismissSelectCreditCardRequest(selectCreditCardRequest)
+ }
+
+ @Test
+ fun `WHEN dismissSelectPrompts is called THEN the active addressPicker dismiss should be called`() {
+ val addressPickerView: SelectablePromptView<Address> = mock()
+ val addressDelegate: AddressDelegate = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ addressDelegate = addressDelegate,
+ ) { },
+ )
+ feature.addressPicker = addressPicker
+ feature.activePromptRequest = mock<SingleChoice>()
+
+ whenever(addressDelegate.addressPickerView).thenReturn(addressPickerView)
+ whenever(addressPickerView.asView()).thenReturn(mock())
+ whenever(addressPickerView.asView().visibility).thenReturn(View.VISIBLE)
+
+ feature.dismissSelectPrompts()
+ verify(feature.addressPicker!!, never()).dismissSelectAddressRequest(any())
+
+ val selectAddressPromptRequest = mock<PromptRequest.SelectAddress>()
+ feature.activePromptRequest = selectAddressPromptRequest
+
+ feature.dismissSelectPrompts()
+
+ verify(feature.addressPicker!!).dismissSelectAddressRequest(selectAddressPromptRequest)
+
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN addressPickerView is not visible WHEN dismissSelectPrompts is called THEN dismissSelectPrompts returns false`() {
+ val addressPickerView: SelectablePromptView<Address> = mock()
+ val addressDelegate: AddressDelegate = mock()
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ addressDelegate = addressDelegate,
+ ) { },
+ )
+ val selectAddressRequest = mock<PromptRequest.SelectAddress>()
+ feature.addressPicker = addressPicker
+ feature.activePromptRequest = selectAddressRequest
+
+ whenever(addressDelegate.addressPickerView).thenReturn(addressPickerView)
+ whenever(addressPickerView.asView()).thenReturn(mock())
+ whenever(addressPickerView.asView().visibility).thenReturn(View.GONE)
+
+ val result = feature.dismissSelectPrompts()
+
+ assertEquals(false, result)
+ }
+
+ @Test
+ fun `Calling onCancel will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ ) { }
+
+ val singleChoiceRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, singleChoiceRequest))
+ .joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(singleChoiceRequest, tab()!!.content.promptRequests[0])
+ feature.onCancel(tabId, singleChoiceRequest.uid)
+
+ store.waitUntilIdle()
+ assertTrue(tab()?.content?.promptRequests?.isEmpty() ?: false)
+ }
+
+ @Test
+ fun `Selecting an item in a single choice dialog will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ feature.start()
+
+ val singleChoiceRequest = SingleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, singleChoiceRequest))
+ .joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(singleChoiceRequest, tab()!!.content.promptRequests[0])
+ feature.onConfirm(tabId, singleChoiceRequest.uid, mock<Choice>())
+
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `Selecting an item in a menu choice dialog will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ feature.start()
+
+ val menuChoiceRequest = MenuChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, menuChoiceRequest))
+ .joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(menuChoiceRequest, tab()!!.content.promptRequests[0])
+ feature.onConfirm(tabId, menuChoiceRequest.uid, mock<Choice>())
+
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `Selecting items on multiple choice dialog will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ feature.start()
+
+ val multipleChoiceRequest = MultipleChoice(arrayOf(), {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, multipleChoiceRequest))
+ .joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(multipleChoiceRequest, tab()!!.content.promptRequests[0])
+ feature.onConfirm(tabId, multipleChoiceRequest.uid, arrayOf<Choice>())
+
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `onNoMoreDialogsChecked will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+
+ var onShowNoMoreAlertsWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptRequest = Alert(
+ "title",
+ "message",
+ false,
+ { onShowNoMoreAlertsWasCalled = true },
+ { onDismissWasCalled = true },
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.onConfirm(tabId, promptRequest.uid, false)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onShowNoMoreAlertsWasCalled)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `Calling onCancel with an alert request will consume promptRequest and call onDismiss`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onDismissWasCalled = false
+ val promptRequest = Alert("title", "message", false, {}, { onDismissWasCalled = true })
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(onDismissWasCalled)
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `onConfirmTextPrompt will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onConfirmWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptRequest = TextPrompt(
+ "title",
+ "message",
+ "input",
+ false,
+ { _, _ -> onConfirmWasCalled = true },
+ { onDismissWasCalled = true },
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.onConfirm(tabId, promptRequest.uid, false to "")
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onConfirmWasCalled)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `Calling onCancel with an TextPrompt request will consume promptRequest and call onDismiss`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onDismissWasCalled = false
+
+ val promptRequest = TextPrompt(
+ "title",
+ "message",
+ "value",
+ false,
+ { _, _ -> },
+ { onDismissWasCalled = true },
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `selecting a time will consume promptRequest`() {
+ val timeSelectionTypes = listOf(
+ PromptRequest.TimeSelection.Type.DATE,
+ PromptRequest.TimeSelection.Type.DATE_AND_TIME,
+ PromptRequest.TimeSelection.Type.TIME,
+ PromptRequest.TimeSelection.Type.MONTH,
+ )
+
+ timeSelectionTypes.forEach { type ->
+ val feature = PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onClearWasCalled = false
+ var selectedDate: Date? = null
+ val promptRequest = PromptRequest.TimeSelection(
+ "title",
+ Date(0),
+ null,
+ null,
+ null,
+ type,
+ { date -> selectedDate = date },
+ { onClearWasCalled = true },
+ { },
+ )
+
+ feature.start()
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest))
+ .joinBlocking()
+
+ val now = Date()
+ feature.onConfirm(tabId, promptRequest.uid, now)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+
+ assertEquals(now, selectedDate)
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest))
+ .joinBlocking()
+
+ feature.onClear(tabId, promptRequest.uid)
+ assertTrue(onClearWasCalled)
+ feature.stop()
+ }
+ }
+
+ @Test(expected = InvalidParameterException::class)
+ fun `calling handleDialogsRequest with invalid type will throw an exception`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ ) { }
+ feature.handleDialogsRequest(mock<PromptRequest.File>(), mock())
+ }
+
+ @Test
+ fun `onActivityResult with RESULT_OK and isMultipleFilesSelection false will consume PromptRequest`() {
+ var onSingleFileSelectionWasCalled = false
+
+ val onSingleFileSelection: (Context, Uri) -> Unit = { _, _ ->
+ onSingleFileSelectionWasCalled = true
+ }
+
+ val filePickerRequest =
+ PromptRequest.File(emptyArray(), false, onSingleFileSelection, { _, _ -> }) { }
+ val activity = mock<Activity>()
+ doReturn(mock<ContentResolver>()).`when`(activity).contentResolver
+
+ val feature =
+ PromptFeature(
+ activity = activity,
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ ) { }
+ val intent = Intent()
+
+ intent.data = mock()
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, filePickerRequest))
+ .joinBlocking()
+
+ feature.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, intent, RESULT_OK)
+ store.waitUntilIdle()
+ assertTrue(onSingleFileSelectionWasCalled)
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `onActivityResult with RESULT_OK and isMultipleFilesSelection true will consume PromptRequest of the actual session`() {
+ var onMultipleFileSelectionWasCalled = false
+
+ val onMultipleFileSelection: (Context, Array<Uri>) -> Unit = { _, _ ->
+ onMultipleFileSelectionWasCalled = true
+ }
+
+ val filePickerRequest =
+ PromptRequest.File(emptyArray(), true, { _, _ -> }, onMultipleFileSelection) {}
+ val activity = mock<Activity>()
+ doReturn(mock<ContentResolver>()).`when`(activity).contentResolver
+
+ val feature =
+ PromptFeature(
+ activity = activity,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ ) { }
+ val intent = Intent()
+
+ intent.clipData = mock()
+ val item = mock<ClipData.Item>()
+
+ doReturn(mock<Uri>()).`when`(item).uri
+
+ intent.clipData?.apply {
+ doReturn(1).`when`(this).itemCount
+ doReturn(item).`when`(this).getItemAt(0)
+ }
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, filePickerRequest))
+ .joinBlocking()
+
+ feature.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, intent, RESULT_OK)
+ store.waitUntilIdle()
+ assertTrue(onMultipleFileSelectionWasCalled)
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `onActivityResult with RESULT_CANCELED will consume PromptRequest call onDismiss`() {
+ var onDismissWasCalled = false
+
+ val filePickerRequest =
+ PromptRequest.File(emptyArray(), true, { _, _ -> }, { _, _ -> }) {
+ onDismissWasCalled = true
+ }
+
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ ) { }
+ val intent = Intent()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, filePickerRequest))
+ .joinBlocking()
+
+ feature.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, intent, RESULT_CANCELED)
+ store.waitUntilIdle()
+ assertTrue(onDismissWasCalled)
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `WHEN onActivityResult is called with PIN_REQUEST and RESULT_OK THEN onAuthSuccess) is called`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ isCreditCardAutofillEnabled = { true },
+ ) { }
+ feature.creditCardPicker = creditCardPicker
+ val intent = Intent()
+
+ feature.onActivityResult(PromptFeature.PIN_REQUEST, intent, RESULT_OK)
+
+ verify(creditCardPicker).onAuthSuccess()
+ }
+
+ @Test
+ fun `WHEN onActivityResult is called with PIN_REQUEST and RESULT_CANCELED THEN onAuthFailure is called`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ isCreditCardAutofillEnabled = { true },
+ ) { }
+ feature.creditCardPicker = creditCardPicker
+ val intent = Intent()
+
+ feature.onActivityResult(PromptFeature.PIN_REQUEST, intent, RESULT_CANCELED)
+
+ verify(creditCardPicker).onAuthFailure()
+ }
+
+ @Test
+ fun `GIVEN user successfully authenticates by biometric prompt WHEN onBiometricResult is called THEN onAuthSuccess is called`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ isCreditCardAutofillEnabled = { true },
+ ) { }
+ feature.creditCardPicker = creditCardPicker
+
+ feature.onBiometricResult(isAuthenticated = true)
+
+ verify(creditCardPicker).onAuthSuccess()
+ }
+
+ @Test
+ fun `GIVEN user fails to authenticate by biometric prompt WHEN onBiometricResult is called THEN onAuthFailure) is called`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ isCreditCardAutofillEnabled = { true },
+ ) { }
+ feature.creditCardPicker = creditCardPicker
+
+ feature.onBiometricResult(isAuthenticated = false)
+
+ verify(creditCardPicker).onAuthFailure()
+ }
+
+ @Test
+ fun `Selecting a login confirms the request`() {
+ var onDismissWasCalled = false
+ var confirmedLogin: Login? = null
+
+ val login =
+ Login(guid = "A", origin = "https://www.mozilla.org", username = "username", password = "password")
+ val login2 =
+ Login(guid = "B", origin = "https://www.mozilla.org", username = "username2", password = "password")
+
+ val loginPickerRequest = PromptRequest.SelectLoginPrompt(
+ logins = listOf(login, login2),
+ generatedPassword = null,
+ onConfirm = { confirmedLogin = it },
+ onDismiss = { onDismissWasCalled = true },
+ )
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, loginPickerRequest))
+ .joinBlocking()
+
+ loginPickerRequest.onConfirm(login)
+
+ store.waitUntilIdle()
+
+ assertEquals(confirmedLogin, login)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, loginPickerRequest))
+ .joinBlocking()
+ loginPickerRequest.onDismiss()
+
+ store.waitUntilIdle()
+
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `WHEN a credit card is selected THEN confirm the prompt request with the selected credit card`() {
+ val creditCard = CreditCardEntry(
+ guid = "id",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+ var onDismissCalled = false
+ var onConfirmCalled = false
+ var confirmedCreditCard: CreditCardEntry? = null
+
+ val selectCreditCardRequest = PromptRequest.SelectCreditCard(
+ creditCards = listOf(creditCard),
+ onDismiss = {
+ onDismissCalled = true
+ },
+ onConfirm = {
+ confirmedCreditCard = it
+ onConfirmCalled = true
+ },
+ )
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, selectCreditCardRequest))
+ .joinBlocking()
+
+ selectCreditCardRequest.onConfirm(creditCard)
+
+ store.waitUntilIdle()
+
+ assertEquals(creditCard, confirmedCreditCard)
+ assertTrue(onConfirmCalled)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, selectCreditCardRequest))
+ .joinBlocking()
+ selectCreditCardRequest.onDismiss()
+
+ store.waitUntilIdle()
+
+ assertTrue(onDismissCalled)
+ }
+
+ @Test
+ fun `Calling onConfirmAuthentication will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+
+ var onConfirmWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptRequest = Authentication(
+ uri = "https://www.mozilla.org",
+ title = "title",
+ message = "message",
+ userName = "username",
+ password = "password",
+ method = HOST,
+ level = NONE,
+ onlyShowPassword = false,
+ previousFailed = false,
+ isCrossOrigin = false,
+ onConfirm = { _, _ -> onConfirmWasCalled = true },
+ onDismiss = { onDismissWasCalled = true },
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onConfirm(tabId, promptRequest.uid, "" to "")
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onConfirmWasCalled)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `Calling onConfirm on a BeforeUnload request will consume promptRequest`() {
+ val fragment: Fragment = mock()
+ whenever(fragment.getString(R.string.mozac_feature_prompt_before_unload_dialog_title)).thenReturn(
+ "",
+ )
+ whenever(fragment.getString(R.string.mozac_feature_prompt_before_unload_dialog_body)).thenReturn(
+ "",
+ )
+ whenever(fragment.getString(R.string.mozac_feature_prompts_before_unload_stay)).thenReturn("")
+ whenever(fragment.getString(R.string.mozac_feature_prompts_before_unload_leave)).thenReturn(
+ "",
+ )
+
+ val feature =
+ PromptFeature(
+ fragment = fragment,
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+
+ var onLeaveWasCalled = false
+
+ val promptRequest = PromptRequest.BeforeUnload(
+ title = "title",
+ onLeave = { onLeaveWasCalled = true },
+ onStay = { },
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onConfirm(tabId, promptRequest.uid, "" to "")
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onLeaveWasCalled)
+ }
+
+ @Test
+ fun `Calling onCancel on a authentication request will consume promptRequest and call onDismiss`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onDismissWasCalled = false
+
+ val promptRequest = Authentication(
+ uri = "https://www.mozilla.org",
+ title = "title",
+ message = "message",
+ userName = "username",
+ password = "password",
+ method = HOST,
+ level = NONE,
+ onlyShowPassword = false,
+ previousFailed = false,
+ isCrossOrigin = false,
+ onConfirm = { _, _ -> },
+ onDismiss = { onDismissWasCalled = true },
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `Calling onConfirm on a color request will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+
+ var onConfirmWasCalled = false
+ var onDismissWasCalled = false
+
+ val promptRequest = Color(
+ "#e66465",
+ {
+ onConfirmWasCalled = true
+ },
+ ) {
+ onDismissWasCalled = true
+ }
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onConfirm(tabId, promptRequest.uid, "#f6b73c")
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onConfirmWasCalled)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onDismissWasCalled)
+ }
+
+ @Test
+ fun `Calling onConfirm on a popup request will consume promptRequest`() {
+ val fragment: Fragment = mock()
+ whenever(fragment.getString(R.string.mozac_feature_prompts_popup_dialog_title)).thenReturn("")
+ whenever(fragment.getString(R.string.mozac_feature_prompts_allow)).thenReturn("")
+ whenever(fragment.getString(R.string.mozac_feature_prompts_deny)).thenReturn("")
+
+ val feature =
+ PromptFeature(
+ fragment = fragment,
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onConfirmWasCalled = false
+
+ val promptRequest = PromptRequest.Popup(
+ "http://www.popuptest.com/",
+ { onConfirmWasCalled = true },
+ { },
+ ) {}
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onConfirm(tabId, promptRequest.uid, true)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onConfirmWasCalled)
+ }
+
+ @Test
+ fun `Calling onCancel on a popup request will consume promptRequest`() {
+ val fragment: Fragment = mock()
+ whenever(fragment.getString(R.string.mozac_feature_prompts_popup_dialog_title)).thenReturn("")
+ whenever(fragment.getString(R.string.mozac_feature_prompts_allow)).thenReturn("")
+ whenever(fragment.getString(R.string.mozac_feature_prompts_deny)).thenReturn("")
+
+ val feature =
+ PromptFeature(
+ fragment = fragment,
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onCancelWasCalled = false
+
+ val promptRequest = PromptRequest.Popup(
+ "http://www.popuptest.com/",
+ onAllow = { },
+ onDeny = {
+ onCancelWasCalled = true
+ },
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onCancel(tabId, promptRequest.uid, true)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onCancelWasCalled)
+ }
+
+ @Test
+ fun `Calling onCancel on a BeforeUnload request will consume promptRequest`() {
+ val fragment: Fragment = mock()
+ whenever(fragment.getString(R.string.mozac_feature_prompt_before_unload_dialog_title)).thenReturn(
+ "",
+ )
+ whenever(fragment.getString(R.string.mozac_feature_prompt_before_unload_dialog_body)).thenReturn(
+ "",
+ )
+ whenever(fragment.getString(R.string.mozac_feature_prompts_before_unload_stay)).thenReturn("")
+ whenever(fragment.getString(R.string.mozac_feature_prompts_before_unload_leave)).thenReturn(
+ "",
+ )
+
+ val feature =
+ PromptFeature(
+ fragment = fragment,
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onCancelWasCalled = false
+
+ val promptRequest = PromptRequest.BeforeUnload("http://www.test.com/", { }) {
+ onCancelWasCalled = true
+ }
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onCancelWasCalled)
+ }
+
+ @Test
+ fun `Calling onConfirm on a confirm request will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onPositiveButtonWasCalled = false
+ var onNegativeButtonWasCalled = false
+ var onNeutralButtonWasCalled = false
+
+ val onConfirmPositiveButton: (Boolean) -> Unit = {
+ onPositiveButtonWasCalled = true
+ }
+
+ val onConfirmNegativeButton: (Boolean) -> Unit = {
+ onNegativeButtonWasCalled = true
+ }
+
+ val onConfirmNeutralButton: (Boolean) -> Unit = {
+ onNeutralButtonWasCalled = true
+ }
+
+ val promptRequest = PromptRequest.Confirm(
+ "title",
+ "message",
+ false,
+ "positive",
+ "negative",
+ "neutral",
+ onConfirmPositiveButton,
+ onConfirmNegativeButton,
+ onConfirmNeutralButton,
+ ) {}
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.onConfirm(tabId, promptRequest.uid, true to MultiButtonDialogFragment.ButtonType.POSITIVE)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onPositiveButtonWasCalled)
+
+ feature.promptAbuserDetector.resetJSAlertAbuseState()
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.onConfirm(tabId, promptRequest.uid, true to MultiButtonDialogFragment.ButtonType.NEGATIVE)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onNegativeButtonWasCalled)
+
+ feature.promptAbuserDetector.resetJSAlertAbuseState()
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.onConfirm(tabId, promptRequest.uid, true to MultiButtonDialogFragment.ButtonType.NEUTRAL)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onNeutralButtonWasCalled)
+ }
+
+ @Test
+ fun `Calling onCancel on a confirm request will consume promptRequest`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onCancelWasCalled = false
+
+ val onConfirm: (Boolean) -> Unit = { }
+
+ val onDismiss: () -> Unit = {
+ onCancelWasCalled = true
+ }
+
+ val promptRequest = PromptRequest.Confirm(
+ "title",
+ "message",
+ false,
+ "positive",
+ "negative",
+ "neutral",
+ onConfirm,
+ onConfirm,
+ onConfirm,
+ onDismiss,
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ feature.onCancel(tabId, promptRequest.uid)
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onCancelWasCalled)
+ }
+
+ @Test
+ fun `When dialogs are being abused prompts are not allowed`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onDismissWasCalled: Boolean
+ val onDismiss = { onDismissWasCalled = true }
+ val alertRequest = Alert("", "", false, {}, onDismiss)
+ val textRequest = TextPrompt("", "", "", false, { _, _ -> }, onDismiss)
+ val confirmRequest =
+ PromptRequest.Confirm("", "", false, "+", "-", "", {}, {}, {}, onDismiss)
+
+ val promptRequests = arrayOf<PromptRequest>(alertRequest, textRequest, confirmRequest)
+
+ feature.start()
+ feature.promptAbuserDetector.userWantsMoreDialogs(false)
+
+ promptRequests.forEach { request ->
+ onDismissWasCalled = false
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, request)).joinBlocking()
+ verify(fragmentManager, never()).beginTransaction()
+ assertTrue(onDismissWasCalled)
+ }
+ }
+
+ @Test
+ fun `When dialogs are being abused but the page is refreshed prompts are allowed`() {
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+ var onDismissWasCalled = false
+ val onDismiss = { onDismissWasCalled = true }
+ val alertRequest = Alert("", "", false, {}, onDismiss)
+
+ feature.start()
+ feature.promptAbuserDetector.userWantsMoreDialogs(false)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, alertRequest)).joinBlocking()
+
+ verify(fragmentManager, never()).beginTransaction()
+ assertTrue(onDismissWasCalled)
+
+ // Simulate reloading page
+ store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, true)).joinBlocking()
+ store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, false)).joinBlocking()
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, alertRequest)).joinBlocking()
+
+ assertTrue(feature.promptAbuserDetector.shouldShowMoreDialogs)
+ verify(fragmentManager).beginTransaction()
+ }
+
+ @Test
+ fun `User can stop further popups from being displayed on the current page`() {
+ val feature = PromptFeature(
+ activity = Robolectric.buildActivity(Activity::class.java).setup().get(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ ) { }
+
+ var onDenyCalled = false
+ val onDeny = { onDenyCalled = true }
+ val popupPrompt = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
+
+ feature.start()
+ assertTrue(feature.promptAbuserDetector.shouldShowMoreDialogs)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt)).joinBlocking()
+ verify(fragmentManager, times(1)).beginTransaction()
+ feature.onCancel(tabId, popupPrompt.uid, true)
+ assertFalse(feature.promptAbuserDetector.shouldShowMoreDialogs)
+ assertTrue(onDenyCalled)
+
+ onDenyCalled = false
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt)).joinBlocking()
+ verify(fragmentManager, times(1)).beginTransaction()
+ assertFalse(feature.promptAbuserDetector.shouldShowMoreDialogs)
+ assertTrue(onDenyCalled)
+ }
+
+ @Test
+ fun `When page is refreshed login dialog is dismissed`() {
+ val loginPickerView: SelectablePromptView<Login> = mock()
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ loginDelegate = object : LoginDelegate {
+ override val loginPickerView = loginPickerView
+ override val onManageLogins = {}
+ },
+ ) { }
+ feature.loginPicker = loginPicker
+ val onLoginDismiss: () -> Unit = {}
+ val onLoginConfirm: (Login) -> Unit = {}
+
+ val login = Login(guid = "A", origin = "origin", username = "username", password = "password")
+ val selectLoginRequest =
+ PromptRequest.SelectLoginPrompt(listOf(login), null, onLoginConfirm, onLoginDismiss)
+
+ whenever(loginPickerView.asView()).thenReturn(mock())
+ whenever(loginPickerView.asView().visibility).thenReturn(View.VISIBLE)
+
+ feature.start()
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, selectLoginRequest))
+ .joinBlocking()
+
+ verify(loginPicker).handleSelectLoginRequest(selectLoginRequest)
+
+ // Simulate reloading page
+ store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, true)).joinBlocking()
+
+ verify(loginPicker).dismissCurrentLoginSelect(selectLoginRequest)
+ }
+
+ @Test
+ fun `WHEN page is refreshed THEN credit card prompt is dismissed`() {
+ val creditCardPickerView: SelectablePromptView<CreditCardEntry> = mock()
+ val feature =
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ creditCardDelegate = object : CreditCardDelegate {
+ override val creditCardPickerView = creditCardPickerView
+ override val onSelectCreditCard = {}
+ override val onManageCreditCards = {}
+ },
+ isCreditCardAutofillEnabled = { true },
+ ) { }
+ feature.creditCardPicker = creditCardPicker
+ val onDismiss: () -> Unit = {}
+ val onConfirm: (CreditCardEntry) -> Unit = {}
+ val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+ val selectCreditCardRequest =
+ PromptRequest.SelectCreditCard(listOf(creditCard), onConfirm, onDismiss)
+
+ whenever(creditCardPickerView.asView()).thenReturn(mock())
+ whenever(creditCardPickerView.asView().visibility).thenReturn(View.VISIBLE)
+
+ feature.start()
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, selectCreditCardRequest))
+ .joinBlocking()
+
+ verify(creditCardPicker).handleSelectCreditCardRequest(selectCreditCardRequest)
+
+ // Simulate reloading page
+ store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, true)).joinBlocking()
+
+ verify(creditCardPicker).dismissSelectCreditCardRequest(selectCreditCardRequest)
+ }
+
+ @Test
+ fun `Share prompt calls ShareDelegate`() {
+ val delegate: ShareDelegate = mock()
+ val activity: Activity = mock()
+ val feature = spy(
+ PromptFeature(
+ activity,
+ store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ customTabId = "custom-tab",
+ shareDelegate = delegate,
+ fragmentManager = fragmentManager,
+ ) { },
+ )
+ feature.start()
+
+ val promptRequest = PromptRequest.Share(ShareData("Title", "Text", null), {}, {}, {})
+ store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", promptRequest))
+ .joinBlocking()
+
+ verify(feature).onPromptRequested(store.state.customTabs.first())
+ verify(delegate).showShareSheet(
+ eq(activity),
+ eq(promptRequest.data),
+ onDismiss = any(),
+ onSuccess = any(),
+ )
+ }
+
+ @Test
+ fun `GIVEN credit card autofill enabled and cards available WHEN getting a SelectCreditCard request THEN that request is handled`() {
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ tabsUseCases = mock(),
+ customTabId = "custom-tab",
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ isCreditCardAutofillEnabled = { true },
+ ) { },
+ )
+ feature.creditCardPicker = creditCardPicker
+ feature.start()
+ val selectCreditCardRequest = PromptRequest.SelectCreditCard(listOf(mock()), {}, {})
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", selectCreditCardRequest))
+ .joinBlocking()
+
+ verify(feature).onPromptRequested(store.state.customTabs.first())
+ verify(creditCardPicker).handleSelectCreditCardRequest(selectCreditCardRequest)
+ }
+
+ @Test
+ fun `GIVEN credit card autofill enabled but no cards available WHEN getting a SelectCreditCard request THEN that request is not acted upon`() {
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ customTabId = "custom-tab",
+ fragmentManager = fragmentManager,
+ isCreditCardAutofillEnabled = { true },
+ ) { },
+ )
+ feature.creditCardPicker = creditCardPicker
+ feature.start()
+ val selectCreditCardRequest = PromptRequest.SelectCreditCard(emptyList(), {}, {})
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", selectCreditCardRequest))
+ .joinBlocking()
+
+ verify(feature).onPromptRequested(store.state.customTabs.first())
+ verify(creditCardPicker, never()).handleSelectCreditCardRequest(selectCreditCardRequest)
+ }
+
+ @Test
+ fun `GIVEN credit card autofill disabled and cards available WHEN getting a SelectCreditCard request THEN that request is handled`() {
+ val feature = spy(
+ PromptFeature(
+ mock<Activity>(),
+ store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ customTabId = "custom-tab",
+ fragmentManager = fragmentManager,
+ isCreditCardAutofillEnabled = { false },
+ ) { },
+ )
+ feature.creditCardPicker = creditCardPicker
+ feature.start()
+ val selectCreditCardRequest = PromptRequest.SelectCreditCard(listOf(mock()), {}, {})
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", selectCreditCardRequest))
+ .joinBlocking()
+
+ verify(feature).onPromptRequested(store.state.customTabs.first())
+ verify(creditCardPicker, never()).handleSelectCreditCardRequest(selectCreditCardRequest)
+ }
+
+ @Test
+ fun `GIVEN a custom tab WHEN a new prompt is requested THEN exit fullscreen`() {
+ val exitFullScreenUseCase: SessionUseCases.ExitFullScreenUseCase = mock()
+ val feature = PromptFeature(
+ fragment = mock(),
+ store = store,
+ fileUploadsDirCleaner = mock(),
+ tabsUseCases = mock(),
+ customTabId = "custom-tab",
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = exitFullScreenUseCase,
+ ) { }
+ val promptRequest: Alert = mock()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", promptRequest)).joinBlocking()
+ feature.start()
+
+ verify(exitFullScreenUseCase).invoke("custom-tab")
+ }
+
+ @Test
+ fun `GIVEN a normal tab WHEN a new prompt is requested THEN exit fullscreen`() {
+ val exitFullScreenUseCase: SessionUseCases.ExitFullScreenUseCase = mock()
+ val feature = PromptFeature(
+ fragment = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = exitFullScreenUseCase,
+ ) { }
+ val promptRequest: Alert = mock()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+ feature.start()
+
+ verify(exitFullScreenUseCase).invoke(tabId)
+ }
+
+ @Test
+ fun `GIVEN a private tab WHEN a new prompt is requested THEN exit fullscreen`() {
+ val privateTabId = "private-tab"
+ val exitFullScreenUseCase: SessionUseCases.ExitFullScreenUseCase = mock()
+ store = BrowserStore(
+ initialState = store.state.copy(
+ tabs = store.state.tabs + TabSessionState(
+ id = privateTabId,
+ content = ContentState(url = "", private = true),
+ ),
+ selectedTabId = privateTabId,
+ ),
+ )
+ val feature = PromptFeature(
+ fragment = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = exitFullScreenUseCase,
+ ) { }
+ val promptRequest: Alert = mock()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(privateTabId, promptRequest)).joinBlocking()
+ feature.start()
+
+ verify(exitFullScreenUseCase).invoke(privateTabId)
+ }
+
+ @Test
+ fun `GIVEN isCreditCardAutofillEnabled is false WHEN SaveCreditCard request is handled THEN dismiss SaveCreditCard`() {
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+ val promptRequest = spy(
+ PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {},
+ ),
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ creditCardValidationDelegate = mock(),
+ isCreditCardAutofillEnabled = { false },
+ ) {},
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
+ @Test
+ fun `GIVEN creditCardValidationDelegate is null WHEN SaveCreditCard request is handled THEN dismiss SaveCreditCard`() {
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+ val promptRequest = spy(
+ PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {},
+ ),
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ creditCardValidationDelegate = null,
+ isCreditCardAutofillEnabled = { true },
+ ) {},
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
+ @Test
+ fun `GIVEN prompt request credit card is invalid WHEN SaveCreditCard request is handled THEN dismiss SaveCreditCard`() {
+ val invalidMonth = ""
+ val invalidYear = ""
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = invalidMonth,
+ expiryYear = invalidYear,
+ cardType = "",
+ )
+ val promptRequest = spy(
+ PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {},
+ ),
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ creditCardValidationDelegate = mock(),
+ fileUploadsDirCleaner = mock(),
+ isCreditCardAutofillEnabled = { true },
+ ) {},
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
+ @Test
+ fun `Selecting an item in a share dialog will consume promptRequest`() {
+ val delegate: ShareDelegate = mock()
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ exitFullscreenUsecase = mock(),
+ shareDelegate = delegate,
+ ) { }
+ feature.start()
+
+ var onSuccessCalled = false
+
+ val shareRequest = PromptRequest.Share(
+ ShareData("Title", "Text", null),
+ onSuccess = { onSuccessCalled = true },
+ onFailure = {},
+ onDismiss = {},
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, shareRequest)).joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(shareRequest, tab()!!.content.promptRequests[0])
+ feature.onConfirm(tabId, shareRequest.uid, null)
+
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onSuccessCalled)
+ }
+
+ @Test
+ fun `Dismissing a share dialog will consume promptRequest`() {
+ val delegate: ShareDelegate = mock()
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ shareDelegate = delegate,
+ ) { }
+ feature.start()
+
+ var onDismissCalled = false
+
+ val shareRequest = PromptRequest.Share(
+ ShareData("Title", "Text", null),
+ onSuccess = {},
+ onFailure = {},
+ onDismiss = { onDismissCalled = true },
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, shareRequest)).joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(shareRequest, tab()!!.content.promptRequests[0])
+ feature.onCancel(tabId, shareRequest.uid)
+
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ assertTrue(onDismissCalled)
+ }
+
+ @Test
+ fun `dialog will be dismissed if tab ID changes`() {
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ shareDelegate = mock(),
+ ) { },
+ )
+ feature.start()
+
+ val shareRequest = PromptRequest.Share(
+ ShareData("Title", "Text", null),
+ onSuccess = {},
+ onFailure = {},
+ onDismiss = {},
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, shareRequest)).joinBlocking()
+
+ val fragment = mock<PromptDialogFragment>()
+ whenever(fragment.shouldDismissOnLoad).thenReturn(true)
+ whenever(fragment.sessionId).thenReturn(tabId)
+ feature.activePrompt = WeakReference(fragment)
+
+ val secondTabId = "second-test-tab"
+ store.dispatch(
+ TabListAction.AddTabAction(
+ TabSessionState(
+ id = secondTabId,
+ content = ContentState(url = "mozilla.org"),
+ ),
+ select = true,
+ ),
+ ).joinBlocking()
+
+ verify(fragment, times(1)).dismiss()
+ }
+
+ @Test
+ fun `dialog will be dismissed if tab changes`() {
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ shareDelegate = mock(),
+ ) { },
+ )
+ feature.start()
+
+ val shareRequest = PromptRequest.Share(
+ ShareData("Title", "Text", null),
+ onSuccess = {},
+ onFailure = {},
+ onDismiss = {},
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, shareRequest)).joinBlocking()
+
+ val fragment = mock<PromptDialogFragment>()
+ whenever(fragment.shouldDismissOnLoad).thenReturn(true)
+ whenever(fragment.sessionId).thenReturn(tabId)
+ feature.activePrompt = WeakReference(fragment)
+
+ val newTabId = "test-tab-2"
+
+ store.dispatch(TabListAction.SelectTabAction(newTabId)).joinBlocking()
+
+ verify(fragment, times(1)).dismiss()
+ }
+
+ @Test
+ fun `dialog will be dismissed if tab URL changes`() {
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ exitFullscreenUsecase = mock(),
+ shareDelegate = mock(),
+ ) { },
+ )
+ feature.start()
+
+ val shareRequest = PromptRequest.Share(
+ ShareData("Title", "Text", null),
+ onSuccess = {},
+ onFailure = {},
+ onDismiss = {},
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, shareRequest)).joinBlocking()
+
+ val fragment = mock<PromptDialogFragment>()
+ whenever(fragment.shouldDismissOnLoad).thenReturn(true)
+ feature.activePrompt = WeakReference(fragment)
+
+ store.dispatch(ContentAction.UpdateUrlAction(tabId, "mozilla.org")).joinBlocking()
+ verify(fragment, times(1)).dismiss()
+ }
+
+ @Test
+ fun `prompt will always start the save login dialog with an icon`() {
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ shareDelegate = mock(),
+ isSaveLoginEnabled = { true },
+ loginValidationDelegate = mock(),
+ ) { }
+ val loginUsername = "username"
+ val loginPassword = "password"
+ val entry: LoginEntry = mock()
+ `when`(entry.username).thenReturn(loginUsername)
+ `when`(entry.password).thenReturn(loginPassword)
+ val loginsPrompt = PromptRequest.SaveLoginPrompt(2, listOf(entry), { }, { })
+ val websiteIcon: Bitmap = mock()
+ val contentState: ContentState = mock()
+ val session: TabSessionState = mock()
+ val sessionId = "sessionId"
+ `when`(contentState.icon).thenReturn(websiteIcon)
+ `when`(session.content).thenReturn(contentState)
+ `when`(session.id).thenReturn(sessionId)
+
+ feature.handleDialogsRequest(
+ loginsPrompt,
+ session,
+ )
+
+ // Only interested in the icon, but it doesn't hurt to be sure we show a properly configured dialog.
+ assertTrue(feature.activePrompt!!.get() is SaveLoginDialogFragment)
+ val dialogFragment = feature.activePrompt!!.get() as SaveLoginDialogFragment
+ assertEquals(loginUsername, dialogFragment.username)
+ assertEquals(loginPassword, dialogFragment.password)
+ assertEquals(websiteIcon, dialogFragment.icon)
+ assertEquals(sessionId, dialogFragment.sessionId)
+ }
+
+ @Test
+ fun `save login dialog will not be dismissed on page load`() {
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ shareDelegate = mock(),
+ ) { },
+ )
+ feature.start()
+
+ val shareRequest = PromptRequest.Share(
+ ShareData("Title", "Text", null),
+ onSuccess = {},
+ onFailure = {},
+ onDismiss = {},
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, shareRequest)).joinBlocking()
+
+ val fragment = spy(
+ SaveLoginDialogFragment.newInstance(
+ tabId,
+ shareRequest.uid,
+ false,
+ 0,
+ LoginEntry(
+ origin = "https://www.mozilla.org",
+ username = "username",
+ password = "password",
+ ),
+ ),
+ )
+ feature.activePrompt = WeakReference(fragment)
+
+ store.dispatch(ContentAction.UpdateProgressAction(tabId, 0)).joinBlocking()
+ store.dispatch(ContentAction.UpdateProgressAction(tabId, 10)).joinBlocking()
+ store.dispatch(ContentAction.UpdateProgressAction(tabId, 100)).joinBlocking()
+
+ verify(fragment, times(0)).dismiss()
+ }
+
+ @Test
+ fun `confirm dialogs will not be automatically dismissed`() {
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ shareDelegate = mock(),
+ ) { },
+ )
+ feature.start()
+
+ val promptRequest = PromptRequest.Confirm(
+ "title",
+ "message",
+ false,
+ "positive",
+ "negative",
+ "neutral",
+ { },
+ { },
+ { },
+ { },
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
+
+ val prompt = feature.activePrompt?.get()
+ assertNotNull(prompt)
+ assertFalse(prompt!!.shouldDismissOnLoad)
+ }
+
+ @Test
+ fun `A Repost PromptRequest prompt will be shown as a ConfirmDialogFragment`() {
+ val feature = PromptFeature(
+ // Proper activity here to allow for the feature to properly execute "container.context.getString"
+ activity = Robolectric.buildActivity(Activity::class.java).setup().get(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ shareDelegate = mock(),
+ isSaveLoginEnabled = { true },
+ loginValidationDelegate = mock(),
+ ) { }
+ val repostPromptRequest: PromptRequest.Repost = mock()
+ doReturn("uid").`when`(repostPromptRequest).uid
+
+ feature.handleDialogsRequest(repostPromptRequest, mock())
+
+ val dialog: ConfirmDialogFragment = feature.activePrompt!!.get() as ConfirmDialogFragment
+ assertEquals(testContext.getString(R.string.mozac_feature_prompt_repost_title), dialog.title)
+ assertEquals(testContext.getString(R.string.mozac_feature_prompt_repost_message), dialog.message)
+ assertEquals(
+ testContext.getString(R.string.mozac_feature_prompt_repost_positive_button_text),
+ dialog.positiveButtonText,
+ )
+ assertEquals(
+ testContext.getString(R.string.mozac_feature_prompt_repost_negative_button_text),
+ dialog.negativeButtonText,
+ )
+ }
+
+ @Test
+ fun `Positive button on a Repost dialog will call onAccept and consume the dialog`() {
+ val feature = PromptFeature(
+ activity = Robolectric.buildActivity(Activity::class.java).setup().get(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ exitFullscreenUsecase = mock(),
+ ) { }
+ feature.start()
+
+ var acceptCalled = false
+ val repostRequest = PromptRequest.Repost(
+ { acceptCalled = true },
+ { },
+ )
+ store
+ .dispatch(ContentAction.UpdatePromptRequestAction(tabId, repostRequest))
+ .joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(repostRequest, tab()!!.content.promptRequests[0])
+ feature.onConfirm(tabId, repostRequest.uid, null)
+
+ store.waitUntilIdle()
+ assertTrue(acceptCalled)
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `Negative button on a Repost dialog will call onDismiss and consume the dialog`() {
+ val feature = PromptFeature(
+ activity = Robolectric.buildActivity(Activity::class.java).setup().get(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ exitFullscreenUsecase = mock(),
+ ) { }
+ feature.start()
+
+ var dismissCalled = false
+ val repostRequest = PromptRequest.Repost(
+ { },
+ { dismissCalled = true },
+ )
+ store
+ .dispatch(ContentAction.UpdatePromptRequestAction(tabId, repostRequest))
+ .joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(repostRequest, tab()!!.content.promptRequests[0])
+ feature.onCancel(tabId, repostRequest.uid)
+
+ store.waitUntilIdle()
+ assertTrue(dismissCalled)
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `WHEN onConfirm is called on a SaveCreditCard dialog THEN a confirm request will consume the dialog`() {
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ fileUploadsDirCleaner = mock(),
+ isCreditCardAutofillEnabled = { true },
+ creditCardValidationDelegate = mock(),
+ ) { }
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+
+ val request = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {},
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, request)).joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+
+ feature.onConfirm(
+ sessionId = tabId,
+ promptRequestUID = request.uid,
+ value = creditCardEntry,
+ )
+
+ store.waitUntilIdle()
+
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `WHEN a credit card is confirmed to save THEN confirm the prompt request with the selected credit card`() {
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+ var onDismissCalled = false
+ var onConfirmCalled = false
+ var confirmedCreditCard: CreditCardEntry? = null
+
+ val request = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {
+ confirmedCreditCard = it
+ onConfirmCalled = true
+ },
+ onDismiss = {
+ onDismissCalled = true
+ },
+ )
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, request)).joinBlocking()
+
+ request.onConfirm(creditCardEntry)
+
+ store.waitUntilIdle()
+
+ assertEquals(creditCardEntry, confirmedCreditCard)
+ assertTrue(onConfirmCalled)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, request)).joinBlocking()
+
+ request.onDismiss()
+
+ store.waitUntilIdle()
+
+ assertTrue(onDismissCalled)
+ }
+
+ @Test
+ fun `WHEN the save credit card dialog fragment is created THEN the credit card entry is passed into the instance`() {
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ isCreditCardAutofillEnabled = { true },
+ creditCardValidationDelegate = mock(),
+ ) { }
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+ val request = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {},
+ )
+
+ val contentState: ContentState = mock()
+ val session: TabSessionState = mock()
+ val sessionId = "sessionId"
+
+ `when`(session.content).thenReturn(contentState)
+ `when`(session.id).thenReturn(sessionId)
+
+ feature.handleDialogsRequest(
+ promptRequest = request,
+ session = session,
+ )
+
+ assertTrue(feature.activePrompt!!.get() is CreditCardSaveDialogFragment)
+
+ val dialogFragment = feature.activePrompt!!.get() as CreditCardSaveDialogFragment
+
+ assertEquals(sessionId, dialogFragment.sessionId)
+ assertEquals(creditCardEntry, dialogFragment.creditCard)
+ }
+
+ @Test
+ fun `GIVEN SaveCreditCard prompt is shown WHEN prompt is removed from state THEN dismiss SaveCreditCard prompt`() {
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+ val promptRequest = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {},
+ )
+ val dialogFragment: CreditCardSaveDialogFragment = mock()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest))
+ .joinBlocking()
+ store.waitUntilIdle()
+
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ fileUploadsDirCleaner = mock(),
+ isCreditCardAutofillEnabled = { true },
+ creditCardValidationDelegate = mock(),
+ ) { }
+
+ feature.start()
+ feature.activePrompt = WeakReference(dialogFragment)
+ feature.activePromptRequest = promptRequest
+
+ store.dispatch(ContentAction.ConsumePromptRequestAction(tabId, promptRequest))
+ .joinBlocking()
+
+ verify(dialogFragment).dismissAllowingStateLoss()
+ }
+
+ @Test
+ fun `WHEN SaveCreditCard is handled THEN the credit card save prompt shown fact is emitted`() {
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ fileUploadsDirCleaner = mock(),
+ isCreditCardAutofillEnabled = { true },
+ creditCardValidationDelegate = mock(),
+ ) { }
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "CC",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+ val request = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {},
+ )
+ val session: TabSessionState = mock()
+ val sessionId = "sessionId"
+ `when`(session.id).thenReturn(sessionId)
+
+ CollectionProcessor.withFactCollection { facts ->
+ feature.handleDialogsRequest(
+ promptRequest = request,
+ session = session,
+ )
+
+ val fact = facts.find { it.item == CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SAVE_PROMPT_SHOWN }!!
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.DISPLAY, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SAVE_PROMPT_SHOWN,
+ fact.item,
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN choice promptRequest is dismissed by the engine THEN the active prompt will be cleared`() {
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fileUploadsDirCleaner = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ shareDelegate = mock(),
+ ) { },
+ )
+ feature.start()
+
+ val singleChoicePrompt = SingleChoice(
+ choices = arrayOf(),
+ onConfirm = {},
+ onDismiss = {},
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, singleChoicePrompt))
+ .joinBlocking()
+ val fragment = mock<ChoiceDialogFragment>()
+ whenever(fragment.isAdded).thenReturn(false)
+
+ store.dispatch(ContentAction.ConsumePromptRequestAction(tabId, singleChoicePrompt))
+ .joinBlocking()
+ assertEquals(null, feature.activePrompt?.get())
+ assertTrue(feature.activePromptsToDismiss.isEmpty())
+ }
+
+ @Test
+ fun `WHEN promptRequest is updated THEN the replaced active prompt will be dismissed`() {
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ fileUploadsDirCleaner = mock(),
+ store = store,
+ tabsUseCases = mock(),
+ fragmentManager = fragmentManager,
+ exitFullscreenUsecase = mock(),
+ shareDelegate = mock(),
+ ) { },
+ )
+ feature.start()
+
+ val previousPrompt = SingleChoice(
+ choices = arrayOf(),
+ onConfirm = {},
+ onDismiss = {},
+ )
+ val updatedPrompt = SingleChoice(
+ choices = arrayOf(),
+ onConfirm = {},
+ onDismiss = {},
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, previousPrompt)).joinBlocking()
+
+ val fragment = mock<ChoiceDialogFragment>()
+ whenever(fragment.shouldDismissOnLoad).thenReturn(true)
+ whenever(fragment.isAdded).thenReturn(true)
+ feature.activePrompt = WeakReference(fragment)
+
+ store.dispatch(ContentAction.ReplacePromptRequestAction(tabId, previousPrompt.uid, updatedPrompt)).joinBlocking()
+ verify(fragment).dismiss()
+ }
+
+ private fun mockFragmentManager(): FragmentManager {
+ val fragmentManager: FragmentManager = mock()
+ val transaction: FragmentTransaction = mock()
+ doReturn(transaction).`when`(fragmentManager).beginTransaction()
+ doReturn(transaction).`when`(transaction).remove(any())
+ return fragmentManager
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptMiddlewareTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptMiddlewareTest.kt
new file mode 100644
index 0000000000..6e2e9c84da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptMiddlewareTest.kt
@@ -0,0 +1,111 @@
+/* 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.prompts
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class PromptMiddlewareTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var store: BrowserStore
+
+ private val tabId = "test-tab"
+ private fun tab(): TabSessionState? {
+ return store.state.tabs.find { it.id == tabId }
+ }
+
+ @Before
+ @ExperimentalCoroutinesApi
+ fun setUp() {
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = tabId),
+ ),
+ selectedTabId = tabId,
+ ),
+ middleware = listOf(PromptMiddleware()),
+ )
+ }
+
+ @Test
+ fun `Process only one popup prompt request at a time`() {
+ val onDeny = spy { }
+ val popupPrompt1 = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt1)).joinBlocking()
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(popupPrompt1, tab()!!.content.promptRequests[0])
+ verify(onDeny, never()).invoke()
+
+ val popupPrompt2 = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt2)).joinBlocking()
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(popupPrompt1, tab()!!.content.promptRequests[0])
+ verify(onDeny).invoke()
+ }
+
+ @Test
+ fun `Process popup followed by other prompt request`() {
+ val onDeny = spy { }
+ val popupPrompt = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt)).joinBlocking()
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(popupPrompt, tab()!!.content.promptRequests[0])
+ verify(onDeny, never()).invoke()
+
+ val alert = PromptRequest.Alert("title", "message", false, { }, { })
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, alert)).joinBlocking()
+ assertEquals(2, tab()!!.content.promptRequests.size)
+ assertEquals(popupPrompt, tab()!!.content.promptRequests[0])
+ assertEquals(alert, tab()!!.content.promptRequests[1])
+ }
+
+ @Test
+ fun `Process popup after other prompt request`() {
+ val alert = PromptRequest.Alert("title", "message", false, { }, { })
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, alert)).joinBlocking()
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(alert, tab()!!.content.promptRequests[0])
+
+ val onDeny = spy { }
+ val popupPrompt = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt)).joinBlocking()
+ assertEquals(2, tab()!!.content.promptRequests.size)
+ assertEquals(alert, tab()!!.content.promptRequests[0])
+ assertEquals(popupPrompt, tab()!!.content.promptRequests[1])
+ verify(onDeny, never()).invoke()
+ }
+
+ @Test
+ fun `Process other prompt requests`() {
+ val alert = PromptRequest.Alert("title", "message", false, { }, { })
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, alert)).joinBlocking()
+ assertEquals(1, tab()!!.content.promptRequests.size)
+ assertEquals(alert, tab()!!.content.promptRequests[0])
+
+ val beforeUnloadPrompt = PromptRequest.BeforeUnload("title", onLeave = { }, onStay = { })
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, beforeUnloadPrompt)).joinBlocking()
+ assertEquals(2, tab()!!.content.promptRequests.size)
+ assertEquals(alert, tab()!!.content.promptRequests[0])
+ assertEquals(beforeUnloadPrompt, tab()!!.content.promptRequests[1])
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt
new file mode 100644
index 0000000000..5423741dab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt
@@ -0,0 +1,82 @@
+/* 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.prompts.address
+
+import android.view.LayoutInflater
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AddressAdapterTest {
+
+ private val address = Address(
+ guid = "1",
+ name = "Jane Marie Doe",
+ organization = "Mozilla",
+ streetAddress = "1230 Main st",
+ addressLevel3 = "Location3",
+ addressLevel2 = "Location2",
+ addressLevel1 = "Location1",
+ postalCode = "90237",
+ country = "USA",
+ tel = "00",
+ email = "email",
+ )
+
+ @Test
+ fun testAddressDiffCallback() {
+ val address2 = address.copy()
+
+ assertTrue(
+ AddressDiffCallback.areItemsTheSame(address, address2),
+ )
+ assertTrue(
+ AddressDiffCallback.areContentsTheSame(address, address2),
+ )
+
+ val address3 = address.copy(guid = "2")
+
+ assertFalse(
+ AddressDiffCallback.areItemsTheSame(address, address3),
+ )
+ assertFalse(
+ AddressDiffCallback.areItemsTheSame(address, address3),
+ )
+ }
+
+ @Test
+ fun `WHEN an address is bound to the adapter THEN set the address display name`() {
+ val view =
+ LayoutInflater.from(testContext)
+ .inflate(R.layout.mozac_feature_prompts_address_list_item, null)
+ val addressName: TextView = view.findViewById(R.id.address_name)
+
+ AddressViewHolder(view, onAddressSelected = {}).bind(address)
+
+ assertEquals(address.addressLabel, addressName.text)
+ }
+
+ @Test
+ fun `WHEN an address item is clicked THEN call the onAddressSelected callback`() {
+ var addressSelected = false
+ val view =
+ LayoutInflater.from(testContext)
+ .inflate(R.layout.mozac_feature_prompts_address_list_item, null)
+ val onAddressSelect: (Address) -> Unit = { addressSelected = true }
+
+ AddressViewHolder(view, onAddressSelect).bind(address)
+ view.performClick()
+
+ assertTrue(addressSelected)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt
new file mode 100644
index 0000000000..7469f498c4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt
@@ -0,0 +1,145 @@
+/* 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.prompts.address
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.facts.AddressAutofillDialogFacts
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class AddressPickerTest {
+
+ private lateinit var store: BrowserStore
+ private lateinit var state: BrowserState
+ private lateinit var addressPicker: AddressPicker
+ private lateinit var addressSelectBar: AddressSelectBar
+
+ private val address = Address(
+ guid = "1",
+ name = "Jane Marie Doe",
+ organization = "Mozilla",
+ streetAddress = "1230 Main st",
+ addressLevel3 = "Location3",
+ addressLevel2 = "Location2",
+ addressLevel1 = "Location1",
+ postalCode = "90237",
+ country = "USA",
+ tel = "00",
+ email = "email",
+ )
+
+ private var onDismissCalled = false
+ private var confirmedAddress: Address? = null
+
+ private val promptRequest = PromptRequest.SelectAddress(
+ addresses = listOf(address),
+ onDismiss = { onDismissCalled = true },
+ onConfirm = { confirmedAddress = it },
+ )
+
+ @Before
+ fun setup() {
+ store = mock()
+ state = mock()
+ addressSelectBar = mock()
+ addressPicker = AddressPicker(
+ store = store,
+ addressSelectBar = addressSelectBar,
+ )
+
+ whenever(store.state).thenReturn(state)
+ }
+
+ @Test
+ fun `WHEN onOptionSelect is called with an address THEN selectAddressCallback is invoked and prompt is hidden`() {
+ val content: ContentState = mock()
+ whenever(content.promptRequests).thenReturn(listOf(promptRequest))
+ val selectedTab = TabSessionState("browser-tab", content, mock(), mock())
+ whenever(state.selectedTabId).thenReturn(selectedTab.id)
+ whenever(state.tabs).thenReturn(listOf(selectedTab))
+
+ addressPicker.onOptionSelect(address)
+
+ verify(addressSelectBar).hidePrompt()
+ assertEquals(address, confirmedAddress)
+ }
+
+ @Test
+ fun `GIVEN a prompt request WHEN handleSelectAddressRequest is called THEN the prompt is shown with the provided addresses`() {
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ assertEquals(0, facts.size)
+
+ addressPicker.handleSelectAddressRequest(promptRequest)
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_SHOWN, item)
+ }
+
+ verify(addressSelectBar).showPrompt(promptRequest.addresses)
+ }
+
+ @Test
+ fun `GIVEN a custom tab and a prompt request WHEN handleSelectAddressRequest is called THEN the prompt is shown with the provided addresses`() {
+ val customTabContent: ContentState = mock()
+ val customTab = CustomTabSessionState(id = "custom-tab", content = customTabContent, trackingProtection = mock(), config = mock())
+ whenever(customTabContent.promptRequests).thenReturn(listOf(promptRequest))
+ whenever(state.customTabs).thenReturn(listOf(customTab))
+
+ addressPicker.handleSelectAddressRequest(promptRequest)
+
+ verify(addressSelectBar).showPrompt(promptRequest.addresses)
+ }
+
+ @Test
+ fun `WHEN onManageOptions is called THEN onManageAddresses is invoked and prompt is hidden`() {
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ assertEquals(0, facts.size)
+
+ addressPicker.onManageOptions()
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_DISMISSED, item)
+ }
+
+ verify(addressSelectBar).hidePrompt()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt
new file mode 100644
index 0000000000..fbffde17c8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt
@@ -0,0 +1,141 @@
+/* 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.prompts.address
+
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.facts.AddressAutofillDialogFacts
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+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.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AddressSelectBarTest {
+
+ private lateinit var addressSelectBar: AddressSelectBar
+
+ private val address = Address(
+ guid = "1",
+ name = "Jane Marie Doe",
+ organization = "Mozilla",
+ streetAddress = "1230 Main st",
+ addressLevel3 = "Location3",
+ addressLevel2 = "Location2",
+ addressLevel1 = "Location1",
+ postalCode = "90237",
+ country = "USA",
+ tel = "00",
+ email = "email",
+ )
+
+ @Before
+ fun setup() {
+ addressSelectBar = AddressSelectBar(appCompatContext)
+ }
+
+ @Test
+ fun `WHEN showPrompt is called THEN the select bar is shown`() {
+ val addresses = listOf(address)
+
+ addressSelectBar.showPrompt(addresses)
+
+ assertTrue(addressSelectBar.isVisible)
+ }
+
+ @Test
+ fun `WHEN hidePrompt is called THEN the select bar is hidden`() {
+ assertTrue(addressSelectBar.isVisible)
+
+ addressSelectBar.hidePrompt()
+
+ assertFalse(addressSelectBar.isVisible)
+ }
+
+ @Test
+ fun `WHEN the selectBar header is clicked two times THEN the list of addresses is shown, then hidden`() {
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ addressSelectBar.showPrompt(listOf(address))
+
+ assertEquals(0, facts.size)
+
+ addressSelectBar.findViewById<AppCompatTextView>(R.id.select_address_header).performClick()
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_EXPANDED, item)
+ }
+
+ assertTrue(addressSelectBar.findViewById<RecyclerView>(R.id.address_list).isVisible)
+
+ addressSelectBar.findViewById<AppCompatTextView>(R.id.select_address_header).performClick()
+
+ assertFalse(addressSelectBar.findViewById<RecyclerView>(R.id.address_list).isVisible)
+ }
+
+ @Test
+ fun `GIVEN a listener WHEN an address is clicked THEN onOptionSelected is called`() {
+ val listener: SelectablePromptView.Listener<Address> = mock()
+
+ assertNull(addressSelectBar.listener)
+
+ addressSelectBar.listener = listener
+
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ addressSelectBar.showPrompt(listOf(address))
+ val adapter = addressSelectBar.findViewById<RecyclerView>(R.id.address_list).adapter as AddressAdapter
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), 0)
+ adapter.bindViewHolder(holder, 0)
+
+ assertEquals(0, facts.size)
+
+ holder.itemView.performClick()
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_SUCCESS, item)
+ }
+
+ verify(listener).onOptionSelect(address)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt
new file mode 100644
index 0000000000..27d49e7026
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt
@@ -0,0 +1,71 @@
+/* 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.prompts.creditcard
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.utils.CreditCardNetworkType
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CreditCardItemViewHolderTest {
+
+ private lateinit var view: View
+ private lateinit var cardLogoView: ImageView
+ private lateinit var cardNumberView: TextView
+ private lateinit var expirationDateView: TextView
+ private lateinit var onCreditCardSelected: (CreditCardEntry) -> Unit
+
+ private val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111111",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = CreditCardNetworkType.VISA.cardName,
+ )
+
+ @Before
+ fun setup() {
+ view = LayoutInflater.from(testContext).inflate(CreditCardItemViewHolder.LAYOUT_ID, null)
+ cardLogoView = view.findViewById(R.id.credit_card_logo)
+ cardNumberView = view.findViewById(R.id.credit_card_number)
+ expirationDateView = view.findViewById(R.id.credit_card_expiration_date)
+ onCreditCardSelected = mock()
+ }
+
+ @Test
+ fun `GIVEN a credit card item WHEN bind is called THEN set the card number, logo and expiry date`() {
+ CreditCardItemViewHolder(view, onCreditCardSelected).bind(creditCard)
+
+ assertNotNull(cardLogoView.drawable)
+ assertEquals(creditCard.obfuscatedCardNumber, cardNumberView.text)
+ assertEquals("0${creditCard.expiryMonth}/${creditCard.expiryYear}", expirationDateView.text)
+ }
+
+ @Test
+ fun `GIVEN a credit card item WHEN a credit item is clicked THEN onCreditCardSelected is called with the given credit card item`() {
+ var onCreditCardSelectedCalled: CreditCardEntry? = null
+ val onCreditCardSelected = { creditCard: CreditCardEntry ->
+ onCreditCardSelectedCalled = creditCard
+ }
+ CreditCardItemViewHolder(view, onCreditCardSelected).bind(creditCard)
+
+ view.performClick()
+
+ assertEquals(creditCard, onCreditCardSelectedCalled)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt
new file mode 100644
index 0000000000..1df4372bcf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt
@@ -0,0 +1,190 @@
+/* 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.prompts.creditcard
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.CreditCardEntry
+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.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class CreditCardPickerTest {
+
+ private lateinit var store: BrowserStore
+ private lateinit var state: BrowserState
+ private lateinit var creditCardPicker: CreditCardPicker
+ private lateinit var creditCardSelectBar: CreditCardSelectBar
+
+ private val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+ var onDismissCalled = false
+ var confirmedCreditCard: CreditCardEntry? = null
+ private val promptRequest = PromptRequest.SelectCreditCard(
+ creditCards = listOf(creditCard),
+ onDismiss = { onDismissCalled = true },
+ onConfirm = { confirmedCreditCard = it },
+ )
+
+ var manageCreditCardsCalled = false
+ var selectCreditCardCalled = false
+ private val manageCreditCardsCallback: () -> Unit = { manageCreditCardsCalled = true }
+ private val selectCreditCardCallback: () -> Unit = { selectCreditCardCalled = true }
+
+ @Before
+ fun setup() {
+ store = mock()
+ state = mock()
+ creditCardSelectBar = mock()
+ creditCardPicker = CreditCardPicker(
+ store = store,
+ creditCardSelectBar = creditCardSelectBar,
+ manageCreditCardsCallback = manageCreditCardsCallback,
+ selectCreditCardCallback = selectCreditCardCallback,
+ )
+
+ whenever(store.state).thenReturn(state)
+ }
+
+ @Test
+ fun `WHEN onOptionSelect is called with a credit card THEN selectCreditCardCallback is invoked and prompt is hidden`() {
+ assertNull(creditCardPicker.selectedCreditCard)
+
+ setupSessionState(promptRequest)
+
+ creditCardPicker.onOptionSelect(creditCard)
+
+ verify(creditCardSelectBar).hidePrompt()
+
+ assertTrue(selectCreditCardCalled)
+ assertEquals(creditCard, creditCardPicker.selectedCreditCard)
+ }
+
+ @Test
+ fun `WHEN onManageOptions is called THEN manageCreditCardsCallback is invoked and prompt is hidden`() {
+ setupSessionState(promptRequest)
+
+ creditCardPicker.onManageOptions()
+
+ verify(creditCardSelectBar).hidePrompt()
+
+ assertTrue(manageCreditCardsCalled)
+ assertTrue(onDismissCalled)
+ }
+
+ @Test
+ fun `GIVEN a prompt request WHEN handleSelectCreditCardRequest is called THEN the prompt is shown with the provided request credit cards`() {
+ creditCardPicker.handleSelectCreditCardRequest(promptRequest)
+
+ verify(creditCardSelectBar).showPrompt(promptRequest.creditCards)
+ }
+
+ @Test
+ fun `GIVEN a custom tab and a prompt request WHEN handleSelectCreditCardRequest is called THEN the prompt is shown with the provided request credit cards`() {
+ val customTabContent: ContentState = mock()
+ val customTab = CustomTabSessionState(id = "custom-tab", content = customTabContent, trackingProtection = mock(), config = mock())
+
+ whenever(customTabContent.promptRequests).thenReturn(listOf(promptRequest))
+ whenever(state.customTabs).thenReturn(listOf(customTab))
+
+ creditCardPicker.handleSelectCreditCardRequest(promptRequest)
+
+ verify(creditCardSelectBar).showPrompt(promptRequest.creditCards)
+ }
+
+ @Test
+ fun `GIVEN a selected credit card WHEN onAuthSuccess is called THEN the confirmed credit card is received`() {
+ assertNull(creditCardPicker.selectedCreditCard)
+
+ setupSessionState(promptRequest)
+
+ creditCardPicker.onOptionSelect(creditCard)
+ creditCardPicker.onAuthSuccess()
+
+ assertEquals(creditCard, confirmedCreditCard)
+ assertNull(creditCardPicker.selectedCreditCard)
+ }
+
+ @Test
+ fun `GIVEN a selected credit card WHEN onAuthFailure is called THEN the prompt request is dismissed`() {
+ assertNull(creditCardPicker.selectedCreditCard)
+
+ setupSessionState(promptRequest)
+
+ creditCardPicker.onOptionSelect(creditCard)
+ creditCardPicker.onAuthFailure()
+
+ assertNull(creditCardPicker.selectedCreditCard)
+ assertTrue(onDismissCalled)
+ }
+
+ @Test
+ fun `WHEN dismissSelectCreditCardRequest is invoked without a parameter THEN the active prompt request is dismissed and removed from the session`() {
+ val session = setupSessionState(promptRequest)
+ creditCardPicker = CreditCardPicker(
+ store = store,
+ creditCardSelectBar = creditCardSelectBar,
+ manageCreditCardsCallback = manageCreditCardsCallback,
+ selectCreditCardCallback = selectCreditCardCallback,
+ sessionId = session.id,
+ )
+
+ verify(store, never()).dispatch(any())
+ creditCardPicker.dismissSelectCreditCardRequest()
+
+ assertTrue(onDismissCalled)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(session.id, promptRequest))
+ }
+
+ @Test
+ fun `WHEN dismissSelectCreditCardRequest is invoked with the active prompt request as parameter THEN the request is dismissed and removed from the session`() {
+ val session = setupSessionState(promptRequest)
+ creditCardPicker = CreditCardPicker(
+ store = store,
+ creditCardSelectBar = creditCardSelectBar,
+ manageCreditCardsCallback = manageCreditCardsCallback,
+ selectCreditCardCallback = selectCreditCardCallback,
+ sessionId = session.id,
+ )
+
+ verify(store, never()).dispatch(any())
+ creditCardPicker.dismissSelectCreditCardRequest(promptRequest)
+
+ assertTrue(onDismissCalled)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(session.id, promptRequest))
+ }
+
+ private fun setupSessionState(request: PromptRequest? = null): TabSessionState {
+ val promptRequest: PromptRequest = request ?: mock()
+ val content: ContentState = mock()
+
+ whenever(content.promptRequests).thenReturn(listOf(promptRequest))
+
+ val selected = TabSessionState("browser-tab", content, mock(), mock())
+
+ whenever(state.selectedTabId).thenReturn(selected.id)
+ whenever(state.tabs).thenReturn(listOf(selected))
+
+ return selected
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragmentTest.kt
new file mode 100644
index 0000000000..9d40249e2a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragmentTest.kt
@@ -0,0 +1,315 @@
+/* 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.prompts.creditcard
+
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardValidationDelegate
+import mozilla.components.feature.prompts.PromptFeature
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class CreditCardSaveDialogFragmentTest {
+
+ private val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+ private val sessionId = "sessionId"
+ private val promptRequestUID = "uid"
+
+ @Test
+ fun `WHEN the credit card save dialog fragment view is created THEN the credit card entry is displayed`() {
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard,
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_header
+ },
+ )
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_message
+ },
+ )
+ addView(Button(appCompatContext).apply { id = R.id.save_confirm })
+ addView(Button(appCompatContext).apply { id = R.id.save_cancel })
+ addView(ImageView(appCompatContext).apply { id = R.id.credit_card_logo })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_number })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_expiration_date })
+ }
+ }.`when`(fragment).onCreateView(any(), any(), any())
+
+ val view = fragment.onCreateView(mock(), mock(), mock())
+ fragment.onViewCreated(view, mock())
+
+ val cardNumberTextView = view.findViewById<TextView>(R.id.credit_card_number)
+ val iconImageView = view.findViewById<ImageView>(R.id.credit_card_logo)
+ val expiryDateView = view.findViewById<TextView>(R.id.credit_card_expiration_date)
+
+ assertEquals(creditCard.obfuscatedCardNumber, cardNumberTextView.text)
+ assertEquals(creditCard.expiryDate, expiryDateView.text)
+ assertNotNull(iconImageView.drawable)
+ }
+
+ @Test
+ fun `WHEN setViewText is called with new header and button text THEN the header and button text are updated in the view`() {
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard,
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_header
+ },
+ )
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_message
+ },
+ )
+ addView(Button(appCompatContext).apply { id = R.id.save_confirm })
+ addView(Button(appCompatContext).apply { id = R.id.save_cancel })
+ addView(ImageView(appCompatContext).apply { id = R.id.credit_card_logo })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_number })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_expiration_date })
+ }
+ }.`when`(fragment).onCreateView(any(), any(), any())
+
+ val view = fragment.onCreateView(mock(), mock(), mock())
+ fragment.onViewCreated(view, mock())
+
+ val headerTextView = view.findViewById<AppCompatTextView>(R.id.save_credit_card_header)
+ val messageTextView = view.findViewById<AppCompatTextView>(R.id.save_credit_card_message)
+ val cancelButtonView = view.findViewById<Button>(R.id.save_cancel)
+ val confirmButtonView = view.findViewById<Button>(R.id.save_confirm)
+
+ val header = "header"
+ val cancelButtonText = "cancelButtonText"
+ val confirmButtonText = "confirmButtonText"
+
+ fragment.setViewText(
+ view = view,
+ header = header,
+ cancelButtonText = cancelButtonText,
+ confirmButtonText = confirmButtonText,
+ showMessageBody = false,
+ )
+
+ assertEquals(header, headerTextView.text)
+ assertEquals(cancelButtonText, cancelButtonView.text)
+ assertEquals(confirmButtonText, confirmButtonView.text)
+ assertFalse(messageTextView.isVisible)
+
+ fragment.setViewText(
+ view = view,
+ header = header,
+ cancelButtonText = cancelButtonText,
+ confirmButtonText = confirmButtonText,
+ showMessageBody = true,
+ )
+
+ assertTrue(messageTextView.isVisible)
+ }
+
+ @Test
+ fun `WHEN the confirm button is clicked THEN the prompt feature is notified`() {
+ val mockFeature: PromptFeature = mock()
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard,
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_header
+ },
+ )
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_message
+ },
+ )
+ addView(Button(appCompatContext).apply { id = R.id.save_confirm })
+ addView(Button(appCompatContext).apply { id = R.id.save_cancel })
+ addView(ImageView(appCompatContext).apply { id = R.id.credit_card_logo })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_number })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_expiration_date })
+ }
+ }.`when`(fragment).onCreateView(any(), any(), any())
+ doNothing().`when`(fragment).dismiss()
+
+ val view = fragment.onCreateView(mock(), mock(), mock())
+ fragment.onViewCreated(view, mock())
+
+ val buttonView = view.findViewById<Button>(R.id.save_confirm)
+
+ buttonView.performClick()
+
+ verify(mockFeature).onConfirm(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ value = creditCard,
+ )
+ }
+
+ @Test
+ fun `WHEN the cancel button is clicked THEN the prompt feature is notified`() {
+ val mockFeature: PromptFeature = mock()
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard,
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_header
+ },
+ )
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_message
+ },
+ )
+ addView(Button(appCompatContext).apply { id = R.id.save_confirm })
+ addView(Button(appCompatContext).apply { id = R.id.save_cancel })
+ addView(ImageView(appCompatContext).apply { id = R.id.credit_card_logo })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_number })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_expiration_date })
+ }
+ }.`when`(fragment).onCreateView(any(), any(), any())
+ doNothing().`when`(fragment).dismiss()
+
+ val view = fragment.onCreateView(mock(), mock(), mock())
+ fragment.onViewCreated(view, mock())
+
+ val buttonView = view.findViewById<Button>(R.id.save_cancel)
+
+ buttonView.performClick()
+
+ verify(mockFeature).onCancel(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ )
+ }
+
+ @Test
+ fun `WHEN the confirm save button is clicked THEN the appropriate fact is emitted`() {
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard,
+ ),
+ )
+
+ fragment.confirmResult = CreditCardValidationDelegate.Result.CanBeCreated
+
+ CollectionProcessor.withFactCollection { facts ->
+ fragment.emitSaveUpdateFact()
+
+ assertEquals(1, facts.size)
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.CONFIRM, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_CREATED,
+ fact.item,
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN the confirm update button is clicked THEN the appropriate fact is emitted`() {
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard,
+ ),
+ )
+
+ fragment.confirmResult = CreditCardValidationDelegate.Result.CanBeUpdated(mock())
+
+ CollectionProcessor.withFactCollection { facts ->
+ fragment.emitSaveUpdateFact()
+
+ assertEquals(1, facts.size)
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.CONFIRM, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_UPDATED,
+ fact.item,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt
new file mode 100644
index 0000000000..f3b8bcfb8d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt
@@ -0,0 +1,146 @@
+/* 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.prompts.creditcard
+
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+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.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class CreditCardSelectBarTest {
+
+ private lateinit var creditCardSelectBar: CreditCardSelectBar
+
+ private val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "",
+ )
+
+ @Before
+ fun setup() {
+ creditCardSelectBar = CreditCardSelectBar(appCompatContext)
+ }
+
+ @Test
+ fun `GIVEN a list of credit cards WHEN prompt is shown THEN credit cards are shown`() {
+ val creditCards = listOf(creditCard)
+
+ creditCardSelectBar.showPrompt(creditCards)
+
+ assertTrue(creditCardSelectBar.isVisible)
+ }
+
+ @Test
+ fun `WHEN the prompt is hidden THEN view is hidden`() {
+ creditCardSelectBar.hidePrompt()
+
+ assertFalse(creditCardSelectBar.isVisible)
+ }
+
+ @Test
+ fun `GIVEN a listener WHEN manage credit cards button is clicked THEN onManageOptions is called`() {
+ val listener: SelectablePromptView.Listener<CreditCardEntry> = mock()
+
+ assertNull(creditCardSelectBar.listener)
+
+ creditCardSelectBar.listener = listener
+
+ creditCardSelectBar.showPrompt(listOf(creditCard))
+ creditCardSelectBar.findViewById<AppCompatTextView>(R.id.manage_credit_cards).performClick()
+
+ verify(listener).onManageOptions()
+ }
+
+ @Test
+ fun `GIVEN a listener WHEN a credit card is selected THEN onOptionSelect is called`() = runTest {
+ val listener: SelectablePromptView.Listener<CreditCardEntry> = mock()
+ creditCardSelectBar.listener = listener
+
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ creditCardSelectBar.showPrompt(listOf(creditCard))
+
+ val adapter = creditCardSelectBar.findViewById<RecyclerView>(R.id.credit_cards_list).adapter as CreditCardsAdapter
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), 0)
+ adapter.bindViewHolder(holder, 0)
+
+ holder.itemView.performClick()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SUCCESS, item)
+ }
+ verify(listener).onOptionSelect(creditCard)
+ }
+
+ @Test
+ fun `WHEN the header is clicked THEN view is expanded or collapsed`() {
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ creditCardSelectBar.showPrompt(listOf(creditCard))
+
+ creditCardSelectBar.findViewById<AppCompatTextView>(R.id.select_credit_card_header).performClick()
+
+ assertTrue(creditCardSelectBar.findViewById<RecyclerView>(R.id.credit_cards_list).isVisible)
+ assertTrue(creditCardSelectBar.findViewById<AppCompatTextView>(R.id.manage_credit_cards).isVisible)
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_EXPANDED, item)
+ }
+
+ creditCardSelectBar.findViewById<AppCompatTextView>(R.id.select_credit_card_header).performClick()
+
+ assertFalse(creditCardSelectBar.findViewById<RecyclerView>(R.id.credit_cards_list).isVisible)
+ assertFalse(creditCardSelectBar.findViewById<AppCompatTextView>(R.id.manage_credit_cards).isVisible)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt
new file mode 100644
index 0000000000..6eb7318d87
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt
@@ -0,0 +1,49 @@
+/* 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.prompts.creditcard
+
+import mozilla.components.concept.storage.CreditCardEntry
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class CreditCardsAdapterTest {
+
+ @Test
+ fun testDiffCallback() {
+ val creditCard1 = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+ val creditCard2 = creditCard1.copy()
+
+ assertTrue(
+ CreditCardsAdapter.DiffCallback.areItemsTheSame(creditCard1, creditCard2),
+ )
+ assertTrue(
+ CreditCardsAdapter.DiffCallback.areContentsTheSame(creditCard1, creditCard2),
+ )
+
+ val creditCard3 = CreditCardEntry(
+ guid = "2",
+ name = "Pineapple Orange",
+ number = "4111111111115555",
+ expiryMonth = "1",
+ expiryYear = "2030",
+ cardType = "amex",
+ )
+
+ assertFalse(
+ CreditCardsAdapter.DiffCallback.areItemsTheSame(creditCard1, creditCard3),
+ )
+ assertFalse(
+ CreditCardsAdapter.DiffCallback.areContentsTheSame(creditCard1, creditCard3),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AlertDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AlertDialogFragmentTest.kt
new file mode 100644
index 0000000000..f9e897dc55
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AlertDialogFragmentTest.kt
@@ -0,0 +1,147 @@
+/* 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.prompts.dialog
+
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.os.Looper.getMainLooper
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.PromptFeature
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.R.id
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+class AlertDialogFragmentTest {
+
+ @Mock private lateinit var mockFeature: Prompter
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `build dialog`() {
+ val fragment = spy(
+ AlertDialogFragment.newInstance("sessionId", "uid", true, "title", "message", true),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val titleTextView = dialog.findViewById<TextView>(appcompatR.id.alertTitle)
+ val messageTextView = dialog.findViewById<TextView>(R.id.message)
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+
+ assertEquals(fragment.sessionId, "sessionId")
+ assertEquals(fragment.promptRequestUID, "uid")
+ assertEquals(fragment.message, "message")
+ assertEquals(fragment.hasShownManyDialogs, true)
+
+ assertEquals(titleTextView.text, "title")
+ assertEquals(fragment.title, "title")
+ assertEquals(messageTextView.text.toString(), "message")
+ assertTrue(checkBox.isVisible)
+ }
+
+ @Test
+ fun `Alert with hasShownManyDialogs equals false should not have a checkbox`() {
+ val fragment = spy(
+ AlertDialogFragment.newInstance("sessionId", "uid", false, "title", "message", false),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+
+ assertFalse(checkBox.isVisible)
+ }
+
+ @Test
+ fun `Clicking on positive button notifies the feature`() {
+ val mockFeature: PromptFeature = mock()
+
+ val fragment = spy(
+ AlertDialogFragment.newInstance("sessionId", "uid", true, "title", "message", false),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+
+ @Test
+ fun `After checking no more dialogs checkbox feature onNoMoreDialogsChecked must be called`() {
+ val fragment = spy(
+ AlertDialogFragment.newInstance("sessionId", "uid", false, "title", "message", true),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+
+ checkBox.isChecked = true
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", true)
+ }
+
+ @Test
+ fun `touching outside of the dialog must notify the feature onCancel`() {
+ val fragment = spy(
+ AlertDialogFragment.newInstance("sessionId", "uid", true, "title", "message", true),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ fragment.onCancel(mock())
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragmentTest.kt
new file mode 100644
index 0000000000..dc0b8f238c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragmentTest.kt
@@ -0,0 +1,196 @@
+/* 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.prompts.dialog
+
+import android.content.DialogInterface
+import android.os.Looper.getMainLooper
+import android.view.View.GONE
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.R.id
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+class AuthenticationDialogFragmentTest {
+
+ @Mock private lateinit var mockFeature: Prompter
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `build dialog`() {
+ val fragment = spy(
+ AuthenticationDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ true,
+ "title",
+ "message",
+ "username",
+ "password",
+ onlyShowPassword = false,
+ url = "https://mozilla.com",
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val titleTextView = dialog.findViewById<TextView>(appcompatR.id.alertTitle)
+ val messageTextView = dialog.findViewById<TextView>(android.R.id.message)
+ val usernameEditText = dialog.findViewById<AutofillEditText>(id.username)
+ val passwordEditText = dialog.findViewById<AutofillEditText>(id.password)
+
+ assertEquals(fragment.sessionId, "sessionId")
+ assertEquals(fragment.promptRequestUID, "uid")
+ assertEquals(fragment.title, "title")
+ assertEquals(fragment.message, "message")
+ assertEquals(fragment.username, "username")
+ assertEquals(fragment.password, "password")
+ assertEquals(fragment.onlyShowPassword, false)
+
+ assertEquals(titleTextView.text, "title")
+ assertEquals(messageTextView.text, "message")
+ assertEquals(usernameEditText.text.toString(), "username")
+ assertEquals(passwordEditText.text.toString(), "password")
+
+ usernameEditText.setText("new_username")
+ passwordEditText.setText("new_password")
+
+ assertEquals(usernameEditText.text.toString(), "new_username")
+ assertEquals(usernameEditText.url, "https://mozilla.com")
+ assertEquals(passwordEditText.text.toString(), "new_password")
+ assertEquals(passwordEditText.url, "https://mozilla.com")
+ }
+
+ @Test
+ fun `dialog with onlyShowPassword must not have a username field`() {
+ val fragment = spy(
+ AuthenticationDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ false,
+ "title",
+ "message",
+ "username",
+ "password",
+ true,
+ url = "https://mozilla.com",
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val usernameEditText = dialog.findViewById<AutofillEditText>(id.username)
+
+ assertEquals(usernameEditText.visibility, GONE)
+ }
+
+ @Test
+ fun `when the title is not provided the dialog must has a default value`() {
+ val fragment = spy(
+ AuthenticationDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ true,
+ "",
+ "message",
+ "username",
+ "password",
+ true,
+ url = "https://mozilla.com",
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val titleTextView = dialog.findViewById<TextView>(appcompatR.id.alertTitle)
+
+ val defaultTitle = appCompatContext.getString(AuthenticationDialogFragment.DEFAULT_TITLE)
+ assertEquals(titleTextView.text.toString(), defaultTitle)
+ }
+
+ @Test
+ fun `Clicking on positive button notifies the feature`() {
+ val fragment = spy(
+ AuthenticationDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ false,
+ "title",
+ "message",
+ "username",
+ "password",
+ false,
+ url = "https://mozilla.com",
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", "username" to "password")
+ }
+
+ @Test
+ fun `touching outside of the dialog must notify the feature onCancel`() {
+ val fragment = spy(
+ AuthenticationDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ true,
+ "title",
+ "message",
+ "username",
+ "password",
+ false,
+ url = "https://mozilla.com",
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ fragment.onCancel(mock())
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt
new file mode 100644
index 0000000000..54500d2c19
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt
@@ -0,0 +1,692 @@
+/* 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.prompts.dialog
+
+import android.content.DialogInterface.BUTTON_NEGATIVE
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.os.Looper.getMainLooper
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.prompt.Choice
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.Companion.TYPE_GROUP
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.Companion.TYPE_MENU
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.Companion.TYPE_MENU_SEPARATOR
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.Companion.TYPE_MULTIPLE
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.Companion.TYPE_SINGLE
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.GroupViewHolder
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.MenuViewHolder
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.MultipleViewHolder
+import mozilla.components.feature.prompts.dialog.ChoiceAdapter.SingleViewHolder
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.MENU_CHOICE_DIALOG_TYPE
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.MULTIPLE_CHOICE_DIALOG_TYPE
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.SINGLE_CHOICE_DIALOG_TYPE
+import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.newInstance
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class ChoiceDialogFragmentTest {
+
+ @Mock private lateinit var mockFeature: Prompter
+ private val item = Choice(id = "", label = "item1")
+ private val subItem = Choice(id = "", label = "sub-item1")
+ private val separator = Choice(id = "", label = "item1", isASeparator = true)
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `Build single choice dialog`() {
+ val fragment = spy(newInstance(arrayOf(), "sessionId", "uid", true, SINGLE_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ assertNotNull(dialog)
+ }
+
+ @Test
+ fun `cancelling the dialog cancels the feature`() {
+ val fragment = spy(newInstance(arrayOf(), "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ doNothing().`when`(fragment).dismiss()
+
+ assertNotNull(dialog)
+
+ dialog.show()
+
+ fragment.onCancel(dialog)
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+
+ @Test
+ fun `Build menu choice dialog`() {
+ val fragment = spy(newInstance(arrayOf(), "sessionId", "uid", true, MENU_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ assertNotNull(dialog)
+ }
+
+ @Test
+ fun `Build multiple choice dialog`() {
+ val fragment = spy(newInstance(arrayOf(), "sessionId", "uid", false, MULTIPLE_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ assertNotNull(dialog)
+ }
+
+ @Test(expected = Exception::class)
+ fun `Building a unknown dialog type will throw an exception`() {
+ val fragment = spy(newInstance(arrayOf(), "sessionId", "uid", true, -1))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ fragment.onCreateDialog(null)
+ }
+
+ @Test
+ fun `Will show a single choice item`() {
+ val choices = arrayOf(item)
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val adapter = getAdapterFrom(fragment)
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_SINGLE) as SingleViewHolder
+ val labelView = holder.labelView
+ adapter.bindViewHolder(holder, 0)
+
+ assertEquals(1, adapter.itemCount)
+ assertEquals("item1", labelView.text)
+ }
+
+ @Test
+ fun `Will show a menu choice item`() {
+ val choices = arrayOf(item)
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", true, MENU_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val adapter = getAdapterFrom(fragment)
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_MENU) as MenuViewHolder
+ val labelView = holder.labelView
+ adapter.bindViewHolder(holder, 0)
+
+ assertEquals(1, adapter.itemCount)
+ assertEquals("item1", labelView.text)
+ }
+
+ @Test
+ fun `Will show a menu choice separator item`() {
+ val choices = arrayOf(separator)
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, MENU_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val adapter = getAdapterFrom(fragment)
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_MENU_SEPARATOR)
+ adapter.bindViewHolder(holder, 0)
+
+ assertEquals(1, adapter.itemCount)
+ assertNotNull(holder.itemView)
+ }
+
+ @Test(expected = Exception::class)
+ fun `Will throw an exception to try to create a invalid choice type item`() {
+ val choices = arrayOf(separator)
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", true, MENU_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val adapter = getAdapterFrom(fragment)
+ adapter.onCreateViewHolder(LinearLayout(testContext), -1)
+ }
+
+ @Test
+ fun `Will adapter will return correct view type `() {
+ val choices = arrayOf(
+ item,
+ Choice(id = "", label = "item1", children = arrayOf()),
+ Choice(id = "", label = "menu", children = arrayOf()),
+ Choice(id = "", label = "separator", children = arrayOf(), isASeparator = true),
+ Choice(id = "", label = "multiple choice"),
+ )
+
+ var fragment = spy(newInstance(choices, "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ var adapter = getAdapterFrom(fragment)
+ var type = adapter.getItemViewType(0)
+
+ assertEquals(type, TYPE_SINGLE)
+
+ fragment = spy(newInstance(choices, "sessionId", "uid", true, MULTIPLE_CHOICE_DIALOG_TYPE))
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ adapter = getAdapterFrom(fragment)
+
+ type = adapter.getItemViewType(1)
+ assertEquals(type, TYPE_GROUP)
+
+ fragment = spy(newInstance(choices, "sessionId", "uid", false, MENU_CHOICE_DIALOG_TYPE))
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ adapter = getAdapterFrom(fragment)
+
+ type = adapter.getItemViewType(2)
+ assertEquals(type, TYPE_MENU)
+
+ type = adapter.getItemViewType(3)
+ assertEquals(type, TYPE_MENU_SEPARATOR)
+
+ fragment = spy(newInstance(choices, "sessionId", "uid", true, MULTIPLE_CHOICE_DIALOG_TYPE))
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ adapter = getAdapterFrom(fragment)
+
+ type = adapter.getItemViewType(4)
+ assertEquals(type, TYPE_MULTIPLE)
+ }
+
+ @Test
+ fun `Will show a multiple choice item`() {
+ val choices =
+ arrayOf(Choice(id = "", label = "item1", children = arrayOf(Choice(id = "", label = "sub-item1"))))
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, MULTIPLE_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val adapter = getAdapterFrom(fragment)
+
+ val holder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_MULTIPLE) as MultipleViewHolder
+ val groupHolder = adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_GROUP) as GroupViewHolder
+
+ adapter.bindViewHolder(holder, 0)
+ adapter.bindViewHolder(groupHolder, 1)
+
+ assertEquals(2, adapter.itemCount)
+ assertEquals("item1", holder.labelView.text)
+ assertEquals("sub-item1", groupHolder.labelView.text.trim())
+ }
+
+ @Test
+ fun `Will show a multiple choice item with selected element`() {
+ val choices = arrayOf(
+ Choice(
+ id = "",
+ label = "item1",
+ children = arrayOf(Choice(id = "", label = "sub-item1", selected = true)),
+ ),
+ )
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", true, MULTIPLE_CHOICE_DIALOG_TYPE))
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val adapter = getAdapterFrom(fragment)
+
+ assertEquals(2, adapter.itemCount)
+
+ val groupHolder = adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_GROUP) as GroupViewHolder
+ val holder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_MULTIPLE) as MultipleViewHolder
+
+ adapter.bindViewHolder(groupHolder, 0)
+ adapter.bindViewHolder(holder, 1)
+
+ assertEquals("item1", (groupHolder.labelView as TextView).text)
+ assertEquals("sub-item1", holder.labelView.text.trim())
+ assertEquals(true, holder.labelView.isChecked)
+ }
+
+ @Test
+ fun `Clicking on single choice item notifies the feature`() {
+ val choices = arrayOf(item)
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = getAdapterFrom(fragment)
+
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_SINGLE) as SingleViewHolder
+
+ adapter.bindViewHolder(holder, 0)
+
+ holder.itemView.performClick()
+ shadowOf(getMainLooper()).idle()
+ verify(mockFeature).onConfirm("sessionId", "uid", choices.first())
+
+ dialog.dismiss()
+ shadowOf(getMainLooper()).idle()
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+
+ @Test
+ fun `Clicking on menu choice item notifies the feature`() {
+ val choices = arrayOf(item)
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", true, MENU_CHOICE_DIALOG_TYPE))
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = getAdapterFrom(fragment)
+
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_MENU)
+
+ adapter.bindViewHolder(holder, 0)
+
+ holder.itemView.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", choices.first())
+
+ dialog.dismiss()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+
+ @Test
+ fun `Clicking on multiple choice item notifies the feature`() {
+ val choices =
+ arrayOf(Choice(id = "", label = "item1", children = arrayOf(subItem)))
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, MULTIPLE_CHOICE_DIALOG_TYPE))
+
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_MULTIPLE)
+
+ adapter.bindViewHolder(holder, 1)
+
+ holder.itemView.performClick()
+
+ assertTrue(fragment.mapSelectChoice.isNotEmpty())
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", fragment.mapSelectChoice.keys.toTypedArray())
+
+ val negativeButton = dialog.getButton(BUTTON_NEGATIVE)
+ negativeButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature, times(2)).onCancel("sessionId", "uid")
+ }
+
+ @Test
+ fun `Clicking on selected multiple choice item will notify feature`() {
+ val choices =
+ arrayOf(item.copy(selected = true))
+ val fragment = spy(newInstance(choices, "sessionId", "uid", true, MULTIPLE_CHOICE_DIALOG_TYPE))
+
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ val holder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), TYPE_MULTIPLE) as MultipleViewHolder
+
+ adapter.bindViewHolder(holder, 0)
+
+ assertTrue(holder.labelView.isChecked)
+
+ holder.itemView.performClick()
+
+ assertTrue(fragment.mapSelectChoice.isEmpty())
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", fragment.mapSelectChoice.keys.toTypedArray())
+ }
+
+ @Test
+ fun `single choice item with multiple sub-menu groups`() {
+ val choices = arrayOf(
+ Choice(
+ id = "group1",
+ label = "group1",
+ children = arrayOf(Choice(id = "item_group_1", label = "item group 1")),
+ ),
+ Choice(
+ id = "group2",
+ label = "group2",
+ children = arrayOf(Choice(id = "item_group_2", label = "item group 2")),
+ ),
+ )
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ val groupViewHolder = adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(0))
+ as GroupViewHolder
+
+ adapter.bindViewHolder(groupViewHolder, 0)
+
+ assertFalse(groupViewHolder.labelView.isEnabled)
+ assertEquals(groupViewHolder.labelView.text, "group1")
+
+ val singleViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(1)) as SingleViewHolder
+
+ adapter.bindViewHolder(singleViewHolder, 1)
+
+ with(singleViewHolder) {
+ assertTrue(labelView.isEnabled)
+
+ val choiceGroup1 = choices[0].children!![0]
+ assertEquals(labelView.text, choiceGroup1.label)
+
+ itemView.performClick()
+ verify(mockFeature).onConfirm("sessionId", "uid", choiceGroup1)
+ }
+ }
+
+ @Test
+ fun `disabled single choice item is not clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice"),
+ )
+
+ val fragment =
+ spy(newInstance(choices, "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test disabled item
+ val disabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(1))
+ as SingleViewHolder
+
+ adapter.bindViewHolder(disabledItemViewHolder, 1)
+
+ with(disabledItemViewHolder) {
+ assertEquals(labelView.text, "Disabled choice")
+ assertFalse(labelView.isEnabled)
+ assertFalse(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `enabled single choice item is clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice"),
+ )
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test enabled item
+ val enabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(0)) as SingleViewHolder
+
+ adapter.bindViewHolder(enabledItemViewHolder, 0)
+
+ with(enabledItemViewHolder) {
+ assertEquals(labelView.text, "Enabled choice")
+ assertTrue(labelView.isEnabled)
+ assertTrue(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `disabled multiple choice item is not clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice"),
+ )
+
+ val fragment =
+ spy(newInstance(choices, "sessionId", "uid", false, MULTIPLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test disabled item
+ val disabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(1))
+ as MultipleViewHolder
+
+ adapter.bindViewHolder(disabledItemViewHolder, 1)
+
+ with(disabledItemViewHolder) {
+ assertEquals(labelView.text, "Disabled choice")
+ assertFalse(labelView.isEnabled)
+ assertFalse(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `enabled multiple choice item is clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice"),
+ )
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, MULTIPLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test enabled item
+ val enabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(0)) as MultipleViewHolder
+
+ adapter.bindViewHolder(enabledItemViewHolder, 0)
+
+ with(enabledItemViewHolder) {
+ assertEquals(labelView.text, "Enabled choice")
+ assertTrue(labelView.isEnabled)
+ assertTrue(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `disabled menu choice item is not clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice"),
+ )
+
+ val fragment =
+ spy(newInstance(choices, "sessionId", "uid", false, MENU_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test disabled item
+ val disabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(1))
+ as MenuViewHolder
+
+ adapter.bindViewHolder(disabledItemViewHolder, 1)
+
+ with(disabledItemViewHolder) {
+ assertEquals(labelView.text, "Disabled choice")
+ assertFalse(labelView.isEnabled)
+ assertFalse(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `enabled menu choice item is clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice"),
+ )
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, MENU_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById<RecyclerView>(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test enabled item
+ val enabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(0)) as MenuViewHolder
+
+ adapter.bindViewHolder(enabledItemViewHolder, 0)
+
+ with(enabledItemViewHolder) {
+ assertEquals(labelView.text, "Enabled choice")
+ assertTrue(labelView.isEnabled)
+ assertTrue(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `scroll to selected item`() {
+ // array of 20 choices; 10th one is selected
+ val choices = Array(20) { index ->
+ if (index == 10) {
+ item.copy(selected = true, label = "selected")
+ } else {
+ item.copy(label = "item$index")
+ }
+ }
+ val fragment = newInstance(choices, "sessionId", "uid", true, SINGLE_CHOICE_DIALOG_TYPE)
+ val inflater = LayoutInflater.from(testContext)
+ val dialog = fragment.createDialogContentView(inflater)
+ val recyclerView = dialog.findViewById<RecyclerView>(R.id.recyclerView)
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+ // these two lines are a bit of a hack to get the layout manager to draw the view.
+ recyclerView.measure(0, 0)
+ recyclerView.layout(0, 0, 100, 250)
+
+ val selectedItemIndex = layoutManager.findLastCompletelyVisibleItemPosition()
+ assertEquals(10, selectedItemIndex)
+ }
+
+ @Test
+ fun `test toArrayOfChoices`() {
+ val parcelables = Array<Parcelable>(1) { Choice(id = "id", label = "label") }
+ val choice = parcelables.toArrayOfChoices()
+ assertNotNull(choice)
+ }
+
+ private fun getAdapterFrom(fragment: ChoiceDialogFragment): ChoiceAdapter {
+ val inflater = LayoutInflater.from(testContext)
+ val view = fragment.createDialogContentView(inflater)
+ val recyclerViewId = R.id.recyclerView
+
+ return view.findViewById<RecyclerView>(recyclerViewId).adapter as ChoiceAdapter
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragmentTest.kt
new file mode 100644
index 0000000000..e452e75897
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragmentTest.kt
@@ -0,0 +1,159 @@
+/* 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.prompts.dialog
+
+import android.content.DialogInterface
+import android.os.Looper.getMainLooper
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class ColorPickerDialogFragmentTest {
+
+ @Mock private lateinit var mockFeature: Prompter
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `build dialog`() {
+ val fragment = spy(
+ ColorPickerDialogFragment.newInstance("sessionId", "uid", true, "#e66465"),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ assertEquals(fragment.sessionId, "sessionId")
+ assertEquals(fragment.promptRequestUID, "uid")
+ assertEquals(fragment.selectedColor.toHexColor(), "#e66465")
+ }
+
+ @Test
+ fun `clicking on positive button notifies the feature`() {
+ val fragment = spy(
+ ColorPickerDialogFragment.newInstance("sessionId", "uid", false, "#e66465"),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ fragment.onColorChange("#4f4663".toColor())
+
+ val positiveButton = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", "#4f4663")
+ }
+
+ @Test
+ fun `clicking on negative button notifies the feature`() {
+ val fragment = spy(
+ ColorPickerDialogFragment.newInstance("sessionId", "uid", true, "#e66465"),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val negativeButton = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_NEGATIVE)
+ negativeButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+
+ @Test
+ fun `touching outside of the dialog must notify the feature onCancel`() {
+ val fragment = spy(
+ ColorPickerDialogFragment.newInstance("sessionId", "uid", false, "#e66465"),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ fragment.onCancel(mock())
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+
+ @Test
+ fun `will show a color item`() {
+ val fragment = spy(
+ ColorPickerDialogFragment.newInstance("sessionId", "uid", true, "#e66465"),
+ )
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val adapter = getAdapterFrom(fragment)
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), 0)
+ adapter.bindViewHolder(holder, 0)
+
+ val selectedColor = appCompatContext.resources
+ .obtainTypedArray(R.array.mozac_feature_prompts_default_colors).let {
+ it.getColor(0, 0)
+ }
+
+ assertEquals(selectedColor, holder.color)
+ }
+
+ @Test
+ fun `clicking on a item will update the selected color`() {
+ val fragment = spy(
+ ColorPickerDialogFragment.newInstance("sessionId", "uid", false, "#e66465"),
+ )
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val adapter = getAdapterFrom(fragment)
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), 0)
+ val colorItem = holder.itemView as TextView
+ adapter.bindViewHolder(holder, 0)
+
+ colorItem.performClick()
+
+ val selectedColor = appCompatContext.resources
+ .obtainTypedArray(R.array.mozac_feature_prompts_default_colors).let {
+ it.getColor(0, 0)
+ }
+ assertEquals(fragment.selectedColor, selectedColor)
+ }
+
+ private fun getAdapterFrom(fragment: ColorPickerDialogFragment): BasicColorAdapter {
+ val view = fragment.createDialogContentView()
+ val recyclerViewId = R.id.recyclerView
+
+ return view.findViewById<RecyclerView>(recyclerViewId).adapter as BasicColorAdapter
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragmentTest.kt
new file mode 100644
index 0000000000..20a1cfc76a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragmentTest.kt
@@ -0,0 +1,126 @@
+/* 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.prompts.dialog
+
+import android.content.DialogInterface
+import android.os.Looper.getMainLooper
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.test.ext.appCompatContext
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+class ConfirmDialogFragmentTest {
+
+ @Mock private lateinit var mockFeature: Prompter
+ private lateinit var fragment: ConfirmDialogFragment
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ fragment = spy(
+ ConfirmDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ true,
+ "title",
+ "message",
+ "positiveLabel",
+ "negativeLabel",
+ ),
+ )
+ }
+
+ @Test
+ fun `build dialog`() {
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val titleTextView = dialog.findViewById<TextView>(appcompatR.id.alertTitle)
+ val messageTextView = dialog.findViewById<TextView>(R.id.message)
+
+ assertEquals(fragment.sessionId, "sessionId")
+ assertEquals(fragment.promptRequestUID, "uid")
+ assertEquals(fragment.message, "message")
+
+ val positiveButton = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
+ val negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
+
+ assertEquals("title", titleTextView.text)
+ assertEquals("message", messageTextView.text.toString())
+ assertEquals("positiveLabel", positiveButton.text)
+ assertEquals("negativeLabel", negativeButton.text)
+ }
+
+ @Test
+ fun `clicking on positive button notifies the feature`() {
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", false)
+ }
+
+ @Test
+ fun `clicking on negative button notifies the feature`() {
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val negativeButton = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_NEGATIVE)
+ negativeButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onCancel("sessionId", "uid", false)
+ }
+
+ @Test
+ fun `cancelling the dialog cancels the feature`() {
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ Mockito.doNothing().`when`(fragment).dismiss()
+
+ Assert.assertNotNull(dialog)
+
+ dialog.show()
+
+ fragment.onCancel(dialog)
+
+ verify(mockFeature).onCancel("sessionId", "uid", false)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragmentTest.kt
new file mode 100644
index 0000000000..07e060c578
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragmentTest.kt
@@ -0,0 +1,252 @@
+/* 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.prompts.dialog
+
+import android.content.DialogInterface
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.os.Looper.getMainLooper
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.R.id
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+class MultiButtonDialogFragmentTest {
+
+ @Mock private lateinit var mockFeature: Prompter
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `Build dialog`() {
+ val fragment = spy(
+ MultiButtonDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ "title",
+ "message",
+ true,
+ false,
+ "positiveButton",
+ "negativeButton",
+ "neutralButton",
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val titleTextView = dialog.findViewById<TextView>(appcompatR.id.alertTitle)
+ val messageTextView = dialog.findViewById<TextView>(R.id.message)
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ val negativeButton = (dialog).getButton(DialogInterface.BUTTON_NEGATIVE)
+ val neutralButton = (dialog).getButton(DialogInterface.BUTTON_NEUTRAL)
+
+ assertEquals(fragment.sessionId, "sessionId")
+ assertEquals(fragment.promptRequestUID, "uid")
+ assertEquals(fragment.message, "message")
+ assertEquals(fragment.hasShownManyDialogs, true)
+
+ assertEquals(titleTextView.text, "title")
+ assertEquals(fragment.title, "title")
+ assertEquals(messageTextView.text.toString(), "message")
+ assertTrue(checkBox.isVisible)
+
+ assertEquals(positiveButton.text, "positiveButton")
+ assertEquals(negativeButton.text, "negativeButton")
+ assertEquals(neutralButton.text, "neutralButton")
+ }
+
+ @Test
+ fun `Dialog with hasShownManyDialogs equals false should not have a checkbox`() {
+ val fragment = spy(
+ MultiButtonDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ "title",
+ "message",
+ false,
+ false,
+ "positiveButton",
+ "negativeButton",
+ "neutralButton",
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+
+ assertFalse(checkBox.isVisible)
+ }
+
+ @Test
+ fun `Clicking on a positive button notifies the feature`() {
+ val fragment = spy(
+ MultiButtonDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ "title",
+ "message",
+ false,
+ false,
+ "positiveButton",
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", false to MultiButtonDialogFragment.ButtonType.POSITIVE)
+ }
+
+ @Test
+ fun `Clicking on a negative button notifies the feature`() {
+ val fragment = spy(
+ MultiButtonDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ "title",
+ "message",
+ false,
+ false,
+ negativeButton = "negative",
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val negativeButton = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_NEGATIVE)
+ negativeButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", false to MultiButtonDialogFragment.ButtonType.NEGATIVE)
+ }
+
+ @Test
+ fun `Clicking on a neutral button notifies the feature`() {
+ val fragment = spy(
+ MultiButtonDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ "title",
+ "message",
+ false,
+ false,
+ neutralButton = "neutral",
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val neutralButton = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_NEUTRAL)
+ neutralButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", false to MultiButtonDialogFragment.ButtonType.NEUTRAL)
+ }
+
+ @Test
+ fun `After checking no more dialogs checkbox onConfirm must be called with NoMoreDialogs equals true`() {
+ val fragment = spy(
+ MultiButtonDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ "title",
+ "message",
+ true,
+ false,
+ positiveButton = "positive",
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+
+ checkBox.isChecked = true
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", true to MultiButtonDialogFragment.ButtonType.POSITIVE)
+ }
+
+ @Test
+ fun `Touching outside of the dialog must notify the feature onCancel`() {
+ val fragment = spy(
+ MultiButtonDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ "title",
+ "message",
+ true,
+ false,
+ positiveButton = "positive",
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ fragment.onCancel(mock())
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragmentTest.kt
new file mode 100644
index 0000000000..3ac2c1404c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragmentTest.kt
@@ -0,0 +1,132 @@
+/* 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.prompts.dialog
+
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.material.textfield.TextInputEditText
+import junit.framework.TestCase
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows
+import mozilla.components.ui.icons.R as iconsR
+
+@RunWith(AndroidJUnit4::class)
+class SaveLoginDialogFragmentTest : TestCase() {
+ @Test
+ fun `dialog should always set the website icon if it is available`() {
+ val sessionId = "sessionId"
+ val requestUID = "uid"
+ val shouldDismissOnLoad = true
+ val hint = 42
+ val loginUsername = "username"
+ val loginPassword = "password"
+ val entry: LoginEntry = mock() // valid image to be used as favicon
+ `when`(entry.username).thenReturn(loginUsername)
+ `when`(entry.password).thenReturn(loginPassword)
+ val icon: Bitmap = mock()
+ val fragment = spy(
+ SaveLoginDialogFragment.newInstance(
+ sessionId,
+ requestUID,
+ shouldDismissOnLoad,
+ hint,
+ entry,
+ icon,
+ ),
+ )
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(TextInputEditText(appCompatContext).apply { id = R.id.username_field })
+ addView(TextInputEditText(appCompatContext).apply { id = R.id.password_field })
+ addView(ImageView(appCompatContext).apply { id = R.id.host_icon })
+ }
+ }.`when`(fragment).inflateRootView(any())
+
+ val fragmentView = fragment.onCreateView(mock(), mock(), mock())
+
+ verify(fragment).inflateRootView(any())
+ verify(fragment).setupRootView(any())
+ assertEquals(sessionId, fragment.sessionId)
+ assertEquals(requestUID, fragment.promptRequestUID)
+ // Using assertTrue since assertEquals / assertSame would fail here
+ assertTrue(loginUsername == fragmentView.findViewById<TextInputEditText>(R.id.username_field).text.toString())
+ assertTrue(loginPassword == fragmentView.findViewById<TextInputEditText>(R.id.password_field).text.toString())
+
+ // Actually verifying that the provided image is used
+ verify(fragment, times(0)).setImageViewTint(any())
+ assertSame(icon, (fragmentView.findViewById<ImageView>(R.id.host_icon).drawable as BitmapDrawable).bitmap)
+ }
+
+ @Test
+ fun `dialog should use a default tinted icon if favicon is not available`() {
+ val sessionId = "sessionId"
+ val requestUID = "uid"
+ val shouldDismissOnLoad = false
+ val hint = 42
+ val loginUsername = "username"
+ val loginPassword = "password"
+ val entry: LoginEntry = mock()
+ `when`(entry.username).thenReturn(loginUsername)
+ `when`(entry.password).thenReturn(loginPassword)
+ val icon: Bitmap? = null // null favicon
+ val fragment = spy(
+ SaveLoginDialogFragment.newInstance(
+ sessionId,
+ requestUID,
+ shouldDismissOnLoad,
+ hint,
+ entry,
+ icon,
+ ),
+ )
+ val defaultIconResource = iconsR.drawable.mozac_ic_globe_24
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(TextInputEditText(appCompatContext).apply { id = R.id.username_field })
+ addView(TextInputEditText(appCompatContext).apply { id = R.id.password_field })
+ addView(
+ ImageView(appCompatContext).apply {
+ id = R.id.host_icon
+ setImageResource(defaultIconResource)
+ },
+ )
+ }
+ }.`when`(fragment).inflateRootView(any())
+
+ val fragmentView = fragment.onCreateView(mock(), mock(), mock())
+
+ verify(fragment).inflateRootView(any())
+ verify(fragment).setupRootView(any())
+ assertEquals(sessionId, fragment.sessionId)
+ // Using assertTrue since assertEquals / assertSame would fail here
+ assertTrue(loginUsername == fragmentView.findViewById<TextInputEditText>(R.id.username_field).text.toString())
+ assertTrue(loginPassword == fragmentView.findViewById<TextInputEditText>(R.id.password_field).text.toString())
+
+ // Actually verifying that the tinted default image is used
+ val iconView = fragmentView.findViewById<ImageView>(R.id.host_icon)
+ verify(fragment).setImageViewTint(iconView)
+ assertNotNull(iconView.imageTintList)
+ // The icon sent was null, we want the default instead
+ assertNotNull(iconView.drawable)
+ assertEquals(defaultIconResource, Shadows.shadowOf(iconView.drawable).createdFromResId)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragmentTest.kt
new file mode 100644
index 0000000000..b0fee5f9a1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragmentTest.kt
@@ -0,0 +1,153 @@
+/* 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.prompts.dialog
+
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.os.Looper.getMainLooper
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.R.id
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.robolectric.Shadows.shadowOf
+import androidx.appcompat.R as appcompatR
+
+@RunWith(AndroidJUnit4::class)
+class TextPromptDialogFragmentTest {
+
+ @Mock private lateinit var mockFeature: Prompter
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ }
+
+ @Test
+ fun `build dialog`() {
+ val fragment = spy(
+ TextPromptDialogFragment.newInstance("sessionId", "uid", true, "title", "label", "defaultValue", true),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val titleTextView = dialog.findViewById<TextView>(appcompatR.id.alertTitle)
+ val inputLabel = dialog.findViewById<TextView>(id.input_label)
+ val inputValue = dialog.findViewById<TextView>(id.input_value)
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+
+ assertEquals(fragment.sessionId, "sessionId")
+ assertEquals(fragment.promptRequestUID, "uid")
+ assertEquals(fragment.title, "title")
+ assertEquals(fragment.labelInput, "label")
+ assertEquals(fragment.defaultInputValue, "defaultValue")
+ assertEquals(fragment.hasShownManyDialogs, true)
+
+ assertEquals(titleTextView.text, "title")
+ assertEquals(fragment.title, "title")
+ assertEquals(inputLabel.text, "label")
+ assertEquals(inputValue.text.toString(), "defaultValue")
+ assertTrue(checkBox.isVisible)
+
+ checkBox.isChecked = true
+ assertTrue(fragment.userSelectionNoMoreDialogs)
+
+ inputValue.text = "NewValue"
+ assertEquals(inputValue.text.toString(), "NewValue")
+ }
+
+ @Test
+ fun `TextPrompt with hasShownManyDialogs equals false should not have a checkbox`() {
+ val fragment = spy(
+ TextPromptDialogFragment.newInstance("sessionId", "uid", false, "title", "label", "defaultValue", false),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+
+ assertFalse(checkBox.isVisible)
+ }
+
+ @Test
+ fun `Clicking on positive button notifies the feature`() {
+ val fragment = spy(
+ TextPromptDialogFragment.newInstance("sessionId", "uid", true, "title", "label", "defaultValue", false),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", false to "defaultValue")
+ }
+
+ @Test
+ fun `After checking no more dialogs checkbox feature onNoMoreDialogsChecked must be called`() {
+ val fragment = spy(
+ TextPromptDialogFragment.newInstance("sessionId", "uid", false, "title", "label", "defaultValue", true),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(id.mozac_feature_prompts_no_more_dialogs_check_box)
+
+ checkBox.isChecked = true
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm("sessionId", "uid", true to "defaultValue")
+ }
+
+ @Test
+ fun `touching outside of the dialog must notify the feature onCancel`() {
+ val fragment = spy(
+ TextPromptDialogFragment.newInstance("sessionId", "uid", true, "title", "label", "defaultValue", true),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ fragment.onCancel(mock())
+
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt
new file mode 100644
index 0000000000..722c836f80
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt
@@ -0,0 +1,284 @@
+/* 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.prompts.dialog
+
+import android.app.AlertDialog
+import android.app.DatePickerDialog
+import android.app.TimePickerDialog
+import android.content.DialogInterface.BUTTON_NEUTRAL
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.os.Looper.getMainLooper
+import android.widget.DatePicker
+import android.widget.NumberPicker
+import android.widget.TimePicker
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.dialog.TimePickerDialogFragment.Companion.SELECTION_TYPE_DATE_AND_TIME
+import mozilla.components.feature.prompts.dialog.TimePickerDialogFragment.Companion.SELECTION_TYPE_MONTH
+import mozilla.components.feature.prompts.dialog.TimePickerDialogFragment.Companion.SELECTION_TYPE_TIME
+import mozilla.components.feature.prompts.ext.month
+import mozilla.components.feature.prompts.ext.toCalendar
+import mozilla.components.feature.prompts.ext.year
+import mozilla.components.support.ktx.kotlin.toDate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+import java.util.Calendar
+import java.util.Date
+
+@RunWith(AndroidJUnit4::class)
+class TimePickerDialogFragmentTest {
+
+ @Mock private lateinit var mockFeature: Prompter
+
+ @Before
+ fun setup() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `build dialog`() {
+ val initialDate = "2019-11-29".toDate("yyyy-MM-dd")
+ val minDate = "2019-11-28".toDate("yyyy-MM-dd")
+ val maxDate = "2019-11-30".toDate("yyyy-MM-dd")
+ val fragment = spy(
+ TimePickerDialogFragment.newInstance("sessionId", "uid", true, initialDate, minDate, maxDate),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val datePicker = (dialog as DatePickerDialog).datePicker
+ assertEquals("sessionId", fragment.sessionId)
+ assertEquals("uid", fragment.promptRequestUID)
+ assertEquals(2019, datePicker.year)
+ assertEquals(11, datePicker.month + 1)
+ assertEquals(29, datePicker.dayOfMonth)
+ assertEquals(minDate, Date(datePicker.minDate))
+ assertEquals(maxDate, Date(datePicker.maxDate))
+ }
+
+ @Test
+ fun `Clicking on positive, neutral and negative button notifies the feature`() {
+ val initialDate = "2019-11-29".toDate("yyyy-MM-dd")
+ val fragment = spy(
+ TimePickerDialogFragment.newInstance("sessionId", "uid", false, initialDate, null, null),
+ )
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onConfirm(eq("sessionId"), eq("uid"), any())
+
+ val neutralButton = dialog.getButton(BUTTON_NEUTRAL)
+ neutralButton.performClick()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockFeature).onClear("sessionId", "uid")
+ }
+
+ @Test
+ fun `touching outside of the dialog must notify the feature onCancel`() {
+ val fragment = spy(
+ TimePickerDialogFragment.newInstance("sessionId", "uid", true, Date(), null, null),
+ )
+ fragment.feature = mockFeature
+ doReturn(testContext).`when`(fragment).requireContext()
+ fragment.onCancel(mock())
+ verify(mockFeature).onCancel("sessionId", "uid")
+ }
+
+ @Test
+ fun `onTimeChanged must update the selectedDate`() {
+ val dialogPicker = TimePickerDialogFragment.newInstance("sessionId", "uid", false, Date(), null, null)
+
+ dialogPicker.onTimeChanged(mock(), 1, 12)
+
+ val calendar = dialogPicker.selectedDate.toCalendar()
+
+ assertEquals(calendar.hour, 1)
+ assertEquals(calendar.minutes, 12)
+ }
+
+ @Test
+ fun `building a date and time picker`() {
+ val initialDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm")
+ val minDate = "2018-06-07T00:00".toDate("yyyy-MM-dd'T'HH:mm")
+ val maxDate = "2018-06-14T00:00".toDate("yyyy-MM-dd'T'HH:mm")
+ val fragment = spy(
+ TimePickerDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ true,
+ initialDate,
+ minDate,
+ maxDate,
+ SELECTION_TYPE_DATE_AND_TIME,
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val datePicker = dialog.findViewById<DatePicker>(R.id.date_picker)
+
+ assertEquals(2018, datePicker.year)
+ assertEquals(6, datePicker.month + 1)
+ assertEquals(12, datePicker.dayOfMonth)
+
+ assertEquals(minDate, Date(datePicker.minDate))
+ assertEquals(maxDate, Date(datePicker.maxDate))
+
+ val timePicker = dialog.findViewById<TimePicker>(R.id.datetime_picker)
+
+ assertEquals(19, timePicker.hour)
+ assertEquals(30, timePicker.minute)
+ }
+
+ @Test
+ fun `building a month picker`() {
+ val initialDate = "2018-06".toDate("yyyy-MM")
+ val minDate = "2018-04".toDate("yyyy-MM")
+ val maxDate = "2018-09".toDate("yyyy-MM")
+
+ val initialDateCal = initialDate.toCalendar()
+ val minCal = minDate.toCalendar()
+ val maxCal = maxDate.toCalendar()
+
+ val fragment = spy(
+ TimePickerDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ false,
+ initialDate,
+ minDate,
+ maxDate,
+ SELECTION_TYPE_MONTH,
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val monthPicker = dialog.findViewById<NumberPicker>(R.id.month_chooser)
+ val yearPicker = dialog.findViewById<NumberPicker>(R.id.year_chooser)
+
+ assertEquals(initialDateCal.year, yearPicker.value)
+ assertEquals(initialDateCal.month, monthPicker.value)
+
+ assertEquals(minCal.year, yearPicker.minValue)
+ assertEquals(minCal.month, monthPicker.minValue)
+
+ assertEquals(maxCal.year, yearPicker.maxValue)
+ assertEquals(maxCal.month, monthPicker.maxValue)
+
+ fragment.onDateSet(mock(), 8, 2019)
+ val selectedDate = fragment.selectedDate.toCalendar()
+
+ assertEquals(2019, selectedDate.year)
+ assertEquals(7, selectedDate.month)
+ }
+
+ @Test
+ fun `building a time picker`() {
+ val initialDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm")
+ val minDate = "2018-06-07T00:00".toDate("yyyy-MM-dd'T'HH:mm")
+ val maxDate = "2018-06-14T00:00".toDate("yyyy-MM-dd'T'HH:mm")
+ val fragment = spy(
+ TimePickerDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ true,
+ initialDate,
+ minDate,
+ maxDate,
+ SELECTION_TYPE_TIME,
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ assertTrue(dialog is TimePickerDialog)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `creating a TimePickerDialogFragment with an invalid type selection will throw an exception`() {
+ val initialDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm")
+ val minDate = "2018-06-07T00:00".toDate("yyyy-MM-dd'T'HH:mm")
+ val maxDate = "2018-06-14T00:00".toDate("yyyy-MM-dd'T'HH:mm")
+ val fragment = spy(
+ TimePickerDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ false,
+ initialDate,
+ minDate,
+ maxDate,
+ -223,
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `creating a TimePickerDialogFragment with empty title and an invalid type selection will throw an exception `() {
+ val initialDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm")
+ val minDate = "2018-06-07T00:00".toDate("yyyy-MM-dd'T'HH:mm")
+ val maxDate = "2018-06-14T00:00".toDate("yyyy-MM-dd'T'HH:mm")
+ val fragment = spy(
+ TimePickerDialogFragment.newInstance(
+ "sessionId",
+ "uid",
+ true,
+ initialDate,
+ minDate,
+ maxDate,
+ -223,
+ ),
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+ }
+
+ private val Calendar.minutes: Int
+ get() = get(Calendar.MINUTE)
+ private val Calendar.hour: Int
+ get() = get(Calendar.HOUR_OF_DAY)
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/ext/EditTextTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/ext/EditTextTest.kt
new file mode 100644
index 0000000000..5e82b01242
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/ext/EditTextTest.kt
@@ -0,0 +1,44 @@
+/* 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.prompts.ext
+
+import android.view.inputmethod.EditorInfo
+import android.widget.EditText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class EditTextTest {
+ private val onDonePressed: () -> Unit = spy {}
+
+ @Test
+ fun `GIVEN a callback, WHEN action done is performed - IME_ACTION_DONE -, THEN onDonePress should be called`() {
+ val view = EditText(testContext)
+ val editorInfo = EditorInfo()
+ val inputConnection = view.onCreateInputConnection(editorInfo)
+
+ view.onDone(false, onDonePressed)
+ inputConnection.performEditorAction(EditorInfo.IME_ACTION_DONE)
+
+ verify(onDonePressed).invoke()
+ }
+
+ @Test
+ fun `GIVEN a callback, WHEN a different action is performed - IME_ACTION_SEARCH -, THEN onDonePress shouldn't be called `() {
+ val view = EditText(testContext)
+ val editorInfo = EditorInfo()
+ val inputConnection = view.onCreateInputConnection(editorInfo)
+
+ view.onDone(false, onDonePressed)
+ inputConnection.performEditorAction(EditorInfo.IME_ACTION_SEARCH)
+
+ verify(onDonePressed, never()).invoke()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/ext/PromptRequestTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/ext/PromptRequestTest.kt
new file mode 100644
index 0000000000..07e3d50b24
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/ext/PromptRequestTest.kt
@@ -0,0 +1,55 @@
+/* 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.prompts.ext
+
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.prompt.PromptRequest.Alert
+import mozilla.components.concept.engine.prompt.PromptRequest.Confirm
+import mozilla.components.concept.engine.prompt.PromptRequest.Popup
+import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
+import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import kotlin.reflect.KClass
+
+class PromptRequestTest {
+ @Test
+ fun `GIVEN only a subset of prompts should be shown in fullscreen WHEN checking which are not THEN return the expected result`() {
+ val expected = listOf<KClass<out PromptRequest>>(
+ Alert::class,
+ TextPrompt::class,
+ Confirm::class,
+ Popup::class,
+ )
+
+ assertEquals(expected, PROMPTS_TO_EXIT_FULLSCREEN_FOR)
+ }
+
+ @Test
+ fun `GIVEN a prompt which should not be shown in fullscreen WHEN trying to execute code such prompts THEN execute the code`() {
+ var invocations = 0
+ val alert: Alert = mock()
+ val text: TextPrompt = mock()
+ val confirm: Confirm = mock()
+ val popup: Popup = mock()
+ val windowedPrompts = listOf(alert, text, confirm, popup)
+
+ windowedPrompts.forEachIndexed { index, prompt ->
+ prompt.executeIfWindowedPrompt { invocations++ }
+ assertEquals(index + 1, invocations)
+ }
+ }
+
+ @Test
+ fun `GIVEN a prompt which should be shown in fullscreen WHEN trying to execute code for windowed prompts THEN don't do anything`() {
+ var invocations = 0
+ val choice: SingleChoice = mock()
+
+ choice.executeIfWindowedPrompt { invocations++ }
+
+ assertEquals(0, invocations)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/AddressAutofillDialogFactsTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/AddressAutofillDialogFactsTest.kt
new file mode 100644
index 0000000000..3d990f063b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/AddressAutofillDialogFactsTest.kt
@@ -0,0 +1,94 @@
+/* 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.prompts.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class AddressAutofillDialogFactsTest {
+
+ @Test
+ fun `Emits facts for address autofill form detected events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSuccessfulAddressAutofillFormDetectedFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_FORM_DETECTED, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill success events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSuccessfulAddressAutofillSuccessFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_SUCCESS, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill shown events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitAddressAutofillShownFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_SHOWN, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill expanded events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitAddressAutofillExpandedFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_EXPANDED, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill dismissed events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitAddressAutofillDismissedFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_DISMISSED, item)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFactsTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFactsTest.kt
new file mode 100644
index 0000000000..9de04521c3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFactsTest.kt
@@ -0,0 +1,147 @@
+/* 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.prompts.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class CreditCardAutofillDialogFactsTest {
+
+ @Test
+ fun `Emits facts for autofill form detected events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSuccessfulCreditCardAutofillFormDetectedFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_FORM_DETECTED, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill success events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSuccessfulCreditCardAutofillSuccessFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SUCCESS, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill shown events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitCreditCardAutofillShownFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_SHOWN, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill expanded events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitCreditCardAutofillExpandedFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_EXPANDED, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill dismissed events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitCreditCardAutofillDismissedFact()
+
+ assertEquals(1, facts.size)
+
+ facts[0].apply {
+ assertEquals(Component.FEATURE_PROMPTS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_DISMISSED, item)
+ }
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill confirm and create events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitCreditCardAutofillCreatedFact()
+
+ assertEquals(1, facts.size)
+
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.CONFIRM, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_CREATED,
+ fact.item,
+ )
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill confirm and update events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitCreditCardAutofillUpdatedFact()
+
+ assertEquals(1, facts.size)
+
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.CONFIRM, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_UPDATED,
+ fact.item,
+ )
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill save prompt shown event`() {
+ CollectionProcessor.withFactCollection { facts ->
+ emitCreditCardSaveShownFact()
+
+ assertEquals(1, facts.size)
+
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.DISPLAY, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SAVE_PROMPT_SHOWN,
+ fact.item,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FilePickerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FilePickerTest.kt
new file mode 100644
index 0000000000..4f52501984
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FilePickerTest.kt
@@ -0,0 +1,405 @@
+/* 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.prompts.file
+
+import android.Manifest
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
+import android.content.ClipData
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.feature.prompts.PromptContainer
+import mozilla.components.feature.prompts.file.FilePicker.Companion.FILE_PICKER_ACTIVITY_REQUEST_CODE
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.grantPermission
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertArrayEquals
+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.Mockito.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class FilePickerTest {
+
+ private val noopSingle: (Context, Uri) -> Unit = { _, _ -> }
+ private val noopMulti: (Context, Array<Uri>) -> Unit = { _, _ -> }
+ private val request = PromptRequest.File(
+ mimeTypes = emptyArray(),
+ onSingleFileSelected = noopSingle,
+ onMultipleFilesSelected = noopMulti,
+ onDismiss = {},
+ )
+
+ private lateinit var fragment: PromptContainer
+ private lateinit var store: BrowserStore
+ private lateinit var state: BrowserState
+ private lateinit var filePicker: FilePicker
+ private lateinit var fileUploadsDirCleaner: FileUploadsDirCleaner
+
+ @Before
+ fun setup() {
+ fileUploadsDirCleaner = mock()
+ fragment = spy(PromptContainer.TestPromptContainer(testContext))
+ state = mock()
+ store = mock()
+ whenever(store.state).thenReturn(state)
+ filePicker = FilePicker(
+ fragment,
+ store,
+ fileUploadsDirCleaner = fileUploadsDirCleaner,
+ ) { }
+ }
+
+ @Test
+ fun `FilePicker acts on a given (custom tab) session or the selected session`() {
+ val customTabContent: ContentState = mock()
+ whenever(customTabContent.promptRequests).thenReturn(listOf(request))
+ val customTab = CustomTabSessionState(id = "custom-tab", content = customTabContent, trackingProtection = mock(), config = mock())
+
+ whenever(state.customTabs).thenReturn(listOf(customTab))
+ filePicker = FilePicker(
+ fragment,
+ store,
+ customTab.id,
+ fileUploadsDirCleaner = mock(),
+ ) { }
+ filePicker.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, 0, null)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(customTab.id, request))
+
+ val selected = prepareSelectedSession(request)
+ filePicker = FilePicker(
+ fragment,
+ store,
+ fileUploadsDirCleaner = mock(),
+ ) { }
+ filePicker.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, 0, null)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(selected.id, request))
+ }
+
+ @Test
+ fun `handleFilePickerRequest without the required permission will call askAndroidPermissionsForRequest`() {
+ var onRequestPermissionWasCalled = false
+ val context = ApplicationProvider.getApplicationContext<Context>()
+
+ filePicker = spy(
+ FilePicker(
+ fragment,
+ store,
+ fileUploadsDirCleaner = mock(),
+ ) {
+ onRequestPermissionWasCalled = true
+ },
+ )
+
+ doReturn(context).`when`(fragment).context
+
+ filePicker.handleFileRequest(request)
+
+ assertTrue(onRequestPermissionWasCalled)
+ verify(filePicker).askAndroidPermissionsForRequest(any(), eq(request))
+ verify(fragment, never()).startActivityForResult(Intent(), 1)
+ }
+
+ @Test
+ fun `handleFilePickerRequest with the required permission will call startActivityForResult`() {
+ var onRequestPermissionWasCalled = false
+
+ filePicker = FilePicker(
+ fragment,
+ store,
+ fileUploadsDirCleaner = mock(),
+ ) {
+ onRequestPermissionWasCalled = true
+ }
+
+ grantPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+
+ filePicker.handleFileRequest(request)
+
+ assertFalse(onRequestPermissionWasCalled)
+ verify(fragment).startActivityForResult(any(), anyInt())
+ }
+
+ @Test
+ fun `onPermissionsGranted will forward call to filePickerRequest`() {
+ stubContext()
+ filePicker = spy(filePicker)
+ filePicker.currentRequest = request
+
+ filePicker.onPermissionsGranted()
+
+ // The original prompt that started the request permission flow is persisted in the store
+ // That should not be accesses / modified in any way.
+ verifyNoInteractions(store)
+ // After the permission is granted we should retry picking a file based on the original request.
+ verify(filePicker).handleFileRequest(eq(request), eq(false))
+ }
+
+ @Test
+ fun `onPermissionsDeny will call onDismiss and consume the file PromptRequest of the actual session`() {
+ var onDismissWasCalled = false
+ val filePickerRequest = request.copy {
+ onDismissWasCalled = true
+ }
+
+ val selected = prepareSelectedSession(filePickerRequest)
+
+ stubContext()
+
+ filePicker.onPermissionsDenied()
+
+ assertTrue(onDismissWasCalled)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(selected.id, filePickerRequest))
+ }
+
+ @Test
+ fun `onActivityResult with RESULT_OK and isMultipleFilesSelection false will consume PromptRequest of the actual session`() {
+ var onSingleFileSelectionWasCalled = false
+
+ val onSingleFileSelection: (Context, Uri) -> Unit = { _, _ ->
+ onSingleFileSelectionWasCalled = true
+ }
+
+ val filePickerRequest = request.copy(onSingleFileSelected = onSingleFileSelection)
+
+ val selected = prepareSelectedSession(filePickerRequest)
+ val intent = Intent()
+
+ intent.data = mock()
+
+ stubContext()
+
+ filePicker.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, RESULT_OK, intent)
+
+ assertTrue(onSingleFileSelectionWasCalled)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(selected.id, filePickerRequest))
+ }
+
+ @Test
+ fun `onActivityResult with RESULT_OK and isMultipleFilesSelection true will consume PromptRequest of the actual session`() {
+ var onMultipleFileSelectionWasCalled = false
+
+ val onMultipleFileSelection: (Context, Array<Uri>) -> Unit = { _, _ ->
+ onMultipleFileSelectionWasCalled = true
+ }
+
+ val filePickerRequest = request.copy(
+ isMultipleFilesSelection = true,
+ onMultipleFilesSelected = onMultipleFileSelection,
+ )
+
+ val selected = prepareSelectedSession(filePickerRequest)
+ val intent = Intent()
+
+ intent.clipData = mock()
+ val item = mock<ClipData.Item>()
+
+ doReturn(mock<Uri>()).`when`(item).uri
+
+ intent.clipData?.apply {
+ doReturn(1).`when`(this).itemCount
+ doReturn(item).`when`(this).getItemAt(0)
+ }
+
+ stubContext()
+
+ filePicker.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, RESULT_OK, intent)
+
+ assertTrue(onMultipleFileSelectionWasCalled)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(selected.id, filePickerRequest))
+ }
+
+ @Test
+ fun `onActivityResult with not RESULT_OK will consume PromptRequest of the actual session and call onDismiss `() {
+ var onDismissWasCalled = false
+
+ val filePickerRequest = request.copy(isMultipleFilesSelection = true) {
+ onDismissWasCalled = true
+ }
+
+ val selected = prepareSelectedSession(filePickerRequest)
+ val intent = Intent()
+
+ filePicker.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, RESULT_CANCELED, intent)
+
+ assertTrue(onDismissWasCalled)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(selected.id, filePickerRequest))
+ }
+
+ @Test
+ fun `onActivityResult will not process any PromptRequest that is not a File request`() {
+ var wasConfirmed = false
+ var wasDismissed = false
+ val onConfirm: (Boolean) -> Unit = { wasConfirmed = true }
+ val onDismiss = { wasDismissed = true }
+ val invalidRequest = PromptRequest.Alert("", "", false, onConfirm, onDismiss)
+ val spiedFilePicker = spy(filePicker)
+ val selected = prepareSelectedSession(invalidRequest)
+ val intent = Intent()
+
+ spiedFilePicker.onActivityResult(FILE_PICKER_ACTIVITY_REQUEST_CODE, RESULT_OK, intent)
+
+ assertFalse(wasConfirmed)
+ assertFalse(wasDismissed)
+ verify(store, never()).dispatch(ContentAction.ConsumePromptRequestAction(selected.id, request))
+ verify(spiedFilePicker, never()).handleFilePickerIntentResult(intent, request)
+ }
+
+ @Test
+ fun `onActivityResult returns false if the request code is not the same`() {
+ val intent = Intent()
+ val result = filePicker.onActivityResult(10101, RESULT_OK, intent)
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `onRequestPermissionsResult with FILE_PICKER_REQUEST and PERMISSION_GRANTED will call onPermissionsGranted`() {
+ stubContext()
+ filePicker = spy(filePicker)
+ filePicker.currentRequest = request
+
+ filePicker.onPermissionsResult(emptyArray(), IntArray(1) { PERMISSION_GRANTED })
+
+ verify(filePicker).onPermissionsGranted()
+ }
+
+ @Test
+ fun `onRequestPermissionsResult with FILE_PICKER_REQUEST and PERMISSION_DENIED will call onPermissionsDeny`() {
+ filePicker = spy(filePicker)
+ filePicker.onPermissionsResult(emptyArray(), IntArray(1) { PERMISSION_DENIED })
+
+ verify(filePicker).onPermissionsDenied()
+ }
+
+ @Test
+ fun `askAndroidPermissionsForRequest should cache the current request and then ask for permissions`() {
+ val permissions = setOf("PermissionA")
+ var permissionsRequested = emptyArray<String>()
+ filePicker = spy(
+ FilePicker(fragment, store, null, fileUploadsDirCleaner = mock()) { requested ->
+ permissionsRequested = requested
+ },
+ )
+
+ filePicker.askAndroidPermissionsForRequest(permissions, request)
+
+ assertEquals(request, filePicker.currentRequest)
+ assertArrayEquals(permissions.toTypedArray(), permissionsRequested)
+ }
+
+ @Test
+ fun `handleFilePickerIntentResult called with null Intent will make captureUri null`() {
+ stubContext()
+ captureUri = "randomSaveLocationOnDisk".toUri()
+ val onSingleFileSelection: (Context, Uri) -> Unit = { _, _ -> Unit }
+ val promptRequest = mock<PromptRequest.File>()
+ doReturn(onSingleFileSelection).`when`(promptRequest).onSingleFileSelected
+
+ filePicker.handleFilePickerIntentResult(null, promptRequest)
+
+ assertNull(captureUri)
+ }
+
+ @Test
+ fun `handleFilePickerIntentResult called with valid Intent will make captureUri null also if request is dismissed`() {
+ stubContext()
+ captureUri = "randomSaveLocationOnDisk".toUri()
+ val promptRequest = mock<PromptRequest.File>()
+ doReturn({ }).`when`(promptRequest).onDismiss
+ // A private file cannot be picked so the request will be dismissed.
+ val intent = Intent().apply {
+ data = ("file://" + File(testContext.applicationInfo.dataDir, "randomFile").canonicalPath).toUri()
+ }
+
+ filePicker.handleFilePickerIntentResult(intent, promptRequest)
+
+ assertNull(captureUri)
+ }
+
+ @Test
+ fun `handleFilePickerIntentResult for multiple files selection will make captureUri null`() {
+ stubContext()
+ captureUri = "randomSaveLocationOnDisk".toUri()
+ val onMultipleFilesSelected: (Context, Array<Uri>) -> Unit = { _, _ -> Unit }
+ val promptRequest = mock<PromptRequest.File>()
+ doReturn(onMultipleFilesSelected).`when`(promptRequest).onMultipleFilesSelected
+ doReturn(true).`when`(promptRequest).isMultipleFilesSelection
+ val intent = Intent().apply {
+ clipData = (ClipData.newRawUri("Test", "https://www.mozilla.org".toUri()))
+ }
+
+ filePicker.handleFilePickerIntentResult(intent, promptRequest)
+
+ verify(fileUploadsDirCleaner).enqueueForCleanup(any())
+ assertNull(captureUri)
+ }
+
+ @Test
+ fun `handleFilePickerIntentResult for multiple files selection will make captureUri null also if request is dismissed`() {
+ stubContext()
+ captureUri = "randomSaveLocationOnDisk".toUri()
+ val promptRequest = mock<PromptRequest.File>()
+ doReturn({ }).`when`(promptRequest).onDismiss
+ doReturn(true).`when`(promptRequest).isMultipleFilesSelection
+ // A private file cannot be picked so the request will be dismissed.
+ val intent = Intent().apply {
+ clipData = (
+ ClipData.newRawUri(
+ "Test",
+ ("file://" + File(testContext.applicationInfo.dataDir, "randomFile").canonicalPath).toUri(),
+ )
+ )
+ }
+
+ filePicker.handleFilePickerIntentResult(intent, promptRequest)
+
+ assertNull(captureUri)
+ }
+
+ private fun prepareSelectedSession(request: PromptRequest? = null): TabSessionState {
+ val promptRequest: PromptRequest = request ?: mock()
+ val content: ContentState = mock()
+ whenever(content.promptRequests).thenReturn(listOf(promptRequest))
+
+ val selected = TabSessionState("browser-tab", content, mock(), mock())
+ whenever(state.selectedTabId).thenReturn(selected.id)
+ whenever(state.tabs).thenReturn(listOf(selected))
+ return selected
+ }
+
+ private fun stubContext() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ doReturn(context).`when`(fragment).context
+ filePicker = FilePicker(fragment, store, fileUploadsDirCleaner = fileUploadsDirCleaner) {}
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerMiddlewareTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerMiddlewareTest.kt
new file mode 100644
index 0000000000..85fa560102
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerMiddlewareTest.kt
@@ -0,0 +1,59 @@
+/* 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.prompts.file
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+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.Rule
+import org.junit.Test
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class FileUploadsDirCleanerMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun `WHEN an action that indicates the user has navigated to another webiste THEN clean up temporary uploads`() {
+ val fileUploadsDirCleaner = mock<FileUploadsDirCleaner>()
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(
+ middleware = listOf(
+ FileUploadsDirCleanerMiddleware(
+ fileUploadsDirCleaner = fileUploadsDirCleaner,
+ ),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ )
+
+ store.dispatch(TabListAction.SelectTabAction("test-tab")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(fileUploadsDirCleaner).cleanRecentUploads()
+
+ store.dispatch(ContentAction.UpdateLoadRequestAction("test-tab", mock())).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(fileUploadsDirCleaner, times(2)).cleanRecentUploads()
+
+ store.dispatch(ContentAction.UpdateUrlAction("test-tab", "url")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(fileUploadsDirCleaner, times(3)).cleanRecentUploads()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerTest.kt
new file mode 100644
index 0000000000..6f67c2ce9c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/FileUploadsDirCleanerTest.kt
@@ -0,0 +1,87 @@
+/* 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.prompts.file
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import mozilla.components.concept.engine.prompt.PromptRequest.File.Companion.DEFAULT_UPLOADS_DIR_NAME
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class FileUploadsDirCleanerTest {
+ private lateinit var fileCleaner: FileUploadsDirCleaner
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ fileCleaner = FileUploadsDirCleaner(
+ scope = MainScope(),
+ ) {
+ testContext.cacheDir
+ }
+ fileCleaner.fileNamesToBeDeleted = emptyList()
+ fileCleaner.dispatcher = Dispatchers.Main
+ }
+
+ @Test
+ fun `WHEN calling enqueueForCleanup THEN fileName should be added to fileNamesToBeDeleted list`() {
+ val expectedFileName = "my_file.txt"
+ fileCleaner.enqueueForCleanup(expectedFileName)
+ assertEquals(expectedFileName, fileCleaner.fileNamesToBeDeleted.first())
+ }
+
+ @Test
+ fun `WHEN calling cleanRecentUploads THEN all the enqueued files should be deleted and not enqueued files must be kept`() =
+ runTestOnMain {
+ val cachedDir = File(testContext.cacheDir, DEFAULT_UPLOADS_DIR_NAME)
+ assertTrue(cachedDir.mkdir())
+
+ val fileToBeDeleted = File(cachedDir, "my_file.txt")
+ val fileToBeKept = File(cachedDir, "file_to_be_kept.txt")
+
+ assertTrue(fileToBeDeleted.createNewFile())
+ assertTrue(fileToBeKept.createNewFile())
+ assertTrue(fileToBeDeleted.exists())
+ assertTrue(fileToBeKept.exists())
+
+ fileCleaner.enqueueForCleanup(fileToBeDeleted.name)
+
+ fileCleaner.cleanRecentUploads()
+
+ assertTrue(fileCleaner.fileNamesToBeDeleted.isEmpty())
+ assertFalse(fileToBeDeleted.exists())
+ assertTrue(fileToBeKept.exists())
+ }
+
+ @Test
+ fun `WHEN calling cleanUploadsDirectory THEN the uploads directory should emptied`() =
+ runTestOnMain {
+ val cachedDir = File(testContext.cacheDir, DEFAULT_UPLOADS_DIR_NAME)
+ assertTrue(cachedDir.mkdir())
+
+ val fileToBeDeleted = File(cachedDir, "my_file.txt")
+
+ assertTrue(fileToBeDeleted.createNewFile())
+ assertTrue(fileToBeDeleted.exists())
+
+ fileCleaner.cleanUploadsDirectory()
+
+ assertFalse(fileToBeDeleted.exists())
+ assertFalse(cachedDir.exists())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/MimeTypeTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/MimeTypeTest.kt
new file mode 100644
index 0000000000..483c108889
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/file/MimeTypeTest.kt
@@ -0,0 +1,392 @@
+/* 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.prompts.file
+
+import android.content.Context
+import android.content.Intent.ACTION_GET_CONTENT
+import android.content.Intent.CATEGORY_OPENABLE
+import android.content.Intent.EXTRA_ALLOW_MULTIPLE
+import android.content.Intent.EXTRA_MIME_TYPES
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.MATCH_DEFAULT_ONLY
+import android.content.pm.ProviderInfo
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import android.provider.MediaStore.ACTION_IMAGE_CAPTURE
+import android.provider.MediaStore.ACTION_VIDEO_CAPTURE
+import android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION
+import android.provider.MediaStore.EXTRA_OUTPUT
+import android.webkit.MimeTypeMap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.support.test.robolectric.testContext
+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.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.notNull
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class MimeTypeTest {
+
+ private val request = PromptRequest.File(
+ mimeTypes = emptyArray(),
+ onSingleFileSelected = { _, _ -> },
+ onMultipleFilesSelected = { _, _ -> },
+ onDismiss = {},
+ )
+ private val capture = PromptRequest.File.FacingMode.ANY
+
+ private lateinit var context: Context
+ private lateinit var packageManager: PackageManager
+
+ @Before
+ fun setup() {
+ context = mock(Context::class.java)
+ packageManager = mock(PackageManager::class.java)
+
+ `when`(context.packageManager).thenReturn(packageManager)
+ `when`(context.packageName).thenReturn("org.mozilla.browser")
+ @Suppress("DEPRECATION")
+ `when`(packageManager.resolveActivity(notNull(), eq(MATCH_DEFAULT_ONLY))).thenReturn(null)
+ }
+
+ @Test
+ fun `matches empty list of mime types`() {
+ assertTypes(setOf(MimeType.Wildcard)) {
+ it.matches(emptyArray())
+ }
+ }
+
+ @Test
+ fun `matches varied list of mime types`() {
+ assertTypes(setOf(MimeType.Image(), MimeType.Audio, MimeType.Wildcard)) {
+ it.matches(arrayOf("image/*", "audio/*"))
+ }
+ assertTypes(setOf(MimeType.Audio, MimeType.Video, MimeType.Wildcard)) {
+ it.matches(arrayOf("video/mp4", "audio/*"))
+ }
+ }
+
+ @Test
+ fun `matches image types`() {
+ assertTypes(setOf(MimeType.Image(), MimeType.Wildcard)) {
+ it.matches(arrayOf("image/*"))
+ }
+ assertTypes(setOf(MimeType.Image(), MimeType.Wildcard)) {
+ it.matches(arrayOf("image/jpg"))
+ }
+ assertTypes(setOf(MimeType.Wildcard)) {
+ it.matches(arrayOf(".webp"))
+ }
+ assertTypes(setOf(MimeType.Image(), MimeType.Wildcard)) {
+ it.matches(arrayOf(".jpg", "image/*", ".gif"))
+ }
+ }
+
+ @Test
+ fun `matches video types`() {
+ assertTypes(setOf(MimeType.Video, MimeType.Wildcard)) {
+ it.matches(arrayOf("video/*"))
+ }
+ assertTypes(setOf(MimeType.Video, MimeType.Wildcard)) {
+ it.matches(arrayOf("video/avi"))
+ }
+ assertTypes(setOf(MimeType.Wildcard)) {
+ it.matches(arrayOf(".webm"))
+ }
+ assertTypes(setOf(MimeType.Video, MimeType.Wildcard)) {
+ it.matches(arrayOf("video/*", ".mov", ".mp4"))
+ }
+ }
+
+ @Test
+ fun `matches audio types`() {
+ assertTypes(setOf(MimeType.Audio, MimeType.Wildcard)) {
+ it.matches(arrayOf("audio/*"))
+ }
+ assertTypes(setOf(MimeType.Audio, MimeType.Wildcard)) {
+ it.matches(arrayOf("audio/wav"))
+ }
+ assertTypes(setOf(MimeType.Wildcard)) {
+ it.matches(arrayOf(".mp3"))
+ }
+ assertTypes(setOf(MimeType.Audio, MimeType.Wildcard)) {
+ it.matches(arrayOf(".ogg", "audio/wav", "audio/*"))
+ }
+ }
+
+ @Test
+ fun `matches document types`() {
+ assertTypes(setOf(MimeType.Wildcard)) {
+ it.matches(arrayOf("application/json"))
+ }
+ assertTypes(setOf(MimeType.Wildcard)) {
+ it.matches(arrayOf(".doc"))
+ }
+ assertTypes(setOf(MimeType.Wildcard)) {
+ it.matches(arrayOf(".txt", "text/html"))
+ }
+ }
+
+ @Test
+ fun `shouldCapture empty list of mime types`() {
+ assertTypes(setOf()) {
+ it.shouldCapture(emptyArray(), capture)
+ }
+ }
+
+ @Test
+ fun `shouldCapture varied list of mime types`() {
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf("image/*", "video/*"), capture)
+ }
+ }
+
+ @Test
+ fun `shouldCapture image types`() {
+ assertTypes(setOf(MimeType.Image())) {
+ it.shouldCapture(arrayOf("image/*"), capture)
+ }
+ assertTypes(setOf(MimeType.Image())) {
+ it.shouldCapture(arrayOf("image/jpg"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf(".webp"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf(".jpg", "image/*", ".gif"), capture)
+ }
+ assertTypes(setOf(MimeType.Image())) {
+ it.shouldCapture(arrayOf("image/png", "image/jpg"), capture)
+ }
+ }
+
+ @Test
+ fun `shouldCapture video types`() {
+ assertTypes(setOf(MimeType.Video)) {
+ it.shouldCapture(arrayOf("video/*"), capture)
+ }
+ assertTypes(setOf(MimeType.Video)) {
+ it.shouldCapture(arrayOf("video/avi"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf(".webm"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf("video/*", ".mov", ".mp4"), capture)
+ }
+ assertTypes(setOf(MimeType.Video)) {
+ it.shouldCapture(arrayOf("video/webm", "video/*"), capture)
+ }
+ }
+
+ @Test
+ fun `shouldCapture audio types`() {
+ assertTypes(setOf(MimeType.Audio)) {
+ it.shouldCapture(arrayOf("audio/*"), capture)
+ }
+ assertTypes(setOf(MimeType.Audio)) {
+ it.shouldCapture(arrayOf("audio/wav"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf(".mp3"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf(".ogg", "audio/wav", "audio/*"), capture)
+ }
+ assertTypes(setOf(MimeType.Audio)) {
+ it.shouldCapture(arrayOf("audio/wav", "audio/ogg"), capture)
+ }
+ }
+
+ @Test
+ fun `shouldCapture document types`() {
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf("application/json"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf(".doc"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf(".txt", "text/html"), capture)
+ }
+ assertTypes(setOf()) {
+ it.shouldCapture(arrayOf("text/plain", "text/html"), capture)
+ }
+ }
+
+ @Test
+ fun `Image buildIntent`() {
+ assertNull(MimeType.Image().buildIntent(context, request))
+
+ val uri = Uri.parse("context://abcd")
+ val image = MimeType.Image { _, _, _ -> uri }
+
+ @Suppress("DEPRECATION")
+ `when`(
+ packageManager
+ .resolveContentProvider(eq("org.mozilla.browser.fileprovider"), anyInt()),
+ )
+ .thenReturn(mock(ProviderInfo::class.java))
+ mockResolveActivity()
+
+ image.buildIntent(context, request)?.run {
+ assertEquals(action, ACTION_IMAGE_CAPTURE)
+ assertEquals(1, extras?.size())
+
+ @Suppress("DEPRECATION")
+ val photoUri = extras!!.get(EXTRA_OUTPUT) as Uri
+ assertEquals(uri, photoUri)
+ }
+
+ val anyCaptureRequest = request.copy(captureMode = PromptRequest.File.FacingMode.ANY)
+ image.buildIntent(context, anyCaptureRequest)?.run {
+ assertEquals(action, ACTION_IMAGE_CAPTURE)
+ assertEquals(1, extras?.size())
+ }
+
+ val frontCaptureRequest = request.copy(captureMode = PromptRequest.File.FacingMode.FRONT_CAMERA)
+ image.buildIntent(context, frontCaptureRequest)?.run {
+ assertEquals(action, ACTION_IMAGE_CAPTURE)
+ assertEquals(1, extras!!.getInt(MimeType.CAMERA_FACING))
+ assertEquals(1, extras!!.getInt(MimeType.LENS_FACING_FRONT))
+ assertEquals(true, extras!!.getBoolean(MimeType.USE_FRONT_CAMERA))
+ }
+
+ val backCaptureRequest = request.copy(captureMode = PromptRequest.File.FacingMode.BACK_CAMERA)
+ image.buildIntent(context, backCaptureRequest)?.run {
+ assertEquals(action, ACTION_IMAGE_CAPTURE)
+ assertEquals(0, extras!!.getInt(MimeType.CAMERA_FACING))
+ assertEquals(1, extras!!.getInt(MimeType.LENS_FACING_BACK))
+ assertEquals(true, extras!!.getBoolean(MimeType.USE_BACK_CAMERA))
+ }
+ }
+
+ @Test
+ fun `Video buildIntent`() {
+ assertNull(MimeType.Video.buildIntent(context, request))
+
+ mockResolveActivity()
+ MimeType.Video.buildIntent(context, request)?.run {
+ assertEquals(action, ACTION_VIDEO_CAPTURE)
+ assertNull(extras)
+ }
+
+ val anyCaptureRequest = request.copy(captureMode = PromptRequest.File.FacingMode.ANY)
+ MimeType.Video.buildIntent(context, anyCaptureRequest)?.run {
+ assertEquals(action, ACTION_VIDEO_CAPTURE)
+ assertNull(extras)
+ }
+
+ val frontCaptureRequest = request.copy(captureMode = PromptRequest.File.FacingMode.FRONT_CAMERA)
+ MimeType.Video.buildIntent(context, frontCaptureRequest)?.run {
+ assertEquals(action, ACTION_VIDEO_CAPTURE)
+ assertEquals(1, extras!!.getInt(MimeType.CAMERA_FACING))
+ assertEquals(1, extras!!.getInt(MimeType.LENS_FACING_FRONT))
+ assertEquals(true, extras!!.getBoolean(MimeType.USE_FRONT_CAMERA))
+ }
+
+ val backCaptureRequest = request.copy(captureMode = PromptRequest.File.FacingMode.BACK_CAMERA)
+ MimeType.Video.buildIntent(context, backCaptureRequest)?.run {
+ assertEquals(action, ACTION_VIDEO_CAPTURE)
+ assertEquals(0, extras!!.getInt(MimeType.CAMERA_FACING))
+ assertEquals(1, extras!!.getInt(MimeType.LENS_FACING_BACK))
+ assertEquals(true, extras!!.getBoolean(MimeType.USE_BACK_CAMERA))
+ }
+ }
+
+ @Test
+ fun `Audio buildIntent`() {
+ assertNull(MimeType.Audio.buildIntent(context, request))
+
+ mockResolveActivity()
+ MimeType.Audio.buildIntent(context, request)?.run {
+ assertEquals(action, RECORD_SOUND_ACTION)
+ }
+ }
+
+ @Test
+ fun `Wildcard buildIntent`() {
+ // allowMultipleFiles false and empty mimeTypes will create an intent
+ // without EXTRA_ALLOW_MULTIPLE and EXTRA_MIME_TYPES
+ with(MimeType.Wildcard.buildIntent(testContext, request)) {
+ assertEquals(action, ACTION_GET_CONTENT)
+ assertEquals(type, "*/*")
+ assertTrue(categories.contains(CATEGORY_OPENABLE))
+
+ val mimeType = extras!!.getStringArray(EXTRA_MIME_TYPES)
+ assertNull(mimeType)
+
+ val allowMultipleFiles = extras!!.getBoolean(EXTRA_ALLOW_MULTIPLE)
+ assertFalse(allowMultipleFiles)
+ }
+
+ // allowMultipleFiles true and not empty mimeTypes will create an intent
+ // with EXTRA_ALLOW_MULTIPLE and EXTRA_MIME_TYPES
+ val multiJpegRequest = request.copy(
+ mimeTypes = arrayOf("image/jpeg"),
+ isMultipleFilesSelection = true,
+ )
+ with(MimeType.Wildcard.buildIntent(testContext, multiJpegRequest)) {
+ assertEquals(action, ACTION_GET_CONTENT)
+ assertEquals(type, "*/*")
+ assertTrue(categories.contains(CATEGORY_OPENABLE))
+
+ val mimeTypes = extras!!.getStringArray(EXTRA_MIME_TYPES) as Array<*>
+ assertEquals(mimeTypes.first(), "image/jpeg")
+
+ val allowMultipleFiles = extras!!.getBoolean(EXTRA_ALLOW_MULTIPLE)
+ assertTrue(allowMultipleFiles)
+ }
+ }
+
+ @Test
+ fun `Wildcard buildIntent with file extensions`() {
+ shadowOf(MimeTypeMap.getSingleton()).apply {
+ addExtensionMimeTypeMapping(".gif", "image/gif")
+ addExtensionMimeTypeMapping(
+ "docx",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ )
+ }
+
+ val extensionsRequest = request.copy(mimeTypes = arrayOf(".gif", "image/jpeg", "docx", ".fun"))
+
+ with(MimeType.Wildcard.buildIntent(testContext, extensionsRequest)) {
+ assertEquals(action, ACTION_GET_CONTENT)
+
+ val mimeTypes = extras!!.getStringArray(EXTRA_MIME_TYPES) as Array<*>
+ assertEquals(mimeTypes[0], "image/gif")
+ assertEquals(mimeTypes[1], "image/jpeg")
+ assertEquals(mimeTypes[2], "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
+ assertEquals(mimeTypes[3], "*/*")
+ }
+ }
+
+ private fun mockResolveActivity() {
+ val info = ResolveInfo()
+ info.activityInfo = ActivityInfo()
+ info.activityInfo.applicationInfo = ApplicationInfo()
+ info.activityInfo.applicationInfo.packageName = "com.example.app"
+ info.activityInfo.name = "SomeActivity"
+ @Suppress("DEPRECATION")
+ `when`(packageManager.resolveActivity(notNull(), eq(MATCH_DEFAULT_ONLY))).thenReturn(info)
+ }
+
+ private fun assertTypes(valid: Set<MimeType>, func: (MimeType) -> Boolean) {
+ assertEquals(valid, MimeType.values().filter(func).toSet())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/BasicLoginAdapterTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/BasicLoginAdapterTest.kt
new file mode 100644
index 0000000000..6050d731ab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/BasicLoginAdapterTest.kt
@@ -0,0 +1,64 @@
+/* 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.prompts.login
+
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.prompts.R
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class BasicLoginAdapterTest {
+
+ val login =
+ Login(guid = "A", origin = "https://www.mozilla.org", username = "username", password = "password")
+ val login2 =
+ Login(guid = "B", origin = "https://www.mozilla.org", username = "username2", password = "password")
+
+ @Test
+ fun `getItemCount should return the number of logins`() {
+ var onLoginSelected: (Login) -> Unit = { }
+
+ val adapter = BasicLoginAdapter(onLoginSelected)
+
+ Assert.assertEquals(0, adapter.itemCount)
+
+ adapter.submitList(
+ listOf(login, login2),
+ )
+ Assert.assertEquals(2, adapter.itemCount)
+ }
+
+ @Test
+ fun `creates and binds login viewholder`() {
+ var confirmedLogin: Login? = null
+ var onLoginSelected: (Login) -> Unit = { confirmedLogin = it }
+
+ val adapter = BasicLoginAdapter(onLoginSelected)
+
+ adapter.submitList(listOf(login, login2))
+
+ val holder = adapter.createViewHolder(FrameLayout(testContext), 0)
+ adapter.bindViewHolder(holder, 0)
+
+ Assert.assertEquals(login, holder.login)
+ val userName = holder.itemView.findViewById<TextView>(R.id.username)
+ Assert.assertEquals("username", userName.text)
+ val password = holder.itemView.findViewById<TextView>(R.id.password)
+ Assert.assertEquals("password".length, password.text.length)
+ Assert.assertTrue(holder.itemView.isClickable)
+
+ holder.itemView.performClick()
+
+ Assert.assertEquals(confirmedLogin, login)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginPickerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginPickerTest.kt
new file mode 100644
index 0000000000..b8dae6b473
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginPickerTest.kt
@@ -0,0 +1,142 @@
+/* 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.prompts.login
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.Login
+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.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class LoginPickerTest {
+ val login =
+ Login(guid = "A", origin = "https://www.mozilla.org", username = "username", password = "password")
+ val login2 =
+ Login(guid = "B", origin = "https://www.mozilla.org", username = "username2", password = "password")
+
+ var onDismissWasCalled = false
+ var confirmedLogin: Login? = null
+ private val request = PromptRequest.SelectLoginPrompt(
+ logins = listOf(login, login2),
+ generatedPassword = null,
+ onConfirm = { confirmedLogin = it },
+ onDismiss = { onDismissWasCalled = true },
+ )
+
+ var manageLoginsCalled = false
+ private lateinit var store: BrowserStore
+ private lateinit var state: BrowserState
+ private lateinit var loginPicker: LoginPicker
+ private lateinit var loginSelectBar: LoginSelectBar
+ private var onManageLogins: () -> Unit = { manageLoginsCalled = true }
+
+ @Before
+ fun setup() {
+ state = mock()
+ store = mock()
+ loginSelectBar = mock()
+ whenever(store.state).thenReturn(state)
+ loginPicker = LoginPicker(store, loginSelectBar, onManageLogins)
+ }
+
+ @Test
+ fun `LoginPicker shows the login select bar on a custom tab`() {
+ val customTabContent: ContentState = mock()
+ whenever(customTabContent.promptRequests).thenReturn(listOf(request))
+ val customTab = CustomTabSessionState(id = "custom-tab", content = customTabContent, trackingProtection = mock(), config = mock())
+
+ whenever(state.customTabs).thenReturn(listOf(customTab))
+ loginPicker = LoginPicker(store, loginSelectBar, onManageLogins, customTab.id)
+ loginPicker.handleSelectLoginRequest(request)
+ verify(loginSelectBar).showPrompt(request.logins)
+ }
+
+ @Test
+ fun `LoginPicker shows the login select bar on a selected tab`() {
+ prepareSelectedSession(request)
+ loginPicker = LoginPicker(store, loginSelectBar, onManageLogins)
+ loginPicker.handleSelectLoginRequest(request)
+ verify(loginSelectBar).showPrompt(request.logins)
+ }
+
+ @Test
+ fun `LoginPicker selects and login through the request and hides view`() {
+ prepareSelectedSession(request)
+ loginPicker = LoginPicker(store, loginSelectBar, onManageLogins)
+
+ loginPicker.handleSelectLoginRequest(request)
+
+ loginPicker.onOptionSelect(login)
+
+ assertEquals(confirmedLogin, login)
+ verify(loginSelectBar).hidePrompt()
+ }
+
+ @Test
+ fun `LoginPicker invokes manage logins and hides view`() {
+ manageLoginsCalled = false
+ onDismissWasCalled = false
+
+ prepareSelectedSession(request)
+ loginPicker = LoginPicker(store, loginSelectBar, onManageLogins)
+
+ loginPicker.handleSelectLoginRequest(request)
+
+ loginPicker.onManageOptions()
+
+ assertTrue(manageLoginsCalled)
+ assertTrue(onDismissWasCalled)
+ verify(loginSelectBar).hidePrompt()
+ }
+
+ @Test
+ fun `WHEN dismissCurrentLoginSelect is called without a parameter THEN the active login prompt is dismissed`() {
+ val selectedSession = prepareSelectedSession(request)
+ loginPicker = LoginPicker(store, loginSelectBar, onManageLogins, selectedSession.id)
+
+ verify(store, never()).dispatch(any())
+ loginPicker.dismissCurrentLoginSelect()
+
+ assertTrue(onDismissWasCalled)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(selectedSession.id, request))
+ verify(loginSelectBar).hidePrompt()
+ }
+
+ @Test
+ fun `WHEN dismissCurrentLoginSelect is called with the active login prompt passed as parameter THEN the prompt is dismissed`() {
+ val selectedSession = prepareSelectedSession(request)
+ loginPicker = LoginPicker(store, loginSelectBar, onManageLogins, selectedSession.id)
+
+ verify(store, never()).dispatch(any())
+ loginPicker.dismissCurrentLoginSelect(request)
+
+ assertTrue(onDismissWasCalled)
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(selectedSession.id, request))
+ verify(loginSelectBar).hidePrompt()
+ }
+
+ private fun prepareSelectedSession(request: PromptRequest? = null): TabSessionState {
+ val promptRequest: PromptRequest = request ?: mock()
+ val content: ContentState = mock()
+ whenever(content.promptRequests).thenReturn(listOf(promptRequest))
+
+ val selected = TabSessionState("browser-tab", content, mock(), mock())
+ whenever(state.selectedTabId).thenReturn(selected.id)
+ whenever(state.tabs).thenReturn(listOf(selected))
+ return selected
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginSelectBarTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginSelectBarTest.kt
new file mode 100644
index 0000000000..1a5855b4a2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginSelectBarTest.kt
@@ -0,0 +1,104 @@
+/* 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.prompts.login
+
+import android.view.View
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.storage.Login
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class LoginSelectBarTest {
+ val login =
+ Login(guid = "A", origin = "https://www.mozilla.org", username = "username", password = "password")
+ val login2 =
+ Login(guid = "B", origin = "https://www.mozilla.org", username = "username2", password = "password")
+
+ @Test
+ fun `showPicker updates visibility`() {
+ val bar = LoginSelectBar(appCompatContext)
+
+ bar.showPrompt(listOf(login, login2))
+
+ assertTrue(bar.isVisible)
+ }
+
+ @Test
+ fun `hidePicker updates visibility`() {
+ val bar = spy(LoginSelectBar(appCompatContext))
+
+ bar.hidePrompt()
+
+ verify(bar).visibility = View.GONE
+ }
+
+ @Test
+ fun `listener is invoked when clicking manage logins option`() {
+ val bar = LoginSelectBar(appCompatContext)
+ val listener: SelectablePromptView.Listener<Login> = mock()
+
+ assertNull(bar.listener)
+
+ bar.listener = listener
+ bar.showPrompt(listOf(login, login2))
+
+ bar.findViewById<AppCompatTextView>(R.id.manage_logins).performClick()
+
+ verify(listener).onManageOptions()
+ }
+
+ @Test
+ fun `listener is invoked when clicking a login option`() {
+ val bar = LoginSelectBar(appCompatContext)
+ val listener: SelectablePromptView.Listener<Login> = mock()
+
+ assertNull(bar.listener)
+
+ bar.listener = listener
+ bar.showPrompt(listOf(login, login2))
+
+ val adapter = bar.findViewById<RecyclerView>(R.id.logins_list).adapter as BasicLoginAdapter
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), 0)
+ adapter.bindViewHolder(holder, 0)
+
+ holder.itemView.performClick()
+
+ verify(listener).onOptionSelect(login)
+ }
+
+ @Test
+ fun `view is expanded when clicking header`() {
+ val bar = LoginSelectBar(appCompatContext)
+
+ bar.showPrompt(listOf(login, login2))
+
+ bar.findViewById<AppCompatTextView>(R.id.saved_logins_header).performClick()
+
+ // Expanded
+ assertTrue(bar.findViewById<RecyclerView>(R.id.logins_list).isVisible)
+ assertTrue(bar.findViewById<AppCompatTextView>(R.id.manage_logins).isVisible)
+
+ bar.findViewById<AppCompatTextView>(R.id.saved_logins_header).performClick()
+
+ // Hidden
+ assertFalse(bar.findViewById<RecyclerView>(R.id.logins_list).isVisible)
+ assertFalse(bar.findViewById<AppCompatTextView>(R.id.manage_logins).isVisible)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/StrongPasswordPromptViewListenerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/StrongPasswordPromptViewListenerTest.kt
new file mode 100644
index 0000000000..454b362e5d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/StrongPasswordPromptViewListenerTest.kt
@@ -0,0 +1,143 @@
+/* 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.prompts.login
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.Login
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+
+class StrongPasswordPromptViewListenerTest {
+ private val suggestedPassword = "generatedPassword123#"
+
+ private val login =
+ Login(
+ guid = "A",
+ origin = "https://www.mozilla.org",
+ username = "username",
+ password = "password",
+ )
+ private val login2 =
+ Login(
+ guid = "B",
+ origin = "https://www.mozilla.org",
+ username = "username2",
+ password = "password",
+ )
+
+ private var onDismissWasCalled = false
+ private var confirmedLogin: Login? = null
+
+ private val request = PromptRequest.SelectLoginPrompt(
+ logins = listOf(login, login2),
+ generatedPassword = suggestedPassword,
+ onConfirm = { confirmedLogin = it },
+ onDismiss = { onDismissWasCalled = true },
+ )
+
+ private lateinit var store: BrowserStore
+ private lateinit var state: BrowserState
+
+ private lateinit var suggestStrongPasswordPromptViewListener: StrongPasswordPromptViewListener
+ private lateinit var suggestStrongPasswordBar: SuggestStrongPasswordBar
+
+ private val onSaveLoginWithGeneratedPass: (String, String) -> Unit = mock()
+ private val url = "https://www.mozilla.org"
+
+ @Before
+ fun setup() {
+ state = mock()
+ store = mock()
+ suggestStrongPasswordBar = mock()
+ whenever(store.state).thenReturn(state)
+ suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
+ }
+
+ @Test
+ fun `StrongPasswordGenerator shows the suggest strong password bar on a custom tab`() {
+ val customTabContent: ContentState = mock()
+ whenever(customTabContent.promptRequests).thenReturn(listOf(request))
+ val customTab = CustomTabSessionState(
+ id = "custom-tab",
+ content = customTabContent,
+ trackingProtection = mock(),
+ config = mock(),
+ )
+
+ whenever(state.customTabs).thenReturn(listOf(customTab))
+
+ suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
+ suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest(request, url, onSaveLoginWithGeneratedPass)
+ Mockito.verify(suggestStrongPasswordBar).showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass)
+ }
+
+ @Test
+ fun `StrongPasswordGenerator shows the suggest strong password bar on a selected tab`() {
+ prepareSelectedSession(request)
+ suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
+ suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest(request, url, onSaveLoginWithGeneratedPass)
+ Mockito.verify(suggestStrongPasswordBar).showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass)
+ }
+
+ @Test
+ fun `StrongPasswordGenerator invokes use the suggested password and hides view`() {
+ prepareSelectedSession(request)
+ suggestStrongPasswordPromptViewListener = StrongPasswordPromptViewListener(store, suggestStrongPasswordBar)
+ suggestStrongPasswordPromptViewListener.handleSuggestStrongPasswordRequest(request, "") { _, _ -> }
+ suggestStrongPasswordPromptViewListener.onUseGeneratedPassword(suggestedPassword, "") { _, _ -> }
+ Mockito.verify(suggestStrongPasswordBar).hidePrompt()
+ }
+
+ @Test
+ fun `WHEN dismissCurrentSuggestStrongPassword is called without a parameter THEN the active login prompt is dismissed`() {
+ val selectedSession = prepareSelectedSession(request)
+ suggestStrongPasswordPromptViewListener =
+ StrongPasswordPromptViewListener(store, suggestStrongPasswordBar, selectedSession.id)
+
+ Mockito.verify(store, Mockito.never()).dispatch(any())
+ suggestStrongPasswordPromptViewListener.dismissCurrentSuggestStrongPassword()
+
+ Assert.assertTrue(onDismissWasCalled)
+ Mockito.verify(store)
+ .dispatch(ContentAction.ConsumePromptRequestAction(selectedSession.id, request))
+ Mockito.verify(suggestStrongPasswordBar).hidePrompt()
+ }
+
+ @Test
+ fun `WHEN dismissCurrentSuggestStrongPassword is called with the active login prompt passed as parameter THEN the prompt is dismissed`() {
+ val selectedSession = prepareSelectedSession(request)
+ suggestStrongPasswordPromptViewListener =
+ StrongPasswordPromptViewListener(store, suggestStrongPasswordBar, selectedSession.id)
+
+ Mockito.verify(store, Mockito.never()).dispatch(any())
+ suggestStrongPasswordPromptViewListener.dismissCurrentSuggestStrongPassword(request)
+
+ Assert.assertTrue(onDismissWasCalled)
+ Mockito.verify(store)
+ .dispatch(ContentAction.ConsumePromptRequestAction(selectedSession.id, request))
+ Mockito.verify(suggestStrongPasswordBar).hidePrompt()
+ }
+
+ private fun prepareSelectedSession(request: PromptRequest? = null): TabSessionState {
+ val promptRequest: PromptRequest = request ?: mock()
+ val content: ContentState = mock()
+ whenever(content.promptRequests).thenReturn(listOf(promptRequest))
+
+ val selected = TabSessionState("browser-tab", content, mock(), mock())
+ whenever(state.selectedTabId).thenReturn(selected.id)
+ whenever(state.tabs).thenReturn(listOf(selected))
+ return selected
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordBarTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordBarTest.kt
new file mode 100644
index 0000000000..1c1278ebc1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/SuggestStrongPasswordBarTest.kt
@@ -0,0 +1,61 @@
+/* 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.prompts.login
+
+import android.view.View
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.PasswordPromptView
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+
+@RunWith(AndroidJUnit4::class)
+class SuggestStrongPasswordBarTest {
+
+ @Test
+ fun `hide prompt updates visibility`() {
+ val bar = Mockito.spy(SuggestStrongPasswordBar(appCompatContext))
+ bar.hidePrompt()
+ Mockito.verify(bar).visibility = View.GONE
+ }
+
+ @Test
+ fun `listener is invoked when clicking use strong password option`() {
+ val bar = SuggestStrongPasswordBar(appCompatContext)
+ val listener: PasswordPromptView.Listener = mock()
+ val suggestedPassword = "generatedPassword123#"
+ val url = "https://wwww.abc.com"
+ val onSaveLoginWithGeneratedPass: (String, String) -> Unit = mock()
+ Assert.assertNull(bar.listener)
+ bar.listener = listener
+ bar.showPrompt(suggestedPassword, url, onSaveLoginWithGeneratedPass)
+ bar.findViewById<AppCompatTextView>(R.id.use_strong_password).performClick()
+ Mockito.verify(listener)
+ .onUseGeneratedPassword(suggestedPassword, url, onSaveLoginWithGeneratedPass)
+ }
+
+ @Test
+ fun `view is expanded when clicking header`() {
+ val bar = SuggestStrongPasswordBar(appCompatContext)
+ val suggestedPassword = "generatedPassword123#"
+
+ bar.showPrompt(suggestedPassword, "") { _, _ -> }
+
+ bar.findViewById<AppCompatTextView>(R.id.suggest_strong_password_header).performClick()
+ // Expanded
+ Assert.assertTrue(bar.findViewById<RecyclerView>(R.id.use_strong_password).isVisible)
+
+ bar.findViewById<AppCompatTextView>(R.id.suggest_strong_password_header).performClick()
+ // Hidden
+ Assert.assertFalse(bar.findViewById<RecyclerView>(R.id.use_strong_password).isVisible)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/share/DefaultShareDelegateTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/share/DefaultShareDelegateTest.kt
new file mode 100644
index 0000000000..fb21601947
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/share/DefaultShareDelegateTest.kt
@@ -0,0 +1,70 @@
+/* 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.prompts.share
+
+import android.content.ActivityNotFoundException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.prompt.ShareData
+import mozilla.components.support.test.any
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class DefaultShareDelegateTest {
+
+ private lateinit var shareDelegate: ShareDelegate
+
+ @Before
+ fun setup() {
+ shareDelegate = DefaultShareDelegate()
+ }
+
+ @Test
+ fun `calls onSuccess after starting share chooser`() {
+ val context = spy(testContext)
+ doNothing().`when`(context).startActivity(any())
+
+ var dismissed = false
+ var succeeded = false
+
+ shareDelegate.showShareSheet(
+ context,
+ ShareData(title = "Title", text = "Text", url = null),
+ onDismiss = { dismissed = true },
+ onSuccess = { succeeded = true },
+ )
+
+ verify(context).startActivity(any())
+ assertFalse(dismissed)
+ assertTrue(succeeded)
+ }
+
+ @Test
+ fun `calls onDismiss after share chooser throws error`() {
+ val context = spy(testContext)
+ doThrow(ActivityNotFoundException()).`when`(context).startActivity(any())
+
+ var dismissed = false
+ var succeeded = false
+
+ shareDelegate.showShareSheet(
+ context,
+ ShareData(title = null, text = "Text", url = "https://example.com"),
+ onDismiss = { dismissed = true },
+ onSuccess = { succeeded = true },
+ )
+
+ assertTrue(dismissed)
+ assertFalse(succeeded)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/MonthAndYearPickerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/MonthAndYearPickerTest.kt
new file mode 100644
index 0000000000..12dd30c808
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/MonthAndYearPickerTest.kt
@@ -0,0 +1,196 @@
+/* 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.prompts.widget
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.ext.month
+import mozilla.components.feature.prompts.ext.now
+import mozilla.components.feature.prompts.ext.toCalendar
+import mozilla.components.feature.prompts.ext.year
+import mozilla.components.feature.prompts.widget.MonthAndYearPicker.Companion.DEFAULT_MAX_YEAR
+import mozilla.components.feature.prompts.widget.MonthAndYearPicker.Companion.DEFAULT_MIN_YEAR
+import mozilla.components.support.ktx.kotlin.toDate
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Calendar.DECEMBER
+import java.util.Calendar.FEBRUARY
+import java.util.Calendar.JANUARY
+
+@RunWith(AndroidJUnit4::class)
+class MonthAndYearPickerTest {
+
+ @Test
+ fun `WHEN picker widget THEN initial values must be displayed`() {
+ val initialDate = "2018-06".toDate("yyyy-MM").toCalendar()
+ val minDate = "2018-04".toDate("yyyy-MM").toCalendar()
+ val maxDate = "2018-09".toDate("yyyy-MM").toCalendar()
+
+ val monthAndYearPicker = MonthAndYearPicker(
+ context = testContext,
+ selectedDate = initialDate,
+ minDate = minDate,
+ maxDate = maxDate,
+ )
+
+ with(monthAndYearPicker.monthView) {
+ assertEquals(initialDate.month, value)
+ assertEquals(minDate.month, minValue)
+ assertEquals(maxDate.month, maxValue)
+ }
+
+ with(monthAndYearPicker.yearView) {
+ assertEquals(initialDate.year, value)
+ assertEquals(minDate.year, minValue)
+ assertEquals(maxDate.year, maxValue)
+ }
+ }
+
+ @Test
+ fun `WHEN selectedDate is a year less than maxDate THEN month picker MUST allow selecting until the last month of the year`() {
+ val initialDate = "2018-06".toDate("yyyy-MM").toCalendar()
+ val minDate = "2018-04".toDate("yyyy-MM").toCalendar()
+ val maxDate = "2019-09".toDate("yyyy-MM").toCalendar()
+
+ val monthAndYearPicker = MonthAndYearPicker(
+ context = testContext,
+ selectedDate = initialDate,
+ minDate = minDate,
+ maxDate = maxDate,
+ )
+
+ with(monthAndYearPicker.monthView) {
+ assertEquals(initialDate.month, value)
+ assertEquals(minDate.month, minValue)
+ assertEquals(DECEMBER, maxValue)
+ }
+
+ with(monthAndYearPicker.yearView) {
+ assertEquals(initialDate.year, value)
+ assertEquals(minDate.year, minValue)
+ assertEquals(maxDate.year, maxValue)
+ }
+ }
+
+ @Test
+ fun `WHEN changing month picker from DEC to JAN THEN year picker MUST be increased by 1`() {
+ val initialDate = "2018-06".toDate("yyyy-MM")
+ val initialCal = "2018-06".toDate("yyyy-MM").toCalendar()
+
+ val monthAndYearPicker = MonthAndYearPicker(
+ context = testContext,
+ selectedDate = initialDate.toCalendar(),
+ )
+
+ val yearView = monthAndYearPicker.yearView
+ assertEquals(initialCal.year, yearView.value)
+
+ monthAndYearPicker.onValueChange(monthAndYearPicker.monthView, DECEMBER, JANUARY)
+
+ assertEquals(initialCal.year + 1, yearView.value)
+ }
+
+ @Test
+ fun `WHEN changing month picker from JAN to DEC THEN year picker MUST be decreased by 1`() {
+ val initialDate = "2018-06".toDate("yyyy-MM")
+ val initialCal = "2018-06".toDate("yyyy-MM").toCalendar()
+
+ val monthAndYearPicker = MonthAndYearPicker(
+ context = testContext,
+ selectedDate = initialDate.toCalendar(),
+ )
+
+ val yearView = monthAndYearPicker.yearView
+ assertEquals(initialCal.year, yearView.value)
+
+ monthAndYearPicker.onValueChange(monthAndYearPicker.monthView, JANUARY, DECEMBER)
+
+ assertEquals(initialCal.year - 1, yearView.value)
+ }
+
+ @Test
+ fun `WHEN selecting a month or a year THEN dateSetListener MUST be notified`() {
+ val initialDate = "2018-06".toDate("yyyy-MM")
+ val initialCal = "2018-06".toDate("yyyy-MM").toCalendar()
+
+ val monthAndYearPicker = MonthAndYearPicker(
+ context = testContext,
+ selectedDate = initialDate.toCalendar(),
+ )
+
+ var newMonth = 0
+ var newYear = 0
+
+ monthAndYearPicker.dateSetListener = object : MonthAndYearPicker.OnDateSetListener {
+ override fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int) {
+ newMonth = month
+ newYear = year
+ }
+ }
+
+ assertEquals(0, newMonth)
+ assertEquals(0, newYear)
+
+ val yearView = monthAndYearPicker.yearView
+ val monthView = monthAndYearPicker.monthView
+
+ monthAndYearPicker.onValueChange(yearView, initialCal.year - 1, initialCal.year + 1)
+
+ assertEquals(initialCal.year + 1, newYear)
+
+ monthAndYearPicker.onValueChange(monthView, JANUARY, FEBRUARY)
+
+ assertEquals(FEBRUARY + 1, newMonth) // Month is zero based
+ }
+
+ @Test
+ fun `WHEN max or min date are in a illogical range THEN picker must allow to select the default values for max and min`() {
+ val initialDate = "2018-06".toDate("yyyy-MM").toCalendar()
+ val minDate = "2019-04".toDate("yyyy-MM").toCalendar()
+ val maxDate = "2018-09".toDate("yyyy-MM").toCalendar()
+
+ val monthAndYearPicker = MonthAndYearPicker(
+ context = testContext,
+ selectedDate = initialDate,
+ minDate = minDate,
+ maxDate = maxDate,
+ )
+
+ with(monthAndYearPicker.monthView) {
+ assertEquals(JANUARY, minValue)
+ assertEquals(DECEMBER, maxValue)
+ }
+
+ with(monthAndYearPicker.yearView) {
+ assertEquals(DEFAULT_MIN_YEAR, minValue)
+ assertEquals(DEFAULT_MAX_YEAR, maxValue)
+ }
+ }
+
+ @Test
+ fun `WHEN selecting a date that is before or after min or max date THEN selectDate will be set to min date`() {
+ val minDate = "2018-04".toDate("yyyy-MM").toCalendar()
+ val maxDate = "2018-09".toDate("yyyy-MM").toCalendar()
+ val initialDate = now()
+
+ initialDate.year = minDate.year - 1
+
+ val monthAndYearPicker = MonthAndYearPicker(
+ context = testContext,
+ selectedDate = initialDate,
+ minDate = minDate,
+ maxDate = maxDate,
+ )
+
+ with(monthAndYearPicker.monthView) {
+ assertEquals(minDate.month, value)
+ }
+
+ with(monthAndYearPicker.yearView) {
+ assertEquals(minDate.year, value)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/TimePrecisionPickerTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/TimePrecisionPickerTest.kt
new file mode 100644
index 0000000000..43e462b679
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/TimePrecisionPickerTest.kt
@@ -0,0 +1,106 @@
+/* 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.prompts.widget
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.prompts.ext.hour
+import mozilla.components.feature.prompts.ext.millisecond
+import mozilla.components.feature.prompts.ext.minute
+import mozilla.components.feature.prompts.ext.now
+import mozilla.components.feature.prompts.ext.second
+import mozilla.components.feature.prompts.ext.toCalendar
+import mozilla.components.support.ktx.kotlin.toDate
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TimePrecisionPickerTest {
+ private val initialTime = "12:00".toDate("HH:mm").toCalendar()
+ private var minTime = "10:30".toDate("HH:mm").toCalendar()
+ private var maxTime = "18:45".toDate("HH:mm").toCalendar()
+ private val stepValue = 0.1f
+
+ @Test
+ fun `WHEN picker widget THEN initial values must be displayed`() {
+ val timePicker = TimePrecisionPicker(
+ context = testContext,
+ selectedTime = initialTime,
+ stepValue = stepValue,
+ )
+
+ assertEquals(initialTime.hour, timePicker.hourView.value)
+ assertEquals(initialTime.minute, timePicker.minuteView.value)
+ }
+
+ @Test
+ fun `WHEN selectedTime is outside the bounds of min and max time THEN the displayed time is minTime`() {
+ minTime = "14:30".toDate("HH:mm").toCalendar()
+ val timePicker = TimePrecisionPicker(
+ context = testContext,
+ selectedTime = initialTime,
+ minTime = minTime,
+ maxTime = maxTime,
+ stepValue = stepValue,
+ )
+
+ assertEquals(minTime.hour, timePicker.hourView.value)
+ assertEquals(minTime.minute, timePicker.minuteView.value)
+ }
+
+ @Test
+ fun `WHEN minTime and maxTime are in illogical order AND selectedTime is outside limits THEN the min and max limits are ignored when initializing the time`() {
+ minTime = "15:30".toDate("HH:mm").toCalendar()
+ maxTime = "09:30".toDate("HH:mm").toCalendar()
+ val timePicker = TimePrecisionPicker(
+ context = testContext,
+ selectedTime = initialTime,
+ minTime = minTime,
+ maxTime = maxTime,
+ stepValue = stepValue,
+ )
+
+ assertEquals(initialTime.hour, timePicker.hourView.value)
+ assertEquals(initialTime.minute, timePicker.minuteView.value)
+ }
+
+ @Test
+ fun `WHEN changing the selected time THEN timeSetListener MUST be notified`() {
+ val updatedTime = now()
+
+ val timePicker = TimePrecisionPicker(
+ context = testContext,
+ selectedTime = initialTime,
+ minTime = minTime,
+ maxTime = maxTime,
+ stepValue = stepValue,
+ timeSetListener = object : TimePrecisionPicker.OnTimeSetListener {
+ override fun onTimeSet(
+ picker: TimePrecisionPicker,
+ hour: Int,
+ minute: Int,
+ second: Int,
+ millisecond: Int,
+ ) {
+ updatedTime.hour = hour
+ updatedTime.minute = minute
+ updatedTime.second = second
+ updatedTime.millisecond = millisecond
+ }
+ },
+ )
+
+ timePicker.onValueChange(timePicker.hourView, initialTime.hour, 13)
+ timePicker.onValueChange(timePicker.minuteView, initialTime.minute, 20)
+ timePicker.onValueChange(timePicker.secondView, initialTime.second, 30)
+ timePicker.onValueChange(timePicker.millisecondView, initialTime.millisecond, 100)
+
+ assertEquals(13, updatedTime.hour)
+ assertEquals(20, updatedTime.minute)
+ assertEquals(30, updatedTime.second)
+ assertEquals(100, updatedTime.millisecond)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/prompts/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/prompts/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/prompts/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/prompts/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/prompts/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/prompts/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/push/README.md b/mobile/android/android-components/components/feature/push/README.md
new file mode 100644
index 0000000000..292f0e2478
--- /dev/null
+++ b/mobile/android/android-components/components/feature/push/README.md
@@ -0,0 +1,154 @@
+# [Android Components](../../../README.md) > Feature > Push
+
+A component that implements push notifications with a supported push service.
+
+## Usage
+
+Add a supported push service for providing the encrypted messages (for example, Firebase Cloud Messaging via `lib-push-firebase`):
+```kotlin
+class FirebasePush : AbstractFirebasePushService()
+```
+
+Create a push configuration with the project info and also place the required service's API keys in the project directory:
+
+```kotlin
+PushConfig(
+ senderId = "push-test-f408f",
+ serverHost = "updates.push.services.mozilla.com",
+ serviceType = ServiceType.FCM,
+ protocol = Protocol.HTTPS
+)
+```
+
+We can then start the AutoPushFeature to get the subscription info and decrypted push message:
+```kotlin
+val service = FirebasePush()
+
+val feature = AutoPushFeature(
+ context = context,
+ service = pushService,
+ config = pushConfig
+)
+
+// To start the feature and the service.
+feature.initialize()
+
+// To stop the feature and the service.
+feature.shutdown()
+
+// To receive the subscription info for all the subscription changes.
+feature.register(object : AutoPushFeature.Observer {
+ override fun onSubscriptionChanged(scope: PushScope) {
+ // Handle subscription info here.
+ }
+})
+
+// Subscribe for a unique scope (identifier).
+feature.subscribe("push_subscription_scope_id")
+
+// To receive messages:
+feature.register(object : AutoPushFeature.Observer {
+ override fun onMessageReceived(scope: String, message: ByteArray?) {
+ // Handle decrypted message here.
+ }
+})
+```
+
+### 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-push:{latest-version}"
+```
+
+## Implementation Notes
+
+The features of WebPush/AutoPush are to:
+1. Implement the WebPush specification for Mozilla services/products.
+2. Provide a bridge between a mobile or desktop client and server app without the server needing to know about ecosystem-specific push providers (FCM/APN/ADM).
+3. End-to-end encryption between client app and server app.
+
+### Key actors & processes
+Below is a sequence diagram for the four main processes that take place for push messaging:
+1. (First-install) initialization
+2. Creating a WebPush subscription
+3. Sending WebPush message
+4. Un-subscribing (e.g. account log-out)
+
+* **Client App**: this is your Android device that includes the `AutoPushFeature` that contains the AutoPush rust component to create/delete subscriptions and encrypt/decrypt messages. As well as the`lib-push-firebase` which is the push service bridge.
+* **Server App** - the application server that has a web push server implementation.
+* **AutoPush** - the server bridge between app servers and their clients.
+* **Push Provider** - the platform push service that does the "last-mile" message delivered.
+
+![generated sequence diagram](assets/autopush-sequence-diagram.png)
+
+<details>
+
+<summary>Sequence diagram source code</summary>
+
+<!-- Github Markdown has support for rendering mermaid graphs; use mermaid.js.org to generate output for the diagram alternatively -->
+
+```mermaid
+sequenceDiagram
+ participant Device as Client App
+ participant AutoPush
+ participant Provider as Push Provider (FCM/APN)
+ participant Server as Server App
+ rect rgb(191, 223, 255)
+ Note over Device,Server: Initialization
+ Note right of Device: Generate pub-priv keys
+ Device->>Provider: Request device registration token
+ Provider-->>Device: Receive device registration token
+ Device->>AutoPush: Send token
+ end
+ rect rgb(191, 223, 255)
+ Note over Device,Server: Creating a push subscription
+ Device->>AutoPush: Request subscription endpoint
+ AutoPush-->>Device: Receive subscription
+ Device->>Server: Send subscription endpoint + public key
+ end
+ rect rgb(191, 223, 255)
+ Note over Device,Server: Sending WebPush message
+ Note left of Server: Encrypt message
+ Server->>AutoPush: Send encrypted message
+ AutoPush->>Provider: Forward encrypted message
+ Provider->>Device: Deliver encrypted message
+ Note right of Device: Decrypt message
+ end
+ rect rgb(191, 223, 255)
+ Note over Device,Server: Un-subscribing (e.g. account log-out)
+ Device->>AutoPush: Unsubscribe
+ Device->>Server: Unsubscribe (notify server that the subscription is dead)
+ end
+```
+
+</details>
+
+### Miscellaneous
+
+Q. Why do we need to verify connections, and what happens when we do?
+- Various services may need to communicate with us via push messages. Examples: FxA events (send tab, etc), WebPush (a web app receives a push message from its server).
+- To send these push messages, services (FxA, random internet servers talking to their web apps) post an HTTP request to a "push endpoint" maintained by [Mozilla's Autopush service][0]. This push endpoint is specific to its recipient - so one instance of an app may have many endpoints associated with it: one for the current FxA device, a few for web apps, etc.
+- Important point here: servers (FxA, services behind web apps, etc.) need to be told about subscription info we get from Autopush.
+- Here is where things start to get complicated: client (us) and server (Autopush) may disagree on which channels are associated with the current UAID (remember: our subscriptions are per-channel). Channels may expire (TTL'd) or may be deleted by some server's Cron job if they're unused. For example, if this happens, services that use this subscription info (e.g. FxA servers) to communication with their clients (FxA devices) will fail to deliver push messages.
+- So the client needs to be able to find out that this is the case, re-create channel subscriptions on Autopush, and update any dependent services with new subscription info (e.g. update the FxA device record for `PushType.Services`, or notify the JS code with a `pushsubscriptionchanged` event for WebPush).
+- The Autopush side of this is `verify_connection` API - we're expected to call this periodically, and that library will compare channel registrations that the server knows about vs those that the client knows about.
+- If those are misaligned, we need to re-register affected (or, all?) channels, and notify related services so that they may update their own server-side records.
+- For FxA, this means that we need to have an instance of the rust FirefoxAccount object around in order to call `setDevicePushSubscriptionAsync` once we re-generate our push subscription.
+- For consumers such as Fenix, easiest way to access that method is via an `account manager`.
+- However, neither account object itself, nor the account manager, aren't available from within a Worker. It's possible to "re-hydrate" (instantiate rust object from the locally persisted state) a FirefoxAccount instance, but that's a separate can of worms, and needs to be carefully considered.
+- Similarly for WebPush (in the future), we will need to have Gecko around in order to fire `pushsubscriptionchanged` javascript events.
+
+Q. Where do we find more details about AutoPush?
+- The Cloud Services team have [an architecture doc][0] for in-depth details on how the AutoPush server works with clients.
+
+[0]: https://autopush.readthedocs.io/en/latest/architecture.html
+
+## 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/
+
+[0]: https://github.com/mozilla-services/autopush
diff --git a/mobile/android/android-components/components/feature/push/assets/autopush-sequence-diagram.png b/mobile/android/android-components/components/feature/push/assets/autopush-sequence-diagram.png
new file mode 100644
index 0000000000..4ffc961d31
--- /dev/null
+++ b/mobile/android/android-components/components/feature/push/assets/autopush-sequence-diagram.png
Binary files differ
diff --git a/mobile/android/android-components/components/feature/push/build.gradle b/mobile/android/android-components/components/feature/push/build.gradle
new file mode 100644
index 0000000000..d1003afad1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/push/build.gradle
@@ -0,0 +1,48 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.push'
+}
+
+
+dependencies {
+ implementation project(':concept-push')
+
+ implementation ComponentsDependencies.mozilla_appservices_push
+
+ // Remove when the MessageBus is implemented somewhere else.
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_work_runtime
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/push/proguard-rules.pro b/mobile/android/android-components/components/feature/push/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/push/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/push/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/push/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/push/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/push/src/main/java/mozilla/components/feature/push/AutoPushFeature.kt b/mobile/android/android-components/components/feature/push/src/main/java/mozilla/components/feature/push/AutoPushFeature.kt
new file mode 100644
index 0000000000..23aa1eab89
--- /dev/null
+++ b/mobile/android/android-components/components/feature/push/src/main/java/mozilla/components/feature/push/AutoPushFeature.kt
@@ -0,0 +1,412 @@
+/* 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.push
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
+import mozilla.appservices.push.BridgeType
+import mozilla.appservices.push.PushApiException
+import mozilla.appservices.push.PushApiException.UaidNotRecognizedException
+import mozilla.appservices.push.PushConfiguration
+import mozilla.appservices.push.PushHttpProtocol
+import mozilla.appservices.push.PushManager
+import mozilla.appservices.push.PushManagerInterface
+import mozilla.appservices.push.SubscriptionResponse
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.push.PushError
+import mozilla.components.concept.push.PushProcessor
+import mozilla.components.concept.push.PushService
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import mozilla.components.support.base.utils.NamedThreadFactory
+import java.io.File
+import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
+
+typealias PushScope = String
+typealias AppServerKey = String
+
+/**
+ * A implementation of a [PushProcessor] that should live as a singleton by being installed
+ * in the Application's onCreate. It receives messages from a service and forwards them
+ * to be decrypted and routed.
+ *
+ * ```kotlin
+ * class Application {
+ * override fun onCreate() {
+ * val feature = AutoPushFeature(context, service, configuration)
+ * PushProvider.install(push)
+ * }
+ * }
+ * ```
+ *
+ * Observe for subscription information changes for each registered scope:
+ *
+ * ```kotlin
+ * feature.register(object: AutoPushFeature.Observer {
+ * override fun onSubscriptionChanged(scope: PushScope) { }
+ * })
+ *
+ * feature.subscribe("push_subscription_scope_id")
+ * ```
+ *
+ * You should also observe for push messages:
+ *
+ * ```kotlin
+ * feature.register(object: AutoPushFeature.Observer {
+ * override fun onMessageReceived(scope: PushScope, message: ByteArray?) { }
+ * })
+ * ```
+ *
+ * @param context the application [Context].
+ * @param service A [PushService] bridge that receives the encrypted push messages - eg, Firebase.
+ * @param config An instance of [PushConfig] to configure the feature.
+ * @param coroutineContext An instance of [CoroutineContext] used for executing async push tasks.
+ * @param crashReporter An optional instance of a [CrashReporting].
+ */
+
+@Suppress("LargeClass")
+class AutoPushFeature(
+ private val context: Context,
+ private val service: PushService,
+ val config: PushConfig,
+ coroutineContext: CoroutineContext = Executors.newSingleThreadExecutor(
+ NamedThreadFactory("AutoPushFeature"),
+ ).asCoroutineDispatcher(),
+ private val crashReporter: CrashReporting? = null,
+) : PushProcessor, Observable<AutoPushFeature.Observer> by ObserverRegistry() {
+
+ private val logger = Logger("AutoPushFeature")
+
+ // The preference that stores new registration tokens.
+ private val prefToken: String?
+ get() = preferences(context).getString(PREF_TOKEN, null)
+
+ private val coroutineScope = CoroutineScope(coroutineContext) + SupervisorJob() + exceptionHandler { onError(it) }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var connection: PushManagerInterface? = null
+
+ /**
+ * Starts the push feature and initialization work needed. Also starts the [PushService] to ensure new messages
+ * come through.
+ */
+ override fun initialize() {
+ // If we have a token, initialize the rust component on a different thread.
+ coroutineScope.launch {
+ if (connection == null) {
+ val databasePath = File(context.filesDir, DB_NAME).canonicalPath
+ connection = PushManager(
+ PushConfiguration(
+ serverHost = config.serverHost,
+ httpProtocol = config.protocol.toRustHttpProtocol(),
+ bridgeType = config.serviceType.toBridgeType(),
+ senderId = config.senderId,
+ databasePath = databasePath,
+ // Default is one request in 24 hours
+ verifyConnectionRateLimiter = null,
+ ),
+ )
+ }
+ prefToken?.let { token ->
+ logger.debug("Initializing rust component with the cached token.")
+ connection?.update(token)
+ verifyActiveSubscriptions()
+ }
+ }
+ // Starts the (FCM) push feature so that we receive messages if the service is not already started (safe call).
+ service.start(context)
+ }
+
+ /**
+ * Un-subscribes from all push message channels and stops periodic verifications.
+ *
+ * We do not stop the push service in case there are other consumers are using it as well. The app should
+ * explicitly stop the service if desired.
+ *
+ * This should only be done on an account logout or app data deletion.
+ */
+ override fun shutdown() {
+ withConnection {
+ it.unsubscribeAll()
+ }
+ }
+
+ /**
+ * New registration tokens are received and sent to the AutoPush server which also performs subscriptions for
+ * each push type and notifies the subscribers.
+ */
+ override fun onNewToken(newToken: String) {
+ val currentConnection = connection
+ coroutineScope.launch {
+ logger.info("Received a new registration token from push service.")
+
+ saveToken(context, newToken)
+
+ // Tell the autopush service about it and update subscriptions.
+ currentConnection?.update(newToken)
+ verifyActiveSubscriptions()
+ }
+ }
+
+ /**
+ * New encrypted messages received from a supported push messaging service.
+ */
+ override fun onMessageReceived(message: Map<String, String>) {
+ withConnection {
+ val decryptResponse = it.decrypt(
+ payload = message,
+ )
+ logger.info("New push message decrypted.")
+ notifyObservers { onMessageReceived(decryptResponse.scope, decryptResponse.result.toByteArray()) }
+ }
+ }
+
+ override fun onError(error: PushError) {
+ logger.error("${error.javaClass.simpleName} error: ${error.message}")
+
+ crashReporter?.submitCaughtException(error)
+ }
+
+ /**
+ * Subscribes for push notifications and invokes the [onSubscribe] callback with the subscription information.
+ *
+ * @param scope The subscription identifier which usually represents the website's URI.
+ * @param appServerKey An optional key provided by the application server.
+ * @param onSubscribeError The callback invoked with an [Exception] if the call does not successfully complete.
+ * @param onSubscribe The callback invoked when a subscription for the [scope] is created.
+ */
+ fun subscribe(
+ scope: String,
+ appServerKey: String? = null,
+ onSubscribeError: (Exception) -> Unit = {},
+ onSubscribe: ((AutoPushSubscription) -> Unit) = {},
+ ) {
+ withConnection(errorBlock = { exception -> onSubscribeError(exception) }) {
+ val sub = it.subscribe(scope, appServerKey ?: "")
+ onSubscribe(sub.toPushSubscription(scope, appServerKey ?: ""))
+ }
+ }
+
+ /**
+ * Un-subscribes from a valid subscription and invokes the [onUnsubscribe] callback with the result.
+ *
+ * @param scope The subscription identifier which usually represents the website's URI.
+ * @param onUnsubscribeError The callback invoked with an [Exception] if the call does not successfully complete.
+ * @param onUnsubscribe The callback invoked when a subscription for the [scope] is removed.
+ */
+ fun unsubscribe(
+ scope: String,
+ onUnsubscribeError: (Exception) -> Unit = {},
+ onUnsubscribe: (Boolean) -> Unit = {},
+ ) {
+ withConnection(errorBlock = { exception -> onUnsubscribeError(exception) }) {
+ onUnsubscribe(it.unsubscribe(scope))
+ }
+ }
+
+ /**
+ * Checks if a subscription for the [scope] already exists.
+ *
+ * @param scope The subscription identifier which usually represents the website's URI.
+ * @param appServerKey An optional key provided by the application server.
+ * @param block The callback invoked when a subscription for the [scope] is found, otherwise null. Note: this will
+ * not execute on the calls thread.
+ */
+ fun getSubscription(
+ scope: String,
+ appServerKey: String? = null,
+ block: (AutoPushSubscription?) -> Unit,
+ ) {
+ withConnection {
+ block(it.getSubscription(scope)?.toPushSubscription(scope, appServerKey))
+ }
+ }
+
+ /**
+ * Deletes the FCM registration token locally so that it forces the service to get a new one the
+ * next time hits it's messaging server.
+ * XXX - this is suspect - the only caller of this is FxA, and it calls it when the device
+ * record indicates the end-point is expired. If that's truly necessary, then it will mean
+ * push never recovers for non-FxA users. If that's not truly necessary, we should remove it!
+ */
+ override fun renewRegistration() {
+ logger.warn("Forcing FCM registration renewal by deleting our (cached) token.")
+
+ // Remove the cached token we have.
+ deleteToken(context)
+
+ // Tell the service to delete the token as well, which will trigger a new token to be
+ // retrieved the next time it hits the server.
+ service.deleteToken()
+
+ // Starts the service if needed to trigger a new registration.
+ service.start(context)
+ }
+
+ /**
+ * Verifies status (active, expired) of the push subscriptions and then notifies observers.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun verifyActiveSubscriptions(forceVerify: Boolean = false) {
+ withConnection {
+ val subscriptionChanges = it.verifyConnection(forceVerify)
+
+ if (subscriptionChanges.isNotEmpty()) {
+ logger.info("Subscriptions have changed; notifying observers..")
+
+ subscriptionChanges.forEach { sub ->
+ notifyObservers { onSubscriptionChanged(sub.scope) }
+ }
+ } else {
+ logger.info("No change to subscriptions. Doing nothing.")
+ }
+ }
+ }
+
+ private fun saveToken(context: Context, value: String) {
+ preferences(context).edit().putString(PREF_TOKEN, value).apply()
+ }
+
+ private fun deleteToken(context: Context) {
+ preferences(context).edit().remove(PREF_TOKEN).apply()
+ }
+
+ private fun preferences(context: Context): SharedPreferences =
+ context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
+
+ /**
+ * Observers that want to receive updates for new subscriptions and messages.
+ */
+ interface Observer {
+
+ /**
+ * A subscription for the scope is available.
+ */
+ fun onSubscriptionChanged(scope: PushScope) = Unit
+
+ /**
+ * A messaged has been received for the [scope].
+ */
+ fun onMessageReceived(scope: PushScope, message: ByteArray?) = Unit
+ }
+
+ private fun exceptionHandler(onError: (PushError) -> Unit) = CoroutineExceptionHandler { _, e ->
+ when (e) {
+ is UaidNotRecognizedException,
+ -> onError(PushError.Rust(e, e.message.orEmpty()))
+ else -> logger.warn("Internal error occurred in AutoPushFeature.", e)
+ }
+ }
+
+ companion object {
+ internal const val PREFERENCE_NAME = "mozac_feature_push"
+ internal const val PREF_TOKEN = "token"
+ internal const val DB_NAME = "push.sqlite"
+ }
+
+ private fun withConnection(errorBlock: (Exception) -> Unit = {}, block: (PushManagerInterface) -> Unit) {
+ val currentConnection = connection
+ currentConnection?.let {
+ coroutineScope.launch {
+ try {
+ block(it)
+ } catch (e: PushApiException) {
+ errorBlock(e)
+
+ // rethrow
+ throw e
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Supported push services. This are currently limited to Firebase Cloud Messaging and
+ * (previously) Amazon Device Messaging.
+ */
+enum class ServiceType {
+ FCM,
+}
+
+/**
+ * Supported network protocols.
+ */
+enum class Protocol {
+ HTTP,
+ HTTPS,
+}
+
+/**
+ * The subscription information from AutoPush that can be used to send push messages to other devices.
+ */
+data class AutoPushSubscription(
+ val scope: PushScope,
+ val endpoint: String,
+ val publicKey: String,
+ val authKey: String,
+ val appServerKey: String?,
+)
+
+/**
+ * Configuration object for initializing the Push Manager with an AutoPush server.
+ *
+ * @param senderId The project identifier set by the server. Contact your server ops team to know what value to set.
+ * @param serverHost The sync server address.
+ * @param protocol The socket protocol to use when communicating with the server.
+ * @param serviceType The push services that the AutoPush server supports.
+ * @param disableRateLimit A flag to disable our rate-limit logic. This is useful when debugging.
+ */
+data class PushConfig(
+ val senderId: String,
+ val serverHost: String = "updates.push.services.mozilla.com",
+ val protocol: Protocol = Protocol.HTTPS,
+ val serviceType: ServiceType = ServiceType.FCM,
+ val disableRateLimit: Boolean = false,
+)
+
+/**
+ * Helper function to get the corresponding support [BridgeType] from the support set.
+ */
+@VisibleForTesting
+internal fun ServiceType.toBridgeType() = when (this) {
+ ServiceType.FCM -> BridgeType.FCM
+}
+
+/**
+ * A helper to convert the internal data class.
+ */
+private fun Protocol.toRustHttpProtocol(): PushHttpProtocol {
+ return when (this) {
+ Protocol.HTTPS -> PushHttpProtocol.HTTPS
+ Protocol.HTTP -> PushHttpProtocol.HTTP
+ }
+}
+
+/**
+ * A helper to convert the internal data class.
+ */
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+fun SubscriptionResponse.toPushSubscription(
+ scope: String,
+ appServerKey: AppServerKey? = null,
+): AutoPushSubscription {
+ return AutoPushSubscription(
+ scope = scope,
+ endpoint = subscriptionInfo.endpoint,
+ authKey = subscriptionInfo.keys.auth,
+ publicKey = subscriptionInfo.keys.p256dh,
+ appServerKey = appServerKey,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureTest.kt b/mobile/android/android-components/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureTest.kt
new file mode 100644
index 0000000000..f6d49600f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureTest.kt
@@ -0,0 +1,489 @@
+/* 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.push
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.appservices.push.BridgeType
+import mozilla.appservices.push.DecryptResponse
+import mozilla.appservices.push.KeyInfo
+import mozilla.appservices.push.PushApiException
+import mozilla.appservices.push.PushManagerInterface
+import mozilla.appservices.push.PushSubscriptionChanged
+import mozilla.appservices.push.SubscriptionInfo
+import mozilla.appservices.push.SubscriptionResponse
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.push.PushError
+import mozilla.components.concept.push.PushService
+import mozilla.components.feature.push.AutoPushFeature.Companion.PREFERENCE_NAME
+import mozilla.components.feature.push.AutoPushFeature.Companion.PREF_TOKEN
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.nullable
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+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.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class AutoPushFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val connection: PushManagerInterface = mock()
+
+ @Test
+ fun `initialize starts push service`() {
+ val service: PushService = mock()
+ val config = PushConfig("push-test")
+ val feature = AutoPushFeature(testContext, service, config)
+ feature.connection = connection
+
+ feature.initialize()
+
+ verify(service).start(testContext)
+
+ verifyNoMoreInteractions(service)
+ }
+
+ @Test
+ fun `updateToken not called if no token in prefs`() = runTestOnMain {
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ feature.connection = connection
+
+ verify(connection, never()).update(anyString())
+ }
+
+ @Test
+ fun `updateToken called if token is in prefs`() = runTestOnMain {
+ preference(testContext).edit().putString(PREF_TOKEN, "token").apply()
+
+ val feature = AutoPushFeature(
+ testContext,
+ mock(),
+ mock(),
+ coroutineContext = coroutineContext,
+ )
+
+ feature.connection = connection
+
+ feature.initialize()
+
+ verify(connection).update("token")
+ }
+
+ @Test
+ fun `shutdown stops service and unsubscribes all`() = runTestOnMain {
+ val service: PushService = mock()
+
+ AutoPushFeature(testContext, service, mock(), coroutineContext).also {
+ it.connection = connection
+ it.shutdown()
+ }
+
+ verify(connection).unsubscribeAll()
+ }
+
+ @Test
+ fun `onNewToken updates connection and saves pref`() = runTestOnMain {
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ feature.connection = connection
+
+ whenever(connection.subscribe(anyString(), nullable())).thenReturn(mock())
+
+ feature.onNewToken("token")
+
+ verify(connection).update("token")
+
+ val pref = preference(testContext).getString(PREF_TOKEN, null)
+ assertNotNull(pref)
+ assertEquals("token", pref)
+ }
+
+ @Test
+ fun `onMessageReceived decrypts message and notifies observers`() = runTestOnMain {
+ val encryptedMessage: Map<String, String> = mock()
+ val owner: LifecycleOwner = mock()
+ val lifecycle: Lifecycle = mock()
+ val observer: AutoPushFeature.Observer = mock()
+ whenever(owner.lifecycle).thenReturn(lifecycle)
+ whenever(lifecycle.currentState).thenReturn(Lifecycle.State.STARTED)
+ whenever(connection.decrypt(any()))
+ .thenReturn(null) // If we get null, we shouldn't notify observers.
+ .thenReturn(DecryptResponse(result = "test".toByteArray().asList(), scope = "testScope"))
+
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ feature.connection = connection
+ feature.register(observer)
+
+ feature.onMessageReceived(encryptedMessage)
+
+ verify(observer, never()).onMessageReceived("testScope", "test".toByteArray())
+
+ feature.onMessageReceived(encryptedMessage)
+
+ verify(observer).onMessageReceived("testScope", "test".toByteArray())
+ }
+
+ @Test
+ fun `subscribe calls native layer and notifies observers`() = runTestOnMain {
+ val connection: PushManagerInterface = mock()
+
+ var invoked = false
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ whenever(connection.subscribe(any(), any())).thenReturn(
+ SubscriptionResponse(
+ channelId = "test-cid",
+ subscriptionInfo = SubscriptionInfo(
+ endpoint = "https://foo",
+ keys = KeyInfo(auth = "auth", p256dh = "p256dh"),
+ ),
+ ),
+ )
+ feature.connection = connection
+
+ feature.subscribe("testScope") {
+ invoked = true
+ }
+
+ assertTrue(invoked)
+ }
+
+ @Test
+ fun `subscribe invokes error callback`() = runTestOnMain {
+ val subscription: AutoPushSubscription = mock()
+ var invoked = false
+ var errorInvoked = false
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ feature.connection = connection
+
+ feature.subscribe(
+ scope = "testScope",
+ onSubscribeError = {
+ errorInvoked = true
+ },
+ onSubscribe = {
+ invoked = true
+ },
+ )
+
+ assertFalse(invoked)
+ assertFalse(errorInvoked)
+
+ whenever(connection.subscribe(anyString(), nullable())).thenAnswer { throw PushApiException.InternalException("") }
+ whenever(subscription.scope).thenReturn("testScope")
+
+ feature.subscribe(
+ scope = "testScope",
+ onSubscribeError = {
+ errorInvoked = true
+ },
+ onSubscribe = {
+ invoked = true
+ },
+ )
+
+ assertFalse(invoked)
+ assertTrue(errorInvoked)
+ }
+
+ @Test
+ fun `unsubscribe calls native layer and notifies observers`() = runTestOnMain {
+ var invoked = false
+ var errorInvoked = false
+
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ feature.connection = connection
+ feature.unsubscribe(
+ scope = "testScope",
+ onUnsubscribeError = {
+ errorInvoked = true
+ },
+ onUnsubscribe = {
+ invoked = true
+ },
+ )
+
+ assertTrue(invoked)
+ assertFalse(errorInvoked)
+ }
+
+ @Test
+ fun `unsubscribe invokes error callback on native exception`() = runTestOnMain {
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ feature.connection = connection
+ var invoked = false
+ var errorInvoked = false
+
+ whenever(connection.unsubscribe(anyString())).thenAnswer { throw PushApiException.InternalException("") }
+
+ feature.unsubscribe(
+ scope = "testScope",
+ onUnsubscribeError = {
+ errorInvoked = true
+ },
+ onUnsubscribe = {
+ invoked = true
+ },
+ )
+
+ assertFalse(invoked)
+ assertTrue(errorInvoked)
+ }
+
+ @Test
+ fun `getSubscription returns null when there is no subscription`() = runTestOnMain {
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ feature.connection = connection
+ var invoked = false
+
+ whenever(connection.getSubscription(anyString())).thenReturn(null)
+
+ feature.getSubscription(
+ scope = "testScope",
+ appServerKey = null,
+ ) {
+ invoked = it == null
+ }
+
+ assertTrue(invoked)
+ }
+
+ @Test
+ fun `getSubscription invokes subscribe when there is a subscription`() = runTestOnMain {
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ feature.connection = connection
+ var invoked = false
+
+ whenever(connection.getSubscription(anyString())).thenReturn(
+ SubscriptionResponse(
+ channelId = "cid",
+ subscriptionInfo = SubscriptionInfo(
+ endpoint = "endpoint",
+ keys = KeyInfo(
+ auth = "auth",
+ p256dh = "p256dh",
+ ),
+ ),
+ ),
+ )
+
+ feature.getSubscription(
+ scope = "testScope",
+ appServerKey = null,
+ ) {
+ invoked = it != null
+ }
+
+ assertTrue(invoked)
+ }
+
+ @Test
+ fun `forceRegistrationRenewal deletes pref and calls service`() = runTestOnMain {
+ val service: PushService = mock()
+ val feature = AutoPushFeature(testContext, service, mock(), coroutineContext)
+ feature.connection = connection
+
+ feature.renewRegistration()
+
+ verify(service).deleteToken()
+ verify(service).start(testContext)
+
+ val pref = preference(testContext).getString(PREF_TOKEN, null)
+ assertNull(pref)
+ }
+
+ @Test
+ fun `verifyActiveSubscriptions notifies observers`() = runTestOnMain {
+ val connection: PushManagerInterface = mock()
+ val owner: LifecycleOwner = mock()
+ val lifecycle: Lifecycle = mock()
+ val observers: AutoPushFeature.Observer = mock()
+ val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext)
+ whenever(connection.verifyConnection()).thenReturn(emptyList())
+ feature.connection = connection
+ whenever(owner.lifecycle).thenReturn(lifecycle)
+ whenever(lifecycle.currentState).thenReturn(Lifecycle.State.STARTED)
+
+ feature.register(observers)
+
+ // When there are NO subscription updates, observers should not be notified.
+ feature.verifyActiveSubscriptions()
+
+ verify(observers, never()).onSubscriptionChanged(any())
+
+ // When there are no subscription updates, observers should not be notified.
+ whenever(connection.verifyConnection()).thenReturn(emptyList())
+ feature.verifyActiveSubscriptions()
+
+ verify(observers, never()).onSubscriptionChanged(any())
+
+ // When there are subscription updates, observers should be notified.
+ whenever(connection.verifyConnection()).thenReturn(listOf(PushSubscriptionChanged(scope = "scope", channelId = "1246")))
+ feature.verifyActiveSubscriptions()
+
+ verify(observers).onSubscriptionChanged("scope")
+ }
+
+ @Test
+ fun `new FCM token executes verifyActiveSubscription`() = runTestOnMain {
+ val feature = spy(
+ AutoPushFeature(
+ context = testContext,
+ service = mock(),
+ config = mock(),
+ coroutineContext = coroutineContext,
+ ),
+ )
+ feature.connection = connection
+
+ feature.initialize()
+ // no token yet so should not have even tried.
+ verify(feature, never()).verifyActiveSubscriptions()
+
+ // new token == "check now"
+ feature.onNewToken("test-token")
+ verify(feature).verifyActiveSubscriptions()
+ }
+
+ @Test
+ fun `verification doesn't happen until we've got the token`() = runTestOnMain {
+ val feature = spy(
+ AutoPushFeature(
+ context = testContext,
+ service = mock(),
+ config = mock(),
+ coroutineContext = coroutineContext,
+ ),
+ )
+
+ feature.connection = connection
+
+ feature.initialize()
+
+ verify(feature, never()).verifyActiveSubscriptions()
+ }
+
+ @Test
+ fun `crash reporter is notified of errors`() = runTestOnMain {
+ val connection: PushManagerInterface = mock()
+ val crashReporter: CrashReporting = mock()
+ val feature = AutoPushFeature(
+ context = testContext,
+ service = mock(),
+ config = mock(),
+ coroutineContext = coroutineContext,
+ crashReporter = crashReporter,
+ )
+ feature.connection = connection
+
+ feature.onError(PushError.Rust(PushError.MalformedMessage("Bad things happened!")))
+
+ verify(crashReporter).submitCaughtException(any<PushError.Rust>())
+ }
+
+ @Test
+ fun `Non-Internal errors are submitted to crash reporter`() = runTestOnMain {
+ val crashReporter: CrashReporting = mock()
+ val feature = AutoPushFeature(
+ context = testContext,
+ service = mock(),
+ config = mock(),
+ coroutineContext = coroutineContext,
+ crashReporter = crashReporter,
+ )
+
+ feature.connection = connection
+
+ whenever(connection.unsubscribe(any())).thenAnswer {
+ throw PushApiException.UaidNotRecognizedException("test")
+ }
+ feature.unsubscribe("123") {}
+
+ verify(crashReporter).submitCaughtException(any<PushError.Rust>())
+ }
+
+ @Test
+ fun `Internal errors errors are not reported`() = runTestOnMain {
+ val crashReporter: CrashReporting = mock()
+ val feature = AutoPushFeature(
+ context = testContext,
+ service = mock(),
+ config = mock(),
+ coroutineContext = coroutineContext,
+ crashReporter = crashReporter,
+ )
+
+ feature.connection = connection
+
+ whenever(connection.unsubscribe(any())).thenAnswer { throw PushApiException.InternalException("") }
+
+ feature.unsubscribe("123") {}
+
+ verify(crashReporter, never()).submitCaughtException(any<PushError.Rust>())
+ }
+
+ @Test
+ fun `asserts PushConfig's default values`() {
+ val config = PushConfig("sample-browser")
+ assertEquals("sample-browser", config.senderId)
+ assertEquals("updates.push.services.mozilla.com", config.serverHost)
+ assertEquals(Protocol.HTTPS, config.protocol)
+ assertEquals(ServiceType.FCM, config.serviceType)
+ }
+
+ @Test
+ fun `transform response to PushSubscription`() {
+ val response = SubscriptionResponse(
+ "992a0f0542383f1ea5ef51b7cf4ae6c4",
+ SubscriptionInfo("https://mozilla.com", KeyInfo("123", "456")),
+ )
+ val sub = response.toPushSubscription("scope")
+
+ assertEquals(response.subscriptionInfo.endpoint, sub.endpoint)
+ assertEquals(response.subscriptionInfo.keys.auth, sub.authKey)
+ assertEquals(response.subscriptionInfo.keys.p256dh, sub.publicKey)
+ assertEquals("scope", sub.scope)
+ assertNull(sub.appServerKey)
+
+ val sub2 = response.toPushSubscription("scope", "key")
+
+ assertEquals(response.subscriptionInfo.endpoint, sub.endpoint)
+ assertEquals(response.subscriptionInfo.keys.auth, sub.authKey)
+ assertEquals(response.subscriptionInfo.keys.p256dh, sub.publicKey)
+ assertEquals("scope", sub2.scope)
+ assertEquals("key", sub2.appServerKey)
+ }
+
+ @Test
+ fun `ServiceType to BridgeType`() {
+ assertEquals(BridgeType.FCM, ServiceType.FCM.toBridgeType())
+ }
+
+ companion object {
+ private fun preference(context: Context): SharedPreferences {
+ return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/push/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/push/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/push/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/push/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/push/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/push/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/pwa/README.md b/mobile/android/android-components/components/feature/pwa/README.md
new file mode 100644
index 0000000000..61da697817
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/README.md
@@ -0,0 +1,85 @@
+# [Android Components](../../../README.md) > Feature > Progressive Web Apps (PWA)
+
+Feature implementation for Progressive Web Apps (PWA).
+
+- https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps
+- https://developer.mozilla.org/en-US/docs/Web/Manifest
+
+## Usage
+
+### 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-pwa:{latest-version}"
+```
+
+### Adding feature to application
+
+#### Creating basic homescreen shortcuts
+
+`WebAppUseCases` includes a use case to pin websites to the homescreen. When called, the method will show a prompt to the user to let them drag a shortcut with the currently selected session onto their homescreen.
+
+If you don't want to support full web apps and only want the shortcut functionality, set the `supportWebApps` parameter to `false` when creating `WebAppUseCases`. This causes all shortcuts to open a new tab in the browser.
+
+#### Opening web apps
+
+To open the pinned shortcut as a progressive web app, add `mozilla.components.feature.pwa.PWA_LAUNCHER` to your intent filter.
+
+```xml
+<intent-filter>
+ <action android:name="mozilla.components.feature.pwa.VIEW_PWA" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="https" />
+</intent-filter>
+```
+
+You must also process the intent with the `WebAppIntentProcessor`. This processor will create a new session with the `webAppManifest` and `customTabConfig` fields set.
+
+The web app manifest will also be serialized onto the intent as a JSON string extra. It can be retrieved using the `Intent.getWebAppManifest` extension function.
+
+#### Hiding the toolbar
+
+`WebAppHideToolbarFeature` is used to hide the toolbar view when the user is visiting the website tied to the shortcut. Once they navigate away, the feature will show the toolbar again.
+
+This functionality pairs well with the `CustomTabsToolbarFeature`, which can be used to show a custom tab toolbar instead of a regular toolbar.
+
+#### Immersive mode, orientation settings, and recents entries.
+
+`WebAppActivityFeature` will set activity-level settings corresponding to the web app.
+
+The recents screen will show the icon and title of the web app.
+
+The activity orientation will be restricted to match `"orientation"` in the web app manifest.
+
+When `"display": "fullscreen"` is set in the web app manifest, the web app will be displayed in immersive mode.
+
+#### Displaying controls without a toolbar
+
+`WebAppSiteControlsFeature` will display a silent notification whenever a web app is open. This notification contains controls to interact with the web app, such as a refresh button and a shortcut to copy the URL.
+
+### Web content variables
+
+`WebAppContentFeature` will set web content variables to the web apps. Since the `"display"` value in the web app manifest file has to be applied in the CSS media query, the feature will apply it.
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Extras | Description |
+|--------|---------|----------------|------------------------------------|
+| CLICK | install_shortcut | | The user installs a PWA shortcut. |
+| CLICK | homescreen_icon_tap | | The user tapped the PWA icon on the homescreen. |
+
+#### `itemExtras`
+
+| Key | Type | Value |
+|--------------|---------|-----------------------------------|
+| timingNs | Long | The current system time when a foreground or background action is taken. |
+
+## 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/pwa/build.gradle b/mobile/android/android-components/components/feature/pwa/build.gradle
new file mode 100644
index 0000000000..5d8b90b313
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/build.gradle
@@ -0,0 +1,89 @@
+/* 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"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.pwa'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-icons')
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':concept-fetch')
+ implementation project(':feature-customtabs')
+ implementation project(':feature-tabs')
+ implementation project(':feature-intent')
+ implementation project(':feature-session')
+ implementation project(':service-digitalassetlinks')
+ implementation project(':support-base')
+ implementation project(':support-images')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.androidx_browser
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.kotlin_reflect
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+}
+
+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/pwa/proguard-rules.pro b/mobile/android/android-components/components/feature/pwa/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/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/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/0.json b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/0.json
new file mode 100644
index 0000000000..3a1209f2e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/0.json
@@ -0,0 +1,51 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "dd9b306a96bcf751f6cfdc739f38f2b4",
+ "entities": [
+ {
+ "tableName": "manifests",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manifest` TEXT NOT NULL, `start_url` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`start_url`))",
+ "fields": [
+ {
+ "fieldPath": "manifest",
+ "columnName": "manifest",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startUrl",
+ "columnName": "start_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "start_url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"dd9b306a96bcf751f6cfdc739f38f2b4\")"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/1.json b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/1.json
new file mode 100644
index 0000000000..879029a799
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/1.json
@@ -0,0 +1,73 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "2dbe6c2ed8111e6d63f6bb78035424aa",
+ "entities": [
+ {
+ "tableName": "manifests",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manifest` TEXT NOT NULL, `start_url` TEXT NOT NULL, `scope` TEXT, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `used_at` INTEGER NOT NULL, PRIMARY KEY(`start_url`))",
+ "fields": [
+ {
+ "fieldPath": "manifest",
+ "columnName": "manifest",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startUrl",
+ "columnName": "start_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scope",
+ "columnName": "scope",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedAt",
+ "columnName": "used_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "start_url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_manifests_scope",
+ "unique": false,
+ "columnNames": [
+ "scope"
+ ],
+ "createSql": "CREATE INDEX `index_manifests_scope` ON `${TABLE_NAME}` (`scope`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2dbe6c2ed8111e6d63f6bb78035424aa')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/2.json b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/2.json
new file mode 100644
index 0000000000..6de5b9ed0d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/2.json
@@ -0,0 +1,73 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "2dbe6c2ed8111e6d63f6bb78035424aa",
+ "entities": [
+ {
+ "tableName": "manifests",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manifest` TEXT NOT NULL, `start_url` TEXT NOT NULL, `scope` TEXT, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `used_at` INTEGER NOT NULL, PRIMARY KEY(`start_url`))",
+ "fields": [
+ {
+ "fieldPath": "manifest",
+ "columnName": "manifest",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startUrl",
+ "columnName": "start_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scope",
+ "columnName": "scope",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedAt",
+ "columnName": "used_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "start_url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_manifests_scope",
+ "unique": false,
+ "columnNames": [
+ "scope"
+ ],
+ "createSql": "CREATE INDEX `index_manifests_scope` ON `${TABLE_NAME}` (`scope`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2dbe6c2ed8111e6d63f6bb78035424aa')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/3.json b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/3.json
new file mode 100644
index 0000000000..565165ec3b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/3.json
@@ -0,0 +1,87 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "eb8c34cf7bcaf5f84bf0c3b407c8061a",
+ "entities": [
+ {
+ "tableName": "manifests",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manifest` TEXT NOT NULL, `start_url` TEXT NOT NULL, `scope` TEXT, `has_share_targets` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `used_at` INTEGER NOT NULL, PRIMARY KEY(`start_url`))",
+ "fields": [
+ {
+ "fieldPath": "manifest",
+ "columnName": "manifest",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startUrl",
+ "columnName": "start_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scope",
+ "columnName": "scope",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hasShareTargets",
+ "columnName": "has_share_targets",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedAt",
+ "columnName": "used_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "start_url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_manifests_scope",
+ "unique": false,
+ "columnNames": [
+ "scope"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_manifests_scope` ON `${TABLE_NAME}` (`scope`)"
+ },
+ {
+ "name": "index_manifests_has_share_targets",
+ "unique": false,
+ "columnNames": [
+ "has_share_targets"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_manifests_has_share_targets` ON `${TABLE_NAME}` (`has_share_targets`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb8c34cf7bcaf5f84bf0c3b407c8061a')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/androidTest/java/mozilla/components/feature/pwa/db/ManifestDatabaseMigrationTest.kt b/mobile/android/android-components/components/feature/pwa/src/androidTest/java/mozilla/components/feature/pwa/db/ManifestDatabaseMigrationTest.kt
new file mode 100644
index 0000000000..00659e3af2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/androidTest/java/mozilla/components/feature/pwa/db/ManifestDatabaseMigrationTest.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.pwa.db
+
+import androidx.core.database.getStringOrNull
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import java.io.IOException
+
+class ManifestDatabaseMigrationTest {
+ private val TEST_DB = "migration-test"
+
+ @Rule
+ @JvmField
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ ManifestDatabase::class.java,
+ )
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate2To3() {
+ helper.createDatabase(TEST_DB, 2).apply {
+ // db has schema version 2. insert some data using SQL queries.
+ // You cannot use DAO classes because they expect the latest schema.
+ execSQL("INSERT INTO manifests (start_url, created_at, updated_at, manifest, used_at, scope) VALUES ('https://mozilla.org', 1, 2, '{}', 3, 'https://mozilla.org')")
+
+ // Prepare for the next version.
+ close()
+ }
+
+ // Re-open the database with version 2 and provide
+ // MIGRATION_1_2 as the migration process.
+ helper.runMigrationsAndValidate(TEST_DB, 3, true, ManifestDatabase.MIGRATION_2_3).apply {
+ val result = query("SELECT scope, has_share_targets FROM manifests WHERE start_url = 'https://mozilla.org'")
+
+ result.moveToNext()
+
+ assertEquals(1, result.count)
+ assertTrue(result.isFirst)
+ assertTrue(result.isLast)
+ assertEquals("https://mozilla.org", result.getStringOrNull(0))
+ assertEquals(0, result.getInt(1))
+
+ close()
+ }
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate1To2() {
+ helper.createDatabase(TEST_DB, 1).apply {
+ // db has schema version 1. insert some data using SQL queries.
+ // You cannot use DAO classes because they expect the latest schema.
+ execSQL("INSERT INTO manifests (start_url, created_at, updated_at, manifest, used_at, scope) VALUES ('https://mozilla.org', 1, 2, '{}', 3, 'https://mozilla.org')")
+
+ // Prepare for the next version.
+ close()
+ }
+
+ // Re-open the database with version 2 and provide
+ // MIGRATION_1_2 as the migration process.
+ helper.runMigrationsAndValidate(TEST_DB, 2, true, ManifestDatabase.MIGRATION_1_2).apply {
+ val result = query("SELECT scope, used_at FROM manifests WHERE start_url = 'https://mozilla.org'")
+
+ result.moveToNext()
+
+ assertEquals(1, result.count)
+ assertTrue(result.isFirst)
+ assertTrue(result.isLast)
+ assertEquals("https://mozilla.org", result.getStringOrNull(0))
+ assertEquals(3, result.getLong(1))
+
+ close()
+ }
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate0To2() {
+ helper.createDatabase(TEST_DB, 0).apply {
+ // db has schema version 0 which was the original version 1. insert some data using SQL queries.
+ // You cannot use DAO classes because they expect the latest schema.
+ execSQL("INSERT INTO manifests (start_url, created_at, updated_at, manifest) VALUES ('https://mozilla.org', 1, 2, '{}')")
+
+ // Prepare for the next version.
+ close()
+ }
+
+ // Re-open the database with version 2 and provide
+ // MIGRATION_1_2 as the migration process.
+ helper.runMigrationsAndValidate(TEST_DB, 2, true, ManifestDatabase.MIGRATION_1_2).apply {
+ val result = query("SELECT scope, used_at FROM manifests WHERE start_url = 'https://mozilla.org'")
+
+ result.moveToNext()
+
+ assertEquals(1, result.count)
+ assertTrue(result.isFirst)
+ assertTrue(result.isLast)
+ assertNull(result.getStringOrNull(0))
+ assertEquals(2, result.getLong(1))
+
+ close()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/pwa/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..289f2ba72c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission
+ android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
+ android:maxSdkVersion="26" />
+
+ <application>
+
+ <activity android:name=".WebAppLauncherActivity"
+ android:theme="@style/Theme.AppCompat.Translucent"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="mozilla.components.feature.pwa.PWA_LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ManifestStorage.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ManifestStorage.kt
new file mode 100644
index 0000000000..acf793cd88
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ManifestStorage.kt
@@ -0,0 +1,159 @@
+/* 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.pwa
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.db.ManifestDatabase
+import mozilla.components.feature.pwa.db.ManifestEntity
+
+/**
+ * Disk storage for [WebAppManifest]. Other components use this class to reload a saved manifest.
+ *
+ * @param context the application context this storage is associated with
+ * @param activeThresholdMs a timeout in milliseconds after which the storage will consider a manifest
+ * as unused. By default this is [ACTIVE_THRESHOLD_MS].
+ */
+class ManifestStorage(context: Context, private val activeThresholdMs: Long = ACTIVE_THRESHOLD_MS) {
+
+ @VisibleForTesting
+ internal var manifestDao = lazy { ManifestDatabase.get(context).manifestDao() }
+ internal var installedScopes: MutableMap<String, String>? = null
+
+ /**
+ * Load a Web App Manifest for the given URL from disk.
+ * If no manifest is found, null is returned.
+ *
+ * @param startUrl URL of site. Should correspond to manifest's start_url.
+ */
+ suspend fun loadManifest(startUrl: String): WebAppManifest? = withContext(IO) {
+ manifestDao.value.getManifest(startUrl)?.manifest
+ }
+
+ /**
+ * Load all Web App Manifests with a matching scope for the given URL from disk.
+ * If no manifests are found, an empty list is returned.
+ *
+ * @param url URL of site. Should correspond to an url covered by the scope of a stored manifest.
+ */
+ suspend fun loadManifestsByScope(url: String): List<WebAppManifest> = withContext(IO) {
+ manifestDao.value.getManifestsByScope(url).map { it.manifest }
+ }
+
+ /**
+ * Checks whether there is a currently used manifest with a scope that matches the url.
+ *
+ * @param url the url to match with manifest scopes.
+ * @param currentTimeMs the current time in milliseconds.
+ */
+ suspend fun hasRecentManifest(
+ url: String,
+ @VisibleForTesting currentTimeMs: Long = System.currentTimeMillis(),
+ ): Boolean = withContext(IO) {
+ manifestDao.value.hasRecentManifest(url, thresholdMs = currentTimeMs - activeThresholdMs) > 0
+ }
+
+ /**
+ * Counts number of recently used manifests, as configured by [activeThresholdMs].
+ *
+ * @param currentTimeMs current time, exposed for testing
+ * @param activeThresholdMs a time threshold within which manifests are considered to be recently used.
+ */
+ suspend fun recentManifestsCount(
+ activeThresholdMs: Long = this.activeThresholdMs,
+ @VisibleForTesting currentTimeMs: Long = System.currentTimeMillis(),
+ ): Int = withContext(IO) {
+ manifestDao.value.recentManifestsCount(thresholdMs = currentTimeMs - activeThresholdMs)
+ }
+
+ /**
+ * Returns the cached scope for an url if the url falls into a web app scope that has been installed by the user.
+ *
+ * @param url the url to match against installed web app scopes.
+ */
+ fun getInstalledScope(url: String) = installedScopes?.keys?.sortedDescending()?.find { url.startsWith(it) }
+
+ /**
+ * Returns a cached start url for an installed web app scope.
+ *
+ * @param scope the scope url to look up.
+ */
+ fun getStartUrlForInstalledScope(scope: String) = installedScopes?.get(scope)
+
+ /**
+ * Populates a cache of currently installed web app scopes and their start urls.
+ *
+ * @param currentTime the current time is used to determine which web apps are still installed.
+ */
+ suspend fun warmUpScopes(currentTime: Long) = withContext(IO) {
+ installedScopes = manifestDao.value
+ .getInstalledScopes(deadline(currentTime))
+ .mapNotNull { manifest -> manifest.scope?.let { scope -> Pair(scope, manifest.startUrl) } }
+ .toMap()
+ .toMutableMap()
+ }
+
+ /**
+ * Load all Web App Manifests that contain share targets.
+ * If no manifests are found, an empty list is returned.
+ *
+ * @param currentTime the current time in milliseconds.
+ */
+ suspend fun loadShareableManifests(currentTime: Long): List<WebAppManifest> = withContext(IO) {
+ manifestDao.value.getRecentShareableManifests(deadline(currentTime)).map { it.manifest }
+ }
+
+ /**
+ * Save a Web App Manifest to disk.
+ */
+ suspend fun saveManifest(manifest: WebAppManifest) = withContext(IO) {
+ val entity = ManifestEntity(manifest, currentTime = System.currentTimeMillis())
+ manifestDao.value.insertManifest(entity)
+ }
+
+ /**
+ * Update an existing Web App Manifest on disk.
+ */
+ suspend fun updateManifest(manifest: WebAppManifest) = withContext(IO) {
+ manifestDao.value.getManifest(manifest.startUrl)?.let { existing ->
+ val update = existing.copy(manifest = manifest, updatedAt = System.currentTimeMillis())
+ manifestDao.value.updateManifest(update)
+ }
+ }
+
+ /**
+ * Update the last time a web app was used.
+ *
+ * @param manifest the manifest to update
+ */
+ suspend fun updateManifestUsedAt(manifest: WebAppManifest) = withContext(IO) {
+ manifestDao.value.getManifest(manifest.startUrl)?.let { existing ->
+ val update = existing.copy(usedAt = System.currentTimeMillis())
+ manifestDao.value.updateManifest(update)
+
+ existing.scope?.let { scope ->
+ installedScopes?.put(scope, existing.startUrl)
+ }
+
+ return@let
+ }
+ }
+
+ /**
+ * Delete all manifests associated with the list of URLs.
+ */
+ suspend fun removeManifests(startUrls: List<String>) = withContext(IO) {
+ manifestDao.value.deleteManifests(startUrls)
+ }
+
+ private fun deadline(currentTime: Long) = currentTime - activeThresholdMs
+
+ companion object {
+ const val ACTIVE_THRESHOLD_MS = 86400000 * 30L // 30 days
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ProgressiveWebAppFacts.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ProgressiveWebAppFacts.kt
new file mode 100644
index 0000000000..0abbbfc6e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ProgressiveWebAppFacts.kt
@@ -0,0 +1,50 @@
+/* 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.pwa
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [PwaFeature]
+ */
+class ProgressiveWebAppFacts {
+ /**
+ * Items that specify which portion of the [PwaFeature] was interacted with
+ */
+ object Items {
+ const val INSTALL_SHORTCUT = "install_shortcut"
+ const val HOMESCREEN_ICON_TAP = "homescreen_icon_tap"
+ }
+}
+
+private fun emitPwaFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_PWA,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitPwaInstallFact() =
+ emitPwaFact(
+ Action.CLICK,
+ ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT,
+ )
+
+internal fun emitHomescreenIconTapFact() =
+ emitPwaFact(
+ Action.CLICK,
+ ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP,
+ )
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppInterceptor.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppInterceptor.kt
new file mode 100644
index 0000000000..169a90c2a7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppInterceptor.kt
@@ -0,0 +1,62 @@
+/* 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.pwa
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.feature.pwa.ext.putUrlOverride
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
+
+/**
+ * This feature will intercept requests and reopen them in the corresponding installed PWA, if any.
+ *
+ * @param shortcutManager current shortcut manager instance to lookup web app install states
+ */
+class WebAppInterceptor(
+ private val context: Context,
+ private val manifestStorage: ManifestStorage,
+ private val launchFromInterceptor: Boolean = true,
+) : RequestInterceptor {
+
+ @Suppress("ReturnCount")
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ val scope = manifestStorage.getInstalledScope(uri) ?: return null
+ val startUrl = manifestStorage.getStartUrlForInstalledScope(scope) ?: return null
+ val intent = createIntentFromUri(startUrl, uri)
+
+ if (!launchFromInterceptor) {
+ return RequestInterceptor.InterceptionResponse.AppIntent(intent, uri)
+ }
+
+ intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(intent)
+
+ return RequestInterceptor.InterceptionResponse.Deny
+ }
+
+ /**
+ * Creates a new VIEW_PWA intent for a URL.
+ *
+ * @param uri target URL for the new intent
+ */
+ private fun createIntentFromUri(startUrl: String, urlOverride: String = startUrl): Intent {
+ return Intent(WebAppIntentProcessor.ACTION_VIEW_PWA, Uri.parse(startUrl)).apply {
+ this.addCategory(Intent.CATEGORY_DEFAULT)
+ this.putUrlOverride(urlOverride)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppLauncherActivity.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppLauncherActivity.kt
new file mode 100644
index 0000000000..fe4eb94897
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppLauncherActivity.kt
@@ -0,0 +1,105 @@
+/* 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.pwa
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.appcompat.app.AppCompatActivity
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * This activity is launched by Web App shortcuts on the home screen.
+ *
+ * Based on the Web App Manifest (display) it will decide whether the app is launched in the
+ * browser or in a standalone activity.
+ */
+class WebAppLauncherActivity : AppCompatActivity() {
+
+ private val scope = MainScope()
+ private val logger = Logger("WebAppLauncherActivity")
+ private lateinit var storage: ManifestStorage
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ storage = ManifestStorage(applicationContext)
+
+ val startUrl = intent.data ?: return finish()
+
+ scope.launch {
+ val manifest = loadManifest(startUrl.toString())
+ routeManifest(startUrl, manifest)
+
+ finish()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ scope.cancel()
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun routeManifest(startUrl: Uri, manifest: WebAppManifest?) {
+ when (manifest?.display) {
+ WebAppManifest.DisplayMode.FULLSCREEN,
+ WebAppManifest.DisplayMode.STANDALONE,
+ WebAppManifest.DisplayMode.MINIMAL_UI,
+ -> {
+ emitHomescreenIconTapFact()
+ launchWebAppShell(startUrl)
+ }
+
+ // If no manifest is saved for this site, just open the browser.
+ WebAppManifest.DisplayMode.BROWSER, null -> launchBrowser(startUrl)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun launchBrowser(startUrl: Uri) {
+ val intent = Intent(Intent.ACTION_VIEW, startUrl).apply {
+ addCategory(SHORTCUT_CATEGORY)
+ `package` = packageName
+ }
+
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ logger.error("Package does not handle VIEW intent. Can't launch browser.")
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun launchWebAppShell(startUrl: Uri) {
+ val intent = Intent(ACTION_VIEW_PWA, startUrl).apply {
+ `package` = packageName
+ }
+
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ logger.error("Packages does not handle ACTION_VIEW_PWA intent. Can't launch as web app.", e)
+ // Fall back to normal browser
+ launchBrowser(startUrl)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal suspend fun loadManifest(startUrl: String): WebAppManifest? {
+ return storage.loadManifest(startUrl)
+ }
+
+ companion object {
+ internal const val ACTION_PWA_LAUNCHER = "mozilla.components.feature.pwa.PWA_LAUNCHER"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppShortcutManager.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppShortcutManager.kt
new file mode 100644
index 0000000000..a97b3b8415
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppShortcutManager.kt
@@ -0,0 +1,287 @@
+/* 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.pwa
+
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_MAIN
+import android.content.Intent.CATEGORY_HOME
+import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.pm.ShortcutManager
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.getSystemService
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+import androidx.core.net.toUri
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.decoder.ICOIconDecoder
+import mozilla.components.browser.icons.extension.toIconRequest
+import mozilla.components.browser.icons.generator.DefaultIconGenerator
+import mozilla.components.browser.icons.loader.DataUriIconLoader
+import mozilla.components.browser.icons.loader.HttpIconLoader
+import mozilla.components.browser.icons.loader.MemoryIconLoader
+import mozilla.components.browser.icons.preparer.MemoryIconPreparer
+import mozilla.components.browser.icons.processor.AdaptiveIconProcessor
+import mozilla.components.browser.icons.processor.ColorProcessor
+import mozilla.components.browser.icons.processor.MemoryIconProcessor
+import mozilla.components.browser.icons.processor.ResizingProcessor
+import mozilla.components.browser.icons.utils.IconMemoryCache
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.feature.pwa.WebAppLauncherActivity.Companion.ACTION_PWA_LAUNCHER
+import mozilla.components.feature.pwa.ext.hasLargeIcons
+import mozilla.components.feature.pwa.ext.installableManifest
+import mozilla.components.support.images.decoder.AndroidImageDecoder
+import mozilla.components.support.utils.PendingIntentUtils
+import java.util.UUID
+
+private val pwaIconMemoryCache = IconMemoryCache()
+
+const val SHORTCUT_CATEGORY = mozilla.components.feature.customtabs.SHORTCUT_CATEGORY
+
+/**
+ * Helper to manage pinned shortcuts for websites.
+ *
+ * @param httpClient Fetch client used to load website icons.
+ * @param storage Storage used to save web app manifests to disk.
+ * @param supportWebApps If true, Progressive Web Apps will be pinnable.
+ * If false, all web sites will be bookmark shortcuts even if they have a manifest.
+ */
+class WebAppShortcutManager(
+ context: Context,
+ httpClient: Client,
+ private val storage: ManifestStorage,
+ internal val supportWebApps: Boolean = true,
+) {
+
+ internal val icons = webAppIcons(context, httpClient)
+
+ private val fallbackLabel = context.getString(R.string.mozac_feature_pwa_default_shortcut_label)
+
+ /**
+ * Request to create a new shortcut on the home screen.
+ * @param context The current context.
+ * @param session The session to create the shortcut for.
+ * @param overrideShortcutName (optional) The name of the shortcut. Ignored for PWAs.
+ */
+ suspend fun requestPinShortcut(
+ context: Context,
+ session: SessionState,
+ overrideShortcutName: String? = null,
+ ) {
+ if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
+ val manifest = session.installableManifest()
+ val shortcut = if (supportWebApps && manifest != null) {
+ emitPwaInstallFact()
+ buildWebAppShortcut(context, manifest)
+ } else {
+ buildBasicShortcut(context, session, overrideShortcutName)
+ }
+
+ if (shortcut != null) {
+ val intent = Intent(ACTION_MAIN).apply {
+ addCategory(CATEGORY_HOME)
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntentUtils.defaultFlags or FLAG_UPDATE_CURRENT,
+ )
+ val intentSender = pendingIntent.intentSender
+
+ ShortcutManagerCompat.requestPinShortcut(context, shortcut, intentSender)
+ }
+ }
+ }
+
+ /**
+ * Update existing PWA shortcuts with the latest info from web app manifests.
+ *
+ * Devices before 7.1 do not allow shortcuts to be dynamically updated,
+ * so this method will do nothing.
+ */
+ suspend fun updateShortcuts(context: Context, manifests: List<WebAppManifest>) {
+ if (SDK_INT >= VERSION_CODES.N_MR1) {
+ context.getSystemService<ShortcutManager>()?.apply {
+ val shortcuts = manifests.mapNotNull { buildWebAppShortcut(context, it)?.toShortcutInfo() }
+ updateShortcuts(shortcuts)
+ }
+ }
+ }
+
+ /**
+ * Create a new basic pinned website shortcut using info from the session.
+ * Consuming `SHORTCUT_CATEGORY` in `AndroidManifest` is required for the package to be launched
+ */
+ suspend fun buildBasicShortcut(
+ context: Context,
+ session: SessionState,
+ overrideShortcutName: String? = null,
+ ): ShortcutInfoCompat {
+ val shortcutIntent = Intent(Intent.ACTION_VIEW, session.content.url.toUri()).apply {
+ addCategory(SHORTCUT_CATEGORY)
+ `package` = context.packageName
+ }
+
+ val manifest = session.content.webAppManifest
+ val shortLabel = overrideShortcutName
+ ?: manifest?.shortName
+ ?: manifest?.name
+ ?: session.content.title
+
+ val fallback = fallbackLabel
+ val fixedLabel = shortLabel.ifBlank { fallback }
+
+ val builder = ShortcutInfoCompat.Builder(context, UUID.randomUUID().toString())
+ .setShortLabel(fixedLabel)
+ .setIntent(shortcutIntent)
+
+ val icon = if (manifest != null && manifest.hasLargeIcons()) {
+ buildIconFromManifest(manifest)
+ } else {
+ session.content.icon?.let { IconCompat.createWithBitmap(it) }
+ }
+ icon?.let {
+ builder.setIcon(it)
+ }
+
+ return builder.build()
+ }
+
+ /**
+ * Create a new Progressive Web App shortcut using a web app manifest.
+ */
+ suspend fun buildWebAppShortcut(
+ context: Context,
+ manifest: WebAppManifest,
+ ): ShortcutInfoCompat? {
+ val shortcutIntent = Intent(context, WebAppLauncherActivity::class.java).apply {
+ action = ACTION_PWA_LAUNCHER
+ data = manifest.startUrl.toUri()
+ flags = FLAG_ACTIVITY_NEW_DOCUMENT
+ `package` = context.packageName
+ }
+
+ val shortLabel = manifest.shortName ?: manifest.name
+ storage.saveManifest(manifest)
+
+ return ShortcutInfoCompat.Builder(context, manifest.startUrl)
+ .setLongLabel(manifest.name)
+ .setShortLabel(shortLabel.ifBlank { fallbackLabel })
+ .setIcon(buildIconFromManifest(manifest))
+ .setIntent(shortcutIntent)
+ .build()
+ }
+
+ @VisibleForTesting
+ internal suspend fun buildIconFromManifest(manifest: WebAppManifest): IconCompat {
+ val request = manifest.toIconRequest()
+ val icon = icons.loadIcon(request).await()
+ return if (icon.maskable) {
+ IconCompat.createWithAdaptiveBitmap(icon.bitmap)
+ } else {
+ IconCompat.createWithBitmap(icon.bitmap)
+ }
+ }
+
+ /**
+ * Finds the shortcut associated with the given startUrl.
+ * This method can be used to check if a web app was added to the homescreen.
+ */
+ fun findShortcut(context: Context, startUrl: String) =
+ if (SDK_INT >= VERSION_CODES.N_MR1) {
+ context.getSystemService<ShortcutManager>()?.pinnedShortcuts?.find { it.id == startUrl }
+ } else {
+ null
+ }
+
+ /**
+ * Checks if there is a currently installed web app to which this URL belongs.
+ *
+ * @param url url that is covered by the scope of a web app installed by the user
+ * @param currentTimeMs the current time in milliseconds, exposed for testing
+ */
+ suspend fun getWebAppInstallState(
+ url: String,
+ @VisibleForTesting currentTimeMs: Long = System.currentTimeMillis(),
+ ): WebAppInstallState {
+ if (storage.hasRecentManifest(url, currentTimeMs = currentTimeMs)) {
+ return WebAppInstallState.Installed
+ }
+
+ return WebAppInstallState.NotInstalled
+ }
+
+ /**
+ * Counts number of recently used web apps. See [ManifestStorage.activeThresholdMs].
+ *
+ * @param activeThresholdMs defines a time window within which a web app is considered recently used.
+ * Defaults to [ManifestStorage.ACTIVE_THRESHOLD_MS].
+ * @return count of recently used web apps
+ */
+ suspend fun recentlyUsedWebAppsCount(
+ activeThresholdMs: Long = ManifestStorage.ACTIVE_THRESHOLD_MS,
+ ): Int {
+ return storage.recentManifestsCount(activeThresholdMs = activeThresholdMs)
+ }
+
+ /**
+ * Updates the usedAt timestamp of the web app this url is associated with.
+ *
+ * @param manifest the manifest to update
+ */
+ suspend fun reportWebAppUsed(manifest: WebAppManifest): Unit? {
+ return storage.updateManifestUsedAt(manifest)
+ }
+
+ /**
+ * Possible install states of a Web App.
+ */
+ enum class WebAppInstallState {
+ NotInstalled,
+ Installed,
+ }
+}
+
+/**
+ * Creates custom version of [BrowserIcons] for loading web app icons.
+ *
+ * This version has its own cache to avoid affecting tab icons.
+ */
+private fun webAppIcons(
+ context: Context,
+ httpClient: Client,
+) = BrowserIcons(
+ context = context,
+ httpClient = httpClient,
+ generator = DefaultIconGenerator(cornerRadiusDimen = null),
+ preparers = listOf(
+ MemoryIconPreparer(pwaIconMemoryCache),
+ ),
+ loaders = listOf(
+ MemoryIconLoader(pwaIconMemoryCache),
+ HttpIconLoader(httpClient),
+ DataUriIconLoader(),
+ ),
+ decoders = listOf(
+ AndroidImageDecoder(),
+ ICOIconDecoder(),
+ ),
+ processors = listOf(
+ MemoryIconProcessor(pwaIconMemoryCache),
+ ResizingProcessor(),
+ ColorProcessor(),
+ AdaptiveIconProcessor(),
+ ),
+)
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppUseCases.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppUseCases.kt
new file mode 100644
index 0000000000..b598706d03
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppUseCases.kt
@@ -0,0 +1,88 @@
+/* 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.pwa
+
+import android.content.Context
+import androidx.core.content.pm.ShortcutManagerCompat
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.pwa.ext.installableManifest
+
+/**
+ * These use cases allow for adding a web app or web site to the homescreen.
+ */
+class WebAppUseCases(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val shortcutManager: WebAppShortcutManager,
+) {
+ /**
+ * Checks if the launcher supports adding shortcuts.
+ */
+ fun isPinningSupported() =
+ ShortcutManagerCompat.isRequestPinShortcutSupported(applicationContext)
+
+ /**
+ * Checks to see if the current session can be installed as a Progressive Web App.
+ */
+ fun isInstallable() =
+ store.state.selectedTab?.installableManifest() != null && shortcutManager.supportWebApps
+
+ /**
+ * Let the user add the selected session to the homescreen.
+ *
+ * If the selected session represents a Progressive Web App, then the
+ * manifest will be saved and the web app will be launched based on the
+ * manifest values.
+ *
+ * Otherwise, the pinned shortcut will act like a simple bookmark for the site.
+ */
+ class AddToHomescreenUseCase internal constructor(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val shortcutManager: WebAppShortcutManager,
+ ) {
+
+ /**
+ * @param overrideBasicShortcutName (optional) Custom label used if the current session
+ * is NOT a Progressive Web App
+ */
+ suspend operator fun invoke(overrideBasicShortcutName: String? = null) {
+ val session = store.state.selectedTab ?: return
+ shortcutManager.requestPinShortcut(applicationContext, session, overrideBasicShortcutName)
+ }
+ }
+
+ val addToHomescreen by lazy {
+ AddToHomescreenUseCase(applicationContext, store, shortcutManager)
+ }
+
+ /**
+ * Checks the current install state of a Web App.
+ *
+ * Returns WebAppShortcutManager.InstallState.Installed if the user has installed
+ * or used the web app in the past 30 days.
+ *
+ * Otherwise, WebAppShortcutManager.InstallState.NotInstalled is returned.
+ */
+ class GetInstallStateUseCase internal constructor(
+ private val store: BrowserStore,
+ private val shortcutManager: WebAppShortcutManager,
+ ) {
+ /**
+ * @param currentTimeMs the current time against which manifest usage timeouts will be validated
+ */
+ suspend operator fun invoke(
+ currentTimeMs: Long = System.currentTimeMillis(),
+ ): WebAppShortcutManager.WebAppInstallState? {
+ val session = store.state.selectedTab ?: return null
+ return shortcutManager.getWebAppInstallState(session.content.url, currentTimeMs)
+ }
+ }
+
+ val getInstallState by lazy {
+ GetInstallStateUseCase(store, shortcutManager)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestConverter.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestConverter.kt
new file mode 100644
index 0000000000..2f93f373db
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestConverter.kt
@@ -0,0 +1,26 @@
+/* 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.pwa.db
+
+import androidx.room.TypeConverter
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifestParser
+
+/**
+ * Converts a web app manifest to and from JSON strings
+ */
+internal class ManifestConverter {
+ private val parser = WebAppManifestParser()
+
+ @TypeConverter
+ fun fromJsonString(json: String): WebAppManifest =
+ when (val result = parser.parse(json)) {
+ is WebAppManifestParser.Result.Success -> result.manifest
+ is WebAppManifestParser.Result.Failure -> throw result.exception
+ }
+
+ @TypeConverter
+ fun toJsonString(manifest: WebAppManifest): String = parser.serialize(manifest).toString()
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDao.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDao.kt
new file mode 100644
index 0000000000..5db04cc8fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDao.kt
@@ -0,0 +1,61 @@
+/* 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.pwa.db
+
+import androidx.annotation.WorkerThread
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+
+/**
+ * Internal DAO for accessing [ManifestEntity] instances.
+ */
+@Dao
+internal interface ManifestDao {
+ @WorkerThread
+ @Query("SELECT * from manifests WHERE start_url = :startUrl")
+ fun getManifest(startUrl: String): ManifestEntity?
+
+ @WorkerThread
+ @Query("SELECT * from manifests WHERE :url LIKE scope||'%' ORDER BY LENGTH(scope) DESC")
+ fun getManifestsByScope(url: String): List<ManifestEntity>
+
+ @WorkerThread
+ @Query("SELECT count(start_url) from manifests WHERE :url LIKE scope||'%' AND used_at > :thresholdMs")
+ fun hasRecentManifest(url: String, thresholdMs: Long): Int
+
+ @WorkerThread
+ @Query("SELECT count(start_url) from manifests WHERE used_at > :thresholdMs")
+ fun recentManifestsCount(thresholdMs: Long): Int
+
+ @WorkerThread
+ @Query(
+ """
+ SELECT * from manifests
+ WHERE has_share_targets == 1
+ AND used_at > :deadline
+ ORDER BY used_at DESC
+ """,
+ )
+ fun getRecentShareableManifests(deadline: Long): List<ManifestEntity>
+
+ @WorkerThread
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertManifest(manifest: ManifestEntity): Long
+
+ @WorkerThread
+ @Update
+ fun updateManifest(manifest: ManifestEntity)
+
+ @WorkerThread
+ @Query("DELETE FROM manifests WHERE start_url IN (:startUrls)")
+ fun deleteManifests(startUrls: List<String>)
+
+ @WorkerThread
+ @Query("SELECT * from manifests WHERE used_at > :expiresAt ORDER BY LENGTH(scope)")
+ fun getInstalledScopes(expiresAt: Long): List<ManifestEntity>
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDatabase.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDatabase.kt
new file mode 100644
index 0000000000..fdf36d5734
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDatabase.kt
@@ -0,0 +1,70 @@
+/* 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.pwa.db
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+/**
+ * Internal database for storing web app manifests and metadata.
+ */
+@Database(entities = [ManifestEntity::class], version = 3)
+@TypeConverters(ManifestConverter::class)
+internal abstract class ManifestDatabase : RoomDatabase() {
+ abstract fun manifestDao(): ManifestDao
+
+ @Suppress("MagicNumber")
+ companion object {
+ @Volatile private var instance: ManifestDatabase? = null
+
+ @VisibleForTesting
+ internal val MIGRATION_1_2: Migration = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ val cursor = db.query("SELECT * FROM manifests LIMIT 0,1")
+
+ if (cursor.getColumnIndex("used_at") < 0) {
+ db.execSQL("ALTER TABLE manifests ADD COLUMN used_at INTEGER NOT NULL DEFAULT 0")
+ }
+
+ if (cursor.getColumnIndex("scope") < 0) {
+ db.execSQL("ALTER TABLE manifests ADD COLUMN scope TEXT")
+ }
+
+ db.execSQL("CREATE INDEX IF NOT EXISTS index_manifests_scope ON manifests (scope)")
+ db.execSQL("UPDATE manifests SET used_at = updated_at WHERE used_at = 0")
+ }
+ }
+
+ @VisibleForTesting
+ internal val MIGRATION_2_3: Migration = object : Migration(2, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("ALTER TABLE manifests ADD COLUMN has_share_targets INTEGER NOT NULL DEFAULT 0")
+
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS index_manifests_has_share_targets ON manifests (has_share_targets)",
+ )
+ }
+ }
+
+ @Synchronized
+ fun get(context: Context): ManifestDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ ManifestDatabase::class.java,
+ "manifests",
+ ).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build().also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestEntity.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestEntity.kt
new file mode 100644
index 0000000000..84ab17ac27
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestEntity.kt
@@ -0,0 +1,50 @@
+/* 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.pwa.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.concept.engine.manifest.WebAppManifest
+
+/**
+ * Internal entity representing a web app manifest.
+ */
+@Entity(tableName = "manifests")
+internal data class ManifestEntity(
+ val manifest: WebAppManifest,
+
+ @PrimaryKey
+ @ColumnInfo(name = "start_url")
+ val startUrl: String,
+
+ @ColumnInfo(name = "scope", index = true)
+ val scope: String?,
+
+ @ColumnInfo(name = "has_share_targets", index = true)
+ val hasShareTargets: Int,
+
+ @ColumnInfo(name = "created_at")
+ val createdAt: Long,
+
+ @ColumnInfo(name = "updated_at")
+ val updatedAt: Long,
+
+ @ColumnInfo(name = "used_at")
+ val usedAt: Long,
+) {
+ constructor(
+ manifest: WebAppManifest,
+ currentTime: Long = System.currentTimeMillis(),
+ ) : this(
+ manifest,
+ startUrl = manifest.startUrl,
+ scope = manifest.scope,
+ hasShareTargets = if (manifest.shareTarget != null) 1 else 0,
+ createdAt = currentTime,
+ updatedAt = currentTime,
+ usedAt = currentTime,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Activity.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Activity.kt
new file mode 100644
index 0000000000..a64d4c4c86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Activity.kt
@@ -0,0 +1,26 @@
+/* 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.pwa.ext
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifest.Orientation
+
+/**
+ * Sets the requested orientation of the [Activity] to the orientation provided by the given [WebAppManifest] (See
+ * [WebAppManifest.orientation] and [WebAppManifest.Orientation].
+ */
+fun Activity.applyOrientation(manifest: WebAppManifest?) {
+ requestedOrientation = when (manifest?.orientation) {
+ Orientation.NATURAL, Orientation.ANY, null -> ActivityInfo.SCREEN_ORIENTATION_USER
+ Orientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ Orientation.LANDSCAPE_PRIMARY -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ Orientation.LANDSCAPE_SECONDARY -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
+ Orientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+ Orientation.PORTRAIT_PRIMARY -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ Orientation.PORTRAIT_SECONDARY -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Bundle.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Bundle.kt
new file mode 100644
index 0000000000..44670d4b10
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Bundle.kt
@@ -0,0 +1,31 @@
+/* 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.pwa.ext
+
+import android.os.Bundle
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifestParser
+import mozilla.components.concept.engine.manifest.getOrNull
+
+internal const val EXTRA_WEB_APP_MANIFEST = "mozilla.components.feature.pwa.EXTRA_WEB_APP_MANIFEST"
+
+/**
+ * Serializes and inserts a [WebAppManifest] value into the mapping of this [Bundle],
+ * replacing any existing web app manifest.
+ */
+fun Bundle.putWebAppManifest(webAppManifest: WebAppManifest?) {
+ val json = webAppManifest?.let { WebAppManifestParser().serialize(it).toString() }
+ putString(EXTRA_WEB_APP_MANIFEST, json)
+}
+
+/**
+ * Parses and returns the [WebAppManifest] associated with this [Bundle],
+ * or null if no mapping of the desired type exists.
+ */
+fun Bundle.getWebAppManifest(): WebAppManifest? {
+ return getString(EXTRA_WEB_APP_MANIFEST)?.let { json ->
+ WebAppManifestParser().parse(json).getOrNull()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/CustomTabState.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/CustomTabState.kt
new file mode 100644
index 0000000000..2b949cabe4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/CustomTabState.kt
@@ -0,0 +1,22 @@
+/* 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.pwa.ext
+
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.VerificationStatus.PENDING
+import mozilla.components.feature.customtabs.store.VerificationStatus.SUCCESS
+
+/**
+ * Returns a list of trusted (or pending) origins.
+ */
+val CustomTabState.trustedOrigins
+ get() = relationships.mapNotNull { (pair, status) ->
+ if (pair.relation == RELATION_HANDLE_ALL_URLS && (status == PENDING || status == SUCCESS)) {
+ pair.origin
+ } else {
+ null
+ }
+ }
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Intent.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Intent.kt
new file mode 100644
index 0000000000..7258a7341c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Intent.kt
@@ -0,0 +1,48 @@
+/* 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.pwa.ext
+
+import android.content.Intent
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifestParser
+
+internal const val EXTRA_URL_OVERRIDE = "mozilla.components.feature.pwa.EXTRA_URL_OVERRIDE"
+
+/**
+ * Add extended [WebAppManifest] data to the intent.
+ */
+fun Intent.putWebAppManifest(webAppManifest: WebAppManifest) {
+ val json = WebAppManifestParser().serialize(webAppManifest)
+ putExtra(EXTRA_WEB_APP_MANIFEST, json.toString())
+}
+
+/**
+ * Retrieve extended [WebAppManifest] data from the intent.
+ */
+fun Intent.getWebAppManifest(): WebAppManifest? {
+ return extras?.getWebAppManifest()
+}
+
+/**
+ * Add [String] URL override to the intent.
+ *
+ * @param url The URL override value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see [getUrlOverride]
+ */
+fun Intent.putUrlOverride(url: String?): Intent {
+ return putExtra(EXTRA_URL_OVERRIDE, url)
+}
+
+/**
+ * Retrieves [String] Url override from the intent.
+ *
+ * @return The URL override previously added with [putUrlOverride],
+ * or null if no URL was found.
+ */
+fun Intent.getUrlOverride(): String? = getStringExtra(EXTRA_URL_OVERRIDE)
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/SessionState.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/SessionState.kt
new file mode 100644
index 0000000000..27cb4cf780
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/SessionState.kt
@@ -0,0 +1,28 @@
+/* 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.pwa.ext
+
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifest.DisplayMode.BROWSER
+
+/**
+ * Checks if the current session represents an installable web app.
+ * If so, return the web app manifest. Otherwise, return null.
+ *
+ * Websites are installable if:
+ * - The site is served over HTTPS
+ * - The site has a valid manifest with a name or short_name
+ * - The manifest display mode is standalone, fullscreen, or minimal-ui
+ * - The icons array in the manifest contains an icon of at least 192x192
+ */
+fun SessionState.installableManifest(): WebAppManifest? {
+ val manifest = content.webAppManifest ?: return null
+ return if (content.securityInfo.secure && manifest.display != BROWSER && manifest.hasLargeIcons()) {
+ manifest
+ } else {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Uri.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Uri.kt
new file mode 100644
index 0000000000..4fac485fd1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Uri.kt
@@ -0,0 +1,25 @@
+/* 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.pwa.ext
+
+import android.net.Uri
+
+/**
+ * Returns just the origin of the [Uri].
+ *
+ * The origin is a URL that contains only the scheme, host, and port.
+ * https://html.spec.whatwg.org/multipage/origin.html#concept-origin
+ *
+ * Null is returned if the URI was invalid (i.e.: `"/foo/bar".toUri()`)
+ */
+fun Uri.toOrigin(): Uri? {
+ var authority = host
+ if (port != -1) {
+ authority += ":$port"
+ }
+
+ val result = Uri.Builder().scheme(scheme).encodedAuthority(authority).build().normalizeScheme()
+ return if (result.toString().isNotEmpty()) result else null
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/WebAppManifest.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/WebAppManifest.kt
new file mode 100644
index 0000000000..c222a3c73d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/WebAppManifest.kt
@@ -0,0 +1,98 @@
+/* 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.pwa.ext
+
+import android.app.ActivityManager.TaskDescription
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.net.Uri
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.core.net.toUri
+import mozilla.components.browser.state.state.ColorSchemeParams
+import mozilla.components.browser.state.state.ColorSchemes
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifest.Icon.Purpose
+import mozilla.components.support.utils.ColorUtils.isDark
+
+private const val MIN_INSTALLABLE_ICON_SIZE = 192
+
+/**
+ * Checks if the web app manifest can be used to create a shortcut icon.
+ *
+ * Websites have an installable icon if the manifest contains an icon of at least 192x192.
+ * @see [installableManifest]
+ */
+fun WebAppManifest.hasLargeIcons() = icons.any { icon ->
+ (Purpose.ANY in icon.purpose || Purpose.MASKABLE in icon.purpose) &&
+ icon.sizes.any { size ->
+ size.minLength >= MIN_INSTALLABLE_ICON_SIZE
+ }
+}
+
+/**
+ * Creates a [TaskDescription] for the activity manager based on the manifest.
+ *
+ * Since the web app icon is provided dynamically by the web site, we can't provide a resource ID.
+ * Instead we use the deprecated constructor.
+ */
+@Suppress("Deprecation")
+fun WebAppManifest.toTaskDescription(icon: Bitmap?) =
+ TaskDescription(name, icon, themeColor ?: 0)
+
+/**
+ * Creates a [CustomTabConfig] that styles a custom tab toolbar to match the manifest theme.
+ */
+fun WebAppManifest.toCustomTabConfig(): CustomTabConfig {
+ val backgroundColor = this.backgroundColor
+ val colorSchemes = if (themeColor != null && backgroundColor != null) {
+ ColorSchemes(
+ ColorSchemeParams(
+ toolbarColor = themeColor,
+ navigationBarColor = getVersionSafeNavBarColor(backgroundColor),
+ ),
+ )
+ } else {
+ null
+ }
+
+ return CustomTabConfig(
+ colorSchemes = colorSchemes,
+ closeButtonIcon = null,
+ enableUrlbarHiding = true,
+ actionButtonConfig = null,
+ showCloseButton = false,
+ showShareMenuItem = true,
+ menuItems = emptyList(),
+ externalAppType = ExternalAppType.PROGRESSIVE_WEB_APP,
+ )
+}
+
+private fun getVersionSafeNavBarColor(backgroundColor: Int) = if (SDK_INT >= Build.VERSION_CODES.O) {
+ if (isDark(backgroundColor)) Color.BLACK else Color.WHITE
+} else {
+ null
+}
+
+/**
+ * Returns the scope of the manifest as a [Uri] for use
+ * with [mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature].
+ *
+ * Null is returned when the scope should be ignored, such as with display: minimal-ui,
+ * where the toolbar should always be displayed.
+ */
+fun WebAppManifest.getTrustedScope(): Uri? {
+ return when (display) {
+ WebAppManifest.DisplayMode.FULLSCREEN,
+ WebAppManifest.DisplayMode.STANDALONE,
+ -> (scope ?: startUrl).toUri()
+
+ WebAppManifest.DisplayMode.MINIMAL_UI,
+ WebAppManifest.DisplayMode.BROWSER,
+ -> null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeature.kt
new file mode 100644
index 0000000000..9b9c30cbbd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeature.kt
@@ -0,0 +1,94 @@
+/* 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.pwa.feature
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.WebAppShortcutManager
+import mozilla.components.lib.state.ext.flow
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Feature used to update the existing web app manifest and web app shortcut.
+ *
+ * @param shortcutManager Shortcut manager used to update pinned shortcuts.
+ * @param storage Manifest storage used to have updated manifests.
+ * @param sessionId ID of the web app session to observe.
+ * @param initialManifest Loaded manifest for the current web app.
+ */
+class ManifestUpdateFeature(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val shortcutManager: WebAppShortcutManager,
+ private val storage: ManifestStorage,
+ private val sessionId: String,
+ private var initialManifest: WebAppManifest,
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var updateJob: Job? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var updateUsageJob: Job? = null
+
+ /**
+ * Updates the manifest on disk then updates the pinned shortcut to reflect changes.
+ */
+ @VisibleForTesting
+ internal suspend fun updateStoredManifest(manifest: WebAppManifest) {
+ storage.updateManifest(manifest)
+ shortcutManager.updateShortcuts(applicationContext, listOf(manifest))
+ initialManifest = manifest
+ }
+
+ override fun start() {
+ scope = MainScope().also { observeManifestChanges(it) }
+ updateUsageJob?.cancel()
+
+ updateUsageJob = scope?.launch {
+ storage.updateManifestUsedAt(initialManifest)
+ }
+ }
+
+ private fun observeManifestChanges(scope: CoroutineScope) = scope.launch {
+ store.flow()
+ .mapNotNull { state -> state.findCustomTab(sessionId) }
+ .map { tab -> tab.content.webAppManifest }
+ .distinctUntilChanged()
+ .collect { manifest -> onWebAppManifestChanged(manifest) }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ /**
+ * When the manifest is changed, compare it to the existing manifest.
+ * If it is different, update the disk and shortcut. Ignore if called with a null
+ * manifest or a manifest with a different start URL.
+ */
+ private fun onWebAppManifestChanged(manifest: WebAppManifest?) {
+ if (manifest?.startUrl == initialManifest.startUrl && manifest != initialManifest) {
+ updateJob?.cancel()
+ updateUsageJob?.cancel()
+
+ updateJob = scope?.launch { updateStoredManifest(manifest) }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/SiteControlsBuilder.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/SiteControlsBuilder.kt
new file mode 100644
index 0000000000..f7c1f3c1a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/SiteControlsBuilder.kt
@@ -0,0 +1,130 @@
+/* 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.pwa.feature
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.widget.Toast
+import androidx.core.content.getSystemService
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.feature.pwa.R
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.utils.PendingIntentUtils
+
+/**
+ * Callback for [WebAppSiteControlsFeature] that lets the displayed notification be customized.
+ */
+interface SiteControlsBuilder {
+
+ /**
+ * Create the notification to be displayed. Initial values are set in the provided [builder]
+ * and additional actions can be added here. Actions should be represented as [PendingIntent]
+ * that are filtered by [getFilter] and handled in [onReceiveBroadcast].
+ */
+ fun buildNotification(context: Context, builder: Notification.Builder)
+
+ /**
+ * Return an intent filter that matches the actions specified in [buildNotification].
+ */
+ fun getFilter(): IntentFilter
+
+ /**
+ * Handle actions the user selected in the site controls notification.
+ */
+ fun onReceiveBroadcast(context: Context, tab: CustomTabSessionState, intent: Intent)
+
+ /**
+ * Default implementation of [SiteControlsBuilder] that copies the URL of the site when tapped.
+ */
+ open class Default : SiteControlsBuilder {
+
+ override fun getFilter() = IntentFilter().apply {
+ addAction(ACTION_COPY)
+ }
+
+ override fun buildNotification(context: Context, builder: Notification.Builder) {
+ val copyIntent = createPendingIntent(context, ACTION_COPY, 1)
+
+ builder.setContentText(context.getString(R.string.mozac_feature_pwa_site_controls_notification_text))
+ builder.setContentIntent(copyIntent)
+ }
+
+ override fun onReceiveBroadcast(context: Context, tab: CustomTabSessionState, intent: Intent) {
+ when (intent.action) {
+ ACTION_COPY -> {
+ context.getSystemService<ClipboardManager>()?.let { clipboardManager ->
+ clipboardManager.setPrimaryClip(ClipData.newPlainText(tab.content.url, tab.content.url))
+ Toast.makeText(
+ context,
+ context.getString(R.string.mozac_feature_pwa_copy_success),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+ }
+
+ protected fun createPendingIntent(context: Context, action: String, requestCode: Int): PendingIntent {
+ val intent = Intent(action)
+ intent.setPackage(context.packageName)
+ return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntentUtils.defaultFlags)
+ }
+
+ companion object {
+ private const val ACTION_COPY = "mozilla.components.feature.pwa.COPY"
+ }
+ }
+
+ /**
+ * Implementation of [SiteControlsBuilder] that adds a Refresh button and
+ * copies the URL of the site when tapped.
+ */
+ class CopyAndRefresh(
+ private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
+ ) : Default() {
+
+ override fun getFilter() = super.getFilter().apply {
+ addAction(ACTION_REFRESH)
+ }
+
+ override fun buildNotification(context: Context, builder: Notification.Builder) {
+ super.buildNotification(context, builder)
+
+ val title = context.getString(R.string.mozac_feature_pwa_site_controls_refresh)
+ val intent = createPendingIntent(context, ACTION_REFRESH, 2)
+ val refreshAction = if (SDK_INT >= Build.VERSION_CODES.M) {
+ Notification.Action.Builder(
+ Icon.createWithResource(context, R.drawable.ic_refresh),
+ title,
+ intent,
+ )
+ } else {
+ @Suppress("Deprecation")
+ Notification.Action.Builder(R.drawable.ic_refresh, title, intent)
+ }.build()
+
+ builder.addAction(refreshAction)
+ }
+
+ override fun onReceiveBroadcast(context: Context, tab: CustomTabSessionState, intent: Intent) {
+ when (intent.action) {
+ ACTION_REFRESH -> reloadUrlUseCase(tab.id)
+ else -> super.onReceiveBroadcast(context, tab, intent)
+ }
+ }
+
+ companion object {
+ private const val ACTION_REFRESH = "mozilla.components.feature.pwa.REFRESH"
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppActivityFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppActivityFeature.kt
new file mode 100644
index 0000000000..81c6424641
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppActivityFeature.kt
@@ -0,0 +1,54 @@
+/* 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.pwa.feature
+
+import android.app.Activity
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.extension.toIconRequest
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.ext.applyOrientation
+import mozilla.components.feature.pwa.ext.toTaskDescription
+import mozilla.components.support.ktx.android.view.enterImmersiveMode
+
+/**
+ * Feature used to handle window effects for "standalone" and "fullscreen" web apps.
+ */
+class WebAppActivityFeature(
+ private val activity: Activity,
+ private val icons: BrowserIcons,
+ private val manifest: WebAppManifest,
+) : DefaultLifecycleObserver {
+
+ private val scope = MainScope()
+
+ override fun onResume(owner: LifecycleOwner) {
+ if (manifest.display == WebAppManifest.DisplayMode.FULLSCREEN) {
+ activity.enterImmersiveMode()
+ }
+
+ activity.applyOrientation(manifest)
+
+ scope.launch {
+ updateRecentEntry()
+ }
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ scope.cancel()
+ }
+
+ @VisibleForTesting
+ internal suspend fun updateRecentEntry() {
+ val icon = icons.loadIcon(manifest.toIconRequest()).await()
+
+ activity.setTaskDescription(manifest.toTaskDescription(icon.bitmap))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppContentFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppContentFeature.kt
new file mode 100644
index 0000000000..975c774716
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppContentFeature.kt
@@ -0,0 +1,30 @@
+/* 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.pwa.feature
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+
+/**
+ * Feature used to handle web content settings from manifest file.
+ */
+class WebAppContentFeature(
+ private val store: BrowserStore,
+ private val tabId: String? = null,
+ private val manifest: WebAppManifest,
+) : DefaultLifecycleObserver {
+
+ override fun onCreate(owner: LifecycleOwner) {
+ setDisplayMode(manifest.display)
+ }
+
+ private fun setDisplayMode(display: WebAppManifest.DisplayMode) {
+ val tab = store.state.findTabOrCustomTabOrSelectedTab(tabId)
+ tab?.engineState?.engineSession?.setDisplayMode(display)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeature.kt
new file mode 100644
index 0000000000..954793bb80
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeature.kt
@@ -0,0 +1,117 @@
+/* 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.pwa.feature
+
+import androidx.core.net.toUri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.pwa.ext.getTrustedScope
+import mozilla.components.feature.pwa.ext.trustedOrigins
+import mozilla.components.lib.state.ext.flow
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.android.net.isInScope
+
+/**
+ * Hides a custom tab toolbar for Progressive Web Apps and Trusted Web Activities.
+ *
+ * When the tab with [tabId] is inside a trusted scope, the toolbar will be hidden.
+ * Once the tab with [tabId] navigates to another scope, the toolbar will be revealed.
+ * The toolbar is also hidden in fullscreen mode or picture in picture mode.
+ *
+ * In standard custom tabs, no scopes are trusted.
+ * As a result the URL has no impact on toolbar visibility.
+ *
+ * @param store Reference to the browser store where tab state is located.
+ * @param customTabsStore Reference to the store that communicates with the custom tabs service.
+ * @param tabId ID of the tab session, or null if the selected session should be used.
+ * @param manifest Reference to the cached [WebAppManifest] for the current PWA.
+ * Null if this feature is not used in a PWA context.
+ * @param setToolbarVisibility Callback to show or hide the toolbar.
+ */
+class WebAppHideToolbarFeature(
+ private val store: BrowserStore,
+ private val customTabsStore: CustomTabsServiceStore,
+ private val tabId: String? = null,
+ manifest: WebAppManifest? = null,
+ private val setToolbarVisibility: (Boolean) -> Unit,
+) : LifecycleAwareFeature {
+
+ private val manifestScope = listOfNotNull(manifest?.getTrustedScope())
+ private var scope: CoroutineScope? = null
+
+ init {
+ // Hide the toolbar by default to prevent a flash.
+ val tab = store.state.findTabOrCustomTabOrSelectedTab(tabId)
+ val customTabState = customTabsStore.state.getCustomTabStateForTab(tab)
+ setToolbarVisibility(shouldToolbarBeVisible(tab, customTabState))
+ }
+
+ override fun start() {
+ scope = MainScope().apply {
+ launch {
+ // Since we subscribe to both store and customTabsStore,
+ // we don't extend another non-external-apps feature for hiding the toolbar
+ // as very little code would be shared.
+ val sessionFlow = store.flow()
+ .map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .distinctUntilChanged()
+ val customTabServiceMapFlow = customTabsStore.flow()
+
+ sessionFlow.combine(customTabServiceMapFlow) { tab, customTabServiceState ->
+ tab to customTabServiceState.getCustomTabStateForTab(tab)
+ }
+ .map { (tab, customTabState) -> shouldToolbarBeVisible(tab, customTabState) }
+ .distinctUntilChanged()
+ .collect { toolbarVisible ->
+ setToolbarVisibility(toolbarVisible)
+ }
+ }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ /**
+ * Reports if the toolbar should be shown for the given external app session.
+ * If the URL is in the same scope as the [WebAppManifest]
+ */
+ private fun shouldToolbarBeVisible(
+ session: SessionState?,
+ customTabState: CustomTabState?,
+ ): Boolean {
+ val url = session?.content?.url?.toUri() ?: return true
+
+ val trustedOrigins = customTabState?.trustedOrigins.orEmpty()
+ val inScope = url.isInScope(manifestScope + trustedOrigins)
+
+ return !inScope && !session.content.fullScreen && !session.content.pictureInPictureEnabled
+ }
+
+ /**
+ * Find corresponding custom tab state, if any.
+ */
+ private fun CustomTabsServiceState.getCustomTabStateForTab(
+ tab: SessionState?,
+ ): CustomTabState? {
+ return (tab as? CustomTabSessionState)?.config?.sessionToken?.let { sessionToken ->
+ tabs[sessionToken]
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeature.kt
new file mode 100644
index 0000000000..b53ce3ec5a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeature.kt
@@ -0,0 +1,198 @@
+/* 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.pwa.feature
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.BADGE_ICON_NONE
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.launch
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.extension.toMonochromeIconRequest
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.R
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.utils.ext.registerReceiverCompat
+
+/**
+ * Displays site controls notification for fullscreen web apps.
+ * @param sessionId ID of the web app session to observe.
+ * @param manifest Web App Manifest reference used to populate the notification.
+ * @param controlsBuilder Customizes the created notification.
+ */
+class WebAppSiteControlsFeature(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val sessionId: String,
+ private val manifest: WebAppManifest? = null,
+ private val controlsBuilder: SiteControlsBuilder = SiteControlsBuilder.Default(),
+ private val icons: BrowserIcons? = null,
+ private val notificationsDelegate: NotificationsDelegate,
+) : BroadcastReceiver(), DefaultLifecycleObserver {
+
+ constructor(
+ applicationContext: Context,
+ store: BrowserStore,
+ reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
+ sessionId: String,
+ manifest: WebAppManifest? = null,
+ controlsBuilder: SiteControlsBuilder = SiteControlsBuilder.CopyAndRefresh(reloadUrlUseCase),
+ icons: BrowserIcons? = null,
+ notificationsDelegate: NotificationsDelegate,
+ ) : this(
+ applicationContext,
+ store,
+ sessionId,
+ manifest,
+ controlsBuilder,
+ icons,
+ notificationsDelegate,
+ )
+
+ private var notificationIcon: Deferred<mozilla.components.browser.icons.Icon>? = null
+
+ /**
+ * Starts loading the [notificationIcon] on create.
+ */
+ override fun onCreate(owner: LifecycleOwner) {
+ if (SDK_INT >= Build.VERSION_CODES.M && manifest != null && icons != null) {
+ val request = manifest.toMonochromeIconRequest()
+ if (request.resources.isNotEmpty()) {
+ notificationIcon = icons.loadIcon(request)
+ }
+ }
+ }
+
+ /**
+ * Displays a notification from the given [SiteControlsBuilder.buildNotification] that will be
+ * shown as long as the lifecycle is in the foreground. Registers this class as a broadcast
+ * receiver to receive events from the notification and call [SiteControlsBuilder.onReceiveBroadcast].
+ */
+ override fun onResume(owner: LifecycleOwner) {
+ val filter = controlsBuilder.getFilter()
+ registerReceiver(filter)
+
+ val iconAsync = notificationIcon
+ if (iconAsync != null) {
+ owner.lifecycleScope.launch {
+ val bitmap = iconAsync.await().bitmap
+ notificationsDelegate.notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification(bitmap))
+ }
+ } else {
+ notificationsDelegate.notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification(null))
+ }
+ }
+
+ @VisibleForTesting
+ internal fun registerReceiver(filter: IntentFilter) {
+ applicationContext.registerReceiverCompat(
+ this,
+ filter,
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+ }
+
+ /**
+ * Cancels the site controls notification and unregisters the broadcast receiver.
+ */
+ override fun onPause(owner: LifecycleOwner) {
+ applicationContext.unregisterReceiver(this)
+
+ NotificationManagerCompat.from(applicationContext)
+ .cancel(NOTIFICATION_TAG, NOTIFICATION_ID)
+ }
+
+ /**
+ * Cancels the [notificationIcon] loading job on destroy.
+ */
+ override fun onDestroy(owner: LifecycleOwner) {
+ notificationIcon?.cancel()
+ }
+
+ /**
+ * Responds to [PendingIntent]s fired by the site controls notification.
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ store.state.findCustomTab(sessionId)?.also { tab ->
+ controlsBuilder.onReceiveBroadcast(context, tab, intent)
+ }
+ }
+
+ /**
+ * Build the notification with site controls to be displayed while the web app is active.
+ */
+ private fun buildNotification(icon: Bitmap?): Notification {
+ val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
+ val channelId = ensureChannelExists()
+ Notification.Builder(applicationContext, channelId).apply {
+ setBadgeIconType(BADGE_ICON_NONE)
+ }
+ } else {
+ @Suppress("Deprecation")
+ Notification.Builder(applicationContext).apply {
+ setPriority(Notification.PRIORITY_MIN)
+ }
+ }
+ if (icon != null && SDK_INT >= Build.VERSION_CODES.M) {
+ builder.setSmallIcon(Icon.createWithBitmap(icon))
+ } else {
+ builder.setSmallIcon(R.drawable.ic_pwa)
+ }
+ builder.setContentTitle(manifest?.name ?: manifest?.shortName)
+ builder.setColor(manifest?.themeColor ?: NotificationCompat.COLOR_DEFAULT)
+ builder.setShowWhen(false)
+ builder.setOngoing(true)
+ controlsBuilder.buildNotification(applicationContext, builder)
+ return builder.build()
+ }
+
+ /**
+ * Make sure a notification channel for site controls notifications exists.
+ *
+ * Returns the channel id to be used for notifications.
+ */
+ private fun ensureChannelExists(): String {
+ if (SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager: NotificationManager = applicationContext.getSystemService()!!
+
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ applicationContext.getString(R.string.mozac_feature_pwa_site_controls_notification_channel),
+ NotificationManager.IMPORTANCE_MIN,
+ )
+
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ return NOTIFICATION_CHANNEL_ID
+ }
+
+ companion object {
+ private const val NOTIFICATION_CHANNEL_ID = "Site Controls"
+ private const val NOTIFICATION_TAG = "SiteControls"
+ private const val NOTIFICATION_ID = 1
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt
new file mode 100644
index 0000000000..8038ad7a88
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt
@@ -0,0 +1,96 @@
+/* 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.pwa.intent
+
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsSessionToken
+import androidx.browser.trusted.TrustedWebActivityIntentBuilder.EXTRA_ADDITIONAL_TRUSTED_ORIGINS
+import androidx.core.net.toUri
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.feature.customtabs.createCustomTabConfigFromIntent
+import mozilla.components.feature.customtabs.feature.OriginVerifierFeature
+import mozilla.components.feature.customtabs.isTrustedWebActivityIntent
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.intent.ext.putSessionId
+import mozilla.components.feature.intent.processing.IntentProcessor
+import mozilla.components.feature.pwa.ext.toOrigin
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.utils.toSafeIntent
+
+/**
+ * Processor for intents which open Trusted Web Activities.
+ */
+@Deprecated("TWAs are not supported. See https://github.com/mozilla-mobile/android-components/issues/12024")
+class TrustedWebActivityIntentProcessor(
+ private val addNewTabUseCase: CustomTabsUseCases.AddCustomTabUseCase,
+ packageManager: PackageManager,
+ relationChecker: RelationChecker,
+ private val store: CustomTabsServiceStore,
+) : IntentProcessor {
+
+ private val verifier = OriginVerifierFeature(packageManager, relationChecker) { store.dispatch(it) }
+ private val scope = MainScope()
+
+ private fun matches(intent: Intent): Boolean {
+ val safeIntent = intent.toSafeIntent()
+ return safeIntent.action == ACTION_VIEW && isTrustedWebActivityIntent(safeIntent)
+ }
+
+ override fun process(intent: Intent): Boolean {
+ val safeIntent = SafeIntent(intent)
+ val url = safeIntent.dataString
+
+ return if (!url.isNullOrEmpty() && matches(intent)) {
+ val customTabConfig = createCustomTabConfigFromIntent(intent, null).copy(
+ externalAppType = ExternalAppType.TRUSTED_WEB_ACTIVITY,
+ )
+
+ val tabId = addNewTabUseCase.invoke(
+ url,
+ source = SessionState.Source.Internal.HomeScreen,
+ customTabConfig = customTabConfig,
+ )
+
+ intent.putSessionId(tabId)
+
+ customTabConfig.sessionToken?.let { token ->
+ val origin = listOfNotNull(safeIntent.data?.toOrigin())
+ val additionalOrigins = safeIntent
+ .getStringArrayListExtra(EXTRA_ADDITIONAL_TRUSTED_ORIGINS)
+ .orEmpty()
+ .mapNotNull { it.toUri().toOrigin() }
+
+ // Launch verification separately so the intent processing isn't held up
+ scope.launch {
+ verify(token, origin + additionalOrigins)
+ }
+ }
+
+ true
+ } else {
+ false
+ }
+ }
+
+ private suspend fun verify(token: CustomTabsSessionToken, origins: List<Uri>) {
+ val tabState = store.state.tabs[token] ?: return
+ origins.map { origin ->
+ scope.async {
+ verifier.verify(tabState, token, RELATION_HANDLE_ALL_URLS, origin)
+ }
+ }.awaitAll()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt
new file mode 100644
index 0000000000..5669638355
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt
@@ -0,0 +1,95 @@
+/* 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.pwa.intent
+
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.browser.state.state.SessionState.Source
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.intent.ext.putSessionId
+import mozilla.components.feature.intent.processing.IntentProcessor
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.ext.getUrlOverride
+import mozilla.components.feature.pwa.ext.putWebAppManifest
+import mozilla.components.feature.pwa.ext.toCustomTabConfig
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.utils.toSafeIntent
+
+/**
+ * Processor for intents which trigger actions related to web apps.
+ */
+class WebAppIntentProcessor(
+ private val store: BrowserStore,
+ private val addTabUseCase: CustomTabsUseCases.AddWebAppTabUseCase,
+ private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
+ private val storage: ManifestStorage,
+) : IntentProcessor {
+
+ /**
+ * Returns true if this intent should launch a progressive web app.
+ */
+ private fun matches(intent: Intent) =
+ intent.toSafeIntent().action == ACTION_VIEW_PWA
+
+ /**
+ * Processes the given [Intent] by creating a [Session] with a corresponding web app manifest.
+ *
+ * A custom tab config is also set so a custom tab toolbar can be shown when the user leaves
+ * the scope defined in the manifest.
+ */
+ override fun process(intent: Intent): Boolean {
+ val url = intent.toSafeIntent().dataString
+
+ return if (!url.isNullOrEmpty() && matches(intent)) {
+ val webAppManifest = runBlocking { storage.loadManifest(url) } ?: return false
+ val targetUrl = intent.getUrlOverride() ?: url
+
+ val id = findExistingSession(webAppManifest) ?: createSession(webAppManifest, url)
+
+ if (targetUrl !== url) {
+ loadUrlUseCase(targetUrl, id, EngineSession.LoadUrlFlags.external())
+ }
+
+ intent.flags = FLAG_ACTIVITY_NEW_DOCUMENT
+ intent.putSessionId(id)
+ intent.putWebAppManifest(webAppManifest)
+
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Returns an existing web app session that matches the manifest.
+ */
+ private fun findExistingSession(webAppManifest: WebAppManifest): String? {
+ return store.state.customTabs.find { tab ->
+ tab.config.externalAppType == ExternalAppType.PROGRESSIVE_WEB_APP &&
+ tab.content.webAppManifest?.startUrl == webAppManifest.startUrl
+ }?.id
+ }
+
+ /**
+ * Returns a new web app session.
+ */
+ private fun createSession(webAppManifest: WebAppManifest, url: String): String {
+ return addTabUseCase.invoke(
+ url = url,
+ source = Source.Internal.HomeScreen,
+ webAppManifest = webAppManifest,
+ customTabConfig = webAppManifest.toCustomTabConfig(),
+ )
+ }
+
+ companion object {
+ const val ACTION_VIEW_PWA = "mozilla.components.feature.pwa.VIEW_PWA"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_pwa.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_pwa.xml
new file mode 100644
index 0000000000..1c5e00215d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_pwa.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M16.72 14.42l0.58-1.46h1.67l-0.8-2.22 1-2.5L22 15.76h-2.1l-0.48-1.35h-2.7zM14.94 15.77l3.03-7.54h-2.01l-2.08 4.87-1.48-4.86h-1.54L9.27 13.1l-1.12-2.22-1 3.12 1.02 1.77h1.98l1.43-4.37 1.37 4.37h1.99zM3.91 13.18h1.24c0.38 0 0.71-0.04 1-0.13l0.32-0.98 0.9-2.76A2.2 2.2 0 0 0 7.14 9c-0.46-0.51-1.14-0.77-2.02-0.77H2v7.54h1.91v-2.59zm1.64-3.21c0.18 0.18 0.27 0.42 0.27 0.72s-0.08 0.55-0.24 0.73c-0.17 0.2-0.49 0.3-0.95 0.3H3.9V9.7h0.72c0.44 0 0.74 0.09 0.92 0.27z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_refresh.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_refresh.xml
new file mode 100644
index 0000000000..9f9d3120f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_refresh.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M21,4.04a0.96,0.96 0,0 0,-0.96 0.96V8a8.981,8.981 0,1 0,-1.676 10.361A1,1 0,0 0,16.95 16.95a7.031,7.031 0,1 1,1.72 -6.91H15a0.96,0.96 0,0 0,0 1.92h6a0.96,0.96 0,0 0,0.96 -0.96V5A0.96,0.96 0,0 0,21 4.04Z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..7a7d6451cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-am/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ድረ-ገፅ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">የሙሉ ማያ ገጽ መቆጣጠሪያዎች</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">የዚህን መተግበሪያ URL ለመቅዳት ይንኩ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">አድስ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ተቀድቷል።</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..0220920827
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-an/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Puesto web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controls d\'o puesto en pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca pa copiar la URL d\'esta aplicación</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refrescar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..39989440c7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ar/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">الموقع</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">تحكّمات الموقع حين يملأ الشاشة</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">انقر لنسخ مسار هذا التطبيق</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">أعِد التحميل</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">نُسِخَ المسار.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..5befbc6461
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ast/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitiu web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles de sitios a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca pa copiar la URL d\'esta aplicación</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Anovar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Copióse la URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..3405a6766e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-az/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sayt</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Tam ekran sayt idarəsi</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Bu tətbiqin ünvanını köçürtmək üçün toxunun</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Yenilə</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Ünvan köçürüldü.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..b7d75d181f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-azb/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">وبسایت</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">بوتون صفحه سایت کونترول‌لاری</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">بو اپ‌ین اینترنت آدرسینی کوپی ائتمک اوچون توخونون.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">رفرش</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">اینترنت آدرسی کوپی اولدو.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..15441343cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ban/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Situs web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrol situs layar penuh</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ketuk anggen nyalin URL ring aplikasi niki</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Segerang</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kasalin.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..29222db78a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-be/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Вэб-сайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Элементы кіравання поўнаэкранным сайтам</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Націсніце, каб скапіяваць URL для гэтай праграмы</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Абнавіць</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL скапіяваны.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..ad6a638ee7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bg/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Страница</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Управление сайт на цял екран</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">При докосване се копира адреса на приложението</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Презареждане</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Адресът е копиран.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..70fc4ae777
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bn/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ওয়েবসাইট</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">পূর্ণ পর্দার সাইট নিয়ন্ত্রণ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">অ্যাপের ইউআরএল অনুলিপি করাতে ট্যাপ করুন</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">রিফ্রেশ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL কপি হয়েছে</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..f54073d383
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-br/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Lec’hienn</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Reoliadurioù al lec’hienn er skramm a-bezh</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Stokit da eilañ an URL evit an arload-mañ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Azbevaat</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL eilet.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..0966d3a0e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bs/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Web stranica</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen kontrole sajta</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Dodirnite za kopiranje URL-a za ovu aplikaciju</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Osvježi</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopiran.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..16131de509
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ca/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Lloc web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controls de lloc en pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toqueu per copiar l’URL d’aquesta aplicació</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualitza</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">S’ha copiat l’URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..91544b69e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cak/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Ruxaq Ajk\'amaya\'l</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Ruchajixik ruxaq pa tz\'aqät ruwa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tachapa\' richin nawachib\'ej ri URL richin re chokoy re\'</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Titzolïx</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL xwachib\'ëx</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..6083a4c297
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen nga control sa site</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">i-Tap para makopya ang URL ani nga app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL nakopya na.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..a3ce4aa97a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ماڵپەڕ</string>
+
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">پەنجە دابگرە بۆ لەبەرگرتنەوەی بەستەری ئەم بەرنامەیە</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">نوێکردنەوە</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">بەستەر لەبەرگیرا.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..bce7b4f5fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-co/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Situ web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Cuntrolli di situ di u screnu sanu</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Picchichjà per cupià l’indirizzu per st’appiecazione</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Attualizà</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Indirizzu web cupiatu.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..03818cb038
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cs/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Server</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Ovládací prvky režimu celé obrazovky</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Klepnutím zkopírujte URL adresu pro tuto aplikaci</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Znovu načíst</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL adresa zkopírována.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..97cc456da3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cy/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Gwefan</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Rheolaethau gwefan sgrin lawn</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tapio i gopïo’r URL ar gyfer yr ap hwn</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Adnewyddu</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL wedi ei gopïo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..39491424b5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-da/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Websted</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrolelementer til websted i fuld skærm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tryk for at kopiere webadressen til denne app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Opdater</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopieret.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..71f1d22443
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-de/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Website-Steuerelemente im Vollbildmodus</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tippen Sie hier, um die URL für diese App zu kopieren</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Aktualisieren</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Adresse kopiert.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..6cde676900
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Websedło</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Wóźeńske elementy sedła w połnej wobrazowce</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Pótusniśo how, aby URL za toś to nałoženje kopěrował</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Aktualizěrowaś</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL jo kopěrowany.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..c558ee23a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-el/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Ιστότοπος</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Κουμπιά ελέγχου πλήρους οθόνης</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Πατήστε για αντιγραφή URL για αυτήν την εφαρμογή</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Ανανέωση</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Το URL αντιγράφτηκε.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..35087f8f0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen site controls</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tap to copy the URL for this app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copied.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..3db32067f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Web site</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen site controls</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tap to copy the URL for this app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copied.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..05a6de8646
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-eo/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Retejo</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Regiloj por retejo en plenekrana reĝimo</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tuŝetu por kopii la retadreson por tiu ĉi programo</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refreŝigi</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Retadreso kopiita.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..bcc3bd196e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio para pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocá para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Recargar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..ae5c98253c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refrescar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..ae5c98253c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refrescar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..9e298da4ab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Recargar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..ae5c98253c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refrescar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..69e329519f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-et/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sait</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Täisekraanil saidi juhtnupud</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Puuduta selle äpi URLi kopeerimiseks</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Värskenda</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopeeritud.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..242ee3b357
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-eu/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Webgunea</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Pantaila osoaren gune-kontrolak</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Sakatu aplikazio honen URLa kopiatzeko</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Berritu</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URLa kopiatu da</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..efbe1dcc1b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fa/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">پایگاه اینترنتی</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">واپایش‌های پایگاه در حالت تمام‌صفحه</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">برای رونوشت از نشانی این کاره ضربه بزنید</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">بازخوانی</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">نشانی رونوشت شد.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..908a906e22
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fi/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Verkkosivusto</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Koko näytön sivuston säätimet</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Napauta kopioidaksesi tämän sovelluksen verkko-osoitteen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Päivitä</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Osoite kopioitu.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..15df8f7e02
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Contrôles du site en plein écran</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Appuyez pour copier l’URL de cette application</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualiser</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Adresse web copiée.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..f0c272c217
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fur/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sît web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controi dal sît a plen schermi</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocje par copiâ l’URL par cheste aplicazion</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Inzorne</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiât.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..c2afbbb476
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sitebetsjinning folslein skerm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tik om de URL foar dizze app te kopiearjen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Fernije</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopiearre.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..1f2a1324cc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gd/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Làrach-lìn</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Uidheaman-smachd na làraich airson na làn-sgrìn</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Thoir gnogag airson lethbhreac a dhèanamh de URL na h-aplacaid seo</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Ath-nuadhaich</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Chaidh lethbhreac dhen URL a dhèanamh.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..4aa4400b79
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gl/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controis do sitio en pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toque para copiar o URL desta aplicación</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualizar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Copiouse o URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..e57bf2ce43
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gn/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Ñanduti renda</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Tenda ñeñangreko mba’erechaha tuichavévape</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Eikutu embohasa hag̃ua URL ko tembiporu’ípe.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Mbopiro’y</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL monguatiapyre</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..ada97dea46
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">વેબસાઇટ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..39ed3f939e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">वेबसाइट</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">पूर्ण स्क्रीन साइट नियंत्रक </string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">इस ऐप के लिए URL को कॉपी करने के लिए टैप करें</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">रीफ़्रेश करें</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL कॉपी हो गया।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..568bf7f541
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hil/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..03f463821d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Web stranica</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrole stranice u prikazu preko cijelog ekrana</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Pritisnite za kopiranje URLa za ovu aplikaciju</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Osvježi</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL je kopiran.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..633468894a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Websydło</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Wodźenske elementy sydła w połnej wobrazowce</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Podótkńće so tu, zo byšće URL za tute nałoženje kopěrował</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Aktualizować</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL je kopěrowany.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..61641ae97b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hu/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Webhely</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Teljes képernyős webhelyvezérlők</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Koppintson az alkalmazás URL-jének másolásához</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Frissítés</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL másolva.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..003da434df
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Կայք</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Լիաէկրան կայքի կառավարում</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Հպեք՝ այս հավելվածի համար URL-ն պատճենելու համար</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Թարմացնել</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL-ն պատճենվել է</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..c0aa5b6764
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ia/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sito web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controlos del sito a plen schermo</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocca pro copiar le URL de iste app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualisar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiate.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..803706cbf5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-in/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Situs web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kendali situs layar penuh</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ketuk untuk menyalin URL untuk apl ini</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Segarkan</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL tersalin.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..d0a050f9e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-is/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Vefsvæði</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Stýringar vefsvæðis á öllum skjánum</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ýttu til að afrita slóðina fyrir þetta forrit</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Endurlesa</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Vistfang afritað.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..2988de3743
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-it/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sito web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controlli del sito a schermo intero</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocca per copiare l’indirizzo per questa app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Aggiorna</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Indirizzo copiato.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..f498515972
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-iw/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">אתר</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">פקדי אתר במסך מלא</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">יש להקיש כדי להעתיק את הכתובת עבור יישומון זה</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">רענון</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">הקישור הועתק.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..02fc90da65
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ja/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ウェブサイト</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">全画面サイトコントロール</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">タップしてこのアプリの URL をコピー</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">再読み込み</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL をコピーしました。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..4f1536cdea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ka/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ვებსაიტი</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">საიტის სამართავი სრულ ეკრანზე</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">შეეხეთ, ბმულის ასლის ასაღებად ამ აპისთვის</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">განახლება</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">ბმული აღებულია.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..9541854019
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sayt</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sayttı tolıq ekranda kórıw basqarıwları</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text"> Bul baǵdarlamaǵa siltemeni kóshirip alıw ushın basıń</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Jańalaw</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kóshirip alındı.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..1eb4cd2882
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kab/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Asmel Web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Isenqaden n ugdil aččuran n usmel</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Senned akken ad tneɣleḍ tansa URL i usnas-a</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Smiren</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Tansa URL tettwanɣel.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..d9eba97a18
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kk/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Веб-сайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Толық экрандағы сайттарды басқару элементтері</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Бұл қолданбаның URL сілтемесін көшіру үшін шертіңіз</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Жаңарту</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL сілтемесі көшірілді.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..91c7e53cac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Malper</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrolên malpera ekrana tije</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ji bo navnîşana vê sepanê kopî bikî, bitikîne</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Nû bike</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Navnîşan hate kopîkirin.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..14b0a62186
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kn/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ಜಾಲತಾಣ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">ಪೂರ್ಣ ಪರದೆ ಜಾಲ ನಿಯಂತ್ರಣಗಳು</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..fa6ca5c1e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ko/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">웹 사이트</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">전체 화면 사이트 컨트롤</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">이 앱의 URL을 복사하려면 누르세요</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">새로 고침</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL 복사됨.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..a2b17fe149
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-lo/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ເວັບໄຊທ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">ການຄວບຄຸມເວັບໄຊທແບບເຕັ້ມຫນ້າຈໍ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ແຕະເພື່ອສຳເນົາ URL ສຳລັບແອັບນີ້</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ລີເຟສ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">ສຳເນົາ URL ແລ້ວ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..078d9b4364
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-lt/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Svetainė</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Svetainės valdikliai viso ekrano veiksenoje</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Bakstelėkite, norėdami nukopijuoti šios programos URL</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Atnaujinti</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL nukopijuotas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..ae0c204c72
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-mix/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio Web</string>
+
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Ndatava URL</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..e7a5b0bb4c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-mr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">संकेतस्थळ</string>
+
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">पुनःदाखल करा</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ची प्रत बनवली.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..a47c9238b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-my/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ဝဘ်ဆိုက်</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">မျက်နှာပြင်အပြည့် ဆိုက် ထိန်းချုပ်မှု</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ဤ အက်ပ် အတွက် URL ကူးယူရန်  ထိတို့ ပါ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ပြန်ရယူရန်</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ကူးပြီး။</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..e474cf3d04
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Nettsted</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrollelementer til nettsted i fullskjerm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Trykk for å kopiere nettadressen for denne appen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Oppdater</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopiert.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..91dc6c3930
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">वेबसाइट</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">पूर्ण स्क्रिन साइट नियन्त्रणहरू</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">यो एपको URL कपि गर्नका लागि ट्याप गर्नुहोस्</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ताजा गर्नुहोस्</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL कपि गरियो।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..6001040634
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nl/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sitebediening volledig scherm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tik om de URL voor deze app te kopiëren</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Vernieuwen</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL gekopieerd.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..137d50a6ad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Nettstad</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrollelement til nettstad i fullskjerm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Trykk for å kopiere nettadressa for denne appen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Oppdater</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopiert.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..8467e79ffe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-oc/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Contraròtles del plen ecran</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocatz per copiar l’URL d’aquesta aplicacion</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualizar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..5879423c5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-or/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL କପି କରିନିଆଗଲା।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..8d5d5ed3c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ਵੈੱਬਸਾਈਟ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">ਪੂਰੀ ਸਕਰੀਨ ਸਾਈਟ ਕੰਟਰੋਲ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ਇਸ ਐਪ ਲਈ URL ਨੂੰ ਕਾਪੀ ਕਰਨ ਲਈ ਛੂਹੋ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ਤਾਜ਼ਾ ਕਰੋ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ਕਾਪੀ ਕੀਤਾ।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..8b74c1300b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ویب‌سائٹ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">پوری سکرین دیاں چوݨاں</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ایس ایپ لئی پتے نوں کاپی کرن لئی چھوہو</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">تازہ کرو</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">کاپی کیتا گیا۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..ee86506d6e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pl/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Witryna</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sterowanie witryną pełnoekranową</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Stuknij, aby skopiować adres tej aplikacji</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Odśwież</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Skopiowano adres</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..3190b55f61
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles de site em tela inteira</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toque para copiar a URL deste aplicativo</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Atualizar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..a1c384e152
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controlos de site em ecrã completo</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toque para copiar o endereço para esta aplicação</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Atualizar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Endereço copiado.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..5fe99cd6fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-rm/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controllas da la website en maletg entir</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tutgar per copiar l\'URL da questa app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualisar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Copià l\'URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..6505043d1f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ro/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Comenzi pentru site la vizualizare pe tot ecranul</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Atinge pentru copierea URL-ului acestei aplicații</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Reîmprospătează</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiat.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..b06c6827b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ru/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Сайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Управление полноэкранным просмотром сайта</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Нажмите, чтобы скопировать ссылку для этого приложения</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Обновить</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Сетевой адрес скопирован.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..574d4473ff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sat/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ᱣᱮᱵᱥᱟᱭᱤᱴ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">ᱯᱩᱨᱟᱹ ᱤᱥᱠᱨᱤᱱ ᱥᱟᱭᱤᱴ ᱥᱟᱸᱢᱲᱟᱣ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ᱱᱚᱶᱟ ᱮᱯ ᱨᱮᱭᱟᱜ URL ᱱᱚᱠᱚᱞ ᱞᱟᱹᱜᱤᱫ ᱛᱮ ᱡᱚᱴᱮᱫ ᱢᱮ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ᱱᱟᱶᱟ ᱟᱹᱨᱩ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ᱱᱚᱠᱚᱞᱮᱱᱟ ᱾</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..7e4fd21077
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sc/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Situ web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controllos de sitos a ischermu intreu</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca pro copiare s’URL pro custa aplicatzione</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Atualiza</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiadu.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..1b5d601500
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-si/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">අඩවිය</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">පූර්ණ තිර අඩවි පාලන</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">මෙම යෙදුම සඳහා ඒ.ස.නි. පිටපත් කිරීමට ඔබන්න</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">නැවුම් කරන්න</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">ඒ.ස.නි. පිටපත් විය</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..5c4d020bb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sk/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Webová stránka</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Ovládacie prvky pre stránku v režime celej obrazovky</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ťuknutím skopírujete webovú adresu pre túto aplikáciu</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Obnoviť</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Adresa bola skopírovaná.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..c22b7fc8f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-skr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ویب سائٹ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">فل سکرین سائٹ کنٹرول</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ایں ایپ کنوں یوآرایل دی نقل کرݨ کیتے انگل پھیرو</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">تازہ کرو</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">یوآرایل نقل تھی ڳیا۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..8e1c5cc9be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sl/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Spletno mesto</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrolniki celozaslonskega načina</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tapnite, da kopirate spletni naslov za to aplikacijo</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Osveži</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Spletni naslov kopiran.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..39c3df59b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sq/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sajt</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrolle sajti sa krejt ekrani</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Prekeni që të kopjohet URL-ja për këtë aplikacion</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Rifreskoje</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL-ja u kopjua.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..6a9bec3c3b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Веб страница</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Контроле странице на целом екрану</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Додирните да копирате адресу за ову апликацију</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Освежи</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Адреса је копирана.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..631735b8d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-su/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Raramatloka</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrol loka layar pinuh</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toél pikeun niron URL jang ieu aplikasi</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Segerkeun</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ditiron.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..de149d7368
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Webbplats</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Helskärmskontroller på webbsidan</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tryck om du vill kopiera webbadressen till appen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Uppdatera</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopierad</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..ff445ac441
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ta/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">வலைத்தளம்</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">முழு திரை தள கட்டுப்பாடுகள்</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">இச்செயலிக்கான உரலியை நகலெடுக்க தட்டவும்</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">புதுப்பி</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">தொடுப்பு நகலெடுக்கப்பட்டது.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..ad827968d3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-te/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">వెబ్‌సైటు</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">నిండు తెర సైటు నియంత్రణలు</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ఈ అనువర్తనపు చిరునామాను కాపీచేసుకోడానికి తాకండి</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">రిఫ్రెష్ చేయి</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL కాపీ అయ్యింది.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..414344900a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tg/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Сомона</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Идоракунии намоиши сомона дар экрани пурра</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Барои нусха бардоштани нишонии ин барнома зарба занед</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Нав кардан</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Нишонӣ нусха бардошта шуд.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..31c77f5ac2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-th/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">เว็บไซต์</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">การควบคุมไซต์แบบเต็มหน้าจอ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">แตะเพื่อคัดลอก URL สำหรับแอปนี้</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">เรียกใหม่</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">คัดลอก URL แล้ว</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..f354c17c1a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tl/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Mga full screen site control</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">i-Tap para makopya ang URL para sa app na ito</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">i-Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Nakopya na ang URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..cfdc11dd95
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Web sitesi</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Tam ekran site kontrolleri</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Bu uygulamanın adresini kopyalamak için dokunun</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Yenile</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Adres kopyalandı.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..6bb020883e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-trs/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Si kontrô sîtio guendâ nahuin gachrà\’ riña aga\’a</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Gūru\’man ra\’a da\’ gūxūnt si URL app nan</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Nāgi\'iaj nākà</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Ngà naka URL</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..fb48394eff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tt/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Веб-сайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Тулы экрандагы сайтлар белән идарә итү</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Бу кушымтаның URL сылтамасын күчереп алу өчен басыгыз</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Яңарту</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL копияләнде.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..3985de99de
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Asmel Web</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..8b222cc65c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ug/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">تور بېكەت</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">بېكەت پۈتۈن ئېكران كونترولى</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">چېكىلسە ئەپنىڭ تور ئادرېسىنى كۆچۈرىدۇ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">يېڭىلا</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL كۆچۈرۈلدى.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..b73352ef4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-uk/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Вебсайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Елементи керування сайтом в повноекранному режимі</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Торкніться, щоб скопіювати URL-адресу цієї програми</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Оновити</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL скопійовано.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..688c60ee5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ur/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ویب سائٹ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">فل سکرین سائٹ کنٹرول</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">اس ایپ سے URL کی نقل کرنے کے لئے ٹیپ پڑیں</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">تازہ کریں</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL کی نقل کی گئی۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..6b50d12b0d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-uz/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sayt</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Saytni toʻliq ekranda koʻrish boshqaruvlari</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Bu ilova uchun URLdan nusxa olish uchun bosing</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Yangilash</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URLdan nusxa olindi.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..1c69b01df5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-vi/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Trang web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kiểm soát trang web toàn màn hình</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Nhấn để sao chép URL cho ứng dụng này</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Làm mới</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Đã sao chép URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..8f67cb1d1c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-yo/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Ojúlé wẹ́ẹ̀bù</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sáíìtì ìṣàkóso ìwòran gbogbogbò</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tẹ̀ẹ́ to bá fẹ́ da URL fún áàpù yí kọ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Sọdọ̀tun</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ti wà ní àdàkọ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..0830d7ca1e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">网站</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">网站全屏控件</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">点按即可复制此应用程序的网址</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">刷新</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">网址已复制。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..72649c8bc8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">網站</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">全螢幕網站控制元件</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">點擊即可複製此應用程式的網址</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">重新整理</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">已複製網址。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..c53d2c2b16
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?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>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen site controls</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tap to copy the URL for this app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copied.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values/styles.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..46b03ddac9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values/styles.xml
@@ -0,0 +1,16 @@
+<?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>
+
+ <style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.NoActionBar">
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt
new file mode 100644
index 0000000000..078619b892
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt
@@ -0,0 +1,305 @@
+/* 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.pwa
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.db.ManifestDao
+import mozilla.components.feature.pwa.db.ManifestEntity
+import mozilla.components.support.test.any
+import mozilla.components.support.test.capture
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+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.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class ManifestStorageTest {
+
+ private val firefoxManifest = WebAppManifest(
+ name = "Firefox",
+ startUrl = "https://firefox.com",
+ scope = "/",
+ )
+
+ private val googleMapsManifest = WebAppManifest(
+ name = "Google Maps",
+ startUrl = "https://google.com/maps",
+ scope = "https://google.com/maps/",
+ )
+
+ private val exampleWebAppManifest = WebAppManifest(
+ name = "Example Web App",
+ startUrl = "https://pwa.example.com/dashboard",
+ scope = "https://pwa.example.com/",
+ )
+
+ @Test
+ fun `load returns null if entry does not exist`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ mockDatabase(storage)
+ assertNull(storage.loadManifest("https://example.com"))
+ }
+
+ @Test
+ fun `load returns valid manifest`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org")
+ whenever(dao.getManifest("https://mozilla.org"))
+ .thenReturn(ManifestEntity(manifest))
+
+ assertEquals(manifest, storage.loadManifest("https://mozilla.org"))
+ }
+
+ @Test
+ fun `save saves the manifest as JSON`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ storage.saveManifest(firefoxManifest)
+ verify(dao).insertManifest(any())
+ Unit
+ }
+
+ @Test
+ fun `update replaces the manifest as JSON`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val existing = ManifestEntity(firefoxManifest, currentTime = 0)
+
+ `when`(dao.getManifest("https://firefox.com")).thenReturn(existing)
+
+ storage.updateManifest(firefoxManifest)
+ verify(dao).updateManifest(any())
+ Unit
+ }
+
+ @Test
+ fun `update does not replace non-existed manifest`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ `when`(dao.getManifest("https://firefox.com")).thenReturn(null)
+
+ storage.updateManifest(firefoxManifest)
+ verify(dao, never()).updateManifest(any())
+ Unit
+ }
+
+ @Test
+ fun `remove deletes saved manifests`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ storage.removeManifests(listOf("https://example.com", "https://proxx.app"))
+ verify(dao).deleteManifests(listOf("https://example.com", "https://proxx.app"))
+ Unit
+ }
+
+ @Test
+ fun `loading manifests by scope returns list of manifests`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val manifest1 = WebAppManifest(name = "Mozilla1", startUrl = "https://mozilla.org", scope = "https://mozilla.org/pwa/1/")
+ val manifest2 = WebAppManifest(name = "Mozilla2", startUrl = "https://mozilla.org", scope = "https://mozilla.org/pwa/1/")
+ val manifest3 = WebAppManifest(name = "Mozilla3", startUrl = "https://mozilla.org", scope = "https://mozilla.org/pwa/")
+
+ whenever(dao.getManifestsByScope("https://mozilla.org/index.html?key=value"))
+ .thenReturn(listOf(ManifestEntity(manifest1), ManifestEntity(manifest2), ManifestEntity(manifest3)))
+
+ assertEquals(
+ listOf(manifest1, manifest2, manifest3),
+ storage.loadManifestsByScope("https://mozilla.org/index.html?key=value"),
+ )
+ }
+
+ @Test
+ fun `loading manifests with share targets returns list of manifests`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val manifest1 = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ shareTarget = WebAppManifest.ShareTarget("https://mozilla.org/share"),
+ )
+ val manifest2 = WebAppManifest(
+ name = "Firefox",
+ startUrl = "https://firefox.com",
+ shareTarget = WebAppManifest.ShareTarget("https://firefox.com/share"),
+ )
+ val timeout = ManifestStorage.ACTIVE_THRESHOLD_MS
+ val currentTime = System.currentTimeMillis()
+ val deadline = currentTime - timeout
+
+ whenever(dao.getRecentShareableManifests(deadline))
+ .thenReturn(listOf(ManifestEntity(manifest1), ManifestEntity(manifest2)))
+
+ assertEquals(
+ listOf(manifest1, manifest2),
+ storage.loadShareableManifests(currentTime),
+ )
+ }
+
+ @Test
+ fun `updateManifestUsedAt updates usedAt to current timestamp`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org")
+ val entity = ManifestEntity(manifest, currentTime = 0)
+
+ val entityCaptor = ArgumentCaptor.forClass(ManifestEntity::class.java)
+
+ whenever(dao.getManifest(manifest.startUrl))
+ .thenReturn(entity)
+
+ assertEquals(0, entity.usedAt)
+
+ storage.updateManifestUsedAt(manifest)
+
+ verify(dao).updateManifest(capture<ManifestEntity>(entityCaptor))
+ assert(entityCaptor.value.usedAt > 0)
+ }
+
+ @Test
+ fun `has recent manifest returns false if no manifest is found`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val timeout = ManifestStorage.ACTIVE_THRESHOLD_MS
+ val currentTime = System.currentTimeMillis()
+ val deadline = currentTime - timeout
+
+ whenever(dao.hasRecentManifest("https://mozilla.org/", deadline))
+ .thenReturn(0)
+
+ assertFalse(storage.hasRecentManifest("https://mozilla.org/", currentTime))
+ }
+
+ @Test
+ fun `has recent manifest returns true if one or more manifests have been found`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val timeout = ManifestStorage.ACTIVE_THRESHOLD_MS
+ val currentTime = System.currentTimeMillis()
+ val deadline = currentTime - timeout
+
+ whenever(dao.hasRecentManifest("https://mozilla.org/", deadline))
+ .thenReturn(1)
+
+ assertTrue(storage.hasRecentManifest("https://mozilla.org/", currentTime))
+
+ whenever(dao.hasRecentManifest("https://mozilla.org/", deadline))
+ .thenReturn(5)
+
+ assertTrue(storage.hasRecentManifest("https://mozilla.org/", currentTime))
+ }
+
+ @Test
+ fun `recently used manifest count`() = runTest {
+ val testThreshold = 1000 * 60 * 24L
+ val storage = spy(ManifestStorage(testContext, activeThresholdMs = testThreshold))
+ val dao = mockDatabase(storage)
+ val currentTime = System.currentTimeMillis()
+ val deadline = currentTime - testThreshold
+
+ whenever(dao.recentManifestsCount(deadline))
+ .thenReturn(0)
+
+ assertEquals(0, storage.recentManifestsCount(currentTimeMs = currentTime))
+
+ whenever(dao.recentManifestsCount(deadline))
+ .thenReturn(5)
+
+ assertEquals(5, storage.recentManifestsCount(currentTimeMs = currentTime))
+
+ whenever(dao.recentManifestsCount(deadline - 10L))
+ .thenReturn(3)
+
+ assertEquals(
+ 3,
+ storage.recentManifestsCount(
+ activeThresholdMs = testThreshold + 10L,
+ currentTimeMs = currentTime,
+ ),
+ )
+ }
+
+ @Test
+ fun `warmUpScopes populates cache of already installed web app scopes`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ val manifest1 = ManifestEntity(manifest = firefoxManifest, currentTime = 0)
+ val manifest2 = ManifestEntity(manifest = googleMapsManifest, currentTime = 0)
+ val manifest3 = ManifestEntity(manifest = exampleWebAppManifest, currentTime = 0)
+
+ whenever(dao.getInstalledScopes(0)).thenReturn(listOf(manifest1, manifest2, manifest3))
+
+ storage.warmUpScopes(ManifestStorage.ACTIVE_THRESHOLD_MS)
+
+ assertEquals(
+ mapOf(
+ Pair("/", "https://firefox.com"),
+ Pair("https://google.com/maps/", "https://google.com/maps"),
+ Pair("https://pwa.example.com/", "https://pwa.example.com/dashboard"),
+ ),
+ storage.installedScopes,
+ )
+ }
+
+ @Test
+ fun `getInstalledScope returns cached scope for an url`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ val manifest1 = ManifestEntity(manifest = firefoxManifest, currentTime = 0)
+ val manifest2 = ManifestEntity(manifest = googleMapsManifest, currentTime = 0)
+ val manifest3 = ManifestEntity(manifest = exampleWebAppManifest, currentTime = 0)
+
+ whenever(dao.getInstalledScopes(0)).thenReturn(listOf(manifest1, manifest2, manifest3))
+
+ storage.warmUpScopes(ManifestStorage.ACTIVE_THRESHOLD_MS)
+
+ val result = storage.getInstalledScope("https://pwa.example.com/profile/me")
+
+ assertEquals("https://pwa.example.com/", result)
+ }
+
+ @Test
+ fun `getStartUrlForInstalledScope returns cached start url for a currently installed scope`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ val manifest1 = ManifestEntity(manifest = firefoxManifest, currentTime = 0)
+ val manifest2 = ManifestEntity(manifest = googleMapsManifest, currentTime = 0)
+ val manifest3 = ManifestEntity(manifest = exampleWebAppManifest, currentTime = 0)
+
+ whenever(dao.getInstalledScopes(0)).thenReturn(listOf(manifest1, manifest2, manifest3))
+
+ storage.warmUpScopes(ManifestStorage.ACTIVE_THRESHOLD_MS)
+
+ val result = storage.getStartUrlForInstalledScope("https://pwa.example.com/")
+
+ assertEquals("https://pwa.example.com/dashboard", result)
+ }
+
+ private fun mockDatabase(storage: ManifestStorage): ManifestDao = mock<ManifestDao>().also {
+ storage.manifestDao = lazy { it }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppInterceptorTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppInterceptorTest.kt
new file mode 100644
index 0000000000..9d0219c53d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppInterceptorTest.kt
@@ -0,0 +1,102 @@
+/* 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.pwa
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WebAppInterceptorTest {
+ private lateinit var mockContext: Context
+ private lateinit var mockEngineSession: EngineSession
+ private lateinit var mockManifestStorage: ManifestStorage
+ private lateinit var webAppInterceptor: WebAppInterceptor
+
+ private val webUrl = "https://example.com"
+ private val webUrlWithWebApp = "https://google.com/maps/"
+ private val webUrlOutOfScope = "https://google.com/search/"
+
+ @Before
+ fun setup() {
+ mockContext = mock()
+ mockEngineSession = mock()
+ mockManifestStorage = mock()
+
+ webAppInterceptor = WebAppInterceptor(
+ context = mockContext,
+ manifestStorage = mockManifestStorage,
+ launchFromInterceptor = true,
+ )
+ }
+
+ @Test
+ fun `request is intercepted when navigating to an installed web app`() {
+ whenever(mockManifestStorage.getInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrlWithWebApp, null, true, false, false, false, false)
+
+ assert(response is RequestInterceptor.InterceptionResponse.Deny)
+ }
+
+ @Test
+ fun `request is not intercepted when url is out of scope`() {
+ whenever(mockManifestStorage.getInstalledScope(webUrlOutOfScope)).thenReturn(null)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrlOutOfScope)).thenReturn(null)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrlOutOfScope, null, true, false, false, false, false)
+
+ assertNull(response)
+ }
+
+ @Test
+ fun `request is not intercepted when url is not part of a web app`() {
+ whenever(mockManifestStorage.getInstalledScope(webUrl)).thenReturn(null)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrl)).thenReturn(null)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrl, null, true, false, false, false, false)
+
+ assertNull(response)
+ }
+
+ @Test
+ fun `request is intercepted with app intent if not launchFromInterceptor`() {
+ webAppInterceptor = WebAppInterceptor(
+ context = mockContext,
+ manifestStorage = mockManifestStorage,
+ launchFromInterceptor = false,
+ )
+
+ whenever(mockManifestStorage.getInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrlWithWebApp, null, true, false, false, false, false)
+
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `launchFromInterceptor is enabled by default`() {
+ webAppInterceptor = WebAppInterceptor(
+ context = mockContext,
+ manifestStorage = mockManifestStorage,
+ )
+
+ whenever(mockManifestStorage.getInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrlWithWebApp, null, true, false, false, false, false)
+
+ assert(response is RequestInterceptor.InterceptionResponse.Deny)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppLauncherActivityTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppLauncherActivityTest.kt
new file mode 100644
index 0000000000..3a3049b904
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppLauncherActivityTest.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.pwa
+
+import android.content.Intent
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import org.junit.Assert.assertEquals
+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.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class WebAppLauncherActivityTest {
+
+ private val baseManifest = WebAppManifest(
+ name = "Test",
+ startUrl = "https://www.mozilla.org",
+ )
+
+ @Test
+ fun `DisplayMode-Browser launches browser`() {
+ val activity = spy(WebAppLauncherActivity())
+ doNothing().`when`(activity).launchBrowser(any())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.BROWSER)
+
+ activity.routeManifest(manifest.startUrl.toUri(), manifest)
+
+ verify(activity).launchBrowser(manifest.startUrl.toUri())
+ }
+
+ @Test
+ fun `DisplayMode-minimalui launches web app shell`() {
+ val activity = spy(WebAppLauncherActivity())
+ doNothing().`when`(activity).launchWebAppShell("https://www.mozilla.org".toUri())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.MINIMAL_UI)
+
+ activity.routeManifest(manifest.startUrl.toUri(), manifest)
+
+ verify(activity).launchWebAppShell(manifest.startUrl.toUri())
+ }
+
+ @Test
+ fun `DisplayMode-fullscreen launches web app shell`() {
+ val activity = spy(WebAppLauncherActivity())
+ doNothing().`when`(activity).launchWebAppShell("https://www.mozilla.org".toUri())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.FULLSCREEN)
+
+ activity.routeManifest(manifest.startUrl.toUri(), manifest)
+
+ verify(activity).launchWebAppShell(manifest.startUrl.toUri())
+ }
+
+ @Test
+ fun `DisplayMode-standalone launches web app shell`() {
+ val activity = spy(WebAppLauncherActivity())
+ doNothing().`when`(activity).launchWebAppShell("https://www.mozilla.org".toUri())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.STANDALONE)
+
+ activity.routeManifest(manifest.startUrl.toUri(), manifest)
+
+ verify(activity).launchWebAppShell(manifest.startUrl.toUri())
+ }
+
+ @Test
+ fun `launchBrowser starts activity with VIEW intent`() {
+ val activity = spy(WebAppLauncherActivity())
+ doReturn("test").`when`(activity).packageName
+ doNothing().`when`(activity).startActivity(any())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.BROWSER)
+
+ activity.launchBrowser(manifest.startUrl.toUri())
+
+ val captor = argumentCaptor<Intent>()
+ verify(activity).startActivity(captor.capture())
+
+ assertEquals(Intent.ACTION_VIEW, captor.value.action)
+ assertEquals("https://www.mozilla.org", captor.value.data!!.toString())
+ assertEquals("test", captor.value.`package`)
+ }
+
+ @Test
+ fun `launchWebAppShell starts activity with SHELL intent`() {
+ val activity = spy(WebAppLauncherActivity())
+ doReturn("test").`when`(activity).packageName
+ doNothing().`when`(activity).startActivity(any())
+
+ val url = "https://example.com".toUri()
+
+ activity.launchWebAppShell(url)
+
+ val captor = argumentCaptor<Intent>()
+ verify(activity).startActivity(captor.capture())
+
+ assertEquals(ACTION_VIEW_PWA, captor.value.action)
+ assertEquals(url, captor.value.data)
+ assertEquals("test", captor.value.`package`)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt
new file mode 100644
index 0000000000..48f556829c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt
@@ -0,0 +1,355 @@
+/* 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.pwa
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.os.Build
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.graphics.drawable.IconCompat
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.feature.pwa.WebAppLauncherActivity.Companion.ACTION_PWA_LAUNCHER
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.util.ReflectionHelpers.setStaticField
+import kotlin.reflect.jvm.javaField
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class WebAppShortcutManagerTest {
+ private lateinit var context: Context
+
+ @Mock private lateinit var httpClient: Client
+
+ @Mock private lateinit var packageManager: PackageManager
+
+ @Mock private lateinit var shortcutManager: ShortcutManager
+
+ @Mock private lateinit var storage: ManifestStorage
+
+ @Mock private lateinit var icons: BrowserIcons
+ private lateinit var manager: WebAppShortcutManager
+ private val baseManifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://example.com",
+ )
+
+ @Before
+ fun setup() {
+ setSdkInt(0)
+ openMocks(this)
+ context = spy(testContext)
+
+ doReturn(packageManager).`when`(context).packageManager
+ doReturn(shortcutManager).`when`(context).getSystemService(ShortcutManager::class.java)
+ doReturn("").`when`(context).getString(R.string.mozac_feature_pwa_default_shortcut_label)
+
+ manager = spy(WebAppShortcutManager(context, httpClient, storage))
+ doReturn(icons).`when`(manager).icons
+ }
+
+ @After
+ fun teardown() = setSdkInt(0)
+
+ @Test
+ fun `requestPinShortcut no-op if pinning unsupported`() = runTest {
+ val manifest = baseManifest.copy(
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://example.com/icon.png",
+ sizes = listOf(Size(192, 192)),
+ ),
+ ),
+ )
+ val session = buildInstallableSession(manifest)
+ @Suppress("DEPRECATION")
+ `when`(packageManager.queryBroadcastReceivers(any(), anyInt())).thenReturn(emptyList())
+
+ manager.requestPinShortcut(context, session)
+ verify(manager, never()).buildWebAppShortcut(context, manifest)
+
+ setSdkInt(Build.VERSION_CODES.O)
+ `when`(shortcutManager.isRequestPinShortcutSupported).thenReturn(false)
+ clearInvocations(manager)
+
+ manager.requestPinShortcut(context, session)
+ verify(manager, never()).buildWebAppShortcut(context, manifest)
+ }
+
+ @Test
+ fun `requestPinShortcut won't make a PWA icon if the session is not installable`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+ val manifest = baseManifest.copy(
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = emptyList(), // no icons
+ )
+ val session = buildInstallableSession(manifest)
+ val shortcutCompat: ShortcutInfoCompat = mock()
+ `when`(shortcutManager.isRequestPinShortcutSupported).thenReturn(true)
+ doReturn(shortcutCompat).`when`(manager).buildBasicShortcut(context, session)
+
+ manager.requestPinShortcut(context, session)
+ verify(manager, never()).buildWebAppShortcut(context, manifest)
+ verify(manager).buildBasicShortcut(context, session)
+ }
+
+ @Test
+ fun `requestPinShortcut pins PWA shortcut`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val manifest = baseManifest.copy(
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://example.com/icon.png",
+ sizes = listOf(Size(192, 192)),
+ ),
+ ),
+ )
+
+ val session = buildInstallableSession(manifest)
+
+ val shortcutCompat: ShortcutInfoCompat = mock()
+ `when`(shortcutManager.isRequestPinShortcutSupported).thenReturn(true)
+ doReturn(shortcutCompat).`when`(manager).buildWebAppShortcut(context, manifest)
+
+ manager.requestPinShortcut(context, session)
+ verify(manager).buildWebAppShortcut(context, manifest)
+ verify(shortcutManager).requestPinShortcut(any(), any())
+ }
+
+ @Test
+ fun `requestPinShortcut pins basic shortcut`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val session = buildInstallableSession()
+
+ val shortcutCompat: ShortcutInfoCompat = mock()
+ `when`(shortcutManager.isRequestPinShortcutSupported).thenReturn(true)
+ doReturn(shortcutCompat).`when`(manager).buildBasicShortcut(context, session)
+
+ manager.requestPinShortcut(context, session)
+ verify(manager).buildBasicShortcut(context, session)
+ verify(shortcutManager).requestPinShortcut(any(), any())
+ }
+
+ @Test
+ fun `buildBasicShortcut uses manifest short name as label by default`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val session = createTab("https://www.mozilla.org", title = "Internet for people, not profit — Mozilla").let {
+ it.copy(
+ content = it.content.copy(
+ webAppManifest = WebAppManifest(
+ name = "Mozilla",
+ shortName = "Moz",
+ startUrl = "https://mozilla.org",
+ ),
+ ),
+ )
+ }
+
+ val shortcut = manager.buildBasicShortcut(context, session)
+
+ assertEquals("Moz", shortcut.shortLabel)
+ }
+
+ @Test
+ fun `buildBasicShortcut uses manifest name as label by default`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val session = createTab("https://www.mozilla.org", title = "Internet for people, not profit — Mozilla").let {
+ it.copy(
+ content = it.content.copy(
+ webAppManifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ ),
+ ),
+ )
+ }
+
+ val shortcut = manager.buildBasicShortcut(context, session)
+
+ assertEquals("Mozilla", shortcut.shortLabel)
+ }
+
+ @Test
+ fun `buildBasicShortcut uses session title as label if there is no manifest`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val expectedTitle = "Internet for people, not profit — Mozilla"
+
+ val session = createTab("https://mozilla.org", title = expectedTitle)
+
+ val shortcut = manager.buildBasicShortcut(context, session)
+
+ assertEquals(expectedTitle, shortcut.shortLabel)
+ }
+
+ @Test
+ fun `buildBasicShortcut can create a shortcut with a custom name`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val title = "Internet for people, not profit — Mozilla"
+ val expectedName = "Mozilla"
+
+ val session = createTab("https://mozilla.org", title = title)
+
+ val shortcut = manager.buildBasicShortcut(context, session, expectedName)
+
+ assertEquals(expectedName, shortcut.shortLabel)
+ }
+
+ @Test
+ fun `updateShortcuts no-op`() = runTest {
+ val manifests = listOf(baseManifest)
+ doReturn(null).`when`(manager).buildWebAppShortcut(context, manifests[0])
+
+ manager.updateShortcuts(context, manifests)
+ verify(manager, never()).buildWebAppShortcut(context, manifests[0])
+ verify(shortcutManager, never()).updateShortcuts(any())
+
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+ manager.updateShortcuts(context, manifests)
+ verify(shortcutManager).updateShortcuts(emptyList())
+ }
+
+ @Test
+ fun `updateShortcuts updates list of existing shortcuts`() = runTest {
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+ val manifests = listOf(baseManifest)
+ val shortcutCompat: ShortcutInfoCompat = mock()
+ val shortcut: ShortcutInfo = mock()
+ doReturn(shortcutCompat).`when`(manager).buildWebAppShortcut(context, manifests[0])
+ doReturn(shortcut).`when`(shortcutCompat).toShortcutInfo()
+
+ manager.updateShortcuts(context, manifests)
+ verify(shortcutManager).updateShortcuts(listOf(shortcut))
+ }
+
+ @Test
+ fun `buildWebAppShortcut builds shortcut and saves manifest`() = runTest {
+ doReturn(mock<IconCompat>()).`when`(manager).buildIconFromManifest(baseManifest)
+
+ val shortcut = manager.buildWebAppShortcut(context, baseManifest)!!
+ val intent = shortcut.intent
+
+ verify(storage).saveManifest(baseManifest)
+
+ assertEquals("https://example.com", shortcut.id)
+ assertEquals("Demo", shortcut.longLabel)
+ assertEquals("Demo", shortcut.shortLabel)
+ assertEquals(ACTION_PWA_LAUNCHER, intent.action)
+ assertEquals("https://example.com".toUri(), intent.data)
+ }
+
+ @Test
+ fun `buildWebAppShortcut builds shortcut with short name`() = runTest {
+ val manifest = WebAppManifest(name = "Demo Demo", shortName = "DD", startUrl = "https://example.com")
+ doReturn(mock<IconCompat>()).`when`(manager).buildIconFromManifest(manifest)
+
+ val shortcut = manager.buildWebAppShortcut(context, manifest)!!
+
+ assertEquals("https://example.com", shortcut.id)
+ assertEquals("Demo Demo", shortcut.longLabel)
+ assertEquals("DD", shortcut.shortLabel)
+ }
+
+ @Test
+ fun `findShortcut returns shortcut`() {
+ assertNull(manager.findShortcut(context, "https://mozilla.org"))
+
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+ val exampleShortcut = mock<ShortcutInfo>().apply {
+ `when`(id).thenReturn("https://example.com")
+ }
+ `when`(shortcutManager.pinnedShortcuts).thenReturn(listOf(exampleShortcut))
+
+ assertNull(manager.findShortcut(context, "https://mozilla.org"))
+
+ val mozShortcut = mock<ShortcutInfo>().apply {
+ `when`(id).thenReturn("https://mozilla.org")
+ }
+ `when`(shortcutManager.pinnedShortcuts).thenReturn(listOf(mozShortcut, exampleShortcut))
+
+ assertEquals(mozShortcut, manager.findShortcut(context, "https://mozilla.org"))
+ }
+
+ @Test
+ fun `checking unknown url returns uninstalled state`() = runTest {
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+
+ val url = "https://mozilla.org"
+ val currentTime = System.currentTimeMillis()
+
+ whenever(storage.hasRecentManifest(url, currentTime))
+ .thenReturn(false)
+
+ val installState = manager.getWebAppInstallState(url, currentTime)
+
+ assertEquals(WebAppShortcutManager.WebAppInstallState.NotInstalled, installState)
+ }
+
+ @Test
+ fun `checking a known url returns installed state`() = runTest {
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+
+ val url = "https://mozilla.org/pwa/"
+ val currentTime = System.currentTimeMillis()
+
+ whenever(storage.hasRecentManifest(url, currentTime))
+ .thenReturn(true)
+
+ val installState = manager.getWebAppInstallState(url, currentTime)
+
+ assertEquals(WebAppShortcutManager.WebAppInstallState.Installed, installState)
+ }
+
+ private fun setSdkInt(sdkVersion: Int) {
+ setStaticField(Build.VERSION::SDK_INT.javaField, sdkVersion)
+ }
+
+ private fun buildInstallableSession(manifest: WebAppManifest? = null): SessionState {
+ val tab = createTab(manifest?.startUrl ?: "https://www.mozilla.org")
+
+ return tab.copy(
+ content = tab.content.copy(
+ webAppManifest = manifest,
+ securityInfo = SecurityInfoState(secure = true),
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt
new file mode 100644
index 0000000000..5bc24230f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt
@@ -0,0 +1,149 @@
+/* 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.pwa
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class WebAppUseCasesTest {
+ @Test
+ fun `isInstallable returns false if currentSession has no manifest`() {
+ val session = createTestSession(
+ secure = true,
+ manifest = null,
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(session),
+ selectedTabId = session.id,
+ ),
+ )
+
+ val webAppUseCases = WebAppUseCases(testContext, store, mock<WebAppShortcutManager>())
+ assertFalse(webAppUseCases.isInstallable())
+ }
+
+ @Test
+ fun `isInstallable returns true if currentSession has a manifest`() {
+ val manifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://example.com",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://example.com/icon.png",
+ sizes = listOf(Size(192, 192)),
+ ),
+ ),
+ )
+
+ val session = createTestSession(secure = true, manifest = manifest)
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(session),
+ selectedTabId = session.id,
+ ),
+ )
+
+ val shortcutManager: WebAppShortcutManager = mock()
+ `when`(shortcutManager.supportWebApps).thenReturn(true)
+
+ val webAppUseCases = WebAppUseCases(testContext, store, shortcutManager)
+ assertTrue(webAppUseCases.isInstallable())
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `isInstallable returns false if supportWebApps is false`() {
+ val manifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://example.com",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://example.com/icon.png",
+ sizes = listOf(Size(192, 192)),
+ ),
+ ),
+ )
+
+ val session = createTestSession(
+ secure = true,
+ manifest = manifest,
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(session),
+ selectedTabId = session.id,
+ ),
+ )
+
+ val shortcutManager: WebAppShortcutManager = mock()
+ `when`(shortcutManager.supportWebApps).thenReturn(false)
+
+ assertFalse(WebAppUseCases(testContext, store, shortcutManager).isInstallable())
+ }
+
+ @Test
+ fun `getInstallState returns Installed if manifest exists`() = runTest {
+ val httpClient: Client = mock()
+ val storage: ManifestStorage = mock()
+ val shortcutManager = WebAppShortcutManager(testContext, httpClient, storage)
+ val currentTime = System.currentTimeMillis()
+
+ val session = createTestSession(secure = true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(session),
+ selectedTabId = session.id,
+ ),
+ )
+
+ `when`(storage.hasRecentManifest("https://www.mozilla.org", currentTime)).thenReturn(true)
+
+ assertEquals(WebAppShortcutManager.WebAppInstallState.Installed, WebAppUseCases(testContext, store, shortcutManager).getInstallState(currentTime))
+ }
+}
+
+private fun createTestSession(
+ secure: Boolean,
+ manifest: WebAppManifest? = null,
+): TabSessionState {
+ val protocol = if (secure) {
+ "https"
+ } else {
+ "http"
+ }
+ val tab = createTab("$protocol://www.mozilla.org")
+
+ return tab.copy(
+ content = tab.content.copy(
+ securityInfo = SecurityInfoState(secure = secure),
+ webAppManifest = manifest,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/ActivityKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/ActivityKtTest.kt
new file mode 100644
index 0000000000..6721427f40
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/ActivityKtTest.kt
@@ -0,0 +1,89 @@
+/* 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.pwa.ext
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.view.reportFullyDrawnSafe
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class ActivityKtTest {
+ private val baseManifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "/",
+ )
+
+ private lateinit var activity: Activity
+ private lateinit var logger: Logger
+
+ @Before
+ fun setUp() {
+ activity = mock()
+ logger = mock()
+ }
+
+ @Test
+ fun `applyOrientation calls setRequestedOrientation for every value`() {
+ WebAppManifest.Orientation.values().forEach { orientation ->
+ val activity: Activity = mock()
+ activity.applyOrientation(baseManifest.copy(orientation = orientation))
+ verify(activity).requestedOrientation = anyInt()
+ }
+ }
+
+ @Test
+ fun `applyOrientation applies common orientations`() {
+ run {
+ val activity: Activity = mock()
+ activity.applyOrientation(baseManifest.copy(orientation = WebAppManifest.Orientation.ANY))
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ }
+
+ run {
+ val activity: Activity = mock()
+ activity.applyOrientation(baseManifest.copy(orientation = WebAppManifest.Orientation.PORTRAIT))
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+ }
+
+ run {
+ val activity: Activity = mock()
+ activity.applyOrientation(baseManifest.copy(orientation = WebAppManifest.Orientation.LANDSCAPE))
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ }
+
+ run {
+ val activity: Activity = mock()
+ activity.applyOrientation(null)
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ }
+ }
+
+ @Test
+ fun `WHEN reportFullyDrawnSafe is called THEN reportFullyDrawn is called`() {
+ activity.reportFullyDrawnSafe(logger)
+ verify(activity).reportFullyDrawn()
+ }
+
+ @Test
+ fun `GIVEN reportFullyDrawn throws a SecurityException WHEN reportFullyDrawnSafe is called THEN the exception is caught and a log statement with fully drawn is logged`() {
+ val expectedSecurityException = SecurityException()
+ `when`(activity.reportFullyDrawn()).thenThrow(expectedSecurityException)
+ activity.reportFullyDrawnSafe(logger) // If an exception is thrown, this test will fail.
+
+ val msgArg = argumentCaptor<String>()
+ verify(logger).error(msgArg.capture(), eq(expectedSecurityException))
+ assertTrue(msgArg.value, msgArg.value.contains("Fully drawn"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/CustomTabStateKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/CustomTabStateKtTest.kt
new file mode 100644
index 0000000000..03974a3942
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/CustomTabStateKtTest.kt
@@ -0,0 +1,59 @@
+/* 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.pwa.ext
+
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.OriginRelationPair
+import mozilla.components.feature.customtabs.store.VerificationStatus.FAILURE
+import mozilla.components.feature.customtabs.store.VerificationStatus.PENDING
+import mozilla.components.feature.customtabs.store.VerificationStatus.SUCCESS
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CustomTabStateKtTest {
+
+ @Test
+ fun `trustedOrigins is empty when there are no relationships`() {
+ val state = CustomTabState(relationships = emptyMap())
+ assertEquals(emptyList<Uri>(), state.trustedOrigins)
+ }
+
+ @Test
+ fun `trustedOrigins only includes the HANDLE_ALL_URLS relationship`() {
+ val state = CustomTabState(
+ relationships = mapOf(
+ OriginRelationPair("https://firefox.com".toUri(), RELATION_HANDLE_ALL_URLS) to SUCCESS,
+ OriginRelationPair("https://example.com".toUri(), RELATION_USE_AS_ORIGIN) to SUCCESS,
+ OriginRelationPair("https://mozilla.org".toUri(), RELATION_HANDLE_ALL_URLS) to PENDING,
+ ),
+ )
+ assertEquals(
+ listOf("https://firefox.com".toUri(), "https://mozilla.org".toUri()),
+ state.trustedOrigins,
+ )
+ }
+
+ @Test
+ fun `trustedOrigins only includes pending or success statuses`() {
+ val state = CustomTabState(
+ relationships = mapOf(
+ OriginRelationPair("https://firefox.com".toUri(), RELATION_HANDLE_ALL_URLS) to SUCCESS,
+ OriginRelationPair("https://example.com".toUri(), RELATION_USE_AS_ORIGIN) to FAILURE,
+ OriginRelationPair("https://mozilla.org".toUri(), RELATION_HANDLE_ALL_URLS) to PENDING,
+ ),
+ )
+ assertEquals(
+ listOf("https://firefox.com".toUri(), "https://mozilla.org".toUri()),
+ state.trustedOrigins,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/SessionStateKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/SessionStateKtTest.kt
new file mode 100644
index 0000000000..24009f5279
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/SessionStateKtTest.kt
@@ -0,0 +1,169 @@
+/* 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.pwa.ext
+
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class SessionStateKtTest {
+ private val demoManifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://mozilla.com",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ )
+ private val demoIcon = WebAppManifest.Icon(src = "https://mozilla.com/example.png")
+
+ @Test
+ fun `web app must be HTTPS to be installable`() {
+ val httpSession = createTestSession(secure = false)
+ assertNull(httpSession.installableManifest())
+ }
+
+ @Test
+ fun `web app must have manifest to be installable`() {
+ val noManifestSession = createTestSession(
+ secure = true,
+ manifest = null,
+ )
+ assertNull(noManifestSession.installableManifest())
+ }
+
+ @Test
+ fun `web app must have an icon to be installable`() {
+ val noIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest,
+ )
+ assertNull(noIconSession.installableManifest())
+
+ val noSizeIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(icons = listOf(demoIcon)),
+ )
+ assertNull(noSizeIconSession.installableManifest())
+
+ val onlyBadgeIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(
+ sizes = listOf(Size(512, 512)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ ),
+ )
+ assertNull(onlyBadgeIconSession.installableManifest())
+ }
+
+ @Test
+ fun `web app must have 192x192 icons to be installable`() {
+ val smallIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(32, 32))),
+ ),
+ ),
+ )
+ assertNull(smallIconSession.installableManifest())
+
+ val weirdSizeSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(50, 200))),
+ ),
+ ),
+ )
+ assertNull(weirdSizeSession.installableManifest())
+
+ val largeIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(192, 192))),
+ ),
+ ),
+ )
+ assertEquals(
+ demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(192, 192))),
+ ),
+ ),
+ largeIconSession.installableManifest(),
+ )
+
+ val multiSizeIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(16, 16), Size(512, 512))),
+ ),
+ ),
+ )
+ assertEquals(
+ demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(16, 16), Size(512, 512))),
+ ),
+ ),
+ multiSizeIconSession.installableManifest(),
+ )
+
+ val multiIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(191, 193))),
+ demoIcon.copy(sizes = listOf(Size(512, 512))),
+ demoIcon.copy(
+ sizes = listOf(Size(192, 192)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ ),
+ )
+ assertEquals(
+ demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(191, 193))),
+ demoIcon.copy(sizes = listOf(Size(512, 512))),
+ demoIcon.copy(
+ sizes = listOf(Size(192, 192)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ ),
+ multiIconSession.installableManifest(),
+ )
+ }
+}
+
+private fun createTestSession(
+ secure: Boolean,
+ manifest: WebAppManifest? = null,
+): SessionState {
+ val protocol = if (secure) {
+ "https"
+ } else {
+ "http"
+ }
+ val tab = createTab("$protocol://www.mozilla.org")
+
+ return tab.copy(
+ content = tab.content.copy(
+ securityInfo = SecurityInfoState(secure = secure),
+ webAppManifest = manifest,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/UriKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/UriKtTest.kt
new file mode 100644
index 0000000000..52911f0ca3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/UriKtTest.kt
@@ -0,0 +1,56 @@
+/* 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.pwa.ext
+
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.android.net.sameHostWithoutMobileSubdomainAs
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UriKtTest {
+
+ @Test
+ fun `extracts scheme, host and port`() {
+ assertEquals("https://example.com", "https://example.com".toUri().toOrigin())
+ assertEquals("http://mozilla.org:80", "http://mozilla.org:80".toUri().toOrigin())
+ assertEquals("http://localhost:8080", "http://localhost:8080".toUri().toOrigin())
+ }
+
+ @Test
+ fun `removes user info`() {
+ assertEquals("https://example.com", "https://bob@example.com".toUri().toOrigin())
+ assertEquals("http://google.com", "HTTP://bob:pass@google.com".toUri().toOrigin())
+ }
+
+ @Test
+ fun `removes path`() {
+ assertEquals("https://example.com", "https://example.com/".toUri().toOrigin())
+ assertEquals("http://google.com", "http://google.com/search".toUri().toOrigin())
+ assertEquals("http://firefox.com", "http://firefox.com/en-US/foo".toUri().toOrigin())
+ }
+
+ @Test
+ fun `preserves missing scheme`() {
+ assertNull("example.com".toUri().toOrigin())
+ assertNull("/foo/bar".toUri().toOrigin())
+ }
+
+ @Test
+ fun `GIVEN Uris having the same host, one containing mobile subdomains WHEN compared THEN they have the same host without mobile subdomains`() {
+ assertTrue("https://m.youtube.com".toUri().sameHostWithoutMobileSubdomainAs("https://www.youtube.com".toUri()))
+ assertTrue("https://en.m.wikipedia.com".toUri().sameHostWithoutMobileSubdomainAs("https://en.wikipedia.com".toUri()))
+ assertFalse("https://m.en.youtube.com".toUri().sameHostWithoutMobileSubdomainAs("https://www.youtube.com".toUri()))
+ assertFalse("https://en.m.wikipedia.com".toUri().sameHostWithoutMobileSubdomainAs("https://it.wikipedia.com".toUri()))
+ }
+
+ private fun assertEquals(expected: String, actual: Uri?) = assertEquals(expected.toUri(), actual)
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/WebAppManifestKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/WebAppManifestKtTest.kt
new file mode 100644
index 0000000000..25240745f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/WebAppManifestKtTest.kt
@@ -0,0 +1,172 @@
+/* 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.pwa.ext
+
+import android.graphics.Color
+import android.graphics.Color.rgb
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WebAppManifestKtTest {
+
+ private val demoManifest = WebAppManifest(name = "Demo", startUrl = "https://mozilla.com")
+ private val demoIcon = WebAppManifest.Icon(src = "https://mozilla.com/example.png")
+
+ @Test
+ fun `should use name as label`() {
+ val taskDescription = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://example.com",
+ ).toTaskDescription(null)
+ assertEquals("Demo", taskDescription.label)
+ assertEquals(0, taskDescription.primaryColor)
+ }
+
+ @Test
+ fun `should use themeColor as primaryColor`() {
+ val taskDescription = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com",
+ themeColor = rgb(255, 0, 255),
+ ).toTaskDescription(null)
+ assertEquals("My App", taskDescription.label)
+ assertEquals(rgb(255, 0, 255), taskDescription.primaryColor)
+ }
+
+ @Test
+ fun `should use themeColor as toolbarColor`() {
+ val config = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com",
+ themeColor = rgb(255, 0, 255),
+ backgroundColor = rgb(230, 230, 230),
+ ).toCustomTabConfig()
+ assertEquals(rgb(255, 0, 255), config.colorSchemes?.defaultColorSchemeParams?.toolbarColor)
+ assertEquals(Color.WHITE, config.colorSchemes?.defaultColorSchemeParams?.navigationBarColor)
+ assertNull(config.closeButtonIcon)
+ assertTrue(config.enableUrlbarHiding)
+ assertNull(config.actionButtonConfig)
+ assertTrue(config.showShareMenuItem)
+ assertEquals(0, config.menuItems.size)
+ }
+
+ @Test
+ fun `should return the scope as a uri`() {
+ val scope = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com/pwa",
+ scope = "https://example.com/",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ ).getTrustedScope()
+ assertEquals("https://example.com/".toUri(), scope)
+
+ val fallbackToStartUrl = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com/pwa",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ ).getTrustedScope()
+ assertEquals("https://example.com/pwa".toUri(), fallbackToStartUrl)
+ }
+
+ @Test
+ fun `should not return the scope if display mode is minimal-ui`() {
+ val scope = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com/pwa",
+ scope = "https://example.com/",
+ display = WebAppManifest.DisplayMode.MINIMAL_UI,
+ ).getTrustedScope()
+ assertNull(scope)
+
+ val fallbackToStartUrl = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com/pwa",
+ display = WebAppManifest.DisplayMode.MINIMAL_UI,
+ ).getTrustedScope()
+ assertNull(fallbackToStartUrl)
+ }
+
+ @Test
+ fun `web app must have an icon to be installable`() {
+ val noIconManifest = demoManifest
+ assertFalse(noIconManifest.hasLargeIcons())
+
+ val noSizeIconManifest = demoManifest.copy(icons = listOf(demoIcon))
+ assertFalse(noSizeIconManifest.hasLargeIcons())
+
+ val onlyBadgeIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(
+ sizes = listOf(Size(512, 512)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ )
+ assertFalse(onlyBadgeIconManifest.hasLargeIcons())
+ }
+
+ @Test
+ fun `web app must have 192x192 icons to be installable`() {
+ val smallIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(32, 32))),
+ ),
+ )
+ assertFalse(smallIconManifest.hasLargeIcons())
+
+ val weirdSizeManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(50, 200))),
+ ),
+ )
+ assertFalse(weirdSizeManifest.hasLargeIcons())
+
+ val largeIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(192, 192))),
+ ),
+ )
+ assertTrue(largeIconManifest.hasLargeIcons())
+
+ val multiSizeIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(16, 16), Size(512, 512))),
+ ),
+ )
+ assertTrue(multiSizeIconManifest.hasLargeIcons())
+
+ val multiIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(191, 193))),
+ demoIcon.copy(sizes = listOf(Size(512, 512))),
+ demoIcon.copy(
+ sizes = listOf(Size(192, 192)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ )
+ assertTrue(multiIconManifest.hasLargeIcons())
+
+ val onlyBadgeManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(191, 191))),
+ demoIcon.copy(
+ sizes = listOf(Size(192, 192)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ )
+ assertFalse(onlyBadgeManifest.hasLargeIcons())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt
new file mode 100644
index 0000000000..27b07cefb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt
@@ -0,0 +1,255 @@
+/* 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.pwa.feature
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.WebAppShortcutManager
+import mozilla.components.support.test.any
+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.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ManifestUpdateFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var shortcutManager: WebAppShortcutManager
+ private lateinit var storage: ManifestStorage
+ private lateinit var store: BrowserStore
+
+ private val sessionId = "external-app-session-id"
+ private val baseManifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ scope = "https://mozilla.org",
+ )
+
+ @Before
+ fun setUp() {
+ storage = mock()
+ shortcutManager = mock()
+
+ store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(
+ createCustomTab("https://mozilla.org", id = sessionId),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `start and stop handle null session`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ "not existing",
+ baseManifest,
+ )
+
+ feature.start()
+
+ store.waitUntilIdle()
+
+ feature.stop()
+
+ verify(storage).updateManifestUsedAt(baseManifest)
+ verify(storage, never()).updateManifest(any())
+ }
+
+ @Test
+ fun `Last usage is updated when feature is started`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ // Insert base manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ feature.updateUsageJob!!.joinBlocking()
+
+ verify(storage).updateManifestUsedAt(baseManifest)
+ }
+
+ @Test
+ fun `updateStoredManifest is called when the manifest changes`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ // Insert base manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ val newManifest = baseManifest.copy(shortName = "Moz")
+
+ // Update manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ newManifest,
+ ),
+ ).joinBlocking()
+
+ feature.updateJob!!.joinBlocking()
+
+ verify(storage).updateManifest(newManifest)
+ }
+
+ @Test
+ fun `updateStoredManifest is not called when the manifest is the same`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ feature.start()
+
+ // Update manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.updateJob?.joinBlocking()
+
+ verify(storage, never()).updateManifest(any())
+ }
+
+ @Test
+ fun `updateStoredManifest is not called when the manifest is removed`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ // Insert base manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ // Update manifest
+ store.dispatch(
+ ContentAction.RemoveWebAppManifestAction(
+ sessionId,
+ ),
+ ).joinBlocking()
+
+ feature.updateJob?.joinBlocking()
+
+ verify(storage, never()).updateManifest(any())
+ }
+
+ @Test
+ fun `updateStoredManifest is not called when the manifest has a different start URL`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ // Insert base manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ // Update manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ WebAppManifest(name = "Mozilla", startUrl = "https://netscape.com"),
+ ),
+ ).joinBlocking()
+
+ feature.updateJob?.joinBlocking()
+
+ verify(storage, never()).updateManifest(any())
+ }
+
+ @Test
+ fun `updateStoredManifest updates storage and shortcut`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(testContext, store, shortcutManager, storage, sessionId, baseManifest)
+
+ val manifest = baseManifest.copy(shortName = "Moz")
+ feature.updateStoredManifest(manifest)
+
+ verify(storage).updateManifest(manifest)
+ verify(shortcutManager).updateShortcuts(testContext, listOf(manifest))
+ }
+
+ @Test
+ fun `start updates last web app usage`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(testContext, store, shortcutManager, storage, sessionId, baseManifest)
+
+ feature.start()
+
+ verify(storage).updateManifestUsedAt(baseManifest)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppActivityFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppActivityFeatureTest.kt
new file mode 100644
index 0000000000..9d3a844223
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppActivityFeatureTest.kt
@@ -0,0 +1,95 @@
+/* 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.pwa.feature
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import android.os.Looper.getMainLooper
+import android.view.View
+import android.view.Window
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CompletableDeferred
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.Icon
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class WebAppActivityFeatureTest {
+
+ @Mock private lateinit var activity: Activity
+
+ @Mock private lateinit var window: Window
+
+ @Mock private lateinit var decorView: View
+
+ @Mock private lateinit var layoutParams: WindowManager.LayoutParams
+
+ @Mock private lateinit var icons: BrowserIcons
+
+ @Before
+ fun setup() {
+ openMocks(this)
+
+ `when`(activity.window).thenReturn(window)
+ `when`(window.decorView).thenReturn(decorView)
+ `when`(window.attributes).thenReturn(layoutParams)
+ `when`(icons.loadIcon(any())).thenReturn(CompletableDeferred(mock<Icon>()))
+ }
+
+ @Test
+ fun `enters immersive mode only when display mode is fullscreen`() {
+ val basicManifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://mozilla.com",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ )
+ WebAppActivityFeature(activity, icons, basicManifest).onResume(mock())
+
+ val fullscreenManifest = basicManifest.copy(
+ display = WebAppManifest.DisplayMode.FULLSCREEN,
+ )
+ WebAppActivityFeature(activity, icons, fullscreenManifest).onResume(mock())
+ }
+
+ @Test
+ fun `applies orientation`() {
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "/",
+ orientation = WebAppManifest.Orientation.LANDSCAPE,
+ )
+
+ WebAppActivityFeature(activity, icons, manifest).onResume(mock())
+
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `sets task description`() {
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "/",
+ )
+ val icon = Icon(mock(), source = Icon.Source.GENERATOR)
+ `when`(icons.loadIcon(any())).thenReturn(CompletableDeferred(icon))
+
+ WebAppActivityFeature(activity, icons, manifest).onResume(mock())
+ shadowOf(getMainLooper()).idle()
+
+ verify(activity).setTaskDescription(any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppContentFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppContentFeatureTest.kt
new file mode 100644
index 0000000000..ee2c1f323a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppContentFeatureTest.kt
@@ -0,0 +1,53 @@
+/* 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.pwa.feature
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class WebAppContentFeatureTest {
+ private val customTabId = "custom-id"
+
+ @Test
+ fun `display mode is fullscreen based on PWA manifest`() {
+ val engineSession = mock<EngineSession>()
+ val engineState = EngineState(engineSession = engineSession)
+
+ val tab = CustomTabSessionState(
+ id = customTabId,
+ content = ContentState("https://mozilla.org"),
+ config = CustomTabConfig(),
+ engineState = engineState,
+ )
+
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val manifest = mockManifest(WebAppManifest.DisplayMode.FULLSCREEN)
+
+ val feature = WebAppContentFeature(
+ store,
+ tabId = tab.id,
+ manifest = manifest,
+ )
+ feature.onCreate(mock())
+
+ verify(engineSession).setDisplayMode(WebAppManifest.DisplayMode.FULLSCREEN)
+ }
+
+ private fun mockManifest(display: WebAppManifest.DisplayMode) = WebAppManifest(
+ name = "Mock",
+ startUrl = "https://mozilla.org",
+ scope = "https://mozilla.org",
+ display = display,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeatureTest.kt
new file mode 100644
index 0000000000..cd3323ba14
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeatureTest.kt
@@ -0,0 +1,343 @@
+/* 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.pwa.feature
+
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsSessionToken
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+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.manifest.WebAppManifest
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.customtabs.store.OriginRelationPair
+import mozilla.components.feature.customtabs.store.ValidateRelationshipAction
+import mozilla.components.feature.customtabs.store.VerificationStatus
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class WebAppHideToolbarFeatureTest {
+
+ private val customTabId = "custom-id"
+ private var toolbarVisible = false
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ toolbarVisible = false
+ }
+
+ @Test
+ fun `hides toolbar immediately based on PWA manifest`() {
+ val tab = CustomTabSessionState(
+ id = customTabId,
+ content = ContentState("https://mozilla.org"),
+ config = CustomTabConfig(),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+
+ val feature = WebAppHideToolbarFeature(
+ store,
+ CustomTabsServiceStore(),
+ tabId = tab.id,
+ manifest = mockManifest("https://mozilla.org"),
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `hides toolbar immediately based on trusted origins`() {
+ val token = mock<CustomTabsSessionToken>()
+ val tab = CustomTabSessionState(
+ id = customTabId,
+ content = ContentState("https://mozilla.org"),
+ config = CustomTabConfig(sessionToken = token),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val customTabsStore = CustomTabsServiceStore(
+ CustomTabsServiceState(
+ tabs = mapOf(token to mockCustomTabState("https://firefox.com", "https://mozilla.org")),
+ ),
+ )
+
+ val feature = WebAppHideToolbarFeature(
+ store,
+ customTabsStore,
+ tabId = tab.id,
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `does not hide toolbar for a normal tab`() {
+ val tab = createTab("https://mozilla.org")
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+
+ val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertTrue(toolbarVisible)
+ }
+
+ @Test
+ fun `does not hide toolbar for an invalid tab`() {
+ val store = BrowserStore()
+
+ val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore()) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertTrue(toolbarVisible)
+ }
+
+ @Test
+ fun `does hide toolbar for a normal tab in fullscreen`() {
+ val tab = TabSessionState(
+ content = ContentState(
+ url = "https://mozilla.org",
+ fullScreen = true,
+ ),
+ )
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+
+ val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `does hide toolbar for a normal tab in PIP`() {
+ val tab = TabSessionState(
+ content = ContentState(
+ url = "https://mozilla.org",
+ pictureInPictureEnabled = true,
+ ),
+ )
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+
+ val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `does not hide toolbar if origin is not trusted`() {
+ val token = mock<CustomTabsSessionToken>()
+ val tab = createCustomTab(
+ id = customTabId,
+ url = "https://firefox.com",
+ config = CustomTabConfig(sessionToken = token),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val customTabsStore = CustomTabsServiceStore(
+ CustomTabsServiceState(
+ tabs = mapOf(token to mockCustomTabState("https://mozilla.org")),
+ ),
+ )
+
+ val feature = WebAppHideToolbarFeature(
+ store,
+ customTabsStore,
+ tabId = tab.id,
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertTrue(toolbarVisible)
+ }
+
+ @Test
+ fun `onUrlChanged hides toolbar if URL is in origin`() {
+ val token = mock<CustomTabsSessionToken>()
+ val tab = createCustomTab(
+ id = customTabId,
+ url = "https://mozilla.org",
+ config = CustomTabConfig(sessionToken = token),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val customTabsStore = CustomTabsServiceStore(
+ CustomTabsServiceState(
+ tabs = mapOf(token to mockCustomTabState("https://mozilla.com", "https://m.mozilla.com")),
+ ),
+ )
+ val feature = WebAppHideToolbarFeature(
+ store,
+ customTabsStore,
+ tabId = customTabId,
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.com/example-page"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://firefox.com/out-of-scope"),
+ ).joinBlocking()
+ assertTrue(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.com/back-in-scope"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://m.mozilla.com/second-origin"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `onUrlChanged hides toolbar if URL is in scope`() {
+ val tab = createCustomTab(id = customTabId, url = "https://mozilla.org")
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val feature = WebAppHideToolbarFeature(
+ store,
+ CustomTabsServiceStore(),
+ tabId = customTabId,
+ manifest = mockManifest("https://mozilla.github.io/my-app/"),
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app/"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://firefox.com/out-of-scope"),
+ ).joinBlocking()
+ assertTrue(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app-almost-in-scope"),
+ ).joinBlocking()
+ assertTrue(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app/sub-page"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `onUrlChanged hides toolbar if URL is in ambiguous scope`() {
+ val tab = createCustomTab(id = customTabId, url = "https://mozilla.org")
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val feature = WebAppHideToolbarFeature(
+ store,
+ CustomTabsServiceStore(),
+ tabId = customTabId,
+ manifest = mockManifest("https://mozilla.github.io/prefix"),
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/prefix/"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/prefix-of/resource.html"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `onTrustedScopesChange hides toolbar if URL is in origin`() {
+ val token = mock<CustomTabsSessionToken>()
+ val tab = createCustomTab(
+ id = customTabId,
+ url = "https://mozilla.com/example-page",
+ config = CustomTabConfig(sessionToken = token),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val customTabsStore = CustomTabsServiceStore(
+ CustomTabsServiceState(
+ tabs = mapOf(token to mockCustomTabState()),
+ ),
+ )
+ val feature = WebAppHideToolbarFeature(
+ store,
+ customTabsStore,
+ tabId = customTabId,
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+
+ customTabsStore.dispatch(
+ ValidateRelationshipAction(
+ token,
+ RELATION_HANDLE_ALL_URLS,
+ "https://m.mozilla.com".toUri(),
+ VerificationStatus.PENDING,
+ ),
+ ).joinBlocking()
+ assertTrue(toolbarVisible)
+
+ customTabsStore.dispatch(
+ ValidateRelationshipAction(
+ token,
+ RELATION_HANDLE_ALL_URLS,
+ "https://mozilla.com".toUri(),
+ VerificationStatus.PENDING,
+ ),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+ }
+
+ private fun mockCustomTabState(vararg origins: String) = CustomTabState(
+ relationships = origins.map { origin ->
+ OriginRelationPair(origin.toUri(), RELATION_HANDLE_ALL_URLS) to VerificationStatus.PENDING
+ }.toMap(),
+ )
+
+ private fun mockManifest(scope: String) = WebAppManifest(
+ name = "Mock",
+ startUrl = scope,
+ scope = scope,
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeatureTest.kt
new file mode 100644
index 0000000000..6ad7780d0b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeatureTest.kt
@@ -0,0 +1,147 @@
+/* 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.pwa.feature
+
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class WebAppSiteControlsFeatureTest {
+
+ @Test
+ fun `register receiver on resume`() {
+ val controlsBuilder: SiteControlsBuilder = mock()
+ val filter: IntentFilter = mock()
+ whenever(controlsBuilder.getFilter()).thenReturn(filter)
+
+ val feature = spy(
+ WebAppSiteControlsFeature(
+ testContext,
+ mock(),
+ "session-id",
+ controlsBuilder = controlsBuilder,
+ notificationsDelegate = mock(),
+ ),
+ )
+
+ feature.onResume(mock())
+
+ verify(feature).registerReceiver(filter)
+ }
+
+ @Test
+ fun `unregister receiver on pause`() {
+ val context = spy(testContext)
+
+ doNothing().`when`(context).unregisterReceiver(any())
+
+ val feature = WebAppSiteControlsFeature(context, mock(), "session-id", mock(), notificationsDelegate = mock())
+ feature.onPause(mock())
+
+ verify(context).unregisterReceiver(feature)
+ }
+
+ @Test
+ fun `reload page when reload action is activated`() {
+ val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase = mock()
+
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(
+ createCustomTab("https://www.mozilla.org", id = "session-id"),
+ ),
+ ),
+ )
+
+ val feature = WebAppSiteControlsFeature(
+ testContext,
+ store,
+ reloadUrlUseCase,
+ "session-id",
+ mock(),
+ notificationsDelegate = mock(),
+ )
+ feature.onReceive(testContext, Intent("mozilla.components.feature.pwa.REFRESH"))
+
+ verify(reloadUrlUseCase).invoke("session-id")
+ }
+
+ @Test
+ fun `load monochrome icon if defined in manifest`() {
+ val icons: BrowserIcons = mock()
+ val manifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ scope = "https://mozilla.org",
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://mozilla.org/logo_color.svg",
+ sizes = listOf(Size.ANY),
+ type = "image/svg+xml",
+ purpose = setOf(WebAppManifest.Icon.Purpose.ANY, WebAppManifest.Icon.Purpose.MASKABLE),
+ ),
+ WebAppManifest.Icon(
+ src = "https://mozilla.org/logo_black.svg",
+ sizes = listOf(Size.ANY),
+ type = "image/svg+xml",
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ )
+
+ val session = createCustomTab("https://www.mozilla.org", id = "session-id")
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(session),
+ ),
+ )
+
+ val feature = WebAppSiteControlsFeature(
+ testContext,
+ store,
+ "session-id",
+ manifest,
+ icons = icons,
+ notificationsDelegate = mock(),
+ )
+ feature.onCreate(mock())
+
+ verify(icons).loadIcon(
+ IconRequest(
+ url = "https://mozilla.org",
+ size = IconRequest.Size.DEFAULT,
+ resources = listOf(
+ IconRequest.Resource(
+ url = "https://mozilla.org/logo_black.svg",
+ type = IconRequest.Resource.Type.MANIFEST_ICON,
+ sizes = listOf(Size.ANY),
+ mimeType = "image/svg+xml",
+ maskable = false,
+ ),
+ ),
+ color = Color.WHITE,
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt
new file mode 100644
index 0000000000..679c5c753e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt
@@ -0,0 +1,97 @@
+/* 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.pwa.intent
+
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.os.Bundle
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION
+import androidx.browser.customtabs.TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY
+import androidx.core.net.toUri
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@Suppress("DEPRECATION")
+@Ignore("TrustedWebActivityIntentProcessorTest] is deprecated. See https://github.com/mozilla-mobile/android-components/issues/12024")
+class TrustedWebActivityIntentProcessorTest {
+
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setup() {
+ store = BrowserStore()
+ }
+
+ @Test
+ fun `process checks if intent action is not valid`() {
+ val processor = TrustedWebActivityIntentProcessor(mock(), mock(), mock(), mock())
+
+ assertFalse(processor.process(Intent(ACTION_VIEW_PWA)))
+ assertFalse(processor.process(Intent(ACTION_VIEW)))
+ assertFalse(
+ processor.process(
+ Intent(ACTION_VIEW).apply { putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true) },
+ ),
+ )
+ assertFalse(
+ processor.process(
+ Intent(ACTION_VIEW).apply {
+ putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false)
+ putExtra(EXTRA_SESSION, null as Bundle?)
+ },
+ ),
+ )
+ assertFalse(
+ processor.process(
+ Intent(ACTION_VIEW).apply {
+ putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
+ putExtra(EXTRA_SESSION, null as Bundle?)
+ },
+ ),
+ )
+ assertFalse(
+ processor.process(
+ Intent(ACTION_VIEW, null).apply {
+ putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
+ putExtra(EXTRA_SESSION, null as Bundle?)
+ },
+ ),
+ )
+ }
+
+ @Test
+ fun `process adds custom tab config`() {
+ val intent = Intent(ACTION_VIEW, "https://example.com".toUri()).apply {
+ putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
+ putExtra(EXTRA_SESSION, null as Bundle?)
+ }
+
+ val customTabsStore: CustomTabsServiceStore = mock()
+ val addTabUseCase: CustomTabsUseCases.AddCustomTabUseCase = mock()
+
+ val processor = TrustedWebActivityIntentProcessor(addTabUseCase, mock(), mock(), customTabsStore)
+ assertTrue(processor.process(intent))
+
+ verify(addTabUseCase).invoke(
+ "https://example.com",
+ source = SessionState.Source.Internal.HomeScreen,
+ customTabConfig = CustomTabConfig(externalAppType = ExternalAppType.TRUSTED_WEB_ACTIVITY),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt
new file mode 100644
index 0000000000..bd7da70474
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt
@@ -0,0 +1,154 @@
+/* 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.pwa.intent
+
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.intent.ext.getSessionId
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.ext.getWebAppManifest
+import mozilla.components.feature.pwa.ext.putUrlOverride
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+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.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class WebAppIntentProcessorTest {
+ @Test
+ fun `process checks if intent action is not valid`() {
+ val store = BrowserStore()
+
+ val processor = WebAppIntentProcessor(store, mock(), mock(), mock())
+
+ assertFalse(processor.process(Intent(ACTION_VIEW)))
+ assertFalse(processor.process(Intent(ACTION_VIEW_PWA, null)))
+ assertFalse(processor.process(Intent(ACTION_VIEW_PWA, "".toUri())))
+ }
+
+ @Test
+ fun `process returns false if no manifest is in storage`() = runTest {
+ val storage: ManifestStorage = mock()
+ val processor = WebAppIntentProcessor(mock(), mock(), mock(), storage)
+
+ `when`(storage.loadManifest("https://mozilla.com")).thenReturn(null)
+
+ assertFalse(processor.process(Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())))
+ }
+
+ @Test
+ fun `process adds session ID and manifest to intent`() = runTest {
+ val store = BrowserStore()
+ val storage: ManifestStorage = mock()
+
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "https://mozilla.com",
+ )
+ `when`(storage.loadManifest("https://mozilla.com")).thenReturn(manifest)
+
+ val addTabUseCase: CustomTabsUseCases.AddWebAppTabUseCase = mock()
+ whenever(
+ addTabUseCase.invoke(
+ url = "https://mozilla.com",
+ source = SessionState.Source.Internal.HomeScreen,
+ customTabConfig = CustomTabConfig(
+ externalAppType = ExternalAppType.PROGRESSIVE_WEB_APP,
+ enableUrlbarHiding = true,
+ showCloseButton = false,
+ showShareMenuItem = true,
+
+ ),
+ webAppManifest = manifest,
+ ),
+ ).thenReturn("42")
+
+ val processor = WebAppIntentProcessor(store, addTabUseCase, mock(), storage)
+
+ val intent = Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())
+ assertTrue(processor.process(intent))
+
+ assertNotNull(intent.getSessionId())
+ assertEquals("42", intent.getSessionId())
+ assertEquals(manifest, intent.getWebAppManifest())
+ }
+
+ @Test
+ fun `process adds custom tab config`() = runTest {
+ val intent = Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())
+
+ val storage: ManifestStorage = mock()
+ val store = BrowserStore()
+
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "https://mozilla.com",
+ )
+ `when`(storage.loadManifest("https://mozilla.com")).thenReturn(manifest)
+
+ val addTabUseCase: CustomTabsUseCases.AddWebAppTabUseCase = mock()
+
+ val processor = WebAppIntentProcessor(store, addTabUseCase, mock(), storage)
+ assertTrue(processor.process(intent))
+
+ verify(addTabUseCase).invoke(
+ url = "https://mozilla.com",
+ source = SessionState.Source.Internal.HomeScreen,
+ customTabConfig = CustomTabConfig(
+ externalAppType = ExternalAppType.PROGRESSIVE_WEB_APP,
+ enableUrlbarHiding = true,
+ showCloseButton = false,
+ showShareMenuItem = true,
+
+ ),
+ webAppManifest = manifest,
+ )
+ }
+
+ @Test
+ fun `url override is applied to session if present`() = runTest {
+ val store = BrowserStore()
+
+ val storage: ManifestStorage = mock()
+ val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase = mock()
+ val processor = WebAppIntentProcessor(store, mock(), loadUrlUseCase, storage)
+ val urlOverride = "https://mozilla.com/deep/link/index.html"
+
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "https://mozilla.com",
+ )
+
+ `when`(storage.loadManifest("https://mozilla.com")).thenReturn(manifest)
+
+ val intent = Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())
+
+ intent.putUrlOverride(urlOverride)
+
+ assertTrue(processor.process(intent))
+ verify(loadUrlUseCase).invoke(eq(urlOverride), any(), any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/pwa/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/pwa/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/pwa/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/pwa/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/qr/README.md b/mobile/android/android-components/components/feature/qr/README.md
new file mode 100644
index 0000000000..531cd6f8e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/README.md
@@ -0,0 +1,42 @@
+# [Android Components](../../../README.md) > Libraries > QR
+
+A component that provides functionality for scanning QR coes.
+
+## Usage
+
+### 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-qr:{latest-version}"
+```
+
+### Integration
+
+Initializing the feature:
+
+```kotlin
+qrFeature = QrFeature(
+ context,
+ fragmentManager = supportFragmentManager,
+ onNeedToRequestPermissions = { permissions ->
+ requestPermissions(this, permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
+ },
+ onScanResult = { result ->
+ // result is a String (e.g. a URL) returned by the QR scanner.
+ }
+)
+```
+
+When ready to scan use the following:
+
+```kotlin
+qrFeature.scan()
+```
+
+## 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/qr/build.gradle b/mobile/android/android-components/components/feature/qr/build.gradle
new file mode 100644
index 0000000000..f491a3e0a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/build.gradle
@@ -0,0 +1,46 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.qr'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_appcompat
+
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.thirdparty_zxing
+ testImplementation ComponentsDependencies.thirdparty_zxing
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/qr/proguard-rules.pro b/mobile/android/android-components/components/feature/qr/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/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/qr/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/qr/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..592249325c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.CAMERA" />
+</manifest>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFeature.kt b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFeature.kt
new file mode 100644
index 0000000000..87a55dbcc4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFeature.kt
@@ -0,0 +1,145 @@
+/* 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.qr
+
+import android.Manifest.permission.CAMERA
+import android.content.Context
+import androidx.annotation.MainThread
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.fragment.app.FragmentManager
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.OnNeedToRequestPermissions
+import mozilla.components.support.base.feature.PermissionsFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+
+typealias OnScanResult = (result: String) -> Unit
+
+/**
+ * Feature implementation that provides QR scanning functionality via the [QrFragment].
+ *
+ * @property context a reference to the context.
+ * @property fragmentManager a reference to a [FragmentManager], used to start
+ * the [QrFragment].
+ * @property onScanResult a callback invoked with the result of the QR scan.
+ * The callback will always be invoked on the main thread.
+ * @property onNeedToRequestPermissions a callback invoked when permissions
+ * need to be requested before a QR scan can be performed. Once the request
+ * is completed, [onPermissionsResult] needs to be invoked. This feature
+ * will request [android.Manifest.permission.CAMERA].
+ * @property scanMessage (Optional) String resource for an optional message
+ * to be laid out below the QR scan viewfinder
+ */
+class QrFeature(
+ private val context: Context,
+ private val fragmentManager: FragmentManager,
+ private val onScanResult: OnScanResult = { },
+ override val onNeedToRequestPermissions: OnNeedToRequestPermissions = { },
+ @StringRes
+ private var scanMessage: Int? = null,
+) : LifecycleAwareFeature, UserInteractionHandler, PermissionsFeature {
+ private var containerViewId: Int = 0
+
+ private val qrFragment
+ get() = fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG) as? QrFragment
+
+ @Suppress("MemberVisibilityCanBePrivate")
+ val isScanInProgress
+ get() = qrFragment != null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val scanCompleteListener: QrFragment.OnScanCompleteListener = object : QrFragment.OnScanCompleteListener {
+ @MainThread
+ override fun onScanComplete(result: String) {
+ setScanCompleteListener(null)
+ removeQrFragment()
+ onScanResult(result)
+ }
+ }
+
+ override fun start() {
+ setScanCompleteListener(scanCompleteListener)
+ }
+
+ override fun stop() {
+ // Prevent an already in progress qr decode operation informing us later of a result
+ // and so triggering an IllegalStateException when trying to remove the qr fragment.
+ setScanCompleteListener(null)
+ }
+
+ override fun onBackPressed(): Boolean {
+ return removeQrFragment()
+ }
+
+ /**
+ * Starts the QR scanner fragment and listens for scan results.
+ *
+ * @param containerViewId optional id of the container this fragment is to
+ * be placed in, defaults to [android.R.id.content].
+ *
+ * @return true if the scanner was started or false if permissions still
+ * need to be requested.
+ */
+ fun scan(containerViewId: Int = android.R.id.content): Boolean {
+ this.containerViewId = containerViewId
+
+ return if (context.isPermissionGranted(CAMERA)) {
+ when (isScanInProgress) {
+ true -> qrFragment?.startScanning()
+ false -> fragmentManager.beginTransaction()
+ .add(containerViewId, QrFragment.newInstance(scanCompleteListener, scanMessage), QR_FRAGMENT_TAG)
+ .commit()
+ }
+ true
+ } else {
+ onNeedToRequestPermissions(arrayOf(CAMERA))
+ false
+ }
+ }
+
+ /**
+ * Notifies the feature that the permission request was completed. If the
+ * requested permissions were granted it will open the QR scanner.
+ */
+ override fun onPermissionsResult(permissions: Array<String>, grantResults: IntArray) {
+ if (context.isPermissionGranted(CAMERA)) {
+ scan(containerViewId)
+ } else {
+ // It is possible that we started scanning then the user is will update
+ // the camera permission in Android settings.
+ // The client app is expected to ask again for the camera permission when the app is resumed
+ // and this request can be denied by the user so we should interrupt the in-progress scanning.
+ removeQrFragment()
+ }
+ }
+
+ /**
+ * Removes the QR fragment.
+ *
+ * @return true if the fragment was removed, otherwise false.
+ */
+ internal fun removeQrFragment(): Boolean {
+ qrFragment?.let {
+ fragmentManager.beginTransaction().remove(it).commit()
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Set a callback for when a qr code has been successfully scanned and decoded.
+ */
+ @VisibleForTesting
+ internal fun setScanCompleteListener(listener: QrFragment.OnScanCompleteListener?) {
+ (fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG) as? QrFragment)?.let {
+ it.scanCompleteListener = listener
+ }
+ }
+
+ companion object {
+ internal const val QR_FRAGMENT_TAG = "MOZAC_QR_FRAGMENT"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFragment.kt b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFragment.kt
new file mode 100644
index 0000000000..e92e37cb7b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFragment.kt
@@ -0,0 +1,786 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. */
+
+package mozilla.components.feature.qr
+
+import android.Manifest.permission
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.params.OutputConfiguration
+import android.hardware.camera2.params.SessionConfiguration
+import android.media.Image
+import android.media.ImageReader
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.util.Size
+import android.view.LayoutInflater
+import android.view.Surface
+import android.view.TextureView
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat.getColor
+import androidx.core.view.WindowInsetsCompat
+import androidx.fragment.app.Fragment
+import com.google.zxing.BinaryBitmap
+import com.google.zxing.LuminanceSource
+import com.google.zxing.MultiFormatReader
+import com.google.zxing.PlanarYUVLuminanceSource
+import com.google.zxing.common.HybridBinarizer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.feature.qr.views.AutoFitTextureView
+import mozilla.components.feature.qr.views.CustomViewFinder
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.content.hasCamera
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+import java.io.Serializable
+import java.util.ArrayList
+import java.util.Collections
+import java.util.Comparator
+import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.RejectedExecutionException
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * A [Fragment] that displays a QR scanner.
+ *
+ * This class is based on Camera2BasicFragment from:
+ *
+ * https://github.com/googlesamples/android-Camera2Basic
+ * https://github.com/kismkof/camera2basic
+ */
+@Suppress("LargeClass", "TooManyFunctions")
+class QrFragment : Fragment() {
+ private val logger = Logger("mozac-qr")
+
+ @VisibleForTesting
+ internal var multiFormatReader = MultiFormatReader()
+ private val coroutineScope = CoroutineScope(Dispatchers.Default)
+
+ /**
+ * [TextureView.SurfaceTextureListener] handles several lifecycle events on a [TextureView].
+ */
+ private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+
+ override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
+ tryOpenCamera(width, height)
+ }
+
+ override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {
+ configureTransform(width, height)
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onSurfaceTextureUpdated(texture: SurfaceTexture) { }
+
+ override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
+ return true
+ }
+ }
+
+ internal lateinit var textureView: AutoFitTextureView
+ internal lateinit var customViewFinder: CustomViewFinder
+ internal lateinit var cameraErrorView: TextView
+
+ @StringRes
+ internal var scanMessage: Int? = null
+ internal var cameraId: String? = null
+ private var captureSession: CameraCaptureSession? = null
+ internal var cameraDevice: CameraDevice? = null
+ internal var previewSize: Size? = null
+
+ /**
+ * Listener invoked when the QR scan completed successfully.
+ */
+ interface OnScanCompleteListener : Serializable {
+ /**
+ * Invoked to provide access to the result of the QR scan.
+ */
+ fun onScanComplete(result: String)
+ }
+
+ @Volatile internal var scanCompleteListener: OnScanCompleteListener? = null
+ set(value) {
+ field = object : OnScanCompleteListener {
+ override fun onScanComplete(result: String) {
+ Handler(Looper.getMainLooper()).apply {
+ post {
+ context?.let {
+ customViewFinder.setViewFinderColor(
+ getColor(it, R.color.mozac_feature_qr_scan_success_color),
+ )
+ }
+ value?.onScanComplete(result)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * [CameraDevice.StateCallback] is called when [CameraDevice] changes its state.
+ */
+ internal val stateCallback = object : CameraDevice.StateCallback() {
+
+ override fun onOpened(cameraDevice: CameraDevice) {
+ cameraOpenCloseLock.release()
+ this@QrFragment.cameraDevice = cameraDevice
+ createCameraPreviewSession()
+ }
+
+ override fun onDisconnected(cameraDevice: CameraDevice) {
+ cameraOpenCloseLock.release()
+ cameraDevice.close()
+ this@QrFragment.cameraDevice = null
+ }
+
+ override fun onError(cameraDevice: CameraDevice, error: Int) {
+ cameraOpenCloseLock.release()
+ cameraDevice.close()
+ this@QrFragment.cameraDevice = null
+ }
+ }
+
+ /**
+ * An additional thread for running tasks that shouldn't block the UI.
+ * A [Handler] for running tasks in the background.
+ */
+ @VisibleForTesting
+ internal var backgroundThread: HandlerThread? = null
+
+ @VisibleForTesting
+ internal var backgroundHandler: Handler? = null
+
+ @VisibleForTesting
+ internal var backgroundExecutor: ExecutorService? = null
+ private var previewRequestBuilder: CaptureRequest.Builder? = null
+ private var previewRequest: CaptureRequest? = null
+
+ /**
+ * A [Semaphore] to prevent the app from exiting before closing the camera.
+ */
+ private val cameraOpenCloseLock = Semaphore(1)
+
+ /**
+ * Orientation of the camera sensor
+ */
+ private var sensorOrientation: Int = 0
+
+ /**
+ * An [ImageReader] that handles still image capture.
+ * This is the output file for our picture.
+ */
+ private var imageReader: ImageReader? = null
+ private val imageAvailableListener = object : ImageReader.OnImageAvailableListener {
+
+ private var image: Image? = null
+
+ override fun onImageAvailable(reader: ImageReader) {
+ try {
+ image = reader.acquireNextImage()
+ val availableImage = image
+ if (availableImage != null && scanCompleteListener != null) {
+ val source = readImageSource(availableImage)
+ if (qrState == STATE_FIND_QRCODE) {
+ qrState = STATE_DECODE_PROGRESS
+
+ coroutineScope.launch {
+ tryScanningSource(source)
+ }
+ }
+ }
+ } finally {
+ image?.close()
+ }
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_layout, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ textureView = view.findViewById<View>(R.id.texture) as AutoFitTextureView
+ customViewFinder = view.findViewById<View>(R.id.view_finder) as CustomViewFinder
+ cameraErrorView = view.findViewById<View>(R.id.camera_error) as TextView
+
+ CustomViewFinder.setMessage(scanMessage)
+ qrState = STATE_FIND_QRCODE
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // It's possible that the Fragment is resumed to a scanning state
+ // while in the meantime the camera permission was removed. Avoid any issues.
+ if (requireContext().isPermissionGranted(permission.CAMERA)) {
+ startScanning()
+ }
+ }
+
+ override fun onPause() {
+ closeCamera()
+ stopBackgroundThread()
+ stopExecutorService()
+ super.onPause()
+ }
+
+ override fun onStop() {
+ // Ensure we'll continue tracking qr codes when the user returns to the application
+ qrState = STATE_FIND_QRCODE
+
+ super.onStop()
+ }
+
+ internal fun maybeStartBackgroundThread() {
+ if (backgroundThread == null) {
+ backgroundThread = HandlerThread("CameraBackground")
+ }
+
+ backgroundThread?.let {
+ if (!it.isAlive) {
+ it.start()
+ backgroundHandler = Handler(it.looper)
+ }
+ }
+ }
+
+ internal fun stopBackgroundThread() {
+ backgroundThread?.quitSafely()
+ try {
+ backgroundThread?.join()
+ backgroundThread = null
+ backgroundHandler = null
+ } catch (e: InterruptedException) {
+ logger.debug("Interrupted while stopping background thread", e)
+ }
+ }
+
+ internal fun maybeStartExecutorService() {
+ if (backgroundExecutor == null) {
+ backgroundExecutor = Executors.newSingleThreadExecutor()
+ }
+ }
+
+ internal fun stopExecutorService() {
+ backgroundExecutor?.shutdownNow()
+ backgroundExecutor = null
+ }
+
+ /**
+ * Open the camera and start the qr scanning functionality.
+ * Assumes the camera permission is granted for the app.
+ * If any issues occur this will fail gracefully and show an error message.
+ */
+ fun startScanning() {
+ maybeStartBackgroundThread()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ maybeStartExecutorService()
+ }
+ // When the screen is turned off and turned back on, the SurfaceTexture is already
+ // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
+ // a camera and start preview from here (otherwise, we wait until the surface is ready in
+ // the SurfaceTextureListener).
+ if (textureView.isAvailable) {
+ tryOpenCamera(textureView.width, textureView.height)
+ } else {
+ textureView.surfaceTextureListener = surfaceTextureListener
+ }
+ }
+
+ /**
+ * Sets up member variables related to camera.
+ *
+ * @param width The width of available size for camera preview
+ * @param height The height of available size for camera preview
+ */
+ @Suppress("ComplexMethod")
+ internal fun setUpCameraOutputs(width: Int, height: Int) {
+ val displayRotation = getScreenRotation()
+
+ val manager = activity?.getSystemService(Context.CAMERA_SERVICE) as CameraManager? ?: return
+
+ for (cameraId in manager.cameraIdList) {
+ val characteristics = manager.getCameraCharacteristics(cameraId)
+
+ val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
+ if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
+ continue
+ }
+
+ val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+ ?: continue
+ val largest = Collections.max(map.getOutputSizes(ImageFormat.YUV_420_888).asList(), CompareSizesByArea())
+ imageReader = ImageReader.newInstance(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT, ImageFormat.YUV_420_888, 2)
+ .apply { setOnImageAvailableListener(imageAvailableListener, backgroundHandler) }
+
+ // Find out if we need to swap dimension to get the preview size relative to sensor coordinate.
+
+ sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) as Int
+
+ @Suppress("MagicNumber")
+ val swappedDimensions = when (displayRotation) {
+ Surface.ROTATION_0, Surface.ROTATION_180 -> sensorOrientation == 90 || sensorOrientation == 270
+ Surface.ROTATION_90, Surface.ROTATION_270 -> sensorOrientation == 0 || sensorOrientation == 180
+ else -> false
+ }
+
+ val displaySize = activity?.windowManager?.getDisplaySize() ?: Point()
+
+ var rotatedPreviewWidth = width
+ var rotatedPreviewHeight = height
+ var maxPreviewWidth = displaySize.x
+ var maxPreviewHeight = displaySize.y
+
+ if (swappedDimensions) {
+ rotatedPreviewWidth = height
+ rotatedPreviewHeight = width
+ maxPreviewWidth = displaySize.y
+ maxPreviewHeight = displaySize.x
+ }
+
+ maxPreviewWidth = min(maxPreviewWidth, MAX_PREVIEW_WIDTH)
+ maxPreviewHeight = min(maxPreviewHeight, MAX_PREVIEW_HEIGHT)
+
+ val optimalSize = chooseOptimalSize(
+ map.getOutputSizes(SurfaceTexture::class.java),
+ rotatedPreviewWidth,
+ rotatedPreviewHeight,
+ maxPreviewWidth,
+ maxPreviewHeight,
+ largest,
+ )
+
+ adjustPreviewSize(optimalSize)
+ this.cameraId = cameraId
+ return
+ }
+ }
+
+ internal fun adjustPreviewSize(optimalSize: Size) {
+ // We're seeing slow unreliable scans with distorted screens on some devices
+ // so we're making the preview and scan area a square of the optimal size
+ // to prevent that.
+ val length = min(optimalSize.height, optimalSize.width)
+ textureView.setAspectRatio(length, length)
+ this.previewSize = Size(length, length)
+ }
+
+ /**
+ * Tries to open the camera and displays an error message in case
+ * there's no camera available or we fail to open it. Applications
+ * should ideally check for camera availability, but we use this
+ * as a fallback in case they don't.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ internal fun tryOpenCamera(width: Int, height: Int, skipCheck: Boolean = false) {
+ try {
+ if (context?.hasCamera() == true || skipCheck) {
+ openCamera(width, height)
+ hideNoCameraAvailableError()
+ } else {
+ showNoCameraAvailableError()
+ }
+ } catch (e: Exception) {
+ showNoCameraAvailableError()
+ }
+ }
+
+ private fun showNoCameraAvailableError() {
+ cameraErrorView.visibility = View.VISIBLE
+ customViewFinder.visibility = View.GONE
+ }
+
+ private fun hideNoCameraAvailableError() {
+ cameraErrorView.visibility = View.GONE
+ customViewFinder.visibility = View.VISIBLE
+ }
+
+ /**
+ * Opens the camera specified by [QrFragment.cameraId].
+ */
+ @SuppressLint("MissingPermission")
+ @Suppress("ThrowsCount")
+ internal fun openCamera(width: Int, height: Int) {
+ try {
+ setUpCameraOutputs(width, height)
+ if (cameraId == null) {
+ throw IllegalStateException("No camera found on device")
+ }
+
+ configureTransform(width, height)
+
+ val activity = activity
+ val manager = activity?.getSystemService(Context.CAMERA_SERVICE) as CameraManager?
+
+ if (!cameraOpenCloseLock.tryAcquire(CAMERA_CLOSE_LOCK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ throw IllegalStateException("Time out waiting to lock camera opening.")
+ }
+ manager?.openCamera(cameraId as String, stateCallback, backgroundHandler)
+ } catch (e: InterruptedException) {
+ throw IllegalStateException("Interrupted while trying to lock camera opening.", e)
+ } catch (e: CameraAccessException) {
+ logger.error("Failed to open camera", e)
+ }
+ }
+
+ /**
+ * Closes the current [CameraDevice].
+ */
+ internal fun closeCamera() {
+ try {
+ cameraOpenCloseLock.acquire()
+ cameraDevice?.close()
+ cameraDevice = null
+ imageReader?.close()
+ imageReader = null
+
+ // captureSession should be closed as a last step in case background executor terminated
+ captureSession?.close()
+ captureSession = null
+ } catch (e: InterruptedException) {
+ throw IllegalStateException("Interrupted while trying to lock camera closing.", e)
+ } catch (e: RejectedExecutionException) { // This exception was found in automated testing
+ logger.error("backgroundExecutor terminated", e)
+ } finally {
+ cameraOpenCloseLock.release()
+ }
+ }
+
+ /**
+ * Configures the necessary [android.graphics.Matrix] transformation to `textureView`.
+ * This method should be called after the camera preview size is determined in
+ * [setUpCameraOutputs] and also the size of `textureView` is fixed.
+ *
+ * @param viewWidth The width of `textureView`
+ * @param viewHeight The height of `textureView`
+ */
+ @VisibleForTesting
+ internal fun configureTransform(viewWidth: Int, viewHeight: Int) {
+ val size = previewSize ?: return
+
+ val rotation = getScreenRotation()
+ val matrix = Matrix()
+ val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
+ val bufferRect = RectF(0f, 0f, size.height.toFloat(), size.width.toFloat())
+ val centerX = viewRect.centerX()
+ val centerY = viewRect.centerY()
+
+ @Suppress("MagicNumber")
+ if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
+ bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
+ matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
+ val scale = max(viewHeight.toFloat() / size.height, viewWidth.toFloat() / size.width)
+ matrix.postScale(scale, scale, centerX, centerY)
+ matrix.postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY)
+ } else if (Surface.ROTATION_180 == rotation) {
+ matrix.postRotate(180f, centerX, centerY)
+ }
+ textureView.setTransform(matrix)
+ }
+
+ /**
+ * Creates a new [CameraCaptureSession] for camera preview.
+ */
+ @Suppress("ComplexMethod")
+ internal fun createCameraPreviewSession() {
+ val texture = textureView.surfaceTexture
+
+ val size = previewSize as Size
+ // We configure the size of default buffer to be the size of camera preview we want.
+ texture?.setDefaultBufferSize(size.width, size.height)
+
+ val surface = Surface(texture)
+ val mImageSurface = imageReader?.surface
+
+ handleCaptureException("Failed to create camera preview session") {
+ cameraDevice?.let {
+ previewRequestBuilder = it.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
+ addTarget(mImageSurface as Surface)
+ addTarget(surface)
+ }
+
+ val captureCallback = object : CameraCaptureSession.CaptureCallback() {}
+ val stateCallback = object : CameraCaptureSession.StateCallback() {
+ override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {
+ if (null == cameraDevice) return
+
+ previewRequestBuilder?.set(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+ )
+
+ previewRequest = previewRequestBuilder?.build()
+ captureSession = cameraCaptureSession
+
+ handleCaptureException("Failed to request capture") {
+ cameraCaptureSession.setRepeatingRequest(
+ previewRequest as CaptureRequest,
+ captureCallback,
+ backgroundHandler,
+ )
+ }
+ }
+
+ override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) {
+ logger.error("Failed to configure CameraCaptureSession")
+ }
+ }
+ createCaptureSessionCompat(it, mImageSurface as Surface, surface, stateCallback)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createCaptureSessionCompat(
+ camera: CameraDevice,
+ imageSurface: Surface,
+ surface: Surface,
+ stateCallback: CameraCaptureSession.StateCallback,
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ if (shouldStartExecutorService()) {
+ maybeStartExecutorService()
+ }
+ val sessionConfig = SessionConfiguration(
+ SessionConfiguration.SESSION_REGULAR,
+ listOf(OutputConfiguration(imageSurface), OutputConfiguration(surface)),
+ backgroundExecutor as Executor,
+ stateCallback,
+ )
+ camera.createCaptureSession(sessionConfig)
+ } else {
+ @Suppress("DEPRECATION")
+ camera.createCaptureSession(listOf(imageSurface, surface), stateCallback, null)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun shouldStartExecutorService(): Boolean = backgroundExecutor == null
+
+ @Suppress("TooGenericExceptionCaught")
+ private fun handleCaptureException(msg: String, block: () -> Unit) {
+ try {
+ block()
+ } catch (e: Exception) {
+ when (e) {
+ is CameraAccessException, is IllegalStateException -> {
+ logger.error(msg, e)
+ }
+ else -> throw e
+ }
+ }
+ }
+
+ /**
+ * Compares two `Size`s based on their areas.
+ */
+ internal class CompareSizesByArea : Comparator<Size> {
+ override fun compare(lhs: Size, rhs: Size): Int {
+ return java.lang.Long.signum(lhs.width.toLong() * lhs.height - rhs.width.toLong() * rhs.height)
+ }
+ }
+
+ companion object {
+ internal const val STATE_FIND_QRCODE = 0
+ internal const val STATE_DECODE_PROGRESS = 1
+ internal const val STATE_QRCODE_EXIST = 2
+
+ internal const val MAX_PREVIEW_WIDTH = 786
+ internal const val MAX_PREVIEW_HEIGHT = 786
+
+ private const val CAMERA_CLOSE_LOCK_TIMEOUT_MS = 2500L
+
+ /**
+ * Returns a new instance of QR Fragment
+ * @param listener Listener invoked when the QR scan completed successfully.
+ * @param scanMessage (Optional) Scan message to be displayed.
+ */
+ fun newInstance(listener: OnScanCompleteListener, scanMessage: Int? = null): QrFragment {
+ return QrFragment().apply {
+ scanCompleteListener = listener
+ this.scanMessage = scanMessage
+ }
+ }
+
+ /**
+ * Given `choices` of `Size`s supported by a camera, choose the smallest one that
+ * is at least as large as the respective texture view size, and that is at most as large as the
+ * respective max size, and whose aspect ratio matches with the specified value. If such size
+ * doesn't exist, choose the largest one that is at most as large as the respective max size,
+ * and whose aspect ratio matches with the specified value.
+ *
+ * @param choices The list of sizes that the camera supports for the intended output class
+ * @param textureViewWidth The width of the texture view relative to sensor coordinate
+ * @param textureViewHeight The height of the texture view relative to sensor coordinate
+ * @param maxWidth The maximum width that can be chosen
+ * @param maxHeight The maximum height that can be chosen
+ * @param aspectRatio The aspect ratio
+ * @return The optimal `Size`, or an arbitrary one if none were big enough.
+ */
+ @Suppress("ComplexMethod")
+ internal fun chooseOptimalSize(
+ choices: Array<Size>,
+ textureViewWidth: Int,
+ textureViewHeight: Int,
+ maxWidth: Int,
+ maxHeight: Int,
+ aspectRatio: Size,
+ ): Size {
+ // Collect the supported resolutions that are at least as big as the preview Surface
+ val bigEnough = ArrayList<Size>()
+ // Collect the supported resolutions that are smaller than the preview Surface
+ val notBigEnough = ArrayList<Size>()
+ val w = aspectRatio.width
+ val h = aspectRatio.height
+ for (option in choices) {
+ if (option.width <= maxWidth && option.height <= maxHeight &&
+ option.height == option.width * h / w
+ ) {
+ if (option.width >= textureViewWidth && option.height >= textureViewHeight) {
+ bigEnough.add(option)
+ } else {
+ notBigEnough.add(option)
+ }
+ }
+ }
+
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
+ return when {
+ bigEnough.size > 0 -> Collections.min(bigEnough, CompareSizesByArea())
+ notBigEnough.size > 0 -> Collections.max(notBigEnough, CompareSizesByArea())
+ else -> choices[0]
+ }
+ }
+
+ internal fun readImageSource(image: Image): PlanarYUVLuminanceSource {
+ val plane = image.planes[0]
+ val buffer = plane.buffer
+ val data = ByteArray(buffer.remaining()).also { buffer.get(it) }
+
+ val height = image.height
+ val width = image.width
+ val dataWidth = width + ((plane.rowStride - plane.pixelStride * width) / plane.pixelStride)
+ return PlanarYUVLuminanceSource(data, dataWidth, height, 0, 0, width, height, false)
+ }
+
+ @Volatile internal var qrState: Int = 0
+ }
+
+ @VisibleForTesting
+ internal fun tryScanningSource(source: LuminanceSource) {
+ if (qrState != STATE_DECODE_PROGRESS) {
+ return
+ }
+ val result = decodeSource(source) ?: decodeSource(source.invert())
+ result?.let {
+ scanCompleteListener?.onScanComplete(it)
+ }
+ }
+
+ @VisibleForTesting
+ @Suppress("TooGenericExceptionCaught")
+ internal fun decodeSource(source: LuminanceSource): String? {
+ return try {
+ val bitmap = createBinaryBitmap(source)
+ val rawResult = multiFormatReader.decodeWithState(bitmap)
+ if (rawResult != null) {
+ qrState = STATE_QRCODE_EXIST
+ rawResult.toString()
+ } else {
+ qrState = STATE_FIND_QRCODE
+ null
+ }
+ } catch (e: Exception) {
+ qrState = STATE_FIND_QRCODE
+ null
+ } finally {
+ multiFormatReader.reset()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createBinaryBitmap(source: LuminanceSource) =
+ BinaryBitmap(HybridBinarizer(source))
+
+ /**
+ * Returns the screen rotation
+ *
+ * @return the actual rotation of the device is one of these values:
+ * [Surface.ROTATION_0], [Surface.ROTATION_90], [Surface.ROTATION_180], [Surface.ROTATION_270]
+ */
+ @VisibleForTesting
+ internal fun getScreenRotation(): Int? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ this.context?.display?.rotation
+ } else {
+ @Suppress("DEPRECATION")
+ activity?.windowManager?.defaultDisplay?.rotation
+ }
+ }
+}
+
+/**
+ * Returns the size of the display, in pixels.
+ */
+@VisibleForTesting
+internal fun WindowManager.getDisplaySize(): Point {
+ val size = Point()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Tests for this branch will be added after
+ // https://github.com/mozilla-mobile/android-components/issues/9684 is implemented.
+ val windowMetrics = this.currentWindowMetrics
+ val windowInsets: WindowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets)
+
+ val insets = windowInsets.getInsetsIgnoringVisibility(
+ WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.displayCutout(),
+ )
+ val insetsWidth = insets.right + insets.left
+ val insetsHeight = insets.top + insets.bottom
+
+ val bounds: Rect = windowMetrics.bounds
+ size.set(bounds.width() - insetsWidth, bounds.height() - insetsHeight)
+ } else {
+ @Suppress("DEPRECATION")
+ this.defaultDisplay.getSize(size)
+ }
+ return size
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/AutoFitTextureView.kt b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/AutoFitTextureView.kt
new file mode 100644
index 0000000000..51c6759d0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/AutoFitTextureView.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. */
+
+package mozilla.components.feature.qr.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.TextureView
+import android.view.View
+import androidx.annotation.VisibleForTesting
+
+/**
+ * A [TextureView] that can be adjusted to a specified aspect ratio.
+ */
+open class AutoFitTextureView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0,
+) : TextureView(context, attrs, defStyle) {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var mRatioWidth = 0
+ private set
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var mRatioHeight = 0
+ private set
+
+ /**
+ * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
+ * calculated from the parameters. Note that the actual sizes of parameters don't matter, that
+ * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
+ *
+ * @param width Relative horizontal size
+ * @param height Relative vertical size
+ */
+ fun setAspectRatio(width: Int, height: Int) {
+ if (width < 0 || height < 0) {
+ throw IllegalArgumentException("Size cannot be negative.")
+ }
+ mRatioWidth = width
+ mRatioHeight = height
+ requestLayout()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ val width = View.MeasureSpec.getSize(widthMeasureSpec)
+ val height = View.MeasureSpec.getSize(heightMeasureSpec)
+ if (0 == mRatioWidth || 0 == mRatioHeight) {
+ setMeasuredDimension(width, height)
+ } else {
+ if (width < height * mRatioWidth / mRatioHeight) {
+ setMeasuredDimension(width, width * mRatioHeight / mRatioWidth)
+ } else {
+ setMeasuredDimension(height * mRatioWidth / mRatioHeight, height)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/CustomViewFinder.kt b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/CustomViewFinder.kt
new file mode 100644
index 0000000000..47051f3f20
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/CustomViewFinder.kt
@@ -0,0 +1,298 @@
+/* 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.qr.views
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.Rect
+import android.os.Build
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.ColorInt
+import androidx.annotation.Px
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat
+import androidx.core.text.HtmlCompat
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.ktx.android.util.spToPx
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+/**
+ * A [View] that shows a ViewFinder positioned in center of the camera view and draws an Overlay
+ */
+@Suppress("LargeClass")
+class CustomViewFinder @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+) : AppCompatImageView(context, attrs) {
+ private var messageResource: Int? = null
+ private val overlayPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ internal val viewFinderPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private var viewFinderPath: Path = Path()
+ private var viewFinderPathSaved: Boolean = false
+ private var overlayPath: Path = Path()
+ private var overlayPathSaved: Boolean = false
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal lateinit var viewFinderRectangle: Rect
+ private var viewFinderCornersSize: Float = 0f
+ private var viewFinderCornersRadius: Float = 0f
+ private var viewFinderTop: Float = 0f
+ private var viewFinderLeft: Float = 0f
+ private var viewFinderRight: Float = 0f
+ private var viewFinderBottom: Float = 0f
+ private var normalizedRadius: Float = 0f
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var scanMessageLayout: StaticLayout? = null
+ private lateinit var messageTextPaint: TextPaint
+
+ init {
+ isSaveEnabled = true
+ overlayPaint.style = Paint.Style.FILL
+ viewFinderPaint.style = Paint.Style.STROKE
+ overlayPath.fillType = Path.FillType.EVEN_ODD
+ viewFinderPath.fillType = Path.FillType.EVEN_ODD
+
+ this.layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ )
+ this.setOverlayColor(DEFAULT_OVERLAY_COLOR)
+ this.setViewFinderColor(DEFAULT_VIEWFINDER_COLOR)
+ this.setViewFinderStroke(DEFAULT_VIEWFINDER_THICKNESS_DP.dpToPx(resources.displayMetrics))
+ this.setViewFinderCornerRadius(DEFAULT_VIEWFINDER_CORNERS_RADIUS_DP.dpToPx(resources.displayMetrics))
+ }
+
+ /** Calculates viewfinder rectangle and triggers calculating message layout that depends on it */
+ internal fun computeViewFinderRect(width: Int, height: Int) {
+ if (width > 0 && height > 0) {
+ val minimumDimension = min(width.toFloat(), height.toFloat())
+
+ val viewFinderSide = (minimumDimension * DEFAULT_VIEWFINDER_WIDTH_RATIO).roundToInt()
+
+ val viewFinderLeftOrRight = (width - viewFinderSide) / 2
+ val viewFinderTopOrBottom = (height - viewFinderSide) / 2
+ viewFinderRectangle = Rect(
+ viewFinderLeftOrRight,
+ viewFinderTopOrBottom,
+ viewFinderLeftOrRight + viewFinderSide,
+ viewFinderTopOrBottom + viewFinderSide,
+ )
+
+ this.setViewFinderCornerSize(DEFAULT_VIEWFINDER_CORNER_SIZE_RATIO * viewFinderRectangle.width())
+
+ viewFinderTop = viewFinderRectangle.top.toFloat()
+ viewFinderLeft = viewFinderRectangle.left.toFloat()
+ viewFinderRight = viewFinderRectangle.right.toFloat()
+ viewFinderBottom = viewFinderRectangle.bottom.toFloat()
+ normalizedRadius = min(viewFinderCornersRadius, (viewFinderCornersSize - 1).coerceAtLeast(0f))
+
+ showMessage(scanMessageStringRes)
+ }
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ computeViewFinderRect(width, height)
+ }
+
+ // useful when you have a disappearing keyboard
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ redraw()
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration?) {
+ super.onConfigurationChanged(newConfig)
+ redraw()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ drawOverlay(canvas)
+ drawViewFinder(canvas)
+ drawMessage(canvas)
+ }
+
+ private fun redraw() {
+ overlayPathSaved = false
+ viewFinderPathSaved = false
+ computeViewFinderRect(width, height)
+ invalidate()
+ }
+
+ /** Draws the Overlay */
+ private fun drawOverlay(canvas: Canvas) {
+ if (!overlayPathSaved) {
+ overlayPath.apply {
+ reset()
+ moveTo(viewFinderLeft, viewFinderTop + normalizedRadius)
+ quadTo(viewFinderLeft, viewFinderTop, viewFinderLeft + normalizedRadius, viewFinderTop)
+ lineTo(viewFinderRight - normalizedRadius, viewFinderTop)
+ quadTo(viewFinderRight, viewFinderTop, viewFinderRight, viewFinderTop + normalizedRadius)
+ lineTo(viewFinderRight, viewFinderBottom - normalizedRadius)
+ quadTo(viewFinderRight, viewFinderBottom, viewFinderRight - normalizedRadius, viewFinderBottom)
+ lineTo(viewFinderLeft + normalizedRadius, viewFinderBottom)
+ quadTo(viewFinderLeft, viewFinderBottom, viewFinderLeft, viewFinderBottom - normalizedRadius)
+ lineTo(viewFinderLeft, viewFinderTop + normalizedRadius)
+ moveTo(0f, 0f)
+ lineTo(width.toFloat(), 0f)
+ lineTo(width.toFloat(), height.toFloat())
+ lineTo(0f, height.toFloat())
+ lineTo(0f, 0f)
+ }
+ overlayPathSaved = true
+ }
+ canvas.drawPath(overlayPath, overlayPaint)
+ }
+
+ /** Draws the ViewFinder */
+ private fun drawViewFinder(canvas: Canvas) {
+ if (!viewFinderPathSaved) {
+ viewFinderPath.apply {
+ reset()
+ moveTo(viewFinderLeft, viewFinderTop + viewFinderCornersSize)
+ lineTo(viewFinderLeft, viewFinderTop + normalizedRadius)
+ quadTo(viewFinderLeft, viewFinderTop, viewFinderLeft + normalizedRadius, viewFinderTop)
+ lineTo(viewFinderLeft + viewFinderCornersSize, viewFinderTop)
+ moveTo(viewFinderRight - viewFinderCornersSize, viewFinderTop)
+ lineTo(viewFinderRight - normalizedRadius, viewFinderTop)
+ quadTo(viewFinderRight, viewFinderTop, viewFinderRight, viewFinderTop + normalizedRadius)
+ lineTo(viewFinderRight, viewFinderTop + viewFinderCornersSize)
+ moveTo(viewFinderRight, viewFinderBottom - viewFinderCornersSize)
+ lineTo(viewFinderRight, viewFinderBottom - normalizedRadius)
+ quadTo(viewFinderRight, viewFinderBottom, viewFinderRight - normalizedRadius, viewFinderBottom)
+ lineTo(viewFinderRight - viewFinderCornersSize, viewFinderBottom)
+ moveTo(viewFinderLeft + viewFinderCornersSize, viewFinderBottom)
+ lineTo(viewFinderLeft + normalizedRadius, viewFinderBottom)
+ quadTo(viewFinderLeft, viewFinderBottom, viewFinderLeft, viewFinderBottom - normalizedRadius)
+ lineTo(viewFinderLeft, viewFinderBottom - viewFinderCornersSize)
+ }
+ viewFinderPathSaved = true
+ }
+ canvas.drawPath(viewFinderPath, this.viewFinderPaint)
+ }
+
+ /**
+ * Creates a Static Layout used to show a message below the viewfinder
+ */
+ @Suppress("Deprecation")
+ private fun showMessage(@StringRes scanMessageId: Int?) {
+ val scanMessage = if (scanMessageId != null) {
+ HtmlCompat.fromHtml(
+ context.getString(scanMessageId),
+ HtmlCompat.FROM_HTML_MODE_LEGACY,
+ )
+ } else {
+ ""
+ }
+ messageTextPaint = TextPaint().apply {
+ isAntiAlias = true
+ color = ContextCompat.getColor(context, android.R.color.white)
+ textSize = SCAN_MESSAGE_TEXT_SIZE_SP.spToPx(resources.displayMetrics)
+ }
+
+ scanMessageLayout =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ StaticLayout.Builder.obtain(
+ scanMessage,
+ 0,
+ scanMessage.length,
+ messageTextPaint,
+ viewFinderRectangle.width(),
+ ).setAlignment(Layout.Alignment.ALIGN_CENTER).build()
+ } else {
+ StaticLayout(
+ scanMessage,
+ messageTextPaint,
+ viewFinderRectangle.width(),
+ Layout.Alignment.ALIGN_CENTER,
+ 1.0f,
+ 0.0f,
+ true,
+ )
+ }
+
+ messageResource = scanMessageId
+ }
+
+ /** Draws text below the ViewFinder. */
+ private fun drawMessage(canvas: Canvas) {
+ canvas.save()
+ canvas.translate(
+ viewFinderRectangle.left.toFloat(),
+ viewFinderRectangle.bottom.toFloat() +
+ SCAN_MESSAGE_TOP_PADDING_DP.dpToPx(resources.displayMetrics),
+ )
+ scanMessageLayout?.draw(canvas)
+ canvas.restore()
+ }
+
+ /** Sets the color for the Overlay. */
+ private fun setOverlayColor(@ColorInt color: Int) {
+ overlayPaint.color = color
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ /** Sets the stroke color for the ViewFinder. */
+ fun setViewFinderColor(@ColorInt color: Int) {
+ viewFinderPaint.color = color
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ /** Sets the stroke width for the ViewFinder. */
+ private fun setViewFinderStroke(@Px stroke: Float) {
+ viewFinderPaint.strokeWidth = stroke
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ /** Sets the corner size for the ViewFinder. */
+ private fun setViewFinderCornerSize(@Px size: Float) {
+ viewFinderCornersSize = size
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ /** Sets the corner radius for the ViewFinder. */
+ private fun setViewFinderCornerRadius(@Px radius: Float) {
+ viewFinderCornersRadius = radius
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ companion object {
+ internal const val DEFAULT_VIEWFINDER_WIDTH_RATIO = 0.5f
+ private const val DEFAULT_OVERLAY_COLOR = 0x77000000
+ private const val DEFAULT_VIEWFINDER_COLOR = Color.WHITE
+ private const val DEFAULT_VIEWFINDER_THICKNESS_DP = 4f
+ private const val DEFAULT_VIEWFINDER_CORNER_SIZE_RATIO = 0.25f
+ private const val DEFAULT_VIEWFINDER_CORNERS_RADIUS_DP = 8f
+ private const val SCAN_MESSAGE_TOP_PADDING_DP = 10f
+ internal const val SCAN_MESSAGE_TEXT_SIZE_SP = 12f
+ internal var scanMessageStringRes: Int? = null
+
+ /** Sets the message to be displayed below ViewFinder. */
+ fun setMessage(scanMessageStringRes: Int?) {
+ this.scanMessageStringRes = scanMessageStringRes
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-hdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-hdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..9c9f4585e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-hdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-ldpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-ldpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..64f1ff0285
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-ldpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-mdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-mdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..0f06992180
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-mdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xhdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xhdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..eb0340a6be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xhdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxhdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxhdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..d26d0b8a34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxhdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxxhdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxxhdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..5c28c6b470
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxxhdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/layout/fragment_layout.xml b/mobile/android/android-components/components/feature/qr/src/main/res/layout/fragment_layout.xml
new file mode 100644
index 0000000000..0b930d33e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/layout/fragment_layout.xml
@@ -0,0 +1,35 @@
+<?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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/background_dark"
+ android:clickable="true"
+ android:focusable="true"
+ tools:ignore="Overdraw">
+
+ <TextView
+ android:id="@+id/camera_error"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:text="@string/mozac_feature_qr_scanner_no_camera"
+ android:textColor="@android:color/white"
+ android:visibility="gone" />
+
+ <mozilla.components.feature.qr.views.AutoFitTextureView
+ android:id="@+id/texture"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerInParent="true" />
+
+ <mozilla.components.feature.qr.views.CustomViewFinder
+ android:id="@+id/view_finder"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerInParent="true" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..5c33f13247
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-am/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">የQR ስካነር</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">በመሳሪያው ላይ ምንም ካሜራ የለም</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..6b2518105f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-an/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escaner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Lo dispositivo no tiene camara disponible</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..a4563264a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ar/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ماسح رمز QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ما من كمرة متاحة في الجهاز</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..c1ae3c897f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ast/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de códigos QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nun hai nenguna cámara disponible nel preséu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..db835de8d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-az/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skaner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Cihazda kamera yoxdur</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..e101130566
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-azb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR اسکنچی‌سی</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">جهازدا کامئرا یوخ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..06d0258b86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ban/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ten wénten kaméra ring perangkat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..b69588cfef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-be/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-сканэр</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Няма даступнай камеры на прыладзе</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..2e354a11f7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-bg/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Скенер за QR код</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Устройството не разполага с камера</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..ccafaf7766
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-bn/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR স্ক্যানার</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ডিভাইসে কোনো ক্যামেরা নেই</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..f076513b3e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-br/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">C’hwilerver QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">N’eus bet kavet kamera ebet</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..ea89910221
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-bs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skener</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nema dostupne kamere na uređaju</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..f6c743ec4c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ca/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escàner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hi ha cap càmera disponible en el dispositiu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..ede6264fbc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-cak/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR tz\'ajwachib\'äl</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Majun elesäy wachib\'äl pa ri okisab\'äl</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..36247920f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Wala\'y camera nga magamit sa device</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..8928c305dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">پشکنەری QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">کامێرا بوونی نیە لەسەر ئەم ئامێرە</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..1c38f1ea2f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-co/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Analiza di codice QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Alcunu apparechju-fotò hè dispunibule nant’à l’apparechju</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..394f65f77a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-cs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skener QR kódů</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Zařízení nemá fotoaparát</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..ca4c7127b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-cy/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Sganiwr QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nid oes camera ar gael ar y ddyfais</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..af86f6552d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-da/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Intet kamera tilgængelig på enheden</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..79aa856fa0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-de/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-Scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Keine Kamera auf dem Gerät verfügbar</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..1d0c62d9a4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Žedna kamera njejo k dispoziciji na rěźe</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..4fa0a90e5b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-el/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Σάρωση QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Η συσκευή σας δεν διαθέτει κάμερα</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..5fae94c403
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No camera available on device</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..5fae94c403
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No camera available on device</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..2be7a096dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-eo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skanilo QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Neniu fimilo disponebla en la aparato</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..9e31766b3c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-et/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Seadme kaamera pole saadaval</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..ecd1d47fa6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-eu/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR eskanerra</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Kamera ez dago erabilgarri gailuan</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..66d7e8892f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fa/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">پویندهٔ رمزینهٔ پاس</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">دوربین در این افزاره در دسترس نیست</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..ca95dd65f3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fi/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanneri</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Kameraa ei ole käytettävissä tällä laitteella</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..9e580ec1d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Aucune caméra disponible sur l’appareil</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..e9ff7a2bd9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fur/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scansionadôr QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nissune fotocjamare disponibile sul dispositîf</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..866102f55b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Gjin kamera beskikber op apparaat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..e326071aff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-gd/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Sganair QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Chan eil camara ri làimh air an uidheam</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..95eea190f8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-gl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Non hai cámara dispoñíbel no dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..29987db474
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-gn/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR moha’ãngaha</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ndaipóri ta’ãngamýi mba’e’okápe</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..18860156b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR સ્કેનર</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ઉપકરણ પર કોઈ કેમેરો ઉપલબ્ધ નથી</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..1bc5719127
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR स्कैनर</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">डिवाइस पर कोई कैमरा उपलब्ध नहीं है</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..5e64717bca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hil/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..e3829462a1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skener</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Na uređaju nema kamere</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..357f90e1b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skener</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Na graće žana kamera k dispoziciji njeje</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..1818bcbaea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hu/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-leolvasó</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nincs elérhető kamera az eszközön</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..5deb3d814c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR սկաներ</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Սարքում տեսախցիկը հասնելի չէ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..dfcbe5740d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ia/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanditor de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nulle camera disponibile sur le apparato</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..358ce12175
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-in/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Pemindai QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Tidak ada kamera yang tersedia di perangkat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..6927a627a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-is/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanni</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Engin myndavél tiltæk á tækinu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..7c936c965b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-it/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nessuna fotocamera disponibile sul dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..82896d047c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-iw/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">סורק QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">אין מצלמה זמינה במכשיר</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..a4eb8620c6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ja/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR コードスキャナー</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">使用可能なカメラが端末にありません</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..6ed347acb1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ka/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-წამკითხველი</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">მოწყობილობაზე კამერა არაა</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..c87ff57355
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skaner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Qurılmada kamera joq</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..a95b95687a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kab/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">alnafraḍ QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ulac takamiṛat deg ibenk</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..dd74fd220b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR сканері</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Құрылғыда камера жоқ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..808536e55d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Xwînera QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Di cîhazê de kamera tune</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..d059ae06be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ಕ್ಯೂಆರ್ ಸ್ಕ್ಯಾನರ್</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..b57f4803f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ko/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR 스캐너</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">기기에 사용 가능한 카메라 없음</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..aff4d28c6c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-lo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ຕົວສະແກນ QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ອຸປະກອນນີ້ບໍ່ມີກ້ອງ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..dde9b8fc28
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-lt/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skaitytuvas</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Įrenginyje nėra kameros</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..e4e5924041
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-mix/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Ndatava QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Koo ña ndatava nu kaa ndusu ku</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..ce55ae4e3c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-mr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR स्कॅनर</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">उपकरणावर कोणताही कॅमेरा उपलब्ध नाही</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..5e66f1b09c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-my/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR ဖတ်ရန်</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">စက်တွင်ကင်မရာမပါပါ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..7e1f547927
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ingen kamera tilgjengelige på enheten</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..1b22f9fc30
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR स्क्यानर</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">यन्त्रमा कुनै क्यामरा उपलब्ध छैन</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..bbd95f9d57
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-nl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Geen camera beschikbaar op apparaat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..24e3198e26
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skannar</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ingen kamera tilgjengelege på eininga</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..1828bb344f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-oc/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Numerizador QR còdi</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Cap de camèra pas disponibla sul periferic</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..097b5d7efb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR ਸਕੈਨਰ</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ਡਿਵਾਈਸ ਉੱਤੇ ਕੈਮਰਾ ਉਪਲਬਧ ਨਹੀਂ ਹੈ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..425257dbbf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">کیوآر کوڈ سکین والا</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">کوئی کیمرہ نہیں لبھیا</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..4f6016e055
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skaner kodów QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Urządzenie nie ma aparatu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..eb2379dd0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nenhuma câmera disponível no dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..cf4d062a1b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Digitalizador QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nenhuma câmara disponível no dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..46cc46ca31
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-rm/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nagina camera disponibla en l\'apparat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..534096197c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ro/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nu există camere disponibile pe dispozitiv</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..8c14a264d3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ru/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Считыватель штрих-кодов</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">На этом устройстве камера недоступна</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..74a96b7d83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sat/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR ᱥᱠᱟᱱᱚᱨ</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ᱥᱟᱫᱷᱚᱱ ᱨᱮ ᱠᱮᱢᱨᱟ ᱵᱟᱹᱱᱩᱜ-ᱟ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..ee4c7fd470
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sc/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Iscansionadore de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nissuna càmera a disponimentu in su dispositivu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..d841d8365a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-si/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR සුපිරික්සකය</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">උපාංගයෙහි රූගතයක් නැත</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..1f60626aa0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skener QR kódov</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Zariadenie nemá fotoaparát</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..e44642f62c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-skr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR سکینر</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ڈیوائس تے کوئی کیمرہ دستیاب کائنی</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..efffc8172e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Bralnik QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Naprava nima razpoložljive kamere</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..8c8796d692
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sq/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skanues QR-esh</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">S’ka kamera të përdorshme në pajisje</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..91503317dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR скенер</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">На уређају није доступна камера</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..9091036029
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-su/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Paminday QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Henteu aya kaméra dina alat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..cc0601fb83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ingen kamera tillgänglig på enheten</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..18cb652d7e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ta/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR வருடி</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">சாதனத்தில் படக்கருவி இல்லை</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..8ab2d2d6da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-te/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR స్కానర్</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">పరికరంలో కెమెరా లేదు</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..f614203639
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tg/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Аксбардории QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ягон камера дар дастгоҳ дастрас нест</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..03d2a7c3a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-th/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ตัวสแกน QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ไม่มีกล้องที่พร้อมใช้งานบนอุปกรณ์</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..f61d1973fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Walang magagamit na camera sa aparato</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..43d57c4a0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tok/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ilo lukin pi leko sona</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ilo sina li jo ala e ilo lukin</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..b64a1decc7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR tarayıcı</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Cihazda kamera yok</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..0f85b43439
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-trs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Sa natsij nej QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nitāj aga\’ nari ñadu\’ua nikāj aga\’ nan</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..2828e3f906
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tt/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR сканеры</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Җиһазда камера юк</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..67ff29d85d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ug/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR سايىلىغۇچ</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">كامېرا يوق ئىكەن</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..4154a29991
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-uk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Сканер QR-кодів</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Немає доступу до камери пристрою</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..dc2d085e48
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ur/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR سکینر</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">آلہ پر کوئی کمیرہ دستیاب نہیں ہے</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..ba70db4633
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-uz/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skaner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Qurilmada kamera mavjud emas</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..78039314fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-vi/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Quét mã QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Hiện không tìm thấy máy ảnh trên thiết bị</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..a17b5f6d7a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-yo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Síkánà QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Kò sí ẹ̀rọ-ayàwòrán kankan lórí ẹ̀rọ yìí</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..cdf50696d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">扫码器</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">设备上没有可用的相机</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..5023389874
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR Code 掃描器</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">裝置上無攝影機可用</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..004d4bbd59
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+<?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>
+ <color name="mozac_feature_qr_scan_success_color">#9059FF</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..dcf8ae522c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+<?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>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No camera available on device</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFeatureTest.kt b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFeatureTest.kt
new file mode 100644
index 0000000000..1c8ece369c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFeatureTest.kt
@@ -0,0 +1,269 @@
+/* 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.qr
+
+import android.Manifest.permission.CAMERA
+import android.content.pm.PackageManager
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.qr.QrFeature.Companion.QR_FRAGMENT_TAG
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.grantPermission
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+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.MockitoAnnotations.openMocks
+
+@RunWith(AndroidJUnit4::class)
+class QrFeatureTest {
+
+ @Mock
+ lateinit var fragmentManager: FragmentManager
+
+ @Before
+ fun setUp() {
+ openMocks(this)
+
+ mock<FragmentTransaction>().let { transaction ->
+ whenever(fragmentManager.beginTransaction())
+ .thenReturn(transaction)
+ whenever(transaction.add(anyInt(), any(), anyString()))
+ .thenReturn(transaction)
+ whenever(transaction.remove(any()))
+ .thenReturn(transaction)
+ }
+ }
+
+ fun `scanning is in progress if the scanning fragment is shown`() {
+ val feature = QrFeature(testContext, fragmentManager)
+
+ assertFalse(feature.isScanInProgress)
+
+ doReturn(mock<QrFragment>()).`when`(fragmentManager).findFragmentByTag(QR_FRAGMENT_TAG)
+ assertTrue(feature.isScanInProgress)
+ }
+
+ @Test
+ fun `feature requests camera permission if required`() {
+ // Given
+ var callbackInvoked = false
+ val permissionsCallback: (permissions: Array<String>) -> Unit = {
+ callbackInvoked = true
+ }
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ onNeedToRequestPermissions = permissionsCallback,
+ )
+
+ // When
+ val scanResult = feature.scan()
+
+ // Then
+ assertFalse(scanResult)
+ assertTrue(callbackInvoked)
+ }
+
+ @Test
+ fun `scan starts qr fragment if permissions granted`() {
+ // Given
+ grantPermission(CAMERA)
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ )
+
+ // When
+ val scanResult = feature.scan()
+
+ // Then
+ assertTrue(scanResult)
+ verify(fragmentManager).beginTransaction()
+ }
+
+ @Test
+ fun `scan resumes qr fragment if permissions granted and scanning was already started`() {
+ grantPermission(CAMERA)
+ val feature = QrFeature(testContext, fragmentManager)
+ val qrFragment: QrFragment = mock()
+ doReturn(qrFragment).`when`(fragmentManager).findFragmentByTag(QR_FRAGMENT_TAG)
+
+ val scanResult = feature.scan()
+
+ assertTrue(scanResult)
+ verify(qrFragment).startScanning()
+ }
+
+ @Test
+ fun `onPermissionsResult displays scanner only if permission granted`() {
+ // Given
+ val feature = spy(
+ QrFeature(
+ testContext,
+ fragmentManager,
+ ),
+ )
+
+ // When
+ resolvePermissionRequestFrom(feature) { PermissionResolution.DENIED }
+
+ // Then
+ verify(feature, never()).scan(anyInt())
+ verify(feature).removeQrFragment()
+
+ // When
+ grantPermission(CAMERA)
+ resolvePermissionRequestFrom(feature) { PermissionResolution.GRANTED }
+
+ // Then
+ verify(feature, times(1)).scan(anyInt())
+ verify(feature, times(1)).removeQrFragment()
+ }
+
+ @Test
+ fun `scan result is forwarded to caller`() {
+ // Given
+ var scanResult: String? = null
+ val scanResultCallback: OnScanResult = { result ->
+ scanResult = result
+ }
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ onScanResult = scanResultCallback,
+ )
+
+ // When
+ feature.scanCompleteListener.onScanComplete("result")
+
+ // Then
+ assertEquals("result", scanResult)
+ }
+
+ @Test
+ fun `qr fragment is removed on back pressed`() {
+ // Given
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(mock())
+
+ val feature = spy(
+ QrFeature(
+ testContext,
+ fragmentManager,
+ ),
+ )
+
+ // When
+ feature.onBackPressed()
+
+ // Then
+ verify(feature).removeQrFragment()
+ }
+
+ @Test
+ fun `start attaches scan complete listener`() {
+ // Given
+ val fragment = mock<QrFragment>()
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(fragment)
+
+ val feature = spy(
+ QrFeature(
+ testContext,
+ fragmentManager,
+ ),
+ )
+ val listener = feature.scanCompleteListener
+
+ // When
+ feature.start()
+
+ // Then
+ verify(feature).setScanCompleteListener(listener)
+ }
+
+ @Test
+ fun `stop attaches a null listener`() {
+ // Given
+ val fragment = mock<QrFragment>()
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(fragment)
+ val feature = spy(
+ QrFeature(
+ testContext,
+ fragmentManager,
+ ),
+ )
+
+ // When
+ feature.stop()
+
+ // Then
+ verify(feature).setScanCompleteListener(null)
+ }
+
+ @Test
+ fun `setScanCompleteListener allows setting a null callback in QrFragment`() {
+ // Given
+ val fragment = mock<QrFragment>()
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(fragment)
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ )
+ fragment.scanCompleteListener = feature.scanCompleteListener
+
+ // When
+ feature.setScanCompleteListener(null)
+ // Then
+ verify(fragment).scanCompleteListener = null
+ }
+
+ @Test
+ fun `setScanCompleteListener allows setting a valid callback in QrFragment`() {
+ // Given
+ val fragment = mock<QrFragment>()
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(fragment)
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ )
+ fragment.scanCompleteListener = null
+
+ // When
+ feature.setScanCompleteListener(feature.scanCompleteListener)
+ // Then
+ verify(fragment).scanCompleteListener = feature.scanCompleteListener
+ }
+}
+
+private enum class PermissionResolution(val value: Int) {
+ GRANTED(PackageManager.PERMISSION_GRANTED),
+ DENIED(PackageManager.PERMISSION_DENIED),
+}
+
+private fun resolvePermissionRequestFrom(
+ feature: QrFeature,
+ resolution: () -> PermissionResolution,
+) {
+ feature.onPermissionsResult(emptyArray(), IntArray(1) { resolution().value })
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFragmentTest.kt b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFragmentTest.kt
new file mode 100644
index 0000000000..98c58deec0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFragmentTest.kt
@@ -0,0 +1,786 @@
+/* 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.qr
+
+import android.Manifest.permission
+import android.content.Context
+import android.content.pm.PackageManager
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.params.SessionConfiguration
+import android.media.Image
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper.getMainLooper
+import android.util.Size
+import android.view.Display
+import android.view.Surface
+import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.fragment.app.FragmentActivity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.BinaryBitmap
+import com.google.zxing.LuminanceSource
+import com.google.zxing.MultiFormatReader
+import com.google.zxing.NotFoundException
+import com.google.zxing.PlanarYUVLuminanceSource
+import mozilla.components.feature.qr.QrFragment.Companion.chooseOptimalSize
+import mozilla.components.feature.qr.views.AutoFitTextureView
+import mozilla.components.feature.qr.views.CustomViewFinder
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doNothing
+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`
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import java.nio.ByteBuffer
+import java.util.concurrent.ExecutorService
+
+@RunWith(AndroidJUnit4::class)
+class QrFragmentTest {
+
+ @Test
+ fun `initialize QR fragment`() {
+ val scanCompleteListener = mock<QrFragment.OnScanCompleteListener>()
+ val qrFragment = spy(QrFragment.newInstance(scanCompleteListener))
+
+ qrFragment.scanCompleteListener?.onScanComplete("result")
+ shadowOf(getMainLooper()).idle()
+ verify(scanCompleteListener).onScanComplete("result")
+ }
+
+ @Test
+ fun `onPause closes camera, stops background thread, and shuts down executor service`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ qrFragment.onPause()
+
+ verify(qrFragment).stopBackgroundThread()
+ verify(qrFragment).stopExecutorService()
+ verify(qrFragment).closeCamera()
+ }
+
+ @Test
+ fun `onResume opens camera, starts background thread and starts executor service`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val context: Context = mock()
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(context).checkPermission(eq(permission.CAMERA), anyInt(), anyInt())
+ doReturn(context).`when`(qrFragment).context
+ doNothing().`when`(qrFragment).startScanning()
+
+ qrFragment.onResume()
+
+ verify(qrFragment).startScanning()
+ }
+
+ @Test
+ fun `onResume avoids starting scanning if the camera permission is missing`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val context: Context = mock()
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .`when`(context).checkPermission(eq(permission.CAMERA), anyInt(), anyInt())
+ doReturn(context).`when`(qrFragment).context
+ doNothing().`when`(qrFragment).startScanning()
+
+ qrFragment.onResume()
+
+ verify(qrFragment, never()).startScanning()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `WHEN running a device lower than P THEN startExecutorService should not be executed`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ whenever(qrFragment.textureView.isAvailable).thenReturn(true)
+ doNothing().`when`(qrFragment).maybeStartBackgroundThread()
+ doNothing().`when`(qrFragment).tryOpenCamera(anyInt(), anyInt(), anyBoolean())
+ val context: Context = mock()
+ doReturn(PackageManager.PERMISSION_GRANTED).`when`(context).checkSelfPermission(permission.CAMERA)
+ doReturn(context).`when`(qrFragment).context
+
+ qrFragment.onResume()
+
+ verify(qrFragment, never()).maybeStartExecutorService()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `WHEN calling createCaptureSessionCompat on a device lower than P THEN use older API`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val camera = mock<CameraDevice>()
+ val imageSurface = mock<Surface>()
+ val surface = mock<Surface>()
+ val stateCallback = mock<CameraCaptureSession.StateCallback>()
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ whenever(qrFragment.textureView.isAvailable).thenReturn(true)
+
+ qrFragment.createCaptureSessionCompat(camera, imageSurface, surface, stateCallback)
+
+ @Suppress("DEPRECATION")
+ verify(camera).createCaptureSession(listOf(imageSurface, surface), stateCallback, null)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.P])
+ fun `WHEN calling createCaptureSessionCompat on a device higher than P THEN use newer api`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val camera = mock<CameraDevice>()
+ val imageSurface = mock<Surface>()
+ val surface = mock<Surface>()
+ val stateCallback = mock<CameraCaptureSession.StateCallback>()
+
+ doNothing().`when`(qrFragment).maybeStartExecutorService()
+ whenever(qrFragment.shouldStartExecutorService()).thenReturn(true)
+
+ qrFragment.backgroundExecutor = mock()
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ whenever(qrFragment.textureView.isAvailable).thenReturn(true)
+
+ qrFragment.createCaptureSessionCompat(camera, imageSurface, surface, stateCallback)
+
+ verify(camera).createCaptureSession(any<SessionConfiguration>())
+ }
+
+ @Test
+ fun `onStop resets state`() {
+ val qrFragment = QrFragment.newInstance(mock())
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ qrFragment.onStop()
+
+ assertEquals(QrFragment.STATE_FIND_QRCODE, QrFragment.qrState)
+ }
+
+ @Test
+ fun `onViewCreated sets initial state`() {
+ val qrFragment = QrFragment.newInstance(mock())
+ val view: View = mock()
+ val textureView: AutoFitTextureView = mock()
+ val viewFinder: CustomViewFinder = mock()
+ val cameraErrorView: TextView = mock()
+
+ whenever(view.findViewById<AutoFitTextureView>(R.id.texture)).thenReturn(textureView)
+ whenever(view.findViewById<CustomViewFinder>(R.id.view_finder)).thenReturn(viewFinder)
+ whenever(view.findViewById<TextView>(R.id.camera_error)).thenReturn(cameraErrorView)
+
+ qrFragment.onViewCreated(view, mock())
+ assertEquals(QrFragment.STATE_FIND_QRCODE, QrFragment.qrState)
+ }
+
+ @Test
+ fun `qr fragment has the correct scan message resource`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+
+ val qrFragmentWithMessage = QrFragment.newInstance(listener, R.string.mozac_feature_qr_scanner)
+ val qrFragmentNoMessage = QrFragment.newInstance(listener, null)
+
+ assertEquals(null, qrFragmentNoMessage.scanMessage)
+ assertEquals(R.string.mozac_feature_qr_scanner, qrFragmentWithMessage.scanMessage)
+ }
+
+ @Test
+ fun `listener is invoked on successful qr scan`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val source = mock<PlanarYUVLuminanceSource>()
+ val result = com.google.zxing.Result("qrcode-result", ByteArray(0), emptyArray(), BarcodeFormat.ITF)
+ whenever(reader.decodeWithState(any())).thenReturn(result)
+ qrFragment.multiFormatReader = reader
+ qrFragment.scanCompleteListener = listener
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ qrFragment.tryScanningSource(source)
+ shadowOf(getMainLooper()).idle()
+
+ verify(listener).onScanComplete(eq("qrcode-result"))
+ assertEquals(QrFragment.STATE_QRCODE_EXIST, QrFragment.qrState)
+ }
+
+ @Test
+ fun `resets state after each decoding attempt`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+
+ val source = mock<PlanarYUVLuminanceSource>()
+ val invertedSource = mock<PlanarYUVLuminanceSource>()
+
+ val bitmap = mock<BinaryBitmap>()
+ val invertedBitmap = mock<BinaryBitmap>()
+
+ whenever(source.invert()).thenReturn(invertedSource)
+
+ with(qrFragment) {
+ whenever(createBinaryBitmap(source)).thenReturn(bitmap)
+ whenever(createBinaryBitmap(invertedSource)).thenReturn(invertedBitmap)
+ }
+
+ qrFragment.multiFormatReader = reader
+
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ qrFragment.tryScanningSource(source)
+
+ assertEquals(QrFragment.STATE_FIND_QRCODE, QrFragment.qrState)
+ verify(reader, times(2)).reset()
+ }
+
+ @Test
+ fun `don't consider scanning complete if decoding not in progress`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val source = mock<PlanarYUVLuminanceSource>()
+ qrFragment.scanCompleteListener = listener
+ qrFragment.multiFormatReader = reader
+ whenever(reader.decodeWithState(any())).thenThrow(NotFoundException::class.java)
+ QrFragment.qrState = QrFragment.STATE_FIND_QRCODE
+
+ qrFragment.tryScanningSource(source)
+
+ verify(reader, never()).decodeWithState(any())
+ verify(listener, never()).onScanComplete(any())
+ }
+
+ @Test
+ fun `early return null for decoding attempt if decoding not in progress`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val source = mock<PlanarYUVLuminanceSource>()
+ qrFragment.multiFormatReader = reader
+ whenever(reader.decodeWithState(any())).thenThrow(NotFoundException::class.java)
+ QrFragment.qrState = QrFragment.STATE_FIND_QRCODE
+ qrFragment.tryScanningSource(source)
+
+ verify(qrFragment, never()).decodeSource(any())
+ verify(reader, never()).decodeWithState(any())
+ verify(listener, never()).onScanComplete(any())
+ }
+
+ @Test
+ fun `async scanning decodes original unmodified source`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val imageCaptor = argumentCaptor<BinaryBitmap>()
+ val source = mock<LuminanceSource>()
+ val bitmap = mock<BinaryBitmap>()
+ val result = mock<com.google.zxing.Result>()
+ qrFragment.multiFormatReader = reader
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ with(qrFragment) {
+ whenever(createBinaryBitmap(source)).thenReturn(bitmap)
+ }
+
+ whenever(reader.decodeWithState(bitmap)).thenReturn(result)
+
+ qrFragment.tryScanningSource(source)
+ verify(reader).decodeWithState(imageCaptor.capture())
+ assertSame(bitmap, imageCaptor.value)
+ }
+
+ @Test
+ fun `camera is closed on disconnect and error`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ var camera: CameraDevice = mock()
+ qrFragment.stateCallback.onDisconnected(camera)
+ verify(camera).close()
+
+ camera = mock()
+ qrFragment.stateCallback.onError(camera, 0)
+ verify(camera).close()
+ }
+
+ @Test
+ fun `catches and handles CameraAccessException when creating preview session`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ val camera: CameraDevice = mock()
+ whenever(camera.createCaptureRequest(anyInt())).thenThrow(CameraAccessException(123))
+ qrFragment.cameraDevice = camera
+
+ val textureView: AutoFitTextureView = mock()
+ whenever(textureView.surfaceTexture).thenReturn(mock())
+ qrFragment.textureView = textureView
+
+ qrFragment.previewSize = mock()
+
+ try {
+ qrFragment.createCameraPreviewSession()
+ } catch (e: CameraAccessException) {
+ fail("CameraAccessException should have been caught and logged, not re-thrown.")
+ }
+ }
+
+ @Test
+ fun `catches and handles IllegalStateException when creating preview session`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ val camera: CameraDevice = mock()
+ whenever(camera.createCaptureRequest(anyInt())).thenThrow(IllegalStateException("CameraDevice was already closed"))
+ qrFragment.cameraDevice = camera
+
+ val textureView: AutoFitTextureView = mock()
+ whenever(textureView.surfaceTexture).thenReturn(mock())
+ qrFragment.textureView = textureView
+
+ qrFragment.previewSize = mock()
+
+ try {
+ qrFragment.createCameraPreviewSession()
+ } catch (e: IllegalStateException) {
+ fail("IllegalStateException should have been caught and logged, not re-thrown.")
+ }
+ }
+
+ @Test
+ fun `catches and handles CameraAccessException when opening camera`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ whenever(qrFragment.setUpCameraOutputs(anyInt(), anyInt())).then { }
+
+ val cameraManager: CameraManager = mock()
+ whenever(cameraManager.openCamera(anyString(), any<CameraDevice.StateCallback>(), any()))
+ .thenThrow(CameraAccessException(123))
+
+ val activity: FragmentActivity = mock()
+ whenever(activity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+ whenever(qrFragment.activity).thenReturn(activity)
+ qrFragment.cameraId = "mockCamera"
+
+ try {
+ qrFragment.openCamera(1920, 1080)
+ } catch (e: CameraAccessException) {
+ fail("CameraAccessException should have been caught and logged, not re-thrown.")
+ }
+ }
+
+ @Test
+ fun `throws exception on device without camera`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ whenever(qrFragment.setUpCameraOutputs(anyInt(), anyInt())).then { }
+
+ val cameraManager: CameraManager = mock()
+ val activity: FragmentActivity = mock()
+ whenever(activity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+ whenever(qrFragment.activity).thenReturn(activity)
+
+ qrFragment.cameraId = null
+ try {
+ qrFragment.openCamera(1920, 1080)
+ fail("Expected IllegalStateException")
+ } catch (e: IllegalStateException) {
+ assertEquals("No camera found on device", e.message)
+ }
+ }
+
+ @Test
+ fun `choose optimal size`() {
+ var size = chooseOptimalSize(
+ arrayOf(Size(640, 480), Size(1024, 768)),
+ 640,
+ 480,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(16, 9),
+ )
+
+ assertEquals(640, size.width)
+ assertEquals(480, size.height)
+
+ size = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(640, 480)),
+ 1024,
+ 768,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(4, 3),
+ )
+
+ assertEquals(640, size.width)
+ assertEquals(480, size.height)
+
+ size = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(640, 480), Size(320, 240)),
+ 2048,
+ 768,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(4, 3),
+ )
+
+ assertEquals(640, size.width)
+ assertEquals(480, size.height)
+
+ size = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(640, 480), Size(320, 240)),
+ 1024,
+ 1024,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(4, 3),
+ )
+
+ assertEquals(640, size.width)
+ assertEquals(480, size.height)
+
+ size = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(786, 480), Size(320, 240)),
+ 2048,
+ 1024,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(16, 9),
+ )
+
+ assertEquals(1024, size.width)
+ assertEquals(768, size.height)
+ }
+
+ @Test
+ fun `read image source adjusts for rowstride`() {
+ val image: Image = mock()
+ val plane: Image.Plane = mock()
+
+ `when`(image.height).thenReturn(1080)
+ `when`(image.width).thenReturn(1920)
+ `when`(plane.pixelStride).thenReturn(1)
+ `when`(image.planes).thenReturn(arrayOf(plane))
+
+ // Create an image source where rowstride is equal to the width
+ val bytesWithEqualRowStride: ByteBuffer = ByteBuffer.allocate(1080 * 1920)
+ `when`(plane.buffer).thenReturn(bytesWithEqualRowStride)
+ `when`(plane.rowStride).thenReturn(1920)
+ assertArrayEquals(bytesWithEqualRowStride.array(), QrFragment.readImageSource(image).matrix)
+
+ // Create an image source where rowstride is greater than the width
+ val bytesWithNotEqualRowStride: ByteBuffer = ByteBuffer.allocate(1080 * (1920 + 128))
+ `when`(plane.buffer).thenReturn(bytesWithNotEqualRowStride)
+ `when`(plane.rowStride).thenReturn(2048)
+
+ // The rowstride / offset should have been taken into account resulting
+ // in the same 1080 * 1920 image source as if the rowstride was equal to the width
+ assertArrayEquals(bytesWithEqualRowStride.array(), QrFragment.readImageSource(image).matrix)
+ }
+
+ @Test
+ fun `uses square preview of optimal size`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val textureView: AutoFitTextureView = mock()
+ qrFragment.textureView = textureView
+
+ var optimalSize = chooseOptimalSize(
+ arrayOf(Size(640, 480), Size(1024, 768)),
+ 640,
+ 480,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(16, 9),
+ )
+ qrFragment.adjustPreviewSize(optimalSize)
+ verify(textureView).setAspectRatio(480, 480)
+ assertEquals(480, qrFragment.previewSize?.width)
+ assertEquals(480, qrFragment.previewSize?.height)
+
+ optimalSize = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(640, 480), Size(320, 240)),
+ 2048,
+ 1024,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(16, 9),
+ )
+ qrFragment.adjustPreviewSize(optimalSize)
+ verify(textureView).setAspectRatio(768, 768)
+ assertEquals(768, qrFragment.previewSize?.width)
+ assertEquals(768, qrFragment.previewSize?.height)
+ }
+
+ @Test
+ fun `tryOpenCamera displays error message if no camera is available`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+
+ qrFragment.tryOpenCamera(0, 0)
+ verify(qrFragment.cameraErrorView).visibility = View.VISIBLE
+ verify(qrFragment.customViewFinder).visibility = View.GONE
+ }
+
+ @Test
+ fun `tryOpenCamera opens camera if available and hides the error message is shown`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ doNothing().`when`(qrFragment).openCamera(anyInt(), anyInt())
+
+ qrFragment.tryOpenCamera(0, 0, skipCheck = true)
+
+ verify(qrFragment).openCamera(0, 0)
+ verify(qrFragment.cameraErrorView).visibility = View.GONE
+ verify(qrFragment.customViewFinder).visibility = View.VISIBLE
+ }
+
+ @Test
+ fun `tryOpenCamera displays error message if camera throws exception`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ whenever(qrFragment.setUpCameraOutputs(anyInt(), anyInt())).then { }
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+
+ val cameraManager: CameraManager = mock()
+ whenever(cameraManager.openCamera(anyString(), any<CameraDevice.StateCallback>(), any()))
+ .thenThrow(IllegalStateException("no camera"))
+
+ val activity: FragmentActivity = mock()
+ whenever(activity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+ whenever(qrFragment.activity).thenReturn(activity)
+ qrFragment.cameraId = "mockCamera"
+
+ qrFragment.tryOpenCamera(0, 0, skipCheck = true)
+ verify(qrFragment.cameraErrorView).visibility = View.VISIBLE
+ verify(qrFragment.customViewFinder).visibility = View.GONE
+ }
+
+ @Test
+ fun `tries to decode inverted source on original source decode exception`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val imageCaptor = argumentCaptor<BinaryBitmap>()
+
+ val source = mock<LuminanceSource>()
+ val invertedSource = mock<LuminanceSource>()
+ whenever(source.invert()).thenReturn(invertedSource)
+
+ val bitmap = mock<BinaryBitmap>()
+ val invertedBitmap = mock<BinaryBitmap>()
+
+ qrFragment.multiFormatReader = reader
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ with(qrFragment) {
+ whenever(createBinaryBitmap(source)).thenReturn(bitmap)
+ whenever(createBinaryBitmap(invertedSource)).thenReturn(invertedBitmap)
+ }
+
+ whenever(reader.decodeWithState(bitmap)).thenThrow(NotFoundException::class.java)
+
+ qrFragment.tryScanningSource(source)
+
+ verify(reader, times(2)).decodeWithState(imageCaptor.capture())
+ assertSame(bitmap, imageCaptor.allValues[0])
+ assertSame(invertedBitmap, imageCaptor.allValues[1])
+ }
+
+ @Test
+ fun `tries to decode inverted source when original source returns null`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val imageCaptor = argumentCaptor<BinaryBitmap>()
+
+ val source = mock<LuminanceSource>()
+ val invertedSource = mock<LuminanceSource>()
+ whenever(source.invert()).thenReturn(invertedSource)
+
+ val bitmap = mock<BinaryBitmap>()
+ val invertedBitmap = mock<BinaryBitmap>()
+
+ qrFragment.multiFormatReader = reader
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ with(qrFragment) {
+ whenever(createBinaryBitmap(source)).thenReturn(bitmap)
+ whenever(createBinaryBitmap(invertedSource)).thenReturn(invertedBitmap)
+ }
+
+ whenever(reader.decodeWithState(bitmap)).thenReturn(null)
+
+ qrFragment.tryScanningSource(source)
+
+ verify(reader, times(2)).decodeWithState(imageCaptor.capture())
+ assertSame(bitmap, imageCaptor.allValues[0])
+ assertSame(invertedBitmap, imageCaptor.allValues[1])
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `GIVEN a device rotation of 90 deg WHEN getting the device rotation on a device below SDK 30 THEN the rotation should be 90 deg`() {
+ val mockActivity: FragmentActivity = mock()
+ val mockManager: WindowManager = mock()
+ val mockDisplay: Display = mock()
+
+ val testRotation = Surface.ROTATION_90
+
+ whenever(mockActivity.windowManager).thenReturn(mockManager)
+ whenever(mockManager.defaultDisplay).thenReturn(mockDisplay)
+ whenever(mockDisplay.rotation).thenReturn(testRotation)
+
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ whenever(qrFragment.activity).thenReturn(mockActivity)
+
+ val rotation = qrFragment.getScreenRotation()
+
+ assertEquals(testRotation, rotation)
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `configureTransform uses getScreenRotation method to get rotation`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val textureView: AutoFitTextureView = mock()
+
+ qrFragment.previewSize = Size(4, 4)
+ qrFragment.textureView = textureView
+
+ qrFragment.configureTransform(4, 4)
+
+ verify(qrFragment, times(1)).getScreenRotation()
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `setUpCameraOutputs uses getScreenRotation method to get rotation`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+
+ qrFragment.setUpCameraOutputs(4, 4)
+
+ verify(qrFragment, times(1)).getScreenRotation()
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `getDisplaySize calls defaultDisplay getSize for SDK below 30`() {
+ val mockActivity: FragmentActivity = mock()
+ val mockManager: WindowManager = mock()
+ val mockDisplay: Display = mock()
+
+ whenever(mockActivity.windowManager).thenReturn(mockManager)
+ whenever(mockManager.defaultDisplay).thenReturn(mockDisplay)
+ whenever(mockDisplay.getSize(any())).then { }
+
+ mockManager.getDisplaySize()
+
+ verify(mockDisplay, times(1)).getSize(any())
+ }
+
+ @Test
+ fun `maybeStartBackgroundThread does nothing if the thread already exists`() {
+ val qrFragment = QrFragment()
+ val existingBackgroundThread = HandlerThread("test").apply {
+ start() // need the thread to be "alive"
+ }
+ val existingBackgroundHandler: Handler = mock()
+ qrFragment.backgroundThread = existingBackgroundThread
+ qrFragment.backgroundHandler = existingBackgroundHandler
+
+ qrFragment.maybeStartBackgroundThread()
+
+ assertSame(existingBackgroundThread, qrFragment.backgroundThread)
+ assertSame(existingBackgroundHandler, qrFragment.backgroundHandler)
+ }
+
+ @Test
+ fun `maybeStartBackgroundThread creates and starts a new background thread and handler if doesn't already exist`() {
+ val qrFragment = QrFragment()
+ qrFragment.backgroundThread = null
+ qrFragment.backgroundHandler = null
+
+ qrFragment.maybeStartBackgroundThread()
+
+ assertNotNull(qrFragment.backgroundThread)
+ assertTrue(qrFragment.backgroundThread!!.isAlive)
+ assertNotNull(qrFragment.backgroundHandler)
+ }
+
+ @Test
+ fun `maybeStartExecutorService does nothing if the executor already exists`() {
+ val qrFragment = QrFragment()
+ val existingExecutorService: ExecutorService = mock()
+ qrFragment.backgroundExecutor = existingExecutorService
+
+ qrFragment.maybeStartExecutorService()
+
+ assertSame(existingExecutorService, qrFragment.backgroundExecutor)
+ }
+
+ @Test
+ fun `maybeStartExecutorService creates a new executor service if doesn't exist already`() {
+ val qrFragment = QrFragment()
+ qrFragment.backgroundExecutor = null
+
+ qrFragment.maybeStartExecutorService()
+
+ assertNotNull(null, qrFragment.backgroundExecutor)
+ }
+
+ @Test
+ fun `startScanning opens camera, starts background thread and starts executor service`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ whenever(qrFragment.setUpCameraOutputs(anyInt(), anyInt())).then { }
+ val context: Context = mock()
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(context).checkPermission(eq(permission.CAMERA), anyInt(), anyInt())
+ doReturn(context).`when`(qrFragment).context
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ qrFragment.startScanning()
+ verify(qrFragment, never()).tryOpenCamera(anyInt(), anyInt(), anyBoolean())
+
+ whenever(qrFragment.textureView.isAvailable).thenReturn(true)
+ qrFragment.cameraId = "mockCamera"
+ qrFragment.startScanning()
+ verify(qrFragment, times(2)).maybeStartBackgroundThread()
+ verify(qrFragment, times(2)).maybeStartExecutorService()
+ verify(qrFragment).tryOpenCamera(anyInt(), anyInt(), anyBoolean())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/AutoFitTextureViewTest.kt b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/AutoFitTextureViewTest.kt
new file mode 100644
index 0000000000..32ab0ab398
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/AutoFitTextureViewTest.kt
@@ -0,0 +1,82 @@
+/* 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.qr.views
+
+import android.view.View
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AutoFitTextureViewTest {
+
+ @Test
+ fun `set aspect ratio`() {
+ val view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(16, 9)
+
+ assertEquals(16, view.mRatioWidth)
+ assertEquals(9, view.mRatioHeight)
+ verify(view).requestLayout()
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `width must not be negative when setting aspect ratio`() {
+ val view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(-1, 0)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `height must not be negative when setting aspect ratio`() {
+ val view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(0, -1)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `width and height must not be negative when setting aspect ratio`() {
+ val view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(-1, -1)
+ }
+
+ @Test
+ fun `measure`() {
+ val width = View.MeasureSpec.getSize(640)
+ val height = View.MeasureSpec.getSize(480)
+
+ var view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(0, 0)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(height, view.measuredHeight)
+
+ view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(640, 0)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(height, view.measuredHeight)
+
+ view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(0, 480)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(height, view.measuredHeight)
+
+ view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(16, 9)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(360, view.measuredHeight)
+
+ view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(4, 3)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(height, view.measuredHeight)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/CustomViewFinderTest.kt b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/CustomViewFinderTest.kt
new file mode 100644
index 0000000000..a718d13ffb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/CustomViewFinderTest.kt
@@ -0,0 +1,102 @@
+/* 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.qr.views
+
+import android.graphics.Rect
+import androidx.core.content.ContextCompat
+import androidx.core.text.HtmlCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.qr.R
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+
+@RunWith(AndroidJUnit4::class)
+class CustomViewFinderTest {
+
+ @Test
+ fun `Static Layout is null on view init`() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ assertNull(customViewFinder.scanMessageLayout)
+ }
+
+ @Test
+ fun `calling setupMessage initializes the StaticLayout`() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ val rect = mock(Rect::class.java)
+ customViewFinder.viewFinderRectangle = rect
+
+ CustomViewFinder.setMessage(R.string.mozac_feature_qr_scanner)
+ assertNotNull(CustomViewFinder.scanMessageStringRes)
+ }
+
+ @Test
+ fun `calling setupMessage with null value clears scan message `() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ val rect = mock(Rect::class.java)
+ customViewFinder.viewFinderRectangle = rect
+
+ CustomViewFinder.setMessage(R.string.mozac_feature_qr_scanner)
+ assertNotNull(CustomViewFinder.scanMessageStringRes)
+
+ CustomViewFinder.setMessage(null)
+ assertNull(CustomViewFinder.scanMessageStringRes)
+ }
+
+ @Test
+ fun `message has the correct attributes`() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ val rect = mock(Rect::class.java)
+ customViewFinder.viewFinderRectangle = rect
+ val mockWidth = 200
+ val mockHeight = 300
+ val testScanMessage = HtmlCompat.fromHtml(
+ testContext.getString(R.string.mozac_feature_qr_scanner),
+ HtmlCompat.FROM_HTML_MODE_LEGACY,
+ )
+
+ CustomViewFinder.setMessage(R.string.mozac_feature_qr_scanner)
+ customViewFinder.computeViewFinderRect(mockWidth, mockHeight)
+
+ assertEquals(
+ ContextCompat.getColor(testContext, android.R.color.white),
+ customViewFinder.scanMessageLayout?.paint?.color,
+ )
+
+ assertEquals(
+ mockWidth * CustomViewFinder.DEFAULT_VIEWFINDER_WIDTH_RATIO,
+ customViewFinder.scanMessageLayout?.width?.toFloat(),
+ )
+
+ assertEquals(
+ testScanMessage,
+ customViewFinder.scanMessageLayout?.text,
+ )
+
+ assertEquals(
+ CustomViewFinder.SCAN_MESSAGE_TEXT_SIZE_SP,
+ customViewFinder.scanMessageLayout?.paint?.textSize,
+ )
+ }
+
+ @Test
+ fun `setViewFinderColor sets the proper color to viewfinder`() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ val rect = mock(Rect::class.java)
+ customViewFinder.viewFinderRectangle = rect
+
+ customViewFinder.setViewFinderColor(android.R.color.holo_red_dark)
+
+ assertEquals(
+ android.R.color.holo_red_dark,
+ customViewFinder.viewFinderPaint.color,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/qr/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/qr/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/qr/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/qr/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/readerview/.gitignore b/mobile/android/android-components/components/feature/readerview/.gitignore
new file mode 100644
index 0000000000..2ddf5f27b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/.gitignore
@@ -0,0 +1 @@
+manifest.json
diff --git a/mobile/android/android-components/components/feature/readerview/README.md b/mobile/android/android-components/components/feature/readerview/README.md
new file mode 100644
index 0000000000..c3a4439fdd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/README.md
@@ -0,0 +1,49 @@
+# [Android Components](../../../README.md) > Feature > Reader View
+
+ A component wrapping/providing a Reader View WebExtension.
+
+## Usage
+
+### 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-readerview:{latest-version}"
+```
+
+### Integration
+
+Initializing the feature:
+
+```kotlin
+val readerViewFeature = ReaderViewFeature(
+ context,
+ engine,
+ sessionManager,
+ onReaderViewAvailableChange = {
+ // e.g. readerViewToolbarActionVisible = it
+ }
+)
+
+```
+
+Showing and hiding Reader View:
+
+```kotlin
+readerViewFeature.showReaderView()
+readerViewFeature.hideReaderView()
+```
+
+Showing and hiding the Reader View appearance UI (to adjust font size, font type and color scheme). Note that changes to the appearance settings are automatically persisted as user preferences.
+
+```kotlin
+readerViewFeature.showAppearanceControls()
+readerViewFeature.hideAppearanceControls()
+```
+
+## 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/readerview/build.gradle b/mobile/android/android-components/components/feature/readerview/build.gradle
new file mode 100644
index 0000000000..896753f6d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/build.gradle
@@ -0,0 +1,61 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.readerview'
+}
+
+tasks.register("updateBuiltInExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/readerview')
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-menu')
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+ implementation project(':support-webextensions')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_constraintlayout
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
+
+preBuild.dependsOn updateBuiltInExtensionVersion
diff --git a/mobile/android/android-components/components/feature/readerview/proguard-rules.pro b/mobile/android/android-components/components/feature/readerview/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/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/readerview/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/readerview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1eccdee26a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/manifest.template.json b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/manifest.template.json
new file mode 100644
index 0000000000..616b2036eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/manifest.template.json
@@ -0,0 +1,30 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "readerview@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - ReaderView",
+ "version": "${version}",
+ "content_scripts": [
+ {
+ "matches": ["<all_urls>"],
+ "js": ["readability/readability-readerable-0.4.2.js", "readerview-content.js"],
+ "run_at": "document_idle"
+ }
+ ],
+ "background": {
+ "scripts": ["readerview-background.js"],
+ "persistent": false
+ },
+ "permissions": [
+ "mozillaAddons",
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent",
+ "storage",
+ "tabs",
+ "<all_urls>"
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/JSDOMParser-0.4.2.js b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/JSDOMParser-0.4.2.js
new file mode 100644
index 0000000000..a6a7f24666
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/JSDOMParser-0.4.2.js
@@ -0,0 +1,1196 @@
+/*eslint-env es6:false*/
+/* 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/. */
+
+/**
+ * This is a relatively lightweight DOMParser that is safe to use in a web
+ * worker. This is far from a complete DOM implementation; however, it should
+ * contain the minimal set of functionality necessary for Readability.js.
+ *
+ * Aside from not implementing the full DOM API, there are other quirks to be
+ * aware of when using the JSDOMParser:
+ *
+ * 1) Properly formed HTML/XML must be used. This means you should be extra
+ * careful when using this parser on anything received directly from an
+ * XMLHttpRequest. Providing a serialized string from an XMLSerializer,
+ * however, should be safe (since the browser's XMLSerializer should
+ * generate valid HTML/XML). Therefore, if parsing a document from an XHR,
+ * the recommended approach is to do the XHR in the main thread, use
+ * XMLSerializer.serializeToString() on the responseXML, and pass the
+ * resulting string to the worker.
+ *
+ * 2) Live NodeLists are not supported. DOM methods and properties such as
+ * getElementsByTagName() and childNodes return standard arrays. If you
+ * want these lists to be updated when nodes are removed or added to the
+ * document, you must take care to manually update them yourself.
+ */
+(function (global) {
+
+ // XML only defines these and the numeric ones:
+
+ var entityTable = {
+ "lt": "<",
+ "gt": ">",
+ "amp": "&",
+ "quot": '"',
+ "apos": "'",
+ };
+
+ var reverseEntityTable = {
+ "<": "&lt;",
+ ">": "&gt;",
+ "&": "&amp;",
+ '"': "&quot;",
+ "'": "&apos;",
+ };
+
+ function encodeTextContentHTML(s) {
+ return s.replace(/[&<>]/g, function(x) {
+ return reverseEntityTable[x];
+ });
+ }
+
+ function encodeHTML(s) {
+ return s.replace(/[&<>'"]/g, function(x) {
+ return reverseEntityTable[x];
+ });
+ }
+
+ function decodeHTML(str) {
+ return str.replace(/&(quot|amp|apos|lt|gt);/g, function(match, tag) {
+ return entityTable[tag];
+ }).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, function(match, hex, numStr) {
+ var num = parseInt(hex || numStr, hex ? 16 : 10); // read num
+ return String.fromCharCode(num);
+ });
+ }
+
+ // When a style is set in JS, map it to the corresponding CSS attribute
+ var styleMap = {
+ "alignmentBaseline": "alignment-baseline",
+ "background": "background",
+ "backgroundAttachment": "background-attachment",
+ "backgroundClip": "background-clip",
+ "backgroundColor": "background-color",
+ "backgroundImage": "background-image",
+ "backgroundOrigin": "background-origin",
+ "backgroundPosition": "background-position",
+ "backgroundPositionX": "background-position-x",
+ "backgroundPositionY": "background-position-y",
+ "backgroundRepeat": "background-repeat",
+ "backgroundRepeatX": "background-repeat-x",
+ "backgroundRepeatY": "background-repeat-y",
+ "backgroundSize": "background-size",
+ "baselineShift": "baseline-shift",
+ "border": "border",
+ "borderBottom": "border-bottom",
+ "borderBottomColor": "border-bottom-color",
+ "borderBottomLeftRadius": "border-bottom-left-radius",
+ "borderBottomRightRadius": "border-bottom-right-radius",
+ "borderBottomStyle": "border-bottom-style",
+ "borderBottomWidth": "border-bottom-width",
+ "borderCollapse": "border-collapse",
+ "borderColor": "border-color",
+ "borderImage": "border-image",
+ "borderImageOutset": "border-image-outset",
+ "borderImageRepeat": "border-image-repeat",
+ "borderImageSlice": "border-image-slice",
+ "borderImageSource": "border-image-source",
+ "borderImageWidth": "border-image-width",
+ "borderLeft": "border-left",
+ "borderLeftColor": "border-left-color",
+ "borderLeftStyle": "border-left-style",
+ "borderLeftWidth": "border-left-width",
+ "borderRadius": "border-radius",
+ "borderRight": "border-right",
+ "borderRightColor": "border-right-color",
+ "borderRightStyle": "border-right-style",
+ "borderRightWidth": "border-right-width",
+ "borderSpacing": "border-spacing",
+ "borderStyle": "border-style",
+ "borderTop": "border-top",
+ "borderTopColor": "border-top-color",
+ "borderTopLeftRadius": "border-top-left-radius",
+ "borderTopRightRadius": "border-top-right-radius",
+ "borderTopStyle": "border-top-style",
+ "borderTopWidth": "border-top-width",
+ "borderWidth": "border-width",
+ "bottom": "bottom",
+ "boxShadow": "box-shadow",
+ "boxSizing": "box-sizing",
+ "captionSide": "caption-side",
+ "clear": "clear",
+ "clip": "clip",
+ "clipPath": "clip-path",
+ "clipRule": "clip-rule",
+ "color": "color",
+ "colorInterpolation": "color-interpolation",
+ "colorInterpolationFilters": "color-interpolation-filters",
+ "colorProfile": "color-profile",
+ "colorRendering": "color-rendering",
+ "content": "content",
+ "counterIncrement": "counter-increment",
+ "counterReset": "counter-reset",
+ "cursor": "cursor",
+ "direction": "direction",
+ "display": "display",
+ "dominantBaseline": "dominant-baseline",
+ "emptyCells": "empty-cells",
+ "enableBackground": "enable-background",
+ "fill": "fill",
+ "fillOpacity": "fill-opacity",
+ "fillRule": "fill-rule",
+ "filter": "filter",
+ "cssFloat": "float",
+ "floodColor": "flood-color",
+ "floodOpacity": "flood-opacity",
+ "font": "font",
+ "fontFamily": "font-family",
+ "fontSize": "font-size",
+ "fontStretch": "font-stretch",
+ "fontStyle": "font-style",
+ "fontVariant": "font-variant",
+ "fontWeight": "font-weight",
+ "glyphOrientationHorizontal": "glyph-orientation-horizontal",
+ "glyphOrientationVertical": "glyph-orientation-vertical",
+ "height": "height",
+ "imageRendering": "image-rendering",
+ "kerning": "kerning",
+ "left": "left",
+ "letterSpacing": "letter-spacing",
+ "lightingColor": "lighting-color",
+ "lineHeight": "line-height",
+ "listStyle": "list-style",
+ "listStyleImage": "list-style-image",
+ "listStylePosition": "list-style-position",
+ "listStyleType": "list-style-type",
+ "margin": "margin",
+ "marginBottom": "margin-bottom",
+ "marginLeft": "margin-left",
+ "marginRight": "margin-right",
+ "marginTop": "margin-top",
+ "marker": "marker",
+ "markerEnd": "marker-end",
+ "markerMid": "marker-mid",
+ "markerStart": "marker-start",
+ "mask": "mask",
+ "maxHeight": "max-height",
+ "maxWidth": "max-width",
+ "minHeight": "min-height",
+ "minWidth": "min-width",
+ "opacity": "opacity",
+ "orphans": "orphans",
+ "outline": "outline",
+ "outlineColor": "outline-color",
+ "outlineOffset": "outline-offset",
+ "outlineStyle": "outline-style",
+ "outlineWidth": "outline-width",
+ "overflow": "overflow",
+ "overflowX": "overflow-x",
+ "overflowY": "overflow-y",
+ "padding": "padding",
+ "paddingBottom": "padding-bottom",
+ "paddingLeft": "padding-left",
+ "paddingRight": "padding-right",
+ "paddingTop": "padding-top",
+ "page": "page",
+ "pageBreakAfter": "page-break-after",
+ "pageBreakBefore": "page-break-before",
+ "pageBreakInside": "page-break-inside",
+ "pointerEvents": "pointer-events",
+ "position": "position",
+ "quotes": "quotes",
+ "resize": "resize",
+ "right": "right",
+ "shapeRendering": "shape-rendering",
+ "size": "size",
+ "speak": "speak",
+ "src": "src",
+ "stopColor": "stop-color",
+ "stopOpacity": "stop-opacity",
+ "stroke": "stroke",
+ "strokeDasharray": "stroke-dasharray",
+ "strokeDashoffset": "stroke-dashoffset",
+ "strokeLinecap": "stroke-linecap",
+ "strokeLinejoin": "stroke-linejoin",
+ "strokeMiterlimit": "stroke-miterlimit",
+ "strokeOpacity": "stroke-opacity",
+ "strokeWidth": "stroke-width",
+ "tableLayout": "table-layout",
+ "textAlign": "text-align",
+ "textAnchor": "text-anchor",
+ "textDecoration": "text-decoration",
+ "textIndent": "text-indent",
+ "textLineThrough": "text-line-through",
+ "textLineThroughColor": "text-line-through-color",
+ "textLineThroughMode": "text-line-through-mode",
+ "textLineThroughStyle": "text-line-through-style",
+ "textLineThroughWidth": "text-line-through-width",
+ "textOverflow": "text-overflow",
+ "textOverline": "text-overline",
+ "textOverlineColor": "text-overline-color",
+ "textOverlineMode": "text-overline-mode",
+ "textOverlineStyle": "text-overline-style",
+ "textOverlineWidth": "text-overline-width",
+ "textRendering": "text-rendering",
+ "textShadow": "text-shadow",
+ "textTransform": "text-transform",
+ "textUnderline": "text-underline",
+ "textUnderlineColor": "text-underline-color",
+ "textUnderlineMode": "text-underline-mode",
+ "textUnderlineStyle": "text-underline-style",
+ "textUnderlineWidth": "text-underline-width",
+ "top": "top",
+ "unicodeBidi": "unicode-bidi",
+ "unicodeRange": "unicode-range",
+ "vectorEffect": "vector-effect",
+ "verticalAlign": "vertical-align",
+ "visibility": "visibility",
+ "whiteSpace": "white-space",
+ "widows": "widows",
+ "width": "width",
+ "wordBreak": "word-break",
+ "wordSpacing": "word-spacing",
+ "wordWrap": "word-wrap",
+ "writingMode": "writing-mode",
+ "zIndex": "z-index",
+ "zoom": "zoom",
+ };
+
+ // Elements that can be self-closing
+ var voidElems = {
+ "area": true,
+ "base": true,
+ "br": true,
+ "col": true,
+ "command": true,
+ "embed": true,
+ "hr": true,
+ "img": true,
+ "input": true,
+ "link": true,
+ "meta": true,
+ "param": true,
+ "source": true,
+ "wbr": true
+ };
+
+ var whitespace = [" ", "\t", "\n", "\r"];
+
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
+ var nodeTypes = {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12
+ };
+
+ function getElementsByTagName(tag) {
+ tag = tag.toUpperCase();
+ var elems = [];
+ var allTags = (tag === "*");
+ function getElems(node) {
+ var length = node.children.length;
+ for (var i = 0; i < length; i++) {
+ var child = node.children[i];
+ if (allTags || (child.tagName === tag))
+ elems.push(child);
+ getElems(child);
+ }
+ }
+ getElems(this);
+ elems._isLiveNodeList = true;
+ return elems;
+ }
+
+ var Node = function () {};
+
+ Node.prototype = {
+ attributes: null,
+ childNodes: null,
+ localName: null,
+ nodeName: null,
+ parentNode: null,
+ textContent: null,
+ nextSibling: null,
+ previousSibling: null,
+
+ get firstChild() {
+ return this.childNodes[0] || null;
+ },
+
+ get firstElementChild() {
+ return this.children[0] || null;
+ },
+
+ get lastChild() {
+ return this.childNodes[this.childNodes.length - 1] || null;
+ },
+
+ get lastElementChild() {
+ return this.children[this.children.length - 1] || null;
+ },
+
+ appendChild: function (child) {
+ if (child.parentNode) {
+ child.parentNode.removeChild(child);
+ }
+
+ var last = this.lastChild;
+ if (last)
+ last.nextSibling = child;
+ child.previousSibling = last;
+
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ child.previousElementSibling = this.children[this.children.length - 1] || null;
+ this.children.push(child);
+ child.previousElementSibling && (child.previousElementSibling.nextElementSibling = child);
+ }
+ this.childNodes.push(child);
+ child.parentNode = this;
+ },
+
+ removeChild: function (child) {
+ var childNodes = this.childNodes;
+ var childIndex = childNodes.indexOf(child);
+ if (childIndex === -1) {
+ throw "removeChild: node not found";
+ } else {
+ child.parentNode = null;
+ var prev = child.previousSibling;
+ var next = child.nextSibling;
+ if (prev)
+ prev.nextSibling = next;
+ if (next)
+ next.previousSibling = prev;
+
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ prev = child.previousElementSibling;
+ next = child.nextElementSibling;
+ if (prev)
+ prev.nextElementSibling = next;
+ if (next)
+ next.previousElementSibling = prev;
+ this.children.splice(this.children.indexOf(child), 1);
+ }
+
+ child.previousSibling = child.nextSibling = null;
+ child.previousElementSibling = child.nextElementSibling = null;
+
+ return childNodes.splice(childIndex, 1)[0];
+ }
+ },
+
+ replaceChild: function (newNode, oldNode) {
+ var childNodes = this.childNodes;
+ var childIndex = childNodes.indexOf(oldNode);
+ if (childIndex === -1) {
+ throw "replaceChild: node not found";
+ } else {
+ // This will take care of updating the new node if it was somewhere else before:
+ if (newNode.parentNode)
+ newNode.parentNode.removeChild(newNode);
+
+ childNodes[childIndex] = newNode;
+
+ // update the new node's sibling properties, and its new siblings' sibling properties
+ newNode.nextSibling = oldNode.nextSibling;
+ newNode.previousSibling = oldNode.previousSibling;
+ if (newNode.nextSibling)
+ newNode.nextSibling.previousSibling = newNode;
+ if (newNode.previousSibling)
+ newNode.previousSibling.nextSibling = newNode;
+
+ newNode.parentNode = this;
+
+ // Now deal with elements before we clear out those values for the old node,
+ // because it can help us take shortcuts here:
+ if (newNode.nodeType === Node.ELEMENT_NODE) {
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
+ // Both were elements, which makes this easier, we just swap things out:
+ newNode.previousElementSibling = oldNode.previousElementSibling;
+ newNode.nextElementSibling = oldNode.nextElementSibling;
+ if (newNode.previousElementSibling)
+ newNode.previousElementSibling.nextElementSibling = newNode;
+ if (newNode.nextElementSibling)
+ newNode.nextElementSibling.previousElementSibling = newNode;
+ this.children[this.children.indexOf(oldNode)] = newNode;
+ } else {
+ // Hard way:
+ newNode.previousElementSibling = (function() {
+ for (var i = childIndex - 1; i >= 0; i--) {
+ if (childNodes[i].nodeType === Node.ELEMENT_NODE)
+ return childNodes[i];
+ }
+ return null;
+ })();
+ if (newNode.previousElementSibling) {
+ newNode.nextElementSibling = newNode.previousElementSibling.nextElementSibling;
+ } else {
+ newNode.nextElementSibling = (function() {
+ for (var i = childIndex + 1; i < childNodes.length; i++) {
+ if (childNodes[i].nodeType === Node.ELEMENT_NODE)
+ return childNodes[i];
+ }
+ return null;
+ })();
+ }
+ if (newNode.previousElementSibling)
+ newNode.previousElementSibling.nextElementSibling = newNode;
+ if (newNode.nextElementSibling)
+ newNode.nextElementSibling.previousElementSibling = newNode;
+
+ if (newNode.nextElementSibling)
+ this.children.splice(this.children.indexOf(newNode.nextElementSibling), 0, newNode);
+ else
+ this.children.push(newNode);
+ }
+ } else if (oldNode.nodeType === Node.ELEMENT_NODE) {
+ // new node is not an element node.
+ // if the old one was, update its element siblings:
+ if (oldNode.previousElementSibling)
+ oldNode.previousElementSibling.nextElementSibling = oldNode.nextElementSibling;
+ if (oldNode.nextElementSibling)
+ oldNode.nextElementSibling.previousElementSibling = oldNode.previousElementSibling;
+ this.children.splice(this.children.indexOf(oldNode), 1);
+
+ // If the old node wasn't an element, neither the new nor the old node was an element,
+ // and the children array and its members shouldn't need any updating.
+ }
+
+
+ oldNode.parentNode = null;
+ oldNode.previousSibling = null;
+ oldNode.nextSibling = null;
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
+ oldNode.previousElementSibling = null;
+ oldNode.nextElementSibling = null;
+ }
+ return oldNode;
+ }
+ },
+
+ __JSDOMParser__: true,
+ };
+
+ for (var nodeType in nodeTypes) {
+ Node[nodeType] = Node.prototype[nodeType] = nodeTypes[nodeType];
+ }
+
+ var Attribute = function (name, value) {
+ this.name = name;
+ this._value = value;
+ };
+
+ Attribute.prototype = {
+ get value() {
+ return this._value;
+ },
+ setValue: function(newValue) {
+ this._value = newValue;
+ },
+ getEncodedValue: function() {
+ return encodeHTML(this._value);
+ },
+ };
+
+ var Comment = function () {
+ this.childNodes = [];
+ };
+
+ Comment.prototype = {
+ __proto__: Node.prototype,
+
+ nodeName: "#comment",
+ nodeType: Node.COMMENT_NODE
+ };
+
+ var Text = function () {
+ this.childNodes = [];
+ };
+
+ Text.prototype = {
+ __proto__: Node.prototype,
+
+ nodeName: "#text",
+ nodeType: Node.TEXT_NODE,
+ get textContent() {
+ if (typeof this._textContent === "undefined") {
+ this._textContent = decodeHTML(this._innerHTML || "");
+ }
+ return this._textContent;
+ },
+ get innerHTML() {
+ if (typeof this._innerHTML === "undefined") {
+ this._innerHTML = encodeTextContentHTML(this._textContent || "");
+ }
+ return this._innerHTML;
+ },
+
+ set innerHTML(newHTML) {
+ this._innerHTML = newHTML;
+ delete this._textContent;
+ },
+ set textContent(newText) {
+ this._textContent = newText;
+ delete this._innerHTML;
+ },
+ };
+
+ var Document = function (url) {
+ this.documentURI = url;
+ this.styleSheets = [];
+ this.childNodes = [];
+ this.children = [];
+ };
+
+ Document.prototype = {
+ __proto__: Node.prototype,
+
+ nodeName: "#document",
+ nodeType: Node.DOCUMENT_NODE,
+ title: "",
+
+ getElementsByTagName: getElementsByTagName,
+
+ getElementById: function (id) {
+ function getElem(node) {
+ var length = node.children.length;
+ if (node.id === id)
+ return node;
+ for (var i = 0; i < length; i++) {
+ var el = getElem(node.children[i]);
+ if (el)
+ return el;
+ }
+ return null;
+ }
+ return getElem(this);
+ },
+
+ createElement: function (tag) {
+ var node = new Element(tag);
+ return node;
+ },
+
+ createTextNode: function (text) {
+ var node = new Text();
+ node.textContent = text;
+ return node;
+ },
+
+ get baseURI() {
+ if (!this.hasOwnProperty("_baseURI")) {
+ this._baseURI = this.documentURI;
+ var baseElements = this.getElementsByTagName("base");
+ var href = baseElements[0] && baseElements[0].getAttribute("href");
+ if (href) {
+ try {
+ this._baseURI = (new URL(href, this._baseURI)).href;
+ } catch (ex) {/* Just fall back to documentURI */}
+ }
+ }
+ return this._baseURI;
+ },
+ };
+
+ var Element = function (tag) {
+ // We use this to find the closing tag.
+ this._matchingTag = tag;
+ // We're explicitly a non-namespace aware parser, we just pretend it's all HTML.
+ var lastColonIndex = tag.lastIndexOf(":");
+ if (lastColonIndex != -1) {
+ tag = tag.substring(lastColonIndex + 1);
+ }
+ this.attributes = [];
+ this.childNodes = [];
+ this.children = [];
+ this.nextElementSibling = this.previousElementSibling = null;
+ this.localName = tag.toLowerCase();
+ this.tagName = tag.toUpperCase();
+ this.style = new Style(this);
+ };
+
+ Element.prototype = {
+ __proto__: Node.prototype,
+
+ nodeType: Node.ELEMENT_NODE,
+
+ getElementsByTagName: getElementsByTagName,
+
+ get className() {
+ return this.getAttribute("class") || "";
+ },
+
+ set className(str) {
+ this.setAttribute("class", str);
+ },
+
+ get id() {
+ return this.getAttribute("id") || "";
+ },
+
+ set id(str) {
+ this.setAttribute("id", str);
+ },
+
+ get href() {
+ return this.getAttribute("href") || "";
+ },
+
+ set href(str) {
+ this.setAttribute("href", str);
+ },
+
+ get src() {
+ return this.getAttribute("src") || "";
+ },
+
+ set src(str) {
+ this.setAttribute("src", str);
+ },
+
+ get srcset() {
+ return this.getAttribute("srcset") || "";
+ },
+
+ set srcset(str) {
+ this.setAttribute("srcset", str);
+ },
+
+ get nodeName() {
+ return this.tagName;
+ },
+
+ get innerHTML() {
+ function getHTML(node) {
+ var i = 0;
+ for (i = 0; i < node.childNodes.length; i++) {
+ var child = node.childNodes[i];
+ if (child.localName) {
+ arr.push("<" + child.localName);
+
+ // serialize attribute list
+ for (var j = 0; j < child.attributes.length; j++) {
+ var attr = child.attributes[j];
+ // the attribute value will be HTML escaped.
+ var val = attr.getEncodedValue();
+ var quote = (val.indexOf('"') === -1 ? '"' : "'");
+ arr.push(" " + attr.name + "=" + quote + val + quote);
+ }
+
+ if (child.localName in voidElems && !child.childNodes.length) {
+ // if this is a self-closing element, end it here
+ arr.push("/>");
+ } else {
+ // otherwise, add its children
+ arr.push(">");
+ getHTML(child);
+ arr.push("</" + child.localName + ">");
+ }
+ } else {
+ // This is a text node, so asking for innerHTML won't recurse.
+ arr.push(child.innerHTML);
+ }
+ }
+ }
+
+ // Using Array.join() avoids the overhead from lazy string concatenation.
+ var arr = [];
+ getHTML(this);
+ return arr.join("");
+ },
+
+ set innerHTML(html) {
+ var parser = new JSDOMParser();
+ var node = parser.parse(html);
+ var i;
+ for (i = this.childNodes.length; --i >= 0;) {
+ this.childNodes[i].parentNode = null;
+ }
+ this.childNodes = node.childNodes;
+ this.children = node.children;
+ for (i = this.childNodes.length; --i >= 0;) {
+ this.childNodes[i].parentNode = this;
+ }
+ },
+
+ set textContent(text) {
+ // clear parentNodes for existing children
+ for (var i = this.childNodes.length; --i >= 0;) {
+ this.childNodes[i].parentNode = null;
+ }
+
+ var node = new Text();
+ this.childNodes = [ node ];
+ this.children = [];
+ node.textContent = text;
+ node.parentNode = this;
+ },
+
+ get textContent() {
+ function getText(node) {
+ var nodes = node.childNodes;
+ for (var i = 0; i < nodes.length; i++) {
+ var child = nodes[i];
+ if (child.nodeType === 3) {
+ text.push(child.textContent);
+ } else {
+ getText(child);
+ }
+ }
+ }
+
+ // Using Array.join() avoids the overhead from lazy string concatenation.
+ // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes
+ var text = [];
+ getText(this);
+ return text.join("");
+ },
+
+ getAttribute: function (name) {
+ for (var i = this.attributes.length; --i >= 0;) {
+ var attr = this.attributes[i];
+ if (attr.name === name) {
+ return attr.value;
+ }
+ }
+ return undefined;
+ },
+
+ setAttribute: function (name, value) {
+ for (var i = this.attributes.length; --i >= 0;) {
+ var attr = this.attributes[i];
+ if (attr.name === name) {
+ attr.setValue(value);
+ return;
+ }
+ }
+ this.attributes.push(new Attribute(name, value));
+ },
+
+ removeAttribute: function (name) {
+ for (var i = this.attributes.length; --i >= 0;) {
+ var attr = this.attributes[i];
+ if (attr.name === name) {
+ this.attributes.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ hasAttribute: function (name) {
+ return this.attributes.some(function (attr) {
+ return attr.name == name;
+ });
+ },
+ };
+
+ var Style = function (node) {
+ this.node = node;
+ };
+
+ // getStyle() and setStyle() use the style attribute string directly. This
+ // won't be very efficient if there are a lot of style manipulations, but
+ // it's the easiest way to make sure the style attribute string and the JS
+ // style property stay in sync. Readability.js doesn't do many style
+ // manipulations, so this should be okay.
+ Style.prototype = {
+ getStyle: function (styleName) {
+ var attr = this.node.getAttribute("style");
+ if (!attr)
+ return undefined;
+
+ var styles = attr.split(";");
+ for (var i = 0; i < styles.length; i++) {
+ var style = styles[i].split(":");
+ var name = style[0].trim();
+ if (name === styleName)
+ return style[1].trim();
+ }
+
+ return undefined;
+ },
+
+ setStyle: function (styleName, styleValue) {
+ var value = this.node.getAttribute("style") || "";
+ var index = 0;
+ do {
+ var next = value.indexOf(";", index) + 1;
+ var length = next - index - 1;
+ var style = (length > 0 ? value.substr(index, length) : value.substr(index));
+ if (style.substr(0, style.indexOf(":")).trim() === styleName) {
+ value = value.substr(0, index).trim() + (next ? " " + value.substr(next).trim() : "");
+ break;
+ }
+ index = next;
+ } while (index);
+
+ value += " " + styleName + ": " + styleValue + ";";
+ this.node.setAttribute("style", value.trim());
+ }
+ };
+
+ // For each item in styleMap, define a getter and setter on the style
+ // property.
+ for (var jsName in styleMap) {
+ (function (cssName) {
+ Style.prototype.__defineGetter__(jsName, function () {
+ return this.getStyle(cssName);
+ });
+ Style.prototype.__defineSetter__(jsName, function (value) {
+ this.setStyle(cssName, value);
+ });
+ })(styleMap[jsName]);
+ }
+
+ var JSDOMParser = function () {
+ this.currentChar = 0;
+
+ // In makeElementNode() we build up many strings one char at a time. Using
+ // += for this results in lots of short-lived intermediate strings. It's
+ // better to build an array of single-char strings and then join() them
+ // together at the end. And reusing a single array (i.e. |this.strBuf|)
+ // over and over for this purpose uses less memory than using a new array
+ // for each string.
+ this.strBuf = [];
+
+ // Similarly, we reuse this array to return the two arguments from
+ // makeElementNode(), which saves us from having to allocate a new array
+ // every time.
+ this.retPair = [];
+
+ this.errorState = "";
+ };
+
+ JSDOMParser.prototype = {
+ error: function(m) {
+ if (typeof dump !== "undefined") {
+ dump("JSDOMParser error: " + m + "\n");
+ } else if (typeof console !== "undefined") {
+ console.log("JSDOMParser error: " + m + "\n");
+ }
+ this.errorState += m + "\n";
+ },
+
+ /**
+ * Look at the next character without advancing the index.
+ */
+ peekNext: function () {
+ return this.html[this.currentChar];
+ },
+
+ /**
+ * Get the next character and advance the index.
+ */
+ nextChar: function () {
+ return this.html[this.currentChar++];
+ },
+
+ /**
+ * Called after a quote character is read. This finds the next quote
+ * character and returns the text string in between.
+ */
+ readString: function (quote) {
+ var str;
+ var n = this.html.indexOf(quote, this.currentChar);
+ if (n === -1) {
+ this.currentChar = this.html.length;
+ str = null;
+ } else {
+ str = this.html.substring(this.currentChar, n);
+ this.currentChar = n + 1;
+ }
+
+ return str;
+ },
+
+ /**
+ * Called when parsing a node. This finds the next name/value attribute
+ * pair and adds the result to the attributes list.
+ */
+ readAttribute: function (node) {
+ var name = "";
+
+ var n = this.html.indexOf("=", this.currentChar);
+ if (n === -1) {
+ this.currentChar = this.html.length;
+ } else {
+ // Read until a '=' character is hit; this will be the attribute key
+ name = this.html.substring(this.currentChar, n);
+ this.currentChar = n + 1;
+ }
+
+ if (!name)
+ return;
+
+ // After a '=', we should see a '"' for the attribute value
+ var c = this.nextChar();
+ if (c !== '"' && c !== "'") {
+ this.error("Error reading attribute " + name + ", expecting '\"'");
+ return;
+ }
+
+ // Read the attribute value (and consume the matching quote)
+ var value = this.readString(c);
+
+ node.attributes.push(new Attribute(name, decodeHTML(value)));
+
+ return;
+ },
+
+ /**
+ * Parses and returns an Element node. This is called after a '<' has been
+ * read.
+ *
+ * @returns an array; the first index of the array is the parsed node;
+ * the second index is a boolean indicating whether this is a void
+ * Element
+ */
+ makeElementNode: function (retPair) {
+ var c = this.nextChar();
+
+ // Read the Element tag name
+ var strBuf = this.strBuf;
+ strBuf.length = 0;
+ while (whitespace.indexOf(c) == -1 && c !== ">" && c !== "/") {
+ if (c === undefined)
+ return false;
+ strBuf.push(c);
+ c = this.nextChar();
+ }
+ var tag = strBuf.join("");
+
+ if (!tag)
+ return false;
+
+ var node = new Element(tag);
+
+ // Read Element attributes
+ while (c !== "/" && c !== ">") {
+ if (c === undefined)
+ return false;
+ while (whitespace.indexOf(this.html[this.currentChar++]) != -1) {
+ // Advance cursor to first non-whitespace char.
+ }
+ this.currentChar--;
+ c = this.nextChar();
+ if (c !== "/" && c !== ">") {
+ --this.currentChar;
+ this.readAttribute(node);
+ }
+ }
+
+ // If this is a self-closing tag, read '/>'
+ var closed = false;
+ if (c === "/") {
+ closed = true;
+ c = this.nextChar();
+ if (c !== ">") {
+ this.error("expected '>' to close " + tag);
+ return false;
+ }
+ }
+
+ retPair[0] = node;
+ retPair[1] = closed;
+ return true;
+ },
+
+ /**
+ * If the current input matches this string, advance the input index;
+ * otherwise, do nothing.
+ *
+ * @returns whether input matched string
+ */
+ match: function (str) {
+ var strlen = str.length;
+ if (this.html.substr(this.currentChar, strlen).toLowerCase() === str.toLowerCase()) {
+ this.currentChar += strlen;
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Searches the input until a string is found and discards all input up to
+ * and including the matched string.
+ */
+ discardTo: function (str) {
+ var index = this.html.indexOf(str, this.currentChar) + str.length;
+ if (index === -1)
+ this.currentChar = this.html.length;
+ this.currentChar = index;
+ },
+
+ /**
+ * Reads child nodes for the given node.
+ */
+ readChildren: function (node) {
+ var child;
+ while ((child = this.readNode())) {
+ // Don't keep Comment nodes
+ if (child.nodeType !== 8) {
+ node.appendChild(child);
+ }
+ }
+ },
+
+ discardNextComment: function() {
+ if (this.match("--")) {
+ this.discardTo("-->");
+ } else {
+ var c = this.nextChar();
+ while (c !== ">") {
+ if (c === undefined)
+ return null;
+ if (c === '"' || c === "'")
+ this.readString(c);
+ c = this.nextChar();
+ }
+ }
+ return new Comment();
+ },
+
+
+ /**
+ * Reads the next child node from the input. If we're reading a closing
+ * tag, or if we've reached the end of input, return null.
+ *
+ * @returns the node
+ */
+ readNode: function () {
+ var c = this.nextChar();
+
+ if (c === undefined)
+ return null;
+
+ // Read any text as Text node
+ var textNode;
+ if (c !== "<") {
+ --this.currentChar;
+ textNode = new Text();
+ var n = this.html.indexOf("<", this.currentChar);
+ if (n === -1) {
+ textNode.innerHTML = this.html.substring(this.currentChar, this.html.length);
+ this.currentChar = this.html.length;
+ } else {
+ textNode.innerHTML = this.html.substring(this.currentChar, n);
+ this.currentChar = n;
+ }
+ return textNode;
+ }
+
+ if (this.match("![CDATA[")) {
+ var endChar = this.html.indexOf("]]>", this.currentChar);
+ if (endChar === -1) {
+ this.error("unclosed CDATA section");
+ return null;
+ }
+ textNode = new Text();
+ textNode.textContent = this.html.substring(this.currentChar, endChar);
+ this.currentChar = endChar + ("]]>").length;
+ return textNode;
+ }
+
+ c = this.peekNext();
+
+ // Read Comment node. Normally, Comment nodes know their inner
+ // textContent, but we don't really care about Comment nodes (we throw
+ // them away in readChildren()). So just returning an empty Comment node
+ // here is sufficient.
+ if (c === "!" || c === "?") {
+ // We're still before the ! or ? that is starting this comment:
+ this.currentChar++;
+ return this.discardNextComment();
+ }
+
+ // If we're reading a closing tag, return null. This means we've reached
+ // the end of this set of child nodes.
+ if (c === "/") {
+ --this.currentChar;
+ return null;
+ }
+
+ // Otherwise, we're looking at an Element node
+ var result = this.makeElementNode(this.retPair);
+ if (!result)
+ return null;
+
+ var node = this.retPair[0];
+ var closed = this.retPair[1];
+ var localName = node.localName;
+
+ // If this isn't a void Element, read its child nodes
+ if (!closed) {
+ this.readChildren(node);
+ var closingTag = "</" + node._matchingTag + ">";
+ if (!this.match(closingTag)) {
+ this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length));
+ return null;
+ }
+ }
+
+ // Only use the first title, because SVG might have other
+ // title elements which we don't care about (medium.com
+ // does this, at least).
+ if (localName === "title" && !this.doc.title) {
+ this.doc.title = node.textContent.trim();
+ } else if (localName === "head") {
+ this.doc.head = node;
+ } else if (localName === "body") {
+ this.doc.body = node;
+ } else if (localName === "html") {
+ this.doc.documentElement = node;
+ }
+
+ return node;
+ },
+
+ /**
+ * Parses an HTML string and returns a JS implementation of the Document.
+ */
+ parse: function (html, url) {
+ this.html = html;
+ var doc = this.doc = new Document(url);
+ this.readChildren(doc);
+
+ // If this is an HTML document, remove root-level children except for the
+ // <html> node
+ if (doc.documentElement) {
+ for (var i = doc.childNodes.length; --i >= 0;) {
+ var child = doc.childNodes[i];
+ if (child !== doc.documentElement) {
+ doc.removeChild(child);
+ }
+ }
+ }
+
+ return doc;
+ }
+ };
+
+ // Attach the standard DOM types to the global scope
+ global.Node = Node;
+ global.Comment = Comment;
+ global.Document = Document;
+ global.Element = Element;
+ global.Text = Text;
+
+ // Attach JSDOMParser to the global scope
+ global.JSDOMParser = JSDOMParser;
+
+})(this);
+
+if (typeof module === "object") {
+ module.exports = this.JSDOMParser;
+} \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/readability-0.4.2.js b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/readability-0.4.2.js
new file mode 100644
index 0000000000..ce06df459d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/readability-0.4.2.js
@@ -0,0 +1,2283 @@
+/*eslint-env es6:false*/
+/*
+ * Copyright (c) 2010 Arc90 Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This code is heavily based on Arc90's readability.js (1.7.1) script
+ * available at: http://code.google.com/p/arc90labs-readability
+ */
+
+/**
+ * Public constructor.
+ * @param {HTMLDocument} doc The document to parse.
+ * @param {Object} options The options object.
+ */
+function Readability(doc, options) {
+ // In some older versions, people passed a URI as the first argument. Cope:
+ if (options && options.documentElement) {
+ doc = options;
+ options = arguments[2];
+ } else if (!doc || !doc.documentElement) {
+ throw new Error("First argument to Readability constructor should be a document object.");
+ }
+ options = options || {};
+
+ this._doc = doc;
+ this._docJSDOMParser = this._doc.firstChild.__JSDOMParser__;
+ this._articleTitle = null;
+ this._articleByline = null;
+ this._articleDir = null;
+ this._articleSiteName = null;
+ this._attempts = [];
+
+ // Configurable options
+ this._debug = !!options.debug;
+ this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE;
+ this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES;
+ this._charThreshold = options.charThreshold || this.DEFAULT_CHAR_THRESHOLD;
+ this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []);
+ this._keepClasses = !!options.keepClasses;
+ this._serializer = options.serializer || function(el) {
+ return el.innerHTML;
+ };
+ this._disableJSONLD = !!options.disableJSONLD;
+
+ // Start with all flags set
+ this._flags = this.FLAG_STRIP_UNLIKELYS |
+ this.FLAG_WEIGHT_CLASSES |
+ this.FLAG_CLEAN_CONDITIONALLY;
+
+
+ // Control whether log messages are sent to the console
+ if (this._debug) {
+ let logNode = function(node) {
+ if (node.nodeType == node.TEXT_NODE) {
+ return `${node.nodeName} ("${node.textContent}")`;
+ }
+ let attrPairs = Array.from(node.attributes || [], function(attr) {
+ return `${attr.name}="${attr.value}"`;
+ }).join(" ");
+ return `<${node.localName} ${attrPairs}>`;
+ };
+ this.log = function () {
+ if (typeof dump !== "undefined") {
+ var msg = Array.prototype.map.call(arguments, function(x) {
+ return (x && x.nodeName) ? logNode(x) : x;
+ }).join(" ");
+ dump("Reader: (Readability) " + msg + "\n");
+ } else if (typeof console !== "undefined") {
+ let args = Array.from(arguments, arg => {
+ if (arg && arg.nodeType == this.ELEMENT_NODE) {
+ return logNode(arg);
+ }
+ return arg;
+ });
+ args.unshift("Reader: (Readability)");
+ console.log.apply(console, args);
+ }
+ };
+ } else {
+ this.log = function () {};
+ }
+}
+
+Readability.prototype = {
+ FLAG_STRIP_UNLIKELYS: 0x1,
+ FLAG_WEIGHT_CLASSES: 0x2,
+ FLAG_CLEAN_CONDITIONALLY: 0x4,
+
+ // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
+ ELEMENT_NODE: 1,
+ TEXT_NODE: 3,
+
+ // Max number of nodes supported by this parser. Default: 0 (no limit)
+ DEFAULT_MAX_ELEMS_TO_PARSE: 0,
+
+ // The number of top candidates to consider when analysing how
+ // tight the competition is among candidates.
+ DEFAULT_N_TOP_CANDIDATES: 5,
+
+ // Element tags to score by default.
+ DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
+
+ // The default number of chars an article must have in order to return a result
+ DEFAULT_CHAR_THRESHOLD: 500,
+
+ // All of the regular expressions in use within readability.
+ // Defined up here so we don't instantiate them repeatedly in loops.
+ REGEXPS: {
+ // NOTE: These two regular expressions are duplicated in
+ // Readability-readerable.js. Please keep both copies in sync.
+ unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
+ okMaybeItsACandidate: /and|article|body|column|content|main|shadow/i,
+
+ positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
+ negative: /-ad-|hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
+ extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
+ byline: /byline|author|dateline|writtenby|p-author/i,
+ replaceFonts: /<(\/?)font[^>]*>/gi,
+ normalize: /\s{2,}/g,
+ videos: /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,
+ shareElements: /(\b|_)(share|sharedaddy)(\b|_)/i,
+ nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
+ prevLink: /(prev|earl|old|new|<|«)/i,
+ tokenize: /\W+/g,
+ whitespace: /^\s*$/,
+ hasContent: /\S$/,
+ hashUrl: /^#.+/,
+ srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,
+ b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,
+ // See: https://schema.org/Article
+ jsonLdArticleTypes: /^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/
+ },
+
+ UNLIKELY_ROLES: [ "menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog" ],
+
+ DIV_TO_P_ELEMS: new Set([ "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL" ]),
+
+ ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"],
+
+ PRESENTATIONAL_ATTRIBUTES: [ "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "hspace", "rules", "style", "valign", "vspace" ],
+
+ DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ "TABLE", "TH", "TD", "HR", "PRE" ],
+
+ // The commented out elements qualify as phrasing content but tend to be
+ // removed by readability when put into paragraphs, so we ignore them here.
+ PHRASING_ELEMS: [
+ // "CANVAS", "IFRAME", "SVG", "VIDEO",
+ "ABBR", "AUDIO", "B", "BDO", "BR", "BUTTON", "CITE", "CODE", "DATA",
+ "DATALIST", "DFN", "EM", "EMBED", "I", "IMG", "INPUT", "KBD", "LABEL",
+ "MARK", "MATH", "METER", "NOSCRIPT", "OBJECT", "OUTPUT", "PROGRESS", "Q",
+ "RUBY", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "SUB",
+ "SUP", "TEXTAREA", "TIME", "VAR", "WBR"
+ ],
+
+ // These are the classes that readability sets itself.
+ CLASSES_TO_PRESERVE: [ "page" ],
+
+ // These are the list of HTML entities that need to be escaped.
+ HTML_ESCAPE_MAP: {
+ "lt": "<",
+ "gt": ">",
+ "amp": "&",
+ "quot": '"',
+ "apos": "'",
+ },
+
+ /**
+ * Run any post-process modifications to article content as necessary.
+ *
+ * @param Element
+ * @return void
+ **/
+ _postProcessContent: function(articleContent) {
+ // Readability cannot open relative uris so we convert them to absolute uris.
+ this._fixRelativeUris(articleContent);
+
+ this._simplifyNestedElements(articleContent);
+
+ if (!this._keepClasses) {
+ // Remove classes.
+ this._cleanClasses(articleContent);
+ }
+ },
+
+ /**
+ * Iterates over a NodeList, calls `filterFn` for each node and removes node
+ * if function returned `true`.
+ *
+ * If function is not passed, removes all the nodes in node list.
+ *
+ * @param NodeList nodeList The nodes to operate on
+ * @param Function filterFn the function to use as a filter
+ * @return void
+ */
+ _removeNodes: function(nodeList, filterFn) {
+ // Avoid ever operating on live node lists.
+ if (this._docJSDOMParser && nodeList._isLiveNodeList) {
+ throw new Error("Do not pass live node lists to _removeNodes");
+ }
+ for (var i = nodeList.length - 1; i >= 0; i--) {
+ var node = nodeList[i];
+ var parentNode = node.parentNode;
+ if (parentNode) {
+ if (!filterFn || filterFn.call(this, node, i, nodeList)) {
+ parentNode.removeChild(node);
+ }
+ }
+ }
+ },
+
+ /**
+ * Iterates over a NodeList, and calls _setNodeTag for each node.
+ *
+ * @param NodeList nodeList The nodes to operate on
+ * @param String newTagName the new tag name to use
+ * @return void
+ */
+ _replaceNodeTags: function(nodeList, newTagName) {
+ // Avoid ever operating on live node lists.
+ if (this._docJSDOMParser && nodeList._isLiveNodeList) {
+ throw new Error("Do not pass live node lists to _replaceNodeTags");
+ }
+ for (const node of nodeList) {
+ this._setNodeTag(node, newTagName);
+ }
+ },
+
+ /**
+ * Iterate over a NodeList, which doesn't natively fully implement the Array
+ * interface.
+ *
+ * For convenience, the current object context is applied to the provided
+ * iterate function.
+ *
+ * @param NodeList nodeList The NodeList.
+ * @param Function fn The iterate function.
+ * @return void
+ */
+ _forEachNode: function(nodeList, fn) {
+ Array.prototype.forEach.call(nodeList, fn, this);
+ },
+
+ /**
+ * Iterate over a NodeList, and return the first node that passes
+ * the supplied test function
+ *
+ * For convenience, the current object context is applied to the provided
+ * test function.
+ *
+ * @param NodeList nodeList The NodeList.
+ * @param Function fn The test function.
+ * @return void
+ */
+ _findNode: function(nodeList, fn) {
+ return Array.prototype.find.call(nodeList, fn, this);
+ },
+
+ /**
+ * Iterate over a NodeList, return true if any of the provided iterate
+ * function calls returns true, false otherwise.
+ *
+ * For convenience, the current object context is applied to the
+ * provided iterate function.
+ *
+ * @param NodeList nodeList The NodeList.
+ * @param Function fn The iterate function.
+ * @return Boolean
+ */
+ _someNode: function(nodeList, fn) {
+ return Array.prototype.some.call(nodeList, fn, this);
+ },
+
+ /**
+ * Iterate over a NodeList, return true if all of the provided iterate
+ * function calls return true, false otherwise.
+ *
+ * For convenience, the current object context is applied to the
+ * provided iterate function.
+ *
+ * @param NodeList nodeList The NodeList.
+ * @param Function fn The iterate function.
+ * @return Boolean
+ */
+ _everyNode: function(nodeList, fn) {
+ return Array.prototype.every.call(nodeList, fn, this);
+ },
+
+ /**
+ * Concat all nodelists passed as arguments.
+ *
+ * @return ...NodeList
+ * @return Array
+ */
+ _concatNodeLists: function() {
+ var slice = Array.prototype.slice;
+ var args = slice.call(arguments);
+ var nodeLists = args.map(function(list) {
+ return slice.call(list);
+ });
+ return Array.prototype.concat.apply([], nodeLists);
+ },
+
+ _getAllNodesWithTag: function(node, tagNames) {
+ if (node.querySelectorAll) {
+ return node.querySelectorAll(tagNames.join(","));
+ }
+ return [].concat.apply([], tagNames.map(function(tag) {
+ var collection = node.getElementsByTagName(tag);
+ return Array.isArray(collection) ? collection : Array.from(collection);
+ }));
+ },
+
+ /**
+ * Removes the class="" attribute from every element in the given
+ * subtree, except those that match CLASSES_TO_PRESERVE and
+ * the classesToPreserve array from the options object.
+ *
+ * @param Element
+ * @return void
+ */
+ _cleanClasses: function(node) {
+ var classesToPreserve = this._classesToPreserve;
+ var className = (node.getAttribute("class") || "")
+ .split(/\s+/)
+ .filter(function(cls) {
+ return classesToPreserve.indexOf(cls) != -1;
+ })
+ .join(" ");
+
+ if (className) {
+ node.setAttribute("class", className);
+ } else {
+ node.removeAttribute("class");
+ }
+
+ for (node = node.firstElementChild; node; node = node.nextElementSibling) {
+ this._cleanClasses(node);
+ }
+ },
+
+ /**
+ * Converts each <a> and <img> uri in the given element to an absolute URI,
+ * ignoring #ref URIs.
+ *
+ * @param Element
+ * @return void
+ */
+ _fixRelativeUris: function(articleContent) {
+ var baseURI = this._doc.baseURI;
+ var documentURI = this._doc.documentURI;
+ function toAbsoluteURI(uri) {
+ // Leave hash links alone if the base URI matches the document URI:
+ if (baseURI == documentURI && uri.charAt(0) == "#") {
+ return uri;
+ }
+
+ // Otherwise, resolve against base URI:
+ try {
+ return new URL(uri, baseURI).href;
+ } catch (ex) {
+ // Something went wrong, just return the original:
+ }
+ return uri;
+ }
+
+ var links = this._getAllNodesWithTag(articleContent, ["a"]);
+ this._forEachNode(links, function(link) {
+ var href = link.getAttribute("href");
+ if (href) {
+ // Remove links with javascript: URIs, since
+ // they won't work after scripts have been removed from the page.
+ if (href.indexOf("javascript:") === 0) {
+ // if the link only contains simple text content, it can be converted to a text node
+ if (link.childNodes.length === 1 && link.childNodes[0].nodeType === this.TEXT_NODE) {
+ var text = this._doc.createTextNode(link.textContent);
+ link.parentNode.replaceChild(text, link);
+ } else {
+ // if the link has multiple children, they should all be preserved
+ var container = this._doc.createElement("span");
+ while (link.firstChild) {
+ container.appendChild(link.firstChild);
+ }
+ link.parentNode.replaceChild(container, link);
+ }
+ } else {
+ link.setAttribute("href", toAbsoluteURI(href));
+ }
+ }
+ });
+
+ var medias = this._getAllNodesWithTag(articleContent, [
+ "img", "picture", "figure", "video", "audio", "source"
+ ]);
+
+ this._forEachNode(medias, function(media) {
+ var src = media.getAttribute("src");
+ var poster = media.getAttribute("poster");
+ var srcset = media.getAttribute("srcset");
+
+ if (src) {
+ media.setAttribute("src", toAbsoluteURI(src));
+ }
+
+ if (poster) {
+ media.setAttribute("poster", toAbsoluteURI(poster));
+ }
+
+ if (srcset) {
+ var newSrcset = srcset.replace(this.REGEXPS.srcsetUrl, function(_, p1, p2, p3) {
+ return toAbsoluteURI(p1) + (p2 || "") + p3;
+ });
+
+ media.setAttribute("srcset", newSrcset);
+ }
+ });
+ },
+
+ _simplifyNestedElements: function(articleContent) {
+ var node = articleContent;
+
+ while (node) {
+ if (node.parentNode && ["DIV", "SECTION"].includes(node.tagName) && !(node.id && node.id.startsWith("readability"))) {
+ if (this._isElementWithoutContent(node)) {
+ node = this._removeAndGetNext(node);
+ continue;
+ } else if (this._hasSingleTagInsideElement(node, "DIV") || this._hasSingleTagInsideElement(node, "SECTION")) {
+ var child = node.children[0];
+ for (var i = 0; i < node.attributes.length; i++) {
+ child.setAttribute(node.attributes[i].name, node.attributes[i].value);
+ }
+ node.parentNode.replaceChild(child, node);
+ node = child;
+ continue;
+ }
+ }
+
+ node = this._getNextNode(node);
+ }
+ },
+
+ /**
+ * Get the article title as an H1.
+ *
+ * @return string
+ **/
+ _getArticleTitle: function() {
+ var doc = this._doc;
+ var curTitle = "";
+ var origTitle = "";
+
+ try {
+ curTitle = origTitle = doc.title.trim();
+
+ // If they had an element with id "title" in their HTML
+ if (typeof curTitle !== "string")
+ curTitle = origTitle = this._getInnerText(doc.getElementsByTagName("title")[0]);
+ } catch (e) {/* ignore exceptions setting the title. */}
+
+ var titleHadHierarchicalSeparators = false;
+ function wordCount(str) {
+ return str.split(/\s+/).length;
+ }
+
+ // If there's a separator in the title, first remove the final part
+ if ((/ [\|\-\\\/>»] /).test(curTitle)) {
+ titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle);
+ curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, "$1");
+
+ // If the resulting title is too short (3 words or fewer), remove
+ // the first part instead:
+ if (wordCount(curTitle) < 3)
+ curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, "$1");
+ } else if (curTitle.indexOf(": ") !== -1) {
+ // Check if we have an heading containing this exact string, so we
+ // could assume it's the full title.
+ var headings = this._concatNodeLists(
+ doc.getElementsByTagName("h1"),
+ doc.getElementsByTagName("h2")
+ );
+ var trimmedTitle = curTitle.trim();
+ var match = this._someNode(headings, function(heading) {
+ return heading.textContent.trim() === trimmedTitle;
+ });
+
+ // If we don't, let's extract the title out of the original title string.
+ if (!match) {
+ curTitle = origTitle.substring(origTitle.lastIndexOf(":") + 1);
+
+ // If the title is now too short, try the first colon instead:
+ if (wordCount(curTitle) < 3) {
+ curTitle = origTitle.substring(origTitle.indexOf(":") + 1);
+ // But if we have too many words before the colon there's something weird
+ // with the titles and the H tags so let's just use the original title instead
+ } else if (wordCount(origTitle.substr(0, origTitle.indexOf(":"))) > 5) {
+ curTitle = origTitle;
+ }
+ }
+ } else if (curTitle.length > 150 || curTitle.length < 15) {
+ var hOnes = doc.getElementsByTagName("h1");
+
+ if (hOnes.length === 1)
+ curTitle = this._getInnerText(hOnes[0]);
+ }
+
+ curTitle = curTitle.trim().replace(this.REGEXPS.normalize, " ");
+ // If we now have 4 words or fewer as our title, and either no
+ // 'hierarchical' separators (\, /, > or ») were found in the original
+ // title or we decreased the number of words by more than 1 word, use
+ // the original title.
+ var curTitleWordCount = wordCount(curTitle);
+ if (curTitleWordCount <= 4 &&
+ (!titleHadHierarchicalSeparators ||
+ curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)) {
+ curTitle = origTitle;
+ }
+
+ return curTitle;
+ },
+
+ /**
+ * Prepare the HTML document for readability to scrape it.
+ * This includes things like stripping javascript, CSS, and handling terrible markup.
+ *
+ * @return void
+ **/
+ _prepDocument: function() {
+ var doc = this._doc;
+
+ // Remove all style tags in head
+ this._removeNodes(this._getAllNodesWithTag(doc, ["style"]));
+
+ if (doc.body) {
+ this._replaceBrs(doc.body);
+ }
+
+ this._replaceNodeTags(this._getAllNodesWithTag(doc, ["font"]), "SPAN");
+ },
+
+ /**
+ * Finds the next node, starting from the given node, and ignoring
+ * whitespace in between. If the given node is an element, the same node is
+ * returned.
+ */
+ _nextNode: function (node) {
+ var next = node;
+ while (next
+ && (next.nodeType != this.ELEMENT_NODE)
+ && this.REGEXPS.whitespace.test(next.textContent)) {
+ next = next.nextSibling;
+ }
+ return next;
+ },
+
+ /**
+ * Replaces 2 or more successive <br> elements with a single <p>.
+ * Whitespace between <br> elements are ignored. For example:
+ * <div>foo<br>bar<br> <br><br>abc</div>
+ * will become:
+ * <div>foo<br>bar<p>abc</p></div>
+ */
+ _replaceBrs: function (elem) {
+ this._forEachNode(this._getAllNodesWithTag(elem, ["br"]), function(br) {
+ var next = br.nextSibling;
+
+ // Whether 2 or more <br> elements have been found and replaced with a
+ // <p> block.
+ var replaced = false;
+
+ // If we find a <br> chain, remove the <br>s until we hit another node
+ // or non-whitespace. This leaves behind the first <br> in the chain
+ // (which will be replaced with a <p> later).
+ while ((next = this._nextNode(next)) && (next.tagName == "BR")) {
+ replaced = true;
+ var brSibling = next.nextSibling;
+ next.parentNode.removeChild(next);
+ next = brSibling;
+ }
+
+ // If we removed a <br> chain, replace the remaining <br> with a <p>. Add
+ // all sibling nodes as children of the <p> until we hit another <br>
+ // chain.
+ if (replaced) {
+ var p = this._doc.createElement("p");
+ br.parentNode.replaceChild(p, br);
+
+ next = p.nextSibling;
+ while (next) {
+ // If we've hit another <br><br>, we're done adding children to this <p>.
+ if (next.tagName == "BR") {
+ var nextElem = this._nextNode(next.nextSibling);
+ if (nextElem && nextElem.tagName == "BR")
+ break;
+ }
+
+ if (!this._isPhrasingContent(next))
+ break;
+
+ // Otherwise, make this node a child of the new <p>.
+ var sibling = next.nextSibling;
+ p.appendChild(next);
+ next = sibling;
+ }
+
+ while (p.lastChild && this._isWhitespace(p.lastChild)) {
+ p.removeChild(p.lastChild);
+ }
+
+ if (p.parentNode.tagName === "P")
+ this._setNodeTag(p.parentNode, "DIV");
+ }
+ });
+ },
+
+ _setNodeTag: function (node, tag) {
+ this.log("_setNodeTag", node, tag);
+ if (this._docJSDOMParser) {
+ node.localName = tag.toLowerCase();
+ node.tagName = tag.toUpperCase();
+ return node;
+ }
+
+ var replacement = node.ownerDocument.createElement(tag);
+ while (node.firstChild) {
+ replacement.appendChild(node.firstChild);
+ }
+ node.parentNode.replaceChild(replacement, node);
+ if (node.readability)
+ replacement.readability = node.readability;
+
+ for (var i = 0; i < node.attributes.length; i++) {
+ try {
+ replacement.setAttribute(node.attributes[i].name, node.attributes[i].value);
+ } catch (ex) {
+ /* it's possible for setAttribute() to throw if the attribute name
+ * isn't a valid XML Name. Such attributes can however be parsed from
+ * source in HTML docs, see https://github.com/whatwg/html/issues/4275,
+ * so we can hit them here and then throw. We don't care about such
+ * attributes so we ignore them.
+ */
+ }
+ }
+ return replacement;
+ },
+
+ /**
+ * Prepare the article node for display. Clean out any inline styles,
+ * iframes, forms, strip extraneous <p> tags, etc.
+ *
+ * @param Element
+ * @return void
+ **/
+ _prepArticle: function(articleContent) {
+ this._cleanStyles(articleContent);
+
+ // Check for data tables before we continue, to avoid removing items in
+ // those tables, which will often be isolated even though they're
+ // visually linked to other content-ful elements (text, images, etc.).
+ this._markDataTables(articleContent);
+
+ this._fixLazyImages(articleContent);
+
+ // Clean out junk from the article content
+ this._cleanConditionally(articleContent, "form");
+ this._cleanConditionally(articleContent, "fieldset");
+ this._clean(articleContent, "object");
+ this._clean(articleContent, "embed");
+ this._clean(articleContent, "footer");
+ this._clean(articleContent, "link");
+ this._clean(articleContent, "aside");
+
+ // Clean out elements with little content that have "share" in their id/class combinations from final top candidates,
+ // which means we don't remove the top candidates even they have "share".
+
+ var shareElementThreshold = this.DEFAULT_CHAR_THRESHOLD;
+
+ this._forEachNode(articleContent.children, function (topCandidate) {
+ this._cleanMatchedNodes(topCandidate, function (node, matchString) {
+ return this.REGEXPS.shareElements.test(matchString) && node.textContent.length < shareElementThreshold;
+ });
+ });
+
+ this._clean(articleContent, "iframe");
+ this._clean(articleContent, "input");
+ this._clean(articleContent, "textarea");
+ this._clean(articleContent, "select");
+ this._clean(articleContent, "button");
+ this._cleanHeaders(articleContent);
+
+ // Do these last as the previous stuff may have removed junk
+ // that will affect these
+ this._cleanConditionally(articleContent, "table");
+ this._cleanConditionally(articleContent, "ul");
+ this._cleanConditionally(articleContent, "div");
+
+ // replace H1 with H2 as H1 should be only title that is displayed separately
+ this._replaceNodeTags(this._getAllNodesWithTag(articleContent, ["h1"]), "h2");
+
+ // Remove extra paragraphs
+ this._removeNodes(this._getAllNodesWithTag(articleContent, ["p"]), function (paragraph) {
+ var imgCount = paragraph.getElementsByTagName("img").length;
+ var embedCount = paragraph.getElementsByTagName("embed").length;
+ var objectCount = paragraph.getElementsByTagName("object").length;
+ // At this point, nasty iframes have been removed, only remain embedded video ones.
+ var iframeCount = paragraph.getElementsByTagName("iframe").length;
+ var totalCount = imgCount + embedCount + objectCount + iframeCount;
+
+ return totalCount === 0 && !this._getInnerText(paragraph, false);
+ });
+
+ this._forEachNode(this._getAllNodesWithTag(articleContent, ["br"]), function(br) {
+ var next = this._nextNode(br.nextSibling);
+ if (next && next.tagName == "P")
+ br.parentNode.removeChild(br);
+ });
+
+ // Remove single-cell tables
+ this._forEachNode(this._getAllNodesWithTag(articleContent, ["table"]), function(table) {
+ var tbody = this._hasSingleTagInsideElement(table, "TBODY") ? table.firstElementChild : table;
+ if (this._hasSingleTagInsideElement(tbody, "TR")) {
+ var row = tbody.firstElementChild;
+ if (this._hasSingleTagInsideElement(row, "TD")) {
+ var cell = row.firstElementChild;
+ cell = this._setNodeTag(cell, this._everyNode(cell.childNodes, this._isPhrasingContent) ? "P" : "DIV");
+ table.parentNode.replaceChild(cell, table);
+ }
+ }
+ });
+ },
+
+ /**
+ * Initialize a node with the readability object. Also checks the
+ * className/id for special names to add to its score.
+ *
+ * @param Element
+ * @return void
+ **/
+ _initializeNode: function(node) {
+ node.readability = {"contentScore": 0};
+
+ switch (node.tagName) {
+ case "DIV":
+ node.readability.contentScore += 5;
+ break;
+
+ case "PRE":
+ case "TD":
+ case "BLOCKQUOTE":
+ node.readability.contentScore += 3;
+ break;
+
+ case "ADDRESS":
+ case "OL":
+ case "UL":
+ case "DL":
+ case "DD":
+ case "DT":
+ case "LI":
+ case "FORM":
+ node.readability.contentScore -= 3;
+ break;
+
+ case "H1":
+ case "H2":
+ case "H3":
+ case "H4":
+ case "H5":
+ case "H6":
+ case "TH":
+ node.readability.contentScore -= 5;
+ break;
+ }
+
+ node.readability.contentScore += this._getClassWeight(node);
+ },
+
+ _removeAndGetNext: function(node) {
+ var nextNode = this._getNextNode(node, true);
+ node.parentNode.removeChild(node);
+ return nextNode;
+ },
+
+ /**
+ * Traverse the DOM from node to node, starting at the node passed in.
+ * Pass true for the second parameter to indicate this node itself
+ * (and its kids) are going away, and we want the next node over.
+ *
+ * Calling this in a loop will traverse the DOM depth-first.
+ */
+ _getNextNode: function(node, ignoreSelfAndKids) {
+ // First check for kids if those aren't being ignored
+ if (!ignoreSelfAndKids && node.firstElementChild) {
+ return node.firstElementChild;
+ }
+ // Then for siblings...
+ if (node.nextElementSibling) {
+ return node.nextElementSibling;
+ }
+ // And finally, move up the parent chain *and* find a sibling
+ // (because this is depth-first traversal, we will have already
+ // seen the parent nodes themselves).
+ do {
+ node = node.parentNode;
+ } while (node && !node.nextElementSibling);
+ return node && node.nextElementSibling;
+ },
+
+ // compares second text to first one
+ // 1 = same text, 0 = completely different text
+ // works the way that it splits both texts into words and then finds words that are unique in second text
+ // the result is given by the lower length of unique parts
+ _textSimilarity: function(textA, textB) {
+ var tokensA = textA.toLowerCase().split(this.REGEXPS.tokenize).filter(Boolean);
+ var tokensB = textB.toLowerCase().split(this.REGEXPS.tokenize).filter(Boolean);
+ if (!tokensA.length || !tokensB.length) {
+ return 0;
+ }
+ var uniqTokensB = tokensB.filter(token => !tokensA.includes(token));
+ var distanceB = uniqTokensB.join(" ").length / tokensB.join(" ").length;
+ return 1 - distanceB;
+ },
+
+ _checkByline: function(node, matchString) {
+ if (this._articleByline) {
+ return false;
+ }
+
+ if (node.getAttribute !== undefined) {
+ var rel = node.getAttribute("rel");
+ var itemprop = node.getAttribute("itemprop");
+ }
+
+ if ((rel === "author" || (itemprop && itemprop.indexOf("author") !== -1) || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) {
+ this._articleByline = node.textContent.trim();
+ return true;
+ }
+
+ return false;
+ },
+
+ _getNodeAncestors: function(node, maxDepth) {
+ maxDepth = maxDepth || 0;
+ var i = 0, ancestors = [];
+ while (node.parentNode) {
+ ancestors.push(node.parentNode);
+ if (maxDepth && ++i === maxDepth)
+ break;
+ node = node.parentNode;
+ }
+ return ancestors;
+ },
+
+ /***
+ * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is
+ * most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
+ *
+ * @param page a document to run upon. Needs to be a full document, complete with body.
+ * @return Element
+ **/
+ _grabArticle: function (page) {
+ this.log("**** grabArticle ****");
+ var doc = this._doc;
+ var isPaging = page !== null;
+ page = page ? page : this._doc.body;
+
+ // We can't grab an article if we don't have a page!
+ if (!page) {
+ this.log("No body found in document. Abort.");
+ return null;
+ }
+
+ var pageCacheHtml = page.innerHTML;
+
+ while (true) {
+ this.log("Starting grabArticle loop");
+ var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS);
+
+ // First, node prepping. Trash nodes that look cruddy (like ones with the
+ // class name "comment", etc), and turn divs into P tags where they have been
+ // used inappropriately (as in, where they contain no other block level elements.)
+ var elementsToScore = [];
+ var node = this._doc.documentElement;
+
+ let shouldRemoveTitleHeader = true;
+
+ while (node) {
+
+ if (node.tagName === "HTML") {
+ this._articleLang = node.getAttribute("lang");
+ }
+
+ var matchString = node.className + " " + node.id;
+
+ if (!this._isProbablyVisible(node)) {
+ this.log("Removing hidden node - " + matchString);
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+
+ // Check to see if this node is a byline, and remove it if it is.
+ if (this._checkByline(node, matchString)) {
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+
+ if (shouldRemoveTitleHeader && this._headerDuplicatesTitle(node)) {
+ this.log("Removing header: ", node.textContent.trim(), this._articleTitle.trim());
+ shouldRemoveTitleHeader = false;
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+
+ // Remove unlikely candidates
+ if (stripUnlikelyCandidates) {
+ if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
+ !this.REGEXPS.okMaybeItsACandidate.test(matchString) &&
+ !this._hasAncestorTag(node, "table") &&
+ !this._hasAncestorTag(node, "code") &&
+ node.tagName !== "BODY" &&
+ node.tagName !== "A") {
+ this.log("Removing unlikely candidate - " + matchString);
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+
+ if (this.UNLIKELY_ROLES.includes(node.getAttribute("role"))) {
+ this.log("Removing content with role " + node.getAttribute("role") + " - " + matchString);
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+ }
+
+ // Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe).
+ if ((node.tagName === "DIV" || node.tagName === "SECTION" || node.tagName === "HEADER" ||
+ node.tagName === "H1" || node.tagName === "H2" || node.tagName === "H3" ||
+ node.tagName === "H4" || node.tagName === "H5" || node.tagName === "H6") &&
+ this._isElementWithoutContent(node)) {
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+
+ if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) {
+ elementsToScore.push(node);
+ }
+
+ // Turn all divs that don't have children block level elements into p's
+ if (node.tagName === "DIV") {
+ // Put phrasing content into paragraphs.
+ var p = null;
+ var childNode = node.firstChild;
+ while (childNode) {
+ var nextSibling = childNode.nextSibling;
+ if (this._isPhrasingContent(childNode)) {
+ if (p !== null) {
+ p.appendChild(childNode);
+ } else if (!this._isWhitespace(childNode)) {
+ p = doc.createElement("p");
+ node.replaceChild(p, childNode);
+ p.appendChild(childNode);
+ }
+ } else if (p !== null) {
+ while (p.lastChild && this._isWhitespace(p.lastChild)) {
+ p.removeChild(p.lastChild);
+ }
+ p = null;
+ }
+ childNode = nextSibling;
+ }
+
+ // Sites like http://mobile.slate.com encloses each paragraph with a DIV
+ // element. DIVs with only a P element inside and no text content can be
+ // safely converted into plain P elements to avoid confusing the scoring
+ // algorithm with DIVs with are, in practice, paragraphs.
+ if (this._hasSingleTagInsideElement(node, "P") && this._getLinkDensity(node) < 0.25) {
+ var newNode = node.children[0];
+ node.parentNode.replaceChild(newNode, node);
+ node = newNode;
+ elementsToScore.push(node);
+ } else if (!this._hasChildBlockElement(node)) {
+ node = this._setNodeTag(node, "P");
+ elementsToScore.push(node);
+ }
+ }
+ node = this._getNextNode(node);
+ }
+
+ /**
+ * Loop through all paragraphs, and assign a score to them based on how content-y they look.
+ * Then add their score to their parent node.
+ *
+ * A score is determined by things like number of commas, class names, etc. Maybe eventually link density.
+ **/
+ var candidates = [];
+ this._forEachNode(elementsToScore, function(elementToScore) {
+ if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === "undefined")
+ return;
+
+ // If this paragraph is less than 25 characters, don't even count it.
+ var innerText = this._getInnerText(elementToScore);
+ if (innerText.length < 25)
+ return;
+
+ // Exclude nodes with no ancestor.
+ var ancestors = this._getNodeAncestors(elementToScore, 5);
+ if (ancestors.length === 0)
+ return;
+
+ var contentScore = 0;
+
+ // Add a point for the paragraph itself as a base.
+ contentScore += 1;
+
+ // Add points for any commas within this paragraph.
+ contentScore += innerText.split(",").length;
+
+ // For every 100 characters in this paragraph, add another point. Up to 3 points.
+ contentScore += Math.min(Math.floor(innerText.length / 100), 3);
+
+ // Initialize and score ancestors.
+ this._forEachNode(ancestors, function(ancestor, level) {
+ if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === "undefined")
+ return;
+
+ if (typeof(ancestor.readability) === "undefined") {
+ this._initializeNode(ancestor);
+ candidates.push(ancestor);
+ }
+
+ // Node score divider:
+ // - parent: 1 (no division)
+ // - grandparent: 2
+ // - great grandparent+: ancestor level * 3
+ if (level === 0)
+ var scoreDivider = 1;
+ else if (level === 1)
+ scoreDivider = 2;
+ else
+ scoreDivider = level * 3;
+ ancestor.readability.contentScore += contentScore / scoreDivider;
+ });
+ });
+
+ // After we've calculated scores, loop through all of the possible
+ // candidate nodes we found and find the one with the highest score.
+ var topCandidates = [];
+ for (var c = 0, cl = candidates.length; c < cl; c += 1) {
+ var candidate = candidates[c];
+
+ // Scale the final candidates score based on link density. Good content
+ // should have a relatively small link density (5% or less) and be mostly
+ // unaffected by this operation.
+ var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate));
+ candidate.readability.contentScore = candidateScore;
+
+ this.log("Candidate:", candidate, "with score " + candidateScore);
+
+ for (var t = 0; t < this._nbTopCandidates; t++) {
+ var aTopCandidate = topCandidates[t];
+
+ if (!aTopCandidate || candidateScore > aTopCandidate.readability.contentScore) {
+ topCandidates.splice(t, 0, candidate);
+ if (topCandidates.length > this._nbTopCandidates)
+ topCandidates.pop();
+ break;
+ }
+ }
+ }
+
+ var topCandidate = topCandidates[0] || null;
+ var neededToCreateTopCandidate = false;
+ var parentOfTopCandidate;
+
+ // If we still have no top candidate, just use the body as a last resort.
+ // We also have to copy the body node so it is something we can modify.
+ if (topCandidate === null || topCandidate.tagName === "BODY") {
+ // Move all of the page's children into topCandidate
+ topCandidate = doc.createElement("DIV");
+ neededToCreateTopCandidate = true;
+ // Move everything (not just elements, also text nodes etc.) into the container
+ // so we even include text directly in the body:
+ while (page.firstChild) {
+ this.log("Moving child out:", page.firstChild);
+ topCandidate.appendChild(page.firstChild);
+ }
+
+ page.appendChild(topCandidate);
+
+ this._initializeNode(topCandidate);
+ } else if (topCandidate) {
+ // Find a better top candidate node if it contains (at least three) nodes which belong to `topCandidates` array
+ // and whose scores are quite closed with current `topCandidate` node.
+ var alternativeCandidateAncestors = [];
+ for (var i = 1; i < topCandidates.length; i++) {
+ if (topCandidates[i].readability.contentScore / topCandidate.readability.contentScore >= 0.75) {
+ alternativeCandidateAncestors.push(this._getNodeAncestors(topCandidates[i]));
+ }
+ }
+ var MINIMUM_TOPCANDIDATES = 3;
+ if (alternativeCandidateAncestors.length >= MINIMUM_TOPCANDIDATES) {
+ parentOfTopCandidate = topCandidate.parentNode;
+ while (parentOfTopCandidate.tagName !== "BODY") {
+ var listsContainingThisAncestor = 0;
+ for (var ancestorIndex = 0; ancestorIndex < alternativeCandidateAncestors.length && listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ancestorIndex++) {
+ listsContainingThisAncestor += Number(alternativeCandidateAncestors[ancestorIndex].includes(parentOfTopCandidate));
+ }
+ if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) {
+ topCandidate = parentOfTopCandidate;
+ break;
+ }
+ parentOfTopCandidate = parentOfTopCandidate.parentNode;
+ }
+ }
+ if (!topCandidate.readability) {
+ this._initializeNode(topCandidate);
+ }
+
+ // Because of our bonus system, parents of candidates might have scores
+ // themselves. They get half of the node. There won't be nodes with higher
+ // scores than our topCandidate, but if we see the score going *up* in the first
+ // few steps up the tree, that's a decent sign that there might be more content
+ // lurking in other places that we want to unify in. The sibling stuff
+ // below does some of that - but only if we've looked high enough up the DOM
+ // tree.
+ parentOfTopCandidate = topCandidate.parentNode;
+ var lastScore = topCandidate.readability.contentScore;
+ // The scores shouldn't get too low.
+ var scoreThreshold = lastScore / 3;
+ while (parentOfTopCandidate.tagName !== "BODY") {
+ if (!parentOfTopCandidate.readability) {
+ parentOfTopCandidate = parentOfTopCandidate.parentNode;
+ continue;
+ }
+ var parentScore = parentOfTopCandidate.readability.contentScore;
+ if (parentScore < scoreThreshold)
+ break;
+ if (parentScore > lastScore) {
+ // Alright! We found a better parent to use.
+ topCandidate = parentOfTopCandidate;
+ break;
+ }
+ lastScore = parentOfTopCandidate.readability.contentScore;
+ parentOfTopCandidate = parentOfTopCandidate.parentNode;
+ }
+
+ // If the top candidate is the only child, use parent instead. This will help sibling
+ // joining logic when adjacent content is actually located in parent's sibling node.
+ parentOfTopCandidate = topCandidate.parentNode;
+ while (parentOfTopCandidate.tagName != "BODY" && parentOfTopCandidate.children.length == 1) {
+ topCandidate = parentOfTopCandidate;
+ parentOfTopCandidate = topCandidate.parentNode;
+ }
+ if (!topCandidate.readability) {
+ this._initializeNode(topCandidate);
+ }
+ }
+
+ // Now that we have the top candidate, look through its siblings for content
+ // that might also be related. Things like preambles, content split by ads
+ // that we removed, etc.
+ var articleContent = doc.createElement("DIV");
+ if (isPaging)
+ articleContent.id = "readability-content";
+
+ var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2);
+ // Keep potential top candidate's parent node to try to get text direction of it later.
+ parentOfTopCandidate = topCandidate.parentNode;
+ var siblings = parentOfTopCandidate.children;
+
+ for (var s = 0, sl = siblings.length; s < sl; s++) {
+ var sibling = siblings[s];
+ var append = false;
+
+ this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : "");
+ this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : "Unknown");
+
+ if (sibling === topCandidate) {
+ append = true;
+ } else {
+ var contentBonus = 0;
+
+ // Give a bonus if sibling nodes and top candidates have the example same classname
+ if (sibling.className === topCandidate.className && topCandidate.className !== "")
+ contentBonus += topCandidate.readability.contentScore * 0.2;
+
+ if (sibling.readability &&
+ ((sibling.readability.contentScore + contentBonus) >= siblingScoreThreshold)) {
+ append = true;
+ } else if (sibling.nodeName === "P") {
+ var linkDensity = this._getLinkDensity(sibling);
+ var nodeContent = this._getInnerText(sibling);
+ var nodeLength = nodeContent.length;
+
+ if (nodeLength > 80 && linkDensity < 0.25) {
+ append = true;
+ } else if (nodeLength < 80 && nodeLength > 0 && linkDensity === 0 &&
+ nodeContent.search(/\.( |$)/) !== -1) {
+ append = true;
+ }
+ }
+ }
+
+ if (append) {
+ this.log("Appending node:", sibling);
+
+ if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) {
+ // We have a node that isn't a common block level element, like a form or td tag.
+ // Turn it into a div so it doesn't get filtered out later by accident.
+ this.log("Altering sibling:", sibling, "to div.");
+
+ sibling = this._setNodeTag(sibling, "DIV");
+ }
+
+ articleContent.appendChild(sibling);
+ // Fetch children again to make it compatible
+ // with DOM parsers without live collection support.
+ siblings = parentOfTopCandidate.children;
+ // siblings is a reference to the children array, and
+ // sibling is removed from the array when we call appendChild().
+ // As a result, we must revisit this index since the nodes
+ // have been shifted.
+ s -= 1;
+ sl -= 1;
+ }
+ }
+
+ if (this._debug)
+ this.log("Article content pre-prep: " + articleContent.innerHTML);
+ // So we have all of the content that we need. Now we clean it up for presentation.
+ this._prepArticle(articleContent);
+ if (this._debug)
+ this.log("Article content post-prep: " + articleContent.innerHTML);
+
+ if (neededToCreateTopCandidate) {
+ // We already created a fake div thing, and there wouldn't have been any siblings left
+ // for the previous loop, so there's no point trying to create a new div, and then
+ // move all the children over. Just assign IDs and class names here. No need to append
+ // because that already happened anyway.
+ topCandidate.id = "readability-page-1";
+ topCandidate.className = "page";
+ } else {
+ var div = doc.createElement("DIV");
+ div.id = "readability-page-1";
+ div.className = "page";
+ while (articleContent.firstChild) {
+ div.appendChild(articleContent.firstChild);
+ }
+ articleContent.appendChild(div);
+ }
+
+ if (this._debug)
+ this.log("Article content after paging: " + articleContent.innerHTML);
+
+ var parseSuccessful = true;
+
+ // Now that we've gone through the full algorithm, check to see if
+ // we got any meaningful content. If we didn't, we may need to re-run
+ // grabArticle with different flags set. This gives us a higher likelihood of
+ // finding the content, and the sieve approach gives us a higher likelihood of
+ // finding the -right- content.
+ var textLength = this._getInnerText(articleContent, true).length;
+ if (textLength < this._charThreshold) {
+ parseSuccessful = false;
+ page.innerHTML = pageCacheHtml;
+
+ if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) {
+ this._removeFlag(this.FLAG_STRIP_UNLIKELYS);
+ this._attempts.push({articleContent: articleContent, textLength: textLength});
+ } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) {
+ this._removeFlag(this.FLAG_WEIGHT_CLASSES);
+ this._attempts.push({articleContent: articleContent, textLength: textLength});
+ } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) {
+ this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY);
+ this._attempts.push({articleContent: articleContent, textLength: textLength});
+ } else {
+ this._attempts.push({articleContent: articleContent, textLength: textLength});
+ // No luck after removing flags, just return the longest text we found during the different loops
+ this._attempts.sort(function (a, b) {
+ return b.textLength - a.textLength;
+ });
+
+ // But first check if we actually have something
+ if (!this._attempts[0].textLength) {
+ return null;
+ }
+
+ articleContent = this._attempts[0].articleContent;
+ parseSuccessful = true;
+ }
+ }
+
+ if (parseSuccessful) {
+ // Find out text direction from ancestors of final top candidate.
+ var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate));
+ this._someNode(ancestors, function(ancestor) {
+ if (!ancestor.tagName)
+ return false;
+ var articleDir = ancestor.getAttribute("dir");
+ if (articleDir) {
+ this._articleDir = articleDir;
+ return true;
+ }
+ return false;
+ });
+ return articleContent;
+ }
+ }
+ },
+
+ /**
+ * Check whether the input string could be a byline.
+ * This verifies that the input is a string, and that the length
+ * is less than 100 chars.
+ *
+ * @param possibleByline {string} - a string to check whether its a byline.
+ * @return Boolean - whether the input string is a byline.
+ */
+ _isValidByline: function(byline) {
+ if (typeof byline == "string" || byline instanceof String) {
+ byline = byline.trim();
+ return (byline.length > 0) && (byline.length < 100);
+ }
+ return false;
+ },
+
+ /**
+ * Converts some of the common HTML entities in string to their corresponding characters.
+ *
+ * @param str {string} - a string to unescape.
+ * @return string without HTML entity.
+ */
+ _unescapeHtmlEntities: function(str) {
+ if (!str) {
+ return str;
+ }
+
+ var htmlEscapeMap = this.HTML_ESCAPE_MAP;
+ return str.replace(/&(quot|amp|apos|lt|gt);/g, function(_, tag) {
+ return htmlEscapeMap[tag];
+ }).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, function(_, hex, numStr) {
+ var num = parseInt(hex || numStr, hex ? 16 : 10);
+ return String.fromCharCode(num);
+ });
+ },
+
+ /**
+ * Try to extract metadata from JSON-LD object.
+ * For now, only Schema.org objects of type Article or its subtypes are supported.
+ * @return Object with any metadata that could be extracted (possibly none)
+ */
+ _getJSONLD: function (doc) {
+ var scripts = this._getAllNodesWithTag(doc, ["script"]);
+
+ var metadata;
+
+ this._forEachNode(scripts, function(jsonLdElement) {
+ if (!metadata && jsonLdElement.getAttribute("type") === "application/ld+json") {
+ try {
+ // Strip CDATA markers if present
+ var content = jsonLdElement.textContent.replace(/^\s*<!\[CDATA\[|\]\]>\s*$/g, "");
+ var parsed = JSON.parse(content);
+ if (
+ !parsed["@context"] ||
+ !parsed["@context"].match(/^https?\:\/\/schema\.org$/)
+ ) {
+ return;
+ }
+
+ if (!parsed["@type"] && Array.isArray(parsed["@graph"])) {
+ parsed = parsed["@graph"].find(function(it) {
+ return (it["@type"] || "").match(
+ this.REGEXPS.jsonLdArticleTypes
+ );
+ });
+ }
+
+ if (
+ !parsed ||
+ !parsed["@type"] ||
+ !parsed["@type"].match(this.REGEXPS.jsonLdArticleTypes)
+ ) {
+ return;
+ }
+
+ metadata = {};
+
+ if (typeof parsed.name === "string" && typeof parsed.headline === "string" && parsed.name !== parsed.headline) {
+ // we have both name and headline element in the JSON-LD. They should both be the same but some websites like aktualne.cz
+ // put their own name into "name" and the article title to "headline" which confuses Readability. So we try to check if either
+ // "name" or "headline" closely matches the html title, and if so, use that one. If not, then we use "name" by default.
+
+ var title = this._getArticleTitle();
+ var nameMatches = this._textSimilarity(parsed.name, title) > 0.75;
+ var headlineMatches = this._textSimilarity(parsed.headline, title) > 0.75;
+
+ if (headlineMatches && !nameMatches) {
+ metadata.title = parsed.headline;
+ } else {
+ metadata.title = parsed.name;
+ }
+ } else if (typeof parsed.name === "string") {
+ metadata.title = parsed.name.trim();
+ } else if (typeof parsed.headline === "string") {
+ metadata.title = parsed.headline.trim();
+ }
+ if (parsed.author) {
+ if (typeof parsed.author.name === "string") {
+ metadata.byline = parsed.author.name.trim();
+ } else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") {
+ metadata.byline = parsed.author
+ .filter(function(author) {
+ return author && typeof author.name === "string";
+ })
+ .map(function(author) {
+ return author.name.trim();
+ })
+ .join(", ");
+ }
+ }
+ if (typeof parsed.description === "string") {
+ metadata.excerpt = parsed.description.trim();
+ }
+ if (
+ parsed.publisher &&
+ typeof parsed.publisher.name === "string"
+ ) {
+ metadata.siteName = parsed.publisher.name.trim();
+ }
+ return;
+ } catch (err) {
+ this.log(err.message);
+ }
+ }
+ });
+ return metadata ? metadata : {};
+ },
+
+ /**
+ * Attempts to get excerpt and byline metadata for the article.
+ *
+ * @param {Object} jsonld — object containing any metadata that
+ * could be extracted from JSON-LD object.
+ *
+ * @return Object with optional "excerpt" and "byline" properties
+ */
+ _getArticleMetadata: function(jsonld) {
+ var metadata = {};
+ var values = {};
+ var metaElements = this._doc.getElementsByTagName("meta");
+
+ // property is a space-separated list of values
+ var propertyPattern = /\s*(dc|dcterm|og|twitter)\s*:\s*(author|creator|description|title|site_name)\s*/gi;
+
+ // name is a single value
+ var namePattern = /^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i;
+
+ // Find description tags.
+ this._forEachNode(metaElements, function(element) {
+ var elementName = element.getAttribute("name");
+ var elementProperty = element.getAttribute("property");
+ var content = element.getAttribute("content");
+ if (!content) {
+ return;
+ }
+ var matches = null;
+ var name = null;
+
+ if (elementProperty) {
+ matches = elementProperty.match(propertyPattern);
+ if (matches) {
+ // Convert to lowercase, and remove any whitespace
+ // so we can match below.
+ name = matches[0].toLowerCase().replace(/\s/g, "");
+ // multiple authors
+ values[name] = content.trim();
+ }
+ }
+ if (!matches && elementName && namePattern.test(elementName)) {
+ name = elementName;
+ if (content) {
+ // Convert to lowercase, remove any whitespace, and convert dots
+ // to colons so we can match below.
+ name = name.toLowerCase().replace(/\s/g, "").replace(/\./g, ":");
+ values[name] = content.trim();
+ }
+ }
+ });
+
+ // get title
+ metadata.title = jsonld.title ||
+ values["dc:title"] ||
+ values["dcterm:title"] ||
+ values["og:title"] ||
+ values["weibo:article:title"] ||
+ values["weibo:webpage:title"] ||
+ values["title"] ||
+ values["twitter:title"];
+
+ if (!metadata.title) {
+ metadata.title = this._getArticleTitle();
+ }
+
+ // get author
+ metadata.byline = jsonld.byline ||
+ values["dc:creator"] ||
+ values["dcterm:creator"] ||
+ values["author"];
+
+ // get description
+ metadata.excerpt = jsonld.excerpt ||
+ values["dc:description"] ||
+ values["dcterm:description"] ||
+ values["og:description"] ||
+ values["weibo:article:description"] ||
+ values["weibo:webpage:description"] ||
+ values["description"] ||
+ values["twitter:description"];
+
+ // get site name
+ metadata.siteName = jsonld.siteName ||
+ values["og:site_name"];
+
+ // in many sites the meta value is escaped with HTML entities,
+ // so here we need to unescape it
+ metadata.title = this._unescapeHtmlEntities(metadata.title);
+ metadata.byline = this._unescapeHtmlEntities(metadata.byline);
+ metadata.excerpt = this._unescapeHtmlEntities(metadata.excerpt);
+ metadata.siteName = this._unescapeHtmlEntities(metadata.siteName);
+
+ return metadata;
+ },
+
+ /**
+ * Check if node is image, or if node contains exactly only one image
+ * whether as a direct child or as its descendants.
+ *
+ * @param Element
+ **/
+ _isSingleImage: function(node) {
+ if (node.tagName === "IMG") {
+ return true;
+ }
+
+ if (node.children.length !== 1 || node.textContent.trim() !== "") {
+ return false;
+ }
+
+ return this._isSingleImage(node.children[0]);
+ },
+
+ /**
+ * Find all <noscript> that are located after <img> nodes, and which contain only one
+ * <img> element. Replace the first image with the image from inside the <noscript> tag,
+ * and remove the <noscript> tag. This improves the quality of the images we use on
+ * some sites (e.g. Medium).
+ *
+ * @param Element
+ **/
+ _unwrapNoscriptImages: function(doc) {
+ // Find img without source or attributes that might contains image, and remove it.
+ // This is done to prevent a placeholder img is replaced by img from noscript in next step.
+ var imgs = Array.from(doc.getElementsByTagName("img"));
+ this._forEachNode(imgs, function(img) {
+ for (var i = 0; i < img.attributes.length; i++) {
+ var attr = img.attributes[i];
+ switch (attr.name) {
+ case "src":
+ case "srcset":
+ case "data-src":
+ case "data-srcset":
+ return;
+ }
+
+ if (/\.(jpg|jpeg|png|webp)/i.test(attr.value)) {
+ return;
+ }
+ }
+
+ img.parentNode.removeChild(img);
+ });
+
+ // Next find noscript and try to extract its image
+ var noscripts = Array.from(doc.getElementsByTagName("noscript"));
+ this._forEachNode(noscripts, function(noscript) {
+ // Parse content of noscript and make sure it only contains image
+ var tmp = doc.createElement("div");
+ tmp.innerHTML = noscript.innerHTML;
+ if (!this._isSingleImage(tmp)) {
+ return;
+ }
+
+ // If noscript has previous sibling and it only contains image,
+ // replace it with noscript content. However we also keep old
+ // attributes that might contains image.
+ var prevElement = noscript.previousElementSibling;
+ if (prevElement && this._isSingleImage(prevElement)) {
+ var prevImg = prevElement;
+ if (prevImg.tagName !== "IMG") {
+ prevImg = prevElement.getElementsByTagName("img")[0];
+ }
+
+ var newImg = tmp.getElementsByTagName("img")[0];
+ for (var i = 0; i < prevImg.attributes.length; i++) {
+ var attr = prevImg.attributes[i];
+ if (attr.value === "") {
+ continue;
+ }
+
+ if (attr.name === "src" || attr.name === "srcset" || /\.(jpg|jpeg|png|webp)/i.test(attr.value)) {
+ if (newImg.getAttribute(attr.name) === attr.value) {
+ continue;
+ }
+
+ var attrName = attr.name;
+ if (newImg.hasAttribute(attrName)) {
+ attrName = "data-old-" + attrName;
+ }
+
+ newImg.setAttribute(attrName, attr.value);
+ }
+ }
+
+ noscript.parentNode.replaceChild(tmp.firstElementChild, prevElement);
+ }
+ });
+ },
+
+ /**
+ * Removes script tags from the document.
+ *
+ * @param Element
+ **/
+ _removeScripts: function(doc) {
+ this._removeNodes(this._getAllNodesWithTag(doc, ["script"]), function(scriptNode) {
+ scriptNode.nodeValue = "";
+ scriptNode.removeAttribute("src");
+ return true;
+ });
+ this._removeNodes(this._getAllNodesWithTag(doc, ["noscript"]));
+ },
+
+ /**
+ * Check if this node has only whitespace and a single element with given tag
+ * Returns false if the DIV node contains non-empty text nodes
+ * or if it contains no element with given tag or more than 1 element.
+ *
+ * @param Element
+ * @param string tag of child element
+ **/
+ _hasSingleTagInsideElement: function(element, tag) {
+ // There should be exactly 1 element child with given tag
+ if (element.children.length != 1 || element.children[0].tagName !== tag) {
+ return false;
+ }
+
+ // And there should be no text nodes with real content
+ return !this._someNode(element.childNodes, function(node) {
+ return node.nodeType === this.TEXT_NODE &&
+ this.REGEXPS.hasContent.test(node.textContent);
+ });
+ },
+
+ _isElementWithoutContent: function(node) {
+ return node.nodeType === this.ELEMENT_NODE &&
+ node.textContent.trim().length == 0 &&
+ (node.children.length == 0 ||
+ node.children.length == node.getElementsByTagName("br").length + node.getElementsByTagName("hr").length);
+ },
+
+ /**
+ * Determine whether element has any children block level elements.
+ *
+ * @param Element
+ */
+ _hasChildBlockElement: function (element) {
+ return this._someNode(element.childNodes, function(node) {
+ return this.DIV_TO_P_ELEMS.has(node.tagName) ||
+ this._hasChildBlockElement(node);
+ });
+ },
+
+ /***
+ * Determine if a node qualifies as phrasing content.
+ * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Phrasing_content
+ **/
+ _isPhrasingContent: function(node) {
+ return node.nodeType === this.TEXT_NODE || this.PHRASING_ELEMS.indexOf(node.tagName) !== -1 ||
+ ((node.tagName === "A" || node.tagName === "DEL" || node.tagName === "INS") &&
+ this._everyNode(node.childNodes, this._isPhrasingContent));
+ },
+
+ _isWhitespace: function(node) {
+ return (node.nodeType === this.TEXT_NODE && node.textContent.trim().length === 0) ||
+ (node.nodeType === this.ELEMENT_NODE && node.tagName === "BR");
+ },
+
+ /**
+ * Get the inner text of a node - cross browser compatibly.
+ * This also strips out any excess whitespace to be found.
+ *
+ * @param Element
+ * @param Boolean normalizeSpaces (default: true)
+ * @return string
+ **/
+ _getInnerText: function(e, normalizeSpaces) {
+ normalizeSpaces = (typeof normalizeSpaces === "undefined") ? true : normalizeSpaces;
+ var textContent = e.textContent.trim();
+
+ if (normalizeSpaces) {
+ return textContent.replace(this.REGEXPS.normalize, " ");
+ }
+ return textContent;
+ },
+
+ /**
+ * Get the number of times a string s appears in the node e.
+ *
+ * @param Element
+ * @param string - what to split on. Default is ","
+ * @return number (integer)
+ **/
+ _getCharCount: function(e, s) {
+ s = s || ",";
+ return this._getInnerText(e).split(s).length - 1;
+ },
+
+ /**
+ * Remove the style attribute on every e and under.
+ * TODO: Test if getElementsByTagName(*) is faster.
+ *
+ * @param Element
+ * @return void
+ **/
+ _cleanStyles: function(e) {
+ if (!e || e.tagName.toLowerCase() === "svg")
+ return;
+
+ // Remove `style` and deprecated presentational attributes
+ for (var i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) {
+ e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES[i]);
+ }
+
+ if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) {
+ e.removeAttribute("width");
+ e.removeAttribute("height");
+ }
+
+ var cur = e.firstElementChild;
+ while (cur !== null) {
+ this._cleanStyles(cur);
+ cur = cur.nextElementSibling;
+ }
+ },
+
+ /**
+ * Get the density of links as a percentage of the content
+ * This is the amount of text that is inside a link divided by the total text in the node.
+ *
+ * @param Element
+ * @return number (float)
+ **/
+ _getLinkDensity: function(element) {
+ var textLength = this._getInnerText(element).length;
+ if (textLength === 0)
+ return 0;
+
+ var linkLength = 0;
+
+ // XXX implement _reduceNodeList?
+ this._forEachNode(element.getElementsByTagName("a"), function(linkNode) {
+ var href = linkNode.getAttribute("href");
+ var coefficient = href && this.REGEXPS.hashUrl.test(href) ? 0.3 : 1;
+ linkLength += this._getInnerText(linkNode).length * coefficient;
+ });
+
+ return linkLength / textLength;
+ },
+
+ /**
+ * Get an elements class/id weight. Uses regular expressions to tell if this
+ * element looks good or bad.
+ *
+ * @param Element
+ * @return number (Integer)
+ **/
+ _getClassWeight: function(e) {
+ if (!this._flagIsActive(this.FLAG_WEIGHT_CLASSES))
+ return 0;
+
+ var weight = 0;
+
+ // Look for a special classname
+ if (typeof(e.className) === "string" && e.className !== "") {
+ if (this.REGEXPS.negative.test(e.className))
+ weight -= 25;
+
+ if (this.REGEXPS.positive.test(e.className))
+ weight += 25;
+ }
+
+ // Look for a special ID
+ if (typeof(e.id) === "string" && e.id !== "") {
+ if (this.REGEXPS.negative.test(e.id))
+ weight -= 25;
+
+ if (this.REGEXPS.positive.test(e.id))
+ weight += 25;
+ }
+
+ return weight;
+ },
+
+ /**
+ * Clean a node of all elements of type "tag".
+ * (Unless it's a youtube/vimeo video. People love movies.)
+ *
+ * @param Element
+ * @param string tag to clean
+ * @return void
+ **/
+ _clean: function(e, tag) {
+ var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1;
+
+ this._removeNodes(this._getAllNodesWithTag(e, [tag]), function(element) {
+ // Allow youtube and vimeo videos through as people usually want to see those.
+ if (isEmbed) {
+ // First, check the elements attributes to see if any of them contain youtube or vimeo
+ for (var i = 0; i < element.attributes.length; i++) {
+ if (this.REGEXPS.videos.test(element.attributes[i].value)) {
+ return false;
+ }
+ }
+
+ // For embed with <object> tag, check inner HTML as well.
+ if (element.tagName === "object" && this.REGEXPS.videos.test(element.innerHTML)) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ },
+
+ /**
+ * Check if a given node has one of its ancestor tag name matching the
+ * provided one.
+ * @param HTMLElement node
+ * @param String tagName
+ * @param Number maxDepth
+ * @param Function filterFn a filter to invoke to determine whether this node 'counts'
+ * @return Boolean
+ */
+ _hasAncestorTag: function(node, tagName, maxDepth, filterFn) {
+ maxDepth = maxDepth || 3;
+ tagName = tagName.toUpperCase();
+ var depth = 0;
+ while (node.parentNode) {
+ if (maxDepth > 0 && depth > maxDepth)
+ return false;
+ if (node.parentNode.tagName === tagName && (!filterFn || filterFn(node.parentNode)))
+ return true;
+ node = node.parentNode;
+ depth++;
+ }
+ return false;
+ },
+
+ /**
+ * Return an object indicating how many rows and columns this table has.
+ */
+ _getRowAndColumnCount: function(table) {
+ var rows = 0;
+ var columns = 0;
+ var trs = table.getElementsByTagName("tr");
+ for (var i = 0; i < trs.length; i++) {
+ var rowspan = trs[i].getAttribute("rowspan") || 0;
+ if (rowspan) {
+ rowspan = parseInt(rowspan, 10);
+ }
+ rows += (rowspan || 1);
+
+ // Now look for column-related info
+ var columnsInThisRow = 0;
+ var cells = trs[i].getElementsByTagName("td");
+ for (var j = 0; j < cells.length; j++) {
+ var colspan = cells[j].getAttribute("colspan") || 0;
+ if (colspan) {
+ colspan = parseInt(colspan, 10);
+ }
+ columnsInThisRow += (colspan || 1);
+ }
+ columns = Math.max(columns, columnsInThisRow);
+ }
+ return {rows: rows, columns: columns};
+ },
+
+ /**
+ * Look for 'data' (as opposed to 'layout') tables, for which we use
+ * similar checks as
+ * https://searchfox.org/mozilla-central/rev/f82d5c549f046cb64ce5602bfd894b7ae807c8f8/accessible/generic/TableAccessible.cpp#19
+ */
+ _markDataTables: function(root) {
+ var tables = root.getElementsByTagName("table");
+ for (var i = 0; i < tables.length; i++) {
+ var table = tables[i];
+ var role = table.getAttribute("role");
+ if (role == "presentation") {
+ table._readabilityDataTable = false;
+ continue;
+ }
+ var datatable = table.getAttribute("datatable");
+ if (datatable == "0") {
+ table._readabilityDataTable = false;
+ continue;
+ }
+ var summary = table.getAttribute("summary");
+ if (summary) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ var caption = table.getElementsByTagName("caption")[0];
+ if (caption && caption.childNodes.length > 0) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ // If the table has a descendant with any of these tags, consider a data table:
+ var dataTableDescendants = ["col", "colgroup", "tfoot", "thead", "th"];
+ var descendantExists = function(tag) {
+ return !!table.getElementsByTagName(tag)[0];
+ };
+ if (dataTableDescendants.some(descendantExists)) {
+ this.log("Data table because found data-y descendant");
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ // Nested tables indicate a layout table:
+ if (table.getElementsByTagName("table")[0]) {
+ table._readabilityDataTable = false;
+ continue;
+ }
+
+ var sizeInfo = this._getRowAndColumnCount(table);
+ if (sizeInfo.rows >= 10 || sizeInfo.columns > 4) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+ // Now just go by size entirely:
+ table._readabilityDataTable = sizeInfo.rows * sizeInfo.columns > 10;
+ }
+ },
+
+ /* convert images and figures that have properties like data-src into images that can be loaded without JS */
+ _fixLazyImages: function (root) {
+ this._forEachNode(this._getAllNodesWithTag(root, ["img", "picture", "figure"]), function (elem) {
+ // In some sites (e.g. Kotaku), they put 1px square image as base64 data uri in the src attribute.
+ // So, here we check if the data uri is too short, just might as well remove it.
+ if (elem.src && this.REGEXPS.b64DataUrl.test(elem.src)) {
+ // Make sure it's not SVG, because SVG can have a meaningful image in under 133 bytes.
+ var parts = this.REGEXPS.b64DataUrl.exec(elem.src);
+ if (parts[1] === "image/svg+xml") {
+ return;
+ }
+
+ // Make sure this element has other attributes which contains image.
+ // If it doesn't, then this src is important and shouldn't be removed.
+ var srcCouldBeRemoved = false;
+ for (var i = 0; i < elem.attributes.length; i++) {
+ var attr = elem.attributes[i];
+ if (attr.name === "src") {
+ continue;
+ }
+
+ if (/\.(jpg|jpeg|png|webp)/i.test(attr.value)) {
+ srcCouldBeRemoved = true;
+ break;
+ }
+ }
+
+ // Here we assume if image is less than 100 bytes (or 133B after encoded to base64)
+ // it will be too small, therefore it might be placeholder image.
+ if (srcCouldBeRemoved) {
+ var b64starts = elem.src.search(/base64\s*/i) + 7;
+ var b64length = elem.src.length - b64starts;
+ if (b64length < 133) {
+ elem.removeAttribute("src");
+ }
+ }
+ }
+
+ // also check for "null" to work around https://github.com/jsdom/jsdom/issues/2580
+ if ((elem.src || (elem.srcset && elem.srcset != "null")) && elem.className.toLowerCase().indexOf("lazy") === -1) {
+ return;
+ }
+
+ for (var j = 0; j < elem.attributes.length; j++) {
+ attr = elem.attributes[j];
+ if (attr.name === "src" || attr.name === "srcset" || attr.name === "alt") {
+ continue;
+ }
+ var copyTo = null;
+ if (/\.(jpg|jpeg|png|webp)\s+\d/.test(attr.value)) {
+ copyTo = "srcset";
+ } else if (/^\s*\S+\.(jpg|jpeg|png|webp)\S*\s*$/.test(attr.value)) {
+ copyTo = "src";
+ }
+ if (copyTo) {
+ //if this is an img or picture, set the attribute directly
+ if (elem.tagName === "IMG" || elem.tagName === "PICTURE") {
+ elem.setAttribute(copyTo, attr.value);
+ } else if (elem.tagName === "FIGURE" && !this._getAllNodesWithTag(elem, ["img", "picture"]).length) {
+ //if the item is a <figure> that does not contain an image or picture, create one and place it inside the figure
+ //see the nytimes-3 testcase for an example
+ var img = this._doc.createElement("img");
+ img.setAttribute(copyTo, attr.value);
+ elem.appendChild(img);
+ }
+ }
+ }
+ });
+ },
+
+ _getTextDensity: function(e, tags) {
+ var textLength = this._getInnerText(e, true).length;
+ if (textLength === 0) {
+ return 0;
+ }
+ var childrenLength = 0;
+ var children = this._getAllNodesWithTag(e, tags);
+ this._forEachNode(children, (child) => childrenLength += this._getInnerText(child, true).length);
+ return childrenLength / textLength;
+ },
+
+ /**
+ * Clean an element of all tags of type "tag" if they look fishy.
+ * "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
+ *
+ * @return void
+ **/
+ _cleanConditionally: function(e, tag) {
+ if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY))
+ return;
+
+ // Gather counts for other typical elements embedded within.
+ // Traverse backwards so we can remove nodes at the same time
+ // without effecting the traversal.
+ //
+ // TODO: Consider taking into account original contentScore here.
+ this._removeNodes(this._getAllNodesWithTag(e, [tag]), function(node) {
+ // First check if this node IS data table, in which case don't remove it.
+ var isDataTable = function(t) {
+ return t._readabilityDataTable;
+ };
+
+ var isList = tag === "ul" || tag === "ol";
+ if (!isList) {
+ var listLength = 0;
+ var listNodes = this._getAllNodesWithTag(node, ["ul", "ol"]);
+ this._forEachNode(listNodes, (list) => listLength += this._getInnerText(list).length);
+ isList = listLength / this._getInnerText(node).length > 0.9;
+ }
+
+ if (tag === "table" && isDataTable(node)) {
+ return false;
+ }
+
+ // Next check if we're inside a data table, in which case don't remove it as well.
+ if (this._hasAncestorTag(node, "table", -1, isDataTable)) {
+ return false;
+ }
+
+ if (this._hasAncestorTag(node, "code")) {
+ return false;
+ }
+
+ var weight = this._getClassWeight(node);
+
+ this.log("Cleaning Conditionally", node);
+
+ var contentScore = 0;
+
+ if (weight + contentScore < 0) {
+ return true;
+ }
+
+ if (this._getCharCount(node, ",") < 10) {
+ // If there are not very many commas, and the number of
+ // non-paragraph elements is more than paragraphs or other
+ // ominous signs, remove the element.
+ var p = node.getElementsByTagName("p").length;
+ var img = node.getElementsByTagName("img").length;
+ var li = node.getElementsByTagName("li").length - 100;
+ var input = node.getElementsByTagName("input").length;
+ var headingDensity = this._getTextDensity(node, ["h1", "h2", "h3", "h4", "h5", "h6"]);
+
+ var embedCount = 0;
+ var embeds = this._getAllNodesWithTag(node, ["object", "embed", "iframe"]);
+
+ for (var i = 0; i < embeds.length; i++) {
+ // If this embed has attribute that matches video regex, don't delete it.
+ for (var j = 0; j < embeds[i].attributes.length; j++) {
+ if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) {
+ return false;
+ }
+ }
+
+ // For embed with <object> tag, check inner HTML as well.
+ if (embeds[i].tagName === "object" && this.REGEXPS.videos.test(embeds[i].innerHTML)) {
+ return false;
+ }
+
+ embedCount++;
+ }
+
+ var linkDensity = this._getLinkDensity(node);
+ var contentLength = this._getInnerText(node).length;
+
+ var haveToRemove =
+ (img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, "figure")) ||
+ (!isList && li > p) ||
+ (input > Math.floor(p/3)) ||
+ (!isList && headingDensity < 0.9 && contentLength < 25 && (img === 0 || img > 2) && !this._hasAncestorTag(node, "figure")) ||
+ (!isList && weight < 25 && linkDensity > 0.2) ||
+ (weight >= 25 && linkDensity > 0.5) ||
+ ((embedCount === 1 && contentLength < 75) || embedCount > 1);
+ return haveToRemove;
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Clean out elements that match the specified conditions
+ *
+ * @param Element
+ * @param Function determines whether a node should be removed
+ * @return void
+ **/
+ _cleanMatchedNodes: function(e, filter) {
+ var endOfSearchMarkerNode = this._getNextNode(e, true);
+ var next = this._getNextNode(e);
+ while (next && next != endOfSearchMarkerNode) {
+ if (filter.call(this, next, next.className + " " + next.id)) {
+ next = this._removeAndGetNext(next);
+ } else {
+ next = this._getNextNode(next);
+ }
+ }
+ },
+
+ /**
+ * Clean out spurious headers from an Element.
+ *
+ * @param Element
+ * @return void
+ **/
+ _cleanHeaders: function(e) {
+ let headingNodes = this._getAllNodesWithTag(e, ["h1", "h2"]);
+ this._removeNodes(headingNodes, function(node) {
+ let shouldRemove = this._getClassWeight(node) < 0;
+ if (shouldRemove) {
+ this.log("Removing header with low class weight:", node);
+ }
+ return shouldRemove;
+ });
+ },
+
+ /**
+ * Check if this node is an H1 or H2 element whose content is mostly
+ * the same as the article title.
+ *
+ * @param Element the node to check.
+ * @return boolean indicating whether this is a title-like header.
+ */
+ _headerDuplicatesTitle: function(node) {
+ if (node.tagName != "H1" && node.tagName != "H2") {
+ return false;
+ }
+ var heading = this._getInnerText(node, false);
+ this.log("Evaluating similarity of header:", heading, this._articleTitle);
+ return this._textSimilarity(this._articleTitle, heading) > 0.75;
+ },
+
+ _flagIsActive: function(flag) {
+ return (this._flags & flag) > 0;
+ },
+
+ _removeFlag: function(flag) {
+ this._flags = this._flags & ~flag;
+ },
+
+ _isProbablyVisible: function(node) {
+ // Have to null-check node.style and node.className.indexOf to deal with SVG and MathML nodes.
+ return (!node.style || node.style.display != "none")
+ && !node.hasAttribute("hidden")
+ //check for "fallback-image" so that wikimedia math images are displayed
+ && (!node.hasAttribute("aria-hidden") || node.getAttribute("aria-hidden") != "true" || (node.className && node.className.indexOf && node.className.indexOf("fallback-image") !== -1));
+ },
+
+ /**
+ * Runs readability.
+ *
+ * Workflow:
+ * 1. Prep the document by removing script tags, css, etc.
+ * 2. Build readability's DOM tree.
+ * 3. Grab the article content from the current dom tree.
+ * 4. Replace the current DOM tree with the new one.
+ * 5. Read peacefully.
+ *
+ * @return void
+ **/
+ parse: function () {
+ // Avoid parsing too large documents, as per configuration option
+ if (this._maxElemsToParse > 0) {
+ var numTags = this._doc.getElementsByTagName("*").length;
+ if (numTags > this._maxElemsToParse) {
+ throw new Error("Aborting parsing document; " + numTags + " elements found");
+ }
+ }
+
+ // Unwrap image from noscript
+ this._unwrapNoscriptImages(this._doc);
+
+ // Extract JSON-LD metadata before removing scripts
+ var jsonLd = this._disableJSONLD ? {} : this._getJSONLD(this._doc);
+
+ // Remove script tags from the document.
+ this._removeScripts(this._doc);
+
+ this._prepDocument();
+
+ var metadata = this._getArticleMetadata(jsonLd);
+ this._articleTitle = metadata.title;
+
+ var articleContent = this._grabArticle();
+ if (!articleContent)
+ return null;
+
+ this.log("Grabbed: " + articleContent.innerHTML);
+
+ this._postProcessContent(articleContent);
+
+ // If we haven't found an excerpt in the article's metadata, use the article's
+ // first paragraph as the excerpt. This is used for displaying a preview of
+ // the article's content.
+ if (!metadata.excerpt) {
+ var paragraphs = articleContent.getElementsByTagName("p");
+ if (paragraphs.length > 0) {
+ metadata.excerpt = paragraphs[0].textContent.trim();
+ }
+ }
+
+ var textContent = articleContent.textContent;
+ return {
+ title: this._articleTitle,
+ byline: metadata.byline || this._articleByline,
+ dir: this._articleDir,
+ lang: this._articleLang,
+ content: this._serializer(articleContent),
+ textContent: textContent,
+ length: textContent.length,
+ excerpt: metadata.excerpt,
+ siteName: metadata.siteName || this._articleSiteName
+ };
+ }
+};
+
+if (typeof module === "object") {
+ module.exports = Readability;
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/readability-readerable-0.4.2.js b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/readability-readerable-0.4.2.js
new file mode 100644
index 0000000000..64be5e15e8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readability/readability-readerable-0.4.2.js
@@ -0,0 +1,108 @@
+/* eslint-env es6:false */
+/*
+ * Copyright (c) 2010 Arc90 Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This code is heavily based on Arc90's readability.js (1.7.1) script
+ * available at: http://code.google.com/p/arc90labs-readability
+ */
+
+var REGEXPS = {
+ // NOTE: These two regular expressions are duplicated in
+ // Readability.js. Please keep both copies in sync.
+ unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
+ okMaybeItsACandidate: /and|article|body|column|content|main|shadow/i,
+};
+
+function isNodeVisible(node) {
+ // Have to null-check node.style and node.className.indexOf to deal with SVG and MathML nodes.
+ return (!node.style || node.style.display != "none")
+ && !node.hasAttribute("hidden")
+ //check for "fallback-image" so that wikimedia math images are displayed
+ && (!node.hasAttribute("aria-hidden") || node.getAttribute("aria-hidden") != "true" || (node.className && node.className.indexOf && node.className.indexOf("fallback-image") !== -1));
+}
+
+/**
+ * Decides whether or not the document is reader-able without parsing the whole thing.
+ * @param {Object} options Configuration object.
+ * @param {number} [options.minContentLength=140] The minimum node content length used to decide if the document is readerable.
+ * @param {number} [options.minScore=20] The minumum cumulated 'score' used to determine if the document is readerable.
+ * @param {Function} [options.visibilityChecker=isNodeVisible] The function used to determine if a node is visible.
+ * @return {boolean} Whether or not we suspect Readability.parse() will suceeed at returning an article object.
+ */
+function isProbablyReaderable(doc, options = {}) {
+ // For backward compatibility reasons 'options' can either be a configuration object or the function used
+ // to determine if a node is visible.
+ if (typeof options == "function") {
+ options = { visibilityChecker: options };
+ }
+
+ var defaultOptions = { minScore: 20, minContentLength: 140, visibilityChecker: isNodeVisible };
+ options = Object.assign(defaultOptions, options);
+
+ var nodes = doc.querySelectorAll("p, pre, article");
+
+ // Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
+ // Some articles' DOM structures might look like
+ // <div>
+ // Sentences<br>
+ // <br>
+ // Sentences<br>
+ // </div>
+ var brNodes = doc.querySelectorAll("div > br");
+ if (brNodes.length) {
+ var set = new Set(nodes);
+ [].forEach.call(brNodes, function (node) {
+ set.add(node.parentNode);
+ });
+ nodes = Array.from(set);
+ }
+
+ var score = 0;
+ // This is a little cheeky, we use the accumulator 'score' to decide what to return from
+ // this callback:
+ return [].some.call(nodes, function (node) {
+ if (!options.visibilityChecker(node)) {
+ return false;
+ }
+
+ var matchString = node.className + " " + node.id;
+ if (REGEXPS.unlikelyCandidates.test(matchString) &&
+ !REGEXPS.okMaybeItsACandidate.test(matchString)) {
+ return false;
+ }
+
+ if (node.matches("li p")) {
+ return false;
+ }
+
+ var textContentLength = node.textContent.trim().length;
+ if (textContentLength < options.minContentLength) {
+ return false;
+ }
+
+ score += Math.sqrt(textContentLength - options.minContentLength);
+
+ if (score > options.minScore) {
+ return true;
+ }
+ return false;
+ });
+}
+
+if (typeof module === "object") {
+ module.exports = isProbablyReaderable;
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview-background.js b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview-background.js
new file mode 100644
index 0000000000..136e5e40e3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview-background.js
@@ -0,0 +1,22 @@
+/* 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/. */
+
+// This background script is needed to update the current tab
+// and activate reader view.
+
+browser.runtime.onMessage.addListener(message => {
+ switch (message.action) {
+ case 'addSerializedDoc':
+ browser.storage.session.set({ [message.id]: message.doc });
+ return Promise.resolve();
+ case 'getSerializedDoc':
+ return (async () => {
+ let doc = await browser.storage.session.get(message.id);
+ browser.storage.session.remove(message.id);
+ return doc[message.id];
+ })();
+ default:
+ console.error(`Received unsupported action ${message.action}`);
+ }
+});
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview-content.js b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview-content.js
new file mode 100644
index 0000000000..1d5859e793
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview-content.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+// This script is injected into content to determine whether or not a
+// page is readerable, and to open a reader view extension page via
+// the background script.
+
+const supportedProtocols = ["http:", "https:"];
+
+// Prevent false positives for these sites. This list is taken from Fennec:
+// https://dxr.mozilla.org/mozilla-central/rev/7d47e7fa2489550ffa83aae67715c5497048923f/toolkit/components/reader/Readerable.js#45
+const blockedHosts = [
+ "amazon.com",
+ "github.com",
+ "mail.google.com",
+ "pinterest.com",
+ "reddit.com",
+ "twitter.com",
+ "youtube.com"
+];
+
+function isReaderable() {
+ if (!supportedProtocols.includes(location.protocol)) {
+ return false;
+ }
+
+ if (blockedHosts.some(blockedHost => location.hostname.endsWith(blockedHost))) {
+ return false;
+ }
+
+ if (location.pathname == "/") {
+ return false;
+ }
+
+ return isProbablyReaderable(document, _isNodeVisible);
+}
+
+function _isNodeVisible(node) {
+ return node.clientHeight > 0 && node.clientWidth > 0;
+}
+
+function connectNativePort() {
+ let port = browser.runtime.connectNative("mozacReaderview");
+ port.onMessage.addListener((message) => {
+ switch (message.action) {
+ case 'cachePage':
+ let serializedDoc = new XMLSerializer().serializeToString(document);
+ browser.runtime.sendMessage({action: "addSerializedDoc", doc: serializedDoc, id: message.id});
+ break;
+ case 'checkReaderState':
+ port.postMessage({type: 'checkReaderState', baseUrl: browser.runtime.getURL("/"), readerable: isReaderable()});
+ break;
+ default:
+ console.error(`Received unsupported action ${message.action}`);
+ }
+ });
+
+ return port;
+}
+
+let port = connectNativePort();
+
+// When navigating to a cached page, this content script won't run again, but we
+// do want to connect a new native port to trigger a new readerable check and
+// apply the same logic (as on page load) in our feature class (e.g. updating the
+// toolbar etc.)
+window.addEventListener("pageshow", (event) => {
+ port = (port != null)? port : connectNativePort();
+});
+
+window.addEventListener("pagehide", (event) => {
+ port.disconnect();
+ port = null;
+});
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.css b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.css
new file mode 100644
index 0000000000..d058e12061
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.css
@@ -0,0 +1,319 @@
+/* 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/. */
+
+.mozac-readerview-body {
+ padding: 20px;
+ transition-property: background-color, color;
+ transition-duration: 0.4s;
+ max-width: 35em;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.mozac-readerview-body.light {
+ background-color: #ffffff;
+ color: #222222;
+}
+
+.mozac-readerview-body.sepia {
+ color: #5b4636;
+ background-color: #f4ecd8;
+}
+
+.mozac-readerview-body.dark {
+ background-color: #1c1b22;
+ color: #eeeeee;
+}
+
+.mozac-readerview-body.light * {
+ color: #222222;
+}
+
+.mozac-readerview-body.sepia * {
+ color: #5b4636;
+}
+
+.mozac-readerview-body.dark * {
+ color: #eeeeee;
+}
+
+.mozac-readerview-body.sans-serif * {
+ font-family: sans-serif !important;
+}
+
+.mozac-readerview-body.serif * {
+ font-family: serif !important;
+}
+
+/* Override some controls and content styles based on color scheme */
+
+.mozac-readerview-body.light > .container > .header > .domain {
+ color: #ee7600;
+ border-bottom-color: #d0d0d0;
+}
+
+.mozac-readerview-body.light > .container > .header > h1 {
+ color: #222222;
+}
+
+.mozac-readerview-body.light > .container > .header > .credits {
+ color: #898989;
+}
+
+.mozac-readerview-body.dark > .container > .header > .domain {
+ color: #ff9400;
+ border-bottom-color: #777777;
+}
+
+.mozac-readerview-body.dark > .container > .header > h1 {
+ color: #eeeeee;
+}
+
+.mozac-readerview-body.dark > .container > .header > .credits {
+ color: #aaaaaa;
+}
+
+.mozac-readerview-body.sepia > .container > .header > .domain {
+ border-bottom-color: #5b4636 !important;
+}
+
+.mozac-readerview-body.sepia > .container > .footer {
+ background-color: #dedad4 !important;
+}
+
+.mozac-readerview-body.light > .container > .content .caption,
+.mozac-readerview-body.light > .container > .content .wp-caption-text,
+.mozac-readerview-body.light > .container > .content figcaption {
+ color: #898989;
+}
+
+.mozac-readerview-body.dark > .container > .content .caption,
+.mozac-readerview-body.dark > .container > .content .wp-caption-text,
+.mozac-readerview-body.dark > .container > .content figcaption {
+ color: #aaaaaa;
+}
+
+.mozac-readerview-body.light > .container > .content blockquote {
+ color: #898989 !important;
+ border-left-color: #d0d0d0 !important;
+}
+
+.mozac-readerview-body.sepia blockquote {
+ border-inline-start: 2px solid #5b4636 !important;
+}
+
+.mozac-readerview-body.dark > .container > .content blockquote {
+ color: #aaaaaa !important;
+ border-left-color: #777777 !important;
+}
+
+.mozac-readerview-body > .container > hr {
+ margin: 0px;
+}
+
+.mozac-readerview-body > .container > .header {
+ text-align: start;
+ padding-bottom: 10px;
+}
+
+.mozac-readerview-body > .container > .header > .credits {
+ font-size: 0.9em;
+}
+
+.mozac-readerview-body > .container > .header > .domain {
+ margin-top: 10px;
+ padding-bottom: 10px;
+ color: #00acff !important;
+ text-decoration: none;
+}
+
+.mozac-readerview-body > .container > .header > .domain-border {
+ margin-top: 15px;
+ border-bottom: 1.5px solid #777777;
+ width: 50%;
+}
+
+.mozac-readerview-body > .container > .header > h1 {
+ font-size: 1.33em;
+ font-weight: 700;
+ line-height: 1.1em;
+ width: 100%;
+ margin: 0px;
+ margin-top: 32px;
+ margin-bottom: 16px;
+ padding: 0px;
+}
+
+.mozac-readerview-body > .container > .header > .credits {
+ padding: 0px;
+ margin: 0px;
+ margin-bottom: 32px;
+}
+
+.mozac-readerview-body > .container > .header > .meta-data {
+ font-size: 0.65em;
+ margin: 0 0 15px 0;
+}
+
+.mozac-readerview-body > .container > .content {
+ padding-top: 10px;
+ padding-left: 0px;
+ padding-right: 0px;
+}
+
+/*======= Article content =======*/
+.mozac-readerview-content {
+ font-size: 1em;
+}
+
+.mozac-readerview-content a {
+ text-decoration: underline !important;
+ font-weight: normal;
+}
+
+.mozac-readerview-body.dark :is(
+ .mozac-readerview-content a,
+ .mozac-readerview-content a:hover,
+ .mozac-readerview-content a:active
+ ):not(.mozac-readerview-content a:visited) {
+ color: #45a1ff !important;
+}
+
+.mozac-readerview-content a,
+.mozac-readerview-content a:hover,
+.mozac-readerview-content a:active
+:not(.mozac-readerview-content a:visited) {
+ color: #0060df !important;
+}
+
+.mozac-readerview-content a:visited {
+ color: #b5007f !important;
+}
+
+.mozac-readerview-content h1 {
+ margin-top: 16px;
+ margin-bottom: 16px;
+ font-weight: 700;
+ font-size: 1.6em;
+}
+
+.mozac-readerview-content h2 {
+ margin-top: 16px;
+ margin-bottom: 16px;
+ font-weight: 700;
+ font-size: 1.2em;
+}
+
+.mozac-readerview-content h3 {
+ margin-top: 16px;
+ margin-bottom: 16px;
+ font-weight: 700;
+ font-size: 1em;
+}
+
+.mozac-readerview-content * {
+ max-width: 100% !important;
+ height: auto !important;
+}
+
+.mozac-readerview-content p {
+ font-size: 1em !important;
+ line-height: 1.4em !important;
+ margin: 0px !important;
+ margin-bottom: 20px !important;
+}
+
+/* Covers all images showing edge-to-edge using a
+ an optional caption text */
+.mozac-readerview-content .wp-caption,
+.mozac-readerview-content figure {
+ display: block !important;
+ width: 100% !important;
+ margin: 0px !important;
+ margin-bottom: 32px !important;
+}
+
+/* Images marked to be shown edge-to-edge with an
+ optional captio ntext */
+.mozac-readerview-content p > img:only-child,
+.mozac-readerview-content p > a:only-child > img:only-child,
+.mozac-readerview-content .wp-caption img,
+.mozac-readerview-content figure img {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Account for body padding to make image full width */
+.mozac-readerview-content img[moz-reader-full-width] {
+ width: calc(100% + 40px);
+ margin-left: -20px;
+ margin-right: -20px;
+ max-width: none !important;
+}
+
+/* Image caption text */
+.mozac-readerview-content .caption,
+.mozac-readerview-content .wp-caption-text,
+.mozac-readerview-content figcaption {
+ font-size: 0.9em;
+ font-family: sans-serif;
+ margin: 0px !important;
+ padding-top: 4px !important;
+}
+
+/* Ensure all pre-formatted code inside the reader content
+ are properly wrapped inside content width */
+.mozac-readerview-content code,
+.mozac-readerview-content pre {
+ white-space: pre-wrap !important;
+ margin-bottom: 20px !important;
+}
+
+.mozac-readerview-content blockquote {
+ margin: 0px !important;
+ margin-bottom: 20px !important;
+ padding: 0px !important;
+ padding-inline-start: 16px !important;
+ border: 0px !important;
+ border-left: 2px solid !important;
+}
+
+.mozac-readerview-content ul,
+.mozac-readerview-content ol {
+ margin: 0px !important;
+ margin-bottom: 20px !important;
+ padding: 0px !important;
+ line-height: 1.5em;
+}
+
+.mozac-readerview-content ul {
+ padding-inline-start: 30px !important;
+ list-style: disc !important;
+}
+
+.mozac-readerview-content ol {
+ padding-inline-start: 35px !important;
+ list-style: decimal !important;
+}
+
+/* Hide elements with common "hidden" class names */
+.mozac-readerview-content .visually-hidden,
+.mozac-readerview-content .visuallyhidden,
+.mozac-readerview-content .hidden,
+.mozac-readerview-content .invisible,
+.mozac-readerview-content .sr-only {
+}
+
+/* Enforce wordpress and similar emoji/smileys aren't sized to be full-width,
+ * see bug 1399616 for context. */
+.mozac-readerview-content img.wp-smiley,
+.mozac-readerview-content img.emoji {
+ display: inline-block;
+ border-width: 0;
+ /* height: auto is implied from `.mozac-readerview-content *` rule. */
+ width: 1em;
+ margin: 0 .07em;
+ padding: 0;
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.html b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.html
new file mode 100644
index 0000000000..b09deb7811
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.html
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<html>
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <meta http-equiv="cache-control" content="no-store" />
+
+ <link rel="stylesheet" href="readerview.css" />
+
+ <script src="readability/JSDOMParser-0.4.2.js"></script>
+ <script src="readability/readability-0.4.2.js"></script>
+ <script src="readerview.js"></script>
+ </head>
+</html>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.js b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.js
new file mode 100644
index 0000000000..23b1f35250
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/assets/extensions/readerview/readerview.js
@@ -0,0 +1,366 @@
+/* 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/. */
+
+// Class names to preserve in the readerized output. We preserve these class
+// names so that rules in readerview.css can match them. This list is taken from Fennec:
+// https://dxr.mozilla.org/mozilla-central/rev/7d47e7fa2489550ffa83aae67715c5497048923f/toolkit/components/reader/ReaderMode.jsm#21
+const preservedClasses = [
+ "caption",
+ "emoji",
+ "hidden",
+ "invisible",
+ "sr-only",
+ "visually-hidden",
+ "visuallyhidden",
+ "wp-caption",
+ "wp-caption-text",
+ "wp-smiley"
+];
+
+class ReaderView {
+
+ static get MIN_FONT_SIZE() {
+ return 1;
+ }
+
+ static get MAX_FONT_SIZE() {
+ return 9;
+ }
+
+ /**
+ * Shows a reader view for the provided document. This method is used when activating
+ * reader view on the original page. In this case, we already have the DOM (passed
+ * through in the message from the background script) and can parse it directly.
+ *
+ * @param doc the document to make readerable.
+ * @param url the url of the article.
+ * @param options the fontSize, fontType and colorScheme to use.
+ */
+ show(doc, url, options = {fontSize: 4, fontType: "sans-serif", colorScheme: "light"}) {
+ let result = new Readability(doc, {classesToPreserve: preservedClasses}).parse();
+ result.language = doc.documentElement.lang;
+ document.title = result.title;
+
+ let article = Object.assign(
+ result,
+ {url: new URL(url)},
+ {readingTime: this.getReadingTime(result.length, result.language)},
+ {byline: this.getByline(result)},
+ {dir: this.getTextDirection(result)},
+ {title: this.getTitle(result)}
+ );
+
+ document.body.outerHTML = this.createHtmlBody(article);
+
+ this.setFontSize(options.fontSize);
+ this.setFontType(options.fontType);
+ this.setColorScheme(options.colorScheme);
+ if (options.scrollY) {
+ window.scrollTo({top: options.scrollY, left: 0, behavior: "instant"});
+ }
+ }
+
+ /**
+ * Allows adjusting the font size in discrete steps between ReaderView.MIN_FONT_SIZE
+ * and ReaderView.MAX_FONT_SIZE.
+ *
+ * @param changeAmount e.g. +1, or -1.
+ */
+ changeFontSize(changeAmount) {
+ var size = Math.max(ReaderView.MIN_FONT_SIZE, Math.min(ReaderView.MAX_FONT_SIZE, this.fontSize + changeAmount));
+ this.setFontSize(size);
+ }
+
+ /**
+ * Sets the font size.
+ *
+ * @param fontSize must be value between ReaderView.MIN_FONT_SIZE
+ * and ReaderView.MAX_FONT_SIZE.
+ */
+ setFontSize(fontSize) {
+ let size = (10 + 2 * fontSize) + "px";
+ let readerView = document.getElementById("mozac-readerview-container");
+ readerView.style.setProperty("font-size", size);
+ this.fontSize = fontSize;
+ }
+
+ /**
+ * Sets the font type.
+ *
+ * @param fontType the font type to use.
+ */
+ setFontType(fontType) {
+ let bodyClasses = document.body.classList;
+
+ if (this.fontType) {
+ bodyClasses.remove(this.fontType);
+ }
+
+ this.fontType = fontType;
+ bodyClasses.add(this.fontType);
+ }
+
+ /**
+ * Sets the color scheme.
+ *
+ * @param colorScheme the color scheme to use, must be either light, dark
+ * or sepia.
+ */
+ setColorScheme(colorScheme) {
+ if(!['light', 'sepia', 'dark'].includes(colorScheme)) {
+ console.error(`Invalid color scheme specified: ${colorScheme}`)
+ return;
+ }
+
+ let bodyClasses = document.body.classList;
+
+ if (this.colorScheme) {
+ bodyClasses.remove(this.colorScheme);
+ }
+
+ this.colorScheme = colorScheme;
+ bodyClasses.add(this.colorScheme);
+ }
+
+ /**
+ * Create the reader view HTML body.
+ *
+ * @param article a JSONObject representing the article to show.
+ */
+ createHtmlBody(article) {
+ const safeDir = this.escapeHTML(article.dir);
+ const safeTitle = this.escapeHTML(article.title);
+ const safeByline = this.escapeHTML(article.byline);
+ const safeReadingTime = this.escapeHTML(article.readingTime);
+ return `
+ <body class="mozac-readerview-body">
+ <div id="mozac-readerview-container" class="container" dir="${safeDir}">
+ <div class="header">
+ <a class="domain" href="${article.url.href}">${article.url.hostname}</a>
+ <div class="domain-border"></div>
+ <h1>${safeTitle}</h1>
+ <div class="credits">${safeByline}</div>
+ <div>
+ <div>${safeReadingTime}</div>
+ </div>
+ </div>
+ <hr>
+
+ <div class="content">
+ <div class="mozac-readerview-content">${article.content}</div>
+ </div>
+ </div>
+ </body>
+ `
+ }
+
+ /**
+ * Returns the estimated reading time as localized string.
+ *
+ * @param length of the article (number of chars).
+ * @param optional language of the article, defaults to en.
+ */
+ getReadingTime(length, lang = "en") {
+ const [readingSpeed, readingSpeedLang] = this.getReadingSpeedForLanguage(lang);
+ const charactersPerMinuteLow = readingSpeed.cpm - readingSpeed.variance;
+ const charactersPerMinuteHigh = readingSpeed.cpm + readingSpeed.variance;
+ const readingTimeMinsSlow = Math.ceil(length / charactersPerMinuteLow);
+ const readingTimeMinsFast = Math.ceil(length / charactersPerMinuteHigh);
+
+ // Construct a localized and "humanized" reading time in minutes.
+ // If we have both a fast and slow reading time we'll show both e.g.
+ // "2 - 4 minutes", otherwise we'll just show "4 minutes".
+ try {
+ var parts = new Intl.RelativeTimeFormat(readingSpeedLang).formatToParts(readingTimeMinsSlow, 'minute');
+ if (parts.length == 3) {
+ // No need to use part[0] which represents the literal "in".
+ var readingTime = parts[1].value; // reading time in minutes
+ var minutesLiteral = parts[2].value; // localized singular or plural literal of 'minute'
+ var readingTimeString = `${readingTime} ${minutesLiteral}`;
+ if (readingTimeMinsSlow != readingTimeMinsFast) {
+ readingTimeString = `${readingTimeMinsFast} - ${readingTimeString}`;
+ }
+ return readingTimeString;
+ }
+ }
+ catch(error) {
+ console.error(`Failed to format reading time: ${error}`);
+ }
+
+ return "";
+ }
+
+ /**
+ * Returns the reading speed of a selection of languages with likely variance.
+ *
+ * Reading speed estimated from a study done on reading speeds in various languages.
+ * study can be found here: http://iovs.arvojournals.org/article.aspx?articleid=2166061
+ *
+ * @return object with characters per minute and variance. Defaults to English
+ * if no suitable language is found in the collection.
+ */
+ getReadingSpeedForLanguage(lang) {
+ const readingSpeed = new Map([
+ [ "en", {cpm: 987, variance: 118 } ],
+ [ "ar", {cpm: 612, variance: 88 } ],
+ [ "de", {cpm: 920, variance: 86 } ],
+ [ "es", {cpm: 1025, variance: 127 } ],
+ [ "fi", {cpm: 1078, variance: 121 } ],
+ [ "fr", {cpm: 998, variance: 126 } ],
+ [ "he", {cpm: 833, variance: 130 } ],
+ [ "it", {cpm: 950, variance: 140 } ],
+ [ "jw", {cpm: 357, variance: 56 } ],
+ [ "nl", {cpm: 978, variance: 143 } ],
+ [ "pl", {cpm: 916, variance: 126 } ],
+ [ "pt", {cpm: 913, variance: 145 } ],
+ [ "ru", {cpm: 986, variance: 175 } ],
+ [ "sk", {cpm: 885, variance: 145 } ],
+ [ "sv", {cpm: 917, variance: 156 } ],
+ [ "tr", {cpm: 1054, variance: 156 } ],
+ [ "zh", {cpm: 255, variance: 29 } ],
+ ]);
+
+ return readingSpeed.has(lang) ? [readingSpeed.get(lang), lang] : [readingSpeed.get("en"), "en"];
+ }
+
+ getByline(article) {
+ return article.byline || "";
+ }
+
+ /**
+ * Attempts to read the optional text direction from the article and uses
+ * language mapping to detect rtl, if missing.
+ */
+ getTextDirection(article) {
+ if (article.dir) {
+ return article.dir;
+ }
+
+ if (["ar", "fa", "he", "ug", "ur"].includes(article.language)) {
+ return "rtl";
+ }
+
+ return "ltr";
+ }
+
+ getTitle(article) {
+ return article.title || "";
+ }
+
+ escapeHTML(text) {
+ return text
+ .replace(/\&/g, "&amp;")
+ .replace(/\</g, "&lt;")
+ .replace(/\>/g, "&gt;")
+ .replace(/\"/g, "&quot;")
+ .replace(/\'/g, "&#039;");
+ }
+}
+
+function fetchDocument(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.onerror = evt => reject(evt.error);
+ xhr.responseType = "document";
+ xhr.onload = evt => {
+ if (xhr.status !== 200) {
+ reject("Reader mode XHR failed with status: " + xhr.status);
+ return;
+ }
+ let doc = xhr.responseXML;
+ if (!doc) {
+ reject("Reader mode XHR didn't return a document");
+ return;
+ }
+ resolve(doc);
+ };
+ xhr.send();
+ });
+}
+
+function getPreparedDocument(id, url) {
+ return new Promise((resolve, reject) => {
+
+ browser.runtime.sendMessage({action: "getSerializedDoc", id: id}).then((serializedDoc) => {
+ if (serializedDoc) {
+ let doc = new JSDOMParser().parse(serializedDoc, url);
+ resolve(doc);
+ } else {
+ reject();
+ }
+ }
+ );
+ });
+}
+
+let readerView = new ReaderView();
+connectNativePort();
+prepareBody();
+
+function connectNativePort() {
+ let url = new URL(window.location.href);
+ let articleUrl = url.searchParams.get("url");
+ let id = url.searchParams.get("id");
+ let baseUrl = browser.runtime.getURL("/");
+
+ let port = browser.runtime.connectNative("mozacReaderviewActive");
+ port.onMessage.addListener((message) => {
+ switch (message.action) {
+ case 'show':
+ async function showAsync(options) {
+ try {
+ let doc;
+ if (typeof Promise.any === "function") {
+ doc = await Promise.any([fetchDocument(articleUrl), getPreparedDocument(id, articleUrl)]);
+ } else {
+ try {
+ doc = await getPreparedDocument(id, articleUrl);
+ } catch(e) {
+ doc = await fetchDocument(articleUrl);
+ }
+ }
+ readerView.show(doc, articleUrl, options);
+ } catch(e) {
+ console.log(e);
+ // We weren't able to find the prepared document and also
+ // failed to fetch it. Let's load the original page which
+ // will make sure we show an appropriate error page.
+ window.location.href = articleUrl;
+ }
+ }
+ showAsync(message.value);
+ break;
+ case 'hide':
+ window.location.href = articleUrl;
+ case 'setColorScheme':
+ readerView.setColorScheme(message.value.toLowerCase());
+ break;
+ case 'changeFontSize':
+ readerView.changeFontSize(message.value);
+ break;
+ case 'setFontType':
+ readerView.setFontType(message.value.toLowerCase());
+ break;
+ case 'checkReaderState':
+ port.postMessage({baseUrl: baseUrl, activeUrl: articleUrl, readerable: true});
+ break;
+ default:
+ console.error(`Received invalid action ${message.action}`);
+ }
+ });
+}
+
+/**
+ * Applies the configured color scheme to the HTML body while reader view is loading. This is to
+ * prevent "flashes" caused by having to change the color later.
+ */
+function prepareBody() {
+ let url = new URL(window.location.href);
+ let colorScheme = url.searchParams.get("colorScheme");
+ let body = document.createElement("body");
+ body.classList.add("mozac-readerview-body");
+ body.classList.add(colorScheme);
+ document.body = body;
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewFeature.kt b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewFeature.kt
new file mode 100644
index 0000000000..51d6083a6c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewFeature.kt
@@ -0,0 +1,382 @@
+/* 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.readerview
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.ReaderAction
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.feature.readerview.internal.ReaderViewConfig
+import mozilla.components.feature.readerview.internal.ReaderViewControlsInteractor
+import mozilla.components.feature.readerview.internal.ReaderViewControlsPresenter
+import mozilla.components.feature.readerview.view.ReaderViewControlsView
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
+import mozilla.components.support.webextensions.WebExtensionController
+import org.json.JSONObject
+import java.lang.ref.WeakReference
+import java.net.URLEncoder
+import java.util.Locale
+import java.util.UUID
+
+typealias onReaderViewStatusChange = (available: Boolean, active: Boolean) -> Unit
+typealias UUIDCreator = () -> String
+
+/**
+ * Feature implementation that provides a reader view for the selected
+ * session, based on a web extension.
+ *
+ * @property context a reference to the context.
+ * @property engine a reference to the application's browser engine.
+ * @property store a reference to the application's [BrowserStore].
+ * @param controlsView the view to use to display reader mode controls.
+ * @property onReaderViewStatusChange a callback invoked to indicate whether
+ * or not reader view is available and active for the page loaded by the
+ * currently selected session. The callback will be invoked when a page is
+ * loaded or refreshed, on any navigation (back or forward), and when the
+ * selected session changes.
+ */
+class ReaderViewFeature(
+ private val context: Context,
+ private val engine: Engine,
+ private val store: BrowserStore,
+ controlsView: ReaderViewControlsView,
+ private val createUUID: UUIDCreator = { UUID.randomUUID().toString() },
+ private val onReaderViewStatusChange: onReaderViewStatusChange = { _, _ -> Unit },
+) : LifecycleAwareFeature, UserInteractionHandler {
+
+ private var scope: CoroutineScope? = null
+
+ @VisibleForTesting var readerBaseUrl: String? = null
+
+ @VisibleForTesting
+ // This is an internal var to make it mutable for unit testing purposes only
+ internal var extensionController = WebExtensionController(
+ READER_VIEW_EXTENSION_ID,
+ READER_VIEW_EXTENSION_URL,
+ READER_VIEW_CONTENT_PORT,
+ )
+
+ @VisibleForTesting
+ internal val config = ReaderViewConfig(context) { message ->
+ val engineSession = store.state.selectedTab?.engineState?.engineSession
+ extensionController.sendContentMessage(message, engineSession, READER_VIEW_ACTIVE_CONTENT_PORT)
+ }
+
+ private val controlsPresenter = ReaderViewControlsPresenter(controlsView, config)
+ private val controlsInteractor = ReaderViewControlsInteractor(controlsView, config)
+
+ enum class FontType(val value: String) { SANSSERIF("sans-serif"), SERIF("serif") }
+ enum class ColorScheme { LIGHT, SEPIA, DARK }
+
+ override fun start() {
+ ensureExtensionInstalled()
+
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.tabs }
+ .filterChanged {
+ it.readerState
+ }
+ .collect { tab ->
+ if (tab.readerState.connectRequired) {
+ connectReaderViewContentScript(tab)
+ }
+ if (tab.readerState.checkRequired) {
+ checkReaderState(tab)
+ }
+ if (tab.id == store.state.selectedTabId) {
+ maybeNotifyReaderStatusChange(tab.readerState.readerable, tab.readerState.active)
+ }
+ }
+ }
+
+ controlsInteractor.start()
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ controlsInteractor.stop()
+ }
+
+ override fun onBackPressed(): Boolean {
+ store.state.selectedTab?.let {
+ if (it.readerState.active) {
+ if (controlsPresenter.areControlsVisible()) {
+ hideControls()
+ } else {
+ hideReaderView()
+ }
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Shows the reader view UI.
+ */
+ fun showReaderView(session: TabSessionState? = store.state.selectedTab) {
+ session?.let {
+ if (!it.readerState.active) {
+ val id = createUUID()
+ extensionController.sendContentMessage(
+ createCachePageMessage(id),
+ it.engineState.engineSession,
+ READER_VIEW_CONTENT_PORT,
+ )
+
+ val readerUrl = extensionController.createReaderUrl(it.content.url, id) ?: run {
+ Logger.error("FeatureReaderView unable to create ReaderUrl.")
+ return@let
+ }
+
+ store.dispatch(EngineAction.LoadUrlAction(it.id, readerUrl))
+ store.dispatch(ReaderAction.UpdateReaderActiveAction(it.id, true))
+ }
+ }
+ }
+
+ /**
+ * Hides the reader view UI.
+ */
+ fun hideReaderView(session: TabSessionState? = store.state.selectedTab) {
+ session?.let { it ->
+ if (it.readerState.active) {
+ store.dispatch(ReaderAction.UpdateReaderActiveAction(it.id, false))
+ store.dispatch(ReaderAction.UpdateReaderableAction(it.id, false))
+ store.dispatch(ReaderAction.ClearReaderActiveUrlAction(it.id))
+ if (it.content.canGoBack) {
+ it.engineState.engineSession?.goBack(false)
+ } else {
+ extensionController.sendContentMessage(
+ createHideReaderMessage(),
+ it.engineState.engineSession,
+ READER_VIEW_ACTIVE_CONTENT_PORT,
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows the reader view appearance controls.
+ */
+ fun showControls() {
+ controlsPresenter.show()
+ }
+
+ /**
+ * Hides the reader view appearance controls.
+ */
+ fun hideControls() {
+ controlsPresenter.hide()
+ }
+
+ @VisibleForTesting
+ internal fun checkReaderState(session: TabSessionState? = store.state.selectedTab) {
+ session?.engineState?.engineSession?.let { engineSession ->
+ val message = createCheckReaderStateMessage()
+ if (extensionController.portConnected(engineSession, READER_VIEW_CONTENT_PORT)) {
+ extensionController.sendContentMessage(message, engineSession, READER_VIEW_CONTENT_PORT)
+ }
+ if (extensionController.portConnected(engineSession, READER_VIEW_ACTIVE_CONTENT_PORT)) {
+ extensionController.sendContentMessage(message, engineSession, READER_VIEW_ACTIVE_CONTENT_PORT)
+ }
+ store.dispatch(ReaderAction.UpdateReaderableCheckRequiredAction(session.id, false))
+ }
+ }
+
+ @VisibleForTesting
+ internal fun connectReaderViewContentScript(session: TabSessionState? = store.state.selectedTab) {
+ session?.engineState?.engineSession?.let { engineSession ->
+ extensionController.registerContentMessageHandler(
+ engineSession,
+ ActiveReaderViewContentMessageHandler(store, session.id, WeakReference(config)),
+ READER_VIEW_ACTIVE_CONTENT_PORT,
+ )
+ extensionController.registerContentMessageHandler(
+ engineSession,
+ ReaderViewContentMessageHandler(store, session.id),
+ READER_VIEW_CONTENT_PORT,
+ )
+ store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(session.id, false))
+ }
+ }
+
+ private var lastNotified: Pair<Boolean, Boolean>? = null
+
+ @VisibleForTesting
+ internal fun maybeNotifyReaderStatusChange(readerable: Boolean = false, active: Boolean = false) {
+ // Make sure we only notify the UI if needed (an actual change happened) to prevent
+ // it from unnecessarily invalidating toolbar/menu items.
+ if (lastNotified == null || lastNotified != Pair(readerable, active)) {
+ onReaderViewStatusChange(readerable, active)
+ lastNotified = Pair(readerable, active)
+ }
+ }
+
+ private fun ensureExtensionInstalled() {
+ val feature = WeakReference(this)
+ extensionController.install(
+ engine,
+ onSuccess = {
+ it.getMetadata()?.run {
+ readerBaseUrl = baseUrl
+ } ?: run {
+ Logger.error("ReaderView extension missing Metadata")
+ }
+
+ feature.get()?.connectReaderViewContentScript()
+ },
+ )
+ }
+
+ /**
+ * Handles content messages from regular pages.
+ */
+ private open class ReaderViewContentMessageHandler(
+ protected val store: BrowserStore,
+ protected val sessionId: String,
+ ) : MessageHandler {
+ override fun onPortConnected(port: Port) {
+ port.postMessage(createCheckReaderStateMessage())
+ }
+
+ override fun onPortMessage(message: Any, port: Port) {
+ if (message is JSONObject) {
+ val readerable = message.optBoolean(READERABLE_RESPONSE_MESSAGE_KEY, false)
+ store.dispatch(ReaderAction.UpdateReaderableAction(sessionId, readerable))
+ }
+ }
+ }
+
+ /**
+ * Handles content messages from active reader pages.
+ */
+ private class ActiveReaderViewContentMessageHandler(
+ store: BrowserStore,
+ sessionId: String,
+ // This needs to be a weak reference because the engine session this message handler will be
+ // attached to has a longer lifespan than the feature instance i.e. a tab can remain open,
+ // but we don't want to prevent the feature (and therefore its context/fragment) from
+ // being garbage collected. The config has references to both the context and feature.
+ private val config: WeakReference<ReaderViewConfig>,
+ ) : ReaderViewContentMessageHandler(store, sessionId) {
+
+ override fun onPortMessage(message: Any, port: Port) {
+ super.onPortMessage(message, port)
+
+ if (message is JSONObject) {
+ val baseUrl = message.getString(BASE_URL_RESPONSE_MESSAGE_KEY)
+ store.dispatch(ReaderAction.UpdateReaderBaseUrlAction(sessionId, baseUrl))
+
+ port.postMessage(createShowReaderMessage(config.get(), store.state.selectedTab?.readerState?.scrollY))
+
+ val activeUrl = message.getString(ACTIVE_URL_RESPONSE_MESSAGE_KEY)
+ store.dispatch(ReaderAction.UpdateReaderActiveUrlAction(sessionId, activeUrl))
+ }
+ }
+ }
+
+ private fun WebExtensionController.createReaderUrl(url: String, id: String): String? {
+ val colorScheme = config.colorScheme.name.lowercase(Locale.ROOT)
+ // Encode the original page url, otherwise when the readerview page will try to
+ // parse the url and retrieve the readerview url params (ir and colorScheme)
+ // the parser may get confused because the original webpage url being interpolated
+ // may also include its own search params non-escaped (See Bug 1860490).
+ val encodedUrl = URLEncoder.encode(url, "UTF-8")
+ return readerBaseUrl?.let { it + "readerview.html?url=$encodedUrl&id=$id&colorScheme=$colorScheme" }
+ }
+
+ companion object {
+ private val logger = Logger("ReaderView")
+
+ internal const val READER_VIEW_EXTENSION_ID = "readerview@mozac.org"
+
+ // Name of the port connected to all pages for checking whether or not
+ // a page is readerable (see readerview_content.js).
+ internal const val READER_VIEW_CONTENT_PORT = "mozacReaderview"
+
+ // Name of the port connected to active reader pages for updating
+ // appearance configuration (see readerview.js).
+ internal const val READER_VIEW_ACTIVE_CONTENT_PORT = "mozacReaderviewActive"
+ internal const val READER_VIEW_EXTENSION_URL = "resource://android/assets/extensions/readerview/"
+
+ // Constants for building messages sent to the web extension:
+ // Change the font type: {"action": "setFontType", "value": "sans-serif"}
+ // Show reader view: {"action": "show", "value": {"fontSize": 3, "fontType": "serif", "colorScheme": "dark"}}
+ internal const val ACTION_MESSAGE_KEY = "action"
+ internal const val ACTION_CACHE_PAGE = "cachePage"
+ internal const val ACTION_SHOW = "show"
+ internal const val ACTION_HIDE = "hide"
+ internal const val ACTION_CHECK_READER_STATE = "checkReaderState"
+ internal const val ACTION_SET_COLOR_SCHEME = "setColorScheme"
+ internal const val ACTION_CHANGE_FONT_SIZE = "changeFontSize"
+ internal const val ACTION_SET_FONT_TYPE = "setFontType"
+ internal const val ACTION_VALUE = "value"
+ internal const val ACTION_VALUE_SHOW_FONT_SIZE = "fontSize"
+ internal const val ACTION_VALUE_SHOW_FONT_TYPE = "fontType"
+ internal const val ACTION_VALUE_SHOW_COLOR_SCHEME = "colorScheme"
+ internal const val ACTION_VALUE_SCROLLY = "scrollY"
+ internal const val ACTION_VALUE_ID = "id"
+ internal const val READERABLE_RESPONSE_MESSAGE_KEY = "readerable"
+ internal const val BASE_URL_RESPONSE_MESSAGE_KEY = "baseUrl"
+ internal const val ACTIVE_URL_RESPONSE_MESSAGE_KEY = "activeUrl"
+
+ // Constants for storing the reader mode config in shared preferences
+ internal const val SHARED_PREF_NAME = "mozac_feature_reader_view"
+ internal const val COLOR_SCHEME_KEY = "mozac-readerview-colorscheme"
+ internal const val FONT_TYPE_KEY = "mozac-readerview-fonttype"
+ internal const val FONT_SIZE_KEY = "mozac-readerview-fontsize"
+ internal const val FONT_SIZE_DEFAULT = 3
+
+ internal fun createCheckReaderStateMessage(): JSONObject {
+ return JSONObject().put(ACTION_MESSAGE_KEY, ACTION_CHECK_READER_STATE)
+ }
+
+ internal fun createCachePageMessage(id: String): JSONObject {
+ return JSONObject()
+ .put(ACTION_MESSAGE_KEY, ACTION_CACHE_PAGE)
+ .put(ACTION_VALUE_ID, id)
+ }
+
+ internal fun createShowReaderMessage(config: ReaderViewConfig?, scrollY: Int? = null): JSONObject {
+ if (config == null) {
+ logger.warn("No config provided. Falling back to default values.")
+ }
+
+ val fontSize = config?.fontSize ?: FONT_SIZE_DEFAULT
+ val fontType = config?.fontType ?: FontType.SERIF
+ val colorScheme = config?.colorScheme ?: ColorScheme.LIGHT
+ val configJson = JSONObject()
+ .put(ACTION_VALUE_SHOW_FONT_SIZE, fontSize)
+ .put(ACTION_VALUE_SHOW_FONT_TYPE, fontType.value.lowercase(Locale.ROOT))
+ .put(ACTION_VALUE_SHOW_COLOR_SCHEME, colorScheme.name.lowercase(Locale.ROOT))
+ if (scrollY != null) {
+ configJson.put(ACTION_VALUE_SCROLLY, scrollY)
+ }
+ return JSONObject()
+ .put(ACTION_MESSAGE_KEY, ACTION_SHOW)
+ .put(ACTION_VALUE, configJson)
+ }
+
+ internal fun createHideReaderMessage(): JSONObject {
+ return JSONObject().put(ACTION_MESSAGE_KEY, ACTION_HIDE)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewMiddleware.kt b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewMiddleware.kt
new file mode 100644
index 0000000000..b5d602553d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/ReaderViewMiddleware.kt
@@ -0,0 +1,134 @@
+/* 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.readerview
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.ReaderAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.READER_VIEW_CONTENT_PORT
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.READER_VIEW_EXTENSION_ID
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.READER_VIEW_EXTENSION_URL
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.webextensions.WebExtensionController
+
+/**
+ * [Middleware] implementation for translating [BrowserAction]s to
+ * [ReaderAction]s (e.g. if the URL is updated a new "readerable"
+ * check should be executed.)
+ */
+class ReaderViewMiddleware : Middleware<BrowserState, BrowserAction> {
+
+ @VisibleForTesting
+ internal var extensionController = WebExtensionController(
+ READER_VIEW_EXTENSION_ID,
+ READER_VIEW_EXTENSION_URL,
+ READER_VIEW_CONTENT_PORT,
+ )
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ if (preProcess(context, action)) {
+ next(action)
+ postProcess(context, action)
+ }
+ }
+
+ /**
+ * Processes the action before it is dispatched to the store.
+ *
+ * @param context a reference to the [MiddlewareContext].
+ * @param action the action to process.
+ * @return true if the original action should be processed, otherwise false.
+ */
+ private fun preProcess(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ action: BrowserAction,
+ ): Boolean {
+ return when (action) {
+ // We want to bind the feature instance to the lifecycle of the browser
+ // fragment. So it won't necessarily be active when a tab is removed
+ // (e.g. via a tabs tray fragment). In order to disconnect the port as
+ // early as possible it's best to do it here directly.
+ is EngineAction.UnlinkEngineSessionAction -> {
+ context.state.findTab(action.tabId)?.engineState?.engineSession?.let {
+ extensionController.disconnectPort(it, READER_VIEW_EXTENSION_ID)
+ }
+ true
+ }
+ is ContentAction.UpdateUrlAction -> {
+ // Activate reader view when navigating to a reader page and deactivate it
+ // when navigating away. In addition, we want to mask moz-extension://
+ // URLs in the toolbar. So, if we detect the URL is coming from our
+ // extension we show the original URL instead. This is needed until
+ // we have a solution for:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1550144
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1322304
+ // https://github.com/mozilla-mobile/android-components/issues/2879
+ val tab = context.state.findTab(action.sessionId)
+ if (isReaderUrl(tab, action.url)) {
+ val urlReplaced = tab?.readerState?.activeUrl?.let { activeUrl ->
+ context.dispatch(ContentAction.UpdateUrlAction(action.sessionId, activeUrl))
+ true
+ } ?: false
+ context.dispatch(ReaderAction.UpdateReaderActiveAction(action.sessionId, true))
+ !urlReplaced
+ } else {
+ if (action.url != tab?.readerState?.activeUrl) {
+ context.dispatch(ReaderAction.UpdateReaderActiveAction(action.sessionId, false))
+ context.dispatch(ReaderAction.UpdateReaderableAction(action.sessionId, false))
+ context.dispatch(ReaderAction.UpdateReaderableCheckRequiredAction(action.sessionId, true))
+ context.dispatch(ReaderAction.ClearReaderActiveUrlAction(action.sessionId))
+ }
+ true
+ }
+ }
+ else -> true
+ }
+ }
+
+ private fun postProcess(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is TabListAction.SelectTabAction -> {
+ context.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(action.tabId, true))
+ context.dispatch(ReaderAction.UpdateReaderableAction(action.tabId, false))
+ context.dispatch(ReaderAction.UpdateReaderableCheckRequiredAction(action.tabId, true))
+ }
+ is EngineAction.LinkEngineSessionAction -> {
+ context.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(action.tabId, true))
+ }
+ is ReaderAction.UpdateReaderActiveUrlAction -> {
+ // When a tab is restored, the reader page will connect, but we won't get a
+ // UpdateUrlAction. We still want to mask the moz-extension:// URL though
+ // so we update the URL here. See comment on handling UpdateUrlAction.
+ val tab = context.state.findTab(action.tabId)
+ val url = tab?.content?.url
+ if (url != null && isReaderUrl(tab, url)) {
+ context.dispatch(ContentAction.UpdateUrlAction(tab.id, url))
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+
+ private fun isReaderUrl(tab: TabSessionState?, url: String): Boolean {
+ val readerViewBaseUrl = tab?.readerState?.baseUrl
+ return readerViewBaseUrl != null && url.startsWith(readerViewBaseUrl)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewConfig.kt b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewConfig.kt
new file mode 100644
index 0000000000..561d63e0e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewConfig.kt
@@ -0,0 +1,108 @@
+/* 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.readerview.internal
+
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.content.res.Configuration
+import mozilla.components.feature.readerview.ReaderViewFeature
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.ACTION_CHANGE_FONT_SIZE
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.ACTION_MESSAGE_KEY
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.ACTION_SET_FONT_TYPE
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.ACTION_VALUE
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.COLOR_SCHEME_KEY
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.FONT_SIZE_DEFAULT
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.FONT_SIZE_KEY
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.FONT_TYPE_KEY
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.SHARED_PREF_NAME
+import org.json.JSONObject
+
+/**
+ * Stores the user configuration for reader view in shared prefs.
+ * All values are initialized lazily and cached.
+ * @param context Used to lazily obtain shared preferences and to check dark mode status.
+ * @param sendConfigMessage If the config changes, this method will be invoked
+ * with a JSON object which should be sent to the content script so the new
+ * config can be applied.
+ */
+internal class ReaderViewConfig(
+ context: Context,
+ private val sendConfigMessage: (JSONObject) -> Unit,
+) {
+
+ private val prefs by lazy { context.getSharedPreferences(SHARED_PREF_NAME, MODE_PRIVATE) }
+ private val resources = context.resources
+ private var colorSchemeCache: ReaderViewFeature.ColorScheme? = null
+ private var fontTypeCache: ReaderViewFeature.FontType? = null
+ private var fontSizeCache: Int? = null
+
+ var colorScheme: ReaderViewFeature.ColorScheme
+ get() {
+ if (colorSchemeCache == null) {
+ // Default to a dark theme if either the system or local dark theme is active
+ val defaultColor = if (isNightMode()) {
+ ReaderViewFeature.ColorScheme.DARK
+ } else {
+ ReaderViewFeature.ColorScheme.LIGHT
+ }
+ colorSchemeCache = getEnumFromPrefs(COLOR_SCHEME_KEY, defaultColor)
+ }
+ return colorSchemeCache!!
+ }
+ set(value) {
+ if (colorSchemeCache != value) {
+ colorSchemeCache = value
+ prefs.edit().putString(COLOR_SCHEME_KEY, value.name).apply()
+ sendMessage(ReaderViewFeature.ACTION_SET_COLOR_SCHEME) { put(ACTION_VALUE, value.name) }
+ }
+ }
+
+ var fontType: ReaderViewFeature.FontType
+ get() {
+ if (fontTypeCache == null) {
+ fontTypeCache = getEnumFromPrefs(FONT_TYPE_KEY, ReaderViewFeature.FontType.SERIF)
+ }
+ return fontTypeCache!!
+ }
+ set(value) {
+ if (fontTypeCache != value) {
+ fontTypeCache = value
+ prefs.edit().putString(FONT_TYPE_KEY, value.name).apply()
+ sendMessage(ACTION_SET_FONT_TYPE) { put(ACTION_VALUE, value.value) }
+ }
+ }
+
+ var fontSize: Int
+ get() {
+ if (fontSizeCache == null) {
+ fontSizeCache = prefs.getInt(FONT_SIZE_KEY, FONT_SIZE_DEFAULT)
+ }
+ return fontSizeCache!!
+ }
+ set(value) {
+ if (fontSizeCache != value) {
+ val diff = value - fontSize
+ fontSizeCache = value
+ prefs.edit().putInt(FONT_SIZE_KEY, value).apply()
+ sendMessage(ACTION_CHANGE_FONT_SIZE) { put(ACTION_VALUE, diff) }
+ }
+ }
+
+ private inline fun <reified T : Enum<T>> getEnumFromPrefs(key: String, default: T): T {
+ val enumName = prefs.getString(key, default.name) ?: default.name
+ return enumValueOf(enumName)
+ }
+
+ private fun isNightMode(): Boolean {
+ val darkFlag = resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)
+ return darkFlag == Configuration.UI_MODE_NIGHT_YES
+ }
+
+ private inline fun sendMessage(action: String, crossinline builder: JSONObject.() -> Unit) {
+ val message = JSONObject().put(ACTION_MESSAGE_KEY, action)
+ builder(message)
+ sendConfigMessage(message)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewControlsInteractor.kt b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewControlsInteractor.kt
new file mode 100644
index 0000000000..59b0f0d3d5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewControlsInteractor.kt
@@ -0,0 +1,51 @@
+/* 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.readerview.internal
+
+import mozilla.components.feature.readerview.ReaderViewFeature
+import mozilla.components.feature.readerview.ReaderViewFeature.ColorScheme
+import mozilla.components.feature.readerview.ReaderViewFeature.FontType
+import mozilla.components.feature.readerview.view.MAX_TEXT_SIZE
+import mozilla.components.feature.readerview.view.MIN_TEXT_SIZE
+import mozilla.components.feature.readerview.view.ReaderViewControlsView
+
+/**
+ * Interactor that implements [ReaderViewControlsView.Listener] and notifies the feature about actions the user
+ * performed via the [ReaderViewFeature.Config] (e.g. "font changed").
+ */
+internal class ReaderViewControlsInteractor(
+ private val view: ReaderViewControlsView,
+ private val config: ReaderViewConfig,
+) : ReaderViewControlsView.Listener {
+ fun start() {
+ view.listener = this
+ }
+
+ fun stop() {
+ view.listener = null
+ }
+
+ override fun onFontChanged(font: FontType) {
+ config.fontType = font
+ }
+
+ override fun onFontSizeIncreased(): Int {
+ if (config.fontSize < MAX_TEXT_SIZE) {
+ config.fontSize += 1
+ }
+ return config.fontSize
+ }
+
+ override fun onFontSizeDecreased(): Int {
+ if (config.fontSize > MIN_TEXT_SIZE) {
+ config.fontSize -= 1
+ }
+ return config.fontSize
+ }
+
+ override fun onColorSchemeChanged(scheme: ColorScheme) {
+ config.colorScheme = scheme
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewControlsPresenter.kt b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewControlsPresenter.kt
new file mode 100644
index 0000000000..89fa48fc44
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/internal/ReaderViewControlsPresenter.kt
@@ -0,0 +1,43 @@
+/* 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.readerview.internal
+
+import androidx.core.view.isVisible
+import mozilla.components.feature.readerview.view.ReaderViewControlsView
+
+/**
+ * Presenter implementation that will update the view whenever the feature is started.
+ */
+internal class ReaderViewControlsPresenter(
+ private val view: ReaderViewControlsView,
+ private val config: ReaderViewConfig,
+) {
+ /**
+ * Sets the initial state of the ReaderView controls and makes the controls visible.
+ */
+ fun show() {
+ view.apply {
+ tryInflate()
+ setColorScheme(config.colorScheme)
+ setFont(config.fontType)
+ setFontSize(config.fontSize)
+ showControls()
+ }
+ }
+
+ /**
+ * Checks whether or not the ReaderView controls are visible.
+ */
+ fun areControlsVisible(): Boolean {
+ return view.asView().isVisible
+ }
+
+ /**
+ * Hides the controls.
+ */
+ fun hide() {
+ view.hideControls()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/view/ReaderViewControlsBar.kt b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/view/ReaderViewControlsBar.kt
new file mode 100644
index 0000000000..3071b58323
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/view/ReaderViewControlsBar.kt
@@ -0,0 +1,174 @@
+/* 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.readerview.view
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.View
+import android.widget.RadioGroup
+import androidx.annotation.IdRes
+import androidx.appcompat.widget.AppCompatButton
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.feature.readerview.R
+import mozilla.components.feature.readerview.ReaderViewFeature.ColorScheme
+import mozilla.components.feature.readerview.ReaderViewFeature.FontType
+
+const val MAX_TEXT_SIZE = 9
+const val MIN_TEXT_SIZE = 1
+
+/**
+ * A customizable ReaderView control bar implementing [ReaderViewControlsView].
+ */
+class ReaderViewControlsBar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr), ReaderViewControlsView {
+
+ override var listener: ReaderViewControlsView.Listener? = null
+
+ private lateinit var fontIncrementButton: AppCompatButton
+ private lateinit var fontDecrementButton: AppCompatButton
+ private lateinit var fontGroup: RadioGroup
+ private lateinit var colorSchemeGroup: RadioGroup
+
+ private var view: View? = null
+
+ init {
+ isFocusableInTouchMode = true
+ isClickable = true
+ }
+
+ /**
+ * Sets the font type of the current and future ReaderView sessions.
+ *
+ * @param font The applicable font types available.
+ */
+ override fun setFont(font: FontType) {
+ val selected = when (font) {
+ FontType.SERIF -> R.id.mozac_feature_readerview_font_serif
+ FontType.SANSSERIF -> R.id.mozac_feature_readerview_font_sans_serif
+ }
+ fontGroup.check(selected)
+ }
+
+ /**
+ * Sets the font size of the current and future ReaderView sessions.
+ *
+ * Note: The readerview.js implementation under the hood scales the entire page's contents and not just
+ * the text size.
+ *
+ * @param size An integer value in the range [MIN_TEXT_SIZE] to [MAX_TEXT_SIZE].
+ */
+ override fun setFontSize(size: Int) {
+ val (incrementState, decrementState) = when {
+ size <= MIN_TEXT_SIZE -> {
+ Pair(first = true, second = false)
+ }
+ size >= MAX_TEXT_SIZE -> {
+ Pair(first = false, second = true)
+ }
+ else -> {
+ Pair(first = true, second = true)
+ }
+ }
+ fontIncrementButton.isEnabled = incrementState
+ fontDecrementButton.isEnabled = decrementState
+ }
+
+ /**
+ * Sets the color scheme of the current and future ReaderView sessions.
+ *
+ * @param scheme The applicable colour schemes available.
+ */
+ override fun setColorScheme(scheme: ColorScheme) {
+ val selected = when (scheme) {
+ ColorScheme.DARK -> R.id.mozac_feature_readerview_color_dark
+ ColorScheme.SEPIA -> R.id.mozac_feature_readerview_color_sepia
+ ColorScheme.LIGHT -> R.id.mozac_feature_readerview_color_light
+ }
+
+ colorSchemeGroup.check(selected)
+ }
+
+ /**
+ * Updates visibility to [View.VISIBLE] and requests focus for the UI controls.
+ */
+ override fun showControls() {
+ visibility = View.VISIBLE
+ requestFocus()
+ }
+
+ /**
+ * Updates visibility to [View.GONE] of the UI controls.
+ */
+ override fun hideControls() {
+ visibility = View.GONE
+ }
+
+ /**
+ * Tries to inflate the view if needed.
+ *
+ * See: https://github.com/mozilla-mobile/android-components/issues/5491
+ *
+ * @return true if the inflation was completed, false if the view was already inflated.
+ */
+ override fun tryInflate(): Boolean {
+ return if (view == null) {
+ view = View.inflate(context, R.layout.mozac_feature_readerview_view, this)
+ bindViews()
+ true
+ } else {
+ false
+ }
+ }
+
+ override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
+ if (!gainFocus) {
+ hideControls()
+ }
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
+ }
+
+ @Suppress("ComplexMethod")
+ private fun bindViews() {
+ fontGroup = applyCheckedListener(R.id.mozac_feature_readerview_font_group) { checkedId ->
+ val fontType = when (checkedId) {
+ R.id.mozac_feature_readerview_font_sans_serif -> FontType.SANSSERIF
+ R.id.mozac_feature_readerview_font_serif -> FontType.SERIF
+ else -> FontType.SERIF
+ }
+ listener?.onFontChanged(fontType)
+ }
+ colorSchemeGroup = applyCheckedListener(R.id.mozac_feature_readerview_color_scheme_group) { checkedId ->
+ val colorSchemeChoice = when (checkedId) {
+ R.id.mozac_feature_readerview_color_dark -> ColorScheme.DARK
+ R.id.mozac_feature_readerview_color_sepia -> ColorScheme.SEPIA
+ R.id.mozac_feature_readerview_color_light -> ColorScheme.LIGHT
+ else -> ColorScheme.DARK
+ }
+ listener?.onColorSchemeChanged(colorSchemeChoice)
+ }
+ fontIncrementButton = applyClickListener(R.id.mozac_feature_readerview_font_size_increase) {
+ listener?.onFontSizeIncreased()?.let { setFontSize(it) }
+ }
+ fontDecrementButton = applyClickListener(R.id.mozac_feature_readerview_font_size_decrease) {
+ listener?.onFontSizeDecreased()?.let { setFontSize(it) }
+ }
+ }
+
+ private inline fun applyClickListener(@IdRes id: Int, crossinline block: () -> Unit): AppCompatButton {
+ return findViewById<AppCompatButton>(id).apply {
+ setOnClickListener { block() }
+ }
+ }
+
+ private inline fun applyCheckedListener(@IdRes id: Int, crossinline block: (Int) -> Unit): RadioGroup {
+ return findViewById<RadioGroup>(id).apply {
+ setOnCheckedChangeListener { _, checkedId -> block(checkedId) }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/view/ReaderViewControlsView.kt b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/view/ReaderViewControlsView.kt
new file mode 100644
index 0000000000..566566e51f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/java/mozilla/components/feature/readerview/view/ReaderViewControlsView.kt
@@ -0,0 +1,63 @@
+/* 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.readerview.view
+
+import android.view.View
+import mozilla.components.feature.readerview.ReaderViewFeature.ColorScheme
+import mozilla.components.feature.readerview.ReaderViewFeature.FontType
+
+/**
+ * An interface for views that can display ReaderView appearance controls (e.g. font size, font type).
+ */
+interface ReaderViewControlsView {
+
+ var listener: Listener?
+
+ /**
+ * Sets the selected font option.
+ */
+ fun setFont(font: FontType)
+
+ /**
+ * Sets the selected font size.
+ */
+ fun setFontSize(size: Int)
+
+ /**
+ * Sets the selected color scheme.
+ */
+ fun setColorScheme(scheme: ColorScheme)
+
+ /**
+ * Makes the UI controls visible and requests focus.
+ */
+ fun showControls()
+
+ /**
+ * Makes the UI controls invisible.
+ */
+ fun hideControls()
+
+ /**
+ * Casts this [ReaderViewControlsView] interface to an actual Android [View] object.
+ */
+ fun asView(): View = (this as View)
+
+ /**
+ * Tries to inflate the view if needed.
+ *
+ * See: https://github.com/mozilla-mobile/android-components/issues/5491
+ *
+ * @return true if the inflation was completed, false if the view was already inflated.
+ */
+ fun tryInflate(): Boolean
+
+ interface Listener {
+ fun onFontChanged(font: FontType)
+ fun onFontSizeIncreased(): Int
+ fun onFontSizeDecreased(): Int
+ fun onColorSchemeChanged(scheme: ColorScheme)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/fontsize_controls_text_selector.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/fontsize_controls_text_selector.xml
new file mode 100644
index 0000000000..0e7b7319fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/fontsize_controls_text_selector.xml
@@ -0,0 +1,11 @@
+<?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/.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="true" android:color="@color/mozac_feature_readerview_text_color" />
+ <item android:state_enabled="false" android:color="@color/mozac_feature_readerview_text_color_disabled" />
+</selector> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_dark_selector.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_dark_selector.xml
new file mode 100644
index 0000000000..0373fe5973
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_dark_selector.xml
@@ -0,0 +1,24 @@
+<?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/.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true">
+ <shape>
+ <gradient android:angle="270" />
+ <stroke android:width="2dp" android:color="@color/mozac_feature_readerview_selected"/>
+ <corners android:radius="4dp" />
+ <solid android:color="@color/mozac_feature_readerview_dark_background"/>
+ </shape>
+ </item>
+ <item android:state_checked="false">
+ <shape>
+ <gradient android:angle="270" />
+ <corners android:radius="4dp" />
+ <solid android:color="@color/mozac_feature_readerview_dark_background"/>
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_light_selector.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_light_selector.xml
new file mode 100644
index 0000000000..108205802d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_light_selector.xml
@@ -0,0 +1,30 @@
+<?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/.
+ -->
+
+<!--
+ ~ 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/.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true">
+ <shape>
+ <gradient android:angle="270" />
+ <stroke android:width="2dp" android:color="@color/mozac_feature_readerview_selected"/>
+ <corners android:radius="4dp" />
+ <solid android:color="@color/mozac_feature_readerview_light_background"/>
+ </shape>
+ </item>
+ <item android:state_checked="false">
+ <shape>
+ <gradient android:angle="270" />
+ <corners android:radius="4dp" />
+ <solid android:color="@color/mozac_feature_readerview_light_background"/>
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_sepia_selector.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_sepia_selector.xml
new file mode 100644
index 0000000000..a10f0c8e24
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_color_scheme_sepia_selector.xml
@@ -0,0 +1,24 @@
+<?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/.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true">
+ <shape>
+ <gradient android:angle="270" />
+ <stroke android:width="2dp" android:color="@color/mozac_feature_readerview_selected"/>
+ <corners android:radius="4dp" />
+ <solid android:color="@color/mozac_feature_readerview_sepia_background"/>
+ </shape>
+ </item>
+ <item android:state_checked="false">
+ <shape>
+ <gradient android:angle="270" />
+ <corners android:radius="4dp" />
+ <solid android:color="@color/mozac_feature_readerview_sepia_background"/>
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_selected_text_selector.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_selected_text_selector.xml
new file mode 100644
index 0000000000..16854bbbc6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/drawable/radiobutton_selected_text_selector.xml
@@ -0,0 +1,11 @@
+<?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/.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:color="@color/mozac_feature_readerview_selected" />
+ <item android:state_checked="false" android:color="@color/mozac_feature_readerview_text_color" />
+</selector> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/layout/mozac_feature_readerview_view.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/layout/mozac_feature_readerview_view.xml
new file mode 100644
index 0000000000..c42e91d263
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/layout/mozac_feature_readerview_view.xml
@@ -0,0 +1,155 @@
+<?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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:background="#ffffffff"
+ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+ <RadioGroup
+ android:id="@+id/mozac_feature_readerview_font_group"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minHeight="66dp"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:layout_marginEnd="8dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <androidx.appcompat.widget.AppCompatRadioButton
+ android:id="@+id/mozac_feature_readerview_font_sans_serif"
+ style="@style/RadioButtonSelectedTextStyle"
+ android:text="@string/mozac_feature_readerview_sans_serif_font"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:fontFamily="sans-serif"
+ android:textAlignment="center"
+ android:contentDescription="@string/mozac_feature_readerview_sans_serif_font_desc"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:layout_weight="1"
+ app:autoSizeMaxTextSize="24sp"
+ app:autoSizeMinTextSize="14sp"
+ app:autoSizeStepGranularity="2sp"
+ app:autoSizeTextType="uniform" />
+
+ <androidx.appcompat.widget.AppCompatRadioButton
+ android:id="@+id/mozac_feature_readerview_font_serif"
+ style="@style/RadioButtonSelectedTextStyle"
+ android:text="@string/mozac_feature_readerview_serif_font"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:fontFamily="serif"
+ android:textAlignment="center"
+ android:contentDescription="@string/mozac_feature_readerview_serif_font_desc"
+ android:layout_marginEnd="8dp"
+ android:layout_marginStart="8dp"
+ android:layout_weight="1"
+ app:autoSizeMaxTextSize="24sp"
+ app:autoSizeMinTextSize="14sp"
+ app:autoSizeStepGranularity="2sp"
+ app:autoSizeTextType="uniform" />
+ </RadioGroup>
+
+ <androidx.appcompat.widget.AppCompatButton
+ android:id="@+id/mozac_feature_readerview_font_size_decrease"
+ android:text="@string/mozac_feature_readerview_negative_sign"
+ android:textAlignment="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:contentDescription="@string/mozac_feature_readerview_font_size_decrease_desc"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:textColor="@drawable/fontsize_controls_text_selector"
+ android:textSize="50sp"
+ android:fontFamily="sans-serif-light"
+ android:gravity="center"
+ app:layout_constraintEnd_toStartOf="@+id/mozac_feature_readerview_font_size_increase"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/mozac_feature_readerview_font_group" />
+
+ <androidx.appcompat.widget.AppCompatButton
+ android:id="@+id/mozac_feature_readerview_font_size_increase"
+ android:text="@string/mozac_feature_readerview_positive_sign"
+ android:textAlignment="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:layout_marginStart="8dp"
+ android:contentDescription="@string/mozac_feature_readerview_font_size_increase_desc"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:textColor="@drawable/fontsize_controls_text_selector"
+ android:textSize="50sp"
+ android:fontFamily="sans-serif-light"
+ android:gravity="center"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/mozac_feature_readerview_font_size_decrease"
+ app:layout_constraintTop_toBottomOf="@+id/mozac_feature_readerview_font_group" />
+
+ <RadioGroup
+ android:id="@+id/mozac_feature_readerview_color_scheme_group"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/mozac_feature_readerview_font_size_increase">
+
+ <androidx.appcompat.widget.AppCompatRadioButton
+ android:id="@+id/mozac_feature_readerview_color_dark"
+ style="@style/RadioButtonSelectedBorderStyle"
+ android:background="@drawable/radiobutton_color_scheme_dark_selector"
+ android:text="@string/mozac_feature_readerview_dark"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textColor="#F9F9FB"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:layout_weight="1"
+ android:textAlignment="center"
+ android:contentDescription="@string/mozac_feature_readerview_dark_color_scheme_desc" />
+
+ <androidx.appcompat.widget.AppCompatRadioButton
+ android:id="@+id/mozac_feature_readerview_color_sepia"
+ style="@style/RadioButtonSelectedBorderStyle"
+ android:background="@drawable/radiobutton_color_scheme_sepia_selector"
+ android:text="@string/mozac_feature_readerview_sephia"
+ android:layout_width="0dp"
+ android:textColor="#220033"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:layout_marginStart="8dp"
+ android:layout_weight="1"
+ android:textAlignment="center"
+ android:contentDescription="@string/mozac_feature_readerview_sepia_color_scheme_desc" />
+
+ <androidx.appcompat.widget.AppCompatRadioButton
+ android:id="@+id/mozac_feature_readerview_color_light"
+ style="@style/RadioButtonSelectedBorderStyle"
+ android:background="@drawable/radiobutton_color_scheme_light_selector"
+ android:text="@string/mozac_feature_readerview_light"
+ android:textColor="#220033"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:layout_weight="1"
+ android:textAlignment="center"
+ android:contentDescription="@string/mozac_feature_readerview_light_color_scheme_desc" />
+ </RadioGroup>
+</merge> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..bf76298450
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-am/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">ሳንሰ ሰሪፍ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">ሳንሰ ሰሪፍ ቅርጸ-ቁምፊ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">ሰሪፍ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">ሰሪፍ ቅርጸ-ቁምፊ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">የቅርጸ ቁምፊ መጠን ይቀንሳል</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">የቅርጸ ቁምፊ መጠን መጨመር</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">ጠቆር ያለ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ጠቆር ያለ ቀለም ንድፍ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">ቀላ ያለ ቡናማ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">ቀላ ያለ ቡናማ ቀለም ንድፍ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ፈካ ያለ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ፈካ ያለ ቀለም ንድፍ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..9a65c91b12
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-an/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fuent Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fuent Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Reducción de grandaria d’a fuent</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumento de grandaria d’a fuent</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Fosco</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de colors foscas</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de colors sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de colors claras</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..491da6f725
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ar/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">غير مذيّل</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">خط غير مذيّل</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">مذيّل</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">خط مذيّل</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">إنقاص حجم الخط</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">زيادة حجم الخط</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">داكنة</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">مخطّط ألوان داكن</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">بني داكن</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">مخطّط ألوان بني داكن</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">فاتح</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">مخطّط ألوان فاتح</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..26d8c7449e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ast/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fonte Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fonte Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Amenorgamientu del tamañu de la fonte</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumentu del tamañu de la fonte</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Escuridá</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de colores escuros</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de colores sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claridá</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de colores claros</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..8b9781fa9e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-az/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif şrifti</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif şrifti</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Şrift ölçüsünü balacalaşdırma</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Şrift ölçüsünü böyütmə</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tünd</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Tünd rəng sxemi</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepiya</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepiya rəng sxemi</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Açıq</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Açıq rəng sxemi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..6f89a979db
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-azb/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">سریف‌سیز</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">سریف‌سیز فونت</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">سریف‌لی</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">سریف‌لی فونت</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">فونت اؤلچوسونون آزالماسی</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">فونت اؤلچوسونون چوخالماسی</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">قارانلیق</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">قارانلیق رنگ طرحی</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">تورپاق</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">تورپاق رنگی طرحی</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">آچیق</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">آچیق رنگ طرحی</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..41cd944353
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-be/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Без засечак</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Шрыфт без засечак</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">З засечкамі</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Шрыфт з засечкамі</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Паменшыць памер шрыфту</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Павялічыць памер шрыфту</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Цёмная</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Цёмная каляровая тэма</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Сэпія</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Каляровая тэма сепія</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Светлая</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Светлая каляровая тэма</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..0796e1d644
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-bg/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Несерифен</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Несерифен шрифт</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Серифен</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Серифен шрифт</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Намаляване на размера на шрифта</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Увеличаване на размера на шрифта</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Тъмен</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Тъмна цветова схема</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Сепия</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Цветна схема на Сепия</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Светла</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Светла цветова схема</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..01f8fc4796
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-bn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif হরফ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif হরফ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">হরফের আকার হ্রাস করুন</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">হরফের আকার বৃদ্ধি করুন</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">গাঢ়</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">গাঢ় রঙের নকশা</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">কালচে-বাদামী রং</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">কালচে-বাদামী রঙের নকশা</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">হালকা</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">হালকা রঙের স্কিম</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..e976c00d45
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-br/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Hep Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Nodrezh hep serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Nodrezh serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Digresk ment an nodrezh</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Kresk ment an nodrezh</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Teñval</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Livioù teñval</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Livioù sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Sklaer</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Livioù sklaer</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..8d40e36b20
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-bs/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Smanjenje veličine fonta</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Povećanje veličine fonta</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tamna</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Tamna shema boja</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia shema boja</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Svijetla</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Svijetla shema boja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..5c9c864d27
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ca/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Lletra Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Lletra Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Disminució de la mida de la lletra</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Augment de la mida de la lletra</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Fosc</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de colors foscos</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sèpia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de color sèpia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Clar</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de colors clars</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..650ab70746
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-cak/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fuente Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fuente Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Tich\'utinisäx rupalem tz\'ib\'</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Tinimirisäx rupalem tz\'ib\'</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Q\'equ\'m</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Q\'equ\'m rub\'onil chib\'äl</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepya</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepya rub\'onil chib\'äl</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Yuk\'unel</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">yuk\'unel rub\'onil chib\'äl</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..7f18dfd315
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif nga font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif nga font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Pagamyon ang font</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Padakoon ang font</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dark</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Dark nga color scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia nga color scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Light</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Light nga color scheme</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..01fe719a52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">جۆرەپیتی Sans Serif </string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">جۆرەپیتی Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">کەمکردنەوەی قەبارەی جۆرەپیت(فۆنت)</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">گەورەکردنەوەی قەبارەی جۆرەپیت(فۆنت)</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">تاریک</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">شێوە ڕەنگی تاریک</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">خۆڵەمێشی تۆخ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">شێوە ڕەنگی خۆڵەمێشی تۆخ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ڕوون</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">شێوە ڕەنگی ڕوون</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..d16f5dde27
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-co/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Senza patta</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Grafia senza patta</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Patta (Serif)</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Grafia cù patta</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Riduzzione di a dimensione di a grafia</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Crescita di a dimensione di a grafia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Scuru</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Ghjocu di culori scuri</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Ghjocu di culori sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Chjaru</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Ghjocu di culori chjari</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..ecf4cad954
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-cs/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Bezpatkové</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Bezpatkové písmo</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Patkové</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Patkové písmo</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Zmenšení velikosti písma</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Zvětšení velikosti písma</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tmavé</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Tmavé barvy</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sépiové</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sépiové barvy</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Světlé</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Světlé barvy</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..7252d7b1d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-cy/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Ffont Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Ffont serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Lleihad maint ffont</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Cynnydd maint ffont</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tywyll</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Cynllun lliw tywyll</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Cynllun lliw tywyll</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Golau</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Cynllun lliw golau</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..f933bc4565
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-da/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans-serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans-serif-font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif-font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Gør skriftstørrelsen mindre</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Gør skriftstørrelsen større</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Mørk</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Mørkt farveskema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia farveskema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Lyst</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Lyst farveskema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..3f973f6ae1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-de/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Serifenlose Schriftart</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serifenschriftart</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Schriftgröße verringern</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Schriftgröße erhöhen</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dunkel</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Dunkles Farbschema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia-Farbschema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Hell</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Helles Farbschema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..84467bec78
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Pismo sans serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serifowe pismo</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Wjelikosć pisma pómjeńšyś</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Wjelikosć pisma pówětšyś</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Śamny</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Śamna barwowa šema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepija</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Barwowa šema Sepija</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Swětły</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Swětła barwowa šema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..abb18c3715
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-el/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Γραμματοσειρά Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Γραμματοσειρά Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Σμίκρυνση γραμματοσειράς</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Μεγέθυνση γραμματοσειράς</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Σκουρόχρωμο</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Σύνολο σκούρων χρωμάτων</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Σέπια</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Σύνολο χρωμάτων σέπια</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ανοιχτό</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Σύνολο ανοιχτών χρωμάτων</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..98a786a966
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Font size decrease</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Font size increase</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dark</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Dark colour scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia colour scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Light</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Light colour scheme</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..98a786a966
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Font size decrease</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Font size increase</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dark</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Dark colour scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia colour scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Light</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Light colour scheme</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..719f10262b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-eo/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Senserifa</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Tiparo senserifa</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serifa</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Tiparo serifa</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Etigi tiparon</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Grandigi tiparon</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Malhela</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Malhelkolora skemo</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepikolora skemo</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Hela</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Helkolora skemo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..e37f09be5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans-serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fuente Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fuente Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Disminución del tamaño de la fuente</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumento del tamaño de la fuente</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Oscuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de color oscuro</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de color sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de color claro</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..901d2fcb3c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fuente Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fuente Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Reducción del tamaño de la fuente</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Incremento del tamaño de la fuente</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Oscuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de color oscuro</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de color sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de color claro</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..6ff0918f51
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fuente Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fuente Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Disminuir tamaño de fuente</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumentar tamaño de fuente</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Oscuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de colores oscuros</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de colores sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de colores claros</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..266654f423
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fuente Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fuente Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Disminuir tamaño de fuente</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumentar tamaño de fuente</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Oscuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de color oscuro</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de color sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de color claro</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..6ff0918f51
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-es/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fuente Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fuente Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Disminuir tamaño de fuente</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumentar tamaño de fuente</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Oscuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de colores oscuros</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de colores sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de colores claros</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..672015654e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-et/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">seriifideta</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">seriifideta font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">seriifidega</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">seriifidega font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">fondi suuruse vähendamine</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">fondi suuruse suurendamine</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">tume</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">tume värviskeem</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">seepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">seepia värviskeem</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">hele</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">hele värviskeem</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..df496fd857
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-eu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif letra-tipoa</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif letra-tipoa</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Txikiagotu letra tamaina</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Handiagotu letra tamaina</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Iluna</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Kolore-eskema iluna</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia kolore-eskema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Argia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Kolore-eskema argia</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..65abe288c6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fa/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">بدون سِریف</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">قلم بدون سِریف</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">سِریف</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">قلم سِریف</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">کاهش اندازهٔ قلم</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">افزایش اندازهٔ قلم</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">تیره</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">طرح رنگ تیره</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">سوبیایی</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">طرح رنگی سوبیایی</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">روشن</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">طرح رنگ روشن</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..2e43edd832
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ff/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Ustugol darnde alkule</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Ɓeydugol darnde alkule</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Niɓɓo</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Leerɗo</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..964c1c91cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fi/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Päätteetön</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Päätteetön fontti</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Päätteellinen</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Päätteellinen fontti</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Pienennä fontin kokoa</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Suurenna fontin kokoa</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tumma</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Tumma väriteema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Seepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Seepiamainen väriteema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Vaalea</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Vaalea väriteema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..93121bddbc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fr/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans empattement</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Police sans empattement</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Empattement</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Police avec empattement</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Réduction de la taille de la police</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Augmentation de la taille de la police</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Sombre</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Jeu de couleurs sombres</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sépia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Jeu de couleurs sépia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Clair</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Jeu de couleurs claires</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..1d4a29ffd7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fur/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Cence graciis (sans serif)</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Caratar cence graciis (sans serif)</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Cun graciis (serif)</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Caratar cun graciis (serif)</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Diminuìs dimension caratars</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumente dimension caratars</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Scûr</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Scheme di colôrs scûrs</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepât</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Scheme di colôrs sepâts</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Clâr</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Scheme di colôrs clârs</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..93ff9bf9d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Skreefleas</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Lettertype Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Mei skreef</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Lettertype Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Lettergrutte ferlytsje</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Lettertype fergrutsje</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Donker</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Donker kleureskema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia kleureskema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ljocht</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Ljocht kleureskema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..eb2ec6e0b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Cló Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Cló Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Laghdaigh an cló</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Méadaigh an cló</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dorcha</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Scéim dathanna dorcha</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Dúch Cudail</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Scéim dathanna dúch cudail</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Sorcha</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Scéim dathanna sorcha</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..daf58b6252
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-gd/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans-serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Cruth-clò sans-serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Cruth-clò serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Lùghdaich meud a’ chrutha-chlò</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Meudaich an cruth-clò</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dorcha</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Sgeama dhathan dorcha</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sgeama dhathan sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Soilleir</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Sgeama dhathan soilleir</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..f223fabb5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-gl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans-serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Tipo de letra sans-Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Tipo de letra serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Aumento do tamaño da letra</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Redución do tamaño da letra</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Escuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de cores escuras</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de cores sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de cores claras</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..003a68394e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-gn/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans-serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif reñoiha</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif reñoiha</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Emomichĩ tai tuichakue</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Embotuicha tai tuichakue</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Ypytũ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Sa’y ypytũva raity</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sa’y sepia raity</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Tesakã</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Sa’y hesakãva raity</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..a433b5dde6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">સેન્સ-શેરીફ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">સેન્સ શેરીફ ફોન્ટ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">શેરીફ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">શેરીફ ફોન્ટ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">ફોન્ટના કદ ઘટાડો</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">ફોન્ટના કદ વધારો</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">ઘટ્ટ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ઘાટા રંગ યોજના</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">સેપિયા</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">સેપિયા રંગ યોજના</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">આછો</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">આછા રંગ યોજના</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..a9e1ac2996
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans serif फ़ॉन्ट</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif फ़ॉन्ट</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">फ़ॉन्ट आकार कम करें</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">फ़ॉन्ट आकार बढ़ाएं</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">डार्क</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">डार्क कलर स्कीम</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">सेपिया</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">सेपिया कलर स्कीम</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">लाइट</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">लाइट कलर स्कीम</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..14eebfce9c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hil/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif font</string>
+
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..6d21274a81
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Bezserifni</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Bezserifni font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serifni</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serifni font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Smanjenje veličine fonta</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Povećanje veličine fonta</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tamna</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Tamna shema boja</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Smeđa</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Smeđa shema boja</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Svijetla</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Svijetla shema boja</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..34b0fb4c20
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Pismo sans serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serifowe pismo</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Wulkosć pisma pomjeńšić</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Wulkosć pisma powjetšić</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Ćmowy</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Ćmowa barbowa šema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepija</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Barbowa šema Sepija</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Swětły</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Swětła barbowa šema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..8b1b4517da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Talpatlan</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Talpatlan betűkészlet</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Talpas</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Talpas betűtípus</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Betűméret csökkentése</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Betűméret csökkentése</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Sötét</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Sötét színösszeállítás</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Szépia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Szépia színösszeállítás</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Világos</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Világos színösszeállítás</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..c6390958f8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif տառատեսակ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif տառատեսակ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Տառատեսակի չափի փոքրացում</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Տառատեսակի չափի մեծացում</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Մուգ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Մուգ գույնի ուրվակազմ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Դարչնագույն</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Դարչնագույն գույնի ուրվակազմ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Լուսավոր</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Բաց գույնի ուրվակազմ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..b2e9faa119
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ia/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Typo de character Sans serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Typo de character Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Reducer le dimension del characteres</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Accrescer le dimension del characteres</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Obscur</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Schema de color obscur</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Schema de color sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Clar</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Schema de color clar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..673c931872
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-in/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fon Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fon Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Kurangi ukuran fon</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Tingkatkan ukuran fon</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Gelap</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Skema warna gelap</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Skema warna sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Terang</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Skema warna terang</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..ed2aadcb00
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-is/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans-serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif leturgerð</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif leturgerð</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Minnka leturstærð</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Auka leturstærð</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dökkt</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Dökkt litastef</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepía</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepía litastef</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ljóst</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Ljóst litastef</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..22fea45d24
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-it/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Senza grazie</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Carattere senza grazie</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Con grazie</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Carattere con grazie</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Riduci dimensione dei caratteri</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumenta dimensione dei caratteri</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Scuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Combinazione di colori scura</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Seppia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Combinazione di colori seppia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Chiaro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Combinazione di colori chiara</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..2b7451b69d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-iw/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">ללא עיטור</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">גופן ללא עיטורים</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">מעוטר</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">גופן מעוטר</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">הקטנת גודל גופן</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">הגדלת גודל גופן</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">כהה</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ערכת צבעים כהה</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">חום כהה</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">ערכת צבעים חמה</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">בהיר</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ערכת צבעים בהירה</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..6faa51a99e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ja/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">ゴシック体 (Sans-serif)</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">ゴシック体のフォント</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">明朝体 (Serif)</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">明朝体のフォント</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">フォントサイズを小さく</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">フォントサイズを大きく</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dark</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">暗いカラースキーム</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">セピアのカラースキーム</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Light</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">明るいカラースキーム</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..40d031d4c7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ka/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">უნაჭდევო</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">უნაჭდევო შრიფტი</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">ნაჭდევებით</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">ნაჭდევებიანი შრიფტი</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">შრიფტის ზომის შემცირება</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">შრიფტის ზომის მომატება</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">მუქი</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">მუქი ფერები</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">სეპია</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">მოყავისფრო ფერები</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ნათელი</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ნათელი ფერები</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..9a90792a6d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans serif shrifti</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif shrifti</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Shrift ólshemin kishireytiw</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Shrift ólshemin úlkeytiw</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Qarańǵı</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Qara reń gamması</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepiya reń gamması</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Jaqtı</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Ashıq reń gamması</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..edbcc1d2dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kab/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Tasefsit Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Tasefsit Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Asenɣes n teɣzi n tsefsit</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Timerna n teɣzi n tsefsit</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Aberkan</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Initen ubriken</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Initen Sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Aceɛlal</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Initen iceɛlalen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..64e057d5ff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif қарібі</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif қарібі</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Қаріп өлшемін кішірейту</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Қаріп өлшемін үлкейту</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Күңгірт</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Күңгірт түстер схемасы</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Сепия</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Сепия түстер схемасы</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ашық түсті</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Ашық түсті түстер схемасы</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..06f53cd051
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fonta Sans Serifê</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fonta Serifê</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Biçûkkirina mezinahiya fontê</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Mezinkirina mezinahiya fontê</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tarî</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Şemaya rengê tarî</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepya</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Şemaya rengê sepyayî</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ronî</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Şemaya rengê ronî</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..5ca8fe9639
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-kn/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">ಸಾನ್ಸ್ ಸೆರಿಫ್</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">ಸಾನ್ಸ್ ಸೆರಿಫ್ ಫಾಂಟ್</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">ಸೆರಿಫ್</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">ಸೆರಿಫ್ ಫಾಂಟ್</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">ಫಾಂಟ್ ಗಾತ್ರ ಕಡಿಮೆಯಾಗುತ್ತದೆ</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">ಫಾಂಟ್ ಗಾತ್ರ ಹೆಚ್ಚಳ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">ಗಾಢ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ಗಾಢ ಬಣ್ಣದ ಯೋಜನೆ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">ಸೆಪಿಯ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">ಸೆಪಿಯಾ ಬಣ್ಣದ ಯೋಜನೆ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ತಿಳಿ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ತಿಳಿ ಬಣ್ಣದ ಯೋಜನೆ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..3189ebf4c3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ko/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">산세리프</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">산세리프 글꼴</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">세리프</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">세리프 글꼴</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">글꼴 크기 작게</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">글꼴 크기 크게</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">어둡게</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">어두운 색 구성표</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">세피아</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">세피아 색 구성표</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">밝게</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">밝은 색 구성표</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..f2e20c0a7f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-lij/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Carateri ciù picin</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Carateri ciù grendi</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Scuo</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Conbinaçion scua</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Conbinaçion sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ciæo</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Conbinaçion Ciæa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..42b88f3741
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-lo/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">ຟັອນ Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">ຟັອນ Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">ຂະໜາດຟັອນຫຼຸດລົງ</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">ຂະໜາດຟັອນເພີ່ມຂື້ນ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">ມືດ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ຊຸດສີມືດດຳ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">ສີນ້ຳຕານດຳ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">ຊຸດສີນ້ຳຕານດຳ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ແຈ້ງ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ຊຸດສີແຈ້ງສະຫວ່າງ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..a98b68ae91
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-lt/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Be užraitų</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Šriftas be užraitų</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Su užraitas</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Šriftas su užraitais</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Šrifto dydžio mažinimas</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Šrifto dydžio didinimas</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tamsus</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Tamsi spalvų aibė</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepija</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepijos spalvų aibė</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Šviesus</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Šviesi spalvų aibė</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..bcf0cabcc1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ml/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">സാൻസ് സെരിഫ്</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">സാൻസ് സെരിഫ് ഫോണ്ട്</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">സെരിഫ്</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">സെരിഫ് ഫോണ്ട്</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">ഫോണ്ട് വലുപ്പം കുറയ്ക്കുക</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">ഫോണ്ട് വലുപ്പം കൂട്ടുക</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">കടും</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ഡാർക്ക് കളർ സ്കീം</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">സെപിയ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">സെപിയ കളർ സ്കീം</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ലൈറ്റ്</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ലൈറ്റ് കളർ സ്കീം</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..a98aeb3639
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-mr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">सॅन्स सेरिफ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">सॅन्स सेरिफ टंक</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">सेरिफ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">सेरिफ टंक</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">टंकाचा आकार कमी करा</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">टंकाचा आकार वाढावा</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">गडद</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">गडद रंग योजना</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">दाट तपकिरी</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">दाट तपकिरी रंग योजना</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">फिकट</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">फिकट रंग योजना</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..be83dbe7e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-my/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif ဖောင့်</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">serif ဖောင့်</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">စာလုံးအရွယ်အစားလျှေ့ာပါ</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">စာလုံးအရွယ်အစားတိုးပါ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">အမှောင်</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">မှောင်မိုက်အရောင်အစီအစဉ်</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">နီညိုရောင်</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">နီညိုရောင် ပြသမှု</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">အလင်း</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">အလင်းအရောင်အစီအစဉ်</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..21758154bd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Seriffløs skrift</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Seriffløs skrift</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Seriffskrift</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Seriffskrift</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Skriftstørrelse reduseres</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Skriftstørrelse økes</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Mørk</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Mørkt fargevalg</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia fargevalg</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Lys</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Lyst fargevalg</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..08f2ddb96d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">सानस्-सेरिफ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">सानस्-सेरिफ फन्ट</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">सेरिफ</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">सेरिफ फन्ट</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">फन्टको आकार घटाउनुहोस्</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">फन्टको आकार बढाउनुहोस्</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">अँध्यारो</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">अँध्यारो रङ्ग योजना</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">सेपिया</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">सेपिया रंग योजना</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">उज्यालो</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">उज्यालो रङ्ग योजना</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..5cd24cbf48
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-nl/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Schreefloos</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Lettertype Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Met schreef</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Lettertype Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Lettergrootte verkleinen</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Lettergrootte vergroten</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Donker</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Donker kleurenschema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia kleurenschema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Licht</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Licht kleurenschema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..7060f25970
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Serifflaus skrift</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Serifflaus skrift</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Seriffskrift</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Seriffskrift</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Skriftstørrelse minskar</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Skriftstørrelse aukar</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Mørkt</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Mørkt fargeval</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia fargeval</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Lyst</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Lyst fargeval</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..b914e4e856
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-oc/strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Polissa Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Polissa Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Reduccion de la talha de la polissa</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Agrandiment de la talha de la polissa</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Fosc</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Jòc de colors foscas</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sèpia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Jòc de colors sépia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Clar</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Jòc de color claras</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..617dade600
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-or/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">ଗାଢ଼</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ଗାଢ଼ ରଙ୍ଗ ଯୋଜନା</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">ସେପିଆ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">ସେପିଆ ରଙ୍ଗ ଯୋଜନା</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ହାଲୁକା</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ହାଲୁକା ରଙ୍ଗ ଯୋଜନା</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..07052c8e4c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">ਸਨਜ ਸੈਰੀਫ਼</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">ਸਨਜ ਸੈਰੀਫ਼ ਫ਼ੋਂਟ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">ਸੈਰੀਫ਼</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">ਸੈਰੀਫ਼ ਫ਼ੋਂਟ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">ਫ਼ੋਂਟ ਆਕਾਰ ਘਟਾਓ</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">ਫ਼ੋਂਟ ਆਕਾਰ ਵਧਾਓ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">ਗੂੜ੍ਹਾ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ਗੂੜ੍ਹੇ ਰੰਗ ਦੀ ਰੂਪ-ਰੇਖਾ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">ਭੂਰਾ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">ਭੂਰੇ ਰੰਗ ਦੀ ਰੂਪ-ਰੇਖਾ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ਹਲਕਾ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ਹਲਕੀ ਰੰਗ ਸਕੀਮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..0b8fa4d685
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">سیرف بغیر</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">سیرف بغیر فونٹ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">سیرف</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">سیرف فونٹ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">فونٹ آکار گھٹاؤ</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">فونٹ آکار ودھاؤ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">گوڑھا</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">گوڑھے رنگ دی روپ ریکھا</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">بھورا</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">بھورا رنگ دی روپ ریکھا</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ہلکا</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ہلکے رنگ دی روپ ریکھا</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..0c07108257
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Bezszeryfowa</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Czcionka bezszeryfowa</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Szeryfowa</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Czcionka szeryfowa</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Zmniejsz rozmiar czcionki</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Zwiększ rozmiar czcionki</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Ciemny</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Ciemny schemat kolorów</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Schemat kolorów sepii</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Jasny</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Jasny schemat kolorów</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..f0d45e960f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sem serifa</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Fonte sem serifa</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Com serifa</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fonte com serifa</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Reduzir tamanho da fonte</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumentar tamanho da fonte</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Escuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de cores escuras</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sépia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de cores sépia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de cores claras</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..aba390c250
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sem serifa</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Tipo de letra sem serifa</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serifa</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Tipo de letra com serifa</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Redução do tamanho da letra</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumento do tamanho da letra</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Escuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Esquema de cores escuro</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sépia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Esquema de cores sépia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Claro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Esquema de cores claras</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..b0753955e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-rm/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Senza serifas</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Scrittira senza serifas</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Cun serifas</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Scrittira cun serifas</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Empitschnir la scrittira</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Engrondir la scrittira</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Stgir</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Schema stgir</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Schema da sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Cler</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Schema cler</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..808f367aa5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ro/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Font Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Font Serif </string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Scăderea dimensiunii fontului</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Creșterea dimensiunii fontului</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Întunecat</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Paletă de culori închise</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Paletă de culori sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Deschis</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Paletă de culori deschise</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..4d4a49469a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ru/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Без засечек</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Шрифт без засечек</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">С засечками</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Шрифт с засечками</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Уменьшить размер шрифта</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Увеличить размер шрифта</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Тёмная</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Цветовая гамма «Тёмная»</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Сепия</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Цветовая гамма «Сепия»</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Светлая</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Цветовая гамма «Светлая»</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..d96b1fb8cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sat/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">ᱥᱮᱱᱥ ᱥᱮᱨᱤᱯᱷ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">ᱥᱮᱱᱥ ᱥᱮᱨᱤᱯᱷ ᱪᱤᱠᱤ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">ᱥᱮᱨᱤᱯᱷ</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">ᱥᱮᱨᱤᱯᱷ ᱪᱤᱠᱤ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">ᱪᱤᱠᱤ ᱢᱟᱯ ᱠᱚᱢ ᱢᱮ</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">ᱪᱤᱠᱤ ᱢᱟᱯ ᱢᱟᱨᱟᱝ ᱢᱮ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">ᱧᱩᱛ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">ᱧᱩᱛ ᱨᱚᱝ ᱯᱚᱱᱛᱷᱟ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">ᱥᱮᱯᱤᱭᱟ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">ᱥᱮᱯᱤᱭᱟ ᱨᱚᱝ ᱯᱚᱱᱛᱷᱟ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ᱢᱟᱨᱥᱟᱞ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ᱢᱟᱨᱥᱟᱞ ᱨᱚᱝ ᱯᱚᱱᱛᱷᱟ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..e49917faef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sc/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Tipografia Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Tipografia Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Mìnima sa mannària de sa tipografia</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Cresche sa mannària de sa tipografia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Iscuru</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Tema de colores iscuru</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sèpia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Tema de colores sèpia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Craru</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Tema de colores craru</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..c3ee32bdea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-si/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">සාන්ස් සෙරිෆ්</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">සාන්ස් සෙරිෆ් අකුර</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">සෙරිෆ්</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">සෙරිෆ් අකුර</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">අකුරේ තරම අඩු කිරීම</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">අකුරේ තරම වැඩි කිරීම</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">අඳුරු</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">අඳුරු වර්ණ රටාව</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">ලා දුඹුරු</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">ලා දුඹුරු වර්ණ රටාව</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">දීප්ත</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">දීප්ත වර්ණ රටාව</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..77f7c9a80a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sk/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Bezpätkové</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Bezpätkové písmo</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Pätkové</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Pätkové písmo</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Zmenšiť písmo</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Zväčšiť písmo</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tmavá</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Tmavá farebná schéma</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sépia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Farebná schéma sépia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Svetlá</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Svetlá farebná schéma</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..34b1b31c0d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-skr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">سینس سیرف</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">سانس سیرف فونٹ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">سیرف</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">سیرف فونٹ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">فونٹ سائز گھٹاؤ</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">فونٹ سائز ودھاؤ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">شوخ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">شوخ رنگ سکیم</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">سیپیا</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">سیپیا رنگ سکیم</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">پھکا</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">پھکا رنگ سکیم</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..f4f1048988
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Pisava Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Pisava Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Zmanjšanje velikosti pisave</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Povečanje velikosti pisave</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Temna</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Temna barvna shema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia barvna shema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Svetla</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Svetla barvna shema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..df876b388e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sq/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Shkronja Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Shkronja Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Zvogëlim madhësie shkronjash</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Zmadhim madhësie shkronjash</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">I errët</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Skemë ngjyrash të errëta</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Skemë ngjyrash sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">I çelët</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Skemë ngjyrash të çelëta</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..baf441de8d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Бесерифни</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Бесерифни фонт</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Серифни</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Серифни фонт</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Смањење величине фонта</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Повећање величине фонта</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Тамна</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Тамна шема боја</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Сепиа</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Сепиа шема боја</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Светла</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Светла шема боја</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..de0dbbb3a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-su/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans-serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Font Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Font Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Ukuran font ngaalitan</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Ukuran font ngaageungan</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Poék</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Skéma warna poék</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Skéma warna sépia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Caang</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Skéma warna caang</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..f740efd80e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif typsnitt</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif typsnitt</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Teckenstorlek minskar</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Teckenstorlek ökar</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Mörkt</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Mörkt färgschema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia färgschema</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ljust</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Ljust färgschema</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..6249d73494
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ta/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">சான்ஸ் செரிஃப்</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">சான்ஸ் செரிஃப் எழுத்துரு</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">ஷெரிஃப்</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">செரிஃப் எழுத்துரு</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">எழுத்துரு அளவு குறைகிறது</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">எழுத்துரு அளவு அதிகரிக்கும்</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">கருமை</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">இருண்ட வண்ண திட்டம்</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">செபியா</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">செபியா வண்ணத் திட்டம்</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">வெளிர்</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">வெளிர் வண்ணத் திட்டம்</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..9a367ee164
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-te/strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">సాన్స్ సెరీఫ్</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">సాన్స్ సెరిఫ్ ఫాంటు</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">సెరిఫ్</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">సెరిఫ్ ఫాంటు</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">ఫాంటు పరిమాణం తగ్గించు</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">ఫాంటు పరిమాణం పెంచు</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">నల్లని</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">నల్లని రంగు స్కీము</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">సేపియా</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">సేపియా రంగు స్కీము</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">లేత</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">లేత రంగు</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..c54770b1e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tg/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Ғайрибарҷаста</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Ҳуруфи ғайрибарҷаста</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Барҷаста</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Ҳуруфи барҷаста</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Хурд кардани андозаи ҳуруф</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Калон кардани андозаи ҳуруф</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Торик</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Нақшаи рангии торик</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Сепия</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Нақшаи рангии сепия</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Равшан</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Нақшаи рангии равшан</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..9f55fc255b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-th/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">ไม่มีเชิง</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">แบบอักษรไม่มีเชิง</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">มีเชิง</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">แบบอักษรมีเชิง</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">ลดขนาดแบบอักษร</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">เพิ่มขนาดแบบอักษร</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">มืด</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">แบบแผนสีมืด</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">ซีเปีย</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">แบบแผนสีซีเปีย</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">สว่าง</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">แบบแผนสีสว่าง</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..b67303393c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Liitan ang font size</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Lakihan ang font size</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Madilim</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Madilim na color scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia color scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Maliwanag</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Maliwanag na color scheme</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..e4cd63a232
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tok/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">linja lili ala</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">nasin sitelen pi linja lili ala</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">linja lili</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">nasin sitelen pi linja lili</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">sitelen li kama lili</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">sitelen li kama suli</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">pimeja</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">nasin lukin pimeja</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">loje lili</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">nasin lukin pi loje lili</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">suno</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">nasin lukin suno</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..3f8430132c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tr/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans serif yazı tipi</string>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif yazı tipi</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Yazı tipi boyutu küçültme</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Yazı tipi boyutu büyütme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Koyu</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Koyu renk şeması</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepya</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepya renk şeması</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Açık</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Açık renk şeması</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..b494597f2f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-trs/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Lechrâ Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Lechrâ Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Nāgi\'iaj lij lêchra</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Nāgi\'iaj yāchìj lêchra</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Gā rūmin\' man</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Nej dūguì\' kolô rūmin\'in</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Rūguì\'i</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Nej dūguì\' kolô rūguì\'i</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Sa yigìïn</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Nej dūguì\' kolô yigìïn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..9b3aa9dcb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tt/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif шрифты</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif шрифты</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Шрифт үлчәмен кечерәйтү</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Шрифт үлчәмен зурайту</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Караңгы</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Караңгы төсле схема</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Сепия</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Сепия төсләр схемасы</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ачык</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Ачык төсле схема</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..c684bc547f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Tasefsit Serif</string>
+
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Adeɣmum</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Anafaw</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..164979ae7a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ug/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif خەت نۇسخا</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif خەت نۇسخا</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">خەت كىچىكلىتىش</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">خەت چوڭايتىش</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">قاراڭغۇ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">قېنىق رەڭ لايىھەسى</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">قېنىق قوڭۇر رەڭ</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">قېنىق قوڭۇر رەڭ لايىھەسى</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">يورۇق</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">سۇس رەڭ لايىھەسى</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..43ff1f3b0c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-uk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Без зарубок</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Шрифт без зарубок</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Із зарубками</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Шрифт із зарубками</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Зменшити розмір шрифту</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Збільшити розмір шрифту</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Темна</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Темна колірна тема</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Сепія</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Колірна тема сепія</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Світла</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Світла колірна тема</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..3da140cb4e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-ur/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">سینس سیرف</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">سانس سیرف فونٹ</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">سیرف</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">سیرف فونٹ</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">فونٹ سائز کم کریں</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">فونٹ سائز اضافہ کریں</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">گہرا</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">گہرے رنگ کی اسکیم</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">سیپیا</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">سیپیا رنگ سکیم</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">ہلکا</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">ہلکے رنگ کی اسکیم</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..d0ac181290
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-uz/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif shrifti</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif shrifti</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Shrift hajmini kichraytirish</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Shrift hajmini kattalashtirish</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Qora</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">“Qora” rang sxemasi</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">“Sepiya” rang sxemasi</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Yorqin</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">“Yorqin” rang sxemasi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..5802794118
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-vec/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Ridusi dimension de i caratari</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Aumenta dimension de i caratari</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Scuro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Conbinasion de coƚori scura</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepa</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Conbinasion de coƚori sepa</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Ciaro</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Conbinasion de coƚori ciara</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..116ecd027d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-vi/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Phông Sans Serif</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Phông Serif</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Giảm cỡ chữ</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Tăng cỡ chữ</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Tối</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Phối màu tối</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Nâu đen</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Phối màu nâu đen</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Sáng</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Phối màu sáng</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..135da6274e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-yo/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Fọ́ǹtì sérọ̀fù</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Ìwọn fọ́ǹtì dínkù</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Ìwọn fọ́ǹtì pọ̀ si</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dúdú</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Ìlànà aláwọ̀ dúdú</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Ìlànà aláwọ̀ Sepia</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Mọ́lẹ̀</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Ìlànà aláwọ̀ tó mọ́lẹ̀</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..059aab3a88
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">无衬线</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">无衬线字体</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">衬线</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">衬线字体</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">缩小文字</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">增大文字</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">深邃</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">深邃配色</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">纸墨</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">纸墨配色</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">明亮</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">明亮配色</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..21f91277d3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">無襯線</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">無襯線字型</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">襯線字</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">襯線字型</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">縮小字體</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">放大字體</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">暗色</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">暗色佈景主題</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">懷舊風</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">棕褐色調佈景主題</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">亮色</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">亮色佈景主題</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values/attrs.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000..71637e8804
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values/attrs.xml
@@ -0,0 +1,26 @@
+<?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>
+ <declare-styleable name="ReaderViewControlsBar">
+ <attr name="readerViewControlsBackgroundColor" format="reference|color" />
+ <attr name="readerViewControlsSelectedColor" format="reference|color" />
+ <attr name="readerViewControlsTextColor" format="reference|color" />
+ </declare-styleable>
+
+ <style name="RadioButtonSelectedTextStyle" parent="@android:style/Widget.CompoundButton">
+ <item name="android:textColor">@drawable/radiobutton_selected_text_selector</item>
+ <item name="android:button">@null</item>
+ <item name="android:padding">16dp</item>
+ </style>
+
+ <style name="RadioButtonSelectedBorderStyle" parent="@android:style/Widget.CompoundButton">
+ <item name="android:button">@null</item>
+ <item name="android:padding">16dp</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+</resources>
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..94136325dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+<?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>
+ <color name="mozac_feature_readerview_selected">#592ACB</color>
+ <color name="mozac_feature_readerview_text_color">#220033</color>
+ <color name="mozac_feature_readerview_text_color_disabled">#B1B1B3</color>
+ <color name="mozac_feature_readerview_dark_background">#32313B</color>
+ <color name="mozac_feature_readerview_light_background">#EFEFF2</color>
+ <color name="mozac_feature_readerview_sepia_background">#FDF4E0</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values/mozac_feature_readerview_strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values/mozac_feature_readerview_strings.xml
new file mode 100644
index 0000000000..cca9a042ff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values/mozac_feature_readerview_strings.xml
@@ -0,0 +1,11 @@
+<?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>
+ <string name="mozac_feature_readerview_negative_sign" translatable="false">−</string>
+ <string name="mozac_feature_readerview_positive_sign" translatable="false">+</string>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/readerview/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..bd212d57fe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/main/res/values/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_sans_serif_font">Sans serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_sans_serif_font_desc">Sans Serif font</string>
+ <!-- Name for one of the font options available to use -->
+ <string name="mozac_feature_readerview_serif_font">Serif</string>
+ <!-- Accessible description for the font option -->
+ <string name="mozac_feature_readerview_serif_font_desc">Serif font</string>
+
+ <!-- Accessible description for decreasing the font size -->
+ <string name="mozac_feature_readerview_font_size_decrease_desc">Font size decrease</string>
+
+ <!-- Accessible description for increasing the font size -->
+ <string name="mozac_feature_readerview_font_size_increase_desc">Font size increase</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_dark">Dark</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_dark_color_scheme_desc">Dark color scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_sephia">Sepia</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_sepia_color_scheme_desc">Sepia color scheme</string>
+ <!-- Color option for the background -->
+ <string name="mozac_feature_readerview_light">Light</string>
+ <!-- Accessible description for the color option -->
+ <string name="mozac_feature_readerview_light_color_scheme_desc">Light color scheme</string>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewFeatureTest.kt b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewFeatureTest.kt
new file mode 100644
index 0000000000..a4b2536ad5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewFeatureTest.kt
@@ -0,0 +1,608 @@
+/* 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.readerview
+
+import android.content.Context
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.ReaderAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ReaderState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.FONT_SIZE_DEFAULT
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.READER_VIEW_ACTIVE_CONTENT_PORT
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.READER_VIEW_CONTENT_PORT
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.READER_VIEW_EXTENSION_ID
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.READER_VIEW_EXTENSION_URL
+import mozilla.components.feature.readerview.view.ReaderViewControlsBar
+import mozilla.components.feature.readerview.view.ReaderViewControlsView
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import mozilla.components.support.webextensions.WebExtensionController
+import mozilla.ext.appCompatContext
+import org.json.JSONObject
+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.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.Locale
+
+@RunWith(AndroidJUnit4::class)
+class ReaderViewFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ WebExtensionController.installedExtensions.clear()
+ }
+
+ @Test
+ fun `start installs webextension`() {
+ val engine: Engine = mock()
+ val store = BrowserStore()
+ val readerViewFeature = ReaderViewFeature(testContext, engine, store, mock())
+
+ readerViewFeature.start()
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(ReaderViewFeature.READER_VIEW_EXTENSION_ID),
+ eq(ReaderViewFeature.READER_VIEW_EXTENSION_URL),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+
+ onSuccess.value.invoke(mock())
+
+ // Already installed, should not try to install again.
+ readerViewFeature.start()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(ReaderViewFeature.READER_VIEW_EXTENSION_ID),
+ eq(ReaderViewFeature.READER_VIEW_EXTENSION_URL),
+ any(),
+ any(),
+ )
+ }
+
+ @Test
+ fun `start registers content message handlers for selected session`() {
+ val engine: Engine = mock()
+ val view: ReaderViewControlsView = mock()
+ val engineSession: EngineSession = mock()
+ val controller = spy(
+ WebExtensionController(
+ READER_VIEW_EXTENSION_ID,
+ READER_VIEW_EXTENSION_URL,
+ READER_VIEW_CONTENT_PORT,
+ ),
+ )
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ id = "test-tab",
+ engineSession = engineSession,
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ selectedTabId = tab.id,
+ ),
+ )
+
+ val readerViewFeature = ReaderViewFeature(testContext, engine, store, view)
+ readerViewFeature.extensionController = controller
+ readerViewFeature.start()
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(ReaderViewFeature.READER_VIEW_EXTENSION_ID),
+ eq(ReaderViewFeature.READER_VIEW_EXTENSION_URL),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+ onSuccess.value.invoke(mock())
+ verify(controller).registerContentMessageHandler(eq(engineSession), any(), eq(READER_VIEW_ACTIVE_CONTENT_PORT))
+ verify(controller).registerContentMessageHandler(eq(engineSession), any(), eq(READER_VIEW_CONTENT_PORT))
+ }
+
+ @Test
+ fun `start also starts controls interactor`() {
+ val engine: Engine = mock()
+ val store = BrowserStore()
+ val view: ReaderViewControlsView = ReaderViewControlsBar(appCompatContext)
+
+ val readerViewFeature = spy(ReaderViewFeature(testContext, engine, store, view))
+ readerViewFeature.start()
+
+ assertNotNull(view.listener)
+ }
+
+ @Test
+ fun `stop also stops controls interactor`() {
+ val engine: Engine = mock()
+ val store = BrowserStore()
+ val view: ReaderViewControlsView = ReaderViewControlsBar(appCompatContext)
+
+ val readerViewFeature = spy(ReaderViewFeature(testContext, engine, store, view))
+ readerViewFeature.stop()
+
+ assertNull(view.listener)
+ }
+
+ @Test
+ fun `showControls invokes the controls presenter`() {
+ val view: ReaderViewControlsView = mock()
+ val feature = spy(ReaderViewFeature(testContext, mock(), mock(), view))
+
+ feature.showControls()
+
+ verify(view).setColorScheme(any())
+ verify(view).setFont(any())
+ verify(view).setFontSize(anyInt())
+ verify(view).showControls()
+ }
+
+ @Test
+ fun `hideControls invokes the controls presenter`() {
+ val view: ReaderViewControlsView = mock()
+ val feature = spy(ReaderViewFeature(testContext, mock(), mock(), view))
+
+ feature.hideControls()
+
+ verify(view).hideControls()
+ }
+
+ @Test
+ fun `triggers readerable check when required`() {
+ val engine: Engine = mock()
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(initialState = BrowserState(tabs = listOf(tab)))
+ val readerViewFeature = spy(ReaderViewFeature(testContext, engine, store, mock()))
+ readerViewFeature.start()
+
+ store.dispatch(ReaderAction.UpdateReaderableCheckRequiredAction(tab.id, true)).joinBlocking()
+
+ val tabCaptor = argumentCaptor<TabSessionState>()
+ verify(readerViewFeature).checkReaderState(tabCaptor.capture())
+ assertEquals(tab.id, tabCaptor.value.id)
+ }
+
+ @Test
+ fun `connects content script port when required`() {
+ val engine: Engine = mock()
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(initialState = BrowserState(tabs = listOf(tab), selectedTabId = tab.id))
+ val readerViewFeature = spy(ReaderViewFeature(testContext, engine, store, mock()))
+ readerViewFeature.start()
+
+ store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(tab.id, true)).joinBlocking()
+ val tabCaptor = argumentCaptor<TabSessionState>()
+ verify(readerViewFeature).connectReaderViewContentScript(tabCaptor.capture())
+ assertEquals(tab.id, tabCaptor.value.id)
+ }
+
+ @Test
+ fun `notifies readerable state changes of selected tab`() {
+ val readerViewStatusChanges = mutableListOf<Pair<Boolean, Boolean>>()
+ val onReaderViewStatusChange: onReaderViewStatusChange = {
+ readerable, active ->
+ readerViewStatusChanges.add(Pair(readerable, active))
+ }
+
+ val engine: Engine = mock()
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(initialState = BrowserState(tabs = listOf(tab)))
+ val readerViewFeature = spy(ReaderViewFeature(testContext, engine, store, mock(), { "test-uuid" }, onReaderViewStatusChange))
+ readerViewFeature.start()
+ assertTrue(readerViewStatusChanges.isEmpty())
+
+ store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
+ store.dispatch(ReaderAction.UpdateReaderableAction(tab.id, true)).joinBlocking()
+ assertEquals(1, readerViewStatusChanges.size)
+ assertEquals(Pair(true, false), readerViewStatusChanges[0])
+
+ store.dispatch(ReaderAction.UpdateReaderActiveAction(tab.id, true)).joinBlocking()
+ assertEquals(2, readerViewStatusChanges.size)
+ assertEquals(Pair(true, true), readerViewStatusChanges[1])
+
+ store.dispatch(ReaderAction.UpdateReaderableAction(tab.id, true)).joinBlocking()
+ // No change -> No notification should have been sent
+ assertEquals(2, readerViewStatusChanges.size)
+
+ store.dispatch(ReaderAction.UpdateReaderActiveAction(tab.id, false)).joinBlocking()
+ assertEquals(3, readerViewStatusChanges.size)
+ assertEquals(Pair(true, false), readerViewStatusChanges[2])
+
+ store.dispatch(ReaderAction.UpdateReaderableAction(tab.id, false)).joinBlocking()
+ assertEquals(4, readerViewStatusChanges.size)
+ assertEquals(Pair(false, false), readerViewStatusChanges[3])
+ }
+
+ @Test
+ fun `show reader view sends message to web extension`() {
+ val port = mock<Port>()
+ val message = argumentCaptor<JSONObject>()
+ val readerViewFeature = prepareFeatureForTest(port, createUUID = { "test-uuid" })
+
+ readerViewFeature.showReaderView()
+ verify(port).postMessage(message.capture())
+ assertEquals(ReaderViewFeature.ACTION_CACHE_PAGE, message.value[ReaderViewFeature.ACTION_MESSAGE_KEY])
+ assertEquals("test-uuid", message.value[ReaderViewFeature.ACTION_VALUE_ID])
+ }
+
+ @Test
+ fun `show reader view dispatches LoadUrlAction and UpdateReaderActiveAction`() {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ id = "test-tab",
+ engineSession = engineSession,
+ )
+ val store = spy(
+ BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ selectedTabId = tab.id,
+ ),
+ ),
+ )
+
+ val readerViewFeature = ReaderViewFeature(testContext, engine, store, mock(), { "bbbbf5ce-3b0f-4f74-8a1f-986d89bffea7" })
+ readerViewFeature.readerBaseUrl = "moz-extension://012345/"
+ readerViewFeature.showReaderView()
+ verify(store).dispatch(EngineAction.LoadUrlAction(tab.id, "moz-extension://012345/readerview.html?url=https%3A%2F%2Fwww.mozilla.org&id=bbbbf5ce-3b0f-4f74-8a1f-986d89bffea7&colorScheme=light"))
+ verify(store).dispatch(ReaderAction.UpdateReaderActiveAction(tab.id, true))
+ }
+
+ @Test
+ fun `default values used for showing reader view if no config is present`() {
+ val message = ReaderViewFeature.createShowReaderMessage(null)
+ assertEquals(ReaderViewFeature.ACTION_SHOW, message[ReaderViewFeature.ACTION_MESSAGE_KEY])
+ val config = message[ReaderViewFeature.ACTION_VALUE] as JSONObject?
+ assertNotNull(config)
+ assertEquals(FONT_SIZE_DEFAULT, config!![ReaderViewFeature.ACTION_VALUE_SHOW_FONT_SIZE])
+ assertEquals(
+ ReaderViewFeature.FontType.SERIF.value.lowercase(Locale.ROOT),
+ config[ReaderViewFeature.ACTION_VALUE_SHOW_FONT_TYPE],
+ )
+ assertEquals(
+ ReaderViewFeature.ColorScheme.LIGHT.name.lowercase(Locale.ROOT),
+ config[ReaderViewFeature.ACTION_VALUE_SHOW_COLOR_SCHEME],
+ )
+ assertFalse(config.has(ReaderViewFeature.ACTION_VALUE_SCROLLY))
+ }
+
+ @Test
+ fun `pass scrollY for showing reader view`() {
+ val message = ReaderViewFeature.createShowReaderMessage(null, 1234)
+ assertEquals(ReaderViewFeature.ACTION_SHOW, message[ReaderViewFeature.ACTION_MESSAGE_KEY])
+ val config = message[ReaderViewFeature.ACTION_VALUE] as JSONObject?
+ assertNotNull(config)
+ assertEquals(1234, config!![ReaderViewFeature.ACTION_VALUE_SCROLLY])
+ }
+
+ @Test
+ fun `hide reader view navigates back if possible`() {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+ val tab = createTab("https://www.mozilla.org", id = "test-tab", readerState = ReaderState(active = true))
+ val store = BrowserStore(initialState = BrowserState(tabs = listOf(tab)))
+ val readerViewFeature = ReaderViewFeature(testContext, engine, store, mock())
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+ store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
+ store.dispatch(ContentAction.UpdateBackNavigationStateAction(tab.id, true)).joinBlocking()
+
+ readerViewFeature.hideReaderView()
+ verify(engineSession).goBack(false)
+ }
+
+ @Test
+ fun `hide reader view sends message to web extension`() {
+ val port = mock<Port>()
+ val message = argumentCaptor<JSONObject>()
+ val readerViewFeature = prepareFeatureForTest(
+ readerActivePort = port,
+ tab = createTab("https://www.mozilla.org", id = "test-tab", readerState = ReaderState(active = true)),
+ )
+
+ readerViewFeature.hideReaderView()
+ verify(port, times(1)).postMessage(message.capture())
+ assertEquals(ReaderViewFeature.ACTION_HIDE, message.value[ReaderViewFeature.ACTION_MESSAGE_KEY])
+ }
+
+ @Test
+ fun `hide reader view updates state`() {
+ val engine: Engine = mock()
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ id = "test-tab",
+ readerState = ReaderState(active = true),
+ )
+
+ val store = spy(
+ BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ selectedTabId = tab.id,
+ ),
+ ),
+ )
+ val readerViewFeature = ReaderViewFeature(testContext, engine, store, mock())
+ readerViewFeature.hideReaderView()
+ verify(store).dispatch(ReaderAction.UpdateReaderActiveAction(tab.id, false))
+ verify(store).dispatch(ReaderAction.UpdateReaderableAction(tab.id, false))
+ verify(store).dispatch(ReaderAction.ClearReaderActiveUrlAction(tab.id))
+ }
+
+ @Test
+ fun `reader state check sends message to web extension`() {
+ val port = mock<Port>()
+ val message = argumentCaptor<JSONObject>()
+ val readerViewFeature = prepareFeatureForTest(port)
+
+ readerViewFeature.checkReaderState()
+ verify(port, times(1)).postMessage(message.capture())
+ assertEquals(ReaderViewFeature.ACTION_CHECK_READER_STATE, message.value[ReaderViewFeature.ACTION_MESSAGE_KEY])
+ }
+
+ @Test
+ fun `color scheme config change persists and is sent to web extension`() {
+ val port = mock<Port>()
+ val message = argumentCaptor<JSONObject>()
+
+ val readerViewFeature = prepareFeatureForTest(readerActivePort = port)
+ val prefs = testContext.getSharedPreferences(ReaderViewFeature.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+
+ readerViewFeature.config.colorScheme = ReaderViewFeature.ColorScheme.DARK
+ assertEquals(ReaderViewFeature.ColorScheme.DARK.name, prefs.getString(ReaderViewFeature.COLOR_SCHEME_KEY, null))
+
+ verify(port, times(1)).postMessage(message.capture())
+ assertEquals(ReaderViewFeature.ACTION_SET_COLOR_SCHEME, message.value[ReaderViewFeature.ACTION_MESSAGE_KEY])
+ assertEquals(ReaderViewFeature.ColorScheme.DARK.name, message.value[ReaderViewFeature.ACTION_VALUE])
+
+ // Setting to the same value should not cause another message to be sent
+ readerViewFeature.config.colorScheme = ReaderViewFeature.ColorScheme.DARK
+ verify(port, times(1)).postMessage(message.capture())
+ }
+
+ @Test
+ fun `font type config change persists and is sent to web extension`() {
+ val port = mock<Port>()
+ val message = argumentCaptor<JSONObject>()
+
+ val readerViewFeature = prepareFeatureForTest(readerActivePort = port)
+ val prefs = testContext.getSharedPreferences(ReaderViewFeature.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+
+ readerViewFeature.config.fontType = ReaderViewFeature.FontType.SANSSERIF
+ assertEquals(ReaderViewFeature.FontType.SANSSERIF.name, prefs.getString(ReaderViewFeature.FONT_TYPE_KEY, null))
+
+ verify(port, times(1)).postMessage(message.capture())
+ assertEquals(ReaderViewFeature.ACTION_SET_FONT_TYPE, message.value[ReaderViewFeature.ACTION_MESSAGE_KEY])
+ assertEquals(ReaderViewFeature.FontType.SANSSERIF.value, message.value[ReaderViewFeature.ACTION_VALUE])
+
+ // Setting to the same value should not cause another message to be sent
+ readerViewFeature.config.fontType = ReaderViewFeature.FontType.SANSSERIF
+ verify(port, times(1)).postMessage(message.capture())
+ }
+
+ @Test
+ fun `font size config change persists and is sent to web extension`() {
+ val port = mock<Port>()
+ val message = argumentCaptor<JSONObject>()
+
+ val readerViewFeature = prepareFeatureForTest(readerActivePort = port)
+ val prefs = testContext.getSharedPreferences(ReaderViewFeature.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+
+ readerViewFeature.config.fontSize = 4
+ assertEquals(4, prefs.getInt(ReaderViewFeature.FONT_SIZE_KEY, 0))
+
+ verify(port, times(1)).postMessage(message.capture())
+ assertEquals(ReaderViewFeature.ACTION_CHANGE_FONT_SIZE, message.value[ReaderViewFeature.ACTION_MESSAGE_KEY])
+ assertEquals(1, message.value[ReaderViewFeature.ACTION_VALUE])
+
+ // Setting to the same value should not cause another message to be sent
+ readerViewFeature.config.fontSize = 4
+ verify(port, times(1)).postMessage(message.capture())
+ }
+
+ @Test
+ fun `on back pressed hides controls`() {
+ val engine: Engine = mock()
+ val engineSession: EngineSession = mock()
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+ store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
+
+ val controlsView: ReaderViewControlsView = mock()
+ val view: View = mock()
+ whenever(controlsView.asView()).thenReturn(view)
+
+ val readerViewFeature = spy(ReaderViewFeature(testContext, engine, store, controlsView))
+ assertFalse(readerViewFeature.onBackPressed())
+
+ store.dispatch(ReaderAction.UpdateReaderActiveAction(tab.id, true)).joinBlocking()
+ whenever(view.visibility).thenReturn(View.VISIBLE)
+ assertTrue(readerViewFeature.onBackPressed())
+ verify(readerViewFeature, never()).hideReaderView(any())
+ verify(readerViewFeature, times(1)).hideControls()
+
+ whenever(view.visibility).thenReturn(View.GONE)
+ assertTrue(readerViewFeature.onBackPressed())
+ verify(readerViewFeature, times(1)).hideReaderView(any())
+ verify(readerViewFeature, times(1)).hideControls()
+ }
+
+ @Test
+ fun `state is updated when reader state arrives`() {
+ val engine: Engine = mock()
+ val view: ReaderViewControlsView = mock()
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val controller: WebExtensionController = mock()
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ id = "test-tab",
+ engineSession = engineSession,
+ )
+ val store = spy(
+ BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ selectedTabId = tab.id,
+ ),
+ ),
+ )
+
+ WebExtensionController.installedExtensions[ReaderViewFeature.READER_VIEW_EXTENSION_ID] = ext
+
+ val port: Port = mock()
+ whenever(port.engineSession).thenReturn(engineSession)
+ whenever(ext.getConnectedPort(any(), any())).thenReturn(port)
+
+ whenever(controller.portConnected(any(), any())).thenReturn(true)
+ val readerViewFeature = spy(ReaderViewFeature(testContext, engine, store, view))
+ readerViewFeature.extensionController = controller
+
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val message = argumentCaptor<JSONObject>()
+ readerViewFeature.start()
+ store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(tab.id, true)).joinBlocking()
+ verify(controller).registerContentMessageHandler(
+ eq(engineSession),
+ messageHandler.capture(),
+ eq(READER_VIEW_ACTIVE_CONTENT_PORT),
+ )
+
+ messageHandler.value.onPortConnected(port)
+ verify(port).postMessage(message.capture())
+ assertEquals(ReaderViewFeature.ACTION_CHECK_READER_STATE, message.value[ReaderViewFeature.ACTION_MESSAGE_KEY])
+
+ val readerStateMessage = JSONObject()
+ .put("readerable", true)
+ .put("baseUrl", "moz-extension://")
+ .put("activeUrl", "http://mozilla.org/article")
+ messageHandler.value.onPortMessage(readerStateMessage, port)
+ verify(store).dispatch(ReaderAction.UpdateReaderableAction(tab.id, true))
+ verify(store).dispatch(ReaderAction.UpdateReaderBaseUrlAction(tab.id, "moz-extension://"))
+ verify(store).dispatch(ReaderAction.UpdateReaderActiveUrlAction(tab.id, "http://mozilla.org/article"))
+ }
+
+ @Test
+ fun `reader is shown when state arrives from reader page`() {
+ val engine: Engine = mock()
+ val view: ReaderViewControlsView = mock()
+ val engineSession: EngineSession = mock()
+ val ext: WebExtension = mock()
+ val controller: WebExtensionController = mock()
+ val tab = createTab(
+ url = "https://www.mozilla.org",
+ id = "test-tab",
+ engineSession = engineSession,
+ )
+ val store = spy(
+ BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ selectedTabId = tab.id,
+ ),
+ ),
+ )
+
+ WebExtensionController.installedExtensions[READER_VIEW_EXTENSION_ID] = ext
+
+ val port: Port = mock()
+ whenever(port.engineSession).thenReturn(engineSession)
+ whenever(ext.getConnectedPort(any(), any())).thenReturn(port)
+
+ whenever(controller.portConnected(any(), any())).thenReturn(true)
+ val readerViewFeature = spy(ReaderViewFeature(testContext, engine, store, view))
+ readerViewFeature.extensionController = controller
+
+ val messageHandler = argumentCaptor<MessageHandler>()
+ val message = argumentCaptor<JSONObject>()
+ readerViewFeature.start()
+
+ store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(tab.id, true)).joinBlocking()
+ verify(controller).registerContentMessageHandler(
+ eq(engineSession),
+ messageHandler.capture(),
+ eq(READER_VIEW_ACTIVE_CONTENT_PORT),
+ )
+ messageHandler.value.onPortConnected(port)
+
+ val readerStateMessage = JSONObject()
+ .put("readerable", true)
+ .put("baseUrl", "moz-extension://")
+ .put("activeUrl", "http://mozilla.org/article")
+ messageHandler.value.onPortMessage(readerStateMessage, port)
+ verify(port, times(2)).postMessage(message.capture())
+ assertEquals(ReaderViewFeature.ACTION_CHECK_READER_STATE, message.allValues[0][ReaderViewFeature.ACTION_MESSAGE_KEY])
+ assertEquals(ReaderViewFeature.ACTION_SHOW, message.allValues[1][ReaderViewFeature.ACTION_MESSAGE_KEY])
+ }
+
+ private fun prepareFeatureForTest(
+ contentPort: Port? = null,
+ readerActivePort: Port? = null,
+ tab: TabSessionState = createTab("https://www.mozilla.org", id = "test-tab"),
+ engineSession: EngineSession = mock(),
+ controller: WebExtensionController? = null,
+ createUUID: UUIDCreator = { "" },
+ ): ReaderViewFeature {
+ val engine: Engine = mock()
+
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+ store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
+
+ val ext: WebExtension = mock()
+ contentPort?.let {
+ whenever(ext.getConnectedPort(eq(ReaderViewFeature.READER_VIEW_CONTENT_PORT), any()))
+ .thenReturn(contentPort)
+ }
+ readerActivePort?.let {
+ whenever(ext.getConnectedPort(eq(ReaderViewFeature.READER_VIEW_ACTIVE_CONTENT_PORT), any()))
+ .thenReturn(readerActivePort)
+ }
+ WebExtensionController.installedExtensions[ReaderViewFeature.READER_VIEW_EXTENSION_ID] = ext
+
+ val feature = ReaderViewFeature(testContext, engine, store, mock(), createUUID)
+ if (controller != null) {
+ feature.extensionController = controller
+ }
+ return feature
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewMiddlewareTest.kt b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewMiddlewareTest.kt
new file mode 100644
index 0000000000..5573954e86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewMiddlewareTest.kt
@@ -0,0 +1,187 @@
+/* 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.readerview
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.ReaderAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ReaderState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.READER_VIEW_EXTENSION_ID
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.webextensions.WebExtensionController
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ReaderViewMiddlewareTest {
+
+ @Test
+ fun `state is updated to connect content script port when tab is added and engine session linked`() {
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(ReaderViewMiddleware()),
+ )
+
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, mock())).joinBlocking()
+
+ val readerState = store.state.findTab(tab.id)!!.readerState
+ assertTrue(readerState.connectRequired)
+ }
+
+ @Test
+ fun `state is updated to disconnect content script port when tab is removed`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab")
+ val engineSession: EngineSession = mock()
+ val controller: WebExtensionController = mock()
+ val middleware = ReaderViewMiddleware().apply {
+ extensionController = controller
+ }
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
+ assertSame(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
+ verify(controller, never()).disconnectPort(engineSession, READER_VIEW_EXTENSION_ID)
+
+ store.dispatch(EngineAction.UnlinkEngineSessionAction(tab.id)).joinBlocking()
+ assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
+ verify(controller).disconnectPort(engineSession, READER_VIEW_EXTENSION_ID)
+ }
+
+ @Test
+ fun `state is updated to reconnect and trigger readerable check when new tab is selected`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "test-tab1")
+ val tab2 = createTab(
+ "https://www.firefox.com",
+ id = "test-tab2",
+ readerState = ReaderState(readerable = true),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab1, tab2)),
+ middleware = listOf(ReaderViewMiddleware()),
+ )
+ assertFalse(store.state.findTab(tab1.id)!!.readerState.connectRequired)
+ assertFalse(store.state.findTab(tab1.id)!!.readerState.checkRequired)
+ assertFalse(store.state.findTab(tab1.id)!!.readerState.readerable)
+ assertFalse(store.state.findTab(tab2.id)!!.readerState.connectRequired)
+ assertFalse(store.state.findTab(tab2.id)!!.readerState.checkRequired)
+ assertTrue(store.state.findTab(tab2.id)!!.readerState.readerable)
+
+ store.dispatch(TabListAction.SelectTabAction(tab2.id)).joinBlocking()
+ assertFalse(store.state.findTab(tab1.id)!!.readerState.readerable)
+ assertFalse(store.state.findTab(tab1.id)!!.readerState.checkRequired)
+ assertFalse(store.state.findTab(tab1.id)!!.readerState.connectRequired)
+ assertFalse(store.state.findTab(tab2.id)!!.readerState.readerable)
+ assertTrue(store.state.findTab(tab2.id)!!.readerState.checkRequired)
+ assertTrue(store.state.findTab(tab2.id)!!.readerState.connectRequired)
+ }
+
+ @Test
+ fun `state is updated to trigger readerable check when URL changes`() {
+ val tab = createTab("https://www.mozilla.org", id = "test-tab1")
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(ReaderViewMiddleware()),
+ )
+ assertFalse(store.state.findTab(tab.id)!!.readerState.checkRequired)
+
+ store.dispatch(ContentAction.UpdateUrlAction(tab.id, "https://www.firefox.com")).joinBlocking()
+ assertTrue(store.state.findTab(tab.id)!!.readerState.checkRequired)
+ }
+
+ @Test
+ fun `state is updated to enter and leave reader view when URL changes`() {
+ val tab = createTab(
+ "https://www.mozilla.org",
+ id = "test-tab1",
+ readerState = ReaderState(active = false, baseUrl = "moz-extension://123"),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(ReaderViewMiddleware()),
+ )
+ assertFalse(store.state.findTab(tab.id)!!.readerState.active)
+
+ store.dispatch(ContentAction.UpdateUrlAction(tab.id, "moz-extension://123?url=articleLink")).joinBlocking()
+ assertTrue(store.state.findTab(tab.id)!!.readerState.active)
+
+ store.dispatch(ContentAction.UpdateUrlAction(tab.id, "https://www.firefox.com")).joinBlocking()
+ assertFalse(store.state.findTab(tab.id)!!.readerState.active)
+ assertFalse(store.state.findTab(tab.id)!!.readerState.readerable)
+ assertTrue(store.state.findTab(tab.id)!!.readerState.checkRequired)
+ assertNull(store.state.findTab(tab.id)!!.readerState.activeUrl)
+ }
+
+ @Test
+ fun `state is updated to mask extension page URL when navigating to reader view page`() {
+ val tab = createTab(
+ "https://www.mozilla.org",
+ id = "test-tab1",
+ readerState = ReaderState(
+ active = true,
+ baseUrl = "moz-extension://123",
+ activeUrl = "https://mozilla.org/article1",
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(ReaderViewMiddleware()),
+ )
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(
+ tab.id,
+ "moz-extension://123?url=https%3A%2F%2Fmozilla.org%2Farticle1",
+ ),
+ ).joinBlocking()
+
+ assertTrue(store.state.findTab(tab.id)!!.readerState.active)
+ assertEquals("https://mozilla.org/article1", store.state.findTab(tab.id)!!.content.url)
+ }
+
+ @Test
+ fun `state is updated to mask extension page URL when reader view connects`() {
+ val tab = createTab(
+ "moz-extension://123?url=https%3A%2F%2Fmozilla.org%2Farticle1",
+ id = "test-tab1",
+ readerState = ReaderState(
+ active = true,
+ baseUrl = "moz-extension://123",
+ ),
+ )
+ val store = BrowserStore(
+ initialState = BrowserState(tabs = listOf(tab)),
+ middleware = listOf(ReaderViewMiddleware()),
+ )
+
+ store.dispatch(
+ ReaderAction.UpdateReaderActiveUrlAction(
+ tab.id,
+ activeUrl = "https://mozilla.org/article1",
+ ),
+ ).joinBlocking()
+
+ assertEquals("https://mozilla.org/article1", store.state.findTab(tab.id)!!.content.url)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewConfigTest.kt b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewConfigTest.kt
new file mode 100644
index 0000000000..13b0ec9b25
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewConfigTest.kt
@@ -0,0 +1,108 @@
+/* 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.readerview.internal
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.content.res.Resources
+import mozilla.components.feature.readerview.ReaderViewFeature
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.COLOR_SCHEME_KEY
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.FONT_SIZE_DEFAULT
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.FONT_SIZE_KEY
+import mozilla.components.feature.readerview.ReaderViewFeature.Companion.FONT_TYPE_KEY
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.whenever
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+class ReaderViewConfigTest {
+
+ @Mock private lateinit var context: Context
+
+ @Mock private lateinit var prefs: SharedPreferences
+
+ @Mock private lateinit var editor: SharedPreferences.Editor
+
+ @Mock private lateinit var sendConfigMessage: (JSONObject) -> Unit
+
+ @Mock private lateinit var resources: Resources
+
+ @Mock private lateinit var configuration: Configuration
+
+ private lateinit var config: ReaderViewConfig
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs)
+ whenever(prefs.edit()).thenReturn(editor)
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.configuration).thenReturn(configuration)
+ configuration.uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
+
+ config = ReaderViewConfig(context, sendConfigMessage)
+ }
+
+ @Test
+ fun `color scheme should read from shared prefs`() {
+ whenever(prefs.getString(COLOR_SCHEME_KEY, "LIGHT")).thenReturn("SEPIA")
+ verify(prefs, never()).getString(eq(COLOR_SCHEME_KEY), anyString())
+
+ assertEquals(ReaderViewFeature.ColorScheme.SEPIA, config.colorScheme)
+ verify(prefs, times(1)).getString(eq(COLOR_SCHEME_KEY), anyString())
+
+ assertEquals(ReaderViewFeature.ColorScheme.SEPIA, config.colorScheme)
+ verify(prefs, times(1)).getString(eq(COLOR_SCHEME_KEY), anyString())
+ }
+
+ @Test
+ fun `color scheme default should respect active dark mode`() {
+ whenever(prefs.getString(COLOR_SCHEME_KEY, "LIGHT")).thenReturn("LIGHT")
+ whenever(prefs.getString(COLOR_SCHEME_KEY, "DARK")).thenReturn("DARK")
+ // reset config and test for a default of DARK for dark mode
+ configuration.uiMode = Configuration.UI_MODE_NIGHT_YES
+ config = ReaderViewConfig(context, sendConfigMessage)
+ assertEquals(ReaderViewFeature.ColorScheme.DARK, config.colorScheme)
+ // reset config and test for a default of LIGHT for explicit non-dark (light) mode
+ configuration.uiMode = Configuration.UI_MODE_NIGHT_NO
+ config = ReaderViewConfig(context, sendConfigMessage)
+ assertEquals(ReaderViewFeature.ColorScheme.LIGHT, config.colorScheme)
+ // test for UI_MODE_NIGHT_UNDEFINED was already done in the above test function
+ }
+
+ @Test
+ fun `font type should read from shared prefs`() {
+ whenever(prefs.getString(FONT_TYPE_KEY, "SERIF")).thenReturn("SANSSERIF")
+ verify(prefs, never()).getString(eq(FONT_TYPE_KEY), anyString())
+
+ assertEquals(ReaderViewFeature.FontType.SANSSERIF, config.fontType)
+ verify(prefs, times(1)).getString(eq(FONT_TYPE_KEY), anyString())
+
+ assertEquals(ReaderViewFeature.FontType.SANSSERIF, config.fontType)
+ verify(prefs, times(1)).getString(eq(FONT_TYPE_KEY), anyString())
+ }
+
+ @Test
+ fun `font size should read from shared prefs`() {
+ whenever(prefs.getInt(FONT_SIZE_KEY, FONT_SIZE_DEFAULT)).thenReturn(4)
+ verify(prefs, never()).getInt(eq(FONT_SIZE_KEY), anyInt())
+
+ assertEquals(4, config.fontSize)
+ verify(prefs, times(1)).getInt(eq(FONT_SIZE_KEY), anyInt())
+
+ assertEquals(4, config.fontSize)
+ verify(prefs, times(1)).getInt(eq(FONT_SIZE_KEY), anyInt())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewControlsInteractorTest.kt b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewControlsInteractorTest.kt
new file mode 100644
index 0000000000..0db805b7c3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewControlsInteractorTest.kt
@@ -0,0 +1,90 @@
+/* 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.readerview.internal
+
+import mozilla.components.feature.readerview.ReaderViewFeature
+import mozilla.components.feature.readerview.view.ReaderViewControlsView
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+class ReaderViewControlsInteractorTest {
+
+ @Test
+ fun `interactor assigns listener to self`() {
+ val view = mock<ReaderViewControlsView>()
+ val interactor = ReaderViewControlsInteractor(view, mock())
+
+ interactor.start()
+
+ verify(view).listener = interactor
+ verifyNoMoreInteractions(view)
+ }
+
+ @Test
+ fun `interactor un-assigns self from listener`() {
+ val view = mock<ReaderViewControlsView>()
+ val interactor = ReaderViewControlsInteractor(view, mock())
+
+ interactor.stop()
+
+ verify(view).listener = null
+ verifyNoMoreInteractions(view)
+ }
+
+ @Test
+ fun `update config on change`() {
+ val config: ReaderViewConfig = mock()
+ val interactor = ReaderViewControlsInteractor(mock(), config)
+
+ interactor.onFontChanged(ReaderViewFeature.FontType.SANSSERIF)
+
+ verify(config).fontType = ReaderViewFeature.FontType.SANSSERIF
+
+ interactor.onColorSchemeChanged(ReaderViewFeature.ColorScheme.SEPIA)
+
+ verify(config).colorScheme = ReaderViewFeature.ColorScheme.SEPIA
+ }
+
+ @Test
+ fun `update config when font size increased`() {
+ val config: ReaderViewConfig = mock()
+ val interactor = ReaderViewControlsInteractor(mock(), config)
+
+ whenever(config.fontSize).thenReturn(7)
+ interactor.onFontSizeIncreased()
+ verify(config).fontSize = 8
+
+ whenever(config.fontSize).thenReturn(8)
+ interactor.onFontSizeIncreased()
+ verify(config).fontSize = 9
+
+ // Can't increase past MAX_FONT_SIZE
+ whenever(config.fontSize).thenReturn(9)
+ interactor.onFontSizeIncreased()
+ verify(config).fontSize = 9
+ }
+
+ @Test
+ fun `update config when font size decreased`() {
+ val config: ReaderViewConfig = mock()
+ val interactor = ReaderViewControlsInteractor(mock(), config)
+
+ whenever(config.fontSize).thenReturn(3)
+ interactor.onFontSizeDecreased()
+ verify(config).fontSize = 2
+
+ whenever(config.fontSize).thenReturn(2)
+ interactor.onFontSizeDecreased()
+ verify(config).fontSize = 1
+
+ // Can't decrease below MIN_FONT_SIZE
+ whenever(config.fontSize).thenReturn(1)
+ interactor.onFontSizeDecreased()
+ verify(config).fontSize = 1
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewControlsPresenterTest.kt b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewControlsPresenterTest.kt
new file mode 100644
index 0000000000..399bceaea6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/internal/ReaderViewControlsPresenterTest.kt
@@ -0,0 +1,61 @@
+/* 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.readerview.internal
+
+import android.view.View
+import mozilla.components.feature.readerview.view.ReaderViewControlsView
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class ReaderViewControlsPresenterTest {
+
+ @Test
+ fun `start applies config to view`() {
+ val config: ReaderViewConfig = mock()
+ val view = mock<ReaderViewControlsView>()
+ val presenter = ReaderViewControlsPresenter(view, config)
+
+ whenever(config.colorScheme).thenReturn(mock())
+ whenever(config.fontSize).thenReturn(5)
+ whenever(config.fontType).thenReturn(mock())
+
+ presenter.show()
+
+ verify(view).tryInflate()
+ verify(view).setColorScheme(any())
+ verify(view).setFontSize(5)
+ verify(view).setFont(any())
+ verify(view).showControls()
+ }
+
+ @Test
+ fun `are controls visible`() {
+ val controlsView = mock<ReaderViewControlsView>()
+ val view = mock<View>()
+ whenever(controlsView.asView()).thenReturn(view)
+ val presenter = ReaderViewControlsPresenter(controlsView, mock())
+
+ whenever(view.visibility).thenReturn(View.GONE)
+ assertFalse(presenter.areControlsVisible())
+
+ whenever(view.visibility).thenReturn(View.VISIBLE)
+ assertTrue(presenter.areControlsVisible())
+ }
+
+ @Test
+ fun `hide updates the visibility of the controls`() {
+ val view = mock<ReaderViewControlsView>()
+ val presenter = ReaderViewControlsPresenter(view, mock())
+
+ presenter.hide()
+
+ verify(view).hideControls()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/view/ReaderViewControlsBarTest.kt b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/view/ReaderViewControlsBarTest.kt
new file mode 100644
index 0000000000..0af16267b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/view/ReaderViewControlsBarTest.kt
@@ -0,0 +1,204 @@
+/* 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.readerview.view
+
+import android.view.View
+import androidx.appcompat.widget.AppCompatButton
+import androidx.appcompat.widget.AppCompatRadioButton
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.readerview.R
+import mozilla.components.feature.readerview.ReaderViewFeature
+import mozilla.components.support.test.mock
+import mozilla.ext.appCompatContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ReaderViewControlsBarTest {
+
+ @Test
+ fun `flags are set on UI init`() {
+ val bar = spy(ReaderViewControlsBar(appCompatContext))
+
+ assertTrue(bar.isFocusableInTouchMode)
+ assertTrue(bar.isClickable)
+ }
+
+ @Test
+ fun `font options are set`() {
+ val bar = ReaderViewControlsBar(appCompatContext)
+ bar.tryInflate()
+
+ val serifButton = bar.findViewById<AppCompatRadioButton>(R.id.mozac_feature_readerview_font_serif)
+ val sansSerifButton = bar.findViewById<AppCompatRadioButton>(R.id.mozac_feature_readerview_font_sans_serif)
+
+ assertFalse(serifButton.isChecked)
+
+ bar.setFont(ReaderViewFeature.FontType.SERIF)
+
+ assertTrue(serifButton.isChecked)
+
+ assertFalse(sansSerifButton.isChecked)
+
+ bar.setFont(ReaderViewFeature.FontType.SANSSERIF)
+
+ assertTrue(sansSerifButton.isChecked)
+ }
+
+ @Test
+ fun `font size buttons are enabled or disabled`() {
+ val bar = ReaderViewControlsBar(appCompatContext)
+ bar.tryInflate()
+
+ val sizeDecreaseButton = bar.findViewById<AppCompatButton>(R.id.mozac_feature_readerview_font_size_decrease)
+ val sizeIncreaseButton = bar.findViewById<AppCompatButton>(R.id.mozac_feature_readerview_font_size_increase)
+
+ bar.setFontSize(5)
+
+ assertTrue(sizeDecreaseButton.isEnabled)
+ assertTrue(sizeIncreaseButton.isEnabled)
+
+ bar.setFontSize(1)
+
+ assertFalse(sizeDecreaseButton.isEnabled)
+ assertTrue(sizeIncreaseButton.isEnabled)
+
+ bar.setFontSize(0)
+
+ assertFalse(sizeDecreaseButton.isEnabled)
+ assertTrue(sizeIncreaseButton.isEnabled)
+
+ bar.setFontSize(9)
+
+ assertTrue(sizeDecreaseButton.isEnabled)
+ assertFalse(sizeIncreaseButton.isEnabled)
+
+ bar.setFontSize(10)
+
+ assertTrue(sizeDecreaseButton.isEnabled)
+ assertFalse(sizeIncreaseButton.isEnabled)
+ }
+
+ @Test
+ fun `color scheme is set`() {
+ val bar = ReaderViewControlsBar(appCompatContext)
+ bar.tryInflate()
+
+ val colorOptionDark = bar.findViewById<AppCompatRadioButton>(R.id.mozac_feature_readerview_color_dark)
+ val colorOptionSepia = bar.findViewById<AppCompatRadioButton>(R.id.mozac_feature_readerview_color_sepia)
+ val colorOptionLight = bar.findViewById<AppCompatRadioButton>(R.id.mozac_feature_readerview_color_light)
+
+ bar.setColorScheme(ReaderViewFeature.ColorScheme.DARK)
+
+ assertTrue(colorOptionDark.isChecked)
+ assertFalse(colorOptionSepia.isChecked)
+ assertFalse(colorOptionLight.isChecked)
+
+ bar.setColorScheme(ReaderViewFeature.ColorScheme.SEPIA)
+
+ assertFalse(colorOptionDark.isChecked)
+ assertTrue(colorOptionSepia.isChecked)
+ assertFalse(colorOptionLight.isChecked)
+
+ bar.setColorScheme(ReaderViewFeature.ColorScheme.LIGHT)
+
+ assertFalse(colorOptionDark.isChecked)
+ assertFalse(colorOptionSepia.isChecked)
+ assertTrue(colorOptionLight.isChecked)
+ }
+
+ @Test
+ fun `showControls updates visibility and requests focus`() {
+ val bar = spy(ReaderViewControlsBar(appCompatContext))
+
+ bar.showControls()
+
+ verify(bar).visibility = View.VISIBLE
+ verify(bar).requestFocus()
+ }
+
+ @Test
+ fun `hideControls updates visibility`() {
+ val bar = spy(ReaderViewControlsBar(appCompatContext))
+
+ bar.hideControls()
+
+ verify(bar).visibility = View.GONE
+ }
+
+ @Test
+ fun `when focus is lost, hide controls`() {
+ val bar = spy(ReaderViewControlsBar(appCompatContext))
+
+ bar.clearFocus()
+
+ verify(bar, never()).hideControls()
+
+ bar.requestFocus()
+
+ bar.clearFocus()
+
+ verify(bar).hideControls()
+ }
+
+ @Test
+ fun `listener is invoked when clicking a font option`() {
+ val bar = ReaderViewControlsBar(appCompatContext)
+ val listener: ReaderViewControlsView.Listener = mock()
+
+ assertNull(bar.listener)
+
+ bar.listener = listener
+ bar.tryInflate()
+
+ bar.findViewById<AppCompatRadioButton>(R.id.mozac_feature_readerview_font_sans_serif).performClick()
+
+ verify(listener).onFontChanged(ReaderViewFeature.FontType.SANSSERIF)
+ }
+
+ @Test
+ fun `listener is invoked when clicking a font size option`() {
+ val bar = ReaderViewControlsBar(appCompatContext)
+ val listener: ReaderViewControlsView.Listener = mock()
+
+ assertNull(bar.listener)
+
+ bar.listener = listener
+ bar.tryInflate()
+
+ bar.findViewById<AppCompatButton>(R.id.mozac_feature_readerview_font_size_increase).performClick()
+
+ verify(listener).onFontSizeIncreased()
+ }
+
+ @Test
+ fun `listener is invoked when clicking a color scheme`() {
+ val bar = ReaderViewControlsBar(appCompatContext)
+ val listener: ReaderViewControlsView.Listener = mock()
+
+ assertNull(bar.listener)
+
+ bar.listener = listener
+ bar.tryInflate()
+
+ bar.findViewById<AppCompatRadioButton>(R.id.mozac_feature_readerview_color_sepia).performClick()
+
+ verify(listener).onColorSchemeChanged(ReaderViewFeature.ColorScheme.SEPIA)
+ }
+
+ @Test
+ fun `tryInflate is only successfully once`() {
+ val bar = ReaderViewControlsBar(appCompatContext)
+
+ assertTrue(bar.tryInflate())
+ assertFalse(bar.tryInflate())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/ext/context.kt b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/ext/context.kt
new file mode 100644
index 0000000000..64575ce095
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/java/mozilla/ext/context.kt
@@ -0,0 +1,18 @@
+/* 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.ext
+
+import android.content.Context
+import androidx.appcompat.R
+import androidx.appcompat.view.ContextThemeWrapper
+import mozilla.components.support.test.robolectric.testContext
+
+/**
+ * `testContext` wrapped with AppCompat theme.
+ *
+ * Useful for views that uses theme attributes, for example.
+ */
+internal val appCompatContext: Context
+ get() = ContextThemeWrapper(testContext, R.style.Theme_AppCompat)
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/readerview/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..49324d83c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,3 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
+
diff --git a/mobile/android/android-components/components/feature/readerview/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/readerview/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/readerview/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/README.md b/mobile/android/android-components/components/feature/recentlyclosed/README.md
new file mode 100644
index 0000000000..893f648919
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > RecentlyClosed
+
+Feature implementation for saving and restoring of recently closed tabs.
+
+## Usage
+
+### 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-recentlyclosed:{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/recentlyclosed/build.gradle b/mobile/android/android-components/components/feature/recentlyclosed/build.gradle
new file mode 100644
index 0000000000..586afb1edc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/build.gradle
@@ -0,0 +1,81 @@
+/* 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/. */
+
+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"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/proguard/androidx-annotations.pro'
+ }
+
+ sourceSets {
+ test.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.recentlyclosed'
+}
+
+dependencies {
+ implementation project(':concept-engine')
+
+ implementation project(':browser-state')
+ implementation project(':browser-session-storage')
+
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':feature-session')
+ 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_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.kotlin_coroutines
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation project(':support-test-fakes')
+
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+}
+
+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/recentlyclosed/proguard-rules.pro b/mobile/android/android-components/components/feature/recentlyclosed/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/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/recentlyclosed/schemas/mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase/1.json b/mobile/android/android-components/components/feature/recentlyclosed/schemas/mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase/1.json
new file mode 100644
index 0000000000..c14a8fe68d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/schemas/mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase/1.json
@@ -0,0 +1,52 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "e7ff8844186c753ba34fbc5a6aabd320",
+ "entities": [
+ {
+ "tableName": "recently_closed_tabs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`uuid`))",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "uuid"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e7ff8844186c753ba34fbc5a6aabd320')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/androidTest/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageOnDeviceTest.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/androidTest/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageOnDeviceTest.kt
new file mode 100644
index 0000000000..4a6ee0923f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/androidTest/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageOnDeviceTest.kt
@@ -0,0 +1,103 @@
+/* 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.recentlyclosed
+
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.EngineSessionStateStorage
+import mozilla.components.support.test.fakes.engine.FakeEngine
+import mozilla.components.support.test.fakes.engine.FakeEngineSessionState
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class RecentlyClosedTabsStorageOnDeviceTest {
+ private val engineState = FakeEngineSessionState("testId")
+ private val storage = RecentlyClosedTabsStorage(
+ context = ApplicationProvider.getApplicationContext(),
+ engine = FakeEngine(),
+ crashReporting = FakeCrashReporting(),
+ engineStateStorage = FakeEngineSessionStateStorage(),
+ )
+
+ @Test
+ fun testRowTooBigExceptionCaughtAndStorageCleared() = runBlocking {
+ val closedTab1 = RecoverableTab(
+ engineSessionState = engineState,
+ state = TabState(
+ id = "test",
+ title = "Pocket",
+ url = "test",
+ lastAccess = System.currentTimeMillis(),
+ ),
+ )
+ val closedTab2 = closedTab1.copy(
+ state = closedTab1.state.copy(
+ url = "test".repeat(1_000_000), // much more than 2MB of data. Just to be sure.
+ ),
+ )
+
+ // First check what happens if too large tabs are persisted and then asked for
+ storage.addTabsToCollectionWithMax(listOf(closedTab1, closedTab2), 2)
+ assertFalse((storage.engineStateStorage() as FakeEngineSessionStateStorage).data.isEmpty())
+ val corruptedTabsResult = storage.getTabs().first()
+ assertTrue(corruptedTabsResult.isEmpty())
+ assertTrue((storage.engineStateStorage() as FakeEngineSessionStateStorage).data.isEmpty())
+
+ // Then check that new data is persisted and queried successfully
+ val closedTab3 = RecoverableTab(
+ engineSessionState = engineState,
+ state = TabState(
+ id = "test2",
+ title = "Pocket2",
+ url = "test2",
+ lastAccess = System.currentTimeMillis(),
+ ),
+ )
+ storage.addTabState(closedTab3)
+ val recentlyClosedTabsResult = storage.getTabs().first()
+ assertEquals(listOf(closedTab3.state), recentlyClosedTabsResult)
+ assertEquals(1, (storage.engineStateStorage() as FakeEngineSessionStateStorage).data.size)
+ }
+}
+
+private class FakeCrashReporting : CrashReporting {
+ override fun submitCaughtException(throwable: Throwable): Job {
+ return MainScope().launch {}
+ }
+
+ override fun recordCrashBreadcrumb(breadcrumb: Breadcrumb) {}
+}
+
+private class FakeEngineSessionStateStorage : EngineSessionStateStorage {
+ val data: MutableMap<String, EngineSessionState?> = mutableMapOf()
+
+ override suspend fun write(uuid: String, state: EngineSessionState): Boolean {
+ data[uuid] = state
+ return true
+ }
+
+ override suspend fun read(uuid: String): EngineSessionState? {
+ return data[uuid]
+ }
+
+ override suspend fun delete(uuid: String) {
+ data.remove(uuid)
+ }
+
+ override suspend fun deleteAll() {
+ data.clear()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/recentlyclosed/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/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/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddleware.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddleware.kt
new file mode 100644
index 0000000000..6abfb19650
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddleware.kt
@@ -0,0 +1,144 @@
+/* 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.recentlyclosed
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.action.RecentlyClosedAction
+import mozilla.components.browser.state.action.UndoAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+
+/**
+ * [Middleware] implementation for handling [RecentlyClosedAction]s and syncing the closed tabs in
+ * [BrowserState.closedTabs] with the [RecentlyClosedTabsStorage].
+ */
+class RecentlyClosedMiddleware(
+ private val storage: Lazy<Storage>,
+ private val maxSavedTabs: Int,
+ private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+) : Middleware<BrowserState, BrowserAction> {
+
+ @Suppress("ComplexMethod")
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is UndoAction.ClearRecoverableTabs -> {
+ if (action.tag == context.state.undoHistory.tag) {
+ // If the user has removed tabs and not invoked "undo" then let's save all non
+ // private tabs.
+ context.store.dispatch(
+ RecentlyClosedAction.AddClosedTabsAction(
+ context.state.undoHistory.tabs.filter { tab -> !tab.state.private },
+ ),
+ )
+ }
+ }
+ is UndoAction.AddRecoverableTabs -> {
+ if (context.state.undoHistory.tabs.isNotEmpty()) {
+ // If new tabs get added to the undo history and there were some previously
+ // then add them to the list of closed tabs now since they will never go through
+ // the clear call above.
+ context.store.dispatch(
+ RecentlyClosedAction.AddClosedTabsAction(
+ context.state.undoHistory.tabs.filter { tab -> !tab.state.private },
+ ),
+ )
+ }
+ }
+ is RecentlyClosedAction.AddClosedTabsAction -> {
+ addTabsToStorage(action.tabs)
+ }
+ is RecentlyClosedAction.RemoveAllClosedTabAction -> {
+ removeAllTabs()
+ }
+ is RecentlyClosedAction.RemoveClosedTabAction -> {
+ removeTab(action)
+ }
+ is InitAction -> {
+ initializeRecentlyClosed(context.store)
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+
+ pruneTabs(context.store)
+ }
+
+ private fun pruneTabs(store: Store<BrowserState, BrowserAction>) {
+ if (store.state.closedTabs.size > maxSavedTabs) {
+ store.dispatch(RecentlyClosedAction.PruneClosedTabsAction(maxSavedTabs))
+ }
+ }
+
+ private fun initializeRecentlyClosed(
+ store: Store<BrowserState, BrowserAction>,
+ ) = scope.launch {
+ storage.value.getTabs().collect { tabs ->
+ store.dispatch(RecentlyClosedAction.ReplaceTabsAction(tabs))
+ }
+ }
+
+ private fun addTabsToStorage(
+ tabList: List<RecoverableTab>,
+ ) = scope.launch {
+ storage.value.addTabsToCollectionWithMax(
+ tabList,
+ maxSavedTabs,
+ )
+ }
+
+ private fun removeTab(
+ action: RecentlyClosedAction.RemoveClosedTabAction,
+ ) = scope.launch {
+ storage.value.removeTab(action.tab)
+ }
+
+ private fun removeAllTabs() = scope.launch {
+ storage.value.removeAllTabs()
+ }
+
+ /**
+ * Interface for a storage saving snapshots of recently closed tabs / sessions.
+ */
+ interface Storage {
+ /**
+ * Returns an observable list of recently closed tabs as List of [RecoverableTab]s.
+ */
+ suspend fun getTabs(): Flow<List<TabState>>
+
+ /**
+ * Removes the given saved [RecoverableTab].
+ */
+ suspend fun removeTab(recentlyClosedTab: TabState)
+
+ /**
+ * Removes all saved [RecoverableTab]s.
+ */
+ suspend fun removeAllTabs()
+
+ /**
+ * Adds up to [maxTabs] [TabSessionState]s to storage, and then prunes storage to keep only
+ * the newest [maxTabs].
+ */
+ suspend fun addTabsToCollectionWithMax(tabs: List<RecoverableTab>, maxTabs: Int)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorage.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorage.kt
new file mode 100644
index 0000000000..ee4f68f8fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorage.kt
@@ -0,0 +1,136 @@
+/* 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.recentlyclosed
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.session.storage.FileEngineSessionStateStorage
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSessionStateStorage
+import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase
+import mozilla.components.feature.recentlyclosed.db.toRecentlyClosedTabEntity
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Wraps exceptions that are caught by [RecentlyClosedTabsStorage].
+ * Instances of this class are submitted via [CrashReporting]. This wrapping helps easily identify
+ * exceptions related to [RecentlyClosedTabsStorage].
+ */
+private class RecentlyClosedTabsStorageException(e: Throwable) : Throwable(e)
+
+/**
+ * A storage implementation that saves snapshots of recently closed tabs / sessions.
+ */
+class RecentlyClosedTabsStorage(
+ context: Context,
+ engine: Engine,
+ private val crashReporting: CrashReporting,
+ private val engineStateStorage: EngineSessionStateStorage = FileEngineSessionStateStorage(context, engine),
+) : RecentlyClosedMiddleware.Storage {
+ private val logger = Logger("RecentlyClosedTabsStorage")
+
+ @VisibleForTesting
+ internal var database: Lazy<RecentlyClosedTabsDatabase> =
+ lazy { RecentlyClosedTabsDatabase.get(context) }
+
+ /**
+ * Returns an observable list of [TabState]s.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ override suspend fun getTabs(): Flow<List<TabState>> {
+ return database.value.recentlyClosedTabDao().getTabs()
+ .catch { exception ->
+ crashReporting.submitCaughtException(RecentlyClosedTabsStorageException(exception))
+ // If the database is "corrupted" then we clean the database and also the file storage
+ // to allow for a fresh set of recently closed tabs later.
+ removeAllTabs()
+ // Inform all observers of this data that recent tabs are cleared
+ // to prevent users from trying to restore nonexistent recently closed tabs.
+ emit(emptyList())
+ }
+ .map { list ->
+ list.map { it.asTabState() }
+ }
+ }
+
+ /**
+ * Removes the given [TabState].
+ */
+ override suspend fun removeTab(recentlyClosedTab: TabState) {
+ val entity = recentlyClosedTab.toRecentlyClosedTabEntity()
+ engineStateStorage.delete(entity.uuid)
+ database.value.recentlyClosedTabDao().deleteTab(entity)
+ }
+
+ /**
+ * Removes all [TabState]s.
+ */
+ override suspend fun removeAllTabs() {
+ engineStateStorage.deleteAll()
+ database.value.recentlyClosedTabDao().removeAllTabs()
+ }
+
+ /**
+ * Adds up to [maxTabs] [TabSessionState]s to storage, and then prunes storage to keep only the newest [maxTabs].
+ */
+ @Suppress("TooGenericExceptionCaught")
+ override suspend fun addTabsToCollectionWithMax(
+ tabs: List<RecoverableTab>,
+ maxTabs: Int,
+ ) {
+ try {
+ tabs.takeLast(maxTabs).forEach { addTabState(it) }
+ pruneTabsWithMax(maxTabs)
+ } catch (e: Exception) {
+ crashReporting.submitCaughtException(RecentlyClosedTabsStorageException(e))
+ }
+ }
+
+ /**
+ * @return An [EngineSessionStateStorage] instance used to persist engine state of tabs.
+ */
+ fun engineStateStorage(): EngineSessionStateStorage {
+ return engineStateStorage
+ }
+
+ private suspend fun pruneTabsWithMax(maxTabs: Int) {
+ val tabs = database.value.recentlyClosedTabDao().getTabs().first()
+
+ // No pruning required
+ if (tabs.size <= maxTabs) return
+
+ tabs.subList(maxTabs, tabs.size).forEach { entity ->
+ engineStateStorage.delete(entity.uuid)
+ database.value.recentlyClosedTabDao().deleteTab(entity)
+ }
+ }
+
+ @VisibleForTesting
+ internal suspend fun addTabState(tab: RecoverableTab) {
+ val entity = tab.state.toRecentlyClosedTabEntity()
+ // Even if engine session state persistence fails, degrade gracefully by storing the tab
+ // itself in the db - that will allow user to restore it with a "fresh" engine state.
+ // That's a form of data loss, but not much we can do here other than log.
+ tab.engineSessionState?.let {
+ try {
+ if (!engineStateStorage.write(entity.uuid, it)) {
+ logger.warn("Failed to write engine session state for tab UUID = ${entity.uuid}")
+ }
+ } catch (e: OutOfMemoryError) {
+ crashReporting.submitCaughtException(e)
+ logger.error("Failed to save state to disk due to OutOfMemoryError", e)
+ }
+ }
+ database.value.recentlyClosedTabDao().insertTab(entity)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabDao.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabDao.kt
new file mode 100644
index 0000000000..aaecd6ab40
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabDao.kt
@@ -0,0 +1,43 @@
+/* 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.recentlyclosed.db
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Internal DAO for accessing [RecentlyClosedTabEntity] instances.
+ */
+@Dao
+internal interface RecentlyClosedTabDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertTab(tab: RecentlyClosedTabEntity): Long
+
+ @Delete
+ fun deleteTab(tab: RecentlyClosedTabEntity)
+
+ @Transaction
+ @Query(
+ """
+ SELECT *
+ FROM recently_closed_tabs
+ ORDER BY created_at DESC
+ """,
+ )
+ fun getTabs(): Flow<List<RecentlyClosedTabEntity>>
+
+ @Transaction
+ @Query(
+ """
+ DELETE FROM recently_closed_tabs
+ """,
+ )
+ fun removeAllTabs()
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabEntity.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabEntity.kt
new file mode 100644
index 0000000000..84f60de53f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabEntity.kt
@@ -0,0 +1,52 @@
+/* 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.recentlyclosed.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.browser.state.state.recover.TabState
+
+/**
+ * Internal entity representing recently closed tabs.
+ */
+@Entity(
+ tableName = "recently_closed_tabs",
+)
+internal data class RecentlyClosedTabEntity(
+ /**
+ * Generated UUID for this closed tab.
+ */
+ @PrimaryKey
+ @ColumnInfo(name = "uuid")
+ var uuid: String,
+
+ @ColumnInfo(name = "title")
+ var title: String,
+
+ @ColumnInfo(name = "url")
+ var url: String,
+
+ @ColumnInfo(name = "created_at")
+ var createdAt: Long,
+) {
+ internal fun asTabState(): TabState {
+ return TabState(
+ id = uuid,
+ title = title,
+ url = url,
+ lastAccess = createdAt,
+ )
+ }
+}
+
+internal fun TabState.toRecentlyClosedTabEntity(): RecentlyClosedTabEntity {
+ return RecentlyClosedTabEntity(
+ uuid = id,
+ title = title,
+ url = url,
+ createdAt = lastAccess,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabsDatabase.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabsDatabase.kt
new file mode 100644
index 0000000000..cd4e60e2e3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/main/java/mozilla/components/feature/recentlyclosed/db/RecentlyClosedTabsDatabase.kt
@@ -0,0 +1,36 @@
+/* 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.recentlyclosed.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+/**
+ * Internal database for storing recently closed tabs.
+ */
+@Database(entities = [RecentlyClosedTabEntity::class], version = 1)
+internal abstract class RecentlyClosedTabsDatabase : RoomDatabase() {
+ abstract fun recentlyClosedTabDao(): RecentlyClosedTabDao
+
+ companion object {
+ @Volatile
+ private var instance: RecentlyClosedTabsDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): RecentlyClosedTabsDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ RecentlyClosedTabsDatabase::class.java,
+ "recently_closed_tabs",
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddlewareTest.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddlewareTest.kt
new file mode 100644
index 0000000000..0efc5d1e35
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddlewareTest.kt
@@ -0,0 +1,383 @@
+/* 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.recentlyclosed
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flow
+import mozilla.components.browser.state.action.RecentlyClosedAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.UndoAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.session.middleware.undo.UndoMiddleware
+import mozilla.components.support.test.argumentCaptor
+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 mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@ExperimentalCoroutinesApi
+class RecentlyClosedMiddlewareTest {
+ lateinit var store: BrowserStore
+ lateinit var engine: Engine
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setup() {
+ store = mock()
+ engine = mock()
+ }
+
+ // Test tab
+ private val closedTab = RecoverableTab(
+ engineSessionState = null,
+ state = TabState(
+ id = "tab-id",
+ title = "Mozilla",
+ url = "https://mozilla.org",
+ lastAccess = 1234,
+ ),
+ )
+
+ @Test
+ fun `closed tab storage stores the provided tab on add tab action`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(RecentlyClosedAction.AddClosedTabsAction(listOf(closedTab))).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(storage).addTabsToCollectionWithMax(
+ listOf(closedTab),
+ 5,
+ )
+ }
+
+ @Test
+ fun `closed tab storage adds normal tabs removed with TabListAction`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+
+ val tab = createTab("https://www.mozilla.org", private = false, id = "1234")
+ val tab2 = createTab("https://www.firefox.com", private = false, id = "5678")
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab, tab2),
+ ),
+ middleware = listOf(UndoMiddleware(mainScope = scope), middleware),
+ )
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf("1234", "5678"))).joinBlocking()
+ store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ val closedTabCaptor = argumentCaptor<List<RecoverableTab>>()
+ verify(storage).addTabsToCollectionWithMax(
+ closedTabCaptor.capture(),
+ eq(5),
+ )
+ assertEquals(2, closedTabCaptor.value.size)
+ assertEquals(tab.content.title, closedTabCaptor.value[0].state.title)
+ assertEquals(tab.content.url, closedTabCaptor.value[0].state.url)
+ assertEquals(tab2.content.title, closedTabCaptor.value[1].state.title)
+ assertEquals(tab2.content.url, closedTabCaptor.value[1].state.url)
+ assertEquals(
+ tab.engineState.engineSessionState,
+ closedTabCaptor.value[0].engineSessionState,
+ )
+ assertEquals(
+ tab2.engineState.engineSessionState,
+ closedTabCaptor.value[1].engineSessionState,
+ )
+ }
+
+ @Test
+ fun `closed tab storage adds a normal tab removed with TabListAction`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+
+ val tab = createTab("https://www.mozilla.org", private = false, id = "1234")
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ middleware = listOf(UndoMiddleware(mainScope = scope), middleware),
+ )
+
+ store.dispatch(TabListAction.RemoveTabAction("1234")).joinBlocking()
+ store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ val closedTabCaptor = argumentCaptor<List<RecoverableTab>>()
+ verify(storage).addTabsToCollectionWithMax(
+ closedTabCaptor.capture(),
+ eq(5),
+ )
+ assertEquals(1, closedTabCaptor.value.size)
+ assertEquals(tab.content.title, closedTabCaptor.value[0].state.title)
+ assertEquals(tab.content.url, closedTabCaptor.value[0].state.url)
+ assertEquals(
+ tab.engineState.engineSessionState,
+ closedTabCaptor.value[0].engineSessionState,
+ )
+ }
+
+ @Test
+ fun `closed tab storage does not add a private tab removed with TabListAction`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+
+ val tab = createTab("https://www.mozilla.org", private = true, id = "1234")
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(TabListAction.RemoveTabAction("1234")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(storage).getTabs()
+ verifyNoMoreInteractions(storage)
+ }
+
+ @Test
+ fun `closed tab storage adds all normals tab removed with TabListAction RemoveAllNormalTabsAction`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+
+ val tab = createTab("https://www.mozilla.org", private = false, id = "1234")
+ val tab2 = createTab("https://www.firefox.com", private = true, id = "3456")
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab, tab2),
+ ),
+ middleware = listOf(UndoMiddleware(mainScope = scope), middleware),
+ )
+
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
+ store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ val closedTabCaptor = argumentCaptor<List<RecoverableTab>>()
+ verify(storage).addTabsToCollectionWithMax(
+ closedTabCaptor.capture(),
+ eq(5),
+ )
+ assertEquals(1, closedTabCaptor.value.size)
+ assertEquals(tab.content.title, closedTabCaptor.value[0].state.title)
+ assertEquals(tab.content.url, closedTabCaptor.value[0].state.url)
+ assertEquals(
+ tab.engineState.engineSessionState,
+ closedTabCaptor.value[0].engineSessionState,
+ )
+ }
+
+ @Test
+ fun `closed tab storage adds all normal tabs and no private tabs removed with TabListAction RemoveAllTabsAction`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+
+ val tab = createTab("https://www.mozilla.org", private = false, id = "1234")
+ val tab2 = createTab("https://www.firefox.com", private = true, id = "3456")
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab, tab2),
+ ),
+ middleware = listOf(UndoMiddleware(mainScope = scope), middleware),
+ )
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+ store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ val closedTabCaptor = argumentCaptor<List<RecoverableTab>>()
+ verify(storage).addTabsToCollectionWithMax(
+ closedTabCaptor.capture(),
+ eq(5),
+ )
+ assertEquals(1, closedTabCaptor.value.size)
+ assertEquals(tab.content.title, closedTabCaptor.value[0].state.title)
+ assertEquals(tab.content.url, closedTabCaptor.value[0].state.url)
+ assertEquals(
+ tab.engineState.engineSessionState,
+ closedTabCaptor.value[0].engineSessionState,
+ )
+ }
+
+ @Test
+ fun `closed tabs storage adds tabs closed one after the other without clear actions in between`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+
+ val store = BrowserStore(
+ middleware = listOf(UndoMiddleware(mainScope = scope), middleware),
+ )
+
+ store.dispatch(TabListAction.AddTabAction(createTab("https://www.mozilla.org", id = "tab1"))).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(createTab("https://www.firefox.com", id = "tab2"))).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(createTab("https://getpocket.com", id = "tab3"))).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(createTab("https://theverge.com", id = "tab4"))).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(createTab("https://www.google.com", id = "tab5"))).joinBlocking()
+ assertEquals(5, store.state.tabs.size)
+
+ store.dispatch(TabListAction.RemoveTabAction("tab2")).joinBlocking()
+ store.dispatch(TabListAction.RemoveTabAction("tab3")).joinBlocking()
+ store.dispatch(TabListAction.RemoveTabAction("tab1")).joinBlocking()
+ store.dispatch(TabListAction.RemoveTabAction("tab5")).joinBlocking()
+
+ store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("tab4", store.state.selectedTabId)
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ val closedTabCaptor = argumentCaptor<List<RecoverableTab>>()
+
+ verify(storage, times(4)).addTabsToCollectionWithMax(
+ closedTabCaptor.capture(),
+ eq(5),
+ )
+
+ val tabs = closedTabCaptor.allValues
+ assertEquals(4, tabs.size)
+
+ tabs[0].also { tab ->
+ assertEquals(1, tab.size)
+ assertEquals("tab2", tab[0].state.id)
+ }
+ tabs[1].also { tab ->
+ assertEquals(1, tab.size)
+ assertEquals("tab3", tab[0].state.id)
+ }
+ tabs[2].also { tab ->
+ assertEquals(1, tab.size)
+ assertEquals("tab1", tab[0].state.id)
+ }
+ tabs[3].also { tab ->
+ assertEquals(1, tab.size)
+ assertEquals("tab5", tab[0].state.id)
+ }
+ Unit
+ }
+
+ @Test
+ fun `fetch the tabs from the recently closed storage and load into browser state on initialize tab state action`() = runTestOnMain {
+ val storage = mockStorage(tabs = listOf(closedTab.state))
+
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+ val store = BrowserStore(initialState = BrowserState(), middleware = listOf(middleware))
+
+ // Wait for Init action of store to be processed
+ store.waitUntilIdle()
+
+ // Now wait for Middleware to process Init action and store to process action from middleware
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ verify(storage).getTabs()
+ assertEquals(closedTab.state, store.state.closedTabs[0])
+ }
+
+ @Test
+ fun `recently closed storage removes the provided tab on remove tab action`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+
+ val store = BrowserStore(
+ initialState = BrowserState(
+ closedTabs = listOf(
+ closedTab.state,
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(closedTab.state)).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+ verify(storage).removeTab(closedTab.state)
+ }
+
+ @Test
+ fun `recently closed storage removes all tabs on remove all tabs action`() = runTestOnMain {
+ val storage = mockStorage()
+ val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
+ val store = BrowserStore(
+ initialState = BrowserState(
+ closedTabs = listOf(
+ closedTab.state,
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+ verify(storage).removeAllTabs()
+ }
+}
+
+private suspend fun mockStorage(
+ tabs: List<TabState> = emptyList(),
+): RecentlyClosedMiddleware.Storage {
+ val storage: RecentlyClosedMiddleware.Storage = mock()
+
+ whenever(storage.getTabs()).thenReturn(
+ flow {
+ emit(tabs)
+ },
+ )
+
+ return storage
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabDaoTest.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabDaoTest.kt
new file mode 100644
index 0000000000..b650a81915
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabDaoTest.kt
@@ -0,0 +1,136 @@
+/* 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.recentlyclosed
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabDao
+import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabEntity
+import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.UUID
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class RecentlyClosedTabDaoTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: RecentlyClosedTabsDatabase
+ private lateinit var tabDao: RecentlyClosedTabDao
+
+ @Before
+ fun setUp() {
+ database = Room
+ .inMemoryDatabaseBuilder(context, RecentlyClosedTabsDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+ tabDao = database.recentlyClosedTabDao()
+ }
+
+ @Test
+ fun testAddingTabs() = runTestOnMain {
+ val tab1 = RecentlyClosedTabEntity(
+ title = "RecentlyClosedTab One",
+ url = "https://www.mozilla.org",
+ uuid = UUID.randomUUID().toString(),
+ createdAt = 200,
+ ).also {
+ tabDao.insertTab(it)
+ }
+
+ val tab2 = RecentlyClosedTabEntity(
+ title = "RecentlyClosedTab Two",
+ url = "https://www.firefox.com",
+ uuid = UUID.randomUUID().toString(),
+ createdAt = 100,
+ ).also {
+ tabDao.insertTab(it)
+ }
+
+ tabDao.getTabs().first().apply {
+ assertEquals(2, this.size)
+ assertEquals(tab1, this[0])
+ assertEquals(tab2, this[1])
+ }
+ Unit
+ }
+
+ @Test
+ fun testRemovingTab() = runTestOnMain {
+ val tab1 = RecentlyClosedTabEntity(
+ title = "RecentlyClosedTab One",
+ url = "https://www.mozilla.org",
+ uuid = UUID.randomUUID().toString(),
+ createdAt = 200,
+ ).also {
+ tabDao.insertTab(it)
+ }
+
+ val tab2 = RecentlyClosedTabEntity(
+ title = "RecentlyClosedTab Two",
+ url = "https://www.firefox.com",
+ uuid = UUID.randomUUID().toString(),
+ createdAt = 100,
+ ).also {
+ tabDao.insertTab(it)
+ }
+
+ tabDao.deleteTab(tab1)
+
+ tabDao.getTabs().first().apply {
+ assertEquals(1, this.size)
+ assertEquals(tab2, this[0])
+ }
+ Unit
+ }
+
+ @Test
+ fun testRemovingAllTabs() = runTestOnMain {
+ RecentlyClosedTabEntity(
+ title = "RecentlyClosedTab One",
+ url = "https://www.mozilla.org",
+ uuid = UUID.randomUUID().toString(),
+ createdAt = 200,
+ ).also {
+ tabDao.insertTab(it)
+ }
+
+ RecentlyClosedTabEntity(
+ title = "RecentlyClosedTab Two",
+ url = "https://www.firefox.com",
+ uuid = UUID.randomUUID().toString(),
+ createdAt = 100,
+ ).also {
+ tabDao.insertTab(it)
+ }
+
+ tabDao.removeAllTabs()
+
+ tabDao.getTabs().first().apply {
+ assertEquals(0, this.size)
+ }
+ Unit
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageTest.kt b/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageTest.kt
new file mode 100644
index 0000000000..ca309552f7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageTest.kt
@@ -0,0 +1,355 @@
+/* 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.recentlyclosed
+
+import androidx.room.Room
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.EngineSessionStateStorage
+import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import java.io.IOException
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class RecentlyClosedTabsStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var storage: RecentlyClosedTabsStorage
+ private lateinit var engineStateStorage: TestEngineSessionStateStorage
+ private lateinit var database: RecentlyClosedTabsDatabase
+ private lateinit var crashReporting: CrashReporting
+
+ private class TestEngineSessionStateStorage() : EngineSessionStateStorage {
+ val data: MutableMap<String, EngineSessionState?> = mutableMapOf()
+ var throwsOutOfMemoryOnWrite: Boolean = false
+
+ override suspend fun write(uuid: String, state: EngineSessionState): Boolean {
+ if (throwsOutOfMemoryOnWrite) {
+ throw OutOfMemoryError()
+ }
+
+ if (uuid.contains("fail")) {
+ return false
+ }
+ if (uuid.contains("boom")) {
+ throw IllegalStateException("boom!")
+ }
+ data[uuid] = state
+ return true
+ }
+
+ override suspend fun read(uuid: String): EngineSessionState? {
+ return data[uuid]
+ }
+
+ override suspend fun delete(uuid: String) {
+ data.remove(uuid)
+ }
+
+ override suspend fun deleteAll() {
+ data.clear()
+ }
+ }
+
+ @Before
+ fun setUp() {
+ crashReporting = mock()
+ database = Room
+ .inMemoryDatabaseBuilder(testContext, RecentlyClosedTabsDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+
+ engineStateStorage = TestEngineSessionStateStorage()
+ storage = RecentlyClosedTabsStorage(
+ testContext,
+ mock(),
+ crashReporting,
+ engineStateStorage = engineStateStorage,
+ )
+ storage.database = lazy { database }
+ }
+
+ @After
+ @Throws(IOException::class)
+ fun closeDb() {
+ database.close()
+ }
+
+ @Test
+ fun testAddingTabsWithMax() = runTestOnMain {
+ // Test tab
+ val t1 = System.currentTimeMillis()
+ val closedTab = RecoverableTab(
+ engineSessionState = null,
+ state = TabState(
+ id = "first-tab",
+ title = "Mozilla",
+ url = "https://mozilla.org",
+ lastAccess = t1,
+ ),
+ )
+
+ // Test tab
+ val engineState2: EngineSessionState = mock()
+ val secondClosedTab = RecoverableTab(
+ engineSessionState = engineState2,
+ TabState(
+ id = "second-tab",
+ title = "Pocket",
+ url = "https://pocket.com",
+ lastAccess = t1 - 1000,
+ ),
+ )
+
+ storage.addTabsToCollectionWithMax(listOf(closedTab, secondClosedTab), 1)
+ val tabs = storage.getTabs().first()
+
+ assertEquals(1, engineStateStorage.data.size)
+ assertEquals(engineState2, engineStateStorage.data["second-tab"])
+
+ assertEquals(1, tabs.size)
+ assertEquals(secondClosedTab.state.url, tabs[0].url)
+ assertEquals(secondClosedTab.state.title, tabs[0].title)
+ assertEquals(secondClosedTab.state.lastAccess, tabs[0].lastAccess)
+
+ // Test tab
+ val engineState3: EngineSessionState = mock()
+ val thirdClosedTab = RecoverableTab(
+ engineSessionState = engineState3,
+ state = TabState(
+ id = "third-tab",
+ title = "Firefox",
+ url = "https://firefox.com",
+ lastAccess = System.currentTimeMillis(),
+ ),
+ )
+
+ storage.addTabsToCollectionWithMax(listOf(thirdClosedTab), 1)
+ val newTabs = storage.getTabs().first()
+
+ assertEquals(1, engineStateStorage.data.size)
+ assertEquals(engineState3, engineStateStorage.data["third-tab"])
+
+ assertEquals(1, newTabs.size)
+ assertEquals(thirdClosedTab.state.url, newTabs[0].url)
+ assertEquals(thirdClosedTab.state.title, newTabs[0].title)
+ assertEquals(thirdClosedTab.state.lastAccess, newTabs[0].lastAccess)
+ }
+
+ @Test
+ fun testAllowAddingSameTabTwice() = runTestOnMain {
+ // Test tab
+ val engineState: EngineSessionState = mock()
+ val closedTab = RecoverableTab(
+ engineSessionState = engineState,
+ state = TabState(
+ id = "first-tab",
+ title = "Mozilla",
+ url = "https://mozilla.org",
+ lastAccess = System.currentTimeMillis(),
+ ),
+ )
+
+ val updatedTab = closedTab.copy(state = closedTab.state.copy(title = "updated"))
+ storage.addTabsToCollectionWithMax(listOf(closedTab), 2)
+ storage.addTabsToCollectionWithMax(listOf(updatedTab), 2)
+ val tabs = storage.getTabs().first()
+
+ assertEquals(1, engineStateStorage.data.size)
+ assertEquals(engineState, engineStateStorage.data["first-tab"])
+
+ assertEquals(1, tabs.size)
+ assertEquals(updatedTab.state.url, tabs[0].url)
+ assertEquals(updatedTab.state.title, tabs[0].title)
+ assertEquals(updatedTab.state.lastAccess, tabs[0].lastAccess)
+ }
+
+ @Test
+ fun testRemovingAllTabs() = runTestOnMain {
+ // Test tab
+ val t1 = System.currentTimeMillis()
+ val closedTab = RecoverableTab(
+ engineSessionState = mock(),
+ state = TabState(
+ id = "first-tab",
+ title = "Mozilla",
+ url = "https://mozilla.org",
+ lastAccess = t1,
+ ),
+ )
+
+ // Test tab
+ val secondClosedTab = RecoverableTab(
+ engineSessionState = mock(),
+ state = TabState(
+ id = "second-tab",
+ title = "Pocket",
+ url = "https://pocket.com",
+ lastAccess = t1 - 1000,
+ ),
+ )
+
+ storage.addTabsToCollectionWithMax(listOf(closedTab, secondClosedTab), 2)
+ val tabs = storage.getTabs().first()
+
+ assertEquals(2, engineStateStorage.data.size)
+ assertEquals(2, tabs.size)
+ assertEquals(closedTab.state.url, tabs[0].url)
+ assertEquals(closedTab.state.title, tabs[0].title)
+ assertEquals(closedTab.state.lastAccess, tabs[0].lastAccess)
+ assertEquals(secondClosedTab.state.url, tabs[1].url)
+ assertEquals(secondClosedTab.state.title, tabs[1].title)
+ assertEquals(secondClosedTab.state.lastAccess, tabs[1].lastAccess)
+
+ storage.removeAllTabs()
+ val newTabs = storage.getTabs().first()
+
+ assertEquals(0, engineStateStorage.data.size)
+ assertEquals(0, newTabs.size)
+ }
+
+ @Test
+ fun testRemovingOneTab() = runTestOnMain {
+ // Test tab
+ val engineState1: EngineSessionState = mock()
+ val t1 = System.currentTimeMillis()
+ val closedTab = RecoverableTab(
+ engineSessionState = engineState1,
+ state = TabState(
+ id = "first-tab",
+ title = "Mozilla",
+ url = "https://mozilla.org",
+ lastAccess = t1,
+ ),
+ )
+
+ // Test tab
+ val engineState2: EngineSessionState = mock()
+ val secondClosedTab = RecoverableTab(
+ engineSessionState = engineState2,
+ state = TabState(
+ id = "second-tab",
+ title = "Pocket",
+ url = "https://pocket.com",
+ lastAccess = t1 - 1000,
+ ),
+ )
+
+ storage.addTabState(closedTab)
+ storage.addTabState(secondClosedTab)
+ val tabs = storage.getTabs().first()
+
+ assertEquals(2, engineStateStorage.data.size)
+ assertEquals(2, tabs.size)
+ assertEquals(closedTab.state.url, tabs[0].url)
+ assertEquals(closedTab.state.title, tabs[0].title)
+ assertEquals(closedTab.state.lastAccess, tabs[0].lastAccess)
+ assertEquals(secondClosedTab.state.url, tabs[1].url)
+ assertEquals(secondClosedTab.state.title, tabs[1].title)
+ assertEquals(secondClosedTab.state.lastAccess, tabs[1].lastAccess)
+
+ storage.removeTab(tabs[0])
+ val newTabs = storage.getTabs().first()
+
+ assertEquals(1, engineStateStorage.data.size)
+ assertEquals(engineState2, engineStateStorage.data["second-tab"])
+ assertEquals(1, newTabs.size)
+ assertEquals(secondClosedTab.state.url, newTabs[0].url)
+ assertEquals(secondClosedTab.state.title, newTabs[0].title)
+ assertEquals(secondClosedTab.state.lastAccess, newTabs[0].lastAccess)
+ }
+
+ @Test
+ fun testAddingTabWithEngineStateStorageFailure() = runTestOnMain {
+ // 'fail' in tab's id will cause test engine session storage to fail on writing engineSessionState.
+ val closedTab = RecoverableTab(
+ engineSessionState = mock(),
+ state = TabState(
+ id = "second-tab-fail",
+ title = "Pocket",
+ url = "https://pocket.com",
+ lastAccess = System.currentTimeMillis(),
+ ),
+ )
+
+ storage.addTabState(closedTab)
+ val tabs = storage.getTabs().first()
+ // if it's empty, we know state write failed
+ assertEquals(0, engineStateStorage.data.size)
+ // but the tab was still written into the database.
+ assertEquals(1, tabs.size)
+ }
+
+ @Test
+ fun testAddingTabWithEngineStateStorageCausingOOM() = runTestOnMain {
+ // OutOfMemoryError on EngineSessionStateStorage::write will cause test engine session
+ // storage to fail on writing engineSessionState.
+ engineStateStorage.throwsOutOfMemoryOnWrite = true
+
+ // Test tab
+ val engineState1: EngineSessionState = mock()
+ val t1 = System.currentTimeMillis()
+ val closedTab = RecoverableTab(
+ engineSessionState = engineState1,
+ state = TabState(
+ id = "first-tab",
+ title = "Mozilla",
+ url = "https://mozilla.org",
+ lastAccess = t1,
+ ),
+ )
+
+ storage.addTabState(closedTab)
+ val tabs = storage.getTabs().first()
+ // if it's empty, we know state write failed
+ assertEquals(0, engineStateStorage.data.size)
+ // but the tab was still written into the database.
+ assertEquals(1, tabs.size)
+ }
+
+ @Test
+ fun testEngineSessionStorageObtainable() {
+ assertEquals(engineStateStorage, storage.engineStateStorage())
+ }
+
+ @Test
+ fun testStorageFailuresAreCaught() = runTestOnMain {
+ val engineState: EngineSessionState = mock()
+ val closedTab = RecoverableTab(
+ engineSessionState = engineState,
+ state = TabState(
+ id = "second-tab-boom", // boom will cause an exception to be thrown
+ title = "Pocket",
+ url = "https://pocket.com",
+ lastAccess = System.currentTimeMillis(),
+ ),
+ )
+ try {
+ storage.addTabsToCollectionWithMax(listOf(closedTab), 2)
+ verify(crashReporting).submitCaughtException(any())
+ } catch (e: Exception) {
+ fail("Thrown exception was not caught")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/recentlyclosed/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/recentlyclosed/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/recentlyclosed/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/recentlyclosed/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/recentlyclosed/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/recentlyclosed/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/search/.gitignore b/mobile/android/android-components/components/feature/search/.gitignore
new file mode 100644
index 0000000000..2ddf5f27b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/.gitignore
@@ -0,0 +1 @@
+manifest.json
diff --git a/mobile/android/android-components/components/feature/search/README.md b/mobile/android/android-components/components/feature/search/README.md
new file mode 100644
index 0000000000..2cc4bc7374
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Search
+
+A component that connects an (concept) engine implementation with the browser search module.
+
+## Usage
+
+### 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-search:{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/search/build.gradle b/mobile/android/android-components/components/feature/search/build.gradle
new file mode 100644
index 0000000000..2444b45437
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/build.gradle
@@ -0,0 +1,80 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ sourceSets {
+ test {
+ resources {
+ // We want to access the assets from unit tests. With this configuration we can just
+ // read the files directly and do not need to rely on Robolectric.
+ srcDir "${projectDir}/src/main/assets/"
+ }
+ }
+ }
+
+ namespace 'mozilla.components.feature.search'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += [
+ "-opt-in=kotlinx.coroutines.FlowPreview",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+ ]
+}
+
+dependencies {
+ implementation project(":feature-tabs")
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':service-location')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+ implementation project(':ui-colors')
+ implementation project(':support-base')
+ implementation project(':support-remotesettings')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.mozilla_remote_settings
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-fakes')
+ testImplementation project(':support-test-libstate')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+tasks.register("updateAdsExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/ads')
+}
+
+tasks.register("updateCookiesExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/search')
+}
+
+preBuild.dependsOn "updateAdsExtensionVersion"
+preBuild.dependsOn "updateCookiesExtensionVersion"
+
+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/search/proguard-rules.pro b/mobile/android/android-components/components/feature/search/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/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/search/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/search/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/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/search/src/main/assets/extensions/ads/adsTelemetry.js b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/adsTelemetry.js
new file mode 100644
index 0000000000..65bf306835
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/adsTelemetry.js
@@ -0,0 +1,82 @@
+/* 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/. */
+
+/**
+ * Send
+ * - current URL
+ * - cookies of this page
+ * - all links found in this page
+ * to the native application.
+ */
+function sendCurrentState() {
+ let message = {
+ 'url': document.location.href,
+ 'urls': getLinks(),
+ 'cookies': getCookies()
+ };
+ browser.runtime.sendNativeMessage("MozacBrowserAdsMessage", message);
+}
+
+/**
+ * Get all links in the current page.
+ *
+ * @return {Array<string>} containing all current links in the current page.
+ */
+function getLinks() {
+ let urls = [];
+
+ let anchors = document.getElementsByTagName("a");
+ for (let anchor of anchors) {
+ if (!anchor.href) {
+ continue;
+ }
+ urls.push(anchor.href);
+ }
+
+ return urls;
+}
+
+/**
+ * Get all cookies for the current document.
+ *
+ * @return {Array<{name: string, value: string}>} containing all cookies.
+ */
+function getCookies() {
+ let cookiesList = document.cookie.split("; ");
+ let result = [];
+
+ cookiesList.forEach(cookie => {
+ var [name, ...value] = cookie.split('=');
+ // For that special cases where the value contains '='.
+ value = value.join("=")
+
+ result.push({
+ "name" : name,
+ "value" : value
+ });
+ });
+
+ return result;
+}
+
+// Whenever a page is first accessed or when loaded from cache
+// send all needed data about the ads provider to the app.
+const events = ["pageshow", "load"];
+const eventLogger = event => {
+ switch (event.type) {
+ case "load":
+ sendCurrentState();
+ break;
+ case "pageshow":
+ if (event.persisted) {
+ sendCurrentState();
+ }
+ break;
+ default:
+ console.log('Event:', event.type);
+ }
+};
+events.forEach(eventName =>
+ window.addEventListener(eventName, eventLogger)
+);
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/manifest.template.json b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/manifest.template.json
new file mode 100644
index 0000000000..219e41b554
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/manifest.template.json
@@ -0,0 +1,219 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "ads@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - Ads Telemetry",
+ "version": "${version}",
+ "content_scripts": [
+ {
+ "matches": ["https://*/*"],
+ "include_globs": [
+ "https://www.google.com/search*",
+ "https://www.google.ad/search*",
+ "https://www.google.ae/search*",
+ "https://www.google.com.af/search*",
+ "https://www.google.com.ag/search*",
+ "https://www.google.com.ai/search*",
+ "https://www.google.al/search*",
+ "https://www.google.am/search*",
+ "https://www.google.co.ao/search*",
+ "https://www.google.com.ar/search*",
+ "https://www.google.as/search*",
+ "https://www.google.at/search*",
+ "https://www.google.com.au/search*",
+ "https://www.google.az/search*",
+ "https://www.google.ba/search*",
+ "https://www.google.com.bd/search*",
+ "https://www.google.be/search*",
+ "https://www.google.bf/search*",
+ "https://www.google.bg/search*",
+ "https://www.google.com.bh/search*",
+ "https://www.google.bi/search*",
+ "https://www.google.bj/search*",
+ "https://www.google.com.bn/search*",
+ "https://www.google.com.bo/search*",
+ "https://www.google.com.br/search*",
+ "https://www.google.bs/search*",
+ "https://www.google.bt/search*",
+ "https://www.google.co.bw/search*",
+ "https://www.google.by/search*",
+ "https://www.google.com.bz/search*",
+ "https://www.google.ca/search*",
+ "https://www.google.cd/search*",
+ "https://www.google.cf/search*",
+ "https://www.google.cg/search*",
+ "https://www.google.ch/search*",
+ "https://www.google.ci/search*",
+ "https://www.google.co.ck/search*",
+ "https://www.google.cl/search*",
+ "https://www.google.cm/search*",
+ "https://www.google.cn/search*",
+ "https://www.google.com.co/search*",
+ "https://www.google.co.cr/search*",
+ "https://www.google.com.cu/search*",
+ "https://www.google.cv/search*",
+ "https://www.google.com.cy/search*",
+ "https://www.google.cz/search*",
+ "https://www.google.de/search*",
+ "https://www.google.dj/search*",
+ "https://www.google.dk/search*",
+ "https://www.google.dm/search*",
+ "https://www.google.com.do/search*",
+ "https://www.google.dz/search*",
+ "https://www.google.com.ec/search*",
+ "https://www.google.ee/search*",
+ "https://www.google.com.eg/search*",
+ "https://www.google.es/search*",
+ "https://www.google.com.et/search*",
+ "https://www.google.fi/search*",
+ "https://www.google.com.fj/search*",
+ "https://www.google.fm/search*",
+ "https://www.google.fr/search*",
+ "https://www.google.ga/search*",
+ "https://www.google.ge/search*",
+ "https://www.google.gg/search*",
+ "https://www.google.com.gh/search*",
+ "https://www.google.com.gi/search*",
+ "https://www.google.gl/search*",
+ "https://www.google.gm/search*",
+ "https://www.google.gr/search*",
+ "https://www.google.com.gt/search*",
+ "https://www.google.gy/search*",
+ "https://www.google.com.hk/search*",
+ "https://www.google.hn/search*",
+ "https://www.google.hr/search*",
+ "https://www.google.ht/search*",
+ "https://www.google.hu/search*",
+ "https://www.google.co.id/search*",
+ "https://www.google.ie/search*",
+ "https://www.google.co.il/search*",
+ "https://www.google.im/search*",
+ "https://www.google.co.in/search*",
+ "https://www.google.iq/search*",
+ "https://www.google.is/search*",
+ "https://www.google.it/search*",
+ "https://www.google.je/search*",
+ "https://www.google.com.jm/search*",
+ "https://www.google.jo/search*",
+ "https://www.google.co.jp/search*",
+ "https://www.google.co.ke/search*",
+ "https://www.google.com.kh/search*",
+ "https://www.google.ki/search*",
+ "https://www.google.kg/search*",
+ "https://www.google.co.kr/search*",
+ "https://www.google.com.kw/search*",
+ "https://www.google.kz/search*",
+ "https://www.google.la/search*",
+ "https://www.google.com.lb/search*",
+ "https://www.google.li/search*",
+ "https://www.google.lk/search*",
+ "https://www.google.co.ls/search*",
+ "https://www.google.lt/search*",
+ "https://www.google.lu/search*",
+ "https://www.google.lv/search*",
+ "https://www.google.com.ly/search*",
+ "https://www.google.co.ma/search*",
+ "https://www.google.md/search*",
+ "https://www.google.me/search*",
+ "https://www.google.mg/search*",
+ "https://www.google.mk/search*",
+ "https://www.google.ml/search*",
+ "https://www.google.com.mm/search*",
+ "https://www.google.mn/search*",
+ "https://www.google.ms/search*",
+ "https://www.google.com.mt/search*",
+ "https://www.google.mu/search*",
+ "https://www.google.mv/search*",
+ "https://www.google.mw/search*",
+ "https://www.google.com.mx/search*",
+ "https://www.google.com.my/search*",
+ "https://www.google.co.mz/search*",
+ "https://www.google.com.na/search*",
+ "https://www.google.com.ng/search*",
+ "https://www.google.com.ni/search*",
+ "https://www.google.ne/search*",
+ "https://www.google.nl/search*",
+ "https://www.google.no/search*",
+ "https://www.google.com.np/search*",
+ "https://www.google.nr/search*",
+ "https://www.google.nu/search*",
+ "https://www.google.co.nz/search*",
+ "https://www.google.com.om/search*",
+ "https://www.google.com.pa/search*",
+ "https://www.google.com.pe/search*",
+ "https://www.google.com.pg/search*",
+ "https://www.google.com.ph/search*",
+ "https://www.google.com.pk/search*",
+ "https://www.google.pl/search*",
+ "https://www.google.pn/search*",
+ "https://www.google.com.pr/search*",
+ "https://www.google.ps/search*",
+ "https://www.google.pt/search*",
+ "https://www.google.com.py/search*",
+ "https://www.google.com.qa/search*",
+ "https://www.google.ro/search*",
+ "https://www.google.ru/search*",
+ "https://www.google.rw/search*",
+ "https://www.google.com.sa/search*",
+ "https://www.google.com.sb/search*",
+ "https://www.google.sc/search*",
+ "https://www.google.se/search*",
+ "https://www.google.com.sg/search*",
+ "https://www.google.sh/search*",
+ "https://www.google.si/search*",
+ "https://www.google.sk/search*",
+ "https://www.google.com.sl/search*",
+ "https://www.google.sn/search*",
+ "https://www.google.so/search*",
+ "https://www.google.sm/search*",
+ "https://www.google.sr/search*",
+ "https://www.google.st/search*",
+ "https://www.google.com.sv/search*",
+ "https://www.google.td/search*",
+ "https://www.google.tg/search*",
+ "https://www.google.co.th/search*",
+ "https://www.google.com.tj/search*",
+ "https://www.google.tl/search*",
+ "https://www.google.tm/search*",
+ "https://www.google.tn/search*",
+ "https://www.google.to/search*",
+ "https://www.google.com.tr/search*",
+ "https://www.google.tt/search*",
+ "https://www.google.com.tw/search*",
+ "https://www.google.co.tz/search*",
+ "https://www.google.com.ua/search*",
+ "https://www.google.co.ug/search*",
+ "https://www.google.co.uk/search*",
+ "https://www.google.com.uy/search*",
+ "https://www.google.co.uz/search*",
+ "https://www.google.com.vc/search*",
+ "https://www.google.co.ve/search*",
+ "https://www.google.vg/search*",
+ "https://www.google.co.vi/search*",
+ "https://www.google.com.vn/search*",
+ "https://www.google.vu/search*",
+ "https://www.google.ws/search*",
+ "https://www.google.rs/search*",
+ "https://www.google.co.za/search*",
+ "https://www.google.co.zm/search*",
+ "https://www.google.co.zw/search*",
+ "https://www.google.cat/search*",
+ "https://www.bing.com/search*",
+ "https://www.baidu.com/*",
+ "https://m.baidu.com/*",
+ "https://duckduckgo.com/*",
+ "https://www.ecosia.org/*"
+ ],
+ "js": ["adsTelemetry.js"],
+ "run_at": "document_end"
+ }
+ ],
+ "permissions": [
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent"
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/manifest.template.json b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/manifest.template.json
new file mode 100644
index 0000000000..ae9cf13dac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/manifest.template.json
@@ -0,0 +1,220 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "cookies@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - Search Telemetry",
+ "version": "${version}",
+ "content_scripts": [
+ {
+ "matches": ["https://*/*"],
+ "include_globs": [
+ "https://www.google.com/search*",
+ "https://www.google.ad/search*",
+ "https://www.google.ae/search*",
+ "https://www.google.com.af/search*",
+ "https://www.google.com.ag/search*",
+ "https://www.google.com.ai/search*",
+ "https://www.google.al/search*",
+ "https://www.google.am/search*",
+ "https://www.google.co.ao/search*",
+ "https://www.google.com.ar/search*",
+ "https://www.google.as/search*",
+ "https://www.google.at/search*",
+ "https://www.google.com.au/search*",
+ "https://www.google.az/search*",
+ "https://www.google.ba/search*",
+ "https://www.google.com.bd/search*",
+ "https://www.google.be/search*",
+ "https://www.google.bf/search*",
+ "https://www.google.bg/search*",
+ "https://www.google.com.bh/search*",
+ "https://www.google.bi/search*",
+ "https://www.google.bj/search*",
+ "https://www.google.com.bn/search*",
+ "https://www.google.com.bo/search*",
+ "https://www.google.com.br/search*",
+ "https://www.google.bs/search*",
+ "https://www.google.bt/search*",
+ "https://www.google.co.bw/search*",
+ "https://www.google.by/search*",
+ "https://www.google.com.bz/search*",
+ "https://www.google.ca/search*",
+ "https://www.google.cd/search*",
+ "https://www.google.cf/search*",
+ "https://www.google.cg/search*",
+ "https://www.google.ch/search*",
+ "https://www.google.ci/search*",
+ "https://www.google.co.ck/search*",
+ "https://www.google.cl/search*",
+ "https://www.google.cm/search*",
+ "https://www.google.cn/search*",
+ "https://www.google.com.co/search*",
+ "https://www.google.co.cr/search*",
+ "https://www.google.com.cu/search*",
+ "https://www.google.cv/search*",
+ "https://www.google.com.cy/search*",
+ "https://www.google.cz/search*",
+ "https://www.google.de/search*",
+ "https://www.google.dj/search*",
+ "https://www.google.dk/search*",
+ "https://www.google.dm/search*",
+ "https://www.google.com.do/search*",
+ "https://www.google.dz/search*",
+ "https://www.google.com.ec/search*",
+ "https://www.google.ee/search*",
+ "https://www.google.com.eg/search*",
+ "https://www.google.es/search*",
+ "https://www.google.com.et/search*",
+ "https://www.google.fi/search*",
+ "https://www.google.com.fj/search*",
+ "https://www.google.fm/search*",
+ "https://www.google.fr/search*",
+ "https://www.google.ga/search*",
+ "https://www.google.ge/search*",
+ "https://www.google.gg/search*",
+ "https://www.google.com.gh/search*",
+ "https://www.google.com.gi/search*",
+ "https://www.google.gl/search*",
+ "https://www.google.gm/search*",
+ "https://www.google.gr/search*",
+ "https://www.google.com.gt/search*",
+ "https://www.google.gy/search*",
+ "https://www.google.com.hk/search*",
+ "https://www.google.hn/search*",
+ "https://www.google.hr/search*",
+ "https://www.google.ht/search*",
+ "https://www.google.hu/search*",
+ "https://www.google.co.id/search*",
+ "https://www.google.ie/search*",
+ "https://www.google.co.il/search*",
+ "https://www.google.im/search*",
+ "https://www.google.co.in/search*",
+ "https://www.google.iq/search*",
+ "https://www.google.is/search*",
+ "https://www.google.it/search*",
+ "https://www.google.je/search*",
+ "https://www.google.com.jm/search*",
+ "https://www.google.jo/search*",
+ "https://www.google.co.jp/search*",
+ "https://www.google.co.ke/search*",
+ "https://www.google.com.kh/search*",
+ "https://www.google.ki/search*",
+ "https://www.google.kg/search*",
+ "https://www.google.co.kr/search*",
+ "https://www.google.com.kw/search*",
+ "https://www.google.kz/search*",
+ "https://www.google.la/search*",
+ "https://www.google.com.lb/search*",
+ "https://www.google.li/search*",
+ "https://www.google.lk/search*",
+ "https://www.google.co.ls/search*",
+ "https://www.google.lt/search*",
+ "https://www.google.lu/search*",
+ "https://www.google.lv/search*",
+ "https://www.google.com.ly/search*",
+ "https://www.google.co.ma/search*",
+ "https://www.google.md/search*",
+ "https://www.google.me/search*",
+ "https://www.google.mg/search*",
+ "https://www.google.mk/search*",
+ "https://www.google.ml/search*",
+ "https://www.google.com.mm/search*",
+ "https://www.google.mn/search*",
+ "https://www.google.ms/search*",
+ "https://www.google.com.mt/search*",
+ "https://www.google.mu/search*",
+ "https://www.google.mv/search*",
+ "https://www.google.mw/search*",
+ "https://www.google.com.mx/search*",
+ "https://www.google.com.my/search*",
+ "https://www.google.co.mz/search*",
+ "https://www.google.com.na/search*",
+ "https://www.google.com.ng/search*",
+ "https://www.google.com.ni/search*",
+ "https://www.google.ne/search*",
+ "https://www.google.nl/search*",
+ "https://www.google.no/search*",
+ "https://www.google.com.np/search*",
+ "https://www.google.nr/search*",
+ "https://www.google.nu/search*",
+ "https://www.google.co.nz/search*",
+ "https://www.google.com.om/search*",
+ "https://www.google.com.pa/search*",
+ "https://www.google.com.pe/search*",
+ "https://www.google.com.pg/search*",
+ "https://www.google.com.ph/search*",
+ "https://www.google.com.pk/search*",
+ "https://www.google.pl/search*",
+ "https://www.google.pn/search*",
+ "https://www.google.com.pr/search*",
+ "https://www.google.ps/search*",
+ "https://www.google.pt/search*",
+ "https://www.google.com.py/search*",
+ "https://www.google.com.qa/search*",
+ "https://www.google.ro/search*",
+ "https://www.google.ru/search*",
+ "https://www.google.rw/search*",
+ "https://www.google.com.sa/search*",
+ "https://www.google.com.sb/search*",
+ "https://www.google.sc/search*",
+ "https://www.google.se/search*",
+ "https://www.google.com.sg/search*",
+ "https://www.google.sh/search*",
+ "https://www.google.si/search*",
+ "https://www.google.sk/search*",
+ "https://www.google.com.sl/search*",
+ "https://www.google.sn/search*",
+ "https://www.google.so/search*",
+ "https://www.google.sm/search*",
+ "https://www.google.sr/search*",
+ "https://www.google.st/search*",
+ "https://www.google.com.sv/search*",
+ "https://www.google.td/search*",
+ "https://www.google.tg/search*",
+ "https://www.google.co.th/search*",
+ "https://www.google.com.tj/search*",
+ "https://www.google.tl/search*",
+ "https://www.google.tm/search*",
+ "https://www.google.tn/search*",
+ "https://www.google.to/search*",
+ "https://www.google.com.tr/search*",
+ "https://www.google.tt/search*",
+ "https://www.google.com.tw/search*",
+ "https://www.google.co.tz/search*",
+ "https://www.google.com.ua/search*",
+ "https://www.google.co.ug/search*",
+ "https://www.google.co.uk/search*",
+ "https://www.google.com.uy/search*",
+ "https://www.google.co.uz/search*",
+ "https://www.google.com.vc/search*",
+ "https://www.google.co.ve/search*",
+ "https://www.google.vg/search*",
+ "https://www.google.co.vi/search*",
+ "https://www.google.com.vn/search*",
+ "https://www.google.vu/search*",
+ "https://www.google.ws/search*",
+ "https://www.google.rs/search*",
+ "https://www.google.co.za/search*",
+ "https://www.google.co.zm/search*",
+ "https://www.google.co.zw/search*",
+ "https://www.google.cat/search*",
+ "https://www.baidu.com/*",
+ "https://m.baidu.com/*",
+ "https://*search.yahoo.com/search*",
+ "https://www.bing.com/search*",
+ "https://duckduckgo.com/*",
+ "https://www.ecosia.org/*"
+ ],
+ "js": ["searchTelemetry.js"],
+ "run_at": "document_end"
+ }
+ ],
+ "permissions": [
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent"
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/searchTelemetry.js b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/searchTelemetry.js
new file mode 100644
index 0000000000..3199335fdf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/searchTelemetry.js
@@ -0,0 +1,61 @@
+/* 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/. */
+
+/**
+ * Send
+ * - current URL
+ * - cookies of this page
+ * to the native application.
+ */
+function sendCurrentState() {
+ let message = {
+ 'url': document.location.href,
+ 'cookies': getCookies()
+ };
+ browser.runtime.sendNativeMessage("MozacBrowserSearchMessage", message);
+}
+
+/**
+ * Get all cookies for the current document.
+ *
+ * @return {Array<{name: string, value: string}>} containing all cookies.
+ */
+function getCookies() {
+ let cookiesList = document.cookie.split("; ");
+ let result = [];
+
+ cookiesList.forEach(cookie => {
+ var [name, ...value] = cookie.split('=');
+ // For that special cases where the cookie value contains '='.
+ value = value.join("=");
+
+ result.push({
+ "name" : name,
+ "value" : value
+ });
+ });
+
+ return result;
+}
+
+// Whenever a page is first accessed or when loaded from cache
+// send all needed data about the search provider to the app.
+const events = ["pageshow", "load"];
+const eventLogger = event => {
+ switch (event.type) {
+ case "load":
+ sendCurrentState();
+ break;
+ case "pageshow":
+ if (event.persisted) {
+ sendCurrentState();
+ }
+ break;
+ default:
+ console.log('Event:', event.type);
+ }
+};
+events.forEach(eventName =>
+ window.addEventListener(eventName, eventLogger)
+);
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/search/list.json b/mobile/android/android-components/components/feature/search/src/main/assets/search/list.json
new file mode 100644
index 0000000000..b2bb3d4698
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/search/list.json
@@ -0,0 +1,923 @@
+{
+ "default": {
+ "searchDefault": "Google",
+ "searchOrder": ["Google", "Bing"],
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia"
+ ]
+ },
+ "regionOverrides": {
+ "US": {
+ "google-b-m": "google-b-1-m"
+ }
+ },
+ "locales": {
+ "ach": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia"
+ ]
+ }
+ },
+ "an": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-es","wikipedia-an"
+ ]
+ }
+ },
+ "ar": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-ar"
+ ]
+ }
+ },
+ "as": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-as"
+ ]
+ }
+ },
+ "ast": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "ebay-es", "wikipedia-ast"
+ ]
+ }
+ },
+ "az": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "azerdict", "wikipedia-az"
+ ]
+ }
+ },
+ "be": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-be"
+ ]
+ },
+ "BY": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "ddg", "wikipedia-be"
+ ]
+ },
+ "KZ": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "ddg", "wikipedia-be"
+ ]
+ },
+ "RU": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "ddg", "wikipedia-be"
+ ]
+ },
+ "TR": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "ddg", "wikipedia-be"
+ ]
+ }
+ },
+ "bg": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "pazaruvaj", "wikipedia-bg"
+ ]
+ }
+ },
+ "bn": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-bn"
+ ]
+ }
+ },
+ "bn-BD": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-bn"
+ ]
+ }
+ },
+ "bn-IN": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-bn"
+ ]
+ }
+ },
+ "br": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-br"
+ ]
+ }
+ },
+ "bs": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-bs"
+ ]
+ }
+ },
+ "ca": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-es", "wikipedia-ca"
+ ]
+ }
+ },
+ "cak": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-es"
+ ]
+ }
+ },
+ "cs": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "mapy-cz", "seznam-cz", "wikipedia-cz"
+ ]
+ }
+ },
+ "cy": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-co-uk", "ddg", "ebay-co-uk", "wikipedia-cy"
+ ]
+ }
+ },
+ "da": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "amazon-co-uk", "bing", "ddg", "wikipedia-da"
+ ]
+ }
+ },
+ "de": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-de", "ddg", "ecosia", "qwant", "wikipedia-de", "ebay-de"
+ ]
+ }
+ },
+ "de-AT": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-de", "ddg", "ecosia", "qwant", "wikipedia-de", "ebay-at"
+ ]
+ }
+ },
+ "dsb": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-de", "ddg", "wikipedia-dsb", "ebay-de"
+ ]
+ }
+ },
+ "el": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-el"
+ ]
+ }
+ },
+ "en-AU": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-au", "ddg", "wikipedia", "ebay-au"
+ ]
+ }
+ },
+ "en-CA": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-ca", "ddg", "wikipedia", "ebay-ca"
+ ]
+ }
+ },
+ "en-IE": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-co-uk", "ddg", "qwant", "wikipedia", "ebay-ie"
+ ]
+ }
+ },
+ "en-GB": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-co-uk", "ddg", "qwant", "wikipedia", "ebay-co-uk"
+ ]
+ },
+ "BY": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "amazon-co-uk", "ddg", "qwant", "wikipedia"
+ ]
+ },
+ "KZ": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "amazon-co-uk", "ddg", "qwant", "wikipedia"
+ ]
+ },
+ "RU": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "amazon-co-uk", "ddg", "qwant", "wikipedia"
+ ]
+ },
+ "TR": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "amazon-co-uk", "ddg", "qwant", "wikipedia"
+ ]
+ }
+ },
+ "en-US": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "ebay", "wikipedia"
+ ]
+ },
+ "BY": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "amazondotcom", "ddg", "wikipedia"
+ ]
+ },
+ "KZ": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "amazondotcom", "ddg", "wikipedia"
+ ]
+ },
+ "RU": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "amazondotcom", "ddg", "wikipedia"
+ ]
+ },
+ "TR": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "amazondotcom", "ddg", "wikipedia"
+ ]
+ }
+ },
+ "en-ZA": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "wikipedia"
+ ]
+ }
+ },
+ "eo": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-eo"
+ ]
+ }
+ },
+ "es-AR": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "mercadolibre-ar", "wikipedia-es"
+ ]
+ }
+ },
+ "es-CL": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "mercadolibre-cl", "wikipedia-es"
+ ]
+ }
+ },
+ "es-ES": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-es", "amazon-es", "ebay-es"
+ ]
+ }
+ },
+ "es-MX": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "mercadolibre-mx", "wikipedia-es"
+ ]
+ }
+ },
+ "et": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "amazon-co-uk", "ddg", "wikipedia-et"
+ ]
+ }
+ },
+ "eu": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-es", "wikipedia-eu"
+ ]
+ }
+ },
+ "fa": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-fa"
+ ]
+ }
+ },
+ "ff": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-fr", "ddg", "wikipedia-fr"
+ ]
+ }
+ },
+ "fi": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "amazondotcom", "bing", "ddg", "wikipedia-fi"
+ ]
+ }
+ },
+ "fr-BE": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "qwant", "wikipedia-fr", "ebay-befr"
+ ]
+ }
+ },
+ "fr-CA": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-ca", "ddg", "wikipedia-fr", "ebay-ca"
+ ]
+ }
+ },
+ "fr-FR": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "qwant", "wikipedia-fr", "amazon-fr", "ebay-fr"
+ ]
+ }
+ },
+ "fr": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-fr", "qwant", "wikipedia-fr"
+ ]
+ }
+ },
+ "fy-NL": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-nl", "wikipedia-fy-NL"
+ ]
+ }
+ },
+ "ga-IE": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "amazon-co-uk", "ddg", "ebay-ie", "wikipedia-ga-IE"
+ ]
+ }
+ },
+ "gd": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-co-uk", "faclair-beag", "wikipedia-gd"
+ ]
+ }
+ },
+ "gl": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "ebay-es", "wikipedia-gl"
+ ]
+ }
+ },
+ "gn": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-gn"
+ ]
+ }
+ },
+ "gu-IN": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-gu"
+ ]
+ }
+ },
+ "he": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-he"
+ ]
+ }
+ },
+ "hi-IN": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-hi"
+ ]
+ }
+ },
+ "hr": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-co-uk", "ddg", "wikipedia-hr"
+ ]
+ }
+ },
+ "hsb": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-de", "ddg", "wikipedia-hsb", "ebay-de"
+ ]
+ }
+ },
+ "hu": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "vatera", "wikipedia-hu"
+ ]
+ }
+ },
+ "hy-AM": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-hy-AM"
+ ]
+ }
+ },
+ "ia": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-ia"
+ ]
+ }
+ },
+ "id": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-id"
+ ]
+ }
+ },
+ "is": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-is"
+ ]
+ }
+ },
+ "it": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-it", "amazon-it", "ebay-it"
+ ]
+ }
+ },
+ "ja": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "amazon-jp", "bing", "ddg", "rakuten", "wikipedia-ja", "yahoo-jp", "yahoo-jp-auctions"
+ ]
+ }
+ },
+ "ka": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-ka"
+ ]
+ }
+ },
+ "kab": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-kab"
+ ]
+ }
+ },
+ "kk": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-kk"
+ ]
+ },
+ "KZ": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "ddg", "wikipedia-kk"
+ ]
+ },
+ "BY": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "ddg", "wikipedia-kk"
+ ]
+ },
+ "RU": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "ddg", "wikipedia-kk"
+ ]
+ },
+ "TR": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "bing", "ddg", "wikipedia-kk"
+ ]
+ }
+ },
+ "km": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-km"
+ ]
+ }
+ },
+ "kn": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-kn", "wiktionary-kn"
+ ]
+ }
+ },
+ "ko": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "daum-kr"
+ ]
+ }
+ },
+ "lij": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-it", "ddg", "wikipedia-lij", "ebay-it"
+ ]
+ }
+ },
+ "lo": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-lo"
+ ]
+ }
+ },
+ "lt": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-lt"
+ ]
+ }
+ },
+ "ltg": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-co-uk", "wikipedia-ltg"
+ ]
+ }
+ },
+ "lv": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "salidzinilv", "wikipedia-lv"
+ ]
+ }
+ },
+ "mai": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-hi"
+ ]
+ }
+ },
+ "meh": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-es"
+ ]
+ }
+ },
+ "mix": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-es"
+ ]
+ }
+ },
+ "ml": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-ml"
+ ]
+ }
+ },
+ "mr": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-mr"
+ ]
+ }
+ },
+ "ms": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-ms"
+ ]
+ }
+ },
+ "my": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-my"
+ ]
+ }
+ },
+ "nb-NO": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "gulesider-mobile-NO", "wikipedia-NO"
+ ]
+ }
+ },
+ "ne-NP": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-ne"
+ ]
+ }
+ },
+ "nl-NL": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-nl", "amazon-nl", "ebay-nl"
+ ]
+ }
+ },
+ "nl": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-nl", "wikipedia-nl"
+ ]
+ }
+ },
+ "nn-NO": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "gulesider-mobile-NO", "wikipedia-NN"
+ ]
+ }
+ },
+ "oc": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-oc", "wiktionary-oc"
+ ]
+ }
+ },
+ "or": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-or", "wiktionary-or"
+ ]
+ }
+ },
+ "pa-IN": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-pa"
+ ]
+ }
+ },
+ "pl": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-pl", "ebay-pl"
+ ]
+ }
+ },
+ "pt-BR": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-pt"
+ ]
+ }
+ },
+ "pt-PT": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "wikipedia-pt"
+ ]
+ }
+ },
+ "rm": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-ch", "leo_ende_de", "wikipedia-rm"
+ ]
+ }
+ },
+ "ro": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-ro"
+ ]
+ }
+ },
+ "ru": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "wikipedia-ru"
+ ]
+ },
+ "RU": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "ddg", "wikipedia-ru"
+ ]
+ },
+ "BY": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "ddg", "wikipedia-ru"
+ ]
+ },
+ "KZ": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "ddg", "wikipedia-ru"
+ ]
+ },
+ "TR": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "ddg", "wikipedia-ru"
+ ]
+ }
+ },
+ "sk": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "wikipedia-sk"
+ ]
+ }
+ },
+ "sl": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "ceneje", "odpiralni", "wikipedia-sl"
+ ]
+ }
+ },
+ "son": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "ddg", "bing", "amazon-fr", "wikipedia-fr"
+ ]
+ }
+ },
+ "sq": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-co-uk", "ddg", "wikipedia-sq"
+ ]
+ }
+ },
+ "sr": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-sr"
+ ]
+ }
+ },
+ "sv-SE": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "prisjakt-sv-SE", "ddg", "wikipedia-sv-SE", "amazon-se", "ebay-ch"
+ ]
+ }
+ },
+ "ta": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-ta", "wiktionary-ta"
+ ]
+ }
+ },
+ "te": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-te", "wiktionary-te"
+ ]
+ }
+ },
+ "th": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-th"
+ ]
+ }
+ },
+ "tl": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg"
+ ]
+ }
+ },
+ "tr": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-tr"
+ ]
+ },
+ "TR": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "ddg", "bing", "wikipedia-tr"
+ ]
+ },
+ "BY": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "ddg", "bing", "wikipedia-tr"
+ ]
+ },
+ "KZ": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "ddg", "bing", "wikipedia-tr"
+ ]
+ },
+ "RU": {
+ "visibleDefaultEngines": [
+ "google-com-nocodes", "ddg", "bing", "wikipedia-tr"
+ ]
+ }
+ },
+ "trs": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-es"
+ ]
+ }
+ },
+ "uk": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-uk"
+ ]
+ }
+ },
+ "ur": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazon-in", "ddg", "wikipedia-ur"
+ ]
+ }
+ },
+ "uz": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-uz"
+ ]
+ }
+ },
+ "vi": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "coccoc", "ddg", "wikipedia-vi"
+ ]
+ }
+ },
+ "wo": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "ebay-fr", "wikipedia-wo"
+ ]
+ }
+ },
+ "xh": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia"
+ ]
+ }
+ },
+ "zam": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-es"
+ ]
+ }
+ },
+ "zh-CN": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "baidu", "bing", "ddg", "wikipedia-zh-CN"
+ ]
+ },
+ "CN": {
+ "searchDefault": "百度"
+ }
+ },
+ "zh-TW": {
+ "default": {
+ "visibleDefaultEngines": [
+ "google-b-m", "bing", "ddg", "wikipedia-zh-TW"
+ ]
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/search/search_telemetry_v2.json b/mobile/android/android-components/components/feature/search/src/main/assets/search/search_telemetry_v2.json
new file mode 100644
index 0000000000..1803977187
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/search/search_telemetry_v2.json
@@ -0,0 +1,657 @@
+{
+ "data": [
+ {
+ "isSPA": true,
+ "schema": 1707523204491,
+ "components": [
+ {
+ "type": "ad_image_row",
+ "included": {
+ "parent": {
+ "selector": "[data-testid='pam.container']"
+ },
+ "children": [
+ {
+ "selector": "[data-slide-index]",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "included": {
+ "parent": {
+ "selector": "[data-testid='adResult']"
+ }
+ }
+ },
+ {
+ "type": "incontent_searchbox",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "._1zdrb._1cR1n"
+ },
+ "related": {
+ "selector": "#search-suggestions"
+ },
+ "children": [
+ {
+ "selector": "input[type='search']"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "default": true
+ }
+ ],
+ "taggedCodes": [
+ "brz-moz",
+ "firefoxqwant"
+ ],
+ "telemetryId": "qwant",
+ "organicCodes": [],
+ "codeParamName": "client",
+ "queryParamName": "q",
+ "queryParamNames": [
+ "q"
+ ],
+ "searchPageRegexp": "^https://www\\.qwant\\.com/",
+ "filter_expression": "env.version|versionCompare(\"124.0a1\")>=0",
+ "followOnParamNames": [],
+ "defaultPageQueryParam": {
+ "key": "t",
+ "value": "web"
+ },
+ "extraAdServersRegexps": [
+ "^https://www\\.bing\\.com/acli?c?k",
+ "^https://api\\.qwant\\.com/v3/r/"
+ ],
+ "id": "19c434a3-d173-4871-9743-290ac92a3f6b",
+ "last_modified": 1707833261849
+ },
+ {
+ "schema": 1705948294201,
+ "components": [
+ {
+ "type": "ad_carousel",
+ "included": {
+ "parent": {
+ "selector": ".pla-exp-container"
+ },
+ "related": {
+ "selector": "g-right-button, g-left-button, .exp-button"
+ },
+ "children": [
+ {
+ "selector": "[data-dtld]",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "refined_search_buttons",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "#appbar g-scrolling-carousel"
+ },
+ "related": {
+ "selector": "g-right-button, g-left-button"
+ },
+ "children": [
+ {
+ "selector": "a"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "excluded": {
+ "parent": {
+ "selector": "#rhs"
+ }
+ },
+ "included": {
+ "parent": {
+ "selector": "[data-text-ad='1']"
+ },
+ "children": [
+ {
+ "type": "ad_sitelink",
+ "selector": "[role='list']"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_sidebar",
+ "included": {
+ "parent": {
+ "selector": "#rhs"
+ },
+ "children": [
+ {
+ "selector": ".pla-unit, .mnr-c",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "incontent_searchbox",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "form[role='search']"
+ },
+ "related": {
+ "selector": "div.logo + div + div"
+ },
+ "children": [
+ {
+ "selector": "input[type='text']"
+ },
+ {
+ "selector": "textarea[name='q']"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_image_row",
+ "excluded": {
+ "parent": {
+ "selector": ".pla-exp-container"
+ }
+ },
+ "included": {
+ "parent": {
+ "selector": ".top-pla-group-inner"
+ },
+ "children": [
+ {
+ "selector": "[data-dtld]",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "default": true
+ }
+ ],
+ "shoppingTab": {
+ "regexp": "&tbm=shop",
+ "selector": "div[role='navigation'] a",
+ "inspectRegexpInSERP": true
+ },
+ "taggedCodes": [
+ "firefox-a",
+ "firefox-b",
+ "firefox-b-1",
+ "firefox-b-ab",
+ "firefox-b-1-ab",
+ "firefox-b-d",
+ "firefox-b-1-d",
+ "firefox-b-e",
+ "firefox-b-1-e",
+ "firefox-b-m",
+ "firefox-b-1-m",
+ "firefox-b-o",
+ "firefox-b-1-o",
+ "firefox-b-lm",
+ "firefox-b-1-lm",
+ "firefox-b-lg",
+ "firefox-b-huawei-h1611",
+ "firefox-b-is-oem1",
+ "firefox-b-oem1",
+ "firefox-b-oem2",
+ "firefox-b-tinno",
+ "firefox-b-pn-wt",
+ "firefox-b-pn-wt-us",
+ "ubuntu",
+ "ubuntu-sn"
+ ],
+ "telemetryId": "google",
+ "organicCodes": [],
+ "codeParamName": "client",
+ "queryParamName": "q",
+ "queryParamNames": [
+ "q"
+ ],
+ "domainExtraction": {
+ "ads": [
+ {
+ "method": "data-attribute",
+ "options": {
+ "dataAttributeKey": "dtld"
+ },
+ "selectors": "[data-dtld]"
+ }
+ ],
+ "nonAds": [
+ {
+ "method": "href",
+ "selectors": "#rso div.g[jscontroller] > div > div > div > div a[data-usg]"
+ }
+ ]
+ },
+ "searchPageRegexp": "^https://www\\.google\\.(?:.+)/search",
+ "nonAdsLinkRegexps": [
+ "^https?://www\\.google\\.(?:.+)/url?(?:.+)&url="
+ ],
+ "adServerAttributes": [
+ "rw"
+ ],
+ "followOnParamNames": [
+ "oq",
+ "ved",
+ "ei"
+ ],
+ "extraAdServersRegexps": [
+ "^https?://www\\.google(?:adservices)?\\.com/(?:pagead/)?aclk"
+ ],
+ "id": "635a3325-1995-42d6-be09-dbe4b2a95453",
+ "last_modified": 1706198445460
+ },
+ {
+ "schema": 1705363206938,
+ "components": [
+ {
+ "type": "ad_carousel",
+ "included": {
+ "parent": {
+ "selector": ".module--carousel"
+ },
+ "related": {
+ "selector": ".module--carousel__left, .module--carousel__right"
+ },
+ "children": [
+ {
+ "selector": ".module--carousel__item",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "excluded": {
+ "parent": {
+ "selector": ".js-results-sidebar"
+ }
+ },
+ "included": {
+ "parent": {
+ "selector": "article[data-testid='ad']"
+ },
+ "children": [
+ {
+ "type": "ad_sitelink",
+ "selector": "ul"
+ }
+ ]
+ }
+ },
+ {
+ "type": "incontent_searchbox",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "form#search_form"
+ },
+ "related": {
+ "selector": "input#search_button, .search__autocomplete"
+ },
+ "children": [
+ {
+ "selector": " input#search_form_input"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_sidebar",
+ "included": {
+ "parent": {
+ "selector": ".js-results-sidebar"
+ },
+ "children": [
+ {
+ "selector": "article[data-testid='ad']",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "default": true
+ }
+ ],
+ "shoppingTab": {
+ "regexp": "&iax=shopping&ia=shopping",
+ "selector": "#duckbar a[data-zci-link='products']"
+ },
+ "taggedCodes": [
+ "ffab",
+ "ffcm",
+ "ffhp",
+ "ffip",
+ "ffit",
+ "ffnt",
+ "ffocus",
+ "ffos",
+ "ffsb",
+ "fpas",
+ "fpsa",
+ "ftas",
+ "ftsa",
+ "lm",
+ "newext"
+ ],
+ "telemetryId": "duckduckgo",
+ "organicCodes": [],
+ "codeParamName": "t",
+ "queryParamName": "q",
+ "queryParamNames": [
+ "q"
+ ],
+ "domainExtraction": {
+ "ads": [
+ {
+ "method": "href",
+ "options": {
+ "queryParamKey": "ad_domain"
+ },
+ "selectors": ".products-carousel a.js-carousel-item-title, [data-testid='ad'] a[data-testid='result-title-a']"
+ }
+ ],
+ "nonAds": [
+ {
+ "method": "href",
+ "selectors": "[data-layout='organic'] a[data-testid='result-title-a']"
+ }
+ ]
+ },
+ "searchPageRegexp": "^https://duckduckgo\\.com/",
+ "expectedOrganicCodes": [
+ "h_",
+ "ha",
+ "hb",
+ "hc",
+ "hd",
+ "he",
+ "hf",
+ "hg",
+ "hh",
+ "hi",
+ "hj",
+ "hk",
+ "hl",
+ "hm",
+ "hn",
+ "ho",
+ "hp",
+ "hq",
+ "hr",
+ "hs",
+ "ht",
+ "hu",
+ "hv",
+ "hw",
+ "hx",
+ "hy",
+ "hz"
+ ],
+ "extraAdServersRegexps": [
+ "^https://duckduckgo.com/y\\.js?.*ad_provider\\=",
+ "^https://www\\.amazon\\.(?:[a-z.]{2,24}).*(?:tag=duckduckgo-)"
+ ],
+ "id": "9dfd626b-26f2-4913-9d0a-27db6cb7d8ca",
+ "last_modified": 1706198445456
+ },
+ {
+ "schema": 1698656464939,
+ "taggedCodes": [
+ "monline_dg",
+ "monline_3_dg",
+ "monline_4_dg",
+ "monline_7_dg"
+ ],
+ "telemetryId": "baidu",
+ "organicCodes": [],
+ "codeParamName": "tn",
+ "queryParamName": "wd",
+ "queryParamNames": [
+ "wd",
+ "word"
+ ],
+ "searchPageRegexp": "^https://(?:m|www)\\.baidu\\.com/(?:s|baidu)",
+ "followOnParamNames": [
+ "oq"
+ ],
+ "extraAdServersRegexps": [
+ "^https?://www\\.baidu\\.com/baidu\\.php?"
+ ],
+ "id": "19c434a3-d173-4871-9743-290ac92a3f6a",
+ "last_modified": 1698666532326
+ },
+ {
+ "schema": 1698656463945,
+ "components": [
+ {
+ "type": "ad_carousel",
+ "included": {
+ "parent": {
+ "selector": ".product-ads-carousel"
+ },
+ "related": {
+ "selector": ".snippet__control"
+ },
+ "children": [
+ {
+ "selector": ".product-ads-carousel__item",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "included": {
+ "parent": {
+ "selector": ".ad-result"
+ },
+ "children": [
+ {
+ "type": "ad_sitelink",
+ "selector": ".result__extra-content .deep-links--descriptions"
+ }
+ ]
+ }
+ },
+ {
+ "type": "incontent_searchbox",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "form.search-form"
+ },
+ "related": {
+ "selector": ".search-form__suggestions"
+ },
+ "children": [
+ {
+ "selector": ".search-form__input, .search-form__submit"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "default": true
+ }
+ ],
+ "shoppingTab": {
+ "regexp": "/shopping?",
+ "selector": "nav li[data-test-id='search-navigation-item-shopping'] a"
+ },
+ "taggedCodes": [
+ "mzl",
+ "813cf1dd",
+ "16eeffc4"
+ ],
+ "telemetryId": "ecosia",
+ "organicCodes": [],
+ "codeParamName": "tt",
+ "queryParamName": "q",
+ "queryParamNames": [
+ "q"
+ ],
+ "searchPageRegexp": "^https://www\\.ecosia\\.org/",
+ "filter_expression": "env.version|versionCompare(\"110.0a1\")>=0",
+ "expectedOrganicCodes": [],
+ "extraAdServersRegexps": [
+ "^https://www\\.bing\\.com/acli?c?k"
+ ],
+ "id": "9a487171-3a06-4647-8866-36250ec84f3a",
+ "last_modified": 1698666532324
+ },
+ {
+ "schema": 1698656462833,
+ "components": [
+ {
+ "type": "ad_carousel",
+ "included": {
+ "parent": {
+ "selector": ".adsMvCarousel"
+ },
+ "related": {
+ "selector": ".cr"
+ },
+ "children": [
+ {
+ "selector": ".pa_item",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "excluded": {
+ "parent": {
+ "selector": "aside"
+ }
+ },
+ "included": {
+ "parent": {
+ "selector": ".sb_adTA"
+ },
+ "children": [
+ {
+ "type": "ad_sitelink",
+ "selector": ".b_vlist2col"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_sidebar",
+ "included": {
+ "parent": {
+ "selector": "aside"
+ },
+ "children": [
+ {
+ "selector": ".pa_item, .sb_adTA",
+ "countChildren": true
+ }
+ ]
+ }
+ },
+ {
+ "type": "incontent_searchbox",
+ "topDown": true,
+ "included": {
+ "parent": {
+ "selector": "form#sb_form"
+ },
+ "related": {
+ "selector": "#sw_as"
+ },
+ "children": [
+ {
+ "selector": "input[name='q']"
+ }
+ ]
+ }
+ },
+ {
+ "type": "ad_link",
+ "default": true
+ }
+ ],
+ "shoppingTab": {
+ "regexp": "^/shop?",
+ "selector": "#b-scopeListItem-shop a"
+ },
+ "taggedCodes": [
+ "MOZ2",
+ "MOZ4",
+ "MOZ5",
+ "MOZA",
+ "MOZB",
+ "MOZD",
+ "MOZE",
+ "MOZI",
+ "MOZL",
+ "MOZM",
+ "MOZO",
+ "MOZR",
+ "MOZT",
+ "MOZW",
+ "MOZX",
+ "MZSL01",
+ "MZSL02",
+ "MZSL03"
+ ],
+ "telemetryId": "bing",
+ "organicCodes": [],
+ "codeParamName": "pc",
+ "queryParamName": "q",
+ "followOnCookies": [
+ {
+ "host": "www.bing.com",
+ "name": "SRCHS",
+ "codeParamName": "PC",
+ "extraCodePrefixes": [
+ "QBRE"
+ ],
+ "extraCodeParamName": "form"
+ }
+ ],
+ "queryParamNames": [
+ "q"
+ ],
+ "searchPageRegexp": "^https://www\\.bing\\.com/search",
+ "nonAdsLinkRegexps": [
+ "^https://www.bing.com/ck/a"
+ ],
+ "extraAdServersRegexps": [
+ "^https://www\\.bing\\.com/acli?c?k"
+ ],
+ "id": "e1eec461-f1f3-40de-b94b-3b670b78108c",
+ "last_modified": 1698666532321
+ }
+ ],
+ "timestamp": 1707833261849
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-au.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-au.xml
new file mode 100644
index 0000000000..6e8801d893
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-au.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.com.au</ShortName>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.com.au/s">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.com.au/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-ca.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-ca.xml
new file mode 100644
index 0000000000..932f62b276
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-ca.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.ca</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.ca/s" resultdomain="amazon.ca">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.ca/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-co-uk.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-co-uk.xml
new file mode 100644
index 0000000000..5ff238cc73
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-co-uk.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.co.uk</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.co.uk/s" resultdomain="amazon.co.uk">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.co.uk/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-de.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-de.xml
new file mode 100644
index 0000000000..137abd4b94
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-de.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.de</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.de/s" resultdomain="amazon.de">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.de/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-es.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-es.xml
new file mode 100644
index 0000000000..c989b9f361
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-es.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.es</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.es/s" resultdomain="amazon.es">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.es/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-fr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-fr.xml
new file mode 100644
index 0000000000..abfb75bee5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-fr.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.fr</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.fr/s" resultdomain="amazon.fr">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.fr/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-in.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-in.xml
new file mode 100644
index 0000000000..2c1be1f73a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-in.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.in</ShortName>
+<InputEncoding>utf-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.in/s">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.in/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-it.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-it.xml
new file mode 100644
index 0000000000..805bbf0af2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-it.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.it</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.it/s" resultdomain="amazon.it">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.it/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-jp.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-jp.xml
new file mode 100644
index 0000000000..7d57698067
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-jp.xml
@@ -0,0 +1,31 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.co.jp</ShortName>
+<Description>検索エンジン - Amazon.co.jp 検索</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://completion.amazon.co.jp/search/complete?q={searchTerms}&amp;client=amazon-search-ui&amp;search-alias=aps&amp;mkt=6"/>
+<Url type="text/html" method="GET" template="https://www.amazon.co.jp/exec/obidos/external-search/" resultdomain="amazon.co.jp">
+ <Param name="field-keywords" value="{searchTerms}"/>
+ <Param name="mode" value="blended"/>
+ <!--
+ <Param name="mode" value="books-jp"/>
+ <Param name="mode" value="books-us"/>
+ -->
+ <Param name="tag" value="moz-jp-mbl-22"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+ <!--
+ <Param name="sz" value="25"/>
+ <Param name="rank" value="+salesrank"/>
+ <Param name="rank" value="+pricerank"/>
+ <Param name="rank" value="+inverse-pricerank"/>
+ <Param name="rank" value="+daterank"/>
+ <Param name="rank" value="+titlerank"/>
+ <Param name="rank" value="-titlerank"/>
+ -->
+</Url>
+<SearchForm>https://www.amazon.co.jp/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-nl.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-nl.xml
new file mode 100644
index 0000000000..75530332cc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-nl.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.nl</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.nl/s">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.nl/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-se.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-se.xml
new file mode 100644
index 0000000000..98f7b2d35a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazon-se.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.se</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABaFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////mQACAgH7+/uEhIT9mACFhYX9/f0JCQkFBQWwagBtQgAJBQANDQ36lgDr6+vj4+O7u7u3t7dDQ0M9PT0oKCgbGxvVgADGdwCqZgBYNQBCKAAkFgAcEQARCgDw8PDS0tKysrKtra2Xl5eSkpJtbW1dXV1aWlpLS0tHR0c5OTkzMzMuLi4qKirrjQDniwDehgC7cACZXACKUwBjOwBPLwA8JAAsGwAgEwD19fXn5+fZ2dnPz8/Ly8vHx8ekpKSdnZ2JiYl/f394eHggICD7lwD2lADRfgDPfACeXwCETwBdOAA4IgA0IAAWDgDd3d3W1ta/v7+oqKiPj497e3twcHBpaWljY2NWVlYUFBTsjgDiiADZgwDAcwC3bgCmYwCPVgB2RwBnPgBKLQAMCADt7e1fX19SUlIWFhYSEhLwkADujwCRVwB8SwB6SgBSMgAPCQBOQY7nAAAABnRSTlMA8si8ZBgxEvPEAAADLklEQVRo3u2aZ1MiMRiA0dO8WeWkd6miNEVF6c3ee++ent3r9e/fgm6iggJuMjfO7PMxs+wTkrcEJiqRluYmxIGm5hZVidZ3iBvvWkUBj/dTg7g+iCstqmbElWZVE+JKkwpxRhEoAkWgCBTBfxBoNVpEYCz41hexbbUBfBoPtQ8KzAUOm/huyui+kanAaYWn9HxmKBgwQxUizAT2NqjKHCNBZw9UZ2SejcAGzzHNROAAgqU92n8wDgSzwEIwBRLvNUhEmKGGGAOBQCJoRhqaJIIvDARDJPBJah0SQS8DgWbnPkjDZGiQCGaZRJEw7+ibnZ5yVn4paOdTrofptqO3JdAOH89O28ZHO7gIBtsnSEliL9AeBYDAQeAYA+ApmOsAroJ9oHQErFORMNs8sAPBfPC13CCYCjRbIGE1kjxgKOgDiQkNl1KxAxJ2aSjGUqAhEWShhwyWAlqag2Ssl4yF5QuOQSJU5RQQlC+ge2wlxxhakyzyBVF6VpTO1SGgDMgW2OFpA56DBwS0cgUfaB1qi5aOwXvwiD3ZeRAAyvautaInxOQKeuFl+uUKjKNQwcQuSMzIr6YDFd0gqDGO3W/LIYt+cPRk3SMCQvMWEBnpZ9PR7GNAmXTcZZsZwDJEzmaFnF6vv/HXFnQJVXt+NDhS7mfbYTLn2GTIKX0oHcd3eGoKXK7ccwnR6TSiqmQWsMRiTUFxGbt9qDGuvfmC/7Z7PY3x99p7YLjAeClrQK+gG+Ozejb5So3xj0wRNULxt6cbFTFerSuKfEmMsVr35xbVx6beLT6/gdYwvqkvTA2XaixiSum7aq9LNlV6OOlDaAUnhXrzYH0Z3+HK5PzoOfze9HJ5KgsZA0LCCdY3kGjej1gicb66lvcLj1ZlY23Vc4rv0RWQSA4nDI1kspAtKSjquMud0ul05+6lU9PD8YsCKqMzrTdaKnweE65B4meXtBmm7Ctq0aY+9YLjJO0TaGTkX1ns/npXlhZwBXHdVZ68XfYPEMPG9aXHnYwvYvViwnW28stL4peJgFJ9zm/qb01FgJAiUASKACFFUAH3SwPcrz1wv7jB++oJ/8sz3K///AMH5R5E/GGrogAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.se/s" resultdomain="amazon.se">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.se/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazondotcom.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazondotcom.xml
new file mode 100644
index 0000000000..2af84b936c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/amazondotcom.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.com</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="32" height="32">data:image/png;base64,AAABAAIAEBAAAAEACACtAQAAJgAAACAgAAABACAArAMAANMBAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAAF0SURBVDiNpVM9S8NQFD2RvC6KCE6V5gdUHBonF6eQrQ4q6dbVjkKH/gN3oVmLk8VFK7gKuiklU6lguliHpliXhrRLKxyH1NB8aEUP3OHdd+9559z3niQLgelkQvwBIpWSJJKx5k6ng1qthpduFwCwl8+jWCwmszACy7IoCxGLSqUSLSVJxgiOSiXKQlDTddq2HawzipJIELPQ7/fhOA5c14XneTiv19FoNAAA08lksQXbtplT1UQbv7JgFAqUhWBOVek4Dqum+SPBUlTRl1zDMJBOp9FqtYK90Wi02MK8fE3XQxaqpvmNhXEvSFiWxYyiBJOvmiaNQoGartPzPLJzRjbLfgQEN1ly+JzoMYS7fb/x6ZS8wBxB79ZPNMshNTGMe2T3yj/scp3k/Dt4bQCPB/5g1nYB5TA8rLd7YHAN7FwBYhUYtoHNY4Qf0rsFtE/8wiiWs8B2FdjQgI/ZbcgrSPxMcG1g8ABM3ZmiLb8xAdJ/v/Mnx1XnNJorMikAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAANzSURBVFiFtZdPaBRXHMc/r+4GlGAbylYWXZFQaCOt7CSK/+hJt/XSogUDvfTQ0OzBg2hgblEQFA9iLeauBDyYUB3x0kv6J0pr4gZLum0yFIpt3V3cUJoymxwS4edhMnEn+3bmJSFfGNj3m9+832d+7/d7bxaWJCKbRaRPRP6QjdPkUozN1EtE2jc4sA6kHUAt0UwCb2OoWq2G67rLY6UU6XSadDptOgXAr8B+ROScKbbjOHI0l5NEMqm9juZy4jjOajLRhxik3vO8yMArL9u2TQEKmHid7O4OBdiUSMRCGGZiPhbAcRxtgKxlycnu7qaZOZLLmQBILMCXvb2xKbZtWwthotfiSlUp1WDr7+8PjXt6elZT/eH5RUTinOrbTilFZ2dng0+ypaXBtriwEAuQMKFsbW2lq6treVypVCiXy/z86BEAo6OjAIiINmPrBgiC3rh5k+HhYYrFotZntcEBjNrQcRzZkckY7wOrKcLYGpiYmODAwYPae2fPnCGzcye7Ozr46NixhvsmNRCbgaxlabfcUqkU8ltrBiIBXNfVTlwoFEJ+5XJ5Y/aBqakprb2+I6L8KpVK7ApEAvzz7JnWXqvVQuPbQ0Nav/Hx8ViAyCUYGRnRHj71W/Hg4GDTAyprWeJ5XuQSRAI0W9tgcpPWvD4wsHYAkcajOApopW1HJiOu664PoNnHSH3KbdsWz/NCp2LWsmKDGwEEENcHBpZBNiUSkrUs6c3nG1ry4qVL0pvPh9d+riRSfSxSHRdZDNdEeCf8/WvYkoFdn8ZXr4nmyzB2CqoOBFH2XIPdp5t0wey0yDdvinx33CderxY9f565kv/7fofIb9dCLuF94PV34JOnsPAvjOyD70/A0zvwItz3xqqOQfEi3N8OcyWoTUHqUMhFfxi9qMGT8/Dn1Vepy3wBbXsgdRja3oVEa+Mz/03DbBGqD+D5Pf9FFHDgDiS3QvECfPhj/VP/KxGZBN7XvsFMAcY+98mDo34lrqqzrfwc2HYcrMt+ZmceQ8tW//crPVQi0gdc0QIE+usuTH8Fsw/8YEFQHZQC2s/Crs8gtTdyWiAf/DX7CcjGeTNfhpkxmPsbnv+wFFBBss1fnjfeg7f2Ny6PXg+VUh/4LyCSEpF76y97Y90SkVSQsGWJyAngY2Avzepi7foFeAIMKaW+DYwvASfwPtL4PwlOAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://www.amazon.com/s" resultdomain="amazon.com">
+ <Param name="k" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.amazon.com/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/azerdict.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/azerdict.xml
new file mode 100644
index 0000000000..779c658f60
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/azerdict.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Azerdict</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAB3VBMVEUAAAD86x776hf76hf76hf76hf76hf76hf87DL76hf87DL76hf76hf76hdXV1f87DL96xb/7hX+7BZTU1n/8hTSyVd+fGD/8xNraU9UVFhWVlfJv1//8BRVVVhPUFpTVFhlZFZQUV1ZWVxRUVn05SB7eWbTyWJdXV1eXltiYlloZlO9tUjr3S756Br/8BZnaG5fYGidmWGEgWFbW1vq3Ubt3iL35yD66h7/7xmUknOemmiRj2hYWmOwq2BgYF+Oil2/uFpYWVpWVlnXzVRsalPSyEb05jf46Cfm1yT/8CP76hq0sH13d3RubnJpanG0r2tmZmrHwGaGg2W1r2RnZ2S9tmLSyl5UVV53dVvOxlrOxVdzcldvblfSx03Nw0zFvEzaz0rQxUqdl0i5sELg1UG2rUDazz+wpzzs3zjl2Dju4DH15jDh1DDy4ij87CLu3yGLiXvZ0XiDgXSYlXOfm3GPjHHDvG+jn26Zlm15d23X0Gy9t2x+fWnn3GiWkWh0c2hra2ZpaWWrpmRqaWSoomJqaWDZ0F/JwF9+e11qaFrLw1a8s1TBuFGalVDh106rpE6XkUzWy0vJv0XFu0Xj10Pn2kHMwkHVyT7g1D2poDjn2THazC7/8if66Sb45xkFAQu1AAAADXRSTlMABtL5xWwbz8LAb2/zKa6xSQAAAxJJREFUaN7tmmVX21AYgANdiy3hRtjWLAnVVaiXtrj7cHeGO8zd3d3tt65lg8DO2uZtd884O3l+QJ5773tyn3wIQRAZ2hwNiQFNjjaDiKLLIrGRpYuuP5PESGYGoSWxoiUOkFg5QGhIrGgIEjOqQBWogsQCtAUJACRAwtzsp8U3HyMcjXAI0Gq1o1asCzqcgb45WkgigQuQ6wZP/UI0V5yu/BKyuBFCf02wYdVTu+BZyWd9ufh6uUTeS1oC4bZI/Q7P19kcDeUDLo5m0hYgJxUHXpR85560bMYOLA2By0AlQDQbuwMz840rgnxgQMG6LIi7k1q7YazscVh2gGYwRiVGHn7HxfehNrCArmGpZMhD8b5oRkABybTzlHLq/BGogAzdghikUQQVoLXqCklUbOhshV924Zap0k6JVbaRwstMCj1AtNA8WjZssJv4ZBr+OA0UyA7PUuPsTK/XaBYxCLbL47ZsttSUeiV9vJ3oTzFpJ5Ohuaaes+MOe+0fDqzjK2DICQ+sZGl6/tnVbuPe4fvuo/QFcqkjltBa30mfPBRTTxu8yckPrPhKeYPDFgxWWa/RJEAAObDWd9NFU89bEQkQwNj9ZbP/PrxUQXIBswOdiFSHTG/UF2zz4Gh8Rt7KCojAdcbI7qBPQKHZ+/Q7WEC/uqOnFMNWW4ACVGzjKQD66wJMUGKlYJgbQT1AJ+wUkIccSFAUhAqcFpjABBUY1kGCCRtUMA6Lfv4jCoZYCRPQ50UKAn/TA3zRPHeHIOu/twyOfqSmQlS6fPbS5xTuIrqpq/eQIspHOBIu2Go6pwiB3rfBUQX/XIAYZaQc/W+x6A8eizFYEJ/6JiG16Jf+jH5hDDYRUvukGx79iX5R+VXH+t3g6Jtg0S/DHX1pAXf0AwLmJjubcUd/BSSYtIF30AYa8qqBgsH7BVj0u/Sw51eFETD6fiPo+R8Y6FURrhxS/KqJ/QupRL/4wvARJTQEBjwopegL3GFFcGr0VcEeVMH/KNCQWDmI/7eHPBIrefh/PSF0GA2ZOiJKRm52Phayc6Pr/wHPrTIRNf30iwAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://api.azerdict.com/english/autocomplete">
+ <Param name="action" value="opensearch" />
+ <Param name="query" value="{searchTerms}" />
+</Url>
+<Url type="text/html" method="GET" template="https://azerdict.com/english/" resultdomain="azerdict.com">
+ <Param name="word" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://azerdict.com/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/baidu.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/baidu.xml
new file mode 100644
index 0000000000..494d8b1b06
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/baidu.xml
@@ -0,0 +1,25 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>百度</ShortName>
+ <InputEncoding>UTF-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAC/VBMVEX6+voAAAD6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+voAbLf///8Aa7cAbLYAZbQAbrsAbrwAbLgAbbkAZLMAcL0AaLUAbrkAabYAZbMAcb+Qv98Ab70Web3+/v/k7/f5+fkAccC11OoAcL88j8j09fW92exPms0VeL329/fy8/SaxeIAccHv8vRLjLkAZ7T4+/200+oMdLshergZd7jt9frl6ezg5eje4+gKdL4AcLzV5/Pc4eWly+WdxuPK0tdnqNS1xdCatMcAcsJ4ob8zhLsUdrpBh7cSdLfo7O7a3uKYxOLO2uHP2N6MvN59s9nBztdSm86zwsyju8tBkcmet8g4i8Zfl71clLwsgLwhfLwbertvnLoJcLkceLgEbrhmlbdSjbczgbcuf7cqfbcadbPy9vjs7/Dq7e/m6+7l6Ovi5+rf4+aozebW29/M1dy9ztm5ydTCzNNGlcuPs8k7jsiluceJrscthsSLrMOCqMNimsElgcEJdcFXlcBrnr8ifr+IqL1omr1PkbwIcrxGirsygbsRdbsFcbsOcrg7hLdakbZKibUAX7Hp7/PA2+2w0enU3+bK1+XH0tq4zdq8yth3r9dvrNa2yNSxxtSowdJfotK9ydGjvtFcodG6xs+vws9Vnc+Hsc1qoMcwicV2o8SYscNLj8MogcNyoL8Od78Fcr9jmL4/hr46hr1rmbpak7lWj7fq8/nm8Pjs8fPl7PLJ4PC82Ozb4+rM3enj5ujW2+ahyeS40eGSwODR19qqxdm1yddzrtdxrNbGz9WxwdGZuc+LscyrvcmLsMh4pciErMZ+qMWRsMMWfMMMdsMaesCRrL59oblGibdymrVrlrU4gbVHha9gjKz5+frf7fbF3e/b5u6vy924ydycwtyEt9yyzNukxNukudmuv9eTutabt9SCqc1amMxvo8ddksI4gb9vnr5SjLyEpbt0nrtNibpkl7kxfbGYQtMsAAAAFnRSTlPhANvFsBQD7bxmV0Dz8OTAtJCKJyQYp4Li+AAACL9JREFUaN7FmgO03EAUhmtb08kGzctummK7tW3btm3btm3btm3bttvTzSBqt5u3PXv6d0/zMsneL/feyZ2ZbMKEDRspavhEQVGsuAnDhg0TNiIyHxxFjh42TCRkP1iKET9MtERBVYQwcRIFVSnDhEsUXIUJY97/L4DK54ZtWJIX/RkMQL7t4zmOY3P3TpkoKIBuDUUAGAAEuV4ay6FiK1etrPKvgMLDOaCKgSDzMNORikNFiZc2fcz/b4AWMvSah+jj7ms4MDu3iMgFG1X/F8BrGQAEUMU2yKIdOJFTwp4xbNnq/wAYUxBA9A9h3CW18KeXVOOYO7xzwIDK4+nVY1NjSHu7MpzeCj31Awb0U+3oSeAnkIyWdgOj5MWBAlqIBEA2ynLUfGwjjxMA8EacFCAgTSsWm6CQzCNQ+0jZzBXK5gsM0L6NmkpkhMGAO2rzwWQ8ssvQA1B4ERhgT1limSrzfbV5OYv7D/ofbbjegQJQrK2AasAq7nKgIWJxIGigMt9WmwexJLk64EJggAwtVQAJh57koZIVIDwJsJvusHbTmgjAArP4CYUDBNzwIMPkA4Xy+Ywh0vzgyoXqPkipjywzRjOQBEjdclkT/THJmUvaB2SpM7Tl2HUDaJHPykEUfQhVkgfX64UsGSKIB0LDDrYBp/uLkiDwnvT9MqD9mpm1+EMgjU2Dh5qcvLkUDbBd7HqWl7A93tOiHWrZLqPoIJU6Q04bxRnts1sz2gXkG85CesHyOnS5BxuIgNovTc9bvMnggnC1mO0B54dHL83Q/Rm1HWqloAZBbqwnv7QHUEkFatoeMmcWABBqMeflEqi147CcnCKyuZYYO9oamcP5defqTtvatn3gB9BYNJZmyG4ll/y4zs2vC7KYvzf7VhNBloXct3EHaleyxerhg+vemZ3hL4DduQR68+MK7Vn2N9/3L1i7dv5U/HeJXLICIMOLXLnKvgHdJWCuMfK2RPZ0fgjLkGrFbazpEzBCJsGhG2mLPftdr0pAk7Sxpy/AJNFawxpksAUYafoi1zKLD0AfiPsQ7UlQyJXFjv3O6XnqNlQ3nt4+AIMhJDWBbITcU+0Alkj4ouiHbZXRBwBYJDTdbQewyjo8uJdl+HOIjFMdnIO2JksdSlSdO7fqfGvpLGcFKAMr20syW85UCOulZ1Fr7uamRUHG1pIVsH35HwFXZKurzfXz8tdnOZ5EzlOgtCHKFXIJ1jlG1lp/BMyYYJmkeOpox3o0VQwHeXmLPpXr2EawjqCT65gBVN8LmnNcthM9srCAJQxi68JajacAKt+AOW7GWCrEYdqBAjxp005QytBlZ6c2/kNEtdmYBel6fhqEwRL4TfIX3zmov8AHoEIZ0Xtx+CMNOaOVApkBtPn3kTjNb71IujjTCqDKP1HkcfzFpiVoY03UZhLiXSND5S2Womlsa1WwAHRledeIExVOynUxr1ZqGrLAtM6hYlviecEa0VIjRx/728Rraokb50pW16tcmlEKA/4oRqyPu4BkSUG5UK30G7uBT5UagQbjLebu7a4VGsAl4n9IEq+yAWgakfiCyNbinBLUy7VnVMpQAC4oPIp8SI2q1apVqx1CUkE//LUe6lmvCnA8aRPkrZ1Cs4yl+XMcTuzVUQewSMo9Sz1vVtYmssByklRqQuP2oZi+lxZJ74OOoiogqUvdMYlt1A3loXLJDzuyjlq1tnAo1gcZB3kdJyKAJMAqhm1YJcCHIXkne3Q7mgd/InQLCFD4kwj9ARhMaFQ9AMCJpgo24gxRNSWTAWAVW/5pqAEDRqPFNnQ4miRTlbMIBqj3gysJ7UzZknjlgt7C2Dd0gAzN3Si9IdnqViqUByk7BjjrFi9e/FQNcqPVXrR06aIaTRjAy1dCA0jznsOBT384eWIq3ItwLqbhdZszlbqzK1kIALy7cQb7gOYFsf31hbzfJ4jkBIBykdYLYBgGA1KoAAaI6/LaBQwshatPMtW+ruQIUAkBiAcUQIbpQ/YAy/ACDIYU8RpN7rV2ct68ecULqYSkLq8HahNawRo8QCq4vrMdQMYyJAG1UVSyF0d1dMp0dUf3AJo9wBKzprQBOOuG6NvZuqDAFJ/iVPccOWgOfAHUllIl/QPStGRR2XWm36l+ey/jBJACdA+cXiqkgPQhdHYtNMjoF9BdILVhNYrQ8yT42iwe6DlIjjygks/7BQxUkL+Mow+K0AqXuqN54CIAJ4QYQD0ADBrNuHp+AS04gEQAvVBV8OkBzQGV0LqtH0DKuwSQrT8qDouSmAGuTJb7ILkZUD6/H0CanxhAk9zFhQdhc5L3OVFEdA/oqg6M7+YHkIF6AJ3T0G1QV3UBWnIwLZsTGkuFvrgbP8tfDrJSgOskqg6F1qP6PGW6elfTEGWv4XBky+ZIMh2FKD1OOfJgdBV/gA0KuWtCru9Cac5TNV2vXr1q78UeJKmaGBH2FenSpUiRFLQXEUGhfCd/gOM8fZrrWJrYKAxw1ba20SQzaGxrldIfIF9ZgV6O41Ty3wAwpCjdQQfNvcjPfUDXmnQOzbjSPbQCQEiTIhaqEeDu6x9QQp/swiTj5hbKbrB11OXtv84Vj7L7AAiNOtoYD8aIjIYISdJsdY10VCv6OxEW9k9tUN1m2vlyczsDTs8CvOGhiNPhcrhcDodL3YSQtmwOo7B9CCA/pIqtIbN3ZuBPxkk21Hbc32wO+jdlQTNELw+LV+ULKo/JaHde9LY8h8dlHSKwLA+a5Rw3LmczADhFoD+uaOs/Xsza3v7MruugnJKisJJXrKIogiDnnjjyzZFiXStW7FrsyLM1k5tyCisATTzLjbuUJlRz04pnN9+bWGbs2DIT7+3YtvL46bzmW7RCj9LlcgOWlThFFFmhWavLhQP6BSRjRt9P7VLmPbCw34bN9ettG/jyQJrg/OSeZf/M7vN7VJ/xt0d7wX9pIEKioCpl8F/cCBslURAVM4H35ZnYiYIk9PIMev0nSspEwVC4eBHDhv0FtGxqU9LjY5QAAAAASUVORK5CYII=</Image>
+ <Url type="application/x-suggestions+json" method="GET" template="https://m.baidu.com/su">
+ <Param name="wd" value="{searchTerms}"/>
+ <Param name="action" value="opensearch"/>
+ <Param name="ie" value="UTF-8"/>b
+ </Url>
+ <Url type="text/html" method="GET" template="https://m.baidu.com/s">
+ <Param name="word" value="{searchTerms}"/>
+ </Url>
+ <!-- As of Bug 861164, we can do a client-side detection to determine whether a user is using
+ tablet or a phone(relative to this case).
+ Note: The order of <URL> DOES not affect the way we pick between them.
+ -->
+ <Url type="application/x-moz-tabletsearch" method="GET" template="https://m.baidu.com/s">
+ <Param name="word" value="{searchTerms}"/>
+ </Url>
+ <SearchForm>https://m.baidu.com/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/bing.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/bing.xml
new file mode 100644
index 0000000000..0b3bc82461
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/bing.xml
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Bing</ShortName>
+<Image width="32" height="32">data:image/png;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAABILAAASCwAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD+0k4S/NFTevzNUtb6yFL/98JQ//W8Tv/ytEz/76tIuO6hREf///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A/tVSNv7UVeT80VX/+81T//rIUv/3wlD/9bxO//K0TP/vq0n/76JG/++YQrjwijcS////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP/WUzb+1lby/tRW//zRVf/7zVP/+shS//fCUP/1vE7/8rRM/++rSf/uokb/75hD//COP/Lxgzp6////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD/108S/9dX8v7WVv/+1Fb//NFV//vNU//6yFL/98JQ//S8Tv/ytEz/7qtJ/+6hRf/umEP/8I4///CEO//xezfH8HEyJP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP/YVInrk0Tz310u6dxTKffjcDbn8adK9vrIUv/3wlD/9LxO//K0TP/uq0n/7qFF/+6XQv/vjT7/74M7/+96N//ucDLy7mYsWP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A64RB7N9PI//fTiH/304h/99OIf/fUCT/5XE67vfCUP/0vE7/8bNL/+6qSf/toUX/7ZZB/+6MPf/ugjn/7ng1/+xuMP/qZSz/4381vf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////APerRWHkWiz/41Qk/+NUJP/jVCT/41Qk/+NUJP/jVCT/98JPR/S7TOTxs0v/7apI/+ygRP/slkH/7Ys8/+yAN//rdjP/6Wwt/+dhKP/jWyT31NxgQP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A6nAwUudbJ//nWyf/51sn/+dbJ//nWyf/51sn/+dbJ/////8A////APGySInsqUfy7J9D/+uUP//riTr/6n41/+lzMP/maCr/410k/95THv/V212w////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDqYStF6mEr/+phK//qYSv/6mEr/+phK//qYSv/6mEr/////wD///8A////AOynRCTqnUC46ZI9/+mHOP/nezP/5XAt/+JkJv/eWB//2FAZ99bbWv/V3F8Q////AP///wD///8A////AP///wD///8A////AP///wD///8A////AO1oLUXtaC3/7Wgt/+1oLf/taC3/7Wgt/+1oLf/taC3/////AP///wD///8A////AP///wDnkDpH5oQ01uR4L//hbCn/3V8h/9hTGv/VbyHq19pV/9XbXED///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A724vRe9uL//vbi//724v/+9uL//vbi//724v/+9uL/////8A////AP///wD///8A////AP///wD///8A4HMqetxnJPLXWRv/1GMZ7dvOQv/Z2k//1ttYQP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDyczFF8nMx//JzMf/yczH/8nMx//JzMf/yczH/8nMx/////wD///8A////AP///wD///8A////AO3NGxDp0BRg5MYctt2wIv/f1jT/3NhA/9rZSv/Y2lNA////AP///wD///8A////AP///wD///8A////AP///wD///8A////APR6MkX0ejL/9Hoy//R6Mv/0ejL/9Hoy//R6Mv/0ejL/////AP///wD///8A////APbEKhDzxyWQ78of/+vOGP/n0RT/5NMh/+HVLf/e1zn/29hF/9naTjD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A9YAzRfWAM//1gDP/9YAz//WAM//1gDP/9YAz//WAM/////8A////AP///wD///8A+MMswPXFKP/xyCL/7cwc/+nQFf/l0hr/49Qm/+DWMv/d1z/g////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD3hzRF94c0//eHNP/3hzT/94c0//eHNP/3hzT/94c0/////wD///8A////APzAMiD5wi7/98Qq//TGJv/wyiD/7M4Z/+fREv/k0h//4dQr/97WN4D///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////APiNNUX4jTX/+I01//iNNf/4jTX/+I01//iNNf/4jTX/////AP///wD///8A/b8zkPvBMP/4wyz/9sUp//LII//uyx3/6s8W/+bSGP/j0yTQ4NUwEP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A+pI2RfqSNv/6kjb/+pI2//qSNv/6kjb/+pI2//qSNv////8A////AP///wD9vzTw/L8y//rBL//3wyv/9MYn//DJIP/szRr/6NET8OXSHTD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD7mDdF+5g3//uYN//7mDf/+5g3//uYN//7mDf/+5g3/////wD///8A/r42UP6+Nf/9vzP/+8Ax//nCLf/2xCn/88ck/+/LHsDrzxcQ////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////APudN0X7nTf/+503//udN//7nTf/+503//udN//7nTf/////AP///wD/vTew/r41//6+NP/9vzP/+sEv//jDLMD1xShA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A+qA4RfqgOP/6oDj/+qA4//qgOP/6oDj/+qA4//qgOP////8A////AP+9N/D/vTb//r41//2/NOD8wDFQ////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD5ozhF+aM4//mjOP/5ozj/+aM4//mjOP/5ozj/+aM4/////wD///8A/703MP+9N8D+vjZg////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////APmnOUX5pzn/+ac5//mnOf/5pzn/+ac5//mnOf/5pzn/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A+ao5RfmqOf/5qjn/+ao5//mqOf/5qjn/+ao5//mqOf////8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD5rTlF+a05//mtOf/5rTn/+a05//mtOf/5rTn/+a05/////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////APixOkX4sTr/+LE6//ixOv/4sTr/+LE6//ixOv/4sTr/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A97M6RfezOv/3szr/97M6//ezOv/3szr/97M6//ezOfH///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD3tztF97c7//e3O//3tzv/97c7//e3O//3tzv/97c6hv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////APa6O0X2ujv/9ro7//a6O//2ujv/9ro7//a6Oob///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A9b08RfW9PP/1vTz/9b08//W9O9T1vTs0////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD1wD4S9b871PW/O/H1vzp2////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A///////AH///gAf//wAD//4AAP/+AAB//gAAP/wAAB/8AwAf/AOAD/wD4A/8A/gP/APwD/wDwA/8A8Af/AOAH/wDgB/8A4A//AMAf/wDAf/8Awf//AMf//wD///8A////AP///wD///8A////AP///wH///8D////D////////8=</Image>
+<Url type="application/x-suggestions+json" template="https://www.bing.com/osjson.aspx">
+ <Param name="query" value="{searchTerms}"/>
+ <Param name="language" value="{moz:locale}"/>
+</Url>
+<!-- this is effectively x-moz-phonesearch, but search service expects a text/html entry -->
+<Url type="text/html" method="GET" template="https://www.bing.com/search">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="pc" value="MOZB" />
+ <Param name="form" value="MOZMBA" />
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://www.bing.com/search">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="pc" value="MOZA" />
+ <Param name="form" value="MOZAT" />
+</Url>
+<SearchForm>http://www.bing.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ceneje.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ceneje.xml
new file mode 100644
index 0000000000..624c8ebde4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ceneje.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>m.Ceneje.si</ShortName>
+<Description>Mobilni iskalnik Ceneje.si</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image height="16" width="16" type="image/x-icon">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAnFBMVEX6qxgAAAD6qxj6qxj6qxj6qxj////7wFL+6sX9/f391Yz7+/v6+fn4+Pjs7Ozv7+/8y2/w7/Do6Oj19fXz8/Py8vLn5ubk5OTi4uL7u0P/+vH65sH5vU/2ukz7v1D29fXy8fH14bzw3Lfq1rL704n80H36tTT6sCb+9eL93qT3z4X8xWDz7uTz693yyoH3tz/+79Tt49H95bfvx37SQQ9XAAAABXRSTlPvAJ8gMC39vXwAAAHiSURBVGje7drpasJAFIbhE+1kc4vdo0laTd1bu93/vXWkH1UIhszUr1SY9+/AeRAimQkjntduiaIkrbbniXchipZceNIWRUyPbylqLRFFDfOJOcABDnCAAxzggN8D81yXZVlRFEFdqSWQvtzf9geP171uJ4r8uhIrIB0NKoC5IDXz/QpgIUjN/ApgI8jx+RXASpCj8yuAnSDH5lcAS0GOzDcEIDQFMN9/LsvpdDIZ68K6ngfotSHgo1A1q7xHOQu4RSxg2kc0YIAyEjC5QTTgCrGAcQ/RgC4qWEAHnQ6Yz+LkAIjQyYDNSK++LRQKIxScCFjo+brVD+CjUwEB1s8XmGPZGohQFUArXzdKGQBKwnCVKmugg8aqNnugi2hAD9GABzRhAdeIBgzRlAX0EQ24RCULuEMl+xe8x3U96dbr9Xb7+eKjtCGQ+A2Kok63d3Uz7F/eDQ02vxAMAZPtOwQzwOgAAsEIMDtCQTABDA+BEAwA02MshOYA5ifLZWwAQGgCJPu9QGwAqDSoqyiKTJfnOY5m8Q4IiZ9zZjvgQyECsFjutxoMQAvJbK7Q//jiZdksTBQiAN9P90YhBhAf/BsowObg1UYBVBCnCp3pU+QABzjgzwGyIPSLG/SrJ/TLM/TrP1+lfdf1LcQ9IwAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.ceneje.si/search_new.aspx">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="FF-SearchBox" value="1" />
+</Url>
+<SearchForm>https://ceneje.si</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/coccoc.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/coccoc.xml
new file mode 100644
index 0000000000..1baa2a3118
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/coccoc.xml
@@ -0,0 +1,21 @@
+<?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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Cốc Cốc</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAC91BMVEUAAAD///////////////////////////////9ub3FOuGnwYDaExUiDxEmJxkWLxkSNx0JWumR/xEuOx0J+w0x7w015wk5zwVJ1wVFjvVyFxUeGxUeHxkZcvGBevF9au2GBxEpYu2NgvF2RyEBRuWdTuWZmvlp3wlBqv1h4wk+QyEFtv1ZuwFX+/v5lvVtovllpvllsv1f9/vxwwFRxwFP5/fpvcHKUyD77/fn4/PX7+/v3/Pj1+vD1kHPw+fPz+u3q9+3r9ufj9OTh4eHf8dbK5aOAgYNjwHhdvnbyc0329/by8vP+8/Dg8uXl893V7trK6tDD5sLAwMHO6b3V6rXL57P5wK+mpqf4sZyw2X57fH1lwXx3eHqJyFj/+/r++ffl5eXt9uH95d/q9d384Nfl8tbS0tPl8tHPz9Dg8c7a7sjd7sW65MO24sC7u7y44K6V1aShoqOv3KGbnJ6s2pf3qZO33Y94yo33pY10dXec0XSr1WqWzmhcvGWi0WLzeVWKyFSPyU+ZzEr7/vvu7u7+7+vw+Orm9erp6erx+Ofq9eHb8N3Y2NnQ7NbV1db72tDW7c3Fxcau3ran3LWztLW/46+i2q+trq/Q6K6e2KzD46rC4qO64KKQ05243puXl5nD4ZWCzZWSk5WPkJKNjY+/346o2I2g1o2124mc1IiEhYe63IX2n4WV0YOf1IC12num1XqEy3qLzXdYvHFVu2uKy2plv2VxwmCg0F2Qy1uWy1CLyEvxbkeSyUTxakLxZTz+9vP96uXP68/71svS68b6zcCZ1qn4t6TI45yM0ZyKi42FzIdwxoZqw4F8yX5wxXySz3Z8x3SVzm6f0Wr0iGdpwGSQy2OGyGOYzl/zf117xVuZzVeEx1eezlV8xFKIxkrY7tPF6M7F58vJycrR6sHX7L/6ybq2t7i94rP5w7PI5av4uaae16HF4ZeZ1JfA4JaIzYj1l3x2xXV3xnOq1XFpwW+f0W52xWtdvWvzgmGExlGTykyNx0hMjSfTAAAACHRSTlMA+qcu6Fzn6bOFBvEAAAaVSURBVGjetdplWBRBGAfwE+s9z+7u1jVOwbsDBAxUDBREMTBBKQERE0Gxu7u7u7u7u7u7Wz+4N7N77LEzG8D+P3H33vP+bmdnhrl7TqfTpXVIB5oknUNaHZv0aUCzpEnPvv/U6c848hlqJ6TVOYCyuB2bt2H7dP5Rh+OtJjKCao+tmWyZDYI46DKCgjSbdyVvhXL5898BnC6DDAaD6yFbvXkmYZyF90EHsjH7XcmTJy8C8pnw+2f7s1kHXIzthP0bMCCILGCeE5A7tw3wQM8dN+A04QdI2L9hU1ABGOcMzplTANRDz77lgC6A4tyQ0l8eCBmYpbgdMAk97Y/7HzACylRyf3nAODdzliSALyr0NgjvQW3BDGo3EpQDI35kFgF4nq7DQCCgnBD0dwflQNfdBcVANCp9Rv1/A4r7Nlv/8+6gHAhNKEQAVqCaKwJeA8p8W/9ztUE5ELK3MAlYj4rX0C3uiP5uSuovDwQlZCUCW1D1thXoByiL+f59nUE50DkhKxm4icqDEheBN6G/PGCOz0YB8qNtYB/b/xqg9OX6L3YEFUCbbDQgn9laP8gCL9ArHxP6ywMts1MBtBkxbP99HdBSPI/7nzaCCsBtDx1Am9EhFuiNXhqO+0cxoAZok10COAYAw1ngMLBxbID6n7D17zickQeCcmWXuYKOBw2u6KVTUP8ZwGU4uwAbtZIFYunA9mi8XR/1b4J2uYZ2/Y/uQwu8lQzQPRcFeBDjAfaZbe0/FXAYfwNOIxlgIwVYMUK0m1u36R6A08TVwKeDJDB6PxG45wuiMOwtDgeciY1s/Q8YJYH6FUlAwBggZORUb446aUhMP5AE4knAvWZADx4eW1w7SgKdS5GAeny57rSlnu09l0SMAkECE4enUb9AmXUwiwQs4Nsv0XNx6jkM+LQ6aOuODnvSwEYCcMOEa5Od9Inp343vj7sP8g9UtFXcJwB+uBRh7bvMp+5M7iKQgE8Yt/27KNyL3EqJgQB8Ad3Y998+GABa89fQCd/h3ifRvqQM6E4A8B2weOr1bTuBZfJC20AtAUpqO1KBlgQgFFV82EEZB8PW6AUZB6I4e0853a50O2caUJ8AuKDKGb2+F3h56oVpDcI4ju0x/2KOMmVKly6dyZsGLBIDu1GhMTswE2Ca3i5tbbtSnRbvLsSVyMEGA81pwHIxMBAvAXaELOCpt48Xqh05u6NIkWLFSigB1oqBAagQbH2/FqckAF7QX4oWTQqMp14BDZjAzlHw0icJXs47xMBYGvCeNkSd2HZPzyTp72RBN7ekGGiqYhYNZtAyaK8XZxlYU4cAuNOAWYRpiv8V9LSu3Zm97IAIVGlBAKjrIIwAzMHTyMk675kIwZX0b4wqH8RAQ6ABownAQFxi37yTD4Bl3OTgthh4idfXdTHQlwrAEMJuiv+deVl3iV6d8HBZs9TCjZAYiKIDsQRgEy6NQstsTc+ZrdEVLMMDZPxKAMLpwCICkDMU1xovFMzQ1hZAiSxAAEbSge4k4IYLV52w1Am3X1gXcOpcJwANgA4YhxCA3ANMfL1xsM80n25e/EOXywUIQJQEAGuJ56IVJiDFvLkACRgvBYQRgTwD3EAcj8tVSID0ty3MLvLZ9OYk0cmxxc6yRGAGSAEwi3a63mB/vHu0pWpZIrDNXRow7aF+Prg03YObCs0iN5evRgGiQBqA+lKfcO5cWr1y9ZadlaqXpwDoAqSBobskgHz5ataoXLmSBDAb5AAISQnQwFkegDYpAJ6AAsDlbrKB+aAEgKB/yQQuOisD4E3ygF91QCEAfZIDxI0HxQCzKhlAC1AOgLGNWiAuHMgATeijDohrDuoAAL+HKoDv3qAaYPyUA2drg0qg66qrewtmHqwM2DHFCCoB0y00i+b6BigAPgmmv8p1sAlcFvyRAS40B1ANhHEr2Xq0dpsXIAFsPsKAesBlD79VdEbj5bvhARH4GS0aHJXfm+428+SzmO32wM71kWNFt1b196ahIIi5nm9kzMrV61fGRL+a5EFtLg+M/ssDfUA+6gHmW3YOuGXWABCcKh4GgRZA1/08MBe0AEzx/MnuqlETYBF/+N07BlIS+Q8gfqAFYL7PA5sgRZH9MiRhBGgBhNXigeegBeA2hAdWgSbAxloccNcMWgAta3FArhDQBIjlgY8AmgCnOCDeBKAJAC3RTd7fFVIzunSCB51jWaA+pGbS2f9wY+ipXcsZSM04oJ+eaJc0aTX/8Qz6+U8G0CQZrD//+Q/a0ARJJQe3ZQAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET"
+ template="https://coccoc.com/composer/autocomplete">
+ <Param name="of" value="b" />
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="s" value="ff"/>
+</Url>
+<Url type="text/html" method="GET" template="https://coccoc.com/search" resultdomain="coccoc.com">
+ <Param name="query" value="{searchTerms}"/>
+ <Param name="s" value="ff"/>
+ <Param name="utm_source" value="ffmobile"/>
+</Url>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/daum-kr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/daum-kr.xml
new file mode 100644
index 0000000000..b86cb446ee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/daum-kr.xml
@@ -0,0 +1,21 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>다음</ShortName>
+<InputEncoding>utf-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAARVBMVEUAAAD5+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fn5+flNkP//1Smn3wAoffz9dG7kWlfjngNhtAAke/0gdv9isiRZrCSQTyACAAAACnRSTlMAwwbS+W9sG8/zKHP4TgAAALhJREFUaN7t2ksOwjAMRdGXfzEQoAH2v1QKE6ARIi0yQvTdmSc5Y0cGYKPxopA30WIodaJWlwDrRDFnEUW1CCOqGXhRzUOUI0CAAAECBL4D5MaO+dyPO1TtqyYA/bKBcu3+Wrn1NM0HmjvNB0peN7St2lTtHiJAgAABAgQIECBAgAABApOB1j5cY9/264s4gdfAH/x4jWYCBAiIECCwSMCLaiv9s4cgqgX90xMkRcElDNlgRCUTLHAB68OktAP7gdIAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="http://sug.search.daum.net/search_nsuggest">
+ <Param name="mod" value="fxjson" />
+ <Param name="code" value="utf_in_out" />
+ <Param name="q" value="{searchTerms}" />
+</Url>
+
+<Url type="text/html" method="GET" template="https://m.search.daum.net/search">
+ <Param name="w" value="tot"/>
+ <Param name="nil_ch" value="ffsr"/>
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://m.search.daum.net</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ddg.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ddg.xml
new file mode 100644
index 0000000000..21364b08aa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ddg.xml
@@ -0,0 +1,23 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="32" height="32">data:image/png;base64,AAABAAIAEBAAAAEAIAB5AwAAJgAAACAgAAABACAAUAcAAJ8DAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAANASURBVDiNpZNNTFsFAMd/76t9rx+UsZYCGTNsIytVsXGGA5qNmAjG0Ok+OsXNuIMas4MnT4o6g4bTYnYw7jCnS/QAZHMmbBlKluzCphlK5mAbAcamlKyUQt+jtH19Hx6MZpz93f6X3+n/g/+J8OiYTe0KObLysikqRz3F9WeVcsVTUr1rjs9z04vTr66YVxqGx4sCuBsELgj39rW1CtWevkDjzqT60mHU5icRbBvbKmOvrmKODrF669qIvZr/aFvLjXHhU5z/BHOH2p6SNkdGwq8cjUqxXVTSM6xfGcYYHESMBAkeeofgiylYnGPpu5O5tfRMsuXc5BiAeG/PHlUO+ftq33gvaksa6SMdWOlZNn1wkoYz55DDtegDp/irpx3TWCOServGH6k/PptsigKIVqPdEYi3JcXGGJkP30Lt7kHb/y4KoD3dTrj/NJ5YHEJBMn3HsMNbCTXFXyAQTg6mkMQKbq+3qwdjZACpfivSa8e4PZ1leaWA48KkZwfLWiOyK+NKGsblAZTnDyAGQnvjk3FJpGgm5Ooa9KGvkULViKEwur5OJmciCpDOLFNqqMX/hA81plL+5WeUphZESdpdVaVLslYu+SVfACuTRaxrxLOWIxGvQ7H+xJkfYq9gU9zxALPawV1SyN+YBcdFFgkthm1Briieius6indnM1ZBpzh5nWjnQTDuYM+8T26sFtv1IdYoFCZcxFAEx1yn4rJgZiVXNP3qWOnuTdTnunBWsli3fvvnIb527HA/eANomoKdFnBWdYIH3oRCAWxnKpxVHVFx7W/Kl76nqqsHwTCwp+9iAkib8FTtQ/gjwsV1l/lFG3G5SLDzINbV8wjF0pfNMzOmLD+sXDCU6WHt/lR39PMz5E8c5/dfB/lKuYSl51lOZpmvdei/bZD4+BTkFliauDaa0x9cfQxccfvoeN61jN7sD6cfSppM3bcX8W/egmWVEVSN1+V2DrsdxE6cR/BB5uwXufLC4ieJH+/nN7ZwJNEqaDWfVcef6RY6X6UYDhGoieIt2RhT49iXByjM3fnJ0fXeba0TG1v4l7HUFq1Ord9dcb29brGc0MqlgKl4zYpPvS5jnzXN/IXHh6ZWHq3xb489Z7RfaFfAAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAHF0lEQVRYhZ2XX2xT9xXHP7/r6z+x48Qhc1lCWWzKopFOqluHDW0SCZuElmpIqaASoRXwMCL2MNE1PGyTNog6qXsoLbBpMHgBNAqVyBaaqbDugaSaSlXwlq4hrNkgzhgOYEJMnFw7zvX97eHa13bsFLrzZJ/7+53v95x7/l3BY8p4Z7sv65rptCHapCSEQgCJDwBBAoOoFAxL5JAtXdMf7B9MPI5d8UjgresCSH2PVOROC/AxRMIJBXtv8OxH0f+LwHhnu4+qmX1SilfyOteaMO617VS1hFH9DShuLwCGliQTHWN+YgztyiDp65ECESEOKim9N9g/XDEiFQmMb10XkCxcAgIA1es3UbdlF6q/8fOcsUSPx5g+d5zZDwbyqqjAvqFSNMoIjG9bG5KG8UcgoPobWd7zBo6mZsvThxfOkB6NoMcn0eMxABSPF0dTM+7WdjytbRZRPR5j8rXd+XNRoSgvBN++MrwkgWLPves3sWzHqyhuL3o8xtztKE6HE0NLMndlCC0yiDGXrBiB6rZN1G02I2ZoSeJHetGuDlaMRAmBm1vD40DA3drO8p43AHh44Qwzl87je/nHiIYmHLXLcDgc6PEYU6fezBsuE8Xtxbelm9qOLgDuH+0lOTRgkkhnn83nhM3yvuu5t0B8T/U38uWfHkbYnUz3HWP6zG8wZh6g/fU9tEvnma+px/nkKhw1dVR/ayN2fyOZiTEMrTQaciFD6pPLIKCqJYyrJczc5b9gaEmftNtch0dif7YikAv9OMDKw++i+ht5eOEMD04dsAw6NnRS/+IPcdXVV/S4yMMyWba9h9qOLjITY9z+ybZc6O3B4NmPogqAwcI+MLNd9Teix2Mk+o4VwDteouEHP1sS3AR5dckqSZw7hh6P4Whqpnr9JgAMufAKgDK+M+QTsBOgbssuAKb7jhcSrO4Jlm/7EZ/+8w7vf/AZs9p8GcC/J6a4PDrFQkd3RQKGliR+tJdiDKGwY7wz5FOzaaVTwWwyee9ni0Jp+8Z3UVUVj8fJxpbKHq5uqmd1Uz2GVs+tPm/F6kiPRjC0JKq/EVdLmPRoxJd12TsVgWgDcK9tB2Du6lDJRfvK1RbIrDbPnXjl0vvtqQ+5PHof15pwxecAD987A4C71cSyYbSpQhBCgjPXbMrKyuWxfvb8coDqKie9PRupdjtLjn0YiQLwNX/DkgTyLbqqxSQphQip5NqtI2ASyEyMlVxayHU7gG+HAwBl4AC/P2Rm9/S5EQAUF1Q/DVUrIX4RjDTo8UkAVIukDKj5CWcNlkXvT0/OWL+3b24tPMgmYepPoJUS9j75Gd5uUGtyXt8ywYFC685hIfGpZa4sljtRMpkMDoejVD/6EmRiZcdVOxiGCZwcgdlrn29eRZBA4jO0JIrba1VCXsTkBKlUqpRAaswCT92Ce+dNL9UaMDIFjxdL8fg2jZNQgCiAfm/x+8nJ5DjzD6dLdY4GsJnGbC5QcimhzywNDlhTNRPNvzYRVZF8AoRS1yM4As241oRJj0ZKLmo3rsFXAgWFzQvNv4Obe3H4Y6zcBZl4KbhaC5l7cLe/oMuX+nwu0YWUw4qBHIRC+dU+31XG3Bj5GF3XS5XuZvj6u+iebpLXAGmCqrUmkeSImf3F4mltoxgL5JBqcxn9ct72Vno04tPjseJOZV0Uk1Hm5uaora0tI5ccgcSFMnWZFM8Zy3ba6FeCJ4YTEk6COQMA/Lv3oXi8hdvj10gnpioati8aQP9qsPHfeqVEp3i8JXMGQMCJYP9wQgFQZPYgwOzQAJnoGKq/kbrNpYMldfM6ALMLST598Hfm9FkAXC3PMeVV0JyCP3zTzq87HFwMlVZ33ebuCnMm2wugAgTPDkdvdIUPCcmeu2/uZcWvTlPT0YUxl2Q6N5aNkY9JrfsOF28PcHL8CABP1z7DvfQd4i86MbIGQhEIKIlA3eZu05aWZPK13QBIyaFV7wxHAayTijO7H4jmVy0A35Zu6rf3oHi8iMko6XRpjf1j6m/c0XI9Q0BWzwLwoFqgeLzUb+/Bt8WMZPxIr7WcPvVOxFr1LQLBE8MJQXYDEJ0dGuDugb0YWpKaji5WvH6a6pWrSE1P8VTNV5FSmlExDOy6HbtuJ/CfecLX01TfT7HM/iVWvH7a8jx+tHgpzW4oduKRa3nDz48W1mxdJy1TvPz+9/HYa9gb+gXPPrEWMLcegJrnu6yOl5kY4+6BvUVrefaF4NvDS6/lFomtoYDEVvgwKVqzwUzEaru30lWTaDzGdN/x4sUmKshuCJ413/sjCeTlRlf4oJDsyf93tYRxt7bjbGrGEWgu6e36vUlS1yNoVwdLeoiUHFLms/u/0KdZseSisR/Y8aizRVYT0uCkIrIHK3n9hQhYRDpDvqxL6VQQ7QjxDMjSz3NEVEgxnCU7ZEsb/Ut5vFj+B4mSK5j/ya9iAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ac.duckduckgo.com/ac/">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<!-- this is effectively x-moz-phonesearch, but search service expects a text/html entry -->
+<Url type="text/html" method="GET" template="https://duckduckgo.com/">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="t" value="fpas" />
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://duckduckgo.com/">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="t" value="fpas" />
+</Url>
+<SearchForm>https://duckduckgo.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-at.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-at.xml
new file mode 100644
index 0000000000..31ac7f6b9b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-at.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.at/autosug?sId=16&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.at/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="5221-53469-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.at/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-au.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-au.xml
new file mode 100644
index 0000000000..490b34de9e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-au.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.au/autosug?sId=15&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.com.au/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="705-53470-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.com.au/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-befr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-befr.xml
new file mode 100644
index 0000000000..db76954be3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-befr.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.befr.ebay.be/autosug?sId=23&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.befr.ebay.be/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="1553-53471-19255-0"/>
+</Url>
+<SearchForm>https://www.befr.ebay.be/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ca.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ca.xml
new file mode 100644
index 0000000000..7cf4fc531e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ca.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.ca/autosug?sId=2&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.ca/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="706-53473-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.ca/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ch.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ch.xml
new file mode 100644
index 0000000000..7557e24c6c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ch.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.ch/autosug?sId=193&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.ch/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="5222-53480-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.ch/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-co-uk.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-co-uk.xml
new file mode 100644
index 0000000000..06e8f22820
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-co-uk.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.co.uk/autosug?sId=3&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.co.uk/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="710-53481-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.co.uk/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-de.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-de.xml
new file mode 100644
index 0000000000..8e05c24fe8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-de.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.de/autosug?sId=77&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.de/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="707-53477-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.de/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-es.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-es.xml
new file mode 100644
index 0000000000..6e7d644d1d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-es.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.es/autosug?sId=186&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.es/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="1185-53479-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.es/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-fr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-fr.xml
new file mode 100644
index 0000000000..a060af1ca4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-fr.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.fr/autosug?sId=71&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.fr/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="709-53476-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.fr/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ie.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ie.xml
new file mode 100644
index 0000000000..2397a28e79
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-ie.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.ie/autosug?sId=205&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.ie/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="5282-53468-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.ie/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-it.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-it.xml
new file mode 100644
index 0000000000..8fe7c4313b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-it.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.it/autosug?sId=101&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.it/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="724-53478-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.it/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-nl.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-nl.xml
new file mode 100644
index 0000000000..f265d912a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-nl.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.nl/autosug?sId=146&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.nl/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="1346-53482-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.nl/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-pl.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-pl.xml
new file mode 100644
index 0000000000..66ec476113
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay-pl.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="text/html" method="GET" template="https://www.ebay.pl/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="4908-226936-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.pl/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay.xml
new file mode 100644
index 0000000000..67c9251ef5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ebay.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>eBay</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="68" height="68">data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QB+RXhpZgAASUkqAAgAAAACADEBAgAHAAAAJgAAAGmHBAABAAAALgAAAAAAAABHb29nbGUAAAMAAJAHAAQAAAAwMjIwAaADAAEAAAABAAAABaAEAAEAAABYAAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAP/bAIQAAwICCw0LCwMJCgsJCQgKCAgKDQgLCQgJCAoICwoKCQgICAoICgoJCAgJDgkNCwgICAoVFQsIDAwYDQsYCAsKFAEDBAQGBQYKBgYKEA4LDg8VEBISEg4RDxANDRIQEQ8NDg4QDQ0UDhIOEQ8PFA0NDxAOEhIQEA8OEA4ODQ4QDRQQ/8AAEQgARABEAwERAAIRAQMRAf/EABsAAQADAQADAAAAAAAAAAAAAAAGBwgFAwQJ/8QALxAAAgEDAgUCBgEFAQAAAAAAAQIDAAQRBRIGBxMhIjEyCBRBQlFiYRUjM1JxFv/EABwBAQADAQEBAQEAAAAAAAAAAAAFBgcECAMCAf/EADgRAAEDAgQEBAMDDQAAAAAAAAEAAhEDBAUSITEGQVFhEyJxgRSRoTJCsQcWIyQzUnKCosHC0fD/2gAMAwEAAhEDEQA/APqnREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoirrSuettJqc3LJVm+btYhO5ZUEBUpC/i/ULFsSp2MY7g9+wqcqYPXp2LMQJHhuMDXWZI1ER908+ii2YjSfdOsxOdok6aRod57qxag1KJREoiURKIlEVIc1PiCljvE5e6Xai91B1DydR+nBaoRuDSkd2IXazDcmFdfJi6oYyvdubUFGk2XfQeq0DB+GqNaydieJVjStwYECX1DMQ0eugMHUHQAEiO8U84tbsEGu6nY2VxZhkWVtNkmEsAchQzCbs3cgDsBuIBdM5r4vuLmgM1VoLe06fNSdngeBYu/4bD7iqytByiq1uV8awCzbTXrGwKomHnBs4nu+JbGB9Qk1C0torOKMiPqvcWtjIpldv8UcaI5kYg7dhzjBYb1b2tO+4aty+oGUw4vc4iYbmeNANySQAF5qvjWwziG5oPpk1R5Moj7QDee0aTPTXZXPxnzf4jsozxPeafYXFlHhplsZZ+vAn1d3csCF+50hcD1O0AsK9aYZgt88W9CvUbUOxe1uVx6ADXXlJHuVJ3N9ido01qtJjmDcNJzNHUz/AGH0VnaZz2jns7fiWwie4+cyEUggwsh2SJOV3BXjk8cA4ONwbGGrIeMb274eqC1Zbuq1y6AGzljSHFwGgMiJjnMQVe8Bp0MVp/EeIGU4kzE+gE7iNfpMhezqfEmoxIdTlhtnjQbnWJpBIqj1OSSO31IDfnBqk3mK8S4fRdeXFCg6m0S5rHOD2t5mSY05wCrBRtMLuHijTqVA46AkCCeW2uvLZcjl7z4M90eH5olh3qzQMGJ6m3J2kEYyUDdwfchFffh7jMYrcCi+mGBwJYZ3g7HvAO3Qqr3dF9pcvtqu459RuD7j/Sn/AB9xgttbya0/fpr4rnG927Rpn9mIyQDgZP0q7YtiLMOtKly/XKNB+846NHufluvg9+RpK8XLniKWe3j1ieNYmmBdVQk4jPsYk/Vx5f8AGH81+MHvK17aMuK7Awu1ABny/dOvUa+hC/lNxc2Ssg3vDr/+lvdLe+l0uS8XfBND08zq6wFIMyDG19jgY9ZLfZ69q4iw/GOaXFs7Hrtp/wB0XoendU/zXt6zLdtdtMw5rp8hBdLvLrIzAns+dlPeb/K35e1efVOIr75WXETK8ccpm3keCxRo0j49TsU4VSxwFJrruKGRh8WsY/FVzBMY+Lu2tw7C6PijzAhzhljmXOdlHQSdSQBqQqH5CanZ2PEYgadntZraKK2nuk+XbN5bW88LSI4Uxb1JjXcB71zjca2+2sqlbhGgy3l2Ul22pbnfy7TPoF56x/ES/jG7q3bWse4hhAOZrXhjGmHTBnLvO5W4uefGkFtp91qVyyiM288aqxGZ3kRlSFFJ82kJxgfQk9gCaomEWlW6vKVOiDOYH+EAyXHsN1KYhcU6Fu99U6ZT76bD1WVPg7e6Syg4e6ptY764mukcjDmJ1WJdpOCFeSJym0jJkByQRVF/K7j1zccV2+C2dbwWmm3O8RJcS85QT2AaIM5jB2hSnAWHU6OC1L6uzOc5LWmYDYaNR83emq0bxxwSsVvJeSXt0x2Mqq83jK7DCoU2+SsfcoPtz+KzXiPh6nh+HVq9fELknKQGureV7iIDS2NQTuOk8ldMNxF1xcsp07ekNdwzVo5mZ0I5Hqq617hJjp9vxjB2uLCSRyR6mISs2T+ei/lj/Vnqu4bh7zw7bYjb6VaLnH+XxCf6Tr6ZuqguKmfrznjcAfgF0Nd4l/qtxZ8Px5FuiLd3WM+LY84yf0z01YfdMT9tWq7vRxLd21pS/ZACpU7Hm32+wO7p5Kql3jODRtuVo6OMACMDAAAAHYAD0AH0A/FbGAAIGykFDeZPJyyvlFvfW6zbMhGy0csYbGQkqMrhWIGU3YOBkHFc9a3p1hFQSp3CscvsKeX2dUtncaFp6S1wInoYkcioVonwf6VHIuoNA9w0fsF3NNcIuO+Ok77GX08XVh/FczcPoNMxPqSVP3HG+L1qZpNqBgO+RjWE+7RM9wQpDrPw6aZLPPxBPZpNPexiCcytK6uiiIKBEZOnGyCOLbJEikFMgjJNW6ljl9Ro06FOqWsYZaAAIOvOJI8xkExrssuqYVa1Kr6z2AucIMkmRpymOQ1AlQ2w+CDRlkW8NtJKI/ZHPcTywp+ojaTyT08HLDt3BqVfxbibmFoqATuQxoJ9wN+4XC3h6xDgSwmNgXEgexKtrXuAreVVtJIl2xALHs/tmNRgBU24wgAHh6dvTtWX4zgVljIAvqecjYyQ4Tv5gZ15g6K52V/Xsj+gdA6cu2h0XP07lRaqersaRsFQZXeQqGGDtycKcfcBn+RUTbcH4XQdn8MvdBAL3udAIg5ZMAxzAkciF2Vcau6gy5g0TPlAEkdYGvovDxZJHZ2UqwxF1CuscYDy9R5cgKQdzFCSS36g1016NvgeGPp2lI5QDlZ5ny53LWXRJk9pURe3dSu41aplx7D0GwUZ+G7l8YLb+pSLia7w5DDDJEuekhB7gt3cj9wPtqL4Mwg2Nn4tQRUqamdw0fZH+R9Y5Lgt6eVsncq3K0BdSURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlESiJREoiURKIlEX/9k=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://autosug.ebay.com/autosug?sId=0&amp;fmt=osr&amp;kwd={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.ebay.com/sch/">
+ <Param name="kw" value="{searchTerms}"/>
+ <Param name="toolid" value="20004"/>
+ <Param name="campid" value="5338791379"/>
+ <Param name="mkevt" value="1"/>
+ <Param name="mkcid" value="1"/>
+ <Param name="mkrid" value="711-53200-19255-0"/>
+</Url>
+<SearchForm>https://www.ebay.com/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ecosia.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ecosia.xml
new file mode 100644
index 0000000000..81d2aa4856
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/ecosia.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Ecosia</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAUVBMVEX////z8+7x8evw8Ozw8Orw8Ovw8Ovw8Ovx8evw8Ovw8Ovx8ez///vw8Ozz8+zw8Ozw8Ozw8OuSxJOXx5bf6NsAgAkHgw9vtHLj695grWP///8j4WLWAAAAEXRSTlMCGFuSutvt+2zM9I8HeidRuGeLDAwAAAABYktHRACIBR1IAAAAB3RJTUUH5gkHFxQIpRLiZQAAARFJREFUSMelltmawiAMhQNd2LpFnVHf/0XVLgqFImfmXPbLX0ISkhDtJWRVN61SbVNXUtAXCW04kNE5RlSWI1l3hHQ6YT4jfZeyHwwfygyx/Wg5Izvu7aXirJQO7fX6+RTrvCIBMa7/v/zE+t3O8LwaNv9zANv3zbt3fLIAmy26PZcB3K/5taWAXXJecSnAbnfAV2A+QnM5MCfDIIB5esQIwIJkCrjePAWA9GPkAYdF6KjGgIkaDGiozV36HgEtKQxQKGBxl+BLw2GFE5cujYunAJB48cHljT8g+ImSKwfc39oM3MjwVgk3Y7zdfwbKOSEvBf8YWfhQxMcuPthfOXfQ6jAj2HKyMNJNy/ozucT68wBb1oA/nm9/dAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wOS0wN1QyMzoyMDowOCswMDowMDZRlR8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDktMDdUMjM6MjA6MDgrMDA6MDBHDC2jAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIyLTA5LTA3VDIzOjIwOjA4KzAwOjAwEBkMfAAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ac.ecosia.org/autocomplete">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<Url type="text/html" method="GET" template="https://www.ecosia.org/search">
+ <Param name="tt" value="813cf1dd"/>
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.ecosia.org/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/faclair-beag.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/faclair-beag.xml
new file mode 100644
index 0000000000..a2fcef2fe2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/faclair-beag.xml
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Am Faclair Beag</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA+VBMVEUAAAD////w8PDr6+vt7e3z8/P09PQFBQXn5+f29vYNDQ0JCQkRERHj4+Pf398wMDAYGBgnJycVFRUhISH4+PgeHh41NTUbGxvU1NSZmZlAQED7+/vHx8fMzMwqKiojIyPX19e0tLTc3NxUVFSqqqotLS2np6ecnJzExMRJSUmvr69jY2NaWlqNjY1MTExGRkY7Ozs4ODiHh4dxcXFeXl6KioqBgYHR0dGxsbGjo6OgoKCEhIR3d3dXV1dRUVFDQ0OWlpbZ2dnBwcG8vLyTk5OQkJB+fn5ubm5qamrOzs65ubm+vr50dHR5eXmsrKxOTk7JyclnZ2d7e3uQnfwVAAAMA0lEQVRo3r2ah3bqSAxAHfduDNiY3nvvvYQA6W3//2NWM2MTCE5edk92dYIfsT260hRJMy/U1e9JPO5zEwD/rRAAnauVJjeZb988M4+nJfJFy84eHqelcmlSKEeJbDbRQlI6BzzkU5FgJGQP+otCMjpJOjknHi6sYkaq2mq3Xt9ryYTXQCw9v99WbSO1TF6xlX4sv9CSDSsSDATUo8hqPw4WeYDcfGnLFBZBCRZToZBZ7C4mdUOBO7IiKFTASNt35cL2/q1020kHZEsQBCpoG6YlC0G70QlQn8V8LuhHD8oXz4FkVJXzO0owEEqnAwKFqKD4+ABYPu2V9CjsAYYnqnya+TYXqD9KOip6AJn6T8SYSgSwUaj/RIRGgsaATIr6b0TNOxjA2NR/IUGZUjMYUDGEX9cumPVDirLiGJDsquovI5RGzYma+SkZZCdlBX4XEOnfxxOxFixmDJip6u92T3ejS29Lql3JEQ96iv97kc4qv8rHgv9QvznIsnGngb4VCGAn+06C5S5xref0xMIICj/v/Eg1mqM5vQ9frfSQAGZ+gGDfoZGwXCY7bP+wD9VOfZhgaVpfoIBldu4JoKD6xLqnDINEZBiaY7JPrcif1cvdvx41MInWnsmNvBuLshcApfooikg/632S/cCfhjZw+yByNM3Q4kggnbB3AYnPbeV8gmbBU/TDEjfE8TD9vfpWL8OwNAv29NwuDw11ArhRP9n/WuFAN40hR0Zy8I36YGyfBD9BGGYf9PQYB98uEtZZmuZAM48uYBMBZJ6+7vzYMCnCa0iudyeexsIko8ln+mNZBgE4jgDAMNxUaypfRbX3hIbUI/sTT6GjIlloiz6zqFMDpaAeADwHCNbtIm2n+s/8zh46HwMYcbJST54IpkbG4HQKdgssR7NSWJIQ49hJoqg3/QCCPa+A+UT0va2cPqOKDAaUYsLHOi+z0D8So+liGEFoJAyasrmez3q0FltQjwG0WGhEhLPETZkEsDePN0NNESanXrrLt+rTa0aSeAIAgha9XGvmRscu0hyv1QYh5ZN3lBkmHoQ8cGCkI32FlCygCqlVFiXSRyB682KpxWYajWzn6etCHpR8FmVNpun4mJPzSR7UVV6Plcd8DO7jacQm8p80mE83Is2A9fTb/NUSfGbv6zPJB3zPXRrtLAsTSJsLJyHvnqweOveX+inYTq9ZsJ+TxMLKP1DJ9sCtTZkBJkQ2DA2qxvnT2maCtDA0G7XOGheH9wzq+zD92DeUL0J3sOsVv0NsQmzLMSCFU1WBGegBv146Z01XMx3NMIlO9GLK1xEk7wHIFL+9RnEz93T2TlNDMa/WOYvl84SI1Iczw476TfjORz1ADa1vNYqjf2V99tZfOkyrSezkTqpfE8F8Lq5HV98motfkEeAUUYy4QROenZzXeQOHkxKtk6RVf9E5pJ6tNf6Qrbu7jQfQEODuGmbLBaD6Fk7UlY9BXzhgPcvzN7ehP6f/kgfIgdJgFOcwOls8X0yVkwUgV1/w0uWZsvHnDJo2px4gC7OoMaZxkvwEMPuto37rdsvzLExOvRf4QYERStU8QClIBcsMkZf0FxsReb3XJRyaKs8/21GEHj1AwoIyjCaAmr/zsvGUwHmOvS68Kn4vWFYgEghYoWPGVGOaBxCLVD/HEkBm5GNesHNb0yQOVrWefSr6JeXOXbPZ3I/m+81zLCgQkxrO0YOQGhXdpMck6p9ytBKM9baihPKb+NbrCpfGp1p/PTI0L8XhJ8ze9KoqLjSqYw+QtNYJUO2WHZVGUP7QHqneNhMcTgxMprSUL7QHjPeSQ4d5iecl+MCVrhxsAUbPPALuzXqGBvvd9H4zb6TSRaOYTtmdavMeZWgU8fRs37w0vtHLanxY4ngskGVxRizEINYZGQ+g2T0NbAcbSY0iJgvlWjZbftkmxiI0RblfvO+lLpdSv+BA16Dug2tYInUCINhpUVCM5Ec03WssqXVFPBbwDdsCH3Jhc6WlerkRKIu4/MAi8aRQIAWbPgxSqeMgr2TIlUREkqWQt0TI7fvFZWSI3N2IgEcvgnYixwZcLSUYHoAzKIjKxGyMR/4SADSHB8xNQ75cRiOHNMEAyesefAETswZlsy4gm6bmOgKQx14DrB6VeCIpbM6lW9YZFjdwCbxnO40JDzZVvXIBS4GaEw+IdiTEIuz/uGdf6FcbWc3Vx3uCqw+NIbcBIBw8AKSr1gODB8ezBtS7HPpmELnQ397B5HVfRlMUzx9SAhIAS29jwp0HgLWv1O9hLR3bAAL9E+e0wiImX/T+7SM2H96gnYfsbL+ZzvbNQg4tVTIH4XOzpl7DHx5Qcj0phcEKzlsxUjwu3syXpnw5eUoZFqvnmEx0ZRimBQdJoZBdb44ZzGCRK1OLKtIeQECE9s7hwfRwOB6GS5i7L/XtyxMAuV12GA7UxyWnOaie1nOqucdlAxmLISV7Y8CnSFVpNfZvNA/ucRAWEoc2GH8hxmErIi/DYqXZClyU2jsdGQ9CV6rUkhEJgLFcQ4XQ6/Po6fbpsPirvvbRDimtpqOdbZjJLtZ+SS1W0xgsdClAGVEXkIyc5Dl0CCjLsu85XAz22Dya6MleR/ZPk4sMLsVZbSTA+5ILCPzsCKK+ZfDWTZuuji3kTwHqr2u8u+YqbUo223ECcH4EEO4yYQ6tlOThI6l2eoNzD4Y6gwlDmVKsljvIL8GfJPCRFkbLBDYa8ke4eBGH54AeBtDgAIgXKsrqnw+ABhMa6adzTUP40D/h6PO9pzDXcf0ZDQAs7a3kB5USvlWvVqfMFY/UT/rWSZE64zh2dr43GAFAZPU+XjEzF6CHBOW7Amq9z8R56Hzx8dk+LYI3Og6aZ6aURQhHbKKNi/DoMZqu1a+t7y7eJAl6h0lu2qeOpocax0BMW5+tg0eGAcAsjQDpuQd46X8FUIznCouDoJitn3WGBSkKz6n86RAccijYiQsB79DdLgJpf3k2ORElnNIys7VyRr7N4EMGLgfKjhKa4Fi0rZI+fDkCQr7qA7FdDmURVLDcpc8frrZk98lqJ9lOqOJjoPGdTHAlD0D7AIRgdfQo0tjj5EVKS08h5mAALT7WVUXATVIlEZUNz4pbcxw9cKxL9a1pBt7Gsf2hcTFEjQSOOCTpJ+e3jYCiqNbTNd7GF4/7Dw8wjnyudVfTHOOdeD34FNONN1yfuTmZZnLR+uB2lkGD8tA9zuMbD1A6jxVyq5zBg4W6x9n5BU4402NYDCAIjtE0gnurfuylMx7g/CTIekoQ98H6zC4f8Vvl+YoIBBACIKUImM87y5NIonmAQ+B0E7zLodFDCDgksPzXeD7BiN4oY+1wQXtDZ3Dirj32AL0PNUp+orGkNGCdnvFVCGlVMMArY4l+KDBP9VNWwQNU8kVXk/CeFN2Nwji6tL6MgXbWO1clnYTqO5q/Pi8w0w8egHf67pNBDk9Nms3sViHlm+wwRfrxSB2F0+9cLZ6xVy4gfsW6sWKZ4XC9qZVXIeHb9FYt5fAsIwiwCOyH/jmT7r3nwfWKaMs7KCrzbOLZvrD+sva9xurdkRC1x8bnNubEA9Tdw4sxx0BS2c6rPznPT7VGSZiaeLDRKZbxuY2yPC60EJkZSahHx81BR6V+JoH8YULDxhIk57f1b+kuIIk1rhMSrRWWAeofiFBc1LaV7cso79fMDruAGQLYDxJfIbvIf4RI21077d+ja8YFRGX4JQthsCVQvynFEkcAG5myJmF9lKZ+WWI3rgcC9b6t1WXqt0XeEECtEbtbWr+uXolYIwIQNTh0/X2Ru+uFl5PvFOr3RViXK15VMfWWoeBLEv7d7DIrV3EXIJXbKhn3uqUijaqn2q1fDDWdNs10RD5DKcoFOaB4NslyxPtvLsSpDbATB6e/ohQ5ko+YAVWlAkUBXlWUUTaq6xrjZA+Ddcow4JEaMs327XOj3VVlRZaFYAR44Om8C0ZFIoJcfNps3ggAC50s24JMDa+0cmBZf89Ma5v3vPk+stXWyi7uWO7KFU53rmub8szBv8V5rXxYjOb16C5WrFdtc9Yy26FDs5i6E9FjAiDfcqVptDm+Oheo2eM8w4bxG38WKJJR5Gb0sf5//vHMfyx/A9K6KzQkP80/AAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://www.faclair.com/m" resultdomain="faclair.com">
+ <Param name="txtSearch" value="{searchTerms}" />
+</Url>
+<SearchForm>https://www.faclair.com/m</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-b-1-m.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-b-1-m.xml
new file mode 100644
index 0000000000..ed4ee58ad3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-b-1-m.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Google</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAB71BMVEUAAAD29vb29vb29vb39/f////39/f19fXqQzU0qFNChfT7vAX09PRjmvSVufXqSDr19PTywr41qVRVtnDrT0Lv8/D06unviYHufnQ7q1nrW07rV0rqRTZ4p/Xz9PPj7ub36sTvlo93w4xywYdnvH5euXZIsGQ/rVz6xSr7wRj6vQrk6/XR3/WnxfVUkfXg6fSQtvR+q/RHifPq8ez04d/N5dPE4svyx8Op17bxsq2X0KaAx5Nrv4FNsWbsa2BDrl9Brl/rSz77wBPo7fW90/W4zvWwyvWHsfVclvRMjPRFh/T18O/17+718+z06ejf7OLz4+Lb6+D1793V6Nr27tbz1NHzzMhipsi938a33cLyu7al1bLxt7L25K/25K6g067wrqjxrKaOzJ/woJqFyJj435X43Iv42oT42Xxju3rugnlPs2rtcmjtcGacwWXtZ1v50FnsYVXsXFD5zU/2sEvrU0b5vDz5xzb6xzPt8PXX4vXH2PVrn/RpnfT06+tMjurf7eNTleH03dzz2tjz2NZbnday2rzxwLxnrbid0qtosqer1KXwp6HwpJ6IyZnvn5jxspdltZRdtIPueW9yu26Hvm1etmuzwVjsY1f0oE/xj0/we0vGwkjua0bsW0DqTzrXwDX5wCnkvyX6wB3wvhYYaN+hAAAAB3RSTlMA8si8ZBhlc+JuAAAAA9xJREFUaN7dmmdT2zAchw2EysYuhCSQEDYkhNFC2Xu3UEYZpWxaZhezUKB77733Hh+0oclR21L0lzA+7vq8yyX6PbEkS7IlwY8lPAzBKNgHgLBwi7BBZARHOKckItIviGAP55dECMIuI+XhH1mEcKCoQUO4EIZMJUxAJIA/z1NA4M6HDbBAAfOBUoDAALCAs3bgwoKxfNgggPm8hp1vA/MERuuHHCOw5l8tn3YtvE+MSXIkLrnSx0pYDQJT/u305SRRi9s5ABiY2yB+ckkkkjgdDxaGBR1PHGJIGtNBhUCvIGtlokhlbYJSSbCgZ0EEeRUPCCj59xwiA+5+mkGg5KeLbMRUEg1QI1udIisxt7bQi6yz7PmxW+mmLkP5sOAZTz4owJs4NokY5lipq3M3suYrAQEp/6aDEO4c6wl82zXiXGP6/0rIKprH4pcnOrV3uJuSD7bBCDbmPLZivWwyBstnFXTqx5864ujf72bIRwKhCZ7r8l0d5LJdA+DcRryCQzmrmj7ktCIDEATHZXn967/8eSCfX5Aty/KvT2KQlXi0zYI0eYPcD0FBuaF4hSA4JQdY/7aRP4sMggvOy0F+fBHFpJJtFxyWN2n6KLqQYYH+Ljgqq1gtN5qvYFdwUi1owrvobioX4So6oxacRRhRVPbDgnNqwQlewV5YkK0WHOMV7IMFOWpBGq+gFRbkqgVHeAWFZguSza6iZLMbudDsbtpq9o0WDQiAoQIWtCC+wW6xDCsQradNLbiEDXa04fr3aykTQTS3qwUFPBPO91pJsnVDggPq/HykhzJlvkuQ/GRAgmRdL8UFCnnSfyMFKKPn76EPpkqoZcvPt1KQuWLoAvAmgBden+ulTTwplPwWTf4VhEFeOi7aJBXVSugKKiLUELz4fSRpyYwLlZ+vyW9rZlv8orhancHbTe6hReTpDH4AGZZ0JIzj1ZQ6M6jNb89jFiCPpMdeoa2n61U+yfYQGOhCPwT2NkgY9dV3U4NVWDruCXSDl9dUd/EFRH4IJBqGbBIJ35z3tD1L9V3tHcqSSKE9iFdJbCTcp7QwTYAyJEZe/O1LlxVeQQqzwX4jKupgHuIVIKWa1dAwSMyHX0hNsRq8BaQ/yPBKbaieKf9pCimf6aVgqgeO9w0jSj4gQEpFFpBf04sAAUDcgwZKfGYpVF5AIMWjXnK6LaMPKMr8crxvym7T38I1Famhu/gWXu8Xl1Vl+sehBJsvy14zM1pKm0gV0zco/t89HGT+Npf5G3WGtxp3vA38GN/u3fkNa8DAVcD0QwPmH3uw8BTl/5HF9KMn8OEZfjSHZ0w//vMHVqViODGkXAcAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?client=firefox&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <Param name="oe" value="utf-8"/>
+ <Param name="client" value="firefox-b-1-m"/>
+</Url>
+<SearchForm>https://www.google.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-b-m.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-b-m.xml
new file mode 100644
index 0000000000..232bbce214
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-b-m.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Google</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAB71BMVEUAAAD29vb29vb29vb39/f////39/f19fXqQzU0qFNChfT7vAX09PRjmvSVufXqSDr19PTywr41qVRVtnDrT0Lv8/D06unviYHufnQ7q1nrW07rV0rqRTZ4p/Xz9PPj7ub36sTvlo93w4xywYdnvH5euXZIsGQ/rVz6xSr7wRj6vQrk6/XR3/WnxfVUkfXg6fSQtvR+q/RHifPq8ez04d/N5dPE4svyx8Op17bxsq2X0KaAx5Nrv4FNsWbsa2BDrl9Brl/rSz77wBPo7fW90/W4zvWwyvWHsfVclvRMjPRFh/T18O/17+718+z06ejf7OLz4+Lb6+D1793V6Nr27tbz1NHzzMhipsi938a33cLyu7al1bLxt7L25K/25K6g067wrqjxrKaOzJ/woJqFyJj435X43Iv42oT42Xxju3rugnlPs2rtcmjtcGacwWXtZ1v50FnsYVXsXFD5zU/2sEvrU0b5vDz5xzb6xzPt8PXX4vXH2PVrn/RpnfT06+tMjurf7eNTleH03dzz2tjz2NZbnday2rzxwLxnrbid0qtosqer1KXwp6HwpJ6IyZnvn5jxspdltZRdtIPueW9yu26Hvm1etmuzwVjsY1f0oE/xj0/we0vGwkjua0bsW0DqTzrXwDX5wCnkvyX6wB3wvhYYaN+hAAAAB3RSTlMA8si8ZBhlc+JuAAAAA9xJREFUaN7dmmdT2zAchw2EysYuhCSQEDYkhNFC2Xu3UEYZpWxaZhezUKB77733Hh+0oclR21L0lzA+7vq8yyX6PbEkS7IlwY8lPAzBKNgHgLBwi7BBZARHOKckItIviGAP55dECMIuI+XhH1mEcKCoQUO4EIZMJUxAJIA/z1NA4M6HDbBAAfOBUoDAALCAs3bgwoKxfNgggPm8hp1vA/MERuuHHCOw5l8tn3YtvE+MSXIkLrnSx0pYDQJT/u305SRRi9s5ABiY2yB+ckkkkjgdDxaGBR1PHGJIGtNBhUCvIGtlokhlbYJSSbCgZ0EEeRUPCCj59xwiA+5+mkGg5KeLbMRUEg1QI1udIisxt7bQi6yz7PmxW+mmLkP5sOAZTz4owJs4NokY5lipq3M3suYrAQEp/6aDEO4c6wl82zXiXGP6/0rIKprH4pcnOrV3uJuSD7bBCDbmPLZivWwyBstnFXTqx5864ujf72bIRwKhCZ7r8l0d5LJdA+DcRryCQzmrmj7ktCIDEATHZXn967/8eSCfX5Aty/KvT2KQlXi0zYI0eYPcD0FBuaF4hSA4JQdY/7aRP4sMggvOy0F+fBHFpJJtFxyWN2n6KLqQYYH+Ljgqq1gtN5qvYFdwUi1owrvobioX4So6oxacRRhRVPbDgnNqwQlewV5YkK0WHOMV7IMFOWpBGq+gFRbkqgVHeAWFZguSza6iZLMbudDsbtpq9o0WDQiAoQIWtCC+wW6xDCsQradNLbiEDXa04fr3aykTQTS3qwUFPBPO91pJsnVDggPq/HykhzJlvkuQ/GRAgmRdL8UFCnnSfyMFKKPn76EPpkqoZcvPt1KQuWLoAvAmgBden+ulTTwplPwWTf4VhEFeOi7aJBXVSugKKiLUELz4fSRpyYwLlZ+vyW9rZlv8orhancHbTe6hReTpDH4AGZZ0JIzj1ZQ6M6jNb89jFiCPpMdeoa2n61U+yfYQGOhCPwT2NkgY9dV3U4NVWDruCXSDl9dUd/EFRH4IJBqGbBIJ35z3tD1L9V3tHcqSSKE9iFdJbCTcp7QwTYAyJEZe/O1LlxVeQQqzwX4jKupgHuIVIKWa1dAwSMyHX0hNsRq8BaQ/yPBKbaieKf9pCimf6aVgqgeO9w0jSj4gQEpFFpBf04sAAUDcgwZKfGYpVF5AIMWjXnK6LaMPKMr8crxvym7T38I1Famhu/gWXu8Xl1Vl+sehBJsvy14zM1pKm0gV0zco/t89HGT+Npf5G3WGtxp3vA38GN/u3fkNa8DAVcD0QwPmH3uw8BTl/5HF9KMn8OEZfjSHZ0w//vMHVqViODGkXAcAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?client=firefox&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <Param name="oe" value="utf-8"/>
+ <Param name="client" value="firefox-b-m"/>
+</Url>
+<SearchForm>https://www.google.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-com-nocodes.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-com-nocodes.xml
new file mode 100644
index 0000000000..2df8e9d277
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/google-com-nocodes.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Google</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAB71BMVEUAAAD29vb29vb29vb39/f////39/f19fXqQzU0qFNChfT7vAX09PRjmvSVufXqSDr19PTywr41qVRVtnDrT0Lv8/D06unviYHufnQ7q1nrW07rV0rqRTZ4p/Xz9PPj7ub36sTvlo93w4xywYdnvH5euXZIsGQ/rVz6xSr7wRj6vQrk6/XR3/WnxfVUkfXg6fSQtvR+q/RHifPq8ez04d/N5dPE4svyx8Op17bxsq2X0KaAx5Nrv4FNsWbsa2BDrl9Brl/rSz77wBPo7fW90/W4zvWwyvWHsfVclvRMjPRFh/T18O/17+718+z06ejf7OLz4+Lb6+D1793V6Nr27tbz1NHzzMhipsi938a33cLyu7al1bLxt7L25K/25K6g067wrqjxrKaOzJ/woJqFyJj435X43Iv42oT42Xxju3rugnlPs2rtcmjtcGacwWXtZ1v50FnsYVXsXFD5zU/2sEvrU0b5vDz5xzb6xzPt8PXX4vXH2PVrn/RpnfT06+tMjurf7eNTleH03dzz2tjz2NZbnday2rzxwLxnrbid0qtosqer1KXwp6HwpJ6IyZnvn5jxspdltZRdtIPueW9yu26Hvm1etmuzwVjsY1f0oE/xj0/we0vGwkjua0bsW0DqTzrXwDX5wCnkvyX6wB3wvhYYaN+hAAAAB3RSTlMA8si8ZBhlc+JuAAAAA9xJREFUaN7dmmdT2zAchw2EysYuhCSQEDYkhNFC2Xu3UEYZpWxaZhezUKB77733Hh+0oclR21L0lzA+7vq8yyX6PbEkS7IlwY8lPAzBKNgHgLBwi7BBZARHOKckItIviGAP55dECMIuI+XhH1mEcKCoQUO4EIZMJUxAJIA/z1NA4M6HDbBAAfOBUoDAALCAs3bgwoKxfNgggPm8hp1vA/MERuuHHCOw5l8tn3YtvE+MSXIkLrnSx0pYDQJT/u305SRRi9s5ABiY2yB+ckkkkjgdDxaGBR1PHGJIGtNBhUCvIGtlokhlbYJSSbCgZ0EEeRUPCCj59xwiA+5+mkGg5KeLbMRUEg1QI1udIisxt7bQi6yz7PmxW+mmLkP5sOAZTz4owJs4NokY5lipq3M3suYrAQEp/6aDEO4c6wl82zXiXGP6/0rIKprH4pcnOrV3uJuSD7bBCDbmPLZivWwyBstnFXTqx5864ujf72bIRwKhCZ7r8l0d5LJdA+DcRryCQzmrmj7ktCIDEATHZXn967/8eSCfX5Aty/KvT2KQlXi0zYI0eYPcD0FBuaF4hSA4JQdY/7aRP4sMggvOy0F+fBHFpJJtFxyWN2n6KLqQYYH+Ljgqq1gtN5qvYFdwUi1owrvobioX4So6oxacRRhRVPbDgnNqwQlewV5YkK0WHOMV7IMFOWpBGq+gFRbkqgVHeAWFZguSza6iZLMbudDsbtpq9o0WDQiAoQIWtCC+wW6xDCsQradNLbiEDXa04fr3aykTQTS3qwUFPBPO91pJsnVDggPq/HykhzJlvkuQ/GRAgmRdL8UFCnnSfyMFKKPn76EPpkqoZcvPt1KQuWLoAvAmgBden+ulTTwplPwWTf4VhEFeOi7aJBXVSugKKiLUELz4fSRpyYwLlZ+vyW9rZlv8orhancHbTe6hReTpDH4AGZZ0JIzj1ZQ6M6jNb89jFiCPpMdeoa2n61U+yfYQGOhCPwT2NkgY9dV3U4NVWDruCXSDl9dUd/EFRH4IJBqGbBIJ35z3tD1L9V3tHcqSSKE9iFdJbCTcp7QwTYAyJEZe/O1LlxVeQQqzwX4jKupgHuIVIKWa1dAwSMyHX0hNsRq8BaQ/yPBKbaieKf9pCimf6aVgqgeO9w0jSj4gQEpFFpBf04sAAUDcgwZKfGYpVF5AIMWjXnK6LaMPKMr8crxvym7T38I1Famhu/gWXu8Xl1Vl+sehBJsvy14zM1pKm0gV0zco/t89HGT+Npf5G3WGtxp3vA38GN/u3fkNa8DAVcD0QwPmH3uw8BTl/5HF9KMn8OEZfjSHZ0w//vMHVqViODGkXAcAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?client=firefox&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.google.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/gulesider-mobile-NO.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/gulesider-mobile-NO.xml
new file mode 100644
index 0000000000..bfd642190a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/gulesider-mobile-NO.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Gule sider mobil</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAYFBMVEX/zDMAAAD/zDP/zDP/zDP/zDP/zDP/zDP/zDP/zDP/zDP/zDP/zDP/zDP/zDP/zDP/zDMAAADhtC3MoylpVBVmUhQyKAkGBQH7yTHwwDDesix1XhdNPQ9NPg5BMwzPpikazJPsAAAAEHRSTlPhAMQV27ID8u28jWZXQCWuasw+GwAAANhJREFUaN7t2ssOgjAQheEpinfxUEDu+P5vaXTV1gVBmETJ+deTfOlqkmbEmN05gkpy2hgjZhNBrcPVyC6CYvtELlAtlhi6yRa6icCLAAECAAECBIAVAf3drasQVHXeQD8ZSP0sgmwwQIAAAQJ/CZSpW1kjqA4GJgM2c2vwUeMN2NkL51F4ZYtvtDz1ygkQIECAwDeAbQun1s4GxsWFF87iKzNsbOkTIECAwCqBakjdBvzchxQBAgQIECCwZPpHAzF00z7cuIk5QrF9on488z7/UXrE9nX+8wScyz9pRUX+FgAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.gulesider.no/search">
+ <Param name="what" value="all" />
+ <Param name="cmpid" value="fre_partner_fire_gssbtop"/>
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://m.gulesider.no/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/leo_ende_de.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/leo_ende_de.xml
new file mode 100644
index 0000000000..5e8eb8681f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/leo_ende_de.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>LEO Eng-Tud</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAACEFBMVEUAAAD19fX19fX19fX19fX19fX19fX19fX19fX19fX40xMAAAACAgD////10BMIBwEGBQASDwEZFgIMCgEUEQIODAHmwxEiHQUKCAHpxxKbhAxYSwfsyRLnxRJvXwhdTwcdGQLyzhLwzBLDpQ9PQwY2LgQgGwLz8/K4nA6ljA16Zwl3ZQlzYglpWQgXFALIqg+/og+tkw2PeQuJdAo7MwVNRRvjwRHfvRHauRGEcQolHwPRshC9oA6giAwfGwYsJQOYj2KJgFHuyxLZuRE1Lw2AbQpgUgfZ1cHGwKLCu5tlXC3TtBDOrxCiigyWgAtlVghVSAZIPQVBNwUnIQTVtRDFpw+7ng4pJApsXAhEOgUzKwQqIwPy8u/q6OHk4dVaUiVCOhU8NRKzmA6njg18agliUwcuJwTs7Obc2chxZzhrYjJTSh9SRQY4MAQwKQTv7urn5dq8tZO4so2RiFpIQRnhvxEvKQyMdwtMQQbg3c7V0LrJxKiwqYKspHqelWl/dkZDQj8wMC/evRHcuxHKrA+ylw4PDw6vlQ2TfQuRewvRzLS1rYiupn6on3amnnN9c0NfVylFPhfXtxC1mg6pkA1JPwr4+Pja2trS0tLe28rOybBxcGuFe0x3bTwpKSeSfAvLy8uYmJhUVFRMS0d5cEBeVSgcGxrAwMC0tLStra2kpKR/f3xnZmJcVzw4NzVdVCd0R+oYAAAACXRSTlMA0gb4xG9sG8ChAtauAAAKIUlEQVRo3rWaZ1vbSBRG2WSTXc07kdw7tgHbdBMwJvQWelhC70loIfT03nvd9Gzvve9fXM3INnLBVjYP5wNm9Bgd+c69d0bCWVlZO3Z/uFPYBnZ+uHtHlswH7wnbxnsfyNe/befnhh1Zu4VtZXfW+6kOX2qZCY7NhntOCu/K+1k7U51fjwgv8t9RsDMrxcFDYwjW3B2+7ViT0Cy8I6kEzbBaCMeHI9shOIJ5olCiFhy+fPISez21OrP8efxfdM1YG69pFzSiKiJwxAT5V8M6AI3hlTBkTE/Vf3DKDJlrmgVj2E8IEbMJyYsKutqwidG3CCznb340GzZq/NCd1CoIo56FZ5GQbLSxA8dWKDDQkFd1x2OtHH+VQ8SQDasnou+/DmMVEUtguqRRsAwHIbkwiIQ0UTnaXWOAnzlVVL+BcXlJSbogitihQcxqFHyMYnbxtn2ElCI8/ZUOtIYkUXABNPxkWhB6YM7mTiNOaRNcREAkrXq8IWQeaAQCeSQVB0ptMKzOAO1kriSHkHbMaBMIVpanZaWlLmKBTL+dcHIKO9YNEZdlhP88SAFdBXGA5UU9jBoFP8N8hyiYgPOEIxYFATowooyKEfTMu+TLqGioI/spnCKLqk6joBnQFym1EMAE4dgHYSp9bSER9h2lgFSUywcNU3yOHlBJo6AbkBW3eJz7sgnHj8FWouZs3xuKdSV2PFw+oEej4Atg3YB+oiIPQ8wUz0uPKfqr+FAPhA9pFHSC1pUNlBMV44GXpMDpLHbkxIJUUxqqjvYUywbQeC1f0Ci4gkqSgJi7dwiSUYfa/YTzwAbA1hfJr0roW45r7kXCCyQX1n4z/TN/z7EbWBTZcET346+ffncTSoHXwHTiLdr1ZdDbSYJSfLmH8SP2yqPc2htsINwYy1GStk3QLuj04gJJYgjTXPApLZZHc/iMj75DiMjUUd2SZsGSlcU5CcO9PZzP+Pws/KKMPrk3GKm7Fa2Ci2PwW0gyxm/YCX+7+Qsm5dHETX76779EI+FY9fnaBIfPYdBFUhDkQb8HYEMeDXDBDwDalCQL4LI2wXNYefk7+sviBVP/sAkAwHtHyTdR3cHI2opuTYJOHcqVmNK98QKf7ZM9e/5gAgc7IT3EpkPmFZFpNaBH2xx8jSki0w6Uknj2sjT9HoBkZ2m6cUPY8zsAk6jUwYzGNP2JB6Ceoj+XJODD3399A9ju89Gw+d4PN4HaXt5p9fhKo+A0S8IDBpSQZOb8AKzRxa23xGqudCtLUTnwXKPgJFA0qmfnT0VOtYukosoL2q2xDpYh00Hekv0GjB3TJpieBVBH3pYyI85Ma6vky0Atnzh1OVdnZzKUUxzJ19aLwqzV5RabR9UbFG99JkMF0KxN8AS19vtDkNQXLZppkZjBcBSmw0q3yU8vuAJQgDYQNQ7Avz+9oLUWP8v7yNPn4O1OK7gKGfN9Ek8HYK5I/yHyKHp6jHz73ZVO8AKwnreTBHKPAmhKPxNFkDE8ex5sfLq14NEMBstVV+pyPoxGSQLg3JdGIAYA8+EMk3wOzrganYQhlusBAMayrQX1AOij9IInwFH17i0AWkiiZHs2TJUsuVI3DBe7APSkF7QB8LhUqYeGFEUlfUvisAyU8H0HnB5Ku9MJnoDhjV10k9L25tZGiYq7gFtUl6EVC/KLHueZZez41oIlA5oWJgCEiIIXI0qOUl+F46Py+l6l+pzA4LcfRenTYyKXHe2XrRYbrmwpON6GC/I7C4OQhqOCCr6a2RBBWuDRXkcclarVyYTmLQX/Yi1HmUxEEr4GtgJu8FMoGHintfuhYq2VkOH2XsKx4YutBNfRlBPNyMgvuUHgqIU3guGRgvmG+YJI7O2TiFGkXAoC4465126gpfvSiVSCFejuJJf/FKAPtZIksisRoY9Eo6iiJVmQb0NTqlVyQQJM7SkaXYMBjGih7/Po0guEMNZJKiwLegBTxXli4l0sBVCs6hTVo44iHzDz9HHKZncSus1sLxiUTI5YI67gNWrqSLhdPggYs0niMe/hrSb5DKg7siUZAKNg8+LmfRJk/OVx9/qAm8QzCnpqy0p+pANYrG91UGDqzSKa1D3HXh46GAQqyCaFfIbV1JlwPU0vsgLjhFRIwOCIPGt6TCbs428vAufj+v9k3MRUe/EszZr8lAJwewC90oteA7XDCQ3zAuCMWgsp5NEBEuOBGV+lWfSv6vpreOZNRBeVEuVeXo1YTGFaW3jwsKKiXYdKNwC/u6SovSZU7FlUZ2eqOpBEUgdIqiiXm4BARXyc6r2IMpVN2qHmdLpty/EgtZPzGKiOe1zglACdz9GrCrW9Y7JpyOo1GNbOsgou2TBKSonRDPui0/CNY6iVV6X7VqzMQrV8nzDw8DZJg9jL9l3pBdPnACzwvmJEoTooTgkMnz2N4T6WM25+u3TAHCszfWL9WO4+KJ7idxtbITaFNdxlXgeq5GQ24GCqrtcuwRgtZCWOqi1MyHxRyCzoBEbIgVo4SUrqAqChmC9khCHWm17aWgQNgtPAotsMX1x15orqTRImCnNZRAqHgEVIpffLX+19SUiH9XhmwfTXPNtoSQ5R8dpbuzlu9QAwX3B6DICuQXRH66FYeiZkFvwERm1Bwq4/fkIazODQylHWTn0UClc0CLwoPWs5m7Bi6jAQDZHy6ipraG8POaqizSmvb3yQAh9nFhzHEEliHZWu2BQXkC0o1GE1s+ARPMn3FP4acXOpcpKtKDDiWkbBMco7vb2apCaQuCcYVm0Eykz4OuMcnCsihCXG0dSCJkwk3G5Qt8oWwPVMgh4fIT7I3EkpGEdhwtMRiiJVXRsyNrtm074+WFsa0c9Xx8LEWN1KVJYom2R7QxVPOIl2pRd8gQ1z22HhxDnKVkEPbHdIenLGKeS3in4jz68+jB1LK8jXw9TJmipPJ5cV3qqMDxDce/kjR75DFoNYTt+LevSPlf2RrprvSaJ5+crbv9m4R1N4vLDdlV/6cSa94NBS9Nl7KbsgK2gdv5+R0LeZjpPKfAwYSjcFlQB1sKiuanzyO8s/wrjyEar08Mc6h15JpTk9oFoagny9c1Kc1ii4Co9yA0jzCFkDHYkmplmJ2qgE9dPzXqDlSiMA/SGNgvwhOIh9CIDVFdp8PFhgQwePkxmA1JEdayKsApbaYLqq+el7tyS5DYCkg39zAz0nwehS9p+0beUMJu2RIqazfDl5fFHQLBBOsU985PMl9uKP3hwDZlng8mKF3SF9rFtXDAfxWON/Y9WcODJ7kq3SVkSa7LxO/zws9YukGGeU/cM1eFkV2I2zgmZBMp16Je4NtLFTDp2ZBrEancoWSt1lI350/X8BiwT/j6ODWjv55MyGm1WB5Pm5IryTQOhBk+VbGNj5k7jY8sL6LD+DYGcGwbE2GGG+LPxPdipfe0hrWJXaksOs/WsPu4RtZde2f/Vk2788I7Nj14fCtvD+Lvn6/wPimqIPg5XxTQAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="get" template="https://dict.leo.org/dictQuery/m-query/conf/ende/query.conf/strlist.json">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="sort" value="PLa"/>
+ <Param name="shortQuery"/>
+ <Param name="noDescription"/>
+ <Param name="noQueryURLs"/>
+</Url>
+<Url type="text/html" method="GET" template="https://dict.leo.org/englisch-deutsch/{searchTerms}" resultdomain="leo.org"/>
+<SearchForm>https://dict.leo.org</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mapy-cz.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mapy-cz.xml
new file mode 100644
index 0000000000..6f198bf526
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mapy-cz.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Mapy.cz</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAt1BMVEUAAAAorAYorAYoqwYoqwUqqgAprAf///8srQr3/PUzsBP9/vxLuS5AtSLX8NCk3JWe2o99zWhVvjv7/frw+e7p9+Wz4qer356T1YLl9eHB57eO03x1yV9tx1ZdwUNZvz41sRX0+/Le8tnc8dbR7cq55K5oxFBGuCgwrxD4/Pfs+Ori9N3N7MbH6b2v4KOn3ZmZ2IiH0XSEz3B/zms6sxotrgzJ6sF4y2NwyFlkw0tQuzS14qlmxE6e93VUAAAABnRSTlMA8si8ZBgxEvPEAAACyklEQVRo3u2a2VbyMBCAAXXSHcoulR0FRBbZVPT9n+uHHpJpAn+hg7nwnH53bUq/kExmAieZPfe5LGggm7vPHHi4A23cPewFOt6Phv34gFbuMznQSi6TBa1kM6CZVJAKUkEqSAV7RoFnml7QCS80CCyPhVSKAFoE7+yIWQQtgjzjPFpaBAsmKICeIRKM3fOzdJtgxZAanKFjzt1bBE8MsYfnHxgXLLrghUXIwymDMIibvyMw16DydWyq3yw4/xrD403+rwjK6mAv8du5NMEnk1jKrW43OkH0KEImhjRAW4b0aQKfyUjhUpeGjyYIFMEWkB872mLTBAWmgEm1WJYaujRBVRV88xbnlSH0IXpTBaVjOA4rSsOEJsBuyovNeVTvD0gCS3RcdLhi4PhEaZAERf7xqWNGIrVdZifUSAIfy1mAXW1yWZQ6RYAl+QNGfNRtflPGpwgwiHaYNaJEpvqHIjBMPseb/UWfqUytEuPsCAKcYw8TX4TGCHriYk0QYMmfhd+np8TNJjKGY6AIqnIdWErJLTjcaojZIAnE6moftyiR3NYESTCgCIait8dSOWOcngMhz/zGM0WwUqvVl3idpQ5iniDA7s0PVzjN5up0lgKCwBAZ54nfapUOw+1IYUDftrQYB7ck7Vnt0wCkJlIgQVAQMwr/Zc6faREEE0yllzuxSy4oMlwFKqc7l2FyQR4X6RU/UNzEAkMs48U1W8t1YgHGkBMjaPOHOkkFGIBvEINlUwVWF6tlHB5RgOW+PIoV1LEcJBN4V/48HtqYDpMIWpGtYjzT8LFqUsE3FssYMIX7CQUuDw4bYzRumkvrhIKFWqguLIU5JBNsXvELXObdbFgJBR9YCKnECwZ85zYC0CDgiXriGwA6BGEa6lZfwtdrEfS3i9Ym5jN/63/TVJAKUkEqSAXXoP3QgPZjD9oPbug+eqL/8Iz24z//AHok9WmpkU9rAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://www.mapy.cz/" resultdomain="mapy.cz">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="sourceid" value="Searchmodule_3"/>
+</Url>
+<SearchForm>https://www.mapy.cz/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-ar.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-ar.xml
new file mode 100644
index 0000000000..f886dad815
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-ar.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>MercadoLibre Argentina</ShortName>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAADAFBMVEX/5gAAAAD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD///8tMnf/5wD/7QD/6wD/6QApLnX/7wAiKH3/8QD/8AD8/P0nLHQlKnIrMHUkKnwpL3owNXVuaVD64gP5+fv09fgkKXEiKHAWHGiSiD3+5QEfJn4WHXQfJGxybU3m5u/U1eK+v9Scnr4eJHMuMmx4ckrk0z/l0A729/rr6/K6vNKHirKAg6xvcqFDSIUgJm9oZFLEuE3Muh3b2+fY2eVqbqBJTYg5Pn4lLHsfJXpPUXkyN3kcInc/QWsbIGsxNGk3OWdQUGNAQWJaWVtiX1XRvxrVwRjgzBHv2Qnt7vPNz9/JytzGyNqusMtpbJwxN4YsMoNCRXobIXI4PHEoLW6FgGR/eEaJgUL13xT75BD75wL/8wDe3+qztM6Eh69vc6Z1eKRZXZNVWZExN38nLXoqMHhcXGqTjWRMTF7OwEiHf0SOhT68rCbj0CX65BjbyBT44Q/q1Az85Af34QTx8vbv7/Xi4+yipMKUl7qMj7V6faldYZcnLYEaInwjKXg2OXQsMXAlKW53cU+omzDEsyHJuB/z3AfCxNdhZZ5CSI1IS3gzN3B3dGhISWcSGGZFRmGjm19gXVtUU1jIu0uDe0SYjjqckDeklzTcyi7o1i2woyy/sCTq1w/95gr7+u/k49m3uM+mqMVYXZhhZZdJTpNMUY0WHnhWVnNlZGdVVWeclWNWVWC+slDWyETczEOflDbq2DO2qCfYxBbn0g7z4Ab///d9fpVNUpJSVo84P4tqa4o/RIhjZIRXWYA/QnRNT29ta2x/fGtvbWiupma3rVaTi1XKvlSlnE/Sw0fTwy+toC3v2ynGtSDfyxnx3Bj39urt7OGqrMiNjqB2d5BQVI09QXpZW3ZpaXN7dl7FuEO4rEPNvj/k0jLu3DG9riy2pyrJuSnV1M7Ix8S/vsCZmq6KjKM4PoZGSHSzqVq1qUn/+gD6vvcxAAAAFHRSTlPhALHbFvLtxrxmVyXkw8CQikU8ElPY3sEAAAkdSURBVGje7ZpleBNBEIYp7pqexT2QpDFKkzRQp1SoUHenXqQObWlLDXd3d3d3d3d3d9fLFUkuSWmB/OCh359sLnP7zs7ubWb2uRomJiYN6hGMosbNapuY1DCpXY9gNNVtgQKaEIyoOo1qNCQYVTVr1CQYVaIatQjGVY0a2t+rAdWAagCBUA2oBvyrAFHfwhdPzuZMO7W5C6qTp6blnH3yYlFf0V8BLC/M29K7axag8JvjwE3sgyqR6zDHzxvI6tp7S17h8j8DFC+atTFrdC7XbHdQyQQeiFC7o6IgIG9CSdBuM1nu6KyNsy4n/y6g9NkW9xF+sivBZRAFgmEEAUEQ+wEEEQSB0YtlwUuUM0dMyXnG+w1AytxNI/wSx0+gUmEE1G8CIjCJOsFx3swRm+amVBGQ0tGdEbB7AgVCCL8QAlH2FTjETzmXUgVA6WP30bIgmIro9RvUYZCgPYnx7o9LKwt4fkbO3QNBoB5vqRQK6jI6JbgfIWqwjH9qV6UApZ89csdBkK4pTC0LGpsoC3BQ5heoulFwDBgan8s+t/zXgOQEvmzfF1jHfZBUYpZr7xYTffhwDCs23n+BCj9GEm+B9+kbvwIU9h7J8eaO3QuiHoI/oo6GpdtCnw2Tra06hdLaDwhfnRbtqVAGYkYgZgEiJBLP0Vfqu7higGWXYyvbpcV4KgIWBPJgdBGp74REqmtLuJKjt9ub/lBo5Bo3gf+CIB4Io/NCQkQhS5NmyrPTjygsKwJYxh+5id7d9rZz9HDBTOXYwBAeT1WQNEcBjPC07WCqrfD0aM/Rfg7z5u/cma+c4y0XT7YOM11xIN7SMKDQ78iKb3d36DlkHdlztLePj7fcbd1QImMwDdc/ZpS65jBZPHy4OGad86DwNupr++96FxoC9O19/KbG7e07RVqnTZqUZuE0IENgUX6t3+ooOxebsJ9GtNC25hHhPdr+jN6KY759DQDOx6001ac2GcJv/Vu5CAV2rkKBTX9Tw7p1IkE/YLv0rf7+o4TW5Y1UL4aNeb/+7TKF9zpVQHgjzdMHSN58d78+c1r6jCFYY0DGjExzrNV6kL2HVQWEA9P76gFsU+gPUKRwCDa/7Vy9Un9Eup2HvUUFQZr6SReQsvmAfmuLGTZt27RxspFyIjWumrsIbWgGCXd8i3UAO6Sv9RuHRkn5dnb2XmudtJfo2hlRBqd6ZfwOPKA45/gKA9atJw62dbaxwvvbfohwvaGp3n8sIRkHuLr5vWlVNUjqGm7gp3e+i3GAvBPHra3aVpEQSWSu0ruwze+M3IYDXPSYGusWs2awVYeqECJcdRYTzWl12lGWm8d5HGAMx3FcfoB3rGf0pNU9Kk/o5OKV2lrD9YjB64dJ5H7cJI4MB+BylnWngCXj8/0VkphJkaGVJXQYKrTt920xhFsfdZMoAuY7hlCXcbg6ABVMAGH0jyV4bIBC8sG6ssNokz4jQz13PQatd5P4yV6qEAqEwHt1AYAjhDVAmAoHz8+VDLPuHx61fugq09ZO5vonP2wgDQu6tdAuLMI5RuKjLAghkbA/W3gPgAeMYS8hYQ2MQSl7mRtrxwQOuUptMxhSjyH6pt4cSC8P/0B7JnG0/5IStPfvSUYBW4ZfRcPmUTQTFKrILPZeD9P+zkKP1IG2XoecOnVqjydM5K8tD/8qYOpSdOA/b6fkD8Ovoo5ZvmVaeRalwNNKHQcXJ8xHgVB6SOepaifILI/eoNjxJM0MROSfhX8Onm8kOkJaSchYt4nY3xq2Vuxsew72AMJ0CHyXHtinZIkmANrDPGmJAyQneCpJoKZRkDwdXXoDyp+eTmi0zZnOP3ruH0ErJ9BdzVErZ3mgpnfUPm7TbuB30wvykUWQVgqqHD7JwjZKY49ztmvzvdnW1eYbAXBNHZS5QamZgcOqOHlHAh5wdaRYppXPIWVJ3lKvoRqAIUSLMNqP4NiWNy0E9lIfMx6imQMmkjmLdQCEBH72bopWqguHFDnEOP3ctl0E9oKo789EpJcttkh7jsgvCtHK8qnjejFOE3QBlozsLsGQdoHR3ZFx0Mm0rRUNAwyMCLPxymwdaoX1bCG0RSMWuia+iKJVocB7p3dmbNcDEJ1mkP33wbi0fCFj2Lqs77HHuu3ZzyMdeySsvTJ6Dpy8wUw7B4bLZpPpZ5brALC8kSnmdtMmgFCgcs7MzuiuTxtAwxasa5RpBMNZTaBlxkokfgu1Cyyk2xgywN+hP/GaxQfIsn0QbgwUWNVlWOpEGxdsGDSXtegM2w8NRbO8yT7jAkNI2v53GyMGBDkGU0c6IJ69jIqvDKCiAP6GWHoEtgExBqtnlpFhFXlw+M7uuBqOFOJABugnDaWOhF1TiQB5eiAFu0vLr8ClBYqDPdDkK1MydGKHNuZ2crmnz0JcmQJSgnzJAHGUpeH0fccoJtCZM5+nPW5sD+9+hRGdlhbNmK0YHj2ZnG12bWmJthUIdVs4kgUwR+VVVIDMHUUE2GT/QGzjxTGuOfj4BIyDixbIuInjYSoVwVVwQQEsNtp/x4pLqLypdABgceapqDB+FBDC44EQCJPUZT6IH6GqD4cFoPGZ+6si0PIjAwB6keOSgiGdQKHCPnXQkCopjtwLAOgntv+6jL06jc4E0DiNVI7nkTBP8cI7L3JMjCOzAYAp2LS4MoV4ysU4OqBGsLfODxJBJO1Y42t8sGjn1l7q7gEi52xxJY8SFk8jqhG9Oos5vvlL9yIQCcKfiCAwehEpGWe2lUPu3As1pjM2Lar8YUjp04d0IlDOYHXx7zPWMTgEhDSETAh2vDLPv0u2uDNbbUfnb8wrrdJxzvKnZ5gMjAFks8hidtx0X4cxSWaYksY4bJ0exxaTWdmYAZFBfDi3uMoHUrxFW9z5dHTCMbE7s1gs8jehTcxxzHc6f0rO5dLfO1JL3p7jzuQTUYheMemo7+4Jl278yaFg8qJtjx7cZwrQcNGJP0QHiHQB8/6DR+cuJ/+FY82UwlcXZiXM7u0+pSumKe69ZyfMuvDqespfPJgtXnZ9l+WljpguWe66vqxY9C+d/FYDNFUNqAb8lwBjvzRg9NcejP/ihkl9gtGEvXpi0qopwWiq29IEe/2nvohgDNVqjr7+8xVMQ88CFe38/QAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.mercadolibre.com.ar/jm/search">
+ <Param name="as_word" value="{searchTerms}"/>
+ <Param name="source" value="firefox_box"/>
+</Url>
+<SearchForm>https://www.mercadolibre.com.ar/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-cl.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-cl.xml
new file mode 100644
index 0000000000..3881fc348f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-cl.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>MercadoLibre Chile</ShortName>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAADAFBMVEX/5gAAAAD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD///8tMnf/5wD/7QD/6wD/6QApLnX/7wAiKH3/8QD/8AD8/P0nLHQlKnIrMHUkKnwpL3owNXVuaVD64gP5+fv09fgkKXEiKHAWHGiSiD3+5QEfJn4WHXQfJGxybU3m5u/U1eK+v9Scnr4eJHMuMmx4ckrk0z/l0A729/rr6/K6vNKHirKAg6xvcqFDSIUgJm9oZFLEuE3Muh3b2+fY2eVqbqBJTYg5Pn4lLHsfJXpPUXkyN3kcInc/QWsbIGsxNGk3OWdQUGNAQWJaWVtiX1XRvxrVwRjgzBHv2Qnt7vPNz9/JytzGyNqusMtpbJwxN4YsMoNCRXobIXI4PHEoLW6FgGR/eEaJgUL13xT75BD75wL/8wDe3+qztM6Eh69vc6Z1eKRZXZNVWZExN38nLXoqMHhcXGqTjWRMTF7OwEiHf0SOhT68rCbj0CX65BjbyBT44Q/q1Az85Af34QTx8vbv7/Xi4+yipMKUl7qMj7V6faldYZcnLYEaInwjKXg2OXQsMXAlKW53cU+omzDEsyHJuB/z3AfCxNdhZZ5CSI1IS3gzN3B3dGhISWcSGGZFRmGjm19gXVtUU1jIu0uDe0SYjjqckDeklzTcyi7o1i2woyy/sCTq1w/95gr7+u/k49m3uM+mqMVYXZhhZZdJTpNMUY0WHnhWVnNlZGdVVWeclWNWVWC+slDWyETczEOflDbq2DO2qCfYxBbn0g7z4Ab///d9fpVNUpJSVo84P4tqa4o/RIhjZIRXWYA/QnRNT29ta2x/fGtvbWiupma3rVaTi1XKvlSlnE/Sw0fTwy+toC3v2ynGtSDfyxnx3Bj39urt7OGqrMiNjqB2d5BQVI09QXpZW3ZpaXN7dl7FuEO4rEPNvj/k0jLu3DG9riy2pyrJuSnV1M7Ix8S/vsCZmq6KjKM4PoZGSHSzqVq1qUn/+gD6vvcxAAAAFHRSTlPhALHbFvLtxrxmVyXkw8CQikU8ElPY3sEAAAkdSURBVGje7ZpleBNBEIYp7pqexT2QpDFKkzRQp1SoUHenXqQObWlLDXd3d3d3d3d3d9fLFUkuSWmB/OCh359sLnP7zs7ubWb2uRomJiYN6hGMosbNapuY1DCpXY9gNNVtgQKaEIyoOo1qNCQYVTVr1CQYVaIatQjGVY0a2t+rAdWAagCBUA2oBvyrAFHfwhdPzuZMO7W5C6qTp6blnH3yYlFf0V8BLC/M29K7axag8JvjwE3sgyqR6zDHzxvI6tp7S17h8j8DFC+atTFrdC7XbHdQyQQeiFC7o6IgIG9CSdBuM1nu6KyNsy4n/y6g9NkW9xF+sivBZRAFgmEEAUEQ+wEEEQSB0YtlwUuUM0dMyXnG+w1AytxNI/wSx0+gUmEE1G8CIjCJOsFx3swRm+amVBGQ0tGdEbB7AgVCCL8QAlH2FTjETzmXUgVA6WP30bIgmIro9RvUYZCgPYnx7o9LKwt4fkbO3QNBoB5vqRQK6jI6JbgfIWqwjH9qV6UApZ89csdBkK4pTC0LGpsoC3BQ5heoulFwDBgan8s+t/zXgOQEvmzfF1jHfZBUYpZr7xYTffhwDCs23n+BCj9GEm+B9+kbvwIU9h7J8eaO3QuiHoI/oo6GpdtCnw2Tra06hdLaDwhfnRbtqVAGYkYgZgEiJBLP0Vfqu7higGWXYyvbpcV4KgIWBPJgdBGp74REqmtLuJKjt9ub/lBo5Bo3gf+CIB4Io/NCQkQhS5NmyrPTjygsKwJYxh+5id7d9rZz9HDBTOXYwBAeT1WQNEcBjPC07WCqrfD0aM/Rfg7z5u/cma+c4y0XT7YOM11xIN7SMKDQ78iKb3d36DlkHdlztLePj7fcbd1QImMwDdc/ZpS65jBZPHy4OGad86DwNupr++96FxoC9O19/KbG7e07RVqnTZqUZuE0IENgUX6t3+ooOxebsJ9GtNC25hHhPdr+jN6KY759DQDOx6001ac2GcJv/Vu5CAV2rkKBTX9Tw7p1IkE/YLv0rf7+o4TW5Y1UL4aNeb/+7TKF9zpVQHgjzdMHSN58d78+c1r6jCFYY0DGjExzrNV6kL2HVQWEA9P76gFsU+gPUKRwCDa/7Vy9Un9Eup2HvUUFQZr6SReQsvmAfmuLGTZt27RxspFyIjWumrsIbWgGCXd8i3UAO6Sv9RuHRkn5dnb2XmudtJfo2hlRBqd6ZfwOPKA45/gKA9atJw62dbaxwvvbfohwvaGp3n8sIRkHuLr5vWlVNUjqGm7gp3e+i3GAvBPHra3aVpEQSWSu0ruwze+M3IYDXPSYGusWs2awVYeqECJcdRYTzWl12lGWm8d5HGAMx3FcfoB3rGf0pNU9Kk/o5OKV2lrD9YjB64dJ5H7cJI4MB+BylnWngCXj8/0VkphJkaGVJXQYKrTt920xhFsfdZMoAuY7hlCXcbg6ABVMAGH0jyV4bIBC8sG6ssNokz4jQz13PQatd5P4yV6qEAqEwHt1AYAjhDVAmAoHz8+VDLPuHx61fugq09ZO5vonP2wgDQu6tdAuLMI5RuKjLAghkbA/W3gPgAeMYS8hYQ2MQSl7mRtrxwQOuUptMxhSjyH6pt4cSC8P/0B7JnG0/5IStPfvSUYBW4ZfRcPmUTQTFKrILPZeD9P+zkKP1IG2XoecOnVqjydM5K8tD/8qYOpSdOA/b6fkD8Ovoo5ZvmVaeRalwNNKHQcXJ8xHgVB6SOepaifILI/eoNjxJM0MROSfhX8Onm8kOkJaSchYt4nY3xq2Vuxsew72AMJ0CHyXHtinZIkmANrDPGmJAyQneCpJoKZRkDwdXXoDyp+eTmi0zZnOP3ruH0ErJ9BdzVErZ3mgpnfUPm7TbuB30wvykUWQVgqqHD7JwjZKY49ztmvzvdnW1eYbAXBNHZS5QamZgcOqOHlHAh5wdaRYppXPIWVJ3lKvoRqAIUSLMNqP4NiWNy0E9lIfMx6imQMmkjmLdQCEBH72bopWqguHFDnEOP3ctl0E9oKo789EpJcttkh7jsgvCtHK8qnjejFOE3QBlozsLsGQdoHR3ZFx0Mm0rRUNAwyMCLPxymwdaoX1bCG0RSMWuia+iKJVocB7p3dmbNcDEJ1mkP33wbi0fCFj2Lqs77HHuu3ZzyMdeySsvTJ6Dpy8wUw7B4bLZpPpZ5brALC8kSnmdtMmgFCgcs7MzuiuTxtAwxasa5RpBMNZTaBlxkokfgu1Cyyk2xgywN+hP/GaxQfIsn0QbgwUWNVlWOpEGxdsGDSXtegM2w8NRbO8yT7jAkNI2v53GyMGBDkGU0c6IJ69jIqvDKCiAP6GWHoEtgExBqtnlpFhFXlw+M7uuBqOFOJABugnDaWOhF1TiQB5eiAFu0vLr8ClBYqDPdDkK1MydGKHNuZ2crmnz0JcmQJSgnzJAHGUpeH0fccoJtCZM5+nPW5sD+9+hRGdlhbNmK0YHj2ZnG12bWmJthUIdVs4kgUwR+VVVIDMHUUE2GT/QGzjxTGuOfj4BIyDixbIuInjYSoVwVVwQQEsNtp/x4pLqLypdABgceapqDB+FBDC44EQCJPUZT6IH6GqD4cFoPGZ+6si0PIjAwB6keOSgiGdQKHCPnXQkCopjtwLAOgntv+6jL06jc4E0DiNVI7nkTBP8cI7L3JMjCOzAYAp2LS4MoV4ysU4OqBGsLfODxJBJO1Y42t8sGjn1l7q7gEi52xxJY8SFk8jqhG9Oos5vvlL9yIQCcKfiCAwehEpGWe2lUPu3As1pjM2Lar8YUjp04d0IlDOYHXx7zPWMTgEhDSETAh2vDLPv0u2uDNbbUfnb8wrrdJxzvKnZ5gMjAFks8hidtx0X4cxSWaYksY4bJ0exxaTWdmYAZFBfDi3uMoHUrxFW9z5dHTCMbE7s1gs8jehTcxxzHc6f0rO5dLfO1JL3p7jzuQTUYheMemo7+4Jl278yaFg8qJtjx7cZwrQcNGJP0QHiHQB8/6DR+cuJ/+FY82UwlcXZiXM7u0+pSumKe69ZyfMuvDqespfPJgtXnZ9l+WljpguWe66vqxY9C+d/FYDNFUNqAb8lwBjvzRg9NcejP/ihkl9gtGEvXpi0qopwWiq29IEe/2nvohgDNVqjr7+8xVMQ88CFe38/QAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.mercadolibre.cl/jm/search">
+ <Param name="as_word" value="{searchTerms}"/>
+ <Param name="source" value="firefox_box"/>
+</Url>
+<SearchForm>https://www.mercadolibre.cl/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-mx.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-mx.xml
new file mode 100644
index 0000000000..ae628ab554
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/mercadolibre-mx.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>MercadoLibre Mexico</ShortName>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAADAFBMVEX/5gAAAAD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD/5gD///8tMnf/5wD/7QD/6wD/6QApLnX/7wAiKH3/8QD/8AD8/P0nLHQlKnIrMHUkKnwpL3owNXVuaVD64gP5+fv09fgkKXEiKHAWHGiSiD3+5QEfJn4WHXQfJGxybU3m5u/U1eK+v9Scnr4eJHMuMmx4ckrk0z/l0A729/rr6/K6vNKHirKAg6xvcqFDSIUgJm9oZFLEuE3Muh3b2+fY2eVqbqBJTYg5Pn4lLHsfJXpPUXkyN3kcInc/QWsbIGsxNGk3OWdQUGNAQWJaWVtiX1XRvxrVwRjgzBHv2Qnt7vPNz9/JytzGyNqusMtpbJwxN4YsMoNCRXobIXI4PHEoLW6FgGR/eEaJgUL13xT75BD75wL/8wDe3+qztM6Eh69vc6Z1eKRZXZNVWZExN38nLXoqMHhcXGqTjWRMTF7OwEiHf0SOhT68rCbj0CX65BjbyBT44Q/q1Az85Af34QTx8vbv7/Xi4+yipMKUl7qMj7V6faldYZcnLYEaInwjKXg2OXQsMXAlKW53cU+omzDEsyHJuB/z3AfCxNdhZZ5CSI1IS3gzN3B3dGhISWcSGGZFRmGjm19gXVtUU1jIu0uDe0SYjjqckDeklzTcyi7o1i2woyy/sCTq1w/95gr7+u/k49m3uM+mqMVYXZhhZZdJTpNMUY0WHnhWVnNlZGdVVWeclWNWVWC+slDWyETczEOflDbq2DO2qCfYxBbn0g7z4Ab///d9fpVNUpJSVo84P4tqa4o/RIhjZIRXWYA/QnRNT29ta2x/fGtvbWiupma3rVaTi1XKvlSlnE/Sw0fTwy+toC3v2ynGtSDfyxnx3Bj39urt7OGqrMiNjqB2d5BQVI09QXpZW3ZpaXN7dl7FuEO4rEPNvj/k0jLu3DG9riy2pyrJuSnV1M7Ix8S/vsCZmq6KjKM4PoZGSHSzqVq1qUn/+gD6vvcxAAAAFHRSTlPhALHbFvLtxrxmVyXkw8CQikU8ElPY3sEAAAkdSURBVGje7ZpleBNBEIYp7pqexT2QpDFKkzRQp1SoUHenXqQObWlLDXd3d3d3d3d3d9fLFUkuSWmB/OCh359sLnP7zs7ubWb2uRomJiYN6hGMosbNapuY1DCpXY9gNNVtgQKaEIyoOo1qNCQYVTVr1CQYVaIatQjGVY0a2t+rAdWAagCBUA2oBvyrAFHfwhdPzuZMO7W5C6qTp6blnH3yYlFf0V8BLC/M29K7axag8JvjwE3sgyqR6zDHzxvI6tp7S17h8j8DFC+atTFrdC7XbHdQyQQeiFC7o6IgIG9CSdBuM1nu6KyNsy4n/y6g9NkW9xF+sivBZRAFgmEEAUEQ+wEEEQSB0YtlwUuUM0dMyXnG+w1AytxNI/wSx0+gUmEE1G8CIjCJOsFx3swRm+amVBGQ0tGdEbB7AgVCCL8QAlH2FTjETzmXUgVA6WP30bIgmIro9RvUYZCgPYnx7o9LKwt4fkbO3QNBoB5vqRQK6jI6JbgfIWqwjH9qV6UApZ89csdBkK4pTC0LGpsoC3BQ5heoulFwDBgan8s+t/zXgOQEvmzfF1jHfZBUYpZr7xYTffhwDCs23n+BCj9GEm+B9+kbvwIU9h7J8eaO3QuiHoI/oo6GpdtCnw2Tra06hdLaDwhfnRbtqVAGYkYgZgEiJBLP0Vfqu7higGWXYyvbpcV4KgIWBPJgdBGp74REqmtLuJKjt9ub/lBo5Bo3gf+CIB4Io/NCQkQhS5NmyrPTjygsKwJYxh+5id7d9rZz9HDBTOXYwBAeT1WQNEcBjPC07WCqrfD0aM/Rfg7z5u/cma+c4y0XT7YOM11xIN7SMKDQ78iKb3d36DlkHdlztLePj7fcbd1QImMwDdc/ZpS65jBZPHy4OGad86DwNupr++96FxoC9O19/KbG7e07RVqnTZqUZuE0IENgUX6t3+ooOxebsJ9GtNC25hHhPdr+jN6KY759DQDOx6001ac2GcJv/Vu5CAV2rkKBTX9Tw7p1IkE/YLv0rf7+o4TW5Y1UL4aNeb/+7TKF9zpVQHgjzdMHSN58d78+c1r6jCFYY0DGjExzrNV6kL2HVQWEA9P76gFsU+gPUKRwCDa/7Vy9Un9Eup2HvUUFQZr6SReQsvmAfmuLGTZt27RxspFyIjWumrsIbWgGCXd8i3UAO6Sv9RuHRkn5dnb2XmudtJfo2hlRBqd6ZfwOPKA45/gKA9atJw62dbaxwvvbfohwvaGp3n8sIRkHuLr5vWlVNUjqGm7gp3e+i3GAvBPHra3aVpEQSWSu0ruwze+M3IYDXPSYGusWs2awVYeqECJcdRYTzWl12lGWm8d5HGAMx3FcfoB3rGf0pNU9Kk/o5OKV2lrD9YjB64dJ5H7cJI4MB+BylnWngCXj8/0VkphJkaGVJXQYKrTt920xhFsfdZMoAuY7hlCXcbg6ABVMAGH0jyV4bIBC8sG6ssNokz4jQz13PQatd5P4yV6qEAqEwHt1AYAjhDVAmAoHz8+VDLPuHx61fugq09ZO5vonP2wgDQu6tdAuLMI5RuKjLAghkbA/W3gPgAeMYS8hYQ2MQSl7mRtrxwQOuUptMxhSjyH6pt4cSC8P/0B7JnG0/5IStPfvSUYBW4ZfRcPmUTQTFKrILPZeD9P+zkKP1IG2XoecOnVqjydM5K8tD/8qYOpSdOA/b6fkD8Ovoo5ZvmVaeRalwNNKHQcXJ8xHgVB6SOepaifILI/eoNjxJM0MROSfhX8Onm8kOkJaSchYt4nY3xq2Vuxsew72AMJ0CHyXHtinZIkmANrDPGmJAyQneCpJoKZRkDwdXXoDyp+eTmi0zZnOP3ruH0ErJ9BdzVErZ3mgpnfUPm7TbuB30wvykUWQVgqqHD7JwjZKY49ztmvzvdnW1eYbAXBNHZS5QamZgcOqOHlHAh5wdaRYppXPIWVJ3lKvoRqAIUSLMNqP4NiWNy0E9lIfMx6imQMmkjmLdQCEBH72bopWqguHFDnEOP3ctl0E9oKo789EpJcttkh7jsgvCtHK8qnjejFOE3QBlozsLsGQdoHR3ZFx0Mm0rRUNAwyMCLPxymwdaoX1bCG0RSMWuia+iKJVocB7p3dmbNcDEJ1mkP33wbi0fCFj2Lqs77HHuu3ZzyMdeySsvTJ6Dpy8wUw7B4bLZpPpZ5brALC8kSnmdtMmgFCgcs7MzuiuTxtAwxasa5RpBMNZTaBlxkokfgu1Cyyk2xgywN+hP/GaxQfIsn0QbgwUWNVlWOpEGxdsGDSXtegM2w8NRbO8yT7jAkNI2v53GyMGBDkGU0c6IJ69jIqvDKCiAP6GWHoEtgExBqtnlpFhFXlw+M7uuBqOFOJABugnDaWOhF1TiQB5eiAFu0vLr8ClBYqDPdDkK1MydGKHNuZ2crmnz0JcmQJSgnzJAHGUpeH0fccoJtCZM5+nPW5sD+9+hRGdlhbNmK0YHj2ZnG12bWmJthUIdVs4kgUwR+VVVIDMHUUE2GT/QGzjxTGuOfj4BIyDixbIuInjYSoVwVVwQQEsNtp/x4pLqLypdABgceapqDB+FBDC44EQCJPUZT6IH6GqD4cFoPGZ+6si0PIjAwB6keOSgiGdQKHCPnXQkCopjtwLAOgntv+6jL06jc4E0DiNVI7nkTBP8cI7L3JMjCOzAYAp2LS4MoV4ysU4OqBGsLfODxJBJO1Y42t8sGjn1l7q7gEi52xxJY8SFk8jqhG9Oos5vvlL9yIQCcKfiCAwehEpGWe2lUPu3As1pjM2Lar8YUjp04d0IlDOYHXx7zPWMTgEhDSETAh2vDLPv0u2uDNbbUfnb8wrrdJxzvKnZ5gMjAFks8hidtx0X4cxSWaYksY4bJ0exxaTWdmYAZFBfDi3uMoHUrxFW9z5dHTCMbE7s1gs8jehTcxxzHc6f0rO5dLfO1JL3p7jzuQTUYheMemo7+4Jl278yaFg8qJtjx7cZwrQcNGJP0QHiHQB8/6DR+cuJ/+FY82UwlcXZiXM7u0+pSumKe69ZyfMuvDqespfPJgtXnZ9l+WljpguWe66vqxY9C+d/FYDNFUNqAb8lwBjvzRg9NcejP/ihkl9gtGEvXpi0qopwWiq29IEe/2nvohgDNVqjr7+8xVMQ88CFe38/QAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.mercadolibre.com.mx/jm/search">
+ <Param name="as_word" value="{searchTerms}"/>
+ <Param name="source" value="firefox_box"/>
+</Url>
+<SearchForm>https://www.mercadolibre.com.mx/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/odpiralni.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/odpiralni.xml
new file mode 100644
index 0000000000..5ad1167292
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/odpiralni.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Odpiralni Časi</ShortName>
+<Description>Odpiralni Časi v Sloveniji</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAC+lBMVEUAAACTmji/u0zm3V7n3V+8w0+WmDjm52Pn6GTn3WDl3F2XmTiZmziVlziUlzjh4eDg4N/h4eGSnTnl52KUnzmYozmVoTnf5F3h5mDg5V7m6GPj5mHn6GXa41ebpznn3WCVlziRnDn////i5mGapTnd5FppZmbe3t1mY2T09/fk4+NWVVZYV1dVVFXY4VKgqzmPmznn6Ofa4lRoZWVnZGSdqDl0cHGnsjiRjY3T3kueqTmirTlcWlvc41hTUlOkrzhZWFhlYmJfXV2fqjnW4U/V4E3Q2keWojn3/Pzr7OxhX19dW1tbWVpgXGZbVWZkYGWmsTjt7u6uuTe0vzX6/f/y9POvuzZeWWZfXGNRUVGqtTe3wTRaV2LS3EliXmTH0kKptDittzfO2Ebo6ei9u7tqZ2JWU2G8xzj1+vnl5eXOzs2Hg4NdWGaksDmyvTa4wzRdWmNYVWFjYGDG0UHEzz++yjvv8O+rqKh6dnZsamRUUGHK1USgsDqltDl+e3tkYWNST2BxcV6tuDdxbW3r6VzCzT7S0dG6t7e0sbHb2VL07FDM10SdrDujpTd3dWRtbWCNlUaeoDexvDa7vTPH1DLV1NTDwcGSj4+Cfn5raGiVlWZxcGJ6fFrh21n47lDZ103u6UTS3T7t6TirrTe5xTa/zDS7yjLZ2dl/glbR0kTd40OanDiwsjbC0C/d4i7U3ivq6umvrKydmpqNiYmHh2eEgmbj3Fze2lbu6lL37UnV1UfLzUCXpDu6xzSMjWPR0F93eFzv61nm51DOz0PJ1ELAyz3HyT2ntjmnqTe/wTauvza0tTXl5S2hnp2WkpJ9e2VxcGWWnF6/xFiHjkr17ETExjzCxDmpuDi3uTTJyMiZlpalo2ixsGbBv2GBg16stFrk41iCh1Hg41CXnkixukTl5kLDzzajn5+hoWbc3mGJjVVMTU6SmUicpEWSnjuywjW2uDSno6Opqmbk51urs1SRlVO6xEff3V2xuF2osFTb21PO1kzJ0UrM3CvIrGKRAAAAD3RSTlMAzQb2bRv20M/Hx27zb2/Ao+FEAAAP3UlEQVRo3rRWWWgTQRhePPB46S4+aD0QIm42VyEmiIIuSVDJUZVAAhtYKxgNtn1JqrYmQhvwrpCnqjWiNgUR+1C1ttoqtFSrT4oHKniLeOKTB96C//wzs1stXg9+3cw/Mzv7fTP//89MBUEYNWbC6Jn/AaMnjBklAMaOn/6PmPrX/ePHwvzHTf2PGDdKGDPpv2KMMPEfRk/dgpj6D59MFEZPmjZt2qRJ0+APH6iQH76dxgzYweaDB5sH35a/fPmyfAvUDw5OZa9JQSv4h+PNV6OFU9P+gFmTgLB34N7dYveVq1nA1UJP8e6b+9A7OGnWrD98fUo4Net3mDJry8HmD/f6ryiZTCqlp9Oalk7rUM1ohe4XA70HB2f9Higw5UeQNu8b7Ht0r2dNJpepUvz+cMTnBPgWhP1+pUlPpfyFuwPNzTgcWbBEaxSnhAMGcfmIWm/fQDGbSWmKP+B0ORf4N29sbG1t3TjXE/G5HAG/tyqTauq+DxK/RDkIlE8p/xG8vaVvoL8pn/Yola5KpbGrYXuNNRmtr4+GpJrVDZdr5YjL5/dU5fQr9/t6TY6f6A6AwC/Q96hYldfkOtW3ed7q3dFY0BZNVtsB1e6oLRiLH9lzZ24gEZCbcnrPQN+WyeWTR3KgwOTJ8I7+0FD03ri3BugDarh2hzu4NmQXRUksK8NHkqCUQrZYaM88OREBCa3Y28y+Nku0VGAk+h71pHQ5rIb31sSC7jIRKMtAQCQCxGBTtNssR7r2qRFZy1+9fuPtCBYUOD159ojeRzfur8krHlflyd2WOLAhJStRQYICa9aoxd5Vp/o9Kf3FjXPA9DPZaeH07BE4d+OungHvzN2D9BJMHekkANoyBmiK1pDlSK2rUtZy3W29I7mYwMIf+NuKOU12VHbZgjBHkZDiD0Sski0WTFpZG6IiQSnGYzv8qlfJFx63ARPjQosCC3/EjHNt3fkmj7rvtgWYcLbUMRIRO766q6EsKRrs0A+wW8o2Juo8ueznthk/0e0Uds74AcDfk1f8icZQjIaSBwA9YmtQ1VetUTuRRXbkBz/Z1u5VA3LKs7PtJ7oRAo9h/t66xMlg3CoBB3M3CkA7WJvwVO4TkxJVZIGBOkTitSMip7KP234hMIeZ9mJeqUvstYSsPLaY+IQRrG2ey7tgruQGYYwOqtJB1ZYdzsi+3NWn534WmDMMM9rf5Jf7gf+4lVCYyQJsYJmAHQSoMHRDL87AarfscATkfE/74+GMVKCiooKUcyrar+uaJ3ES+IEOmYGG8JAYo4BDIStgyQp9pKRrFCVLg1qn5F+0AxsF2EPCoQoTTzuyGVltDIaslB4FoJDQG9RFSoQJoM+IISNJCcl0J+HVMtdBwQAILKqogAeKRRXt3TnZuS8UN/2PAsxHInURChBRtjg2ToLq8VijKmeyHR2LgMwUMND+MaUEfLdjVspIg4cPcuEKHCzI0KATJxW+Cms86XHKuf52kxMEFnN0dGR1T+KyRWQhNB1At5lkZhHNHvjR93RLQGm17HHUVenXny7ipIeEM/MBixfPXzz/0rWc7JprSwIVEqAKgrBLLE0xyEQSnUblmQAZaZmXkFOFp8BHcQYFEB1fmrSw73bQarjfOJYxIcmzFrPI7mb9mFl0O/N1RqvlBd7Uw0ucFQRWsOqlYk5W51n4tSLyBGLnATbWoovYCngmQ4n7BGNlhVz16IWO+StMAYqOr4oWDu+Oi4ZLMDnBohzG2IgBVCkz45boGAJ3dK7Tm3l4idEeFY6uWAJYsYREALawyD4kBAh0AU10HgM7CTL2w2OGCisQ5xMub+ZK5wrgBIDAEkSpdDUdDtfEcUuxtOBrgRbSQQyYgGTccAhcKPThKHdos69Ju94JpFRg1ZIlq1at6nyoe9XamIg+R7+IlB0FWRYaRwWfAKYOXzHPhdhzlyfXfwlIUeA8VIhAT0bxbQ+iAI7EFWMsDJeZacoWyfeBcWyjaP3uunB6f6mEvCCwi6BUyqYXbHa78SMseJKiZZNFF+FOxhbb5ixYSI5niq3WoegPO5H4vHB+E0Hnu7SizgsawWJnKN1DfAFmDNgZPixXMdqsRsKcKnYi8Xnh7NCmoaGhzv6Uv3J1UASQqUFhAJuYpKaLMOoYVFSmC8ESN1uNEk4XSruAfwgEVgKGjl3Rw97dIbZcCUAnx6IhUWHjsOPOkQgfzyh42NURb3VWNb0vEeazVKC0Lqs5G6N2dIVEp8ct1URPGTeaeZYjJXclHxvcqyrau2NMYB3g2Ke04uqK4VhOiSXzMt0J5kZj7kFOntTcV9AINjgV/dqxdShwEQWu6YqzIYjveZx5IgHY5Ts8TUWmTYRoi2YrytXX+MP6g2OE+aJwcT0ABPwLtttwCAItGU7FaMUUQDUJddmSMRH4uZuUNkfShZZ1wMwF+vWwv6Yek4g81ENgkZwH0bxwgIxOnvkHY4GdqFVdvbEyvX99Cwrc3Ar49mBZZLOYJLToU8ZNJ4l+xoaRRWyB7L8J8hqVuLWHGn1a9kILMN8Ubl4A01JI+zYmq42TDR7mWCODMKpr99Ig06igMMsgvnvAgEB9raNqzXsQuEAENmx49mw/ZqlxOCA/Wwev421yR0UBmjrUQUQZBYcd4fGTjirvk5YLFzbcEm5tAIHD+5c6W+vt7Cqg00CLbmY5Cfzb5ICX+JJfFnyj8G1nxsqxXHnSAtTDBWAFlJ3+G8TPaCYqAf/32qwlNokgDG9N9OLbWI5EN2SXshBtllajBoEegOIupYWuLQmp0cZqgSY9tDUBDTXRHjAgviM3LTbRJj6ubdJbtUYvPVQ9mBgPJr49ePeff2Z28e3FL7M7M7uz3zfz7z8z/8KlUbek+k7fBVvybpjuAxXmWlQgzQUeHT16dHoaTAQC0IjSWlEb1rGXwN+pSNIB99kka8PXRNqQiW5CATTRLFA/ogKz5CXfPUbfK5u9mLECnME+nYpK+EXSjN+ixsHENKG1K0leco8lMD9by/B9hE8sLKBT4RXkl5CfWKQxFmKhBZ9ycMcVP+lOn7g5PU8ELtwEXHypB9UzcRwiV2C7PQ7/B35swH2SE1NHoBOtdeLAwevzdWC+IFwYAVz8qgeiR1rgHk4D3n/a/Mf+o1HQFeh+gPTUNmxsra7T+4zr9XlgJgKpkdTs14zTfS+8yXRQvlDiek/4g4pq8pMxsMCJj4X1iYV6kUtqUK/NAn8KBFKA2Q+a0/Mmgf7CP2eYi6L/HA9i/48QfgDzYOZuCBThQxua8jv1t7NAzAXmewfTynASdyi29Vm/TRB+N/TfT/g3c/+kORVDHf7u4ULiqc+pfZhOpXpA4FkPQX0hE7x2K4K8ZpiDhmX8Kus/rm/cgzCzBsw/KEhYETu1q06InzGB6Zfwli+3cBthW7YZo32w/9hlhsYdo+GnBexe5Na1oHF9JMUEcj25XK7++qDT8yJMZ6ZlZMoP6wO1j8mDVChiLlfMcCQlphSv/nY6R5g/C59zgGoqN5j2t8ddLMQiCZ+UW54DP7M/Og0hBw0An7/m1wFfLMKwLRmv69VctVr9LDzu7a1Cmq/pgSDYiCnw8Dd+ZtS0jxWgcHdhApt5jXpARJaCWjnXU+0FPAYBRP21ofbtFmW2faAJAOF777y8/yzo5pODuhwais0dpp+4BxaaqXf3MoFuRK53UAtKt+JWyI9srYcn3vUhPwqQO0hj7X3Ms7CAT7qSE25w0hHK+1i4g/nkofpbXYJPTBl7ZG4d8tim4fbLCT6pLAE4N0Z+zFxQksNH/IFMMdU92T0JvHeEO5OThw7B0ZM9FYteOxZp3BGgKEdaxpKy6eJcBGlRgMgxB8VrrqGTPjXzfuQQpQWBQwT9/f3zM7rquy/KLMZEJlzJXHSXwCqL6VEBydlyRFtAksUjSsAoV3spLRHopxhY2puORTtvJWU+WHweWHjEw0mg9mPox4rQr8Nj7W5Vr9wcAEpy3BFuDAz0DyBGZnQJvqJw62catHt8TvGPHosTxWkNIzYYwJs+NVOuTiIjKNwQbuzi6O1XtYBnCl2VbwQ8xiPE1hZmmoVWrI99eehSNOo0Xn9CwgE4GgW6PlV07wHnRjQS9oj1mJvE2iw2M3oOHAd6RLtH0mufgBqBAtkuimzXrqUFMNLEUERGLnN7NtdmBL5skqzR4YggucSnfarm3TWZ3YWUcL4hPNzftR+Q7YKsO+vUvPA1fgzZzO8avnTQAmqypRRjIitUEKc8o7HM+6X90FtIWaAkAg1Yep+Jjfq+iC6+p2Ovrc0XNPiE5T9F4ZkaSLzsj6r6zFIWe0wZHwoP9zZi6ZWuBpUpka6/zCM5ESTqp8ynsMh8CTe+qF/SiysF4NmBbPv37n0onNsBVQCcIWW7a7oadU+JLlwzTHp8BWZf+Xvh2xO1z/FOt5QZzC4iF3LCmQg0oqOwUtSlqDJOf9gkHPxNWhODug0PRzGD93s56paMQMfKnu8IzwnnOjr2dFhoKxSKYCXf0wSsQHyWIZCSeSSS8x0SVyzxnt8vGafsK41kQA0CexrRtocqjPadPCyanwfUZxDsY5uOjhtPHhp66olKmcDcCifiAIG2H2DvKizoMa9POiLG5cYJxT3W2jepmnxYPNMOP75nBtsW7W3QRQTLQMBuoo1mjuziTEaT3MruY2KrzNwJHQhATnwC4B3ZJY49CXrUmF7cW0AWPPHsnHDe/hMcOxaX0xm10yeNx8VWXE/pSo2FTSjD7eYKh8+290UlzZgpZB0/c50XzjscdgcccCIZPdkX54qZtLTP1z4eSYzxGJT9GkbPeCmSSJ49qbi9sYy3stjRjM8jGS+gQCOaWdacLbw6lYl53crpJ2daxDiLKHjEjtMgkgjL4xN+X0A1tFpbAZ9lT+OBAIFmCrj0HdoKtoW04fT6PaPD48eT4fAY+ZtuMwH5q24o0XLr3m5JcQdUzShXCqXmXwMFfomQLV+oFDUj5u1UPIGrV8aPbz4WGUu2DLUk462uM1NPh68pnqDXaRjl5XyePfMrgdvkqo3cDFltbKFmm80WyucrC05Dc3o73T43/FU6Mfxg9+4HwycnTqt+nwLsoF9cLuVJ4+ZQKESeCVEmKCDdbeG27bdAiY8zxZhhxJzOzugBv6J4PIriP7AvGHDGNEMr1yr5/NyfGH4lsJWet0IBkj1fqrxaGCR0WjpGoUEl5i3OLDvypRC2Y49hbjKQAwS2/gWosaWyXCuWy4OIcrlYe1XZaSfsfwEIrGGlLb9rgrfm7KWSPWTb+nH7zu1btoYcpVLJYYPrf8U6oWnLv+KjDd7j3NxcCEb+zw81CWu3/A9s54W1wqomqJL6dpaTjBcwmc15KzzoDesOu8bq/GhaJQirm3ZuB8sS7NwJiWRwRsB1XodkVrEp1ljC62YrpMMrTasFwKoNTWu2/QesaVoP/f8GDdWLNKp78/8AAAAASUVORK5CYII=</Image>
+<UpdateUrl>https://www.odpiralnicasi.com/opensearch/description.xml</UpdateUrl>
+<Url type="text/html" method="GET" template="https://www.odpiralnicasi.com/spots">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="source" value="1"/>
+</Url>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/pazaruvaj.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/pazaruvaj.xml
new file mode 100644
index 0000000000..8e79095eec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/pazaruvaj.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Pazaruvaj</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAACDVBMVEUAAAD////09PT////09PT////09PT////19fX6+voAZpv////19fX09PT9/f3zbyH39/f4+Pj7/PwAZJr6+vr0bR0AYpkAY5r0bh8EaJ0Pb6Hw8PAIap71bBvq6urFx8nm5ufk5OXGxscLbJ/u7u/s7OzT0NDi4uPPzc7zcSPx8fLJycs0gKcYc6IUcaHzcibo6OnW09PFys6+w8fz8/Pf39/O0tfY1tXHzNHh4eHZ19gbdqbX2Nrb2dne3NzGyMq5wMb0eC/wdi7T1tnS09XK0NTMy8uYsL6Ur72Dp7orgKwObqD3mWH2lVz2ikv0dCnNzc+0vsTHw8PIwb8wg65GiKwlfKoid6P1hkTsfj7a29zKyszKysvDxciltsGhtcDKvbZvnbVBjbVglbJYkbDPtqnglmz2jU/ohUrc3eDa3N7V2NzIztNNlLmApbjMua8zha/Yp4venHXjkGDrgUP0divf4OHX293Q1NjOz9CwvMTJv7k9i7M6ibJdlLE3h7EheqgleaTZo4T3nGb2kVXniFH1fjj0ezOpuMKIqrxHkbd0n7ZLi63Qs6PSsJ7TrZbVqZDbn331gj7d4OLQxcBsm7RDh6s9hKkre6X2k1nli1btezfveTRVmb1Sjq4gdqPcnnrhlGftfDr2axm7xs60w8x6orfLvLMAYJnkjlvQyMXPx8Wcsr5ombNRjq5fDs51AAAACnRSTlMA5ua9vbKyZGQZb51a3gAACM9JREFUaN61mgdTE1EUhdeueS/JtuwmbEgjPQRCSAg9IE0FEbFRRLD3gl2xF+y9997Lb/RtYRfYDXmoHBxnmHHuN+eePbmbGQmkBfNmgRnQrHkLCFGL5oAZ05xFCDCD8xEB7QfMqBYQ88CMah6RO1/aAv5dswiQQ6S3rDrwHxA5AKl0sGFpW6xOmICgScE8bYBlsgCw+OvCJx+94R1rBlvrBIBkocz+xIPdFS3Jygck+nUaIixg4g9gXZnY4JYSjjGZuKLXg631rlBtU0Vl99DWO/cO7Xi7qsVDAwv+z0QH0uoHlqxxovGiGIS42hobubDt+eGXEZuo3ru+4yibv3RgRqvfV2ziTZq49jMnfwy///DBZpVks1++lTyWAtN3gFbfh1a/jtHGKy7aN3/6PtxrQwQZUfpuqPIBBXAdqKuvjm3YXMIzJp0YPr7x08N7LxBBVuHLC90VCRrgO5i0ej2Ci9/4+HCbSrDbDt9xN/kxopABINFVvq/YwZlyi+HfXHuoebDarM+3JndBAPAcVLfpVq8T9+bjwQ82JLuyp9JtQ3tCFAD5HQAyvOWrKa+YpQ+HDx7a0bvYWijvSYkivwPgcj9l8gP44mxLxZ7kqv23DlplFzYURbZJsIB8GaQ3OE04WlLNQiHgOZZ8q0XxDUVhBlMD6Mw+Bmc+93i9/Ml0NKk9UCiKkT0hFkwFgK2vOSwHzsF6IBN878Y9si/kKHIXLVG+jsECcJvDZolAH/MdkpJWothxO7k7mttB/aDThCWmZGUVUAjdzxFBRUQO7a/cCYExgM1sZEx44m4EKYXQ1P3cZtVUuPjgSIuXAkYAc/9jDhPArCsPAVls06odigelFe+HfRUuGugAwNMQZ0y4hGtl9Bjh+IhEyBMFQVvq20zY4ovdLqCIrBjpVQiRCVFY6HEiLFTZLx6fgMpm0Qj7e20yYEXpWBTopnpZy3iAkH0yDQAvlU2RuWX/eyXpFUdkhF2+qbRFA3hXlpimIeeGPqAq1bJ1sUIobVQQNvuOW1IUY4C6JabpiN8Shhohuue2SjiiIkq3fUGvNzKBgMHN6oawy6YpsOd2qUKIHGlsXBFRoriHXm+kKAih9Qk3LQvcRlQ2Tf7Ku6V2qxJEY+OVxlLtpkJEIMiypcy0AMz5cg8YJ1fl3YhKuDI6OnqlVPkgH6pIWVAGIdyPOu2yldETCResY4Qjo/fv99y/IhELe4eaKJqgYfCRAsAvWwCMlzd5Qa3zivs9PT2dPRKi8LLPKzY5vbIEl+DQlU0hHLSpBDS/c/v2zivimu5WkARNR8NbOFyARODXtApgIsGtEY70oPmXLj3rXCFacCEAXbfBiQsokkIo2lA/EWDZ6VaPqL1Rmr+8o9NqX/xlFwJYArGbuBaKJAvcmTCcTPAdUj2MivOXdyyPWO1bjxMsy1KZNgcuoEhXNoVwrFsjdC5H8y9ut44BaI+7GNOCs8ChK5ssenf35cKxJaEFdXQ02q2R/U0igMUvm6OgSLlsHqAjrEIExUIHMmC32npXhSQAHSo/j0loly1oZdNEqUfUPnrx4kVkoHBbpSABWHNwI4cZQoFTvWw6wvGhwza5DB0XO+3oIdp/jJUBdHplnMEMoV2y4GhTy6YJVgwdljxElnesQD07WOmnFYAQPsNhhqBYuIkumwFhq/Qdwr78vpTATpolKEls3YYi3BDa5V1drddZkE7cS0SI9EQQZbglxVIKgA6sX8PjhaBY4MTLppew55Zy4myXfSGaQgBWRmSWODBDKBgr21FgSLiz2C5uqfRWBUlRsgMW/UFl4zFDkJ9UXiqbXoFK6YgWvksmaEpzwEJUNswQcpRNO0DDEbt98dBuilIcsCJELBuPGQJKQSubXonkcKTwXqXAygBFbAqVDTOEkhK5bD4XMJQnee9w905WcaAS6k+WMHghxGucatkM5fWNnEhRCkCVH102LEBJzbka+bKt9wNjpQeqWB2Aqht04gCQgbXLnOplMxTbVUvpAKxrPdZlK6g5t3Z1DSNfNnMOgrdf0DuAGazX1BJk4MABh2ghji6bsczuBKsDsF6sssWRgQOnJAu8+p1Nv6O03gFlLluK4QAZWH321GmH/BrpMgbQJ8YA5DhRWK+R4oZOb7p+jpGe1KocAORAmikCNEUxyuY4JxrYtHeTaIHbV2dcBZj1GAEojLI5ZQN7XxWgf/n1c8YY4In5jQAYZWPaJQMIEGdyO6Cbm0lSD0DKX7YaxcBep2jnp3EGIbeLMgRQrnyXzbFWSmDvKXGX/NOBgOHnqXsXlcNBvrIxBZKBs3ETI/62L0gbzffVQjIHgPIOFPN5DJxeLY83cWt8RjfH60bzVQCcpFTZPmaqBFYvO9cuj2e4Jw1GEYd8fZBU5xEknPRT1TBF2dprChyMMr5oc3k1qxtvSUvz1Xk6B6QQfMRNFYL8NxPffLU1Tevn7/IdJcUxOR1AjLLxpqefG8LpKNCJ7nOj+VM7IAN5ysY7ipeWB6ugUb9q3SF5fm4HSNU5vrMpq7/Z5s54WGAgttbtJWE+B5BMxB5zucaLq68O0MBIVHPMg0bndwBzvEbyzPkbK8P1UQvINd9FQahzoBcZMrhsDGcqvlbeqq5eJ3iiP0FCnYwAMBq8xhisPlZWmw3lnN/VHyAhJoCscq/hxo/nxdVnvCm4cyDHBYt2dQUgxAVAc6ahmBu/+oZwXQKK2uXzAgMJ/Sf8cBoAGM00nHHyPMPwnOPJtfJgOjCGrjX6fPP3nxAgFkAjVK8fPFMcjz/93ZYtOyqg7aqErO4EBGLNAsQEaPJUBwcaGgbCGW+UnLC+pqxr0vxscxROH0BCweUNJfxmcrK55qx/wn13o/m4AD3FQKnm/nEEj6/WDLEA+BKasyEWSIJ94vz/DYCpPnfXLpfg99TGYlVwShHmvxJ09XXFYrH+E2k/NE8pYvZfEmDK73IJKZhn/mxivvlvJT2z+TSfWGieUS0kiLnmGdRc8T/PzCBh7iJC1ML5s80zoNnz0X6IP4U+A2/7sk8AAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://www.pazaruvaj.com/CategorySearch.php"
+ resultdomain="pazaruvaj.com">
+ <Param name="st" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.pazaruvaj.com/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/prisjakt-sv-SE.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/prisjakt-sv-SE.xml
new file mode 100644
index 0000000000..1027b39dad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/prisjakt-sv-SE.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Prisjakt</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAC9FBMVEUAAABIlbVIlLVIlbYA//9HlLVKlbVJlbZJlbb/1ADdJynEISj/1gD/1wDfJylIlrf/4QDdJSnBFirDISj/4AD/2gD/0gBDi6rbHCtBk77cHyvfIyU9k8L/3AA4iLT/2QDcISrFGSHdHSA/k8D/zgA7ibLDHinFHSTAFCrfICP/3gD+zADbHSFDlLtOl7fbGizCGSrgJSc9kcFbm7hKlrZHk7M/iq5Di6nbHivcISTFFh3vwgZHlLVEjq5Eja3BGCrsbxjifRT8ygH/3wDCPEPXHSHZGh75xwJWmrhSlrTQJCnCGinKHyhGlbg3iLeXg5Kop0fHNDrTIyjGIijSICTVGh7pvw3/4wBin7talLFUk7FFkLFnoZilZnN+mnCnYGygp1a+MTm6rjjYJSnfLSjiPyPswAqPjZ51nYR6nn52mHeqWGPBsDTLLzPQJyzbJinUJSnHKSblTSDPQiDcaRjtdBfdbBfheBXvgxTzmQ7uowz21Aryrgn6wgVnnrk+jbY/i7B7iZ2Ig5WyWmS3TlbJwjHWICfKMSTVVBzXWhv1oA361gTzxAT91QI7kMBZl7RqmLF8lqtSlquHlah3kKZal6KBjqJfmJyAiJxckJGOe4xikYtxnIhvm4Oic4GhbHmKrHN7mXOvZnGctGOyV2GOn2CrUFujs1K+Rk3BQ0rAQEexrETBwT7FOD7AKDDNwy7FsCzVxyvPGirNJCnmUx/RRx/teRbufhXkhBPliBLokBDtnA31pgw+kLtun7h2nLOKkaRTjplqnY6WeYhrm4ebeYepa3aHoXGMomuXoVa/SlO3Q0zEwjvDJi3IsyrWTx7pYhvecRfivRPjgBPwqQt2ma+FmK1hnp6HiZuQh5eDeYuBqn2TbX2yY26wX2qQq2SVpGKjU1++Nj7hNiXfPCTjQyPjRyLdxx/pZBvqaRrufBXwhhTxkhHxkBH2rAv1sgn6vQf5uQf70QJekaxSjpuSnlu0T1mnqU/KKzHeyB7dyB7lvxD8AUqoAAAACHRSTlMA8si8AWQYZSQrCYoAAAndSURBVGje5ZplcFpBEMfrxR6hOAQJBCgSCCQpbdo0ntQ1dXd3d3d3d3d3d3d3d3f90ntw96BwUCrMtNOdTicZXvZ3t//dPXlkypQpW/YsmWlBsMxZsmfLBCxHVlrQLGsOMP7g+EeEbJly0oJq2TNloQXVsmTKTAuqZc5EC7L9N4Aqo3pfm9MvtXmLokVbNE/tN+fapFFV/hyg8oTqi8rGxqpDbWyHhYaqY2PLLa456e2fABQeXbMM8M1meBg7NDa2edMJhX8TMLLPojRDKMOH2Qxpi6dU/g1A4T6paQY2w5+FRiyfUvhXAeOOpMHB+0WkHekd8yuAkTMHGBgBmWFA9Uo/Dxi3WM1mBGhs9fLePwmIaVjWa/i5wvKJHZYvLJfXJMqWsvwMoMiM8qHfO5eKpfXqr3uU2L594qN19euFifN9D7GlV68SOKBIzQg28ClFPsIiGW02bOsuFBhT+PwUo0DYfdvGNvUi4efOJ3NFLKgSKKBI0wjgVLyhk3OUUnH9ja+VHKOSoENjKY18waun96WOB3JFtu8kDWMwIuZF4wC48auB/7DnHE4ncS4AetBZyBHQvUzAGT6kjVgK/CcaK3ZmkIQFRQIBWGaks8HjQzh0FiBE3n1G8Ak61lgpys71IyMT5QSd86KelMFQV7cEAGhYnvS/jUN64G88lcxh0X0ai695nCgn+ZyXgMAuX+rHgPPlbIywXEM4MA4CJd2fCQmBwDEAMAcwLFu5cT8CRC8xMHLle87xMWKH4T/jdAa5ZChT6QeAmekgbTZw8M7pGo3JpNEI8ZCKTwFBXdPiFzC6HBsAEo1eshJ0U4UTpefXaNKkxvzSqyqY6F5PgJQIYwAZLvgDFD5icCR2BznxvXthgTo1dtsVCoVWC/6zv5tdp4GQ8NA7aQ1ZFYbUwngAlUFgFK307dwJLPqw0pO1Crsor0gUH1+wYHx8L4V2cukCdPdA8ZP0zJYMYOmlfAOiU2EHKqtv5wKwNKumKrSivHnjq+aJo9FCQkJotLiqPUtObWZyPSToqGfWda4PZaJ9Ahqmo/67ZrCA8l/osB24FxXMA3yjJ8kft/d6P4yag7ydvi6YPZoCHlB4kQF1t7vJShT+ArNL2vOK4oF79BxilBjRpAEiEG/uSdESlxrtAzA6jQFN/IxD+W9SUiQSVYXuPRCy6RSBv1XMgFZ+LB4Qc0ANn5A+QIlOFJpdEoQnD3CPNVljKkqCk/ngn6v7WrCA6OY2NIHOfKTvYdJ/HPSPI9wyQUJKlzVsqODaHljAhDSkQH0qx1fZ7d/7D/FglTA3E8LBEG0HQAfpDbEAKkKRG6ECxLCpWpFbfCRR4WZzeJTEnSCZiGTgbLaiGF2PwQCKLIZFkIvxGuaosLRClLcq8i+RVJu7bPXqZXOrSdwRsloaKEL31sXZqBQwgB3l2FDiNgQLZtBkbd549HnUxWVDCdKUQ49Vi3IBzOMbELAWBnGLOnywB+zEAHrHoghRzbSOW4CirnZVUq2p66wolxbhS4UwRpu4PGe1xfbBAPZGoCQdYnSqZqqhyFuQ8j/cMS0k6KwoF6BRMuGcQTcdL8PRjyJmYABz1FCCet2dQyUq7LajCUgudoX+IaFrNQkVozFnCFg0A3W8tc5KwAD6GTySlHXCLoIKhEiOefR/or/ZlVzNqES1Mnll2UDlRRhAKiyzfOtQnyutQCkkqTbUa33ZI3HlkRCqPJjLZDKBDLYyGEBzNizjU0aYpPMVKEJRc73WfuXCERTgoMYZP34HAOBlgDRqgQG0QIDEFNgmaihEcVDHZd5raP+eIUjlxskQ0JGcAZCB3QoDKIoA7SHA1ESLJDCv9gY8LFgCAaYVgluXJBJAylA2UECIH0C8TwCzbqsAQ4SqIPyYN2ClNoQqBPcQkYTiRXEi27xERgC8yCVjcCI77XiAaYoAkmrDvWYwHYnsSlPjYAjIXRtXaKHehVaQhgqtP+EZITvVxiV1UKGtt0LA0QBbRS+qWKt5tIqh+7RxaM2hWkXyQJ0ToJqHAeynmt02V7OLo5r1LMK92SlvKvKEYJodlCA/rtlNQu1a/IRPtWvKS0jULFe7Vg69ieQBJqPa9WYoAS93H/8LjoCOFhwgJCKM2NOfUBLABMTKfYqCbgtOBWrBSWDCEJ3HLZlLqCXzFbVklnTb1Uh67lvY/8vnlQun27VVMUumslBrHUqiaL+LvrgTB01h6vYYtx3F9pJgiQMb36pu+wzJeGrR38JFE7gRg922IJXD7lP75lW9Snx3AUCLiwMbYPdtjKSOEO281sMI8fLjty2VPqGNV74hro3XCOQNb7JDaONlPFeMCY23A791rKlGgDZK19ZR5tf/tAIEHUlMRWiexd/mF+0dkQzT/RBkeypQm98u1ATy7/K1fUd5xAi7pyFQTRVoLCuBd19CNo3yTwxvCxWAOYQD0C6noylEPnY7gNwyS3D+JZJDw+jI5Kf11ARK+T5CLUelEJmodPUFU7OJMi+EWTaxDtAXmSBJj4pgRQ8cAE0B+Ze7dx56gY8Tw8PNrqo2h8vG12og/O4QeJarQxPAAZAKBqd/o9cxdmnjMWaZLByYLEoyptHSChqWxzE5icuDCmAAyC6Q51hp+xTcQfxrs1qHGjdq1PhgrWZnkoWYg/hZkEc8KoXwAEt1NQA8qYi9SqALNabkZJNGSCewVwmbAEDV1IIHUOVchrwM2VLxFy5DthbTQYWxAGTjytnYZXVd0HWOnBD6v86Ro+ucbhnkBMb++EKqFJBhDe+c80KqY7s3fJYfQMrw0+3ghVRrK8ig/YFcqVUHV47FeV0qkomh1w/cKkjxgWAZ5V3a6vXkzUnFbg7/ty0BXQo2jQUE3RYOuVHLsCac7ELgLgWVfMG5QVbQH/TtjBW3ZpD+P0QHdq1ZZQFJSEg6y2W2bMVjJljbbu4u58hdqcMiwK+Ftqwv5uyf+tObihUD/vtVCvRitkpTEKW1CQlgq88uCqrHym19clO3QoSc7zAjkdxt86DW3AQmNKtVRzZp4B8HwBKqp9sYRTMcJ7riZH3qEvS6gW0Hd+iYlNSxw+D1A3Vc2BiQgfhH/8zluKUUuNt3nOfYLTNIAgmxcrl6xz8wYA/Lrdpf5Cev93uXgdf7bCDDj0y1YuzPv6CoVBO+/yBlwBs1/KajfuUVi2VsagRAQBl8u89/Z5flF18SjZxSxoFomeFn9Cv2Vv6N11yVp6Smhdp8yMBjqlR39vb4zRd1IyctaBGhLsfzQPDA2FXMvpcq/4FXjTE9Ll1friquUuXODcMCfBdX1b7RZ5Tlj70srbzzysy+R2sfd5y/ah/tO/PKzsox/9Tr3r8ZEOwvDQT/aw/ZaUG17EH/6knQvzwT9K//fANILzIyPu0V6wAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.prisjakt.nu/plugins/opensearch/suggestions.php">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://m.prisjakt.nu/search/{searchTerms}"/>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://www.prisjakt.nu/#rparams=ss={searchTerms}"/>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/qwant.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/qwant.xml
new file mode 100644
index 0000000000..e5b9dfb328
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/qwant.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Qwant</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAYKADAAQAAAABAAAAYAAAAACK+310AAAMKUlEQVR4Ae1dC1BU1xn+7/JaBBdEUVgUHygqCr6ijYqv+JgEX4kdbfNobKZaptNxJmYybWKnCdWoTWxtOpk2D51JbEI6OrW+U19RSXxgSEYFg4RIFOWloMAuwvLc/v+VJYvs5Zxd7rm74P1nYO895z//+c/333ue/zlXAk5K22UPLKiCOfYWWIJJEiQAs90OZrwO5RTRU9lqJAlK7AAlWMBcyQD748LhZNoKqYGnwIhj5/TLD+1RLTZ4rQXgWeQ0dc6tx7YiYDEApBuMsP6jF6SyzlBRNMCaz+xBlYXwB7TuS/ikh3QmRI9zjQBidw+x29pnMGx8J0Wqd8Xl0gD01DfZYA8meNRVIj3MbQQy/Y3wlKu3oYMBnt9mT2pphkNgh4FuZ6MnUEZAgiJ/CVI+SpVynJnaGUB+8ushSwffGSIVr8kIQTDZ+U3AtuI+UZ0vVzv6k++ARP1fxJYwJqwdwtsMQA0uBup1vgMZcb+PtmIt5yBXQVT1NNfDVb23Iw51Z8nUO/ILguFUFclvAPXzdfCdIRJ7TVgT5pSLRCPcq3egHK/1QZZY3B+UbhneFyINNL2gg/8gNprcmwh7Q+vcjiY56pm0R4CwpzYgoX2wfqchAgkG7AbRjKZOXkCAsDdgi6wbwAvgU5aEvT/++vR8vr2lCe4UZkH51dNguZWHf/lgqy6BRpsVmuprwIBj+wBjbwjs1QdCI4dD7/7xEDFoIvSPnwXG3v29BC13tqFkAJ8jAr3k8mdwPetTKL1yFJob7inq2NxYC/Rns96SDeTMGG4eC7GTfgaDH3kGevXxzblF6bl38UXwESLgC85sh28Pb5IBVU0tXKaKnfBTSFy0HkL7DVNNrBqCfMYANy/shuyDr0FN+VU1yuVShmQIgOHJv4Yxj6+DoNB+Lnm0DvS6AapKvoWsT1Ph7o0szcoeYDTJRhj52FrN8lTKqG02VIlBZHhx9n74/G8zNQWfytNos8DFva/A2Q+fwfajTmQRmbK9ZoDco3+G09tXyD0ZppaCGKjaO/H2HKitKhaUA1us5gZoaW6Eczt+ATkHX0ftvN/+3715AY79ZRq+hV+z0RLAobkBvt75W7jxzS51ioK9GzXIZimDjHeXQE3FNTXEuSVD03HAdyf/Dtcyd7iloIO5d+QIiElaApFxyWAMM0OvsGh5oNXUUAt1ODCrs5SCtSwPinIOwu38k9DSzOUX5RAPDffuwOlty2DeS1+Cf5B2Y1PNekFlOKDKeG8p1jro4sVJBr9AiMNuY9z0VRAWNZozFcjtSlH2Acg9vBGs5d9zpyNG89iFMGP1blwpwZkaDUgTA1ixb39syzTsfVRzFknCEexySFxIA6ehnGk6stnRv6bg7Ha4/L83oN56uyODQsjo+b+HpMXrFWLVDVanEmXodHHP77jB9wsMgemrdsLUlR93CXxSSTL44cArFZ5YdxGrrhkMLX+Mzvv8r/Kc048h4q6EG6C84DTO6xziKkFIxGCYtzYDBiZhVaUiBYX0hTlrDkPctF9xSaUpkZwDf+Ti7SqTcANc3Psql47B2LDOXXsKwmMSufjdZZIM/vDIz/8J8bPWcCUtyt4Ld66f5+LtCpNQA9y8+F+4W/gVUz8DOk4mr/4PkBFE0/hlb0HUqPlc2fA+PFzCFJiEGiD3yCaFbNsHT376PYiIndQ+UNCdhGOHaS+ky2sHrCwqfjgDt7/PYLF1KV6YAaqKc4D+WNR/xGwYMvlpFpuq8QHBYTD+yTe5ZF7/6hMuPk+ZhBngelY6h04SArGZg099lpjERdBv2HSmYKpGW5pcuvYz0/IwCDNAWd5RZv6Dxi+DPrh86C0at3QjM2ta9iz/4SyTz1MGIQaot5ZDNc7zs2jIFNr15D3qN3QqjjXimArczj/F5PGUQYgBKji6bzTgiho1z1O9VUsXk7iYKaviWiaTx1MGIQaw3v6OqU/06AWyRwOTUTADTfCxyHqLXR6WDKV4QQbIV8qvLTwi1nt1f5sSeMGjB820UlsggoQYwGatYOpqNEUzebRg8AsIhoDgcGZWNmzXRJAQAzSh0xSLgnE+31eIR5du9QbQIgmLAkN8wy2E9KTJOhaR85cIEvIG+AUGM3VtrL3D5NGKoeHeXWZWBn92mZhCXDAIMQD5arKotqqUxaJZfC0uabIowChmmVKIAXi8zmzYs/AFam60QWNdFVOVoNBIJo8nDEIMYEIPZRZVojuIL1BlEVsPoylK9sAWoa8QA5CLOItKrxzBSS73PBdYMj2JL8bFexbxlIclQyleiAH6DvmJUn5t4dStu5V/ou3eWxfkHsmifkPF7V8XYgCjaQCYONxICtH/35tE3nA8biv9R8wSpqYQA5C2PMt+heghV1mULaxwLMGX9tHpDJ0TTRpGxrHXDTqXohwrzAC0K4VNdsjev47NJoCjNPcILjeeYkoeNG4p0HSFKBJmgIjYCRAWzd4BW5Z3DMhLWUui9od8lXhoyJTneNg85hFmANJo9IJXuRQ7n76aa/2YSxiLCXdknduxssN+MlfJqDMxYORcV1GqhQk1wOBJK7iWHGkT3pcfLEP3QTEzjs5oXUKHq5LLB52DFK/HLd2kGKdWhFADkJK8haitvAHH354NFvRwFkF2dAq+sPtlyDu+hUu8eUyK7InNxdwFJuEGGBA/B6Jw9YuHaIPe8a0zgBpINamxrhq+QP///Ix3+MSi71Di4jf4eLvIJdwApN+Ep7Zw+9zT/q0v3n8Ssv6dKvv9d6l8WN9fO/8xHN48Eaix56WRc16EcPMYXvYu8Wnink4aFuccwA0Qy/GKf1sSdf/iZ6+R9weQ4y4vkR8PbfS+fHgDl3eGs1wav8z8zX7cHqDJswmaGYAKmXv0TdwbJh8U5VxmruvwmCSIScQdMsOTZR9S8iOlaW/yZK6z3AKaXa0uzZMbWNbueqUMaRfO/JfP4BJlmBKL6uGaGoC0z9zxPBR+s1OVgtAbQtPJ7rxVShnT3mHanmSKGqXEIiRcm/fMSfUpz27DvV7q+P/f3+PLX6U5qdHukub6qdrRGnxSQnMD0Okmyat2QcKCV9qB4K0bGq1TtUNect4gzQ3gKGTioj/hNqRPhM6zOPJS+o1OeEKudtxp4JVkeRruNQOQwrQRb+6LJ4TtilEChWY4ExemwczUPdzdYyVZXQ3XvBF2qTD21+lsoJxDaUAjYlEkSX4wbOpKGJPyOgTjMqMvkG8YoBUJ6r/nZ/wDrhx7CxpqK1XFJzrhcRi3ZDOEmdkztKpmzBDmUwZw6Epdy6JL+/CtSPdo17tDDvXrB01cDkNxSjk0ku2G7kin5a9PGsAZAOpqVuAGifKCM/JEHXle11ajsyy6P94/jkBqOzMuBE/DCotKwDYlCaeRH4OQiFhnUT557fMG6Aw1OsCP5nloQ7ZoSsbN41SNqU2aHtahtvKmAfH4xI+EqpLLaovuIK/o0l4hBvBqN7RDKT0IUGtUzcq6mHb7CzjfsNsbwIwTdFoQHfZRce2c6ll1ewNEDBqPZ4Jq09gWcThxuWuhbm8AKjDPPi93gXHFz+NF5ypdZ2E9wgADOTbadQYCb1xNRQE2+Oztt7zyiK9HGICOMQvsFeFOuT3mLc7e53FaVwl7hAFoHGAem+KqfKqHleSwnXndybRHGIAKrPYhT0og0jGXtZU3laLdDu8xBogaPV+ztQU1G2OaiqA9pWI2QLn9PDxcCfBgRqsB/5U8XMX2qdKWGnBJWzeAl2xC2FMbkOul/PVsEXsDOoCp26/SYeVGgLA30AfoMYWFO5XOqBYCFsLekLZCasB6iOeAN7Uy1uUgAoR5K/Z4Y4T12Bu6pyOjDQKENWFOuVEjDPRdW1xr2ErXOolHgLB2fNZcNgBl2Wcw0BGC4g5HE1+u7pJDZivWsr7tDsmnL2uja04WuvD75lfPugvESnpKUISusZMdTz+xtb0BdCN/YtsPFgIy0r1OKiJA4EuQ4gw+SW9nAAr412opm6yEl3p1RICoQ5nyk58q5TworoMBiIGshPXUbLzcoPeOCBHPqBW7DYTlg0++Q2K7NsAR6PxL7QJ9gL4FgI65NTnH6deKCFjwyU6nrqYS8I6UTAM4GNN22QPpG+itn0BPwIRm7E6ZMf5hn8quwSe9pHVSM5emF2iES4MsB3ad/f4fDbO11HHkpjkAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://api.qwant.com/api/suggest/">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="client" value="ff_android"/>
+</Url>
+<Url type="text/html" method="GET" template="https://www.qwant.com/">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="client" value="ff_android"/>
+</Url>
+<SearchForm>https://www.qwant.com/</SearchForm>
+</SearchPlugin> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/rakuten.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/rakuten.xml
new file mode 100644
index 0000000000..1b6d0e18e8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/rakuten.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>楽天市場</ShortName>
+ <Description>楽天市場 商品検索</Description>
+ <InputEncoding>EUC-JP</InputEncoding>
+ <Image height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAJMUExURQAAALA6J7IzHbIxHbMxHrIyHbMyHbIxHrIxHrIyHrIyHrMzHH8AAK8vH7IyHbIyHbQxHr8/ALMyHbIyHrIyHrQzHrIzIrIxHbMyHqoqKrIzH7MyHbkuF7YtG7MyHbMxHcwzM7IyHrIzHbIyHrIwHK80GrMxHbMzH6oqFbIyHrEzHbMyHbYwGLMyHbIyHrMzH7IyHbMxHbMxHrIyHbMyHrMxHbMyHrMzHbMxHrQwHrIwHLMyHrMyHrIxHbQyHrMyHbMwHrIxHbYzHbEyHbYkJLMxHrIxHrIxHbIxHbMxH7MxHbIxHrA0ILMyH7IxHrMxHbEyH7czHrIwH7MxHbIxHrMyHrIyHrMyHbY2JLMyHrMzH7EuGa4lEK8mEbEsGLEsF8JbS+Cro9+ooNOKfr1NPK8nErIwHM97b/////z29dmXjbEtGM13at2jmq8oE/vz8uOzq+KwqOW6s/rx8MViU7ApFfLd2rAoE60jDq0kD71OPPXl4uKzrPPf3LQ0IK0hC9WNgvHY1bIxHa4jDtCBdPLc2K4kD60jDbMxHu3Py+jBuq8pFPjr6tGEeNCAdNOJfe7Szs15bO3Qy7MxHf/+/urIwrlCMLAsF/37+vXj4Prz8v7+/spyZKwgCvPg3bU3I71OPffp6Pfp57pEMrArF60iDchrXejCvLAqFq4lD9yhmNSMgbIvGu/Tz/z4+MFZSc56bvbn5b5RQPv19PXk4rc+K7IuGrEuGrxLOc98cMpwY7AqFcRfUM99cM58b9CBdb1MO7ApFLArFrIvG0f5g2cAAABZdFJOUwANRoKwz9/dyaZ1NgIgieBtBHru2VUevpcGKLYLHN6zBcF4/j8d8koM5EXvFYOxc9CRn53Ki6hpdjo15dSaM6xU8SNgB7mGtIxix6onUeObOBlJlKCNaz0ObmhQwgAAAAFiS0dEaMts9CIAAAAHdElNRQfnCw0VEQ11qlZgAAAB1klEQVQ4y2NggANGJmYWVjZ2Dk4ubh4GTMDLxx8JB6wCjGjSgkLCkShARBRFXkw8EgNISCLkpdgjsQBpGZi8rFwkViAvCJFXUARzo6JjwCA2Dq6CD6JACcKLT0gEgaTklNQ0qAJlFZC8qhqEF5ueAQGZWdkwFRygAOGDclJzMmAgNw9miToDg4YmkoL8gsKiYqCKklKoIAsDg1YkkoKy8orKquqMjJpUqKC2DoMusoLaurTI+oaMjMYmmCuYGPTQFTS3ZGS0RsEU6DMYoFrR1t7RmZHR1Q0TNWTgR1bQ09vXPwHoyIkwN0QaMcghK4CCSZNhNkQaM5hgKJgyddp0eGibMpghK5gxc1ZGxuw5c6PgCswZLJAVzJu/YGFGxqLFS+AKLBmsULwZuXTZlIyM5StgbrDWYBC0QQ2HlauAzljdBhW0BUaWAKqCNU1rMzLWrYdEl7IdKLVDQyJ2Q0bGRmBIdm/anJGxZes2kJg9OME4QBRs37Fz566oyZGRu/fs3bf/wHagkKMTJEnZQpLcttTUg2DWodTU1MMgnzpDE62LK/ZE64bIdO745YEZywNDWtsTNe9peaHKe/ugZ14eXz1tmKyffwADNhAYxBUcYhQaFh6BJAgAIIcuyjSYZL4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjMtMTEtMTNUMjE6MTc6MTMrMDA6MDDL58yPAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIzLTExLTEzVDIxOjE3OjEzKzAwOjAwurp0MwAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyMy0xMS0xM1QyMToxNzoxMyswMDowMO2vVewAAAAASUVORK5CYII=</Image>
+ <Url method="GET" template="https://pt.afl.rakuten.co.jp/c/013ca98b.cd7c5f0c/" type="text/html">
+ <Param name="sv" value="2" />
+ <Param name="p" value="0" />
+ <Param name="sitem" value="{searchTerms}" />
+ </Url>
+ <SearchForm>https://www.rakuten.co.jp/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/reddit.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/reddit.xml
new file mode 100644
index 0000000000..4f761ba236
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/reddit.xml
@@ -0,0 +1,11 @@
+<?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/. -->
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Reddit</ShortName>
+ <Description>Search Reddit</Description>
+ <LongName>Reddit Search</LongName>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAD9UlEQVR4AcyYA9QkSwyF69k2j59te/Bs27Zt2/batm3btu1v+55eN6p6uPec/P/MlJJCchNTCHCj2YGsuYq0+dqTxp4MJ2VmkDZLkfifh6tNfdRXY0w5weVmD1LmHtKmticLvc8kEY3RWM2huUqn+JVmR9LmDU/mSZFCiObSnJq7eIrfaLYibR7yZJIWLYZobq2htQqr/BXmIFKmuyeUSLprzcIonzanW3a9mKdxen7Kp8wdnizWhGWSxdIhH+XZTOSOxNem1DtvkcXSyf3BFvrOP3A0PHMOXLFjsO3q3eCTO+C7x+C6PWPfhHSzusqieJuxg0FYsRyG94YGv8IX98FjJ8PwXqzD1LGxRki3WBcrH1xw5bXDq1bByyl4/gL4/SVoVwMpG4rvH7d5p4ciI2xR3OVLl8DyZXDZ9sG2bx4mAF0lm3sNi9gK5UV5gH++BkO7h7ddtQuMG8I6TBoJ1+zuEiPeCBCzPLlNtHSsA3V/jG6/cmf4/F748n5dN2futBEBFCMsmgucMQk+vasYc9+z4fWpXRTlbz0YhHsPLwbVqL0uGYnm83nKO9fB/NmQ2qIYBiyU7kbZUcEmlaIPHgNvXQ3fPAI9m8O08b5rfPsaeOg4SG9ZMCOku1GKl9ckl24LH9wMnerBvFlYoRNR3w9v1dh8T+FrGdDYaWdfzcJvL/o7md0GMlvDL8/B7GnkjDnT4feXfUMyW/knpzXeuELfXQxoLAOGx3YSf+nbho0wsi+M6k/BoFgwtAcbYVAXa0yQ7kYVg9hO1T6nbGj4m+0UZugEltqIWNkwY5LtBJbaDZg0iliIlInviBaIsOl7dF/1UV+Nie8rzJ1hNcB+hWp9SwykyEb9pWAE1BYgerFo+o/1CtkfsXarfwciIC4T6B8Jtbn2HT0Art/b+ojd3KiCz6ypbrv6StrxBCyndeVOTm7UPZDNmmK91/4bGEck1ObSVwnQZds5BjJXKjGgIyWDDHOmEq5krupnlAytKrmTOWc6fduhSsopCZ44zZ1OJ0po/n+PUMiwmw+Av9+AxQuJxLKlUOMruGEfWDCHUDT5O3lC45xSyht1aUAoWlb0SyJyq8oD/n8X6v8Ctb+DX1+Ad6+Ha/eAq3eFRr8TiiHdlPwnTykTJfVioo3+iKbKCj6iyo+cCDcf6J+M8oD3bhC3iWavoth+THFP6vMqq3x8O0yfQN6YOxO+fyKY6NjLKvkXtkS1lRMoaiaGSij/vGWrxLkXtvIuLerK/Pg0tKqsXMFnkksX+w9XQXDMQGhf06/MPX1WDnny+tJi4uJu+WX1EIO7Q3p4fUhPcAzpKaaRO8k38NOsoxPd9F9qMLrYY6gttwEAUidatlp2alsAAAAASUVORK5CYII=</Image>
+ <Url type="text/html" method="get" template="https://www.reddit.com/search/?q={searchTerms}"/>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/salidzinilv.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/salidzinilv.xml
new file mode 100644
index 0000000000..436261f8f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/salidzinilv.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Salidzini.lv</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAC+lBMVEX09PQAAAD5+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fkAAAD/c//88Rf/fP//Zv/++joA/wD+9yf//0n++TD+9yr/cf///0v++jn++TI0/zT//xoO/w7//0EG/wb/af9J/0k7/zv39/f//0Yc/xz++C4W/xYA+wD/eP//ZP8l/yX//j7/bv8t/y789h4AnwD//BpE/0RB/0ECCgL99R/98BcFAwX/ev/88xgS/xL++jYzMzMx/zL/+CEq/yv/+RkODA4oKCj79Rr/9RgfGwP/f//FUsUj/yOxqxL/gv/j4+Pb29v7+hmopRAyMAXf39/X19dWVlY8GTw4ODgdHB0ZGBkICAj2bPbPz8+6Ubo9PT0pESkpKAQiIgPu7u7q6urGxsbBwcG0tLROTk4vLy8rLCv//ige/x62sxIREREGMAT6cvrwbfDrbuuZmZlwM3BBG0E4FjgjIyMIoggODQLoaOjn5+feYd7UW9TS0tLKysrATsCkQqR0dXRqampcXVxISEhGRkYwFDDg2Rvu6Br/9xgBFwLl5eXjaeO6urqnp6eeoKCXQJdRKVJBPz///Dr//iAUwR9kXhChmw91cQwIhgqchAkCYQcFcQUBQgQD7wMDzQIWDgEGBQDy8vLLy8vJYMqQkJCJPYl+NH9iYmL//jIbkSILUBCRgQ1pZg0FvAcE3ATMzMyxsbGxRrGsUqyCgoJgMWBeJV5FJkX29kA97D708jIt4i4a7SgiCyKvqyEbdh8pmR4erhoX9xj29BgaYxKVkBE6OAkWFwOSkpKNM42KioqHh4dM/0xE90Xb1T3a4TrT0CkL0xKDfw0kTwcEqQT3f/ewaq8/dD///zk7yTgy2C8U3ywlzSy9vCsOpSSNrSERsBuFhBvw7BjHwxbQvhRCbxTNyRM/Pg4Ucw2qjwwIlwhUUQhDQAcG5waJv5WPVI+FhYWCpYGDU4Ggs3FFpUX69DF1/zDs6S2alyv++SpDpyg/1COcmyN6mCFIRhQK+ApVTQdGTAfK22jvAAAACnRSTlP+ANIGb2wbw8DGUqGPlgAACaVJREFUaN61mndUE0EQxmPXTAKIcCaeGj1Eg0SNCgFFJBBjwYJ0EAUURREFG4IK9t5777333nvvvffee/c9Z+8uRIq+O4jff0nuzW9nvt3Zy95J8uTJV6AwI/0PYgoXyJcnjyRPwbzS/6a8BfNI8omJHzVcIhWlvPkkBcRc36O3n1ScCkgKibi6uz80aygVpUISEf4OGa4H6CuOwAivqaT7YW8AJLSUipFAAMPMSfCPA9jfAgm+FgcwUcPWBoYAdPt5v3ETgH5+FgYMGYGDx/BNTg+k6cbNAZ22IICJPugBqDr7bw2gabmBblwHYLSvxQBDhvVjBz9++4fYWE93udyLJ1gIELU2GMM/uHVy86JFsZ9sXdzlrlq6PjrdzNciAOaIHmbt/3hio4Oz0s7W1t62spwloA+BfpYA9AyCWeOPFy/mUMpGoXBkCaRKLMGjae4Bkf4A408UJwBnGxuFHSJYwiAtqZKHX64BmMCBaw7FihcjBIWNQkkILnLMoRHrw5BcAnwXAkzc6FCq2B85YJF8iNMsoV90rgBMGCZw0tl5zBgHjoAAMwF9ILM1N4DuR3QwfvMihaKmM58DApRmQrv9AIE9mJwDfJtBt/p2i5QKG5tS6ENxJNQ0EXx83Ok9TwCgdVjOAS3joMmAWKWdiVCMEGoq7TiCduDEbjCT9KWcAiQLYdbpyrH2jo5/5mCj5HOgBzSfdZr01tENcwiIjoMWd8Z6eto6KjkCGsFWiXPaa+CEfbfpdk34HMQDmD4Ay24TAJuDghDYJW3yofLYsV5eLCEwjBEPYBJCMAHay8fT3h4R5io5pxPQaLmBJXisFAuIXJkWjAns0RoMBhdCMDv9B0GOMvB9SSSAbAKzngyg3UkMl8w5KEwE/JnvfM38xAHmxIRAt3007Son4nPgfeCrZGfKAVccm4MYACOJxhSa1G+kNRFs03NARMYqkTXdhKw4RjhgWNraGB1Ai8aNBsnNBOKDs9lp0lr5KrGENtGMYMAInS6E3egb02aCPQ7ahvehFPbumgjgCYM4QphgQM9gAKhenRC0XizAYMsRsEoOxbFM6LQNT5Bz+wPxoaFQgO9oGDz1bJVJ7ZHQiCYR3N1deIICmzdZcWR7QALvg1ejdg/IHicQ0L0ftJ9RtGiF5UiYYHbaTCBzSZGBQNP7AKvkKwzQowtMe1YUNWMaEmgvnkB8JgSsEjHaWYEEpKIPPgb6/lMAXUyUMMBBgCkVihKtmIJV4gmuLvaOOP1r2iCB36ZNOWDzXgYQPMJXWImiYmDw49KcOILW1dWA8rTnjFY4m28E7AjBha5/AOJaCZ1Fw+Ng2oySvFZMZQk0TWsHDSIEpWmycjcCSkcsUywmoFsodB2QRj31Rw2Tpk+qjk4PmLh0oNbg7mLryBptJtRU4pKO3V4HujQVuJIlrYKg/fLy6YDyu5GwdGL15tj7XOVmArcBcX1p7ESA3kJ7EdMbE9hd7k91HQxTdt0YaCCrFjcgJbekkcDncPHkUwhqJRTgGwiDd1Ws96dq75p05T2t5SarpycSMvpw8Ws36DtbKGCeHpZsqVQ7oypt3fnorwTnzeMh5KXQdj3EH6ovrlWiVgaVKLFlycwJZgK3pBVca3VwuNYCu7VQQIIHtL9atkQWXZ02k3QNnuCYTkBtXIYWM0IB/sGwd1O1slmFOUze48V3b3PzRgTO0TZosUBAGz3sqFa1WlZV3bR35uRGXpm3h1LFj38nCQgGeIfAjmNlstPRTTuQoOUJKDvlmDGlNp54MwrAH+OIKdHR7AnHFh+Y7EO7mjbR2EXKzcdvfHk+NwDiEgQDEgKh/aUyVbNVmWOL6yzdwxJcPcfGvhuw/dvny1tHaObrIGa2UED3PjBrMZqcrapVu95iqY8W4w+iK99dem/nuCuVLp/XJAdA0AhhANRhstBK/FXX20/20hpo1/oTMfzWEmUrvp0rc1tAWpGoVlGr0l9188xkA313/JnF42rXwsuepSUZG4Rvg0A/Mc3uYb2Kf9e4vfse/ZoyjvtQ7uZ5ayur0PWgR5tFtOtxbcv9RW3btj318MykrW1R5dqWm34hWU1RbiNDoC+GErHh7C7/D51abvq5xvIXoUaKskoJIDum4C1TD9NOlRSk0jvmytSUTBY/H6BfpFBAJG76VUoLUdEVfZJIfJmx7jbQz5MwQgCoeextSyZVmLF8epbvzq6Jd5NRFCULxRT0aXNmCwM09YZpWYJNXxI8IwtgZ7KGolRO1k6q5PU60AW2FHjr2BdvHStkVNEVo1MvVcik6ReSVNbW1k5OTipNeAhA8CphgDBvvPmtkkmTXru96mr60JXV2ccxSTKVEyJUqrqdALybSgQBmIXk7j2z9AuM4cHVI/pHdN6wYcOodajExMQ1oZgBSlO3M0CXOQLXQVgzvT4EUPrEBQsOHTo0MjU1dXWvXinq0JSU5KFDhyYlJXUk6tChQ3r8AIzfihE4TSVhLVst1AFsG8nGCA2Nj4/HuUhRaisrYwNOVmqUm1pDwqs0pD5dhkmE/8uU+PkDBKXGUxpWFAaTodRGo5WVmxVKzc5Niqs/O37vlYyYv7H+OOcSZZQTKxUlI6IwNC83GX5ljo/1byPibywqOgZAF1CXskY5qWSc1Jni4/zh6t+fjF/kWUXUvC4AozrKyPi54WeILzPHp8j4vVuJPgyJHBEE+lQ1hQZw8dPrY1RzRH78vcj8XyURC0DCYYBOQ3GwmeKj3RQHQHHrq3VLSU4OpGZ7g361LEt9cPpwjnRInZ/Mzh+PHjk8szsIsL6DOlt/SeWGboD1qzvj+DF+zgBDgmBbChbEXB8skMlfJ+vQkRGgw/q3lOYUENUHdCPj1RnGb/YX++cCNj6TYwAzTA+dOjbAuFnGn94f2vTM+cEspuABujVWxuzXr6puZx20aZirw/HItSEwamiDLPFZQK8I9NdPmisA08MbUzAas1m/GoxP+k+uAFijNICAlAZGc/kprv9T4f2Jv7l/xOKHKYyae66B0aimzPFVVHhnEl+SWwBqVTBA//nJiJD90Z979cf6N2WkFgAwq7CrhgQkhser1Rnik+MzCwCQ0NAfUBGjRqao1RoygTThEWx8ywBQkQl9dYjQByTWlcmsnVh/W/e05MNSpvvwNA82jcS5GtZfj2jGkgC8aHbPIzFoN3RaHR7APrix/ANriV9CnyBMIoI7prY4gKjhcLZQzaKlYiQRkS0T3UXk+DG+pIiIq5l5cYFhUlEqIskv5vKerdMipaKUn7x68h+VN9//f3kGX//JX1j6X1QoP77+8xv/QAZF0b5pYwAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="GET" template="https://www.salidzini.lv/search.php">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="utm_source" value="firefox_mobile"/>
+</Url>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.salidzini.lv/suggested_search.php">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="utm_source" value="firefox_mobile"/>
+</Url>
+<SearchForm>https://salidzini.lv</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/seznam-cz.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/seznam-cz.xml
new file mode 100644
index 0000000000..98132cd2a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/seznam-cz.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Seznam</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAw1BMVEUAAADMAADMAADMAADMAADMAADMAADMAAD////++vrNBQX//v7dVFTWLy/43d3zw8Pto6PsoKDhaWnTIyP99vb87+/1y8vxt7fSHR3RGBjOCwv77Oz31NTrm5vmg4PbS0vaQkLZPDzQExPqkpLnh4fibGzfX1/eWlrXNjbVKyv98/P65+f54OD32Njyu7vwsbHle3vkd3fUKCjPEBDOCQn54+P1zs7tpqbjcnLgZWXaRkb0yMjvrKzojY3XNDTniYnlfX1mR6ZxAAAAB3RSTlMA8si8ZBhlc+JuAAAAAmtJREFUaN7t2uly2jAQwHFDSHdlG3yAwZhw3+E+wpWjff+nKm3GNsM6oc1KHzLj/wPopxHWWHiknctlM6CgTDan/en+DpR1d38GyPhSBU37AUrLaVlQWlbLgNIyGiguBVIgBVLgewNj2xsBSRYwXpYNxEkVSHxAf3PtJ3zP0uUCQdutlwVetJQH5F1nZuB1Ii8F6BxfapicxQd0syLw49pcYDDFT+vyAN/CG1ksoFXDW5U5QLOEN7MZwNs/jC9GXwe2BbzZpMV4TBt4q4dDwNjJgYefNXPcIgAwgBN+lFdpNDtwjgcMk1elOwwnzgVGSCr1yOAMAKYEMAFkAnSNmnIBcOhTLxfQ99eCsS9yANqKvAmEk5cJQPsBSc/HjTwA9FWJEmIx6HCBOL8ukCbmbpUJxBWdJAIN6+DzgLhqr4bJxiBgAXFBv4CJea8+D4gzK0YiIeo+D4jzf80SidJSZwJx+V7iUu3WDIAYjRkVHpssgBpkhwuTB1Dj5+RKaHMBeip+xsuetlyAdipcCq58AHQb4+YSAFolBh6VAEURC0oAsDCsBkoAJwIWoASYk2OTXGBskP86UoHNQ/QLrFUA/i7ax3mQD+h9L3x32h2QDmyit6ixGAHIBtpOKdzA9TGAZCDfC8/2ojIMAKQC21a3EI1+JEvPBIr9SrQyi8EGSBygOqxHU7caLR1IDKBqdsMdNd0f2luI4gP6qG+/z1zs7NWpCiQGsB50/36j88rOylyTRWEBnWZj7mGt/PLqtsisuUDRXNbtntscB/B/fa9PyymQAimQAvJSfmlA/bWHHCgtp/rqifrLM8qv//wGWy8ZH4yviOAAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://suggest.seznam.cz/fulltext_ff">
+ <Param name="phrase" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://search.seznam.cz/">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="sourceid" value="SearchBox"/>
+</Url>
+<SearchForm>http://search.seznam.cz/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/vatera.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/vatera.xml
new file mode 100644
index 0000000000..8e0929c0b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/vatera.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Vatera.hu</ShortName>
+ <Language>hu</Language>
+ <OutputEncoding>ISO-8859-2</OutputEncoding>
+ <InputEncoding>ISO-8859-2</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA3lBMVEUAAAD5+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fn/WgD59/b/WwH/XAP/XwD/XAD/Xgb59vX/YAD59fP7zrb+div59/X66d/+aRf/ZRL9kVX+dy3+cyj+cCL/Ygz638/7y7D8oG7/YAj58+/57uf65tr9l2H57+r64tX62sj7xqn8tY/8s4v8r4X8rIH8qHr9jE79iUj+gT3+fTb+ejL+bBz62MX707370br7w6X7waD9g0D58e366+P7t5L8pXX9nWj9lV39hUT63Mv61cD7vZv7u5j9m2X+fzgxJ++FAAAACnRSTlMAwwbS+W9sG8/zKHP4TgAABJlJREFUaN61mutS2zAQhR3KpV1Lli0ncaAJt1ASINxyKVBKaaFAef8XalrobLLr+Ezx+Pxj8PJZK8mf5SEIgqXV2jJVkOXa6lIwzdp7qizv16b3/44qzLulYJUqzWpQo0pTC5ap0iwHVHFmAUYGl+O64hEABLhIA+oihJJf53MBhlrDTrvJabc7v/p4DIZ2xk1RNyCjAZ4uXermk55CgKHWuiiL0/v8EXzqxN1kJlHXTTyBeHpyiUg8oEwDpoRfaRTOxtrkgiDioxNVcXebfA7A01Ec2nmAOyWPOvQ5ngdEbkREuSP4pq/9Cjv0RdxVGPHMqWX6MU3mr423tlGPeqqv0cUCgKFrZ+d7FLrbQoChfsfJUU/qCwH7G6JHSXoOpqBhw7nYyH3nvy8BdK56tL5PpgjwXVRYG15RJgA8Y7dOraPrAoCh+kR1qHPDFQqwvaV6dFe4hh4iawXgBxU9Tb/KG4o/f+MCDTiVuyyMd8gvBGR04qwtKFAZqRva/FQwAk+HCRry/7Y0EDVjNWlNnjQ1xwM5YBt/EVcHouRMbUzLy06ZbKiXdQsALq21YuOciZKCjRmlH0lEAvbaTnTVjb0gLH60WPdEXgBk0a7qUfdw0STcqQ5tHABARjtWraNjASh4vA+JCIygv6l6dC9qCgR1TF4BJKGnFLh1MDcEVqy80m79zAHg+8qVgqEbqYKEZSkAqLN5UsjoasrWsgQAFqfaPVoFTssSAvLFOd3/XqtgDGTJALw/e6Qvu0hUh1iWClAsTsfPYL7sxEWFsmQAfkaGcYMB7KakQJYCAJ/yu6LU/LkIm4MBQJyuvTdf7OkWu08DWJyRFVK4lMXnQJYCAMV5Rl6cCoAsBQCK89nM3p6n65h/q2WpAUCcUdwVj7G7NALbXQCAOO2LFPg0tCk7JGQpAVicQ2KCoR15KhCyhOdkFifLcBawC2SpAEicNnKPZHiETbkKWHsAoMWpe+zz5yiDAC3O/FXi6Uw96MQqUwAszjA+Iv9KN3qfjAi1CJ9Oe2T+7fRuHAFZKgAWp+v0ybzQj9UbXXIBAHoIj+qP2AZlL0a6FypItCw1AIvzVQqGDsSvLMsSAIA4J3+lYOgJyhIDyLM41VuJnp7mHhoBECcvFVaBlCUGYHGOXj9/SFlaLUsMyPR7yd9PI7zJpSwxQItTP3DqeSroUU5wi8yz0+cLTw0rO8SyBAAszo19VgWUJQY8yNOUjR/pZuLQyRIDWJz6bz2EamYeyb9tDvK6UT/J6dvbAPnzeTRMwclSA/AHOU78vB6HokMD8hiAxMnTHFq9+wwGAHFy5M8Jf1rFAHyW14nSEzKEAVicHCHLwxIAeeLUidzYlwLsr6seCVny16S3tIjFyVFHn6wEgD/VcvThrQxAvEFogDh+QoDOvSuchAZlpQCZfIuTnwD6JUfg6WdXA1iWP2QBAOCvx+rsbd4M4I8eic1Pwq8TZQAH3dQuGMHL2yoCwAw2bJwXG41aZQCc1kMjL1eH/M5eAgAayCkBMAtDpQHlU/U/DXyo/t8eVqjSrFT/ryfBWoWEd2vBNEsrFc1DbWV6/78B75KyRqivJp0AAAAASUVORK5CYII=</Image>
+ <Url type="text/html" method="GET" template="https://www.vatera.hu/listings/index.php">
+ <Param name="c" value="0" />
+ <Param name="td" value="on" />
+ <Param name="q" value="{searchTerms}" />
+ </Url>
+ <SearchForm>http://m.vatera.hu/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-NN.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-NN.xml
new file mode 100644
index 0000000000..f876b15112
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-NN.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (nn)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://nn.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://nn.wikipedia.org/wiki/Spesial:Søk">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://nn.wikipedia.org/wiki/Spesial:Søk</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-NO.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-NO.xml
new file mode 100644
index 0000000000..75bb1768f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-NO.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (no)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://no.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://no.wikipedia.org/wiki/Spesial:Søk">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://no.wikipedia.org/wiki/Spesial:Søk</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-an.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-an.xml
new file mode 100644
index 0000000000..dbaddfcb5c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-an.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Biquipedia (an)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://an.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://an.wikipedia.org/wiki/Especial:Mirar">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://an.wikipedia.org/wiki/Especial:Mirar</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ar.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ar.xml
new file mode 100644
index 0000000000..78c63e61c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ar.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ويكيبيديا (ar)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ar.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ar.wikipedia.org/wiki/خاص:بحث">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ar.wikipedia.org/wiki/خاص:بحث</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-as.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-as.xml
new file mode 100644
index 0000000000..914e513f0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-as.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>অসমীয়া ৱিকিপিডিয়া (as)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://as.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://as.wikipedia.org/wiki/বিশেষ:সন্ধান">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://as.wikipedia.org/wiki/বিশেষ:সন্ধান</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ast.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ast.xml
new file mode 100644
index 0000000000..0a32de3219
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ast.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (ast)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ast.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ast.wikipedia.org/wiki/Especial:Gueta">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ast.wikipedia.org/wiki/Especial:Gueta</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-az.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-az.xml
new file mode 100644
index 0000000000..dd87a40cc6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-az.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vikipediya (az)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://az.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://az.wikipedia.org/wiki/Xüsusi:Axtar">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://az.wikipedia.org/wiki/Xüsusi:Axtar</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-be.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-be.xml
new file mode 100644
index 0000000000..9aa6bd3651
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-be.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Вікіпедыя (be)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://be.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://be.wikipedia.org/wiki/Адмысловае:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://be.wikipedia.org/wiki/Адмысловае:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bg.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bg.xml
new file mode 100644
index 0000000000..4b98497add
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bg.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Уикипедия (bg)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://bg.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://bg.wikipedia.org/wiki/Специални:Търсене">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://bg.wikipedia.org/wiki/Специални:Търсене</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bn.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bn.xml
new file mode 100644
index 0000000000..b564438636
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bn.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>উইকিপিডিয়া (bn)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://bn.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://bn.wikipedia.org/wiki/বিশেষ:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://bn.wikipedia.org/wiki/বিশেষ:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-br.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-br.xml
new file mode 100644
index 0000000000..41bf921eb4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-br.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (br)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://br.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://br.wikipedia.org/wiki/Dibar:Klask">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://br.wikipedia.org/wiki/Dibar:Klask</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bs.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bs.xml
new file mode 100644
index 0000000000..f54aed3b54
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-bs.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (bs)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://bs.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://bs.wikipedia.org/wiki/Posebno:Pretraga">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://bs.wikipedia.org/wiki/Posebno:Pretraga</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ca.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ca.xml
new file mode 100644
index 0000000000..3b08c30c29
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ca.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Viquipèdia (ca)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ca.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ca.wikipedia.org/wiki/Especial:Cerca">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ca.wikipedia.org/wiki/Especial:Cerca</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-cy.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-cy.xml
new file mode 100644
index 0000000000..72c526eee4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-cy.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wicipedia (cy)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://cy.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://cy.wikipedia.org/wiki/Arbennig:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://cy.wikipedia.org/wiki/Arbennig:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-cz.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-cz.xml
new file mode 100644
index 0000000000..9fa115ce52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-cz.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedie (cs)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://cs.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://cs.wikipedia.org/wiki/Speciální:Hledání">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://cs.wikipedia.org/wiki/Speciální:Hledání</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-da.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-da.xml
new file mode 100644
index 0000000000..7ac52b86f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-da.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (da)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://da.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://da.wikipedia.org/wiki/Speciel:Søgning">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://da.wikipedia.org/wiki/Speciel:Søgning</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-de.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-de.xml
new file mode 100644
index 0000000000..1052b85311
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-de.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (de)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://de.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://de.wikipedia.org/wiki/Spezial:Suche">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://de.wikipedia.org/wiki/Spezial:Suche</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-dsb.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-dsb.xml
new file mode 100644
index 0000000000..52ce1e97b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-dsb.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedija (dsb)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://dsb.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://dsb.wikipedia.org/wiki/Specialne:Pytaś">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://dsb.wikipedia.org/wiki/Specialne:Pytaś</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-el.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-el.xml
new file mode 100644
index 0000000000..db902d9813
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-el.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Βικιπαίδεια (el)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://el.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://el.wikipedia.org/wiki/Ειδικό:Αναζήτηση">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://el.wikipedia.org/wiki/Ειδικό:Αναζήτηση</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-eo.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-eo.xml
new file mode 100644
index 0000000000..b42dd74658
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-eo.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vikipedio (eo)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://eo.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://eo.wikipedia.org/wiki/Specialaĵo:Serĉi">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://eo.wikipedia.org/wiki/Specialaĵo:Serĉi</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-es.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-es.xml
new file mode 100644
index 0000000000..41dac576b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-es.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (es)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://es.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://es.wikipedia.org/wiki/Especial:Buscar">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://es.wikipedia.org/wiki/Especial:Buscar</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-et.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-et.xml
new file mode 100644
index 0000000000..c499e90397
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-et.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vikipeedia (et)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://et.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://et.wikipedia.org/wiki/Eri:Otsimine">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://et.wikipedia.org/wiki/Eri:Otsimine</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-eu.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-eu.xml
new file mode 100644
index 0000000000..9928360286
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-eu.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (eu)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://eu.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://eu.wikipedia.org/wiki/Berezi:Bilatu">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://eu.wikipedia.org/wiki/Berezi:Bilatu</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fa.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fa.xml
new file mode 100644
index 0000000000..757c4cc575
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fa.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ویکی‌پدیا (fa)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://fa.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://fa.wikipedia.org/wiki/ویژه:جستجو">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://fa.wikipedia.org/wiki/ویژه:جستجو</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fi.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fi.xml
new file mode 100644
index 0000000000..2612d72193
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fi.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (fi)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://fi.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://fi.wikipedia.org/wiki/Toiminnot:Haku">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://fi.wikipedia.org/wiki/Toiminnot:Haku</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fr.xml
new file mode 100644
index 0000000000..6f44da1fff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fr.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipédia (fr)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://fr.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://fr.wikipedia.org/wiki/Spécial:Recherche">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://fr.wikipedia.org/wiki/Spécial:Recherche</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fy-NL.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fy-NL.xml
new file mode 100644
index 0000000000..9bfb0a97a0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-fy-NL.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (fy)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://fy.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://fy.wikipedia.org/wiki/Wiki:Sykje">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://fy.wikipedia.org/wiki/Wiki:Sykje</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ga-IE.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ga-IE.xml
new file mode 100644
index 0000000000..d954b45615
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ga-IE.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vicipéid (ga)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ga.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ga.wikipedia.org/wiki/Speisialta:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ga.wikipedia.org/wiki/Speisialta:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gd.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gd.xml
new file mode 100644
index 0000000000..4b87ed7450
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gd.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (gd)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://gd.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://gd.wikipedia.org/wiki/Sònraichte:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://gd.wikipedia.org/wiki/Sònraichte:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gl.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gl.xml
new file mode 100644
index 0000000000..6b354639dc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gl.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (gl)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://gl.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://gl.wikipedia.org/wiki/Especial:Procurar">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://gl.wikipedia.org/wiki/Especial:Procurar</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gn.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gn.xml
new file mode 100644
index 0000000000..17a12cf997
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gn.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vikipetã (gn)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://gn.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://gn.wikipedia.org/wiki/Mba'echĩchĩ:Buscar">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://gn.wikipedia.org/wiki/Mba'echĩchĩ:Buscar</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gu.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gu.xml
new file mode 100644
index 0000000000..0fd505d003
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-gu.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>વિકિપીડિયા (gu)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://gu.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://gu.wikipedia.org/wiki/વિશેષ:શોધ">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://gu.wikipedia.org/wiki/વિશેષ:શોધ</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-he.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-he.xml
new file mode 100644
index 0000000000..2611582d7c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-he.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ויקיפדיה</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://he.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://he.wikipedia.org/wiki/מיוחד:חיפוש">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://he.wikipedia.org/wiki/מיוחד:חיפוש</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hi.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hi.xml
new file mode 100644
index 0000000000..e3a28a278c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hi.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>विकिपीडिया (hi)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://hi.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://hi.wikipedia.org/wiki/विशेष:खोज">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://hi.wikipedia.org/wiki/विशेष:खोज</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hr.xml
new file mode 100644
index 0000000000..02e514dbd7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hr.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedija (hr)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://hr.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://hr.wikipedia.org/wiki/Posebno:Traži">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://hr.wikipedia.org/wiki/Posebno:Traži</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hsb.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hsb.xml
new file mode 100644
index 0000000000..f72d268f9b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hsb.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedija (hsb)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://hsb.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://hsb.wikipedia.org/wiki/Specialnje:Pytać">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://hsb.wikipedia.org/wiki/Specialnje:Pytać</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hu.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hu.xml
new file mode 100644
index 0000000000..7d39f2c500
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hu.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipédia (hu)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://hu.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://hu.wikipedia.org/wiki/Speciális:Keresés">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://hu.wikipedia.org/wiki/Speciális:Keresés</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hy-AM.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hy-AM.xml
new file mode 100644
index 0000000000..bff51bbe49
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-hy-AM.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Վիքիպեդիա (hy)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://hy.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://hy.wikipedia.org/wiki/Սպասարկող:Որոնել">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://hy.wikipedia.org/wiki/Սպասարկող:Որոնել</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ia.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ia.xml
new file mode 100644
index 0000000000..4994148c75
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ia.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (ia)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ia.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ia.wikipedia.org/wiki/Special:Recerca">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ia.wikipedia.org/wiki/Special:Recerca</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-id.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-id.xml
new file mode 100644
index 0000000000..0e82cbcb8f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-id.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (id)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://id.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://id.wikipedia.org/wiki/Istimewa:Pencarian">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://id.wikipedia.org/wiki/Istimewa:Pencarian</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-is.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-is.xml
new file mode 100644
index 0000000000..093f45cc77
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-is.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (is)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://is.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://is.wikipedia.org/wiki/Kerfissíða:Leit">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://is.wikipedia.org/wiki/Kerfissíða:Leit</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-it.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-it.xml
new file mode 100644
index 0000000000..a04aeb42f8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-it.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (it)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://it.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://it.wikipedia.org/wiki/Speciale:Ricerca">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://it.wikipedia.org/wiki/Speciale:Ricerca</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ja.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ja.xml
new file mode 100644
index 0000000000..c27327138b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ja.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (ja)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ja.wikipedia.org/wiki">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ja.wikipedia.org/wiki/特別:検索">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ja.wikipedia.org/wiki/特別:検索</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ka.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ka.xml
new file mode 100644
index 0000000000..15afd0174f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ka.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ვიკიპედია (ka)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ka.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ka.wikipedia.org/wiki/სპეციალური:ძიება">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ka.wikipedia.org/wiki/სპეციალური:ძიება</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kab.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kab.xml
new file mode 100644
index 0000000000..7ef24e7aba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kab.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (kab)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://kab.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://kab.wikipedia.org/wiki/Uslig:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://kab.wikipedia.org/wiki/Uslig:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kk.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kk.xml
new file mode 100644
index 0000000000..328edd40c0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kk.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Уикипедия (kk)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://kk.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://kk.wikipedia.org/wiki/Арнайы:Іздеу">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://kk.wikipedia.org/wiki/Арнайы:Іздеу</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-km.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-km.xml
new file mode 100644
index 0000000000..a69f99b34d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-km.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>វិគីភីឌា (km)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://km.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://km.wikipedia.org/wiki/ពិសេស:ស្វែងរក">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://km.wikipedia.org/wiki/ពិសេស:ស្វែងរក</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kn.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kn.xml
new file mode 100644
index 0000000000..155b515fed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-kn.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ವಿಕಿಪೀಡಿಯ (kn)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://kn.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://kn.wikipedia.org/wiki/ವಿಶೇಷ:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://kn.wikipedia.org/wiki/ವಿಶೇಷ:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lij.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lij.xml
new file mode 100644
index 0000000000..1b75c36c79
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lij.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (lij)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://lij.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://lij.wikipedia.org/wiki/Speçiale:Riçerca">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://lij.wikipedia.org/wiki/Speçiale:Riçerca</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lo.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lo.xml
new file mode 100644
index 0000000000..64b236617b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lo.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ວິກິພີເດຍ (lo)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://lo.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://lo.wikipedia.org/wiki/ພິເສດ:ຊອກຫາ">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://lo.wikipedia.org/wiki/ພິເສດ:ຊອກຫາ</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lt.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lt.xml
new file mode 100644
index 0000000000..8430cb6591
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lt.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (lt)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://lt.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://lt.wikipedia.org/wiki/Specialus:Paieška">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://lt.wikipedia.org/wiki/Specialus:Paieška</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ltg.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ltg.xml
new file mode 100644
index 0000000000..384d206107
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ltg.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vikipedeja (ltg)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ltg.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ltg.wikipedia.org/wiki/Seviškuo:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ltg.wikipedia.org/wiki/Seviškuo:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lv.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lv.xml
new file mode 100644
index 0000000000..e25485901a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-lv.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vikipēdija (lv)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://lv.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://lv.wikipedia.org/wiki/Special:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://lv.wikipedia.org/wiki/Special:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ml.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ml.xml
new file mode 100644
index 0000000000..1d8a5df23c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ml.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>വിക്കിപീഡിയ (ml)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ml.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ml.wikipedia.org/wiki/പ്രത്യേകം:അന്വേഷണം">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ml.wikipedia.org/wiki/പ്രത്യേകം:അന്വേഷണം</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-mr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-mr.xml
new file mode 100644
index 0000000000..a73dac3d99
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-mr.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>विकिपीडिया (mr)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://mr.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://mr.wikipedia.org/wiki/विशेष:शोधा">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://mr.wikipedia.org/wiki/विशेष:शोधा</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ms.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ms.xml
new file mode 100644
index 0000000000..5b892124cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ms.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (ms)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ms.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ms.wikipedia.org/wiki/Khas:Gelintar">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ms.wikipedia.org/wiki/Khas:Gelintar</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-my.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-my.xml
new file mode 100644
index 0000000000..cc781dee19
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-my.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (my)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://my.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://my.wikipedia.org/wiki/Special:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://my.wikipedia.org/wiki/Special:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ne.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ne.xml
new file mode 100644
index 0000000000..93118b3057
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ne.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>विकिपीडिया (ne)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ne.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ne.wikipedia.org/wiki/विशेष:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ne.wikipedia.org/wiki/विशेष:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-nl.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-nl.xml
new file mode 100644
index 0000000000..36b41dd48a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-nl.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (nl)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://nl.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://nl.wikipedia.org/wiki/Speciaal:Zoeken">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://nl.wikipedia.org/wiki/Speciaal:Zoeken</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-oc.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-oc.xml
new file mode 100644
index 0000000000..6493a0b1ed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-oc.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipèdia (oc)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://oc.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://oc.wikipedia.org/wiki/Especial:Recèrca">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://oc.wikipedia.org/wiki/Especial:Recèrca</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-or.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-or.xml
new file mode 100644
index 0000000000..109e9e0fb6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-or.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ଉଇକିପିଡ଼ିଆ (or)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://or.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://or.wikipedia.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://or.wikipedia.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pa.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pa.xml
new file mode 100644
index 0000000000..b423d887da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pa.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (pa)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://pa.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://pa.wikipedia.org/wiki/ਖ਼ਾਸ:ਖੋਜੋ">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://pa.wikipedia.org/wiki/ਖ਼ਾਸ:ਖੋਜੋ</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pl.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pl.xml
new file mode 100644
index 0000000000..e8fb1aafb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pl.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (pl)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://pl.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://pl.wikipedia.org/wiki/Specjalna:Szukaj">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://pl.wikipedia.org/wiki/Specjalna:Szukaj</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pt.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pt.xml
new file mode 100644
index 0000000000..1595395e25
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-pt.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (pt)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://pt.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://pt.wikipedia.org/wiki/Especial:Pesquisar">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://pt.wikipedia.org/wiki/Especial:Pesquisar</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-rm.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-rm.xml
new file mode 100644
index 0000000000..aca3ffdfb6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-rm.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (rm)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://rm.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://rm.wikipedia.org/wiki/Spezial:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://rm.wikipedia.org/wiki/Spezial:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ro.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ro.xml
new file mode 100644
index 0000000000..f91f665dc1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ro.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (ro)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ro.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ro.wikipedia.org/wiki/Special:Căutare">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ro.wikipedia.org/wiki/Special:Căutare</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ru.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ru.xml
new file mode 100644
index 0000000000..33a9a541bd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ru.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Википедия (ru)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ru.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ru.wikipedia.org/wiki/Служебная:Поиск">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ru.wikipedia.org/wiki/Служебная:Поиск</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sk.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sk.xml
new file mode 100644
index 0000000000..44c3f0326a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sk.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipédia (sk)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://sk.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://sk.wikipedia.org/wiki/Špeciálne:Hľadanie">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://sk.wikipedia.org/wiki/Špeciálne:Hľadanie</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sl.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sl.xml
new file mode 100644
index 0000000000..7ff7347b0a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sl.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedija (sl)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://sl.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://sl.wikipedia.org/wiki/Posebno:Iskanje">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://sl.wikipedia.org/wiki/Posebno:Iskanje</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sq.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sq.xml
new file mode 100644
index 0000000000..c100012f37
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sq.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (sq)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://sq.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://sq.wikipedia.org/wiki/Speciale:Kërkim">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://sq.wikipedia.org/wiki/Speciale:Kërkim</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sr.xml
new file mode 100644
index 0000000000..3368eea67e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sr.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Википедија (sr)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://sr.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://sr.wikipedia.org/wiki/Посебно:Претражи">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://sr.wikipedia.org/wiki/Посебно:Претражи</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sv-SE.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sv-SE.xml
new file mode 100644
index 0000000000..3595d01bab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-sv-SE.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (sv)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://sv.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://sv.wikipedia.org/wiki/Special:Sök">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://sv.wikipedia.org/wiki/Special:Sök</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ta.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ta.xml
new file mode 100644
index 0000000000..778e39f733
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ta.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>விக்கிப்பீடியா (ta)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ta.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ta.wikipedia.org/wiki/சிறப்பு:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ta.wikipedia.org/wiki/சிறப்பு:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-te.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-te.xml
new file mode 100644
index 0000000000..79c2b04e8c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-te.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>వికీపీడియా (te)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://te.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://te.wikipedia.org/wiki/ప్రత్యేక:అన్వేషణ">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://te.wikipedia.org/wiki/ప్రత్యేక:అన్వేషణ</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-th.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-th.xml
new file mode 100644
index 0000000000..cbcf5319c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-th.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>วิกิพีเดีย</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://th.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://th.wikipedia.org/wiki/พิเศษ:ค้นหา">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://th.wikipedia.org/wiki/พิเศษ:ค้นหา</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-tr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-tr.xml
new file mode 100644
index 0000000000..4bc9e189d3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-tr.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (tr)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://tr.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://tr.wikipedia.org/wiki/Özel:Ara">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://tr.wikipedia.org/wiki/Özel:Ara</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-uk.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-uk.xml
new file mode 100644
index 0000000000..bc2f4741a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-uk.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Вікіпедія</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://uk.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://uk.wikipedia.org/wiki/Спеціальна:Пошук">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://uk.wikipedia.org/wiki/Спеціальна:Пошук</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ur.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ur.xml
new file mode 100644
index 0000000000..e4b0c8a616
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-ur.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ویکیپیڈیا (ur)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ur.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://ur.wikipedia.org/wiki/خاص:تلاش">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://ur.wikipedia.org/wiki/خاص:تلاش</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-uz.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-uz.xml
new file mode 100644
index 0000000000..a33e6a4d82
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-uz.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Vikipediya (uz)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://uz.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://uz.wikipedia.org/wiki/Maxsus:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://uz.wikipedia.org/wiki/Maxsus:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-vi.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-vi.xml
new file mode 100644
index 0000000000..e3ac224fd9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-vi.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (vi)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="get" template="https://vi.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="get" template="https://vi.wikipedia.org/wiki/Đặc_biệt:Tìm_kiếm">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://vi.wikipedia.org/wiki/Đặc_biệt:Tìm_kiếm</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-wo.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-wo.xml
new file mode 100644
index 0000000000..39caa9839b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-wo.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (wo)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://wo.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://wo.wikipedia.org/wiki/Jagleel:Ceet">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://wo.wikipedia.org/wiki/Jagleel:Ceet</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-zh-CN.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-zh-CN.xml
new file mode 100644
index 0000000000..a168d71dea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-zh-CN.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>维基百科</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://zh.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://zh.wikipedia.org/wiki/Special:搜索">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://zh.wikipedia.org/wiki/Special:搜索</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-zh-TW.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-zh-TW.xml
new file mode 100644
index 0000000000..aeaabe6811
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia-zh-TW.xml
@@ -0,0 +1,19 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia (zh)</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA51BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD////7+/vl5eWxsbH9/f3Ly8tzc3P39/fe3t5cXFzz8/Pt7e3i4uI3NzcaGhoHBwcFBQX5+fnIyMhTU1MXFxcMDAzx8fHX19fFxcXCwsK7u7u1tbWenp6WlpZ8fHxXV1ctLS319fXn5+fV1dWsrKySkpJnZ2cgICDv7+/p6eng4ODQ0NC9vb24uLikpKSPj4+JiYmCgoJiYmJCQkI7OzsyMjIpKSkQEBCoqKigoKCdnZ14eHhubm5PT09LS0tERETk5OTb29unp6c0NDQkJCQOGD7BAAAAB3RSTlMA8si8ZBhlc+JuAAAAAw1JREFUaN7tmtluIjEQRYEhUxd6YYew7xAggRAgCQmB7Ov8//eM3Zg2nXkJvUjz4POAVEbyQXaVy0gOMaKRMAVAOBINcY5+UWD8OmKCIOaXhlDoNwVKNBShQImEwhQo4RAFjBIogRIogRIogRIoQWCCYXqaHJVaWrtl1IfpYp3GiczFqLHQ5kY9m0l0qVM8Txrs+8ZomkhSKpbOzlraolSP15o/EtxoU9gk2iZVujM7HnVe6eUhKaL4aYrWXWMXfd7+dIlScWxpbsjibQiLPm255sH5jYjKOoCEecge5LcTxqokuItZv5F2DID0iR21ALwctslfunNCurKEBRJkgBTZdIDOoVl0CU7FjgsZHpsi+gNoJGlisDlUkC+C8SQHejzOiqCLgXRTNYazw+vg8tsaHZ+DMd7ZlyR5Qix/uOAEgGOhlzy+EPLMMUmyOHVTyQ0wDBlXB2Dw1FzreCZJGfhyIyiDs/qWSEkiarNPxxaX3J1FWcCRLJXidtFW7MO5lmV3AhMMPS8HHsH4oBJmtEcHF+ROUEgD2E/AiVXO98AtSaoJmC4F1AUjXZAD97Bo0R491ApuBRNdVq/FGhz9hPaI48p9w1mI6rVpi1qQ9FmRuRe8iTPaJgfOi6NaTr20zDoYJUfOM2aOHM15EYwBOKbQwHm14wcY5FYgTzi5CHewMPZytO9aIHuj3MY53sF5s3M0S94EFV5bdiLeshQdgdGwc9T0IJC1VROHs4EHSgHY1XKflaFXQQ6y2MpIVIiSYDStgRLOvN/sDDCSImmv7cxaWVuu5z0LxJKU+Y1M9LE4GHOrsWnkXUDx3a5mRR8zRXFsisj5IeiBc0cmPkRxZMBY0BIGeRLIYmJ8smnLDmVuiL5ngWxksUd5JhVqYEzxTv4I1tiSc9Y38OSTgOZwnki0GYhe55Pg1VqjCUnORLf2SWAVb5fIeYPRJ/4JnoFa1THSATTyT3BcQ++f+8DKRwGN299Hltr/+zdWCZRACZRACZSAlEAJDifwRwPBP3uIUqBEg356EvzjmcCf//wFVbAkwfl+5CMAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://zh.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://zh.wikipedia.org/wiki/Special:搜索">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+ <Param name="variant" value="zh-tw"/>
+</Url>
+<SearchForm>https://zh.wikipedia.org/wiki/Special:搜索</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia.xml
new file mode 100644
index 0000000000..ab1d0521e3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wikipedia.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="32" height="32">data:image/png;base64,AAABAAIAICAQAAEABADuAQAAJgAAABAQEAABAAQAYAEAABQCAACJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAAG1SURBVFiF7ZfNccQgDIVJJi2oBlXgGqjBBdADJdACTbgS36mBJqQcMnLEj9jsXpzD7gwnGenj8SR7P5iZ3Y2/zzuLvwH+BcDXzR68X4HbARwR8bZt7Jwb1r7vTERMRIyITQwAOOfMRMQpJQaAJi6xnPOwV+d2UqDW2iSptV7FZYUQrvh5nk2slMLOOUZELqUMe/d9vwrr3E4/lFK6ChzHMSSptV7xEEITO47D3CcAADAczPUFRIVt26aJYoxTlRCRvffTPQKeUhpiblXAUkEgY4zN6ftr0Tlnp58CaJmtE/UqIGJj2H4BwHBlJoA2jHNuaiitgjw7e06rY8WnAOLovhUtFazTEf20uKWkCUBE7L1ftuSqI2Sd57nsjCWASKfNNrtbGUrW3LC66SGAyGcV0ICzFhOFZCK+BKCL9AUQkUMIlwkBoInLeF7lfwjQyzxztjashgSA6eB5GiDnPLxg5PS9YQVSAGe+eBpA9zwiTvu6vyrv/bI1nwIganvemg39K90aPC8B6J63kmsVVoPnJQCi32+B1cyXD4/V4HkZoNbKMcalsUopf3K+Xh/vv2ZvgLsBvgEmf6Ud8idB7wAAAABJRU5ErkJggolQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAASdJREFUOI3FkzGKwzAQRX9M7iDnBGqDywhBSCeUi/gQKdL4BiGtK5Fe1zDug3vjMieYv5WEtWG3ycIKBvQ16I/mDdqQJD5Y1SeX/8Rg+2EHqMZxhLUWVVXhfr8DALz3WS/LAmstrLVYlgWPxwO73Q7ee0zTBIgI53kmAMYYmbTWmrfbjSJC5xzneaaIUERojMl7pI1zrkiEEKiUYoyRXdcV58m4MBiGgQA4DENOaq2ptc46FVrrPIWmaeCcw+VyyYBOpxNer1fW4zhiv9+XFNduMUYC4PP5zNWUUgwhUETYtm3BQkS4XZt576G1Rt/3OBwOOJ/PsNbier3ieDwCAOq6/vkFCRKA3GuakDGm4PMGcR1KqYJ027Zv8H41SAzW8b33FJt//41fMtqn5156WuIAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://en.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://en.wikipedia.org/wiki/Special:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://en.wikipedia.org/wiki/Special:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-kn.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-kn.xml
new file mode 100644
index 0000000000..7e39c48ee2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-kn.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>ವಿಕ್ಷನರಿ (kn)</ShortName>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA21BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD////Z2dnm5uYSEhJCQkJjY2PJycn6+vq4uLhOTk79/f0GBgbg4OBra2tbW1sEBAQeHh729va/v7/u7u4pKSkPDw/d3d2BgYFQUFAjIyMYGBjq6uq0tLSrq6umpqadnZ2Xl5eSkpKNjY1wcHBWVlZGRkY7Ozs1NTUwMDAKCgrDw8OxsbGIiIh6enpoaGhLS0tAQEA3NzcsLCwrKysICAjw8PDW1tbS0tJ0dHTo6Ojj4+PPz898fHxiYmLx8fG6urqhoaFlZWXTVXa3AAAABnRSTlMA8si8ZBgxEvPEAAACy0lEQVRo3u2aa1fiMBCGAXUiiLW2tFguAsr9orBcFBDwsrf//4t2kqbN0d2DS5Pshz15P0BmQvtMMsmclpMU6jiTBg1KZ45TVCdHoE1HJwjQeH8k4PyAVh2nMqBVmVQatCqdAs0yAAMwAAMwAAP414B8pCIzh/m8DYfpMc81+COARKoxc0LI9kBAh3Bl9wPCwC8IOT8Q0CNcp5+MICmg+ymg5Nm2XWskBaxqtr142wfIOsJMAAgzvReQkwecGYAB/AYoLsurOgitV6vyMgdCjSXaEoCpZVmlovDPLVQbhAK0hwkAASGvrKoSlCf8t9SurmO7buF1lQSAB89jceVusENEnCtRwOW9GBGaPxLk4H3Ip05k2YSp8K57IAXoi4hFzW/GKX7CGpmTAtxfYk+fG85pCHDH3OGh8QUkAPyet9y4Q1pQwiHdcUcLO7/KAaCNPc+8Z4btxTl+7EK7kiXkqigJGBDUMF68WacjpmWBzQAkAcUr7JrGzSYS46VPWX1ZAExIVJc8NuN1ShyxulElxCpLA65JNNEtbGBhokmYUXvE0NKAh6haVF4ImfDnkwtWqLBxLQMQS4W0OemNp/YGf+tv6YKVB0CTV4spv1+DFqSzcA9ufAWAeVgt/NeoKgV0asJd0QMFgLJFVyP76sRpD8LldaYC4G/Z1irgpx2nvVShu8IdqwBAl3aOm2FqUZVnuhM8lnslgBpbqK4onLc0CS06IlmAqPpkgymYc8cObfeGjkgNAALCdFUXZZupCYoAM0IlysLYDR2eKsBjGLEoCy1mf2soAYiIR7HjJ7MvQAVAROyuxd5jgII6wMCtVl96wva/o2NTVwcAx/edDw7HB0mA+qfrDyEpfwl82hVQRUioUR+v7v7ViziX6hdxLjspoKcZIEbg/qd/SDEZgAEYAIABGIABaJD2QwPajz1oP7ih++iJ/sMz2o///ALGfwB3vnCGuQAAAABJRU5ErkJggg==</Image>
+<Url type="text/html" method="get" template="https://kn.wiktionary.org/wiki/ವಿಶೇಷ:Search">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-suggestions+json" method="GET" template="https://kn.wiktionary.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="format" value="xml"/>
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="namespace" value="0"/>
+</Url>
+<SearchForm>https://kn.wiktionary.org/wiki/ವಿಶೇಷ:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-oc.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-oc.xml
new file mode 100644
index 0000000000..82bd9724c1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-oc.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikiccionari (oc)</ShortName>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA21BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD////Z2dnm5uYSEhJCQkJjY2PJycn6+vq4uLhOTk79/f0GBgbg4OBra2tbW1sEBAQeHh729va/v7/u7u4pKSkPDw/d3d2BgYFQUFAjIyMYGBjq6uq0tLSrq6umpqadnZ2Xl5eSkpKNjY1wcHBWVlZGRkY7Ozs1NTUwMDAKCgrDw8OxsbGIiIh6enpoaGhLS0tAQEA3NzcsLCwrKysICAjw8PDW1tbS0tJ0dHTo6Ojj4+PPz898fHxiYmLx8fG6urqhoaFlZWXTVXa3AAAABnRSTlMA8si8ZBgxEvPEAAACy0lEQVRo3u2aa1fiMBCGAXUiiLW2tFguAsr9orBcFBDwsrf//4t2kqbN0d2DS5Pshz15P0BmQvtMMsmclpMU6jiTBg1KZ45TVCdHoE1HJwjQeH8k4PyAVh2nMqBVmVQatCqdAs0yAAMwAAMwAAP414B8pCIzh/m8DYfpMc81+COARKoxc0LI9kBAh3Bl9wPCwC8IOT8Q0CNcp5+MICmg+ymg5Nm2XWskBaxqtr142wfIOsJMAAgzvReQkwecGYAB/AYoLsurOgitV6vyMgdCjSXaEoCpZVmlovDPLVQbhAK0hwkAASGvrKoSlCf8t9SurmO7buF1lQSAB89jceVusENEnCtRwOW9GBGaPxLk4H3Ip05k2YSp8K57IAXoi4hFzW/GKX7CGpmTAtxfYk+fG85pCHDH3OGh8QUkAPyet9y4Q1pQwiHdcUcLO7/KAaCNPc+8Z4btxTl+7EK7kiXkqigJGBDUMF68WacjpmWBzQAkAcUr7JrGzSYS46VPWX1ZAExIVJc8NuN1ShyxulElxCpLA65JNNEtbGBhokmYUXvE0NKAh6haVF4ImfDnkwtWqLBxLQMQS4W0OemNp/YGf+tv6YKVB0CTV4spv1+DFqSzcA9ufAWAeVgt/NeoKgV0asJd0QMFgLJFVyP76sRpD8LldaYC4G/Z1irgpx2nvVShu8IdqwBAl3aOm2FqUZVnuhM8lnslgBpbqK4onLc0CS06IlmAqPpkgymYc8cObfeGjkgNAALCdFUXZZupCYoAM0IlysLYDR2eKsBjGLEoCy1mf2soAYiIR7HjJ7MvQAVAROyuxd5jgII6wMCtVl96wva/o2NTVwcAx/edDw7HB0mA+qfrDyEpfwl82hVQRUioUR+v7v7ViziX6hdxLjspoKcZIEbg/qd/SDEZgAEYAIABGIABaJD2QwPajz1oP7ih++iJ/sMz2o///ALGfwB3vnCGuQAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://oc.wiktionary.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="namespace" value="0"/>
+</Url>
+<Url type="text/html" method="get" template="https://oc.wiktionary.org/wiki/Especial:Recèrca">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://oc.wiktionary.org/wiki/Especial:Recèrca</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-or.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-or.xml
new file mode 100644
index 0000000000..bb475cafb5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-or.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wiktionary (or)</ShortName>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA21BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD////Z2dnm5uYSEhJCQkJjY2PJycn6+vq4uLhOTk79/f0GBgbg4OBra2tbW1sEBAQeHh729va/v7/u7u4pKSkPDw/d3d2BgYFQUFAjIyMYGBjq6uq0tLSrq6umpqadnZ2Xl5eSkpKNjY1wcHBWVlZGRkY7Ozs1NTUwMDAKCgrDw8OxsbGIiIh6enpoaGhLS0tAQEA3NzcsLCwrKysICAjw8PDW1tbS0tJ0dHTo6Ojj4+PPz898fHxiYmLx8fG6urqhoaFlZWXTVXa3AAAABnRSTlMA8si8ZBgxEvPEAAACy0lEQVRo3u2aa1fiMBCGAXUiiLW2tFguAsr9orBcFBDwsrf//4t2kqbN0d2DS5Pshz15P0BmQvtMMsmclpMU6jiTBg1KZ45TVCdHoE1HJwjQeH8k4PyAVh2nMqBVmVQatCqdAs0yAAMwAAMwAAP414B8pCIzh/m8DYfpMc81+COARKoxc0LI9kBAh3Bl9wPCwC8IOT8Q0CNcp5+MICmg+ymg5Nm2XWskBaxqtr142wfIOsJMAAgzvReQkwecGYAB/AYoLsurOgitV6vyMgdCjSXaEoCpZVmlovDPLVQbhAK0hwkAASGvrKoSlCf8t9SurmO7buF1lQSAB89jceVusENEnCtRwOW9GBGaPxLk4H3Ip05k2YSp8K57IAXoi4hFzW/GKX7CGpmTAtxfYk+fG85pCHDH3OGh8QUkAPyet9y4Q1pQwiHdcUcLO7/KAaCNPc+8Z4btxTl+7EK7kiXkqigJGBDUMF68WacjpmWBzQAkAcUr7JrGzSYS46VPWX1ZAExIVJc8NuN1ShyxulElxCpLA65JNNEtbGBhokmYUXvE0NKAh6haVF4ImfDnkwtWqLBxLQMQS4W0OemNp/YGf+tv6YKVB0CTV4spv1+DFqSzcA9ufAWAeVgt/NeoKgV0asJd0QMFgLJFVyP76sRpD8LldaYC4G/Z1irgpx2nvVShu8IdqwBAl3aOm2FqUZVnuhM8lnslgBpbqK4onLc0CS06IlmAqPpkgymYc8cObfeGjkgNAALCdFUXZZupCYoAM0IlysLYDR2eKsBjGLEoCy1mf2soAYiIR7HjJ7MvQAVAROyuxd5jgII6wMCtVl96wva/o2NTVwcAx/edDw7HB0mA+qfrDyEpfwl82hVQRUioUR+v7v7ViziX6hdxLjspoKcZIEbg/qd/SDEZgAEYAIABGIABaJD2QwPajz1oP7ih++iJ/sMz2o///ALGfwB3vnCGuQAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://or.wiktionary.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="namespace" value="0"/>
+</Url>
+<Url type="text/html" method="get" template="https://or.wiktionary.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://or.wiktionary.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-ta.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-ta.xml
new file mode 100644
index 0000000000..5b4662a29e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-ta.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>விக்சனரி (ta)</ShortName>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA21BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD////Z2dnm5uYSEhJCQkJjY2PJycn6+vq4uLhOTk79/f0GBgbg4OBra2tbW1sEBAQeHh729va/v7/u7u4pKSkPDw/d3d2BgYFQUFAjIyMYGBjq6uq0tLSrq6umpqadnZ2Xl5eSkpKNjY1wcHBWVlZGRkY7Ozs1NTUwMDAKCgrDw8OxsbGIiIh6enpoaGhLS0tAQEA3NzcsLCwrKysICAjw8PDW1tbS0tJ0dHTo6Ojj4+PPz898fHxiYmLx8fG6urqhoaFlZWXTVXa3AAAABnRSTlMA8si8ZBgxEvPEAAACy0lEQVRo3u2aa1fiMBCGAXUiiLW2tFguAsr9orBcFBDwsrf//4t2kqbN0d2DS5Pshz15P0BmQvtMMsmclpMU6jiTBg1KZ45TVCdHoE1HJwjQeH8k4PyAVh2nMqBVmVQatCqdAs0yAAMwAAMwAAP414B8pCIzh/m8DYfpMc81+COARKoxc0LI9kBAh3Bl9wPCwC8IOT8Q0CNcp5+MICmg+ymg5Nm2XWskBaxqtr142wfIOsJMAAgzvReQkwecGYAB/AYoLsurOgitV6vyMgdCjSXaEoCpZVmlovDPLVQbhAK0hwkAASGvrKoSlCf8t9SurmO7buF1lQSAB89jceVusENEnCtRwOW9GBGaPxLk4H3Ip05k2YSp8K57IAXoi4hFzW/GKX7CGpmTAtxfYk+fG85pCHDH3OGh8QUkAPyet9y4Q1pQwiHdcUcLO7/KAaCNPc+8Z4btxTl+7EK7kiXkqigJGBDUMF68WacjpmWBzQAkAcUr7JrGzSYS46VPWX1ZAExIVJc8NuN1ShyxulElxCpLA65JNNEtbGBhokmYUXvE0NKAh6haVF4ImfDnkwtWqLBxLQMQS4W0OemNp/YGf+tv6YKVB0CTV4spv1+DFqSzcA9ufAWAeVgt/NeoKgV0asJd0QMFgLJFVyP76sRpD8LldaYC4G/Z1irgpx2nvVShu8IdqwBAl3aOm2FqUZVnuhM8lnslgBpbqK4onLc0CS06IlmAqPpkgymYc8cObfeGjkgNAALCdFUXZZupCYoAM0IlysLYDR2eKsBjGLEoCy1mf2soAYiIR7HjJ7MvQAVAROyuxd5jgII6wMCtVl96wva/o2NTVwcAx/edDw7HB0mA+qfrDyEpfwl82hVQRUioUR+v7v7ViziX6hdxLjspoKcZIEbg/qd/SDEZgAEYAIABGIABaJD2QwPajz1oP7ih++iJ/sMz2o///ALGfwB3vnCGuQAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ta.wiktionary.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="namespace" value="0"/>
+</Url>
+<Url type="text/html" method="get" template="https://ta.wiktionary.org/wiki/சிறப்பு:Search">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://ta.wiktionary.org/wiki/சிறப்பு:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-te.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-te.xml
new file mode 100644
index 0000000000..1dd499dc37
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/wiktionary-te.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>విక్షనరీ (te)</ShortName>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAA21BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAD////Z2dnm5uYSEhJCQkJjY2PJycn6+vq4uLhOTk79/f0GBgbg4OBra2tbW1sEBAQeHh729va/v7/u7u4pKSkPDw/d3d2BgYFQUFAjIyMYGBjq6uq0tLSrq6umpqadnZ2Xl5eSkpKNjY1wcHBWVlZGRkY7Ozs1NTUwMDAKCgrDw8OxsbGIiIh6enpoaGhLS0tAQEA3NzcsLCwrKysICAjw8PDW1tbS0tJ0dHTo6Ojj4+PPz898fHxiYmLx8fG6urqhoaFlZWXTVXa3AAAABnRSTlMA8si8ZBgxEvPEAAACy0lEQVRo3u2aa1fiMBCGAXUiiLW2tFguAsr9orBcFBDwsrf//4t2kqbN0d2DS5Pshz15P0BmQvtMMsmclpMU6jiTBg1KZ45TVCdHoE1HJwjQeH8k4PyAVh2nMqBVmVQatCqdAs0yAAMwAAMwAAP414B8pCIzh/m8DYfpMc81+COARKoxc0LI9kBAh3Bl9wPCwC8IOT8Q0CNcp5+MICmg+ymg5Nm2XWskBaxqtr142wfIOsJMAAgzvReQkwecGYAB/AYoLsurOgitV6vyMgdCjSXaEoCpZVmlovDPLVQbhAK0hwkAASGvrKoSlCf8t9SurmO7buF1lQSAB89jceVusENEnCtRwOW9GBGaPxLk4H3Ip05k2YSp8K57IAXoi4hFzW/GKX7CGpmTAtxfYk+fG85pCHDH3OGh8QUkAPyet9y4Q1pQwiHdcUcLO7/KAaCNPc+8Z4btxTl+7EK7kiXkqigJGBDUMF68WacjpmWBzQAkAcUr7JrGzSYS46VPWX1ZAExIVJc8NuN1ShyxulElxCpLA65JNNEtbGBhokmYUXvE0NKAh6haVF4ImfDnkwtWqLBxLQMQS4W0OemNp/YGf+tv6YKVB0CTV4spv1+DFqSzcA9ufAWAeVgt/NeoKgV0asJd0QMFgLJFVyP76sRpD8LldaYC4G/Z1irgpx2nvVShu8IdqwBAl3aOm2FqUZVnuhM8lnslgBpbqK4onLc0CS06IlmAqPpkgymYc8cObfeGjkgNAALCdFUXZZupCYoAM0IlysLYDR2eKsBjGLEoCy1mf2soAYiIR7HjJ7MvQAVAROyuxd5jgII6wMCtVl96wva/o2NTVwcAx/edDw7HB0mA+qfrDyEpfwl82hVQRUioUR+v7v7ViziX6hdxLjspoKcZIEbg/qd/SDEZgAEYAIABGIABaJD2QwPajz1oP7ih++iJ/sMz2o///ALGfwB3vnCGuQAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://te.wiktionary.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="namespace" value="0"/>
+</Url>
+<Url type="text/html" method="get" template="https://te.wiktionary.org/wiki/ప్రత్యేక:అన్వేషణ">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://te.wiktionary.org/wiki/ప్రత్యేక:అన్వేషణ</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yahoo-jp-auctions.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yahoo-jp-auctions.xml
new file mode 100644
index 0000000000..6a3666954d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yahoo-jp-auctions.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Yahoo!オークション</ShortName>
+ <Description>ヤフオク! 検索</Description>
+ <InputEncoding>EUC-JP</InputEncoding>
+ <Image height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRAD/AP8A/6C9p5MAAAcASURBVFjDrZdrbBTXFcf/Z+bOeu31OzHmLZJQbEhokmIwTcHFSjFRH0mAmBaBmkZtYtLKVaqoX/rFkzaq8qGxo1RQ5VWiNomTNXFwCFViHruhhrp0nRQDxs8FSuMHL792vTszd+b0w+4a4xcGeqTRSDP3zvnd/3ncO8BtGjOr8XtyuKdrpXH53NIx7+hG85XbdE5EZDNzcvhS9+6R9i+OX/hXQ4sx0L0ZAPz+F9TbXeC0FggENABgc+gn3N7I1YD56dZMDn5WzTIO4fPpYiZK3JoCuq7ElUg6VvX8B/XbZnHd44j+rRRxiL7NAMBerzoVhLgdAD/8CgCnZfevHxi4cHL1QNhEinuxy4p0ytY3tgqgeo8c6H6CMud+6I1BOETE/xcAn66LYl2XbdW/XXu5s/1wUv8Fwcz2UGRQTU9ZLKxIp2x9c6sA3t8jh/s2i7Tc2skgbik2gdee0QrKXrda3tWLrgY7DobO+DTDGJQkMkXIlJCOjfSUDFCkSwoBsfTp97HwoeLNIi23ltmrEm2xE9+66SoY67w/2HEwfManGUZIkpYlAAceTYFQVAyNDIHd9wgpIVvf+BH+c8z3IQ/3bSLaYnu93tHquCkFWNcF6bqMOz8QavW7otFhSVq6ANvXxjEQthxIx0aGJwsc6bBUQFtaVo1FazZsF6nZ7zKzQkTOjBXwXe/8YKjV54oYQ5biyhBged1YIsCjAZpIQX9/hy1SlmiyD1B6umCGBjcDAGpqCJgmCeNlo6KpiZqamlBQVmZdc+7XosawpWqZGjvWBCGZHajCA3ckKJNzC4TIvmPAfedd7OTM+SI51/NjAEBpqTNlCBLyjH3W4f39ty+2na4PnfG5okZIKlqGYJ7oHOyAhAcyFLTSsvO13JKnQ7Pz84tzvvG9APNpF9G9ZryD8qQAzKwm2qsdHdyuupNHeo8fFp37dr0aPd+SPjJyUZKWkH38dAYpbtihczIt5z6Ru/6p0PwHC4sz89cEplqcGOc80dvd4UvdbzvnT20JRS1c6jiBy1/ug5I614k5tycXjwRss9vxzHpA5Kx9omfB8hWPZeSvCQRee0b7pGeuXVFRweOVpesBYjVqXDm/rL+58bR/4w9t5W5wNBWk3rFASdUk0eRBA0hAdWUj+t9muXDjb8S928o/Tsme81gpoHpjK3Mmm3ldFdTUxO6u7IUhzpofmvfTh1VtHjhr0f0qcZhCpgOHeZxvB6S44VhXnMjZZqtfQD1SV4W2o58/ahvh39UANhE5zDxpxU2WA0p8QlHvyX8eaH/zOdeV1kZLm7VMGw71gSHgcREUIjA7UIQHcjgoPTnLxexvbYW/bjdGTndITyrEhsq9uKdog65qyS9MldwTqIjI0XVdIaIjs5cXrl/ys1fM7LzVmnWxxUrzzALBQdhkOOxAFSmQoaDMmPV1kbu2tDtv01N/XVf+B86aB2Elwdq/43GcbTig20akIvHt8UpM2QkDgYBWUFBgMXNRb3Pjwfa3fqVdbWuU4s48MRy+CqhuuK0Ldnr2EnV2SVlowYqVD2fkFR1n2ywP/sP/6qfPl8AkWLgM7fuv1+HutSXTKjGpceLAwVzUe6LR9JcX8kcbYO3bvpTf+QHsPdvu4xN/eakv3NVQAABevdQVH78jeOwA71wFrloNsyoP3H5oL9tGRB8b6hsCxAeKBERPc6PhLy/kj74Lq/ZRyL9XPMlD3WdPJdRkZkWPh5WZdwSP1vPO1eDKVTBfWQbuOFR3ixA+3yhE74lG8/Pnivg9YKT55V9yf9fJ+tFxMQhKhJaZn+1qqOddD4FfXhGDaD+0l20rlhM3ZWMhuv99NNK954/csf/Ph/iszx1/PppPzExjlHi262gMonJVLBwdh+vYMiZCkA4oDNBkl67rypiceMTs73uHL11Km0rKCRANn/HOb4IrV8KovAvc5f+Yzf6vHrx5JeL/ATOJ43iIYFyJqq9dAxi7F5B+gwOKDnB8r1Dg9ytYt86erpyIiJkZIFKI6E/MjO+8tH8XpIWFhet1LTn5S/Ktgyj2Q+4sSf05HHoRwEUGBBGYeRTIJnAmCEd+cSBUyrEzB2OGNrbuOTq42DBN1Z2e0wYAotgPGwCGRuzkOalq1oDhZCnjdhyHgVQX4WrUWQQABLC3FOqWGtgzAYh3QCIiJndG51gwUbtxXnZyilPUO2ittGwbSS6YPK5FE+DYBFdmCnk+2Zr1pOnIpk0f9J3SdSi6jht3tGvhINTUKCgFgFKHiByhJvHCdE2tdWUpMCQzANeUUgJL56cpb58bUl4EcGpON1RgZgAJCOB61QRI2oOGAIPBDJouCyUDg4YDxcEIACxpn3keTGUiPSmlfciQ9wOAsC2WYuofSbaJ+w0StuV8BQDr/DPLgensf4YuF+VxfZ0MAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIzLTExLTEzVDIwOjM1OjMxKzAwOjAwn1JqogAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMy0xMS0xM1QyMDozNTozMSswMDowMO4P0h4AAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjMtMTEtMTNUMjA6MzU6MzIrMDA6MDCI8ulcAAAAAElFTkSuQmCC</Image>
+ <Url method="GET" template="https://auctions.yahoo.co.jp/search/search" type="text/html">
+ <Param name="p" value="{searchTerms}" />
+ <Param name="ei" value="EUC-JP" />
+ <Param name="fr" value="mozff" />
+ </Url>
+ <SearchForm>https://auctions.yahoo.co.jp/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yahoo-jp.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yahoo-jp.xml
new file mode 100644
index 0000000000..2196d37b61
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yahoo-jp.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo! JAPAN</ShortName>
+<Description>検索エンジン - Yahoo!検索</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAxlBMVEUAAAD/////////////////////////////////ADP/r7///f3/Bjj/Czz/JVH/Hkz/2uH/b4z/aYf/+/z/6+//ma7/9Pb/xtL/vcr/VHb/F0X/EkH/9vj/8vX/7/L/nrL/iqL/dZH/Xn7/PmX/K1b/Dz//1t//0dr/wc3/s8L/qLn/YoH/WXr/Sm7/RGr/lKr/kKb/e5b/T3L/OWH/Mlv/HEn/5uv/4ef/3uX/ytX/q7z/o7b/gJr/IU3/hp7/co7/ucf/ZINYCJxYAAAACHRSTlMA8Qz3rqoz9iHWq2MAAAKtSURBVGje7ZrZlqIwEEDRVrtiEB1RxH3flx4d7VZ7mZn//6nxCJ0SkEga8zB9ch9N5JJUpYrDQdO0ZDoBUkikk9qJzCNI4zFzuv8HkMhDUkuDVNJaAqSS0EAySqAESqAESqAESqAEdxb8Ijx68QU7rqATX1AnodSN1SG+AEqV9rq6zZFLPrKHQuu+QaaFHkG6IMRoxBGgokiQiW+wQMLoU4AeMboVeksA9GIN+ZZvLEeukyucBs9xrM2nDb4AzCVhLADhZfNPAJgw26Zd4gnAurhM2zf2XDl+9Dy58L7o2OfrdQmy5Qqg9I5rH8EV6OBzhdMS+9EgSIUvgNGQTR1YEKTjjBmVkASY36xFts4rE6/OyN+mV8qol24KoIzTy/6xtrN5M/DQJ4ynKNUUj4M+vrY/g4IvboQxoFEE5or9wWgGD+LCCm4bHs8oAmhhKlWB0VyyvPdSxdlRG86YZTtebz84B3ECfhpssv4SVQAzDIObj5P6ecv2EOAPm/sm0DJ/s3/tWufUOifvssXLiXxDQGBi6m0oUMdXpHCFPHGZCTX95xqGwaziDgQYs7INQgKwMQx9PHVB3ojLWFAAa+LhNWTa1h0vgqjA9HSAdthWYhESFUBBj9Cjy1iEhAUwx97QgBCWbhEyvyL4wQQ1CMFyV2lDPEEeQsiyIiRBgLuo72UJqHMc1yBLUHHKlSVN0CovdSxC9xQgzeOaShQgcc+BFMGCCYZXz8CsEU8w1bkCapDhfNr8oqAxLq/IBatqsdspH+2DhX63WbwIC9qGUSNh4EOXewMGFRZsCAdWNUdYpwUFZo6Es/M/TAwtYUEzy8HGdvS0GWInkvMqgdrF2meIv/XLEBclUAIlAFACJVACgP9QIP2zhxRIJSX/0xMtI9HwkNFOJFOyPv9Jne7/HyIEPtcrxWwlAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://search.yahoo.co.jp/search">
+ <Param name="p" value="{searchTerms}"/>
+ <Param name="ei" value="UTF-8"/>
+ <Param name="fr" value="mozff" />
+</Url>
+<SearchForm>https://search.yahoo.co.jp/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-en.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-en.xml
new file mode 100644
index 0000000000..75ba859ca3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-en.xml
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yandex</ShortName>
+<Description>Use Yandex to search the Internet.</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAqFBMVEUAAAD29PL29PL29PP29PLd3d3w8PD29fP/JQD7ion35uT28/H8bmz36+r6paT+Lh7+KRL+Jwz41tX4ysn4wcD7h4X8dHL+OTD27+333t37lZT7jYz8aWb9Qz3+JQH36Of34+H40tH5vLv5uLj5sbD5rKv8e3n9WVb9Uk/9TUn9SEP+Myf+Jgb32tr4xMP6nJv7mpn7goH8gH78X1z9Pzb+NSn+JQD8Yl99ghQbAAAAB3RSTlMA88/IbQYbgu4OFQAAAe5JREFUaN7t2slywjAMBmCzKjSsISxhXwtlXwp9/zcr+VsQKfQU6dCpdfLooG/isWVPxsaYTDqVIIVIpNIZc4lsktQimTUmo1YfQsakSTXSJkWqkTIJUo2EIeWwgAUsYAFVwK84lyhhrALUnTA+MNYA3BKACSEUAB/1y01CKAAHAHWMYwG1/CXqg4cZOgHw4wPvKNT+ma4ifXLjAy1U6j58GNI1ig8EPZQaPv2woQBAM5TKRZMef1dsYIpajWcT1xIBCmUUq0aSizDVC+ID3BP296kAqRnJABtUW7rEsUWqKAS4DZTbEMc8TFQKQgC1AbxyYoCl2yEpYA2gxHNURGIsAvCMOCO6xhFgUw4oAsjfFu6Kp0wGKFQivX8CryoIUD5yenV4a0sBvnO3bJplNCdRwF3eLfwxNE8O4PY/5daxIFnAA3Dk68RWGKC3sOqqfz0sewNpoHhrb21upJJAH1thfv2WqSjAi/8cfG3jckEeGH0fkmPuGnIAb4UuHdD3pAHeCsMuOrcG4AHIn9FIpQHeCtxI5QFsBW6k4gC2At+E5QGcCtxI5QGcCtxIxQG+IO0IoQDQHsAaYxUgB+AFYwtYwAJEFrCABYgs8DR2zi9X97/5c9wCFrDAfwK0Hw2oP3tQf7ih/vRE/fGM+vOfTzkzCrugwvNDAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="http://suggest.yandex.com/suggest-ff.cgi">
+ <Param name="part" value="{searchTerms}"/>
+ <Param name="uil" value="tr"/>
+</Url>
+<Url type="text/html" method="GET" template="https://yandex.com/search">
+ <Param name="clid" value="2186727"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://yandex.com/search">
+ <Param name="clid" value="2186733"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-ru.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-ru.xml
new file mode 100644
index 0000000000..68d5f6bbf8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-ru.xml
@@ -0,0 +1,21 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Яндекс</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAqFBMVEUAAAD29PL29PL29PP29PLd3d3w8PD29fP/Jg76oKL33t3+Jxj27+342dn+Lyv28/H28fD36+r35+f35eT34OD9U1X+LCX4wMH5qqz7kJL+PDz7l5r8Z2v9TE3+OTj+NDH4y8v5ubr5trf5sLH8eHz8dHf9XF/9Pj/37Ov41tb40dH4x8j5vb75s7T6pKb7i438foH8bXD8YWP9UVP9RUb+KR77h4v7g4aogTBNAAAAB3RSTlMA88/IbQYbgu4OFQAAAcpJREFUaN7t2lduwkAURmETINcEV4zpvaXRA8n+dxbkX8ji3echiu8CzucBjRmhcRynXqtWDJhKtVZ3bvP8ZNg8Pd+en+pLqDs1Q6fmVA2dqlMxdCqOwVMCJVACJVAwEC9X46Dp3mdQMPCyurqPUygQ9fXoFJCkalJA8uqigDdWsbneJqGZNYoG1gqOfbsNAHTV23jGAOEly73GBgEL5T6NAjZZ7WwU0FZtiwG9LNaJMOCUxUaGAdrEQwzwFJtjgK/YBwZ0FWtjwFyxFgYsFYsxYKdYQn8H7xgwU6yLAS3FGhhgkyx24oBRFgsi+G3qzjEg1okrxQBbKbfDgLaW0PEpwPrqTd4pIPxSsNOIGMDaA1cz/VnMfN/vFw1IQI/vrTMIaPYBDNjbkQTyl1IzTdMxAnyq+m1mLwigzXBNKGCh6MEgIBpoM7cooKFm3yAgDvIFIMDhvgACyH8SgpgC1ioODQI+FAzeKGCTLwABuupNPQgIL/npEQF6+QIQwJvmC0CAoWoTDwKSjmp7g4CjYpMIAvymYj2DgJFagxACZq5maRDQUyoNH49h2ecW2J/7c7wEsimBEviXAH1pAL/2gF/cwK+e4Jdn8Os/v3xBC8Xv9bfOAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://suggest.yandex.net/suggest-ff.cgi">
+ <Param name="part" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://yandex.ru/search">
+ <Param name="clid" value="2186727"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://yandex.ru/search">
+ <Param name="clid" value="2186733"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.yandex.ru/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-tr.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-tr.xml
new file mode 100644
index 0000000000..5d798c746c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex-tr.xml
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yandex</ShortName>
+<Description>Yandex Türkiye arama motoru</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAqFBMVEUAAAD29PL29PL29PP29PLd3d3w8PD29fP/JQD7ion35uT28/H8bmz36+r6paT+Lh7+KRL+Jwz41tX4ysn4wcD7h4X8dHL+OTD27+333t37lZT7jYz8aWb9Qz3+JQH36Of34+H40tH5vLv5uLj5sbD5rKv8e3n9WVb9Uk/9TUn9SEP+Myf+Jgb32tr4xMP6nJv7mpn7goH8gH78X1z9Pzb+NSn+JQD8Yl99ghQbAAAAB3RSTlMA88/IbQYbgu4OFQAAAe5JREFUaN7t2slywjAMBmCzKjSsISxhXwtlXwp9/zcr+VsQKfQU6dCpdfLooG/isWVPxsaYTDqVIIVIpNIZc4lsktQimTUmo1YfQsakSTXSJkWqkTIJUo2EIeWwgAUsYAFVwK84lyhhrALUnTA+MNYA3BKACSEUAB/1y01CKAAHAHWMYwG1/CXqg4cZOgHw4wPvKNT+ma4ifXLjAy1U6j58GNI1ig8EPZQaPv2woQBAM5TKRZMef1dsYIpajWcT1xIBCmUUq0aSizDVC+ID3BP296kAqRnJABtUW7rEsUWqKAS4DZTbEMc8TFQKQgC1AbxyYoCl2yEpYA2gxHNURGIsAvCMOCO6xhFgUw4oAsjfFu6Kp0wGKFQivX8CryoIUD5yenV4a0sBvnO3bJplNCdRwF3eLfwxNE8O4PY/5daxIFnAA3Dk68RWGKC3sOqqfz0sewNpoHhrb21upJJAH1thfv2WqSjAi/8cfG3jckEeGH0fkmPuGnIAb4UuHdD3pAHeCsMuOrcG4AHIn9FIpQHeCtxI5QFsBW6k4gC2At+E5QGcCtxI5QGcCtxIxQG+IO0IoQDQHsAaYxUgB+AFYwtYwAJEFrCABYgs8DR2zi9X97/5c9wCFrDAfwK0Hw2oP3tQf7ih/vRE/fGM+vOfTzkzCrugwvNDAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="http://suggest.yandex.com.tr/suggest-ff.cgi">
+ <Param name="part" value="{searchTerms}"/>
+ <Param name="uil" value="tr"/>
+</Url>
+<Url type="text/html" method="GET" template="https://yandex.com.tr/search">
+ <Param name="clid" value="2186727"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://yandex.com.tr/search">
+ <Param name="clid" value="2186733"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex.by.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex.by.xml
new file mode 100644
index 0000000000..a4c6ea7a55
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex.by.xml
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Яндекс</ShortName>
+<Description>Пошук з дапамогаю Яндекс</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAqFBMVEUAAAD29PL29PL29PP29PLd3d3w8PD29fP/Jg76oKL33t3+Jxj27+342dn+Lyv28/H28fD36+r35+f35eT34OD9U1X+LCX4wMH5qqz7kJL+PDz7l5r8Z2v9TE3+OTj+NDH4y8v5ubr5trf5sLH8eHz8dHf9XF/9Pj/37Ov41tb40dH4x8j5vb75s7T6pKb7i438foH8bXD8YWP9UVP9RUb+KR77h4v7g4aogTBNAAAAB3RSTlMA88/IbQYbgu4OFQAAAcpJREFUaN7t2lduwkAURmETINcEV4zpvaXRA8n+dxbkX8ji3echiu8CzucBjRmhcRynXqtWDJhKtVZ3bvP8ZNg8Pd+en+pLqDs1Q6fmVA2dqlMxdCqOwVMCJVACJVAwEC9X46Dp3mdQMPCyurqPUygQ9fXoFJCkalJA8uqigDdWsbneJqGZNYoG1gqOfbsNAHTV23jGAOEly73GBgEL5T6NAjZZ7WwU0FZtiwG9LNaJMOCUxUaGAdrEQwzwFJtjgK/YBwZ0FWtjwFyxFgYsFYsxYKdYQn8H7xgwU6yLAS3FGhhgkyx24oBRFgsi+G3qzjEg1okrxQBbKbfDgLaW0PEpwPrqTd4pIPxSsNOIGMDaA1cz/VnMfN/vFw1IQI/vrTMIaPYBDNjbkQTyl1IzTdMxAnyq+m1mLwigzXBNKGCh6MEgIBpoM7cooKFm3yAgDvIFIMDhvgACyH8SgpgC1ioODQI+FAzeKGCTLwABuupNPQgIL/npEQF6+QIQwJvmC0CAoWoTDwKSjmp7g4CjYpMIAvymYj2DgJFagxACZq5maRDQUyoNH49h2ecW2J/7c7wEsimBEviXAH1pAL/2gF/cwK+e4Jdn8Os/v3xBC8Xv9bfOAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://suggest.yandex.by/suggest-ff.cgi">
+ <Param name="part" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://yandex.by/search">
+ <Param name="clid" value="2186727"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://yandex.by/search">
+ <Param name="clid" value="2186733"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.yandex.by/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex.xml
new file mode 100644
index 0000000000..801ff5cbef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/yandex.xml
@@ -0,0 +1,21 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Яндекс</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAqFBMVEUAAAD29PL29PL29PP29PLd3d3w8PD29fP/Jg76oKL33t3+Jxj27+342dn+Lyv28/H28fD36+r35+f35eT34OD9U1X+LCX4wMH5qqz7kJL+PDz7l5r8Z2v9TE3+OTj+NDH4y8v5ubr5trf5sLH8eHz8dHf9XF/9Pj/37Ov41tb40dH4x8j5vb75s7T6pKb7i438foH8bXD8YWP9UVP9RUb+KR77h4v7g4aogTBNAAAAB3RSTlMA88/IbQYbgu4OFQAAAcpJREFUaN7t2lduwkAURmETINcEV4zpvaXRA8n+dxbkX8ji3echiu8CzucBjRmhcRynXqtWDJhKtVZ3bvP8ZNg8Pd+en+pLqDs1Q6fmVA2dqlMxdCqOwVMCJVACJVAwEC9X46Dp3mdQMPCyurqPUygQ9fXoFJCkalJA8uqigDdWsbneJqGZNYoG1gqOfbsNAHTV23jGAOEly73GBgEL5T6NAjZZ7WwU0FZtiwG9LNaJMOCUxUaGAdrEQwzwFJtjgK/YBwZ0FWtjwFyxFgYsFYsxYKdYQn8H7xgwU6yLAS3FGhhgkyx24oBRFgsi+G3qzjEg1okrxQBbKbfDgLaW0PEpwPrqTd4pIPxSsNOIGMDaA1cz/VnMfN/vFw1IQI/vrTMIaPYBDNjbkQTyl1IzTdMxAnyq+m1mLwigzXBNKGCh6MEgIBpoM7cooKFm3yAgDvIFIMDhvgACyH8SgpgC1ioODQI+FAzeKGCTLwABuupNPQgIL/npEQF6+QIQwJvmC0CAoWoTDwKSjmp7g4CjYpMIAvymYj2DgJFagxACZq5maRDQUyoNH49h2ecW2J/7c7wEsimBEviXAH1pAL/2gF/cwK+e4Jdn8Os/v3xBC8Xv9bfOAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://suggest.yandex.net/suggest-ff.cgi">
+ <Param name="part" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://yandex.kz/search">
+ <Param name="clid" value="2186727"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://yandex.kz/search">
+ <Param name="clid" value="2186733"/>
+ <Param name="text" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://www.yandex.kz/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/youtube.xml b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/youtube.xml
new file mode 100644
index 0000000000..60a7897ae4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/assets/searchplugins/youtube.xml
@@ -0,0 +1,12 @@
+<?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/. -->
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>YouTube</ShortName>
+ <Description>Search for videos on YouTube</Description>
+ <Tags>youtube video</Tags>
+ <Image height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABa0lEQVR42u2YoYoCURSGBzSLk62L0TJvsEywWAxabRaFlQ2GQdY38AF8Ci0Gsy9gECybRU0GkQn++4fLbjm7K8wdmQPng6+I3rkfqHPnBIZhGIbxCAiCkL7QiL7SFu3QHu3TIX2nY5rQCf1wTtxrY/eeoftMz63RcmtG7hqhj82+0RX9pDeKJ3tz1165vVQf3XxMTxQF80Tj/zbfoFeKgnqljb8C1hQFd/3b5mv0riDgTmtSQJtCiW0pIFEUkEgBc0UBcylgkXnhKAIqlWcELKSATeaFp1PgeAT6faBUyjNgIwXsfQR8s90CcZxXwF4KOHgM+GG5BOp13wEHKeDiP8CRpsBsBoShr4CLFJB6DJA5n4HBACiXswakUsA9hwCZ3Q5oNjPdjfUH6P8K6f8R6/8b1X8j03+U0H+Y03+c1v9Ao/+RUv9Dvf6xiv7Blv7RojzcHRVouDui1TzG610P4/WuOF43DMMwjAf4ArcA0xFiL427AAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" template="https://www.youtube.com/results?search_query={searchTerms}&amp;page={startPage?}&amp;utm_source=opensearch" />
+ <Query role="example" searchTerms="cat" />
+</SearchPlugin>
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/BrowserStoreSearchAdapter.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/BrowserStoreSearchAdapter.kt
new file mode 100644
index 0000000000..ffc6179d84
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/BrowserStoreSearchAdapter.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.search
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.search.SearchRequest
+
+/**
+ * Adapter which wraps a [browserStore] in order to fulfill the [SearchAdapter] interface.
+ *
+ * This class uses the [browserStore] to determine when private mode is active, and updates the
+ * [browserStore] whenever a new search has been requested.
+ *
+ * NOTE: this will add [SearchRequest]s to [BrowserStore.state] when [sendSearch] is called. Client
+ * code is responsible for consuming these requests and displaying something to the user.
+ *
+ * NOTE: client code is responsible for sending [ContentAction.ConsumeSearchRequestAction]s
+ * after consuming events. See [SearchFeature] for a component that will handle this for you.
+ *
+ * @param browserStore The application's [BrowserStore].
+ * @param tabId ID of the tab that requests the search, or null to use the selected tab.
+ */
+class BrowserStoreSearchAdapter(
+ private val browserStore: BrowserStore,
+ private val tabId: String? = null,
+) : SearchAdapter {
+
+ override fun sendSearch(isPrivate: Boolean, text: String) {
+ val selectedTabId = tabId ?: browserStore.state.selectedTabId ?: return
+ browserStore.dispatch(
+ ContentAction.UpdateSearchRequestAction(selectedTabId, SearchRequest(isPrivate, text)),
+ )
+ }
+
+ override fun isPrivateSession(): Boolean =
+ browserStore.state.findTabOrCustomTabOrSelectedTab(tabId)?.content?.private ?: false
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchAdapter.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchAdapter.kt
new file mode 100644
index 0000000000..0d06911936
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchAdapter.kt
@@ -0,0 +1,21 @@
+/* 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.search
+
+/**
+ * May be implemented by client code in order to allow a component to start searches.
+ */
+interface SearchAdapter {
+
+ /**
+ * Called by the component to indicate that the user should be shown a search.
+ */
+ fun sendSearch(isPrivate: Boolean, text: String)
+
+ /**
+ * Called by the component to check whether or not the currently selected session is private.
+ */
+ fun isPrivateSession(): Boolean
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchFeature.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchFeature.kt
new file mode 100644
index 0000000000..61d89a2844
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchFeature.kt
@@ -0,0 +1,52 @@
+/* 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.search
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.search.SearchRequest
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.utils.ext.toNullablePair
+
+/**
+ * Lifecycle aware feature that forwards [SearchRequest]s from the [store] to [performSearch], and
+ * then consumes them.
+ *
+ * NOTE: that this only consumes SearchRequests, and will not hook up the [store] to a source of
+ * SearchRequests. See [mozilla.components.concept.engine.selection.SelectionActionDelegate] for use
+ * in generating SearchRequests.
+ */
+class SearchFeature(
+ private val store: BrowserStore,
+ private val tabId: String? = null,
+ private val performSearch: (SearchRequest, tabId: String) -> Unit,
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .distinctUntilChangedBy { it?.content?.searchRequest }
+ // Do nothing if searchRequest or sessionId is null
+ .mapNotNull { tab -> Pair(tab?.content?.searchRequest, tab?.id).toNullablePair() }
+ .collect { (searchRequest, sessionId) ->
+ performSearch(searchRequest, sessionId)
+ store.dispatch(ContentAction.ConsumeSearchRequestAction(sessionId))
+ }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt
new file mode 100644
index 0000000000..b3e8a1763b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt
@@ -0,0 +1,343 @@
+/* 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.search
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.feature.search.ext.buildSearchUrl
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Contains use cases related to the search feature.
+ */
+class SearchUseCases(
+ store: BrowserStore,
+ tabsUseCases: TabsUseCases,
+ sessionUseCases: SessionUseCases,
+) {
+ interface SearchUseCase {
+ /**
+ * Triggers a search.
+ */
+ fun invoke(
+ searchTerms: String,
+ searchEngine: SearchEngine? = null,
+ parentSessionId: String? = null,
+ )
+ }
+
+ class DefaultSearchUseCase(
+ private val store: BrowserStore,
+ private val tabsUseCases: TabsUseCases,
+ private val sessionUseCases: SessionUseCases,
+ ) : SearchUseCase {
+ private val logger = Logger("DefaultSearchUseCase")
+
+ /**
+ * Triggers a search in the currently selected session.
+ */
+ override fun invoke(
+ searchTerms: String,
+ searchEngine: SearchEngine?,
+ parentSessionId: String?,
+ ) {
+ invoke(searchTerms, store.state.selectedTabId, searchEngine)
+ }
+
+ /**
+ * Triggers a search using the default search engine for the provided search terms.
+ *
+ * @param searchTerms the search terms.
+ * @param sessionId the ID of the session/tab to use, or null if the currently selected tab
+ * should be used.
+ * @param searchEngine Search Engine to use, or the default search engine if none is provided
+ * @param flags Flags that will be used when loading the URL.
+ * @param additionalHeaders The extra headers to use when loading the URL.
+ */
+ operator fun invoke(
+ searchTerms: String,
+ sessionId: String? = store.state.selectedTabId,
+ searchEngine: SearchEngine? = null,
+ flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ additionalHeaders: Map<String, String>? = null,
+ ) {
+ val searchUrl = searchEngine?.let {
+ searchEngine.buildSearchUrl(searchTerms)
+ } ?: store.state.search.selectedOrDefaultSearchEngine?.buildSearchUrl(searchTerms)
+
+ if (searchUrl == null) {
+ logger.warn("No default search engine available to perform search")
+ return
+ }
+
+ val id = if (sessionId == null) {
+ // If no `sessionId` was passed in then create a new tab
+ tabsUseCases.addTab(
+ url = searchUrl,
+ flags = flags,
+ isSearch = true,
+ searchEngineName = searchEngine?.name,
+ additionalHeaders = additionalHeaders,
+ )
+ } else {
+ // If we got a `sessionId` then try to find the tab and load the search URL in it
+ val existingTab = store.state.findTabOrCustomTab(sessionId)
+ if (existingTab != null) {
+ store.dispatch(ContentAction.UpdateIsSearchAction(existingTab.id, true, searchEngine?.name))
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ tabId = existingTab.id,
+ url = searchUrl,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ ),
+ )
+ existingTab.id
+ } else {
+ // If the tab with the provided id was not found then create a new tab
+ tabsUseCases.addTab(
+ url = searchUrl,
+ isSearch = true,
+ searchEngineName = searchEngine?.name,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ )
+ }
+ }
+
+ store.dispatch(ContentAction.UpdateSearchTermsAction(id, searchTerms))
+ }
+ }
+
+ class NewTabSearchUseCase(
+ private val store: BrowserStore,
+ private val tabsUseCases: TabsUseCases,
+ private val isPrivate: Boolean,
+ ) : SearchUseCase {
+ private val logger = Logger("NewTabSearchUseCase")
+
+ override fun invoke(
+ searchTerms: String,
+ searchEngine: SearchEngine?,
+ parentSessionId: String?,
+ ) {
+ invoke(
+ searchTerms,
+ source = SessionState.Source.Internal.None,
+ selected = true,
+ searchEngine = searchEngine,
+ parentSessionId = parentSessionId,
+ )
+ }
+
+ /**
+ * Triggers a search on a new session, using the default search engine for the provided search terms.
+ *
+ * @param searchTerms the search terms.
+ * @param selected whether or not the new session should be selected, defaults to true.
+ * @param source the source of the new session.
+ * @param searchEngine Search Engine to use, or the default search engine if none is provided
+ * @param parentSessionId optional parent session to attach this new search session to
+ * @param flags Flags that will be used when loading the URL.
+ * @param additionalHeaders The extra headers to use when loading the URL.
+ */
+ operator fun invoke(
+ searchTerms: String,
+ source: SessionState.Source,
+ selected: Boolean = true,
+ searchEngine: SearchEngine? = null,
+ parentSessionId: String? = null,
+ flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ additionalHeaders: Map<String, String>? = null,
+ ) {
+ val searchUrl = searchEngine?.let {
+ searchEngine.buildSearchUrl(searchTerms)
+ } ?: store.state.search.selectedOrDefaultSearchEngine?.buildSearchUrl(searchTerms)
+
+ if (searchUrl == null) {
+ logger.warn("No default search engine available to perform search")
+ return
+ }
+
+ val id = tabsUseCases.addTab(
+ url = searchUrl,
+ parentId = parentSessionId,
+ flags = flags,
+ source = source,
+ selectTab = selected,
+ private = isPrivate,
+ isSearch = true,
+ additionalHeaders = additionalHeaders,
+ )
+
+ store.dispatch(ContentAction.UpdateSearchTermsAction(id, searchTerms))
+ }
+ }
+
+ /**
+ * Adds a new search engine to the list of search engines the user can use for searches.
+ */
+ class AddNewSearchEngineUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Adds the given [searchEngine] to the list of search engines the user can use for searches.
+ */
+ operator fun invoke(
+ searchEngine: SearchEngine,
+ ) {
+ when (searchEngine.type) {
+ SearchEngine.Type.BUNDLED -> store.dispatch(
+ SearchAction.ShowSearchEngineAction(searchEngine.id),
+ )
+
+ SearchEngine.Type.BUNDLED_ADDITIONAL -> store.dispatch(
+ SearchAction.AddAdditionalSearchEngineAction(searchEngine.id),
+ )
+
+ SearchEngine.Type.CUSTOM -> store.dispatch(
+ SearchAction.UpdateCustomSearchEngineAction(searchEngine),
+ )
+
+ SearchEngine.Type.APPLICATION -> { /* Do nothing */ }
+ }
+ }
+ }
+
+ /**
+ * Removes a search engine from the list of search engines the user can use for searches.
+ */
+ class RemoveExistingSearchEngineUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Removes the given [searchEngine] from the list of search engines the user can use for
+ * searches.
+ */
+ operator fun invoke(
+ searchEngine: SearchEngine,
+ ) {
+ when (searchEngine.type) {
+ SearchEngine.Type.BUNDLED -> store.dispatch(
+ SearchAction.HideSearchEngineAction(searchEngine.id),
+ )
+
+ SearchEngine.Type.BUNDLED_ADDITIONAL -> store.dispatch(
+ SearchAction.RemoveAdditionalSearchEngineAction(searchEngine.id),
+ )
+
+ SearchEngine.Type.CUSTOM -> store.dispatch(
+ SearchAction.RemoveCustomSearchEngineAction(searchEngine.id),
+ )
+
+ SearchEngine.Type.APPLICATION -> { /* Do nothing */ }
+ }
+ }
+ }
+
+ /**
+ * Marks a search engine as "selected" by the user to be the default search engine to perform
+ * searches with.
+ */
+ class SelectSearchEngineUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Marks the given [searchEngine] as "selected" by the user to be the default search engine
+ * to perform searches with.
+ */
+ operator fun invoke(
+ searchEngine: SearchEngine,
+ ) {
+ val name = if (searchEngine.type == SearchEngine.Type.BUNDLED) {
+ // For bundled search engines we additionally save the name of the search engine.
+ // We do this because with "home" region changes the previous search plugin/id
+ // may no longer be available, but there may be a clone of the search engine with
+ // a different plugin/id using the same name.
+ // This should be safe to do since Fenix as well as Fennec only kept the name of
+ // the default search engine.
+ // For all other cases (e.g. custom search engines) we only care about the ID and
+ // do not want to switch to a different search engine based on its name once it is
+ // gone.
+ searchEngine.name
+ } else {
+ null
+ }
+
+ store.dispatch(
+ SearchAction.SelectSearchEngineAction(searchEngine.id, name),
+ )
+ }
+ }
+
+ /**
+ * Updates the list of unselected shortcuts, to be hidden from the quick search menus.
+ */
+ class UpdateDisabledSearchEngineIdsUseCase(private val store: BrowserStore) {
+ /**
+ * Updates the list of unselected shortcuts with the given [searchEngineId], to be hidden from
+ * the quick search menus.
+ */
+ operator fun invoke(
+ searchEngineId: String,
+ isEnabled: Boolean,
+ ) {
+ store.dispatch(SearchAction.UpdateDisabledSearchEngineIdsAction(searchEngineId, isEnabled))
+ }
+ }
+
+ /**
+ * Restores bundled search engines that may have been removed.
+ */
+ class RestoreHiddenSearchEnginesUseCase(private val store: BrowserStore) {
+ /**
+ * Restores all hidden engines back to the bundled engine list.
+ */
+ operator fun invoke() {
+ store.dispatch(SearchAction.RestoreHiddenSearchEnginesAction)
+ }
+ }
+
+ val defaultSearch: DefaultSearchUseCase by lazy {
+ DefaultSearchUseCase(store, tabsUseCases, sessionUseCases)
+ }
+
+ val newTabSearch: NewTabSearchUseCase by lazy {
+ NewTabSearchUseCase(store, tabsUseCases, false)
+ }
+
+ val newPrivateTabSearch: NewTabSearchUseCase by lazy {
+ NewTabSearchUseCase(store, tabsUseCases, true)
+ }
+
+ val addSearchEngine: AddNewSearchEngineUseCase by lazy {
+ AddNewSearchEngineUseCase(store)
+ }
+
+ val removeSearchEngine: RemoveExistingSearchEngineUseCase by lazy {
+ RemoveExistingSearchEngineUseCase(store)
+ }
+
+ val selectSearchEngine: SelectSearchEngineUseCase by lazy {
+ SelectSearchEngineUseCase(store)
+ }
+
+ val updateDisabledSearchEngineIds: UpdateDisabledSearchEngineIdsUseCase by lazy {
+ UpdateDisabledSearchEngineIdsUseCase(store)
+ }
+
+ val restoreHiddenSearchEngines: RestoreHiddenSearchEnginesUseCase by lazy {
+ RestoreHiddenSearchEnginesUseCase(store)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/BrowserStore.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/BrowserStore.kt
new file mode 100644
index 0000000000..2fce10dfe8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/BrowserStore.kt
@@ -0,0 +1,36 @@
+/* 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.search.ext
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.Store
+
+/**
+ * Waits (asynchronously, non-blocking) for the search state to be loaded from disk (when using
+ * `RegionMiddleware` and `SearchMiddleware`) and invokes [block] with the default search engine
+ * (or `null` if no default could be loaded).
+ */
+fun BrowserStore.waitForSelectedOrDefaultSearchEngine(
+ block: (mozilla.components.browser.state.search.SearchEngine?) -> Unit,
+) {
+ // Did we already load the search state? In that case we can invoke `block` immediately.
+ if (state.search.complete) {
+ block(state.search.selectedOrDefaultSearchEngine)
+ return
+ }
+
+ // Otherwise: Wait for the search state to be loaded and then invoke `block`.
+ var subscription: Store.Subscription<BrowserState, BrowserAction>? = null
+ subscription = observeManually { state ->
+ if (state.search.complete) {
+ block(state.search.selectedOrDefaultSearchEngine)
+ subscription!!.unsubscribe()
+ }
+ }
+ subscription.resume()
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt
new file mode 100644
index 0000000000..08615935f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt
@@ -0,0 +1,148 @@
+/* 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.search.ext
+
+import android.graphics.Bitmap
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.search.OS_SEARCH_ENGINE_TERMS_PARAM
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.browser.state.state.searchEngines
+import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
+import mozilla.components.feature.search.internal.SearchUrlBuilder
+import mozilla.components.feature.search.storage.SearchEngineReader
+import java.io.InputStream
+import java.lang.IllegalArgumentException
+import java.util.UUID
+
+/**
+ * Creates a custom [SearchEngine].
+ */
+fun createSearchEngine(
+ name: String,
+ url: String,
+ icon: Bitmap,
+ inputEncoding: String? = null,
+ suggestUrl: String? = null,
+ isGeneral: Boolean = false,
+): SearchEngine {
+ if (!url.contains(OS_SEARCH_ENGINE_TERMS_PARAM)) {
+ throw IllegalArgumentException("URL does not contain search terms placeholder")
+ }
+
+ return SearchEngine(
+ id = UUID.randomUUID().toString(),
+ name = name,
+ icon = icon,
+ inputEncoding = inputEncoding,
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf(url),
+ suggestUrl = suggestUrl,
+ isGeneral = isGeneral,
+ )
+}
+
+/**
+ * Creates an application [SearchEngine].
+ */
+fun createApplicationSearchEngine(
+ id: String? = null,
+ name: String,
+ url: String,
+ inputEncoding: String? = null,
+ icon: Bitmap,
+ suggestUrl: String? = null,
+): SearchEngine {
+ return SearchEngine(
+ id = id ?: UUID.randomUUID().toString(),
+ name = name,
+ icon = icon,
+ inputEncoding = inputEncoding,
+ type = SearchEngine.Type.APPLICATION,
+ resultUrls = listOf(url),
+ suggestUrl = suggestUrl,
+ )
+}
+
+/**
+ * Whether this [SearchEngine] has a [SearchEngine.suggestUrl] set and can provide search
+ * suggestions.
+ */
+val SearchEngine.canProvideSearchSuggestions: Boolean
+ get() = suggestUrl != null
+
+/**
+ * Creates an URL to retrieve search suggestions for the provided [query].
+ */
+fun SearchEngine.buildSuggestionsURL(query: String): String? {
+ val builder = SearchUrlBuilder(this)
+ return builder.buildSuggestionUrl(query)
+}
+
+/**
+ * Builds a URL to search for the given search terms with this search engine.
+ */
+fun SearchEngine.buildSearchUrl(searchTerm: String): String {
+ val builder = SearchUrlBuilder(this)
+ return builder.buildSearchUrl(searchTerm)
+}
+
+/**
+ * Parses a [SearchEngine] from the given [stream].
+ */
+@Deprecated("Only for migrating legacy search engines. Will eventually be removed.")
+fun parseLegacySearchEngine(id: String, stream: InputStream): SearchEngine {
+ val reader = SearchEngineReader(SearchEngine.Type.CUSTOM)
+ return reader.loadStream(id, stream)
+}
+
+/**
+ * Given a [SearchState], determine if the passed-in [url] is a known search results page url
+ * and what are the associated search terms.
+ * @return Search terms if [url] is a known search results page, `null` otherwise.
+ */
+fun SearchState.parseSearchTerms(url: String): String? {
+ val parsedUrl = Uri.parse(url)
+ // Default/selected engine is the most likely to match, check it first.
+ val currentEngine = this.selectedOrDefaultSearchEngine
+ // Or go through the rest of known engines.
+ val fallback: () -> String? = fallback@{
+ this.searchEngines.forEach { searchEngine ->
+ searchEngine.parseSearchTerms(parsedUrl)?.let { return@fallback it }
+ }
+ return@fallback null
+ }
+ return currentEngine?.parseSearchTerms(parsedUrl) ?: fallback()
+}
+
+/**
+ * Given a [SearchEngine], determine if the passed-in [url] matches its results template,
+ * and what are the associated search terms.
+ * @return Search terms if [url] matches the results page template, `null` otherwise.
+ */
+@VisibleForTesting
+fun SearchEngine.parseSearchTerms(url: Uri): String? {
+ // Basic approach:
+ // - look at the "base" of the template url; if there's a match, continue
+ // - see if the GET parameter for the search terms is present in the url
+ // - if that param present, its value is our answer if it's non-empty
+ val searchResultsRoot = this.resultsUrl.authority + this.resultsUrl.path
+ val urlRoot = url.authority + url.path
+
+ return if (searchResultsRoot == urlRoot) {
+ val searchTerms = try {
+ this.searchParameterName?.let {
+ url.getQueryParameter(it)
+ }
+ } catch (e: UnsupportedOperationException) {
+ // Non-hierarchical url.
+ null
+ }
+ searchTerms.takeUnless { it.isNullOrEmpty() }
+ } else {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/internal/SearchUrlBuilder.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/internal/SearchUrlBuilder.kt
new file mode 100644
index 0000000000..9fc9f61dd5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/internal/SearchUrlBuilder.kt
@@ -0,0 +1,93 @@
+/* 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.search.internal
+
+import android.net.Uri
+import android.text.TextUtils
+import mozilla.components.browser.state.search.OS_SEARCH_ENGINE_TERMS_PARAM
+import mozilla.components.browser.state.search.SearchEngine
+import java.io.UnsupportedEncodingException
+import java.net.URLEncoder
+import java.util.Locale
+
+// We are using string concatenation here to avoid the Kotlin compiler interpreting this
+// as string templates. It is possible to escape the string accordingly. But this seems to
+// be inconsistent between Kotlin versions. So to be safe we avoid this completely by
+// constructing the strings manually.
+
+// Parameters copied from nsSearchService.js
+private const val MOZ_PARAM_LOCALE = "{" + "moz:locale" + "}"
+private const val MOZ_PARAM_DIST_ID = "{" + "moz:distributionID" + "}"
+private const val MOZ_PARAM_OFFICIAL = "{" + "moz:official" + "}"
+
+// Supported OpenSearch parameters
+// See http://opensearch.a9.com/spec/1.1/querysyntax/#core
+private const val OS_PARAM_USER_DEFINED = OS_SEARCH_ENGINE_TERMS_PARAM
+private const val OS_PARAM_INPUT_ENCODING = "{" + "inputEncoding" + "}"
+private const val OS_PARAM_LANGUAGE = "{" + "language" + "}"
+private const val OS_PARAM_OUTPUT_ENCODING = "{" + "outputEncoding" + "}"
+private const val OS_PARAM_OPTIONAL = "\\{" + "(?:\\w+:)?\\w+?" + "\\}"
+
+internal class SearchUrlBuilder(
+ private val searchEngine: SearchEngine,
+) {
+ fun buildSearchUrl(searchTerms: String): String {
+ // The parser should have put the best URL for this device at the beginning of the list.
+ val template = searchEngine.resultUrls[0]
+ return buildUrl(template, searchTerms)
+ }
+
+ fun buildSuggestionUrl(searchTerms: String): String? {
+ val template = searchEngine.suggestUrl ?: return null
+ return buildUrl(template, searchTerms)
+ }
+
+ private fun buildUrl(template: String, searchTerms: String): String {
+ val templateUri = Uri.decode(template)
+ val inputEncoding = searchEngine.inputEncoding ?: "UTF-8"
+ val query = try {
+ // Although android.net.Uri.encode convert space (U+0x20) to "%20", java.net.URLEncoder convert it to "+".
+ URLEncoder.encode(searchTerms, inputEncoding).replace("+", "%20")
+ } catch (e: UnsupportedEncodingException) {
+ Uri.encode(searchTerms)
+ }
+ val urlWithSubstitutions = paramSubstitution(templateUri, query, inputEncoding)
+ return normalize(urlWithSubstitutions) // User-entered search engines may need normalization.
+ }
+}
+
+/**
+ * Formats template string with proper parameters. Modeled after ParamSubstitution in nsSearchService.js
+ */
+private fun paramSubstitution(template: String, query: String, inputEncoding: String): String {
+ var result = template
+ val locale = Locale.getDefault().toString()
+
+ result = result.replace(MOZ_PARAM_LOCALE, locale)
+ result = result.replace(MOZ_PARAM_DIST_ID, "")
+ result = result.replace(MOZ_PARAM_OFFICIAL, "unofficial")
+
+ result = result.replace(OS_PARAM_USER_DEFINED, query)
+ result = result.replace(OS_PARAM_INPUT_ENCODING, inputEncoding)
+
+ result = result.replace(OS_PARAM_LANGUAGE, locale)
+ result = result.replace(OS_PARAM_OUTPUT_ENCODING, "UTF-8")
+
+ // Replace any optional parameters
+ result = result.replace(OS_PARAM_OPTIONAL.toRegex(), "")
+
+ return result
+}
+
+private fun normalize(input: String): String {
+ val trimmedInput = input.trim { it <= ' ' }
+ var uri = Uri.parse(trimmedInput)
+
+ if (TextUtils.isEmpty(uri.scheme)) {
+ uri = Uri.parse("http://$trimmedInput")
+ }
+
+ return uri.toString()
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddleware.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddleware.kt
new file mode 100644
index 0000000000..5139af8740
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddleware.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.search.middleware
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * [BrowserStore] middleware to be used alongside with [AdsTelemetry] to check when an ad shown
+ * in search results is clicked.
+ */
+class AdsTelemetryMiddleware(
+ private val adsTelemetry: AdsTelemetry,
+) : Middleware<BrowserState, BrowserAction> {
+ @VisibleForTesting
+ internal val redirectChain = mutableMapOf<String, RedirectChain>()
+ private val logger = Logger("AdsTelemetryMiddleware")
+
+ @Suppress("TooGenericExceptionCaught")
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is ContentAction.UpdateLoadRequestAction -> {
+ context.state.findTab(action.sessionId)?.let { tab ->
+ // Collect all load requests in between location changes
+ if (!redirectChain.containsKey(action.sessionId) && action.loadRequest.url != tab.content.url) {
+ redirectChain[action.sessionId] = RedirectChain(tab.content.url)
+ }
+
+ redirectChain[action.sessionId]?.add(action.loadRequest.url)
+ }
+ }
+ is ContentAction.UpdateUrlAction -> {
+ redirectChain[action.sessionId]?.let {
+ // Record ads telemetry providing all redirects
+ try {
+ adsTelemetry.checkIfAddWasClicked(it.root, it.chain)
+ } catch (t: Throwable) {
+ logger.info("Failed to record search telemetry", t)
+ } finally {
+ redirectChain.remove(action.sessionId)
+ }
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+ }
+}
+
+/**
+ * Utility to collect URLs / load requests in between location changes.
+ */
+@VisibleForTesting
+internal class RedirectChain(val root: String) {
+ val chain = mutableListOf<String>()
+
+ fun add(url: String) {
+ chain.add(url)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/SearchMiddleware.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/SearchMiddleware.kt
new file mode 100644
index 0000000000..c748ce9c21
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/SearchMiddleware.kt
@@ -0,0 +1,397 @@
+/* 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.search.middleware
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.feature.search.storage.BundledSearchEnginesStorage
+import mozilla.components.feature.search.storage.CustomSearchEngineStorage
+import mozilla.components.feature.search.storage.SearchMetadataStorage
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+import mozilla.components.support.base.log.logger.Logger
+import java.util.Locale
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Holds data for the search extra params.
+ */
+data class SearchExtraParams(
+ val searchEngineName: String,
+ val featureEnablerName: String?,
+ val featureEnablerParam: String?,
+ val channelIdName: String,
+ val channelIdParam: String,
+)
+
+/**
+ * [Middleware] implementation for loading and saving [SearchEngine]s whenever the state changes.
+ *
+ * @param additionalBundledSearchEngineIds List of (bundled) search engine IDs that will be loaded
+ * in addition to the search engines for the user's region and made available through
+ * [SearchState.additionalSearchEngines] and [SearchState.additionalSearchEngines].
+ * @param migration Interface for a class that can provide data from a legacy system to be imported into the
+ * storage used by the middleware.
+ * @param customStorage A storage for custom search engines of the user.
+ * @param bundleStorage A storage for loading bundled search engines.
+ * @param metadataStorage A storage for saving additional metadata related to search.
+ * @param searchExtraParams Optional search extra params.
+ * @param ioDispatcher The coroutine dispatcher to be used when loading.
+ */
+class SearchMiddleware(
+ context: Context,
+ private val additionalBundledSearchEngineIds: List<String> = emptyList(),
+ private val migration: Migration? = null,
+ private val customStorage: CustomStorage = CustomSearchEngineStorage(context),
+ private val bundleStorage: BundleStorage = BundledSearchEnginesStorage(context),
+ private val metadataStorage: MetadataStorage = SearchMetadataStorage(
+ context,
+ additionalBundledSearchEngineIds.toSet(),
+ ),
+ private val searchExtraParams: SearchExtraParams? = null,
+ private val ioDispatcher: CoroutineContext = Dispatchers.IO,
+) : Middleware<BrowserState, BrowserAction> {
+ private val logger = Logger("SearchMiddleware")
+ private val scope = CoroutineScope(ioDispatcher)
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is SearchAction.SetRegionAction -> loadSearchEngines(context.store, action.regionState, action.distribution)
+ is SearchAction.UpdateCustomSearchEngineAction -> saveCustomSearchEngine(action)
+ is SearchAction.RemoveCustomSearchEngineAction -> removeCustomSearchEngine(action)
+ is SearchAction.SelectSearchEngineAction -> updateSearchEngineSelection(action)
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+
+ when (action) {
+ is SearchAction.ShowSearchEngineAction, is SearchAction.HideSearchEngineAction,
+ is SearchAction.RestoreHiddenSearchEnginesAction,
+ -> updateHiddenSearchEngines(context.state.search.hiddenSearchEngines)
+ is SearchAction.AddAdditionalSearchEngineAction, is SearchAction.RemoveAdditionalSearchEngineAction ->
+ updateAdditionalSearchEngines(context.state.search.additionalSearchEngines)
+ is SearchAction.UpdateDisabledSearchEngineIdsAction -> updateDisabledSearchEngineIds(
+ context.store,
+ action,
+ )
+ else -> {
+ // no-op
+ }
+ }
+ }
+
+ private fun loadSearchEngines(
+ store: Store<BrowserState, BrowserAction>,
+ region: RegionState,
+ distribution: String? = null,
+ ) = scope.launch {
+ val migrationValues = migration?.getValuesToMigrate()
+ performCustomSearchEnginesMigration(migrationValues)
+
+ val regionBundle = async(ioDispatcher) {
+ bundleStorage.load(
+ region = region,
+ distribution = distribution,
+ searchExtraParams = searchExtraParams,
+ coroutineContext = ioDispatcher,
+ )
+ }
+ val customSearchEngines = async(ioDispatcher) { customStorage.loadSearchEngineList() }
+ val hiddenSearchEngineIds = async(ioDispatcher) { metadataStorage.getHiddenSearchEngines() }
+ val disabledSearchEngineIds = async(ioDispatcher) { metadataStorage.getDisabledSearchEngineIds() }
+ val additionalSearchEngineIds = async(ioDispatcher) { metadataStorage.getAdditionalSearchEngines() }
+ val allAdditionalSearchEngines = async(ioDispatcher) {
+ bundleStorage.load(
+ ids = additionalBundledSearchEngineIds,
+ searchExtraParams = searchExtraParams,
+ coroutineContext = ioDispatcher,
+ )
+ }
+
+ val hiddenSearchEngines = mutableListOf<SearchEngine>()
+ val filteredRegionSearchEngines = regionBundle.await().list.filter { searchEngine ->
+ if (hiddenSearchEngineIds.await().contains(searchEngine.id)) {
+ hiddenSearchEngines.add(searchEngine)
+ false
+ } else {
+ true
+ }
+ }
+
+ val regionSearchEngineIds = regionBundle.await().list.map { searchEngine -> searchEngine.id }
+
+ val additionalSearchEngines = allAdditionalSearchEngines.await().filter { searchEngine ->
+ searchEngine.id in additionalSearchEngineIds.await() &&
+ searchEngine.id !in regionSearchEngineIds
+ }
+
+ val additionalAvailableSearchEngines = allAdditionalSearchEngines.await().filter { searchEngine ->
+ searchEngine.id !in additionalSearchEngineIds.await() &&
+ searchEngine.id !in regionSearchEngineIds
+ }
+
+ performDefaultSearchEngineMigration(
+ migrationValues,
+ filteredRegionSearchEngines + customSearchEngines.await() + additionalSearchEngines,
+ )
+ val userChoice = async(ioDispatcher) { metadataStorage.getUserSelectedSearchEngine() }
+
+ val action = SearchAction.SetSearchEnginesAction(
+ regionSearchEngines = filteredRegionSearchEngines,
+ regionDefaultSearchEngineId = regionBundle.await().defaultSearchEngineId,
+ userSelectedSearchEngineId = userChoice.await()?.searchEngineId,
+ userSelectedSearchEngineName = userChoice.await()?.searchEngineName,
+ customSearchEngines = customSearchEngines.await(),
+ hiddenSearchEngines = hiddenSearchEngines,
+ disabledSearchEngineIds = disabledSearchEngineIds.await(),
+ additionalSearchEngines = additionalSearchEngines,
+ additionalAvailableSearchEngines = additionalAvailableSearchEngines,
+ regionSearchEnginesOrder = regionSearchEngineIds,
+ )
+ store.dispatch(action)
+ }
+
+ private fun updateSearchEngineSelection(
+ action: SearchAction.SelectSearchEngineAction,
+ ) = scope.launch {
+ metadataStorage.setUserSelectedSearchEngine(
+ action.searchEngineId,
+ action.searchEngineName,
+ )
+ }
+
+ private fun removeCustomSearchEngine(
+ action: SearchAction.RemoveCustomSearchEngineAction,
+ ) = scope.launch {
+ customStorage.removeSearchEngine(action.searchEngineId)
+ }
+
+ private fun saveCustomSearchEngine(
+ action: SearchAction.UpdateCustomSearchEngineAction,
+ ) = scope.launch {
+ customStorage.saveSearchEngine(action.searchEngine)
+ }
+
+ private fun updateHiddenSearchEngines(
+ hiddenSearchEngines: List<SearchEngine>,
+ ) = scope.launch {
+ metadataStorage.setHiddenSearchEngines(
+ hiddenSearchEngines.map { searchEngine -> searchEngine.id },
+ )
+ }
+
+ private fun updateAdditionalSearchEngines(
+ additionalSearchEngines: List<SearchEngine>,
+ ) = scope.launch {
+ metadataStorage.setAdditionalSearchEngines(
+ additionalSearchEngines.map { searchEngine -> searchEngine.id },
+ )
+ }
+
+ private fun updateDisabledSearchEngineIds(
+ store: Store<BrowserState, BrowserAction>,
+ action: SearchAction.UpdateDisabledSearchEngineIdsAction,
+ ) = scope.launch {
+ val disabledIds = store.state.search.disabledSearchEngineIds
+ val updatedList = if (action.isEnabled) {
+ disabledIds - action.searchEngineId
+ } else {
+ disabledIds + action.searchEngineId
+ }
+ metadataStorage.setDisabledSearchEngineIds(updatedList)
+ }
+
+ private suspend fun performCustomSearchEnginesMigration(values: Migration.MigrationValues?) {
+ if (values == null) {
+ return
+ }
+
+ values.customSearchEngines.forEach { searchEngine ->
+ customStorage.saveSearchEngine(searchEngine)
+ }
+ }
+
+ private suspend fun performDefaultSearchEngineMigration(
+ values: Migration.MigrationValues?,
+ engines: List<SearchEngine>,
+ ) {
+ if (values == null) {
+ return
+ }
+
+ val name = values.defaultSearchEngineName ?: return
+
+ val default = engines.find { searchEngine ->
+ searchEngine.name == name
+ }
+
+ if (default == null) {
+ logger.error("Could not find migrated default search engine ($name)")
+ return
+ }
+
+ metadataStorage.setUserSelectedSearchEngine(
+ id = default.id,
+ name = if (default.type == SearchEngine.Type.BUNDLED) {
+ default.name
+ } else {
+ null
+ },
+ )
+ }
+
+ /**
+ * A storage for custom search engines of the user.
+ */
+ interface CustomStorage {
+ /**
+ * Loads the list of search engines from the storage.
+ */
+ suspend fun loadSearchEngineList(): List<SearchEngine>
+
+ /**
+ * Removes the search engine with the specified [identifier] from the storage.
+ */
+ suspend fun removeSearchEngine(identifier: String)
+
+ /**
+ * Saves the given [searchEngine] to the storage. May replace an already existing search
+ * engine with the same ID.
+ */
+ suspend fun saveSearchEngine(searchEngine: SearchEngine): Boolean
+ }
+
+ /**
+ * A storage for loading bundled search engines.
+ */
+ interface BundleStorage {
+ /**
+ * Loads the bundled search engines for the given [locale] and [region].
+ *
+ * If [distribution] is not null then attempt to load the bundled search engine for the
+ * [distribution] in the specified [locale] and [region] if available.
+ */
+ suspend fun load(
+ region: RegionState,
+ locale: Locale = Locale.getDefault(),
+ distribution: String? = null,
+ searchExtraParams: SearchExtraParams? = null,
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+ ): Bundle
+
+ /**
+ * Loads the bundled search engines with the given [ids].
+ */
+ suspend fun load(
+ ids: List<String>,
+ searchExtraParams: SearchExtraParams? = null,
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+ ): List<SearchEngine>
+
+ /**
+ * A loaded bundle containing the list of search engines and the ID of the default for
+ * the region.
+ */
+ data class Bundle(
+ val list: List<SearchEngine>,
+ val defaultSearchEngineId: String,
+ )
+ }
+
+ /**
+ * A storage for saving additional metadata related to search.
+ */
+ interface MetadataStorage {
+ /**
+ * Gets the ID (and optinally name) of the default search engine the user has picked. Returns
+ * `null` if the user has not made a choice.
+ */
+ suspend fun getUserSelectedSearchEngine(): UserChoice?
+
+ /**
+ * Sets the ID (and optionally name) of the default search engine the user has picked.
+ */
+ suspend fun setUserSelectedSearchEngine(id: String, name: String?)
+
+ /**
+ * Sets the list of IDs of hidden search engines.
+ */
+ suspend fun setHiddenSearchEngines(ids: List<String>)
+
+ /**
+ * Gets the list of IDs of hidden search engines.
+ */
+ suspend fun getHiddenSearchEngines(): List<String>
+
+ /**
+ * Gets the list of IDs of additional search engines that the user explicitly added.
+ */
+ suspend fun getAdditionalSearchEngines(): List<String>
+
+ /**
+ * Sets the list of IDs of additional search engines that the user explicitly added.
+ */
+ suspend fun setAdditionalSearchEngines(ids: List<String>)
+
+ /**
+ * Gets the list of IDs of disabled search engine shortcuts.
+ */
+ suspend fun getDisabledSearchEngineIds(): List<String>
+
+ /**
+ * Sets the list of IDs of disabled search engine shortcuts.
+ */
+ suspend fun setDisabledSearchEngineIds(ids: List<String>)
+
+ /**
+ * Data class holding the ID and name of the selected search engine of the user.
+ */
+ data class UserChoice(
+ val searchEngineId: String,
+ val searchEngineName: String?,
+ )
+ }
+
+ /**
+ * Interface for a class that can provide data from a legacy system to be imported into the
+ * storage used by the middleware.
+ */
+ interface Migration {
+ /**
+ * Returns the values to be migrated. It is expected that the application returns the values
+ * only once. Afterwards the data is assumed to be migrated and should not be provided again.
+ */
+ fun getValuesToMigrate(): MigrationValues?
+
+ /**
+ * Holder data class for values to be migrated.
+ *
+ * @param customSearchEngines List of custom search engines that should be imported.
+ * @param defaultSearchEngineName Name of the default search engine that the user had
+ * selected. Or `null` if the user has not made any choice.
+ */
+ data class MigrationValues(
+ val customSearchEngines: List<SearchEngine>,
+ val defaultSearchEngineName: String?,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/region/RegionManager.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/region/RegionManager.kt
new file mode 100644
index 0000000000..33f75a8a17
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/region/RegionManager.kt
@@ -0,0 +1,126 @@
+/* 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.search.region
+
+import android.content.Context
+import android.content.SharedPreferences
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.service.location.LocationService
+
+// The amount of time (in seconds) we need to be in a new
+// location before we update the home region.
+// Currently set to 2 weeks.
+// https://searchfox.org/mozilla-central/rev/89d33e1c3b0a57a9377b4815c2f4b58d933b7c32/toolkit/modules/Region.jsm#82-85
+private const val UPDATE_INTERVAL_MS = 14 * 24 * 60 * 60 * 1000
+
+// The maximum number of times we retry fetching the region from
+// the location service until we give up. We will try again on
+// the next app start.
+private const val MAX_RETRIES = 3
+
+// Timeout until we try to fetch the region again after a failure.
+private const val RETRY_TIMEOUT_MS: Long = 10L * 60L * 1000L
+
+private const val PREFERENCE_FILE = "mozac_feature_search_region"
+private const val PREFERENCE_KEY_HOME_REGION = "region.home"
+private const val PREFERENCE_KEY_CURRENT_REGION = "region.current"
+private const val PREFERENCE_KEY_REGION_FIRST_SEEN = "region.first_seen"
+
+/**
+ * Internal RegionManager for keeping track of the "current" and "home" region of a user. Used by
+ * [RegionMiddleware].
+ */
+internal class RegionManager(
+ context: Context,
+ private val locationService: LocationService,
+ private val currentTime: () -> Long = { System.currentTimeMillis() },
+ private val preferences: Lazy<SharedPreferences> = lazy {
+ context.getSharedPreferences(
+ PREFERENCE_FILE,
+ Context.MODE_PRIVATE,
+ )
+ },
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+) {
+ private var homeRegion: String?
+ get() = preferences.value.getString(PREFERENCE_KEY_HOME_REGION, null)
+ set(value) = preferences.value.edit().putString(PREFERENCE_KEY_HOME_REGION, value).apply()
+
+ private var currentRegion: String?
+ get() = preferences.value.getString(PREFERENCE_KEY_CURRENT_REGION, null)
+ set(value) = preferences.value.edit().putString(PREFERENCE_KEY_CURRENT_REGION, value).apply()
+
+ private var firstSeen: Long?
+ get() = preferences.value.getLong(PREFERENCE_KEY_REGION_FIRST_SEEN, 0)
+ set(value) = if (value == null) {
+ preferences.value.edit().remove(PREFERENCE_KEY_REGION_FIRST_SEEN).apply()
+ } else {
+ preferences.value.edit().putLong(PREFERENCE_KEY_REGION_FIRST_SEEN, value).apply()
+ }
+
+ fun region(): RegionState? {
+ return homeRegion?.let { region ->
+ RegionState(
+ region,
+ currentRegion ?: region,
+ )
+ }
+ }
+
+ suspend fun update(): RegionState? {
+ val region = fetchRegionWithRetry()?.countryCode ?: return null
+
+ if (homeRegion == null) {
+ // If we do not have a value for the home region yet, then we can set it immediately.
+ homeRegion = region
+ return RegionState(home = region, current = region)
+ }
+
+ return when (region) {
+ homeRegion -> {
+ // If we are in the home region (again) then we can clear a previously seen different
+ // "current" region.
+ currentRegion = null
+ firstSeen = null
+ null
+ }
+
+ currentRegion -> {
+ val now = currentTime()
+ if (now > (firstSeen ?: 0) + UPDATE_INTERVAL_MS) {
+ // We have been in the "current" region longer than the specified "interval".
+ // So we will set the "current" region as our new home region.
+ homeRegion = region
+ RegionState(home = region, current = region)
+ } else {
+ null
+ }
+ }
+
+ else -> {
+ // This region is neither the home region nor the current region. We set it as the
+ // new "current" region and remember when we saw it the first time.
+ firstSeen = currentTime()
+ currentRegion = region
+ null
+ }
+ }
+ }
+
+ private suspend fun fetchRegionWithRetry(): LocationService.Region? = withContext(dispatcher) {
+ repeat(MAX_RETRIES) {
+ val region = locationService.fetchRegion(readFromCache = true)
+ if (region != null) {
+ return@withContext region
+ }
+ delay(RETRY_TIMEOUT_MS)
+ }
+ return@withContext null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/region/RegionMiddleware.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/region/RegionMiddleware.kt
new file mode 100644
index 0000000000..7bf4893c0b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/region/RegionMiddleware.kt
@@ -0,0 +1,72 @@
+/* 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.search.region
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+import mozilla.components.service.location.LocationService
+
+/**
+ * [Middleware] implementation for updating the [RegionState] using the provided [LocationService].
+ */
+class RegionMiddleware(
+ context: Context,
+ locationService: LocationService,
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : Middleware<BrowserState, BrowserAction> {
+ @VisibleForTesting
+ internal var regionManager = RegionManager(context, locationService, dispatcher = ioDispatcher)
+
+ @VisibleForTesting
+ @Volatile
+ internal var updateJob: Job? = null
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ if (action is InitAction || action is SearchAction.RefreshSearchEnginesAction) {
+ updateJob = determineRegion(context.store)
+ }
+
+ next(action)
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ private fun determineRegion(
+ store: Store<BrowserState, BrowserAction>,
+ ) = GlobalScope.launch(ioDispatcher) {
+ // Get the region state from the RegionManager. If there's none then dispatch the default
+ // region to be used.
+ val region = regionManager.region()
+ if (region != null) {
+ store.dispatch(SearchAction.SetRegionAction(region))
+ } else {
+ store.dispatch(SearchAction.SetRegionAction(RegionState.Default))
+ }
+
+ // Ask the RegionManager to perform an update. If the "home" region changed then it will
+ // return a new RegionState.
+ val update = regionManager.update()
+ if (update != null) {
+ store.dispatch(SearchAction.SetRegionAction(update))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorage.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorage.kt
new file mode 100644
index 0000000000..eaa196739d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorage.kt
@@ -0,0 +1,278 @@
+/* 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.search.storage
+
+import android.content.Context
+import android.content.res.AssetManager
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.feature.search.middleware.SearchExtraParams
+import mozilla.components.feature.search.middleware.SearchMiddleware
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.content.res.readJSONObject
+import mozilla.components.support.ktx.android.org.json.toList
+import mozilla.components.support.ktx.android.org.json.tryGetString
+import org.json.JSONArray
+import org.json.JSONObject
+import java.util.Locale
+import kotlin.coroutines.CoroutineContext
+
+private val logger = Logger("BundledSearchEnginesStorage")
+
+/**
+ * A storage implementation for reading bundled [SearchEngine]s from the app's assets.
+ */
+internal class BundledSearchEnginesStorage(
+ private val context: Context,
+) : SearchMiddleware.BundleStorage {
+ /**
+ * Load the [SearchMiddleware.BundleStorage.Bundle] for the given [region] and [locale].
+ */
+ override suspend fun load(
+ region: RegionState,
+ locale: Locale,
+ distribution: String?,
+ searchExtraParams: SearchExtraParams?,
+ coroutineContext: CoroutineContext,
+ ): SearchMiddleware.BundleStorage.Bundle = withContext(coroutineContext) {
+ val localizedConfiguration = loadAndFilterConfiguration(context, region, locale, distribution)
+ val searchEngineIdentifiers = localizedConfiguration.visibleSearchEngines
+
+ val searchEngines = loadSearchEnginesFromList(
+ context = context,
+ searchEngineIdentifiers = searchEngineIdentifiers.distinct(),
+ type = SearchEngine.Type.BUNDLED,
+ searchExtraParams = searchExtraParams,
+ coroutineContext = coroutineContext,
+ )
+
+ // Reorder the list of search engines according to the configuration.
+ // Note: we're using the name of the search engine, not the id, so we can only do this
+ // after we've loaded the search engine from the XML
+ val searchOrder = localizedConfiguration.searchOrder
+ val orderedList = searchOrder
+ .map { name ->
+ searchEngines.filter { it.name == name }
+ }
+ .flatten()
+
+ val unorderedRest = searchEngines
+ .filter {
+ !searchOrder.contains(it.name)
+ }
+
+ val defaultEngine = localizedConfiguration.searchDefault?.let { name ->
+ searchEngines.find { it.name == name }
+ } ?: throw IllegalStateException("No default engine for configuration: locale=$locale, region=$region")
+
+ SearchMiddleware.BundleStorage.Bundle(
+ list = orderedList + unorderedRest,
+ defaultSearchEngineId = defaultEngine.id,
+ )
+ }
+
+ override suspend fun load(
+ ids: List<String>,
+ searchExtraParams: SearchExtraParams?,
+ coroutineContext: CoroutineContext,
+ ): List<SearchEngine> = withContext(coroutineContext) {
+ if (ids.isEmpty()) {
+ emptyList()
+ } else {
+ loadSearchEnginesFromList(
+ context = context,
+ searchEngineIdentifiers = ids.distinct(),
+ type = SearchEngine.Type.BUNDLED_ADDITIONAL,
+ searchExtraParams = searchExtraParams,
+ coroutineContext = coroutineContext,
+ )
+ }
+ }
+}
+
+private data class SearchEngineListConfiguration(
+ val visibleSearchEngines: List<String>,
+ val searchOrder: List<String>,
+ val searchDefault: String?,
+)
+
+private fun loadAndFilterConfiguration(
+ context: Context,
+ region: RegionState,
+ locale: Locale,
+ distribution: String?,
+): SearchEngineListConfiguration {
+ val config = context.assets.readJSONObject("search/list.json")
+
+ val configBlocks = pickConfigurationBlocks(locale, config)
+ val jsonSearchEngineIdentifiers =
+ getSearchEngineIdentifiersFromBlock(region, locale, distribution, configBlocks)
+
+ val searchOrder = getSearchOrderFromBlock(region, configBlocks)
+ val searchDefault = getSearchDefaultFromBlock(region, configBlocks)
+
+ return SearchEngineListConfiguration(
+ applyOverridesIfNeeded(region, config, jsonSearchEngineIdentifiers),
+ searchOrder.toList(),
+ searchDefault,
+ )
+}
+
+private fun pickConfigurationBlocks(
+ locale: Locale,
+ config: JSONObject,
+): Array<JSONObject> {
+ val localesConfig = config.getJSONObject("locales")
+
+ val localizedConfig = when {
+ // First try (Locale): locales/xx_XX/
+ localesConfig.has(locale.languageTag) ->
+ localesConfig.getJSONObject(locale.languageTag)
+
+ // Second try (Language): locales/xx/
+ localesConfig.has(locale.language) ->
+ localesConfig.getJSONObject(locale.language)
+
+ // Give up, and fallback to defaults
+ else -> null
+ }
+
+ return localizedConfig?.let {
+ arrayOf(it, config)
+ } ?: arrayOf(config)
+}
+
+private fun getSearchEngineIdentifiersFromBlock(
+ region: RegionState,
+ locale: Locale,
+ distribution: String?,
+ configBlocks: Array<JSONObject>,
+): JSONArray {
+ // Now test if there's an override for the distribution or region (if it's set)
+ return distribution?.let { getArrayFromBlock(region, distribution, configBlocks) }
+ ?: getArrayFromBlock(region, "visibleDefaultEngines", configBlocks)
+ ?: throw IllegalStateException("No visibleDefaultEngines using region $region and locale $locale")
+}
+
+private fun getSearchDefaultFromBlock(
+ region: RegionState,
+ configBlocks: Array<JSONObject>,
+): String? = getValueFromBlock(region, configBlocks) {
+ it.tryGetString("searchDefault")
+}
+
+private fun getSearchOrderFromBlock(
+ region: RegionState,
+ configBlocks: Array<JSONObject>,
+): JSONArray? = getArrayFromBlock(region, "searchOrder", configBlocks)
+
+private fun getArrayFromBlock(
+ region: RegionState,
+ key: String,
+ blocks: Array<JSONObject>,
+): JSONArray? = getValueFromBlock(region, blocks) {
+ it.optJSONArray(key)
+}
+
+/**
+ * This looks for a JSONObject in the config blocks it is passed that is able to be transformed
+ * into a value. It tries the permutations of locale and region from most specific to least
+ * specific.
+ *
+ * This has to be done on a value basis, not a configBlock basis, as the configuration for a
+ * given locale/region is not grouped into one object, but spread across the json file,
+ * according to these rules.
+ */
+private fun <T : Any> getValueFromBlock(
+ region: RegionState,
+ blocks: Array<JSONObject>,
+ transform: (JSONObject) -> T?,
+): T? {
+ val regions = arrayOf(region.home, "default")
+
+ return blocks
+ .flatMap { block ->
+ regions.mapNotNull { region -> block.optJSONObject(region) }
+ }
+ .mapNotNull(transform)
+ .firstOrNull()
+}
+
+private fun applyOverridesIfNeeded(
+ region: RegionState,
+ config: JSONObject,
+ jsonSearchEngineIdentifiers: JSONArray,
+): List<String> {
+ val overrides = config.getJSONObject("regionOverrides")
+ val searchEngineIdentifiers = mutableListOf<String>()
+ val regionOverrides = if (overrides.has(region.home)) {
+ overrides.getJSONObject(region.home)
+ } else {
+ null
+ }
+
+ for (i in 0 until jsonSearchEngineIdentifiers.length()) {
+ var identifier = jsonSearchEngineIdentifiers.getString(i)
+ if (regionOverrides != null && regionOverrides.has(identifier)) {
+ identifier = regionOverrides.getString(identifier)
+ }
+ searchEngineIdentifiers.add(identifier)
+ }
+
+ return searchEngineIdentifiers
+}
+
+@OptIn(DelicateCoroutinesApi::class)
+private suspend fun loadSearchEnginesFromList(
+ context: Context,
+ searchEngineIdentifiers: List<String>,
+ type: SearchEngine.Type,
+ searchExtraParams: SearchExtraParams?,
+ coroutineContext: CoroutineContext,
+): List<SearchEngine> {
+ val assets = context.assets
+ val reader = SearchEngineReader(type, searchExtraParams)
+
+ val deferredSearchEngines = mutableListOf<Deferred<SearchEngine?>>()
+
+ searchEngineIdentifiers.forEach { identifier ->
+ deferredSearchEngines.add(
+ GlobalScope.async(coroutineContext) {
+ loadSearchEngine(assets, reader, identifier)
+ },
+ )
+ }
+
+ return deferredSearchEngines.mapNotNull { it.await() }
+}
+
+@Suppress("TooGenericExceptionCaught")
+private fun loadSearchEngine(
+ assets: AssetManager,
+ reader: SearchEngineReader,
+ identifier: String,
+): SearchEngine? = try {
+ assets.open("searchplugins/$identifier.xml").use { stream ->
+ reader.loadStream(identifier, stream)
+ }
+} catch (e: Exception) {
+ // Handling all exceptions here (instead of just IOExceptions) as we're
+ // seeing crashes we can't explain currently. Letting the app launch
+ // will eventually help us understand the root cause:
+ // https://github.com/mozilla-mobile/android-components/issues/12304
+
+ // We should also consider logging these errors to Sentry:
+ // https://github.com/mozilla-mobile/android-components/issues/12313
+ logger.error("Could not load additional search engine with ID $identifier", e)
+ null
+}
+
+private val Locale.languageTag: String
+ get() = "$language-$country"
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/CustomSearchEnginesStorage.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/CustomSearchEnginesStorage.kt
new file mode 100644
index 0000000000..f0961d6b63
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/CustomSearchEnginesStorage.kt
@@ -0,0 +1,66 @@
+/* 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.search.storage
+
+import android.content.Context
+import android.util.AtomicFile
+import android.util.Base64
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.feature.search.middleware.SearchMiddleware
+import java.io.File
+import kotlin.coroutines.CoroutineContext
+
+internal const val SEARCH_FILE_EXTENSION = ".xml"
+internal const val SEARCH_DIR_NAME = "search-engines"
+
+/**
+ * A storage implementation for organizing [SearchEngine]s. Its primary use case is for persisting
+ * custom search engines added by users.
+ */
+internal class CustomSearchEngineStorage(
+ private val context: Context,
+ private val coroutineContext: CoroutineContext = Dispatchers.IO,
+) : SearchMiddleware.CustomStorage {
+ private val reader = SearchEngineReader(SearchEngine.Type.CUSTOM)
+ private val writer = SearchEngineWriter()
+
+ override suspend fun loadSearchEngineList(): List<SearchEngine> = withContext(coroutineContext) {
+ val searchEngineList = mutableListOf<SearchEngine>()
+ getFileDirectory().listFiles()?.forEach {
+ val filename = it.name.removeSuffix(SEARCH_FILE_EXTENSION)
+ val identifier = String(Base64.decode(filename, Base64.NO_WRAP or Base64.URL_SAFE))
+ searchEngineList.add(loadSearchEngine(identifier))
+ }
+ searchEngineList.toList()
+ }
+
+ suspend fun loadSearchEngine(identifier: String): SearchEngine = withContext(coroutineContext) {
+ reader.loadFile(identifier, getSearchFile(identifier))
+ }
+
+ override suspend fun saveSearchEngine(searchEngine: SearchEngine): Boolean = withContext(coroutineContext) {
+ writer.saveSearchEngineXML(searchEngine, getSearchFile(searchEngine.id))
+ }
+
+ override suspend fun removeSearchEngine(identifier: String) = withContext(coroutineContext) {
+ getSearchFile(identifier).delete()
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun getSearchFile(identifier: String): AtomicFile {
+ val encodedId = Base64.encodeToString(identifier.toByteArray(), Base64.NO_WRAP or Base64.URL_SAFE)
+ return AtomicFile(File(getFileDirectory(), encodedId + SEARCH_FILE_EXTENSION))
+ }
+
+ private fun getFileDirectory(): File =
+ File(context.filesDir, SEARCH_DIR_NAME).also {
+ if (!it.exists()) {
+ it.mkdirs()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchEngineReader.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchEngineReader.kt
new file mode 100644
index 0000000000..51d2ccec52
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchEngineReader.kt
@@ -0,0 +1,243 @@
+/* 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.search.storage
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.util.AtomicFile
+import android.util.Base64
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.feature.search.middleware.SearchExtraParams
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+import org.xmlpull.v1.XmlPullParserFactory
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
+
+internal const val URL_TYPE_SUGGEST_JSON = "application/x-suggestions+json"
+internal const val URL_TYPE_SEARCH_HTML = "text/html"
+internal const val URL_REL_MOBILE = "mobile"
+internal const val IMAGE_URI_PREFIX = "data:image/png;base64,"
+internal const val GOOGLE_ID = "google"
+
+// List of general search engine ids, taken from
+// https://searchfox.org/mozilla-central/rev/ef0aa879e94534ffd067a3748d034540a9fc10b0/toolkit/components/search/SearchUtils.sys.mjs#200
+internal val GENERAL_SEARCH_ENGINE_IDS = setOf(
+ GOOGLE_ID,
+ "ddg",
+ "bing",
+ "baidu",
+ "ecosia",
+ "qwant",
+ "yahoo-jp",
+ "seznam-cz",
+ "coccoc",
+ "baidu",
+)
+
+/**
+ * A simple XML reader for search engine plugins.
+ *
+ * @param type the [SearchEngine.Type] that the read [SearchEngine]s will get assigned.
+ * @param searchExtraParams Optional search extra params.
+ */
+internal class SearchEngineReader(
+ private val type: SearchEngine.Type,
+ private val searchExtraParams: SearchExtraParams? = null,
+) {
+ private class SearchEngineBuilder(
+ private val type: SearchEngine.Type,
+ private val identifier: String,
+ ) {
+ var resultsUrls: MutableList<String> = mutableListOf()
+ var suggestUrl: String? = null
+ var name: String? = null
+ var icon: Bitmap? = null
+ var inputEncoding: String? = null
+
+ fun toSearchEngine() = SearchEngine(
+ id = identifier,
+ name = name!!,
+ icon = icon!!,
+ type = type,
+ resultUrls = resultsUrls,
+ suggestUrl = suggestUrl,
+ inputEncoding = inputEncoding,
+ isGeneral = isGeneralSearchEngine(identifier, type),
+ )
+
+ /**
+ * Returns true if the provided [type] is a custom search engine or the [identifier] is
+ * included in [GENERAL_SEARCH_ENGINE_IDS].
+ */
+ private fun isGeneralSearchEngine(identifier: String, type: SearchEngine.Type): Boolean =
+ type == SearchEngine.Type.CUSTOM ||
+ identifier.startsWith(GOOGLE_ID) ||
+ GENERAL_SEARCH_ENGINE_IDS.contains(identifier)
+ }
+
+ /**
+ * Loads [SearchEngine] from a provided [file]
+ */
+ fun loadFile(identifier: String, file: AtomicFile): SearchEngine {
+ return loadStream(identifier, file.openRead())
+ }
+
+ /**
+ * Loads a <code>SearchEngine</code> from the given <code>stream</code> and assigns it the given
+ * <code>identifier</code>.
+ */
+ @Throws(IOException::class, XmlPullParserException::class)
+ fun loadStream(identifier: String, stream: InputStream): SearchEngine {
+ val builder = SearchEngineBuilder(type, identifier)
+
+ val parser = XmlPullParserFactory.newInstance().newPullParser()
+ parser.setInput(InputStreamReader(stream, StandardCharsets.UTF_8))
+ parser.next()
+
+ readSearchPlugin(parser, builder)
+
+ return builder.toSearchEngine()
+ }
+
+ @Throws(XmlPullParserException::class, IOException::class)
+ @Suppress("ComplexMethod")
+ private fun readSearchPlugin(parser: XmlPullParser, builder: SearchEngineBuilder) {
+ if (XmlPullParser.START_TAG != parser.eventType) {
+ throw XmlPullParserException("Expected start tag: " + parser.positionDescription)
+ }
+
+ val name = parser.name
+ if ("SearchPlugin" != name && "OpenSearchDescription" != name) {
+ throw XmlPullParserException(
+ "Expected <SearchPlugin> or <OpenSearchDescription> as root tag: ${parser.positionDescription}",
+ )
+ }
+
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+
+ when (parser.name) {
+ "ShortName" -> readShortName(parser, builder)
+ "Url" -> readUrl(parser, builder)
+ "Image" -> readImage(parser, builder)
+ "InputEncoding" -> readInputEncoding(parser, builder)
+ else -> skip(parser)
+ }
+ }
+ }
+
+ @Throws(XmlPullParserException::class, IOException::class)
+ private fun readUrl(parser: XmlPullParser, builder: SearchEngineBuilder) {
+ parser.require(XmlPullParser.START_TAG, null, "Url")
+
+ val type = parser.getAttributeValue(null, "type")
+ val template = parser.getAttributeValue(null, "template")
+ val rel = parser.getAttributeValue(null, "rel")
+
+ val url = buildString {
+ append(readUri(parser, template))
+ searchExtraParams?.let {
+ with(it) {
+ if (builder.name == searchEngineName) {
+ featureEnablerParam?.let { append("&$featureEnablerName=$it") }
+ append("&$channelIdName=$channelIdParam")
+ }
+ }
+ }
+ }
+
+ if (type == URL_TYPE_SEARCH_HTML) {
+ // Prefer mobile URIs.
+ if (rel != null && rel == URL_REL_MOBILE) {
+ builder.resultsUrls.add(0, url)
+ } else {
+ builder.resultsUrls.add(url)
+ }
+ } else if (type == URL_TYPE_SUGGEST_JSON) {
+ builder.suggestUrl = url
+ }
+ }
+
+ @Throws(XmlPullParserException::class, IOException::class)
+ private fun readUri(parser: XmlPullParser, template: String): Uri {
+ var uri = Uri.parse(template)
+
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+
+ if (parser.name == "Param") {
+ val name = parser.getAttributeValue(null, "name")
+ val value = parser.getAttributeValue(null, "value")
+ uri = uri.buildUpon().appendQueryParameter(name, value).build()
+ parser.nextTag()
+ } else {
+ skip(parser)
+ }
+ }
+
+ return uri
+ }
+
+ @Throws(XmlPullParserException::class, IOException::class)
+ private fun skip(parser: XmlPullParser) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ throw IllegalStateException()
+ }
+ var depth = 1
+ while (depth != 0) {
+ when (parser.next()) {
+ XmlPullParser.END_TAG -> depth--
+ XmlPullParser.START_TAG -> depth++
+ // else: Do nothing - we're skipping content
+ }
+ }
+ }
+
+ @Throws(IOException::class, XmlPullParserException::class)
+ private fun readShortName(parser: XmlPullParser, builder: SearchEngineBuilder) {
+ parser.require(XmlPullParser.START_TAG, null, "ShortName")
+ if (parser.next() == XmlPullParser.TEXT) {
+ builder.name = parser.text
+ parser.nextTag()
+ }
+ }
+
+ @Throws(IOException::class, XmlPullParserException::class)
+ private fun readImage(parser: XmlPullParser, builder: SearchEngineBuilder) {
+ parser.require(XmlPullParser.START_TAG, null, "Image")
+
+ if (parser.next() != XmlPullParser.TEXT) {
+ return
+ }
+
+ val uri = parser.text
+ if (!uri.startsWith(IMAGE_URI_PREFIX)) {
+ return
+ }
+
+ val raw = Base64.decode(uri.substring(IMAGE_URI_PREFIX.length), Base64.DEFAULT)
+
+ builder.icon = BitmapFactory.decodeByteArray(raw, 0, raw.size)
+
+ parser.nextTag()
+ }
+
+ @Throws(IOException::class, XmlPullParserException::class)
+ private fun readInputEncoding(parser: XmlPullParser, builder: SearchEngineBuilder) {
+ parser.require(XmlPullParser.START_TAG, null, "InputEncoding")
+ if (parser.next() == XmlPullParser.TEXT) {
+ builder.inputEncoding = parser.text
+ parser.nextTag()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchEngineWriter.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchEngineWriter.kt
new file mode 100644
index 0000000000..15d7f17212
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchEngineWriter.kt
@@ -0,0 +1,115 @@
+/* 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.search.storage
+
+import android.graphics.Bitmap
+import android.util.AtomicFile
+import android.util.Base64
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.search.SearchEngine
+import org.w3c.dom.DOMException
+import org.w3c.dom.Document
+import java.io.ByteArrayOutputStream
+import java.io.File
+import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.parsers.ParserConfigurationException
+import javax.xml.transform.TransformerConfigurationException
+import javax.xml.transform.TransformerException
+import javax.xml.transform.TransformerFactory
+import javax.xml.transform.dom.DOMSource
+import javax.xml.transform.stream.StreamResult
+
+/**
+ * A simple XML writer for search engine plugins.
+ */
+internal class SearchEngineWriter {
+ /**
+ * Builds and save the XML document of [SearchEngine] to the provided [File].
+ *
+ * @param searchEngine the search engine to build XML with.
+ * @param file the file instance to save the search engine XML.
+ * @param document the document instance to build search engine XML with.
+ * @return true if the XML is built and saved successfully, false otherwise.
+ */
+ fun saveSearchEngineXML(
+ searchEngine: SearchEngine,
+ file: AtomicFile,
+ document: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(),
+ ): Boolean {
+ return try {
+ buildSearchEngineXML(searchEngine, document)
+ saveXMLDocumentToFile(document, file)
+ true
+ } catch (e: ParserConfigurationException) {
+ false
+ } catch (e: DOMException) {
+ false
+ } catch (e: TransformerConfigurationException) {
+ false
+ } catch (e: TransformerException) {
+ false
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ @Throws(ParserConfigurationException::class, DOMException::class)
+ internal fun buildSearchEngineXML(
+ searchEngine: SearchEngine,
+ xmlDocument: Document,
+ ) {
+ val rootElement = xmlDocument.createElement("OpenSearchDescription")
+ rootElement.setAttribute("xmlns", "http://a9.com/-/spec/opensearch/1.1/")
+ rootElement.setAttribute("xmlns:moz", "http://www.mozilla.org/2006/browser/search/")
+ xmlDocument.appendChild(rootElement)
+
+ val shortNameElement = xmlDocument.createElement("ShortName")
+ shortNameElement.textContent = searchEngine.name
+ rootElement.appendChild(shortNameElement)
+
+ val descriptionElement = xmlDocument.createElement("Description")
+ descriptionElement.textContent = searchEngine.name
+ rootElement.appendChild(descriptionElement)
+
+ val imageElement = xmlDocument.createElement("Image")
+ imageElement.setAttribute("width", "16")
+ imageElement.setAttribute("height", "16")
+ imageElement.textContent = searchEngine.icon.toBase64()
+ rootElement.appendChild(imageElement)
+
+ searchEngine.inputEncoding?.let { inputEncoding ->
+ val inputEncodingElement = xmlDocument.createElement("InputEncoding")
+ inputEncodingElement.textContent = inputEncoding
+ rootElement.appendChild(inputEncodingElement)
+ }
+
+ searchEngine.resultUrls.forEach { url ->
+ val urlElement = xmlDocument.createElement("Url")
+ urlElement.setAttribute("type", URL_TYPE_SEARCH_HTML)
+ urlElement.setAttribute("template", url)
+ rootElement.appendChild(urlElement)
+ }
+
+ searchEngine.suggestUrl?.let { url ->
+ val urlElement = xmlDocument.createElement("Url")
+ urlElement.setAttribute("type", URL_TYPE_SUGGEST_JSON)
+ val templateSearchString = url.replace("%s", "{searchTerms}")
+ urlElement.setAttribute("template", templateSearchString)
+ rootElement.appendChild(urlElement)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ @Throws(TransformerConfigurationException::class, TransformerException::class)
+ internal fun saveXMLDocumentToFile(doc: Document, file: AtomicFile) =
+ TransformerFactory.newInstance().newTransformer().transform(DOMSource(doc), StreamResult(file.baseFile))
+}
+
+private const val BITMAP_COMPRESS_QUALITY = 100
+private fun Bitmap.toBase64(): String {
+ val stream = ByteArrayOutputStream()
+ compress(Bitmap.CompressFormat.PNG, BITMAP_COMPRESS_QUALITY, stream)
+ val encodedImage = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT)
+ return "data:image/png;base64,$encodedImage"
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchMetadataStorage.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchMetadataStorage.kt
new file mode 100644
index 0000000000..b835f5ab37
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/storage/SearchMetadataStorage.kt
@@ -0,0 +1,103 @@
+/* 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.search.storage
+
+import android.content.Context
+import android.content.SharedPreferences
+import mozilla.components.feature.search.middleware.SearchMiddleware
+
+private const val PREFERENCE_FILE = "mozac_feature_search_metadata"
+
+private const val PREFERENCE_KEY_USER_SELECTED_SEARCH_ENGINE_ID = "user_selected_search_engine_id"
+private const val PREFERENCE_KEY_USER_SELECTED_SEARCH_ENGINE_NAME = "user_selected_search_engine_name"
+private const val PREFERENCE_KEY_HIDDEN_SEARCH_ENGINES = "hidden_search_engines"
+private const val PREFERENCE_KEY_ADDITIONAL_SEARCH_ENGINES = "additional_search_engines"
+private const val PREFERENCE_KEY_DISABLED_SEARCH_ENGINE_ID = "preference_key_disabled_search_engine_id"
+
+/**
+ * Storage for saving additional search related metadata.
+ */
+internal class SearchMetadataStorage(
+ context: Context,
+ private val disabledByDefaultSearchEngineIds: Set<String> = emptySet(),
+ private val preferences: Lazy<SharedPreferences> = lazy {
+ context.getSharedPreferences(
+ PREFERENCE_FILE,
+ Context.MODE_PRIVATE,
+ )
+ },
+) : SearchMiddleware.MetadataStorage {
+ /**
+ * Gets the ID (and optinally name) of the default search engine the user has picked. Returns
+ * `null` if the user has not made a choice.
+ */
+ override suspend fun getUserSelectedSearchEngine(): SearchMiddleware.MetadataStorage.UserChoice? {
+ val id = preferences.value.getString(PREFERENCE_KEY_USER_SELECTED_SEARCH_ENGINE_ID, null)
+ ?: return null
+
+ return SearchMiddleware.MetadataStorage.UserChoice(
+ id,
+ preferences.value.getString(PREFERENCE_KEY_USER_SELECTED_SEARCH_ENGINE_NAME, null),
+ )
+ }
+
+ /**
+ * Sets the ID (and optionally name) of the default search engine the user has picked.
+ */
+ override suspend fun setUserSelectedSearchEngine(id: String, name: String?) {
+ preferences.value.edit()
+ .putString(PREFERENCE_KEY_USER_SELECTED_SEARCH_ENGINE_ID, id)
+ .putString(PREFERENCE_KEY_USER_SELECTED_SEARCH_ENGINE_NAME, name)
+ .apply()
+ }
+
+ /**
+ * Sets the list of IDs of hidden search engines.
+ */
+ override suspend fun setHiddenSearchEngines(ids: List<String>) {
+ preferences.value.edit()
+ .putStringSet(PREFERENCE_KEY_HIDDEN_SEARCH_ENGINES, ids.toSet())
+ .apply()
+ }
+
+ /**
+ * Gets the list of IDs of hidden search engines.
+ */
+ override suspend fun getHiddenSearchEngines(): List<String> {
+ return preferences.value
+ .getStringSet(PREFERENCE_KEY_HIDDEN_SEARCH_ENGINES, emptySet())
+ ?.toList() ?: emptyList()
+ }
+
+ /**
+ * Gets the list of IDs of additional search engines that the user explicitly added.
+ */
+ override suspend fun getAdditionalSearchEngines(): List<String> {
+ return preferences.value
+ .getStringSet(PREFERENCE_KEY_ADDITIONAL_SEARCH_ENGINES, emptySet())
+ ?.toList() ?: emptyList()
+ }
+
+ override suspend fun getDisabledSearchEngineIds(): List<String> {
+ return preferences.value
+ .getStringSet(PREFERENCE_KEY_DISABLED_SEARCH_ENGINE_ID, disabledByDefaultSearchEngineIds)
+ ?.toList() ?: emptyList()
+ }
+
+ override suspend fun setDisabledSearchEngineIds(ids: List<String>) {
+ preferences.value.edit()
+ .putStringSet(PREFERENCE_KEY_DISABLED_SEARCH_ENGINE_ID, ids.toSet())
+ .apply()
+ }
+
+ /**
+ * Sets the list of IDs of additional search engines that the user explicitly added.
+ */
+ override suspend fun setAdditionalSearchEngines(ids: List<String>) {
+ preferences.value.edit()
+ .putStringSet(PREFERENCE_KEY_ADDITIONAL_SEARCH_ENGINES, ids.toSet())
+ .apply()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/suggestions/Parser.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/suggestions/Parser.kt
new file mode 100644
index 0000000000..c52fdf1bdb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/suggestions/Parser.kt
@@ -0,0 +1,79 @@
+/* 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.search.suggestions
+
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.support.ktx.android.org.json.asSequence
+import org.json.JSONArray
+import org.json.JSONObject
+
+/**
+ * The Parser is a function that takes a JSON Response and maps
+ * it to a Suggestion list.
+ */
+typealias JSONResponse = String
+typealias ResponseParser = (JSONResponse) -> List<String>
+
+/**
+ * Builds a Parser that pulls suggestions out of a given index
+ */
+private fun buildJSONArrayParser(resultsIndex: Int): ResponseParser {
+ return { input ->
+ JSONArray(input)
+ .getJSONArray(resultsIndex)
+ .asSequence()
+ .map { it as? String }
+ .filterNotNull()
+ .toList()
+ }
+}
+
+/**
+ * Builds a Parser that pulls suggestions out of a JSON object with the given key
+ */
+private fun buildJSONObjectParser(resultsKey: String): ResponseParser {
+ return { input ->
+ JSONObject(input)
+ .getJSONArray(resultsKey)
+ .asSequence()
+ .map { it as? String }
+ .filterNotNull()
+ .toList()
+ }
+}
+
+/**
+ * Builds a custom parser for Qwant
+ */
+private fun buildQwantParser(): ResponseParser {
+ return { input ->
+ JSONObject(input)
+ .getJSONObject("data")
+ .getJSONArray("items")
+ .asSequence()
+ .map { it as? JSONObject }
+ .map { it?.getString("value") }
+ .filterNotNull()
+ .toList()
+ }
+}
+
+/**
+ * The available Parsers
+ */
+internal val defaultResponseParser = buildJSONArrayParser(1)
+internal val azerdictResponseParser = buildJSONObjectParser("suggestions")
+internal val daumResponseParser = buildJSONObjectParser("items")
+internal val qwantResponseParser = buildQwantParser()
+
+/**
+ * Selects a Parser based on a SearchEngine
+ */
+internal fun selectResponseParser(searchEngine: SearchEngine): ResponseParser = when (searchEngine.name) {
+ "Azerdict" -> azerdictResponseParser
+ "다음지도" -> daumResponseParser
+ "Qwant" -> qwantResponseParser
+ else -> defaultResponseParser
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/suggestions/SearchSuggestionClient.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/suggestions/SearchSuggestionClient.kt
new file mode 100644
index 0000000000..d4a0b5434b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/suggestions/SearchSuggestionClient.kt
@@ -0,0 +1,101 @@
+/* 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.search.suggestions
+
+import android.content.Context
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.search.ext.buildSuggestionsURL
+import mozilla.components.feature.search.ext.canProvideSearchSuggestions
+import mozilla.components.support.base.log.logger.Logger
+import org.json.JSONException
+import java.io.IOException
+
+/**
+ * Async function responsible for taking a URL and returning the results
+ */
+typealias SearchSuggestionFetcher = suspend (url: String) -> String?
+
+/**
+ * Provides an interface to get search suggestions from a given SearchEngine.
+ */
+class SearchSuggestionClient {
+ private val context: Context?
+ private val fetcher: SearchSuggestionFetcher
+ private val logger = Logger("SearchSuggestionClient")
+
+ val store: BrowserStore?
+ var searchEngine: SearchEngine? = null
+ private set
+
+ internal constructor(
+ context: Context?,
+ store: BrowserStore?,
+ searchEngine: SearchEngine?,
+ fetcher: SearchSuggestionFetcher,
+ ) {
+ this.context = context
+ this.store = store
+ this.searchEngine = searchEngine
+ this.fetcher = fetcher
+ }
+
+ constructor(searchEngine: SearchEngine, fetcher: SearchSuggestionFetcher) :
+ this (null, null, searchEngine, fetcher)
+
+ constructor(
+ context: Context,
+ store: BrowserStore,
+ fetcher: SearchSuggestionFetcher,
+ ) : this (context, store, null, fetcher)
+
+ /**
+ * Exception types for errors caught while getting a list of suggestions
+ */
+ class FetchException : Exception("There was a problem fetching suggestions")
+ class ResponseParserException : Exception("There was a problem parsing the suggestion response")
+
+ /**
+ * Gets search suggestions for a given query
+ */
+ suspend fun getSuggestions(query: String): List<String>? {
+ val searchEngine = searchEngine ?: run {
+ requireNotNull(store)
+ requireNotNull(context)
+
+ val searchEngine = store.state.search.selectedOrDefaultSearchEngine
+ if (searchEngine == null) {
+ logger.warn("No default search engine for fetching suggestions")
+ return emptyList()
+ } else {
+ this.searchEngine = searchEngine
+ searchEngine
+ }
+ }
+
+ if (!searchEngine.canProvideSearchSuggestions) {
+ // This search engine doesn't support suggestions. Let's only return a default suggestion
+ // for the entered text.
+ return emptyList()
+ }
+
+ val suggestionsURL = searchEngine.buildSuggestionsURL(query)
+
+ val parser = selectResponseParser(searchEngine)
+
+ val suggestionResults = try {
+ suggestionsURL?.let { fetcher(it) }
+ } catch (_: IOException) {
+ throw FetchException()
+ }
+
+ return try {
+ suggestionResults?.let(parser)
+ } catch (_: JSONException) {
+ throw ResponseParserException()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetry.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetry.kt
new file mode 100644
index 0000000000..971e41583a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetry.kt
@@ -0,0 +1,132 @@
+/* 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.search.telemetry
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action.INTERACTION
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
+import org.json.JSONObject
+
+/**
+ * Main configuration and functionality for tracking ads / web searches with specific providers.
+ */
+abstract class BaseSearchTelemetry {
+ var providerList: List<SearchProviderModel>? = emptyList()
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal fun setProviderList(providerListSerp: List<SearchProviderModel>) {
+ providerList = providerListSerp
+ }
+
+ /**
+ * Finds provider among list of providers that matches regex in url.
+ * This may additionally return null if the provider list is still being initialized.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal fun getProviderForUrl(url: String): SearchProviderModel? =
+ providerList?.find { provider -> provider.searchPageRegexp.containsMatchIn(url) }
+
+ /**
+ * Install the web extensions that this functionality is based on and start listening for updates.
+ */
+ abstract suspend fun install(
+ engine: Engine,
+ store: BrowserStore,
+ providerList: List<SearchProviderModel>,
+ )
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ internal fun installWebExtension(
+ engine: Engine,
+ store: BrowserStore,
+ extensionInfo: ExtensionInfo,
+ ) {
+ engine.installBuiltInWebExtension(
+ id = extensionInfo.id,
+ url = extensionInfo.resourceUrl,
+ onSuccess = { extension ->
+ store.flowScoped { flow ->
+ subscribeToUpdates(flow, extension, extensionInfo)
+ }
+ },
+ onError = { throwable ->
+ Logger.error("Could not install ${extensionInfo.id} extension", throwable)
+ },
+ )
+ }
+
+ protected fun emitFact(
+ event: String,
+ value: String,
+ metadata: Map<String, Any>? = null,
+ ) {
+ Fact(
+ Component.FEATURE_SEARCH,
+ INTERACTION,
+ event,
+ value,
+ metadata,
+ ).collect()
+ }
+
+ protected sealed class Action
+
+ private suspend fun subscribeToUpdates(
+ flow: Flow<BrowserState>,
+ extension: WebExtension,
+ extensionInfo: ExtensionInfo,
+ ) {
+ // Whenever we see a new EngineSession in the store then we register our content message
+ // handler if it has not been added yet.
+ flow.map { it.tabs }
+ .filterChanged { it.engineState.engineSession }
+ .collect { state ->
+ val engineSession = state.engineState.engineSession ?: return@collect
+
+ if (extension.hasContentMessageHandler(engineSession, extensionInfo.messageId)) {
+ return@collect
+ }
+ extension.registerContentMessageHandler(
+ engineSession,
+ extensionInfo.messageId,
+ SearchTelemetryMessageHandler(),
+ )
+ }
+ }
+
+ /**
+ * This method is used to process any valid json message coming from a web-extension.
+ */
+ @VisibleForTesting
+ internal abstract fun processMessage(message: JSONObject)
+
+ @VisibleForTesting
+ internal inner class SearchTelemetryMessageHandler : MessageHandler {
+
+ @Throws(IllegalStateException::class)
+ override fun onMessage(message: Any, source: EngineSession?): Any {
+ if (message is JSONObject) {
+ processMessage(message)
+ } else {
+ throw IllegalStateException("Received unexpected message: $message")
+ }
+
+ return Unit
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ExtensionInfo.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ExtensionInfo.kt
new file mode 100644
index 0000000000..492bdf66f8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ExtensionInfo.kt
@@ -0,0 +1,18 @@
+/* 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.search.telemetry
+
+/**
+ * Configuration data of web extensions used for the search / ads telemetry.
+ *
+ * @property id webExtension unique id.
+ * @property resourceUrl location of the webextension (may be local or web hosted).
+ * @property messageId message key used for communicating from the extension to the native app.
+ */
+internal data class ExtensionInfo(
+ val id: String,
+ val resourceUrl: String,
+ val messageId: String,
+)
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderCookie.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderCookie.kt
new file mode 100644
index 0000000000..eceae777f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderCookie.kt
@@ -0,0 +1,24 @@
+/* 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.search.telemetry
+
+/**
+ * Cookie details used to identify follow-on searches.
+ *
+ * @property extraCodeParamName the query parameter name in the URL that indicates
+ * this might be a follow-on search.
+ * @property extraCodePrefixes possible values for the query parameter in the URL that indicates
+ * this might be a follow-on search.
+ * @property host the hostname on which the cookie is stored.
+ * @property name the name of the cookie to check.
+ * @property codeParamName the name of parameter within the cookie.
+ */
+data class SearchProviderCookie(
+ val extraCodeParamName: String,
+ val extraCodePrefixes: List<String>,
+ val host: String,
+ val name: String,
+ val codeParamName: String,
+)
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderModel.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderModel.kt
new file mode 100644
index 0000000000..b45e17d231
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderModel.kt
@@ -0,0 +1,80 @@
+/* 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.search.telemetry
+
+/**
+ * All data needed to identify ads of a particular provider.
+ *
+ * @property taggedCodes array of partner codes to match against the partner code parameters in the url.
+ * @property telemetryId provider name e.g. "google", "duckduckgo".
+ * @property organicCodes array of partner codes to match against the parameters in the url.
+ * Matching these codes will report the SERP as organic:<partner code>, which means the search
+ * was performed organically rather than through a SAP.
+ * @property codeParamName name of the query parameter for the partner code.
+ * @property followOnCookies array of cookie details that are used to identify follow-on searches.
+ * @property queryParamNames list of names of the query parameters for the user's search string.
+ * @property searchPageRegexp regular expression used to match the provider.
+ * @property adServerAttributes an array of strings that potentially match data-attribute keys of anchors.
+ * @property followOnParamNames array of query parameter names that are used when a follow-on search occurs.
+ * @property extraAdServersRegexps array of regular expressions that match URLs of potential ad servers.
+ * @property expectedOrganicCodes array of partner codes to match against the parameters in the url.
+ * Matching these codes will report the SERP as organic:none which means the user has done a search
+ * through the search engine's website rather than through SAP.
+ */
+data class SearchProviderModel(
+ val schema: Long,
+ val taggedCodes: List<String>,
+ val telemetryId: String,
+ val organicCodes: List<String>?,
+ val codeParamName: String,
+ val followOnCookies: List<SearchProviderCookie>?,
+ val queryParamNames: List<String>?,
+ val searchPageRegexp: Regex,
+ val adServerAttributes: List<String>?,
+ val followOnParamNames: List<String>?,
+ val extraAdServersRegexps: List<Regex>,
+ val expectedOrganicCodes: List<String>?,
+
+) {
+
+ constructor(
+ schema: Long,
+ taggedCodes: List<String> = emptyList(),
+ telemetryId: String,
+ organicCodes: List<String>? = emptyList(),
+ codeParamName: String = "",
+ followOnCookies: List<SearchProviderCookie>? = emptyList(),
+ queryParamNames: List<String> = emptyList(),
+ searchPageRegexp: String,
+ adServerAttributes: List<String>? = emptyList(),
+ followOnParamNames: List<String>? = emptyList(),
+ extraAdServersRegexps: List<String> = emptyList(),
+ expectedOrganicCodes: List<String>? = emptyList(),
+
+ ) : this(
+ schema = schema,
+ taggedCodes = taggedCodes,
+ telemetryId = telemetryId,
+ organicCodes = organicCodes,
+ codeParamName = codeParamName,
+ followOnCookies = followOnCookies,
+ queryParamNames = queryParamNames,
+ searchPageRegexp = searchPageRegexp.toRegex(),
+ adServerAttributes = adServerAttributes,
+ followOnParamNames = followOnParamNames,
+ extraAdServersRegexps = extraAdServersRegexps.map { it.toRegex() },
+ expectedOrganicCodes = expectedOrganicCodes,
+
+ )
+
+ /**
+ * Checks if any of the given URLs represent an ad from the search engine.
+ * Used to check if a clicked link was for an ad.
+ */
+ fun containsAdLinks(urlList: List<String>) = urlList.any { url -> isAd(url) }
+
+ private fun isAd(url: String) =
+ extraAdServersRegexps.any { adsRegex -> adsRegex.containsMatchIn(url) }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SerpTelemetryRepository.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SerpTelemetryRepository.kt
new file mode 100644
index 0000000000..436b0e22db
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SerpTelemetryRepository.kt
@@ -0,0 +1,170 @@
+/* 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.search.telemetry
+
+import mozilla.appservices.remotesettings.RemoteSettingsResponse
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.org.json.asSequence
+import mozilla.components.support.ktx.android.org.json.toList
+import mozilla.components.support.remotesettings.RemoteSettingsClient
+import mozilla.components.support.remotesettings.RemoteSettingsResult
+import org.jetbrains.annotations.VisibleForTesting
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.File
+
+internal const val REMOTE_PROD_ENDPOINT_URL = "https://firefox.settings.services.mozilla.com"
+internal const val REMOTE_ENDPOINT_BUCKET_NAME = "main"
+
+/**
+ * Parse SERP Telemetry json from remote config.
+ */
+class SerpTelemetryRepository(
+ rootStorageDirectory: File,
+ private val readJson: () -> JSONObject,
+ collectionName: String,
+ serverUrl: String = REMOTE_PROD_ENDPOINT_URL,
+ bucketName: String = REMOTE_ENDPOINT_BUCKET_NAME,
+) {
+ val logger = Logger("SerpTelemetryRepository")
+ private var providerList: List<SearchProviderModel> = emptyList()
+
+ @VisibleForTesting
+ internal var remoteSettingsClient = RemoteSettingsClient(
+ serverUrl = serverUrl,
+ bucketName = bucketName,
+ collectionName = collectionName,
+ storageRootDirectory = rootStorageDirectory,
+ )
+
+ /**
+ * Provides list of search providers from cache or dump and fetches from remotes server .
+ */
+ suspend fun updateProviderList(): List<SearchProviderModel> {
+ val (cacheLastModified, cacheResponse) = loadProvidersFromCache()
+ val localResponse = readJson()
+ if (cacheResponse.isEmpty() || cacheLastModified <= localResponse.getString("timestamp").toULong()) {
+ providerList = parseLocalPreinstalledData(localResponse)
+ } else if (cacheLastModified > localResponse.getString("timestamp").toULong()) {
+ providerList = cacheResponse
+ }
+ fetchRemoteResponse(cacheLastModified)
+ return providerList
+ }
+
+ @VisibleForTesting
+ internal suspend fun fetchRemoteResponse(
+ cacheLastModified: ULong?,
+ ) {
+ if (cacheLastModified == null) {
+ return
+ }
+ val remoteResponse = fetchRemoteResponse()
+ if (remoteResponse.lastModified > cacheLastModified) {
+ providerList = parseRemoteResponse(remoteResponse)
+ writeToCache(remoteResponse)
+ }
+ }
+
+ /**
+ * Writes data to local cache.
+ */
+ @VisibleForTesting
+ internal suspend fun writeToCache(records: RemoteSettingsResponse): RemoteSettingsResult {
+ return remoteSettingsClient.write(records)
+ }
+
+ /**
+ * Parses local json response.
+ */
+ @VisibleForTesting
+ internal fun parseLocalPreinstalledData(jsonObject: JSONObject): List<SearchProviderModel> {
+ return jsonObject.getJSONArray("data")
+ .asSequence()
+ .mapNotNull {
+ (it as JSONObject).toSearchProviderModel()
+ }
+ .toList()
+ }
+
+ /**
+ * Parses remote server response.
+ */
+ private fun parseRemoteResponse(response: RemoteSettingsResponse): List<SearchProviderModel> {
+ return response.records.mapNotNull {
+ it.fields.toSearchProviderModel()
+ }
+ }
+
+ /**
+ * Returns data from remote servers.
+ */
+ @VisibleForTesting
+ internal suspend fun fetchRemoteResponse(): RemoteSettingsResponse {
+ val result = remoteSettingsClient.fetch()
+ return if (result is RemoteSettingsResult.Success) {
+ result.response
+ } else {
+ RemoteSettingsResponse(emptyList(), 0u)
+ }
+ }
+
+ /**
+ * Returns search providers from local cache.
+ */
+ @VisibleForTesting
+ internal suspend fun loadProvidersFromCache(): Pair<ULong, List<SearchProviderModel>> {
+ val result = remoteSettingsClient.read()
+ return if (result is RemoteSettingsResult.Success) {
+ val response = result.response.records.mapNotNull {
+ it.fields.toSearchProviderModel()
+ }
+ val lastModified = result.response.lastModified
+ Pair(lastModified, response)
+ } else {
+ Pair(0u, emptyList())
+ }
+ }
+}
+
+@VisibleForTesting
+internal fun JSONObject.toSearchProviderModel(): SearchProviderModel? =
+ try {
+ SearchProviderModel(
+ schema = getLong("schema"),
+ taggedCodes = getJSONArray("taggedCodes").toList(),
+ telemetryId = optString("telemetryId"),
+ organicCodes = getJSONArray("organicCodes").toList(),
+ codeParamName = optString("codeParamName"),
+ followOnCookies = optJSONArray("followOnCookies")?.toListOfCookies(),
+ queryParamNames = optJSONArray("queryParamNames").toList(),
+ searchPageRegexp = optString("searchPageRegexp"),
+ adServerAttributes = optJSONArray("adServerAttributes").toList(),
+ followOnParamNames = optJSONArray("followOnParamNames")?.toList(),
+ extraAdServersRegexps = getJSONArray("extraAdServersRegexps").toList(),
+ expectedOrganicCodes = optJSONArray("expectedOrganicCodes")?.toList(),
+ )
+ } catch (e: JSONException) {
+ Logger("SerpTelemetryRepository").error("JSONException while trying to parse remote config", e)
+ null
+ }
+
+private fun JSONArray.toListOfCookies(): List<SearchProviderCookie> =
+ toList<JSONObject>().mapNotNull { jsonObject -> jsonObject.toSearchProviderCookie() }
+
+private fun JSONObject.toSearchProviderCookie(): SearchProviderCookie? =
+ try {
+ SearchProviderCookie(
+ extraCodeParamName = optString("extraCodeParamName"),
+ extraCodePrefixes = getJSONArray("extraCodePrefixes").toList(),
+ host = optString("host"),
+ name = optString("name"),
+ codeParamName = optString("codeParamName"),
+ )
+ } catch (e: JSONException) {
+ Logger("SerpTelemetryRepository").error("JSONException while trying to parse remote config", e)
+ null
+ }
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/TrackKeyInfo.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/TrackKeyInfo.kt
new file mode 100644
index 0000000000..c212c13b6f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/TrackKeyInfo.kt
@@ -0,0 +1,38 @@
+/* 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.search.telemetry
+
+import java.util.Locale
+
+/**
+ * Key information about a Search Engine Result Page (SERP).
+ *
+ * @property provider The name of the search provider.
+ * @property type The search access point type (SAP). This is either "organic", "sap" or
+ * "sap-follow-on".
+ * @property code The search URL's `code` query parameter.
+ * @property channel The search URL's `channel` query parameter.
+ */
+internal data class TrackKeyInfo(
+ var provider: String,
+ var type: String,
+ var code: String?,
+ var channel: String? = null,
+) {
+ /**
+ * Returns the track key information into the following string format:
+ * `<provider>.in-content.[sap|sap-follow-on|organic].[code|none](.[channel])?`.
+ */
+ fun createTrackKey(): String {
+ return "${provider.lowercase(Locale.ROOT)}.in-content" +
+ ".${type.lowercase(Locale.ROOT)}" +
+ ".${code?.lowercase(Locale.ROOT) ?: "none"}" +
+ if (!channel?.lowercase(Locale.ROOT).isNullOrBlank()) {
+ ".${channel?.lowercase(Locale.ROOT)}"
+ } else {
+ ""
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/Utils.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/Utils.kt
new file mode 100644
index 0000000000..b541c83d5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/Utils.kt
@@ -0,0 +1,116 @@
+/* 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.search.telemetry
+
+import android.net.Uri
+import org.json.JSONObject
+
+private const val SEARCH_TYPE_SAP_FOLLOW_ON = "sap-follow-on"
+private const val SEARCH_TYPE_SAP = "sap"
+private const val SEARCH_TYPE_ORGANIC = "organic"
+private const val CHANNEL_KEY = "channel"
+private val validChannelSet = setOf("ts")
+
+/**
+ * Get a String in a specific format allowing to identify how an ads/search provider was used.
+ *
+ * @see [TrackKeyInfo.createTrackKey]
+ */
+@Suppress("NestedBlockDepth", "ComplexMethod")
+internal fun getTrackKey(
+ provider: SearchProviderModel,
+ uri: Uri,
+ cookies: List<JSONObject>,
+): String {
+ var type = SEARCH_TYPE_ORGANIC
+ val paramSet = uri.queryParameterNames
+ var code: String? = "none"
+
+ if (provider.codeParamName.isNotEmpty()) {
+ code = uri.getQueryParameter(provider.codeParamName)
+ if (code.isNullOrEmpty() &&
+ provider.telemetryId == "baidu" &&
+ uri.toString().contains("from=")
+ ) {
+ code = uri.toString().substringAfter("from=", "")
+ .substringBefore("/", "")
+ }
+ if (code != null) {
+ // The code is only included if it matches one of the specific ones.
+ if (provider.taggedCodes.contains(code)) {
+ type = SEARCH_TYPE_SAP
+ if (provider.followOnParamNames?.any { p -> paramSet.contains(p) } == true) {
+ type = SEARCH_TYPE_SAP_FOLLOW_ON
+ }
+ } else if (provider.organicCodes?.contains(code) == true) {
+ type = SEARCH_TYPE_ORGANIC
+ } else if (provider.expectedOrganicCodes?.contains(code) == true) {
+ code = "none"
+ } else {
+ code = "other"
+ }
+ } else if (provider.followOnCookies != null) {
+ // Try cookies first because Bing has followOnCookies and valid code, but no
+ // followOnParams => would track organic instead of sap-follow-on
+ getTrackKeyFromCookies(provider, uri, cookies)?.let {
+ return it.createTrackKey()
+ }
+ }
+
+ // For Bing if it didn't have a valid cookie and for all the other search engines
+ if (hasValidCode(uri.getQueryParameter(provider.codeParamName), provider)) {
+ var channel = uri.getQueryParameter(CHANNEL_KEY)
+
+ // For Bug 1751955
+ if (!validChannelSet.contains(channel)) {
+ channel = null
+ }
+ return TrackKeyInfo(provider.telemetryId, type, code, channel).createTrackKey()
+ }
+ }
+ return TrackKeyInfo(provider.telemetryId, type, code).createTrackKey()
+}
+
+private fun getTrackKeyFromCookies(
+ provider: SearchProviderModel,
+ uri: Uri,
+ cookies: List<JSONObject>,
+): TrackKeyInfo? {
+ // Especially Bing requires lots of extra work related to cookies.
+ provider.followOnCookies?.forEach { followOnCookie ->
+ val eCode = uri.getQueryParameter(followOnCookie.extraCodeParamName)
+
+ if (eCode == null || !followOnCookie.extraCodePrefixes.any { prefix ->
+ eCode.startsWith(prefix)
+ }
+ ) {
+ return@forEach
+ }
+
+ // If this cookie is present, it's probably an SAP follow-on.
+ // This might be an organic follow-on in the same session, but there
+ // is no way to tell the difference.
+ for (cookie in cookies) {
+ if (cookie.getString("name") != followOnCookie.name) {
+ continue
+ }
+ val valueList = cookie.getString("value")
+ .split("=")
+ .map { item -> item.trim() }
+
+ if (valueList.size == 2 && valueList[0] == followOnCookie.codeParamName &&
+ provider.taggedCodes.any { prefix ->
+ valueList[1] == prefix
+ }
+ ) {
+ return TrackKeyInfo(provider.telemetryId, SEARCH_TYPE_SAP_FOLLOW_ON, valueList[1])
+ }
+ }
+ }
+ return null
+}
+
+private fun hasValidCode(code: String?, provider: SearchProviderModel): Boolean =
+ code != null && provider.taggedCodes.any { prefix -> code == prefix }
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetry.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetry.kt
new file mode 100644
index 0000000000..810baeff67
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetry.kt
@@ -0,0 +1,124 @@
+/* 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.search.telemetry.ads
+
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.search.telemetry.BaseSearchTelemetry
+import mozilla.components.feature.search.telemetry.ExtensionInfo
+import mozilla.components.feature.search.telemetry.SearchProviderModel
+import mozilla.components.feature.search.telemetry.getTrackKey
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.ktx.android.org.json.toList
+import org.json.JSONObject
+
+/**
+ * Telemetry for knowing how often users see/click ads in search and from which provider.
+ *
+ * Implemented as a browser extension based on the WebExtension API:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
+ */
+class AdsTelemetry : BaseSearchTelemetry() {
+
+ // SERP cached cookies used to check whether an ad was clicked.
+ @VisibleForTesting
+ internal var cachedCookies = listOf<JSONObject>()
+
+ override suspend fun install(
+ engine: Engine,
+ store: BrowserStore,
+ providerList: List<SearchProviderModel>,
+ ) {
+ val info = ExtensionInfo(
+ id = ADS_EXTENSION_ID,
+ resourceUrl = ADS_EXTENSION_RESOURCE_URL,
+ messageId = ADS_MESSAGE_ID,
+ )
+ installWebExtension(engine, store, info)
+ setProviderList(providerList)
+ }
+
+ override fun processMessage(message: JSONObject) {
+ // Cache the cookies list when the extension sends a message.
+ cachedCookies = message.getJSONArray(ADS_MESSAGE_COOKIES_KEY).toList()
+
+ val urls = message.getJSONArray(ADS_MESSAGE_DOCUMENT_URLS_KEY).toList<String>()
+ val uri = Uri.parse(message.getString(ADS_MESSAGE_SESSION_URL_KEY))
+ val provider = getProviderForUrl(message.getString(ADS_MESSAGE_SESSION_URL_KEY))
+
+ provider?.let {
+ if (it.containsAdLinks(urls)) {
+ emitFact(
+ SERP_SHOWN_WITH_ADDS,
+ getTrackKey(it, uri, cachedCookies),
+ )
+ }
+ }
+ }
+
+ /**
+ * To be called when the browser is navigating to a new URL, which may be a search ad.
+ *
+ * @param url The URL of the page before the search ad was clicked.
+ * This will be used to determine the originating search provider.
+ * @param urlPath A list of the URLs and load requests collected in between location changes.
+ * Clicking on a search ad generates a list of redirects from the originating search provider
+ * to the ad source. This is used to determine if there was an ad click.
+ */
+ @Suppress("ReturnCount")
+ fun checkIfAddWasClicked(url: String?, urlPath: List<String>) {
+ if (url == null) {
+ return
+ }
+ val uri = Uri.parse(url) ?: return
+ val provider = getProviderForUrl(url) ?: return
+ val paramSet = uri.queryParameterNames
+ val containsQueryParam = provider.queryParamNames?.any { paramSet.contains(it) }
+
+ if (containsQueryParam == false || !provider.containsAdLinks(urlPath)) {
+ // Do nothing if the URL does not have the search provider's query parameter or
+ // there were no ad clicks.
+ return
+ }
+
+ emitFact(
+ SERP_ADD_CLICKED,
+ getTrackKey(provider, uri, cachedCookies),
+ )
+ }
+
+ companion object {
+ /**
+ * [Fact] property indicating the user open a Search Engine Result Page
+ * of one of our search providers which contains ads.
+ */
+ const val SERP_SHOWN_WITH_ADDS = "SERP shown with adds"
+
+ /**
+ * [Fact] property indicating that an ad was clicked in a Search Engine Result Page.
+ */
+ const val SERP_ADD_CLICKED = "SERP add clicked"
+
+ @VisibleForTesting
+ internal const val ADS_EXTENSION_ID = "ads@mozac.org"
+
+ @VisibleForTesting
+ internal const val ADS_EXTENSION_RESOURCE_URL = "resource://android/assets/extensions/ads/"
+
+ @VisibleForTesting
+ internal const val ADS_MESSAGE_SESSION_URL_KEY = "url"
+
+ @VisibleForTesting
+ internal const val ADS_MESSAGE_DOCUMENT_URLS_KEY = "urls"
+
+ @VisibleForTesting
+ internal const val ADS_MESSAGE_COOKIES_KEY = "cookies"
+
+ @VisibleForTesting
+ internal const val ADS_MESSAGE_ID = "MozacBrowserAdsMessage"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetry.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetry.kt
new file mode 100644
index 0000000000..5a888b6ebb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetry.kt
@@ -0,0 +1,86 @@
+/* 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.search.telemetry.incontent
+
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.search.telemetry.BaseSearchTelemetry
+import mozilla.components.feature.search.telemetry.ExtensionInfo
+import mozilla.components.feature.search.telemetry.SearchProviderModel
+import mozilla.components.feature.search.telemetry.getTrackKey
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.ktx.android.org.json.toList
+import org.json.JSONObject
+
+/**
+ * Telemetry for knowing of in-web-content searches (including follow-on searches) and the provider used.
+ *
+ * Implemented as a browser extension based on the WebExtension API:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
+ */
+class InContentTelemetry : BaseSearchTelemetry() {
+
+ override suspend fun install(
+ engine: Engine,
+ store: BrowserStore,
+ providerList: List<SearchProviderModel>,
+ ) {
+ val info = ExtensionInfo(
+ id = SEARCH_EXTENSION_ID,
+ resourceUrl = SEARCH_EXTENSION_RESOURCE_URL,
+ messageId = SEARCH_MESSAGE_ID,
+ )
+ installWebExtension(engine, store, info)
+ setProviderList(providerList)
+ }
+
+ /**
+ * Processes a message containing search-related information.
+ */
+ override fun processMessage(message: JSONObject) {
+ val cookies = message.getJSONArray(SEARCH_MESSAGE_LIST_KEY).toList<JSONObject>()
+ trackPartnerUrlTypeMetric(message.getString(SEARCH_MESSAGE_SESSION_URL_KEY), cookies)
+ }
+
+ @VisibleForTesting
+ internal fun trackPartnerUrlTypeMetric(url: String, cookies: List<JSONObject>) {
+ val provider = getProviderForUrl(url) ?: return
+ val uri = Uri.parse(url)
+ val paramSet = uri.queryParameterNames
+ val containsQueryParam = provider.queryParamNames?.any { paramSet.contains(it) }
+ if (containsQueryParam == false) {
+ return
+ }
+ emitFact(
+ IN_CONTENT_SEARCH,
+ getTrackKey(provider, uri, cookies),
+ )
+ }
+
+ companion object {
+ /**
+ * [Fact] property indicating that the user did a search, be it a new one
+ * or continuing from an existing search.
+ */
+ const val IN_CONTENT_SEARCH = "in content search"
+
+ @VisibleForTesting
+ internal const val SEARCH_EXTENSION_ID = "cookies@mozac.org"
+
+ @VisibleForTesting
+ internal const val SEARCH_EXTENSION_RESOURCE_URL = "resource://android/assets/extensions/search/"
+
+ @VisibleForTesting
+ internal const val SEARCH_MESSAGE_SESSION_URL_KEY = "url"
+
+ @VisibleForTesting
+ internal const val SEARCH_MESSAGE_LIST_KEY = "cookies"
+
+ @VisibleForTesting
+ internal const val SEARCH_MESSAGE_ID = "MozacBrowserSearchMessage"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/widget/AppSearchWidgetProvider.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/widget/AppSearchWidgetProvider.kt
new file mode 100644
index 0000000000..62a4e2d83e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/widget/AppSearchWidgetProvider.kt
@@ -0,0 +1,312 @@
+/* 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.search.widget
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.widget.RemoteViews
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.graphics.drawable.toBitmap
+import mozilla.components.feature.search.R
+import mozilla.components.feature.search.widget.BaseVoiceSearchActivity.Companion.SPEECH_PROCESSING
+import mozilla.components.support.utils.PendingIntentUtils
+
+/**
+ * An abstract [AppWidgetProvider] that implements core behaviour needed to support a Search Widget
+ * on the launcher.
+ */
+abstract class AppSearchWidgetProvider : AppWidgetProvider() {
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray,
+ ) {
+ val textSearchIntent = createTextSearchIntent(context)
+ val voiceSearchIntent = createVoiceSearchIntent(context)
+
+ appWidgetIds.forEach { appWidgetId ->
+ updateWidgetLayout(
+ context = context,
+ appWidgetId = appWidgetId,
+ appWidgetManager = appWidgetManager,
+ voiceSearchIntent = voiceSearchIntent,
+ textSearchIntent = textSearchIntent,
+ )
+ }
+ }
+
+ override fun onAppWidgetOptionsChanged(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetId: Int,
+ newOptions: Bundle?,
+ ) {
+ val textSearchIntent = createTextSearchIntent(context)
+ val voiceSearchIntent = createVoiceSearchIntent(context)
+
+ updateWidgetLayout(
+ context = context,
+ appWidgetId = appWidgetId,
+ appWidgetManager = appWidgetManager,
+ voiceSearchIntent = voiceSearchIntent,
+ textSearchIntent = textSearchIntent,
+ )
+ }
+
+ /**
+ * Builds pending intent that opens the browser and starts a new text search.
+ */
+ abstract fun createTextSearchIntent(context: Context): PendingIntent
+
+ /**
+ * If the microphone will appear on the Search Widget and the user can perform a voice search.
+ */
+ abstract fun shouldShowVoiceSearch(context: Context): Boolean
+
+ /**
+ * Activity that extends BaseVoiceSearchActivity.
+ */
+ abstract fun voiceSearchActivity(): Class<out BaseVoiceSearchActivity>
+
+ /**
+ * Config that sets the icons and the strings for search widget.
+ */
+ abstract val config: SearchWidgetConfig
+
+ /**
+ * Builds pending intent that starts a new voice search.
+ */
+ @VisibleForTesting
+ internal fun createVoiceSearchIntent(context: Context): PendingIntent? {
+ if (!shouldShowVoiceSearch(context)) {
+ return null
+ }
+
+ val voiceIntent = Intent(context, voiceSearchActivity()).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ putExtra(SPEECH_PROCESSING, true)
+ }
+
+ return PendingIntent.getActivity(
+ context,
+ REQUEST_CODE_VOICE,
+ voiceIntent,
+ PendingIntentUtils.defaultFlags,
+ )
+ }
+
+ private fun updateWidgetLayout(
+ context: Context,
+ appWidgetId: Int,
+ appWidgetManager: AppWidgetManager,
+ voiceSearchIntent: PendingIntent?,
+ textSearchIntent: PendingIntent,
+ ) {
+ val currentWidth =
+ appWidgetManager.getAppWidgetOptions(appWidgetId).getInt(OPTION_APPWIDGET_MIN_WIDTH)
+ val layoutSize = getLayoutSize(currentWidth)
+ // It's not enough to just hide the microphone on the "small" sized widget due to its design.
+ // The "small" widget needs a complete redesign, meaning it needs a new layout file.
+ val showMic = (voiceSearchIntent != null)
+ val layout = getLayout(layoutSize, showMic)
+ val text = getText(layoutSize, context)
+
+ val views =
+ createRemoteViews(context, layout, textSearchIntent, voiceSearchIntent, text)
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
+
+ private fun createRemoteViews(
+ context: Context,
+ layout: Int,
+ textSearchIntent: PendingIntent,
+ voiceSearchIntent: PendingIntent?,
+ text: String?,
+ ): RemoteViews {
+ return RemoteViews(context.packageName, layout).apply {
+ setSearchWidgetIcon(context)
+ setMicrophoneIcon(context)
+ when (layout) {
+ R.layout.mozac_search_widget_extra_small_v1,
+ R.layout.mozac_search_widget_extra_small_v2,
+ R.layout.mozac_search_widget_small_no_mic,
+ -> {
+ setOnClickPendingIntent(
+ R.id.mozac_button_search_widget_new_tab,
+ textSearchIntent,
+ )
+ }
+ R.layout.mozac_search_widget_small -> {
+ setOnClickPendingIntent(
+ R.id.mozac_button_search_widget_new_tab,
+ textSearchIntent,
+ )
+ setOnClickPendingIntent(
+ R.id.mozac_button_search_widget_voice,
+ voiceSearchIntent,
+ )
+ }
+ R.layout.mozac_search_widget_medium,
+ R.layout.mozac_search_widget_large,
+ -> {
+ setOnClickPendingIntent(
+ R.id.mozac_button_search_widget_new_tab,
+ textSearchIntent,
+ )
+ setOnClickPendingIntent(
+ R.id.mozac_button_search_widget_voice,
+ voiceSearchIntent,
+ )
+ setOnClickPendingIntent(
+ R.id.mozac_button_search_widget_new_tab_icon,
+ textSearchIntent,
+ )
+ setTextViewText(R.id.mozac_button_search_widget_new_tab, text)
+
+ // Unlike "small" widget, "medium" and "large" sizes do not have separate layouts
+ // that exclude the microphone icon, which is why we must hide it accordingly here.
+ if (voiceSearchIntent == null) {
+ setViewVisibility(R.id.mozac_button_search_widget_voice, View.GONE)
+ }
+ }
+ }
+ }
+ }
+
+ private fun RemoteViews.setMicrophoneIcon(context: Context) {
+ setImageView(
+ context,
+ R.id.mozac_button_search_widget_voice,
+ config.searchWidgetMicrophoneResource,
+ )
+ }
+
+ private fun RemoteViews.setSearchWidgetIcon(context: Context) {
+ setImageView(
+ context,
+ R.id.mozac_button_search_widget_new_tab_icon,
+ config.searchWidgetIconResource,
+ )
+ val appName = context.getString(config.appName)
+ setContentDescription(
+ R.id.mozac_button_search_widget_new_tab_icon,
+ context.getString(R.string.search_widget_content_description, appName),
+ )
+ }
+
+ private fun RemoteViews.setImageView(context: Context, viewId: Int, resourceId: Int) {
+ // gradient color available for android:fillColor only on SDK 24+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ setImageViewResource(
+ viewId,
+ resourceId,
+ )
+ } else {
+ setImageViewBitmap(
+ viewId,
+ AppCompatResources.getDrawable(
+ context,
+ resourceId,
+ )?.toBitmap(),
+ )
+ }
+ }
+
+ // Cell sizes obtained from the actual dimensions listed in search widget specs.
+ companion object {
+ private const val DP_EXTRA_SMALL = 64
+ private const val DP_SMALL = 100
+ private const val DP_MEDIUM = 192
+ private const val DP_LARGE = 256
+ private const val REQUEST_CODE_VOICE = 1
+
+ /**
+ * It updates AppSearchWidgetProvider size and microphone icon visibility.
+ */
+ fun updateAllWidgets(context: Context, clazz: Class<out AppSearchWidgetProvider>) {
+ val widgetManager = AppWidgetManager.getInstance(context)
+ val widgetIds = widgetManager.getAppWidgetIds(
+ ComponentName(
+ context,
+ clazz,
+ ),
+ )
+ if (widgetIds.isNotEmpty()) {
+ context.sendBroadcast(
+ Intent(context, clazz).apply {
+ action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds)
+ },
+ )
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getLayoutSize(@Dimension(unit = DP) dp: Int) = when {
+ dp >= DP_LARGE -> SearchWidgetProviderSize.LARGE
+ dp >= DP_MEDIUM -> SearchWidgetProviderSize.MEDIUM
+ dp >= DP_SMALL -> SearchWidgetProviderSize.SMALL
+ dp >= DP_EXTRA_SMALL -> SearchWidgetProviderSize.EXTRA_SMALL_V2
+ else -> SearchWidgetProviderSize.EXTRA_SMALL_V1
+ }
+
+ /**
+ * Get the layout resource to use for the search widget.
+ */
+ @VisibleForTesting
+ internal fun getLayout(size: SearchWidgetProviderSize, showMic: Boolean) = when (size) {
+ SearchWidgetProviderSize.LARGE -> R.layout.mozac_search_widget_large
+ SearchWidgetProviderSize.MEDIUM -> R.layout.mozac_search_widget_medium
+ SearchWidgetProviderSize.SMALL -> {
+ if (showMic) {
+ R.layout.mozac_search_widget_small
+ } else {
+ R.layout.mozac_search_widget_small_no_mic
+ }
+ }
+ SearchWidgetProviderSize.EXTRA_SMALL_V2 -> R.layout.mozac_search_widget_extra_small_v2
+ SearchWidgetProviderSize.EXTRA_SMALL_V1 -> R.layout.mozac_search_widget_extra_small_v1
+ }
+
+ /**
+ * Get the text to place in the search widget.
+ */
+ @VisibleForTesting
+ internal fun getText(layout: SearchWidgetProviderSize, context: Context) = when (layout) {
+ SearchWidgetProviderSize.MEDIUM -> context.getString(R.string.search_widget_text_short)
+ SearchWidgetProviderSize.LARGE -> context.getString(R.string.search_widget_text_long)
+ else -> null
+ }
+ }
+}
+
+/**
+ * Client App can set from this config icons and the app name for search widget.
+ */
+data class SearchWidgetConfig(
+ val searchWidgetIconResource: Int,
+ val searchWidgetMicrophoneResource: Int,
+ val appName: Int,
+)
+
+internal enum class SearchWidgetProviderSize {
+ EXTRA_SMALL_V1,
+ EXTRA_SMALL_V2,
+ SMALL,
+ MEDIUM,
+ LARGE,
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivity.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivity.kt
new file mode 100644
index 0000000000..1c70f9809d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivity.kt
@@ -0,0 +1,134 @@
+/* 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.search.widget
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.speech.RecognizerIntent
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AppCompatActivity
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.ext.getParcelableCompat
+import java.util.Locale
+
+/**
+ * Launches voice recognition then uses it to start a new web search.
+ */
+abstract class BaseVoiceSearchActivity : AppCompatActivity() {
+
+ /**
+ * Holds the intent that initially started this activity
+ * so that it can persist through the speech activity.
+ */
+ private var previousIntent: Intent? = null
+
+ private var activityResultLauncher: ActivityResultLauncher<Intent> = getActivityResultLauncher()
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putParcelable(PREVIOUS_INTENT, previousIntent)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // Retrieve the previous intent from the saved state
+ previousIntent = savedInstanceState?.getParcelableCompat(PREVIOUS_INTENT, Intent::class.java)
+ if (previousIntent.isForSpeechProcessing()) {
+ // Don't reopen the speech recognizer
+ return
+ }
+
+ // The intent property is nullable, but the rest of the code below assumes it is not.
+ val intent = intent?.let { Intent(intent) } ?: Intent()
+ if (intent.isForSpeechProcessing()) {
+ previousIntent = intent
+ displaySpeechRecognizer()
+ } else {
+ finish()
+ }
+ }
+
+ /**
+ * Language locale for Voice Search.
+ */
+ abstract fun getCurrentLocale(): Locale
+
+ /**
+ * Speech recognizer popup is shown.
+ */
+ abstract fun onSpeechRecognitionStarted()
+
+ /**
+ * Start intent after voice search ,for example a browser page is open with the spokenText.
+ * @param spokenText what the user voice search
+ */
+ abstract fun onSpeechRecognitionEnded(spokenText: String)
+
+ @VisibleForTesting
+ internal fun activityResultImplementation(activityResult: ActivityResult) {
+ if (activityResult.resultCode == Activity.RESULT_OK) {
+ val spokenText =
+ activityResult.data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
+ ?.first()
+ previousIntent?.apply {
+ spokenText?.let { onSpeechRecognitionEnded(it) }
+ }
+ }
+ finish()
+ }
+
+ private fun getActivityResultLauncher(): ActivityResultLauncher<Intent> {
+ return registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult(),
+ ) {
+ activityResultImplementation(it)
+ }
+ }
+
+ /**
+ * Displays a speech recognizer popup that listens for input from the user.
+ */
+ private fun displaySpeechRecognizer() {
+ val intentSpeech = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
+ putExtra(
+ RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
+ )
+ putExtra(
+ RecognizerIntent.EXTRA_LANGUAGE,
+ getCurrentLocale(),
+ )
+ }
+ onSpeechRecognitionStarted()
+ try {
+ activityResultLauncher.launch(intentSpeech)
+ } catch (e: ActivityNotFoundException) {
+ Logger(TAG).error("ActivityNotFoundException " + e.message.toString())
+ finish()
+ }
+ }
+
+ /**
+ * Returns true if the [SPEECH_PROCESSING] extra is present and set to true.
+ * Returns false if the intent is null.
+ */
+ private fun Intent?.isForSpeechProcessing(): Boolean =
+ this?.getBooleanExtra(SPEECH_PROCESSING, false) == true
+
+ companion object {
+ const val PREVIOUS_INTENT = "org.mozilla.components.previous_intent"
+
+ /**
+ * In [BaseVoiceSearchActivity] activity, used to store if the speech processing should start.
+ */
+ const val SPEECH_PROCESSING = "speech_processing"
+ const val TAG = "BaseVoiceSearchActivity"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/drawable/mozac_rounded_search_widget_background.xml b/mobile/android/android-components/components/feature/search/src/main/res/drawable/mozac_rounded_search_widget_background.xml
new file mode 100644
index 0000000000..fd61fe2557
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/drawable/mozac_rounded_search_widget_background.xml
@@ -0,0 +1,9 @@
+<?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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/mozac_feature_search_widget_background_color" />
+ <corners android:radius="@dimen/mozac_tab_corner_radius"/>
+</shape>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_extra_small_v1.xml b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_extra_small_v1.xml
new file mode 100644
index 0000000000..ee95021031
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_extra_small_v1.xml
@@ -0,0 +1,20 @@
+<?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/. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/mozac_button_search_widget_new_tab"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:background="@drawable/mozac_rounded_search_widget_background">
+
+ <ImageView
+ android:id="@+id/mozac_button_search_widget_new_tab_icon"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:contentDescription="@string/search_widget_content_description"
+ android:scaleType="centerInside" />
+
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_extra_small_v2.xml b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_extra_small_v2.xml
new file mode 100644
index 0000000000..468df05b75
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_extra_small_v2.xml
@@ -0,0 +1,19 @@
+<?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/. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/mozac_button_search_widget_new_tab"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:background="@drawable/mozac_rounded_search_widget_background">
+
+ <ImageView
+ android:id="@+id/mozac_button_search_widget_new_tab_icon"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:contentDescription="@string/search_widget_content_description"
+ android:scaleType="centerInside" />
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_large.xml b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_large.xml
new file mode 100644
index 0000000000..7801adb460
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_large.xml
@@ -0,0 +1,45 @@
+<?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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:background="@drawable/mozac_rounded_search_widget_background">
+
+ <ImageView
+ android:id="@+id/mozac_button_search_widget_new_tab_icon"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_alignParentStart="true"
+ android:contentDescription="@string/search_widget_content_description"
+ android:scaleType="centerInside" />
+
+ <TextView
+ android:id="@+id/mozac_button_search_widget_new_tab"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignEnd="@id/mozac_button_search_widget_voice"
+ android:layout_marginStart="9dp"
+ android:layout_toEndOf="@id/mozac_button_search_widget_new_tab_icon"
+ android:gravity="start|center_vertical"
+ android:letterSpacing="-0.025"
+ android:textAlignment="viewStart"
+ android:textColor="@color/mozac_feature_search_widget_text_color"
+ android:textSize="15sp"
+ tools:text="Search the web" />
+
+ <ImageButton
+ android:id="@+id/mozac_button_search_widget_voice"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentEnd="true"
+ android:background="@android:color/transparent"
+ android:layout_centerVertical="true"
+ android:padding="8dp"
+ android:layout_marginEnd="1dp"
+ android:contentDescription="@string/search_widget_voice" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_medium.xml b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_medium.xml
new file mode 100644
index 0000000000..5cbcdffd84
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_medium.xml
@@ -0,0 +1,45 @@
+<?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/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:background="@drawable/mozac_rounded_search_widget_background">
+
+ <ImageView
+ android:id="@+id/mozac_button_search_widget_new_tab_icon"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_alignParentStart="true"
+ android:contentDescription="@string/search_widget_content_description"
+ android:scaleType="centerInside" />
+
+ <TextView
+ android:id="@+id/mozac_button_search_widget_new_tab"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignEnd="@id/mozac_button_search_widget_voice"
+ android:layout_marginStart="9dp"
+ android:layout_toEndOf="@id/mozac_button_search_widget_new_tab_icon"
+ android:gravity="start|center_vertical"
+ android:letterSpacing="-0.025"
+ android:textAlignment="viewStart"
+ android:textColor="@color/mozac_feature_search_widget_text_color"
+ android:textSize="15sp"
+ tools:text="Search" />
+
+ <ImageButton
+ android:id="@+id/mozac_button_search_widget_voice"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:padding="8dp"
+ android:background="@android:color/transparent"
+ android:layout_marginEnd="1dp"
+ android:contentDescription="@string/search_widget_voice" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_small.xml b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_small.xml
new file mode 100644
index 0000000000..c75217e90f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_small.xml
@@ -0,0 +1,27 @@
+<?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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:background="@drawable/mozac_rounded_search_widget_background" >
+
+ <ImageView
+ android:id="@+id/mozac_button_search_widget_new_tab_icon"
+ android:layout_alignParentStart="true"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:contentDescription="@string/search_widget_content_description"
+ android:scaleType="centerInside" />
+
+ <ImageView
+ android:id="@+id/mozac_button_search_widget_voice"
+ android:layout_alignParentEnd="true"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:contentDescription="@string/search_widget_voice"
+ android:padding="10dp"
+ android:background="@android:color/transparent"
+ android:scaleType="centerInside" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_small_no_mic.xml b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_small_no_mic.xml
new file mode 100644
index 0000000000..61d2152b4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/layout/mozac_search_widget_small_no_mic.xml
@@ -0,0 +1,19 @@
+<?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/. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/mozac_button_search_widget_new_tab"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:background="@drawable/mozac_rounded_search_widget_background"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/mozac_button_search_widget_new_tab_icon"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_gravity="center"
+ android:contentDescription="@string/search_widget_content_description" />
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..05a821675e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-am/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">አዲስ %1$s ትር ክፈት</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ፈልግ</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ድሩን ይፈልጉ</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">የድምጽ ፍለጋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..d75141a022
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ar/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">افتح لسان %1$s جديد</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ابحث</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ابحث في الوِب</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">البحث الصوتي</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..dab4e3690a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ast/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir nuna llingüeta de %1$s nueva</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Buscar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Buscar na web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Busca pela voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..dad76ea236
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-azb/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">یئنی بیر %1$s تاغی آچین</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">آختاریش</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">وب‌ده آختار</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">سس‌لی آختاریش</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..d01b3ed8ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-be/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Адкрыць новую картку %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Пошук</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Пошук у інтэрнэце</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Галасавы пошук</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..62aa6628d4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-bg/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Отваряне на раздел с %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Търсене</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Търсене в интернет</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Гласово търсене</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..efdc6aa03a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-br/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Digeriñ un ivinell %1$s nevez</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Klask</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Klask er web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Klask dre vouezh</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..8d89c63779
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-bs/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Otvori novi %1$s tab</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Traži</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Pretraži web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Glasovna pretraga</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..1821b2b9f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ca/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Obre en una pestanya nova en %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Cerca</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Cerca al web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Cerca per veu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..c4630b9637
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-cak/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Tijaq jun k\'ak\'a\' %1$s ruwi\'</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Tikanöx</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Tikanöx pan ajk\'amaya\'l</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Tikanöx chi ch\'ab\'äl</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..4f31f2232f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">بازدەرێکی %1$sی نوێ بکەرەوە</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">گەڕان</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">بە وێبدا بگەڕێ</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">گەڕانی دەنگی</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..a38dd9e634
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-co/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Apre una nova unghjetta in %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Ricercà</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Ricercà nant’à u web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Ricerca vucale</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..b889d8f9a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-cs/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Otevřít nový panel v aplikaci %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Hledat</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Vyhledat na webu</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Hlasové vyhledávání</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..5c8f73d4bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-cy/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Agor tab %1$s newydd</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Chwilio</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Chwilio’r we</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Chwilio llais</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..a8af438d90
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-da/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Åbn et nyt %1$s-faneblad</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Søg</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Søg på nettet</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Stemme-søgning</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..0b98201168
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-de/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Neuen %1$s-Tab öffnen</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Suche</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Das Web durchsuchen</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Sprachsuche</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..c5b19b7f83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Nowy rejtarik %1$s wócyniś</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Pytaś</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Web pśepytaś</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Głosowe pytanje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..d5ef9aeb67
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-el/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Άνοιγμα νέας καρτέλας %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Αναζήτηση</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Αναζήτηση στο διαδίκτυο</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Φωνητική αναζήτηση</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..93a653aed7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Open a new %1$s tab</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Search</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Search the web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Voice search</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..93a653aed7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Open a new %1$s tab</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Search</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Search the web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Voice search</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..5ab8dc03ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-eo/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Malfermi novan langeton de %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Serĉi</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Serĉi en la reto</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Voĉa serĉo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..f68109e762
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir una nueva pestaña de %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Buscar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Buscar en la web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Búsqueda por voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..f68109e762
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir una nueva pestaña de %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Buscar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Buscar en la web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Búsqueda por voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..60b93ee599
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir una pestaña nueva de %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Buscar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Buscar en la web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Búsqueda por voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..3fb6e2091c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir una nueva pestaña en %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Buscar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Buscar en la web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Búsqueda por voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..60b93ee599
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-es/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir una pestaña nueva de %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Buscar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Buscar en la web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Búsqueda por voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..077b8a047c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-et/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Ava uus kaart %1$sis</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Otsing</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Otsi veebist</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Häälotsing</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..9550d75f1f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-eu/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Ireki %1$s fitxa berria</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Bilatu</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Bilatu webean</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Ahots bidezko bilaketa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..9c2489d604
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-fa/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">گشودن یک زبانهٔ جدید %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">جست‌وجو</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">جست‌وجوی وب</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">جست‌وجوی صوتی</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..c22e74b613
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ff/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Uddit tabbere hesre %1$s</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Njiilaw sawto</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..ab054f5e6e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-fi/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Avaa uusi %1$s-välilehti</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Hae</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Hae verkosta</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Äänihaku</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..4785d484bd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-fr/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Ouvrir un nouvel onglet %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Rechercher</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Rechercher sur le Web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Recherche vocale</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..29ec17dd5b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-fur/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Vierç intune gnove schede in %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Cîr</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Cîr tal web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Ricercje vocâl</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..3fd2614b8a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">In nij %1$s-ljepblêd iepenje</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Sykje</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Sykje op it web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Sprutsen sykopdracht</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..9bf121ff5c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-gd/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Fosgail taba %1$s ùr</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Lorg</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Lorg air an lìon</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Lorg-gutha</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..3915de9a04
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-gl/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir unha nova lapela en %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Buscar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Buscar na web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Busca por voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..7dc6496f36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-gn/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Embojuruja tendayke pyahu %1$s-pe</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Heka</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Eheka ñandutípe</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Ayvu rupi jeheka</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..a5a05a0f6a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-hr/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Otvori novu %1$s karticu</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Traži</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Pretraži web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Glasovno pretraživanje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..56a86b500b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Nowy rajtark %1$s wočinić</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Pytać</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Web přepytać</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Hłosowe pytanje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..ec925efad6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-hu/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Új %1$s lap megnyitása</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Keresés</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Keresés a weben</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Hangalapú keresés</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..c069279db8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Բացել նոր %1$s ներդիր</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Որոնում</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Որոնել համացանցում</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Ձայնային որոնում</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..89c47c731e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ia/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Aperir un nove scheda %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Cercar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Cercar in le web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Recerca vocal</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..19650ec7ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-in/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Buka di tab %1$s baru</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Cari</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Cari di Web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Pencarian suara</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..ba5965b35c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-is/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Opna nýjan %1$s-flipa</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Leita</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Leita á vefnum</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Raddleit</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..ccc30fb548
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-it/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Apri una nuova scheda in %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Cerca</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Cerca sul Web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Ricerca vocale</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..cb0f9346ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-iw/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">פתיחת לשונית %1$s חדשה</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">חיפוש</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">חיפוש ברשת</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">חיפוש קולי</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..bc9d0bf5b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ja/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">%1$s の新しいタブで開く</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">検索</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ウェブを検索</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">音声検索</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..e815df0a74
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ka/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">ახალი %1$s-ჩანართის გახსნა</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">მოიძიეთ</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">მოიძიეთ ინტერნეტში</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">ხმოვანი ძიება</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..e968e33430
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Jańa%1$sbetin ashıw</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Izlew</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Internetten izlew</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Dawıslı izlew</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..db660eeff2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-kab/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Ldi-t iccer amaynut %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Nadi</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Nadi di web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Anadi aɣectan</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..bd9f756463
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-kk/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Жаңа %1$s бетін ашу</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Іздеу</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Интернетте іздеу</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Дауыстық іздеу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..6c7ebf5a20
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Hilpekînek nû ya %1$s `ê veke</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Lêgerîn</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Di webê de bigere</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Lêgerîna dengî</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..65c8238a99
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ko/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">새 %1$s 탭 열기</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">검색</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">웹 검색</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">음성 검색</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..119d048839
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-lo/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">ເປີດແຖບ %1$s ໃໝ່</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ຄົ້ນຫາ</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ຄົ້ນຫາເວັບໄຊທ</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">ຄົ້ນຫາດ້ວຍສຽງ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..6387d888d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-lt/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Atverti naują „%1$s“ kortelę</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Ieškoti</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Ieškokite internete</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Paieška balsu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..4b497f553d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-my/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">တပ်ဘ်%1$s အသစ်ဖွင့်မည်</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ရှာဖွေမည်</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ဝက်ဘ်တွင်ရှာမည်</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">အသံဖြင့်ရှာမည်</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..2093f48242
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Åpne en ny %1$s-fane</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Søk</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Søk på nettet</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Stemmesøk</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-night/colors.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000000..50b0875a99
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-night/colors.xml
@@ -0,0 +1,10 @@
+<?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>
+ <color name="mozac_feature_search_widget_background_color">@color/photonDarkGrey60</color>
+ <color name="mozac_feature_search_widget_color">@color/photonLightGrey05</color>
+ <color name="mozac_feature_search_widget_text_color">@color/photonLightGrey05</color>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..dc238c1191
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-nl/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Een nieuw %1$s-tabblad openen</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Zoeken</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Zoeken op het web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Gesproken zoekopdracht</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..dfad13db60
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Opne ei ny %1$s-fane</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Søk</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Søk på nettet</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Stemmesøk</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..9d63b2eb12
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-oc/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Dobrir dins un onglet %1$s novèl</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Recèrca</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Recercar sul web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Recèrca a la votz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..4a35511cdc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">ਨਵੀਂ %1$s ਟੈਬ ਖੋਲ੍ਹੋ</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ਖੋਜ</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ਵੈੱਬ ‘ਤੇ ਖੋਜੋ</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">ਆਵਾਜ਼ ਰਾਹੀਂ ਖੋਜੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..961ba1dcc4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">%1$s نال نویں ٹیب کھولھو</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">کھوج</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ویب دی کھوج</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">آواز دی کھوج</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..eb4e6c3b1c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-pl/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Otwórz nową kartę w przeglądarce %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Szukaj</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Szukaj w Internecie</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Wyszukiwanie głosowe</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..8f34cd8284
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir nova aba no %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Pesquisar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Pesquisar na web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Pesquisa por voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..b8d8f409b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Abrir um novo separador do %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Pesquisar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Pesquisar na Internet</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Pesquisa por voz</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..c4af7423be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-rm/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Avrir in nov tab da %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Tschertgar</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Tschertgar en il web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Tschertga vocala</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..057ba662d6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ru/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Открыть новую вкладку %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Поиск</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Поиск в Интернете</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Голосовой поиск</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..c7d64349f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-sat/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">ᱱᱟᱶᱟ ᱴᱮᱵᱽ %1$s ᱡᱷᱤᱡᱽ ᱢᱮ</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ᱥᱮᱸᱫᱽᱨᱟ</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ᱣᱮᱵᱽ ᱨᱮ ᱥᱮᱸᱫᱽᱨᱟᱭ ᱢᱮ</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">ᱨᱚᱲ ᱥᱮᱸᱫᱽᱨᱟ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..6c458d92a3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-sc/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Aberi in un’ischeda de %1$s noa</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Chirca</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Chirca in sa rete</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Chirca cun sa boghe</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..7a6ddb27e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-si/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">නව %1$s පටිත්තක් අරින්න</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">සොයන්න</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">වියමනහි සොයන්න</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">හඬ සෙවුම</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..8cc11bc103
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-sk/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Otvoriť novú kartu %1$su</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Hľadať</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Vyhľadávanie na webe</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Hlasové vyhľadávanie</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..f2dbedb987
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-skr/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">نویں %1$s ٹیب کھولو</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ڳولو</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ویب ڳولو</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">آواز نال ڳولݨ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..96d3701731
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-sl/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Odpri nov zavihek v %1$su</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Išči</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Iskanje po spletu</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Glasovno iskanje</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..cfe062bbc3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-sq/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Hapni një skedë të re %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Kërko</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Kërkoni në web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Kërkim zanor</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..2cc2c10161
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-sr/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Отвори нови %1$s језичак</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Претражи</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Претражи веб</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Гласовна претрага</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..f180cfabf5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-su/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Buka dina tab %1$s anyar</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Paluruh</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Paluruh raramat</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Paluruh sora</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..9c3f5f1428
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Öppna en ny %1$s-flik</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Sök</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Sök på webben</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Röstsökning</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..d04a02b330
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ta/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">%1$s ஐ புதிய கீற்றில் திற</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">தேடு</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">இணையத்தில் தேடு</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">குரல் தேடல்</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..dae1fcac80
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-te/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">వెతకండి</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">జాలంలో వెతకండి</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..b25dee92ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-tg/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Кушодани варақаи нави %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Ҷустуҷӯ</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Ҷустуҷӯ дар Интернет</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Ҷустуҷӯи овозӣ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..224c92e420
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-th/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">เปิดแท็บ %1$s ใหม่</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ค้นหา</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">ค้นหาเว็บ</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">ค้นหาด้วยเสียง</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..28f70ae9be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-tl/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Buksan ang bagong %1$s tab</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Hanapin</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Hanapin sa web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Paghanap gamit ang boses</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..92c352702a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-tr/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Yeni %1$s sekmesi aç</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Ara</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Web’de ara</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Sesle ara</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..c42a2fe23b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-trs/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Nā\'nïn riña rakïj ñanj nakàa %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Nānà\'huì\'</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Nānà\'huì\' riña web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Nānà\'huì\' ngà nanèt</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..e2b8d1f5b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-tt/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Яңа %1$s табын ачу</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Эзләү</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Интернетта эзләү</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Тавышлы эзләү</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..27df6dae14
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-ug/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">يېڭى%1$sبەتكۈچىنى ئېچىش</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">ئىزدەش</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">توردىن ئىزدەش</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">ئاۋازلىق ئىزدەش</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..b3060b0b0a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-uk/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Відкрити нову вкладку %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Пошук</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Пошук в Інтернеті</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Голосовий пошук</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..9ecebb4caa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-uz/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Yangi %1$s ta varaqni ochish</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Qidirish</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Internetdan qidirish</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Ovozli qidiruv</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..361cbdc149
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-vi/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Mở thẻ %1$s mới</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Tìm kiếm</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Tìm kiếm trên mạng</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Tìm kiếm bằng giọng nói</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..e530cea4a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-yo/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Ṣí táàbù tuntun %1$s</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Ṣàwarí</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Wá lórí wẹ́ẹ̀bù</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Wíwá pẹ̀lú ohùn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..0fd0474f78
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">新建 %1$s 标签页</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">搜索</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">网上搜索</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">语音搜索</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..a4baf4e3d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">開啟新 %1$s 分頁</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">搜尋</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">搜尋 Web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">語音搜尋</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/search/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..07c5961bbe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values/colors.xml
@@ -0,0 +1,9 @@
+<?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>
+ <color name="mozac_feature_search_widget_background_color">@color/photonLightGrey10</color>
+ <color name="mozac_feature_search_widget_text_color">@color/photonDarkGrey90</color>
+ <color name="mozac_feature_search_widget_color">@color/photonDarkGrey90</color>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values/dimens.xml b/mobile/android/android-components/components/feature/search/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..52c1f7545a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+<?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>
+ <dimen name="mozac_tab_corner_radius">8dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/search/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..6f1f2b42e7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/main/res/values/strings.xml
@@ -0,0 +1,16 @@
+<?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>
+ <!-- Search Widget -->
+ <!-- Content description for searching with a widget. The first parameter is the name of the application.-->
+ <string name="search_widget_content_description">Open a new %1$s tab</string>
+ <!-- Text preview for smaller sized widgets -->
+ <string name="search_widget_text_short">Search</string>
+ <!-- Text preview for larger sized widgets -->
+ <string name="search_widget_text_long">Search the web</string>
+ <!-- Content description (not visible, for screen readers etc.): Voice search -->
+ <string name="search_widget_voice">Voice search</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/BrowserStoreSeachAdapterTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/BrowserStoreSeachAdapterTest.kt
new file mode 100644
index 0000000000..37c2f83468
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/BrowserStoreSeachAdapterTest.kt
@@ -0,0 +1,97 @@
+/* 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.search
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+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.search.SearchRequest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+private const val SELECTED_TAB_ID = "1"
+private const val CUSTOM_TAB_ID = "2"
+
+class BrowserStoreSearchAdapterTest {
+
+ private lateinit var browserStore: BrowserStore
+ private val state = BrowserState(
+ tabs = listOf(createTab(id = SELECTED_TAB_ID, url = "https://mozilla.org", private = true)),
+ customTabs = listOf(createCustomTab(id = CUSTOM_TAB_ID, url = "https://firefox.com", source = SessionState.Source.Internal.CustomTab)),
+ selectedTabId = SELECTED_TAB_ID,
+ )
+
+ @Before
+ fun setup() {
+ browserStore = mock()
+ whenever(browserStore.state).thenReturn(state)
+ }
+
+ @Test
+ fun `adapter does nothing with null tab`() {
+ whenever(browserStore.state).thenReturn(BrowserState())
+ val searchAdapter = BrowserStoreSearchAdapter(browserStore)
+
+ searchAdapter.sendSearch(isPrivate = false, text = "normal search")
+ searchAdapter.sendSearch(isPrivate = true, text = "private search")
+
+ verify(browserStore, never()).dispatch(any())
+ assertFalse(searchAdapter.isPrivateSession())
+ }
+
+ @Test
+ fun `sendSearch with selected tab`() {
+ val searchAdapter = BrowserStoreSearchAdapter(browserStore)
+ searchAdapter.sendSearch(isPrivate = false, text = "normal search")
+ verify(browserStore).dispatch(
+ ContentAction.UpdateSearchRequestAction(
+ SELECTED_TAB_ID,
+ SearchRequest(isPrivate = false, query = "normal search"),
+ ),
+ )
+
+ searchAdapter.sendSearch(isPrivate = true, text = "private search")
+ verify(browserStore).dispatch(
+ ContentAction.UpdateSearchRequestAction(
+ SELECTED_TAB_ID,
+ SearchRequest(isPrivate = true, query = "private search"),
+ ),
+ )
+
+ assertTrue(searchAdapter.isPrivateSession())
+ }
+
+ @Test
+ fun `sendSearch with custom tab`() {
+ val searchAdapter = BrowserStoreSearchAdapter(browserStore, CUSTOM_TAB_ID)
+ searchAdapter.sendSearch(isPrivate = false, text = "normal search")
+ verify(browserStore).dispatch(
+ ContentAction.UpdateSearchRequestAction(
+ CUSTOM_TAB_ID,
+ SearchRequest(isPrivate = false, query = "normal search"),
+ ),
+ )
+
+ searchAdapter.sendSearch(isPrivate = true, text = "private search")
+ verify(browserStore).dispatch(
+ ContentAction.UpdateSearchRequestAction(
+ CUSTOM_TAB_ID,
+ SearchRequest(isPrivate = true, query = "private search"),
+ ),
+ )
+
+ assertFalse(searchAdapter.isPrivateSession())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/SearchFeatureTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/SearchFeatureTest.kt
new file mode 100644
index 0000000000..17d6d9bfe3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/SearchFeatureTest.kt
@@ -0,0 +1,134 @@
+/* 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.search
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.search.SearchRequest
+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.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val SELECTED_TAB_ID = "1"
+
+class SearchFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var performSearch: (SearchRequest, String) -> Unit
+ private lateinit var store: BrowserStore
+ private lateinit var searchFeature: SearchFeature
+
+ @Before
+ fun before() {
+ store = BrowserStore(
+ mockBrowserState(),
+ )
+ performSearch = mock()
+ searchFeature = SearchFeature(store, null, performSearch).apply {
+ start()
+ }
+ }
+
+ private fun mockBrowserState(): BrowserState {
+ return BrowserState(
+ tabs = listOf(
+ createTab("https://www.duckduckgo.com", id = "0"),
+ createTab("https://www.mozilla.org", id = SELECTED_TAB_ID),
+ createTab("https://www.wikipedia.org", id = "2"),
+ ),
+ selectedTabId = SELECTED_TAB_ID,
+ )
+ }
+
+ @After
+ fun after() {
+ searchFeature.stop()
+ }
+
+ @Test
+ fun `GIVEN a tab is selected WHEN a search request is sent THEN a search should be performed`() {
+ verify(performSearch, times(0)).invoke(any(), eq(SELECTED_TAB_ID))
+
+ val normalSearchRequest = SearchRequest(isPrivate = false, query = "query")
+ store.dispatch(ContentAction.UpdateSearchRequestAction(SELECTED_TAB_ID, normalSearchRequest)).joinBlocking()
+
+ verify(performSearch, times(1)).invoke(any(), eq(SELECTED_TAB_ID))
+ verify(performSearch, times(1)).invoke(normalSearchRequest, SELECTED_TAB_ID)
+
+ val privateSearchRequest = SearchRequest(isPrivate = true, query = "query")
+ store.dispatch(ContentAction.UpdateSearchRequestAction(SELECTED_TAB_ID, privateSearchRequest)).joinBlocking()
+
+ verify(performSearch, times(2)).invoke(any(), eq(SELECTED_TAB_ID))
+ verify(performSearch, times(1)).invoke(privateSearchRequest, SELECTED_TAB_ID)
+ }
+
+ @Test
+ fun `GIVEN no tab is selected WHEN a search request is sent THEN no search should be performed`() {
+ store.dispatch(TabListAction.RemoveTabAction(tabId = SELECTED_TAB_ID, selectParentIfExists = false))
+
+ verify(performSearch, times(0)).invoke(any(), eq(SELECTED_TAB_ID))
+
+ val normalSearchRequest = SearchRequest(isPrivate = false, query = "query")
+ store.dispatch(ContentAction.UpdateSearchRequestAction(SELECTED_TAB_ID, normalSearchRequest)).joinBlocking()
+
+ verify(performSearch, times(0)).invoke(any(), eq(SELECTED_TAB_ID))
+ verify(performSearch, times(0)).invoke(normalSearchRequest, SELECTED_TAB_ID)
+
+ val privateSearchRequest = SearchRequest(isPrivate = true, query = "query")
+ store.dispatch(ContentAction.UpdateSearchRequestAction(SELECTED_TAB_ID, privateSearchRequest)).joinBlocking()
+
+ verify(performSearch, times(0)).invoke(any(), eq(SELECTED_TAB_ID))
+ verify(performSearch, times(0)).invoke(privateSearchRequest, SELECTED_TAB_ID)
+ }
+
+ @Test
+ fun `WHEN a search request has been handled THEN that request should have been consumed`() {
+ val normalSearchRequest = SearchRequest(isPrivate = false, query = "query")
+ store.dispatch(ContentAction.UpdateSearchRequestAction(SELECTED_TAB_ID, normalSearchRequest)).joinBlocking()
+ store.waitUntilIdle()
+
+ assertNull(store.state.selectedTab!!.content.searchRequest)
+
+ val privateSearchRequest = SearchRequest(isPrivate = true, query = "query")
+ store.dispatch(ContentAction.UpdateSearchRequestAction(SELECTED_TAB_ID, privateSearchRequest)).joinBlocking()
+ store.waitUntilIdle()
+
+ assertNull(store.state.selectedTab!!.content.searchRequest)
+ }
+
+ @Test
+ fun `WHEN the same search is requested two times THEN both search requests are preformed and consumed`() {
+ val searchRequest = SearchRequest(isPrivate = false, query = "query")
+ verify(performSearch, times(0)).invoke(searchRequest, SELECTED_TAB_ID)
+
+ store.dispatch(ContentAction.UpdateSearchRequestAction(SELECTED_TAB_ID, searchRequest)).joinBlocking()
+ store.waitUntilIdle()
+
+ verify(performSearch, times(1)).invoke(searchRequest, SELECTED_TAB_ID)
+ assertNull(store.state.selectedTab!!.content.searchRequest)
+
+ store.dispatch(ContentAction.UpdateSearchRequestAction(SELECTED_TAB_ID, searchRequest)).joinBlocking()
+ store.waitUntilIdle()
+
+ verify(performSearch, times(2)).invoke(searchRequest, SELECTED_TAB_ID)
+ assertNull(store.state.selectedTab!!.content.searchRequest)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt
new file mode 100644
index 0000000000..0168491b0b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt
@@ -0,0 +1,663 @@
+/* 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.search
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.availableSearchEngines
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.searchEngines
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.feature.search.ext.createSearchEngine
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SearchUseCasesTest {
+
+ private lateinit var searchEngine: SearchEngine
+ private lateinit var store: BrowserStore
+ private lateinit var useCases: SearchUseCases
+ private lateinit var tabsUseCases: TabsUseCases
+ private lateinit var sessionUseCases: SessionUseCases
+ private lateinit var loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase
+
+ private val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+
+ private val searchTerms = "mozilla android"
+ private val searchUrl = "https://example.org/?q=mozilla%20android"
+ private val searchEngineName = "Test"
+
+ @Before
+ fun setup() {
+ searchEngine = createSearchEngine(
+ name = searchEngineName,
+ url = "https://example.org/?q={searchTerms}",
+ icon = mock(),
+ )
+
+ tabsUseCases = mock()
+ sessionUseCases = mock()
+ loadUrlUseCase = mock()
+ doReturn(loadUrlUseCase).`when`(sessionUseCases).loadUrl
+
+ store = BrowserStore(
+ initialState = BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(searchEngine),
+ ),
+ ),
+ middleware = listOf(middleware),
+ )
+
+ useCases = SearchUseCases(
+ store,
+ tabsUseCases,
+ sessionUseCases,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ middleware.reset()
+ }
+
+ @Test
+ fun `GIVEN existing Session and Tab WHEN default search invoked THEN expected actions are dispatched`() {
+ val id = "mozilla"
+ store.dispatch(
+ TabListAction.AddTabAction(
+ tab = createTab(url = "https://www.mozilla.org", id = id),
+ select = true,
+ ),
+ ).joinBlocking()
+
+ useCases.defaultSearch(
+ searchTerms = searchTerms,
+ searchEngine = searchEngine,
+ )
+ store.waitUntilIdle()
+
+ val isSearchAction = middleware.findFirstAction(ContentAction.UpdateIsSearchAction::class)
+ assertEquals(id, isSearchAction.sessionId)
+ assertEquals(true, isSearchAction.isSearch)
+ assertEquals(searchEngineName, isSearchAction.searchEngineName)
+
+ middleware.assertLastAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals(id, action.tabId)
+ assertEquals(searchUrl, action.url)
+ }
+ }
+
+ @Test
+ fun `GIVEN existing Session, no existing Tab WHEN default search invoked THEN add tab is called`() {
+ val newTabUseCase: TabsUseCases.AddNewTabUseCase = mock()
+ whenever(tabsUseCases.addTab).thenReturn(newTabUseCase)
+ val newTabId = "9876"
+ whenever(
+ newTabUseCase(
+ url = searchUrl,
+ isSearch = true,
+ searchEngineName = searchEngineName,
+ ),
+ ).thenReturn(newTabId)
+
+ useCases.defaultSearch(
+ searchTerms = searchTerms,
+ sessionId = "mozilla",
+ searchEngine = searchEngine,
+ )
+ store.waitUntilIdle()
+
+ verify(newTabUseCase).invoke(
+ url = searchUrl,
+ isSearch = true,
+ searchEngineName = searchEngineName,
+ )
+
+ middleware.assertLastAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals(newTabId, action.sessionId)
+ assertEquals(searchTerms, action.searchTerms)
+ }
+ }
+
+ @Test
+ fun defaultSearchOnNewSession() {
+ val searchTerms = "mozilla android"
+
+ val newTabUseCase: TabsUseCases.AddNewTabUseCase = mock()
+ whenever(tabsUseCases.addTab).thenReturn(newTabUseCase)
+ whenever(newTabUseCase(searchUrl, isSearch = true)).thenReturn("2342")
+
+ useCases.newTabSearch(searchTerms, SessionState.Source.Internal.NewTab)
+ store.waitUntilIdle()
+
+ verify(newTabUseCase).invoke(
+ searchUrl,
+ parentId = null,
+ selectTab = true,
+ source = SessionState.Source.Internal.NewTab,
+ isSearch = true,
+ )
+
+ val searchTermsAction = middleware.findFirstAction(ContentAction.UpdateSearchTermsAction::class)
+ assertEquals("2342", searchTermsAction.sessionId)
+ assertEquals(searchTerms, searchTermsAction.searchTerms)
+ }
+
+ @Test
+ fun `GIVEN additional headers and a load url flag WHEN NewTabSearchUseCase creates a new tab THEN addTab is called`() {
+ val source = SessionState.Source.Internal.UserEntered
+ val flags = LoadUrlFlags.select(LoadUrlFlags.ALLOW_JAVASCRIPT_URL)
+ val additionalHeaders = mapOf("X-Extra-Header" to "true")
+ val sessionId = "2342"
+
+ val newTabUseCase: TabsUseCases.AddNewTabUseCase = mock()
+ whenever(tabsUseCases.addTab).thenReturn(newTabUseCase)
+ whenever(
+ newTabUseCase(
+ url = searchUrl,
+ isSearch = true,
+ flags = flags,
+ source = source,
+ additionalHeaders = additionalHeaders,
+ ),
+ ).thenReturn(sessionId)
+
+ useCases.newTabSearch(
+ searchTerms = searchTerms,
+ source = source,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ )
+ store.waitUntilIdle()
+
+ verify(newTabUseCase).invoke(
+ url = searchUrl,
+ flags = flags,
+ source = source,
+ isSearch = true,
+ additionalHeaders = additionalHeaders,
+ )
+
+ val searchTermsAction =
+ middleware.findFirstAction(ContentAction.UpdateSearchTermsAction::class)
+ assertEquals(sessionId, searchTermsAction.sessionId)
+ assertEquals(searchTerms, searchTermsAction.searchTerms)
+ }
+
+ @Test
+ fun `DefaultSearchUseCase creates new tab if no session is selected`() {
+ val newTabUseCase: TabsUseCases.AddNewTabUseCase = mock()
+ whenever(tabsUseCases.addTab).thenReturn(newTabUseCase)
+ whenever(newTabUseCase(searchUrl, isSearch = true)).thenReturn("2342")
+
+ useCases.defaultSearch(searchTerms)
+ store.waitUntilIdle()
+
+ verify(newTabUseCase).invoke(
+ searchUrl,
+ parentId = null,
+ selectTab = true,
+ source = SessionState.Source.Internal.NewTab,
+ isSearch = true,
+ )
+
+ val searchTermsAction = middleware.findFirstAction(ContentAction.UpdateSearchTermsAction::class)
+ assertEquals("2342", searchTermsAction.sessionId)
+ assertEquals(searchTerms, searchTermsAction.searchTerms)
+ }
+
+ @Test
+ fun `GIVEN additional headers and a load url flag WHEN DefaultSearchUseCase creates new tab THEN addTab is called`() {
+ val flags = LoadUrlFlags.select(LoadUrlFlags.ALLOW_JAVASCRIPT_URL)
+ val additionalHeaders = mapOf("X-Extra-Header" to "true")
+ val sessionId = "2342"
+
+ val newTabUseCase: TabsUseCases.AddNewTabUseCase = mock()
+ whenever(tabsUseCases.addTab).thenReturn(newTabUseCase)
+ whenever(
+ newTabUseCase(
+ url = searchUrl,
+ flags = flags,
+ isSearch = true,
+ searchEngineName = searchEngineName,
+ additionalHeaders = additionalHeaders,
+ ),
+ ).thenReturn(sessionId)
+
+ useCases.defaultSearch(
+ searchTerms = searchTerms,
+ searchEngine = searchEngine,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ )
+ store.waitUntilIdle()
+
+ verify(newTabUseCase).invoke(
+ url = searchUrl,
+ flags = flags,
+ isSearch = true,
+ searchEngineName = searchEngineName,
+ additionalHeaders = additionalHeaders,
+ )
+
+ val searchTermsAction = middleware.findFirstAction(ContentAction.UpdateSearchTermsAction::class)
+ assertEquals(sessionId, searchTermsAction.sessionId)
+ assertEquals(searchTerms, searchTermsAction.searchTerms)
+ }
+
+ @Test
+ fun newPrivateTabSearch() {
+ val newTabUseCase: TabsUseCases.AddNewTabUseCase = mock()
+ whenever(tabsUseCases.addTab).thenReturn(newTabUseCase)
+ whenever(
+ newTabUseCase(
+ searchUrl,
+ source = SessionState.Source.Internal.None,
+ private = true,
+ isSearch = true,
+ ),
+ ).thenReturn("1177")
+
+ useCases.newPrivateTabSearch.invoke(searchTerms)
+ store.waitUntilIdle()
+
+ verify(newTabUseCase).invoke(
+ searchUrl,
+ parentId = null,
+ selectTab = true,
+ private = true,
+ source = SessionState.Source.Internal.None,
+ isSearch = true,
+ )
+
+ val searchTermsAction = middleware.findFirstAction(ContentAction.UpdateSearchTermsAction::class)
+ assertEquals("1177", searchTermsAction.sessionId)
+ assertEquals(searchTerms, searchTermsAction.searchTerms)
+ }
+
+ @Test
+ fun newPrivateTabSearchWithParentSession() {
+ val newTabUseCase: TabsUseCases.AddNewTabUseCase = mock()
+ whenever(tabsUseCases.addTab).thenReturn(newTabUseCase)
+ whenever(
+ newTabUseCase(
+ searchUrl,
+ source = SessionState.Source.Internal.None,
+ parentId = "test-parent",
+ private = true,
+ isSearch = true,
+ ),
+ ).thenReturn("1177")
+
+ useCases.newPrivateTabSearch.invoke(searchTerms, parentSessionId = "test-parent")
+
+ store.waitUntilIdle()
+
+ verify(newTabUseCase).invoke(
+ searchUrl,
+ parentId = "test-parent",
+ selectTab = true,
+ private = true,
+ source = SessionState.Source.Internal.None,
+ isSearch = true,
+ )
+
+ val searchTermsAction = middleware.findFirstAction(ContentAction.UpdateSearchTermsAction::class)
+ assertEquals("1177", searchTermsAction.sessionId)
+ assertEquals(searchTerms, searchTermsAction.searchTerms)
+ }
+
+ @Test
+ fun `Selecting search engine`() {
+ val store = BrowserStore(getBrowserState())
+
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ useCases.selectSearchEngine.invoke(
+ store.findSearchEngineById("engine-d"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals("engine-d", store.state.search.userSelectedSearchEngineId)
+ assertNull(store.state.search.userSelectedSearchEngineName)
+
+ useCases.selectSearchEngine.invoke(
+ store.findSearchEngineById("engine-b"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals("engine-b", store.state.search.userSelectedSearchEngineId)
+ assertEquals("Engine B", store.state.search.userSelectedSearchEngineName)
+
+ useCases.selectSearchEngine.invoke(
+ store.findSearchEngineById("engine-f"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals("engine-f", store.state.search.userSelectedSearchEngineId)
+ assertNull(store.state.search.userSelectedSearchEngineName)
+ }
+
+ @Test
+ fun `addSearchEngine - add bundled engine`() {
+ val store = BrowserStore(getBrowserState())
+
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(3, store.state.search.availableSearchEngines.size)
+
+ useCases.addSearchEngine.invoke(
+ store.findSearchEngineById("engine-i"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(8, store.state.search.searchEngines.size)
+ assertEquals(2, store.state.search.availableSearchEngines.size)
+
+ assertEquals(4, store.state.search.regionSearchEngines.size)
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+
+ assertEquals("engine-i", store.state.search.regionSearchEngines[3].id)
+ assertEquals("Engine I", store.state.search.regionSearchEngines[3].name)
+ }
+
+ @Test
+ fun `addSearchEngine - add additional bundled engine`() {
+ val store = BrowserStore(getBrowserState())
+
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(3, store.state.search.availableSearchEngines.size)
+
+ useCases.addSearchEngine.invoke(
+ store.findSearchEngineById("engine-h"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(8, store.state.search.searchEngines.size)
+ assertEquals(2, store.state.search.availableSearchEngines.size)
+
+ assertEquals(1, store.state.search.additionalAvailableSearchEngines.size)
+ assertEquals(2, store.state.search.additionalSearchEngines.size)
+
+ assertEquals("engine-h", store.state.search.additionalSearchEngines[1].id)
+ assertEquals("Engine H", store.state.search.additionalSearchEngines[1].name)
+ }
+
+ @Test
+ fun `addSearchEngine - add custom engine`() {
+ val store = BrowserStore(getBrowserState())
+
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(3, store.state.search.availableSearchEngines.size)
+
+ useCases.addSearchEngine.invoke(
+ createSearchEngine(
+ name = "Engine X",
+ url = "https://www.example.org/?q={searchTerms}",
+ icon = mock(),
+ ),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(8, store.state.search.searchEngines.size)
+ assertEquals(3, store.state.search.availableSearchEngines.size)
+
+ assertEquals(3, store.state.search.customSearchEngines.size)
+ assertEquals("Engine X", store.state.search.customSearchEngines[2].name)
+ assertEquals(
+ "https://www.example.org/?q={searchTerms}",
+ store.state.search.customSearchEngines[2].resultUrls[0],
+ )
+ }
+
+ @Test
+ fun `removeSearchEngine - remove bundled engine`() {
+ val store = BrowserStore(getBrowserState())
+
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(3, store.state.search.availableSearchEngines.size)
+
+ useCases.removeSearchEngine.invoke(
+ store.findSearchEngineById("engine-b"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(4, store.state.search.availableSearchEngines.size)
+
+ assertEquals(2, store.state.search.regionSearchEngines.size)
+ assertEquals(2, store.state.search.hiddenSearchEngines.size)
+
+ assertEquals("engine-b", store.state.search.hiddenSearchEngines[1].id)
+ assertEquals("Engine B", store.state.search.hiddenSearchEngines[1].name)
+ }
+
+ @Test
+ fun `removeSearchEngine - remove additional bundled engine`() {
+ val store = BrowserStore(getBrowserState())
+
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(3, store.state.search.availableSearchEngines.size)
+
+ useCases.removeSearchEngine.invoke(
+ store.findSearchEngineById("engine-f"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(4, store.state.search.availableSearchEngines.size)
+
+ assertEquals(0, store.state.search.additionalSearchEngines.size)
+ assertEquals(3, store.state.search.additionalAvailableSearchEngines.size)
+
+ assertEquals("engine-f", store.state.search.additionalAvailableSearchEngines[2].id)
+ assertEquals("Engine F", store.state.search.additionalAvailableSearchEngines[2].name)
+ }
+
+ @Test
+ fun `removeSearchEngine - remove custom engine`() {
+ val store = BrowserStore(getBrowserState())
+
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(3, store.state.search.availableSearchEngines.size)
+
+ useCases.removeSearchEngine.invoke(
+ store.findSearchEngineById("engine-d"),
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(3, store.state.search.availableSearchEngines.size)
+
+ assertEquals(1, store.state.search.customSearchEngines.size)
+ }
+
+ @Test
+ fun `GIVEN disable search engine use case is invoked WHEN engine gets unselected THEN ID is stored in search state`() {
+ val store = BrowserStore(getBrowserState())
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(0, store.state.search.disabledSearchEngineIds.size)
+
+ useCases.updateDisabledSearchEngineIds.invoke(
+ searchEngineId = "engine-d",
+ isEnabled = false,
+ )
+ store.waitUntilIdle()
+
+ assertEquals(1, store.state.search.disabledSearchEngineIds.size)
+ }
+
+ @Test
+ fun `GIVEN disable search engine use case is invoked WHEN engine gets selected THEN ID is removed from search state`() {
+ val store = BrowserStore(getBrowserState(disabledSearchEngineIds = listOf("engine-d")))
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(1, store.state.search.disabledSearchEngineIds.size)
+
+ useCases.updateDisabledSearchEngineIds.invoke(
+ searchEngineId = "engine-d",
+ isEnabled = true,
+ )
+ store.waitUntilIdle()
+
+ assertEquals(0, store.state.search.disabledSearchEngineIds.size)
+ }
+
+ @Test
+ fun `WHEN restore search engines use case is invoked GIVEN there are hidden engines THEN hidden engines are added back to the bundled engine list`() {
+ val regionSearchEngines = listOf(
+ SearchEngine("bundled-engine-a", "Regional Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("bundled-engine-b", "Regional Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ )
+
+ val hiddenEngine = SearchEngine(
+ "bundled-engine-c",
+ "Regional Engine C",
+ mock(),
+ type = SearchEngine.Type.BUNDLED,
+ )
+
+ val store = BrowserStore(getBrowserState(hiddenSearchEngine = listOf(hiddenEngine), regionSearchEngines = regionSearchEngines))
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(2, store.state.search.regionSearchEngines.size)
+ assertEquals(1, store.state.search.hiddenSearchEngines.size)
+
+ assertEquals("bundled-engine-a", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bundled-engine-b", store.state.search.regionSearchEngines[1].id)
+ assertEquals("bundled-engine-c", store.state.search.hiddenSearchEngines[0].id)
+
+ useCases.restoreHiddenSearchEngines.invoke()
+ store.waitUntilIdle()
+
+ assertEquals(3, store.state.search.regionSearchEngines.size)
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+
+ assertEquals("bundled-engine-a", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bundled-engine-b", store.state.search.regionSearchEngines[1].id)
+ assertEquals("bundled-engine-c", store.state.search.regionSearchEngines[2].id)
+ }
+
+ @Test
+ fun `WHEN restore search engines use case is invoked GIVEN there are no hidden engines THEN do nothing`() {
+ val regionSearchEngines = listOf(
+ SearchEngine("bundled-engine-a", "Regional Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("bundled-engine-b", "Regional Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("bundled-engine-c", "Regional Engine C", mock(), type = SearchEngine.Type.BUNDLED),
+ )
+ val store = BrowserStore(getBrowserState(hiddenSearchEngine = emptyList(), regionSearchEngines = regionSearchEngines))
+ val useCases = SearchUseCases(store, mock(), mock())
+
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+ assertEquals(3, store.state.search.regionSearchEngines.size)
+
+ assertEquals("bundled-engine-a", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bundled-engine-b", store.state.search.regionSearchEngines[1].id)
+ assertEquals("bundled-engine-c", store.state.search.regionSearchEngines[2].id)
+
+ useCases.restoreHiddenSearchEngines.invoke()
+ store.waitUntilIdle()
+
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+ assertEquals(3, store.state.search.regionSearchEngines.size)
+
+ assertEquals("bundled-engine-a", store.state.search.regionSearchEngines[0].id)
+ assertEquals("bundled-engine-b", store.state.search.regionSearchEngines[1].id)
+ assertEquals("bundled-engine-c", store.state.search.regionSearchEngines[2].id)
+ }
+}
+
+private fun getBrowserState(
+ disabledSearchEngineIds: List<String> = emptyList(),
+ regionSearchEngines: List<SearchEngine> = listOf(
+ SearchEngine("engine-a", "Engine A", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-b", "Engine B", mock(), type = SearchEngine.Type.BUNDLED),
+ SearchEngine("engine-c", "Engine C", mock(), type = SearchEngine.Type.BUNDLED),
+ ),
+ hiddenSearchEngine: List<SearchEngine> = listOf(
+ SearchEngine(
+ "engine-i",
+ "Engine I",
+ mock(),
+ type = SearchEngine.Type.BUNDLED,
+ ),
+ ),
+) = BrowserState(
+ search = SearchState(
+ region = RegionState("US", "US"),
+ regionSearchEngines = regionSearchEngines,
+ customSearchEngines = listOf(
+ SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
+ SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM),
+ ),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
+ additionalSearchEngines = listOf(
+ SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ additionalAvailableSearchEngines = listOf(
+ SearchEngine("engine-g", "Engine G", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ SearchEngine("engine-h", "Engine H", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL),
+ ),
+ hiddenSearchEngines = hiddenSearchEngine,
+ disabledSearchEngineIds = disabledSearchEngineIds,
+ regionDefaultSearchEngineId = "engine-b",
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ ),
+)
+
+private fun BrowserStore.findSearchEngineById(id: String): SearchEngine {
+ val searchEngine = (state.search.searchEngines + state.search.availableSearchEngines).find {
+ it.id == id
+ }
+ return requireNotNull(searchEngine)
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/ext/BrowserStoreKtTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/ext/BrowserStoreKtTest.kt
new file mode 100644
index 0000000000..c29082e0e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/ext/BrowserStoreKtTest.kt
@@ -0,0 +1,117 @@
+/* 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.search.ext
+
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+class BrowserStoreKtTest {
+ @Test
+ fun `waitForDefaultSearchEngine - with state already loaded`() {
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(
+ SearchEngine(
+ id = "google",
+ name = "Google",
+ icon = mock(),
+ type = SearchEngine.Type.BUNDLED,
+ ),
+ ),
+ userSelectedSearchEngineId = "google",
+ complete = true,
+ ),
+ ),
+ )
+
+ val latch = CountDownLatch(1)
+
+ store.waitForSelectedOrDefaultSearchEngine { searchEngine ->
+ assertNotNull(searchEngine)
+ assertEquals("google", searchEngine!!.id)
+ latch.countDown()
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS))
+ }
+
+ @Test
+ fun `waitForDefaultSearchEngine - with state dispatched later`() {
+ val store = BrowserStore()
+
+ val latch = CountDownLatch(1)
+
+ store.waitForSelectedOrDefaultSearchEngine { searchEngine ->
+ assertNotNull(searchEngine)
+ assertEquals("google", searchEngine!!.id)
+ latch.countDown()
+ }
+
+ store.dispatch(
+ SearchAction.SetSearchEnginesAction(
+ regionSearchEngines = listOf(
+ SearchEngine(
+ id = "google",
+ name = "Google",
+ icon = mock(),
+ type = SearchEngine.Type.BUNDLED,
+ ),
+ ),
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ regionDefaultSearchEngineId = "google",
+ customSearchEngines = emptyList(),
+ hiddenSearchEngines = emptyList(),
+ disabledSearchEngineIds = emptyList(),
+ additionalAvailableSearchEngines = emptyList(),
+ additionalSearchEngines = emptyList(),
+ regionSearchEnginesOrder = listOf("google"),
+ ),
+ )
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS))
+ }
+
+ @Test
+ fun `waitForDefaultSearchEngine - no default was loaded`() {
+ val store = BrowserStore()
+
+ val latch = CountDownLatch(1)
+
+ store.waitForSelectedOrDefaultSearchEngine { searchEngine ->
+ assertNull(searchEngine)
+ latch.countDown()
+ }
+
+ store.dispatch(
+ SearchAction.SetSearchEnginesAction(
+ regionSearchEngines = listOf(),
+ userSelectedSearchEngineId = null,
+ userSelectedSearchEngineName = null,
+ regionDefaultSearchEngineId = "default",
+ customSearchEngines = emptyList(),
+ hiddenSearchEngines = emptyList(),
+ disabledSearchEngineIds = emptyList(),
+ additionalAvailableSearchEngines = emptyList(),
+ additionalSearchEngines = emptyList(),
+ regionSearchEnginesOrder = listOf("google"),
+ ),
+ )
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/ext/SearchEngineKtTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/ext/SearchEngineKtTest.kt
new file mode 100644
index 0000000000..6c117132ff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/ext/SearchEngineKtTest.kt
@@ -0,0 +1,196 @@
+/* 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.search.ext
+
+import android.graphics.Bitmap
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.UUID
+
+@RunWith(AndroidJUnit4::class)
+class SearchEngineKtTest {
+
+ @Test
+ fun `WHEN search engine is created THEN the correct properties are set`() {
+ val name = "name"
+ val url = "https://www.example.com/search?q={searchTerms}"
+ val icon: Bitmap = mock()
+ val suggestUrl = "https://www.example.com/search"
+ val isGeneral = true
+ val searchEngine = createSearchEngine(
+ name = name,
+ url = url,
+ icon = icon,
+ suggestUrl = suggestUrl,
+ isGeneral = isGeneral,
+ )
+
+ assertNotNull(searchEngine.id)
+ assertEquals(name, searchEngine.name)
+ assertEquals(icon, searchEngine.icon)
+ assertEquals(SearchEngine.Type.CUSTOM, searchEngine.type)
+ assertEquals(listOf(url), searchEngine.resultUrls)
+ assertEquals(suggestUrl, searchEngine.suggestUrl)
+ assertEquals(isGeneral, searchEngine.isGeneral)
+ }
+
+ @Test
+ fun `Create search URL for startpage`() {
+ val searchEngine = SearchEngine(
+ id = UUID.randomUUID().toString(),
+ name = "Escosia",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf(
+ "https://www.startpage.com/sp/search?q={searchTerms}",
+ ),
+ )
+
+ assertEquals(
+ "https://www.startpage.com/sp/search?q=Hello%20World",
+ searchEngine.buildSearchUrl("Hello World"),
+ )
+ }
+
+ @Test
+ fun `Create search URL for ecosia`() {
+ val searchEngine = createSearchEngine(
+ name = "Ecosia",
+ icon = mock(),
+ url = "https://www.ecosia.org/search?q={searchTerms}",
+ )
+
+ assertEquals(
+ "https://www.ecosia.org/search?q=Hello%20World",
+ searchEngine.buildSearchUrl("Hello World"),
+ )
+ }
+
+ @Test
+ fun `GIVEN ecosia search engine and a set of urls THEN search terms are determined when present`() {
+ val searchEngine = createSearchEngine(
+ name = "Ecosia",
+ icon = mock(),
+ url = "https://www.ecosia.org/search?q={searchTerms}",
+ )
+
+ assertNull(searchEngine.parseSearchTerms(Uri.parse("https://www.ecosia.org/search?q=")))
+ assertNull(searchEngine.parseSearchTerms(Uri.parse("https://www.ecosia.org/search?attr=moz-test")))
+
+ assertEquals(
+ "second test search",
+ searchEngine.parseSearchTerms(Uri.parse("https://www.ecosia.org/search?q=second%20test%20search")),
+ )
+
+ assertEquals(
+ "Another test",
+ searchEngine.parseSearchTerms(Uri.parse("https://www.ecosia.org/search?r=134s7&attr=moz-test&q=Another%20test&d=136697676793")),
+ )
+ }
+
+ @Test
+ fun `GIVEN empty search state THEN search terms are never determined`() {
+ val searchState = SearchState()
+ assertNull(searchState.parseSearchTerms("https://google.com/search/?q=the%20sandbaggers"))
+ }
+
+ @Test
+ fun `GIVEN a search state and a set of urls THEN search terms are determined when present`() {
+ val google = createSearchEngine(
+ name = "Google",
+ icon = mock(),
+ url = "https://google.com/search/?q={searchTerms}",
+ )
+ val ecosia = createSearchEngine(
+ name = "Ecosia",
+ icon = mock(),
+ url = "https://www.ecosia.org/search?q={searchTerms}",
+ )
+ val baidu = createSearchEngine(
+ name = "Baidu",
+ icon = mock(),
+ url = "https://www.baidu.com/s?wd={searchTerms}",
+ )
+ val searchState = SearchState(
+ regionSearchEngines = listOf(google, baidu),
+ additionalSearchEngines = listOf(ecosia),
+ customSearchEngines = listOf(baidu, ecosia),
+ )
+
+ assertNull(searchState.parseSearchTerms("https://www.ecosia.org/search?q="))
+ assertNull(searchState.parseSearchTerms("http://help.baidu.com/"))
+ assertEquals(
+ "神舟十二号载人飞行任务标识发布",
+ searchState.parseSearchTerms("https://www.baidu.com/s?cl=3&tn=baidutop10&fr=top1000&wd=%E7%A5%9E%E8%88%9F%E5%8D%81%E4%BA%8C%E5%8F%B7%E8%BD%BD%E4%BA%BA%E9%A3%9E%E8%A1%8C%E4%BB%BB%E5%8A%A1%E6%A0%87%E8%AF%86%E5%8F%91%E5%B8%83&rsv_idx=2&rsv_dl=fyb_n_homepage&hisfilter=1"),
+ )
+ assertEquals(
+ "the sandbaggers",
+ searchState.parseSearchTerms("https://google.com/search/?q=the%20sandbaggers"),
+ )
+ assertEquals(
+ "фаерфокс",
+ searchState.parseSearchTerms("https://google.com/search/?q=%D1%84%D0%B0%D0%B5%D1%80%D1%84%D0%BE%D0%BA%D1%81"),
+ )
+ assertEquals(
+ "Another test",
+ searchState.parseSearchTerms("https://www.ecosia.org/search?r=134s7&attr=moz-test&q=Another%20test&d=136697676793"),
+ )
+ }
+
+ @Test
+ fun `GIVEN search engine parameter can not be found THEN search terms are never determined`() {
+ val invalidEngine = SearchEngine(
+ id = UUID.randomUUID().toString(),
+ name = "invalid",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://mozilla.org/search/?q={invalid}"),
+ )
+
+ val searchState = SearchState(
+ regionSearchEngines = listOf(invalidEngine),
+ )
+
+ assertNull(searchState.parseSearchTerms("https://mozilla.org/search/?q=test"))
+ }
+
+ @Test
+ fun `GIVEN a search state and a set of input encoding THEN search terms are encoded by input encoding parameter`() {
+ val searchEngine = createSearchEngine(
+ name = "Yahoo! Auctions",
+ icon = mock(),
+ url = "https://auctions.yahoo.co.jp/search/search&p={searchTerms}",
+ inputEncoding = "EUC-JP",
+ )
+
+ assertEquals(
+ "https://auctions.yahoo.co.jp/search/search&p=%A5%D5%A5%A1%A5%A4%A5%E4%A1%BC%A5%D5%A5%A9%A5%C3%A5%AF%A5%B9",
+ searchEngine.buildSearchUrl("ファイヤーフォックス"),
+ )
+ }
+
+ @Test
+ fun `GIVEN invalid input encoding THEN encoding of search terms are determined as UTF-8`() {
+ val searchEngine = createSearchEngine(
+ name = "name",
+ icon = mock(),
+ url = "https://www.example.com/search?q={searchTerms}",
+ inputEncoding = "INVALID-ENOCODING",
+ )
+
+ assertEquals(
+ "https://www.example.com/search?q=%E7%81%AB%E7%8B%90",
+ searchEngine.buildSearchUrl("火狐"),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddlewareTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddlewareTest.kt
new file mode 100644
index 0000000000..b144facd8e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddlewareTest.kt
@@ -0,0 +1,126 @@
+/* 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.search.middleware
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.LoadRequestState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class AdsTelemetryMiddlewareTest {
+ val sessionId = "session"
+ lateinit var adsMiddleware: AdsTelemetryMiddleware
+ lateinit var browserState: BrowserState
+
+ @Before
+ fun setup() {
+ adsMiddleware = AdsTelemetryMiddleware(mock())
+ browserState = BrowserState(
+ tabs = listOf(TabSessionState(content = ContentState("https://mozilla.org"), id = sessionId)),
+ )
+ }
+
+ @Test
+ fun `GIVEN redirectChain empty WHEN a new URL loads THEN the redirectChain starts from the current tab url`() {
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(adsMiddleware),
+ )
+
+ store.dispatch(
+ ContentAction.UpdateLoadRequestAction(
+ sessionId,
+ LoadRequestState(
+ url = "https://mozilla.org/firefox",
+ triggeredByRedirect = false,
+ triggeredByUser = false,
+ ),
+ ),
+ ).joinBlocking()
+
+ assertEquals(1, adsMiddleware.redirectChain.size)
+ assertEquals("https://mozilla.org", adsMiddleware.redirectChain[sessionId]!!.root)
+ }
+
+ @Test
+ fun `GIVEN redirectChain is not empty WHEN a new URL loads THEN that URL is added to the chain`() {
+ adsMiddleware.redirectChain[sessionId] = RedirectChain("https://mozilla.org")
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(adsMiddleware),
+ )
+
+ store.dispatch(
+ ContentAction.UpdateLoadRequestAction(
+ sessionId,
+ LoadRequestState(
+ url = "https://mozilla.org/firefox",
+ triggeredByRedirect = false,
+ triggeredByUser = false,
+ ),
+ ),
+ ).joinBlocking()
+
+ assertEquals(1, adsMiddleware.redirectChain.size)
+ assertEquals("https://mozilla.org", adsMiddleware.redirectChain[sessionId]!!.root)
+ assertEquals(1, adsMiddleware.redirectChain.size)
+ assertEquals("https://mozilla.org/firefox", adsMiddleware.redirectChain[sessionId]!!.chain[0])
+ }
+
+ @Test
+ fun `WHEN the session URL is updated THEN check if an ad was clicked`() {
+ val adsTelemetry: AdsTelemetry = mock()
+ val adsMiddleware = AdsTelemetryMiddleware(adsTelemetry)
+ adsMiddleware.redirectChain[sessionId] = RedirectChain("https://mozilla.org")
+ adsMiddleware.redirectChain[sessionId]!!.chain.add("https://mozilla.org/firefox")
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(adsMiddleware),
+ )
+
+ store
+ .dispatch(ContentAction.UpdateUrlAction(sessionId, "https://mozilla.org/firefox"))
+ .joinBlocking()
+
+ verify(adsTelemetry).checkIfAddWasClicked(
+ "https://mozilla.org",
+ listOf("https://mozilla.org/firefox"),
+ )
+ }
+
+ @Test
+ fun `GIVEN a location update WHEN ads telemetry is recorded THEN redirect chain is reset`() {
+ val tab = createTab(id = "1", url = "http://mozilla.org")
+ val store = BrowserStore(
+ initialState = browserState,
+ middleware = listOf(adsMiddleware),
+ )
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ store.dispatch(
+ ContentAction.UpdateLoadRequestAction(
+ tab.id,
+ LoadRequestState("https://mozilla.org", true, true),
+ ),
+ ).joinBlocking()
+
+ assertNotNull(adsMiddleware.redirectChain[tab.id])
+
+ store.dispatch(ContentAction.UpdateUrlAction(tab.id, "https://mozilla.org")).joinBlocking()
+ assertNull(adsMiddleware.redirectChain[tab.id])
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/SearchMiddlewareTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/SearchMiddlewareTest.kt
new file mode 100644
index 0000000000..a93a6e1700
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/SearchMiddlewareTest.kt
@@ -0,0 +1,1921 @@
+/* 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.search.middleware
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.TestDispatcher
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.availableSearchEngines
+import mozilla.components.browser.state.state.searchEngines
+import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.search.ext.createSearchEngine
+import mozilla.components.feature.search.storage.CustomSearchEngineStorage
+import mozilla.components.feature.search.storage.SearchMetadataStorage
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.fakes.android.FakeSharedPreferences
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+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.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.util.Locale
+import java.util.UUID
+
+@RunWith(AndroidJUnit4::class)
+class SearchMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ private lateinit var originalLocale: Locale
+
+ @Before
+ fun setUp() {
+ originalLocale = Locale.getDefault()
+ }
+
+ @After
+ fun tearDown() {
+ if (Locale.getDefault() != originalLocale) {
+ Locale.setDefault(originalLocale)
+ }
+ }
+
+ @Test
+ fun `Loads search engines for locale (US)`() {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertNotNull(store.state.search.regionSearchEngines.find { it.name == "Google" })
+ assertNull(store.state.search.regionSearchEngines.find { it.name == "Yandex" })
+ }
+
+ @Test
+ fun `WHEN distribution doesn't exist THEN Loads default search engines`() {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ "test",
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertNotNull(store.state.search.regionSearchEngines.find { it.name == "Google" })
+ assertNull(store.state.search.regionSearchEngines.find { it.name == "Yandex" })
+ }
+
+ fun `Loads search engines for locale (An)`() {
+ Locale.setDefault(Locale("an", "AN"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("AN", "AN"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(5, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[2].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipedia", store.state.search.regionSearchEngines[4].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (CA)`() {
+ Locale.setDefault(Locale("CA", "CA"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("CA", "CA"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(5, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[2].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Viquipèdia (ca)", store.state.search.regionSearchEngines[4].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (CY)`() {
+ Locale.setDefault(Locale("cy", "CY"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("CY", "CY"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.co.uk", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Wicipedia (cy)", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (fy-NL)`() {
+ Locale.setDefault(Locale("fy", "NL"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("FY", "NL"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(5, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[2].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipedia (fy)", store.state.search.regionSearchEngines[4].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (en-AU)`() {
+ Locale.setDefault(Locale("en", "AU"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("EN", "AU"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.com.au", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipedia", store.state.search.regionSearchEngines[4].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (en-GB)`() {
+ Locale.setDefault(Locale("en", "GB"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("EN", "GB"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(7, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.co.uk", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Qwant", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Wikipedia", store.state.search.regionSearchEngines[5].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[6].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (en-IE)`() {
+ Locale.setDefault(Locale("en", "IE"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("EN", "IE"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(7, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.co.uk", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Qwant", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Wikipedia", store.state.search.regionSearchEngines[5].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[6].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (fr-BE)`() {
+ Locale.setDefault(Locale("fr", "BE"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("FR", "BE"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[2].name)
+ assertEquals("Qwant", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipédia (fr)", store.state.search.regionSearchEngines[4].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (fr-CA)`() {
+ Locale.setDefault(Locale("fr", "CA"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("FR", "CA"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.ca", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipédia (fr)", store.state.search.regionSearchEngines[4].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (fr-FR)`() {
+ Locale.setDefault(Locale("fr", "FR"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("FR", "FR"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(7, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[2].name)
+ assertEquals("Qwant", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipédia (fr)", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Amazon.fr", store.state.search.regionSearchEngines[5].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[6].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (de-AT)`() {
+ Locale.setDefault(Locale("de", "AT"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("DE", "AT"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(8, store.state.search.regionSearchEngines.size)
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.de", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Ecosia", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Qwant", store.state.search.regionSearchEngines[5].name)
+ assertEquals("Wikipedia (de)", store.state.search.regionSearchEngines[6].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[7].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (DE)`() {
+ Locale.setDefault(Locale("de", "DE"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("DE", "DE"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(8, store.state.search.regionSearchEngines.size)
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.de", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Ecosia", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Qwant", store.state.search.regionSearchEngines[5].name)
+ assertEquals("Wikipedia (de)", store.state.search.regionSearchEngines[6].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[7].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (DSB)`() {
+ Locale.setDefault(Locale("dsb", "DE"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("DSB", "DE"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.de", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipedija (dsb)", store.state.search.regionSearchEngines[4].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (HSB)`() {
+ Locale.setDefault(Locale("hsb", "DE"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("HSB", "DE"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.de", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipedija (hsb)", store.state.search.regionSearchEngines[4].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (ES)`() {
+ Locale.setDefault(Locale("es", "ES"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("ES", "ES"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[2].name)
+ assertEquals("Wikipedia (es)", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Amazon.es", store.state.search.regionSearchEngines[4].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (IT)`() {
+ Locale.setDefault(Locale("it", "IT"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("it", "IT"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[2].name)
+ assertEquals("Wikipedia (it)", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Amazon.it", store.state.search.regionSearchEngines[4].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for Locale (lij)`() {
+ Locale.setDefault(Locale("lij", "ZE"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("lij", "ZE"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.it", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipedia (lij)", store.state.search.regionSearchEngines[4].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (SE)`() {
+ Locale.setDefault(Locale("sv", "SE"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("sv", "SE"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(7, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Prisjakt", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("Wikipedia (sv)", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Amazon.se", store.state.search.regionSearchEngines[5].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[6].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (PL)`() {
+ Locale.setDefault(Locale("pl", "PL"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("pl", "PL"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(5, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[2].name)
+ assertEquals("Wikipedia (pl)", store.state.search.regionSearchEngines[3].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[4].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (RU)`() {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("RU", "RU"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertNull(store.state.search.regionSearchEngines.find { it.name == "Yandex" })
+ assertNotNull(store.state.search.regionSearchEngines.find { it.name == "Google" })
+ assertNotNull(store.state.search.regionSearchEngines.find { it.name == "DuckDuckGo" })
+ }
+
+ @Test
+ fun `Loads additional search engines`() {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(2, store.state.search.additionalAvailableSearchEngines.size)
+
+ val first = store.state.search.additionalAvailableSearchEngines[0]
+ assertEquals("Reddit", first.name)
+ assertEquals("reddit", first.id)
+
+ val second = store.state.search.additionalAvailableSearchEngines[1]
+ assertEquals("YouTube", second.name)
+ assertEquals("youtube", second.id)
+
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+ }
+
+ @Test
+ fun `Loads additional search engine and honors user choice`() = runTestOnMain {
+ val metadataStorage = SearchMetadataStorage(testContext, preferences = lazy { FakeSharedPreferences() })
+ metadataStorage.setAdditionalSearchEngines(listOf("reddit"))
+
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
+ metadataStorage = metadataStorage,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isNotEmpty())
+
+ assertEquals(1, store.state.search.additionalAvailableSearchEngines.size)
+ assertEquals(1, store.state.search.additionalSearchEngines.size)
+
+ val additional = store.state.search.additionalSearchEngines[0]
+ assertEquals("Reddit", additional.name)
+ assertEquals("reddit", additional.id)
+
+ val available = store.state.search.additionalAvailableSearchEngines[0]
+ assertEquals("YouTube", available.name)
+ assertEquals("youtube", available.id)
+
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNotNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+ }
+
+ @Test
+ fun `Loads custom search engines`() = runTestOnMain {
+ val searchEngine = SearchEngine(
+ id = "test-search",
+ name = "Test Engine",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf(),
+ suggestUrl = null,
+ )
+
+ val storage = CustomSearchEngineStorage(testContext, dispatcher)
+ storage.saveSearchEngine(searchEngine)
+
+ val store = BrowserStore(
+ middleware = listOf(
+ SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = storage,
+ ),
+ ),
+ )
+
+ store.dispatch(
+ SearchAction.SetRegionAction(RegionState.Default),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.customSearchEngines.isNotEmpty())
+ assertNull(store.state.search.userSelectedSearchEngineId)
+ }
+
+ @Test
+ fun `Loads default search engine ID`() = runTestOnMain {
+ val storage = SearchMetadataStorage(testContext)
+ storage.setUserSelectedSearchEngine("test-id", null)
+
+ val middleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ metadataStorage = storage,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(
+ SearchAction.SetRegionAction(RegionState.Default),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals("test-id", store.state.search.userSelectedSearchEngineId)
+ }
+
+ @Test
+ fun `Update default search engine`() {
+ val storage = SearchMetadataStorage(testContext)
+ val id = "test-id-${UUID.randomUUID()}"
+
+ run {
+ val store = BrowserStore(
+ middleware = listOf(
+ SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ metadataStorage = storage,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ SearchAction.SetRegionAction(RegionState.Default),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertNull(store.state.search.userSelectedSearchEngineId)
+
+ store.dispatch(SearchAction.SelectSearchEngineAction(id, null)).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals(id, store.state.search.userSelectedSearchEngineId)
+ }
+
+ run {
+ val store = BrowserStore(
+ middleware = listOf(
+ SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ metadataStorage = storage,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ ),
+ ),
+ )
+
+ store.dispatch(
+ SearchAction.SetRegionAction(RegionState.Default),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals(id, store.state.search.userSelectedSearchEngineId)
+ }
+ }
+
+ @Test
+ fun `Updates and persists additional search engines`() {
+ val storage = SearchMetadataStorage(testContext, preferences = lazy { FakeSharedPreferences() })
+ val middleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ metadataStorage = storage,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ additionalBundledSearchEngineIds = listOf(
+ "reddit",
+ "youtube",
+ ),
+ )
+
+ // First run: Add additional search engine
+ run {
+ val store = BrowserStore(middleware = listOf(middleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(RegionState.Default),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(2, store.state.search.additionalAvailableSearchEngines.size)
+
+ val first = store.state.search.additionalAvailableSearchEngines[0]
+ assertEquals("Reddit", first.name)
+ assertEquals("reddit", first.id)
+
+ val second = store.state.search.additionalAvailableSearchEngines[1]
+ assertEquals("YouTube", second.name)
+ assertEquals("youtube", second.id)
+
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ store.dispatch(
+ SearchAction.AddAdditionalSearchEngineAction("youtube"),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isNotEmpty())
+
+ assertEquals(1, store.state.search.additionalSearchEngines.size)
+ assertEquals(1, store.state.search.additionalAvailableSearchEngines.size)
+
+ assertEquals("Reddit", store.state.search.additionalAvailableSearchEngines[0].name)
+ assertEquals("reddit", store.state.search.additionalAvailableSearchEngines[0].id)
+
+ assertEquals("YouTube", store.state.search.additionalSearchEngines[0].name)
+ assertEquals("youtube", store.state.search.additionalSearchEngines[0].id)
+
+ assertNotNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ assertNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+ }
+
+ // Second run: Restores additional search engine and removes it
+ run {
+ val store = BrowserStore(middleware = listOf(middleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(RegionState.Default),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isNotEmpty())
+
+ assertEquals(1, store.state.search.additionalSearchEngines.size)
+ assertEquals(1, store.state.search.additionalAvailableSearchEngines.size)
+
+ assertEquals("Reddit", store.state.search.additionalAvailableSearchEngines[0].name)
+ assertEquals("reddit", store.state.search.additionalAvailableSearchEngines[0].id)
+
+ assertEquals("YouTube", store.state.search.additionalSearchEngines[0].name)
+ assertEquals("youtube", store.state.search.additionalSearchEngines[0].id)
+
+ assertNotNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ assertNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ store.dispatch(
+ SearchAction.RemoveAdditionalSearchEngineAction(
+ "youtube",
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(2, store.state.search.additionalAvailableSearchEngines.size)
+
+ val first = store.state.search.additionalAvailableSearchEngines[0]
+ assertEquals("Reddit", first.name)
+ assertEquals("reddit", first.id)
+
+ val second = store.state.search.additionalAvailableSearchEngines[1]
+ assertEquals("YouTube", second.name)
+ assertEquals("youtube", second.id)
+
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+ }
+
+ // Third run: Restores without additional search engine
+ run {
+ val store = BrowserStore(middleware = listOf(middleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(RegionState.Default),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(2, store.state.search.additionalAvailableSearchEngines.size)
+
+ val first = store.state.search.additionalAvailableSearchEngines[0]
+ assertEquals("Reddit", first.name)
+ assertEquals("reddit", first.id)
+
+ val second = store.state.search.additionalAvailableSearchEngines[1]
+ assertEquals("YouTube", second.name)
+ assertEquals("youtube", second.id)
+
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNull(store.state.search.searchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "youtube" })
+ assertNotNull(store.state.search.availableSearchEngines.find { searchEngine -> searchEngine.id == "reddit" })
+ }
+ }
+
+ @Test
+ fun `Custom search engines - Create, Update, Delete`() {
+ runTestOnMain {
+ val storage: SearchMiddleware.CustomStorage = mock()
+ doReturn(emptyList<SearchEngine>()).`when`(storage).loadSearchEngineList()
+
+ val store = BrowserStore(
+ middleware = listOf(
+ SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = storage,
+ ),
+ ),
+ )
+
+ store.dispatch(
+ SearchAction.SetRegionAction(RegionState.Default),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.customSearchEngines.isEmpty())
+ verify(storage).loadSearchEngineList()
+ verifyNoMoreInteractions(storage)
+
+ // Add a custom search engine
+
+ val engine1 = SearchEngine("test-id-1", "test engine one", mock(), type = SearchEngine.Type.CUSTOM)
+ store.dispatch(SearchAction.UpdateCustomSearchEngineAction(engine1)).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.customSearchEngines.isNotEmpty())
+ assertEquals(1, store.state.search.customSearchEngines.size)
+ verify(storage).saveSearchEngine(engine1)
+ verifyNoMoreInteractions(storage)
+
+ // Add another custom search engine
+
+ val engine2 = SearchEngine("test-id-2", "test engine two", mock(), type = SearchEngine.Type.CUSTOM)
+ store.dispatch(SearchAction.UpdateCustomSearchEngineAction(engine2)).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.customSearchEngines.isNotEmpty())
+ assertEquals(2, store.state.search.customSearchEngines.size)
+ verify(storage).saveSearchEngine(engine2)
+ verifyNoMoreInteractions(storage)
+
+ assertEquals("test engine one", store.state.search.customSearchEngines[0].name)
+ assertEquals("test engine two", store.state.search.customSearchEngines[1].name)
+
+ // Update first engine
+
+ val updated = engine1.copy(
+ name = "updated engine",
+ )
+ store.dispatch(SearchAction.UpdateCustomSearchEngineAction(updated)).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.customSearchEngines.isNotEmpty())
+ assertEquals(2, store.state.search.customSearchEngines.size)
+ verify(storage).saveSearchEngine(updated)
+ verifyNoMoreInteractions(storage)
+
+ assertEquals("updated engine", store.state.search.customSearchEngines[0].name)
+ assertEquals("test engine two", store.state.search.customSearchEngines[1].name)
+
+ // Remove second engine
+
+ store.dispatch(SearchAction.RemoveCustomSearchEngineAction(engine2.id)).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.customSearchEngines.isNotEmpty())
+ assertEquals(1, store.state.search.customSearchEngines.size)
+ verify(storage).removeSearchEngine(engine2.id)
+ verifyNoMoreInteractions(storage)
+
+ assertEquals("updated engine", store.state.search.customSearchEngines[0].name)
+ }
+ }
+
+ @Test
+ fun `GIVEN disabled engines list contains elements WHEN metadata storage is created THEN the engines are disabled`() = runTestOnMain {
+ val additionalBundledSearchEngineIds = setOf("reddit", "youtube")
+ val metadataStorage = SearchMetadataStorage(
+ testContext,
+ additionalBundledSearchEngineIds,
+ lazy { FakeSharedPreferences() },
+ )
+ val disabledSearchEngineIds = metadataStorage.getDisabledSearchEngineIds()
+ assertTrue(disabledSearchEngineIds.contains("reddit"))
+ assertTrue(disabledSearchEngineIds.contains("youtube"))
+ }
+
+ @Test
+ fun `WHEN update disabled engine action is sent THEN search state and storage get updated`() = runTestOnMain {
+ val metadataStorage = SearchMetadataStorage(testContext, preferences = lazy { FakeSharedPreferences() })
+ metadataStorage.setAdditionalSearchEngines(listOf("reddit"))
+
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
+ metadataStorage = metadataStorage,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertFalse(metadataStorage.getDisabledSearchEngineIds().contains("bing"))
+ assertFalse(store.state.search.disabledSearchEngineIds.contains("bing"))
+
+ store.dispatch(
+ SearchAction.UpdateDisabledSearchEngineIdsAction(
+ "bing",
+ false,
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(metadataStorage.getDisabledSearchEngineIds().contains("bing"))
+ assertTrue(store.state.search.disabledSearchEngineIds.contains("bing"))
+
+ store.dispatch(
+ SearchAction.UpdateDisabledSearchEngineIdsAction(
+ "bing",
+ true,
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertFalse(metadataStorage.getDisabledSearchEngineIds().contains("bing"))
+ assertFalse(store.state.search.disabledSearchEngineIds.contains("bing"))
+ }
+
+ @Test
+ fun `WHEN restore hidden search engines action THEN hidden engines are added back to bundled engines list`() = runTestOnMain {
+ val metadataStorage = SearchMetadataStorage(testContext, preferences = lazy { FakeSharedPreferences() })
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ metadataStorage = metadataStorage,
+ )
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+ wait(store, dispatcher)
+
+ val google = store.state.search.regionSearchEngines.find { searchEngine -> searchEngine.name == "Google" }
+ assertNotNull(google!!)
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+ assertEquals(0, metadataStorage.getHiddenSearchEngines().size)
+
+ store.dispatch(SearchAction.HideSearchEngineAction(google.id)).joinBlocking()
+ wait(store, dispatcher)
+
+ assertNull(store.state.search.regionSearchEngines.find { it.id == google.id })
+
+ assertEquals(1, store.state.search.hiddenSearchEngines.size)
+ assertEquals(1, metadataStorage.getHiddenSearchEngines().size)
+ assertNotNull(store.state.search.hiddenSearchEngines.find { it.id == google.id })
+ assertNotNull(metadataStorage.getHiddenSearchEngines().find { it == google.id })
+
+ store.dispatch(SearchAction.RestoreHiddenSearchEnginesAction).joinBlocking()
+ wait(store, dispatcher)
+
+ assertNotNull(store.state.search.regionSearchEngines.find { it.id == google.id })
+
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+ assertEquals(0, metadataStorage.getHiddenSearchEngines().size)
+ assertNull(store.state.search.hiddenSearchEngines.find { it.id == google.id })
+ assertNull(metadataStorage.getHiddenSearchEngines().find { it == google.id })
+ }
+
+ @Test
+ fun `Hiding and showing search engines`() {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ metadataStorage = SearchMetadataStorage(testContext),
+ )
+
+ val google = BrowserStore(middleware = listOf(searchMiddleware)).let { store ->
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ store.state.search.regionSearchEngines.find { searchEngine -> searchEngine.name == "Google" }
+ }
+ assertNotNull(google!!)
+
+ run {
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertNotNull(store.state.search.regionSearchEngines.find { it.id == google.id })
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+
+ store.dispatch(
+ SearchAction.HideSearchEngineAction(google.id),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertNull(store.state.search.regionSearchEngines.find { it.id == google.id })
+ assertEquals(1, store.state.search.hiddenSearchEngines.size)
+ assertNotNull(store.state.search.hiddenSearchEngines.find { it.id == google.id })
+ }
+
+ run {
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertNull(store.state.search.regionSearchEngines.find { it.id == google.id })
+ assertEquals(1, store.state.search.hiddenSearchEngines.size)
+ assertNotNull(store.state.search.hiddenSearchEngines.find { it.id == google.id })
+
+ store.dispatch(
+ SearchAction.ShowSearchEngineAction(google.id),
+ ).joinBlocking()
+
+ assertNotNull(store.state.search.regionSearchEngines.find { it.id == google.id })
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+ }
+
+ run {
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertNotNull(store.state.search.regionSearchEngines.find { it.id == google.id })
+ assertEquals(0, store.state.search.hiddenSearchEngines.size)
+ }
+ }
+
+ @Test
+ fun `Keeps user choice based on search engine name even if search engine id changes`() {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ metadataStorage = SearchMetadataStorage(testContext),
+ )
+
+ run {
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ val google = store.state.search.searchEngines.find { it.name == "Google" }
+ assertNotNull(google!!)
+ assertEquals("google-b-1-m", google.id)
+
+ store.dispatch(
+ SearchAction.SelectSearchEngineAction(
+ searchEngineId = "google-b-1-m",
+ searchEngineName = "Google",
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals("google-b-1-m", store.state.search.userSelectedSearchEngineId)
+ assertEquals("Google", store.state.search.userSelectedSearchEngineName)
+
+ val searchEngine = store.state.search.selectedOrDefaultSearchEngine
+ assertNotNull(searchEngine!!)
+ assertEquals("google-b-1-m", searchEngine.id)
+ assertEquals("Google", searchEngine.name)
+ }
+
+ run {
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("DE", "DE"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals("google-b-1-m", store.state.search.userSelectedSearchEngineId)
+ assertEquals("Google", store.state.search.userSelectedSearchEngineName)
+
+ val searchEngine = store.state.search.selectedOrDefaultSearchEngine
+ assertNotNull(searchEngine!!)
+ assertEquals("google-b-m", searchEngine.id)
+ assertEquals("Google", searchEngine.name)
+ }
+ }
+
+ @Test
+ fun `Adding and restoring custom search engine`() {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ metadataStorage = SearchMetadataStorage(testContext),
+ )
+
+ run {
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals(0, store.state.search.customSearchEngines.size)
+
+ store.dispatch(
+ SearchAction.UpdateCustomSearchEngineAction(
+ SearchEngine(
+ id = UUID.randomUUID().toString(),
+ name = "Example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf(
+ "https://example.org/?q=%s",
+ ),
+ ),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals(1, store.state.search.customSearchEngines.size)
+ }
+
+ run {
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals(1, store.state.search.customSearchEngines.size)
+ }
+ }
+
+ @Test
+ fun `Migration - custom search engine and default search engine`() {
+ val customStorage = CustomSearchEngineStorage(testContext, dispatcher)
+ val metadataStorage = SearchMetadataStorage(testContext)
+
+ run {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = customStorage,
+ metadataStorage = metadataStorage,
+ migration = object : SearchMiddleware.Migration {
+ override fun getValuesToMigrate() = SearchMiddleware.Migration.MigrationValues(
+ customSearchEngines = listOf(
+ createSearchEngine(
+ name = "Example",
+ url = "https://example.org/?q={searchTerms}",
+ icon = mock(),
+ ),
+ ),
+ defaultSearchEngineName = "Example",
+ )
+ },
+ )
+
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals(1, store.state.search.customSearchEngines.size)
+
+ val selectedSearchEngine = store.state.search.selectedOrDefaultSearchEngine
+ assertNotNull(selectedSearchEngine!!)
+
+ assertEquals("Example", selectedSearchEngine.name)
+ assertEquals("https://example.org/?q={searchTerms}", selectedSearchEngine.resultUrls[0])
+ }
+
+ run {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = customStorage,
+ metadataStorage = metadataStorage,
+ )
+
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertEquals(1, store.state.search.customSearchEngines.size)
+
+ val selectedSearchEngine = store.state.search.selectedOrDefaultSearchEngine
+ assertNotNull(selectedSearchEngine!!)
+
+ assertEquals("Example", selectedSearchEngine.name)
+ assertEquals("https://example.org/?q={searchTerms}", selectedSearchEngine.resultUrls[0])
+ }
+ }
+
+ @Test
+ fun `Migration - default search engine`() {
+ val customStorage = CustomSearchEngineStorage(testContext, dispatcher)
+ val metadataStorage = SearchMetadataStorage(testContext)
+
+ run {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = customStorage,
+ metadataStorage = metadataStorage,
+ migration = object : SearchMiddleware.Migration {
+ override fun getValuesToMigrate() = SearchMiddleware.Migration.MigrationValues(
+ customSearchEngines = listOf(),
+ defaultSearchEngineName = "Amazon.com",
+ )
+ },
+ )
+
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ val selectedSearchEngine = store.state.search.selectedOrDefaultSearchEngine
+ assertNotNull(selectedSearchEngine!!)
+
+ assertEquals("Amazon.com", selectedSearchEngine.name)
+ assertTrue(selectedSearchEngine.resultUrls[0].startsWith("https://www.amazon.com/"))
+ }
+
+ run {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = customStorage,
+ metadataStorage = metadataStorage,
+ )
+
+ val store = BrowserStore(middleware = listOf(searchMiddleware))
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ val selectedSearchEngine = store.state.search.selectedOrDefaultSearchEngine
+ assertNotNull(selectedSearchEngine!!)
+
+ assertEquals("Amazon.com", selectedSearchEngine.name)
+ assertTrue(selectedSearchEngine.resultUrls[0].startsWith("https://www.amazon.com/"))
+ }
+ }
+
+ @Test
+ fun `Reorders list of region search engines after adding previously removed search engines`() {
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("US", "US"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////
+ // Verify initial state
+ // ///////////////////////////////////////////////////////////////////////////////////////////
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.com", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Wikipedia", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+
+ store.dispatch(
+ SearchAction.HideSearchEngineAction(
+ "google-b-1-m",
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ SearchAction.HideSearchEngineAction(
+ "ddg",
+ ),
+ ).joinBlocking()
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////
+ // Verify after hiding search engines
+ // ///////////////////////////////////////////////////////////////////////////////////////////
+
+ assertEquals(4, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Bing", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Amazon.com", store.state.search.regionSearchEngines[1].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[2].name)
+ assertEquals("Wikipedia", store.state.search.regionSearchEngines[3].name)
+
+ assertEquals("Bing", store.state.search.selectedOrDefaultSearchEngine!!.name)
+
+ println(store.state.search.regionSearchEngines)
+
+ store.dispatch(
+ SearchAction.ShowSearchEngineAction("google-b-1-m"),
+ ).joinBlocking()
+
+ store.dispatch(
+ SearchAction.ShowSearchEngineAction("ddg"),
+ ).joinBlocking()
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////
+ // Verify state after adding search engines back
+ // ///////////////////////////////////////////////////////////////////////////////////////////
+
+ assertEquals(6, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.com", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("eBay", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Wikipedia", store.state.search.regionSearchEngines[5].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+
+ @Test
+ fun `Loads search engines for locale (JA)`() {
+ Locale.setDefault(Locale("ja", "JA"))
+ val searchMiddleware = SearchMiddleware(
+ testContext,
+ ioDispatcher = dispatcher,
+ customStorage = CustomSearchEngineStorage(testContext, dispatcher),
+ )
+
+ val store = BrowserStore(
+ middleware = listOf(searchMiddleware),
+ )
+
+ assertTrue(store.state.search.regionSearchEngines.isEmpty())
+
+ store.dispatch(
+ SearchAction.SetRegionAction(
+ RegionState("JA", "JA"),
+ ),
+ ).joinBlocking()
+
+ wait(store, dispatcher)
+
+ assertTrue(store.state.search.regionSearchEngines.isNotEmpty())
+ assertTrue(store.state.search.additionalAvailableSearchEngines.isEmpty())
+ assertTrue(store.state.search.additionalSearchEngines.isEmpty())
+
+ assertEquals(8, store.state.search.regionSearchEngines.size)
+
+ assertEquals("Google", store.state.search.regionSearchEngines[0].name)
+ assertEquals("Bing", store.state.search.regionSearchEngines[1].name)
+ assertEquals("Amazon.co.jp", store.state.search.regionSearchEngines[2].name)
+ assertEquals("DuckDuckGo", store.state.search.regionSearchEngines[3].name)
+ assertEquals("楽天市場", store.state.search.regionSearchEngines[4].name)
+ assertEquals("Wikipedia (ja)", store.state.search.regionSearchEngines[5].name)
+ assertEquals("Yahoo! JAPAN", store.state.search.regionSearchEngines[6].name)
+ assertEquals("Yahoo!オークション", store.state.search.regionSearchEngines[7].name)
+
+ assertEquals("Google", store.state.search.selectedOrDefaultSearchEngine!!.name)
+ }
+}
+
+private fun wait(store: BrowserStore, dispatcher: TestDispatcher) {
+ // First we wait for the InitAction that may still need to be processed.
+ store.waitUntilIdle()
+
+ // Now we wait for the Middleware that may need to asynchronously process an action the test dispatched
+ dispatcher.scheduler.advanceUntilIdle()
+
+ // Since the Middleware may have dispatched an action, we now wait for the store again.
+ store.waitUntilIdle()
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionManagerTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionManagerTest.kt
new file mode 100644
index 0000000000..1103680e3e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionManagerTest.kt
@@ -0,0 +1,146 @@
+/* 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.search.region
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.location.LocationService
+import mozilla.components.support.test.fakes.FakeClock
+import mozilla.components.support.test.fakes.android.FakeContext
+import mozilla.components.support.test.fakes.android.FakeSharedPreferences
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class RegionManagerTest {
+ @Test
+ fun `Initial state`() {
+ val regionManager = RegionManager(
+ context = FakeContext(),
+ locationService = FakeLocationService(),
+ currentTime = FakeClock()::time,
+ preferences = lazy { FakeSharedPreferences() },
+ )
+
+ assertNull(regionManager.region())
+ }
+
+ @Test
+ fun `First update`() = runTest {
+ val locationService = FakeLocationService(
+ region = LocationService.Region("DE", "Germany"),
+ )
+
+ val regionManager = RegionManager(
+ context = FakeContext(),
+ locationService = locationService,
+ currentTime = FakeClock()::time,
+ preferences = lazy { FakeSharedPreferences() },
+ )
+
+ val updatedRegion = regionManager.update()
+ assertNotNull(updatedRegion!!)
+ assertEquals("DE", updatedRegion.current)
+ assertEquals("DE", updatedRegion.home)
+ }
+
+ @Test
+ fun `Updating to new home region`() = runTest {
+ val clock = FakeClock()
+
+ val locationService = FakeLocationService(
+ region = LocationService.Region("DE", "Germany"),
+ )
+
+ val regionManager = RegionManager(
+ context = FakeContext(),
+ locationService = locationService,
+ currentTime = clock::time,
+ preferences = lazy { FakeSharedPreferences() },
+ )
+
+ regionManager.update()
+
+ locationService.region = LocationService.Region("FR", "France")
+
+ // Should not be updated since the "home" region didn't change
+ assertNull(regionManager.update())
+ assertEquals("DE", regionManager.region()?.home)
+ assertEquals("FR", regionManager.region()?.current)
+
+ // Let's jump one week into the future!
+ clock.advanceBy(60L * 60L * 24L * 7L * 1000L)
+
+ // Still not updated because we switch after two weeks
+ assertNull(regionManager.update())
+ assertEquals("DE", regionManager.region()?.home)
+ assertEquals("FR", regionManager.region()?.current)
+
+ // Let's move the clock 8 more days into the future
+ clock.advanceBy(60L * 60L * 24L * 8L * 1000L)
+
+ val updatedRegion = (regionManager.update())
+ assertNotNull(updatedRegion!!)
+ assertEquals("FR", updatedRegion.home)
+ assertEquals("FR", updatedRegion.current)
+ assertEquals("FR", regionManager.region()?.home)
+ assertEquals("FR", regionManager.region()?.current)
+ }
+
+ @Test
+ fun `Switching back to home region after staying in different region shortly`() = runTest {
+ val clock = FakeClock()
+
+ val locationService = FakeLocationService(
+ region = LocationService.Region("DE", "Germany"),
+ )
+
+ val regionManager = RegionManager(
+ context = FakeContext(),
+ locationService = locationService,
+ currentTime = clock::time,
+ preferences = lazy { FakeSharedPreferences() },
+ )
+
+ regionManager.update()
+
+ // Let's jump one week into the future!
+ clock.advanceBy(60L * 60L * 24L * 7L * 1000L)
+
+ locationService.region = LocationService.Region("FR", "France")
+
+ // Should not be updated since the "home" region didn't change
+ assertNull(regionManager.update())
+ assertEquals("DE", regionManager.region()?.home)
+ assertEquals("FR", regionManager.region()?.current)
+
+ // Next day, we are back in the home region
+ clock.advanceBy(60L * 60L * 24L * 1000L)
+
+ locationService.region = LocationService.Region("DE", "Germany")
+ assertNull(regionManager.update())
+ assertEquals("DE", regionManager.region()?.home)
+ assertEquals("DE", regionManager.region()?.current)
+
+ // Another week forward, we are back in France
+ clock.advanceBy(60L * 60L * 24L * 7L * 1000L)
+
+ locationService.region = LocationService.Region("FR", "France")
+
+ // The "home" region should not have changed since we haven't been in the other region the
+ // whole time.
+ assertNull(regionManager.update())
+ assertEquals("DE", regionManager.region()?.home)
+ assertEquals("FR", regionManager.region()?.current)
+ }
+}
+
+class FakeLocationService(
+ var region: LocationService.Region? = null,
+ private val hasRegionCached: Boolean = false,
+) : LocationService {
+ override suspend fun fetchRegion(readFromCache: Boolean): LocationService.Region? = region
+ override fun hasRegionCached(): Boolean = hasRegionCached
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionMiddlewareTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionMiddlewareTest.kt
new file mode 100644
index 0000000000..b05dc5e7c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionMiddlewareTest.kt
@@ -0,0 +1,153 @@
+/* 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.search.region
+
+import mozilla.components.browser.state.action.InitAction
+import mozilla.components.browser.state.action.SearchAction.RefreshSearchEnginesAction
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.service.location.LocationService
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.fakes.FakeClock
+import mozilla.components.support.test.fakes.android.FakeContext
+import mozilla.components.support.test.fakes.android.FakeSharedPreferences
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class RegionMiddlewareTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ private lateinit var locationService: FakeLocationService
+ private lateinit var clock: FakeClock
+ private lateinit var regionManager: RegionManager
+
+ @Before
+ fun setUp() {
+ clock = FakeClock()
+ locationService = FakeLocationService()
+ regionManager = RegionManager(
+ context = FakeContext(),
+ locationService = locationService,
+ currentTime = clock::time,
+ preferences = lazy { FakeSharedPreferences() },
+ )
+ }
+
+ @Test
+ fun `Updates region on init`() {
+ val middleware = RegionMiddleware(FakeContext(), locationService, dispatcher)
+ middleware.regionManager = regionManager
+
+ locationService.region = LocationService.Region("FR", "France")
+
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ store.waitUntilIdle()
+ middleware.updateJob?.joinBlocking()
+ store.waitUntilIdle()
+
+ assertNotEquals(RegionState.Default, store.state.search.region)
+ assertEquals("FR", store.state.search.region!!.home)
+ assertEquals("FR", store.state.search.region!!.current)
+ }
+
+ @Test
+ fun `Uses default region if could never get updated`() {
+ val middleware = RegionMiddleware(FakeContext(), locationService, dispatcher)
+ middleware.regionManager = regionManager
+
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(InitAction).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ assertEquals(RegionState.Default, store.state.search.region)
+ assertEquals("XX", store.state.search.region!!.home)
+ assertEquals("XX", store.state.search.region!!.current)
+ }
+
+ @Test
+ fun `Dispatches cached home region and update later`() = runTestOnMain {
+ val middleware = RegionMiddleware(FakeContext(), locationService, dispatcher)
+ middleware.regionManager = regionManager
+
+ locationService.region = LocationService.Region("FR", "France")
+ regionManager.update()
+
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(InitAction).joinBlocking()
+ middleware.updateJob?.joinBlocking()
+ store.waitUntilIdle()
+
+ assertEquals("FR", store.state.search.region!!.home)
+ assertEquals("FR", store.state.search.region!!.current)
+
+ locationService.region = LocationService.Region("DE", "Germany")
+ regionManager.update()
+
+ store.dispatch(InitAction).joinBlocking()
+ middleware.updateJob?.joinBlocking()
+ store.waitUntilIdle()
+
+ assertEquals("FR", store.state.search.region!!.home)
+ assertEquals("DE", store.state.search.region!!.current)
+
+ clock.advanceBy(1000L * 60L * 60L * 24L * 21L)
+
+ store.dispatch(InitAction).joinBlocking()
+ middleware.updateJob?.joinBlocking()
+ store.waitUntilIdle()
+
+ assertEquals("DE", store.state.search.region!!.home)
+ assertEquals("DE", store.state.search.region!!.current)
+ }
+
+ @Test
+ fun `GIVEN a locale is already selected WHEN the locale changes THEN update region on RefreshSearchEngines`() = runTestOnMain {
+ val middleware = RegionMiddleware(FakeContext(), locationService, dispatcher)
+ middleware.regionManager = regionManager
+
+ locationService.region = LocationService.Region("FR", "France")
+
+ val store = BrowserStore(
+ middleware = listOf(middleware),
+ )
+
+ store.dispatch(InitAction).joinBlocking()
+ middleware.updateJob?.joinBlocking()
+ store.waitUntilIdle()
+
+ assertEquals("FR", store.state.search.region!!.home)
+ assertEquals("FR", store.state.search.region!!.current)
+
+ locationService.region = LocationService.Region("DE", "Germany")
+ regionManager.update()
+
+ store.dispatch(RefreshSearchEnginesAction).joinBlocking()
+ middleware.updateJob?.joinBlocking()
+ store.waitUntilIdle()
+
+ assertEquals("FR", store.state.search.region!!.home)
+ assertEquals("DE", store.state.search.region!!.current)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorageTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorageTest.kt
new file mode 100644
index 0000000000..bfd6691432
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorageTest.kt
@@ -0,0 +1,196 @@
+/* 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.search.storage
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.search.RegionState
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Locale
+
+@RunWith(AndroidJUnit4::class)
+class BundledSearchEnginesStorageTest {
+ @Test
+ fun `Load search engines for en-US from assets`() = runTest {
+ val storage = BundledSearchEnginesStorage(testContext)
+
+ val engines = storage.load(RegionState("US", "US"), Locale("en", "US"))
+ val searchEngines = engines.list
+
+ assertEquals(6, searchEngines.size)
+ }
+
+ @Test
+ fun `Load search engines for all known locales without region`() = runTest {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val locales = Locale.getAvailableLocales()
+ assertTrue(locales.isNotEmpty())
+
+ for (locale in locales) {
+ val engines = storage.load(RegionState.Default, locale)
+ assertTrue(engines.list.isNotEmpty())
+ assertFalse(engines.defaultSearchEngineId.isEmpty())
+ }
+ }
+
+ @Test
+ fun `Load search engines for de-DE with global US region override`() = runTest {
+ // Without region
+ run {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val engines = storage.load(RegionState.Default, Locale("de", "DE"))
+ val searchEngines = engines.list
+
+ assertEquals(8, searchEngines.size)
+ assertContainsSearchEngine("google-b-m", searchEngines)
+ assertContainsNotSearchEngine("google-2018", searchEngines)
+ }
+ // With region
+ run {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val engines = storage.load(RegionState("US", "US"), Locale("de", "DE"))
+ val searchEngines = engines.list
+
+ assertEquals(8, searchEngines.size)
+ assertContainsSearchEngine("google-b-1-m", searchEngines)
+ assertContainsNotSearchEngine("google", searchEngines)
+ }
+ }
+
+ @Test
+ fun `Load search engines for en-US with local RU region override`() = runTest {
+ // Without region
+ run {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val engines = storage.load(RegionState.Default, Locale("en", "US"))
+ val searchEngines = engines.list
+
+ println("searchEngines = $searchEngines")
+ assertEquals(6, searchEngines.size)
+ assertContainsNotSearchEngine("yandex-en", searchEngines)
+ }
+ // With region
+ run {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val engines = storage.load(RegionState("RU", "RU"), Locale("en", "US"))
+ val searchEngines = engines.list
+
+ println("searchEngines = $searchEngines")
+ assertEquals(5, searchEngines.size)
+ assertContainsSearchEngine("google-com-nocodes", searchEngines)
+ assertContainsNotSearchEngine("yandex-en", searchEngines)
+ }
+ }
+
+ @Test
+ fun `Load search engines for zh-CN_CN locale with searchDefault override`() = runTest {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val engines = storage.load(RegionState("CN", "CN"), Locale("zh", "CN"))
+ val searchEngines = engines.list
+
+ // visibleDefaultEngines: ["google-b-m", "bing", "baidu", "ddg", "wikipedia-zh-CN"]
+ // searchOrder (default): ["Google", "Bing"]
+
+ assertEquals(
+ listOf("google-b-m", "bing", "baidu", "ddg", "wikipedia-zh-CN"),
+ searchEngines.map { it.id },
+ )
+
+ // searchDefault: "百度"
+ val default = searchEngines.find { it.id == engines.defaultSearchEngineId }
+ assertNotNull(default)
+ assertEquals("baidu", default!!.id)
+ }
+
+ @Test
+ fun `Load search engines for ru_RU locale with engines not in searchOrder`() = runTest {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val engines = storage.load(RegionState("RU", "RU"), Locale("ru", "RU"))
+ val searchEngines = engines.list
+
+ assertEquals(
+ listOf("google-com-nocodes", "ddg", "wikipedia-ru"),
+ searchEngines.map { it.id },
+ )
+
+ // searchDefault: "Google"
+ val default = searchEngines.find { it.id == engines.defaultSearchEngineId }
+ assertNotNull(default)
+ assertEquals("google-com-nocodes", default!!.id)
+ }
+
+ @Test
+ fun `Load search engines for trs locale with non-google initial engines and no default`() = runTest {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val engines = storage.load(RegionState.Default, Locale("trs", ""))
+ val searchEngines = engines.list
+
+ // visibleDefaultEngines: ["google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-es"]
+ // searchOrder (default): ["Google", "Bing"]
+
+ assertEquals(
+ listOf("google-b-m", "bing", "amazondotcom", "ddg", "wikipedia-es"),
+ searchEngines.map { it.id },
+ )
+
+ // searchDefault (default): "Google"
+ val default = searchEngines.find { it.id == engines.defaultSearchEngineId }
+ assertNotNull(default)
+ assertEquals("google-b-m", default!!.id)
+ }
+
+ @Test
+ fun `Load search engines for locale not in configuration`() = runTest {
+ val storage = BundledSearchEnginesStorage(testContext)
+ val engines = storage.load(RegionState.Default, Locale("xx", "XX"))
+ val searchEngines = engines.list
+
+ assertEquals(5, searchEngines.size)
+ }
+
+ private fun assertContainsSearchEngine(identifier: String, searchEngines: List<SearchEngine>) {
+ searchEngines.forEach {
+ if (identifier == it.id) {
+ return
+ }
+ }
+ throw AssertionError("Search engine $identifier not in list")
+ }
+
+ private fun assertContainsNotSearchEngine(identifier: String, searchEngines: List<SearchEngine>) {
+ searchEngines.forEach {
+ if (identifier == it.id) {
+ throw AssertionError("Search engine $identifier in list")
+ }
+ }
+ }
+
+ @Test
+ fun `Verify values of Google search engine`() = runTest {
+ val storage = BundledSearchEnginesStorage(testContext)
+
+ val engines = storage.load(RegionState("US", "US"), Locale("en", "US"))
+ val searchEngines = engines.list
+
+ assertEquals(6, searchEngines.size)
+
+ val google = searchEngines.find { it.name == "Google" }
+ assertNotNull(google!!)
+
+ assertEquals("google-b-1-m", google.id)
+ assertEquals("Google", google.name)
+ assertEquals(SearchEngine.Type.BUNDLED, google.type)
+ assertNotNull(google.icon)
+ assertEquals("https://www.google.com/complete/search?client=firefox&q={searchTerms}", google.suggestUrl)
+ assertTrue(google.resultUrls.isNotEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/CustomSearchEngineStorageTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/CustomSearchEngineStorageTest.kt
new file mode 100644
index 0000000000..2da25c0b5d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/CustomSearchEngineStorageTest.kt
@@ -0,0 +1,105 @@
+/* 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.search.storage
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CustomSearchEngineStorageTest {
+ @Test
+ fun `saveSearchEngine successfully saves`() = runTest {
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search"),
+ )
+
+ val storage = CustomSearchEngineStorage(testContext, coroutineContext)
+ assertTrue(storage.saveSearchEngine(searchEngine))
+ assertTrue(storage.getSearchFile(searchEngine.id).baseFile.exists())
+ }
+
+ @Test
+ fun `loadSearchEngine successfully loads after saving`() = runTest {
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search"),
+ )
+
+ val storage = CustomSearchEngineStorage(testContext, coroutineContext)
+ assertTrue(storage.saveSearchEngine(searchEngine))
+
+ val storedSearchEngine = storage.loadSearchEngine(searchEngine.id)
+ assertEquals(searchEngine.id, storedSearchEngine.id)
+ assertEquals(searchEngine.name, storedSearchEngine.name)
+ assertEquals(searchEngine.type, storedSearchEngine.type)
+ assertEquals(searchEngine.resultUrls, storedSearchEngine.resultUrls)
+ }
+
+ @Test
+ @Ignore("https://github.com/mozilla-mobile/android-components/issues/8124")
+ fun `loadSearchEngineList successfully loads after saving`() = runTest {
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search"),
+ )
+ val searchEngineTwo = SearchEngine(
+ id = "id2",
+ name = "searchTwo",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.searchtwo.com/search"),
+ )
+
+ val storage = CustomSearchEngineStorage(testContext, coroutineContext)
+ assertTrue(storage.saveSearchEngine(searchEngine))
+ assertTrue(storage.saveSearchEngine(searchEngineTwo))
+
+ val storedSearchEngines = storage.loadSearchEngineList()
+ assertEquals(2, storedSearchEngines.size)
+ assertEquals(searchEngine.id, storedSearchEngines[0].id)
+ assertEquals(searchEngine.name, storedSearchEngines[0].name)
+ assertEquals(searchEngine.type, storedSearchEngines[0].type)
+ assertEquals(searchEngine.resultUrls, storedSearchEngines[0].resultUrls)
+ assertEquals(searchEngineTwo.id, storedSearchEngines[1].id)
+ assertEquals(searchEngineTwo.name, storedSearchEngines[1].name)
+ assertEquals(searchEngineTwo.type, storedSearchEngines[1].type)
+ assertEquals(searchEngineTwo.resultUrls, storedSearchEngines[1].resultUrls)
+ }
+
+ @Test
+ fun `removeSearchEngine successfully deletes`() = runTest {
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search"),
+ )
+
+ val storage = CustomSearchEngineStorage(testContext, coroutineContext)
+ assertTrue(storage.saveSearchEngine(searchEngine))
+
+ storage.removeSearchEngine(searchEngine.id)
+ assertTrue(storage.loadSearchEngineList().isEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/ParseSearchPluginsTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/ParseSearchPluginsTest.kt
new file mode 100644
index 0000000000..662fbd76b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/ParseSearchPluginsTest.kt
@@ -0,0 +1,106 @@
+/* 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.search.storage
+
+import android.text.TextUtils
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.feature.search.middleware.SearchExtraParams
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import java.io.File
+import java.io.FileInputStream
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+class ParseSearchPluginsTest(private val searchEngineIdentifier: String) {
+
+ @Test
+ @Throws(Exception::class)
+ fun parserNoSearchExtraParams() {
+ val stream = FileInputStream(File(basePath, searchEngineIdentifier))
+ val searchEngine = SearchEngineReader(type = SearchEngine.Type.BUNDLED)
+ .loadStream(searchEngineIdentifier, stream)
+
+ assertEquals(searchEngineIdentifier, searchEngine.id)
+
+ assertNotNull(searchEngine.name)
+ assertFalse(TextUtils.isEmpty(searchEngine.name))
+
+ assertNotNull(searchEngine.icon)
+
+ val searchUrl = searchEngine.resultUrls
+ assertTrue(searchUrl.isNotEmpty())
+
+ stream.close()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun parserWithSearchExtraParams() {
+ val stream = FileInputStream(File(basePath, searchEngineIdentifier))
+ val searchEngineName = "test"
+ val featureEnablerName = "t"
+ val featureEnablerParam = "enabled"
+ val channelIdName = "p"
+ val channelIdParam = "12345"
+ val searchExtraParams = SearchExtraParams(
+ searchEngineName = searchEngineName,
+ featureEnablerName = featureEnablerName,
+ featureEnablerParam = featureEnablerParam,
+ channelIdName = channelIdName,
+ channelIdParam = channelIdParam,
+ )
+ val searchEngine =
+ SearchEngineReader(
+ type = SearchEngine.Type.BUNDLED,
+ searchExtraParams = searchExtraParams,
+ ).loadStream(
+ identifier = searchEngineIdentifier,
+ stream = stream,
+ )
+
+ assertEquals(searchEngineIdentifier, searchEngine.id)
+
+ assertNotNull(searchEngine.name)
+ assertFalse(TextUtils.isEmpty(searchEngine.name))
+
+ assertNotNull(searchEngine.icon)
+
+ val searchUrl = searchEngine.resultUrls
+ assertTrue(searchUrl.isNotEmpty())
+
+ if (searchEngine.name.startsWith(searchEngineName)) {
+ for (url in searchUrl) {
+ assertTrue(url.contains("=$featureEnablerParam"))
+ assertTrue(url.endsWith("=$channelIdParam"))
+ }
+ } else {
+ for (url in searchUrl) {
+ assertFalse(url.endsWith("=$channelIdParam"))
+ assertFalse(url.contains("=$featureEnablerParam"))
+ }
+ }
+
+ stream.close()
+ }
+
+ companion object {
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+ fun searchPlugins(): Collection<Array<Any>> = basePath.list().orEmpty()
+ .mapNotNull { it as Any }
+ .map { arrayOf(it) }
+ .apply { if (isEmpty()) { throw IllegalStateException("No search plugins found.") } }
+
+ private val basePath: File
+ get() = ParseSearchPluginsTest::class.java.classLoader!!
+ .getResource("searchplugins").file
+ .let { File(it) }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/SearchEngineReaderTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/SearchEngineReaderTest.kt
new file mode 100644
index 0000000000..e1bdea0901
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/SearchEngineReaderTest.kt
@@ -0,0 +1,84 @@
+/* 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.search.storage
+
+import android.util.AtomicFile
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class SearchEngineReaderTest {
+ @Test
+ fun `SearchEngineReader can read from a file`() {
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ inputEncoding = "ISO-8859-1",
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search"),
+ )
+ val readSearchEngine = saveAndLoadSearchEngine(searchEngine)
+
+ assertEquals(searchEngine.id, readSearchEngine.id)
+ assertEquals(searchEngine.name, readSearchEngine.name)
+ assertEquals(searchEngine.inputEncoding, readSearchEngine.inputEncoding)
+ assertEquals(searchEngine.type, readSearchEngine.type)
+ assertEquals(searchEngine.resultUrls, readSearchEngine.resultUrls)
+ assertTrue(readSearchEngine.isGeneral)
+ }
+
+ @Test(expected = IOException::class)
+ fun `Parsing not existing file will throw exception`() {
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search"),
+ )
+ val reader = SearchEngineReader(type = SearchEngine.Type.CUSTOM)
+ val invalidFile = AtomicFile(File("", ""))
+ reader.loadFile(searchEngine.id, invalidFile)
+ }
+
+ @Test
+ fun `WHEN SearchEngineReader is loading bundled search engines from a file THEN the correct SearchEngine properties are parsed`() {
+ for (id in GENERAL_SEARCH_ENGINE_IDS + setOf("mozilla", "wikipedia")) {
+ val searchEngine = SearchEngine(
+ id = id,
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.BUNDLED,
+ resultUrls = listOf("https://www.example.com/search"),
+ )
+ val readSearchEngine = saveAndLoadSearchEngine(searchEngine)
+
+ assertEquals(searchEngine.id, readSearchEngine.id)
+ assertEquals(searchEngine.name, readSearchEngine.name)
+ assertEquals(searchEngine.type, readSearchEngine.type)
+ assertEquals(searchEngine.resultUrls, readSearchEngine.resultUrls)
+ assertEquals(id in GENERAL_SEARCH_ENGINE_IDS, readSearchEngine.isGeneral)
+ }
+ }
+ private fun saveAndLoadSearchEngine(searchEngine: SearchEngine): SearchEngine {
+ val storage = CustomSearchEngineStorage(testContext)
+ val writer = SearchEngineWriter()
+ val reader = SearchEngineReader(type = searchEngine.type)
+ val file = storage.getSearchFile(searchEngine.id)
+
+ writer.saveSearchEngineXML(searchEngine, file)
+
+ return reader.loadFile(searchEngine.id, file)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/SearchEngineWriterTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/SearchEngineWriterTest.kt
new file mode 100644
index 0000000000..487231f6f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/storage/SearchEngineWriterTest.kt
@@ -0,0 +1,212 @@
+/* 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.search.storage
+
+import android.util.AtomicFile
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.w3c.dom.DOMException
+import org.w3c.dom.DOMException.INVALID_CHARACTER_ERR
+import org.w3c.dom.Document
+import java.io.File
+import java.io.StringWriter
+import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.transform.OutputKeys
+import javax.xml.transform.TransformerConfigurationException
+import javax.xml.transform.TransformerException
+import javax.xml.transform.TransformerFactory
+import javax.xml.transform.dom.DOMSource
+import javax.xml.transform.stream.StreamResult
+
+@RunWith(AndroidJUnit4::class)
+class SearchEngineWriterTest {
+ @Test
+ fun `buildSearchEngineXML builds search engine xml correctly`() {
+ val writer = SearchEngineWriter()
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ inputEncoding = "UTF-8",
+ resultUrls = listOf("https://www.example.com/search?q={searchTerms}'"),
+ )
+
+ val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
+ writer.buildSearchEngineXML(searchEngine, document)
+ val searchXML = document.xmlToString()
+ assertTrue(searchXML!!.contains(searchEngine.name))
+ assertTrue(searchXML.contains(IMAGE_URI_PREFIX))
+ assertTrue(searchXML.contains(URL_TYPE_SEARCH_HTML))
+ assertTrue(searchXML.contains("https://www.example.com/search?q={searchTerms}"))
+ assertTrue(searchXML.contains("UTF-8"))
+ assertFalse(searchXML.contains(URL_TYPE_SUGGEST_JSON))
+ }
+
+ @Test
+ fun `buildSearchEngineXML builds multiple resultUrls correctly`() {
+ val writer = SearchEngineWriter()
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf(
+ "https://www.example.com/search?q=%s",
+ "https://www.example.com/search1?q=%s",
+ "https://www.example.com/search2?q=%s",
+ ),
+ )
+
+ val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
+ writer.buildSearchEngineXML(searchEngine, document)
+ val searchXML = document.xmlToString()
+ assertTrue(searchXML!!.contains(searchEngine.name))
+ assertTrue(searchXML.contains(IMAGE_URI_PREFIX))
+ assertTrue(searchXML.contains(URL_TYPE_SEARCH_HTML))
+ searchEngine.resultUrls.forEach {
+ val url = it.replace("%s", "{searchTerms}")
+ searchXML.contains(url)
+ }
+ assertFalse(searchXML.contains(URL_TYPE_SUGGEST_JSON))
+ }
+
+ @Test
+ fun `buildSearchEngineXML builds suggestUrl correctly`() {
+ val writer = SearchEngineWriter()
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ suggestUrl = "https://www.example.com/search-suggestion?q=%s",
+ )
+
+ val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
+ writer.buildSearchEngineXML(searchEngine, document)
+ val searchXML = document.xmlToString()
+ assertTrue(searchXML!!.contains(searchEngine.name))
+ assertTrue(searchXML.contains(IMAGE_URI_PREFIX))
+ assertFalse(searchXML.contains(URL_TYPE_SEARCH_HTML))
+ assertTrue(searchXML.contains(URL_TYPE_SUGGEST_JSON))
+ assertTrue(searchXML.contains("https://www.example.com/search-suggestion?q={searchTerms}"))
+ }
+
+ @Test
+ fun `buildSearchEngineXML successfully for search engines with XML escaped characters`() {
+ val writer = SearchEngineWriter()
+ val invalidSearchEngineNameAmp = SearchEngine(
+ id = "id1",
+ name = "&&&example&&&",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ suggestUrl = "https://www.example.com/search-suggestion?q=%s",
+ )
+ val invalidResultUrlLessSign = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search?<q=%s"),
+ )
+ val invalidResultUrlGreaterSign = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search?>q=%s"),
+ )
+ val invalidSuggestionUrlApo = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ suggestUrl = "https://www.example.com/search-'suggestion'?q=%s",
+ )
+
+ assertNotNull(
+ writer.buildSearchEngineXML(
+ invalidSearchEngineNameAmp,
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(),
+ ),
+ )
+ assertNotNull(
+ writer.buildSearchEngineXML(
+ invalidResultUrlLessSign,
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(),
+ ),
+ )
+ assertNotNull(
+ writer.buildSearchEngineXML(
+ invalidResultUrlGreaterSign,
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(),
+ ),
+ )
+ assertNotNull(
+ writer.buildSearchEngineXML(
+ invalidSuggestionUrlApo,
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(),
+ ),
+ )
+ }
+
+ @Test
+ fun `saveSearchEngineXML returns false when failed to write to a bad file data`() {
+ val writer = spy(SearchEngineWriter())
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search?q=%s'"),
+ )
+
+ val badFile = AtomicFile(File("", ""))
+ assertFalse(writer.saveSearchEngineXML(searchEngine, badFile))
+ }
+
+ @Test
+ fun `saveSearchEngineXML returns false when there's DOMException while generating XML doc`() {
+ val storage = CustomSearchEngineStorage(testContext)
+ val writer = spy(SearchEngineWriter())
+ val searchEngine = SearchEngine(
+ id = "id1",
+ name = "example",
+ icon = mock(),
+ type = SearchEngine.Type.CUSTOM,
+ resultUrls = listOf("https://www.example.com/search?q=%s'"),
+ )
+
+ val file = storage.getSearchFile(searchEngine.id)
+ val mockDoc: Document = mock()
+ whenever(mockDoc.createElement(any())).thenThrow(DOMException(INVALID_CHARACTER_ERR, ""))
+ assertFalse(writer.saveSearchEngineXML(searchEngine, file, mockDoc))
+ }
+}
+
+private fun Document.xmlToString(): String? {
+ val writer = StringWriter()
+ try {
+ val tf = TransformerFactory.newInstance().newTransformer()
+ tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
+ tf.transform(DOMSource(this), StreamResult(writer))
+ } catch (e: TransformerConfigurationException) {
+ return null
+ } catch (e: TransformerException) {
+ return null
+ }
+
+ return writer.toString()
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/ParserTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/ParserTest.kt
new file mode 100644
index 0000000000..6d69abd67b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/ParserTest.kt
@@ -0,0 +1,149 @@
+/* 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.search.suggestions
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ParserTest {
+
+ @Test
+ fun `can parse a response from Google`() {
+ val json = "[\"firefox\",[\"firefox\",\"firefox for mac\",\"firefox quantum\",\"firefox update\",\"firefox esr\",\"firefox focus\",\"firefox addons\",\"firefox extensions\",\"firefox nightly\",\"firefox clear cache\"]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox", "firefox for mac", "firefox quantum", "firefox update", "firefox esr", "firefox focus", "firefox addons", "firefox extensions", "firefox nightly", "firefox clear cache")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Amazon`() {
+ val json = "[\"firefox\",[\"firefox for fire tv\",\"firefox movie\",\"firefox app\",\"firefox books\",\"firefox glider\",\"firefox stick\",\"firefox for firestick\",\"firefox\",\"firefox books series\",\"firefox browser\"],[{\"nodes\":[{\"name\":\"Apps & Games\",\"alias\":\"mobile-apps\"}]},{},{},{},{},{},{},{},{},{}],[],\"344I6KZL0KU9N\"]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox for fire tv", "firefox movie", "firefox app", "firefox books", "firefox glider", "firefox stick", "firefox for firestick", "firefox", "firefox books series", "firefox browser")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Azerdict`() {
+ val json = "{\"query\":\"code\",\"suggestions\":[\"code\",\"codec\",\"codex\",\"codeine\",\"code of laws\"]}"
+
+ val results = azerdictResponseParser(json)
+ val expectedResults = listOf("code", "codec", "codex", "codeine", "code of laws")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Baidu`() {
+ val json = "[\"firefox\",[\"firefox rocket\",\"firefox手机浏览器\",\"firefox是什么意思\",\"firefox focus\",\"firefox风哥\",\"firefox官网\",\"firefox吧\",\"firefox os\",\"firefox国际版\",\"firefox android\"]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox rocket", "firefox手机浏览器", "firefox是什么意思", "firefox focus", "firefox风哥", "firefox官网", "firefox吧", "firefox os", "firefox国际版", "firefox android")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Bing`() {
+ val json = "[\"firefox\",[\"firefox\",\"firefox download\",\"firefox for windows 10\",\"firefox browser\",\"firefox quantum\",\"firefox esr\",\"firefox 64-bit\",\"firefox mozilla\",\"firefox nightly\",\"firefox update\",\"firefox install\",\"firefox focus\",\"firefox beta\",\"firefox developer edition\",\"firefox portable\",\"firefox add-ons\"]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox", "firefox download", "firefox for windows 10", "firefox browser", "firefox quantum", "firefox esr", "firefox 64-bit", "firefox mozilla", "firefox nightly", "firefox update", "firefox install", "firefox focus", "firefox beta", "firefox developer edition", "firefox portable", "firefox add-ons")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Coccoc`() {
+ val json = "[\"firefox\",[\"firefox\",\"firefox tiếng việt\",\"firefox download\",\"firefox quantum\",\"firefox 51\",\"firefox 42\",\"firefox english\",\"firefox portable\",\"firefox 49\",\"firefox viet nam\"],[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"],[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"],{\"google:suggesttype\":[\"QUERY\",\"QUERY\",\"QUERY\",\"QUERY\",\"QUERY\",\"QUERY\",\"QUERY\",\"QUERY\",\"QUERY\",\"QUERY\"]}]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox", "firefox tiếng việt", "firefox download", "firefox quantum", "firefox 51", "firefox 42", "firefox english", "firefox portable", "firefox 49", "firefox viet nam")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Daum`() {
+ val json = "{\"q\":\"firefox\",\"rq\":\"firefox\",\"items\":[\"firefox\",\"firefox download\",\"firefox focus\",\"mozilla firefox\",\"firefox adobe flash\",\"firefox 삭제\",\"firefox 한글판\",\"firefox 즐겨찾기 가져오기\",\"firefox quantum\",\"firefox 57\",\"mozila firefox\",\"mozilla firefox download\",\"mozilla firefox 삭제\",\"Hacking Firefox\",\"Programming Firefox\"],\"r_items\":[]}"
+
+ val results = daumResponseParser(json)
+ val expectedResults = listOf("firefox", "firefox download", "firefox focus", "mozilla firefox", "firefox adobe flash", "firefox 삭제", "firefox 한글판", "firefox 즐겨찾기 가져오기", "firefox quantum", "firefox 57", "mozila firefox", "mozilla firefox download", "mozilla firefox 삭제", "Hacking Firefox", "Programming Firefox")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Duck Duck Go`() {
+ val json = "[\"firefox\",[\"firefox\",\"firefox browser\",\"firefox.com\",\"firefox update\",\"firefox for mac\",\"firefox quantum\",\"firefox extensions\",\"firefox esr\",\"firefox clear cache\",\"firefox themes\"]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox", "firefox browser", "firefox.com", "firefox update", "firefox for mac", "firefox quantum", "firefox extensions", "firefox esr", "firefox clear cache", "firefox themes")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Naver`() {
+ val json = "[\"firefox\",[\"firefox\",\"Mozilla Firefox\",\"firefox add-on to detect vulnerable websites\",\"firefox ak\",\"firefox as gaeilge\",\"firefox developer edition\",\"firefox down\",\"firefox for dummies\",\"firefox for mac\",\"firefox for mobile\"],[],[\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=Mozilla+Firefox\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox+add-on+to+detect+vulnerable+websites\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox+ak\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox+as+gaeilge\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox+developer+edition\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox+down\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox+for+dummies\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox+for+mac\",\"http://search.naver.com/search.naver?where=nexearch&sm=osp_sug&ie=utf8&query=firefox+for+mobile\"]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox", "Mozilla Firefox", "firefox add-on to detect vulnerable websites", "firefox ak", "firefox as gaeilge", "firefox developer edition", "firefox down", "firefox for dummies", "firefox for mac", "firefox for mobile")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Prisjakt`() {
+ val json = "[\"firefox\",[\"Firefox\",\"Firefox\",\"Mountain Equipment Firefox Pants (Herr)\",\"Mountain Equipment Firefox (Herr)\",\"Firefox (DE)\"],[\"L\\u00e4gsta pris: 149:-\",\"L\\u00e4gsta pris: 205:-\",\"L\\u00e4gsta pris: 1 559:-\",\"L\\u00e4gsta pris: 2 773:-\",\"L\\u00e4gsta pris: 198:-\"],[\"https:\\/\\/www.prisjakt.nu\\/produkt.php?p=886794\",\"https:\\/\\/www.prisjakt.nu\\/produkt.php?p=53272\",\"https:\\/\\/www.prisjakt.nu\\/produkt.php?p=3548472\",\"https:\\/\\/www.prisjakt.nu\\/produkt.php?p=1822581\",\"https:\\/\\/www.prisjakt.nu\\/produkt.php?p=3408551\"]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("Firefox", "Firefox", "Mountain Equipment Firefox Pants (Herr)", "Mountain Equipment Firefox (Herr)", "Firefox (DE)")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Qwant`() {
+ val json = "{\"status\":\"success\",\"data\":{\"items\":[{\"value\":\"firefox (video game)\",\"suggestType\":3},{\"value\":\"firefox addons\",\"suggestType\":12},{\"value\":\"firefox\",\"suggestType\":2},{\"value\":\"firefox quantum\",\"suggestType\":12},{\"value\":\"firefox focus\",\"suggestType\":12}],\"special\":[],\"availableQwick\":[]}}"
+
+ val results = qwantResponseParser(json)
+ val expectedResults = listOf("firefox (video game)", "firefox addons", "firefox", "firefox quantum", "firefox focus")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Seznam`() {
+ val json = "[\"firefox\", [\"firefox\", \"firefox ak keep it up\", \"firefox ak city to city\", \"firefox ak all those people\", \"firefox ak who can act\", \"firefox ak color the trees\", \"firefox ak habibi\", \"firefox ak zodiac\", \"firefox ak the draft\", \"mozilla firefox\"], [], []]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox", "firefox ak keep it up", "firefox ak city to city", "firefox ak all those people", "firefox ak who can act", "firefox ak color the trees", "firefox ak habibi", "firefox ak zodiac", "firefox ak the draft", "mozilla firefox")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Wikipedia`() {
+ val json = "[\"code\",[\"Code\",\"Code Geass\",\"Codeine\",\"Codename: Kids Next Door\",\"Code page\",\"Codex Sinaiticus\",\"Code talker\",\"Code Black (TV series)\",\"Codependency\",\"Codex Vaticanus\"],[\"In communications and information processing, code is a system of rules to convert information\\u2014such as a letter, word, sound, image, or gesture\\u2014into another form or representation, sometimes shortened or secret, for communication through a communication channel or storage in a storage medium.\",\"Code Geass: Lelouch of the Rebellion (\\u30b3\\u30fc\\u30c9\\u30ae\\u30a2\\u30b9 \\u53cd\\u9006\\u306e\\u30eb\\u30eb\\u30fc\\u30b7\\u30e5, K\\u014ddo Giasu: Hangyaku no Rur\\u016bshu), often referred to as simply Code Geass, is a Japanese anime series created by Sunrise, directed by Gor\\u014d Taniguchi, and written by Ichir\\u014d \\u014ckouchi, with original character designs by manga authors Clamp.\",\"Codeine is an opiate used to treat pain, as a cough medicine, and for diarrhea. It is typically used to treat mild to moderate degrees of pain.\",\"Codename: Kids Next Door, commonly abbreviated to Kids Next Door or KND, is an American animated television series created by Tom Warburton for Cartoon Network, and the 13th of the network's Cartoon Cartoons.\",\"In computing, a code page is a table of values that describes the character set used for encoding a particular set of characters, usually combined with a number of control characters.\",\"Codex Sinaiticus (Greek: \\u03a3\\u03b9\\u03bd\\u03b1\\u03ca\\u03c4\\u03b9\\u03ba\\u03cc\\u03c2 \\u039a\\u03ce\\u03b4\\u03b9\\u03ba\\u03b1\\u03c2, Hebrew: \\u05e7\\u05d5\\u05d3\\u05e7\\u05e1 \\u05e1\\u05d9\\u05e0\\u05d0\\u05d9\\u05d8\\u05d9\\u05e7\\u05d5\\u05e1\\u200e; Shelfmarks and references: London, Brit.\",\"Code talkers are people in the 20th century who used obscure languages as a means of secret communication during wartime.\",\"Code Black is an American medical drama television series created by Michael Seitzman which premiered on CBS on September 30, 2015. It takes place in an overcrowded and understaffed emergency room in Los Angeles, California, and is based on a documentary by Ryan McGarry.\",\"Codependency is a controversial concept for a dysfunctional helping relationship where one person supports or enables another person's addiction, poor mental health, immaturity, irresponsibility, or under-achievement.\",\"The Codex Vaticanus (The Vatican, Bibl. Vat., Vat. gr. 1209; no. B or 03 Gregory-Aland, \\u03b4 1 von Soden) is regarded as the oldest extant manuscript of the Greek Bible (Old and New Testament), one of the four great uncial codices.\"],[\"https://en.wikipedia.org/wiki/Code\",\"https://en.wikipedia.org/wiki/Code_Geass\",\"https://en.wikipedia.org/wiki/Codeine\",\"https://en.wikipedia.org/wiki/Codename:_Kids_Next_Door\",\"https://en.wikipedia.org/wiki/Code_page\",\"https://en.wikipedia.org/wiki/Codex_Sinaiticus\",\"https://en.wikipedia.org/wiki/Code_talker\",\"https://en.wikipedia.org/wiki/Code_Black_(TV_series)\",\"https://en.wikipedia.org/wiki/Codependency\",\"https://en.wikipedia.org/wiki/Codex_Vaticanus\"]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("Code", "Code Geass", "Codeine", "Codename: Kids Next Door", "Code page", "Codex Sinaiticus", "Code talker", "Code Black (TV series)", "Codependency", "Codex Vaticanus")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Yahoo`() {
+ val json = "[\"firefox\",[\"firefox\",\"firefox browser\",\"firefox.com\",\"firefox update\"],[],[]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox", "firefox browser", "firefox.com", "firefox update")
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `can parse a response from Yandex`() {
+ val json = "[\"firefox\",[\"firefox\",\"firefox download\",\"firefox browser\",\"firefox update\",\"firefox.com\"]]"
+
+ val results = defaultResponseParser(json)
+ val expectedResults = listOf("firefox", "firefox download", "firefox browser", "firefox update", "firefox.com")
+ assertEquals(expectedResults, results)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/SearchSuggestionClientTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/SearchSuggestionClientTest.kt
new file mode 100644
index 0000000000..3ffd14698a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/SearchSuggestionClientTest.kt
@@ -0,0 +1,110 @@
+/* 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.search.suggestions
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SearchState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.search.ext.createSearchEngine
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class SearchSuggestionClientTest {
+ companion object {
+ val GOOGLE_MOCK_RESPONSE: SearchSuggestionFetcher = { "[\"firefox\",[\"firefox\",\"firefox for mac\",\"firefox quantum\",\"firefox update\",\"firefox esr\",\"firefox focus\",\"firefox addons\",\"firefox extensions\",\"firefox nightly\",\"firefox clear cache\"]]" }
+ val QWANT_MOCK_RESPONSE: SearchSuggestionFetcher = { "{\"status\":\"success\",\"data\":{\"items\":[{\"value\":\"firefox (video game)\",\"suggestType\":3},{\"value\":\"firefox addons\",\"suggestType\":12},{\"value\":\"firefox\",\"suggestType\":2},{\"value\":\"firefox quantum\",\"suggestType\":12},{\"value\":\"firefox focus\",\"suggestType\":12}],\"special\":[],\"availableQwick\":[]}}" }
+ val SERVER_ERROR_RESPONSE: SearchSuggestionFetcher = { "Server error. Try again later" }
+ }
+
+ private val searchEngine = createSearchEngine(
+ name = "Test",
+ url = "https://localhost?q={searchTerms}",
+ suggestUrl = "https://localhost/suggestions?q={searchTerms}",
+ icon = mock(),
+ )
+
+ @Test
+ fun `Get a list of results based on the Google search engine`() = runTest {
+ val client = SearchSuggestionClient(searchEngine, GOOGLE_MOCK_RESPONSE)
+ val expectedResults = listOf("firefox", "firefox for mac", "firefox quantum", "firefox update", "firefox esr", "firefox focus", "firefox addons", "firefox extensions", "firefox nightly", "firefox clear cache")
+
+ val results = client.getSuggestions("firefox")
+
+ assertEquals(expectedResults, results)
+ }
+
+ @Test
+ fun `Get a list of results based on a non google search engine`() = runTest {
+ val qwant = createSearchEngine(
+ name = "Qwant",
+ url = "https://localhost?q={searchTerms}",
+ suggestUrl = "https://localhost/suggestions?q={searchTerms}",
+ icon = mock(),
+ )
+ val client = SearchSuggestionClient(qwant, QWANT_MOCK_RESPONSE)
+ val expectedResults = listOf("firefox (video game)", "firefox addons", "firefox", "firefox quantum", "firefox focus")
+
+ val results = client.getSuggestions("firefox")
+
+ assertEquals(expectedResults, results)
+ }
+
+ @Test(expected = SearchSuggestionClient.ResponseParserException::class)
+ fun `Check that a bad response will throw a parser exception`() = runTest {
+ val client = SearchSuggestionClient(searchEngine, SERVER_ERROR_RESPONSE)
+
+ client.getSuggestions("firefox")
+ }
+
+ @Test(expected = SearchSuggestionClient.FetchException::class)
+ fun `Check that an exception in the suggestionFetcher will re-throw an IOException`() = runTest {
+ val client = SearchSuggestionClient(searchEngine) { throw IOException() }
+
+ client.getSuggestions("firefox")
+ }
+
+ @Test
+ fun `Check that a search engine without a suggestURI will return an empty suggestion list`() = runTest {
+ val searchEngine = createSearchEngine(
+ name = "Test",
+ url = "https://localhost?q={searchTerms}",
+ icon = mock(),
+ )
+ val client = SearchSuggestionClient(searchEngine) { "no-op" }
+
+ val results = client.getSuggestions("firefox")
+
+ assertEquals(emptyList<String>(), results)
+ }
+
+ @Test
+ fun `Default search engine is used if search engine manager provided`() = runTest {
+ val store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(searchEngine),
+ ),
+ ),
+ )
+
+ val client = SearchSuggestionClient(
+ testContext,
+ store,
+ GOOGLE_MOCK_RESPONSE,
+ )
+ val expectedResults = listOf("firefox", "firefox for mac", "firefox quantum", "firefox update", "firefox esr", "firefox focus", "firefox addons", "firefox extensions", "firefox nightly", "firefox clear cache")
+
+ val results = client.getSuggestions("firefox")
+
+ assertEquals(expectedResults, results)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetryTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetryTest.kt
new file mode 100644
index 0000000000..90024bbb56
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetryTest.kt
@@ -0,0 +1,187 @@
+/* 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.search.telemetry
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class BaseSearchTelemetryTest {
+
+ private lateinit var baseTelemetry: BaseSearchTelemetry
+ private lateinit var handler: BaseSearchTelemetry.SearchTelemetryMessageHandler
+
+ @Mock
+ private lateinit var mockRepo: SerpTelemetryRepository
+
+ private val mockReadJson: () -> JSONObject = mock()
+ private val mockRootStorageDirectory: File = mock()
+
+ private fun createMockProviderList(): List<SearchProviderModel> = listOf(
+ SearchProviderModel(
+ schema = 1698656464939,
+ taggedCodes = listOf("monline_7_dg"),
+ telemetryId = "baidu",
+ organicCodes = emptyList(),
+ codeParamName = "tn",
+ queryParamNames = listOf("wd"),
+ searchPageRegexp = "^https://(?:m|www)\\\\.baidu\\\\.com/(?:s|baidu)",
+ followOnParamNames = listOf("oq"),
+ extraAdServersRegexps = listOf("^https?://www\\\\.baidu\\\\.com/baidu\\\\.php?"),
+ expectedOrganicCodes = emptyList(),
+ ),
+ )
+
+ private val rawJson = """
+ {
+ "data": [
+ {
+ "schema": 1698656464939,
+ "taggedCodes": [
+ "monline_7_dg"
+ ],
+ "telemetryId": "baidu",
+ "organicCodes": [],
+ "codeParamName": "tn",
+ "queryParamNames": [
+ "wd"
+ ],
+ "searchPageRegexp": "^https://(?:m|www)\\.baidu\\.com/(?:s|baidu)",
+ "followOnParamNames": [
+ "oq"
+ ],
+ "extraAdServersRegexps": [
+ "^https?://www\\.baidu\\.com/baidu\\.php?"
+ ],
+ "id": "19c434a3-d173-4871-9743-290ac92a3f6a",
+ "last_modified": 1698666532326
+ }],
+ "timestamp": 16
+}
+ """.trimIndent()
+
+ @Before
+ fun setup() {
+ baseTelemetry = spy(
+ object : BaseSearchTelemetry() {
+ override suspend fun install(
+ engine: Engine,
+ store: BrowserStore,
+ providerList: List<SearchProviderModel>,
+ ) {
+ // mock, do nothing
+ }
+
+ override fun processMessage(message: JSONObject) {
+ // mock, do nothing
+ }
+ },
+ )
+ handler = baseTelemetry.SearchTelemetryMessageHandler()
+ mockRepo = spy(SerpTelemetryRepository(mockRootStorageDirectory, mockReadJson, "test"))
+ }
+
+ @Test
+ fun `GIVEN an engine WHEN installWebExtension is called THEN the provided extension is installed in engine`() {
+ val engine: Engine = mock()
+ val store: BrowserStore = mock()
+ val id = "id"
+ val resourceUrl = "resourceUrl"
+ val messageId = "messageId"
+ val extensionInfo = ExtensionInfo(id, resourceUrl, messageId)
+
+ baseTelemetry.installWebExtension(engine, store, extensionInfo)
+
+ verify(engine).installBuiltInWebExtension(
+ id = eq(id),
+ url = eq(resourceUrl),
+ onSuccess = any(),
+ onError = any(),
+ )
+ }
+
+ @Test
+ fun `GIVEN a search provider does not exist for the url WHEN getProviderForUrl is called THEN return null`() {
+ val url = "https://www.mozilla.com/search?q=firefox"
+ baseTelemetry.providerList = createMockProviderList()
+
+ assertEquals(null, baseTelemetry.getProviderForUrl(url))
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `GIVEN an extension message WHEN that cannot be processed THEN throw IllegalStateException`() {
+ val message = "message"
+
+ handler.onMessage(message, mock())
+ }
+
+ @Test
+ fun `GIVEN an extension message WHEN received THEN pass it to processMessage`() {
+ val message = JSONObject()
+
+ handler.onMessage(message, mock())
+
+ verify(baseTelemetry).processMessage(message)
+ }
+
+ @Test
+ fun `GIVEN empty cacheResponse WHEN initializeProviderList is called THEN update providerList`(): Unit =
+ runBlocking {
+ val localResponse = JSONObject(rawJson)
+ val cacheResponse: Pair<ULong, List<SearchProviderModel>> = Pair(0u, emptyList())
+
+ `when`(mockRepo.loadProvidersFromCache()).thenReturn(cacheResponse)
+ doAnswer {
+ localResponse
+ }.`when`(mockReadJson)()
+
+ `when`(mockRepo.parseLocalPreinstalledData(localResponse)).thenReturn(createMockProviderList())
+ doReturn(Unit).`when`(mockRepo).fetchRemoteResponse(any())
+
+ baseTelemetry.setProviderList(mockRepo.updateProviderList())
+
+ assertEquals(baseTelemetry.providerList.toString(), createMockProviderList().toString())
+ }
+
+ @Test
+ fun `GIVEN non-empty cacheResponse WHEN initializeProviderList is called THEN update providerList`(): Unit =
+ runBlocking {
+ val localResponse = JSONObject(rawJson)
+ val cacheResponse: Pair<ULong, List<SearchProviderModel>> = Pair(123u, createMockProviderList())
+
+ `when`(mockRepo.loadProvidersFromCache()).thenReturn(cacheResponse)
+ doAnswer {
+ localResponse
+ }.`when`(mockReadJson)()
+ doReturn(Unit).`when`(mockRepo).fetchRemoteResponse(any())
+
+ baseTelemetry.setProviderList(mockRepo.updateProviderList())
+
+ assertEquals(baseTelemetry.providerList.toString(), createMockProviderList().toString())
+ }
+
+ fun getProviderForUrl(url: String): SearchProviderModel? {
+ return createMockProviderList().find { provider ->
+ provider.searchPageRegexp.pattern.toRegex().containsMatchIn(url)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SearchProviderModelTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SearchProviderModelTest.kt
new file mode 100644
index 0000000000..89e4620101
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SearchProviderModelTest.kt
@@ -0,0 +1,40 @@
+/* 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.search.telemetry
+
+import org.junit.Assert
+import org.junit.Test
+
+class SearchProviderModelTest {
+ private val testSearchProvider =
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("mzl", "813cf1dd", "16eeffc4"),
+ telemetryId = "test",
+ organicCodes = listOf(),
+ codeParamName = "tt",
+ queryParamNames = listOf("q"),
+ searchPageRegexp = "^https://www\\.ecosia\\.org/",
+ expectedOrganicCodes = listOf(),
+ extraAdServersRegexps = listOf(
+ "^https:\\/\\/www\\.bing\\.com\\/acli?c?k",
+ "^https:\\/\\/www\\.bing\\.com\\/fd\\/ls\\/GLinkPingPost\\.aspx.*acli?c?k",
+ ),
+ )
+
+ @Test
+ fun `test search provider contains ads`() {
+ val ad = "https://www.bing.com/aclick"
+ val nonAd = "https://www.bing.com/notanad"
+ Assert.assertTrue(testSearchProvider.containsAdLinks(listOf(ad, nonAd)))
+ }
+
+ @Test
+ fun `test search provider does not contain ads`() {
+ val nonAd1 = "https://www.yahoo.com/notanad"
+ val nonAd2 = "https://www.google.com/"
+ Assert.assertFalse(testSearchProvider.containsAdLinks(listOf(nonAd1, nonAd2)))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SerpTelemetryRepositoryTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SerpTelemetryRepositoryTest.kt
new file mode 100644
index 0000000000..50ae3c6aee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SerpTelemetryRepositoryTest.kt
@@ -0,0 +1,92 @@
+/* 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.search.telemetry
+
+import kotlinx.coroutines.runBlocking
+import mozilla.appservices.remotesettings.RemoteSettingsRecord
+import mozilla.appservices.remotesettings.RemoteSettingsResponse
+import mozilla.components.support.remotesettings.RemoteSettingsClient
+import mozilla.components.support.remotesettings.RemoteSettingsResult
+import mozilla.components.support.test.mock
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SerpTelemetryRepositoryTest {
+ @Mock
+ private lateinit var mockRemoteSettingsClient: RemoteSettingsClient
+
+ private lateinit var serpTelemetryRepository: SerpTelemetryRepository
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ serpTelemetryRepository = SerpTelemetryRepository(
+ rootStorageDirectory = mock(),
+ readJson = mock(),
+ collectionName = "",
+ serverUrl = "https://test.server",
+ bucketName = "",
+ )
+
+ serpTelemetryRepository.remoteSettingsClient = mockRemoteSettingsClient
+ }
+
+ @Test
+ fun `GIVEN non-empty response WHEN writeToCache is called THEN the result is a success`() = runBlocking {
+ val records = listOf(
+ RemoteSettingsRecord("1", 123u, false, null, JSONObject()),
+ RemoteSettingsRecord("2", 456u, true, null, JSONObject()),
+ )
+ val response = RemoteSettingsResponse(records, 125614567U)
+
+ `when`(mockRemoteSettingsClient.write(response))
+ .thenReturn(RemoteSettingsResult.Success(response))
+
+ val result = serpTelemetryRepository.writeToCache(response)
+
+ assertTrue(result is RemoteSettingsResult.Success)
+ assertEquals(response, (result as RemoteSettingsResult.Success).response)
+ }
+
+ @Test
+ fun `GIVEN non-empty response WHEN fetchRemoteResponse is called THEN the result is equal to the response`() = runBlocking {
+ val records = listOf(
+ RemoteSettingsRecord("1", 123u, false, null, JSONObject()),
+ RemoteSettingsRecord("2", 456u, true, null, JSONObject()),
+ )
+ val response = RemoteSettingsResponse(records, 125614567U)
+ `when`(mockRemoteSettingsClient.fetch())
+ .thenReturn(RemoteSettingsResult.Success(response))
+
+ val result = serpTelemetryRepository.fetchRemoteResponse()
+
+ assertEquals(response, result)
+ }
+
+ @Test
+ fun `GIVEN non-empty response WHEN loadProvidersFromCache is called THEN the result is equal to the response`() = runBlocking {
+ val records = listOf(
+ RemoteSettingsRecord("1", 123u, false, null, JSONObject()),
+ RemoteSettingsRecord("2", 456u, true, null, JSONObject()),
+ )
+ val response = RemoteSettingsResponse(records, 125614567U)
+ `when`(mockRemoteSettingsClient.read())
+ .thenReturn(RemoteSettingsResult.Success(response))
+
+ val result = serpTelemetryRepository.loadProvidersFromCache()
+
+ assertEquals(response.lastModified, result.first)
+ assertEquals(response.records.mapNotNull { it.fields.toSearchProviderModel() }, result.second)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetryTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetryTest.kt
new file mode 100644
index 0000000000..d804ad98fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetryTest.kt
@@ -0,0 +1,271 @@
+/* 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.search.telemetry.ads
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.search.telemetry.ExtensionInfo
+import mozilla.components.feature.search.telemetry.SearchProviderCookie
+import mozilla.components.feature.search.telemetry.SearchProviderModel
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_EXTENSION_ID
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_EXTENSION_RESOURCE_URL
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_COOKIES_KEY
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_DOCUMENT_URLS_KEY
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_ID
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_SESSION_URL_KEY
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AdsTelemetryTest {
+ private lateinit var telemetry: AdsTelemetry
+
+ fun createMockProviderList(): List<SearchProviderModel> = listOf(
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("monline_7_dg", "monline_4_dg", "monline_3_dg", "monline_dg"),
+ telemetryId = "baidu",
+ organicCodes = emptyList(),
+ codeParamName = "tn",
+ followOnParamNames = listOf("oq"),
+ queryParamNames = listOf("wd", "word"),
+ searchPageRegexp = "^https://(?:m|www)\\.baidu\\.com/(?:s|baidu)",
+ extraAdServersRegexps = listOf("^https?://www\\.baidu\\.com/baidu\\.php?"),
+ expectedOrganicCodes = emptyList(),
+ ),
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("firefox-b-m", "fpas", "lm"),
+ telemetryId = "duckduckgo",
+ organicCodes = emptyList(),
+ codeParamName = "t",
+ queryParamNames = listOf("q"),
+ searchPageRegexp = "^https:\\/\\/duckduckgo\\.com\\/",
+ extraAdServersRegexps = listOf("^https://duckduckgo.com/y\\.js?.*ad_provider\\="),
+ expectedOrganicCodes = emptyList(),
+ ),
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("firefox-b-m", "fpas", "def"),
+ telemetryId = "google",
+ organicCodes = emptyList(),
+ codeParamName = "client",
+ followOnParamNames = listOf("oq", "ved", "ei"),
+ queryParamNames = listOf("q"),
+ searchPageRegexp = "^https://www\\.google\\.(?:.+)/search",
+ extraAdServersRegexps = listOf("^https?://www\\.google(?:adservices)?\\.com/(?:pagead/)?aclk"),
+ expectedOrganicCodes = emptyList(),
+ ),
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("MOZMBA", "MOZL", "def"),
+ telemetryId = "bing",
+ organicCodes = emptyList(),
+ codeParamName = "pc",
+ queryParamNames = listOf("q"),
+ searchPageRegexp = "^https://www\\.bing\\.com/search",
+ extraAdServersRegexps = listOf("^https://www\\.bing\\.com/acli?c?k"),
+ followOnCookies = listOf(
+ SearchProviderCookie(
+ extraCodeParamName = "form",
+ extraCodePrefixes = listOf("QBRE"),
+ host = "www.bing.com",
+ name = "SRCHS",
+ codeParamName = "PC",
+ ),
+ ),
+ expectedOrganicCodes = emptyList(),
+ ),
+ )
+
+ @Before
+ fun setUp() {
+ telemetry = spy(AdsTelemetry())
+ }
+
+ @Test
+ fun `WHEN installWebExtension is called THEN install a properly configured extension`() {
+ val engine: Engine = mock()
+ val store: BrowserStore = mock()
+ val extensionCaptor = argumentCaptor<ExtensionInfo>()
+
+ runBlocking {
+ doNothing().`when`(telemetry).setProviderList(any())
+ telemetry.install(engine, store, mock())
+ }
+
+ verify(telemetry).installWebExtension(eq(engine), eq(store), extensionCaptor.capture())
+ assertEquals(ADS_EXTENSION_ID, extensionCaptor.value.id)
+ assertEquals(ADS_EXTENSION_RESOURCE_URL, extensionCaptor.value.resourceUrl)
+ assertEquals(ADS_MESSAGE_ID, extensionCaptor.value.messageId)
+ }
+
+ @Test
+ fun `WHEN checkIfAddWasClicked is called with a null session URL THEN don't emit a Fact`() {
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.checkIfAddWasClicked(null, listOf())
+
+ assertTrue(facts.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN no ads in the redirect path WHEN checkIfAddWasClicked is called THEN don't emit a Fact`() {
+ val sessionUrl = "https://www.google.com/search?q=aaa"
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.checkIfAddWasClicked(sessionUrl, listOf("https://www.aaa.com"))
+
+ assertTrue(facts.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN ads are in the redirect path WHEN checkIfAddWasClicked is called THEN emit an appropriate SERP_ADD_CLICKED Fact`() {
+ val sessionUrl = "https://www.google.com/search?q=aaa"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.checkIfAddWasClicked(
+ sessionUrl,
+ listOf("https://www.google.com/aclk", "https://www.aaa.com"),
+ )
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(AdsTelemetry.SERP_ADD_CLICKED, facts[0].item)
+ assertEquals("google.in-content.organic.none", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a message containing ad links from the extension WHEN processMessage is called THEN track a SERP_SHOWN_WITH_ADDS Fact`() {
+ val first = "https://www.google.com/aclk"
+ val second = "https://www.google.com/aaa"
+ val urls = JSONArray()
+ urls.put(first)
+ urls.put(second)
+ val cookies = JSONArray()
+ val message = JSONObject()
+ message.put(ADS_MESSAGE_DOCUMENT_URLS_KEY, urls)
+ message.put(ADS_MESSAGE_SESSION_URL_KEY, "https://www.google.com/search?q=aaa")
+ message.put(ADS_MESSAGE_COOKIES_KEY, cookies)
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.processMessage(message)
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(AdsTelemetry.SERP_SHOWN_WITH_ADDS, facts[0].item)
+ assertEquals("google.in-content.organic.none", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a message not containing ad links from the extension WHEN processMessage is called THEN don't emit any Fact`() {
+ val first = "https://www.google.com/aaaaaa"
+ val second = "https://www.google.com/aaa"
+ val urls = JSONArray()
+ urls.put(first)
+ urls.put(second)
+ val cookies = JSONArray()
+ val message = JSONObject()
+ message.put(ADS_MESSAGE_DOCUMENT_URLS_KEY, urls)
+ message.put(ADS_MESSAGE_SESSION_URL_KEY, "https://www.google.com/search?q=aaa")
+ message.put(ADS_MESSAGE_COOKIES_KEY, cookies)
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.processMessage(message)
+
+ assertTrue(facts.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN a Bing sap-follow-on with cookies WHEN checkIfAddWasClicked is called THEN emit an appropriate SERP_ADD_CLICKED Fact`() {
+ val url = "https://www.bing.com/search?q=aaa&form=QBRERANDOM"
+ telemetry.providerList = createMockProviderList()
+ telemetry.cachedCookies = createCookieList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.checkIfAddWasClicked(url, listOf("https://www.bing.com/aclik", "https://www.aaa.com"))
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(AdsTelemetry.SERP_ADD_CLICKED, facts[0].item)
+ assertEquals("bing.in-content.sap-follow-on.mozl", facts[0].value)
+ }
+
+ private fun createCookieList(): List<JSONObject> {
+ val first = JSONObject()
+ first.put("name", "SRCHS")
+ first.put("value", "PC=MOZL")
+ val second = JSONObject()
+ second.put("name", "RANDOM")
+ second.put("value", "RANDOM")
+ return listOf(first, second)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetryTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetryTest.kt
new file mode 100644
index 0000000000..2de381f11d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetryTest.kt
@@ -0,0 +1,467 @@
+/* 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.search.telemetry.incontent
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.search.telemetry.ExtensionInfo
+import mozilla.components.feature.search.telemetry.SearchProviderCookie
+import mozilla.components.feature.search.telemetry.SearchProviderModel
+import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_EXTENSION_ID
+import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_EXTENSION_RESOURCE_URL
+import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_MESSAGE_ID
+import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_MESSAGE_LIST_KEY
+import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_MESSAGE_SESSION_URL_KEY
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class InContentTelemetryTest {
+ private lateinit var telemetry: InContentTelemetry
+
+ fun createMockProviderList(): List<SearchProviderModel> = listOf(
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("monline_7_dg", "monline_4_dg", "monline_3_dg", "monline_dg"),
+ telemetryId = "baidu",
+ organicCodes = emptyList(),
+ codeParamName = "tn",
+ followOnParamNames = listOf("oq"),
+ queryParamNames = listOf("wd", "word"),
+ searchPageRegexp = "^https://(?:m|www)\\.baidu\\.com/(?:s|baidu)",
+ extraAdServersRegexps = listOf("^https?://www\\.baidu\\.com/baidu\\.php?"),
+ expectedOrganicCodes = emptyList(),
+ ),
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("firefox-b-m", "fpas", "lm"),
+ telemetryId = "example",
+ organicCodes = listOf("foo"),
+ codeParamName = "pc",
+ queryParamNames = listOf("q"),
+ searchPageRegexp = "^https:\\/\\/example\\.com\\/",
+ extraAdServersRegexps = listOf("^https://example.com/y\\\\.js?.*ad_provider\\\\="),
+ expectedOrganicCodes = emptyList(),
+ ),
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("firefox-b-m", "fpas", "lm"),
+ telemetryId = "duckduckgo",
+ organicCodes = emptyList(),
+ codeParamName = "t",
+ queryParamNames = listOf("q"),
+ searchPageRegexp = "^https:\\/\\/duckduckgo\\.com\\/",
+ extraAdServersRegexps = listOf("^https://duckduckgo.com/y\\\\.js?.*ad_provider\\\\="),
+ expectedOrganicCodes = listOf("ha"),
+ ),
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("firefox-b-m", "fpas", "def"),
+ telemetryId = "google",
+ organicCodes = emptyList(),
+ codeParamName = "client",
+ followOnParamNames = listOf("oq", "ved", "ei"),
+ queryParamNames = listOf("q"),
+ searchPageRegexp = "^https://www\\.google\\.(?:.+)/search",
+ extraAdServersRegexps = listOf("^https?://www\\\\.google(?:adservices)?\\\\.com/(?:pagead/)?aclk"),
+ expectedOrganicCodes = emptyList(),
+ ),
+ SearchProviderModel(
+ schema = 1671479978127,
+ taggedCodes = listOf("MOZ2", "MOZL", "def"),
+ telemetryId = "bing",
+ organicCodes = emptyList(),
+ codeParamName = "pc",
+ queryParamNames = listOf("q"),
+ searchPageRegexp = "^https://www\\.bing\\.com/search",
+ extraAdServersRegexps = listOf("^https://www\\\\.bing\\\\.com/acli?c?k"),
+ followOnCookies = listOf(
+ SearchProviderCookie(
+ extraCodeParamName = "form",
+ extraCodePrefixes = listOf("QBRE"),
+ host = "name",
+ name = "SRCHS",
+ codeParamName = "PC",
+ ),
+ ),
+ expectedOrganicCodes = emptyList(),
+ ),
+ )
+
+ @Before
+ fun setup() {
+ telemetry = spy(InContentTelemetry())
+ }
+
+ @Test
+ fun `WHEN installWebExtension is called THEN install a properly configured extension`() {
+ val engine: Engine = mock()
+ val store: BrowserStore = mock()
+ val extensionCaptor = argumentCaptor<ExtensionInfo>()
+
+ runBlocking {
+ doNothing().`when`(telemetry).setProviderList(any())
+ telemetry.install(engine, store, mock())
+ }
+
+ verify(telemetry).installWebExtension(eq(engine), eq(store), extensionCaptor.capture())
+ assertEquals(SEARCH_EXTENSION_ID, extensionCaptor.value.id)
+ assertEquals(SEARCH_EXTENSION_RESOURCE_URL, extensionCaptor.value.resourceUrl)
+ assertEquals(SEARCH_MESSAGE_ID, extensionCaptor.value.messageId)
+ }
+
+ @Test
+ fun `GIVEN a message from the extension WHEN processMessage is called THEN track the search`() {
+ val first = JSONObject()
+ val second = JSONObject()
+ val array = JSONArray()
+ array.put(first)
+ array.put(second)
+ val message = JSONObject()
+ val url = "https://www.google.com/search?q=aaa"
+ message.put(SEARCH_MESSAGE_LIST_KEY, array)
+ message.put(SEARCH_MESSAGE_SESSION_URL_KEY, url)
+
+ telemetry.processMessage(message)
+
+ verify(telemetry).trackPartnerUrlTypeMetric(url, listOf(first, second))
+ }
+
+ @Test
+ fun `GIVEN a Example search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://example.com/?q=aaa&pc=foo"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("example.in-content.organic.foo", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a Google search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.google.com/search?q=aaa&client=firefox-b-m"
+ telemetry.providerList = createMockProviderList()
+
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("google.in-content.sap.firefox-b-m", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a DuckDuckGo search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://duckduckgo.com/?q=aaa&t=fpas"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("duckduckgo.in-content.sap.fpas", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN an invalid Bing search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.bing.com/search?q=aaa&pc=MOZMBA"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("bing.in-content.organic.other", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a Google sap-follow-on WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.google.com/search?q=aaa&client=firefox-b-m&oq=random"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("google.in-content.sap-follow-on.firefox-b-m", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN an invalid Google sap-follow-on WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.google.com/search?q=aaa&client=firefox-b-mTesting&oq=random"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("google.in-content.organic.other", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a Google sap-follow-on from topSite WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.google.com/search?q=aaa&client=firefox-b-m&channel=ts&oq=random"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("google.in-content.sap-follow-on.firefox-b-m.ts", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN an invalid Google channel from topSite WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.google.com/search?q=aaa&client=firefox-b-m&channel=tsTest&oq=random"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("google.in-content.sap-follow-on.firefox-b-m", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a Bing sap-follow-on with cookies WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.bing.com/search?q=aaa&form=QBRERANDOM"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, createCookieList())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("bing.in-content.sap-follow-on.mozl", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a Google organic search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.google.com/search?q=aaa"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("google.in-content.organic.none", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a DuckDuckGo organic search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://duckduckgo.com/?q=aaa"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("duckduckgo.in-content.organic.none", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a DuckDuckGo organic search with expected organic code WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://duckduckgo.com/?t=ha&q=aaa"
+ val facts = mutableListOf<Fact>()
+ telemetry.providerList = createMockProviderList()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("duckduckgo.in-content.organic.none", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a Bing organic search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://www.bing.com/search?q=aaa"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("bing.in-content.organic.none", facts[0].value)
+ }
+
+ @Test
+ fun `GIVEN a Baidu organic search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() {
+ val url = "https://m.baidu.com/s?word=aaa"
+ telemetry.providerList = createMockProviderList()
+ val facts = mutableListOf<Fact>()
+ Facts.registerProcessor(
+ object : FactProcessor {
+ override fun process(fact: Fact) {
+ facts.add(fact)
+ }
+ },
+ )
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ assertEquals(1, facts.size)
+ assertEquals(Component.FEATURE_SEARCH, facts[0].component)
+ assertEquals(Action.INTERACTION, facts[0].action)
+ assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item)
+ assertEquals("baidu.in-content.organic.none", facts[0].value)
+ }
+
+ private fun createCookieList(): List<JSONObject> {
+ val first = JSONObject()
+ first.put("name", "SRCHS")
+ first.put("value", "PC=MOZL")
+ val second = JSONObject()
+ second.put("name", "RANDOM")
+ second.put("value", "RANDOM")
+ return listOf(first, second)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/AppSearchWidgetProviderTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/AppSearchWidgetProviderTest.kt
new file mode 100644
index 0000000000..b0b74bec65
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/AppSearchWidgetProviderTest.kt
@@ -0,0 +1,159 @@
+/* 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.search.widget
+
+import android.content.Context
+import mozilla.components.feature.search.R
+import mozilla.components.feature.search.widget.AppSearchWidgetProvider.Companion.getLayout
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+
+class AppSearchWidgetProviderTest {
+
+ private val testContext: Context = mock()
+
+ @Test
+ fun testGetLayoutSize() {
+ val sizes = mapOf(
+ 0 to SearchWidgetProviderSize.EXTRA_SMALL_V1,
+ 10 to SearchWidgetProviderSize.EXTRA_SMALL_V1,
+ 63 to SearchWidgetProviderSize.EXTRA_SMALL_V1,
+ 64 to SearchWidgetProviderSize.EXTRA_SMALL_V2,
+ 99 to SearchWidgetProviderSize.EXTRA_SMALL_V2,
+ 100 to SearchWidgetProviderSize.SMALL,
+ 191 to SearchWidgetProviderSize.SMALL,
+ 192 to SearchWidgetProviderSize.MEDIUM,
+ 255 to SearchWidgetProviderSize.MEDIUM,
+ 256 to SearchWidgetProviderSize.LARGE,
+ 1000 to SearchWidgetProviderSize.LARGE,
+ )
+
+ for ((dp, layoutSize) in sizes) {
+ assertEquals(layoutSize, AppSearchWidgetProvider.getLayoutSize(dp))
+ }
+ }
+
+ @Test
+ fun testGetLargeLayout() {
+ assertEquals(
+ R.layout.mozac_search_widget_large,
+ getLayout(SearchWidgetProviderSize.LARGE, showMic = false),
+ )
+ assertEquals(
+ R.layout.mozac_search_widget_large,
+ getLayout(SearchWidgetProviderSize.LARGE, showMic = true),
+ )
+ }
+
+ @Test
+ fun testGetMediumLayout() {
+ assertEquals(
+ R.layout.mozac_search_widget_medium,
+ getLayout(SearchWidgetProviderSize.MEDIUM, showMic = false),
+ )
+ assertEquals(
+ R.layout.mozac_search_widget_medium,
+ getLayout(SearchWidgetProviderSize.MEDIUM, showMic = true),
+ )
+ }
+
+ @Test
+ fun testGetSmallLayout() {
+ assertEquals(
+ R.layout.mozac_search_widget_small_no_mic,
+ getLayout(SearchWidgetProviderSize.SMALL, showMic = false),
+ )
+ assertEquals(
+ R.layout.mozac_search_widget_small,
+ getLayout(SearchWidgetProviderSize.SMALL, showMic = true),
+ )
+ }
+
+ @Test
+ fun testGetExtraSmall2Layout() {
+ assertEquals(
+ R.layout.mozac_search_widget_extra_small_v2,
+ getLayout(
+ SearchWidgetProviderSize.EXTRA_SMALL_V2,
+ showMic = false,
+ ),
+ )
+ assertEquals(
+ R.layout.mozac_search_widget_extra_small_v2,
+ getLayout(
+ SearchWidgetProviderSize.EXTRA_SMALL_V2,
+ showMic = true,
+ ),
+ )
+ }
+
+ @Test
+ fun testGetExtraSmall1Layout() {
+ assertEquals(
+ R.layout.mozac_search_widget_extra_small_v1,
+ getLayout(
+ SearchWidgetProviderSize.EXTRA_SMALL_V1,
+ showMic = false,
+ ),
+ )
+ assertEquals(
+ R.layout.mozac_search_widget_extra_small_v1,
+ getLayout(
+ SearchWidgetProviderSize.EXTRA_SMALL_V1,
+ showMic = true,
+ ),
+ )
+ }
+
+ @Test
+ fun testGetText() {
+ assertEquals(
+ testContext.getString(R.string.search_widget_text_long),
+ AppSearchWidgetProvider.getText(
+ SearchWidgetProviderSize.LARGE,
+ testContext,
+ ),
+ )
+ assertEquals(
+ testContext.getString(R.string.search_widget_text_short),
+ AppSearchWidgetProvider.getText(
+ SearchWidgetProviderSize.MEDIUM,
+ testContext,
+ ),
+ )
+ assertNull(
+ AppSearchWidgetProvider.getText(
+ SearchWidgetProviderSize.SMALL,
+ testContext,
+ ),
+ )
+ assertNull(
+ AppSearchWidgetProvider.getText(
+ SearchWidgetProviderSize.EXTRA_SMALL_V1,
+ testContext,
+ ),
+ )
+ assertNull(
+ AppSearchWidgetProvider.getText(
+ SearchWidgetProviderSize.EXTRA_SMALL_V2,
+ testContext,
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN voice search is disabled WHEN createVoiceSearchIntent is called THEN it returns null`() {
+ val appSearchWidgetProvider: AppSearchWidgetProvider =
+ mock()
+ doReturn(false).`when`(appSearchWidgetProvider).shouldShowVoiceSearch(testContext)
+
+ val result = appSearchWidgetProvider.createVoiceSearchIntent(testContext)
+
+ assertNull(result)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivityExtendedForTests.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivityExtendedForTests.kt
new file mode 100644
index 0000000000..5777364226
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivityExtendedForTests.kt
@@ -0,0 +1,24 @@
+/* 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.search.widget
+
+import androidx.core.view.MenuProvider
+import java.util.Locale
+
+class BaseVoiceSearchActivityExtendedForTests : BaseVoiceSearchActivity() {
+
+ override fun getCurrentLocale(): Locale {
+ return Locale.getDefault()
+ }
+
+ override fun onSpeechRecognitionStarted() {
+ }
+
+ override fun onSpeechRecognitionEnded(spokenText: String) {
+ }
+
+ override fun addMenuProvider(provider: MenuProvider) {
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivityTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivityTest.kt
new file mode 100644
index 0000000000..bf865e3313
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/widget/BaseVoiceSearchActivityTest.kt
@@ -0,0 +1,134 @@
+/* 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.search.widget
+
+import android.app.Activity
+import android.app.Activity.RESULT_OK
+import android.content.ComponentName
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Bundle
+import android.speech.RecognizerIntent.ACTION_RECOGNIZE_SPEECH
+import android.speech.RecognizerIntent.EXTRA_RESULTS
+import androidx.activity.result.ActivityResult
+import mozilla.components.feature.search.widget.BaseVoiceSearchActivity.Companion.PREVIOUS_INTENT
+import mozilla.components.feature.search.widget.BaseVoiceSearchActivity.Companion.SPEECH_PROCESSING
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.utils.ext.getParcelableCompat
+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.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.android.controller.ActivityController
+import org.robolectric.shadows.ShadowActivity
+
+@RunWith(RobolectricTestRunner::class)
+class BaseVoiceSearchActivityTest {
+
+ private lateinit var controller: ActivityController<BaseVoiceSearchActivityExtendedForTests>
+ private lateinit var activity: BaseVoiceSearchActivityExtendedForTests
+ private lateinit var shadow: ShadowActivity
+
+ @Before
+ fun setup() {
+ val intent = Intent()
+ intent.putExtra(SPEECH_PROCESSING, true)
+
+ controller = Robolectric.buildActivity(BaseVoiceSearchActivityExtendedForTests::class.java, intent)
+ activity = controller.get()
+ shadow = shadowOf(activity)
+ }
+
+ private fun allowVoiceIntentToResolveActivity() {
+ val shadowPackageManager = shadowOf(testContext.packageManager)
+ val component = ComponentName("com.test", "Test")
+ shadowPackageManager.addActivityIfNotPresent(component)
+ shadowPackageManager.addIntentFilterForActivity(
+ component,
+ IntentFilter(ACTION_RECOGNIZE_SPEECH).apply { addCategory(Intent.CATEGORY_DEFAULT) },
+ )
+ }
+
+ @Test
+ fun `process intent with speech processing set to true`() {
+ val intent = Intent()
+ intent.putStringArrayListExtra(EXTRA_RESULTS, ArrayList<String>(listOf("hello world")))
+ val activityResult = ActivityResult(RESULT_OK, intent)
+ controller.get().activityResultImplementation(activityResult)
+
+ assertTrue(activity.isFinishing)
+ }
+
+ @Test
+ fun `process intent with speech processing set to false`() {
+ allowVoiceIntentToResolveActivity()
+ val intent = Intent()
+ intent.putExtra(SPEECH_PROCESSING, false)
+
+ val controller = Robolectric.buildActivity(BaseVoiceSearchActivityExtendedForTests::class.java, intent)
+ val activity = controller.get()
+
+ controller.create()
+
+ assertTrue(activity.isFinishing)
+ }
+
+ @Test
+ fun `process null intent`() {
+ allowVoiceIntentToResolveActivity()
+ val controller = Robolectric.buildActivity(BaseVoiceSearchActivityExtendedForTests::class.java, null)
+ val activity = controller.get()
+
+ controller.create()
+
+ assertTrue(activity.isFinishing)
+ }
+
+ @Test
+ fun `save previous intent to instance state`() {
+ allowVoiceIntentToResolveActivity()
+ val previousIntent = Intent().apply {
+ putExtra(SPEECH_PROCESSING, true)
+ }
+ val savedInstanceState = Bundle().apply {
+ putParcelable(PREVIOUS_INTENT, previousIntent)
+ }
+ val outState = Bundle()
+
+ controller.create(savedInstanceState)
+ controller.saveInstanceState(outState)
+
+ assertEquals(previousIntent, outState.getParcelableCompat(PREVIOUS_INTENT, Intent::class.java))
+ }
+
+ @Test
+ fun `process intent with speech processing in previous intent set to true`() {
+ allowVoiceIntentToResolveActivity()
+ val savedInstanceState = Bundle()
+ val previousIntent = Intent().apply {
+ putExtra(SPEECH_PROCESSING, true)
+ }
+ savedInstanceState.putParcelable(PREVIOUS_INTENT, previousIntent)
+
+ controller.create(savedInstanceState)
+
+ assertFalse(activity.isFinishing)
+ assertNull(shadow.peekNextStartedActivityForResult())
+ }
+
+ @Test
+ fun `handle invalid result code`() {
+ val activityResult = ActivityResult(Activity.RESULT_CANCELED, Intent())
+ controller.get().activityResultImplementation(activityResult)
+
+ assertTrue(activity.isFinishing)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/search/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/search/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/search/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/search/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/search/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/search/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/serviceworker/README.md b/mobile/android/android-components/components/feature/serviceworker/README.md
new file mode 100644
index 0000000000..315ee5443c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/serviceworker/README.md
@@ -0,0 +1,30 @@
+# [Android Components](../../../README.md) > Feature > Service Worker
+
+A component for adding support for all service workers' events and callbacks.
+
+## Usage
+
+### 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-serviceworker:{latest-version}"
+```
+
+### Using it in an application
+
+This needs to be installed as high up and as soon as possible, preferable in the `android.app.Application`.
+
+```Kotlin
+ServiceWorkerSupport.install(
+ <Engine>,
+ <TabsUseCases.AddNewTabUseCase>
+)
+```
+
+## 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/serviceworker/build.gradle b/mobile/android/android-components/components/feature/serviceworker/build.gradle
new file mode 100644
index 0000000000..d0abebc584
--- /dev/null
+++ b/mobile/android/android-components/components/feature/serviceworker/build.gradle
@@ -0,0 +1,42 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.serviceworker'
+}
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':feature-tabs')
+ implementation project(':browser-state')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ testImplementation project(':support-test')
+ testImplementation project(':browser-engine-gecko')
+ 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/serviceworker/proguard-rules.pro b/mobile/android/android-components/components/feature/serviceworker/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/serviceworker/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/serviceworker/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/serviceworker/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/serviceworker/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/serviceworker/src/main/java/mozilla/components/feature/serviceworker/ServiceWorkerSupport.kt b/mobile/android/android-components/components/feature/serviceworker/src/main/java/mozilla/components/feature/serviceworker/ServiceWorkerSupport.kt
new file mode 100644
index 0000000000..e392c65e70
--- /dev/null
+++ b/mobile/android/android-components/components/feature/serviceworker/src/main/java/mozilla/components/feature/serviceworker/ServiceWorkerSupport.kt
@@ -0,0 +1,52 @@
+/* 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.serviceworker
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
+import mozilla.components.feature.tabs.TabsUseCases.AddNewTabUseCase
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Add support for all [Engine] registered service workers' events and callbacks.
+ * Call [install] to actually start observing them.
+ */
+object ServiceWorkerSupport {
+ @VisibleForTesting internal lateinit var addTabUseCase: AddNewTabUseCase
+ private val logger = Logger("ServiceWorkerSupport")
+
+ /**
+ * Start observing service workers' events and callbacks.
+ *
+ * @param engine [Engine] for which to observe service workers events and callbacks.
+ * @param addTabUseCase [AddNewTabUseCase] delegate for opening new tabs when requested by service workers.
+ */
+ fun install(
+ engine: Engine,
+ addTabUseCase: AddNewTabUseCase,
+ ) {
+ try {
+ engine.registerServiceWorkerDelegate(
+ object : ServiceWorkerDelegate {
+ override fun addNewTab(engineSession: EngineSession): Boolean {
+ addTabUseCase(
+ flags = LoadUrlFlags.external(),
+ engineSession = engineSession,
+ source = SessionState.Source.Internal.None,
+ )
+
+ return true
+ }
+ },
+ )
+ } catch (e: UnsupportedOperationException) {
+ logger.error("failed to register a service worker delegate", e)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/serviceworker/src/test/java/mozilla/components/feature/serviceworker/ServiceWorkerSupportTest.kt b/mobile/android/android-components/components/feature/serviceworker/src/test/java/mozilla/components/feature/serviceworker/ServiceWorkerSupportTest.kt
new file mode 100644
index 0000000000..7d7a5f5e1c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/serviceworker/src/test/java/mozilla/components/feature/serviceworker/ServiceWorkerSupportTest.kt
@@ -0,0 +1,54 @@
+/* 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.serviceworker
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.GeckoEngine
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.feature.tabs.TabsUseCases.AddNewTabUseCase
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mozilla.geckoview.GeckoRuntime
+
+@RunWith(AndroidJUnit4::class)
+class ServiceWorkerSupportTest {
+ @Test
+ fun `GIVEN service worker support is installed WHEN runtime is called to open a new window THEN do so and return a new EngineSession`() {
+ val runtime = GeckoRuntime.getDefault(testContext)
+ val settings = DefaultSettings()
+ val engine = GeckoEngine(testContext, runtime = runtime, defaultSettings = settings)
+ val addNewTabUseCase = mock<AddNewTabUseCase>()
+ ServiceWorkerSupport.install(engine, addNewTabUseCase)
+
+ val result = runtime.serviceWorkerDelegate!!.onOpenWindow("")
+
+ assertNotNull(result.poll(1))
+ verify(addNewTabUseCase).invoke(
+ url = eq("about:blank"),
+ selectTab = eq(true), // default
+ startLoading = eq(true), // default
+ parentId = eq(null), // default
+ flags = eq(LoadUrlFlags.external()),
+ contextId = eq(null), // default
+ engineSession = any<EngineSession>(),
+ source = eq(SessionState.Source.Internal.None),
+ searchTerms = eq(""), // default
+ private = eq(false), // default
+ historyMetadata = eq(null), // default
+ isSearch = eq(false), // default
+ searchEngineName = eq(null), // default
+ additionalHeaders = eq(null), // default
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/serviceworker/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/serviceworker/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/serviceworker/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/serviceworker/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/serviceworker/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/serviceworker/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/session/README.md b/mobile/android/android-components/components/feature/session/README.md
new file mode 100644
index 0000000000..785420bfa5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/README.md
@@ -0,0 +1,67 @@
+# [Android Components](../../../README.md) > Feature > Session
+
+A component that connects an (concept) engine implementation with the browser session module.
+A HistoryTrackingDelegate implementation is also provided, which allows tying together
+an engine implementation with a storage module.
+
+## Usage
+
+### 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-session:{latest-version}"
+```
+
+### SwipeRefreshFeature
+Sample code can be found in [Sample Browser app](https://github.com/mozilla-mobile/android-components/tree/main/samples/browser).
+
+Class to add pull to refresh functionality to browsers. You should pass it a reference to a [`SwipeRefreshLayout`](https://developer.android.com/reference/kotlin/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.html) and the SessionManager.
+
+Your layout should have a `SwipeRefreshLayout` with an `EngineView` as its only child view.
+
+```xml
+<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+ android:id="@+id/swipeRefreshLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <mozilla.components.concept.engine.EngineView
+ android:id="@+id/engineView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+```
+
+In your fragment code, use `SwipeRefreshFeature` to connect the `SwipeRefreshLayout` with your `SessionManager` and `ReloadUrlUseCase`.
+
+```kotlin
+ val feature = BrowserSwipeRefresh(sessionManager, sessionUseCases.reload, swipeRefreshLayout)
+ lifecycle.addObserver(feature)
+```
+
+`SwipeRefreshFeature` provides its own [`SwipeRefreshLayout.OnChildScrollUpCallback`](https://developer.android.com/reference/kotlin/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.OnChildScrollUpCallback.html) and [`SwipeRefreshLayout.OnRefreshListener`](https://developer.android.com/reference/kotlin/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.OnRefreshListener.html) implementations that you should not override.
+
+### ThumbnailsFeature
+
+Feature implementation for automatically taking thumbnails of sites. The feature will take a screenshot when the page finishes loading, and will add it to the `Session.thumbnail` property.
+
+```kotlin
+ val feature = ThumbnailsFeature(context, engineView, sessionManager)
+ lifecycle.addObserver(feature)
+```
+
+If the OS is under low memory conditions, the screenshot will be not taken. Ideally, this should be used in conjunction with [SessionManager.onLowMemory](https://github.com/mozilla-mobile/android-components/blob/024e3de456e3b46e9bf6718db9500ecc52da3d29/components/browser/session/src/main/java/mozilla/components/browser/session/SessionManager.kt#L472) to allow free up some `Session.thumbnail` from memory.
+
+ ```kotlin
+ // Wherever you implement ComponentCallbacks2
+ override fun onTrimMemory(level: Int) {
+ sessionManager.onLowMemory()
+ }
+```
+
+## 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/session/build.gradle b/mobile/android/android-components/components/feature/session/build.gradle
new file mode 100644
index 0000000000..3844ecba77
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/build.gradle
@@ -0,0 +1,56 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.session'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':concept-storage')
+ implementation project(':concept-toolbar')
+ implementation project(':concept-engine')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_swiperefreshlayout
+ implementation ComponentsDependencies.google_material
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-fakes')
+ testImplementation project(':support-test-libstate')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.androidx_browser
+}
+
+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/session/proguard-rules.pro b/mobile/android/android-components/components/feature/session/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/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/session/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/session/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/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/session/src/main/java/mozilla/components/feature/session/CoordinateScrollingFeature.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/CoordinateScrollingFeature.kt
new file mode 100644
index 0000000000..3d88f88c40
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/CoordinateScrollingFeature.kt
@@ -0,0 +1,72 @@
+/* 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.session
+
+import android.view.View
+import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
+import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
+import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
+import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Feature implementation for connecting an [EngineView] with any View that you want to coordinate scrolling
+ * behavior with.
+ *
+ * A use case could be collapsing a toolbar every time that the user scrolls.
+ */
+class CoordinateScrollingFeature(
+ private val store: BrowserStore,
+ private val engineView: EngineView,
+ private val view: View,
+ private val scrollFlags: Int = DEFAULT_SCROLL_FLAGS,
+) : LifecycleAwareFeature {
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Start feature: Starts adding scrolling behavior for the indicated view.
+ */
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.selectedTab }
+ .map { tab -> tab.content.loading }
+ .distinctUntilChanged()
+ .collect { onLoadingStateChanged() }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ private fun onLoadingStateChanged() {
+ val params = view.layoutParams as AppBarLayout.LayoutParams
+
+ if (engineView.canScrollVerticallyDown()) {
+ params.scrollFlags = scrollFlags
+ } else {
+ params.scrollFlags = 0
+ }
+
+ view.layoutParams = params
+ }
+
+ companion object {
+ const val DEFAULT_SCROLL_FLAGS = SCROLL_FLAG_SCROLL or
+ SCROLL_FLAG_ENTER_ALWAYS or
+ SCROLL_FLAG_SNAP or
+ SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/FullScreenFeature.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/FullScreenFeature.kt
new file mode 100644
index 0000000000..0e87260201
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/FullScreenFeature.kt
@@ -0,0 +1,97 @@
+/* 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.session
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+
+/**
+ * Feature implementation for handling fullscreen mode (exiting and back button presses).
+ */
+open class FullScreenFeature(
+ private val store: BrowserStore,
+ private val sessionUseCases: SessionUseCases,
+ private val tabId: String? = null,
+ private val viewportFitChanged: (Int) -> Unit = {},
+ private val fullScreenChanged: (Boolean) -> Unit,
+) : LifecycleAwareFeature, UserInteractionHandler {
+ private var scope: CoroutineScope? = null
+ private var observation: Observation = createDefaultObservation()
+
+ /**
+ * Starts the feature and a observer to listen for fullscreen changes.
+ */
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .map { tab -> tab.toObservation() }
+ .distinctUntilChanged()
+ .collect { observation -> onChange(observation) }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ private fun onChange(observation: Observation) {
+ if (observation.inFullScreen != this.observation.inFullScreen) {
+ fullScreenChanged(observation.inFullScreen)
+ }
+
+ if (observation.layoutInDisplayCutoutMode != this.observation.layoutInDisplayCutoutMode) {
+ viewportFitChanged(observation.layoutInDisplayCutoutMode)
+ }
+
+ this.observation = observation
+ }
+
+ /**
+ * To be called when the back button is pressed, so that only fullscreen mode closes.
+ *
+ * @return Returns true if the fullscreen mode was successfully exited; false if no effect was taken.
+ */
+ override fun onBackPressed(): Boolean {
+ val observation = observation
+
+ if (observation.inFullScreen && observation.tabId != null) {
+ sessionUseCases.exitFullscreen(observation.tabId)
+ return true
+ }
+
+ return false
+ }
+}
+
+/**
+ * Simple holder data class to keep a reference to the last values we observed.
+ */
+private data class Observation(
+ val tabId: String?,
+ val inFullScreen: Boolean,
+ val layoutInDisplayCutoutMode: Int,
+)
+
+private fun SessionState?.toObservation(): Observation {
+ return if (this != null) {
+ Observation(id, content.fullScreen, content.layoutInDisplayCutoutMode)
+ } else {
+ createDefaultObservation()
+ }
+}
+
+private fun createDefaultObservation() = Observation(
+ tabId = null,
+ inFullScreen = false,
+ layoutInDisplayCutoutMode = 0,
+)
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/HistoryDelegate.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/HistoryDelegate.kt
new file mode 100644
index 0000000000..293aa76b70
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/HistoryDelegate.kt
@@ -0,0 +1,40 @@
+/* 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.session
+
+import mozilla.components.concept.engine.history.HistoryTrackingDelegate
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.concept.storage.PageObservation
+import mozilla.components.concept.storage.PageVisit
+
+/**
+ * Implementation of the [HistoryTrackingDelegate] which delegates work to an instance of [HistoryStorage].
+ */
+class HistoryDelegate(private val historyStorage: Lazy<HistoryStorage>) : HistoryTrackingDelegate {
+ override suspend fun onVisited(uri: String, visit: PageVisit) {
+ historyStorage.value.recordVisit(uri, visit)
+ }
+
+ override suspend fun onTitleChanged(uri: String, title: String) {
+ historyStorage.value.recordObservation(uri, PageObservation(title = title))
+ }
+
+ override suspend fun onPreviewImageChange(uri: String, previewImageUrl: String) {
+ historyStorage.value.recordObservation(
+ uri,
+ PageObservation(previewImageUrl = previewImageUrl),
+ )
+ }
+
+ override suspend fun getVisited(uris: List<String>): List<Boolean> {
+ return historyStorage.value.getVisited(uris)
+ }
+
+ override suspend fun getVisited(): List<String> {
+ return historyStorage.value.getVisited()
+ }
+
+ override fun shouldStoreUri(uri: String) = historyStorage.value.canAddUri(uri)
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/PictureInPictureFeature.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/PictureInPictureFeature.kt
new file mode 100644
index 0000000000..a65629957c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/PictureInPictureFeature.kt
@@ -0,0 +1,89 @@
+/* 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.session
+
+import android.app.Activity
+import android.app.PictureInPictureParams
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.annotation.RequiresApi
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A simple implementation of Picture-in-picture mode if on a supported platform.
+ *
+ * @param store Browser Store for observing the selected session's fullscreen mode changes.
+ * @param activity the activity with the EngineView for calling PIP mode when required; the AndroidX Fragment
+ * doesn't support this.
+ * @param crashReporting Instance of `CrashReporting` to record unexpected caught exceptions
+ * @param tabId ID of tab or custom tab session.
+ */
+class PictureInPictureFeature(
+ private val store: BrowserStore,
+ private val activity: Activity,
+ private val crashReporting: CrashReporting? = null,
+ private val tabId: String? = null,
+) {
+ internal val logger = Logger("PictureInPictureFeature")
+
+ private val hasSystemFeature = SDK_INT >= Build.VERSION_CODES.N &&
+ activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+
+ fun onHomePressed(): Boolean {
+ if (!hasSystemFeature) {
+ return false
+ }
+
+ val session = store.state.findTabOrCustomTabOrSelectedTab(tabId)
+ val fullScreenMode = session?.content?.fullScreen == true
+ val contentIsPlaying = session?.mediaSessionState?.playbackState == MediaSession.PlaybackState.PLAYING
+ return fullScreenMode && contentIsPlaying && try {
+ enterPipModeCompat()
+ } catch (e: IllegalStateException) {
+ // On certain Samsung devices, if accessibility mode is enabled, this will throw an
+ // IllegalStateException even if we check for the system feature beforehand. So let's
+ // catch it, log it, and not enter PiP. See https://stackoverflow.com/q/55288858
+ logger.warn("Entering PipMode failed", e)
+ crashReporting?.submitCaughtException(e)
+ false
+ }
+ }
+
+ /**
+ * Enter Picture-in-Picture mode.
+ */
+ fun enterPipModeCompat() = when {
+ !hasSystemFeature -> false
+ SDK_INT >= Build.VERSION_CODES.O -> enterPipModeForO()
+ SDK_INT >= Build.VERSION_CODES.N -> enterPipModeForN()
+ else -> false
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun enterPipModeForO() =
+ activity.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
+
+ @Suppress("Deprecation")
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun enterPipModeForN() = run {
+ activity.enterPictureInPictureMode()
+ true
+ }
+
+ /**
+ * Should be called when the system informs you of changes to and from picture-in-picture mode.
+ * @param enabled True if the activity is in picture-in-picture mode.
+ */
+ fun onPictureInPictureModeChanged(enabled: Boolean) {
+ val sessionId = tabId ?: store.state.selectedTabId ?: return
+ store.dispatch(ContentAction.PictureInPictureChangedAction(sessionId, enabled))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/ScreenOrientationFeature.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/ScreenOrientationFeature.kt
new file mode 100644
index 0000000000..6b57b355c0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/ScreenOrientationFeature.kt
@@ -0,0 +1,37 @@
+/* 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.session
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.activity.OrientationDelegate
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Feature that automatically reacts to [Engine] requests of updating the app's screen orientation.
+ */
+class ScreenOrientationFeature(
+ private val engine: Engine,
+ private val activity: Activity,
+) : LifecycleAwareFeature, OrientationDelegate {
+ override fun start() {
+ engine.registerScreenOrientationDelegate(this)
+ }
+
+ override fun stop() {
+ engine.unregisterScreenOrientationDelegate()
+ }
+
+ override fun onOrientationLock(requestedOrientation: Int): Boolean {
+ activity.requestedOrientation = requestedOrientation
+ return true
+ }
+
+ override fun onOrientationUnlock() {
+ // As indicated by GeckoView - https://bugzilla.mozilla.org/show_bug.cgi?id=1744101#c3
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SessionFeature.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SessionFeature.kt
new file mode 100644
index 0000000000..9dae8d3aac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SessionFeature.kt
@@ -0,0 +1,67 @@
+/* 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.session
+
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.session.engine.EngineViewPresenter
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+
+/**
+ * Feature implementation for connecting the engine module with the session module.
+ */
+class SessionFeature(
+ private val store: BrowserStore,
+ private val goBackUseCase: SessionUseCases.GoBackUseCase,
+ private val engineView: EngineView,
+ private val tabId: String? = null,
+) : LifecycleAwareFeature, UserInteractionHandler {
+ internal val presenter = EngineViewPresenter(store, engineView, tabId)
+
+ /**
+ * Start feature: App is in the foreground.
+ */
+ override fun start() {
+ presenter.start()
+ }
+
+ /**
+ * Handler for back pressed events in activities that use this feature.
+ *
+ * @return true if the event was handled, otherwise false.
+ */
+ override fun onBackPressed(): Boolean {
+ val tab = store.state.findTabOrCustomTabOrSelectedTab(tabId)
+
+ if (engineView.canClearSelection()) {
+ engineView.clearSelection()
+ return true
+ } else if (tab?.content?.canGoBack == true) {
+ goBackUseCase(tab.id)
+ return true
+ }
+
+ return false
+ }
+
+ /**
+ * Stop feature: App is in the background.
+ */
+ override fun stop() {
+ presenter.stop()
+ }
+
+ /**
+ * Stops the feature from rendering sessions on the [EngineView] (until explicitly started again)
+ * and releases an already rendering session from the [EngineView].
+ */
+ fun release() {
+ // Once we fully migrated to BrowserStore we may be able to get rid of the need for cleanup().
+ // See https://github.com/mozilla-mobile/android-components/issues/7657
+ presenter.stop()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SessionUseCases.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SessionUseCases.kt
new file mode 100644
index 0000000000..565ba34632
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SessionUseCases.kt
@@ -0,0 +1,544 @@
+/* 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.session
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.LastAccessAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.TranslationsAction
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.concept.engine.translate.TranslationOptions
+
+/**
+ * Contains use cases related to the session feature.
+ */
+class SessionUseCases(
+ store: BrowserStore,
+ onNoTab: (String) -> TabSessionState = { url ->
+ createTab(url).apply { store.dispatch(TabListAction.AddTabAction(this)) }
+ },
+) {
+
+ /**
+ * Contract for use cases that load a provided URL.
+ */
+ interface LoadUrlUseCase {
+ /**
+ * Loads the provided URL using the currently selected session.
+ */
+ fun invoke(
+ url: String,
+ flags: LoadUrlFlags = LoadUrlFlags.none(),
+ additionalHeaders: Map<String, String>? = null,
+ )
+ }
+
+ class DefaultLoadUrlUseCase internal constructor(
+ private val store: BrowserStore,
+ private val onNoTab: (String) -> TabSessionState,
+ ) : LoadUrlUseCase {
+
+ /**
+ * Loads the provided URL using the currently selected session. If
+ * there's no selected session a new session will be created using
+ * [onNoTab].
+ *
+ * @param url The URL to be loaded using the selected session.
+ * @param flags The [LoadUrlFlags] to use when loading the provided url.
+ * @param additionalHeaders the extra headers to use when loading the provided url.
+ */
+ override operator fun invoke(
+ url: String,
+ flags: LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {
+ this.invoke(url, store.state.selectedTabId, flags, additionalHeaders)
+ }
+
+ /**
+ * Loads the provided URL using the specified session. If no session
+ * is provided the currently selected session will be used. If there's
+ * no selected session a new session will be created using [onNoTab].
+ *
+ * @param url The URL to be loaded using the provided session.
+ * @param sessionId the ID of the session for which the URL should be loaded.
+ * @param flags The [LoadUrlFlags] to use when loading the provided url.
+ * @param additionalHeaders the extra headers to use when loading the provided url.
+ */
+ operator fun invoke(
+ url: String,
+ sessionId: String? = null,
+ flags: LoadUrlFlags = LoadUrlFlags.none(),
+ additionalHeaders: Map<String, String>? = null,
+ ) {
+ val loadSessionId = sessionId
+ ?: store.state.selectedTabId
+ ?: onNoTab.invoke(url).id
+
+ val tab = store.state.findTabOrCustomTab(loadSessionId)
+ val engineSession = tab?.engineState?.engineSession
+
+ // If we already have an engine session load Url directly to prevent
+ // context switches.
+ if (engineSession != null) {
+ val parentEngineSession = if (tab is TabSessionState) {
+ tab.parentId?.let { store.state.findTabOrCustomTab(it)?.engineState?.engineSession }
+ } else {
+ null
+ }
+ engineSession.loadUrl(
+ url = url,
+ parent = parentEngineSession,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ )
+ // Update the url in content immediately until the engine updates with any new changes to the state.
+ store.dispatch(
+ ContentAction.UpdateUrlAction(
+ loadSessionId,
+ url,
+ ),
+ )
+ store.dispatch(
+ EngineAction.OptimizedLoadUrlTriggeredAction(
+ loadSessionId,
+ url,
+ flags,
+ additionalHeaders,
+ ),
+ )
+ } else {
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ loadSessionId,
+ url,
+ flags,
+ additionalHeaders,
+ ),
+ )
+ }
+ }
+ }
+
+ class LoadDataUseCase internal constructor(
+ private val store: BrowserStore,
+ private val onNoTab: (String) -> TabSessionState,
+ ) {
+ /**
+ * Loads the provided data based on the mime type using the provided session (or the
+ * currently selected session if none is provided).
+ */
+ operator fun invoke(
+ data: String,
+ mimeType: String,
+ encoding: String = "UTF-8",
+ tabId: String? = store.state.selectedTabId,
+ ) {
+ val loadTabId = tabId ?: onNoTab.invoke("about:blank").id
+
+ store.dispatch(
+ EngineAction.LoadDataAction(
+ loadTabId,
+ data,
+ mimeType,
+ encoding,
+ ),
+ )
+ }
+ }
+
+ class ReloadUrlUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Reloads the current URL of the provided session (or the currently
+ * selected session if none is provided).
+ *
+ * @param tabId the ID of the tab for which the reload should be triggered.
+ * @param flags the [LoadUrlFlags] to use when reloading the given session.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ flags: LoadUrlFlags = LoadUrlFlags.none(),
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(
+ EngineAction.ReloadAction(
+ tabId,
+ flags,
+ ),
+ )
+ }
+ }
+
+ class StopLoadingUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Stops the current URL of the provided session from loading.
+ *
+ * @param tabId the ID of the tab for which loading should be stopped.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.state.findTabOrCustomTab(tabId)
+ ?.engineState
+ ?.engineSession
+ ?.stopLoading()
+ }
+ }
+
+ class GoBackUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Navigates back in the history of the currently selected tab
+ *
+ * @param userInteraction informs the engine whether the action was user invoked.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ userInteraction: Boolean = true,
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(
+ EngineAction.GoBackAction(
+ tabId,
+ userInteraction,
+ ),
+ )
+ }
+ }
+
+ class GoForwardUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Navigates forward in the history of the currently selected session
+ *
+ * @param userInteraction informs the engine whether the action was user invoked.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ userInteraction: Boolean = true,
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(
+ EngineAction.GoForwardAction(
+ tabId,
+ userInteraction,
+ ),
+ )
+ }
+ }
+
+ /**
+ * Use case to jump to an arbitrary history index in a session's backstack.
+ */
+ class GoToHistoryIndexUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Navigates to a specific index in the [HistoryState] of the given session.
+ * Invalid index values will be ignored.
+ *
+ * @param index the index in the session's [HistoryState] to navigate to.
+ * @param session the session whose [HistoryState] is being accessed, defaulting
+ * to the selected session.
+ */
+ operator fun invoke(
+ index: Int,
+ tabId: String? = store.state.selectedTabId,
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(
+ EngineAction.GoToHistoryIndexAction(
+ tabId,
+ index,
+ ),
+ )
+ }
+ }
+
+ class RequestDesktopSiteUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Requests the desktop version of the current session and reloads the page.
+ */
+ operator fun invoke(
+ enable: Boolean,
+ tabId: String? = store.state.selectedTabId,
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(
+ EngineAction.ToggleDesktopModeAction(
+ tabId,
+ enable,
+ ),
+ )
+ }
+ }
+
+ class ExitFullScreenUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Exits fullscreen mode of the current session.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(
+ EngineAction.ExitFullScreenModeAction(
+ tabId,
+ ),
+ )
+ }
+ }
+
+ /**
+ * Tries to recover from a crash by restoring the last know state.
+ */
+ class CrashRecoveryUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Tries to recover the state of all crashed sessions.
+ */
+ fun invoke() {
+ val tabIds = store.state.let {
+ it.tabs + it.customTabs
+ }.filter {
+ it.engineState.crashed
+ }.map {
+ it.id
+ }
+
+ return invoke(tabIds)
+ }
+
+ /**
+ * Tries to recover the state of all sessions.
+ */
+ fun invoke(tabIds: List<String>) {
+ tabIds.forEach { tabId ->
+ store.dispatch(
+ CrashAction.RestoreCrashedSessionAction(tabId),
+ )
+ }
+ }
+ }
+
+ /**
+ * UseCase for purging the (back and forward) history of all tabs and custom tabs.
+ */
+ class PurgeHistoryUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Purges the (back and forward) history of all tabs and custom tabs.
+ */
+ operator fun invoke() {
+ store.dispatch(EngineAction.PurgeHistoryAction)
+ }
+ }
+
+ /**
+ * Sets the [TabSessionState.lastAccess] timestamp of the provided tab. This timestamp
+ * is updated automatically by our EngineViewPresenter and LastAccessMiddleware, but
+ * there are app-specific flows where this can't happen automatically e.g., the app
+ * being resumed to the home screen despite having a selected tab. In this case, the app
+ * may want to update the last access timestamp of the selected tab.
+ *
+ * It will likely make sense to support finer-grained timestamps in the future so applications
+ * can differentiate viewing from tab selection for instance.s
+ */
+ class UpdateLastAccessUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Updates [TabSessionState.lastAccess] of the tab with the provided ID. Note that this
+ * method has no effect in case the tab doesn't exist or is a custom tab.
+ *
+ * @param tabId the ID of the tab to update, defaults to the ID of the currently selected tab.
+ * @param lastAccess the timestamp to set [TabSessionState.lastAccess] to, defaults to now.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ lastAccess: Long = System.currentTimeMillis(),
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(
+ LastAccessAction.UpdateLastAccessAction(
+ tabId,
+ lastAccess,
+ ),
+ )
+ }
+ }
+
+ /**
+ * A use case for requesting a given tab to generate a PDF from it's content.
+ */
+ class SaveToPdfUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Request a PDF to be generated from the given [tabId].
+ *
+ * If the tab is not loaded, [BrowserStore] will ensure the session has been created and
+ * loaded, however, this does not guarantee the page contents will be correctly painted
+ * into the PDF. Typically, a session is required to have been painted on the screen (by
+ * being the selected tab) for a PDF to be generated successfully.
+ *
+ * ⚠️ Make sure to have a middleware that handles the [EngineAction.SaveToPdfExceptionAction]`,
+ * or your application will crash when an error happens when
+ * requesting a page to be saved a PDF.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(EngineAction.SaveToPdfAction(tabId))
+ }
+ }
+
+ /**
+ * A use case for requesting a given tab to print it's content.
+ */
+ class PrintContentUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Request that Android print the current [tabId].
+ *
+ * The same caveats in the [SaveToPdfUseCase] apply here because the Engine makes a PDF prior
+ * to sending the print request on to the Android print spooler. This means the session should
+ * have been painted first to successfully make a PDF.
+ *
+ * ⚠️ Make sure to have a middleware that handles the [EngineAction.PrintContentExceptionAction]`
+ * to handle print errors. Handling [EngineAction.PrintContentCompletedAction] is only necessary for
+ * telemetry or if any extra actions need to be completed.
+ *
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ ) {
+ if (tabId == null) {
+ return
+ }
+
+ store.dispatch(EngineAction.PrintContentAction(tabId))
+ }
+ }
+
+ /**
+ * A use case for requesting a given tab's content to be translated.
+ */
+ class TranslateUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Request that Android translate the content of the current [tabId].
+ *
+ * Typically, a session is required to have been painted on the screen (by
+ * being the selected tab) for a translation to occur successfully.
+ *
+ * @param tabId The [tabId] associated with the request.
+ * @param fromLanguage The BCP 47 language tag that the page should be translated from.
+ * @param toLanguage The BCP 47 language tag that the page should be translated to.
+ * @param options Options for how the translation should be processed.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ fromLanguage: String,
+ toLanguage: String,
+ options: TranslationOptions?,
+ ) {
+ if (tabId == null) {
+ return
+ }
+ store.dispatch(TranslationsAction.TranslateAction(tabId, fromLanguage, toLanguage, options))
+ }
+ }
+
+ /**
+ * A use case for requesting a given tab's content be restored after a translation.
+ */
+ class TranslateRestoreUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Request that the translations engine restore the translated content of the current
+ * [tabId] back to the original.
+ *
+ * Will be a no-op, if there is nothing to restore.
+ *
+ * @param tabId The [tabId] associated with the request.
+ */
+ operator fun invoke(
+ tabId: String? = store.state.selectedTabId,
+ ) {
+ if (tabId == null) {
+ return
+ }
+ store.dispatch(TranslationsAction.TranslateRestoreAction(tabId))
+ }
+ }
+
+ val loadUrl: DefaultLoadUrlUseCase by lazy { DefaultLoadUrlUseCase(store, onNoTab) }
+ val loadData: LoadDataUseCase by lazy { LoadDataUseCase(store, onNoTab) }
+ val reload: ReloadUrlUseCase by lazy { ReloadUrlUseCase(store) }
+ val stopLoading: StopLoadingUseCase by lazy { StopLoadingUseCase(store) }
+ val goBack: GoBackUseCase by lazy { GoBackUseCase(store) }
+ val goForward: GoForwardUseCase by lazy { GoForwardUseCase(store) }
+ val goToHistoryIndex: GoToHistoryIndexUseCase by lazy { GoToHistoryIndexUseCase(store) }
+ val requestDesktopSite: RequestDesktopSiteUseCase by lazy { RequestDesktopSiteUseCase(store) }
+ val exitFullscreen: ExitFullScreenUseCase by lazy { ExitFullScreenUseCase(store) }
+ val saveToPdf: SaveToPdfUseCase by lazy { SaveToPdfUseCase(store) }
+ val printContent: PrintContentUseCase by lazy { PrintContentUseCase(store) }
+ val translate: TranslateUseCase by lazy { TranslateUseCase(store) }
+ val translateRestore: TranslateRestoreUseCase by lazy { TranslateRestoreUseCase(store) }
+ val crashRecovery: CrashRecoveryUseCase by lazy { CrashRecoveryUseCase(store) }
+ val purgeHistory: PurgeHistoryUseCase by lazy { PurgeHistoryUseCase(store) }
+ val updateLastAccess: UpdateLastAccessUseCase by lazy { UpdateLastAccessUseCase(store) }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SettingsUseCases.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SettingsUseCases.kt
new file mode 100644
index 0000000000..0183cf728c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SettingsUseCases.kt
@@ -0,0 +1,56 @@
+/* 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.session
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.Settings
+
+/**
+ * Contains use cases related to engine [Settings].
+ *
+ * @param engine reference to the application's browser [Engine].
+ * @param store the application's [BrowserStore].
+ */
+class SettingsUseCases(
+ engine: Engine,
+ store: BrowserStore,
+) {
+ /**
+ * Updates the tracking protection policy to the given policy value when invoked.
+ * All active sessions are automatically updated with the new policy.
+ */
+ class UpdateTrackingProtectionUseCase internal constructor(
+ private val engine: Engine,
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Updates the tracking protection policy for all current and future [EngineSession]
+ * instances.
+ */
+ operator fun invoke(policy: TrackingProtectionPolicy) {
+ engine.settings.trackingProtectionPolicy = policy
+
+ store.state.forEachEngineSession { engineSession ->
+ engineSession.updateTrackingProtection(policy)
+ }
+
+ engine.clearSpeculativeSession()
+ }
+ }
+
+ val updateTrackingProtection: UpdateTrackingProtectionUseCase by lazy {
+ UpdateTrackingProtectionUseCase(engine, store)
+ }
+}
+
+private fun BrowserState.forEachEngineSession(block: (EngineSession) -> Unit) {
+ (tabs + customTabs)
+ .mapNotNull { it.engineState.engineSession }
+ .map { block(it) }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SwipeRefreshFeature.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SwipeRefreshFeature.kt
new file mode 100644
index 0000000000..4668777378
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/SwipeRefreshFeature.kt
@@ -0,0 +1,92 @@
+/* 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.session
+
+import android.view.View
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.action.ContentAction.UpdateRefreshCanceledStateAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
+
+/**
+ * Feature implementation to add pull to refresh functionality to browsers.
+ *
+ * @param swipeRefreshLayout Reference to SwipeRefreshLayout that has an [EngineView] as its child.
+ */
+class SwipeRefreshFeature(
+ private val store: BrowserStore,
+ private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
+ private val swipeRefreshLayout: SwipeRefreshLayout,
+ private val onRefreshCallback: (() -> Unit)? = null,
+ private val tabId: String? = null,
+) : LifecycleAwareFeature,
+ SwipeRefreshLayout.OnChildScrollUpCallback,
+ SwipeRefreshLayout.OnRefreshListener {
+ private var scope: CoroutineScope? = null
+
+ init {
+ swipeRefreshLayout.setOnRefreshListener(this)
+ swipeRefreshLayout.setOnChildScrollUpCallback(this)
+ }
+
+ /**
+ * Start feature: Starts adding pull to refresh behavior for the active session.
+ */
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .ifAnyChanged {
+ arrayOf(it?.content?.loading, it?.content?.refreshCanceled)
+ }
+ .collect { tab ->
+ tab?.let {
+ if (!tab.content.loading || tab.content.refreshCanceled) {
+ swipeRefreshLayout.isRefreshing = false
+ if (tab.content.refreshCanceled) {
+ // In case the user tries to refresh again
+ // we need to reset refreshCanceled, to be able to
+ // get a subsequent event.
+ store.dispatch(UpdateRefreshCanceledStateAction(tab.id, false))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ /**
+ * Callback that checks whether it is possible for the child view to scroll up.
+ * If the child view cannot scroll up and the scroll event is not handled by the webpage
+ * it means we need to trigger the pull down to refresh functionality.
+ */
+ @Suppress("Deprecation")
+ override fun canChildScrollUp(parent: SwipeRefreshLayout, child: View?) =
+ if (child is EngineView) {
+ !child.getInputResultDetail().canOverscrollTop()
+ } else {
+ true
+ }
+
+ /**
+ * Called when a swipe gesture triggers a refresh.
+ */
+ override fun onRefresh() {
+ onRefreshCallback?.invoke()
+ store.state.findTabOrCustomTabOrSelectedTab(tabId)?.let { tab ->
+ reloadUrlUseCase(tab.id)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/TrackingProtectionUseCases.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/TrackingProtectionUseCases.kt
new file mode 100644
index 0000000000..403885d68d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/TrackingProtectionUseCases.kt
@@ -0,0 +1,194 @@
+/* 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.session
+
+import androidx.core.net.toUri
+import mozilla.components.browser.state.action.TrackingProtectionAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.content.blocking.TrackerLog
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
+import mozilla.components.support.base.log.logger.Logger
+import java.lang.Exception
+
+/**
+ * Contains use cases related to the tracking protection.
+ *
+ * @param store the application's [BrowserStore].
+ * @param engine the application's [Engine].
+ */
+class TrackingProtectionUseCases(
+ val store: BrowserStore,
+ val engine: Engine,
+) {
+
+ /**
+ * Use case for adding a new tab to the exception list.
+ */
+ class AddExceptionUseCase internal constructor(
+ private val store: BrowserStore,
+ private val engine: Engine,
+ ) {
+ private val logger = Logger("TrackingProtectionUseCases")
+
+ /**
+ * Adds a new tab to the exception list, as a result this tab will not get applied any
+ * tracking protection policy.
+ * @param tabId The id of the tab that will be added to the exception list.
+ * @param persistInPrivateMode Indicates if the exception should be persistent in private mode
+ * defaults to false.
+ */
+ operator fun invoke(tabId: String, persistInPrivateMode: Boolean = false) {
+ val engineSession = store.state.findTabOrCustomTabOrSelectedTab(tabId)?.engineState?.engineSession
+ ?: return logger.error("The engine session should not be null")
+
+ engine.trackingProtectionExceptionStore.add(engineSession, persistInPrivateMode)
+ }
+ }
+
+ /**
+ * Use case for removing a tab or a [TrackingProtectionException] from the exception list.
+ */
+ class RemoveExceptionUseCase internal constructor(
+ private val store: BrowserStore,
+ private val engine: Engine,
+ ) {
+ private val logger = Logger("TrackingProtectionUseCases")
+
+ /**
+ * Removes a tab from the exception list.
+ * @param tabId The id of the tab that will be removed from the exception list.
+ */
+ operator fun invoke(tabId: String) {
+ val engineSession = store.state.findTabOrCustomTabOrSelectedTab(tabId)?.engineState?.engineSession
+ ?: return logger.error("The engine session should not be null")
+
+ engine.trackingProtectionExceptionStore.remove(engineSession)
+ }
+
+ /**
+ * Removes a [exception] from the exception list.
+ * @param exception The [TrackingProtectionException] that will be removed from the exception list.
+ */
+ operator fun invoke(exception: TrackingProtectionException) {
+ engine.trackingProtectionExceptionStore.remove(exception)
+ // Find all tabs that need to update their tracking protection status.
+ val tabs = (store.state.tabs + store.state.customTabs).filter { tab ->
+ val tabDomain = tab.content.url.toUri().host
+ val exceptionDomain = exception.url.toUri().host
+ tabDomain == exceptionDomain
+ }
+ tabs.forEach {
+ store.dispatch(TrackingProtectionAction.ToggleExclusionListAction(it.id, false))
+ }
+ }
+ }
+
+ /**
+ * Use case for removing all tabs from the exception list.
+ */
+ class RemoveAllExceptionsUseCase internal constructor(
+ private val store: BrowserStore,
+ private val engine: Engine,
+ ) {
+ /**
+ * Removes all domains from the exception list.
+ */
+ operator fun invoke(onRemove: () -> Unit = {}) {
+ val engineSessions = (store.state.tabs + store.state.customTabs).mapNotNull { tab ->
+ tab.engineState.engineSession
+ }
+
+ engine.trackingProtectionExceptionStore.removeAll(engineSessions, onRemove)
+ }
+ }
+
+ /**
+ * Use case for verifying if a tab is in the exception list.
+ */
+ class ContainsExceptionUseCase internal constructor(
+ private val store: BrowserStore,
+ private val engine: Engine,
+ ) {
+ /**
+ * Indicates if a given tab is in the exception list.
+ * @param tabId The id of the tab to verify.
+ * @param onResult A callback to inform if the given tab is on
+ * the exception list, true if it is in otherwise false.
+ */
+ operator fun invoke(
+ tabId: String,
+ onResult: (Boolean) -> Unit,
+ ) {
+ val engineSession = store.state.findTabOrCustomTabOrSelectedTab(tabId)?.engineState?.engineSession
+ ?: return onResult(false)
+
+ engine.trackingProtectionExceptionStore.contains(engineSession, onResult)
+ }
+ }
+
+ /**
+ * Use case for fetching all exceptions in the exception list.
+ */
+ class FetchExceptionsUseCase internal constructor(
+ private val engine: Engine,
+ ) {
+ /**
+ * Fetch all domains that will be ignored for tracking protection.
+ * @param onResult A callback to inform that the domains on the exception list has been fetched,
+ * it provides a list of [TrackingProtectionException] that are on the exception list, if there are none domains
+ * on the exception list, an empty list will be provided.
+ */
+ operator fun invoke(onResult: (List<TrackingProtectionException>) -> Unit) {
+ engine.trackingProtectionExceptionStore.fetchAll(onResult)
+ }
+ }
+
+ /**
+ * Use case for fetching all the tracking protection logged information.
+ */
+ class FetchTrackingLogUserCase internal constructor(
+ private val store: BrowserStore,
+ private val engine: Engine,
+ ) {
+ /**
+ * Fetch all the tracking protection logged information of a given tab.
+ *
+ * @param tabId the id of the tab for which loading should be stopped.
+ * @param onSuccess callback invoked if the data was fetched successfully.
+ * @param onError (optional) callback invoked if fetching the data caused an exception.
+ */
+ operator fun invoke(
+ tabId: String,
+ onSuccess: (List<TrackerLog>) -> Unit,
+ onError: (Throwable) -> Unit,
+ ) {
+ val engineSession = store.state.findTabOrCustomTabOrSelectedTab(tabId)?.engineState?.engineSession
+ ?: return onError(Exception("The engine session should not be null"))
+
+ engine.getTrackersLog(engineSession, onSuccess, onError)
+ }
+ }
+
+ val fetchTrackingLogs: FetchTrackingLogUserCase by lazy {
+ FetchTrackingLogUserCase(store, engine)
+ }
+ val addException: AddExceptionUseCase by lazy {
+ AddExceptionUseCase(store, engine)
+ }
+ val removeException: RemoveExceptionUseCase by lazy {
+ RemoveExceptionUseCase(store, engine)
+ }
+ val containsException: ContainsExceptionUseCase by lazy {
+ ContainsExceptionUseCase(store, engine)
+ }
+ val removeAllExceptions: RemoveAllExceptionsUseCase by lazy {
+ RemoveAllExceptionsUseCase(store, engine)
+ }
+ val fetchExceptions: FetchExceptionsUseCase by lazy {
+ FetchExceptionsUseCase(engine)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/engine/EngineViewPresenter.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/engine/EngineViewPresenter.kt
new file mode 100644
index 0000000000..af440358da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/engine/EngineViewPresenter.kt
@@ -0,0 +1,92 @@
+/* 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.session.engine
+
+import android.view.View
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.LastAccessAction
+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.EngineView
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
+
+/**
+ * Presenter implementation for EngineView.
+ */
+internal class EngineViewPresenter(
+ private val store: BrowserStore,
+ private val engineView: EngineView,
+ private val tabId: String?,
+) {
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Start presenter and display data in view.
+ */
+ fun start() {
+ scope = store.flowScoped { flow ->
+ flow.map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ // Render if the tab itself changed and when an engine session is linked
+ .ifAnyChanged { tab ->
+ arrayOf(
+ tab?.id,
+ tab?.engineState?.engineSession,
+ tab?.engineState?.crashed,
+ tab?.content?.firstContentfulPaint,
+ )
+ }
+ .collect { tab -> onTabToRender(tab) }
+ }
+ }
+
+ /**
+ * Stop presenter from updating view.
+ */
+ fun stop() {
+ scope?.cancel()
+ engineView.release()
+ }
+
+ private fun onTabToRender(tab: SessionState?) {
+ if (tab == null) {
+ engineView.release()
+ } else {
+ renderTab(tab)
+ }
+ }
+
+ private fun renderTab(tab: SessionState) {
+ val engineSession = tab.engineState.engineSession
+
+ val actualView = engineView.asView()
+
+ if (tab.engineState.crashed) {
+ engineView.release()
+ return
+ }
+
+ if (tab.content.firstContentfulPaint) {
+ actualView.visibility = View.VISIBLE
+ }
+
+ if (engineSession == null) {
+ // This tab does not have an EngineSession that we can render yet. Let's dispatch an
+ // action to request creating one. Once one was created and linked to this session, this
+ // method will get invoked again.
+ store.dispatch(EngineAction.CreateEngineSessionAction(tab.id))
+ } else {
+ // Since we render the tab again let's update its last access flag. In the future, we
+ // may need more fine-grained flags to differentiate viewing from tab selection.
+ store.dispatch(LastAccessAction.UpdateLastAccessAction(tab.id, System.currentTimeMillis()))
+ engineView.render(engineSession)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/LastAccessMiddleware.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/LastAccessMiddleware.kt
new file mode 100644
index 0000000000..c1a90dc0d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/LastAccessMiddleware.kt
@@ -0,0 +1,78 @@
+/* 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.session.middleware
+
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.LastAccessAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+
+/**
+ * [Middleware] that handles updating the [TabSessionState.lastAccess] when a tab is selected.
+ */
+class LastAccessMiddleware : Middleware<BrowserState, BrowserAction> {
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ // Since tab removal can affect tab selection we save the
+ // selected tab ID before removal to determine if it changed.
+ val selectionBeforeRemoval = when (action) {
+ is TabListAction.RemoveTabAction,
+ is TabListAction.RemoveTabsAction,
+ // NB: RemoveAllNormalTabsAction and RemoveAllPrivateTabsAction never update tab selection
+ -> {
+ context.state.selectedTabId
+ }
+ else -> null
+ }
+
+ next(action)
+
+ when (action) {
+ is TabListAction.RemoveTabAction,
+ is TabListAction.RemoveTabsAction,
+ // NB: RemoveAllNormalTabsAction and RemoveAllPrivateTabsAction never updates tab selection
+ -> {
+ // If the selected tab changed during removal we make sure to update
+ // the lastAccess state of the newly selected tab.
+ val newSelection = context.state.selectedTabId
+ if (newSelection != null && newSelection != selectionBeforeRemoval) {
+ context.dispatchUpdateActionForId(newSelection)
+ }
+ }
+ is TabListAction.SelectTabAction -> {
+ context.dispatchUpdateActionForId(action.tabId)
+ }
+ is TabListAction.AddTabAction -> {
+ if (action.select) {
+ context.dispatchUpdateActionForId(action.tab.id)
+ }
+ }
+ is TabListAction.RestoreAction -> {
+ action.selectedTabId?.let {
+ context.dispatchUpdateActionForId(it)
+ }
+ }
+ is ContentAction.UpdateUrlAction -> {
+ if (action.sessionId == context.state.selectedTabId) {
+ context.dispatchUpdateActionForId(action.sessionId)
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+
+ private fun MiddlewareContext<BrowserState, BrowserAction>.dispatchUpdateActionForId(id: String) {
+ dispatch(LastAccessAction.UpdateLastAccessAction(id, System.currentTimeMillis()))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/undo/UndoMiddleware.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/undo/UndoMiddleware.kt
new file mode 100644
index 0000000000..694f1d67d0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/undo/UndoMiddleware.kt
@@ -0,0 +1,153 @@
+/* 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.session.middleware.undo
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.UndoAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.normalTabs
+import mozilla.components.browser.state.selector.privateTabs
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.toRecoverableTab
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+import mozilla.components.support.base.log.logger.Logger
+import java.util.UUID
+import mozilla.components.support.base.coroutines.Dispatchers as MozillaDispatchers
+
+/**
+ * [Middleware] implementation that adds removed tabs to [BrowserState.undoHistory] for a short
+ * amount of time ([clearAfterMillis]). Dispatching [UndoAction.RestoreRecoverableTabs] will restore
+ * the tabs from [BrowserState.undoHistory].
+ */
+class UndoMiddleware(
+ private val clearAfterMillis: Long = 5000, // For comparison: a LENGTH_LONG Snackbar takes 2750.
+ private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main),
+ private val waitScope: CoroutineScope = CoroutineScope(MozillaDispatchers.Cached),
+) : Middleware<BrowserState, BrowserAction> {
+ private val logger = Logger("UndoMiddleware")
+ private var clearJob: Job? = null
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ val state = context.state
+
+ when (action) {
+ // Remember removed tabs
+ is TabListAction.RemoveAllNormalTabsAction -> onTabsRemoved(
+ context,
+ state.normalTabs,
+ state.selectedTabId,
+ )
+ is TabListAction.RemoveAllPrivateTabsAction -> onTabsRemoved(
+ context,
+ state.privateTabs,
+ state.selectedTabId,
+ )
+ is TabListAction.RemoveAllTabsAction -> {
+ if (action.recoverable) {
+ onTabsRemoved(context, state.tabs, state.selectedTabId)
+ }
+ }
+ is TabListAction.RemoveTabAction -> state.findTab(action.tabId)?.let {
+ onTabsRemoved(context, listOf(it), state.selectedTabId)
+ }
+ is TabListAction.RemoveTabsAction -> {
+ action.tabIds.mapNotNull { state.findTab(it) }.let {
+ onTabsRemoved(context, it, state.selectedTabId)
+ }
+ }
+
+ // Restore
+ is UndoAction.RestoreRecoverableTabs -> restore(context.store, context.state)
+
+ // Do nothing when an action different from above is passed in.
+ else -> { }
+ }
+
+ next(action)
+ }
+
+ private fun onTabsRemoved(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ tabs: List<SessionState>,
+ selectedTabId: String?,
+ ) {
+ clearJob?.cancel()
+
+ val recoverableTabs = mutableListOf<RecoverableTab>()
+ tabs.forEach { tab ->
+ if (tab is TabSessionState) {
+ val index = context.state.tabs.indexOfFirst { it.id == tab.id }
+ recoverableTabs.add(tab.toRecoverableTab(index))
+ }
+ }
+
+ if (recoverableTabs.isEmpty()) {
+ logger.debug("No recoverable tabs to add to undo history.")
+ return
+ }
+
+ val tag = UUID.randomUUID().toString()
+
+ val selectionToRestore = selectedTabId?.let {
+ recoverableTabs.find { it.state.id == selectedTabId }?.state?.id
+ }
+
+ context.dispatch(
+ UndoAction.AddRecoverableTabs(tag, recoverableTabs, selectionToRestore),
+ )
+
+ val store = context.store
+
+ clearJob = waitScope.launch {
+ delay(clearAfterMillis)
+ store.dispatch(UndoAction.ClearRecoverableTabs(tag))
+ }
+ }
+
+ private fun restore(
+ store: Store<BrowserState, BrowserAction>,
+ state: BrowserState,
+ ) = mainScope.launch {
+ clearJob?.cancel()
+
+ // Since we have to restore into SessionManager (until we can nuke it from orbit and only use BrowserStore),
+ // this is a bit crude. For example we do not restore into the previous position. The goal is to make this
+ // nice once we can restore directly into BrowserState.
+
+ val undoHistory = state.undoHistory
+ val tabs = undoHistory.tabs
+ if (tabs.isEmpty()) {
+ logger.debug("No recoverable tabs for undo.")
+ return@launch
+ }
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs,
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX,
+ ),
+ )
+
+ // Restore the previous selection if needed.
+ undoHistory.selectedTabId?.let { tabId ->
+ store.dispatch(TabListAction.SelectTabAction(tabId))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/CoordinateScrollingFeatureTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/CoordinateScrollingFeatureTest.kt
new file mode 100644
index 0000000000..f4442a9d23
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/CoordinateScrollingFeatureTest.kt
@@ -0,0 +1,89 @@
+/* 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.session
+
+import android.os.Looper.getMainLooper
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.material.appbar.AppBarLayout
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.session.CoordinateScrollingFeature.Companion.DEFAULT_SCROLL_FLAGS
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class CoordinateScrollingFeatureTest {
+
+ private lateinit var scrollFeature: CoordinateScrollingFeature
+ private lateinit var mockEngineView: EngineView
+ private lateinit var mockView: View
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setup() {
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ mockEngineView = mock()
+ mockView = mock()
+ scrollFeature = CoordinateScrollingFeature(store, mockEngineView, mockView)
+
+ whenever(mockView.layoutParams).thenReturn(mock<AppBarLayout.LayoutParams>())
+ }
+
+ @Test
+ fun `when session loading StateChanged and engine canScrollVertically is false must remove scrollFlags`() {
+ scrollFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ store.dispatch(ContentAction.UpdateLoadingStateAction("mozilla", true)).joinBlocking()
+
+ verify((mockView.layoutParams as AppBarLayout.LayoutParams)).scrollFlags = 0
+ verify(mockView).layoutParams = any()
+ }
+
+ @Test
+ fun `when session loading StateChanged and engine canScrollVertically is true must add DEFAULT_SCROLL_FLAGS `() {
+ whenever(mockEngineView.canScrollVerticallyDown()).thenReturn(true)
+
+ scrollFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ store.dispatch(ContentAction.UpdateLoadingStateAction("mozilla", true)).joinBlocking()
+
+ verify((mockView.layoutParams as AppBarLayout.LayoutParams)).scrollFlags = DEFAULT_SCROLL_FLAGS
+ verify(mockView).layoutParams = any()
+ }
+
+ @Test
+ fun `when session loading StateChanged and engine canScrollVertically is true must add custom scrollFlags`() {
+ whenever(mockEngineView.canScrollVerticallyDown()).thenReturn(true)
+ scrollFeature = CoordinateScrollingFeature(store, mockEngineView, mockView, 12)
+ scrollFeature.start()
+ shadowOf(getMainLooper()).idle()
+
+ store.dispatch(ContentAction.UpdateLoadingStateAction("mozilla", true)).joinBlocking()
+
+ verify((mockView.layoutParams as AppBarLayout.LayoutParams)).scrollFlags = 12
+ verify(mockView).layoutParams = any()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/FullScreenFeatureTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/FullScreenFeatureTest.kt
new file mode 100644
index 0000000000..ca95692827
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/FullScreenFeatureTest.kt
@@ -0,0 +1,465 @@
+/* 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.session
+
+import android.view.WindowManager
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+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.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class FullScreenFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `Starting without tabs`() {
+ var viewPort: Int? = null
+ var fullscreen: Boolean? = null
+
+ val store = BrowserStore()
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = mock(),
+ tabId = null,
+ viewportFitChanged = { value -> viewPort = value },
+ fullScreenChanged = { value -> fullscreen = value },
+ )
+
+ feature.start()
+ store.waitUntilIdle()
+
+ assertNull(viewPort)
+ assertNull(fullscreen)
+ }
+
+ @Test
+ fun `Starting with selected tab will not invoke callbacks with default state`() {
+ var viewPort: Int? = null
+ var fullscreen: Boolean? = null
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
+ selectedTabId = "A",
+ ),
+ )
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = mock(),
+ tabId = null,
+ viewportFitChanged = { value -> viewPort = value },
+ fullScreenChanged = { value -> fullscreen = value },
+ )
+
+ feature.start()
+ store.waitUntilIdle()
+
+ assertNull(viewPort)
+ assertNull(fullscreen)
+ }
+
+ @Test
+ fun `Starting with selected tab`() {
+ var viewPort: Int? = null
+ var fullscreen: Boolean? = null
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
+ selectedTabId = "A",
+ ),
+ )
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "A",
+ true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ "A",
+ 42,
+ ),
+ ).joinBlocking()
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = mock(),
+ tabId = null,
+ viewportFitChanged = { value -> viewPort = value },
+ fullScreenChanged = { value -> fullscreen = value },
+ )
+
+ feature.start()
+ store.waitUntilIdle()
+
+ assertEquals(42, viewPort)
+ assertTrue(fullscreen!!)
+ }
+
+ @Test
+ fun `Selected tab switching to fullscreen mode`() {
+ var viewPort: Int? = null
+ var fullscreen: Boolean? = null
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
+ selectedTabId = "A",
+ ),
+ )
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = mock(),
+ tabId = null,
+ viewportFitChanged = { value -> viewPort = value },
+ fullScreenChanged = { value -> fullscreen = value },
+ )
+
+ feature.start()
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "A",
+ true,
+ ),
+ ).joinBlocking()
+
+ assertNull(viewPort)
+ assertTrue(fullscreen!!)
+ }
+
+ @Test
+ fun `Selected tab changing viewport`() {
+ var viewPort: Int? = null
+ var fullscreen: Boolean? = null
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
+ selectedTabId = "A",
+ ),
+ )
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = mock(),
+ tabId = null,
+ viewportFitChanged = { value -> viewPort = value },
+ fullScreenChanged = { value -> fullscreen = value },
+ )
+
+ feature.start()
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "A",
+ true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ "A",
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,
+ ),
+ ).joinBlocking()
+
+ assertNotEquals(0, viewPort)
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, viewPort)
+ assertTrue(fullscreen!!)
+ }
+
+ @Test
+ fun `Fixed tab switching to fullscreen mode and back`() {
+ var viewPort: Int? = null
+ var fullscreen: Boolean? = null
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "A"),
+ createTab("https://www.firefox.com", id = "B"),
+ createTab("https://getpocket.com", id = "C"),
+ ),
+ selectedTabId = "A",
+ ),
+ )
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = mock(),
+ tabId = "B",
+ viewportFitChanged = { value -> viewPort = value },
+ fullScreenChanged = { value -> fullscreen = value },
+ )
+
+ feature.start()
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "B",
+ true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ "B",
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,
+ ),
+ ).joinBlocking()
+
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, viewPort)
+ assertTrue(fullscreen!!)
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "B",
+ false,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ "B",
+ 0,
+ ),
+ ).joinBlocking()
+
+ assertEquals(0, viewPort)
+ assertFalse(fullscreen!!)
+ }
+
+ @Test
+ fun `Callback functions no longer get invoked when stopped, but get new value on next start`() {
+ var viewPort: Int? = null
+ var fullscreen: Boolean? = null
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "A"),
+ createTab("https://www.firefox.com", id = "B"),
+ createTab("https://getpocket.com", id = "C"),
+ ),
+ selectedTabId = "A",
+ ),
+ )
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = mock(),
+ tabId = "B",
+ viewportFitChanged = { value -> viewPort = value },
+ fullScreenChanged = { value -> fullscreen = value },
+ )
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "B",
+ true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ "B",
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, viewPort)
+ assertTrue(fullscreen!!)
+
+ feature.stop()
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "B",
+ false,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ "B",
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,
+ ),
+ ).joinBlocking()
+
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, viewPort)
+ assertTrue(fullscreen!!)
+
+ feature.start()
+
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, viewPort)
+ assertFalse(fullscreen!!)
+ }
+
+ @Test
+ fun `onBackPressed will invoke usecase for active fullscreen mode`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "A"),
+ createTab("https://www.firefox.com", id = "B"),
+ createTab("https://getpocket.com", id = "C"),
+ ),
+ selectedTabId = "A",
+ ),
+ )
+
+ val exitUseCase: SessionUseCases.ExitFullScreenUseCase = mock()
+ val useCases: SessionUseCases = mock()
+ doReturn(exitUseCase).`when`(useCases).exitFullscreen
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = useCases,
+ tabId = "B",
+ fullScreenChanged = {},
+ )
+
+ feature.start()
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "B",
+ true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ "B",
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,
+ ),
+ ).joinBlocking()
+
+ assertTrue(feature.onBackPressed())
+
+ verify(exitUseCase).invoke("B")
+ }
+
+ @Test
+ fun `Fullscreen tab gets removed`() {
+ var viewPort: Int? = null
+ var fullscreen: Boolean? = null
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
+ selectedTabId = "A",
+ ),
+ )
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = mock(),
+ tabId = null,
+ viewportFitChanged = { value -> viewPort = value },
+ fullScreenChanged = { value -> fullscreen = value },
+ )
+
+ feature.start()
+
+ store.dispatch(
+ ContentAction.FullScreenChangedAction(
+ "A",
+ true,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ ContentAction.ViewportFitChangedAction(
+ "A",
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,
+ ),
+ ).joinBlocking()
+
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, viewPort)
+ assertTrue(fullscreen!!)
+
+ store.dispatch(
+ TabListAction.RemoveTabAction(tabId = "A"),
+ ).joinBlocking()
+
+ assertEquals(0, viewPort)
+ assertFalse(fullscreen!!)
+ }
+
+ @Test
+ fun `onBackPressed will not invoke usecase if not in fullscreen mode`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "A"),
+ createTab("https://www.firefox.com", id = "B"),
+ createTab("https://getpocket.com", id = "C"),
+ ),
+ selectedTabId = "A",
+ ),
+ )
+
+ val exitUseCase: SessionUseCases.ExitFullScreenUseCase = mock()
+ val useCases: SessionUseCases = mock()
+ doReturn(exitUseCase).`when`(useCases).exitFullscreen
+
+ val feature = FullScreenFeature(
+ store = store,
+ sessionUseCases = useCases,
+ fullScreenChanged = {},
+ )
+
+ feature.start()
+
+ assertFalse(feature.onBackPressed())
+
+ verify(exitUseCase, never()).invoke("B")
+ }
+
+ @Test
+ fun `onBackPressed getting invoked without any tabs to observe`() {
+ val exitUseCase: SessionUseCases.ExitFullScreenUseCase = mock()
+ val useCases: SessionUseCases = mock()
+ doReturn(exitUseCase).`when`(useCases).exitFullscreen
+
+ val feature = FullScreenFeature(
+ store = BrowserStore(),
+ sessionUseCases = useCases,
+ fullScreenChanged = {},
+ )
+
+ // Invoking onBackPressed without fullscreen mode
+ assertFalse(feature.onBackPressed())
+
+ verify(exitUseCase, never()).invoke(ArgumentMatchers.anyString())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/HistoryDelegateTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/HistoryDelegateTest.kt
new file mode 100644
index 0000000000..8506e345b5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/HistoryDelegateTest.kt
@@ -0,0 +1,191 @@
+/* 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.session
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.storage.FrecencyThresholdOption
+import mozilla.components.concept.storage.HistoryStorage
+import mozilla.components.concept.storage.PageObservation
+import mozilla.components.concept.storage.PageVisit
+import mozilla.components.concept.storage.SearchResult
+import mozilla.components.concept.storage.TopFrecentSiteInfo
+import mozilla.components.concept.storage.VisitInfo
+import mozilla.components.concept.storage.VisitType
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.concept.toolbar.AutocompleteResult
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class HistoryDelegateTest {
+
+ @Test
+ fun `history delegate passes through onVisited calls`() = runTest {
+ val storage = mock<HistoryStorage>()
+ val delegate = HistoryDelegate(lazy { storage })
+
+ delegate.onVisited("http://www.mozilla.org", PageVisit(VisitType.LINK))
+ verify(storage).recordVisit("http://www.mozilla.org", PageVisit(VisitType.LINK))
+
+ delegate.onVisited("http://www.firefox.com", PageVisit(VisitType.RELOAD))
+ verify(storage).recordVisit("http://www.firefox.com", PageVisit(VisitType.RELOAD))
+
+ delegate.onVisited("http://www.firefox.com", PageVisit(VisitType.BOOKMARK))
+ verify(storage).recordVisit("http://www.firefox.com", PageVisit(VisitType.BOOKMARK))
+ }
+
+ @Test
+ fun `history delegate passes through onTitleChanged calls`() = runTest {
+ val storage = mock<HistoryStorage>()
+ val delegate = HistoryDelegate(lazy { storage })
+
+ delegate.onTitleChanged("http://www.mozilla.org", "Mozilla")
+ verify(storage).recordObservation("http://www.mozilla.org", PageObservation("Mozilla"))
+ }
+
+ @Test
+ fun `history delegate passes through onPreviewImageChange calls`() = runTest {
+ val storage = mock<HistoryStorage>()
+ val delegate = HistoryDelegate(lazy { storage })
+
+ val previewImageUrl = "https://test.com/og-image-url"
+ delegate.onPreviewImageChange("http://www.mozilla.org", previewImageUrl)
+ verify(storage).recordObservation(
+ "http://www.mozilla.org",
+ PageObservation(previewImageUrl = previewImageUrl),
+ )
+ }
+
+ @Test
+ fun `history delegate passes through getVisited calls`() = runTest {
+ val storage = TestHistoryStorage()
+ val delegate = HistoryDelegate(lazy { storage })
+
+ assertFalse(storage.getVisitedPlainCalled)
+ assertFalse(storage.getVisitedListCalled)
+ assertFalse(storage.canAddUriCalled)
+
+ delegate.getVisited()
+ assertTrue(storage.getVisitedPlainCalled)
+ assertFalse(storage.getVisitedListCalled)
+ assertFalse(storage.canAddUriCalled)
+
+ delegate.getVisited(listOf("http://www.mozilla.org", "http://www.firefox.com"))
+ assertTrue(storage.getVisitedListCalled)
+ assertFalse(storage.canAddUriCalled)
+ }
+
+ @Test
+ fun `history delegate checks with storage canAddUriCalled`() = runTest {
+ val storage = TestHistoryStorage()
+ val delegate = HistoryDelegate(lazy { storage })
+
+ assertFalse(storage.canAddUriCalled)
+ delegate.shouldStoreUri("test")
+ assertTrue(storage.canAddUriCalled)
+ }
+
+ private class TestHistoryStorage : HistoryStorage, AutocompleteProvider {
+ var getVisitedListCalled = false
+ var getVisitedPlainCalled = false
+ var canAddUriCalled = false
+
+ override suspend fun warmUp() {
+ fail()
+ }
+
+ override suspend fun recordVisit(uri: String, visit: PageVisit) {}
+
+ override suspend fun recordObservation(uri: String, observation: PageObservation) {}
+
+ override fun canAddUri(uri: String): Boolean {
+ canAddUriCalled = true
+ return true
+ }
+
+ override suspend fun getVisited(uris: List<String>): List<Boolean> {
+ getVisitedListCalled = true
+ assertEquals(listOf("http://www.mozilla.org", "http://www.firefox.com"), uris)
+ return emptyList()
+ }
+
+ override suspend fun getVisited(): List<String> {
+ getVisitedPlainCalled = true
+ return emptyList()
+ }
+
+ override suspend fun getDetailedVisits(start: Long, end: Long, excludeTypes: List<VisitType>): List<VisitInfo> {
+ fail()
+ return emptyList()
+ }
+
+ override suspend fun getVisitsPaginated(offset: Long, count: Long, excludeTypes: List<VisitType>): List<VisitInfo> {
+ fail()
+ return emptyList()
+ }
+
+ override suspend fun getTopFrecentSites(
+ numItems: Int,
+ frecencyThreshold: FrecencyThresholdOption,
+ ): List<TopFrecentSiteInfo> {
+ fail()
+ return emptyList()
+ }
+
+ override fun getSuggestions(query: String, limit: Int): List<SearchResult> {
+ fail()
+ return listOf()
+ }
+
+ override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
+ fail()
+ return null
+ }
+
+ override suspend fun deleteEverything() {
+ fail()
+ }
+
+ override suspend fun deleteVisitsSince(since: Long) {
+ fail()
+ }
+
+ override suspend fun deleteVisitsBetween(startTime: Long, endTime: Long) {
+ fail()
+ }
+
+ override suspend fun deleteVisitsFor(url: String) {
+ fail()
+ }
+
+ override suspend fun deleteVisit(url: String, timestamp: Long) {
+ fail()
+ }
+
+ override suspend fun runMaintenance(dbSizeLimit: UInt) {
+ fail()
+ }
+
+ override fun cleanup() {
+ fail()
+ }
+
+ override fun cancelWrites() {
+ fail()
+ }
+
+ override fun cancelReads() {
+ fail()
+ }
+
+ override fun cancelReads(nextQuery: String) {
+ fail()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/PictureInPictureFeatureTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/PictureInPictureFeatureTest.kt
new file mode 100644
index 0000000000..dd25a303dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/PictureInPictureFeatureTest.kt
@@ -0,0 +1,317 @@
+/* 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.session
+
+import android.app.Activity
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.MediaSessionState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.mediasession.MediaSession
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+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.verifyNoInteractions
+import org.mockito.verification.VerificationMode
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class PictureInPictureFeatureTest {
+
+ private val crashReporting: CrashReporting = mock()
+ private val activity: Activity = Mockito.mock(Activity::class.java, Mockito.RETURNS_DEEP_STUBS)
+
+ @Before
+ fun setUp() {
+ whenever(activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
+ .thenReturn(true)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `on home pressed without system feature on android m and lower`() {
+ val store = mock<BrowserStore>()
+ whenever(activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
+ .thenReturn(false)
+
+ val pictureInPictureFeature =
+ spy(PictureInPictureFeature(store, activity, crashReporting))
+
+ assertFalse(pictureInPictureFeature.onHomePressed())
+ verifyNoInteractions(store)
+ verifyNoInteractions(activity.packageManager)
+ verify(pictureInPictureFeature, never()).enterPipModeCompat()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `on home pressed without system feature on android n and above`() {
+ val store = mock<BrowserStore>()
+ whenever(activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
+ .thenReturn(false)
+
+ val pictureInPictureFeature =
+ spy(PictureInPictureFeature(store, activity, crashReporting))
+
+ assertFalse(pictureInPictureFeature.onHomePressed())
+ verifyNoInteractions(store)
+ verify(activity.packageManager).hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+ verify(pictureInPictureFeature, never()).enterPipModeCompat()
+ }
+
+ @Test
+ fun `on home pressed without a selected session`() {
+ val store = BrowserStore()
+ val pictureInPictureFeature =
+ spy(PictureInPictureFeature(store, activity, crashReporting))
+
+ assertFalse(pictureInPictureFeature.onHomePressed())
+ verify(activity.packageManager).hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+ verify(pictureInPictureFeature, never()).enterPipModeCompat()
+ }
+
+ @Test
+ fun `on home pressed with a selected session without a fullscreen mode`() {
+ val selectedSession = createTab("https://mozilla.org").copyWithFullScreen(false)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(selectedSession),
+ selectedTabId = selectedSession.id,
+ ),
+ )
+ val pictureInPictureFeature =
+ spy(PictureInPictureFeature(store, activity, crashReporting))
+
+ assertFalse(selectedSession.content.fullScreen)
+ assertFalse(pictureInPictureFeature.onHomePressed())
+ verify(pictureInPictureFeature, never()).enterPipModeCompat()
+ }
+
+ @Test
+ fun `on home pressed with a selected session in fullscreen without media playing and without pip mode`() {
+ val controller = mock<MediaSession.Controller>()
+ val selectedSession = createTab(
+ url = "https://mozilla.org",
+ mediaSessionState = MediaSessionState(
+ playbackState = MediaSession.PlaybackState.UNKNOWN,
+ controller = controller,
+ ),
+ ).copyWithFullScreen(true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(selectedSession),
+ selectedTabId = selectedSession.id,
+ ),
+ )
+ val pictureInPictureFeature =
+ spy(PictureInPictureFeature(store, activity, crashReporting))
+
+ doReturn(false).`when`(pictureInPictureFeature).enterPipModeCompat()
+
+ assertFalse(selectedSession.mediaSessionState?.playbackState == MediaSession.PlaybackState.PLAYING)
+ assertFalse(pictureInPictureFeature.onHomePressed())
+ verify(pictureInPictureFeature, never()).enterPipModeCompat()
+ }
+
+ @Test
+ fun `on home pressed with a selected session in fullscreen with media playing and without pip mode`() {
+ val controller = mock<MediaSession.Controller>()
+ val selectedSession = createTab(
+ url = "https://mozilla.org",
+ mediaSessionState = MediaSessionState(
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ controller = controller,
+ ),
+ ).copyWithFullScreen(true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(selectedSession),
+ selectedTabId = selectedSession.id,
+ ),
+ )
+ val pictureInPictureFeature =
+ spy(PictureInPictureFeature(store, activity, crashReporting))
+
+ doReturn(false).`when`(pictureInPictureFeature).enterPipModeCompat()
+
+ assertTrue(selectedSession.mediaSessionState?.playbackState == MediaSession.PlaybackState.PLAYING)
+ assertFalse(pictureInPictureFeature.onHomePressed())
+ verify(pictureInPictureFeature).enterPipModeCompat()
+ }
+
+ @Test
+ fun `on home pressed with a selected session in fullscreen without media playing and with pip mode`() {
+ val controller = mock<MediaSession.Controller>()
+ val selectedSession = createTab(
+ url = "https://mozilla.org",
+ mediaSessionState = MediaSessionState(
+ playbackState = MediaSession.PlaybackState.UNKNOWN,
+ controller = controller,
+ ),
+ ).copyWithFullScreen(true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(selectedSession),
+ selectedTabId = selectedSession.id,
+ ),
+ )
+ val pictureInPictureFeature =
+ spy(PictureInPictureFeature(store, activity, crashReporting))
+
+ doReturn(true).`when`(pictureInPictureFeature).enterPipModeCompat()
+
+ assertFalse(selectedSession.mediaSessionState?.playbackState == MediaSession.PlaybackState.PLAYING)
+ assertFalse(pictureInPictureFeature.onHomePressed())
+ verify(pictureInPictureFeature, never()).enterPipModeCompat()
+ }
+
+ @Test
+ fun `on home pressed with a selected session in fullscreen with media playing and with pip mode`() {
+ val controller = mock<MediaSession.Controller>()
+ val selectedSession = createTab(
+ url = "https://mozilla.org",
+ mediaSessionState = MediaSessionState(
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ controller = controller,
+ ),
+ ).copyWithFullScreen(true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(selectedSession),
+ selectedTabId = selectedSession.id,
+ ),
+ )
+ val pictureInPictureFeature =
+ spy(PictureInPictureFeature(store, activity, crashReporting))
+
+ doReturn(true).`when`(pictureInPictureFeature).enterPipModeCompat()
+
+ assertTrue(selectedSession.mediaSessionState?.playbackState == MediaSession.PlaybackState.PLAYING)
+ assertTrue(pictureInPictureFeature.onHomePressed())
+ verify(pictureInPictureFeature).enterPipModeCompat()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `enter pip mode compat on android m and below`() {
+ val store = mock<BrowserStore>()
+ val pictureInPictureFeature = PictureInPictureFeature(store, activity, crashReporting)
+
+ assertFalse(pictureInPictureFeature.enterPipModeCompat())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O])
+ fun `enter pip mode compat without system feature on android o`() {
+ whenever(activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
+ .thenReturn(false)
+
+ val pictureInPictureFeature =
+ PictureInPictureFeature(mock(), activity, crashReporting)
+
+ assertFalse(pictureInPictureFeature.enterPipModeCompat())
+ verify(activity, never()).enterPictureInPictureMode(any())
+ verifyDeprecatedPictureInPictureMode(activity, never())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O])
+ fun `enter pip mode compat with system feature on android o but entering throws exception`() {
+ val controller = mock<MediaSession.Controller>()
+ val selectedSession = createTab(
+ url = "https://mozilla.org",
+ mediaSessionState = MediaSessionState(
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ controller = controller,
+ ),
+ ).copyWithFullScreen(true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(selectedSession),
+ selectedTabId = selectedSession.id,
+ ),
+ )
+
+ whenever(activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
+ .thenReturn(true)
+ whenever(activity.enterPictureInPictureMode(any())).thenThrow(IllegalStateException())
+
+ val pictureInPictureFeature =
+ PictureInPictureFeature(store, activity, crashReporting)
+ assertFalse(pictureInPictureFeature.onHomePressed())
+ verify(crashReporting).submitCaughtException(any<IllegalStateException>())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O])
+ fun `enter pip mode compat on android o and above`() {
+ val pictureInPictureFeature =
+ PictureInPictureFeature(mock(), activity, crashReporting)
+
+ whenever(activity.enterPictureInPictureMode(any())).thenReturn(true)
+
+ assertTrue(pictureInPictureFeature.enterPipModeCompat())
+ verify(activity).enterPictureInPictureMode(any())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `enter pip mode compat on android n and above`() {
+ val pictureInPictureFeature =
+ PictureInPictureFeature(mock(), activity, crashReporting)
+
+ assertTrue(pictureInPictureFeature.enterPipModeCompat())
+ verifyDeprecatedPictureInPictureMode(activity)
+ }
+
+ @Test
+ fun `on pip mode changed`() {
+ val store = mock<BrowserStore>()
+ val pipFeature = PictureInPictureFeature(
+ store,
+ activity,
+ crashReporting,
+ tabId = "tab-id",
+ )
+
+ pipFeature.onPictureInPictureModeChanged(true)
+ verify(store).dispatch(ContentAction.PictureInPictureChangedAction("tab-id", true))
+
+ pipFeature.onPictureInPictureModeChanged(false)
+ verify(store).dispatch(ContentAction.PictureInPictureChangedAction("tab-id", false))
+
+ verify(activity, never()).enterPictureInPictureMode(any())
+ verifyDeprecatedPictureInPictureMode(activity, never())
+ }
+
+ @Suppress("Deprecation")
+ private fun verifyDeprecatedPictureInPictureMode(
+ activity: Activity,
+ mode: VerificationMode = times(1),
+ ) {
+ verify(activity, mode).enterPictureInPictureMode()
+ }
+
+ @Suppress("Unchecked_Cast")
+ private fun <T : SessionState> T.copyWithFullScreen(fullScreen: Boolean): T =
+ createCopy(content = content.copy(fullScreen = fullScreen)) as T
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/ScreenOrientationFeatureTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/ScreenOrientationFeatureTest.kt
new file mode 100644
index 0000000000..4135b6d39b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/ScreenOrientationFeatureTest.kt
@@ -0,0 +1,56 @@
+/* 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.session
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import mozilla.components.concept.engine.Engine
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class ScreenOrientationFeatureTest {
+ @Test
+ fun `WHEN the feature starts THEN register itself as a screen orientation delegate`() {
+ val engine = mock<Engine>()
+ val feature = ScreenOrientationFeature(engine, mock())
+
+ feature.start()
+
+ verify(engine).registerScreenOrientationDelegate(feature)
+ }
+
+ @Test
+ fun `WHEN the feature stops THEN unregister itself as the screen orientation delegate`() {
+ val engine = mock<Engine>()
+ val feature = ScreenOrientationFeature(engine, mock())
+
+ feature.stop()
+
+ verify(engine).unregisterScreenOrientationDelegate()
+ }
+
+ @Test
+ fun `WHEN asked to set a screen orientation THEN set it on the activity property and return true`() {
+ val activity = mock<Activity>()
+ val feature = ScreenOrientationFeature(mock(), activity)
+
+ val result = feature.onOrientationLock(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ assertTrue(result)
+ }
+
+ @Test
+ fun `WHEN asked to reset screen orientation THEN set it to UNSPECIFIED`() {
+ val activity = mock<Activity>()
+ val feature = ScreenOrientationFeature(mock(), activity)
+
+ feature.onOrientationUnlock()
+
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt
new file mode 100644
index 0000000000..0192f04773
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt
@@ -0,0 +1,401 @@
+/* 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.session
+
+import android.view.View
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+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.concept.engine.EngineView
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class SessionFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `start renders selected session`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view)
+ verify(view, never()).render(any())
+
+ feature.start()
+
+ store.waitUntilIdle()
+ verify(view).render(engineSession)
+ }
+
+ @Test
+ fun `start renders fixed session`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("C", engineSession)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view, tabId = "C")
+ verify(view, never()).render(any())
+
+ feature.start()
+
+ store.waitUntilIdle()
+
+ verify(view).render(engineSession)
+ }
+
+ @Test
+ fun `start renders custom tab session`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("D", engineSession)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view, tabId = "D")
+ verify(view, never()).render(any())
+ feature.start()
+
+ store.waitUntilIdle()
+
+ verify(view).render(engineSession)
+ }
+
+ @Test
+ fun `renders selected tab after changes`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSessionA: EngineSession = mock()
+ val engineSessionB: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("A", engineSessionA)).joinBlocking()
+ store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSessionB)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view)
+ verify(view, never()).render(any())
+
+ feature.start()
+ store.waitUntilIdle()
+ verify(view).render(engineSessionB)
+
+ store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking()
+ store.waitUntilIdle()
+ verify(view).render(engineSessionA)
+ }
+
+ @Test
+ fun `creates engine session if needed`() {
+ val store = spy(prepareStore())
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val feature = SessionFeature(store, mock(), view)
+ verify(view, never()).render(any())
+
+ feature.start()
+ store.waitUntilIdle()
+ verify(store).dispatch(EngineAction.CreateEngineSessionAction("B"))
+ }
+
+ @Test
+ fun `does not render new selected session after stop`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSessionA: EngineSession = mock()
+ val engineSessionB: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("A", engineSessionA)).joinBlocking()
+ store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSessionB)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view)
+ verify(view, never()).render(any())
+
+ feature.start()
+ store.waitUntilIdle()
+ verify(view).render(engineSessionB)
+
+ feature.stop()
+
+ store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking()
+ store.waitUntilIdle()
+ verify(view, never()).render(engineSessionA)
+ }
+
+ @Test
+ fun `releases when last selected session gets removed`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking()
+ val feature = SessionFeature(store, mock(), view)
+
+ feature.start()
+
+ store.waitUntilIdle()
+
+ verify(view).render(engineSession)
+ verify(view, never()).release()
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+ verify(view).release()
+ }
+
+ @Test
+ fun `release stops observing and releases session from view`() {
+ val store = prepareStore()
+ val actualView: View = mock()
+
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view)
+ verify(view, never()).render(any())
+
+ feature.start()
+
+ store.waitUntilIdle()
+
+ verify(view).render(engineSession)
+
+ val newEngineSession: EngineSession = mock()
+ feature.release()
+ verify(view).release()
+
+ store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking()
+ verify(view, never()).render(newEngineSession)
+ }
+
+ @Test
+ fun `releases when custom tab gets removed`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("D", engineSession)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view, tabId = "D")
+ verify(view, never()).render(any())
+
+ feature.start()
+
+ store.waitUntilIdle()
+
+ verify(view).render(engineSession)
+ verify(view, never()).release()
+
+ store.dispatch(CustomTabListAction.RemoveCustomTabAction("D")).joinBlocking()
+ verify(view).release()
+ }
+
+ @Test
+ fun `onBackPressed clears selection if it exists`() {
+ run {
+ val view: EngineView = mock()
+ doReturn(false).`when`(view).canClearSelection()
+
+ val feature = SessionFeature(BrowserStore(), mock(), view)
+ assertFalse(feature.onBackPressed())
+
+ verify(view, never()).clearSelection()
+ }
+
+ run {
+ val view: EngineView = mock()
+ doReturn(true).`when`(view).canClearSelection()
+
+ val feature = SessionFeature(BrowserStore(), mock(), view)
+ assertTrue(feature.onBackPressed())
+
+ verify(view).clearSelection()
+ }
+ }
+
+ @Test
+ fun `onBackPressed() invokes GoBackUseCase if back navigation is possible`() {
+ run {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
+ selectedTabId = "A",
+ ),
+ )
+
+ val useCase: SessionUseCases.GoBackUseCase = mock()
+
+ val feature = SessionFeature(store, useCase, mock())
+
+ assertFalse(feature.onBackPressed())
+ verify(useCase, never()).invoke("A")
+ }
+
+ run {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
+ selectedTabId = "A",
+ ),
+ )
+
+ store.dispatch(
+ ContentAction.UpdateBackNavigationStateAction(
+ "A",
+ canGoBack = true,
+ ),
+ ).joinBlocking()
+
+ val useCase: SessionUseCases.GoBackUseCase = mock()
+
+ val feature = SessionFeature(store, useCase, mock())
+
+ assertTrue(feature.onBackPressed())
+ verify(useCase).invoke("A")
+ }
+ }
+
+ @Test
+ fun `stop releases engine view`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("D", engineSession)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view, tabId = "D")
+ verify(view, never()).render(any())
+ feature.start()
+
+ store.waitUntilIdle()
+
+ verify(view).render(engineSession)
+
+ feature.stop()
+ verify(view).release()
+ }
+
+ @Test
+ fun `presenter observes crash state and does not create new engine session immediately`() {
+ val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+ val store = prepareStore(middleware)
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("A", engineSession)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view, tabId = "A")
+ verify(view, never()).render(any())
+ feature.start()
+
+ store.dispatch(CrashAction.SessionCrashedAction("A")).joinBlocking()
+ store.waitUntilIdle()
+ verify(view, atLeastOnce()).release()
+ middleware.assertNotDispatched(EngineAction.CreateEngineSessionAction::class)
+ }
+
+ @Test
+ fun `last access is updated when session is rendered`() {
+ val store = prepareStore()
+
+ val actualView: View = mock()
+ val view: EngineView = mock()
+ doReturn(actualView).`when`(view).asView()
+
+ val engineSession: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking()
+
+ val feature = SessionFeature(store, mock(), view)
+ verify(view, never()).render(any())
+
+ assertEquals(0L, store.state.findTab("B")?.lastAccess)
+ feature.start()
+ store.waitUntilIdle()
+
+ assertNotEquals(0L, store.state.findTab("B")?.lastAccess)
+ verify(view).render(engineSession)
+ }
+
+ private fun prepareStore(
+ middleware: CaptureActionsMiddleware<BrowserState, BrowserAction>? = null,
+ ): BrowserStore = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "A"),
+ createTab("https://getpocket.com", id = "B"),
+ createTab("https://www.firefox.com", id = "C"),
+ ),
+ customTabs = listOf(
+ createCustomTab("https://hubs.mozilla.com/", id = "D"),
+ ),
+ selectedTabId = "B",
+ ),
+ middleware = (if (middleware != null) listOf(middleware) else emptyList()) + EngineMiddleware.create(
+ engine = mock(),
+ scope = scope,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt
new file mode 100644
index 0000000000..4461577d25
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt
@@ -0,0 +1,519 @@
+/* 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.session
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.CrashAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+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.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SessionUseCasesTest {
+ private lateinit var middleware: CaptureActionsMiddleware<BrowserState, BrowserAction>
+ private lateinit var store: BrowserStore
+ private lateinit var useCases: SessionUseCases
+ private lateinit var engineSession: EngineSession
+ private lateinit var childEngineSession: EngineSession
+
+ @Before
+ fun setUp() {
+ engineSession = mock()
+ childEngineSession = mock()
+ middleware = CaptureActionsMiddleware()
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ url = "https://mozilla.org",
+ id = "mozilla",
+ engineSession = engineSession,
+ ),
+ createTab(
+ url = "https://bugzilla.com",
+ id = "bugzilla",
+ engineSession = childEngineSession,
+ parentId = "mozilla",
+ ),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ middleware = listOf(middleware) + EngineMiddleware.create(engine = mock()),
+ )
+ useCases = SessionUseCases(store)
+ }
+
+ @Test
+ fun loadUrlWithEngineSession() {
+ useCases.loadUrl("https://getpocket.com")
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(EngineAction.LoadUrlAction::class)
+ verify(engineSession).loadUrl(url = "https://getpocket.com")
+ middleware.assertLastAction(EngineAction.OptimizedLoadUrlTriggeredAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("https://getpocket.com", action.url)
+ }
+ assertEquals("https://getpocket.com", store.state.selectedTab?.content?.url)
+
+ useCases.loadUrl("https://www.mozilla.org", LoadUrlFlags.select(LoadUrlFlags.EXTERNAL))
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(EngineAction.LoadUrlAction::class)
+ verify(engineSession).loadUrl(
+ url = "https://www.mozilla.org",
+ flags = LoadUrlFlags.select(LoadUrlFlags.EXTERNAL),
+ )
+ middleware.assertLastAction(EngineAction.OptimizedLoadUrlTriggeredAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("https://www.mozilla.org", action.url)
+ assertEquals(LoadUrlFlags.select(LoadUrlFlags.EXTERNAL), action.flags)
+ }
+ assertEquals("https://www.mozilla.org", store.state.selectedTab?.content?.url)
+
+ useCases.loadUrl("https://firefox.com", store.state.selectedTabId)
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(EngineAction.LoadUrlAction::class)
+ verify(engineSession).loadUrl(url = "https://firefox.com")
+ middleware.assertLastAction(EngineAction.OptimizedLoadUrlTriggeredAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("https://firefox.com", action.url)
+ }
+ assertEquals("https://firefox.com", store.state.selectedTab?.content?.url)
+
+ useCases.loadUrl.invoke(
+ "https://developer.mozilla.org",
+ store.state.selectedTabId,
+ LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY),
+ )
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(EngineAction.LoadUrlAction::class)
+ verify(engineSession).loadUrl(
+ url = "https://developer.mozilla.org",
+ flags = LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY),
+ )
+ middleware.assertLastAction(EngineAction.OptimizedLoadUrlTriggeredAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("https://developer.mozilla.org", action.url)
+ assertEquals(LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY), action.flags)
+ }
+
+ useCases.loadUrl.invoke(
+ "https://www.mozilla.org/en-CA/firefox/browsers/mobile/",
+ "bugzilla",
+ )
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(EngineAction.LoadUrlAction::class)
+ verify(childEngineSession).loadUrl(
+ url = "https://www.mozilla.org/en-CA/firefox/browsers/mobile/",
+ parent = engineSession,
+ )
+ middleware.assertLastAction(EngineAction.OptimizedLoadUrlTriggeredAction::class) { action ->
+ assertEquals("bugzilla", action.tabId)
+ assertEquals("https://www.mozilla.org/en-CA/firefox/browsers/mobile/", action.url)
+ }
+ }
+
+ @Test
+ fun loadUrlWithoutEngineSession() {
+ store.dispatch(EngineAction.UnlinkEngineSessionAction("mozilla")).joinBlocking()
+
+ useCases.loadUrl("https://getpocket.com")
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("https://getpocket.com", action.url)
+ }
+
+ useCases.loadUrl("https://www.mozilla.org", LoadUrlFlags.select(LoadUrlFlags.EXTERNAL))
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("https://www.mozilla.org", action.url)
+ assertEquals(LoadUrlFlags.select(LoadUrlFlags.EXTERNAL), action.flags)
+ }
+
+ useCases.loadUrl("https://firefox.com", store.state.selectedTabId)
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("https://firefox.com", action.url)
+ }
+
+ useCases.loadUrl.invoke(
+ "https://developer.mozilla.org",
+ store.state.selectedTabId,
+ LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY),
+ )
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("https://developer.mozilla.org", action.url)
+ assertEquals(LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY), action.flags)
+ }
+ }
+
+ @Test
+ fun loadData() {
+ useCases.loadData("<html><body></body></html>", "text/html")
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.LoadDataAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("<html><body></body></html>", action.data)
+ assertEquals("text/html", action.mimeType)
+ assertEquals("UTF-8", action.encoding)
+ }
+
+ useCases.loadData(
+ "Should load in WebView",
+ "text/plain",
+ tabId = store.state.selectedTabId,
+ )
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.LoadDataAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("Should load in WebView", action.data)
+ assertEquals("text/plain", action.mimeType)
+ assertEquals("UTF-8", action.encoding)
+ }
+
+ useCases.loadData(
+ "Should also load in WebView",
+ "text/plain",
+ "base64",
+ store.state.selectedTabId,
+ )
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.LoadDataAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals("Should also load in WebView", action.data)
+ assertEquals("text/plain", action.mimeType)
+ assertEquals("base64", action.encoding)
+ }
+ }
+
+ @Test
+ fun reload() {
+ useCases.reload()
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(EngineAction.ReloadAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ }
+
+ useCases.reload(store.state.selectedTabId, LoadUrlFlags.select(LoadUrlFlags.EXTERNAL))
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(EngineAction.ReloadAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals(LoadUrlFlags.select(LoadUrlFlags.EXTERNAL), action.flags)
+ }
+ }
+
+ @Test
+ fun reloadBypassCache() {
+ val flags = LoadUrlFlags.select(LoadUrlFlags.BYPASS_CACHE)
+ useCases.reload(store.state.selectedTabId, flags = flags)
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(EngineAction.ReloadAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals(flags, action.flags)
+ }
+ }
+
+ @Test
+ fun stopLoading() = runTest {
+ useCases.stopLoading()
+ store.waitUntilIdle()
+ verify(engineSession).stopLoading()
+
+ useCases.stopLoading(store.state.selectedTabId)
+ store.waitUntilIdle()
+ verify(engineSession, times(2)).stopLoading()
+ }
+
+ @Test
+ fun goBack() {
+ useCases.goBack(null)
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(EngineAction.GoBackAction::class)
+
+ useCases.goBack(store.state.selectedTabId)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.GoBackAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertTrue(action.userInteraction)
+ }
+ middleware.reset()
+
+ useCases.goBack(userInteraction = false)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.GoBackAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertFalse(action.userInteraction)
+ }
+ }
+
+ @Test
+ fun goForward() {
+ useCases.goForward(null)
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(EngineAction.GoForwardAction::class)
+
+ useCases.goForward(store.state.selectedTabId)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.GoForwardAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertTrue(action.userInteraction)
+ }
+ middleware.reset()
+
+ useCases.goForward(userInteraction = false)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.GoForwardAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertFalse(action.userInteraction)
+ }
+ }
+
+ @Test
+ fun goToHistoryIndex() {
+ useCases.goToHistoryIndex(tabId = null, index = 0)
+ store.waitUntilIdle()
+ middleware.assertNotDispatched(EngineAction.GoToHistoryIndexAction::class)
+
+ useCases.goToHistoryIndex(tabId = "test", index = 5)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.GoToHistoryIndexAction::class) { action ->
+ assertEquals("test", action.tabId)
+ assertEquals(5, action.index)
+ }
+
+ useCases.goToHistoryIndex(index = 10)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.GoToHistoryIndexAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertEquals(10, action.index)
+ }
+ }
+
+ @Test
+ fun requestDesktopSite() {
+ useCases.requestDesktopSite(true, store.state.selectedTabId)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.ToggleDesktopModeAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertTrue(action.enable)
+ }
+
+ useCases.requestDesktopSite(false)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.ToggleDesktopModeAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertFalse(action.enable)
+ }
+ useCases.requestDesktopSite(true, store.state.selectedTabId)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.ToggleDesktopModeAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertTrue(action.enable)
+ }
+ useCases.requestDesktopSite(false, store.state.selectedTabId)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.ToggleDesktopModeAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ assertFalse(action.enable)
+ }
+ }
+
+ @Test
+ fun exitFullscreen() {
+ useCases.exitFullscreen(store.state.selectedTabId)
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.ExitFullScreenModeAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ }
+ middleware.reset()
+
+ useCases.exitFullscreen()
+ store.waitUntilIdle()
+ middleware.assertLastAction(EngineAction.ExitFullScreenModeAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ }
+ }
+
+ @Test
+ fun `LoadUrlUseCase will invoke onNoSession lambda if no selected session exists`() {
+ var createdTab: TabSessionState? = null
+ var tabCreatedForUrl: String? = null
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+
+ val loadUseCase = SessionUseCases.DefaultLoadUrlUseCase(store) { url ->
+ tabCreatedForUrl = url
+ createTab(url).also { createdTab = it }
+ }
+
+ loadUseCase("https://www.example.com")
+ store.waitUntilIdle()
+
+ assertEquals("https://www.example.com", tabCreatedForUrl)
+ assertNotNull(createdTab)
+
+ middleware.assertLastAction(EngineAction.LoadUrlAction::class) { action ->
+ assertEquals(createdTab!!.id, action.tabId)
+ assertEquals(tabCreatedForUrl, action.url)
+ }
+ }
+
+ @Test
+ fun `LoadDataUseCase will invoke onNoSession lambda if no selected session exists`() {
+ var createdTab: TabSessionState? = null
+ var tabCreatedForUrl: String? = null
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+ store.waitUntilIdle()
+
+ val loadUseCase = SessionUseCases.LoadDataUseCase(store) { url ->
+ tabCreatedForUrl = url
+ createTab(url).also { createdTab = it }
+ }
+
+ loadUseCase("Hello", mimeType = "text/plain", encoding = "UTF-8")
+ store.waitUntilIdle()
+
+ assertEquals("about:blank", tabCreatedForUrl)
+ assertNotNull(createdTab)
+
+ middleware.assertLastAction(EngineAction.LoadDataAction::class) { action ->
+ assertEquals(createdTab!!.id, action.tabId)
+ assertEquals("Hello", action.data)
+ assertEquals("text/plain", action.mimeType)
+ assertEquals("UTF-8", action.encoding)
+ }
+ }
+
+ @Test
+ fun `CrashRecoveryUseCase will restore specified session`() {
+ useCases.crashRecovery.invoke(listOf("mozilla"))
+ store.waitUntilIdle()
+
+ middleware.assertLastAction(CrashAction.RestoreCrashedSessionAction::class) { action ->
+ assertEquals("mozilla", action.tabId)
+ }
+ }
+
+ @Test
+ fun `CrashRecoveryUseCase will restore list of crashed sessions`() {
+ val store = spy(
+ BrowserStore(
+ middleware = listOf(middleware),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab(url = "https://wwww.mozilla.org", id = "tab1", crashed = true),
+ ),
+ customTabs = listOf(
+ createCustomTab(
+ "https://wwww.mozilla.org",
+ id = "customTab1",
+ crashed = true,
+ ),
+ ),
+ ),
+ ),
+ )
+ val useCases = SessionUseCases(store)
+
+ useCases.crashRecovery.invoke()
+ store.waitUntilIdle()
+
+ middleware.assertFirstAction(CrashAction.RestoreCrashedSessionAction::class) { action ->
+ assertEquals("tab1", action.tabId)
+ }
+
+ middleware.assertLastAction(CrashAction.RestoreCrashedSessionAction::class) { action ->
+ assertEquals("customTab1", action.tabId)
+ }
+ }
+
+ @Test
+ fun `PurgeHistoryUseCase dispatches PurgeHistory action`() {
+ useCases.purgeHistory()
+ store.waitUntilIdle()
+
+ middleware.findFirstAction(EngineAction.PurgeHistoryAction::class)
+ }
+
+ @Test
+ fun `UpdateLastAccessUseCase sets timestamp`() {
+ val tab = createTab("https://firefox.com")
+ val otherTab = createTab("https://example.com")
+ val customTab = createCustomTab("https://getpocket.com")
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab, otherTab),
+ customTabs = listOf(customTab),
+ ),
+ )
+ useCases = SessionUseCases(store)
+
+ // Make sure use case doesn't crash for custom tab and non-existent tab
+ useCases.updateLastAccess(customTab.id)
+ store.waitUntilIdle()
+ assertEquals(0L, store.state.findTab(tab.id)?.lastAccess)
+
+ // Update last access for a specific tab with default value
+ useCases.updateLastAccess(tab.id)
+ store.waitUntilIdle()
+ assertNotEquals(0L, store.state.findTab(tab.id)?.lastAccess)
+
+ // Update last access for a specific tab with specific value
+ useCases.updateLastAccess(tab.id, 123L)
+ store.waitUntilIdle()
+ assertEquals(123L, store.state.findTab(tab.id)?.lastAccess)
+
+ // Update last access for currently selected tab
+ store.dispatch(TabListAction.SelectTabAction(otherTab.id)).joinBlocking()
+ assertEquals(0L, store.state.findTab(otherTab.id)?.lastAccess)
+ useCases.updateLastAccess()
+ store.waitUntilIdle()
+ assertNotEquals(0L, store.state.findTab(otherTab.id)?.lastAccess)
+
+ // Update last access for currently selected tab with specific value
+ store.dispatch(TabListAction.SelectTabAction(otherTab.id)).joinBlocking()
+ useCases.updateLastAccess(lastAccess = 345L)
+ store.waitUntilIdle()
+ assertEquals(345L, store.state.findTab(otherTab.id)?.lastAccess)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SettingsUseCasesTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SettingsUseCasesTest.kt
new file mode 100644
index 0000000000..2a89bdfe7e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SettingsUseCasesTest.kt
@@ -0,0 +1,73 @@
+/* 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.session
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
+import mozilla.components.concept.engine.Settings
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+class SettingsUseCasesTest {
+ @Test
+ fun updateTrackingProtection() {
+ val engineSessionA: EngineSession = mock()
+ val engineSessionB: EngineSession = mock()
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "A"),
+ createTab("https://www.mozilla.org", id = "B"),
+ ),
+ selectedTabId = "A",
+ ),
+ )
+
+ store.dispatch(
+ EngineAction.LinkEngineSessionAction(
+ tabId = "A",
+ engineSession = engineSessionA,
+ ),
+ ).joinBlocking()
+
+ store.dispatch(
+ EngineAction.LinkEngineSessionAction(
+ tabId = "B",
+ engineSession = engineSessionB,
+ ),
+ ).joinBlocking()
+
+ val engine: Engine = mock()
+ val settings: Settings = mock()
+ doReturn(settings).`when`(engine).settings
+
+ val useCases = SettingsUseCases(engine, store)
+
+ useCases.updateTrackingProtection(TrackingProtectionPolicy.none())
+ verify(settings).trackingProtectionPolicy = TrackingProtectionPolicy.none()
+ verify(engineSessionA).updateTrackingProtection(TrackingProtectionPolicy.none())
+ verify(engineSessionB).updateTrackingProtection(TrackingProtectionPolicy.none())
+ verify(engine).clearSpeculativeSession()
+
+ reset(engine)
+ doReturn(settings).`when`(engine).settings
+
+ useCases.updateTrackingProtection(TrackingProtectionPolicy.strict())
+ verify(settings).trackingProtectionPolicy = TrackingProtectionPolicy.strict()
+ verify(engineSessionA).updateTrackingProtection(TrackingProtectionPolicy.strict())
+ verify(engineSessionB).updateTrackingProtection(TrackingProtectionPolicy.strict())
+ verify(engine).clearSpeculativeSession()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SwipeRefreshFeatureTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SwipeRefreshFeatureTest.kt
new file mode 100644
index 0000000000..2205de348f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/SwipeRefreshFeatureTest.kt
@@ -0,0 +1,130 @@
+/* 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.session
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.widget.FrameLayout
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.InputResultDetail
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+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.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class SwipeRefreshFeatureTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var store: BrowserStore
+ private lateinit var refreshFeature: SwipeRefreshFeature
+ private val mockLayout = mock<SwipeRefreshLayout>()
+ private val useCase = mock<SessionUseCases.ReloadUrlUseCase>()
+
+ @Before
+ fun setup() {
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "A"),
+ createTab("https://www.firefox.com", id = "B"),
+ ),
+ selectedTabId = "B",
+ ),
+ )
+
+ refreshFeature = SwipeRefreshFeature(store, useCase, mockLayout)
+ }
+
+ @Test
+ fun `sets the onRefreshListener and onChildScrollUpCallback`() {
+ verify(mockLayout).setOnRefreshListener(refreshFeature)
+ verify(mockLayout).setOnChildScrollUpCallback(refreshFeature)
+ }
+
+ @Test
+ fun `gesture should only work if the content can be overscrolled`() {
+ val engineView: DummyEngineView = mock()
+ val inputResultDetail: InputResultDetail = mock()
+ doReturn(inputResultDetail).`when`(engineView).getInputResultDetail()
+
+ doReturn(true).`when`(inputResultDetail).canOverscrollTop()
+ assertFalse(refreshFeature.canChildScrollUp(mockLayout, engineView))
+
+ doReturn(false).`when`(inputResultDetail).canOverscrollTop()
+ assertTrue(refreshFeature.canChildScrollUp(mockLayout, engineView))
+ }
+
+ @Test
+ fun `onRefresh should refresh the active session`() {
+ refreshFeature.start()
+ refreshFeature.onRefresh()
+
+ verify(useCase).invoke("B")
+ }
+
+ @Test
+ fun `feature MUST reset refreshCanceled after is used`() {
+ refreshFeature.start()
+
+ val selectedTab = store.state.findCustomTabOrSelectedTab()!!
+
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(selectedTab.id, true)).joinBlocking()
+ store.waitUntilIdle()
+
+ assertFalse(selectedTab.content.refreshCanceled)
+ }
+
+ @Test
+ fun `feature clears the swipeRefreshLayout#isRefreshing when tab fishes loading or a refreshCanceled`() {
+ refreshFeature.start()
+ store.waitUntilIdle()
+
+ val selectedTab = store.state.findCustomTabOrSelectedTab()!!
+
+ // Ignoring the first event from the initial state.
+ reset(mockLayout)
+
+ store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(selectedTab.id, true)).joinBlocking()
+ store.waitUntilIdle()
+
+ verify(mockLayout, times(2)).isRefreshing = false
+
+ // To trigger to an event we have to change loading from its previous value (false to true).
+ // As if we dispatch with loading = false, none event will be trigger.
+ store.dispatch(ContentAction.UpdateLoadingStateAction(selectedTab.id, true)).joinBlocking()
+ store.dispatch(ContentAction.UpdateLoadingStateAction(selectedTab.id, false)).joinBlocking()
+
+ verify(mockLayout, times(3)).isRefreshing = false
+ }
+
+ private open class DummyEngineView(context: Context) : FrameLayout(context), EngineView {
+ override fun setVerticalClipping(clippingHeight: Int) {}
+ override fun setDynamicToolbarMaxHeight(height: Int) {}
+ override fun setActivityContext(context: Context?) {}
+ override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) = Unit
+ override fun clearSelection() {}
+ override fun render(session: EngineSession) {}
+ override fun release() {}
+ override var selectionActionDelegate: SelectionActionDelegate? = null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/TrackingProtectionUseCasesTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/TrackingProtectionUseCasesTest.kt
new file mode 100644
index 0000000000..4e72afed45
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/TrackingProtectionUseCasesTest.kt
@@ -0,0 +1,266 @@
+/* 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.session
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.TrackingProtectionState
+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.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.content.blocking.TrackerLog
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
+import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
+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.whenever
+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.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class TrackingProtectionUseCasesTest {
+ private lateinit var exceptionStore: TrackingProtectionExceptionStorage
+ private lateinit var engine: Engine
+ private lateinit var store: BrowserStore
+ private lateinit var engineSession: EngineSession
+ private lateinit var useCases: TrackingProtectionUseCases
+
+ @Before
+ fun setUp() {
+ exceptionStore = mock()
+
+ engine = mock()
+ whenever(engine.trackingProtectionExceptionStore).thenReturn(exceptionStore)
+
+ engineSession = mock()
+
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ id = "A",
+ content = ContentState("https://www.mozilla.org"),
+ engineState = EngineState(engineSession),
+ ),
+ ),
+ selectedTabId = "A",
+ ),
+ )
+
+ useCases = TrackingProtectionUseCases(store, engine)
+ }
+
+ @Test
+ fun `fetch trackers logged successfully`() {
+ var trackersLog: List<TrackerLog>? = null
+ var onSuccessCalled = false
+ var onErrorCalled = false
+ val onSuccess: (List<TrackerLog>) -> Unit = {
+ trackersLog = it
+ onSuccessCalled = true
+ }
+
+ whenever(engine.getTrackersLog(any(), any(), any())).then {
+ onSuccess(emptyList())
+ }
+
+ val useCases = TrackingProtectionUseCases(store, engine)
+
+ useCases.fetchTrackingLogs(
+ "A",
+ onSuccess = {
+ trackersLog = it
+ onSuccessCalled = true
+ },
+ onError = {
+ onErrorCalled = true
+ },
+ )
+
+ assertNotNull(trackersLog)
+ assertTrue(onSuccessCalled)
+ assertFalse(onErrorCalled)
+ }
+
+ @Test
+ fun `calling fetchTrackingLogs with a null engine session will call onError`() {
+ var trackersLog: List<TrackerLog>? = null
+ var onSuccessCalled = false
+ var onErrorCalled = false
+ val onSuccess: (List<TrackerLog>) -> Unit = {
+ trackersLog = it
+ onSuccessCalled = true
+ }
+
+ store.dispatch(
+ EngineAction.UnlinkEngineSessionAction("A"),
+ ).joinBlocking()
+
+ whenever(engine.getTrackersLog(any(), any(), any())).then {
+ onSuccess(emptyList())
+ }
+
+ useCases.fetchTrackingLogs(
+ "A",
+ onSuccess = {
+ trackersLog = it
+ onSuccessCalled = true
+ },
+ onError = {
+ onErrorCalled = true
+ },
+ )
+
+ assertNull(trackersLog)
+ assertTrue(onErrorCalled)
+ assertFalse(onSuccessCalled)
+ }
+
+ @Test
+ fun `add exception`() {
+ useCases.addException("A")
+
+ verify(exceptionStore).add(engineSession)
+ }
+
+ @Test
+ fun `add exception with a null engine session will not call the store`() {
+ store.dispatch(
+ EngineAction.UnlinkEngineSessionAction("A"),
+ ).joinBlocking()
+
+ useCases.addException("A")
+
+ verify(exceptionStore, never()).add(any(), eq(false))
+ }
+
+ @Test
+ fun `GIVEN a persistent in private mode exception WHEN adding THEN add the exception and indicate that it is persistent`() {
+ val tabId = "A"
+ useCases.addException(tabId, persistInPrivateMode = true)
+
+ verify(exceptionStore).add(engineSession, true)
+ }
+
+ @Test
+ fun `remove a session exception`() {
+ useCases.removeException("A")
+
+ verify(exceptionStore).remove(engineSession)
+ }
+
+ @Test
+ fun `remove a tracking protection exception`() {
+ val tab1 = createTab("https://www.mozilla.org")
+ .copy(trackingProtection = TrackingProtectionState(ignoredOnTrackingProtection = true))
+
+ val tab2 = createTab("https://wiki.mozilla.org/")
+ .copy(trackingProtection = TrackingProtectionState(ignoredOnTrackingProtection = true))
+
+ val tab3 = createTab("https://www.mozilla.org/en-CA/")
+ .copy(trackingProtection = TrackingProtectionState(ignoredOnTrackingProtection = true))
+
+ val customTab = createCustomTab("https://www.mozilla.org/en-CA/")
+ .copy(trackingProtection = TrackingProtectionState(ignoredOnTrackingProtection = true))
+
+ val exception = object : TrackingProtectionException {
+ override val url: String = tab1.content.url
+ }
+
+ store.dispatch(TabListAction.AddTabAction(tab1)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab3)).joinBlocking()
+ store.dispatch(CustomTabListAction.AddCustomTabAction(customTab)).joinBlocking()
+ store.waitUntilIdle()
+
+ assertTrue(store.state.findTab(tab1.id)!!.trackingProtection.ignoredOnTrackingProtection)
+ assertTrue(store.state.findTab(tab2.id)!!.trackingProtection.ignoredOnTrackingProtection)
+ assertTrue(store.state.findTab(tab3.id)!!.trackingProtection.ignoredOnTrackingProtection)
+ assertTrue(store.state.findCustomTab(customTab.id)!!.trackingProtection.ignoredOnTrackingProtection)
+
+ useCases.removeException(exception)
+
+ verify(exceptionStore).remove(exception)
+
+ store.waitUntilIdle()
+
+ assertFalse(store.state.findTab(tab1.id)!!.trackingProtection.ignoredOnTrackingProtection)
+
+ // Different domain from tab1 MUST not be affected
+ assertTrue(store.state.findTab(tab2.id)!!.trackingProtection.ignoredOnTrackingProtection)
+
+ // Another tabs with the same domain as tab1 MUST be updated
+ assertFalse(store.state.findTab(tab3.id)!!.trackingProtection.ignoredOnTrackingProtection)
+ assertFalse(store.state.findCustomTab(customTab.id)!!.trackingProtection.ignoredOnTrackingProtection)
+ }
+
+ @Test
+ fun `remove exception with a null engine session will not call the store`() {
+ store.dispatch(
+ EngineAction.UnlinkEngineSessionAction("A"),
+ ).joinBlocking()
+
+ useCases.removeException("A")
+
+ verify(exceptionStore, never()).remove(any<EngineSession>())
+ }
+
+ @Test
+ fun `removeAll exceptions`() {
+ useCases.removeAllExceptions()
+
+ verify(exceptionStore).removeAll(any(), any())
+ }
+
+ @Test
+ fun `contains exception`() {
+ val callback: (Boolean) -> Unit = {}
+ useCases.containsException("A", callback)
+
+ verify(exceptionStore).contains(engineSession, callback)
+ }
+
+ @Test
+ fun `contains exception with a null engine session will not call the store`() {
+ var contains = true
+
+ store.dispatch(
+ EngineAction.UnlinkEngineSessionAction("A"),
+ ).joinBlocking()
+
+ useCases.containsException("A") {
+ contains = it
+ }
+
+ assertFalse(contains)
+ verify(exceptionStore, never()).contains(any(), any())
+ }
+
+ @Test
+ fun `fetch exceptions`() {
+ useCases.fetchExceptions {}
+ verify(exceptionStore).fetchAll(any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/LastAccessMiddlewareTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/LastAccessMiddlewareTest.kt
new file mode 100644
index 0000000000..9961a8b084
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/LastAccessMiddlewareTest.kt
@@ -0,0 +1,319 @@
+/* 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.session.middleware
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.BrowserAction
+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.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.test.ext.joinBlocking
+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.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class LastAccessMiddlewareTest {
+ lateinit var store: BrowserStore
+ lateinit var context: MiddlewareContext<BrowserState, BrowserAction>
+
+ @Before
+ fun setup() {
+ store = BrowserStore()
+ context = mock()
+
+ whenever(context.store).thenReturn(store)
+ whenever(context.state).thenReturn(store.state)
+ }
+
+ @Test
+ fun `UpdateLastAction is dispatched when tab is selected`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(
+ createTab("https://mozilla.org", id = "123"),
+ createTab("https://firefox.com", id = "456"),
+ ),
+ selectedTabId = "123",
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+
+ assertEquals(0L, store.state.tabs[0].lastAccess)
+ assertEquals(0L, store.state.tabs[1].lastAccess)
+
+ store.dispatch(TabListAction.SelectTabAction("456")).joinBlocking()
+
+ assertEquals(0L, store.state.tabs[0].lastAccess)
+ assertNotEquals(0L, store.state.tabs[1].lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is dispatched when a new tab is selected`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(
+ createTab("https://mozilla.org", id = "123"),
+ ),
+ selectedTabId = "123",
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+
+ assertEquals(0L, store.state.selectedTab?.lastAccess)
+
+ val newTab = createTab("https://firefox.com", id = "456")
+ store.dispatch(TabListAction.AddTabAction(newTab, select = true)).joinBlocking()
+
+ assertEquals("456", store.state.selectedTabId)
+ assertNotEquals(0L, store.state.selectedTab?.lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is not dispatched when a new tab is not selected`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(
+ createTab("https://mozilla.org", id = "123"),
+ ),
+ selectedTabId = "123",
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+
+ assertEquals(0L, store.state.selectedTab?.lastAccess)
+
+ val newTab = createTab("https://firefox.com", id = "456")
+ store.dispatch(TabListAction.AddTabAction(newTab, select = false)).joinBlocking()
+
+ assertEquals("123", store.state.selectedTabId)
+ assertEquals(0L, store.state.selectedTab?.lastAccess)
+ assertEquals(0L, store.state.tabs[1].lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is dispatched when URL of selected tab changes`() {
+ val tab = createTab("https://firefox.com", id = "123")
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(tab),
+ selectedTabId = tab.id,
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+ assertEquals(0L, store.state.selectedTab?.lastAccess)
+
+ store.dispatch(ContentAction.UpdateUrlAction(tab.id, "https://mozilla.org")).joinBlocking()
+ assertNotEquals(0L, store.state.selectedTab?.lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is not dispatched when URL of non-selected tab changes`() {
+ val tab = createTab("https://firefox.com", id = "123")
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(tab),
+ selectedTabId = tab.id,
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+ assertEquals(0L, store.state.selectedTab?.lastAccess)
+
+ val newTab = createTab("https://mozilla.org", id = "456")
+ store.dispatch(TabListAction.AddTabAction(newTab)).joinBlocking()
+ store.dispatch(ContentAction.UpdateUrlAction(newTab.id, "https://mozilla.org")).joinBlocking()
+ assertEquals(0L, store.state.selectedTab?.lastAccess)
+ assertEquals(0L, store.state.findTab(newTab.id)?.lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is dispatched when tab is selected during removal of single tab`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(
+ createTab("https://mozilla.org", id = "123"),
+ createTab("https://firefox.com", id = "456"),
+ ),
+ selectedTabId = "123",
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+
+ assertEquals(0L, store.state.tabs[0].lastAccess)
+ assertEquals(0L, store.state.tabs[1].lastAccess)
+
+ store.dispatch(TabListAction.RemoveTabAction("123")).joinBlocking()
+
+ val selectedTab = store.state.findTab("456")
+ assertNotNull(selectedTab)
+ assertEquals(selectedTab!!.id, store.state.selectedTabId)
+ assertNotEquals(0L, selectedTab.lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is not dispatched when no new tab is selected during removal of single tab`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(
+ createTab("https://mozilla.org", id = "123"),
+ createTab("https://firefox.com", id = "456"),
+ ),
+ selectedTabId = "123",
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+
+ assertEquals(0L, store.state.tabs[0].lastAccess)
+ assertEquals(0L, store.state.tabs[1].lastAccess)
+
+ store.dispatch(TabListAction.RemoveTabAction("456")).joinBlocking()
+ val selectedTab = store.state.findTab("123")
+ assertNotNull(selectedTab)
+ assertEquals(selectedTab!!.id, store.state.selectedTabId)
+ assertEquals(0L, selectedTab.lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is dispatched when tab is selected during removal of multiple tab`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(
+ createTab("https://mozilla.org", id = "123"),
+ createTab("https://firefox.com", id = "456"),
+ createTab("https://getpocket.com", id = "789"),
+ ),
+ selectedTabId = "123",
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+
+ assertEquals(0L, store.state.tabs[0].lastAccess)
+ assertEquals(0L, store.state.tabs[1].lastAccess)
+ assertEquals(0L, store.state.tabs[2].lastAccess)
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf("123", "456"))).joinBlocking()
+
+ val selectedTab = store.state.findTab("789")
+ assertNotNull(selectedTab)
+ assertEquals(selectedTab!!.id, store.state.selectedTabId)
+ assertNotEquals(0L, selectedTab.lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is not dispatched when no new tab is selected during removal of multiple tab`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(
+ createTab("https://mozilla.org", id = "123"),
+ createTab("https://firefox.com", id = "456"),
+ createTab("https://getpocket.com", id = "789"),
+ ),
+ selectedTabId = "123",
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+
+ assertEquals(0L, store.state.tabs[0].lastAccess)
+ assertEquals(0L, store.state.tabs[1].lastAccess)
+ assertEquals(0L, store.state.tabs[2].lastAccess)
+
+ store.dispatch(TabListAction.RemoveTabsAction(listOf("456", "789"))).joinBlocking()
+ val selectedTab = store.state.findTab("123")
+ assertEquals(selectedTab!!.id, store.state.selectedTabId)
+ assertEquals(0L, selectedTab.lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is not dispatched when no new tab is selected during removal of all private tab`() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ listOf(
+ createTab("https://mozilla.org", id = "123"),
+ createTab("https://firefox.com", id = "456", private = true),
+ createTab("https://getpocket.com", id = "789", private = true),
+ ),
+ selectedTabId = "123",
+ ),
+ middleware = listOf(LastAccessMiddleware()),
+ )
+
+ assertEquals(0L, store.state.tabs[0].lastAccess)
+ assertEquals(0L, store.state.tabs[1].lastAccess)
+ assertEquals(0L, store.state.tabs[2].lastAccess)
+
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ val selectedTab = store.state.findTab("123")
+ assertNotNull(selectedTab)
+ assertEquals(selectedTab!!.id, store.state.selectedTabId)
+ assertEquals(0L, selectedTab.lastAccess)
+ }
+
+ @Test
+ fun `UpdateLastAction is invoked for selected tab from RestoreAction`() {
+ val recentTime = System.currentTimeMillis()
+ val lastAccess = 3735928559
+ val store = BrowserStore(
+ middleware = listOf(LastAccessMiddleware()),
+ )
+ val recoverableTabs = listOf(
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(url = "https://firefox.com", id = "1", lastAccess = lastAccess),
+ ),
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState(url = "https://mozilla.org", id = "2", lastAccess = lastAccess),
+ ),
+ )
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ recoverableTabs,
+ "2",
+ TabListAction.RestoreAction.RestoreLocation.BEGINNING,
+ ),
+ ).joinBlocking()
+
+ assertTrue(store.state.tabs.size == 2)
+
+ val restoredTab1 = store.state.findTab("1")
+ val restoredTab2 = store.state.findTab("2")
+ assertNotNull(restoredTab1)
+ assertNotNull(restoredTab2)
+
+ assertNotEquals(restoredTab2!!.lastAccess, lastAccess)
+ assertTrue(restoredTab2.lastAccess > lastAccess)
+ assertTrue(restoredTab2.lastAccess > recentTime)
+
+ assertEquals(restoredTab1!!.lastAccess, lastAccess)
+ assertFalse(restoredTab1.lastAccess > lastAccess)
+ assertFalse(restoredTab1.lastAccess > recentTime)
+ }
+
+ @Test
+ fun `sanity check - next is always invoked in the middleware`() {
+ var nextInvoked = false
+ val middleware = LastAccessMiddleware()
+
+ middleware.invoke(context, { nextInvoked = true }, TabListAction.RemoveTabAction("123"))
+
+ assertTrue(nextInvoked)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/undo/UndoMiddlewareTest.kt b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/undo/UndoMiddlewareTest.kt
new file mode 100644
index 0000000000..59c7ff6141
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/undo/UndoMiddlewareTest.kt
@@ -0,0 +1,294 @@
+/* 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.session.middleware.undo
+
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.UndoAction
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+
+class UndoMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun `Undo scenario - Removing single tab`() = runTestOnMain {
+ val store = BrowserStore(
+ middleware = listOf(
+ UndoMiddleware(clearAfterMillis = 60000),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://getpocket.com", id = "pocket"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
+
+ store.dispatch(
+ TabListAction.RemoveTabAction(tabId = "mozilla"),
+ ).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
+
+ restoreRecoverableTabs(dispatcher, store)
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
+ }
+
+ @Test
+ fun `Undo scenario - Removing list of tabs`() = runTestOnMain {
+ val store = BrowserStore(
+ middleware = listOf(
+ UndoMiddleware(clearAfterMillis = 60000),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://getpocket.com", id = "pocket"),
+ createTab("https://firefox.com", id = "firefox"),
+ ),
+ selectedTabId = "mozilla",
+ ),
+ )
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
+
+ store.dispatch(
+ TabListAction.RemoveTabsAction(listOf("mozilla", "pocket")),
+ ).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://firefox.com", store.state.selectedTab!!.content.url)
+
+ restoreRecoverableTabs(dispatcher, store)
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
+ }
+
+ @Test
+ fun `Undo scenario - Removing all normal tabs`() = runTestOnMain {
+ val store = BrowserStore(
+ middleware = listOf(
+ UndoMiddleware(clearAfterMillis = 60000),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://getpocket.com", id = "pocket"),
+ createTab("https://reddit.com/r/firefox", id = "reddit", private = true),
+ ),
+ selectedTabId = "pocket",
+ ),
+ )
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
+
+ store.dispatch(
+ TabListAction.RemoveAllNormalTabsAction,
+ ).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertNull(store.state.selectedTab)
+
+ restoreRecoverableTabs(dispatcher, store)
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
+ }
+
+ @Test
+ fun `Undo scenario - Removing all tabs`() = runTestOnMain {
+ val store = BrowserStore(
+ middleware = listOf(
+ UndoMiddleware(clearAfterMillis = 60000),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://getpocket.com", id = "pocket"),
+ createTab("https://reddit.com/r/firefox", id = "reddit", private = true),
+ ),
+ selectedTabId = "pocket",
+ ),
+ )
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
+
+ store.dispatch(
+ TabListAction.RemoveAllTabsAction(),
+ ).joinBlocking()
+
+ assertEquals(0, store.state.tabs.size)
+ assertNull(store.state.selectedTab)
+
+ restoreRecoverableTabs(dispatcher, store)
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
+ }
+
+ @Test
+ fun `Undo scenario - Removing all tabs non-recoverable`() = runTestOnMain {
+ val store = BrowserStore(
+ middleware = listOf(
+ UndoMiddleware(clearAfterMillis = 60000),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://getpocket.com", id = "pocket"),
+ createTab("https://reddit.com/r/firefox", id = "reddit", private = true),
+ ),
+ selectedTabId = "pocket",
+ ),
+ )
+
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
+
+ store.dispatch(
+ TabListAction.RemoveAllTabsAction(false),
+ ).joinBlocking()
+
+ assertEquals(0, store.state.tabs.size)
+ assertNull(store.state.selectedTab)
+
+ restoreRecoverableTabs(dispatcher, store)
+
+ store.waitUntilIdle()
+
+ assertEquals(0, store.state.tabs.size)
+ }
+
+ @Test
+ fun `Undo History in State is written`() = runTestOnMain {
+ val store = BrowserStore(
+ middleware = listOf(
+ UndoMiddleware(clearAfterMillis = 60000),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://getpocket.com", id = "pocket"),
+ createTab("https://reddit.com/r/firefox", id = "reddit", private = true),
+ ),
+ selectedTabId = "pocket",
+ ),
+ )
+
+ assertNull(store.state.undoHistory.selectedTabId)
+ assertTrue(store.state.undoHistory.tabs.isEmpty())
+ assertEquals(3, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RemoveAllPrivateTabsAction,
+ ).joinBlocking()
+
+ assertNull(store.state.undoHistory.selectedTabId)
+ assertEquals(1, store.state.undoHistory.tabs.size)
+ assertEquals("https://reddit.com/r/firefox", store.state.undoHistory.tabs[0].state.url)
+ assertEquals(2, store.state.tabs.size)
+
+ store.dispatch(
+ TabListAction.RemoveAllNormalTabsAction,
+ ).joinBlocking()
+
+ assertEquals("pocket", store.state.undoHistory.selectedTabId)
+ assertEquals(2, store.state.undoHistory.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.undoHistory.tabs[0].state.url)
+ assertEquals("https://getpocket.com", store.state.undoHistory.tabs[1].state.url)
+ assertEquals(0, store.state.tabs.size)
+
+ restoreRecoverableTabs(dispatcher, store)
+
+ assertNull(store.state.undoHistory.selectedTabId)
+ assertTrue(store.state.undoHistory.tabs.isEmpty())
+ assertEquals(0, store.state.undoHistory.tabs.size)
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertEquals("https://getpocket.com", store.state.tabs[1].content.url)
+ }
+
+ @Test
+ fun `Undo History gets cleared after time`() = runTestOnMain {
+ val store = BrowserStore(
+ middleware = listOf(
+ UndoMiddleware(clearAfterMillis = 60000, waitScope = coroutinesTestRule.scope),
+ ),
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "mozilla"),
+ createTab("https://getpocket.com", id = "pocket"),
+ createTab("https://reddit.com/r/firefox", id = "reddit", private = true),
+ ),
+ selectedTabId = "pocket",
+ ),
+ )
+ assertEquals(3, store.state.tabs.size)
+ assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
+
+ store.dispatch(
+ TabListAction.RemoveAllNormalTabsAction,
+ ).joinBlocking()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://reddit.com/r/firefox", store.state.tabs[0].content.url)
+ assertEquals("pocket", store.state.undoHistory.selectedTabId)
+ assertEquals(2, store.state.undoHistory.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.undoHistory.tabs[0].state.url)
+ assertEquals("https://getpocket.com", store.state.undoHistory.tabs[1].state.url)
+
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+
+ assertNull(store.state.undoHistory.selectedTabId)
+ assertTrue(store.state.undoHistory.tabs.isEmpty())
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://reddit.com/r/firefox", store.state.tabs[0].content.url)
+
+ restoreRecoverableTabs(dispatcher, store)
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://reddit.com/r/firefox", store.state.tabs[0].content.url)
+ }
+}
+
+private suspend fun restoreRecoverableTabs(dispatcher: TestDispatcher, store: BrowserStore) {
+ withContext(dispatcher) {
+ // We need to pause the test dispatcher here to avoid it dispatching immediately.
+ // Otherwise we deadlock the test here when we wait for the store to complete and
+ // at the same time the middleware dispatches a coroutine on the dispatcher which will
+ // also block on the store in SessionManager.restore().
+ store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
+ }
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
+}
diff --git a/mobile/android/android-components/components/feature/session/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/session/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/session/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/session/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/session/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/session/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/share/README.md b/mobile/android/android-components/components/feature/share/README.md
new file mode 100644
index 0000000000..6f188d49d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Recents
+
+Feature implementation for saving and sorting recent apps used for sharing.
+
+## Usage
+
+### 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-share:{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/share/build.gradle b/mobile/android/android-components/components/feature/share/build.gradle
new file mode 100644
index 0000000000..a44df774e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/build.gradle
@@ -0,0 +1,73 @@
+/* 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/. */
+
+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"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/proguard/androidx-annotations.pro'
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.share'
+}
+
+dependencies {
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+}
+
+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/share/proguard-rules.pro b/mobile/android/android-components/components/feature/share/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/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/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/1.json b/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/1.json
new file mode 100644
index 0000000000..6a26877289
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/1.json
@@ -0,0 +1,40 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "05028edca931d077160a0d3a3e19b20f",
+ "entities": [
+ {
+ "tableName": "RECENT_APPS_TABLE",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `score` REAL NOT NULL, PRIMARY KEY(`packageName`))",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "score",
+ "columnName": "score",
+ "affinity": "REAL",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "packageName"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '05028edca931d077160a0d3a3e19b20f')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/2.json b/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/2.json
new file mode 100644
index 0000000000..241b612fa9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/2.json
@@ -0,0 +1,40 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "8ab7d915ce81c5d52503917cbafbe4df",
+ "entities": [
+ {
+ "tableName": "RECENT_APPS_TABLE",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`activityName` TEXT NOT NULL, `score` REAL NOT NULL, PRIMARY KEY(`activityName`))",
+ "fields": [
+ {
+ "fieldPath": "activityName",
+ "columnName": "activityName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "score",
+ "columnName": "score",
+ "affinity": "REAL",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "activityName"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8ab7d915ce81c5d52503917cbafbe4df')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/androidTest/java/mozilla/components/feature/share/RecentAppsDaoTest.kt b/mobile/android/android-components/components/feature/share/src/androidTest/java/mozilla/components/feature/share/RecentAppsDaoTest.kt
new file mode 100644
index 0000000000..b0986964f1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/androidTest/java/mozilla/components/feature/share/RecentAppsDaoTest.kt
@@ -0,0 +1,105 @@
+/* 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.share
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import mozilla.components.feature.share.db.RecentAppEntity
+import mozilla.components.feature.share.db.RecentAppsDao
+import mozilla.components.feature.share.db.RecentAppsDatabase
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class RecentAppsDaoTest {
+
+ private lateinit var database: RecentAppsDatabase
+ private lateinit var dao: RecentAppsDao
+
+ @Before
+ fun setup() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ database = Room.inMemoryDatabaseBuilder(context, RecentAppsDatabase::class.java).build()
+ dao = database.recentAppsDao()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ }
+
+ @Test
+ fun testGetTwoMostRecentApps() {
+ assertEquals(emptyList<RecentApp>(), dao.getRecentAppsUpTo(2))
+
+ dao.insertRecentApps(
+ listOf(
+ RecentAppEntity("first"),
+ RecentAppEntity("second"),
+ RecentAppEntity("third"),
+ ),
+ )
+
+ assertEquals(
+ listOf(
+ RecentAppEntity("first"),
+ RecentAppEntity("second"),
+ ),
+ dao.getRecentAppsUpTo(2),
+ )
+ }
+
+ @Test
+ fun testIncrementSelectedAppCountAndDecayAllOthers() {
+ val activityName = "activityName"
+ val selectedAppEntity = RecentAppEntity(score = 1.0, activityName = activityName)
+ val otherAppEntity = RecentAppEntity(score = 100.0, activityName = "other")
+ val allRecentApps = listOf(selectedAppEntity, otherAppEntity)
+
+ dao.insertRecentApps(allRecentApps)
+
+ dao.updateRecentAppAndDecayRest(activityName)
+
+ val allAppsResult = dao.getRecentAppsUpTo(2)
+ val selectedResult = allAppsResult.first { it.activityName == activityName }
+ val otherResult = allAppsResult.first { it.activityName == "other" }
+
+ assertEquals(95.0, otherResult.score, 0.0001)
+ assertEquals(2.0, selectedResult.score, 0.0001)
+ }
+
+ @Test
+ fun testAddNewlyInstalledAppsToOurDatabase() {
+ val firstActivityName = "first"
+ val secondActivityName = "second"
+ val thirdActivityName = "third"
+ val fourthActivityName = "fourth"
+ val appsInDatabase = listOf(
+ RecentAppEntity(firstActivityName),
+ RecentAppEntity(secondActivityName),
+ RecentAppEntity(thirdActivityName),
+ )
+ val additionalApp = RecentAppEntity(fourthActivityName)
+
+ dao.insertRecentApps(appsInDatabase)
+ dao.insertRecentApps(appsInDatabase + additionalApp)
+
+ assertEquals(appsInDatabase + additionalApp, dao.getRecentAppsUpTo(7))
+ }
+
+ @Test
+ fun testDeleteAnAppFromOurDatabase() {
+ val deleteAppName = "delete"
+ val recentApp = RecentAppEntity(score = 1.0, activityName = deleteAppName)
+
+ dao.insertRecentApps(listOf(recentApp))
+
+ dao.deleteRecentApp(recentApp.activityName)
+
+ assertEquals(emptyList<RecentApp>(), dao.getRecentAppsUpTo(1))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/share/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/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/share/src/main/java/mozilla/components/feature/share/RecentApp.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentApp.kt
new file mode 100644
index 0000000000..cacfdc8192
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentApp.kt
@@ -0,0 +1,24 @@
+/* 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.share
+
+/**
+ * Interface used for adapting recent apps database entities
+ *
+ * @property activityName - unique identifier of the app
+ * @property score - value used for sorting in descending order the recent apps (most recent first)
+ */
+interface RecentApp {
+
+ /**
+ * The activityName of the recent app.
+ */
+ val activityName: String
+
+ /**
+ * The score of the recent app (calculated based on number of selections - decay)
+ */
+ val score: Double
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentAppsStorage.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentAppsStorage.kt
new file mode 100644
index 0000000000..563eff85d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentAppsStorage.kt
@@ -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/. */
+
+package mozilla.components.feature.share
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.feature.share.db.RecentAppEntity
+import mozilla.components.feature.share.db.RecentAppsDao
+import mozilla.components.feature.share.db.RecentAppsDatabase
+
+/**
+ * Class used for storing and retrieving the most recent apps
+ */
+class RecentAppsStorage(context: Context) {
+
+ @VisibleForTesting
+ internal var recentAppsDao: Lazy<RecentAppsDao> = lazy {
+ RecentAppsDatabase.get(context).recentAppsDao()
+ }
+
+ /**
+ * Increment the value stored in the database for the selected app. Then, apply a decay to all
+ * other apps in the database. This allows newly installed apps to catch up and appear in the
+ * most recent section faster. We do not need to handle overflow as it's not reasonably expected
+ * to reach Double.MAX_VALUE for users
+ */
+ fun updateRecentApp(selectedActivityName: String) {
+ recentAppsDao.value.updateRecentAppAndDecayRest(selectedActivityName)
+ }
+
+ /**
+ * Deletes an app form the recent apps list
+ * @param activityName - name of the activity of the app
+ */
+ fun deleteRecentApp(activityName: String) {
+ recentAppsDao.value.deleteRecentApp(activityName)
+ }
+
+ /**
+ * Get a descending ordered list of the most recent apps
+ * @param limit - size of list
+ */
+ fun getRecentAppsUpTo(limit: Int): List<RecentApp> {
+ return recentAppsDao.value.getRecentAppsUpTo(limit)
+ }
+
+ /**
+ * If there are apps that could resolve our share and are not added in our database, we add them
+ * with a 0 count, so they can be updated later when a user uses that app
+ */
+ fun updateDatabaseWithNewApps(activityNames: List<String>) {
+ recentAppsDao.value.insertRecentApps(
+ activityNames.map { activityName ->
+ RecentAppEntity(activityName)
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/adapter/RecentAppAdapter.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/adapter/RecentAppAdapter.kt
new file mode 100644
index 0000000000..ff0a582b8f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/adapter/RecentAppAdapter.kt
@@ -0,0 +1,30 @@
+/* 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.share.adapter
+
+import mozilla.components.feature.share.RecentApp
+import mozilla.components.feature.share.db.RecentAppEntity
+
+internal class RecentAppAdapter(
+ internal val entity: RecentAppEntity,
+) : RecentApp {
+
+ override val activityName: String
+ get() = entity.activityName
+
+ override val score: Double
+ get() = entity.score
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is RecentAppAdapter) {
+ return false
+ }
+ return entity == other.entity
+ }
+
+ override fun hashCode(): Int {
+ return entity.hashCode()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppEntity.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppEntity.kt
new file mode 100644
index 0000000000..c519c91b4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppEntity.kt
@@ -0,0 +1,22 @@
+/* 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.share.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.feature.share.RecentApp
+import mozilla.components.feature.share.db.RecentAppsDatabase.Companion.RECENT_APPS_TABLE
+
+@Entity(tableName = RECENT_APPS_TABLE)
+internal data class RecentAppEntity(
+
+ @PrimaryKey
+ @ColumnInfo(name = "activityName")
+ override var activityName: String,
+
+ @ColumnInfo(name = "score")
+ override var score: Double = 0.0,
+) : RecentApp
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDao.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDao.kt
new file mode 100644
index 0000000000..9493ca20c0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDao.kt
@@ -0,0 +1,73 @@
+/* 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.share.db
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+
+private const val DECAY_MULTIPLIER = 0.95
+
+@Dao
+internal abstract class RecentAppsDao {
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ abstract fun insertRecentApps(recentApps: List<RecentAppEntity>)
+
+ @Query(
+ """
+ DELETE FROM recent_apps_table
+ WHERE activityName = :activityName
+ """,
+ )
+ abstract fun deleteRecentApp(activityName: String)
+
+ @Query(
+ """
+ SELECT * FROM recent_apps_table
+ ORDER BY score DESC
+ LIMIT :limit
+ """,
+ )
+ abstract fun getRecentAppsUpTo(limit: Int): List<RecentAppEntity>
+
+ /**
+ * Increments the score of a recent app.
+ * @param activityName - Name of the recent app to update.
+ */
+ @Query(
+ """
+ UPDATE recent_apps_table
+ SET score = score + 1
+ WHERE activityName = :activityName
+ """,
+ )
+ abstract fun updateRecentAppScore(activityName: String)
+
+ /**
+ * Decreases the score of all but one app (exponential decay).
+ * @param exceptActivity - ID of recent app to leave alone
+ * @param decay - Amount to decay by. Should be between 0 and 1.
+ */
+ @Query(
+ """
+ UPDATE recent_apps_table
+ SET score = score * :decay
+ WHERE activityName != :exceptActivity
+ """,
+ )
+ abstract fun decayAllRecentApps(
+ exceptActivity: String,
+ decay: Double = DECAY_MULTIPLIER,
+ )
+
+ @Transaction
+ open fun updateRecentAppAndDecayRest(activityName: String) {
+ updateRecentAppScore(activityName)
+ decayAllRecentApps(exceptActivity = activityName)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDatabase.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDatabase.kt
new file mode 100644
index 0000000000..d554bd3209
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDatabase.kt
@@ -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/. */
+
+package mozilla.components.feature.share.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import mozilla.components.feature.share.db.RecentAppsDatabase.Companion.RECENT_APPS_TABLE
+
+/**
+ * Internal database for storing apps and their scores that determine if they are most recently used.
+ */
+@Database(entities = [RecentAppEntity::class], version = 2)
+internal abstract class RecentAppsDatabase : RoomDatabase() {
+ abstract fun recentAppsDao(): RecentAppsDao
+
+ companion object {
+
+ const val RECENT_APPS_TABLE = "RECENT_APPS_TABLE"
+
+ @Volatile
+ private var instance: RecentAppsDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): RecentAppsDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ RecentAppsDatabase::class.java,
+ RECENT_APPS_TABLE,
+ ).addMigrations(
+ Migrations.migration_1_2,
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
+
+internal object Migrations {
+ val migration_1_2 = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("DROP TABLE RECENT_APPS_TABLE")
+ db.execSQL(
+ "CREATE TABLE IF NOT EXISTS " + RECENT_APPS_TABLE +
+ "(" +
+ "`activityName` TEXT NOT NULL, " +
+ "`score` DOUBLE NOT NULL, " +
+ " PRIMARY KEY(`activityName`)" +
+ ")",
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/test/java/mozilla/components/feature/share/RecentAppStorageTest.kt b/mobile/android/android-components/components/feature/share/src/test/java/mozilla/components/feature/share/RecentAppStorageTest.kt
new file mode 100644
index 0000000000..6a8dee764b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/test/java/mozilla/components/feature/share/RecentAppStorageTest.kt
@@ -0,0 +1,76 @@
+/* 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.share
+
+import android.content.Context
+import mozilla.components.feature.share.db.RecentAppEntity
+import mozilla.components.feature.share.db.RecentAppsDao
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class RecentAppStorageTest {
+
+ private lateinit var context: Context
+ private lateinit var recentAppsDao: RecentAppsDao
+ private lateinit var recentAppsStorage: RecentAppsStorage
+
+ @Before
+ fun setup() {
+ context = mock()
+ recentAppsDao = mock()
+
+ recentAppsStorage = RecentAppsStorage(context)
+ recentAppsStorage.recentAppsDao = lazyOf(recentAppsDao)
+ }
+
+ @Test
+ fun `get the two most recent apps`() {
+ whenever(recentAppsDao.getRecentAppsUpTo(2)).thenReturn(emptyList())
+
+ assertEquals(emptyList<RecentApp>(), recentAppsStorage.getRecentAppsUpTo(2))
+ }
+
+ @Test
+ fun `increment selected app count and decay all others`() {
+ val activityName = "activityName"
+
+ recentAppsStorage.updateRecentApp(activityName)
+
+ verify(recentAppsDao).updateRecentAppAndDecayRest(activityName)
+ }
+
+ @Test
+ fun `add newly installed apps to our database`() {
+ val firstActivityName = "first"
+ val secondActivityName = "second"
+ val thirdActivityName = "third"
+ val fourthActivityName = "fourth"
+ val currentApps = listOf(firstActivityName, secondActivityName, thirdActivityName, fourthActivityName)
+ val appsInDatabase = listOf(
+ RecentAppEntity(firstActivityName),
+ RecentAppEntity(secondActivityName),
+ RecentAppEntity(thirdActivityName),
+ RecentAppEntity(fourthActivityName),
+ )
+
+ recentAppsStorage.updateDatabaseWithNewApps(currentApps)
+
+ verify(recentAppsDao).insertRecentApps(appsInDatabase)
+ }
+
+ @Test
+ fun `delete an app from our database`() {
+ val deleteAppName = "delete"
+ val recentApp = RecentAppEntity(score = 1.0, activityName = deleteAppName)
+
+ recentAppsStorage.deleteRecentApp(recentApp.activityName)
+
+ verify(recentAppsDao).deleteRecentApp(deleteAppName)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/README.md b/mobile/android/android-components/components/feature/sitepermissions/README.md
new file mode 100644
index 0000000000..5eefdb579c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/README.md
@@ -0,0 +1,61 @@
+# [Android Components](../../../README.md) > Feature > Site Permissions
+
+A feature for showing site permission request prompts.
+
+## Usage
+
+### 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-sitepermissions:{latest-version}"
+```
+
+### SitePermissionsFeature
+
+ ```
+ Add these permissions to your ``AndroidManifest.xml`` file.
+ ```XML
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ ```
+
+ ```kotlin
+ val onNeedToRequestPermissions : (Array<String>) -> Unit = { permissions ->
+ /* You are in charge of triggering the request for the permissions needed,
+ * this way you can control, when you request the permissions,
+ * in case that you want to show an informative dialog,
+ * to clarify the use of these permissions.
+ */
+ this.requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS)
+ }
+
+ val sitePermissionsFeature = SitePermissionsFeature(
+ anchorView = toolbar,
+ sessionManager = components.sessionManager,
+ fragmentManager = requireFragmentManager(),
+ onNeedToRequestPermissions = onNeedToRequestPermissions
+ )
+
+ // It will start listing for new permissionRequest.
+ sitePermissionsFeature.start()
+
+ // It will stop listing for new permissionRequest.
+ sitePermissionsFeature.stop()
+
+
+ // Notify the feature if the permissions requested were granted or rejected.
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
+ when (requestCode) {
+ REQUEST_CODE_APP_PERMISSIONS -> sitePermissionsFeature.onPermissionsResult(grantResults)
+ }
+ }
+
+## 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/sitepermissions/build.gradle b/mobile/android/android-components/components/feature/sitepermissions/build.gradle
new file mode 100644
index 0000000000..06fa58246b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/build.gradle
@@ -0,0 +1,88 @@
+/* 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: 'kotlin-parcelize'
+
+apply plugin: 'com.google.devtools.ksp'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/proguard/androidx-annotations.pro'
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.sitepermissions'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':concept-engine')
+ implementation project(':ui-icons')
+ implementation project(':support-ktx')
+ implementation project(':feature-tabs')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+ implementation ComponentsDependencies.androidx_paging
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/sitepermissions/proguard-rules.pro b/mobile/android/android-components/components/feature/sitepermissions/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/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/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/1.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/1.json
new file mode 100644
index 0000000000..1363bf43de
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/1.json
@@ -0,0 +1,81 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "462054044d4b7f0e4f80f84380d5cc1e",
+ "entities": [
+ {
+ "tableName": "site_permissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera_back` INTEGER NOT NULL, `camera_front` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notification",
+ "columnName": "notification",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "microphone",
+ "columnName": "microphone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "cameraBack",
+ "columnName": "camera_back",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "cameraFront",
+ "columnName": "camera_front",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bluetooth",
+ "columnName": "bluetooth",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localStorage",
+ "columnName": "local_storage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "savedAt",
+ "columnName": "saved_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "origin"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"462054044d4b7f0e4f80f84380d5cc1e\")"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/2.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/2.json
new file mode 100644
index 0000000000..344aaccc30
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/2.json
@@ -0,0 +1,75 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "279719818fbc84cac905ddf942282eae",
+ "entities": [
+ {
+ "tableName": "site_permissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notification",
+ "columnName": "notification",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "microphone",
+ "columnName": "microphone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "camera",
+ "columnName": "camera",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bluetooth",
+ "columnName": "bluetooth",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localStorage",
+ "columnName": "local_storage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "savedAt",
+ "columnName": "saved_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "origin"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"279719818fbc84cac905ddf942282eae\")"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/3.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/3.json
new file mode 100644
index 0000000000..bfc2906dc9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/3.json
@@ -0,0 +1,88 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "f5350dbda12da0f415e9d09d00c36f49",
+ "entities": [
+ {
+ "tableName": "site_permissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `autoplay_audible` INTEGER NOT NULL, `autoplay_inaudible` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notification",
+ "columnName": "notification",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "microphone",
+ "columnName": "microphone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "camera",
+ "columnName": "camera",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bluetooth",
+ "columnName": "bluetooth",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localStorage",
+ "columnName": "local_storage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayAudible",
+ "columnName": "autoplay_audible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayInaudible",
+ "columnName": "autoplay_inaudible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "savedAt",
+ "columnName": "saved_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "origin"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5350dbda12da0f415e9d09d00c36f49')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/4.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/4.json
new file mode 100644
index 0000000000..44e6233c31
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/4.json
@@ -0,0 +1,94 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "f5379c8eb4f1519eb5994e508626ca10",
+ "entities": [
+ {
+ "tableName": "site_permissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `autoplay_audible` INTEGER NOT NULL, `autoplay_inaudible` INTEGER NOT NULL, `media_key_system_access` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notification",
+ "columnName": "notification",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "microphone",
+ "columnName": "microphone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "camera",
+ "columnName": "camera",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bluetooth",
+ "columnName": "bluetooth",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localStorage",
+ "columnName": "local_storage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayAudible",
+ "columnName": "autoplay_audible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayInaudible",
+ "columnName": "autoplay_inaudible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mediaKeySystemAccess",
+ "columnName": "media_key_system_access",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "savedAt",
+ "columnName": "saved_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "origin"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5379c8eb4f1519eb5994e508626ca10')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/5.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/5.json
new file mode 100644
index 0000000000..c570ac0037
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/5.json
@@ -0,0 +1,94 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 5,
+ "identityHash": "f5379c8eb4f1519eb5994e508626ca10",
+ "entities": [
+ {
+ "tableName": "site_permissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `autoplay_audible` INTEGER NOT NULL, `autoplay_inaudible` INTEGER NOT NULL, `media_key_system_access` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notification",
+ "columnName": "notification",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "microphone",
+ "columnName": "microphone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "camera",
+ "columnName": "camera",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bluetooth",
+ "columnName": "bluetooth",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localStorage",
+ "columnName": "local_storage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayAudible",
+ "columnName": "autoplay_audible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayInaudible",
+ "columnName": "autoplay_inaudible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mediaKeySystemAccess",
+ "columnName": "media_key_system_access",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "savedAt",
+ "columnName": "saved_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "origin"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5379c8eb4f1519eb5994e508626ca10')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/6.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/6.json
new file mode 100644
index 0000000000..5780fd467f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/6.json
@@ -0,0 +1,94 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 6,
+ "identityHash": "f5379c8eb4f1519eb5994e508626ca10",
+ "entities": [
+ {
+ "tableName": "site_permissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `autoplay_audible` INTEGER NOT NULL, `autoplay_inaudible` INTEGER NOT NULL, `media_key_system_access` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notification",
+ "columnName": "notification",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "microphone",
+ "columnName": "microphone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "camera",
+ "columnName": "camera",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bluetooth",
+ "columnName": "bluetooth",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localStorage",
+ "columnName": "local_storage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayAudible",
+ "columnName": "autoplay_audible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayInaudible",
+ "columnName": "autoplay_inaudible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mediaKeySystemAccess",
+ "columnName": "media_key_system_access",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "savedAt",
+ "columnName": "saved_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "origin"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5379c8eb4f1519eb5994e508626ca10')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/7.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/7.json
new file mode 100644
index 0000000000..87a2a06f6a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/7.json
@@ -0,0 +1,94 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 7,
+ "identityHash": "f5379c8eb4f1519eb5994e508626ca10",
+ "entities": [
+ {
+ "tableName": "site_permissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `autoplay_audible` INTEGER NOT NULL, `autoplay_inaudible` INTEGER NOT NULL, `media_key_system_access` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notification",
+ "columnName": "notification",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "microphone",
+ "columnName": "microphone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "camera",
+ "columnName": "camera",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bluetooth",
+ "columnName": "bluetooth",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localStorage",
+ "columnName": "local_storage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayAudible",
+ "columnName": "autoplay_audible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayInaudible",
+ "columnName": "autoplay_inaudible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mediaKeySystemAccess",
+ "columnName": "media_key_system_access",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "savedAt",
+ "columnName": "saved_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "origin"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5379c8eb4f1519eb5994e508626ca10')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/8.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/8.json
new file mode 100644
index 0000000000..97a9421bd1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/8.json
@@ -0,0 +1,100 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 8,
+ "identityHash": "a4391f9f5b2a6448070c7f5cefb1b086",
+ "entities": [
+ {
+ "tableName": "site_permissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `autoplay_audible` INTEGER NOT NULL, `autoplay_inaudible` INTEGER NOT NULL, `media_key_system_access` INTEGER NOT NULL, `cross_origin_storage_access` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notification",
+ "columnName": "notification",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "microphone",
+ "columnName": "microphone",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "camera",
+ "columnName": "camera",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bluetooth",
+ "columnName": "bluetooth",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localStorage",
+ "columnName": "local_storage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayAudible",
+ "columnName": "autoplay_audible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoplayInaudible",
+ "columnName": "autoplay_inaudible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mediaKeySystemAccess",
+ "columnName": "media_key_system_access",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "crossOriginStorageAccess",
+ "columnName": "cross_origin_storage_access",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "savedAt",
+ "columnName": "saved_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "origin"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a4391f9f5b2a6448070c7f5cefb1b086')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/OnDeviceSitePermissionsStorageTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/OnDeviceSitePermissionsStorageTest.kt
new file mode 100644
index 0000000000..83b2a5a042
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/OnDeviceSitePermissionsStorageTest.kt
@@ -0,0 +1,230 @@
+/* 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.sitepermissions.db
+
+import android.content.Context
+import androidx.core.net.toUri
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status
+import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
+import mozilla.components.support.ktx.kotlin.getOrigin
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+private const val MIGRATION_TEST_DB = "migration-test"
+
+class OnDeviceSitePermissionsStorageTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var storage: OnDiskSitePermissionsStorage
+ private lateinit var database: SitePermissionsDatabase
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ SitePermissionsDatabase::class.java,
+ )
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, SitePermissionsDatabase::class.java).build()
+ storage = OnDiskSitePermissionsStorage(context)
+ storage.databaseInitializer = {
+ database
+ }
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ }
+
+ @Test
+ fun testStorageInteraction() = runTest {
+ val origin = "https://www.mozilla.org".toUri().host!!
+ val sitePermissions = SitePermissions(
+ origin = origin,
+ camera = Status.BLOCKED,
+ savedAt = System.currentTimeMillis(),
+ )
+ storage.save(sitePermissions, private = false)
+ val sitePermissionsFromStorage = storage.findSitePermissionsBy(origin, private = false)!!
+
+ assertEquals(origin, sitePermissionsFromStorage.origin)
+ assertEquals(Status.BLOCKED, sitePermissionsFromStorage.camera)
+ }
+
+ @Test
+ fun migrate1to2() {
+ val dbVersion1 = helper.createDatabase(MIGRATION_TEST_DB, 1).apply {
+ execSQL(
+ "INSERT INTO " +
+ "site_permissions " +
+ "(origin, location, notification, microphone,camera_front,camera_back,bluetooth,local_storage,saved_at) " +
+ "VALUES " +
+ "('mozilla.org',1,1,1,1,1,1,1,1)",
+ )
+ }
+
+ dbVersion1.query("SELECT * FROM site_permissions").use { cursor ->
+ assertEquals(9, cursor.columnCount)
+ }
+
+ val dbVersion2 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB,
+ 2,
+ true,
+ Migrations.migration_1_2,
+ ).apply {
+ execSQL(
+ "INSERT INTO " +
+ "site_permissions " +
+ "(origin, location, notification, microphone,camera,bluetooth,local_storage,saved_at) " +
+ "VALUES " +
+ "('mozilla.org',1,1,1,1,1,1,1)",
+ )
+ }
+
+ dbVersion2.query("SELECT * FROM site_permissions").use { cursor ->
+ assertEquals(8, cursor.columnCount)
+
+ cursor.moveToFirst()
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("camera")))
+ }
+ }
+
+ @Test
+ fun migrate2to3() {
+ helper.createDatabase(MIGRATION_TEST_DB, 2).apply {
+ query("SELECT * FROM site_permissions").use { cursor ->
+ assertEquals(8, cursor.columnCount)
+ }
+ execSQL(
+ "INSERT INTO " +
+ "site_permissions " +
+ "(origin, location, notification, microphone,camera,bluetooth,local_storage,saved_at) " +
+ "VALUES " +
+ "('mozilla.org',1,1,1,1,1,1,1)",
+ )
+ }
+
+ val dbVersion3 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 3, true, Migrations.migration_2_3)
+
+ dbVersion3.query("SELECT * FROM site_permissions").use { cursor ->
+ assertEquals(10, cursor.columnCount)
+
+ cursor.moveToFirst()
+ assertEquals(Status.BLOCKED.id, cursor.getInt(cursor.getColumnIndexOrThrow("autoplay_audible")))
+ assertEquals(Status.ALLOWED.id, cursor.getInt(cursor.getColumnIndexOrThrow("autoplay_inaudible")))
+ }
+ }
+
+ @Test
+ fun migrate3to4() {
+ helper.createDatabase(MIGRATION_TEST_DB, 3).apply {
+ query("SELECT * FROM site_permissions").use { cursor ->
+ assertEquals(10, cursor.columnCount)
+ }
+ execSQL(
+ "INSERT INTO " +
+ "site_permissions " +
+ "(origin, location, notification, microphone,camera,bluetooth,local_storage,autoplay_audible,autoplay_inaudible,saved_at) " +
+ "VALUES " +
+ "('mozilla.org',1,1,1,1,1,1,1,1,1)",
+ )
+ }
+
+ val dbVersion3 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 4, true, Migrations.migration_3_4)
+
+ dbVersion3.query("SELECT * FROM site_permissions").use { cursor ->
+ assertEquals(11, cursor.columnCount)
+
+ cursor.moveToFirst()
+ assertEquals(Status.NO_DECISION.id, cursor.getInt(cursor.getColumnIndexOrThrow("media_key_system_access")))
+ }
+ }
+
+ @Test
+ fun migrate4to5() {
+ helper.createDatabase(MIGRATION_TEST_DB, 4).apply {
+ execSQL(
+ "INSERT INTO " +
+ "site_permissions " +
+ "(origin, location, notification, microphone,camera,bluetooth,local_storage,autoplay_audible,autoplay_inaudible,media_key_system_access,saved_at) " +
+ "VALUES " +
+ "('mozilla.org',1,1,1,1,1,1,0,0,1,1)",
+ )
+ }
+
+ val dbVersion5 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 5, true, Migrations.migration_4_5)
+
+ dbVersion5.query("SELECT * FROM site_permissions").use { cursor ->
+ cursor.moveToFirst()
+ assertEquals(AutoplayStatus.BLOCKED.id, cursor.getInt(cursor.getColumnIndexOrThrow("autoplay_audible")))
+ assertEquals(AutoplayStatus.ALLOWED.id, cursor.getInt(cursor.getColumnIndexOrThrow("autoplay_inaudible")))
+ }
+ }
+
+ @Test
+ fun migrate5to6() {
+ val url = "https://permission.site/"
+
+ helper.createDatabase(MIGRATION_TEST_DB, 5).apply {
+ execSQL(
+ "INSERT INTO " +
+ "site_permissions " +
+ "(origin, location, notification, microphone,camera,bluetooth,local_storage,autoplay_audible,autoplay_inaudible,media_key_system_access,saved_at) " +
+ "VALUES " +
+ "('${url.tryGetHostFromUrl()}',1,1,1,1,1,1,0,0,1,1)",
+ )
+ }
+
+ val dbVersion6 =
+ helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 6, true, Migrations.migration_5_6)
+
+ dbVersion6.query("SELECT * FROM site_permissions").use { cursor ->
+ cursor.moveToFirst()
+ val urlDB = cursor.getString(cursor.getColumnIndexOrThrow("origin"))
+ assertEquals(url.getOrigin(), urlDB)
+ }
+ }
+
+ @Test
+ fun migrate6to7() {
+ val url = "https://permission.site/"
+
+ helper.createDatabase(MIGRATION_TEST_DB, 6).apply {
+ execSQL(
+ "INSERT INTO " +
+ "site_permissions " +
+ "(origin, location, notification, microphone,camera,bluetooth,local_storage,autoplay_audible,autoplay_inaudible,media_key_system_access,saved_at) " +
+ "VALUES " +
+ "('${url.tryGetHostFromUrl()}',1,1,1,1,1,1,-1,-1,1,1)",
+ ) // Block audio and video.
+ }
+
+ val dbVersion6 =
+ helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 7, true, Migrations.migration_6_7)
+
+ dbVersion6.query("SELECT * FROM site_permissions").use { cursor ->
+ cursor.moveToFirst()
+ val audible = cursor.getInt(cursor.getColumnIndexOrThrow("autoplay_audible"))
+ val inaudible = cursor.getInt(cursor.getColumnIndexOrThrow("autoplay_inaudible"))
+ assertEquals(-1, audible) // Block audio.
+ assertEquals(1, inaudible) // Allow inaudible.
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDaoTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDaoTest.kt
new file mode 100644
index 0000000000..60eba935dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDaoTest.kt
@@ -0,0 +1,112 @@
+/* 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.sitepermissions.db
+
+import android.content.Context
+import androidx.core.net.toUri
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+class SitePermissionsDaoTest {
+
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: SitePermissionsDatabase
+ private lateinit var dao: SitePermissionsDao
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, SitePermissionsDatabase::class.java).build()
+ dao = database.sitePermissionsDao()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ }
+
+ @Test
+ fun testInsertingAndReadingSitePermissions() {
+ val origin = insertMockSitePermissions("https://www.mozilla.org")
+
+ val siteFromDb = dao.getSitePermissionsBy(origin)!!.toSitePermission()
+
+ assertEquals(origin, siteFromDb.origin)
+ assertEquals(BLOCKED, siteFromDb.camera)
+ assertEquals(AutoplayStatus.BLOCKED, siteFromDb.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, siteFromDb.autoplayInaudible)
+ }
+
+ @Test
+ fun testRemoveAllSitePermissions() {
+ for (index in 1..4) {
+ val origin = insertMockSitePermissions("https://www.mozilla$index.org")
+
+ val sitePermissionFromDb = dao.getSitePermissionsBy(origin)
+
+ assertEquals(origin, sitePermissionFromDb!!.origin)
+ }
+
+ dao.deleteAllSitePermissions()
+
+ val isEmpty = dao.getSitePermissions().isEmpty()
+
+ assertTrue(isEmpty)
+ }
+
+ @Test
+ fun testUpdateAndDeleteSitePermissions() {
+ val origin = insertMockSitePermissions("https://www.mozilla.org")
+ var siteFromDb = dao.getSitePermissionsBy(origin)!!.toSitePermission()
+
+ assertEquals(BLOCKED, siteFromDb.camera)
+ assertEquals(AutoplayStatus.BLOCKED, siteFromDb.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, siteFromDb.autoplayInaudible)
+
+ dao.update(
+ siteFromDb.copy(
+ camera = ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ ).toSitePermissionsEntity(),
+ )
+
+ siteFromDb = dao.getSitePermissionsBy(origin)!!.toSitePermission()
+
+ assertEquals(ALLOWED, siteFromDb.camera)
+ assertEquals(AutoplayStatus.ALLOWED, siteFromDb.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, siteFromDb.autoplayInaudible)
+
+ dao.deleteSitePermissions(siteFromDb.toSitePermissionsEntity())
+
+ val notFoundSitePermissions = dao.getSitePermissionsBy(origin)?.toSitePermission()
+
+ assertNull(notFoundSitePermissions)
+ }
+
+ private fun insertMockSitePermissions(url: String): String {
+ val origin = url.toUri().host!!
+ val sitePermissions = SitePermissions(
+ origin = origin,
+ camera = BLOCKED,
+ savedAt = System.currentTimeMillis(),
+ )
+ dao.insert(
+ sitePermissions.toSitePermissionsEntity(),
+ )
+ return origin
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/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/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorage.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorage.kt
new file mode 100644
index 0000000000..9941761d75
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorage.kt
@@ -0,0 +1,194 @@
+/* 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.sitepermissions
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.paging.DataSource
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.launch
+import mozilla.components.concept.engine.DataCleanable
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.Engine.BrowsingData.Companion.PERMISSIONS
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.Status
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissionsStorage
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.AUTOPLAY_AUDIBLE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.AUTOPLAY_INAUDIBLE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.BLUETOOTH
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.CAMERA
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.LOCAL_STORAGE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.LOCATION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.MEDIA_KEY_SYSTEM_ACCESS
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.MICROPHONE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.NOTIFICATION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.STORAGE_ACCESS
+import mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase
+import mozilla.components.feature.sitepermissions.db.toSitePermissionsEntity
+
+/**
+ * A storage implementation to save [SitePermissions] on disk.
+ */
+class OnDiskSitePermissionsStorage(
+ context: Context,
+ private val dataCleanable: DataCleanable? = null,
+) : SitePermissionsStorage {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var databaseInitializer = {
+ SitePermissionsDatabase.get(context)
+ }
+
+ private val coroutineScope = CoroutineScope(Main)
+ private val database by lazy { databaseInitializer() }
+
+ /**
+ * Persists the [sitePermissions] provided as a parameter.
+ * @param sitePermissions the [sitePermissions] to be stored.
+ * @param private indicates if the [SitePermissions] belongs to a private session.
+ */
+ override suspend fun save(
+ sitePermissions: SitePermissions,
+ request: PermissionRequest?,
+ private: Boolean,
+ ) {
+ if (private) return // never save private browsing site permissions
+ database
+ .sitePermissionsDao()
+ .insert(
+ sitePermissions.toSitePermissionsEntity(),
+ )
+ }
+
+ /**
+ * Replaces an existing SitePermissions with the values of [sitePermissions] provided as a parameter.
+ * @param sitePermissions the sitePermissions to be updated.
+ * @param private indicates if the [SitePermissions] belongs to a private session.
+ */
+ override suspend fun update(sitePermissions: SitePermissions, private: Boolean) {
+ if (private) return
+ coroutineScope.launch {
+ dataCleanable?.clearData(Engine.BrowsingData.select(PERMISSIONS), sitePermissions.origin)
+ }
+ database.sitePermissionsDao()
+ .update(sitePermissions.toSitePermissionsEntity())
+ }
+
+ /**
+ * Finds all SitePermissions that match the [origin].
+ * @param origin the site to be used as filter in the search.
+ * @param private indicates if the [SitePermissions] belongs to a private session.
+ */
+ override suspend fun findSitePermissionsBy(
+ origin: String,
+ includeTemporary: Boolean,
+ private: Boolean,
+ ): SitePermissions? {
+ if (private) return null
+ return database
+ .sitePermissionsDao()
+ .getSitePermissionsBy(origin)
+ ?.toSitePermission()
+ }
+
+ /**
+ * Returns all saved [SitePermissions] instances as a [DataSource.Factory].
+ *
+ * A consuming app can transform the data source into a `LiveData<PagedList>` of when using RxJava2 into a
+ * `Flowable<PagedList>` or `Observable<PagedList>`, that can be observed.
+ *
+ * - https://developer.android.com/topic/libraries/architecture/paging/data
+ * - https://developer.android.com/topic/libraries/architecture/paging/ui
+ */
+ override suspend fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissions> {
+ return database
+ .sitePermissionsDao()
+ .getSitePermissionsPaged()
+ .map { entity ->
+ entity.toSitePermission()
+ }
+ }
+
+ /**
+ * Finds all SitePermissions grouped by [Permission].
+ * @return a map of site grouped by [Permission].
+ */
+ suspend fun findAllSitePermissionsGroupedByPermission(): Map<Permission, List<SitePermissions>> {
+ val sitePermissions = all()
+ val map = mutableMapOf<Permission, MutableList<SitePermissions>>()
+
+ sitePermissions.forEach { permission ->
+ with(permission) {
+ map.putIfAllowed(BLUETOOTH, bluetooth, permission)
+ map.putIfAllowed(MICROPHONE, microphone, permission)
+ map.putIfAllowed(CAMERA, camera, permission)
+ map.putIfAllowed(LOCAL_STORAGE, localStorage, permission)
+ map.putIfAllowed(NOTIFICATION, notification, permission)
+ map.putIfAllowed(LOCATION, location, permission)
+ map.putIfAllowed(AUTOPLAY_AUDIBLE, autoplayAudible.toStatus(), permission)
+ map.putIfAllowed(AUTOPLAY_INAUDIBLE, autoplayInaudible.toStatus(), permission)
+ map.putIfAllowed(MEDIA_KEY_SYSTEM_ACCESS, mediaKeySystemAccess, permission)
+ map.putIfAllowed(STORAGE_ACCESS, crossOriginStorageAccess, permission)
+ }
+ }
+ return map
+ }
+
+ /**
+ * Deletes all sitePermissions that match the sitePermissions provided as a parameter.
+ * @param sitePermissions the sitePermissions to be deleted from the storage.
+ */
+ override suspend fun remove(sitePermissions: SitePermissions, private: Boolean) {
+ coroutineScope.launch {
+ dataCleanable?.clearData(Engine.BrowsingData.select(PERMISSIONS), sitePermissions.origin)
+ }
+ database
+ .sitePermissionsDao()
+ .deleteSitePermissions(
+ sitePermissions.toSitePermissionsEntity(),
+ )
+ }
+
+ /**
+ * Deletes all sitePermissions sitePermissions.
+ */
+ override suspend fun removeAll() {
+ coroutineScope.launch {
+ dataCleanable?.clearData(Engine.BrowsingData.select(PERMISSIONS))
+ }
+ return database
+ .sitePermissionsDao()
+ .deleteAllSitePermissions()
+ }
+
+ /**
+ * Returns all sitePermissions in the store.
+ */
+ override suspend fun all(): List<SitePermissions> {
+ return database
+ .sitePermissionsDao()
+ .getSitePermissions()
+ .map {
+ it.toSitePermission()
+ }
+ }
+
+ private fun MutableMap<Permission, MutableList<SitePermissions>>.putIfAllowed(
+ permission: Permission,
+ status: Status,
+ sitePermissions: SitePermissions,
+ ) {
+ if (status == ALLOWED) {
+ if (this.containsKey(permission)) {
+ this[permission]?.add(sitePermissions)
+ } else {
+ this[permission] = mutableListOf(sitePermissions)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt
new file mode 100644
index 0000000000..61a6b10d8b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt
@@ -0,0 +1,258 @@
+/* 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.sitepermissions
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.ImageView
+import android.widget.LinearLayout.LayoutParams
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatDialogFragment
+import androidx.core.content.ContextCompat
+
+internal const val KEY_SESSION_ID = "KEY_SESSION_ID"
+internal const val KEY_TITLE = "KEY_TITLE"
+private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY"
+private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT"
+private const val KEY_TITLE_ICON = "KEY_TITLE_ICON"
+private const val KEY_MESSAGE = "KEY_MESSAGE"
+private const val KEY_NEGATIVE_BUTTON_TEXT = "KEY_NEGATIVE_BUTTON_TEXT"
+private const val KEY_POSITIVE_BUTTON_BACKGROUND_COLOR = "KEY_POSITIVE_BUTTON_BACKGROUND_COLOR"
+private const val KEY_POSITIVE_BUTTON_TEXT_COLOR = "KEY_POSITIVE_BUTTON_TEXT_COLOR"
+private const val KEY_SHOULD_SHOW_LEARN_MORE_LINK = "KEY_SHOULD_SHOW_LEARN_MORE_LINK"
+private const val KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX = "KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX"
+private const val KEY_SHOULD_PRESELECT_DO_NOT_ASK_AGAIN_CHECKBOX = "KEY_SHOULD_PRESELECT_DO_NOT_ASK_AGAIN_CHECKBOX"
+private const val KEY_IS_NOTIFICATION_REQUEST = "KEY_IS_NOTIFICATION_REQUEST"
+private const val DEFAULT_VALUE = Int.MAX_VALUE
+private const val KEY_PERMISSION_ID = "KEY_PERMISSION_ID"
+
+internal open class SitePermissionsDialogFragment : AppCompatDialogFragment() {
+
+ // Safe Arguments
+
+ private val safeArguments get() = requireNotNull(arguments)
+
+ internal val sessionId: String get() =
+ safeArguments.getString(KEY_SESSION_ID, "")
+ internal val title: String get() =
+ safeArguments.getString(KEY_TITLE, "")
+ internal val icon get() =
+ safeArguments.getInt(KEY_TITLE_ICON, DEFAULT_VALUE)
+ internal val message: String? get() =
+ safeArguments.getString(KEY_MESSAGE, null)
+ internal val negativeButtonText: String? get() =
+ safeArguments.getString(KEY_NEGATIVE_BUTTON_TEXT, null)
+
+ internal val dialogGravity: Int get() =
+ safeArguments.getInt(KEY_DIALOG_GRAVITY, DEFAULT_VALUE)
+ internal val dialogShouldWidthMatchParent: Boolean get() =
+ safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT)
+
+ internal val positiveButtonBackgroundColor get() =
+ safeArguments.getInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, DEFAULT_VALUE)
+ internal val positiveButtonTextColor get() =
+ safeArguments.getInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, DEFAULT_VALUE)
+
+ internal val isNotificationRequest get() =
+ safeArguments.getBoolean(KEY_IS_NOTIFICATION_REQUEST, false)
+
+ internal val shouldShowLearnMoreLink: Boolean get() =
+ safeArguments.getBoolean(KEY_SHOULD_SHOW_LEARN_MORE_LINK, false)
+ internal val shouldShowDoNotAskAgainCheckBox: Boolean get() =
+ safeArguments.getBoolean(KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX, true)
+ internal val shouldPreselectDoNotAskAgainCheckBox: Boolean get() =
+ safeArguments.getBoolean(KEY_SHOULD_PRESELECT_DO_NOT_ASK_AGAIN_CHECKBOX, false)
+ internal val permissionRequestId: String get() =
+ safeArguments.getString(KEY_PERMISSION_ID, "")
+
+ // State
+
+ internal var feature: SitePermissionsFeature? = null
+ internal var userSelectionCheckBox: Boolean = false
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ userSelectionCheckBox = shouldPreselectDoNotAskAgainCheckBox
+
+ val sheetDialog = Dialog(requireContext())
+ sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ sheetDialog.setCanceledOnTouchOutside(true)
+
+ val rootView = createContainer()
+
+ sheetDialog.setContainerView(rootView)
+
+ sheetDialog.window?.apply {
+ if (dialogGravity != DEFAULT_VALUE) {
+ setGravity(dialogGravity)
+ }
+
+ if (dialogShouldWidthMatchParent) {
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ // This must be called after addContentView, or it won't fully fill to the edge.
+ setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+ }
+
+ return sheetDialog
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ super.onDismiss(dialog)
+ feature?.onDismiss(permissionRequestId, sessionId)
+ }
+
+ private fun Dialog.setContainerView(rootView: View) {
+ if (dialogShouldWidthMatchParent) {
+ setContentView(rootView)
+ } else {
+ addContentView(
+ rootView,
+ LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT,
+ ),
+ )
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ private fun createContainer(): View {
+ val rootView = LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_site_permissions_prompt,
+ null,
+ false,
+ )
+
+ rootView.findViewById<TextView>(R.id.title).text = title
+ rootView.findViewById<ImageView>(R.id.icon).setImageResource(icon)
+ message?.let {
+ rootView.findViewById<TextView>(R.id.message).apply {
+ visibility = VISIBLE
+ text = it
+ }
+ }
+ if (shouldShowLearnMoreLink) {
+ rootView.findViewById<TextView>(R.id.learn_more).apply {
+ visibility = VISIBLE
+ isLongClickable = false
+ setOnClickListener {
+ dismiss()
+ feature?.onLearnMorePress(permissionRequestId, sessionId)
+ }
+ }
+ }
+
+ val positiveButton = rootView.findViewById<Button>(R.id.allow_button)
+ val negativeButton = rootView.findViewById<Button>(R.id.deny_button)
+
+ positiveButton.setOnClickListener {
+ feature?.onPositiveButtonPress(permissionRequestId, sessionId, userSelectionCheckBox)
+ dismiss()
+ }
+
+ if (positiveButtonBackgroundColor != DEFAULT_VALUE) {
+ val backgroundTintList = ContextCompat.getColorStateList(requireContext(), positiveButtonBackgroundColor)
+ positiveButton.backgroundTintList = backgroundTintList
+ }
+
+ if (positiveButtonTextColor != DEFAULT_VALUE) {
+ val color = ContextCompat.getColor(requireContext(), positiveButtonTextColor)
+ positiveButton.setTextColor(color)
+ }
+
+ negativeButton.setOnClickListener {
+ feature?.onNegativeButtonPress(permissionRequestId, sessionId, userSelectionCheckBox)
+ dismiss()
+ }
+ if (isNotificationRequest) {
+ positiveButton.setText(R.string.mozac_feature_sitepermissions_always_allow)
+ negativeButton.setText(R.string.mozac_feature_sitepermissions_never_allow)
+ }
+ negativeButtonText?.let {
+ negativeButton.text = it
+ }
+
+ if (shouldShowDoNotAskAgainCheckBox) {
+ showDoNotAskAgainCheckbox(rootView, checked = shouldPreselectDoNotAskAgainCheckBox)
+ }
+
+ return rootView
+ }
+
+ private fun showDoNotAskAgainCheckbox(containerView: View, checked: Boolean) {
+ containerView.findViewById<CheckBox>(R.id.do_not_ask_again).apply {
+ visibility = VISIBLE
+ isChecked = checked
+ setOnCheckedChangeListener { _, isChecked ->
+ userSelectionCheckBox = isChecked
+ }
+ }
+ }
+
+ companion object {
+ fun newInstance(
+ sessionId: String,
+ title: String,
+ titleIcon: Int,
+ permissionRequestId: String = "",
+ feature: SitePermissionsFeature,
+ shouldShowDoNotAskAgainCheckBox: Boolean,
+ shouldSelectDoNotAskAgainCheckBox: Boolean = false,
+ isNotificationRequest: Boolean = false,
+ message: String? = null,
+ negativeButtonText: String? = null,
+ shouldShowLearnMoreLink: Boolean = false,
+ ): SitePermissionsDialogFragment {
+ val fragment = SitePermissionsDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ arguments.apply {
+ putString(KEY_SESSION_ID, sessionId)
+ putString(KEY_TITLE, title)
+ putInt(KEY_TITLE_ICON, titleIcon)
+ putString(KEY_MESSAGE, message)
+ putString(KEY_NEGATIVE_BUTTON_TEXT, negativeButtonText)
+ putString(KEY_PERMISSION_ID, permissionRequestId)
+ putBoolean(KEY_SHOULD_SHOW_LEARN_MORE_LINK, shouldShowLearnMoreLink)
+
+ putBoolean(KEY_IS_NOTIFICATION_REQUEST, isNotificationRequest)
+ if (isNotificationRequest) {
+ putBoolean(KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX, false)
+ putBoolean(KEY_SHOULD_PRESELECT_DO_NOT_ASK_AGAIN_CHECKBOX, true)
+ } else {
+ putBoolean(KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX, shouldShowDoNotAskAgainCheckBox)
+ putBoolean(KEY_SHOULD_PRESELECT_DO_NOT_ASK_AGAIN_CHECKBOX, shouldSelectDoNotAskAgainCheckBox)
+ }
+
+ feature.promptsStyling?.apply {
+ putInt(KEY_DIALOG_GRAVITY, gravity)
+ putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, shouldWidthMatchParent)
+
+ positiveButtonBackgroundColor?.apply {
+ putInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, this)
+ }
+
+ positiveButtonTextColor?.apply {
+ putInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, this)
+ }
+ }
+ }
+ fragment.feature = feature
+ fragment.arguments = arguments
+ return fragment
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsFacts.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsFacts.kt
new file mode 100644
index 0000000000..580df84f79
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsFacts.kt
@@ -0,0 +1,65 @@
+/* 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.sitepermissions
+
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.support.base.Component.FEATURE_SITEPERMISSIONS
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the site permissions prompt.
+ */
+class SitePermissionsFacts {
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val PERMISSIONS = "permissions"
+ }
+}
+
+internal fun emitPermissionDialogDisplayed(permission: Permission) = emitSitePermissionsFact(
+ action = Action.DISPLAY,
+ permissions = permission.name,
+)
+
+internal fun emitPermissionsDialogDisplayed(permissions: List<Permission>) = emitSitePermissionsFact(
+ action = Action.DISPLAY,
+ permissions = permissions.distinctBy { it.name }.joinToString { it.name },
+)
+
+internal fun emitPermissionDenied(permission: Permission) = emitSitePermissionsFact(
+ action = Action.CANCEL,
+ permissions = permission.name,
+)
+
+internal fun emitPermissionsDenied(permissions: List<Permission>) = emitSitePermissionsFact(
+ action = Action.CANCEL,
+ permissions = permissions.distinctBy { it.name }.joinToString { it.name },
+)
+
+internal fun emitPermissionAllowed(permission: Permission) = emitSitePermissionsFact(
+ action = Action.CONFIRM,
+ permissions = permission.name,
+)
+
+internal fun emitPermissionsAllowed(permissions: List<Permission>) = emitSitePermissionsFact(
+ action = Action.CONFIRM,
+ permissions = permissions.distinctBy { it.name }.joinToString { it.name },
+)
+
+private fun emitSitePermissionsFact(
+ action: Action,
+ permissions: String,
+) {
+ Fact(
+ FEATURE_SITEPERMISSIONS,
+ action,
+ SitePermissionsFacts.Items.PERMISSIONS,
+ permissions,
+ ).collect()
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsFeature.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsFeature.kt
new file mode 100644
index 0000000000..cea90ffe2d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsFeature.kt
@@ -0,0 +1,1057 @@
+/* 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.sitepermissions
+
+import android.Manifest.permission.CAMERA
+import android.Manifest.permission.RECORD_AUDIO
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.fragment.app.FragmentManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayAudibleBlockingAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayAudibleChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayInAudibleBlockingAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayInAudibleChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.CameraChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.LocationChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.MediaKeySystemAccesChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.MicrophoneChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.NotificationChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.PersistentStorageChangedAction
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.concept.engine.permission.Permission.AppAudio
+import mozilla.components.concept.engine.permission.Permission.AppCamera
+import mozilla.components.concept.engine.permission.Permission.AppLocationCoarse
+import mozilla.components.concept.engine.permission.Permission.AppLocationFine
+import mozilla.components.concept.engine.permission.Permission.ContentAudioCapture
+import mozilla.components.concept.engine.permission.Permission.ContentAudioMicrophone
+import mozilla.components.concept.engine.permission.Permission.ContentAutoPlayAudible
+import mozilla.components.concept.engine.permission.Permission.ContentAutoPlayInaudible
+import mozilla.components.concept.engine.permission.Permission.ContentCrossOriginStorageAccess
+import mozilla.components.concept.engine.permission.Permission.ContentGeoLocation
+import mozilla.components.concept.engine.permission.Permission.ContentMediaKeySystemAccess
+import mozilla.components.concept.engine.permission.Permission.ContentNotification
+import mozilla.components.concept.engine.permission.Permission.ContentPersistentStorage
+import mozilla.components.concept.engine.permission.Permission.ContentVideoCamera
+import mozilla.components.concept.engine.permission.Permission.ContentVideoCapture
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import mozilla.components.concept.engine.permission.SitePermissionsStorage
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.sitepermissions.SitePermissionsFeature.DialogConfig
+import mozilla.components.feature.tabs.TabsUseCases.SelectOrAddUseCase
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.OnNeedToRequestPermissions
+import mozilla.components.support.base.feature.PermissionsFeature
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+import mozilla.components.support.ktx.kotlin.getOrigin
+import mozilla.components.support.ktx.kotlin.stripDefaultPort
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
+import java.security.InvalidParameterException
+import mozilla.components.ui.icons.R as iconsR
+
+internal const val PROMPT_FRAGMENT_TAG = "mozac_feature_sitepermissions_prompt_dialog"
+
+@VisibleForTesting
+internal const val STORAGE_ACCESS_DOCUMENTATION_URL =
+ "https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API"
+
+/**
+ * This feature will collect [PermissionRequest] from [ContentState] and display
+ * suitable [SitePermissionsDialogFragment].
+ * Once the dialog is closed the [PermissionRequest] will be consumed.
+ *
+ * @property context a reference to the context.
+ * @property sessionId optional sessionId to be observed if null the selected session will be observed.
+ * @property storage the object in charge of persisting all the [SitePermissions] objects.
+ * @property sitePermissionsRules indicates how permissions should behave per permission category.
+ * @property fragmentManager a reference to a [FragmentManager], used to show permissions prompts.
+ * @property promptsStyling optional styling for prompts.
+ * @property dialogConfig optional customization for dialog initial state. See [DialogConfig].
+ * @property onNeedToRequestPermissions a callback invoked when permissions
+ * need to be requested. Once the request is completed, [onPermissionsResult] needs to be invoked.
+ * @property onShouldShowRequestPermissionRationale a callback that allows the feature to query
+ * the ActivityCompat.shouldShowRequestPermissionRationale or the Fragment.shouldShowRequestPermissionRationale values.
+ * @property exitFullscreenUseCase optional the use case in charge of exiting fullscreen
+ * @property shouldShowDoNotAskAgainCheckBox optional Visibility for Do not ask again Checkbox
+ **/
+
+@Suppress("TooManyFunctions", "LargeClass")
+class SitePermissionsFeature(
+ private val context: Context,
+ @set:VisibleForTesting
+ internal var sessionId: String? = null,
+ private val storage: SitePermissionsStorage = OnDiskSitePermissionsStorage(context),
+ var sitePermissionsRules: SitePermissionsRules? = null,
+ private val fragmentManager: FragmentManager,
+ var promptsStyling: PromptsStyling? = null,
+ private val dialogConfig: DialogConfig? = null,
+ override val onNeedToRequestPermissions: OnNeedToRequestPermissions,
+ val onShouldShowRequestPermissionRationale: (permission: String) -> Boolean,
+ private val store: BrowserStore,
+ private val exitFullscreenUseCase: SessionUseCases.ExitFullScreenUseCase = SessionUseCases(store).exitFullscreen,
+ private val shouldShowDoNotAskAgainCheckBox: Boolean = true,
+) : LifecycleAwareFeature, PermissionsFeature {
+ @VisibleForTesting
+ internal val selectOrAddUseCase by lazy {
+ SelectOrAddUseCase(store)
+ }
+
+ private val logger = Logger("SitePermissionsFeature")
+
+ internal val ioCoroutineScope by lazy { coroutineScopeInitializer() }
+
+ internal var coroutineScopeInitializer = {
+ CoroutineScope(Dispatchers.IO)
+ }
+ private var sitePermissionScope: CoroutineScope? = null
+ private var appPermissionScope: CoroutineScope? = null
+ private var loadingScope: CoroutineScope? = null
+
+ override fun start() {
+ fragmentManager.findFragmentByTag(PROMPT_FRAGMENT_TAG)?.let { fragment ->
+ // There's still a [SitePermissionsDialogFragment] visible from the last time. Re-attach
+ // this feature so that the fragment can invoke the callback on this feature once the user
+ // makes a selection. This can happen when the app was in the background and on resume
+ // the activity and fragments get recreated.
+ reattachFragment(fragment as SitePermissionsDialogFragment)
+ }
+
+ setupPermissionRequestsCollector()
+ setupAppPermissionRequestsCollector()
+ setupLoadingCollector()
+ }
+
+ @VisibleForTesting
+ internal fun setupLoadingCollector() {
+ loadingScope = store.flowScoped { flow ->
+ flow.mapNotNull { state ->
+ state.findTabOrCustomTabOrSelectedTab(sessionId)
+ }.distinctUntilChangedBy { it.content.loading }.collect { tab ->
+ if (tab.content.loading) {
+ // Clears stale permission indicators in the toolbar,
+ // after the session starts loading.
+ store.dispatch(UpdatePermissionHighlightsStateAction.Reset(tab.id))
+ storage.clearTemporaryPermissions()
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun setupAppPermissionRequestsCollector() {
+ appPermissionScope =
+ store.flowScoped { flow ->
+ flow.mapNotNull { state ->
+ state.findTabOrCustomTabOrSelectedTab(sessionId)?.content?.appPermissionRequestsList
+ }
+ .filterChanged { it }
+ .collect { appPermissionRequest ->
+ val permissions = appPermissionRequest.permissions.map { it.id ?: "" }
+ onNeedToRequestPermissions(permissions.toTypedArray())
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun setupPermissionRequestsCollector() {
+ sitePermissionScope =
+ store.flowScoped { flow ->
+ flow.mapNotNull { state ->
+ state.findTabOrCustomTabOrSelectedTab(sessionId)?.content?.permissionRequestsList
+ }
+ .filterChanged { it }
+ .collect { permissionRequest ->
+ val origin: String = permissionRequest.uri?.getOrigin().orEmpty()
+
+ if (origin.isEmpty()) {
+ permissionRequest.consumeAndReject()
+ } else {
+ if (permissionRequest.permissions.all { it.isSupported() }) {
+ onContentPermissionRequested(
+ permissionRequest,
+ origin,
+ )
+ } else {
+ permissionRequest.consumeAndReject()
+ }
+ }
+ }
+ }
+ }
+
+ private fun PermissionRequest.consumeAndReject() {
+ consumePermissionRequest(this)
+ this.reject()
+ }
+
+ @VisibleForTesting
+ internal fun consumePermissionRequest(
+ permissionRequest: PermissionRequest,
+ optionalSessionId: String? = null,
+ ) {
+ val thisSessionId = optionalSessionId ?: getCurrentTabState()?.id
+ thisSessionId?.let { sessionId ->
+ store.dispatch(ContentAction.ConsumePermissionsRequest(sessionId, permissionRequest))
+ }
+ }
+
+ @VisibleForTesting
+ internal fun consumeAppPermissionRequest(
+ appPermissionRequest: PermissionRequest,
+ optionalSessionId: String? = null,
+ ) {
+ val thisSessionId = optionalSessionId ?: getCurrentTabState()?.id
+ thisSessionId?.let { sessionId ->
+ store.dispatch(
+ ContentAction.ConsumeAppPermissionsRequest(
+ sessionId,
+ appPermissionRequest,
+ ),
+ )
+ }
+ }
+
+ override fun stop() {
+ sitePermissionScope?.cancel()
+ appPermissionScope?.cancel()
+ loadingScope?.cancel()
+ storage.clearTemporaryPermissions()
+ }
+
+ /**
+ * Notifies the feature that the permissions requested were completed.
+ *
+ * @param grantResults the grant results for the corresponding permissions
+ * @see [onNeedToRequestPermissions].
+ */
+ @Suppress("NestedBlockDepth")
+ override fun onPermissionsResult(permissions: Array<String>, grantResults: IntArray) {
+ val currentContentSate = getCurrentContentState()
+ val appPermissionRequest = findRequestedAppPermission(permissions)
+
+ if (appPermissionRequest != null && currentContentSate != null) {
+ val allPermissionWereGranted = grantResults.all { grantResult ->
+ grantResult == PackageManager.PERMISSION_GRANTED
+ }
+
+ if (grantResults.isNotEmpty() && allPermissionWereGranted) {
+ appPermissionRequest.grant()
+ } else {
+ appPermissionRequest.reject()
+ permissions.forEach { systemPermission ->
+ if (!onShouldShowRequestPermissionRationale(systemPermission)) {
+ // The system permission is denied permanently
+ storeSitePermissions(
+ currentContentSate,
+ appPermissionRequest,
+ status = BLOCKED,
+ )
+ }
+ }
+ }
+ consumeAppPermissionRequest(appPermissionRequest)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getCurrentContentState() = getCurrentTabState()?.content
+
+ @VisibleForTesting
+ internal fun getCurrentTabState() = store.state.findTabOrCustomTabOrSelectedTab(sessionId)
+
+ @VisibleForTesting
+ internal fun findRequestedAppPermission(permissions: Array<String>): PermissionRequest? {
+ return getCurrentContentState()?.appPermissionRequestsList?.find {
+ permissions.contains(it.permissions.first().id)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun findRequestedPermission(permissionId: String): PermissionRequest? {
+ return getCurrentContentState()?.permissionRequestsList?.find {
+ it.id == permissionId
+ }
+ }
+
+ @VisibleForTesting
+ internal fun onContentPermissionGranted(
+ permissionRequest: PermissionRequest,
+ shouldStore: Boolean,
+ ) {
+ permissionRequest.grant()
+ if (shouldStore) {
+ getCurrentContentState()?.let { contentState ->
+ storeSitePermissions(contentState, permissionRequest, ALLOWED)
+ }
+ } else {
+ storage.saveTemporary(permissionRequest)
+ }
+ }
+
+ internal fun onPositiveButtonPress(
+ permissionId: String,
+ sessionId: String,
+ shouldStore: Boolean,
+ ) {
+ findRequestedPermission(permissionId)?.let { permissionRequest ->
+ consumePermissionRequest(permissionRequest, sessionId)
+ onContentPermissionGranted(permissionRequest, shouldStore)
+
+ if (!permissionRequest.containsVideoAndAudioSources()) {
+ emitPermissionAllowed(permissionRequest.permissions.first())
+ } else {
+ emitPermissionsAllowed(permissionRequest.permissions)
+ }
+ }
+ }
+
+ internal fun onNegativeButtonPress(
+ permissionId: String,
+ sessionId: String,
+ shouldStore: Boolean,
+ ) {
+ findRequestedPermission(permissionId)?.let { permissionRequest ->
+ consumePermissionRequest(permissionRequest, sessionId)
+ onContentPermissionDeny(permissionRequest, shouldStore)
+
+ if (!permissionRequest.containsVideoAndAudioSources()) {
+ emitPermissionDenied(permissionRequest.permissions.first())
+ } else {
+ emitPermissionsDenied(permissionRequest.permissions)
+ }
+ }
+ }
+
+ internal fun onLearnMorePress(
+ permissionId: String,
+ sessionId: String,
+ ) {
+ findRequestedPermission(permissionId)?.let { permissionRequest ->
+ consumePermissionRequest(permissionRequest, sessionId)
+ onContentPermissionDeny(permissionRequest, false)
+
+ val permission = permissionRequest.permissions.first()
+ if (permission is ContentCrossOriginStorageAccess) {
+ store.state.findTabOrCustomTabOrSelectedTab(sessionId)?.let {
+ selectOrAddUseCase.invoke(
+ url = STORAGE_ACCESS_DOCUMENTATION_URL,
+ private = it.content.private,
+ source = SessionState.Source.Internal.TextSelection,
+ )
+ }
+ }
+ }
+ }
+
+ internal fun onDismiss(
+ permissionId: String,
+ sessionId: String,
+ ) {
+ findRequestedPermission(permissionId)?.let { permissionRequest ->
+ consumePermissionRequest(permissionRequest, sessionId)
+ onContentPermissionDeny(permissionRequest, false)
+ }
+ }
+
+ internal fun storeSitePermissions(
+ contentState: ContentState,
+ request: PermissionRequest,
+ status: SitePermissions.Status,
+ coroutineScope: CoroutineScope = ioCoroutineScope,
+ ) {
+ if (contentState.private) {
+ return
+ }
+ updatePermissionToolbarIndicator(request, status, true)
+ coroutineScope.launch {
+ request.uri?.getOrigin()?.let { origin ->
+ var sitePermissions =
+ storage.findSitePermissionsBy(origin, private = false)
+
+ if (sitePermissions == null) {
+ sitePermissions =
+ request.toSitePermissions(
+ origin,
+ status = status,
+ permissions = request.permissions,
+ )
+ storage.save(sitePermissions, request, private = false)
+ } else {
+ sitePermissions = request.toSitePermissions(origin, status, sitePermissions)
+ storage.update(sitePermissions = sitePermissions, private = false)
+ }
+ }
+ }
+ }
+
+ internal fun onContentPermissionDeny(
+ permissionRequest: PermissionRequest,
+ shouldStore: Boolean,
+ ) {
+ permissionRequest.reject()
+ if (shouldStore) {
+ getCurrentContentState()?.let { contentState ->
+ storeSitePermissions(contentState, permissionRequest, BLOCKED)
+ }
+ } else {
+ storage.saveTemporary(permissionRequest)
+ }
+ }
+
+ internal suspend fun onContentPermissionRequested(
+ permissionRequest: PermissionRequest,
+ origin: String,
+ coroutineScope: CoroutineScope = ioCoroutineScope,
+ ): SitePermissionsDialogFragment? {
+ // We want to warranty that all media permissions have the required system
+ // permissions are granted first, otherwise, we reject the request
+ if (permissionRequest.isMedia && !permissionRequest.areAllMediaPermissionsGranted) {
+ permissionRequest.reject()
+ consumePermissionRequest(permissionRequest)
+ return null
+ }
+ val tab = store.state.findTabOrCustomTabOrSelectedTab(sessionId)
+ if (tab == null) {
+ logger.error("Unable to find a tab for $sessionId rejecting the prompt request")
+ permissionRequest.reject()
+ consumePermissionRequest(permissionRequest)
+ return null
+ }
+
+ val permissionFromStorage = withContext(coroutineScope.coroutineContext) {
+ storage.findSitePermissionsBy(origin, private = tab.content.private)
+ }
+ val prompt = if (shouldApplyRules(permissionFromStorage)) {
+ handleRuledFlow(permissionRequest, origin)
+ } else {
+ handleNoRuledFlow(permissionFromStorage, permissionRequest, origin)
+ }
+
+ return if (prompt == null) {
+ null
+ } else {
+ // If we are in fullscreen, then exit to show the permission prompt.
+ // This won't have any effect if we are not in fullscreen.
+ exitFullscreenUseCase.invoke(tab.id)
+ prompt.show(fragmentManager, PROMPT_FRAGMENT_TAG)
+ prompt
+ }
+ }
+
+ @VisibleForTesting
+ internal fun handleNoRuledFlow(
+ permissionFromStorage: SitePermissions?,
+ permissionRequest: PermissionRequest,
+ host: String,
+ ): SitePermissionsDialogFragment? {
+ return if (shouldShowPrompt(permissionRequest, permissionFromStorage)) {
+ createPrompt(permissionRequest, host)
+ } else {
+ val status = if (permissionFromStorage.isGranted(permissionRequest)) {
+ permissionRequest.grant()
+ ALLOWED
+ } else {
+ permissionRequest.reject()
+ BLOCKED
+ }
+ updatePermissionToolbarIndicator(
+ permissionRequest,
+ status,
+ permissionFromStorage != null,
+ )
+ consumePermissionRequest(permissionRequest)
+ null
+ }
+ }
+
+ @VisibleForTesting
+ internal fun shouldShowPrompt(
+ permissionRequest: PermissionRequest,
+ permissionFromStorage: SitePermissions?,
+ ): Boolean {
+ return if (permissionRequest.isForAutoplay()) {
+ false
+ } else {
+ (
+ permissionFromStorage == null ||
+ !permissionRequest.doNotAskAgain(permissionFromStorage)
+ )
+ }
+ }
+
+ @VisibleForTesting
+ internal fun handleRuledFlow(
+ permissionRequest: PermissionRequest,
+ origin: String,
+ ): SitePermissionsDialogFragment? {
+ return when (sitePermissionsRules?.getActionFrom(permissionRequest)) {
+ SitePermissionsRules.Action.ALLOWED -> {
+ permissionRequest.grant()
+ consumePermissionRequest(permissionRequest)
+ updatePermissionToolbarIndicator(permissionRequest, ALLOWED)
+ null
+ }
+ SitePermissionsRules.Action.BLOCKED -> {
+ permissionRequest.reject()
+ consumePermissionRequest(permissionRequest)
+ updatePermissionToolbarIndicator(permissionRequest, BLOCKED)
+ null
+ }
+ SitePermissionsRules.Action.ASK_TO_ALLOW -> {
+ createPrompt(permissionRequest, origin)
+ }
+ null -> {
+ consumePermissionRequest(permissionRequest)
+ null
+ }
+ }
+ }
+
+ @VisibleForTesting
+ @Suppress("ComplexMethod")
+ internal fun updatePermissionToolbarIndicator(
+ request: PermissionRequest,
+ value: SitePermissions.Status,
+ permanent: Boolean = false,
+ ) {
+ val isAutoPlayAudibleBlocking: Boolean? =
+ if (request.isForAutoplayAudible()) value == BLOCKED else null
+ val isAutoPlayInAudibleBlocking: Boolean? =
+ if (request.isForAutoplayInaudible()) value == BLOCKED else null
+
+ getCurrentTabState()?.let { tab ->
+ // At the moment, we don't have APIs for controlling temporary permissions,
+ // after they are ALLOWED/BLOCKED, for this reason, we are not notifying users
+ // when permissions have changed from their default values,
+ // as they are not going have a way to change permissions.
+ // Either way, temporary permissions have to be ALLOWED/BLOCKED per sessions
+ // users are already aware of them.
+ // The autoplay permissions work a bit different, as it is a global permissions,
+ // users are never prompt to select its value, and it can't be temporary,
+ // they only can change it's value per site persistently.
+ if (permanent || request.isForAutoplay()) {
+ val action = when {
+ request.isForNotification() -> NotificationChangedAction(
+ tab.id,
+ value != sitePermissionsRules?.notification?.toStatus(),
+ )
+ request.isForCamera() -> CameraChangedAction(
+ tab.id,
+ value != sitePermissionsRules?.camera?.toStatus(),
+ )
+ request.isForLocation() -> LocationChangedAction(
+ tab.id,
+ value != sitePermissionsRules?.location?.toStatus(),
+ )
+ request.isForMicrophone() -> MicrophoneChangedAction(
+ tab.id,
+ value != sitePermissionsRules?.microphone?.toStatus(),
+ )
+ request.isForPersistentStorage() -> PersistentStorageChangedAction(
+ tab.id,
+ value != sitePermissionsRules?.persistentStorage?.toStatus(),
+ )
+ request.isForMediaKeySystemAccess() -> MediaKeySystemAccesChangedAction(
+ tab.id,
+ value != sitePermissionsRules?.mediaKeySystemAccess?.toStatus(),
+ )
+ request.isForAutoplayAudible() -> AutoPlayAudibleChangedAction(
+ tab.id,
+ value != sitePermissionsRules?.autoplayAudible?.toAutoplayStatus()
+ ?.toStatus(),
+ )
+ request.isForAutoplayInaudible() -> AutoPlayInAudibleChangedAction(
+ tab.id,
+ value != sitePermissionsRules?.autoplayInaudible?.toAutoplayStatus()
+ ?.toStatus(),
+ )
+ else -> null
+ }
+ action?.let {
+ store.dispatch(it)
+ }
+ }
+ isAutoPlayAudibleBlocking?.let {
+ store.dispatch(AutoPlayAudibleBlockingAction(tab.id, it))
+ }
+ isAutoPlayInAudibleBlocking?.let {
+ store.dispatch(AutoPlayInAudibleBlockingAction(tab.id, it))
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun shouldApplyRules(permissionFromStorage: SitePermissions?) =
+ sitePermissionsRules != null && permissionFromStorage == null
+
+ private fun PermissionRequest.doNotAskAgain(permissionFromStore: SitePermissions): Boolean {
+ return permissions.any { permission ->
+ when (permission) {
+ is ContentGeoLocation -> {
+ permissionFromStore.location.doNotAskAgain()
+ }
+ is ContentNotification -> {
+ permissionFromStore.notification.doNotAskAgain()
+ }
+ is ContentAudioCapture, is ContentAudioMicrophone -> {
+ permissionFromStore.microphone.doNotAskAgain()
+ }
+ is ContentVideoCamera, is ContentVideoCapture -> {
+ permissionFromStore.camera.doNotAskAgain()
+ }
+ is ContentPersistentStorage -> {
+ permissionFromStore.localStorage.doNotAskAgain()
+ }
+ is ContentMediaKeySystemAccess -> {
+ permissionFromStore.mediaKeySystemAccess.doNotAskAgain()
+ }
+ is ContentCrossOriginStorageAccess -> {
+ permissionFromStore.crossOriginStorageAccess.doNotAskAgain()
+ }
+ else -> false
+ }
+ }
+ }
+
+ private fun PermissionRequest.toSitePermissions(
+ host: String,
+ status: SitePermissions.Status,
+ initialSitePermission: SitePermissions = getInitialSitePermissions(host),
+ permissions: List<Permission> = this.permissions,
+ ): SitePermissions {
+ var sitePermissions = initialSitePermission
+ for (permission in permissions) {
+ sitePermissions = updateSitePermissionsStatus(status, permission, sitePermissions)
+ }
+ return sitePermissions
+ }
+
+ @VisibleForTesting
+ internal fun getInitialSitePermissions(
+ host: String,
+ ): SitePermissions {
+ val rules = sitePermissionsRules
+ return rules?.toSitePermissions(
+ host,
+ savedAt = System.currentTimeMillis(),
+ )
+ ?: SitePermissions(host, savedAt = System.currentTimeMillis())
+ }
+
+ private fun PermissionRequest.isForAutoplay() =
+ this.permissions.any { it is ContentAutoPlayInaudible || it is ContentAutoPlayAudible }
+
+ private fun PermissionRequest.isForNotification() =
+ this.permissions.any { it is ContentNotification }
+
+ private fun PermissionRequest.isForCamera() =
+ this.permissions.any {
+ it is ContentVideoCamera || it is ContentVideoCapture || it is AppCamera
+ }
+
+ private fun PermissionRequest.isForAutoplayInaudible() =
+ this.permissions.any { it is ContentAutoPlayInaudible }
+
+ private fun PermissionRequest.isForAutoplayAudible() =
+ this.permissions.any { it is ContentAutoPlayAudible }
+
+ private fun PermissionRequest.isForLocation() =
+ this.permissions.any {
+ it is ContentGeoLocation ||
+ it is AppLocationCoarse ||
+ it is AppLocationFine
+ }
+
+ private fun PermissionRequest.isForMicrophone() =
+ this.permissions.any {
+ it is ContentAudioCapture || it is ContentAudioMicrophone ||
+ it is AppAudio
+ }
+
+ private fun PermissionRequest.isForPersistentStorage() =
+ this.permissions.any { it is ContentPersistentStorage }
+
+ private fun PermissionRequest.isForMediaKeySystemAccess() =
+ this.permissions.any { it is ContentMediaKeySystemAccess }
+
+ @VisibleForTesting
+ internal fun updateSitePermissionsStatus(
+ status: SitePermissions.Status,
+ permission: Permission,
+ sitePermissions: SitePermissions,
+ ): SitePermissions {
+ return when (permission) {
+ is ContentGeoLocation, is AppLocationCoarse, is AppLocationFine -> {
+ sitePermissions.copy(location = status)
+ }
+ is ContentNotification -> {
+ sitePermissions.copy(notification = status)
+ }
+ is ContentAudioCapture, is ContentAudioMicrophone, is AppAudio -> {
+ sitePermissions.copy(microphone = status)
+ }
+ is ContentVideoCamera, is ContentVideoCapture, is AppCamera -> {
+ sitePermissions.copy(camera = status)
+ }
+ is ContentAutoPlayAudible -> {
+ sitePermissions.copy(autoplayAudible = status.toAutoplayStatus())
+ }
+ is ContentAutoPlayInaudible -> {
+ sitePermissions.copy(autoplayInaudible = status.toAutoplayStatus())
+ }
+ is ContentPersistentStorage -> {
+ sitePermissions.copy(localStorage = status)
+ }
+ is ContentMediaKeySystemAccess -> {
+ sitePermissions.copy(mediaKeySystemAccess = status)
+ }
+ is ContentCrossOriginStorageAccess -> {
+ sitePermissions.copy(crossOriginStorageAccess = status)
+ }
+ else ->
+ throw InvalidParameterException("$permission is not a valid permission.")
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createPrompt(
+ permissionRequest: PermissionRequest,
+ host: String,
+ ): SitePermissionsDialogFragment {
+ return if (!permissionRequest.containsVideoAndAudioSources()) {
+ val permission = permissionRequest.permissions.first()
+ handlingSingleContentPermissions(permissionRequest, permission, host).also {
+ emitPermissionDialogDisplayed(permission)
+ }
+ } else {
+ createSinglePermissionPrompt(
+ context,
+ host,
+ permissionRequest,
+ R.string.mozac_feature_sitepermissions_camera_and_microphone,
+ iconsR.drawable.mozac_ic_microphone_24,
+ showDoNotAskAgainCheckBox = shouldShowDoNotAskAgainCheckBox,
+ shouldSelectRememberChoice = dialogConfig?.shouldPreselectDoNotAskAgain
+ ?: DialogConfig.DEFAULT_PRESELECT_DO_NOT_ASK_AGAIN,
+ ).also {
+ emitPermissionsDialogDisplayed(permissionRequest.permissions)
+ }
+ }
+ }
+
+ @Suppress("LongMethod")
+ @VisibleForTesting
+ internal fun handlingSingleContentPermissions(
+ permissionRequest: PermissionRequest,
+ permission: Permission,
+ host: String,
+ ): SitePermissionsDialogFragment {
+ return when (permission) {
+ is ContentGeoLocation -> {
+ createSinglePermissionPrompt(
+ context,
+ host,
+ permissionRequest,
+ R.string.mozac_feature_sitepermissions_location_title,
+ iconsR.drawable.mozac_ic_location_24,
+ showDoNotAskAgainCheckBox = shouldShowDoNotAskAgainCheckBox,
+ shouldSelectRememberChoice = dialogConfig?.shouldPreselectDoNotAskAgain
+ ?: DialogConfig.DEFAULT_PRESELECT_DO_NOT_ASK_AGAIN,
+ )
+ }
+ is ContentNotification -> {
+ createSinglePermissionPrompt(
+ context,
+ host,
+ permissionRequest,
+ R.string.mozac_feature_sitepermissions_notification_title,
+ iconsR.drawable.mozac_ic_notification_24,
+ showDoNotAskAgainCheckBox = false,
+ shouldSelectRememberChoice = false,
+ isNotificationRequest = true,
+ )
+ }
+ is ContentAudioCapture, is ContentAudioMicrophone -> {
+ createSinglePermissionPrompt(
+ context,
+ host,
+ permissionRequest,
+ R.string.mozac_feature_sitepermissions_microfone_title,
+ iconsR.drawable.mozac_ic_microphone_24,
+ showDoNotAskAgainCheckBox = shouldShowDoNotAskAgainCheckBox,
+ shouldSelectRememberChoice = dialogConfig?.shouldPreselectDoNotAskAgain
+ ?: DialogConfig.DEFAULT_PRESELECT_DO_NOT_ASK_AGAIN,
+ )
+ }
+ is ContentVideoCamera, is ContentVideoCapture -> {
+ createSinglePermissionPrompt(
+ context,
+ host,
+ permissionRequest,
+ R.string.mozac_feature_sitepermissions_camera_title,
+ iconsR.drawable.mozac_ic_camera_24,
+ showDoNotAskAgainCheckBox = shouldShowDoNotAskAgainCheckBox,
+ shouldSelectRememberChoice = dialogConfig?.shouldPreselectDoNotAskAgain
+ ?: DialogConfig.DEFAULT_PRESELECT_DO_NOT_ASK_AGAIN,
+ )
+ }
+ is ContentPersistentStorage -> {
+ createSinglePermissionPrompt(
+ context,
+ host,
+ permissionRequest,
+ R.string.mozac_feature_sitepermissions_persistent_storage_title,
+ iconsR.drawable.mozac_ic_storage_24,
+ showDoNotAskAgainCheckBox = false,
+ shouldSelectRememberChoice = true,
+ )
+ }
+ is ContentMediaKeySystemAccess -> {
+ createSinglePermissionPrompt(
+ context,
+ host,
+ permissionRequest,
+ R.string.mozac_feature_sitepermissions_media_key_system_access_title,
+ iconsR.drawable.mozac_ic_link_24,
+ showDoNotAskAgainCheckBox = false,
+ shouldSelectRememberChoice = true,
+ )
+ }
+ is ContentCrossOriginStorageAccess -> {
+ createContentCrossOriginStorageAccessPermissionPrompt(
+ context = context,
+ host = host,
+ permissionRequest = permissionRequest,
+ showDoNotAskAgainCheckBox = false,
+ shouldSelectRememberChoice = true,
+ )
+ }
+ else ->
+ throw InvalidParameterException("$permission is not a valid permission.")
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createSinglePermissionPrompt(
+ context: Context,
+ host: String,
+ permissionRequest: PermissionRequest,
+ @StringRes titleId: Int,
+ @DrawableRes iconId: Int,
+ showDoNotAskAgainCheckBox: Boolean,
+ shouldSelectRememberChoice: Boolean,
+ isNotificationRequest: Boolean = false,
+ ): SitePermissionsDialogFragment {
+ val title = context.getString(titleId, host)
+
+ val currentSessionId: String = store.state.findTabOrCustomTabOrSelectedTab(sessionId)?.id
+ ?: throw IllegalStateException("Unable to find session for $sessionId or selected session")
+
+ return SitePermissionsDialogFragment.newInstance(
+ currentSessionId,
+ title,
+ iconId,
+ permissionRequest.id,
+ this,
+ showDoNotAskAgainCheckBox,
+ isNotificationRequest = isNotificationRequest,
+ shouldSelectDoNotAskAgainCheckBox = shouldSelectRememberChoice,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun createContentCrossOriginStorageAccessPermissionPrompt(
+ context: Context,
+ host: String,
+ permissionRequest: PermissionRequest,
+ showDoNotAskAgainCheckBox: Boolean,
+ shouldSelectRememberChoice: Boolean,
+ ): SitePermissionsDialogFragment {
+ val currentSession = store.state.findTabOrCustomTabOrSelectedTab(sessionId)
+ ?: throw IllegalStateException("Unable to find session for $sessionId or selected session")
+
+ val title = context.getString(
+ R.string.mozac_feature_sitepermissions_storage_access_title,
+ host.stripDefaultPort(),
+ currentSession.content.url.stripDefaultPort(),
+ )
+ val message = context.getString(
+ R.string.mozac_feature_sitepermissions_storage_access_message,
+ host.stripDefaultPort(),
+ )
+ val negativeButtonText = context.getString(R.string.mozac_feature_sitepermissions_storage_access_not_allow)
+
+ return SitePermissionsDialogFragment.newInstance(
+ sessionId = currentSession.id,
+ title = title,
+ titleIcon = iconsR.drawable.mozac_ic_cookies_24,
+ message = message,
+ negativeButtonText = negativeButtonText,
+ permissionRequestId = permissionRequest.id,
+ feature = this,
+ shouldShowDoNotAskAgainCheckBox = showDoNotAskAgainCheckBox,
+ isNotificationRequest = false,
+ shouldSelectDoNotAskAgainCheckBox = shouldSelectRememberChoice,
+ shouldShowLearnMoreLink = true,
+ )
+ }
+
+ private val PermissionRequest.isMedia: Boolean
+ get() {
+ return when (permissions.first()) {
+ is ContentVideoCamera, is ContentVideoCapture,
+ is ContentAudioCapture, is ContentAudioMicrophone,
+ -> true
+ else -> false
+ }
+ }
+
+ private val PermissionRequest.areAllMediaPermissionsGranted: Boolean
+ get() {
+ val systemPermissions = mutableListOf<String>()
+ permissions.forEach { permission ->
+ when (permission) {
+ is ContentVideoCamera, is ContentVideoCapture -> {
+ systemPermissions.add(CAMERA)
+ }
+ is ContentAudioCapture, is ContentAudioMicrophone -> {
+ systemPermissions.add(RECORD_AUDIO)
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+ return systemPermissions.all { context.isPermissionGranted((it)) }
+ }
+
+ data class PromptsStyling(
+ val gravity: Int,
+ val shouldWidthMatchParent: Boolean = false,
+ @ColorRes
+ val positiveButtonBackgroundColor: Int? = null,
+ @ColorRes
+ val positiveButtonTextColor: Int? = null,
+ )
+
+ /**
+ * Customization options for feature request dialog
+ */
+ data class DialogConfig(
+ /** Use **true** to pre-select "Do not ask again" checkbox. */
+ val shouldPreselectDoNotAskAgain: Boolean = false,
+ ) {
+
+ companion object {
+ /** Default values for [DialogConfig.shouldPreselectDoNotAskAgain] */
+ internal const val DEFAULT_PRESELECT_DO_NOT_ASK_AGAIN = false
+ }
+ }
+
+ /**
+ * Re-attaches a fragment that is still visible but not linked to this feature anymore.
+ */
+ private fun reattachFragment(fragment: SitePermissionsDialogFragment) {
+ val currentTab = store.state.findTabOrCustomTabOrSelectedTab(fragment.sessionId)?.content
+ if (currentTab == null || (noPermissionRequests(currentTab))) {
+ fragmentManager.beginTransaction()
+ .remove(fragment)
+ .commitAllowingStateLoss()
+ } else {
+ // Re-assign the feature instance so that the fragment can invoke us once the
+ // user makes a selection or cancels the dialog.
+ fragment.feature = this
+ }
+ }
+
+ @VisibleForTesting
+ internal fun noPermissionRequests(contentState: ContentState) =
+ contentState.appPermissionRequestsList.isEmpty() &&
+ contentState.permissionRequestsList.isEmpty()
+}
+
+internal fun SitePermissions?.isGranted(permissionRequest: PermissionRequest): Boolean {
+ return if (this != null) {
+ permissionRequest.permissions.all { permission ->
+ isPermissionGranted(permission, this)
+ }
+ } else {
+ false
+ }
+}
+
+@VisibleForTesting
+internal fun isPermissionGranted(
+ permission: Permission,
+ permissionFromStorage: SitePermissions,
+): Boolean {
+ return when (permission) {
+ is ContentGeoLocation -> {
+ permissionFromStorage.location.isAllowed()
+ }
+ is ContentNotification -> {
+ permissionFromStorage.notification.isAllowed()
+ }
+ is ContentAudioCapture, is ContentAudioMicrophone -> {
+ permissionFromStorage.microphone.isAllowed()
+ }
+ is ContentVideoCamera, is ContentVideoCapture -> {
+ permissionFromStorage.camera.isAllowed()
+ }
+ is ContentPersistentStorage -> {
+ permissionFromStorage.localStorage.isAllowed()
+ }
+ is ContentCrossOriginStorageAccess -> {
+ permissionFromStorage.crossOriginStorageAccess.isAllowed()
+ }
+ is ContentMediaKeySystemAccess -> {
+ permissionFromStorage.mediaKeySystemAccess.isAllowed()
+ }
+ is ContentAutoPlayAudible -> {
+ permissionFromStorage.autoplayAudible.isAllowed()
+ }
+ is ContentAutoPlayInaudible -> {
+ permissionFromStorage.autoplayInaudible.isAllowed()
+ }
+ else ->
+ throw InvalidParameterException("$permission is not a valid permission.")
+ }
+}
+
+private fun Permission.isSupported(): Boolean {
+ return when (this) {
+ is ContentGeoLocation,
+ is ContentNotification,
+ is ContentPersistentStorage,
+ is ContentCrossOriginStorageAccess,
+ is ContentAudioCapture, is ContentAudioMicrophone,
+ is ContentVideoCamera, is ContentVideoCapture,
+ is ContentAutoPlayAudible, is ContentAutoPlayInaudible,
+ is ContentMediaKeySystemAccess,
+ -> true
+ else -> false
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsRules.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsRules.kt
new file mode 100644
index 0000000000..a58d50da15
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsRules.kt
@@ -0,0 +1,126 @@
+/* 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.sitepermissions
+
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ASK_TO_ALLOW
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED
+
+/**
+ * Indicate how site permissions must behave by permission category.
+ */
+data class SitePermissionsRules constructor(
+ val camera: Action,
+ val location: Action,
+ val notification: Action,
+ val microphone: Action,
+ val autoplayAudible: AutoplayAction,
+ val autoplayInaudible: AutoplayAction,
+ val persistentStorage: Action,
+ val mediaKeySystemAccess: Action,
+ val crossOriginStorageAccess: Action,
+) {
+
+ enum class Action {
+ ALLOWED, BLOCKED, ASK_TO_ALLOW;
+
+ fun toStatus(): SitePermissions.Status = when (this) {
+ ALLOWED -> SitePermissions.Status.ALLOWED
+ BLOCKED -> SitePermissions.Status.BLOCKED
+ ASK_TO_ALLOW -> SitePermissions.Status.NO_DECISION
+ }
+ }
+
+ /**
+ * Autoplay requests will never prompt the user
+ */
+ enum class AutoplayAction {
+ ALLOWED, BLOCKED;
+
+ internal fun toAction(): Action = when (this) {
+ ALLOWED -> Action.ALLOWED
+ BLOCKED -> Action.BLOCKED
+ }
+
+ /**
+ * Convert from an AutoplayAction to an AutoplayStatus.
+ */
+ fun toAutoplayStatus(): AutoplayStatus = when (this) {
+ ALLOWED -> AutoplayStatus.ALLOWED
+ BLOCKED -> AutoplayStatus.BLOCKED
+ }
+ }
+
+ internal fun getActionFrom(request: PermissionRequest): Action {
+ return if (request.containsVideoAndAudioSources()) {
+ getActionForCombinedPermission()
+ } else {
+ getActionForSinglePermission(request.permissions.first())
+ }
+ }
+
+ private fun getActionForSinglePermission(permission: Permission): Action {
+ return when (permission) {
+ is Permission.ContentGeoLocation -> {
+ location
+ }
+ is Permission.ContentNotification -> {
+ notification
+ }
+ is Permission.ContentPersistentStorage -> {
+ persistentStorage
+ }
+ is Permission.ContentAudioCapture, is Permission.ContentAudioMicrophone -> {
+ microphone
+ }
+ is Permission.ContentVideoCamera, is Permission.ContentVideoCapture -> {
+ camera
+ }
+ is Permission.ContentAutoPlayAudible -> {
+ autoplayAudible.toAction()
+ }
+ is Permission.ContentAutoPlayInaudible -> {
+ autoplayInaudible.toAction()
+ }
+ is Permission.ContentMediaKeySystemAccess -> {
+ mediaKeySystemAccess
+ }
+ is Permission.ContentCrossOriginStorageAccess -> {
+ crossOriginStorageAccess
+ }
+ else -> ASK_TO_ALLOW
+ }
+ }
+
+ private fun getActionForCombinedPermission(): Action {
+ return if (camera == BLOCKED || microphone == BLOCKED) {
+ BLOCKED
+ } else {
+ ASK_TO_ALLOW
+ }
+ }
+
+ /**
+ * Converts a [SitePermissionsRules] object into a [SitePermissions] .
+ */
+ fun toSitePermissions(origin: String, savedAt: Long = System.currentTimeMillis()): SitePermissions {
+ return SitePermissions(
+ origin = origin,
+ location = location.toStatus(),
+ notification = notification.toStatus(),
+ microphone = microphone.toStatus(),
+ camera = camera.toStatus(),
+ autoplayAudible = autoplayAudible.toAutoplayStatus(),
+ autoplayInaudible = autoplayInaudible.toAutoplayStatus(),
+ localStorage = persistentStorage.toStatus(),
+ mediaKeySystemAccess = mediaKeySystemAccess.toStatus(),
+ crossOriginStorageAccess = crossOriginStorageAccess.toStatus(),
+ savedAt = savedAt,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDao.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDao.kt
new file mode 100644
index 0000000000..75f828aae4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDao.kt
@@ -0,0 +1,40 @@
+/* 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.sitepermissions.db
+
+import androidx.paging.DataSource
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+
+/**
+ * Internal dao for accessing and modifying sitePermissions in the database.
+ */
+@Dao
+internal interface SitePermissionsDao {
+
+ @Insert
+ fun insert(entity: SitePermissionsEntity): Long
+
+ @Update
+ fun update(entity: SitePermissionsEntity)
+
+ @Query("SELECT * FROM site_permissions ORDER BY saved_at DESC")
+ fun getSitePermissions(): List<SitePermissionsEntity>
+
+ @Query("SELECT * FROM site_permissions where origin =:origin LIMIT 1")
+ fun getSitePermissionsBy(origin: String): SitePermissionsEntity?
+
+ @Delete
+ fun deleteSitePermissions(entity: SitePermissionsEntity)
+
+ @Query("DELETE FROM site_permissions")
+ fun deleteAllSitePermissions()
+
+ @Query("SELECT * FROM site_permissions ORDER BY saved_at DESC")
+ fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissionsEntity>
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDatabase.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDatabase.kt
new file mode 100644
index 0000000000..bdf82b53e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsDatabase.kt
@@ -0,0 +1,189 @@
+/* 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.sitepermissions.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import mozilla.components.concept.engine.permission.SitePermissions
+
+/**
+ * Internal database for saving site permissions.
+ */
+@Database(entities = [SitePermissionsEntity::class], version = 8)
+@TypeConverters(StatusConverter::class)
+internal abstract class SitePermissionsDatabase : RoomDatabase() {
+ abstract fun sitePermissionsDao(): SitePermissionsDao
+
+ companion object {
+ @Volatile
+ private var instance: SitePermissionsDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): SitePermissionsDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ SitePermissionsDatabase::class.java,
+ "site_permissions_database",
+ ).addMigrations(
+ Migrations.migration_1_2,
+ ).addMigrations(
+ Migrations.migration_2_3,
+ ).addMigrations(
+ Migrations.migration_3_4,
+ ).addMigrations(
+ Migrations.migration_4_5,
+ ).addMigrations(
+ Migrations.migration_5_6,
+ ).addMigrations(
+ Migrations.migration_6_7,
+ ).addMigrations(
+ Migrations.migration_7_8,
+ ).build().also { instance = it }
+ }
+ }
+}
+
+@Suppress("unused")
+internal class StatusConverter {
+ private val autoplayStatusArray = SitePermissions.AutoplayStatus.values()
+
+ private val statusArray = SitePermissions.Status.values()
+
+ @TypeConverter
+ fun toInt(status: SitePermissions.Status): Int {
+ return status.id
+ }
+
+ @TypeConverter
+ fun toStatus(index: Int): SitePermissions.Status? {
+ return statusArray.find { it.id == index }
+ }
+
+ @TypeConverter
+ fun toInt(status: SitePermissions.AutoplayStatus): Int {
+ return status.id
+ }
+
+ @TypeConverter
+ fun toAutoplayStatus(index: Int): SitePermissions.AutoplayStatus {
+ return autoplayStatusArray.find { it.id == index } ?: SitePermissions.AutoplayStatus.BLOCKED
+ }
+}
+
+internal object Migrations {
+ val migration_1_2 = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Version 1 is used in Nightly builds of Fenix, but not in production. Let's just skip actually migrating
+ // anything and let's re-create the "site_permissions" table.
+
+ db.execSQL("DROP TABLE site_permissions")
+ db.execSQL(
+ "CREATE TABLE IF NOT EXISTS `site_permissions` (" +
+ "`origin` TEXT NOT NULL, " +
+ "`location` INTEGER NOT NULL, " +
+ "`notification` INTEGER NOT NULL, " +
+ "`microphone` INTEGER NOT NULL, " +
+ "`camera` INTEGER NOT NULL, " +
+ "`bluetooth` INTEGER NOT NULL, " +
+ "`local_storage` INTEGER NOT NULL, " +
+ "`saved_at` INTEGER NOT NULL," +
+ " PRIMARY KEY(`origin`))",
+ )
+ }
+ }
+
+ @Suppress("MagicNumber")
+ val migration_2_3 = object : Migration(2, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ val haveAutoPlayColumns = db.query("SELECT * FROM site_permissions").columnCount == 10
+ // We just want to apply this migration for user that do not have
+ // the new autoplay fields autoplay_audible and autoplay_inaudible
+ if (!haveAutoPlayColumns) {
+ db.execSQL(
+ "ALTER TABLE site_permissions ADD COLUMN autoplay_audible INTEGER NOT NULL DEFAULT ''",
+ )
+ db.execSQL(
+ "ALTER TABLE site_permissions ADD COLUMN autoplay_inaudible INTEGER NOT NULL DEFAULT ''",
+ )
+
+ db.execSQL(
+ " UPDATE site_permissions" +
+ " SET autoplay_audible = -1, " + // BLOCKED by default
+ " `autoplay_inaudible` = 1", // ALLOWED by default
+ )
+ }
+ }
+ }
+
+ @Suppress("MagicNumber")
+ val migration_3_4 = object : Migration(3, 4) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ val hasEmeColumn = db.query("SELECT * FROM site_permissions").columnCount == 11
+ if (!hasEmeColumn) {
+ db.execSQL(
+ "ALTER TABLE site_permissions ADD COLUMN media_key_system_access INTEGER NOT NULL DEFAULT 0",
+ )
+ // default is NO_DECISION
+ db.execSQL("UPDATE site_permissions SET media_key_system_access = 0")
+ }
+ }
+ }
+
+ @Suppress("MagicNumber")
+ val migration_4_5 = object : Migration(4, 5) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Updating any previous autoplay sites with 0 (NO_DECISION) with the supported values
+ // Autoplay permission doesn't support 0 (NO_DECISION),
+ // it only supports 1 (ALLOWED) or -1 (BLOCKED)
+ db.execSQL("UPDATE site_permissions SET autoplay_audible = -1 WHERE autoplay_audible = 0 ")
+ db.execSQL("UPDATE site_permissions SET autoplay_inaudible = 1 WHERE autoplay_inaudible = 0 ")
+ }
+ }
+
+ @Suppress("MagicNumber")
+ val migration_5_6 = object : Migration(5, 6) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL(
+ "UPDATE site_permissions SET origin = 'https://'||origin||':443'",
+ )
+ }
+ }
+
+ @Suppress("MagicNumber")
+ val migration_6_7 = object : Migration(6, 7) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Update any site with our previous default value (block audio and video) to block audio only.
+ // autoplay_audible BLOCKED (-1) and autoplay_inaudible BLOCKED (-1) to
+ // autoplay_audible BLOCKED (-1) and autoplay_inaudible ALLOWED (1)
+ // This match the default value of desktop block audio only.
+ db.execSQL(
+ "UPDATE site_permissions SET autoplay_audible = -1, autoplay_inaudible= 1 " +
+ "WHERE autoplay_audible = -1 AND autoplay_inaudible = -1",
+ )
+ }
+ }
+
+ @Suppress("MagicNumber")
+ val migration_7_8 = object : Migration(7, 8) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ val hasCrossOriginStorageAccessColumn = db.query("SELECT * FROM site_permissions").columnCount == 12
+ if (!hasCrossOriginStorageAccessColumn) {
+ db.execSQL(
+ "ALTER TABLE site_permissions ADD COLUMN cross_origin_storage_access INTEGER NOT NULL DEFAULT 0",
+ )
+ // default is NO_DECISION
+ db.execSQL("UPDATE site_permissions SET cross_origin_storage_access = 0")
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsEntity.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsEntity.kt
new file mode 100644
index 0000000000..39346ebab0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/db/SitePermissionsEntity.kt
@@ -0,0 +1,89 @@
+/* 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.sitepermissions.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.concept.engine.permission.SitePermissions
+
+/**
+ * Internal entity representing a site permission as it gets saved to the database.
+ */
+@Entity(tableName = "site_permissions")
+internal data class SitePermissionsEntity(
+
+ @PrimaryKey
+ @ColumnInfo(name = "origin")
+ var origin: String,
+
+ @ColumnInfo(name = "location")
+ var location: SitePermissions.Status,
+
+ @ColumnInfo(name = "notification")
+ var notification: SitePermissions.Status,
+
+ @ColumnInfo(name = "microphone")
+ var microphone: SitePermissions.Status,
+
+ @ColumnInfo(name = "camera")
+ var camera: SitePermissions.Status,
+
+ @ColumnInfo(name = "bluetooth")
+ var bluetooth: SitePermissions.Status,
+
+ @ColumnInfo(name = "local_storage")
+ var localStorage: SitePermissions.Status,
+
+ @ColumnInfo(name = "autoplay_audible")
+ var autoplayAudible: SitePermissions.AutoplayStatus,
+
+ @ColumnInfo(name = "autoplay_inaudible")
+ var autoplayInaudible: SitePermissions.AutoplayStatus,
+
+ @ColumnInfo(name = "media_key_system_access")
+ var mediaKeySystemAccess: SitePermissions.Status,
+
+ @ColumnInfo(name = "cross_origin_storage_access")
+ var crossOriginStorageAccess: SitePermissions.Status,
+
+ @ColumnInfo(name = "saved_at")
+ var savedAt: Long,
+) {
+
+ internal fun toSitePermission(): SitePermissions {
+ return SitePermissions(
+ origin,
+ location,
+ notification,
+ microphone,
+ camera,
+ bluetooth,
+ localStorage,
+ autoplayAudible,
+ autoplayInaudible,
+ mediaKeySystemAccess,
+ crossOriginStorageAccess,
+ savedAt,
+ )
+ }
+}
+
+internal fun SitePermissions.toSitePermissionsEntity(): SitePermissionsEntity {
+ return SitePermissionsEntity(
+ origin,
+ location,
+ notification,
+ microphone,
+ camera,
+ bluetooth,
+ localStorage,
+ autoplayAudible,
+ autoplayInaudible,
+ mediaKeySystemAccess,
+ crossOriginStorageAccess,
+ savedAt,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/layout/mozac_site_permissions_prompt.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/layout/mozac_site_permissions_prompt.xml
new file mode 100644
index 0000000000..482205f133
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/layout/mozac_site_permissions_prompt.xml
@@ -0,0 +1,127 @@
+<!-- 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/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:windowBackground"
+ android:orientation="vertical"
+ tools:ignore="Overdraw">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ app:tint="?android:attr/textColorPrimary"
+ tools:src="@android:drawable/ic_menu_camera"
+ tools:tint="#000000" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/icon"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="11dp"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ tools:text="Allow wikipedia.org to use your camera?"
+ tools:textColor="#000000" />
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title"
+ android:layout_alignStart="@id/title"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="11dp"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ tools:text="This is a message offering more context about the permission."
+ tools:textColor="#000000"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/learn_more"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/message"
+ android:layout_alignStart="@id/title"
+ android:layout_marginStart="3dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="11dp"
+ android:layout_toEndOf="@id/icon"
+ android:paddingStart="5dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="5dp"
+ android:text="@string/mozac_feature_sitepermissions_learn_more_title"
+ android:textColor="?android:attr/textColorLink"
+ android:textSize="16sp"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <CheckBox
+ android:id="@+id/do_not_ask_again"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/learn_more"
+ android:layout_alignStart="@id/title"
+ android:layout_marginTop="2dp"
+ android:checked="true"
+ android:paddingStart="0dp"
+ android:paddingTop="18dp"
+ android:paddingBottom="14dp"
+ android:paddingEnd="4dp"
+ android:text="@string/mozac_feature_sitepermissions_do_not_ask_again_on_this_site2"
+ android:textColor="?android:attr/textColorPrimary"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <Button
+ android:id="@+id/deny_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/do_not_ask_again"
+ android:layout_marginTop="2dp"
+ android:layout_toStartOf="@id/allow_button"
+ android:text="@string/mozac_feature_sitepermissions_not_allow"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/allow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/do_not_ask_again"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="2dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/mozac_feature_sitepermissions_allow"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..6c6af28974
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-am/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s ማሳወቂያዎችን እንዲልክ ይፈቀድለት?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s ካሜራዎን እንዲጠቀም ይፈቀድለት?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s ማይክሮፎንዎን እንዲጠቀም ይፈቀድለት?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s አካባቢዎን እንዲጠቀም ይፈቀድለት?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s የእርስዎን ካሜራ እና ማይክሮፎን እንዲጠቀም ይፈቀድለት?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ማይክሮፎን 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">የኋላ ካሜራ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">የፊት ካሜራ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ፍቀድ</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">አትፍቀድ</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">የዚህን ድረ-ገፅ ውሳኔ አስታውስ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ሁሌም</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">በፍጹም</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s በቋሚ ማከማቻ ውስጥ ውሂብ እንዲያከማች ይፈቀድለት?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s በDRM የሚቆጣጠረውን ይዘት እንዲያጫውት ይፈቀድለት?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s ኩኪዎቹን በ%2$s ላይ እንዲጠቀም ይፈቀድለት?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">%s ለምን ይህ ውሂብ እንደሚያስፈልገው ግልጽ ካልሆነ መዳረሻን ማገድ ይፈልጉ ይሆናል።</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">አግድ</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ተጨማሪ ይወቁ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..1d2fffe064
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-an/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permitir que %1$s ninvie notificacions?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permitir que %1$s emplegue la tuya camara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permitir que %1$s emplegue lo tuyo microfono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permitir que %1$s emplege la tuya localización?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permitir que %1$s emplegue la camara y lo microfono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Camara zaguera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Camara debantera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Recordar la decisión pa este puesto</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Siempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..9447cec41d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ar/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">أتسمح بأن يُرسل %1$s التنبيهات؟</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">أتسمح بأن يستعمل %1$s الكمرة؟</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">أتسمح بأن يستعمل %1$s الميكروفون؟</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">أتسمح بأن يستعمل %1$s مكانك؟</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">أستسمح بأن يستعمل %1$s الكمرة والميكروفون؟</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ميكروفون 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">الكمرة الخلفية</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">الكمرة الأمامية</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">اسمح</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">لا تسمح</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">تذكّر هذا القرار لهذا الموقع</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">دائمًا</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">أبدًا</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">هل تسمح أي يحفظ %1$s بيانات في مساحة تخزين دائما؟</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">هل تسمح بأن يُشغّل %1$s محتوى خاضع لإدارة الحقوق الرقمية؟</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">أتسمح بأن يستعمل %1$s كعكاته على %2$s؟</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">يمكنك حجب الوصول إليها إن لم يكن السبب وراء احتياج %s لهذه البيانات واضحًا.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">احجب</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">اطّلع على المزيد</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..1a4f8a717a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ast/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">¿Quies permitir a «%1$s» qu\'unvie avisos?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">¿Quies permitir a «%1$s» qu\'use la cámara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">¿Quies permitir a «%1$s» qu\'use\'l micrófonu?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">¿Quies permitir a «%1$s» qu\'use la to llocalización?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">¿Quies permitir a «%1$s» qu\'use la cámara ya\'l micrófonu?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micrófonu 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Cámara d\'atrás</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Cámara d\'alantre</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Nun permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Recordar la decisión pa esti sitiu</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Siempres</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Enxamás</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">¿Quies permitir a «%1$s» qu\'atroxe datos nel almacenamientu permanente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">¿Quies permitir a «%1$s» que reproduza conteníu con DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">¿Quies permitir a «%1$s» qu\'use les sos cookies en «%2$s»?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Si nun ta claro por qué «%s» precisa estos datos, ye probable que quieras bloquiar l\'accesu.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquiar</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Saber más</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..673643b1e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-az/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s bildiriş göndərə bilsin?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s kameranızı işlədə bilsin?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s mikrofonunuzu işlədə bilsin?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s mövqeyinizi işlədə bilsin?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s kamera və mikrofonunuzu işlədə bilsin?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Arxa kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Ön kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">İcazə ver</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">İcazə vermə</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Seçimi bu sayt üçün yadda saxla</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Hər zaman</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Heç vaxt</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s üçün qalıcı yaddaşda məlumat saxlamağa icazə verilsin?</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..26d646b790
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-azb/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s بیلدیریش گؤندره ‌بیلسین؟</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s کامئرازی ایشه آلسین؟</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s میکروفونوزو ایشه آلسین؟</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s قونوموزو ایشه آلسین؟</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s کامئرا و میکروفونوزو ایشه آلسین؟</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">میکروفون</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">سلفی کامئرا</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">قاباق کامئرا</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ایجازه وئر</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ایجازه وئرمه</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">بو سایت اوچون قراری یاددا ساخلا</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">هر زامان</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">هئچ زامان</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s قالیجی مخزنینده دیتا ساخلیا بیلسین؟</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s سایتی DRM-کونترول موحتوالاری اوینادا بیلسین؟</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s، کوکی‌لرینی %2$s سایتیندا ایشه آلسین؟</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">%s سایتینین بو دیتایا نیه احتیاج اولدوغونو بیلمیرسیز ال‌چاتمانی مسدود ائلیه بیلرسیز.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">مسدود ائله</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">آرتیق بیلین</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..9494effbb7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-be/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Дазволіць %1$s адпраўляць вам абвесткі?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Дазволіць %1$s выкарыстоўваць вашу камеру?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Дазволіць %1$s выкарыстоўваць ваш мікрафон?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Дазволіць %1$s выкарыстоўваць ваша месцазнаходжанне?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Дазволіць %1$s выкарыстоўваць вашу камеру і мікрафон?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Мікрафон 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Задняя камера</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Пярэдняя камера</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Дазволіць</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Не дазваляць</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Памятаць рашэнне для гэтага сайта</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Заўсёды</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Ніколі</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Дазволіць %1$s захоўваць дадзеныя ў пастаянным сховішчы?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Дазволіць %1$s прайграваць змесціва, якое кантралюецца DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Дазволіць %1$s выкарыстоўваць свае кукi на %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Вы можаце заблакаваць доступ, калі няясна, навошта %s патрэбны гэтыя дадзеныя.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Блакаваць</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Падрабязней</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..825e5fefe9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bg/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Разрешение %1$s да изпраща известия?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Разрешение %1$s да използва камера?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Разрешение %1$s да използва микрофон?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Разрешение %1$s да използва местоположение?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Разрешение %1$s да използва камера и микрофон?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Микрофон 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Задна камера</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Предна камера</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Разрешаване</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Забраняване</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Запомняне на решение за страницата</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Винаги</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Никога</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Разрешение %1$s да запазва в постоянно хранилище?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Разрешение %1$s да възпроизвежда контролирано с DRM съдържание?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Разрешение на %1$s да използва бисквитки в %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Бихте могли да забраните ако не ясно защо %s се нуждае то тези данни.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Забраняване</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Научете повече</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..92b56c412e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bn/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s কে বিজ্ঞপ্তি পাঠানোর অনুমতি দিবেন?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s কে আপনার ক্যামেরা ব্যবহার করার অনুমতি দিবেন?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s কে আপনার মাইক্রোফোন ব্যবহার করার অনুমতি দিবেন?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s কে আপনার অবস্থান ব্যবহার করার অনুমতি দিবেন?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s কে আপনার ক্যামেরা এবং মাইক্রোফোন ব্যবহার করার অনুমতি দিবেন?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">মাইক্রোফোন 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">পিছনের দিকের ক্যামেরা</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">সামনের দিকের ক্যামেরা</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">অনুমতি দিন</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">অনুমতি দিবেন না</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">এই সাইটের জন্য সিদ্ধান্ত মনে রাখুন</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">সবসময়</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">কখনো না</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$sকে স্থায়ী স্টোরেজে তথ্য রাখতে দিবে?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$sকে ডিআরএম-নিয়ন্ত্রিত মিডিয়া প্লে করতে দিবে?</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..d74d4f03f7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-br/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Aotren %1$s da gas rebuzadurioù deocʼh?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Aotren %1$s da arverañ ho kamera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Aotren %1$s da arverañ ho klevell?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Aotren %1$s da arverañ ho lecʼhiadur?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Aotren %1$s da arverañ ho kamera hag ho klevell?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Klevell 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kamera a-dreñv</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kamera a-dal</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Aotren</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Na aotren</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Dercʼhel soñj eus an dibab-se evit al lecʼhienn-mañ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Atav</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Morse</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Aotren %1$s da gadaviñ roadennoù er c’hadaviñ diastal?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Aotren %1$s da lenn endalc’had reoliet gant DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Aotren %1$s da implij he zoupinoù war %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Gallout a rafec’h kaout c’hoant nac’hañ ma n’eo ket sklaer perak e fell da %s kaout ar roadennoù-mañ.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Stankañ</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Gouzout hiroc’h</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..474c1fae0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-bs/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Dozvoliti da %1$s šalje obavijesti?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Dozvoliti da %1$s koristi vašu kameru?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Dozvoliti da %1$s koristi vaš mikrofon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Dozvoliti da %1$s koristi vašu lokaciju?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Dozvoliti da %1$s koristi vašu kameru i mikrofon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Zadnja kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Prednja kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Dozvoli</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ne dozvoli</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Zapamti odluku za ovu stranicu</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Uvijek</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nikad</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Dozvoli %1$s da čuva informacije u trajnom skladištu?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Dozvoli %1$s da pušta DRM-kontrolisani sadržaj?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Dozvoli %1$s da koristi svoje kolačiće na %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Možda želite blokirati pristup ako nije jasno zašto %s treba ove podatke.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokiraj</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Saznajte više</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..a01e196433
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ca/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Voleu permetre que %1$s enviï notificacions?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Voleu permetre que %1$s utilitzi la càmera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Voleu permetre que %1$s utilitzi el micròfon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Voleu permetre que %1$s utilitzi la vostra ubicació?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Voleu permetre que %1$s utilitzi la càmera i el micròfon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micròfon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Càmera posterior</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Càmera frontal</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permet</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No ho permetis</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Recorda la decisió per a aquest lloc</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Sempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mai</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Voleu permetre que %1$s desi dades en l’emmagatzematge persistent?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Voleu permetre que %1$s reprodueixi contingut controlat per DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Voleu permetre que %1$s utilitzi galetes en %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Si no teniu clar per què %s necessita aquestes dades, podeu blocar-ne l’accés.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloca</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Més informació</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..412099cbd1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cak/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">¿La niya\' q\'ij chi ri %1$s yerutäq taq rutzijol?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">¿La niya\' q\'ij chi re ri %1$s richin nrokisaj ri elesäy wachib\'äl?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">¿La niya\' q\'ij chi re ri %1$s richin nrokisaj ri q\asäy ch\'ab\'äl?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">¿La niya\' q\'ij chi re ri %1$s richin nrokisaj ri ak\'ojlemal?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">¿La niya\' q\'ij chi re ri %1$s richin nrokisaj ri elsäy wachib\'äl chuqa\' q\'asäy ch\'ab\'äl?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Q\'asäy ch\'ab\'äl 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Rij elesäy wachib\'äl</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Ruwäch elesäy wachib\'äl</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Tiya\' q\'ij</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Man tiya\' q\'ij</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Tinatäx ri xna\'ojïx pa re ruxaq re\'</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Junelïk</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Majub\'ey</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">¿La niya\' q\'ij chi ri %1$s keruyaka\' taq tzij pa jutaqil yakoj?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">¿La niya\' q\'ij chi ri %1$s nutzïj rupam samajin ruma DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">¿La niya\' q\'ij chi ri %1$s? kerokisaj taq kuki pa %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Rik\'in jub\'a\' nawajo\' naq\'ät ri okem we man q\'aläj ta ruma chi ri %s nik\'atzin chi re re taq tzij re\'.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Tiq\'at</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Tetamäx ch\'aqa\' chik</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..031b0dddcd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Tugotan ang %1$s magsend ug notifications?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Tugotan ang %1$s mogamit ug camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Tugotan ang %1$s nga mogamit ug microphone?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Tugotan ang %1$s nga mogamit sa imong location?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Tugotan ang %1$s nga gamiton imong camera ug microphone?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microphone</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Likod nga camera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Atbang nga camera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Tugotan</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ayaw tugoti</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Hinumdomi ang desisyon ni ini nga site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Kanunay</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Dili</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Tugotan ang %1$s magbutang ug data sa permanent storage</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Tugotan ang %1$s mag play ang DRM controlled content?</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..cf7756aa51
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">ڕێگەدەدەیت %1$s ئاگانامە بنێرێت بۆت؟</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">ڕێگەدەدەیت %1$s کامێراکەت بەکاربێنێ؟</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">رێگەدەدەیت %1$s مایکرۆفۆنەکەت بەکاربێنێت؟</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">ڕێگەدەدەیت %1$s شوێنەکەت بەکاربێنێت؟</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">ڕێگەدەدەیت %1$s کامێرا و مایکرۆفۆنەکەت بەکاربێنێ؟</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">مایکرۆفۆن 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">کامێرای دوواوە</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">کامێرای پێشەوە</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ڕێگەبدە</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ڕێگە مەدە</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">بڕیارەکەت بیربێت بۆ ئەم ماڵپەڕە</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">هەمووکات</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">هیچ کات</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">ڕێگەدەدەیت کە %1$s زانیاری پاشەکەوتبکات لە بیرگەی هەمیشەیی؟</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">ڕێگەدەدەیت کە %1$s ناوەڕۆکی پارێزراوی ژمارەیی DRM لێبدات؟</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..2810bb03c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-co/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permette à %1$s di mandà nutificazioni ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permette à %1$s d’impiegà u vostru apparechju-fotò ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permette à %1$s d’impiegà u vostru microfonu ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permette à %1$s d’accede à a vostra lucalizazione ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permette à %1$s d’impiegà u vostru apparechju-fotò è u vostru microfonu ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfonu 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Apparechju-fotò daretu</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Apparechju-fotò davanti</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permette</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ùn permette micca</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Arricurdassi di sta scelta per stu situ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Sempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mai</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Permette à %1$s d’allucà dati in a memoria permanente ?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Permette à %1$s di leghje cuntenutu cuntrollatu da DRM ?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Permette à %1$s d’impiegà i so canistrelli nant’à %2$s ?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Forse preferite bluccà l’accessu s’ellu ùn hè micca chjaru perchè %s abbia bisognu di sti dati.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bluccà</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Sapene di più</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..4222f12df7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cs/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Chcete serveru %1$s povolit zasílat vám oznámení?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Chcete serveru %1$s povolit používat vaši kameru?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Chcete serveru %1$s povolit používat váš mikrofon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Chcete serveru %1$s povolit přístup k vaší poloze?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Chcete serveru %1$s povolit používat vaši kameru a mikrofon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Zadní kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Přední kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Povolit</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Nepovolit</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Pamatovat si pro tento server</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Vždy</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nikdy</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Chcete povolit serveru %1$s ukládat data natrvalo?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Chcete serveru %1$s povolit přehrávat obsah chráněný pomocí DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Povolit serveru %1$s používat své cookies také na serveru %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Pokud není jasné, proč server %s tato data potřebuje, nejspíše můžete jeho požadavek zablokovat.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokovat</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Zjistit více</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..e937f6fc0a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-cy/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Caniatáu i %1$s anfon hysbysiadau?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Caniatáu i%1$s ddefnyddio’ch camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Caniatáu i %1$s ddefnyddio’ch camera?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Caniatáu i %1$s ddefnyddio’ch camera?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Caniatáu i %1$s ddefnyddio eich camera a’ch meicroffon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Meicroffon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Camera’n wynebu nôl</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Camera’n wynebu ymlaen</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Caniatáu</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Peidio â chaniatáu</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Cofio’r penderfyniad am y wefan hon</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Bob tro</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Byth</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Caniatáu i %1$s storio data mewn storfa barhaus?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Caniatáu i %1$s chwarae cynnwys sy’n cael ei reoli gan DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Caniatáu i %1$s ddefnyddio ei gwcis ar %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Efallai y byddwch am rwystro mynediad os nad yw’n glir pam mae angen y data hwn ar %s.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Rhwystro</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Darllen rhagor</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..5c7b31b5a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-da/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Vil du give %1$s lov til at sende meddelelser?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Vil du give %1$s lov til at anvende dit kamera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Vil du give %1$s lov til at anvende din mikrofon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Vil du give %1$s lov til at anvende din position?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Vil du give %1$s lov til at anvende dit kamera og din mikrofon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Bagudrettet kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Fremadrettet kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Tillad</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Tillad ikke</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Husk mit valg for dette websted</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Altid</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Aldrig</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Vil du give %1$s lov til at gemme data i vedvarende lager?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Vil du give %1$s love til at afspille DRM-kontrolleret indhold?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Tillad %1$s at bruge sine cookies på %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Overvej at blokere adgangen, hvis det ikke er tydeligt, hvorfor %s behøver disse data.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloker</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Læs mere</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..cee0c058d0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-de/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s erlauben, Benachrichtigungen zu senden?</string>
+
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s erlauben, Ihre Kamera zu verwenden?</string>
+
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s erlauben, Ihr Mikrofon zu verwenden?</string>
+
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s erlauben, Ihren Standort zu verwenden?</string>
+
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s erlauben, Ihre Kamera und Ihr Mikrofon zu verwenden?</string>
+
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kamera Rückseite</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kamera Vorderseite</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Erlauben</string>
+
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Nicht erlauben</string>
+
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Diese Entscheidung für diese Website merken</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Immer</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nie</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Soll %1$s Daten im dauerhaften Speicher speichern dürfen?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s die Wiedergabe von DRM-kontrollierten Inhalten erlauben?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s erlauben, seine Cookies auf %2$s zu verwenden?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Möglicherweise möchten Sie den Zugriff blockieren, wenn nicht klar ist, warum %1$s diese Daten benötigt.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blockieren</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Weitere Informationen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..c1654b04b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s dowóliś, powěźeńki słaś?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s dowóliś, wašu kameru wužywaś?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s dowóliś, waš mikrofon wužywaś?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s dowóliś, wašo stojnišćo wužywaś?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s dowóliś, wašu kameru a waš mikrofon wužywaś?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Slězna kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Prědna kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Dowóliś</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Njedowóliś</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Rozsud za toś to sedło se spomnjeś</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Pśecej</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nigda</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Cośo %1$s dowóliś, daty w trajnem składowaku składowaś?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s dowóliś, aby wopśimjeśe wótgrał, kótaryž se pśez DRM wóźi?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s dowóliś, jogo cookieje na %2$s wužywaś?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Móžośo pśistup blokěrowaś, jolic njejo jasnje, cogodla %s toś te daty trjeba.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokěrowaś</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Dalšne informacije</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..42d9abede5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-el/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Να επιτρέπεται στο %1$s η αποστολή ειδοποιήσεων;</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Να επιτρέπεται στο %1$s η χρήση της κάμεράς σας;</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Να επιτρέπεται στο %1$s η χρήση του μικροφώνου σας;</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Να επιτρέπεται στο %1$s η χρήση της τοποθεσίας σας;</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Να επιτρέπεται στο %1$s η χρήση της κάμερας και του μικροφώνου σας;</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Μικρόφωνο 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Πίσω κάμερα</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Μπροστινή κάμερα</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Αποδοχή</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Απόρριψη</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Αποθήκευση απόφασης για τον ιστότοπο</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Πάντα</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Ποτέ</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Να επιτρέπεται στο %1$s η αποθήκευση δεδομένων σε μόνιμη αποθήκευση;</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Να επιτρέπεται στο %1$s η αναπαραγωγή περιεχομένου με έλεγχο DRM;</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Να επιτρέπεται στο %1$s να χρησιμοποιεί τα cookie του στο %2$s;</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Ίσως θελήσετε να αποκλείσετε την πρόσβαση εάν δεν είναι σαφές το γιατί το %s χρειάζεται αυτά τα δεδομένα.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Φραγή</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Μάθετε περισσότερα</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..9d9ff2ee68
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Allow %1$s to send notifications?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Allow %1$s to use your camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Allow %1$s to use your microphone?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Allow %1$s to use your location?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Allow %1$s to use your camera and microphone?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microphone 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Rear-facing camera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Front-facing camera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Allow</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Don’t allow</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Remember decision for this site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Always</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Never</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Allow %1$s to store data in persistent storage?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Allow %1$s to play DRM-controlled content?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Allow %1$s to use its cookies on %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">You may want to block access if it\'s not clear why %s needs this data.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Block</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Learn more</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..9d9ff2ee68
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Allow %1$s to send notifications?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Allow %1$s to use your camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Allow %1$s to use your microphone?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Allow %1$s to use your location?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Allow %1$s to use your camera and microphone?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microphone 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Rear-facing camera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Front-facing camera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Allow</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Don’t allow</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Remember decision for this site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Always</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Never</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Allow %1$s to store data in persistent storage?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Allow %1$s to play DRM-controlled content?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Allow %1$s to use its cookies on %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">You may want to block access if it\'s not clear why %s needs this data.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Block</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Learn more</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..0504bef917
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-eo/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Ĉu permesi al %1$s sendi sciigojn?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Ĉu permesi al %1$s uzi vian filmilon?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Ĉu permesi al %1$s uzi vian mikrofonon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Ĉu permesi al %1$s uzi vian pozicion?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Ĉu permesi al %1$s uzi vian filmilon kaj mikrofonon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Dorsa fimilo</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Fronta filmilo</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permesi</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ne permesi</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Memori decidon por tiu ĉi retejo</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Ĉiam</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Neniam</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Ĉu permesi al %1$s konservi datumojn en konstanta konservejo?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Ĉu permesi al %1$s ludi enhavon protektitan de DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Ĉu permesi al %1$s uzi siajn kuketojn en %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Vi povus voli bloki la aliron, se ne estas klare, kial %s bezonas tiujn ĉi datumojn.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloki</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Pli da informo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..7ffaadd7ca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">¿Permitir que %1$s envíe notificaciones?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">¿Permitir que %1$s use la cámara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">¿Permitir que %1$s use el micrófono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">¿Permitir que %1$s use tu ubicación?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">¿Permitir que %1$s use la cámara y el micrófono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micrófono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Cámara trasera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Cámara frontal</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Recordar para este sitio</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Siempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">¿Permitir que %1$s guarde datos en el almacenamiento persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">¿Permitir que %1$s reproduzca contenido controlado por DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">¿Permitir que %1$s use sus cookies en %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Podés querer bloquear el acceso si no está claro por qué %s necesita estos datos.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquear</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Conocer más</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..07ed53b907
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">¿Permitir a %1$s enviar notificaciones?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">¿Permitir a %1$s usar tu cámara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">¿Permitir a %1$s usar tu micrófono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">¿Permitir a %1$s usar tu ubicación?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">¿Permitir a %1$s usar tu cámara y micrófono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micrófono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Cámara trasera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Cámara delantera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Recordar decisión para este sitio</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Siempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">¿Permitir a %1$s almacenar datos en el almacenamiento persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">¿Permitir a %1$s reproducir contenido controlado por DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">¿Permitir que %1$s use sus cookies en %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Puede que quieras bloquear el acceso si no está claro por qué %s necesita estos datos.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquear</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Aprender más</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..b23c8a4bb6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">¿Permitir que %1$s te envíe notificaciones?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">¿Permitir que %1$s use tu cámara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">¿Permitir que %1$s use tu micrófono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">¿Permitir que %1$s acceda a tu ubicación?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">¿Permitir que %1$s use tu cámara y micrófono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micrófono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Cámara trasera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Cámara delantera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Recordar para este sitio</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Siempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">¿Permitir a %1$s almacenar datos en el almacenamiento persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">¿Permitir a %1$s reproducir contenido controlado por DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">¿Quieres permitir que %1$s utilice sus cookies en %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Puede que quieras bloquear el acceso si no está claro por qué %s necesita estos datos.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquear</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Saber más</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..93a023dcbf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">¿Permitir que %1$s envíe notificaciones?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">¿Permitir que %1$s use tu cámara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">¿Permitir que %1$s use tu micrófono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">¿Permitir que %1$s use tu ubicación?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">¿Permitir que %1$s use tu cámara y micrófono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micrófono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Cámara trasera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Cámara delantera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Recordar decisión para este sitio</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Siempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">¿Permitir que %1$s almacene datos en el almacenamiento persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">¿Permitir que %1$s reproduzca contenido controlado por DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">¿Permitir que %1$s use sus cookies en %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Puede que quieras bloquear el acceso si no está claro por que %s necesita estos datos.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquear</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Saber más</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..b23c8a4bb6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-es/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">¿Permitir que %1$s te envíe notificaciones?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">¿Permitir que %1$s use tu cámara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">¿Permitir que %1$s use tu micrófono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">¿Permitir que %1$s acceda a tu ubicación?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">¿Permitir que %1$s use tu cámara y micrófono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micrófono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Cámara trasera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Cámara delantera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Recordar para este sitio</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Siempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">¿Permitir a %1$s almacenar datos en el almacenamiento persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">¿Permitir a %1$s reproducir contenido controlado por DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">¿Quieres permitir que %1$s utilice sus cookies en %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Puede que quieras bloquear el acceso si no está claro por qué %s necesita estos datos.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquear</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Saber más</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..854bb486a3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-et/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Kas lubad saidil %1$s saata teavitusi?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Kas lubad saidil %1$s kasutada oma kaamerat?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Kas lubad saidil %1$s kasutada oma mikrofoni?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Kas lubad saidil %1$s kasutada sinu asukoha teavet?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Kas lubad saidil %1$s kasutada oma kaamerat ja mikrofoni?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">tagumine kaamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">eesmine kaamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Luba</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ära luba</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Peetakse selle saidi jaoks meeles</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Alati</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mitte kunagi</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Kas lubada saidil %1$s salvestada andmeid püsivalt?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Kas lubada saidil %1$s esitada DRMiga kaitstud sisu?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Kas lubad saidil %1$s kasutada oma küpsiseid saidil %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Võid selle ligipääsu blokkida, kui pole selge, miks sait %s neid andmeid vajab.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloki</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Rohkem teavet</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..8ce26c1175
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-eu/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Baimendu %1$s(r)i jakinarazpenak bidaltzea?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Baimendu %1$s(r)i zure kamera erabiltzea?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Baimendu %1$s(r)i zure mikrofonoa erabiltzea?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Baimendu %1$s(r)i zure kokapena erabiltzea?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Baimendu %1$s(r)i zure kamera eta mikrofonoa erabiltzea?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">1 Mikrofonoa</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Atzealdeko kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Aurreko kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Baimendu</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ez baimendu</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Gogoratu erabakia gune honetarako</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Beti</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Inoiz ez</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Baimendu %1$s guneari datuak biltegiratze iraunkorrean gordetzea?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Baimendu %1$s helbideari DRM bidez kontrolatutako edukia erreproduzitzen?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Baimendu %1$s guneari bere cookieak %2$s gunean erabiltzea?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Sarbidea blokeatu nahiko duzu ez badago garbi zergatik behar dituen datu hauek %s guneak.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokeatu</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Argibide gehiago</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..fa3cac3003
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fa/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">به %1$s اجازه می‌دهید آگاهی بفرستد؟</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">به %1$s اجازه می‌دهید از دوربین شما استفاده کند؟</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">آیا %1$s اجازهٔ استفاده از صدابَر شما را دارد؟</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">به %1$s اجازه می‌دهید از مکان شما استفاده کند؟</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">آیا %1$s اجازهٔ استفاده از دوربین و صدابَر شما را دارد؟</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">صدابَر 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">دوربین عقب</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">دوربین جلو</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">اجازه دادن</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">اجازه ندادن</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">تصمیم را برای این پایگاه به خاطر بسپار</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">همیشه</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">هرگز</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">به %1$s اجازه‌ می‌دهید داده‌ها را در ذخیره‌گاه دائمی ذخیره کند؟</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">به %1$s اجازه می‌دهید محتوای واپایش شده با DRM را پخش کند؟</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">به %1$s اجازهٔ استفاده از کلوچک‌های خود در %2$s را می‌دهید؟</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">اگر مشخص نیست چرا %s به این داده‌ها نیاز دارد، ممکن است بخواهید دسترسی‌اش را مسدود کنید.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">مسدود کردن</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">بیشتر بدانید</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..5929ed5628
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ff/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Yamir %1$s yo neldu tintine?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Yamir %1$s yo huutoro kameraa maa?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Yamir %1$s yo huutoro mikkoroo maa?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Yamir %1$s yo huutoro nokku maa?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Yamir %1$s yo huutoro kameraa maa e mikkoroo maa?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikkoroo1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kameraa caggal</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kameraa yeeso</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Yamir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Hoto Yamir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Siftor ɗuum e ndee lowre</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Ddaañaa</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Abadan</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Ɓeydu humpito</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..4e494ce63c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fi/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Sallitko, että %1$s lähettää ilmoituksia?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Sallitko, että %1$s käyttää kameraa?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Sallitko, että %1$s käyttää mikrofonia?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Sallitko, että %1$s käyttää sijaintia?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Sallitko, että %1$s käyttää kameraa ja mikrofonia?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofoni 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Takakamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Etukamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Salli</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Älä salli</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Muista valinta tälle sivustolle</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Aina</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">En koskaan</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Sallitko, että %1$s tallettaa tietoja pysyvään tallennustilaan?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Sallitko, että %1$s toistaa DRM-suojattua sisältöä?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Saako sivusto %1$s käyttää evästeitään sivustolla %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Haluat ehkä estää käytön, jos ei ole selvää, miksi %s tarvitsee nämä tiedot.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Estä</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Lue lisää</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..bbdde61732
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Autoriser %1$s à envoyer des notifications ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Autoriser %1$s à utiliser votre appareil photo ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Autoriser %1$s à utiliser votre microphone ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Autoriser %1$s à accéder à votre localisation ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Autoriser %1$s à utiliser votre appareil photo et votre microphone ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microphone 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Appareil photo arrière</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Appareil photo avant</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Autoriser</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ne pas autoriser</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Se souvenir de ce choix pour ce site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Toujours</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Jamais</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Autoriser %1$s à conserver des données dans le stockage persistant ?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Autoriser %1$s à lire du contenu contrôlé par DRM ?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Autoriser %1$s à utiliser ses cookies sur %2$s ?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Vous voudrez peut-être bloquer l’accès si vous ne savez pas exactement pourquoi %s a besoin de ces données.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquer</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">En savoir plus</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..b813fd922d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fur/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permeti a %1$s di inviâ notifichis?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permeti a %1$s di doprâ la fotocjamare?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permeti a %1$s di doprâ il to microfon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permeti a %1$s di doprâ la tô posizion?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permeti a %1$s di doprâ la fotocjamare e il microfon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Fotocjamare posteriôr</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Fotocjamare frontâl</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permet</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No sta permeti</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Visiti de decision par chest sît</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Simpri</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mai</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Permeti a %1$s di archiviâ dâts te memorie persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Permeti a %1$s di riprodusi contignûts controlâts di DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Permeti a %1$s di doprâ i siei cookies su %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Si consee di blocâ l’acès se nol è clâr il motîf che al puarte %s a vê bisugne di chescj dâts.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloche</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Plui informazions</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..bdf99aba53
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s tastean om meldingen te ferstjoeren?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s tastean om jo kamera te brûken?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s tastean om jo mikrofoan te brûken?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s tastean om jo lokaasje te brûken?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s tastean om jo kamera en mikrofoan te brûken?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofoan 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kamera oan efterkant</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kamera oan foarkant</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Tastean</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Net tastean</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Beslissing ûnthâlde foar dizze website</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Altyd</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nea</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Bewarjen fan gegevens yn permaninte opslach troch %1$s tastean?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s tastean om DRM-kontrolearre ynhâld ôf te spyljen?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s tastean harren cookies te brûken op %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Jo kinne de tagong blokkearje as it net dúdlik is wêrom %s dizze gegevens nedich hat.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokkearje</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Mear ynfo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..c4ae48aba9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Tabhair cead do %1$s fógraí a sheoladh?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Tabhair cead do %1$s an ceamara a úsáid?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Tabhair cead do %1$s an micreafón a úsáid?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Tabhair cead do %1$s do shuíomh a fheiceáil?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Tabhair cead do %1$s an ceamara agus an micreafón a úsáid?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micreafón 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Ceamara cúil</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Ceamara tosaigh</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Ceadaigh</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ná ceadaigh</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Meabhraigh an cinneadh don suíomh seo</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">I gcónaí</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Ná meabhraigh riamh</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..5dd5cc7805
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gd/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">A bheil thu airson cead a thoirt dha %1$s brathan a chur?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">A bheil thu airson cead a thoirt dha %1$s an camara agad a chleachdadh?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">A bheil thu airson cead a thoirt dha %1$s am micreofon agad a chleachdadh?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">A bheil thu airson cothrom air d’ ionad a thoirt dha %1$s?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">A bheil thu airson cead a thoirt dha %1$s an camara ’s am micreofon agad a chleachdadh?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micreofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">An camara cùil</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">An camara beòil</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Ceadaich</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Na ceadaich</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Cuimhnich mo cho-dhùnadh airson na làraich seo</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">An-còmhnaidh</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Chan ann idir</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">An doir thu cead dha %1$s dàta a chumail san stòras bhuan?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">A bheil thu airson cead a thoirt dha %1$s susbaint fo smachd DRM a chluich?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">A bheil thu airson leigeil le %1$s na briosgaidean aice a chleachadh air %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Dh’fhaoidte gum b’ fheàirrde dhut an t-inntrigeadh a bhacadh mur eil thu cinnteach carson a dh’fheumas %s an dàta seo.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bac</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Barrachd fiosrachaidh</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..9daf16fa99
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gl/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permitir que %1$s envíe notificacións?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permitir que %1$s empregue a súa cámara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permitir que %1$s empregue o seu micrófono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permitir que %1$s empregue a súa posición?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permitir que %1$s empregue a súa cámara e o seu micrófono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micrófono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Cámara posterior</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Cámara frontal</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Non permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Lembrar decisión para este sitio</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Sempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Permitir que %1$s almacene datos en almacenamento persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Permitir que %1$s reproduza contido controlado por DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Permitir que %1$s use as súas cookies en %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Será mellor que bloque o acceso se non ten claro para que necesita %s os seus datos.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquear</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Máis información</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..bbe85a8a60
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gn/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Emoneĩpa %1$s omondóvo marandu’i?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Emoneĩpa %1$s oiporúvo ta’ãnganohẽha?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Emoneĩpa %1$s oiporúvo ñe’ẽatãha?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Emoneĩpa %1$s oikévo nerendaitépe?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Emoneĩpa %1$s oiporúvo ta’ãnganohẽha ha ñe’ẽatãha?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Ñe’ẽatãha 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Ta’ãnganohẽha kupegua</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Ta’ãnganohẽha tenondegua</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Moneĩ</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ani emoneĩ</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Eñemomandu’a je’ete ko tendápe g̃uarã</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Tapiaite</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Araka’eve</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">¿Emoneĩ %1$s ombyatývo mba’ekuaarã mbyatyrenda hi’arévape?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">¿Emoneĩ %1$s-pe ombohetávo tetepy DRM ohechapyre?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">¿Emoneĩse %1$s oiporúvo ikookie %2$s-pe?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Ikatu rejokose jeike nahesakãirõ mba’ére %s oikotevẽ ko’ã mba’ekuaarã.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Joko</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Kuaave</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..e527d320ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$sને સૂચનાઓ મોકલવાની મંજૂરી આપીએ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$sને તમારા કેમેરાનો ઉપયોગ કરવાની મંજૂરી આપીએ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$sને તમારા માઇક્રોફોનનો ઉપયોગ કરવાની મંજૂરી આપીએ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$sને તમારું સ્થાન વાપરવાની મંજૂરી આપીએ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$sને તમારા કેમેરા અને માઇક્રોફોનનો ઉપયોગ કરવાની મંજૂરી આપીએ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">માઇક્રોફોન 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">રીઅર-ફેસિંગ કેમેરો</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">ફ્રન્ટ-ફેસિંગ કેમેરો</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">પરવાનગી આપો</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">મંજૂરી આપશો નહીં</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">આ સાઇટ માટે નિર્ણય યાદ રાખો</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">હંમેશા</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ક્યારેય નહિં</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..c8e4900565
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s को सूचनाएँ भेजने की अनुमति दें?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s को अपने कैमरे का उपयोग करने की अनुमति दें?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s को अपने माइक्रोफोन के उपयोग की अनुमति दें?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s को अपने स्थान के उपयोग की जानकारी दें?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s को अपने कैमरे और माइक्रोफोन के उपयोग की अनुमति दें?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">माइक्रोफोन 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">पीछे का कैमरा</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">आगे का कैमरा</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">अनुमति दें</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">अनुमति न दें</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">इस साइट के लिए निर्णय याद रखें</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">हमेशा</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">कभी नहीं</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..e44b8bf943
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hil/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Pahanugtan</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..fd3321a064
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Dozvoliti da %1$s šalje obavijesti?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Dozvoliti da %1$s koristi tvoju kameru?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Dozvoliti da %1$s koristi tvoj mikrofon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Dozvoliti da %1$s koristi tvoju lokaciju?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Dozvoliti da %1$s koristi tvoju kameru i mikrofon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Stražnja kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Prednja kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Dozvoli</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Nemoj dozvoliti</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Zapamti odluku za ovu stranicu</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Uvijek</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nikada</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Dopustiti stranici %1$s spremanje podataka u trajnu pohranu?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Dopustiti stranici %1$s reprodukciju sadržaja kontroliranog DRM-om?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Dopusti %1$s da koristi svoje kolačiće na %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Vjerojatno ćete htjeti blokirati pristup ukoliko nije jasno zašto %s treba ove podatke.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokiraj</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Saznajte više</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..240b3f439c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s dowolić, zdźělenki słać?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s dowolić, wašu kameru wužiwać?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s dowolić, waš mikrofon wužiwać?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s dowolić, waše stejnišćo wužiwać?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s dowolić, wašu kameru a waš mikrofon wužiwać?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Zadnja kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Prědnja kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Dowolić</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Njedowolić</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Rozsud za tute sydło sej spomjatkować</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Přeco</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Ženje</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Smě %1$s daty w trajnym składowaku składować?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s dowolić, zo byšće wobsah wothrał, kotryž so přez DRM wodźi?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s dowolić, jeho placki na %2$s wužiwać?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Móžeće přistup blokować, jeli jasnje njeje, čehodla %s tute daty trjeba.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokować</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Dalše informacije</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..11021d9a92
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hu/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Engedélyezi, hogy a(z) %1$s értesítéseket küldjön?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Engedélyezi, hogy a(z) %1$s használja a kameráját?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Engedélyezi, hogy a(z) %1$s használja a mikrofonját?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Engedélyezi, hogy a(z) %1$s elérje a tartózkodási helyét?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Engedélyezi, hogy a(z) %1$s használja a kameráját és mikrofonját?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">1. mikrofon</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Hátlapi kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Előlapi kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Engedélyezés</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Tiltás</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Döntés megjegyzése ehhez az oldalhoz</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Mindig</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Soha</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Engedélyezi, hogy a(z) %1$s adatokat tároljon az állandó tárolóban?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Engedélyezi, hogy a(z) %1$s DRM által vezérelt tartalmat játsszon le?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Engedélyezi, hogy a(z) %1$s használja a sütijeit a(z) %2$s webhelyen?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Érdemes lehet letiltani a hozzáférést, ha nem világos, hogy a(z) %s webhelynek miért van szüksége ezekre az adatokra.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Tiltás</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">További tudnivalók</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..fc0ec838d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Թույլատրե՞լ %1$s-ին ծանուցումներ ուղարկել:</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Թույլատրե՞լ %1$s-ին օգտագործել Ձեր տեսախցիկը:</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Թույլատրե՞լ %1$s-ին օգտագործել ձեր խոսափողը:</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Թույլատրե՞լ %1$s-ին օգտագործել ձեր տեղադրությունը:</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Թույլատրե՞լ %1$s-ին օգտագործել Ձեր տեսախցիկը և խոսափողը:</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Խոսափող 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Ետին տեսախցիկ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Դիմային տեսախցիկ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Թույլատրել</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Չթույլատրել</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Հիշել այս կայքի որոշումը</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Միշտ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Երբեք</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Թույլատրե՞լ %1$s-ին պահել տվյալներ մշտական պահեստում:</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Թույլատրե՞լ %1$s-ին նվագարկել DRM ղեկավարվող բովանդակություն:</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Թույլատրե՞լ %1$s-ին օգտագործել իր թխուկները %2$s-ում:</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Դուք կարող եք արգելափակել մատչումը, եթե պարզ չէ, թե ինչու են %s-ին անհրաժեշտ այս տվյալները:</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Արգելափակել</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Իմանալ ավելին</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..eff151bc66
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ia/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permitte tu que %1$s invia notificationes?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permitte tu que %1$s usa tu camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permitte tu que %1$s usa tu microphono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permitte tu que %1$s usa tu ubication?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permitte tu que %1$s usa tu camera e tu microphono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microphono1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Camera dorsal</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Camera frontal</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitter</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Non permitter</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Rememorar le decision pro iste sito</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Sempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunquam</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Permitte tu que %1$s stocka datos in le memoria persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Permitte tu a %1$s de reproducer le contento protegite per DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Permitter que %1$s usa su cookies sur %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Es un bon idea blocar le accesso si non es clar proque %s require iste datos.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blocar</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Pro saper plus</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..f365049aa7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-in/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Izinkan %1$s untuk mengirim pemberitahuan?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Izinkan %1$s untuk menggunakan kamera Anda?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Izinkan %1$s untuk menggunakan mikrofon Anda?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Izinkan %1$s menggunakan lokasi Anda?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Izinkan %1$s menggunakan kamera dan mikrofon Anda?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kamera hadap belakang</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kamera hadap depan</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Izinkan</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Jangan izinkan</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Ingat pilihan pada situs ini</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Selalu</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Tidak Pernah</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Izinkan %1$s untuk menyimpan data di penyimpanan persisten?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Izinkan %1$s untuk memutar konten yang dikendalikan DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Izinkan %1$s untuk menggunakan kuki-nya di %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Anda mungkin ingin memblokir akses jika alasan %s membutuhkan data ini tidak jelas.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokir</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Pelajari lebih lanjut</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..97849f46b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-is/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Leyfa %1$s að senda tilkynningar?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Leyfa %1$s að nota myndavélina þína?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Leyfa %1$s að nota hljóðnemann þinn?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Leyfa %1$s að nota staðsetninguna þína?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Leyfa %1$s að nota myndvélina þína og hljóðnema?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Hljóðnemi 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Bak myndavélin</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Fram myndavélin</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Leyfa</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ekki leyfa</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Mundu ákvörðun fyrir þetta vefsvæði</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Alltaf</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Aldrei</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Leyfa %1$s að geyma gögn í varanlegri geymslu?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Leyfa %1$s að spila DRM-stýrt efni?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Leyfa %1$s að nota vefkökur sínar á %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Þú gætir viljað loka fyrir aðgang ef það er ekki ljóst hvers vegna %s þarfnast þessara gagna.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Loka á</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Frekari upplýsingar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..96d1bbe036
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-it/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Consentire a %1$s di inviare notifiche?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Consentire a %1$s di utilizzare la fotocamera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Consentire a %1$s di utilizzare il microfono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Consentire a %1$s di utilizzare la posizione?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Consentire a %1$s di utilizzare la fotocamera e il microfono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Fotocamera posteriore</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Fotocamera frontale</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Consenti</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Non permettere</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Ricorda la scelta per questo sito</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Sempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mai</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Consentire a %1$s di salvare i dati nella memoria permanente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Consentire a %1$s di riprodurre contenuti protetti da DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Consentire a %1$s di utilizzare i propri cookie su %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">È consigliabile bloccare l’accesso se non è chiaro per quale motivo %s abbia bisogno di questi dati.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blocca</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Ulteriori informazioni</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..250fcbd7a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-iw/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">האם לאפשר ל־%1$s לשלוח התרעות?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">האם לאפשר ל־%1$s להשתמש במצלמה שלך?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">האם לאפשר ל־%1$s להשתמש במיקרופון שלך?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">האם לאפשר ל־%1$s להשתמש במיקום שלך?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">האם לאפשר ל־%1$s להשתמש במצלמה ובמיקרופון שלך?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">מיקרופון 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">מצלמה אחורית</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">מצלמה קדמית</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">לאפשר</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">לא לאפשר</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">שמירת הבחירה עבור אתר זה</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">תמיד</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">לעולם לא</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">האם לאפשר ל־%1$s לשמור מידע באחסון הקבוע?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">האם לאפשר ל־%1$s לנגן תוכן מוגן DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">לאפשר ל־%1$s להשתמש בעוגיות שלו ב־%2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">אם לא ברור לך מדוע %s זקוק לנתונים האלו, ייתכן שכדאי לחסום את הגישה.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">חסימה</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">מידע נוסף</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..c485853162
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ja/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s に通知の送信を許可しますか?</string>
+
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s にカメラの使用を許可しますか?</string>
+
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s にマイクの使用を許可しますか?</string>
+
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s に位置情報の使用を許可しますか?</string>
+
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s にカメラとマイクの使用を許可しますか?</string>
+
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">マイク 1</string>
+
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">背面カメラ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">前面カメラ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">許可する</string>
+
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">許可しない</string>
+
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">今後も同様に処理する</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">常に許可</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">常に不許可</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s に永続ストレージへのデータの格納を許可しますか?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s に DRM 制御されたコンテンツの再生を許可しますか?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%2$s で Cookie の利用を %1$s に許可しますか?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">%s がこのデータを利用する理由が不明な場合は、アクセスのブロックをおすすめします。</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">ブロック</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">詳細情報</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..d198f64a48
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ka/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">ნებას რთავთ %1$s-ს გაჩვენოთ შეტყობინებები?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">ნებას რთავთ %1$s-ს, გამოიყენოს კამერა?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">ნებას რთავთ %1$s-ს, გამოიყენოს მიკროფონი?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">ნებას რთავთ %1$s-ს, გამოიყენოს თქვენი მდებარეობა?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">ნებას რთავთ %1$s-ს, გამოიყენოს კამერა და მიკროფონი?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">მიკროფონი 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">უკანა ხედვის კამერა</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">წინა ხედვის კამერა</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">დაშვება</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">აკრძალვა</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">დამახსოვრება ამ საიტისთვის</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ყოველთვის</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">არასდროს</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">ნებას რთავთ %1$s-ს, შეინახოს მონაცემები მუდმივ მეხსიერებაზე?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">გსურთ, %1$s-მ გაუშვას DRM-დაქვემდებარებული შიგთავსი?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">ნებას რთავთ %1$s-ს, გამოიყენოს ფუნთუშები %2$s-ზე?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">უმჯობესია აკრძალოთ წვდომა, თუ ნათელი არაა, რისთვის საჭიროებს %s ამ მონაცემებს.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">აკრძალვა</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ვრცლად</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..bf3121685a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s ushın xabarlamalardı jiberiwdi ruqsat etiw kerek pe?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s ushın kamerańızdan qollanıwdı ruqsat etiw kerek pe?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s ushın mikrofonıńızdan paydalanıwdı ruqsat etiw kerek pe?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s ushın jaylasqan ornıńızdı paydalanıwǵa ruqsat etiw kerek pe?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s ushın kamera hám mikrofonıńızdı paydalanıwǵa ruqsat etiw kerek pe?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Artqı kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Aldınǵı kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Ruqsat beriw</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ruqsat etpew</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Bul sayt ushın qarardı este saqlaw</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Hámiyshe</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Heshqashan</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s ushın maǵlıwmatlardı turaqlı saqlaw ornında saqlawǵa ruqsat etiw kerek pe?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s ushın avtor huqıqları menen qorǵalǵan kontentti qoyıwdı ruqsat etiw kerek pe?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s ǵa %2$s daǵi cookielerdi paydalanıwǵa ruqsat etilsin be?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Eger siz %s saytına bul maǵlıwmatlar nege kerek ekenligin túsinbeseńiz, kiriwdi bloklawıńız múmkin.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloklaw</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Tolıǵıraq úyreniw</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..0e0d63ad13
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kab/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Sireg %1$s ad yazen tilɣa?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Sireg %1$s ad isegdec takamiṛat?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Sireg %1$s ad isegdec aṣawaḍ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Sireg %1$s ad iseqdec adig-ik?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Sireg %1$s ad iseqdec takamiṛat-ik akked usawaḍ-ik?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Asawaḍ 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Ibenk n tkamiṛat n deffir</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Takamirat n sdat</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Sireg</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ur sirig ara</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Cfu ɣef ufran-iw i usmel-a</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Yal tikkelt</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Werǧin</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Ad teǧǧeḍ %1$s ad issekles isefka deg uselkim-ik•im?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Sireg %1$s ad d-iɣer agbur yettwasneqden sɣur DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Sireg %1$s ad isseqdec inagan-is n tuqqna ɣef %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Ahat tebɣiḍ ad tesweḥleḍ anekcum ma yella ur d-iban ara ayɣer %s yesra isefka-a.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Sewḥel</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Issin ugar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..620a106672
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kk/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s үшін хабарламаларды жіберуді рұқсат ету керек пе?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s үшін камераңызды қолдану рұқсат ету керек пе?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s үшін микрофоныңызды қолдану рұқсат ету керек пе?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s үшін орналасуыңызды қолдану рұқсат ету керек пе?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s үшін камера және микрофоныңызды қолдануды рұқсат ету керек пе?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Микрофон 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Артқы камера</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Алдыңғы камера</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Рұқсат ету</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Рұқсат етпеу</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Бұл сайт үшін таңдауымды есте сақтау</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Әрқашан</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Ешқашан</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s үшін деректерді тұрақты қоймада сақтауды рұқсат ету керек пе?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s үшін DRM-мен басқарылатын құраманы ойнатуды рұқсат ету керек пе?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s үшін %2$s жеріндегі cookie файлдарын қолдануға рұқсат беру керек пе?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">%s бұл деректерге қатынау мақсаты түсініксіз болса, қатынау құқығын бұғаттауыңызға болады.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Блоктау</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Көбірек білу</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..ab701f2540
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s, bila danezanan bişîne?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s, bila kameraya te bi kar bîne?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s, bila mîkrofona te bi kar bîne?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s, bila cîgeha te bi kar bîne?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s, bila kamera û mîkrofona te bi kar bîne?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mîkrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kameraya paşiyê</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kameraya pêşiyê</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Destûrê bide</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Destûrê nede</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Biryara min ji bo vê malperê bi bîr bîne</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Timî</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Qet</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s, bila daneyan li bîrgeha mayinde hilîne?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s, bila naverokên bi DRM-kontrolkirî bilîzîne?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Destûrê bidî %1$s`ê ku çerezên xwe di %2$s`ê de bi kar bîne?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Heke ne zelal be ka çima pêdiviya %s`ê bi van agahiyan heye, tu dikarî gihîştinê asteng bikî.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Asteng bike</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Agahiyên zêdetir</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..45f6aeef23
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-kn/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">ಅಧಿಸೂಚನೆಗಳನ್ನು ಕಳುಹಿಸಲು %1$s ಅನ್ನು ಅನುಮತಿಸುವುದೇ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">ನಿಮ್ಮ ಕ್ಯಾಮೆರಾ ಬಳಸಲು %1$s ಅನ್ನು ಅನುಮತಿಸುವುದೇ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">ನಿಮ್ಮ ಮೈಕ್ರೊಫೋನ್ ಬಳಸಲು %1$s ಅನ್ನು ಅನುಮತಿಸುವುದೇ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">ನಿಮ್ಮ ಸ್ಥಳವನ್ನು ಬಳಸಲು %1$s ಅನ್ನು ಅನುಮತಿಸುವುದೇ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">ನಿಮ್ಮ ಕ್ಯಾಮೆರಾ ಮತ್ತು ಮೈಕ್ರೊಫೋನ್ ಬಳಸಲು %1$s ಅನ್ನು ಅನುಮತಿಸುವುದೇ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ಮೈಕ್ರೊಫೋನ್ 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">ಹಿಂಭಾಗದ ಕ್ಯಾಮೆರಾ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">ಮುಮ್ಮುಖ ಕ್ಯಾಮೆರ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ಅನುಮತಿಸು</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ಅನುಮತಿಸ ಬೇಡ‍</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">ಈ ಸೈಟ್‌ನ ನಿರ್ಧಾರವನ್ನು ನೆನಪಿಡಿ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ಯಾವಾಗಲೂ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ಎಂದಿಗೂ ಬೇಡ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..c2a607dd77
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ko/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s가 알림을 보내도록 허용하시겠습니까?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s가 카메라를 사용하도록 허용하시겠습니까?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s가 마이크를 사용하도록 허용하시겠습니까?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s가 사용자 위치를 사용하도록 허용하시겠습니까?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s가 카메라와 마이크를 사용하도록 허용하시겠습니까?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">마이크 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">후면 카메라</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">전면 카메라</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">허용</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">허용 안 함</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">이 사이트에 대한 결정을 기억하기</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">항상</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">안 함</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s 페이지가 데이터를 영구 저장소에 저장하도록 허용하시겠습니까?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s에서 DRM 제어 콘텐츠를 재생하도록 허용하시겠습니까?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s 사이트가 %2$s 사이트에서 쿠키를 사용하도록 허용하시겠습니까?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">%s에 이 데이터가 필요한 이유가 명확하지 않은 경우 액세스를 차단할 수 있습니다.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">차단</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">더 알아보기</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..b166befcd9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lij/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permette a %1$s de mandâ notifiche?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permette a %1$s de deuviâ a fòtocamera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permette a %1$s de deuviâ o micròfono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permette a %1$s de deuviâ a teu poxiçion?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permette a %1$s de deuviâ a fòtocamera e micròfono ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micròfono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Fòtocamera derê</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Fòtocamera davanti</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permetti</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No permette</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Aregòrda sta decixon pe sto scito</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">De longo</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mai</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..a561eec25f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lo/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">ອະນຸຍາດໃຫ້ %1$s ສົ່ງການແຈ້ງເຕືອນບໍ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">ອະນຸຍາດໃຫ້ %1$s ໃຊ້ກ້ອງຖ່າຍຮູບຂອງທ່ານບໍ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">ອະນຸຍາດໃຫ້ %1$s ໃຊ້ໄມໂຄຣໂຟນຂອງທ່ານບໍ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">ອະນຸຍາດໃຫ້ %1$s ໃຊ້ທີ່ຢູ່ຂອງທ່ານບໍ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">ອະນຸຍາດໃຫ້ %1$s ໃຊ້ກ້ອງຖ່າຍຮູບ ແລະ ໄມໂຄຣໂຟນຂອງທ່ານບໍ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ໄມໂຄຣໂຟນ 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">ກ້ອງຫລັງ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">ກ້ອງຫນ້າ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ອະນຸຍາດ</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ບໍ່ອະນຸຍາດ</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">ຈືຂໍ້ມູນການຕັດສິນໃຈສໍາລັບເວັບໄຊທນີ້</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ອະນຸຍາດຕະຫຼອດ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ບໍ່ອະນຸຍາດເລີຍ</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">ອະນຸຍາດໃຫ້ %1$s ເກັບຂໍ້ມູນໄວ້ໃນສະຕໍເລຢ່າງຖາວອນ?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">ອະນຸຍາດໃຫ້ %1$s ເປີດເນື້ອຫາ DRM-controlled ບໍ?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">ອະນຸຍາດໃຫ້ %1$s ໃຊ້ຄຸກກີ້ຂອງມັນຢູ່ໃນ %2$s ຫລືບໍ?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">ທ່ານສາມາດບລັອກການເຂົ້າເຖິງໄດ້ຫາກວ່າທ່ານບໍ່ແນ່ໃຈໃນເຫດຜົນທີ່ %s ຕ້ອງການຂໍ້ມູນນີ້.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">ບລັອກ</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ຮຽນຮູ້ເພີ່ມເຕີມ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..ab5e195305
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-lt/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Leisti %1$s siųsti pranešimus?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Leisti %1$s naudoti jūsų kamerą?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Leisti %1$s naudoti jūsų mikrofoną?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Leisti %1$s nustatyti jūsų buvimo vietą?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Leisti %1$s naudoti jūsų kamerą ir mikrofoną?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofonas 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Galinė kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Priekinė kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Leisti</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Neleisti</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Įsiminti pasirinkimą šiai svetainei</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Visada</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Niekada</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Leisti %1$s laikyti duomenis išliekančioje atmintyje?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Liesti %1$s groti DRM apsaugotą turinį?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Leisti %1$s naudoti savo slapukus esant %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Galite neleisti, jei nėra aišku, kodėl %s reikia šių duomenų.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Neleisti</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Sužinoti daugiau</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..511fda396d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ml/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">അറിയിപ്പുകൾ അയയ്ക്കാൻ %1$s നെ അനുവദിക്കണോ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">താങ്കളുTe ക്യാമറ ഉപയോഗിക്കാൻ %1$s നെ അനുവദിക്കണോ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">താങ്കളുടെ മൈക്രോഫോൺ ഉപയോഗിക്കാൻ %1$s നെ അനുവദിക്കണോ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">താങ്കളുടെ സ്ഥാനം ഉപയോഗിക്കാൻ %1$s നെ അനുവദിക്കണോ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">താങ്കളുTe ക്യാമറയും മൈക്രോഫോണും ഉപയോഗിക്കാൻ %1$s നെ അനുവദിക്കണോ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">മൈക്രോഫോൺ 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">പിൻവശത്തെ ക്യാമറ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">മുൻവശത്തെ ക്യാമറ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">അനുവദിക്കുക</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">അനുവദിക്കരുത്</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">ഈ സൈറ്റിനായുള്ള തീരുമാനം ഓർമ്മിക്കുക</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">എപ്പോഴും</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ഒരിക്കലും അരുത്</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..d61d2bff13
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-mr/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s ला सूचना पाठविण्यास परवानगी द्यायची?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s ला आपला कॅमेरा वापरण्याची परवानगी द्यायची?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s ला आपला मायक्रोफोन वापरण्याची परवानगी द्यायची?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s ला आपले स्थान वापरण्याची परवानगी द्यायची?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s ला आपला कॅमेरा आणि मायक्रोफोन वापरण्याची परवानगी द्यायची?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">मायक्रोफोन 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">मागचा कॅमेरा</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">समोरचा कॅमेरा</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">परवानगी द्या</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">परवानगी देऊ नका</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">या साइटसाठी निर्णय लक्षात ठेवा</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">नेहमी</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">कधीच नाही</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..0e1f032e68
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-my/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s အား အသိပေးချက်များကို ပေးပို့ရန် ခွင့်ပြုပါ။</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">သင့်ကင်မရာ အသုံးပြုရန် %1$s ကို ခွင့်ပေးပါမည်လား။</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">သင့်မိုက်ကရိုဖုန်း အသုံးပြုရန် %1$s ကို ခွင့်ပေးပါမည်လား။</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">သင့်တည်နေရာကို %1$s သုံးရန်ခွင့်ပြုမလား။</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">သင့်ကင်မရာနှင့် မိုက်ကရိုဖုန်း အသုံးပြုရန် %1$s ကို ခွင့်ပေးပါမည်လား။</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">မိုက်ကရိုဖုန်း 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">နောက်ဘက်ကင်မရာ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">မျက်နှာဘက် ကင်မရာ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ခွင့်ပြုပါ</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ခွင့်မပြုပါနှင့်</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">ဤဆိုဘ်အတွက် ဆုံးဖြတ်ချက်ကို မှတ်ထားပါ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">အမြဲတမ်း</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ဘယ်တော့မှ</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">အမြဲမပြတ်သိုလှောင်မှုတွင် %1$s ကို အချက်အလက်သိမ်းဆည်းခွင့်ပြုမည်လား</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">DRM ထိန်းချုပ်ထားသော အကြောင်းအရာများကို %1$s အား ဆောင်ရွက် ခွင့်ပြုမည်လား။</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..fb36bb863e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Tillat %1$s å sende varsler?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Tillat %1$s å bruke kameraet?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Tillat %1$s å bruke mikrofonen?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Tillat %1$s å bruke posisjonen din?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Tillat %1$s å bruke kameraet og mikrofonen?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Bakovervendt kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Fremovervendt kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Tillat</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ikke tillat</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Husk avgjørelsen for dette nettstedet</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Alltid</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Aldri</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Tillate %1$s å lagre data i vedvarende lagring?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Tillate %1$s å spille av DRM-kontrollert innhold?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Vil du tillate %1$s å bruke sine infokapsler på %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Det kan være lurt å blokkere tilgang hvis det ikke er klart hvorfor %s trenger disse dataene.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokker</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Les mer</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..f7ea5c009f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s लाई सूचनाहरू पठाउन अनुमति दिन चाहानुहुन्छ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s लाई आफ्नो क्यामरा प्रयोग गर्न दिन चाहानुहुन्छ ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s लाई आफ्नो माइक्रोफोन प्रयोग गर्न दिन चाहानुहुन्छ ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s लाई आफ्नो स्थान प्रयोग गर्न दिन चाहानुहुन्छ ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s लाई आफ्नो क्यामरा र माइक्रोफोन प्रयोग गर्न दिन चाहानुहुन्छ ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">माइक्रोफोन 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">पछाडि फर्किएको क्यामरा</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">अगाडी फर्किएको क्यामरा</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">अनुमति दिनुहोस्</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">अनुमति नदिनुहोस्</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">यस साइटको लागि निर्णय सम्झनुहोस्</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">सधैँ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">कहिल्यै पनि होइन</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s लाई लगातार भण्डारणमा डेटा भण्डारण गर्न अनुमति दिन चाहानुहुन्छ?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">DRM-नियन्त्रित सामग्री बजाउन %1$s लाई अनुमति दिन चाहानुहुन्छ?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s लाई %2$s मा यसको कुकीहरू प्रयोग गर्न दिने हो?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">%s लाई यो डाटा किन चाहिन्छ भन्ने कुरा स्पष्ट नभएको खण्डमा तपाईले पहुँचलाई रोक लगाउन सक्नुहुन्छ।</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">अझ जान्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..5e9095c0cc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nl/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s toestaan om meldingen te verzenden?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s toestaan om uw camera te gebruiken?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s toestaan om uw microfoon te gebruiken?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s toestaan om uw locatie te gebruiken?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s toestaan om uw camera en microfoon te gebruiken?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfoon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Camera aan achterzijde</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Camera aan voorzijde</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Toestaan</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Niet toestaan</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Beslissing onthouden voor deze website</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Altijd</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nooit</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Bewaren van gegevens in permanente opslag door %1$s toestaan?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s toestaan om DRM-gecontroleerde inhoud af te spelen?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s toestaan zijn cookies te gebruiken op %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">U kunt de toegang blokkeren als het niet duidelijk is waarom %s deze gegevens nodig heeft.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokkeren</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Meer info</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..03df810bf0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Tillate %1$s å sende varsel?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Tillate %1$s å bruke kameraet?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Tillate %1$s å bruke mikrofonen?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Tillate %1$s å bruke plasseringa di?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Tillate %1$s å bruke kamera og mikrofon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Bakovervendt kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Framovervendt kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Tillat</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ikkje tillat</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Hugs avgjerda for denne sida</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Alltid</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Aldri</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Tillate %1$s å lagre data i vedvarande lagring?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Tillate %1$s å spele DRM-kontrollert innhald?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Tilate %1$s å bruke infokapslane sine på %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Det kan vere lurt å blokkere tilgang viss det ikkje er klart kvifor %s treng desse dataa.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blokker</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Les meir</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..58d57d4cd7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-oc/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Autorizar %1$s a enviar de notificacions ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Autorizar %1$s a utilizar vòstra camèra ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Autorizar %1$s a utilizar vòstre microfòn ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Autorizar %1$s a utilizar vòstre emplaçament ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Autorizar %1$s a utilizar vòstra camèra e vòstre microfòn ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfòn 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Camèra rèire</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Camèra frontala</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Autorizar</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Autorizar pas</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Se remembrar d’aquesta causida per aqueste site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Totjorn</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Pas jamai</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Voletz autorizar %1$s a servar las donadas dins l’estocatge persistent ?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Permetre a %1$s de legir lo contengut contrarotlat per DRM ?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Permetre a %1$s d’utilizar sos cookies sus %2$s ?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Volriatz benlèu blocar l’accès se sabètz pas exactament perque %s a mestièr de vòstras donadas.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blocar</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Ne saber mai</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..3805a262ac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-or/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ମାଇକ୍ରୋଫୋନ 1</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ସର୍ବଦା</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">କଦାପି ନୁହେଁ</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ଅଧିକ ଜାଣନ୍ତୁ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..e53aa49003
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s ਨੂੰ ਸੂਚਨਾਵਾਂ ਦੇਣ ਦੀ ਆਗਿਆ ਦੇਣੀ ਹੈ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s ਨੂੰ ਆਪਣਾ ਕੈਮਰਾ ਵਰਤਣ ਦੀ ਆਗਿਆ ਦੇਣੀ ਹੈ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s ਨੂੰ ਆਪਣਾ ਮਾਈਕਰੋਫ਼ੋਨ ਵਰਤਣ ਦੀ ਆਗਿਆ ਦੇਣੀ ਹੈ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s ਨੂੰ ਆਪਣਾ ਟਿਕਾਣਾ ਵਰਤਣ ਦੀ ਆਗਿਆ ਦੇਣੀ ਹੈ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s ਨੂੰ ਆਪਣਾ ਕੈਮਰਾ ਅਤੇ ਮਾਈਕਰੋਫ਼ੋਨ ਵਰਤਣ ਦੀ ਆਗਿਆ ਦੇਣੀ ਹੈ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ਮਾਈਕਰੋਫ਼ੋਨ 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">ਪਿਛਲੇ ਪਾਸੇ ਵਾਲਾ ਕੈਮਰਾ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">ਅੱਗੇ ਪਾਸੇ ਵਾਲਾ ਕੈਮਰਾ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ਆਗਿਆ ਦਿਓ</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ਆਗਿਆ ਨਾ ਦਿਓ</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">ਇਸ ਸਾਈਟ ਲਈ ਫ਼ੈਸਲਾ ਯਾਦ ਰੱਖੋ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ਹਮੇਸ਼ਾਂ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ਕਦੇ ਨਹੀਂ</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">ਕੀ %1$s ਨੂੰ ਪੱਕੀ ਸਟੋਰੇਜ਼ ਵਿੱਚ ਡਾਟਾ ਸਟੋਰ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਦੇਣੀ ਹੈ?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">ਕੀ %1$s ਨੂੰ DRM-ਕੰਟਰੋਲ ਕੀਤੀ ਸਮੱਗਰੀ ਚਲਾਉਣ ਦੀ ਮਨਜ਼ੂਰੀ ਦੇਣੀ ਹੈ?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s ਨੂੰ %2$s ਉੱਤੇ ਆਪਣੇ ਕੂਕੀਜ਼ ਵਰਤਣ ਦੀ ਇਜਾਜ਼ਤ ਦੇਣੀ ਹੈ?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">ਜੇ ਤੁਹਾਨੂੰ ਭਰੋਸਾ ਨਹੀਂ ਹੈ ਕਿ %s ਨੂੰ ਇਹ ਡਾਟਾ ਕਿਓ ਚਾਹੀਦਾ ਹੈ ਤਾਂ ਤੁਸੀਂ ਇਹ ਪਹੁੰਚ ਤੇ ਰੋਕ ਲਾ ਸਕਦੇ ਹੋ।</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">ਪਾਬੰਦੀ</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ਹੋਰ ਸਿੱਖੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..3d4a2e517c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s نوں نوٹ دیوݨ دی اجازت دیو؟</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s نوں کیمرے ورتݨ دی اجازت دیو؟</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s نوں مائیکروفون ورتݨ دی اجازت دیو؟</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s نوں مقام ورتݨ دی اجازت دیو؟</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s نوں کیمرے تے مائیکروفون ورتݨ دی اجازت دیو؟</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">مائیکروفون 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">پچھلے پاسے والا کیمرا</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">اگے پاسے والا کیمرا</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">اجازت دیو</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">اجازت نہ دیو</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">ایس سائٹ لئی فیصلہ یاد رکھو</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ہمیشہ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">کدے نہیں</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s نوں فون وچ ڈیٹے رکھݨ دی اجازت دیو؟</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s نوں کاپی منع دیاں فائلاں ورتݨ دی اجازت دیو؟</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s نوں %2$s تے کوکی ورتݨ دی اجازت دیو؟</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">جے تہانوں بھروسہ نہیں اے کہ %s نوں ایہہ ڈیٹا کیوں چاہیدا اے تاں تسیں ایہہ پہنچ تے روک لا سکدے او۔</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">اجازت نہ دیو</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ہور جاݨو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..6c86763df5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pl/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Czy odbierać powiadomienia od witryny „%1$s”?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Czy udostępnić obraz z aparatu witrynie „%1$s”?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Czy udostępnić dźwięk z mikrofonu witrynie „%1$s”?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Czy udostępnić witrynie „%1$s” informacje o położeniu?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Czy udostępnić obraz z aparatu i dźwięk z mikrofonu witrynie „%1$s”?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">1. mikrofon</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Tylny aparat</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Przedni aparat</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Zezwól</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Nie zezwalaj</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Nie pytaj ponownie na tej witrynie</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Zawsze</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nigdy</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Czy zezwolić witrynie „%1$s” na przechowywanie danych na urządzeniu?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Czy zezwolić witrynie „%1$s” na odtwarzanie treści chronionych przez DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Czy zezwolić witrynie „%1$s” na używanie swoich ciasteczek na witrynie „%2$s”?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Możesz zablokować dostęp, jeśli nie jest jasne, dlaczego witryna „%s” potrzebuje tych danych.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Zablokuj</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Więcej informacji</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..39ffd54e09
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permitir que %1$s envie notificações?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permitir que %1$s use sua câmera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permitir que %1$s use seu microfone?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permitir que %1$s use sua localização?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permitir que %1$s use sua câmera e microfone?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfone 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Câmera traseira</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Câmera frontal</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Não permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Memorizar decisão para este site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Sempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Permitir ao %1$s armazenar dados em armazenamento persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Permitir que %1$s reproduza conteúdo controlado por direitos autorais?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Permitir que %1$s use seus cookies em %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Você pode escolher bloquear o acesso, se não estiver claro o motivo de %s precisar desses dados.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquear</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Saiba mais</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..8e8be27325
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permitir que %1$s envie notificações?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permitir que %1$s utilize a sua câmara?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permitir que %1$s utilize o seu microfone?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permitir que %1$s utilize a sua localização?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permitir que %1$s utilize a sua câmara e microfone?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfone 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Câmara traseira</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Câmara frontal</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permitir</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Não permitir</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Memorizar decisão para este site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Sempre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nunca</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Permitir que %1$s armazene dados no armazenamento persistente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Permitir que %1$s reproduza conteúdo controlado por DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Permitir que %1$s utilize os seus cookies em %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Pode bloquear o acesso se não for claro porque %s necessita destes dados.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloquear</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Saber mais</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..4034b98ddb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-rm/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permetter a %1$s da trametter communicaziuns?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permetter a %1$s da duvrar tia camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permetter a %1$s da duvrar tes microfon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permetter a %1$s dad acceder a tia posiziun?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permetter a %1$s da duvrar tia camera e tes microfon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Camera davos</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Camera davant</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permetter</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Betg permetter</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Memorisar la decisiun per questa website</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Adina</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mai</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Permetter a %1$s da memorisar datas en la memoria durabla?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Permetter a %1$s da reproducir cuntegn controllà da DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Permetter a %1$s dad utilisar ses cookies sin %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Igl è recumandabel da bloccar l\'access sch\'i n\'è betg cler pertge che %s dovra questas datas.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloccar</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Ulteriuras infurmaziuns</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..00aa17bde3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ro/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Permiți %1$s să trimită notificări?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Permiți %1$s să îți utilizeze camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Permiți %1$s să îți folosească microfonul?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Permiți %1$s să îți folosească locația?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Permiți %1$s să îți folosească microfonul și camera?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Cameră posterioară</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Cameră frontală</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permite</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Nu permite</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Ține minte decizia pentru acest site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Întotdeauna</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Niciodată</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..3a0167b4b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ru/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Разрешить %1$s отправлять уведомления?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Разрешить %1$s использовать вашу камеру?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Разрешить %1$s использовать ваш микрофон?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Разрешить %1$s использовать ваше местоположение?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Разрешить %1$s использовать вашу камеру и микрофон?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Микрофон 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Задняя камера</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Передняя камера</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Разрешить</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Не разрешать</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Запомнить для этого сайта</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Всегда</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Никогда</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Разрешить %1$s хранить данные в постоянном хранилище?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Разрешить %1$s воспроизводить защищённое авторским правом содержимое?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Разрешить %1$s использовать собственные куки на %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Вы можете заблокировать доступ, если не понимаете, зачем %s нужны эти данные.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Заблокировать</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Подробнее</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..921025084f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sat/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">ᱤᱛᱞᱟᱹᱭ ᱠᱩᱞ ᱞᱟᱹᱜᱤᱫ ᱛᱮ %1$s ᱦᱮᱥᱟᱨᱤᱭᱟᱹ ᱮᱢᱟᱭ ᱢᱮ?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">ᱟᱢᱟᱜ ᱠᱮᱢᱨᱟ ᱵᱮᱵᱷᱟᱨ ᱞᱟᱹᱜᱤᱫ ᱛᱮ %1$s ᱦᱮᱥᱟᱨᱤᱭᱟᱹ ᱮᱢᱟᱭ ᱢᱮ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">ᱟᱢᱟᱜ ᱢᱟᱭᱠᱨᱚᱯᱷᱚᱱ ᱵᱮᱵᱷᱟᱨ ᱞᱟᱹᱜᱤᱫ ᱛᱮ %1$s ᱦᱮᱥᱟᱨᱤᱭᱟᱹ ᱮᱢᱟᱭ ᱢᱮ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">ᱟᱢᱟᱜ ᱡᱟᱭᱜᱟ ᱵᱮᱵᱷᱟᱨ ᱞᱟᱹᱜᱤᱫ ᱛᱮ %1$s ᱦᱮᱥᱟᱨᱤᱭᱟᱹ ᱮᱢᱟᱭ ᱢᱮ?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">ᱟᱢᱟᱜ ᱠᱮᱢᱨᱟ ᱟᱨ ᱢᱟᱭᱠᱨᱚᱯᱷᱚᱱ ᱵᱮᱵᱷᱟᱨ ᱞᱟᱹᱜᱤᱫ ᱛᱮ %1$s ᱦᱮᱥᱟᱨᱤᱭᱟᱹ ᱮᱢᱟᱭ ᱢᱮ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ᱢᱟᱭᱠᱨᱚᱯᱷᱚᱱ 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">ᱛᱟᱭᱚᱢ ᱠᱮᱢᱨᱟ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">ᱥᱟᱢᱟᱝ ᱥᱮᱫᱟᱜ ᱠᱮᱢᱨᱟ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ᱦᱮᱥᱟᱹᱨᱤᱭᱟᱹ</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ᱟᱞᱚ ᱢᱟᱸᱡᱩᱨᱮᱭᱟᱢ</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">ᱱᱤᱥᱴᱟᱹ ᱫᱤᱥᱚᱸ ᱫᱚᱦᱚᱭ ᱢᱮ ᱱᱚᱶᱟ ᱥᱟᱭᱤᱴ ᱞᱟᱹᱜᱤᱫ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ᱡᱟᱣᱜᱮ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ᱛᱤᱥ ᱦᱚᱸ ᱵᱟᱝ</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title"> %1$s ᱵᱮᱥ ᱡᱟᱭᱜᱟ ᱨᱮ ᱰᱟᱴᱟ ᱫᱚᱦᱚ ᱪᱷᱚᱭᱮᱢ ᱥᱮ?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s DRM-ᱠᱚᱵᱽᱡᱟ ᱡᱤᱱᱤᱥ ᱠᱚ ᱯᱞᱮ ᱪᱷᱚ ᱞᱟᱹᱜᱤᱫ ᱚᱱᱩᱢᱚᱛᱤ ᱮᱢᱟᱭᱟ ᱥᱮ? </string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s ᱫᱚ %2$s ᱨᱮ ᱠᱩᱠᱤᱡᱽ ᱵᱮᱵᱷᱟᱨ ᱪᱷᱚᱣᱟᱭᱟᱢ ᱥᱮ ?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">ᱟᱢ ᱫᱚ ᱱᱚᱶᱟ ᱵᱞᱚᱠ ᱥᱮᱱᱟᱢ ᱠᱟᱱᱟ\ᱡᱩᱫᱤ ᱟᱢ ᱵᱟᱝ ᱵᱟᱲᱟᱭᱟᱢ %s ᱰᱟᱴᱟ ᱪᱮᱫᱟᱜ ᱫᱚᱨᱠᱟᱨ ᱠᱟᱱᱟ ᱾</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">ᱟᱠᱚᱴ</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ᱰᱷᱮᱨ ᱥᱮᱬᱟᱭ ᱢᱮ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..a8a2e9d0e8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sc/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Boles permìtere chi %1$s imbiet notìficas?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Boles permìtere chi %1$s impreet sa càmera tua?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Boles permìtere chi %1$s impreet su micròfonu tuo?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Boles permìtere chi %1$s impreet sa positzione tua?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Boles permìtere chi %1$s impreet sa càmera e su micròfonu tuos?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micròfonu 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Càmera posteriore</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Càmera anteriore</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Permite</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Non permitas</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Regorda sa detzisione pro custu situ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Semper</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mai</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Boles permìtere a %1$s de cunservare datos in s’archiviatzione permanente?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Boles permìtere a %1$s de reprodùere cuntenutos protetos dae DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Boles permìtere a %1$s de impreare is testimòngios suos in %2$s?</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloca</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Àteras informatziones</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..da8e984bed
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-si/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s සඳහා දැනුම්දීම් යැවීමට ඉඩ දෙන්නද?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s සඳහා රූගතය භාවිතයට ඉඩ දෙන්නද?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s සඳහා ශබ්දවාහිනිය භාවිතයට ඉඩ දෙන්නද?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s සඳහා ස්ථානයට ප්‍රවේශයට ඉඩ දෙන්නද?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s සඳහා රූගතය හා ශබ්දවාහිනිය භාවිතයට ඉඩ දෙන්නද?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ශබ්දවාහිනිය 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">පසුපස රූගත උපාංගය</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">ඉදිරිපස රූගත උපාංගය</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ඉඩ දෙන්න</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ඉඩ නොදෙන්න</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">අඩවිය සඳහා තීරණය මතක තබා ගන්න</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">සැමවිට</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">කවදාවත්</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s වෙත අනවරත ආචයනයේ දත්ත ගබඩා කිරීමට ඉඩ දෙන්නද?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s වෙත DRM-පාලිත අන්තර්ගත වාදනයට ඉඩ දෙන්නද?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s සඳහා එහි දත්තකඩ %2$s හි භාවිතයට ඉඩ දෙන්නද?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">%s වෙත මෙම දත්ත අවශ්‍ය වන්නේ මන්දැයි අපැහැදිලි නම් ඔබට ප්‍රවේශය අවහිර කිරීමට අවශ්‍ය විය හැකිය.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">අවහිර</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">තව දැනගන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..e0d95d238d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sk/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Chcete stránke %1$s povoliť odosielanie upozornení?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Chcete stránke %1$s povoliť používanie vašej kamery?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Chcete stránke %1$s povoliť používanie vášho mikrofónu?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Chcete stránke %1$s povoliť používanie vašej polohy?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Chcete stránke %1$s povoliť používanie vašej kamery a mikrofónu?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofón 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Zadná kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Predná kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Povoliť</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Nepovoliť</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Zapamätať si toto rozhodnutie pre túto stránku</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Vždy</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nikdy</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Chcete stránke %1$s povoliť ukladanie údajov do trvalého úložiska?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Chcete stránke %1$s povoliť prehrávanie obsahu chráneného pomocou DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Chcete povoliť stránke %1$s používať svoje súbory cookie aj na stránke %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Ak nie je jasné, prečo %s potrebuje tieto údaje, môžete jej prístup zakázať.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Zakázať</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Ďalšie informácie</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..78fdd041e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-skr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s کوں اطلاعاں پٹھݨ دی اجازت ݙیندے ہو؟</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s کوں آپݨاں کیمرہ ورتݨ دی اجازت ݙیندے ہو؟</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s کوں آپݨاں مائیکروفون ورتݨ دی اجازت ݙیندے ہو؟</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s کوں آپݨاں مقام ورتݨ دی اجازت ݙیندے ہو؟</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s کوں آپݨاں کیمرہ تے مائیکروفون ورتݨ دی اجازت ݙیندے ہو؟</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">مائیکروفون ١</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">پچھوں آلا کیمرہ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">سامھِݨے آلا کیمرہ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">اجازت ݙیوو</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">اجازت نہ ݙیوو</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">ایں سائٹ کیتے فیصلہ یاد رکھو</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ہمیشہ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">کݙاہیں نہ</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s کوں مستقل ذخیرے وچ ڈیٹا ذخیرہ کرݨ دی اجازت ݙیووں؟</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s کوں ڈی آر ایم دے کنٹرول تھئے مواد چلاوݨ دی اجازت ݙیووں؟</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s کوں آپݨیاں کوکیاں %2$s تے ورتݨ دی اجازت ݙیووں؟</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">جے ایہ واضح کائنی جو %s کوں ایں ڈیٹا دی لوڑ کیوں ہے تاں تساں رسائی تے پابندی لاوݨ پسند کریسو۔</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">بلاک</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ٻیا سِکھو</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..a8dbfd63fe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sl/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Dovolite %1$s pošiljanje obvestil?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Dovolite %1$s uporabo kamere?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Dovolite %1$s uporabo mikrofona?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Dovolite %1$s uporabo lokacije?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Dovolite %1$s uporabo kamere in mikrofona?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Zadnja kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Sprednja kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Dovoli</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ne dovoli</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Zapomni si odločitev za to stran</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Vedno</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nikoli</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Dovolite strani %1$s shranjevati podatke v trajno shrambo?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Dovolite %1$s predvajanje vsebine, zaščitene z DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Ali dovolite %1$s uporabo svojih piškotkov na %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Če ni jasno, zakaj %s potrebuje te podatke, lahko zavrnete dostop.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Zavrni</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Več o tem</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..8053f7e7f7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sq/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Të lejohet %1$s të dërgojë njoftime?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Të lejohet %1$s të përdorë kamerën tuaj?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Të lejohet %1$s të përdorë mikrofonin tuaj?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Të lejohet %1$s të përdorë vendndodhjen tuaj?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Të lejohet %1$s të përdorë kamerën dhe mikrofonin tuaj?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kamera e pasme</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kamera ballore</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Lejoje</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Mos e lejo</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Mbaje mend vendimin për këtë sajt</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Përherë</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Kurrë</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Të lejohet %1$s të depozitojë të dhëna në depozitë të qëndrueshme?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Të lejohet %1$s të luajë lëndë nën DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Të lejohet %1$s të përdorë cookie-t e veta në %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Mund të doni t’i bllokoni hyrjen, nëse s’është e qartë pse i duhen këto të dhëna %s.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bllokoje</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Mësoni më tepër</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..4217960f6c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Дазволити %1$s да шаље обавештења?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Дозволити %1$s да користи вашу камеру?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Дозволити %1$s да користи ваш микрофон?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Дозволити %1$s да користи вашу локацију?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Дозволити %1$s да користи вашу камеру и микрофон?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Микрофон 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Главна камера</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Предња камера</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Дозволи</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Не дозволи</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Запамти одлуку за ову страницу</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Увек</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Никад</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Дозволити %1$s да складишти податке у трајном складишту?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Дозволи %1$s да репродукује садржаја контролисаног DRM-ом?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Дозволи %1$s да користи своје колачиће на %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Размислите о блокирању приступа ако није јасно зашто %s треба ове податке.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Блокирај</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Сазнајте више</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..54088a0c0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-su/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Idinan %1$s pikeun ngirim iber?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Idinan %1$s maké kaméra anjeun?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Idinan %1$s maké mikropon anjeun?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Idinan %1$s maké lokasi anjeun?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Idinan %1$s maké kaméra jeung mikropon anjeun?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikropon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kaméra tukang</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kaméra hareup</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Idinan</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Hulag</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Emut kaputusan pikeun ieu loka</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Matuh</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Kungsi</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Idinan %1$s pikeun nyimpen data dina panyimpenan anu tetep?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Idinan %1$s pikeun maénkeun kontén anu diatur ku DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Idinan %1$s maké réréméhna di %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Anjeun bisa meungpeuk aksés lamun teu puguh naha %s butuh ieu data.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Peungpeuk</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Lenyepan</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..383ccd5344
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Tillåt %1$s att skicka aviseringar?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Tillåt %1$s att använda din kamera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Tillåt %1$s att använda din mikrofon?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Tillåt %1$s att använda din plats?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Tillåt %1$s att använda din kamera och mikrofon?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kameran på baksidan av telefonen</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kameran på framsidan av telefonen</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Tillåt</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Tillåt inte</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Kom ihåg beslutet för denna webbplats</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Alltid</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Aldrig</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Tillåta %1$s att lagra data i beständig lagring?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Tillåt %1$s att spela DRM-kontrollerat innehåll?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Tillåt %1$s att använda kakor på %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Du kanske vill blockera åtkomst om det inte är klart varför %s behöver dessa data.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Blockera</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Läs mer</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..4684de636b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ta/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">அறிவிப்புகளை அனுப்ப %1$s ஐ அனுமதிக்கவா?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">உங்கள் படக்கருவியைப் பயன்படுத்த %1$s ஐ அனுமதிக்கவா?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">உங்கள் ஒலிவாங்கியைப் பயன்படுத்த %1$s ஐ அனுமதிக்கவா?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">உங்கள் இருப்பிடத்தைப் பயன்படுத்த %1$s ஐ அனுமதிக்கவா?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">உங்கள் படக்கருவியையும் ஒலிவாங்கியையும் பயன்படுத்த %1$s ஐ அனுமதிக்கவா?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ஒலிவாங்கி 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">பின்பக்கப் படக்கருவி</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">முன்பக்க புகைப்பட கருவி</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">அனுமதி</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">அனுமதிக்காதே</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">இந்த தளத்திற்கான முடிவை நினைவில் கொள்க</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">எப்போதும்</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ஒரு போதும் இல்லை</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">நிரந்தரச் சேமிப்பில் தரவைச் சேமிக்க %1$s தளத்தை அனுமதிக்கவா?</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..3cff876a07
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-te/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">గమనింపులను పంపడానికి %1$s‌ని అనుమతించాలా?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">మీ కెమెరా వాడటానికి %1$s‌ని అనుమతించాలా?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">మీ మైక్రోఫోను వాడటానికి %1$s‌ని అనుమతించాలా?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">మీ స్థానాన్ని చూడటానికి %1$s‌ని అనుమతించాలా?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">మీ కెమెరా, మైక్రోఫోన్లనూ వాడటానికి %1$sను అనుమతిస్తారా?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">మైక్రోఫోను 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">వెనుకవైపు కెమెరా</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">ముందువైపు కెమెరా</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">అనుమతించు</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">అనుమతించవద్దు</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">నిర్ణయాన్ని ఈ సైటుకి గుర్తుంచుకో</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ఎల్లప్పుడూ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ఎప్పటికీకాదు</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">నిరంతర నిల్వలో డేటాను నిల్వ చేయడానికి %1$sని అనుమతించాలా?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">DRM- నియంత్రిత విషయాన్ని ప్లే చేయడానికి %1$s ని అనుమతించాలా?</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">నిరోధించు</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">ఇంకా తెలుసుకోండి</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..a2a85c08d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tg/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Ба %1$s барои фиристодани огоҳномаҳо иҷозат медиҳед?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Ба %1$s барои истифодаи камераи шумо иҷозат медиҳед?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Ба %1$s барои истифодаи микрофони шумо иҷозат медиҳед?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Ба %1$s барои истифодаи ҷойгиршавии шумо иҷозат медиҳед?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Ба %1$s барои истифодаи камера ва микрофони шумо иҷозат медиҳед?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Микрофони 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Камераи қафо</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Камераи пеш</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Иҷозат додан</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Иҷозат дода нашавад</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Қарори ҷорӣ барои ин сомона дар хотир дошта шавад</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Ҳамеша</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Ҳеҷ гоҳ</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Ба %1$s иҷозат медиҳед, ки маълумотро дар захирагоҳи доимӣ нигоҳ дорад?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Ба %1$s иҷозат медиҳед, ки муҳтавои идорашавандаи DRM-ро пахш кунад?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Ба %1$s иҷозат медиҳед, ки он кукиҳои худро дар %2$s истифода барад?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Шумо метавонед дастрасиро манъ кунед, агар маълум набошад, ки чаро ба %s ин маълумот лозим аст.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Манъ кардан</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Маълумоти бештар</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..f39cdcaf6e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-th/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">อนุญาตให้ %1$s ส่งการแจ้งเตือนหรือไม่?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">อนุญาตให้ %1$s ใช้กล้องของคุณหรือไม่?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">อนุญาตให้ %1$s ใช้ไมโครโฟนของคุณหรือไม่?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">อนุญาตให้ %1$s ใช้ตำแหน่งที่ตั้งของคุณหรือไม่?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">อนุญาตให้ %1$s ใช้กล้องและไมโครโฟนของคุณหรือไม่?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">ไมโครโฟน 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">กล้องหลัง</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">กล้องหน้า</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">อนุญาต</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">ไม่อนุญาต</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">จดจำการตัดสินใจสำหรับไซต์นี้</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">เสมอ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ไม่เลย</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">อนุญาตให้ %1$s จัดเก็บข้อมูลในที่เก็บข้อมูลถาวรหรือไม่?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">อนุญาตให้ %1$s เล่นเนื้อหาที่ควบคุมด้วย DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">อนุญาตให้ %1$s ใช้คุกกี้บน %2$s หรือไม่?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">คุณสามารถปิดกั้นการเข้าถึงได้หากคุณไม่แน่ใจเหตุผลที่ %s ต้องการข้อมูลนี้</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">ปิดกั้น</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">เรียนรู้เพิ่มเติม</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..11210a1a26
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tl/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Payagan ang %1$s na magpadala ng mga abiso?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Payagan ang %1$s na magamit ang iyong camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Payagan ang %1$s na magamit ang iyong mikropono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Payagan ang %1$s na magamit ang iyong lokasyon?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Payagan ang %1$s na magamit ang iyong camera at mikropono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikropono</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kamera sa likuran</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kamera sa harapan</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Payagan</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Huwag Payagan</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Tandaan ang desisyon para sa site na ito</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Palagi</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Hindi kailanman</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Payagan ang %1$s na mag-imbak ng data sa persistence storage?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Payagan ang %1$s na magpaandar ng DRM-controlled content?</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">I block</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Matuto pa</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..1845aecafd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s bildirim gönderebilsin mi?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s kameranızı kullanabilsin mi?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s mikrofonunuzu kullanabilsin mi?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s konumunuzu kullanabilsin mi?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s kameranızı ve mikrofonunuzu kullanabilsin mi?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Arka kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Ön kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">İzin ver</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">İzin verme</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Bu sitede bu kararımı hatırla</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Her zaman</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Asla</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s kalıcı depolama alanında veri depolayabilsin mi?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s sitesi DRM denetimli içerikleri oynatabilsin mi?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s, çerezlerini %2$s sitesinde kullanabilsin mi?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">%s sitesinin bu veriye neden ihtiyaç duyduğunu bilmiyorsanız erişimi engelleyebilirsiniz.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Engelle</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Daha fazla bilgi al</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..7f29c0ce1a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-trs/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Gā\'nïnt riña %1$s da\' gānātàj a\'ngô nuguan\' huā aj.</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Gā\'nïnt riña %1$s da\' gārasunj si kamarâ raj.</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Gā\'nïnt riña %1$s da\' gārasunj si āgâ\'t aga\' uxun nanèe aj.</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Gā\'nïnt riña %1$s da\' gārasunj lūguâ nun raj.</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Gā\'nïnt riña %1$s da\' gārasunj si kamarât ngà si āgâ\'t aga\' uxun nanèe aj.</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Aga\' uxun nanèe 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Kamarâ nù ne\' rūkùu</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Kamarâ nù ne\' ñāan</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Gā\'nïn</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Sī ga\'nïnjt</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Nanun ruhuâ nuguan\' nan guendâ sitiô nan</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Nīgànj chre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Nitāj āmān</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Gā\’nïnt riña %1$s da\' nachra chre\’ nâ man nej datô huā aj.</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Gā’nïnt riña %1$s da’ dūguachrá man sa dugumîn DRM aj.</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Gā’nïnt riña %1$s da’ gārasun man nej si kokij riña %2$s aj.</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Si gūruhuaj nī nāránt ña gātūt nan dadin’ sa gū’nàj %s achín nej si datôt.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Garûn\'</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Gāhuin chrūn doj</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..be1cf0bb70
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tt/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s искәртүләр җибәрә алсынмы?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s камерагызны куллана алсынмы?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s микрофоныгызны куллана алсынмы?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s урнашуыгызны куллана алсынмы?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s камера һәм микрофоныгызны куллана алсынмы?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Микрофон 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Арткы камера</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Алгы камера</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Рөхсәт итү</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Рөхсәт итмәү</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Бу сайт өчен карарны истә калдыру</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Һәрвакыт</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Беркайчан да</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s мәгълүматларны даими саклагычка саклый алсынмы?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s сайты DRM-лы эчтәлекләрне уйната алсынмы?</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Блоклау</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Күбрәк белү</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..aab9240198
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">ssureg %1$s ad yazen tineɣmisin?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">ssureg %1$s ad issemres takamṛa-nnek?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">ssureg %1$s ad issemres amikṛu-nnek?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">ssureg %1$s ad issemres tansa-nnek?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">ssureg %1$s ad issemres takamṛa d umikṛu-nnek?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Amikṛu1</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">ssureg</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ad ur ssirig</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Kti taɣtast n usmel-a</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Abda</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Usar</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Isin uggar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..023a260377
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ug/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s نىڭ ئۇقتۇرۇش ئەۋەتىشىگە يول قويامدۇ؟</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s كامېرايىڭىزنى ئىشلىتىشىگە يول قويامسىز؟</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s مىكروفونىڭىزنى ئىشلىتىشىگە يول قويامسىز؟</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s ئورنىڭىزنى ئىشلىتىشىگە يول قويامسىز؟</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s كامېرا ۋە مىكروفونىڭىزنى ئىشلىتىشىگە يول قويامسىز؟</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">مىكروفون 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">ئارقا كامېرا</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">ئالدى كامېرا</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">يول قوي</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">رۇخسەت بەرمە</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">بۇ بېكەتتىكى قارارىڭىزنى ئەستە تۇتىدۇ</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ھەمىشە</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">ھىچقاچان</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$s سانلىق مەلۇماتلارنى ئىزچىل ساقلىشىغا يول قويامدۇ؟</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">DRM تىزگىنلىنىدىغان مەزمۇننى %1$s نىڭ قويۇشىغا يول قويامدۇ؟</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%2$s دىكى cookies نى %1$s نىڭ ئىشلىتىشىگە يول قويامدۇ؟</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">ئەگەر %s بۇ سانلىق مەلۇماتلارنى نېمىگە ئىشلىتىدىغانلىقىنىڭ سەۋەبىنى ئېنىق بايان قىلمىسا، ئۇنى چەكلىشىڭىز كېرەك.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">توسۇش</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">تەپسىلاتى</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..bccd8857bf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-uk/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Дозволити %1$s надсилати сповіщення?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Дозволити %1$s використовувати вашу камеру?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Дозволити %1$s використовувати ваш мікрофон?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Дозволити %1$s використовувати ваше розташування?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Дозволити %1$s використовувати ваші камеру й мікрофон?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Мікрофон 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Задня камера</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Передня камера</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Дозволити</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Не дозволяти</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Запам’ятати рішення для цього сайту</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Завжди</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Ніколи</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Дозволити %1$s зберігати дані у постійному сховищі?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Дозволити %1$s відтворювати вміст, контрольований DRM?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Дозволити %1$s використовувати свої файли cookie на %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Ви можете заблокувати доступ, якщо вам незрозуміло, нащо %s потребує ці дані.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Заблокувати</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Докладніше</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..c8b1a68726
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-ur/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s کو اطلاعات بھیجنے کی اجازت دیں؟</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s کو اپنا کیمرا استعمال کرنے کی اجازت دیں؟</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s کو اپنا مائکروفون استعمال کرنے کی اجازت دیں؟</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s کو اپنا محل وقوع جاننے کی اجازت دیں؟</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s کو اپنا کیمرا اور مائکروفون کا استعمال کرنے کی اجازت دیں؟</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one"> 1مائیکروفون</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">پیچھے والا کیمرہ</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">سامنے والا کیمرہ</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">اجازت دیں</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">اجازت مت دیں</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">اس سائٹ کے بارے میں فیصلہ یاد رکھیں</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">ہمیشہ</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">کبھی نہیں</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">%1$s کو DRM کے زیر کنٹرول شدہ مواد چلانے کی اجازت دیں؟</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">مزید سیکھیں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..a43e77b301
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-uz/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">%1$s bildirishnomalar yuborishga ruxsat berilsinmi?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">%1$s kameradan foydalanishiga ruxsat berilsinmi?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">%1$s mikrofondan foydalanishiga ruxsat berilsinmi?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">%1$s joylashuv axborotidan foydalanishi uchun ruxsat berilsinmi?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">%1$s kamera va mikrofondan foydalanishiga ruxsat berilsinmi?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Mikrofon 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Orqa kamera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Old kamera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Ruxsat berish</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Ruxsat berilmasin</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Bu sayt uchun qaror eslab qolinsin</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Doimo</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Hech qachon</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">%1$sga maʼlumotlarni mavjud xotiraga joylashga ruxsat berilsinmi?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">DRM tomonidan boshqariladigan kontentni %1$s ishga tushirishiga ruxsat berilsinmi?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">%1$s saytiga %2$s saytidagi cookie fayllardan foydalansihga ruxsat berasizmi?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Agar bu maʼlumotlar %s saytiga nima uchun kerakligi aniq boʻlmasa, kirishni bloklashingiz mumkin.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Bloklash</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Batafsil</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..7d3f70a8d7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-vec/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Parmeti a %1$s de mandare notifeghe?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Pasrmeti a %1$s de uxare ƚa to teƚecamera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Parmeti a %1$s de uxare el to microfono?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Parmeti a %1$s de uxare ƚa to poxision?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Parmeti a %1$s de uxare ƚa to camera e el to microfono?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microfono 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Teƚecamera de drio</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Teƚecamera de davanti</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Parmeti</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">No parmetere</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Ricordate ƚa decixion par sto sito</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Senpre</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Mai</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..537c8d41cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-vi/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Cho phép %1$s gửi thông báo?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Cho phép %1$s sử dụng máy ảnh của bạn?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Cho phép %1$s sử dụng micrô của bạn?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Cho phép %1$s sử dụng vị trí của bạn?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Cho phép %1$s sử dụng máy ảnh và micrô của bạn?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Micrô 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Máy ảnh sau</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Máy ảnh trước</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Cho phép</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Không cho phép</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Ghi nhớ quyết định cho trang web này</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Luôn luôn</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Không bao giờ</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Cho phép %1$s lưu trữ dữ liệu trong bộ nhớ lâu dài?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Cho phép %1$s phát nội dung được DRM bảo vệ?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Cho phép %1$s sử dụng cookie của nó trên %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">Bạn có thể muốn chặn truy cập nếu không rõ tại sao %s cần dữ liệu này.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Chặn</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Tìm hiểu thêm</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..ca9ed2c180
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-yo/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Fàyègba %1$s láti fi àwọn ìtanilólobó ráńṣẹ́?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Fàyègba %1$s láti lo èrọ-ayàwòrán rẹ?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Fàyègba %1$s láti lo ẹ̀rọ-ìgbóhùn rẹ?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Gbà láàyè %1$s láti lo ibi tí o wà?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Fàyègba %1$s láti lo ẹ̀rọ-ayàwòrán àti ẹ̀rọ-ìgbóhùn rẹ?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Ẹ̀rọ-ìgbóhùn 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Ẹ̀rọ-ayàwòrán tó ń kọjú-sẹ́hìn</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Ẹ̀rọ-ayàwòrán tó ń kojú-síwájú</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Fàyègbà</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Má ṣe fàyègbà</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Rántí ìpinnu fún sáìtì yí</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Ní gbogbo ìgbà</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Láéláé</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Fàyègba %1$s láti fi dátà pamọ́ sí àyè tó dúróore?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Fàyègba %1$s láti mú àkóónú ìṣàkóso DRM ṣiṣẹ́ bí?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Falyègba %1$s láti lo àwọn kúkì rẹ̀ lórí %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">O lè fẹ́ dẹ̀nà ìwọlé tó bá rú ọ lójú ìdí %s tó fi nílò dátà yí.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Dẹ̀nà</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Kẹ́kọ̀ọ́ si</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..6f5f603c5d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">要允许 %1$s 发送通知吗?</string>
+
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">要允许 %1$s 使用您的相机吗?</string>
+
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">要允许 %1$s 使用您的麦克风吗?</string>
+
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">要允许 %1$s 获取您的位置吗?</string>
+
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">要允许 %1$s 使用您的相机和麦克风吗?</string>
+
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">麦克风 1</string>
+
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">后置摄像头</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">前置摄像头</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">允许</string>
+
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">不允许</string>
+
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">记住对此网站的决定</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">总是</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">一律拒绝</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">要允许 %1$s 持久存储数据吗?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">要允许 %1$s 播放受 DRM 控制的内容吗?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">要允许 %1$s 在 %2$s 上使用它自己的 Cookie 吗?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">若 %s 未明确阐述需要此数据的原因,您应该阻止它。</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">阻止</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">详细了解</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..f8ec799bf0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">要允許 %1$s 傳送通知嗎?</string>
+
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">要允許 %1$s 使用您的攝影機嗎?</string>
+
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">要允許 %1$s 使用您的麥克風嗎?</string>
+
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">要允許 %1$s 知道您的所在位置嗎?</string>
+
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">要允許 %1$s 使用您的攝影機與麥克風嗎?</string>
+
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">1 號麥克風</string>
+
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">後鏡頭</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">前鏡頭</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">允許</string>
+
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">不允許</string>
+
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">對此網站記住我的決定</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">總是</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">永不</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">要允許 %1$s 將資料儲存於持續性儲存空間嗎?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">要允許 %1$s 播放 DRM 控制的內容嗎?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">要允許 %1$s 在 %2$s 上使用該網站自己的 cookie 嗎?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">如果 %s 沒有清楚解釋為什麼需要此資料,您應該封鎖它。</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">封鎖</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">了解更多</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..4fdaeb3721
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/res/values/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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>
+ <!-- Title of a dialog for a notification permission request. -->
+ <string name="mozac_feature_sitepermissions_notification_title">Allow %1$s to send notifications?</string>
+ <!-- Title of a dialog for a camera permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_title">Allow %1$s to use your camera?</string>
+ <!-- Title of a dialog for a microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_microfone_title">Allow %1$s to use your microphone?</string>
+ <!-- Title of a dialog for a location permission request. -->
+ <string name="mozac_feature_sitepermissions_location_title">Allow %1$s to use your location?</string>
+ <!-- Title of a dialog for a camera and microphone permission request. -->
+ <string name="mozac_feature_sitepermissions_camera_and_microphone">Allow %1$s to use your camera and microphone?</string>
+ <!-- Option in a dialog for requesting a microphone permission, this option will give access to
+ the first microphone-->
+ <string name="mozac_feature_sitepermissions_option_microphone_one">Microphone 1</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ back facing camera-->
+ <string name="mozac_feature_sitepermissions_back_facing_camera2">Rear-facing camera</string>
+ <!-- Option in a dialog for requesting a camera permission, this option will give access to
+ front facing camera-->
+ <string name="mozac_feature_sitepermissions_selfie_camera2">Front-facing camera</string>
+ <!-- Text for a positive button in a permission request dialog, this button will give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_allow">Allow</string>
+ <!-- Text for a negative button in a permission request dialog, this button will do not give
+ access to this permission-->
+ <string name="mozac_feature_sitepermissions_not_allow">Don’t allow</string>
+ <!-- Text for a checkbox in a permission request dialog, this will allow to not show again the prompt-->
+ <string name="mozac_feature_sitepermissions_do_not_ask_again_on_this_site2">Remember decision for this site</string>
+ <!-- Text for a positive button in a permission request dialog. This will always allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_always_allow">Always</string>
+ <!-- Text for a negative button in a permission request dialog. This will never allow and remember the decision of the user, this is the special case of the notification permission, that is only ask one time-->
+ <string name="mozac_feature_sitepermissions_never_allow">Never</string>
+ <!-- Title of a dialog to require to save data in persistent storage. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_persistent_storage_title">Allow %1$s to store data in persistent storage?</string>
+ <!-- Title of a dialog to use EME. %1$s will be replaced with the URL of the current page -->
+ <string name="mozac_feature_sitepermissions_media_key_system_access_title">Allow %1$s to play DRM-controlled content?</string>
+ <!-- Title of a dialog to use cross origin storage permission. %1$s is the name of the site URL (www.site1.example) trying to track the user's activity.
+ %2$s is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar -->
+ <string name="mozac_feature_sitepermissions_storage_access_title">Allow %1$s to use its cookies on %2$s?</string>
+ <!-- Message of a dialog offering more context about the cross origin storage permission.
+ %s is the name of the site URL (www.site1.example) trying to track the user's activity. -->
+ <string name="mozac_feature_sitepermissions_storage_access_message">You may want to block access if it\'s not clear why %s needs this data.</string>
+ <!-- Text for a negative button in the storage access permission request dialog. This button will not give access to this permission. -->
+ <string name="mozac_feature_sitepermissions_storage_access_not_allow">Block</string>
+ <!-- Clickable text that will open a new tab navigating the user to online documentation about specific features. -->
+ <string name="mozac_feature_sitepermissions_learn_more_title">Learn more</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorageTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorageTest.kt
new file mode 100644
index 0000000000..aef533b39e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorageTest.kt
@@ -0,0 +1,251 @@
+/* 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.sitepermissions
+
+import android.os.Looper.getMainLooper
+import androidx.paging.DataSource
+import androidx.room.DatabaseConfiguration
+import androidx.room.InvalidationTracker
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.DataCleanable
+import mozilla.components.concept.engine.Engine.BrowsingData
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.AUTOPLAY_AUDIBLE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.AUTOPLAY_INAUDIBLE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.BLUETOOTH
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.CAMERA
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.LOCAL_STORAGE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.LOCATION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.MICROPHONE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.NOTIFICATION
+import mozilla.components.feature.sitepermissions.db.SitePermissionsDao
+import mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase
+import mozilla.components.feature.sitepermissions.db.SitePermissionsEntity
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class OnDiskSitePermissionsStorageTest {
+
+ private lateinit var mockDAO: SitePermissionsDao
+ private lateinit var storage: OnDiskSitePermissionsStorage
+ private lateinit var mockDataCleanable: DataCleanable
+
+ @Before
+ fun setup() {
+ mockDAO = mock()
+ mockDataCleanable = mock()
+ storage = spy(
+ OnDiskSitePermissionsStorage(mock(), mockDataCleanable).apply {
+ databaseInitializer = { mockDatabase(mockDAO) }
+ },
+ )
+ }
+
+ @Test
+ fun `save a new SitePermission`() = runTest {
+ val sitePermissions = createNewSitePermission()
+
+ storage.save(sitePermissions, private = false)
+
+ verify(mockDAO).insert(any())
+ }
+
+ @Test
+ fun `GIVEN a private permission WHEN save THEN the SitePermission is not store`() = runTest {
+ val sitePermissions = createNewSitePermission()
+
+ storage.save(sitePermissions, private = true)
+
+ verify(mockDAO, times(0)).insert(any())
+ }
+
+ @Test
+ fun `update a SitePermission`() = runTest {
+ val sitePermissions = createNewSitePermission()
+
+ storage.update(sitePermissions, private = false)
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockDAO).update(any())
+ verify(mockDataCleanable).clearData(BrowsingData.select(BrowsingData.PERMISSIONS), sitePermissions.origin)
+ }
+
+ @Test
+ fun `GIVEN a private permission WHEN update THEN the SitePermission is not store`() = runTest {
+ val sitePermissions = createNewSitePermission()
+
+ storage.update(sitePermissions, private = true)
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockDAO, times(0)).update(any())
+ verify(mockDataCleanable, times(0)).clearData(
+ BrowsingData.select(BrowsingData.PERMISSIONS),
+ sitePermissions.origin,
+ )
+ }
+
+ @Test
+ fun `find a SitePermissions by origin`() = runTest {
+ storage.findSitePermissionsBy("mozilla.org", private = false)
+
+ verify(mockDAO).getSitePermissionsBy("mozilla.org")
+ }
+
+ @Test
+ fun `GIVEN private sitePermissions WHEN findSitePermissionsBy THEN reset SitePermissions`() =
+ runTest {
+ val dbPermission = SitePermissionsEntity(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = ALLOWED,
+ location = BLOCKED,
+ notification = NO_DECISION,
+ microphone = ALLOWED,
+ camera = BLOCKED,
+ bluetooth = ALLOWED,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED,
+ mediaKeySystemAccess = NO_DECISION,
+ savedAt = 0,
+ )
+
+ doReturn(dbPermission).`when`(mockDAO)
+ .getSitePermissionsBy(origin = dbPermission.origin)
+
+ val foundPermissions =
+ storage.findSitePermissionsBy(dbPermission.origin, private = true)
+
+ assertNull(foundPermissions)
+ }
+
+ @Test
+ fun `find all sitePermissions grouped by permission`() = runTest {
+ doReturn(dummySitePermissionEntitiesList())
+ .`when`(mockDAO).getSitePermissions()
+
+ val map = storage.findAllSitePermissionsGroupedByPermission()
+
+ verify(mockDAO).getSitePermissions()
+
+ assertEquals(2, map[LOCAL_STORAGE]?.size)
+ assertFalse(LOCATION in map)
+ assertFalse(NOTIFICATION in map)
+ assertFalse(CAMERA in map)
+ assertFalse(AUTOPLAY_AUDIBLE in map)
+ assertFalse(AUTOPLAY_INAUDIBLE in map)
+ assertEquals(2, map[BLUETOOTH]?.size)
+ assertEquals(2, map[MICROPHONE]?.size)
+ }
+
+ @Test
+ fun `remove a SitePermissions`() = runTest {
+ val sitePermissions = createNewSitePermission()
+
+ storage.remove(sitePermissions, private = false)
+
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockDAO).deleteSitePermissions(any())
+ verify(mockDataCleanable).clearData(BrowsingData.select(BrowsingData.PERMISSIONS), sitePermissions.origin)
+ }
+
+ @Test
+ fun `remove all SitePermissions`() = runTest {
+ storage.removeAll()
+ shadowOf(getMainLooper()).idle()
+
+ verify(mockDAO).deleteAllSitePermissions()
+ verify(mockDataCleanable).clearData(BrowsingData.select(BrowsingData.PERMISSIONS))
+ }
+
+ @Test
+ fun `get all SitePermissions paged`() = runTest {
+ val mockDataSource: DataSource<Int, SitePermissionsEntity> = mock()
+
+ doReturn(
+ object : DataSource.Factory<Int, SitePermissionsEntity>() {
+ override fun create(): DataSource<Int, SitePermissionsEntity> {
+ return mockDataSource
+ }
+ },
+ ).`when`(mockDAO).getSitePermissionsPaged()
+
+ storage.getSitePermissionsPaged()
+
+ verify(mockDAO).getSitePermissionsPaged()
+ }
+
+ private fun createNewSitePermission(): SitePermissions {
+ return SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ location = BLOCKED,
+ notification = NO_DECISION,
+ microphone = NO_DECISION,
+ camera = NO_DECISION,
+ bluetooth = ALLOWED,
+ savedAt = 0,
+ )
+ }
+
+ private fun dummySitePermissionEntitiesList(): List<SitePermissionsEntity> {
+ return listOf(
+ SitePermissionsEntity(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = ALLOWED,
+ location = BLOCKED,
+ notification = NO_DECISION,
+ microphone = ALLOWED,
+ camera = BLOCKED,
+ bluetooth = ALLOWED,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED,
+ mediaKeySystemAccess = NO_DECISION,
+ savedAt = 0,
+ ),
+ SitePermissionsEntity(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = ALLOWED,
+ location = BLOCKED,
+ notification = NO_DECISION,
+ microphone = ALLOWED,
+ camera = BLOCKED,
+ bluetooth = ALLOWED,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED,
+ mediaKeySystemAccess = NO_DECISION,
+ savedAt = 0,
+ ),
+ )
+ }
+
+ private fun mockDatabase(dao: SitePermissionsDao) = object : SitePermissionsDatabase() {
+ override fun sitePermissionsDao() = dao
+
+ override fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper = mock()
+ override fun createInvalidationTracker(): InvalidationTracker = mock()
+ override fun clearAllTables() = Unit
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragmentTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragmentTest.kt
new file mode 100644
index 0000000000..6e55cc002d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragmentTest.kt
@@ -0,0 +1,481 @@
+/* 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.sitepermissions
+
+import android.view.Gravity.TOP
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.sitepermissions.SitePermissionsFeature.PromptsStyling
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+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.mockito.Mockito.verify
+import androidx.core.R as coreR
+
+@RunWith(AndroidJUnit4::class)
+class SitePermissionsDialogFragmentTest {
+
+ private val permissionRequestId = "permissionID"
+ private val titleIcon = coreR.drawable.notification_icon_background
+
+ @Test
+ fun `build dialog`() {
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = true,
+ ),
+ )
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(R.id.do_not_ask_again)
+
+ assertTrue("Checkbox should be displayed", checkBox.isVisible)
+ assertFalse("Checkbox shouldn't be checked", checkBox.isChecked)
+ assertFalse("User selection property should be false", fragment.userSelectionCheckBox)
+ }
+
+ @Test
+ fun `display dialog with unselected 'don't ask again'`() {
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = true,
+ shouldSelectDoNotAskAgainCheckBox = false,
+ ),
+ )
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(R.id.do_not_ask_again)
+
+ assertTrue("Checkbox should be displayed", checkBox.isVisible)
+ assertFalse("Checkbox shouldn't be checked", checkBox.isChecked)
+ assertFalse("User selection property should be false", fragment.userSelectionCheckBox)
+ }
+
+ @Test
+ fun `display dialog with preselected 'don't ask again'`() {
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = true,
+ shouldSelectDoNotAskAgainCheckBox = true,
+ ),
+ )
+
+ doReturn(testContext).`when`(fragment).requireContext()
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(R.id.do_not_ask_again)
+
+ assertTrue("Checkbox should be displayed", checkBox.isVisible)
+ assertTrue("Checkbox should be checked", checkBox.isChecked)
+ assertTrue("User selection property should be true", fragment.userSelectionCheckBox)
+ }
+
+ @Test
+ fun `dialog with shouldShowDoNotAskAgainCheckBox equals false should not have a checkbox`() {
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = false,
+ ),
+ )
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(R.id.do_not_ask_again)
+
+ assertFalse("Checkbox shouldn't be displayed", checkBox.isVisible)
+ assertFalse("User selection property should be false", fragment.userSelectionCheckBox)
+ }
+
+ @Test
+ fun `dialog with a default null message should not have a message section`() {
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = false,
+ ),
+ )
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val message = dialog.findViewById<TextView>(R.id.message)
+
+ assertFalse("Message shouldn't be displayed", message.isVisible)
+ }
+
+ @Test
+ fun `dialog with passed in message should display that message`() {
+ val expectedMessage = "This is just a test"
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = false,
+ message = expectedMessage,
+ ),
+ )
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val message = dialog.findViewById<TextView>(R.id.message)
+
+ assertTrue("Message should be displayed", message.isVisible)
+ assertEquals(expectedMessage, message.text)
+ }
+
+ @Test
+ fun `dialog with a default shouldShowLearnMoreLink being equal to false should not have a Learn more link`() {
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = false,
+ ),
+ )
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val learnMoreLink = dialog.findViewById<TextView>(R.id.learn_more)
+
+ assertFalse("Learn more link shouldn't be displayed", learnMoreLink.isVisible)
+ }
+
+ @Test
+ fun `dialog with shouldShowLearnMoreLink equals true should show a properly configured Learn more link`() {
+ val feature: SitePermissionsFeature = mock()
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = feature,
+ shouldShowDoNotAskAgainCheckBox = false,
+ shouldShowLearnMoreLink = true,
+ ),
+ )
+ doNothing().`when`(fragment).dismiss()
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val learnMoreLink = dialog.findViewById<TextView>(R.id.learn_more)
+
+ assertTrue("Learn more link shouldn't be displayed", learnMoreLink.isVisible)
+ assertFalse("Learn more link should not be long clickable", learnMoreLink.isLongClickable)
+ learnMoreLink.callOnClick()
+ verify(fragment).dismiss()
+ verify(feature).onLearnMorePress(permissionRequestId, "sessionId")
+ }
+
+ @Test
+ fun `clicking on positive button notifies the feature (temporary)`() {
+ val mockFeature: SitePermissionsFeature = mock()
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mockFeature,
+ shouldShowDoNotAskAgainCheckBox = false,
+ shouldSelectDoNotAskAgainCheckBox = false,
+ ),
+ )
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.feature = mockFeature
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = dialog.findViewById<Button>(R.id.allow_button)
+ positiveButton.performClick()
+ verify(mockFeature).onPositiveButtonPress(permissionRequestId, "sessionId", false)
+ }
+
+ @Test
+ fun `dismissing the dialog notifies the feature`() {
+ val mockFeature: SitePermissionsFeature = mock()
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mockFeature,
+ shouldShowDoNotAskAgainCheckBox = false,
+ shouldSelectDoNotAskAgainCheckBox = false,
+ ),
+ )
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.feature = mockFeature
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ doReturn(mockFragmentManager()).`when`(fragment).parentFragmentManager
+
+ fragment.onDismiss(mock())
+
+ verify(mockFeature).onDismiss(permissionRequestId, "sessionId")
+ }
+
+ fun `dialog with passed in text for the negative button should use it`() {
+ val expectedText = "This is just a test"
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = false,
+ negativeButtonText = expectedText,
+ ),
+ )
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val negativeButton = dialog.findViewById<Button>(R.id.deny_button)
+ assertEquals(expectedText, negativeButton.text)
+ }
+
+ fun `dialog with a text for the negative button not passed has a default available`() {
+ val expectedText = testContext.getString(R.string.mozac_feature_sitepermissions_not_allow)
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = false,
+ ),
+ )
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val negativeButton = dialog.findViewById<Button>(R.id.deny_button)
+ assertEquals(expectedText, negativeButton.text)
+ }
+
+ @Test
+ fun `clicking on negative button notifies the feature (temporary)`() {
+ val mockFeature: SitePermissionsFeature = mock()
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mockFeature,
+ shouldShowDoNotAskAgainCheckBox = false,
+ shouldSelectDoNotAskAgainCheckBox = false,
+ ),
+ )
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.feature = mockFeature
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = dialog.findViewById<Button>(R.id.deny_button)
+ positiveButton.performClick()
+ verify(mockFeature)
+ .onNegativeButtonPress(permissionRequestId, "sessionId", false)
+ }
+
+ @Test
+ fun `clicking on positive button notifies the feature (permanent)`() {
+ val mockFeature: SitePermissionsFeature = mock()
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mockFeature,
+ shouldShowDoNotAskAgainCheckBox = false,
+ shouldSelectDoNotAskAgainCheckBox = true,
+ ),
+ )
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.feature = mockFeature
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = dialog.findViewById<Button>(R.id.allow_button)
+ positiveButton.performClick()
+ verify(mockFeature)
+ .onPositiveButtonPress(permissionRequestId, "sessionId", true)
+ }
+
+ @Test
+ fun `clicking on negative button notifies the feature (permanent)`() {
+ val mockFeature: SitePermissionsFeature = mock()
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mockFeature,
+ shouldShowDoNotAskAgainCheckBox = false,
+ shouldSelectDoNotAskAgainCheckBox = true,
+ ),
+ )
+ doNothing().`when`(fragment).dismiss()
+
+ fragment.feature = mockFeature
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = dialog.findViewById<Button>(R.id.deny_button)
+ positiveButton.performClick()
+ verify(mockFeature)
+ .onNegativeButtonPress(permissionRequestId, "sessionId", true)
+ }
+
+ @Test
+ fun `dialog must have all the styles of the feature promptsStyling object`() {
+ val mockFeature: SitePermissionsFeature = mock()
+
+ doReturn(PromptsStyling(TOP, true)).`when`(mockFeature).promptsStyling
+
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mockFeature,
+ shouldShowDoNotAskAgainCheckBox = false,
+ ),
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ val dialogAttributes = dialog.window!!.attributes
+
+ assertTrue(dialogAttributes.gravity == TOP)
+ assertTrue(dialogAttributes.width == ViewGroup.LayoutParams.MATCH_PARENT)
+ }
+
+ @Test
+ fun `dialog with isNotificationRequest equals true should not have a checkbox`() {
+ val fragment = spy(
+ SitePermissionsDialogFragment.newInstance(
+ "sessionId",
+ "title",
+ titleIcon,
+ permissionRequestId = permissionRequestId,
+ feature = mock(),
+ shouldShowDoNotAskAgainCheckBox = true,
+ shouldSelectDoNotAskAgainCheckBox = false,
+ isNotificationRequest = true,
+ ),
+ )
+
+ doReturn(testContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val checkBox = dialog.findViewById<CheckBox>(R.id.do_not_ask_again)
+
+ assertFalse("Checkbox shouldn't be displayed", checkBox.isVisible)
+ assertTrue("User selection property should be true", fragment.userSelectionCheckBox)
+ }
+
+ 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/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFactsTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFactsTest.kt
new file mode 100644
index 0000000000..f57911594a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFactsTest.kt
@@ -0,0 +1,116 @@
+/* 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.sitepermissions
+
+import mozilla.components.concept.engine.permission.Permission.AppCamera
+import mozilla.components.concept.engine.permission.Permission.AppLocationFine
+import mozilla.components.concept.engine.permission.Permission.ContentAudioCapture
+import mozilla.components.concept.engine.permission.Permission.ContentCrossOriginStorageAccess
+import mozilla.components.concept.engine.permission.Permission.ContentVideoCapture
+import mozilla.components.concept.engine.permission.Permission.Generic
+import mozilla.components.support.base.Component.FEATURE_SITEPERMISSIONS
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SitePermissionsFactsTest {
+
+ @Test
+ fun `GIVEN a fact for a prompt shown for one permission WHEN it is emitted THEN it is properly configured`() {
+ CollectionProcessor.withFactCollection { facts ->
+ emitPermissionDialogDisplayed(Generic("test", "test"))
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.DISPLAY, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals("Generic", facts[0].value)
+ }
+ }
+
+ @Test
+ fun `GIVEN a fact for a prompt shown for multiple permissions WHEN it is emitted THEN it is properly configured`() {
+ CollectionProcessor.withFactCollection { facts ->
+ emitPermissionsDialogDisplayed(
+ listOf(
+ AppCamera("test", "test"),
+ ContentCrossOriginStorageAccess("test", "test"),
+ AppCamera("test2", "test2"),
+ ),
+ )
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.DISPLAY, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals("AppCamera, ContentCrossOriginStorageAccess", facts[0].value)
+ }
+ }
+
+ @Test
+ fun `GIVEN a fact for a permission prompt being allowed WHEN it is emitted THEN it is properly configured`() {
+ CollectionProcessor.withFactCollection { facts ->
+ emitPermissionAllowed(ContentAudioCapture("test", "test"))
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.CONFIRM, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals("ContentAudioCapture", facts[0].value)
+ }
+ }
+
+ @Test
+ fun `GIVEN a fact for a multiple permission prompt being allowed WHEN it is emitted THEN it is properly configured`() {
+ CollectionProcessor.withFactCollection { facts ->
+ emitPermissionsAllowed(
+ listOf(
+ ContentAudioCapture("test", "test"),
+ ContentVideoCapture("test", "test"),
+ ContentVideoCapture("test2", "test2"),
+ ),
+ )
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.CONFIRM, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals("ContentAudioCapture, ContentVideoCapture", facts[0].value)
+ }
+ }
+
+ @Test
+ fun `GIVEN a fact for a permission prompt being blocked WHEN it is emitted THEN it is properly configured`() {
+ CollectionProcessor.withFactCollection { facts ->
+ emitPermissionDenied(AppLocationFine("test", "test"))
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.CANCEL, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals("AppLocationFine", facts[0].value)
+ }
+ }
+
+ @Test
+ fun `GIVEN a fact for a multiple permission prompt being blocked WHEN it is emitted THEN it is properly configured`() {
+ CollectionProcessor.withFactCollection { facts ->
+ emitPermissionsDenied(
+ listOf(
+ ContentAudioCapture("test", "test"),
+ ContentVideoCapture("test", "test"),
+ ContentAudioCapture("test2", "test2"),
+ ),
+ )
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.CANCEL, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals("ContentAudioCapture, ContentVideoCapture", facts[0].value)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFeatureTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFeatureTest.kt
new file mode 100644
index 0000000000..58672732ee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFeatureTest.kt
@@ -0,0 +1,1450 @@
+/* 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.sitepermissions
+
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayAudibleBlockingAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayAudibleChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayInAudibleBlockingAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayInAudibleChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.CameraChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.LocationChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.MediaKeySystemAccesChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.MicrophoneChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.NotificationChangedAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.PersistentStorageChangedAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.concept.engine.permission.Permission.AppAudio
+import mozilla.components.concept.engine.permission.Permission.ContentAudioCapture
+import mozilla.components.concept.engine.permission.Permission.ContentAudioMicrophone
+import mozilla.components.concept.engine.permission.Permission.ContentAutoPlayAudible
+import mozilla.components.concept.engine.permission.Permission.ContentAutoPlayInaudible
+import mozilla.components.concept.engine.permission.Permission.ContentCrossOriginStorageAccess
+import mozilla.components.concept.engine.permission.Permission.ContentGeoLocation
+import mozilla.components.concept.engine.permission.Permission.ContentMediaKeySystemAccess
+import mozilla.components.concept.engine.permission.Permission.ContentNotification
+import mozilla.components.concept.engine.permission.Permission.ContentPersistentStorage
+import mozilla.components.concept.engine.permission.Permission.ContentVideoCamera
+import mozilla.components.concept.engine.permission.Permission.ContentVideoCapture
+import mozilla.components.concept.engine.permission.Permission.Generic
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage
+import mozilla.components.feature.tabs.TabsUseCases.SelectOrAddUseCase
+import mozilla.components.support.base.Component.FEATURE_SITEPERMISSIONS
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.base.feature.OnNeedToRequestPermissions
+import mozilla.components.support.ktx.kotlin.stripDefaultPort
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+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.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.doNothing
+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 java.security.InvalidParameterException
+import java.util.UUID
+import mozilla.components.ui.icons.R as iconsR
+
+@RunWith(AndroidJUnit4::class)
+class SitePermissionsFeatureTest {
+ private lateinit var sitePermissionFeature: SitePermissionsFeature
+ private lateinit var mockOnNeedToRequestPermissions: OnNeedToRequestPermissions
+ private lateinit var mockStorage: SitePermissionsStorage
+ private lateinit var mockFragmentManager: FragmentManager
+ private lateinit var mockStore: BrowserStore
+ private lateinit var mockContentState: ContentState
+ private lateinit var mockPermissionRequest: PermissionRequest
+ private lateinit var mockAppPermissionRequest: PermissionRequest
+ private lateinit var mockSitePermissionRules: SitePermissionsRules
+ private lateinit var selectedTab: TabSessionState
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ companion object {
+ const val SESSION_ID = "testSessionId"
+ const val URL = "http://www.example.com"
+ const val PERMISSION_ID = "PERMISSION_ID"
+ }
+
+ @Before
+ fun setup() {
+ mockOnNeedToRequestPermissions = mock()
+ mockStorage = mock()
+ mockFragmentManager = mockFragmentManager()
+ mockContentState = mock()
+ mockPermissionRequest = mock()
+ doReturn(true).`when`(mockPermissionRequest).containsVideoAndAudioSources()
+ mockAppPermissionRequest = mock()
+ mockSitePermissionRules = mock()
+
+ selectedTab = mozilla.components.browser.state.state.createTab(
+ url = "https://www.mozilla.org",
+ id = SESSION_ID,
+ )
+ mockStore = spy(BrowserStore(initialState = BrowserState(tabs = listOf(selectedTab), selectedTabId = selectedTab.id)))
+ sitePermissionFeature = spy(
+ SitePermissionsFeature(
+ context = testContext,
+ sitePermissionsRules = mockSitePermissionRules,
+ onNeedToRequestPermissions = mockOnNeedToRequestPermissions,
+ storage = mockStorage,
+ fragmentManager = mockFragmentManager,
+ onShouldShowRequestPermissionRationale = { false },
+ store = mockStore,
+ sessionId = SESSION_ID,
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN a tab load THEN stale permission indicators should be clear up and temporary permissions`() {
+ sitePermissionFeature.start()
+
+ verify(sitePermissionFeature).setupLoadingCollector()
+
+ // when
+ mockStore.dispatch(ContentAction.UpdateLoadingStateAction(SESSION_ID, true)).joinBlocking()
+
+ // then
+ verify(mockStore).dispatch(
+ UpdatePermissionHighlightsStateAction.Reset(SESSION_ID),
+ )
+ verify(mockStorage).clearTemporaryPermissions()
+ }
+
+ @Test
+ fun `GIVEN a tab load after stop is called THEN none stale permission indicators should be clear up`() {
+ sitePermissionFeature.start()
+
+ verify(sitePermissionFeature).setupLoadingCollector()
+
+ sitePermissionFeature.stop()
+
+ // when
+ mockStore.dispatch(ContentAction.UpdateLoadingStateAction(SESSION_ID, true)).joinBlocking()
+
+ verify(mockStorage).clearTemporaryPermissions()
+
+ // then
+ verify(mockStore, never()).dispatch(
+ UpdatePermissionHighlightsStateAction.Reset(SESSION_ID),
+ )
+ }
+
+ @Test
+ fun `GIVEN a sessionId WHEN calling consumePermissionRequest THEN dispatch ConsumePermissionsRequest action with given id`() {
+ // when
+ sitePermissionFeature.consumePermissionRequest(mockPermissionRequest, "sessionIdTest")
+
+ // then
+ verify(mockStore).dispatch(
+ ContentAction.ConsumePermissionsRequest
+ ("sessionIdTest", mockPermissionRequest),
+ )
+ }
+
+ @Test
+ fun `GIVEN no sessionId WHEN calling consumePermissionRequest THEN dispatch ConsumePermissionsRequest action with selected tab`() {
+ // when
+ sitePermissionFeature.consumePermissionRequest(mockPermissionRequest)
+
+ // then
+ verify(mockStore).dispatch(
+ ContentAction.ConsumePermissionsRequest
+ (selectedTab.id, mockPermissionRequest),
+ )
+ }
+
+ @Test
+ fun `GIVEN a sessionId WHEN calling consumeAppPermissionRequest THEN dispatch ConsumeAppPermissionsRequest action with given id`() {
+ // when
+ sitePermissionFeature.consumeAppPermissionRequest(mockAppPermissionRequest, "sessionIdTest")
+
+ // then
+ verify(mockStore).dispatch(
+ ContentAction.ConsumeAppPermissionsRequest
+ ("sessionIdTest", mockAppPermissionRequest),
+ )
+ }
+
+ @Test
+ fun `GIVEN no sessionId WHEN calling consumeAppPermissionRequest THEN dispatch ConsumeAppPermissionsRequest action with selected tab`() {
+ // when
+ sitePermissionFeature.consumeAppPermissionRequest(mockAppPermissionRequest)
+
+ // then
+ verify(mockStore).dispatch(
+ ContentAction.ConsumeAppPermissionsRequest
+ (selectedTab.id, mockAppPermissionRequest),
+ )
+ }
+
+ @Test
+ fun `GIVEN an appPermissionRequest with all granted permissions WHEN onPermissionsResult() THEN grant() and consumeAppPermissionRequest() are called`() {
+ // given
+ doReturn(mockAppPermissionRequest).`when`(sitePermissionFeature)
+ .findRequestedAppPermission(any())
+
+ // when
+ sitePermissionFeature.onPermissionsResult(arrayOf("permission"), arrayOf(PERMISSION_GRANTED).toIntArray())
+
+ // then
+ verify(mockAppPermissionRequest).grant()
+ verify(sitePermissionFeature).consumeAppPermissionRequest(mockAppPermissionRequest)
+ }
+
+ @Test
+ fun `GIVEN an appPermissionRequest with blocked permissions and !onShouldShowRequestPermissionRationale WHEN onPermissionsResult() THEN reject(), storeSitePermissions, consume are called`() {
+ // given
+ doReturn(mockAppPermissionRequest).`when`(sitePermissionFeature)
+ .findRequestedAppPermission(any())
+ doNothing().`when`(sitePermissionFeature)
+ .storeSitePermissions(any(), any(), any(), any())
+
+ // when
+ sitePermissionFeature.onPermissionsResult(
+ arrayOf("permission1", "permission2"),
+ arrayOf(PERMISSION_DENIED, PERMISSION_DENIED).toIntArray(),
+ )
+
+ // then
+ verify(mockAppPermissionRequest).reject()
+ verify(sitePermissionFeature, times(2))
+ .storeSitePermissions(selectedTab.content, mockAppPermissionRequest, BLOCKED)
+ verify(sitePermissionFeature).consumeAppPermissionRequest(mockAppPermissionRequest)
+ }
+
+ @Test
+ fun `GIVEN shouldStore true WHEN onContentPermissionGranted() THEN storeSitePermissions() called`() {
+ // given
+ doNothing().`when`(sitePermissionFeature)
+ .storeSitePermissions(any(), any(), any(), any())
+
+ // when
+ sitePermissionFeature.onContentPermissionGranted(mockPermissionRequest, true)
+
+ // then
+ verify(sitePermissionFeature)
+ .storeSitePermissions(selectedTab.content, mockPermissionRequest, ALLOWED)
+ }
+
+ @Test
+ fun `GIVEN shouldStore false WHEN onContentPermissionGranted() THEN storeSitePermissions() MUST NOT BE called`() {
+ // given
+ doNothing().`when`(sitePermissionFeature)
+ .storeSitePermissions(any(), any(), any(), any())
+
+ // when
+ sitePermissionFeature.onContentPermissionGranted(mockPermissionRequest, false)
+
+ // then
+ verify(sitePermissionFeature, never())
+ .storeSitePermissions(selectedTab.content, mockPermissionRequest, ALLOWED)
+ }
+
+ @Test
+ fun `GIVEN permissionRequest WHEN onPositiveButtonPress() THEN consumePermissionRequest, onContentPermissionGranted are called`() {
+ // given
+ doNothing().`when`(sitePermissionFeature).consumePermissionRequest(any(), any())
+ doNothing().`when`(sitePermissionFeature)
+ .onContentPermissionGranted(mockPermissionRequest, true)
+ doReturn(mockPermissionRequest).`when`(sitePermissionFeature).findRequestedPermission(
+ anyString(),
+ )
+
+ // when
+ sitePermissionFeature.onPositiveButtonPress(PERMISSION_ID, SESSION_ID, true)
+
+ // then
+ verify(sitePermissionFeature)
+ .consumePermissionRequest(mockPermissionRequest, SESSION_ID)
+ verify(sitePermissionFeature)
+ .onContentPermissionGranted(mockPermissionRequest, true)
+ }
+
+ @Test
+ fun `GIVEN a permission prompt WHEN one permission is allowed THEN emit a fact`() {
+ CollectionProcessor.withFactCollection { facts ->
+ doNothing().`when`(sitePermissionFeature).consumePermissionRequest(any(), any())
+ doNothing().`when`(sitePermissionFeature)
+ .onContentPermissionGranted(mockPermissionRequest, true)
+ doReturn(listOf(ContentCrossOriginStorageAccess())).`when`(mockPermissionRequest).permissions
+ doReturn(mockPermissionRequest).`when`(sitePermissionFeature).findRequestedPermission(anyString())
+
+ sitePermissionFeature.onPositiveButtonPress(PERMISSION_ID, SESSION_ID, true)
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.CONFIRM, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals(ContentCrossOriginStorageAccess().id, facts[0].value)
+ }
+ }
+
+ @Test
+ fun `GIVEN a permission prompt WHEN multiple permission are allowed THEN emit a fact`() {
+ CollectionProcessor.withFactCollection { facts ->
+ doReturn(
+ listOf(ContentVideoCapture(), ContentVideoCamera(), ContentAudioMicrophone()),
+ ).`when`(mockPermissionRequest).permissions
+ doReturn(true).`when`(mockPermissionRequest).containsVideoAndAudioSources()
+ doReturn(mockPermissionRequest).`when`(sitePermissionFeature).findRequestedPermission(anyString())
+
+ sitePermissionFeature.onPositiveButtonPress(PERMISSION_ID, SESSION_ID, true)
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.CONFIRM, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals(
+ listOf(ContentVideoCapture(), ContentVideoCamera(), ContentAudioMicrophone()).joinToString { it.id!! },
+ facts[0].value,
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN permissionRequest WHEN onNegativeButtonPress() THEN consumePermissionRequest, onContentPermissionDeny are called`() {
+ // given
+ doNothing().`when`(sitePermissionFeature).consumePermissionRequest(any(), any())
+ doNothing().`when`(sitePermissionFeature)
+ .onContentPermissionDeny(mockPermissionRequest, true)
+ doReturn(mockPermissionRequest).`when`(sitePermissionFeature).findRequestedPermission(
+ anyString(),
+ )
+
+ // when
+ sitePermissionFeature.onNegativeButtonPress(PERMISSION_ID, SESSION_ID, true)
+
+ // then
+ verify(sitePermissionFeature)
+ .consumePermissionRequest(mockPermissionRequest, SESSION_ID)
+ verify(sitePermissionFeature)
+ .onContentPermissionDeny(mockPermissionRequest, true)
+ }
+
+ @Test
+ fun `GIVEN a permission prompt WHEN the permission is denied THEN emit a fact`() {
+ CollectionProcessor.withFactCollection { facts ->
+ doNothing().`when`(sitePermissionFeature).consumePermissionRequest(any(), any())
+ doNothing().`when`(sitePermissionFeature)
+ .onContentPermissionDeny(mockPermissionRequest, true)
+ doReturn(listOf(ContentGeoLocation())).`when`(mockPermissionRequest).permissions
+ doReturn(mockPermissionRequest).`when`(sitePermissionFeature).findRequestedPermission(anyString())
+
+ sitePermissionFeature.onNegativeButtonPress(PERMISSION_ID, SESSION_ID, true)
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.CANCEL, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals(ContentGeoLocation().id, facts[0].value)
+ }
+ }
+
+ @Test
+ fun `GIVEN a permission prompt WHEN multiple permissions are denied THEN emit a fact`() {
+ CollectionProcessor.withFactCollection { facts ->
+ doReturn(
+ listOf(ContentVideoCapture(), ContentVideoCamera(), ContentAudioMicrophone()),
+ ).`when`(mockPermissionRequest).permissions
+ doReturn(true).`when`(mockPermissionRequest).containsVideoAndAudioSources()
+ doReturn(mockPermissionRequest).`when`(sitePermissionFeature).findRequestedPermission(anyString())
+
+ sitePermissionFeature.onNegativeButtonPress(PERMISSION_ID, SESSION_ID, true)
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.CANCEL, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals(
+ listOf(ContentVideoCapture(), ContentVideoCamera(), ContentAudioMicrophone()).joinToString { it.id!! },
+ facts[0].value,
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN permissionRequest WHEN onDismiss() THEN consumePermissionRequest, onContentPermissionDeny are called`() {
+ // given
+ doNothing().`when`(sitePermissionFeature).consumePermissionRequest(any(), any())
+ doNothing().`when`(sitePermissionFeature)
+ .onContentPermissionDeny(mockPermissionRequest, false)
+ doReturn(mockPermissionRequest).`when`(sitePermissionFeature).findRequestedPermission(
+ anyString(),
+ )
+
+ // when
+ sitePermissionFeature.onDismiss(PERMISSION_ID, SESSION_ID)
+
+ // then
+ verify(sitePermissionFeature)
+ .consumePermissionRequest(mockPermissionRequest, SESSION_ID)
+ verify(sitePermissionFeature)
+ .onContentPermissionDeny(mockPermissionRequest, false)
+ }
+
+ @Test
+ fun `GIVEN permissionRequest WHEN onLearnMorePress() THEN consumePermissionRequest, onContentPermissionDeny are called and SelectOrAddUseCase is used`() {
+ // given
+ val permission: ContentCrossOriginStorageAccess = mock()
+ val permissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(permission))
+ }
+ doNothing().`when`(sitePermissionFeature).consumePermissionRequest(any(), any())
+ doNothing().`when`(sitePermissionFeature)
+ .onContentPermissionDeny(mockPermissionRequest, true)
+ doReturn(permissionRequest).`when`(sitePermissionFeature).findRequestedPermission(
+ anyString(),
+ )
+ doReturn(mock<SelectOrAddUseCase>()).`when`(sitePermissionFeature).selectOrAddUseCase
+
+ // when
+ sitePermissionFeature.onLearnMorePress(PERMISSION_ID, SESSION_ID)
+
+ // then
+ verify(sitePermissionFeature)
+ .consumePermissionRequest(permissionRequest, SESSION_ID)
+ verify(sitePermissionFeature)
+ .onContentPermissionDeny(permissionRequest, false)
+ verify(sitePermissionFeature.selectOrAddUseCase).invoke(
+ url = STORAGE_ACCESS_DOCUMENTATION_URL,
+ private = false,
+ source = SessionState.Source.Internal.TextSelection,
+ )
+ }
+
+ @Test
+ fun `GIVEN a new permissionRequest WHEN storeSitePermissions() THEN save(permissionRequest) is called`() = runTestOnMain {
+ // given
+ val sitePermissions = SitePermissions(origin = "origin", savedAt = 0)
+ doReturn(null).`when`(mockStorage)
+ .findSitePermissionsBy(ArgumentMatchers.anyString(), anyBoolean(), anyBoolean())
+ doNothing().`when`(sitePermissionFeature).updatePermissionToolbarIndicator(
+ any(),
+ any(),
+ anyBoolean(),
+ )
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(Permission.AppAudio(id = "permission")))
+ whenever(uri).thenReturn(URL)
+ }
+ doReturn(sitePermissions).`when`(sitePermissionFeature)
+ .updateSitePermissionsStatus(any(), any(), any())
+
+ // when
+ sitePermissionFeature.storeSitePermissions(
+ mockContentState,
+ mockPermissionRequest,
+ ALLOWED,
+ scope,
+ )
+
+ // then
+ verify(mockStorage).save(sitePermissions, mockPermissionRequest, private = selectedTab.content.private)
+ verify(sitePermissionFeature).updatePermissionToolbarIndicator(
+ mockPermissionRequest,
+ ALLOWED,
+ true,
+ )
+ }
+
+ @Test
+ fun `GIVEN an already saved permissionRequest WHEN storeSitePermissions() THEN update(permissionRequest) is called`() = runTestOnMain {
+ // given
+ val sitePermissions = SitePermissions(origin = "origin", savedAt = 0)
+ doReturn(sitePermissions).`when`(mockStorage)
+ .findSitePermissionsBy(ArgumentMatchers.anyString(), anyBoolean(), anyBoolean())
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(uri).thenReturn(URL)
+ whenever(permissions).thenReturn(listOf(AppAudio(id = "permission")))
+ }
+ doReturn(sitePermissions).`when`(sitePermissionFeature)
+ .updateSitePermissionsStatus(any(), any(), any())
+
+ // when
+ sitePermissionFeature.storeSitePermissions(
+ mockContentState,
+ mockPermissionRequest,
+ ALLOWED,
+ scope,
+ )
+
+ // then
+ verify(mockStorage).update(eq(sitePermissions), anyBoolean())
+ }
+
+ @Test
+ fun `GIVEN a permissionRequest WITH a private tab WHEN storeSitePermissions() THEN save or update MUST NOT BE called`() = runTestOnMain {
+ // then
+ sitePermissionFeature.storeSitePermissions(
+ selectedTab.content.copy(private = true),
+ mockPermissionRequest,
+ ALLOWED,
+ scope,
+ )
+
+ // when
+ verify(mockStorage, never()).save(any(), any(), anyBoolean())
+ verify(mockStorage, never()).update(any(), anyBoolean())
+ }
+
+ @Test
+ fun `GIVEN a permanent permissionRequest WHEN onContentPermissionDeny THEN reject(), storeSitePermissions are called`() {
+ // given
+ doNothing().`when`(mockPermissionRequest).reject()
+ doNothing().`when`(sitePermissionFeature).storeSitePermissions(any(), any(), any(), any())
+
+ // then
+ sitePermissionFeature.onContentPermissionDeny(mockPermissionRequest, true)
+
+ // when
+ verify(mockPermissionRequest).reject()
+ verify(sitePermissionFeature).storeSitePermissions(
+ selectedTab.content,
+ mockPermissionRequest,
+ BLOCKED,
+ )
+ }
+
+ @Test
+ fun `GIVEN a temporary permissionRequest WHEN denying THEN store it on memory`() {
+ doNothing().`when`(mockPermissionRequest).reject()
+ doNothing().`when`(sitePermissionFeature).storeSitePermissions(any(), any(), any(), any())
+
+ sitePermissionFeature.onContentPermissionDeny(mockPermissionRequest, shouldStore = false)
+
+ verify(mockPermissionRequest).reject()
+ verify(mockStorage).saveTemporary(mockPermissionRequest)
+
+ verify(sitePermissionFeature, never()).storeSitePermissions(
+ selectedTab.content,
+ mockPermissionRequest,
+ BLOCKED,
+ )
+ }
+
+ @Test
+ fun `GIVEN a temporary permissionRequest WHEN granting THEN store it on memory`() {
+ doNothing().`when`(mockPermissionRequest).reject()
+ doNothing().`when`(sitePermissionFeature).storeSitePermissions(any(), any(), any(), any())
+
+ sitePermissionFeature.onContentPermissionGranted(mockPermissionRequest, shouldStore = false)
+
+ verify(mockPermissionRequest).grant()
+ verify(mockStorage).saveTemporary(mockPermissionRequest)
+
+ verify(sitePermissionFeature, never()).storeSitePermissions(
+ selectedTab.content,
+ mockPermissionRequest,
+ ALLOWED,
+ )
+ }
+
+ @Test
+ fun `GIVEN a media permissionRequest without all media permissions granted WHEN onContentPermissionRequested() THEN reject, consumePermissionRequest are called `() {
+ // given
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentVideoCamera(id = "permission")))
+ }
+ doNothing().`when`(mockPermissionRequest).reject()
+
+ // when
+ runTestOnMain {
+ sitePermissionFeature.onContentPermissionRequested(mockPermissionRequest, URL)
+ }
+
+ // then
+ verify(mockPermissionRequest).reject()
+ verify(sitePermissionFeature).consumePermissionRequest(mockPermissionRequest)
+ }
+
+ @Test
+ fun `GIVEN sessionId which does not match a selected or custom tab WHEN onContentPermissionRequested() THEN reject, consumePermissionRequest are called `() {
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentVideoCamera(id = "permission")))
+ }
+
+ doNothing().`when`(mockPermissionRequest).reject()
+
+ sitePermissionFeature.sessionId = null
+
+ runTestOnMain {
+ sitePermissionFeature.onContentPermissionRequested(mockPermissionRequest, URL)
+ }
+
+ verify(mockPermissionRequest).reject()
+ verify(sitePermissionFeature).consumePermissionRequest(mockPermissionRequest)
+ }
+
+ @Test
+ fun `GIVEN location permissionRequest and shouldApplyRules is true WHEN onContentPermissionRequested() THEN handleRuledFlow is called`() = runTestOnMain {
+ // given
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
+ }
+ val sitePermissions = SitePermissions(origin = "origin", savedAt = 0)
+ val mockedSitePermissionsDialogFragment = SitePermissionsDialogFragment()
+ doReturn(sitePermissions).`when`(mockStorage).findSitePermissionsBy(URL, private = selectedTab.content.private)
+ doReturn(true).`when`(sitePermissionFeature).shouldApplyRules(any())
+ doReturn(mockedSitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .handleRuledFlow(mockPermissionRequest, URL)
+
+ // when
+ runTestOnMain {
+ sitePermissionFeature.onContentPermissionRequested(
+ mockPermissionRequest,
+ URL,
+ scope,
+ )
+ }
+
+ // then
+ verify(mockStorage).findSitePermissionsBy(URL, private = selectedTab.content.private)
+ verify(sitePermissionFeature).handleRuledFlow(mockPermissionRequest, URL)
+ }
+
+ @Test
+ fun `GIVEN location permissionRequest and shouldApplyRules is false WHEN onContentPermissionRequested() THEN handleNoRuledFlow is called`() = runTestOnMain {
+ // given
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
+ }
+ val sitePermissions = SitePermissions(origin = "origin", savedAt = 0)
+ val sitePermissionsDialogFragment = SitePermissionsDialogFragment()
+ doReturn(sitePermissions).`when`(mockStorage)
+ .findSitePermissionsBy(URL, private = selectedTab.content.private)
+ doReturn(false).`when`(sitePermissionFeature).shouldApplyRules(any())
+ doReturn(sitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .handleNoRuledFlow(sitePermissions, mockPermissionRequest, URL)
+
+ // when
+ runTestOnMain {
+ sitePermissionFeature.onContentPermissionRequested(
+ mockPermissionRequest,
+ URL,
+ scope,
+ )
+ }
+
+ // then
+ verify(mockStorage).findSitePermissionsBy(URL, private = selectedTab.content.private)
+ verify(sitePermissionFeature).handleNoRuledFlow(sitePermissions, mockPermissionRequest, URL)
+ }
+
+ @Test
+ fun `GIVEN autoplay permissionRequest and shouldApplyRules is false WHEN onContentPermissionRequested() THEN handleNoRuledFlow is called`() = runTestOnMain {
+ // given
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentAutoPlayInaudible(id = "permission")))
+ }
+ val sitePermissions = SitePermissions(origin = "origin", savedAt = 0)
+ val sitePermissionsDialogFragment = SitePermissionsDialogFragment()
+
+ doReturn(sitePermissions).`when`(mockStorage).findSitePermissionsBy(URL, private = selectedTab.content.private)
+ doReturn(false).`when`(sitePermissionFeature).shouldApplyRules(any())
+ doReturn(sitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .handleNoRuledFlow(sitePermissions, mockPermissionRequest, URL)
+
+ // when
+ runTestOnMain {
+ sitePermissionFeature.onContentPermissionRequested(
+ mockPermissionRequest,
+ URL,
+ scope,
+ )
+ }
+
+ // then
+ verify(mockStorage).findSitePermissionsBy(URL, private = selectedTab.content.private)
+ verify(sitePermissionFeature).handleNoRuledFlow(sitePermissions, mockPermissionRequest, URL)
+ }
+
+ @Test
+ fun `GIVEN shouldShowPrompt with isForAutoplay false AND null permissionFromStorage THEN return true`() = runTestOnMain {
+ // given
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(Permission.ContentGeoLocation(id = "permission")))
+ }
+
+ // when
+ val shouldShowPrompt = sitePermissionFeature.shouldShowPrompt(mockPermissionRequest, null)
+
+ // then
+ assertTrue(shouldShowPrompt)
+ }
+
+ @Test
+ fun `GIVEN shouldShowPrompt with isForAutoplay true THEN return false`() {
+ // given
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(Permission.ContentAutoPlayInaudible(id = "permission")))
+ }
+
+ // when
+ val shouldShowPrompt = sitePermissionFeature.shouldShowPrompt(mockPermissionRequest, mock())
+
+ // then
+ assertFalse(shouldShowPrompt)
+ }
+
+ @Test
+ fun `GIVEN shouldShowPrompt true WHEN handleNoRuledFlow THEN createPrompt is called`() {
+ // given
+ val sitePermissionsDialogFragment = SitePermissionsDialogFragment()
+ val sitePermissions = SitePermissions(origin = "origin", savedAt = 0)
+ doReturn(true).`when`(sitePermissionFeature)
+ .shouldShowPrompt(mockPermissionRequest, sitePermissions)
+ doReturn(sitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .createPrompt(any(), any())
+
+ // when
+ sitePermissionFeature.handleNoRuledFlow(sitePermissions, mockPermissionRequest, URL)
+
+ // then
+ verify(sitePermissionFeature).createPrompt(mockPermissionRequest, URL)
+ }
+
+ @Test
+ fun `GIVEN shouldShowPrompt false and permissionFromStorage not granted WHEN handleNoRuledFlow THEN reject, consumePermissionRequest are called `() {
+ // given
+ doReturn(false).`when`(sitePermissionFeature).shouldShowPrompt(mockPermissionRequest, null)
+ doNothing().`when`(sitePermissionFeature).updatePermissionToolbarIndicator(mockPermissionRequest, BLOCKED)
+
+ // when
+ sitePermissionFeature.handleNoRuledFlow(null, mockPermissionRequest, URL)
+
+ // then
+ verify(mockPermissionRequest).reject()
+ verify(sitePermissionFeature).consumePermissionRequest(mockPermissionRequest)
+ verify(sitePermissionFeature).updatePermissionToolbarIndicator(
+ mockPermissionRequest,
+ BLOCKED,
+ )
+ }
+
+ @Test
+ fun `GIVEN AutoplayAudible request WHEN calling updatePermissionToolbarIndicator THEN dispatch AutoPlayAudibleBlockingAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentAutoPlayAudible(id = "permission")))
+ }
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED)
+
+ verify(mockStore).dispatch(AutoPlayAudibleBlockingAction(tab1.id, true))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED)
+
+ verify(mockStore).dispatch(AutoPlayAudibleBlockingAction(tab1.id, false))
+ }
+
+ @Test
+ fun `GIVEN AutoplayInaudible request WHEN calling updatePermissionToolbarIndicator THEN dispatch AutoPlayInAudibleBlockingAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentAutoPlayInaudible(id = "permission")))
+ }
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED)
+
+ verify(mockStore).dispatch(AutoPlayInAudibleBlockingAction(tab1.id, true))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED)
+
+ verify(mockStore).dispatch(AutoPlayInAudibleBlockingAction(tab1.id, false))
+ }
+
+ @Test
+ fun `GIVEN notification request WHEN calling updatePermissionToolbarIndicator THEN dispatch NotificationChangedAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentNotification(id = "permission")))
+ }
+
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+ doReturn(SitePermissionsRules.Action.BLOCKED).`when`(mockSitePermissionRules).notification
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, false)
+
+ verify(mockStore, never()).dispatch(any<NotificationChangedAction>())
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, true)
+
+ verify(mockStore).dispatch(NotificationChangedAction(tab1.id, false))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED, true)
+
+ verify(mockStore).dispatch(NotificationChangedAction(tab1.id, true))
+ }
+
+ @Test
+ fun `GIVEN camera request WHEN calling updatePermissionToolbarIndicator THEN dispatch CameraChangedAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentVideoCamera(id = "permission")))
+ }
+
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+ doReturn(SitePermissionsRules.Action.BLOCKED).`when`(mockSitePermissionRules).camera
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, false)
+
+ verify(mockStore, never()).dispatch(any<CameraChangedAction>())
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, true)
+
+ verify(mockStore).dispatch(CameraChangedAction(tab1.id, false))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED, true)
+
+ verify(mockStore).dispatch(CameraChangedAction(tab1.id, true))
+ }
+
+ @Test
+ fun `GIVEN location request WHEN calling updatePermissionToolbarIndicator THEN dispatch CameraChangedAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
+ }
+
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+ doReturn(SitePermissionsRules.Action.BLOCKED).`when`(mockSitePermissionRules).location
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, false)
+
+ verify(mockStore, never()).dispatch(any<LocationChangedAction>())
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, true)
+
+ verify(mockStore).dispatch(LocationChangedAction(tab1.id, false))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED, true)
+
+ verify(mockStore).dispatch(LocationChangedAction(tab1.id, true))
+ }
+
+ @Test
+ fun `GIVEN microphone request WHEN calling updatePermissionToolbarIndicator THEN dispatch MicrophoneChangedAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentAudioCapture(id = "permission")))
+ }
+
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+ doReturn(SitePermissionsRules.Action.BLOCKED).`when`(mockSitePermissionRules).microphone
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, false)
+
+ verify(mockStore, never()).dispatch(any<MicrophoneChangedAction>())
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, true)
+
+ verify(mockStore).dispatch(MicrophoneChangedAction(tab1.id, false))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED, true)
+
+ verify(mockStore).dispatch(MicrophoneChangedAction(tab1.id, true))
+ }
+
+ @Test
+ fun `GIVEN persistentStorage request WHEN calling updatePermissionToolbarIndicator THEN dispatch PersistentStorageChangedAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentPersistentStorage(id = "permission")))
+ }
+
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+ doReturn(SitePermissionsRules.Action.BLOCKED).`when`(mockSitePermissionRules).persistentStorage
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, false)
+
+ verify(mockStore, never()).dispatch(any<PersistentStorageChangedAction>())
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, true)
+
+ verify(mockStore).dispatch(PersistentStorageChangedAction(tab1.id, false))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED, true)
+
+ verify(mockStore).dispatch(PersistentStorageChangedAction(tab1.id, true))
+ }
+
+ @Test
+ fun `GIVEN mediaKeySystemAccess request WHEN calling updatePermissionToolbarIndicator THEN dispatch MediaKeySystemAccesChangedAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentMediaKeySystemAccess(id = "permission")))
+ }
+
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+ doReturn(SitePermissionsRules.Action.BLOCKED).`when`(mockSitePermissionRules).mediaKeySystemAccess
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, false)
+
+ verify(mockStore, never()).dispatch(any<MediaKeySystemAccesChangedAction>())
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, true)
+
+ verify(mockStore).dispatch(MediaKeySystemAccesChangedAction(tab1.id, false))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED, true)
+
+ verify(mockStore).dispatch(MediaKeySystemAccesChangedAction(tab1.id, true))
+ }
+
+ @Test
+ fun `GIVEN autoplayAudible request WHEN calling updatePermissionToolbarIndicator THEN dispatch AutoPlayAudibleChangedAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentAutoPlayAudible(id = "permission")))
+ }
+
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+ doReturn(SitePermissionsRules.AutoplayAction.BLOCKED).`when`(mockSitePermissionRules).autoplayAudible
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, true)
+
+ verify(mockStore).dispatch(AutoPlayAudibleChangedAction(tab1.id, false))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED, true)
+
+ verify(mockStore).dispatch(AutoPlayAudibleChangedAction(tab1.id, true))
+ }
+
+ @Test
+ fun `GIVEN autoplayInaudible request WHEN calling updatePermissionToolbarIndicator THEN dispatch AutoPlayInAudibleChangedAction`() {
+ val tab1 = createTab("https://www.mozilla.org", id = "1")
+ val request: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentAutoPlayInaudible(id = "permission")))
+ }
+
+ doReturn(tab1).`when`(sitePermissionFeature).getCurrentTabState()
+ doReturn(SitePermissionsRules.AutoplayAction.BLOCKED).`when`(mockSitePermissionRules).autoplayInaudible
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, BLOCKED, true)
+
+ verify(mockStore).dispatch(AutoPlayInAudibleChangedAction(tab1.id, false))
+
+ sitePermissionFeature.updatePermissionToolbarIndicator(request, ALLOWED, true)
+
+ verify(mockStore).dispatch(AutoPlayInAudibleChangedAction(tab1.id, true))
+ }
+
+ @Test
+ fun `GIVEN shouldShowPrompt false and permissionFromStorage granted WHEN handleNoRuledFlow THEN grant, consumePermissionRequest are called `() {
+ // given
+ val sitePermissions = SitePermissions(origin = "origin", savedAt = 0, location = ALLOWED)
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
+ }
+ doNothing().`when`(sitePermissionFeature)
+ .updatePermissionToolbarIndicator(mockPermissionRequest, ALLOWED, true)
+
+ doReturn(false).`when`(sitePermissionFeature)
+ .shouldShowPrompt(mockPermissionRequest, sitePermissions)
+
+ // when
+ sitePermissionFeature.handleNoRuledFlow(sitePermissions, mockPermissionRequest, URL)
+
+ // then
+ verify(mockPermissionRequest, atLeastOnce()).grant()
+ verify(sitePermissionFeature).consumePermissionRequest(mockPermissionRequest)
+ verify(sitePermissionFeature).updatePermissionToolbarIndicator(mockPermissionRequest, ALLOWED, true)
+ }
+
+ @Test
+ fun `GIVEN permissionRequest with isForAutoplay true and BLOCKED WHEN handleRuledFlow THEN reject, consumePermissionRequest are called`() {
+ // given
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentAutoPlayAudible(id = "permission")))
+ }
+ doNothing().`when`(sitePermissionFeature)
+ .updatePermissionToolbarIndicator(mockPermissionRequest, BLOCKED)
+ doReturn(mockSitePermissionRules).`when`(sitePermissionFeature).sitePermissionsRules
+ doReturn(SitePermissionsRules.Action.BLOCKED).`when`(mockSitePermissionRules)
+ .getActionFrom(mockPermissionRequest)
+
+ // when
+ sitePermissionFeature.handleRuledFlow(mockPermissionRequest, URL)
+
+ // then
+ verify(mockPermissionRequest).reject()
+ verify(sitePermissionFeature).consumePermissionRequest(mockPermissionRequest)
+ verify(sitePermissionFeature).updatePermissionToolbarIndicator(
+ mockPermissionRequest,
+ BLOCKED,
+ )
+ }
+
+ @Test
+ fun `GIVEN permissionRequest with isForAutoplay false and ASK_TO_ALLOW WHEN handleRuledFlow THEN createPrompt is called`() {
+ // given
+ val sitePermissionsDialogFragment = SitePermissionsDialogFragment()
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
+ }
+ doReturn(mockSitePermissionRules).`when`(sitePermissionFeature).sitePermissionsRules
+ doReturn(SitePermissionsRules.Action.ASK_TO_ALLOW).`when`(mockSitePermissionRules)
+ .getActionFrom(mockPermissionRequest)
+ doReturn(sitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .createPrompt(mockPermissionRequest, URL)
+
+ // when
+ sitePermissionFeature.handleRuledFlow(mockPermissionRequest, URL)
+
+ // then
+ verify(sitePermissionFeature).createPrompt(mockPermissionRequest, URL)
+ }
+
+ @Test
+ fun `GIVEN permissionRequest and containsVideoAndAudioSources false WHEN createPrompt THEN handlingSingleContentPermissions is called`() {
+ // given
+ val sitePermissionsDialogFragment = SitePermissionsDialogFragment()
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
+ }
+ doReturn(sitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .handlingSingleContentPermissions(any(), any(), any())
+
+ // when
+ sitePermissionFeature.createPrompt(mockPermissionRequest, URL)
+
+ // then
+ verify(sitePermissionFeature).handlingSingleContentPermissions(
+ mockPermissionRequest,
+ ContentGeoLocation(id = "permission"),
+ URL,
+ )
+ }
+
+ @Test
+ fun `GIVEN a ContentStorageAccess request WHEN handlingSingleContentPermissions is called THEN create a specific prompt`() {
+ // given
+ val host = "https://mozilla.org"
+ val permission = ContentCrossOriginStorageAccess(id = "permission")
+ val permissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(permission))
+ whenever(id).thenReturn("id")
+ }
+
+ // when
+ sitePermissionFeature.handlingSingleContentPermissions(permissionRequest, permission, host)
+
+ // then
+ verify(sitePermissionFeature).createContentCrossOriginStorageAccessPermissionPrompt(
+ context = testContext,
+ host,
+ permissionRequest,
+ false,
+ true,
+ )
+ }
+
+ @Test
+ fun `GIVEN a ContentStorageAccess request WHEN createContentStorageAccessPermissionPrompt is called THEN create a specific SitePermissionsDialogFragment`() {
+ // given
+ val host = "https://mozilla.org"
+ val permission = ContentCrossOriginStorageAccess(id = "permission")
+ val permissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(permission))
+ whenever(id).thenReturn("id")
+ }
+
+ // when
+ val dialog = sitePermissionFeature.createContentCrossOriginStorageAccessPermissionPrompt(
+ testContext,
+ host,
+ permissionRequest,
+ false,
+ true,
+ )
+
+ // then
+ assertEquals(SESSION_ID, dialog.sessionId)
+ assertEquals(
+ testContext.getString(
+ R.string.mozac_feature_sitepermissions_storage_access_title,
+ host.stripDefaultPort(),
+ selectedTab.content.url.stripDefaultPort(),
+ ),
+ dialog.title,
+ )
+ assertEquals(iconsR.drawable.mozac_ic_cookies_24, dialog.icon)
+ assertEquals(permissionRequest.id, dialog.permissionRequestId)
+ assertEquals(sitePermissionFeature, dialog.feature)
+ assertEquals(false, dialog.shouldShowDoNotAskAgainCheckBox)
+ assertEquals(true, dialog.shouldPreselectDoNotAskAgainCheckBox)
+ assertEquals(false, dialog.isNotificationRequest)
+ assertEquals(
+ testContext.getString(
+ R.string.mozac_feature_sitepermissions_storage_access_message,
+ host.stripDefaultPort(),
+ ),
+ dialog.message,
+ )
+ assertEquals(
+ testContext.getString(R.string.mozac_feature_sitepermissions_storage_access_not_allow),
+ dialog.negativeButtonText,
+ )
+ assertEquals(true, dialog.shouldShowLearnMoreLink)
+ }
+
+ @Test
+ fun `GIVEN permissionRequest and containsVideoAndAudioSources true WHEN createPrompt THEN createSinglePermissionPrompt is called`() {
+ // given
+ val permissionRequest: PermissionRequest = object : PermissionRequest {
+ override val uri: String?
+ get() = "http://www.mozilla.org"
+ override val id: String
+ get() = PERMISSION_ID
+
+ override val permissions: List<Permission>
+ get() = listOf(
+ ContentVideoCapture("", "back camera"),
+ ContentVideoCamera("", "front camera"),
+ ContentAudioMicrophone(),
+ )
+
+ override fun grant(permissions: List<Permission>) {
+ }
+
+ override fun containsVideoAndAudioSources() = true
+
+ override fun reject() = Unit
+ }
+ val sitePermissionsDialogFragment = SitePermissionsDialogFragment()
+ doReturn(sitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .createSinglePermissionPrompt(
+ any(),
+ ArgumentMatchers.anyString(),
+ any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ )
+
+ // when
+ sitePermissionFeature.createPrompt(permissionRequest, URL)
+
+ // then
+ verify(sitePermissionFeature).createSinglePermissionPrompt(
+ any(),
+ ArgumentMatchers.anyString(),
+ any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ )
+ }
+
+ @Test
+ fun `GIVEN a request for one permission WHEN a prompt is created THEN emit a fact`() {
+ CollectionProcessor.withFactCollection { facts ->
+ val sitePermissionsDialogFragment = SitePermissionsDialogFragment()
+ val mockPermissionRequest: PermissionRequest = mock {
+ whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
+ }
+ doReturn(sitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .handlingSingleContentPermissions(any(), any(), any())
+
+ sitePermissionFeature.createPrompt(mockPermissionRequest, URL)
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.DISPLAY, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals("ContentGeoLocation", facts[0].value)
+ }
+ }
+
+ @Test
+ fun `GIVEN a request for a permission with video and audio sources WHEN a prompt is created THEN emit a fact`() {
+ CollectionProcessor.withFactCollection { facts ->
+ val permissionRequest: PermissionRequest = object : PermissionRequest {
+ override val uri = "http://www.mozilla.org"
+ override val id = PERMISSION_ID
+
+ override val permissions: List<Permission>
+ get() = listOf(ContentVideoCapture(), ContentVideoCamera(), ContentAudioMicrophone())
+
+ override fun grant(permissions: List<Permission>) {}
+
+ override fun containsVideoAndAudioSources() = true
+
+ override fun reject() = Unit
+ }
+ val sitePermissionsDialogFragment = SitePermissionsDialogFragment()
+ doReturn(sitePermissionsDialogFragment).`when`(sitePermissionFeature)
+ .createSinglePermissionPrompt(
+ any(),
+ ArgumentMatchers.anyString(),
+ any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ )
+
+ sitePermissionFeature.createPrompt(permissionRequest, URL)
+
+ assertEquals(1, facts.size)
+ assertEquals(FEATURE_SITEPERMISSIONS, facts[0].component)
+ assertEquals(Action.DISPLAY, facts[0].action)
+ assertEquals(SitePermissionsFacts.Items.PERMISSIONS, facts[0].item)
+ assertEquals(permissionRequest.permissions.joinToString { it.id!! }, facts[0].value)
+ }
+ }
+
+ @Test
+ fun `is SitePermission granted in the storage`() = runTestOnMain {
+ val sitePermissionsList = listOf(
+ ContentGeoLocation(),
+ ContentNotification(),
+ ContentAudioCapture(),
+ ContentAudioMicrophone(),
+ ContentVideoCamera(),
+ ContentVideoCapture(),
+ ContentPersistentStorage(),
+ ContentAutoPlayAudible(),
+ ContentAutoPlayInaudible(),
+ ContentMediaKeySystemAccess(),
+ )
+
+ sitePermissionsList.forEach { permission ->
+ val request: PermissionRequest = mock()
+ val sitePermissionFromStorage: SitePermissions = mock()
+
+ doReturn(listOf(permission)).`when`(request).permissions
+ doReturn(sitePermissionFromStorage).`when`(mockStorage)
+ .findSitePermissionsBy(anyString(), anyBoolean(), anyBoolean())
+ doReturn(ALLOWED).`when`(sitePermissionFromStorage).location
+ doReturn(ALLOWED).`when`(sitePermissionFromStorage).notification
+ doReturn(ALLOWED).`when`(sitePermissionFromStorage).camera
+ doReturn(ALLOWED).`when`(sitePermissionFromStorage).microphone
+ doReturn(ALLOWED).`when`(sitePermissionFromStorage).localStorage
+ doReturn(ALLOWED).`when`(sitePermissionFromStorage).crossOriginStorageAccess
+ doReturn(ALLOWED).`when`(sitePermissionFromStorage).mediaKeySystemAccess
+ doReturn(AutoplayStatus.ALLOWED).`when`(sitePermissionFromStorage).autoplayAudible
+ doReturn(AutoplayStatus.ALLOWED).`when`(sitePermissionFromStorage).autoplayInaudible
+
+ val isAllowed = sitePermissionFromStorage.isGranted(request)
+ assertTrue(isAllowed)
+ }
+ }
+
+ @Test
+ fun `is SitePermission blocked in the storage`() = runTestOnMain {
+ val sitePermissionsList = listOf(
+ ContentGeoLocation(),
+ ContentNotification(),
+ ContentAudioCapture(),
+ ContentAudioMicrophone(),
+ ContentVideoCamera(),
+ ContentVideoCapture(),
+ ContentCrossOriginStorageAccess(),
+ Generic(),
+ )
+
+ var exceptionThrown = false
+ sitePermissionsList.forEach { permission ->
+ val request: PermissionRequest = mock()
+ val sitePermissionFromStorage: SitePermissions = mock()
+
+ doReturn(listOf(permission)).`when`(request).permissions
+ doReturn(sitePermissionFromStorage).`when`(mockStorage)
+ .findSitePermissionsBy(anyString(), anyBoolean(), anyBoolean())
+ doReturn(BLOCKED).`when`(sitePermissionFromStorage).location
+ doReturn(BLOCKED).`when`(sitePermissionFromStorage).notification
+ doReturn(BLOCKED).`when`(sitePermissionFromStorage).camera
+ doReturn(BLOCKED).`when`(sitePermissionFromStorage).microphone
+ doReturn(BLOCKED).`when`(sitePermissionFromStorage).crossOriginStorageAccess
+
+ try {
+ val isAllowed = sitePermissionFromStorage.isGranted(request)
+ assertFalse(isAllowed)
+ } catch (e: InvalidParameterException) {
+ exceptionThrown = true
+ }
+ }
+ assertTrue(exceptionThrown)
+ }
+
+ @Test
+ fun `feature will re-attach to already existing fragment`() {
+ doReturn(false).`when`(sitePermissionFeature).noPermissionRequests(any())
+
+ val fragment: SitePermissionsDialogFragment = mock()
+ doReturn(selectedTab.id).`when`(fragment).sessionId
+ doReturn(fragment).`when`(mockFragmentManager).findFragmentByTag(any())
+
+ sitePermissionFeature.start()
+ verify(fragment).feature = sitePermissionFeature
+ }
+
+ @Test
+ fun `already existing fragment will be removed if session has none permissions request set anymore`() {
+ // given
+ val session = selectedTab
+ val fragment: SitePermissionsDialogFragment = mock()
+ doReturn(session.id).`when`(fragment).sessionId
+ val transaction: FragmentTransaction = mock()
+ doReturn(fragment).`when`(mockFragmentManager).findFragmentByTag(any())
+ doReturn(transaction).`when`(mockFragmentManager).beginTransaction()
+ doReturn(transaction).`when`(transaction).remove(fragment)
+ doReturn(mockContentState).`when`(sitePermissionFeature)
+ .getCurrentContentState()
+ doNothing().`when`(sitePermissionFeature).setupPermissionRequestsCollector()
+ doNothing().`when`(sitePermissionFeature).setupAppPermissionRequestsCollector()
+
+ // when
+ sitePermissionFeature.start()
+
+ // then
+ verify(mockFragmentManager).beginTransaction()
+ verify(transaction).remove(fragment)
+ }
+
+ @Test
+ fun `already existing fragment will be removed if session does not exist anymore`() {
+ val fragment: SitePermissionsDialogFragment = mock()
+ doReturn(UUID.randomUUID().toString()).`when`(fragment).sessionId
+ doReturn(mockContentState).`when`(sitePermissionFeature)
+ .getCurrentContentState()
+ doNothing().`when`(sitePermissionFeature).setupPermissionRequestsCollector()
+ doNothing().`when`(sitePermissionFeature).setupAppPermissionRequestsCollector()
+
+ val transaction: FragmentTransaction = mock()
+
+ doReturn(fragment).`when`(mockFragmentManager).findFragmentByTag(any())
+ doReturn(transaction).`when`(mockFragmentManager).beginTransaction()
+ doReturn(transaction).`when`(transaction).remove(fragment)
+
+ sitePermissionFeature.start()
+ verify(mockFragmentManager).beginTransaction()
+ verify(transaction).remove(fragment)
+ }
+
+ @Test
+ fun `getInitialSitePermissions - WHEN sitePermissionsRules is present the function MUST use the sitePermissionsRules values to create a SitePermissions object`() = runTestOnMain {
+ val rules = SitePermissionsRules(
+ location = SitePermissionsRules.Action.BLOCKED,
+ camera = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ notification = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ microphone = SitePermissionsRules.Action.BLOCKED,
+ autoplayAudible = SitePermissionsRules.AutoplayAction.BLOCKED,
+ autoplayInaudible = SitePermissionsRules.AutoplayAction.ALLOWED,
+ persistentStorage = SitePermissionsRules.Action.BLOCKED,
+ crossOriginStorageAccess = SitePermissionsRules.Action.ALLOWED,
+ mediaKeySystemAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ )
+
+ sitePermissionFeature.sitePermissionsRules = rules
+
+ val sitePermissions = sitePermissionFeature.getInitialSitePermissions(URL)
+
+ assertEquals(URL, sitePermissions.origin)
+ assertEquals(BLOCKED, sitePermissions.location)
+ assertEquals(NO_DECISION, sitePermissions.camera)
+ assertEquals(NO_DECISION, sitePermissions.notification)
+ assertEquals(BLOCKED, sitePermissions.microphone)
+ assertEquals(BLOCKED, sitePermissions.autoplayAudible.toStatus())
+ assertEquals(ALLOWED, sitePermissions.autoplayInaudible.toStatus())
+ assertEquals(BLOCKED, sitePermissions.localStorage)
+ assertEquals(ALLOWED, sitePermissions.crossOriginStorageAccess)
+ assertEquals(NO_DECISION, sitePermissions.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `any media request must be rejected WHEN system permissions are not granted first`() = runTestOnMain {
+ val permissions = listOf(
+ ContentVideoCapture("", "back camera"),
+ ContentVideoCamera("", "front camera"),
+ ContentAudioCapture(),
+ ContentAudioMicrophone(),
+ )
+
+ permissions.forEach { permission ->
+ var grantWasCalled = false
+
+ val permissionRequest: PermissionRequest = object : PermissionRequest {
+ override val uri: String?
+ get() = "http://www.mozilla.org"
+ override val id: String
+ get() = PERMISSION_ID
+ override val permissions: List<Permission>
+ get() = listOf(permission)
+
+ override fun grant(permissions: List<Permission>) {
+ grantWasCalled = true
+ }
+
+ override fun reject() = Unit
+ }
+
+ mockStorage = mock()
+
+ val prompt = sitePermissionFeature
+ .onContentPermissionRequested(permissionRequest, URL)
+ assertNull(prompt)
+ assertFalse(grantWasCalled)
+ }
+
+ Unit
+ }
+
+ 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/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsRulesTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsRulesTest.kt
new file mode 100644
index 0000000000..8ccd9a8a7e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsRulesTest.kt
@@ -0,0 +1,199 @@
+/* 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.sitepermissions
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.permission.Permission
+import mozilla.components.concept.engine.permission.Permission.ContentAudioCapture
+import mozilla.components.concept.engine.permission.Permission.ContentGeoLocation
+import mozilla.components.concept.engine.permission.Permission.ContentNotification
+import mozilla.components.concept.engine.permission.Permission.ContentVideoCapture
+import mozilla.components.concept.engine.permission.Permission.Generic
+import mozilla.components.concept.engine.permission.PermissionRequest
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status
+import mozilla.components.concept.engine.permission.SitePermissionsStorage
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ALLOWED
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ASK_TO_ALLOW
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
+import mozilla.components.support.base.feature.OnNeedToRequestPermissions
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+
+@RunWith(AndroidJUnit4::class)
+class SitePermissionsRulesTest {
+
+ private lateinit var anchorView: View
+ private lateinit var rules: SitePermissionsFeature
+ private lateinit var mockOnNeedToRequestPermissions: OnNeedToRequestPermissions
+ private lateinit var mockStorage: SitePermissionsStorage
+
+ @Before
+ fun setup() {
+ anchorView = View(testContext)
+ mockOnNeedToRequestPermissions = mock()
+ mockStorage = mock()
+
+ rules = SitePermissionsFeature(
+ context = testContext,
+ onNeedToRequestPermissions = mockOnNeedToRequestPermissions,
+ storage = mockStorage,
+ fragmentManager = mock(),
+ onShouldShowRequestPermissionRationale = mock(),
+ store = BrowserStore(),
+ )
+ }
+
+ @Test
+ fun `getActionFrom must return the right action per permission`() {
+ val rules = SitePermissionsRules(
+ camera = ASK_TO_ALLOW,
+ location = BLOCKED,
+ notification = ASK_TO_ALLOW,
+ microphone = BLOCKED,
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.ALLOWED,
+ persistentStorage = BLOCKED,
+ crossOriginStorageAccess = ALLOWED,
+ mediaKeySystemAccess = ASK_TO_ALLOW,
+ )
+
+ val mockRequest: PermissionRequest = mock()
+
+ doReturn(listOf(ContentGeoLocation())).`when`(mockRequest).permissions
+ var action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.location)
+
+ doReturn(listOf(ContentNotification())).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.notification)
+
+ doReturn(listOf(ContentAudioCapture())).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.microphone)
+
+ doReturn(listOf(ContentVideoCapture())).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.camera)
+
+ doReturn(listOf(Permission.ContentAutoPlayAudible())).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.autoplayAudible.toAction())
+
+ doReturn(listOf(Permission.ContentAutoPlayInaudible())).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.autoplayInaudible.toAction())
+
+ doReturn(listOf(Generic("", ""))).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.camera)
+
+ doReturn(listOf(Permission.ContentPersistentStorage())).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.persistentStorage)
+
+ doReturn(listOf(Permission.ContentCrossOriginStorageAccess())).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.crossOriginStorageAccess)
+
+ doReturn(listOf(Permission.ContentMediaKeySystemAccess())).`when`(mockRequest).permissions
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, rules.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `getActionFrom must return the right action for a Camera + Microphone permission`() {
+ var rules = SitePermissionsRules(
+ camera = ASK_TO_ALLOW,
+ location = BLOCKED,
+ crossOriginStorageAccess = ALLOWED,
+ persistentStorage = BLOCKED,
+ notification = ASK_TO_ALLOW,
+ microphone = BLOCKED,
+ autoplayInaudible = AutoplayAction.ALLOWED,
+ autoplayAudible = AutoplayAction.BLOCKED,
+ mediaKeySystemAccess = ASK_TO_ALLOW,
+ )
+
+ val mockRequest: PermissionRequest = mock()
+ doReturn(true).`when`(mockRequest).containsVideoAndAudioSources()
+
+ var action = rules.getActionFrom(mockRequest)
+ assertEquals(action, BLOCKED)
+
+ rules = SitePermissionsRules(
+ camera = ASK_TO_ALLOW,
+ location = BLOCKED,
+ crossOriginStorageAccess = ALLOWED,
+ notification = ASK_TO_ALLOW,
+ microphone = ASK_TO_ALLOW,
+ autoplayInaudible = AutoplayAction.ALLOWED,
+ autoplayAudible = AutoplayAction.BLOCKED,
+ persistentStorage = BLOCKED,
+ mediaKeySystemAccess = ASK_TO_ALLOW,
+ )
+
+ action = rules.getActionFrom(mockRequest)
+ assertEquals(action, ASK_TO_ALLOW)
+ }
+
+ @Test
+ fun `toSitePermissions - converts a SitePermissionsRules to SitePermissions`() {
+ val expectedSitePermission = SitePermissions(
+ origin = "origin",
+ camera = Status.NO_DECISION,
+ location = Status.BLOCKED,
+ localStorage = Status.BLOCKED,
+ crossOriginStorageAccess = Status.ALLOWED,
+ notification = Status.NO_DECISION,
+ microphone = Status.BLOCKED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ mediaKeySystemAccess = Status.BLOCKED,
+ savedAt = 1L,
+ )
+
+ val rules = SitePermissionsRules(
+ camera = ASK_TO_ALLOW,
+ location = BLOCKED,
+ notification = ASK_TO_ALLOW,
+ microphone = BLOCKED,
+ autoplayInaudible = AutoplayAction.ALLOWED,
+ autoplayAudible = AutoplayAction.BLOCKED,
+ persistentStorage = BLOCKED,
+ crossOriginStorageAccess = ALLOWED,
+ mediaKeySystemAccess = BLOCKED,
+ )
+
+ val convertedSitePermissions = rules.toSitePermissions(origin = "origin", savedAt = 1L)
+
+ assertEquals(expectedSitePermission.origin, convertedSitePermissions.origin)
+ assertEquals(expectedSitePermission.camera, convertedSitePermissions.camera)
+ assertEquals(expectedSitePermission.location, convertedSitePermissions.location)
+ assertEquals(expectedSitePermission.notification, convertedSitePermissions.notification)
+ assertEquals(expectedSitePermission.microphone, convertedSitePermissions.microphone)
+ assertEquals(expectedSitePermission.autoplayInaudible, convertedSitePermissions.autoplayInaudible)
+ assertEquals(expectedSitePermission.autoplayAudible, convertedSitePermissions.autoplayAudible)
+ assertEquals(expectedSitePermission.localStorage, convertedSitePermissions.localStorage)
+ assertEquals(expectedSitePermission.crossOriginStorageAccess, convertedSitePermissions.crossOriginStorageAccess)
+ assertEquals(expectedSitePermission.mediaKeySystemAccess, convertedSitePermissions.mediaKeySystemAccess)
+ assertEquals(expectedSitePermission.savedAt, convertedSitePermissions.savedAt)
+ }
+
+ @Test
+ fun `AutoplayAction - toAutoplayStatus`() {
+ assertEquals(AutoplayStatus.ALLOWED, AutoplayAction.ALLOWED.toAutoplayStatus())
+ assertEquals(AutoplayStatus.BLOCKED, AutoplayAction.BLOCKED.toAutoplayStatus())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsTest.kt
new file mode 100644
index 0000000000..20ef50cf53
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsTest.kt
@@ -0,0 +1,88 @@
+/* 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.sitepermissions
+
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.AUTOPLAY_AUDIBLE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.AUTOPLAY_INAUDIBLE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.BLUETOOTH
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.CAMERA
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.LOCAL_STORAGE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.LOCATION
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.MICROPHONE
+import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.NOTIFICATION
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SitePermissionsTest {
+ @Test
+ fun `Tests get() against direct property access`() {
+ var sitePermissions = SitePermissions(origin = "mozilla.dev", savedAt = 0)
+
+ assertEquals(NO_DECISION, sitePermissions[NOTIFICATION])
+ assertEquals(NO_DECISION, sitePermissions[LOCATION])
+ assertEquals(NO_DECISION, sitePermissions[LOCAL_STORAGE])
+ assertEquals(NO_DECISION, sitePermissions[MICROPHONE])
+ assertEquals(NO_DECISION, sitePermissions[BLUETOOTH])
+ assertEquals(NO_DECISION, sitePermissions[CAMERA])
+ assertEquals(BLOCKED, sitePermissions[AUTOPLAY_AUDIBLE])
+ assertEquals(ALLOWED, sitePermissions[AUTOPLAY_INAUDIBLE])
+
+ sitePermissions = sitePermissions.copy(
+ location = ALLOWED,
+ notification = BLOCKED,
+ microphone = NO_DECISION,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.BLOCKED,
+ )
+
+ assertEquals(BLOCKED, sitePermissions[NOTIFICATION])
+ assertEquals(ALLOWED, sitePermissions[LOCATION])
+ assertEquals(NO_DECISION, sitePermissions[MICROPHONE])
+ assertEquals(NO_DECISION, sitePermissions[BLUETOOTH])
+ assertEquals(NO_DECISION, sitePermissions[CAMERA])
+ assertEquals(NO_DECISION, sitePermissions[LOCAL_STORAGE])
+ assertEquals(ALLOWED, sitePermissions[AUTOPLAY_AUDIBLE])
+ assertEquals(BLOCKED, sitePermissions[AUTOPLAY_INAUDIBLE])
+ }
+
+ @Test
+ fun `AutoplayStatus - toStatus`() {
+ var sitePermissions = SitePermissions(
+ origin = "mozilla.dev",
+ autoplayInaudible = AutoplayStatus.BLOCKED,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ savedAt = 0,
+ )
+
+ assertEquals(BLOCKED, sitePermissions.autoplayAudible.toStatus())
+ assertEquals(BLOCKED, sitePermissions.autoplayInaudible.toStatus())
+
+ sitePermissions = sitePermissions.copy(
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ )
+
+ assertEquals(ALLOWED, sitePermissions.autoplayAudible.toStatus())
+ assertEquals(ALLOWED, sitePermissions.autoplayInaudible.toStatus())
+ }
+
+ @Test
+ fun `Status to AutoplayStatus`() {
+ assertEquals(AutoplayStatus.BLOCKED, BLOCKED.toAutoplayStatus())
+ assertEquals(AutoplayStatus.ALLOWED, ALLOWED.toAutoplayStatus())
+ assertEquals(AutoplayStatus.BLOCKED, NO_DECISION.toAutoplayStatus())
+ }
+
+ @Test
+ fun `AutoplayStatus ids are aligned with Status`() {
+ assertEquals(AutoplayStatus.BLOCKED.id, AutoplayStatus.BLOCKED.id)
+ assertEquals(AutoplayStatus.ALLOWED, AutoplayStatus.ALLOWED)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/db/SitePermissionEntityTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/db/SitePermissionEntityTest.kt
new file mode 100644
index 0000000000..5a3af2de33
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/db/SitePermissionEntityTest.kt
@@ -0,0 +1,86 @@
+/* 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.sitepermissions.db
+
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SitePermissionEntityTest {
+
+ @Test
+ fun `convert from db entity to domain class`() {
+ val dbEntity = SitePermissionsEntity(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = BLOCKED,
+ location = BLOCKED,
+ notification = NO_DECISION,
+ microphone = NO_DECISION,
+ camera = NO_DECISION,
+ bluetooth = ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ mediaKeySystemAccess = NO_DECISION,
+ savedAt = 0,
+ )
+
+ val domainClass = dbEntity.toSitePermission()
+
+ with(dbEntity) {
+ assertEquals(origin, domainClass.origin)
+ assertEquals(localStorage, domainClass.localStorage)
+ assertEquals(crossOriginStorageAccess, domainClass.crossOriginStorageAccess)
+ assertEquals(location, domainClass.location)
+ assertEquals(notification, domainClass.notification)
+ assertEquals(microphone, domainClass.microphone)
+ assertEquals(camera, domainClass.camera)
+ assertEquals(bluetooth, domainClass.bluetooth)
+ assertEquals(autoplayAudible, domainClass.autoplayAudible)
+ assertEquals(autoplayInaudible, domainClass.autoplayInaudible)
+ assertEquals(mediaKeySystemAccess, domainClass.mediaKeySystemAccess)
+ assertEquals(savedAt, domainClass.savedAt)
+ }
+ }
+
+ @Test
+ fun `convert from domain class to db entity`() {
+ val domainClass = SitePermissions(
+ origin = "mozilla.dev",
+ localStorage = ALLOWED,
+ crossOriginStorageAccess = BLOCKED,
+ location = BLOCKED,
+ notification = NO_DECISION,
+ microphone = NO_DECISION,
+ camera = NO_DECISION,
+ bluetooth = ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ mediaKeySystemAccess = NO_DECISION,
+ savedAt = 0,
+ )
+
+ val dbEntity = domainClass.toSitePermissionsEntity()
+
+ with(dbEntity) {
+ assertEquals(origin, domainClass.origin)
+ assertEquals(localStorage, domainClass.localStorage)
+ assertEquals(crossOriginStorageAccess, domainClass.crossOriginStorageAccess)
+ assertEquals(location, domainClass.location)
+ assertEquals(notification, domainClass.notification)
+ assertEquals(microphone, domainClass.microphone)
+ assertEquals(camera, domainClass.camera)
+ assertEquals(bluetooth, domainClass.bluetooth)
+ assertEquals(autoplayAudible, domainClass.autoplayAudible)
+ assertEquals(autoplayInaudible, domainClass.autoplayInaudible)
+ assertEquals(mediaKeySystemAccess, domainClass.mediaKeySystemAccess)
+ assertEquals(savedAt, domainClass.savedAt)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/db/StatusConverterTest.kt b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/db/StatusConverterTest.kt
new file mode 100644
index 0000000000..ba52476153
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/db/StatusConverterTest.kt
@@ -0,0 +1,72 @@
+/* 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.sitepermissions.db
+
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
+import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
+import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class StatusConverterTest {
+
+ @Test
+ fun `convert from int to status`() {
+ val converter = StatusConverter()
+
+ var status = converter.toStatus(BLOCKED.id)
+ assertEquals(status, BLOCKED)
+
+ status = converter.toStatus(NO_DECISION.id)
+ assertEquals(status, NO_DECISION)
+
+ status = converter.toStatus(ALLOWED.id)
+ assertEquals(status, ALLOWED)
+
+ status = converter.toStatus(Int.MAX_VALUE)
+ assertNull(status)
+ }
+
+ @Test
+ fun `convert from status to int`() {
+ val converter = StatusConverter()
+
+ var index = converter.toInt(ALLOWED)
+ assertEquals(index, ALLOWED.id)
+
+ index = converter.toInt(BLOCKED)
+ assertEquals(index, BLOCKED.id)
+
+ index = converter.toInt(NO_DECISION)
+ assertEquals(index, NO_DECISION.id)
+ }
+
+ @Test
+ fun `convert from int to autoplay status`() {
+ val converter = StatusConverter()
+
+ var status = converter.toAutoplayStatus(AutoplayStatus.BLOCKED.id)
+ assertEquals(status, AutoplayStatus.BLOCKED)
+
+ status = converter.toAutoplayStatus(AutoplayStatus.ALLOWED.id)
+ assertEquals(status, AutoplayStatus.ALLOWED)
+
+ status = converter.toAutoplayStatus(Int.MAX_VALUE)
+ assertEquals(AutoplayStatus.BLOCKED, status)
+ }
+
+ @Test
+ fun `convert from autoplay status to int`() {
+ val converter = StatusConverter()
+
+ var index = converter.toInt(AutoplayStatus.ALLOWED)
+ assertEquals(index, AutoplayStatus.ALLOWED.id)
+
+ index = converter.toInt(AutoplayStatus.BLOCKED)
+ assertEquals(index, AutoplayStatus.BLOCKED.id)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/sitepermissions/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/sitepermissions/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/sitepermissions/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/sitepermissions/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/syncedtabs/README.md b/mobile/android/android-components/components/feature/syncedtabs/README.md
new file mode 100644
index 0000000000..433b7a96de
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/README.md
@@ -0,0 +1,36 @@
+# [Android Components](../../../README.md) > Feature > Synced Tabs
+
+Feature component for viewing tabs from other devices with a registered Fx Account.
+
+## Usage
+
+### 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-syncedtabs:{latest-version}"
+```
+
+## Usage
+
+In order to make use of the synced tab feature here, it's required to have an an FxA Account setup and Sync enabled.
+See the [service-firefox-accounts](../../service/firefox-accounts/README.md) for more information how to set this up.
+
+```kotlin
+ // The feature will start listening to local tabs changes.
+ val syncedTabsFeature = SyncedTabsFeature(
+ accountManager = accountManager,
+ store = browserStore,
+ tabsStorage = tabsStorage
+ )
+ // Grab the list of opened tabs on other devices.
+ val otherDevicesTabs: Map<Device, List<Tab>> = syncedTabsFeature.getSyncedTabs()
+
+```
+
+## 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/syncedtabs/build.gradle b/mobile/android/android-components/components/feature/syncedtabs/build.gradle
new file mode 100644
index 0000000000..7934e6270a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/build.gradle
@@ -0,0 +1,64 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+/* 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/.
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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 {
+ exclude 'META-INF/proguard/androidx-annotations.pro'
+ }
+
+ namespace 'mozilla.components.feature.sendtab'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':service-firefox-accounts')
+ implementation project(':browser-icons')
+ implementation project(':browser-state')
+ implementation project(':browser-storage-sync')
+ implementation project(':concept-awesomebar')
+ implementation project(':concept-engine')
+ implementation project(':concept-toolbar')
+ implementation project(':feature-session')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.androidx_work_runtime
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ 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/syncedtabs/proguard-rules.pro b/mobile/android/android-components/components/feature/syncedtabs/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/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/syncedtabs/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/syncedtabs/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/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/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/ClientTabPair.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/ClientTabPair.kt
new file mode 100644
index 0000000000..afcc2c570b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/ClientTabPair.kt
@@ -0,0 +1,17 @@
+/* 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.syncedtabs
+
+import mozilla.components.browser.storage.sync.TabEntry
+import mozilla.components.concept.sync.DeviceType
+
+/**
+ * Mapping of a device and the active [TabEntry] for each synced tab.
+ */
+internal data class ClientTabPair(
+ val clientName: String,
+ val tab: TabEntry,
+ val lastUsed: Long,
+ val deviceType: DeviceType,
+)
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsAutocompleteProvider.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsAutocompleteProvider.kt
new file mode 100644
index 0000000000..71ee683f59
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsAutocompleteProvider.kt
@@ -0,0 +1,51 @@
+/* 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.syncedtabs
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.concept.toolbar.AutocompleteResult
+import mozilla.components.feature.syncedtabs.ext.getActiveDeviceTabs
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+import mozilla.components.support.utils.doesUrlStartsWithText
+import mozilla.components.support.utils.segmentAwareDomainMatch
+
+@VisibleForTesting
+internal const val SYNCED_TABS_AUTOCOMPLETE_SOURCE_NAME = "syncedTabs"
+
+/**
+ * Provide autocomplete suggestions from synced tabs.
+ *
+ * @param syncedTabs [SyncedTabsStorage] containing the information about the available synced tabs.
+ * @param autocompletePriority Order in which this provider will be queried for autocomplete suggestions
+ * in relation ot others.
+ * - a lower priority means that this provider must be called before others with a higher priority.
+ * - an equal priority offers no ordering guarantees.
+ *
+ * Defaults to `0`.
+ */
+class SyncedTabsAutocompleteProvider(
+ private val syncedTabs: SyncedTabsStorage,
+ override val autocompletePriority: Int = 0,
+) : AutocompleteProvider {
+ override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
+ val tabUrl = syncedTabs
+ .getActiveDeviceTabs { doesUrlStartsWithText(it.url, query) }
+ .firstOrNull()
+ ?.tab?.url
+ ?: return null
+
+ val resultText = segmentAwareDomainMatch(query, arrayListOf(tabUrl))
+ return resultText?.let {
+ AutocompleteResult(
+ input = query,
+ text = it.matchedSegment,
+ url = it.url,
+ source = SYNCED_TABS_AUTOCOMPLETE_SOURCE_NAME,
+ totalItems = 1,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsFeature.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsFeature.kt
new file mode 100644
index 0000000000..02354bc51c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsFeature.kt
@@ -0,0 +1,74 @@
+/* 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.syncedtabs
+
+import android.content.Context
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.Dispatchers
+import mozilla.components.browser.storage.sync.Tab
+import mozilla.components.feature.syncedtabs.controller.DefaultController
+import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
+import mozilla.components.feature.syncedtabs.interactor.DefaultInteractor
+import mozilla.components.feature.syncedtabs.interactor.SyncedTabsInteractor
+import mozilla.components.feature.syncedtabs.presenter.DefaultPresenter
+import mozilla.components.feature.syncedtabs.presenter.SyncedTabsPresenter
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Feature implementation that will keep a [SyncedTabsView] notified with other synced device tabs for
+ * the Firefox Sync account.
+ *
+ * @param storage The synced tabs storage that stores the current device's and remote device tabs.
+ * @param accountManager Firefox Account Manager that holds a Firefox Sync account.
+ * @param view An implementor of [SyncedTabsView] that will be notified of changes.
+ * @param lifecycleOwner Android Lifecycle Owner to bind observers onto.
+ * @param coroutineContext A coroutine context that can be used to perform work off the main thread.
+ * @param onTabClicked Invoked when a tab is selected by the user on the [SyncedTabsView].
+ * @param controller See [SyncedTabsController].
+ * @param presenter See [SyncedTabsPresenter].
+ * @param interactor See [SyncedTabsInteractor].
+ */
+class SyncedTabsFeature(
+ context: Context,
+ storage: SyncedTabsStorage,
+ accountManager: FxaAccountManager,
+ view: SyncedTabsView,
+ lifecycleOwner: LifecycleOwner,
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+ onTabClicked: (Tab) -> Unit,
+ private val controller: SyncedTabsController = DefaultController(
+ storage,
+ accountManager,
+ view,
+ coroutineContext,
+ ),
+ private val presenter: SyncedTabsPresenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ ),
+ private val interactor: SyncedTabsInteractor = DefaultInteractor(
+ controller,
+ view,
+ onTabClicked,
+ ),
+) : LifecycleAwareFeature {
+
+ override fun start() {
+ presenter.start()
+ interactor.start()
+ }
+
+ override fun stop() {
+ presenter.stop()
+ interactor.stop()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProvider.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProvider.kt
new file mode 100644
index 0000000000..b4fffdfdc2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProvider.kt
@@ -0,0 +1,92 @@
+/* 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.syncedtabs
+
+import android.graphics.drawable.Drawable
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.concept.awesomebar.AwesomeBar.Suggestion.Flag
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.syncedtabs.ext.getActiveDeviceTabs
+import mozilla.components.feature.syncedtabs.facts.emitSyncedTabSuggestionClickedFact
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+import java.util.UUID
+
+/**
+ * A [AwesomeBar.SuggestionProvider] implementation that provides suggestions for remote tabs
+ * based on [SyncedTabsStorage].
+ */
+class SyncedTabsStorageSuggestionProvider(
+ private val syncedTabs: SyncedTabsStorage,
+ private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ private val icons: BrowserIcons? = null,
+ private val deviceIndicators: DeviceIndicators = DeviceIndicators(),
+ private val suggestionsHeader: String? = null,
+ @get:VisibleForTesting val resultsUrlFilter: ((String) -> Boolean)? = null,
+) : AwesomeBar.SuggestionProvider {
+ override val id: String = UUID.randomUUID().toString()
+
+ override fun groupTitle(): String? {
+ return suggestionsHeader
+ }
+
+ override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
+ if (text.isEmpty()) {
+ return emptyList()
+ }
+
+ val results = syncedTabs.getActiveDeviceTabs { tab ->
+ // This is a fairly naive match implementation, but this is what we do on Desktop 🤷.
+ (tab.url.contains(text, ignoreCase = true) || tab.title.contains(text, ignoreCase = true)) &&
+ resultsUrlFilter?.invoke(tab.url) != false
+ }
+
+ return results.sortedByDescending { it.lastUsed }.into()
+ }
+
+ /**
+ * Expects list of BookmarkNode to be specifically of bookmarks (e.g. nodes with a url).
+ */
+ private suspend fun List<ClientTabPair>.into(): List<AwesomeBar.Suggestion> {
+ val iconRequests = this.map { client ->
+ client.tab.iconUrl?.let { iconUrl ->
+ icons?.loadIcon(
+ IconRequest(url = iconUrl, waitOnNetworkLoad = false),
+ )
+ }
+ }
+
+ return this.zip(iconRequests) { result, icon ->
+ AwesomeBar.Suggestion(
+ provider = this@SyncedTabsStorageSuggestionProvider,
+ icon = icon?.await()?.bitmap,
+ indicatorIcon = when (result.deviceType) {
+ DeviceType.DESKTOP -> deviceIndicators.desktop
+ DeviceType.MOBILE -> deviceIndicators.mobile
+ DeviceType.TABLET -> deviceIndicators.tablet
+ else -> null
+ },
+ flags = setOf(Flag.SYNC_TAB),
+ title = result.tab.title,
+ description = result.clientName,
+ onSuggestionClicked = {
+ loadUrlUseCase.invoke(result.tab.url)
+ emitSyncedTabSuggestionClickedFact()
+ },
+ )
+ }
+ }
+}
+
+/**
+ * AwesomeBar suggestion indicators data class for desktop, mobile, tablet device types.
+ */
+data class DeviceIndicators(
+ val desktop: Drawable? = null,
+ val mobile: Drawable? = null,
+ val tablet: Drawable? = null,
+)
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/DefaultController.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/DefaultController.kt
new file mode 100644
index 0000000000..c59fece6e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/DefaultController.kt
@@ -0,0 +1,68 @@
+/* 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.syncedtabs.controller
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsProvider
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.ext.withConstellation
+import mozilla.components.service.fxa.sync.SyncReason
+import kotlin.coroutines.CoroutineContext
+
+internal class DefaultController(
+ override val provider: SyncedTabsProvider,
+ override val accountManager: FxaAccountManager,
+ override val view: SyncedTabsView,
+ coroutineContext: CoroutineContext,
+) : SyncedTabsController {
+
+ private val scope = CoroutineScope(coroutineContext)
+
+ /**
+ * See [SyncedTabsController.refreshSyncedTabs]
+ */
+ override fun refreshSyncedTabs() {
+ scope.launch {
+ accountManager.withConstellation {
+ val syncedDeviceTabs = provider.getSyncedDeviceTabs()
+ val otherDevices = state()?.otherDevices
+
+ scope.launch(Dispatchers.Main) {
+ if (syncedDeviceTabs.isEmpty() && otherDevices?.isEmpty() == true) {
+ view.onError(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)
+ } else if (syncedDeviceTabs.all { it.tabs.isEmpty() }) {
+ view.onError(ErrorType.NO_TABS_AVAILABLE)
+ } else {
+ view.displaySyncedTabs(syncedDeviceTabs)
+ }
+ }
+ }
+
+ scope.launch(Dispatchers.Main) {
+ view.stopLoading()
+ }
+ }
+ }
+
+ /**
+ * See [SyncedTabsController.syncAccount]
+ */
+ override fun syncAccount() {
+ view.startLoading()
+ scope.launch {
+ accountManager.withConstellation { refreshDevices() }
+ accountManager.syncNow(
+ SyncReason.User,
+ customEngineSubset = listOf(SyncEngine.Tabs),
+ debounce = true,
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/SyncedTabsController.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/SyncedTabsController.kt
new file mode 100644
index 0000000000..cc10d36fa4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/SyncedTabsController.kt
@@ -0,0 +1,30 @@
+/* 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.syncedtabs.controller
+
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsProvider
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.service.fxa.manager.FxaAccountManager
+
+/**
+ * A controller for making the appropriate request for remote tabs from [SyncedTabsProvider] when the
+ * [FxaAccountManager] account is in the appropriate state. The [SyncedTabsView] can then be notified.
+ */
+interface SyncedTabsController {
+ val provider: SyncedTabsProvider
+ val accountManager: FxaAccountManager
+ val view: SyncedTabsView
+
+ /**
+ * Requests for remote tabs and notifies the [SyncedTabsView] when available with [SyncedTabsView.displaySyncedTabs]
+ * otherwise notifies the appropriate error to [SyncedTabsView.onError].
+ */
+ fun refreshSyncedTabs()
+
+ /**
+ * Requests for the account on the [FxaAccountManager] to perform a sync.
+ */
+ fun syncAccount()
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/ext/SyncedTabsStorage.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/ext/SyncedTabsStorage.kt
new file mode 100644
index 0000000000..0b8247365a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/ext/SyncedTabsStorage.kt
@@ -0,0 +1,43 @@
+/* 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.syncedtabs.ext
+
+import mozilla.components.browser.storage.sync.TabEntry
+import mozilla.components.feature.syncedtabs.ClientTabPair
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+
+/**
+ * Get all the synced tabs that match the optional filter.
+ *
+ * @param limit How many synced tabs to query. A negative value will query all tabs. Defaults to `-1`.
+ * @param filter Optional filter for the active [TabEntry] of each tab.
+ */
+internal suspend fun SyncedTabsStorage.getActiveDeviceTabs(
+ limit: Int = -1,
+ filter: (TabEntry) -> Boolean = { true },
+): List<ClientTabPair> {
+ if (limit == 0) return emptyList()
+
+ return getSyncedDeviceTabs().fold(mutableListOf()) { result, (client, tabs) ->
+ tabs.forEach { tab ->
+ val activeTabEntry = tab.active()
+ if (filter(activeTabEntry)) {
+ result.add(
+ ClientTabPair(
+ clientName = client.displayName,
+ tab = activeTabEntry,
+ lastUsed = tab.lastUsed,
+ deviceType = client.deviceType,
+ ),
+ )
+
+ if (result.size == limit) {
+ return result
+ }
+ }
+ }
+ result
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/facts/SyncedTabsFacts.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/facts/SyncedTabsFacts.kt
new file mode 100644
index 0000000000..5ccede45b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/facts/SyncedTabsFacts.kt
@@ -0,0 +1,44 @@
+/* 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.syncedtabs.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the Synced Tabs feature.
+ */
+class SyncedTabsFacts {
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val SYNCED_TABS_SUGGESTION_CLICKED = "synced_tabs_suggestion_clicked"
+ }
+}
+
+private fun emitSyncedTabsFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_SYNCEDTABS,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitSyncedTabSuggestionClickedFact() {
+ emitSyncedTabsFact(
+ Action.INTERACTION,
+ SyncedTabsFacts.Items.SYNCED_TABS_SUGGESTION_CLICKED,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractor.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractor.kt
new file mode 100644
index 0000000000..a7826ff559
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractor.kt
@@ -0,0 +1,32 @@
+/* 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.syncedtabs.interactor
+
+import mozilla.components.browser.storage.sync.Tab
+import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+
+internal class DefaultInteractor(
+ override val controller: SyncedTabsController,
+ override val view: SyncedTabsView,
+ override val tabClicked: (Tab) -> Unit,
+) : SyncedTabsInteractor {
+
+ override fun start() {
+ view.listener = this
+ }
+
+ override fun stop() {
+ view.listener = null
+ }
+
+ override fun onTabClicked(tab: Tab) {
+ tabClicked(tab)
+ }
+
+ override fun onRefresh() {
+ controller.syncAccount()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/interactor/SyncedTabsInteractor.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/interactor/SyncedTabsInteractor.kt
new file mode 100644
index 0000000000..55016e89ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/interactor/SyncedTabsInteractor.kt
@@ -0,0 +1,19 @@
+/* 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.syncedtabs.interactor
+
+import mozilla.components.browser.storage.sync.Tab
+import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * An interactor that handles events from [SyncedTabsView.Listener].
+ */
+interface SyncedTabsInteractor : SyncedTabsView.Listener, LifecycleAwareFeature {
+ val controller: SyncedTabsController
+ val view: SyncedTabsView
+ val tabClicked: (Tab) -> Unit
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenter.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenter.kt
new file mode 100644
index 0000000000..21172bb0a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenter.kt
@@ -0,0 +1,142 @@
+/* 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.syncedtabs.presenter
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.SyncEnginesStorage
+import mozilla.components.service.fxa.sync.SyncStatusObserver
+
+/**
+ * The tricky part in this class is to handle all possible Sync+FxA states:
+ *
+ * - No Sync account
+ * - Connected to FxA but not Sync (impossible state on mobile at the moment).
+ * - Connected to Sync, but needs reconnection.
+ * - Connected to Sync, but tabs syncing disabled.
+ * - Connected to Sync, but tabs haven't been synced yet (they stay in memory after the first sync).
+ * - Connected to Sync, but only one device in the account (us), so no other tab to show.
+ * - Connected to Sync.
+ *
+ */
+internal class DefaultPresenter(
+ private val context: Context,
+ override val controller: SyncedTabsController,
+ override val accountManager: FxaAccountManager,
+ override val view: SyncedTabsView,
+ private val lifecycleOwner: LifecycleOwner,
+) : SyncedTabsPresenter {
+
+ @VisibleForTesting
+ internal val eventObserver = SyncedTabsSyncObserver(context, view, controller)
+
+ @VisibleForTesting
+ internal val accountObserver = SyncedTabsAccountObserver(view, controller)
+
+ override fun start() {
+ accountManager.registerForSyncEvents(
+ observer = eventObserver,
+ owner = lifecycleOwner,
+ autoPause = true,
+ )
+
+ accountManager.register(
+ observer = accountObserver,
+ owner = lifecycleOwner,
+ autoPause = true,
+ )
+
+ // No authenticated account present at all.
+ if (accountManager.authenticatedAccount() == null) {
+ view.onError(ErrorType.SYNC_UNAVAILABLE)
+ return
+ }
+
+ // Have an account, but it ran into auth issues.
+ if (accountManager.accountNeedsReauth()) {
+ view.onError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
+ return
+ }
+
+ // Synced tabs not enabled.
+ if (!isSyncedTabsEngineEnabled(context)) {
+ view.onError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
+ return
+ }
+
+ controller.syncAccount()
+ }
+
+ override fun stop() {
+ accountManager.unregisterForSyncEvents(eventObserver)
+ accountManager.unregister(accountObserver)
+ }
+
+ companion object {
+ // This status isn't always set before it's inspected. This causes erroneous reports of the
+ // sync engine being unavailable. Tabs are included in sync by default, so it's safe to
+ // default to true until they are deliberately disabled.
+ private fun isSyncedTabsEngineEnabled(context: Context): Boolean {
+ return SyncEnginesStorage(context).getStatus()[SyncEngine.Tabs] ?: true
+ }
+ }
+
+ internal class SyncedTabsAccountObserver(
+ private val view: SyncedTabsView,
+ private val controller: SyncedTabsController,
+ ) : AccountObserver {
+
+ override fun onLoggedOut() {
+ CoroutineScope(Dispatchers.Main).launch { view.onError(ErrorType.SYNC_UNAVAILABLE) }
+ }
+
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ CoroutineScope(Dispatchers.Main).launch {
+ controller.syncAccount()
+ }
+ }
+
+ override fun onAuthenticationProblems() {
+ CoroutineScope(Dispatchers.Main).launch {
+ view.onError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
+ }
+ }
+ }
+
+ internal class SyncedTabsSyncObserver(
+ private val context: Context,
+ private val view: SyncedTabsView,
+ private val controller: SyncedTabsController,
+ ) : SyncStatusObserver {
+
+ override fun onIdle() {
+ if (isSyncedTabsEngineEnabled(context)) {
+ controller.refreshSyncedTabs()
+ } else {
+ view.onError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
+ }
+ }
+
+ override fun onError(error: Exception?) {
+ view.onError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
+ }
+
+ override fun onStarted() {
+ view.startLoading()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/SyncedTabsPresenter.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/SyncedTabsPresenter.kt
new file mode 100644
index 0000000000..3f714651e6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/SyncedTabsPresenter.kt
@@ -0,0 +1,20 @@
+/* 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.syncedtabs.presenter
+
+import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * A presenter that orchestrates the [FxaAccountManager] being in the correct state to request remote tabs from the
+ * [SyncedTabsController] or notifies [SyncedTabsView.onError] otherwise.
+ */
+interface SyncedTabsPresenter : LifecycleAwareFeature {
+ val controller: SyncedTabsController
+ val accountManager: FxaAccountManager
+ val view: SyncedTabsView
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsProvider.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsProvider.kt
new file mode 100644
index 0000000000..573ac9bdeb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsProvider.kt
@@ -0,0 +1,18 @@
+/* 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.syncedtabs.storage
+
+import mozilla.components.browser.storage.sync.SyncedDeviceTabs
+
+/**
+ * Provides tabs from remote Firefox Sync devices.
+ */
+interface SyncedTabsProvider {
+
+ /**
+ * A list of [SyncedDeviceTabs], each containing a synced device and its current tabs.
+ */
+ suspend fun getSyncedDeviceTabs(): List<SyncedDeviceTabs>
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorage.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorage.kt
new file mode 100644
index 0000000000..9c599e7230
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorage.kt
@@ -0,0 +1,119 @@
+/* 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.syncedtabs.storage
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.isActive
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.storage.sync.RemoteTabsStorage
+import mozilla.components.browser.storage.sync.SyncedDeviceTabs
+import mozilla.components.browser.storage.sync.Tab
+import mozilla.components.browser.storage.sync.TabEntry
+import mozilla.components.concept.sync.Device
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.ext.withConstellation
+import mozilla.components.service.fxa.sync.SyncReason
+
+/**
+ * A storage that listens to the [BrowserStore] changes to synchronize the local tabs state
+ * with [RemoteTabsStorage] and then synchronize with [accountManager].
+ *
+ * @param accountManager Account manager used to retrieve synced tabs.
+ * @param store Browser store to observe for state changes.
+ * @param tabsStorage Storage layer for tabs to sync.
+ * @param debounceMillis Length to debounce rapid changes for storing and syncing.
+ */
+class SyncedTabsStorage(
+ private val accountManager: FxaAccountManager,
+ private val store: BrowserStore,
+ private val tabsStorage: RemoteTabsStorage,
+ private val maxActiveTime: Long,
+ private val debounceMillis: Long = 1000L,
+) : SyncedTabsProvider {
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Start listening to browser store changes.
+ */
+ @OptIn(FlowPreview::class)
+ fun start() {
+ scope = store.flowScoped { flow ->
+ flow.distinctUntilChangedBy { it.toSyncTabState() }
+ .map { state ->
+ // TO-DO: https://github.com/mozilla-mobile/android-components/issues/5179
+ val iconUrl = null
+ state.tabs.filter { !it.content.private && !it.content.loading }.map { tab ->
+ val history = listOf(TabEntry(tab.content.title, tab.content.url, iconUrl))
+ Tab(history, 0, tab.lastAccess, !tab.isActive(maxActiveTime))
+ }
+ }
+ .debounce(debounceMillis)
+ .collect { tabs ->
+ tabsStorage.store(tabs)
+ accountManager.syncNow(
+ reason = SyncReason.User,
+ customEngineSubset = listOf(SyncEngine.Tabs),
+ debounce = true,
+ )
+ }
+ }
+ }
+
+ /**
+ * Stop listening to browser store changes.
+ */
+ fun stop() {
+ scope?.cancel()
+ }
+
+ /**
+ * See [SyncedTabsProvider.getSyncedDeviceTabs].
+ */
+ override suspend fun getSyncedDeviceTabs(): List<SyncedDeviceTabs> {
+ val otherDevices = syncClients() ?: return emptyList()
+ return tabsStorage.getAll()
+ .mapNotNull { (client, tabs) ->
+ val fxaDevice = otherDevices.find { it.id == client.id }
+
+ fxaDevice?.let { SyncedDeviceTabs(fxaDevice, tabs.sortedByDescending { it.lastUsed }) }
+ }
+ .sortedByDescending {
+ it.tabs.firstOrNull()?.lastUsed
+ }
+ }
+
+ /**
+ * List of synced devices.
+ */
+ @VisibleForTesting
+ internal fun syncClients(): List<Device>? {
+ accountManager.withConstellation {
+ return state()?.otherDevices
+ }
+ return null
+ }
+
+ private data class SyncComponents(
+ val selectedId: String?,
+ val lastAccessed: List<Long>,
+ val loadedTabs: List<TabSessionState>,
+ )
+
+ private fun BrowserState.toSyncTabState() = SyncComponents(
+ selectedId = selectedTabId,
+ lastAccessed = tabs.map { it.lastAccess },
+ loadedTabs = tabs.filter { it.content.loading },
+ )
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/view/SyncedTabsView.kt b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/view/SyncedTabsView.kt
new file mode 100644
index 0000000000..2165c1d739
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/view/SyncedTabsView.kt
@@ -0,0 +1,89 @@
+/* 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.syncedtabs.view
+
+import android.view.View
+import mozilla.components.browser.storage.sync.SyncedDeviceTabs
+import mozilla.components.browser.storage.sync.Tab
+
+/**
+ * An interface for views that can display Firefox Sync "synced tabs" and related UI controls.
+ */
+interface SyncedTabsView {
+ var listener: Listener?
+
+ /**
+ * When tab syncing has started.
+ */
+ fun startLoading() = Unit
+
+ /**
+ * When tab syncing has completed.
+ */
+ fun stopLoading() = Unit
+
+ /**
+ * New tabs have been received to display.
+ */
+ fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>)
+
+ /**
+ * An error has occurred that may require various user-interactions based on the [ErrorType].
+ */
+ fun onError(error: ErrorType)
+
+ /**
+ * Casts this [SyncedTabsView] interface to an actual Android [View] object.
+ */
+ fun asView(): View = (this as View)
+
+ /**
+ * An interface for notifying the listener of the [SyncedTabsView].
+ */
+ interface Listener {
+
+ /**
+ * Invoked when a tab has been selected.
+ */
+ fun onTabClicked(tab: Tab)
+
+ /**
+ * Invoked when receiving a request to refresh the synced tabs.
+ */
+ fun onRefresh()
+ }
+
+ /**
+ * The various types of errors that can occur from syncing tabs.
+ */
+ enum class ErrorType {
+
+ /**
+ * Other devices found but there are no tabs to sync.
+ * */
+ NO_TABS_AVAILABLE,
+
+ /**
+ * There are no other devices found with this account and therefore no tabs to sync.
+ */
+ MULTIPLE_DEVICES_UNAVAILABLE,
+
+ /**
+ * The engine for syncing tabs is unavailable. This is mostly due to a user turning off the feature on the
+ * Firefox Sync account.
+ */
+ SYNC_ENGINE_UNAVAILABLE,
+
+ /**
+ * There is no Firefox Sync account available. A user needs to sign-in before this feature.
+ */
+ SYNC_UNAVAILABLE,
+
+ /**
+ * The Firefox Sync account requires user-intervention to re-authenticate the account.
+ */
+ SYNC_NEEDS_REAUTHENTICATION,
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsAutocompleteProviderKtTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsAutocompleteProviderKtTest.kt
new file mode 100644
index 0000000000..6f939958a6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsAutocompleteProviderKtTest.kt
@@ -0,0 +1,68 @@
+/* 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.syncedtabs
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.storage.sync.SyncedDeviceTabs
+import mozilla.components.feature.syncedtabs.helper.getDevice1Tabs
+import mozilla.components.feature.syncedtabs.helper.getDevice2Tabs
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class SyncedTabsAutocompleteProviderKtTest {
+ private val syncedTabs: SyncedTabsStorage = mock()
+
+ @Test
+ fun `GIVEN synced tabs exist WHEN asked for autocomplete suggestions THEN return the first matching tab`() = runTest {
+ val deviceTabs1 = getDevice1Tabs()
+ val deviceTabs2 = getDevice2Tabs()
+ doReturn(listOf(deviceTabs1, deviceTabs2)).`when`(syncedTabs).getSyncedDeviceTabs()
+ val provider = SyncedTabsAutocompleteProvider(syncedTabs)
+
+ var suggestion = provider.getAutocompleteSuggestion("mozilla")
+ assertNull(suggestion)
+
+ suggestion = provider.getAutocompleteSuggestion("foo")
+ assertNotNull(suggestion)
+ assertEquals("foo", suggestion?.input)
+ assertEquals("foo.bar", suggestion?.text)
+ assertEquals("https://foo.bar", suggestion?.url)
+ assertEquals(SYNCED_TABS_AUTOCOMPLETE_SOURCE_NAME, suggestion?.source)
+ assertEquals(1, suggestion?.totalItems)
+
+ suggestion = provider.getAutocompleteSuggestion("obob")
+ assertNotNull(suggestion)
+ assertEquals("obob", suggestion?.input)
+ assertEquals("obob.bar", suggestion?.text)
+ assertEquals("https://obob.bar", suggestion?.url)
+ assertEquals(SYNCED_TABS_AUTOCOMPLETE_SOURCE_NAME, suggestion?.source)
+ assertEquals(1, suggestion?.totalItems)
+ }
+
+ @Test
+ fun `GIVEN open tabs exist WHEN asked for autocomplete suggestions and only private tabs match THEN return null`() = runTest {
+ doReturn(emptyList<SyncedDeviceTabs>()).`when`(syncedTabs).getSyncedDeviceTabs()
+ val provider = SyncedTabsAutocompleteProvider(syncedTabs)
+
+ var suggestion = provider.getAutocompleteSuggestion("mozilla")
+ assertNull(suggestion)
+
+ suggestion = provider.getAutocompleteSuggestion("foo")
+ assertNull(suggestion)
+
+ suggestion = provider.getAutocompleteSuggestion("bar")
+ assertNull(suggestion)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsFeatureTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsFeatureTest.kt
new file mode 100644
index 0000000000..c0c66fc7ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsFeatureTest.kt
@@ -0,0 +1,55 @@
+/* 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.syncedtabs
+
+import android.content.Context
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.feature.syncedtabs.interactor.SyncedTabsInteractor
+import mozilla.components.feature.syncedtabs.presenter.SyncedTabsPresenter
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class SyncedTabsFeatureTest {
+
+ private val context: Context = mock()
+ private val storage: SyncedTabsStorage = mock()
+ private val accountManager: FxaAccountManager = mock()
+ private val view: SyncedTabsView = mock()
+ private val lifecycleOwner: LifecycleOwner = mock()
+
+ private val presenter: SyncedTabsPresenter = mock()
+ private val interactor: SyncedTabsInteractor = mock()
+ private val feature: SyncedTabsFeature =
+ SyncedTabsFeature(
+ context,
+ storage,
+ accountManager,
+ view,
+ lifecycleOwner,
+ onTabClicked = {},
+ presenter = presenter,
+ interactor = interactor,
+ )
+
+ @Test
+ fun start() {
+ feature.start()
+
+ verify(presenter).start()
+ verify(interactor).start()
+ }
+
+ @Test
+ fun stop() {
+ feature.stop()
+
+ verify(presenter).stop()
+ verify(interactor).stop()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProviderTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProviderTest.kt
new file mode 100644
index 0000000000..25a81fa050
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProviderTest.kt
@@ -0,0 +1,88 @@
+/* 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.syncedtabs
+
+import android.graphics.drawable.Drawable
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.awesomebar.AwesomeBar.Suggestion.Flag
+import mozilla.components.feature.syncedtabs.helper.getDevice1Tabs
+import mozilla.components.feature.syncedtabs.helper.getDevice2Tabs
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+
+class SyncedTabsStorageSuggestionProviderTest {
+ private lateinit var syncedTabs: SyncedTabsStorage
+ private lateinit var indicatorIcon: DeviceIndicators
+ private lateinit var indicatorIconDesktop: Drawable
+ private lateinit var indicatorIconMobile: Drawable
+
+ @Before
+ fun setup() {
+ syncedTabs = mock()
+ indicatorIcon = mock()
+ indicatorIconDesktop = mock()
+ indicatorIconMobile = mock()
+ }
+
+ @Test
+ fun `matches remote tabs`() = runTest {
+ val provider = SyncedTabsStorageSuggestionProvider(syncedTabs, mock(), mock(), indicatorIcon)
+ val deviceTabs1 = getDevice1Tabs()
+ val deviceTabs2 = getDevice2Tabs()
+ whenever(syncedTabs.getSyncedDeviceTabs()).thenReturn(listOf(deviceTabs1, deviceTabs2))
+ whenever(indicatorIcon.desktop).thenReturn(indicatorIconDesktop)
+ whenever(indicatorIcon.mobile).thenReturn(indicatorIconMobile)
+
+ val suggestions = provider.onInputChanged("bobo")
+ assertEquals(3, suggestions.size)
+ assertEquals("Hello Bobo", suggestions[0].title)
+ assertEquals("Foo Client", suggestions[0].description)
+ assertEquals("In URL", suggestions[1].title)
+ assertEquals("Foo Client", suggestions[1].description)
+ assertEquals("BOBO in CAPS", suggestions[2].title)
+ assertEquals("Bar Client", suggestions[2].description)
+ assertEquals(setOf(Flag.SYNC_TAB), suggestions[0].flags)
+ assertEquals(setOf(Flag.SYNC_TAB), suggestions[1].flags)
+ assertEquals(setOf(Flag.SYNC_TAB), suggestions[2].flags)
+ assertEquals(indicatorIconDesktop, suggestions[0].indicatorIcon)
+ assertEquals(indicatorIconDesktop, suggestions[1].indicatorIcon)
+ assertEquals(indicatorIconMobile, suggestions[2].indicatorIcon)
+ assertNotNull(suggestions[0].indicatorIcon)
+ assertNotNull(suggestions[1].indicatorIcon)
+ assertNotNull(suggestions[2].indicatorIcon)
+ }
+
+ @Test
+ fun `GIVEN an external filter WHEN querying tabs THEN return only the results that pass through the filter`() = runTest {
+ val deviceTabs1 = getDevice1Tabs()
+ val deviceTabs2 = getDevice2Tabs()
+ whenever(syncedTabs.getSyncedDeviceTabs()).thenReturn(listOf(deviceTabs1, deviceTabs2))
+ whenever(indicatorIcon.desktop).thenReturn(indicatorIconDesktop)
+ whenever(indicatorIcon.mobile).thenReturn(indicatorIconMobile)
+
+ val provider = SyncedTabsStorageSuggestionProvider(
+ syncedTabs = syncedTabs,
+ loadUrlUseCase = mock(),
+ icons = mock(),
+ deviceIndicators = indicatorIcon,
+ resultsUrlFilter = {
+ it.tryGetHostFromUrl() == "https://foo.bar".tryGetHostFromUrl()
+ },
+ )
+
+ val suggestions = provider.onInputChanged("foo")
+
+ assertEquals(2, suggestions.size)
+ // The url is behind the "onSuggestionClicked" lambda.
+ // Check the descriptions of the only two tabs that have the "foo.bar" host.
+ assertEquals(2, suggestions.map { it.description }.filter { it == "Foo Client" }.size)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/controller/DefaultControllerTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/controller/DefaultControllerTest.kt
new file mode 100644
index 0000000000..0241a0d0a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/controller/DefaultControllerTest.kt
@@ -0,0 +1,149 @@
+/* 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.syncedtabs.controller
+
+import mozilla.components.browser.storage.sync.SyncedDeviceTabs
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.sync.SyncReason
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+
+class DefaultControllerTest {
+ private val storage: SyncedTabsStorage = mock()
+ private val accountManager: FxaAccountManager = mock()
+ private val view: SyncedTabsView = mock()
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `update view only when no account available`() = runTestOnMain {
+ val controller = DefaultController(
+ storage,
+ accountManager,
+ view,
+ coroutineContext,
+ )
+
+ controller.refreshSyncedTabs()
+
+ verify(view).stopLoading()
+
+ verifyNoMoreInteractions(view)
+ }
+
+ @Test
+ fun `notify if there are no other devices synced`() = runTestOnMain {
+ val controller = DefaultController(
+ storage,
+ accountManager,
+ view,
+ coroutineContext,
+ )
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val state: ConstellationState = mock()
+
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.state()).thenReturn(state)
+ `when`(state.otherDevices).thenReturn(emptyList())
+
+ `when`(storage.getSyncedDeviceTabs()).thenReturn(emptyList())
+
+ controller.refreshSyncedTabs()
+
+ verify(view).onError(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)
+ }
+
+ @Test
+ fun `notify if there are no tabs from other devices to sync`() = runTestOnMain {
+ val controller = DefaultController(
+ storage,
+ accountManager,
+ view,
+ coroutineContext,
+ )
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val state: ConstellationState = mock()
+
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.state()).thenReturn(state)
+ `when`(state.otherDevices).thenReturn(listOf(mock()))
+
+ `when`(storage.getSyncedDeviceTabs()).thenReturn(emptyList())
+
+ controller.refreshSyncedTabs()
+
+ verify(view, never()).onError(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)
+ verify(view).onError(ErrorType.NO_TABS_AVAILABLE)
+ }
+
+ @Test
+ fun `display synced tabs`() = runTestOnMain {
+ val controller = DefaultController(
+ storage,
+ accountManager,
+ view,
+ coroutineContext,
+ )
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val state: ConstellationState = mock()
+ val syncedDeviceTabs = SyncedDeviceTabs(mock(), listOf(mock()))
+ val listOfSyncedDeviceTabs = listOf(syncedDeviceTabs)
+
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.state()).thenReturn(state)
+ `when`(state.otherDevices).thenReturn(listOf(mock()))
+
+ `when`(storage.getSyncedDeviceTabs()).thenReturn(listOfSyncedDeviceTabs)
+
+ controller.refreshSyncedTabs()
+
+ verify(view, never()).onError(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)
+ verify(view, never()).onError(ErrorType.NO_TABS_AVAILABLE)
+ verify(view).displaySyncedTabs(listOfSyncedDeviceTabs)
+ }
+
+ @Test
+ fun `WHEN syncAccount is called THEN view is loading, devices are refreshed, and sync started`() = runTestOnMain {
+ val controller = DefaultController(
+ storage,
+ accountManager,
+ view,
+ coroutineContext,
+ )
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.refreshDevices()).thenReturn(true)
+
+ controller.syncAccount()
+
+ verify(view).startLoading()
+ verify(constellation).refreshDevices()
+ verify(accountManager).syncNow(SyncReason.User, true, listOf(SyncEngine.Tabs))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/ext/SyncedTabsStorageKtTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/ext/SyncedTabsStorageKtTest.kt
new file mode 100644
index 0000000000..def4a3479d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/ext/SyncedTabsStorageKtTest.kt
@@ -0,0 +1,65 @@
+/* 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.syncedtabs.ext
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.feature.syncedtabs.helper.getDevice1Tabs
+import mozilla.components.feature.syncedtabs.helper.getDevice2Tabs
+import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+
+class SyncedTabsStorageKtTest {
+ private val syncedTabs: SyncedTabsStorage = mock()
+
+ @Test
+ fun `GIVEN synced tabs exist WHEN asked for active device tabs THEN return all tabs`() = runTest {
+ val device1Tabs = getDevice1Tabs()
+ val device2Tabs = getDevice2Tabs()
+ doReturn(listOf(device1Tabs, device2Tabs)).`when`(syncedTabs).getSyncedDeviceTabs()
+
+ val result = syncedTabs.getActiveDeviceTabs()
+ assertNotNull(result)
+ assertEquals(4, result.size)
+ assertEquals(3, result.filter { it.clientName == device1Tabs.device.displayName }.size)
+ assertEquals(1, result.filter { it.clientName == device2Tabs.device.displayName }.size)
+ }
+
+ @Test
+ fun `GIVEN synced tabs exist WHEN asked for a lower number of active device tabs THEN return tabs up to that number`() = runTest {
+ val device1Tabs = getDevice1Tabs()
+ val device2Tabs = getDevice2Tabs()
+ doReturn(listOf(device1Tabs, device2Tabs)).`when`(syncedTabs).getSyncedDeviceTabs()
+
+ var result = syncedTabs.getActiveDeviceTabs(2)
+ assertNotNull(result)
+ assertEquals(2, result.size)
+ assertEquals(2, result.filter { it.clientName == device1Tabs.device.displayName }.size)
+
+ result = syncedTabs.getActiveDeviceTabs(7)
+ assertNotNull(result)
+ assertEquals(4, result.size)
+ assertEquals(3, result.filter { it.clientName == device1Tabs.device.displayName }.size)
+ assertEquals(1, result.filter { it.clientName == device2Tabs.device.displayName }.size)
+ }
+
+ @Test
+ fun `GIVEN synced tabs exist WHEN asked for active device tabs and a filter is passed THEN return all tabs matching the filter`() = runTest {
+ val device1Tabs = getDevice1Tabs()
+ val device2Tabs = getDevice2Tabs()
+ doReturn(listOf(device1Tabs, device2Tabs)).`when`(syncedTabs).getSyncedDeviceTabs()
+ val filteredTitle = device1Tabs.tabs[0].active().title
+
+ val result = syncedTabs.getActiveDeviceTabs {
+ it.title == filteredTitle
+ }
+ assertNotNull(result)
+ assertEquals(1, result.size)
+ assertEquals(filteredTitle, result[0].tab.title)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/facts/SyncedTabsFactsTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/facts/SyncedTabsFactsTest.kt
new file mode 100644
index 0000000000..b33abd336c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/facts/SyncedTabsFactsTest.kt
@@ -0,0 +1,29 @@
+/* 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.syncedtabs.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SyncedTabsFactsTest {
+
+ @Test
+ fun `Emits facts for current state`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitSyncedTabSuggestionClickedFact()
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_SYNCEDTABS, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(SyncedTabsFacts.Items.SYNCED_TABS_SUGGESTION_CLICKED, item)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/helper/SyncedTabsProvider.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/helper/SyncedTabsProvider.kt
new file mode 100644
index 0000000000..c39befa40f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/helper/SyncedTabsProvider.kt
@@ -0,0 +1,83 @@
+/* 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.syncedtabs.helper
+
+import mozilla.components.browser.storage.sync.SyncedDeviceTabs
+import mozilla.components.browser.storage.sync.Tab
+import mozilla.components.browser.storage.sync.TabEntry
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceType.DESKTOP
+import mozilla.components.concept.sync.DeviceType.MOBILE
+
+/**
+ * Get fake tabs from a fake desktop device.
+ */
+internal fun getDevice1Tabs() = SyncedDeviceTabs(
+ Device(
+ id = "client1",
+ displayName = "Foo Client",
+ deviceType = DESKTOP,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(),
+ subscriptionExpired = false,
+ subscription = null,
+ ),
+ listOf(
+ Tab(
+ listOf(
+ TabEntry("Foo", "https://foo.bar", null), // active tab
+ TabEntry("Bobo", "https://foo.bar", null),
+ TabEntry("Foo", "https://bobo.bar", null),
+ ),
+ 0,
+ 1,
+ false,
+ ),
+ Tab(
+ listOf(
+ TabEntry("Hello Bobo", "https://foo.bar", null), // active tab
+ ),
+ 0,
+ 5,
+ false,
+ ),
+ Tab(
+ listOf(
+ TabEntry("In URL", "https://bobo.bar", null), // active tab
+ ),
+ 0,
+ 2,
+ false,
+ ),
+ ),
+)
+
+/**
+ * Get fake tabs from a fake mobile device.
+ */
+internal fun getDevice2Tabs() = SyncedDeviceTabs(
+ Device(
+ id = "client2",
+ displayName = "Bar Client",
+ deviceType = MOBILE,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(),
+ subscriptionExpired = false,
+ subscription = null,
+ ),
+ listOf(
+ Tab(
+ listOf(
+ TabEntry("Bar", "https://bar.bar", null),
+ TabEntry("BOBO in CAPS", "https://obob.bar", null), // active tab
+ ),
+ 1,
+ 1,
+ false,
+ ),
+ ),
+)
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractorTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractorTest.kt
new file mode 100644
index 0000000000..d003a87301
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractorTest.kt
@@ -0,0 +1,107 @@
+/* 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.syncedtabs.interactor
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.storage.sync.SyncedDeviceTabs
+import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class DefaultInteractorTest {
+
+ private val view: SyncedTabsView = mock()
+ private val controller: SyncedTabsController = mock()
+
+ @Test
+ fun start() = runTest {
+ val view =
+ TestSyncedTabsView()
+ val feature = DefaultInteractor(
+ controller,
+ view,
+ ) {}
+
+ assertNull(view.listener)
+
+ feature.start()
+
+ assertNotNull(view.listener)
+ }
+
+ @Test
+ fun stop() = runTest {
+ val view =
+ TestSyncedTabsView()
+ val feature = DefaultInteractor(
+ controller,
+ view,
+ ) {}
+
+ assertNull(view.listener)
+
+ feature.start()
+
+ assertNotNull(view.listener)
+
+ feature.stop()
+
+ assertNull(view.listener)
+ }
+
+ @Test
+ fun `onTabClicked invokes callback`() = runTest {
+ var invoked = false
+ val feature = DefaultInteractor(
+ controller,
+ view,
+ ) {
+ invoked = true
+ }
+
+ feature.onTabClicked(mock())
+
+ assertTrue(invoked)
+ }
+
+ @Test
+ fun `onRefresh does not update devices when there is no constellation`() = runTest {
+ val feature = DefaultInteractor(
+ controller,
+ view,
+ ) {}
+
+ feature.onRefresh()
+
+ verify(controller).syncAccount()
+ }
+
+ @Test
+ fun `onRefresh updates devices when there is a constellation`() = runTest {
+ val feature = DefaultInteractor(
+ controller,
+ view,
+ ) {}
+
+ feature.onRefresh()
+
+ verify(controller).syncAccount()
+ }
+
+ private class TestSyncedTabsView : SyncedTabsView {
+ override var listener: SyncedTabsView.Listener? = null
+
+ override fun onError(error: SyncedTabsView.ErrorType) {
+ }
+
+ override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenterTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenterTest.kt
new file mode 100644
index 0000000000..4e057d7e42
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenterTest.kt
@@ -0,0 +1,256 @@
+/* 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.syncedtabs.presenter
+
+import android.content.Context
+import android.os.Looper.getMainLooper
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView
+import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.SyncEnginesStorage.Companion.SYNC_ENGINES_KEY
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class DefaultPresenterTest {
+
+ private val context: Context = testContext
+ private val controller: SyncedTabsController = mock()
+ private val accountManager: FxaAccountManager = mock()
+ private val view: SyncedTabsView = mock()
+ private val lifecycleOwner: LifecycleOwner = mock()
+
+ private val prefs = testContext.getSharedPreferences(SYNC_ENGINES_KEY, Context.MODE_PRIVATE)
+
+ @Test
+ fun `start returns when there is no profile`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ presenter.start()
+
+ verify(view).onError(ErrorType.SYNC_UNAVAILABLE)
+ }
+
+ @Test
+ fun `start returns if sync engine is not enabled`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ // disable sync storage
+ prefs.edit().putBoolean("tabs", false).apply()
+ `when`(accountManager.authenticatedAccount()).thenReturn(mock())
+
+ presenter.start()
+
+ verify(view).onError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
+ }
+
+ @Test
+ fun `start returns if sync needs reauthentication`() {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ `when`(accountManager.authenticatedAccount()).thenReturn(mock())
+ `when`(accountManager.accountNeedsReauth()).thenReturn(true)
+
+ presenter.start()
+
+ verify(view).onError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
+ }
+
+ @Test
+ fun `start invokes syncTabs - account profile is absent`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ prefs.edit().putBoolean("tabs", true).apply()
+ `when`(accountManager.authenticatedAccount()).thenReturn(mock())
+ `when`(accountManager.accountProfile()).thenReturn(null)
+ `when`(accountManager.accountNeedsReauth()).thenReturn(false)
+
+ presenter.start()
+
+ verify(controller).syncAccount()
+ }
+
+ @Test
+ fun `start invokes syncTabs - account profile is present`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ prefs.edit().putBoolean("tabs", true).apply()
+ `when`(accountManager.authenticatedAccount()).thenReturn(mock())
+ `when`(accountManager.accountProfile()).thenReturn(mock())
+ `when`(accountManager.accountNeedsReauth()).thenReturn(false)
+
+ presenter.start()
+
+ verify(controller).syncAccount()
+ }
+
+ @Test
+ fun `notify on logout`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ presenter.accountObserver.onLoggedOut()
+ shadowOf(getMainLooper()).idle()
+
+ verify(view).onError(ErrorType.SYNC_UNAVAILABLE)
+ }
+
+ @Test
+ fun `notify on authenticated`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ presenter.accountObserver.onAuthenticated(mock(), mock<AuthType.Existing>())
+ shadowOf(getMainLooper()).idle()
+
+ verify(controller).syncAccount()
+ }
+
+ @Test
+ fun `notify on authentication problems`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ presenter.accountObserver.onAuthenticationProblems()
+ shadowOf(getMainLooper()).idle()
+
+ verify(view).onError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
+ }
+
+ @Test
+ fun `sync tabs on idle status - tabs sync enabled`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ prefs.edit().putBoolean("tabs", true).apply()
+ presenter.eventObserver.onIdle()
+
+ verify(controller).refreshSyncedTabs()
+ }
+
+ @Test
+ fun `sync tabs on idle status - tabs sync disabled`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ prefs.edit().putBoolean("tabs", false).apply()
+ presenter.eventObserver.onIdle()
+
+ verifyNoInteractions(controller)
+ verify(view).onError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
+ }
+
+ @Test
+ fun `show loading state on started status`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ presenter.eventObserver.onStarted()
+
+ verify(view).startLoading()
+ }
+
+ @Test
+ fun `notify on error`() = runTest {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ presenter.eventObserver.onError(mock())
+
+ verify(view).onError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
+ }
+
+ @Test
+ fun `GIVEN the presenter is started WHEN it is stopped THEN unregister the account and sync events observers`() {
+ val presenter = DefaultPresenter(
+ context,
+ controller,
+ accountManager,
+ view,
+ lifecycleOwner,
+ )
+
+ presenter.stop()
+
+ verify(accountManager).unregisterForSyncEvents(presenter.eventObserver)
+ verify(accountManager).unregister(presenter.accountObserver)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorageTest.kt b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorageTest.kt
new file mode 100644
index 0000000000..e0e5c081f3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorageTest.kt
@@ -0,0 +1,392 @@
+/* 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.syncedtabs.storage
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.LastAccessAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.storage.sync.RemoteTabsStorage
+import mozilla.components.browser.storage.sync.SyncClient
+import mozilla.components.browser.storage.sync.SyncedDeviceTabs
+import mozilla.components.browser.storage.sync.Tab
+import mozilla.components.browser.storage.sync.TabEntry
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.sync.SyncReason
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class SyncedTabsStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var store: BrowserStore
+ private lateinit var tabsStorage: RemoteTabsStorage
+ private lateinit var accountManager: FxaAccountManager
+
+ @Before
+ fun setup() {
+ store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "tab1", url = "https://www.mozilla.org", lastAccess = 123L),
+ createTab(id = "tab2", url = "https://www.foo.bar", lastAccess = 124L),
+ createTab(id = "private", url = "https://private.tab", private = true, lastAccess = 125L),
+ ),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+ tabsStorage = mock()
+ accountManager = mock()
+ }
+
+ @Test
+ fun `listens to browser store changes, stores state changes, and calls onStoreComplete`() = runTestOnMain {
+ val feature = SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ debounceMillis = 0,
+ )
+ feature.start()
+
+ // This action will change the state due to lastUsed timestamp, but will run the flow.
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ verify(tabsStorage, times(2)).store(
+ listOf(
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.mozilla.org", iconUrl = null)), active = 0, lastUsed = 123L, true),
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.foo.bar", iconUrl = null)), active = 0, lastUsed = 124L, true),
+ // Private tab is absent.
+ ),
+ )
+ verify(accountManager, times(2)).syncNow(
+ SyncReason.User,
+ true,
+ listOf(SyncEngine.Tabs),
+ )
+ }
+
+ @Test
+ fun `stops listening to browser store changes on stop()`() = runTestOnMain {
+ val feature = SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ debounceMillis = 0,
+ )
+ feature.start()
+ // Run the flow.
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ verify(tabsStorage, times(2)).store(
+ listOf(
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.mozilla.org", iconUrl = null)), active = 0, lastUsed = 123L, true),
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.foo.bar", iconUrl = null)), active = 0, lastUsed = 124L, true),
+ ),
+ )
+
+ feature.stop()
+ // Run the flow.
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
+
+ verify(tabsStorage, never()).store(listOf()) // any() is not working so we send garbage
+ }
+
+ @Test
+ fun `getSyncedTabs matches tabs with FxA devices`() = runTestOnMain {
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ ),
+ )
+ val device1 = Device(
+ id = "client1",
+ displayName = "Foo Client",
+ deviceType = DeviceType.DESKTOP,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(),
+ subscriptionExpired = false,
+ subscription = null,
+ )
+ val device2 = Device(
+ id = "client2",
+ displayName = "Bar Client",
+ deviceType = DeviceType.MOBILE,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(),
+ subscriptionExpired = false,
+ subscription = null,
+ )
+ doReturn(listOf(device1, device2)).`when`(feature).syncClients()
+ val tabsClient1 = listOf(Tab(listOf(TabEntry("Foo", "https://foo.bar", null)), 0, 0, true))
+ val tabsClient2 = listOf(Tab(listOf(TabEntry("Foo", "https://foo.bar", null)), 0, 0, true))
+ whenever(tabsStorage.getAll()).thenReturn(
+ mapOf(
+ SyncClient("client1") to tabsClient1,
+ SyncClient("client2") to tabsClient2,
+ SyncClient("client-unknown") to listOf(Tab(listOf(TabEntry("Foo", "https://foo.bar", null)), 0, 0, true)),
+ ),
+ )
+
+ val result = feature.getSyncedDeviceTabs()
+ assertEquals(device1, result[0].device)
+ assertEquals(device2, result[1].device)
+ assertEquals(tabsClient1, result[0].tabs)
+ assertEquals(tabsClient2, result[1].tabs)
+ }
+
+ @Test
+ fun `getSyncedTabs returns empty list if syncClients() is null`() = runTestOnMain {
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ ),
+ )
+ doReturn(null).`when`(feature).syncClients()
+ assertEquals(emptyList<SyncedDeviceTabs>(), feature.getSyncedDeviceTabs())
+ }
+
+ @Test
+ fun `syncClients returns clients if the account is set and constellation state is set too`() {
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ ),
+ )
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val state: ConstellationState = mock()
+ whenever(accountManager.authenticatedAccount()).thenReturn(account)
+ whenever(account.deviceConstellation()).thenReturn(constellation)
+ whenever(constellation.state()).thenReturn(state)
+ val otherDevices = listOf(
+ Device(
+ id = "client2",
+ displayName = "Bar Client",
+ deviceType = DeviceType.MOBILE,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(),
+ subscriptionExpired = false,
+ subscription = null,
+ ),
+ )
+ whenever(state.otherDevices).thenReturn(otherDevices)
+ assertEquals(otherDevices, feature.syncClients())
+ }
+
+ @Test
+ fun `syncClients returns null if the account is set but constellation state is null`() {
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ ),
+ )
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ whenever(accountManager.authenticatedAccount()).thenReturn(account)
+ whenever(account.deviceConstellation()).thenReturn(constellation)
+ whenever(constellation.state()).thenReturn(null)
+ assertEquals(null, feature.syncClients())
+ }
+
+ @Test
+ fun `syncClients returns null if the account is null`() {
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ ),
+ )
+ whenever(accountManager.authenticatedAccount()).thenReturn(null)
+ assertEquals(null, feature.syncClients())
+ }
+
+ @Test
+ fun `tabs are stored when loaded`() = runTestOnMain {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "tab1", url = "https://www.mozilla.org", lastAccess = 123L),
+ createTab(id = "tab2", url = "https://www.foo.bar", lastAccess = 124L),
+ ),
+ selectedTabId = "tab1",
+ ),
+ )
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ debounceMillis = 0,
+ ),
+ )
+ feature.start()
+
+ // Tabs are only stored when initial state is collected, since they are already loaded
+ verify(tabsStorage, times(1)).store(
+ listOf(
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.mozilla.org", iconUrl = null)), active = 0, lastUsed = 123L, true),
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.foo.bar", iconUrl = null)), active = 0, lastUsed = 124L, true),
+ ),
+ )
+
+ // Change a tab besides loading it
+ store.dispatch(ContentAction.UpdateProgressAction("tab1", 50)).joinBlocking()
+
+ reset(tabsStorage)
+
+ verify(tabsStorage, never()).store(any())
+ }
+
+ @Test
+ fun `only loaded tabs are stored on load`() = runTestOnMain {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createUnloadedTab(id = "tab1", url = "https://www.mozilla.org", lastAccess = 123L),
+ createUnloadedTab(id = "tab2", url = "https://www.foo.bar", lastAccess = 124L),
+ ),
+ selectedTabId = "tab1",
+ ),
+ )
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ debounceMillis = 0,
+ ),
+ )
+ feature.start()
+
+ store.dispatch(ContentAction.UpdateLoadingStateAction("tab1", false)).joinBlocking()
+
+ verify(tabsStorage).store(
+ listOf(
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.mozilla.org", iconUrl = null)), active = 0, lastUsed = 123L, true),
+ ),
+ )
+ }
+
+ @Test
+ fun `tabs are stored when selected tab changes`() = runTestOnMain {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "tab1", url = "https://www.mozilla.org", lastAccess = 123L),
+ createTab(id = "tab2", url = "https://www.foo.bar", lastAccess = 124L),
+ ),
+ selectedTabId = "tab1",
+ ),
+ )
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ System.currentTimeMillis() * 2,
+ debounceMillis = 0,
+ ),
+ )
+ feature.start()
+
+ store.dispatch(TabListAction.SelectTabAction("tab2")).joinBlocking()
+
+ verify(tabsStorage, times(2)).store(
+ listOf(
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.mozilla.org", iconUrl = null)), active = 0, lastUsed = 123L, false),
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.foo.bar", iconUrl = null)), active = 0, lastUsed = 124L, false),
+ ),
+ )
+ }
+
+ @Test
+ fun `tabs are stored when lastAccessed is changed for any tab`() = runTestOnMain {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "tab1", url = "https://www.mozilla.org", lastAccess = 123L),
+ createTab(id = "tab2", url = "https://www.foo.bar", lastAccess = 124L),
+ ),
+ selectedTabId = "tab1",
+ ),
+ )
+ val feature = spy(
+ SyncedTabsStorage(
+ accountManager,
+ store,
+ tabsStorage,
+ 0,
+ debounceMillis = 0,
+ ),
+ )
+ feature.start()
+
+ store.dispatch(LastAccessAction.UpdateLastAccessAction("tab1", 300L)).joinBlocking()
+
+ verify(tabsStorage, times(1)).store(
+ listOf(
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.mozilla.org", iconUrl = null)), active = 0, lastUsed = 123L, true),
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.foo.bar", iconUrl = null)), active = 0, lastUsed = 124L, true),
+ ),
+ )
+ verify(tabsStorage, times(1)).store(
+ listOf(
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.mozilla.org", iconUrl = null)), active = 0, lastUsed = 300L, true),
+ Tab(history = listOf(TabEntry(title = "", url = "https://www.foo.bar", iconUrl = null)), active = 0, lastUsed = 124L, true),
+ ),
+ )
+ }
+
+ private fun createUnloadedTab(id: String, url: String, lastAccess: Long) = createTab(id = id, url = url, lastAccess = lastAccess).run {
+ copy(content = this.content.copy(loading = true))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/syncedtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/syncedtabs/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/syncedtabs/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/syncedtabs/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/syncedtabs/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/syncedtabs/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/tab-collections/README.md b/mobile/android/android-components/components/feature/tab-collections/README.md
new file mode 100644
index 0000000000..cef17fbcbc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Tab-Collections
+
+Feature implementation for saving, restoring and organizing collections of tabs.
+
+## Usage
+
+### 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-tab-collections:{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/tab-collections/build.gradle b/mobile/android/android-components/components/feature/tab-collections/build.gradle
new file mode 100644
index 0000000000..74cd5c76b7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/build.gradle
@@ -0,0 +1,87 @@
+/* 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/. */
+
+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"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/proguard/androidx-annotations.pro'
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.tab.collections'
+}
+
+dependencies {
+ implementation project(':feature-tabs')
+
+ implementation project(':concept-engine')
+
+ implementation project(':browser-state')
+ implementation project(':browser-session-storage')
+
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_paging
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.kotlin_coroutines
+
+ androidTestImplementation project(':support-android-test')
+ androidTestImplementation project(':support-test-fakes')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_arch_core_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/tab-collections/proguard-rules.pro b/mobile/android/android-components/components/feature/tab-collections/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/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/tab-collections/schemas/mozilla.components.feature.tab.collections.db.TabCollectionDatabase/1.json b/mobile/android/android-components/components/feature/tab-collections/schemas/mozilla.components.feature.tab.collections.db.TabCollectionDatabase/1.json
new file mode 100644
index 0000000000..e125b8a11f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/schemas/mozilla.components.feature.tab.collections.db.TabCollectionDatabase/1.json
@@ -0,0 +1,122 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "cf6d8bdd8e16b3f92043f9430524c80d",
+ "entities": [
+ {
+ "tableName": "tab_collections",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `updated_at` INTEGER NOT NULL, `created_at` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "tabs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `stat_file` TEXT NOT NULL, `tab_collection_id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, FOREIGN KEY(`tab_collection_id`) REFERENCES `tab_collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "stateFile",
+ "columnName": "stat_file",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tabCollectionId",
+ "columnName": "tab_collection_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_tabs_tab_collection_id",
+ "unique": false,
+ "columnNames": [
+ "tab_collection_id"
+ ],
+ "createSql": "CREATE INDEX `index_tabs_tab_collection_id` ON `${TABLE_NAME}` (`tab_collection_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "tab_collections",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "tab_collection_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"cf6d8bdd8e16b3f92043f9430524c80d\")"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/TabCollectionStorageTest.kt b/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/TabCollectionStorageTest.kt
new file mode 100644
index 0000000000..3be60286a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/TabCollectionStorageTest.kt
@@ -0,0 +1,493 @@
+/* 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.tab.collections
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.feature.tab.collections.db.TabCollectionDatabase
+import mozilla.components.feature.tab.collections.db.TabEntity
+import mozilla.components.support.ktx.java.io.truncateDirectory
+import mozilla.components.support.test.fakes.engine.FakeEngine
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+@ExperimentalCoroutinesApi // for runTest
+@Suppress("LargeClass") // Large test is large
+class TabCollectionStorageTest {
+ private lateinit var context: Context
+ private lateinit var storage: TabCollectionStorage
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+
+ context = ApplicationProvider.getApplicationContext()
+ val database = Room.inMemoryDatabaseBuilder(context, TabCollectionDatabase::class.java).build()
+
+ storage = TabCollectionStorage(context)
+ storage.database = lazy { database }
+ }
+
+ @After
+ fun tearDown() {
+ TabEntity.getStateDirectory(context.filesDir).truncateDirectory()
+
+ executor.shutdown()
+ }
+
+ @Test
+ fun testCreatingCollections() {
+ storage.createCollection("Empty")
+ storage.createCollection(
+ "Recipes",
+ listOf(
+ createTab("https://www.mozilla.org", title = "Mozilla"),
+ createTab("https://www.firefox.com", title = "Firefox"),
+ ),
+ )
+
+ val collections = getAllCollections()
+
+ assertEquals(2, collections.size)
+
+ assertEquals("Recipes", collections[0].title)
+ assertEquals(2, collections[0].tabs.size)
+
+ assertEquals("https://www.firefox.com", collections[0].tabs[0].url)
+ assertEquals("Firefox", collections[0].tabs[0].title)
+ assertEquals("https://www.mozilla.org", collections[0].tabs[1].url)
+ assertEquals("Mozilla", collections[0].tabs[1].title)
+
+ assertEquals("Empty", collections[1].title)
+ assertEquals(0, collections[1].tabs.size)
+ }
+
+ @Test
+ fun testAddingTabsToExistingCollection() {
+ storage.createCollection("Articles")
+ var id: Long?
+
+ getAllCollections().let { collections ->
+ assertEquals(1, collections.size)
+ assertEquals(0, collections[0].tabs.size)
+
+ id = storage.addTabsToCollection(
+ collections[0],
+ listOf(
+ createTab("https://www.mozilla.org", title = "Mozilla"),
+ createTab("https://www.firefox.com", title = "Firefox"),
+ ),
+ )
+ }
+
+ getAllCollections().let { collections ->
+ assertEquals(1L, id)
+ assertEquals(1, collections.size)
+ assertEquals(2, collections[0].tabs.size)
+
+ assertEquals("https://www.firefox.com", collections[0].tabs[0].url)
+ assertEquals("Firefox", collections[0].tabs[0].title)
+ assertEquals("https://www.mozilla.org", collections[0].tabs[1].url)
+ assertEquals("Mozilla", collections[0].tabs[1].title)
+ }
+ }
+
+ @Test
+ fun testRemovingTabsFromCollection() {
+ storage.createCollection(
+ "Articles",
+ listOf(
+ createTab("https://www.mozilla.org", title = "Mozilla"),
+ createTab("https://www.firefox.com", title = "Firefox"),
+ ),
+ )
+
+ getAllCollections().let { collections ->
+ assertEquals(1, collections.size)
+ assertEquals(2, collections[0].tabs.size)
+
+ storage.removeTabFromCollection(collections[0], collections[0].tabs[0])
+ }
+
+ getAllCollections().let { collections ->
+ assertEquals(1, collections.size)
+ assertEquals(1, collections[0].tabs.size)
+
+ assertEquals("https://www.mozilla.org", collections[0].tabs[0].url)
+ assertEquals("Mozilla", collections[0].tabs[0].title)
+ }
+ }
+
+ @Test
+ fun testRenamingCollection() {
+ storage.createCollection("Articles")
+
+ getAllCollections().let { collections ->
+ assertEquals(1, collections.size)
+ storage.renameCollection(collections[0], "Blog Articles")
+ }
+
+ getAllCollections().let { collections ->
+ assertEquals(1, collections.size)
+ assertEquals("Blog Articles", collections[0].title)
+ }
+ }
+
+ @Test
+ fun testRemovingCollection() {
+ storage.createCollection("Articles")
+ storage.createCollection("Recipes")
+
+ getAllCollections().let { collections ->
+ assertEquals(2, collections.size)
+ assertEquals("Recipes", collections[0].title)
+ assertEquals("Articles", collections[1].title)
+
+ storage.removeCollection(collections[0])
+ }
+
+ getAllCollections().let { collections ->
+ assertEquals(1, collections.size)
+
+ assertEquals("Articles", collections[0].title)
+ }
+ }
+
+ @Test
+ fun testCreatingCollectionAndRestoringState() {
+ val session1 = createTab("https://www.mozilla.org", title = "Mozilla")
+ val session2 = createTab("https://www.firefox.com", title = "Firefox")
+
+ storage.createCollection("Articles", listOf(session1, session2))
+
+ getAllCollections().let { collections ->
+ assertEquals(1, collections.size)
+
+ val collection = collections[0]
+
+ val sessions = collection.restore(context, FakeEngine(), restoreSessionId = true)
+ assertEquals(2, sessions.size)
+
+ // We restored the same sessions
+ matches(session1, sessions[0])
+ matches(session2, sessions[1])
+
+ assertEquals(session1.id, sessions[0].state.id)
+ assertEquals(session2.id, sessions[1].state.id)
+ }
+
+ getAllCollections().let { collections ->
+ assertEquals(1, collections.size)
+
+ val collection = collections[0]
+
+ val sessions = collection.restore(context, FakeEngine(), restoreSessionId = false)
+ assertEquals(2, sessions.size)
+
+ // The sessions are not the same but contain the same data
+ assertNotEquals(session1, sessions[0])
+ assertNotEquals(session2, sessions[1])
+
+ assertNotEquals(session1.id, sessions[0].state.id)
+ assertNotEquals(session2.id, sessions[1].state.id)
+
+ assertEquals(session1.content.url, sessions[0].state.url)
+ assertEquals(session2.content.url, sessions[1].state.url)
+
+ assertEquals(session1.content.title, sessions[0].state.title)
+ assertEquals(session2.content.title, sessions[1].state.title)
+ }
+ }
+
+ @Test
+ @Suppress("ComplexMethod")
+ fun testGettingCollections() = runTest {
+ storage.createCollection(
+ "Articles",
+ listOf(
+ createTab("https://www.mozilla.org", title = "Mozilla"),
+ ),
+ )
+ storage.createCollection(
+ "Recipes",
+ listOf(
+ createTab("https://www.firefox.com", title = "Firefox"),
+ ),
+ )
+ storage.createCollection(
+ "Books",
+ listOf(
+ createTab("https://www.youtube.com", title = "YouTube"),
+ createTab("https://www.amazon.com", title = "Amazon"),
+ ),
+ )
+ storage.createCollection(
+ "News",
+ listOf(
+ createTab("https://www.google.com", title = "Google"),
+ createTab("https://www.facebook.com", title = "Facebook"),
+ ),
+ )
+ storage.createCollection(
+ "Blogs",
+ listOf(
+ createTab("https://www.wikipedia.org", title = "Wikipedia"),
+ ),
+ )
+
+ val collections = storage.getCollections().first()
+
+ assertEquals(5, collections.size)
+
+ with(collections[0]) {
+ assertEquals("Blogs", title)
+ assertEquals(1, tabs.size)
+ assertEquals("https://www.wikipedia.org", tabs[0].url)
+ assertEquals("Wikipedia", tabs[0].title)
+ }
+
+ with(collections[1]) {
+ assertEquals("News", title)
+ assertEquals(2, tabs.size)
+ assertEquals("https://www.facebook.com", tabs[0].url)
+ assertEquals("Facebook", tabs[0].title)
+ assertEquals("https://www.google.com", tabs[1].url)
+ assertEquals("Google", tabs[1].title)
+ }
+
+ with(collections[2]) {
+ assertEquals("Books", title)
+ assertEquals(2, tabs.size)
+ assertEquals("https://www.amazon.com", tabs[0].url)
+ assertEquals("Amazon", tabs[0].title)
+ assertEquals("https://www.youtube.com", tabs[1].url)
+ assertEquals("YouTube", tabs[1].title)
+ }
+
+ with(collections[3]) {
+ assertEquals("Recipes", title)
+ assertEquals(1, tabs.size)
+
+ assertEquals("https://www.firefox.com", tabs[0].url)
+ assertEquals("Firefox", tabs[0].title)
+ }
+
+ with(collections[4]) {
+ assertEquals("Articles", title)
+ assertEquals(1, tabs.size)
+
+ assertEquals("https://www.mozilla.org", tabs[0].url)
+ assertEquals("Mozilla", tabs[0].title)
+ }
+ }
+
+ @Test
+ @Suppress("ComplexMethod")
+ fun testGettingCollectionsList() = runTest {
+ storage.createCollection(
+ "Articles",
+ listOf(
+ createTab("https://www.mozilla.org", title = "Mozilla"),
+ ),
+ )
+ storage.createCollection(
+ "Recipes",
+ listOf(
+ createTab("https://www.firefox.com", title = "Firefox"),
+ ),
+ )
+ storage.createCollection(
+ "Books",
+ listOf(
+ createTab("https://www.youtube.com", title = "YouTube"),
+ createTab("https://www.amazon.com", title = "Amazon"),
+ ),
+ )
+ storage.createCollection(
+ "News",
+ listOf(
+ createTab("https://www.google.com", title = "Google"),
+ createTab("https://www.facebook.com", title = "Facebook"),
+ ),
+ )
+ storage.createCollection(
+ "Blogs",
+ listOf(
+ createTab("https://www.wikipedia.org", title = "Wikipedia"),
+ ),
+ )
+
+ val collections = storage.getCollectionsList()
+ assertEquals(5, collections.size)
+
+ with(collections[0]) {
+ assertEquals("Blogs", title)
+ assertEquals(1, tabs.size)
+ assertEquals("https://www.wikipedia.org", tabs[0].url)
+ assertEquals("Wikipedia", tabs[0].title)
+ }
+
+ with(collections[1]) {
+ assertEquals("News", title)
+ assertEquals(2, tabs.size)
+ assertEquals("https://www.facebook.com", tabs[0].url)
+ assertEquals("Facebook", tabs[0].title)
+ assertEquals("https://www.google.com", tabs[1].url)
+ assertEquals("Google", tabs[1].title)
+ }
+
+ with(collections[2]) {
+ assertEquals("Books", title)
+ assertEquals(2, tabs.size)
+ assertEquals("https://www.amazon.com", tabs[0].url)
+ assertEquals("Amazon", tabs[0].title)
+ assertEquals("https://www.youtube.com", tabs[1].url)
+ assertEquals("YouTube", tabs[1].title)
+ }
+
+ with(collections[3]) {
+ assertEquals("Recipes", title)
+ assertEquals(1, tabs.size)
+
+ assertEquals("https://www.firefox.com", tabs[0].url)
+ assertEquals("Firefox", tabs[0].title)
+ }
+
+ with(collections[4]) {
+ assertEquals("Articles", title)
+ assertEquals(1, tabs.size)
+
+ assertEquals("https://www.mozilla.org", tabs[0].url)
+ assertEquals("Mozilla", tabs[0].title)
+ }
+ }
+
+ @Test
+ fun testGettingTabCollectionCount() = runTest {
+ assertEquals(0, storage.getTabCollectionsCount())
+
+ storage.createCollection(
+ "Articles",
+ listOf(
+ createTab("https://www.mozilla.org", title = "Mozilla"),
+ ),
+ )
+ storage.createCollection(
+ "Recipes",
+ listOf(
+ createTab("https://www.firefox.com", title = "Firefox"),
+ ),
+ )
+
+ assertEquals(2, storage.getTabCollectionsCount())
+
+ val collections = storage.getCollections().first()
+ assertEquals(2, collections.size)
+
+ storage.removeCollection(collections[0])
+
+ assertEquals(1, storage.getTabCollectionsCount())
+ }
+
+ @Test
+ fun testRemovingAllCollections() {
+ storage.createCollection(
+ "Articles",
+ listOf(
+ createTab("https://www.mozilla.org", title = "Mozilla"),
+ ),
+ )
+ storage.createCollection(
+ "Recipes",
+ listOf(
+ createTab("https://www.firefox.com", title = "Firefox"),
+ ),
+ )
+
+ assertEquals(2, storage.getTabCollectionsCount())
+ assertEquals(2, TabEntity.getStateDirectory(context.filesDir).listFiles()?.size)
+
+ storage.removeAllCollections()
+
+ assertEquals(0, storage.getTabCollectionsCount())
+ assertEquals(0, TabEntity.getStateDirectory(context.filesDir).listFiles()?.size)
+ }
+
+ private fun getAllCollections(): List<TabCollection> {
+ val pagedList = mutableListOf<TabCollection>()
+ storage.getCollectionsPaged().create().map {
+ pagedList.add(it)
+ }
+
+ return pagedList
+ }
+}
+
+/*
+class FakeEngine : Engine {
+ override val version: EngineVersion
+ get() = throw NotImplementedError("Not needed for test")
+
+ override fun createView(context: Context, attrs: AttributeSet?): EngineView =
+ throw UnsupportedOperationException()
+
+ override fun createSession(private: Boolean, contextId: String?): EngineSession =
+ throw UnsupportedOperationException()
+
+ override fun createSessionState(json: JSONObject) = FakeEngineSessionState()
+
+ override fun createSessionStateFrom(reader: JsonReader): EngineSessionState {
+ reader.beginObject()
+ reader.endObject()
+ return FakeEngineSessionState()
+ }
+
+ override fun name(): String =
+ throw UnsupportedOperationException()
+
+ override fun speculativeConnect(url: String) =
+ throw UnsupportedOperationException()
+
+ override val profiler: Profiler?
+ get() = throw NotImplementedError("Not needed for test")
+
+ override val settings: Settings = DefaultSettings()
+}
+
+class FakeEngineSessionState : EngineSessionState {
+ override fun writeTo(writer: JsonWriter) {
+ writer.beginObject()
+ writer.endObject()
+ }
+}
+ */
+
+private fun matches(state: TabSessionState, tab: RecoverableTab) {
+ assertEquals(state.content.url, tab.state.url)
+ assertEquals(state.content.title, tab.state.title)
+ assertEquals(state.id, tab.state.id)
+ assertEquals(state.parentId, tab.state.parentId)
+ assertEquals(state.contextId, tab.state.contextId)
+ assertEquals(state.lastAccess, tab.state.lastAccess)
+ assertEquals(state.readerState, tab.state.readerState)
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/db/TabCollectionDaoTest.kt b/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/db/TabCollectionDaoTest.kt
new file mode 100644
index 0000000000..28bdfae5b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/db/TabCollectionDaoTest.kt
@@ -0,0 +1,158 @@
+/* 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.tab.collections.db
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class TabCollectionDaoTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: TabCollectionDatabase
+ private lateinit var tabCollectionDao: TabCollectionDao
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, TabCollectionDatabase::class.java).build()
+ tabCollectionDao = database.tabCollectionDao()
+ executor = Executors.newSingleThreadExecutor()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+
+ @Test
+ fun testInsertingAndReadingCollections() {
+ val collection1 = TabCollectionEntity(title = "Collection One", updatedAt = 10)
+ val collection2 = TabCollectionEntity(title = "Collection Two", updatedAt = 50)
+
+ tabCollectionDao.insertTabCollection(collection1)
+ tabCollectionDao.insertTabCollection(collection2)
+
+ val pagedList = mutableListOf<TabCollectionWithTabs>()
+ tabCollectionDao.getTabCollectionsPaged().create().map {
+ pagedList.add(it)
+ }
+
+ assertEquals(2, pagedList.size)
+
+ assertEquals("Collection Two", pagedList[1].collection.title)
+ assertEquals("Collection One", pagedList[0].collection.title)
+ }
+
+ @Test
+ fun testUpdatingCollections() {
+ val collection1 = TabCollectionEntity(title = "Collection One", createdAt = 10)
+ val collection2 = TabCollectionEntity(title = "Collection Two", createdAt = 50)
+
+ collection1.id = tabCollectionDao.insertTabCollection(collection1)
+ collection2.id = tabCollectionDao.insertTabCollection(collection2)
+
+ collection1.createdAt = 100
+ collection1.title = "Updated collection"
+
+ tabCollectionDao.updateTabCollection(collection1)
+
+ val pagedList = mutableListOf<TabCollectionWithTabs>()
+ tabCollectionDao.getTabCollectionsPaged().create().map {
+ pagedList.add(it)
+ }
+
+ assertEquals(2, pagedList.size)
+
+ assertEquals("Updated collection", pagedList[0].collection.title)
+ assertEquals("Collection Two", pagedList[1].collection.title)
+ }
+
+ @Test
+ fun testRemovingCollections() {
+ val collection1 = TabCollectionEntity(title = "Collection One", updatedAt = 10)
+ val collection2 = TabCollectionEntity(title = "Collection Two", updatedAt = 50)
+ val collection3 = TabCollectionEntity(title = "Collection Three", updatedAt = 75)
+
+ collection1.id = tabCollectionDao.insertTabCollection(collection1)
+ collection2.id = tabCollectionDao.insertTabCollection(collection2)
+ collection3.id = tabCollectionDao.insertTabCollection(collection3)
+
+ tabCollectionDao.deleteTabCollection(collection2)
+
+ val pagedList = mutableListOf<TabCollectionWithTabs>()
+ tabCollectionDao.getTabCollectionsPaged().create().map {
+ pagedList.add(it)
+ }
+
+ assertEquals(2, pagedList.size)
+
+ assertEquals("Collection Three", pagedList[1].collection.title)
+ assertEquals("Collection One", pagedList[0].collection.title)
+ }
+
+ @Test
+ fun testGettingCollections() = runBlocking {
+ val collection1 = TabCollectionEntity(title = "Collection One", updatedAt = 10)
+ val collection2 = TabCollectionEntity(title = "Collection Two", updatedAt = 50)
+
+ collection1.id = tabCollectionDao.insertTabCollection(collection1)
+ collection2.id = tabCollectionDao.insertTabCollection(collection2)
+
+ val data = tabCollectionDao.getTabCollections()
+
+ val collections = data.first()
+ assertEquals(2, collections.size)
+ assertEquals("Collection Two", collections[1].collection.title)
+ assertEquals("Collection One", collections[0].collection.title)
+ }
+
+ @Test
+ fun testGettingCollectionsList() = runBlocking {
+ val collection1 = TabCollectionEntity(title = "Collection One", updatedAt = 10)
+ val collection2 = TabCollectionEntity(title = "Collection Two", updatedAt = 50)
+
+ collection1.id = tabCollectionDao.insertTabCollection(collection1)
+ collection2.id = tabCollectionDao.insertTabCollection(collection2)
+
+ val tabCollections = tabCollectionDao.getTabCollectionsList()
+
+ assertEquals(2, tabCollections.size)
+ assertEquals("Collection Two", tabCollections[1].collection.title)
+ assertEquals("Collection One", tabCollections[0].collection.title)
+ }
+
+ @Test
+ fun testCountingTabCollections() {
+ assertEquals(0, tabCollectionDao.countTabCollections())
+
+ val collection1 = TabCollectionEntity(title = "Collection One", createdAt = 10)
+ val collection2 = TabCollectionEntity(title = "Collection Two", createdAt = 50)
+
+ collection1.id = tabCollectionDao.insertTabCollection(collection1)
+ collection2.id = tabCollectionDao.insertTabCollection(collection2)
+
+ assertEquals(2, tabCollectionDao.countTabCollections())
+
+ tabCollectionDao.deleteTabCollection(collection2)
+
+ assertEquals(1, tabCollectionDao.countTabCollections())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/db/TabDaoTest.kt b/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/db/TabDaoTest.kt
new file mode 100644
index 0000000000..d6839062cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/db/TabDaoTest.kt
@@ -0,0 +1,115 @@
+/* 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.tab.collections.db
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import java.util.UUID
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class TabDaoTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: TabCollectionDatabase
+ private lateinit var tabCollectionDao: TabCollectionDao
+ private lateinit var tabDao: TabDao
+ private lateinit var executor: ExecutorService
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, TabCollectionDatabase::class.java).build()
+ tabCollectionDao = database.tabCollectionDao()
+ tabDao = database.tabDao()
+ executor = Executors.newSingleThreadExecutor()
+ }
+
+ @Test
+ fun testAddingTabsToCollection() {
+ val collection = TabCollectionEntity(title = "Collection One", createdAt = 10).also {
+ it.id = tabCollectionDao.insertTabCollection(it)
+ }
+
+ val tab1 = TabEntity(
+ title = "Tab One",
+ url = "https://www.mozilla.org",
+ stateFile = UUID.randomUUID().toString(),
+ tabCollectionId = collection.id!!,
+ createdAt = 200,
+ ).also {
+ it.id = tabDao.insertTab(it)
+ }
+
+ val tab2 = TabEntity(
+ title = "Tab Two",
+ url = "https://www.firefox.com",
+ stateFile = UUID.randomUUID().toString(),
+ tabCollectionId = collection.id!!,
+ createdAt = 100,
+ ).also {
+ it.id = tabDao.insertTab(it)
+ }
+
+ val pagedList = mutableListOf<TabCollectionWithTabs>()
+ tabCollectionDao.getTabCollectionsPaged().create().map {
+ pagedList.add(it)
+ }
+
+ assertEquals(1, pagedList.size)
+ assertEquals(2, pagedList[0].tabs.size)
+ assertEquals(tab1, pagedList[0].tabs[0])
+ assertEquals(tab2, pagedList[0].tabs[1])
+ }
+
+ @Test
+ fun testRemovingTabFromCollection() {
+ val collection = TabCollectionEntity(title = "Collection One", createdAt = 10).also {
+ it.id = tabCollectionDao.insertTabCollection(it)
+ }
+
+ val tab1 = TabEntity(
+ title = "Tab One",
+ url = "https://www.mozilla.org",
+ stateFile = UUID.randomUUID().toString(),
+ tabCollectionId = collection.id!!,
+ createdAt = 200,
+ ).also {
+ it.id = tabDao.insertTab(it)
+ }
+
+ val tab2 = TabEntity(
+ title = "Tab Two",
+ url = "https://www.firefox.com",
+ stateFile = UUID.randomUUID().toString(),
+ tabCollectionId = collection.id!!,
+ createdAt = 100,
+ ).also {
+ it.id = tabDao.insertTab(it)
+ }
+
+ tabDao.deleteTab(tab1)
+
+ val pagedList = mutableListOf<TabCollectionWithTabs>()
+ tabCollectionDao.getTabCollectionsPaged().create().map {
+ pagedList.add(it)
+ }
+
+ assertEquals(1, pagedList.size)
+ assertEquals(1, pagedList[0].tabs.size)
+ assertEquals(tab2, pagedList[0].tabs[0])
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/tab-collections/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/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/tab-collections/src/main/java/mozilla/components/feature/tab/collections/Tab.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/Tab.kt
new file mode 100644
index 0000000000..308b897017
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/Tab.kt
@@ -0,0 +1,42 @@
+/* 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.tab.collections
+
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.concept.engine.Engine
+import java.io.File
+
+/**
+ * A tab of a [TabCollection].
+ */
+interface Tab {
+ /**
+ * Unique ID identifying this tab.
+ */
+ val id: Long
+
+ /**
+ * The title of the tab.
+ */
+ val title: String
+
+ /**
+ * The URL of the tab.
+ */
+ val url: String
+
+ /**
+ * Restores a single tab from this collection and returns a matching [RecoverableTab].
+ *
+ * @param restoreSessionId If true the original tab ID will be restored. Otherwise a new ID
+ * will be generated. An app may prefer to use a new ID if it expects sessions to get restored
+ * multiple times - otherwise breaking the promise of a unique ID per tab.
+ */
+ fun restore(
+ filesDir: File,
+ engine: Engine,
+ restoreSessionId: Boolean = false,
+ ): RecoverableTab?
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/TabCollection.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/TabCollection.kt
new file mode 100644
index 0000000000..25652e7c5a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/TabCollection.kt
@@ -0,0 +1,57 @@
+/* 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.tab.collections
+
+import android.content.Context
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.concept.engine.Engine
+
+/**
+ * A collection of tabs.
+ */
+interface TabCollection {
+ /**
+ * Unique ID of this tab collection.
+ */
+ val id: Long
+
+ /**
+ * Title of this tab collection.
+ */
+ val title: String
+
+ /**
+ * List of tabs in this tab collection.
+ */
+ val tabs: List<Tab>
+
+ /**
+ * Restores all tabs in this collection and returns a matching list of [RecoverableTab] objects.
+ *
+ * @param restoreSessionId If true the original ID of the tabs will be restored. Otherwise a new ID
+ * will be generated. An app may prefer to use a new ID if it expects tab to get restored multiple times -
+ * otherwise breaking the promise of a unique ID per tab.
+ */
+ fun restore(
+ context: Context,
+ engine: Engine,
+ restoreSessionId: Boolean = false,
+ ): List<RecoverableTab>
+
+ /**
+ * Restores a subset of the tabs in this collection and returns a matching list of
+ * [RecoverableTab] objects.
+ *
+ * @param restoreSessionId If true the original ID of the tabs will be restored. Otherwise a new ID
+ * will be generated. An app may prefer to use a new ID if it expects tab to get restored multiple times -
+ * otherwise breaking the promise of a unique ID per tab.
+ */
+ fun restoreSubset(
+ context: Context,
+ engine: Engine,
+ tabs: List<Tab>,
+ restoreSessionId: Boolean = false,
+ ): List<RecoverableTab>
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/TabCollectionStorage.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/TabCollectionStorage.kt
new file mode 100644
index 0000000000..6da13eb21b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/TabCollectionStorage.kt
@@ -0,0 +1,170 @@
+/* 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.tab.collections
+
+import android.content.Context
+import androidx.paging.DataSource
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.session.storage.serialize.BrowserStateWriter
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.feature.tab.collections.adapter.TabAdapter
+import mozilla.components.feature.tab.collections.adapter.TabCollectionAdapter
+import mozilla.components.feature.tab.collections.db.TabCollectionDatabase
+import mozilla.components.feature.tab.collections.db.TabCollectionEntity
+import mozilla.components.feature.tab.collections.db.TabEntity
+import mozilla.components.support.ktx.java.io.truncateDirectory
+import java.io.File
+import java.util.UUID
+
+/**
+ * A storage implementation that saves snapshots of tabs / sessions in named collections.
+ */
+class TabCollectionStorage(
+ context: Context,
+ private val writer: BrowserStateWriter = BrowserStateWriter(),
+ private val filesDir: File = context.filesDir,
+) {
+ internal var database: Lazy<TabCollectionDatabase> = lazy { TabCollectionDatabase.get(context) }
+
+ /**
+ * Creates a new [TabCollection] and save the state of the given [TabSessionState]s in it.
+ */
+ fun createCollection(title: String, sessions: List<TabSessionState> = emptyList()): Long? {
+ val entity = TabCollectionEntity(
+ title = title,
+ updatedAt = System.currentTimeMillis(),
+ createdAt = System.currentTimeMillis(),
+ ).also { entity ->
+ entity.id = database.value.tabCollectionDao().insertTabCollection(entity)
+ }
+
+ addTabsToCollection(entity, sessions)
+ return entity.id
+ }
+
+ /**
+ * Adds the state of the given [TabSessionState]s to the [TabCollection].
+ */
+ fun addTabsToCollection(collection: TabCollection, sessions: List<TabSessionState>): Long? {
+ val collectionEntity = (collection as TabCollectionAdapter).entity.collection
+ return addTabsToCollection(collectionEntity, sessions)
+ }
+
+ private fun addTabsToCollection(collection: TabCollectionEntity, sessions: List<TabSessionState>): Long? {
+ sessions.forEach { session ->
+ val fileName = UUID.randomUUID().toString()
+
+ val entity = TabEntity(
+ title = session.content.title,
+ url = session.content.url,
+ stateFile = fileName,
+ tabCollectionId = collection.id!!,
+ createdAt = System.currentTimeMillis(),
+ )
+
+ val success = writer.writeTab(session, entity.getStateFile(filesDir))
+ if (success) {
+ database.value.tabDao().insertTab(entity)
+ }
+ }
+
+ collection.updatedAt = System.currentTimeMillis()
+ database.value.tabCollectionDao().updateTabCollection(collection)
+ return collection.id
+ }
+
+ /**
+ * Removes the given [Tab] from the [TabCollection].
+ */
+ fun removeTabFromCollection(collection: TabCollection, tab: Tab) {
+ val collectionEntity = (collection as TabCollectionAdapter).entity.collection
+ val tabEntity = (tab as TabAdapter).entity
+
+ tabEntity.getStateFile(filesDir)
+ .delete()
+
+ database.value.tabDao().deleteTab(tabEntity)
+
+ collectionEntity.updatedAt = System.currentTimeMillis()
+ database.value.tabCollectionDao().updateTabCollection(collectionEntity)
+ }
+
+ /**
+ * Returns all [TabCollection]s as a [DataSource.Factory].
+ *
+ * A consuming app can transform the data source into a `LiveData<PagedList>` of when using RxJava2 into a
+ * `Flowable<PagedList>` or `Observable<PagedList>`, that can be observed.
+ *
+ * - https://developer.android.com/topic/libraries/architecture/paging/data
+ * - https://developer.android.com/topic/libraries/architecture/paging/ui
+ */
+ fun getCollectionsPaged(): DataSource.Factory<Int, TabCollection> = database.value
+ .tabCollectionDao()
+ .getTabCollectionsPaged()
+ .map { entity -> TabCollectionAdapter(entity) }
+
+ /**
+ * Returns the last [TabCollection] instances as a [Flow] list.
+ */
+ fun getCollections(): Flow<List<TabCollection>> {
+ return database.value.tabCollectionDao().getTabCollections().map { list ->
+ list.map { entity -> TabCollectionAdapter(entity) }
+ }
+ }
+
+ /**
+ * Returns all [TabCollection] instances as a list.
+ */
+ suspend fun getCollectionsList(): List<TabCollection> {
+ return database.value.tabCollectionDao().getTabCollectionsList().map { e ->
+ TabCollectionAdapter(e)
+ }
+ }
+
+ /**
+ * Renames a collection.
+ */
+ fun renameCollection(collection: TabCollection, title: String) {
+ val collectionEntity = (collection as TabCollectionAdapter).entity.collection
+
+ collectionEntity.title = title
+ collectionEntity.updatedAt = System.currentTimeMillis()
+
+ database.value.tabCollectionDao().updateTabCollection(collectionEntity)
+ }
+
+ /**
+ * Removes a collection and all its tabs.
+ */
+ fun removeCollection(collection: TabCollection) {
+ val collectionWithTabs = (collection as TabCollectionAdapter).entity
+
+ database.value
+ .tabCollectionDao()
+ .deleteTabCollection(collectionWithTabs.collection)
+
+ collectionWithTabs.tabs.forEach { tab ->
+ tab.getStateFile(filesDir).delete()
+ }
+ }
+
+ /**
+ * Removes all collections and all tabs.
+ */
+ fun removeAllCollections() {
+ database.value.clearAllTables()
+
+ TabEntity.getStateDirectory(filesDir)
+ .truncateDirectory()
+ }
+
+ /**
+ * Returns the number of tab collections.
+ */
+ fun getTabCollectionsCount(): Int {
+ return database.value.tabCollectionDao().countTabCollections()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/adapter/TabAdapter.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/adapter/TabAdapter.kt
new file mode 100644
index 0000000000..1810dbd4f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/adapter/TabAdapter.kt
@@ -0,0 +1,47 @@
+/* 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.tab.collections.adapter
+
+import mozilla.components.browser.session.storage.serialize.BrowserStateReader
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.tab.collections.Tab
+import mozilla.components.feature.tab.collections.db.TabEntity
+import java.io.File
+
+internal class TabAdapter(
+ val entity: TabEntity,
+) : Tab {
+ override val id: Long
+ get() = entity.id!!
+
+ override val title: String
+ get() = entity.title
+
+ override val url: String
+ get() = entity.url
+
+ override fun restore(
+ filesDir: File,
+ engine: Engine,
+ restoreSessionId: Boolean,
+ ): RecoverableTab? {
+ val reader = BrowserStateReader()
+ val file = entity.getStateFile(filesDir)
+ return reader.readTab(engine, file, restoreSessionId, restoreParentId = false)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is TabAdapter) {
+ return false
+ }
+
+ return entity == other.entity
+ }
+
+ override fun hashCode(): Int {
+ return entity.hashCode()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/adapter/TabCollectionAdapter.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/adapter/TabCollectionAdapter.kt
new file mode 100644
index 0000000000..c4a2b265c8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/adapter/TabCollectionAdapter.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.tab.collections.adapter
+
+import android.content.Context
+import mozilla.components.browser.session.storage.serialize.BrowserStateReader
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.tab.collections.Tab
+import mozilla.components.feature.tab.collections.TabCollection
+import mozilla.components.feature.tab.collections.db.TabCollectionWithTabs
+import mozilla.components.feature.tab.collections.db.TabEntity
+
+internal class TabCollectionAdapter(
+ internal val entity: TabCollectionWithTabs,
+) : TabCollection {
+ override val title: String
+ get() = entity.collection.title
+
+ override val tabs: List<Tab> by lazy {
+ entity
+ .tabs
+ .sortedByDescending { it.createdAt }
+ .map { TabAdapter(it) }
+ }
+
+ override val id: Long
+ get() = entity.collection.id!!
+
+ override fun restore(
+ context: Context,
+ engine: Engine,
+ restoreSessionId: Boolean,
+ ): List<RecoverableTab> {
+ return restore(context, engine, entity.tabs, restoreSessionId)
+ }
+
+ override fun restoreSubset(
+ context: Context,
+ engine: Engine,
+ tabs: List<Tab>,
+ restoreSessionId: Boolean,
+ ): List<RecoverableTab> {
+ val entities = entity.tabs.filter {
+ candidate ->
+ tabs.find { tab -> tab.id == candidate.id } != null
+ }
+ return restore(context, engine, entities, restoreSessionId)
+ }
+
+ private fun restore(
+ context: Context,
+ engine: Engine,
+ tabs: List<TabEntity>,
+ restoreSessionId: Boolean,
+ ): List<RecoverableTab> {
+ val reader = BrowserStateReader()
+ return tabs.mapNotNull { tab ->
+ val file = tab.getStateFile(context.filesDir)
+ reader.readTab(engine, file, restoreSessionId, restoreParentId = false)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is TabCollectionAdapter) {
+ return false
+ }
+
+ return entity == other.entity
+ }
+
+ override fun hashCode(): Int {
+ return entity.hashCode()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionDao.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionDao.kt
new file mode 100644
index 0000000000..482a8d44ff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionDao.kt
@@ -0,0 +1,63 @@
+/* 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.tab.collections.db
+
+import androidx.paging.DataSource
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Update
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Internal DAO for accessing [TabCollectionEntity] instances.
+ */
+@Dao
+internal interface TabCollectionDao {
+ @Insert
+ fun insertTabCollection(collection: TabCollectionEntity): Long
+
+ @Delete
+ fun deleteTabCollection(collection: TabCollectionEntity)
+
+ @Update
+ fun updateTabCollection(collection: TabCollectionEntity)
+
+ @Transaction
+ @Query(
+ """
+ SELECT tab_collections.id, tab_collections.title, tab_collections.created_at, tab_collections.updated_at
+ FROM tab_collections LEFT JOIN tabs ON tab_collections.id = tab_collection_id
+ GROUP BY tab_collections.id
+ ORDER BY tab_collections.created_at DESC
+ """,
+ )
+ fun getTabCollectionsPaged(): DataSource.Factory<Int, TabCollectionWithTabs>
+
+ @Transaction
+ @Query(
+ """
+ SELECT *
+ FROM tab_collections
+ ORDER BY created_at DESC
+ """,
+ )
+ fun getTabCollections(): Flow<List<TabCollectionWithTabs>>
+
+ @Transaction
+ @Query(
+ """
+ SELECT *
+ FROM tab_collections
+ ORDER BY created_at DESC
+ """,
+ )
+ suspend fun getTabCollectionsList(): List<TabCollectionWithTabs>
+
+ @Query("SELECT COUNT(*) FROM tab_collections")
+ fun countTabCollections(): Int
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionDatabase.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionDatabase.kt
new file mode 100644
index 0000000000..3000b35721
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionDatabase.kt
@@ -0,0 +1,36 @@
+/* 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.tab.collections.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+/**
+ * Internal database for storing collections and their tabs.
+ */
+@Database(entities = [TabCollectionEntity::class, TabEntity::class], version = 1)
+internal abstract class TabCollectionDatabase : RoomDatabase() {
+ abstract fun tabCollectionDao(): TabCollectionDao
+ abstract fun tabDao(): TabDao
+
+ companion object {
+ @Volatile private var instance: TabCollectionDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): TabCollectionDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ TabCollectionDatabase::class.java,
+ "tab_collections",
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionEntity.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionEntity.kt
new file mode 100644
index 0000000000..dbc2d4cf05
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionEntity.kt
@@ -0,0 +1,28 @@
+/* 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.tab.collections.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * Internal entity representing a collection of tabs.
+ */
+@Entity(tableName = "tab_collections")
+internal data class TabCollectionEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var id: Long? = null,
+
+ @ColumnInfo(name = "title")
+ var title: String,
+
+ @ColumnInfo(name = "updated_at")
+ var updatedAt: Long = System.currentTimeMillis(),
+
+ @ColumnInfo(name = "created_at")
+ var createdAt: Long = System.currentTimeMillis(),
+)
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionWithTabs.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionWithTabs.kt
new file mode 100644
index 0000000000..25ca4fe2a7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabCollectionWithTabs.kt
@@ -0,0 +1,19 @@
+/* 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.tab.collections.db
+
+import androidx.room.Embedded
+import androidx.room.Relation
+
+/**
+ * Class representing a [TabCollectionEntity] joined with its [TabEntity] instances.
+ */
+internal class TabCollectionWithTabs {
+ @Embedded
+ lateinit var collection: TabCollectionEntity
+
+ @Relation(parentColumn = "id", entityColumn = "tab_collection_id", entity = TabEntity::class)
+ lateinit var tabs: List<TabEntity>
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabDao.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabDao.kt
new file mode 100644
index 0000000000..69d3d3d410
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabDao.kt
@@ -0,0 +1,21 @@
+/* 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.tab.collections.db
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+
+/**
+ * Internal DAO for accessing [TabEntity] instances.
+ */
+@Dao
+internal interface TabDao {
+ @Insert
+ fun insertTab(tab: TabEntity): Long
+
+ @Delete
+ fun deleteTab(tab: TabEntity)
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabEntity.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabEntity.kt
new file mode 100644
index 0000000000..87ec9a2870
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/db/TabEntity.kt
@@ -0,0 +1,63 @@
+/* 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.tab.collections.db
+
+import android.util.AtomicFile
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import java.io.File
+
+/**
+ * Internal entity representing a tab that is part of a collection.
+ */
+@Entity(
+ tableName = "tabs",
+ foreignKeys = [
+ ForeignKey(
+ entity = TabCollectionEntity::class,
+ parentColumns = ["id"],
+ childColumns = ["tab_collection_id"],
+ onDelete = ForeignKey.CASCADE,
+ ),
+ ],
+ indices = [
+ Index(value = ["tab_collection_id"]),
+ ],
+)
+internal data class TabEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var id: Long? = null,
+
+ @ColumnInfo(name = "title")
+ var title: String,
+
+ @ColumnInfo(name = "url")
+ var url: String,
+
+ @ColumnInfo(name = "stat_file")
+ var stateFile: String,
+
+ @ColumnInfo(name = "tab_collection_id")
+ var tabCollectionId: Long,
+
+ @ColumnInfo(name = "created_at")
+ var createdAt: Long,
+) {
+ internal fun getStateFile(filesDir: File): AtomicFile {
+ return AtomicFile(File(getStateDirectory(filesDir), stateFile))
+ }
+
+ companion object {
+ internal fun getStateDirectory(filesDir: File): File {
+ return File(filesDir, "mozac.feature.tab.collections").apply {
+ mkdirs()
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/ext/TabsUseCases.kt b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/ext/TabsUseCases.kt
new file mode 100644
index 0000000000..412fdca6c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/main/java/mozilla/components/feature/tab/collections/ext/TabsUseCases.kt
@@ -0,0 +1,86 @@
+/* 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.tab.collections.ext
+
+import mozilla.components.browser.state.action.LastAccessAction
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.tab.collections.Tab
+import mozilla.components.feature.tab.collections.TabCollection
+import mozilla.components.feature.tabs.TabsUseCases
+import java.io.File
+
+/**
+ * Restores the given [Tab] from a [TabCollection]. Will invoke [onTabRestored] on successful restore
+ * and [onFailure] otherwise.
+ *
+ * Will update the last accessed property of the tab if [updateLastAccess] is true.
+ */
+operator fun TabsUseCases.RestoreUseCase.invoke(
+ filesDir: File,
+ engine: Engine,
+ tab: Tab,
+ updateLastAccess: Boolean = true,
+ onTabRestored: (String) -> Unit,
+ onFailure: () -> Unit,
+) {
+ val item = tab.restore(
+ filesDir = filesDir,
+ engine = engine,
+ restoreSessionId = false,
+ )
+
+ if (item == null) {
+ // We were unable to restore the tab. Let the app know so that it can workaround that
+ onFailure()
+ } else {
+ invoke(listOf(item), item.state.id)
+
+ if (updateLastAccess) {
+ store.dispatch(LastAccessAction.UpdateLastAccessAction(item.state.id))
+ }
+
+ onTabRestored(item.state.id)
+ }
+}
+
+/**
+ * Restores the given [TabCollection].
+ *
+ * Will invoke [onFailure] if restoring a single [Tab] of the collection failed. The URL of the
+ * tab will be passed to [onFailure].
+ *
+ * Will update the last accessed property of the tab if [updateLastAccess] is true.
+ */
+operator fun TabsUseCases.RestoreUseCase.invoke(
+ filesDir: File,
+ engine: Engine,
+ collection: TabCollection,
+ updateLastAccess: Boolean = true,
+ onFailure: (String) -> Unit,
+) {
+ val tabs = collection.tabs.reversed().mapNotNull { tab ->
+ val recoverableTab = tab.restore(filesDir, engine, restoreSessionId = false)
+ if (recoverableTab == null) {
+ // We were unable to restore the tab. Let the app know so that it can workaround that
+ onFailure(tab.url)
+ }
+ recoverableTab
+ }
+
+ if (tabs.isEmpty()) {
+ return
+ }
+
+ invoke(tabs, selectTabId = tabs.firstOrNull()?.state?.id)
+
+ if (!updateLastAccess) {
+ return
+ }
+
+ val restoredTabIds = tabs.map { it.state.id }
+ restoredTabIds.forEach { tabId ->
+ store.dispatch(LastAccessAction.UpdateLastAccessAction(tabId))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/test/java/mozilla/components/feature/tab/collections/ext/TabsUseCasesKtTest.kt b/mobile/android/android-components/components/feature/tab-collections/src/test/java/mozilla/components/feature/tab/collections/ext/TabsUseCasesKtTest.kt
new file mode 100644
index 0000000000..0a4f7d902d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/test/java/mozilla/components/feature/tab/collections/ext/TabsUseCasesKtTest.kt
@@ -0,0 +1,94 @@
+/* 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.tab.collections.ext
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.toRecoverableTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.feature.tab.collections.Tab
+import mozilla.components.feature.tab.collections.TabCollection
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertNotEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import java.io.File
+
+class TabsUseCasesKtTest {
+
+ private lateinit var store: BrowserStore
+ private lateinit var tabsUseCases: TabsUseCases
+ private lateinit var engine: Engine
+ private lateinit var engineSession: EngineSession
+
+ private lateinit var collection: TabCollection
+ private lateinit var tab: Tab
+ private lateinit var filesDir: File
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ engineSession = mock()
+ engine = mock()
+ filesDir = mock()
+ whenever(filesDir.path).thenReturn("/test")
+
+ whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
+ store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ ),
+ )
+ tabsUseCases = TabsUseCases(store)
+
+ val recoveredTab = createTab(
+ id = "123",
+ url = "https://mozilla.org",
+ lastAccess = 3735928559L,
+ ).toRecoverableTab()
+
+ tab = mock<Tab>().apply {
+ whenever(id).thenReturn(123)
+ whenever(title).thenReturn("Firefox")
+ whenever(url).thenReturn("https://firefox.com")
+ whenever(restore(filesDir, engine, false)).thenReturn(recoveredTab)
+ }
+ collection = mock<TabCollection>().apply {
+ whenever(tabs).thenReturn(listOf(tab))
+ }
+ }
+
+ @Test
+ fun `RestoreUseCase updates last access when restoring collection`() {
+ tabsUseCases.restore.invoke(filesDir, engine, collection) {}
+
+ store.waitUntilIdle()
+
+ assertNotEquals(3735928559L, store.state.findTab("123")!!.lastAccess)
+ }
+
+ @Test
+ fun `RestoreUseCase updates last access when restoring single tab in collection`() {
+ tabsUseCases.restore.invoke(filesDir, engine, tab, onTabRestored = {}, onFailure = {})
+
+ store.waitUntilIdle()
+
+ assertNotEquals(3735928559L, store.state.findTab("123")!!.lastAccess)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tab-collections/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/tab-collections/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/tab-collections/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/tab-collections/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/tab-collections/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tab-collections/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/tabs/README.md b/mobile/android/android-components/components/feature/tabs/README.md
new file mode 100644
index 0000000000..8cc92ed781
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/README.md
@@ -0,0 +1,21 @@
+# [Android Components](../../../README.md) > Feature > Tabs
+
+A component that connects a trabs tray implementation with the session and toolbar modules.
+
+## Usage
+
+See the [`ui/tabcounter` component](../../ui/tabcounter/README.md) instructions on how to style the counter.
+
+### 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-tabs:{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/tabs/build.gradle b/mobile/android/android-components/components/feature/tabs/build.gradle
new file mode 100644
index 0000000000..c233b83daf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/build.gradle
@@ -0,0 +1,67 @@
+/* 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'
+
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.tabs'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += [
+ "-opt-in=kotlinx.coroutines.FlowPreview",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+ ]
+}
+
+dependencies {
+ implementation project(':browser-state')
+ implementation project(':browser-session-storage')
+ implementation project(':browser-thumbnails')
+ implementation project(':browser-tabstray')
+ api project(':feature-session')
+ implementation project(':concept-engine')
+ implementation project(':concept-tabstray')
+ implementation project(':concept-toolbar')
+ implementation project(':concept-menu')
+ implementation project(':ui-icons')
+ implementation project(':ui-tabcounter')
+ implementation project(':support-ktx')
+
+ implementation ComponentsDependencies.androidx_fragment
+ implementation ComponentsDependencies.androidx_recyclerview
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ // In tests we are constructing our own SessionManager instance which needs to know about an "engine".
+ testImplementation project(':concept-engine')
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(":support-test-libstate")
+}
+
+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/tabs/proguard-rules.pro b/mobile/android/android-components/components/feature/tabs/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/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/tabs/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/tabs/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/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/tabs/src/main/java/mozilla/components/feature/tabs/CustomTabsUseCases.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/CustomTabsUseCases.kt
new file mode 100644
index 0000000000..b2e17309a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/CustomTabsUseCases.kt
@@ -0,0 +1,135 @@
+/* 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.tabs
+
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.session.SessionUseCases
+
+/**
+ * UseCases for custom tabs.
+ */
+class CustomTabsUseCases(
+ store: BrowserStore,
+ loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
+) {
+ /**
+ * Use case for adding a new custom tab.
+ */
+ class AddCustomTabUseCase(
+ private val store: BrowserStore,
+ private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
+ ) {
+ /**
+ * Adds a new custom tab with URL [url].
+ */
+ operator fun invoke(
+ url: String,
+ customTabConfig: CustomTabConfig,
+ private: Boolean = false,
+ additionalHeaders: Map<String, String>? = null,
+ source: SessionState.Source,
+ ): String {
+ val loadUrlFlags = EngineSession.LoadUrlFlags.external()
+ val tab = createCustomTab(
+ url = url,
+ private = private,
+ source = source,
+ config = customTabConfig,
+ initialLoadFlags = loadUrlFlags,
+ )
+
+ store.dispatch(CustomTabListAction.AddCustomTabAction(tab))
+ loadUrlUseCase(url, tab.id, loadUrlFlags, additionalHeaders)
+ return tab.id
+ }
+ }
+
+ /**
+ * Use case for adding a new Web App tab.
+ */
+ class AddWebAppTabUseCase(
+ private val store: BrowserStore,
+ private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
+ ) {
+ /**
+ * Adds a new web app tab with the given manifest.
+ */
+ operator fun invoke(
+ url: String,
+ source: SessionState.Source,
+ customTabConfig: CustomTabConfig,
+ webAppManifest: WebAppManifest,
+ ): String {
+ val loadUrlFlags = EngineSession.LoadUrlFlags.external()
+ val tab = createCustomTab(
+ url = url,
+ source = source,
+ config = customTabConfig,
+ webAppManifest = webAppManifest,
+ initialLoadFlags = loadUrlFlags,
+ )
+
+ store.dispatch(CustomTabListAction.AddCustomTabAction(tab))
+ loadUrlUseCase(url, tab.id, loadUrlFlags)
+ return tab.id
+ }
+ }
+
+ /**
+ * Use case for removing a custom tab.
+ */
+ class RemoveCustomTabUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Removes the custom tab with the given [customTabId].
+ */
+ operator fun invoke(customTabId: String): Boolean {
+ val tab = store.state.findCustomTab(customTabId)
+ if (tab != null) {
+ store.dispatch(CustomTabListAction.RemoveCustomTabAction(tab.id))
+ return true
+ }
+ return false
+ }
+ }
+
+ /**
+ * Use case for migrating a custom tab to a regular tab.
+ */
+ class MigrateCustomTabUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Migrates the custom tab with the given [customTabId] to a regular
+ * tab. This method has no effect if the custom tab does not exist.
+ *
+ * @param customTabId the custom tab to turn into a regular tab.
+ * @param select whether or not to select the regular tab, defaults to true.
+ */
+ operator fun invoke(customTabId: String, select: Boolean = true) {
+ store.dispatch(
+ CustomTabListAction.TurnCustomTabIntoNormalTabAction(customTabId),
+ )
+
+ if (select) {
+ store.dispatch(TabListAction.SelectTabAction(customTabId))
+ }
+ }
+ }
+
+ val add by lazy { AddCustomTabUseCase(store, loadUrlUseCase) }
+ val remove by lazy { RemoveCustomTabUseCase(store) }
+ val migrate by lazy { MigrateCustomTabUseCase(store) }
+ val addWebApp by lazy { AddWebAppTabUseCase(store, loadUrlUseCase) }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt
new file mode 100644
index 0000000000..6c5fe387b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt
@@ -0,0 +1,547 @@
+/* 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.tabs
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.browser.session.storage.RecoverableBrowserState
+import mozilla.components.browser.session.storage.SessionStorage
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.RestoreCompleteAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.UndoAction
+import mozilla.components.browser.state.selector.findNormalOrPrivateTabByUrl
+import mozilla.components.browser.state.selector.findNormalOrPrivateTabByUrlIgnoringFragment
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.concept.engine.EngineSessionStateStorage
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.feature.session.SessionUseCases.LoadUrlUseCase
+
+/**
+ * Contains use cases related to the tabs feature.
+ */
+class TabsUseCases(
+ store: BrowserStore,
+) {
+ /**
+ * Contract for use cases that select a tab.
+ */
+ interface SelectTabUseCase {
+ /**
+ * Select tab with the given [tabId].
+ */
+ operator fun invoke(tabId: String)
+ }
+
+ class DefaultSelectTabUseCase internal constructor(
+ private val store: BrowserStore,
+ ) : SelectTabUseCase {
+ /**
+ * Marks the tab with the provided [tabId] as selected.
+ */
+ override fun invoke(tabId: String) {
+ store.dispatch(TabListAction.SelectTabAction(tabId))
+ }
+ }
+
+ /**
+ * Contract for use cases that remove a tab.
+ */
+ interface RemoveTabUseCase {
+ /**
+ * Removes the session with the provided ID. This method
+ * has no effect if the session doesn't exist.
+ *
+ * @param tabId The ID of the session to remove.
+ */
+ operator fun invoke(tabId: String)
+
+ /**
+ * Removes the session with the provided ID. This method
+ * has no effect if the session doesn't exist.
+ *
+ * @param tabId The ID of the session to remove.
+ * @param selectParentIfExists Whether or not to select the parent tab
+ * of the removed tab if a parent exists. Note that the default implementation
+ * of this method will ignore [selectParentIfExists] and never select a parent.
+ * This is a temporary workaround to prevent additional API breakage for
+ * subtypes other than [DefaultRemoveTabUseCase]. The default implementation
+ * should be removed together with invoke(Session).
+ */
+ operator fun invoke(tabId: String, selectParentIfExists: Boolean) = invoke(tabId)
+ }
+
+ /**
+ * Default implementation of [RemoveTabUseCase].
+ */
+ class DefaultRemoveTabUseCase internal constructor(
+ private val store: BrowserStore,
+ ) : RemoveTabUseCase {
+
+ /**
+ * Removes the tab with the provided ID. This method
+ * has no effect if the tab doesn't exist.
+ *
+ * @param tabId The ID of the tab to remove.
+ */
+ override operator fun invoke(tabId: String) {
+ store.dispatch(TabListAction.RemoveTabAction(tabId))
+ }
+
+ /**
+ * Removes the session with the provided ID. This method
+ * has no effect if the session doesn't exist.
+ *
+ * @param tabId The ID of the session to remove.
+ * @param selectParentIfExists Whether or not to select the parent tab
+ * of the removed tab if a parent exists.
+ */
+ override operator fun invoke(tabId: String, selectParentIfExists: Boolean) {
+ store.dispatch(TabListAction.RemoveTabAction(tabId, selectParentIfExists))
+ }
+ }
+
+ class AddNewTabUseCase internal constructor(
+ private val store: BrowserStore,
+ ) : LoadUrlUseCase {
+
+ /**
+ * Adds a new tab and loads the provided URL.
+ *
+ * @param url The URL to be loaded in the new tab.
+ * @param flags the [LoadUrlFlags] to use when loading the provided URL.
+ */
+ override fun invoke(
+ url: String,
+ flags: LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {
+ this.invoke(url, selectTab = true, startLoading = true, parentId = null, flags = flags)
+ }
+
+ /**
+ * Adds a new tab and loads the provided URL.
+ *
+ * @param url The URL to be loaded in the new tab.
+ * @param selectTab True (default) if the new tab should be selected immediately.
+ * @param startLoading True (default) if the new tab should start loading immediately.
+ * @param parentId the id of the parent tab to use for the newly created tab.
+ * @param flags the [LoadUrlFlags] to use when loading the provided URL.
+ * @param contextId the session context id to use for this tab.
+ * @param engineSession (optional) engine session to use for this tab.
+ * @param source The [SessionState.Source] of the new tab.
+ * @param searchTerms The search terms of this new tab if it represents an active
+ * search (result) page.
+ * @param private Whether or not the new tab should be private.
+ * @param historyMetadata the [HistoryMetadataKey] of the new tab in case this tab
+ * was opened from history.
+ * @param isSearch whether or not the provided URL is the result of a search.
+ * @param searchEngineName The search engine name.
+ * @param additionalHeaders The extra headers to use when loading the provided URL.
+ * @return The ID of the created tab.
+ */
+ operator fun invoke(
+ url: String = "about:blank",
+ selectTab: Boolean = true,
+ startLoading: Boolean = true,
+ parentId: String? = null,
+ flags: LoadUrlFlags = LoadUrlFlags.none(),
+ contextId: String? = null,
+ engineSession: EngineSession? = null,
+ source: SessionState.Source = SessionState.Source.Internal.NewTab,
+ searchTerms: String = "",
+ private: Boolean = false,
+ historyMetadata: HistoryMetadataKey? = null,
+ isSearch: Boolean = false,
+ searchEngineName: String? = null,
+ additionalHeaders: Map<String, String>? = null,
+ ): String {
+ val tab = createTab(
+ url = url,
+ private = private,
+ source = source,
+ contextId = contextId,
+ parent = parentId?.let { store.state.findTab(it) },
+ engineSession = engineSession,
+ searchTerms = searchTerms,
+ initialLoadFlags = flags,
+ initialAdditionalHeaders = additionalHeaders,
+ historyMetadata = historyMetadata,
+ )
+
+ store.dispatch(TabListAction.AddTabAction(tab, select = selectTab))
+
+ store.dispatch(ContentAction.UpdateIsSearchAction(tab.id, isSearch, searchEngineName))
+
+ // If an engine session is specified then loading will have already started when linking
+ // the tab to its engine session. Otherwise we ask to load the URL here.
+ if (startLoading && engineSession == null) {
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ tabId = tab.id,
+ url = url,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ ),
+ )
+ }
+
+ return tab.id
+ }
+ }
+
+ /**
+ * Use case for removing a list of tabs.
+ */
+ class RemoveTabsUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Removes a specified list of tabs.
+ */
+ operator fun invoke(ids: List<String>) {
+ store.dispatch(TabListAction.RemoveTabsAction(ids))
+ }
+ }
+
+ class RemoveAllTabsUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Removes all tabs.
+ * @param recoverable Indicates whether removed tabs should be recoverable.
+ */
+ operator fun invoke(recoverable: Boolean = true) {
+ store.dispatch(TabListAction.RemoveAllTabsAction(recoverable))
+ }
+ }
+
+ /**
+ * Use case for removing all normal (non-private) tabs.
+ */
+ class RemoveNormalTabsUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Removes all normal (non-private) tabs.
+ */
+ operator fun invoke() {
+ store.dispatch(TabListAction.RemoveAllNormalTabsAction)
+ }
+ }
+
+ /**
+ * Use case for removing all private tabs.
+ */
+ class RemovePrivateTabsUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Removes all private tabs.
+ */
+ operator fun invoke() {
+ store.dispatch(TabListAction.RemoveAllPrivateTabsAction)
+ }
+ }
+
+ /**
+ * Use case for restoring removed tabs ("undo").
+ */
+ class UndoTabRemovalUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Restores the list of tabs in the undo history.
+ */
+ operator fun invoke() {
+ store.dispatch(UndoAction.RestoreRecoverableTabs)
+ }
+ }
+
+ /**
+ * Use case for restoring tabs.
+ */
+ class RestoreUseCase(
+ val store: BrowserStore,
+ private val selectTab: SelectTabUseCase,
+ ) {
+ /**
+ * Restores the given list of [RecoverableTab]s.
+ */
+ operator fun invoke(tabs: List<RecoverableTab>, selectTabId: String? = null) {
+ store.dispatch(
+ TabListAction.RestoreAction(
+ tabs,
+ selectTabId,
+ TabListAction.RestoreAction.RestoreLocation.BEGINNING,
+ ),
+ )
+ }
+
+ /**
+ * Restores the given [RecoverableBrowserState].
+ */
+ operator fun invoke(state: RecoverableBrowserState) {
+ invoke(state.tabs, state.selectedTabId)
+ }
+
+ /**
+ * Restores the browsing session from the given [SessionStorage]. Also dispatches
+ * [RestoreCompleteAction] on the [BrowserStore] once restore has been completed.
+ *
+ * @param storage the [SessionStorage] to restore state from.
+ * @param tabTimeoutInMs the amount of time in milliseconds after which inactive
+ * tabs will be discarded and not restored. Defaults to Long.MAX_VALUE, meaning
+ * all tabs will be restored by default.
+ */
+ suspend operator fun invoke(
+ storage: SessionStorage,
+ tabTimeoutInMs: Long = Long.MAX_VALUE,
+ ) = withContext(Dispatchers.IO) {
+ val now = System.currentTimeMillis()
+ val state = storage.restore {
+ val lastActiveTime = maxOf(it.state.lastAccess, it.state.createdAt)
+ now - lastActiveTime <= tabTimeoutInMs
+ }
+ if (state != null) {
+ withContext(Dispatchers.Main) {
+ invoke(state)
+ }
+ }
+ store.dispatch(RestoreCompleteAction)
+ }
+
+ /**
+ * Restores the given [TabState] and updates the selected tab if [updateSelection] is
+ * `true`.
+ */
+ suspend operator fun invoke(
+ tab: TabState,
+ engineStateReader: EngineSessionStateStorage,
+ updateSelection: Boolean = true,
+ ) = withContext(Dispatchers.IO) {
+ val recoverableTab = RecoverableTab(
+ state = tab,
+ engineSessionState = engineStateReader.read(tab.id),
+ )
+ invoke(listOf(recoverableTab))
+
+ if (updateSelection) {
+ selectTab(recoverableTab.state.id)
+ }
+ }
+ }
+
+ /**
+ * Use case for selecting an existing tab or creating a new tab with a specific URL.
+ */
+ class SelectOrAddUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Selects an already existing tab with the matching [HistoryMetadataKey] or otherwise
+ * creates a new tab with the given [url].
+ *
+ * @param url The URL to be selected or loaded in the new tab.
+ * @param historyMetadata The [HistoryMetadataKey] to match for existing tabs.
+ * @return The ID of the selected or created tab.
+ */
+ operator fun invoke(
+ url: String,
+ historyMetadata: HistoryMetadataKey,
+ ): String {
+ val tab = store.state.tabs.find { it.historyMetadata == historyMetadata }
+
+ return if (tab != null) {
+ store.dispatch(TabListAction.SelectTabAction(tab.id))
+ tab.id
+ } else {
+ this.invoke(url)
+ }
+ }
+
+ /**
+ * Selects an already existing tab displaying [url] or otherwise creates a new tab.
+ *
+ * @param url The URL to be loaded in the new tab.
+ * @param private Whether or not this session should use private mode.
+ * @param source The origin of a session to describe how and why it was created.
+ * @param flags The [LoadUrlFlags] to use when loading the provided URL.
+ * @param ignoreFragment Whether to ignore the fragment identifier of the url while
+ * comparing with existing tabs.
+ * @return The ID of the selected or created tab.
+ */
+ operator fun invoke(
+ url: String,
+ private: Boolean = false,
+ source: SessionState.Source = SessionState.Source.Internal.NewTab,
+ flags: LoadUrlFlags = LoadUrlFlags.none(),
+ ignoreFragment: Boolean = false,
+ ): String {
+ val existingTab = if (ignoreFragment) {
+ store.state.findNormalOrPrivateTabByUrlIgnoringFragment(url, private)
+ } else {
+ store.state.findNormalOrPrivateTabByUrl(url, private)
+ }
+
+ return if (existingTab != null) {
+ store.dispatch(TabListAction.SelectTabAction(existingTab.id))
+ existingTab.id
+ } else {
+ val tab = createTab(
+ url = url,
+ private = private,
+ source = source,
+ initialLoadFlags = flags,
+ )
+ store.dispatch(TabListAction.AddTabAction(tab, select = true))
+ store.dispatch(
+ EngineAction.LoadUrlAction(
+ tab.id,
+ url,
+ flags,
+ ),
+ )
+ tab.id
+ }
+ }
+ }
+
+ /**
+ * Use case for duplicating a tab.
+ */
+ class DuplicateTabUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Creates a duplicate of the currently selected tab (including history) and
+ * selects it if [selectNewTab] is true.
+ *
+ * @param selectNewTab Whether or not the duplicate tab should be selected.
+ * @return The ID of the duplicated tab, or null if there is no selected tab.
+ */
+ fun invoke(selectNewTab: Boolean): String? {
+ return store.state.selectedTab?.let {
+ this.invoke(it, selectNewTab)
+ }
+ }
+
+ /**
+ * Creates a duplicate of [tab] (including history) and selects it if [selectNewTab] is true.
+ *
+ * @param tab The tab to duplicate.
+ * @param selectNewTab Whether or not the duplicate tab should be selected.
+ * @return The ID of the duplicated tab.
+ */
+ operator fun invoke(
+ tab: TabSessionState,
+ selectNewTab: Boolean = true,
+ ): String {
+ val duplicate = createTab(
+ url = tab.content.url,
+ private = tab.content.private,
+ contextId = tab.contextId,
+ parent = tab,
+ engineSessionState = tab.engineState.engineSessionState,
+ )
+
+ store.dispatch(
+ TabListAction.AddTabAction(
+ duplicate,
+ select = selectNewTab,
+ ),
+ )
+ return duplicate.id
+ }
+ }
+
+ /**
+ * Use case for moving a collection of tabs.
+ */
+ class MoveTabsUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Moves the tabs of [tabIds] next to [targetTabId], before/after based on [placeAfter]
+ *
+ * @property tabIds The IDs of the tabs to move.
+ * @property targetTabId A tab that the moved tabs will be moved next to
+ * @property placeAfter True if the moved tabs should be placed after the target,
+ * False for placing before the target. Irrelevant if the target is one of the tabs being moved,
+ * since then the whole list is moved to where the target was. Ordering of the moved tabs
+ * relative to each other is preserved.
+ */
+ operator fun invoke(
+ tabIds: List<String>,
+ targetTabId: String,
+ placeAfter: Boolean,
+ ) {
+ store.dispatch(
+ TabListAction.MoveTabsAction(
+ tabIds,
+ targetTabId,
+ placeAfter,
+ ),
+ )
+ }
+ }
+
+ /**
+ * Use case for reopening a private tab as a regular (ie, non-private) tab.
+ *
+ * To avoid complications with tab parenting etc (ie, to avoid the scenario where
+ * private tabs are parented by non-private tabs) this is not a "move" operation
+ * but instead more of a "close + open" operation.
+ */
+ class MigratePrivateTabUseCase(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * @param tabId the ID of the session to move.
+ * @param alternativeUrl url to load. If not specified the URL from the tab will be used.
+ * @return the ID of the tab that was re-created as part of the move.
+ */
+ operator fun invoke(
+ tabId: String,
+ alternativeUrl: String? = null,
+ ): String {
+ val tab = store.state.findTab(tabId) ?: throw IllegalStateException("Tab does not exist.")
+
+ require(tab.content.private) { "The tab we are trying to move is not private." }
+
+ val url = alternativeUrl ?: tab.content.url
+ val newTab = createTab(url)
+
+ store.dispatch(TabListAction.RemoveTabAction(tabId, false))
+ store.dispatch(TabListAction.AddTabAction(newTab, true))
+
+ return newTab.id
+ }
+ }
+
+ val selectTab: SelectTabUseCase by lazy { DefaultSelectTabUseCase(store) }
+ val removeTab: RemoveTabUseCase by lazy { DefaultRemoveTabUseCase(store) }
+ val addTab: AddNewTabUseCase by lazy { AddNewTabUseCase(store) }
+ val removeAllTabs: RemoveAllTabsUseCase by lazy { RemoveAllTabsUseCase(store) }
+ val removeTabs: RemoveTabsUseCase by lazy { RemoveTabsUseCase(store) }
+ val removeNormalTabs: RemoveNormalTabsUseCase by lazy { RemoveNormalTabsUseCase(store) }
+ val removePrivateTabs: RemovePrivateTabsUseCase by lazy { RemovePrivateTabsUseCase(store) }
+ val undo by lazy { UndoTabRemovalUseCase(store) }
+ val restore: RestoreUseCase by lazy { RestoreUseCase(store, selectTab) }
+ val selectOrAddTab: SelectOrAddUseCase by lazy { SelectOrAddUseCase(store) }
+ val duplicateTab: DuplicateTabUseCase by lazy { DuplicateTabUseCase(store) }
+ val moveTabs: MoveTabsUseCase by lazy { MoveTabsUseCase(store) }
+ val migratePrivateTabUseCase: MigratePrivateTabUseCase by lazy { MigratePrivateTabUseCase(store) }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/WindowFeature.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/WindowFeature.kt
new file mode 100644
index 0000000000..65e6328433
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/WindowFeature.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.tabs
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
+
+/**
+ * Feature implementation for handling window requests by opening and closing tabs.
+ */
+class WindowFeature(
+ private val store: BrowserStore,
+ private val tabsUseCases: TabsUseCases,
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Starts observing the selected session to listen for window requests
+ * and opens / closes tabs as needed.
+ */
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.tabs }
+ .filterChanged {
+ it.content.windowRequest
+ }
+ .collect { state ->
+ val windowRequest = state.content.windowRequest
+ when (windowRequest?.type) {
+ WindowRequest.Type.CLOSE -> consumeWindowRequest(state.id) {
+ store.state.tabs.find { it.engineState.engineSession === windowRequest.prepare() }?.let {
+ tabsUseCases.removeTab(it.id)
+ }
+ }
+ WindowRequest.Type.OPEN -> consumeWindowRequest(state.id) {
+ tabsUseCases.addTab(
+ selectTab = true,
+ parentId = state.id,
+ engineSession = windowRequest.prepare(),
+ private = state.content.private,
+ )
+ windowRequest.start()
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+ }
+ }
+
+ private fun consumeWindowRequest(
+ tabId: String,
+ consume: () -> Unit,
+ ) {
+ consume()
+ store.dispatch(ContentAction.ConsumeWindowRequestAction(tabId))
+ }
+
+ /**
+ * Stops observing the selected session for incoming window requests.
+ */
+ override fun stop() {
+ scope?.cancel()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/BrowserState.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/BrowserState.kt
new file mode 100644
index 0000000000..be4e2d9013
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/BrowserState.kt
@@ -0,0 +1,38 @@
+/* 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.tabs.ext
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.feature.tabs.tabstray.Tabs
+
+/**
+ * Converts the tabs in [BrowserState], using [tabsFilter], to a [Tabs] object.
+ *
+ * This implementation is use to help observe changes to the [BrowserState] tabs when only a select
+ * few properties have changed.
+ */
+internal fun BrowserState.toTabs(
+ tabsFilter: (TabSessionState) -> Boolean = { true },
+): Tabs {
+ val (tabStates, selectedTabId) = toTabList(tabsFilter)
+ val tabs = tabStates.map { it.toTab() }
+ return Tabs(tabs, selectedTabId)
+}
+
+/**
+ * Returns a list of tabs with the applied [tabsFilter] and the selected tab ID.
+ */
+internal fun BrowserState.toTabList(
+ tabsFilter: (TabSessionState) -> Boolean = { true },
+): Pair<List<TabSessionState>, String?> {
+ val tabStates = tabs.filter(tabsFilter)
+ val selectedTabId = tabStates
+ .filter(tabsFilter)
+ .firstOrNull { it.id == selectedTabId }
+ ?.id
+
+ return Pair(tabStates, selectedTabId)
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/TabSessionState.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/TabSessionState.kt
new file mode 100644
index 0000000000..416f735165
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/TabSessionState.kt
@@ -0,0 +1,29 @@
+/* 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.tabs.ext
+
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.feature.tabs.tabstray.Tab
+
+internal fun TabSessionState.toTab() = Tab(
+ id,
+ content.url,
+ content.title,
+ content.private,
+ content.icon,
+ mediaSessionState?.playbackState,
+ mediaSessionState?.controller,
+ lastAccess,
+ createdAt,
+ if (content.searchTerms.isNotEmpty()) content.searchTerms else historyMetadata?.searchTerm ?: "",
+)
+
+/**
+ * Check whether this tab has played media before - any media which started playing in this HTML document,
+ * irrespective of it's current state (eg: playing, paused, stopped).
+ */
+fun TabSessionState.hasMediaPlayed(): Boolean {
+ return lastMediaAccessState.lastMediaUrl == content.url || lastMediaAccessState.mediaSessionActive
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/Tab.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/Tab.kt
new file mode 100644
index 0000000000..dfa295ee97
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/Tab.kt
@@ -0,0 +1,38 @@
+/* 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.tabs.tabstray
+
+import android.graphics.Bitmap
+import mozilla.components.browser.tabstray.TabsTray
+import mozilla.components.concept.engine.mediasession.MediaSession
+
+/**
+ *
+ * An internal only data class that is used for collecting the values to observe in the [TabsTray].
+ *
+ * @property id Unique ID of the tab.
+ * @property url Current URL of the tab.
+ * @property title Current title of the tab (or an empty [String]]).
+ * @property private whether or not the session is private.
+ * @property icon Current icon of the tab (or null)
+ * @property playbackState Current media session playback state for the tab (or null)
+ * @property controller Current media session controller for the tab (or null)
+ * @property lastAccess The last time this tab was selected.
+ * @property createdAt When the tab was first created.
+ * @property searchTerm the last used search term for this tab or from the originating tab, or an
+ * empty string if no search was executed.
+ */
+internal data class Tab(
+ val id: String,
+ val url: String,
+ val title: String = "",
+ val private: Boolean = false,
+ val icon: Bitmap? = null,
+ val playbackState: MediaSession.PlaybackState? = null,
+ val controller: MediaSession.Controller? = null,
+ val lastAccess: Long = 0L,
+ val createdAt: Long = 0L,
+ val searchTerm: String = "",
+)
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabList.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabList.kt
new file mode 100644
index 0000000000..4e28163700
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabList.kt
@@ -0,0 +1,12 @@
+/* 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.tabs.tabstray
+
+import mozilla.components.browser.state.state.TabSessionState
+
+internal data class TabList(
+ val list: List<TabSessionState>,
+ val selectedTabId: String?,
+)
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/Tabs.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/Tabs.kt
new file mode 100644
index 0000000000..a0b27c65c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/Tabs.kt
@@ -0,0 +1,19 @@
+/* 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.tabs.tabstray
+
+import mozilla.components.browser.tabstray.TabsTray
+
+/**
+ * An internal only data class that is used for collecting the values to observe in the [TabsTray].
+ * Aggregate data type keeping a reference to the list of tabs and the selected tab id.
+ *
+ * @property list The list of tabs.
+ * @property selectedTabId Id of the selected tab in the list of tabs (or null).
+ */
+internal data class Tabs(
+ val list: List<Tab>,
+ val selectedTabId: String?,
+)
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt
new file mode 100644
index 0000000000..a3779977e6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt
@@ -0,0 +1,61 @@
+/* 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.tabs.tabstray
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.tabstray.TabsTray
+import mozilla.components.feature.tabs.ext.toTabList
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Feature implementation for connecting a tabs tray implementation with the session module.
+ *
+ * @param defaultTabsFilter A tab filter that is used for the initial presenting of tabs.
+ * @param defaultTabPartitionsFilter A tab partition filter that is used for the initial presenting of
+ * tabs.
+ * @param onCloseTray a callback invoked when the last tab is closed.
+ */
+class TabsFeature(
+ private val tabsTray: TabsTray,
+ private val store: BrowserStore,
+ private val onCloseTray: () -> Unit = {},
+ private val defaultTabPartitionsFilter: (Map<String, TabPartition>) -> TabPartition? = { null },
+ private val defaultTabsFilter: (TabSessionState) -> Boolean = { true },
+) : LifecycleAwareFeature {
+ @VisibleForTesting
+ internal var presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ defaultTabsFilter,
+ defaultTabPartitionsFilter,
+ onCloseTray,
+ )
+
+ override fun start() {
+ presenter.start()
+ }
+
+ override fun stop() {
+ presenter.stop()
+ }
+
+ /**
+ * Filter the list of tabs using [tabsFilter].
+ *
+ * @param tabsFilter A filter function returning `true` for all tabs that should be displayed in
+ * the tabs tray. Uses the [defaultTabsFilter] if none is provided.
+ */
+ fun filterTabs(tabsFilter: (TabSessionState) -> Boolean = defaultTabsFilter) {
+ presenter.tabsFilter = tabsFilter
+
+ val state = store.state
+ val (tabs, selectedTabId) = state.toTabList(tabsFilter)
+
+ tabsTray.updateTabs(tabs, null, selectedTabId)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt
new file mode 100644
index 0000000000..450b488ef5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt
@@ -0,0 +1,56 @@
+/* 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.tabs.tabstray
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.tabstray.TabsTray
+import mozilla.components.feature.tabs.ext.toTabList
+import mozilla.components.feature.tabs.ext.toTabs
+import mozilla.components.lib.state.ext.flowScoped
+
+/**
+ * Presenter implementation for a tabs tray implementation in order to update the tabs tray whenever
+ * the state of the session manager changes.
+ */
+class TabsTrayPresenter(
+ private val tabsTray: TabsTray,
+ private val store: BrowserStore,
+ internal var tabsFilter: (TabSessionState) -> Boolean,
+ internal var tabPartitionsFilter: (Map<String, TabPartition>) -> TabPartition?,
+ private val closeTabsTray: () -> Unit,
+) {
+ private var scope: CoroutineScope? = null
+ private var initialOpen: Boolean = true
+
+ fun start() {
+ scope = store.flowScoped { flow -> collect(flow) }
+ }
+
+ fun stop() {
+ scope?.cancel()
+ }
+
+ private suspend fun collect(flow: Flow<BrowserState>) {
+ flow.distinctUntilChangedBy { Pair(it.toTabs(tabsFilter), tabPartitionsFilter(it.tabPartitions)) }
+ .collect { state ->
+ val (tabs, selectedTabId) = state.toTabList(tabsFilter)
+ // Do not invoke the callback on start if this is the initial state.
+ if (tabs.isEmpty() && !initialOpen) {
+ closeTabsTray.invoke()
+ }
+
+ tabsTray.updateTabs(tabs, tabPartitionsFilter(state.tabPartitions), selectedTabId)
+
+ initialOpen = false
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/toolbar/TabCounterToolbarButton.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/toolbar/TabCounterToolbarButton.kt
new file mode 100644
index 0000000000..fcacae9b20
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/toolbar/TabCounterToolbarButton.kt
@@ -0,0 +1,115 @@
+/* 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.tabs.toolbar
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.tabs.R
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.ui.tabcounter.TabCounter
+import mozilla.components.ui.tabcounter.TabCounterMenu
+import java.lang.ref.WeakReference
+
+/**
+ * A [Toolbar.Action] implementation that shows a [TabCounter].
+ */
+open class TabCounterToolbarButton(
+ private val lifecycleOwner: LifecycleOwner,
+ private val countBasedOnSelectedTabType: Boolean = true,
+ private val showTabs: () -> Unit,
+ private val store: BrowserStore,
+ private val menu: TabCounterMenu? = null,
+ private val showMaskInPrivateMode: Boolean = false,
+) : Toolbar.Action {
+
+ private var reference = WeakReference<TabCounter>(null)
+
+ override fun createView(parent: ViewGroup): View {
+ store.flowScoped(lifecycleOwner) { flow ->
+ flow.map { state -> getTabCount(state) }
+ .distinctUntilChanged()
+ .collect {
+ tabs ->
+ updateCount(tabs)
+ }
+ }
+
+ val tabCounter = TabCounter(parent.context).apply {
+ reference = WeakReference(this)
+ setOnClickListener {
+ showTabs.invoke()
+ }
+
+ menu?.let { menu ->
+ setOnLongClickListener {
+ menu.menuController.show(anchor = it)
+ true
+ }
+ }
+
+ addOnAttachStateChangeListener(
+ object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ setCount(getTabCount(store.state))
+ }
+
+ override fun onViewDetachedFromWindow(v: View) { /* no-op */ }
+ },
+ )
+
+ contentDescription = parent.context.getString(R.string.mozac_feature_tabs_toolbar_tabs_button)
+
+ toggleCounterMask(showMaskInPrivateMode && isPrivate(store))
+ }
+
+ // Set selectableItemBackgroundBorderless
+ tabCounter.setBackgroundResource(
+ parent.context.theme.resolveAttribute(
+ android.R.attr.selectableItemBackgroundBorderless,
+ ),
+ )
+
+ return tabCounter
+ }
+
+ override fun bind(view: View) = Unit
+
+ private fun getTabCount(state: BrowserState): Int {
+ return if (countBasedOnSelectedTabType) {
+ state.getNormalOrPrivateTabs(isPrivate(store)).size
+ } else {
+ state.tabs.size
+ }
+ }
+
+ /**
+ * Update the tab counter button on the toolbar.
+ *
+ * @property count the updated tab count
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ fun updateCount(count: Int) {
+ reference.get()?.setCountWithAnimation(count)
+ }
+
+ /**
+ * Check if the selected tab is private.
+ *
+ * @property store the [BrowserStore] associated with this instance
+ */
+ fun isPrivate(store: BrowserStore): Boolean {
+ return store.state.selectedTab?.content?.private ?: false
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/toolbar/TabsToolbarFeature.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/toolbar/TabsToolbarFeature.kt
new file mode 100644
index 0000000000..5b6d3433b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/toolbar/TabsToolbarFeature.kt
@@ -0,0 +1,45 @@
+/* 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.tabs.toolbar
+
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.ui.tabcounter.TabCounterMenu
+
+/**
+ * Feature implementation for connecting a tabs tray implementation with a toolbar implementation.
+ *
+ * @param countBasedOnSelectedTabType if true the count is based on the selected tab i.e. if a
+ * private tab is selected private tabs will be counter, otherwise normal tabs. If false, all
+ * tabs will be counted.
+ */
+// TODO Refactor or remove this feature: https://github.com/mozilla-mobile/android-components/issues/9129
+class TabsToolbarFeature(
+ toolbar: Toolbar,
+ store: BrowserStore,
+ sessionId: String? = null,
+ lifecycleOwner: LifecycleOwner,
+ showTabs: () -> Unit,
+ tabCounterMenu: TabCounterMenu? = null,
+ countBasedOnSelectedTabType: Boolean = true,
+) {
+ init {
+ run {
+ // this feature is not used for Custom Tabs
+ if (sessionId != null && store.state.findCustomTab(sessionId) != null) return@run
+
+ val tabsAction = TabCounterToolbarButton(
+ lifecycleOwner = lifecycleOwner,
+ showTabs = showTabs,
+ store = store,
+ menu = tabCounterMenu,
+ countBasedOnSelectedTabType = countBasedOnSelectedTabType,
+ )
+ toolbar.addBrowserAction(tabsAction)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..ac89a45a65
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ትሮች</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..71ac533618
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-an/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Pestanyas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..e936e104ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ar/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">الألسنة</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..0388a27500
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Llingüetes</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..db9a695d66
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-az/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Vərəqlər</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..f9282d3cd0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">تاغ‌لار</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..f7510b95b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..35acd49446
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Карткі</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..fb76dfe524
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Раздели</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..1b456b8d31
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-bn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ট্যাবগুলি</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..b23637b727
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Ivinelloù</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..48a1dacd83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabovi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..5a319ee95a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Pestanyes</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..8d4151d152
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Taq ruwi\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..06498b0f3a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Mga Tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..c68368cf0a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">بازدەرەکان</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..0cbabfad0c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Unghjette</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..1e585bd567
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Panely</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..c3566ec738
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabiau</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..5bd017c4e8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Faneblade</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..ef9b77406e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabs</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..1980db3efc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Rejtariki</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..8ae3a452cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Καρτέλες</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..ef9b77406e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabs</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..ef9b77406e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabs</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..132744e364
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Langetoj</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..e1481954ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Pestañas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..e1481954ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Pestañas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..e1481954ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Pestañas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..e1481954ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Pestañas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..e1481954ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Pestañas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..a401608b50
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Kaardid</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..8ed4d9d5ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Etiketak</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..3c8217c13a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">زبانه‌ها</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..a70fee45ab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ff/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabbe</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..43d0a4b9ee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Välilehdet</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..76785c119e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Onglets</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..3ae38f5096
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Schedis</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..8d641f53b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Ljepblêden</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..ed205abebd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Cluaisíní</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..0614fbfa07
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabaichean</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..da19e9f34e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Lapelas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..2a6de9efe1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tendayke</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..20a6799abd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ટૅબ્સ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..aeb0c9b2c3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">टैब</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..65ba6d3981
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hil/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Mga tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..675d46bb0d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Kartice</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..26511018a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Rajtarki</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..d80f19d0f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Lapok</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..e03ad52413
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Ներդիրներ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..3deec4e2b2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Schedas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..f7510b95b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..04073974c1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Flipar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..12e2ce337d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Schede</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..a65df314e3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">לשוניות</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..9ca1197c7c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">タブ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..066a5fb472
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ჩანართები</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..f466e72150
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Betler</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..fb3e08b9f7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Iccaren</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..36f5d502ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Беттер</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..d7e703ecc1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Hilpekîn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..19e11c2208
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-kn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ಟ್ಯಾಬ್‌ಗಳು</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..1ce5a2ed58
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">탭</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..53f80feb7a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-lij/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Feuggi</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..5313815846
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ແທັບ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..f4917fba69
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Kortelės</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..13fd54b9d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ml/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ടാബുകൾ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..5cd22bafc1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-mr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">टॅब</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..605515ff41
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-my/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">တပ်ဗ်များ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..c89c552904
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Faner</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..ccaf504426
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ट्याबहरु</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..2c762edf1d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabbladen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..c89c552904
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Faner</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..76785c119e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Onglets</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..b4afae0551
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ਟੈਬਾਂ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..52923cf750
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ٹیباں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..476d9beec4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Karty</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..a71b344a36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Abas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..d49bcb86bf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Separadores</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..ef9b77406e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabs</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..d2ee827744
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ro/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">File</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..94c2833453
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Вкладки</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..4a972af757
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ᱴᱮᱵᱽ ᱠᱚ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..b2db0b56d7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Ischedas</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..4d4fff6662
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">පටිති</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..476d9beec4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Karty</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..52923cf750
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ٹیباں</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..9eb8df387d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Zavihki</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..8d3773a0cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Skeda</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..6617bbf49e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Језичци</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..f7510b95b1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..9115f22335
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Flikar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..ef24fbf761
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ta/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">கீற்றுகள்</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..655c4c0fec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-te/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ట్యాబులు</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..cf5d73cb85
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Варақаҳо</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..a42d5055d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">แท็บ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..06498b0f3a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Mga Tab</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..525e2e6317
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Sekmeler</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..7b5fd0e245
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Nej rakïj ñaj</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..2cb6c12c5c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Таблар</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..c801fb166d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">iseksal</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..9fe63b8083
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">بەتكۈچ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..94c2833453
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Вкладки</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..cf1e4c3f16
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-ur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">ٹیبس</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..71344991da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Varaqlar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..12e2ce337d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-vec/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Schede</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..b9574f5451
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Thẻ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..5c52ab2051
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Àwọn táàbù</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..bf2219240b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">标签页</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..38c2aef895
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">分頁</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/tabs/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..dbd7d68929
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Content description (not visible, for screen readers etc.): Description for the "tabs" button in the browser toolbar. -->
+ <string name="mozac_feature_tabs_toolbar_tabs_button">Tabs</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/CustomTabsUseCasesTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/CustomTabsUseCasesTest.kt
new file mode 100644
index 0000000000..a9de4e95e7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/CustomTabsUseCasesTest.kt
@@ -0,0 +1,76 @@
+/* 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.tabs
+
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.any
+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 mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+
+class CustomTabsUseCasesTest {
+
+ private lateinit var store: BrowserStore
+ private lateinit var tabsUseCases: CustomTabsUseCases
+ private lateinit var engine: Engine
+ private lateinit var engineSession: EngineSession
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ engineSession = mock()
+ engine = mock()
+
+ whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
+ store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ ),
+ )
+ tabsUseCases = CustomTabsUseCases(store, mock())
+ }
+
+ @Test
+ fun `MigrateCustomTabUseCase - turns custom tab into regular tab and selects it`() {
+ val customTab = createCustomTab("https://mozilla.org")
+ store.dispatch(CustomTabListAction.AddCustomTabAction(customTab)).joinBlocking()
+ assertEquals(0, store.state.tabs.size)
+ assertEquals(1, store.state.customTabs.size)
+
+ tabsUseCases.migrate(customTab.id, select = false)
+ store.waitUntilIdle()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(0, store.state.customTabs.size)
+ assertNull(store.state.selectedTabId)
+
+ val otherCustomTab = createCustomTab("https://firefox.com")
+ store.dispatch(CustomTabListAction.AddCustomTabAction(otherCustomTab)).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(1, store.state.customTabs.size)
+
+ tabsUseCases.migrate(otherCustomTab.id, select = true)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(0, store.state.customTabs.size)
+ assertEquals(otherCustomTab.id, store.state.selectedTabId)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt
new file mode 100644
index 0000000000..594ac139bc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt
@@ -0,0 +1,632 @@
+/* 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.tabs
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.session.storage.SessionStorage
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.selector.findNormalOrPrivateTabByUrl
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.toRecoverableTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+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 mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+const val DAY_IN_MS = 24 * 60 * 60 * 1000L
+
+class TabsUseCasesTest {
+
+ private lateinit var store: BrowserStore
+ private lateinit var tabsUseCases: TabsUseCases
+ private lateinit var engine: Engine
+ private lateinit var engineSession: EngineSession
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Before
+ fun setup() {
+ engineSession = mock()
+ engine = mock()
+
+ whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
+ store = BrowserStore(
+ middleware = EngineMiddleware.create(
+ engine = engine,
+ ),
+ )
+ tabsUseCases = TabsUseCases(store)
+ }
+
+ @Test
+ fun `SelectTabUseCase - tab is marked as selected in store`() {
+ val tab = createTab("https://mozilla.org")
+ val otherTab = createTab("https://firefox.com")
+ store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(otherTab.id, store.state.selectedTabId)
+ assertEquals(otherTab, store.state.selectedTab)
+
+ tabsUseCases.selectTab(tab.id)
+ store.waitUntilIdle()
+ assertEquals(tab.id, store.state.selectedTabId)
+ assertEquals(tab, store.state.selectedTab)
+ }
+
+ @Test
+ fun `RemoveTabUseCase - session will be removed from store`() {
+ val tab = createTab("https://mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+
+ tabsUseCases.removeTab(tab.id)
+ store.waitUntilIdle()
+ assertEquals(0, store.state.tabs.size)
+ }
+
+ @Test
+ fun `RemoveTabUseCase - remove by ID and select parent if it exists`() {
+ val parentTab = createTab("https://firefox.com")
+ store.dispatch(TabListAction.AddTabAction(parentTab)).joinBlocking()
+
+ val tab = createTab("https://mozilla.org", parent = parentTab)
+ store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(tab.id, store.state.selectedTabId)
+
+ tabsUseCases.removeTab(tab.id, selectParentIfExists = true)
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(parentTab.id, store.state.selectedTabId)
+ }
+
+ @Test
+ fun `RemoveTabsUseCase - list of sessions can be removed`() {
+ val tab = createTab("https://mozilla.org")
+ val otherTab = createTab("https://firefox.com")
+ store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(otherTab.id, store.state.selectedTabId)
+ assertEquals(otherTab, store.state.selectedTab)
+
+ tabsUseCases.removeTabs(listOf(tab.id, otherTab.id))
+ store.waitUntilIdle()
+ assertEquals(0, store.state.tabs.size)
+ }
+
+ @Test
+ fun `AddNewTabUseCase - session will be added to store`() {
+ tabsUseCases.addTab("https://www.mozilla.org")
+
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertFalse(store.state.tabs[0].content.private)
+ }
+
+ @Test
+ fun `AddNewTabUseCase - private session will be added to store`() {
+ tabsUseCases.addTab("https://www.mozilla.org", private = true)
+
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertTrue(store.state.tabs[0].content.private)
+ }
+
+ @Test
+ fun `AddNewTabUseCase will not load URL if flag is set to false`() {
+ tabsUseCases.addTab("https://www.mozilla.org", startLoading = false)
+
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ verify(engineSession, never()).loadUrl(anyString(), any(), any(), any())
+ }
+
+ @Test
+ fun `AddNewTabUseCase will load URL if flag is set to true`() {
+ tabsUseCases.addTab("https://www.mozilla.org", startLoading = true)
+
+ // Wait for CreateEngineSessionAction and middleware
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ // Wait for LinkEngineSessionAction and middleware
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ verify(engineSession, times(1)).loadUrl("https://www.mozilla.org")
+ }
+
+ @Test
+ fun `AddNewTabUseCase forwards load flags to engine`() {
+ tabsUseCases.addTab.invoke("https://www.mozilla.org", flags = LoadUrlFlags.external(), startLoading = true)
+
+ // Wait for CreateEngineSessionAction and middleware
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ // Wait for LinkEngineSessionAction and middleware
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ verify(engineSession, times(1)).loadUrl("https://www.mozilla.org", null, LoadUrlFlags.external(), null)
+ }
+
+ @Test
+ fun `AddNewTabUseCase uses provided engine session`() {
+ val session: EngineSession = mock()
+ tabsUseCases.addTab.invoke(
+ "https://www.mozilla.org",
+ flags = LoadUrlFlags.external(),
+ startLoading = true,
+ engineSession = session,
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertSame(session, store.state.tabs[0].engineState.engineSession)
+ }
+
+ @Test
+ fun `AddNewTabUseCase uses provided contextId`() {
+ val contextId = "1"
+ tabsUseCases.addTab.invoke(
+ "https://www.mozilla.org",
+ flags = LoadUrlFlags.external(),
+ startLoading = true,
+ contextId = contextId,
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertEquals(contextId, store.state.tabs[0].contextId)
+ }
+
+ @Test
+ fun `AddNewTabUseCase uses provided history metadata`() {
+ val historyMetadata = HistoryMetadataKey(
+ "https://www.mozilla.org",
+ searchTerm = "test",
+ referrerUrl = "http://firefox.com",
+ )
+
+ tabsUseCases.addTab.invoke(
+ "https://www.mozilla.org",
+ flags = LoadUrlFlags.external(),
+ startLoading = true,
+ historyMetadata = historyMetadata,
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertEquals(historyMetadata, store.state.tabs[0].historyMetadata)
+ }
+
+ @Test
+ fun `GIVEN a search is performed WHEN adding a new tab THEN the resulting tab is flagged as the result of a search`() {
+ tabsUseCases.addTab.invoke(
+ "https://www.mozilla.org",
+ flags = LoadUrlFlags.external(),
+ isSearch = true,
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(true, store.state.tabs.single().content.isSearch)
+ }
+
+ @Test
+ fun `GIVEN a search is performed with load URL flags and additional headers WHEN adding a new tab THEN the resulting tab is loaded as a search result with the correct load flags and headers`() {
+ val url = "https://www.mozilla.org"
+ val flags = LoadUrlFlags.select(LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS)
+ val additionalHeaders = mapOf("X-Extra-Header" to "true")
+
+ tabsUseCases.addTab.invoke(
+ url = url,
+ flags = flags,
+ isSearch = true,
+ additionalHeaders = additionalHeaders,
+ )
+
+ store.waitUntilIdle()
+
+ assertEquals(1, store.state.tabs.size)
+ assertTrue(store.state.tabs.single().content.isSearch)
+ assertEquals(flags, store.state.tabs.single().engineState.initialLoadFlags)
+ assertEquals(
+ additionalHeaders,
+ store.state.tabs.single().engineState.initialAdditionalHeaders,
+ )
+
+ verify(engineSession, times(1)).loadUrl(
+ url = url,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ )
+ }
+
+ @Test
+ fun `RemoveAllTabsUseCase will remove all sessions`() {
+ val tab = createTab("https://mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+
+ val tab2 = createTab("https://firefox.com", private = true)
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ assertEquals(2, store.state.tabs.size)
+
+ tabsUseCases.removeAllTabs()
+ store.waitUntilIdle()
+ assertEquals(0, store.state.tabs.size)
+ }
+
+ @Test
+ fun `RemoveNormalTabsUseCase and RemovePrivateTabsUseCase will remove sessions for particular type of tabs private or normal`() {
+ val tab = createTab("https://mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+
+ val privateTab = createTab("https://firefox.com", private = true)
+ store.dispatch(TabListAction.AddTabAction(privateTab)).joinBlocking()
+ assertEquals(2, store.state.tabs.size)
+
+ tabsUseCases.removeNormalTabs()
+ store.waitUntilIdle()
+ assertEquals(1, store.state.tabs.size)
+
+ tabsUseCases.removePrivateTabs()
+ store.waitUntilIdle()
+ assertEquals(0, store.state.tabs.size)
+ }
+
+ @Test
+ fun `RestoreUseCase - filters based on tab timeout`() = runTest {
+ val useCases = TabsUseCases(BrowserStore())
+
+ val now = System.currentTimeMillis()
+ val twoDays = now - 2 * DAY_IN_MS
+ val threeDays = now - 3 * DAY_IN_MS
+ val tabs = listOf(
+ createTab("https://mozilla.org", lastAccess = 0).toRecoverableTab(),
+ createTab("https://mozilla.org", lastAccess = now).toRecoverableTab(),
+ createTab("https://firefox.com", lastAccess = twoDays, createdAt = threeDays).toRecoverableTab(),
+ createTab("https://getpocket.com", lastAccess = threeDays, createdAt = threeDays).toRecoverableTab(),
+ )
+
+ val sessionStorage: SessionStorage = mock()
+ useCases.restore(sessionStorage, tabTimeoutInMs = DAY_IN_MS)
+
+ val predicateCaptor = argumentCaptor<(RecoverableTab) -> Boolean>()
+ verify(sessionStorage).restore(predicateCaptor.capture())
+
+ // Only the first two tab should be restored, all others "timed out."
+ val restoredTabs = tabs.filter(predicateCaptor.value)
+ assertEquals(2, restoredTabs.size)
+ assertEquals(tabs.first(), restoredTabs.first())
+ }
+
+ @Test
+ fun `selectOrAddTab selects already existing tab`() {
+ val tab = createTab("https://mozilla.org")
+ val otherTab = createTab("https://firefox.com")
+
+ store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(otherTab.id, store.state.selectedTabId)
+ assertEquals(otherTab, store.state.selectedTab)
+ assertEquals(2, store.state.tabs.size)
+
+ val tabID = tabsUseCases.selectOrAddTab(tab.content.url)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(tab.id, store.state.selectedTabId)
+ assertEquals(tab, store.state.selectedTab)
+ assertEquals(tab.id, tabID)
+ }
+
+ @Test
+ fun `selectOrAddTab selects already existing tab with matching historyMetadata`() {
+ val historyMetadata = HistoryMetadataKey(
+ url = "https://mozilla.org",
+ referrerUrl = "https://firefox.com",
+ )
+
+ val tab = createTab("https://mozilla.org", historyMetadata = historyMetadata)
+ val otherTab = createTab("https://firefox.com")
+
+ store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(otherTab.id, store.state.selectedTabId)
+ assertEquals(otherTab, store.state.selectedTab)
+ assertEquals(2, store.state.tabs.size)
+
+ val tabID = tabsUseCases.selectOrAddTab(tab.content.url, historyMetadata = historyMetadata)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(tab.id, store.state.selectedTabId)
+ assertEquals(tab, store.state.selectedTab)
+ assertEquals(tab.id, tabID)
+ }
+
+ @Test
+ fun `selectOrAddTab adds new tab if no matching existing tab could be found`() {
+ val tab = createTab("https://mozilla.org")
+
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(tab.id, store.state.selectedTabId)
+ assertEquals(tab, store.state.selectedTab)
+ assertEquals(1, store.state.tabs.size)
+
+ val tabID = tabsUseCases.selectOrAddTab("https://firefox.com")
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertNotNull(store.state.findNormalOrPrivateTabByUrl("https://firefox.com", false))
+ assertEquals(store.state.selectedTabId, tabID)
+ }
+
+ @Test
+ fun `selectOrAddTab adds new tab if no matching existing history metadata could be found`() {
+ val tab = createTab("https://mozilla.org")
+ val historyMetadata = HistoryMetadataKey(
+ url = "https://mozilla.org",
+ referrerUrl = "https://firefox.com",
+ )
+
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(tab.id, store.state.selectedTabId)
+ assertEquals(tab, store.state.selectedTab)
+ assertEquals(1, store.state.tabs.size)
+
+ val tabID =
+ tabsUseCases.selectOrAddTab("https://firefox.com", historyMetadata = historyMetadata)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertNotNull(store.state.findNormalOrPrivateTabByUrl("https://firefox.com", false))
+ assertEquals(store.state.selectedTabId, tabID)
+ }
+
+ @Test
+ fun `selectOrAddTab selects already existing tab with matching url when ignoreFragment is set to true`() {
+ val tab = createTab("https://mozilla.org")
+ val otherTab = createTab("https://firefox.com")
+
+ store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(otherTab, store.state.selectedTab)
+ assertEquals(2, store.state.tabs.size)
+
+ val actualTabId = tabsUseCases.selectOrAddTab(url = "https://mozilla.org/#welcome", ignoreFragment = true)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(tab, store.state.selectedTab)
+ assertEquals(store.state.selectedTabId, actualTabId)
+ }
+
+ @Test
+ fun `selectOrAddTab adds new tab if no matching existing tab could be found with ignoreFragment set to true`() {
+ val tab = createTab("https://mozilla.org")
+
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ assertEquals(tab.id, store.state.selectedTabId)
+ assertEquals(tab, store.state.selectedTab)
+ assertEquals(1, store.state.tabs.size)
+
+ val tabID = tabsUseCases.selectOrAddTab(url = "https://firefox.com", ignoreFragment = true)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(store.state.selectedTabId, tabID)
+ }
+
+ private fun assertTabsDuplicates(tab: TabSessionState, dup: TabSessionState) {
+ assertEquals(tab.content.url, dup.content.url)
+ assertEquals(tab.content.private, dup.content.private)
+ assertEquals(tab.contextId, dup.contextId)
+ assertEquals(tab.engineState.engineSessionState, dup.engineState.engineSessionState)
+ assertNotEquals(tab.id, dup.id)
+ assertEquals(tab.id, dup.parentId)
+ }
+
+ @Test
+ fun `duplicateTab creates a duplicate of the given tab`() {
+ store.dispatch(
+ TabListAction.AddTabAction(
+ createTab(id = "mozilla", url = "https://www.mozilla.org"),
+ ),
+ ).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+
+ val engineSessionState: EngineSessionState = mock()
+ store.dispatch(
+ EngineAction.UpdateEngineSessionStateAction("mozilla", engineSessionState),
+ ).joinBlocking()
+
+ val tab = store.state.findTab("mozilla")!!
+ val dupId = tabsUseCases.duplicateTab.invoke(tab)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals(dupId, store.state.tabs[1].id)
+ assertEquals("mozilla", store.state.tabs[0].id)
+ assertFalse(store.state.tabs[0].content.private)
+ assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
+ assertEquals(engineSessionState, store.state.tabs[0].engineState.engineSessionState)
+ assertNull(store.state.tabs[0].parentId)
+ assertTabsDuplicates(tab, store.state.tabs[1])
+ }
+
+ @Test
+ fun `duplicateTab creates duplicates of private tabs`() {
+ val tab = createTab(id = "mozilla", url = "https://www.mozilla.org", private = true)
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ tabsUseCases.duplicateTab.invoke(tab)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertTrue(store.state.tabs[0].content.private)
+ assertTrue(store.state.tabs[1].content.private)
+ }
+
+ @Test
+ fun `duplicateTab keeps contextId`() {
+ val tab = createTab(id = "mozilla", url = "https://www.mozilla.org", contextId = "work")
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ tabsUseCases.duplicateTab.invoke(tab)
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("work", store.state.tabs[0].contextId)
+ assertEquals("work", store.state.tabs[1].contextId)
+ }
+
+ @Test
+ fun `duplicateTab without tab argument uses the selected tab`() {
+ var tab = createTab(url = "https://www.mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ var dupId = tabsUseCases.duplicateTab.invoke(selectNewTab = true)!!
+ store.waitUntilIdle()
+
+ assertEquals(2, store.state.tabs.size)
+ assertNotNull(dupId)
+ var dup = store.state.findTab(dupId)!!
+ assertTabsDuplicates(tab, dup)
+ assertEquals(dup, store.state.selectedTab)
+
+ tab = dup
+ dupId = tabsUseCases.duplicateTab.invoke(selectNewTab = false)!!
+ store.waitUntilIdle()
+
+ assertEquals(3, store.state.tabs.size)
+ assertNotNull(dupId)
+ dup = store.state.findTab(dupId)!!
+ assertTabsDuplicates(tab, dup)
+ assertEquals(tab, store.state.selectedTab)
+ }
+
+ @Test
+ fun `MoveTabsUseCase will move a tab`() {
+ val tab = createTab("https://mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ val tab2 = createTab("https://firefox.com", private = true)
+ store.dispatch(TabListAction.AddTabAction(tab2)).joinBlocking()
+ assertEquals(2, store.state.tabs.size)
+ assertEquals("https://mozilla.org", store.state.tabs[0].content.url)
+ assertEquals("https://firefox.com", store.state.tabs[1].content.url)
+
+ val tab1Id = store.state.tabs[0].id
+ val tab2Id = store.state.tabs[1].id
+ tabsUseCases.moveTabs(listOf(tab1Id), tab2Id, true)
+ store.waitUntilIdle()
+ assertEquals("https://firefox.com", store.state.tabs[0].content.url)
+ assertEquals("https://mozilla.org", store.state.tabs[1].content.url)
+ }
+
+ @Test
+ fun `MigratePrivateTabUseCase will migrate a private tab`() {
+ val tab = createTab("https://mozilla.org", private = true)
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+ assertEquals(true, store.state.tabs[0].content.private)
+
+ tabsUseCases.migratePrivateTabUseCase(tab.id)
+ store.waitUntilIdle()
+ // Still only 1 tab and that tab still has the same URL...
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://mozilla.org", store.state.tabs[0].content.url)
+ // But it's no longer private and has a different tabId.
+ assertEquals(false, store.state.tabs[0].content.private)
+ assertNotEquals(store.state.tabs[0].id, tab.id)
+ }
+
+ @Test
+ fun `MigratePrivateTabUseCase will respect alternativeUrl`() {
+ // This (obviously!) isn't a real reader-mode URL, but is fine for the purposes of this test.
+ val tab = createTab("https://mozilla.org/reader-mode", private = true)
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+
+ tabsUseCases.migratePrivateTabUseCase(store.state.tabs[0].id, "https://mozilla.org/not-reader-mode")
+ store.waitUntilIdle()
+ // Still only 1 tab with our alternative URL
+ assertEquals(1, store.state.tabs.size)
+ assertEquals("https://mozilla.org/not-reader-mode", store.state.tabs[0].content.url)
+ assertEquals(false, store.state.tabs[0].content.private)
+ }
+
+ @Test
+ fun `MigratePrivateTabUseCase will fail on a regular tab`() {
+ val tab = createTab("https://mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ assertEquals(1, store.state.tabs.size)
+ assertThrows(IllegalArgumentException::class.java) {
+ tabsUseCases.migratePrivateTabUseCase(tab.id)
+ }
+ }
+
+ @Test
+ fun `MigratePrivateTabUseCase will fail if the tab can't be found`() {
+ assertThrows(IllegalStateException::class.java) {
+ tabsUseCases.migratePrivateTabUseCase("invalid-tab-id")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/WindowFeatureTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/WindowFeatureTest.kt
new file mode 100644
index 0000000000..5a6331fcbf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/WindowFeatureTest.kt
@@ -0,0 +1,119 @@
+/* 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.tabs
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class WindowFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var store: BrowserStore
+ private lateinit var engineSession: EngineSession
+ private lateinit var tabsUseCases: TabsUseCases
+ private lateinit var addTabUseCase: TabsUseCases.AddNewTabUseCase
+ private lateinit var removeTabUseCase: TabsUseCases.RemoveTabUseCase
+ private val tabId = "test-tab"
+ private val privateTabId = "test-tab-private"
+
+ @Before
+ fun setup() {
+ engineSession = mock()
+ store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = tabId, url = "https://www.mozilla.org", engineSession = engineSession),
+ createTab(id = privateTabId, url = "https://www.mozilla.org", private = true),
+ ),
+ selectedTabId = tabId,
+ ),
+ ),
+ )
+ addTabUseCase = mock()
+ removeTabUseCase = mock()
+ tabsUseCases = mock()
+ whenever(tabsUseCases.addTab).thenReturn(addTabUseCase)
+ whenever(tabsUseCases.removeTab).thenReturn(removeTabUseCase)
+ }
+
+ @Test
+ fun `handles request to open window`() {
+ val feature = WindowFeature(store, tabsUseCases)
+ feature.start()
+
+ val windowRequest: WindowRequest = mock()
+ whenever(windowRequest.type).thenReturn(WindowRequest.Type.OPEN)
+ whenever(windowRequest.url).thenReturn("https://www.firefox.com")
+
+ store.dispatch(ContentAction.UpdateWindowRequestAction(tabId, windowRequest)).joinBlocking()
+
+ verify(addTabUseCase).invoke(url = "about:blank", selectTab = true, parentId = tabId)
+ verify(store).dispatch(ContentAction.ConsumeWindowRequestAction(tabId))
+ }
+
+ @Test
+ fun `handles request to open private window`() {
+ val feature = WindowFeature(store, tabsUseCases)
+ feature.start()
+
+ val windowRequest: WindowRequest = mock()
+ whenever(windowRequest.type).thenReturn(WindowRequest.Type.OPEN)
+ whenever(windowRequest.url).thenReturn("https://www.firefox.com")
+
+ store.dispatch(TabListAction.SelectTabAction(privateTabId)).joinBlocking()
+ store.dispatch(ContentAction.UpdateWindowRequestAction(privateTabId, windowRequest)).joinBlocking()
+
+ verify(addTabUseCase).invoke(url = "about:blank", selectTab = true, parentId = privateTabId, private = true)
+ verify(store).dispatch(ContentAction.ConsumeWindowRequestAction(privateTabId))
+ }
+
+ @Test
+ fun `handles request to close window`() {
+ val feature = WindowFeature(store, tabsUseCases)
+ feature.start()
+
+ val windowRequest: WindowRequest = mock()
+ whenever(windowRequest.type).thenReturn(WindowRequest.Type.CLOSE)
+ whenever(windowRequest.prepare()).thenReturn(engineSession)
+
+ store.dispatch(ContentAction.UpdateWindowRequestAction(tabId, windowRequest)).joinBlocking()
+
+ verify(removeTabUseCase).invoke(tabId)
+ verify(store).dispatch(ContentAction.ConsumeWindowRequestAction(tabId))
+ }
+
+ @Test
+ fun `handles no requests when stopped`() {
+ val feature = WindowFeature(store, tabsUseCases)
+ feature.start()
+ feature.stop()
+
+ val windowRequest: WindowRequest = mock()
+ whenever(windowRequest.type).thenReturn(WindowRequest.Type.CLOSE)
+
+ store.dispatch(ContentAction.UpdateWindowRequestAction(tabId, windowRequest)).joinBlocking()
+
+ verify(removeTabUseCase, never()).invoke(tabId)
+ verify(store, never()).dispatch(ContentAction.ConsumeWindowRequestAction(tabId))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/ext/TabSessionStateTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/ext/TabSessionStateTest.kt
new file mode 100644
index 0000000000..a6e0a8f6d1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/ext/TabSessionStateTest.kt
@@ -0,0 +1,56 @@
+/* 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.tabs.ext
+
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.LastMediaAccessState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class TabSessionStateTest {
+ @Test
+ fun `GIVEN lastMediaUrl is the same as the current tab url and mediaSessionActive is true WHEN hasMediaPlayed is called THEN return true`() {
+ val tab = TabSessionState(
+ content = ContentState(url = "https://mozilla.org"),
+ lastMediaAccessState = LastMediaAccessState(lastMediaUrl = "https://mozilla.org", mediaSessionActive = true),
+ )
+
+ assertTrue(tab.hasMediaPlayed())
+ }
+
+ @Test
+ fun `GIVEN lastMediaUrl is the same as the current tab url and and mediaSessionActive is false WHEN hasMediaPlayed is called THEN return true`() {
+ val tab = TabSessionState(
+ content = ContentState(url = "https://mozilla.org"),
+ lastMediaAccessState = LastMediaAccessState(lastMediaUrl = "https://mozilla.org", mediaSessionActive = false),
+ )
+
+ assertTrue(tab.hasMediaPlayed())
+ }
+
+ @Test
+ fun `GIVEN lastMediaUrl is different than the current tab url and mediaSessionActive is false WHEN hasMediaPlayed is called THEN return false`() {
+ val tab = TabSessionState(
+ content = ContentState(url = "https://mozilla.org"),
+ lastMediaAccessState = LastMediaAccessState(lastMediaUrl = "https://firefox.com", mediaSessionActive = false),
+ )
+
+ assertFalse(tab.hasMediaPlayed())
+ }
+
+ @Test
+ fun `WHEN creating a new TabSessionState THEN createAt is initialized with currentTimeMillis`() {
+ val currentTime = System.currentTimeMillis()
+
+ val newTab = TabSessionState(content = ContentState(url = "https://mozilla.org"))
+ val newTab2 = createTab(url = "https://mozilla.org")
+
+ assertTrue(currentTime <= newTab.createdAt)
+ assertTrue(currentTime <= newTab2.createdAt)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt
new file mode 100644
index 0000000000..9bb035a97b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt
@@ -0,0 +1,114 @@
+/* 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.tabs.tabstray
+
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.tabstray.TabsTray
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class TabsFeatureTest {
+
+ @Test
+ fun `asserting getters`() {
+ val store = BrowserStore()
+ val presenter: TabsTrayPresenter = mock()
+ val tabsFeature = spy(
+ TabsFeature(
+ mock(),
+ store,
+ ) { true },
+ )
+
+ assertNotEquals(tabsFeature.presenter, presenter)
+
+ tabsFeature.presenter = presenter
+
+ assertEquals(tabsFeature.presenter, presenter)
+ }
+
+ @Test
+ fun start() {
+ val store = BrowserStore()
+ val presenter: TabsTrayPresenter = mock()
+ val tabsFeature = spy(
+ TabsFeature(
+ mock(),
+ store,
+ ) { true },
+ )
+
+ tabsFeature.presenter = presenter
+
+ tabsFeature.start()
+
+ verify(presenter).start()
+ }
+
+ @Test
+ fun stop() {
+ val store = BrowserStore()
+ val presenter: TabsTrayPresenter = mock()
+ val tabsFeature = spy(
+ TabsFeature(
+ mock(),
+ store,
+ ) { true },
+ )
+
+ tabsFeature.presenter = presenter
+
+ tabsFeature.stop()
+
+ verify(presenter).stop()
+ }
+
+ @Test
+ fun filterTabs() {
+ val store = BrowserStore()
+ val presenter: TabsTrayPresenter = mock()
+ val tabsTray: TabsTray = mock()
+ val tabsFeature = spy(
+ TabsFeature(
+ tabsTray,
+ store,
+ ) { true },
+ )
+
+ tabsFeature.presenter = presenter
+
+ val filter: (TabSessionState) -> Boolean = { true }
+
+ tabsFeature.filterTabs(filter)
+
+ verify(presenter).tabsFilter = filter
+ verify(tabsTray).updateTabs(emptyList(), null, null)
+ }
+
+ @Test
+ fun `filterTabs uses default filter if a new one is not provided`() {
+ val store = BrowserStore()
+ val filter: (TabSessionState) -> Boolean = { false }
+ val tabsFeature = spy(
+ TabsFeature(
+ mock(),
+ store,
+ defaultTabsFilter = filter,
+ ),
+ )
+ val presenter: TabsTrayPresenter = mock()
+
+ tabsFeature.presenter = presenter
+
+ tabsFeature.filterTabs()
+
+ verify(presenter).tabsFilter = filter
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt
new file mode 100644
index 0000000000..9b42b739f1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt
@@ -0,0 +1,359 @@
+/* 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.tabs.tabstray
+
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabPartition
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.tabstray.TabsTray
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+class TabsTrayPresenterTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun `initial set of sessions will be passed to tabs tray`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "a"),
+ createTab("https://getpocket.com", id = "b"),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ closeTabsTray = {},
+ tabPartitionsFilter = { null },
+ tabsFilter = { true },
+ )
+
+ verifyNoMoreInteractions(tabsTray)
+
+ presenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertNotNull(tabsTray.updateTabs)
+
+ assertEquals("a", tabsTray.selectedTabId!!)
+ assertEquals(2, tabsTray.updateTabs!!.size)
+ assertEquals("https://www.mozilla.org", tabsTray.updateTabs!![0].content.url)
+ assertEquals("https://getpocket.com", tabsTray.updateTabs!![1].content.url)
+
+ presenter.stop()
+ }
+
+ @Test
+ fun `tab tray will get updated if session gets added`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "a"),
+ createTab("https://getpocket.com", id = "b"),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ closeTabsTray = {},
+ tabPartitionsFilter = { null },
+ tabsFilter = { true },
+ )
+
+ presenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(2, tabsTray.updateTabs!!.size)
+
+ store.dispatch(
+ TabListAction.AddTabAction(
+ createTab("https://developer.mozilla.org/"),
+ ),
+ ).joinBlocking()
+
+ assertEquals(3, tabsTray.updateTabs!!.size)
+
+ presenter.stop()
+ }
+
+ @Test
+ fun `tabs tray will get updated if session gets removed`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "a"),
+ createTab("https://getpocket.com", id = "b"),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ closeTabsTray = {},
+ tabPartitionsFilter = { null },
+ tabsFilter = { true },
+ )
+
+ presenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(2, tabsTray.updateTabs!!.size)
+
+ store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(1, tabsTray.updateTabs!!.size)
+
+ store.dispatch(TabListAction.RemoveTabAction("b")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(0, tabsTray.updateTabs!!.size)
+
+ presenter.stop()
+ }
+
+ @Test
+ fun `tabs tray will get updated if all sessions get removed`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "a"),
+ createTab("https://getpocket.com", id = "b"),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ closeTabsTray = {},
+ tabPartitionsFilter = { null },
+ tabsFilter = { true },
+ )
+
+ presenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(2, tabsTray.updateTabs!!.size)
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(0, tabsTray.updateTabs!!.size)
+
+ presenter.stop()
+ }
+
+ @Test
+ fun `tabs tray will get updated if selection changes`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "a"),
+ createTab("https://getpocket.com", id = "b"),
+ createTab("https://developer.mozilla.org", id = "c"),
+ createTab("https://www.firefox.com", id = "d"),
+ createTab("https://www.google.com", id = "e"),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ closeTabsTray = {},
+ tabPartitionsFilter = { null },
+ tabsFilter = { true },
+ )
+
+ presenter.start()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(5, tabsTray.updateTabs!!.size)
+ assertEquals("a", tabsTray.selectedTabId)
+
+ store.dispatch(TabListAction.SelectTabAction("d")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ println("Selection: " + store.state.selectedTabId)
+ assertEquals("d", tabsTray.selectedTabId)
+ }
+
+ @Test
+ fun `presenter invokes session filtering`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "a"),
+ createTab("https://getpocket.com", id = "b", private = true),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ closeTabsTray = {},
+ tabPartitionsFilter = { null },
+ tabsFilter = { it.content.private },
+ )
+
+ presenter.start()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertTrue(tabsTray.updateTabs?.size == 1)
+ }
+
+ @Test
+ fun `presenter will close tabs tray when all sessions get removed`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "a"),
+ createTab("https://getpocket.com", id = "b"),
+ createTab("https://developer.mozilla.org", id = "c"),
+ createTab("https://www.firefox.com", id = "d"),
+ createTab("https://www.google.com", id = "e"),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ var closed = false
+
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ tabPartitionsFilter = { null },
+ tabsFilter = { true },
+ closeTabsTray = { closed = true },
+ )
+
+ presenter.start()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ Assert.assertFalse(closed)
+
+ store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertTrue(closed)
+
+ presenter.stop()
+ }
+
+ @Test
+ fun `presenter will close tabs tray when last session gets removed`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "a"),
+ createTab("https://getpocket.com", id = "b"),
+ ),
+ selectedTabId = "a",
+ ),
+ )
+
+ var closed = false
+
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ tabPartitionsFilter = { null },
+ tabsFilter = { true },
+ closeTabsTray = { closed = true },
+ )
+
+ presenter.start()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ Assert.assertFalse(closed)
+
+ store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ Assert.assertFalse(closed)
+
+ store.dispatch(TabListAction.RemoveTabAction("b")).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertTrue(closed)
+
+ presenter.stop()
+ }
+
+ @Test
+ fun `tabs tray should not invoke the close callback on start`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = emptyList(),
+ selectedTabId = null,
+ ),
+ )
+
+ var invoked = false
+ val tabsTray: MockedTabsTray = spy(MockedTabsTray())
+ val presenter = TabsTrayPresenter(
+ tabsTray,
+ store,
+ tabPartitionsFilter = { null },
+ tabsFilter = { it.content.private },
+ closeTabsTray = { invoked = true },
+ )
+
+ presenter.start()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertFalse(invoked)
+ }
+}
+
+private class MockedTabsTray : TabsTray {
+ var updateTabs: List<TabSessionState>? = null
+ var selectedTabId: String? = null
+
+ override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) {
+ updateTabs = tabs
+ this.selectedTabId = selectedTabId
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/toolbar/TabCounterToolbarButtonTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/toolbar/TabCounterToolbarButtonTest.kt
new file mode 100644
index 0000000000..2d4c86dc35
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/toolbar/TabCounterToolbarButtonTest.kt
@@ -0,0 +1,268 @@
+/* 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.tabs.toolbar
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.recover.TabState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import mozilla.components.ui.tabcounter.R
+import mozilla.components.ui.tabcounter.TabCounter
+import mozilla.components.ui.tabcounter.TabCounterMenu
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+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.Mockito.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class TabCounterToolbarButtonTest {
+ private val showTabs: () -> Unit = mock()
+ private val tabCounterMenu: TabCounterMenu = mock()
+ private val menuController: MenuController = mock()
+
+ private lateinit var lifecycleOwner: MockedLifecycleOwner
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {
+ override val lifecycle: Lifecycle = LifecycleRegistry(this).apply {
+ currentState = initialState
+ }
+ }
+
+ @Before
+ fun setUp() {
+ whenever(tabCounterMenu.menuController).thenReturn(menuController)
+ lifecycleOwner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+ }
+
+ @Test
+ fun `WHEN tab counter is created THEN count is 0`() {
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = showTabs,
+ store = BrowserStore(),
+ menu = tabCounterMenu,
+ ),
+ )
+
+ val view = button.createView(LinearLayout(testContext) as ViewGroup) as TabCounter
+ val counterText: TextView = view.findViewById(R.id.counter_text)
+ assertEquals("0", counterText.text)
+ }
+
+ @Test
+ fun `GIVEN showMaskInPrivateMode is false WHEN tab counter is created THEN badge is not visible`() {
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = showTabs,
+ store = BrowserStore(),
+ menu = tabCounterMenu,
+ showMaskInPrivateMode = false,
+ ),
+ )
+
+ val view = button.createView(LinearLayout(testContext) as ViewGroup) as TabCounter
+ val counterMask: View = view.findViewById(R.id.counter_mask)
+ assertFalse(counterMask.isVisible)
+ }
+
+ @Test
+ fun `GIVEN showMaskInPrivateMode is true WHEN tab counter is created THEN badge is visible`() {
+ val tab = createTab("https://www.mozilla.org", true, "test-id")
+ val store = BrowserStore(BrowserState(tabs = listOf(tab), selectedTabId = "test-id"))
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = showTabs,
+ store = store,
+ menu = tabCounterMenu,
+ showMaskInPrivateMode = true,
+ ),
+ )
+
+ val view = button.createView(LinearLayout(testContext) as ViewGroup) as TabCounter
+ val counterMask: View = view.findViewById(R.id.counter_mask)
+ assertTrue(counterMask.isVisible)
+ }
+
+ @Test
+ fun `WHEN tab is added THEN tab count is updated`() {
+ val store = BrowserStore()
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = showTabs,
+ store = store,
+ menu = tabCounterMenu,
+ ),
+ )
+
+ whenever(button.updateCount(anyInt())).then { }
+ button.createView(LinearLayout(testContext) as ViewGroup) as TabCounter
+
+ store.dispatch(
+ TabListAction.AddTabAction(createTab("https://www.mozilla.org")),
+ ).joinBlocking()
+
+ verify(button).updateCount(eq(1))
+ }
+
+ @Test
+ fun `WHEN tab is restored THEN tab count is updated`() {
+ val store = BrowserStore()
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = showTabs,
+ store = store,
+ menu = tabCounterMenu,
+ ),
+ )
+
+ whenever(button.updateCount(anyInt())).then { }
+ button.createView(LinearLayout(testContext) as ViewGroup) as TabCounter
+
+ store.dispatch(
+ TabListAction.RestoreAction(
+ listOf(
+ RecoverableTab(
+ engineSessionState = null,
+ state = TabState("a", "https://www.mozilla.org"),
+ ),
+ ),
+ restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING,
+ ),
+ ).joinBlocking()
+
+ verify(button).updateCount(eq(1))
+ }
+
+ @Test
+ fun `WHEN tab is removed THEN tab count is updated`() {
+ val tab = createTab("https://www.mozilla.org")
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = showTabs,
+ store = store,
+ menu = tabCounterMenu,
+ ),
+ )
+
+ whenever(button.updateCount(anyInt())).then { }
+ button.createView(LinearLayout(testContext) as ViewGroup) as TabCounter
+
+ store.dispatch(TabListAction.RemoveTabAction(tab.id)).joinBlocking()
+ verify(button).updateCount(eq(0))
+ }
+
+ @Test
+ fun `WHEN private tab is added THEN tab count is updated`() {
+ val store = BrowserStore()
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = showTabs,
+ store = store,
+ menu = tabCounterMenu,
+ ),
+ )
+
+ whenever(button.updateCount(anyInt())).then { }
+ whenever(button.isPrivate(store)).then { true }
+
+ button.createView(LinearLayout(testContext) as ViewGroup) as TabCounter
+
+ store.dispatch(
+ TabListAction.AddTabAction(createTab("https://www.mozilla.org", private = true)),
+ ).joinBlocking()
+
+ verify(button).updateCount(eq(1))
+ }
+
+ @Test
+ fun `WHEN private tab is removed THEN tab count is updated`() {
+ val tab = createTab("https://www.mozilla.org", private = true)
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = showTabs,
+ store = store,
+ menu = tabCounterMenu,
+ ),
+ )
+
+ whenever(button.updateCount(anyInt())).then { }
+ whenever(button.isPrivate(store)).then { true }
+
+ button.createView(LinearLayout(testContext) as ViewGroup) as TabCounter
+
+ store.dispatch(TabListAction.RemoveTabAction(tab.id)).joinBlocking()
+ verify(button).updateCount(eq(0))
+ }
+
+ @Test
+ fun `WHEN tab counter is clicked THEN showTabs function is invoked`() {
+ var callbackInvoked = false
+ val store = BrowserStore(BrowserState(tabs = listOf()))
+ val button = spy(
+ TabCounterToolbarButton(
+ lifecycleOwner,
+ false,
+ showTabs = {
+ callbackInvoked = true
+ },
+ store = store,
+ menu = tabCounterMenu,
+ ),
+ )
+
+ val parent = spy(LinearLayout(testContext))
+ doReturn(true).`when`(parent).isAttachedToWindow
+
+ val view = button.createView(parent) as TabCounter
+ view.performClick()
+ assertTrue(callbackInvoked)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/toolbar/TabsToolbarFeatureTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/toolbar/TabsToolbarFeatureTest.kt
new file mode 100644
index 0000000000..ad1ad461b3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/toolbar/TabsToolbarFeatureTest.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.tabs.toolbar
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.ui.tabcounter.TabCounterMenu
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class TabsToolbarFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val showTabs: () -> Unit = mock()
+ private val tabCounterMenu: TabCounterMenu = mock()
+ val toolbar: Toolbar = mock()
+
+ private lateinit var tabsToolbarFeature: TabsToolbarFeature
+ private lateinit var lifecycleOwner: MockedLifecycleOwner
+
+ internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {
+ override val lifecycle: Lifecycle = LifecycleRegistry(this).apply {
+ currentState = initialState
+ }
+ }
+
+ @Before
+ fun setUp() {
+ lifecycleOwner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+ }
+
+ @Test
+ fun `feature adds 'tabs' button to toolbar`() {
+ val store = BrowserStore()
+ val sessionId: String? = null
+
+ tabsToolbarFeature = TabsToolbarFeature(
+ toolbar = toolbar,
+ store = store,
+ sessionId = sessionId,
+ lifecycleOwner = lifecycleOwner,
+ showTabs = showTabs,
+ tabCounterMenu = tabCounterMenu,
+ )
+
+ verify(toolbar).addBrowserAction(any())
+ }
+
+ @Test
+ fun `feature does not add tabs button when session is a customtab`() {
+ val customTabId = "custom-id"
+ val customTabSessionState =
+ CustomTabSessionState(
+ id = customTabId,
+ content = ContentState("https://mozilla.org"),
+ config = mock(),
+ )
+
+ val browserState = BrowserState(customTabs = listOf(customTabSessionState))
+ val store = BrowserStore(initialState = browserState)
+
+ tabsToolbarFeature = TabsToolbarFeature(
+ toolbar = toolbar,
+ store = store,
+ sessionId = customTabId,
+ lifecycleOwner = lifecycleOwner,
+ showTabs = showTabs,
+ tabCounterMenu = tabCounterMenu,
+ )
+
+ verify(toolbar, never()).addBrowserAction(any())
+ }
+
+ @Test
+ fun `feature adds tab button when session found but not a customtab`() {
+ val tabId = "tab-id"
+
+ val browserState = BrowserState()
+ val store = BrowserStore(initialState = browserState)
+
+ tabsToolbarFeature = TabsToolbarFeature(
+ toolbar = toolbar,
+ store = store,
+ sessionId = tabId,
+ lifecycleOwner = lifecycleOwner,
+ showTabs = showTabs,
+ tabCounterMenu = tabCounterMenu,
+ )
+
+ verify(toolbar).addBrowserAction(any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/tabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/tabs/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/tabs/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/tabs/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/tabs/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/tabs/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/toolbar/README.md b/mobile/android/android-components/components/feature/toolbar/README.md
new file mode 100644
index 0000000000..bc9834da10
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Toolbar
+
+A component that connects a (concept) toolbar implementation with the browser session module.
+
+## Usage
+
+### 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-toolbar:{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/toolbar/build.gradle b/mobile/android/android-components/components/feature/toolbar/build.gradle
new file mode 100644
index 0000000000..e950512518
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/build.gradle
@@ -0,0 +1,57 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.toolbar'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ api project(':concept-toolbar')
+ implementation project(':feature-session')
+ implementation project(':browser-state')
+ implementation project(':browser-domains')
+ implementation project(':concept-engine')
+ implementation project(':concept-storage')
+ implementation project(':lib-publicsuffixlist')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/toolbar/proguard-rules.pro b/mobile/android/android-components/components/feature/toolbar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/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/toolbar/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/toolbar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1eccdee26a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ContainerToolbarAction.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ContainerToolbarAction.kt
new file mode 100644
index 0000000000..33d166a9c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ContainerToolbarAction.kt
@@ -0,0 +1,89 @@
+/* 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.toolbar
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import androidx.core.content.ContextCompat.getColor
+import mozilla.components.browser.state.state.ContainerState
+import mozilla.components.browser.state.state.ContainerState.Color
+import mozilla.components.browser.state.state.ContainerState.Icon
+import mozilla.components.concept.toolbar.Toolbar.Action
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.ktx.android.view.setPadding
+import mozilla.components.support.utils.DrawableUtils.loadAndTintDrawable
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * An action button that represents a container to be added to the toolbar.
+ *
+ * @param container Associated [ContainerState]'s icon and color to render in the toolbar.
+ * @param padding A optional custom padding.
+ * @param listener A optional callback that will be invoked whenever the button is pressed.
+ */
+class ContainerToolbarAction(
+ internal val container: ContainerState,
+ internal val padding: Padding? = null,
+ private var listener: (() -> Unit)? = null,
+) : Action {
+ override fun createView(parent: ViewGroup): View {
+ val rootView = LayoutInflater.from(parent.context)
+ .inflate(R.layout.mozac_feature_toolbar_container_action_layout, parent, false)
+
+ listener?.let { clickListener ->
+ rootView.setOnClickListener { clickListener.invoke() }
+ }
+
+ padding?.let { rootView.setPadding(it) }
+
+ return rootView
+ }
+
+ override fun bind(view: View) {
+ val imageView = view.findViewById<ImageView>(R.id.container_action_image)
+ imageView.contentDescription = container.name
+ imageView.setImageDrawable(getIcon(view.context, container))
+ }
+
+ @Suppress("ComplexMethod")
+ internal fun getIcon(context: Context, container: ContainerState): Drawable {
+ @ColorInt val tint = getTint(context, container.color)
+
+ return when (container.icon) {
+ Icon.FINGERPRINT -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_fingerprinter_24, tint)
+ Icon.BRIEFCASE -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_briefcase, tint)
+ Icon.DOLLAR -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_dollar, tint)
+ Icon.CART -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_cart, tint)
+ Icon.CIRCLE -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_circle, tint)
+ Icon.GIFT -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_gift, tint)
+ Icon.VACATION -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_vacation, tint)
+ Icon.FOOD -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_food, tint)
+ Icon.FRUIT -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_fruit, tint)
+ Icon.PET -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_pet, tint)
+ Icon.TREE -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_tree, tint)
+ Icon.CHILL -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_chill, tint)
+ Icon.FENCE -> loadAndTintDrawable(context, iconsR.drawable.mozac_ic_fence, tint)
+ }
+ }
+
+ private fun getTint(context: Context, color: Color): Int {
+ return when (color) {
+ Color.BLUE -> getColor(context, R.color.mozac_feature_toolbar_container_blue)
+ Color.TURQUOISE -> getColor(context, R.color.mozac_feature_toolbar_container_turquoise)
+ Color.GREEN -> getColor(context, R.color.mozac_feature_toolbar_container_green)
+ Color.YELLOW -> getColor(context, R.color.mozac_feature_toolbar_container_yellow)
+ Color.ORANGE -> getColor(context, R.color.mozac_feature_toolbar_container_orange)
+ Color.RED -> getColor(context, R.color.mozac_feature_toolbar_container_red)
+ Color.PINK -> getColor(context, R.color.mozac_feature_toolbar_container_pink)
+ Color.PURPLE -> getColor(context, R.color.mozac_feature_toolbar_container_purple)
+ Color.TOOLBAR -> getColor(context, R.color.mozac_feature_toolbar_container_toolbar)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ContainerToolbarFeature.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ContainerToolbarFeature.kt
new file mode 100644
index 0000000000..a83eac6e6b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ContainerToolbarFeature.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.toolbar
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Container toolbar implementation that updates the toolbar with the container page action
+ * whenever the selected tab changes.
+ */
+class ContainerToolbarFeature(
+ private val toolbar: Toolbar,
+ private var store: BrowserStore,
+) : LifecycleAwareFeature {
+ private var containerPageAction: ContainerToolbarAction? = null
+ private var scope: CoroutineScope? = null
+
+ init {
+ renderContainerAction(store.state)
+ }
+
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ flow.distinctUntilChangedBy { it.selectedTab }
+ .collect { state ->
+ renderContainerAction(state, state.selectedTab)
+ }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun renderContainerAction(state: BrowserState, tab: SessionState? = null) {
+ val containerState = state.containers[tab?.contextId]
+
+ if (containerState == null) {
+ // Entered a normal tab from a container tab. Remove the old container
+ // page action.
+ containerPageAction?.let {
+ toolbar.removePageAction(it)
+ toolbar.invalidateActions()
+ containerPageAction = null
+ }
+ return
+ } else if (containerState == containerPageAction?.container) {
+ // Do nothing since we're still in a tab with same container.
+ return
+ }
+
+ // Remove the old container page action and create a new action with the new
+ // container state.
+ containerPageAction?.let {
+ toolbar.removePageAction(it)
+ containerPageAction = null
+ }
+
+ containerPageAction = ContainerToolbarAction(containerState).also { action ->
+ toolbar.addPageAction(action)
+ toolbar.invalidateActions()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeature.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeature.kt
new file mode 100644
index 0000000000..f1082f67c2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeature.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.toolbar
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.concept.toolbar.Toolbar
+import java.util.SortedSet
+
+/**
+ * Feature implementation for connecting a toolbar with a list of autocomplete providers.
+ *
+ * @param toolbar the [Toolbar] to connect to autocomplete providers.
+ * @param engine (optional) instance of a browser [Engine] to issue
+ * [Engine.speculativeConnect] calls on successful URL autocompletion.
+ * @param shouldAutocomplete (optional) lambda expression that returns true if
+ * autocomplete is shown. Otherwise, autocomplete is not shown.
+ * @param scope (optional) [CoroutineScope] in which to query autocompletion providers.
+ */
+class ToolbarAutocompleteFeature(
+ val toolbar: Toolbar,
+ val engine: Engine? = null,
+ val shouldAutocomplete: () -> Boolean = { true },
+) {
+ @VisibleForTesting
+ internal var autocompleteProviders: SortedSet<AutocompleteProvider> = sortedSetOf()
+
+ init {
+ toolbar.setAutocompleteListener { query, delegate ->
+ if (!shouldAutocomplete() || autocompleteProviders.isEmpty() || query.isBlank()) {
+ delegate.noAutocompleteResult(query)
+ } else {
+ val result = autocompleteProviders
+ .firstNotNullOfOrNull { it.getAutocompleteSuggestion(query) }
+
+ if (result != null) {
+ delegate.applyAutocompleteResult(result) {
+ engine?.speculativeConnect(result.url)
+ }
+ } else {
+ delegate.noAutocompleteResult(query)
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the list of providers used for autocompletion results.
+ * Changes will take effect the next time user changes their input.
+ *
+ * @param providers New list of autocomplete providers.
+ * The list can be empty in which case autocompletion will be disabled until there is at least one provider.
+ * @param refreshAutocomplete Whether to immediately update the autocompletion suggestion
+ * based on the new providers.
+ */
+ @Synchronized
+ fun updateAutocompleteProviders(
+ providers: List<AutocompleteProvider>,
+ refreshAutocomplete: Boolean = true,
+ ) {
+ autocompleteProviders.clear()
+ autocompleteProviders.addAll(providers)
+ if (refreshAutocomplete) toolbar.refreshAutocomplete()
+ }
+
+ /**
+ * Adds the specified provider to the current list of providers.
+ *
+ * @param provider New [AutocompleteProvider] to add to the current list.
+ * If this exact instance already exists it will not be added again.
+ * @param refreshAutocomplete Whether to immediately update the autocompletion suggestion
+ * based on the new providers.
+ *
+ * @return `true` if the provider has been added, `false` if the provider already exists.
+ */
+ @Synchronized
+ fun addAutocompleteProvider(
+ provider: AutocompleteProvider,
+ refreshAutocomplete: Boolean = true,
+ ): Boolean {
+ return autocompleteProviders.add(provider).also {
+ if (refreshAutocomplete) toolbar.refreshAutocomplete()
+ }
+ }
+
+ /**
+ * Remove an autocomplete provider from the current providers list.
+ *
+ * @param provider [AutocompleteProvider] instance to remove from the current list.
+ * If it isn't set already calling this method will have no effect.
+ * @param refreshAutocomplete Whether to immediately update the autocompletion suggestion
+ * based on the new providers.
+ *
+ * @return `true` if the provider has been removed, `false` if the provider could not be found.
+ */
+ @Synchronized
+ fun removeAutocompleteProvider(
+ provider: AutocompleteProvider,
+ refreshAutocomplete: Boolean = true,
+ ): Boolean {
+ return autocompleteProviders.remove(provider).also {
+ if (refreshAutocomplete) toolbar.refreshAutocomplete()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarBehaviorController.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarBehaviorController.kt
new file mode 100644
index 0000000000..7928f1ac64
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarBehaviorController.kt
@@ -0,0 +1,79 @@
+/* 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.toolbar
+
+import androidx.annotation.VisibleForTesting
+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.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.ScrollableToolbar
+import mozilla.components.lib.state.ext.flowScoped
+
+/**
+ * Controls how a dynamic toolbar should behave based on the current tab state.
+ *
+ * Responsible to enforce the following:
+ * - toolbar should not be scrollable if the page has not finished loading
+ */
+class ToolbarBehaviorController(
+ private val toolbar: ScrollableToolbar,
+ private val store: BrowserStore,
+ private val customTabId: String? = null,
+) {
+ @VisibleForTesting
+ internal var updatesScope: CoroutineScope? = null
+
+ /**
+ * Starts listening for changes in the current tab and updates how the toolbar should behave.
+ */
+ fun start() {
+ updatesScope = store.flowScoped { flow ->
+ flow.mapNotNull { state ->
+ state.findCustomTabOrSelectedTab(customTabId)
+ }.distinctUntilChangedBy {
+ arrayOf(it.content.loading, it.content.showToolbarAsExpanded)
+ }.collect { state ->
+ if (state.content.showToolbarAsExpanded) {
+ expandToolbar()
+ store.dispatch(ContentAction.UpdateExpandedToolbarStateAction(state.id, false))
+ return@collect
+ }
+
+ if (state.content.loading) {
+ expandToolbar()
+ disableScrolling()
+ } else if (!state.content.loading) {
+ enableScrolling()
+ }
+ }
+ }
+ }
+
+ /**
+ * Stop listening for changes in the current tab.
+ */
+ fun stop() {
+ updatesScope?.cancel()
+ }
+
+ @VisibleForTesting
+ internal fun expandToolbar() {
+ toolbar.expand()
+ }
+
+ @VisibleForTesting
+ internal fun disableScrolling() {
+ toolbar.disableScrolling()
+ }
+
+ @VisibleForTesting
+ internal fun enableScrolling() {
+ toolbar.enableScrolling()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarFeature.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarFeature.kt
new file mode 100644
index 0000000000..e8ba395dd4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarFeature.kt
@@ -0,0 +1,102 @@
+/* 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.toolbar
+
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+
+/**
+ * A function representing the search use case, accepting
+ * the search terms as string.
+ */
+typealias SearchUseCase = (String) -> Unit
+
+/**
+ * Feature implementation for connecting a toolbar implementation with the session module.
+ */
+class ToolbarFeature(
+ private val toolbar: Toolbar,
+ store: BrowserStore,
+ loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ searchUseCase: SearchUseCase? = null,
+ customTabId: String? = null,
+ shouldDisplaySearchTerms: Boolean = false,
+ urlRenderConfiguration: UrlRenderConfiguration? = null,
+) : LifecycleAwareFeature, UserInteractionHandler {
+ @VisibleForTesting
+ internal var presenter = ToolbarPresenter(
+ toolbar,
+ store,
+ customTabId,
+ shouldDisplaySearchTerms,
+ urlRenderConfiguration,
+ )
+
+ @VisibleForTesting
+ internal var interactor = ToolbarInteractor(toolbar, loadUrlUseCase, searchUseCase)
+
+ @VisibleForTesting
+ internal var controller = ToolbarBehaviorController(toolbar, store, customTabId)
+
+ /**
+ * Start feature: App is in the foreground.
+ */
+ override fun start() {
+ interactor.start()
+ presenter.start()
+ controller.start()
+ }
+
+ /**
+ * Handler for back pressed events in activities that use this feature.
+ *
+ * @return true if the event was handled, otherwise false.
+ */
+ override fun onBackPressed(): Boolean = toolbar.onBackPressed()
+
+ /**
+ * Stop feature: App is in the background.
+ */
+ override fun stop() {
+ presenter.stop()
+ controller.stop()
+ toolbar.onStop()
+ }
+
+ /**
+ * Configuration that controls how URLs are rendered.
+ *
+ * @property publicSuffixList A shared/global [PublicSuffixList] object required to extract certain domain parts.
+ * @property registrableDomainColor Text color that should be used for the registrable domain of the URL (see
+ * [PublicSuffixList.getPublicSuffixPlusOne] for an explanation of "registrable domain".
+ * @property urlColor Optional text color used for the URL.
+ * @property renderStyle Sealed class that controls the style of the url to be displayed
+ */
+ data class UrlRenderConfiguration(
+ internal val publicSuffixList: PublicSuffixList,
+ @ColorInt internal val registrableDomainColor: Int,
+ @ColorInt internal val urlColor: Int? = null,
+ internal val renderStyle: RenderStyle = RenderStyle.ColoredUrl,
+ )
+
+ /**
+ * Controls how the url should be styled
+ *
+ * RegistrableDomain: displays only the url, uncolored
+ * ColoredUrl: displays the registrableDomain with color and url with another color
+ * UncoloredUrl: displays the full url, uncolored
+ */
+ sealed class RenderStyle {
+ object RegistrableDomain : RenderStyle()
+ object ColoredUrl : RenderStyle()
+ object UncoloredUrl : RenderStyle()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarInteractor.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarInteractor.kt
new file mode 100644
index 0000000000..0026af04d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarInteractor.kt
@@ -0,0 +1,36 @@
+/* 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.toolbar
+
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.ktx.kotlin.isUrl
+import mozilla.components.support.ktx.kotlin.toNormalizedUrl
+
+/**
+ * Connects a toolbar instance to the browser engine via use cases
+ */
+class ToolbarInteractor(
+ private val toolbar: Toolbar,
+ private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
+ private val searchUseCase: SearchUseCase? = null,
+) {
+
+ /**
+ * Starts this interactor. Makes sure this interactor is listening
+ * to relevant UI changes and triggers the corresponding use-cases
+ * in response.
+ */
+ fun start() {
+ toolbar.setOnUrlCommitListener { text ->
+ when {
+ text.isUrl() -> loadUrlUseCase.invoke(text.toNormalizedUrl())
+ searchUseCase != null -> searchUseCase.invoke(text)
+ else -> loadUrlUseCase.invoke(text)
+ }
+ true
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarPresenter.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarPresenter.kt
new file mode 100644
index 0000000000..d8951a1417
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarPresenter.kt
@@ -0,0 +1,114 @@
+/* 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.toolbar
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.concept.toolbar.Toolbar.Highlight
+import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection
+import mozilla.components.feature.toolbar.internal.URLRenderer
+import mozilla.components.lib.state.ext.flowScoped
+
+/**
+ * Presenter implementation for a toolbar implementation in order to update the toolbar whenever
+ * the state of the selected session.
+ */
+class ToolbarPresenter(
+ private val toolbar: Toolbar,
+ private val store: BrowserStore,
+ private val customTabId: String? = null,
+ private val shouldDisplaySearchTerms: Boolean = false,
+ urlRenderConfiguration: ToolbarFeature.UrlRenderConfiguration? = null,
+) {
+ @VisibleForTesting
+ internal var renderer = URLRenderer(toolbar, urlRenderConfiguration)
+
+ private var scope: CoroutineScope? = null
+
+ /**
+ * Start presenter: Display data in toolbar.
+ */
+ fun start() {
+ renderer.start()
+
+ scope = store.flowScoped { flow ->
+ flow.distinctUntilChangedBy { it.findCustomTabOrSelectedTab(customTabId) }
+ .collect { state ->
+ render(state)
+ }
+ }
+ }
+
+ fun stop() {
+ scope?.cancel()
+ renderer.stop()
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun render(state: BrowserState) {
+ val tab = state.findCustomTabOrSelectedTab(customTabId)
+
+ if (tab != null) {
+ if (shouldDisplaySearchTerms && tab.content.searchTerms.isNotBlank()) {
+ toolbar.url = tab.content.searchTerms
+ } else {
+ renderer.post(tab.content.url)
+ }
+
+ toolbar.setSearchTerms(tab.content.searchTerms)
+ toolbar.displayProgress(tab.content.progress)
+
+ toolbar.siteSecure = if (tab.content.securityInfo.secure) {
+ Toolbar.SiteSecurity.SECURE
+ } else {
+ Toolbar.SiteSecurity.INSECURE
+ }
+
+ toolbar.siteTrackingProtection = when {
+ tab.trackingProtection.ignoredOnTrackingProtection -> SiteTrackingProtection.OFF_FOR_A_SITE
+ tab.trackingProtection.enabled && tab.trackingProtection.blockedTrackers.isNotEmpty() ->
+ SiteTrackingProtection.ON_TRACKERS_BLOCKED
+
+ tab.trackingProtection.enabled -> SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED
+
+ else -> SiteTrackingProtection.OFF_GLOBALLY
+ }
+
+ updateHighlight(tab)
+ } else {
+ clear()
+ }
+ }
+
+ private fun updateHighlight(tab: SessionState) {
+ toolbar.highlight = when {
+ tab.content.permissionHighlights.permissionsChanged ||
+ tab.trackingProtection.ignoredOnTrackingProtection
+ -> Highlight.PERMISSIONS_CHANGED
+ else -> Highlight.NONE
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun clear() {
+ renderer.post("")
+
+ toolbar.setSearchTerms("")
+ toolbar.displayProgress(0)
+
+ toolbar.siteSecure = Toolbar.SiteSecurity.INSECURE
+
+ toolbar.siteTrackingProtection = SiteTrackingProtection.OFF_GLOBALLY
+ toolbar.highlight = Highlight.NONE
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/WebExtensionToolbarAction.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/WebExtensionToolbarAction.kt
new file mode 100644
index 0000000000..653f6e7148
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/WebExtensionToolbarAction.kt
@@ -0,0 +1,98 @@
+/* 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.toolbar
+
+import android.graphics.drawable.BitmapDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.ktx.android.view.setPadding
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * An action button that represents an web extension item to be added to the toolbar.
+ *
+ * @param action Associated [WebExtensionBrowserAction]
+ * @param listener Callback that will be invoked whenever the button is pressed
+ */
+open class WebExtensionToolbarAction(
+ internal var action: WebExtensionBrowserAction,
+ internal val padding: Padding? = null,
+ internal val iconJobDispatcher: CoroutineDispatcher,
+ internal val listener: () -> Unit,
+) : Toolbar.Action {
+ internal var iconJob: Job? = null
+
+ override fun createView(parent: ViewGroup): View {
+ val rootView = LayoutInflater.from(parent.context)
+ .inflate(R.layout.mozac_feature_toolbar_web_extension_action_layout, parent, false)
+
+ rootView.isEnabled = action.enabled ?: true
+ rootView.setOnClickListener { listener.invoke() }
+
+ val backgroundResource =
+ parent.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless)
+
+ rootView.setBackgroundResource(backgroundResource)
+ padding?.let { rootView.setPadding(it) }
+
+ parent.addOnAttachStateChangeListener(
+ object : View.OnAttachStateChangeListener {
+ override fun onViewDetachedFromWindow(view: View) {
+ iconJob?.cancel()
+ }
+
+ override fun onViewAttachedToWindow(view: View) = Unit
+ },
+ )
+ return rootView
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ override fun bind(view: View) {
+ val imageView = view.findViewById<ImageView>(R.id.action_image)
+ val textView = view.findViewById<TextView>(R.id.badge_text)
+
+ iconJob = CoroutineScope(iconJobDispatcher).launch {
+ try {
+ val icon = action.loadIcon?.invoke(imageView.measuredHeight)
+ icon?.let {
+ MainScope().launch {
+ imageView.setImageDrawable(BitmapDrawable(view.context.resources, it))
+ }
+ }
+ } catch (throwable: Throwable) {
+ MainScope().launch {
+ imageView.setImageResource(
+ iconsR.drawable.mozac_ic_web_extension_default_icon,
+ )
+ }
+ Log.log(
+ Log.Priority.ERROR,
+ "mozac-webextensions",
+ throwable,
+ "Failed to load browser action icon, falling back to default.",
+ )
+ }
+ }
+
+ action.title?.let { imageView.contentDescription = it }
+ action.badgeText?.let { textView.text = it }
+ action.badgeTextColor?.let { textView.setTextColor(it) }
+ action.badgeBackgroundColor?.let { textView.setBackgroundColor(it) }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeature.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeature.kt
new file mode 100644
index 0000000000..b00228dd15
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeature.kt
@@ -0,0 +1,170 @@
+/* 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.toolbar
+
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collect
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
+
+/**
+ * Web extension toolbar implementation that updates the toolbar whenever the state of web
+ * extensions changes.
+ */
+class WebExtensionToolbarFeature(
+ private val toolbar: Toolbar,
+ private var store: BrowserStore,
+) : LifecycleAwareFeature {
+ // This maps web extension ids to [WebExtensionToolbarAction]s for efficient
+ // updates of global and tab-specific browser/page actions within the same
+ // lifecycle.
+ @VisibleForTesting
+ internal val webExtensionBrowserActions = HashMap<String, WebExtensionToolbarAction>()
+ internal val webExtensionPageActions = HashMap<String, WebExtensionToolbarAction>()
+
+ private var scope: CoroutineScope? = null
+
+ internal val iconThread = HandlerThread("IconThread")
+ internal val iconHandler by lazy {
+ iconThread.start()
+ Handler(iconThread.looper)
+ }
+
+ internal var iconJobDispatcher: CoroutineDispatcher = Dispatchers.Main
+
+ init {
+ renderWebExtensionActions(store.state)
+ }
+
+ /**
+ * Starts observing for the state of web extensions changes
+ */
+ override fun start() {
+ // The feature could start with an existing view and toolbar so
+ // we have to check if any stale actions (from uninstalled or
+ // disabled extensions) are being displayed and remove them.
+ webExtensionBrowserActions
+ .filterKeys { !store.state.extensions.containsKey(it) || store.state.extensions[it]?.enabled == false }
+ .forEach { (extensionId, action) ->
+ toolbar.removeBrowserAction(action)
+ toolbar.invalidateActions()
+ webExtensionBrowserActions.remove(extensionId)
+ }
+
+ webExtensionPageActions
+ .filterKeys { !store.state.extensions.containsKey(it) || store.state.extensions[it]?.enabled == false }
+ .forEach { (extensionId, action) ->
+ toolbar.removePageAction(action)
+ toolbar.invalidateActions()
+ webExtensionPageActions.remove(extensionId)
+ }
+
+ iconJobDispatcher = iconHandler.asCoroutineDispatcher("WebExtensionIconDispatcher")
+ scope = store.flowScoped { flow ->
+ flow.ifAnyChanged { arrayOf(it.selectedTab, it.extensions) }
+ .collect { state ->
+ renderWebExtensionActions(state, state.selectedTab)
+ }
+ }
+ }
+
+ override fun stop() {
+ iconJobDispatcher.cancel()
+ scope?.cancel()
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun renderWebExtensionActions(state: BrowserState, tab: SessionState? = null) {
+ val extensions = state.extensions.values.toList()
+ extensions.filter { it.enabled }.sortedBy { it.name }.forEach { extension ->
+ if (extensionNotAllowedInTab(extension, tab)) {
+ webExtensionPageActions[extension.id]?.let {
+ toolbar.removePageAction(it)
+ toolbar.invalidateActions()
+ webExtensionPageActions.remove(extension.id)
+ }
+ webExtensionBrowserActions[extension.id]?.let {
+ toolbar.removeBrowserAction(it)
+ toolbar.invalidateActions()
+ webExtensionBrowserActions.remove(extension.id)
+ }
+ return@forEach
+ }
+
+ extension.browserAction?.let { browserAction ->
+ addOrUpdateAction(
+ extension = extension,
+ globalAction = browserAction,
+ tabAction = tab?.extensionState?.get(extension.id)?.browserAction,
+ )
+ }
+
+ extension.pageAction?.let { pageAction ->
+ val tabPageAction = tab?.extensionState?.get(extension.id)?.pageAction
+
+ // Unlike browser actions, page actions are not displayed by default (only if enabled):
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action
+ if (pageAction.copyWithOverride(tabPageAction).enabled == true) {
+ addOrUpdateAction(
+ extension = extension,
+ globalAction = pageAction,
+ tabAction = tabPageAction,
+ isPageAction = true,
+ )
+ }
+ }
+ }
+ }
+
+ private fun extensionNotAllowedInTab(
+ extension: WebExtensionState?,
+ tab: SessionState?,
+ ): Boolean = extension?.allowedInPrivateBrowsing == false && tab?.content?.private == true
+
+ private fun addOrUpdateAction(
+ extension: WebExtensionState,
+ globalAction: Action,
+ tabAction: Action?,
+ isPageAction: Boolean = false,
+ ) {
+ val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions
+ // Add the global page/browser action if it doesn't exist
+ val toolbarAction = actionMap.getOrPut(extension.id) {
+ val toolbarAction = WebExtensionToolbarAction(
+ action = globalAction,
+ listener = globalAction.onClick,
+ iconJobDispatcher = iconJobDispatcher,
+ )
+ if (isPageAction) {
+ toolbar.addPageAction(toolbarAction)
+ } else {
+ toolbar.addBrowserAction(toolbarAction)
+ }
+ toolbar.invalidateActions()
+ toolbarAction
+ }
+
+ // Apply tab-specific override of page/browser action
+ tabAction?.let {
+ toolbarAction.action = globalAction.copyWithOverride(it)
+ toolbar.invalidateActions()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/internal/URLRenderer.kt b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/internal/URLRenderer.kt
new file mode 100644
index 0000000000..52adb9e999
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/internal/URLRenderer.kt
@@ -0,0 +1,126 @@
+/* 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.toolbar.internal
+
+import android.text.SpannableStringBuilder
+import android.text.SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE
+import android.text.style.ForegroundColorSpan
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.trySendBlocking
+import kotlinx.coroutines.launch
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.toolbar.ToolbarFeature
+
+/**
+ * Asynchronous URL renderer.
+ *
+ * This "renderer" will create a (potentially) colored URL (using spans) in a coroutine and set it on the [Toolbar].
+ */
+internal class URLRenderer(
+ private val toolbar: Toolbar,
+ private val configuration: ToolbarFeature.UrlRenderConfiguration?,
+) {
+ private val scope = CoroutineScope(Dispatchers.Main)
+
+ @VisibleForTesting internal var job: Job? = null
+
+ @VisibleForTesting internal val channel = Channel<String>(capacity = Channel.CONFLATED)
+
+ /**
+ * Starts this renderer which will listen for incoming URLs to render.
+ */
+ fun start() {
+ job = scope.launch {
+ for (url in channel) {
+ updateUrl(url)
+ }
+ }
+ }
+
+ /**
+ * Stops this renderer.
+ */
+ fun stop() {
+ job?.cancel()
+ }
+
+ /**
+ * Posts this [url] to the renderer.
+ */
+ fun post(url: String) {
+ try {
+ channel.trySendBlocking(url)
+ } catch (e: InterruptedException) {
+ // Ignore
+ }
+ }
+
+ @VisibleForTesting
+ internal suspend fun updateUrl(url: String) {
+ if (url.isEmpty() || configuration == null) {
+ toolbar.url = url
+ return
+ }
+
+ toolbar.url = when (configuration.renderStyle) {
+ // Display only the URL, uncolored
+ ToolbarFeature.RenderStyle.RegistrableDomain -> {
+ val host = url.toUri().host?.ifEmpty { null }
+ host?.let { getRegistrableDomain(host, configuration) } ?: url
+ }
+ // Display the registrableDomain with color and URL with another color
+ ToolbarFeature.RenderStyle.ColoredUrl -> SpannableStringBuilder(url).apply {
+ color(configuration.urlColor)
+ colorRegistrableDomain(configuration)
+ }
+ // Display the full URL, uncolored
+ ToolbarFeature.RenderStyle.UncoloredUrl -> url
+ }
+ }
+}
+
+private suspend fun getRegistrableDomain(host: String, configuration: ToolbarFeature.UrlRenderConfiguration) =
+ configuration.publicSuffixList.getPublicSuffixPlusOne(host).await()
+
+private suspend fun SpannableStringBuilder.colorRegistrableDomain(
+ configuration: ToolbarFeature.UrlRenderConfiguration,
+) {
+ val url = toString()
+ val host = url.toUri().host ?: return
+
+ val registrableDomain = configuration
+ .publicSuffixList
+ .getPublicSuffixPlusOne(host)
+ .await() ?: return
+
+ val index = url.indexOf(registrableDomain)
+ if (index == -1) {
+ return
+ }
+
+ setSpan(
+ ForegroundColorSpan(configuration.registrableDomainColor),
+ index,
+ index + registrableDomain.length,
+ SPAN_INCLUSIVE_INCLUSIVE,
+ )
+}
+
+private fun SpannableStringBuilder.color(@ColorInt urlColor: Int?) {
+ urlColor ?: return
+
+ setSpan(
+ ForegroundColorSpan(urlColor),
+ 0,
+ length,
+ SPAN_INCLUSIVE_INCLUSIVE,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/res/layout/mozac_feature_toolbar_container_action_layout.xml b/mobile/android/android-components/components/feature/toolbar/src/main/res/layout/mozac_feature_toolbar_container_action_layout.xml
new file mode 100644
index 0000000000..803aa1b068
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/res/layout/mozac_feature_toolbar_container_action_layout.xml
@@ -0,0 +1,20 @@
+<?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/. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:background="?android:selectableItemBackgroundBorderless"
+ tools:ignore="Overdraw">
+
+ <ImageView
+ android:id="@+id/container_action_image"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center"
+ android:importantForAccessibility="no" />
+
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/res/layout/mozac_feature_toolbar_web_extension_action_layout.xml b/mobile/android/android-components/components/feature/toolbar/src/main/res/layout/mozac_feature_toolbar_web_extension_action_layout.xml
new file mode 100644
index 0000000000..8e6a440829
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/res/layout/mozac_feature_toolbar_web_extension_action_layout.xml
@@ -0,0 +1,29 @@
+<?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/. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/counter_root"
+ android:layout_width="32dp"
+ android:layout_height="32dp">
+
+ <ImageView
+ android:id="@+id/action_image"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/badge_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|end"
+ android:background="#3B3B3C"
+ android:textColor="#FFFFFF"
+ android:textSize="12sp"
+ tools:text="18" />
+
+</FrameLayout>
diff --git a/mobile/android/android-components/components/feature/toolbar/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/toolbar/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..4d019dc690
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+<?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>
+ <color name="mozac_feature_toolbar_container_blue">#37adff</color>
+ <color name="mozac_feature_toolbar_container_turquoise">#00c79a</color>
+ <color name="mozac_feature_toolbar_container_green">#51cd00</color>
+ <color name="mozac_feature_toolbar_container_yellow">#ffcb00</color>
+ <color name="mozac_feature_toolbar_container_orange">#ff9f00</color>
+ <color name="mozac_feature_toolbar_container_red">#ff613d</color>
+ <color name="mozac_feature_toolbar_container_pink">#ff4bda</color>
+ <color name="mozac_feature_toolbar_container_purple">#af51f5</color>
+ <color name="mozac_feature_toolbar_container_toolbar">#7c7c7d</color>
+</resources>
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarActionTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarActionTest.kt
new file mode 100644
index 0000000000..44bed56d21
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarActionTest.kt
@@ -0,0 +1,67 @@
+/* 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.toolbar
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.ContainerState
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ContainerToolbarActionTest {
+
+ // Test container
+ private val container = ContainerState(
+ contextId = "contextId",
+ name = "Personal",
+ color = ContainerState.Color.GREEN,
+ icon = ContainerState.Icon.CART,
+ )
+
+ @Test
+ fun bind() {
+ val imageView: ImageView = spy(ImageView(testContext))
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.container_action_image)).thenReturn(imageView)
+ whenever(view.context).thenReturn(testContext)
+
+ val action = spy(ContainerToolbarAction(container))
+ action.bind(view)
+
+ verify(imageView).contentDescription = container.name
+ verify(imageView).setImageDrawable(any())
+ }
+
+ @Test
+ fun createView() {
+ var listenerWasClicked = false
+
+ val action = ContainerToolbarAction(container, padding = Padding(1, 2, 3, 4)) {
+ listenerWasClicked = true
+ }
+
+ val rootView = action.createView(LinearLayout(testContext))
+ rootView.performClick()
+
+ assertTrue(listenerWasClicked)
+ assertEquals(rootView.paddingLeft, 1)
+ assertEquals(rootView.paddingTop, 2)
+ assertEquals(rootView.paddingRight, 3)
+ assertEquals(rootView.paddingBottom, 4)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarFeatureTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarFeatureTest.kt
new file mode 100644
index 0000000000..d1d920b16e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarFeatureTest.kt
@@ -0,0 +1,97 @@
+/* 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.toolbar
+
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContainerState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class ContainerToolbarFeatureTest {
+ // Test container
+ private val container = ContainerState(
+ contextId = "1",
+ name = "Personal",
+ color = ContainerState.Color.GREEN,
+ icon = ContainerState.Icon.FINGERPRINT,
+ )
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `render a container action from browser state`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.example.org", id = "tab1", contextId = "1"),
+ ),
+ selectedTabId = "tab1",
+ containers = mapOf(
+ container.contextId to container,
+ ),
+ ),
+ ),
+ )
+ val containerToolbarFeature = getContainerToolbarFeature(toolbar, store)
+
+ verify(store).observeManually(any())
+ verify(containerToolbarFeature).renderContainerAction(any(), any())
+
+ val pageActionCaptor = argumentCaptor<ContainerToolbarAction>()
+ verify(toolbar).addPageAction(pageActionCaptor.capture())
+ assertEquals(container, pageActionCaptor.value.container)
+ }
+
+ @Test
+ fun `remove container page action when selecting a normal tab`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://www.example.org", id = "tab1", contextId = "1"),
+ createTab("https://www.mozilla.org", id = "tab2"),
+ ),
+ selectedTabId = "tab1",
+ containers = mapOf(
+ container.contextId to container,
+ ),
+ ),
+ ),
+ )
+ val containerToolbarFeature = getContainerToolbarFeature(toolbar, store)
+ store.dispatch(TabListAction.SelectTabAction("tab2")).joinBlocking()
+ coroutinesTestRule.testDispatcher.scheduler.advanceUntilIdle()
+
+ verify(store).observeManually(any())
+ verify(containerToolbarFeature, times(2)).renderContainerAction(any(), any())
+ verify(toolbar).removePageAction(any())
+ }
+
+ private fun getContainerToolbarFeature(
+ toolbar: Toolbar = mock(),
+ store: BrowserStore = BrowserStore(),
+ ): ContainerToolbarFeature {
+ val containerToolbarFeature = spy(ContainerToolbarFeature(toolbar, store))
+ containerToolbarFeature.start()
+ return containerToolbarFeature
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt
new file mode 100644
index 0000000000..3a5dbc1cfe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt
@@ -0,0 +1,596 @@
+/* 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.toolbar
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.domains.Domain
+import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
+import mozilla.components.browser.domains.autocomplete.DomainList
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.toolbar.AutocompleteDelegate
+import mozilla.components.concept.toolbar.AutocompleteProvider
+import mozilla.components.concept.toolbar.AutocompleteResult
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class ToolbarAutocompleteFeatureTest {
+ class TestToolbar : Toolbar {
+ override var highlight: Toolbar.Highlight = Toolbar.Highlight.NONE
+ override var siteTrackingProtection: Toolbar.SiteTrackingProtection =
+ Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+ override var title: String = ""
+ override var url: CharSequence = ""
+ override var siteSecure: Toolbar.SiteSecurity = Toolbar.SiteSecurity.INSECURE
+ override var private: Boolean = false
+
+ var autocompleteFilter: (suspend (String, AutocompleteDelegate) -> Unit)? = null
+
+ override fun setSearchTerms(searchTerms: String) {
+ fail()
+ }
+
+ override fun displayProgress(progress: Int) {
+ fail()
+ }
+
+ override fun onBackPressed(): Boolean {
+ fail()
+ return false
+ }
+
+ override fun onStop() {
+ fail()
+ }
+
+ override fun setOnUrlCommitListener(listener: (String) -> Boolean) {
+ fail()
+ }
+
+ override fun setAutocompleteListener(filter: suspend (String, AutocompleteDelegate) -> Unit) {
+ autocompleteFilter = filter
+ }
+
+ override fun addBrowserAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun removeBrowserAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun removePageAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun addPageAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun addNavigationAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun removeNavigationAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun setOnEditListener(listener: Toolbar.OnEditListener) {
+ fail()
+ }
+
+ override fun displayMode() {
+ fail()
+ }
+
+ override fun editMode(cursorPlacement: Toolbar.CursorPlacement) {
+ fail()
+ }
+
+ override fun addEditActionStart(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun addEditActionEnd(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun removeEditActionEnd(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun hideMenuButton() {
+ fail()
+ }
+
+ override fun showMenuButton() {
+ fail()
+ }
+
+ override fun setDisplayHorizontalPadding(horizontalPadding: Int) {
+ fail()
+ }
+
+ override fun hidePageActionSeparator() {
+ fail()
+ }
+
+ override fun showPageActionSeparator() {
+ fail()
+ }
+
+ override fun invalidateActions() {
+ fail()
+ }
+
+ override fun dismissMenu() {
+ fail()
+ }
+
+ override fun enableScrolling() {
+ fail()
+ }
+
+ override fun disableScrolling() {
+ fail()
+ }
+
+ override fun collapse() {
+ fail()
+ }
+
+ override fun expand() {
+ fail()
+ }
+ }
+
+ @Test
+ fun `feature can be used without providers`() = runTest {
+ val toolbar = TestToolbar()
+
+ ToolbarAutocompleteFeature(toolbar)
+
+ assertNotNull(toolbar.autocompleteFilter)
+
+ val autocompleteDelegate: AutocompleteDelegate = mock()
+ toolbar.autocompleteFilter!!("moz", autocompleteDelegate)
+
+ verify(autocompleteDelegate, never()).applyAutocompleteResult(any(), any())
+ verify(autocompleteDelegate, times(1)).noAutocompleteResult("moz")
+ }
+
+ @Test
+ fun `feature can be configured with providers`() = runTest {
+ val toolbar = TestToolbar()
+ var feature = ToolbarAutocompleteFeature(toolbar)
+ val autocompleteDelegate: AutocompleteDelegate = mock()
+
+ var history: AutocompleteProvider = mock()
+ val domains = object : BaseDomainAutocompleteProvider(DomainList.CUSTOM, { emptyList() }, 22) {
+ fun testDomains(list: List<Domain>) {
+ domains = list
+ }
+ }
+
+ // Can autocomplete with just an empty history provider.
+ feature.addAutocompleteProvider(history)
+ verifyNoAutocompleteResult(toolbar, autocompleteDelegate, "hi")
+
+ // Can autocomplete with a non-empty history provider.
+ doReturn(
+ AutocompleteResult(
+ input = "mo",
+ text = "mozilla.org",
+ url = "https://www.mozilla.org",
+ source = "memoryHistory",
+ totalItems = 1,
+ ),
+ ).`when`(history).getAutocompleteSuggestion("mo")
+
+ verifyNoAutocompleteResult(toolbar, autocompleteDelegate, "hi")
+ verifyAutocompleteResult(
+ toolbar,
+ autocompleteDelegate,
+ "mo",
+ AutocompleteResult(
+ input = "mo",
+ text = "mozilla.org",
+ url = "https://www.mozilla.org",
+ source = "memoryHistory",
+ totalItems = 1,
+ ),
+ )
+
+ // Can autocomplete with just an empty domain provider.
+ feature = ToolbarAutocompleteFeature(toolbar)
+ feature.addAutocompleteProvider(domains)
+
+ verifyNoAutocompleteResult(toolbar, autocompleteDelegate, "hi")
+
+ // Can autocomplete with a non-empty domain provider.
+ domains.testDomains(
+ listOf(
+ Domain.create("https://www.mozilla.org"),
+ ),
+ )
+
+ verifyNoAutocompleteResult(toolbar, autocompleteDelegate, "hi")
+ verifyAutocompleteResult(
+ toolbar,
+ autocompleteDelegate,
+ "mo",
+ AutocompleteResult(
+ input = "mo",
+ text = "mozilla.org",
+ url = "https://www.mozilla.org",
+ source = "custom",
+ totalItems = 1,
+ ),
+ )
+
+ // Can autocomplete with empty history and domain providers.
+ history = spy(AutocompleteProviderFake()) // use a real object so that the comparator will work
+ domains.testDomains(listOf())
+ feature.addAutocompleteProvider(history)
+
+ verifyNoAutocompleteResult(toolbar, autocompleteDelegate, "hi")
+
+ // Can autocomplete with both domains providing data; test that history is prioritized,
+ // falling back to domains.
+ domains.testDomains(
+ listOf(
+ Domain.create("https://www.mozilla.org"),
+ Domain.create("https://moscow.ru"),
+ ),
+ )
+
+ verifyAutocompleteResult(
+ toolbar,
+ autocompleteDelegate,
+ "mo",
+ AutocompleteResult(
+ input = "mo",
+ text = "mozilla.org",
+ url = "https://www.mozilla.org",
+ source = "custom",
+ totalItems = 2,
+ ),
+ )
+
+ doReturn(
+ AutocompleteResult(
+ input = "mo",
+ text = "mozilla.org",
+ url = "https://www.mozilla.org",
+ source = "memoryHistory",
+ totalItems = 1,
+ ),
+ ).`when`(history).getAutocompleteSuggestion("mo")
+
+ verifyAutocompleteResult(
+ toolbar,
+ autocompleteDelegate,
+ "mo",
+ AutocompleteResult(
+ input = "mo",
+ text = "mozilla.org",
+ url = "https://www.mozilla.org",
+ source = "memoryHistory",
+ totalItems = 1,
+ ),
+ )
+
+ verifyAutocompleteResult(
+ toolbar,
+ autocompleteDelegate,
+ "mos",
+ AutocompleteResult(
+ input = "mos",
+ text = "moscow.ru",
+ url = "https://moscow.ru",
+ source = "custom",
+ totalItems = 2,
+ ),
+ )
+ }
+
+ @Test
+ fun `feature triggers speculative connect for results if engine provided`() = runTest {
+ val toolbar = TestToolbar()
+ val engine: Engine = mock()
+ val feature = ToolbarAutocompleteFeature(toolbar, engine) { true }
+ val autocompleteDelegate: AutocompleteDelegate = mock()
+
+ val domains = object : BaseDomainAutocompleteProvider(DomainList.CUSTOM, { emptyList() }) {
+ fun testDomains(list: List<Domain>) {
+ domains = list
+ }
+ }
+ domains.testDomains(listOf(Domain.create("https://www.mozilla.org")))
+ feature.addAutocompleteProvider(domains)
+
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+
+ val callbackCaptor = argumentCaptor<() -> Unit>()
+ verify(autocompleteDelegate, times(1)).applyAutocompleteResult(any(), callbackCaptor.capture())
+ verify(engine, never()).speculativeConnect("https://www.mozilla.org")
+ callbackCaptor.value.invoke()
+ verify(engine).speculativeConnect("https://www.mozilla.org")
+ }
+
+ @Test
+ fun `WHEN should autocomplete returns false THEN return no results`() = runTest {
+ val toolbar = TestToolbar()
+ val engine: Engine = mock()
+ var shouldAutoComplete = false
+ val feature = ToolbarAutocompleteFeature(toolbar, engine) { shouldAutoComplete }
+ val autocompleteDelegate: AutocompleteDelegate = mock()
+
+ val domains = object : BaseDomainAutocompleteProvider(DomainList.CUSTOM, { emptyList() }) {
+ fun testDomains(list: List<Domain>) {
+ domains = list
+ }
+ }
+ domains.testDomains(listOf(Domain.create("https://www.mozilla.org")))
+ feature.addAutocompleteProvider(domains)
+
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+
+ verify(autocompleteDelegate, times(1)).noAutocompleteResult(any())
+ verify(engine, never()).speculativeConnect("https://www.mozilla.org")
+
+ shouldAutoComplete = true
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+
+ val callbackCaptor = argumentCaptor<() -> Unit>()
+ verify(autocompleteDelegate, times(1)).applyAutocompleteResult(any(), callbackCaptor.capture())
+ verify(engine, never()).speculativeConnect("https://www.mozilla.org")
+ callbackCaptor.value.invoke()
+ verify(engine).speculativeConnect("https://www.mozilla.org")
+ }
+
+ @Test
+ fun `GIVEN no autocomplete providers WHEN checking for autocomplete results THEN silently fail with no results`() = runTest {
+ val toolbar = TestToolbar()
+ val engine: Engine = mock()
+ val feature = ToolbarAutocompleteFeature(toolbar, engine) { true }
+ val autocompleteDelegate: AutocompleteDelegate = mock()
+
+ val domains = object : BaseDomainAutocompleteProvider(DomainList.CUSTOM, { emptyList() }) {
+ fun testDomains(list: List<Domain>) {
+ domains = list
+ }
+ }
+ domains.testDomains(listOf(Domain.create("https://www.mozilla.org")))
+
+ feature.addAutocompleteProvider(domains)
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+ val callbackCaptor = argumentCaptor<() -> Unit>()
+ verify(autocompleteDelegate, times(1)).applyAutocompleteResult(any(), callbackCaptor.capture())
+ verify(engine, never()).speculativeConnect("https://www.mozilla.org")
+ callbackCaptor.value.invoke()
+ verify(engine).speculativeConnect("https://www.mozilla.org")
+
+ // After checking the results for when a provider exists test what happens when no providers exist.
+ feature.removeAutocompleteProvider(domains)
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+ verify(autocompleteDelegate, times(1)).noAutocompleteResult(any())
+ verify(engine, times(1)).speculativeConnect("https://www.mozilla.org")
+ }
+
+ @Test
+ fun `GIVEN no initial autocomplete providers and one is added WHEN checking for autocomplete results THEN return autocomplete suggestions`() = runTest {
+ val toolbar = TestToolbar()
+ val engine: Engine = mock()
+ val feature = ToolbarAutocompleteFeature(toolbar, engine) { true }
+ val autocompleteDelegate: AutocompleteDelegate = mock()
+
+ val domains = object : BaseDomainAutocompleteProvider(DomainList.CUSTOM, { emptyList() }) {
+ fun testDomains(list: List<Domain>) {
+ domains = list
+ }
+ }
+ domains.testDomains(listOf(Domain.create("https://www.mozilla.org")))
+
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+ verify(autocompleteDelegate, times(1)).noAutocompleteResult(any())
+ verify(engine, never()).speculativeConnect("https://www.mozilla.org")
+
+ // After checking no results for when no providers exist test what happens when a new one is added.
+ feature.addAutocompleteProvider(domains)
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+ val callbackCaptor = argumentCaptor<() -> Unit>()
+ verify(autocompleteDelegate, times(1)).applyAutocompleteResult(any(), callbackCaptor.capture())
+ verify(engine, never()).speculativeConnect("https://www.mozilla.org")
+ callbackCaptor.value.invoke()
+ verify(engine).speculativeConnect("https://www.mozilla.org")
+ }
+
+ @Test
+ fun `GIVEN providers exist WHEN a new one is added THEN they are sorted by their priority`() {
+ val feature = ToolbarAutocompleteFeature(mock())
+
+ val provider1 = AutocompleteProviderFake(autocompletePriority = 11).also {
+ assertTrue(feature.addAutocompleteProvider(it))
+ }
+ val provider2 = AutocompleteProviderFake(autocompletePriority = 22).also {
+ assertTrue(feature.addAutocompleteProvider(it))
+ }
+ val provider3 = AutocompleteProviderFake(autocompletePriority = 3).also {
+ assertTrue(feature.addAutocompleteProvider(it))
+ }
+
+ assertEquals(
+ listOf(provider3, provider1, provider2),
+ feature.autocompleteProviders.toList(),
+ )
+ }
+
+ @Test
+ fun `GIVEN providers exist WHEN trying to add an existing one THEN avoid adding`() {
+ val feature = ToolbarAutocompleteFeature(mock())
+ val provider1 = AutocompleteProviderFake(autocompletePriority = 11).also {
+ feature.addAutocompleteProvider(it)
+ }
+ val provider2 = AutocompleteProviderFake(autocompletePriority = 22).also {
+ feature.addAutocompleteProvider(it)
+ }
+ val provider3 = AutocompleteProviderFake(autocompletePriority = 3).also {
+ feature.addAutocompleteProvider(it)
+ }
+ val provider4 = AutocompleteProviderFake(autocompletePriority = 15).also {
+ feature.addAutocompleteProvider(it)
+ }
+
+ assertTrue(feature.removeAutocompleteProvider(provider4))
+
+ assertEquals(
+ listOf(provider3, provider1, provider2),
+ feature.autocompleteProviders.toList(),
+ )
+ }
+
+ @Test
+ fun `GIVEN providers don't exist WHEN trying to remove one THEN avoid fail gracefully`() {
+ val feature = ToolbarAutocompleteFeature(mock())
+ val provider1 = AutocompleteProviderFake(autocompletePriority = 11).also {
+ feature.addAutocompleteProvider(it)
+ }
+ val provider2 = AutocompleteProviderFake(autocompletePriority = 22).also {
+ feature.addAutocompleteProvider(it)
+ }
+ val provider3 = AutocompleteProviderFake(autocompletePriority = 3)
+
+ assertFalse(feature.removeAutocompleteProvider(provider3))
+
+ assertEquals(
+ listOf(provider1, provider2),
+ feature.autocompleteProviders.toList(),
+ )
+ }
+
+ @Test
+ fun `GIVEN providers exist WHEN removing one THEN keep the other sorted`() {
+ val feature = ToolbarAutocompleteFeature(mock())
+ val provider1 = AutocompleteProviderFake(autocompletePriority = 11).also {
+ assertTrue(feature.addAutocompleteProvider(it))
+ }
+ val provider2 = AutocompleteProviderFake(autocompletePriority = 22).also {
+ assertTrue(feature.addAutocompleteProvider(it))
+ }
+
+ assertFalse(feature.addAutocompleteProvider(provider1))
+
+ assertEquals(
+ listOf(provider1, provider2),
+ feature.autocompleteProviders.toList(),
+ )
+ }
+
+ @Test
+ fun `GIVEN providers exist WHEN when they are updated THEN the old ones are replaced by new ones sorted by priority`() {
+ val feature = ToolbarAutocompleteFeature(mock())
+ feature.autocompleteProviders = sortedSetOf(
+ AutocompleteProviderFake(autocompletePriority = 11),
+ AutocompleteProviderFake(autocompletePriority = 22),
+ )
+ val provider1 = AutocompleteProviderFake(autocompletePriority = 11).also {
+ feature.addAutocompleteProvider(it)
+ }
+ val provider2 = AutocompleteProviderFake(autocompletePriority = 2).also {
+ feature.addAutocompleteProvider(it)
+ }
+ val provider3 = AutocompleteProviderFake().also {
+ feature.addAutocompleteProvider(it)
+ }
+
+ feature.updateAutocompleteProviders(listOf(provider1, provider2, provider3))
+
+ assertEquals(
+ listOf(provider3, provider2, provider1),
+ feature.autocompleteProviders.toList(),
+ )
+ }
+
+ @Test
+ fun `GIVEN a request to refresh autocomplete WHEN the providers are updated THEN also refresh autocomplete`() {
+ val toolbar: Toolbar = mock()
+ val feature = ToolbarAutocompleteFeature(toolbar)
+
+ feature.updateAutocompleteProviders(emptyList(), false)
+ verify(toolbar, never()).refreshAutocomplete()
+
+ feature.updateAutocompleteProviders(emptyList(), true)
+ verify(toolbar).refreshAutocomplete()
+ }
+
+ @Test
+ fun `GIVEN a request to refresh autocomplete WHEN a provider is added THEN also refresh autocomplete`() {
+ val toolbar: Toolbar = mock()
+ val feature = ToolbarAutocompleteFeature(toolbar)
+
+ feature.addAutocompleteProvider(mock(), false)
+ verify(toolbar, never()).refreshAutocomplete()
+
+ feature.addAutocompleteProvider(mock(), true)
+ verify(toolbar).refreshAutocomplete()
+ }
+
+ @Test
+ fun `GIVEN a request to refresh autocomplete WHEN a provider is removed THEN also refresh autocomplete`() {
+ val toolbar: Toolbar = mock()
+ val feature = ToolbarAutocompleteFeature(toolbar)
+
+ feature.removeAutocompleteProvider(mock(), false)
+ verify(toolbar, never()).refreshAutocomplete()
+
+ feature.removeAutocompleteProvider(mock(), true)
+ verify(toolbar).refreshAutocomplete()
+ }
+
+ @Suppress("SameParameterValue")
+ private fun verifyNoAutocompleteResult(toolbar: TestToolbar, autocompleteDelegate: AutocompleteDelegate, query: String) = runTest {
+ toolbar.autocompleteFilter!!(query, autocompleteDelegate)
+
+ verify(autocompleteDelegate, never()).applyAutocompleteResult(any(), any())
+ verify(autocompleteDelegate, times(1)).noAutocompleteResult(query)
+ reset(autocompleteDelegate)
+ }
+
+ private fun verifyAutocompleteResult(toolbar: TestToolbar, autocompleteDelegate: AutocompleteDelegate, query: String, result: AutocompleteResult) = runTest {
+ toolbar.autocompleteFilter!!.invoke(query, autocompleteDelegate)
+
+ verify(autocompleteDelegate, times(1)).applyAutocompleteResult(eq(result), any())
+ verify(autocompleteDelegate, never()).noAutocompleteResult(query)
+ reset(autocompleteDelegate)
+ }
+}
+
+/**
+ * Empty implementation of [AutocompleteProvider].
+ * [getAutocompleteSuggestion] will return `null` by default.
+ *
+ * @param resultToReturn Optional nullable [AutocompleteResult] to return for all queries.
+ */
+private class AutocompleteProviderFake(
+ val resultToReturn: AutocompleteResult? = null,
+ override val autocompletePriority: Int = 0,
+) : AutocompleteProvider {
+ override suspend fun getAutocompleteSuggestion(query: String) = resultToReturn
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarBehaviorControllerTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarBehaviorControllerTest.kt
new file mode 100644
index 0000000000..48d583c530
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarBehaviorControllerTest.kt
@@ -0,0 +1,200 @@
+/* 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.toolbar
+
+import android.os.Looper.getMainLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.isActive
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class ToolbarBehaviorControllerTest {
+
+ @Test
+ fun `Controller should check the status of the provided custom tab id`() {
+ val customTabContent: ContentState = mock()
+ val normalTabContent: ContentState = mock()
+ val state = spy(
+ BrowserState(
+ tabs = listOf(TabSessionState("123", normalTabContent)),
+ customTabs = listOf(CustomTabSessionState("ct", customTabContent, config = mock())),
+ selectedTabId = "123",
+ ),
+ )
+ val store = BrowserStore(state)
+ val controller = ToolbarBehaviorController(mock(), store, "ct")
+
+ assertNull(controller.updatesScope)
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+
+ assertNotNull(controller.updatesScope)
+ verify(customTabContent, times(3)).loading
+ verify(normalTabContent, never()).loading
+ }
+
+ @Test
+ fun `Controller should check the status of the currently selected tab if not initialized with a custom tab id`() {
+ val customTabContent: ContentState = mock()
+ val normalTabContent: ContentState = mock()
+ val state = spy(
+ BrowserState(
+ tabs = listOf(TabSessionState("123", normalTabContent)),
+ customTabs = listOf(CustomTabSessionState("ct", customTabContent, config = mock())),
+ selectedTabId = "123",
+ ),
+ )
+ val store = BrowserStore(state)
+ val controller = ToolbarBehaviorController(mock(), store)
+
+ assertNull(controller.updatesScope)
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+
+ assertNotNull(controller.updatesScope)
+ verify(customTabContent, never()).loading
+ verify(normalTabContent, times(3)).loading
+ }
+
+ @Test
+ fun `Controller should disableScrolling if the current tab is loading`() {
+ val normalTabContent = ContentState("url", loading = true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(TabSessionState("123", normalTabContent)),
+ selectedTabId = "123",
+ ),
+ )
+ val controller = spy(ToolbarBehaviorController(mock(), store))
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(controller).disableScrolling()
+ }
+
+ @Test
+ fun `Controller should enableScrolling if the current tab is not loading`() {
+ val normalTabContent = ContentState("url", loading = false)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(TabSessionState("123", normalTabContent)),
+ selectedTabId = "123",
+ ),
+ )
+ val controller = spy(ToolbarBehaviorController(mock(), store))
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(controller).enableScrolling()
+ }
+
+ @Test
+ fun `Controller should listening for tab updates if stop is called`() {
+ val controller = spy(ToolbarBehaviorController(mock(), BrowserStore(BrowserState())))
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(controller.updatesScope!!.isActive)
+
+ controller.stop()
+ assertFalse(controller.updatesScope!!.isActive)
+ }
+
+ @Test
+ fun `Controller should disable toolbar scrolling when disableScrolling is called`() {
+ val toolbar: Toolbar = mock()
+ val controller = spy(ToolbarBehaviorController(toolbar, mock()))
+
+ controller.disableScrolling()
+
+ verify(toolbar).disableScrolling()
+ }
+
+ @Test
+ fun `Controller should enable toolbar scrolling when enableScrolling is called`() {
+ val toolbar: Toolbar = mock()
+ val controller = spy(ToolbarBehaviorController(toolbar, mock()))
+
+ controller.enableScrolling()
+
+ verify(toolbar).enableScrolling()
+ }
+
+ @Test
+ fun `Controller should expand the toolbar and set showToolbarAsExpanded to false when showToolbarAsExpanded is true`() {
+ val normalTabContent = ContentState("url", showToolbarAsExpanded = true)
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(TabSessionState("123", normalTabContent)),
+ selectedTabId = "123",
+ ),
+ ),
+ )
+ val controller = spy(ToolbarBehaviorController(mock(), store))
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(controller).expandToolbar()
+ verify(store).dispatch(ContentAction.UpdateExpandedToolbarStateAction("123", false))
+ }
+
+ @Test
+ fun `Controller should not expand the toolbar and not update the current state if showToolbarAsExpanded is false`() {
+ val normalTabContent = ContentState("url", showToolbarAsExpanded = false)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(TabSessionState("123", normalTabContent)),
+ selectedTabId = "123",
+ ),
+ )
+ val controller = spy(ToolbarBehaviorController(mock(), store))
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(controller, never()).expandToolbar()
+ }
+
+ @Test
+ fun `GIVEN the current tab is loading an url WHEN the page is scrolled THEN expand toolbar`() {
+ val tabContent = ContentState("loading", loading = true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(TabSessionState("tab_1", tabContent)),
+ selectedTabId = "tab_1",
+ ),
+ )
+ val controller = spy(ToolbarBehaviorController(mock(), store))
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(controller).expandToolbar()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarFeatureTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarFeatureTest.kt
new file mode 100644
index 0000000000..4c460ff6aa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarFeatureTest.kt
@@ -0,0 +1,100 @@
+/* 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.toolbar
+
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class ToolbarFeatureTest {
+
+ @Test
+ fun `when app is backgrounded, toolbar onStop method is called`() {
+ val toolbar: Toolbar = mock()
+ val toolbarFeature = ToolbarFeature(toolbar, store = mock(), loadUrlUseCase = mock())
+
+ toolbarFeature.stop()
+
+ verify(toolbar).onStop()
+ }
+
+ @Test
+ fun `GIVEN ToolbarFeature, WHEN start() is called THEN it should call controller#start()`() {
+ val mockedController: ToolbarBehaviorController = mock()
+ val feature = ToolbarFeature(mock(), mock(), mock()).apply {
+ controller = mockedController
+ // mock other dependencies to limit real code running and error-ing.
+ presenter = mock()
+ interactor = mock()
+ }
+
+ feature.start()
+
+ verify(mockedController).start()
+ }
+
+ @Test
+ fun `GIVEN ToolbarFeature, WHEN start() is called THEN it should call presenter#start()`() {
+ val mockedPresenter: ToolbarPresenter = mock()
+ val feature = ToolbarFeature(mock(), mock(), mock()).apply {
+ controller = mock()
+ presenter = mockedPresenter
+ interactor = mock()
+ }
+
+ feature.start()
+
+ verify(mockedPresenter).start()
+ }
+
+ @Test
+ fun `GIVEN ToolbarFeature, WHEN start() is called THEN it should call interactor#start()`() {
+ val mockedInteractor: ToolbarInteractor = mock()
+ val feature = ToolbarFeature(mock(), mock(), mock()).apply {
+ controller = mock()
+ presenter = mock()
+ interactor = mockedInteractor
+ }
+
+ feature.start()
+
+ verify(mockedInteractor).start()
+ }
+
+ @Test
+ fun `GIVEN ToolbarFeature, WHEN stop() is called THEN it should call controller#stop()`() {
+ val mockedController: ToolbarBehaviorController = mock()
+ val feature = ToolbarFeature(mock(), mock(), mock()).apply {
+ controller = mockedController
+ }
+
+ feature.stop()
+
+ verify(mockedController).stop()
+ }
+
+ @Test
+ fun `GIVEN ToolbarFeature, WHEN stop() is called THEN it should call presenter#stop()`() {
+ val mockedPresenter: ToolbarPresenter = mock()
+ val feature = ToolbarFeature(mock(), mock(), mock()).apply {
+ presenter = mockedPresenter
+ }
+
+ feature.stop()
+
+ verify(mockedPresenter).stop()
+ }
+
+ @Test
+ fun `GIVEN ToolbarFeature, WHEN onBackPressed() is called THEN it should call toolbar#onBackPressed()`() {
+ val toolbar: Toolbar = mock()
+ val feature = ToolbarFeature(toolbar, store = mock(), loadUrlUseCase = mock())
+
+ feature.onBackPressed()
+
+ verify(toolbar).onBackPressed()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt
new file mode 100644
index 0000000000..3dcee39e7b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt
@@ -0,0 +1,164 @@
+/* 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.toolbar
+
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.toolbar.AutocompleteDelegate
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.session.SessionUseCases
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import org.mockito.Mockito.spy
+
+class ToolbarInteractorTest {
+
+ class TestToolbar : Toolbar {
+ override var highlight: Toolbar.Highlight = Toolbar.Highlight.NONE
+ override var url: CharSequence = ""
+ override var siteSecure: Toolbar.SiteSecurity = Toolbar.SiteSecurity.INSECURE
+ override var private: Boolean = false
+ override var title: String = ""
+
+ override var siteTrackingProtection: Toolbar.SiteTrackingProtection =
+ Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+
+ override fun setSearchTerms(searchTerms: String) {
+ fail()
+ }
+
+ override fun displayProgress(progress: Int) {
+ fail()
+ }
+
+ override fun onBackPressed(): Boolean {
+ fail()
+ return false
+ }
+
+ override fun onStop() {
+ fail()
+ }
+
+ override fun setOnUrlCommitListener(listener: (String) -> Boolean) {
+ listener("https://mozilla.org")
+ }
+
+ override fun setAutocompleteListener(filter: suspend (String, AutocompleteDelegate) -> Unit) {
+ fail()
+ }
+
+ override fun addBrowserAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun removeBrowserAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun removePageAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun addPageAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun addNavigationAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun removeNavigationAction(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun setOnEditListener(listener: Toolbar.OnEditListener) {
+ fail()
+ }
+
+ override fun displayMode() {
+ fail()
+ }
+
+ override fun editMode(cursorPlacement: Toolbar.CursorPlacement) {
+ fail()
+ }
+
+ override fun addEditActionStart(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun addEditActionEnd(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun removeEditActionEnd(action: Toolbar.Action) {
+ fail()
+ }
+
+ override fun hideMenuButton() {
+ fail()
+ }
+
+ override fun showMenuButton() {
+ fail()
+ }
+
+ override fun setDisplayHorizontalPadding(horizontalPadding: Int) {
+ fail()
+ }
+
+ override fun hidePageActionSeparator() {
+ fail()
+ }
+
+ override fun showPageActionSeparator() {
+ fail()
+ }
+
+ override fun invalidateActions() {
+ fail()
+ }
+
+ override fun dismissMenu() {
+ fail()
+ }
+
+ override fun enableScrolling() {
+ fail()
+ }
+
+ override fun disableScrolling() {
+ fail()
+ }
+
+ override fun collapse() {
+ fail()
+ }
+
+ override fun expand() {
+ fail()
+ }
+ }
+
+ @Test
+ fun `provide custom use case for loading url`() {
+ var useCaseInvokedWithUrl = ""
+ val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
+ override fun invoke(
+ url: String,
+ flags: EngineSession.LoadUrlFlags,
+ additionalHeaders: Map<String, String>?,
+ ) {
+ useCaseInvokedWithUrl = url
+ }
+ }
+
+ val toolbarInteractor = spy(ToolbarInteractor(TestToolbar(), loadUrlUseCase))
+ toolbarInteractor.start()
+
+ assertEquals("https://mozilla.org", useCaseInvokedWithUrl)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt
new file mode 100644
index 0000000000..00987ee1b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt
@@ -0,0 +1,548 @@
+/* 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.toolbar
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction
+import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.NotificationChangedAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.TrackingProtectionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.TrackingProtectionState
+import mozilla.components.browser.state.state.content.PermissionHighlightsState
+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.toolbar.Toolbar
+import mozilla.components.feature.toolbar.internal.URLRenderer
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+class ToolbarPresenterTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun `start with no custom tab id registers on store and renders selected tab`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "tab1")),
+ customTabs = listOf(createCustomTab("https://www.example.org", id = "ct")),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(store).observeManually(any())
+
+ verify(toolbarPresenter).render(any())
+
+ verify(toolbarPresenter.renderer).post("https://www.mozilla.org")
+ verify(toolbar).setSearchTerms("")
+ verify(toolbar).displayProgress(0)
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE
+ }
+
+ @Test
+ fun `start with custom tab id registers on store and renders custom tab`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "tab1")),
+ customTabs = listOf(createCustomTab("https://www.example.org", id = "ct")),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store, customTabId = "ct"))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(store).observeManually(any())
+ verify(toolbarPresenter).render(any())
+
+ verify(toolbarPresenter.renderer).post("https://www.example.org")
+ verify(toolbar).setSearchTerms("")
+ verify(toolbar).displayProgress(0)
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE
+ }
+
+ @Test
+ fun `SecurityInfoState change updates toolbar`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "tab1")),
+ customTabs = listOf(createCustomTab("https://www.example.org", id = "ct")),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar, never()).siteSecure = Toolbar.SiteSecurity.SECURE
+
+ store.dispatch(
+ ContentAction.UpdateSecurityInfoAction(
+ "tab1",
+ SecurityInfoState(
+ secure = true,
+ host = "mozilla.org",
+ issuer = "Mozilla",
+ ),
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.SECURE
+ }
+
+ @Test
+ fun `Toolbar gets cleared when all tabs are removed`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ id = "tab1",
+ content = ContentState(
+ url = "https://www.mozilla.org",
+ securityInfo = SecurityInfoState(true, "mozilla.org", "Mozilla"),
+ searchTerms = "Hello World",
+ progress = 60,
+ ),
+ ),
+ ),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbarPresenter.renderer).start()
+ verify(toolbarPresenter.renderer).post("https://www.mozilla.org")
+ verify(toolbar).setSearchTerms("Hello World")
+ verify(toolbar).displayProgress(60)
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.SECURE
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+ verify(toolbar).highlight = Toolbar.Highlight.NONE
+ verifyNoMoreInteractions(toolbarPresenter.renderer)
+ verifyNoMoreInteractions(toolbar)
+
+ store.dispatch(TabListAction.RemoveTabAction("tab1")).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbarPresenter.renderer).post("")
+ verify(toolbar).setSearchTerms("")
+ verify(toolbar).displayProgress(0)
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE
+ }
+
+ @Test
+ fun `Search terms changes updates toolbar`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "tab1")),
+ customTabs = listOf(createCustomTab("https://www.example.org", id = "ct")),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar, never()).setSearchTerms("Hello World")
+
+ store.dispatch(
+ ContentAction.UpdateSearchTermsAction(
+ sessionId = "tab1",
+ searchTerms = "Hello World",
+ ),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).setSearchTerms("Hello World")
+ }
+
+ @Test
+ fun `Progress changes updates toolbar`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "tab1")),
+ customTabs = listOf(createCustomTab("https://www.example.org", id = "ct")),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar, never()).displayProgress(75)
+
+ store.dispatch(
+ ContentAction.UpdateProgressAction("tab1", 75),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).displayProgress(75)
+
+ verify(toolbar, never()).displayProgress(90)
+
+ store.dispatch(
+ ContentAction.UpdateProgressAction("tab1", 90),
+ ).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).displayProgress(90)
+ }
+
+ @Test
+ fun `Toolbar does not get cleared if a background tab gets removed`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ id = "tab1",
+ content = ContentState(
+ url = "https://www.mozilla.org",
+ securityInfo = SecurityInfoState(true, "mozilla.org", "Mozilla"),
+ searchTerms = "Hello World",
+ progress = 60,
+ ),
+ ),
+ createTab(id = "tab2", url = "https://www.example.org"),
+ ),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ store.dispatch(TabListAction.RemoveTabAction("tab2")).joinBlocking()
+
+ verify(toolbarPresenter.renderer).start()
+ verify(toolbarPresenter.renderer).post("https://www.mozilla.org")
+ verify(toolbar).setSearchTerms("Hello World")
+ verify(toolbar).displayProgress(60)
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.SECURE
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+ verify(toolbar).highlight = Toolbar.Highlight.NONE
+ verifyNoMoreInteractions(toolbarPresenter.renderer)
+ verifyNoMoreInteractions(toolbar)
+ }
+
+ @Test
+ fun `Toolbar is updated when selected tab changes`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ id = "tab1",
+ content = ContentState(
+ url = "https://www.mozilla.org",
+ securityInfo = SecurityInfoState(true, "mozilla.org", "Mozilla"),
+ searchTerms = "Hello World",
+ progress = 60,
+ ),
+ ),
+ TabSessionState(
+ id = "tab2",
+ content = ContentState(
+ url = "https://www.example.org",
+ securityInfo = SecurityInfoState(false, "example.org", "Example"),
+ searchTerms = "Example",
+ permissionHighlights = PermissionHighlightsState(true),
+ progress = 90,
+ ),
+ trackingProtection = TrackingProtectionState(enabled = true),
+ ),
+ ),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbarPresenter.renderer).start()
+ verify(toolbarPresenter.renderer).post("https://www.mozilla.org")
+ verify(toolbar).setSearchTerms("Hello World")
+ verify(toolbar).displayProgress(60)
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.SECURE
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+ verify(toolbar).highlight = Toolbar.Highlight.NONE
+ verifyNoMoreInteractions(toolbarPresenter.renderer)
+ verifyNoMoreInteractions(toolbar)
+
+ store.dispatch(TabListAction.SelectTabAction("tab2")).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbarPresenter.renderer).post("https://www.example.org")
+ verify(toolbar).setSearchTerms("Example")
+ verify(toolbar).displayProgress(90)
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED
+ verify(toolbar).highlight = Toolbar.Highlight.PERMISSIONS_CHANGED
+ verifyNoMoreInteractions(toolbarPresenter.renderer)
+ verifyNoMoreInteractions(toolbar)
+ }
+
+ @Test
+ fun `displaying different tracking protection states`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ id = "tab",
+ content = ContentState(
+ url = "https://www.mozilla.org",
+ securityInfo = SecurityInfoState(true, "mozilla.org", "Mozilla"),
+ searchTerms = "Hello World",
+ progress = 60,
+ ),
+ ),
+ ),
+ selectedTabId = "tab",
+ ),
+ ),
+ )
+
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+
+ store.dispatch(TrackingProtectionAction.ToggleAction("tab", true))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED
+
+ store.dispatch(TrackingProtectionAction.TrackerBlockedAction("tab", mock()))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.ON_TRACKERS_BLOCKED
+
+ store.dispatch(TrackingProtectionAction.ToggleExclusionListAction("tab", true))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_FOR_A_SITE
+ }
+
+ @Test
+ fun `displaying different dot notification states`() {
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ TabSessionState(
+ id = "tab",
+ content = ContentState(
+ url = "https://www.mozilla.org",
+ securityInfo = SecurityInfoState(true, "mozilla.org", "Mozilla"),
+ searchTerms = "Hello World",
+ progress = 60,
+ ),
+ ),
+ ),
+ selectedTabId = "tab",
+ ),
+ ),
+ )
+
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).highlight = Toolbar.Highlight.NONE
+
+ store.dispatch(NotificationChangedAction("tab", true)).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).highlight = Toolbar.Highlight.PERMISSIONS_CHANGED
+
+ store.dispatch(TrackingProtectionAction.ToggleExclusionListAction("tab", true))
+ .joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar, times(2)).highlight = Toolbar.Highlight.PERMISSIONS_CHANGED
+
+ store.dispatch(UpdatePermissionHighlightsStateAction.Reset("tab")).joinBlocking()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).highlight = Toolbar.Highlight.NONE
+ }
+
+ @Test
+ fun `Stopping presenter stops renderer`() {
+ val store = BrowserStore()
+ val presenter = ToolbarPresenter(mock(), store)
+
+ val renderer: URLRenderer = mock()
+ presenter.renderer = renderer
+
+ presenter.start()
+
+ verify(renderer, never()).stop()
+
+ presenter.stop()
+
+ verify(renderer).stop()
+ }
+
+ @Test
+ fun `Toolbar displays empty state without tabs`() {
+ val store = BrowserStore()
+ val toolbar: Toolbar = mock()
+ val presenter = ToolbarPresenter(toolbar, store)
+ presenter.renderer = mock()
+
+ presenter.start()
+
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(presenter.renderer).post("")
+ verify(toolbar).setSearchTerms("")
+ verify(toolbar).displayProgress(0)
+ verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE
+ verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY
+ verify(toolbar).highlight = Toolbar.Highlight.NONE
+ }
+
+ @Test
+ fun `GIVEN search terms should not be shown in display mode WHEN rendering a state with search terms set THEN toolbar url is the tab url`() {
+ val url = "https://www.mozilla.org"
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab(url, id = "tab1", searchTerms = "search terms")),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store, shouldDisplaySearchTerms = false))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbarPresenter.renderer).post(url)
+ }
+
+ @Test
+ fun `GIVEN search terms should be shown in display mode WHEN rendering a state with search terms set THEN toolbar url is set to the search terms`() {
+ val searchTerm = "mozilla firefox"
+ val toolbar: Toolbar = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab("https://www.mozilla.org", id = "tab1", searchTerms = searchTerm)),
+ selectedTabId = "tab1",
+ ),
+ ),
+ )
+ val toolbarPresenter = spy(ToolbarPresenter(toolbar, store, shouldDisplaySearchTerms = true))
+ toolbarPresenter.renderer = mock()
+
+ toolbarPresenter.start()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(toolbar).url = searchTerm
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeatureTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeatureTest.kt
new file mode 100644
index 0000000000..a5e74a0284
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeatureTest.kt
@@ -0,0 +1,432 @@
+/* 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.toolbar
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
+import mozilla.components.concept.engine.webextension.WebExtensionPageAction
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class WebExtensionToolbarFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun `render web extension actions from browser state`() {
+ val defaultPageAction =
+ WebExtensionPageAction("default_page_action_title", true, mock(), "", 0, 0) {}
+ val overriddenPageAction =
+ WebExtensionPageAction("overridden_page_action_title", true, mock(), "", 0, 0) {}
+ val defaultBrowserAction =
+ WebExtensionBrowserAction("default_browser_action_title", true, mock(), "", 0, 0) {}
+ val overriddenBrowserAction =
+ WebExtensionBrowserAction("overridden_browser_action_title", true, mock(), "", 0, 0) {}
+ val toolbar: Toolbar = mock()
+ val extensions: Map<String, WebExtensionState> = mapOf(
+ "id" to WebExtensionState("id", "url", "name", true, browserAction = defaultBrowserAction, pageAction = defaultPageAction),
+ )
+ val overriddenExtensions: Map<String, WebExtensionState> = mapOf(
+ "id" to WebExtensionState("id", "url", "name", true, browserAction = overriddenBrowserAction, pageAction = overriddenPageAction),
+ )
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.example.org",
+ id = "tab1",
+ extensions = overriddenExtensions,
+ ),
+ ),
+ selectedTabId = "tab1",
+ extensions = extensions,
+ ),
+ ),
+ )
+ val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar, store)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(store).observeManually(any())
+ verify(webExtToolbarFeature).renderWebExtensionActions(any(), any())
+
+ val browserActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ verify(toolbar).addBrowserAction(browserActionCaptor.capture())
+ assertEquals("overridden_browser_action_title", browserActionCaptor.value.action.title)
+
+ val pageActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ verify(toolbar).addPageAction(pageActionCaptor.capture())
+ assertEquals("overridden_page_action_title", pageActionCaptor.value.action.title)
+ }
+
+ @Test
+ fun `does not render actions from disabled extensions`() {
+ val enablePageAction =
+ WebExtensionPageAction("enable_page_action", true, mock(), "", 0, 0) {}
+ val disablePageAction =
+ WebExtensionPageAction("disable_page_action", true, mock(), "", 0, 0) {}
+ val enabledAction = WebExtensionBrowserAction("enable_browser_action", true, mock(), "", 0, 0) {}
+ val disabledAction = WebExtensionBrowserAction("disable_browser_action", true, mock(), "", 0, 0) {}
+ val toolbar: Toolbar = mock()
+ val extensions = mapOf(
+ "enabled" to WebExtensionState(
+ "enabled",
+ "url",
+ "name",
+ true,
+ browserAction = enabledAction,
+ pageAction = enablePageAction,
+ ),
+ "disabled" to WebExtensionState(
+ "disabled",
+ "url",
+ "name",
+ false,
+ browserAction = disabledAction,
+ pageAction = disablePageAction,
+ ),
+ )
+
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ ),
+ )
+ val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar, store)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(store).observeManually(any())
+ verify(webExtToolbarFeature, times(1)).renderWebExtensionActions(any(), any())
+ val browserActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ val pageActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ verify(toolbar, times(1)).addBrowserAction(browserActionCaptor.capture())
+
+ verify(toolbar, times(1)).addPageAction(pageActionCaptor.capture())
+ assertEquals("enable_browser_action", browserActionCaptor.value.action.title)
+ assertEquals("enable_page_action", pageActionCaptor.value.action.title)
+ }
+
+ @Test
+ fun `actions can be overridden per tab`() {
+ val webExtToolbarFeature = getWebExtensionToolbarFeature()
+
+ val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() }
+
+ val pageAction = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val pageActionOverride = Action(
+ title = "updatedTitle",
+ loadIcon = null,
+ enabled = true,
+ badgeText = "updatedText",
+ badgeTextColor = Color.RED,
+ badgeBackgroundColor = Color.GREEN,
+ ) {}
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val browserActionOverride = Action(
+ title = "updatedTitle",
+ loadIcon = null,
+ enabled = false,
+ badgeText = "updatedText",
+ badgeTextColor = Color.RED,
+ badgeBackgroundColor = Color.GREEN,
+ ) {}
+
+ // Verify rendering global default browser action
+ val browserExtensions = HashMap<String, WebExtensionState>()
+ browserExtensions["1"] = WebExtensionState(id = "1", browserAction = browserAction, pageAction = pageAction)
+
+ val browserState = BrowserState(extensions = browserExtensions)
+ webExtToolbarFeature.renderWebExtensionActions(browserState, mock())
+
+ // verifying global browser action
+ assertEquals(1, webExtToolbarFeature.webExtensionBrowserActions.size)
+ var ext1 = webExtToolbarFeature.webExtensionBrowserActions["1"]
+ assertTrue(ext1?.action?.enabled!!)
+ assertEquals("badgeText", ext1.action.badgeText!!)
+ assertEquals("title", ext1.action.title!!)
+ assertEquals(loadIcon, ext1.action.loadIcon!!)
+ assertEquals(Color.WHITE, ext1.action.badgeTextColor!!)
+ assertEquals(Color.BLUE, ext1.action.badgeBackgroundColor!!)
+
+ // verifying global page action
+ assertEquals(1, webExtToolbarFeature.webExtensionPageActions.size)
+ ext1 = webExtToolbarFeature.webExtensionPageActions["1"]!!
+ assertTrue(ext1.action.enabled!!)
+ assertEquals("badgeText", ext1.action.badgeText!!)
+ assertEquals("title", ext1.action.title!!)
+ assertEquals(loadIcon, ext1.action.loadIcon!!)
+ assertEquals(Color.WHITE, ext1.action.badgeTextColor!!)
+ assertEquals(Color.BLUE, ext1.action.badgeBackgroundColor!!)
+
+ // Verify rendering session-specific actions override
+ val tabExtensions = HashMap<String, WebExtensionState>()
+ tabExtensions["1"] = WebExtensionState(id = "1", browserAction = browserActionOverride, pageAction = pageActionOverride)
+
+ val tabSessionState = TabSessionState(
+ content = mock(),
+ extensionState = tabExtensions,
+ )
+ webExtToolbarFeature.renderWebExtensionActions(browserState, tabSessionState)
+
+ // verifying session-specific browser action
+ assertEquals(1, webExtToolbarFeature.webExtensionBrowserActions.size)
+ var updatedExt1 = webExtToolbarFeature.webExtensionBrowserActions["1"]
+ assertFalse(updatedExt1?.action?.enabled!!)
+ assertEquals("updatedText", updatedExt1.action.badgeText!!)
+ assertEquals("updatedTitle", updatedExt1.action.title!!)
+ assertEquals(loadIcon, updatedExt1.action.loadIcon!!)
+ assertEquals(Color.RED, updatedExt1.action.badgeTextColor!!)
+ assertEquals(Color.GREEN, updatedExt1.action.badgeBackgroundColor!!)
+
+ // verifying session-specific page action
+ assertEquals(1, webExtToolbarFeature.webExtensionPageActions.size)
+ updatedExt1 = webExtToolbarFeature.webExtensionPageActions["1"]!!
+ assertTrue(updatedExt1.action.enabled!!)
+ assertEquals("updatedText", updatedExt1.action.badgeText!!)
+ assertEquals("updatedTitle", updatedExt1.action.title!!)
+ assertEquals(loadIcon, updatedExt1.action.loadIcon!!)
+ assertEquals(Color.RED, updatedExt1.action.badgeTextColor!!)
+ assertEquals(Color.GREEN, updatedExt1.action.badgeBackgroundColor!!)
+ }
+
+ @Test
+ fun `stale actions (from uninstalled or disabled extensions) are removed when feature is restarted`() {
+ val browserExtensions = HashMap<String, WebExtensionState>()
+ val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() }
+ val browserAction = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val pageAction = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ browserExtensions["1"] =
+ WebExtensionState(id = "1", browserAction = browserAction, pageAction = pageAction)
+
+ browserExtensions["2"] =
+ WebExtensionState(id = "2", browserAction = browserAction, pageAction = pageAction)
+
+ val browserState = BrowserState(extensions = browserExtensions)
+ val store = BrowserStore(browserState)
+ val toolbar: Toolbar = mock()
+ val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar, store)
+
+ webExtToolbarFeature.renderWebExtensionActions(browserState, mock())
+ assertEquals(2, webExtToolbarFeature.webExtensionBrowserActions.size)
+ assertEquals(2, webExtToolbarFeature.webExtensionPageActions.size)
+
+ store.dispatch(WebExtensionAction.UninstallWebExtensionAction("1")).joinBlocking()
+ store.dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction("2", false)).joinBlocking()
+
+ webExtToolbarFeature.start()
+ assertEquals(0, webExtToolbarFeature.webExtensionBrowserActions.size)
+ assertEquals(0, webExtToolbarFeature.webExtensionPageActions.size)
+
+ val browserActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ val pageActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ verify(toolbar, times(2)).removeBrowserAction(browserActionCaptor.capture())
+ verify(toolbar, times(2)).removePageAction(pageActionCaptor.capture())
+ assertEquals(browserAction, browserActionCaptor.value.action)
+ assertEquals(pageAction, pageActionCaptor.value.action)
+ }
+
+ @Test
+ fun `actions can are sorted per extension name`() {
+ val toolbar: Toolbar = mock()
+ val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar)
+
+ val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() }
+
+ val actionExt1 = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val actionExt2 = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val browserExtensions = HashMap<String, WebExtensionState>()
+ browserExtensions["1"] = WebExtensionState(id = "1", name = "extensionA", browserAction = actionExt1)
+ browserExtensions["2"] = WebExtensionState(id = "2", name = "extensionB", pageAction = actionExt2)
+
+ val browserState = BrowserState(extensions = browserExtensions)
+ webExtToolbarFeature.renderWebExtensionActions(browserState, mock())
+
+ val inOrder = inOrder(toolbar)
+ val browserActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ inOrder.verify(toolbar).addBrowserAction(browserActionCaptor.capture())
+ assertEquals(actionExt1, browserActionCaptor.value.action)
+
+ val pageActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ inOrder.verify(toolbar).addPageAction(pageActionCaptor.capture())
+ assertEquals(actionExt2, pageActionCaptor.value.action)
+ }
+
+ @Test
+ fun `renderWebExtensionActions depends on allowedInPrivateBrowsing and whether the current tab is private`() {
+ val toolbar: Toolbar = mock()
+ val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar)
+ val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() }
+
+ val actionExt1 = Action(
+ title = "title",
+ loadIcon = loadIcon,
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val tabSessionState = TabSessionState(
+ content = mock(),
+ extensionState = emptyMap(),
+ )
+
+ whenever(tabSessionState.content.private).thenReturn(true)
+ val browserActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+
+ val browserExtensions = HashMap<String, WebExtensionState>()
+ browserExtensions["1"] =
+ WebExtensionState(id = "1", name = "extensionA", browserAction = actionExt1)
+ val browserState = BrowserState(extensions = browserExtensions)
+ webExtToolbarFeature.renderWebExtensionActions(browserState, tabSessionState)
+ verify(toolbar, never()).addBrowserAction(browserActionCaptor.capture())
+
+ val browserExtensionsAllowedInPrivateBrowsing = HashMap<String, WebExtensionState>()
+ browserExtensionsAllowedInPrivateBrowsing["1"] =
+ WebExtensionState(id = "1", allowedInPrivateBrowsing = true, name = "extensionA", browserAction = actionExt1)
+ val browserStateAllowedInPrivateBrowsing = BrowserState(extensions = browserExtensionsAllowedInPrivateBrowsing)
+ webExtToolbarFeature.renderWebExtensionActions(browserStateAllowedInPrivateBrowsing, tabSessionState)
+ verify(toolbar, times(1)).addBrowserAction(browserActionCaptor.capture())
+ assertEquals(actionExt1, browserActionCaptor.value.action)
+ }
+
+ @Test
+ fun `disabled page actions are not rendered`() {
+ val enablePageAction =
+ WebExtensionPageAction("enable_page_action", true, mock(), "", 0, 0) {}
+ val disablePageAction =
+ WebExtensionPageAction("disable_page_action", false, mock(), "", 0, 0) {}
+ val toolbar: Toolbar = mock()
+ val extensions = mapOf(
+ "ext1" to WebExtensionState(
+ "ext1",
+ "url",
+ "name",
+ true,
+ pageAction = enablePageAction,
+ ),
+ "ext2" to WebExtensionState(
+ "ext2",
+ "url",
+ "name",
+ true,
+ pageAction = disablePageAction,
+ ),
+ )
+
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ extensions = extensions,
+ ),
+ ),
+ )
+ val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar, store)
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(store).observeManually(any())
+ verify(webExtToolbarFeature).renderWebExtensionActions(any(), any())
+
+ val pageActionCaptor = argumentCaptor<WebExtensionToolbarAction>()
+ verify(toolbar, times(1)).addPageAction(pageActionCaptor.capture())
+ assertEquals("enable_page_action", pageActionCaptor.value.action.title)
+ }
+
+ private fun getWebExtensionToolbarFeature(
+ toolbar: Toolbar = mock(),
+ store: BrowserStore = BrowserStore(),
+ ): WebExtensionToolbarFeature {
+ val webExtToolbarFeature = spy(WebExtensionToolbarFeature(toolbar, store))
+ val handler: Handler = mock()
+ val looper: Looper = mock()
+ val iconThread: HandlerThread = mock()
+
+ doReturn(looper).`when`(iconThread).looper
+ doReturn(iconThread).`when`(webExtToolbarFeature).iconThread
+ doReturn(handler).`when`(webExtToolbarFeature).iconHandler
+ webExtToolbarFeature.start()
+ return webExtToolbarFeature
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarTest.kt
new file mode 100644
index 0000000000..c467e0b9bb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarTest.kt
@@ -0,0 +1,179 @@
+/* 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.toolbar
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.delay
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import mozilla.components.ui.icons.R as iconsR
+
+@RunWith(AndroidJUnit4::class)
+class WebExtensionToolbarTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val testDispatcher = coroutinesTestRule.testDispatcher
+
+ @Test
+ fun bind() {
+ val icon: Bitmap = mock()
+ val imageView: ImageView = mock()
+ val textView: TextView = mock()
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(textView)
+ whenever(view.context).thenReturn(mock())
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { icon },
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val action = WebExtensionToolbarAction(browserAction, iconJobDispatcher = testDispatcher) {}
+ action.bind(view)
+ action.iconJob?.joinBlocking()
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ val iconCaptor = argumentCaptor<BitmapDrawable>()
+ verify(imageView).setImageDrawable(iconCaptor.capture())
+ assertEquals(icon, iconCaptor.value.bitmap)
+
+ verify(imageView).contentDescription = "title"
+ verify(textView).setText("badgeText")
+ verify(textView).setTextColor(Color.WHITE)
+ verify(textView).setBackgroundColor(Color.BLUE)
+ }
+
+ @Test
+ fun fallbackToDefaultIcon() {
+ val imageView: ImageView = mock()
+ val textView: TextView = mock()
+ val view: View = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(textView)
+ whenever(view.context).thenReturn(mock())
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { throw IllegalArgumentException() },
+ enabled = true,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val action = WebExtensionToolbarAction(browserAction, iconJobDispatcher = testDispatcher) {}
+ action.bind(view)
+ action.iconJob?.joinBlocking()
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ verify(imageView).setImageResource(
+ iconsR.drawable.mozac_ic_web_extension_default_icon,
+ )
+ }
+
+ @Test
+ fun createView() {
+ var listenerWasClicked = false
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = { mock() },
+ enabled = false,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val action = WebExtensionToolbarAction(
+ browserAction,
+ padding = Padding(1, 2, 3, 4),
+ iconJobDispatcher = testDispatcher,
+ ) {
+ listenerWasClicked = true
+ }
+
+ val rootView = action.createView(LinearLayout(testContext))
+ rootView.performClick()
+
+ assertFalse(rootView.isEnabled)
+ assertTrue(listenerWasClicked)
+ assertEquals(rootView.paddingLeft, 1)
+ assertEquals(rootView.paddingTop, 2)
+ assertEquals(rootView.paddingRight, 3)
+ assertEquals(rootView.paddingBottom, 4)
+ }
+
+ @Test
+ fun cancelLoadIconWhenViewIsDetached() {
+ val view: View = mock()
+ val imageView: ImageView = mock()
+ val textView: TextView = mock()
+
+ whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView)
+ whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(textView)
+ whenever(view.context).thenReturn(mock())
+
+ val browserAction = Action(
+ title = "title",
+ loadIcon = @Suppress("UNREACHABLE_CODE") {
+ while (true) { delay(10) }
+ mock()
+ },
+ enabled = false,
+ badgeText = "badgeText",
+ badgeTextColor = Color.WHITE,
+ badgeBackgroundColor = Color.BLUE,
+ ) {}
+
+ val action = WebExtensionToolbarAction(
+ browserAction,
+ padding = Padding(1, 2, 3, 4),
+ iconJobDispatcher = testDispatcher,
+ ) {}
+
+ val attachListenerCaptor = argumentCaptor<View.OnAttachStateChangeListener>()
+ val parent = spy(LinearLayout(testContext))
+ action.createView(parent)
+ verify(parent).addOnAttachStateChangeListener(attachListenerCaptor.capture())
+
+ action.bind(view)
+ assertNotNull(action.iconJob)
+ assertFalse(action.iconJob?.isCancelled!!)
+
+ attachListenerCaptor.value.onViewDetachedFromWindow(parent)
+ testDispatcher.scheduler.advanceUntilIdle()
+ assertTrue(action.iconJob?.isCancelled!!)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt
new file mode 100644
index 0000000000..0983d38ee7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt
@@ -0,0 +1,106 @@
+/* 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.toolbar.internal
+
+import android.graphics.Color
+import android.text.SpannableStringBuilder
+import android.text.style.ForegroundColorSpan
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.Dispatchers
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.toolbar.ToolbarFeature
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+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.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class URLRendererTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `Lifecycle methods start and stop job`() {
+ val renderer = URLRenderer(mock(), mock())
+
+ assertNull(renderer.job)
+
+ renderer.start()
+
+ assertNotNull(renderer.job)
+ assertTrue(renderer.job!!.isActive)
+
+ renderer.stop()
+
+ assertNotNull(renderer.job)
+ assertFalse(renderer.job!!.isActive)
+ }
+
+ @Test
+ fun `Render with configuration`() {
+ runTestOnMain {
+ val configuration = ToolbarFeature.UrlRenderConfiguration(
+ publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
+ registrableDomainColor = Color.RED,
+ urlColor = Color.GREEN,
+ )
+
+ val toolbar: Toolbar = mock()
+
+ val renderer = URLRenderer(toolbar, configuration)
+
+ renderer.updateUrl("https://www.mozilla.org/")
+
+ val captor = argumentCaptor<CharSequence>()
+ verify(toolbar).url = captor.capture()
+
+ assertNotNull(captor.value)
+ assertTrue(captor.value is SpannableStringBuilder)
+ val url = captor.value as SpannableStringBuilder
+
+ assertEquals("https://www.mozilla.org/", url.toString())
+
+ val spans = url.getSpans(0, url.length, ForegroundColorSpan::class.java)
+
+ assertEquals(2, spans.size)
+ assertEquals(Color.GREEN, spans[0].foregroundColor)
+ assertEquals(Color.RED, spans[1].foregroundColor)
+
+ val domain = url.subSequence(12, 23)
+ assertEquals("mozilla.org", domain.toString())
+
+ val domainSpans = url.getSpans(13, 23, ForegroundColorSpan::class.java)
+ assertEquals(2, domainSpans.size)
+ assertEquals(Color.GREEN, domainSpans[0].foregroundColor)
+ assertEquals(Color.RED, domainSpans[1].foregroundColor)
+
+ val prefix = url.subSequence(0, 12)
+ assertEquals("https://www.", prefix.toString())
+
+ val prefixSpans = url.getSpans(0, 12, ForegroundColorSpan::class.java)
+ assertEquals(1, prefixSpans.size)
+ assertEquals(Color.GREEN, prefixSpans[0].foregroundColor)
+
+ val suffix = url.subSequence(23, url.length)
+ assertEquals("/", suffix.toString())
+
+ val suffixSpans = url.getSpans(23, url.length, ForegroundColorSpan::class.java)
+ assertEquals(1, suffixSpans.size)
+ assertEquals(Color.GREEN, suffixSpans[0].foregroundColor)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/toolbar/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/toolbar/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/toolbar/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/toolbar/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/toolbar/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/top-sites/README.md b/mobile/android/android-components/components/feature/top-sites/README.md
new file mode 100644
index 0000000000..43da2f004b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Top Sites
+
+Feature implementation for saving and removing top sites.
+
+## Usage
+
+### 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-top-sites:{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/top-sites/build.gradle b/mobile/android/android-components/components/feature/top-sites/build.gradle
new file mode 100644
index 0000000000..2361376051
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/build.gradle
@@ -0,0 +1,82 @@
+/* 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/. */
+
+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"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/proguard/androidx-annotations.pro'
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.top.sites'
+}
+
+dependencies {
+ implementation project(':browser-storage-sync')
+ implementation project(':concept-toolbar')
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_paging
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.kotlin_coroutines
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_arch_core_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/top-sites/proguard-rules.pro b/mobile/android/android-components/components/feature/top-sites/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/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/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/1.json b/mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/1.json
new file mode 100644
index 0000000000..5cf0219da6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/1.json
@@ -0,0 +1,52 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "ce733d9c47cd10312a1c13de8efb7e8d",
+ "entities": [
+ {
+ "tableName": "top_sites",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `created_at` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ce733d9c47cd10312a1c13de8efb7e8d')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/2.json b/mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/2.json
new file mode 100644
index 0000000000..8bc2effe6e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/2.json
@@ -0,0 +1,58 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "4c6cae8272b2580de8cb444de31f27d5",
+ "entities": [
+ {
+ "tableName": "top_sites",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `is_default` INTEGER NOT NULL, `created_at` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isDefault",
+ "columnName": "is_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4c6cae8272b2580de8cb444de31f27d5')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/3.json b/mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/3.json
new file mode 100644
index 0000000000..e7b4ad010e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/schemas/mozilla.components.feature.top.sites.db.TopSiteDatabase/3.json
@@ -0,0 +1,58 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "4c6cae8272b2580de8cb444de31f27d5",
+ "entities": [
+ {
+ "tableName": "top_sites",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `is_default` INTEGER NOT NULL, `created_at` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isDefault",
+ "columnName": "is_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4c6cae8272b2580de8cb444de31f27d5')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/OnDevicePinnedSitesStorageTest.kt b/mobile/android/android-components/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/OnDevicePinnedSitesStorageTest.kt
new file mode 100644
index 0000000000..ac5563190d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/OnDevicePinnedSitesStorageTest.kt
@@ -0,0 +1,313 @@
+/* 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.top.sites
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.feature.top.sites.db.Migrations
+import mozilla.components.feature.top.sites.db.TopSiteDatabase
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+private const val MIGRATION_TEST_DB = "migration-test"
+
+@ExperimentalCoroutinesApi // for runTest
+@Suppress("LargeClass")
+class OnDevicePinnedSitesStorageTest {
+ private lateinit var context: Context
+ private lateinit var storage: PinnedSiteStorage
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ TopSiteDatabase::class.java,
+ )
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+
+ context = ApplicationProvider.getApplicationContext()
+ val database = Room.inMemoryDatabaseBuilder(context, TopSiteDatabase::class.java).build()
+
+ storage = PinnedSiteStorage(context)
+ storage.database = lazy { database }
+ }
+
+ @After
+ fun tearDown() {
+ executor.shutdown()
+ }
+
+ @Test
+ fun testAddingAllDefaultSites() = runTest {
+ val defaultTopSites = listOf(
+ Pair("Mozilla", "https://www.mozilla.org"),
+ Pair("Firefox", "https://www.firefox.com"),
+ Pair("Wikipedia", "https://www.wikipedia.com"),
+ Pair("Pocket", "https://www.getpocket.com"),
+ )
+
+ storage.addAllPinnedSites(defaultTopSites, isDefault = true)
+
+ val topSites = storage.getPinnedSites()
+
+ assertEquals(4, topSites.size)
+ assertEquals(4, storage.getPinnedSitesCount())
+
+ assertEquals("Mozilla", topSites[0].title)
+ assertEquals("https://www.mozilla.org", topSites[0].url)
+ assertTrue(topSites[0] is TopSite.Default)
+ assertEquals("Firefox", topSites[1].title)
+ assertEquals("https://www.firefox.com", topSites[1].url)
+ assertTrue(topSites[1] is TopSite.Default)
+ assertEquals("Wikipedia", topSites[2].title)
+ assertEquals("https://www.wikipedia.com", topSites[2].url)
+ assertTrue(topSites[2] is TopSite.Default)
+ assertEquals("Pocket", topSites[3].title)
+ assertEquals("https://www.getpocket.com", topSites[3].url)
+ assertTrue(topSites[3] is TopSite.Default)
+ }
+
+ @Test
+ fun testAddingPinnedSite() = runTest {
+ storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
+ storage.addPinnedSite("Firefox", "https://www.firefox.com", isDefault = true)
+
+ val topSites = storage.getPinnedSites()
+
+ assertEquals(2, topSites.size)
+ assertEquals(2, storage.getPinnedSitesCount())
+
+ assertEquals("Mozilla", topSites[0].title)
+ assertEquals("https://www.mozilla.org", topSites[0].url)
+ assertTrue(topSites[0] is TopSite.Pinned)
+ assertEquals("Firefox", topSites[1].title)
+ assertEquals("https://www.firefox.com", topSites[1].url)
+ assertTrue(topSites[1] is TopSite.Default)
+ }
+
+ @Test
+ fun testRemovingPinnedSites() = runTest {
+ storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
+ storage.addPinnedSite("Firefox", "https://www.firefox.com")
+
+ storage.getPinnedSites().let { topSites ->
+ assertEquals(2, topSites.size)
+ assertEquals(2, storage.getPinnedSitesCount())
+
+ storage.removePinnedSite(topSites[0])
+ }
+
+ storage.getPinnedSites().let { topSites ->
+ assertEquals(1, topSites.size)
+ assertEquals(1, storage.getPinnedSitesCount())
+
+ assertEquals("Firefox", topSites[0].title)
+ assertEquals("https://www.firefox.com", topSites[0].url)
+ }
+ }
+
+ @Test
+ fun testGettingPinnedSites() = runTest {
+ storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
+ storage.addPinnedSite("Firefox", "https://www.firefox.com", isDefault = true)
+
+ val topSites = storage.getPinnedSites()
+
+ assertNotNull(topSites)
+ assertEquals(2, topSites.size)
+ assertEquals(2, storage.getPinnedSitesCount())
+
+ with(topSites[0]) {
+ assertEquals("Mozilla", title)
+ assertEquals("https://www.mozilla.org", url)
+ assertTrue(this is TopSite.Pinned)
+ }
+
+ with(topSites[1]) {
+ assertEquals("Firefox", title)
+ assertEquals("https://www.firefox.com", url)
+ assertTrue(this is TopSite.Default)
+ }
+ }
+
+ @Test
+ fun testUpdatingPinnedSites() = runTest {
+ storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
+ var pinnedSites = storage.getPinnedSites()
+
+ assertEquals(1, pinnedSites.size)
+ assertEquals(1, storage.getPinnedSitesCount())
+ assertEquals("https://www.mozilla.org", pinnedSites[0].url)
+ assertEquals("Mozilla", pinnedSites[0].title)
+
+ storage.updatePinnedSite(pinnedSites[0], "", "")
+
+ pinnedSites = storage.getPinnedSites()
+ assertEquals(1, pinnedSites.size)
+ assertEquals(1, storage.getPinnedSitesCount())
+ assertEquals("", pinnedSites[0].url)
+ assertEquals("", pinnedSites[0].title)
+
+ storage.updatePinnedSite(pinnedSites[0], "Mozilla Firefox", "https://www.firefox.com")
+
+ pinnedSites = storage.getPinnedSites()
+ assertEquals(1, pinnedSites.size)
+ assertEquals(1, storage.getPinnedSitesCount())
+ assertEquals("https://www.firefox.com", pinnedSites[0].url)
+ assertEquals("Mozilla Firefox", pinnedSites[0].title)
+ }
+
+ @Test
+ fun migrate1to2() {
+ val dbVersion1 = helper.createDatabase(MIGRATION_TEST_DB, 1).apply {
+ execSQL(
+ "INSERT INTO " +
+ "top_sites " +
+ "(title, url, created_at) " +
+ "VALUES " +
+ "('Mozilla','mozilla.org',1)," +
+ "('Top Articles','https://getpocket.com/fenix-top-articles',2)," +
+ "('Wikipedia','https://www.wikipedia.org/',3)," +
+ "('YouTube','https://www.youtube.com/',4)",
+ )
+ }
+
+ dbVersion1.query("SELECT * FROM top_sites").use { cursor ->
+ assertEquals(4, cursor.columnCount)
+ }
+
+ val dbVersion2 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB,
+ 2,
+ true,
+ Migrations.migration_1_2,
+ ).apply {
+ execSQL(
+ "INSERT INTO " +
+ "top_sites " +
+ "(title, url, is_default, created_at) " +
+ "VALUES " +
+ "('Firefox','firefox.com',1,5)," +
+ "('Monitor','https://monitor.firefox.com/',0,5)",
+ )
+ }
+
+ dbVersion2.query("SELECT * FROM top_sites").use { cursor ->
+ assertEquals(5, cursor.columnCount)
+
+ // Check is_default for Mozilla
+ cursor.moveToFirst()
+ assertEquals(0, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+
+ // Check is_default for Top Articles
+ cursor.moveToNext()
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+
+ // Check is_default for Wikipedia
+ cursor.moveToNext()
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+
+ // Check is_default for YouTube
+ cursor.moveToNext()
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+
+ // Check is_default for Firefox
+ cursor.moveToNext()
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+
+ // Check is_default for Monitor
+ cursor.moveToNext()
+ assertEquals(0, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+ }
+ }
+
+ @Test
+ fun migrate2to3() {
+ val dbVersion2 = helper.createDatabase(MIGRATION_TEST_DB, 2).apply {
+ execSQL(
+ "INSERT INTO " +
+ "top_sites " +
+ "(title, url, is_default, created_at) " +
+ "VALUES " +
+ "('Mozilla','mozilla.org',0,1)," +
+ "('Top Articles','https://getpocket.com/fenix-top-articles',0,2)," +
+ "('Wikipedia','https://www.wikipedia.org/',0,3)," +
+ "('YouTube','https://www.youtube.com/',0,4)",
+ )
+ }
+
+ dbVersion2.query("SELECT * FROM top_sites").use { cursor ->
+ assertEquals(5, cursor.columnCount)
+ }
+
+ val dbVersion3 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB,
+ 3,
+ true,
+ Migrations.migration_2_3,
+ )
+
+ dbVersion3.query("SELECT * FROM top_sites").use { cursor ->
+ assertEquals(5, cursor.columnCount)
+ assertEquals(4, cursor.count)
+
+ // Check isDefault for Mozilla
+ cursor.moveToFirst()
+ assertEquals("Mozilla", cursor.getString(cursor.getColumnIndexOrThrow("title")))
+ assertEquals("mozilla.org", cursor.getString(cursor.getColumnIndexOrThrow("url")))
+ assertEquals(0, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("created_at")))
+
+ // Check isDefault for Top Articles
+ cursor.moveToNext()
+ assertEquals("Top Articles", cursor.getString(cursor.getColumnIndexOrThrow("title")))
+ assertEquals(
+ "https://getpocket.com/fenix-top-articles",
+ cursor.getString(cursor.getColumnIndexOrThrow("url")),
+ )
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+ assertEquals(2, cursor.getInt(cursor.getColumnIndexOrThrow("created_at")))
+
+ // Check isDefault for Wikipedia
+ cursor.moveToNext()
+ assertEquals("Wikipedia", cursor.getString(cursor.getColumnIndexOrThrow("title")))
+ assertEquals(
+ "https://www.wikipedia.org/",
+ cursor.getString(cursor.getColumnIndexOrThrow("url")),
+ )
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+ assertEquals(3, cursor.getInt(cursor.getColumnIndexOrThrow("created_at")))
+
+ // Check isDefault for YouTube
+ cursor.moveToNext()
+ assertEquals("YouTube", cursor.getString(cursor.getColumnIndexOrThrow("title")))
+ assertEquals(
+ "https://www.youtube.com/",
+ cursor.getString(cursor.getColumnIndexOrThrow("url")),
+ )
+ assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("is_default")))
+ assertEquals(4, cursor.getInt(cursor.getColumnIndexOrThrow("created_at")))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/db/PinnedSiteDaoTest.kt b/mobile/android/android-components/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/db/PinnedSiteDaoTest.kt
new file mode 100644
index 0000000000..ca146dee82
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/db/PinnedSiteDaoTest.kt
@@ -0,0 +1,118 @@
+/* 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.top.sites.db
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class PinnedSiteDaoTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: TopSiteDatabase
+ private lateinit var pinnedSiteDao: PinnedSiteDao
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, TopSiteDatabase::class.java).build()
+ pinnedSiteDao = database.pinnedSiteDao()
+ executor = Executors.newSingleThreadExecutor()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+
+ @Test
+ fun testAddingTopSite() {
+ val topSite = PinnedSiteEntity(
+ title = "Mozilla",
+ url = "https://www.mozilla.org",
+ isDefault = false,
+ createdAt = 200,
+ ).also {
+ it.id = pinnedSiteDao.insertPinnedSite(it)
+ }
+
+ val pinnedSites = pinnedSiteDao.getPinnedSites()
+
+ assertEquals(1, pinnedSites.size)
+ assertEquals(1, pinnedSiteDao.getPinnedSitesCount())
+ assertEquals(topSite, pinnedSites[0])
+ }
+
+ @Test
+ fun testUpdatingTopSite() {
+ val topSite = PinnedSiteEntity(
+ title = "Mozilla",
+ url = "https://www.mozilla.org",
+ isDefault = false,
+ createdAt = 200,
+ ).also {
+ it.id = pinnedSiteDao.insertPinnedSite(it)
+ }
+
+ topSite.title = "Mozilla (IT)"
+ topSite.url = "https://www.mozilla.org/it"
+ pinnedSiteDao.updatePinnedSite(topSite)
+
+ val pinnedSites = pinnedSiteDao.getPinnedSites()
+
+ assertEquals(1, pinnedSites.size)
+ assertEquals(1, pinnedSiteDao.getPinnedSitesCount())
+ assertEquals(topSite, pinnedSites[0])
+ assertEquals(topSite.title, pinnedSites[0].title)
+ assertEquals(topSite.url, pinnedSites[0].url)
+ }
+
+ @Test
+ fun testRemovingTopSite() {
+ val topSite1 = PinnedSiteEntity(
+ title = "Mozilla",
+ url = "https://www.mozilla.org",
+ isDefault = false,
+ createdAt = 200,
+ ).also {
+ it.id = pinnedSiteDao.insertPinnedSite(it)
+ }
+
+ val topSite2 = PinnedSiteEntity(
+ title = "Firefox",
+ url = "https://www.firefox.com",
+ isDefault = false,
+ createdAt = 100,
+ ).also {
+ it.id = pinnedSiteDao.insertPinnedSite(it)
+ }
+
+ var pinnedSites = pinnedSiteDao.getPinnedSites()
+
+ assertEquals(2, pinnedSites.size)
+ assertEquals(2, pinnedSiteDao.getPinnedSitesCount())
+
+ pinnedSiteDao.deletePinnedSite(topSite1)
+
+ pinnedSites = pinnedSiteDao.getPinnedSites()
+
+ assertEquals(1, pinnedSites.size)
+ assertEquals(1, pinnedSiteDao.getPinnedSitesCount())
+ assertEquals(topSite2, pinnedSites[0])
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/top-sites/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/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/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt
new file mode 100644
index 0000000000..960215abef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt
@@ -0,0 +1,140 @@
+/* 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.top.sites
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.feature.top.sites.ext.hasHost
+import mozilla.components.feature.top.sites.ext.hasUrl
+import mozilla.components.feature.top.sites.ext.toTopSite
+import mozilla.components.feature.top.sites.facts.emitTopSitesCountFact
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Default implementation of [TopSitesStorage].
+ *
+ * @param pinnedSitesStorage An instance of [PinnedSiteStorage], used for storing pinned sites.
+ * @param historyStorage An instance of [PlacesHistoryStorage], used for retrieving top frecent
+ * sites from history.
+ * @param topSitesProvider An optional instance of [TopSitesProvider], used for retrieving
+ * additional top sites from a provider. The returned top sites are added before pinned sites.
+ * @param defaultTopSites A list containing a title to url pair of default top sites to be added
+ * to the [PinnedSiteStorage].
+ */
+class DefaultTopSitesStorage(
+ private val pinnedSitesStorage: PinnedSiteStorage,
+ private val historyStorage: PlacesHistoryStorage,
+ private val topSitesProvider: TopSitesProvider? = null,
+ private val defaultTopSites: List<Pair<String, String>> = listOf(),
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+) : TopSitesStorage, Observable<TopSitesStorage.Observer> by ObserverRegistry() {
+
+ private var scope = CoroutineScope(coroutineContext)
+ private val logger = Logger("DefaultTopSitesStorage")
+
+ // Cache of the last retrieved top sites
+ var cachedTopSites = listOf<TopSite>()
+
+ init {
+ if (defaultTopSites.isNotEmpty()) {
+ scope.launch {
+ pinnedSitesStorage.addAllPinnedSites(defaultTopSites, isDefault = true)
+ }
+ }
+ }
+
+ override fun addTopSite(title: String, url: String, isDefault: Boolean) {
+ scope.launch {
+ pinnedSitesStorage.addPinnedSite(title, url, isDefault)
+ notifyObservers { onStorageUpdated() }
+ }
+ }
+
+ override fun removeTopSite(topSite: TopSite) {
+ scope.launch {
+ if (topSite is TopSite.Default || topSite is TopSite.Pinned) {
+ pinnedSitesStorage.removePinnedSite(topSite)
+ }
+
+ // Remove the top site from both history and pinned sites storage to avoid having it
+ // show up as a frecent site if it is a pinned site.
+ if (topSite !is TopSite.Provided) {
+ historyStorage.deleteVisitsFor(topSite.url)
+ }
+
+ notifyObservers { onStorageUpdated() }
+ }
+ }
+
+ override fun updateTopSite(topSite: TopSite, title: String, url: String) {
+ scope.launch {
+ if (topSite is TopSite.Default || topSite is TopSite.Pinned) {
+ pinnedSitesStorage.updatePinnedSite(topSite, title, url)
+ }
+
+ notifyObservers { onStorageUpdated() }
+ }
+ }
+
+ @Suppress("ComplexCondition", "TooGenericExceptionCaught")
+ override suspend fun getTopSites(
+ totalSites: Int,
+ frecencyConfig: TopSitesFrecencyConfig?,
+ providerConfig: TopSitesProviderConfig?,
+ ): List<TopSite> {
+ val topSites = ArrayList<TopSite>()
+ val pinnedSites = pinnedSitesStorage.getPinnedSites().take(totalSites)
+ var providerTopSites = emptyList<TopSite>()
+ var numSitesRequired = totalSites - pinnedSites.size
+
+ if (topSitesProvider != null &&
+ providerConfig != null &&
+ providerConfig.showProviderTopSites &&
+ pinnedSites.size < providerConfig.maxThreshold
+ ) {
+ try {
+ providerTopSites = topSitesProvider
+ .getTopSites(allowCache = true)
+ .filter { providerConfig.providerFilter?.invoke(it) ?: true }
+ .take(numSitesRequired)
+ .take(providerConfig.maxThreshold - pinnedSites.size)
+ topSites.addAll(providerTopSites)
+ numSitesRequired -= providerTopSites.size
+ } catch (e: Exception) {
+ logger.error("Failed to fetch top sites from provider", e)
+ }
+ }
+
+ topSites.addAll(pinnedSites)
+
+ if (frecencyConfig?.frecencyTresholdOption != null && numSitesRequired > 0) {
+ // Get 'totalSites' sites for duplicate entries with
+ // existing pinned sites
+ val frecentSites = historyStorage
+ .getTopFrecentSites(totalSites, frecencyConfig.frecencyTresholdOption)
+ .map { it.toTopSite() }
+ .filter {
+ !pinnedSites.hasUrl(it.url) &&
+ !providerTopSites.hasHost(it.url) &&
+ frecencyConfig.frecencyFilter?.invoke(it) ?: true
+ }
+ .take(numSitesRequired)
+
+ topSites.addAll(frecentSites)
+ }
+
+ if (topSites != cachedTopSites) {
+ emitTopSitesCountFact(pinnedSites.size)
+ cachedTopSites = topSites
+ }
+
+ return topSites
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/PinnedSiteStorage.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/PinnedSiteStorage.kt
new file mode 100644
index 0000000000..d77a599ace
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/PinnedSiteStorage.kt
@@ -0,0 +1,104 @@
+/* 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.top.sites
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
+import mozilla.components.feature.top.sites.db.PinnedSiteEntity
+import mozilla.components.feature.top.sites.db.TopSiteDatabase
+import mozilla.components.feature.top.sites.db.toPinnedSite
+
+/**
+ * A storage implementation for organizing pinned sites.
+ */
+class PinnedSiteStorage(context: Context) {
+
+ @VisibleForTesting
+ internal var currentTimeMillis: () -> Long = { System.currentTimeMillis() }
+
+ @VisibleForTesting
+ internal var database: Lazy<TopSiteDatabase> = lazy { TopSiteDatabase.get(context) }
+ private val pinnedSiteDao by lazy { database.value.pinnedSiteDao() }
+
+ /**
+ * Adds the given list pinned sites.
+ *
+ * @param topSites A list containing a title to url pair of top sites to be added.
+ * @param isDefault Whether or not the pinned site added should be a default pinned site. This
+ * is used to identify pinned sites that are added by the application.
+ */
+ suspend fun addAllPinnedSites(
+ topSites: List<Pair<String, String>>,
+ isDefault: Boolean = false,
+ ) = withContext(IO) {
+ val siteEntities = topSites.map { (title, url) ->
+ PinnedSiteEntity(
+ title = title,
+ url = url,
+ isDefault = isDefault,
+ createdAt = currentTimeMillis(),
+ )
+ }
+ pinnedSiteDao.insertAllPinnedSites(siteEntities)
+ }
+
+ /**
+ * Adds a new pinned site.
+ *
+ * @param title The title string.
+ * @param url The URL string.
+ * @param isDefault Whether or not the pinned site added should be a default pinned site. This
+ * is used to identify pinned sites that are added by the application.
+ */
+ suspend fun addPinnedSite(title: String, url: String, isDefault: Boolean = false) =
+ withContext(IO) {
+ val entity = PinnedSiteEntity(
+ title = title,
+ url = url,
+ isDefault = isDefault,
+ createdAt = currentTimeMillis(),
+ )
+ entity.id = pinnedSiteDao.insertPinnedSite(entity)
+ }
+
+ /**
+ * Returns a list of all the pinned sites.
+ */
+ suspend fun getPinnedSites(): List<TopSite> = withContext(IO) {
+ pinnedSiteDao.getPinnedSites().map { entity -> entity.toTopSite() }
+ }
+
+ /**
+ * Removes the given pinned site.
+ *
+ * @param site The pinned site.
+ */
+ suspend fun removePinnedSite(site: TopSite) = withContext(IO) {
+ pinnedSiteDao.deletePinnedSite(site.toPinnedSite())
+ }
+
+ /**
+ * Updates the given pinned site.
+ *
+ * @param site The pinned site.
+ * @param title The new title for the top site.
+ * @param url The new url for the top site.
+ */
+ suspend fun updatePinnedSite(site: TopSite, title: String, url: String) = withContext(IO) {
+ val pinnedSite = site.toPinnedSite()
+ pinnedSite.title = title
+ pinnedSite.url = url
+ pinnedSiteDao.updatePinnedSite(pinnedSite)
+ }
+
+ /**
+ * Returns a count of pinned sites.
+ */
+ suspend fun getPinnedSitesCount(): Int = withContext(IO) {
+ pinnedSiteDao.getPinnedSitesCount()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSite.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSite.kt
new file mode 100644
index 0000000000..badc544d8a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSite.kt
@@ -0,0 +1,90 @@
+/* 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.top.sites
+
+/**
+ * A top site.
+ */
+sealed class TopSite {
+ abstract val id: Long?
+ abstract val title: String?
+ abstract val url: String
+ abstract val createdAt: Long?
+ abstract val type: String
+
+ /**
+ * This top site was added as a default by the application.
+ *
+ * @property id Unique ID of this top site.
+ * @property title The title of the top site.
+ * @property url The URL of the top site.
+ * @property createdAt The optional date the top site was added.
+ * @property type The type name of the top site.
+ */
+ data class Default(
+ override val id: Long?,
+ override val title: String?,
+ override val url: String,
+ override val createdAt: Long?,
+ override val type: String = "DEFAULT",
+ ) : TopSite()
+
+ /**
+ * This top site was pinned by an user.
+ *
+ * @property id Unique ID of this top site.
+ * @property title The title of the top site.
+ * @property url The URL of the top site.
+ * @property createdAt The optional date the top site was added.
+ * @property type The type name of the top site.
+ */
+ data class Pinned(
+ override val id: Long?,
+ override val title: String?,
+ override val url: String,
+ override val createdAt: Long?,
+ override val type: String = "PINNED",
+ ) : TopSite()
+
+ /**
+ * This top site is auto-generated from the history storage based on the most frecent site.
+ *
+ * @property id Unique ID of this top site.
+ * @property title The title of the top site.
+ * @property url The URL of the top site.
+ * @property createdAt The optional date the top site was added.
+ * @property type The type name of the top site.
+ */
+ data class Frecent(
+ override val id: Long?,
+ override val title: String?,
+ override val url: String,
+ override val createdAt: Long?,
+ override val type: String = "FRECENT",
+ ) : TopSite()
+
+ /**
+ * This top site is provided by the [TopSitesProvider].
+ *
+ * @property id Unique ID of this top site.
+ * @property title The title of the top site.
+ * @property url The URL of the top site.
+ * @property clickUrl The click URL of the top site.
+ * @property imageUrl The image URL of the top site.
+ * @property impressionUrl The URL that needs to be fired when the top site is displayed.
+ * @property createdAt The optional date the top site was added.
+ * @property type The type name of the top site.
+ */
+ data class Provided(
+ override val id: Long?,
+ override val title: String?,
+ override val url: String,
+ val clickUrl: String,
+ val imageUrl: String,
+ val impressionUrl: String,
+ override val createdAt: Long?,
+ override val type: String = "PROVIDED",
+ ) : TopSite()
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesConfig.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesConfig.kt
new file mode 100644
index 0000000000..d9a95bf138
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesConfig.kt
@@ -0,0 +1,50 @@
+/* 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.top.sites
+
+import mozilla.components.concept.storage.FrecencyThresholdOption
+
+/**
+ * Top sites configuration to specify the number of top sites to display and
+ * whether or not to include top frecent sites in the top sites feature.
+ *
+ * @property totalSites A total number of sites that will be displayed.
+ * @property frecencyConfig An instance of [TopSitesFrecencyConfig] that specifies which top
+ * frecent sites should be included.
+ * @property providerConfig An instance of [TopSitesProviderConfig] that specifies whether or
+ * not to fetch top sites from the [TopSitesProvider].
+ */
+data class TopSitesConfig(
+ val totalSites: Int,
+ val frecencyConfig: TopSitesFrecencyConfig? = null,
+ val providerConfig: TopSitesProviderConfig? = null,
+)
+
+/**
+ * Top sites provider configuration to specify whether or not to fetch top sites from the provider.
+ *
+ * @property showProviderTopSites Whether or not to display the top sites from the provider.
+ * @property maxThreshold Only fetch the top sites from the provider if the number of top sites are
+ * below the maximum threshold.
+ * @property providerFilter Optional function used to filter the top sites from the provider.
+ */
+data class TopSitesProviderConfig(
+ val showProviderTopSites: Boolean,
+ val maxThreshold: Int = Int.MAX_VALUE,
+ val providerFilter: ((TopSite) -> Boolean)? = null,
+)
+
+/**
+ * Top sites frecency configuration used to specify which top frecent sites should be included.
+ *
+ * @property frecencyTresholdOption If [frecencyTresholdOption] is specified, only visited sites with a frecency
+ * score above the given threshold will be returned. Otherwise, frecent top site results are
+ * not included.
+ * @property frecencyFilter Optional function used to filter the top frecent sites.
+ */
+data class TopSitesFrecencyConfig(
+ val frecencyTresholdOption: FrecencyThresholdOption? = null,
+ val frecencyFilter: ((TopSite) -> Boolean)? = null,
+)
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesFeature.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesFeature.kt
new file mode 100644
index 0000000000..412c1e0666
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesFeature.kt
@@ -0,0 +1,38 @@
+/* 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.top.sites
+
+import mozilla.components.feature.top.sites.presenter.DefaultTopSitesPresenter
+import mozilla.components.feature.top.sites.presenter.TopSitesPresenter
+import mozilla.components.feature.top.sites.view.TopSitesView
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * View-bound feature that updates the UI when the [TopSitesStorage] is updated.
+ *
+ * @param view An implementor of [TopSitesView] that will be notified of changes to the storage.
+ * @param storage The top sites storage that stores pinned and frecent sites.
+ * @param config Lambda expression that returns [TopSitesConfig] which species the number of top
+ * sites to return and whether or not to include frequently visited sites.
+ */
+class TopSitesFeature(
+ private val view: TopSitesView,
+ val storage: TopSitesStorage,
+ val config: () -> TopSitesConfig,
+ private val presenter: TopSitesPresenter = DefaultTopSitesPresenter(
+ view,
+ storage,
+ config,
+ ),
+) : LifecycleAwareFeature {
+
+ override fun start() {
+ presenter.start()
+ }
+
+ override fun stop() {
+ presenter.stop()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesProvider.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesProvider.kt
new file mode 100644
index 0000000000..4ac8200901
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesProvider.kt
@@ -0,0 +1,20 @@
+/* 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.top.sites
+
+/**
+ * A contract that indicates how a top sites provider must behave.
+ */
+interface TopSitesProvider {
+
+ /**
+ * Provides a list of top sites.
+ *
+ * @param allowCache Whether or not the result may be provided from a previously
+ * cached response.
+ * @return a list of top sites from the provider.
+ */
+ suspend fun getTopSites(allowCache: Boolean = true): List<TopSite>
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesStorage.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesStorage.kt
new file mode 100644
index 0000000000..b27f8a3ae0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesStorage.kt
@@ -0,0 +1,65 @@
+/* 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.top.sites
+
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.support.base.observer.Observable
+
+/**
+ * Abstraction layer above the [PinnedSiteStorage] and [PlacesHistoryStorage] storages.
+ */
+interface TopSitesStorage : Observable<TopSitesStorage.Observer> {
+ /**
+ * Adds a new top site.
+ *
+ * @param title The title string.
+ * @param url The URL string.
+ * @param isDefault Whether or not the pinned site added should be a default pinned site. This
+ * is used to identify pinned sites that are added by the application.
+ */
+ fun addTopSite(title: String, url: String, isDefault: Boolean = false)
+
+ /**
+ * Removes the given [TopSite].
+ *
+ * @param topSite The top site.
+ */
+ fun removeTopSite(topSite: TopSite)
+
+ /**
+ * Updates the given [TopSite].
+ *
+ * @param topSite The top site.
+ * @param title The new title for the top site.
+ * @param url The new url for the top site.
+ */
+ fun updateTopSite(topSite: TopSite, title: String, url: String)
+
+ /**
+ * Return a unified list of top sites based on the given number of sites desired.
+ * If `frecencyConfig` is specified, fill in any missing top sites with frecent top site results.
+ *
+ * @param totalSites A total number of sites that will be retrieve if possible.
+ * @param frecencyConfig An instance of [TopSitesFrecencyConfig] that specifies which top
+ * frecent sites to be included.
+ * @param providerConfig An instance of [TopSitesProviderConfig] that specifies whether or
+ * not to fetch top sites from the [TopSitesProvider].
+ */
+ suspend fun getTopSites(
+ totalSites: Int,
+ frecencyConfig: TopSitesFrecencyConfig? = null,
+ providerConfig: TopSitesProviderConfig? = null,
+ ): List<TopSite>
+
+ /**
+ * Interface to be implemented by classes that want to observe the top site storage.
+ */
+ interface Observer {
+ /**
+ * Notify the observer when changes are made to the storage.
+ */
+ fun onStorageUpdated()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesUseCases.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesUseCases.kt
new file mode 100644
index 0000000000..c7eb2f4fe6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesUseCases.kt
@@ -0,0 +1,67 @@
+/* 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.top.sites
+
+/**
+ * Contains use cases related to the top sites feature.
+ */
+class TopSitesUseCases(topSitesStorage: TopSitesStorage) {
+ /**
+ * Add a pinned site use case.
+ */
+ class AddPinnedSiteUseCase internal constructor(private val storage: TopSitesStorage) {
+ /**
+ * Adds a new [PinnedSite].
+ *
+ * @param title The title string.
+ * @param url The URL string.
+ */
+ operator fun invoke(title: String, url: String, isDefault: Boolean = false) {
+ storage.addTopSite(title, url, isDefault)
+ }
+ }
+
+ /**
+ * Remove a top site use case.
+ */
+ class RemoveTopSiteUseCase internal constructor(private val storage: TopSitesStorage) {
+ /**
+ * Removes the given [TopSite].
+ *
+ * @param topSite The top site.
+ */
+ operator fun invoke(topSite: TopSite) {
+ storage.removeTopSite(topSite)
+ }
+ }
+
+ /**
+ * Update a top site use case.
+ */
+ class UpdateTopSiteUseCase internal constructor(private val storage: TopSitesStorage) {
+ /**
+ * Updates the given [TopSite].
+ *
+ * @param topSite The top site.
+ * @param title The new title for the top site.
+ * @param url The new url for the top site.
+ */
+ operator fun invoke(topSite: TopSite, title: String, url: String) {
+ storage.updateTopSite(topSite, title, url)
+ }
+ }
+
+ val addPinnedSites: AddPinnedSiteUseCase by lazy {
+ AddPinnedSiteUseCase(topSitesStorage)
+ }
+
+ val removeTopSites: RemoveTopSiteUseCase by lazy {
+ RemoveTopSiteUseCase(topSitesStorage)
+ }
+
+ val updateTopSites: UpdateTopSiteUseCase by lazy {
+ UpdateTopSiteUseCase(topSitesStorage)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteDao.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteDao.kt
new file mode 100644
index 0000000000..02c672b3fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteDao.kt
@@ -0,0 +1,48 @@
+/* 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.top.sites.db
+
+import androidx.annotation.WorkerThread
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Update
+
+/**
+ * Internal DAO for accessing [PinnedSiteEntity] instances.
+ */
+@Dao
+internal interface PinnedSiteDao {
+ @WorkerThread
+ @Insert
+ fun insertPinnedSite(site: PinnedSiteEntity): Long
+
+ @WorkerThread
+ @Update
+ fun updatePinnedSite(site: PinnedSiteEntity)
+
+ @WorkerThread
+ @Delete
+ fun deletePinnedSite(site: PinnedSiteEntity)
+
+ @WorkerThread
+ @Transaction
+ fun insertAllPinnedSites(sites: List<PinnedSiteEntity>): List<Long> {
+ return sites.map { entity ->
+ val id = insertPinnedSite(entity)
+ entity.id = id
+ id
+ }
+ }
+
+ @WorkerThread
+ @Query("SELECT * FROM top_sites")
+ fun getPinnedSites(): List<PinnedSiteEntity>
+
+ @Query("SELECT COUNT(*) FROM top_sites")
+ fun getPinnedSitesCount(): Int
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteEntity.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteEntity.kt
new file mode 100644
index 0000000000..fb106bf91f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteEntity.kt
@@ -0,0 +1,59 @@
+/* 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.top.sites.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.feature.top.sites.TopSite
+
+/**
+ * Internal entity representing a pinned site.
+ */
+@Entity(tableName = "top_sites")
+internal data class PinnedSiteEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var id: Long? = null,
+
+ @ColumnInfo(name = "title")
+ var title: String,
+
+ @ColumnInfo(name = "url")
+ var url: String,
+
+ @ColumnInfo(name = "is_default")
+ var isDefault: Boolean = false,
+
+ @ColumnInfo(name = "created_at")
+ var createdAt: Long = System.currentTimeMillis(),
+) {
+ internal fun toTopSite(): TopSite =
+ if (isDefault) {
+ TopSite.Default(
+ id = id,
+ title = title,
+ url = url,
+ createdAt = createdAt,
+ )
+ } else {
+ TopSite.Pinned(
+ id = id,
+ title = title,
+ url = url,
+ createdAt = createdAt,
+ )
+ }
+}
+
+internal fun TopSite.toPinnedSite(): PinnedSiteEntity {
+ return PinnedSiteEntity(
+ id = id,
+ title = title ?: "",
+ url = url,
+ isDefault = this is TopSite.Default,
+ createdAt = createdAt ?: 0,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/TopSiteDatabase.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/TopSiteDatabase.kt
new file mode 100644
index 0000000000..59aa469f54
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/TopSiteDatabase.kt
@@ -0,0 +1,107 @@
+/* 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.top.sites.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+/**
+ * Internal database for storing top sites.
+ */
+@Database(entities = [PinnedSiteEntity::class], version = 3)
+internal abstract class TopSiteDatabase : RoomDatabase() {
+ abstract fun pinnedSiteDao(): PinnedSiteDao
+
+ companion object {
+ @Volatile
+ private var instance: TopSiteDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): TopSiteDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ TopSiteDatabase::class.java,
+ "top_sites",
+ ).addMigrations(
+ Migrations.migration_1_2,
+ ).addMigrations(
+ Migrations.migration_2_3,
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
+
+internal object Migrations {
+ val migration_1_2 = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Add the new is_default column and set is_default to 0 (false) for every entry.
+ db.execSQL(
+ "ALTER TABLE top_sites ADD COLUMN is_default INTEGER NOT NULL DEFAULT 0",
+ )
+
+ // Prior to version 2, pocket top sites, wikipedia and youtube were added as default
+ // sites in Fenix. Look for these entries and set is_default to 1 (true).
+ db.execSQL(
+ "UPDATE top_sites " +
+ "SET is_default = 1 " +
+ "WHERE url IN " +
+ "('https://getpocket.com/fenix-top-articles', " +
+ "'https://www.wikipedia.org/', " +
+ "'https://www.youtube.com/')",
+ )
+ }
+ }
+
+ @Suppress("MagicNumber")
+ val migration_2_3 = object : Migration(2, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Create a temporary top sites table of version 1.
+ db.execSQL(
+ "CREATE TABLE IF NOT EXISTS `top_sites_temp` (" +
+ "`id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "`title` TEXT NOT NULL, " +
+ "`url` TEXT NOT NULL, " +
+ "`is_default` INTEGER NOT NULL, " +
+ "`created_at` INTEGER NOT NULL)",
+ )
+
+ // Insert every entry from the old table into the temporary top sites table.
+ db.execSQL(
+ "INSERT INTO top_sites_temp (title, url, created_at, is_default) " +
+ "SELECT title, url, created_at, 0 FROM top_sites",
+ )
+
+ // Assume there are consumers of version 2 with the mismatched isDefault and is_default
+ // column name. Drop the old table.
+ db.execSQL(
+ "DROP TABLE top_sites",
+ )
+
+ // Rename the temporary table to top_sites.
+ db.execSQL(
+ "ALTER TABLE top_sites_temp RENAME TO top_sites",
+ )
+
+ // Prior to version 2, pocket top sites, wikipedia and youtube were added as default
+ // sites in Fenix. Look for these entries and set isDefault to 1 (true).
+ db.execSQL(
+ "UPDATE top_sites " +
+ "SET is_default = 1 " +
+ "WHERE url IN " +
+ "('https://getpocket.com/fenix-top-articles', " +
+ "'https://www.wikipedia.org/', " +
+ "'https://www.youtube.com/')",
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopFrecentSiteInfo.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopFrecentSiteInfo.kt
new file mode 100644
index 0000000000..9fa513d6de
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopFrecentSiteInfo.kt
@@ -0,0 +1,21 @@
+/* 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.top.sites.ext
+
+import mozilla.components.concept.storage.TopFrecentSiteInfo
+import mozilla.components.feature.top.sites.TopSite
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+
+/**
+ * Returns a [TopSite] for the given [TopFrecentSiteInfo].
+ */
+fun TopFrecentSiteInfo.toTopSite(): TopSite {
+ return TopSite.Frecent(
+ id = null,
+ title = this.title?.takeIf(String::isNotBlank) ?: this.url.tryGetHostFromUrl(),
+ url = this.url,
+ createdAt = null,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopSite.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopSite.kt
new file mode 100644
index 0000000000..c9d703484c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopSite.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.top.sites.ext
+
+import mozilla.components.feature.top.sites.TopSite
+import mozilla.components.support.ktx.kotlin.getRepresentativeSnippet
+import mozilla.components.support.ktx.util.URLStringUtils
+
+/**
+ * Returns true if the given url is in the list top site and false otherwise.
+ *
+ * @param url The URL string.
+ */
+fun List<TopSite>.hasUrl(url: String): Boolean {
+ for (topSite in this) {
+ // Strip the https/http and WWW prefixes from the urls.
+ if (URLStringUtils.toDisplayUrl(topSite.url) == URLStringUtils.toDisplayUrl(url)) {
+ return true
+ }
+ }
+
+ return false
+}
+
+/**
+ * Returns true if the given url host/domain is in the list top site and false otherwise.
+ *
+ * @param url The URL string.
+ */
+fun List<TopSite>.hasHost(url: String): Boolean {
+ return this.any { it.url.getRepresentativeSnippet().equals(url.getRepresentativeSnippet(), true) }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/facts/TopSitesFacts.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/facts/TopSitesFacts.kt
new file mode 100644
index 0000000000..c667613d65
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/facts/TopSitesFacts.kt
@@ -0,0 +1,31 @@
+/* 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.top.sites.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [TopSitesFeature]
+ */
+class TopSitesFacts {
+ /**
+ * Items that specify which portion of the [TopSitesFeature] was interacted with
+ */
+ object Items {
+ const val COUNT = "count"
+ }
+}
+
+internal fun emitTopSitesCountFact(count: Int) {
+ Fact(
+ Component.FEATURE_TOP_SITES,
+ Action.INTERACTION,
+ TopSitesFacts.Items.COUNT,
+ count.toString(),
+ ).collect()
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/presenter/DefaultTopSitesPresenter.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/presenter/DefaultTopSitesPresenter.kt
new file mode 100644
index 0000000000..f1d9adb7fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/presenter/DefaultTopSitesPresenter.kt
@@ -0,0 +1,58 @@
+/* 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.top.sites.presenter
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.feature.top.sites.TopSitesConfig
+import mozilla.components.feature.top.sites.TopSitesStorage
+import mozilla.components.feature.top.sites.view.TopSitesView
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Default implementation of [TopSitesPresenter]. Connects the [TopSitesView] with the
+ * [TopSitesStorage] to update the view whenever the storage is updated.
+ *
+ * @param view An implementor of [TopSitesView] that will be notified of changes to the storage.
+ * @param storage The top sites storage that stores pinned and frecent sites.
+ * @param config Lambda expression that returns [TopSitesConfig] which species the number of top
+ * sites to return and whether or not to include frequently visited sites.
+ */
+internal class DefaultTopSitesPresenter(
+ override val view: TopSitesView,
+ override val storage: TopSitesStorage,
+ private val config: () -> TopSitesConfig,
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+) : TopSitesPresenter, TopSitesStorage.Observer {
+
+ private val scope = CoroutineScope(coroutineContext)
+
+ override fun start() {
+ onStorageUpdated()
+
+ storage.register(this)
+ }
+
+ override fun stop() {
+ storage.unregister(this)
+ }
+
+ override fun onStorageUpdated() {
+ val innerConfig = config.invoke()
+
+ scope.launch {
+ val topSites = storage.getTopSites(
+ totalSites = innerConfig.totalSites,
+ frecencyConfig = innerConfig.frecencyConfig,
+ providerConfig = innerConfig.providerConfig,
+ )
+
+ scope.launch(Dispatchers.Main) {
+ view.displayTopSites(topSites)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/presenter/TopSitesPresenter.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/presenter/TopSitesPresenter.kt
new file mode 100644
index 0000000000..92d03492bb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/presenter/TopSitesPresenter.kt
@@ -0,0 +1,17 @@
+/* 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.top.sites.presenter
+
+import mozilla.components.feature.top.sites.TopSitesStorage
+import mozilla.components.feature.top.sites.view.TopSitesView
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * A presenter that connects the [TopSitesView] with the [TopSitesStorage].
+ */
+interface TopSitesPresenter : LifecycleAwareFeature {
+ val view: TopSitesView
+ val storage: TopSitesStorage
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/view/TopSitesView.kt b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/view/TopSitesView.kt
new file mode 100644
index 0000000000..58c62fff6d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/view/TopSitesView.kt
@@ -0,0 +1,17 @@
+/* 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.top.sites.view
+
+import mozilla.components.feature.top.sites.TopSite
+
+/**
+ * Implemented by the application for displaying onto the UI.
+ */
+interface TopSitesView {
+ /**
+ * Updates the UI with new list of top sites.
+ */
+ fun displayTopSites(topSites: List<TopSite>)
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt
new file mode 100644
index 0000000000..db650a1b6b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt
@@ -0,0 +1,1169 @@
+/* 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.top.sites
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.concept.storage.FrecencyThresholdOption
+import mozilla.components.concept.storage.TopFrecentSiteInfo
+import mozilla.components.feature.top.sites.ext.toTopSite
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class DefaultTopSitesStorageTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val pinnedSitesStorage: PinnedSiteStorage = mock()
+ private val historyStorage: PlacesHistoryStorage = mock()
+ private val topSitesProvider: TopSitesProvider = mock()
+
+ @Test
+ fun `default top sites are added to pinned site storage on init`() = runTestOnMain {
+ val defaultTopSites = listOf(
+ Pair("Mozilla", "https://mozilla.com"),
+ Pair("Firefox", "https://firefox.com"),
+ )
+
+ DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ defaultTopSites = defaultTopSites,
+ coroutineContext = coroutineContext,
+ )
+
+ verify(pinnedSitesStorage).addAllPinnedSites(defaultTopSites, isDefault = true)
+ }
+
+ @Test
+ fun `addPinnedSite`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ defaultTopSitesStorage.addTopSite("Mozilla", "https://mozilla.com", isDefault = false)
+
+ verify(pinnedSitesStorage).addPinnedSite(
+ "Mozilla",
+ "https://mozilla.com",
+ isDefault = false,
+ )
+ }
+
+ @Test
+ fun `removeTopSite`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val frecentSite = TopSite.Frecent(
+ id = 1,
+ title = "Mozilla",
+ url = "https://mozilla.com",
+ createdAt = 1,
+ )
+
+ defaultTopSitesStorage.removeTopSite(frecentSite)
+
+ verify(historyStorage).deleteVisitsFor(frecentSite.url)
+
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 2,
+ )
+
+ defaultTopSitesStorage.removeTopSite(pinnedSite)
+
+ verify(pinnedSitesStorage).removePinnedSite(pinnedSite)
+ verify(historyStorage).deleteVisitsFor(pinnedSite.url)
+
+ val defaultSite = TopSite.Default(
+ id = 3,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 3,
+ )
+
+ defaultTopSitesStorage.removeTopSite(defaultSite)
+
+ verify(pinnedSitesStorage).removePinnedSite(defaultSite)
+ verify(historyStorage).deleteVisitsFor(defaultSite.url)
+ }
+
+ @Test
+ fun `updateTopSite`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+
+ defaultTopSitesStorage.updateTopSite(defaultSite, "Mozilla Firefox", "https://mozilla.com")
+
+ verify(pinnedSitesStorage).updatePinnedSite(defaultSite, "Mozilla Firefox", "https://mozilla.com")
+
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+
+ defaultTopSitesStorage.updateTopSite(pinnedSite, "Wiki", "https://en.wikipedia.org/wiki/Wiki")
+
+ verify(pinnedSitesStorage).updatePinnedSite(pinnedSite, "Wiki", "https://en.wikipedia.org/wiki/Wiki")
+
+ val frecentSite = TopSite.Frecent(
+ id = 1,
+ title = "Mozilla",
+ url = "https://mozilla.com",
+ createdAt = 1,
+ )
+
+ defaultTopSitesStorage.updateTopSite(frecentSite, "Moz", "")
+
+ verify(pinnedSitesStorage, never()).updatePinnedSite(frecentSite, "Moz", "")
+ }
+
+ @Test
+ fun `GIVEN frecencyConfig and providerConfig are null WHEN getTopSites is called THEN only default and pinned sites are returned`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite,
+ ),
+ )
+ whenever(pinnedSitesStorage.getPinnedSitesCount()).thenReturn(2)
+
+ var topSites = defaultTopSitesStorage.getTopSites(totalSites = 0)
+
+ assertTrue(topSites.isEmpty())
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(totalSites = 1)
+
+ assertEquals(1, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(totalSites = 2)
+
+ assertEquals(2, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(totalSites = 5)
+
+ assertEquals(2, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `GIVEN providerConfig is specified WHEN getTopSites is called THEN default, pinned and provided top sites are returned`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ topSitesProvider = topSitesProvider,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+ val providedSite = TopSite.Provided(
+ id = 3,
+ title = "Mozilla",
+ url = "https://mozilla.com",
+ clickUrl = "https://mozilla.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite,
+ ),
+ )
+ whenever(topSitesProvider.getTopSites()).thenReturn(listOf(providedSite))
+
+ var topSites = defaultTopSitesStorage.getTopSites(totalSites = 0)
+
+ assertTrue(topSites.isEmpty())
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 1,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ assertEquals(1, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 2,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ assertEquals(2, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ assertEquals(3, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite, topSites[2])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = false,
+ ),
+ )
+
+ assertEquals(2, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ maxThreshold = 8,
+ ),
+ )
+
+ assertEquals(3, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite, topSites[2])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ maxThreshold = 2,
+ ),
+ )
+
+ assertEquals(2, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `GIVEN providerConfig with maxThreshold is specified WHEN getTopSites is called THEN the correct number of provided top sites are returned`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ topSitesProvider = topSitesProvider,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite1 = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+ val pinnedSite2 = TopSite.Pinned(
+ id = 3,
+ title = "Example",
+ url = "https://example.com",
+ createdAt = 3,
+ )
+ val providedSite1 = TopSite.Provided(
+ id = 4,
+ title = "Mozilla",
+ url = "https://mozilla.com",
+ clickUrl = "https://mozilla.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+ val providedSite2 = TopSite.Provided(
+ id = 5,
+ title = "Pocket",
+ url = "https://pocket.com",
+ clickUrl = "https://mozilla.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite1,
+ pinnedSite2,
+ defaultSite,
+ pinnedSite1,
+ pinnedSite2,
+ ),
+ )
+ whenever(topSitesProvider.getTopSites()).thenReturn(listOf(providedSite1, providedSite2))
+
+ var topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 8,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ maxThreshold = 8,
+ ),
+ )
+
+ assertEquals(8, topSites.size)
+ assertEquals(providedSite1, topSites[0])
+ assertEquals(providedSite2, topSites[1])
+ assertEquals(defaultSite, topSites[2])
+ assertEquals(pinnedSite1, topSites[3])
+ assertEquals(pinnedSite2, topSites[4])
+ assertEquals(defaultSite, topSites[5])
+ assertEquals(pinnedSite1, topSites[6])
+ assertEquals(pinnedSite2, topSites[7])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite1,
+ pinnedSite2,
+ defaultSite,
+ pinnedSite1,
+ pinnedSite2,
+ defaultSite,
+ ),
+ )
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 8,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ maxThreshold = 8,
+ ),
+ )
+
+ assertEquals(8, topSites.size)
+ assertEquals(providedSite1, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite1, topSites[2])
+ assertEquals(pinnedSite2, topSites[3])
+ assertEquals(defaultSite, topSites[4])
+ assertEquals(pinnedSite1, topSites[5])
+ assertEquals(pinnedSite2, topSites[6])
+ assertEquals(defaultSite, topSites[7])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite1,
+ pinnedSite2,
+ defaultSite,
+ pinnedSite1,
+ pinnedSite2,
+ defaultSite,
+ pinnedSite1,
+ ),
+ )
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 8,
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ maxThreshold = 8,
+ ),
+ )
+
+ assertEquals(8, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite1, topSites[1])
+ assertEquals(pinnedSite2, topSites[2])
+ assertEquals(defaultSite, topSites[3])
+ assertEquals(pinnedSite1, topSites[4])
+ assertEquals(pinnedSite2, topSites[5])
+ assertEquals(defaultSite, topSites[6])
+ assertEquals(pinnedSite1, topSites[7])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `GIVEN frecencyConfig and providerConfig are specified WHEN getTopSites is called THEN default, pinned, provided and frecent top sites are returned`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ topSitesProvider = topSitesProvider,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+ val providedSite = TopSite.Provided(
+ id = 3,
+ title = "Mozilla",
+ url = "https://mozilla.com",
+ clickUrl = "https://mozilla.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite,
+ ),
+ )
+ whenever(topSitesProvider.getTopSites()).thenReturn(listOf(providedSite))
+
+ val frecentSite1 = TopFrecentSiteInfo("https://getpocket.com", "Pocket")
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(listOf(frecentSite1))
+
+ var topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 0,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ assertTrue(topSites.isEmpty())
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 1,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ assertEquals(1, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 2,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ assertEquals(2, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 3,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ assertEquals(3, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite, topSites[2])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ assertEquals(4, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite, topSites[2])
+ assertEquals(frecentSite1.toTopSite(), topSites[3])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `getTopSites returns pinned and frecent sites when frecencyConfig is specified`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite,
+ ),
+ )
+ whenever(pinnedSitesStorage.getPinnedSitesCount()).thenReturn(2)
+
+ val frecentSite1 = TopFrecentSiteInfo("https://mozilla.com", "Mozilla")
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(listOf(frecentSite1))
+
+ var topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 0,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ )
+
+ assertTrue(topSites.isEmpty())
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 1,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ )
+
+ assertEquals(1, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 2,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ )
+
+ assertEquals(2, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ )
+
+ assertEquals(3, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(frecentSite1.toTopSite(), topSites[2])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ val frecentSite2 = TopFrecentSiteInfo("https://example.com", "Example")
+ val frecentSite3 = TopFrecentSiteInfo("https://getpocket.com", "Pocket")
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(
+ listOf(
+ frecentSite1,
+ frecentSite2,
+ frecentSite3,
+ ),
+ )
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ )
+
+ assertEquals(5, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(frecentSite1.toTopSite(), topSites[2])
+ assertEquals(frecentSite2.toTopSite(), topSites[3])
+ assertEquals(frecentSite3.toTopSite(), topSites[4])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ val frecentSite4 = TopFrecentSiteInfo("https://example2.com", "Example2")
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(
+ listOf(
+ frecentSite1,
+ frecentSite2,
+ frecentSite3,
+ frecentSite4,
+ ),
+ )
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ )
+
+ assertEquals(5, topSites.size)
+ assertEquals(defaultSite, topSites[0])
+ assertEquals(pinnedSite, topSites[1])
+ assertEquals(frecentSite1.toTopSite(), topSites[2])
+ assertEquals(frecentSite2.toTopSite(), topSites[3])
+ assertEquals(frecentSite3.toTopSite(), topSites[4])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `getTopSites filters out frecent sites that already exist in pinned sites`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSiteFirefox = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite1 = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+ val pinnedSite2 = TopSite.Pinned(
+ id = 3,
+ title = "Example",
+ url = "https://example.com",
+ createdAt = 3,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSiteFirefox,
+ pinnedSite1,
+ pinnedSite2,
+ ),
+ )
+ whenever(pinnedSitesStorage.getPinnedSitesCount()).thenReturn(3)
+
+ val frecentSiteWithNoTitle = TopFrecentSiteInfo("https://mozilla.com", "")
+ val frecentSiteFirefox = TopFrecentSiteInfo("https://firefox.com", "Firefox")
+ val frecentSite1 = TopFrecentSiteInfo("https://getpocket.com", "Pocket")
+ val frecentSite2 = TopFrecentSiteInfo("https://www.example.com", "Example")
+
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(
+ listOf(
+ frecentSiteWithNoTitle,
+ frecentSiteFirefox,
+ frecentSite1,
+ frecentSite2,
+ ),
+ )
+
+ val topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ )
+
+ verify(historyStorage).getTopFrecentSites(5, frecencyThreshold = FrecencyThresholdOption.NONE)
+
+ assertEquals(5, topSites.size)
+ assertEquals(defaultSiteFirefox, topSites[0])
+ assertEquals(pinnedSite1, topSites[1])
+ assertEquals(pinnedSite2, topSites[2])
+ assertEquals(frecentSiteWithNoTitle.toTopSite(), topSites[3])
+ assertEquals(frecentSite1.toTopSite(), topSites[4])
+ assertEquals("mozilla.com", frecentSiteWithNoTitle.toTopSite().title)
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `GIVEN providerFilter is set WHEN getTopSites is called THEN the provided top sites are filtered`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ topSitesProvider = topSitesProvider,
+ coroutineContext = coroutineContext,
+ )
+
+ val filteredUrl = "https://test.com"
+
+ val providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ providerFilter = { topSite -> topSite.url != filteredUrl },
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Test",
+ url = filteredUrl,
+ createdAt = 2,
+ )
+ val providedSite = TopSite.Provided(
+ id = 3,
+ title = "Mozilla",
+ url = "https://mozilla.com",
+ clickUrl = "https://mozilla.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+ val providedFilteredSite = TopSite.Provided(
+ id = 3,
+ title = "Filtered",
+ url = filteredUrl,
+ clickUrl = "https://test.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite,
+ ),
+ )
+ whenever(topSitesProvider.getTopSites()).thenReturn(listOf(providedSite, providedFilteredSite))
+
+ val frecentSite1 = TopFrecentSiteInfo("https://getpocket.com", "Pocket")
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(listOf(frecentSite1))
+
+ var topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 3,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = providerConfig,
+ )
+
+ assertEquals(3, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite, topSites[2])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 4,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = providerConfig,
+ )
+
+ assertEquals(4, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite, topSites[2])
+ assertEquals(frecentSite1.toTopSite(), topSites[3])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `GIVEN frecent top sites exist as a pinned or provided site WHEN top sites are retrieved THEN filters out frecent sites that already exist in pinned or provided sites`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ topSitesProvider = topSitesProvider,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSiteFirefox = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite1 = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+ val pinnedSite2 = TopSite.Pinned(
+ id = 3,
+ title = "Example",
+ url = "https://example.com",
+ createdAt = 3,
+ )
+ val providedSite = TopSite.Provided(
+ id = 3,
+ title = "Firefox",
+ url = "https://getfirefox.com",
+ clickUrl = "https://getfirefox.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSiteFirefox,
+ pinnedSite1,
+ pinnedSite2,
+ ),
+ )
+ whenever(pinnedSitesStorage.getPinnedSitesCount()).thenReturn(3)
+ whenever(topSitesProvider.getTopSites()).thenReturn(listOf(providedSite))
+
+ val frecentSiteWithNoTitle = TopFrecentSiteInfo("https://mozilla.com", "")
+ val frecentSiteFirefox = TopFrecentSiteInfo("https://firefox.com", "Firefox")
+ val frecentSite1 = TopFrecentSiteInfo("https://getpocket.com", "Pocket")
+ val frecentSite2 = TopFrecentSiteInfo("https://www.example.com", "Example")
+ val frecentSite3 = TopFrecentSiteInfo("https://www.getfirefox.com", "Firefox")
+
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(
+ listOf(
+ frecentSiteWithNoTitle,
+ frecentSiteFirefox,
+ frecentSite1,
+ frecentSite2,
+ frecentSite3,
+ ),
+ )
+
+ val topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 10,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ verify(historyStorage).getTopFrecentSites(10, frecencyThreshold = FrecencyThresholdOption.NONE)
+
+ assertEquals(6, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSiteFirefox, topSites[1])
+ assertEquals(pinnedSite1, topSites[2])
+ assertEquals(pinnedSite2, topSites[3])
+ assertEquals(frecentSiteWithNoTitle.toTopSite(), topSites[4])
+ assertEquals(frecentSite1.toTopSite(), topSites[5])
+ assertEquals("mozilla.com", frecentSiteWithNoTitle.toTopSite().title)
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `GIVEN frecencyFilter is set WHEN getTopSites is called THEN the frecent top sites are filtered`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ topSitesProvider = topSitesProvider,
+ coroutineContext = coroutineContext,
+ )
+
+ val filterMethod: ((TopSite) -> Boolean) = { topSite ->
+ val uri = Uri.parse(topSite.url)
+ if (!uri.queryParameterNames.contains("key")) {
+ true
+ } else {
+ uri.getQueryParameter("key") != "value"
+ }
+ }
+
+ val filteredUrl = "https://test.com/?key=value"
+
+ val frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ frecencyFilter = filterMethod,
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Test",
+ url = "https://test.com",
+ createdAt = 2,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite,
+ ),
+ )
+
+ val providedFilteredSite = TopSite.Provided(
+ id = 3,
+ title = "Filtered",
+ url = "https://test.com",
+ clickUrl = "https://test.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+
+ whenever(topSitesProvider.getTopSites()).thenReturn(
+ listOf(
+ providedFilteredSite,
+ ),
+ )
+
+ val frecentSite = TopFrecentSiteInfo("https://getpocket.com", "Pocket")
+
+ val frecentFilteredSite = TopFrecentSiteInfo(filteredUrl, "testSearch")
+
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(
+ listOf(
+ frecentSite,
+ frecentFilteredSite,
+ ),
+ )
+
+ var topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 4,
+ frecencyConfig = frecencyConfig,
+ providerConfig = TopSitesProviderConfig(showProviderTopSites = true),
+ )
+
+ assertEquals(4, topSites.size)
+ assertTrue(topSites.contains(frecentSite.toTopSite()))
+ assertTrue(topSites.contains(providedFilteredSite))
+ assertTrue(topSites.contains(defaultSite))
+ assertTrue(topSites.contains(pinnedSite))
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 5,
+ frecencyConfig = frecencyConfig,
+ providerConfig = TopSitesProviderConfig(showProviderTopSites = true),
+ )
+
+ assertEquals(4, topSites.size)
+ assertTrue(topSites.contains(frecentSite.toTopSite()))
+ assertTrue(topSites.contains(providedFilteredSite))
+ assertTrue(topSites.contains(defaultSite))
+ assertTrue(topSites.contains(pinnedSite))
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `GIVEN frecent top sites host exist as a provided site WHEN top sites are retrieved THEN filters out frecent sites with host that already exist in provided sites`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ topSitesProvider = topSitesProvider,
+ defaultTopSites = listOf(),
+ coroutineContext = coroutineContext,
+ )
+
+ val defaultSiteFirefox = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+ val pinnedSite1 = TopSite.Pinned(
+ id = 2,
+ title = "Google",
+ url = "https://google.com",
+ createdAt = 2,
+ )
+ val providedSite1 = TopSite.Provided(
+ id = 3,
+ title = "Amazon",
+ url = "https://www.amazon.com/?tag=sponsored-shortcut",
+ clickUrl = "https://www.amazon.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3,
+ )
+ val providedSite2 = TopSite.Provided(
+ id = 4,
+ title = "UnderArmour",
+ url = "https://www.underarmour.com/?tag=sponsored-shortcut",
+ clickUrl = "https://www.underarmour.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 4,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSiteFirefox,
+ pinnedSite1,
+ ),
+ )
+ whenever(pinnedSitesStorage.getPinnedSitesCount()).thenReturn(2)
+ whenever(topSitesProvider.getTopSites()).thenReturn(
+ listOf(
+ providedSite1,
+ providedSite2,
+ ),
+ )
+
+ val frecentSite1 = TopFrecentSiteInfo("https://www.amazon.com", "Amazon")
+ val frecentSite2 = TopFrecentSiteInfo("https://www.amazon.com/Wireless-Charging-Station-Charger-AirPods/dp/B09KTY5GM7?pf_rd_r=NCJV8SPRQ2K43XM6WWKS&pf_rd_p=7b590888-dba4-4742-b2f2-7b20b1700e00&pd_rd_r=4fbaf1df-96be-470a-9811-0bc2aa8b415f&pd_rd_w=Viqqz&pd_rd_wg=9Emfa", "Amazon")
+ val frecentSite3 = TopFrecentSiteInfo("https://www.underarmour.com", "UnderArmour")
+ val frecentSite4 = TopFrecentSiteInfo("https://www.underarmour.com/en-us/p/curry_brand_shoes_and_gear/mens_curry_sour_then_sweet_crewneck/195253758836.html", "UnderArmour")
+ val frecentSite5 = TopFrecentSiteInfo("https://www.example.com", "Example")
+ val frecentSite6 = TopFrecentSiteInfo("https://www.getfirefox.com", "Firefox")
+
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(
+ listOf(
+ frecentSite1,
+ frecentSite2,
+ frecentSite3,
+ frecentSite4,
+ frecentSite5,
+ frecentSite6,
+ ),
+ )
+
+ val topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 10,
+ frecencyConfig = TopSitesFrecencyConfig(
+ frecencyTresholdOption = FrecencyThresholdOption.NONE,
+ ),
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ ),
+ )
+
+ verify(historyStorage).getTopFrecentSites(10, frecencyThreshold = FrecencyThresholdOption.NONE)
+
+ assertEquals(6, topSites.size)
+ assertEquals(providedSite1, topSites[0])
+ assertEquals(providedSite2, topSites[1])
+ assertFalse(topSites.contains(frecentSite1.toTopSite()))
+ assertFalse(topSites.contains(frecentSite2.toTopSite()))
+ assertFalse(topSites.contains(frecentSite3.toTopSite()))
+ assertFalse(topSites.contains(frecentSite4.toTopSite()))
+ assertEquals(defaultSiteFirefox, topSites[2])
+ assertEquals(pinnedSite1, topSites[3])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/PinnedSitesStorageTest.kt b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/PinnedSitesStorageTest.kt
new file mode 100644
index 0000000000..69c30de8d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/PinnedSitesStorageTest.kt
@@ -0,0 +1,135 @@
+/* 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.top.sites
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.feature.top.sites.db.PinnedSiteDao
+import mozilla.components.feature.top.sites.db.PinnedSiteEntity
+import mozilla.components.feature.top.sites.db.TopSiteDatabase
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi // for runTest
+class PinnedSitesStorageTest {
+
+ @Test
+ fun addAllDefaultSites() = runTest {
+ val storage = PinnedSiteStorage(mock())
+ val dao = mockDao(storage)
+
+ storage.currentTimeMillis = { 42 }
+
+ storage.addAllPinnedSites(
+ listOf(
+ Pair("Mozilla", "https://www.mozilla.org"),
+ Pair("Firefox", "https://www.firefox.com"),
+ Pair("Wikipedia", "https://www.wikipedia.com"),
+ Pair("Pocket", "https://www.getpocket.com"),
+ ),
+ isDefault = true,
+ )
+
+ verify(dao).insertAllPinnedSites(
+ listOf(
+ PinnedSiteEntity(title = "Mozilla", url = "https://www.mozilla.org", isDefault = true, createdAt = 42),
+ PinnedSiteEntity(title = "Firefox", url = "https://www.firefox.com", isDefault = true, createdAt = 42),
+ PinnedSiteEntity(title = "Wikipedia", url = "https://www.wikipedia.com", isDefault = true, createdAt = 42),
+ PinnedSiteEntity(title = "Pocket", url = "https://www.getpocket.com", isDefault = true, createdAt = 42),
+ ),
+ )
+
+ Unit
+ }
+
+ @Test
+ fun addPinnedSite() = runTest {
+ val storage = PinnedSiteStorage(mock())
+ val dao = mockDao(storage)
+
+ storage.currentTimeMillis = { 3 }
+
+ storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
+ storage.addPinnedSite("Firefox", "https://www.firefox.com", isDefault = true)
+
+ // PinnedSiteDao.insertPinnedSite is actually called with "id = null", but due to an
+ // extraneous assignment ("entity.id = ") in PinnedSiteStorage.addPinnedSite we can for now
+ // only verify the call with "id = 0". See issue #9708.
+ verify(dao).insertPinnedSite(PinnedSiteEntity(id = 0, title = "Mozilla", url = "https://www.mozilla.org", isDefault = false, createdAt = 3))
+ verify(dao).insertPinnedSite(PinnedSiteEntity(id = 0, title = "Firefox", url = "https://www.firefox.com", isDefault = true, createdAt = 3))
+
+ Unit
+ }
+
+ @Test
+ fun removePinnedSite() = runTest {
+ val storage = PinnedSiteStorage(mock())
+ val dao = mockDao(storage)
+
+ storage.removePinnedSite(TopSite.Pinned(1, "Mozilla", "https://www.mozilla.org", 1))
+ storage.removePinnedSite(TopSite.Default(2, "Firefox", "https://www.firefox.com", 1))
+
+ verify(dao).deletePinnedSite(PinnedSiteEntity(1, "Mozilla", "https://www.mozilla.org", false, 1))
+ verify(dao).deletePinnedSite(PinnedSiteEntity(2, "Firefox", "https://www.firefox.com", true, 1))
+ }
+
+ @Test
+ fun getPinnedSites() = runTest {
+ val storage = PinnedSiteStorage(mock())
+ val dao = mockDao(storage)
+
+ `when`(dao.getPinnedSites()).thenReturn(
+ listOf(
+ PinnedSiteEntity(1, "Mozilla", "https://www.mozilla.org", false, 10),
+ PinnedSiteEntity(2, "Firefox", "https://www.firefox.com", true, 10),
+ ),
+ )
+ `when`(dao.getPinnedSitesCount()).thenReturn(2)
+
+ val topSites = storage.getPinnedSites()
+ val topSitesCount = storage.getPinnedSitesCount()
+
+ assertNotNull(topSites)
+ assertEquals(2, topSites.size)
+ assertEquals(2, topSitesCount)
+
+ with(topSites[0]) {
+ assertEquals(1L, id)
+ assertEquals("Mozilla", title)
+ assertEquals("https://www.mozilla.org", url)
+ assertEquals(10L, createdAt)
+ }
+
+ with(topSites[1]) {
+ assertEquals(2L, id)
+ assertEquals("Firefox", title)
+ assertEquals("https://www.firefox.com", url)
+ assertEquals(10L, createdAt)
+ }
+ }
+
+ @Test
+ fun updatePinnedSite() = runTest {
+ val storage = PinnedSiteStorage(mock())
+ val dao = mockDao(storage)
+
+ val site = TopSite.Pinned(1, "Mozilla", "https://www.mozilla.org", 1)
+ storage.updatePinnedSite(site, "Mozilla (IT)", "https://www.mozilla.org/it")
+
+ verify(dao).updatePinnedSite(PinnedSiteEntity(1, "Mozilla (IT)", "https://www.mozilla.org/it", false, 1))
+ }
+
+ private fun mockDao(storage: PinnedSiteStorage): PinnedSiteDao {
+ val db = mock<TopSiteDatabase>()
+ storage.database = lazy { db }
+ val dao = mock<PinnedSiteDao>()
+ `when`(db.pinnedSiteDao()).thenReturn(dao)
+ return dao
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesFeatureTest.kt b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesFeatureTest.kt
new file mode 100644
index 0000000000..21d7274e7c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesFeatureTest.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.top.sites
+
+import mozilla.components.feature.top.sites.presenter.TopSitesPresenter
+import mozilla.components.feature.top.sites.view.TopSitesView
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class TopSitesFeatureTest {
+
+ private val view: TopSitesView = mock()
+ private val storage: TopSitesStorage = mock()
+ private val presenter: TopSitesPresenter = mock()
+ private val config: () -> TopSitesConfig = mock()
+ private val feature: TopSitesFeature = TopSitesFeature(view, storage, config, presenter)
+
+ @Test
+ fun start() {
+ feature.start()
+
+ verify(presenter).start()
+ }
+
+ @Test
+ fun stop() {
+ feature.stop()
+
+ verify(presenter).stop()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesUseCasesTest.kt b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesUseCasesTest.kt
new file mode 100644
index 0000000000..6c100cdb57
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesUseCasesTest.kt
@@ -0,0 +1,65 @@
+/* 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.top.sites
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi // for runTest
+
+class TopSitesUseCasesTest {
+
+ @Test
+ fun `AddPinnedSiteUseCase`() = runTest {
+ val topSitesStorage: TopSitesStorage = mock()
+ val useCases = TopSitesUseCases(topSitesStorage)
+
+ useCases.addPinnedSites("Mozilla", "https://www.mozilla.org", isDefault = true)
+ verify(topSitesStorage).addTopSite(
+ "Mozilla",
+ "https://www.mozilla.org",
+ isDefault = true,
+ )
+ }
+
+ @Test
+ fun `RemoveTopSiteUseCase`() = runTest {
+ val topSitesStorage: TopSitesStorage = mock()
+ val topSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+
+ val useCases = TopSitesUseCases(topSitesStorage)
+
+ useCases.removeTopSites(topSite)
+
+ verify(topSitesStorage).removeTopSite(topSite)
+ }
+
+ @Test
+ fun `UpdateTopSiteUseCase`() = runTest {
+ val topSitesStorage: TopSitesStorage = mock()
+ val topSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1,
+ )
+
+ val useCases = TopSitesUseCases(topSitesStorage)
+
+ val title = "New title"
+ val url = "https://www.example.com/new-url"
+ useCases.updateTopSites(topSite, title, url)
+
+ verify(topSitesStorage).updateTopSite(topSite, title, url)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/ext/TopSiteTest.kt b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/ext/TopSiteTest.kt
new file mode 100644
index 0000000000..210c6e0b09
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/ext/TopSiteTest.kt
@@ -0,0 +1,108 @@
+/* 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.top.sites.ext
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.top.sites.TopSite
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TopSiteTest {
+
+ @Test
+ fun hasUrl() {
+ val topSites = listOf(
+ TopSite.Frecent(
+ id = 1,
+ title = "Mozilla",
+ url = "https://mozilla.com",
+ createdAt = 1,
+ ),
+ )
+
+ assertTrue(topSites.hasUrl("https://mozilla.com"))
+ assertTrue(topSites.hasUrl("https://www.mozilla.com"))
+ assertTrue(topSites.hasUrl("http://mozilla.com"))
+ assertTrue(topSites.hasUrl("http://www.mozilla.com"))
+ assertTrue(topSites.hasUrl("mozilla.com"))
+ assertTrue(topSites.hasUrl("https://mozilla.com/"))
+
+ assertFalse(topSites.hasUrl("https://m.mozilla.com"))
+ assertFalse(topSites.hasUrl("https://mozilla.com/path"))
+ assertFalse(topSites.hasUrl("https://firefox.com"))
+ assertFalse(topSites.hasUrl("https://mozilla.com/path/is/long"))
+ assertFalse(topSites.hasUrl("https://mozilla.com/path#anchor"))
+ }
+
+ @Test
+ fun hasHostOneItem() {
+ val topSites = listOf(
+ TopSite.Frecent(
+ id = 1,
+ title = "Amazon",
+ url = "https://amazon.com/playstation",
+ createdAt = 1,
+ ),
+ )
+
+ assertTrue(topSites.hasHost("https://amazon.com"))
+ assertTrue(topSites.hasHost("https://www.amazon.com"))
+ assertTrue(topSites.hasHost("http://amazon.com"))
+ assertTrue(topSites.hasHost("http://www.amazon.com"))
+ assertTrue(topSites.hasHost("amazon.com"))
+ assertTrue(topSites.hasHost("https://amazon.com/"))
+ assertTrue(topSites.hasHost("HTTPS://AMAZON.COM/"))
+ assertFalse(topSites.hasHost("https://amzn.com/"))
+ assertFalse(topSites.hasHost("https://aws.amazon.com/"))
+ assertFalse(topSites.hasHost("https://youtube.com/"))
+ }
+
+ @Test
+ fun hasHostNoItem() {
+ val topSites = emptyList<TopSite.Frecent>()
+
+ assertFalse(topSites.hasHost("https://amazon.com"))
+ assertFalse(topSites.hasHost("https://www.amazon.com"))
+ assertFalse(topSites.hasHost("http://amazon.com"))
+ assertFalse(topSites.hasHost("http://www.amazon.com"))
+ assertFalse(topSites.hasHost("amazon.com"))
+ assertFalse(topSites.hasHost("https://amazon.com/"))
+ assertFalse(topSites.hasHost("HTTPS://AMAZON.COM/"))
+ assertFalse(topSites.hasHost("https://amzn.com/"))
+ assertFalse(topSites.hasHost("https://aws.amazon.com/"))
+ }
+
+ @Test
+ fun hasHostMultipleItems() {
+ val topSites = listOf(
+ TopSite.Frecent(
+ id = 1,
+ title = "Amazon",
+ url = "https://amazon.com/playstation",
+ createdAt = 1,
+ ),
+ TopSite.Frecent(
+ id = 2,
+ title = "Hotels",
+ url = "https://www.hotels.com/",
+ createdAt = 2,
+ ),
+ TopSite.Frecent(
+ id = 3,
+ title = "eBay",
+ url = "https://www.ebay.com/n/all-categories",
+ createdAt = 3,
+ ),
+ )
+
+ assertTrue(topSites.hasHost("https://amazon.com"))
+ assertTrue(topSites.hasHost("https://hotels.com"))
+ assertTrue(topSites.hasHost("http://ebay.com"))
+ assertFalse(topSites.hasHost("http://google.com"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/fact/TopSitesFactsTest.kt b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/fact/TopSitesFactsTest.kt
new file mode 100644
index 0000000000..a8ab8eded8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/fact/TopSitesFactsTest.kt
@@ -0,0 +1,44 @@
+/* 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.top.sites.fact
+
+import mozilla.components.feature.top.sites.facts.TopSitesFacts
+import mozilla.components.feature.top.sites.facts.emitTopSitesCountFact
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class TopSitesFactsTest {
+
+ @Test
+ fun `Emits facts for current state`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ assertEquals(0, facts.size)
+
+ emitTopSitesCountFact(5)
+
+ assertEquals(1, facts.size)
+ facts[0].apply {
+ assertEquals(Component.FEATURE_TOP_SITES, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(TopSitesFacts.Items.COUNT, item)
+ assertEquals(5, value?.toInt())
+ }
+
+ emitTopSitesCountFact(1)
+
+ assertEquals(2, facts.size)
+ facts[1].apply {
+ assertEquals(Component.FEATURE_TOP_SITES, component)
+ assertEquals(Action.INTERACTION, action)
+ assertEquals(TopSitesFacts.Items.COUNT, item)
+ assertEquals(1, value?.toInt())
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/presenter/DefaultTopSitesPresenterTest.kt b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/presenter/DefaultTopSitesPresenterTest.kt
new file mode 100644
index 0000000000..eb4a14ac24
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/presenter/DefaultTopSitesPresenterTest.kt
@@ -0,0 +1,37 @@
+/* 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.top.sites.presenter
+
+import mozilla.components.feature.top.sites.DefaultTopSitesStorage
+import mozilla.components.feature.top.sites.TopSitesConfig
+import mozilla.components.feature.top.sites.view.TopSitesView
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class DefaultTopSitesPresenterTest {
+
+ private val view: TopSitesView = mock()
+ private val storage: DefaultTopSitesStorage = mock()
+ private val config: () -> TopSitesConfig = mock()
+ private val presenter: DefaultTopSitesPresenter =
+ spy(DefaultTopSitesPresenter(view, storage, config))
+
+ @Test
+ fun start() {
+ presenter.start()
+
+ verify(presenter).onStorageUpdated()
+ verify(storage).register(presenter)
+ }
+
+ @Test
+ fun stop() {
+ presenter.stop()
+
+ verify(storage).unregister(presenter)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/top-sites/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/top-sites/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/top-sites/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/top-sites/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/top-sites/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/top-sites/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/webauthn/README.md b/mobile/android/android-components/components/feature/webauthn/README.md
new file mode 100644
index 0000000000..ba8f1c8c66
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webauthn/README.md
@@ -0,0 +1,59 @@
+# [Android Components](../../../README.md) > Feature > WebAuthn
+
+A feature that provides WebAuthn functionality for supported engines.
+
+## Usage
+
+Add the feature to the Activity/Fragment:
+
+```kotlin
+val webAuthnFeature = WebAuthnFeature(
+ engine = GeckoEngine,
+ activity = requireActivity()
+)
+```
+
+**Note:** If the feature is on the fragment, ensure that `onActivityResult` calls from the activity are forwarded to the fragment.
+
+Allow the feature to consume the `onActivityResult` data:
+
+```kotlin
+override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ webAuthFeature.onActiviyResult(requestCode, data, resultCode)
+}
+```
+
+As with other features in Android Components, `WebAuthnFeature` implements `LifecycleAwareFeature`, so it's recommended to use `ViewBoundFeatureWrapper` to handle the lifecycle events of the feature:
+
+```kotlin
+private val webAuthnFeature = ViewBoundFeatureWrapper<WebAuthnFeature>()
+
+override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ webAuthnFeature.set(
+ feature = WebAuthnFeature(
+ engine = GeckoEngine,
+ activity = requireActivity()
+ ),
+ owner = this,
+ view = view
+ )
+}
+
+override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ webAuthnFeature.onActivityResult(requestCode, data, resultCode) }
+}
+```
+
+### 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-webauthn:{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/webauthn/build.gradle b/mobile/android/android-components/components/feature/webauthn/build.gradle
new file mode 100644
index 0000000000..519b717dcf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webauthn/build.gradle
@@ -0,0 +1,36 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.webauthn'
+}
+
+dependencies {
+ implementation project(':concept-engine')
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+}
+
+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/webauthn/proguard-rules.pro b/mobile/android/android-components/components/feature/webauthn/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webauthn/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/webauthn/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/webauthn/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webauthn/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/webauthn/src/main/java/mozilla/components/feature/webauthn/WebAuthnFeature.kt b/mobile/android/android-components/components/feature/webauthn/src/main/java/mozilla/components/feature/webauthn/WebAuthnFeature.kt
new file mode 100644
index 0000000000..c896421af5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webauthn/src/main/java/mozilla/components/feature/webauthn/WebAuthnFeature.kt
@@ -0,0 +1,66 @@
+/* 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.webauthn
+
+import android.app.Activity
+import android.content.Intent
+import android.content.IntentSender
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.activity.ActivityDelegate
+import mozilla.components.support.base.feature.ActivityResultHandler
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A feature that implementing the [ActivityDelegate] to adds support
+ * for [WebAuthn](https://tools.ietf.org/html/rfc8809).
+ */
+class WebAuthnFeature(
+ private val engine: Engine,
+ private val activity: Activity,
+ private val exitFullScreen: (String?) -> Unit,
+ private val currentTab: () -> String?,
+) : LifecycleAwareFeature, ActivityResultHandler, ActivityDelegate {
+ private val logger = Logger("WebAuthnFeature")
+ private var requestCodeCounter = ACTIVITY_REQUEST_CODE
+ private var callbackRef: ((Intent?) -> Unit)? = null
+
+ override fun start() {
+ engine.registerActivityDelegate(this)
+ }
+
+ override fun stop() {
+ engine.unregisterActivityDelegate()
+ }
+
+ override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
+ logger.info(
+ "Received activity result with " +
+ "code: $requestCode " +
+ "and original request code: $requestCodeCounter",
+ )
+
+ if (requestCode != requestCodeCounter) {
+ return false
+ }
+
+ requestCodeCounter++
+
+ callbackRef?.invoke(data)
+
+ return true
+ }
+
+ override fun startIntentSenderForResult(intent: IntentSender, onResult: (Intent?) -> Unit) {
+ logger.info("Received activity delegate request with code: $requestCodeCounter")
+ exitFullScreen(currentTab())
+ activity.startIntentSenderForResult(intent, requestCodeCounter, null, 0, 0, 0)
+ callbackRef = onResult
+ }
+
+ companion object {
+ const val ACTIVITY_REQUEST_CODE = 10
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webauthn/src/test/java/mozilla/components/feature/webauthn/WebAuthnFeatureTest.kt b/mobile/android/android-components/components/feature/webauthn/src/test/java/mozilla/components/feature/webauthn/WebAuthnFeatureTest.kt
new file mode 100644
index 0000000000..1714d44c17
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webauthn/src/test/java/mozilla/components/feature/webauthn/WebAuthnFeatureTest.kt
@@ -0,0 +1,92 @@
+/* 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.webauthn
+
+import android.app.Activity
+import android.content.Intent
+import android.content.IntentSender
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.webauthn.WebAuthnFeature.Companion.ACTIVITY_REQUEST_CODE
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.nullable
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.verify
+
+class WebAuthnFeatureTest {
+ private lateinit var engine: Engine
+ private lateinit var activity: Activity
+ private val exitFullScreen: (String?) -> Unit = { _ -> exitFullScreenUseCaseCalled = true }
+ private var exitFullScreenUseCaseCalled = false
+
+ @Before
+ fun setup() {
+ engine = mock()
+ activity = mock()
+ }
+
+ @Test
+ fun `feature registers itself on start`() {
+ val feature = webAuthnFeature()
+
+ feature.start()
+
+ verify(engine).registerActivityDelegate(feature)
+ }
+
+ @Test
+ fun `feature unregisters itself on stop`() {
+ val feature = webAuthnFeature()
+
+ feature.stop()
+
+ verify(engine).unregisterActivityDelegate()
+ }
+
+ @Test
+ fun `activity delegate starts intent sender`() {
+ val feature = webAuthnFeature()
+ val callback: ((Intent?) -> Unit) = { }
+ val intentSender: IntentSender = mock()
+
+ feature.startIntentSenderForResult(intentSender, callback)
+
+ verify(activity).startIntentSenderForResult(eq(intentSender), anyInt(), nullable(), eq(0), eq(0), eq(0))
+ }
+
+ @Test
+ fun `callback is invoked`() {
+ val feature = webAuthnFeature()
+ var callbackInvoked = false
+ val callback: ((Intent?) -> Unit) = { callbackInvoked = true }
+ val intentSender: IntentSender = mock()
+
+ feature.onActivityResult(ACTIVITY_REQUEST_CODE, Intent(), 0)
+
+ assertFalse(callbackInvoked)
+
+ feature.startIntentSenderForResult(intentSender, callback)
+ feature.onActivityResult(ACTIVITY_REQUEST_CODE + 1, Intent(), 0)
+
+ assertTrue(callbackInvoked)
+ }
+
+ @Test
+ fun `feature won't process results with the wrong request code`() {
+ val feature = webAuthnFeature()
+
+ val result = feature.onActivityResult(ACTIVITY_REQUEST_CODE - 5, Intent(), 0)
+
+ assertFalse(result)
+ }
+
+ private fun webAuthnFeature(): WebAuthnFeature {
+ return WebAuthnFeature(engine, activity, { exitFullScreen("") }) { "" }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webauthn/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/webauthn/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/webauthn/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/webauthn/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/webauthn/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webauthn/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/README.md b/mobile/android/android-components/components/feature/webcompat-reporter/README.md
new file mode 100644
index 0000000000..88ad600f91
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/README.md
@@ -0,0 +1,39 @@
+# [Android Components](../../../README.md) > Feature > WebCompat Reporter
+
+A feature that enables users to report site issues to Mozilla's Web Compatibility team for further diagnosis.
+
+## Usage
+
+### 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-webcompat-reporter:{latest-version}"
+```
+
+### Install WebExtension
+
+To install the WebExtension, run
+
+```kotlin
+WebCompatReporterFeature.install(engine)
+```
+
+Please make sure to only run this once, as the feature itself does not check if the extension is already installed.
+
+### Providing the browser-XXX label for reports
+
+The `install` function has an optional second parameter, `productName`. This allows reports to be labelled using the correct broswer-XXX label on webcompat.com. For example,
+
+```
+WebCompatReporterFeature.install(engine, "fenix")
+```
+
+would add the `browser-fenix` label to the report. Note that simply inventing new values here does not work, as each product name has to be safelisted by the WebCompat team on webcompat.com, so [please get in touch](https://wiki.mozilla.org/Compatibility#Core_Team) when you need to add a new product name.
+
+## 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/webcompat-reporter/build.gradle b/mobile/android/android-components/components/feature/webcompat-reporter/build.gradle
new file mode 100644
index 0000000000..e887cee571
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/build.gradle
@@ -0,0 +1,42 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.webcompat.reporter'
+}
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':support-webextensions')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/webcompat-reporter/proguard-rules.pro b/mobile/android/android-components/components/feature/webcompat-reporter/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/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/webcompat-reporter/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..ed898bd85a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/.eslintrc.js b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/.eslintrc.js
new file mode 100644
index 0000000000..e26bd6da3f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/.eslintrc.js
@@ -0,0 +1,76 @@
+/* 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/. */
+
+"use strict";
+
+module.exports = {
+ rules: {
+ // Rules from the mozilla plugin
+ "mozilla/balanced-listeners": "error",
+ "mozilla/no-aArgs": "error",
+ "mozilla/var-only-at-top-level": "error",
+
+ "valid-jsdoc": [
+ "error",
+ {
+ prefer: {
+ return: "returns",
+ },
+ preferType: {
+ Boolean: "boolean",
+ Number: "number",
+ String: "string",
+ bool: "boolean",
+ },
+ requireParamDescription: false,
+ requireReturn: false,
+ requireReturnDescription: false,
+ },
+ ],
+
+ // No expressions where a statement is expected
+ "no-unused-expressions": "error",
+
+ // No declaring variables that are never used
+ "no-unused-vars": "error",
+
+ // Disallow using variables outside the blocks they are defined (especially
+ // since only let and const are used, see "no-var").
+ "block-scoped-var": "error",
+
+ // Warn about cyclomatic complexity in functions.
+ complexity: ["error", { max: 26 }],
+
+ // Maximum depth callbacks can be nested.
+ "max-nested-callbacks": ["error", 4],
+
+ // Allow the console API aside from console.log.
+ "no-console": ["error", { allow: ["error", "info", "trace", "warn"] }],
+
+ // Disallow fallthrough of case statements, except if there is a comment.
+ "no-fallthrough": "error",
+
+ // Disallow use of multiline strings (use template strings instead).
+ "no-multi-str": "error",
+
+ // Disallow usage of __proto__ property.
+ "no-proto": "error",
+
+ // Disallow use of assignment in return statement. It is preferable for a
+ // single line of code to have only one easily predictable effect.
+ "no-return-assign": "error",
+
+ // Require use of the second argument for parseInt().
+ radix: "error",
+
+ // Require "use strict" to be defined globally in the script.
+ strict: ["error", "global"],
+
+ // Disallow Yoda conditions (where literal value comes first).
+ yoda: "error",
+
+ // Disallow function or variable declarations in nested blocks
+ "no-inner-declarations": "error",
+ },
+};
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/background.js b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/background.js
new file mode 100644
index 0000000000..8c8234e52f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/background.js
@@ -0,0 +1,93 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser */
+
+const desktopReporterConfig = {
+ src: "desktop-reporter",
+ utm_campaign: "report-site-issue-button",
+ utm_source: "desktop-reporter",
+};
+
+const androidReporterConfig = {
+ src: "android-components-reporter",
+ utm_campaign: "report-site-issue-button",
+ utm_source: "android-components-reporter",
+};
+
+const getReporterConfig = (() => {
+ let promise;
+ return async () => {
+ promise ??= new Promise(resolve => {
+ browser.permissions
+ .contains({ permissions: ["nativeMessaging"] })
+ .then(needProductName => {
+ if (needProductName) {
+ const port = browser.runtime.connectNative(
+ "mozacWebcompatReporter"
+ );
+ port.onMessage.addListener(message => {
+ if ("productName" in message) {
+ androidReporterConfig.productName = message.productName;
+ resolve(androidReporterConfig);
+
+ // For now, setting the productName is the only use for this port, and that's only happening
+ // once after startup, so let's disconnect the port when we're done.
+ port.disconnect();
+ }
+ });
+ } else {
+ resolve(desktopReporterConfig);
+ }
+ });
+ });
+ return promise;
+ };
+})();
+
+async function loadTab(url) {
+ const newTab = await browser.tabs.create({ url });
+ return new Promise(resolve => {
+ const listener = (tabId, changeInfo, tab) => {
+ if (
+ tabId == newTab.id &&
+ tab.url !== "about:blank" &&
+ changeInfo.status == "complete"
+ ) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve(newTab);
+ }
+ };
+ browser.tabs.onUpdated.addListener(listener);
+ });
+}
+
+async function captureAndSendReport(tab) {
+ const { id, url } = tab;
+ try {
+ const { endpointUrl, webcompatInfo } =
+ await browser.tabExtras.getWebcompatInfo(id);
+ const reporterConfig = await getReporterConfig();
+ const dataToSend = {
+ endpointUrl,
+ reportUrl: url,
+ reporterConfig,
+ webcompatInfo,
+ };
+ const newTab = await loadTab(endpointUrl);
+ browser.tabExtras.sendWebcompatInfo(newTab.id, dataToSend);
+ } catch (err) {
+ console.error("WebCompat Reporter: unexpected error", err);
+ }
+}
+
+if ("helpMenu" in browser) {
+ // desktop
+ browser.helpMenu.onHelpMenuCommand.addListener(captureAndSendReport);
+} else {
+ // Android
+ browser.pageAction.onClicked.addListener(captureAndSendReport);
+}
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/experimentalAPIs/tabExtras.js b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/experimentalAPIs/tabExtras.js
new file mode 100644
index 0000000000..48ef117630
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/experimentalAPIs/tabExtras.js
@@ -0,0 +1,53 @@
+/* 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/. */
+
+"use strict";
+
+/* global ExtensionAPI */
+
+const lazy = {};
+
+const DEFAULT_NEW_REPORT_ENDPOINT = "https://webcompat.com/issues/new";
+const NEW_REPORT_ENDPOINT_PREF =
+ "ui.new-webcompat-reporter.new-report-endpoint";
+
+this.tabExtras = class extends ExtensionAPI {
+ getAPI(context) {
+ const { tabManager } = context.extension;
+ const queryReportBrokenSiteActor = (tabId, name, params) => {
+ const { browser } = tabManager.get(tabId);
+ const windowGlobal = browser.browsingContext.currentWindowGlobal;
+ if (!windowGlobal) {
+ return null;
+ }
+ return windowGlobal.getActor("ReportBrokenSite").sendQuery(name, params);
+ };
+ return {
+ tabExtras: {
+ async getWebcompatInfo(tabId) {
+ const endpointUrl = Services.prefs.getStringPref(
+ NEW_REPORT_ENDPOINT_PREF,
+ DEFAULT_NEW_REPORT_ENDPOINT
+ );
+ const webcompatInfo = await queryReportBrokenSiteActor(
+ tabId,
+ "GetWebCompatInfo"
+ );
+ return {
+ webcompatInfo,
+ endpointUrl,
+ };
+ },
+ async sendWebcompatInfo(tabId, info) {
+ console.error(info);
+ return queryReportBrokenSiteActor(
+ tabId,
+ "SendDataToWebcompatCom",
+ info
+ );
+ },
+ },
+ };
+ }
+};
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/experimentalAPIs/tabExtras.json b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/experimentalAPIs/tabExtras.json
new file mode 100644
index 0000000000..5e0098ab32
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/experimentalAPIs/tabExtras.json
@@ -0,0 +1,37 @@
+[
+ {
+ "namespace": "tabExtras",
+ "description": "experimental tab API extensions",
+ "functions": [
+ {
+ "name": "getWebcompatInfo",
+ "type": "function",
+ "description": "Gets the content blocking status and script log for a given tab",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "tabId",
+ "minimum": 0
+ }
+ ],
+ "async": true
+ },
+ {
+ "name": "sendWebcompatInfo",
+ "type": "function",
+ "description": "Sends the given webcompat info to the given tab",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "tabId",
+ "minimum": 0
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/icons/lightbulb.svg b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/icons/lightbulb.svg
new file mode 100644
index 0000000000..fd7434f02b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/icons/lightbulb.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="M15.3 22H8.7a.8.8 0 0 1 0-1.5h6.6a.8.8 0 0 1 0 1.5zm-1.4-4H10a2 2 0 0 1-2-1.6L7.8 14a6.7 6.7 0 1 1 8.5-10.5 6.7 6.7 0 0 1 0 10.5l-.4 2.4a2 2 0 0 1-2 1.6zm-2-14.5-1 .1A5.2 5.2 0 0 0 8.7 13c.3.2.5.5.5.8l.4 2.4.5.4H14l.5-.4.4-2.4c0-.3.2-.6.5-.8a5.2 5.2 0 0 0 2-4.1A5.3 5.3 0 0 0 12 3.5z"/>
+</svg>
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/manifest.json b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/manifest.json
new file mode 100644
index 0000000000..53051559e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/assets/extensions/webcompat-reporter/manifest.json
@@ -0,0 +1,50 @@
+{
+ "manifest_version": 2,
+ "name": "Mozilla Android Components - WebCompat Reporter",
+ "description": "Report site compatibility issues on webcompat.com",
+ "author": "Thomas Wisniewski <twisniewski@mozilla.com>",
+ "version": "2.1.0",
+ "homepage_url": "https://github.com/mozilla/webcompat-reporter",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "webcompat-reporter@mozilla.org"
+ }
+ },
+ "experiment_apis": {
+ "tabExtras": {
+ "schema": "experimentalAPIs/tabExtras.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/tabExtras.js",
+ "paths": [["tabExtras"]]
+ }
+ }
+ },
+ "icons": {
+ "16": "icons/lightbulb.svg",
+ "32": "icons/lightbulb.svg",
+ "48": "icons/lightbulb.svg",
+ "96": "icons/lightbulb.svg",
+ "128": "icons/lightbulb.svg"
+ },
+ "permissions": [
+ "geckoViewAddons",
+ "nativeMessaging",
+ "tabs",
+ "<all_urls>"
+ ],
+ "background": {
+ "persistent": false,
+ "type": "module",
+ "scripts": [
+ "background.js"
+ ]
+ },
+ "page_action": {
+ "browser_style": true,
+ "default_icon": "icons/lightbulb.svg",
+ "default_title": "Report Site Issue…",
+ "pinned": false,
+ "show_matches": ["http://*/*", "https://*/*"]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/main/java/mozilla/components/feature/webcompat/reporter/WebCompatReporterFeature.kt b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/java/mozilla/components/feature/webcompat/reporter/WebCompatReporterFeature.kt
new file mode 100644
index 0000000000..99db095dd2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/main/java/mozilla/components/feature/webcompat/reporter/WebCompatReporterFeature.kt
@@ -0,0 +1,65 @@
+/* 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.webcompat.reporter
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.concept.engine.webextension.WebExtensionRuntime
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.webextensions.WebExtensionController
+import org.json.JSONObject
+
+/**
+ * A feature that enables users to report site issues to Mozilla's Web Compatibility team for
+ * further diagnosis.
+ */
+object WebCompatReporterFeature {
+ private val logger = Logger("mozac-webcompat-reporter")
+
+ const val WEBCOMPAT_REPORTER_EXTENSION_ID = "webcompat-reporter@mozilla.org"
+ internal const val WEBCOMPAT_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/webcompat-reporter/"
+ internal const val WEBCOMPAT_REPORTER_MESSAGING_ID = "mozacWebcompatReporter"
+
+ @VisibleForTesting
+ // This is an internal var to make it mutable for unit testing purposes only
+ internal var extensionController = WebExtensionController(
+ WEBCOMPAT_REPORTER_EXTENSION_ID,
+ WEBCOMPAT_REPORTER_EXTENSION_URL,
+ WEBCOMPAT_REPORTER_MESSAGING_ID,
+ )
+
+ private class WebcompatReporterBackgroundMessageHandler(
+ // This information will be provided as a browser-XXX label to the reporting backend, allowing
+ // us to differentiate different android-components based products.
+ private val productName: String,
+ ) : MessageHandler {
+ override fun onPortConnected(port: Port) {
+ port.postMessage(JSONObject().put("productName", productName))
+ }
+ }
+
+ /**
+ * Installs the web extension in the runtime through the WebExtensionRuntime install method
+ *
+ * @param runtime a WebExtensionRuntime.
+ * @param productName a custom product name used to automatically label reports. Defaults to
+ * "android-components".
+ */
+ fun install(runtime: WebExtensionRuntime, productName: String = "android-components") {
+ extensionController.registerBackgroundMessageHandler(
+ WebcompatReporterBackgroundMessageHandler(productName),
+ )
+ extensionController.install(
+ runtime,
+ onSuccess = {
+ logger.debug("Installed WebCompat Reporter webextension: ${it.id}")
+ },
+ onError = { throwable ->
+ logger.error("Failed to install WebCompat Reporter webextension: ", throwable)
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/test/java/mozilla/components/feature/webcompat/reporter/WebCompatReporterFeatureTest.kt b/mobile/android/android-components/components/feature/webcompat-reporter/src/test/java/mozilla/components/feature/webcompat/reporter/WebCompatReporterFeatureTest.kt
new file mode 100644
index 0000000000..a79612309c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/test/java/mozilla/components/feature/webcompat/reporter/WebCompatReporterFeatureTest.kt
@@ -0,0 +1,115 @@
+/* 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.webcompat.reporter
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.webextensions.WebExtensionController
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class WebCompatReporterFeatureTest {
+
+ @Before
+ fun setup() {
+ WebExtensionController.installedExtensions.clear()
+ }
+
+ @Test
+ fun `installs the webextension`() {
+ val engine: Engine = mock()
+ val controller: WebExtensionController = mock()
+ installFeatureForTest(engine, controller)
+ }
+
+ @Test
+ fun `install registers the background message handler`() {
+ val engine: Engine = mock()
+ val controller: WebExtensionController = mock()
+ installFeatureForTest(engine, controller)
+
+ verify(controller).registerBackgroundMessageHandler(any(), any())
+ }
+
+ @Test
+ fun `backgroundMessageHandler sends the default productName if unset`() {
+ val engine: Engine = mock()
+ val controller: WebExtensionController = mock()
+ installFeatureForTest(engine, controller)
+
+ val messageHandler = argumentCaptor<MessageHandler>()
+ verify(controller).registerBackgroundMessageHandler(messageHandler.capture(), any())
+
+ val port: Port = mock()
+ val message = argumentCaptor<JSONObject>()
+ messageHandler.value.onPortConnected(port)
+ verify(port).postMessage(message.capture())
+
+ val productNameMessage = JSONObject().put("productName", "android-components")
+ verify(port, times(1)).postMessage(message.capture())
+
+ assertEquals(productNameMessage.toString(), message.value.toString())
+ }
+
+ @Test
+ fun `backgroundMessageHandler sends the correct productName if set`() {
+ val engine: Engine = mock()
+ val controller: WebExtensionController = mock()
+ installFeatureForTest(engine, controller, "test")
+
+ val messageHandler = argumentCaptor<MessageHandler>()
+ verify(controller).registerBackgroundMessageHandler(messageHandler.capture(), any())
+
+ val port: Port = mock()
+ val message = argumentCaptor<JSONObject>()
+ messageHandler.value.onPortConnected(port)
+ verify(port).postMessage(message.capture())
+
+ val productNameMessage = JSONObject().put("productName", "test")
+ verify(port, times(1)).postMessage(message.capture())
+
+ assertEquals(productNameMessage.toString(), message.value.toString())
+ }
+
+ private fun installFeatureForTest(
+ engine: Engine,
+ controller: WebExtensionController,
+ productName: String? = null,
+ ): WebCompatReporterFeature {
+ val reporterFeature = spy(WebCompatReporterFeature)
+ reporterFeature.extensionController = controller
+
+ if (productName == null) {
+ reporterFeature.install(engine)
+ } else {
+ reporterFeature.install(engine, productName)
+ }
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(controller, times(1)).install(
+ eq(engine),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+
+ onSuccess.value.invoke(mock())
+ return reporterFeature
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat-reporter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/webcompat-reporter/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/webcompat-reporter/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/webcompat-reporter/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/webcompat-reporter/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat-reporter/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/webcompat/README.md b/mobile/android/android-components/components/feature/webcompat/README.md
new file mode 100644
index 0000000000..68f80cf4e7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/README.md
@@ -0,0 +1,32 @@
+# [Android Components](../../../README.md) > Feature > WebCompat
+
+Feature to enable website-hotfixing via the Web Compatibility System-Addon. More details are available at:
+
+* [`github.com/mozilla/webcompat-addon`](https://github.com/mozilla/webcompat-addon), where the Web-Extension sources are tracked.
+* [The Mozilla Wiki](https://wiki.mozilla.org/Compatibility/Go_Faster_Addon), where more information about the background of this extensions are available and our processes for authoring site patches are outlined.
+
+## Usage
+
+### 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-webcompat:{latest-version}"
+```
+
+### Install WebExtension
+
+To install the WebExtension, run
+
+```kotlin
+WebCompatFeature.install(engine)
+```
+
+Please make sure to only run this once, as the feature itself does not check if the extension is already installed.
+
+## 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/webcompat/build.gradle b/mobile/android/android-components/components/feature/webcompat/build.gradle
new file mode 100644
index 0000000000..a3694600ee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/build.gradle
@@ -0,0 +1,42 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.webcompat'
+}
+
+dependencies {
+ implementation project(':concept-engine')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-webextensions')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/webcompat/proguard-rules.pro b/mobile/android/android-components/components/feature/webcompat/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/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/webcompat/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/webcompat/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1eccdee26a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/AboutCompat.sys.mjs b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/AboutCompat.sys.mjs
new file mode 100644
index 0000000000..bedcdd668d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/AboutCompat.sys.mjs
@@ -0,0 +1,35 @@
+/* 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/. */
+
+const addonID = "webcompat@mozilla.org";
+const addonPageRelativeURL = "/about-compat/aboutCompat.html";
+
+export function AboutCompat() {
+ this.chromeURL =
+ WebExtensionPolicy.getByID(addonID).getURL(addonPageRelativeURL);
+}
+
+AboutCompat.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]),
+ getURIFlags() {
+ return (
+ Ci.nsIAboutModule.URI_MUST_LOAD_IN_EXTENSION_PROCESS |
+ Ci.nsIAboutModule.IS_SECURE_CHROME_UI
+ );
+ },
+
+ newChannel(aURI, aLoadInfo) {
+ const uri = Services.io.newURI(this.chromeURL);
+ const channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+ channel.originalURI = aURI;
+
+ channel.owner = (
+ Services.scriptSecurityManager.createContentPrincipal ||
+ // Handles fallback to earlier versions.
+ // eslint-disable-next-line mozilla/valid-services-property
+ Services.scriptSecurityManager.createCodebasePrincipal
+ )(uri, aLoadInfo.originAttributes);
+ return channel;
+ },
+};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.css
new file mode 100644
index 0000000000..b51db7f9f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.css
@@ -0,0 +1,187 @@
+/* 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/. */
+
+@media (any-pointer: fine) {
+ :root {
+ font-family: sans-serif;
+ margin: 40px auto;
+ min-width: 30em;
+ max-width: 60em;
+ }
+
+ table {
+ width: 100%;
+ padding-bottom: 2em;
+ border-spacing: 0;
+ }
+
+ td {
+ border-bottom: 1px solid var(--in-content-border-color);
+ }
+
+ td:last-child > button {
+ float: inline-end;
+ }
+}
+
+/* Mobile UI where common.css is not loaded */
+
+@media (any-pointer: coarse), (any-pointer: none) {
+ * {
+ margin: 0;
+ padding: 0;
+ }
+
+ :root {
+ --background-color: #fff;
+ --text-color: #0c0c0d;
+ --border-color: #e1e1e2;
+ --button-background-color: #f5f5f5;
+ --selected-tab-text-color: #0061e0;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --background-color: #292833;
+ --text-color: #f9f9fa;
+ --border-color: rgba(255, 255, 255, 0.15);
+ --button-background-color: rgba(0, 0, 0, 0.15);
+ --selected-tab-text-color: #00ddff;
+ }
+ }
+
+ body {
+ background-color: var(--background-color);
+ color: var(--text-color);
+ font: message-box;
+ font-size: 14px;
+ -moz-text-size-adjust: none;
+ display: grid;
+ grid-template-areas: "a b c" "d d d";
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-template-rows: fit-content(100%) 1fr;
+ }
+
+ .tab[data-l10n-id="label-overrides"] {
+ grid-area: a;
+ }
+
+ .tab[data-l10n-id="label-interventions"] {
+ grid-area: b;
+ }
+
+ .tab[data-l10n-id="label-smartblock"] {
+ grid-area: c;
+ }
+
+ table {
+ grid-area: d;
+ }
+
+ table,
+ tr,
+ p {
+ display: block;
+ }
+
+ table {
+ border-top: 2px solid var(--border-color);
+ margin-top: -2px;
+ width: 100%;
+ z-index: 1;
+ display: none;
+ }
+
+ tr {
+ border-bottom: 1px solid var(--border-color);
+ padding: 0;
+ }
+
+ a {
+ color: inherit;
+ font-size: 94%;
+ }
+
+ .tab {
+ cursor: pointer;
+ z-index: 2;
+ display: inline-block;
+ text-align: left;
+ border-block: 2px solid transparent;
+ font-size: 1em;
+ font-weight: bold;
+ padding: 1em;
+ }
+
+ .tab.active {
+ color: var(--selected-tab-text-color);
+ border-bottom-color: currentColor;
+ margin-bottom: 0;
+ padding-bottom: calc(1em + 2px);
+ }
+
+ .tab.active + table {
+ display: block;
+ }
+
+ td {
+ grid-area: b;
+ padding-left: 1em;
+ }
+
+ td:first-child {
+ grid-area: a;
+ padding-top: 1em;
+ }
+
+ td:last-child {
+ grid-area: c;
+ padding-bottom: 1em;
+ }
+
+ tr {
+ display: grid;
+ grid-template-areas: "a c" "b c";
+ grid-template-columns: 1fr 6.5em;
+ }
+
+ td[colspan="4"] {
+ padding: 1em;
+ font-style: italic;
+ text-align: center;
+ }
+
+ td:not([colspan]):nth-child(1) {
+ font-weight: bold;
+ padding-bottom: 0.25em;
+ }
+
+ td:nth-child(2) {
+ padding-bottom: 1em;
+ }
+
+ td:nth-child(3) {
+ display: flex;
+ padding: 0;
+ }
+
+ button {
+ cursor: pointer;
+ width: 100%;
+ height: 100%;
+ background: var(--button-background-color);
+ color: inherit;
+ inset-inline-end: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ border-inline-start: 1px solid var(--border-color);
+ font-weight: 600;
+ appearance: none;
+ }
+
+ button::-moz-focus-inner {
+ border: 0;
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.html b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.html
new file mode 100644
index 0000000000..d820f20ee2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.html
@@ -0,0 +1,51 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <base />
+
+ <!-- If you change this script tag you must update the hash in the extension's
+ `content_security_policy` 'sha256-MmZkN2QaIHhfRWPZ8TVRjijTn5Ci1iEabtTEWrt9CCo=' -->
+ <script>
+ /* globals browser */ document.head.firstElementChild.href =
+ browser.runtime.getURL("");
+ </script>
+
+ <meta charset="utf-8" />
+ <meta name="color-scheme" content="light dark" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="stylesheet" href="about-compat/aboutCompat.css" />
+ <link
+ rel="stylesheet"
+ media="screen and (pointer:fine), projection"
+ type="text/css"
+ href="chrome://global/skin/in-content/common.css"
+ />
+ <link rel="localization" href="toolkit/about/aboutCompat.ftl" />
+ <title data-l10n-id="text-title"></title>
+ <script src="about-compat/aboutCompat.js"></script>
+ </head>
+ <body>
+ <h2 class="tab active" data-l10n-id="label-overrides"></h2>
+ <table id="overrides">
+ <col />
+ <col />
+ <col />
+ </table>
+ <h2 class="tab" data-l10n-id="label-interventions"></h2>
+ <table id="interventions">
+ <col />
+ <col />
+ <col />
+ </table>
+ <h2 class="tab" data-l10n-id="label-smartblock"></h2>
+ <table id="smartblock" class="shims">
+ <col />
+ <col />
+ <col />
+ </table>
+ </body>
+</html>
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.js
new file mode 100644
index 0000000000..edf467edeb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutCompat.js
@@ -0,0 +1,285 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser */
+
+let availablePatches;
+
+const portToAddon = (function () {
+ let port;
+
+ function connect() {
+ port = browser.runtime.connect({ name: "AboutCompatTab" });
+ port.onMessage.addListener(onMessageFromAddon);
+ port.onDisconnect.addListener(e => {
+ port = undefined;
+ });
+ }
+
+ connect();
+
+ async function send(message) {
+ if (port) {
+ return port.postMessage(message);
+ }
+ return Promise.reject("background script port disconnected");
+ }
+
+ return { send };
+})();
+
+const $ = function (sel) {
+ return document.querySelector(sel);
+};
+
+const DOMContentLoadedPromise = new Promise(resolve => {
+ document.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+});
+
+Promise.all([
+ browser.runtime.sendMessage("getAllInterventions"),
+ DOMContentLoadedPromise,
+]).then(([info]) => {
+ document.body.addEventListener("click", async evt => {
+ const ele = evt.target;
+ if (ele.nodeName === "BUTTON") {
+ const row = ele.closest("[data-id]");
+ if (row) {
+ evt.preventDefault();
+ ele.disabled = true;
+ const id = row.getAttribute("data-id");
+ try {
+ await browser.runtime.sendMessage({ command: "toggle", id });
+ } catch (_) {
+ ele.disabled = false;
+ }
+ }
+ } else if (ele.classList.contains("tab")) {
+ document.querySelectorAll(".tab").forEach(tab => {
+ tab.classList.remove("active");
+ });
+ ele.classList.add("active");
+ }
+ });
+
+ availablePatches = info;
+ redraw();
+});
+
+async function onMessageFromAddon(msg) {
+ const alsoShowHidden = location.hash === "#all";
+
+ await DOMContentLoadedPromise;
+
+ if ("interventionsChanged" in msg) {
+ redrawTable($("#interventions"), msg.interventionsChanged, alsoShowHidden);
+ }
+
+ if ("overridesChanged" in msg) {
+ redrawTable($("#overrides"), msg.overridesChanged, alsoShowHidden);
+ }
+
+ if ("shimsChanged" in msg) {
+ updateShimTables(msg.shimsChanged, alsoShowHidden);
+ }
+
+ const id = msg.toggling || msg.toggled;
+ const button = $(`[data-id="${id}"] button`);
+ if (!button) {
+ return;
+ }
+ const active = msg.active;
+ document.l10n.setAttributes(
+ button,
+ active ? "label-disable" : "label-enable"
+ );
+ button.disabled = !!msg.toggling;
+}
+
+function redraw() {
+ if (!availablePatches) {
+ return;
+ }
+ const { overrides, interventions, shims } = availablePatches;
+ const alsoShowHidden = location.hash === "#all";
+ redrawTable($("#overrides"), overrides, alsoShowHidden);
+ redrawTable($("#interventions"), interventions, alsoShowHidden);
+ updateShimTables(shims, alsoShowHidden);
+}
+
+function clearTableAndAddMessage(table, msgId) {
+ table.querySelectorAll("tr").forEach(tr => {
+ tr.remove();
+ });
+
+ const tr = document.createElement("tr");
+ tr.className = "message";
+ tr.id = msgId;
+
+ const td = document.createElement("td");
+ td.setAttribute("colspan", "3");
+ document.l10n.setAttributes(td, msgId);
+ tr.appendChild(td);
+
+ table.appendChild(tr);
+}
+
+function hideMessagesOnTable(table) {
+ table.querySelectorAll("tr.message").forEach(tr => {
+ tr.remove();
+ });
+}
+
+function updateShimTables(shimsChanged, alsoShowHidden) {
+ const tables = document.querySelectorAll("table.shims");
+ if (!tables.length) {
+ return;
+ }
+
+ for (const { bug, disabledReason, hidden, id, name, type } of shimsChanged) {
+ // if any shim is disabled by global pref, all of them are. just show the
+ // "disabled in about:config" message on each shim table in that case.
+ if (disabledReason === "globalPref") {
+ for (const table of tables) {
+ clearTableAndAddMessage(table, "text-disabled-in-about-config");
+ }
+ return;
+ }
+
+ // otherwise, find which table the shim belongs in. if there is none,
+ // ignore the shim (we're not showing it on the UI for whatever reason).
+ const table = document.querySelector(`table.shims#${type}`);
+ if (!table) {
+ continue;
+ }
+
+ // similarly, skip shims hidden from the UI (only for testing, etc).
+ if (!alsoShowHidden && hidden) {
+ continue;
+ }
+
+ // also, hide the shim if it is disabled because it is not meant for this
+ // platform, release (etc) rather than being disabled by pref/about:compat
+ const notApplicable =
+ disabledReason &&
+ disabledReason !== "pref" &&
+ disabledReason !== "session";
+ if (!alsoShowHidden && notApplicable) {
+ continue;
+ }
+
+ // create an updated table-row for the shim
+ const tr = document.createElement("tr");
+ tr.setAttribute("data-id", id);
+
+ let td = document.createElement("td");
+ td.innerText = name;
+ tr.appendChild(td);
+
+ td = document.createElement("td");
+ const a = document.createElement("a");
+ a.href = `https://bugzilla.mozilla.org/show_bug.cgi?id=${bug}`;
+ document.l10n.setAttributes(a, "label-more-information", { bug });
+ a.target = "_blank";
+ td.appendChild(a);
+ tr.appendChild(td);
+
+ td = document.createElement("td");
+ tr.appendChild(td);
+ const button = document.createElement("button");
+ document.l10n.setAttributes(
+ button,
+ disabledReason ? "label-enable" : "label-disable"
+ );
+ td.appendChild(button);
+
+ // is it already in the table?
+ const row = table.querySelector(`tr[data-id="${id}"]`);
+ if (row) {
+ row.replaceWith(tr);
+ } else {
+ table.appendChild(tr);
+ }
+ }
+
+ for (const table of tables) {
+ if (!table.querySelector("tr:not(.message)")) {
+ // no shims? then add a message that none are available for this platform/config
+ clearTableAndAddMessage(table, `text-no-${table.id}`);
+ } else {
+ // otherwise hide any such message, since we have shims on the list
+ hideMessagesOnTable(table);
+ }
+ }
+}
+
+function redrawTable(table, data, alsoShowHidden) {
+ const df = document.createDocumentFragment();
+ table.querySelectorAll("tr").forEach(tr => {
+ tr.remove();
+ });
+
+ let noEntriesMessage;
+ if (data === false) {
+ noEntriesMessage = "text-disabled-in-about-config";
+ } else if (data.length === 0) {
+ noEntriesMessage = `text-no-${table.id}`;
+ }
+
+ if (noEntriesMessage) {
+ const tr = document.createElement("tr");
+ df.appendChild(tr);
+
+ const td = document.createElement("td");
+ td.setAttribute("colspan", "3");
+ document.l10n.setAttributes(td, noEntriesMessage);
+ tr.appendChild(td);
+
+ table.appendChild(df);
+ return;
+ }
+
+ for (const row of data) {
+ if (row.hidden && !alsoShowHidden) {
+ continue;
+ }
+
+ const tr = document.createElement("tr");
+ tr.setAttribute("data-id", row.id);
+ df.appendChild(tr);
+
+ let td = document.createElement("td");
+ td.innerText = row.domain;
+ tr.appendChild(td);
+
+ td = document.createElement("td");
+ const a = document.createElement("a");
+ const bug = row.bug;
+ a.href = `https://bugzilla.mozilla.org/show_bug.cgi?id=${bug}`;
+ document.l10n.setAttributes(a, "label-more-information", { bug });
+ a.target = "_blank";
+ td.appendChild(a);
+ tr.appendChild(td);
+
+ td = document.createElement("td");
+ tr.appendChild(td);
+ const button = document.createElement("button");
+ document.l10n.setAttributes(
+ button,
+ row.active ? "label-disable" : "label-enable"
+ );
+ td.appendChild(button);
+ }
+ table.appendChild(df);
+}
+
+window.onhashchange = redraw;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPage.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPage.js
new file mode 100644
index 0000000000..0df6d1e0f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPage.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+"use strict";
+
+/* global ExtensionAPI, XPCOMUtils, Services */
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "resProto",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsISubstitutingProtocolHandler"
+);
+
+const ResourceSubstitution = "webcompat";
+const ProcessScriptURL = "resource://webcompat/aboutPageProcessScript.js";
+const ContractID = "@mozilla.org/network/protocol/about;1?what=compat";
+
+this.aboutPage = class extends ExtensionAPI {
+ onStartup() {
+ const { rootURI } = this.extension;
+
+ resProto.setSubstitution(
+ ResourceSubstitution,
+ Services.io.newURI("about-compat/", null, rootURI)
+ );
+
+ if (!(ContractID in Cc)) {
+ Services.ppmm.loadProcessScript(ProcessScriptURL, true);
+ this.processScriptRegistered = true;
+ }
+ }
+
+ onShutdown() {
+ resProto.setSubstitution(ResourceSubstitution, null);
+
+ if (this.processScriptRegistered) {
+ Services.ppmm.removeDelayedProcessScript(ProcessScriptURL);
+ }
+ }
+};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPage.json b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPage.json
new file mode 100644
index 0000000000..42e6114188
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPage.json
@@ -0,0 +1,6 @@
+[
+ {
+ "namespace": "aboutCompat",
+ "description": "Enables the about:compat page"
+ }
+]
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPageProcessScript.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPageProcessScript.js
new file mode 100644
index 0000000000..24d44e53fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/about-compat/aboutPageProcessScript.js
@@ -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/. */
+
+/* eslint-env mozilla/process-script */
+
+"use strict";
+
+// Note: This script is used only when a static registration for our
+// component is not already present in the libxul binary.
+
+const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+const classID = Components.ID("{97bf9550-2a7b-11e9-b56e-0800200c9a66}");
+
+if (!Cm.isCIDRegistered(classID)) {
+ const { ComponentUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ComponentUtils.sys.mjs"
+ );
+
+ const factory = ComponentUtils.generateSingletonFactory(function () {
+ const { AboutCompat } = ChromeUtils.importESModule(
+ "resource://webcompat/AboutCompat.sys.mjs"
+ );
+ return new AboutCompat();
+ });
+
+ Cm.registerFactory(
+ classID,
+ "about:compat",
+ "@mozilla.org/network/protocol/about;1?what=compat",
+ factory
+ );
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/injections.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/injections.js
new file mode 100644
index 0000000000..87b1da747b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/injections.js
@@ -0,0 +1,1061 @@
+/* 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/. */
+
+"use strict";
+
+/* globals module, require */
+
+// This is a hack for the tests.
+if (typeof InterventionHelpers === "undefined") {
+ var InterventionHelpers = require("../lib/intervention_helpers");
+}
+
+/**
+ * For detailed information on our policies, and a documention on this format
+ * and its possibilites, please check the Mozilla-Wiki at
+ *
+ * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
+ */
+const AVAILABLE_INJECTIONS = [
+ {
+ id: "testbed-injection",
+ platform: "all",
+ domain: "webcompat-addon-testbed.herokuapp.com",
+ bug: "0000000",
+ hidden: true,
+ contentScripts: {
+ matches: ["*://webcompat-addon-testbed.herokuapp.com/*"],
+ css: [
+ {
+ file: "injections/css/bug0000000-testbed-css-injection.css",
+ },
+ ],
+ js: [
+ {
+ file: "injections/js/bug0000000-testbed-js-injection.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1452707",
+ platform: "all",
+ domain: "ib.absa.co.za",
+ bug: "1452707",
+ contentScripts: {
+ matches: ["https://ib.absa.co.za/*"],
+ js: [
+ {
+ file: "injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1457335",
+ platform: "desktop",
+ domain: "histography.io",
+ bug: "1457335",
+ contentScripts: {
+ matches: ["*://histography.io/*"],
+ js: [
+ {
+ file: "injections/js/bug1457335-histography.io-ua-change.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1472075",
+ platform: "desktop",
+ domain: "bankofamerica.com",
+ bug: "1472075",
+ contentScripts: {
+ matches: [
+ "*://*.bankofamerica.com/*",
+ "*://*.ml.com/*", // #120104
+ ],
+ js: [
+ {
+ file: "injections/js/bug1472075-bankofamerica.com-ua-change.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1579159",
+ platform: "android",
+ domain: "m.tailieu.vn",
+ bug: "1579159",
+ contentScripts: {
+ matches: ["*://m.tailieu.vn/*", "*://m.elib.vn/*"],
+ js: [
+ {
+ file: "injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1583366",
+ platform: "desktop",
+ domain: "Download prompt for files with no content-type",
+ bug: "1583366",
+ data: {
+ urls: ["https://ads-us.rd.linksynergy.com/as.php*"],
+ contentType: {
+ name: "content-type",
+ value: "text/html; charset=utf-8",
+ },
+ },
+ customFunc: "noSniffFix",
+ },
+ {
+ id: "bug1575000",
+ platform: "all",
+ domain: "apply.lloydsbank.co.uk",
+ bug: "1575000",
+ contentScripts: {
+ matches: ["*://apply.lloydsbank.co.uk/*"],
+ css: [
+ {
+ file: "injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1610344",
+ platform: "all",
+ domain: "directv.com.co",
+ bug: "1610344",
+ contentScripts: {
+ matches: [
+ "https://*.directv.com.co/*",
+ "https://*.directv.com.ec/*", // bug 1827706
+ ],
+ css: [
+ {
+ file: "injections/css/bug1610344-directv.com.co-hide-unsupported-message.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1644830",
+ platform: "desktop",
+ domain: "usps.com",
+ bug: "1644830",
+ contentScripts: {
+ matches: ["https://*.usps.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1651917",
+ platform: "android",
+ domain: "teletrader.com",
+ bug: "1651917",
+ contentScripts: {
+ matches: ["*://*.teletrader.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1651917-teletrader.com.body-transform-origin.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1653075",
+ platform: "desktop",
+ domain: "livescience.com",
+ bug: "1653075",
+ contentScripts: {
+ matches: ["*://*.livescience.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1653075-livescience.com-scrollbar-width.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1654907",
+ platform: "android",
+ domain: "reactine.ca",
+ bug: "1654907",
+ contentScripts: {
+ matches: ["*://*.reactine.ca/*"],
+ css: [
+ {
+ file: "injections/css/bug1654907-reactine.ca-hide-unsupported.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1694470",
+ platform: "android",
+ domain: "m.myvidster.com",
+ bug: "1694470",
+ contentScripts: {
+ matches: ["https://m.myvidster.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1694470-myvidster.com-content-not-shown.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1707795",
+ platform: "desktop",
+ domain: "Office Excel spreadsheets",
+ bug: "1707795",
+ contentScripts: {
+ matches: [
+ "*://*.live.com/*",
+ "*://*.office.com/*",
+ "*://*.sharepoint.com/*",
+ ],
+ css: [
+ {
+ file: "injections/css/bug1707795-office365-sheets-overscroll-disable.css",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1712833",
+ platform: "all",
+ domain: "buskocchi.desuca.co.jp",
+ bug: "1712833",
+ contentScripts: {
+ matches: ["*://buskocchi.desuca.co.jp/*"],
+ css: [
+ {
+ file: "injections/css/bug1712833-buskocchi.desuca.co.jp-fix-map-height.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1722955",
+ platform: "android",
+ domain: "frontgate.com",
+ bug: "1722955",
+ contentScripts: {
+ matches: ["*://*.frontgate.com/*"],
+ js: [
+ {
+ file: "lib/ua_helpers.js",
+ },
+ {
+ file: "injections/js/bug1722955-frontgate.com-ua-override.js",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1724868",
+ platform: "android",
+ domain: "news.yahoo.co.jp",
+ bug: "1724868",
+ contentScripts: {
+ matches: ["*://news.yahoo.co.jp/articles/*", "*://s.yimg.jp/*"],
+ js: [
+ {
+ file: "injections/js/bug1724868-news.yahoo.co.jp-ua-override.js",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1741234",
+ platform: "all",
+ domain: "patient.alphalabs.ca",
+ bug: "1741234",
+ contentScripts: {
+ matches: ["*://patient.alphalabs.ca/*"],
+ css: [
+ {
+ file: "injections/css/bug1741234-patient.alphalabs.ca-height-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1739489",
+ platform: "desktop",
+ domain: "Sites using draft.js",
+ bug: "1739489",
+ contentScripts: {
+ matches: [
+ "*://draftjs.org/*", // Bug 1739489
+ "*://www.facebook.com/*", // Bug 1739489
+ "*://twitter.com/*", // Bug 1776229
+ "*://mobile.twitter.com/*", // Bug 1776229
+ "*://*.reddit.com/*", // Bug 1829755
+ ],
+ js: [
+ {
+ file: "injections/js/bug1739489-draftjs-beforeinput.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1765947",
+ platform: "android",
+ domain: "veniceincoming.com",
+ bug: "1765947",
+ contentScripts: {
+ matches: ["*://veniceincoming.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1765947-veniceincoming.com-left-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug11769762",
+ platform: "all",
+ domain: "tiktok.com",
+ bug: "1769762",
+ contentScripts: {
+ matches: ["https://www.tiktok.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1769762-tiktok.com-plugins-shim.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1770962",
+ platform: "all",
+ domain: "coldwellbankerhomes.com",
+ bug: "1770962",
+ contentScripts: {
+ matches: ["*://*.coldwellbankerhomes.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1770962-coldwellbankerhomes.com-image-height.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1774490",
+ platform: "all",
+ domain: "rainews.it",
+ bug: "1774490",
+ contentScripts: {
+ matches: ["*://www.rainews.it/*"],
+ css: [
+ {
+ file: "injections/css/bug1774490-rainews.it-gallery-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1774005",
+ platform: "all",
+ domain: "Sites relying on window.InstallTrigger",
+ bug: "1774005",
+ contentScripts: {
+ matches: [
+ "*://*.crunchyroll.com/*", // Bug 1777597
+ "*://*.ersthelfer.tv/*", // Bug 1817520
+ "*://*.webex.com/*", // Bug 1788934
+ "*://ifcinema.institutfrancais.com/*", // Bug 1806423
+ "*://islamionline.islamicbank.ps/*", // Bug 1821439
+ "*://*.itv.com/*", // Bug 1830203
+ "*://mobilevikings.be/*/registration/*", // Bug 1797400
+ "*://www.schoolnutritionandfitness.com/*", // Bug 1793761
+ ],
+ js: [
+ {
+ file: "injections/js/bug1774005-installtrigger-shim.js",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1859617",
+ platform: "all",
+ domain: "Sites relying on there being no window.InstallTrigger",
+ bug: "1859617",
+ contentScripts: {
+ matches: [
+ "*://*.stallionexpress.ca/*", // Bug 1859617
+ ],
+ js: [
+ {
+ file: "injections/js/bug1859617-installtrigger-removal-shim.js",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1784141",
+ platform: "android",
+ domain: "aveeno.com and acuvue.com",
+ bug: "1784141",
+ contentScripts: {
+ matches: [
+ "*://*.aveeno.com/*",
+ "*://*.aveeno.ca/*",
+ "*://*.aveeno.com.au/*",
+ "*://*.aveeno.co.kr/*",
+ "*://*.aveeno.co.uk/*",
+ "*://*.aveeno.ie/*",
+ "*://*.acuvue.com/*", // 1804730
+ "*://*.acuvue.com.ar/*",
+ "*://*.acuvue.com.br/*",
+ "*://*.acuvue.ca/*",
+ "*://*.acuvue-fr.ca/*",
+ "*://*.acuvue.cl/*",
+ "*://*.acuvue.co.cr/*",
+ "*://*.acuvue.com.co/*",
+ "*://*.acuvue.com.do/*",
+ "*://*.acuvue.com.pe/*",
+ "*://*.acuvue.com.sv/*",
+ "*://*.acuvue.com.gt/*",
+ "*://*.acuvue.hn/*",
+ "*://*.acuvue.com.mx/*",
+ "*://*.acuvue.com.pa/*",
+ "*://*.acuvue.com.py/*",
+ "*://*.acuvue.com.pr/*",
+ "*://*.acuvue.com.uy/*",
+ "*://*.acuvue.com.au/*",
+ "*://*.acuvue.com.cn/*",
+ "*://*.acuvue.com.hk/*",
+ "*://*.acuvue.co.in/*",
+ "*://*.acuvue.co.id/*",
+ "*://acuvuevision.jp/*",
+ "*://*.acuvue.co.kr/*",
+ "*://*.acuvue.com.my/*",
+ "*://*.acuvue.co.nz/*",
+ "*://*.acuvue.com.sg/*",
+ "*://*.acuvue.com.tw/*",
+ "*://*.acuvue.co.th/*",
+ "*://*.acuvue.com.vn/*",
+ "*://*.acuvue.at/*",
+ "*://*.acuvue.be/*",
+ "*://*.fr.acuvue.be/*",
+ "*://*.acuvue-croatia.com/*",
+ "*://*.acuvue.cz/*",
+ "*://*.acuvue.dk/*",
+ "*://*.acuvue.fi/*",
+ "*://*.acuvue.fr/*",
+ "*://*.acuvue.de/*",
+ "*://*.acuvue.gr/*",
+ "*://*.acuvue.hu/*",
+ "*://*.acuvue.ie/*",
+ "*://*.acuvue.co.il/*",
+ "*://*.acuvue.it/*",
+ "*://*.acuvuekz.com/*",
+ "*://*.acuvue.lu/*",
+ "*://*.en.acuvuearabia.com/*",
+ "*://*.acuvuearabia.com/*",
+ "*://*.acuvue.nl/*",
+ "*://*.acuvue.no/*",
+ "*://*.acuvue.pl/*",
+ "*://*.acuvue.pt/*",
+ "*://*.acuvue.ro/*",
+ "*://*.acuvue.ru/*",
+ "*://*.acuvue.sk/*",
+ "*://*.acuvue.si/*",
+ "*://*.acuvue.co.za/*",
+ "*://*.jnjvision.com.tr/*",
+ "*://*.acuvue.co.uk/*",
+ "*://*.acuvue.ua/*",
+ "*://*.acuvue.com.pe/*",
+ "*://*.acuvue.es/*",
+ "*://*.acuvue.se/*",
+ "*://*.acuvue.ch/*",
+ ],
+ css: [
+ {
+ file: "injections/css/bug1784141-aveeno.com-acuvue.com-unsupported.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1784199",
+ platform: "all",
+ domain: "Sites based on Entrata Platform",
+ bug: "1784199",
+ contentScripts: {
+ matches: [
+ "*://*.7streetbrownstones.com/*", // #129553
+ "*://*.aptsovation.com/*",
+ "*://*.avanabayview.com/*", // #118617
+ "*://*.breakpointeandcoronado.com/*", // #117735
+ "*://*.courtsatspringmill.com/*", // #128404
+ "*://*.fieldstoneamherst.com/*", // #132974
+ "*://*.gslbriarcreek.com/*", // #126401
+ "*://*.hpixeniatrails.com/*", // #131703
+ "*://*.liveatlasathens.com/*", // #111189
+ "*://*.liveobserverpark.com/*", // #105244
+ "*://*.liveupark.com/*", // #121083
+ "*://*.midwayurban.com/*", // #116523
+ "*://*.nhcalaska.com/*",
+ "*://*.prospectportal.com/*", // #115206
+ "*://*.securityproperties.com/*",
+ "*://*.thefoundryat41st.com/*", // #128994
+ "*://*.theloftsorlando.com/*",
+ "*://*.vanallenapartments.com/*", // #120056
+ ],
+ css: [
+ {
+ file: "injections/css/bug1784199-entrata-platform-unsupported.css",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1799968",
+ platform: "linux",
+ domain: "www.samsung.com",
+ bug: "1799968",
+ contentScripts: {
+ matches: ["*://www.samsung.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1799968-www.samsung.com-appVersion-linux-fix.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1799980",
+ platform: "all",
+ domain: "healow.com",
+ bug: "1799980",
+ contentScripts: {
+ matches: ["*://healow.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1799980-healow.com-infinite-loop-fix.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1448747",
+ platform: "android",
+ domain: "FastClick breakage",
+ bug: "1448747",
+ contentScripts: {
+ matches: [
+ "*://*.co2meter.com/*", // 10959
+ "*://*.franmar.com/*", // 27273
+ "*://*.themusiclab.org/*", // 49667
+ "*://*.oregonfoodbank.org/*", // 53203
+ "*://*.fourbarrelcoffee.com/*", // 59427
+ "*://bluetokaicoffee.com/*", // 99867
+ "*://bathpublishing.com/*", // 100145
+ "*://dylantalkstone.com/*", // 101356
+ "*://renewd.com.au/*", // 104998
+ "*://*.lamudi.co.id/*", // 106767
+ "*://*.thehawksmoor.com/*", // 107549
+ "*://weaversofireland.com/*", // 116816
+ "*://*.iledefrance-mobilites.fr/*", // 117344
+ "*://*.lawnmowerpartsworld.com/*", // 117577
+ "*://*.discountcoffee.co.uk/*", // 118757
+ "*://torguard.net/*", // 120113
+ "*://*.arcsivr.com/*", // 120716
+ "*://drafthouse.com/*", // 126385
+ "*://*.lafoodbank.org/*", // 127006
+ "*://rutamayacoffee.com/*", // 129353
+ ],
+ js: [
+ {
+ file: "injections/js/bug1448747-fastclick-shim.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1818818",
+ platform: "android",
+ domain: "FastClick breakage - legacy",
+ bug: "1818818",
+ contentScripts: {
+ matches: [
+ "*://*.chatiw.com/*", // 5544
+ "*://*.wellcare.com/*", // 116595
+ ],
+ js: [
+ {
+ file: "injections/js/bug1818818-fastclick-legacy-shim.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1819476",
+ platform: "all",
+ domain: "axisbank.com",
+ bug: "1819476",
+ contentScripts: {
+ matches: ["*://*.axisbank.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1819476-axisbank.com-webkitSpeechRecognition-shim.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1819450",
+ platform: "android",
+ domain: "cmbchina.com",
+ bug: "1819450",
+ contentScripts: {
+ matches: ["*://www.cmbchina.com/*", "*://cmbchina.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1819450-cmbchina.com-ua-change.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1827678-webc77727",
+ platform: "android",
+ domain: "free4talk.com",
+ bug: "1827678",
+ contentScripts: {
+ matches: ["*://www.free4talk.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1819678-free4talk.com-window-chrome-shim.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1827678-webc119017",
+ platform: "desktop",
+ domain: "nppes.cms.hhs.gov",
+ bug: "1827678",
+ contentScripts: {
+ matches: ["*://nppes.cms.hhs.gov/*"],
+ css: [
+ {
+ file: "injections/css/bug1819678-nppes.cms.hhs.gov-unsupported-banner.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1830776",
+ platform: "all",
+ domain: "blueshieldca.com",
+ bug: "1830776",
+ contentScripts: {
+ matches: ["*://*.blueshieldca.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1830776-blueshieldca.com-unsupported.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1829949",
+ platform: "desktop",
+ domain: "tomshardware.com",
+ bug: "1829949",
+ contentScripts: {
+ matches: ["*://*.tomshardware.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1829949-tomshardware.com-scrollbar-width.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1830747",
+ platform: "android",
+ domain: "my.babbel.com",
+ bug: "1830747",
+ contentScripts: {
+ matches: ["*://my.babbel.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1830747-babbel.com-page-height.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1830752",
+ platform: "all",
+ domain: "afisha.ru",
+ bug: "1830752",
+ contentScripts: {
+ matches: ["*://*.afisha.ru/*"],
+ css: [
+ {
+ file: "injections/css/bug1830752-afisha.ru-slider-pointer-events.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1830761",
+ platform: "all",
+ domain: "91mobiles.com",
+ bug: "1830761",
+ contentScripts: {
+ matches: ["*://*.91mobiles.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1830761-91mobiles.com-content-height.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1830796",
+ platform: "android",
+ domain: "copyleaks.com",
+ bug: "1830796",
+ contentScripts: {
+ matches: ["*://*.copyleaks.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1830796-copyleaks.com-hide-unsupported.css",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1830810",
+ platform: "all",
+ domain: "interceramic.com",
+ bug: "1830810",
+ contentScripts: {
+ matches: ["*://interceramic.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1830810-interceramic.com-hide-unsupported.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1830813",
+ platform: "desktop",
+ domain: "onstove.com",
+ bug: "1830813",
+ contentScripts: {
+ matches: ["*://*.onstove.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1830813-page.onstove.com-hide-unsupported.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1831007",
+ platform: "all",
+ domain: "All international Nintendo domains",
+ bug: "1831007",
+ contentScripts: {
+ matches: [
+ "*://*.mojenintendo.cz/*",
+ "*://*.nintendo-europe.com/*",
+ "*://*.nintendo.at/*",
+ "*://*.nintendo.be/*",
+ "*://*.nintendo.ch/*",
+ "*://*.nintendo.co.il/*",
+ "*://*.nintendo.co.jp/*",
+ "*://*.nintendo.co.kr/*",
+ "*://*.nintendo.co.nz/*",
+ "*://*.nintendo.co.uk/*",
+ "*://*.nintendo.co.za/*",
+ "*://*.nintendo.com.au/*",
+ "*://*.nintendo.com.hk/*",
+ "*://*.nintendo.com/*",
+ "*://*.nintendo.de/*",
+ "*://*.nintendo.dk/*",
+ "*://*.nintendo.es/*",
+ "*://*.nintendo.fi/*",
+ "*://*.nintendo.fr/*",
+ "*://*.nintendo.gr/*",
+ "*://*.nintendo.hu/*",
+ "*://*.nintendo.it/*",
+ "*://*.nintendo.nl/*",
+ "*://*.nintendo.no/*",
+ "*://*.nintendo.pt/*",
+ "*://*.nintendo.ru/*",
+ "*://*.nintendo.se/*",
+ "*://*.nintendo.sk/*",
+ "*://*.nintendo.tw/*",
+ "*://*.nintendoswitch.com.cn/*",
+ ],
+ js: [
+ {
+ file: "injections/js/bug1831007-nintendo-window-OnetrustActiveGroups.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1836157",
+ platform: "android",
+ domain: "thai-masszazs.net",
+ bug: "1836157",
+ contentScripts: {
+ matches: ["*://*.thai-masszazs.net/*"],
+ js: [
+ {
+ file: "injections/js/bug1836157-thai-masszazs-niceScroll-disable.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1836103",
+ platform: "all",
+ domain: "autostar-novoross.ru",
+ bug: "1836103",
+ contentScripts: {
+ matches: ["*://autostar-novoross.ru/*"],
+ css: [
+ {
+ file: "injections/css/bug1836103-autostar-novoross.ru-make-map-taller.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1836105",
+ platform: "all",
+ domain: "cnn.com",
+ bug: "1836105",
+ contentScripts: {
+ matches: ["*://*.cnn.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1836105-cnn.com-fix-blank-pages-when-printing.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1842437",
+ platform: "desktop",
+ domain: "www.youtube.com",
+ bug: "1842437",
+ contentScripts: {
+ matches: ["*://www.youtube.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1842437-www.youtube.com-performance-now-precision.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1848711",
+ platform: "android",
+ domain: "vio.com",
+ bug: "1848711",
+ contentScripts: {
+ matches: ["*://*.vio.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1848711-vio.com-page-height.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1848713",
+ platform: "all",
+ domain: "cleanrider.com",
+ bug: "1848713",
+ contentScripts: {
+ matches: ["*://*.cleanrider.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1848713-cleanrider.com-slider.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1848849",
+ platform: "all",
+ domain: "theaa.com",
+ bug: "1848849",
+ contentScripts: {
+ matches: ["*://*.theaa.com/route-planner/*"],
+ css: [
+ {
+ file: "injections/css/bug1848849-theaa.com-printing-mode-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1849019",
+ platform: "android",
+ domain: "axa-assistance.pl",
+ bug: "1849019",
+ contentScripts: {
+ matches: ["*://*.axa-assistance.pl/*"],
+ css: [
+ {
+ file: "injections/css/bug1849019-axa-assistance.pl-datepicker-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1849058",
+ platform: "all",
+ domain: "nicochannel.jp",
+ bug: "1849058",
+ contentScripts: {
+ matches: ["*://nicochannel.jp/*", "*://gs-ch.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1849058-nicochannel.jp-picture-in-picture-shim.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1849388",
+ platform: "android",
+ domain: "kucharkaprodceru.cz",
+ bug: "1849388",
+ contentScripts: {
+ matches: ["*://*.kucharkaprodceru.cz/*"],
+ css: [
+ {
+ file: "injections/css/bug1849388-kucharkaprodceru.cz-scroll-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1855014",
+ platform: "android",
+ domain: "eksiseyler.com",
+ bug: "1855014",
+ contentScripts: {
+ matches: ["*://eksiseyler.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1855014-eksiseyler.com.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1855071",
+ platform: "android",
+ domain: "www.meteoam.it",
+ bug: "1855071",
+ contentScripts: {
+ matches: ["*://www.meteoam.it/*"],
+ js: [
+ {
+ file: "injections/js/bug1855071-www.meteoam.it.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1864564",
+ platform: "all",
+ domain: "Esri breakage",
+ bug: "1864564",
+ contentScripts: {
+ matches: [
+ "*://*.ncep.noaa.gov/*",
+ "*://*.northumberland.gov.uk/*",
+ "*://webmap.gis.gov.mo/*",
+ ],
+ js: [
+ {
+ file: "injections/js/bug1864564-esri-transfrom-names-shim.js",
+ },
+ ],
+ allFrames: true,
+ },
+ },
+ {
+ id: "bug1868345",
+ platform: "desktop",
+ domain: "tvmovie.de",
+ bug: "1868345",
+ contentScripts: {
+ matches: [
+ "*://www.tvmovie.de/tv/fernsehprogramm",
+ "*://www.tvmovie.de/tv/fernsehprogramm*",
+ ],
+ css: [
+ {
+ file: "injections/css/bug1868345-tvmovie.de-scroll-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1877346",
+ platform: "android",
+ domain: "offerup.com",
+ bug: "1877346",
+ contentScripts: {
+ matches: ["*://offerup.com/*"],
+ css: [
+ {
+ file: "injections/css/bug1877346-offerup.com-infinite-scroll-fix.css",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1884842",
+ platform: "android",
+ domain: "foodora.cz",
+ bug: "1884842",
+ contentScripts: {
+ matches: ["*://*.foodora.cz/*"],
+ css: [
+ {
+ file: "injections/css/bug1884842-foodora.cz-height-fix.css",
+ },
+ ],
+ },
+ },
+];
+
+module.exports = AVAILABLE_INJECTIONS;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/shims.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/shims.js
new file mode 100644
index 0000000000..f26ad96d04
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/shims.js
@@ -0,0 +1,889 @@
+/* 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/. */
+
+"use strict";
+
+/* globals module, require */
+
+const AVAILABLE_SHIMS = [
+ {
+ hiddenInAboutCompat: true,
+ id: "LiveTestShim",
+ platform: "all",
+ name: "Live test shim",
+ bug: "livetest",
+ file: "live-test-shim.js",
+ matches: ["*://webcompat-addon-testbed.herokuapp.com/shims_test.js"],
+ needsShimHelpers: ["getOptions", "optIn"],
+ },
+ {
+ hiddenInAboutCompat: true,
+ id: "MochitestShim",
+ platform: "all",
+ branch: ["all:ignoredOtherPlatform"],
+ name: "Test shim for Mochitests",
+ bug: "mochitest",
+ file: "mochitest-shim-1.js",
+ matches: [
+ "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test.js",
+ ],
+ needsShimHelpers: ["getOptions", "optIn"],
+ options: {
+ simpleOption: true,
+ complexOption: { a: 1, b: "test" },
+ branchValue: { value: true, branches: [] },
+ platformValue: { value: true, platform: "neverUsed" },
+ },
+ unblocksOnOptIn: ["*://trackertest.org/*"],
+ },
+ {
+ hiddenInAboutCompat: true,
+ disabled: true,
+ id: "MochitestShim2",
+ platform: "all",
+ name: "Test shim for Mochitests (disabled by default)",
+ bug: "mochitest",
+ file: "mochitest-shim-2.js",
+ matches: [
+ "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_2.js",
+ ],
+ needsShimHelpers: ["getOptions", "optIn"],
+ options: {
+ simpleOption: true,
+ complexOption: { a: 1, b: "test" },
+ branchValue: { value: true, branches: [] },
+ platformValue: { value: true, platform: "neverUsed" },
+ },
+ unblocksOnOptIn: ["*://trackertest.org/*"],
+ },
+ {
+ hiddenInAboutCompat: true,
+ id: "MochitestShim3",
+ platform: "all",
+ name: "Test shim for Mochitests (host)",
+ bug: "mochitest",
+ file: "mochitest-shim-3.js",
+ notHosts: ["example.com"],
+ matches: [
+ "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
+ ],
+ },
+ {
+ hiddenInAboutCompat: true,
+ id: "MochitestShim4",
+ platform: "all",
+ name: "Test shim for Mochitests (notHost)",
+ bug: "mochitest",
+ file: "mochitest-shim-3.js",
+ hosts: ["example.net"],
+ matches: [
+ "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
+ ],
+ },
+ {
+ hiddenInAboutCompat: true,
+ id: "MochitestShim5",
+ platform: "all",
+ name: "Test shim for Mochitests (branch)",
+ bug: "mochitest",
+ file: "mochitest-shim-3.js",
+ branches: ["never matches"],
+ matches: [
+ "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
+ ],
+ },
+ {
+ hiddenInAboutCompat: true,
+ id: "MochitestShim6",
+ platform: "never matches",
+ name: "Test shim for Mochitests (platform)",
+ bug: "mochitest",
+ file: "mochitest-shim-3.js",
+ matches: [
+ "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
+ ],
+ },
+ {
+ id: "AddThis",
+ platform: "all",
+ name: "AddThis",
+ bug: "1713694",
+ file: "addthis-angular.js",
+ matches: [
+ "*://s7.addthis.com/icons/official-addthis-angularjs/current/dist/official-addthis-angularjs.min.js*",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Adform",
+ platform: "all",
+ name: "Adform",
+ bug: "1713695",
+ file: "adform.js",
+ matches: [
+ "*://track.adform.net/serving/scripts/trackpoint/",
+ "*://track.adform.net/serving/scripts/trackpoint/async/",
+ {
+ patterns: ["*://track.adform.net/Serving/TrackPoint/*"],
+ target: "tracking-pixel.png",
+ types: ["image", "imageset", "xmlhttprequest"],
+ },
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "AdNexusAST",
+ platform: "all",
+ name: "AdNexus AST",
+ bug: "1734130",
+ file: "adnexus-ast.js",
+ matches: ["*://*.adnxs.com/*/ast.js*"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "AdNexusPrebid",
+ platform: "all",
+ name: "AdNexus Prebid",
+ bug: "1713696",
+ file: "adnexus-prebid.js",
+ matches: ["*://*.adnxs.com/*/pb.js*", "*://*.adnxs.com/*/prebid*"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "AdobeEverestJS",
+ platform: "all",
+ name: "Adobe EverestJS",
+ bug: "1728114",
+ file: "everest.js",
+ matches: ["*://www.everestjs.net/static/st.v3.js*"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ // keep this above AdSafeProtectedTrackingPixels
+ id: "AdSafeProtectedGoogleIMAAdapter",
+ platform: "all",
+ name: "Ad Safe Protected Google IMA Adapter",
+ bug: "1508639",
+ file: "adsafeprotected-ima.js",
+ matches: ["*://static.adsafeprotected.com/vans-adapter-google-ima.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "AdsByGoogle",
+ platform: "all",
+ name: "Ads by Google",
+ bug: "1713726",
+ file: "google-ads.js",
+ matches: [
+ "*://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js",
+ {
+ patterns: [
+ "*://pagead2.googlesyndication.com/pagead/*.js*fcd=true",
+ "*://pagead2.googlesyndication.com/pagead/js/*.js*fcd=true",
+ ],
+ target: "empty-script.js",
+ types: ["xmlhttprequest"],
+ },
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "AdvertisingCom",
+ platform: "all",
+ name: "advertising.com",
+ bug: "1701685",
+ matches: [
+ {
+ patterns: ["*://pixel.advertising.com/firefox-etp"],
+ target: "tracking-pixel.png",
+ types: ["image", "imageset", "xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ patterns: ["*://cdn.cmp.advertising.com/firefox-etp"],
+ target: "empty-script.js",
+ types: ["xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ patterns: ["*://*.advertising.com/*.js*"],
+ target: "https://cdn.cmp.advertising.com/firefox-etp",
+ types: ["image", "imageset", "xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ patterns: ["*://*.advertising.com/*"],
+ target: "https://pixel.advertising.com/firefox-etp",
+ types: ["image", "imageset", "xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ ],
+ },
+ {
+ id: "Branch",
+ platform: "all",
+ name: "Branch Web SDK",
+ bug: "1716220",
+ file: "branch.js",
+ matches: ["*://cdn.branch.io/branch-latest.min.js*"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "DoubleVerify",
+ platform: "all",
+ name: "DoubleVerify",
+ bug: "1771557",
+ file: "doubleverify.js",
+ matches: ["*://pub.doubleverify.com/signals/pub.js*"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "AmazonTAM",
+ platform: "all",
+ name: "Amazon Transparent Ad Marketplace",
+ bug: "1713698",
+ file: "apstag.js",
+ matches: ["*://c.amazon-adsystem.com/aax2/apstag.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "BmAuth",
+ platform: "all",
+ name: "BmAuth by 9c9media",
+ bug: "1486337",
+ file: "bmauth.js",
+ matches: ["*://auth.9c9media.ca/auth/main.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Chartbeat",
+ platform: "all",
+ name: "Chartbeat",
+ bug: "1713699",
+ file: "chartbeat.js",
+ matches: [
+ "*://static.chartbeat.com/js/chartbeat.js",
+ "*://static.chartbeat.com/js/chartbeat_video.js",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Criteo",
+ platform: "all",
+ name: "Criteo",
+ bug: "1713720",
+ file: "criteo.js",
+ matches: ["*://static.criteo.net/js/ld/publishertag.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ // keep this above AdSafeProtectedTrackingPixels
+ id: "Doubleclick",
+ platform: "all",
+ name: "Doubleclick",
+ bug: "1713693",
+ matches: [
+ {
+ patterns: [
+ "*://securepubads.g.doubleclick.net/gampad/*ad-blk*",
+ "*://pubads.g.doubleclick.net/gampad/*ad-blk*",
+ ],
+ target: "empty-shim.txt",
+ types: ["image", "imageset", "xmlhttprequest"],
+ },
+ {
+ patterns: [
+ "*://securepubads.g.doubleclick.net/gampad/*xml_vmap1*",
+ "*://pubads.g.doubleclick.net/gampad/*xml_vmap1*",
+ ],
+ target: "vmad.xml",
+ types: ["image", "imageset", "xmlhttprequest"],
+ },
+ {
+ patterns: [
+ "*://vast.adsafeprotected.com/vast*",
+ "*://securepubads.g.doubleclick.net/gampad/*xml_vmap2*",
+ "*://pubads.g.doubleclick.net/gampad/*xml_vmap2*",
+ ],
+ target: "vast2.xml",
+ types: ["image", "imageset", "xmlhttprequest"],
+ },
+ {
+ patterns: [
+ "*://securepubads.g.doubleclick.net/gampad/*ad*",
+ "*://pubads.g.doubleclick.net/gampad/*ad*",
+ ],
+ target: "vast3.xml",
+ types: ["image", "imageset", "xmlhttprequest"],
+ },
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Eluminate",
+ platform: "all",
+ name: "Eluminate",
+ bug: "1503211",
+ file: "eluminate.js",
+ matches: ["*://libs.coremetrics.com/eluminate.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "FacebookSDK",
+ platform: "all",
+ branches: ["nightly:android"],
+ name: "Facebook SDK",
+ bug: "1226498",
+ file: "facebook-sdk.js",
+ logos: ["facebook.svg", "play.svg"],
+ matches: [
+ "*://connect.facebook.net/*/sdk.js*",
+ "*://connect.facebook.net/*/all.js*",
+ {
+ patterns: ["*://www.facebook.com/platform/impression.php*"],
+ target: "tracking-pixel.png",
+ types: ["image", "imageset", "xmlhttprequest"],
+ },
+ ],
+ needsShimHelpers: ["optIn", "getOptions"],
+ onlyIfBlockedByETP: true,
+ unblocksOnOptIn: [
+ "*://connect.facebook.net/*/sdk.js*",
+ "*://connect.facebook.net/*/all.js*",
+ "*://*.xx.fbcdn.net/*", // covers:
+ // "*://scontent-.*-\d.xx.fbcdn.net/*",
+ // "*://static.xx.fbcdn.net/rsrc.php/*",
+ "*://graph.facebook.com/v2*access_token*",
+ "*://graph.facebook.com/v*/me*",
+ "*://graph.facebook.com/*/picture*",
+ "*://www.facebook.com/*/plugins/login_button.php*",
+ "*://www.facebook.com/x/oauth/status*",
+ {
+ patterns: [
+ "*://www.facebook.com/*/plugins/video.php*",
+ "*://www.facebook.com/rsrc.php/*",
+ ],
+ branches: ["nightly"],
+ },
+ ],
+ },
+ {
+ id: "Fastclick",
+ platform: "all",
+ name: "Fastclick",
+ bug: "1738220",
+ file: "fastclick.js",
+ matches: [
+ "*://secure.cdn.fastclick.net/js/cnvr-launcher/*/launcher-stub.min.js*",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "GoogleAnalyticsAndTagManager",
+ platform: "all",
+ name: "Google Analytics and Tag Manager",
+ bug: "1713687",
+ file: "google-analytics-and-tag-manager.js",
+ matches: [
+ "*://www.google-analytics.com/analytics.js*",
+ "*://www.google-analytics.com/gtm/js*",
+ "*://www.googletagmanager.com/gtm.js*",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "GoogleAnalyticsECommercePlugin",
+ platform: "all",
+ name: "Google Analytics E-Commerce Plugin",
+ bug: "1620533",
+ file: "google-analytics-ecommerce-plugin.js",
+ matches: ["*://www.google-analytics.com/plugins/ua/ec.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "GoogleAnalyticsLegacy",
+ platform: "all",
+ name: "Google Analytics (legacy version)",
+ bug: "1487072",
+ file: "google-analytics-legacy.js",
+ matches: ["*://ssl.google-analytics.com/ga.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "GoogleIMA",
+ platform: "all",
+ name: "Google Interactive Media Ads",
+ bug: "1713690",
+ file: "google-ima.js",
+ matches: [
+ "*://s0.2mdn.net/instream/html5/ima3.js",
+ "*://imasdk.googleapis.com/js/sdkloader/ima3.js",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "GooglePageAd",
+ platform: "all",
+ name: "Google Page Ad",
+ bug: "1713692",
+ file: "google-page-ad.js",
+ matches: ["*://www.googleadservices.com/pagead/conversion_async.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "GooglePublisherTags",
+ platform: "all",
+ name: "Google Publisher Tags",
+ bug: "1713685",
+ file: "google-publisher-tags.js",
+ matches: [
+ "*://www.googletagservices.com/tag/js/gpt.js*",
+ "*://pagead2.googlesyndication.com/tag/js/gpt.js*",
+ "*://pagead2.googlesyndication.com/gpt/pubads_impl_*.js*",
+ "*://securepubads.g.doubleclick.net/tag/js/gpt.js*",
+ "*://securepubads.g.doubleclick.net/gpt/pubads_impl_*.js*",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Google SafeFrame",
+ platform: "all",
+ name: "Google SafeFrame",
+ bug: "1713691",
+ matches: [
+ {
+ patterns: [
+ "*://tpc.googlesyndication.com/safeframe/*/html/container.html",
+ "*://*.safeframe.googlesyndication.com/safeframe/*/html/container.html",
+ ],
+ target: "google-safeframe.html",
+ types: ["sub_frame"],
+ },
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "GoogleTrends",
+ platform: "all",
+ name: "Google Trends",
+ bug: "1624914",
+ custom: "google-trends-dfpi-fix",
+ onlyIfDFPIActive: true,
+ matches: [
+ {
+ patterns: ["*://trends.google.com/trends/embed*"],
+ types: ["sub_frame"],
+ },
+ ],
+ },
+ {
+ id: "IAM",
+ platform: "all",
+ name: "INFOnline IAM",
+ bug: "1761774",
+ file: "iam.js",
+ matches: ["*://script.ioam.de/iam.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ // keep this above AdSafeProtectedTrackingPixels
+ id: "IASPET",
+ platform: "all",
+ name: "Integral Ad Science PET",
+ bug: "1713701",
+ file: "iaspet.js",
+ matches: [
+ "*://cdn.adsafeprotected.com/iasPET.1.js",
+ "*://static.adsafeprotected.com/iasPET.1.js",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "MNet",
+ platform: "all",
+ name: "Media.net Ads",
+ bug: "1713703",
+ file: "empty-script.js",
+ matches: ["*://adservex.media.net/videoAds.js*"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Moat",
+ platform: "all",
+ name: "Moat",
+ bug: "1713704",
+ file: "moat.js",
+ matches: [
+ "*://*.moatads.com/*/moatad.js*",
+ "*://*.moatads.com/*/moatapi.js*",
+ "*://*.moatads.com/*/moatheader.js*",
+ "*://*.moatads.com/*/yi.js*",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Nielsen",
+ platform: "all",
+ name: "Nielsen",
+ bug: "1760754",
+ file: "nielsen.js",
+ matches: ["*://*.imrworldwide.com/v60.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Optimizely",
+ platform: "all",
+ name: "Optimizely",
+ bug: "1714431",
+ file: "optimizely.js",
+ matches: [
+ "*://cdn.optimizely.com/js/*.js",
+ "*://cdn.optimizely.com/public/*.js",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Rambler",
+ platform: "all",
+ name: "Rambler Authenticator",
+ bug: "1606428",
+ file: "rambler-authenticator.js",
+ matches: ["*://id.rambler.ru/rambler-id-helper/auth_events.js"],
+ needsShimHelpers: ["optIn"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "RichRelevance",
+ platform: "all",
+ name: "Rich Relevance",
+ bug: "1713725",
+ file: "rich-relevance.js",
+ matches: ["*://media.richrelevance.com/rrserver/js/1.2/p13n.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Firebase",
+ platform: "all",
+ name: "Firebase",
+ bug: "1771783",
+ onlyIfPrivateBrowsing: true,
+ runFirst: "firebase.js",
+ matches: [
+ // bugs 1750699, 1767407
+ "*://www.gstatic.com/firebasejs/*/firebase-messaging.js*",
+ ],
+ contentScripts: [
+ {
+ js: "firebase.js",
+ runAt: "document_start",
+ matches: [
+ "*://www.homedepot.ca/*", // bug 1778993
+ "*://orangerie.eu/*", // bug 1758442
+ "*://web.whatsapp.com/*", // bug 1767407
+ "*://www.tripadvisor.com/*", // bug 1779536
+ "*://www.office.com/*", // bug 1783921
+ ],
+ },
+ ],
+ },
+ {
+ id: "StickyAdsTV",
+ platform: "all",
+ name: "StickyAdsTV",
+ bug: "1717806",
+ matches: [
+ {
+ patterns: ["https://ads.stickyadstv.com/firefox-etp"],
+ target: "tracking-pixel.png",
+ types: ["image", "imageset", "xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ patterns: [
+ "*://ads.stickyadstv.com/auto-user-sync*",
+ "*://ads.stickyadstv.com/user-matching*",
+ ],
+ target: "https://ads.stickyadstv.com/firefox-etp",
+ types: ["image", "imageset", "xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ ],
+ },
+ {
+ id: "Vidible",
+ branch: ["nightly"],
+ platform: "all",
+ name: "Vidible",
+ bug: "1713710",
+ file: "vidible.js",
+ logos: ["play.svg"],
+ matches: [
+ "*://*.vidible.tv/*/vidible-min.js*",
+ "*://vdb-cdn-files.s3.amazonaws.com/*/vidible-min.js*",
+ ],
+ needsShimHelpers: ["optIn"],
+ onlyIfBlockedByETP: true,
+ unblocksOnOptIn: [
+ "*://delivery.vidible.tv/jsonp/pid=*/vid=*/*.js*",
+ "*://delivery.vidible.tv/placement/*",
+ "*://img.vidible.tv/prod/*",
+ "*://cdn-ssl.vidible.tv/prod/player/js/*.js",
+ "*://hlsrv.vidible.tv/prod/*.m3u8*",
+ "*://videos.vidible.tv/prod/*.key*",
+ "*://videos.vidible.tv/prod/*.mp4*",
+ "*://videos.vidible.tv/prod/*.webm*",
+ "*://videos.vidible.tv/prod/*.ts*",
+ ],
+ },
+ {
+ id: "Kinja",
+ platform: "all",
+ name: "Kinja",
+ bug: "1656171",
+ contentScripts: [
+ {
+ js: "kinja.js",
+ matches: [
+ "*://www.avclub.com/*",
+ "*://deadspin.com/*",
+ "*://gizmodo.com/*",
+ "*://jalopnik.com/*",
+ "*://jezebel.com/*",
+ "*://kotaku.com/*",
+ "*://lifehacker.com/*",
+ "*://www.theonion.com/*",
+ "*://www.theroot.com/*",
+ "*://thetakeout.com/*",
+ "*://theinventory.com/*",
+ ],
+ runAt: "document_start",
+ allFrames: true,
+ },
+ ],
+ onlyIfDFPIActive: true,
+ },
+ {
+ id: "MicrosoftLogin",
+ platform: "desktop",
+ name: "Microsoft Login",
+ bug: "1638383",
+ requestStorageAccessForRedirect: [
+ ["*://web.powerva.microsoft.com/*", "*://login.microsoftonline.com/*"],
+ ["*://teams.microsoft.com/*", "*://login.microsoftonline.com/*"],
+ ["*://*.teams.microsoft.us/*", "*://login.microsoftonline.us/*"],
+ ["*://www.msn.com/*", "*://login.microsoftonline.com/*"],
+ ],
+ contentScripts: [
+ {
+ js: "microsoftLogin.js",
+ matches: [
+ "*://web.powerva.microsoft.com/*",
+ "*://teams.microsoft.com/*",
+ "*://*.teams.microsoft.us/*",
+ "*://www.msn.com/*",
+ ],
+ runAt: "document_start",
+ },
+ ],
+ onlyIfDFPIActive: true,
+ },
+ {
+ id: "MicrosoftVirtualAssistant",
+ platform: "all",
+ name: "Microsoft Virtual Assistant",
+ bug: "1801277",
+ contentScripts: [
+ {
+ js: "microsoftVirtualAssistant.js",
+ matches: ["*://publisher.liveperson.net/*"],
+ runAt: "document_start",
+ allFrames: true,
+ },
+ ],
+ },
+ {
+ id: "History",
+ platform: "all",
+ name: "History.com",
+ bug: "1624853",
+ contentScripts: [
+ {
+ js: "history.js",
+ matches: ["*://play.history.com/*"],
+ runAt: "document_start",
+ },
+ ],
+ onlyIfDFPIActive: true,
+ },
+ {
+ id: "Crave.ca",
+ platform: "all",
+ name: "Crave.ca",
+ bug: "1746439",
+ contentScripts: [
+ {
+ js: "crave-ca.js",
+ matches: ["*://account.bellmedia.ca/login*service=crave*"],
+ runAt: "document_start",
+ },
+ ],
+ onlyIfDFPIActive: true,
+ },
+ {
+ id: "Instagram.com",
+ platform: "android",
+ name: "Instagram.com",
+ bug: "1804445",
+ contentScripts: [
+ {
+ js: "instagram.js",
+ matches: ["*://www.instagram.com/*"],
+ runAt: "document_start",
+ },
+ ],
+ onlyIfDFPIActive: true,
+ },
+ {
+ id: "MaxMindGeoIP",
+ platform: "all",
+ name: "MaxMind GeoIP",
+ bug: "1754389",
+ file: "maxmind-geoip.js",
+ matches: ["*://js.maxmind.com/js/apis/geoip2/*/geoip2.js"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "WebTrends",
+ platform: "all",
+ name: "WebTrends",
+ bug: "1766414",
+ file: "webtrends.js",
+ matches: [
+ "*://s.webtrends.com/js/advancedLinkTracking.js",
+ "*://s.webtrends.com/js/webtrends.js",
+ "*://s.webtrends.com/js/webtrends.min.js",
+ ],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ id: "Blogger",
+ platform: "all",
+ name: "Blogger",
+ bug: "1776869",
+ contentScripts: [
+ {
+ js: "blogger.js",
+ matches: ["*://www.blogger.com/comment/frame/*"],
+ runAt: "document_start",
+ allFrames: true,
+ },
+ {
+ js: "bloggerAccount.js",
+ matches: ["*://www.blogger.com/blog/*"],
+ runAt: "document_end",
+ },
+ ],
+ onlyIfDFPIActive: true,
+ },
+ {
+ // keep this below any other shims checking adsafeprotected URLs
+ id: "AdSafeProtectedTrackingPixels",
+ platform: "all",
+ name: "Ad Safe Protected tracking pixels",
+ bug: "1717806",
+ matches: [
+ {
+ patterns: ["https://static.adsafeprotected.com/firefox-etp-pixel"],
+ target: "tracking-pixel.png",
+ types: ["image", "imageset", "xmlhttprequest"],
+ },
+ {
+ patterns: ["https://static.adsafeprotected.com/firefox-etp-js"],
+ target: "empty-script.js",
+ types: ["xmlhttprequest"],
+ },
+ {
+ patterns: [
+ "*://*.adsafeprotected.com/*.gif*",
+ "*://*.adsafeprotected.com/*.png*",
+ ],
+ target: "https://static.adsafeprotected.com/firefox-etp-pixel",
+ types: ["image", "imageset", "xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ patterns: [
+ "*://*.adsafeprotected.com/*.js*",
+ "*://*.adsafeprotected.com/*/adj*",
+ "*://*.adsafeprotected.com/*/imp/*",
+ "*://*.adsafeprotected.com/*/Serving/*",
+ "*://*.adsafeprotected.com/*/unit/*",
+ "*://*.adsafeprotected.com/jload",
+ "*://*.adsafeprotected.com/jload?*",
+ "*://*.adsafeprotected.com/jsvid",
+ "*://*.adsafeprotected.com/jsvid?*",
+ "*://*.adsafeprotected.com/mon*",
+ "*://*.adsafeprotected.com/tpl",
+ "*://*.adsafeprotected.com/tpl?*",
+ "*://*.adsafeprotected.com/services/pub*",
+ ],
+ target: "https://static.adsafeprotected.com/firefox-etp-js",
+ types: ["image", "imageset", "xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ {
+ // note, fallback case seems to be an image
+ patterns: ["*://*.adsafeprotected.com/*"],
+ target: "https://static.adsafeprotected.com/firefox-etp-pixel",
+ types: ["image", "imageset", "xmlhttprequest"],
+ onlyIfBlockedByETP: true,
+ },
+ ],
+ },
+ {
+ id: "SpotifyEmbed",
+ platform: "all",
+ name: "SpotifyEmbed",
+ bug: "1792395",
+ contentScripts: [
+ {
+ js: "spotify-embed.js",
+ matches: ["*://open.spotify.com/embed/*"],
+ runAt: "document_start",
+ allFrames: true,
+ },
+ ],
+ onlyIfDFPIActive: true,
+ },
+ {
+ id: "tsn.ca",
+ platform: "all",
+ name: "tsn.ca login",
+ bug: "1802340",
+ contentScripts: [
+ {
+ js: "tsn-ca.js",
+ matches: ["*://account.bellmedia.ca/login*service=tsn*"],
+ runAt: "document_start",
+ },
+ ],
+ onlyIfDFPIActive: true,
+ },
+ {
+ id: "emeraude.my.salesforce.com",
+ platform: "all",
+ name: "Salesforce IndexedDB Script Access",
+ bug: "1855139",
+ contentScripts: [
+ {
+ js: "salesforce.js",
+ matches: ["*://emeraude.my.salesforce.com/*"],
+ runAt: "document_start",
+ allFrames: true,
+ },
+ ],
+ },
+];
+
+module.exports = AVAILABLE_SHIMS;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/ua_overrides.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/ua_overrides.js
new file mode 100644
index 0000000000..3645e962f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/data/ua_overrides.js
@@ -0,0 +1,1298 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser, module, require */
+
+// This is a hack for the tests.
+if (typeof InterventionHelpers === "undefined") {
+ var InterventionHelpers = require("../lib/intervention_helpers");
+}
+if (typeof UAHelpers === "undefined") {
+ var UAHelpers = require("../lib/ua_helpers");
+}
+
+/**
+ * For detailed information on our policies, and a documention on this format
+ * and its possibilites, please check the Mozilla-Wiki at
+ *
+ * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
+ */
+const AVAILABLE_UA_OVERRIDES = [
+ {
+ id: "testbed-override",
+ platform: "all",
+ domain: "webcompat-addon-testbed.herokuapp.com",
+ bug: "0000000",
+ config: {
+ hidden: true,
+ matches: ["*://webcompat-addon-testbed.herokuapp.com/*"],
+ uaTransformer: originalUA => {
+ return (
+ UAHelpers.getPrefix(originalUA) +
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36 for WebCompat"
+ );
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1577519 - directv.com - Create a UA override for directv.com for playback on desktop
+ * WebCompat issue #3846 - https://webcompat.com/issues/3846
+ *
+ * directv.com (attwatchtv.com) is blocking Firefox via UA sniffing. Spoofing as Chrome allows
+ * to access the site and playback works fine. This is former directvnow.com
+ */
+ id: "bug1577519",
+ platform: "desktop",
+ domain: "directv.com",
+ bug: "1577519",
+ config: {
+ matches: [
+ "*://*.attwatchtv.com/*",
+ "*://*.directv.com.ec/*", // bug 1827706
+ "*://*.directv.com/*",
+ ],
+ uaTransformer: originalUA => {
+ return (
+ UAHelpers.getPrefix(originalUA) +
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"
+ );
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1570108 - steamcommunity.com - UA override for steamcommunity.com
+ * WebCompat issue #34171 - https://webcompat.com/issues/34171
+ *
+ * steamcommunity.com blocks chat feature for Firefox users showing unsupported browser message.
+ * When spoofing as Chrome the chat works fine
+ */
+ id: "bug1570108",
+ platform: "desktop",
+ domain: "steamcommunity.com",
+ bug: "1570108",
+ config: {
+ matches: ["*://steamcommunity.com/chat*"],
+ uaTransformer: originalUA => {
+ return (
+ UAHelpers.getPrefix(originalUA) +
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
+ );
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1582582 - sling.com - UA override for sling.com
+ * WebCompat issue #17804 - https://webcompat.com/issues/17804
+ *
+ * sling.com blocks Firefox users showing unsupported browser message.
+ * When spoofing as Chrome playing content works fine
+ */
+ id: "bug1582582",
+ platform: "desktop",
+ domain: "sling.com",
+ bug: "1582582",
+ config: {
+ matches: ["https://watch.sling.com/*", "https://www.sling.com/*"],
+ uaTransformer: originalUA => {
+ return (
+ UAHelpers.getPrefix(originalUA) +
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
+ );
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1610026 - www.mobilesuica.com - UA override for www.mobilesuica.com
+ * WebCompat issue #4608 - https://webcompat.com/issues/4608
+ *
+ * mobilesuica.com showing unsupported message for Firefox users
+ * Spoofing as Chrome allows to access the page
+ */
+ id: "bug1610026",
+ platform: "all",
+ domain: "www.mobilesuica.com",
+ bug: "1610026",
+ config: {
+ matches: ["https://www.mobilesuica.com/*"],
+ uaTransformer: originalUA => {
+ return (
+ UAHelpers.getPrefix(originalUA) +
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
+ );
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1385206 - Create UA override for rakuten.co.jp on Firefox Android
+ * (Imported from ua-update.json.in)
+ *
+ * rakuten.co.jp serves a Desktop version if Firefox is included in the UA.
+ */
+ id: "bug1385206",
+ platform: "android",
+ domain: "rakuten.co.jp",
+ bug: "1385206",
+ config: {
+ matches: ["*://*.rakuten.co.jp/*"],
+ uaTransformer: originalUA => {
+ return originalUA.replace(/Firefox.+$/, "");
+ },
+ },
+ },
+ {
+ /*
+ * Bug 969844 - mobile.de sends desktop site to Firefox on Android
+ *
+ * mobile.de sends the desktop site to Firefox Mobile.
+ * Spoofing as Chrome works fine.
+ */
+ id: "bug969844",
+ platform: "android",
+ domain: "mobile.de",
+ bug: "969844",
+ config: {
+ matches: ["*://*.mobile.de/*"],
+ uaTransformer: _ => {
+ return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1509873 - zmags.com - Add UA override for secure.viewer.zmags.com
+ * WebCompat issue #21576 - https://webcompat.com/issues/21576
+ *
+ * The zmags viewer locks out Firefox Mobile with a "Browser unsupported"
+ * message, but tests showed that it works just fine with a Chrome UA.
+ * Outreach attempts were unsuccessful, and as the site has a relatively
+ * high rank, we alter the UA.
+ */
+ id: "bug1509873",
+ platform: "android",
+ domain: "zmags.com",
+ bug: "1509873",
+ config: {
+ matches: ["*://*.viewer.zmags.com/*"],
+ uaTransformer: originalUA => {
+ return (
+ UAHelpers.getPrefix(originalUA) +
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36"
+ );
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1574522 - UA override for enuri.com on Firefox for Android
+ * WebCompat issue #37139 - https://webcompat.com/issues/37139
+ *
+ * enuri.com returns a different template for Firefox on Android
+ * based on server side UA detection. This results in page content cut offs.
+ * Spoofing as Chrome fixes the issue
+ */
+ id: "bug1574522",
+ platform: "android",
+ domain: "enuri.com",
+ bug: "1574522",
+ config: {
+ matches: ["*://enuri.com/*"],
+ uaTransformer: _ => {
+ return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G900M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36";
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1574564 - UA override for ceskatelevize.cz on Firefox for Android
+ * WebCompat issue #15467 - https://webcompat.com/issues/15467
+ *
+ * ceskatelevize sets streamingProtocol depending on the User-Agent it sees
+ * in the request headers, returning DASH for Chrome, HLS for iOS,
+ * and Flash for Firefox Mobile. Since Mobile has no Flash, the video
+ * doesn't work. Spoofing as Chrome makes the video play
+ */
+ id: "bug1574564",
+ platform: "android",
+ domain: "ceskatelevize.cz",
+ bug: "1574564",
+ config: {
+ matches: ["*://*.ceskatelevize.cz/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1577267 - UA override for metfone.com.kh on Firefox for Android
+ * WebCompat issue #16363 - https://webcompat.com/issues/16363
+ *
+ * metfone.com.kh has a server side UA detection which returns desktop site
+ * for Firefox for Android. Spoofing as Chrome allows to receive mobile version
+ */
+ id: "bug1577267",
+ platform: "android",
+ domain: "metfone.com.kh",
+ bug: "1577267",
+ config: {
+ matches: ["*://*.metfone.com.kh/*"],
+ uaTransformer: originalUA => {
+ return (
+ UAHelpers.getPrefix(originalUA) +
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36"
+ );
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1598198 - User Agent extension for Samsung's galaxy.store URLs
+ *
+ * Samsung's galaxy.store shortlinks are supposed to redirect to a Samsung
+ * intent:// URL on Samsung devices, but to an error page on other brands.
+ * As we do not provide device info in our user agent string, this check
+ * fails, and even Samsung users land on an error page if they use Firefox
+ * for Android.
+ * This intervention adds a simple "Samsung" identifier to the User Agent
+ * on only the Galaxy Store URLs if the device happens to be a Samsung.
+ */
+ id: "bug1598198",
+ platform: "android",
+ domain: "galaxy.store",
+ bug: "1598198",
+ config: {
+ matches: [
+ "*://galaxy.store/*",
+ "*://dev.galaxy.store/*",
+ "*://stg.galaxy.store/*",
+ ],
+ uaTransformer: originalUA => {
+ if (!browser.systemManufacturer) {
+ return originalUA;
+ }
+
+ const manufacturer = browser.systemManufacturer.getManufacturer();
+ if (manufacturer && manufacturer.toLowerCase() === "samsung") {
+ return originalUA.replace("Mobile;", "Mobile; Samsung;");
+ }
+
+ return originalUA;
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1622063 - UA override for wp1-ext.usps.gov
+ * Webcompat issue #29867 - https://webcompat.com/issues/29867
+ *
+ * The Job Search site for USPS does not work for Firefox Mobile
+ * browsers (a 500 is returned).
+ */
+ id: "bug1622063",
+ platform: "android",
+ domain: "wp1-ext.usps.gov",
+ bug: "1622063",
+ config: {
+ matches: ["*://wp1-ext.usps.gov/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1646791 - bancosantander.es - Re-add UA override.
+ * Bug 1665129 - *.gruposantander.es - Add wildcard domains.
+ * WebCompat issue #33462 - https://webcompat.com/issues/33462
+ * SuMo request - https://support.mozilla.org/es/questions/1291085
+ *
+ * santanderbank expects UA to have 'like Gecko', otherwise it runs
+ * xmlDoc.onload whose support has been dropped. It results in missing labels in forms
+ * and some other issues. Adding 'like Gecko' fixes those issues.
+ */
+ id: "bug1646791",
+ platform: "all",
+ domain: "santanderbank.com",
+ bug: "1646791",
+ config: {
+ matches: [
+ "*://*.bancosantander.es/*",
+ "*://*.gruposantander.es/*",
+ "*://*.santander.co.uk/*",
+ ],
+ uaTransformer: originalUA => {
+ // The first line related to Firefox 100 is for Bug 1743445.
+ // [TODO]: Remove when bug 1743429 gets backed out.
+ return UAHelpers.capVersionTo99(originalUA).replace(
+ "Gecko",
+ "like Gecko"
+ );
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1651292 - UA override for www.jp.square-enix.com
+ * Webcompat issue #53018 - https://webcompat.com/issues/53018
+ *
+ * Unless the UA string contains "Chrome 66+", a section of
+ * www.jp.square-enix.com will show a never ending LOADING
+ * page.
+ */
+ id: "bug1651292",
+ platform: "android",
+ domain: "www.jp.square-enix.com",
+ bug: "1651292",
+ config: {
+ matches: ["*://www.jp.square-enix.com/music/sem/page/FF7R/ost/*"],
+ uaTransformer: originalUA => {
+ return originalUA + " Chrome/83";
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1666754 - Mobile UA override for lffl.org
+ * Bug 1665720 - lffl.org article page takes 2x as much time to load on Moto G
+ *
+ * This site returns desktop site based on server side UA detection.
+ * Spoofing as Chrome allows to get mobile experience
+ */
+ id: "bug1666754",
+ platform: "android",
+ domain: "lffl.org",
+ bug: "1666754",
+ config: {
+ matches: ["*://*.lffl.org/*"],
+ uaTransformer: () => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1704673 - Add UA override for app.xiaomi.com
+ * Webcompat issue #66163 - https://webcompat.com/issues/66163
+ *
+ * The page isn’t redirecting properly error message received.
+ * Spoofing as Chrome makes the page load
+ */
+ id: "bug1704673",
+ platform: "android",
+ domain: "app.xiaomi.com",
+ bug: "1704673",
+ config: {
+ matches: ["*://app.xiaomi.com/*"],
+ uaTransformer: () => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1712807 - Add UA override for www.dealnews.com
+ * Webcompat issue #39341 - https://webcompat.com/issues/39341
+ *
+ * The sites shows Firefox a different layout compared to Chrome.
+ * Spoofing as Chrome fixes this.
+ */
+ id: "bug1712807",
+ platform: "android",
+ domain: "www.dealnews.com",
+ bug: "1712807",
+ config: {
+ matches: ["*://www.dealnews.com/*"],
+ uaTransformer: () => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1719859 - Add UA override for saxoinvestor.fr
+ * Webcompat issue #74678 - https://webcompat.com/issues/74678
+ *
+ * The site blocks Firefox with a server-side UA sniffer. Appending a
+ * Chrome version segment to the UA makes it work.
+ */
+ id: "bug1719859",
+ platform: "android",
+ domain: "saxoinvestor.fr",
+ bug: "1719859",
+ config: {
+ matches: ["*://*.saxoinvestor.fr/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1738317 - Add UA override for vmos.cn
+ * Webcompat issue #90432 - https://github.com/webcompat/web-bugs/issues/90432
+ *
+ * Firefox for Android receives a desktop-only layout based on server-side
+ * UA sniffing. Spoofing as Chrome works fine.
+ */
+ id: "bug1738317",
+ platform: "android",
+ domain: "vmos.cn",
+ bug: "1738317",
+ config: {
+ matches: ["*://*.vmos.cn/*"],
+ uaTransformer: () => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1743627 - Add UA override for renaud-bray.com
+ * Webcompat issue #55276 - https://github.com/webcompat/web-bugs/issues/55276
+ *
+ * Firefox for Android depends on "Version/" being there in the UA string,
+ * or it'll throw a runtime error.
+ */
+ id: "bug1743627",
+ platform: "android",
+ domain: "renaud-bray.com",
+ bug: "1743627",
+ config: {
+ matches: ["*://*.renaud-bray.com/*"],
+ uaTransformer: originalUA => {
+ return originalUA + " Version/0";
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1743751 - Add UA override for slrclub.com
+ * Webcompat issue #91373 - https://github.com/webcompat/web-bugs/issues/91373
+ *
+ * On Firefox Android, the browser is receiving the desktop layout.
+ * Spoofing as Chrome works fine.
+ */
+ id: "bug1743751",
+ platform: "android",
+ domain: "slrclub.com",
+ bug: "1743751",
+ config: {
+ matches: ["*://*.slrclub.com/*"],
+ uaTransformer: () => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1743754 - Add UA override for slrclub.com
+ * Webcompat issue #86839 - https://github.com/webcompat/web-bugs/issues/86839
+ *
+ * On Firefox Android, the browser is failing a UA parsing on Firefox UA.
+ */
+ id: "bug1743754",
+ platform: "android",
+ domain: "workflow.base.vn",
+ bug: "1743754",
+ config: {
+ matches: ["*://workflow.base.vn/*"],
+ uaTransformer: () => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1743429 - Add UA override for sites broken with the Version 100 User Agent
+ *
+ * Some sites have issues with a UA string with Firefox version 100 or higher,
+ * so present as version 99 for now.
+ */
+ id: "bug1743429",
+ platform: "all",
+ domain: "Sites with known Version 100 User Agent breakage",
+ bug: "1743429",
+ config: {
+ matches: [
+ "*://411.ca/", // #121332
+ "*://*.mms.telekom.de/*", // #1800241
+ "*://ubank.com.au/*", // #104099
+ "*://wifi.sncf/*", // #100194
+ ],
+ uaTransformer: originalUA => {
+ return UAHelpers.capVersionTo99(originalUA);
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1753461 - UA override for serieson.naver.com
+ * Webcompat issue #99993 - https://webcompat.com/issues/97298
+ *
+ * The site locks out Firefox users unless a Chrome UA is given,
+ * and locks out Linux users as well (so we use Windows+Chrome).
+ */
+ id: "bug1753461",
+ platform: "desktop",
+ domain: "serieson.naver.com",
+ bug: "1753461",
+ config: {
+ matches: ["*://serieson.naver.com/*"],
+ uaTransformer: originalUA => {
+ return "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36";
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1771200 - UA override for animalplanet.com
+ * Webcompat issue #99993 - https://webcompat.com/issues/103727
+ *
+ * The videos are not playing and an error message is displayed
+ * in Firefox for Android, but work with Chrome UA
+ */
+ id: "bug1771200",
+ platform: "android",
+ domain: "animalplanet.com",
+ bug: "1771200",
+ config: {
+ matches: ["*://*.animalplanet.com/video/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1771200 - UA override for lazada.co.id
+ * Webcompat issue #106229 - https://webcompat.com/issues/106229
+ *
+ * The map is not playing and an error message is displayed
+ * in Firefox for Android, but work with Chrome UA
+ */
+ id: "bug1779059",
+ platform: "android",
+ domain: "lazada.co.id",
+ bug: "1779059",
+ config: {
+ matches: ["*://member-m.lazada.co.id/address/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1778168 - UA override for watch.antennaplus.gr
+ * Webcompat issue #106529 - https://webcompat.com/issues/106529
+ *
+ * The site's content is not loaded unless a Chrome UA is used,
+ * and breaks on Linux (so we claim Windows instead in that case).
+ */
+ id: "bug1778168",
+ platform: "desktop",
+ domain: "watch.antennaplus.gr",
+ bug: "1778168",
+ config: {
+ matches: ["*://watch.antennaplus.gr/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA({
+ desktopOS: "nonLinux",
+ });
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1776897 - UA override for www.edencast.fr
+ * Webcompat issue #106545 - https://webcompat.com/issues/106545
+ *
+ * The site's podcast audio player does not load unless a Chrome UA is used.
+ */
+ id: "bug1776897",
+ platform: "all",
+ domain: "www.edencast.fr",
+ bug: "1776897",
+ config: {
+ matches: ["*://www.edencast.fr/zoomcast*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1784361 - UA override for coldwellbankerhomes.com
+ * Webcompat issue #108535 - https://webcompat.com/issues/108535
+ *
+ * An error is thrown due to missing element, unless Chrome UA is used
+ */
+ id: "bug1784361",
+ platform: "android",
+ domain: "coldwellbankerhomes.com",
+ bug: "1784361",
+ config: {
+ matches: ["*://*.coldwellbankerhomes.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1786404 - UA override for business.help.royalmail.com
+ * Webcompat issue #109070 - https://webcompat.com/issues/109070
+ *
+ * Replacing `Firefox` with `FireFox` to evade one of their UA tests...
+ */
+ id: "bug1786404",
+ platform: "all",
+ domain: "business.help.royalmail.com",
+ bug: "1786404",
+ config: {
+ matches: ["*://business.help.royalmail.com/app/webforms/*"],
+ uaTransformer: originalUA => {
+ return originalUA.replace("Firefox", "FireFox");
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1819702 - UA override for feelgoodcontacts.com
+ * Webcompat issue #118030 - https://webcompat.com/issues/118030
+ *
+ * Spoof the UA to receive the mobile version instead
+ * of the broken desktop version for Android.
+ */
+ id: "bug1819702",
+ platform: "android",
+ domain: "feelgoodcontacts.com",
+ bug: "1819702",
+ config: {
+ matches: ["*://*.feelgoodcontacts.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1823966 - UA override for elearning.dmv.ca.gov
+ * Original report: https://bugzilla.mozilla.org/show_bug.cgi?id=1823785
+ */
+ id: "bug1823966",
+ platform: "all",
+ domain: "elearning.dmv.ca.gov",
+ bug: "1823966",
+ config: {
+ matches: ["*://*.elearning.dmv.ca.gov/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for admissions.nid.edu
+ * Webcompat issue #65753 - https://webcompat.com/issues/65753
+ */
+ id: "bug1827678-webc65753",
+ platform: "all",
+ domain: "admissions.nid.edu",
+ bug: "1827678",
+ config: {
+ matches: ["*://*.admissions.nid.edu/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for bankmandiri.co.id
+ * Webcompat issue #67924 - https://webcompat.com/issues/67924
+ */
+ id: "bug1827678-webc67924",
+ platform: "android",
+ domain: "bankmandiri.co.id",
+ bug: "1827678",
+ config: {
+ matches: ["*://*.bankmandiri.co.id/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for frankfred.com
+ * Webcompat issue #68007 - https://webcompat.com/issues/68007
+ */
+ id: "bug1827678-webc68007",
+ platform: "android",
+ domain: "frankfred.com",
+ bug: "1827678",
+ config: {
+ matches: ["*://*.frankfred.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for mobile.onvue.com
+ * Webcompat issue #68520 - https://webcompat.com/issues/68520
+ */
+ id: "bug1827678-webc68520",
+ platform: "android",
+ domain: "mobile.onvue.com",
+ bug: "1827678",
+ config: {
+ matches: ["*://mobile.onvue.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for avizia.com
+ * Webcompat issue #68635 - https://webcompat.com/issues/68635
+ */
+ id: "bug1827678-webc68635",
+ platform: "all",
+ domain: "avizia.com",
+ bug: "1827678",
+ config: {
+ matches: ["*://*.avizia.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for www.yourtexasbenefits.com
+ * Webcompat issue #76785 - https://webcompat.com/issues/76785
+ */
+ id: "bug1827678-webc76785",
+ platform: "android",
+ domain: "www.yourtexasbenefits.com",
+ bug: "1827678",
+ config: {
+ matches: ["*://www.yourtexasbenefits.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for www.free4talk.com
+ * Webcompat issue #77727 - https://webcompat.com/issues/77727
+ */
+ id: "bug1827678-webc77727",
+ platform: "android",
+ domain: "www.free4talk.com",
+ bug: "1827678",
+ config: {
+ matches: ["*://www.free4talk.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for watch.indee.tv
+ * Webcompat issue #77912 - https://webcompat.com/issues/77912
+ */
+ id: "bug1827678-webc77912",
+ platform: "all",
+ domain: "watch.indee.tv",
+ bug: "1827678",
+ config: {
+ matches: ["*://watch.indee.tv/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for viewer-ebook.books.com.tw
+ * Webcompat issue #80180 - https://webcompat.com/issues/80180
+ */
+ id: "bug1827678-webc80180",
+ platform: "all",
+ domain: "viewer-ebook.books.com.tw",
+ bug: "1827678",
+ config: {
+ matches: ["*://viewer-ebook.books.com.tw/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for jelly.jd.com
+ * Webcompat issue #83269 - https://webcompat.com/issues/83269
+ */
+ id: "bug1827678-webc83269",
+ platform: "android",
+ domain: "jelly.jd.com",
+ bug: "1827678",
+ config: {
+ matches: ["*://jelly.jd.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for kt.com
+ * Webcompat issue #119012 - https://webcompat.com/issues/119012
+ */
+ id: "bug1827678-webc119012",
+ platform: "all",
+ domain: "kt.com",
+ bug: "1827678",
+ config: {
+ matches: ["*://*.kt.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for oirsa.org
+ * Webcompat issue #119402 - https://webcompat.com/issues/119402
+ */
+ id: "bug1827678-webc119402",
+ platform: "all",
+ domain: "oirsa.org",
+ bug: "1827678",
+ config: {
+ matches: ["*://*.oirsa.org/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1827678 - UA override for onp.cloud.waterloo.ca
+ * Webcompat issue #120450 - https://webcompat.com/issues/120450
+ */
+ id: "bug1827678-webc120450",
+ platform: "all",
+ domain: "onp.cloud.waterloo.ca",
+ bug: "1827678",
+ config: {
+ matches: ["*://onp.cloud.waterloo.ca/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1830739 - UA override for casino sites
+ *
+ * The sites are showing unsupported message with the same UI
+ */
+ id: "bug1830739",
+ platform: "android",
+ domain: "casino sites",
+ bug: "1830739",
+ config: {
+ matches: [
+ "*://*.captainjackcasino.com/*", // 79490
+ "*://*.casinoextreme.eu/*", // 118175
+ "*://*.cryptoloko.com/*", // 117911
+ "*://*.123lobbygames.com/*", // 120027
+ "*://*.planet7casino.com/*", // 120609
+ "*://*.yebocasino.co.za/*", // 88409
+ "*://*.yabbycasino.com/*", // 108025
+ ],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1830821 - UA override for webcartop.jp
+ * Webcompat issue #113663 - https://webcompat.com/issues/113663
+ */
+ id: "bug1830821-webc113663",
+ platform: "android",
+ domain: "webcartop.jp",
+ bug: "1830821",
+ config: {
+ matches: ["*://*.webcartop.jp/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1830821 - UA override for enjoy.point.auone.jp
+ * Webcompat issue #90981 - https://webcompat.com/issues/90981
+ */
+ id: "bug1830821-webc90981",
+ platform: "android",
+ domain: "enjoy.point.auone.jp",
+ bug: "1830821",
+ config: {
+ matches: ["*://enjoy.point.auone.jp/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1836109 - UA override for watch.tonton.com.my
+ *
+ * The site's content is not loaded unless a Chrome UA is used.
+ */
+ id: "bug1836109",
+ platform: "all",
+ domain: "watch.tonton.com.my",
+ bug: "1836109",
+ config: {
+ matches: ["*://watch.tonton.com.my/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1836112 - UA override for www.capcut.cn
+ *
+ * The site's content is not loaded unless a Chrome UA is used.
+ */
+ id: "bug1836112",
+ platform: "all",
+ domain: "www.capcut.cn",
+ bug: "1836112",
+ config: {
+ matches: ["*://www.capcut.cn/editor*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1836116 - UA override for www.slushy.com
+ *
+ * The site's content is not loaded without a Chrome UA spoof.
+ */
+ id: "bug1836116",
+ platform: "all",
+ domain: "www.slushy.com",
+ bug: "1836116",
+ config: {
+ matches: ["*://www.slushy.com/*"],
+ uaTransformer: originalUA => {
+ return originalUA + " Chrome/113.0.0.0";
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1836135 - UA override for gts-pro.sdimedia.com
+ *
+ * The site's content is not loaded without a Chrome UA spoof.
+ */
+ id: "bug1836135",
+ platform: "all",
+ domain: "gts-pro.sdimedia.com",
+ bug: "1836135",
+ config: {
+ matches: ["*://gts-pro.sdimedia.com/*"],
+ uaTransformer: originalUA => {
+ return originalUA.replace("Firefox/", "Fx/") + " Chrome/113.0.0.0";
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1836140 - UA override for indices.iriworldwide.com
+ *
+ * The site's content is not loaded without a UA spoof.
+ */
+ id: "bug1836140",
+ platform: "all",
+ domain: "indices.iriworldwide.com",
+ bug: "1836140",
+ config: {
+ matches: ["*://indices.iriworldwide.com/covid19/*"],
+ uaTransformer: originalUA => {
+ return originalUA.replace("Firefox/", "Fx/");
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1836178 - UA override for atracker.pro
+ *
+ * The site's content is not loaded without a Chrome UA spoof.
+ */
+ id: "bug1836178",
+ platform: "all",
+ domain: "atracker.pro",
+ bug: "1836178",
+ config: {
+ matches: ["*://atracker.pro/*"],
+ uaTransformer: originalUA => {
+ return originalUA + " Chrome/113.0.0.0";
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1836182 - UA override for www.flatsatshadowglen.com
+ *
+ * The site's content is not loaded unless a Chrome UA is used.
+ */
+ id: "bug1836182",
+ platform: "all",
+ domain: "www.flatsatshadowglen.com",
+ bug: "1836182",
+ config: {
+ matches: ["*://*.flatsatshadowglen.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1849018 - UA override for carefirst.com
+ * Webcompat issue #125341 - https://webcompat.com/issues/125341
+ *
+ * The site is showing "Application Blocked" message
+ * for Firefox UA.
+ */
+ id: "bug1849018",
+ platform: "all",
+ domain: "carefirst.com",
+ bug: "1849018",
+ config: {
+ matches: ["*://*.carefirst.com/myaccount*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1850455 - UA override for frontgate.com
+ * Webcompat issue #36277 - https://webcompat.com/issues/36277
+ *
+ * The site is showing a desktop view to Firefox mobile user-agents
+ */
+ id: "bug1850455",
+ platform: "android",
+ domain: "frontgate.com",
+ bug: "1850455",
+ config: {
+ matches: ["*://*.frontgate.com/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1855088 - UA override for hrmis2.eghrmis.gov.my
+ * Webcompat issue #125039 - https://webcompat.com/issues/125039
+ *
+ * hrmis2.eghrmis.gov.my showing unsupported message for Firefox users
+ * Spoofing as Chrome allows to access the page
+ */
+ id: "bug1855088",
+ platform: "all",
+ domain: "hrmis2.eghrmis.gov.my",
+ bug: "1855088",
+ config: {
+ matches: ["*://hrmis2.eghrmis.gov.my/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1855102 - UA override for my.southerncross.co.nz
+ * Webcompat issue #121877 - https://webcompat.com/issues/121877
+ *
+ * Spoofing as Chrome for Android allows to access the page
+ */
+ id: "bug1855102",
+ platform: "android",
+ domain: "my.southerncross.co.nz",
+ bug: "1855102",
+ config: {
+ matches: ["*://my.southerncross.co.nz/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1864903 - UA override for Publitas catalogs
+ * Webcompat issue #128814 - https://webcompat.com/issues/128814
+ *
+ * Catalogs break without -moz-transform, since
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1855763 was
+ * shipped, spoofing as Chrome makes them work.
+ */
+ id: "bug1864903",
+ platform: "all",
+ domain: "Publitas catalogs",
+ bug: "1864903",
+ config: {
+ matches: [
+ "*://aktionen.metro.at/*",
+ "*://cataloagele.metro.ro/*",
+ "*://catalogs.metro-cc.ru/*",
+ "*://catalogues.metro.bg/*",
+ "*://catalogues.metro-cc.hr/*",
+ "*://catalogues.metro.ua/*",
+ "*://folders.makro.nl/*",
+ "*://katalog.metro.rs/*",
+ "*://katalogi.metro-kz.com/*",
+ "*://kataloglar.metro-tr.com/*",
+ "*://katalogus.metro.hu/*",
+ "*://letaky.makro.cz/*",
+ "*://letaky.metro.sk/*",
+ "*://ofertas.makro.es/*",
+ "*://oferte.metro.md/*",
+ "*://promotions-deals.metro.pk/*",
+ "*://promocoes.makro.pt/*",
+ "*://prospekt.aldi-sued.de/*",
+ "*://prospekte.metro.de/*",
+ "*://thematiques.metro.fr/*",
+ "*://volantino.metro.it/*",
+ "*://view.publitas.com/*",
+ "*://magazine.kruidvat.be/*",
+ "*://folder.kruidvat.nl/*",
+ ],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1864999 - UA override for autotrader.ca
+ * Webcompat issue #126822 - https://webcompat.com/issues/126822
+ *
+ * Spoofing as Chrome for Android makes filters work on the site
+ */
+ id: "bug1864999",
+ platform: "android",
+ domain: "autotrader.ca",
+ bug: "1864999",
+ config: {
+ matches: ["*://*.autotrader.ca/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1865000 - UA override for bmo.com
+ * Webcompat issue #127620 - https://webcompat.com/issues/127620
+ *
+ * Spoofing as Chrome removes the unsupported message and allows
+ * to proceed with application
+ */
+ id: "bug1865000",
+ platform: "all",
+ domain: "bmo.com",
+ bug: "1865000",
+ config: {
+ matches: ["*://*.bmo.com/main/personal/*/getting-started/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1865004 - UA override for digimart.net
+ * Webcompat issue #126647 - https://webcompat.com/issues/126647
+ *
+ * The site returns desktop layout on Firefox for Android
+ */
+ id: "bug1865004",
+ platform: "android",
+ domain: "digimart.net",
+ bug: "1865004",
+ config: {
+ matches: ["*://*.digimart.net/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1865007 - UA override for portal.circle.ms
+ * Webcompat issue #127739 - https://webcompat.com/issues/127739
+ *
+ * The site returns desktop layout on Firefox for Android
+ */
+ id: "bug1865007",
+ platform: "android",
+ domain: "portal.circle.ms",
+ bug: "1865007",
+ config: {
+ matches: ["*://*.circle.ms/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+ {
+ /*
+ * Bug 1884779 - UA override for
+ * Webcompat issue #134287 - https://webcompat.com/issues/134287
+ *
+ * The site returns desktop layout on Firefox for Android
+ */
+ id: "bug1884779",
+ platform: "android",
+ domain: "memurlar.net",
+ bug: "1884779",
+ config: {
+ matches: ["*://*.memurlar.net/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
+];
+
+module.exports = AVAILABLE_UA_OVERRIDES;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefs.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefs.js
new file mode 100644
index 0000000000..4a239772ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefs.js
@@ -0,0 +1,53 @@
+/* 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/. */
+
+"use strict";
+
+/* global ExtensionAPI, ExtensionCommon, Services */
+
+this.aboutConfigPrefs = class extends ExtensionAPI {
+ getAPI(context) {
+ const EventManager = ExtensionCommon.EventManager;
+ const extensionIDBase = context.extension.id.split("@")[0];
+ const extensionPrefNameBase = `extensions.${extensionIDBase}.`;
+
+ return {
+ aboutConfigPrefs: {
+ onPrefChange: new EventManager({
+ context,
+ name: "aboutConfigPrefs.onUAOverridesPrefChange",
+ register: (fire, name) => {
+ const prefName = `${extensionPrefNameBase}${name}`;
+ const callback = () => {
+ fire.async(name).catch(() => {}); // ignore Message Manager disconnects
+ };
+ Services.prefs.addObserver(prefName, callback);
+ return () => {
+ Services.prefs.removeObserver(prefName, callback);
+ };
+ },
+ }).api(),
+ async getBranch(branchName) {
+ const branch = `${extensionPrefNameBase}${branchName}.`;
+ return Services.prefs.getChildList(branch).map(pref => {
+ const name = pref.replace(branch, "");
+ return { name, value: Services.prefs.getBoolPref(pref) };
+ });
+ },
+ async getPref(name) {
+ try {
+ return Services.prefs.getBoolPref(
+ `${extensionPrefNameBase}${name}`
+ );
+ } catch (_) {
+ return undefined;
+ }
+ },
+ async setPref(name, value) {
+ Services.prefs.setBoolPref(`${extensionPrefNameBase}${name}`, value);
+ },
+ },
+ };
+ }
+};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefs.json b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefs.json
new file mode 100644
index 0000000000..fc13e3039a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefs.json
@@ -0,0 +1,94 @@
+[
+ {
+ "namespace": "aboutConfigPrefs",
+ "description": "experimental API extension to allow access to about:config preferences",
+ "events": [
+ {
+ "name": "onPrefChange",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The preference which changed"
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The preference to monitor"
+ }
+ ]
+ }
+ ],
+ "functions": [
+ {
+ "name": "getBranch",
+ "type": "function",
+ "description": "Get all child prefs for a branch",
+ "parameters": [
+ {
+ "name": "branchName",
+ "type": "string",
+ "description": "The branch name"
+ }
+ ],
+ "async": true
+ },
+ {
+ "name": "getPref",
+ "type": "function",
+ "description": "Get a preference's value",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The preference name"
+ }
+ ],
+ "async": true
+ },
+ {
+ "name": "getBoolPrefSync",
+ "type": "function",
+ "description": "Get a webcompat preference's boolean value synchronously",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The preference name"
+ },
+ {
+ "name": "defaultValue",
+ "type": "boolean",
+ "optional": true,
+ "description": "Default value to return if the pref is not set (defaults to false if omitted)"
+ }
+ ],
+ "returns": {
+ "type": "boolean",
+ "description": "returns the value of a boolean pref."
+ }
+ },
+ {
+ "name": "setPref",
+ "type": "function",
+ "description": "Set a preference's value",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The preference name"
+ },
+ {
+ "name": "value",
+ "type": "boolean",
+ "description": "The new value"
+ }
+ ],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefsChild.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefsChild.js
new file mode 100644
index 0000000000..c063905c98
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/aboutConfigPrefsChild.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+"use strict";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+this.aboutConfigPrefs = class extends ExtensionAPI {
+ getAPI(context) {
+ const extensionIDBase = context.extension.id.split("@")[0];
+ const extensionPrefNameBase = `extensions.${extensionIDBase}.`;
+
+ return {
+ aboutConfigPrefs: {
+ getBoolPrefSync(prefName, defaultValue = false) {
+ try {
+ return Services.prefs.getBoolPref(
+ `${extensionPrefNameBase}${prefName}`,
+ defaultValue
+ );
+ } catch (_) {
+ return defaultValue;
+ }
+ },
+ },
+ };
+ }
+};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/appConstants.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/appConstants.js
new file mode 100644
index 0000000000..2869f299a4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/appConstants.js
@@ -0,0 +1,28 @@
+/* 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/. */
+
+"use strict";
+
+/* global AppConstants, ExtensionAPI, XPCOMUtils */
+
+this.appConstants = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ appConstants: {
+ getReleaseBranch: () => {
+ if (AppConstants.NIGHTLY_BUILD) {
+ return "nightly";
+ } else if (AppConstants.MOZ_DEV_EDITION) {
+ return "dev_edition";
+ } else if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ return "early_beta_or_earlier";
+ } else if (AppConstants.RELEASE_OR_BETA) {
+ return "release_or_beta";
+ }
+ return "unknown";
+ },
+ },
+ };
+ }
+};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/appConstants.json b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/appConstants.json
new file mode 100644
index 0000000000..cf04915eca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/appConstants.json
@@ -0,0 +1,15 @@
+[
+ {
+ "namespace": "appConstants",
+ "description": "experimental API to expose some app constants",
+ "functions": [
+ {
+ "name": "getReleaseBranch",
+ "type": "function",
+ "description": "",
+ "async": true,
+ "parameters": []
+ }
+ ]
+ }
+]
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/matchPatterns.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/matchPatterns.js
new file mode 100644
index 0000000000..422cba5fc4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/matchPatterns.js
@@ -0,0 +1,30 @@
+/* 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/. */
+
+"use strict";
+
+/* global ExtensionAPI */
+
+this.matchPatterns = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ matchPatterns: {
+ getMatcher(patterns) {
+ const set = new MatchPatternSet(patterns);
+ return Cu.cloneInto(
+ {
+ matches: url => {
+ return set.matches(url);
+ },
+ },
+ context.cloneScope,
+ {
+ cloneFunctions: true,
+ }
+ );
+ },
+ },
+ };
+ }
+};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/matchPatterns.json b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/matchPatterns.json
new file mode 100644
index 0000000000..6fb4dc10fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/matchPatterns.json
@@ -0,0 +1,29 @@
+[
+ {
+ "namespace": "matchPatterns",
+ "description": "experimental API extension to expose MatchPattern functionality",
+ "functions": [
+ {
+ "name": "getMatcher",
+ "type": "function",
+ "description": "get a MatchPatternSet",
+ "parameters": [
+ {
+ "name": "patterns",
+ "description": "Array of string URL patterns to match",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "matches": { "type": "function" }
+ }
+ }
+ }
+ ]
+ }
+]
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/systemManufacturer.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/systemManufacturer.js
new file mode 100644
index 0000000000..b7dc68415c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/systemManufacturer.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+"use strict";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+this.systemManufacturer = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ systemManufacturer: {
+ getManufacturer() {
+ try {
+ return Services.sysinfo.getProperty("manufacturer");
+ } catch (_) {
+ return undefined;
+ }
+ },
+ },
+ };
+ }
+};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/systemManufacturer.json b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/systemManufacturer.json
new file mode 100644
index 0000000000..c64fccc46d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/systemManufacturer.json
@@ -0,0 +1,20 @@
+[
+ {
+ "namespace": "systemManufacturer",
+ "description": "experimental API extension to allow reading the device's manufacturer",
+ "functions": [
+ {
+ "name": "getManufacturer",
+ "type": "function",
+ "description": "Get the device's manufacturer",
+ "parameters": [],
+ "returns": {
+ "type": "string",
+ "properties": {},
+ "additionalProperties": { "type": "any" },
+ "description": "The manufacturer's name."
+ }
+ }
+ ]
+ }
+]
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.js
new file mode 100644
index 0000000000..0f5d9a4233
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.js
@@ -0,0 +1,216 @@
+/* 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/. */
+
+"use strict";
+
+/* global ExtensionAPI, ExtensionCommon, ExtensionParent, Services, XPCOMUtils */
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL", "ChannelWrapper"]);
+
+class AllowList {
+ constructor(id) {
+ this._id = id;
+ }
+
+ setShims(patterns, notHosts) {
+ this._shimPatterns = patterns;
+ this._shimMatcher = new MatchPatternSet(patterns || []);
+ this._shimNotHosts = notHosts || [];
+ return this;
+ }
+
+ setAllows(patterns, hosts) {
+ this._allowPatterns = patterns;
+ this._allowMatcher = new MatchPatternSet(patterns || []);
+ this._allowHosts = hosts || [];
+ return this;
+ }
+
+ shims(url, topHost) {
+ return (
+ this._shimMatcher?.matches(url) && !this._shimNotHosts?.includes(topHost)
+ );
+ }
+
+ allows(url, topHost) {
+ return (
+ this._allowMatcher?.matches(url) && this._allowHosts?.includes(topHost)
+ );
+ }
+}
+
+class Manager {
+ constructor() {
+ this._allowLists = new Map();
+ }
+
+ _getAllowList(id) {
+ if (!this._allowLists.has(id)) {
+ this._allowLists.set(id, new AllowList(id));
+ }
+ return this._allowLists.get(id);
+ }
+
+ _ensureStarted() {
+ if (this._classifierObserver) {
+ return;
+ }
+
+ this._unblockedChannelIds = new Set();
+ this._channelClassifier = Cc[
+ "@mozilla.org/url-classifier/channel-classifier-service;1"
+ ].getService(Ci.nsIChannelClassifierService);
+ this._classifierObserver = {};
+ this._classifierObserver.observe = (subject, topic, data) => {
+ switch (topic) {
+ case "http-on-stop-request": {
+ const { channelId } = subject.QueryInterface(Ci.nsIIdentChannel);
+ this._unblockedChannelIds.delete(channelId);
+ break;
+ }
+ case "urlclassifier-before-block-channel": {
+ const channel = subject.QueryInterface(
+ Ci.nsIUrlClassifierBlockedChannel
+ );
+ const { channelId, url } = channel;
+ let topHost;
+ try {
+ topHost = new URL(channel.topLevelUrl).hostname;
+ } catch (_) {
+ return;
+ }
+ // If anti-tracking webcompat is disabled, we only permit replacing
+ // channels, not fully unblocking them.
+ if (Manager.ENABLE_WEBCOMPAT) {
+ // if any allowlist unblocks the request entirely, we allow it
+ for (const allowList of this._allowLists.values()) {
+ if (allowList.allows(url, topHost)) {
+ this._unblockedChannelIds.add(channelId);
+ channel.allow();
+ return;
+ }
+ }
+ }
+ // otherwise, if any allowlist shims the request we say it's replaced
+ for (const allowList of this._allowLists.values()) {
+ if (allowList.shims(url, topHost)) {
+ this._unblockedChannelIds.add(channelId);
+ channel.replace();
+ return;
+ }
+ }
+ break;
+ }
+ }
+ };
+ Services.obs.addObserver(this._classifierObserver, "http-on-stop-request");
+ this._channelClassifier.addListener(this._classifierObserver);
+ }
+
+ stop() {
+ if (!this._classifierObserver) {
+ return;
+ }
+
+ Services.obs.removeObserver(
+ this._classifierObserver,
+ "http-on-stop-request"
+ );
+ this._channelClassifier.removeListener(this._classifierObserver);
+ delete this._channelClassifier;
+ delete this._classifierObserver;
+ }
+
+ wasChannelIdUnblocked(channelId) {
+ return this._unblockedChannelIds?.has(channelId);
+ }
+
+ allow(allowListId, patterns, hosts) {
+ this._ensureStarted();
+ this._getAllowList(allowListId).setAllows(patterns, hosts);
+ }
+
+ shim(allowListId, patterns, notHosts) {
+ this._ensureStarted();
+ this._getAllowList(allowListId).setShims(patterns, notHosts);
+ }
+
+ revoke(allowListId) {
+ this._allowLists.delete(allowListId);
+ }
+}
+var manager = new Manager();
+
+function getChannelId(context, requestId) {
+ const wrapper = ChannelWrapper.getRegisteredChannel(
+ requestId,
+ context.extension.policy,
+ context.xulBrowser.frameLoader.remoteTab
+ );
+ return wrapper?.channel?.QueryInterface(Ci.nsIIdentChannel)?.channelId;
+}
+
+var dFPIPrefName = "network.cookie.cookieBehavior";
+var dFPIPbPrefName = "network.cookie.cookieBehavior.pbmode";
+var dFPIStatus;
+function updateDFPIStatus() {
+ dFPIStatus = {
+ nonPbMode: 5 == Services.prefs.getIntPref(dFPIPrefName),
+ pbMode: 5 == Services.prefs.getIntPref(dFPIPbPrefName),
+ };
+}
+
+this.trackingProtection = class extends ExtensionAPI {
+ onShutdown(isAppShutdown) {
+ if (manager) {
+ manager.stop();
+ }
+ Services.prefs.removeObserver(dFPIPrefName, updateDFPIStatus);
+ Services.prefs.removeObserver(dFPIPbPrefName, updateDFPIStatus);
+ }
+
+ getAPI(context) {
+ Services.prefs.addObserver(dFPIPrefName, updateDFPIStatus);
+ Services.prefs.addObserver(dFPIPbPrefName, updateDFPIStatus);
+ updateDFPIStatus();
+
+ return {
+ trackingProtection: {
+ async shim(allowListId, patterns, notHosts) {
+ manager.shim(allowListId, patterns, notHosts);
+ },
+ async allow(allowListId, patterns, hosts) {
+ manager.allow(allowListId, patterns, hosts);
+ },
+ async revoke(allowListId) {
+ manager.revoke(allowListId);
+ },
+ async wasRequestUnblocked(requestId) {
+ if (!manager) {
+ return false;
+ }
+ const channelId = getChannelId(context, requestId);
+ if (!channelId) {
+ return false;
+ }
+ return manager.wasChannelIdUnblocked(channelId);
+ },
+ async isDFPIActive(isPrivate) {
+ if (isPrivate) {
+ return dFPIStatus.pbMode;
+ }
+ return dFPIStatus.nonPbMode;
+ },
+ },
+ };
+ }
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ Manager,
+ "ENABLE_WEBCOMPAT",
+ "privacy.antitracking.enableWebcompat",
+ false
+);
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.json b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.json
new file mode 100644
index 0000000000..c495f39add
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.json
@@ -0,0 +1,102 @@
+[
+ {
+ "namespace": "trackingProtection",
+ "description": "experimental API allow requests through ETP",
+ "functions": [
+ {
+ "name": "isDFPIActive",
+ "type": "function",
+ "description": "Returns whether dFPI is active for private/non-private browsing tabs",
+ "parameters": [
+ {
+ "type": "boolean",
+ "name": "isPrivate"
+ }
+ ],
+ "async": true
+ },
+ {
+ "name": "shim",
+ "type": "function",
+ "description": "Set specified URL patterns as intended to be shimmed",
+ "parameters": [
+ {
+ "name": "allowlistId",
+ "description": "Identfier for the allow-list, so it may be added-to or revoked",
+ "type": "string"
+ },
+ {
+ "name": "patterns",
+ "description": "Array of match patterns",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "notHosts",
+ "description": "Hosts on which to not shim these patterns",
+ "type": "array",
+ "optional": true,
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "allow",
+ "type": "function",
+ "description": "Set specified URL patterns as intended to be allowed through the content blocker for the specified top hosts",
+ "parameters": [
+ {
+ "name": "allowlistId",
+ "description": "Identfier for the allow-list, so it may be added-to or revoked",
+ "type": "string"
+ },
+ {
+ "name": "patterns",
+ "description": "Array of match patterns",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "hosts",
+ "description": "Hosts to allow the patterns on",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "async": true
+ },
+ {
+ "name": "revoke",
+ "type": "function",
+ "description": "Revokes the given allow-list entirely (both shims and allows)",
+ "parameters": [
+ {
+ "name": "allowListId",
+ "type": "string"
+ }
+ ],
+ "async": true
+ },
+ {
+ "name": "wasRequestUnblocked",
+ "type": "function",
+ "description": "Whether the given requestId was unblocked by any allowList",
+ "parameters": [
+ {
+ "name": "requestId",
+ "type": "string"
+ }
+ ],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug0000000-testbed-css-injection.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug0000000-testbed-css-injection.css
new file mode 100644
index 0000000000..566685c2da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug0000000-testbed-css-injection.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+#css-injection.red {
+ background-color: #0f0;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css
new file mode 100644
index 0000000000..e157dc6920
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css
@@ -0,0 +1,15 @@
+/* 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/. */
+
+/**
+ * apply.lloydsbank.co.uk - radio buttons are misplaced
+ * Bug #1575000 - https://bugzilla.mozilla.org/show_bug.cgi?id=1575000
+ * WebCompat issue #34969 - https://webcompat.com/issues/34969
+ *
+ * Radio buttons are displaced to the left due to positioning issue of ::before
+ * pseudo element, adding position relative to it's parent fixes the issue.
+ */
+.radio-content-field .radio.inline label span.text {
+ position: relative;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1610344-directv.com.co-hide-unsupported-message.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1610344-directv.com.co-hide-unsupported-message.css
new file mode 100644
index 0000000000..9f673bee95
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1610344-directv.com.co-hide-unsupported-message.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+/**
+ * directv.com.co - Browser is not supported message
+ * Bug #1610344 - https://bugzilla.mozilla.org/show_bug.cgi?id=1610344
+ * WebCompat issue #41822 - https://webcompat.com/issues/41822
+ *
+ * directv.com.co is showing a "This browser is not supported" message in
+ * Firefox. Our tests indicated that everything is working just fine, and our
+ * previous contact attempts have not been successful. This intervention
+ * hides the large red unsupported banner.
+ */
+.browser-compatible.compatible.incompatible {
+ display: none;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css
new file mode 100644
index 0000000000..fc1cb7489a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+/**
+ * missingmail.usps.com - Unable to mark the check-boxes from "Disclaimer and
+ * Terms and Conditions" section
+ * Bug #1644830 - https://bugzilla.mozilla.org/show_bug.cgi?id=1644830
+ * WebCompat issue #53950 - https://webcompat.com/issues/53950
+ *
+ * missingmail.usps.com runs into a case of bug 997189, where an absolutely
+ * positioned inline-block element with floating siblings is shifter to the
+ * right, and thus invisible.
+ */
+.mrc-custom-checkbox-container input {
+ margin-left: -3rem;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1651917-teletrader.com.body-transform-origin.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1651917-teletrader.com.body-transform-origin.css
new file mode 100644
index 0000000000..2c4429a301
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1651917-teletrader.com.body-transform-origin.css
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/**
+ * teletrader.com - content is shifted down and right
+ * Bug #1651917 - https://bugzilla.mozilla.org/show_bug.cgi?id=1651917
+ * WebCompat issue #55217 - https://webcompat.com/issues/55217
+ *
+ * The content is shifted down and right, because they use webkit prefixes
+ * for scaling and redefining the origin. Firefox doesn't support
+ * -webkit-transform-origin-x/y
+ * This is the object of https://bugzilla.mozilla.org/show_bug.cgi?id=1584881
+ * Adding transform-origin: 0 0; to body fixes the issue
+ */
+body {
+ transform-origin: 0 0;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1653075-livescience.com-scrollbar-width.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1653075-livescience.com-scrollbar-width.css
new file mode 100644
index 0000000000..ae14d1ec13
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1653075-livescience.com-scrollbar-width.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+/**
+ * livescience.com - a scrollbar covering navigation menu
+ * Bug #1653075 - https://bugzilla.mozilla.org/show_bug.cgi?id=1653075
+ *
+ * The scrollbar is covering navigation items and that makes them half hidden.
+ * There are some ::-webkit-scrollbar css rules applied to the scrollbar,
+ * making it thinner. Adding similar rules for Firefox fixes the issue.
+ */
+
+.trending__list {
+ scrollbar-width: thin;
+ scrollbar-color: #f9ae3b #f5f5f5;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1654907-reactine.ca-hide-unsupported.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1654907-reactine.ca-hide-unsupported.css
new file mode 100644
index 0000000000..6cecb6658a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1654907-reactine.ca-hide-unsupported.css
@@ -0,0 +1,16 @@
+/* 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/. */
+
+/**
+ * reactine.ca - Unsupported browser message
+ * Bug #1654907 - https://bugzilla.mozilla.org/show_bug.cgi?id=1654907
+ * WebCompat issue #55481 - https://webcompat.com/issues/55481
+ *
+ * reactine.ca is showing "Sorry this browser is not supported."
+ * message if Firefox for Android based on UA detection. Site seems
+ * to be working fine, so this intervention is to hide this message
+ */
+#browser-alert {
+ display: none !important;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1694470-myvidster.com-content-not-shown.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1694470-myvidster.com-content-not-shown.css
new file mode 100644
index 0000000000..adec7101ba
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1694470-myvidster.com-content-not-shown.css
@@ -0,0 +1,15 @@
+/* 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/. */
+
+/**
+ * m.myvidster.com - Content is not shown
+ * Bug #1694470 - https://bugzilla.mozilla.org/show_bug.cgi?id=1694470
+ * WebCompat issue #67308 - https://webcompat.com/issues/67308
+ *
+ * The site depends on Sencha Touch and should receive some specific
+ * -webkit-box-flex be working.
+ */
+#home_refresh_var {
+ -webkit-box-flex: 1;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1707795-office365-sheets-overscroll-disable.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1707795-office365-sheets-overscroll-disable.css
new file mode 100644
index 0000000000..7165ceb70f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1707795-office365-sheets-overscroll-disable.css
@@ -0,0 +1,12 @@
+/* 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/. */
+
+/**
+ * www.office.com - There is an overscroll effect on Excel sheets which is
+ * not a very pleasant user experience. This invention disables it.
+ */
+
+.ewr-sheetcontainer {
+ overscroll-behavior: none;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1712833-buskocchi.desuca.co.jp-fix-map-height.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1712833-buskocchi.desuca.co.jp-fix-map-height.css
new file mode 100644
index 0000000000..b4b8ca4a34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1712833-buskocchi.desuca.co.jp-fix-map-height.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+/**
+ * buskocchi.desuca.co.jp - Ensure that the map has a height so it is visible.
+ * Bug #1712833 - https://bugzilla.mozilla.org/show_bug.cgi?id=1712833
+ * WebCompat issue #50837 - https://webcompat.com/issues/50837
+ */
+
+form[name="main"] {
+ height: 100%;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1741234-patient.alphalabs.ca-height-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1741234-patient.alphalabs.ca-height-fix.css
new file mode 100644
index 0000000000..3765d1de1e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1741234-patient.alphalabs.ca-height-fix.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+/**
+ * patient.alphalabs.ca - "Continue" button is overlapped by displaced page footer
+ * Bug #1741234 - https://bugzilla.mozilla.org/show_bug.cgi?id=1741234
+ * WebCompat issue #93156 - https://webcompat.com/issues/93156
+ */
+
+body {
+ height: 100%;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1765947-veniceincoming.com-left-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1765947-veniceincoming.com-left-fix.css
new file mode 100644
index 0000000000..8c1ab7b2ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1765947-veniceincoming.com-left-fix.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+/**
+ * veniceincoming.com - site is not usable
+ * Bug #1765947 - https://bugzilla.mozilla.org/show_bug.cgi?id=1765947
+ * WebCompat issue #102133 - https://webcompat.com/issues/102133
+ */
+
+.tour-list .single-tour .mobile-link {
+ left: 0;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1770962-coldwellbankerhomes.com-image-height.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1770962-coldwellbankerhomes.com-image-height.css
new file mode 100644
index 0000000000..f9460951af
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1770962-coldwellbankerhomes.com-image-height.css
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/**
+ * coldwellbankerhomes.com - Property images are displayed squeezed
+ * Bug #1770962 - https://bugzilla.mozilla.org/show_bug.cgi?id=1770962
+ * WebCompat issue #102872 - https://webcompat.com/issues/102872
+ */
+
+.property-snapshot-psr-panel
+ .prop-pix
+ .photo-carousel.owl
+ .owl-stage-outer
+ .owl-item
+ img {
+ height: -moz-available;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1774490-rainews.it-gallery-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1774490-rainews.it-gallery-fix.css
new file mode 100644
index 0000000000..840e4ad7fb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1774490-rainews.it-gallery-fix.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+/**
+ * rainews.it - Image slideshow is not shown
+ * Bug #1774490 - https://bugzilla.mozilla.org/show_bug.cgi?id=1774490
+ * WebCompat issue #105402 - https://webcompat.com/issues/105402
+ */
+
+.photogallery-swiper .swiper-slide {
+ height: auto;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1784141-aveeno.com-acuvue.com-unsupported.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1784141-aveeno.com-acuvue.com-unsupported.css
new file mode 100644
index 0000000000..50b5bb2e90
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1784141-aveeno.com-acuvue.com-unsupported.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+/**
+ * aveeno.com and acuvue.com - Unsupported message is displayed
+ *
+ * Bug #1784141 - aveeno.com - https://bugzilla.mozilla.org/show_bug.cgi?id=1784141
+ * Bug #1804730 - acuvue.com - https://bugzilla.mozilla.org/show_bug.cgi?id=1804730
+ *
+ * WebCompat issue #103557 - https://webcompat.com/issues/103557
+ * WebCompat issue #103557 - https://webcompat.com/issues/110797
+ */
+
+#browser-alert {
+ display: none !important;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1784199-entrata-platform-unsupported.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1784199-entrata-platform-unsupported.css
new file mode 100644
index 0000000000..d20b84a99b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1784199-entrata-platform-unsupported.css
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/**
+ * aptsovation.com - Unsupported message is displayed on sites based on Entrata platform
+ * Bug #1784199 - https://bugzilla.mozilla.org/show_bug.cgi?id=1784199
+ * WebCompat issue #100131 - https://webcompat.com/issues/100131
+ */
+
+* {
+ color: unset;
+}
+
+#propertyProduct,
+.banner_overlay {
+ display: none;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1819678-nppes.cms.hhs.gov-unsupported-banner.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1819678-nppes.cms.hhs.gov-unsupported-banner.css
new file mode 100644
index 0000000000..ab9788ddc1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1819678-nppes.cms.hhs.gov-unsupported-banner.css
@@ -0,0 +1,15 @@
+/* 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/. */
+
+/**
+ * nppes.cms.hhs.gov - Firefox is an unsupported browser
+ * WebCompat issue #119017 - https://github.com/webcompat/web-bugs/issues/119017
+ *
+ * As everything seems to work just fine, this intervention simply hides the
+ * banner.
+ */
+
+#unsupportedDiv {
+ display: none !important;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1829949-tomshardware.com-scrollbar-width.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1829949-tomshardware.com-scrollbar-width.css
new file mode 100644
index 0000000000..f6bee8c878
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1829949-tomshardware.com-scrollbar-width.css
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/**
+ * tomshardware.com - a scrollbar covering navigation menu
+ * Bug #1829949 - https://bugzilla.mozilla.org/show_bug.cgi?id=1829949
+ * WebCompat issue #121170 - https://github.com/webcompat/web-bugs/issues/121170
+ *
+ * The scrollbar is covering navigation items and that makes them half hidden.
+ * There are some ::-webkit-scrollbar css rules applied to the scrollbar,
+ * making it thinner. Adding similar rules for Firefox fixes the issue.
+ */
+
+.trending__list {
+ scrollbar-width: thin;
+ scrollbar-color: #000 #f5f5f5;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830747-babbel.com-page-height.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830747-babbel.com-page-height.css
new file mode 100644
index 0000000000..1f7585e637
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830747-babbel.com-page-height.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+/**
+ * my.babbel.com - "Next" button is not visible
+ * Bug #1830747 - https://bugzilla.mozilla.org/show_bug.cgi?id=1830747
+ * WebCompat issue #119212 - https://github.com/webcompat/web-bugs/issues/119212
+ *
+ * The next button on the bottom of the page is not visible in Firefox,
+ * but visible in Chrome since the site is using -webkit-fill-available rule.
+ * Adding height: 100% to the page wrapper allows to see the button.
+ */
+
+[data-main] {
+ height: 100%;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830752-afisha.ru-slider-pointer-events.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830752-afisha.ru-slider-pointer-events.css
new file mode 100644
index 0000000000..4a9950b810
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830752-afisha.ru-slider-pointer-events.css
@@ -0,0 +1,27 @@
+/* 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/. */
+
+/**
+ * afisha.ru - Slider not working
+ * Bug #1830752 - https://bugzilla.mozilla.org/show_bug.cgi?id=1830752
+ * WebCompat issue #120455 - https://github.com/webcompat/web-bugs/issues/120455
+ *
+ * The range slider for price filtering is not working because of pointer-events:none applied
+ * on the slider element. It's working in Chrome because of webkit specific rules
+ * set with -moz-range-thumb that override the pointer events on the slider thumb to auto.
+ * Setting the same rule with -moz-range-thumb makes the slider to work.
+ */
+
+.XdRPG,
+.gkKBB {
+ pointer-events: auto;
+}
+.XdRPG::-moz-range-thumb,
+.gkKBB::-moz-range-thumb {
+ background-color: #ff3118;
+ border-color: #ff3118;
+ border-radius: 50%;
+ cursor: pointer;
+ pointer-events: auto;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830761-91mobiles.com-content-height.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830761-91mobiles.com-content-height.css
new file mode 100644
index 0000000000..f2e24346e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830761-91mobiles.com-content-height.css
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/**
+ * 91mobiles.com - Text overlapping
+ * Bug #1830761 - https://bugzilla.mozilla.org/show_bug.cgi?id=1830761
+ * WebCompat issue #117029 - https://github.com/webcompat/web-bugs/issues/117029
+ *
+ * The content overlaps dedicated space since Firefox honors small heights on <td>
+ * due to https://bugzilla.mozilla.org/show_bug.cgi?id=1461852. Setting the height to
+ * fit-content makes it work as expected.
+ */
+
+#fixed-table tr td .cmp-summary-box,
+.cmpr-table .textpanel {
+ height: fit-content;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830796-copyleaks.com-hide-unsupported.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830796-copyleaks.com-hide-unsupported.css
new file mode 100644
index 0000000000..753835de6a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830796-copyleaks.com-hide-unsupported.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+/**
+ * copyleaks.com - Unsupported message
+ * Bug #1830796 - https://bugzilla.mozilla.org/show_bug.cgi?id=1830796
+ * WebCompat issue #121395 - https://github.com/webcompat/web-bugs/issues/121395
+ */
+
+#outdated {
+ display: none !important;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830810-interceramic.com-hide-unsupported.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830810-interceramic.com-hide-unsupported.css
new file mode 100644
index 0000000000..7726140d0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830810-interceramic.com-hide-unsupported.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+/**
+ * interceramic.com - Unsupported message
+ * Bug #1830810 - https://bugzilla.mozilla.org/show_bug.cgi?id=1830810
+ * WebCompat issue #117807 - https://github.com/webcompat/web-bugs/issues/117807
+ */
+
+#ff-modal {
+ display: none !important;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830813-page.onstove.com-hide-unsupported.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830813-page.onstove.com-hide-unsupported.css
new file mode 100644
index 0000000000..23069c4259
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1830813-page.onstove.com-hide-unsupported.css
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/**
+ * onstove.com - Unsupported message
+ * Bug #1830813 - https://bugzilla.mozilla.org/show_bug.cgi?id=1830813
+ * WebCompat issue #116760 - https://github.com/webcompat/web-bugs/issues/116760
+ */
+
+.gnb-alerts.gnb-old-browser {
+ max-height: 0;
+}
+
+.isCampaign .gnb-stove.gnb-default-fixed,
+.isCampaign .layout.layout-base .layout-header {
+ height: 68px;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1836103-autostar-novoross.ru-make-map-taller.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1836103-autostar-novoross.ru-make-map-taller.css
new file mode 100644
index 0000000000..70fc01b86f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1836103-autostar-novoross.ru-make-map-taller.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+/**
+ * autostar-novoross.ru - Map is not as tall as expected
+ * Bug #1836103 - https://bugzilla.mozilla.org/show_bug.cgi?id=1836103
+ * WebCompat issue #80763 - https://github.com/webcompat/web-bugs/issues/80763
+ */
+
+.t396 .tn-atom {
+ height: 100%;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1836105-cnn.com-fix-blank-pages-when-printing.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1836105-cnn.com-fix-blank-pages-when-printing.css
new file mode 100644
index 0000000000..db6f018619
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1836105-cnn.com-fix-blank-pages-when-printing.css
@@ -0,0 +1,19 @@
+/* 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/. */
+
+/**
+ * cnn.com - Printing in portrait mode results in blank pages
+ * Bug #1836105 - https://bugzilla.mozilla.org/show_bug.cgi?id=1836105
+ *
+ * Disable some of CNN's giant styles for height, top and margin-bottom,
+ * since they break print layout in Firefox (as per bug 1830307)
+ */
+
+@media print {
+ .header__wrapper-outer {
+ height: initial !important;
+ top: initial !important;
+ margin-bottom: initial !important;
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848711-vio.com-page-height.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848711-vio.com-page-height.css
new file mode 100644
index 0000000000..85ad4d3bee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848711-vio.com-page-height.css
@@ -0,0 +1,16 @@
+/* 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/. */
+
+/**
+ * vio.com - Map is not displayed
+ * Bug #1848711 - https://bugzilla.mozilla.org/show_bug.cgi?id=1848711
+ * WebCompat issue #118126 - https://github.com/webcompat/web-bugs/issues/118126
+ *
+ * vio.com uses min-height: -webkit-fill-available; on the canvas map parent
+ * element, which is not supported in Firefox, so the element has zero height.
+ */
+
+.ec-44lvm8 {
+ height: 100%;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848713-cleanrider.com-slider.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848713-cleanrider.com-slider.css
new file mode 100644
index 0000000000..5cfb2263d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848713-cleanrider.com-slider.css
@@ -0,0 +1,20 @@
+/* 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/. */
+
+/**
+ * www.cleanrider.com - The price slider does not work
+ * Bug #1848713 - https://bugzilla.mozilla.org/show_bug.cgi?id=1848713
+ * WebCompat issue #123642 - https://github.com/webcompat/web-bugs/issues/123642
+ *
+ * the site disables pointer events on the slider and only allows them
+ * in -webkit-slider-thumb style which is not supported in Firefox and
+ * depends on https://bugzilla.mozilla.org/show_bug.cgi?id=1735575
+ */
+
+input[type="range"]::-moz-range-thumb {
+ -webkit-appearance: none;
+ height: 24px;
+ pointer-events: all;
+ width: 24px;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848849-theaa.com-printing-mode-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848849-theaa.com-printing-mode-fix.css
new file mode 100644
index 0000000000..48ab165347
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1848849-theaa.com-printing-mode-fix.css
@@ -0,0 +1,19 @@
+/* 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/. */
+
+/**
+ * www.theaa.com - Print preview is blank
+ * Bug #1848849 - https://bugzilla.mozilla.org/show_bug.cgi?id=1848849
+ * WebCompat issue #117996 - https://github.com/webcompat/web-bugs/issues/117996
+ *
+ * The site is adding a style rule of size: 16px for @page, which is very small and Chrome
+ * is ignoring the rule, while Firefox honours it.
+ * Depends on https://bugzilla.mozilla.org/show_bug.cgi?id=1807985
+ */
+
+@media print {
+ @page {
+ size: a3 !important;
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1849019-axa-assistance.pl-datepicker-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1849019-axa-assistance.pl-datepicker-fix.css
new file mode 100644
index 0000000000..239bbbfe5b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1849019-axa-assistance.pl-datepicker-fix.css
@@ -0,0 +1,19 @@
+/* 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/. */
+
+/**
+ * www.axa-assistance.pl - The buttons for picking a date are not working
+ * Bug #1849019 - https://bugzilla.mozilla.org/show_bug.cgi?id=1849019
+ * WebCompat issue #123068 - https://github.com/webcompat/web-bugs/issues/123068
+ *
+ * The site delegates click events to a hidden date input to open its UI,
+ * which doesn't work in Firefox, but works in Chrome
+ * Depends on https://bugzilla.mozilla.org/show_bug.cgi?id=1840668
+ */
+
+.hiddenDate {
+ width: 100%;
+ height: 100%;
+ z-index: auto;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1849388-kucharkaprodceru.cz-scroll-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1849388-kucharkaprodceru.cz-scroll-fix.css
new file mode 100644
index 0000000000..b619b7f778
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1849388-kucharkaprodceru.cz-scroll-fix.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+/**
+ * www.kucharkaprodceru.cz - Scrolling up triggers pull-refresh
+ * Bug #1849388 - https://bugzilla.mozilla.org/show_bug.cgi?id=1849388
+ * WebCompat issue #123452 - https://github.com/webcompat/web-bugs/issues/123452
+ *
+ * The site uses older version of jquery.nicescroll.js which adds
+ * overflow-y: auto to the body element only in Firefox. Resetting
+ * makes the scrolling work as expected
+ */
+
+body {
+ overflow-y: auto !important;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1868345-tvmovie.de-scroll-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1868345-tvmovie.de-scroll-fix.css
new file mode 100644
index 0000000000..de932ee597
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1868345-tvmovie.de-scroll-fix.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+/**
+ * www.tvmovie.de - Can't scroll tv program on tvmovie.de
+ * Bug #1868345 - https://bugzilla.mozilla.org/show_bug.cgi?id=1868345
+ * Bug #1840166 - https://bugzilla.mozilla.org/show_bug.cgi?id=1840166
+ * WebCompat issue #123254 - https://github.com/webcompat/web-bugs/issues/123254
+ *
+ * Due to an unfortunate combination of table layout issues and schroll
+ * anchoring issues, the page keeps scrolling down forever, with no ability
+ * to scroll back up.
+ */
+:root {
+ overflow-anchor: none;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1877346-offerup.com-infinite-scroll-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1877346-offerup.com-infinite-scroll-fix.css
new file mode 100644
index 0000000000..24681686f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1877346-offerup.com-infinite-scroll-fix.css
@@ -0,0 +1,20 @@
+/* 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/. */
+
+/**
+ * offerup.com - Infinite scroll doesn't work
+ * Bug #1720060 - https://bugzilla.mozilla.org/show_bug.cgi?id=1720060
+ *
+ * The site uses IntersectionObserver to show new items on the page,
+ * as the page being scrolled and it doesn't work with with
+ * Dynamic Toolbar enabled. Adding an empty element after the content
+ * to make up for the height of the dynamic toolbar makes it work.
+ */
+
+#__next::after {
+ display: block;
+ width: 100%;
+ height: 10px;
+ content: "";
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1884842-foodora.cz-height-fix.css b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1884842-foodora.cz-height-fix.css
new file mode 100644
index 0000000000..eb43bc5203
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1884842-foodora.cz-height-fix.css
@@ -0,0 +1,25 @@
+/* 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/. */
+
+/**
+ * foodora.cz - Unable to pick an address
+ * Bug #1884842 - https://bugzilla.mozilla.org/show_bug.cgi?id=1884842
+ * WebCompat issue #130975 - https://github.com/webcompat/web-bugs/issues/130975
+ *
+ * Unable to confirm an adress on the delivery site due to button being outside
+ * the viewport and parent container unscrollable.
+ * Depends on https://bugzilla.mozilla.org/show_bug.cgi?id=1481876
+ */
+
+@media (max-width: 527.98px) {
+ .bds-c-modal--is-mobile-bottom-sheet .bds-c-modal__content-window {
+ height: 100%;
+ }
+}
+
+@media (min-height: 528px) {
+ .map-modal__map.map-box {
+ height: 362px;
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug0000000-testbed-js-injection.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug0000000-testbed-js-injection.js
new file mode 100644
index 0000000000..7a192d6c41
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug0000000-testbed-js-injection.js
@@ -0,0 +1,15 @@
+/* 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/. */
+
+"use strict";
+
+/* globals exportFunction */
+
+Object.defineProperty(window.wrappedJSObject, "isTestFeatureSupported", {
+ get: exportFunction(function () {
+ return true;
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1448747-fastclick-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1448747-fastclick-shim.js
new file mode 100644
index 0000000000..7a8e85f538
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1448747-fastclick-shim.js
@@ -0,0 +1,35 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1448747 - Neutralize FastClick
+ *
+ * The patch is applied on sites using FastClick library
+ * to make sure `FastClick.notNeeded` returns `true`.
+ * This allows to disable FastClick and fix various breakage caused
+ * by the library (mainly non-functioning drop-down lists).
+ */
+
+/* globals exportFunction */
+
+(function () {
+ const proto = CSS2Properties.prototype.wrappedJSObject;
+ const descriptor = Object.getOwnPropertyDescriptor(proto, "touchAction");
+ const { get } = descriptor;
+
+ descriptor.get = exportFunction(function () {
+ try {
+ throw Error();
+ } catch (e) {
+ if (e.stack?.includes("notNeeded")) {
+ return "none";
+ }
+ }
+ return get.call(this);
+ }, window);
+
+ Object.defineProperty(proto, "touchAction", descriptor);
+})();
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js
new file mode 100644
index 0000000000..40e17b4a36
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1452707 - Build site patch for ib.absa.co.za
+ * WebCompat issue #16401 - https://webcompat.com/issues/16401
+ *
+ * The online banking at ib.absa.co.za detect if window.controllers is a
+ * non-falsy value to detect if the current browser is Firefox or something
+ * else. In bug 1448045, this shim has been disabled for Firefox Nightly 61+,
+ * which breaks the UA detection on this site and results in a "Browser
+ * unsuppored" error message.
+ *
+ * This site patch simply sets window.controllers to a string, resulting in
+ * their check to work again.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "window.controllers has been shimmed for compatibility reasons. See https://webcompat.com/issues/16401 for details."
+);
+
+Object.defineProperty(window.wrappedJSObject, "controllers", {
+ get: exportFunction(function () {
+ return true;
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js
new file mode 100644
index 0000000000..06085acc5a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1457335 - histography.io - Override UA & navigator.vendor
+ * WebCompat issue #1804 - https://webcompat.com/issues/1804
+ *
+ * This site is using a strict matching of navigator.userAgent and
+ * navigator.vendor to allow access for Safari or Chrome. Here, we set the
+ * values appropriately so we get recognized as Chrome.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "The user agent has been overridden for compatibility reasons. See https://webcompat.com/issues/1804 for details."
+);
+
+const CHROME_UA = navigator.userAgent + " Chrome for WebCompat";
+
+Object.defineProperty(window.navigator.wrappedJSObject, "userAgent", {
+ get: exportFunction(function () {
+ return CHROME_UA;
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
+
+Object.defineProperty(window.navigator.wrappedJSObject, "vendor", {
+ get: exportFunction(function () {
+ return "Google Inc.";
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js
new file mode 100644
index 0000000000..5aa72e75ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1472075 - Build UA override for Bank of America for OSX & Linux
+ * WebCompat issue #2787 - https://webcompat.com/issues/2787
+ *
+ * BoA is showing a red warning to Linux and macOS users, while accepting
+ * Windows users without warning. From our side, there is no difference here
+ * and we receive a lot of user complains about the warnings, so we spoof
+ * as Firefox on Windows in those cases.
+ */
+
+/* globals exportFunction */
+
+if (!navigator.platform.includes("Win")) {
+ console.info(
+ "The user agent has been overridden for compatibility reasons. See https://webcompat.com/issues/2787 for details."
+ );
+
+ const WINDOWS_UA = navigator.userAgent.replace(
+ /\(.*; rv:/i,
+ "(Windows NT 10.0; Win64; x64; rv:"
+ );
+
+ Object.defineProperty(window.navigator.wrappedJSObject, "userAgent", {
+ get: exportFunction(function () {
+ return WINDOWS_UA;
+ }, window),
+
+ set: exportFunction(function () {}, window),
+ });
+
+ Object.defineProperty(window.navigator.wrappedJSObject, "appVersion", {
+ get: exportFunction(function () {
+ return "appVersion";
+ }, window),
+
+ set: exportFunction(function () {}, window),
+ });
+
+ Object.defineProperty(window.navigator.wrappedJSObject, "platform", {
+ get: exportFunction(function () {
+ return "Win64";
+ }, window),
+
+ set: exportFunction(function () {}, window),
+ });
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js
new file mode 100644
index 0000000000..5c757466c6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js
@@ -0,0 +1,32 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * m.tailieu.vn - Override PDFJS.disableWorker to be true
+ * WebCompat issue #39057 - https://webcompat.com/issues/39057
+ *
+ * Custom viewer built with PDF.js is not working in Firefox for Android
+ * Disabling worker to match Chrome behavior fixes the issue
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "window.PDFJS.disableWorker has been set to true for compatibility reasons. See https://webcompat.com/issues/39057 for details."
+);
+
+let globals = {};
+
+Object.defineProperty(window.wrappedJSObject, "PDFJS", {
+ get: exportFunction(function () {
+ return globals;
+ }, window),
+
+ set: exportFunction(function (value = {}) {
+ globals = value;
+ globals.disableWorker = true;
+ }, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1722955-frontgate.com-ua-override.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1722955-frontgate.com-ua-override.js
new file mode 100644
index 0000000000..577a55450a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1722955-frontgate.com-ua-override.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+"use strict";
+
+/*
+ * Bug 1722955 - Add UA override for frontgate.com
+ * Webcompat issue #36277 - https://github.com/webcompat/web-bugs/issues/36277
+ *
+ * The website is sending the desktop version to Firefox on mobile devices
+ * based on UA sniffing. Spoofing as Chrome fixes this.
+ */
+
+/* globals exportFunction, UAHelpers */
+
+console.info(
+ "The user agent has been overridden for compatibility reasons. See https://webcompat.com/issues/36277 for details."
+);
+
+UAHelpers.overrideWithDeviceAppropriateChromeUA();
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1724868-news.yahoo.co.jp-ua-override.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1724868-news.yahoo.co.jp-ua-override.js
new file mode 100644
index 0000000000..ab7b76c799
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1724868-news.yahoo.co.jp-ua-override.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1724868 - news.yahoo.co.jp - Override UA
+ * WebCompat issue #82605 - https://webcompat.com/issues/82605
+ *
+ * Yahoo Japan news doesn't allow playing video in Firefox on Android
+ * as they don't have it in their support matrix. They check UA override twice
+ * and display different ui with the same error. Changing UA to Chrome via
+ * content script allows playing the videos.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "The user agent has been overridden for compatibility reasons. See https://webcompat.com/issues/82605 for details."
+);
+
+Object.defineProperty(window.navigator.wrappedJSObject, "userAgent", {
+ get: exportFunction(function () {
+ return "Mozilla/5.0 (Linux; Android 11; Pixel 4a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Mobile Safari/537.36";
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1739489-draftjs-beforeinput.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1739489-draftjs-beforeinput.js
new file mode 100644
index 0000000000..5ae55ec6f3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1739489-draftjs-beforeinput.js
@@ -0,0 +1,116 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1739489 - Entering an emoji using the MacOS IME "crashes" Draft.js editors.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "textInput event has been remapped to beforeinput for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1739489 for details."
+);
+
+window.wrappedJSObject.TextEvent = window.wrappedJSObject.InputEvent;
+
+const { CustomEvent, Event, EventTarget } = window.wrappedJSObject;
+var Remapped = [
+ [CustomEvent, "constructor"],
+ [Event, "constructor"],
+ [Event, "initEvent"],
+ [EventTarget, "addEventListener"],
+ [EventTarget, "removeEventListener"],
+];
+
+for (const [obj, name] of Remapped) {
+ const { prototype } = obj;
+ const orig = prototype[name];
+ Object.defineProperty(prototype, name, {
+ value: exportFunction(function (type, b, c, d) {
+ if (type?.toLowerCase() === "textinput") {
+ type = "beforeinput";
+ }
+ return orig.call(this, type, b, c, d);
+ }, window),
+ });
+}
+
+if (location.host === "www.reddit.com") {
+ (function () {
+ const EditorCSS = ".public-DraftEditor-content[contenteditable=true]";
+ let obsEditor, obsStart, obsText, obsKey, observer;
+ const obsConfig = { characterData: true, childList: true, subtree: true };
+ const obsHandler = () => {
+ observer.disconnect();
+ const finalTextNode = obsEditor.querySelector(
+ `[data-offset-key="${obsKey}"] [data-text='true']`
+ ).firstChild;
+ const end = obsStart + obsText.length;
+ window
+ .getSelection()
+ .setBaseAndExtent(finalTextNode, end, finalTextNode, end);
+ };
+ observer = new MutationObserver(obsHandler);
+
+ document.documentElement.addEventListener(
+ "beforeinput",
+ e => {
+ if (e.inputType != "insertFromPaste") {
+ return;
+ }
+ const { target } = e;
+ obsEditor = target.closest(EditorCSS);
+ if (!obsEditor) {
+ return;
+ }
+ const items = e?.dataTransfer.items;
+ for (let item of items) {
+ if (item.type === "text/plain") {
+ e.preventDefault();
+ item.getAsString(text => {
+ obsText = text;
+
+ // find the editor-managed <span> which contains the text node the
+ // cursor starts on, and the cursor's location (or the selection start)
+ const sel = window.getSelection();
+ obsStart = sel.anchorOffset;
+ let anchor = sel.anchorNode;
+ if (!anchor.closest) {
+ anchor = anchor.parentElement;
+ }
+ anchor = anchor.closest("[data-offset-key]");
+ obsKey = anchor.getAttribute("data-offset-key");
+
+ // set us up to wait for the editor to either update or replace the
+ // <span> with that key (the one containing the text to be changed).
+ // we will then make sure the cursor is after the pasted text, as if
+ // the editor recreates the node, the cursor position is lost
+ observer.observe(obsEditor, obsConfig);
+
+ // force the editor to "paste". sending paste or other events will not
+ // work, nor using execCommand (adding HTML will screw up the DOM that
+ // the editor expects, and adding plain text will make it ignore newlines).
+ target.dispatchEvent(
+ new InputEvent("beforeinput", {
+ inputType: "insertText",
+ data: text,
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+
+ // blur the editor to force it to update/flush its state, because otherwise
+ // the paste works, but the editor doesn't show it (until it is re-focused).
+ obsEditor.blur();
+ });
+ break;
+ }
+ }
+ },
+ true
+ );
+ })();
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js
new file mode 100644
index 0000000000..7383a4e567
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js
@@ -0,0 +1,35 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1769762 - Empty out navigator.plugins
+ * WebCompat issue #103612 - https://webcompat.com/issues/103612
+ *
+ * Certain features of the site are breaking if navigator.plugins array is not empty:
+ *
+ * 1. "Likes" on the comments are not saved
+ * 2. Can't reply to other people's comments
+ * 3. "Likes" on the videos are not saved
+ * 4. Can't follow an account (after refreshing "Follow" button is visible again)
+ *
+ * (note that the first 2 are still broken if you open devtools even with this intervention)
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "The PluginArray has been overridden for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1753874 for details."
+);
+
+const pluginsArray = new window.wrappedJSObject.Array();
+Object.setPrototypeOf(pluginsArray, PluginArray.prototype);
+
+Object.defineProperty(navigator.wrappedJSObject, "plugins", {
+ get: exportFunction(function () {
+ return pluginsArray;
+ }, window),
+ set: exportFunction(function (val) {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1774005-installtrigger-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1774005-installtrigger-shim.js
new file mode 100644
index 0000000000..ca7ef5b6c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1774005-installtrigger-shim.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1774005 - Generic window.InstallTrigger shim
+ *
+ * This interventions shims window.InstallTrigger to a string, which evaluates
+ * as `true` in web developers browser sniffing code. This intervention will
+ * be applied to multiple domains, see bug 1774005 for more information.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "The InstallTrigger has been shimmed for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1774005 for details."
+);
+
+Object.defineProperty(window.wrappedJSObject, "InstallTrigger", {
+ get: exportFunction(function () {
+ return "This property has been shimed for Web Compatibility reasons.";
+ }, window),
+ set: exportFunction(function (_) {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1799968-www.samsung.com-appVersion-linux-fix.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1799968-www.samsung.com-appVersion-linux-fix.js
new file mode 100644
index 0000000000..941f071e2c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1799968-www.samsung.com-appVersion-linux-fix.js
@@ -0,0 +1,31 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1799968 - Build site patch for www.samsung.com
+ * WebCompat issue #108993 - https://webcompat.com/issues/108993
+ *
+ * Samsung's Watch pages try to detect the OS via navigator.appVersion,
+ * but fail with Linux because they expect it to contain the literal
+ * string "linux", and their JS breaks.
+ *
+ * As such this site patch sets appVersion to "5.0 (Linux)", and is
+ * only meant to be applied on Linux.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "navigator.appVersion has been shimmed for compatibility reasons. See https://webcompat.com/issues/108993 for details."
+);
+
+Object.defineProperty(navigator.wrappedJSObject, "appVersion", {
+ get: exportFunction(function () {
+ return "5.0 (Linux)";
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1799980-healow.com-infinite-loop-fix.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1799980-healow.com-infinite-loop-fix.js
new file mode 100644
index 0000000000..191e97dec1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1799980-healow.com-infinite-loop-fix.js
@@ -0,0 +1,37 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1799980 - Healow gets stuck in an infinite loop while pages load
+ *
+ * This patch keeps Healow's localization scripts from getting stuck in
+ * an infinite loop while their pages are loading.
+ *
+ * This happens because they use synchronous XMLHttpRequests to fetch a
+ * JSON file with their localized text on the first call to their i18n
+ * function, and then force subsequent calls to wait for it by waiting
+ * in an infinite loop.
+ *
+ * But since they're in an infinite loop, the code after the syncXHR will
+ * never be able to run, so this ultimately triggers a slow script warning.
+ *
+ * We can improve this by just preventing the infinite loop from happening,
+ * though since they disable caching on their JSON files it means that more
+ * XHRs may happen. But since those files are small, this seems like a
+ * reasonable compromise until they migrate to a better i18n solution.
+ *
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1799980 for details.
+ */
+
+/* globals exportFunction */
+
+Object.defineProperty(window.wrappedJSObject, "ajaxRequestProcessing", {
+ get: exportFunction(function () {
+ return false;
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1818818-fastclick-legacy-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1818818-fastclick-legacy-shim.js
new file mode 100644
index 0000000000..91f1c1a19a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1818818-fastclick-legacy-shim.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1818818 - Neutralize FastClick
+ *
+ * The patch is applied on sites using older version of FastClick library.
+ * This allows to disable FastClick and fix various breakage caused
+ * by the library.
+ */
+
+/* globals exportFunction */
+
+const proto = CSS2Properties.prototype.wrappedJSObject;
+Object.defineProperty(proto, "msTouchAction", {
+ get: exportFunction(function () {
+ return "none";
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819450-cmbchina.com-ua-change.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819450-cmbchina.com-ua-change.js
new file mode 100644
index 0000000000..bbe76c465f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819450-cmbchina.com-ua-change.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1819450 - cmbchina.com - Override UA
+ *
+ * The site is using UA detection to redirect to
+ * m.cmbchina.com (mobile version of the site). Adding `SAMSUNG` allows
+ * to bypass the detection of mobile browser.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "The user agent has been overridden for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1081239 for details."
+);
+
+const MODIFIED_UA = navigator.userAgent + " SAMSUNG";
+
+Object.defineProperty(window.navigator.wrappedJSObject, "userAgent", {
+ get: exportFunction(function () {
+ return MODIFIED_UA;
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819476-axisbank.com-webkitSpeechRecognition-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819476-axisbank.com-webkitSpeechRecognition-shim.js
new file mode 100644
index 0000000000..a72e938e4f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819476-axisbank.com-webkitSpeechRecognition-shim.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * axisbank.com - Shim webkitSpeechRecognition
+ * WebCompat issue #117770 - https://webcompat.com/issues/117770
+ *
+ * The page with bank offerings is not loading options due to the
+ * site relying on webkitSpeechRecognition, which is undefined in Firefox.
+ * Shimming it to `class {}` makes the pages work.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "webkitSpeechRecognition was shimmed for compatibility reasons. See https://webcompat.com/issues/117770 for details."
+);
+
+Object.defineProperty(window.wrappedJSObject, "webkitSpeechRecognition", {
+ value: exportFunction(function () {
+ return class {};
+ }, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819678-free4talk.com-window-chrome-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819678-free4talk.com-window-chrome-shim.js
new file mode 100644
index 0000000000..6e6b5823cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1819678-free4talk.com-window-chrome-shim.js
@@ -0,0 +1,25 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1827678 - UA spoof for www.free4talk.com
+ *
+ * This site is checking for window.chrome, so let's spoof that.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "window.chrome has been shimmed for compatibility reasons. See https://github.com/webcompat/web-bugs/issues/77727 for details."
+);
+
+Object.defineProperty(window.wrappedJSObject, "chrome", {
+ get: exportFunction(function () {
+ return true;
+ }, window),
+
+ set: exportFunction(function () {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1830776-blueshieldca.com-unsupported.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1830776-blueshieldca.com-unsupported.js
new file mode 100644
index 0000000000..2b1eb11baf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1830776-blueshieldca.com-unsupported.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1830776 - blueshieldca.com
+ * WebCompat issue #112630 - https://webcompat.com/issues/112630
+ *
+ * The site is showing unsupported message in Firefox.
+ * They're also checking for "browserCollapsed" item in sessionStorage
+ * before showing the message, to only show it once. Adding this
+ * item to sessionStorage will make sure the message is not shown
+ * on the initial load.
+ */
+
+console.info(
+ "browserCollapsed in sessionStorage has been shimmed for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1830776 for details."
+);
+
+if (!sessionStorage.getItem("browserCollapsed")) {
+ sessionStorage.setItem("browserCollapsed", "true");
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1831007-nintendo-window-OnetrustActiveGroups.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1831007-nintendo-window-OnetrustActiveGroups.js
new file mode 100644
index 0000000000..433c416770
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1831007-nintendo-window-OnetrustActiveGroups.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1831007 - Shim window.OnetrustActiveGroups for Nintendo sites
+ *
+ * Nintendo relies on `window.OnetrustActiveGroups` being defined. If it's not,
+ * users may have intermittent issues signing into their account, as they're
+ * then trying to call `.split()` on `undefined`.
+ *
+ * This intervention sets a default value (an empty string), but still allows
+ * the value to be overwritten at any time.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "The window.OnetrustActiveGroups property has been shimmed for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1831007 for details."
+);
+
+Object.defineProperty(window.wrappedJSObject, "OnetrustActiveGroups", {
+ value: "",
+ writable: true,
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1836157-thai-masszazs-niceScroll-disable.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1836157-thai-masszazs-niceScroll-disable.js
new file mode 100644
index 0000000000..719267748b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1836157-thai-masszazs-niceScroll-disable.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+/**
+ * Bug 1836157 - Shim navigator.platform on www.thai-massaszs.net/en/
+ *
+ * This page adds niceScroll on Android, which breaks scrolling and
+ * zooming on Firefox. Adding ` Mac` to `navigator.platform` makes
+ * the page avoid adding niceScroll entirely, unbreaking the page.
+ */
+
+var plat = navigator.platform;
+if (!plat.includes("Mac")) {
+ console.info(
+ "The navigator.platform property has been shimmed to include 'Mac' for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1836157 for details."
+ );
+
+ Object.defineProperty(navigator.__proto__.wrappedJSObject, "platform", {
+ value: plat + " Mac",
+ writable: true,
+ });
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1842437-www.youtube.com-performance-now-precision.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1842437-www.youtube.com-performance-now-precision.js
new file mode 100644
index 0000000000..2d328de108
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1842437-www.youtube.com-performance-now-precision.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1842437 - When attempting to go back on youtube.com, the content remains the same
+ *
+ * If consecutive session history entries had history.state.entryTime set to same value,
+ * back button doesn't work as expected. The entryTime value is coming from performance.now()
+ * and modifying its return value slightly to make sure two close consecutive calls don't
+ * get the same result helped with resolving the issue.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "performance.now precision has been modified for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1756970 for details."
+);
+
+const origPerf = performance.wrappedJSObject;
+const origNow = origPerf.now;
+
+let counter = 0;
+let previousVal = 0;
+
+Object.defineProperty(window.performance.wrappedJSObject, "now", {
+ value: exportFunction(function () {
+ let originalVal = origNow.call(origPerf);
+ if (originalVal === previousVal) {
+ originalVal += 0.00000003 * ++counter;
+ } else {
+ previousVal = originalVal;
+ counter = 0;
+ }
+ return originalVal;
+ }, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1849058-nicochannel.jp-picture-in-picture-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1849058-nicochannel.jp-picture-in-picture-shim.js
new file mode 100644
index 0000000000..16245858ca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1849058-nicochannel.jp-picture-in-picture-shim.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1849058 - Shim PictureInPictureWindow for nicochannel.jp
+ * WebCompat issue #124463 - https://webcompat.com/issues/124463
+ *
+ * The page is showing unsupported message based on typeof
+ * window.PictureInPictureWindow, which is undefined in Firefox.
+ * Shimming it to `class {}` makes the pages work.
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "PictureInPictureWindow was shimmed for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1849058 for details."
+);
+
+Object.defineProperty(window.wrappedJSObject, "PictureInPictureWindow", {
+ value: exportFunction(function () {
+ return class {};
+ }, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js
new file mode 100644
index 0000000000..9c22c762a9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * eksiseyler.com - Set window.loggingEnabled = false
+ * WebCompat issue #77221 - https://webcompat.com/issues/77221
+ *
+ * A scripting error on the site causes images to not load unless
+ * window.loggingEnabled = false
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "loggingEnabled been set to true for compatibility reasons. See https://webcompat.com/issues/77221 for details."
+);
+
+Object.defineProperty(window.wrappedJSObject, "loggingEnabled", {
+ get: exportFunction(function () {
+ return false;
+ }, window),
+
+ set: exportFunction(function (value = {}) {}, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1855071-www.meteoam.it.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1855071-www.meteoam.it.js
new file mode 100644
index 0000000000..2bf38345dc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1855071-www.meteoam.it.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * www.meteoam.it - virtual keyboard is hidden as it is opened
+ * webcompat issue #121197 - https://webcompat.com/issues/121197
+ *
+ * the site's map is 75vh tall, and it hides the keyboard onresize,
+ * meaning the keyboard is closed as it it brought up in Firefox.
+ */
+
+console.info(
+ "Map iframe height is being managed for compatibility reasons. see https://webcompat.com/issues/77221 for details."
+);
+
+const selector = "#iframe_map";
+
+const moOptions = {
+ childList: true,
+ subtree: true,
+};
+
+const mo = new MutationObserver(() => {
+ const map = document.querySelector(selector);
+ let lastSize;
+ if (map) {
+ mo.disconnect();
+ const maybeGrowMap = () => {
+ const winHeight = window.outerHeight;
+ if (lastSize && lastSize > winHeight) {
+ return;
+ }
+ map.style.height = winHeight * 0.75 + "px";
+ lastSize = winHeight;
+ };
+ maybeGrowMap();
+ window.addEventListener("resize", () =>
+ window.requestAnimationFrame(maybeGrowMap)
+ );
+ }
+});
+
+mo.observe(document.documentElement, moOptions);
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1859617-installtrigger-removal-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1859617-installtrigger-removal-shim.js
new file mode 100644
index 0000000000..e91ef64422
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1859617-installtrigger-removal-shim.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1859617 - Generic window.InstallTrigger removal shim
+ *
+ * This interventions shims window.InstallTrigger to undefine it.
+ */
+
+/* globals exportFunction */
+
+if (typeof window.InstallTrigger !== "undefined") {
+ console.info(
+ "window.InstallTrigger has been undefined for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1859617 for details."
+ );
+
+ Object.defineProperty(window.wrappedJSObject, "InstallTrigger", {
+ get: exportFunction(function () {
+ return undefined;
+ }, window),
+ set: exportFunction(function (_) {}, window),
+ });
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1864564-esri-transfrom-names-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1864564-esri-transfrom-names-shim.js
new file mode 100644
index 0000000000..99bad007cc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1864564-esri-transfrom-names-shim.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+"use strict";
+
+/*
+ * Bug 1864564 - Override esri._css.names containing -moz-prefixed css rules names
+ * Webcompat issue #129144 - https://github.com/webcompat/web-bugs/issues/129144
+ *
+ * Esri library is applying -moz-transform to maps built with it, based on UA detection.
+ * Since support for -moz-transform has been removed in
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1855763, this results
+ * in maps partially or incorrectly displayed. Overriding esri._css.names
+ * containing -moz-prefixed css properties to their unprefixed versions
+ * fixes the issues.
+ */
+
+/* globals exportFunction, cloneInto */
+
+console.info(
+ "Overriding esri._css.names for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1864564 for details."
+);
+
+const transformNames = {
+ transition: "transition",
+ transform: "transform",
+ transformName: "transform",
+ origin: "transformOrigin",
+ endEvent: "transitionend",
+};
+
+let esriGlobal;
+
+Object.defineProperty(window.wrappedJSObject, "esri", {
+ get: exportFunction(function () {
+ if ("_css" in esriGlobal && "names" in esriGlobal._css) {
+ esriGlobal._css.names = cloneInto(transformNames, esriGlobal);
+ }
+ return esriGlobal;
+ }, window),
+
+ set: exportFunction(function (value) {
+ esriGlobal = value;
+ }, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/about_compat_broker.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/about_compat_broker.js
new file mode 100644
index 0000000000..faaa56a38e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/about_compat_broker.js
@@ -0,0 +1,141 @@
+/* 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/. */
+
+"use strict";
+
+/* global browser, module, onMessageFromTab */
+
+class AboutCompatBroker {
+ constructor(bindings) {
+ this._injections = bindings.injections;
+ this._uaOverrides = bindings.uaOverrides;
+ this._shims = bindings.shims;
+
+ if (!this._injections && !this._uaOverrides && !this._shims) {
+ throw new Error("No interventions; about:compat broker is not needed");
+ }
+
+ this.portsToAboutCompatTabs = this.buildPorts();
+ this._injections?.bindAboutCompatBroker(this);
+ this._uaOverrides?.bindAboutCompatBroker(this);
+ this._shims?.bindAboutCompatBroker(this);
+ }
+
+ buildPorts() {
+ const ports = new Set();
+
+ browser.runtime.onConnect.addListener(port => {
+ ports.add(port);
+ port.onDisconnect.addListener(function () {
+ ports.delete(port);
+ });
+ });
+
+ async function broadcast(message) {
+ for (const port of ports) {
+ port.postMessage(message);
+ }
+ }
+
+ return { broadcast };
+ }
+
+ filterOverrides(overrides) {
+ return overrides
+ .filter(override => override.availableOnPlatform)
+ .map(override => {
+ const { id, active, bug, domain, hidden } = override;
+ return { id, active, bug, domain, hidden };
+ });
+ }
+
+ getInterventionById(id) {
+ for (const [type, things] of Object.entries({
+ overrides: this._uaOverrides?.getAvailableOverrides() || [],
+ interventions: this._injections?.getAvailableInjections() || [],
+ shims: this._shims?.getAvailableShims() || [],
+ })) {
+ for (const what of things) {
+ if (what.id === id) {
+ return { type, what };
+ }
+ }
+ }
+ return {};
+ }
+
+ bootup() {
+ onMessageFromTab(msg => {
+ switch (msg.command || msg) {
+ case "toggle": {
+ const id = msg.id;
+ const { type, what } = this.getInterventionById(id);
+ if (!what) {
+ return Promise.reject(
+ `No such override or intervention to toggle: ${id}`
+ );
+ }
+ const active = type === "shims" ? !what.disabledReason : what.active;
+ this.portsToAboutCompatTabs
+ .broadcast({ toggling: id, active })
+ .then(async () => {
+ switch (type) {
+ case "interventions": {
+ if (active) {
+ await this._injections?.disableInjection(what);
+ } else {
+ await this._injections?.enableInjection(what);
+ }
+ break;
+ }
+ case "overrides": {
+ if (active) {
+ await this._uaOverrides?.disableOverride(what);
+ } else {
+ await this._uaOverrides?.enableOverride(what);
+ }
+ break;
+ }
+ case "shims": {
+ if (active) {
+ await this._shims?.disableShimForSession(id);
+ } else {
+ await this._shims?.enableShimForSession(id);
+ }
+ // no need to broadcast the "toggled" signal for shims, as
+ // they send a shimsUpdated message themselves instead
+ return;
+ }
+ }
+ this.portsToAboutCompatTabs.broadcast({
+ toggled: id,
+ active: !active,
+ });
+ });
+ break;
+ }
+ case "getAllInterventions": {
+ return Promise.resolve({
+ overrides:
+ (this._uaOverrides?.isEnabled() &&
+ this.filterOverrides(
+ this._uaOverrides?.getAvailableOverrides()
+ )) ||
+ false,
+ interventions:
+ (this._injections?.isEnabled() &&
+ this.filterOverrides(
+ this._injections?.getAvailableInjections()
+ )) ||
+ false,
+ shims: this._shims?.getAvailableShims() || false,
+ });
+ }
+ }
+ return undefined;
+ });
+ }
+}
+
+module.exports = AboutCompatBroker;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/custom_functions.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/custom_functions.js
new file mode 100644
index 0000000000..97603e0424
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/custom_functions.js
@@ -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/. */
+
+"use strict";
+
+/* globals browser, module */
+
+const replaceStringInRequest = (
+ requestId,
+ inString,
+ outString,
+ inEncoding = "utf-8"
+) => {
+ const filter = browser.webRequest.filterResponseData(requestId);
+ const decoder = new TextDecoder(inEncoding);
+ const encoder = new TextEncoder();
+ const RE = new RegExp(inString, "g");
+ const carryoverLength = inString.length;
+ let carryover = "";
+
+ filter.ondata = event => {
+ const replaced = (
+ carryover + decoder.decode(event.data, { stream: true })
+ ).replace(RE, outString);
+ filter.write(encoder.encode(replaced.slice(0, -carryoverLength)));
+ carryover = replaced.slice(-carryoverLength);
+ };
+
+ filter.onstop = event => {
+ if (carryover.length) {
+ filter.write(encoder.encode(carryover));
+ }
+ filter.close();
+ };
+};
+
+const CUSTOM_FUNCTIONS = {
+ detectSwipeFix: injection => {
+ const { urls, types } = injection.data;
+ const listener = (injection.data.listener = ({ requestId }) => {
+ replaceStringInRequest(
+ requestId,
+ "preventDefault:true",
+ "preventDefault:false"
+ );
+ return {};
+ });
+ browser.webRequest.onBeforeRequest.addListener(listener, { urls, types }, [
+ "blocking",
+ ]);
+ },
+ detectSwipeFixDisable: injection => {
+ const { listener } = injection.data;
+ browser.webRequest.onBeforeRequest.removeListener(listener);
+ delete injection.data.listener;
+ },
+ noSniffFix: injection => {
+ const { urls, contentType } = injection.data;
+ const listener = (injection.data.listener = e => {
+ e.responseHeaders.push(contentType);
+ return { responseHeaders: e.responseHeaders };
+ });
+
+ browser.webRequest.onHeadersReceived.addListener(listener, { urls }, [
+ "blocking",
+ "responseHeaders",
+ ]);
+ },
+ noSniffFixDisable: injection => {
+ const { listener } = injection.data;
+ browser.webRequest.onHeadersReceived.removeListener(listener);
+ delete injection.data.listener;
+ },
+ runScriptBeforeRequest: injection => {
+ const { bug, message, request, script, types } = injection;
+ const warning = `${message} See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
+
+ const listener = (injection.listener = e => {
+ const { tabId, frameId } = e;
+ return browser.tabs
+ .executeScript(tabId, {
+ file: script,
+ frameId,
+ runAt: "document_start",
+ })
+ .then(() => {
+ browser.tabs.executeScript(tabId, {
+ code: `console.warn(${JSON.stringify(warning)})`,
+ runAt: "document_start",
+ });
+ })
+ .catch(_ => {});
+ });
+
+ browser.webRequest.onBeforeRequest.addListener(
+ listener,
+ { urls: request, types: types || ["script"] },
+ ["blocking"]
+ );
+ },
+ runScriptBeforeRequestDisable: injection => {
+ const { listener } = injection;
+ browser.webRequest.onBeforeRequest.removeListener(listener);
+ delete injection.data.listener;
+ },
+};
+
+module.exports = CUSTOM_FUNCTIONS;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/injections.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/injections.js
new file mode 100644
index 0000000000..92fdc5fbb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/injections.js
@@ -0,0 +1,272 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser, module */
+
+class Injections {
+ constructor(availableInjections, customFunctions) {
+ this.INJECTION_PREF = "perform_injections";
+
+ this._injectionsEnabled = true;
+
+ this._availableInjections = availableInjections;
+ this._activeInjections = new Set();
+ // Only used if this.shouldUseScriptingAPI is false and we are falling back
+ // to use the contentScripts API.
+ this._activeInjectionHandles = new Map();
+ this._customFunctions = customFunctions;
+
+ this.shouldUseScriptingAPI =
+ browser.aboutConfigPrefs.getBoolPrefSync("useScriptingAPI");
+ // Debug log emit only on nightly (similarly to the debug
+ // helper used in shims.js for similar purpose).
+ browser.appConstants.getReleaseBranch().then(releaseBranch => {
+ if (releaseBranch !== "release_or_beta") {
+ console.debug(
+ `WebCompat Injections will be injected using ${
+ this.shouldUseScriptingAPI ? "scripting" : "contentScripts"
+ } API`
+ );
+ }
+ });
+ }
+
+ bindAboutCompatBroker(broker) {
+ this._aboutCompatBroker = broker;
+ }
+
+ bootup() {
+ browser.aboutConfigPrefs.onPrefChange.addListener(() => {
+ this.checkInjectionPref();
+ }, this.INJECTION_PREF);
+ this.checkInjectionPref();
+ }
+
+ checkInjectionPref() {
+ browser.aboutConfigPrefs.getPref(this.INJECTION_PREF).then(value => {
+ if (value === undefined) {
+ browser.aboutConfigPrefs.setPref(this.INJECTION_PREF, true);
+ } else if (value === false) {
+ this.unregisterContentScripts();
+ } else {
+ this.registerContentScripts();
+ }
+ });
+ }
+
+ getAvailableInjections() {
+ return this._availableInjections;
+ }
+
+ isEnabled() {
+ return this._injectionsEnabled;
+ }
+
+ async getPromiseRegisteredScriptIds(scriptIds) {
+ let registeredScriptIds = [];
+
+ // Try to avoid re-registering scripts already registered
+ // (e.g. if the webcompat background page is restarted
+ // after an extension process crash, after having registered
+ // the content scripts already once), but do not prevent
+ // to try registering them again if the getRegisteredContentScripts
+ // method returns an unexpected rejection.
+ try {
+ const registeredScripts =
+ await browser.scripting.getRegisteredContentScripts({
+ // By default only look for script ids that belongs to Injections
+ // (and ignore the ones that may belong to Shims).
+ ids: scriptIds ?? this._availableInjections.map(inj => inj.id),
+ });
+ registeredScriptIds = registeredScripts.map(script => script.id);
+ } catch (ex) {
+ console.error(
+ "Retrieve WebCompat GoFaster registered content scripts failed: ",
+ ex
+ );
+ }
+
+ return registeredScriptIds;
+ }
+
+ async registerContentScripts() {
+ const platformInfo = await browser.runtime.getPlatformInfo();
+ const platformMatches = [
+ "all",
+ platformInfo.os,
+ platformInfo.os == "android" ? "android" : "desktop",
+ ];
+
+ let registeredScriptIds = this.shouldUseScriptingAPI
+ ? await this.getPromiseRegisteredScriptIds()
+ : [];
+
+ for (const injection of this._availableInjections) {
+ if (platformMatches.includes(injection.platform)) {
+ injection.availableOnPlatform = true;
+ await this.enableInjection(injection, registeredScriptIds);
+ }
+ }
+
+ this._injectionsEnabled = true;
+ this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
+ interventionsChanged: this._aboutCompatBroker.filterOverrides(
+ this._availableInjections
+ ),
+ });
+ }
+
+ buildContentScriptRegistrations(contentScripts) {
+ let finalConfig = Object.assign({}, contentScripts);
+
+ if (!finalConfig.runAt) {
+ finalConfig.runAt = "document_start";
+ }
+
+ if (this.shouldUseScriptingAPI) {
+ // Don't persist the content scripts across browser restarts
+ // (at least not yet, we would need to apply some more changes
+ // to adjust webcompat for accounting for the scripts to be
+ // already registered).
+ //
+ // NOTE: scripting API has been introduced in Gecko 102,
+ // prior to Gecko 105 persistAcrossSessions option was required
+ // and only accepted false persistAcrossSessions, after Gecko 105
+ // is optional and defaults to true.
+
+ finalConfig.persistAcrossSessions = false;
+
+ // Convert js/css from contentScripts.register API method
+ // format to scripting.registerContentScripts API method
+ // format.
+ if (Array.isArray(finalConfig.js)) {
+ finalConfig.js = finalConfig.js.map(e => e.file);
+ }
+
+ if (Array.isArray(finalConfig.css)) {
+ finalConfig.css = finalConfig.css.map(e => e.file);
+ }
+ }
+
+ return finalConfig;
+ }
+
+ async enableInjection(injection, registeredScriptIds) {
+ if (injection.active) {
+ return undefined;
+ }
+
+ if (injection.customFunc) {
+ return this.enableCustomInjection(injection);
+ }
+
+ return this.enableContentScripts(injection, registeredScriptIds);
+ }
+
+ enableCustomInjection(injection) {
+ if (injection.customFunc in this._customFunctions) {
+ this._customFunctions[injection.customFunc](injection);
+ injection.active = true;
+ } else {
+ console.error(
+ `Provided function ${injection.customFunc} wasn't found in functions list`
+ );
+ }
+ }
+
+ async enableContentScripts(injection, registeredScriptIds) {
+ let injectProps;
+ try {
+ const { id } = injection;
+ if (this.shouldUseScriptingAPI) {
+ // enableContentScripts receives a registeredScriptIds already
+ // pre-computed once from registerContentScripts to register all
+ // the injection, whereas it does not expect to receive one when
+ // it is called from the AboutCompatBroker to re-enable one specific
+ // injection.
+ let activeScriptIds = Array.isArray(registeredScriptIds)
+ ? registeredScriptIds
+ : await this.getPromiseRegisteredScriptIds([id]);
+ injectProps = this.buildContentScriptRegistrations(
+ injection.contentScripts
+ );
+ injectProps.id = id;
+ if (!activeScriptIds.includes(id)) {
+ await browser.scripting.registerContentScripts([injectProps]);
+ }
+ this._activeInjections.add(id);
+ } else {
+ const handle = await browser.contentScripts.register(
+ this.buildContentScriptRegistrations(injection.contentScripts)
+ );
+ this._activeInjections.add(id);
+ this._activeInjectionHandles.set(id, handle);
+ }
+
+ injection.active = true;
+ } catch (ex) {
+ console.error(
+ "Registering WebCompat GoFaster content scripts failed: ",
+ { injection, injectProps },
+ ex
+ );
+ }
+ }
+
+ unregisterContentScripts() {
+ for (const injection of this._availableInjections) {
+ this.disableInjection(injection);
+ }
+
+ this._injectionsEnabled = false;
+ this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
+ interventionsChanged: false,
+ });
+ }
+
+ async disableInjection(injection) {
+ if (!injection.active) {
+ return undefined;
+ }
+
+ if (injection.customFunc) {
+ return this.disableCustomInjections(injection);
+ }
+
+ return this.disableContentScripts(injection);
+ }
+
+ disableCustomInjections(injection) {
+ const disableFunc = injection.customFunc + "Disable";
+
+ if (disableFunc in this._customFunctions) {
+ this._customFunctions[disableFunc](injection);
+ injection.active = false;
+ } else {
+ console.error(
+ `Provided function ${disableFunc} for disabling injection wasn't found in functions list`
+ );
+ }
+ }
+
+ async disableContentScripts(injection) {
+ if (this._activeInjections.has(injection.id)) {
+ if (this.shouldUseScriptingAPI) {
+ await browser.scripting.unregisterContentScripts({
+ ids: [injection.id],
+ });
+ } else {
+ const handle = this._activeInjectionHandles.get(injection.id);
+ await handle.unregister();
+ this._activeInjectionHandles.delete(injection.id);
+ }
+ this._activeInjections.delete(injection);
+ }
+ injection.active = false;
+ }
+}
+
+module.exports = Injections;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/intervention_helpers.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/intervention_helpers.js
new file mode 100644
index 0000000000..16ea6572f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/intervention_helpers.js
@@ -0,0 +1,233 @@
+/* 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/. */
+
+"use strict";
+
+/* globals module */
+
+const GOOGLE_TLDS = [
+ "com",
+ "ac",
+ "ad",
+ "ae",
+ "com.af",
+ "com.ag",
+ "com.ai",
+ "al",
+ "am",
+ "co.ao",
+ "com.ar",
+ "as",
+ "at",
+ "com.au",
+ "az",
+ "ba",
+ "com.bd",
+ "be",
+ "bf",
+ "bg",
+ "com.bh",
+ "bi",
+ "bj",
+ "com.bn",
+ "com.bo",
+ "com.br",
+ "bs",
+ "bt",
+ "co.bw",
+ "by",
+ "com.bz",
+ "ca",
+ "com.kh",
+ "cc",
+ "cd",
+ "cf",
+ "cat",
+ "cg",
+ "ch",
+ "ci",
+ "co.ck",
+ "cl",
+ "cm",
+ "cn",
+ "com.co",
+ "co.cr",
+ "com.cu",
+ "cv",
+ "com.cy",
+ "cz",
+ "de",
+ "dj",
+ "dk",
+ "dm",
+ "com.do",
+ "dz",
+ "com.ec",
+ "ee",
+ "com.eg",
+ "es",
+ "com.et",
+ "fi",
+ "com.fj",
+ "fm",
+ "fr",
+ "ga",
+ "ge",
+ "gf",
+ "gg",
+ "com.gh",
+ "com.gi",
+ "gl",
+ "gm",
+ "gp",
+ "gr",
+ "com.gt",
+ "gy",
+ "com.hk",
+ "hn",
+ "hr",
+ "ht",
+ "hu",
+ "co.id",
+ "iq",
+ "ie",
+ "co.il",
+ "im",
+ "co.in",
+ "io",
+ "is",
+ "it",
+ "je",
+ "com.jm",
+ "jo",
+ "co.jp",
+ "co.ke",
+ "ki",
+ "kg",
+ "co.kr",
+ "com.kw",
+ "kz",
+ "la",
+ "com.lb",
+ "com.lc",
+ "li",
+ "lk",
+ "co.ls",
+ "lt",
+ "lu",
+ "lv",
+ "com.ly",
+ "co.ma",
+ "md",
+ "me",
+ "mg",
+ "mk",
+ "ml",
+ "com.mm",
+ "mn",
+ "ms",
+ "com.mt",
+ "mu",
+ "mv",
+ "mw",
+ "com.mx",
+ "com.my",
+ "co.mz",
+ "com.na",
+ "ne",
+ "com.nf",
+ "com.ng",
+ "com.ni",
+ "nl",
+ "no",
+ "com.np",
+ "nr",
+ "nu",
+ "co.nz",
+ "com.om",
+ "com.pk",
+ "com.pa",
+ "com.pe",
+ "com.ph",
+ "pl",
+ "com.pg",
+ "pn",
+ "com.pr",
+ "ps",
+ "pt",
+ "com.py",
+ "com.qa",
+ "ro",
+ "rs",
+ "ru",
+ "rw",
+ "com.sa",
+ "com.sb",
+ "sc",
+ "se",
+ "com.sg",
+ "sh",
+ "si",
+ "sk",
+ "com.sl",
+ "sn",
+ "sm",
+ "so",
+ "st",
+ "sr",
+ "com.sv",
+ "td",
+ "tg",
+ "co.th",
+ "com.tj",
+ "tk",
+ "tl",
+ "tm",
+ "to",
+ "tn",
+ "com.tr",
+ "tt",
+ "com.tw",
+ "co.tz",
+ "com.ua",
+ "co.ug",
+ "co.uk",
+ "com",
+ "com.uy",
+ "co.uz",
+ "com.vc",
+ "co.ve",
+ "vg",
+ "co.vi",
+ "com.vn",
+ "vu",
+ "ws",
+ "co.za",
+ "co.zm",
+ "co.zw",
+];
+
+var InterventionHelpers = {
+ /**
+ * Useful helper to generate a list of domains with a fixed base domain and
+ * multiple country-TLDs or other cases with various TLDs.
+ *
+ * Example:
+ * matchPatternsForTLDs("*://mozilla.", "/*", ["com", "org"])
+ * => ["*://mozilla.com/*", "*://mozilla.org/*"]
+ */
+ matchPatternsForTLDs(base, suffix, tlds) {
+ return tlds.map(tld => base + tld + suffix);
+ },
+
+ /**
+ * A modified version of matchPatternsForTLDs that always returns the match
+ * list for all known Google country TLDs.
+ */
+ matchPatternsForGoogle(base, suffix = "/*") {
+ return InterventionHelpers.matchPatternsForTLDs(base, suffix, GOOGLE_TLDS);
+ },
+};
+
+module.exports = InterventionHelpers;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/messaging_helper.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/messaging_helper.js
new file mode 100644
index 0000000000..d978ed384f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/messaging_helper.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser */
+
+// By default, only the first handler for browser.runtime.onMessage which
+// returns a value will get to return one. As such, we need to let them all
+// receive the message, and all have a chance to return a response (with the
+// first non-undefined result being the one that is ultimately returned).
+// This way, about:compat and the shims library can both get a chance to
+// process a message, and just return undefined if they wish to ignore it.
+
+const onMessageFromTab = (function () {
+ const handlers = new Set();
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ const promises = [...handlers.values()].map(fn => fn(msg, sender));
+ return Promise.allSettled(promises).then(results => {
+ for (const { reason, value } of results) {
+ if (reason) {
+ console.error(reason);
+ } else if (value !== undefined) {
+ return value;
+ }
+ }
+ return undefined;
+ });
+ });
+
+ return function (handler) {
+ handlers.add(handler);
+ };
+})();
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/module_shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/module_shim.js
new file mode 100644
index 0000000000..2fd39fdbbd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/module_shim.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * We cannot yet use proper JS modules within webextensions, as support for them
+ * is highly experimental and highly instable. So we end up just including all
+ * the JS files we need as separate background scripts, and since they all are
+ * executed within the same context, this works for our in-browser deployment.
+ *
+ * However, this code is tracked outside of mozilla-central, and we work on
+ * shipping this code in other products, like android-components as well.
+ * Because of that, we have automated tests running within that repository. To
+ * make our lives easier, we add `module.exports` statements to the JS source
+ * files, so we can easily import their contents into our NodeJS-based test
+ * suite.
+ *
+ * This works fine, but obviously, `module` is not defined when running
+ * in-browser. So let's use this empty object as a shim, so we don't run into
+ * runtime exceptions because of that.
+ */
+var module = {};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/requestStorageAccess_helper.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/requestStorageAccess_helper.js
new file mode 100644
index 0000000000..032225bb78
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/requestStorageAccess_helper.js
@@ -0,0 +1,30 @@
+/* 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/. */
+
+/* globals browser */
+
+// Helper for calling the internal requestStorageAccessForOrigin method. The
+// method is called on the first-party document for the third-party which needs
+// first-party storage access.
+browser.runtime.onMessage.addListener(request => {
+ let { requestStorageAccessOrigin, warning } = request;
+ if (!requestStorageAccessOrigin) {
+ return false;
+ }
+
+ // Log a warning to the web console, informing about the shim.
+ console.warn(warning);
+
+ // Call the internal storage access API. Passing false means we don't require
+ // user activation, but will always show the storage access prompt. The user
+ // has to explicitly allow storage access.
+ return document
+ .requestStorageAccessForOrigin(requestStorageAccessOrigin, false)
+ .then(() => {
+ return { success: true };
+ })
+ .catch(() => {
+ return { success: false };
+ });
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/shim_messaging_helper.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/shim_messaging_helper.js
new file mode 100644
index 0000000000..ee109713a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/shim_messaging_helper.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser */
+
+if (!window.Shims) {
+ window.Shims = new Map();
+}
+
+if (!window.ShimsHelperReady) {
+ window.ShimsHelperReady = true;
+
+ browser.runtime.onMessage.addListener(details => {
+ const { shimId, warning } = details;
+ if (!shimId) {
+ return;
+ }
+ window.Shims.set(shimId, details);
+ if (warning) {
+ console.warn(warning);
+ }
+ });
+
+ async function handleMessage(port, shimId, messageId, message) {
+ let response;
+ const shim = window.Shims.get(shimId);
+ if (shim) {
+ const { needsShimHelpers, origin } = shim;
+ if (origin === location.origin) {
+ if (needsShimHelpers?.includes(message)) {
+ const msg = { shimId, message };
+ try {
+ response = await browser.runtime.sendMessage(msg);
+ } catch (_) {}
+ }
+ }
+ }
+ port.postMessage({ messageId, response });
+ }
+
+ window.addEventListener(
+ "ShimConnects",
+ e => {
+ e.stopPropagation();
+ e.preventDefault();
+ const { port, pendingMessages, shimId } = e.detail;
+ const shim = window.Shims.get(shimId);
+ if (!shim) {
+ return;
+ }
+ port.onmessage = ({ data }) => {
+ handleMessage(port, shimId, data.messageId, data.message);
+ };
+ for (const [messageId, message] of pendingMessages) {
+ handleMessage(port, shimId, messageId, message);
+ }
+ },
+ true
+ );
+
+ window.dispatchEvent(new CustomEvent("ShimHelperReady"));
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/shims.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/shims.js
new file mode 100644
index 0000000000..fedb4c38e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/shims.js
@@ -0,0 +1,1110 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser, module, onMessageFromTab */
+
+// To grant shims access to bundled logo images without risking
+// exposing our moz-extension URL, we have the shim request them via
+// nonsense URLs which we then redirect to the actual files (but only
+// on tabs where a shim using a given logo happens to be active).
+const LogosBaseURL = "https://smartblock.firefox.etp/";
+
+const releaseBranchPromise = browser.appConstants.getReleaseBranch();
+
+const platformPromise = browser.runtime.getPlatformInfo().then(info => {
+ return info.os === "android" ? "android" : "desktop";
+});
+
+let debug = async function () {
+ if ((await releaseBranchPromise) !== "release_or_beta") {
+ console.debug.apply(this, arguments);
+ }
+};
+let error = async function () {
+ if ((await releaseBranchPromise) !== "release_or_beta") {
+ console.error.apply(this, arguments);
+ }
+};
+let warn = async function () {
+ if ((await releaseBranchPromise) !== "release_or_beta") {
+ console.warn.apply(this, arguments);
+ }
+};
+
+class Shim {
+ constructor(opts, manager) {
+ this.manager = manager;
+
+ const { contentScripts, matches, unblocksOnOptIn } = opts;
+
+ this.branches = opts.branches;
+ this.bug = opts.bug;
+ this.isGoogleTrendsDFPIFix = opts.custom == "google-trends-dfpi-fix";
+ this.file = opts.file;
+ this.hiddenInAboutCompat = opts.hiddenInAboutCompat;
+ this.hosts = opts.hosts;
+ this.id = opts.id;
+ this.logos = opts.logos || [];
+ this.matches = [];
+ this.name = opts.name;
+ this.notHosts = opts.notHosts;
+ this.onlyIfBlockedByETP = opts.onlyIfBlockedByETP;
+ this.onlyIfDFPIActive = opts.onlyIfDFPIActive;
+ this.onlyIfPrivateBrowsing = opts.onlyIfPrivateBrowsing;
+ this._options = opts.options || {};
+ this.needsShimHelpers = opts.needsShimHelpers;
+ this.platform = opts.platform || "all";
+ this.runFirst = opts.runFirst;
+ this.unblocksOnOptIn = unblocksOnOptIn;
+ this.requestStorageAccessForRedirect = opts.requestStorageAccessForRedirect;
+ this.shouldUseScriptingAPI =
+ browser.aboutConfigPrefs.getBoolPrefSync("useScriptingAPI");
+ debug(
+ `WebCompat Shim ${this.id} will be injected using ${
+ this.shouldUseScriptingAPI ? "scripting" : "contentScripts"
+ } API`
+ );
+
+ this._hostOptIns = new Set();
+
+ this._disabledByConfig = opts.disabled;
+ this._disabledGlobally = false;
+ this._disabledForSession = false;
+ this._disabledByPlatform = false;
+ this._disabledByReleaseBranch = false;
+
+ this._activeOnTabs = new Set();
+ this._showedOptInOnTabs = new Set();
+
+ const pref = `disabled_shims.${this.id}`;
+
+ this.redirectsRequests = !!this.file && matches?.length;
+
+ // NOTE: _contentScriptRegistrations is an array of string ids when
+ // shouldUseScriptingAPI is true and an array of script handles returned
+ // by contentScripts.register otherwise.
+ this._contentScriptRegistrations = [];
+
+ this.contentScripts = contentScripts || [];
+ for (const script of this.contentScripts) {
+ if (typeof script.css === "string") {
+ script.css = [
+ this.shouldUseScriptingAPI
+ ? `/shims/${script.css}`
+ : { file: `/shims/${script.css}` },
+ ];
+ }
+ if (typeof script.js === "string") {
+ script.js = [
+ this.shouldUseScriptingAPI
+ ? `/shims/${script.js}`
+ : { file: `/shims/${script.js}` },
+ ];
+ }
+ }
+
+ for (const match of matches || []) {
+ if (!match.types) {
+ this.matches.push({ patterns: [match], types: ["script"] });
+ } else {
+ this.matches.push(match);
+ }
+ if (match.target) {
+ this.redirectsRequests = true;
+ }
+ }
+
+ browser.aboutConfigPrefs.onPrefChange.addListener(async () => {
+ const value = await browser.aboutConfigPrefs.getPref(pref);
+ this._disabledPrefValue = value;
+ this._onEnabledStateChanged();
+ }, pref);
+
+ this.ready = Promise.all([
+ browser.aboutConfigPrefs.getPref(pref),
+ platformPromise,
+ releaseBranchPromise,
+ ]).then(([disabledPrefValue, platform, branch]) => {
+ this._disabledPrefValue = disabledPrefValue;
+
+ this._disabledByPlatform =
+ this.platform !== "all" && this.platform !== platform;
+
+ this._disabledByReleaseBranch = false;
+ for (const supportedBranchAndPlatform of this.branches || []) {
+ const [supportedBranch, supportedPlatform] =
+ supportedBranchAndPlatform.split(":");
+ if (
+ (!supportedPlatform || supportedPlatform == platform) &&
+ supportedBranch != branch
+ ) {
+ this._disabledByReleaseBranch = true;
+ }
+ }
+
+ this._preprocessOptions(platform, branch);
+ this._onEnabledStateChanged();
+ });
+ }
+
+ _preprocessOptions(platform, branch) {
+ // options may be any value, but can optionally be gated for specified
+ // platform/branches, if in the format `{value, branches, platform}`
+ this.options = {};
+ for (const [k, v] of Object.entries(this._options)) {
+ if (v?.value) {
+ if (
+ (!v.platform || v.platform === platform) &&
+ (!v.branches || v.branches.includes(branch))
+ ) {
+ this.options[k] = v.value;
+ }
+ } else {
+ this.options[k] = v;
+ }
+ }
+ }
+
+ get enabled() {
+ if (this._disabledGlobally || this._disabledForSession) {
+ return false;
+ }
+
+ if (this._disabledPrefValue !== undefined) {
+ return !this._disabledPrefValue;
+ }
+
+ return (
+ !this._disabledByConfig &&
+ !this._disabledByPlatform &&
+ !this._disabledByReleaseBranch
+ );
+ }
+
+ get disabledReason() {
+ if (this._disabledGlobally) {
+ return "globalPref";
+ }
+
+ if (this._disabledForSession) {
+ return "session";
+ }
+
+ if (this._disabledPrefValue !== undefined) {
+ if (this._disabledPrefValue === true) {
+ return "pref";
+ }
+ return false;
+ }
+
+ if (this._disabledByConfig) {
+ return "config";
+ }
+
+ if (this._disabledByPlatform) {
+ return "platform";
+ }
+
+ if (this._disabledByReleaseBranch) {
+ return "releaseBranch";
+ }
+
+ return false;
+ }
+
+ onAllShimsEnabled() {
+ const wasEnabled = this.enabled;
+ this._disabledGlobally = false;
+ if (!wasEnabled) {
+ this._onEnabledStateChanged();
+ }
+ }
+
+ onAllShimsDisabled() {
+ const wasEnabled = this.enabled;
+ this._disabledGlobally = true;
+ if (wasEnabled) {
+ this._onEnabledStateChanged();
+ }
+ }
+
+ enableForSession() {
+ const wasEnabled = this.enabled;
+ this._disabledForSession = false;
+ if (!wasEnabled) {
+ this._onEnabledStateChanged();
+ }
+ }
+
+ disableForSession() {
+ const wasEnabled = this.enabled;
+ this._disabledForSession = true;
+ if (wasEnabled) {
+ this._onEnabledStateChanged();
+ }
+ }
+
+ async _onEnabledStateChanged() {
+ this.manager?.onShimStateChanged(this.id);
+ if (!this.enabled) {
+ await this._unregisterContentScripts();
+ return this._revokeRequestsInETP();
+ }
+ await this._registerContentScripts();
+ return this._allowRequestsInETP();
+ }
+
+ async _registerContentScripts() {
+ if (
+ this.contentScripts.length &&
+ !this._contentScriptRegistrations.length
+ ) {
+ const matches = [];
+ let idx = 0;
+ for (const options of this.contentScripts) {
+ matches.push(options.matches);
+ if (this.shouldUseScriptingAPI) {
+ // Some shims includes more than one script (e.g. Blogger one contains
+ // a content script to be run on document_start and one to be run
+ // on document_end.
+ options.id = `shim-${this.id}-${idx++}`;
+ options.persistAcrossSessions = false;
+ // Having to call getRegisteredContentScripts each time we are going to
+ // register a Shim content script is suboptimal, but avoiding that
+ // may require a bit more changes (e.g. rework both Injections, Shim and Shims
+ // classes to more easily register all content scripts with a single
+ // call to the scripting API methods when the background script page is loading
+ // and one per injection or shim being enabled from the AboutCompatBroker).
+ // In the short term we call getRegisteredContentScripts and restrict it to
+ // the script id we are about to register.
+ let isAlreadyRegistered = false;
+ try {
+ const registeredScripts =
+ await browser.scripting.getRegisteredContentScripts({
+ ids: [options.id],
+ });
+ isAlreadyRegistered = !!registeredScripts.length;
+ } catch (ex) {
+ console.error(
+ "Retrieve WebCompat GoFaster registered content scripts failed: ",
+ ex
+ );
+ }
+ try {
+ if (!isAlreadyRegistered) {
+ await browser.scripting.registerContentScripts([options]);
+ }
+ this._contentScriptRegistrations.push(options.id);
+ } catch (ex) {
+ console.error(
+ "Registering WebCompat Shim content scripts failed: ",
+ options,
+ ex
+ );
+ }
+ } else {
+ const reg = await browser.contentScripts.register(options);
+ this._contentScriptRegistrations.push(reg);
+ }
+ }
+ const urls = Array.from(new Set(matches.flat()));
+ debug("Enabling content scripts for these URLs:", urls);
+ }
+ }
+
+ async _unregisterContentScripts() {
+ if (this.shouldUseScriptingAPI) {
+ const ids = this._contentScriptRegistrations;
+ await browser.scripting.unregisterContentScripts({ ids });
+ } else {
+ for (const registration of this._contentScriptRegistrations) {
+ registration.unregister();
+ }
+ }
+ this._contentScriptRegistrations = [];
+ }
+
+ async _allowRequestsInETP() {
+ const matches = this.matches.map(m => m.patterns).flat();
+ if (matches.length) {
+ await browser.trackingProtection.shim(this.id, matches);
+ }
+
+ if (this._hostOptIns.size) {
+ const optIns = this.getApplicableOptIns();
+ if (optIns.length) {
+ await browser.trackingProtection.allow(
+ this.id,
+ this._optInPatterns,
+ Array.from(this._hostOptIns)
+ );
+ }
+ }
+ }
+
+ _revokeRequestsInETP() {
+ return browser.trackingProtection.revoke(this.id);
+ }
+
+ setActiveOnTab(tabId, active = true) {
+ if (active) {
+ this._activeOnTabs.add(tabId);
+ } else {
+ this._activeOnTabs.delete(tabId);
+ this._showedOptInOnTabs.delete(tabId);
+ }
+ }
+
+ isActiveOnTab(tabId) {
+ return this._activeOnTabs.has(tabId);
+ }
+
+ meantForHost(host) {
+ const { hosts, notHosts } = this;
+ if (hosts || notHosts) {
+ if (
+ (notHosts && notHosts.includes(host)) ||
+ (hosts && !hosts.includes(host))
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ async unblocksURLOnOptIn(url) {
+ if (!this._optInPatterns) {
+ this._optInPatterns = await this.getApplicableOptIns();
+ }
+
+ if (!this._optInMatcher) {
+ this._optInMatcher = browser.matchPatterns.getMatcher(
+ Array.from(this._optInPatterns)
+ );
+ }
+
+ return this._optInMatcher.matches(url);
+ }
+
+ isTriggeredByURLAndType(url, type) {
+ for (const entry of this.matches || []) {
+ if (!entry.types.includes(type)) {
+ continue;
+ }
+ if (!entry.matcher) {
+ entry.matcher = browser.matchPatterns.getMatcher(
+ Array.from(entry.patterns)
+ );
+ }
+ if (entry.matcher.matches(url)) {
+ return entry;
+ }
+ }
+
+ return undefined;
+ }
+
+ async getApplicableOptIns() {
+ if (this._applicableOptIns) {
+ return this._applicableOptIns;
+ }
+ const optins = [];
+ for (const unblock of this.unblocksOnOptIn || []) {
+ if (typeof unblock === "string") {
+ optins.push(unblock);
+ continue;
+ }
+ const { branches, patterns, platforms } = unblock;
+ if (platforms?.length) {
+ const platform = await platformPromise;
+ if (platform !== "all" && !platforms.includes(platform)) {
+ continue;
+ }
+ }
+ if (branches?.length) {
+ const branch = await releaseBranchPromise;
+ if (!branches.includes(branch)) {
+ continue;
+ }
+ }
+ optins.push.apply(optins, patterns);
+ }
+ this._applicableOptIns = optins;
+ return optins;
+ }
+
+ async onUserOptIn(host) {
+ const optins = await this.getApplicableOptIns();
+ if (optins.length) {
+ this.userHasOptedIn = true;
+ this._hostOptIns.add(host);
+ await browser.trackingProtection.allow(
+ this.id,
+ optins,
+ Array.from(this._hostOptIns)
+ );
+ }
+ }
+
+ hasUserOptedInAlready(host) {
+ return this._hostOptIns.has(host);
+ }
+
+ showOptInWarningOnce(tabId, origin) {
+ if (this._showedOptInOnTabs.has(tabId)) {
+ return Promise.resolve();
+ }
+ this._showedOptInOnTabs.add(tabId);
+
+ const { bug, name } = this;
+ const warning = `${name} is allowed on ${origin} for this browsing session due to user opt-in. See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
+ return browser.tabs
+ .executeScript(tabId, {
+ code: `console.warn(${JSON.stringify(warning)})`,
+ runAt: "document_start",
+ })
+ .catch(() => {});
+ }
+}
+
+class Shims {
+ constructor(availableShims) {
+ if (!browser.trackingProtection) {
+ console.error("Required experimental add-on APIs for shims unavailable");
+ return;
+ }
+
+ this._registerShims(availableShims);
+
+ onMessageFromTab(this._onMessageFromShim.bind(this));
+
+ this.ENABLED_PREF = "enable_shims";
+ browser.aboutConfigPrefs.onPrefChange.addListener(() => {
+ this._checkEnabledPref();
+ }, this.ENABLED_PREF);
+ this._haveCheckedEnabledPref = this._checkEnabledPref();
+ }
+
+ bindAboutCompatBroker(broker) {
+ this._aboutCompatBroker = broker;
+ }
+
+ getShimInfoForAboutCompat(shim) {
+ const { bug, disabledReason, hiddenInAboutCompat, id, name } = shim;
+ const type = "smartblock";
+ return { bug, disabledReason, hidden: hiddenInAboutCompat, id, name, type };
+ }
+
+ disableShimForSession(id) {
+ const shim = this.shims.get(id);
+ shim?.disableForSession();
+ }
+
+ enableShimForSession(id) {
+ const shim = this.shims.get(id);
+ shim?.enableForSession();
+ }
+
+ onShimStateChanged(id) {
+ if (!this._aboutCompatBroker) {
+ return;
+ }
+
+ const shim = this.shims.get(id);
+ if (!shim) {
+ return;
+ }
+
+ const shimsChanged = [this.getShimInfoForAboutCompat(shim)];
+ this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({ shimsChanged });
+ }
+
+ getAvailableShims() {
+ const shims = Array.from(this.shims.values()).map(
+ this.getShimInfoForAboutCompat
+ );
+ shims.sort((a, b) => a.name.localeCompare(b.name));
+ return shims;
+ }
+
+ _registerShims(shims) {
+ if (this.shims) {
+ throw new Error("_registerShims has already been called");
+ }
+
+ this.shims = new Map();
+ for (const shimOpts of shims) {
+ const { id } = shimOpts;
+ if (!this.shims.has(id)) {
+ this.shims.set(shimOpts.id, new Shim(shimOpts, this));
+ }
+ }
+
+ // Register onBeforeRequest listener which handles storage access requests
+ // on matching redirects.
+ let redirectTargetUrls = Array.from(shims.values())
+ .filter(shim => shim.requestStorageAccessForRedirect)
+ .flatMap(shim => shim.requestStorageAccessForRedirect)
+ .map(([, dstUrl]) => dstUrl);
+
+ // Unique target urls.
+ redirectTargetUrls = Array.from(new Set(redirectTargetUrls));
+
+ if (redirectTargetUrls.length) {
+ debug("Registering redirect listener for requestStorageAccess helper", {
+ redirectTargetUrls,
+ });
+ browser.webRequest.onBeforeRequest.addListener(
+ this._onRequestStorageAccessRedirect.bind(this),
+ { urls: redirectTargetUrls, types: ["main_frame"] },
+ ["blocking"]
+ );
+ }
+
+ function addTypePatterns(type, patterns, set) {
+ if (!set.has(type)) {
+ set.set(type, { patterns: new Set() });
+ }
+ const allSet = set.get(type).patterns;
+ for (const pattern of patterns) {
+ allSet.add(pattern);
+ }
+ }
+
+ const allMatchTypePatterns = new Map();
+ const allHeaderChangingMatchTypePatterns = new Map();
+ const allLogos = [];
+ for (const shim of this.shims.values()) {
+ const { logos, matches } = shim;
+ allLogos.push(...logos);
+ for (const { patterns, target, types } of matches || []) {
+ for (const type of types) {
+ if (shim.isGoogleTrendsDFPIFix) {
+ addTypePatterns(type, patterns, allHeaderChangingMatchTypePatterns);
+ }
+ if (target || shim.file || shim.runFirst) {
+ addTypePatterns(type, patterns, allMatchTypePatterns);
+ }
+ }
+ }
+ }
+
+ if (allLogos.length) {
+ const urls = Array.from(new Set(allLogos)).map(l => {
+ return `${LogosBaseURL}${l}`;
+ });
+ debug("Allowing access to these logos:", urls);
+ const unmarkShimsActive = tabId => {
+ for (const shim of this.shims.values()) {
+ shim.setActiveOnTab(tabId, false);
+ }
+ };
+ browser.tabs.onRemoved.addListener(unmarkShimsActive);
+ browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.discarded || changeInfo.url) {
+ unmarkShimsActive(tabId);
+ }
+ });
+ browser.webRequest.onBeforeRequest.addListener(
+ this._redirectLogos.bind(this),
+ { urls, types: ["image"] },
+ ["blocking"]
+ );
+ }
+
+ if (allHeaderChangingMatchTypePatterns) {
+ for (const [
+ type,
+ { patterns },
+ ] of allHeaderChangingMatchTypePatterns.entries()) {
+ const urls = Array.from(patterns);
+ debug("Shimming these", type, "URLs:", urls);
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ this._onBeforeSendHeaders.bind(this),
+ { urls, types: [type] },
+ ["blocking", "requestHeaders"]
+ );
+ browser.webRequest.onHeadersReceived.addListener(
+ this._onHeadersReceived.bind(this),
+ { urls, types: [type] },
+ ["blocking", "responseHeaders"]
+ );
+ }
+ }
+
+ if (!allMatchTypePatterns.size) {
+ debug("Skipping shims; none enabled");
+ return;
+ }
+
+ for (const [type, { patterns }] of allMatchTypePatterns.entries()) {
+ const urls = Array.from(patterns);
+ debug("Shimming these", type, "URLs:", urls);
+
+ browser.webRequest.onBeforeRequest.addListener(
+ this._ensureShimForRequestOnTab.bind(this),
+ { urls, types: [type] },
+ ["blocking"]
+ );
+ }
+ }
+
+ async _checkEnabledPref() {
+ await browser.aboutConfigPrefs.getPref(this.ENABLED_PREF).then(value => {
+ if (value === undefined) {
+ browser.aboutConfigPrefs.setPref(this.ENABLED_PREF, true);
+ } else if (value === false) {
+ this.enabled = false;
+ } else {
+ this.enabled = true;
+ }
+ });
+ }
+
+ get enabled() {
+ return this._enabled;
+ }
+
+ set enabled(enabled) {
+ if (enabled === this._enabled) {
+ return;
+ }
+
+ this._enabled = enabled;
+
+ for (const shim of this.shims.values()) {
+ if (enabled) {
+ shim.onAllShimsEnabled();
+ } else {
+ shim.onAllShimsDisabled();
+ }
+ }
+ }
+
+ async _onRequestStorageAccessRedirect({
+ originUrl: srcUrl,
+ url: dstUrl,
+ tabId,
+ }) {
+ debug("Detected redirect", { srcUrl, dstUrl, tabId });
+
+ // Check if a shim needs to request storage access for this redirect. This
+ // handler is called when the *source url* matches a shims redirect pattern,
+ // but we still need to check if the *destination url* matches.
+ const matchingShims = Array.from(this.shims.values()).filter(shim => {
+ const { enabled, requestStorageAccessForRedirect } = shim;
+
+ if (!enabled || !requestStorageAccessForRedirect) {
+ return false;
+ }
+
+ return requestStorageAccessForRedirect.some(
+ ([srcPattern, dstPattern]) =>
+ browser.matchPatterns.getMatcher([srcPattern]).matches(srcUrl) &&
+ browser.matchPatterns.getMatcher([dstPattern]).matches(dstUrl)
+ );
+ });
+
+ // For each matching shim, find out if its enabled in regard to dFPI state.
+ const bugNumbers = new Set();
+ let isDFPIActive = null;
+ await Promise.all(
+ matchingShims.map(async shim => {
+ if (shim.onlyIfDFPIActive) {
+ // Only get the dFPI state for the first shim which requires it.
+ if (isDFPIActive === null) {
+ const tabIsPB = (await browser.tabs.get(tabId)).incognito;
+ isDFPIActive = await browser.trackingProtection.isDFPIActive(
+ tabIsPB
+ );
+ }
+ if (!isDFPIActive) {
+ return;
+ }
+ }
+ bugNumbers.add(shim.bug);
+ })
+ );
+
+ // If there is no shim which needs storage access for this redirect src/dst
+ // pair, resume it.
+ if (!bugNumbers.size) {
+ return;
+ }
+
+ // Inject the helper to call requestStorageAccessForOrigin on the document.
+ await browser.tabs.executeScript(tabId, {
+ file: "/lib/requestStorageAccess_helper.js",
+ runAt: "document_start",
+ });
+
+ const bugUrls = Array.from(bugNumbers)
+ .map(bugNo => `https://bugzilla.mozilla.org/show_bug.cgi?id=${bugNo}`)
+ .join(", ");
+ const warning = `Firefox calls the Storage Access API for ${dstUrl} on behalf of ${srcUrl}. See the following bugs for details: ${bugUrls}`;
+
+ // Request storage access for the origin of the destination url of the
+ // redirect.
+ const { origin: requestStorageAccessOrigin } = new URL(dstUrl);
+
+ // Wait for the requestStorageAccess request to finish before resuming the
+ // redirect.
+ const { success } = await browser.tabs.sendMessage(tabId, {
+ requestStorageAccessOrigin,
+ warning,
+ });
+ debug("requestStorageAccess callback", {
+ success,
+ requestStorageAccessOrigin,
+ srcUrl,
+ dstUrl,
+ bugNumbers,
+ });
+ }
+
+ async _onMessageFromShim(payload, sender, sendResponse) {
+ const { tab, frameId } = sender;
+ const { id, url } = tab;
+ const { shimId, message } = payload;
+
+ // Ignore unknown messages (for instance, from about:compat).
+ if (message !== "getOptions" && message !== "optIn") {
+ return undefined;
+ }
+
+ if (sender.id !== browser.runtime.id || id === -1) {
+ throw new Error("not allowed");
+ }
+
+ // Important! It is entirely possible for sites to spoof
+ // these messages, due to shims allowing web pages to
+ // communicate with the extension.
+
+ const shim = this.shims.get(shimId);
+ if (!shim?.needsShimHelpers?.includes(message)) {
+ throw new Error("not allowed");
+ }
+
+ if (message === "getOptions") {
+ return Object.assign(
+ {
+ platform: await platformPromise,
+ releaseBranch: await releaseBranchPromise,
+ },
+ shim.options
+ );
+ } else if (message === "optIn") {
+ try {
+ await shim.onUserOptIn(new URL(url).hostname);
+ const origin = new URL(tab.url).origin;
+ warn(
+ "** User opted in for",
+ shim.name,
+ "shim on",
+ origin,
+ "on tab",
+ id,
+ "frame",
+ frameId
+ );
+ await shim.showOptInWarningOnce(id, origin);
+ } catch (err) {
+ console.error(err);
+ throw new Error("error");
+ }
+ }
+
+ return undefined;
+ }
+
+ async _redirectLogos(details) {
+ await this._haveCheckedEnabledPref;
+
+ if (!this.enabled) {
+ return { cancel: true };
+ }
+
+ const { tabId, url } = details;
+ const logo = new URL(url).pathname.slice(1);
+
+ for (const shim of this.shims.values()) {
+ await shim.ready;
+
+ if (!shim.enabled) {
+ continue;
+ }
+
+ if (shim.onlyIfDFPIActive) {
+ const isPB = (await browser.tabs.get(details.tabId)).incognito;
+ if (!(await browser.trackingProtection.isDFPIActive(isPB))) {
+ continue;
+ }
+ }
+
+ if (!shim.logos.includes(logo)) {
+ continue;
+ }
+
+ if (shim.isActiveOnTab(tabId)) {
+ return { redirectUrl: browser.runtime.getURL(`shims/${logo}`) };
+ }
+ }
+
+ return { cancel: true };
+ }
+
+ async _onHeadersReceived(details) {
+ await this._haveCheckedEnabledPref;
+
+ for (const shim of this.shims.values()) {
+ await shim.ready;
+
+ if (!shim.enabled) {
+ continue;
+ }
+
+ if (shim.onlyIfDFPIActive) {
+ const isPB = (await browser.tabs.get(details.tabId)).incognito;
+ if (!(await browser.trackingProtection.isDFPIActive(isPB))) {
+ continue;
+ }
+ }
+
+ if (shim.isGoogleTrendsDFPIFix) {
+ if (shim.GoogleNidCookieToUse) {
+ continue;
+ }
+
+ for (const header of details.responseHeaders) {
+ if (header.name == "set-cookie") {
+ shim.GoogleNidCookieToUse = header.value;
+ return { redirectUrl: details.url };
+ }
+ }
+ }
+ }
+
+ return undefined;
+ }
+
+ async _onBeforeSendHeaders(details) {
+ await this._haveCheckedEnabledPref;
+
+ const { frameId, requestHeaders, tabId } = details;
+
+ if (!this.enabled) {
+ return { requestHeaders };
+ }
+
+ for (const shim of this.shims.values()) {
+ await shim.ready;
+
+ if (!shim.enabled) {
+ continue;
+ }
+
+ if (shim.isGoogleTrendsDFPIFix) {
+ const value = shim.GoogleNidCookieToUse;
+
+ if (!value) {
+ continue;
+ }
+
+ let found;
+ for (let header of requestHeaders) {
+ if (header.name.toLowerCase() === "cookie") {
+ header.value = value;
+ found = true;
+ }
+ }
+ if (!found) {
+ requestHeaders.push({ name: "Cookie", value });
+ }
+
+ browser.tabs
+ .get(tabId)
+ .then(({ url }) => {
+ debug(
+ `Google Trends dFPI fix used on tab ${tabId} frame ${frameId} (${url})`
+ );
+ })
+ .catch(() => {});
+
+ const warning = `Working around Google Trends tracking protection breakage. See https://bugzilla.mozilla.org/show_bug.cgi?id=${shim.bug} for details.`;
+ browser.tabs
+ .executeScript(tabId, {
+ code: `console.warn(${JSON.stringify(warning)})`,
+ runAt: "document_start",
+ })
+ .catch(() => {});
+ }
+ }
+
+ return { requestHeaders };
+ }
+
+ async _ensureShimForRequestOnTab(details) {
+ await this._haveCheckedEnabledPref;
+
+ if (!this.enabled) {
+ return undefined;
+ }
+
+ // We only ever reach this point if a request is for a URL which ought to
+ // be shimmed. We never get here if a request is blocked, and we only
+ // unblock requests if at least one shim matches it.
+
+ const { frameId, originUrl, requestId, tabId, type, url } = details;
+
+ // Ignore requests unrelated to tabs
+ if (tabId < 0) {
+ return undefined;
+ }
+
+ // We need to base our checks not on the frame's host, but the tab's.
+ const topHost = new URL((await browser.tabs.get(tabId)).url).hostname;
+ const unblocked = await browser.trackingProtection.wasRequestUnblocked(
+ requestId
+ );
+
+ let match;
+ let shimToApply;
+ for (const shim of this.shims.values()) {
+ await shim.ready;
+
+ if (!shim.enabled || (!shim.redirectsRequests && !shim.runFirst)) {
+ continue;
+ }
+
+ if (shim.onlyIfDFPIActive || shim.onlyIfPrivateBrowsing) {
+ const isPB = (await browser.tabs.get(details.tabId)).incognito;
+ if (!isPB && shim.onlyIfPrivateBrowsing) {
+ continue;
+ }
+ if (
+ shim.onlyIfDFPIActive &&
+ !(await browser.trackingProtection.isDFPIActive(isPB))
+ ) {
+ continue;
+ }
+ }
+
+ // Do not apply the shim if it is only meant to apply when strict mode ETP
+ // (content blocking) was going to block the request.
+ if (!unblocked && shim.onlyIfBlockedByETP) {
+ continue;
+ }
+
+ if (!shim.meantForHost(topHost)) {
+ continue;
+ }
+
+ // If this URL and content type isn't meant for this shim, don't apply it.
+ match = shim.isTriggeredByURLAndType(url, type);
+ if (match) {
+ if (!unblocked && match.onlyIfBlockedByETP) {
+ continue;
+ }
+
+ // If the user has already opted in for this shim, all requests it covers
+ // should be allowed; no need for a shim anymore.
+ if (shim.hasUserOptedInAlready(topHost)) {
+ warn(
+ `Allowing tracking ${type} ${url} on tab ${tabId} frame ${frameId} due to opt-in`
+ );
+ shim.showOptInWarningOnce(tabId, new URL(originUrl).origin);
+ return undefined;
+ }
+ shimToApply = shim;
+ break;
+ }
+ }
+
+ let runFirst = false;
+
+ if (shimToApply) {
+ // Note that sites may request the same shim twice, but because the requests
+ // may differ enough for some to fail (CSP/CORS/etc), we always let the request
+ // complete via local redirect. Shims should gracefully handle this as well.
+
+ const { target } = match;
+ const { bug, file, id, name, needsShimHelpers } = shimToApply;
+ runFirst = shimToApply.runFirst;
+
+ const redirect = target || file;
+
+ warn(
+ `Shimming tracking ${type} ${url} on tab ${tabId} frame ${frameId} with ${
+ redirect || runFirst
+ }`
+ );
+
+ const warning = `${name} is being shimmed by Firefox. See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
+
+ let needConsoleMessage = true;
+
+ if (runFirst) {
+ try {
+ await browser.tabs.executeScript(tabId, {
+ file: `/shims/${runFirst}`,
+ frameId,
+ runAt: "document_start",
+ });
+ } catch (_) {}
+ }
+
+ // For scripts, we also set up any needed shim helpers.
+ if (type === "script" && needsShimHelpers?.length) {
+ try {
+ await browser.tabs.executeScript(tabId, {
+ file: "/lib/shim_messaging_helper.js",
+ frameId,
+ runAt: "document_start",
+ });
+ const origin = new URL(originUrl).origin;
+ await browser.tabs.sendMessage(
+ tabId,
+ { origin, shimId: id, needsShimHelpers, warning },
+ { frameId }
+ );
+ needConsoleMessage = false;
+ shimToApply.setActiveOnTab(tabId);
+ } catch (_) {}
+ }
+
+ if (needConsoleMessage) {
+ try {
+ await browser.tabs.executeScript(tabId, {
+ code: `console.warn(${JSON.stringify(warning)})`,
+ runAt: "document_start",
+ });
+ } catch (_) {}
+ }
+
+ if (!redirect.indexOf("http://") || !redirect.indexOf("https://")) {
+ return { redirectUrl: redirect };
+ }
+
+ // If any shims matched the request to replace it, then redirect to the local
+ // file bundled with SmartBlock, so the request never hits the network.
+ return { redirectUrl: browser.runtime.getURL(`shims/${redirect}`) };
+ }
+
+ // Sanity check: if no shims end up handling this request,
+ // yet it was meant to be blocked by ETP, then block it now.
+ if (unblocked) {
+ error(`unexpected: ${url} not shimmed on tab ${tabId} frame ${frameId}`);
+ return { cancel: true };
+ }
+
+ if (!runFirst) {
+ debug(`ignoring ${url} on tab ${tabId} frame ${frameId}`);
+ }
+ return undefined;
+ }
+}
+
+module.exports = Shims;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/ua_helpers.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/ua_helpers.js
new file mode 100644
index 0000000000..2cc848f8b0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/ua_helpers.js
@@ -0,0 +1,99 @@
+/* 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/. */
+
+"use strict";
+
+/* globals exportFunction, module */
+
+var UAHelpers = {
+ _deviceAppropriateChromeUAs: {},
+ getDeviceAppropriateChromeUA(config = {}) {
+ const { version = "103.0.5060.71", androidDevice, desktopOS } = config;
+ const key = `${version}:${androidDevice}:${desktopOS}`;
+ if (!UAHelpers._deviceAppropriateChromeUAs[key]) {
+ const userAgent =
+ typeof navigator !== "undefined" ? navigator.userAgent : "";
+ const RunningFirefoxVersion = (userAgent.match(/Firefox\/([0-9.]+)/) || [
+ "",
+ "58.0",
+ ])[1];
+
+ if (userAgent.includes("Android")) {
+ const RunningAndroidVersion =
+ userAgent.match(/Android [0-9.]+/) || "Android 6.0";
+ if (androidDevice) {
+ UAHelpers._deviceAppropriateChromeUAs[
+ key
+ ] = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; ${androidDevice}) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Mobile Safari/537.36`;
+ } else {
+ const ChromePhoneUA = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; Nexus 5 Build/MRA58N) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Mobile Safari/537.36`;
+ const ChromeTabletUA = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; Nexus 7 Build/JSS15Q) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`;
+ const IsPhone = userAgent.includes("Mobile");
+ UAHelpers._deviceAppropriateChromeUAs[key] = IsPhone
+ ? ChromePhoneUA
+ : ChromeTabletUA;
+ }
+ } else {
+ let osSegment = "Windows NT 10.0; Win64; x64";
+ if (desktopOS === "macOS" || userAgent.includes("Macintosh")) {
+ osSegment = "Macintosh; Intel Mac OS X 10_15_7";
+ }
+ if (
+ desktopOS !== "nonLinux" &&
+ (desktopOS === "linux" || userAgent.includes("Linux"))
+ ) {
+ osSegment = "X11; Ubuntu; Linux x86_64";
+ }
+
+ UAHelpers._deviceAppropriateChromeUAs[
+ key
+ ] = `Mozilla/5.0 (${osSegment}) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`;
+ }
+ }
+ return UAHelpers._deviceAppropriateChromeUAs[key];
+ },
+ getPrefix(originalUA) {
+ return originalUA.substr(0, originalUA.indexOf(")") + 1);
+ },
+ overrideWithDeviceAppropriateChromeUA(config) {
+ const chromeUA = UAHelpers.getDeviceAppropriateChromeUA(config);
+ Object.defineProperty(window.navigator.wrappedJSObject, "userAgent", {
+ get: exportFunction(() => chromeUA, window),
+ set: exportFunction(function () {}, window),
+ });
+ },
+ capVersionTo99(originalUA) {
+ const ver = originalUA.match(/Firefox\/(\d+\.\d+)/);
+ if (!ver || parseFloat(ver[1]) < 100) {
+ return originalUA;
+ }
+ return originalUA
+ .replace(`Firefox/${ver[1]}`, "Firefox/99.0")
+ .replace(`rv:${ver[1]}`, "rv:99.0");
+ },
+ capRvTo109(originalUA) {
+ const ver = originalUA.match(/rv:(\d+\.\d+)/);
+ if (!ver || parseFloat(ver[1]) <= 109) {
+ return originalUA;
+ }
+ return originalUA.replace(`rv:${ver[1]}`, "rv:109.0");
+ },
+ capVersionToNumber(originalUA, cap = 120) {
+ const ver = originalUA.match(/Firefox\/(\d+\.\d+)/);
+ if (!ver || parseFloat(ver[1]) <= cap) {
+ return originalUA;
+ }
+ const capped = `Firefox/${cap}.0`;
+ return originalUA.replace(`Firefox/${ver[1]}`, capped);
+ },
+ getWindowsUA(originalUA) {
+ const rv = originalUA.match("rv:[0-9]+.[0-9]+")[0];
+ const ver = originalUA.match("Firefox/[0-9]+.[0-9]+")[0];
+ return `Mozilla/5.0 (Windows NT 10.0; Win64; x64; ${rv}) Gecko/20100101 ${ver}`;
+ },
+};
+
+if (typeof module !== "undefined") {
+ module.exports = UAHelpers;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/ua_overrides.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/ua_overrides.js
new file mode 100644
index 0000000000..2426293f3f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/lib/ua_overrides.js
@@ -0,0 +1,210 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser, module */
+
+class UAOverrides {
+ constructor(availableOverrides) {
+ this.OVERRIDE_PREF = "perform_ua_overrides";
+
+ this._overridesEnabled = true;
+
+ this._availableOverrides = availableOverrides;
+ this._activeListeners = new Map();
+ }
+
+ bindAboutCompatBroker(broker) {
+ this._aboutCompatBroker = broker;
+ }
+
+ bootup() {
+ browser.aboutConfigPrefs.onPrefChange.addListener(() => {
+ this.checkOverridePref();
+ }, this.OVERRIDE_PREF);
+ this.checkOverridePref();
+ }
+
+ checkOverridePref() {
+ browser.aboutConfigPrefs.getPref(this.OVERRIDE_PREF).then(value => {
+ if (value === undefined) {
+ browser.aboutConfigPrefs.setPref(this.OVERRIDE_PREF, true);
+ } else if (value === false) {
+ this.unregisterUAOverrides();
+ } else {
+ this.registerUAOverrides();
+ }
+ });
+ }
+
+ getAvailableOverrides() {
+ return this._availableOverrides;
+ }
+
+ isEnabled() {
+ return this._overridesEnabled;
+ }
+
+ enableOverride(override) {
+ if (override.active) {
+ return;
+ }
+
+ const { blocks, matches, uaTransformer } = override.config;
+ const listener = details => {
+ // Don't actually override the UA for an experiment if the user is not
+ // part of the experiment (unless they force-enabed the override).
+ if (
+ !override.config.experiment ||
+ override.permanentPrefEnabled === true
+ ) {
+ for (const header of details.requestHeaders) {
+ if (header.name.toLowerCase() === "user-agent") {
+ // Don't override the UA if we're on a mobile device that has the
+ // "Request Desktop Site" mode enabled. The UA for the desktop mode
+ // is set inside Gecko with a simple string replace, so we can use
+ // that as a check, see https://searchfox.org/mozilla-central/rev/89d33e1c3b0a57a9377b4815c2f4b58d933b7c32/mobile/android/chrome/geckoview/GeckoViewSettingsChild.js#23-28
+ let isMobileWithDesktopMode =
+ override.currentPlatform == "android" &&
+ header.value.includes("X11; Linux x86_64");
+
+ if (!isMobileWithDesktopMode) {
+ header.value = uaTransformer(header.value);
+ }
+ }
+ }
+ }
+ return { requestHeaders: details.requestHeaders };
+ };
+
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ listener,
+ { urls: matches },
+ ["blocking", "requestHeaders"]
+ );
+
+ const listeners = { onBeforeSendHeaders: listener };
+ if (blocks) {
+ const blistener = details => {
+ return { cancel: true };
+ };
+
+ browser.webRequest.onBeforeRequest.addListener(
+ blistener,
+ { urls: blocks },
+ ["blocking"]
+ );
+
+ listeners.onBeforeRequest = blistener;
+ }
+ this._activeListeners.set(override, listeners);
+ override.active = true;
+ }
+
+ onOverrideConfigChanged(override) {
+ // Check whether the override should be hidden from about:compat.
+ override.hidden = override.config.hidden;
+
+ // Setting the override's permanent pref overrules whether it is hidden.
+ if (override.permanentPrefEnabled !== undefined) {
+ override.hidden = !override.permanentPrefEnabled;
+ }
+
+ // Also check whether the override should be active.
+ let shouldBeActive = true;
+
+ // Overrides can be force-deactivated by their permanent preference.
+ if (override.permanentPrefEnabled === false) {
+ shouldBeActive = false;
+ }
+
+ // Overrides gated behind an experiment the user is not part of do not
+ // have to be activated, unless they are gathering telemetry, or the
+ // user has force-enabled them with their permanent pref.
+ if (override.config.experiment && override.permanentPrefEnabled !== true) {
+ shouldBeActive = false;
+ }
+
+ if (shouldBeActive) {
+ this.enableOverride(override);
+ } else {
+ this.disableOverride(override);
+ }
+
+ if (this._overridesEnabled) {
+ this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
+ overridesChanged: this._aboutCompatBroker.filterOverrides(
+ this._availableOverrides
+ ),
+ });
+ }
+ }
+
+ async registerUAOverrides() {
+ const platformMatches = ["all"];
+ let platformInfo = await browser.runtime.getPlatformInfo();
+ platformMatches.push(platformInfo.os == "android" ? "android" : "desktop");
+
+ for (const override of this._availableOverrides) {
+ if (platformMatches.includes(override.platform)) {
+ override.availableOnPlatform = true;
+ override.currentPlatform = platformInfo.os;
+
+ // If there is a specific about:config preference governing
+ // this override, monitor its state.
+ const pref = override.config.permanentPref;
+ override.permanentPrefEnabled =
+ pref && (await browser.aboutConfigPrefs.getPref(pref));
+ if (pref) {
+ const checkOverridePref = () => {
+ browser.aboutConfigPrefs.getPref(pref).then(value => {
+ override.permanentPrefEnabled = value;
+ this.onOverrideConfigChanged(override);
+ });
+ };
+ browser.aboutConfigPrefs.onPrefChange.addListener(
+ checkOverridePref,
+ pref
+ );
+ }
+
+ this.onOverrideConfigChanged(override);
+ }
+ }
+
+ this._overridesEnabled = true;
+ this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
+ overridesChanged: this._aboutCompatBroker.filterOverrides(
+ this._availableOverrides
+ ),
+ });
+ }
+
+ unregisterUAOverrides() {
+ for (const override of this._availableOverrides) {
+ this.disableOverride(override);
+ }
+
+ this._overridesEnabled = false;
+ this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
+ overridesChanged: false,
+ });
+ }
+
+ disableOverride(override) {
+ if (!override.active) {
+ return;
+ }
+
+ const listeners = this._activeListeners.get(override);
+ for (const [name, listener] of Object.entries(listeners)) {
+ browser.webRequest[name].removeListener(listener);
+ }
+ override.active = false;
+ this._activeListeners.delete(override);
+ }
+}
+
+module.exports = UAOverrides;
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/manifest.json b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/manifest.json
new file mode 100644
index 0000000000..6f6d519d7c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/manifest.json
@@ -0,0 +1,160 @@
+{
+ "manifest_version": 2,
+ "name": "Mozilla Android Components - Web Compatibility Interventions",
+ "description": "Urgent post-release fixes for web compatibility.",
+ "version": "125.0.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "webcompat@mozilla.org",
+ "strict_min_version": "102.0"
+ }
+ },
+
+ "experiment_apis": {
+ "aboutConfigPrefs": {
+ "schema": "experiment-apis/aboutConfigPrefs.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experiment-apis/aboutConfigPrefs.js",
+ "paths": [["aboutConfigPrefs"]]
+ },
+ "child": {
+ "scopes": ["addon_child"],
+ "script": "experiment-apis/aboutConfigPrefsChild.js",
+ "paths": [["aboutConfigPrefs"]]
+ }
+ },
+ "appConstants": {
+ "schema": "experiment-apis/appConstants.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experiment-apis/appConstants.js",
+ "paths": [["appConstants"]]
+ }
+ },
+ "aboutPage": {
+ "schema": "about-compat/aboutPage.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "about-compat/aboutPage.js",
+ "events": ["startup"]
+ }
+ },
+ "matchPatterns": {
+ "schema": "experiment-apis/matchPatterns.json",
+ "child": {
+ "scopes": ["addon_child"],
+ "script": "experiment-apis/matchPatterns.js",
+ "paths": [["matchPatterns"]]
+ }
+ },
+ "systemManufacturer": {
+ "schema": "experiment-apis/systemManufacturer.json",
+ "child": {
+ "scopes": ["addon_child"],
+ "script": "experiment-apis/systemManufacturer.js",
+ "paths": [["systemManufacturer"]]
+ }
+ },
+ "trackingProtection": {
+ "schema": "experiment-apis/trackingProtection.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experiment-apis/trackingProtection.js",
+ "paths": [["trackingProtection"]]
+ }
+ }
+ },
+
+ "content_security_policy": "script-src 'self' 'sha256-PeZc2H1vv7M8NXqlFyNbN4y4oM6wXmYEbf73m+Aqpak='; default-src 'self'; base-uri moz-extension://*; object-src 'none'",
+
+ "permissions": [
+ "mozillaAddons",
+ "scripting",
+ "tabs",
+ "webNavigation",
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>"
+ ],
+
+ "background": {
+ "scripts": [
+ "lib/module_shim.js",
+ "lib/messaging_helper.js",
+ "lib/intervention_helpers.js",
+ "lib/requestStorageAccess_helper.js",
+ "lib/ua_helpers.js",
+ "data/injections.js",
+ "data/shims.js",
+ "data/ua_overrides.js",
+ "lib/about_compat_broker.js",
+ "lib/custom_functions.js",
+ "lib/injections.js",
+ "lib/shims.js",
+ "lib/ua_overrides.js",
+ "run.js"
+ ]
+ },
+
+ "web_accessible_resources": [
+ "shims/addthis-angular.js",
+ "shims/adform.js",
+ "shims/adnexus-ast.js",
+ "shims/adnexus-prebid.js",
+ "shims/adsafeprotected-ima.js",
+ "shims/apstag.js",
+ "shims/blogger.js",
+ "shims/bloggerAccount.js",
+ "shims/bmauth.js",
+ "shims/branch.js",
+ "shims/chartbeat.js",
+ "shims/crave-ca.js",
+ "shims/criteo.js",
+ "shims/cxense.js",
+ "shims/doubleverify.js",
+ "shims/eluminate.js",
+ "shims/empty-script.js",
+ "shims/empty-shim.txt",
+ "shims/everest.js",
+ "shims/facebook-sdk.js",
+ "shims/facebook.svg",
+ "shims/fastclick.js",
+ "shims/firebase.js",
+ "shims/google-ads.js",
+ "shims/google-analytics-and-tag-manager.js",
+ "shims/google-analytics-ecommerce-plugin.js",
+ "shims/google-analytics-legacy.js",
+ "shims/google-ima.js",
+ "shims/google-page-ad.js",
+ "shims/google-publisher-tags.js",
+ "shims/google-safeframe.html",
+ "shims/history.js",
+ "shims/iam.js",
+ "shims/iaspet.js",
+ "shims/instagram.js",
+ "shims/kinja.js",
+ "shims/live-test-shim.js",
+ "shims/maxmind-geoip.js",
+ "shims/microsoftLogin.js",
+ "shims/microsoftVirtualAssistant.js",
+ "shims/moat.js",
+ "shims/mochitest-shim-1.js",
+ "shims/mochitest-shim-2.js",
+ "shims/mochitest-shim-3.js",
+ "shims/nielsen.js",
+ "shims/optimizely.js",
+ "shims/play.svg",
+ "shims/rambler-authenticator.js",
+ "shims/rich-relevance.js",
+ "shims/salesforce.js",
+ "shims/spotify-embed.js",
+ "shims/tracking-pixel.png",
+ "shims/tsn-ca.js",
+ "shims/vast2.xml",
+ "shims/vast3.xml",
+ "shims/vidible.js",
+ "shims/vmad.xml",
+ "shims/webtrends.js"
+ ]
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/run.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/run.js
new file mode 100644
index 0000000000..5822dbb2ca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/run.js
@@ -0,0 +1,45 @@
+/* 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/. */
+
+"use strict";
+
+/* globals AboutCompatBroker, AVAILABLE_INJECTIONS, AVAILABLE_SHIMS,
+ AVAILABLE_PIP_OVERRIDES, AVAILABLE_UA_OVERRIDES, CUSTOM_FUNCTIONS,
+ Injections, Shims, UAOverrides */
+
+let injections, shims, uaOverrides;
+
+try {
+ injections = new Injections(AVAILABLE_INJECTIONS, CUSTOM_FUNCTIONS);
+ injections.bootup();
+} catch (e) {
+ console.error("Injections failed to start", e);
+ injections = undefined;
+}
+
+try {
+ uaOverrides = new UAOverrides(AVAILABLE_UA_OVERRIDES);
+ uaOverrides.bootup();
+} catch (e) {
+ console.error("UA overrides failed to start", e);
+ uaOverrides = undefined;
+}
+
+try {
+ shims = new Shims(AVAILABLE_SHIMS);
+} catch (e) {
+ console.error("Shims failed to start", e);
+ shims = undefined;
+}
+
+try {
+ const aboutCompatBroker = new AboutCompatBroker({
+ injections,
+ shims,
+ uaOverrides,
+ });
+ aboutCompatBroker.bootup();
+} catch (e) {
+ console.error("about:compat broker failed to start", e);
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/addthis-angular.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/addthis-angular.js
new file mode 100644
index 0000000000..0f0cdd5029
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/addthis-angular.js
@@ -0,0 +1,16 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713694 - Shim AddThis Angular module
+ *
+ * Sites using Angular with AddThis can break entirely if the module is
+ * blocked. This shim mitigates that breakage by loading an empty module.
+ */
+
+if (!window.addthisModule) {
+ window.addthisModule = window?.angular?.module("addthis", ["ng"]);
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adform.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adform.js
new file mode 100644
index 0000000000..d6727d500e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adform.js
@@ -0,0 +1,30 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713695 - Shim Adform tracking
+ *
+ * Sites such as m.tim.it may gate content behind AdForm's trackpoint,
+ * breaking download links and such if blocked. This shim stubs out the
+ * script and its related tracking pixel, so the content still works.
+ */
+
+if (!window.Adform) {
+ window.Adform = {
+ Opt: {
+ disableRedirect() {},
+ getStatus(clientID, callback) {
+ callback({
+ clientID,
+ errorMessage: undefined,
+ optIn() {},
+ optOut() {},
+ status: "nocookie",
+ });
+ },
+ },
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adnexus-ast.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adnexus-ast.js
new file mode 100644
index 0000000000..ae07fa6a03
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adnexus-ast.js
@@ -0,0 +1,210 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1734130 - Shim AdNexus AST
+ *
+ * Some sites expect AST to successfully load, or they break.
+ * This shim mitigates that breakage.
+ */
+
+if (!window.apntag?.loaded) {
+ const anq = window.apntag?.anq || [];
+
+ const gTags = new Map();
+ const gAds = new Map();
+ const gEventHandlers = {};
+
+ const Ad = class {
+ adType = "banner";
+ auctionId = "-";
+ banner = {
+ width: 1,
+ height: 1,
+ content: "",
+ trackers: {
+ impression_urls: [],
+ video_events: {},
+ },
+ };
+ brandCategoryId = 0;
+ buyerMemberId = 0;
+ cpm = 0.1;
+ cpm_publisher_currency = 0.1;
+ creativeId = 0;
+ dealId = undefined;
+ height = 1;
+ mediaSubtypeId = 1;
+ mediaTypeId = 1;
+ publisher_currency_code = "US";
+ source = "-";
+ tagId = -1;
+ targetId = "";
+ width = 1;
+
+ constructor(tagId, targetId) {
+ this.tagId = tagId;
+ this.targetId = targetId;
+ }
+ };
+
+ const fireAdEvent = (type, adObj) => {
+ const { targetId } = adObj;
+ const handlers = gEventHandlers[type]?.[targetId];
+ if (!handlers) {
+ return Promise.resolve();
+ }
+ const evt = { adObj, type };
+ return new Promise(done => {
+ setTimeout(() => {
+ for (const cb of handlers) {
+ try {
+ cb(evt);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ done();
+ }, 1);
+ });
+ };
+
+ const refreshTag = targetId => {
+ const tag = gTags.get(targetId);
+ if (!tag) {
+ return;
+ }
+ if (!gAds.has(targetId)) {
+ gAds.set(targetId, new Ad(tag.tagId, targetId));
+ }
+ const adObj = gAds.get(targetId);
+ fireAdEvent("adRequested", adObj).then(() => {
+ // TODO: do some sites expect adAvailable+adLoaded instead of adNoBid?
+ fireAdEvent("adNoBid", adObj);
+ });
+ };
+
+ const off = (type, targetId, cb) => {
+ gEventHandlers[type]?.[targetId]?.delete(cb);
+ };
+
+ const on = (type, targetId, cb) => {
+ gEventHandlers[type] = gEventHandlers[type] || {};
+ gEventHandlers[type][targetId] =
+ gEventHandlers[type][targetId] || new Set();
+ gEventHandlers[type][targetId].add(cb);
+ };
+
+ const Tag = class {
+ static #nextId = 0;
+ debug = undefined;
+ displayed = false;
+ initialHeight = 1;
+ initialWidth = 1;
+ keywords = {};
+ member = 0;
+ showTagCalled = false;
+ sizes = [];
+ targetId = "";
+ utCalled = true;
+ utDivId = "";
+ utiframeId = "";
+ uuid = "";
+
+ constructor(raw) {
+ const { keywords, sizes, targetId } = raw;
+ this.tagId = Tag.#nextId++;
+ this.keywords = keywords || {};
+ this.sizes = sizes || [];
+ this.targetId = targetId || "";
+ }
+ modifyTag() {}
+ off(type, cb) {
+ off(type, this.targetId, cb);
+ }
+ on(type, cb) {
+ on(type, this.targetId, cb);
+ }
+ setKeywords(kw) {
+ this.keywords = kw;
+ }
+ };
+
+ window.apntag = {
+ anq,
+ attachClickTrackers() {},
+ checkAdAvailable() {},
+ clearPageTargeting() {},
+ clearRequest() {},
+ collapseAd() {},
+ debug: false,
+ defineTag(dfn) {
+ const { targetId } = dfn;
+ if (!targetId) {
+ return;
+ }
+ gTags.set(targetId, new Tag(dfn));
+ },
+ disableDebug() {},
+ dongle: undefined,
+ emitEvent(adObj, type) {
+ fireAdEvent(type, adObj);
+ },
+ enableCookieSet() {},
+ enableDebug() {},
+ fireImpressionTrackers() {},
+ getAdMarkup: () => "",
+ getAdWrap() {},
+ getAstVersion: () => "0.49.0",
+ getPageTargeting() {},
+ getTag(targetId) {
+ return gTags.get(targetId);
+ },
+ handleCb() {},
+ handleMediationBid() {},
+ highlightAd() {},
+ loaded: true,
+ loadTags() {
+ for (const tagName of gTags.keys()) {
+ refreshTag(tagName);
+ }
+ },
+ modifyTag() {},
+ notify() {},
+ offEvent(type, target, cb) {
+ off(type, target, cb);
+ },
+ onEvent(type, target, cb) {
+ on(type, target, cb);
+ },
+ recordErrorEvent() {},
+ refresh() {},
+ registerRenderer() {},
+ requests: {},
+ resizeAd() {},
+ setEndpoint() {},
+ setKeywords() {},
+ setPageOpts() {},
+ setPageTargeting() {},
+ setSafeFrameConfig() {},
+ setSizes() {},
+ showTag() {},
+ };
+
+ const push = function (fn) {
+ if (typeof fn === "function") {
+ try {
+ fn();
+ } catch (e) {
+ console.trace(e);
+ }
+ }
+ };
+
+ anq.push = push;
+
+ anq.forEach(push);
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adnexus-prebid.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adnexus-prebid.js
new file mode 100644
index 0000000000..f0f810f0e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adnexus-prebid.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1694401 - Shim Prebid.js
+ *
+ * Some sites rely on prebid.js to place content, perhaps in conjunction with
+ * other services like Google Publisher Tags and Amazon TAM. This shim prevents
+ * site breakage like image galleries breaking as the user browsers them, by
+ * allowing the content placement to succeed.
+ */
+
+if (!window.pbjs?.requestBids) {
+ const que = window.pbjs?.que || [];
+ const cmd = window.pbjs?.cmd || [];
+ const adUnits = window.pbjs?.adUnits || [];
+
+ window.pbjs = {
+ adUnits,
+ addAdUnits(arr) {
+ if (!Array.isArray(arr)) {
+ arr = [arr];
+ }
+ adUnits.push(arr);
+ },
+ cmd,
+ offEvent() {},
+ que,
+ refreshAds() {},
+ removeAdUnit(codes) {
+ if (!Array.isArray(codes)) {
+ codes = [codes];
+ }
+ for (const code of codes) {
+ for (let i = adUnits.length - 1; i >= 0; i--) {
+ if (adUnits[i].code === code) {
+ adUnits.splice(i, 1);
+ }
+ }
+ }
+ },
+ renderAd() {},
+ requestBids(params) {
+ params?.bidsBackHandler?.();
+ },
+ setConfig() {},
+ setTargetingForGPTAsync() {},
+ };
+
+ const push = function (fn) {
+ if (typeof fn === "function") {
+ try {
+ fn();
+ } catch (e) {
+ console.trace(e);
+ }
+ }
+ };
+
+ que.push = push;
+ cmd.push = push;
+
+ que.forEach(push);
+ cmd.forEach(push);
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adsafeprotected-ima.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adsafeprotected-ima.js
new file mode 100644
index 0000000000..93cd8e1eab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/adsafeprotected-ima.js
@@ -0,0 +1,19 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ *
+ * Sites relying on Ad Safe Protected's adapter for Google IMA may
+ * have broken videos when the script is blocked. This shim stubs
+ * out the API to help mitigate major breakage.
+ */
+
+if (!window.googleImaVansAdapter) {
+ window.googleImaVansAdapter = {
+ init() {},
+ dispose() {},
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/apstag.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/apstag.js
new file mode 100644
index 0000000000..55be05916b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/apstag.js
@@ -0,0 +1,73 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713698 - Shim Amazon Transparent Ad Marketplace's apstag.js
+ *
+ * Some sites such as politico.com rely on Amazon TAM tracker to serve ads,
+ * breaking functionality like galleries if it is blocked. This shim helps
+ * mitigate major breakage in that case.
+ */
+
+if (!window.apstag?._getSlotIdToNameMapping) {
+ const _Q = window.apstag?._Q || [];
+
+ const newBid = config => {
+ return {
+ amznbid: "",
+ amzniid: "",
+ amznp: "",
+ amznsz: "0x0",
+ size: "0x0",
+ slotID: config.slotID,
+ };
+ };
+
+ window.apstag = {
+ _Q,
+ _getSlotIdToNameMapping() {},
+ bids() {},
+ debug() {},
+ deleteId() {},
+ fetchBids(cfg, cb) {
+ if (!Array.isArray(cfg?.slots)) {
+ return;
+ }
+ setTimeout(() => {
+ cb(cfg.slots.map(s => newBid(s)));
+ }, 1);
+ },
+ init() {},
+ punt() {},
+ renderImp() {},
+ renewId() {},
+ setDisplayBids() {},
+ targetingKeys: () => [],
+ thirdPartyData: {},
+ updateId() {},
+ };
+
+ window.apstagLOADED = true;
+
+ _Q.push = function (prefix, args) {
+ try {
+ switch (prefix) {
+ case "f":
+ window.apstag.fetchBids(...args);
+ break;
+ case "i":
+ window.apstag.init(...args);
+ break;
+ }
+ } catch (e) {
+ console.trace(e);
+ }
+ };
+
+ for (const cmd of _Q) {
+ _Q.push(cmd);
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/blogger.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/blogger.js
new file mode 100644
index 0000000000..2182c04f16
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/blogger.js
@@ -0,0 +1,53 @@
+/* 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/. */
+
+/* globals exportFunction */
+
+"use strict";
+
+/**
+ * Blogger powered blogs rely on storage access to https://blogger.com to enable
+ * oauth with Google. For dFPI, sites need to use the Storage Access API to gain
+ * first party storage access. This shim calls requestStorageAccess on behalf of
+ * the site when a user wants to log in via oauth.
+ */
+
+console.warn(
+ `When using oauth, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1776869 for details.`
+);
+
+const GOOGLE_OAUTH_PATH_PREFIX = "https://accounts.google.com/ServiceLogin";
+
+// After permission was granted request (use) storage access and reload
+async function requestGrantedAccess() {
+ const storageAccessPermission = await navigator.permissions.query({
+ name: "storage-access",
+ });
+ const hasStorageAccess = await document.hasStorageAccess();
+ if (storageAccessPermission.state === "granted" && !hasStorageAccess) {
+ await document.requestStorageAccess();
+ location.reload();
+ }
+}
+
+requestGrantedAccess();
+
+// Overwrite the window.open method so we can detect oauth related popups.
+const origOpen = window.wrappedJSObject.open;
+Object.defineProperty(window.wrappedJSObject, "open", {
+ value: exportFunction((url, ...args) => {
+ // Filter oauth popups.
+ if (!url.startsWith(GOOGLE_OAUTH_PATH_PREFIX)) {
+ return origOpen(url, ...args);
+ }
+ // Request storage access for the Blogger iframe.
+ document.requestStorageAccess().then(() => {
+ origOpen(url, ...args);
+ });
+ // We don't have the window object yet which window.open returns, since the
+ // sign-in flow is dependent on the async storage access request. This isn't
+ // a problem as long as the website does not consume it.
+ return null;
+ }, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/bloggerAccount.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/bloggerAccount.js
new file mode 100644
index 0000000000..19e80dbfbe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/bloggerAccount.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+/* globals exportFunction */
+
+"use strict";
+
+/**
+ * Blogger uses Google as the auth provider. The account panel uses a
+ * third-party iframe of https://ogs.google.com, which requires first-party
+ * storage access to authenticate. This shim calls requestStorageAccess on
+ * behalf of the site when the user opens the account panel.
+ */
+
+console.warn(
+ `When logging in with Google, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1777690 for details.`
+);
+
+const STORAGE_ACCESS_ORIGIN = "https://ogs.google.com";
+
+document.documentElement.addEventListener(
+ "click",
+ e => {
+ const { target, isTrusted } = e;
+ if (!isTrusted) {
+ return;
+ }
+
+ const anchorEl = target.closest("a");
+ if (!anchorEl) {
+ return;
+ }
+
+ if (
+ !anchorEl.href.startsWith("https://accounts.google.com/SignOutOptions")
+ ) {
+ return;
+ }
+
+ // The storage access request below runs async so the panel won't open
+ // immediately. Mitigate this UX issue by updating the clicked element's
+ // style so the user gets some immediate feedback.
+ anchorEl.style.opacity = 0.5;
+ e.stopPropagation();
+ e.preventDefault();
+
+ document
+ .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN)
+ .then(() => {
+ // Reload all iframes of ogs.google.com so the first-party cookies are
+ // sent to the server.
+ // The reload mechanism here is a bit of a hack, since we don't have
+ // access to the content window of a cross-origin iframe.
+ document
+ .querySelectorAll("iframe[src^='https://ogs.google.com/']")
+ .forEach(frame => (frame.src += ""));
+ })
+ // Show the panel in both success and error state. When the user denies
+ // the storage access prompt they will see an error message in the account
+ // panel.
+ .finally(() => {
+ anchorEl.style.opacity = 1.0;
+ target.click();
+ });
+ },
+ true
+);
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/bmauth.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/bmauth.js
new file mode 100644
index 0000000000..944f2100d6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/bmauth.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+"use strict";
+
+if (!window.BmAuth) {
+ window.BmAuth = {
+ init: () => new Promise(() => {}),
+ handleSignIn: () => {
+ // TODO: handle this properly!
+ },
+ isAuthenticated: () => Promise.resolve(false),
+ addListener: () => {},
+ api: {
+ event: {
+ addListener: () => {},
+ },
+ },
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/branch.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/branch.js
new file mode 100644
index 0000000000..31e8f4eeec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/branch.js
@@ -0,0 +1,84 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1716220 - Shim Branch Web SDK
+ *
+ * Sites such as TataPlay may not load properly if Branch Web SDK is
+ * blocked. This shim stubs out its script so the page still loads.
+ */
+
+if (!window?.branch?.b) {
+ const queue = window?.branch?._q || [];
+ window.branch = new (class {
+ V = {};
+ g = 0;
+ X = "web2.62.0";
+ b = {
+ A: {},
+ clear() {},
+ get() {},
+ getAll() {},
+ isEnabled: () => true,
+ remove() {},
+ set() {},
+ ca() {},
+ g: [],
+ l: 0,
+ o: 0,
+ s: null,
+ };
+ addListener() {}
+ applyCode() {}
+ autoAppIndex() {}
+ banner() {}
+ c() {}
+ closeBanner() {}
+ closeJourney() {}
+ constructor() {}
+ creditHistory() {}
+ credits() {}
+ crossPlatformIds() {}
+ data() {}
+ deepview() {}
+ deepviewCta() {}
+ disableTracking() {}
+ first() {}
+ getBrowserFingerprintId() {}
+ getCode() {}
+ init(key, ...args) {
+ const cb = args.pop();
+ if (typeof cb === "function") {
+ cb(undefined, {});
+ }
+ }
+ lastAttributedTouchData() {}
+ link() {}
+ logEvent() {}
+ logout() {}
+ qrCode() {}
+ redeem() {}
+ referrals() {}
+ removeListener() {}
+ renderFinalize() {}
+ renderQueue() {}
+ sendSMS() {}
+ setAPIResponseCallback() {}
+ setBranchViewData() {}
+ setIdentity() {}
+ track() {}
+ trackCommerceEvent() {}
+ validateCode() {}
+ })();
+ const push = ([fn, ...args]) => {
+ try {
+ window.branch[fn].apply(window.branch, args);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+ queue.forEach(push);
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/chartbeat.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/chartbeat.js
new file mode 100644
index 0000000000..0e57fc6da1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/chartbeat.js
@@ -0,0 +1,18 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713699 - Shim ChartBeat tracking
+ *
+ * Sites may rely on chartbeat's tracking as they might with Google Analytics,
+ * expecting it to be present for interactive site content to function. This
+ * shim mitigates related breakage.
+ */
+
+window.pSUPERFLY = {
+ activity() {},
+ virtualPage() {},
+};
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/crave-ca.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/crave-ca.js
new file mode 100644
index 0000000000..b4d93ccdfa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/crave-ca.js
@@ -0,0 +1,56 @@
+/* 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/. */
+
+"use strict";
+
+/*
+ * Bug 1746439 - crave.ca login broken with dFPI enabled
+ *
+ * Crave.ca relies upon a login page that is out-of-origin. That login page
+ * sets a cookie for https://www.crave.ca, which is then used as an proof of
+ * authentication on redirect back to the main site. This shim adds a request
+ * for storage access for https://www.crave.ca when the user tries to log in.
+ */
+
+console.warn(
+ `When logging in, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1746439 for details.`
+);
+
+// Third-party origin we need to request storage access for.
+const STORAGE_ACCESS_ORIGIN = "https://www.crave.ca";
+
+document.documentElement.addEventListener(
+ "click",
+ e => {
+ const { target, isTrusted } = e;
+ if (!isTrusted) {
+ return;
+ }
+ const button = target.closest("button");
+ if (!button) {
+ return;
+ }
+ const form = target.closest(".login-form");
+ if (!form) {
+ return;
+ }
+
+ console.warn(
+ "Calling the Storage Access API on behalf of " + STORAGE_ACCESS_ORIGIN
+ );
+ button.disabled = true;
+ e.stopPropagation();
+ e.preventDefault();
+ document
+ .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN)
+ .then(() => {
+ button.disabled = false;
+ target.click();
+ })
+ .catch(() => {
+ button.disabled = false;
+ });
+ },
+ true
+);
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/criteo.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/criteo.js
new file mode 100644
index 0000000000..afdc00b888
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/criteo.js
@@ -0,0 +1,64 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713720 - Shim Criteo
+ *
+ * Sites relying on window.Criteo to be loaded can experience
+ * breakage if it is blocked. Stubbing out the API in a shim can
+ * mitigate this breakage.
+ */
+
+if (window.Criteo?.CallRTA === undefined) {
+ window.Criteo = {
+ CallRTA() {},
+ ComputeStandaloneDFPTargeting() {},
+ DisplayAcceptableAdIfAdblocked() {},
+ DisplayAd() {},
+ GetBids() {},
+ GetBidsForAdUnit() {},
+ Passback: {
+ RequestBids() {},
+ RenderAd() {},
+ },
+ PubTag: {
+ Adapters: {
+ AMP() {},
+ Prebid() {},
+ },
+ Context: {
+ GetIdfs() {},
+ SetIdfs() {},
+ },
+ DirectBidding: {
+ DirectBiddingEvent() {},
+ DirectBiddingSlot() {},
+ DirectBiddingUrlBuilder() {},
+ Size() {},
+ },
+ RTA: {
+ DefaultCrtgContentName: "crtg_content",
+ DefaultCrtgRtaCookieName: "crtg_rta",
+ },
+ },
+ RenderAd() {},
+ RequestBids() {},
+ RequestBidsOnGoogleTagSlots() {},
+ SetCCPAExplicitOptOut() {},
+ SetCeh() {},
+ SetDFPKeyValueTargeting() {},
+ SetLineItemRanges() {},
+ SetPublisherExt() {},
+ SetSlotsExt() {},
+ SetTargeting() {},
+ SetUserExt() {},
+ events: {
+ push() {},
+ },
+ passbackEvents: [],
+ usePrebidEvents: true,
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/cxense.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/cxense.js
new file mode 100644
index 0000000000..55862f4fb5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/cxense.js
@@ -0,0 +1,593 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713721 - Shim Cxense
+ *
+ * Sites relying on window.cX can experience breakage if it is blocked.
+ * Stubbing out the API in a shim can mitigate this breakage. There are
+ * two versions of the API, one including window.cX.CCE, but both appear
+ * to be very similar so we use one shim for both.
+ */
+
+if (window.cX?.getUserSegmentIds === undefined) {
+ const callQueue = window.cX?.callQueue || [];
+ const callQueueCCE = window.cX?.CCE?.callQueue || [];
+
+ function getRandomString(l = 16) {
+ const v = crypto.getRandomValues(new Uint8Array(l));
+ const s = Array.from(v, c => c.toString(16)).join("");
+ return s.slice(0, l);
+ }
+
+ const call = (cb, ...args) => {
+ if (typeof cb !== "function") {
+ return;
+ }
+ try {
+ cb(...args);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ const invokeOn = lib => {
+ return (fn, ...args) => {
+ try {
+ lib[fn](...args);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+ };
+
+ const userId = getRandomString();
+ const cxUserId = `cx:${getRandomString(25)}:${getRandomString(12)}`;
+ const topLeft = { left: 0, top: 0 };
+ const margins = { left: 0, top: 0, right: 0, bottom: 0 };
+ const ccePushUrl =
+ "https://comcluster.cxense.com/cce/push?callback={{callback}}";
+ const displayWidget = (divId, a, ctx, callback) => call(callback, ctx, divId);
+ const getUserSegmentIds = a => call(a?.callback, a?.defaultValue || []);
+ const init = (a, b, c, d, callback) => call(callback);
+ const render = (a, data, ctx, callback) => call(callback, data, ctx);
+ const run = (params, ctx, callback) => call(callback, params, ctx);
+ const runCtrlVersion = (a, b, callback) => call(callback);
+ const runCxVersion = (a, data, b, ctx, callback) => call(callback, data, ctx);
+ const runTest = (a, divId, b, c, ctx, callback) => call(callback, divId, ctx);
+ const sendConversionEvent = (a, options) => call(options?.callback, {});
+ const sendEvent = (a, b, args) => call(args?.callback, {});
+
+ const getDivId = className => {
+ const e = document.querySelector(`.${className}`);
+ if (e) {
+ return `${className}-01`;
+ }
+ return null;
+ };
+
+ const getDocumentSize = () => {
+ const width = document.body.clientWidth;
+ const height = document.body.clientHeight;
+ return { width, height };
+ };
+
+ const getNowSeconds = () => {
+ return Math.round(new Date().getTime() / 1000);
+ };
+
+ const getPageContext = () => {
+ return {
+ location: location.href,
+ pageViewRandom: "",
+ userId,
+ };
+ };
+
+ const getWindowSize = () => {
+ const width = window.innerWidth;
+ const height = window.innerHeight;
+ return { width, height };
+ };
+
+ const isObject = i => {
+ return typeof i === "object" && i !== null && !Array.isArray(i);
+ };
+
+ const runMulti = widgets => {
+ widgets?.forEach(({ widgetParams, widgetContext, widgetCallback }) => {
+ call(widgetCallback, widgetParams, widgetContext);
+ });
+ };
+
+ let testGroup = -1;
+ let snapPoints = [];
+ const startTime = new Date();
+
+ const library = {
+ addCustomerScript() {},
+ addEventListener() {},
+ addExternalId() {},
+ afterInitializePage() {},
+ allUserConsents() {},
+ backends: {
+ production: {
+ baseAdDeliveryUrl: "http://adserver.cxad.cxense.com/adserver/search",
+ secureBaseAdDeliveryUrl:
+ "https://s-adserver.cxad.cxense.com/adserver/search",
+ },
+ sandbox: {
+ baseAdDeliveryUrl:
+ "http://adserver.sandbox.cxad.cxense.com/adserver/search",
+ secureBaseAdDeliveryUrl:
+ "https://s-adserver.sandbox.cxad.cxense.com/adserver/search",
+ },
+ },
+ calculateAdSpaceSize(adCount, adUnitSize, marginA, marginB) {
+ return adCount * (adUnitSize + marginA + marginB);
+ },
+ cdn: {
+ template: {
+ direct: {
+ http: "http://cdn.cxpublic.com/",
+ https: "https://cdn.cxpublic.com/",
+ },
+ mapped: {
+ http: "http://cdn-templates.cxpublic.com/",
+ https: "https://cdn-templates.cxpublic.com/",
+ },
+ },
+ },
+ cint() {},
+ cleanUpGlobalIds: [],
+ clearBaseUrl: "https://scdn.cxense.com/sclear.html",
+ clearCustomParameters() {},
+ clearIdUrl: "https://scomcluster.cxense.com/public/clearid",
+ clearIds() {},
+ clickTracker: (a, b, callback) => call(callback),
+ clientStorageUrl: "https://clientstorage.cxense.com",
+ combineArgs: () => Object.create(),
+ combineKeywordsIntoArray: () => [],
+ consentClasses: ["pv", "segment", "ad", "recs"],
+ consentClassesV2: ["geo", "device"],
+ cookieSyncRUrl: "csyn-r.cxense.com",
+ createDelegate() {},
+ csdUrls: {
+ domainScriptUrl: "//csd.cxpublic.com/d/",
+ customerScriptUrl: "//csd.cxpublic.com/t/",
+ },
+ cxenseGlobalIdIframeUrl: "https://scdn.cxense.com/sglobal.html",
+ cxenseUserIdUrl: "https://id.cxense.com/public/user/id",
+ decodeUrlEncodedNameValuePairs: () => Object.create(),
+ defaultAdRenderer: () => "",
+ deleteCookie() {},
+ denyWithoutConsent: {
+ addExternalId: "pv",
+ getUserSegmentIds: "segment",
+ insertAdSpace: "ad",
+ insertMultipleAdSpaces: "ad",
+ sendEvent: "pv",
+ sendPageViewEvent: "pv",
+ sync: "ad",
+ },
+ dmpPushUrl: "https://comcluster.cxense.com/dmp/push?callback={{callback}}",
+ emptyWidgetUrl: "https://scdn.cxense.com/empty.html",
+ eventReceiverBaseUrl: "https://scomcluster.cxense.com/Repo/rep.html",
+ eventReceiverBaseUrlGif: "https://scomcluster.cxense.com/Repo/rep.gif",
+ getAllText: () => "",
+ getClientStorageVariable() {},
+ getCookie: () => null,
+ getCxenseUserId: () => cxUserId,
+ getDocumentSize,
+ getElementPosition: () => topLeft,
+ getHashFragment: () => location.hash.substr(1),
+ getLocalStats: () => Object.create(),
+ getNodeValue: n => n.nodeValue,
+ getNowSeconds,
+ getPageContext,
+ getRandomString,
+ getScrollPos: () => topLeft,
+ getSessionId: () => "",
+ getSiteId: () => "",
+ getTimezoneOffset: () => new Date().getTimezoneOffset(),
+ getTopLevelDomain: () => location.hostname,
+ getUserId: () => userId,
+ getUserSegmentIds,
+ getWindowSize,
+ hasConsent: () => true,
+ hasHistory: () => true,
+ hasLocalStorage: () => true,
+ hasPassiveEventListeners: () => true,
+ hasPostMessage: () => true,
+ hasSessionStorage() {},
+ initializePage() {},
+ insertAdSpace() {},
+ insertMultipleAdSpaces() {},
+ insertWidget() {},
+ invoke: invokeOn(library),
+ isAmpIFrame() {},
+ isArray() {},
+ isCompatModeActive() {},
+ isConsentRequired() {},
+ isEdge: () => false,
+ isFirefox: () => true,
+ isIE6Or7: () => false,
+ isObject,
+ isRecsDestination: () => false,
+ isSafari: () => false,
+ isTextNode: n => n?.nodeType === 3,
+ isTopWindow: () => window === top,
+ jsonpRequest: () => false,
+ loadScript() {},
+ m_accountId: "0",
+ m_activityEvents: false,
+ m_activityState: {
+ activeTime: startTime,
+ currScrollLeft: 0,
+ currScrollTop: 0,
+ exitLink: "",
+ hadHIDActivity: false,
+ maxViewLeft: 1,
+ maxViewTop: 1,
+ parentMetrics: undefined,
+ prevActivityTime: startTime + 2,
+ prevScreenX: 0,
+ prevScreenY: 0,
+ prevScrollLeft: 0,
+ prevScrollTop: 0,
+ prevTime: startTime + 1,
+ prevWindowHeight: 1,
+ prevWindowWidth: 1,
+ scrollDepthPercentage: 0,
+ scrollDepthPixels: 0,
+ },
+ m_atfr: null,
+ m_c1xTpWait: 0,
+ m_clientStorage: {
+ iframeEl: null,
+ iframeIsLoaded: false,
+ iframeOrigin: "https://clientstorage.cxense.com",
+ iframePath: "/clientstorage_v2.html",
+ messageContexts: {},
+ messageQueue: [],
+ },
+ m_compatMode: {},
+ m_compatModeActive: false,
+ m_compatPvSent: false,
+ m_consentVersion: 1,
+ m_customParameters: [],
+ m_documentSizeRequestedFromChild: false,
+ m_externalUserIds: [],
+ m_globalIdLoading: {
+ globalIdIFrameEl: null,
+ globalIdIFrameElLoaded: false,
+ },
+ m_isSpaRecsDestination: false,
+ m_knownMessageSources: [],
+ m_p1Complete: false,
+ m_prevLocationHash: "",
+ m_previousPageViewReport: null,
+ m_rawCustomParameters: {},
+ m_rnd: getRandomString(),
+ m_scriptStartTime: startTime,
+ m_siteId: "0",
+ m_spaRecsClickUrl: null,
+ m_thirdPartyIds: true,
+ m_usesConsent: false,
+ m_usesIabConsent: false,
+ m_usesSecureCookies: true,
+ m_usesTcf20Consent: false,
+ m_widgetSpecs: {},
+ Object,
+ onClearIds() {},
+ onFFP1() {},
+ onP1() {},
+ p1BaseUrl: "https://scdn.cxense.com/sp1.html",
+ p1JsUrl: "https://p1cluster.cxense.com/p1.js",
+ parseHashArgs: () => Object.create(),
+ parseMargins: () => margins,
+ parseUrlArgs: () => Object.create(),
+ postMessageToParent() {},
+ publicWidgetDataUrl: "https://api.cxense.com/public/widget/data",
+ removeClientStorageVariable() {},
+ removeEventListener() {},
+ renderContainedImage: () => "<div/>",
+ renderTemplate: () => "<div/>",
+ reportActivity() {},
+ requireActivityEvents() {},
+ requireConsent() {},
+ requireOnlyFirstPartyIds() {},
+ requireSecureCookies() {},
+ requireTcf20() {},
+ sendEvent,
+ sendSpaRecsClick: (a, callback) => call(callback),
+ setAccountId() {},
+ setAllConsentsTo() {},
+ setClientStorageVariable() {},
+ setCompatMode() {},
+ setConsent() {},
+ setCookie() {},
+ setCustomParameters() {},
+ setEventAttributes() {},
+ setGeoPosition() {},
+ setNodeValue() {},
+ setRandomId() {},
+ setRestrictionsToConsentClasses() {},
+ setRetargetingParameters() {},
+ setSiteId() {},
+ setUserProfileParameters() {},
+ setupIabCmp() {},
+ setupTcfApi() {},
+ shouldPollActivity() {},
+ startLocalStats() {},
+ startSessionAnnotation() {},
+ stopAllSessionAnnotations() {},
+ stopSessionAnnotation() {},
+ sync() {},
+ trackAmpIFrame() {},
+ trackElement() {},
+ trim: s => s.trim(),
+ tsridUrl: "https://tsrid.cxense.com/lookup?callback={{callback}}",
+ userSegmentUrl:
+ "https://api.cxense.com/profile/user/segment?callback={{callback}}",
+ };
+
+ const libraryCCE = {
+ "__cx-toolkit__": {
+ isShown: true,
+ data: [],
+ },
+ activeSnapPoint: null,
+ activeWidgets: [],
+ ccePushUrl,
+ clickTracker: () => "",
+ displayResult() {},
+ displayWidget,
+ getDivId,
+ getTestGroup: () => testGroup,
+ init,
+ insertMaster() {},
+ instrumentClickLinks() {},
+ invoke: invokeOn(libraryCCE),
+ noCache: false,
+ offerProductId: null,
+ persistedQueryId: null,
+ prefix: null,
+ previewCampaign: null,
+ previewDiv: null,
+ previewId: null,
+ previewTestId: null,
+ processCxResult() {},
+ render,
+ reportTestImpression() {},
+ run,
+ runCtrlVersion,
+ runCxVersion,
+ runMulti,
+ runTest,
+ sendConversionEvent,
+ sendPageViewEvent: (a, b, c, callback) => call(callback),
+ setSnapPoints(x) {
+ snapPoints = x;
+ },
+ setTestGroup(x) {
+ testGroup = x;
+ },
+ setVisibilityField() {},
+ get snapPoints() {
+ return snapPoints;
+ },
+ startTime,
+ get testGroup() {
+ return testGroup;
+ },
+ testVariant: null,
+ trackTime: 0.5,
+ trackVisibility() {},
+ updateRecsClickUrls() {},
+ utmParams: [],
+ version: "2.42",
+ visibilityField: "timeHalf",
+ };
+
+ const CCE = {
+ activeSnapPoint: null,
+ activeWidgets: [],
+ callQueue: callQueueCCE,
+ ccePushUrl,
+ clickTracker: () => "",
+ displayResult() {},
+ displayWidget,
+ getDivId,
+ getTestGroup: () => testGroup,
+ init,
+ insertMaster() {},
+ instrumentClickLinks() {},
+ invoke: invokeOn(libraryCCE),
+ library: libraryCCE,
+ noCache: false,
+ offerProductId: null,
+ persistedQueryId: null,
+ prefix: null,
+ previewCampaign: null,
+ previewDiv: null,
+ previewId: null,
+ previewTestId: null,
+ processCxResult() {},
+ render,
+ reportTestImpression() {},
+ run,
+ runCtrlVersion,
+ runCxVersion,
+ runMulti,
+ runTest,
+ sendConversionEvent,
+ sendPageViewEvent: (a, b, c, callback) => call(callback),
+ setSnapPoints(x) {
+ snapPoints = x;
+ },
+ setTestGroup(x) {
+ testGroup = x;
+ },
+ setVisibilityField() {},
+ get snapPoints() {
+ return snapPoints;
+ },
+ startTime,
+ get testGroup() {
+ return testGroup;
+ },
+ testVariant: null,
+ trackTime: 0.5,
+ trackVisibility() {},
+ updateRecsClickUrls() {},
+ utmParams: [],
+ version: "2.42",
+ visibilityField: "timeHalf",
+ };
+
+ window.cX = {
+ addCustomerScript() {},
+ addEventListener() {},
+ addExternalId() {},
+ afterInitializePage() {},
+ allUserConsents: () => undefined,
+ Array,
+ calculateAdSpaceSize: () => 0,
+ callQueue,
+ CCE,
+ cint: () => undefined,
+ clearCustomParameters() {},
+ clearIds() {},
+ clickTracker: () => "",
+ combineArgs: () => Object.create(),
+ combineKeywordsIntoArray: () => [],
+ createDelegate() {},
+ decodeUrlEncodedNameValuePairs: () => Object.create(),
+ defaultAdRenderer: () => "",
+ deleteCookie() {},
+ getAllText: () => "",
+ getClientStorageVariable() {},
+ getCookie: () => null,
+ getCxenseUserId: () => cxUserId,
+ getDocumentSize,
+ getElementPosition: () => topLeft,
+ getHashFragment: () => location.hash.substr(1),
+ getLocalStats: () => Object.create(),
+ getNodeValue: n => n.nodeValue,
+ getNowSeconds,
+ getPageContext,
+ getRandomString,
+ getScrollPos: () => topLeft,
+ getSessionId: () => "",
+ getSiteId: () => "",
+ getTimezoneOffset: () => new Date().getTimezoneOffset(),
+ getTopLevelDomain: () => location.hostname,
+ getUserId: () => userId,
+ getUserSegmentIds,
+ getWindowSize,
+ hasConsent: () => true,
+ hasHistory: () => true,
+ hasLocalStorage: () => true,
+ hasPassiveEventListeners: () => true,
+ hasPostMessage: () => true,
+ hasSessionStorage() {},
+ initializePage() {},
+ insertAdSpace() {},
+ insertMultipleAdSpaces() {},
+ insertWidget() {},
+ invoke: invokeOn(library),
+ isAmpIFrame() {},
+ isArray() {},
+ isCompatModeActive() {},
+ isConsentRequired() {},
+ isEdge: () => false,
+ isFirefox: () => true,
+ isIE6Or7: () => false,
+ isObject,
+ isRecsDestination: () => false,
+ isSafari: () => false,
+ isTextNode: n => n?.nodeType === 3,
+ isTopWindow: () => window === top,
+ JSON,
+ jsonpRequest: () => false,
+ library,
+ loadScript() {},
+ Object,
+ onClearIds() {},
+ onFFP1() {},
+ onP1() {},
+ parseHashArgs: () => Object.create(),
+ parseMargins: () => margins,
+ parseUrlArgs: () => Object.create(),
+ postMessageToParent() {},
+ removeClientStorageVariable() {},
+ removeEventListener() {},
+ renderContainedImage: () => "<div/>",
+ renderTemplate: () => "<div/>",
+ reportActivity() {},
+ requireActivityEvents() {},
+ requireConsent() {},
+ requireOnlyFirstPartyIds() {},
+ requireSecureCookies() {},
+ requireTcf20() {},
+ sendEvent,
+ sendPageViewEvent: (a, callback) => call(callback, {}),
+ sendSpaRecsClick() {},
+ setAccountId() {},
+ setAllConsentsTo() {},
+ setClientStorageVariable() {},
+ setCompatMode() {},
+ setConsent() {},
+ setCookie() {},
+ setCustomParameters() {},
+ setEventAttributes() {},
+ setGeoPosition() {},
+ setNodeValue() {},
+ setRandomId() {},
+ setRestrictionsToConsentClasses() {},
+ setRetargetingParameters() {},
+ setSiteId() {},
+ setUserProfileParameters() {},
+ setupIabCmp() {},
+ setupTcfApi() {},
+ shouldPollActivity() {},
+ startLocalStats() {},
+ startSessionAnnotation() {},
+ stopAllSessionAnnotations() {},
+ stopSessionAnnotation() {},
+ sync() {},
+ trackAmpIFrame() {},
+ trackElement() {},
+ trim: s => s.trim(),
+ };
+
+ window.cxTest = window.cX;
+
+ window.cx_pollActiveTime = () => undefined;
+ window.cx_pollActivity = () => undefined;
+ window.cx_pollFragmentMessage = () => undefined;
+
+ const execQueue = (lib, queue) => {
+ return () => {
+ const invoke = invokeOn(lib);
+ setTimeout(() => {
+ queue.push = cmd => {
+ setTimeout(() => invoke(...cmd), 1);
+ };
+ for (const cmd of queue) {
+ invoke(...cmd);
+ }
+ }, 25);
+ };
+ };
+
+ window.cx_callQueueExecute = execQueue(library, callQueue);
+ window.cxCCE_callQueueExecute = execQueue(libraryCCE, callQueueCCE);
+
+ window.cx_callQueueExecute();
+ window.cxCCE_callQueueExecute();
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/doubleverify.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/doubleverify.js
new file mode 100644
index 0000000000..7eaf945d77
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/doubleverify.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1771557 - Shim DoubleVerify analytics
+ *
+ * Some sites such as Sports Illustrated expect DoubleVerify's
+ * analytics script to load, otherwise odd breakage may occur.
+ * This shim helps mitigate such breakage.
+ */
+
+if (!window?.PQ?.loaded) {
+ const cmd = [];
+ cmd.push = function (c) {
+ try {
+ c?.();
+ } catch (_) {}
+ };
+
+ window.apntag = {
+ anq: [],
+ };
+
+ window.PQ = {
+ cmd,
+ loaded: true,
+ getTargeting: (_, cb) => cb?.([]),
+ init: () => {},
+ loadSignals: (_, cb) => cb?.(),
+ loadSignalsForSlots: (_, cb) => cb?.(),
+ PTS: {},
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/eluminate.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/eluminate.js
new file mode 100644
index 0000000000..3fa65c048c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/eluminate.js
@@ -0,0 +1,95 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1606448 - Shim CoreMetrics Eluminate analytics
+ *
+ * Sites may rely on eluminate.js tracking in ways which cause breakage,
+ * which has been seen on shopping sites such as Vans.com, where the
+ * search filtering UX is broken. This shim mitigates such breakage.
+ */
+
+if (!window.CM_DDX) {
+ window.CM_DDX = {
+ domReadyFired: false,
+ headScripts: true,
+ dispatcherLoadRequested: false,
+ firstPassFunctionBinding: false,
+ BAD_PAGE_ID_ELAPSED_TIMEOUT: 5000,
+ version: -1,
+ standalone: false,
+ test: {
+ syndicate: true,
+ testCounter: "",
+ doTest: false,
+ newWin: false,
+ process: () => {},
+ },
+ partner: {},
+ invokeFunctionWhenAvailable: a => {
+ a();
+ },
+ gup: d => "",
+ privacy: {
+ isDoNotTrackEnabled: () => false,
+ setDoNotTrack: () => {},
+ getDoNotTrack: () => false,
+ },
+ setSubCookie: () => {},
+ };
+ const noopfn = () => {};
+ const w = window;
+ w.cmAddShared = noopfn;
+ w.cmCalcSKUString = noopfn;
+ w.cmCreateManualImpressionTag = noopfn;
+ w.cmCreateManualLinkClickTag = noopfn;
+ w.cmCreateManualPageviewTag = noopfn;
+ w.cmCreateOrderTag = noopfn;
+ w.cmCreatePageviewTag = noopfn;
+ w.cmExecuteTagQueue = noopfn;
+ w.cmRetrieveUserID = noopfn;
+ w.cmSetClientID = noopfn;
+ w.cmSetCurrencyCode = noopfn;
+ w.cmSetFirstPartyIDs = noopfn;
+ w.cmSetSubCookie = noopfn;
+ w.cmSetupCookieMigration = noopfn;
+ w.cmSetupNormalization = noopfn;
+ w.cmSetupOther = noopfn;
+ w.cmStartTagSet = noopfn;
+ w.cmCreateConversionEventTag = noopfn;
+ w.cmCreateDefaultPageviewTag = noopfn;
+ w.cmCreateElementTag = noopfn;
+ w.cmCreateManualImpressionTag = noopfn;
+ w.cmCreateManualLinkClickTag = noopfn;
+ w.cmCreateManualPageviewTag = noopfn;
+ w.cmCreatePageElementTag = noopfn;
+ w.cmCreatePageviewTag = noopfn;
+ w.cmCreateProductElementTag = noopfn;
+ w.cmCreateProductviewTag = noopfn;
+ w.cmCreateTechPropsTag = noopfn;
+ w.cmLoadIOConfig = noopfn;
+ w.cmSetClientID = noopfn;
+ w.cmSetCurrencyCode = noopfn;
+ w.cmSetFirstPartyIDs = noopfn;
+ w.cmSetupCookieMigration = noopfn;
+ w.cmSetupNormalization = noopfn;
+
+ w.cmSetupOther = b => {
+ for (const a in b) {
+ window[a] = b[a];
+ }
+ };
+
+ const techProps = {};
+
+ w.coremetrics = {
+ cmLastReferencedPageID: "",
+ cmLoad: noopfn,
+ cmUpdateConfig: noopfn,
+ getTechProps: () => techProps,
+ isDef: c => typeof c !== "undefined" && c,
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/empty-script.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/empty-script.js
new file mode 100644
index 0000000000..d01f2ab537
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/empty-script.js
@@ -0,0 +1,5 @@
+/* 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/. */
+
+/* This script is intentionally empty */
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/empty-shim.txt b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/empty-shim.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/empty-shim.txt
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/everest.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/everest.js
new file mode 100644
index 0000000000..259ab9033e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/everest.js
@@ -0,0 +1,171 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1728114 - Shim Adobe EverestJS
+ *
+ * Sites assuming EverestJS will load can break if it is blocked.
+ * This shim mitigates that breakage.
+ */
+
+if (!window.__ql) {
+ window.__ql = {};
+}
+
+if (!window.EF) {
+ const AdCloudLocalStorage = {
+ get: (_, cb) => cb(),
+ isInitDone: true,
+ isInitSuccess: true,
+ };
+
+ const emptyObj = {};
+
+ const nullSrc = {
+ getHosts: () => [undefined],
+ getProtocols: () => [undefined],
+ hash: {},
+ hashParamsOrder: [],
+ host: undefined,
+ path: [],
+ port: undefined,
+ query: {},
+ queryDelimiter: "&",
+ queryParamsOrder: [],
+ queryPrefix: "?",
+ queryWithoutEncode: {},
+ respectEmptyQueryParamValue: undefined,
+ scheme: undefined,
+ text: "//",
+ userInfo: undefined,
+ };
+
+ const pixelDetailsEvent = {
+ addToDom() {},
+ canAddToDom: () => false,
+ fire() {},
+ getDomElement() {},
+ initializeUri() {},
+ pixelDetailsReceiver() {},
+ scheme: "https:",
+ uri: nullSrc,
+ userid: 0,
+ };
+
+ window.EF = {
+ AdCloudLocalStorage,
+ accessTopUrl: 0,
+ acquireCookieMatchingSlot() {},
+ addListener() {},
+ addPixelDetailsReadyListener() {},
+ addToDom() {},
+ allow3rdPartyPixels: 1,
+ appData: "",
+ appendDictionary() {},
+ checkGlobalSid() {},
+ checkUrlParams() {},
+ cmHost: "cm.everesttech.net",
+ context: {
+ isFbApp: () => 0,
+ isPageview: () => false,
+ isSegmentation: () => false,
+ isTransaction: () => false,
+ },
+ conversionData: "",
+ cookieMatchingSlots: 1,
+ debug: 0,
+ deserializeUrlParams: () => emptyObj,
+ doCookieMatching() {},
+ ef_itp_ls: false,
+ eventType: "",
+ executeAfterLoad() {},
+ executeOnloadCallbacks() {},
+ expectedTrackingParams: ["ev_cl", "ev_sid"],
+ fbIsApp: 0,
+ fbsCM: 0,
+ fbsPixelId: 0,
+ filterList: () => [],
+ getArrayIndex: -1,
+ getConversionData: () => "",
+ getConversionDataFromLocalStorage: cb => cb(),
+ getDisplayClickUri: () => "",
+ getEpochFromEfUniq: () => 0,
+ getFirstLevelObjectCopy: () => emptyObj,
+ getInvisibleIframeElement() {},
+ getInvisibleImageElement() {},
+ getMacroSubstitutedText: () => "",
+ getPixelDetails: cb => cb({}),
+ getScriptElement() {},
+ getScriptSrc: () => "",
+ getServerParams: () => emptyObj,
+ getSortedAttributes: () => [],
+ getTrackingParams: () => emptyObj,
+ getTransactionParams: () => emptyObj,
+ handleConversionData() {},
+ impressionProperties: "",
+ impressionTypes: ["impression", "impression_served"],
+ inFloodlight: 0,
+ init(config) {
+ try {
+ const { userId } = config;
+ window.EF.userId = userId;
+ pixelDetailsEvent.userId = userId;
+ } catch (_) {}
+ },
+ initializeEFVariables() {},
+ isArray: a => Array.isArray(a),
+ isEmptyDictionary: () => true,
+ isITPEnabled: () => false,
+ isPermanentCookieSet: () => false,
+ isSearchClick: () => 0,
+ isXSSReady() {},
+ jsHost: "www.everestjs.net",
+ jsTagAdded: 0,
+ location: nullSrc,
+ locationHref: nullSrc,
+ locationSkipBang: nullSrc,
+ log() {},
+ main() {},
+ main2() {},
+ newCookieMatchingEvent: () => emptyObj,
+ newFbsCookieMatching: () => emptyObj,
+ newImpression: () => emptyObj,
+ newPageview: () => emptyObj,
+ newPixelDetails: () => emptyObj,
+ newPixelEvent: () => emptyObj,
+ newPixelServerDisplayClickRedirectUri: () => emptyObj,
+ newPixelServerGenericRedirectUri: () => emptyObj,
+ newPixelServerUri: () => emptyObj,
+ newProductSegment: () => emptyObj,
+ newSegmentJavascript: () => emptyObj,
+ newTransaction: () => emptyObj,
+ newUri: () => emptyObj,
+ onloadCallbacks: [],
+ pageViewProperties: "",
+ pageviewProperties: "",
+ pixelDetails: {},
+ pixelDetailsAdded: 1,
+ pixelDetailsEvent,
+ pixelDetailsParams: [],
+ pixelDetailsReadyCallbackFns: [],
+ pixelDetailsRecieverCalled: 1,
+ pixelHost: "pixel.everesttech.net",
+ protocol: document?.location?.protocol || "",
+ referrer: nullSrc,
+ removeListener() {},
+ searchSegment: "",
+ segment: "",
+ serverParamsListener() {},
+ sid: 0,
+ sku: "",
+ throttleCookie: "",
+ trackingJavascriptSrc: nullSrc,
+ transactionObjectList: [],
+ transactionProperties: "",
+ userServerParams: {},
+ userid: 0,
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/facebook-sdk.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/facebook-sdk.js
new file mode 100644
index 0000000000..1e995ff047
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/facebook-sdk.js
@@ -0,0 +1,554 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1226498 - Shim Facebook SDK
+ *
+ * This shim provides functionality to enable Facebook's authenticator on third
+ * party sites ("continue/log in with Facebook" buttons). This includes rendering
+ * the button as the SDK would, if sites require it. This way, if users wish to
+ * opt into the Facebook login process regardless of the tracking consequences,
+ * they only need to click the button as usual.
+ *
+ * In addition, the shim also attempts to provide placeholders for Facebook
+ * videos, which users may click to opt into seeing the video (also despite
+ * the increased tracking risks). This is an experimental feature enabled
+ * that is only currently enabled on nightly builds.
+ *
+ * Finally, this shim also stubs out as much of the SDK as possible to prevent
+ * breaking on sites which expect that it will always successfully load.
+ */
+
+if (!window.FB) {
+ const FacebookLogoURL = "https://smartblock.firefox.etp/facebook.svg";
+ const PlayIconURL = "https://smartblock.firefox.etp/play.svg";
+
+ const originalUrl = document.currentScript.src;
+
+ let haveUnshimmed;
+ let initInfo;
+ let activeOnloginAttribute;
+ const placeholdersToRemoveOnUnshim = new Set();
+ const loggedGraphApiCalls = [];
+ const eventHandlers = new Map();
+
+ function getGUID() {
+ const v = crypto.getRandomValues(new Uint8Array(20));
+ return Array.from(v, c => c.toString(16)).join("");
+ }
+
+ const sendMessageToAddon = (function () {
+ const shimId = "FacebookSDK";
+ const pendingMessages = new Map();
+ const channel = new MessageChannel();
+ channel.port1.onerror = console.error;
+ channel.port1.onmessage = event => {
+ const { messageId, response } = event.data;
+ const resolve = pendingMessages.get(messageId);
+ if (resolve) {
+ pendingMessages.delete(messageId);
+ resolve(response);
+ }
+ };
+ function reconnect() {
+ const detail = {
+ pendingMessages: [...pendingMessages.values()],
+ port: channel.port2,
+ shimId,
+ };
+ window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+ }
+ window.addEventListener("ShimHelperReady", reconnect);
+ reconnect();
+ return function (message) {
+ const messageId = getGUID();
+ return new Promise(resolve => {
+ const payload = { message, messageId, shimId };
+ pendingMessages.set(messageId, resolve);
+ channel.port1.postMessage(payload);
+ });
+ };
+ })();
+
+ const isNightly = sendMessageToAddon("getOptions").then(opts => {
+ return opts.releaseBranch === "nightly";
+ });
+
+ function makeLoginPlaceholder(target) {
+ // Sites may provide their own login buttons, or rely on the Facebook SDK
+ // to render one for them. For the latter case, we provide placeholders
+ // which try to match the examples and documentation here:
+ // https://developers.facebook.com/docs/facebook-login/web/login-button/
+
+ if (target.textContent || target.hasAttribute("fb-xfbml-state")) {
+ return;
+ }
+ target.setAttribute("fb-xfbml-state", "");
+
+ const size = target.getAttribute("data-size") || "large";
+
+ let font, margin, minWidth, maxWidth, height, iconHeight;
+ if (size === "small") {
+ font = 11;
+ margin = 8;
+ minWidth = maxWidth = 200;
+ height = 20;
+ iconHeight = 12;
+ } else if (size === "medium") {
+ font = 13;
+ margin = 8;
+ minWidth = 200;
+ maxWidth = 320;
+ height = 28;
+ iconHeight = 16;
+ } else {
+ font = 16;
+ minWidth = 240;
+ maxWidth = 400;
+ margin = 12;
+ height = 40;
+ iconHeight = 24;
+ }
+
+ const wattr = target.getAttribute("data-width") || "";
+ const width =
+ wattr === "100%" ? wattr : `${parseFloat(wattr) || minWidth}px`;
+
+ const round = target.getAttribute("data-layout") === "rounded" ? 20 : 4;
+
+ const text =
+ target.getAttribute("data-button-type") === "continue_with"
+ ? "Continue with Facebook"
+ : "Log in with Facebook";
+
+ const button = document.createElement("div");
+ button.style = `
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding-left: ${margin + iconHeight}px;
+ ${width};
+ min-width: ${minWidth}px;
+ max-width: ${maxWidth}px;
+ height: ${height}px;
+ border-radius: ${round}px;
+ -moz-text-size-adjust: none;
+ -moz-user-select: none;
+ color: #fff;
+ font-size: ${font}px;
+ font-weight: bold;
+ font-family: Helvetica, Arial, sans-serif;
+ letter-spacing: .25px;
+ background-color: #1877f2;
+ background-repeat: no-repeat;
+ background-position: ${margin}px 50%;
+ background-size: ${iconHeight}px ${iconHeight}px;
+ background-image: url(${FacebookLogoURL});
+ `;
+ button.textContent = text;
+ target.appendChild(button);
+ target.addEventListener("click", () => {
+ activeOnloginAttribute = target.getAttribute("onlogin");
+ });
+ }
+
+ async function makeVideoPlaceholder(target) {
+ // For videos, we provide a more generic placeholder of roughly the
+ // expected size with a play button, as well as a Facebook logo.
+ if (!(await isNightly) || target.hasAttribute("fb-xfbml-state")) {
+ return;
+ }
+ target.setAttribute("fb-xfbml-state", "");
+
+ let width = parseInt(target.getAttribute("data-width"));
+ let height = parseInt(target.getAttribute("data-height"));
+ if (height) {
+ height = `${width * 0.6}px`;
+ } else {
+ height = `100%; min-height:${width * 0.75}px`;
+ }
+ if (width) {
+ width = `${width}px`;
+ } else {
+ width = `100%; min-width:200px`;
+ }
+
+ const placeholder = document.createElement("div");
+ placeholdersToRemoveOnUnshim.add(placeholder);
+ placeholder.style = `
+ width: ${width};
+ height: ${height};
+ top: 0px;
+ left: 0px;
+ background: #000;
+ color: #fff;
+ text-align: center;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-image: url(${FacebookLogoURL}), url(${PlayIconURL});
+ background-position: calc(100% - 24px) 24px, 50% 47.5%;
+ background-repeat: no-repeat, no-repeat;
+ background-size: 43px 42px, 25% 25%;
+ -moz-text-size-adjust: none;
+ -moz-user-select: none;
+ color: #fff;
+ align-items: center;
+ padding-top: 200px;
+ font-size: 14pt;
+ `;
+ placeholder.textContent = "Click to allow blocked Facebook content";
+ placeholder.addEventListener("click", evt => {
+ if (!evt.isTrusted) {
+ return;
+ }
+ allowFacebookSDK(() => {
+ placeholdersToRemoveOnUnshim.forEach(p => p.remove());
+ });
+ });
+
+ target.innerHTML = "";
+ target.appendChild(placeholder);
+ }
+
+ // We monitor for XFBML objects as Facebook SDK does, so we
+ // can provide placeholders for dynamically-added ones.
+ const xfbmlObserver = new MutationObserver(mutations => {
+ for (let { addedNodes, target, type } of mutations) {
+ const nodes = type === "attributes" ? [target] : addedNodes;
+ for (const node of nodes) {
+ if (node?.classList?.contains("fb-login-button")) {
+ makeLoginPlaceholder(node);
+ }
+ if (node?.classList?.contains("fb-video")) {
+ makeVideoPlaceholder(node);
+ }
+ }
+ }
+ });
+
+ xfbmlObserver.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ["class"],
+ });
+
+ const needPopup =
+ !/app_runner/.test(window.name) && !/iframe_canvas/.test(window.name);
+ const popupName = getGUID();
+ let activePopup;
+
+ if (needPopup) {
+ const oldWindowOpen = window.open;
+ window.open = function (href, name, params) {
+ try {
+ const url = new URL(href, window.location.href);
+ if (
+ url.protocol === "https:" &&
+ (url.hostname === "m.facebook.com" ||
+ url.hostname === "www.facebook.com") &&
+ url.pathname.endsWith("/oauth")
+ ) {
+ name = popupName;
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ return oldWindowOpen.call(window, href, name, params);
+ };
+ }
+
+ let allowingFacebookPromise;
+
+ async function allowFacebookSDK(postInitCallback) {
+ if (allowingFacebookPromise) {
+ return allowingFacebookPromise;
+ }
+
+ let resolve, reject;
+ allowingFacebookPromise = new Promise((_resolve, _reject) => {
+ resolve = _resolve;
+ reject = _reject;
+ });
+
+ await sendMessageToAddon("optIn");
+
+ xfbmlObserver.disconnect();
+
+ const shim = window.FB;
+ window.FB = undefined;
+
+ // We need to pass the site's initialization info to the real
+ // SDK as it loads, so we use the fbAsyncInit mechanism to
+ // do so, also ensuring our own post-init callbacks are called.
+ const oldInit = window.fbAsyncInit;
+ window.fbAsyncInit = () => {
+ try {
+ if (typeof initInfo !== "undefined") {
+ window.FB.init(initInfo);
+ } else if (oldInit) {
+ oldInit();
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ // Also re-subscribe any SDK event listeners as early as possible.
+ for (const [name, fns] of eventHandlers.entries()) {
+ for (const fn of fns) {
+ window.FB.Event.subscribe(name, fn);
+ }
+ }
+
+ // Allow the shim to do any post-init work early as well, while the
+ // SDK script finishes loading and we ask it to re-parse XFBML etc.
+ postInitCallback?.();
+ };
+
+ const script = document.createElement("script");
+ script.src = originalUrl;
+
+ script.addEventListener("error", () => {
+ allowingFacebookPromise = null;
+ script.remove();
+ activePopup?.close();
+ window.FB = shim;
+ reject();
+ alert("Failed to load Facebook SDK; please try again");
+ });
+
+ script.addEventListener("load", () => {
+ haveUnshimmed = true;
+
+ // After the real SDK has fully loaded we re-issue any Graph API
+ // calls the page is waiting on, as well as requesting for it to
+ // re-parse any XBFML elements (including ones with placeholders).
+
+ for (const args of loggedGraphApiCalls) {
+ try {
+ window.FB.api.apply(window.FB, args);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ window.FB.XFBML.parse(document.body, resolve);
+ });
+
+ document.head.appendChild(script);
+
+ return allowingFacebookPromise;
+ }
+
+ function buildPopupParams() {
+ // We try to match Facebook's popup size reasonably closely.
+ const { outerWidth, outerHeight, screenX, screenY } = window;
+ const { width, height } = window.screen;
+ const w = Math.min(width, 400);
+ const h = Math.min(height, 400);
+ const ua = navigator.userAgent;
+ const isMobile = ua.includes("Mobile") || ua.includes("Tablet");
+ const left = screenX + (screenX < 0 ? width : 0) + (outerWidth - w) / 2;
+ const top = screenY + (screenY < 0 ? height : 0) + (outerHeight - h) / 2.5;
+ let params = `left=${left},top=${top},width=${w},height=${h},scrollbars=1,toolbar=0,location=1`;
+ if (!isMobile) {
+ params = `${params},width=${w},height=${h}`;
+ }
+ return params;
+ }
+
+ // If a page stores the window.FB reference of the shim, then we
+ // want to have it proxy calls to the real SDK once we've unshimmed.
+ function ensureProxiedToUnshimmed(obj) {
+ const shim = {};
+ for (const key in obj) {
+ const value = obj[key];
+ if (typeof value === "function") {
+ shim[key] = function () {
+ if (haveUnshimmed) {
+ return window.FB[key].apply(window.FB, arguments);
+ }
+ return value.apply(this, arguments);
+ };
+ } else if (typeof value !== "object" || value === null) {
+ shim[key] = value;
+ } else {
+ shim[key] = ensureProxiedToUnshimmed(value);
+ }
+ }
+ return new Proxy(shim, {
+ get: (shimmed, key) => (haveUnshimmed ? window.FB : shimmed)[key],
+ });
+ }
+
+ window.FB = ensureProxiedToUnshimmed({
+ api() {
+ loggedGraphApiCalls.push(arguments);
+ },
+ AppEvents: {
+ activateApp() {},
+ clearAppVersion() {},
+ clearUserID() {},
+ EventNames: {
+ ACHIEVED_LEVEL: "fb_mobile_level_achieved",
+ ADDED_PAYMENT_INFO: "fb_mobile_add_payment_info",
+ ADDED_TO_CART: "fb_mobile_add_to_cart",
+ ADDED_TO_WISHLIST: "fb_mobile_add_to_wishlist",
+ COMPLETED_REGISTRATION: "fb_mobile_complete_registration",
+ COMPLETED_TUTORIAL: "fb_mobile_tutorial_completion",
+ INITIATED_CHECKOUT: "fb_mobile_initiated_checkout",
+ PAGE_VIEW: "fb_page_view",
+ RATED: "fb_mobile_rate",
+ SEARCHED: "fb_mobile_search",
+ SPENT_CREDITS: "fb_mobile_spent_credits",
+ UNLOCKED_ACHIEVEMENT: "fb_mobile_achievement_unlocked",
+ VIEWED_CONTENT: "fb_mobile_content_view",
+ },
+ getAppVersion: () => "",
+ getUserID: () => "",
+ logEvent() {},
+ logPageView() {},
+ logPurchase() {},
+ ParameterNames: {
+ APP_USER_ID: "_app_user_id",
+ APP_VERSION: "_appVersion",
+ CONTENT_ID: "fb_content_id",
+ CONTENT_TYPE: "fb_content_type",
+ CURRENCY: "fb_currency",
+ DESCRIPTION: "fb_description",
+ LEVEL: "fb_level",
+ MAX_RATING_VALUE: "fb_max_rating_value",
+ NUM_ITEMS: "fb_num_items",
+ PAYMENT_INFO_AVAILABLE: "fb_payment_info_available",
+ REGISTRATION_METHOD: "fb_registration_method",
+ SEARCH_STRING: "fb_search_string",
+ SUCCESS: "fb_success",
+ },
+ setAppVersion() {},
+ setUserID() {},
+ updateUserProperties() {},
+ },
+ Canvas: {
+ getHash: () => "",
+ getPageInfo(cb) {
+ cb?.call(this, {
+ clientHeight: 1,
+ clientWidth: 1,
+ offsetLeft: 0,
+ offsetTop: 0,
+ scrollLeft: 0,
+ scrollTop: 0,
+ });
+ },
+ Plugin: {
+ hidePluginElement() {},
+ showPluginElement() {},
+ },
+ Prefetcher: {
+ COLLECT_AUTOMATIC: 0,
+ COLLECT_MANUAL: 1,
+ addStaticResource() {},
+ setCollectionMode() {},
+ },
+ scrollTo() {},
+ setAutoGrow() {},
+ setDoneLoading() {},
+ setHash() {},
+ setSize() {},
+ setUrlHandler() {},
+ startTimer() {},
+ stopTimer() {},
+ },
+ Event: {
+ subscribe(e, f) {
+ if (!eventHandlers.has(e)) {
+ eventHandlers.set(e, new Set());
+ }
+ eventHandlers.get(e).add(f);
+ },
+ unsubscribe(e, f) {
+ eventHandlers.get(e)?.delete(f);
+ },
+ },
+ frictionless: {
+ init() {},
+ isAllowed: () => false,
+ },
+ gamingservices: {
+ friendFinder() {},
+ uploadImageToMediaLibrary() {},
+ },
+ getAccessToken: () => null,
+ getAuthResponse() {
+ return { status: "" };
+ },
+ getLoginStatus(cb) {
+ cb?.call(this, { status: "unknown" });
+ },
+ getUserID() {},
+ init(_initInfo) {
+ initInfo = _initInfo; // in case the site is not using fbAsyncInit
+ },
+ login(cb, opts) {
+ // We have to load Facebook's script, and then wait for it to call
+ // window.open. By that time, the popup blocker will likely trigger.
+ // So we open a popup now with about:blank, and then make sure FB
+ // will re-use that same popup later.
+ if (needPopup) {
+ activePopup = window.open("about:blank", popupName, buildPopupParams());
+ }
+ allowFacebookSDK(() => {
+ activePopup = undefined;
+ function runPostLoginCallbacks() {
+ try {
+ cb?.apply(this, arguments);
+ } catch (e) {
+ console.error(e);
+ }
+ if (activeOnloginAttribute) {
+ setTimeout(activeOnloginAttribute, 1);
+ activeOnloginAttribute = undefined;
+ }
+ }
+ window.FB.login(runPostLoginCallbacks, opts);
+ }).catch(() => {
+ activePopup = undefined;
+ activeOnloginAttribute = undefined;
+ try {
+ cb?.({});
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ },
+ logout(cb) {
+ cb?.call(this);
+ },
+ ui(params, fn) {
+ if (params.method === "permissions.oauth") {
+ window.FB.login(fn, params);
+ }
+ },
+ XFBML: {
+ parse(node, cb) {
+ node = node || document;
+ node.querySelectorAll(".fb-login-button").forEach(makeLoginPlaceholder);
+ node.querySelectorAll(".fb-video").forEach(makeVideoPlaceholder);
+ try {
+ cb?.call(this);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ },
+ });
+
+ window.FB.XFBML.parse();
+
+ window?.fbAsyncInit?.();
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/facebook.svg b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/facebook.svg
new file mode 100644
index 0000000000..df63700a9e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/facebook.svg
@@ -0,0 +1,3 @@
+<!-- copyright is dedicated to the Public Domain.
+ https://en.wikipedia.org/wiki/File:Facebook_f_logo_(2019).svg -->
+<svg xmlns="http://www.w3.org/2000/svg" width="1365.12" height="1365.12" viewBox="0 0 14222 14222"><circle cx="7111" cy="7112" r="7111" fill="#fff"/><path d="M9879 9168l315-2056H8222V5778c0-562 275-1111 1159-1111h897V2917s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9168z" fill="#1977f3"/></svg>
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/fastclick.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/fastclick.js
new file mode 100644
index 0000000000..ad6814c995
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/fastclick.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1738220 - Shim Conversant FastClick
+ *
+ * Sites assuming FastClick will load can break if it is blocked.
+ * This shim mitigates that breakage.
+ */
+
+// FastClick bundles nodeJS packages/core-js/internals/dom-iterables.js
+// which is known to be needed by at least one site.
+if (!HTMLCollection.prototype.forEach) {
+ const DOMIterables = [
+ "CSSRuleList",
+ "CSSStyleDeclaration",
+ "CSSValueList",
+ "ClientRectList",
+ "DOMRectList",
+ "DOMStringList",
+ "DOMTokenList",
+ "DataTransferItemList",
+ "FileList",
+ "HTMLAllCollection",
+ "HTMLCollection",
+ "HTMLFormElement",
+ "HTMLSelectElement",
+ "MediaList",
+ "MimeTypeArray",
+ "NamedNodeMap",
+ "NodeList",
+ "PaintRequestList",
+ "Plugin",
+ "PluginArray",
+ "SVGLengthList",
+ "SVGNumberList",
+ "SVGPathSegList",
+ "SVGPointList",
+ "SVGStringList",
+ "SVGTransformList",
+ "SourceBufferList",
+ "StyleSheetList",
+ "TextTrackCueList",
+ "TextTrackList",
+ "TouchList",
+ ];
+
+ const forEach = Array.prototype.forEach;
+
+ const handlePrototype = proto => {
+ if (!proto || proto.forEach === forEach) {
+ return;
+ }
+ try {
+ Object.defineProperty(proto, "forEach", {
+ enumerable: false,
+ get: () => forEach,
+ });
+ } catch (_) {
+ proto.forEach = forEach;
+ }
+ };
+
+ for (const name of DOMIterables) {
+ handlePrototype(window[name]?.prototype);
+ }
+}
+
+if (!window.conversant?.launch) {
+ const c = (window.conversant = window.conversant || {});
+ c.launch = () => {};
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/firebase.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/firebase.js
new file mode 100644
index 0000000000..8ac049c5e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/firebase.js
@@ -0,0 +1,95 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1767407 - Shim Firebase
+ *
+ * Sites relying on firebase-messaging.js will break in Private
+ * browsing mode because it assumes that they require service
+ * workers and indexedDB, when they generally do not.
+ */
+
+/* globals cloneInto */
+
+(function () {
+ const win = window.wrappedJSObject;
+ const emptyObj = new win.Object();
+ const emptyArr = new win.Array();
+ const emptyMsg = cloneInto({ message: "" }, window);
+ const noOpFn = cloneInto(function () {}, window, { cloneFunctions: true });
+
+ if (!win.indexedDB) {
+ const idb = {
+ open: () => win.Promise.reject(emptyMsg),
+ };
+
+ Object.defineProperty(win, "indexedDB", {
+ value: cloneInto(idb, window, { cloneFunctions: true }),
+ });
+ }
+
+ // bug 1778993
+ for (const name of [
+ "IDBCursor",
+ "IDBDatabase",
+ "IDBIndex",
+ "IDBOpenDBRequest",
+ "IDBRequest",
+ "IDBTransaction",
+ ]) {
+ if (!win[name]) {
+ Object.defineProperty(win, name, { value: emptyObj });
+ }
+ }
+
+ if (!win.serviceWorker) {
+ const sw = {
+ addEventListener() {},
+ getRegistrations: () => win.Promise.resolve(emptyArr),
+ register: () => win.Promise.reject(emptyMsg),
+ };
+
+ Object.defineProperty(navigator.wrappedJSObject, "serviceWorker", {
+ value: cloneInto(sw, window, { cloneFunctions: true }),
+ });
+
+ // bug 1779536
+ Object.defineProperty(navigator.wrappedJSObject.serviceWorker, "ready", {
+ value: new win.Promise(noOpFn),
+ });
+ }
+
+ // bug 1750699
+ if (!win.PushManager) {
+ Object.defineProperty(win, "PushManager", { value: emptyObj });
+ }
+
+ // bug 1750699
+ if (!win.PushSubscription) {
+ const ps = {
+ prototype: {
+ getKey() {},
+ },
+ };
+
+ Object.defineProperty(win, "PushSubscription", {
+ value: cloneInto(ps, window, { cloneFunctions: true }),
+ });
+ }
+
+ // bug 1750699
+ if (!win.ServiceWorkerRegistration) {
+ const swr = {
+ prototype: {
+ showNotification() {},
+ },
+ };
+
+ Object.defineProperty(win, "ServiceWorkerRegistration", {
+ value: cloneInto(swr, window, { cloneFunctions: true }),
+ });
+ }
+})();
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ads.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ads.js
new file mode 100644
index 0000000000..a432186f43
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ads.js
@@ -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/. */
+
+"use strict";
+
+/**
+ * Bug 1713726 - Shim Ads by Google
+ *
+ * Sites relying on window.adsbygoogle may encounter breakage if it is blocked.
+ * This shim provides a stub for that API to mitigate that breakage.
+ */
+
+if (window.adsbygoogle?.loaded === undefined) {
+ window.adsbygoogle = {
+ loaded: true,
+ push() {},
+ };
+}
+
+if (window.gapi?._pl === undefined) {
+ const stub = {
+ go() {},
+ render: () => "",
+ };
+ window.gapi = {
+ _pl: true,
+ additnow: stub,
+ autocomplete: stub,
+ backdrop: stub,
+ blogger: stub,
+ commentcount: stub,
+ comments: stub,
+ community: stub,
+ donation: stub,
+ family_creation: stub,
+ follow: stub,
+ hangout: stub,
+ health: stub,
+ interactivepost: stub,
+ load() {},
+ logutil: {
+ enableDebugLogging() {},
+ },
+ page: stub,
+ partnersbadge: stub,
+ person: stub,
+ platform: {
+ go() {},
+ },
+ playemm: stub,
+ playreview: stub,
+ plus: stub,
+ plusone: stub,
+ post: stub,
+ profile: stub,
+ ratingbadge: stub,
+ recobar: stub,
+ savetoandroidpay: stub,
+ savetodrive: stub,
+ savetowallet: stub,
+ share: stub,
+ sharetoclassroom: stub,
+ shortlists: stub,
+ signin: stub,
+ signin2: stub,
+ surveyoptin: stub,
+ visibility: stub,
+ youtube: stub,
+ ytsubscribe: stub,
+ zoomableimage: stub,
+ };
+}
+
+for (const e of document.querySelectorAll("ins.adsbygoogle")) {
+ e.style.maxWidth = "0px";
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-and-tag-manager.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-and-tag-manager.js
new file mode 100644
index 0000000000..8809fca8ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-and-tag-manager.js
@@ -0,0 +1,187 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713687 - Shim Google Analytics and Tag Manager
+ *
+ * Sites often rely on the Google Analytics window object and will
+ * break if it fails to load or is blocked. This shim works around
+ * such breakage.
+ *
+ * Sites also often use the Google Optimizer (asynchide) code snippet,
+ * only for it to cause multi-second delays if Google Analytics does
+ * not load. This shim also avoids such delays.
+ *
+ * They also rely on Google Tag Manager, which often goes hand-in-
+ * hand with Analytics, but is not always blocked by anti-tracking
+ * lists. Handling both in the same shim handles both cases.
+ */
+
+if (window[window.GoogleAnalyticsObject || "ga"]?.loaded === undefined) {
+ const DEFAULT_TRACKER_NAME = "t0";
+
+ const trackers = new Map();
+
+ const run = function (fn, ...args) {
+ if (typeof fn === "function") {
+ try {
+ fn(...args);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ };
+
+ const create = (id, cookie, name, opts) => {
+ id = id || opts?.trackerId;
+ if (!id) {
+ return undefined;
+ }
+ cookie = cookie || opts?.cookieDomain || "_ga";
+ name = name || opts?.name || DEFAULT_TRACKER_NAME;
+ if (!trackers.has(name)) {
+ let props;
+ try {
+ props = new Map(Object.entries(opts));
+ } catch (_) {
+ props = new Map();
+ }
+ trackers.set(name, {
+ get(p) {
+ if (p === "name") {
+ return name;
+ } else if (p === "trackingId") {
+ return id;
+ } else if (p === "cookieDomain") {
+ return cookie;
+ }
+ return props.get(p);
+ },
+ ma() {},
+ requireSync() {},
+ send() {},
+ set(p, v) {
+ if (typeof p !== "object") {
+ p = Object.fromEntries([[p, v]]);
+ }
+ for (const k in p) {
+ props.set(k, p[k]);
+ if (k === "hitCallback") {
+ run(p[k]);
+ }
+ }
+ },
+ });
+ }
+ return trackers.get(name);
+ };
+
+ const cmdRE = /((?<name>.*?)\.)?((?<plugin>.*?):)?(?<method>.*)/;
+
+ function ga(cmd, ...args) {
+ if (arguments.length === 1 && typeof cmd === "function") {
+ run(cmd, trackers.get(DEFAULT_TRACKER_NAME));
+ return undefined;
+ }
+
+ if (typeof cmd !== "string") {
+ return undefined;
+ }
+
+ const groups = cmdRE.exec(cmd)?.groups;
+ if (!groups) {
+ console.error("Could not parse GA command", cmd);
+ return undefined;
+ }
+
+ let { name, plugin, method } = groups;
+
+ if (plugin) {
+ return undefined;
+ }
+
+ if (cmd === "set") {
+ trackers.get(name)?.set(args[0], args[1]);
+ }
+
+ if (method === "remove") {
+ trackers.delete(name);
+ return undefined;
+ }
+
+ if (cmd === "send") {
+ run(args.at(-1)?.hitCallback);
+ return undefined;
+ }
+
+ if (method === "create") {
+ let id, cookie, fields;
+ for (const param of args.slice(0, 4)) {
+ if (typeof param === "object") {
+ fields = param;
+ break;
+ }
+ if (id === undefined) {
+ id = param;
+ } else if (cookie === undefined) {
+ cookie = param;
+ } else {
+ name = param;
+ }
+ }
+ return create(id, cookie, name, fields);
+ }
+
+ return undefined;
+ }
+
+ Object.assign(ga, {
+ create: (a, b, c, d) => ga("create", a, b, c, d),
+ getAll: () => Array.from(trackers.values()),
+ getByName: name => trackers.get(name),
+ loaded: true,
+ remove: t => ga("remove", t),
+ });
+
+ // Process any GA command queue the site pre-declares (bug 1736850)
+ const q = window[window.GoogleAnalyticsObject || "ga"]?.q;
+ window[window.GoogleAnalyticsObject || "ga"] = ga;
+
+ if (Array.isArray(q)) {
+ const push = o => {
+ ga(...o);
+ return true;
+ };
+ q.push = push;
+ q.forEach(o => push(o));
+ }
+
+ // Also process the Google Tag Manager dataLayer (bug 1713688)
+ const dl = window.dataLayer;
+
+ if (Array.isArray(dl) && !dl.find(e => e["gtm.start"])) {
+ const push = function (o) {
+ setTimeout(() => run(o?.eventCallback), 1);
+ return true;
+ };
+ dl.push = push;
+ dl.forEach(o => push(o));
+ }
+
+ // Run dataLayer.hide.end to handle asynchide (bug 1628151)
+ run(window.dataLayer?.hide?.end);
+}
+
+if (!window?.gaplugins?.Linker) {
+ window.gaplugins = window.gaplugins || {};
+ window.gaplugins.Linker = class {
+ autoLink() {}
+ decorate(url) {
+ return url;
+ }
+ passthrough() {}
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js
new file mode 100644
index 0000000000..60b49df120
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js
@@ -0,0 +1,13 @@
+/* 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/. */
+
+"use strict";
+
+if (!window.gaplugins) {
+ window.gaplugins = {};
+}
+
+if (!window.gaplugins.EC) {
+ window.gaplugins.EC = () => {};
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-legacy.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-legacy.js
new file mode 100644
index 0000000000..da1a638e12
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-analytics-legacy.js
@@ -0,0 +1,137 @@
+/* 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/. */
+
+// based on https://github.com/gorhill/uBlock/blob/6f49e079db0262e669b70f4169924f796ac8db7c/src/web_accessible_resources/google-analytics_ga.js
+
+"use strict";
+
+if (!window._gaq) {
+ function noopfn() {}
+
+ const gaq = {
+ Na: noopfn,
+ O: noopfn,
+ Sa: noopfn,
+ Ta: noopfn,
+ Va: noopfn,
+ _createAsyncTracker: noopfn,
+ _getAsyncTracker: noopfn,
+ _getPlugin: noopfn,
+ push: a => {
+ if (typeof a === "function") {
+ a();
+ return;
+ }
+ if (!Array.isArray(a)) {
+ return;
+ }
+ if (
+ typeof a[0] === "string" &&
+ /(^|\.)_link$/.test(a[0]) &&
+ typeof a[1] === "string"
+ ) {
+ window.location.assign(a[1]);
+ }
+ if (
+ a[0] === "_set" &&
+ a[1] === "hitCallback" &&
+ typeof a[2] === "function"
+ ) {
+ a[2]();
+ }
+ },
+ };
+
+ const tracker = {
+ _addIgnoredOrganic: noopfn,
+ _addIgnoredRef: noopfn,
+ _addItem: noopfn,
+ _addOrganic: noopfn,
+ _addTrans: noopfn,
+ _clearIgnoredOrganic: noopfn,
+ _clearIgnoredRef: noopfn,
+ _clearOrganic: noopfn,
+ _cookiePathCopy: noopfn,
+ _deleteCustomVar: noopfn,
+ _getName: noopfn,
+ _setAccount: noopfn,
+ _getAccount: noopfn,
+ _getClientInfo: noopfn,
+ _getDetectFlash: noopfn,
+ _getDetectTitle: noopfn,
+ _getLinkerUrl: a => a,
+ _getLocalGifPath: noopfn,
+ _getServiceMode: noopfn,
+ _getVersion: noopfn,
+ _getVisitorCustomVar: noopfn,
+ _initData: noopfn,
+ _link: noopfn,
+ _linkByPost: noopfn,
+ _setAllowAnchor: noopfn,
+ _setAllowHash: noopfn,
+ _setAllowLinker: noopfn,
+ _setCampContentKey: noopfn,
+ _setCampMediumKey: noopfn,
+ _setCampNameKey: noopfn,
+ _setCampNOKey: noopfn,
+ _setCampSourceKey: noopfn,
+ _setCampTermKey: noopfn,
+ _setCampaignCookieTimeout: noopfn,
+ _setCampaignTrack: noopfn,
+ _setClientInfo: noopfn,
+ _setCookiePath: noopfn,
+ _setCookiePersistence: noopfn,
+ _setCookieTimeout: noopfn,
+ _setCustomVar: noopfn,
+ _setDetectFlash: noopfn,
+ _setDetectTitle: noopfn,
+ _setDomainName: noopfn,
+ _setLocalGifPath: noopfn,
+ _setLocalRemoteServerMode: noopfn,
+ _setLocalServerMode: noopfn,
+ _setReferrerOverride: noopfn,
+ _setRemoteServerMode: noopfn,
+ _setSampleRate: noopfn,
+ _setSessionTimeout: noopfn,
+ _setSiteSpeedSampleRate: noopfn,
+ _setSessionCookieTimeout: noopfn,
+ _setVar: noopfn,
+ _setVisitorCookieTimeout: noopfn,
+ _trackEvent: noopfn,
+ _trackPageLoadTime: noopfn,
+ _trackPageview: noopfn,
+ _trackSocial: noopfn,
+ _trackTiming: noopfn,
+ _trackTrans: noopfn,
+ _visitCode: noopfn,
+ };
+
+ const gat = {
+ _anonymizeIP: noopfn,
+ _createTracker: noopfn,
+ _forceSSL: noopfn,
+ _getPlugin: noopfn,
+ _getTracker: () => tracker,
+ _getTrackerByName: () => tracker,
+ _getTrackers: noopfn,
+ aa: noopfn,
+ ab: noopfn,
+ hb: noopfn,
+ la: noopfn,
+ oa: noopfn,
+ pa: noopfn,
+ u: noopfn,
+ };
+
+ window._gat = gat;
+
+ const aa = window._gaq || [];
+ if (Array.isArray(aa)) {
+ while (aa[0]) {
+ gaq.push(aa.shift());
+ }
+ }
+
+ window._gaq = gaq.qf = gaq;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ima.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ima.js
new file mode 100644
index 0000000000..1f5e56239d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ima.js
@@ -0,0 +1,620 @@
+/* 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/. */
+
+/**
+ * Bug 1713690 - Shim Google Interactive Media Ads ima3.js
+ *
+ * Many sites use ima3.js for ad bidding and placement, often in conjunction
+ * with Google Publisher Tags, Prebid.js and/or other scripts. This shim
+ * provides a stubbed-out version of the API which helps work around related
+ * site breakage, such as black bxoes where videos ought to be placed.
+ */
+
+if (!window.google?.ima?.VERSION) {
+ const VERSION = "3.517.2";
+
+ const CheckCanAutoplay = (function () {
+ // Sourced from: https://searchfox.org/mozilla-central/source/dom/media/gtest/negative_duration.mp4
+ const TEST_VIDEO = new Blob(
+ [
+ new Uint32Array([
+ 469762048, 1887007846, 1752392036, 0, 913273705, 1717987696,
+ 828601953, -1878917120, 1987014509, 1811939328, 1684567661, 0, 0, 0,
+ -402456576, 0, 256, 1, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 64, 0, 0, 0,
+ 0, 0, 0, 33554432, -201261056, 1801548404, 1744830464, 1684564852,
+ 251658241, 0, 0, 0, 0, 16777216, 0, -1, -1, 0, 0, 0, 0, 256, 0, 0, 0,
+ 256, 0, 0, 0, 64, 5, 53250, -2080309248, 1634296941, 738197504,
+ 1684563053, 1, 0, 0, 0, 0, -2137614336, -1, -1, 50261, 754974720,
+ 1919706216, 0, 0, 1701079414, 0, 0, 0, 1701079382, 1851869295,
+ 1919249508, 16777216, 1852402979, 102, 1752004116, 100, 1, 0, 0,
+ 1852400676, 102, 1701995548, 102, 0, 1, 1819440396, 32, 1, 1651799011,
+ 108, 1937011607, 100, 0, 1, 1668702599, 49, 0, 1, 0, 0, 0, 33555712,
+ 4718800, 4718592, 0, 65536, 0, 0, 0, 0, 0, 0, 0, 0, 16776984,
+ 1630601216, 21193590, -14745500, 1729626337, -1407254428, 89161945,
+ 1049019, 9453056, -251611125, 27269507, -379058688, -1329024392,
+ 268435456, 1937011827, 0, 0, 268435456, 1668510835, 0, 0, 335544320,
+ 2054386803, 0, 0, 0, 268435456, 1868788851, 0, 0, 671088640,
+ 2019915373, 536870912, 2019914356, 0, 16777216, 16777216, 0, 0, 0,
+ ]),
+ ],
+ { type: "video/mp4" }
+ );
+
+ let testVideo = undefined;
+
+ return function () {
+ if (!testVideo) {
+ testVideo = document.createElement("video");
+ testVideo.style =
+ "position:absolute; width:0; height:0; left:0; right:0; z-index:-1; border:0";
+ testVideo.setAttribute("muted", "muted");
+ testVideo.setAttribute("playsinline", "playsinline");
+ testVideo.src = URL.createObjectURL(TEST_VIDEO);
+ document.body.appendChild(testVideo);
+ }
+ return testVideo.play();
+ };
+ })();
+
+ let ima = {};
+
+ class AdDisplayContainer {
+ destroy() {}
+ initialize() {}
+ }
+
+ class ImaSdkSettings {
+ #c = true;
+ #f = {};
+ #i = false;
+ #l = "";
+ #p = "";
+ #r = 0;
+ #t = "";
+ #v = "";
+ getCompanionBackfill() {}
+ getDisableCustomPlaybackForIOS10Plus() {
+ return this.#i;
+ }
+ getFeatureFlags() {
+ return this.#f;
+ }
+ getLocale() {
+ return this.#l;
+ }
+ getNumRedirects() {
+ return this.#r;
+ }
+ getPlayerType() {
+ return this.#t;
+ }
+ getPlayerVersion() {
+ return this.#v;
+ }
+ getPpid() {
+ return this.#p;
+ }
+ isCookiesEnabled() {
+ return this.#c;
+ }
+ setAutoPlayAdBreaks() {}
+ setCompanionBackfill() {}
+ setCookiesEnabled(c) {
+ this.#c = !!c;
+ }
+ setDisableCustomPlaybackForIOS10Plus(i) {
+ this.#i = !!i;
+ }
+ setFeatureFlags(f) {
+ this.#f = f;
+ }
+ setLocale(l) {
+ this.#l = l;
+ }
+ setNumRedirects(r) {
+ this.#r = r;
+ }
+ setPlayerType(t) {
+ this.#t = t;
+ }
+ setPlayerVersion(v) {
+ this.#v = v;
+ }
+ setPpid(p) {
+ this.#p = p;
+ }
+ setSessionId(s) {}
+ setVpaidAllowed(a) {}
+ setVpaidMode(m) {}
+ }
+ ImaSdkSettings.CompanionBackfillMode = {
+ ALWAYS: "always",
+ ON_MASTER_AD: "on_master_ad",
+ };
+ ImaSdkSettings.VpaidMode = {
+ DISABLED: 0,
+ ENABLED: 1,
+ INSECURE: 2,
+ };
+
+ class EventHandler {
+ #listeners = new Map();
+
+ _dispatch(e) {
+ const listeners = this.#listeners.get(e.type) || [];
+ for (const listener of Array.from(listeners)) {
+ try {
+ listener(e);
+ } catch (r) {
+ console.error(r);
+ }
+ }
+ }
+
+ addEventListener(t, c) {
+ if (!this.#listeners.has(t)) {
+ this.#listeners.set(t, new Set());
+ }
+ this.#listeners.get(t).add(c);
+ }
+
+ removeEventListener(t, c) {
+ this.#listeners.get(t)?.delete(c);
+ }
+ }
+
+ class AdsLoader extends EventHandler {
+ #settings = new ImaSdkSettings();
+ contentComplete() {}
+ destroy() {}
+ getSettings() {
+ return this.#settings;
+ }
+ getVersion() {
+ return VERSION;
+ }
+ requestAds(r, c) {
+ // If autoplay is disabled and the page is trying to autoplay a tracking
+ // ad, then IMA fails with an error, and the page is expected to request
+ // ads again later when the user clicks to play.
+ CheckCanAutoplay().then(
+ () => {
+ const { ADS_MANAGER_LOADED } = AdsManagerLoadedEvent.Type;
+ this._dispatch(new ima.AdsManagerLoadedEvent(ADS_MANAGER_LOADED));
+ },
+ () => {
+ const e = new ima.AdError(
+ "adPlayError",
+ 1205,
+ 1205,
+ "The browser prevented playback initiated without user interaction."
+ );
+ this._dispatch(new ima.AdErrorEvent(e));
+ }
+ );
+ }
+ }
+
+ class AdsManager extends EventHandler {
+ #volume = 1;
+ collapse() {}
+ configureAdsManager() {}
+ destroy() {}
+ discardAdBreak() {}
+ expand() {}
+ focus() {}
+ getAdSkippableState() {
+ return false;
+ }
+ getCuePoints() {
+ return [0];
+ }
+ getCurrentAd() {
+ return currentAd;
+ }
+ getCurrentAdCuePoints() {
+ return [];
+ }
+ getRemainingTime() {
+ return 0;
+ }
+ getVolume() {
+ return this.#volume;
+ }
+ init(w, h, m, e) {}
+ isCustomClickTrackingUsed() {
+ return false;
+ }
+ isCustomPlaybackUsed() {
+ return false;
+ }
+ pause() {}
+ requestNextAdBreak() {}
+ resize(w, h, m) {}
+ resume() {}
+ setVolume(v) {
+ this.#volume = v;
+ }
+ skip() {}
+ start() {
+ requestAnimationFrame(() => {
+ for (const type of [
+ AdEvent.Type.LOADED,
+ AdEvent.Type.STARTED,
+ AdEvent.Type.CONTENT_RESUME_REQUESTED,
+ AdEvent.Type.AD_BUFFERING,
+ AdEvent.Type.FIRST_QUARTILE,
+ AdEvent.Type.MIDPOINT,
+ AdEvent.Type.THIRD_QUARTILE,
+ AdEvent.Type.COMPLETE,
+ AdEvent.Type.ALL_ADS_COMPLETED,
+ ]) {
+ try {
+ this._dispatch(new ima.AdEvent(type));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ });
+ }
+ stop() {}
+ updateAdsRenderingSettings(s) {}
+ }
+
+ class AdsRenderingSettings {}
+
+ class AdsRequest {
+ setAdWillAutoPlay() {}
+ setAdWillPlayMuted() {}
+ setContinuousPlayback() {}
+ }
+
+ class AdPodInfo {
+ getAdPosition() {
+ return 1;
+ }
+ getIsBumper() {
+ return false;
+ }
+ getMaxDuration() {
+ return -1;
+ }
+ getPodIndex() {
+ return 1;
+ }
+ getTimeOffset() {
+ return 0;
+ }
+ getTotalAds() {
+ return 1;
+ }
+ }
+
+ class Ad {
+ _pi = new AdPodInfo();
+ getAdId() {
+ return "";
+ }
+ getAdPodInfo() {
+ return this._pi;
+ }
+ getAdSystem() {
+ return "";
+ }
+ getAdvertiserName() {
+ return "";
+ }
+ getApiFramework() {
+ return null;
+ }
+ getCompanionAds() {
+ return [];
+ }
+ getContentType() {
+ return "";
+ }
+ getCreativeAdId() {
+ return "";
+ }
+ getCreativeId() {
+ return "";
+ }
+ getDealId() {
+ return "";
+ }
+ getDescription() {
+ return "";
+ }
+ getDuration() {
+ return 8.5;
+ }
+ getHeight() {
+ return 0;
+ }
+ getMediaUrl() {
+ return null;
+ }
+ getMinSuggestedDuration() {
+ return -2;
+ }
+ getSkipTimeOffset() {
+ return -1;
+ }
+ getSurveyUrl() {
+ return null;
+ }
+ getTitle() {
+ return "";
+ }
+ getTraffickingParameters() {
+ return {};
+ }
+ getTraffickingParametersString() {
+ return "";
+ }
+ getUiElements() {
+ return [""];
+ }
+ getUniversalAdIdRegistry() {
+ return "unknown";
+ }
+ getUniversalAdIds() {
+ return [""];
+ }
+ getUniversalAdIdValue() {
+ return "unknown";
+ }
+ getVastMediaBitrate() {
+ return 0;
+ }
+ getVastMediaHeight() {
+ return 0;
+ }
+ getVastMediaWidth() {
+ return 0;
+ }
+ getWidth() {
+ return 0;
+ }
+ getWrapperAdIds() {
+ return [""];
+ }
+ getWrapperAdSystems() {
+ return [""];
+ }
+ getWrapperCreativeIds() {
+ return [""];
+ }
+ isLinear() {
+ return true;
+ }
+ isSkippable() {
+ return true;
+ }
+ }
+
+ class CompanionAd {
+ getAdSlotId() {
+ return "";
+ }
+ getContent() {
+ return "";
+ }
+ getContentType() {
+ return "";
+ }
+ getHeight() {
+ return 1;
+ }
+ getWidth() {
+ return 1;
+ }
+ }
+
+ class AdError {
+ #errorCode = -1;
+ #message = "";
+ #type = "";
+ #vastErrorCode = -1;
+ constructor(type, code, vast, message) {
+ this.#errorCode = code;
+ this.#message = message;
+ this.#type = type;
+ this.#vastErrorCode = vast;
+ }
+ getErrorCode() {
+ return this.#errorCode;
+ }
+ getInnerError() {}
+ getMessage() {
+ return this.#message;
+ }
+ getType() {
+ return this.#type;
+ }
+ getVastErrorCode() {
+ return this.#vastErrorCode;
+ }
+ toString() {
+ return `AdError ${this.#errorCode}: ${this.#message}`;
+ }
+ }
+ AdError.ErrorCode = {};
+ AdError.Type = {};
+
+ const isEngadget = () => {
+ try {
+ for (const ctx of Object.values(window.vidible._getContexts())) {
+ if (ctx.getPlayer()?.div?.innerHTML.includes("www.engadget.com")) {
+ return true;
+ }
+ }
+ } catch (_) {}
+ return false;
+ };
+
+ const currentAd = isEngadget() ? undefined : new Ad();
+
+ class AdEvent {
+ constructor(type) {
+ this.type = type;
+ }
+ getAd() {
+ return currentAd;
+ }
+ getAdData() {
+ return {};
+ }
+ }
+ AdEvent.Type = {
+ AD_BREAK_READY: "adBreakReady",
+ AD_BUFFERING: "adBuffering",
+ AD_CAN_PLAY: "adCanPlay",
+ AD_METADATA: "adMetadata",
+ AD_PROGRESS: "adProgress",
+ ALL_ADS_COMPLETED: "allAdsCompleted",
+ CLICK: "click",
+ COMPLETE: "complete",
+ CONTENT_PAUSE_REQUESTED: "contentPauseRequested",
+ CONTENT_RESUME_REQUESTED: "contentResumeRequested",
+ DURATION_CHANGE: "durationChange",
+ EXPANDED_CHANGED: "expandedChanged",
+ FIRST_QUARTILE: "firstQuartile",
+ IMPRESSION: "impression",
+ INTERACTION: "interaction",
+ LINEAR_CHANGE: "linearChange",
+ LINEAR_CHANGED: "linearChanged",
+ LOADED: "loaded",
+ LOG: "log",
+ MIDPOINT: "midpoint",
+ PAUSED: "pause",
+ RESUMED: "resume",
+ SKIPPABLE_STATE_CHANGED: "skippableStateChanged",
+ SKIPPED: "skip",
+ STARTED: "start",
+ THIRD_QUARTILE: "thirdQuartile",
+ USER_CLOSE: "userClose",
+ VIDEO_CLICKED: "videoClicked",
+ VIDEO_ICON_CLICKED: "videoIconClicked",
+ VIEWABLE_IMPRESSION: "viewable_impression",
+ VOLUME_CHANGED: "volumeChange",
+ VOLUME_MUTED: "mute",
+ };
+
+ class AdErrorEvent {
+ type = "adError";
+ #error = "";
+ constructor(error) {
+ this.#error = error;
+ }
+ getError() {
+ return this.#error;
+ }
+ getUserRequestContext() {
+ return {};
+ }
+ }
+ AdErrorEvent.Type = {
+ AD_ERROR: "adError",
+ };
+
+ const manager = new AdsManager();
+
+ class AdsManagerLoadedEvent {
+ constructor(type) {
+ this.type = type;
+ }
+ getAdsManager() {
+ return manager;
+ }
+ getUserRequestContext() {
+ return {};
+ }
+ }
+ AdsManagerLoadedEvent.Type = {
+ ADS_MANAGER_LOADED: "adsManagerLoaded",
+ };
+
+ class CustomContentLoadedEvent {}
+ CustomContentLoadedEvent.Type = {
+ CUSTOM_CONTENT_LOADED: "deprecated-event",
+ };
+
+ class CompanionAdSelectionSettings {}
+ CompanionAdSelectionSettings.CreativeType = {
+ ALL: "All",
+ FLASH: "Flash",
+ IMAGE: "Image",
+ };
+ CompanionAdSelectionSettings.ResourceType = {
+ ALL: "All",
+ HTML: "Html",
+ IFRAME: "IFrame",
+ STATIC: "Static",
+ };
+ CompanionAdSelectionSettings.SizeCriteria = {
+ IGNORE: "IgnoreSize",
+ SELECT_EXACT_MATCH: "SelectExactMatch",
+ SELECT_NEAR_MATCH: "SelectNearMatch",
+ };
+
+ class AdCuePoints {
+ getCuePoints() {
+ return [];
+ }
+ }
+
+ class AdProgressData {}
+
+ class UniversalAdIdInfo {
+ getAdIdRegistry() {
+ return "";
+ }
+ getAdIsValue() {
+ return "";
+ }
+ }
+
+ Object.assign(ima, {
+ AdCuePoints,
+ AdDisplayContainer,
+ AdError,
+ AdErrorEvent,
+ AdEvent,
+ AdPodInfo,
+ AdProgressData,
+ AdsLoader,
+ AdsManager: manager,
+ AdsManagerLoadedEvent,
+ AdsRenderingSettings,
+ AdsRequest,
+ CompanionAd,
+ CompanionAdSelectionSettings,
+ CustomContentLoadedEvent,
+ gptProxyInstance: {},
+ ImaSdkSettings,
+ OmidAccessMode: {
+ DOMAIN: "domain",
+ FULL: "full",
+ LIMITED: "limited",
+ },
+ settings: new ImaSdkSettings(),
+ UiElements: {
+ AD_ATTRIBUTION: "adAttribution",
+ COUNTDOWN: "countdown",
+ },
+ UniversalAdIdInfo,
+ VERSION,
+ ViewMode: {
+ FULLSCREEN: "fullscreen",
+ NORMAL: "normal",
+ },
+ });
+
+ if (!window.google) {
+ window.google = {};
+ }
+
+ window.google.ima = ima;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-page-ad.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-page-ad.js
new file mode 100644
index 0000000000..42f3a0fca5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-page-ad.js
@@ -0,0 +1,17 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713692 - Shim Google Page Ad conversion tracker
+ *
+ * This shim stubs out the simple API for converstion tracking with
+ * Google Page Ad, mitigating major breakage on pages which presume
+ * the API will always successfully load.
+ */
+
+if (!window.google_trackConversion) {
+ window.google_trackConversion = () => {};
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-publisher-tags.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-publisher-tags.js
new file mode 100644
index 0000000000..e5174d3244
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-publisher-tags.js
@@ -0,0 +1,509 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713685 - Shim Google Publisher Tags
+ *
+ * Many sites rely on googletag to place content or drive ad bidding,
+ * and will experience major breakage if it is blocked. This shim provides
+ * enough of the API's frame To mitigate much of that breakage.
+ */
+
+if (window.googletag?.apiReady === undefined) {
+ const version = "2021050601";
+
+ const noopthisfn = function () {
+ return this;
+ };
+
+ const slots = new Map();
+ const slotsById = new Map();
+ const slotsPerPath = new Map();
+ const slotCreatives = new Map();
+ const usedCreatives = new Map();
+ const fetchedSlots = new Set();
+ const eventCallbacks = new Map();
+
+ const fireSlotEvent = (name, slot) => {
+ return new Promise(resolve => {
+ requestAnimationFrame(() => {
+ const size = [0, 0];
+ for (const cb of eventCallbacks.get(name) || []) {
+ cb({ isEmpty: true, size, slot });
+ }
+ resolve();
+ });
+ });
+ };
+
+ const recreateIframeForSlot = slot => {
+ const eid = `google_ads_iframe_${slot.getId()}`;
+ document.getElementById(eid)?.remove();
+ const node = document.getElementById(slot.getSlotElementId());
+ if (node) {
+ const f = document.createElement("iframe");
+ f.id = eid;
+ f.srcdoc = "<body></body>";
+ f.style =
+ "position:absolute; width:0; height:0; left:0; right:0; z-index:-1; border:0";
+ f.setAttribute("width", 0);
+ f.setAttribute("height", 0);
+ node.appendChild(f);
+ }
+ };
+
+ const emptySlotElement = slot => {
+ const node = document.getElementById(slot.getSlotElementId());
+ while (node?.lastChild) {
+ node.lastChild.remove();
+ }
+ };
+
+ const SizeMapping = class extends Array {
+ getCreatives() {
+ const { clientWidth, clientHeight } = document.documentElement;
+ for (const [size, creatives] of this) {
+ if (clientWidth >= size[0] && clientHeight >= size[1]) {
+ return creatives;
+ }
+ }
+ return [];
+ }
+ };
+
+ const fetchSlot = slot => {
+ if (!slot) {
+ return;
+ }
+
+ const id = slot.getSlotElementId();
+
+ const node = document.getElementById(id);
+ if (!node) {
+ return;
+ }
+
+ let creatives = slotCreatives.get(id);
+ if (creatives instanceof SizeMapping) {
+ creatives = creatives.getCreatives();
+ }
+
+ if (!creatives?.length) {
+ return;
+ }
+
+ for (const creative of creatives) {
+ if (usedCreatives.has(creative)) {
+ return;
+ }
+ }
+
+ const creative = creatives[0];
+ usedCreatives.set(creative, slot);
+ fetchedSlots.add(id);
+ };
+
+ const displaySlot = async slot => {
+ if (!slot) {
+ return;
+ }
+
+ const id = slot.getSlotElementId();
+ if (!document.getElementById(id)) {
+ return;
+ }
+
+ if (!fetchedSlots.has(id)) {
+ fetchSlot(slot);
+ }
+
+ const parent = document.getElementById(id);
+ if (parent) {
+ parent.appendChild(document.createElement("div"));
+ }
+
+ emptySlotElement(slot);
+ recreateIframeForSlot(slot);
+ await fireSlotEvent("slotRenderEnded", slot);
+ await fireSlotEvent("slotRequested", slot);
+ await fireSlotEvent("slotResponseReceived", slot);
+ await fireSlotEvent("slotOnload", slot);
+ await fireSlotEvent("impressionViewable", slot);
+ };
+
+ const addEventListener = function (name, listener) {
+ if (!eventCallbacks.has(name)) {
+ eventCallbacks.set(name, new Set());
+ }
+ eventCallbacks.get(name).add(listener);
+ return this;
+ };
+
+ const removeEventListener = function (name, listener) {
+ if (eventCallbacks.has(name)) {
+ return eventCallbacks.get(name).delete(listener);
+ }
+ return false;
+ };
+
+ const companionAdsService = {
+ addEventListener,
+ enable() {},
+ fillSlot() {},
+ getAttributeKeys: () => [],
+ getDisplayAdsCorrelator: () => "",
+ getName: () => "companion_ads",
+ getSlotIdMap: () => {
+ return {};
+ },
+ getSlots: () => [],
+ getVideoStreamCorrelator() {},
+ isRoadblockingSupported: () => false,
+ isSlotAPersistentRoadblock: () => false,
+ notifyUnfilledSlots() {},
+ onImplementationLoaded() {},
+ refreshAllSlots() {
+ for (const slot of slotsById.values()) {
+ fetchSlot(slot);
+ displaySlot(slot);
+ }
+ },
+ removeEventListener,
+ set() {},
+ setRefreshUnfilledSlots() {},
+ setVideoSession() {},
+ slotRenderEnded() {},
+ };
+
+ const contentService = {
+ addEventListener,
+ setContent() {},
+ removeEventListener,
+ };
+
+ const getTargetingValue = v => {
+ if (typeof v === "string") {
+ return [v];
+ }
+ try {
+ return [Array.prototype.flat.call(v)[0]];
+ } catch (_) {}
+ return [];
+ };
+
+ const updateTargeting = (targeting, map) => {
+ if (typeof map === "object") {
+ const entries = Object.entries(map || {});
+ for (const [k, v] of entries) {
+ targeting.set(k, getTargetingValue(v));
+ }
+ }
+ };
+
+ const defineSlot = (adUnitPath, creatives, opt_div) => {
+ if (slotsById.has(opt_div)) {
+ document.getElementById(opt_div)?.remove();
+ return slotsById.get(opt_div);
+ }
+ const attributes = new Map();
+ const targeting = new Map();
+ const exclusions = new Set();
+ const response = {
+ advertiserId: undefined,
+ campaignId: undefined,
+ creativeId: undefined,
+ creativeTemplateId: undefined,
+ lineItemId: undefined,
+ };
+ const sizes = [
+ {
+ getHeight: () => 2,
+ getWidth: () => 2,
+ },
+ ];
+ const num = (slotsPerPath.get(adUnitPath) || 0) + 1;
+ slotsPerPath.set(adUnitPath, num);
+ const id = `${adUnitPath}_${num}`;
+ let clickUrl = "";
+ let collapseEmptyDiv = null;
+ let services = new Set();
+ const slot = {
+ addService(e) {
+ services.add(e);
+ return slot;
+ },
+ clearCategoryExclusions: noopthisfn,
+ clearTargeting(k) {
+ if (k === undefined) {
+ targeting.clear();
+ } else {
+ targeting.delete(k);
+ }
+ },
+ defineSizeMapping(mapping) {
+ slotCreatives.set(opt_div, mapping);
+ return this;
+ },
+ get: k => attributes.get(k),
+ getAdUnitPath: () => adUnitPath,
+ getAttributeKeys: () => Array.from(attributes.keys()),
+ getCategoryExclusions: () => Array.from(exclusions),
+ getClickUrl: () => clickUrl,
+ getCollapseEmptyDiv: () => collapseEmptyDiv,
+ getContentUrl: () => "",
+ getDivStartsCollapsed: () => null,
+ getDomId: () => opt_div,
+ getEscapedQemQueryId: () => "",
+ getFirstLook: () => 0,
+ getId: () => id,
+ getHtml: () => "",
+ getName: () => id,
+ getOutOfPage: () => false,
+ getResponseInformation: () => response,
+ getServices: () => Array.from(services),
+ getSizes: () => sizes,
+ getSlotElementId: () => opt_div,
+ getSlotId: () => slot,
+ getTargeting: k => targeting.get(k) || gTargeting.get(k) || [],
+ getTargetingKeys: () =>
+ Array.from(
+ new Set(Array.of(...gTargeting.keys(), ...targeting.keys()))
+ ),
+ getTargetingMap: () =>
+ Object.assign(
+ Object.fromEntries(gTargeting.entries()),
+ Object.fromEntries(targeting.entries())
+ ),
+ set(k, v) {
+ attributes.set(k, v);
+ return slot;
+ },
+ setCategoryExclusion(e) {
+ exclusions.add(e);
+ return slot;
+ },
+ setClickUrl(u) {
+ clickUrl = u;
+ return slot;
+ },
+ setCollapseEmptyDiv(v) {
+ collapseEmptyDiv = !!v;
+ return slot;
+ },
+ setSafeFrameConfig: noopthisfn,
+ setTagForChildDirectedTreatment: noopthisfn,
+ setTargeting(k, v) {
+ targeting.set(k, getTargetingValue(v));
+ return slot;
+ },
+ toString: () => id,
+ updateTargetingFromMap(map) {
+ updateTargeting(targeting, map);
+ return slot;
+ },
+ };
+ slots.set(adUnitPath, slot);
+ slotsById.set(opt_div, slot);
+ slotCreatives.set(opt_div, creatives);
+ return slot;
+ };
+
+ let initialLoadDisabled = false;
+
+ const gTargeting = new Map();
+ const gAttributes = new Map();
+
+ let imaContent = { vid: "", cmsid: "" };
+ let videoContent = { vid: "", cmsid: "" };
+
+ const pubadsService = {
+ addEventListener,
+ clear() {},
+ clearCategoryExclusions: noopthisfn,
+ clearTagForChildDirectedTreatment: noopthisfn,
+ clearTargeting(k) {
+ if (k === undefined) {
+ gTargeting.clear();
+ } else {
+ gTargeting.delete(k);
+ }
+ },
+ collapseEmptyDivs() {},
+ defineOutOfPagePassback: (a, o) => defineSlot(a, 0, o),
+ definePassback: (a, s, o) => defineSlot(a, s, o),
+ disableInitialLoad() {
+ initialLoadDisabled = true;
+ return this;
+ },
+ display(adUnitPath, sizes, opt_div) {
+ const slot = defineSlot(adUnitPath, sizes, opt_div);
+ displaySlot(slot);
+ },
+ enable() {},
+ enableAsyncRendering() {},
+ enableLazyLoad() {},
+ enableSingleRequest() {},
+ enableSyncRendering() {},
+ enableVideoAds() {},
+ forceExperiment() {},
+ get: k => gAttributes.get(k),
+ getAttributeKeys: () => Array.from(gAttributes.keys()),
+ getCorrelator() {},
+ getImaContent: () => imaContent,
+ getName: () => "publisher_ads",
+ getSlots: () => Array.from(slots.values()),
+ getSlotIdMap() {
+ const map = {};
+ slots.values().forEach(s => {
+ map[s.getId()] = s;
+ });
+ return map;
+ },
+ getTagSessionCorrelator() {},
+ getTargeting: k => gTargeting.get(k) || [],
+ getTargetingKeys: () => Array.from(gTargeting.keys()),
+ getTargetingMap: () => Object.fromEntries(gTargeting.entries()),
+ getVersion: () => version,
+ getVideoContent: () => videoContent,
+ isInitialLoadDisabled: () => initialLoadDisabled,
+ isSRA: () => false,
+ markAsAmp() {},
+ refresh(slts) {
+ if (!slts) {
+ slts = slots.values();
+ } else if (!Array.isArray(slts)) {
+ slts = [slts];
+ }
+ for (const slot of slts) {
+ if (slot) {
+ try {
+ fetchSlot(slot);
+ displaySlot(slot);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ },
+ removeEventListener,
+ set(k, v) {
+ gAttributes[k] = v;
+ return this;
+ },
+ setCategoryExclusion: noopthisfn,
+ setCentering() {},
+ setCookieOptions: noopthisfn,
+ setCorrelator: noopthisfn,
+ setForceSafeFrame: noopthisfn,
+ setImaContent(vid, cmsid) {
+ imaContent = { vid, cmsid };
+ return this;
+ },
+ setLocation: noopthisfn,
+ setPrivacySettings: noopthisfn,
+ setPublisherProvidedId: noopthisfn,
+ setRequestNonPersonalizedAds: noopthisfn,
+ setSafeFrameConfig: noopthisfn,
+ setTagForChildDirectedTreatment: noopthisfn,
+ setTagForUnderAgeOfConsent: noopthisfn,
+ setTargeting(k, v) {
+ gTargeting.set(k, getTargetingValue(v));
+ return this;
+ },
+ setVideoContent(vid, cmsid) {
+ videoContent = { vid, cmsid };
+ return this;
+ },
+ updateCorrelator() {},
+ updateTargetingFromMap(map) {
+ updateTargeting(gTargeting, map);
+ return this;
+ },
+ };
+
+ const SizeMappingBuilder = class {
+ #mapping;
+ constructor() {
+ this.#mapping = new SizeMapping();
+ }
+ addSize(size, creatives) {
+ if (
+ size !== "fluid" &&
+ (!Array.isArray(size) || isNaN(size[0]) || isNaN(size[1]))
+ ) {
+ this.#mapping = null;
+ } else {
+ this.#mapping?.push([size, creatives]);
+ }
+ return this;
+ }
+ build() {
+ return this.#mapping;
+ }
+ };
+
+ let gt = window.googletag;
+ if (!gt) {
+ gt = window.googletag = {};
+ }
+
+ Object.assign(gt, {
+ apiReady: true,
+ companionAds: () => companionAdsService,
+ content: () => contentService,
+ defineOutOfPageSlot: (a, o) => defineSlot(a, 0, o),
+ defineSlot: (a, s, o) => defineSlot(a, s, o),
+ destroySlots() {
+ slots.clear();
+ slotsById.clear();
+ },
+ disablePublisherConsole() {},
+ display(arg) {
+ let id;
+ if (arg?.getSlotElementId) {
+ id = arg.getSlotElementId();
+ } else if (arg?.nodeType) {
+ id = arg.id;
+ } else {
+ id = String(arg);
+ }
+ displaySlot(slotsById.get(id));
+ },
+ enableServices() {},
+ enums: {
+ OutOfPageFormat: {
+ BOTTOM_ANCHOR: 3,
+ INTERSTITIAL: 5,
+ REWARDED: 4,
+ TOP_ANCHOR: 2,
+ },
+ },
+ getVersion: () => version,
+ pubads: () => pubadsService,
+ pubadsReady: true,
+ setAdIframeTitle() {},
+ sizeMapping: () => new SizeMappingBuilder(),
+ });
+
+ const run = function (fn) {
+ if (typeof fn === "function") {
+ try {
+ fn.call(window);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ };
+
+ const cmds = gt.cmd || [];
+ const newCmd = [];
+ newCmd.push = run;
+ gt.cmd = newCmd;
+
+ for (const cmd of cmds) {
+ run(cmd);
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-safeframe.html b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-safeframe.html
new file mode 100644
index 0000000000..34bc1d242f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-safeframe.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!-- 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/.
+
+ Bug 1713691 - Shim Google SafeFrame
+
+ Some sites will break if they cannot load a Google SafeFrame. This
+ shim provides a stand-in for the frame to mitigate that breakage.
+-->
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>SafeFrame Container</title>
+ <script>
+ try {
+ const F = /^([^;]+);(\d+);([\s\S]*)$/.exec(window.name);
+ window.name = "";
+ const P = window.document;
+ P.open("text/html", "replace");
+ P.write(F[3].substr(0, +F[2]));
+ P.close();
+ } catch (e) {
+ console.error(e);
+ }
+ </script>
+ </head>
+ <body></body>
+</html>
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/history.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/history.js
new file mode 100644
index 0000000000..6fbd1fdedb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/history.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+"use strict";
+
+/*
+ * Bug 1624853 - Shim Storage Access API on history.com
+ *
+ * history.com uses Adobe as a necessary third party to authenticating
+ * with a TV provider. In order to accomodate this, we grant storage access
+ * to the Adobe domain via the Storage Access API when the login or logout
+ * buttons are clicked, then forward the click to continue as normal.
+ */
+
+console.warn(
+ `When using oauth, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1624853 for details.`
+);
+
+// Third-party origin we need to request storage access for.
+const STORAGE_ACCESS_ORIGIN = "https://sp.auth.adobe.com";
+
+document.documentElement.addEventListener(
+ "click",
+ e => {
+ const { target, isTrusted } = e;
+ if (!isTrusted) {
+ return;
+ }
+
+ const button = target.closest("a");
+ if (!button) {
+ return;
+ }
+
+ const buttonLink = button.href;
+ if (buttonLink?.startsWith("https://www.history.com/mvpd-auth")) {
+ button.disabled = true;
+ button.style.opacity = 0.5;
+ e.stopPropagation();
+ e.preventDefault();
+ document
+ .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN)
+ .then(() => {
+ target.click();
+ })
+ .catch(() => {
+ button.disabled = false;
+ button.style.opacity = 1.0;
+ });
+ }
+ },
+ true
+);
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iam.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iam.js
new file mode 100644
index 0000000000..84dee0e484
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iam.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1761774 - Shim INFOnline IAM tracker
+ *
+ * Sites using IAM can break if it is blocked. This shim mitigates that
+ * breakage by loading a stand-in module.
+ */
+
+if (!window.iom?.c) {
+ window.iom = {
+ c: () => {},
+ consent: () => {},
+ count: () => {},
+ ct: () => {},
+ deloptout: () => {},
+ doo: () => {},
+ e: () => {},
+ event: () => {},
+ getInvitation: () => {},
+ getPlus: () => {},
+ gi: () => {},
+ gp: () => {},
+ h: () => {},
+ hybrid: () => {},
+ i: () => {},
+ init: () => {},
+ oi: () => {},
+ optin: () => {},
+ setMultiIdentifier: () => {},
+ setoptout: () => {},
+ smi: () => {},
+ soo: () => {},
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iaspet.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iaspet.js
new file mode 100644
index 0000000000..7e19dd52ad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iaspet.js
@@ -0,0 +1,45 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713701 - Shim Integral Ad Science iaspet.js
+ *
+ * Some sites use iaspet to place content, often together with Google Publisher
+ * Tags. This shim prevents breakage when the script is blocked.
+ */
+
+if (!window.__iasPET?.VERSION) {
+ let queue = window?.__iasPET?.queue;
+ if (!Array.isArray(queue)) {
+ queue = [];
+ }
+
+ const response = JSON.stringify({
+ brandSafety: {},
+ slots: {},
+ });
+
+ function run(cmd) {
+ try {
+ cmd?.dataHandler?.(response);
+ } catch (_) {}
+ }
+
+ queue.push = run;
+
+ window.__iasPET = {
+ VERSION: "1.16.18",
+ queue,
+ sessionId: "",
+ setTargetingForAppNexus() {},
+ setTargetingForGPT() {},
+ start() {},
+ };
+
+ while (queue.length) {
+ run(queue.shift());
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/instagram.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/instagram.js
new file mode 100644
index 0000000000..5bf5014fdc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/instagram.js
@@ -0,0 +1,55 @@
+/* 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/. */
+
+"use strict";
+
+/*
+ * Bug 1804445 - instagram login broken with dFPI enabled
+ *
+ * Instagram login with Facebook account requires Facebook to have the storage
+ * access under Instagram. This shim adds a request for storage access for
+ * Facebook when the user tries to log in with a Facebook account.
+ */
+
+console.warn(
+ `When logging in, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1804445 for details.`
+);
+
+// Third-party origin we need to request storage access for.
+const STORAGE_ACCESS_ORIGIN = "https://www.facebook.com";
+
+document.documentElement.addEventListener(
+ "click",
+ e => {
+ const { target, isTrusted } = e;
+ if (!isTrusted) {
+ return;
+ }
+ const button = target.closest("button[type=button]");
+ if (!button) {
+ return;
+ }
+ const form = target.closest("#loginForm");
+ if (!form) {
+ return;
+ }
+
+ console.warn(
+ "Calling the Storage Access API on behalf of " + STORAGE_ACCESS_ORIGIN
+ );
+ button.disabled = true;
+ e.stopPropagation();
+ e.preventDefault();
+ document
+ .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN)
+ .then(() => {
+ button.disabled = false;
+ target.click();
+ })
+ .catch(() => {
+ button.disabled = false;
+ });
+ },
+ true
+);
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/kinja.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/kinja.js
new file mode 100644
index 0000000000..d30425b42d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/kinja.js
@@ -0,0 +1,44 @@
+/* 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/. */
+
+/* globals exportFunction */
+
+"use strict";
+
+/**
+ * Kinja powered blogs rely on storage access to https://kinja.com to enable
+ * oauth with external providers. For dFPI, sites need to use the Storage Access
+ * API to gain first party storage access. This shim calls requestStorageAccess
+ * on behalf of the site when a user wants to log in via oauth.
+ */
+
+// Third-party origin we need to request storage access for.
+const STORAGE_ACCESS_ORIGIN = "https://kinja.com";
+
+// Prefix of the path opened in a new window when users click the oauth login
+// buttons.
+const OAUTH_PATH_PREFIX = "/oauthlogin?provider=";
+
+console.warn(
+ `When using oauth, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1656171 for details.`
+);
+
+// Overwrite the window.open method so we can detect oauth related popups.
+const origOpen = window.wrappedJSObject.open;
+Object.defineProperty(window.wrappedJSObject, "open", {
+ value: exportFunction((url, ...args) => {
+ // Filter oauth popups.
+ if (!url.startsWith(OAUTH_PATH_PREFIX)) {
+ return origOpen(url, ...args);
+ }
+ // Request storage access for Kinja.
+ document.requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN).then(() => {
+ origOpen(url, ...args);
+ });
+ // We don't have the window object yet which window.open returns, since the
+ // sign-in flow is dependent on the async storage access request. This isn't
+ // a problem as long as the website does not consume it.
+ return null;
+ }, window),
+});
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/live-test-shim.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/live-test-shim.js
new file mode 100644
index 0000000000..45ab9ba48b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/live-test-shim.js
@@ -0,0 +1,82 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser */
+
+if (!window.LiveTestShimPromise) {
+ const originalUrl = document.currentScript.src;
+
+ const shimId = "LiveTestShim";
+
+ const sendMessageToAddon = (function () {
+ const pendingMessages = new Map();
+ const channel = new MessageChannel();
+ channel.port1.onerror = console.error;
+ channel.port1.onmessage = event => {
+ const { messageId, response } = event.data;
+ const resolve = pendingMessages.get(messageId);
+ if (resolve) {
+ pendingMessages.delete(messageId);
+ resolve(response);
+ }
+ };
+ function reconnect() {
+ const detail = {
+ pendingMessages: [...pendingMessages.values()],
+ port: channel.port2,
+ shimId,
+ };
+ window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+ }
+ window.addEventListener("ShimHelperReady", reconnect);
+ reconnect();
+ return function (message) {
+ const messageId =
+ Math.random().toString(36).substring(2) + Date.now().toString(36);
+ return new Promise(resolve => {
+ const payload = {
+ message,
+ messageId,
+ shimId,
+ };
+ pendingMessages.set(messageId, resolve);
+ channel.port1.postMessage(payload);
+ });
+ };
+ })();
+
+ async function go(options) {
+ try {
+ const o = document.getElementById("shims");
+ const cl = o.classList;
+ cl.remove("red");
+ cl.add("green");
+ o.innerText = JSON.stringify(options || "");
+ } catch (_) {}
+
+ if (window !== top) {
+ return;
+ }
+
+ await sendMessageToAddon("optIn");
+
+ const s = document.createElement("script");
+ s.src = originalUrl;
+ document.head.appendChild(s);
+ }
+
+ window[`${shimId}Promise`] = sendMessageToAddon("getOptions").then(
+ options => {
+ if (document.readyState !== "loading") {
+ go(options);
+ } else {
+ window.addEventListener("DOMContentLoaded", () => {
+ go(options);
+ });
+ }
+ }
+ );
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/maxmind-geoip.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/maxmind-geoip.js
new file mode 100644
index 0000000000..e5eb1e45a3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/maxmind-geoip.js
@@ -0,0 +1,69 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1754389 - Shim Maxmind GeoIP library
+ *
+ * Some sites rely on Maxmind's GeoIP library which gets blocked by ETP's
+ * fingerprinter blocking. With the library window global not being defined
+ * functionality may break or the site does not render at all. This shim
+ * has it return the United States as the location for all users.
+ */
+
+if (!window.geoip2) {
+ const continent = {
+ code: "NA",
+ geoname_id: 6255149,
+ names: {
+ de: "Nordamerika",
+ en: "North America",
+ es: "Norteamérica",
+ fr: "Amérique du Nord",
+ ja: "北アメリカ",
+ "pt-BR": "América do Norte",
+ ru: "Северная Америка",
+ "zh-CN": "北美洲",
+ },
+ };
+
+ const country = {
+ geoname_id: 6252001,
+ iso_code: "US",
+ names: {
+ de: "USA",
+ en: "United States",
+ es: "Estados Unidos",
+ fr: "États-Unis",
+ ja: "アメリカ合衆国",
+ "pt-BR": "Estados Unidos",
+ ru: "США",
+ "zh-CN": "美国",
+ },
+ };
+
+ const city = {
+ names: {
+ en: "",
+ },
+ };
+
+ const callback = onSuccess => {
+ requestAnimationFrame(() => {
+ onSuccess({
+ city,
+ continent,
+ country,
+ registered_country: country,
+ });
+ });
+ };
+
+ window.geoip2 = {
+ country: callback,
+ city: callback,
+ insights: callback,
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/microsoftLogin.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/microsoftLogin.js
new file mode 100644
index 0000000000..299cbc643a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/microsoftLogin.js
@@ -0,0 +1,31 @@
+/* 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/. */
+
+const SANDBOX_ATTR = "allow-storage-access-by-user-activation";
+
+console.warn(
+ "Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1638383 for details."
+);
+
+// Watches for MS auth iframes and adds missing sandbox attribute. The attribute
+// is required so the third-party iframe can gain access to its first party
+// storage via the Storage Access API.
+function init() {
+ const observer = new MutationObserver(() => {
+ document.body
+ .querySelectorAll(
+ `iframe:is([id^='msalRenewFrame'], [src^="https://login.microsoftonline.com"])[sandbox]`
+ )
+ .forEach(frame => {
+ frame.sandbox.add(SANDBOX_ATTR);
+ });
+ });
+
+ observer.observe(document.body, {
+ attributes: true,
+ subtree: false,
+ childList: true,
+ });
+}
+window.addEventListener("DOMContentLoaded", init);
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/microsoftVirtualAssistant.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/microsoftVirtualAssistant.js
new file mode 100644
index 0000000000..4b4493750c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/microsoftVirtualAssistant.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1801277 - Shim Microsoft virtual assistant.
+ *
+ * The microsoft virtual assistant will break when accessing the indexedDB that
+ * will throw a security error because the virtual assistant is under a
+ * third-party tracking domain 'liveperson.net'. The shim replaces the indexedDB
+ * with a fake interface that won't throw an error.
+ */
+
+/* globals cloneInto */
+
+(function () {
+ const win = window.wrappedJSObject;
+
+ try {
+ // We only replace the indexedDB when liveperson.net is loaded in a
+ // third-party context. Note that this is not strictly correct because
+ // this is a cross-origin check but not a third-party check.
+ if (win.parent == win || win.location.origin == win.top.location.origin) {
+ return;
+ }
+ } catch (e) {
+ // If we get a security error when accessing the top-level origin, this
+ // shows that the window is in a cross-origin context. In this case, we can
+ // proceed to apply the shim.
+ if (e.name != "SecurityError") {
+ throw e;
+ }
+ }
+
+ const emptyMsg = cloneInto({ message: "" }, window);
+
+ const idb = {
+ open: () => win.Promise.reject(emptyMsg),
+ };
+
+ Object.defineProperty(win, "indexedDB", {
+ value: cloneInto(idb, window, { cloneFunctions: true }),
+ });
+})();
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/moat.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/moat.js
new file mode 100644
index 0000000000..9957492684
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/moat.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713704 - Shim Moat ad tracker
+ *
+ * Sites such as Forbes may gate content behind Moat ads, resulting in
+ * breakage like black boxes where videos should be placed. This shim
+ * helps mitigate that breakage by allowing the placement to succeed.
+ */
+
+if (!window.moatPrebidAPI?.__A) {
+ const targeting = new Map();
+
+ const slotConfig = {
+ m_categories: ["moat_safe"],
+ m_data: "0",
+ m_safety: "safe",
+ };
+
+ window.moatPrebidApi = {
+ __A() {},
+ disableLogging() {},
+ enableLogging() {},
+ getMoatTargetingForPage: () => slotConfig,
+ getMoatTargetingForSlot(slot) {
+ return targeting.get(slot?.getSlotElementId());
+ },
+ pageDataAvailable: () => true,
+ safetyDataAvailable: () => true,
+ setMoatTargetingForAllSlots() {
+ for (const slot of window.googletag.pubads().getSlots() || []) {
+ targeting.set(slot.getSlotElementId(), slot.getTargeting());
+ }
+ },
+ setMoatTargetingForSlot(slot) {
+ targeting.set(slot?.getSlotElementId(), slotConfig);
+ },
+ slotDataAvailable() {
+ return window.googletag?.pubads().getSlots().length > 0;
+ },
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-1.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-1.js
new file mode 100644
index 0000000000..d95059cf7a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-1.js
@@ -0,0 +1,87 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser */
+
+if (!window.MochitestShimPromise) {
+ const originalUrl = document.currentScript.src;
+
+ const shimId = "MochitestShim";
+
+ const sendMessageToAddon = (function () {
+ const pendingMessages = new Map();
+ const channel = new MessageChannel();
+ channel.port1.onerror = console.error;
+ channel.port1.onmessage = event => {
+ const { messageId, response } = event.data;
+ const resolve = pendingMessages.get(messageId);
+ if (resolve) {
+ pendingMessages.delete(messageId);
+ resolve(response);
+ }
+ };
+ function reconnect() {
+ const detail = {
+ pendingMessages: [...pendingMessages.values()],
+ port: channel.port2,
+ shimId,
+ };
+ window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+ }
+ window.addEventListener("ShimHelperReady", reconnect);
+ reconnect();
+ return function (message) {
+ const messageId =
+ Math.random().toString(36).substring(2) + Date.now().toString(36);
+ return new Promise(resolve => {
+ const payload = {
+ message,
+ messageId,
+ shimId,
+ };
+ pendingMessages.set(messageId, resolve);
+ channel.port1.postMessage(payload);
+ });
+ };
+ })();
+
+ async function go(options) {
+ try {
+ const o = document.getElementById("shims");
+ const cl = o.classList;
+ cl.remove("red");
+ cl.add("green");
+ o.innerText = JSON.stringify(options || "");
+ } catch (_) {}
+
+ window.shimPromiseResolve("shimmed");
+
+ if (window !== top) {
+ window.optInPromiseResolve(false);
+ return;
+ }
+
+ await sendMessageToAddon("optIn");
+
+ window.doingOptIn = true;
+ const s = document.createElement("script");
+ s.src = originalUrl;
+ s.onerror = () => window.optInPromiseResolve("error");
+ document.head.appendChild(s);
+ }
+
+ window[`${shimId}Promise`] = new Promise(resolve => {
+ sendMessageToAddon("getOptions").then(options => {
+ if (document.readyState !== "loading") {
+ resolve(go(options));
+ } else {
+ window.addEventListener("DOMContentLoaded", () => {
+ resolve(go(options));
+ });
+ }
+ });
+ });
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-2.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-2.js
new file mode 100644
index 0000000000..bc5536405e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-2.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+"use strict";
+
+/* globals browser */
+
+if (!window.testPromise) {
+ const originalUrl = document.currentScript.src;
+
+ const shimId = "MochitestShim2";
+
+ const sendMessageToAddon = (function () {
+ const pendingMessages = new Map();
+ const channel = new MessageChannel();
+ channel.port1.onerror = console.error;
+ channel.port1.onmessage = event => {
+ const { messageId, response } = event.data;
+ const resolve = pendingMessages.get(messageId);
+ if (resolve) {
+ pendingMessages.delete(messageId);
+ resolve(response);
+ }
+ };
+ function reconnect() {
+ const detail = {
+ pendingMessages: [...pendingMessages.values()],
+ port: channel.port2,
+ shimId,
+ };
+ window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+ }
+ window.addEventListener("ShimHelperReady", reconnect);
+ reconnect();
+ return function (message) {
+ const messageId =
+ Math.random().toString(36).substring(2) + Date.now().toString(36);
+ return new Promise(resolve => {
+ const payload = {
+ message,
+ messageId,
+ shimId,
+ };
+ pendingMessages.set(messageId, resolve);
+ channel.port1.postMessage(payload);
+ });
+ };
+ })();
+
+ async function go(options) {
+ try {
+ const o = document.getElementById("shims");
+ const cl = o.classList;
+ cl.remove("red");
+ cl.add("green");
+ o.innerText = JSON.stringify(options || "");
+ } catch (_) {}
+
+ window.shimPromiseResolve("shimmed");
+
+ if (window !== top) {
+ window.optInPromiseResolve(false);
+ return;
+ }
+
+ await sendMessageToAddon("optIn");
+
+ window.doingOptIn = true;
+ const s = document.createElement("script");
+ s.src = originalUrl;
+ s.onerror = () => window.optInPromiseResolve("error");
+ document.head.appendChild(s);
+ }
+
+ sendMessageToAddon("getOptions").then(options => {
+ if (document.readyState !== "loading") {
+ go(options);
+ } else {
+ window.addEventListener("DOMContentLoaded", () => {
+ go(options);
+ });
+ }
+ });
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-3.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-3.js
new file mode 100644
index 0000000000..dc0a8005f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/mochitest-shim-3.js
@@ -0,0 +1,7 @@
+/* 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/. */
+
+"use strict";
+
+window.shimPromiseResolve("shimmed");
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/nielsen.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/nielsen.js
new file mode 100644
index 0000000000..d34528a7c1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/nielsen.js
@@ -0,0 +1,111 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1760754 - Shim Nielsen tracker
+ *
+ * Sites expecting the Nielsen tracker to load properly can break if it
+ * is blocked. This shim mitigates that breakage by loading a stand-in.
+ */
+
+if (!window.nol_t) {
+ const cid = "";
+
+ let domain = "";
+ let schemeHost = "";
+ let scriptName = "";
+ try {
+ const url = document?.currentScript?.src;
+ const { pathname, protocol, host } = new URL(url);
+ domain = host.split(".").slice(0, -2).join(".");
+ schemeHost = `${protocol}//${host}/`;
+ scriptName = pathname.split("/").pop();
+ } catch (_) {}
+
+ const NolTracker = class {
+ CONST = {
+ max_tags: 20,
+ };
+ feat = {};
+ globals = {
+ cid,
+ content: "0",
+ defaultApidFile: "config250",
+ defaultErrorParams: {
+ nol_vcid: "c00",
+ nol_clientid: "",
+ },
+ domain,
+ fpidSfCodeList: [""],
+ init() {},
+ tagCurrRetry: -1,
+ tagMaxRetry: 3,
+ wlCurrRetry: -1,
+ wlMaxRetry: 3,
+ };
+ pmap = [];
+ pvar = {
+ cid,
+ content: "0",
+ cookies_enabled: "n",
+ server: domain,
+ };
+ scriptName = [scriptName];
+ version = "6.0.107";
+
+ addScript() {}
+ catchLinkOverlay() {}
+ clickEvent() {}
+ clickTrack() {}
+ do_sample() {}
+ downloadEvent() {}
+ eventTrack() {}
+ filter() {}
+ fireToUrl() {}
+ getSchemeHost() {
+ return schemeHost;
+ }
+ getVersion() {}
+ iframe() {}
+ in_sample() {
+ return true;
+ }
+ injectBsdk() {}
+ invite() {}
+ linkTrack() {}
+ mergeFeatures() {}
+ pageEvent() {}
+ pause() {}
+ populateWhitelist() {}
+ post() {}
+ postClickTrack() {}
+ postData() {}
+ postEvent() {}
+ postEventTrack() {}
+ postLinkTrack() {}
+ prefix() {
+ return "";
+ }
+ processDdrsSvc() {}
+ random() {}
+ record() {
+ return this;
+ }
+ regLinkOverlay() {}
+ regListen() {}
+ retrieveCiFileViaCors() {}
+ sectionEvent() {}
+ sendALink() {}
+ sendForm() {}
+ sendIt() {}
+ slideEvent() {}
+ whitelistAssigned() {}
+ };
+
+ window.nol_t = () => {
+ return new NolTracker();
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/optimizely.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/optimizely.js
new file mode 100644
index 0000000000..dcda87421d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/optimizely.js
@@ -0,0 +1,205 @@
+/* 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/. */
+
+/**
+ * Bug 1714431 - Shim Optimizely
+ *
+ * This shim stubs out window.optimizely for those sites which
+ * break when it is not successfully loaded.
+ */
+
+if (!window.optimizely?.state) {
+ const behavior = {
+ query: () => [],
+ };
+
+ const dcp = {
+ getAttributeValue() {},
+ waitForAttributeValue: () => Promise.resolve(),
+ };
+
+ const data = {
+ accountId: "",
+ audiences: {},
+ campaigns: {},
+ clientName: "js",
+ clientVersion: "0.166.0",
+ dcpServiceId: null,
+ events: {},
+ experiments: {},
+ groups: {},
+ pages: {},
+ projectId: "",
+ revision: "",
+ snippetId: null,
+ variations: {},
+ };
+
+ const activationId = String(Date.now());
+
+ const state = {
+ getActivationId() {
+ return activationId;
+ },
+ getActiveExperimentIds() {
+ return [];
+ },
+ getCampaignStateLists() {
+ return {};
+ },
+ getCampaignStates() {
+ return {};
+ },
+ getDecisionObject() {
+ return null;
+ },
+ getDecisionString() {
+ return null;
+ },
+ getExperimentStates() {
+ return {};
+ },
+ getPageStates() {
+ return {};
+ },
+ getRedirectInfo() {
+ return null;
+ },
+ getVariationMap() {
+ return {};
+ },
+ isGlobalHoldback() {
+ return false;
+ },
+ };
+
+ const poll = (fn, to) => {
+ setInterval(() => {
+ try {
+ fn();
+ } catch (_) {}
+ }, to);
+ };
+
+ const waitUntil = test => {
+ let interval, resolve;
+ function check() {
+ try {
+ if (test()) {
+ clearInterval(interval);
+ resolve?.();
+ return true;
+ }
+ } catch (_) {}
+ return false;
+ }
+ return new Promise(r => {
+ resolve = r;
+ if (check()) {
+ resolve();
+ return;
+ }
+ interval = setInterval(check, 250);
+ });
+ };
+
+ const waitForElement = sel => {
+ return waitUntil(() => {
+ document.querySelector(sel);
+ });
+ };
+
+ const observeSelector = (sel, fn, opts) => {
+ let interval;
+ const observed = new Set();
+ function check() {
+ try {
+ for (const e of document.querySelectorAll(sel)) {
+ if (observed.has(e)) {
+ continue;
+ }
+ observed.add(e);
+ try {
+ fn(e);
+ } catch (_) {}
+ if (opts.once) {
+ clearInterval(interval);
+ }
+ }
+ } catch (_) {}
+ }
+ interval = setInterval(check, 250);
+ const timeout = { opts };
+ if (timeout) {
+ setTimeout(() => {
+ clearInterval(interval);
+ }, timeout);
+ }
+ };
+
+ const utils = {
+ Promise: window.Promise,
+ observeSelector,
+ poll,
+ waitForElement,
+ waitUntil,
+ };
+
+ const visitorId = {
+ randomId: "",
+ };
+
+ let browserVersion = "";
+ try {
+ browserVersion = navigator.userAgent.match(/rv:(.*)\)/)[1];
+ } catch (_) {}
+
+ const visitor = {
+ browserId: "ff",
+ browserVersion,
+ currentTimestamp: Date.now(),
+ custom: {},
+ customBehavior: {},
+ device: "desktop",
+ device_type: "desktop_laptop",
+ events: [],
+ first_session: true,
+ offset: 240,
+ referrer: null,
+ source_type: "direct",
+ visitorId,
+ };
+
+ window.optimizely = {
+ data: {
+ note: "Obsolete, use optimizely.get('data') instead",
+ },
+ get(e) {
+ switch (e) {
+ case "behavior":
+ return behavior;
+ case "data":
+ return data;
+ case "dcp":
+ return dcp;
+ case "jquery":
+ throw new Error("jQuery not implemented");
+ case "session":
+ return undefined;
+ case "state":
+ return state;
+ case "utils":
+ return utils;
+ case "visitor":
+ return visitor;
+ case "visitor_id":
+ return visitorId;
+ }
+ return undefined;
+ },
+ initialized: true,
+ push() {},
+ state: {},
+ };
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/play.svg b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/play.svg
new file mode 100644
index 0000000000..df5bbcb4f1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/play.svg
@@ -0,0 +1,7 @@
+<!-- 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/.
+ source: https://searchfox.org/mozilla-central/source/devtools/client/themes/images/play.svg -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+ <path fill="#fff" d="M20.436 11.37L5.904 2.116c-.23-.147-.523-.158-.762-.024-.24.132-.39.384-.39.657v18.5c0 .273.15.525.39.657.112.063.236.093.36.093.14 0 .28-.04.402-.117l14.53-9.248c.218-.138.35-.376.35-.633 0-.256-.132-.495-.348-.633z"/>
+</svg>
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/rambler-authenticator.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/rambler-authenticator.js
new file mode 100644
index 0000000000..1fe074b660
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/rambler-authenticator.js
@@ -0,0 +1,84 @@
+/* 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/. */
+
+"use strict";
+
+if (!window.ramblerIdHelper) {
+ const originalScript = document.currentScript.src;
+
+ const sendMessageToAddon = (function () {
+ const shimId = "Rambler";
+ const pendingMessages = new Map();
+ const channel = new MessageChannel();
+ channel.port1.onerror = console.error;
+ channel.port1.onmessage = event => {
+ const { messageId, response } = event.data;
+ const resolve = pendingMessages.get(messageId);
+ if (resolve) {
+ pendingMessages.delete(messageId);
+ resolve(response);
+ }
+ };
+ function reconnect() {
+ const detail = {
+ pendingMessages: [...pendingMessages.values()],
+ port: channel.port2,
+ shimId,
+ };
+ window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+ }
+ window.addEventListener("ShimHelperReady", reconnect);
+ reconnect();
+ return function (message) {
+ const messageId =
+ Math.random().toString(36).substring(2) + Date.now().toString(36);
+ return new Promise(resolve => {
+ const payload = {
+ message,
+ messageId,
+ shimId,
+ };
+ pendingMessages.set(messageId, resolve);
+ channel.port1.postMessage(payload);
+ });
+ };
+ })();
+
+ const ramblerIdHelper = {
+ getProfileInfo: (successCallback, errorCallback) => {
+ successCallback({});
+ },
+ openAuth: () => {
+ sendMessageToAddon("optIn").then(function () {
+ const openAuthArgs = arguments;
+ window.ramblerIdHelper = undefined;
+ const s = document.createElement("script");
+ s.src = originalScript;
+ document.head.appendChild(s);
+ s.addEventListener("load", () => {
+ const helper = window.ramblerIdHelper;
+ for (const { fn, args } of callLog) {
+ helper[fn].apply(helper, args);
+ }
+ helper.openAuth.apply(helper, openAuthArgs);
+ });
+ });
+ },
+ };
+
+ const callLog = [];
+ function addLoggedCall(fn) {
+ ramblerIdHelper[fn] = () => {
+ callLog.push({ fn, args: arguments });
+ };
+ }
+
+ addLoggedCall("registerOnFrameCloseCallback");
+ addLoggedCall("registerOnFrameRedirect");
+ addLoggedCall("registerOnPossibleLoginCallback");
+ addLoggedCall("registerOnPossibleLogoutCallback");
+ addLoggedCall("registerOnPossibleOauthLoginCallback");
+
+ window.ramblerIdHelper = ramblerIdHelper;
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/rich-relevance.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/rich-relevance.js
new file mode 100644
index 0000000000..aea85c030a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/rich-relevance.js
@@ -0,0 +1,288 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713725 - Shim Rich Relevance personalized shopping
+ *
+ * Sites may expect the Rich Relevance personalized shopping API to load,
+ * breaking if it is blocked. This shim attempts to limit breakage on those
+ * site to just the personalized shopping aspects, by stubbing out the APIs.
+ */
+
+if (!window.r3_common) {
+ const jsonCallback = window.RR?.jsonCallback;
+ const defaultCallback = window.RR?.defaultCallback;
+
+ const getRandomString = (l = 66) => {
+ const v = crypto.getRandomValues(new Uint8Array(l));
+ const s = Array.from(v, c => c.toString(16)).join("");
+ return s.slice(0, l);
+ };
+
+ const call = (fn, ...args) => {
+ if (typeof fn === "function") {
+ try {
+ fn(...args);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ };
+
+ class r3_generic {
+ type = "GENERIC";
+ createScript() {}
+ destroy() {}
+ }
+
+ class r3_addtocart extends r3_generic {
+ type = "ADDTOCART";
+ addItemIdToCart() {}
+ }
+
+ class r3_addtoregistry extends r3_generic {
+ type = "ADDTOREGISTRY";
+ addItemIdCentsQuantity() {}
+ }
+
+ class r3_brand extends r3_generic {
+ type = "BRAND";
+ }
+
+ class r3_cart extends r3_generic {
+ type = "CART";
+ addItemId() {}
+ addItemIdCentsQuantity() {}
+ addItemIdDollarsAndCentsQuantity() {}
+ addItemIdPriceQuantity() {}
+ }
+
+ class r3_category extends r3_generic {
+ type = "CATEGORY";
+ addItemId() {}
+ setId() {}
+ setName() {}
+ setParentId() {}
+ setTopName() {}
+ }
+
+ class r3_common extends r3_generic {
+ type = "COMMON";
+ baseUrl = "https://recs.richrelevance.com/rrserver/";
+ devFlags = {};
+ jsFileName = "p13n_generated.js";
+ RICHSORT = {
+ paginate() {},
+ filterPrice() {},
+ filterAttribute() {},
+ };
+ addCategoryHintId() {}
+ addClickthruParams() {}
+ addContext() {}
+ addFilter() {}
+ addFilterBrand() {}
+ addFilterCategory() {}
+ addItemId() {}
+ addItemIdToCart() {}
+ addPlacementType() {}
+ addRefinement() {}
+ addSearchTerm() {}
+ addSegment() {}
+ blockItemId() {}
+ enableCfrad() {}
+ enableRad() {}
+ forceDebugMode() {}
+ forceDevMode() {}
+ forceDisplayMode() {}
+ forceLocale() {}
+ initFromParams() {}
+ setApiKey() {}
+ setBaseUrl() {}
+ setCartValue() {}
+ setChannel() {}
+ setClickthruServer() {}
+ setCurrency() {}
+ setDeviceId() {}
+ setFilterBrandsIncludeMatchingElements() {}
+ setForcedTreatment() {}
+ setImageServer() {}
+ setLanguage() {}
+ setMVTForcedTreatment() {}
+ setNoCookieMode() {}
+ setPageBrand() {}
+ setPrivateMode() {}
+ setRefinementFallback() {}
+ setRegionId() {}
+ setRegistryId() {}
+ setRegistryType() {}
+ setSessionId() {}
+ setUserId() {}
+ useDummyData() {}
+ }
+
+ class r3_error extends r3_generic {
+ type = "ERROR";
+ }
+
+ class r3_home extends r3_generic {
+ type = "HOME";
+ }
+
+ class r3_item extends r3_generic {
+ type = "ITEM";
+ addAttribute() {}
+ addCategory() {}
+ addCategoryId() {}
+ setBrand() {}
+ setEndDate() {}
+ setId() {}
+ setImageId() {}
+ setLinkId() {}
+ setName() {}
+ setPrice() {}
+ setRating() {}
+ setRecommendable() {}
+ setReleaseDate() {}
+ setSalePrice() {}
+ }
+
+ class r3_personal extends r3_generic {
+ type = "PERSONAL";
+ }
+
+ class r3_purchased extends r3_generic {
+ type = "PURCHASED";
+ addItemId() {}
+ addItemIdCentsQuantity() {}
+ addItemIdDollarsAndCentsQuantity() {}
+ addItemIdPriceQuantity() {}
+ setOrderNumber() {}
+ setPromotionCode() {}
+ setShippingCost() {}
+ setTaxes() {}
+ setTotalPrice() {}
+ }
+
+ class r3_search extends r3_generic {
+ type = "SEARCH";
+ addItemId() {}
+ setTerms() {}
+ }
+
+ class r3_wishlist extends r3_generic {
+ type = "WISHLIST";
+ addItemId() {}
+ }
+
+ const RR = {
+ add() {},
+ addItemId() {},
+ addItemIdCentsQuantity() {},
+ addItemIdDollarsAndCentsQuantity() {},
+ addItemIdPriceQuantity() {},
+ addItemIdToCart() {},
+ addObject() {},
+ addSearchTerm() {},
+ c() {},
+ charset: "UTF-8",
+ checkParamCookieValue: () => null,
+ d: document,
+ data: {
+ JSON: {
+ placements: [],
+ },
+ },
+ debugWindow() {},
+ set defaultCallback(fn) {
+ call(fn);
+ },
+ fixName: n => n,
+ genericAddItemPriceQuantity() {},
+ get() {},
+ getDomElement(a) {
+ return typeof a === "string" && a ? document.querySelector(a) : null;
+ },
+ id() {},
+ insert() {},
+ insertDynamicPlacement() {},
+ isArray: a => Array.isArray(a),
+ js() {},
+ set jsonCallback(fn) {
+ call(fn, {});
+ },
+ l: document.location.href,
+ lc() {},
+ noCookieMode: false,
+ ol() {},
+ onloadCalled: true,
+ pq() {},
+ rcsCookieDefaultDuration: 364,
+ registerPageType() {},
+ registeredPageTypes: {
+ ADDTOCART: r3_addtocart,
+ ADDTOREGISTRY: r3_addtoregistry,
+ BRAND: r3_brand,
+ CART: r3_cart,
+ CATEGORY: r3_category,
+ COMMON: r3_common,
+ ERROR: r3_error,
+ GENERIC: r3_generic,
+ HOME: r3_home,
+ ITEM: r3_item,
+ PERSONAL: r3_personal,
+ PURCHASED: r3_purchased,
+ SEARCH: r3_search,
+ WISHLIST: r3_wishlist,
+ },
+ renderDynamicPlacements() {},
+ set() {},
+ setCharset() {},
+ U: "undefined",
+ unregisterAllPageType() {},
+ unregisterPageType() {},
+ };
+
+ Object.assign(window, {
+ r3() {},
+ r3_addtocart,
+ r3_addtoregistry,
+ r3_brand,
+ r3_cart,
+ r3_category,
+ r3_common,
+ r3_error,
+ r3_generic,
+ r3_home,
+ r3_item,
+ r3_personal,
+ r3_placement() {},
+ r3_purchased,
+ r3_search,
+ r3_wishlist,
+ RR,
+ rr_addLoadEvent() {},
+ rr_annotations_array: [undefined],
+ rr_call_after_flush() {},
+ rr_create_script() {},
+ rr_dynamic: {
+ placements: [],
+ },
+ rr_flush() {},
+ rr_flush_onload() {},
+ rr_insert_placement() {},
+ rr_onload_called: true,
+ rr_placement_place_holders: [],
+ rr_placements: [],
+ rr_recs: {
+ placements: [],
+ },
+ rr_remote_data: getRandomString(),
+ rr_v: "1.2.6.20210212",
+ });
+
+ call(jsonCallback);
+ call(defaultCallback, {});
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/salesforce.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/salesforce.js
new file mode 100644
index 0000000000..b51faa5f93
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/salesforce.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1855139 - The pop-up for "Où trouver ma référence ?" option is blank at
+ * garantie30minutes.sncf.com with ETP set to STANDARD
+ *
+ * emeraude.my.salesforce.com is marked as a tracker, and it tries to access localstorage, but the
+ * script returned does not handle the error. The shim replaces localstorage with a fake
+ * interface to avoid the error.
+ *
+ */
+
+/* globals cloneInto */
+
+(function () {
+ const win = window.wrappedJSObject;
+
+ try {
+ // We only replace the indexedDB when emeraude.my.salesforce.com is loaded in a
+ // third-party context. Note that this is not strictly correct because
+ // this is a cross-origin check but not a third-party check.
+ if (win.parent == win || win.location.origin == win.top.location.origin) {
+ return;
+ }
+ } catch (e) {
+ // If we get a security error when accessing the top-level origin, this
+ // shows that the window is in a cross-origin context. In this case, we can
+ // proceed to apply the shim.
+ if (e.name != "SecurityError") {
+ throw e;
+ }
+ }
+
+ const emptyMsg = cloneInto({ message: "" }, window);
+
+ const idb = {
+ open: () => win.Promise.reject(emptyMsg),
+ };
+
+ Object.defineProperty(win, "indexedDB", {
+ value: cloneInto(idb, window, { cloneFunctions: true }),
+ });
+})();
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/spotify-embed.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/spotify-embed.js
new file mode 100644
index 0000000000..62ad05b725
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/spotify-embed.js
@@ -0,0 +1,133 @@
+/* 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/. */
+
+/* globals exportFunction */
+
+"use strict";
+
+/**
+ * Spotify embeds default to "track preview mode". They require first-party
+ * storage access in order to detect the login status and allow the user to play
+ * the whole song or add it to their library.
+ * Upon clicking the "play" button in the preview view this shim attempts to get
+ * storage access and on success, reloads the frame and plays the full track.
+ * This only works if the user is already logged in to Spotify in the
+ * first-party context.
+ */
+
+const AUTOPLAY_FLAG = "shimPlayAfterStorageAccess";
+const SELECTOR_PREVIEW_PLAY = 'div[data-testid="preview-play-pause"] > button';
+const SELECTOR_FULL_PLAY = 'button[data-testid="play-pause-button"]';
+
+/**
+ * Promise-wrapper around DOMContentLoaded event.
+ */
+function waitForDOMContentLoaded() {
+ return new Promise(resolve => {
+ window.addEventListener("DOMContentLoaded", resolve, { once: true });
+ });
+}
+
+/**
+ * Listener for the preview playback button which requests storage access and
+ * reloads the page.
+ */
+function previewPlayButtonListener(event) {
+ const { target, isTrusted } = event;
+ if (!isTrusted) {
+ return;
+ }
+
+ const button = target.closest("button");
+ if (!button) {
+ return;
+ }
+
+ // Filter for the preview playback button. This won't match the full
+ // playback button that is shown when the user is logged in.
+ if (!button.matches(SELECTOR_PREVIEW_PLAY)) {
+ return;
+ }
+
+ // The storage access request below runs async so playback won't start
+ // immediately. Mitigate this UX issue by updating the clicked element's
+ // style so the user gets some immediate feedback.
+ button.style.opacity = 0.5;
+ event.stopPropagation();
+ event.preventDefault();
+
+ console.debug("Requesting storage access.", location.origin);
+ document
+ .requestStorageAccess()
+ // When storage access is granted, reload the frame for the embedded
+ // player to detect the login state and give us full playback
+ // capabilities.
+ .then(() => {
+ // Use a flag to indicate that we want to click play after reload.
+ // This is so the user does not have to click play twice.
+ sessionStorage.setItem(AUTOPLAY_FLAG, "true");
+ console.debug("Reloading after storage access grant.");
+ location.reload();
+ })
+ // If the user denies the storage access prompt we can't use the login
+ // state. Attempt start preview playback instead.
+ .catch(() => {
+ button.click();
+ })
+ // Reset button style for both success and error case.
+ .finally(() => {
+ button.style.opacity = 1.0;
+ });
+}
+
+/**
+ * Attempt to start (full) playback. Waits for the play button to appear and
+ * become ready.
+ */
+async function startFullPlayback() {
+ // Wait for DOMContentLoaded before looking for the playback button.
+ await waitForDOMContentLoaded();
+
+ let numTries = 0;
+ let intervalId = setInterval(() => {
+ try {
+ document.querySelector(SELECTOR_FULL_PLAY).click();
+ clearInterval(intervalId);
+ console.debug("Clicked play after storage access grant.");
+ } catch (e) {}
+ numTries++;
+
+ if (numTries >= 50) {
+ console.debug("Can not start playback. Giving up.");
+ clearInterval(intervalId);
+ }
+ }, 200);
+}
+
+(async () => {
+ // Only run the shim for embedded iframes.
+ if (window.top == window) {
+ return;
+ }
+
+ console.warn(
+ `When using the Spotify embedded player, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1792395 for details.`
+ );
+
+ // Already requested storage access before the reload, trigger playback.
+ if (sessionStorage.getItem(AUTOPLAY_FLAG) == "true") {
+ sessionStorage.removeItem(AUTOPLAY_FLAG);
+
+ await startFullPlayback();
+ return;
+ }
+
+ // Wait for the user to click the preview play button. If the player has
+ // already loaded the full version, this method will do nothing.
+ document.documentElement.addEventListener(
+ "click",
+ previewPlayButtonListener,
+ { capture: true }
+ );
+})();
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/tracking-pixel.png b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/tracking-pixel.png
new file mode 100644
index 0000000000..52c591798e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/tracking-pixel.png
Binary files differ
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/tsn-ca.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/tsn-ca.js
new file mode 100644
index 0000000000..ee8e96b661
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/tsn-ca.js
@@ -0,0 +1,57 @@
+/* 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/. */
+
+"use strict";
+
+/*
+ * Bug 1802340 - tsn.ca login broken with dFPI enabled
+ *
+ * tsn.ca relies upon a login page that is out-of-origin. That login page
+ * sets a cookie for https://www.tsn.ca, which is then used as an proof of
+ * authentication on redirect back to the main site. This shim adds a request
+ * for storage access for https://www.tsn.ca when the user tries to log in.
+ */
+
+console.warn(
+ `When logging in, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1802340 for details.`
+);
+
+// Third-party origin we need to request storage access for.
+const STORAGE_ACCESS_ORIGIN = "https://www.tsn.ca";
+
+document.documentElement.addEventListener(
+ "click",
+ e => {
+ const { target, isTrusted } = e;
+ if (!isTrusted) {
+ return;
+ }
+
+ const button = target.closest("button");
+ if (!button) {
+ return;
+ }
+ const form = target.closest(".login-form");
+ if (!form) {
+ return;
+ }
+
+ console.warn(
+ "Calling the Storage Access API on behalf of " + STORAGE_ACCESS_ORIGIN
+ );
+ button.disabled = true;
+ e.stopPropagation();
+ e.preventDefault();
+ document
+ .requestStorageAccessForOrigin(STORAGE_ACCESS_ORIGIN)
+ .then(() => {
+ button.disabled = false;
+ target.click();
+ })
+ .catch(() => {
+ button.disabled = false;
+ });
+ },
+ true
+);
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vast2.xml b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vast2.xml
new file mode 100644
index 0000000000..3536ccfc0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vast2.xml
@@ -0,0 +1,12 @@
+<?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/.
+
+ Bug 1713693 - Shim Doubleclick
+
+ Some sites rely on an XML VAST Ad response from Doubleclick, or will
+ break (showing black boxes instead of videos, etc). This shim mitigates
+ such breakage.
+-->
+<VAST version="2.0"></VAST>
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vast3.xml b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vast3.xml
new file mode 100644
index 0000000000..ae03f0dc14
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vast3.xml
@@ -0,0 +1,12 @@
+<?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/.
+
+ Bug 1713693 - Shim Doubleclick
+
+ Some sites rely on an XML VAST Ad response from Doubleclick, or will
+ break (showing black boxes instead of videos, etc). This shim mitigates
+ such breakage.
+-->
+<VAST version="3.0"></VAST>
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vidible.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vidible.js
new file mode 100644
index 0000000000..1d45bc0f7e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vidible.js
@@ -0,0 +1,424 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1713710 - Shim Vidible video player
+ *
+ * Sites relying on Vidible's video player may experience broken videos if that
+ * script is blocked. This shim allows users to opt into viewing those videos
+ * regardless of any tracking consequences, by providing placeholders for each.
+ */
+
+if (!window.vidible?.version) {
+ const PlayIconURL = "https://smartblock.firefox.etp/play.svg";
+
+ const originalScript = document.currentScript.src;
+
+ const getGUID = () => {
+ const v = crypto.getRandomValues(new Uint8Array(20));
+ return Array.from(v, c => c.toString(16)).join("");
+ };
+
+ const sendMessageToAddon = (function () {
+ const shimId = "Vidible";
+ const pendingMessages = new Map();
+ const channel = new MessageChannel();
+ channel.port1.onerror = console.error;
+ channel.port1.onmessage = event => {
+ const { messageId, response } = event.data;
+ const resolve = pendingMessages.get(messageId);
+ if (resolve) {
+ pendingMessages.delete(messageId);
+ resolve(response);
+ }
+ };
+ function reconnect() {
+ const detail = {
+ pendingMessages: [...pendingMessages.values()],
+ port: channel.port2,
+ shimId,
+ };
+ window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+ }
+ window.addEventListener("ShimHelperReady", reconnect);
+ reconnect();
+ return function (message) {
+ const messageId = getGUID();
+ return new Promise(resolve => {
+ const payload = { message, messageId, shimId };
+ pendingMessages.set(messageId, resolve);
+ channel.port1.postMessage(payload);
+ });
+ };
+ })();
+
+ const Shimmer = (function () {
+ // If a page might store references to an object before we replace it,
+ // ensure that it only receives proxies to that object created by
+ // `Shimmer.proxy(obj)`. Later when the unshimmed object is created,
+ // call `Shimmer.unshim(proxy, unshimmed)`. This way the references
+ // will automatically "become" the unshimmed object when appropriate.
+
+ const shimmedObjects = new WeakMap();
+ const unshimmedObjects = new Map();
+
+ function proxy(shim) {
+ if (shimmedObjects.has(shim)) {
+ return shimmedObjects.get(shim);
+ }
+
+ const prox = new Proxy(shim, {
+ get: (target, k) => {
+ if (unshimmedObjects.has(prox)) {
+ return unshimmedObjects.get(prox)[k];
+ }
+ return target[k];
+ },
+ apply: (target, thisArg, args) => {
+ if (unshimmedObjects.has(prox)) {
+ return unshimmedObjects.get(prox)(...args);
+ }
+ return target.apply(thisArg, args);
+ },
+ construct: (target, args) => {
+ if (unshimmedObjects.has(prox)) {
+ return new unshimmedObjects.get(prox)(...args);
+ }
+ return new target(...args);
+ },
+ });
+ shimmedObjects.set(shim, prox);
+ shimmedObjects.set(prox, prox);
+
+ for (const key in shim) {
+ const value = shim[key];
+ if (typeof value === "function") {
+ shim[key] = function () {
+ const unshimmed = unshimmedObjects.get(prox);
+ if (unshimmed) {
+ return unshimmed[key].apply(unshimmed, arguments);
+ }
+ return value.apply(this, arguments);
+ };
+ } else if (typeof value !== "object" || value === null) {
+ shim[key] = value;
+ } else {
+ shim[key] = Shimmer.proxy(value);
+ }
+ }
+
+ return prox;
+ }
+
+ function unshim(shim, unshimmed) {
+ unshimmedObjects.set(shim, unshimmed);
+
+ for (const prop in shim) {
+ if (prop in unshimmed) {
+ const un = unshimmed[prop];
+ if (typeof un === "object" && un !== null) {
+ unshim(shim[prop], un);
+ }
+ } else {
+ unshimmedObjects.set(shim[prop], undefined);
+ }
+ }
+ }
+
+ return { proxy, unshim };
+ })();
+
+ const extras = [];
+ const playersByNode = new WeakMap();
+ const playerData = new Map();
+
+ const getJSONPVideoPlacements = () => {
+ return document.querySelectorAll(
+ `script[src*="delivery.vidible.tv/jsonp"]`
+ );
+ };
+
+ const allowVidible = () => {
+ if (allowVidible.promise) {
+ return allowVidible.promise;
+ }
+
+ const shim = window.vidible;
+ window.vidible = undefined;
+
+ allowVidible.promise = sendMessageToAddon("optIn")
+ .then(() => {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement("script");
+ script.src = originalScript;
+ script.addEventListener("load", () => {
+ Shimmer.unshim(shim, window.vidible);
+
+ for (const args of extras) {
+ window.visible.registerExtra(...args);
+ }
+
+ for (const jsonp of getJSONPVideoPlacements()) {
+ const { src } = jsonp;
+ const jscript = document.createElement("script");
+ jscript.onload = resolve;
+ jscript.src = src;
+ jsonp.replaceWith(jscript);
+ }
+
+ for (const [playerShim, data] of playerData.entries()) {
+ const { loadCalled, on, parent, placeholder, setup } = data;
+
+ placeholder?.remove();
+
+ const player = window.vidible.player(parent);
+ Shimmer.unshim(playerShim, player);
+
+ for (const [type, fns] of on.entries()) {
+ for (const fn of fns) {
+ try {
+ player.on(type, fn);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+
+ if (setup) {
+ player.setup(setup);
+ }
+
+ if (loadCalled) {
+ player.load();
+ }
+ }
+
+ resolve();
+ });
+
+ script.addEventListener("error", () => {
+ script.remove();
+ reject();
+ });
+
+ document.head.appendChild(script);
+ });
+ })
+ .catch(() => {
+ window.vidible = shim;
+ delete allowVidible.promise;
+ });
+
+ return allowVidible.promise;
+ };
+
+ const createVideoPlaceholder = (service, callback) => {
+ const placeholder = document.createElement("div");
+ placeholder.style = `
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ min-width: 160px;
+ min-height: 100px;
+ top: 0px;
+ left: 0px;
+ background: #000;
+ color: #fff;
+ text-align: center;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-image: url(${PlayIconURL});
+ background-position: 50% 47.5%;
+ background-repeat: no-repeat;
+ background-size: 25% 25%;
+ -moz-text-size-adjust: none;
+ -moz-user-select: none;
+ color: #fff;
+ align-items: center;
+ padding-top: 200px;
+ font-size: 14pt;
+ `;
+ placeholder.textContent = `Click to allow blocked ${service} content`;
+ placeholder.addEventListener("click", evt => {
+ evt.isTrusted && callback();
+ });
+ return placeholder;
+ };
+
+ const Player = function (parent) {
+ const existing = playersByNode.get(parent);
+ if (existing) {
+ return existing;
+ }
+
+ const player = Shimmer.proxy(this);
+ playersByNode.set(parent, player);
+
+ const placeholder = createVideoPlaceholder("Vidible", allowVidible);
+ parent.parentNode.insertBefore(placeholder, parent);
+
+ playerData.set(player, {
+ on: new Map(),
+ parent,
+ placeholder,
+ });
+ return player;
+ };
+
+ const changeData = function (fn) {
+ const data = playerData.get(this);
+ if (data) {
+ fn(data);
+ playerData.set(this, data);
+ }
+ };
+
+ Player.prototype = {
+ addEventListener() {},
+ destroy() {
+ const { placeholder } = playerData.get(this);
+ placeholder?.remove();
+ playerData.delete(this);
+ },
+ dispatchEvent() {},
+ getAdsPassedTime() {},
+ getAllMacros() {},
+ getCurrentTime() {},
+ getDuration() {},
+ getHeight() {},
+ getPixelsLog() {},
+ getPlayerContainer() {},
+ getPlayerInfo() {},
+ getPlayerStatus() {},
+ getRequestsLog() {},
+ getStripUrl() {},
+ getVolume() {},
+ getWidth() {},
+ hidePlayReplayControls() {},
+ isMuted() {},
+ isPlaying() {},
+ load() {
+ changeData(data => (data.loadCalled = true));
+ },
+ mute() {},
+ on(type, fn) {
+ changeData(({ on }) => {
+ if (!on.has(type)) {
+ on.set(type, new Set());
+ }
+ on.get(type).add(fn);
+ });
+ },
+ off(type, fn) {
+ changeData(({ on }) => {
+ on.get(type)?.delete(fn);
+ });
+ },
+ overrideMacro() {},
+ pause() {},
+ play() {},
+ playVideoByIndex() {},
+ removeEventListener() {},
+ seekTo() {},
+ sendBirthDate() {},
+ sendKey() {},
+ setup(s) {
+ changeData(data => (data.setup = s));
+ return this;
+ },
+ setVideosToPlay() {},
+ setVolume() {},
+ showPlayReplayControls() {},
+ toggleFullscreen() {},
+ toggleMute() {},
+ togglePlay() {},
+ updateBid() {},
+ version() {},
+ volume() {},
+ };
+
+ const vidible = {
+ ADVERT_CLOSED: "advertClosed",
+ AD_END: "adend",
+ AD_META: "admeta",
+ AD_PAUSED: "adpaused",
+ AD_PLAY: "adplay",
+ AD_START: "adstart",
+ AD_TIMEUPDATE: "adtimeupdate",
+ AD_WAITING: "adwaiting",
+ AGE_GATE_DISPLAYED: "agegatedisplayed",
+ BID_UPDATED: "BidUpdated",
+ CAROUSEL_CLICK: "CarouselClick",
+ CONTEXT_ENDED: "contextended",
+ CONTEXT_STARTED: "contextstarted",
+ ENTER_FULLSCREEN: "playerenterfullscreen",
+ EXIT_FULLSCREEN: "playerexitfullscreen",
+ FALLBACK: "fallback",
+ FLOAT_END_ACTION: "floatended",
+ FLOAT_START_ACTION: "floatstarted",
+ HIDE_PLAY_REPLAY_BUTTON: "hideplayreplaybutton",
+ LIGHTBOX_ACTIVATED: "lightboxactivated",
+ LIGHTBOX_DEACTIVATED: "lightboxdeactivated",
+ MUTE: "Mute",
+ PLAYER_CONTROLS_STATE_CHANGE: "playercontrolsstatechaned",
+ PLAYER_DOCKED: "playerDocked",
+ PLAYER_ERROR: "playererror",
+ PLAYER_FLOATING: "playerFloating",
+ PLAYER_READY: "playerready",
+ PLAYER_RESIZE: "playerresize",
+ PLAYLIST_END: "playlistend",
+ SEEK_END: "SeekEnd",
+ SEEK_START: "SeekStart",
+ SHARE_SCREEN_CLOSED: "sharescreenclosed",
+ SHARE_SCREEN_OPENED: "sharescreenopened",
+ SHOW_PLAY_REPLAY_BUTTON: "showplayreplaybutton",
+ SUBTITLES_DISABLED: "subtitlesdisabled",
+ SUBTITLES_ENABLED: "subtitlesenabled",
+ SUBTITLES_READY: "subtitlesready",
+ UNMUTE: "Unmute",
+ VIDEO_DATA_LOADED: "videodataloaded",
+ VIDEO_END: "videoend",
+ VIDEO_META: "videometadata",
+ VIDEO_MODULE_CREATED: "videomodulecreated",
+ VIDEO_PAUSE: "videopause",
+ VIDEO_PLAY: "videoplay",
+ VIDEO_SEEKEND: "videoseekend",
+ VIDEO_SELECTED: "videoselected",
+ VIDEO_START: "videostart",
+ VIDEO_TIMEUPDATE: "videotimeupdate",
+ VIDEO_VOLUME_CHANGED: "videovolumechanged",
+ VOLUME: "Volume",
+ _getContexts: () => [],
+ "content.CLICK": "content.click",
+ "content.IMPRESSION": "content.impression",
+ "content.QUARTILE": "content.quartile",
+ "content.VIEW": "content.view",
+ createPlayer: parent => new Player(parent),
+ createPlayerAsync: parent => new Player(parent),
+ createVPAIDPlayer: parent => new Player(parent),
+ destroyAll() {},
+ extension() {},
+ getContext() {},
+ player: parent => new Player(parent),
+ playerInceptionTime() {
+ return { undefined: 1620149827713 };
+ },
+ registerExtra(a, b, c) {
+ extras.push([a, b, c]);
+ },
+ version: () => "21.1.313",
+ };
+
+ window.vidible = Shimmer.proxy(vidible);
+
+ for (const jsonp of getJSONPVideoPlacements()) {
+ const player = new Player(jsonp);
+ const { placeholder } = playerData.get(player);
+ jsonp.parentNode.insertBefore(placeholder, jsonp);
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vmad.xml b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vmad.xml
new file mode 100644
index 0000000000..5bb9a5a5d5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/vmad.xml
@@ -0,0 +1,12 @@
+<?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/.
+
+ Bug 1713693 - Shim Doubleclick
+
+ Some sites rely on an XML VMAD Ad response from Doubleclick, or will
+ break (showing black boxes instead of videos, etc). This shim mitigates
+ such breakage.
+-->
+<vmap:AdBreak></vmap:AdBreak>
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/webtrends.js b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/webtrends.js
new file mode 100644
index 0000000000..c7ef0069da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/webtrends.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1766414 - Shim WebTrends Core Tag and Advanced Link Tracking
+ *
+ * Sites using WebTrends Core Tag or Link Tracking can break if they are
+ * are blocked. This shim mitigates that breakage by loading an empty module.
+ */
+
+if (!window.dcsMultiTrack) {
+ window.dcsMultiTrack = o => {
+ o?.callback?.({});
+ };
+}
+
+if (!window.WebTrends) {
+ class dcs {
+ addSelector() {
+ return this;
+ }
+ addTransform() {
+ return this;
+ }
+ DCSext = {};
+ init(obj) {
+ return this;
+ }
+ track() {
+ return this;
+ }
+ }
+
+ window.Webtrends = window.WebTrends = {
+ dcs,
+ multiTrack: window.dcsMultiTrack,
+ };
+
+ window.requestAnimationFrame(() => {
+ window.webtrendsAsyncLoad?.(dcs);
+ window.webtrendsAsyncInit?.();
+ });
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/main/java/mozilla/components/feature/webcompat/WebCompatFeature.kt b/mobile/android/android-components/components/feature/webcompat/src/main/java/mozilla/components/feature/webcompat/WebCompatFeature.kt
new file mode 100644
index 0000000000..3ac6afeaf1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/main/java/mozilla/components/feature/webcompat/WebCompatFeature.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.webcompat
+
+import mozilla.components.concept.engine.webextension.WebExtensionRuntime
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Feature to enable website-hotfixing via the Web Compatibility System-Addon.
+ */
+object WebCompatFeature {
+ private val logger = Logger("mozac-webcompat")
+
+ internal const val WEBCOMPAT_EXTENSION_ID = "webcompat@mozilla.org"
+ internal const val WEBCOMPAT_EXTENSION_URL = "resource://android/assets/extensions/webcompat/"
+
+ /**
+ * Installs the web extension in the runtime through the WebExtensionRuntime install method
+ */
+ fun install(runtime: WebExtensionRuntime) {
+ runtime.installBuiltInWebExtension(
+ WEBCOMPAT_EXTENSION_ID,
+ WEBCOMPAT_EXTENSION_URL,
+ onSuccess = {
+ logger.debug("Installed WebCompat webextension: ${it.id}")
+ },
+ onError = { throwable ->
+ logger.error("Failed to install WebCompat webextension", throwable)
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/test/java/mozilla/components/feature/webcompat/WebCompatFeatureTest.kt b/mobile/android/android-components/components/feature/webcompat/src/test/java/mozilla/components/feature/webcompat/WebCompatFeatureTest.kt
new file mode 100644
index 0000000000..b00c8a7f9c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/test/java/mozilla/components/feature/webcompat/WebCompatFeatureTest.kt
@@ -0,0 +1,42 @@
+/* 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.webcompat
+
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.webextensions.WebExtensionController
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class WebCompatFeatureTest {
+
+ @Before
+ fun setup() {
+ WebExtensionController.installedExtensions.clear()
+ }
+
+ @Test
+ fun `installs the webextension`() {
+ val engine: Engine = mock()
+
+ val webcompatFeature = spy(WebCompatFeature)
+ webcompatFeature.install(engine)
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(WebCompatFeature.WEBCOMPAT_EXTENSION_ID),
+ eq(WebCompatFeature.WEBCOMPAT_EXTENSION_URL),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webcompat/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/webcompat/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/webcompat/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/webcompat/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/webcompat/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webcompat/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/feature/webnotifications/README.md b/mobile/android/android-components/components/feature/webnotifications/README.md
new file mode 100644
index 0000000000..1bc66d5ae2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Web Notifications
+
+A component for displaying web notifications.
+
+## Usage
+
+### 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-webnotifications:{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/webnotifications/build.gradle b/mobile/android/android-components/components/feature/webnotifications/build.gradle
new file mode 100644
index 0000000000..c9a1abf2c6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/build.gradle
@@ -0,0 +1,46 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.webnotifications'
+}
+
+dependencies {
+ implementation project(':browser-icons')
+ implementation project(':concept-engine')
+ implementation project(':feature-sitepermissions')
+ implementation project(':feature-intent')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ 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/webnotifications/proguard-rules.pro b/mobile/android/android-components/components/feature/webnotifications/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/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/webnotifications/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/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/webnotifications/src/main/java/mozilla/components/feature/webnotifications/NativeNotificationBridge.kt b/mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/NativeNotificationBridge.kt
new file mode 100644
index 0000000000..f195bb2859
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/NativeNotificationBridge.kt
@@ -0,0 +1,107 @@
+/* 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.webnotifications
+
+import android.app.Activity
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.annotation.DrawableRes
+import androidx.core.app.NotificationCompat
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.Icon.Source
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.IconRequest.Size
+import mozilla.components.concept.engine.webnotifications.WebNotification
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+import mozilla.components.support.utils.PendingIntentUtils
+
+internal class NativeNotificationBridge(
+ private val icons: BrowserIcons,
+ @DrawableRes private val smallIcon: Int,
+) {
+ companion object {
+ internal const val EXTRA_ON_CLICK = "mozac.feature.webnotifications.generic.onclick"
+ }
+
+ /**
+ * Create a system [Notification] from this [WebNotification].
+ */
+ suspend fun convertToAndroidNotification(
+ notification: WebNotification,
+ context: Context,
+ channelId: String,
+ activityClass: Class<out Activity>?,
+ requestId: Int,
+ ): Notification {
+ val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationCompat.Builder(context, channelId)
+ } else {
+ @Suppress("Deprecation")
+ NotificationCompat.Builder(context)
+ }
+
+ with(notification) {
+ activityClass?.let {
+ val intent = Intent(context, activityClass).apply {
+ putExtra(EXTRA_ON_CLICK, notification.engineNotification)
+ }
+
+ PendingIntent.getActivity(context, requestId, intent, PendingIntentUtils.defaultFlags).apply {
+ builder.setContentIntent(this)
+ }
+ }
+
+ builder.setSmallIcon(smallIcon)
+ .setContentTitle(title)
+ .setShowWhen(true)
+ .setWhen(timestamp)
+ .setAutoCancel(true)
+ .setSilent(notification.silent)
+
+ sourceUrl?.let {
+ builder.setSubText(it.tryGetHostFromUrl())
+ }
+
+ body?.let {
+ builder.setContentText(body)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(body))
+ }
+
+ loadIcon(sourceUrl, iconUrl, Size.DEFAULT, true)?.let { iconBitmap ->
+ builder.setLargeIcon(iconBitmap)
+ }
+ }
+
+ return builder.build()
+ }
+
+ /**
+ * Load an icon for a notification.
+ */
+ private suspend fun loadIcon(url: String?, iconUrl: String?, size: Size, isPrivate: Boolean): Bitmap? {
+ url ?: return null
+ iconUrl ?: return null
+ val icon = icons.loadIcon(
+ IconRequest(
+ url = url,
+ size = size,
+ resources = listOf(
+ IconRequest.Resource(
+ url = iconUrl,
+ type = IconRequest.Resource.Type.MANIFEST_ICON,
+ ),
+ ),
+ isPrivate = isPrivate,
+ ),
+ ).await()
+
+ return if (icon.source == Source.GENERATOR) null else icon.bitmap
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationFeature.kt b/mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationFeature.kt
new file mode 100644
index 0000000000..62ace5b5ef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationFeature.kt
@@ -0,0 +1,122 @@
+/* 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.webnotifications
+
+import android.app.Activity
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.annotation.DrawableRes
+import androidx.core.app.NotificationCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.permission.SitePermissionsStorage
+import mozilla.components.concept.engine.webnotifications.WebNotification
+import mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.getOrigin
+import java.lang.UnsupportedOperationException
+import kotlin.coroutines.CoroutineContext
+
+private const val NOTIFICATION_CHANNEL_ID = "mozac.feature.webnotifications.generic.channel"
+private const val PENDING_INTENT_TAG = "mozac.feature.webnotifications.generic.pendingintent"
+internal const val NOTIFICATION_ID = 1
+
+/**
+ * Feature implementation for configuring and displaying web notifications to the user.
+ *
+ * Initialize this feature globally once on app start
+ * ```Kotlin
+ * WebNotificationFeature(
+ * applicationContext, engine, icons, R.mipmap.ic_launcher, sitePermissionsStorage, BrowserActivity::class.java
+ * )
+ * ```
+ *
+ * @param context The application Context.
+ * @param engine The browser engine.
+ * @param browserIcons The entry point for loading the large icon for the notification.
+ * @param smallIcon The small icon for the notification.
+ * @param sitePermissionsStorage The storage for checking notification site permissions.
+ * @param activityClass The Activity that the notification will launch if user taps on it
+ * @param coroutineContext An instance of [CoroutineContext] used for executing async site permission checks.
+ */
+@Suppress("LongParameterList")
+class WebNotificationFeature(
+ private val context: Context,
+ private val engine: Engine,
+ browserIcons: BrowserIcons,
+ @DrawableRes smallIcon: Int,
+ private val sitePermissionsStorage: SitePermissionsStorage,
+ private val activityClass: Class<out Activity>?,
+ private val coroutineContext: CoroutineContext = Dispatchers.IO,
+ private val notificationsDelegate: NotificationsDelegate,
+) : WebNotificationDelegate {
+ private val logger = Logger("WebNotificationFeature")
+ private val nativeNotificationBridge = NativeNotificationBridge(browserIcons, smallIcon)
+
+ init {
+ try {
+ engine.registerWebNotificationDelegate(this)
+ } catch (e: UnsupportedOperationException) {
+ logger.error("failed to register for web notification delegate", e)
+ }
+ }
+
+ override fun onShowNotification(webNotification: WebNotification) {
+ CoroutineScope(coroutineContext).launch {
+ // Only need to check permissions for notifications from web pages. Permissions for
+ // web extensions are managed via the extension's manifest and approved by the user
+ // upon installation.
+ if (!webNotification.triggeredByWebExtension) {
+ val origin = webNotification.sourceUrl?.getOrigin() ?: return@launch
+ val permissions = sitePermissionsStorage.findSitePermissionsBy(
+ origin,
+ private = webNotification.privateBrowsing,
+ )
+ ?: return@launch
+
+ if (!permissions.notification.isAllowed()) {
+ return@launch
+ }
+ }
+
+ ensureNotificationGroupAndChannelExists()
+ notificationsDelegate.notificationManagerCompat.cancel(webNotification.tag, NOTIFICATION_ID)
+
+ val notification = nativeNotificationBridge.convertToAndroidNotification(
+ webNotification,
+ context,
+ NOTIFICATION_CHANNEL_ID,
+ activityClass,
+ SharedIdsHelper.getNextIdForTag(context, PENDING_INTENT_TAG),
+ )
+ notificationsDelegate.notify(webNotification.tag, NOTIFICATION_ID, notification)
+ }
+ }
+
+ override fun onCloseNotification(webNotification: WebNotification) {
+ notificationsDelegate.notificationManagerCompat.cancel(webNotification.tag, NOTIFICATION_ID)
+ }
+
+ private fun ensureNotificationGroupAndChannelExists() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.mozac_feature_notification_channel_name),
+ NotificationManager.IMPORTANCE_DEFAULT,
+ )
+ channel.setShowBadge(true)
+ channel.lockscreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
+
+ notificationsDelegate.notificationManagerCompat.createNotificationChannel(channel)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationIntentProcessor.kt b/mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationIntentProcessor.kt
new file mode 100644
index 0000000000..0a961ccba7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationIntentProcessor.kt
@@ -0,0 +1,36 @@
+/* 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.webnotifications
+
+import android.content.Intent
+import android.os.Parcelable
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.intent.processing.IntentProcessor
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+/**
+ * Intent processor that tries matching a web notification and delegating a click interaction with it.
+ */
+class WebNotificationIntentProcessor(
+ private val engine: Engine,
+) : IntentProcessor {
+ /**
+ * Processes an incoming intent expected to contain information about a web notification.
+ * If such information is available this will inform the web notification about it being clicked.
+ */
+ override fun process(intent: Intent): Boolean {
+ @Suppress("MoveVariableDeclarationIntoWhen")
+ val engineNotification =
+ intent.extras?.getParcelableCompat(NativeNotificationBridge.EXTRA_ON_CLICK, Parcelable::class.java)
+
+ return when (engineNotification) {
+ null -> false
+ else -> {
+ engine.handleWebNotificationClick(engineNotification)
+ true
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..30a71e587e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">የድረ-ገፅ ማሳወቂያዎች</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..eb6b90cd0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-an/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificacions d\'o puesto</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..19e6f3eb4e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ar/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">تنبيهات الموقع</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..95ba78ccd2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Avisos de sitios</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..db28cad1f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-az/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Sayt bildirişləri</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..42ea59b477
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">سایت بیلدیریش‌لری</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..14e7c781d4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Pakeling situs</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..a351d36d46
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Абвесткі сайта</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..a4d969da6c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Известия от страници</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..d96a8cc9ad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">সাইট নোটিফিকেশন</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..ffb34ac823
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Rebuzadurioù al lec’hienn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..8b1c110865
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Obavještenja stranice</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..3a0031aa4a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificacions del lloc</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..29320a3c87
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Taq rutzijol ruxaq</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..9fc8907220
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Pahibalo gikan sa site</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..1d6691c4aa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">ئاگانامەکانی ماڵپەڕ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..7a7e87e240
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Nutificazioni di situ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..245c3a1bb2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Oznámení z webových stránek</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..372503cdb2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Hysbysiadau gwefannau</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..052b5c4617
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Webstedsmeddelelser</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..ba583d7b1e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Website-Benachrichtigungen</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..1a82c9b781
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Sedłowe powěźeńki</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..ca7d493745
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Ειδοποιήσεις ιστοτόπου</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..7170506170
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Site notifications</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..7170506170
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Site notifications</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..e9ef8f1c98
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Retejaj sciigoj</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..7a3a2ebc29
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificaciones del sitio</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..834b5cf0d3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificaciones de sitio</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..7a3a2ebc29
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificaciones del sitio</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..7a3a2ebc29
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificaciones del sitio</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..7a3a2ebc29
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificaciones del sitio</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..f8eee716ea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Saitide teavitused</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..a0f49f1a9c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Gunearen jakinarazpenak</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..3709340044
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">آگاهی‌های پایگاه</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..18fb5ffccd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ff/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Tintine lowre</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..5eb417c3ec
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Sivustoilmoitukset</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..c561a607a1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notifications des sites web</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..18e49fc239
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notifichis sîts</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..9f1e8b9d27
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Websitenotifikaasjes</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..c9965f19dc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Brathan na làraich</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..0ac4b3dd7d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificacións do sitio</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..cd8b29ec5c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Tenda rehegua marandu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..7f516d26dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">સાઇટ સૂચનાઓ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..e7d38e44f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">साइट अधिसूचनाएं</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..033ea3c325
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Obavijesti stranice</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..19c94215b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Sydłowe zdźělenki</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..f387644b93
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Oldalértesítések</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..6c610f6bd7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Կայքի ծանուցումներ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..9632bdca23
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificationes de sito</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..594fdb8d60
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notifikasi situs</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..29c5c0b058
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Tilkynningar vefsvæðis</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..cd6fa36634
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notifiche siti</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..f681095fea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">התרעות אתר</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..11e086f6ae
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">サイト通知</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..6c57fe81fb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">საიტის შეტყობინებები</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..e03efbda15
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Sayt xabarlamaları</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..e4aae9397c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Ilɣa n usmel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..cc75db3922
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Сайт хабарламалары</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..db9990fa40
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Danezanên malperê</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..c14a6e8675
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-kn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">ಜಾಲತಾಣದ ಸೂಚನೆಗಳು</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..50db7cd6df
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">사이트 알림</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..844ce35b60
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">ການແຈ້ງເຕືອນໃນເວັບໄຊທ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..96ac1ef023
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Svetainių pranešimai</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..96c80bd477
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-mr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">साइट सुचना</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..531e3b1a6a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-my/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">ဆိုက် အသိပေးချက်များ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..d958cb46c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Nettstedsvarsler</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..35544562bf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">साइट सुचनाहरु</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..2586eb5206
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Websitenotificaties</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..8c38123b43
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Nettstadvarsel</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..fd1e22200b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificacions dels sites</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..e9b6ffe70f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">ਸਾਈਟ ਸੂਚਨਾਵਾਂ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..0512c7b594
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">سائٹ نوٹس</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..61567690f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Powiadomienia witryn</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..32bc1b219a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificações de sites</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..32bc1b219a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificações de sites</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..927790bd03
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Communicaziuns da websites</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..8c95a95f26
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ro/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notificări site</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..432ae9fd21
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Уведомления сайтов</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..c6cf24fecd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">ᱥᱟᱭᱤᱴ ᱨᱮᱭᱟᱜ ᱤᱛᱞᱟᱹᱭ ᱠᱚ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..cd68d1b150
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Notìficas de su situ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..c91549c4fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">අඩවියේ දැනුම්දීම්</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..a63d5f6b7c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Upozornenia stránok</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..ef5f54b582
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">سائٹ نوٹیفیکیشن</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..06f8bccd33
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Obvestila strani</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..588b8ad287
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Njoftime sajti</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..12362cec74
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Обавештења странице</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..11e059398b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Iber loka</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..3d642114cf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Webbplatsaviseringar</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..794f309489
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ta/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">தள அறிவிப்புகள்</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..606a5636ab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-te/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">సైటు గమనింపులు</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..57dca039c5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Огоҳномаҳои сомона</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..4f012888fe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">การแจ้งเตือนไซต์</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..90dbfa374b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Mga abiso ng site</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..6c13a3bcca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tok/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">sona sin tan lipu</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..19190c551c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Site bildirimleri</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..e007bfd0f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Nej sa atāj na\'ānj dàj hua riña sîtio</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..8eacd49cee
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Сайт искәртүләре</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..1b4ccb8fdf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Tineɣmisin n wasiten</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..5191c3d44a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">بېكەت ئۇقتۇرۇشى</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..23843478b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Сповіщення сайтів</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..6dc6506d02
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-ur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">سائٹ ‏کے ‏اعلانات</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..97cdc8fa8d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Sayt bildirishnomalari</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..e0792efcf1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Các thông báo trang</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..2bde7e11df
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Àwọn ìtalólobó sáìtì</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..f4ba672caa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">网站通知</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..689d463d86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">網站通知</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..d5499eb740
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Default Web Notification Channel Name. -->
+ <string name="mozac_feature_notification_channel_name">Site notifications</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt b/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt
new file mode 100644
index 0000000000..364e6cda16
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt
@@ -0,0 +1,140 @@
+/* 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.webnotifications
+
+import android.app.Notification
+import android.app.Notification.BigTextStyle
+import android.app.Notification.EXTRA_SUB_TEXT
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.IconRequest.Resource
+import mozilla.components.browser.icons.IconRequest.Resource.Type.MANIFEST_ICON
+import mozilla.components.browser.icons.IconRequest.Size.DEFAULT
+import mozilla.components.concept.engine.webnotifications.WebNotification
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+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.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+private const val TEST_TITLE = "test title"
+private const val TEST_TAG = "test tag"
+private const val TEST_TEXT = "test text"
+private const val TEST_URL = "mozilla.org"
+private const val TEST_CHANNEL = "testChannel"
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class NativeNotificationBridgeTest {
+ private val blankNotification = WebNotification(
+ TEST_TITLE, TEST_TAG, TEST_TEXT, TEST_URL, null, null,
+ null, true, mock(), 0, privateBrowsing = false,
+ )
+
+ private lateinit var icons: BrowserIcons
+ private lateinit var bridge: NativeNotificationBridge
+
+ @Before
+ fun setup() {
+ icons = mock()
+ bridge = NativeNotificationBridge(icons, android.R.drawable.ic_dialog_alert)
+
+ val mockIcon = Icon(mock(), source = Icon.Source.GENERATOR)
+ doReturn(CompletableDeferred(mockIcon)).`when`(icons).loadIcon(any())
+ }
+
+ @Test
+ fun `create blank notification`() = runTest {
+ val notification = bridge.convertToAndroidNotification(
+ blankNotification,
+ testContext,
+ TEST_CHANNEL,
+ null,
+ 0,
+ )
+
+ assertNull(notification.actions)
+ assertEquals(TEST_CHANNEL, notification.channelId)
+ assertEquals(0, notification.`when`)
+ assertNotNull(notification.smallIcon)
+ assertNull(notification.getLargeIcon())
+ assertTrue(notification.extras.containsKey(EXTRA_SUB_TEXT))
+ }
+
+ @Test
+ fun `set when`() = runTest {
+ val notification = bridge.convertToAndroidNotification(
+ blankNotification.copy(timestamp = 1234567890),
+ testContext,
+ TEST_CHANNEL,
+ null,
+ 0,
+ )
+
+ assertEquals(1234567890, notification.`when`)
+ }
+
+ @Test
+ fun `icon is loaded from BrowserIcons`() = runTest {
+ bridge.convertToAndroidNotification(
+ blankNotification.copy(sourceUrl = "https://example.com", iconUrl = "https://example.com/large.png"),
+ testContext,
+ TEST_CHANNEL,
+ null,
+ 0,
+ )
+
+ verify(icons).loadIcon(
+ IconRequest(
+ url = "https://example.com",
+ size = DEFAULT,
+ resources = listOf(
+ Resource(
+ url = "https://example.com/large.png",
+ type = MANIFEST_ICON,
+ ),
+ ),
+ isPrivate = true,
+ ),
+ )
+ }
+
+ @Test
+ fun `android notification sets BigTextStyle`() = runTest {
+ val notification = bridge.convertToAndroidNotification(
+ blankNotification.copy(iconUrl = "https://example.com/large.png"),
+ testContext,
+ TEST_CHANNEL,
+ null,
+ 0,
+ )
+
+ val expectedStyle = BigTextStyle().javaClass.name
+ assertEquals(expectedStyle, notification.extras.getString(Notification.EXTRA_TEMPLATE))
+
+ val noBodyNotification = bridge.convertToAndroidNotification(
+ blankNotification.copy(iconUrl = "https://example.com/large.png", body = null),
+ testContext,
+ TEST_CHANNEL,
+ null,
+ 0,
+ )
+
+ assertNotEquals(expectedStyle, noBodyNotification.extras.getString(Notification.EXTRA_TEMPLATE))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt b/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt
new file mode 100644
index 0000000000..9fcb9fb05a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt
@@ -0,0 +1,220 @@
+/* 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.webnotifications
+
+import androidx.core.app.NotificationChannelCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.Icon
+import mozilla.components.browser.icons.Icon.Source
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.permission.SitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.Status
+import mozilla.components.concept.engine.webnotifications.WebNotification
+import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class WebNotificationFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val context = spy(testContext)
+ private val browserIcons: BrowserIcons = mock()
+ private val icon: Icon = mock()
+ private val engine: Engine = mock()
+ private val notificationManager: NotificationManagerCompat = mock()
+ private val permissionsStorage: OnDiskSitePermissionsStorage = mock()
+ private val notificationsDelegate: NotificationsDelegate = mock()
+
+ private val testNotification = WebNotification(
+ "Mozilla",
+ "mozilla.org",
+ "Notification body",
+ "mozilla.org",
+ "https://mozilla.org/image.ico",
+ "rtl",
+ "en",
+ false,
+ mock(),
+ privateBrowsing = false,
+ )
+
+ @Before
+ fun setup() {
+ `when`(notificationsDelegate.notificationManagerCompat).thenReturn(notificationManager)
+ `when`(icon.source).thenReturn(Source.GENERATOR) // to no-op the browser icons call.
+ `when`(browserIcons.loadIcon(any())).thenReturn(CompletableDeferred(icon))
+ }
+
+ @Test
+ fun `register web notification delegate`() {
+ doNothing().`when`(engine).registerWebNotificationDelegate(any())
+ doNothing().`when`(notificationManager).createNotificationChannel(any<NotificationChannelCompat>())
+
+ WebNotificationFeature(
+ context,
+ engine,
+ browserIcons,
+ android.R.drawable.ic_dialog_alert,
+ mock(),
+ null,
+ notificationsDelegate = notificationsDelegate,
+ )
+
+ verify(engine).registerWebNotificationDelegate(any())
+ }
+
+ @Test
+ fun `engine notifies to cancel notification`() {
+ val webNotification: WebNotification = mock()
+ val feature = WebNotificationFeature(
+ context,
+ engine,
+ browserIcons,
+ android.R.drawable.ic_dialog_alert,
+ mock(),
+ null,
+ notificationsDelegate = notificationsDelegate,
+ )
+
+ `when`(webNotification.tag).thenReturn("testTag")
+
+ feature.onCloseNotification(webNotification)
+
+ verify(notificationManager).cancel("testTag", NOTIFICATION_ID)
+ }
+
+ @Test
+ fun `engine notifies to show notification`() = runTestOnMain {
+ val notification = testNotification.copy(sourceUrl = "https://mozilla.org:443")
+ val feature = WebNotificationFeature(
+ context,
+ engine,
+ browserIcons,
+ android.R.drawable.ic_dialog_alert,
+ permissionsStorage,
+ null,
+ coroutineContext,
+ notificationsDelegate = notificationsDelegate,
+ )
+
+ val permission = SitePermissions(origin = "https://mozilla.org:443", notification = Status.ALLOWED, savedAt = 0)
+
+ `when`(
+ permissionsStorage.findSitePermissionsBy(
+ any(),
+ anyBoolean(),
+ anyBoolean(),
+ ),
+ ).thenReturn(permission)
+
+ feature.onShowNotification(notification)
+
+ verify(notificationsDelegate).notify(
+ eq(notification.tag),
+ eq(NOTIFICATION_ID),
+ any(),
+ any(),
+ any(),
+ eq(false),
+ )
+ }
+
+ @Test
+ fun `notification ignored if permissions are not allowed`() = runTestOnMain {
+ val notification = testNotification.copy(sourceUrl = "https://mozilla.org:443")
+ val feature = WebNotificationFeature(
+ context,
+ engine,
+ browserIcons,
+ android.R.drawable.ic_dialog_alert,
+ mock(),
+ null,
+ notificationsDelegate = notificationsDelegate,
+ )
+
+ // No permissions found.
+
+ feature.onShowNotification(notification)
+
+ verify(notificationManager, never()).notify(eq(testNotification.tag), eq(NOTIFICATION_ID), any())
+
+ // When explicitly denied.
+
+ val permission = SitePermissions(origin = "https://mozilla.org:443", notification = Status.BLOCKED, savedAt = 0)
+ `when`(
+ permissionsStorage.findSitePermissionsBy(
+ any(),
+ anyBoolean(),
+ anyBoolean(),
+ ),
+ ).thenReturn(permission)
+
+ feature.onShowNotification(testNotification)
+
+ verify(notificationManager, never()).notify(eq(testNotification.tag), eq(NOTIFICATION_ID), any())
+ }
+
+ @Test
+ fun `notifications always allowed for web extensions`() = runTestOnMain {
+ val webExtensionNotification = WebNotification(
+ "Mozilla",
+ "mozilla.org",
+ "Notification body",
+ "mozilla.org",
+ "https://mozilla.org/image.ico",
+ "rtl",
+ "en",
+ false,
+ mock(),
+ triggeredByWebExtension = true,
+ privateBrowsing = true,
+ )
+
+ val feature = WebNotificationFeature(
+ context,
+ engine,
+ browserIcons,
+ android.R.drawable.ic_dialog_alert,
+ permissionsStorage,
+ null,
+ coroutineContext,
+ notificationsDelegate = notificationsDelegate,
+ )
+
+ feature.onShowNotification(webExtensionNotification)
+
+ verify(notificationsDelegate).notify(
+ eq(testNotification.tag),
+ eq(NOTIFICATION_ID),
+ any(),
+ any(),
+ any(),
+ eq(false),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationIntentProcessorTest.kt b/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationIntentProcessorTest.kt
new file mode 100644
index 0000000000..df20bced9e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationIntentProcessorTest.kt
@@ -0,0 +1,51 @@
+/* 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.webnotifications
+
+import android.content.Intent
+import android.os.Parcelable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.Engine
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class WebNotificationIntentProcessorTest {
+ private val engine: Engine = mock()
+ private val processor = WebNotificationIntentProcessor(engine)
+
+ @Test
+ fun `GIVEN an Intent WHEN it contains a parcelable with our private key THEN processing is successful`() {
+ val notification = mock<Parcelable>()
+ val intent = Intent().putExtra(NativeNotificationBridge.EXTRA_ON_CLICK, notification)
+
+ val result = processor.process(intent)
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN an Intent WHEN it does not contain a parcelable with our private key THEN fail at processing the intent`() {
+ val intent = Intent()
+
+ val result = processor.process(intent)
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `GIVEN an Intent WHEN it contains a parcelable with our private key THEN delegate the engine to handle it`() {
+ val notification = mock<Parcelable>()
+ val intent = Intent().putExtra(NativeNotificationBridge.EXTRA_ON_CLICK, notification)
+
+ processor.process(intent)
+
+ verify(engine).handleWebNotificationClick(notification)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/webnotifications/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/webnotifications/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/webnotifications/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/webnotifications/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/webnotifications/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/webnotifications/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/auth/build.gradle b/mobile/android/android-components/components/lib/auth/build.gradle
new file mode 100644
index 0000000000..26f11505ee
--- /dev/null
+++ b/mobile/android/android-components/components/lib/auth/build.gradle
@@ -0,0 +1,38 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.lib.auth'
+}
+
+dependencies {
+ implementation project(':support-base')
+ implementation ComponentsDependencies.androidx_biometric
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/lib/auth/proguard-rules.pro b/mobile/android/android-components/components/lib/auth/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/mobile/android/android-components/components/lib/auth/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 \ No newline at end of file
diff --git a/mobile/android/android-components/components/lib/auth/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/auth/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/auth/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/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt
new file mode 100644
index 0000000000..c1cb5265c3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt
@@ -0,0 +1,29 @@
+/* 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.lib.auth
+
+/**
+ * Callbacks for BiometricPrompt Authentication
+ */
+interface AuthenticationDelegate {
+
+ /**
+ * Called when a biometric (e.g. fingerprint, face, etc.)
+ * is presented but not recognized as belonging to the user.
+ */
+ fun onAuthFailure()
+
+ /**
+ * Called when a biometric (e.g. fingerprint, face, etc.) is recognized,
+ * indicating that the user has successfully authenticated.
+ */
+ fun onAuthSuccess()
+
+ /**
+ * Called when an unrecoverable error has been encountered and authentication has stopped.
+ * @param errorText A human-readable error string that can be shown on an UI
+ */
+ fun onAuthError(errorText: String)
+}
diff --git a/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.kt b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.kt
new file mode 100644
index 0000000000..a815bebe39
--- /dev/null
+++ b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.kt
@@ -0,0 +1,78 @@
+/* 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.lib.auth
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A [LifecycleAwareFeature] for the Android Biometric API to prompt for user authentication.
+ * The prompt also requests support for the device PIN as a fallback authentication mechanism.
+ *
+ * @param context Android context.
+ * @param fragment The fragment on which this feature will live.
+ * @param authenticationDelegate Callbacks for BiometricPrompt.
+ */
+class BiometricPromptAuth(
+ private val context: Context,
+ private val fragment: Fragment,
+ private val authenticationDelegate: AuthenticationDelegate,
+) : LifecycleAwareFeature {
+ private val logger = Logger(javaClass.simpleName)
+
+ @VisibleForTesting
+ internal var biometricPrompt: BiometricPrompt? = null
+
+ override fun start() {
+ val executor = ContextCompat.getMainExecutor(context)
+ biometricPrompt = BiometricPrompt(fragment, executor, PromptCallback())
+ }
+
+ override fun stop() {
+ biometricPrompt = null
+ }
+
+ /**
+ * Requests the user for biometric authentication.
+ *
+ * @param title Adds a title for the authentication prompt.
+ * @param subtitle Adds a subtitle for the authentication prompt.
+ */
+ fun requestAuthentication(
+ title: String,
+ subtitle: String = "",
+ ) {
+ val promptInfo: BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
+ .setTitle(title)
+ .setSubtitle(subtitle)
+ .build()
+ biometricPrompt?.authenticate(promptInfo)
+ }
+
+ internal inner class PromptCallback : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ logger.error("onAuthenticationError: errorMessage $errString errorCode=$errorCode")
+ authenticationDelegate.onAuthError(errString.toString())
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ logger.debug("onAuthenticationSucceeded")
+ authenticationDelegate.onAuthSuccess()
+ }
+
+ override fun onAuthenticationFailed() {
+ logger.error("onAuthenticationFailed")
+ authenticationDelegate.onAuthFailure()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt
new file mode 100644
index 0000000000..3f4ca88fc1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt
@@ -0,0 +1,51 @@
+/* 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.lib.auth
+
+import android.content.Context
+import android.os.Build
+import androidx.biometric.BiometricManager
+
+/**
+ * Utility class for BiometricPromptAuth
+ */
+
+fun Context.canUseBiometricFeature(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val manager = BiometricManager.from(this)
+ return BiometricUtils.canUseFeature(manager)
+ } else {
+ false
+ }
+}
+
+internal object BiometricUtils {
+
+ /**
+ * Checks if the appropriate SDK version and hardware capabilities are met to use the feature.
+ */
+ internal fun canUseFeature(manager: BiometricManager): Boolean {
+ return isHardwareAvailable(manager) && isEnrolled(manager)
+ }
+
+ /**
+ * Checks if the hardware requirements are met for using the [BiometricManager].
+ */
+ internal fun isHardwareAvailable(biometricManager: BiometricManager): Boolean {
+ val status =
+ biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
+ return status != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE &&
+ status != BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
+ }
+
+ /**
+ * Checks if the user can use the [BiometricManager] and is therefore enrolled.
+ */
+ internal fun isEnrolled(biometricManager: BiometricManager): Boolean {
+ val status =
+ biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
+ return status == BiometricManager.BIOMETRIC_SUCCESS
+ }
+}
diff --git a/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.kt b/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.kt
new file mode 100644
index 0000000000..1c74d24da9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.kt
@@ -0,0 +1,91 @@
+/* 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.lib.auth
+
+import androidx.biometric.BiometricPrompt
+import androidx.fragment.app.Fragment
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.createAddedTestFragment
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class BiometricPromptAuthTest {
+
+ private lateinit var biometricPromptAuth: BiometricPromptAuth
+ private lateinit var fragment: Fragment
+
+ @Before
+ fun setup() {
+ fragment = createAddedTestFragment { Fragment() }
+ biometricPromptAuth = BiometricPromptAuth(
+ testContext,
+ fragment,
+ object : AuthenticationDelegate {
+ override fun onAuthFailure() {
+ }
+
+ override fun onAuthSuccess() {
+ }
+
+ override fun onAuthError(errorText: String) {
+ }
+ },
+ )
+ }
+
+ @Test
+ fun `prompt is created and destroyed on start and stop`() {
+ assertNull(biometricPromptAuth.biometricPrompt)
+
+ biometricPromptAuth.start()
+
+ assertNotNull(biometricPromptAuth.biometricPrompt)
+
+ biometricPromptAuth.stop()
+
+ assertNull(biometricPromptAuth.biometricPrompt)
+ }
+
+ @Test
+ fun `requestAuthentication invokes biometric prompt`() {
+ val prompt: BiometricPrompt = mock()
+
+ biometricPromptAuth.biometricPrompt = prompt
+
+ biometricPromptAuth.requestAuthentication("title", "subtitle")
+
+ verify(prompt).authenticate(any())
+ }
+
+ @Test
+ fun `promptCallback fires feature callbacks`() {
+ val authenticationDelegate: AuthenticationDelegate = mock()
+ val feature = BiometricPromptAuth(testContext, fragment, authenticationDelegate)
+ val callback = feature.PromptCallback()
+ val prompt = BiometricPrompt(fragment, callback)
+
+ feature.biometricPrompt = prompt
+
+ callback.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, "")
+
+ verify(authenticationDelegate).onAuthError("")
+
+ callback.onAuthenticationFailed()
+
+ verify(authenticationDelegate).onAuthFailure()
+
+ callback.onAuthenticationSucceeded(mock())
+
+ verify(authenticationDelegate).onAuthSuccess()
+ }
+}
diff --git a/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt b/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt
new file mode 100644
index 0000000000..c8c9d53b70
--- /dev/null
+++ b/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt
@@ -0,0 +1,50 @@
+/* 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.lib.auth
+
+import android.os.Build
+import androidx.biometric.BiometricManager
+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 org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class BiometricUtilsTest {
+
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Test
+ fun `canUseFeature checks for SDK compatible`() {
+ assertFalse(testContext.canUseBiometricFeature())
+ }
+
+ @Test
+ fun `isHardwareAvailable is true based on AuthenticationStatus`() {
+ val manager: BiometricManager = mock {
+ whenever(canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
+ .thenReturn(BiometricManager.BIOMETRIC_SUCCESS)
+ .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE)
+ .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE)
+ }
+
+ assertTrue(BiometricUtils.isHardwareAvailable(manager))
+ assertFalse(BiometricUtils.isHardwareAvailable(manager))
+ assertFalse(BiometricUtils.isHardwareAvailable(manager))
+ }
+
+ @Test
+ fun `isEnrolled is true based on AuthenticationStatus`() {
+ val manager: BiometricManager = mock {
+ whenever(canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
+ .thenReturn(BiometricManager.BIOMETRIC_SUCCESS)
+ }
+ assertTrue(BiometricUtils.isEnrolled(manager))
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash-sentry/build.gradle b/mobile/android/android-components/components/lib/crash-sentry/build.gradle
new file mode 100644
index 0000000000..caba4d5650
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/build.gradle
@@ -0,0 +1,45 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.lib.crash.sentry'
+}
+
+dependencies {
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+ implementation project(':lib-crash')
+
+ implementation ComponentsDependencies.thirdparty_sentry
+ testImplementation ComponentsDependencies.thirdparty_sentry
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/lib/crash-sentry/proguard-rules.pro b/mobile/android/android-components/components/lib/crash-sentry/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/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/lib/crash-sentry/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/crash-sentry/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..7b04326db6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/src/main/AndroidManifest.xml
@@ -0,0 +1,15 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+ <application>
+ <meta-data
+ android:name="io.sentry.auto-init"
+ android:value="false" />
+ <provider
+ android:name="io.sentry.android.core.SentryInitProvider"
+ android:authorities="${applicationId}.SentryInitProvider"
+ tools:node="remove" />
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt
new file mode 100644
index 0000000000..fbe4c5e874
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt
@@ -0,0 +1,201 @@
+/* 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.lib.crash.sentry
+
+import android.content.Context
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import io.sentry.Breadcrumb
+import io.sentry.Sentry
+import io.sentry.SentryLevel
+import io.sentry.android.core.SentryAndroid
+import io.sentry.protocol.SentryId
+import mozilla.components.Build
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.sentry.eventprocessors.AddMechanismEventProcessor
+import mozilla.components.lib.crash.sentry.eventprocessors.RustCrashEventProcessor
+import mozilla.components.lib.crash.service.CrashReporterService
+import java.util.Locale
+import mozilla.components.concept.base.crash.Breadcrumb as MozillaBreadcrumb
+
+/**
+ * A [CrashReporterService] implementation that uploads crash reports using
+ * the Sentry SDK version 5.6.1 and above.
+ *
+ * This implementation will add default tags to every sent crash report
+ * (like which Android Components version is being used) prefixed with "ac".
+ *
+ * @param applicationContext The application [Context].
+ * @param dsn Data Source Name of the Sentry server.
+ * @param tags A list of additional tags that will be sent together with crash reports.
+ * @param environment An optional, environment name string or null to set none
+ * @param sendEventForNativeCrashes Allows configuring if native crashes should be submitted. Disabled by default.
+ * @param sentryProjectUrl Base URL of the Sentry web interface pointing to the app/project.
+ * @param sendCaughtExceptions Allows configuring if caught exceptions should be submitted. Enabled by default.
+ * @param autoInitializeSentry Initializes the Sentry SDK immediately on service creation.
+ */
+class SentryService(
+ private val applicationContext: Context,
+ private val dsn: String,
+ private val tags: Map<String, String> = emptyMap(),
+ private val environment: String? = null,
+ private val sendEventForNativeCrashes: Boolean = false,
+ private val sentryProjectUrl: String? = null,
+ private val sendCaughtExceptions: Boolean = true,
+) : CrashReporterService {
+
+ override val id: String = "new-sentry-instance"
+ override val name: String = "New Sentry Instance"
+
+ @VisibleForTesting
+ @GuardedBy("this")
+ internal var isInitialized: Boolean = false
+
+ override fun createCrashReportUrl(identifier: String): String? {
+ return sentryProjectUrl?.let {
+ val id = identifier.replace("-", "")
+ return "$it&query=$id"
+ }
+ }
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String {
+ prepareReport(crash.breadcrumbs, SentryLevel.FATAL)
+ return reportToSentry(crash.throwable)
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ return if (sendEventForNativeCrashes) {
+ val level = when (crash.isFatal) {
+ true -> SentryLevel.FATAL
+ else -> SentryLevel.ERROR
+ }
+
+ prepareReport(crash.breadcrumbs, level)
+
+ return reportToSentry(crash)
+ } else {
+ null
+ }
+ }
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<MozillaBreadcrumb>): String? {
+ if (!sendCaughtExceptions) {
+ return null
+ }
+ prepareReport(breadcrumbs, SentryLevel.INFO)
+ return reportToSentry(throwable)
+ }
+
+ @VisibleForTesting
+ internal fun reportToSentry(throwable: Throwable): String {
+ return Sentry.captureException(throwable).alsoClearBreadcrumbs()
+ }
+
+ @VisibleForTesting
+ internal fun reportToSentry(crash: Crash.NativeCodeCrash): String {
+ return Sentry.captureMessage(createMessage(crash)).alsoClearBreadcrumbs()
+ }
+
+ private fun addDefaultTags() {
+ Sentry.setTag("ac.version", Build.version)
+ Sentry.setTag("ac.git", Build.gitHash)
+ Sentry.setTag("ac.as.build_version", Build.applicationServicesVersion)
+ Sentry.setTag("ac.glean.build_version", Build.gleanSdkVersion)
+ Sentry.setTag("user.locale", Locale.getDefault().toString())
+ tags.forEach { entry ->
+ Sentry.setTag(entry.key, entry.value)
+ }
+ }
+
+ /**
+ * Initializes Sentry if needed.
+ *
+ * N.B: We've temporarily made this public so that Fenix can initialize Sentry on startup.
+ * As a result of https://bugzilla.mozilla.org/show_bug.cgi?id=1853059 we will have a better way
+ * to control how / when Sentry gets initialized and we will make this internal again.
+ */
+ @Synchronized
+ fun initIfNeeded() {
+ if (isInitialized) {
+ return
+ }
+ initSentry()
+ addDefaultTags()
+ isInitialized = true
+ }
+
+ @VisibleForTesting
+ internal fun initSentry() {
+ SentryAndroid.init(applicationContext) { options ->
+ // Disable uncaught non-native exceptions from being reported.
+ // We already have our own uncaught exception handler [ExceptionHandler],
+ // so we don't need Sentry's default one.
+ options.setEnableUncaughtExceptionHandler(false)
+ // Disable uncaught native exceptions from being reported.
+ // Sentry don't have a way to disable uncaught native exceptions from being reported.
+ // As a fallback we had to disable all native integrations.
+ // More info can be found https://github.com/getsentry/sentry-java/issues/1993
+ options.isEnableNdk = false
+ options.dsn = dsn
+ options.environment = environment
+ options.addEventProcessor(RustCrashEventProcessor())
+ options.addEventProcessor(AddMechanismEventProcessor())
+ }
+ }
+
+ @VisibleForTesting
+ internal fun prepareReport(
+ breadcrumbs: ArrayList<MozillaBreadcrumb>,
+ level: SentryLevel? = null,
+ ) {
+ initIfNeeded()
+
+ breadcrumbs.forEach {
+ Sentry.addBreadcrumb(it.toSentryBreadcrumb())
+ }
+
+ level?.apply {
+ Sentry.setLevel(level)
+ }
+ }
+
+ private fun SentryId.alsoClearBreadcrumbs(): String {
+ Sentry.clearBreadcrumbs()
+ return this.toString()
+ }
+
+ @VisibleForTesting
+ internal fun createMessage(crash: Crash.NativeCodeCrash): String {
+ val fatal = crash.isFatal.toString()
+ val processType = crash.processType
+ val minidumpSuccess = crash.minidumpSuccess
+
+ return "NativeCodeCrash(fatal=$fatal, processType=$processType, minidumpSuccess=$minidumpSuccess)"
+ }
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun MozillaBreadcrumb.toSentryBreadcrumb(): Breadcrumb {
+ val sentryLevel = this.level.toSentryBreadcrumbLevel()
+ val breadcrumb = Breadcrumb(this.date).apply {
+ message = this@toSentryBreadcrumb.message
+ category = this@toSentryBreadcrumb.category
+ level = sentryLevel
+ type = this@toSentryBreadcrumb.type.value
+ }
+ this.data.forEach {
+ breadcrumb.setData(it.key, it.value)
+ }
+ return breadcrumb
+}
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal fun MozillaBreadcrumb.Level.toSentryBreadcrumbLevel() = when (this) {
+ MozillaBreadcrumb.Level.CRITICAL -> SentryLevel.FATAL
+ MozillaBreadcrumb.Level.ERROR -> SentryLevel.ERROR
+ MozillaBreadcrumb.Level.WARNING -> SentryLevel.WARNING
+ MozillaBreadcrumb.Level.INFO -> SentryLevel.INFO
+ MozillaBreadcrumb.Level.DEBUG -> SentryLevel.DEBUG
+}
diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.kt b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.kt
new file mode 100644
index 0000000000..92adbd2906
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.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.lib.crash.sentry.eventprocessors
+
+import androidx.annotation.VisibleForTesting
+import io.sentry.EventProcessor
+import io.sentry.Hint
+import io.sentry.SentryEvent
+import io.sentry.SentryLevel
+import io.sentry.protocol.Mechanism
+
+/**
+ * A [EventProcessor] implementation that adds a [Machanism]
+ * to [SentryLevel.FATAL] events.
+ */
+class AddMechanismEventProcessor : EventProcessor {
+ override fun process(event: SentryEvent, hint: Hint): SentryEvent {
+ if (event.level == SentryLevel.FATAL) {
+ // Sentry now uses the `Mechanism` to determine whether or not an exception is
+ // handled. Any exception sent with `Sentry.captureException` is assumed to be handled
+ // by Sentry. We can attach a `UncaughtExceptionHandler` mechanism to the `SentryException`
+ // to correctly signal to Sentry that this is an uncaught exception.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1835107
+ event.exceptions?.firstOrNull()?.let { sentryException ->
+ sentryException.mechanism = Mechanism().apply {
+ type = UNCAUGHT_EXCEPTION_TYPE
+ isHandled = false
+ }
+ }
+ }
+
+ return event
+ }
+
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val UNCAUGHT_EXCEPTION_TYPE = "UncaughtExceptionHandler"
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt
new file mode 100644
index 0000000000..a6069699bc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt
@@ -0,0 +1,36 @@
+/* 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.lib.crash.sentry.eventprocessors
+
+import io.sentry.EventProcessor
+import io.sentry.Hint
+import io.sentry.SentryEvent
+import mozilla.components.concept.base.crash.RustCrashReport as RustCrashReport
+
+/**
+ * A [EventProcessor] implementation that cleans up exceptions for
+ * crashes coming from our Rust libraries.
+ */
+class RustCrashEventProcessor : EventProcessor {
+ override fun process(event: SentryEvent, hint: Hint): SentryEvent {
+ val throwable = event.throwable
+
+ if (throwable is RustCrashReport) {
+ event.fingerprints = listOf(throwable.typeName)
+ // Sentry supports multiple exceptions in an event, modify
+ // the top-level one controls how the event is displayed
+ //
+ // It's technically possible for the event to have a null
+ // or empty exception list, but that shouldn't happen in
+ // practice.
+ event.exceptions?.firstOrNull()?.let { sentryException ->
+ sentryException.type = throwable.typeName
+ sentryException.value = throwable.message
+ }
+ }
+
+ return event
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt
new file mode 100644
index 0000000000..e6a2aa25b6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt
@@ -0,0 +1,276 @@
+/* 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.lib.crash.sentry
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.sentry.Sentry
+import io.sentry.SentryLevel
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.Date
+import mozilla.components.concept.base.crash.Breadcrumb as MozillaBreadcrumb
+
+@RunWith(AndroidJUnit4::class)
+class SentryServiceTest {
+ class TestException : Exception()
+
+ @Before
+ fun setup() {
+ Sentry.close()
+ }
+
+ @Test
+ fun `WHEN calling initIfNeeded THEN initialize sentry once`() {
+ val service = spy(
+ SentryService(
+ testContext,
+ "https://not:real6@sentry.prod.example.net/405",
+ sendCaughtExceptions = false,
+ ),
+ )
+
+ assertFalse(service.isInitialized)
+
+ service.initIfNeeded()
+
+ assertTrue(service.isInitialized)
+
+ service.initIfNeeded()
+
+ verify(service, times(1)).initSentry()
+ }
+
+ @Test
+ fun `WHEN report a uncaught exception THEN forward a fatal exception to the Sentry sdk`() {
+ val service = spy(
+ SentryService(
+ testContext,
+ "https://not:real6@sentry.prod.example.net/405",
+ ),
+ )
+
+ val exception = RuntimeException("Hello World")
+ val breadcrumbs = arrayListOf<Breadcrumb>()
+
+ service.report(Crash.UncaughtExceptionCrash(0, exception, breadcrumbs))
+
+ verify(service).prepareReport(breadcrumbs, SentryLevel.FATAL)
+ verify(service).reportToSentry(exception)
+ }
+
+ @Test
+ fun `GIVEN a main process native crash WHEN reporting THEN forward to a fatal crash the Sentry sdk`() {
+ val service = spy(
+ SentryService(
+ applicationContext = testContext,
+ dsn = "https://not:real6@sentry.prod.example.net/405",
+ sendEventForNativeCrashes = true,
+ ),
+ )
+
+ val breadcrumbs = arrayListOf<Breadcrumb>()
+ val nativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = breadcrumbs,
+ remoteType = null,
+ )
+
+ service.report(nativeCrash)
+
+ verify(service).prepareReport(breadcrumbs, SentryLevel.FATAL)
+ verify(service).reportToSentry(nativeCrash)
+ }
+
+ @Test
+ fun `GIVEN a foreground child process native crash WHEN reporting THEN forward an error to the Sentry sdk`() {
+ val service = spy(
+ SentryService(
+ applicationContext = testContext,
+ dsn = "https://not:real6@sentry.prod.example.net/405",
+ sendEventForNativeCrashes = true,
+ ),
+ )
+
+ val breadcrumbs = arrayListOf<Breadcrumb>()
+ val nativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = breadcrumbs,
+ remoteType = null,
+ )
+
+ service.report(nativeCrash)
+
+ verify(service).prepareReport(breadcrumbs, SentryLevel.ERROR)
+ verify(service).reportToSentry(nativeCrash)
+ }
+
+ @Test
+ fun `GIVEN a background child process native crash WHEN reporting THEN forward an error to the Sentry sdk`() {
+ val service = spy(
+ SentryService(
+ applicationContext = testContext,
+ dsn = "https://not:real6@sentry.prod.example.net/405",
+ sendEventForNativeCrashes = true,
+ ),
+ )
+
+ val breadcrumbs = arrayListOf<Breadcrumb>()
+ val nativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = breadcrumbs,
+ remoteType = null,
+ )
+
+ service.report(nativeCrash)
+
+ verify(service).prepareReport(breadcrumbs, SentryLevel.ERROR)
+ verify(service).reportToSentry(nativeCrash)
+ }
+
+ @Test
+ fun `GIVEN sendEventForNativeCrashes is false WHEN reporting a native crash THEN DO NOT forward to the Sentry sdk`() {
+ val service = spy(
+ SentryService(
+ applicationContext = testContext,
+ dsn = "https://not:real6@sentry.prod.example.net/405",
+ sendEventForNativeCrashes = false,
+ ),
+ )
+
+ val breadcrumbs = arrayListOf<Breadcrumb>()
+ val nativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = breadcrumbs,
+ remoteType = null,
+ )
+
+ val result = service.report(nativeCrash)
+
+ verify(service, times(0)).prepareReport(breadcrumbs, SentryLevel.ERROR)
+ verify(service, times(0)).reportToSentry(nativeCrash)
+ assertNull(result)
+ }
+
+ @Test
+ fun `WHEN createMessage THEN create a message version of the Native crash`() {
+ val service = SentryService(
+ applicationContext = testContext,
+ dsn = "https://not:real6@sentry.prod.example.net/405",
+ sendEventForNativeCrashes = false,
+ )
+
+ val breadcrumbs = arrayListOf<Breadcrumb>()
+ val nativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = breadcrumbs,
+ remoteType = null,
+ )
+
+ val result = service.createMessage(nativeCrash)
+ val expected =
+ "NativeCodeCrash(fatal=${nativeCrash.isFatal}, processType=${nativeCrash.processType}, minidumpSuccess=${nativeCrash.minidumpSuccess})"
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `GIVEN MozillaBreadcrumb WHEN calling toSentryBreadcrumb THEN parse it to a SentryBreadcrumb`() {
+ val mozillaBreadcrumb = MozillaBreadcrumb(
+ message = "message",
+ data = mapOf("key" to "value"),
+ category = "category",
+ level = MozillaBreadcrumb.Level.INFO,
+ type = MozillaBreadcrumb.Type.DEFAULT,
+ date = Date(1640995200L), // 2022-01-01
+ )
+ val sentryBreadcrumb = mozillaBreadcrumb.toSentryBreadcrumb()
+
+ assertEquals(mozillaBreadcrumb.message, sentryBreadcrumb.message)
+ assertEquals(mozillaBreadcrumb.data["key"], sentryBreadcrumb.getData("key"))
+ assertEquals(mozillaBreadcrumb.category, sentryBreadcrumb.category)
+ assertEquals(SentryLevel.INFO, sentryBreadcrumb.level)
+ assertEquals(MozillaBreadcrumb.Type.DEFAULT.value, sentryBreadcrumb.type)
+ assertEquals(mozillaBreadcrumb.date, sentryBreadcrumb.timestamp)
+ }
+
+ @Test
+ fun `GIVEN MozillaBreadcrumb level WHEN calling toSentryBreadcrumbLevel THEN parse it to a SentryBreadcrumbLevel`() {
+ assertEquals(MozillaBreadcrumb.Level.CRITICAL.toSentryBreadcrumbLevel(), SentryLevel.FATAL)
+ assertEquals(MozillaBreadcrumb.Level.ERROR.toSentryBreadcrumbLevel(), SentryLevel.ERROR)
+ assertEquals(MozillaBreadcrumb.Level.WARNING.toSentryBreadcrumbLevel(), SentryLevel.WARNING)
+ assertEquals(MozillaBreadcrumb.Level.INFO.toSentryBreadcrumbLevel(), SentryLevel.INFO)
+ assertEquals(MozillaBreadcrumb.Level.DEBUG.toSentryBreadcrumbLevel(), SentryLevel.DEBUG)
+ }
+
+ @Test
+ fun `GIVEN sending caught exceptions disabled WHEN reporting a caught exception THEN do nothing`() {
+ val service = spy(
+ SentryService(
+ testContext,
+ "https://not:real6@sentry.prod.example.net/405",
+ sendCaughtExceptions = false,
+ ),
+ )
+
+ val exception = RuntimeException("Hello World")
+ val breadcrumbs = arrayListOf<Breadcrumb>()
+
+ service.report(exception, breadcrumbs)
+ verify(service, never()).prepareReport(breadcrumbs, SentryLevel.INFO)
+ verify(service, never()).prepareReport(breadcrumbs, SentryLevel.FATAL)
+ verify(service, never()).reportToSentry(exception)
+ }
+
+ @Test
+ fun `GIVEN sending caught exceptions enabled WHEN reporting a caught exception THEN forward it to Sentry SDK with level INFO`() {
+ val service = spy(
+ // Sending caught exceptions is enabled by default.
+ SentryService(
+ testContext,
+ "https://not:real6@sentry.prod.example.net/405",
+ ),
+ )
+
+ val exception = RuntimeException("Hello World")
+ val breadcrumbs = arrayListOf<Breadcrumb>()
+
+ service.report(exception, breadcrumbs)
+
+ verify(service).prepareReport(breadcrumbs, SentryLevel.INFO)
+ verify(service).reportToSentry(exception)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt
new file mode 100644
index 0000000000..5f14b0232a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt
@@ -0,0 +1,46 @@
+/* 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.lib.crash.sentry.eventprocessors
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.sentry.Hint
+import io.sentry.SentryEvent
+import io.sentry.SentryLevel
+import io.sentry.protocol.SentryException
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AddMechanismEventProcessorTest {
+ @Test
+ fun `GIVEN a FATAL SentryEvent WHEN process is called THEN a Mechanism is attached to the exception`() {
+ val processor = AddMechanismEventProcessor()
+ val event = SentryEvent().apply {
+ level = SentryLevel.FATAL
+ exceptions = listOf(SentryException())
+ }
+
+ assertNull(event.exceptions?.first()?.mechanism)
+ processor.process(event, Hint())
+ assertEquals(AddMechanismEventProcessor.UNCAUGHT_EXCEPTION_TYPE, event.exceptions?.first()?.mechanism?.type)
+ assertTrue(event.exceptions?.first()?.mechanism?.isHandled == false)
+ }
+
+ @Test
+ fun `GIVEN a less than FATAL SentryEvent WHEN process is called THEN no Mechanism is attached to the exception`() {
+ val processor = AddMechanismEventProcessor()
+ val event = SentryEvent().apply {
+ level = SentryLevel.INFO
+ exceptions = listOf(SentryException())
+ }
+
+ assertNull(event.exceptions?.first()?.mechanism)
+ processor.process(event, Hint())
+ assertNull(event.exceptions?.first()?.mechanism)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt
new file mode 100644
index 0000000000..7190f44b25
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt
@@ -0,0 +1,35 @@
+/* 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.lib.crash.sentry.eventprocessors
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.sentry.Hint
+import io.sentry.SentryEvent
+import io.sentry.protocol.SentryException
+import junit.framework.TestCase.assertEquals
+import mozilla.components.concept.base.crash.RustCrashReport
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RustCrashEventProcessorTest {
+ class TestRustException : Exception(), RustCrashReport {
+ override val typeName = "test_rust_crash"
+ override val message = "test_rust_message"
+ }
+
+ @Test
+ fun `GIVEN a SentryEvent that contains a RustCrashReport WHEN process is called THEN a fingerprint is added and the exception type and value are cleaned up`() {
+ val processor = RustCrashEventProcessor()
+ val event = SentryEvent(TestRustException()).apply {
+ exceptions = listOf(SentryException())
+ }
+
+ processor.process(event, Hint())
+ assertEquals("test_rust_crash", event.fingerprints?.first())
+ assertEquals("test_rust_crash", event.exceptions?.firstOrNull()?.type)
+ assertEquals("test_rust_message", event.exceptions?.firstOrNull()?.value)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/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/lib/crash-sentry/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/crash/README.md b/mobile/android/android-components/components/lib/crash/README.md
new file mode 100644
index 0000000000..43208dd3ce
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/README.md
@@ -0,0 +1,239 @@
+# [Android Components](../../../README.md) > Libraries > Crash
+
+A generic crash reporter component that can report crashes to multiple services.
+
+Main features:
+
+* Support for multiple crash reporting services (included is support for [Sentry](https://sentry.io) and [Socorro](https://wiki.mozilla.org/Socorro)).
+* Support for crashes caused by uncaught exceptions.
+* Support for native code crashes (currently primarily focused on GeckoView crashes).
+* Can optionally prompt the user for confirmation before sending a crash report.
+* Support for showing in-app confirmation UI for non-fatal crashes.
+
+## Usage
+
+### 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:lib-crash:{latest-version}"
+```
+
+### Setting up crash reporting
+
+In the `onCreate()` method of your Application class create a `CrashReporter` instance and call `install()`:
+
+```Kotlin
+CrashReporter(
+ services = listOf(
+ // List the crash reporting services you want to use
+ )
+).install(this)
+```
+
+With this minimal setup the crash reporting library will capture "uncaught exception" crashes and "native code" crashes and forward them to the configured crash reporting services.
+
+⚠️ Note: To avoid conflicting setups do not use any other crash reporting libraries/services independently from this library.
+
+### Recording crash breadcrumbs to supported services
+
+Using the `CrashReporter` instance to record crash breadcrumbs. These breadcrumbs will then be sent when a crash occurs to aid in debugging. Breadcrumbs are reported only if the underlying crash reporter service supports it.
+
+⚠️ Note: Directly using Sentry's breadcrumb will not work as expected on Android 10 or above. Using the `CrashReporter` breadcrumb is preferred.
+
+```Kotlin
+crashReporter.recordCrashBreadcrumb(
+ CrashBreadcrumb("Settings button clicked", data, "UI", Level.INFO, Type.USER)
+)
+```
+
+### Sending crash reports to Sentry
+
+⚠️ Note: The crash reporter library is compiled against the Sentry SDK but it doesn't require it as a dependency. The app using the component is responsible for adding the Sentry dependency to its build files in order to use Sentry crash reporting.
+
+Add a `SentryService` instance to your `CrashReporter` in order to upload crashes to Sentry:
+
+```Kotlin
+CrashReporter(
+ services = listOf(
+ SentryService(applicationContext, "your sentry DSN")
+ )
+).install(applicationContext)
+```
+
+By default only the `DSN` is needed. But there are additional option configuration parameters:
+
+```Kotlin
+SentryService(
+ applicationContext,
+ "your sentry DSN",
+
+ // Optionally add tags that will be sent with every crash report
+ tags = mapOf(
+ "build_flavor" to BuildConfig.FLAVOR,
+ "build_type" to BuildConfig.BUILD_TYPE
+ ),
+
+ // Send an event to Sentry for every native code crash. Native code crashes
+ // can't be uploaded to Sentry currently. But sending an event to Sentry
+ // gives you an idea about how often native code crashes. For sending native
+ // crash reports add additional services like Socorro.
+ sendEventForNativeCrashes = true
+)
+```
+
+### Sending crash reports to Mozilla Socorro
+
+[Socorro](https://wiki.mozilla.org/Socorro) is the name for the [Mozilla Crash Stats](https://crash-stats.mozilla.org/) project.
+
+⚠️ Note: Socorro filters crashes by "app name". New app names need to be safelisted for the server to accept the crash. [File a bug](https://bugzilla.mozilla.org/enter_bug.cgi?product=Socorro) if you would like to get your app added to the safelist.
+
+Add a `MozillaSocorroService` instance to your `CrashReporter` in order to upload crashes to Socorro:
+
+```Kotlin
+CrashReporter(
+ services = listOf(
+ MozillaSocorroService(applicationContext, "your app name")
+ )
+).install(applicationContext)
+```
+
+`MozillaSocorroService` will report version information such as App version, Android Component version, Glean version, Application Services version, GeckoView version and Build ID
+⚠️ Note: Currently only native code crashes get uploaded to Socorro. Socorro has limited support for "uncaught exception" crashes too, but it is recommended to use a more elaborate solution like Sentry for that.
+
+### Sending crash reports to Glean
+
+[Glean](https://docs.telemetry.mozilla.org/concepts/glean/glean.html) is a new way to collect telemetry by Mozilla.
+This will record crash counts as a labeled counter with each label corresponding to a specific type of crash (`fatal_native_code_crash`, `nonfatal_native_code_crash`, `caught_exception`, `uncaught_exception`, currently).
+The list of collected metrics is available in the [metrics.yaml file](metrics.yaml), with their documentation [living here](https://dictionary.telemetry.mozilla.org/apps/fenix/pings/crash).
+Due to the fact that Glean can only be recorded to in the main process and lib-crash runs in a separate process when it runs to handle the crash,
+lib-crash persists the data in a file format and then reads and records the data from the main process when the application is next run since the `GleanCrashReporterService`
+constructor is loaded from the main process.
+
+Add a `GleanCrashReporterService` instance to your `CrashReporter` in order to record crashes in Glean:
+
+```Kotlin
+CrashReporter(
+ services = listOf(
+ GleanCrashReporterService()
+ )
+).install(applicationContext)
+```
+
+⚠️ Note: Applications using the `GleanCrashReporterService` are **required** to undergo [Data Collection Review](https://wiki.mozilla.org/Firefox/Data_Collection) for the crash counts that they will be collecting.
+
+### Showing a crash reporter prompt
+
+![](images/crash-dialog.png)
+
+Optionally the library can show a prompt asking the user for confirmation before sending a crash report.
+
+The behavior can be controlled using the `shouldPrompt` parameter:
+
+```Kotlin
+CrashReporter(
+ // Always prompt
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+
+ // Or: Only prompt for native crashes
+ shouldPrompt = CrashReporter.Prompt.ONLY_NATIVE_CRASH,
+
+ // Or: Never show the prompt
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+
+ // ..
+).install(applicationContext)
+```
+
+#### Customizing the prompt
+
+The crash reporter prompt can be customized by providing a `PromptConfiguration` object:
+
+```Kotlin
+CrashReporter(
+ promptConfiguration = CrashReporter.PromptConfiguration(
+ appName = "My App",
+ organizationName = "My Organization",
+
+ // An additional message that will be shown in the prompt
+ message = "We are very sorry!"
+
+ // Use a custom theme for the prompt (Extend Theme.Mozac.CrashReporter)
+ theme = android.R.style.Theme_Holo_Dialog
+ ),
+
+ // ..
+).install(applicationContext)
+```
+
+#### Handling non-fatal crashes
+
+A native code crash can be non-fatal. In this situation a child process crashed but the main process (in which the application runs) is not affected. In this situation a crash can be handled more gracefully and instead of using the crash reporter prompt of the component an app may want to show an in-app UI for asking the user for confirmation.
+
+![](images/crash-in-app.png)
+
+Provide a `PendingIntent` that will be invoked when a non-fatal crash occurs:
+
+```Kotlin
+// Launch this activity when a crash occurs.
+val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(this, MyActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ },
+ PendingIntentUtils.defaultFlags
+)
+
+CrashReporter(
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(
+ // ...
+ ),
+ nonFatalCrashIntent = pendingIntent
+).install(this)
+```
+
+In your component that receives the Intent (e.g. `Activity`) you can use `Crash.fromIntent()` to receive the `Crash` object. Once the user has approved sending a report call `submitReport()` on your `CrashReporter` instance.
+
+```Kotlin
+// In your crash handling component (e.g. Activity)
+if (Crash.isCrashIntent(intent) {
+ val crash = Crash.fromIntent(intent)
+
+ ...
+}
+
+// Once the user has confirmed sending a crash report:
+crashReporter.submitReport(crash)
+```
+
+⚠️ Note: `submitReport()` may block and perform I/O on the calling thread.
+
+### Sending GeckoView crash reports
+
+⚠️ Note: For sending GeckoView crash reports GeckoView **64.0** or higher is required!
+
+Register `CrashHandlerService` as crash handler for GeckoView:
+
+```Kotlin
+val settings = GeckoRuntimeSettings.Builder()
+ .crashHandler(CrashHandlerService::class.java)
+ .build()
+
+// Crashes of this runtime will be forwarded to the crash reporter component
+val runtime = GeckoRuntime.create(applicationContext, settings)
+
+// If you are using the browser-engine-gecko component then pass the runtime
+// to your code initializing the engine:
+val engine = GeckoEngine(applicationContext, defaultSettings, runtime)
+```
+
+ℹ️ You can force a child process crash (non fatal!) using a multi-process (E10S) GeckoView by loading the test URL `about:crashcontent`. Using a non-multi-process GeckoView you can use `about:crashparent` to force a fatal crash.
+
+## 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/lib/crash/build.gradle b/mobile/android/android-components/components/lib/crash/build.gradle
new file mode 100644
index 0000000000..afedcf3044
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/build.gradle
@@ -0,0 +1,99 @@
+/* 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/. */
+
+buildscript {
+ repositories {
+ gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
+ maven {
+ url repository
+ if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
+ allowInsecureProtocol = true
+ }
+ }
+ }
+ }
+
+ dependencies {
+ classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}"
+ classpath ComponentsDependencies.plugin_serialization
+ }
+}
+
+plugins {
+ id "com.jetbrains.python.envs" version "$python_envs_plugin"
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.google.devtools.ksp'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+
+ namespace 'mozilla.components.lib.crash'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.kotlin_json
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_recyclerview
+
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ // We only compile against GeckoView and Glean. It's up to the app to add those dependencies if it wants to
+ // send crash reports to Socorro (GV).
+ compileOnly project(":service-glean")
+ testImplementation project(":service-glean")
+ testImplementation ComponentsDependencies.androidx_work_testing
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation ComponentsDependencies.mozilla_glean_forUnitTests
+}
+
+apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
+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/lib/crash/images/crash-dialog.png b/mobile/android/android-components/components/lib/crash/images/crash-dialog.png
new file mode 100644
index 0000000000..6fc96cc167
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/images/crash-dialog.png
Binary files differ
diff --git a/mobile/android/android-components/components/lib/crash/images/crash-in-app.png b/mobile/android/android-components/components/lib/crash/images/crash-in-app.png
new file mode 100644
index 0000000000..25392af00c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/images/crash-in-app.png
Binary files differ
diff --git a/mobile/android/android-components/components/lib/crash/metrics.yaml b/mobile/android/android-components/components/lib/crash/metrics.yaml
new file mode 100644
index 0000000000..bf30991944
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/metrics.yaml
@@ -0,0 +1,154 @@
+# 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/.
+
+# This file defines the metrics that are recorded by glean telemetry. They are
+# automatically converted to Kotlin code at build time using the `glean_parser`
+# PyPI package.
+---
+
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+
+crash_metrics:
+ crash_count:
+ type: labeled_counter
+ description: >
+ Counts the number of crashes that occur in the application.
+ This measures only the counts of each crash in association
+ with the labeled type of the crash.
+ The labels correspond to the types of crashes handled by lib-crash.
+
+ Deprecated: `native_code_crash`, `fatal_native_code_crash` and
+ `nonfatal_native_code_crash` replaced by `main_proc_native_code_crash`,
+ `fg_proc_native_code_crash` and `bg_proc_native_code_crash`.
+ labels:
+ - uncaught_exception
+ - caught_exception
+ - main_proc_native_code_crash
+ - fg_proc_native_code_crash
+ - bg_proc_native_code_crash
+ - fatal_native_code_crash
+ - nonfatal_native_code_crash
+ bugs:
+ - https://bugzilla.mozilla.org/1553935
+ - https://github.com/mozilla-mobile/android-components/issues/5175
+ - https://github.com/mozilla-mobile/android-components/issues/11876
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1553935#c3
+ - https://github.com/mozilla-mobile/android-components/pull/5700#pullrequestreview-347721248
+ - https://github.com/mozilla-mobile/android-components/pull/11908#issuecomment-1075243414
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - android-probes@mozilla.com
+ - jnicol@mozilla.com
+ expires: never
+
+crash:
+ uptime:
+ type: timespan
+ description: >
+ The application uptime. This is equivalent to the legacy crash ping's
+ `UptimeTS` field.
+ notification_emails:
+ - crash-reporting-wg@mozilla.org
+ - stability@mozilla.org
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12
+ data_sensitivity:
+ - technical
+ expires: never
+ send_in_pings:
+ - crash
+
+ process_type:
+ type: string
+ # yamllint disable
+ description: >
+ The type of process that experienced a crash. See the full list of
+ options
+ [here](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/data/crash-ping.html#process-types).
+ # yamllint enable
+ notification_emails:
+ - crash-reporting-wg@mozilla.org
+ - stability@mozilla.org
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12
+ data_sensitivity:
+ - technical
+ expires: never
+ send_in_pings:
+ - crash
+
+ remote_type:
+ type: string
+ description: >
+ Type of the child process, can be set to "web", "file" or "extension" but could also be unavailable.
+ notification_emails:
+ - crash-reporting-wg@mozilla.org
+ - stability@mozilla.org
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1851518
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1851518#c6
+ data_sensitivity:
+ - technical
+ expires: never
+ send_in_pings:
+ - crash
+
+ time:
+ type: datetime
+ time_unit: minute
+ description: >
+ The time at which the crash occurred.
+ notification_emails:
+ - crash-reporting-wg@mozilla.org
+ - stability@mozilla.org
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12
+ data_sensitivity:
+ - technical
+ expires: never
+ send_in_pings:
+ - crash
+
+ startup:
+ type: boolean
+ description: >
+ If true, the crash occurred during process startup.
+ notification_emails:
+ - crash-reporting-wg@mozilla.org
+ - stability@mozilla.org
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12
+ data_sensitivity:
+ - technical
+ expires: never
+ send_in_pings:
+ - crash
+
+ cause:
+ type: string
+ description: >
+ The cause of the crash. May be one of `os_fault` or `java_exception`.
+ notification_emails:
+ - crash-reporting-wg@mozilla.org
+ - stability@mozilla.org
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1839697
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1839697#c5
+ data_sensitivity:
+ - technical
+ expires: never
+ send_in_pings:
+ - crash
diff --git a/mobile/android/android-components/components/lib/crash/pings.yaml b/mobile/android/android-components/components/lib/crash/pings.yaml
new file mode 100644
index 0000000000..620e185872
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/pings.yaml
@@ -0,0 +1,28 @@
+# 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/.
+
+---
+$schema: moz://mozilla.org/schemas/glean/pings/2-0-0
+
+crash:
+ description: >
+ A ping to report crash information. This information is sent as soon as
+ possible after a crash occurs (whether the crash is a background/content
+ process or the main process). It is expected to be used for crash report
+ analysis and to reduce blind spots in crash reporting.
+ include_client_id: true
+ send_if_empty: false
+ notification_emails:
+ - crash-reporting-wg@mozilla.org
+ - stability@mozilla.org
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12
+ reasons:
+ crash: >
+ A process crashed and a ping was immediately sent.
+ event_found: >
+ A process crashed and produced a crash event, which was later found and
+ sent in a ping.
diff --git a/mobile/android/android-components/components/lib/crash/proguard-rules.pro b/mobile/android/android-components/components/lib/crash/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/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/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json b/mobile/android/android-components/components/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json
new file mode 100644
index 0000000000..7ecfe0bbd3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json
@@ -0,0 +1,84 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "212dfa0b59d6a78d81e65cead34d40e0",
+ "entities": [
+ {
+ "tableName": "crashes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `stacktrace` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`uuid`))",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "stacktrace",
+ "columnName": "stacktrace",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "uuid"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "reports",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `crash_uuid` TEXT NOT NULL, `service_id` TEXT NOT NULL, `report_id` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "crashUuid",
+ "columnName": "crash_uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "serviceId",
+ "columnName": "service_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "reportId",
+ "columnName": "report_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '212dfa0b59d6a78d81e65cead34d40e0')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..3e5c8f7da1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+ <application android:supportsRtl="true">
+ <activity android:name=".prompt.CrashReporterActivity"
+ android:process=":mozilla.components.lib.crash.CrashReporter"
+ android:exported="false"
+ android:excludeFromRecents="true"
+ android:theme="@style/Theme.Mozac.CrashReporter" />
+
+ <service android:name=".handler.CrashHandlerService"
+ android:process=":mozilla.components.lib.crash.CrashHandler"
+ android:exported="false"
+ android:foregroundServiceType="specialUse">
+ <property
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="This foreground service allows users to report crashes" />
+ </service>
+
+ <!-- Separate process to avoid starting the application when starting this service -->
+ <service android:name=".service.SendCrashReportService"
+ android:process=":crashReportingProcess"
+ android:exported="false"
+ android:foregroundServiceType="specialUse">
+ <property
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="This foreground service allows users to report crashes" />
+ </service>
+
+ <!-- Separate process to avoid starting the application when starting this service -->
+ <service android:name=".service.SendCrashTelemetryService"
+ android:process=":crashReportingProcess"
+ android:exported="false"
+ android:foregroundServiceType="specialUse">
+ <property
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="This foreground service allows users to report crashes" />
+ </service>
+ </application>
+
+</manifest>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt
new file mode 100644
index 0000000000..db064e7a6c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt
@@ -0,0 +1,175 @@
+/* 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.lib.crash
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.annotation.StringDef
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.support.utils.ext.getParcelableArrayListCompat
+import mozilla.components.support.utils.ext.getSerializableCompat
+import java.io.Serializable
+import java.util.UUID
+
+// Intent extra used to store crash data under when passing crashes in Intent objects
+private const val INTENT_CRASH = "mozilla.components.lib.crash.CRASH"
+
+// Uncaught exception crash intent extras
+private const val INTENT_EXCEPTION = "exception"
+
+// Breadcrumbs intent extras
+private const val INTENT_BREADCRUMBS = "breadcrumbs"
+
+// Crash timestamp intent extras
+private const val INTENT_CRASH_TIMESTAMP = "crashTimestamp"
+
+// Native code crash intent extras (Mirroring GeckoView values)
+private const val INTENT_UUID = "uuid"
+private const val INTENT_MINIDUMP_PATH = "minidumpPath"
+private const val INTENT_EXTRAS_PATH = "extrasPath"
+private const val INTENT_MINIDUMP_SUCCESS = "minidumpSuccess"
+private const val INTENT_PROCESS_TYPE = "processType"
+private const val INTENT_REMOTE_TYPE = "remoteType"
+
+/**
+ * Crash types that are handled by this library.
+ */
+sealed class Crash {
+ /**
+ * Unique ID identifying this crash.
+ */
+ abstract val uuid: String
+
+ /**
+ * A crash caused by an uncaught exception.
+ *
+ * @property timestamp Time of when the crash happened.
+ * @property throwable The [Throwable] that caused the crash.
+ * @property breadcrumbs List of breadcrumbs to send with the crash report.
+ */
+ data class UncaughtExceptionCrash(
+ val timestamp: Long,
+ val throwable: Throwable,
+ val breadcrumbs: ArrayList<Breadcrumb>,
+ override val uuid: String = UUID.randomUUID().toString(),
+ ) : Crash() {
+ override fun toBundle() = Bundle().apply {
+ putString(INTENT_UUID, uuid)
+ putSerializable(INTENT_EXCEPTION, throwable as Serializable)
+ putLong(INTENT_CRASH_TIMESTAMP, timestamp)
+ putParcelableArrayList(INTENT_BREADCRUMBS, breadcrumbs)
+ }
+
+ companion object {
+ internal fun fromBundle(bundle: Bundle) = UncaughtExceptionCrash(
+ uuid = bundle.getString(INTENT_UUID) as String,
+ throwable = bundle.getSerializableCompat(INTENT_EXCEPTION, Throwable::class.java) as Throwable,
+ breadcrumbs = bundle.getParcelableArrayListCompat(INTENT_BREADCRUMBS, Breadcrumb::class.java)
+ ?: arrayListOf(),
+ timestamp = bundle.getLong(INTENT_CRASH_TIMESTAMP, System.currentTimeMillis()),
+ )
+ }
+ }
+
+ /**
+ * A crash that happened in native code.
+ *
+ * @property timestamp Time of when the crash happened.
+ * @property minidumpPath Path to a Breakpad minidump file containing information about the crash.
+ * @property minidumpSuccess Indicating whether or not the crash dump was successfully retrieved. If this is false,
+ * the dump file may be corrupted or incomplete.
+ * @property extrasPath Path to a file containing extra metadata about the crash. The file contains key-value pairs
+ * in the form `Key=Value`. Be aware, it may contain sensitive data such as the URI that was
+ * loaded at the time of the crash.
+ * @property processType The type of process the crash occurred in. Affects whether or not the crash is fatal
+ * or whether the application can recover from it.
+ * @property breadcrumbs List of breadcrumbs to send with the crash report.
+ * @property remoteType The type of child process (when available).
+ */
+ data class NativeCodeCrash(
+ val timestamp: Long,
+ val minidumpPath: String?,
+ val minidumpSuccess: Boolean,
+ val extrasPath: String?,
+ @ProcessType val processType: String?,
+ val breadcrumbs: ArrayList<Breadcrumb>,
+ val remoteType: String?,
+ override val uuid: String = UUID.randomUUID().toString(),
+ ) : Crash() {
+ override fun toBundle() = Bundle().apply {
+ putString(INTENT_UUID, uuid)
+ putString(INTENT_MINIDUMP_PATH, minidumpPath)
+ putBoolean(INTENT_MINIDUMP_SUCCESS, minidumpSuccess)
+ putString(INTENT_EXTRAS_PATH, extrasPath)
+ putString(INTENT_PROCESS_TYPE, processType)
+ putLong(INTENT_CRASH_TIMESTAMP, timestamp)
+ putParcelableArrayList(INTENT_BREADCRUMBS, breadcrumbs)
+ putString(INTENT_REMOTE_TYPE, remoteType)
+ }
+
+ /**
+ * Whether the crash was fatal or not: If true, the main application process was affected by
+ * the crash. If false, only an internal process used by Gecko has crashed and the application
+ * may be able to recover.
+ */
+ val isFatal: Boolean
+ get() = processType == PROCESS_TYPE_MAIN
+
+ companion object {
+ /**
+ * Indicates a crash occurred in the main process and is therefore fatal.
+ */
+ const val PROCESS_TYPE_MAIN = "MAIN"
+
+ /**
+ * Indicates a crash occurred in a foreground child process. The application may be
+ * able to recover from this crash, but it was likely noticeable to the user.
+ */
+ const val PROCESS_TYPE_FOREGROUND_CHILD = "FOREGROUND_CHILD"
+
+ /**
+ * Indicates a crash occurred in a background child process. This should have been
+ * recovered from automatically, and will have had minimal impact to the user, if any.
+ */
+ const val PROCESS_TYPE_BACKGROUND_CHILD = "BACKGROUND_CHILD"
+
+ @StringDef(PROCESS_TYPE_MAIN, PROCESS_TYPE_FOREGROUND_CHILD, PROCESS_TYPE_BACKGROUND_CHILD)
+ @Retention(AnnotationRetention.SOURCE)
+ annotation class ProcessType
+
+ internal fun fromBundle(bundle: Bundle) = NativeCodeCrash(
+ uuid = bundle.getString(INTENT_UUID) ?: UUID.randomUUID().toString(),
+ minidumpPath = bundle.getString(INTENT_MINIDUMP_PATH, null),
+ minidumpSuccess = bundle.getBoolean(INTENT_MINIDUMP_SUCCESS, false),
+ extrasPath = bundle.getString(INTENT_EXTRAS_PATH, null),
+ processType = bundle.getString(INTENT_PROCESS_TYPE, PROCESS_TYPE_MAIN),
+ breadcrumbs = bundle.getParcelableArrayListCompat(INTENT_BREADCRUMBS, Breadcrumb::class.java)
+ ?: arrayListOf(),
+ remoteType = bundle.getString(INTENT_REMOTE_TYPE, null),
+ timestamp = bundle.getLong(INTENT_CRASH_TIMESTAMP, System.currentTimeMillis()),
+ )
+ }
+ }
+
+ internal abstract fun toBundle(): Bundle
+
+ internal fun fillIn(intent: Intent) {
+ intent.putExtra(INTENT_CRASH, toBundle())
+ }
+
+ companion object {
+ fun fromIntent(intent: Intent): Crash {
+ val bundle = intent.getBundleExtra(INTENT_CRASH)!!
+
+ return if (bundle.containsKey(INTENT_MINIDUMP_PATH)) {
+ NativeCodeCrash.fromBundle(bundle)
+ } else {
+ UncaughtExceptionCrash.fromBundle(bundle)
+ }
+ }
+
+ fun isCrashIntent(intent: Intent) = intent.extras?.containsKey(INTENT_CRASH) ?: false
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt
new file mode 100644
index 0000000000..74daaef197
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt
@@ -0,0 +1,376 @@
+/* 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.lib.crash
+
+import android.app.ActivityOptions
+import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.annotation.StyleRes
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.lib.crash.db.CrashDatabase
+import mozilla.components.lib.crash.db.insertCrashSafely
+import mozilla.components.lib.crash.db.insertReportSafely
+import mozilla.components.lib.crash.db.toEntity
+import mozilla.components.lib.crash.db.toReportEntity
+import mozilla.components.lib.crash.handler.ExceptionHandler
+import mozilla.components.lib.crash.notification.CrashNotification
+import mozilla.components.lib.crash.prompt.CrashPrompt
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.lib.crash.service.CrashTelemetryService
+import mozilla.components.lib.crash.service.SendCrashReportService
+import mozilla.components.lib.crash.service.SendCrashTelemetryService
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Stores a list of `Breadcrumb` objects for the crash reporter.
+ *
+ * This is shared between multiple threads and needs to be thread-safe.
+ */
+private class BreadcrumbList(val maxBreadCrumbs: Int) {
+ private val breadcrumbs = ArrayDeque<Breadcrumb>()
+
+ @Synchronized
+ internal fun copy(): ArrayList<Breadcrumb> {
+ return ArrayList<Breadcrumb>(breadcrumbs)
+ }
+
+ @Synchronized
+ internal fun add(breadcrumb: Breadcrumb) {
+ if (breadcrumbs.size >= maxBreadCrumbs) {
+ breadcrumbs.removeFirst()
+ }
+ breadcrumbs.add(breadcrumb)
+ }
+}
+
+/**
+ *
+ * A generic crash reporter that can report crashes to multiple services.
+ *
+ * In the `onCreate()` method of your Application class create a `CrashReporter` instance and call `install()`:
+ *
+ * ```Kotlin
+ * CrashReporter(
+ * services = listOf(
+ * // List the crash reporting services you want to use
+ * )
+ * ).install(this)
+ * ```
+ *
+ * With this minimal setup the crash reporting library will capture "uncaught exception" crashes and "native code"
+ * crashes and forward them to the configured crash reporting services.
+ *
+ * @property enabled Enable/Disable crash reporting.
+ *
+ * @param services List of crash reporting services that should receive crash reports.
+ * @param telemetryServices List of telemetry crash reporting services that should receive crash reports.
+ * @param shouldPrompt Whether or not the user should be prompted to confirm sending crash reports.
+ * @param enabled Enable/Disable crash reporting.
+ * @param promptConfiguration Configuration for customizing the crash reporter prompt.
+ * @param nonFatalCrashIntent A [PendingIntent] that will be launched if a non fatal crash (main process not affected)
+ * happened. This gives the app the opportunity to show an in-app confirmation UI before
+ * sending a crash report. See component README for details.
+ */
+class CrashReporter(
+ context: Context,
+ private val services: List<CrashReporterService> = emptyList(),
+ private val telemetryServices: List<CrashTelemetryService> = emptyList(),
+ private val shouldPrompt: Prompt = Prompt.NEVER,
+ var enabled: Boolean = true,
+ internal val promptConfiguration: PromptConfiguration = PromptConfiguration(),
+ private val nonFatalCrashIntent: PendingIntent? = null,
+ private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+ private val maxBreadCrumbs: Int = 30,
+ private val notificationsDelegate: NotificationsDelegate,
+) : CrashReporting {
+ private val database: CrashDatabase by lazy { CrashDatabase.get(context) }
+
+ internal val logger = Logger("mozac/CrashReporter")
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ private val crashBreadcrumbs = BreadcrumbList(maxBreadCrumbs)
+
+ init {
+ if (services.isEmpty() and telemetryServices.isEmpty()) {
+ throw IllegalArgumentException("No crash reporter services defined")
+ }
+ }
+
+ /**
+ * Install this [CrashReporter] instance. At this point the component will be setup to collect crash reports.
+ */
+ fun install(applicationContext: Context): CrashReporter {
+ instance = this
+
+ val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
+ val handler = ExceptionHandler(applicationContext, this, defaultHandler)
+ Thread.setDefaultUncaughtExceptionHandler(handler)
+
+ return this
+ }
+
+ /**
+ * Get a copy of the crashBreadcrumbs
+ */
+ fun crashBreadcrumbsCopy(): ArrayList<Breadcrumb> {
+ return crashBreadcrumbs.copy()
+ }
+
+ /**
+ * Submit a crash report to all registered services.
+ */
+ fun submitReport(crash: Crash, then: () -> Unit = {}): Job {
+ return scope.launch {
+ services.forEach { service ->
+ val reportId = when (crash) {
+ is Crash.NativeCodeCrash -> service.report(crash)
+ is Crash.UncaughtExceptionCrash -> service.report(crash)
+ }
+
+ if (reportId != null) {
+ database.crashDao().insertReportSafely(service.toReportEntity(crash, reportId))
+ }
+
+ val reportUrl = reportId?.let { service.createCrashReportUrl(it) }
+
+ logger.info("Submitted crash to ${service.name} (id=$reportId, url=$reportUrl)")
+ }
+
+ logger.info("Crash report submitted to ${services.size} services")
+ withContext(Dispatchers.Main) {
+ then()
+ }
+ }
+ }
+
+ /**
+ * Submit a crash report to all registered telemetry services.
+ */
+ fun submitCrashTelemetry(crash: Crash, then: () -> Unit = {}): Job {
+ return scope.launch {
+ telemetryServices.forEach { telemetryService ->
+ when (crash) {
+ is Crash.NativeCodeCrash -> telemetryService.record(crash)
+ is Crash.UncaughtExceptionCrash -> telemetryService.record(crash)
+ }
+ }
+
+ logger.info("Crash report submitted to ${telemetryServices.size} telemetry services")
+ withContext(Dispatchers.Main) {
+ then()
+ }
+ }
+ }
+
+ /**
+ * Submit a caught exception report to all registered services.
+ */
+ override fun submitCaughtException(throwable: Throwable): Job {
+ /*
+ * if stacktrace is empty, replace throwable with UnexpectedlyMissingStacktrace exception so
+ * we can figure out which module is submitting caught exception reports without a stacktrace.
+ */
+ var reportThrowable = throwable
+ if (throwable.stackTrace.isEmpty()) {
+ reportThrowable = CrashReporterException.UnexpectedlyMissingStacktrace("Missing Stacktrace", throwable)
+ }
+
+ logger.info("Caught Exception report submitted to ${services.size} services")
+ return scope.launch {
+ services.forEach {
+ it.report(reportThrowable, crashBreadcrumbsCopy())
+ }
+ }
+ }
+
+ /**
+ * Add a crash breadcrumb to all registered services with breadcrumb support.
+ *
+ * ```Kotlin
+ * crashReporter.recordCrashBreadcrumb(
+ * Breadcrumb("Settings button clicked", data, "UI", Level.INFO, Type.USER)
+ * )
+ * ```
+ */
+ override fun recordCrashBreadcrumb(breadcrumb: Breadcrumb) {
+ crashBreadcrumbs.add(breadcrumb)
+ }
+
+ internal fun onCrash(context: Context, crash: Crash) {
+ if (!enabled) {
+ return
+ }
+
+ logger.info("Received crash: $crash")
+
+ database.crashDao().insertCrashSafely(crash.toEntity())
+
+ if (telemetryServices.isNotEmpty()) {
+ sendCrashTelemetry(context, crash)
+ }
+
+ // If crash is native code and non fatal then the view will handle the user prompt
+ if (shouldSendIntent(crash)) {
+ // App has registered a pending intent
+ sendNonFatalCrashIntent(context, crash)
+ return
+ }
+
+ if (services.isNotEmpty()) {
+ if (CrashPrompt.shouldPromptForCrash(shouldPrompt, crash)) {
+ showPromptOrNotification(context, crash)
+ } else {
+ sendCrashReport(context, crash)
+ }
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun sendNonFatalCrashIntent(context: Context, crash: Crash) {
+ logger.info("Invoking non-fatal PendingIntent")
+
+ val additionalIntent = Intent()
+ crash.fillIn(additionalIntent)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ val onFinished = null
+ val handler = null
+ val requiredPermission = null
+ val activityOptions = ActivityOptions.makeBasic()
+ activityOptions.pendingIntentBackgroundActivityStartMode =
+ MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+
+ nonFatalCrashIntent?.send(
+ context,
+ 0,
+ additionalIntent,
+ onFinished,
+ handler,
+ requiredPermission,
+ activityOptions.toBundle(),
+ )
+ } else {
+ nonFatalCrashIntent?.send(context, 0, additionalIntent)
+ }
+ }
+
+ private fun showPromptOrNotification(context: Context, crash: Crash) {
+ if (services.isEmpty()) {
+ return
+ }
+
+ if (CrashNotification.shouldShowNotificationInsteadOfPrompt(crash)) {
+ // If this is a fatal crash taking down the app then we may not be able to show a crash reporter
+ // prompt on Android Q+. Unfortunately it's not possible to easily determine if we can launch an
+ // activity here. So instead we fallback to just showing a notification
+ // https://developer.android.com/preview/privacy/background-activity-starts
+ logger.info("Showing notification")
+ val notification = CrashNotification(context, crash, promptConfiguration, notificationsDelegate)
+ notification.show()
+ } else {
+ logger.info("Showing prompt")
+ showPrompt(context, crash)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun sendCrashReport(context: Context, crash: Crash) {
+ ContextCompat.startForegroundService(context, SendCrashReportService.createReportIntent(context, crash))
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun sendCrashTelemetry(context: Context, crash: Crash) {
+ ContextCompat.startForegroundService(context, SendCrashTelemetryService.createReportIntent(context, crash))
+ }
+
+ @VisibleForTesting
+ internal fun showPrompt(context: Context, crash: Crash) {
+ val prompt = CrashPrompt(context, crash)
+ prompt.show()
+ }
+
+ private fun shouldSendIntent(crash: Crash): Boolean {
+ return if (nonFatalCrashIntent == null) {
+ // If the app has not registered any intent then we can't send one.
+ false
+ } else {
+ // If this is a native code crash in a foreground child process then we can recover
+ // and can notify the app. Background child process crashes will be recovered from
+ // automatically, and main process crashes cannot be recovered from, so we do not
+ // send the intent for those.
+ crash is Crash.NativeCodeCrash && crash.processType == Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD
+ }
+ }
+
+ internal fun getCrashReporterServiceById(id: String): CrashReporterService? {
+ return services.firstOrNull { it.id == id }
+ }
+
+ enum class Prompt {
+ /**
+ * Never prompt the user. Always submit crash reports immediately.
+ */
+ NEVER,
+
+ /**
+ * Only prompt the user for native code crashes.
+ */
+ ONLY_NATIVE_CRASH,
+
+ /**
+ * Always prompt the user for confirmation before sending crash reports.
+ */
+ ALWAYS,
+ }
+
+ /**
+ * Configuration for the crash reporter prompt.
+ */
+ data class PromptConfiguration(
+ internal val appName: String = "App",
+ internal val organizationName: String = "Mozilla",
+ internal val message: String? = null,
+ @StyleRes internal val theme: Int = R.style.Theme_Mozac_CrashReporter,
+ )
+
+ companion object {
+ @Volatile
+ private var instance: CrashReporter? = null
+
+ @VisibleForTesting
+ internal fun reset() {
+ instance = null
+ }
+
+ internal val requireInstance: CrashReporter
+ get() = instance ?: throw IllegalStateException(
+ "You need to call install() on your CrashReporter instance from Application.onCreate().",
+ )
+ }
+}
+
+/**
+ * A base class for exceptions describing crash reporter exception.
+ */
+internal abstract class CrashReporterException(message: String, cause: Throwable?) : Exception(message, cause) {
+ /**
+ * Stacktrace was expected to be present, but it wasn't.
+ */
+ internal class UnexpectedlyMissingStacktrace(
+ message: String,
+ cause: Throwable?,
+ ) : CrashReporterException(message, cause)
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.kt
new file mode 100644
index 0000000000..48dcf7aefe
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.kt
@@ -0,0 +1,78 @@
+/* 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.lib.crash.db
+
+import android.annotation.SuppressLint
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import java.lang.Exception
+
+/**
+ * Dao for saving and accessing crash related information.
+ */
+@Dao
+internal interface CrashDao {
+ /**
+ * Inserts a crash into the database.
+ */
+ @Insert
+ fun insertCrash(crash: CrashEntity): Long
+
+ /**
+ * Inserts a report to the database.
+ */
+ @Insert
+ fun insertReport(report: ReportEntity): Long
+
+ /**
+ * Returns saved crashes with their reports.
+ */
+ @Transaction
+ @Query("SELECT * FROM crashes ORDER BY created_at DESC")
+ fun getCrashesWithReports(): LiveData<List<CrashWithReports>>
+
+ /**
+ * Delete table.
+ */
+ @Transaction
+ @Query("DELETE FROM crashes")
+ fun deleteAll()
+}
+
+/**
+ * Insert crash into database safely, ignoring any exceptions.
+ *
+ * When handling a crash we want to avoid causing another crash when writing to the database. In the
+ * case of an error we will just ignore it and continue without saving to the database.
+ */
+@SuppressLint("LogUsage") // We do not want to use our custom logger while handling the crash
+@Suppress("TooGenericExceptionCaught")
+internal fun CrashDao.insertCrashSafely(entity: CrashEntity) {
+ try {
+ insertCrash(entity)
+ } catch (e: Exception) {
+ Log.e("CrashDao", "Failed to insert crash into database", e)
+ }
+}
+
+/**
+ * Insert report into database safely, ignoring any exceptions.
+ *
+ * When handling a crash we want to avoid causing another crash when writing to the database. In the
+ * case of an error we will just ignore it and continue without saving to the database.
+ */
+@SuppressLint("LogUsage") // We do not want to use our custom logger while handling the crash
+@Suppress("TooGenericExceptionCaught")
+internal fun CrashDao.insertReportSafely(entity: ReportEntity) {
+ try {
+ insertReport(entity)
+ } catch (e: Exception) {
+ Log.e("CrashDao", "Failed to insert report into database", e)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt
new file mode 100644
index 0000000000..2a7ac4e4e3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt
@@ -0,0 +1,40 @@
+/* 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.lib.crash.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+/**
+ * Internal database for storing collections and their tabs.
+ */
+@Database(
+ entities = [CrashEntity::class, ReportEntity::class],
+ version = 1,
+)
+internal abstract class CrashDatabase : RoomDatabase() {
+ abstract fun crashDao(): CrashDao
+
+ companion object {
+ @Volatile private var instance: CrashDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): CrashDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(context.applicationContext, CrashDatabase::class.java, "crashes")
+ // We are allowing main thread queries here since we need to write to disk blocking
+ // in a crash event before the process gets shutdown. At this point the app already
+ // crashed and temporarily blocking the UI thread is not that problematic anymore.
+ .allowMainThreadQueries()
+ .build()
+ .also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt
new file mode 100644
index 0000000000..26ba0b0991
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt
@@ -0,0 +1,54 @@
+/* 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.lib.crash.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.lib.crash.Crash
+import mozilla.components.support.base.ext.getStacktraceAsString
+
+/**
+ * Database entity modeling a crash that has happened.
+ */
+@Entity(
+ tableName = "crashes",
+)
+internal data class CrashEntity(
+ /**
+ * Generated UUID for this crash.
+ */
+ @PrimaryKey
+ @ColumnInfo(name = "uuid")
+ var uuid: String,
+
+ /**
+ * The stacktrace of the crash (if this crash was caused by an exception/throwable): otherwise
+ * a string describing the type of crash.
+ */
+ @ColumnInfo(name = "stacktrace")
+ var stacktrace: String,
+
+ /**
+ * Timestamp (in milliseconds) of when the crash happened.
+ */
+ @ColumnInfo(name = "created_at")
+ var createdAt: Long,
+)
+
+internal fun Crash.toEntity(): CrashEntity {
+ return CrashEntity(
+ uuid = uuid,
+ stacktrace = getStacktrace(),
+ createdAt = System.currentTimeMillis(),
+ )
+}
+
+internal fun Crash.getStacktrace(): String {
+ return when (this) {
+ is Crash.NativeCodeCrash -> "<native crash>"
+ is Crash.UncaughtExceptionCrash -> throwable.getStacktraceAsString()
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt
new file mode 100644
index 0000000000..079b283168
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt
@@ -0,0 +1,22 @@
+/* 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.lib.crash.db
+
+import androidx.room.Embedded
+import androidx.room.Relation
+
+/**
+ * Data class modelling the relationship between [CrashEntity] and [ReportEntity] objects.
+ */
+internal data class CrashWithReports(
+ @Embedded
+ val crash: CrashEntity,
+
+ @Relation(
+ parentColumn = "uuid",
+ entityColumn = "crash_uuid",
+ )
+ val reports: List<ReportEntity>,
+)
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt
new file mode 100644
index 0000000000..5136a8526e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt
@@ -0,0 +1,52 @@
+/* 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.lib.crash.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.service.CrashReporterService
+
+/**
+ * Datanase entry describing a crash report that was sent to a crash reporting service.
+ */
+@Entity(
+ tableName = "reports",
+)
+internal data class ReportEntity(
+ /**
+ * Database internal primary key of the entry.
+ */
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var id: Long? = null,
+
+ /**
+ * UUID of the crash that was reported.
+ */
+ @ColumnInfo(name = "crash_uuid")
+ var crashUuid: String,
+
+ /**
+ * Id of the service the crash was reported to (matching [CrashReporterService.id].
+ */
+ @ColumnInfo(name = "service_id")
+ var serviceId: String,
+
+ /**
+ * The id of the crash report as returned by [CrashReporterService.report].
+ */
+ @ColumnInfo(name = "report_id")
+ var reportId: String,
+)
+
+internal fun CrashReporterService.toReportEntity(crash: Crash, reportId: String): ReportEntity {
+ return ReportEntity(
+ crashUuid = crash.uuid,
+ serviceId = id,
+ reportId = reportId,
+ )
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt
new file mode 100644
index 0000000000..2c21298412
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt
@@ -0,0 +1,76 @@
+/* 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.lib.crash.handler
+
+import android.app.Service
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.core.app.NotificationCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.R
+import mozilla.components.lib.crash.notification.CrashNotification
+import mozilla.components.support.base.ids.SharedIdsHelper
+
+private const val NOTIFICATION_TAG = "mozac.lib.crash.handlecrash"
+
+/**
+ * Service receiving native code crashes (from GeckoView).
+ */
+class CrashHandlerService : Service() {
+ private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent != null) {
+ crashReporter.logger.error("CrashHandlerService received native code crash")
+ handleCrashIntent(intent)
+ } else {
+ crashReporter.logger.error("CrashHandlerService received a null intent unable to handle")
+ }
+ return START_NOT_STICKY
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ // We don't provide binding, so return null
+ return null
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun handleCrashIntent(
+ intent: Intent,
+ scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = CrashNotification.ensureChannelExists(this)
+ val notification = NotificationCompat.Builder(this, channel)
+ .setContentTitle(
+ getString(R.string.mozac_lib_gathering_crash_data_in_progress),
+ )
+ .setSmallIcon(R.drawable.mozac_lib_crash_notification)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+ .setAutoCancel(true)
+ .build()
+
+ val notificationId = SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG)
+ startForeground(notificationId, notification)
+ }
+
+ scope.launch {
+ intent.extras?.let { extras ->
+ val crash = Crash.NativeCodeCrash.fromBundle(extras)
+ CrashReporter.requireInstance.onCrash(this@CrashHandlerService, crash)
+ } ?: crashReporter.logger.error("Received intent with null extras")
+
+ stopSelf()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt
new file mode 100644
index 0000000000..1c764da8fe
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt
@@ -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/. */
+
+package mozilla.components.lib.crash.handler
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Process
+import android.util.Log
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+
+private const val TAG = "ExceptionHandler"
+
+/**
+ * [Thread.UncaughtExceptionHandler] implementation that forwards crashes to the [CrashReporter] instance.
+ */
+class ExceptionHandler(
+ private val context: Context,
+ private val crashReporter: CrashReporter,
+ private val defaultExceptionHandler: Thread.UncaughtExceptionHandler? = null,
+) : Thread.UncaughtExceptionHandler {
+ private var crashing = false
+
+ @SuppressLint("LogUsage") // We do not want to use our custom logger while handling the crash
+ override fun uncaughtException(thread: Thread, throwable: Throwable) {
+ Log.e(TAG, "Uncaught exception handled: ", throwable)
+
+ if (crashing) {
+ return
+ }
+
+ // We want to catch and log all exceptions that can take down the crash reporter.
+ // This is the best we can do without being able to report it.
+ @Suppress("TooGenericExceptionCaught")
+ try {
+ crashing = true
+
+ crashReporter.onCrash(
+ context,
+ Crash.UncaughtExceptionCrash(
+ timestamp = System.currentTimeMillis(),
+ throwable = throwable,
+ breadcrumbs = crashReporter.crashBreadcrumbsCopy(),
+ ),
+ )
+
+ defaultExceptionHandler?.uncaughtException(thread, throwable)
+ } catch (e: Exception) {
+ Log.e(TAG, "Crash reporter has crashed.", e)
+ } finally {
+ terminateProcess()
+ }
+ }
+
+ private fun terminateProcess() {
+ Process.killProcess(Process.myPid())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt
new file mode 100644
index 0000000000..73b4c0c789
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt
@@ -0,0 +1,121 @@
+/* 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.lib.crash.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.R
+import mozilla.components.lib.crash.prompt.CrashPrompt
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.utils.PendingIntentUtils
+
+private const val NOTIFICATION_SDK_LEVEL = 29 // On Android Q+ we show a notification instead of a prompt
+
+internal const val NOTIFICATION_TAG = "mozac.lib.crash.notification"
+internal const val NOTIFICATION_ID = 1
+private const val NOTIFICATION_CHANNEL_ID = "mozac.lib.crash.channel"
+private const val PENDING_INTENT_TAG = "mozac.lib.crash.pendingintent"
+
+internal class CrashNotification(
+ private val context: Context,
+ private val crash: Crash,
+ private val configuration: CrashReporter.PromptConfiguration,
+ private val notificationsDelegate: NotificationsDelegate,
+) {
+ fun show() {
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ SharedIdsHelper.getNextIdForTag(context, PENDING_INTENT_TAG),
+ CrashPrompt.createIntent(context, crash),
+ getNotificationFlag(),
+ )
+
+ val channel = ensureChannelExists(context)
+
+ val title = if (crash is Crash.NativeCodeCrash &&
+ crash.processType == Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD
+ ) {
+ context.getString(
+ R.string.mozac_lib_crash_background_process_notification_title,
+ configuration.appName,
+ )
+ } else {
+ context.getString(R.string.mozac_lib_crash_dialog_title, configuration.appName)
+ }
+
+ val notification = NotificationCompat.Builder(context, channel)
+ .setContentTitle(title)
+ .setSmallIcon(R.drawable.mozac_lib_crash_notification)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+ .setContentIntent(pendingIntent)
+ .addAction(
+ R.drawable.mozac_lib_crash_notification,
+ context.getString(
+ R.string.mozac_lib_crash_notification_action_report,
+ ),
+ pendingIntent,
+ )
+ .setAutoCancel(true)
+ .build()
+
+ notificationsDelegate.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification)
+ }
+
+ companion object {
+ /**
+ * Whether to show a notification instead of a prompt (activity). Android introduced restrictions on background
+ * services launching activities in Q+. On those system we may need to show a notification for the given [crash]
+ * and launch the reporter from the notification.
+ */
+ fun shouldShowNotificationInsteadOfPrompt(
+ crash: Crash,
+ sdkLevel: Int = Build.VERSION.SDK_INT,
+ ): Boolean {
+ return when {
+ // We can always launch an activity from a background service pre Android Q.
+ sdkLevel < NOTIFICATION_SDK_LEVEL -> false
+
+ // We may not be able to launch an activity if a background process crash occurs
+ // while the application is in the background.
+ crash is Crash.NativeCodeCrash && crash.processType ==
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD -> true
+
+ // An uncaught exception is crashing the app and we may not be able to launch an activity from here.
+ crash is Crash.UncaughtExceptionCrash -> true
+
+ // This is a fatal native crash. We may not be able to launch an activity from here.
+ else -> crash is Crash.NativeCodeCrash && crash.isFatal
+ }
+ }
+
+ fun ensureChannelExists(context: Context): String {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager: NotificationManager = context.getSystemService(
+ Context.NOTIFICATION_SERVICE,
+ ) as NotificationManager
+
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.mozac_lib_crash_channel),
+ NotificationManager.IMPORTANCE_DEFAULT,
+ )
+
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ return NOTIFICATION_CHANNEL_ID
+ }
+ }
+
+ private fun getNotificationFlag() = PendingIntentUtils.defaultFlags
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt
new file mode 100644
index 0000000000..dcdf2a7a2e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt
@@ -0,0 +1,48 @@
+/* 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.lib.crash.prompt
+
+import android.content.Context
+import android.content.Intent
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+
+internal class CrashPrompt(
+ private val context: Context,
+ private val crash: Crash,
+) {
+ fun show() {
+ context.startActivity(createIntent(context, crash))
+ }
+
+ companion object {
+ fun createIntent(context: Context, crash: Crash): Intent {
+ val intent = Intent(context, CrashReporterActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+ // For background process native crashes we want to keep the browser visible in the
+ // background behind the prompt. For other types we want to clear the existing task.
+ if (crash is Crash.NativeCodeCrash &&
+ crash.processType == Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD
+ ) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ } else {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ }
+
+ crash.fillIn(intent)
+
+ return intent
+ }
+
+ fun shouldPromptForCrash(shouldPrompt: CrashReporter.Prompt, crash: Crash): Boolean {
+ return when (shouldPrompt) {
+ CrashReporter.Prompt.ALWAYS -> true
+ CrashReporter.Prompt.NEVER -> false
+ CrashReporter.Prompt.ONLY_NATIVE_CRASH -> crash is Crash.NativeCodeCrash
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt
new file mode 100644
index 0000000000..fad866f139
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt
@@ -0,0 +1,158 @@
+/* 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.lib.crash.prompt
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.appcompat.app.AppCompatActivity
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.app.NotificationManagerCompat
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.R
+import mozilla.components.lib.crash.databinding.MozacLibCrashCrashreporterBinding
+import mozilla.components.lib.crash.notification.CrashNotification
+import mozilla.components.lib.crash.notification.NOTIFICATION_ID
+import mozilla.components.lib.crash.notification.NOTIFICATION_TAG
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * Activity showing the crash reporter prompt asking the user for confirmation before submitting a crash report.
+ */
+class CrashReporterActivity : AppCompatActivity() {
+
+ private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance }
+ private val crash: Crash by lazy { Crash.fromIntent(intent) }
+ private val sharedPreferences by lazy {
+ getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ }
+
+ /**
+ * Coroutine context for crash reporter operations. Can be used to setup dispatcher for tests.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var reporterCoroutineContext: CoroutineContext = EmptyCoroutineContext
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal lateinit var binding: MozacLibCrashCrashreporterBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ // if the activity is started by user tapping on the crash notification's report button,
+ // remove the crash notification.
+ if (CrashNotification.shouldShowNotificationInsteadOfPrompt(crash)) {
+ NotificationManagerCompat.from(applicationContext).cancel(NOTIFICATION_TAG, NOTIFICATION_ID)
+ }
+
+ setTheme(crashReporter.promptConfiguration.theme)
+
+ super.onCreate(savedInstanceState)
+
+ binding = MozacLibCrashCrashreporterBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setupViews()
+ }
+
+ private fun setupViews() {
+ val appName = crashReporter.promptConfiguration.appName
+ val organizationName = crashReporter.promptConfiguration.organizationName
+
+ binding.titleView.text = when (isRecoverableBackgroundCrash(crash)) {
+ true -> getString(
+ R.string.mozac_lib_crash_background_process_notification_title,
+ appName,
+ )
+ false -> getString(R.string.mozac_lib_crash_dialog_title, appName)
+ }
+
+ binding.sendCheckbox.text = getString(R.string.mozac_lib_crash_dialog_checkbox, organizationName)
+ binding.sendCheckbox.isChecked = sharedPreferences.getBoolean(PREFERENCE_KEY_SEND_REPORT, true)
+
+ binding.restartButton.apply {
+ text = getString(R.string.mozac_lib_crash_dialog_button_restart, appName)
+ setOnClickListener { restart() }
+ }
+ binding.closeButton.setOnClickListener { close() }
+
+ // For background crashes show just the close button. Otherwise show close and restart.
+ if (isRecoverableBackgroundCrash(crash)) {
+ binding.restartButton.visibility = View.GONE
+ val closeButtonParams = binding.closeButton.layoutParams as ConstraintLayout.LayoutParams
+ closeButtonParams.startToStart = ConstraintLayout.LayoutParams.UNSET
+ closeButtonParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
+ } else {
+ binding.restartButton.visibility = View.VISIBLE
+ val closeButtonParams = binding.closeButton.layoutParams as ConstraintLayout.LayoutParams
+ closeButtonParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
+ closeButtonParams.endToEnd = ConstraintLayout.LayoutParams.UNSET
+ }
+
+ if (crashReporter.promptConfiguration.message == null) {
+ binding.messageView.visibility = View.GONE
+ } else {
+ binding.messageView.text = crashReporter.promptConfiguration.message
+ }
+ }
+
+ private fun close() {
+ sendCrashReportIfNeeded {
+ finish()
+ }
+ }
+
+ private fun restart() {
+ sendCrashReportIfNeeded {
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
+ if (launchIntent != null) {
+ launchIntent.flags = launchIntent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
+ startActivity(launchIntent)
+ }
+
+ finish()
+ }
+ }
+
+ private fun sendCrashReportIfNeeded(then: () -> Unit) {
+ sharedPreferences.edit().putBoolean(PREFERENCE_KEY_SEND_REPORT, binding.sendCheckbox.isChecked).apply()
+
+ if (!binding.sendCheckbox.isChecked) {
+ then()
+ return
+ }
+
+ crashReporter.submitReport(crash) {
+ then()
+ }
+ }
+
+ override fun onBackPressed() {
+ sendCrashReportIfNeeded {
+ finish()
+ }
+ }
+
+ /*
+ * Return true if the crash occurred in the background and is recoverable. (ex: GPU process crash)
+ */
+ @VisibleForTesting
+ internal fun isRecoverableBackgroundCrash(crash: Crash): Boolean {
+ return crash is Crash.NativeCodeCrash &&
+ crash.processType == Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD
+ }
+
+ companion object {
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal const val SHARED_PREFERENCES_NAME = "mozac_lib_crash_settings"
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal const val PREFERENCE_KEY_SEND_REPORT = "sendCrashReport"
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt
new file mode 100644
index 0000000000..698d33128b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt
@@ -0,0 +1,54 @@
+/* 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.lib.crash.service
+
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+
+const val LIB_CRASH_INFO_PREFIX = "[INFO]"
+
+/**
+ * Interface to be implemented by external services that accept crash reports.
+ */
+interface CrashReporterService {
+ /**
+ * A unique ID to identify this crash reporter service.
+ */
+ val id: String
+
+ /**
+ * A human-readable name for this crash reporter service (to be displayed in UI).
+ */
+ val name: String
+
+ /**
+ * Returns a URL to a website with the crash report if possible. Otherwise returns null.
+ */
+ fun createCrashReportUrl(identifier: String): String?
+
+ /**
+ * Submits a crash report for this [Crash.UncaughtExceptionCrash].
+ *
+ * @return Unique crash report identifier that can be used by/with this crash reporter service
+ * to find this reported crash - or null if no identifier can be provided.
+ */
+ fun report(crash: Crash.UncaughtExceptionCrash): String?
+
+ /**
+ * Submits a crash report for this [Crash.NativeCodeCrash].
+ *
+ * @return Unique crash report identifier that can be used by/with this crash reporter service
+ * to find this reported crash - or null if no identifier can be provided.
+ */
+ fun report(crash: Crash.NativeCodeCrash): String?
+
+ /**
+ * Submits a caught exception report for this [Throwable].
+ *
+ * @return Unique crash report identifier that can be used by/with this crash reporter service
+ * to find this reported crash - or null if no identifier can be provided.
+ */
+ fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String?
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt
new file mode 100644
index 0000000000..ce85fee75e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt
@@ -0,0 +1,27 @@
+/* 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.lib.crash.service
+
+import mozilla.components.lib.crash.Crash
+
+/**
+ * Interface to be implemented by external services that collect telemetry about crash reports.
+ */
+interface CrashTelemetryService {
+ /**
+ * Records telemetry for this [Crash.UncaughtExceptionCrash].
+ */
+ fun record(crash: Crash.UncaughtExceptionCrash)
+
+ /**
+ * Records telemetry for this [Crash.NativeCodeCrash].
+ */
+ fun record(crash: Crash.NativeCodeCrash)
+
+ /**
+ * Records telemetry for this caught [Throwable] (non-crash).
+ */
+ fun record(throwable: Throwable)
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt
new file mode 100644
index 0000000000..be6b411816
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt
@@ -0,0 +1,312 @@
+/* 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.lib.crash.service
+
+import android.content.Context
+import android.os.SystemClock
+import androidx.annotation.VisibleForTesting
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.DecodeSequenceMode
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeToSequence
+import kotlinx.serialization.json.encodeToStream
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.GleanMetrics.CrashMetrics
+import mozilla.components.lib.crash.GleanMetrics.Pings
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.content.isMainProcess
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.Date
+import mozilla.components.lib.crash.GleanMetrics.Crash as GleanCrash
+
+/**
+ * A [CrashReporterService] implementation for recording metrics with Glean. The purpose of this
+ * crash reporter is to collect crash count metrics by capturing [Crash.UncaughtExceptionCrash],
+ * [Throwable] and [Crash.NativeCodeCrash] events and record to the respective
+ * [mozilla.components.service.glean.private.CounterMetricType].
+ */
+class GleanCrashReporterService(
+ val context: Context,
+ @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val file: File = File(context.applicationInfo.dataDir, CRASH_FILE_NAME),
+) : CrashTelemetryService {
+ companion object {
+ // This file is stored in the application's data directory, so it should be located in the
+ // same location as the application.
+ // The format of this file is simple and uses the keys named below, one per line, to record
+ // crashes. That format allows for multiple crashes to be appended to the file if, for some
+ // reason, the application cannot run and record them.
+ const val CRASH_FILE_NAME = "glean_crash_counts"
+
+ // These keys correspond to the labels found for crashCount metric in metrics.yaml as well
+ // as the persisted crashes in the crash count file (see above comment)
+ const val UNCAUGHT_EXCEPTION_KEY = "uncaught_exception"
+ const val CAUGHT_EXCEPTION_KEY = "caught_exception"
+ const val MAIN_PROCESS_NATIVE_CODE_CRASH_KEY = "main_proc_native_code_crash"
+ const val FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY = "fg_proc_native_code_crash"
+ const val BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY = "bg_proc_native_code_crash"
+
+ // These keys are deprecated and should be removed after a period to allow for persisted
+ // crashes to be submitted.
+ const val FATAL_NATIVE_CODE_CRASH_KEY = "fatal_native_code_crash"
+ const val NONFATAL_NATIVE_CODE_CRASH_KEY = "nonfatal_native_code_crash"
+ }
+
+ /**
+ * The subclasses of GleanCrashAction are used to persist Glean actions to handle them later
+ * (in the application which has Glean initialized). They are serialized to JSON objects and
+ * appended to a file, in case multiple crashes occur prior to being able to submit the metrics
+ * to Glean.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ @Serializable
+ internal sealed class GleanCrashAction {
+ /**
+ * Submit the glean metrics/pings.
+ */
+ abstract fun submit()
+
+ @Serializable
+ @SerialName("count")
+ data class Count(val label: String) : GleanCrashAction() {
+ override fun submit() {
+ CrashMetrics.crashCount[label].add()
+ }
+ }
+
+ @Serializable
+ @SerialName("ping")
+ data class Ping(
+ val uptimeNanos: Long,
+ val processType: String,
+ val timeMillis: Long,
+ val startup: Boolean,
+ val reason: Pings.crashReasonCodes,
+ val cause: String = "os_fault",
+ val remoteType: String = "",
+ ) : GleanCrashAction() {
+ override fun submit() {
+ GleanCrash.uptime.setRawNanos(uptimeNanos)
+ GleanCrash.processType.set(processType)
+ GleanCrash.remoteType.set(remoteType)
+ GleanCrash.time.set(Date(timeMillis))
+ GleanCrash.startup.set(startup)
+ GleanCrash.cause.set(cause)
+ Pings.crash.submit(reason)
+ }
+ }
+ }
+
+ private val logger = Logger("glean/GleanCrashReporterService")
+ private val creationTime = SystemClock.elapsedRealtimeNanos()
+
+ init {
+ run {
+ // We only want to record things on the main process because that is the only one in which
+ // Glean is properly initialized. Checking to see if we are on the main process here will
+ // prevent the situation that arises because the integrating app's Application will be
+ // re-created when prompting to report the crash, and Glean is not initialized there since
+ // it's not technically the main process.
+ if (!context.isMainProcess()) {
+ logger.info("GleanCrashReporterService initialized off of main process")
+ return@run
+ }
+
+ if (!checkFileConditions()) {
+ // checkFileConditions() internally logs error conditions
+ return@run
+ }
+
+ // Parse the persisted crashes
+ parseCrashFile()
+
+ // Clear persisted counts by deleting the file
+ file.delete()
+ }
+ }
+
+ /**
+ * Calculates the application uptime based on the creation time of this class (assuming it is
+ * created in the application's `OnCreate`).
+ */
+ private fun uptime() = SystemClock.elapsedRealtimeNanos() - creationTime
+
+ /**
+ * Checks the file conditions to ensure it can be opened and read.
+ *
+ * @return True if the file exists and is able to be read, otherwise false
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun checkFileConditions(): Boolean {
+ return if (!file.exists()) {
+ // This is just an info line, as most of the time we hope there is no file which means
+ // there were no crashes
+ logger.info("No crashes to record, or file not found.")
+ false
+ } else if (!file.canRead()) {
+ logger.error("Cannot read file")
+ false
+ } else if (!file.isFile) {
+ logger.error("Expected file, but found directory")
+ false
+ } else {
+ true
+ }
+ }
+
+ /**
+ * Parses the crashes collected in the persisted crash file. The format of this file is simple,
+ * a stream of serialized JSON GleanCrashAction objects.
+ *
+ * Example:
+ *
+ * <--Beginning of file-->
+ * {"type":"count","label":"uncaught_exception"}\n
+ * {"type":"count","label":"uncaught_exception"}\n
+ * {"type":"count","label":"main_process_native_code_crash"}\n
+ * {"type":"ping","uptimeNanos":2000000,"processType":"main","timeMillis":42000000000,
+ * "startup":false}\n
+ * <--End of file-->
+ *
+ * It is unlikely that there will be more than one crash in a file, but not impossible. This
+ * could happen, for instance, if the application crashed again before the file could be
+ * processed.
+ */
+ @Suppress("ComplexMethod")
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun parseCrashFile() {
+ try {
+ @OptIn(ExperimentalSerializationApi::class)
+ val actionSequence = Json.decodeToSequence<GleanCrashAction>(
+ file.inputStream(),
+ DecodeSequenceMode.WHITESPACE_SEPARATED,
+ )
+ for (action in actionSequence) {
+ action.submit()
+ }
+ } catch (e: IOException) {
+ logger.error("Error reading crash file", e)
+ return
+ } catch (e: SerializationException) {
+ logger.error("Error deserializing crash file", e)
+ return
+ }
+ }
+
+ /**
+ * This function handles the actual recording of the crash to the persisted crash file. We are
+ * only guaranteed runtime for the lifetime of the [CrashReporterService.report] function,
+ * anything that we do in this function **MUST** be synchronous and blocking. We cannot spawn
+ * work to background processes or threads here if we want to guarantee that the work is
+ * completed. Also, since the [CrashReporterService.report] functions are called synchronously,
+ * and from lib-crash's own process, it is unlikely that this would be called from more than one
+ * place at the same time.
+ *
+ * @param action Pass in the crash action to record.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun recordCrashAction(action: GleanCrashAction) {
+ // Persist the crash in a file so that it can be recorded on the next application start. We
+ // cannot directly record to Glean here because CrashHandler process is not the same process
+ // as Glean is initialized in.
+ // Create the file if it doesn't exist
+ if (!file.exists()) {
+ try {
+ file.createNewFile()
+ } catch (e: IOException) {
+ logger.error("Failed to create crash file", e)
+ }
+ }
+
+ // Add a line representing the crash that was received
+ if (file.canWrite()) {
+ try {
+ @OptIn(ExperimentalSerializationApi::class)
+ Json.encodeToStream(action, FileOutputStream(file, true))
+ file.appendText("\n")
+ } catch (e: IOException) {
+ logger.error("Failed to write to crash file", e)
+ }
+ }
+ }
+
+ override fun record(crash: Crash.UncaughtExceptionCrash) {
+ recordCrashAction(GleanCrashAction.Count(UNCAUGHT_EXCEPTION_KEY))
+ recordCrashAction(
+ GleanCrashAction.Ping(
+ uptimeNanos = uptime(),
+ processType = "main",
+ remoteType = "",
+ timeMillis = crash.timestamp,
+ startup = false,
+ reason = Pings.crashReasonCodes.crash,
+ cause = "java_exception",
+ ),
+ )
+ }
+
+ override fun record(crash: Crash.NativeCodeCrash) {
+ when (crash.processType) {
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN ->
+ recordCrashAction(GleanCrashAction.Count(MAIN_PROCESS_NATIVE_CODE_CRASH_KEY))
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD ->
+ recordCrashAction(
+ GleanCrashAction.Count(
+ FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY,
+ ),
+ )
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD ->
+ recordCrashAction(
+ GleanCrashAction.Count(
+ BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY,
+ ),
+ )
+ }
+
+ // The `processType` property on a crash is a bit confusing because it does not map to the actual process types
+ // (like main, content, gpu, etc.). This property indicates what UI we should show to users given that "main"
+ // crashes essentially kill the app, "foreground child" crashes are likely tab crashes, and "background child"
+ // crashes are occurring in other processes (like GPU and extensions) for which users shouldn't notice anything
+ // (because there shouldn't be any noticeable impact in the app and the processes will be recreated
+ // automatically).
+ val processType = when (crash.processType) {
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN -> "main"
+
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD -> {
+ when (crash.remoteType) {
+ // The extensions process is a content process as per:
+ // https://firefox-source-docs.mozilla.org/dom/ipc/process_model.html#webextensions
+ "extension" -> "content"
+
+ else -> "utility"
+ }
+ }
+
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD -> "content"
+
+ else -> "main"
+ }
+ recordCrashAction(
+ GleanCrashAction.Ping(
+ uptimeNanos = uptime(),
+ processType = processType,
+ remoteType = crash.remoteType ?: "",
+ timeMillis = crash.timestamp,
+ startup = false,
+ reason = Pings.crashReasonCodes.crash,
+ cause = "os_fault",
+ ),
+ )
+ }
+
+ override fun record(throwable: Throwable) {
+ recordCrashAction(GleanCrashAction.Count(CAUGHT_EXCEPTION_KEY))
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt
new file mode 100644
index 0000000000..df50580b0c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt
@@ -0,0 +1,566 @@
+/* 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.lib.crash.service
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.pm.PackageInfoCompat
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.support.base.ext.getStacktraceAsJsonString
+import mozilla.components.support.base.ext.getStacktraceAsString
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.ext.getPackageInfoCompat
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.BufferedReader
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileReader
+import java.io.IOException
+import java.io.InputStreamReader
+import java.io.OutputStream
+import java.net.HttpURLConnection
+import java.net.URL
+import java.nio.channels.Channels
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+import java.util.zip.GZIPOutputStream
+import kotlin.random.Random
+import mozilla.components.Build as AcBuild
+
+/* This ID is used for all Mozilla products. Setting as default if no ID is passed in */
+private const val MOZILLA_PRODUCT_ID = "{eeb82917-e434-4870-8148-5c03d4caa81b}"
+
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal const val CAUGHT_EXCEPTION_TYPE = "caught exception"
+internal const val UNCAUGHT_EXCEPTION_TYPE = "uncaught exception"
+internal const val FATAL_NATIVE_CRASH_TYPE = "fatal native crash"
+internal const val NON_FATAL_NATIVE_CRASH_TYPE = "non-fatal native crash"
+
+internal const val DEFAULT_VERSION_NAME = "N/A"
+internal const val DEFAULT_VERSION_CODE = "N/A"
+internal const val DEFAULT_VERSION = "N/A"
+internal const val DEFAULT_BUILD_ID = "N/A"
+internal const val DEFAULT_VENDOR = "N/A"
+internal const val DEFAULT_RELEASE_CHANNEL = "N/A"
+internal const val DEFAULT_DISTRIBUTION_ID = "N/A"
+
+private const val KEY_CRASH_ID = "CrashID"
+
+private const val MINI_DUMP_FILE_EXT = "dmp"
+private const val EXTRAS_FILE_EXT = "extra"
+private const val FILE_REGEX = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\."
+
+/**
+ * A [CrashReporterService] implementation uploading crash reports to crash-stats.mozilla.com.
+ *
+ * @param applicationContext The application [Context].
+ * @param appName A human-readable app name. This name is used on crash-stats.mozilla.com to filter crashes by app.
+ * The name needs to be safelisted for the server to accept the crash.
+ * [File a bug](https://bugzilla.mozilla.org/enter_bug.cgi?product=Socorro) if you would like to get your
+ * app added to the safelist.
+ * @param appId The application ID assigned by Socorro server.
+ * @param version The engine version.
+ * @param buildId The engine build ID.
+ * @param vendor The application vendor name.
+ * @param serverUrl The URL of the server.
+ * @param versionName The version of the application.
+ * @param versionCode The version code of the application.
+ * @param releaseChannel The release channel of the application.
+ * @param distributionId The distribution id of the application.
+ */
+@Suppress("LargeClass")
+class MozillaSocorroService(
+ private val applicationContext: Context,
+ private val appName: String,
+ private val appId: String = MOZILLA_PRODUCT_ID,
+ private val version: String = DEFAULT_VERSION,
+ private val buildId: String = DEFAULT_BUILD_ID,
+ private val vendor: String = DEFAULT_VENDOR,
+ @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var serverUrl: String? = null,
+ private var versionName: String = DEFAULT_VERSION_NAME,
+ private var versionCode: String = DEFAULT_VERSION_CODE,
+ private val releaseChannel: String = DEFAULT_RELEASE_CHANNEL,
+ private val distributionId: String = DEFAULT_DISTRIBUTION_ID,
+) : CrashReporterService {
+ private val logger = Logger("mozac/MozillaSocorroCrashHelperService")
+ private val startTime = System.currentTimeMillis()
+ private val ignoreKeys = hashSetOf("URL", "ServerURL", "StackTraces")
+
+ override val id: String = "socorro"
+
+ override val name: String = "Socorro"
+
+ override fun createCrashReportUrl(identifier: String): String? {
+ return "https://crash-stats.mozilla.org/report/index/$identifier"
+ }
+
+ init {
+ val packageInfo = try {
+ applicationContext.packageManager.getPackageInfoCompat(applicationContext.packageName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ logger.error("package name not found, failed to get application version")
+ null
+ }
+
+ packageInfo?.let {
+ if (versionName == DEFAULT_VERSION_NAME) {
+ try {
+ versionName = packageInfo.versionName ?: DEFAULT_VERSION_NAME
+ } catch (e: IllegalStateException) {
+ logger.error("failed to get application version")
+ }
+ }
+
+ if (versionCode == DEFAULT_VERSION_CODE) {
+ try {
+ versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString()
+ } catch (e: IllegalStateException) {
+ logger.error("failed to get application version code")
+ }
+ }
+ }
+
+ if (serverUrl == null) {
+ serverUrl = Uri.parse("https://crash-reports.mozilla.com/submit")
+ .buildUpon()
+ .appendQueryParameter("id", appId)
+ .appendQueryParameter("version", versionName)
+ .appendQueryParameter("android_component_version", AcBuild.version)
+ .build().toString()
+ }
+ }
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? {
+ return sendReport(
+ crash.timestamp,
+ crash.throwable,
+ miniDumpFilePath = null,
+ extrasFilePath = null,
+ isNativeCodeCrash = false,
+ isFatalCrash = true,
+ breadcrumbs = crash.breadcrumbs,
+ )
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ return sendReport(
+ crash.timestamp,
+ throwable = null,
+ miniDumpFilePath = crash.minidumpPath,
+ extrasFilePath = crash.extrasPath,
+ isNativeCodeCrash = true,
+ isFatalCrash = crash.isFatal,
+ breadcrumbs = crash.breadcrumbs,
+ )
+ }
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? {
+ /* Not sending caught exceptions to Socorro */
+ return null
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun sendReport(
+ timestamp: Long,
+ throwable: Throwable?,
+ miniDumpFilePath: String?,
+ extrasFilePath: String?,
+ isNativeCodeCrash: Boolean,
+ isFatalCrash: Boolean,
+ breadcrumbs: ArrayList<Breadcrumb>,
+ ): String? {
+ val url = URL(serverUrl)
+ val boundary = generateBoundary()
+ var conn: HttpURLConnection? = null
+
+ val breadcrumbsJson = JSONArray()
+ for (breadcrumb in breadcrumbs) {
+ breadcrumbsJson.put(breadcrumb.toJson())
+ }
+
+ try {
+ conn = url.openConnection() as HttpURLConnection
+ conn.requestMethod = "POST"
+ conn.doOutput = true
+ conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary")
+ conn.setRequestProperty("Content-Encoding", "gzip")
+
+ sendCrashData(
+ conn.outputStream, boundary, timestamp, throwable, miniDumpFilePath, extrasFilePath,
+ isNativeCodeCrash, isFatalCrash, breadcrumbsJson.toString(),
+ )
+
+ BufferedReader(InputStreamReader(conn.inputStream)).use { reader ->
+ val map = parseResponse(reader)
+
+ val id = map?.get(KEY_CRASH_ID)
+
+ if (id != null) {
+ logger.info("Crash reported to Socorro: $id")
+ } else {
+ logger.info("Server rejected crash report")
+ }
+
+ return id
+ }
+ } catch (e: IOException) {
+ try {
+ logger.error("failed to send report to Socorro with " + conn?.responseCode, e)
+ } catch (e: IOException) {
+ logger.error("failed to send report to Socorro", e)
+ }
+
+ return null
+ } finally {
+ conn?.disconnect()
+ }
+ }
+
+ private fun parseResponse(reader: BufferedReader): Map<String, String>? {
+ val map = mutableMapOf<String, String>()
+
+ reader.readLines().forEach { line ->
+ val position = line.indexOf("=")
+ if (position != -1) {
+ val key = line.substring(0, position)
+ val value = unescape(line.substring(position + 1))
+ map[key] = value
+ }
+ }
+
+ return map
+ }
+
+ @Suppress("LongParameterList", "LongMethod", "ComplexMethod")
+ private fun sendCrashData(
+ os: OutputStream,
+ boundary: String,
+ timestamp: Long,
+ throwable: Throwable?,
+ miniDumpFilePath: String?,
+ extrasFilePath: String?,
+ isNativeCodeCrash: Boolean,
+ isFatalCrash: Boolean,
+ breadcrumbs: String,
+ ) {
+ val nameSet = mutableSetOf<String>()
+ val gzipOs = GZIPOutputStream(os)
+ sendPart(gzipOs, boundary, "ProductName", appName, nameSet)
+ sendPart(gzipOs, boundary, "ProductID", appId, nameSet)
+ sendPart(gzipOs, boundary, "Version", versionName, nameSet)
+ sendPart(gzipOs, boundary, "ApplicationBuildID", versionCode, nameSet)
+ sendPart(gzipOs, boundary, "AndroidComponentVersion", AcBuild.version, nameSet)
+ sendPart(gzipOs, boundary, "GleanVersion", AcBuild.gleanSdkVersion, nameSet)
+ sendPart(gzipOs, boundary, "ApplicationServicesVersion", AcBuild.applicationServicesVersion, nameSet)
+ sendPart(gzipOs, boundary, "GeckoViewVersion", version, nameSet)
+ sendPart(gzipOs, boundary, "BuildID", buildId, nameSet)
+ sendPart(gzipOs, boundary, "Vendor", vendor, nameSet)
+ sendPart(gzipOs, boundary, "Breadcrumbs", breadcrumbs, nameSet)
+ sendPart(gzipOs, boundary, "useragent_locale", Locale.getDefault().toLanguageTag(), nameSet)
+ sendPart(gzipOs, boundary, "DistributionID", distributionId, nameSet)
+
+ extrasFilePath?.let {
+ val regex = "$FILE_REGEX$EXTRAS_FILE_EXT".toRegex()
+ if (regex.matchEntire(it.substringAfterLast("/")) != null) {
+ val extrasFile = File(it)
+ val extrasMap = readExtrasFromFile(extrasFile)
+ for (key in extrasMap.keys) {
+ sendPart(gzipOs, boundary, key, extrasMap[key], nameSet)
+ }
+ extrasFile.delete()
+ }
+ }
+
+ if (throwable?.stackTrace?.isEmpty() == false) {
+ sendPart(
+ gzipOs,
+ boundary,
+ "JavaStackTrace",
+ getExceptionStackTrace(
+ throwable,
+ !isNativeCodeCrash && !isFatalCrash,
+ ),
+ nameSet,
+ )
+
+ sendPart(gzipOs, boundary, "JavaException", throwable.getStacktraceAsJsonString(), nameSet)
+ }
+
+ miniDumpFilePath?.let {
+ val regex = "$FILE_REGEX$MINI_DUMP_FILE_EXT".toRegex()
+ if (regex.matchEntire(it.substringAfterLast("/")) != null) {
+ val minidumpFile = File(it)
+ sendFile(gzipOs, boundary, "upload_file_minidump", minidumpFile, nameSet)
+ minidumpFile.delete()
+ }
+ }
+
+ when {
+ isNativeCodeCrash && isFatalCrash ->
+ sendPart(gzipOs, boundary, "CrashType", FATAL_NATIVE_CRASH_TYPE, nameSet)
+ isNativeCodeCrash && !isFatalCrash ->
+ sendPart(gzipOs, boundary, "CrashType", NON_FATAL_NATIVE_CRASH_TYPE, nameSet)
+ !isNativeCodeCrash && isFatalCrash ->
+ sendPart(gzipOs, boundary, "CrashType", UNCAUGHT_EXCEPTION_TYPE, nameSet)
+ !isNativeCodeCrash && !isFatalCrash ->
+ sendPart(gzipOs, boundary, "CrashType", CAUGHT_EXCEPTION_TYPE, nameSet)
+ }
+
+ sendPackageInstallTime(gzipOs, boundary, nameSet)
+ sendProcessName(gzipOs, boundary, nameSet)
+ sendPart(gzipOs, boundary, "ReleaseChannel", releaseChannel, nameSet)
+ sendPart(
+ gzipOs,
+ boundary,
+ "StartupTime",
+ TimeUnit.MILLISECONDS.toSeconds(startTime).toString(),
+ nameSet,
+ )
+ sendPart(
+ gzipOs,
+ boundary,
+ "CrashTime",
+ TimeUnit.MILLISECONDS.toSeconds(timestamp).toString(),
+ nameSet,
+ )
+ sendPart(gzipOs, boundary, "Android_PackageName", applicationContext.packageName, nameSet)
+ sendPart(gzipOs, boundary, "Android_Manufacturer", Build.MANUFACTURER, nameSet)
+ sendPart(gzipOs, boundary, "Android_Model", Build.MODEL, nameSet)
+ sendPart(gzipOs, boundary, "Android_Board", Build.BOARD, nameSet)
+ sendPart(gzipOs, boundary, "Android_Brand", Build.BRAND, nameSet)
+ sendPart(gzipOs, boundary, "Android_Device", Build.DEVICE, nameSet)
+ sendPart(gzipOs, boundary, "Android_Display", Build.DISPLAY, nameSet)
+ sendPart(gzipOs, boundary, "Android_Fingerprint", Build.FINGERPRINT, nameSet)
+ sendPart(gzipOs, boundary, "Android_Hardware", Build.HARDWARE, nameSet)
+ sendPart(
+ gzipOs,
+ boundary,
+ "Android_Version",
+ "${Build.VERSION.SDK_INT} (${Build.VERSION.CODENAME})",
+ nameSet,
+ )
+
+ if (Build.SUPPORTED_ABIS.isNotEmpty()) {
+ sendPart(gzipOs, boundary, "Android_CPU_ABI", Build.SUPPORTED_ABIS[0], nameSet)
+ if (Build.SUPPORTED_ABIS.size >= 2) {
+ sendPart(gzipOs, boundary, "Android_CPU_ABI2", Build.SUPPORTED_ABIS[1], nameSet)
+ }
+ }
+
+ gzipOs.write(("\r\n--$boundary--\r\n").toByteArray())
+ gzipOs.flush()
+ gzipOs.close()
+ }
+
+ private fun sendProcessName(os: OutputStream, boundary: String, nameSet: MutableSet<String>) {
+ val pid = android.os.Process.myPid()
+ val manager = applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ manager.runningAppProcesses.filter { it.pid == pid }.forEach {
+ sendPart(os, boundary, "Android_ProcessName", it.processName, nameSet)
+ return
+ }
+ }
+
+ private fun sendPackageInstallTime(os: OutputStream, boundary: String, nameSet: MutableSet<String>) {
+ val packageManager = applicationContext.packageManager
+ try {
+ val packageInfo = packageManager.getPackageInfoCompat(applicationContext.packageName, 0)
+ sendPart(
+ os,
+ boundary,
+ "InstallTime",
+ TimeUnit.MILLISECONDS.toSeconds(
+ packageInfo.lastUpdateTime,
+ ).toString(),
+ nameSet,
+ )
+ } catch (e: PackageManager.NameNotFoundException) {
+ logger.error("Error getting package info", e)
+ }
+ }
+
+ private fun generateBoundary(): String {
+ val r0 = Random.nextInt(0, Int.MAX_VALUE)
+ val r1 = Random.nextInt(0, Int.MAX_VALUE)
+ return String.format("---------------------------%08X%08X", r0, r1)
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun sendPart(
+ os: OutputStream,
+ boundary: String,
+ name: String,
+ data: String?,
+ nameSet: MutableSet<String>,
+ ) {
+ if (data == null) {
+ return
+ }
+
+ if (nameSet.contains(name)) {
+ return
+ } else {
+ nameSet.add(name)
+ }
+
+ try {
+ os.write(
+ (
+ "--$boundary\r\nContent-Disposition: form-data; " +
+ "name=$name\r\n\r\n$data\r\n"
+ ).toByteArray(),
+ )
+ } catch (e: IOException) {
+ logger.error("Exception when sending $name", e)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun sendFile(
+ os: OutputStream,
+ boundary: String,
+ name: String,
+ file: File,
+ nameSet: MutableSet<String>,
+ ) {
+ if (nameSet.contains(name)) {
+ return
+ } else {
+ nameSet.add(name)
+ }
+
+ try {
+ os.write(
+ (
+ "--${boundary}\r\n" +
+ "Content-Disposition: form-data; name=\"$name\"; " +
+ "filename=\"${file.getName()}\"\r\n" +
+ "Content-Type: application/octet-stream\r\n\r\n"
+ ).toByteArray(),
+ )
+ } catch (e: IOException) {
+ logger.error("failed to write boundary", e)
+ return
+ }
+
+ try {
+ val fileInputStream = FileInputStream(file).channel
+ fileInputStream.transferTo(0, fileInputStream.size(), Channels.newChannel(os))
+ fileInputStream.close()
+ } catch (e: IOException) {
+ logger.error("failed to send file", e)
+ }
+
+ try {
+ // Add EOL to separate from the next part
+ os.write("\r\n".toByteArray())
+ } catch (e: IOException) {
+ logger.error("failed to write EOL", e)
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun unescape(string: String): String {
+ return string.replace("\\\\\\\\", "\\").replace("\\\\n", "\n").replace("\\\\t", "\t")
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun jsonUnescape(string: String): String {
+ return string.replace("""\\\\""", "\\").replace("""\n""", "\n").replace("""\t""", "\t")
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ @Suppress("NestedBlockDepth")
+ internal fun readExtrasFromLegacyFile(file: File): HashMap<String, String> {
+ var fileReader: FileReader? = null
+ var bufReader: BufferedReader? = null
+ var line: String?
+ val map = HashMap<String, String>()
+
+ try {
+ fileReader = FileReader(file)
+ bufReader = BufferedReader(fileReader)
+ line = bufReader.readLine()
+ while (line != null) {
+ val equalsPos = line.indexOf('=')
+ if ((equalsPos) != -1) {
+ val key = line.substring(0, equalsPos)
+ val value = unescape(line.substring(equalsPos + 1))
+ if (!ignoreKeys.contains(key)) {
+ map[key] = value
+ }
+ }
+ line = bufReader.readLine()
+ }
+ } catch (e: IOException) {
+ logger.error("failed to convert extras to map", e)
+ } finally {
+ try {
+ fileReader?.close()
+ bufReader?.close()
+ } catch (e: IOException) {
+ // do nothing
+ }
+ }
+
+ return map
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ @Suppress("NestedBlockDepth")
+ internal fun readExtrasFromFile(file: File): HashMap<String, String> {
+ var resultMap = HashMap<String, String>()
+ var notJson = false
+
+ try {
+ FileReader(file).use { fileReader ->
+ val input = fileReader.readLines().firstOrNull()
+ ?: throw JSONException("failed to read json file")
+
+ val jsonObject = JSONObject(input)
+ for (key in jsonObject.keys()) {
+ if (!key.isNullOrEmpty() && !ignoreKeys.contains(key)) {
+ resultMap[key] = jsonUnescape(jsonObject.getString(key))
+ }
+ }
+ }
+ } catch (e: FileNotFoundException) {
+ logger.error("failed to find extra file", e)
+ } catch (e: IOException) {
+ logger.error("failed read the extra file", e)
+ } catch (e: JSONException) {
+ logger.info("extras file JSON syntax error, trying legacy format")
+ notJson = true
+ }
+
+ if (notJson) {
+ resultMap = readExtrasFromLegacyFile(file)
+ }
+
+ return resultMap
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ // printStackTrace() can throw a NullPointerException exception even if throwable is not null
+ private fun getExceptionStackTrace(throwable: Throwable, isCaughtException: Boolean): String? {
+ return try {
+ when (isCaughtException) {
+ true -> "$LIB_CRASH_INFO_PREFIX ${throwable.getStacktraceAsString()}"
+ false -> throwable.getStacktraceAsString()
+ }
+ } catch (e: NullPointerException) {
+ null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt
new file mode 100644
index 0000000000..5c1d8d18b7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt
@@ -0,0 +1,93 @@
+/* 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.lib.crash.service
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.R
+import mozilla.components.lib.crash.notification.CrashNotification
+import mozilla.components.support.base.ids.SharedIdsHelper
+
+private const val NOTIFICATION_TAG = "mozac.lib.crash.sendcrash"
+private const val NOTIFICATION_ID = 1
+
+@VisibleForTesting(otherwise = PRIVATE)
+internal const val NOTIFICATION_TAG_KEY = "mozac.lib.crash.notification.tag"
+
+@VisibleForTesting(otherwise = PRIVATE)
+internal const val NOTIFICATION_ID_KEY = "mozac.lib.crash.notification.id"
+
+class SendCrashReportService : Service() {
+ private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ intent.getStringExtra(NOTIFICATION_TAG_KEY)?.apply {
+ NotificationManagerCompat.from(applicationContext)
+ .cancel(this, intent.getIntExtra(NOTIFICATION_ID_KEY, 0))
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = CrashNotification.ensureChannelExists(this)
+ val notification = NotificationCompat.Builder(this, channel)
+ .setContentTitle(
+ getString(
+ R.string.mozac_lib_send_crash_report_in_progress,
+ crashReporter.promptConfiguration.organizationName,
+ ),
+ )
+ .setSmallIcon(R.drawable.mozac_lib_crash_notification)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+ .setAutoCancel(true)
+ .setProgress(0, 0, true)
+ .build()
+
+ val notificationId = SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG)
+ startForeground(notificationId, notification)
+ }
+
+ NotificationManagerCompat.from(this).cancel(NOTIFICATION_TAG, NOTIFICATION_ID)
+ val crash = Crash.fromIntent(intent)
+ crashReporter.submitReport(crash) {
+ stopSelf()
+ }
+
+ return START_NOT_STICKY
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ // We don't provide binding, so return null
+ return null
+ }
+
+ companion object {
+ fun createReportIntent(
+ context: Context,
+ crash: Crash,
+ notificationTag: String? = null,
+ notificationId: Int = 0,
+ ): Intent {
+ val intent = Intent(context, SendCrashReportService::class.java)
+
+ notificationTag?.apply {
+ intent.putExtra(NOTIFICATION_TAG_KEY, notificationTag)
+ intent.putExtra(NOTIFICATION_ID_KEY, notificationId)
+ }
+
+ crash.fillIn(intent)
+
+ return intent
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt
new file mode 100644
index 0000000000..1f312911a9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt
@@ -0,0 +1,66 @@
+/* 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.lib.crash.service
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.R
+import mozilla.components.lib.crash.notification.CrashNotification
+import mozilla.components.support.base.ids.SharedIdsHelper
+
+private const val NOTIFICATION_TAG = "mozac.lib.crash.sendtelemetry"
+private const val NOTIFICATION_ID = 1
+
+class SendCrashTelemetryService : Service() {
+ private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = CrashNotification.ensureChannelExists(this)
+ val notification = NotificationCompat.Builder(this, channel)
+ .setContentTitle(
+ getString(R.string.mozac_lib_gathering_crash_telemetry_in_progress),
+ )
+ .setSmallIcon(R.drawable.mozac_lib_crash_notification)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+ .setAutoCancel(true)
+ .setProgress(0, 0, true)
+ .build()
+
+ val notificationId = SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG)
+ startForeground(notificationId, notification)
+ }
+
+ NotificationManagerCompat.from(this).cancel(NOTIFICATION_TAG, NOTIFICATION_ID)
+ val crash = Crash.fromIntent(intent)
+ crashReporter.submitCrashTelemetry(crash) {
+ stopSelf()
+ }
+
+ return START_NOT_STICKY
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ // We don't provide binding, so return null
+ return null
+ }
+
+ companion object {
+ fun createReportIntent(context: Context, crash: Crash): Intent {
+ val intent = Intent(context, SendCrashTelemetryService::class.java)
+ crash.fillIn(intent)
+
+ return intent
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt
new file mode 100644
index 0000000000..eec78c334c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt
@@ -0,0 +1,36 @@
+/* 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.lib.crash.ui
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.R
+
+/**
+ * Activity for displaying the list of reported crashes.
+ */
+abstract class AbstractCrashListActivity : AppCompatActivity() {
+ abstract val crashReporter: CrashReporter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setTitle(R.string.mozac_lib_crash_activity_title)
+
+ if (savedInstanceState == null) {
+ supportFragmentManager.beginTransaction()
+ .add(android.R.id.content, CrashListFragment())
+ .commit()
+ }
+ }
+
+ /**
+ * Gets invoked whenever the user selects a crash reporting service.
+ *
+ * @param url URL pointing to the crash report for the selected crash reporting service.
+ */
+ abstract fun onCrashServiceSelected(url: String)
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt
new file mode 100644
index 0000000000..e3d2b48cba
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt
@@ -0,0 +1,163 @@
+/* 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.lib.crash.ui
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.format.DateUtils
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.R
+import mozilla.components.lib.crash.db.CrashWithReports
+import mozilla.components.lib.crash.db.ReportEntity
+
+/**
+ * RecyclerView adapter for displaying the list of crashes.
+ */
+internal class CrashListAdapter(
+ private val crashReporter: CrashReporter,
+ private val onSelection: (String) -> Unit,
+) : RecyclerView.Adapter<CrashViewHolder>() {
+ private var crashes: List<CrashWithReports> = emptyList()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CrashViewHolder {
+ val view = LayoutInflater.from(
+ parent.context,
+ ).inflate(
+ R.layout.mozac_lib_crash_item_crash,
+ parent,
+ false,
+ )
+
+ return CrashViewHolder(view)
+ }
+
+ override fun getItemCount(): Int {
+ return crashes.size
+ }
+
+ override fun onBindViewHolder(holder: CrashViewHolder, position: Int) {
+ val crashWithReports = crashes[position]
+
+ holder.idView.text = crashWithReports.crash.uuid
+
+ holder.titleView.text = crashWithReports.crash.stacktrace.lines().first()
+
+ val time = DateUtils.getRelativeDateTimeString(
+ holder.footerView.context,
+ crashWithReports.crash.createdAt,
+ DateUtils.MINUTE_IN_MILLIS,
+ DateUtils.WEEK_IN_MILLIS,
+ 0,
+ )
+
+ holder.footerView.text = SpannableStringBuilder(time).apply {
+ append(" - ")
+
+ append(
+ holder.itemView.context.getString(R.string.mozac_lib_crash_share),
+ object : ClickableSpan() {
+ override fun onClick(widget: View) {
+ shareCrash(widget.context, crashWithReports)
+ }
+ },
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
+ )
+
+ if (crashWithReports.reports.isNotEmpty()) {
+ append(" - ")
+ append(crashReporter, crashWithReports.reports, onSelection)
+ }
+ }
+ ViewCompat.enableAccessibleClickableSpanSupport(holder.footerView)
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun updateList(list: List<CrashWithReports>) {
+ crashes = list
+ notifyDataSetChanged()
+ }
+
+ private fun shareCrash(context: Context, crashWithReports: CrashWithReports) {
+ val text = StringBuilder()
+
+ text.append(crashWithReports.crash.uuid)
+ text.appendLine()
+ text.append(crashWithReports.crash.stacktrace.lines().first())
+ text.appendLine()
+
+ crashWithReports.reports.forEach { report ->
+ val service = crashReporter.getCrashReporterServiceById(report.serviceId)
+ text.append(" * ")
+ text.append(service?.name ?: report.serviceId)
+ text.append(": ")
+ text.append(service?.createCrashReportUrl(report.reportId) ?: "<No URL>")
+ text.appendLine()
+ }
+
+ text.append("----")
+ text.appendLine()
+ text.append(crashWithReports.crash.stacktrace)
+ text.appendLine()
+
+ val intent = Intent(Intent.ACTION_SEND)
+ intent.type = "text/plain"
+ intent.putExtra(Intent.EXTRA_TEXT, text.toString())
+ context.startActivity(Intent.createChooser(intent, "Crash"))
+ }
+}
+
+internal class CrashViewHolder(
+ view: View,
+) : RecyclerView.ViewHolder(
+ view,
+) {
+ val titleView = view.findViewById<TextView>(R.id.mozac_lib_crash_title)
+ val idView = view.findViewById<TextView>(R.id.mozac_lib_crash_id)
+ val footerView = view.findViewById<TextView>(R.id.mozac_lib_crash_footer).apply {
+ movementMethod = LinkMovementMethod.getInstance()
+ }
+}
+
+private fun SpannableStringBuilder.append(
+ crashReporter: CrashReporter,
+ services: List<ReportEntity>,
+ onSelection: (String) -> Unit,
+): SpannableStringBuilder {
+ services.forEachIndexed { index, entity ->
+ val service = crashReporter.getCrashReporterServiceById(entity.serviceId)
+ val name = service?.name ?: entity.serviceId
+ val url = service?.createCrashReportUrl(entity.reportId)
+
+ if (url != null) {
+ append(
+ name,
+ object : ClickableSpan() {
+ override fun onClick(widget: View) {
+ onSelection(url)
+ }
+ },
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
+ )
+ } else {
+ append(name)
+ }
+
+ if (index < services.lastIndex) {
+ append(" ")
+ }
+ }
+ return this
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt
new file mode 100644
index 0000000000..4305d2ac16
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt
@@ -0,0 +1,65 @@
+/* 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.lib.crash.ui
+
+import android.database.sqlite.SQLiteBlobTooBigException
+import android.os.Bundle
+import android.view.View
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.lib.crash.R
+import mozilla.components.lib.crash.db.CrashDatabase
+
+/**
+ * Fragment displaying the list of crashes.
+ */
+internal class CrashListFragment : Fragment(R.layout.mozac_lib_crash_crashlist) {
+ private val database by lazy { CrashDatabase.get(requireContext()) }
+ private val reporter by lazy { (activity as AbstractCrashListActivity).crashReporter }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val listView: RecyclerView = view.findViewById(R.id.mozac_lib_crash_list)
+ listView.layoutManager = LinearLayoutManager(
+ requireContext(),
+ LinearLayoutManager.VERTICAL,
+ false,
+ )
+
+ val emptyView = view.findViewById<TextView>(R.id.mozac_lib_crash_empty)
+
+ val adapter = CrashListAdapter(reporter, ::onSelection)
+ listView.adapter = adapter
+
+ val dividerItemDecoration = DividerItemDecoration(
+ requireContext(),
+ LinearLayoutManager.VERTICAL,
+ )
+ listView.addItemDecoration(dividerItemDecoration)
+
+ try {
+ database.crashDao().getCrashesWithReports().observe(
+ viewLifecycleOwner,
+ Observer { list ->
+ if (list.isEmpty()) {
+ emptyView.visibility = View.VISIBLE
+ } else {
+ adapter.updateList(list)
+ }
+ },
+ )
+ } catch (e: SQLiteBlobTooBigException) {
+ /* recover by deleting all entries */
+ database.crashDao().deleteAll()
+ }
+ }
+
+ private fun onSelection(url: String) {
+ (requireActivity() as AbstractCrashListActivity).onCrashServiceSelected(url)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml b/mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml
new file mode 100644
index 0000000000..3eeed541e0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml
@@ -0,0 +1,14 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M3.215,18.106l6.996,-14.004c0.737,-1.475 2.841,-1.475 3.578,0l6.996,14.004A2,2 0,0 1,18.995 21L5.005,21a2,2 0,0 1,-1.79 -2.894zM12,9a1,1 0,0 1,1 1v4a1,1 0,1 1,-2 0v-4a1,1 0,0 1,1 -1zM12,18a1,1 0,1 0,0 -2,1 1,0 0,0 0,2z"
+ android:fillColor="#ffffffff"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml
new file mode 100644
index 0000000000..8754a8ee11
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml
@@ -0,0 +1,23 @@
+<?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/. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/mozac_lib_crash_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <TextView
+ android:id="@+id/mozac_lib_crash_empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:text="@string/mozac_lib_crash_no_crashes"
+ android:textAlignment="center"
+ android:visibility="gone" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml
new file mode 100644
index 0000000000..3214d191c6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml
@@ -0,0 +1,81 @@
+<?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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="8dp">
+
+ <TextView
+ android:id="@+id/titleView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:padding="10dp"
+ android:maxLines="3"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="@string/mozac_lib_crash_dialog_title"
+ style="@style/Base.DialogWindowTitle.AppCompat" />
+
+ <TextView
+ android:id="@+id/messageView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:padding="8dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/titleView"
+ tools:text="As a private browser, we never save and cannot restore your last browsing session." />
+
+ <CheckBox
+ android:id="@+id/sendCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:checked="true"
+ android:padding="10dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/messageView"
+ tools:text="@string/mozac_lib_crash_dialog_checkbox" />
+
+ <Button
+ android:id="@+id/closeButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:text="@string/mozac_lib_crash_dialog_button_close"
+ android:textAlignment="center"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/sendCheckbox"
+ style="@style/Widget.AppCompat.Button.Borderless.Colored" />
+
+ <Button
+ android:id="@+id/restartButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:text="@string/mozac_lib_crash_dialog_button_restart"
+ android:textAlignment="center"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/closeButton"
+ app:layout_constraintTop_toBottomOf="@+id/sendCheckbox"
+ style="@style/Widget.AppCompat.Button.Borderless.Colored" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml
new file mode 100644
index 0000000000..a801b4938e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml
@@ -0,0 +1,40 @@
+<?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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="4dp">
+
+ <TextView
+ android:id="@+id/mozac_lib_crash_id"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="2dp"
+ android:textSize="10sp"
+ tools:text="15b666ae-fc9d-41d1-a5c0-8af6961a22d4"
+ tools:ignore="SmallSp" />
+
+ <TextView
+ android:id="@+id/mozac_lib_crash_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/mozac_lib_crash_id"
+ android:padding="2dp"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:text="java.lang.RuntimeException: Background crash" />
+
+ <TextView
+ android:id="@+id/mozac_lib_crash_footer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/mozac_lib_crash_title"
+ android:padding="2dp"
+ android:textSize="12sp"
+ tools:text="12 minutes ago - Sentry Socorro"/>
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..9555f40a83
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">አዝናለሁ። %1$s ችግር ነበረበት እና ተሰናክሏል።</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">የብልሽት ሪፖርት ወደ %1$s ላክ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ዝጋ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$sን እንደገና ያስጀምሩ</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ብልሽቶች</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">አዝናለሁ። በ%1$s ውስጥ ችግር ተፈጥሯል።</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ሪፖርት ያድርጉ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">የብልሽት ሪፖርት ወደ %1$s በመላክ ላይ</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">የብልሽት ውሂብ በመሰብሰብ ላይ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">የብልሽት ቴሌሜትሪ መረጃን በመሰብሰብ ላይ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">የብልሽት ሪፖርቶች</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ምንም የብልሽት ሪፖርቶች አልገቡም።</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">አጋራ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..baab129de7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">%1$s ha teniu un problema y ha fallau.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Ninviar reporte de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zarrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Reportar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Ninviar reporte de fallos a %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Reportes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No s’ha ninviau garra reporte de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..6e5064e869
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">نأسف. واجه %1$s مشكلة وانهار.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">أرسِل تقرير الانهيار إلى %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">أغلِق</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">أعِد تشغيل %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">الانهيارات</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">أبلِغ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">يُرسل بلاغ الانهيار إلى %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">بلاغات الانهيار</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">لم تُرسل أي بلاغات بانهيار.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">شارك</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..d4d75fadfa
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sentímoslo, %1$s tuvo un problema ya cascó.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Unviar l\'informe del error a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zarrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reaniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Casques</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sentímoslo, prodúxose un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Unviando l\'informe del error a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recoyendo los datos del casque</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recoyendo los datos telemétricos del casque</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de casques</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nun s\'unvió nengún informe de casques.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..c8dfad29d5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Üzr istəyirik. %1$s səyyahında xəta oldu və çökdü.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Çökmə hesabatını %1$s üçün göndər</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Qapat</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s tətbiqini yenidən başlat</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Qəzalar</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Xəbər ver</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Çökmə xəbəri %1$s üçün göndərilir</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Çökmə Hesabatları</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Hələlik heç bir çökmə hesabatı göndərilməyib.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..f17a92f5a0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">باغیشلایین. %1$s موشکولونه اوزلشدی و سیندی.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">سینماق راپورتونو %1$s -یه گؤندر</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">باغلا</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s اَپینی یئنی‌دن باشلات</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">سینماق‌لار</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">باغیشلایین، %1$s اپینده موشکول قاباغا گلدی.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">راپورت</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">سینماق راپورتو %1$s-یا گؤندریلیر</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">سینماق دیتالاری یئغیلیر</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">سینماق تله‌متری دیتاسی یئغیلیر</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">سینماق راپورت‌لاری</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">هئچ بیر سینماق راپورتو گؤندیریلمه‌دی</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">پایلاش</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..f60b516a08
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ampura. %1$s wenten galat lan usak.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Gatra</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bagiang</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..d342233088
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Прабачце. %1$s меў цяжкасці, і адбыўся збой.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Адправіць справаздачу аб краху ў %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Закрыць</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Перазапусціць %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Крахі</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Прабачце. Узнікла праблема ў %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Паведаміць</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Дасыланне справаздачы пра крах у %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Збор дадзеных пра збой</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Збор дадзеных тэлеметрыі аб збоях</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Cправаздачы пра крахі</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ніякага паведамлення аб збоі даслана не было.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Падзяліцца</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..97d7148bf7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Извинявайте. %1$s имаше проблем и се срина.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Изпращане на докладите за срив до %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Затваряне</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Рестартиране на %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Сривове</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Извинявайте. Възникна проблем с %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Докладване</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Изпращане на докладите за срив до %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Събиране на информация за срива</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Събиране на телеметрични данни за срива</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Доклади за срив</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Няма изпратени доклади за срив.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Споделяне</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..e219bb78aa
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">দুঃখিত। %1$s এর একটি সমস্যা ছিল এবং ক্র্যাশ করেছে।</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s এ ক্র্যাশ প্রতিবেদন পাঠান</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">বন্ধ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s পুনরায় চালু করুন</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ক্র্যাশসমূহ</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">প্রতিবেদন</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s এ ক্র্যাশ প্রতিবেদন পাঠানো হচ্ছে</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ক্র্যাশ রিপোর্ট</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">কোনো ক্র্যাশের রিপোর্ট জমা দেওয়া হয়নি।</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">শেয়ার করুন</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..c17ba9bdbc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Digarezit, ur gudenn a zo bet gant %1$s ha sacʼhet eo.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Kas an danevell sacʼhadenn da: %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Serriñ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Adlocʼhañ %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Sacʼhadennoù</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Hon digarezit, degouezhet ez eus bet ur fazi e %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Danevelliñ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">O kas an danevell sacʼhadenn da: %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">O tastum roadennoù ar sac’hadenn</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">O tastum roadennoù telemetrek ar sac’hadenn</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Danevelloù sacʼhadenn</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Danevell sacʼhadenn ebet bet treuzkaset.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Rannañ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..acb7937611
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Oprostite. %1$s je imao problem i srušio se.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Pošalji izvještaj o rušenju %1$s-i</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zatvori</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restartuj %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Rušenja</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Nažalost, došlo je do problema u pozadinskom procesu %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Prijavi</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Šaljem izvještaj o rušenju %1$s-i</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Prikupljanje podataka o padu</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Prikupljanje telemetrijskih podataka o padu</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Izvještaji o rušenju</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nema poslanih izvještaja o rušenju.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Podijeli</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..8e201ba93e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">El %1$s ha tingut un problema i ha fallat.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Envia un informe de fallada a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Tanca</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reinicia el %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallades</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">S\'ha produït un problema al %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informa</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">S’està enviant l’informe de fallada a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">S’estan recollint dades de la fallada</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">S’estan recollint dades de la fallada de telemetria</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de fallada</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No s’ha enviat cap informe de fallada.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Comparteix</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..df611ad6bd
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Kojakuyu\'. %1$s xk\'oje\' jun ruk\'ayewal chuqa\' xsach.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Titaq rutzijol sachoj chi re %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Titz\'apïx</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Titikirisäx chik %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Taq sachoj</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Takuyu\'. Xk\'oje\' jun k\'ayewal pa %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Tiya\' rutzijol</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Nitaq rutzijol sachoj chi re %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Rumolik sachoj taq tzij</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Kimolik taq rutzij rusachoj telemetriya\'</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rutzijol Taq Sachoj</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Man etaqon ta ri taq rutzijol sachoj.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Tikomonïx</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..5b6f44d93c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. Ang %1$s nagproblema ug nicrash</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">i-Padala ang crash report sa %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">i-Close</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">i-Restart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Mga Crash</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Report</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">i-Padala ang crash report sa %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crash Report</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Walay crash report nga ge submit.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">i-Share</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..91d84be3b9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ببورە. %1$s کێشەیەکی هەبوو تێکشکا.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ڕاپۆرتی داخستنی لەناکاو بنێرە بۆ %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">داخستن</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">پێکردنەوی %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">تێکشکانەکان</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ڕاپۆرت</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">ناردنی ڕاپۆرتی تێکشکان بۆ %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ڕاپۆرتی داخستنی لەناکاو</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">هیچ ڕاپۆرتێکی داخستنی لەناکاو نەنێردراوە.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">بڵاوکردنەوە</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..1f2b3fa104
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Per disgrazia, %1$s hà scuntratu un prublema chì hà cagiunatu un accidente.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Mandà un raportu d’accidente à %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Chjode</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Rilancià %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Accidenti</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Per disgrazia, un prublema hè accadutu in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Signalà</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Inviu di u raportu d’accidente à %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Culletta di i dati di l’accidente</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Culletta di i dati di telemetria di l’accidente</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Raporti d’accidente</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Alcunu raportu d’accidente ùn hè statu mandatu.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Sparte</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..95e35643ac
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Promiňte. V aplikaci %1$s nastal problém a spadla.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Poslat hlášení o pádu společnosti %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zavřít</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restartovat aplikaci %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Pády</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">V aplikaci %1$s došlo k chybě.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Nahlásit</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Odesílání hlášení o pádu společnosti %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Shromažďování dat o pádu</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Shromažďování telemetrických dat o pádu</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Hlášení pádů</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Žádná hlášení nebyla odeslána.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Sdílet</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..ee8b79fc90
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ymddiheuriadau. Cafodd %1$s anhawster a chwalu.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Anfon adroddiad chwalu at %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cau</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ailgychwyn %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Chwalfeydd</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ymddiheuriadau. Digwyddodd anhawster yn %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Adrodd</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Anfon adroddiad chwalu at %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Casglu data chwaliadau</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Casglu data telemetreg chwaliadau</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Adroddiadau Chwalu</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Does dim adroddiadau chwalu wedi eu cyflwyno.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Rhannu</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..e4d0ef279e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Beklager, %1$s fik et problem og gik ned.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send fejlrapport til %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Luk</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Genstart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Nedbrud</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Beklager, men der opstod et problem i %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapporter</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sender fejlrapport til %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Indsamler data om nedbrud</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Indsamler telemetri-data om nedbrud</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Fejlrapporter</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Du har ikke indsendt nogen fejlrapporter.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Del</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..54e7c24653
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Es tut uns leid. %1$s hatte ein Problem und ist abgestürzt.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Absturzbericht an %1$s senden</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Schließen</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s neu starten</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Abstürze</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Entschuldigung. Bei %1$s ist ein Problem aufgetreten.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Melden</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Absturzbericht wird an %1$s gesendet</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Absturzdaten werden erfasst</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetriedaten zum Absturz werden gesammelt</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Absturzberichte</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Es wurden noch keine Absturzberichte versendet.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Teilen</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..ba05b962f6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Bóžko %1$s jo měł problem a jo se wowalił.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s rozpšawu wowalenja pósłaś</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zacyniś</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s znowego startowaś</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Wówalenja</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Wódajśo. Problem jo nastał w %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">K wěsći daś</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Wowaleńska rozpšawa se %1$s sćelo</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Daty wowalenja se gromaźe</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetrijowe daty wowalenjow se gromaźe</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rozpšawy wowalenjow</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Rozpšawy wó wowalenjach njejsu se rozpósłali.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Źěliś</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..452732b283
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Λυπούμαστε. Το %1$s αντιμετώπισε πρόβλημα και κατέρρευσε.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Αποστολή αναφοράς κατάρρευσης στη %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Κλείσιμο</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Επανεκκίνηση του %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Καταρρεύσεις</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Δυστυχώς, προέκυψε ένα πρόβλημα στο %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Αναφορά</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Αποστολή αναφοράς κατάρρευσης στη %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Συλλογή δεδομένων κατάρρευσης</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Συλλογή δεδομένων τηλεμετρίας κατάρρευσης</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Αναφορές κατάρρευσης</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Δεν έχουν υποβληθεί αναφορές κατάρρευσης.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Κοινή χρήση</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..38b0b2e647
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. %1$s had a problem and crashed.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send crash report to %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Close</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. A problem occurred in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Report</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sending crash report to %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Gathering crash data</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gathering crash telemetry data</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crash Reports</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No crash reports have been submitted.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Share</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..38b0b2e647
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. %1$s had a problem and crashed.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send crash report to %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Close</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. A problem occurred in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Report</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sending crash report to %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Gathering crash data</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gathering crash telemetry data</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crash Reports</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No crash reports have been submitted.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Share</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..5c7bc08637
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Bedaŭrinde %1$s alfrontis problemon kaj paneis.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Sendi raporton pri paneo al %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Fermi</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restartigi %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Paneoj</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Bedaŭrinde problemo okazis en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raporto</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Raporto pri paneo sendata al %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Kolektado de datumoj pri paneo</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemezuraj datumoj akirataj</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Raportoj pri paneo</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Neniu raporto pri paneo estis sendita.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dividi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..05bfa9734c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Disculpá. %1$s tuvo un problema y falló.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe del fallo a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Disculpá. Ocurrió un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando informe de fallo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilando datos de la colgada</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilación de datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se enviaron informes de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..b4cd64fb0d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Lo sentimos. %1$s tuvo un problema y se cayó.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar reporte de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Lo sentimos. Ocurrió un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Reportar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando reporte de fallos a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilando datos de fallos</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilación de datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Reportes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se ha enviado ningún reporte de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..c367b36eb6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Lo sentimos. Hubo un problema con %1$s y se ha cerrado.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Lo sentimos. Ha ocurrido un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informe</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando informe de fallo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilando datos de fallos</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilando datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se ha enviado ningún informe de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..f68b71a3ef
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Lo sentimos. Hubo un problema con %1$s y se cerró.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Lo siento. Ha ocurrido un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando informe de fallo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilación de datos de errores</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilación de datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Reportes de fallo</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se han enviado reportes de fallo.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..c367b36eb6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Lo sentimos. Hubo un problema con %1$s y se ha cerrado.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Lo sentimos. Ha ocurrido un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informe</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando informe de fallo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilando datos de fallos</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilando datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se ha enviado ningún informe de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..53a9e483d6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Vabandust. %1$sil esines probleem ja see jooksis kokku.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$sle saadetakse vearaport</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Sulge</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Taaskäivita %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Kokkujooksmised</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Vabandust. %1$s esines probleem.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raporteeri</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Vearaporti saatmine %1$sle</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Vearaportid</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ühtegi vearaportit pole saadetud.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Jaga</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..de1d055c87
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Barkatu. %1$s(e)k arazo bat izan du eta huts egin du.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Bidali hutsegite-txostena %1$s(e)ra</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Itxi</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Berrabiarazi %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Hutsegiteak</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Barkatu. Arazo bat gertatu da %1$s(e)n.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Jakinarazi</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Hutsegite-txostena %1$s(e)ra bidaltzen</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Hutsegitearen datuak biltzen</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Hutsegitearen datu telemetrikoak biltzen</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Hutsegite-txostenak</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ez da bidali hutsegite-txostenik.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partekatu</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..bc98d38b0f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">متأسفیم؛ %1$s مشکلی داشته و فروپاشیده است.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ارسال گزارش فروپاشی‌ها به %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">بستن</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">شروع دوبارهٔ %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">فروپاشی‌ها</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">متأسفیم؛ مشکلی در %1$s رخ داد.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">گزارش</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">در حال ارسال گزارش فروپاشی به %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">جمع‌آوری داده‌های فروپاشی</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">جمع‌آوری داده‌های دورسنجی فروپاشی</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">گزارش‌های فروپاشی</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">هیچ گزارش فروپاشی‌ای ارسال نشده است.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">هم‌رسانی</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..0b13934ffe
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Njaafoɗaa. %1$s dañiino caɗeele etee hookii.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Neldu jaŋte kooki e %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Uddu</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Fuɗɗito %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Kooki</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Jaŋtol</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Neldugol jaŋte kooki e %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Jaŋtol Kooke</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Alaa jaŋte hookre neldaa.</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..6a720554ce
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Valitettavasti %1$s kohtasi ongelman ja kaatui.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Lähetä kaatumisraportti %1$slle</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Sulje</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Käynnistä %1$s uudelleen</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Kaatumiset</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">%1$sissa ilmeni ongelma.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Lähetä raportti</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Lähetetään kaatumisraportti %1$slle</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Kerätään kaatumistietoja</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Kerätään kaatumistelemetriatietoja</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Kaatumisraportit</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Kaatumisraportteja ei ole lähetetty.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Jaa</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..87ebf25320
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Désolé, %1$s a rencontré un problème et a planté.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Envoyer le rapport de plantage à %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Fermer</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Redémarrer %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Plantages</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Désolé. Un problème est survenu dans %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Signaler</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Envoi du rapport de plantage à %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Collecte des données de plantage</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Collecte des données de télémétrie du plantage</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rapports de plantage</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Aucun rapport de plantage n’a été envoyé.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partager</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..ac731a80af
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Nus displâs. %1$s al à vût un probleme e al è colassât.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Invie la segnalazion di colàs a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Siere</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Torne invie %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Colàs</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Nus displâs. Al è capitât un probleme in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Segnale</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Daûr a inviâ la segnalazion di colàs a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Daûr a racuei i dâts sui colàs</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Daûr a racuei i dâts di telemetrie dai colàs</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Segnalazions di colàs</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No je stade mandade nissune segnalazion di colàs.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Condivît</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..5d6dfb1db1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry, %1$s hie in probleem en is ferûngelokke.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Ungelokrapport nei %1$s ferstjoere</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Slute</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s opnij starte</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Ungelokken</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. Der is in probleem bard yn %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Melde</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Ungelokrapport nei %1$s ferstjoere</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ungelokgegevens sammelje</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gegevens oer ûngelok-telemetry sammelje</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Ungelokrapporten</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Der binne gjin ûngelokrapporten ynstjoerd.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Diele</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..0451788058
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Tá %1$s tar éis tuairteála.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Seol tuairisc tuairteála chuig %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Dún</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Atosaigh %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Tuairteanna</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Déan Tuairisc</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Tuairisc tuairteála á seoladh chuig %1$s</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..cb41a84614
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Tha sinn duilich ach dh’èirich duilgheadas dha %1$s ’s thuislich e.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Cuir aithisg tuislidh gu %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Dùin</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ath-thòisich %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Tuislidhean</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Tha sinn duilich ach dh’èirich duilgheadas ann an %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Dèan aithris</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">A’ cur aithisg an tuislidh gu %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">A’ cruinneachadh an dàta mun tuisleadh</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">A’ cruinneachadh dàta telemeatraidh mun tuisleadh</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Aithisgean tuislidh</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Cha deach aithisg air tuisleadh a chur.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Co-roinn</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..46b18fd78b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sentímolo. %1$s tivo un problema e fallou.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe de fallo a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Pechar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Quebras</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sentímolo. Ocorreu un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviar informe de quebra a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recompilando datos de quebras</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recompilando datos de telemetría de quebras</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de quebra</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Non se enviou ningún informe de quebra.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..d1e7c70dc6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Rombyasy. %1$s iñapañuãi ha oñemboty.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Emomarandu jejavygua %1$s-pe</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Mboty</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Emoñepyrũjey %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Jejavy</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ambyasy. Oiko apañuãi %1$s-pe.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Momarandu</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Emomarandu jejavygua %1$s-pe</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ombyatyhína mba’ekuaarã javypyre</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetría marandu ñembyaty rehegua</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Marandu Javygua</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ndojeguerahaukái jejavy momarandu.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Moherakuã</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..9bff714e6c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">માફ કરશો. %1$s ને કોઈ સમસ્યા હતી અને ક્રેશ થયું.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$sને ક્રેશ રિપોર્ટ મોકલો</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">બંધ કરો</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ફરીથી શરૂ કરો</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ક્રેશ</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">અહેવાલ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s પર ક્રેશ રિપોર્ટ મોકલી રહ્યાં છીએ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ભંગાણ અહેવાલો</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">કોઈ ભંગાણ અહેવાલો જમા થયેલ નથી.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">શેર કરો</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..6e7bd2fbb6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">क्षमा करें, %1$s में एक त्रुटि उत्पन्न हुई और क्रैश हो गया।</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s को क्रैश की रिपोर्ट भेजें</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">बंद करें</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s को पुनः प्रारंभ करें</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">क्रैश</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">रिपोर्ट</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s को क्रैश रिपोर्ट भेजा जा रहा है</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">क्रैश रिपोर्ट</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">कोई क्रैश रिपोर्ट जमा नहीं किया गया है।</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">साझा करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..749f53f61e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Isarado</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Ibalita</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..f82ee68fca
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Oprosti, %1$s je imao problem i urušio se.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Pošalji izvještaj o rušenju na %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zatvori</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ponovo pokreni %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Rušenja</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Oprostite. Došlo je do problema u %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Prijavi</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Šalje se izvještaj o rušenju na %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Prikupljanje podataka o padu</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Prikupljanje telemetrijskih podataka o padu</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Izvještaji rušenja</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nema poslanih izvještaja rušenja.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Podijeli</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..d838bfa9e4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Bohužel je %1$s problem měł a spadnył.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s spadowu rozprawu pósłać</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Začinić</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s znowa startować</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Spady</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Wodajće. Problem je w %1$s nastał.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Zdźělić</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Spadowa rozprawa so %1$s sćele</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Daty spada so hromadźa</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetrijowe daty spadow so hromadźa</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rozprawy wo spadach</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Rozprawy wo spadach njejsu so rozpósłali.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dźělić</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..e5529da97b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sajnáljuk. A %1$s problémába ütközött és összeomlott.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Összeomlás-jelentés elküldése a %1$s számára</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Bezárás</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s újraindítása</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Összeomlások</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Elnézést. Probléma történt itt: %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Jelentés</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Összeomlás-jelentés elküldése a %1$s számára</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Összeomlási adatok gyűjtése</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Összeomlási telemetriai adatok gyűjtése</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Összeomlásjelentések</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nem volt még beküldve jelentés.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Megosztás</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..b30eed3fce
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ներողություն. %1$s-ը խնդիր ունեցավ և խափանվեց:</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Ուղարկել վթարի զեկույցը %1$sին</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Փակել</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Վերամեկնարկել %1$s-ը</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Վթարներ</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ներողություն. %1$s-ում խնդիր է առաջացել:</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Զեկույց</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Ուղարկել վթարի զեկույցը %1$s-ին</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Վթարի տվյալների հավաքում</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Վթարի հեռաչափության տվյալների հավաքում</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Վթարի զեկույց</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Վթարային ոչ մի հաղորդագրություն չի ուղարկվել:</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Տարածել</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..841dcecd25
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Nos regretta. %1$s habeva un problema e collabeva.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Inviar reporto de crash a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Clauder</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reinitiar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Desolate. Un problema occurreva in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Reportar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Invio de reporto de crash a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Collection datos de crash</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Colligente datos de telemetria de crash</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Reportos de collapso</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nulle reportos de collapso esseva submittite.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..24f7d67e80
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Maaf. %1$s mengalami masalah dan mogok.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Kirim laporan mogok ke %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Tutup</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Mulai Ulang %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Mogok</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Maaf. Terjadi masalah pada %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Laporkan</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Kirim laporan mogok ke %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Mengumpulkan data mogok</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Mengumpulkan data telemetri mogok</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Laporan Kerusakan</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Tidak ada laporan kerusakan yang pernah dikirim.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bagikan</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..e13fe8bef7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Því miður þá lenti %1$s í erfiðleikum og lokaðist.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Senda hrunskýrslu til %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Loka</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Endurræsa %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Hrun</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Því miður. Vandamál kom upp í %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Skýrsla</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Senda hrunaskýrslu til %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Safna hrungögnum</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Safna fjarmælingargögnum um hrun</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Hrunskýrslur</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Engar hrunaskýrslur hafa verið sendar.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Deila</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..3a1cb7deef
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Si è verificato un problema in %1$s che ha provocato un arresto anomalo.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Invia la segnalazione di arresto anomalo a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Chiudi</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Riavvia %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Arresti anomali</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Siamo spiacenti. Si è verificato un problema in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Segnala</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Invio in corso della segnalazione di arresto anomalo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Raccolta dei dati sugli arresti anomali</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Raccolta dei dati di telemetria relativi agli arresti anomali</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Segnalazioni di arresto anomalo</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Non è stata inviata alcuna segnalazione.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Condividi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..36da8bccdb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">קרתה תקלה עם %1$s שהובילה לקריסה. עמך הסליחה.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">שליחת דיווח קריסה אל %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">סגירה</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">הפעלת %1$s מחדש</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">קריסות</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">מצטערים. אירעה שגיאה ב־%1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">דיווח</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">דיווח קריסה נשלח אל %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">בתהליך איסוף נתוני קריסה</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">בתהליך איסוף נתוני קריסה</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">דיווחי קריסה</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">לא נשלחו דיווחי קריסה.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">שיתוף</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..94f58ef947
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">申し訳ありません。%1$s に問題がありクラッシュしました。</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">クラッシュレポートを %1$s へ送信する</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">閉じる</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s を再起動</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">クラッシュ</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">申し訳ありません。%1$s で問題が発生しました。</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">レポート</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">クラッシュレポートを %1$s へ送信しています</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">クラッシュデータを収集しています</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">クラッシュのテレメトリーデータを収集しています</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">クラッシュレポート</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">送信したクラッシュレポートはありません。</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">共有</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..ca895abf3d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ვწუხვართ. %1$s გაუმართაობის გამო გაითიშა.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">გათიშვის მოხსენების გადაგზავნა %1$s-სთვის</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">დახურვა</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">ხელახლა გაეშვას %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">უეცარი გათიშვები</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">სამწუხაროდ, ხარვეზს წააწყდა %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">მოხსენება</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">გათიშვის მოხსენება ეგზავნება %1$s-ს</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">გროვდება გათიშვის მონაცემები</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">აღირიცხება უეცარი გათიშვის მონაცემები</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">გათიშვების მოხსენებები</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">გათიშვების მოხსენებები არ გაგზავნილა.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">გაზიარება</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..3a741dc889
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Keshiresiz. %1$s da mashqala sebepli nasazlıq júz berdi.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Nasazlıq haqqındaǵı maģlıwmattı %1$s ǵa jiberiw</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Jabıw</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s qayta baslaw</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Nasazlıqlar</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Keshiresiz. %1$s da nasazlıq júz berdi.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Xabar berıw</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Nasazlıq esabattı %1$s ǵa jiberilip atır</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Nasazlıq haqqındaǵı maģlıwmatlardı toplaw</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetriyanıń nasazlıq haqqındaǵı maǵlıwmatların jıynaw</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Nasazlıqlar haqqında esabatlar</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Hesh qanday nasazlıq haqqında esabat jiberilmegen</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bólisiw</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..093ac3608f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Nesḥasef. %1$s isεa ugur sakin yeɣli.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Azen aneqqis n uɣelluy i %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Mdel</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ales asenker n %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Aɣelluy</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Nesḥassef. Yeḍra-d wugur deg %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Aneqqis</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Tuzna n uneqqis n uɣelluy i %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Alqaḍ n yisefka yerrẓen</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Alqaḍ n yisefka n tilisɣelt yerrẓen</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Ineqqisen n uɣelluy</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ula d yiwen n uneqqis n uɣelluy ur yettwazen.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bḍu</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..631c4cda4e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Кешіріңіз. %1$s мәселеге тап болды және құлады.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s адресіне құлау жөнінде хабарламаны жіберу</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Жабу</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s қайта қосу</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Құлаулар</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Кешіріңіз. %1$s ішінде қате орын алды.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Хабарлау</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s адресіне құлау жөнінде хабарламаны жіберу</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Құлау деректерін жинау</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Құлау телеметрия деректерін жинау</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Құлау туралы хабарлар</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Құлау туралы ешбір хабар жіберілмеген.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Бөлісу</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..1ac1afcd07
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Bibore. %1$s li rastî pirsgirêkekê hat û têk çû.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Rapora têkçûnê ji %1$s’ê re bişîne</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Bigire</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s’ê dîsa bide destpêkirin</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Têkçûn</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Bibore. Di %1$s`ê de problemek derket.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapor bike</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Rapora têkçûnê ji %1$s’ê re tê şandin</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Agahiyên têkçûnê tên berhevkirin</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Agahiyên têkçûnê tên berhevkirin</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Raporên Têkçûnê</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Heta niha ti raporên têkçûnê nehatine şandin.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Parve bike</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..ec0ced1ab7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ಕ್ಷಮಿಸಿ. %1$s ಸಮಸ್ಯೆ ಮತ್ತು ಕ್ರ್ಯಾಶ್ ಆಗಿದೆ.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ಕ್ರ್ಯಾಶ್ ವರದಿಯನ್ನು %1$s ಗೆ ಕಳುಹಿಸಿ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ಮುಚ್ಚು</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ಅನ್ನು ಮರು ಆರಂಭಿಸು</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ಕುಸಿತಗಳು</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ವರದಿ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">ಕ್ರ್ಯಾಶ್ ವರದಿಯನ್ನು %1$s ಗೆ ಕಳುಹಿಸಿ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ಕ್ರಿಯಾವೈಫಲ್ಯ ವರದಿಗಳು</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ಯಾವುದೆ ಕುಸಿತ ವರದಿಗಳನ್ನು ಸಲ್ಲಿಸಲಾಗಿಲ್ಲ.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ಹಂಚು</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..c3b3ce333e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">죄송합니다. %1$s에 문제가 발생하여 충돌했습니다.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s에 충돌 보고서 보내기</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">닫기</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s 다시 시작</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">충돌</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">죄송합니다. %1$s에서 문제가 발생했습니다.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">보고하기</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s에 충돌 보고서 보내기</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">충돌 데이터 수집 중</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">충돌 원격 분석 데이터 수집 중</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">충돌 보고서</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">전송한 충돌 보고서가 없습니다.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">공유</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..13283a6e5d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ne spiaxe. %1$s o l\'à avuto \'n problema e o s\'é ciantou.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Manda a segnalaçion do cianto a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Særa</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Arvi torna %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Cianti</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Denunçia</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Mando o report do cianto a %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Segnalaçion di Cianti</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nisciun report mandou.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Condividdi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..7be9bbe480
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ຂໍ​ອະໄພ. %1$s ມີປັນຫາ ແລະ ລົ້ມເຫລວ.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ສົ່ງລາຍງານການຂັດຂ້ອງໄປຫາ %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ປິດ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">ລີສຕາດ %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ການຂັດຂ້ອງ</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">ຂໍ​ອະໄພ. ໄດ້ມີບັນຫາເກີດຂື້ນໃນ %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ລາຍງານ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">ກຳລັງສົ່ງລາຍງານການຂັດຂ້ອງໄປຫາ %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">ກຳລັງເກັບກຳຂໍ້ມູນມທີມີບັນຫາ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">ກຳລັງເກັບກຳຂໍ້ມູນ telemetry crash</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ລາຍງານຂໍ້ຂັດຂ້ອງ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ຍັງບໍ່ເຄີຍສົ່ງລາຍງານຂໍ້ຜິດພາດຈັກເທື່ອ.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ແບ່ງປັນ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..1915fd6bd8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Atsiprašome. „%1$s“ susidūrė su problema ir užstrigo.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Pranešti apie strigtį „%1$s“</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Užverti</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Paleisti „%1$s“ iš naujo</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Strigtys</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Atsiprašome. „%1$s“ susidūrė su problema.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Siųsti pranešimą</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Strigties pranešimas siunčiamas į „%1$s“</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Renkami strigčių duomenys</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Renkami strigčių telemetrijos duomenys</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Strigčių pranešimai</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Išsiųstų strigčių pranešimų nėra.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dalintis</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..1b9f328dbc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ക്ഷമിക്കണം. %1$s ന് ഒരു പ്രശ്‌നമുണ്ടായി, തകർന്നു.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">തകരാർ റിപ്പോർട്ട് %1$s ലേക്ക് അയയ്ക്കുക</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">അടയ്ക്കുക</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s പുനരാരംഭിക്കുക</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">തകരാറുകള്‍</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">റിപ്പോര്‍ട്ട്</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">തകരാ‍ർ വിവരണം %1$s ലേക്ക് അയയ്ക്കുന്നു</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">തകരാർ വിവരണം</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">തകരാർ വിവരങ്ങൾ സമര്‍പ്പിച്ചിട്ടില്ല.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">പങ്കിടുക</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..33ef33ca32
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">क्षमस्व. %1$s मध्ये समस्या आली आणि बंद पडला.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">समस्येचा अहवाल %1$s ला पाठवा</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">बंद</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s पुन्हा सुरू करा</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">क्रॅश</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">अहवाल द्या</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">समस्येचा चा अहवाल %1$s ला पाठवत आहे</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">क्रॅश अहवाल</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">कुठलेही क्रॅश अहवाल दाखल केले गेले नाहीत.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">सामायिक करा</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..d60c7f80cb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ဝမ်းနည်းပါတယ်။ %1$s တွင် ပြဿနာတစ်ခု ပေါ်ခဲ့သဖြင့် ရပ်ဆိုင်းသွားသည်။</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s သို့ ပျက်စီးမှုအစီရင်ခံစာပို့ပါ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ပိတ်ပါ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ကိုပြန်စပါ</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ပျက်စီးမှုများ</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">အစီရင်ခံပါ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s သို့ ပျက်စီးမှုအစီရင်ခံစာပို့ပါ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ပျက်ဆီးမှု အစီရင်ခံစာများ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">မည်သည့် ပျက်စီးမှု အစီရင်ခံစာ မျှ မတင်သွင်းထားပါ။</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">မျှဝေ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..12f4477c51
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Beklager. %1$s fikk problem og krasjet.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send krasjrapport til %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Lukk</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Start %1$s på nytt</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Krasj</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Beklager. Det oppsto et problem i %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapporter</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sender krasjrapport til %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Samler inn krasjdata</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Samler krasj-telemetridata</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Krasjrapporter</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ingen krasjrapporter er sendt inn.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Del</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..2ee6319010
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">माफ गर्नुहोस्। %1$s मा समस्या थियो र क्र्यास भएको थियो।</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s लाई क्र्यास प्रतिबेदन पठाउनुहोस्</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">बन्द गर्नुहोस्</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s पुनः सुचारु गर्नुहोस्</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">क्र्यासहरु</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">माफ गर्नुहोस्। %1$s मा एउटा समस्या आयो।</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">रिपोर्ट</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s लाई क्र्यास प्रतिबेदन पठाइँदै</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">क्र्यास प्रतिवेदनहरु</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">कुनै पनि क्रा्यास प्रतिबेदनहरु पेश गरिएको छैन।</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">सेयर</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..f0ed896cc7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. %1$s had een probleem en is gecrasht.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Crashrapport naar %1$s verzenden</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Sluiten</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s herstarten</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. Er is een probleem opgetreden in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Melden</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Crashrapport naar %1$s verzenden</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Crashgegevens verzamelen</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gegevens over crash-telemetrie verzamelen</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crashrapporten</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Er zijn geen crashrapporten verzonden.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Delen</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..7406570a66
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Beklagar. %1$s fekk problem og krasja</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send krasjrapport til %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Lat att</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Start %1$s på nytt</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Krasj</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Beklagar. Det oppsto eit problem i %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapporter</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sender krasjrapport til %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Samlar inn krasjdata</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Samlar krasj-telemetridata</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Krasjrapportar</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ingen krasjrapportar er sende inn.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Del</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..58f48f7a0c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">O planhèm. %1$s a rescontrat un problèma e a quitat de foncionar.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar un senhalament de bug a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Tampar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reaviar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Plantatges</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Desolat. Un problèma s’es produch dins %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Senhalar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Mandadís del rapòrt de plantatge a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Amassada de las donadas de plantatge</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Amassada de las donadas de telemetria de plantatge</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rapòrts de plantatge</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Cap de rapòrt de plantatge es pas estat mandat.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partejar</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..39b371daa1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ਅਫ਼ਸੋਸ ਹੈ। %1$s ਨੂੰ ਸਮੱਸਿਆ ਆਈ ਤੇ ਕਰੈਸ਼ ਹੋ ਗਿਆ ਹੈ।</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ਕਰੈਸ਼ ਰਿਪੋਰਟ %1$s ਨੂੰ ਭੇਜੋ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ਬੰਦ ਕਰੋ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ਮੁੜ-ਚਾਲੂ ਕਰੋ</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ਕਰੈਸ਼</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">ਅਫ਼ਸੋਸ ਹੈ। %1$s ਵਿੱਚ ਸਮੱਸਿਆ ਆਈ ਹੈ।</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ਰਿਪੋਰਟ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">ਕਰੈਸ਼ ਰਿਪੋਰਟ %1$s ਨੂੰ ਭੇਜੀ ਜਾ ਰਹੀ ਹੈ</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">ਕਰੈਸ਼ ਸੰਬੰਧੀ ਡਾਟੇ ਨੂੰ ਇਕੱਤਰ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">ਕਰੈਸ਼ ਟੈਲੀਮੈਂਟਰੀ ਡਾਟਾ ਇਕੱਤਰ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ਕਰੈਸ਼ ਰਿਪੋਰਟਾਂ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ਕੋਈ ਕਰੈਸ਼ ਰਿਪੋਰਟ ਨਹੀਂ ਦਿੱਤੀ ਗਈ</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ਸਾਂਝਾ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..f31d7f3974
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">بند کرو</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ریپورٹ کرو</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">سانجھا کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..bd70ea643b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">%1$s uległ awarii.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Zgłoś awarię organizacji %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zamknij</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Uruchom program %1$s ponownie</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Awarie</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">%1$s uległ awarii</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Zgłoś</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Zgłaszanie awarii organizacji %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Zbieranie danych o awarii</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Zbieranie danych telemetrycznych awarii</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Zgłoszenia awarii</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nie przesłano żadnych zgłoszeń awarii.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Udostępnij</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..9da369c166
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Desculpe, o %1$s teve um problema e travou.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar relatório de travamento para a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Fechar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar o %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Travamentos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Desculpe, houve um problema no %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Relatar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando relatório de travamento para a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Coletando dados de falha</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recolhendo dados de telemetria de travamentos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Relatórios de travamento</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nenhum relatório de travamento foi enviado.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartilhar</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..4d52fb69b6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Desculpe. %1$s teve um problema e falhou.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar relatório de falha para %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Fechar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Falhas</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Pedimos desculpa. Ocorreu um problema no %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Reportar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">A enviar relatório de falha para %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">A reunir dados de falha</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recolha de dados de telemetria de falhas</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Relatórios de falhas</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Não foram submetidos relatórios de falhas.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partilhar</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..0787a90e99
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Perstgisa. %1$s ha gì in problem ed è collabà.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Trametter in rapport da collaps a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Serrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reaviar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Collaps</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Perstgisa. Igl ha dà in problem en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapport</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Spediziun dal rapport da collaps a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Datas da collaps vegnan rimnadas</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Rimnada da datas da telemetria davart collaps</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rapports da collaps</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Anc nagins rapports da collaps tramess.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Cundivider</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..0d67a3b1ba
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ne pare rău. %1$s a avut o problemă și s-a închis neașteptat.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Trimite raportul de defecțiune la %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Închide</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Repornește %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Defecțiuni</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raportează</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Se trimite raportul de defecțiune la %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rapoarte de defecțiuni</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nu au fost trimise rapoarte de defecțiuni.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partajează</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..c8629a9534
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Извините. В %1$s возникла проблема и произошёл сбой.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Отправлять сообщения о падениях в %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Закрыть</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Перезапустить %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Падения</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Извините. В %1$s возникла проблема.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Сообщить</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Сообщение о падении отправляется в %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Сбор данных о падении</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Сбор данных телеметрии о падениях</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Сообщения о падениях</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ни одного сообщения о падении отправлено не было.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Сообщить</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..037564fa1e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ᱤᱠᱟᱹ %1$s ᱥᱟᱶ ᱛᱮ ᱫᱤᱜᱫᱷᱟ ᱦᱚᱭ ᱱᱟ ᱟᱨ ᱨᱟᱹᱯᱩᱫᱮᱱᱟ ᱾</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s ᱴᱷᱮᱱ ᱨᱟᱹᱯᱩᱫ ᱨᱤᱯᱚᱨᱴ ᱠᱩᱞ ᱢᱮ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ᱵᱚᱸᱫᱚᱭ ᱢᱮ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ᱫᱩᱦᱲᱟᱹ ᱮᱦᱚᱵᱽ ᱢᱮ</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ᱨᱟᱹᱯᱩᱫ ᱠᱚ</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">ᱤᱠᱟᱹ ᱾ %1$s ᱨᱮ ᱵᱷᱩᱞ ᱦᱩᱭᱮᱱᱟ ᱾</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ᱨᱤᱯᱚᱴ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s ᱴᱷᱮᱱ ᱨᱟᱹᱯᱩᱫ ᱨᱤᱯᱚᱴ ᱠᱩᱞ ᱦᱩᱭᱩ ᱠᱟᱱᱟ</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">ᱠᱨᱟᱥ ᱰᱟᱴᱟ ᱡᱟᱣᱨᱟᱜ ᱠᱟᱱᱟ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">ᱠᱨᱟᱥ ᱴᱮᱞᱤᱢᱮᱴᱨᱤ ᱰᱟᱴᱟ ᱡᱟᱣᱨᱜ ᱠᱟᱱᱟ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ᱨᱟᱹᱯᱩᱫ ᱨᱤᱯᱚᱨᱴ ᱠᱚ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ᱚᱠᱟ ᱦᱚᱸ ᱨᱟᱹᱯᱩᱫ ᱨᱤᱯᱚᱨᱴ ᱡᱚᱢᱟ ᱵᱟᱝ ᱦᱩᱭ ᱠᱟᱱᱟ ᱾</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ᱦᱟᱹᱴᱤᱧ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..7e95e3a65e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">%1$s at tentu unu problema e est faddidu.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Imbia s’informe de faddina a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Serra</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Torra a aviare %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Faddinas</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ddoe est istada una faddina in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Sinnala</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Imbiende s’informe de faddina a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Regollende datos de sa faddina</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Si sunt regollende is datos de telemetria de sa faddina</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de faddinas</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nissunu informe de faddina imbiadu.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Cumpartzi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..a58aed9c7c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">කණගාටුයි. %1$s හි ගැටලුවක් මතු වී බිඳ වැටුණි.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">බිඳ වැටීමේ වාර්තාව %1$s වෙත යවන්න</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">වසන්න</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s යළි අරඹන්න</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">බිඳ වැටීම්</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">කණගාටුයි. %1$s හි ගැටලුවක් මතු විය.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">වාර්තාව</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">බිඳ වැටීමේ වාර්තාව %1$s වෙත යවමින්</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">බිඳ වැටීමේ දත්ත එකතැන් වෙමින්</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">බිඳවැටීම් වාර්තා</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">බිඳවැටීමේ වාර්තා කිසිවක් යොමු කර නැත.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">බෙදාගන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..2d35f006a0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ospravedlňujeme sa. Aplikácia %1$s narazila na problém a zlyhala.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Odoslať správu o zlyhaní spoločnosti %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zavrieť</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reštartovať %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Zlyhania</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ospravedlňujeme sa. Vyskytol sa problém s aplikáciou %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Nahlásiť</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Odosielanie správy o zlyhaní spoločnosti %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Zhromažďujú sa údaje o zlyhaní</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Zhromažďujú sa telemetrické údaje o zlyhaní</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Správy o zlyhaní</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Neboli odoslané žiadne správy o zlyhaní.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Zdieľať</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..6cda4101b6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">افسوس۔ %1$s وچ کوئی مسئلہ ہے تے تباہ تھی ڳئے۔</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s کوں کریش رپوٹ بھیڄو</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">بند کرو</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ولدا شروع کرو</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">کریش</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">افسوس۔ %1$s وچ ہک مسئلہ تھی ڳیا ہے۔</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">رپورٹ کرو</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s کوں کریش رپوٹ بھیڄیندا پئے</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">کریش ڈیٹا کٹھا کریندا پئے</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">کریش ٹیلی میٹری ڈیٹا کٹھا کرݨ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">کریش رپورٹاں</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">کوئی کریش رپوٹاں جمع کائنی کرائیاں۔</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">شیئر</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..a0a020d1c4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Oprostite. %1$s je naletel na težavo in se sesul.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Pošlji poročilo o sesutju organizaciji %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zapri</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ponovno zaženi %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Sesutja</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Oprostite. V %1$su je prišlo do težave.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Prijavi</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Pošiljanje poročila o sesutju organizaciji %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Zbiranje podatkov o sesutju</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Zbiranje telemetričnih podatkov o sesutju</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Poročila o sesutjih</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nobeno poročilo o sesutju ni bilo poslano.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Deli</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..90dadd927d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Na ndjeni. %1$s pati një problem dhe u vithis.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Dërgoni raport vithisjeje te %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Mbylle</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Rinise %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Vithisje</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Na ndjeni. Ndodhi një problem në %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raportoje</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Po dërgohet njoftim vithisjeje te %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Po mblidhen të dhëna vithisjeje</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Po mblidhen të dhëna telemetrike vithisjeje</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Njoftime Vithisjesh</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nuk ka të parashtruar njoftime vithisjesh.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Ndajeni me të tjerët</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..1e5cefabbd
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Извињавам се. %1$s је имао проблем и срушио се.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Пошаљи извештај о паду на %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Затвори</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Поново покрени %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Пад</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Нажалост, догодио се проблем у позадинском процесу %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Извештај</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Слање извештаја о паду на %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Прикупљање података о паду</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Прикупљање телеметријских података о паду</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Извештаји о рушењу</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ниједан извештај о рушењу није поднесен.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Подели</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..e620497e6f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Hampura. %1$s aya masalah tur ruksak.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Kirim laporan nu ruksak ka %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Tutup</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Mimitian deui %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Karuksakan</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Hampura. Aya masalah dina %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Laporan</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Kirim laporan nu ruksak ka %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ngumpulkeun data ruksak</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Ngumpulkeun data telemétri ruksak</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Laporan Karuksakan</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Teu aya laporan karuksakan nu tos dipasihkeun.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bagikeun</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..a4c7ed9511
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Förlåt. %1$s hade problem och kraschade.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Skicka kraschrapport till %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Stäng</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Starta om %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Krascher</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Förlåt. Ett problem uppstod i %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapportera</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Skicka kraschrapport till %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Samlar in kraschdata</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Samlar in krasch-telemetridata</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Kraschrapporter</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Inga kraschrapporter har skickats in.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dela</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..39047ebb22
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">மன்னிக்க. %1$s சிக்கலேற்பட்டுச் செயலிழந்தது.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s க்குச் சிதைவு அறிக்கையை அனுப்பு</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">மூடுக</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ஐ மறுதுவக்கு</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">செயலிழப்புகள்</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">அறிக்கையிடுக</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">செயலிழப்பு அறிக்கையை %1$s க்கு அனுப்புகிறது</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">சிதைவு அறிக்கைகள்</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">எந்தச் சிதைவு அறிக்கைகளும் சமர்பிக்கப்படவில்லை.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">பகிர்</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..3a7ea16d05
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">క్షమించండి. ఏదో సమస్య వల్ల %1$s క్రాష్ అయ్యింది.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">క్రాష్ నివేదికను %1$s‌కి పంపించు</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">మూసివేయి</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s‌ను పునఃప్రారంభించు</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">క్రాషులు</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">నివేదించు</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">క్రాష్ నివేదికను %1$s‌ కి పంపిస్తోంది</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">క్రాష్ నివేదికలు</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">క్రాష్ నివేదికలేమీ సమర్పించబడలేదు.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">పంచుకోండి</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..ed7ddd9b30
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Мутаассифона, %1$s мушкилӣ дошта, бо вайронӣ дучор шуд.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Фиристодани гузориш дар бораи вайронӣ ба %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Пӯшидан</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Аз нав оғоз кардани %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Вайрониҳо</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Бубахшед. Дар %1$s мушкилӣ ба миён омад.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Гузориш додан</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Гузориш дар бораи вайронӣ ба %1$s фиристода шуда истодааст</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ҷамъоварии маълумот дар бораи вайрониҳо</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Ҷамъоварии маълумот дар бораи вайрониҳои дурсанҷӣ (телеметрия)</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Гузоришҳо дар бораи вайронӣ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ягон гузориш дар бораи вайронӣ пешниҳод карда нашуд.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Мубодила кардан</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..9f1fe18638
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ขออภัย %1$s มีปัญหา และหยุดการทำงานแล้ว</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ส่งรายงานข้อขัดข้องไปยัง %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ปิด</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">เริ่ม %1$s ใหม่</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ข้อขัดข้อง</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">ขออภัย เกิดข้อผิดพลาดใน %1$s</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">รายงาน</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">กำลังส่งรายงานข้อขัดข้องไปยัง %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">กำลังรวบรวมข้อมูลข้อขัดข้อง</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">กำลังรวบรวมข้อมูลการวัดและส่งข้อมูลทางไกลเกี่ยวกับข้อขัดข้อง</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">รายงานข้อขัดข้อง</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ยังไม่เคยมีการรายงานข้อขัดข้อง</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">แบ่งปัน</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..1b7d285a21
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Paumanhin. Nagkaproblema ang %1$s.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Ipadala ang crash report sa %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Isara</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">i-Restart ang %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Mga crash</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Iulat</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Ipinadadala ang crash report sa %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Nangongolekta ng data ng pag-crash</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Pagtitipon ng data ng telemetry ng pag-crash</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Mga Crash Report</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Walang pang mga crash report na naipadala.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Ibahagi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..c4b923ffb7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">%1$s bir sorunla karşılaştı ve çöktü.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Çökme raporunu %1$s’ya gönder</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Kapat</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s uygulamasını yeniden başlat</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Çökmeler</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">%1$s uygulamasında bir sorun oluştu.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raporla</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Çökme raporu %1$s\'ya gönderiliyor</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Çökme verileri toplanıyor</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Çökme verileri toplanıyor</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Çökme Raporları</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Henüz hiç çökme raporu gönderilmedi.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..f5d345a3e7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sī ga\'man ruhuât. %1$s ga \'ngō sañuun riñanj.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Gā\'nïnj gan\'ānj nuguan\' rayi\'î sa \'iaj re\'ej riña %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Nārán</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Nāyi\'ì ñû %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Nej sa gahui a\'nan\'</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sī ga’man ruhuât. Huā sa gahui a’nan’ riña %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Gānātà\'</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Hīaj a\'nïn hua\'ānj nuguan\' rayi\'î sa \'iaj re\' riña %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Sa naran’ andaj gire’ riña aga’ nan</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Sa naran’ andaj telemetría gire’ riña aga’ nan</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Nuguan\' nata\' sa gahui a\'nan\'an</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nu gan\'ānj gà\' si \'ngō nuguan\' ganata\'a.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dūyingô\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..8056936c37
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Гафу. %1$s хатага юлыкты һәм ватылды.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s адресына ватылу турында хәбәр җибәрү</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Ябу</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s-ны яңадан ачу</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Өзеклеклер</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Гафу итегез. %1$s кушымтасында проблема килеп чыкты.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Шикаять итү</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s адресына ватылу турында хәбәр җибәрү</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ватылу турында мәгълүмат җыю</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Ватылу турында телеметрия мәгълүматларын туплау</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Ватылу турында хәбәрләр</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ватылу турында хәбәрләр җибәрелмәгән.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Уртаклашу</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..9005336bc5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Mḍel</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bḍu</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..d968d43ce4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">كەچۈرۈڭ، %1$s مەسىلىگە يولۇقۇپ يىمىرىلدى.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">يىمىرىلىش دوكلاتىنى %1$s غا يوللايدۇ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">تاقاش</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart"> %1$s نى قايتا قوزغات</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">يىمىرىلىش</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">كەچۈرۈڭ، %1$s دا مەسىلە كۆرۈلدى.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">دوكلات</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">يىمىرىلىش دوكلاتىنى %1$s غا يوللاۋاتىدۇ</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">يىمىرىلىش سانلىق مەلۇماتلىرىنى توپلاۋاتىدۇ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">يىمىرىلىش تېلېگراف سانلىق مەلۇماتلىرىنى توپلاۋاتىدۇ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">يىمىرىلىش دوكلاتى</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">يىمىرىلىش دوكلاتى تاپشۇرۇلمىدى.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ھەمبەھىرلەش</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..26c3f264f2
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Вибачте. Виникла проблема з %1$s і стався збій.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Надіслати звіт про збій до %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Закрити</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Перезапустити %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Збої</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Вибачте. Виникла проблема в %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Звіт</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Надсилання звіту про збій до %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Збір даних про збої</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Збір даних телеметрії про збої</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Звіти про збої</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Жодних звітів про збої не надсилалось.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Поділитися</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..dfd0f6c7d5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">معاف کریں، %1$s میں کوئی خرابی آئی ہے اور کریش ہو گئی ہے۔</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s کو کریش رپورٹ بھیجیں</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">بند کریں</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s دوبارہ شروع کریں</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">کریش</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">رپورٹ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s کو کریش رپورٹ بھیجآ جا رہا ہے</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">کریش رپورٹیں</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">کوئی کریش رپورٹیں ارسال نہی کی گئی۔</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">شیئر کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..41ca048ca5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Kechirasiz, %1$s ilovasida muammo yuz berdi.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Nosozlik maʼlumotini %1$sga yuborish</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Yopish</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$sni qayta ishga tushirish</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Nosozliklar</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Kechirasiz. %1$s da muammo yuz berdi.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Hisobot berish</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$sga nosozlik hisobotini yuborlmoqda</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Nosozlik maʼlumotlari yigʻilmoqda</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Nosozlik telemetriya maʼlumotlari yigʻilmoqda</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Nosozlik hisobotlari</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Hech qanday nosozlik hisobotlari yuborilmadi.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Ulashish</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..90f030e2f5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Se xe verificà on problema en %1$s che gà provocà on aresto anomaƚo.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Manda na segnaƚasion de aresto anomaƚo a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Sara su</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Invia de novo %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Aresto anomaƚo</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Segnaƚa</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Son drio mandare ƚa segnaƚasion de aresto anomaƚo a %1$s</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..13cae1333c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Rất tiếc. %1$s đã gặp sự cố và buộc phải đóng.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Gửi báo cáo sự cố cho %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Đóng</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Khởi động lại %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Sự cố</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Xin lỗi. Đã xảy ra sự cố trong %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Báo cáo</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Gửi báo cáo sự cố đến %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Thu thập dữ liệu sự cố</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Thu thập dữ liệu đo từ xa của sự cố</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Trình báo cáo lỗi</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Chưa có báo cáo lỗi nào được gửi.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Chia sẻ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..0676f335e1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Pẹ̀lẹ́. %1$s ní ìṣòro tó sì lulẹ̀.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Fi àwọn ìròyìn ìjákulẹ̀ ránṣẹ́ sí %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Padé</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Tún-un bẹ̀rẹ̀ %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Àwọn ìjákulẹ̀</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Pẹ̀lẹ́. Ìṣòrò kan wáyé ní %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Ìròyìn </string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Fifi àwọn ìròyìn ìjákulẹ̀ ránṣẹ́ sí %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Kíkó àwọn dátà tó ti ní ìjákulẹ̀ pọ̀</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Kíkó àwọn dátà tẹlímẹ́tírì tó ti ní ìjákulẹ̀ pọ̀</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Àwọn ìròyìn ìjákulẹ̀</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Kò sí àwọn ìròyìn ìjákulẹ̀ tí a ti fi sílẹ̀.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Pín</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..9a0bf4aadb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">很抱歉,%1$s 遇到问题,已经崩溃。</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">向 %1$s 发送崩溃报告</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">关闭</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">重启 %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">崩溃信息</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">抱歉,%1$s 出现问题。</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">反馈</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">正在向 %1$s 发送崩溃报告</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">正在收集崩溃数据</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">收集崩溃遥测数据</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">崩溃报告</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">尚未提交任何崩溃报告。</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">共享</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..21177e0a22
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">很抱歉,%1$s 遇到問題,發生錯誤。</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">傳送錯誤報告給 %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">關閉</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">重新啟動 %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">程式錯誤</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">很抱歉,%1$s 發生問題。</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">回報</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">正在傳送錯誤報告給 %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">收集錯誤資料</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">正在取得發生錯誤的 telemetry 資料</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">錯誤資訊報表</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">未送出任何錯誤資訊報表。</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">分享</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..c0403c6995
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml
@@ -0,0 +1,44 @@
+<?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>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. %1$s had a problem and crashed.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send crash report to %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Close</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. A problem occurred in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Report</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sending crash report to %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Gathering crash data</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gathering crash telemetry data</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crash Reports</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No crash reports have been submitted.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Share</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..8e441529e9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml
@@ -0,0 +1,13 @@
+<?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>
+ <style name="Theme.Mozac.CrashReporter" parent="Theme.AppCompat.Light.Dialog">
+ <item name="windowNoTitle">true</item>
+ <item name="android:windowMinWidthMajor">96%</item>
+ <item name="android:windowMinWidthMinor">96%</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt
new file mode 100644
index 0000000000..b54947712d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt
@@ -0,0 +1,192 @@
+/* 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.lib.crash
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import java.lang.Thread.sleep
+import java.util.Date
+
+@RunWith(AndroidJUnit4::class)
+class BreadcrumbTest {
+
+ @Before
+ fun setUp() {
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `RecordBreadCrumb stores breadCrumb in reporter`() {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+
+ reporter.crashBreadcrumbsCopy().elementAt(0).let {
+ assertEquals(it.message, testMessage)
+ assertEquals(it.data, testData)
+ assertEquals(it.category, testCategory)
+ assertEquals(it.level, testLevel)
+ assertEquals(it.type, testType)
+ assertNotNull(it.date)
+ }
+ }
+
+ @Test
+ fun `Reporter stores current number of breadcrumbs`() {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+ assertEquals(reporter.crashBreadcrumbsCopy().size, 1)
+
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+ assertEquals(reporter.crashBreadcrumbsCopy().size, 2)
+
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+ assertEquals(reporter.crashBreadcrumbsCopy().size, 3)
+ }
+
+ @Test
+ fun `RecordBreadcumb stores correct date`() {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val beginDate = Date()
+ sleep(100) // make sure time elapsed
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+ sleep(100) // make sure time elapsed
+ val afterDate = Date()
+
+ reporter.crashBreadcrumbsCopy().elementAt(0).let {
+ assertTrue(it.date.after(beginDate))
+ assertTrue(it.date.before(afterDate))
+ }
+
+ val date = Date()
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ date,
+ ),
+ )
+
+ reporter.crashBreadcrumbsCopy().elementAt(1).let {
+ assertEquals(it.date.compareTo(date), 0)
+ }
+ }
+
+ @Test
+ fun `Breadcrumb converts correctly to JSON`() {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+ val testDate = Date(0)
+ val testString = "{\"timestamp\":\"1970-01-01T00:00:00\",\"message\":\"test_Message\"," +
+ "\"category\":\"testing_category\",\"level\":\"Critical\",\"type\":\"User\"," +
+ "\"data\":{\"1\":\"one\",\"2\":\"two\"}}"
+
+ val breadcrumb = Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ testDate,
+ )
+ assertEquals(breadcrumb.toJson().toString(), testString)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt
new file mode 100644
index 0000000000..1ec2333325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt
@@ -0,0 +1,931 @@
+/* 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.lib.crash
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.lib.crash.service.CrashTelemetryService
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.expectException
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+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.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import java.lang.Thread.sleep
+import java.lang.reflect.Modifier
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class CrashReporterTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setUp() {
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `Calling install() will setup uncaught exception handler`() {
+ val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
+
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val newHandler = Thread.getDefaultUncaughtExceptionHandler()
+ assertNotNull(newHandler)
+
+ assertNotEquals(defaultHandler, newHandler)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `CrashReporter throws if no service is defined`() {
+ CrashReporter(
+ context = testContext,
+ services = emptyList(),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+ }
+
+ @Test
+ fun `CrashReporter will submit report immediately if setup with Prompt-NEVER`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter).sendCrashReport(testContext, crash)
+ verify(reporter, never()).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will show prompt if setup with Prompt-ALWAYS`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(reporter).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will submit report immediately for non native crash and with setup Prompt-ONLY_NATIVE_CRASH`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ONLY_NATIVE_CRASH,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter).sendCrashReport(testContext, crash)
+ verify(reporter, never()).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will show prompt for main process native crash and with setup Prompt-ONLY_NATIVE_CRASH`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ONLY_NATIVE_CRASH,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter).showPrompt(any(), eq(crash))
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(service, never()).report(crash)
+ }
+
+ @Test
+ fun `CrashReporter will submit crash telemetry even if crash report requires prompt`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(reporter).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will not prompt the user if there is no crash services`() {
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(reporter, never()).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will not send crash telemetry if there is no telemetry service`() {
+ val service: CrashReporterService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter, never()).sendCrashTelemetry(testContext, crash)
+ verify(reporter).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `Calling install() with no crash services or telemetry crash services will throw exception`() {
+ var exceptionThrown = false
+
+ try {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+ } catch (e: IllegalArgumentException) {
+ exceptionThrown = true
+ }
+
+ assert(exceptionThrown)
+ }
+
+ @Test
+ fun `Calling install() with at least one crash service or telemetry crash service will not throw exception`() {
+ var exceptionThrown = false
+
+ try {
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+ } catch (e: IllegalArgumentException) {
+ exceptionThrown = true
+ }
+ assert(!exceptionThrown)
+
+ try {
+ CrashReporter(
+ context = testContext,
+ telemetryServices = listOf(mock()),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+ } catch (e: IllegalArgumentException) {
+ exceptionThrown = true
+ }
+ assert(!exceptionThrown)
+ }
+
+ @Test
+ fun `CrashReporter is enabled by default`() {
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ shouldPrompt = CrashReporter.Prompt.ONLY_NATIVE_CRASH,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ assertTrue(reporter.enabled)
+ }
+
+ @Test
+ fun `CrashReporter will not prompt and not submit report if not enabled`() {
+ val service: CrashReporterService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.enabled = false
+
+ val crash: Crash.UncaughtExceptionCrash = mock()
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(reporter, never()).sendCrashTelemetry(testContext, crash)
+ verify(reporter, never()).showPrompt(any(), eq(crash))
+
+ verify(service, never()).report(crash)
+ }
+
+ @Test
+ fun `CrashReporter sends telemetry`() {
+ val crash = createUncaughtExceptionCrash()
+
+ val service = mock<CrashReporterService>()
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.onCrash(testContext, crash)
+ verify(reporter, never()).sendCrashTelemetry(testContext, crash)
+ }
+
+ @Test
+ fun `CrashReporter forwards uncaught exception crashes to service`() {
+ var exceptionCrash = false
+
+ val service = object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? {
+ exceptionCrash = true
+ return null
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? = null
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? = null
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.submitReport(
+ Crash.UncaughtExceptionCrash(0, RuntimeException(), arrayListOf()),
+ ).joinBlocking()
+ assertTrue(exceptionCrash)
+ }
+
+ @Test
+ fun `CrashReporter forwards native crashes to service`() {
+ var nativeCrash = false
+
+ val service = object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? = null
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ nativeCrash = true
+ return null
+ }
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? = null
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.submitReport(
+ Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ ),
+ ).joinBlocking()
+ assertTrue(nativeCrash)
+ }
+
+ @Test
+ fun `CrashReporter forwards caught exception crashes to service`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+ var exceptionCrash = false
+ var exceptionThrowable: Throwable? = null
+ var exceptionBreadcrumb: ArrayList<Breadcrumb>? = null
+ val service = object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? = null
+
+ override fun report(crash: Crash.NativeCodeCrash): String? = null
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? {
+ exceptionCrash = true
+ exceptionThrowable = throwable
+ exceptionBreadcrumb = breadcrumbs
+ return null
+ }
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val throwable = RuntimeException()
+ val breadcrumb = Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ )
+ reporter.recordCrashBreadcrumb(breadcrumb)
+ advanceUntilIdle()
+
+ reporter.submitCaughtException(throwable).joinBlocking()
+
+ assertTrue(exceptionCrash)
+ assert(exceptionThrowable == throwable)
+ assert(exceptionBreadcrumb?.get(0) == breadcrumb)
+ }
+
+ @Test
+ fun `Caught exception with no stack trace should be reported as CrashReporterException`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+ var exceptionCrash = false
+ var exceptionThrowable: Throwable? = null
+ var exceptionBreadcrumb: ArrayList<Breadcrumb>? = null
+ val service = object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? = null
+
+ override fun report(crash: Crash.NativeCodeCrash): String? = null
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? {
+ exceptionCrash = true
+ exceptionThrowable = throwable
+ exceptionBreadcrumb = breadcrumbs
+ return null
+ }
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val throwable = RuntimeException()
+ throwable.stackTrace = emptyArray()
+ val breadcrumb = Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ )
+ reporter.recordCrashBreadcrumb(breadcrumb)
+ advanceUntilIdle()
+
+ reporter.submitCaughtException(throwable).joinBlocking()
+
+ assertTrue(exceptionCrash)
+ assert(exceptionThrowable is CrashReporterException.UnexpectedlyMissingStacktrace)
+ assert(exceptionThrowable?.cause is java.lang.RuntimeException)
+ assertEquals(exceptionBreadcrumb?.get(0), breadcrumb)
+ }
+
+ @Test
+ fun `CrashReporter forwards native crashes to telemetry service`() {
+ var nativeCrash = false
+
+ val telemetryService = object : CrashTelemetryService {
+ override fun record(crash: Crash.UncaughtExceptionCrash) = Unit
+
+ override fun record(crash: Crash.NativeCodeCrash) {
+ nativeCrash = true
+ }
+
+ override fun record(throwable: Throwable) = Unit
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.submitCrashTelemetry(
+ Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ ),
+ ).joinBlocking()
+ assertTrue(nativeCrash)
+ }
+
+ @Test
+ fun `Internal reference is set after calling install`() {
+ expectException(IllegalStateException::class) {
+ CrashReporter.requireInstance
+ }
+
+ val reporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ notificationsDelegate = mock(),
+ )
+
+ expectException(IllegalStateException::class) {
+ CrashReporter.requireInstance
+ }
+
+ reporter.install(testContext)
+
+ assertNotNull(CrashReporter.requireInstance)
+ }
+
+ @Test
+ fun `CrashReporter invokes PendingIntent if provided for foreground child process crashes`() {
+ val context = Robolectric.buildActivity(Activity::class.java).setup().get()
+
+ val intent = Intent("action")
+ val pendingIntent = spy(PendingIntent.getActivity(context, 0, intent, 0))
+
+ val reporter = CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(mock()),
+ nonFatalCrashIntent = pendingIntent,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(context, nativeCrash)
+
+ verify(pendingIntent).send(eq(context), eq(0), any())
+
+ val receivedIntent = shadowOf(context).nextStartedActivity
+
+ val receivedCrash = Crash.fromIntent(receivedIntent) as? Crash.NativeCodeCrash
+ ?: throw AssertionError("Expected NativeCodeCrash instance")
+
+ assertEquals(nativeCrash, receivedCrash)
+ assertEquals("dump.path", receivedCrash.minidumpPath)
+ assertEquals(true, receivedCrash.minidumpSuccess)
+ assertEquals("extras.path", receivedCrash.extrasPath)
+ assertEquals(false, receivedCrash.isFatal)
+ assertEquals(Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD, receivedCrash.processType)
+ }
+
+ @Test
+ fun `CrashReporter does not invoke PendingIntent if provided for main process crashes`() {
+ val context = Robolectric.buildActivity(Activity::class.java).setup().get()
+
+ val intent = Intent("action")
+ val pendingIntent = spy(PendingIntent.getActivity(context, 0, intent, 0))
+
+ val reporter = CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(mock()),
+ nonFatalCrashIntent = pendingIntent,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(context, nativeCrash)
+
+ verify(pendingIntent, never()).send(eq(context), eq(0), any())
+ }
+
+ @Test
+ fun `CrashReporter does not invoke PendingIntent if provided for background child process crashes`() {
+ val context = Robolectric.buildActivity(Activity::class.java).setup().get()
+
+ val intent = Intent("action")
+ val pendingIntent = spy(PendingIntent.getActivity(context, 0, intent, 0))
+
+ val reporter = CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(mock()),
+ nonFatalCrashIntent = pendingIntent,
+ notificationsDelegate = mock(),
+ ).install(context)
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(context, nativeCrash)
+
+ verify(pendingIntent, never()).send(eq(context), eq(0), any())
+ }
+
+ @Test
+ fun `CrashReporter sends telemetry but don't send native crash if the crash is in foreground child process and nonFatalPendingIntent is not null`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ nonFatalCrashIntent = mock(),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(testContext, nativeCrash)
+
+ verify(reporter, never()).sendCrashReport(testContext, nativeCrash)
+ verify(reporter, times(1)).sendCrashTelemetry(testContext, nativeCrash)
+ verify(reporter, never()).showPrompt(any(), eq(nativeCrash))
+ }
+
+ @Test
+ fun `CrashReporter sends telemetry and crash if the crash is in foreground child process and nonFatalPendingIntent is null`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(testContext, nativeCrash)
+
+ verify(reporter, times(1)).sendCrashReport(testContext, nativeCrash)
+ verify(reporter, times(1)).sendCrashTelemetry(testContext, nativeCrash)
+ verify(reporter, never()).showPrompt(any(), eq(nativeCrash))
+ }
+
+ @Test
+ fun `CrashReporter instance writes are visible across threads`() {
+ val instanceField = CrashReporter::class.java.getDeclaredField("instance")
+ assertTrue(Modifier.isVolatile(instanceField.modifiers))
+ }
+
+ @Test
+ fun `Breadcrumbs stores only max number of breadcrumbs`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+
+ var crashReporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ maxBreadCrumbs = 5,
+ scope = scope,
+ notificationsDelegate = mock(),
+ )
+
+ repeat(10) {
+ crashReporter.recordCrashBreadcrumb(Breadcrumb(testMessage, testData, testCategory, testLevel, testType))
+ }
+ advanceUntilIdle()
+ assertEquals(crashReporter.crashBreadcrumbsCopy().size, 5)
+
+ crashReporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ maxBreadCrumbs = 5,
+ scope = scope,
+ notificationsDelegate = mock(),
+ )
+ repeat(15) {
+ crashReporter.recordCrashBreadcrumb(Breadcrumb(testMessage, testData, testCategory, testLevel, testType))
+ }
+ advanceUntilIdle()
+ assertEquals(crashReporter.crashBreadcrumbsCopy().size, 5)
+ }
+
+ @Test
+ fun `Breadcrumb priority queue stores the latest breadcrumbs`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testType = Breadcrumb.Type.USER
+ val maxNum = 10
+
+ var crashReporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ maxBreadCrumbs = maxNum,
+ scope = scope,
+ notificationsDelegate = mock(),
+ )
+
+ repeat(maxNum) {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(testMessage, testData, testCategory, Breadcrumb.Level.CRITICAL, testType),
+ )
+ sleep(10) // make sure time elapsed
+ }
+ advanceUntilIdle()
+
+ crashReporter.crashBreadcrumbsCopy().let {
+ for (i in 0 until maxNum) {
+ assertEquals(it.elementAt(i).level, Breadcrumb.Level.CRITICAL)
+ }
+
+ var time = it[0].date
+ for (i in 1 until it.size) {
+ assertTrue(time.before(it[i].date))
+ time = it[i].date
+ }
+ }
+
+ repeat(maxNum) {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(testMessage, testData, testCategory, Breadcrumb.Level.DEBUG, testType),
+ )
+ sleep(10) // make sure time elapsed
+ }
+ advanceUntilIdle()
+
+ crashReporter.crashBreadcrumbsCopy().let {
+ for (i in 0 until maxNum) {
+ assertEquals(it.elementAt(i).level, Breadcrumb.Level.DEBUG)
+ }
+
+ var time = it[0].date
+ for (i in 1 until it.size) {
+ assertTrue(time.before(it[i].date))
+ time = it[i].date
+ }
+ }
+ }
+
+ @Test
+ fun `Breadcrumb priority queue output list result is sorted by time`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testType = Breadcrumb.Type.USER
+ val maxNum = 10
+
+ var crashReporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ maxBreadCrumbs = 5,
+ scope = scope,
+ notificationsDelegate = mock(),
+ )
+
+ repeat(maxNum) {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(testMessage, testData, testCategory, Breadcrumb.Level.DEBUG, testType),
+ )
+ sleep(10) // make sure time elapsed
+ }
+ advanceUntilIdle()
+
+ crashReporter.crashBreadcrumbsCopy().let {
+ var time = it[0].date
+ for (i in 1 until it.size) {
+ assertTrue(time.before(it[i].date))
+ time = it[i].date
+ }
+ }
+
+ repeat(maxNum / 2) {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(testMessage, testData, testCategory, Breadcrumb.Level.INFO, testType),
+ )
+ sleep(10) // make sure time elapsed
+ }
+ advanceUntilIdle()
+
+ crashReporter.crashBreadcrumbsCopy().let {
+ var time = it[0].date
+ for (i in 1 until it.size) {
+ assertTrue(time.before(it[i].date))
+ time = it[i].date
+ }
+ }
+ }
+}
+
+private fun createUncaughtExceptionCrash(): Crash.UncaughtExceptionCrash {
+ return Crash.UncaughtExceptionCrash(
+ 0,
+ RuntimeException(),
+ ArrayList(),
+ )
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt
new file mode 100644
index 0000000000..653655a65a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt
@@ -0,0 +1,105 @@
+/* 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.lib.crash
+
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CrashTest {
+
+ @Test
+ fun `fromIntent() can deserialize a GeckoView crash Intent`() {
+ val originalCrash = Crash.NativeCodeCrash(
+ 123,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ true,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "web",
+ )
+
+ val intent = Intent()
+ originalCrash.fillIn(intent)
+
+ val recoveredCrash = Crash.fromIntent(intent) as? Crash.NativeCodeCrash
+ ?: throw AssertionError("Expected NativeCodeCrash instance")
+
+ assertEquals(recoveredCrash.timestamp, 123)
+ assertEquals(recoveredCrash.minidumpSuccess, true)
+ assertEquals(recoveredCrash.isFatal, false)
+ assertEquals(recoveredCrash.processType, Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD)
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ recoveredCrash.minidumpPath,
+ )
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ recoveredCrash.extrasPath,
+ )
+ assertEquals("web", recoveredCrash.remoteType)
+ }
+
+ @Test
+ fun `Serialize and deserialize UncaughtExceptionCrash`() {
+ val exception = RuntimeException("Hello World!")
+
+ val originalCrash = Crash.UncaughtExceptionCrash(0, exception, arrayListOf())
+
+ val intent = Intent()
+ originalCrash.fillIn(intent)
+
+ val recoveredCrash = Crash.fromIntent(intent) as? Crash.UncaughtExceptionCrash
+ ?: throw AssertionError("Expected UncaughtExceptionCrash instance")
+
+ assertEquals(exception, recoveredCrash.throwable)
+ assertEquals("Hello World!", recoveredCrash.throwable.message)
+ assertArrayEquals(exception.stackTrace, recoveredCrash.throwable.stackTrace)
+ }
+
+ @Test
+ fun `isCrashIntent()`() {
+ assertFalse(Crash.isCrashIntent(Intent()))
+
+ assertFalse(
+ Crash.isCrashIntent(
+ Intent()
+ .putExtra("crash", "I am a crash!"),
+ ),
+ )
+
+ assertTrue(
+ Crash.isCrashIntent(
+ Intent().apply {
+ Crash.UncaughtExceptionCrash(0, RuntimeException(), arrayListOf()).fillIn(this)
+ },
+ ),
+ )
+
+ assertTrue(
+ Crash.isCrashIntent(
+ Intent().apply {
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ "",
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ crash.fillIn(this)
+ },
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt
new file mode 100644
index 0000000000..a8e83154b2
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt
@@ -0,0 +1,74 @@
+/* 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.lib.crash
+
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NativeCodeCrashTest {
+
+ @Test
+ fun `Creating NativeCodeCrash object from sample GeckoView intent`() {
+ val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra("uuid", "afc91225-93d7-4328-b3eb-d26ad5af4d86")
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("processType", "FOREGROUND_CHILD")
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putExtra("remoteType", "web")
+
+ val crash = Crash.NativeCodeCrash.fromBundle(intent.extras!!)
+
+ assertEquals(
+ "afc91225-93d7-4328-b3eb-d26ad5af4d86",
+ crash.uuid,
+ )
+ assertEquals(crash.minidumpSuccess, true)
+ assertEquals(crash.isFatal, false)
+ assertEquals(crash.processType, Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD)
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ crash.minidumpPath,
+ )
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ crash.extrasPath,
+ )
+ assertEquals(crash.remoteType, "web")
+ }
+
+ @Test
+ fun `to and from bundle`() {
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "minidumpPath",
+ true,
+ "extrasPath",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ val bundle = crash.toBundle()
+ val otherCrash = Crash.NativeCodeCrash.fromBundle(bundle)
+
+ assertEquals(crash, otherCrash)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.kt
new file mode 100644
index 0000000000..f8400df289
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.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.lib.crash
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UncaughtExceptionCrashTest {
+
+ @Test
+ fun `UncaughtExceptionCrash wraps exception`() {
+ val exception = RuntimeException("Kaput")
+
+ val crash = Crash.UncaughtExceptionCrash(0, exception, arrayListOf())
+
+ assertEquals(exception, crash.throwable)
+ }
+
+ @Test
+ fun `to and from bundle`() {
+ val exception = RuntimeException("Kaput")
+ val crash = Crash.UncaughtExceptionCrash(0, exception, arrayListOf())
+
+ val bundle = crash.toBundle()
+ val otherCrash = Crash.UncaughtExceptionCrash.fromBundle(bundle)
+
+ assertEquals(crash, otherCrash)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt
new file mode 100644
index 0000000000..14f607b038
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt
@@ -0,0 +1,122 @@
+/* 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.lib.crash.handler
+
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class CrashHandlerServiceTest {
+ private var service: CrashHandlerService? = null
+ private var reporter: CrashReporter? = null
+ private val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setUp() {
+ service = spy(Robolectric.setupService(CrashHandlerService::class.java))
+ reporter = spy(
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ services = listOf(mock()),
+ nonFatalCrashIntent = mock(),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ),
+ ).install(testContext)
+
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "uuid",
+ "94f66ed7-50c7-41d1-96a7-299139a8c2af",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+
+ service!!.startService(intent)
+ }
+
+ @After
+ fun tearDown() {
+ service!!.stopService(intent)
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `CrashHandlerService forwards main process native code crash to crash reporter`() = runTestOnMain {
+ doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
+
+ intent.putExtra("processType", "MAIN")
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
+ verify(reporter)!!.onCrash(any(), any())
+ verify(reporter)!!.sendCrashReport(any(), any())
+ verify(reporter, never())!!.sendNonFatalCrashIntent(any(), any())
+ }
+
+ @Test
+ fun `CrashHandlerService forwards foreground child process native code crash to crash reporter`() = runTestOnMain {
+ doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
+
+ intent.putExtra("processType", "FOREGROUND_CHILD")
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
+ verify(reporter)!!.onCrash(any(), any())
+ verify(reporter)!!.sendNonFatalCrashIntent(any(), any())
+ verify(reporter, never())!!.sendCrashReport(any(), any())
+ }
+
+ @Test
+ fun `CrashHandlerService forwards background child process native code crash to crash reporter`() = runTestOnMain {
+ doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
+
+ intent.putExtra("processType", "BACKGROUND_CHILD")
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
+ verify(reporter)!!.onCrash(any(), any())
+ verify(reporter)!!.sendCrashReport(any(), any())
+ verify(reporter, never())!!.sendNonFatalCrashIntent(any(), any())
+ }
+
+ @Test
+ fun `CrashHandlerService null intent in onStartCommand`() = runTestOnMain {
+ doNothing().`when`(service)!!.handleCrashIntent(any(), any())
+
+ service!!.onStartCommand(null, 0, 0)
+
+ verify(service, times(0))!!.handleCrashIntent(any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt
new file mode 100644
index 0000000000..348c36df10
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt
@@ -0,0 +1,98 @@
+/* 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.lib.crash.handler
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class ExceptionHandlerTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `ExceptionHandler forwards crashes to CrashReporter`() {
+ val service: CrashReporterService = mock()
+
+ val crashReporter = spy(
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ),
+ )
+
+ val handler = ExceptionHandler(
+ testContext,
+ crashReporter,
+ )
+
+ val exception = RuntimeException("Hello World")
+ handler.uncaughtException(Thread.currentThread(), exception)
+
+ verify(crashReporter).onCrash(eq(testContext), any())
+ verify(crashReporter).sendCrashReport(eq(testContext), any())
+ }
+
+ @Test
+ fun `ExceptionHandler invokes default exception handler`() {
+ val defaultExceptionHandler: Thread.UncaughtExceptionHandler = mock()
+
+ val crashReporter = CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ services = listOf(
+ object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? = null
+
+ override fun report(crash: Crash.NativeCodeCrash): String? = null
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? = null
+ },
+ ),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val handler = ExceptionHandler(
+ testContext,
+ crashReporter,
+ defaultExceptionHandler,
+ )
+
+ verify(defaultExceptionHandler, never()).uncaughtException(any(), any())
+
+ val exception = RuntimeException()
+ handler.uncaughtException(Thread.currentThread(), exception)
+
+ verify(defaultExceptionHandler).uncaughtException(Thread.currentThread(), exception)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.kt
new file mode 100644
index 0000000000..ff4d8438ef
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.kt
@@ -0,0 +1,175 @@
+/* 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.lib.crash.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class CrashNotificationTest {
+ @Test
+ fun shouldShowNotificationInsteadOfPrompt() {
+ val foregroundChildNativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 21))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 22))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 23))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 24))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 25))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 26))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 27))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 28))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 29))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 30))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 31))
+
+ val mainProcessNativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 21))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 22))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 23))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 24))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 25))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 26))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 27))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 28))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 29))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 30))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 31))
+
+ val backgroundChildNativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 21))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 22))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 23))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 24))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 25))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 26))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 27))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 28))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 29))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 30))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 31))
+
+ val exceptionCrash = Crash.UncaughtExceptionCrash(0, RuntimeException("Boom"), arrayListOf())
+
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 21))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 22))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 23))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 24))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 25))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 26))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 27))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 28))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 29))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 30))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 31))
+ }
+
+ @Test
+ fun `Showing notification`() {
+ val notificationManager = testContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val shadowNotificationManager = shadowOf(notificationManager)
+
+ assertEquals(0, shadowNotificationManager.notificationChannels.size)
+ assertEquals(0, shadowNotificationManager.size())
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Boom"), arrayListOf())
+ val notificationManagerCompat = spy(NotificationManagerCompat.from(testContext))
+ val notificationsDelegate = NotificationsDelegate(notificationManagerCompat)
+
+ whenever(notificationManagerCompat.areNotificationsEnabled()).thenReturn(true)
+
+ val crashNotification = CrashNotification(
+ testContext,
+ crash,
+ CrashReporter.PromptConfiguration(
+ appName = "TestApp",
+ ),
+ notificationsDelegate = notificationsDelegate,
+ )
+ crashNotification.show()
+
+ assertEquals(1, shadowNotificationManager.notificationChannels.size)
+ assertEquals(
+ "Crashes",
+ (shadowNotificationManager.notificationChannels[0] as NotificationChannel).name,
+ )
+
+ assertEquals(1, shadowNotificationManager.size())
+ }
+
+ @Test
+ fun `not showing notification when permission is denied`() {
+ val notificationManager = testContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val shadowNotificationManager = shadowOf(notificationManager)
+
+ assertEquals(0, shadowNotificationManager.notificationChannels.size)
+ assertEquals(0, shadowNotificationManager.size())
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Boom"), arrayListOf())
+ val notificationManagerCompat = spy(NotificationManagerCompat.from(testContext))
+ val notificationsDelegate = spy(NotificationsDelegate(notificationManagerCompat))
+
+ whenever(notificationManagerCompat.areNotificationsEnabled()).thenReturn(false)
+
+ val crashNotification = CrashNotification(
+ testContext,
+ crash,
+ CrashReporter.PromptConfiguration(
+ appName = "TestApp",
+ ),
+ notificationsDelegate = notificationsDelegate,
+ )
+ crashNotification.show()
+
+ assertEquals(1, shadowNotificationManager.notificationChannels.size)
+ assertEquals(
+ "Crashes",
+ (shadowNotificationManager.notificationChannels[0] as NotificationChannel).name,
+ )
+
+ assertEquals(0, shadowNotificationManager.size())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt
new file mode 100644
index 0000000000..4459d75cae
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt
@@ -0,0 +1,263 @@
+/* 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.lib.crash.prompt
+
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.view.View
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ActivityScenario.launch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.prompt.CrashReporterActivity.Companion.PREFERENCE_KEY_SEND_REPORT
+import mozilla.components.lib.crash.prompt.CrashReporterActivity.Companion.SHARED_PREFERENCES_NAME
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+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.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import kotlin.coroutines.CoroutineContext
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class CrashReporterActivityTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Mock
+ lateinit var service: CrashReporterService
+
+ @Before
+ fun setUp() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `Pressing close button sends report`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ // When
+ activity.closeButton.performClick()
+ }
+
+ // Await for all coroutines to be finished
+ advanceUntilIdle()
+
+ // Then
+ verify(service).report(crash)
+ }
+
+ @Test
+ fun `Pressing restart button sends report`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ // When
+ activity.restartButton.performClick()
+ }
+
+ // Await for all coroutines to be finished
+ advanceUntilIdle()
+
+ // Then
+ verify(service).report(crash)
+ }
+
+ @Test
+ fun `Custom message is set on CrashReporterActivity`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ promptConfiguration = CrashReporter.PromptConfiguration(
+ message = "Hello World!",
+ theme = android.R.style.Theme_DeviceDefault, // Yolo!
+ ),
+ services = listOf(mock()),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ // Then
+ assertEquals("Hello World!", activity.messageView.text)
+ }
+ }
+
+ @Test
+ fun `Sending crash report saves checkbox state`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ // When
+ activity.sendCheckbox.isChecked = true
+
+ // Then
+ assertFalse(activity.isSendReportPreferenceEnabled)
+
+ // When
+ activity.restartButton.performClick()
+
+ // Then
+ assertTrue(activity.isSendReportPreferenceEnabled)
+ }
+ }
+
+ @Test
+ fun `Restart button visible for main process crash`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ assertEquals(activity.restartButton.visibility, View.VISIBLE)
+ }
+ }
+
+ @Test
+ fun `Restart button hidden for background child process crash`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ assertEquals(activity.restartButton.visibility, View.GONE)
+ }
+ }
+
+ @Test
+ fun `WHEN crash is native AND background child THEN is background returns true`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.NativeCodeCrash(
+ 123,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ assert(activity.isRecoverableBackgroundCrash(crash))
+ }
+ }
+}
+
+/**
+ * Launch activity scenario for certain [crash].
+ */
+@ExperimentalCoroutinesApi
+private fun CoroutineContext.launchActivityWithCrash(
+ crash: Crash,
+): ActivityScenario<CrashReporterActivity> = run {
+ val intent = Intent(testContext, CrashReporterActivity::class.java)
+ .also { crash.fillIn(it) }
+
+ launch<CrashReporterActivity>(intent).apply {
+ onActivity { activity ->
+ activity.reporterCoroutineContext = this@run
+ }
+ }
+}
+
+// Views
+private val CrashReporterActivity.closeButton: Button get() = binding.closeButton
+private val CrashReporterActivity.restartButton: Button get() = binding.restartButton
+private val CrashReporterActivity.messageView: TextView get() = binding.messageView
+private val CrashReporterActivity.sendCheckbox: CheckBox get() = binding.sendCheckbox
+
+// Preferences
+private val CrashReporterActivity.preferences: SharedPreferences
+ get() = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+private val CrashReporterActivity.isSendReportPreferenceEnabled: Boolean
+ get() = preferences.getBoolean(PREFERENCE_KEY_SEND_REPORT, false)
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt
new file mode 100644
index 0000000000..1097d3521f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt
@@ -0,0 +1,464 @@
+/* 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.lib.crash.service
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.GleanMetrics.CrashMetrics
+import mozilla.components.service.glean.testing.GleanTestRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import java.io.File
+import java.util.Calendar
+import java.util.Date
+import java.util.GregorianCalendar
+import mozilla.components.lib.crash.GleanMetrics.Crash as GleanCrash
+import mozilla.components.lib.crash.GleanMetrics.Pings as GleanPings
+
+@RunWith(AndroidJUnit4::class)
+class GleanCrashReporterServiceTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ @get:Rule
+ val gleanRule = GleanTestRule(context)
+
+ private fun crashCountJson(key: String): String = "{\"type\":\"count\",\"label\":\"$key\"}"
+
+ private fun crashPingJson(uptime: Long, type: String, time: Long, startup: Boolean): String =
+ "{\"type\":\"ping\",\"uptimeNanos\":$uptime,\"processType\":\"$type\"," +
+ "\"timeMillis\":$time,\"startup\":$startup,\"reason\":\"crash\"}"
+
+ private fun crashPingJsonWithRemoteType(
+ uptime: Long,
+ type: String,
+ time: Long,
+ startup: Boolean,
+ remoteType: String,
+ ): String =
+ "{\"type\":\"ping\",\"uptimeNanos\":$uptime,\"processType\":\"$type\"," +
+ "\"timeMillis\":$time,\"startup\":$startup,\"reason\":\"crash\",\"remoteType\":\"$remoteType\"}"
+
+ private fun exceptionPingJson(uptime: Long, time: Long, startup: Boolean): String =
+ "{\"type\":\"ping\",\"uptimeNanos\":$uptime,\"processType\":\"main\"," +
+ "\"timeMillis\":$time,\"startup\":$startup,\"reason\":\"crash\",\"cause\":\"java_exception\"}"
+
+ @Test
+ fun `GleanCrashReporterService records all crash types`() {
+ val crashTypes = hashMapOf(
+ GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY to Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ ),
+ GleanCrashReporterService.FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY to Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "web",
+ ),
+ GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY to Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ ),
+ GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY to Crash.UncaughtExceptionCrash(
+ 0,
+ RuntimeException("Test"),
+ arrayListOf(),
+ ),
+ GleanCrashReporterService.CAUGHT_EXCEPTION_KEY to RuntimeException("Test"),
+ )
+
+ for ((type, crash) in crashTypes) {
+ // Because of how Glean is implemented, it can potentially persist information between
+ // tests or even between test classes, so we compensate by capturing the initial value
+ // to compare to.
+ val initialValue = try {
+ CrashMetrics.crashCount[type].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ run {
+ val service = spy(GleanCrashReporterService(context))
+
+ assertFalse("No previous persisted crashes must exist", service.file.exists())
+
+ when (crash) {
+ is Crash.NativeCodeCrash -> service.record(crash)
+ is Crash.UncaughtExceptionCrash -> service.record(crash)
+ is Throwable -> service.record(crash)
+ }
+
+ assertTrue("Persistence file must exist", service.file.exists())
+ val lines = service.file.readLines()
+ assertEquals(
+ "Must be $type",
+ crashCountJson(type),
+ lines.first(),
+ )
+ }
+
+ // Initialize a fresh GleanCrashReporterService and ensure metrics are recorded in Glean
+ run {
+ GleanCrashReporterService(context)
+
+ assertEquals(
+ "Glean must record correct value",
+ 1,
+ CrashMetrics.crashCount[type].testGetValue()!! - initialValue,
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `GleanCrashReporterService correctly handles multiple crashes in a single file`() {
+ val initialExceptionValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+ val initialMainProcessNativeCrashValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ val initialForegroundChildProcessNativeCrashValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ val initialBackgroundChildProcessNativeCrashValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ run {
+ val service = spy(GleanCrashReporterService(context))
+
+ assertFalse("No previous persisted crashes must exist", service.file.exists())
+
+ val uncaughtExceptionCrash =
+ Crash.UncaughtExceptionCrash(0, RuntimeException("Test"), arrayListOf())
+ val mainProcessNativeCodeCrash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val foregroundChildProcessNativeCodeCrash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "web",
+ )
+ val backgroundChildProcessNativeCodeCrash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val extensionProcessNativeCodeCrash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "extension",
+ )
+
+ // Record some crashes
+ service.record(uncaughtExceptionCrash)
+ service.record(mainProcessNativeCodeCrash)
+ service.record(uncaughtExceptionCrash)
+ service.record(foregroundChildProcessNativeCodeCrash)
+ service.record(backgroundChildProcessNativeCodeCrash)
+ service.record(extensionProcessNativeCodeCrash)
+
+ // Make sure the file exists
+ assertTrue("Persistence file must exist", service.file.exists())
+
+ // Get the file lines
+ val lines = service.file.readLines().iterator()
+ assertEquals(
+ "element must be uncaught exception",
+ crashCountJson(GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be uncaught exception ping",
+ exceptionPingJson(0, 0, false),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be main process native code crash",
+ crashCountJson(GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be main process crash ping",
+ crashPingJson(0, "main", 0, false),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be uncaught exception",
+ crashCountJson(GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY),
+ lines.next(), // skip crash ping line in this test
+ )
+ assertEquals(
+ "element must be uncaught exception ping",
+ exceptionPingJson(0, 0, false),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be foreground child process native code crash",
+ crashCountJson(GleanCrashReporterService.FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be foreground process crash ping",
+ crashPingJsonWithRemoteType(0, "content", 0, false, "web"),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be background child process native code crash",
+ crashCountJson(GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines.next(), // skip crash ping line
+ )
+ assertEquals(
+ "element must be background process crash ping",
+ crashPingJson(0, "utility", 0, false),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be background child process native code crash",
+ crashCountJson(GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be extensions process crash ping",
+ crashPingJsonWithRemoteType(0, "content", 0, false, "extension"),
+ lines.next(),
+ )
+ assertFalse(lines.hasNext())
+ }
+
+ // Initialize a fresh GleanCrashReporterService and ensure metrics are recorded in Glean
+ run {
+ GleanCrashReporterService(context)
+
+ assertEquals(
+ "Glean must record correct value",
+ 2,
+ CrashMetrics.crashCount[GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY].testGetValue()!! - initialExceptionValue,
+ )
+ assertEquals(
+ "Glean must record correct value",
+ 1,
+ CrashMetrics.crashCount[GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!! - initialMainProcessNativeCrashValue,
+ )
+ assertEquals(
+ "Glean must record correct value",
+ 1,
+ CrashMetrics.crashCount[GleanCrashReporterService.FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!! - initialForegroundChildProcessNativeCrashValue,
+ )
+ assertEquals(
+ "Glean must record correct value",
+ 2,
+ CrashMetrics.crashCount[GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!! - initialBackgroundChildProcessNativeCrashValue,
+ )
+ }
+ }
+
+ @Test
+ fun `GleanCrashReporterService does not crash if it can't write to it's file`() {
+ val file =
+ spy(File(context.applicationInfo.dataDir, GleanCrashReporterService.CRASH_FILE_NAME))
+ whenever(file.canWrite()).thenReturn(false)
+ val service = spy(GleanCrashReporterService(context, file))
+
+ assertFalse("No previous persisted crashes must exist", service.file.exists())
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Test"), arrayListOf())
+ service.record(crash)
+
+ assertTrue("Persistence file must exist", service.file.exists())
+ val lines = service.file.readLines()
+ assertEquals("Must be empty due to mocked write error", 0, lines.count())
+ }
+
+ @Test
+ fun `GleanCrashReporterService does not crash if the persistent file is corrupted`() {
+ // Because of how Glean is implemented, it can potentially persist information between
+ // tests or even between test classes, so we compensate by capturing the initial value
+ // to compare to.
+ val initialValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ run {
+ val service = spy(GleanCrashReporterService(context))
+
+ assertFalse("No previous persisted crashes must exist", service.file.exists())
+
+ val crash = Crash.UncaughtExceptionCrash(
+ 0,
+ RuntimeException("Test"),
+ arrayListOf(),
+ )
+ service.record(crash)
+
+ assertTrue("Persistence file must exist", service.file.exists())
+
+ // Add bad data
+ service.file.appendText("bad data in here\n")
+
+ val lines = service.file.readLines()
+ assertEquals(
+ "must be native code crash",
+ "{\"type\":\"count\",\"label\":\"${GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY}\"}",
+ lines.first(),
+ )
+ assertEquals(
+ "must be uncaught exception ping",
+ exceptionPingJson(0, 0, false),
+ lines[1],
+ )
+ assertEquals("bad data in here", lines[2])
+ }
+
+ run {
+ GleanCrashReporterService(context)
+
+ assertEquals(
+ "Glean must record correct value",
+ 1,
+ CrashMetrics.crashCount[GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY].testGetValue()!! - initialValue,
+ )
+ }
+ }
+
+ @Test
+ fun `GleanCrashReporterService sends crash pings`() {
+ val service = spy(GleanCrashReporterService(context))
+
+ val crash = Crash.NativeCodeCrash(
+ 12340000,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ service.record(crash)
+
+ assertTrue("Persistence file must exist", service.file.exists())
+
+ val lines = service.file.readLines()
+ assertEquals(
+ "First element must be main process native code crash",
+ crashCountJson(GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines[0],
+ )
+ assertEquals(
+ "Second element must be main process crash ping",
+ crashPingJson(0, "main", 12340000, false),
+ lines[1],
+ )
+
+ run {
+ var pingReceived = false
+ GleanPings.crash.testBeforeNextSubmit { _ ->
+ val date = GregorianCalendar().apply {
+ time = Date(12340000)
+ }
+ date.set(Calendar.SECOND, 0)
+ date.set(Calendar.MILLISECOND, 0)
+ assertEquals(date.time, GleanCrash.time.testGetValue())
+ assertEquals(0L, GleanCrash.uptime.testGetValue())
+ assertEquals("main", GleanCrash.processType.testGetValue())
+ assertEquals(false, GleanCrash.startup.testGetValue())
+ assertEquals("os_fault", GleanCrash.cause.testGetValue())
+ assertEquals("", GleanCrash.remoteType.testGetValue())
+ pingReceived = true
+ }
+
+ GleanCrashReporterService(context)
+ assertTrue("Expected ping to be sent", pingReceived)
+ }
+ }
+
+ @Test
+ fun `GleanCrashReporterService serialized pings are forward compatible`() {
+ val service = spy(GleanCrashReporterService(context))
+
+ // Original ping fields (no e.g. `cause` field)
+ service.file.appendText(
+ "{\"type\":\"ping\",\"uptimeNanos\":0,\"processType\":\"main\"," +
+ "\"timeMillis\":0,\"startup\":false,\"reason\":\"crash\"}\n",
+ )
+
+ assertTrue("Persistence file must exist", service.file.exists())
+
+ run {
+ var pingReceived = false
+ GleanPings.crash.testBeforeNextSubmit { _ ->
+ val date = GregorianCalendar().apply {
+ time = Date(0)
+ }
+ date.set(Calendar.SECOND, 0)
+ date.set(Calendar.MILLISECOND, 0)
+ assertEquals(date.time, GleanCrash.time.testGetValue())
+ assertEquals(0L, GleanCrash.uptime.testGetValue())
+ assertEquals("main", GleanCrash.processType.testGetValue())
+ assertEquals(false, GleanCrash.startup.testGetValue())
+ assertEquals("os_fault", GleanCrash.cause.testGetValue())
+ assertEquals("", GleanCrash.remoteType.testGetValue())
+ pingReceived = true
+ }
+
+ GleanCrashReporterService(context)
+ assertTrue("Expected ping to be sent", pingReceived)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt
new file mode 100644
index 0000000000..0b2ace44fd
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt
@@ -0,0 +1,693 @@
+/* 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.lib.crash.service
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.io.Resources.getResource
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.support.test.any
+import mozilla.components.support.test.robolectric.testContext
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.doNothing
+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 java.io.BufferedReader
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.io.InputStreamReader
+import java.util.zip.GZIPInputStream
+
+@RunWith(AndroidJUnit4::class)
+class MozillaSocorroServiceTest {
+
+ @Test
+ fun `MozillaSocorroService sends native code crashes to GeckoView crash reporter`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ doReturn("").`when`(service).sendReport(anyLong(), any(), any(), any(), anyBoolean(), anyBoolean(), any())
+
+ val crash = Crash.NativeCodeCrash(
+ 123,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service).sendReport(123, null, crash.minidumpPath, crash.extrasPath, true, false, crash.breadcrumbs)
+ }
+
+ @Test
+ fun `MozillaSocorroService generated server URL have no spaces`() {
+ val service = MozillaSocorroService(
+ testContext,
+ "Test App",
+ versionName = "test version name",
+ )
+
+ assertFalse(service.serverUrl!!.contains(" "))
+ assertFalse(service.serverUrl!!.contains("}"))
+ assertFalse(service.serverUrl!!.contains("{"))
+ }
+
+ @Test
+ fun `MozillaSocorroService send uncaught exception crashes`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ doReturn("").`when`(service).sendReport(anyLong(), any(), any(), any(), anyBoolean(), anyBoolean(), any())
+
+ val crash = Crash.UncaughtExceptionCrash(123, RuntimeException("Test"), arrayListOf())
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service).sendReport(123, crash.throwable, null, null, false, true, crash.breadcrumbs)
+ }
+
+ @Test
+ fun `MozillaSocorroService do not send caught exception`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ doReturn("").`when`(service).sendReport(anyLong(), any(), any(), any(), anyBoolean(), anyBoolean(), any())
+ val throwable = RuntimeException("Test")
+ val breadcrumbs: ArrayList<Breadcrumb> = arrayListOf()
+ val id = service.report(throwable, breadcrumbs)
+
+ verify(service).report(throwable, breadcrumbs)
+ verify(service, never()).sendReport(anyLong(), any(), any(), any(), anyBoolean(), anyBoolean(), any())
+ assertNull(id)
+ }
+
+ @Test
+ fun `MozillaSocorroService native fatal crash request is correct`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+
+ val fileInputStream =
+ ByteArrayInputStream(mockWebServer.takeRequest().body.inputStream().readBytes())
+ val inputStream = GZIPInputStream(fileInputStream)
+ val reader = InputStreamReader(inputStream)
+ val bufferedReader = BufferedReader(reader)
+ val request = bufferedReader.readText()
+
+ assert(request.contains("name=Android_ProcessName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=ProductID\r\n\r\n{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
+ assert(request.contains("name=Vendor\r\n\r\nN/A"))
+ assert(request.contains("name=ReleaseChannel\r\n\r\nN/A"))
+ assert(request.contains("name=Android_PackageName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=Android_Device\r\n\r\nrobolectric"))
+ assert(request.contains("name=CrashType\r\n\r\n$FATAL_NATIVE_CRASH_TYPE"))
+ assert(request.contains("name=CrashTime\r\n\r\n123"))
+ assert(request.contains("name=useragent_locale\r\n\r\nen-US"))
+
+ verify(service).report(crash)
+ verify(service).sendReport(123456, null, "dump.path", "extras.path", true, true, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `incorrect file extension is ignored in native fatal crash requests`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "test/minidumps/3fa772dc-dc89-c08d-c03e-7f441c50821e.ini",
+ true,
+ "test/file/66dd8af2-643c-ca11-5178-e61c6819f827",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ doReturn(HashMap<String, String>()).`when`(service).readExtrasFromFile(any())
+ doNothing().`when`(service).sendFile(any(), any(), any(), any(), any())
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service, times(0)).readExtrasFromFile(any())
+ verify(service, times(0)).sendFile(any(), any(), any(), any(), any())
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `incorrect file format is ignored in native fatal crash requests`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "test/minidumps/test.dmp",
+ true,
+ "test/file/test.extra",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ doReturn(HashMap<String, String>()).`when`(service).readExtrasFromFile(any())
+ doNothing().`when`(service).sendFile(any(), any(), any(), any(), any())
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service, times(0)).readExtrasFromFile(any())
+ verify(service, times(0)).sendFile(any(), any(), any(), any(), any())
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `correct file format is used in native fatal crash requests`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "test/minidumps/3fa772dc-dc89-c08d-c03e-7f441c50821e.dmp",
+ true,
+ "test/file/66dd8af2-643c-ca11-5178-e61c6819f827.extra",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ doReturn(HashMap<String, String>()).`when`(service).readExtrasFromFile(any())
+ doNothing().`when`(service).sendFile(any(), any(), any(), any(), any())
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service).readExtrasFromFile(any())
+ verify(service).sendFile(any(), any(), any(), any(), any())
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService parameters is reported correctly`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ version = "test version",
+ buildId = "test build id",
+ vendor = "test vendor",
+ serverUrl = serverUrl.toString(),
+ versionName = "1.0.1",
+ versionCode = "1000",
+ releaseChannel = "test channel",
+ distributionId = "test distribution id",
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+
+ val fileInputStream =
+ ByteArrayInputStream(mockWebServer.takeRequest().body.inputStream().readBytes())
+ val inputStream = GZIPInputStream(fileInputStream)
+ val reader = InputStreamReader(inputStream)
+ val bufferedReader = BufferedReader(reader)
+ val request = bufferedReader.readText()
+
+ assert(request.contains("name=Android_ProcessName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=ProductID\r\n\r\n{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
+ assert(request.contains("name=Vendor\r\n\r\ntest vendor"))
+ assert(request.contains("name=ReleaseChannel\r\n\r\ntest channel"))
+ assert(request.contains("name=Android_PackageName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=Android_Device\r\n\r\nrobolectric"))
+ assert(request.contains("name=CrashType\r\n\r\n$FATAL_NATIVE_CRASH_TYPE"))
+ assert(request.contains("name=CrashTime\r\n\r\n123"))
+ assert(request.contains("name=GeckoViewVersion\r\n\r\ntest version"))
+ assert(request.contains("name=BuildID\r\n\r\ntest build id"))
+ assert(request.contains("name=Version\r\n\r\n1.0.1"))
+ assert(request.contains("name=ApplicationBuildID\r\n\r\n1000"))
+ assert(request.contains("name=useragent_locale\r\n\r\nen-US"))
+ assert(request.contains("name=DistributionID\r\n\r\ntest distribution id"))
+
+ verify(service).report(crash)
+ verify(service).sendReport(123456, null, "dump.path", "extras.path", true, true, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService native non-fatal crash request is correct`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ vendor = "Mozilla",
+ releaseChannel = "nightly",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+
+ val fileInputStream =
+ ByteArrayInputStream(mockWebServer.takeRequest().body.inputStream().readBytes())
+ val inputStream = GZIPInputStream(fileInputStream)
+ val reader = InputStreamReader(inputStream)
+ val bufferedReader = BufferedReader(reader)
+ val request = bufferedReader.readText()
+
+ assert(request.contains("name=Android_ProcessName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=ProductID\r\n\r\n{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
+ assert(request.contains("name=Vendor\r\n\r\nMozilla"))
+ assert(request.contains("name=ReleaseChannel\r\n\r\nnightly"))
+ assert(request.contains("name=Android_PackageName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=Android_Device\r\n\r\nrobolectric"))
+ assert(request.contains("name=CrashType\r\n\r\n$NON_FATAL_NATIVE_CRASH_TYPE"))
+ assert(request.contains("name=CrashTime\r\n\r\n123"))
+ assert(request.contains("name=useragent_locale\r\n\r\nen-US"))
+
+ verify(service).report(crash)
+ verify(service).sendReport(123456, null, "dump.path", "extras.path", true, false, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService uncaught exception request is correct`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ vendor = "Mozilla",
+ releaseChannel = "nightly",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.UncaughtExceptionCrash(123456, RuntimeException("Test"), arrayListOf())
+ service.report(crash)
+
+ val fileInputStream =
+ ByteArrayInputStream(mockWebServer.takeRequest().body.inputStream().readBytes())
+ val inputStream = GZIPInputStream(fileInputStream)
+ val reader = InputStreamReader(inputStream)
+ val bufferedReader = BufferedReader(reader)
+ val request = bufferedReader.readText()
+
+ assert(request.contains("name=JavaStackTrace\r\n\r\njava.lang.RuntimeException: Test"))
+ assert(request.contains("name=JavaException\r\n\r\n{\"exception\":{\"values\":[{\"stacktrace\":{\"frames\":[{\"module\":\"mozilla.components.lib.crash.service.MozillaSocorroServiceTest\",\"function\":\"MozillaSocorroService uncaught exception request is correct\",\"in_app\":true"))
+ assert(request.contains("name=Android_ProcessName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=ProductID\r\n\r\n{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
+ assert(request.contains("name=Vendor\r\n\r\nMozilla"))
+ assert(request.contains("name=ReleaseChannel\r\n\r\nnightly"))
+ assert(request.contains("name=Android_PackageName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=Android_Device\r\n\r\nrobolectric"))
+ assert(request.contains("name=CrashType\r\n\r\n$UNCAUGHT_EXCEPTION_TYPE"))
+ assert(request.contains("name=CrashTime\r\n\r\n123"))
+ assert(request.contains("name=useragent_locale\r\n\r\nen-US"))
+
+ verify(service).report(crash)
+ verify(service).sendReport(123456, crash.throwable, null, null, false, true, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService handles 200 response correctly`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.UncaughtExceptionCrash(123, RuntimeException("Test"), arrayListOf())
+ service.report(crash)
+
+ mockWebServer.shutdown()
+ verify(service).report(crash)
+ verify(service).sendReport(123, crash.throwable, null, null, false, true, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService handles 404 response correctly`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(MockResponse().setResponseCode(404).setBody("error"))
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123,
+ null,
+ true,
+ null,
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+ mockWebServer.shutdown()
+
+ verify(service).report(crash)
+ verify(service).sendReport(123, null, crash.minidumpPath, crash.extrasPath, true, false, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService parses extrasFile correctly`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ val file = File(getResource("TestExtrasFile").file)
+ val extrasMap = service.readExtrasFromFile(file)
+
+ assertEquals(extrasMap.size, 25)
+ assertEquals(extrasMap["ContentSandboxLevel"], "2")
+ assertEquals(extrasMap["TelemetryEnvironment"], "{\"EscapedField\":\"EscapedData\n\nfoo\"}")
+ assertEquals(extrasMap["EMCheckCompatibility"], "true")
+ assertEquals(extrasMap["ProductName"], "Firefox")
+ assertEquals(extrasMap["ContentSandboxCapabilities"], "119")
+ assertEquals(extrasMap["TelemetryClientId"], "")
+ assertEquals(extrasMap["Vendor"], "Mozilla")
+ assertEquals(extrasMap["InstallTime"], "1000000000")
+ assertEquals(extrasMap["Theme"], "classic/1.0")
+ assertEquals(extrasMap["ReleaseChannel"], "default")
+ assertEquals(extrasMap["SafeMode"], "0")
+ assertEquals(extrasMap["ContentSandboxCapable"], "1")
+ assertEquals(extrasMap["useragent_locale"], "en-US")
+ assertEquals(extrasMap["Version"], "55.0a1")
+ assertEquals(extrasMap["BuildID"], "20170512114708")
+ assertEquals(extrasMap["ProductID"], "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
+ assertEquals(extrasMap["TelemetryServerURL"], "")
+ assertEquals(extrasMap["DOMIPCEnabled"], "1")
+ assertEquals(extrasMap["Add-ons"], "")
+ assertEquals(extrasMap["CrashTime"], "1494582646")
+ assertEquals(extrasMap["UptimeTS"], "14.9179586")
+ assertEquals(extrasMap["ThreadIdNameMapping"], "")
+ assertEquals(extrasMap["ContentSandboxEnabled"], "1")
+ assertEquals(extrasMap["StartupTime"], "1000000000")
+ assertFalse(extrasMap.contains("URL"))
+ assertFalse(extrasMap.contains("ServerURL"))
+ assertFalse(extrasMap.contains("StackTraces"))
+ }
+
+ @Test
+ fun `MozillaSocorroService parses legacyExtraFile correctly`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ val file = File(getResource("TestLegacyExtrasFile").file)
+ val extrasMap = service.readExtrasFromFile(file)
+
+ assertEquals(extrasMap.size, 25)
+ assertEquals(extrasMap["ContentSandboxLevel"], "2")
+ assertEquals(extrasMap["TelemetryEnvironment"], "{\"EscapedField\":\"EscapedData\n\nfoo\"}")
+ assertEquals(extrasMap["EMCheckCompatibility"], "true")
+ assertEquals(extrasMap["ProductName"], "Firefox")
+ assertEquals(extrasMap["ContentSandboxCapabilities"], "119")
+ assertEquals(extrasMap["TelemetryClientId"], "")
+ assertEquals(extrasMap["Vendor"], "Mozilla")
+ assertEquals(extrasMap["InstallTime"], "1000000000")
+ assertEquals(extrasMap["Theme"], "classic/1.0")
+ assertEquals(extrasMap["ReleaseChannel"], "default")
+ assertEquals(extrasMap["SafeMode"], "0")
+ assertEquals(extrasMap["ContentSandboxCapable"], "1")
+ assertEquals(extrasMap["useragent_locale"], "en-US")
+ assertEquals(extrasMap["Version"], "55.0a1")
+ assertEquals(extrasMap["BuildID"], "20170512114708")
+ assertEquals(extrasMap["ProductID"], "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
+ assertEquals(extrasMap["TelemetryServerURL"], "")
+ assertEquals(extrasMap["DOMIPCEnabled"], "1")
+ assertEquals(extrasMap["Add-ons"], "")
+ assertEquals(extrasMap["CrashTime"], "1494582646")
+ assertEquals(extrasMap["UptimeTS"], "14.9179586")
+ assertEquals(extrasMap["ThreadIdNameMapping"], "")
+ assertEquals(extrasMap["ContentSandboxEnabled"], "1")
+ assertEquals(extrasMap["StartupTime"], "1000000000")
+ assertFalse(extrasMap.contains("URL"))
+ assertFalse(extrasMap.contains("ServerURL"))
+ assertFalse(extrasMap.contains("StackTraces"))
+ }
+
+ @Test
+ fun `MozillaSocorroService handles bad extrasFile correctly`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ val file = File(getResource("BadTestExtrasFile").file)
+ val extrasMap = service.readExtrasFromFile(file)
+
+ assertEquals(extrasMap.size, 0)
+ }
+
+ @Test
+ fun `MozillaSocorroService unescape strings correctly`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ val test1 = "\\\\\\\\"
+ val expected1 = "\\"
+ assert(service.unescape(test1) == expected1)
+
+ val test2 = "\\\\n"
+ val expected2 = "\n"
+ assert(service.unescape(test2) == expected2)
+
+ val test3 = "\\\\t"
+ val expected3 = "\t"
+ assert(service.unescape(test3) == expected3)
+
+ val test4 = "\\\\\\\\\\\\t\\\\t\\\\n\\\\\\\\"
+ val expected4 = "\\\t\t\n\\"
+ assert(service.unescape(test4) == expected4)
+ }
+
+ @Test
+ fun `MozillaSocorroService returns crash id from Socorro`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+
+ val service = MozillaSocorroService(
+ testContext,
+ "Test App",
+ "{1234-1234-1234}",
+ "0.1",
+ "1.0",
+ "Mozilla Test",
+ mockWebServer.url("/").toString(),
+ "0.0.1",
+ "123",
+ "test channel",
+ "test distribution id",
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val id = service.report(crash)
+
+ assertEquals("bp-924121d3-4de3-4b32-ab12-026fc0190928", id)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
new file mode 100644
index 0000000000..e44dc64c9a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
@@ -0,0 +1,169 @@
+/* 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.lib.crash.service
+
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class SendCrashReportServiceTest {
+ private var service: SendCrashReportService? = null
+ private val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setUp() {
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("fatal", false)
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putParcelableArrayListExtra("breadcrumbs", null)
+ service = spy(Robolectric.setupService(SendCrashReportService::class.java))
+ service?.startService(intent)
+ }
+
+ @After
+ fun tearDown() {
+ service?.stopService(intent)
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `Send crash report will forward same crash to crash service`() {
+ var caughtCrash: Crash.NativeCodeCrash? = null
+ val crashReporter = spy(
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ services = listOf(
+ object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? {
+ fail("Didn't expect uncaught exception crash")
+ return null
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ caughtCrash = crash
+ return null
+ }
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? {
+ fail("Didn't expect caught exception")
+ return null
+ }
+ },
+ ),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ),
+ ).install(testContext)
+ val originalCrash = Crash.NativeCodeCrash(
+ 123,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ true,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("processType", "FOREGROUND_CHILD")
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putParcelableArrayListExtra("breadcrumbs", null)
+ originalCrash.fillIn(intent)
+
+ service?.onStartCommand(intent, 0, 0)
+ verify(crashReporter).submitReport(eq(originalCrash), any())
+ assertNotNull(caughtCrash)
+
+ val nativeCrash = caughtCrash
+ ?: throw AssertionError("Expected NativeCodeCrash instance")
+
+ assertEquals(123, nativeCrash.timestamp)
+ assertEquals(true, nativeCrash.minidumpSuccess)
+ assertEquals(false, nativeCrash.isFatal)
+ assertEquals(Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD, nativeCrash.processType)
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ nativeCrash.minidumpPath,
+ )
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ nativeCrash.extrasPath,
+ )
+ }
+
+ @Test
+ fun `notification tag and id is added to the report intent`() {
+ val crash: Crash = Crash.NativeCodeCrash(
+ 123,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ val intent = SendCrashReportService.createReportIntent(testContext, crash, "test_tag", 123)
+
+ assertEquals(intent.getStringExtra(NOTIFICATION_TAG_KEY), "test_tag")
+ assertEquals(intent.getIntExtra(NOTIFICATION_ID_KEY, 0), 123)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt
new file mode 100644
index 0000000000..7aa7dcbe55
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt
@@ -0,0 +1,142 @@
+/* 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.lib.crash.service
+
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class SendCrashTelemetryServiceTest {
+ private var service: SendCrashTelemetryService? = null
+ private val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setUp() {
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("fatal", false)
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putParcelableArrayListExtra("breadcrumbs", null)
+ service = spy(Robolectric.setupService(SendCrashTelemetryService::class.java))
+ service?.startService(intent)
+ }
+
+ @After
+ fun tearDown() {
+ service?.stopService(intent)
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `Send crash telemetry will forward same crash to crash telemetry service`() {
+ var caughtCrash: Crash.NativeCodeCrash? = null
+ val crashReporter = spy(
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ telemetryServices = listOf(
+ object : CrashTelemetryService {
+ override fun record(crash: Crash.UncaughtExceptionCrash) {
+ fail("Didn't expect uncaught exception crash")
+ }
+
+ override fun record(crash: Crash.NativeCodeCrash) {
+ caughtCrash = crash
+ }
+
+ override fun record(throwable: Throwable) {
+ fail("Didn't expect caught exception")
+ }
+ },
+ ),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ),
+ ).install(testContext)
+ val originalCrash = Crash.NativeCodeCrash(
+ 123,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ true,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "null",
+ )
+
+ val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("processType", "FOREGROUND_CHILD")
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putParcelableArrayListExtra("breadcrumbs", null)
+ originalCrash.fillIn(intent)
+
+ service?.onStartCommand(intent, 0, 0)
+
+ verify(crashReporter).submitCrashTelemetry(eq(originalCrash), any())
+ assertNotNull(caughtCrash)
+
+ val nativeCrash = caughtCrash
+ ?: throw AssertionError("Expected NativeCodeCrash instance")
+
+ assertEquals(123, nativeCrash.timestamp)
+ assertEquals(true, nativeCrash.minidumpSuccess)
+ assertEquals(false, nativeCrash.isFatal)
+ assertEquals(Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD, nativeCrash.processType)
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ nativeCrash.minidumpPath,
+ )
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ nativeCrash.extrasPath,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile b/mobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile
new file mode 100755
index 0000000000..20098e30d8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile
@@ -0,0 +1 @@
+{"ContentSandboxLevel":"2","TelemetryEnvironment":"{"EscapedField":"EscapedData\\n\\nfoo"}","EMCheckCompatibility":"true","ProductName":"Firefox","ContentSandboxCapabilities":"119","TelemetryClientId":"","Vendor":"Mozilla","InstallTime":"1000000000","Theme":"classic/1.0","ReleaseChannel":"default","ServerURL":"https://crash-reports.mozilla.com","SafeMode":"0","ContentSandboxCapable":"1","useragent_locale":"en-US","Version":"55.0a1","BuildID":"20170512114708","ProductID":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}","TelemetryServerURL":"","DOMIPCEnabled":"1","Add-ons":"","CrashTime":"1494582646","UptimeTS":"14.9179586","ThreadIdNameMapping":"","ContentSandboxEnabled":"1","ProcessType":"content","StartupTime":"1000000000","URL":"about:home"}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile b/mobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile
new file mode 100755
index 0000000000..a95eb68ac3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile
@@ -0,0 +1 @@
+{"ContentSandboxLevel":"2","TelemetryEnvironment":"{\"EscapedField\":\"EscapedData\\n\\nfoo\"}","EMCheckCompatibility":"true","ProductName":"Firefox","ContentSandboxCapabilities":"119","TelemetryClientId":"","Vendor":"Mozilla","InstallTime":"1000000000","Theme":"classic/1.0","ReleaseChannel":"default","ServerURL":"https://crash-reports.mozilla.com","SafeMode":"0","ContentSandboxCapable":"1","useragent_locale":"en-US","Version":"55.0a1","BuildID":"20170512114708","ProductID":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}","TelemetryServerURL":"","DOMIPCEnabled":"1","Add-ons":"","CrashTime":"1494582646","UptimeTS":"14.9179586","ThreadIdNameMapping":"","ContentSandboxEnabled":"1","ProcessType":"content","StartupTime":"1000000000","URL":"about:home","StackTraces":"test"}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile b/mobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile
new file mode 100755
index 0000000000..7260d4d951
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile
@@ -0,0 +1,31 @@
+ContentSandboxLevel=2
+TelemetryEnvironment={"EscapedField":"EscapedData\\n\\nfoo"}
+EMCheckCompatibility=true
+ProductName=Firefox
+ContentSandboxCapabilities=119
+TelemetryClientId=
+Vendor=Mozilla
+InstallTime=1000000000
+Theme=classic/1.0
+ReleaseChannel=default
+ServerURL=https://crash-reports.mozilla.com
+SafeMode=0
+ContentSandboxCapable=1
+useragent_locale=en-US
+Version=55.0a1
+BuildID=20170512114708
+ProductID={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+TelemetryServerURL=
+DOMIPCEnabled=1
+Add-ons=
+CrashTime=1494582646
+UptimeTS=14.9179586
+ThreadIdNameMapping=
+ContentSandboxLevel=2
+ContentSandboxEnabled=1
+ProcessType=content
+DOMIPCEnabled=1
+StartupTime=1000000000
+URL=about:home
+ContentSandboxCapabilities=119
+StackTraces=test \ No newline at end of file
diff --git a/mobile/android/android-components/components/lib/crash/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/crash/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/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/lib/crash/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/crash/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/dataprotect/README.md b/mobile/android/android-components/components/lib/dataprotect/README.md
new file mode 100644
index 0000000000..b9ca9068a4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Libraries > Dataprotect
+
+A component using AndroidKeyStore to protect user data.
+
+## Usage
+
+### 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:lib-dataprotect:{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/lib/dataprotect/build.gradle b/mobile/android/android-components/components/lib/dataprotect/build.gradle
new file mode 100644
index 0000000000..200ee8b2e0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/build.gradle
@@ -0,0 +1,39 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.lib.dataprotect'
+}
+
+dependencies {
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.androidx_annotation
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/lib/dataprotect/proguard-rules.pro b/mobile/android/android-components/components/lib/dataprotect/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/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/lib/dataprotect/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/dataprotect/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/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/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt
new file mode 100644
index 0000000000..8b0b09f2c2
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt
@@ -0,0 +1,314 @@
+/* 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.lib.dataprotect
+
+import android.annotation.TargetApi
+import android.os.Build.VERSION_CODES.M
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import mozilla.components.support.base.log.logger.Logger
+import java.security.GeneralSecurityException
+import java.security.InvalidKeyException
+import java.security.Key
+import java.security.KeyStore
+import java.security.UnrecoverableKeyException
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+import javax.crypto.spec.GCMParameterSpec
+
+private const val KEYSTORE_TYPE = "AndroidKeyStore"
+private const val ENCRYPTED_VERSION = 0x02
+
+@TargetApi(M)
+internal const val CIPHER_ALG = KeyProperties.KEY_ALGORITHM_AES
+
+@TargetApi(M)
+internal const val CIPHER_MOD = KeyProperties.BLOCK_MODE_GCM
+
+@TargetApi(M)
+internal const val CIPHER_PAD = KeyProperties.ENCRYPTION_PADDING_NONE
+internal const val CIPHER_KEY_LEN = 256
+internal const val CIPHER_TAG_LEN = 128
+internal const val CIPHER_SPEC = "$CIPHER_ALG/$CIPHER_MOD/$CIPHER_PAD"
+
+internal const val CIPHER_NONCE_LEN = 12
+
+/**
+ * Wraps the critical functions around a Java KeyStore to better facilitate testing
+ * and instrumenting.
+ *
+ */
+@TargetApi(M)
+open class KeyStoreWrapper {
+ private var keystore: KeyStore? = null
+ private val logger = Logger("KeyStoreWrapper")
+
+ /**
+ * Retrieves the underlying KeyStore, loading it if necessary.
+ */
+ fun getKeyStore(): KeyStore {
+ var ks = keystore
+ if (ks == null) {
+ ks = loadKeyStore()
+ keystore = ks
+ }
+
+ return ks
+ }
+
+ /**
+ * Retrieves the SecretKey for the given label.
+ *
+ * This method queries for a SecretKey with the given label and no passphrase.
+ *
+ * Subclasses override this method if additional properties are needed
+ * to retrieve the key.
+ *
+ * @param label The label to query
+ * @return The key for the given label, or `null` if not present
+ * @throws InvalidKeyException If there is a Key but it is not a SecretKey
+ * @throws NoSuchAlgorithmException If the recovery algorithm is not supported
+ */
+ open fun getKeyFor(label: String): Key? = try {
+ loadKeyStore().getKey(label, null)
+ } catch (e: UnrecoverableKeyException) {
+ logger.error("Failed to get key", e)
+ null
+ }
+
+ /**
+ * Creates a SecretKey for the given label.
+ *
+ * This method generates a SecretKey pre-bound to the `AndroidKeyStore` and configured
+ * with the strongest "algorithm/blockmode/padding" (and key size) available.
+ *
+ * Subclasses override this method to properly associate the generated key with
+ * the given label in the underlying KeyStore.
+ *
+ * @param label The label to associate with the created key
+ * @return The newly-generated key for `label`
+ * @throws NoSuchAlgorithmException If the cipher algorithm is not supported
+ */
+ open fun makeKeyFor(label: String): SecretKey {
+ val spec = KeyGenParameterSpec.Builder(
+ label,
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
+ )
+ .setKeySize(CIPHER_KEY_LEN)
+ .setBlockModes(CIPHER_MOD)
+ .setEncryptionPaddings(CIPHER_PAD)
+ .build()
+ val gen = KeyGenerator.getInstance(CIPHER_ALG, KEYSTORE_TYPE)
+ gen.init(spec)
+ return gen.generateKey()
+ }
+
+ /**
+ * Deletes a key with the given label.
+ *
+ * @param label The label of the associated key to delete
+ * @throws KeyStoreException If there is no key for `label`
+ */
+ fun removeKeyFor(label: String) {
+ getKeyStore().deleteEntry(label)
+ }
+
+ /**
+ * Creates and initializes the KeyStore in use.
+ *
+ * This method loads a`"AndroidKeyStore"` type KeyStore.
+ *
+ * Subclasses override this to load a KeyStore appropriate to the testing environment.
+ *
+ * @return The KeyStore, already initialized
+ * @throws KeyStoreException if the type of store is not supported
+ */
+ open fun loadKeyStore(): KeyStore {
+ val ks = KeyStore.getInstance(KEYSTORE_TYPE)
+ ks.load(null)
+ return ks
+ }
+}
+
+/**
+ * Manages data protection using a system-isolated cryptographic key.
+ *
+ * This class provides for both:
+ * * management for a specific crypto graphic key (identified by a string label)
+ * * protection (encryption/decryption) of data using the managed key
+ *
+ * The specific cryptographic properties are pre-chosen to be the following:
+ * * Algorithm is "AES/GCM/NoPadding"
+ * * Key size is 256 bits
+ * * Tag size is 128 bits
+ *
+ * @property label The label the cryptographic key is identified as
+ * @constructor Creates a new instance around a key identified by the given label
+ *
+ * Unless `manual` is `true`, the key is created if not already present in the
+ * platform's key storage.
+ */
+@TargetApi(M)
+open class Keystore(
+ val label: String,
+ manual: Boolean = false,
+ internal val wrapper: KeyStoreWrapper = KeyStoreWrapper(),
+) {
+ init {
+ if (!manual and !available()) {
+ generateKey()
+ }
+ }
+
+ private fun getKey(): SecretKey? =
+ wrapper.getKeyFor(label) as? SecretKey?
+
+ /**
+ * Determines if the managed key is available for use. Consumers can use this to
+ * determine if the key was somehow lost and should treat any previously-protected
+ * data as invalid.
+ *
+ * @return `true` if the managed key exists and ready for use.
+ */
+ fun available(): Boolean = (getKey() != null)
+
+ /**
+ * Generates the managed key if it does not already exist.
+ *
+ * @return `true` if a new key was generated; `false` if the key already exists and can
+ * be used.
+ * @throws GeneralSecurityException If the key could not be created
+ */
+ @Throws(GeneralSecurityException::class)
+ fun generateKey(): Boolean {
+ val key = wrapper.getKeyFor(label)
+ if (key != null) {
+ when (key) {
+ is SecretKey -> return false
+ else -> throw InvalidKeyException("unsupported key type")
+ }
+ }
+
+ wrapper.makeKeyFor(label)
+
+ return true
+ }
+
+ /**
+ * Deletes the managed key.
+ *
+ * **NOTE:** Once this method returns, any data protected with the (formerly) managed
+ * key cannot be decrypted and therefore is inaccessble.
+ */
+ fun deleteKey() {
+ val key = wrapper.getKeyFor(label)
+ if (key != null) {
+ wrapper.removeKeyFor(label)
+ }
+ }
+
+ /**
+ * Encrypts data using the managed key.
+ *
+ * The output of this method includes the input factors (i.e., initialization vector),
+ * ciphertext, and authentication tag as a single byte string; this output can be passed
+ * directly to [decryptBytes].
+ *
+ * @param plain The "plaintext" data to encrypt
+ * @return The encrypted data to be stored
+ * @throws GeneralSecurityException If the data could not be encrypted
+ */
+ @Throws(GeneralSecurityException::class)
+ open fun encryptBytes(plain: ByteArray): ByteArray {
+ // 5116-style interface = [ inputs || ciphertext || atag ]
+ // - inputs = [ version = 0x02 || cipher.iv (always 12 bytes) ]
+ // - cipher.doFinal() provides [ ciphertext || atag ]
+ // Cipher operations are not thread-safe so we synchronize over them through doFinal to
+ // prevent crashes with quickly repeated encrypt/decrypt operations
+ // https://github.com/mozilla-mobile/android-components/issues/5342
+ synchronized(this) {
+ val cipher = createEncryptCipher()
+ val cdata = cipher.doFinal(plain)
+ val nonce = cipher.iv
+
+ return byteArrayOf(ENCRYPTED_VERSION.toByte()) + nonce + cdata
+ }
+ }
+
+ /**
+ * Decrypts data using the managed key.
+ *
+ * The input of this method is expected to include input factors (i.e., initialization
+ * vector), ciphertext, and authentication tag as a single byte string; it is the direct
+ * output from [encryptBytes].
+ *
+ * @param encrypted The encrypted data to decrypt
+ * @return The decrypted "plaintext" data
+ * @throws KeystoreException If the data could not be decrypted
+ */
+ @Throws(KeystoreException::class)
+ open fun decryptBytes(encrypted: ByteArray): ByteArray {
+ val version = encrypted[0].toInt()
+ if (version != ENCRYPTED_VERSION) {
+ throw KeystoreException("unsupported encrypted version: $version")
+ }
+
+ // Cipher operations are not thread-safe so we synchronize over them through doFinal to
+ // prevent crashes with quickly repeated encrypt/decrypt operations
+ // https://github.com/mozilla-mobile/android-components/issues/5342
+ synchronized(this) {
+ val iv = encrypted.sliceArray(1..CIPHER_NONCE_LEN)
+ val cdata = encrypted.sliceArray((CIPHER_NONCE_LEN + 1)..encrypted.size - 1)
+ val cipher = createDecryptCipher(iv)
+ return cipher.doFinal(cdata)
+ }
+ }
+
+ /**
+ * Create a cipher initialized for encrypting data with the managed key.
+ *
+ * This "low-level" method is useful when a cryptographic context is needed to integrate with
+ * other APIs, such as the `FingerprintManager`.
+ *
+ * **NOTE:** The caller is responsible for associating certain encryption factors, such as
+ * the initialization vector and/or additional authentication data (AAD), with the resulting
+ * ciphertext or decryption will fail.
+ *
+ * @return The [Cipher], initialized and ready to encrypt data with.
+ * @throws GeneralSecurityException If the Cipher could not be created and initialized
+ */
+ @Throws(GeneralSecurityException::class)
+ open fun createEncryptCipher(): Cipher {
+ val key = getKey() ?: throw InvalidKeyException("unknown label: $label")
+ val cipher = Cipher.getInstance(CIPHER_SPEC)
+ cipher.init(Cipher.ENCRYPT_MODE, key)
+
+ return cipher
+ }
+
+ /**
+ * Create a cipher initialized for decrypting data with the managed key.
+ *
+ * This "low-level" method is useful when a cryptographic context is needed to integrate with
+ * other APIs, such as the `FingerprintManager`.
+ *
+ * **NOTE:** The caller is responsible for associating certain encryption factors, such as
+ * the initialization vector and/or additional authentication data (AAD), with the stored
+ * ciphertext or decryption will fail.
+ *
+ * @param iv The initialization vector/nonce to decrypt with
+ * @return The [Cipher], initialized and ready to decrypt data with.
+ * @throws GeneralSecurityException If the cipher could not be created and initialized
+ */
+ @Throws(GeneralSecurityException::class)
+ open fun createDecryptCipher(iv: ByteArray): Cipher {
+ val key = getKey() ?: throw InvalidKeyException("unknown label: $label")
+ val cipher = Cipher.getInstance(CIPHER_SPEC)
+ cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(CIPHER_TAG_LEN, iv))
+
+ return cipher
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt
new file mode 100644
index 0000000000..3b95d4bc32
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt
@@ -0,0 +1,17 @@
+/* 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.lib.dataprotect
+
+import java.security.GeneralSecurityException
+
+/**
+ * Exception type thrown by {@link Keystore} when an error is encountered that
+ * is not otherwise covered by an existing sub-class to `GeneralSecurityException`.
+ *
+ */
+class KeystoreException(
+ message: String? = null,
+ cause: Throwable? = null,
+) : GeneralSecurityException(message, cause)
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt
new file mode 100644
index 0000000000..28ae337df9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt
@@ -0,0 +1,231 @@
+/* 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.lib.dataprotect
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.Build.VERSION_CODES.M
+import android.util.Base64
+import mozilla.components.support.base.log.logger.Logger
+import java.nio.charset.StandardCharsets
+import java.security.GeneralSecurityException
+
+private interface KeyValuePreferences {
+ /**
+ * Retrieves all key/value pairs present in the store.
+ *
+ * @return A [Map] containing all key/value pairs present in the store.
+ */
+ fun all(): Map<String, String>
+
+ /**
+ * Retrieves a stored [key]. See [putString] for storing a [key].
+ *
+ * @param key A key name.
+ * @return An optional [String] if [key] is present in the store.
+ */
+ fun getString(key: String): String?
+
+ /**
+ * Stores [value] under [key]. Retrieve it using [getString].
+ *
+ * @param key A key name.
+ * @param value A value for [key].
+ */
+ fun putString(key: String, value: String)
+
+ /**
+ * Removes key/value pair from storage for the provided [key].
+ */
+ fun remove(key: String)
+
+ /**
+ * Clears all key/value pairs from the storage.
+ */
+ fun clear()
+}
+
+/**
+ * A wrapper around [SharedPreferences] which encrypts contents on supported API versions (23+).
+ * Otherwise, this simply delegates to [SharedPreferences].
+ *
+ * In rare circumstances (such as APK signing key rotation) a master key which protects this storage may be lost,
+ * in which case previously stored values will be lost as well. Applications are encouraged to instrument such events.
+ *
+ * @param context A [Context], used for accessing [SharedPreferences].
+ * @param name A name for this storage, used for isolating different instances of [SecureAbove22Preferences].
+ * @param forceInsecure A flag indicating whether to force plaintext storage. If set to `true`,
+ * [InsecurePreferencesImpl21] will be used as a storage layer, otherwise a storage implementation
+ * will be decided based on Android API version, with a preference given to secure storage
+ */
+class SecureAbove22Preferences(context: Context, name: String, forceInsecure: Boolean = false) :
+ KeyValuePreferences {
+ private val impl = if (Build.VERSION.SDK_INT >= M && !forceInsecure) {
+ SecurePreferencesImpl23(context, name)
+ } else {
+ InsecurePreferencesImpl21(context, name)
+ }
+
+ override fun all(): Map<String, String> = impl.all()
+
+ override fun getString(key: String) = impl.getString(key)
+
+ override fun putString(key: String, value: String) = impl.putString(key, value)
+
+ override fun remove(key: String) = impl.remove(key)
+
+ override fun clear() = impl.clear()
+}
+
+/**
+ * A simple [KeyValuePreferences] implementation which entirely delegates to [SharedPreferences] and doesn't perform any
+ * encryption/decryption.
+ */
+@SuppressWarnings("TooGenericExceptionCaught")
+private class InsecurePreferencesImpl21(
+ context: Context,
+ name: String,
+ migrateFromSecureStorage: Boolean = true,
+) : KeyValuePreferences {
+ companion object {
+ private const val SUFFIX = "_kp_pre_m"
+ }
+
+ internal val logger = Logger("mozac/InsecurePreferencesImpl21")
+
+ private val prefs = context.getSharedPreferences("$name$SUFFIX", MODE_PRIVATE)
+
+ init {
+ // Check if we have any encrypted values stored on disk.
+ if (migrateFromSecureStorage && Build.VERSION.SDK_INT >= M && prefs.all.isEmpty()) {
+ val secureStorage = SecurePreferencesImpl23(context, name, false)
+ // Copy over any old values.
+ try {
+ secureStorage.all().forEach {
+ putString(it.key, it.value)
+ }
+ } catch (e: Exception) {
+ // Certain devices crash on various Keystore exceptions. While trying to migrate
+ // to use the plaintext storage we don't want to crash if we can't access secure
+ // storage, and just catch the errors.
+ logger.error("Migrating from secure storage failed", e)
+ }
+ // Erase old storage.
+ secureStorage.clear()
+ }
+ }
+
+ override fun all(): Map<String, String> {
+ return prefs.all.mapNotNull {
+ if (it.value is String) {
+ it.key to it.value as String
+ } else {
+ null
+ }
+ }.toMap()
+ }
+
+ override fun getString(key: String) = prefs.getString(key, null)
+
+ override fun putString(key: String, value: String) = prefs.edit().putString(key, value).apply()
+
+ override fun remove(key: String) = prefs.edit().remove(key).apply()
+
+ override fun clear() = prefs.edit().clear().apply()
+}
+
+/**
+ * A [KeyValuePreferences] which is backed by [SharedPreferences] and performs encryption/decryption of values.
+ */
+@TargetApi(M)
+private class SecurePreferencesImpl23(
+ context: Context,
+ name: String,
+ migrateFromPlaintextStorage: Boolean = true,
+) : KeyValuePreferences {
+ companion object {
+ private const val SUFFIX = "_kp_post_m"
+ private const val BASE_64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING
+ }
+
+ private val logger = Logger("SecurePreferencesImpl23")
+ private val prefs = context.getSharedPreferences("$name$SUFFIX", MODE_PRIVATE)
+ private val keystore by lazy { Keystore(context.packageName) }
+
+ init {
+ if (migrateFromPlaintextStorage && prefs.all.isEmpty()) {
+ // Check if we have any plaintext values stored on disk. That indicates that we've hit
+ // an API upgrade situation. We just went from pre-M to post-M. Since we already have
+ // the plaintext keys, we can transparently migrate them to use the encrypted storage layer.
+ val insecureStorage = InsecurePreferencesImpl21(context, name, false)
+ // Copy over any old values.
+ insecureStorage.all().forEach {
+ putString(it.key, it.value)
+ }
+ // Erase old storage.
+ insecureStorage.clear()
+ }
+ }
+
+ override fun all(): Map<String, String> {
+ return prefs.all.keys.mapNotNull { key ->
+ getString(key)?.let { value ->
+ key to value
+ }
+ }.toMap()
+ }
+
+ override fun getString(key: String): String? {
+ // The fact that we're possibly generating a managed key here implies that this key could be lost after being
+ // for some reason. One possible reason for a key to be lost is rotating signing keys for the APK.
+ // Applications are encouraged to instrument such events.
+ generateManagedKeyIfNecessary()
+
+ if (!prefs.contains(key)) {
+ return null
+ }
+
+ val value = prefs.getString(key, "")
+ val encrypted = Base64.decode(value, BASE_64_FLAGS)
+
+ return try {
+ String(keystore.decryptBytes(encrypted), StandardCharsets.UTF_8)
+ } catch (error: IllegalArgumentException) {
+ logger.error("IllegalArgumentException exception: ", error)
+ null
+ } catch (error: GeneralSecurityException) {
+ logger.error("Decrypt exception: ", error)
+ null
+ }
+ }
+
+ override fun putString(key: String, value: String) {
+ generateManagedKeyIfNecessary()
+ val editor = prefs.edit()
+
+ val encrypted = keystore.encryptBytes(value.toByteArray(StandardCharsets.UTF_8))
+ val data = Base64.encodeToString(encrypted, BASE_64_FLAGS)
+
+ editor.putString(key, data).apply()
+ }
+
+ override fun remove(key: String) = prefs.edit().remove(key).apply()
+
+ override fun clear() = prefs.edit().clear().apply()
+
+ /**
+ * Generates a "managed key" - a key used to encrypt data stored by this class. This key is "managed" by [Keystore],
+ * which stores it in system's secure storage layer exposed via [AndroidKeyStore].
+ */
+ private fun generateManagedKeyIfNecessary() {
+ // Do we need to check this on every access, or just during instantiation? Is the overhead here worth it?
+ if (!keystore.available()) {
+ keystore.generateKey()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt
new file mode 100644
index 0000000000..8ca545eb3b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt
@@ -0,0 +1,151 @@
+/* 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.lib.dataprotect
+
+import android.content.Context
+import android.content.SharedPreferences
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+import java.lang.Exception
+
+/**
+ * This class exists so that we can measure how reliable our usage of AndroidKeyStore is.
+ *
+ * All of the actions here are executed against SecureAbove22Preferences, which encrypts/decrypts prefs
+ * using a key managed by AndroidKeyStore.
+ * If device is running on API<23, encryption/decryption won't be used.
+ *
+ * Experiment actions are:
+ * - on every invocation, read a persisted value and verify it's correct; if it's missing write it.
+ * - if an error is encountered (e.g. corrupt/missing value), experiment state is reset and the
+ * experiment starts from scratch.
+ *
+ * For each step (get, write, reset), a Fact is emitted describing what happened (success, type of failure).
+ * A special "experiment" Fact will be emitted in case of an unexpected failure.
+ *
+ * Consumers of this experiment are expected to inspect emitted Facts (e.g. record them into telemetry).
+ */
+class SecurePrefsReliabilityExperiment(private val context: Context) {
+ companion object {
+ const val PREFS_NAME = "KsReliabilityExp"
+ const val PREF_DID_STORE_VALUE = "valueStored"
+ const val SECURE_PREFS_NAME = "KsReliabilityExpSecure"
+ const val PREF_KEY = "expKey"
+ const val PREF_VALUE = "some long, mildly interesting string we'd like to store"
+
+ object Actions {
+ const val EXPERIMENT = "experiment"
+ const val GET = "get"
+ const val WRITE = "write"
+ const val RESET = "reset"
+ }
+
+ @Suppress("MagicNumber")
+ enum class Values(val v: Int) {
+ SUCCESS_MISSING(1),
+ SUCCESS_PRESENT(2),
+ FAIL(3),
+ LOST(4),
+ CORRUPTED(5),
+ PRESENT_UNEXPECTED(6),
+ SUCCESS_WRITE(7),
+ SUCCESS_RESET(8),
+ }
+ }
+
+ private val securePrefs by lazy { SecureAbove22Preferences(context, SECURE_PREFS_NAME) }
+
+ private fun prefs(): SharedPreferences {
+ return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ }
+
+ /**
+ * Runs an experiment. This will emit one or more [Fact]s describing results.
+ */
+ @Suppress("TooGenericExceptionCaught", "ComplexMethod")
+ operator fun invoke() {
+ try {
+ val storedVal = try {
+ securePrefs.getString(PREF_KEY)
+ } catch (e: Exception) {
+ emitFact(Actions.GET, Values.FAIL, mapOf("javaClass" to e.nameForTelemetry()))
+
+ // should this return? or proceed to the write part..?
+ return
+ }
+
+ val valueAlreadyPersisted = prefs().getBoolean(PREF_DID_STORE_VALUE, false)
+
+ val getResult = when {
+ // we didn't store the value yet, and didn't get anything back either
+ (!valueAlreadyPersisted && storedVal == null) -> {
+ Values.SUCCESS_MISSING
+ }
+ // we got back the value we stored
+ (valueAlreadyPersisted && storedVal == PREF_VALUE) -> {
+ Values.SUCCESS_PRESENT
+ }
+ // value was lost
+ (valueAlreadyPersisted && storedVal == null) -> {
+ Values.LOST
+ }
+ // we got some value back, but not what we stored
+ (valueAlreadyPersisted && storedVal != PREF_VALUE) -> {
+ Values.CORRUPTED
+ }
+ // we didn't store the value yet, but got something back either way
+ else -> {
+ Values.PRESENT_UNEXPECTED
+ }
+ }
+
+ emitFact(Actions.GET, getResult)
+
+ when (getResult) {
+ // perform a write of the missing value
+ Values.SUCCESS_MISSING -> {
+ try {
+ securePrefs.putString(PREF_KEY, PREF_VALUE)
+ emitFact(Actions.WRITE, Values.SUCCESS_WRITE)
+ } catch (e: Exception) {
+ emitFact(Actions.WRITE, Values.FAIL, mapOf("javaClass" to e.nameForTelemetry()))
+ }
+ prefs().edit().putBoolean(PREF_DID_STORE_VALUE, true).apply()
+ }
+ // reset our experiment in case of detected failures. this lets us measure the failure rate
+ Values.LOST, Values.CORRUPTED, Values.PRESENT_UNEXPECTED -> {
+ securePrefs.clear()
+ prefs().edit().clear().apply()
+ emitFact(Actions.RESET, Values.SUCCESS_RESET)
+ }
+ else -> {
+ // no-op
+ }
+ }
+ } catch (e: Exception) {
+ emitFact(Actions.EXPERIMENT, Values.FAIL, mapOf("javaClass" to e.nameForTelemetry()))
+ }
+ }
+}
+
+private fun emitFact(
+ item: String,
+ value: SecurePrefsReliabilityExperiment.Companion.Values,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.LIB_DATAPROTECT,
+ Action.IMPLEMENTATION_DETAIL,
+ item,
+ "${value.v}",
+ metadata,
+ ).collect()
+}
+
+private fun Exception.nameForTelemetry(): String {
+ return this.javaClass.canonicalName ?: "anonymous"
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt
new file mode 100644
index 0000000000..ce11b43ad3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt
@@ -0,0 +1,137 @@
+/* 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.lib.dataprotect
+
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import java.nio.charset.StandardCharsets
+import java.security.GeneralSecurityException
+import java.security.Key
+import java.security.KeyStore
+import java.security.SecureRandom
+import java.security.Security
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+
+private val DEFAULTPASS = "testit!".toCharArray()
+
+/* mock keystore wrapper to deal with intricacies of how Java/Anroid key management work */
+internal class MockStoreWrapper : KeyStoreWrapper() {
+ override fun loadKeyStore(): KeyStore {
+ val ks = KeyStore.getInstance("JCEKS")
+ ks.load(null)
+ return ks
+ }
+
+ override fun getKeyFor(label: String): Key? =
+ getKeyStore().getKey(label, DEFAULTPASS)
+ override fun makeKeyFor(label: String): SecretKey {
+ val gen = KeyGenerator.getInstance("AES")
+ gen.init(256)
+ val key = gen.generateKey()
+ getKeyStore().setKeyEntry(label, key, DEFAULTPASS, null)
+
+ return key
+ }
+}
+
+class KeystoreTest {
+
+ private var wrapper = MockStoreWrapper()
+ private var rng = SecureRandom()
+
+ @Before
+ fun setUp() {
+ Security.setProperty("crypto.policy", "unlimited")
+ }
+
+ @Test
+ fun workingWithLabel() {
+ val keystore = Keystore("test-labels", true, wrapper)
+
+ Assert.assertFalse(keystore.available())
+ keystore.generateKey()
+ Assert.assertTrue(keystore.available())
+ keystore.deleteKey()
+ Assert.assertFalse(keystore.available())
+ }
+
+ @Test
+ fun createEncryptCipher() {
+ val keystore = Keystore("test-encrypt-ciphers", true, wrapper)
+
+ Assert.assertFalse(keystore.available())
+ var caught = false
+ var cipher: Cipher? = null
+ try {
+ cipher = keystore.createEncryptCipher()
+ } catch (ex: GeneralSecurityException) {
+ caught = true
+ } finally {
+ Assert.assertTrue("unexpected success", caught)
+ Assert.assertNull(cipher)
+ }
+
+ keystore.generateKey()
+ Assert.assertTrue(keystore.available())
+ cipher = keystore.createEncryptCipher()
+ Assert.assertEquals(CIPHER_SPEC, cipher.algorithm)
+ Assert.assertNotNull(cipher.iv)
+ }
+
+ @Test
+ fun createDecryptCipher() {
+ val keystore = Keystore("test-decrypt-ciphers", true, wrapper)
+ val iv = ByteArray(12)
+ rng.nextBytes(iv)
+
+ Assert.assertFalse(keystore.available())
+ var caught = false
+ var cipher: Cipher? = null
+ try {
+ cipher = keystore.createDecryptCipher(iv)
+ } catch (ex: GeneralSecurityException) {
+ caught = true
+ } finally {
+ Assert.assertTrue("unexpected success", caught)
+ Assert.assertNull(cipher)
+ }
+
+ keystore.generateKey()
+ Assert.assertTrue(keystore.available())
+ cipher = keystore.createDecryptCipher(iv)
+ Assert.assertEquals(CIPHER_SPEC, cipher.algorithm)
+ Assert.assertArrayEquals(iv, cipher.iv)
+ }
+
+ @Test
+ fun testAutoInit() {
+ val keystore = Keystore("test-auto-init", false, wrapper)
+
+ Assert.assertTrue(keystore.available())
+ Assert.assertFalse(keystore.generateKey())
+
+ var cipher: Cipher?
+ cipher = keystore.createEncryptCipher()
+ Assert.assertNotNull(cipher)
+ cipher = keystore.createDecryptCipher(ByteArray(12))
+ Assert.assertNotNull(cipher)
+ }
+
+ @Ignore("https://github.com/mozilla-mobile/android-components/issues/4956")
+ @Test
+ fun cryptoRoundTrip() {
+ val keystore = Keystore("test-roundtrip", wrapper = wrapper)
+
+ var input = "classic plaintext 'hello, world'".toByteArray(StandardCharsets.UTF_8)
+ var encrypted = keystore.encryptBytes(input)
+ Assert.assertNotNull(encrypted)
+ var output = keystore.decryptBytes(encrypted)
+ Assert.assertArrayEquals(input, output)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt
new file mode 100644
index 0000000000..2a8a87308d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt
@@ -0,0 +1,190 @@
+/* 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.lib.dataprotect
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import java.security.Security
+
+@RunWith(AndroidJUnit4::class)
+class SecureAbove22PreferencesTest {
+ @Config(sdk = [21])
+ @Test
+ fun `CRUD tests API level 21 unencrypted`() {
+ val storage = SecureAbove22Preferences(testContext, "hello")
+ val storage2 = SecureAbove22Preferences(testContext, "world")
+
+ // no keys
+ assertNull(storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // single key
+ storage.putString("hello", "world")
+ assertEquals("world", storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // single key, updated
+ storage.putString("hello", "you")
+ assertEquals("you", storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // multiple keys
+ storage.putString("test", "string")
+ assertEquals("string", storage.getString("test"))
+ assertEquals("you", storage.getString("hello"))
+ val all = storage.all()
+ assertEquals(2, all.size)
+ assertEquals("string", all["test"])
+ assertEquals("you", all["hello"])
+ assertTrue(storage2.all().isEmpty())
+
+ // clearing one storage doesn't affect another with a different name
+ storage2.putString("another", "test")
+ assertEquals(1, storage2.all().size)
+ storage2.clear()
+ assertEquals(2, storage.all().size)
+
+ // key removal
+ storage.remove("hello")
+ assertNull(storage.getString("hello"))
+ storage.remove("test")
+ assertNull(storage.getString("test"))
+ assertTrue(storage2.all().isEmpty())
+
+ // clearing
+ storage.putString("one", "two")
+ assertEquals("two", storage.getString("one"))
+ storage.putString("three", "four")
+ assertEquals("four", storage.getString("three"))
+ storage.putString("five", "six")
+ assertEquals("six", storage.getString("five"))
+ assertTrue(storage2.all().isEmpty())
+
+ storage.clear()
+ assertNull(storage.getString("one"))
+ assertNull(storage.getString("three"))
+ assertNull(storage.getString("five"))
+ assertTrue(storage.all().isEmpty())
+ assertTrue(storage2.all().isEmpty())
+ }
+
+ @Config(sdk = [22])
+ @Test
+ fun `CRUD tests API level 22 unencrypted`() {
+ val storage = SecureAbove22Preferences(testContext, "hello")
+ val storage2 = SecureAbove22Preferences(testContext, "world")
+
+ // no keys
+ assertNull(storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // single key
+ storage.putString("hello", "world")
+ assertEquals("world", storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // single key, updated
+ storage.putString("hello", "you")
+ assertEquals("you", storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // multiple keys
+ storage.putString("test", "string")
+ assertEquals("string", storage.getString("test"))
+ assertEquals("you", storage.getString("hello"))
+ val all = storage.all()
+ assertEquals(2, all.size)
+ assertEquals("string", all["test"])
+ assertEquals("you", all["hello"])
+ assertTrue(storage2.all().isEmpty())
+
+ // clearing one storage doesn't affect another with a different name
+ storage2.putString("another", "test")
+ assertEquals(1, storage2.all().size)
+ storage2.clear()
+ assertEquals(2, storage.all().size)
+
+ // key removal
+ storage.remove("hello")
+ assertNull(storage.getString("hello"))
+ storage.remove("test")
+ assertNull(storage.getString("test"))
+ assertTrue(storage2.all().isEmpty())
+
+ // clearing
+ storage.putString("one", "two")
+ assertEquals("two", storage.getString("one"))
+ storage.putString("three", "four")
+ assertEquals("four", storage.getString("three"))
+ storage.putString("five", "six")
+ assertEquals("six", storage.getString("five"))
+ assertTrue(storage2.all().isEmpty())
+
+ storage.clear()
+ assertNull(storage.getString("one"))
+ assertNull(storage.getString("three"))
+ assertNull(storage.getString("five"))
+ assertTrue(storage.all().isEmpty())
+ assertTrue(storage2.all().isEmpty())
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `storage instances of the same name are interchangeable`() {
+ val storage = SecureAbove22Preferences(testContext, "hello")
+ val storage2 = SecureAbove22Preferences(testContext, "hello")
+
+ storage.putString("key1", "value1")
+ assertEquals("value1", storage2.getString("key1"))
+
+ storage2.putString("something", "other")
+ assertEquals("other", storage.getString("something"))
+
+ assertEquals(storage.all().size, storage2.all().size)
+ assertEquals(storage.all(), storage2.all())
+
+ storage.clear()
+ assertTrue(storage2.all().isEmpty())
+ }
+
+ @Ignore("https://github.com/mozilla-mobile/android-components/issues/4956")
+ @Config(sdk = [23])
+ @Test
+ fun `CRUD tests API level 23+ encrypted`() {
+ // TODO find out what this is; lockwise tests set it.
+ Security.setProperty("crypto.policy", "unlimited")
+
+ val storage = SecureAbove22Preferences(testContext, "test")
+
+ // no keys
+ assertNull(storage.getString("hello"))
+
+ // single key
+ storage.putString("hello", "world")
+ assertEquals("world", storage.getString("hello"))
+
+ // single key, updated
+ storage.putString("hello", "you")
+ assertEquals("you", storage.getString("hello"))
+
+ // multiple keys
+ storage.putString("test", "string")
+ assertEquals("string", storage.getString("test"))
+ assertEquals("you", storage.getString("hello"))
+
+ // key removal
+ storage.remove("hello")
+ assertNull(storage.getString("hello"))
+ storage.remove("test")
+ assertNull(storage.getString("test"))
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt
new file mode 100644
index 0000000000..3cd0497dd3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt
@@ -0,0 +1,215 @@
+/* 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.lib.dataprotect
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment.Companion.Actions
+import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment.Companion.Values
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class SecurePrefsReliabilityExperimentTest {
+ @Config(sdk = [21])
+ @Test
+ fun `working first run and rerurns emit correct facts`() {
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_PRESENT,
+ )
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_PRESENT,
+ )
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `corrupt value returned`() {
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ // Now, let's corrupt the value manually
+ val securePrefs = SecureAbove22Preferences(
+ testContext,
+ SecurePrefsReliabilityExperiment.SECURE_PREFS_NAME,
+ )
+ securePrefs.putString(SecurePrefsReliabilityExperiment.PREF_KEY, "wrong test string")
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.CORRUPTED,
+ Actions.RESET to Values.SUCCESS_RESET,
+ )
+
+ // ... and we should be reset now:
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `lost value`() {
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ // Now, let's corrupt the store manually
+ val securePrefs = SecureAbove22Preferences(
+ testContext,
+ SecurePrefsReliabilityExperiment.SECURE_PREFS_NAME,
+ )
+ securePrefs.clear()
+
+ // loss is detected:
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.LOST,
+ Actions.RESET to Values.SUCCESS_RESET,
+ )
+
+ // we should be reset now:
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_PRESENT,
+ )
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `value present unexpectedly`() {
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ // First, let's add the correct value manually:
+ val securePrefs = SecureAbove22Preferences(
+ testContext,
+ SecurePrefsReliabilityExperiment.SECURE_PREFS_NAME,
+ )
+ securePrefs.putString(SecurePrefsReliabilityExperiment.PREF_KEY, SecurePrefsReliabilityExperiment.PREF_VALUE)
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.PRESENT_UNEXPECTED,
+ Actions.RESET to Values.SUCCESS_RESET,
+ )
+
+ // Let's try an incorrect value, as well:
+ securePrefs.putString(SecurePrefsReliabilityExperiment.PREF_KEY, "bad string")
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.PRESENT_UNEXPECTED,
+ Actions.RESET to Values.SUCCESS_RESET,
+ )
+
+ // subsequently, it's all good:
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_PRESENT,
+ )
+ }
+
+ @Test
+ fun `initialization failure`() {
+ // AndroidKeyStore isn't available in the test environment.
+ // This test runs against our target sdk version, so the experiment code will attempt to init
+ // the AndroidKeyStore, that won't be available.
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ SecurePrefsReliabilityExperiment(testContext)()
+
+ val captor = argumentCaptor<Fact>()
+ Mockito.verify(processor).process(captor.capture())
+
+ assertEquals(1, captor.allValues.size)
+ assertExperimentFact(
+ captor.allValues[0],
+ Actions.GET,
+ Values.FAIL,
+ mapOf("javaClass" to "java.security.KeyStoreException"),
+ )
+ }
+
+ private fun triggerAndAssertFacts(processor: FactProcessor, vararg factPairs: Pair<String, Values>) {
+ with(argumentCaptor<Fact>()) {
+ SecurePrefsReliabilityExperiment(testContext)()
+ Mockito.verify(processor, times(factPairs.size)).process(this.capture())
+ assertEquals(factPairs.size, this.allValues.size)
+ factPairs.forEachIndexed { index, pair ->
+ assertExperimentFact(this.allValues[index], pair.first, pair.second)
+ }
+ }
+ reset(processor)
+ }
+
+ private fun assertExperimentFact(
+ fact: Fact,
+ item: String,
+ value: Values,
+ metadata: Map<String, Any>? = null,
+ ) {
+ assertEquals(Component.LIB_DATAPROTECT, fact.component)
+ assertEquals(Action.IMPLEMENTATION_DETAIL, fact.action)
+ assertEquals(item, fact.item)
+ assertEquals("${value.v}", fact.value)
+ assertEquals(metadata, fact.metadata)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md b/mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md
new file mode 100644
index 0000000000..21ed90d434
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md
@@ -0,0 +1,25 @@
+# [Android Components](../../../README.md) > Libraries > Fetch-HttpURLConnection
+
+A [concept-fetch](../../concept/fetch/README.md) implementation using [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html).
+
+This implementation of `concept-fetch` uses [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html) from the standard library of the Android System. Therefore this component has no third-party dependencies and is smaller than other implementations. It's intended use is for apps that have strict APK size constraints.
+
+## Usage
+
+### 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:lib-fetch-httpurlconnection:{latest-version}"
+```
+
+### Performing requests
+
+See the [concept-fetch documentation](../../concept/fetch/README.md) for generic examples of using the API of components implementing `concept-fetch`.
+
+## 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/lib/fetch-httpurlconnection/build.gradle b/mobile/android/android-components/components/lib/fetch-httpurlconnection/build.gradle
new file mode 100644
index 0000000000..7065a851cb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/build.gradle
@@ -0,0 +1,40 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.lib.fetch.httpurlconnection'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation project(':concept-fetch')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockito
+
+ testImplementation project(':tooling-fetch-tests')
+}
+
+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/lib/fetch-httpurlconnection/proguard-rules.pro b/mobile/android/android-components/components/lib/fetch-httpurlconnection/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/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/lib/fetch-httpurlconnection/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/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/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt
new file mode 100644
index 0000000000..7ded64ea15
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt
@@ -0,0 +1,195 @@
+/* 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.lib.fetch.httpurlconnection
+
+import mozilla.components.concept.fetch.BuildConfig
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isDataUri
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient.Companion.getOrCreateCookieManager
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.io.InputStream
+import java.net.CookieHandler
+import java.net.CookieManager
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.zip.GZIPInputStream
+
+/**
+ * [HttpURLConnection] implementation of [Client].
+ */
+class HttpURLConnectionClient : Client() {
+ private val defaultHeaders: Headers = MutableHeaders(
+ "User-Agent" to "MozacFetch/${BuildConfig.LIBRARY_VERSION}",
+ "Accept-Encoding" to "gzip",
+ )
+
+ @Throws(IOException::class)
+ override fun fetch(request: Request): Response {
+ if (request.private) {
+ throw IllegalArgumentException("Client doesn't support private request")
+ }
+ if (request.isDataUri()) {
+ return fetchDataUri(request)
+ }
+
+ val connection = (URL(request.url).openConnection() as HttpURLConnection)
+
+ connection.setupWith(request)
+ connection.addHeadersFrom(request, defaultHeaders)
+ connection.addBodyFrom(request)
+
+ return connection.toResponse()
+ }
+
+ companion object {
+ fun getOrCreateCookieManager(): CookieManager {
+ if (CookieHandler.getDefault() == null) {
+ CookieHandler.setDefault(CookieManager())
+ }
+ return CookieHandler.getDefault() as CookieManager
+ }
+ }
+}
+
+private fun HttpURLConnection.addBodyFrom(request: Request) {
+ if (request.body == null) {
+ return
+ }
+
+ request.body?.let { body ->
+ doOutput = true
+
+ body.useStream { inStream ->
+ outputStream.use { outStream ->
+ inStream
+ .buffered()
+ .copyTo(outStream)
+ outStream.flush()
+ }
+ }
+ }
+}
+
+internal fun HttpURLConnection.setupWith(request: Request) {
+ requestMethod = request.method.name
+ instanceFollowRedirects = request.redirect == Request.Redirect.FOLLOW
+
+ request.connectTimeout?.let { (timeout, unit) ->
+ connectTimeout = unit.toMillis(timeout).toInt()
+ }
+
+ request.readTimeout?.let { (timeout, unit) ->
+ readTimeout = unit.toMillis(timeout).toInt()
+ }
+
+ useCaches = request.useCaches
+
+ // HttpURLConnection can't be configured to omit cookies. As
+ // a workaround, we delete all cookies we have stored for
+ // the request URI.
+ val cookieManager = getOrCreateCookieManager()
+ if (request.cookiePolicy == Request.CookiePolicy.OMIT) {
+ val uri = URL(request.url).toURI()
+ for (cookie in cookieManager.cookieStore.get(uri)) {
+ cookieManager.cookieStore.remove(uri, cookie)
+ }
+ }
+}
+
+private fun HttpURLConnection.addHeadersFrom(request: Request, defaultHeaders: Headers) {
+ defaultHeaders.filter { header ->
+ request.headers?.contains(header.name) != true
+ }.forEach { header ->
+ setRequestProperty(header.name, header.value)
+ }
+
+ request.headers?.forEach { header ->
+ addRequestProperty(header.name, header.value)
+ }
+}
+
+private fun HttpURLConnection.toResponse(): Response {
+ val headers = translateHeaders(this)
+ return Response(
+ url.toString(),
+ responseCode,
+ headers,
+ createBody(this, headers["Content-Type"]),
+ )
+}
+
+private fun translateHeaders(connection: HttpURLConnection): Headers {
+ val headers = MutableHeaders()
+
+ var index = 0
+
+ while (connection.getHeaderField(index) != null) {
+ val name = connection.getHeaderFieldKey(index)
+ if (name == null) {
+ index++
+ continue
+ }
+
+ val value = connection.getHeaderField(index)
+
+ headers.append(name, value)
+
+ index++
+ }
+
+ return headers
+}
+
+private fun createBody(connection: HttpURLConnection, contentType: String?): Response.Body {
+ val gzipped = connection.contentEncoding == "gzip"
+
+ withFileNotFoundExceptionIgnored {
+ return HttpUrlConnectionBody(
+ connection,
+ connection.inputStream,
+ gzipped,
+ contentType,
+ )
+ }
+
+ withFileNotFoundExceptionIgnored {
+ return HttpUrlConnectionBody(
+ connection,
+ connection.errorStream,
+ gzipped,
+ contentType,
+ )
+ }
+
+ return EmptyBody()
+}
+
+private class EmptyBody : Response.Body("".byteInputStream())
+
+private class HttpUrlConnectionBody(
+ private val connection: HttpURLConnection,
+ stream: InputStream,
+ gzipped: Boolean,
+ contentType: String?,
+) : Response.Body(if (gzipped) GZIPInputStream(stream) else stream, contentType) {
+ override fun close() {
+ super.close()
+
+ connection.disconnect()
+ }
+}
+
+private inline fun withFileNotFoundExceptionIgnored(block: () -> Unit) {
+ try {
+ block()
+ } catch (e: FileNotFoundException) {
+ // Ignore
+ }
+}
diff --git a/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.kt b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.kt
new file mode 100644
index 0000000000..77b086c8d3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.kt
@@ -0,0 +1,40 @@
+/* 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.lib.fetch.httpurlconnection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.HttpURLConnection
+import java.net.URL
+
+@RunWith(AndroidJUnit4::class)
+class HttpUrlConnectionFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTestCases() {
+ override fun createNewClient(): Client = HttpURLConnectionClient()
+
+ // Inherits test methods from generic test suite base class
+
+ @Test
+ fun `Client instance`() {
+ // We need at least one test case defined here so that this is recognized as test class.
+ assertTrue(createNewClient() is HttpURLConnectionClient)
+ }
+
+ @Test
+ override fun get200WithCacheControl() {
+ // We can't run the base fetch test case because HttpResponseCache
+ // doesn't work in a unit test. So we test that we set the
+ // flag correctly instead.
+ val connection = (URL("https://mozilla.org").openConnection() as HttpURLConnection)
+ assertTrue(connection.useCaches)
+
+ connection.setupWith((Request("https://mozilla.org", useCaches = false)))
+ assertFalse(connection.useCaches)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/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/lib/fetch-httpurlconnection/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/fetch-okhttp/README.md b/mobile/android/android-components/components/lib/fetch-okhttp/README.md
new file mode 100644
index 0000000000..d1cd0a3d2f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/README.md
@@ -0,0 +1,25 @@
+# [Android Components](../../../README.md) > Libraries > Fetch-OkHttp
+
+A [concept-fetch](../../concept/fetch/README.md) implementation using [OkHttp](https://github.com/square/okhttp).
+
+This implementation of `concept-fetch` uses [OkHttp](https://github.com/square/okhttp) - a third-party library from Square. It is intended for apps that already use OkHttp and want components to use the same client.
+
+## Usage
+
+### 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:lib-fetch-okhttp:{latest-version}"
+```
+
+### Performing requests
+
+See the [concept-fetch documentation](../../concept/fetch/README.md) for generic examples of using the API of components implementing `concept-fetch`.
+
+## 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/lib/fetch-okhttp/build.gradle b/mobile/android/android-components/components/lib/fetch-okhttp/build.gradle
new file mode 100644
index 0000000000..c7f1dd9495
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/build.gradle
@@ -0,0 +1,43 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.lib.fetch.okhttp'
+
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.thirdparty_okhttp
+ implementation ComponentsDependencies.thirdparty_okhttp_urlconnection
+
+ implementation project(':concept-fetch')
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':tooling-fetch-tests')
+}
+
+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/lib/fetch-okhttp/proguard-rules.pro b/mobile/android/android-components/components/lib/fetch-okhttp/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/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/lib/fetch-okhttp/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/fetch-okhttp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/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/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt b/mobile/android/android-components/components/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt
new file mode 100644
index 0000000000..0b885eee44
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt
@@ -0,0 +1,149 @@
+/* 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.lib.fetch.okhttp
+
+import android.content.Context
+import mozilla.components.concept.fetch.BuildConfig
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isDataUri
+import mozilla.components.lib.fetch.okhttp.OkHttpClient.Companion.CACHE_MAX_SIZE
+import mozilla.components.lib.fetch.okhttp.OkHttpClient.Companion.getOrCreateCookieManager
+import okhttp3.Cache
+import okhttp3.CacheControl
+import okhttp3.JavaNetCookieJar
+import okhttp3.OkHttpClient
+import okhttp3.RequestBody.Companion.toRequestBody
+import java.net.CookieHandler
+import java.net.CookieManager
+
+typealias RequestBuilder = okhttp3.Request.Builder
+
+/**
+ * [Client] implementation using OkHttp.
+ */
+class OkHttpClient(
+ private val client: OkHttpClient = OkHttpClient(),
+ private val context: Context? = null,
+) : Client() {
+ private val defaultHeaders: Headers = MutableHeaders(
+ "User-Agent" to "MozacFetch/${BuildConfig.LIBRARY_VERSION}",
+ "Accept-Encoding" to "gzip",
+ )
+
+ override fun fetch(request: Request): Response {
+ if (request.private) {
+ throw IllegalArgumentException("Client doesn't support private request")
+ }
+
+ if (request.isDataUri()) {
+ return fetchDataUri(request)
+ }
+
+ val requestClient = client.rebuildFor(request, context)
+
+ val requestBuilder = createRequestBuilderWithBody(request)
+ requestBuilder.addHeadersFrom(request, defaultHeaders = defaultHeaders)
+
+ if (!request.useCaches) {
+ requestBuilder.cacheControl(CacheControl.FORCE_NETWORK)
+ }
+
+ val actualResponse = requestClient.newCall(
+ requestBuilder.build(),
+ ).execute()
+
+ return actualResponse.toResponse()
+ }
+
+ companion object {
+ internal const val CACHE_MAX_SIZE: Long = 10L * 1024L * 1024L
+
+ fun getOrCreateCookieManager(): CookieManager {
+ if (CookieHandler.getDefault() == null) {
+ CookieHandler.setDefault(CookieManager())
+ }
+ return CookieHandler.getDefault() as CookieManager
+ }
+ }
+}
+
+private fun OkHttpClient.rebuildFor(request: Request, context: Context?): OkHttpClient {
+ @Suppress("ComplexCondition")
+ if (request.connectTimeout != null ||
+ request.readTimeout != null ||
+ request.redirect != Request.Redirect.FOLLOW ||
+ request.cookiePolicy != Request.CookiePolicy.OMIT
+ ) {
+ val clientBuilder = newBuilder()
+
+ request.connectTimeout?.let { (timeout, unit) -> clientBuilder.connectTimeout(timeout, unit) }
+ request.readTimeout?.let { (timeout, unit) -> clientBuilder.readTimeout(timeout, unit) }
+
+ if (request.redirect == Request.Redirect.MANUAL) {
+ clientBuilder.followRedirects(false)
+ }
+
+ if (request.cookiePolicy == Request.CookiePolicy.INCLUDE) {
+ clientBuilder.cookieJar(JavaNetCookieJar(getOrCreateCookieManager()))
+ }
+
+ context?.let {
+ clientBuilder.cache(Cache(context.cacheDir, CACHE_MAX_SIZE))
+ }
+
+ return clientBuilder.build()
+ }
+
+ return this
+}
+
+private fun okhttp3.Response.toResponse(): Response {
+ val body = body
+ val headers = translateHeaders(headers)
+
+ return Response(
+ url = request.url.toString(),
+ headers = headers,
+ status = code,
+ body = if (body != null) Response.Body(body.byteStream(), headers["Content-Type"]) else Response.Body.empty(),
+ )
+}
+
+private fun createRequestBuilderWithBody(request: Request): RequestBuilder {
+ val requestBody = request.body?.useStream { it.readBytes() }?.let {
+ it.toRequestBody(null, 0, it.size)
+ }
+
+ return RequestBuilder()
+ .url(request.url)
+ .method(request.method.name, requestBody)
+}
+
+private fun RequestBuilder.addHeadersFrom(request: Request, defaultHeaders: Headers) {
+ defaultHeaders
+ .filter { header ->
+ request.headers?.contains(header.name) != true
+ }.filter { header ->
+ header.name != "Accept-Encoding" && header.value != "gzip"
+ }.forEach { header ->
+ addHeader(header.name, header.value)
+ }
+
+ request.headers?.forEach { header -> addHeader(header.name, header.value) }
+}
+
+private fun translateHeaders(actualHeaders: okhttp3.Headers): Headers {
+ val headers = MutableHeaders()
+
+ for (i in 0 until actualHeaders.size) {
+ headers.append(actualHeaders.name(i), actualHeaders.value(i))
+ }
+
+ return headers
+}
diff --git a/mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.kt b/mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.kt
new file mode 100644
index 0000000000..50610409c3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.kt
@@ -0,0 +1,27 @@
+/* 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.lib.fetch.okhttp
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.tooling.fetch.tests.FetchTestCases
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class OkHttpFetchTestCases : FetchTestCases() {
+
+ override fun createNewClient(): Client = OkHttpClient(okhttp3.OkHttpClient(), testContext)
+
+ // Inherits test methods from generic test suite base class
+
+ @Test
+ fun `Client instance`() {
+ // We need at least one test case defined here so that this is recognized as test class.
+ assertTrue(createNewClient() is OkHttpClient)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/jexl/README.md b/mobile/android/android-components/components/lib/jexl/README.md
new file mode 100644
index 0000000000..646034363a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/README.md
@@ -0,0 +1,236 @@
+# [Android Components](../../../README.md) > Libraries > JEXL
+
+Javascript Expression Language: Powerful context-based expression parser and evaluator.
+
+This implementation is based on [Mozjexl](https://github.com/mozilla/mozjexl), a fork of Jexl (designed and created at TechnologyAdvice) for use at Mozilla, specifically as a part of SHIELD and Normandy.
+
+Features not supported yet:
+
+* JavaScript object properties (e.g. [String.length](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length))
+* Adding custom operators (binary/unary)
+
+Other implementations:
+
+* [JavaScript](https://github.com/mozilla/mozjexl)
+* [Python](https://github.com/mozilla/pyjexl)
+
+## Usage
+
+### 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:jexl:{latest-version}
+```
+
+### Evaluating expressions
+
+```Kotlin
+val jexl = Jexl()
+
+val result = jexl.evaluate("75 > 42")
+
+// evaluate() returns an object of type JexlValue. Calling toKotlin() converts this
+// into a matching Kotlin type (in this case a Boolean).
+println(result.value) // Prints "true"
+```
+
+Often expressions should return a `Boolean`value. In this case `evaluateBooleanExpression` is a helper that always returns a Kotlin `Boolean` and never throws an exception (Returns false).
+
+```Kotlin
+val jexl = Jexl()
+
+// "result" has type Boolean and value "true"
+val result = jexl.evaluateBooleanExpression("42 + 23 > 50", defaultValue = false)
+```
+
+
+### Unary Operators
+
+| Operation | Symbol |
+|-----------|:------:|
+| Negate | ! |
+
+### Binary Operators
+
+| Operation | Symbol |
+|------------------|:----------------:|
+| Add, Concat | + |
+| Subtract | - |
+| Multiply | * |
+| Divide | / |
+| Divide and floor | // |
+| Modulus | % |
+| Power of | ^ |
+| Logical AND | && |
+| Logical OR | &#124;&#124; |
+
+### Comparison
+
+| Comparison | Symbol |
+|----------------------------|:------:|
+| Equal | == |
+| Not equal | != |
+| Greater than | > |
+| Greater than or equal | >= |
+| Less than | < |
+| Less than or equal | <= |
+| Element in array or string | in |
+
+### Ternary operator
+
+Conditional expressions check to see if the first segment evaluates to a truthy
+value. If so, the consequent segment is evaluated. Otherwise, the alternate
+is. If the consequent section is missing, the test result itself will be used
+instead.
+
+| Expression | Result |
+|-------------------------------------|--------|
+| `"" ? "Full" : "Empty"` | Empty |
+| `"foo" in "foobar" ? "Yes" : "No"` | Yes |
+| `{agent: "Archer"}.agent ?: "Kane"` | Archer |
+
+### Native Types
+
+| Type | Examples |
+|-----------|:------------------------------:|
+| Booleans | `true`, `false` |
+| Strings | "Hello \"user\"", 'Hey there!' |
+| Integers | 6, -7, 5, -3 |
+| Doubles | -7.2, -3.14159 |
+| Objects | {hello: "world!"} |
+| Arrays | ['hello', 'world!'] |
+| Undefined | `undefined` |
+
+The JavaScript implementation of Jexl uses a `Numeric` type. This implementation dynamically casts between `Integer` and `Double` as needed.
+
+### Groups
+
+Parentheses work just how you'd expect them to:
+
+| Expression | Result |
+|---------------------------------------|:-------|
+| `(83 + 1) / 2` | 42 |
+| `1 < 3 && (4 > 2 &#124;&#124; 2 > 4)` | true |
+
+### Identifiers
+
+Access variables in the context object by just typing their name. Objects can
+be traversed with dot notation, or by using brackets to traverse to a dynamic
+property name.
+
+Example context:
+
+```javascript
+{
+ name: {
+ first: "Malory",
+ last: "Archer"
+ },
+ exes: [
+ "Nikolai Jakov",
+ "Len Trexler",
+ "Burt Reynolds"
+ ],
+ lastEx: 2
+}
+```
+
+| Expression | Result |
+|---------------------|---------------|
+| `name.first` | Malory |
+| `name['la' + 'st']` | Archer |
+| `exes[2]` | Burt Reynolds |
+| `exes[lastEx - 1]` | Len Trexler |
+
+### Collections
+
+Collections, or arrays of objects, can be filtered by including a filter
+expression in brackets. Properties of each collection can be referenced by
+prefixing them with a leading dot. The result will be an array of the objects
+for which the filter expression resulted in a truthy value.
+
+Example context:
+
+```javascript
+{
+ employees: [
+ {first: 'Sterling', last: 'Archer', age: 36},
+ {first: 'Malory', last: 'Archer', age: 75},
+ {first: 'Lana', last: 'Kane', age: 33},
+ {first: 'Cyril', last: 'Figgis', age: 45},
+ {first: 'Cheryl', last: 'Tunt', age: 28}
+ ],
+ retireAge: 62
+}
+```
+
+| Expression | Result |
+|-------------------------------------------------|---------------------------------------------------------------------------------------|
+| `employees[.first == 'Sterling']` | [{first: 'Sterling', last: 'Archer', age: 36}] |
+| `employees[.last == 'Tu' + 'nt'].first` | Cheryl |
+| `employees[.age >= 30 && .age < 40]` | [{first: 'Sterling', last: 'Archer', age: 36},{first: 'Lana', last: 'Kane', age: 33}] |
+| `employees[.age >= 30 && .age < 40][.age < 35]` | [{first: 'Lana', last: 'Kane', age: 33}] |
+| `employees[.age >= retireAge].first` | Malory |
+
+### Transforms
+
+The power of Jexl is in transforming data. Transform functions take one or more arguments: The value to be transformed, followed by anything else passed to it in the expression.
+
+```Kotlin
+val jexl = Jexl()
+
+jexl.addTransform("split") { value, arguments ->
+ value.toString().split(arguments.first().toString()).toJexlArray()
+}
+
+jexl.addTransform("lower") { value, _ ->
+ value.toString().toLowerCase().toJexl()
+}
+
+jexl.addTransform("last") { value, _ ->
+ (value as JexlArray).values.last()
+}
+```
+
+| Expression | Result |
+|-------------------------------------------------|-----------------------|
+| `"Pam Poovey"&#124;lower&#124;split(' ')|first` | poovey |
+| `"password==guest"&#124;split('=' + '=')` | ['password', 'guest'] |
+
+### Context
+
+Variable contexts are straightforward Objects that can be accessed
+in the expression.
+
+```Kotlin
+val context = Context(
+ "employees" to JexlArray(
+ JexlObject(
+ "first" to "Sterling".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 36.toJexl()),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 75.toJexl()),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 33.toJexl())
+ )
+)
+```
+
+| Expression | Result |
+|-------------------------------------------------|------------------------------------------------------------------------------|
+| `employees[.age >= 30 && .age < 40]` | [{first=Sterling, last=Archer, age=36}, {first=Malory, last=Archer, age=33}] |
+| `employees[.age >= 30 && .age < 90][.age < 37]` | [{first=Malory, last=Archer, age=33}] |
+
+
+## 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/lib/jexl/build.gradle b/mobile/android/android-components/components/lib/jexl/build.gradle
new file mode 100644
index 0000000000..0c4e21a5af
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/build.gradle
@@ -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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.lib.jexl'
+}
+
+dependencies {
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_mockito
+}
+
+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/lib/jexl/proguard-rules.pro b/mobile/android/android-components/components/lib/jexl/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/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/lib/jexl/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/jexl/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/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/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt
new file mode 100644
index 0000000000..f76544fb23
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt
@@ -0,0 +1,102 @@
+/* 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.lib.jexl
+
+import mozilla.components.lib.jexl.evaluator.Evaluator
+import mozilla.components.lib.jexl.evaluator.EvaluatorException
+import mozilla.components.lib.jexl.evaluator.JexlContext
+import mozilla.components.lib.jexl.evaluator.Transform
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.lexer.Lexer
+import mozilla.components.lib.jexl.lexer.LexerException
+import mozilla.components.lib.jexl.parser.Parser
+import mozilla.components.lib.jexl.parser.ParserException
+import mozilla.components.lib.jexl.value.JexlUndefined
+import mozilla.components.lib.jexl.value.JexlValue
+
+class Jexl(
+ private val grammar: Grammar = Grammar(),
+) {
+ private val lexer: Lexer = Lexer(grammar)
+ private val transforms: MutableMap<String, Transform> = mutableMapOf()
+
+ /**
+ * Adds or replaces a transform function in this Jexl instance.
+ *
+ * @param name The name of the transform function, as it will be used within Jexl expressions.
+ * @param transform The function to be executed when this transform is invoked. It will be
+ * provided with two arguments:
+ * - value: The value to be transformed
+ * - arguments: The list of arguments for this transform.
+ */
+ fun addTransform(name: String, transform: Transform) {
+ transforms[name] = transform
+ }
+
+ /**
+ * Evaluates a Jexl string within an optional context.
+ *
+ * @param expression The Jexl expression to be evaluated.
+ * @param context A mapping of variables to values, which will be made accessible to the Jexl
+ * expression when evaluating it.
+ * @return The result of the evaluation.
+ * @throws JexlException if lexing, parsing or evaluating the expression failed.
+ */
+ @Throws(JexlException::class)
+ @Suppress("ThrowsCount")
+ fun evaluate(expression: String, context: JexlContext = JexlContext()): JexlValue {
+ val parser = Parser(grammar)
+ val evaluator = Evaluator(context, grammar, transforms)
+
+ try {
+ val tokens = lexer.tokenize(expression)
+ val astTree = parser.parse(tokens)
+ ?: return JexlUndefined()
+
+ return evaluator.evaluate(astTree)
+ } catch (e: LexerException) {
+ throw JexlException(e)
+ } catch (e: ParserException) {
+ throw JexlException(e)
+ } catch (e: EvaluatorException) {
+ throw JexlException(e)
+ }
+ }
+
+ /**
+ * Evaluates a Jexl string with an optional context to a Boolean result. Optionally a default
+ * value can be provided that will be returned in the expression does not return a boolean
+ * result.
+ */
+ fun evaluateBooleanExpression(
+ expression: String,
+ context: JexlContext = JexlContext(),
+ defaultValue: Boolean? = null,
+ ): Boolean {
+ val result = try {
+ evaluate(expression, context)
+ } catch (e: EvaluatorException) {
+ throw JexlException(e)
+ }
+
+ return try {
+ result.toBoolean()
+ } catch (e: EvaluatorException) {
+ if (defaultValue != null) {
+ return defaultValue
+ } else {
+ throw JexlException(e)
+ }
+ }
+ }
+}
+
+/**
+ * Generic exception thrown when evaluating an expression failed.
+ */
+class JexlException(
+ cause: Exception? = null,
+ message: String? = null,
+) : Exception(message, cause)
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt
new file mode 100644
index 0000000000..5a7858db1b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt
@@ -0,0 +1,230 @@
+/* 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.lib.jexl.ast
+
+/**
+ * A node of the abstract syntax tree.
+ */
+sealed class AstNode {
+
+ var parent: AstNode? = null
+
+ open fun toString(level: Int, isTopLevel: Boolean = true): String = toString()
+}
+
+internal interface OperatorNode {
+ val operator: String?
+}
+
+internal interface BranchNode {
+ var right: AstNode?
+}
+
+// node types
+
+internal data class Literal(
+ val value: Any?,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("< $value >", "LITERAL", level, isTopLevel)
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class BinaryExpression(
+ override val operator: String?,
+ var left: AstNode?,
+ override var right: AstNode? = null,
+) : AstNode(), OperatorNode, BranchNode {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("[ $operator ]", "BINARY_EXPRESSION", level, isTopLevel) {
+ appendChildNode(left, "left", level + 1)
+ appendChildNode(right, "right", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class UnaryExpression(
+ override val operator: String?,
+ override var right: AstNode? = null,
+) : AstNode(), OperatorNode, BranchNode {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("[ $operator ]", "UNARY_EXPRESSION", level, isTopLevel) {
+ appendChildNode(right, "right", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class Identifier(
+ var value: Any?,
+ var from: AstNode? = null,
+ var relative: Boolean = false,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription(
+ "< $value >",
+ "IDENTIFIER",
+ level,
+ isTopLevel,
+ withinHeader = { append(" [ relative = $relative ]") },
+ ) {
+ appendChildNode(from, "from", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class ObjectLiteral(
+ val properties: Map<String, AstNode>,
+) : AstNode() {
+
+ constructor(vararg properties: Pair<String, AstNode>) : this(properties.toMap())
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("<Object>", "OBJECT_LITERAL", level, isTopLevel) {
+ appendNodeMapValues(this@ObjectLiteral, level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class ConditionalExpression(
+ var test: AstNode?,
+ var consequent: AstNode? = null,
+ var alternate: AstNode? = null,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("< ? >", "CONDITIONAL_EXPRESSION", level, isTopLevel) {
+ appendChildNode(test, "test", level + 1)
+ appendChildNode(consequent, "consequent", level + 1)
+ appendChildNode(alternate, "alternate", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class ArrayLiteral(
+ val values: MutableList<Any?>,
+) : AstNode() {
+
+ constructor(vararg values: Any?) : this(values.toMutableList())
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription(
+ "[Array]",
+ "ARRAY_LITERAL",
+ level,
+ isTopLevel,
+ withinHeader = { append(" [ size = ${values.size} ]") },
+ ) {
+ appendNodeListValues(this@ArrayLiteral, level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class Transformation(
+ var name: String?,
+ val arguments: MutableList<AstNode> = mutableListOf(),
+ var subject: AstNode? = null,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("( $name )", "TRANSFORMATION", level, isTopLevel) {
+ appendChildNode(subject, "subject", level + 1)
+
+ for (argument in arguments) {
+ appendChildNode(argument, "arg", level + 1)
+ }
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class FilterExpression(
+ var expression: AstNode?,
+ var subject: AstNode?,
+ var relative: Boolean,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription(
+ "[ . ]",
+ "FILTER_EXPRESSION",
+ level,
+ isTopLevel,
+ withinHeader = { append(" [ relative = $relative ]") },
+ ) {
+ appendChildNode(expression, "expression", level + 1)
+ appendChildNode(subject, "subject", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+// string representation helpers
+
+private fun buildNodeDescription(
+ value: String,
+ name: String,
+ level: Int,
+ isTopLevel: Boolean = true,
+ withinHeader: StringBuilder.() -> Unit = {},
+ block: StringBuilder.() -> Unit = {},
+) = buildString {
+ if (isTopLevel) {
+ appendLevelPad(level)
+ }
+
+ append(value)
+ append(" ( $name )")
+
+ withinHeader()
+ appendLine()
+
+ block()
+}
+
+private fun StringBuilder.appendNodeMapValues(node: ObjectLiteral, level: Int) {
+ node.properties.forEach { (key, value) ->
+ val objectValue = value.toString(level, isTopLevel = false)
+
+ appendLevelPad(level)
+ append("$key : $objectValue")
+ }
+}
+
+private fun StringBuilder.appendNodeListValues(node: ArrayLiteral, level: Int) {
+ val array = node.values
+
+ array.withIndex().forEach { (i, child) ->
+ appendLevelPad(level)
+
+ val value = if (child is AstNode) {
+ child.toString(level, isTopLevel = false)
+ } else {
+ child.toString()
+ }
+
+ append("$i : $value")
+ }
+}
+
+private fun StringBuilder.appendChildNode(node: AstNode?, name: String, level: Int) {
+ node ?: return
+
+ appendLevelPad(level)
+ append("$name = ")
+ append(node.toString(level, isTopLevel = false))
+}
+
+private fun StringBuilder.appendLevelPad(level: Int) = append("".padStart(level * 2, ' '))
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt
new file mode 100644
index 0000000000..654abf465f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt
@@ -0,0 +1,86 @@
+/* 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.lib.jexl.evaluator
+
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlObject
+import mozilla.components.lib.jexl.value.JexlString
+import mozilla.components.lib.jexl.value.JexlUndefined
+import mozilla.components.lib.jexl.value.JexlValue
+
+typealias Transform = (JexlValue, List<JexlValue>) -> JexlValue
+
+internal class EvaluatorException(message: String) : Exception(message)
+
+/**
+ * The evaluator takes a JEXL abstract syntax tree as generated by the <code>Parser</code> and calculates its value
+ * within a given context.
+ */
+internal class Evaluator(
+ internal val context: JexlContext = JexlContext(),
+ internal val grammar: Grammar = Grammar(),
+ internal val transforms: Map<String, Transform> = emptyMap(),
+ internal val relativeContext: JexlObject = JexlObject(),
+) {
+
+ @Throws(EvaluatorException::class)
+ fun evaluate(node: AstNode): JexlValue =
+ EvaluatorHandlers.evaluateWith(this, node)
+
+ internal fun evaluateArray(nodes: List<AstNode>): List<JexlValue> {
+ return nodes.map { evaluate(it) }
+ }
+
+ internal fun evaluateObject(properties: Map<String, AstNode>): Map<String, JexlValue> {
+ return properties.mapValues { entry ->
+ evaluate(entry.value)
+ }
+ }
+
+ fun filterRelative(subject: JexlValue, expression: AstNode): JexlValue {
+ val filterSubject = subject as? JexlArray ?: JexlArray(
+ subject,
+ )
+
+ val values = filterSubject.value.filter { element ->
+ val subContext = element as? JexlObject ?: JexlObject()
+ val evaluator = Evaluator(context, grammar, transforms, subContext)
+ val value = evaluator.evaluate(expression)
+
+ value.value as Boolean
+ }
+
+ return JexlArray(values)
+ }
+
+ fun filterStatic(subject: JexlValue, expression: AstNode): JexlValue {
+ val result = evaluate(expression)
+
+ return when {
+ result is JexlBoolean -> if (result.value) { subject } else {
+ JexlUndefined()
+ }
+
+ subject is JexlUndefined -> subject
+
+ subject is JexlObject && result is JexlString -> subject.value[result.value]
+ ?: JexlUndefined()
+
+ subject is JexlArray && result is JexlInteger -> subject.value.getOrNull(result.value)
+ ?: JexlUndefined()
+
+ // We just convert a double to int here .. hoping for the best!
+ subject is JexlArray && result is JexlDouble -> subject.value.getOrNull(result.value.toInt())
+ ?: JexlUndefined()
+
+ else -> throw EvaluatorException("Cannot filter $subject by $result")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.kt
new file mode 100644
index 0000000000..a53e2a7aa5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.kt
@@ -0,0 +1,158 @@
+/* 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.lib.jexl.evaluator
+
+import mozilla.components.lib.jexl.JexlException
+import mozilla.components.lib.jexl.ast.ArrayLiteral
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.ast.BinaryExpression
+import mozilla.components.lib.jexl.ast.ConditionalExpression
+import mozilla.components.lib.jexl.ast.FilterExpression
+import mozilla.components.lib.jexl.ast.Identifier
+import mozilla.components.lib.jexl.ast.Literal
+import mozilla.components.lib.jexl.ast.ObjectLiteral
+import mozilla.components.lib.jexl.ast.Transformation
+import mozilla.components.lib.jexl.ast.UnaryExpression
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlObject
+import mozilla.components.lib.jexl.value.JexlString
+import mozilla.components.lib.jexl.value.JexlUndefined
+import mozilla.components.lib.jexl.value.JexlValue
+
+/**
+ * A mapping of AST node type to a function that can evaluate this type of node.
+ *
+ * This mapping could be moved inside [Evaluator].
+ */
+internal object EvaluatorHandlers {
+
+ internal fun evaluateWith(evaluator: Evaluator, node: AstNode): JexlValue = when (node) {
+ is Literal -> evaluateLiteral(node)
+ is BinaryExpression -> evaluateBinaryExpression(evaluator, node)
+ is Identifier -> evaluateIdentifier(evaluator, node)
+ is ObjectLiteral -> evaluateObjectLiteral(evaluator, node)
+ is ArrayLiteral -> evaluateArrayLiteral(evaluator, node)
+ is ConditionalExpression -> evaluateConditionalExpression(evaluator, node)
+ is Transformation -> evaluateTransformation(evaluator, node)
+ is FilterExpression -> evaluateFilterExpression(evaluator, node)
+ is UnaryExpression -> throw JexlException(
+ message = "Unary expression evaluation can't be validated",
+ )
+ }
+
+ private fun evaluateLiteral(node: Literal): JexlValue = when (val value = node.value) {
+ is String -> JexlString(value)
+ is Double -> JexlDouble(value)
+ is Int -> JexlInteger(value)
+ is Boolean -> JexlBoolean(value)
+ else -> throw EvaluatorException("Unknown value type: ${value!!::class}")
+ }
+
+ private fun evaluateBinaryExpression(evaluator: Evaluator, node: BinaryExpression): JexlValue {
+ val left = evaluator.evaluate(node.left!!)
+ val right = evaluator.evaluate(node.right!!)
+ val operator = evaluator.grammar.elements[node.operator!!]
+
+ return operator!!.evaluate?.invoke(left, right)
+ ?: throw EvaluatorException("Can't evaluate _operator: ${node.operator}")
+ }
+
+ private fun evaluateIdentifier(evaluator: Evaluator, node: Identifier): JexlValue =
+ if (node.from != null) {
+ evaluateIdentifierWithScope(evaluator, node)
+ } else {
+ evaluateIdentifierWithoutScope(evaluator, node)
+ }
+
+ private fun evaluateIdentifierWithScope(evaluator: Evaluator, node: Identifier): JexlValue =
+ when (val subContext = evaluator.evaluate(node.from!!)) {
+ is JexlArray -> {
+ val obj = subContext.value[0]
+
+ when (obj) {
+ is JexlUndefined -> obj
+
+ is JexlObject -> obj.value[node.value.toString()]
+ ?: throw EvaluatorException("${node.value} is undefined")
+
+ else -> throw EvaluatorException("$obj is not an object")
+ }
+ }
+
+ is JexlObject -> subContext.value[node.value.toString()]
+ ?: JexlUndefined()
+
+ else -> JexlUndefined()
+ }
+
+ private fun evaluateIdentifierWithoutScope(evaluator: Evaluator, node: Identifier): JexlValue =
+ if (node.relative) {
+ evaluator.relativeContext.value[(node.value.toString())]
+ ?: JexlUndefined()
+ } else {
+ evaluator.context.get(node.value.toString())
+ }
+
+ private fun evaluateObjectLiteral(evaluator: Evaluator, node: ObjectLiteral): JexlValue {
+ val properties = evaluator.evaluateObject(node.properties)
+ return JexlObject(properties)
+ }
+
+ private fun evaluateArrayLiteral(evaluator: Evaluator, node: ArrayLiteral): JexlValue {
+ @Suppress("UNCHECKED_CAST")
+ val values = evaluator.evaluateArray(node.values as List<AstNode>)
+ return JexlArray(values)
+ }
+
+ private fun evaluateConditionalExpression(evaluator: Evaluator, node: ConditionalExpression): JexlValue {
+ val result = evaluator.evaluate(node.test!!)
+
+ return if (result.toBoolean()) {
+ if (node.consequent != null) {
+ evaluator.evaluate(node.consequent!!)
+ } else {
+ result
+ }
+ } else {
+ evaluator.evaluate(node.alternate!!)
+ }
+ }
+
+ private fun evaluateTransformation(evaluator: Evaluator, node: Transformation): JexlValue {
+ val transform = evaluator.transforms[node.name]
+ ?: throw EvaluatorException("Unknown transform ${node.name}")
+
+ if (node.subject == null) {
+ throw EvaluatorException("Missing subject for transform")
+ }
+
+ val subject = evaluator.evaluate(node.subject!!)
+ val arguments = evaluator.evaluateArray(node.arguments)
+
+ return transform.invoke(subject, arguments)
+ }
+
+ private fun evaluateFilterExpression(evaluator: Evaluator, node: FilterExpression): JexlValue {
+ if (node.subject == null) {
+ throw EvaluatorException("Missing subject for filter expression")
+ }
+
+ val subject = evaluator.evaluate(node.subject!!)
+
+ if (node.expression == null) {
+ throw EvaluatorException("Missing expression for filter expression")
+ }
+
+ return if (node.relative) {
+ val result = evaluator.filterRelative(subject, node.expression!!)
+ result
+ } else {
+ evaluator.filterStatic(subject, node.expression!!)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.kt
new file mode 100644
index 0000000000..98397aafe9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.kt
@@ -0,0 +1,51 @@
+/* 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.lib.jexl.evaluator
+
+import mozilla.components.lib.jexl.value.JexlValue
+
+/**
+ * Variables defined in the [JexlContext] are available to expressions.
+ *
+ * Example context:
+ * <code>
+ * val context = JexlContext(
+ * "employees" to JexlArray(
+ * JexlObject(
+ * "first" to "Sterling".toJexl(),
+ * "last" to "Archer".toJexl(),
+ * "age" to 36.toJexl()),
+ * JexlObject(
+ * "first" to "Malory".toJexl(),
+ * "last" to "Archer".toJexl(),
+ * "age" to 75.toJexl()),
+ * JexlObject(
+ * "first" to "Malory".toJexl(),
+ * "last" to "Archer".toJexl(),
+ * "age" to 33.toJexl())
+ * )
+ * )
+ * </code>
+ *
+ * This context can be accessed in an JEXL expression like this:
+ *
+ * <code>
+ * employees[.age >= 30 && .age < 90][.age < 35]
+ * </code>
+ */
+class JexlContext(
+ vararg pairs: Pair<String, JexlValue>,
+) {
+ private val properties: MutableMap<String, JexlValue> = pairs.toMap().toMutableMap()
+
+ fun set(key: String, value: JexlValue) {
+ properties[key] = value
+ }
+
+ fun get(key: String): JexlValue {
+ return properties[key]
+ ?: throw EvaluatorException("$key is undefined")
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt
new file mode 100644
index 0000000000..b2faedff49
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt
@@ -0,0 +1,32 @@
+/* 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.lib.jexl.ext
+
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlString
+
+// Kotlin extensions that make it easier to work with Jexl types and values
+
+inline fun <reified T> List<T>.toJexlArray(): JexlArray {
+ val values = when (T::class) {
+ String::class -> map { JexlString(it as String) }
+ Int::class -> map { JexlInteger(it as Int) }
+ Double::class -> map { JexlDouble(it as Double) }
+ Float::class -> map { JexlDouble((it as Float).toDouble()) }
+ Boolean::class -> map { JexlBoolean(it as Boolean) }
+ else -> throw UnsupportedOperationException("Can't convert type " + T::class + " to Jexl")
+ }
+
+ return JexlArray(values)
+}
+
+fun String.toJexl(): JexlString = JexlString(this)
+fun Int.toJexl(): JexlInteger = JexlInteger(this)
+fun Double.toJexl(): JexlDouble = JexlDouble(this)
+fun Float.toJexl(): JexlDouble = JexlDouble(this.toDouble())
+fun Boolean.toJexl(): JexlBoolean = JexlBoolean(this)
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt
new file mode 100644
index 0000000000..eb61970c96
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt
@@ -0,0 +1,141 @@
+/* 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.lib.jexl.grammar
+
+import mozilla.components.lib.jexl.evaluator.EvaluatorException
+import mozilla.components.lib.jexl.lexer.Token
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlString
+import mozilla.components.lib.jexl.value.JexlValue
+import kotlin.math.floor
+
+/**
+ * Grammar of the JEXL language.
+ *
+ * Note that changes here may require a change in the Lexer or Parser.
+ */
+@Suppress("MagicNumber") // Operator precedence uses numbers and I do not see the need for constants..
+class Grammar {
+ val elements: Map<String, GrammarElement> = mapOf(
+ "." to GrammarElement(Token.Type.DOT),
+ "[" to GrammarElement(Token.Type.OPEN_BRACKET),
+ "]" to GrammarElement(Token.Type.CLOSE_BRACKET),
+ "|" to GrammarElement(Token.Type.PIPE),
+ "{" to GrammarElement(Token.Type.OPEN_CURL),
+ "}" to GrammarElement(Token.Type.CLOSE_CURL),
+ ":" to GrammarElement(Token.Type.COLON),
+ "," to GrammarElement(Token.Type.COMMA),
+ "(" to GrammarElement(Token.Type.OPEN_PAREN),
+ ")" to GrammarElement(Token.Type.CLOSE_PAREN),
+ "?" to GrammarElement(Token.Type.QUESTION),
+ "+" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 30,
+ ) { left, right -> left + right },
+ "-" to GrammarElement(Token.Type.BINARY_OP, 30),
+ "*" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 40,
+ ) { left, right -> left * right },
+ "/" to GrammarElement(Token.Type.BINARY_OP, 40) { left, right ->
+ left / right
+ },
+ "//" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 40,
+ ) { left, right ->
+ when (val result = left.div(right)) {
+ is JexlInteger -> result
+ is JexlDouble -> JexlInteger(
+ floor(
+ result.value,
+ ).toInt(),
+ )
+ else -> throw EvaluatorException("Cannot floor type: " + result::class)
+ }
+ },
+ "%" to GrammarElement(Token.Type.BINARY_OP, 50),
+ "^" to GrammarElement(Token.Type.BINARY_OP, 50),
+ "==" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left == right)
+ },
+ "!=" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left != right)
+ },
+ ">" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left > right)
+ },
+ ">=" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left >= right)
+ },
+ "<" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left < right)
+ },
+ "<=" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left <= right)
+ },
+ "&&" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 10,
+ ) { left, right ->
+ JexlBoolean(left.toBoolean() && right.toBoolean())
+ },
+ "||" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 10,
+ ) { left, right ->
+ JexlBoolean(left.toBoolean() || right.toBoolean())
+ },
+ "in" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ when {
+ left is JexlString -> JexlBoolean(
+ right.toString().contains(left.value),
+ )
+ right is JexlArray -> JexlBoolean(
+ right.value.contains(left),
+ )
+ else -> throw EvaluatorException(
+ "Operator 'in' not applicable to " + left::class + " and " + right::class,
+ )
+ }
+ },
+ "!" to GrammarElement(
+ Token.Type.UNARY_OP,
+ Int.MAX_VALUE,
+ ) { _, right ->
+ JexlBoolean(!right.toBoolean())
+ },
+ )
+}
+
+data class GrammarElement(
+ val type: Token.Type,
+ val precedence: Int = 0,
+ val evaluate: ((JexlValue, JexlValue) -> JexlValue)? = null,
+)
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt
new file mode 100644
index 0000000000..f2386c4255
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt
@@ -0,0 +1,223 @@
+/* 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.lib.jexl.lexer
+
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.grammar.GrammarElement
+
+internal class LexerException(message: String) : Exception(message)
+
+/**
+ * JEXL lexer for the lexical parsing of a JEXL string.
+ *
+ * Its responsibility is to identify the "parts of speech" of a Jexl expression, and tokenize and label each, but
+ * to do only the most minimal syntax checking; the only errors the Lexer should be concerned with are if it's unable
+ * to identify the utility of any of its tokens. Errors stemming from these tokens not being in a sensible
+ * configuration should be left for the Parser to handle.
+ */
+@Suppress("LargeClass")
+internal class Lexer(private val grammar: Grammar) {
+ private val negateAfter = listOf(
+ Token.Type.BINARY_OP,
+ Token.Type.UNARY_OP,
+ Token.Type.OPEN_PAREN,
+ Token.Type.OPEN_BRACKET,
+ Token.Type.QUESTION,
+ Token.Type.COLON,
+ )
+
+ /**
+ * Splits the JEXL expression string into a list of tokens.
+ */
+ @Suppress("ComplexMethod", "LongMethod")
+ @Throws(LexerException::class)
+ fun tokenize(raw: String): List<Token> {
+ val input = LexerInput(raw)
+ val tokens = mutableListOf<Token>()
+
+ var negate = false
+
+ while (!input.end()) {
+ if (negate && !input.character().isDigit() && !input.character().isWhitespace()) {
+ throw LexerException("Negating non digit: " + input.character())
+ }
+
+ when {
+ input.character() == '\'' -> tokens.add(readString(input, input.character()))
+
+ input.character() == '"' -> tokens.add(readString(input, input.character()))
+
+ input.character().isWhitespace() -> consumeWhiteSpaces(input)
+
+ input.peekEquals("true") -> tokens.add(
+ Token(
+ Token.Type.LITERAL,
+ "true",
+ true,
+ ),
+ )
+
+ input.peekEquals("false") -> tokens.add(
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ )
+
+ input.character() == '#' -> discardComment(input)
+
+ input.character() == '-' -> {
+ val token = minusOrNegate(tokens)
+ if (token != null) {
+ tokens.add(token)
+ } else {
+ negate = true
+ }
+ input.proceed()
+ }
+
+ isElement(input, grammar.elements) -> tokens.add(lastFoundElementToken!!)
+
+ input.character().isLetter() || input.character() == '_' || input.character() == '$' ->
+ tokens.add(readIdentifier(input))
+
+ input.character().isDigit() -> {
+ tokens.add(readDigit(input, negate))
+ negate = false
+ }
+
+ else -> throw LexerException("Do not know how to proceed: " + input.character())
+ }
+ }
+
+ return tokens
+ }
+
+ private var lastFoundElementToken: Token? = null
+
+ @Suppress("ReturnCount")
+ private fun isElement(input: LexerInput, elements: Map<String, GrammarElement>): Boolean {
+ val max = elements.keys.map { it.length }.maxOrNull() ?: return false
+
+ for (steps in max downTo 1) {
+ val candidate = input.peekRange(steps)
+ if (elements.containsKey(candidate)) {
+ if (candidate.last().isLetter() && input.peek(steps).isLetter()) {
+ return false
+ }
+
+ val element = elements[candidate]!!
+ lastFoundElementToken = Token(element.type, candidate, candidate)
+ input.proceed(candidate.length)
+
+ return true
+ }
+ }
+
+ return false
+ }
+
+ private fun minusOrNegate(tokens: List<Token>): Token? {
+ if (tokens.isEmpty()) {
+ return null
+ }
+
+ if (tokens.last().type in negateAfter) {
+ return null
+ }
+
+ return Token(Token.Type.BINARY_OP, "-", "-")
+ }
+
+ private fun discardComment(input: LexerInput) {
+ while (!input.end() && input.character() != '\n') {
+ input.proceed()
+ }
+ }
+
+ private fun consumeWhiteSpaces(input: LexerInput) {
+ while (!input.end() && input.character().isWhitespace()) {
+ input.proceed()
+ }
+ }
+
+ private fun readString(input: LexerInput, quote: Char): Token {
+ input.mark()
+ input.proceed()
+
+ while (!input.end()) {
+ // Very simple escaping implementation.
+ if (input.character() == quote && input.previous() != '\\') {
+ break
+ }
+
+ input.proceed()
+ }
+
+ input.proceed()
+
+ val raw = input.emit()
+
+ if (raw.last() != quote) {
+ throw LexerException("String literal not closed")
+ }
+
+ val value = raw.substring(1, raw.length - 1)
+ .replace("\\" + quote, quote.toString())
+ .replace("\\\\", "\\")
+
+ return Token(Token.Type.LITERAL, raw, value)
+ }
+
+ private fun readIdentifier(input: LexerInput): Token {
+ input.mark()
+
+ while (!input.end()) {
+ if (!input.character().isLetterOrDigit() && input.character() != '_' && input.character() != '$') {
+ break
+ }
+
+ input.proceed()
+ }
+
+ val raw = input.emit()
+
+ return Token(Token.Type.IDENTIFIER, raw, raw)
+ }
+
+ @Suppress("ComplexMethod")
+ private fun readDigit(input: LexerInput, negate: Boolean): Token {
+ input.mark()
+
+ var readDot = false
+
+ while (!input.end()) {
+ if (!input.character().isDigit() && input.character() != '.') {
+ break
+ } else if (input.character() == '.' && readDot) {
+ break
+ } else if (input.character() == '.') {
+ readDot = true
+ }
+
+ input.proceed()
+ }
+
+ val raw = if (negate) {
+ "-${input.emit()}"
+ } else {
+ input.emit()
+ }
+
+ val value: Any = if (raw.contains(".")) {
+ raw.toDouble()
+ } else {
+ raw.toInt()
+ }
+
+ return Token(Token.Type.LITERAL, raw, value)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt
new file mode 100644
index 0000000000..f7d946e81a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt
@@ -0,0 +1,85 @@
+/* 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.lib.jexl.lexer
+
+/**
+ * Helper class for reading a string character by character with the ability to "peek" at upcoming characters.
+ */
+internal class LexerInput(private val value: String) {
+ private var position: Int = 0
+ private var mark: Int = 0
+
+ /**
+ * Marks the current position in the input.
+ */
+ fun mark() {
+ mark = position
+ }
+
+ /**
+ * Emits the string from the marked position to the current position.
+ */
+ fun emit(): String {
+ return value.substring(mark, position)
+ }
+
+ /**
+ * Move the current position [steps] steps ahread.
+ */
+ fun proceed(steps: Int = 1) {
+ position += steps
+ }
+
+ /**
+ * Returns true if the string starting as the current position equals [candidate].
+ */
+ fun peekEquals(candidate: String): Boolean {
+ if (position + candidate.length > value.length) {
+ return false
+ }
+
+ for (i in 0 until candidate.length) {
+ if (candidate[i] != value[position + i]) {
+ return false
+ }
+ }
+
+ position += candidate.length
+
+ return true
+ }
+
+ /**
+ * Returns the string from the current position to [steps] ahead without moving the current position.
+ */
+ fun peekRange(steps: Int): String {
+ if (position + steps > value.length) {
+ return ""
+ }
+
+ return value.substring(position, position + steps)
+ }
+
+ /**
+ * Returns the character at the current position
+ */
+ fun character(): Char = value[position]
+
+ /**
+ * Returns true if every character from the input has been read.
+ */
+ fun end() = position == value.length
+
+ /**
+ * Returns the character [steps] steps ahead.
+ */
+ fun peek(steps: Int): Char =
+ if (position + steps == value.length) ' ' else value[position + steps]
+
+ /**
+ * Returns the previous character.
+ */
+ fun previous(): Char = if (position == 0) ' ' else value[position - 1]
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt
new file mode 100644
index 0000000000..2c250a1ef3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt
@@ -0,0 +1,32 @@
+/* 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.lib.jexl.lexer
+
+/**
+ * A token emitted by the [Lexer].
+ */
+data class Token(
+ val type: Type,
+ val raw: String,
+ val value: Any,
+) {
+ enum class Type {
+ LITERAL,
+ IDENTIFIER,
+ DOT,
+ OPEN_BRACKET,
+ CLOSE_BRACKET,
+ PIPE,
+ OPEN_CURL,
+ CLOSE_CURL,
+ COLON,
+ COMMA,
+ OPEN_PAREN,
+ CLOSE_PAREN,
+ QUESTION,
+ BINARY_OP,
+ UNARY_OP,
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt
new file mode 100644
index 0000000000..0dbc47d002
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt
@@ -0,0 +1,252 @@
+/* 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.lib.jexl.parser
+
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.ast.BinaryExpression
+import mozilla.components.lib.jexl.ast.BranchNode
+import mozilla.components.lib.jexl.ast.Identifier
+import mozilla.components.lib.jexl.ast.Literal
+import mozilla.components.lib.jexl.ast.OperatorNode
+import mozilla.components.lib.jexl.ast.UnaryExpression
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.lexer.Token
+
+/**
+ * JEXL parser.
+ *
+ * Takes a list of tokens from the lexer and transforms them into an abstract syntax tree (AST).
+ */
+internal class Parser(
+ internal val grammar: Grammar,
+ private val stopMap: Map<Token.Type, State> = mapOf(),
+) {
+ private var state: State = State.EXPECT_OPERAND
+ internal var tree: AstNode? = null
+ internal var cursor: AstNode? = null
+
+ internal var subParser: Parser? = null
+ private var parentStop: Boolean = false
+ internal var currentObjectKey: String? = null
+
+ internal var nextIdentEncapsulate: Boolean = false
+ internal var nextIdentRelative: Boolean = false
+ internal var relative: Boolean = false
+
+ @Synchronized
+ @Throws(ParserException::class)
+ fun parse(tokens: List<Token>): AstNode? {
+ parseTokens(tokens)
+
+ return complete()
+ }
+
+ private fun complete(): AstNode? {
+ if (cursor != null && !stateMachine[state]!!.completable) {
+ throw ParserException("Unexpected end of expression")
+ }
+
+ if (subParser != null) {
+ endSubExpression()
+ }
+
+ state = State.COMPLETE
+
+ return if (cursor != null) {
+ tree
+ } else {
+ null
+ }
+ }
+
+ private fun parseTokens(tokens: List<Token>) {
+ tokens.forEach { parseToken(it) }
+ }
+
+ @Suppress("ComplexMethod", "LongMethod", "ThrowsCount")
+ private fun parseToken(token: Token): State? {
+ if (state == State.COMPLETE) {
+ throw ParserException("Token after parsing completed")
+ }
+
+ val stateMap = stateMachine[state]
+ ?: throw ParserException("Can't continue from state: $state")
+
+ if (stateMap.subHandler != null) {
+ // Use a sub handler for this state
+ if (subParser == null) {
+ startSubExpression()
+ }
+ val stopState = subParser!!.parseToken(token)
+ if (stopState != null) {
+ endSubExpression()
+ if (parentStop) {
+ return stopState
+ }
+ state = stopState
+ }
+ } else if (stateMap.map.containsKey(token.type)) {
+ val nextState = stateMap.map.getValue(token.type)
+
+ if (nextState.handler != null) {
+ // Use handler for this transition
+ nextState.handler.invoke(this, token)
+ } else {
+ // Use generic handler for this token type (if it exists)
+ val handler = handlers[token.type]
+ handler?.invoke(this, token)
+ }
+
+ nextState.state?.let { state = it }
+ } else if (stopMap.containsKey(token.type)) {
+ return stopMap.getValue(token.type)
+ } else {
+ throw ParserException("Token ${token.raw} (${token.type}) unexpected in state $state")
+ }
+
+ return null
+ }
+
+ internal fun placeAtCursor(node: AstNode) {
+ if (cursor == null) {
+ tree = node
+ } else {
+ cursor?.let { cursor ->
+ if (cursor is BranchNode) {
+ cursor.right = node
+ }
+ }
+ node.parent = cursor
+ }
+
+ cursor = node
+ }
+
+ internal fun placeBeforeCursor(node: AstNode) {
+ cursor = cursor!!.parent
+ placeAtCursor(node)
+ }
+
+ private fun startSubExpression() {
+ var endStates = stateMachine[state]!!.endStates
+ if (endStates.isEmpty()) {
+ parentStop = true
+ endStates = stopMap
+ }
+ this.subParser = Parser(grammar, endStates)
+ }
+
+ private fun endSubExpression() {
+ val stateMap = stateMachine[state]!!
+ val subHandler = stateMap.subHandler!!
+ val subParser = this.subParser!!
+ val node = subParser.complete()
+
+ subHandler.invoke(this, node)
+ this.subParser = null
+ }
+}
+
+class ParserException(message: String) : Exception(message)
+
+internal class StateMap(
+ val map: Map<Token.Type, NextState> = mapOf(),
+ val completable: Boolean = false,
+ val subHandler: ((Parser, AstNode?) -> Unit)? = null,
+ val endStates: Map<Token.Type, State> = mapOf(),
+)
+
+internal enum class State {
+ EXPECT_OPERAND,
+ EXPECT_BIN_OP,
+ IDENTIFIER,
+ SUB_EXPRESSION,
+ EXPECT_OBJECT_KEY,
+ TRAVERSE,
+ ARRAY_VALUE,
+ EXPECT_TRANSFORM,
+ TERNARY_MID,
+ TERNARY_END,
+ COMPLETE,
+ POST_TRANSFORM,
+ EXPECT_KEY_VALUE_SEPARATOR,
+ OBJECT_VALUE,
+ ARGUMENT_VALUE,
+ FILTER,
+ POST_TRANSFORM_ARGUMENTS,
+}
+
+internal class NextState(
+ val state: State? = null,
+ val handler: ((Parser, Token) -> Unit)? = null,
+)
+
+internal val handlers: Map<Token.Type, (Parser, Token) -> Unit> = mapOf(
+ Token.Type.LITERAL to { parser, token ->
+ parser.placeAtCursor(
+ Literal(token.value),
+ )
+ },
+
+ Token.Type.BINARY_OP to { parser, token ->
+ val precedence = parser.grammar.elements[token.value]?.precedence ?: 0
+ var parent = parser.cursor!!.parent
+
+ var operator = (parent as? OperatorNode)?.operator
+
+ while (operator != null &&
+ parser.grammar.elements[operator]!!.precedence > precedence
+ ) {
+ parser.cursor = parent
+ parent = parent?.parent
+ operator = (parent as? OperatorNode)?.operator
+ }
+
+ val node = BinaryExpression(
+ left = parser.cursor,
+ operator = token.value.toString(),
+ )
+
+ parser.cursor!!.parent = node
+ parser.cursor = parent
+ parser.placeAtCursor(node)
+ },
+
+ Token.Type.IDENTIFIER to { parser, token ->
+ val node = Identifier(token.value)
+
+ if (parser.nextIdentEncapsulate) {
+ node.from = parser.cursor
+ parser.placeBeforeCursor(node)
+ parser.nextIdentEncapsulate = false
+ } else {
+ if (parser.nextIdentRelative) {
+ node.relative = true
+ }
+ parser.placeAtCursor(node)
+ }
+ },
+
+ Token.Type.UNARY_OP to { parser, token ->
+ val node = UnaryExpression(
+ operator = token.value.toString(),
+ )
+ parser.placeAtCursor(node)
+ },
+
+ Token.Type.DOT to { parser, _ ->
+ val cursor = parser.cursor
+
+ parser.nextIdentEncapsulate = cursor != null &&
+ (cursor !is BinaryExpression || cursor.right != null) &&
+ cursor !is UnaryExpression
+
+ parser.nextIdentRelative = cursor == null || !parser.nextIdentEncapsulate
+
+ if (parser.nextIdentRelative) {
+ parser.relative = true
+ }
+ },
+)
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt
new file mode 100644
index 0000000000..348fb537ca
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt
@@ -0,0 +1,230 @@
+/* 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.lib.jexl.parser
+
+import mozilla.components.lib.jexl.ast.ArrayLiteral
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.ast.ConditionalExpression
+import mozilla.components.lib.jexl.ast.FilterExpression
+import mozilla.components.lib.jexl.ast.ObjectLiteral
+import mozilla.components.lib.jexl.ast.Transformation
+import mozilla.components.lib.jexl.lexer.Token
+
+internal val stateMachine: Map<State, StateMap> = mapOf(
+ State.EXPECT_OPERAND to StateMap(
+ mapOf(
+ Token.Type.LITERAL to NextState(State.EXPECT_BIN_OP),
+ Token.Type.IDENTIFIER to NextState(
+ State.IDENTIFIER,
+ ),
+ Token.Type.UNARY_OP to NextState(),
+ Token.Type.OPEN_PAREN to NextState(
+ State.SUB_EXPRESSION,
+ ),
+ Token.Type.OPEN_CURL to NextState(
+ State.EXPECT_OBJECT_KEY,
+ ::objectStart,
+ ),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.OPEN_BRACKET to NextState(
+ State.ARRAY_VALUE,
+ ::arrayStart,
+ ),
+ ),
+ ),
+ State.EXPECT_BIN_OP to StateMap(
+ mapOf(
+ Token.Type.BINARY_OP to NextState(
+ State.EXPECT_OPERAND,
+ ),
+ Token.Type.PIPE to NextState(State.EXPECT_TRANSFORM),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.QUESTION to NextState(
+ State.TERNARY_MID,
+ ::ternaryStart,
+ ),
+ ),
+ completable = true,
+ ),
+ State.EXPECT_TRANSFORM to StateMap(
+ mapOf(
+ Token.Type.IDENTIFIER to NextState(
+ State.POST_TRANSFORM,
+ ::transform,
+ ),
+ ),
+ ),
+ State.EXPECT_OBJECT_KEY to StateMap(
+ mapOf(
+ Token.Type.IDENTIFIER to NextState(
+ State.EXPECT_KEY_VALUE_SEPARATOR,
+ ::objectKey,
+ ),
+ Token.Type.CLOSE_CURL to NextState(
+ State.EXPECT_BIN_OP,
+ ),
+ ),
+ ),
+ State.EXPECT_KEY_VALUE_SEPARATOR to StateMap(
+ mapOf(
+ Token.Type.COLON to NextState(State.OBJECT_VALUE),
+ ),
+ ),
+ State.POST_TRANSFORM to StateMap(
+ mapOf(
+ Token.Type.OPEN_PAREN to NextState(
+ State.ARGUMENT_VALUE,
+ ),
+ Token.Type.BINARY_OP to NextState(
+ State.EXPECT_OPERAND,
+ ),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.OPEN_BRACKET to NextState(
+ State.FILTER,
+ ),
+ Token.Type.PIPE to NextState(State.EXPECT_TRANSFORM),
+ ),
+ completable = true,
+ ),
+ State.POST_TRANSFORM_ARGUMENTS to StateMap(
+ mapOf(
+ Token.Type.BINARY_OP to NextState(
+ State.EXPECT_OPERAND,
+ ),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.OPEN_BRACKET to NextState(
+ State.FILTER,
+ ),
+ Token.Type.PIPE to NextState(State.EXPECT_TRANSFORM),
+ ),
+ completable = true,
+ ),
+ State.IDENTIFIER to StateMap(
+ mapOf(
+ Token.Type.BINARY_OP to NextState(
+ State.EXPECT_OPERAND,
+ ),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.OPEN_BRACKET to NextState(
+ State.FILTER,
+ ),
+ Token.Type.PIPE to NextState(State.EXPECT_TRANSFORM),
+ Token.Type.QUESTION to NextState(
+ State.TERNARY_MID,
+ ::ternaryStart,
+ ),
+ ),
+ completable = true,
+ ),
+ State.TRAVERSE to StateMap(
+ mapOf(
+ Token.Type.IDENTIFIER to NextState(
+ State.IDENTIFIER,
+ ),
+ ),
+ ),
+ State.FILTER to StateMap(
+ subHandler = { parser, node ->
+ val expressionNode = FilterExpression(
+ expression = node,
+ relative = parser.subParser!!.relative,
+ subject = parser.cursor,
+ )
+ parser.placeBeforeCursor(expressionNode)
+ },
+ endStates = mapOf(
+ Token.Type.CLOSE_BRACKET to State.IDENTIFIER,
+ ),
+ ),
+ State.SUB_EXPRESSION to StateMap(
+ subHandler = { parser, node ->
+ parser.placeAtCursor(node!!)
+ },
+ endStates = mapOf(
+ Token.Type.CLOSE_PAREN to State.EXPECT_BIN_OP,
+ ),
+ ),
+ State.ARGUMENT_VALUE to StateMap(
+ subHandler = { parser, node ->
+ val cursor = parser.cursor!! as Transformation
+ cursor.arguments.add(node!!)
+ },
+ endStates = mapOf(
+ Token.Type.COMMA to State.ARGUMENT_VALUE,
+ Token.Type.CLOSE_PAREN to State.EXPECT_BIN_OP,
+ ),
+ ),
+ State.OBJECT_VALUE to StateMap(
+ subHandler = { parser, node ->
+ val cursor = parser.cursor as ObjectLiteral
+ val properties = cursor.properties as MutableMap<String, AstNode>
+
+ properties[parser.currentObjectKey!!] = node!!
+ },
+ endStates = mapOf(
+ Token.Type.COMMA to State.EXPECT_OBJECT_KEY,
+ Token.Type.CLOSE_CURL to State.EXPECT_BIN_OP,
+ ),
+ ),
+ State.ARRAY_VALUE to StateMap(
+ subHandler = { parser, node ->
+ if (node != null) {
+ (parser.cursor!! as ArrayLiteral).values.add(node)
+ }
+ },
+ endStates = mapOf(
+ Token.Type.COMMA to State.ARRAY_VALUE,
+ Token.Type.CLOSE_BRACKET to State.EXPECT_BIN_OP,
+ ),
+ ),
+ State.TERNARY_MID to StateMap(
+ subHandler = { parser, node ->
+ val cursor = parser.cursor!! as ConditionalExpression
+ cursor.consequent = node
+ },
+ endStates = mapOf(
+ Token.Type.COLON to State.TERNARY_END,
+ ),
+ ),
+ State.TERNARY_END to StateMap(
+ subHandler = { parser, node ->
+ val cursor = parser.cursor!! as ConditionalExpression
+ cursor.alternate = node
+ },
+ completable = true,
+ ),
+)
+
+private fun objectStart(parser: Parser, @Suppress("UNUSED_PARAMETER") token: Token) {
+ val node = ObjectLiteral(
+ properties = mutableMapOf(),
+ )
+ parser.placeAtCursor(node)
+}
+
+private fun objectKey(parser: Parser, token: Token) {
+ parser.currentObjectKey = token.value.toString()
+}
+
+private fun arrayStart(parser: Parser, @Suppress("UNUSED_PARAMETER") token: Token) {
+ val node = ArrayLiteral()
+ parser.placeAtCursor(node)
+}
+
+private fun transform(parser: Parser, token: Token) {
+ val node = Transformation(
+ name = token.value.toString(),
+ subject = parser.cursor,
+ )
+ parser.placeBeforeCursor(node)
+}
+
+private fun ternaryStart(parser: Parser, @Suppress("UNUSED_PARAMETER") token: Token) {
+ val node = ConditionalExpression(
+ test = parser.tree,
+ )
+ parser.tree = node
+ parser.cursor = node
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt
new file mode 100644
index 0000000000..0ea67a46c8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt
@@ -0,0 +1,307 @@
+/* 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.lib.jexl.value
+
+import mozilla.components.lib.jexl.evaluator.EvaluatorException
+
+/**
+ * A JEXL value type.
+ */
+sealed class JexlValue {
+ abstract val value: Any
+
+ abstract operator fun plus(other: JexlValue): JexlValue
+ abstract operator fun times(other: JexlValue): JexlValue
+ abstract operator fun div(other: JexlValue): JexlValue
+ abstract operator fun compareTo(other: JexlValue): Int
+
+ abstract fun toBoolean(): Boolean
+}
+
+/**
+ * JEXL Integer type.
+ */
+class JexlInteger(override val value: Int) : JexlValue() {
+ override fun div(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger(value / other.value)
+ is JexlDouble -> JexlDouble(value / other.value)
+ else -> throw EvaluatorException("Can't divide by ${other::class}")
+ }
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger(value * other.value)
+ is JexlDouble -> JexlDouble(value * other.value)
+ is JexlBoolean -> JexlInteger(value * other.toInt())
+ else -> throw EvaluatorException("Can't multiply with ${other::class}")
+ }
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger(value + other.value)
+ is JexlDouble -> JexlDouble(value + other.value)
+ is JexlString -> JexlString(value.toString() + other.value)
+ is JexlBoolean -> JexlInteger(value + (other.toInt()))
+ else -> throw EvaluatorException("Can't add ${other::class}")
+ }
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ return when (other) {
+ is JexlInteger -> value.compareTo(other.value)
+ is JexlDouble -> value.compareTo(other.value)
+ else -> throw EvaluatorException("Can't compare ${other::class}")
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return when (other) {
+ is JexlInteger -> value == other.value
+ is JexlDouble -> value.toDouble() == other.value
+ else -> false
+ }
+ }
+
+ override fun toBoolean(): Boolean = value != 0
+
+ override fun toString() = value.toString()
+
+ override fun hashCode() = value.hashCode()
+}
+
+/**
+ * JEXL Double type.
+ */
+class JexlDouble(override val value: Double) : JexlValue() {
+ override fun div(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlDouble(value / other.value)
+ is JexlDouble -> JexlDouble(value / other.value)
+ else -> throw EvaluatorException("Can't divide by ${other::class}")
+ }
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlDouble(value * other.value)
+ is JexlDouble -> JexlDouble(value * other.value)
+ is JexlBoolean -> JexlDouble(value * other.toInt())
+ else -> throw EvaluatorException("Can't multiply with ${other::class}")
+ }
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlDouble(value + other.value)
+ is JexlDouble -> JexlDouble(value + other.value)
+ is JexlString -> JexlString(value.toString() + other.value)
+ is JexlBoolean -> JexlDouble(value + (other.toInt()))
+ else -> throw EvaluatorException("Can't add ${other::class}")
+ }
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ return when (other) {
+ is JexlInteger -> value.compareTo(other.value)
+ is JexlDouble -> value.compareTo(other.value)
+ else -> throw EvaluatorException("Can't compare ${other::class}")
+ }
+ }
+
+ override fun toBoolean(): Boolean = value != 0.0
+
+ override fun equals(other: Any?): Boolean {
+ return when (other) {
+ is JexlDouble -> value == other.value
+ is JexlInteger -> {
+ value == other.value.toDouble()
+ }
+ else -> false
+ }
+ }
+
+ override fun toString() = value.toString()
+
+ override fun hashCode() = value.hashCode()
+}
+
+/**
+ * JEXL Boolean type.
+ */
+class JexlBoolean(override val value: Boolean) : JexlValue() {
+ override fun div(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't divide boolean")
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger(toInt() * other.value)
+ is JexlDouble -> JexlDouble(toInt() * other.value)
+ is JexlBoolean -> JexlInteger(toInt() * other.toInt())
+ else -> throw EvaluatorException("Can't multiply with ${other::class}")
+ }
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger((toInt()) + other.value)
+ is JexlDouble -> JexlDouble((toInt()) + other.value)
+ is JexlString -> JexlString(value.toString() + other.value)
+ is JexlBoolean -> JexlInteger((toInt()) + (other.toInt()))
+ else -> throw EvaluatorException("Can't add ${other::class}")
+ }
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ throw EvaluatorException("Can't compare ${other::class}")
+ }
+
+ fun toInt(): Int = if (value) 1 else 0
+
+ override fun equals(other: Any?) = other is JexlBoolean && value == other.value
+
+ override fun toBoolean() = value
+
+ override fun toString() = value.toString()
+
+ override fun hashCode() = value.hashCode()
+}
+
+/**
+ * JEXL String type.
+ */
+class JexlString(override val value: String) : JexlValue() {
+ override fun div(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't divide string")
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't multiply strings")
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlString(value + other.value)
+ is JexlDouble -> JexlString(value + other.value)
+ is JexlString -> JexlString(value + other.value)
+ is JexlBoolean -> JexlString(value + (if (other.value) 1 else 0))
+ else -> throw EvaluatorException("Can't add ${other::class}")
+ }
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ throw EvaluatorException("Can't compare ${other::class}")
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is JexlString && value == other.value
+ }
+
+ override fun toBoolean(): Boolean {
+ return value.isNotEmpty()
+ }
+
+ override fun toString(): String {
+ return value
+ }
+
+ override fun hashCode(): Int {
+ return value.hashCode()
+ }
+}
+
+/**
+ * JEXL Array type.
+ */
+class JexlArray(
+ override val value: List<JexlValue>,
+) : JexlValue() {
+ constructor(vararg elements: JexlValue) : this(elements.toList())
+
+ override fun div(other: JexlValue): JexlValue = throw EvaluatorException("Can't divide array")
+
+ override fun times(other: JexlValue): JexlValue = throw EvaluatorException("Can't multiply arrays")
+
+ override fun plus(other: JexlValue): JexlValue = throw EvaluatorException("Can't add arrays")
+
+ override fun compareTo(other: JexlValue): Int = throw EvaluatorException("Can't compare ${other::class}")
+
+ override fun equals(other: Any?) = other is JexlArray && value == other.value
+
+ override fun toBoolean(): Boolean = throw EvaluatorException("Can't convert array to boolean")
+
+ override fun toString(): String = value.toString()
+
+ override fun hashCode(): Int = value.hashCode()
+}
+
+/**
+ * JEXL Object type.
+ */
+class JexlObject(
+ override val value: Map<String, JexlValue>,
+) : JexlValue() {
+ constructor(vararg pairs: Pair<String, JexlValue>) : this(pairs.toMap())
+
+ override fun div(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't divide object")
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't multiply objects")
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't add objects")
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ throw EvaluatorException("Can't compare ${other::class}")
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is JexlObject && value == other.value
+ }
+
+ override fun toBoolean(): Boolean {
+ throw EvaluatorException("Can't convert object to boolean")
+ }
+
+ override fun toString(): String {
+ return value.toString()
+ }
+
+ override fun hashCode(): Int {
+ return value.hashCode()
+ }
+}
+
+/**
+ * JEXL undefined type.
+ */
+class JexlUndefined : JexlValue() {
+ override val value = Any()
+
+ override fun plus(other: JexlValue): JexlValue {
+ return this
+ }
+
+ override fun times(other: JexlValue): JexlValue = throw EvaluatorException("Can't multiply undefined values")
+
+ override fun div(other: JexlValue): JexlValue = throw EvaluatorException("Can't divide undefined values")
+
+ override fun compareTo(other: JexlValue) = if (other is JexlUndefined) 0 else 1
+
+ override fun toBoolean(): Boolean = throw EvaluatorException("Can't convert undefined to boolean")
+
+ override fun toString() = "<undefined>"
+
+ override fun equals(other: Any?) = other is JexlUndefined
+
+ override fun hashCode(): Int = 7
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt
new file mode 100644
index 0000000000..b9f04a04a9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt
@@ -0,0 +1,112 @@
+/* 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.lib.jexl
+
+import mozilla.components.lib.jexl.evaluator.JexlContext
+import mozilla.components.lib.jexl.ext.toJexl
+import mozilla.components.lib.jexl.ext.toJexlArray
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlObject
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class JexlTest {
+
+ @Test
+ fun `Should evaluate expressions`() {
+ val jexl = Jexl()
+
+ val result = jexl.evaluate("75 > 42")
+ assertEquals(true, result.value)
+ }
+
+ @Test
+ fun `Should evaluate boolean expressions`() {
+ val jexl = Jexl()
+
+ val result = jexl.evaluateBooleanExpression("42 + 23 > 50", defaultValue = false)
+
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `Should apply transform`() {
+ val jexl = Jexl()
+
+ jexl.addTransform("split") { value, arguments ->
+ value.toString().split(arguments.first().toString()).toJexlArray()
+ }
+
+ jexl.addTransform("lower") { value, _ ->
+ value.toString().lowercase().toJexl()
+ }
+
+ jexl.addTransform("last") { value, _ ->
+ (value as JexlArray).value.last()
+ }
+
+ assertEquals(
+ "poovey".toJexl(),
+ jexl.evaluate(""""Pam Poovey"|lower|split(' ')|last"""),
+ )
+
+ assertEquals(
+ JexlArray("password".toJexl(), "guest".toJexl()),
+ jexl.evaluate(""""password==guest"|split('=' + '=')"""),
+ )
+ }
+
+ @Test
+ fun `Should use context`() {
+ val jexl = Jexl()
+
+ val context = JexlContext(
+ "employees" to JexlArray(
+ JexlObject(
+ "first" to "Sterling".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 36.toJexl(),
+ ),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 75.toJexl(),
+ ),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 33.toJexl(),
+ ),
+ ),
+ )
+
+ assertEquals(
+ JexlArray(
+ JexlObject(
+ "first" to "Sterling".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 36.toJexl(),
+ ),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 33.toJexl(),
+ ),
+ ),
+ jexl.evaluate("employees[.age >= 30 && .age < 40]", context),
+ )
+
+ assertEquals(
+ JexlArray(
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 33.toJexl(),
+ ),
+ ),
+ jexl.evaluate("employees[.age >= 30 && .age < 90][.age < 35]", context),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt
new file mode 100644
index 0000000000..9fd693f816
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt
@@ -0,0 +1,53 @@
+/* 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.lib.jexl
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import kotlin.reflect.KClass
+
+/**
+ * Additional test cases that test various JEXL expressions to get a high test coverage for the lexer, parser and
+ * evaluator.
+ */
+class LanguageTest {
+ @Test
+ fun `Multiple dots in numeric expression should throw exception`() {
+ "27.42.21".evaluationThrows()
+ }
+
+ @Test
+ fun `Negating a literal should throw exception`() {
+ "-employees".evaluationThrows()
+ }
+
+ @Test
+ fun `Using non grammar character should throw exception`() {
+ "§".evaluationThrows()
+ }
+
+ private fun String.evaluatesTo(expectedResult: Any) {
+ val jexl = Jexl()
+ val actualResult = jexl.evaluate(this)
+
+ assertEquals(expectedResult, actualResult)
+ }
+
+ private fun String.evaluationThrows() {
+ evaluationThrows(JexlException::class)
+ }
+
+ private inline fun <reified T : Throwable> String.evaluationThrows(clazz: KClass<T>?) {
+ try {
+ evaluatesTo(Any())
+ fail("Expected exception to be thrown: $clazz")
+ } catch (e: Throwable) {
+ if (e !is T) {
+ throw e
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt
new file mode 100644
index 0000000000..490140bc2b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt
@@ -0,0 +1,375 @@
+/* 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.lib.jexl.evaluator
+
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.lexer.Lexer
+import mozilla.components.lib.jexl.parser.Parser
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlObject
+import mozilla.components.lib.jexl.value.JexlString
+import mozilla.components.lib.jexl.value.JexlUndefined
+import mozilla.components.lib.jexl.value.JexlValue
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+
+class EvaluatorTest {
+ private lateinit var grammar: Grammar
+
+ @Before
+ fun setUp() {
+ grammar = Grammar()
+ }
+
+ @Test
+ fun `Should evaluate a literal`() {
+ assertExpressionYieldsResult("42", 42)
+ assertExpressionYieldsResult("2.0", 2.0)
+
+ assertExpressionYieldsResult("true", true)
+ assertExpressionYieldsResult("false", false)
+
+ assertExpressionYieldsResult("\"hello world\"", "hello world")
+ assertExpressionYieldsResult("'hello world'", "hello world")
+ }
+
+ @Test
+ fun `Should evaluate an arithmetic expression`() {
+ assertExpressionYieldsResult(
+ "(2 + 3) * 4",
+ 20,
+ )
+ }
+
+ @Test
+ fun `Should evaluate a string concat`() {
+ assertExpressionYieldsResult(
+ """
+ "Hello" + (4+4) + "Wo\"rld"
+ """.trimIndent(),
+ "Hello8Wo\"rld",
+ )
+ }
+
+ @Test
+ fun `Should evaluate a true comparison expression`() {
+ assertExpressionYieldsResult(
+ "2 > 1",
+ true,
+ )
+ }
+
+ @Test
+ fun `Should evaluate a false comparison expression`() {
+ assertExpressionYieldsResult(
+ "2 <= 1",
+ false,
+ )
+ }
+
+ @Test
+ fun `Should evaluate a complex expression`() {
+ assertExpressionYieldsResult(
+ "\"foo\" && 6 >= 6 && 0 + 1 && true",
+ true,
+ )
+ }
+
+ @Test
+ fun `Should evaluate an identifier chain`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "baz" to JexlObject(
+ "bar" to JexlString("tek"),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.baz.bar",
+ "tek",
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should apply transforms`() {
+ val context = JexlContext(
+ "foo" to JexlInteger(10),
+ )
+
+ assertExpressionYieldsResult(
+ "foo|half + 3",
+ 8,
+ context = context,
+ transforms = mapOf(
+ "half" to { value, _ ->
+ value.div(JexlInteger(2))
+ },
+ ),
+ )
+ }
+
+ @Test
+ fun `Should filter arrays`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "bar" to JexlArray(
+ JexlObject("tek" to JexlString("hello")),
+ JexlObject("tek" to JexlString("baz")),
+ JexlObject("tok" to JexlString("baz")),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar[.tek == \"baz\"]",
+ listOf(JexlObject("tek" to JexlString("baz"))),
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should assume array index 0 when traversing`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "bar" to JexlArray(
+ JexlObject(
+ "tek" to JexlObject(
+ "hello" to JexlString(
+ "world",
+ ),
+ ),
+ ),
+ JexlObject(
+ "tek" to JexlObject(
+ "hello" to JexlString(
+ "universe",
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar.tek.hello",
+ "world",
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should make array elements addressable by index`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "bar" to JexlArray(
+ JexlObject("tek" to JexlString("tok")),
+ JexlObject("tek" to JexlString("baz")),
+ JexlObject("tek" to JexlString("foz")),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar[1].tek",
+ "baz",
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should allow filters to select object properties`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "baz" to JexlObject(
+ "bar" to JexlString("tek"),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo[\"ba\" + \"z\"].bar",
+ "tek",
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should allow simple filters on undefined objects`() {
+ val context = JexlContext(
+ "foo" to JexlObject(),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar[\"baz\"].tok",
+ JexlUndefined(),
+ context = context,
+ unpack = false,
+ )
+ }
+
+ @Test
+ fun `Should allow complex filters on undefined objects`() {
+ val context = JexlContext(
+ "foo" to JexlObject(),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar[.size > 1].baz",
+ JexlUndefined(),
+ context = context,
+ unpack = false,
+ )
+ }
+
+ @Test(expected = EvaluatorException::class)
+ fun `Should throw when transform does not exist`() {
+ assertExpressionYieldsResult(
+ "\"hello\"|world",
+ "-- should throw",
+ )
+ }
+
+ @Test
+ fun `Should apply the DivFloor operator`() {
+ assertExpressionYieldsResult(
+ "7 // 2",
+ 3,
+ )
+ }
+
+ @Test
+ fun `Should evaluate an object literal`() {
+ assertExpressionYieldsResult(
+ "{foo: {bar: \"tek\"}}",
+ JexlObject(
+ "foo" to JexlObject(
+ "bar" to JexlString(
+ "tek",
+ ),
+ ),
+ ),
+ unpack = false,
+ )
+ }
+
+ @Test
+ fun `Should evaluate an empty object literal`() {
+ assertExpressionYieldsResult(
+ "{}",
+ emptyMap<String, JexlValue>(),
+ )
+ }
+
+ @Test
+ fun `Should evaluate a transform with multiple args`() {
+ assertExpressionYieldsResult(
+ """"foo"|concat("baz", "bar", "tek")""",
+ "foo: bazbartek",
+ transforms = mapOf(
+ "concat" to { value, arguments ->
+ value + JexlString(": ") + JexlString(
+ arguments.joinToString(""),
+ )
+ },
+ ),
+ )
+ }
+
+ @Test
+ fun `Should evaluate dot notation for object literals`() {
+ assertExpressionYieldsResult(
+ "{foo: \"bar\"}.foo",
+ "bar",
+ )
+ }
+
+ @Test
+ @Ignore("JavaScript properties are not implemented yet")
+ fun `Should allow access to literal properties`() {
+ assertExpressionYieldsResult(
+ "\"foo\".length",
+ 3,
+ )
+ }
+
+ @Test
+ fun `Should evaluate array literals`() {
+ assertExpressionYieldsResult(
+ "[\"foo\", 1+2]",
+ listOf(
+ JexlString("foo"),
+ JexlInteger(3),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should apply the 'in' operator to strings`() {
+ assertExpressionYieldsResult(""""bar" in "foobartek"""", true)
+ assertExpressionYieldsResult(""""baz" in "foobartek"""", false)
+ }
+
+ @Test
+ fun `Should apply the 'in' operator to arrays`() {
+ assertExpressionYieldsResult(""""bar" in ["foo","bar","tek"]""", true)
+ assertExpressionYieldsResult(""""baz" in ["foo","bar","tek"]""", false)
+ }
+
+ @Test
+ fun `Should evaluate a conditional expression`() {
+ assertExpressionYieldsResult("\"foo\" ? 1 : 2", 1)
+ assertExpressionYieldsResult("\"\" ? 1 : 2", 2)
+ }
+
+ @Test
+ fun `Should allow missing consequent in ternary`() {
+ assertExpressionYieldsResult(""""foo" ?: "bar"""", "foo")
+ }
+
+ @Test
+ @Ignore("JavaScript properties are not implemented yet")
+ fun `Does not treat falsey properties as undefined`() {
+ assertExpressionYieldsResult("\"\".length", 0)
+ }
+
+ @Test
+ fun `Should handle an expression with arbitrary whitespace`() {
+ assertExpressionYieldsResult("(\t2\n+\n3) *\n4\n\r\n", 20)
+ }
+
+ private fun assertExpressionYieldsResult(
+ expression: String,
+ result: Any,
+ context: JexlContext = JexlContext(),
+ transforms: Map<String, Transform> = emptyMap(),
+ unpack: Boolean = true,
+ ) {
+ val tree = toTree(expression)
+
+ println(tree)
+
+ val evaluator = Evaluator(context, grammar, transforms)
+ val actual = evaluator.evaluate(tree)
+
+ assertEquals(result, if (unpack) actual.value else actual)
+ }
+
+ private fun toTree(
+ expression: String,
+ grammar: Grammar = Grammar(),
+ ): AstNode {
+ val lexer = Lexer(grammar)
+ val parser = Parser(grammar)
+
+ return parser.parse(lexer.tokenize(expression))
+ ?: throw AssertionError("Expression yielded null AST tree")
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.kt
new file mode 100644
index 0000000000..0787d17ea1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.kt
@@ -0,0 +1,65 @@
+/* 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.lib.jexl.ext
+
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlString
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.lang.UnsupportedOperationException
+
+class JexlExtensionsTest {
+ @Test
+ fun `Simple types`() {
+ assertEquals("Hello", "Hello".toJexl().value)
+ assertEquals(23, 23.toJexl().value)
+ assertEquals(23.0, 23.0.toJexl().value, 0.00001)
+ assertEquals(1.0, 1.0f.toJexl().value, 0.00001)
+ assertEquals(0, 0.toJexl().value)
+ assertEquals(true, true.toJexl().value)
+ assertEquals(false, false.toJexl().value)
+ }
+
+ @Test
+ fun `Arrays`() {
+ assertEquals(
+ JexlArray(JexlInteger(1), JexlInteger(2), JexlInteger(3)),
+ listOf(1, 2, 3).toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(),
+ emptyList<String>().toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(JexlString("Hello"), JexlString("World")),
+ listOf("Hello", "World").toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(JexlDouble(1.0), JexlDouble(23.0)),
+ listOf(1.0, 23.0).toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(JexlDouble(52.0), JexlDouble(-2.0)),
+ listOf(52.0f, -2f).toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(JexlBoolean(false), JexlBoolean(true)),
+ listOf(false, true).toJexlArray(),
+ )
+ }
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun `Unsupported array type`() {
+ listOf(Pair(1, 2), Pair(3, 4)).toJexlArray()
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt
new file mode 100644
index 0000000000..fc8b518f0b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt
@@ -0,0 +1,483 @@
+/* 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.lib.jexl.lexer
+
+import mozilla.components.lib.jexl.grammar.Grammar
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class LexerTest {
+ private lateinit var lexer: Lexer
+
+ @Before
+ fun setUp() {
+ lexer = Lexer(Grammar())
+ }
+
+ @Test
+ fun `should count a string as one element`() {
+ val expression = "\"foo\""
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "\"foo\"",
+ "foo",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support single-quote strings`() {
+ val expression = "'foo'"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "'foo'", "foo"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should find multiple strings`() {
+ val expression = "\"foo\" 'bar' \"baz\""
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "\"foo\"",
+ "foo",
+ ),
+ Token(
+ Token.Type.LITERAL,
+ "'bar'",
+ "bar",
+ ),
+ Token(
+ Token.Type.LITERAL,
+ "\"baz\"",
+ "baz",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support escaping double-quotes`() {
+ val expression = "\"f\\\"oo\""
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "\"f\\\"oo\"",
+ "f\"oo",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support escaping single-quotes`() {
+ val expression = "'f\\'oo'"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "'f\\'oo'",
+ "f'oo",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should count an identifier as one element`() {
+ val expression = "alpha12345"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.IDENTIFIER,
+ "alpha12345",
+ "alpha12345",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support boolean true`() {
+ val expression = "true"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "true", true),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support boolean false`() {
+ val expression = "false"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "false", false),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support comments`() {
+ val expression = "# This is a comment"
+
+ assertExpressionYieldsTokens(expression, emptyList())
+ }
+
+ @Test
+ fun `should support comments after expressions`() {
+ val expression = "true false #This is a comment"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.LITERAL, "false", false),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support operators`() {
+ val expression = "true + false - true + false++"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.BINARY_OP, "-", "-"),
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support operators with two characters`() {
+ val expression = "true == false + true != false >= true > in false"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "!=", "!="),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.BINARY_OP, ">=", ">="),
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, ">", ">"),
+ Token(Token.Type.BINARY_OP, "in", "in"),
+ Token(Token.Type.LITERAL, "false", false),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support numerics`() {
+ val expression = "1234 == 782"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "1234", 1234),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(Token.Type.LITERAL, "782", 782),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support negative numerics`() {
+ val expression = "-7.6 + (-20 * -1)"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "-7.6", -7.6),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.OPEN_PAREN, "(", "("),
+ Token(Token.Type.LITERAL, "-20", -20),
+ Token(Token.Type.BINARY_OP, "*", "*"),
+ Token(Token.Type.LITERAL, "-1", -1),
+ Token(Token.Type.CLOSE_PAREN, ")", ")"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support floating point numerics`() {
+ val expression = "1.337 != 2.42"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "1.337",
+ 1.337,
+ ),
+ Token(Token.Type.BINARY_OP, "!=", "!="),
+ Token(Token.Type.LITERAL, "2.42", 2.42),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support identifiers, numerics and operators`() {
+ val expression = "person.age == 12 && (person.hasJob == true || person.onVacation != false)"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.IDENTIFIER,
+ "person",
+ "person",
+ ),
+ Token(Token.Type.DOT, ".", "."),
+ Token(
+ Token.Type.IDENTIFIER,
+ "age",
+ "age",
+ ),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(Token.Type.LITERAL, "12", 12),
+ Token(Token.Type.BINARY_OP, "&&", "&&"),
+ Token(Token.Type.OPEN_PAREN, "(", "("),
+ Token(
+ Token.Type.IDENTIFIER,
+ "person",
+ "person",
+ ),
+ Token(Token.Type.DOT, ".", "."),
+ Token(
+ Token.Type.IDENTIFIER,
+ "hasJob",
+ "hasJob",
+ ),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "||", "||"),
+ Token(
+ Token.Type.IDENTIFIER,
+ "person",
+ "person",
+ ),
+ Token(Token.Type.DOT, ".", "."),
+ Token(
+ Token.Type.IDENTIFIER,
+ "onVacation",
+ "onVacation",
+ ),
+ Token(Token.Type.BINARY_OP, "!=", "!="),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.CLOSE_PAREN, ")", ")"),
+ ),
+ )
+ }
+
+ @Test
+ fun `tokenize math expression`() {
+ val expression = "age * (3 - 1)"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.IDENTIFIER,
+ "age",
+ "age",
+ ),
+ Token(Token.Type.BINARY_OP, "*", "*"),
+ Token(Token.Type.OPEN_PAREN, "(", "("),
+ Token(Token.Type.LITERAL, "3", 3),
+ Token(Token.Type.BINARY_OP, "-", "-"),
+ Token(Token.Type.LITERAL, "1", 1),
+ Token(Token.Type.CLOSE_PAREN, ")", ")"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should not split grammar elements out of transforms`() {
+ val expression = "inString"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.IDENTIFIER,
+ "inString",
+ "inString",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should handle a complex mix of comments in single, multiline and value contexts`() {
+ val expression = """
+ 6+x - -17.55*y #end comment
+ <= !foo.bar["baz\"foz"] # with space
+ && b=="not a #comment" # is a comment
+ # comment # 2nd comment
+ """.trimIndent()
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "6", 6),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.IDENTIFIER, "x", "x"),
+ Token(Token.Type.BINARY_OP, "-", "-"),
+ Token(
+ Token.Type.LITERAL,
+ "-17.55",
+ -17.55,
+ ),
+ Token(Token.Type.BINARY_OP, "*", "*"),
+ Token(Token.Type.IDENTIFIER, "y", "y"),
+ Token(Token.Type.BINARY_OP, "<=", "<="),
+ Token(Token.Type.UNARY_OP, "!", "!"),
+ Token(
+ Token.Type.IDENTIFIER,
+ "foo",
+ "foo",
+ ),
+ Token(Token.Type.DOT, ".", "."),
+ Token(
+ Token.Type.IDENTIFIER,
+ "bar",
+ "bar",
+ ),
+ Token(Token.Type.OPEN_BRACKET, "[", "["),
+ Token(
+ Token.Type.LITERAL,
+ "\"baz\\\"foz\"",
+ "baz\"foz",
+ ),
+ Token(
+ Token.Type.CLOSE_BRACKET,
+ "]",
+ "]",
+ ),
+ Token(Token.Type.BINARY_OP, "&&", "&&"),
+ Token(Token.Type.IDENTIFIER, "b", "b"),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(
+ Token.Type.LITERAL,
+ "\"not a #comment\"",
+ "not a #comment",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should tokenize a full expression`() {
+ val expression = """6+x - -17.55*y<= !foo.bar["baz\\"foz"]"""
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "6", 6),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.IDENTIFIER, "x", "x"),
+ Token(Token.Type.BINARY_OP, "-", "-"),
+ Token(Token.Type.LITERAL, "-17.55", -17.55),
+ Token(Token.Type.BINARY_OP, "*", "*"),
+ Token(Token.Type.IDENTIFIER, "y", "y"),
+ Token(Token.Type.BINARY_OP, "<=", "<="),
+ Token(Token.Type.UNARY_OP, "!", "!"),
+ Token(Token.Type.IDENTIFIER, "foo", "foo"),
+ Token(Token.Type.DOT, ".", "."),
+ Token(Token.Type.IDENTIFIER, "bar", "bar"),
+ Token(Token.Type.OPEN_BRACKET, "[", "["),
+ Token(Token.Type.LITERAL, """"baz\\"foz"""", """baz\"foz"""),
+ Token(Token.Type.CLOSE_BRACKET, "]", "]"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should consider minus to be negative appropriately`() {
+ val expression = "-1?-2:-3"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "-1", -1),
+ Token(Token.Type.QUESTION, "?", "?"),
+ Token(Token.Type.LITERAL, "-2", -2),
+ Token(Token.Type.COLON, ":", ":"),
+ Token(Token.Type.LITERAL, "-3", -3),
+ ),
+ )
+ }
+
+ private fun assertExpressionYieldsTokens(expression: String, tokens: List<Token>) {
+ val actual = lexer.tokenize(expression)
+
+ println(actual)
+
+ assertEquals(tokens.size, actual.size)
+
+ for (i in 0 until tokens.size) {
+ assertEquals(tokens[i], actual[i])
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt
new file mode 100644
index 0000000000..0dbb07bc27
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt
@@ -0,0 +1,506 @@
+/* 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.lib.jexl.parser
+
+import mozilla.components.lib.jexl.ast.ArrayLiteral
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.ast.BinaryExpression
+import mozilla.components.lib.jexl.ast.ConditionalExpression
+import mozilla.components.lib.jexl.ast.FilterExpression
+import mozilla.components.lib.jexl.ast.Identifier
+import mozilla.components.lib.jexl.ast.Literal
+import mozilla.components.lib.jexl.ast.ObjectLiteral
+import mozilla.components.lib.jexl.ast.Transformation
+import mozilla.components.lib.jexl.ast.UnaryExpression
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.lexer.Lexer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+
+class ParserTest {
+
+ @Test
+ fun `Should parse literal`() {
+ val expression = "42"
+
+ assertExpressionYieldsTree(
+ expression,
+ Literal(42),
+ )
+ }
+
+ @Test
+ fun `Should parse math expression`() {
+ val expression = "42 + 23"
+
+ assertExpressionYieldsTree(
+ expression,
+ BinaryExpression(
+ left = Literal(42),
+ right = Literal(23),
+ operator = "+",
+ ),
+ )
+ }
+
+ @Test(expected = ParserException::class)
+ fun `Should throw on incomplete expression`() {
+ val expression = "42 +"
+ parse(expression)
+ }
+
+ @Test
+ fun `Should parse expression with identifier`() {
+ val expression = "age > 21"
+
+ assertExpressionYieldsTree(
+ expression,
+ BinaryExpression(
+ operator = ">",
+ left = Identifier("age"),
+ right = Literal(21),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should parse expression with sub expression`() {
+ val expression = "(age + 5) > 42"
+
+ assertExpressionYieldsTree(
+ expression,
+ BinaryExpression(
+ operator = ">",
+ left = BinaryExpression(
+ operator = "+",
+ left = Identifier("age"),
+ right = Literal(5),
+ ),
+ right = Literal(42),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should parse expression following operator precedence`() {
+ assertExpressionYieldsTree(
+ "5 + 7 * 2",
+ BinaryExpression(
+ operator = "+",
+ left = Literal(5),
+ right = BinaryExpression(
+ operator = "*",
+ left = Literal(7),
+ right = Literal(2),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsTree(
+ "5 * 7 + 2",
+ BinaryExpression(
+ operator = "+",
+ left = BinaryExpression(
+ operator = "*",
+ left = Literal(5),
+ right = Literal(7),
+ ),
+ right = Literal(2),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle encapsulation of subtree`() {
+ assertExpressionYieldsTree(
+ "2+3*4==5/6-7",
+ BinaryExpression(
+ operator = "==",
+ left = BinaryExpression(
+ operator = "+",
+ left = Literal(2),
+ right = BinaryExpression(
+ operator = "*",
+ left = Literal(3),
+ right = Literal(4),
+ ),
+ ),
+ right = BinaryExpression(
+ operator = "-",
+ left = BinaryExpression(
+ operator = "/",
+ left = Literal(5),
+ right = Literal(6),
+ ),
+ right = Literal(7),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle a unary operator`() {
+ assertExpressionYieldsTree(
+ "1*!!true-2",
+ BinaryExpression(
+ operator = "-",
+ left = BinaryExpression(
+ operator = "*",
+ left = Literal(1),
+ right = UnaryExpression(
+ operator = "!",
+ right = UnaryExpression(
+ operator = "!",
+ right = Literal(true),
+ ),
+ ),
+ ),
+ right = Literal(2),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested subexpressions`() {
+ assertExpressionYieldsTree(
+ "(4*(2+3))/5",
+ BinaryExpression(
+ operator = "/",
+ left = BinaryExpression(
+ operator = "*",
+ left = Literal(4),
+ right = BinaryExpression(
+ operator = "+",
+ left = Literal(2),
+ right = Literal(3),
+ ),
+ ),
+ right = Literal(5),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle whitespace in an expression`() {
+ assertExpressionYieldsTree(
+ "\t2\r\n+\n\r3\n\n",
+ BinaryExpression(
+ operator = "+",
+ left = Literal(2),
+ right = Literal(3),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle object literals`() {
+ assertExpressionYieldsTree(
+ "{foo: \"bar\", tek: 1+2}",
+ ObjectLiteral(
+ "foo" to Literal("bar"),
+ "tek" to BinaryExpression(
+ operator = "+",
+ left = Literal(1),
+ right = Literal(2),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested object literals`() {
+ assertExpressionYieldsTree(
+ """{
+ foo: {
+ bar: "tek",
+ baz: 42
+ }
+ }""",
+ ObjectLiteral(
+ "foo" to ObjectLiteral(
+ "bar" to Literal("tek"),
+ "baz" to Literal(42),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle empty object literals`() {
+ assertExpressionYieldsTree(
+ "{}",
+ ObjectLiteral(),
+ )
+ }
+
+ @Test
+ fun `Should handle array literals`() {
+ assertExpressionYieldsTree(
+ "[\"foo\", 1+2]",
+ ArrayLiteral(
+ Literal("foo"),
+ BinaryExpression(
+ operator = "+",
+ left = Literal(1),
+ right = Literal(2),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested array literals`() {
+ assertExpressionYieldsTree(
+ "[\"foo\", [\"bar\", \"tek\"]]",
+ ArrayLiteral(
+ Literal("foo"),
+ ArrayLiteral(
+ Literal("bar"),
+ Literal("tek"),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle empty array literals`() {
+ assertExpressionYieldsTree(
+ "[]",
+ ArrayLiteral(),
+ )
+ }
+
+ @Test
+ fun `Should chain traversed identifiers`() {
+ assertExpressionYieldsTree(
+ "foo.bar.baz + 1",
+ BinaryExpression(
+ operator = "+",
+ left = Identifier(
+ "baz",
+ from = Identifier(
+ "bar",
+ from = Identifier("foo"),
+ ),
+ ),
+ right = Literal(1),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should apply transforms and arguments`() {
+ assertExpressionYieldsTree(
+ "foo|tr1|tr2.baz|tr3({bar:\"tek\"})",
+ Transformation(
+ name = "tr3",
+ arguments = mutableListOf(
+ ObjectLiteral(
+ "bar" to Literal("tek"),
+ ),
+ ),
+ subject = Identifier(
+ value = "baz",
+ from = Transformation(
+ name = "tr2",
+ subject = Transformation(
+ name = "tr1",
+ subject = Identifier("foo"),
+ ),
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle multiple arguments in transforms`() {
+ assertExpressionYieldsTree(
+ "foo|bar(\"tek\", 5, true)",
+ Transformation(
+ name = "bar",
+ subject = Identifier("foo"),
+ arguments = mutableListOf(
+ Literal("tek"),
+ Literal(5),
+ Literal(true),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should apply filters to identifiers`() {
+ assertExpressionYieldsTree(
+ """foo[1][.bar[0] == "tek"].baz""",
+ Identifier(
+ "baz",
+ from = FilterExpression(
+ relative = true,
+ expression = BinaryExpression(
+ operator = "==",
+ left = FilterExpression(
+ relative = false,
+ expression = Literal(0),
+ subject = Identifier(
+ value = "bar",
+ relative = true,
+ ),
+ ),
+ right = Literal("tek"),
+ ),
+ subject = FilterExpression(
+ relative = false,
+ expression = Literal(1),
+ subject = Identifier("foo"),
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should allow dot notation for all operands`() {
+ assertExpressionYieldsTree(
+ "\"foo\".length + {foo: \"bar\"}.foo",
+ BinaryExpression(
+ operator = "+",
+ left = Identifier("length", from = Literal("foo")),
+ right = Identifier(
+ "foo",
+ from = ObjectLiteral(
+ "foo" to Literal("bar"),
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should allow dot notation on subexpressions`() {
+ assertExpressionYieldsTree(
+ "(\"foo\" + \"bar\").length",
+ Identifier(
+ "length",
+ from = BinaryExpression(
+ operator = "+",
+ left = Literal("foo"),
+ right = Literal("bar"),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should allow dot notation on arrays`() {
+ assertExpressionYieldsTree(
+ "[\"foo\", \"bar\"].length",
+ Identifier(
+ "length",
+ from = ArrayLiteral(
+ Literal("foo"),
+ Literal("bar"),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle a ternary expression`() {
+ assertExpressionYieldsTree(
+ "foo ? 1 : 0",
+ ConditionalExpression(
+ test = Identifier("foo"),
+ consequent = Literal(1),
+ alternate = Literal(0),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested and grouped ternary expressions`() {
+ assertExpressionYieldsTree(
+ "foo ? (bar ? 1 : 2) : 3",
+ ConditionalExpression(
+ test = Identifier("foo"),
+ consequent = ConditionalExpression(
+ test = Identifier("bar"),
+ consequent = Literal(1),
+ alternate = Literal(2),
+ ),
+ alternate = Literal(3),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested, non-grouped ternary expressions`() {
+ assertExpressionYieldsTree(
+ "foo ? bar ? 1 : 2 : 3",
+ ConditionalExpression(
+ test = Identifier("foo"),
+ consequent = ConditionalExpression(
+ test = Identifier("bar"),
+ consequent = Literal(1),
+ alternate = Literal(2),
+ ),
+ alternate = Literal(3),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle ternary expression with objects`() {
+ assertExpressionYieldsTree(
+ "foo ? {bar: \"tek\"} : \"baz\"",
+ ConditionalExpression(
+ test = Identifier("foo"),
+ consequent = ObjectLiteral(
+ "bar" to Literal("tek"),
+ ),
+ alternate = Literal("baz"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should correctly balance a binary op between complex identifiers`() {
+ assertExpressionYieldsTree(
+ "a.b == c.d",
+ BinaryExpression(
+ operator = "==",
+ left = Identifier(
+ value = "b",
+ from = Identifier("a"),
+ ),
+ right = Identifier(
+ value = "d",
+ from = Identifier("c"),
+ ),
+ ),
+ )
+ }
+
+ private fun assertExpressionYieldsTree(expression: String, tree: AstNode?) {
+ val actual = parse(expression)
+
+ if (tree != null) {
+ assertNotNull(actual)
+ }
+
+ println(actual)
+
+ assertEquals(tree, actual)
+ }
+
+ private fun parse(expression: String): AstNode? {
+ val grammar = Grammar()
+ val lexer = Lexer(grammar)
+ val parser = Parser(grammar)
+
+ return parser.parse(lexer.tokenize(expression))
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt
new file mode 100644
index 0000000000..8cef9cb0da
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt
@@ -0,0 +1,170 @@
+/* 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.lib.jexl.value
+
+import mozilla.components.lib.jexl.Jexl
+import mozilla.components.lib.jexl.JexlException
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import kotlin.reflect.KClass
+
+class JexlValueTest {
+ @Test
+ fun `double arithmetic`() {
+ "2.0 + 1".evaluatesTo(3.0)
+ "2.0 + 4.0".evaluatesTo(6.0)
+ "2.0 + 'a'".evaluatesTo("2.0a")
+ "2.0 + true".evaluatesTo(3.0)
+ "2.0 + false".evaluatesTo(2.0)
+ "2.0 + {}".evaluationThrows()
+
+ "2.0 * 2".evaluatesTo(4.0)
+ "3.0 * 3.0".evaluatesTo(9.0)
+ "2.0 * true".evaluatesTo(2.0)
+ "2.0 * false".evaluatesTo(0.0)
+ "2.0 * a".evaluationThrows()
+
+ "4.0 / 2".evaluatesTo(2.0)
+ "6.0 / 3.0".evaluatesTo(2.0)
+ "2.0 / 'a'".evaluationThrows()
+
+ "2.0 == 2.0".evaluatesTo(true)
+ "2.0 == 2".evaluatesTo(true)
+ "2.1 == 2.0".evaluatesTo(false)
+
+ "2.0 > 1.0".evaluatesTo(true)
+ "2.0 < 4.0".evaluatesTo(true)
+ "1.0 > 2.0".evaluatesTo(false)
+ "5.0 < 2.0".evaluatesTo(false)
+ "1.0 < 1.0".evaluatesTo(false)
+ "1.0 > 1.0".evaluatesTo(false)
+ }
+
+ @Test
+ fun `integer arithmetic`() {
+ "2 + 1".evaluatesTo(3)
+ "2 + 4.0".evaluatesTo(6.0)
+ "2 + 'a'".evaluatesTo("2a")
+ "2 + true".evaluatesTo(3)
+ "2 + false".evaluatesTo(2)
+ "2 + {}".evaluationThrows()
+
+ "2 * 2".evaluatesTo(4)
+ "3 * 3.0".evaluatesTo(9.0)
+ "2 * true".evaluatesTo(2)
+ "2 * false".evaluatesTo(0)
+ "2 * a".evaluationThrows()
+
+ "4 / 2".evaluatesTo(2)
+ "6 / 3.0".evaluatesTo(2.0)
+ "2 / 'a'".evaluationThrows()
+
+ "2 == 2.0".evaluatesTo(true)
+ "2 == 2".evaluatesTo(true)
+
+ "2 > 1".evaluatesTo(true)
+ "2 < 4".evaluatesTo(true)
+ "1 > 2".evaluatesTo(false)
+ "5 < 2".evaluatesTo(false)
+ "1 < 1".evaluatesTo(false)
+ "1 > 1".evaluatesTo(false)
+ }
+
+ @Test
+ fun `boolean arithmetic`() {
+ "true / false".evaluationThrows()
+
+ "true * 2".evaluatesTo(2)
+ "false * 5".evaluatesTo(0)
+ "true * 2.0".evaluatesTo(2.0)
+ "false * 5.0".evaluatesTo(0.0)
+ "true * {}".evaluationThrows()
+ "true * []".evaluationThrows()
+ "true * true".evaluatesTo(1)
+ "false * false".evaluatesTo(0)
+ "true * false".evaluatesTo(0)
+
+ "true + 1".evaluatesTo(2)
+ "false + 1".evaluatesTo(1)
+ "true + 1.0".evaluatesTo(2.0)
+ "false + 1.0".evaluatesTo(1.0)
+ "true + 'hello'".evaluatesTo("truehello")
+ "true + true".evaluatesTo(2)
+ "true + false".evaluatesTo(1)
+ "false + true".evaluatesTo(1)
+ "false + false".evaluatesTo(0)
+ "false + {}".evaluationThrows()
+ "true + []".evaluationThrows()
+
+ "true > false".evaluationThrows()
+ "false < false".evaluationThrows()
+
+ "true == true".evaluatesTo(true)
+ "false == false".evaluatesTo(true)
+ "true == false".evaluatesTo(false)
+ "false == true".evaluatesTo(false)
+ "true == 'hello'".evaluatesTo(false)
+ }
+
+ @Test
+ fun `string arithmetic`() {
+ "'a' / 2".evaluationThrows()
+ "'a' * 2".evaluationThrows()
+
+ "'hello' + 1".evaluatesTo("hello1")
+ "'hello' + 2.0".evaluatesTo("hello2.0")
+ "'hello' + ' ' + 'world'".evaluatesTo("hello world")
+
+ "'hello' > 'world'".evaluationThrows()
+ "'world' < 'hello'".evaluationThrows()
+
+ "'hello' == true".evaluatesTo(false)
+ "'' == true".evaluatesTo(false)
+ "'hello' == false".evaluatesTo(false)
+ "'' == false".evaluatesTo(false)
+ }
+
+ @Test
+ fun `array arithmetic`() {
+ "[1,2,3] == [1,2,3]".evaluatesTo(true)
+ "[2,3,4] == [2,3,5]".evaluatesTo(false)
+ }
+
+ @Test
+ fun `object arithmetic`() {
+ "{} / 2".evaluationThrows()
+ "{} * 5".evaluationThrows()
+ "{} + 'hello'".evaluationThrows()
+ "{} > 9".evaluationThrows()
+
+ "{} == {}".evaluatesTo(true)
+ "{agent:'Archer'} == { agent: 'Archer' }".evaluatesTo(true)
+ "{a: 1, b: 2} == {b: 2, a: 1}".evaluatesTo(true)
+ "{} == 2".evaluatesTo(false)
+ }
+}
+
+private fun String.evaluatesTo(expectedResult: Any, unpacked: Boolean = true) {
+ val jexl = Jexl()
+ val actualResult = jexl.evaluate(this)
+
+ assertEquals(expectedResult, if (unpacked) actualResult.value else actualResult)
+}
+
+private fun String.evaluationThrows() {
+ evaluationThrows(JexlException::class)
+}
+
+private inline fun <reified T : Throwable> String.evaluationThrows(clazz: KClass<T>?) {
+ try {
+ evaluatesTo(Any())
+ fail("Expected exception to be thrown: $clazz")
+ } catch (e: Throwable) {
+ if (e !is T) {
+ throw e
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/README.md b/mobile/android/android-components/components/lib/publicsuffixlist/README.md
new file mode 100644
index 0000000000..1ac28bcb60
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/README.md
@@ -0,0 +1,64 @@
+# [Android Components](../../../README.md) > Libraries > Public Suffix List
+
+A library for reading and using the Public Suffix List.
+
+> A "public suffix" is one under which Internet users can (or historically could) directly register names. Some examples of public suffixes are .com, .co.uk and pvt.k12.ma.us. The Public Suffix List is a list of all known public suffixes.
+> [https://publicsuffix.org/](https://publicsuffix.org/)
+
+## Usage
+
+### 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:lib-publicsuffixlist:{latest-version}"
+```
+
+### Using the public suffix list
+
+The `PublicSuffixList` class offers multiple methods for using the public suffix list data. For every instance the list needs to be read from disk into memory once. Therefore all methods return [Deferred](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/) types. The list data is cached in the `PublicSuffixList` and therefore it is recommended to keep a single instance in memory when frequently accessing the list. The list data can be prefetched to guarantee fast access for subsequent access.
+
+```Kotlin
+val publicSuffixList = PublicSuffixList(context)
+
+// Not needed, but allows a consumer to decide when the read is happening:
+publicSuffixList.prefetch()
+ // Optionally you can wait for the read to complete:
+publicSuffixList.prefetch().await()
+```
+
+```Kotlin
+// Extracting the effective top-level domain (eTLD)
+publicSuffixList.getPublicSuffixPlusOne("www.mozilla.org") // -> mozilla.org
+publicSuffixList.getPublicSuffixPlusOne("www.bbc.co.uk") // -> bbc.co.uk
+publicSuffixList.getPublicSuffixPlusOne("a.b.ide.kyoto.jp") // -> b.ide.kyoto.jp
+```
+
+```Kotlin
+// Checking whether a value is a public suffix:
+publicSuffixList.isPublicSuffix("org") // -> true
+publicSuffixList.isPublicSuffix("co.uk") // -> true
+publicSuffixList.isPublicSuffix("org") // -> true
+publicSuffixList.isPublicSuffix("ide.kyoto.jp") --> true
+```
+
+```Kotlin
+// Extracting the public suffix from a domain
+publicSuffixList.getPublicSuffix("www.mozilla.org") // -> org
+publicSuffixList.getPublicSuffix("www.bbc.co.uk") // -> co.uk
+publicSuffixList.getPublicSuffix("a.b.ide.kyoto.jp") // -> ide.kyoto.jp
+```
+
+```Kotlin
+// Removing the public suffix from a domain
+publicSuffixList.stripPublicSuffix("www.mozilla.org") // -> www.mozilla
+publicSuffixList.stripPublicSuffix("foobar.blogspot.com") // -> foobar
+publicSuffixList.stripPublicSuffix("www.example.pvt.k12.ma.us") // -> www.example
+```
+
+## 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/lib/publicsuffixlist/build.gradle b/mobile/android/android-components/components/lib/publicsuffixlist/build.gradle
new file mode 100644
index 0000000000..6728b4cc46
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/build.gradle
@@ -0,0 +1,49 @@
+/* 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/. */
+
+plugins {
+ id 'mozac.PublicSuffixListPlugin'
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'mozac.PublicSuffixListPlugin'
+
+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'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'mozilla.components.lib.publicsuffixlist'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_annotation
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/lib/publicsuffixlist/proguard-rules.pro b/mobile/android/android-components/components/lib/publicsuffixlist/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/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/lib/publicsuffixlist/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/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/lib/publicsuffixlist/src/main/assets/publicsuffixes b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/assets/publicsuffixes
new file mode 100644
index 0000000000..6fbd7cfa64
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/assets/publicsuffixes
Binary files differ
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
new file mode 100644
index 0000000000..dbaab5530d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
@@ -0,0 +1,138 @@
+/* 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.lib.publicsuffixlist
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * API for reading and accessing the public suffix list.
+ *
+ * > A "public suffix" is one under which Internet users can (or historically could) directly register names. Some
+ * > examples of public suffixes are .com, .co.uk and pvt.k12.ma.us. The Public Suffix List is a list of all known
+ * > public suffixes.
+ *
+ * Note that this implementation applies the rules of the public suffix list only and does not validate domains.
+ *
+ * https://publicsuffix.org/
+ * https://github.com/publicsuffix/list
+ */
+class PublicSuffixList(
+ context: Context,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ private val scope: CoroutineScope = CoroutineScope(dispatcher),
+) {
+ private val data: PublicSuffixListData by lazy { PublicSuffixListLoader.load(context) }
+
+ /**
+ * Prefetch the public suffix list from disk so that it is available in memory.
+ */
+ fun prefetch(): Deferred<Unit> = scope.async {
+ data.run { Unit }
+ }
+
+ /**
+ * Returns true if the given [domain] is a public suffix; false otherwise.
+ *
+ * E.g.:
+ * ```
+ * co.uk -> true
+ * com -> true
+ * mozilla.org -> false
+ * org -> true
+ * ```
+ *
+ * Note that this method ignores the default "prevailing rule" described in the formal public suffix list algorithm:
+ * If no rule matches then the passed [domain] is assumed to *not* be a public suffix.
+ *
+ * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
+ * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
+ */
+ fun isPublicSuffix(domain: String): Deferred<Boolean> = scope.async {
+ when (data.getPublicSuffixOffset(domain)) {
+ is PublicSuffixOffset.PublicSuffix -> true
+ else -> false
+ }
+ }
+
+ /**
+ * Returns the public suffix and one more level; known as the registrable domain. Returns `null` if
+ * [domain] is a public suffix itself.
+ *
+ * E.g.:
+ * ```
+ * wwww.mozilla.org -> mozilla.org
+ * www.bcc.co.uk -> bbc.co.uk
+ * a.b.ide.kyoto.jp -> b.ide.kyoto.jp
+ * ```
+ *
+ * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
+ * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
+ */
+ fun getPublicSuffixPlusOne(domain: String): Deferred<String?> = scope.async {
+ when (val offset = data.getPublicSuffixOffset(domain)) {
+ is PublicSuffixOffset.Offset ->
+ domain
+ .split('.')
+ .drop(offset.value)
+ .joinToString(separator = ".")
+ else -> null
+ }
+ }
+
+ /**
+ * Returns the public suffix of the given [domain]; known as the effective top-level domain (eTLD). Returns `null`
+ * if the [domain] is a public suffix itself.
+ *
+ * E.g.:
+ * ```
+ * wwww.mozilla.org -> org
+ * www.bcc.co.uk -> co.uk
+ * a.b.ide.kyoto.jp -> ide.kyoto.jp
+ * ```
+ *
+ * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
+ * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
+ */
+ fun getPublicSuffix(domain: String) = scope.async {
+ when (val offset = data.getPublicSuffixOffset(domain)) {
+ is PublicSuffixOffset.Offset ->
+ domain
+ .split('.')
+ .drop(offset.value + 1)
+ .joinToString(separator = ".")
+ else -> null
+ }
+ }
+
+ /**
+ * Strips the public suffix from the given [domain]. Returns the original domain if no public suffix could be
+ * stripped.
+ *
+ * E.g.:
+ * ```
+ * wwww.mozilla.org -> www.mozilla
+ * www.bcc.co.uk -> www.bbc
+ * a.b.ide.kyoto.jp -> a.b
+ * ```
+ *
+ * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
+ * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
+ */
+ fun stripPublicSuffix(domain: String) = scope.async {
+ when (val offset = data.getPublicSuffixOffset(domain)) {
+ is PublicSuffixOffset.Offset ->
+ domain
+ .split('.')
+ .joinToString(separator = ".", limit = offset.value + 1, truncated = "")
+ .dropLast(1)
+ else -> domain
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
new file mode 100644
index 0000000000..85986e5a4f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
@@ -0,0 +1,158 @@
+/* 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.lib.publicsuffixlist
+
+import mozilla.components.lib.publicsuffixlist.ext.binarySearch
+import java.net.IDN
+
+/**
+ * Class wrapping the public suffix list data and offering methods for accessing rules in it.
+ */
+internal class PublicSuffixListData(
+ private val rules: ByteArray,
+ private val exceptions: ByteArray,
+) {
+ private fun binarySearchRules(labels: List<ByteArray>, labelIndex: Int): String? {
+ return rules.binarySearch(labels, labelIndex)
+ }
+
+ private fun binarySearchExceptions(labels: List<ByteArray>, labelIndex: Int): String? {
+ return exceptions.binarySearch(labels, labelIndex)
+ }
+
+ @Suppress("ReturnCount")
+ fun getPublicSuffixOffset(domain: String): PublicSuffixOffset? {
+ if (domain.isEmpty()) {
+ return null
+ }
+
+ val domainLabels = IDN.toUnicode(domain).split('.')
+ if (domainLabels.find { it.isEmpty() } != null) {
+ // At least one of the labels is empty: Bail out.
+ return null
+ }
+
+ val rule = findMatchingRule(domainLabels)
+
+ if (domainLabels.size == rule.size && rule[0][0] != PublicSuffixListData.EXCEPTION_MARKER) {
+ // The domain is a public suffix.
+ return if (rule == PublicSuffixListData.PREVAILING_RULE) {
+ PublicSuffixOffset.PrevailingRule
+ } else {
+ PublicSuffixOffset.PublicSuffix
+ }
+ }
+
+ return if (rule[0][0] == PublicSuffixListData.EXCEPTION_MARKER) {
+ // Exception rules hold the effective TLD plus one.
+ PublicSuffixOffset.Offset(domainLabels.size - rule.size)
+ } else {
+ // Otherwise the rule is for a public suffix, so we must take one more label.
+ PublicSuffixOffset.Offset(domainLabels.size - (rule.size + 1))
+ }
+ }
+
+ /**
+ * Find a matching rule for the given domain labels.
+ *
+ * This algorithm is based on OkHttp's PublicSuffixDatabase class:
+ * https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java
+ */
+ private fun findMatchingRule(domainLabels: List<String>): List<String> {
+ // Break apart the domain into UTF-8 labels, i.e. foo.bar.com turns into [foo, bar, com].
+ val domainLabelsBytes = domainLabels.map { it.toByteArray(Charsets.UTF_8) }
+
+ val exactMatch = findExactMatch(domainLabelsBytes)
+ val wildcardMatch = findWildcardMatch(domainLabelsBytes)
+ val exceptionMatch = findExceptionMatch(domainLabelsBytes, wildcardMatch)
+
+ if (exceptionMatch != null) {
+ return ("${PublicSuffixListData.EXCEPTION_MARKER}$exceptionMatch").split('.')
+ }
+
+ if (exactMatch == null && wildcardMatch == null) {
+ return PublicSuffixListData.PREVAILING_RULE
+ }
+
+ val exactRuleLabels = exactMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE
+ val wildcardRuleLabels = wildcardMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE
+
+ return if (exactRuleLabels.size > wildcardRuleLabels.size) {
+ exactRuleLabels
+ } else {
+ wildcardRuleLabels
+ }
+ }
+
+ /**
+ * Returns an exact match or null.
+ */
+ private fun findExactMatch(labels: List<ByteArray>): String? {
+ // Start by looking for exact matches. We start at the leftmost label. For example, foo.bar.com
+ // will look like: [foo, bar, com], [bar, com], [com]. The longest matching rule wins.
+
+ for (i in 0 until labels.size) {
+ val rule = binarySearchRules(labels, i)
+
+ if (rule != null) {
+ return rule
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Returns a wildcard match or null.
+ */
+ private fun findWildcardMatch(labels: List<ByteArray>): String? {
+ // In theory, wildcard rules are not restricted to having the wildcard in the leftmost position.
+ // In practice, wildcards are always in the leftmost position. For now, this implementation
+ // cheats and does not attempt every possible permutation. Instead, it only considers wildcards
+ // in the leftmost position. We assert this fact when we generate the public suffix file. If
+ // this assertion ever fails we'll need to refactor this implementation.
+ if (labels.size > 1) {
+ val labelsWithWildcard = labels.toMutableList()
+ for (labelIndex in 0 until labelsWithWildcard.size) {
+ labelsWithWildcard[labelIndex] = PublicSuffixListData.WILDCARD_LABEL
+ val rule = binarySearchRules(labelsWithWildcard, labelIndex)
+ if (rule != null) {
+ return rule
+ }
+ }
+ }
+
+ return null
+ }
+
+ private fun findExceptionMatch(labels: List<ByteArray>, wildcardMatch: String?): String? {
+ // Exception rules only apply to wildcard rules, so only try it if we matched a wildcard.
+ if (wildcardMatch == null) {
+ return null
+ }
+
+ for (labelIndex in 0 until labels.size) {
+ val rule = binarySearchExceptions(labels, labelIndex)
+ if (rule != null) {
+ return rule
+ }
+ }
+
+ return null
+ }
+
+ companion object {
+ val WILDCARD_LABEL = byteArrayOf('*'.code.toByte())
+ val PREVAILING_RULE = listOf("*")
+ val EMPTY_RULE = listOf<String>()
+ const val EXCEPTION_MARKER = '!'
+ }
+}
+
+internal sealed class PublicSuffixOffset {
+ data class Offset(val value: Int) : PublicSuffixOffset()
+ object PublicSuffix : PublicSuffixOffset()
+ object PrevailingRule : PublicSuffixOffset()
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt
new file mode 100644
index 0000000000..d9afcbe7b0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt
@@ -0,0 +1,50 @@
+/* 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.lib.publicsuffixlist
+
+import android.content.Context
+import java.io.BufferedInputStream
+import java.io.IOException
+
+private const val PUBLIC_SUFFIX_LIST_FILE = "publicsuffixes"
+
+internal object PublicSuffixListLoader {
+ fun load(context: Context): PublicSuffixListData = context.assets.open(
+ PUBLIC_SUFFIX_LIST_FILE,
+ ).buffered().use { stream ->
+ val publicSuffixSize = stream.readInt()
+ val publicSuffixBytes = stream.readFully(publicSuffixSize)
+
+ val exceptionSize = stream.readInt()
+ val exceptionBytes = stream.readFully(exceptionSize)
+
+ PublicSuffixListData(publicSuffixBytes, exceptionBytes)
+ }
+}
+
+@Suppress("MagicNumber")
+private fun BufferedInputStream.readInt(): Int {
+ return (
+ read() and 0xff shl 24
+ or (read() and 0xff shl 16)
+ or (read() and 0xff shl 8)
+ or (read() and 0xff)
+ )
+}
+
+private fun BufferedInputStream.readFully(size: Int): ByteArray {
+ val bytes = ByteArray(size)
+
+ var offset = 0
+ while (offset < size) {
+ val read = read(bytes, offset, size - offset)
+ if (read == -1) {
+ throw IOException("Unexpected end of stream")
+ }
+ offset += read
+ }
+
+ return bytes
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
new file mode 100644
index 0000000000..c0a215ebe0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
@@ -0,0 +1,122 @@
+/* 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.lib.publicsuffixlist.ext
+
+import kotlin.experimental.and
+
+private const val BITMASK = 0xff.toByte()
+
+/**
+ * Performs a binary search for the provided [labels] on the [ByteArray]'s data.
+ *
+ * This algorithm is based on OkHttp's PublicSuffixDatabase class:
+ * https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java
+ */
+@Suppress("ComplexMethod", "NestedBlockDepth")
+internal fun ByteArray.binarySearch(labels: List<ByteArray>, labelIndex: Int): String? {
+ var low = 0
+ var high = size
+ var match: String? = null
+
+ while (low < high) {
+ val mid = (low + high) / 2
+ val start = findStartOfLineFromIndex(mid)
+ val end = findEndOfLineFromIndex(start)
+
+ val publicSuffixLength = start + end - start
+
+ var compareResult: Int
+ var currentLabelIndex = labelIndex
+ var currentLabelByteIndex = 0
+ var publicSuffixByteIndex = 0
+
+ var expectDot = false
+ while (true) {
+ val byte0 = if (expectDot) {
+ expectDot = false
+ '.'.code.toByte()
+ } else {
+ labels[currentLabelIndex][currentLabelByteIndex] and BITMASK
+ }
+
+ val byte1 = this[start + publicSuffixByteIndex] and BITMASK
+
+ // Compare the bytes. Note that the file stores UTF-8 encoded bytes, so we must compare the
+ // unsigned bytes.
+ @Suppress("EXPERIMENTAL_API_USAGE")
+ compareResult = (byte0.toUByte() - byte1.toUByte()).toInt()
+ if (compareResult != 0) {
+ break
+ }
+
+ publicSuffixByteIndex++
+ currentLabelByteIndex++
+
+ if (publicSuffixByteIndex == publicSuffixLength) {
+ break
+ }
+
+ if (labels[currentLabelIndex].size == currentLabelByteIndex) {
+ // We've exhausted our current label. Either there are more labels to compare, in which
+ // case we expect a dot as the next character. Otherwise, we've checked all our labels.
+ if (currentLabelIndex == labels.size - 1) {
+ break
+ } else {
+ currentLabelIndex++
+ currentLabelByteIndex = -1
+ expectDot = true
+ }
+ }
+ }
+
+ if (compareResult < 0) {
+ high = start - 1
+ } else if (compareResult > 0) {
+ low = start + end + 1
+ } else {
+ // We found a match, but are the lengths equal?
+ val publicSuffixBytesLeft = publicSuffixLength - publicSuffixByteIndex
+ var labelBytesLeft = labels[currentLabelIndex].size - currentLabelByteIndex
+ for (i in currentLabelIndex + 1 until labels.size) {
+ labelBytesLeft += labels[i].size
+ }
+
+ if (labelBytesLeft < publicSuffixBytesLeft) {
+ high = start - 1
+ } else if (labelBytesLeft > publicSuffixBytesLeft) {
+ low = start + end + 1
+ } else {
+ // Found a match.
+ match = String(this, start, publicSuffixLength, Charsets.UTF_8)
+ break
+ }
+ }
+ }
+
+ return match
+}
+
+/**
+ * Search for a '\n' that marks the start of a value. Don't go back past the start of the array.
+ */
+private fun ByteArray.findStartOfLineFromIndex(start: Int): Int {
+ var index = start
+ while (index > -1 && this[index] != '\n'.code.toByte()) {
+ index--
+ }
+ index++
+ return index
+}
+
+/**
+ * Search for a '\n' that marks the end of a value.
+ */
+private fun ByteArray.findEndOfLineFromIndex(start: Int): Int {
+ var end = 1
+ while (this[start + end] != '\n'.code.toByte()) {
+ end++
+ }
+ return end
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt
new file mode 100644
index 0000000000..86ffd43ac9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt
@@ -0,0 +1,482 @@
+/* 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.lib.publicsuffixlist
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class PublicSuffixListTest {
+
+ private val publicSuffixList
+ get() = PublicSuffixList(testContext)
+
+ @Test
+ fun `Verify getPublicSuffixPlusOne for known domains`() = runTest {
+ assertEquals(
+ "mozilla.org",
+ publicSuffixList.getPublicSuffixPlusOne("www.mozilla.org").await(),
+ )
+
+ assertEquals(
+ "google.com",
+ publicSuffixList.getPublicSuffixPlusOne("google.com").await(),
+ )
+
+ assertEquals(
+ "foobar.blogspot.com",
+ publicSuffixList.getPublicSuffixPlusOne("foobar.blogspot.com").await(),
+ )
+
+ assertEquals(
+ "independent.co.uk",
+ publicSuffixList.getPublicSuffixPlusOne("independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "independent.co.uk",
+ publicSuffixList.getPublicSuffixPlusOne("www.independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "biz.com.ua",
+ publicSuffixList.getPublicSuffixPlusOne("www.biz.com.ua").await(),
+ )
+
+ assertEquals(
+ "example.org",
+ publicSuffixList.getPublicSuffixPlusOne("example.org").await(),
+ )
+
+ assertEquals(
+ "example.pvt.k12.ma.us",
+ publicSuffixList.getPublicSuffixPlusOne("www.example.pvt.k12.ma.us").await(),
+ )
+
+ assertEquals(
+ "δπθ.gr",
+ publicSuffixList.getPublicSuffixPlusOne("www.ουτοπία.δπθ.gr").await(),
+ )
+ }
+
+ @Test
+ fun `Verify getPublicSuffix for known domains`() = runTest {
+ assertEquals(
+ "org",
+ publicSuffixList.getPublicSuffix("www.mozilla.org").await(),
+ )
+
+ assertEquals(
+ "com",
+ publicSuffixList.getPublicSuffix("google.com").await(),
+ )
+
+ assertEquals(
+ "blogspot.com",
+ publicSuffixList.getPublicSuffix("foobar.blogspot.com").await(),
+ )
+
+ assertEquals(
+ "co.uk",
+ publicSuffixList.getPublicSuffix("independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "co.uk",
+ publicSuffixList.getPublicSuffix("www.independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "com.ua",
+ publicSuffixList.getPublicSuffix("www.biz.com.ua").await(),
+ )
+
+ assertEquals(
+ "org",
+ publicSuffixList.getPublicSuffix("example.org").await(),
+ )
+
+ assertEquals(
+ "pvt.k12.ma.us",
+ publicSuffixList.getPublicSuffix("www.example.pvt.k12.ma.us").await(),
+ )
+
+ assertEquals(
+ "gr",
+ publicSuffixList.getPublicSuffix("www.ουτοπία.δπθ.gr").await(),
+ )
+ }
+
+ @Test
+ fun `Verify stripPublicSuffix for known domains`() = runTest {
+ assertEquals(
+ "www.mozilla",
+ publicSuffixList.stripPublicSuffix("www.mozilla.org").await(),
+ )
+
+ assertEquals(
+ "google",
+ publicSuffixList.stripPublicSuffix("google.com").await(),
+ )
+
+ assertEquals(
+ "foobar",
+ publicSuffixList.stripPublicSuffix("foobar.blogspot.com").await(),
+ )
+
+ assertEquals(
+ "independent",
+ publicSuffixList.stripPublicSuffix("independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "www.independent",
+ publicSuffixList.stripPublicSuffix("www.independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "www.biz",
+ publicSuffixList.stripPublicSuffix("www.biz.com.ua").await(),
+ )
+
+ assertEquals(
+ "example",
+ publicSuffixList.stripPublicSuffix("example.org").await(),
+ )
+
+ assertEquals(
+ "www.example",
+ publicSuffixList.stripPublicSuffix("www.example.pvt.k12.ma.us").await(),
+ )
+
+ assertEquals(
+ "www.ουτοπία.δπθ",
+ publicSuffixList.stripPublicSuffix("www.ουτοπία.δπθ.gr").await(),
+ )
+ }
+
+ /**
+ * Short set of test data from:
+ * https://raw.githubusercontent.com/publicsuffix/list/master/tests/test_psl.txt
+ */
+ @Test
+ fun `Verify getPublicSuffixPlusOne against official test data`() = runTest {
+ // empty input
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("").await())
+
+ // Mixed case.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("COM").await())
+ assertEquals(
+ "example.COM",
+ publicSuffixList.getPublicSuffixPlusOne("example.COM").await(),
+ )
+ assertEquals(
+ "eXample.COM",
+ publicSuffixList.getPublicSuffixPlusOne("WwW.eXample.COM").await(),
+ )
+
+ // Leading dot.
+ // ArrayIndexOutOfBoundsException: assertEquals("", publicSuffixList.getPublicSuffixPlusOne(".example.com").await())
+
+ // TLD with only 1 rule.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("biz").await())
+ assertEquals(
+ "domain.biz",
+ publicSuffixList.getPublicSuffixPlusOne("domain.biz").await(),
+ )
+ assertEquals(
+ "domain.biz",
+ publicSuffixList.getPublicSuffixPlusOne("b.domain.biz").await(),
+ )
+ assertEquals(
+ "domain.biz",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.domain.biz").await(),
+ )
+
+ // TLD with some 2-level rules.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("com").await())
+ assertEquals(
+ "example.com",
+ publicSuffixList.getPublicSuffixPlusOne("example.com").await(),
+ )
+ assertEquals(
+ "example.com",
+ publicSuffixList.getPublicSuffixPlusOne("b.example.com").await(),
+ )
+ assertEquals(
+ "example.com",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.example.com").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("uk.com").await())
+ assertEquals(
+ "example.uk.com",
+ publicSuffixList.getPublicSuffixPlusOne("example.uk.com").await(),
+ )
+ assertEquals(
+ "example.uk.com",
+ publicSuffixList.getPublicSuffixPlusOne("b.example.uk.com").await(),
+ )
+ assertEquals(
+ "example.uk.com",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.example.uk.com").await(),
+ )
+ assertEquals(
+ "test.ac",
+ publicSuffixList.getPublicSuffixPlusOne("test.ac").await(),
+ )
+
+ // TLD with only 1 (wildcard) rule.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("mm").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("c.mm").await())
+ assertEquals(
+ "b.c.mm",
+ publicSuffixList.getPublicSuffixPlusOne("b.c.mm").await(),
+ )
+ assertEquals(
+ "b.c.mm",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.c.mm").await(),
+ )
+
+ // More complex TLD.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("jp").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("ac.jp").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("kyoto.jp").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("ide.kyoto.jp").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("c.kobe.jp").await())
+ assertEquals(
+ "test.jp",
+ publicSuffixList.getPublicSuffixPlusOne("test.jp").await(),
+ )
+ assertEquals(
+ "test.jp",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.jp").await(),
+ )
+ assertEquals(
+ "test.ac.jp",
+ publicSuffixList.getPublicSuffixPlusOne("test.ac.jp").await(),
+ )
+ assertEquals(
+ "test.ac.jp",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.ac.jp").await(),
+ )
+ assertEquals(
+ "test.kyoto.jp",
+ publicSuffixList.getPublicSuffixPlusOne("test.kyoto.jp").await(),
+ )
+ assertEquals(
+ "b.ide.kyoto.jp",
+ publicSuffixList.getPublicSuffixPlusOne("b.ide.kyoto.jp").await(),
+ )
+ assertEquals(
+ "b.ide.kyoto.jp",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.ide.kyoto.jp").await(),
+ )
+ assertEquals(
+ "b.c.kobe.jp",
+ publicSuffixList.getPublicSuffixPlusOne("b.c.kobe.jp").await(),
+ )
+ assertEquals(
+ "b.c.kobe.jp",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.c.kobe.jp").await(),
+ )
+ assertEquals(
+ "city.kobe.jp",
+ publicSuffixList.getPublicSuffixPlusOne("city.kobe.jp").await(),
+ )
+ assertEquals(
+ "city.kobe.jp",
+ publicSuffixList.getPublicSuffixPlusOne("www.city.kobe.jp").await(),
+ )
+
+ // TLD with a wildcard rule and exceptions.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("ck").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("test.ck").await())
+ assertEquals(
+ "b.test.ck",
+ publicSuffixList.getPublicSuffixPlusOne("b.test.ck").await(),
+ )
+ assertEquals(
+ "b.test.ck",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.test.ck").await(),
+ )
+ assertEquals(
+ "www.ck",
+ publicSuffixList.getPublicSuffixPlusOne("www.ck").await(),
+ )
+ assertEquals(
+ "www.ck",
+ publicSuffixList.getPublicSuffixPlusOne("www.www.ck").await(),
+ )
+
+ // US K12.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("us").await())
+ assertEquals(
+ "test.us",
+ publicSuffixList.getPublicSuffixPlusOne("test.us").await(),
+ )
+ assertEquals(
+ "test.us",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.us").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("ak.us").await())
+ assertEquals(
+ "test.ak.us",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.ak.us").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("k12.ak.us").await())
+ assertEquals(
+ "test.k12.ak.us",
+ publicSuffixList.getPublicSuffixPlusOne("test.k12.ak.us").await(),
+ )
+ assertEquals(
+ "test.k12.ak.us",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.k12.ak.us").await(),
+ )
+
+ // IDN labels.
+ assertEquals(
+ "食狮.com.cn",
+ publicSuffixList.getPublicSuffixPlusOne("食狮.com.cn").await(),
+ )
+ // https://github.com/mozilla-mobile/android-components/issues/1777
+ assertEquals(
+ "食狮.公司.cn",
+ publicSuffixList.getPublicSuffixPlusOne("食狮.公司.cn").await(),
+ )
+ assertEquals(
+ "食狮.公司.cn",
+ publicSuffixList.getPublicSuffixPlusOne("www.食狮.公司.cn").await(),
+ )
+ assertEquals(
+ "shishi.公司.cn",
+ publicSuffixList.getPublicSuffixPlusOne("shishi.公司.cn").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("公司.cn").await())
+ assertEquals(
+ "食狮.中国",
+ publicSuffixList.getPublicSuffixPlusOne("食狮.中国").await(),
+ )
+ assertEquals(
+ "食狮.中国",
+ publicSuffixList.getPublicSuffixPlusOne("www.食狮.中国").await(),
+ )
+ assertEquals(
+ "shishi.中国",
+ publicSuffixList.getPublicSuffixPlusOne("shishi.中国").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("中国").await())
+
+ // Same as above, but punycoded.
+ assertEquals(
+ "xn--85x722f.com.cn",
+ publicSuffixList.getPublicSuffixPlusOne("xn--85x722f.com.cn").await(),
+ )
+ // https://github.com/mozilla-mobile/android-components/issues/1777
+ assertEquals(
+ "xn--85x722f.xn--55qx5d.cn",
+ publicSuffixList.getPublicSuffixPlusOne("xn--85x722f.xn--55qx5d.cn").await(),
+ )
+ assertEquals(
+ "xn--85x722f.xn--55qx5d.cn",
+ publicSuffixList.getPublicSuffixPlusOne("www.xn--85x722f.xn--55qx5d.cn").await(),
+ )
+ assertEquals(
+ "shishi.xn--55qx5d.cn",
+ publicSuffixList.getPublicSuffixPlusOne("shishi.xn--55qx5d.cn").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("xn--55qx5d.cn").await())
+ assertEquals(
+ "xn--85x722f.xn--fiqs8s",
+ publicSuffixList.getPublicSuffixPlusOne("xn--85x722f.xn--fiqs8s").await(),
+ )
+ assertEquals(
+ "xn--85x722f.xn--fiqs8s",
+ publicSuffixList.getPublicSuffixPlusOne("www.xn--85x722f.xn--fiqs8s").await(),
+ )
+ assertEquals(
+ "shishi.xn--fiqs8s",
+ publicSuffixList.getPublicSuffixPlusOne("shishi.xn--fiqs8s").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("xn--fiqs8s").await())
+ }
+
+ @Test
+ fun `Accessing with and without prefetch`() = runTest {
+ run {
+ val publicSuffixList = PublicSuffixList(testContext)
+ assertEquals("org", publicSuffixList.getPublicSuffix("mozilla.org").await())
+ }
+
+ run {
+ val publicSuffixList = PublicSuffixList(testContext).apply {
+ prefetch().await()
+ }
+ assertEquals("org", publicSuffixList.getPublicSuffix("mozilla.org").await())
+ }
+ }
+
+ @Test
+ fun `Verify isPublicSuffix with known and unknown suffixes`() = runTest {
+ assertTrue(publicSuffixList.isPublicSuffix("org").await())
+ assertTrue(publicSuffixList.isPublicSuffix("com").await())
+ assertTrue(publicSuffixList.isPublicSuffix("us").await())
+ assertTrue(publicSuffixList.isPublicSuffix("de").await())
+ assertTrue(publicSuffixList.isPublicSuffix("de.com").await())
+ assertTrue(publicSuffixList.isPublicSuffix("co.uk").await())
+ assertTrue(publicSuffixList.isPublicSuffix("taxi.br").await())
+ assertTrue(publicSuffixList.isPublicSuffix("edu.cw").await())
+ assertTrue(publicSuffixList.isPublicSuffix("chirurgiens-dentistes.fr").await())
+ assertTrue(publicSuffixList.isPublicSuffix("trani-andria-barletta.it").await())
+ assertTrue(publicSuffixList.isPublicSuffix("yabuki.fukushima.jp").await())
+ assertTrue(publicSuffixList.isPublicSuffix("research.museum").await())
+ assertTrue(publicSuffixList.isPublicSuffix("lamborghini").await())
+ assertTrue(publicSuffixList.isPublicSuffix("reisen").await())
+ assertTrue(publicSuffixList.isPublicSuffix("github.io").await())
+
+ assertFalse(publicSuffixList.isPublicSuffix("").await())
+ assertFalse(publicSuffixList.isPublicSuffix("mozilla").await())
+ assertFalse(publicSuffixList.isPublicSuffix("mozilla.org").await())
+ assertFalse(publicSuffixList.isPublicSuffix("ork").await())
+ assertFalse(publicSuffixList.isPublicSuffix("us.com.uk").await())
+ }
+
+ /**
+ * Test cases inspired by Guava tests:
+ * https://github.com/google/guava/blob/master/guava-tests/test/com/google/common/net/InternetDomainNameTest.java
+ */
+ @Test
+ fun `Verify getPublicSuffix can handle obscure and invalid input`() = runTest {
+ assertEquals("cOM", publicSuffixList.getPublicSuffix("f-_-o.cOM").await())
+ assertEquals("com", publicSuffixList.getPublicSuffix("f11-1.com").await())
+ assertNull(publicSuffixList.getPublicSuffix("www").await())
+ assertEquals("a23", publicSuffixList.getPublicSuffix("abc.a23").await())
+ assertEquals("com", publicSuffixList.getPublicSuffix("a\u0394b.com").await())
+ assertNull(publicSuffixList.getPublicSuffix("").await())
+ assertNull(publicSuffixList.getPublicSuffix(" ").await())
+ assertNull(publicSuffixList.getPublicSuffix(".").await())
+ assertNull(publicSuffixList.getPublicSuffix("..").await())
+ assertNull(publicSuffixList.getPublicSuffix("...").await())
+ assertNull(publicSuffixList.getPublicSuffix("woo.com.").await())
+ assertNull(publicSuffixList.getPublicSuffix("::1").await())
+ assertNull(publicSuffixList.getPublicSuffix("13").await())
+
+ // The following input returns an empty string which does not seem correct:
+ // https://github.com/mozilla-mobile/android-components/issues/3541
+ assertEquals("", publicSuffixList.getPublicSuffix("foo.net.us\uFF61ocm").await())
+
+ // Technically that may be correct; but it doesn't make sense to return part of an IP as public suffix:
+ // https://github.com/mozilla-mobile/android-components/issues/3540
+ assertEquals("1", publicSuffixList.getPublicSuffix("127.0.0.1").await())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/push-firebase/README.md b/mobile/android/android-components/components/lib/push-firebase/README.md
new file mode 100644
index 0000000000..f4e00ce8cc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/README.md
@@ -0,0 +1,59 @@
+# [Android Components](../../../README.md) > Libraries > Push-Firebase
+
+A [concept-push](../../concept/push/README.md) implementation using [Firebase Cloud Messaging](https://firebase.google.com/products/cloud-messaging/) (FCM).
+
+This implementation of `concept-push` uses [Firebase Cloud Messaging](https://firebase.google.com/products/cloud-messaging/). It can be used by Android devices that are supposed by Google Play Services.
+
+## Usage
+
+Add the push service for providing the encrypted messages:
+
+```kotlin
+class FirebasePush : AbstractFirebasePushService()
+```
+
+Expose the service in the `AndroidManifest.xml`:
+```xml
+<service android:name=".push.FirebasePush">
+ <intent-filter>
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
+ </intent-filter>
+</service>
+```
+
+The service can be started/stopped directly if required:
+```kotlin
+val service = FirebasePush()
+
+serivce.start()
+serivce.stop()
+```
+
+See `feature-push` for more details on how to use the service with Autopush.
+
+### 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:lib-push-firebase:{latest-version}"
+```
+
+### Adding Firebase Support
+
+Extend `AbstractFirebasePushService` with your own class:
+```kotlin
+class FirebasePush : AbstractFirebasePushService()
+```
+
+Place your keys file (`google-services.json`) for FCM in the app module of the project.
+
+Optionally, add meta tags to your `AndroidManifest.xml` to disable the push service from automatically starting.
+
+See the [concept-push documentation](../../concept/push/README.md) for generic examples of using the API of components implementing `concept-push`.
+
+## 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/lib/push-firebase/build.gradle b/mobile/android/android-components/components/lib/push-firebase/build.gradle
new file mode 100644
index 0000000000..c8bc41debe
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/build.gradle
@@ -0,0 +1,42 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.lib.push.firebase'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation project(':concept-push')
+ implementation project(':support-base')
+
+ api ComponentsDependencies.firebase_messaging
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.testing_junit
+ 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/lib/push-firebase/proguard-rules.pro b/mobile/android/android-components/components/lib/push-firebase/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/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/lib/push-firebase/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/push-firebase/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/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/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt b/mobile/android/android-components/components/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt
new file mode 100644
index 0000000000..e378f199a3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt
@@ -0,0 +1,103 @@
+/* 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.lib.push.firebase
+
+import android.content.Context
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.google.android.gms.common.util.VisibleForTesting
+import com.google.firebase.FirebaseApp
+import com.google.firebase.messaging.FirebaseMessaging
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.concept.push.PushError
+import mozilla.components.concept.push.PushProcessor
+import mozilla.components.concept.push.PushService
+import mozilla.components.concept.push.PushService.Companion.MESSAGE_KEY_CHANNEL_ID
+import mozilla.components.support.base.log.logger.Logger
+import java.io.IOException
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * A Firebase Cloud Messaging implementation of the [PushService] for Android devices that support Google Play Services.
+ */
+abstract class AbstractFirebasePushService(
+ internal val coroutineContext: CoroutineContext = Dispatchers.IO,
+) : FirebaseMessagingService(), PushService {
+
+ private val logger = Logger("AbstractFirebasePushService")
+
+ @VisibleForTesting
+ internal val googleApiAvailability: GoogleApiAvailability
+ get() = GoogleApiAvailability.getInstance()
+
+ /**
+ * Initializes Firebase and starts the messaging service if not already started and enables auto-start as well.
+ */
+ override fun start(context: Context) {
+ logger.info("start")
+ FirebaseApp.initializeApp(context)
+ }
+
+ override fun onNewToken(newToken: String) {
+ logger.info("Got new Firebase token: $newToken")
+ PushProcessor.requireInstance.onNewToken(newToken)
+ }
+
+ @SuppressWarnings("TooGenericExceptionCaught")
+ override fun onMessageReceived(message: RemoteMessage) {
+ logger.info("onMessageReceived")
+ // This is not an AutoPush message we can handle.
+ val chId = message.data.getOrElse(MESSAGE_KEY_CHANNEL_ID) { null }
+
+ if (chId == null) {
+ logger.info("Missing $MESSAGE_KEY_CHANNEL_ID key, skipping this message")
+ return
+ } else {
+ logger.info("Processing message with chId: $chId")
+ }
+
+ // In case of any errors, let the PushProcessor handle this exception. Instead of crashing
+ // here, just drop the message on the floor. This is fine, since we don't really need to
+ // "recover" from a bad incoming message.
+ // PushProcessor will submit relevant issues via a CrashReporter as appropriate.
+ try {
+ PushProcessor.requireInstance.onMessageReceived(message.data)
+ } catch (e: IllegalStateException) {
+ // Re-throw 'requireInstance' exceptions.
+ throw (e)
+ } catch (e: Exception) {
+ PushProcessor.requireInstance.onError(PushError.Rust(e))
+ }
+ }
+
+ /**
+ * Stops the Firebase messaging service and disables auto-start.
+ */
+ final override fun stop() {
+ stopSelf()
+ }
+
+ /**
+ * Removes the Firebase instance ID. This would lead a new token being generated when the
+ * service hits the Firebase servers.
+ */
+ override fun deleteToken() {
+ CoroutineScope(coroutineContext).launch {
+ try {
+ FirebaseMessaging.getInstance().deleteToken()
+ } catch (e: IOException) {
+ logger.error("Force registration renewable failed.", e)
+ }
+ }
+ }
+
+ override fun isServiceAvailable(context: Context): Boolean {
+ return googleApiAvailability.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+ }
+}
diff --git a/mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt b/mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt
new file mode 100644
index 0000000000..b63cbbd6c4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt
@@ -0,0 +1,115 @@
+/* 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.lib.push.firebase
+
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.google.firebase.messaging.RemoteMessage
+import kotlinx.coroutines.Dispatchers
+import mozilla.components.concept.push.PushProcessor
+import mozilla.components.support.test.any
+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.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.`when`
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class AbstractFirebasePushServiceTest {
+
+ private val processor: PushProcessor = mock()
+ private val service = TestService()
+
+ @Before
+ fun setup() {
+ reset(processor)
+ PushProcessor.install(processor)
+ }
+
+ @Test
+ fun `onNewToken passes token to processor`() {
+ service.onNewToken("token")
+
+ verify(processor).onNewToken("token")
+ }
+
+ @Test
+ fun `new encrypted messages are passed to the processor`() {
+ val remoteMessage: RemoteMessage = mock()
+ val data = mapOf(
+ "chid" to "1234",
+ "body" to "contents",
+ "con" to "encoding",
+ "enc" to "salt",
+ "cryptokey" to "dh256",
+ )
+ `when`(remoteMessage.data).thenReturn(data)
+ service.onMessageReceived(remoteMessage)
+
+ verify(processor).onMessageReceived(data)
+ }
+
+ @Test
+ fun `malformed message exception should not be thrown`() {
+ val remoteMessage: RemoteMessage = mock()
+ val data = mapOf(
+ "chid" to "1234",
+ )
+ `when`(remoteMessage.data).thenReturn(data)
+ service.onMessageReceived(remoteMessage)
+
+ verify(processor, never()).onError(any())
+ verify(processor).onMessageReceived(data)
+ }
+
+ @Test
+ fun `do nothing if the message is not for us`() {
+ val remoteMessage: RemoteMessage = mock()
+ val data = mapOf(
+ "con" to "encoding",
+ "enc" to "salt",
+ "cryptokey" to "dh256",
+ )
+ `when`(remoteMessage.data).thenReturn(data)
+
+ service.onMessageReceived(remoteMessage)
+
+ verifyNoInteractions(processor)
+ }
+
+ @Test
+ fun `force registration should never be on Main`() {
+ // Default dispatcher isn't main
+ assertTrue(service.coroutineContext != Dispatchers.Main)
+
+ val service = object : AbstractFirebasePushService(Dispatchers.Default) {}
+ service.deleteToken()
+ }
+
+ @Test
+ fun `service available reflects Google Play Services' availability`() {
+ val service = spy(TestService())
+
+ // By default, service is unavailable.
+ assertFalse(service.isServiceAvailable(testContext))
+
+ val googleApiAvailability = mock<GoogleApiAvailability>()
+ `when`(service.googleApiAvailability).thenReturn(googleApiAvailability)
+ `when`(googleApiAvailability.isGooglePlayServicesAvailable(testContext)).thenReturn(ConnectionResult.SUCCESS)
+
+ assertTrue(service.isServiceAvailable(testContext))
+ }
+
+ class TestService : AbstractFirebasePushService()
+}
diff --git a/mobile/android/android-components/components/lib/push-firebase/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/push-firebase/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/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/lib/push-firebase/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/push-firebase/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/state/README.md b/mobile/android/android-components/components/lib/state/README.md
new file mode 100644
index 0000000000..eb8f54712a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/README.md
@@ -0,0 +1,69 @@
+# [Android Components](../../../README.md) > Libraries > State
+
+A generic library for maintaining the state of a component, screen or application.
+
+The state library is inspired by existing libraries like [Redux](https://redux.js.org/) and provides a `Store` class to hold application state.
+
+## Usage
+
+### 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:lib-state:{latest-version}"
+```
+
+### Action
+
+`Action`s represent payloads of information that send data from your application to the `Store`. You can send actions using `store.dispatch()`. An `Action` will usually be a small data class or object describing a change.
+
+```Kotlin
+data class SetVisibility(val visible: Boolean) : Action
+
+store.dispatch(SetVisibility(true))
+```
+
+### Reducer
+
+`Reducer`s are functions describing how the state should change in response to actions sent to the store.
+
+They take the previous state and an action as parameters, and return the new state as a result of that action.
+
+```Kotlin
+fun reduce(previousState: State, action: Action) = when (action) {
+ is SetVisibility -> previousState.copy(toolbarVisible = action.visible)
+ else -> previousState
+}
+```
+
+### Store
+
+The `Store` brings together actions and reducers. It holds the application state and allows access to it via the `store.state` getter. It allows state to be updated via `store.dispatch()`, and can have listeners registered through `store.observe()`.
+
+Stores can easily be created if you have a reducer.
+
+```Kotlin
+val store = Store<State, Action>(
+ initialState = State(),
+ reducer = ::reduce
+)
+```
+
+Once the store is created, you can react to changes in the state by registering an observer.
+
+```Kotlin
+store.observe(lifecycleOwner) { state ->
+ toolbarView.visibility = if (state.toolbarVisible) View.VISIBLE else View.GONE
+}
+```
+
+`store.observe` is lifecycle aware and will automatically unregister when the lifecycle owner (such as an `Activity` or `Fragment`) is destroyed. Instead of a `LifecycleOwner`, a `View` can be supplied instead.
+
+If you wish to manually control the observer subscription, you can use the `store.observeManually` function. `observeManually` returns a `Subscription` class which has an `unsubscribe` method. Calling `unsubscribe` removes the observer.
+
+## 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/lib/state/build.gradle b/mobile/android/android-components/components/lib/state/build.gradle
new file mode 100644
index 0000000000..4424fce465
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/build.gradle
@@ -0,0 +1,69 @@
+/* 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'
+
+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'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.lib.state'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += [
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+ ]
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_fragment
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_lifecycle_process
+
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+
+ testImplementation platform(ComponentsDependencies.androidx_compose_bom)
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.androidx_compose_ui_test
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation ComponentsDependencies.androidx_test_junit
+ androidTestImplementation ComponentsDependencies.androidx_compose_ui_test_manifest
+ androidTestImplementation ComponentsDependencies.androidx_compose_ui_test
+}
+
+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/lib/state/proguard-rules.pro b/mobile/android/android-components/components/lib/state/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/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/lib/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt b/mobile/android/android-components/components/lib/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt
new file mode 100644
index 0000000000..a97ceebe0d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt
@@ -0,0 +1,182 @@
+/* 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.lib.state.ext
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import kotlinx.coroutines.runBlocking
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+class ComposeExtensionsKtTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun usingInitialValue() {
+ val store = Store(
+ initialState = TestState(counter = 42),
+ reducer = ::reducer,
+ )
+
+ var value: Int? = null
+
+ rule.setContent {
+ val composeState = store.observeAsComposableState { state -> state.counter * 2 }
+ value = composeState.value
+ }
+
+ assertEquals(84, value)
+ }
+
+ @Test
+ fun receivingUpdates() {
+ val store = Store(
+ initialState = TestState(counter = 42),
+ reducer = ::reducer,
+ )
+
+ var value: Int? = null
+
+ rule.setContent {
+ val composeState = store.observeAsComposableState { state -> state.counter * 2 }
+ value = composeState.value
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+
+ rule.runOnIdle {
+ assertEquals(86, value)
+ }
+ }
+
+ @Test
+ fun usingInitialValueWithUpdates() {
+ val loading = "Loading"
+ val content = "Content"
+ val store = Store(
+ initialState = TestState(counter = 0),
+ reducer = ::reducer,
+ )
+
+ val value = mutableListOf<String>()
+
+ rule.setContent {
+ val composeState = store.observeAsState(
+ initialValue = loading,
+ map = { if (it.counter < 5) loading else content },
+ )
+ value.add(composeState.value)
+ }
+
+ rule.runOnIdle {
+ // Initial value when counter is 0.
+ assertEquals(listOf("Loading"), value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+
+ rule.runOnIdle {
+ // Value after 4 increments, aka counter is 4. Note that it doesn't recompose here
+ // as the mapped value has stayed the same. We have 1 item in the list and not 5.
+ assertEquals(listOf(loading), value)
+ }
+
+ // 5th increment
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+
+ rule.runOnIdle {
+ assertEquals(listOf(loading, content), value)
+ assertEquals(content, value.last())
+ }
+ }
+
+ @Test
+ fun receivingUpdatesForPartialStateUpdateOnly() {
+ val store = Store(
+ initialState = TestState(counter = 42),
+ reducer = ::reducer,
+ )
+
+ var value: Int? = null
+
+ rule.setContent {
+ val composeState = store.observeAsComposableState(
+ map = { state -> state.counter * 2 },
+ observe = { state -> state.text },
+ )
+ value = composeState.value
+ }
+
+ assertEquals(84, value)
+
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+
+ rule.runOnIdle {
+ // State value didn't change because value returned by `observer` function did not change
+ assertEquals(84, value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.SetTextAction("Hello World"))
+
+ rule.runOnIdle {
+ // Now, after the value from the observer function changed, we are seeing the new value
+ assertEquals(86, value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.SetValueAction(23))
+
+ rule.runOnIdle {
+ // Observer function result is the same, no state update
+ assertEquals(86, value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.SetTextAction("Hello World"))
+
+ rule.runOnIdle {
+ // Text was updated to the same value, observer function result is the same, no state update
+ assertEquals(86, value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.SetTextAction("Hello World Again"))
+
+ rule.runOnIdle {
+ // Now, after the value from the observer function changed, we are seeing the new value
+ assertEquals(46, value)
+ }
+ }
+
+ private fun Store<TestState, TestAction>.dispatchBlockingOnIdle(action: TestAction) {
+ rule.runOnIdle {
+ val job = dispatch(action)
+ runBlocking { job.join() }
+ }
+ }
+}
+
+fun reducer(state: TestState, action: TestAction): TestState = when (action) {
+ is TestAction.IncrementAction -> state.copy(counter = state.counter + 1)
+ is TestAction.DecrementAction -> state.copy(counter = state.counter - 1)
+ is TestAction.SetValueAction -> state.copy(counter = action.value)
+ is TestAction.SetTextAction -> state.copy(text = action.text)
+}
+
+data class TestState(
+ val counter: Int,
+ val text: String = "",
+) : State
+
+sealed class TestAction : Action {
+ object IncrementAction : TestAction()
+ object DecrementAction : TestAction()
+ data class SetValueAction(val value: Int) : TestAction()
+ data class SetTextAction(val text: String) : TestAction()
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..aa9d1077cc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml
@@ -0,0 +1,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/. -->
+<manifest>
+
+ <application />
+
+</manifest>
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt
new file mode 100644
index 0000000000..e371ac8929
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt
@@ -0,0 +1,14 @@
+/* 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.lib.state
+
+/**
+ * Generic interface for actions to be dispatched on a [Store].
+ *
+ * Actions are used to send data from the application to a [Store]. The [Store] will use the [Action] to
+ * derive a new [State]. Actions should describe what happened, while [Reducer]s will describe how the
+ * state changes.
+ */
+interface Action
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt
new file mode 100644
index 0000000000..18f48b8483
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt
@@ -0,0 +1,23 @@
+/* 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.lib.state
+
+/**
+ *
+ * Marks an [Action] in the [Store] that are **delicate** &mdash;
+ * they have limited use-case and shall ve used with care in general code.
+ * Any use of a delicate declaration has to be carefully reviewed to make sure it is
+ * properly used and is not used for non-debugging or testing purposes.
+ * Carefully read documentation of any declaration marked as `DelicateAction`.
+ */
+@MustBeDocumented
+@Retention(value = AnnotationRetention.BINARY)
+@RequiresOptIn(
+ level = RequiresOptIn.Level.WARNING,
+ message = "This is a delicate Action and should only be used for situations that require debugging or testing." +
+ " Make sure you fully read and understand documentation of the action that is marked as a delicate Action.",
+)
+@Target(AnnotationTarget.CLASS)
+public annotation class DelicateAction
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt
new file mode 100644
index 0000000000..777b8cb77b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt
@@ -0,0 +1,53 @@
+/* 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.lib.state
+
+/**
+ * A [Middleware] sits between the store and the reducer. It provides an extension point between
+ * dispatching an action, and the moment it reaches the reducer.
+ *
+ * A [Middleware] can rewrite an [Action], it can intercept an [Action], dispatch additional
+ * [Action]s or perform side-effects when an [Action] gets dispatched.
+ *
+ * The [Store] will create a chain of [Middleware] instances and invoke them in order. Every
+ * [Middleware] can decide to continue the chain (by calling `next`), intercept the chain (by not
+ * invoking `next`). A [Middleware] has no knowledge of what comes before or after it in the chain.
+ */
+typealias Middleware<S, A> = (context: MiddlewareContext<S, A>, next: (A) -> Unit, action: A) -> Unit
+
+/**
+ * The context a Middleware is running in. Allows access to privileged [Store] functionality. It is
+ * passed to a [Middleware] with every [Action].
+ *
+ * Note that the [MiddlewareContext] should not be passed to other components and calling methods
+ * on non-[Store] threads may throw an exception. Instead the value of the [store] property, granting
+ * access to the underlying store, can safely be used outside of the middleware.
+ */
+interface MiddlewareContext<S : State, A : Action> {
+ /**
+ * Returns the current state of the [Store].
+ */
+ val state: S
+
+ /**
+ * Dispatches an [Action] synchronously on the [Store]. Other than calling [Store.dispatch], this
+ * will block and return after all [Store] observers have been notified about the state change.
+ * The dispatched [Action] will go through the whole chain of middleware again.
+ *
+ * This method is particular useful if a middleware wants to dispatch an additional [Action] and
+ * wait until the [state] has been updated to further process it.
+ *
+ * Note that this method should only ever be called from a [Middleware] and the calling thread.
+ * Calling it from another thread may throw an exception. For dispatching an [Action] from
+ * asynchronous code in the [Middleware] or another component use [store] which returns a
+ * reference to the underlying [Store] that offers methods for asynchronous dispatching.
+ */
+ fun dispatch(action: A)
+
+ /**
+ * Returns a reference to the [Store] the [Middleware] is running in.
+ */
+ val store: Store<S, A>
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt
new file mode 100644
index 0000000000..c5a68d8c17
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt
@@ -0,0 +1,10 @@
+/* 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.lib.state
+
+/**
+ * Listener called when the state changes in the [Store].
+ */
+typealias Observer<S> = (S) -> Unit
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt
new file mode 100644
index 0000000000..0cfcf76bb6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt
@@ -0,0 +1,13 @@
+/* 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.lib.state
+
+/**
+ * Reducers specify how the application's [State] changes in response to [Action]s sent to the [Store].
+ *
+ * Remember that actions only describe what happened, but don't describe how the application's state changes.
+ * Reducers will commonly consist of a `when` statement returning different copies of the [State].
+ */
+typealias Reducer<S, A> = (S, A) -> S
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt
new file mode 100644
index 0000000000..3318ddffe8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt
@@ -0,0 +1,10 @@
+/* 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.lib.state
+
+/**
+ * Generic interface for a [State] maintained by a [Store].
+ */
+interface State
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt
new file mode 100644
index 0000000000..025880d780
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt
@@ -0,0 +1,187 @@
+/* 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.lib.state
+
+import android.os.Handler
+import android.os.Looper
+import androidx.annotation.CheckResult
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.lib.state.internal.ReducerChainBuilder
+import mozilla.components.lib.state.internal.StoreThreadFactory
+import java.lang.ref.WeakReference
+import java.util.Collections
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executors
+
+/**
+ * A generic store holding an immutable [State].
+ *
+ * The [State] can only be modified by dispatching [Action]s which will create a new state and notify all registered
+ * [Observer]s.
+ *
+ * @param initialState The initial state until a dispatched [Action] creates a new state.
+ * @param reducer A function that gets the current [State] and [Action] passed in and will return a new [State].
+ * @param middleware Optional list of [Middleware] sitting between the [Store] and the [Reducer].
+ * @param threadNamePrefix Optional prefix with which to name threads for the [Store]. If not provided,
+ * the naming scheme will be deferred to [Executors.defaultThreadFactory]
+ */
+open class Store<S : State, A : Action>(
+ initialState: S,
+ reducer: Reducer<S, A>,
+ middleware: List<Middleware<S, A>> = emptyList(),
+ threadNamePrefix: String? = null,
+) {
+ private val threadFactory = StoreThreadFactory(threadNamePrefix)
+ private val dispatcher = Executors.newSingleThreadExecutor(threadFactory).asCoroutineDispatcher()
+ private val reducerChainBuilder = ReducerChainBuilder(threadFactory, reducer, middleware)
+ private val scope = CoroutineScope(dispatcher)
+
+ @VisibleForTesting
+ internal val subscriptions = Collections.newSetFromMap(ConcurrentHashMap<Subscription<S, A>, Boolean>())
+ private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
+ // We want exceptions in the reducer to crash the app and not get silently ignored. Therefore we rethrow the
+ // exception on the main thread.
+ Handler(Looper.getMainLooper()).postAtFrontOfQueue {
+ throw StoreException("Exception while reducing state", throwable)
+ }
+
+ // Once an exception happened we do not want to accept any further actions. So let's cancel the scope which
+ // will cancel all jobs and not accept any new ones.
+ scope.cancel()
+ }
+ private val dispatcherWithExceptionHandler = dispatcher + exceptionHandler
+
+ @Volatile private var currentState = initialState
+
+ /**
+ * The current [State].
+ */
+ val state: S
+ get() = currentState
+
+ /**
+ * Registers an [Observer] function that will be invoked whenever the [State] changes.
+ *
+ * It's the responsibility of the caller to keep track of the returned [Subscription] and call
+ * [Subscription.unsubscribe] to stop observing and avoid potentially leaking memory by keeping an unused [Observer]
+ * registered. It's is recommend to use one of the `observe` extension methods that unsubscribe automatically.
+ *
+ * The created [Subscription] is in paused state until explicitly resumed by calling [Subscription.resume].
+ * While paused the [Subscription] will not receive any state updates. Once resumed the [observer]
+ * will get invoked immediately with the latest state.
+ *
+ * @return A [Subscription] object that can be used to unsubscribe from further state changes.
+ */
+ @CheckResult(suggest = "observe")
+ @Synchronized
+ fun observeManually(observer: Observer<S>): Subscription<S, A> {
+ val subscription = Subscription(observer, store = this)
+ subscriptions.add(subscription)
+
+ return subscription
+ }
+
+ /**
+ * Dispatch an [Action] to the store in order to trigger a [State] change.
+ */
+ fun dispatch(action: A) = scope.launch(dispatcherWithExceptionHandler) {
+ synchronized(this@Store) {
+ reducerChainBuilder.get(this@Store).invoke(action)
+ }
+ }
+
+ /**
+ * Transitions from the current [State] to the passed in [state] and notifies all observers.
+ */
+ internal fun transitionTo(state: S) {
+ if (state == currentState) {
+ // Nothing has changed.
+ return
+ }
+
+ currentState = state
+ subscriptions.forEach { subscription -> subscription.dispatch(state) }
+ }
+
+ private fun removeSubscription(subscription: Subscription<S, A>) {
+ subscriptions.remove(subscription)
+ }
+
+ /**
+ * A [Subscription] is returned whenever an observer is registered via the [observeManually] method. Calling
+ * [unsubscribe] on the [Subscription] will unregister the observer.
+ */
+ class Subscription<S : State, A : Action> internal constructor(
+ internal val observer: Observer<S>,
+ store: Store<S, A>,
+ ) {
+ private val storeReference = WeakReference(store)
+ internal var binding: Binding? = null
+ private var active = false
+
+ /**
+ * Resumes the [Subscription]. The [Observer] will get notified for every state change.
+ * Additionally it will get invoked immediately with the latest state.
+ */
+ @Synchronized
+ fun resume() {
+ active = true
+
+ storeReference.get()?.state?.let(observer)
+ }
+
+ /**
+ * Pauses the [Subscription]. The [Observer] will not get notified when the state changes
+ * until [resume] is called.
+ */
+ @Synchronized
+ fun pause() {
+ active = false
+ }
+
+ /**
+ * Notifies this subscription's observer of a state change.
+ *
+ * @param state the updated state.
+ */
+ @Synchronized
+ internal fun dispatch(state: S) {
+ if (active) {
+ observer.invoke(state)
+ }
+ }
+
+ /**
+ * Unsubscribe from the [Store].
+ *
+ * Calling this method will clear all references and the subscription will not longer be
+ * active.
+ */
+ @Synchronized
+ fun unsubscribe() {
+ active = false
+
+ storeReference.get()?.removeSubscription(this)
+ storeReference.clear()
+
+ binding?.unbind()
+ }
+
+ interface Binding {
+ fun unbind()
+ }
+ }
+}
+
+/**
+ * Exception for otherwise unhandled errors caught while reducing state or
+ * while managing/notifying observers.
+ */
+class StoreException(msg: String, val e: Throwable? = null) : Exception(msg, e)
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt
new file mode 100644
index 0000000000..e4633b6239
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt
@@ -0,0 +1,147 @@
+/* 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.lib.state.ext
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import androidx.compose.runtime.State as ComposeState
+
+/**
+ * Starts observing this [Store] and represents the mapped state (using [map]) via [ComposeState].
+ *
+ * Every time the mapped [Store] state changes, the returned [ComposeState] will be updated causing
+ * recomposition of every [ComposeState.value] usage.
+ *
+ * The [Store] observer will automatically be removed when this composable disposes or the current
+ * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
+ */
+@Composable
+fun <S : State, A : Action, R> Store<S, A>.observeAsComposableState(map: (S) -> R): ComposeState<R?> {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val state = remember { mutableStateOf<R?>(map(state)) }
+
+ DisposableEffect(this, lifecycleOwner) {
+ val subscription = observe(lifecycleOwner) { browserState ->
+ state.value = map(browserState)
+ }
+ onDispose { subscription?.unsubscribe() }
+ }
+
+ return state
+}
+
+/**
+ * Starts observing this [Store] and represents the mapped state (using [map]) via [ComposeState].
+ *
+ * Every time the mapped [Store] state changes, the returned [ComposeState] will be updated causing
+ * recomposition of every [ComposeState.value] usage.
+ *
+ * The [Store] observer will automatically be removed when this composable disposes or the current
+ * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
+ *
+ * @param initialValue Initial value emitted.
+ * @param map The applied function to produced the mapped value [R] from [S].
+ * @return A non nullable [ComposeState], making the api more reasonable for callers where the
+ * state is non null.
+ */
+@Composable
+fun <S : State, A : Action, R> Store<S, A>.observeAsState(
+ initialValue: R,
+ map: (S) -> R,
+): ComposeState<R> {
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ return produceState(initialValue = initialValue) {
+ val subscription = observe(lifecycleOwner) { browserState ->
+ value = map(browserState)
+ }
+ awaitDispose { subscription?.unsubscribe() }
+ }
+}
+
+/**
+ * Starts observing this [Store] and represents the mapped state (using [map]) via [ComposeState].
+ *
+ * Everytime the [Store] state changes and the result of the [observe] function changes for this
+ * state, the returned [ComposeState] will be updated causing recomposition of every
+ * [ComposeState.value] usage.
+ *
+ * The [Store] observer will automatically be removed when this composable disposes or the current
+ * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
+ */
+@Composable
+fun <S : State, A : Action, O, R> Store<S, A>.observeAsComposableState(
+ observe: (S) -> O,
+ map: (S) -> R,
+): ComposeState<R?> {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ var lastValue = observe(state)
+ val state = remember { mutableStateOf<R?>(map(state)) }
+
+ DisposableEffect(this, lifecycleOwner) {
+ val subscription = observe(lifecycleOwner) { browserState ->
+ val newValue = observe(browserState)
+ if (newValue != lastValue) {
+ state.value = map(browserState)
+ lastValue = newValue
+ }
+ }
+ onDispose { subscription?.unsubscribe() }
+ }
+
+ return state
+}
+
+/**
+ * Helper for creating a [Store] scoped to a `@Composable` and whose [State] gets saved and restored
+ * on process recreation.
+ */
+@Composable
+inline fun <reified S : State, A : Action> composableStore(
+ crossinline save: (S) -> Parcelable = { state ->
+ if (state is Parcelable) {
+ state
+ } else {
+ throw NotImplementedError(
+ "State of store does not implement Parcelable. Either implement Parcelable or pass " +
+ "custom save function to composableStore()",
+ )
+ }
+ },
+ crossinline restore: (Parcelable) -> S = { parcelable ->
+ if (parcelable is S) {
+ parcelable
+ } else {
+ throw NotImplementedError(
+ "Restored parcelable is not of same class as state. Either the state needs to " +
+ "implement Parcelable or you need to provide a custom restore function to composableStore()",
+ )
+ }
+ },
+ crossinline init: (S?) -> Store<S, A>,
+): Store<S, A> {
+ return rememberSaveable(
+ saver = Saver(
+ save = { store -> save(store.state) },
+ restore = { parcelable ->
+ val state = restore(parcelable)
+ init(state)
+ },
+ ),
+ init = { init(null) },
+ )
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt
new file mode 100644
index 0000000000..0eacb1de04
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt
@@ -0,0 +1,105 @@
+/* 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.lib.state.ext
+
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import mozilla.components.support.ktx.android.view.toScope
+
+/**
+ * Helper extension method for consuming [State] from a [Store] sequentially in order inside a
+ * [Fragment]. The [block] function will get invoked for every [State] update.
+ *
+ * This helper will automatically stop observing the [Store] once the [View] of the [Fragment] gets
+ * detached. The fragment's lifecycle will be used to determine when to resume/pause observing the
+ * [Store].
+ */
+@MainThread
+fun <S : State, A : Action> Fragment.consumeFrom(store: Store<S, A>, block: (S) -> Unit) {
+ val fragment = this
+ val view = checkNotNull(view) { "Fragment has no view yet. Call from onViewCreated()." }
+
+ val scope = view.toScope()
+ val channel = store.channel(owner = this)
+
+ scope.launch {
+ channel.consumeEach { state ->
+ // We are using a scope that is bound to the view being attached here. It can happen
+ // that the "view detached" callback gets executed *after* the fragment was detached. If
+ // a `consumeFrom` runs in exactly this moment then we run inside a detached fragment
+ // without a `Context` and this can cause a variety of issues/crashes.
+ // See: https://github.com/mozilla-mobile/android-components/issues/4125
+ //
+ // To avoid this, we check whether the fragment still has an activity and a view
+ // attached. If not then we run in exactly that moment between fragment detach and view
+ // detach. It would be better if we could use `viewLifecycleOwner` which is bound to
+ // onCreateView() and onDestroyView() of the fragment. But:
+ // - `viewLifecycleOwner` is only available in alpha versions of AndroidX currently.
+ // - We found a bug where `viewLifecycleOwner.lifecycleScope` is not getting cancelled
+ // causing this coroutine to run forever.
+ // See: https://github.com/mozilla-mobile/android-components/issues/3828
+ // Once those two issues get resolved we can remove the `isAdded` check and use
+ // `viewLifecycleOwner.lifecycleScope` instead of the view scope.
+ //
+ // In a previous version we tried using `isAdded` and `isDetached` here. But in certain
+ // situations they reported true/false in situations where no activity was attached to
+ // the fragment. Therefore we switched to explicitly check for the activity and view here.
+ if (fragment.activity != null && fragment.view != null) {
+ block(state)
+ }
+ }
+ }
+}
+
+/**
+ * Helper extension method for consuming [State] from a [Store] as a [Flow].
+ *
+ * The lifetime of the coroutine scope the [Flow] is launched in, and [block] is executed in, is
+ * bound to the [View] of the [Fragment]. Once the [View] gets detached, the coroutine scope will
+ * automatically be cancelled and no longer observe the [Store].
+ *
+ * An optional [LifecycleOwner] can be passed to this method. It will be used to automatically pause
+ * and resume the [Store] subscription. With that an application can, for example, automatically
+ * stop updating the UI if the application is in the background. Once the [Lifecycle] switches back
+ * to at least STARTED state then the latest [State] and further will be passed to the [Flow] again.
+ * By default, the fragment itself is used as a [LifecycleOwner].
+ */
+@MainThread
+fun <S : State, A : Action> Fragment.consumeFlow(
+ from: Store<S, A>,
+ owner: LifecycleOwner? = this,
+ block: suspend (Flow<S>) -> Unit,
+) {
+ val fragment = this
+ val view = checkNotNull(view) { "Fragment has no view yet. Call from onViewCreated()." }
+
+ // It's important to create the flow here directly instead of in the coroutine below,
+ // as otherwise the fragment could be removed before the subscription is created.
+ // This would cause us to create an unnecessary subscription leaking the fragment,
+ // as we only unsubscribe on destroy which already happened.
+ val flow = from.flow(owner)
+
+ val scope = view.toScope()
+ scope.launch {
+ val filtered = flow.filter {
+ // We ignore state updates if the fragment does not have an activity or view
+ // attached anymore.
+ // See comment in [consumeFrom] above.
+ fragment.activity != null && fragment.view != null
+ }
+
+ block(filtered)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt
new file mode 100644
index 0000000000..8250bf1376
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt
@@ -0,0 +1,265 @@
+/* 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.lib.state.ext
+
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.Observer
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+
+/**
+ * Registers an [Observer] function that will be invoked whenever the state changes. The [Store.Subscription]
+ * will be bound to the passed in [LifecycleOwner]. Once the [Lifecycle] state changes to DESTROYED the [Observer] will
+ * be unregistered automatically.
+ *
+ * The [Observer] will get invoked with the current [State] as soon as the [Lifecycle] is in STARTED
+ * state.
+ */
+@MainThread
+fun <S : State, A : Action> Store<S, A>.observe(
+ owner: LifecycleOwner,
+ observer: Observer<S>,
+): Store.Subscription<S, A>? {
+ if (owner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
+ // This owner is already destroyed. No need to register.
+ return null
+ }
+
+ val subscription = observeManually(observer)
+
+ subscription.binding = SubscriptionLifecycleBinding(owner, subscription).apply {
+ owner.lifecycle.addObserver(this)
+ }
+
+ return subscription
+}
+
+/**
+ * Registers an [Observer] function that will be invoked whenever the state changes. The [Store.Subscription]
+ * will be bound to the passed in [View]. Once the [View] gets detached the [Observer] will be unregistered
+ * automatically.
+ *
+ * Note that inside a `Fragment` using [observe] with a `viewLifecycleOwner` may be a better option.
+ * Only use this implementation if you have only access to a [View] - especially if it can exist
+ * outside of a `Fragment`.
+ *
+ * The [Observer] will get invoked with the current [State] as soon as [View] is attached.
+ *
+ * Once the [View] gets detached the [Observer] will get unregistered. It will NOT get automatically
+ * registered again if the same [View] gets attached again.
+ */
+@MainThread
+fun <S : State, A : Action> Store<S, A>.observe(
+ view: View,
+ observer: Observer<S>,
+) {
+ val subscription = observeManually(observer)
+
+ subscription.binding = SubscriptionViewBinding(view, subscription).apply {
+ view.addOnAttachStateChangeListener(this)
+ }
+
+ if (view.isAttachedToWindow) {
+ // This View is already attached. We can resume immediately and do not need to wait for
+ // onViewAttachedToWindow() getting called.
+ subscription.resume()
+ }
+}
+
+/**
+ * Registers an [Observer] function that will observe the store indefinitely.
+ *
+ * Right after registering the [Observer] will be invoked with the current [State].
+ */
+fun <S : State, A : Action> Store<S, A>.observeForever(
+ observer: Observer<S>,
+) {
+ observeManually(observer).resume()
+}
+
+/**
+ * Creates a conflated [Channel] for observing [State] changes in the [Store].
+ *
+ * The advantage of a [Channel] is that [State] changes can be processed sequentially in order from
+ * a single coroutine (e.g. on the main thread).
+ *
+ * @param owner A [LifecycleOwner] that will be used to determine when to pause and resume the store
+ * subscription. When the [Lifecycle] is in STOPPED state then no [State] will be received. Once the
+ * [Lifecycle] switches back to at least STARTED state then the latest [State] and further updates
+ * will be received.
+ */
+@ExperimentalCoroutinesApi
+@MainThread
+fun <S : State, A : Action> Store<S, A>.channel(
+ owner: LifecycleOwner = ProcessLifecycleOwner.get(),
+): ReceiveChannel<S> {
+ if (owner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
+ // This owner is already destroyed. No need to register.
+ throw IllegalArgumentException("Lifecycle is already DESTROYED")
+ }
+
+ val channel = Channel<S>(Channel.CONFLATED)
+
+ val subscription = observeManually { state ->
+ runBlocking {
+ try {
+ channel.send(state)
+ } catch (e: CancellationException) {
+ // It's possible for this channel to have been closed concurrently before
+ // we had a chance to unsubscribe. In this case we can just ignore this
+ // one subscription and keep going.
+ }
+ }
+ }
+
+ subscription.binding = SubscriptionLifecycleBinding(owner, subscription).apply {
+ owner.lifecycle.addObserver(this)
+ }
+
+ channel.invokeOnClose { subscription.unsubscribe() }
+
+ return channel
+}
+
+/**
+ * Creates a [Flow] for observing [State] changes in the [Store].
+ *
+ * @param owner An optional [LifecycleOwner] that will be used to determine when to pause and resume
+ * the store subscription. When the [Lifecycle] is in STOPPED state then no [State] will be received.
+ * Once the [Lifecycle] switches back to at least STARTED state then the latest [State] and further
+ * updates will be emitted.
+ */
+@MainThread
+fun <S : State, A : Action> Store<S, A>.flow(
+ owner: LifecycleOwner? = null,
+): Flow<S> {
+ var destroyed = owner?.lifecycle?.currentState == Lifecycle.State.DESTROYED
+ val ownerDestroyedObserver = object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ destroyed = true
+ }
+ }
+ owner?.lifecycle?.addObserver(ownerDestroyedObserver)
+
+ return channelFlow {
+ // By the time this block executes the fragment or view could already be destroyed
+ // so we exit early to avoid creating an unnecessary subscription. This is important
+ // as otherwise we'd be leaking the owner via the subscription because we only
+ // unsubscribe on destroy which already happened.
+ if (destroyed) {
+ return@channelFlow
+ }
+
+ owner?.lifecycle?.removeObserver(ownerDestroyedObserver)
+
+ val subscription = observeManually { state ->
+ runBlocking {
+ try {
+ send(state)
+ } catch (e: CancellationException) {
+ // It's possible for this channel to have been closed concurrently before
+ // we had a chance to unsubscribe. In this case we can just ignore this
+ // one subscription and keep going.
+ }
+ }
+ }
+
+ if (owner == null) {
+ subscription.resume()
+ } else {
+ subscription.binding = SubscriptionLifecycleBinding(owner, subscription).apply {
+ owner.lifecycle.addObserver(this)
+ }
+ }
+
+ awaitClose {
+ subscription.unsubscribe()
+ }
+ }.buffer(Channel.CONFLATED)
+}
+
+/**
+ * Launches a coroutine in a new [MainScope] and creates a [Flow] for observing [State] changes in
+ * the [Store] in that scope. Invokes [block] inside that scope and passes the [Flow] to it.
+ *
+ * @param owner An optional [LifecycleOwner] that will be used to determine when to pause and resume
+ * the store subscription. When the [Lifecycle] is in STOPPED state then no [State] will be received.
+ * Once the [Lifecycle] switches back to at least STARTED state then the latest [State] and further
+ * updates will be emitted.
+ * @return The [CoroutineScope] [block] is getting executed in.
+ */
+@MainThread
+fun <S : State, A : Action> Store<S, A>.flowScoped(
+ owner: LifecycleOwner? = null,
+ block: suspend (Flow<S>) -> Unit,
+): CoroutineScope {
+ return MainScope().apply {
+ launch {
+ block(flow(owner))
+ }
+ }
+}
+
+/**
+ * GenericLifecycleObserver implementation to bind an observer to a Lifecycle.
+ */
+private class SubscriptionLifecycleBinding<S : State, A : Action>(
+ private val owner: LifecycleOwner,
+ private val subscription: Store.Subscription<S, A>,
+) : DefaultLifecycleObserver, Store.Subscription.Binding {
+ override fun onStart(owner: LifecycleOwner) {
+ subscription.resume()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ subscription.pause()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ subscription.unsubscribe()
+ }
+
+ override fun unbind() {
+ owner.lifecycle.removeObserver(this)
+ }
+}
+
+/**
+ * View.OnAttachStateChangeListener implementation to bind an observer to a View.
+ */
+private class SubscriptionViewBinding<S : State, A : Action>(
+ private val view: View,
+ private val subscription: Store.Subscription<S, A>,
+) : View.OnAttachStateChangeListener, Store.Subscription.Binding {
+ override fun onViewAttachedToWindow(v: View) {
+ subscription.resume()
+ }
+
+ override fun onViewDetachedFromWindow(view: View) {
+ subscription.unsubscribe()
+ }
+
+ override fun unbind() {
+ view.removeOnAttachStateChangeListener(this)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt
new file mode 100644
index 0000000000..aafe7d1ed9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt
@@ -0,0 +1,39 @@
+/* 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.lib.state.ext
+
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.launch
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import mozilla.components.support.ktx.android.view.toScope
+
+/**
+ * Helper extension method for consuming [State] from a [Store] sequentially in order scoped to the
+ * lifetime of the [View]. The [block] function will get invoked for every [State] update.
+ *
+ * This helper will automatically stop observing the [Store] once the [View] gets detached. The
+ * provided [LifecycleOwner] is used to determine when observing should be stopped or resumed.
+ *
+ * Inside a [Fragment] prefer to use [Fragment.consumeFrom].
+ */
+@ExperimentalCoroutinesApi // Channel
+fun <S : State, A : Action> View.consumeFrom(
+ store: Store<S, A>,
+ owner: LifecycleOwner,
+ block: (S) -> Unit,
+) {
+ val scope = toScope()
+ val channel = store.channel(owner)
+
+ scope.launch {
+ channel.consumeEach { state -> block(state) }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt
new file mode 100644
index 0000000000..10a0859192
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt
@@ -0,0 +1,44 @@
+/* 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.lib.state.helpers
+
+import androidx.annotation.CallSuper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.Flow
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Helper class for creating small binding classes that are responsible for reacting to state
+ * changes.
+ */
+@ExperimentalCoroutinesApi // Flow
+abstract class AbstractBinding<in S : State>(
+ private val store: Store<S, out Action>,
+) : LifecycleAwareFeature {
+ private var scope: CoroutineScope? = null
+
+ @CallSuper
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ onState(flow)
+ }
+ }
+
+ @CallSuper
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ /**
+ * A callback that is invoked when a [Flow] on the [store] is available to use.
+ */
+ abstract suspend fun onState(flow: Flow<S>)
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt
new file mode 100644
index 0000000000..69ea7dd52b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt
@@ -0,0 +1,67 @@
+/* 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.lib.state.internal
+
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Reducer
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+
+/**
+ * Builder to lazily create a function that will invoke the chain of [middleware] and finally the
+ * [reducer].
+ */
+internal class ReducerChainBuilder<S : State, A : Action>(
+ private val storeThreadFactory: StoreThreadFactory,
+ private val reducer: Reducer<S, A>,
+ private val middleware: List<Middleware<S, A>>,
+) {
+ private var chain: ((A) -> Unit)? = null
+
+ /**
+ * Returns a function that will invoke the chain of [middleware] and the [reducer] for the given
+ * [Store].
+ */
+ fun get(store: Store<S, A>): (A) -> Unit {
+ chain?.let { return it }
+
+ return build(store).also {
+ chain = it
+ }
+ }
+
+ private fun build(store: Store<S, A>): (A) -> Unit {
+ val context = object : MiddlewareContext<S, A> {
+ override val state: S
+ get() = store.state
+
+ override fun dispatch(action: A) {
+ get(store).invoke(action)
+ }
+
+ override val store: Store<S, A>
+ get() = store
+ }
+
+ var chain: (A) -> Unit = { action ->
+ val state = reducer(store.state, action)
+ store.transitionTo(state)
+ }
+
+ val threadCheck: Middleware<S, A> = { _, next, action ->
+ storeThreadFactory.assertOnThread()
+ next(action)
+ }
+
+ (middleware.reversed() + threadCheck).forEach { middleware ->
+ val next = chain
+ chain = { action -> middleware(context, next, action) }
+ }
+
+ return chain
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt
new file mode 100644
index 0000000000..fb9e53d7da
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt
@@ -0,0 +1,58 @@
+/* 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.lib.state.internal
+
+import mozilla.components.lib.state.Store
+import mozilla.components.support.base.utils.NamedThreadFactory
+import java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+
+/**
+ * Custom [ThreadFactory] implementation wrapping [Executors.defaultThreadFactory]/[NamedThreadFactory]
+ * that allows asserting whether a caller is on the created thread.
+ *
+ * For usage with [Executors.newSingleThreadExecutor]: Only the last created thread is kept and
+ * compared when [assertOnThread] is called.
+ *
+ * @param threadNamePrefix Optional prefix with which to name threads for the [Store]. If not provided,
+ * the naming scheme will be deferred to [Executors.defaultThreadFactory]
+ */
+internal class StoreThreadFactory(
+ threadNamePrefix: String?,
+) : ThreadFactory {
+ @Volatile
+ private var thread: Thread? = null
+
+ private val actualFactory = if (threadNamePrefix != null) {
+ NamedThreadFactory(threadNamePrefix)
+ } else {
+ Executors.defaultThreadFactory()
+ }
+
+ override fun newThread(r: Runnable): Thread {
+ return actualFactory.newThread(r).also {
+ thread = it
+ }
+ }
+
+ /**
+ * Asserts that the calling thread is the thread of this [StoreDispatcher]. Otherwise throws an
+ * [IllegalThreadStateException].
+ */
+ fun assertOnThread() {
+ val currentThread = Thread.currentThread()
+ val currentThreadId = currentThread.id
+ val expectedThreadId = thread?.id
+
+ if (currentThreadId == expectedThreadId) {
+ return
+ }
+
+ throw IllegalThreadStateException(
+ "Expected `store` thread, but running on thread `${currentThread.name}`. " +
+ "Leaked MiddlewareContext or did you mean to use `MiddlewareContext.store.dispatch`?",
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt
new file mode 100644
index 0000000000..34adcf511d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt
@@ -0,0 +1,33 @@
+/* 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.lib.state
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.shadows.ShadowLooper
+
+@RunWith(AndroidJUnit4::class)
+class StoreExceptionTest {
+ // This test is in a separate class because it needs to run with Robolectric (different runner, slower) while all
+ // other tests only need a Java VM (fast).
+ @Test(expected = StoreException::class)
+ fun `Exception in reducer will be rethrown on main thread`() {
+ val throwingReducer: (TestState, TestAction) -> TestState = { _, _ ->
+ throw IllegalStateException("Not reducing today")
+ }
+
+ val store = Store(TestState(counter = 23), throwingReducer)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ // Wait for the main looper to process the re-thrown exception.
+ ShadowLooper.idleMainLooper()
+
+ Assert.fail()
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt
new file mode 100644
index 0000000000..715e3e55ba
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt
@@ -0,0 +1,311 @@
+/* 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.lib.state
+
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.io.IOException
+
+class StoreTest {
+ @Test
+ fun `Dispatching Action executes reducers and creates new State`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertEquals(24, store.state.counter)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+
+ assertEquals(22, store.state.counter)
+ }
+
+ @Test
+ fun `Observer gets notified about state changes`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var observedValue = 0
+
+ store.observeManually { state -> observedValue = state.counter }.also {
+ it.resume()
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertEquals(24, observedValue)
+ }
+
+ @Test
+ fun `Observer gets initial value before state changes`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var observedValue = 0
+
+ store.observeManually { state -> observedValue = state.counter }.also {
+ it.resume()
+ }
+
+ assertEquals(23, observedValue)
+ }
+
+ @Test
+ fun `Observer does not get notified if state does not change`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateChangeObserved = false
+
+ store.observeManually { stateChangeObserved = true }.also {
+ it.resume()
+ }
+
+ // Initial state observed
+ assertTrue(stateChangeObserved)
+ stateChangeObserved = false
+
+ store.dispatch(TestAction.DoNothingAction).joinBlocking()
+
+ assertFalse(stateChangeObserved)
+ }
+
+ @Test
+ fun `Observer does not get notified after unsubscribe`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var observedValue = 0
+
+ val subscription = store.observeManually { state ->
+ observedValue = state.counter
+ }.also {
+ it.resume()
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertEquals(24, observedValue)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+
+ assertEquals(23, observedValue)
+
+ subscription.unsubscribe()
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+
+ assertEquals(23, observedValue)
+ assertEquals(22, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware chain gets executed in order`() {
+ val incrementMiddleware: Middleware<TestState, TestAction> = { store, next, action ->
+ if (action == TestAction.DoNothingAction) {
+ store.dispatch(TestAction.IncrementAction)
+ }
+
+ next(action)
+ }
+
+ val doubleMiddleware: Middleware<TestState, TestAction> = { store, next, action ->
+ if (action == TestAction.DoNothingAction) {
+ store.dispatch(TestAction.DoubleAction)
+ }
+
+ next(action)
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(
+ incrementMiddleware,
+ doubleMiddleware,
+ ),
+ )
+
+ store.dispatch(TestAction.DoNothingAction).joinBlocking()
+
+ assertEquals(2, store.state.counter)
+
+ store.dispatch(TestAction.DoNothingAction).joinBlocking()
+
+ assertEquals(6, store.state.counter)
+
+ store.dispatch(TestAction.DoNothingAction).joinBlocking()
+
+ assertEquals(14, store.state.counter)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+
+ assertEquals(13, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware can intercept actions`() {
+ val interceptingMiddleware: Middleware<TestState, TestAction> = { _, _, _ ->
+ // Do nothing!
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(interceptingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(0, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(0, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(0, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware can rewrite actions`() {
+ val rewritingMiddleware: Middleware<TestState, TestAction> = { _, next, _ ->
+ next(TestAction.DecrementAction)
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(rewritingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-1, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-2, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-3, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware can intercept and dispatch other action instead`() {
+ val rewritingMiddleware: Middleware<TestState, TestAction> = { store, next, action ->
+ if (action == TestAction.IncrementAction) {
+ store.dispatch(TestAction.DecrementAction)
+ } else {
+ next(action)
+ }
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(rewritingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-1, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-2, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-3, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware sees state before and after reducing`() {
+ var countBefore = -1
+ var countAfter = -1
+
+ val observingMiddleware: Middleware<TestState, TestAction> = { store, next, action ->
+ countBefore = store.state.counter
+ next(action)
+ countAfter = store.state.counter
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(observingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(0, countBefore)
+ assertEquals(1, countAfter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(1, countBefore)
+ assertEquals(2, countAfter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(2, countBefore)
+ assertEquals(3, countAfter)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+ assertEquals(3, countBefore)
+ assertEquals(2, countAfter)
+ }
+
+ @Test
+ fun `Middleware can catch exceptions in reducer`() {
+ var caughtException: Exception? = null
+
+ val catchingMiddleware: Middleware<TestState, TestAction> = { _, next, action ->
+ try {
+ next(action)
+ } catch (e: Exception) {
+ caughtException = e
+ }
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ { _: State, _: Action -> throw IOException() },
+ listOf(catchingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertNotNull(caughtException)
+ assertTrue(caughtException is IOException)
+ }
+}
+
+fun reducer(state: TestState, action: TestAction): TestState = when (action) {
+ is TestAction.IncrementAction -> state.copy(counter = state.counter + 1)
+ is TestAction.DecrementAction -> state.copy(counter = state.counter - 1)
+ is TestAction.SetValueAction -> state.copy(counter = action.value)
+ is TestAction.DoubleAction -> state.copy(counter = state.counter * 2)
+ is TestAction.DoNothingAction -> state
+}
+
+data class TestState(
+ val counter: Int,
+) : State
+
+sealed class TestAction : Action {
+ object IncrementAction : TestAction()
+ object DecrementAction : TestAction()
+ object DoNothingAction : TestAction()
+ object DoubleAction : TestAction()
+ data class SetValueAction(val value: Int) : TestAction()
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt
new file mode 100644
index 0000000000..b86e4485f4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt
@@ -0,0 +1,301 @@
+/* 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.lib.state.ext
+
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.setMain
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.TestAction
+import mozilla.components.lib.state.TestState
+import mozilla.components.lib.state.reducer
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.coroutines.CoroutineContext
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class FragmentKtTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ @Synchronized
+ fun `consumeFrom reads states from store`() {
+ val fragment = mock<Fragment>()
+ val view = mock<View>()
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val onAttachListener = argumentCaptor<View.OnAttachStateChangeListener>()
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ doNothing().`when`(view).addOnAttachStateChangeListener(onAttachListener.capture())
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(view).`when`(fragment).view
+ doReturn(owner.lifecycle).`when`(fragment).lifecycle
+
+ fragment.consumeFrom(store) { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ // View gets detached
+ onAttachListener.value.onViewDetachedFromWindow(view)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ @Synchronized
+ fun `consumeFrom does not run when fragment is detached`() {
+ val fragment = mock<Fragment>()
+ val view = mock<View>()
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(view).`when`(fragment).view
+ doReturn(owner.lifecycle).`when`(fragment).lifecycle
+
+ fragment.consumeFrom(store) { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(23, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ doReturn(null).`when`(fragment).activity
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(28, receivedValue)
+ }
+
+ @Test
+ fun `consumeFlow - reads states from store`() {
+ val fragment = mock<Fragment>()
+ val view = mock<View>()
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val onAttachListener = argumentCaptor<View.OnAttachStateChangeListener>()
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ doNothing().`when`(view).addOnAttachStateChangeListener(onAttachListener.capture())
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(view).`when`(fragment).view
+ doReturn(owner.lifecycle).`when`(fragment).lifecycle
+
+ fragment.consumeFlow(
+ from = store,
+ owner = owner,
+ ) { flow ->
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ // View gets detached
+ onAttachListener.value.onViewDetachedFromWindow(view)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ fun `consumeFlow - uses fragment as lifecycle owner by default`() {
+ val fragment = mock<Fragment>()
+ val fragmentLifecycleOwner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+ val view = mock<View>()
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val onAttachListener = argumentCaptor<View.OnAttachStateChangeListener>()
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ doNothing().`when`(view).addOnAttachStateChangeListener(onAttachListener.capture())
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(view).`when`(fragment).view
+ doReturn(fragmentLifecycleOwner.lifecycle).`when`(fragment).lifecycle
+
+ fragment.consumeFlow(
+ from = store,
+ ) { flow ->
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ fragmentLifecycleOwner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+ }
+
+ @Test
+ fun `consumeFlow - creates flow synchronously`() {
+ val fragment = mock<Fragment>()
+ val fragmentLifecycle = mock<LifecycleRegistry>()
+ val view = mock<View>()
+ val store = Store(TestState(counter = 23), ::reducer)
+
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(fragmentLifecycle).`when`(fragment).lifecycle
+ doReturn(view).`when`(fragment).view
+
+ // Verify that we create the flow even if no other coroutine runs past this point
+ val noopDispatcher = object : CoroutineDispatcher() {
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ // NOOP
+ }
+ }
+ Dispatchers.setMain(noopDispatcher)
+ fragment.consumeFlow(store) { flow ->
+ flow.collect { }
+ }
+
+ // Only way to verify that store.flow was called without triggering the channelFlow
+ // producer and in this test we want to make sure we call store.flow before the flow
+ // is "produced."
+ verify(fragmentLifecycle).addObserver(any())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt
new file mode 100644
index 0000000000..c52bdb032e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt
@@ -0,0 +1,572 @@
+/* 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.lib.state.ext
+
+import android.app.Activity
+import android.os.Looper
+import android.os.Looper.getMainLooper
+import android.view.View
+import android.view.WindowManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.launch
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.TestAction
+import mozilla.components.lib.state.TestState
+import mozilla.components.lib.state.reducer
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage.
+class StoreExtensionsKtTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `Observer will not get registered if lifecycle is already destroyed`() = runTestOnMain {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ // We cannot set initial DESTROYED state for LifecycleRegistry
+ // so we simulate lifecycle getting destroyed.
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateObserved = false
+
+ store.observe(owner) { stateObserved = true }
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertFalse(stateObserved)
+ }
+
+ @Test
+ fun `Observer will get unregistered if lifecycle gets destroyed`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateObserved = false
+ store.observe(owner) { stateObserved = true }
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+ }
+
+ @Test
+ fun `non-destroy lifecycle changes do not affect observer registration`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ // Observer does not get invoked since lifecycle is not started
+ var stateObserved = false
+ store.observe(owner) { stateObserved = true }
+ assertFalse(stateObserved)
+
+ // CREATED: Observer does still not get invoked
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+
+ // STARTED: Observer gets initial state and observers updates
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(stateObserved)
+
+ // RESUMED: Observer continues to get updates
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(stateObserved)
+
+ // CREATED: Not observing anymore
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+
+ // DESTROYED: Not observing
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi // Channel
+ fun `Reading state updates from channel`() = runTestOnMain {
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val channel = store.channel(owner)
+
+ val job = launch {
+ channel.consumeEach { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ job.cancelAndJoin()
+ assertTrue(channel.isClosedForReceive)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ @ExperimentalCoroutinesApi // Channel
+ fun `Creating channel throws if lifecycle is already DESTROYED`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ // We cannot set initial DESTROYED state for LifecycleRegistry
+ // so we simulate lifecycle getting destroyed.
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ store.channel(owner)
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi
+ fun `Reading state updates from Flow with lifecycle owner`() = runTestOnMain {
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val flow = store.flow(owner)
+
+ val job = coroutinesTestRule.scope.launch {
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ job.cancelAndJoin()
+
+ // Receiving nothing anymore since coroutine is cancelled
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `Subscription is not added if owner destroyed before flow created`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+ val latch = CountDownLatch(1)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ val flow = store.flow(owner)
+ GlobalScope.launch {
+ flow.collect {
+ latch.countDown()
+ }
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertTrue(store.subscriptions.isEmpty())
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `Subscription is not added if owner destroyed before flow produced`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+ val latch = CountDownLatch(1)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val flow = store.flow(owner)
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ GlobalScope.launch {
+ flow.collect {
+ latch.countDown()
+ }
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertTrue(store.subscriptions.isEmpty())
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi
+ fun `Reading state updates from Flow without lifecycle owner`() = runTestOnMain {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val flow = store.flow()
+
+ val job = GlobalScope.launch {
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Receiving immediately
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(23, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+
+ latch = CountDownLatch(1)
+
+ job.cancelAndJoin()
+
+ // Receiving nothing anymore since coroutine is cancelled
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi
+ fun `Reading state from scoped flow without lifecycle owner`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val scope = store.flowScoped() { flow ->
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Receiving immediately
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(23, receivedValue)
+
+ // Updating state: Nothing received yet.
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+
+ scope.cancel()
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi
+ fun `Reading state from scoped flow with lifecycle owner`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val scope = store.flowScoped(owner) { flow ->
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ latch = CountDownLatch(1)
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+
+ scope.cancel()
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ fun `Observer registered with observeForever will get notified about state changes`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var observedValue = 0
+
+ store.observeForever { state -> observedValue = state.counter }
+ assertEquals(23, observedValue)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(24, observedValue)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+ assertEquals(23, observedValue)
+ }
+
+ @Test
+ fun `Observer bound to view will get unsubscribed if view gets detached`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(view.isAttachedToWindow)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateObserved = false
+ store.observe(view) { stateObserved = true }
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(stateObserved)
+
+ activity.windowManager.removeView(view)
+ shadowOf(getMainLooper()).idle()
+ assertFalse(view.isAttachedToWindow)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+ }
+
+ @Test
+ fun `Observer bound to view will not get notified about state changes until the view is attached`() = runTestOnMain {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+
+ assertFalse(view.isAttachedToWindow)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateObserved = false
+ store.observe(view) { stateObserved = true }
+ assertFalse(stateObserved)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(Looper.getMainLooper()).idle()
+ assertTrue(view.isAttachedToWindow)
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.observe(view) { stateObserved = true }
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.observe(view) { stateObserved = true }
+ assertTrue(stateObserved)
+
+ activity.windowManager.removeView(view)
+ shadowOf(Looper.getMainLooper()).idle()
+
+ assertFalse(view.isAttachedToWindow)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+ }
+}
+
+internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {
+ val lifecycleRegistry = LifecycleRegistry(this).apply {
+ currentState = initialState
+ }
+
+ override val lifecycle: Lifecycle = lifecycleRegistry
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt
new file mode 100644
index 0000000000..6dfde6f9fa
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt
@@ -0,0 +1,89 @@
+/* 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.lib.state.ext
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.TestAction
+import mozilla.components.lib.state.TestState
+import mozilla.components.lib.state.reducer
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class ViewKtTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ @Synchronized
+ fun `consumeFrom reads states from store`() {
+ val view = mock<View>()
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val onAttachListener = argumentCaptor<View.OnAttachStateChangeListener>()
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+ doNothing().`when`(view).addOnAttachStateChangeListener(onAttachListener.capture())
+
+ view.consumeFrom(store, owner) { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ // View gets detached
+ onAttachListener.value.onViewDetachedFromWindow(view)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt
new file mode 100644
index 0000000000..5173ddc39e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt
@@ -0,0 +1,98 @@
+/* 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.lib.state.helpers
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.TestAction
+import mozilla.components.lib.state.TestState
+import mozilla.components.lib.state.reducer
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class AbstractBindingTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `binding onState is invoked when a flow is created`() {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+
+ val binding = TestBinding(store)
+
+ assertFalse(binding.invoked)
+
+ binding.start()
+
+ assertTrue(binding.invoked)
+ }
+
+ @Test
+ fun `binding has no state changes when only stop is invoked`() {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+
+ val binding = TestBinding(store)
+
+ assertFalse(binding.invoked)
+
+ binding.stop()
+
+ assertFalse(binding.invoked)
+ }
+
+ @Test
+ fun `binding does not get state updates after stopped`() {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+
+ var counter = 0
+
+ val binding = TestBinding(store) {
+ counter++
+ // After we stop, we shouldn't get updates for the third action dispatched.
+ if (counter >= 3) {
+ fail()
+ }
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ binding.start()
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ binding.stop()
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ }
+}
+
+@ExperimentalCoroutinesApi
+class TestBinding(
+ store: Store<TestState, TestAction>,
+ private val onStateUpdated: (TestState) -> Unit = {},
+) : AbstractBinding<TestState>(store) {
+ var invoked = false
+ override suspend fun onState(flow: Flow<TestState>) {
+ invoked = true
+ flow.collect { onStateUpdated(it) }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/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/lib/state/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/state/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/contile/README.md b/mobile/android/android-components/components/service/contile/README.md
new file mode 100644
index 0000000000..c53b90af05
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/README.md
@@ -0,0 +1,20 @@
+# [Android Components](../../../README.md) > Service > Contile
+
+A library for communicating with the Contile services API.
+
+## Usage
+
+### 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:service-contile:{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/service/contile/build.gradle b/mobile/android/android-components/components/service/contile/build.gradle
new file mode 100644
index 0000000000..6d0fa94c29
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/build.gradle
@@ -0,0 +1,45 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.service.contile'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_work_runtime
+
+ implementation project(':concept-fetch')
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':feature-top-sites')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.androidx_work_testing
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ testImplementation project(':support-test')
+}
+
+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/service/contile/proguard-rules.pro b/mobile/android/android-components/components/service/contile/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/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/service/contile/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/contile/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/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/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesProvider.kt b/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesProvider.kt
new file mode 100644
index 0000000000..b1d10bb3c9
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesProvider.kt
@@ -0,0 +1,307 @@
+/* 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.service.contile
+
+import android.content.Context
+import android.text.format.DateUtils
+import android.util.AtomicFile
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.feature.top.sites.TopSite
+import mozilla.components.feature.top.sites.TopSitesProvider
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.org.json.asSequence
+import mozilla.components.support.ktx.android.org.json.tryGetLong
+import mozilla.components.support.ktx.util.readAndDeserialize
+import mozilla.components.support.ktx.util.writeString
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.File
+import java.io.IOException
+import java.util.Date
+
+internal const val CONTILE_ENDPOINT_URL = "https://contile.services.mozilla.com/v1/tiles"
+internal const val CACHE_FILE_NAME = "mozilla_components_service_contile.json"
+internal const val CACHE_VALID_FOR_KEY = "valid_for"
+internal const val CACHE_TOP_SITES_KEY = "tiles"
+
+/**
+ * Provides access to the Contile services API.
+ *
+ * @param context A reference to the application context.
+ * @property client [Client] used for interacting with the Contile HTTP API.
+ * @property endPointURL The url of the endpoint to fetch from. Defaults to [CONTILE_ENDPOINT_URL].
+ * @property maxCacheAgeInSeconds Maximum time (in seconds) the cache should remain valid
+ * before a refresh is attempted. Defaults to -1, meaning the max age defined by the server
+ * will be used.
+ */
+class ContileTopSitesProvider(
+ context: Context,
+ private val client: Client,
+ private val endPointURL: String = CONTILE_ENDPOINT_URL,
+ private val maxCacheAgeInSeconds: Long = -1,
+) : TopSitesProvider {
+
+ private val applicationContext = context.applicationContext
+ private val logger = Logger("ContileTopSitesProvider")
+ private val diskCacheLock = Any()
+
+ // Current state of the cache.
+ @VisibleForTesting
+ @Volatile
+ internal var cacheState: CacheState = CacheState()
+
+ /**
+ * Fetches from the top sites [endPointURL] to provide a list of provided top sites.
+ * Returns a cached response if [allowCache] is true and the cache is not expired
+ * (@see [maxCacheAgeInSeconds]).
+ *
+ * @param allowCache Whether or not the result may be provided from a previously cached
+ * response.
+ * @throws IOException if the request failed to fetch any top sites.
+ */
+ @Throws(IOException::class)
+ override suspend fun getTopSites(allowCache: Boolean): List<TopSite.Provided> {
+ val cachedTopSites = if (allowCache) {
+ getCachedTopSitesIfValid(shouldUseServerMaxAge = false)
+ } else {
+ null
+ }
+ if (!cachedTopSites.isNullOrEmpty()) {
+ return cachedTopSites
+ }
+
+ return try {
+ fetchTopSites()
+ } catch (e: IOException) {
+ logger.error("Failed to fetch contile top sites", e)
+ throw e
+ }
+ }
+
+ /**
+ * Refreshes the cache with the latest top sites response from [endPointURL]
+ * if the cache is expired.
+ */
+ suspend fun refreshTopSitesIfCacheExpired() {
+ if (!isCacheExpired(shouldUseServerMaxAge = false)) return
+
+ getTopSites(allowCache = false)
+ }
+
+ private fun fetchTopSites(): List<TopSite.Provided> {
+ client.fetch(
+ Request(url = endPointURL, conservative = true),
+ ).use { response ->
+ if (response.isSuccess) {
+ val responseBody = response.body.string(Charsets.UTF_8)
+
+ if (response.status == Response.NO_CONTENT) {
+ // If the response is 204, we should invalidate the cached top sites
+ cacheState = cacheState.invalidate()
+ getCacheFile().delete()
+ return listOf()
+ }
+
+ return try {
+ val jsonBody = JSONObject(responseBody)
+ writeToDiskCache(
+ response.headers.computeValidFor() * DateUtils.SECOND_IN_MILLIS,
+ jsonBody.getJSONArray(CACHE_TOP_SITES_KEY),
+ )
+
+ jsonBody.getTopSites()
+ } catch (e: JSONException) {
+ throw IOException(e)
+ }
+ } else {
+ // If fetch failed, we should check if the set of top sites is still valid
+ // and use it as fallback.
+ val topSites = getCachedTopSitesIfValid(shouldUseServerMaxAge = true)
+ if (!topSites.isNullOrEmpty()) {
+ return topSites
+ }
+ val errorMessage =
+ "Failed to fetch contile top sites. Status code: ${response.status}"
+ logger.error(errorMessage)
+ throw IOException(errorMessage)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun readFromDiskCache(): CachedData? {
+ synchronized(diskCacheLock) {
+ return getCacheFile().readAndDeserialize {
+ JSONObject(it).let { cachedObject ->
+ CachedData(cachedObject.validFor, cachedObject.getTopSites())
+ }
+ }
+ }
+ }
+
+ /**
+ * Write the validity time and top sites to a file for caching purposes.
+ *
+ * @param validFor Time in milliseconds describing the click validity for the set of top sites.
+ * @param topSites [JSONArray] containing the top sites to be cached.
+ */
+ @VisibleForTesting
+ internal fun writeToDiskCache(validFor: Long, topSites: JSONArray) {
+ val cachedData = JSONObject().apply {
+ put(CACHE_VALID_FOR_KEY, validFor)
+ put(CACHE_TOP_SITES_KEY, topSites)
+ }
+ synchronized(diskCacheLock) {
+ getCacheFile().let {
+ it.writeString { cachedData.toString() }
+
+ // Update the cache state to reflect the current status
+ cacheState = cacheState.computeMaxAges(
+ System.currentTimeMillis(),
+ maxCacheAgeInSeconds * DateUtils.SECOND_IN_MILLIS,
+ validFor,
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns the cached top sites if the cached data is not expired, based on the client or server
+ * specified max age, else null is returned. In the case of a server outage, the cached server
+ * max age should be used as fallback.
+ *
+ * @param shouldUseServerMaxAge True if server cache max age should be used.
+ */
+ private fun getCachedTopSitesIfValid(shouldUseServerMaxAge: Boolean) =
+ if (!isCacheExpired(shouldUseServerMaxAge)) {
+ readFromDiskCache()?.topSites
+ } else {
+ null
+ }
+
+ @VisibleForTesting
+ internal fun isCacheExpired(shouldUseServerMaxAge: Boolean): Boolean {
+ cacheState.getCacheMaxAge(shouldUseServerMaxAge)?.let { return Date().time > it }
+
+ val file = getBaseCacheFile()
+
+ cacheState =
+ if (file.exists()) {
+ cacheState.computeMaxAges(
+ file.lastModified(),
+ maxCacheAgeInSeconds * DateUtils.SECOND_IN_MILLIS,
+ (readFromDiskCache()?.validFor ?: 0L),
+ )
+ } else {
+ cacheState.invalidate()
+ }
+
+ // If cache is invalid, we should also consider it as expired
+ return Date().time > (cacheState.getCacheMaxAge(shouldUseServerMaxAge) ?: -1L)
+ }
+
+ private fun getCacheFile(): AtomicFile = AtomicFile(getBaseCacheFile())
+
+ @VisibleForTesting
+ internal fun getBaseCacheFile(): File = File(applicationContext.filesDir, CACHE_FILE_NAME)
+
+ /**
+ * Data stored in the cache file
+ *
+ * @param validFor Time in milliseconds describing the click validity for the set of top sites.
+ * @param topSites List of provided top sites.
+ */
+ internal data class CachedData(
+ val validFor: Long,
+ val topSites: List<TopSite.Provided>,
+ )
+
+ /**
+ * Current state of the cache.
+ *
+ * @param isCacheValid Whether or not the current set of cached top sites is still valid.
+ * @param localCacheMaxAge Maximum unix timestamp until the current set of cached top sites
+ * is still valid, specified by the client.
+ * @param serverCacheMaxAge Maximum unix timestamp until the current set of cached top sites
+ * is still valid, specified by the server.
+ */
+ internal data class CacheState(
+ val isCacheValid: Boolean = true,
+ val localCacheMaxAge: Long? = null,
+ val serverCacheMaxAge: Long? = null,
+ ) {
+ fun getCacheMaxAge(shouldUseServerMaxAge: Boolean = false) = if (isCacheValid) {
+ if (shouldUseServerMaxAge) serverCacheMaxAge else localCacheMaxAge
+ } else {
+ null
+ }
+
+ fun invalidate(): CacheState =
+ this.copy(isCacheValid = false, localCacheMaxAge = null, serverCacheMaxAge = null)
+
+ /**
+ * Update local and server max age values.
+ *
+ * @param lastModified Unix timestamp when the cache was last modified.
+ * @param localMaxAge Validity of local cache in milliseconds.
+ * @param serverMaxAge Server specified validity in milliseconds. To be used as fallback
+ * when local max age is exceeded and a server outage is detected.
+ */
+ fun computeMaxAges(lastModified: Long, localMaxAge: Long, serverMaxAge: Long): CacheState =
+ this.copy(
+ isCacheValid = true,
+ localCacheMaxAge = lastModified + localMaxAge,
+ serverCacheMaxAge = lastModified + serverMaxAge,
+ )
+ }
+}
+
+/**
+ * To extract the `valid-for` value for the set of provided top sites, we need to sum up the `max-age`
+ * and `stale-if-error` options from the header. These values can be found in the `cache-control` header,
+ * formatted as `max-age=$value` and `stale-if-error=$value`.
+ */
+internal fun Headers.computeValidFor(): Long =
+ getAll("cache-control").sumOf {
+ val valueList = it.split("=")
+ .map { item -> item.trim() }
+
+ if (valueList.size == 2 && valueList[0] in listOf("max-age", "stale-if-error")) {
+ valueList[1].toLong()
+ } else {
+ 0L
+ }
+ }
+
+internal fun JSONObject.getTopSites(): List<TopSite.Provided> =
+ getJSONArray(CACHE_TOP_SITES_KEY)
+ .asSequence { i -> getJSONObject(i) }
+ .mapNotNull { it.toTopSite() }
+ .toList()
+
+private fun JSONObject.toTopSite(): TopSite.Provided? {
+ return try {
+ TopSite.Provided(
+ id = getLong("id"),
+ title = getString("name"),
+ url = getString("url"),
+ clickUrl = getString("click_url"),
+ imageUrl = getString("image_url"),
+ impressionUrl = getString("impression_url"),
+ createdAt = null,
+ )
+ } catch (e: JSONException) {
+ null
+ }
+}
+
+internal val JSONObject.validFor: Long
+ get() = this.tryGetLong(CACHE_VALID_FOR_KEY) ?: 0L
diff --git a/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdater.kt b/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdater.kt
new file mode 100644
index 0000000000..b8d21ec854
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdater.kt
@@ -0,0 +1,81 @@
+/* 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.service.contile
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.worker.Frequency
+import java.util.concurrent.TimeUnit
+
+/**
+ * Provides functionality to schedule updates of Contile top sites.
+ *
+ * @property context A reference to the application context.
+ * @property provider An instance of [ContileTopSitesProvider] which provides access to the Contile
+ * services API for fetching top sites.
+ * @property frequency Optional [Frequency] that specifies how often the Contile top site updates
+ * should happen.
+ */
+class ContileTopSitesUpdater(
+ private val context: Context,
+ private val provider: ContileTopSitesProvider,
+ private val frequency: Frequency = Frequency(1, TimeUnit.DAYS),
+) {
+
+ private val logger = Logger("ContileTopSitesUpdater")
+
+ /**
+ * Starts a work request in the background to periodically update the list of
+ * Contile top sites.
+ */
+ fun startPeriodicWork() {
+ ContileTopSitesUseCases.initialize(provider)
+
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(
+ PERIODIC_WORK_TAG,
+ ExistingPeriodicWorkPolicy.KEEP,
+ createPeriodicWorkRequest(),
+ )
+
+ logger.info("Started periodic work to update Contile top sites")
+ }
+
+ /**
+ * Stops the work request to periodically update the list of Contile top sites.
+ */
+ fun stopPeriodicWork() {
+ ContileTopSitesUseCases.destroy()
+
+ WorkManager.getInstance(context).cancelUniqueWork(PERIODIC_WORK_TAG)
+
+ logger.info("Stopped periodic work to update Contile top sites")
+ }
+
+ @VisibleForTesting
+ internal fun createPeriodicWorkRequest() =
+ PeriodicWorkRequestBuilder<ContileTopSitesUpdaterWorker>(
+ repeatInterval = frequency.repeatInterval,
+ repeatIntervalTimeUnit = frequency.repeatIntervalTimeUnit,
+ ).apply {
+ setConstraints(getWorkerConstraints())
+ addTag(PERIODIC_WORK_TAG)
+ }.build()
+
+ @VisibleForTesting
+ internal fun getWorkerConstraints() = Constraints.Builder()
+ .setRequiresStorageNotLow(true)
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ companion object {
+ internal const val PERIODIC_WORK_TAG = "mozilla.components.service.contile.periodicWork"
+ }
+}
diff --git a/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorker.kt b/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorker.kt
new file mode 100644
index 0000000000..35ce3d6094
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorker.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.service.contile
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * An implementation of [CoroutineWorker] to perform Contile top site updates.
+ */
+internal class ContileTopSitesUpdaterWorker(
+ context: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+
+ private val logger = Logger("ContileTopSitesUpdaterWorker")
+
+ @Suppress("TooGenericExceptionCaught")
+ override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
+ try {
+ ContileTopSitesUseCases().refreshContileTopSites.invoke()
+ Result.success()
+ } catch (e: Exception) {
+ logger.error("Failed to refresh Contile top sites", e)
+ Result.failure()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUseCases.kt b/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUseCases.kt
new file mode 100644
index 0000000000..c3fcaa5077
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUseCases.kt
@@ -0,0 +1,58 @@
+/* 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.service.contile
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Contains use cases related to the Contlie top sites feature.
+ */
+internal class ContileTopSitesUseCases {
+
+ /**
+ * Refresh Contile top sites use case.
+ */
+ class RefreshContileTopSitesUseCase internal constructor() {
+ /**
+ * Refreshes the Contile top sites.
+ */
+ suspend operator fun invoke() {
+ requireContileTopSitesProvider().getTopSites(allowCache = false)
+ }
+ }
+
+ internal companion object {
+ @VisibleForTesting internal var provider: ContileTopSitesProvider? = null
+
+ /**
+ * Initializes the [ContileTopSitesProvider] which will providde access to the Contile
+ * services API.
+ */
+ internal fun initialize(provider: ContileTopSitesProvider) {
+ this.provider = provider
+ }
+
+ /**
+ * Unbinds the [ContileTopSitesProvider].
+ */
+ internal fun destroy() {
+ this.provider = null
+ }
+
+ /**
+ * Returns the [ContileTopSitesProvider], otherwise throw an exception if the [provider]
+ * has not been initialized.
+ */
+ internal fun requireContileTopSitesProvider(): ContileTopSitesProvider {
+ return requireNotNull(provider) {
+ "initialize must be called before trying to access the ContileTopSitesProvider"
+ }
+ }
+ }
+
+ val refreshContileTopSites: RefreshContileTopSitesUseCase by lazy {
+ RefreshContileTopSitesUseCase()
+ }
+}
diff --git a/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt b/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt
new file mode 100644
index 0000000000..be8747881d
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt
@@ -0,0 +1,335 @@
+/* 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.service.contile
+
+import android.text.format.DateUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Header
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Response
+import mozilla.components.feature.top.sites.TopSite
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.file.loadResourceAsString
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.io.File
+import java.io.IOException
+import java.util.Date
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class ContileTopSitesProviderTest {
+
+ @Test
+ fun `GIVEN a successful status response WHEN top sites are fetched THEN response should contain top sites`() =
+ runTest {
+ val client = prepareClient()
+ val provider = ContileTopSitesProvider(testContext, client)
+ val topSites = provider.getTopSites()
+ var topSite = topSites.first()
+
+ assertEquals(2, topSites.size)
+ assertEquals(1L, topSite.id)
+ assertEquals("Firefox", topSite.title)
+ assertEquals("https://firefox.com", topSite.url)
+ assertEquals("https://firefox.com/click", topSite.clickUrl)
+ assertEquals("https://test.com/image1.jpg", topSite.imageUrl)
+ assertEquals("https://test.com", topSite.impressionUrl)
+
+ topSite = topSites.last()
+
+ assertEquals(2L, topSite.id)
+ assertEquals("Mozilla", topSite.title)
+ assertEquals("https://mozilla.com", topSite.url)
+ assertEquals("https://mozilla.com/click", topSite.clickUrl)
+ assertEquals("https://test.com/image2.jpg", topSite.imageUrl)
+ assertEquals("https://example.com", topSite.impressionUrl)
+ }
+
+ @Test(expected = IOException::class)
+ fun `GIVEN a 500 status response WHEN top sites are fetched AND cached top sites are not valid THEN throw an exception`() =
+ runTest {
+ val client = prepareClient(status = 500)
+ val provider = ContileTopSitesProvider(testContext, client)
+ provider.getTopSites()
+ }
+
+ @Test
+ fun `GIVEN a 500 status response WHEN top sites are fetched AND cached top sites are valid THEN return the cached top sites`() =
+ runTest {
+ val client = prepareClient(status = 500)
+ val topSites = mock<List<TopSite.Provided>>()
+ val provider =
+ spy(ContileTopSitesProvider(testContext, client, maxCacheAgeInSeconds = 60L))
+ whenever(provider.isCacheExpired(false)).thenReturn(true)
+ whenever(provider.isCacheExpired(true)).thenReturn(false)
+ whenever(provider.readFromDiskCache()).thenReturn(
+ ContileTopSitesProvider.CachedData(
+ 60L,
+ topSites,
+ ),
+ )
+
+ assertEquals(topSites, provider.getTopSites())
+ }
+
+ @Test
+ fun `GIVEN a cache configuration is allowed and not expired WHEN top sites are fetched THEN read from the disk cache`() =
+ runTest {
+ val client = prepareClient()
+ val provider = spy(ContileTopSitesProvider(testContext, client))
+
+ provider.getTopSites(allowCache = false)
+ verify(provider, never()).readFromDiskCache()
+
+ whenever(provider.isCacheExpired(false)).thenReturn(true)
+ provider.getTopSites(allowCache = true)
+ verify(provider, never()).readFromDiskCache()
+
+ whenever(provider.isCacheExpired(false)).thenReturn(false)
+ provider.getTopSites(allowCache = true)
+ verify(provider).readFromDiskCache()
+ }
+
+ @Test
+ fun `GIVEN a set of top sites is cached WHEN checking the server specified cache max age THEN max age is calculated correctly`() =
+ runTest {
+ val client = prepareClient()
+ val provider = spy(ContileTopSitesProvider(testContext, client))
+ val file = mock<File> {
+ whenever(exists()).thenReturn(true)
+ whenever(lastModified()).thenReturn(Date().time)
+ }
+
+ whenever(provider.readFromDiskCache()).thenReturn(
+ ContileTopSitesProvider.CachedData(
+ 300000,
+ mock(),
+ ),
+ )
+ whenever(provider.getBaseCacheFile()).thenReturn(file)
+
+ assertFalse(provider.isCacheExpired(shouldUseServerMaxAge = true))
+
+ provider.cacheState = provider.cacheState.invalidate()
+ whenever(file.lastModified()).thenReturn(Date().time - 500000)
+ whenever(provider.getBaseCacheFile()).thenReturn(file)
+
+ assertTrue(provider.isCacheExpired(shouldUseServerMaxAge = true))
+ }
+
+ @Test
+ fun `GIVEN a cache max age is specified WHEN top sites are fetched THEN the cache max age is correctly set`() =
+ runTest {
+ val jsonResponse = loadResourceAsString("/contile/contile.json")
+ val client = prepareClient(jsonResponse)
+ val specifiedProvider = spy(
+ ContileTopSitesProvider(
+ context = testContext,
+ client = client,
+ maxCacheAgeInSeconds = 60L,
+ ),
+ )
+
+ specifiedProvider.getTopSites()
+ verify(specifiedProvider).writeToDiskCache(anyLong(), any())
+ assertEquals(
+ specifiedProvider.cacheState.localCacheMaxAge,
+ specifiedProvider.cacheState.getCacheMaxAge(),
+ )
+ assertFalse(specifiedProvider.isCacheExpired(false))
+ }
+
+ @Test
+ fun `GIVEN cache max age is not specified WHEN top sites are fetched THEN the cache is expired`() =
+ runTest {
+ val jsonResponse = loadResourceAsString("/contile/contile.json")
+ val client = prepareClient(jsonResponse)
+ val provider = spy(ContileTopSitesProvider(testContext, client))
+
+ provider.getTopSites()
+ verify(provider).writeToDiskCache(anyLong(), any())
+ assertTrue(provider.isCacheExpired(false))
+ }
+
+ @Test
+ fun `WHEN the base cache file getter is called THEN return existing base cache file`() {
+ val client = prepareClient()
+ val provider = spy(ContileTopSitesProvider(testContext, client))
+ val file = File(testContext.filesDir, CACHE_FILE_NAME)
+
+ file.createNewFile()
+
+ assertTrue(file.exists())
+
+ val cacheFile = provider.getBaseCacheFile()
+
+ assertTrue(cacheFile.exists())
+ assertEquals(file.name, cacheFile.name)
+
+ assertTrue(file.delete())
+ assertFalse(cacheFile.exists())
+ }
+
+ @Test
+ fun `WHEN the cache expiration is checked THEN return whether the cache is expired`() {
+ val provider =
+ spy(ContileTopSitesProvider(testContext, client = mock()))
+
+ provider.cacheState =
+ ContileTopSitesProvider.CacheState(isCacheValid = false)
+ assertTrue(provider.isCacheExpired(false))
+
+ provider.cacheState = ContileTopSitesProvider.CacheState(
+ isCacheValid = true,
+ localCacheMaxAge = Date().time - 60 * DateUtils.MINUTE_IN_MILLIS,
+ )
+ assertTrue(provider.isCacheExpired(false))
+
+ provider.cacheState = ContileTopSitesProvider.CacheState(
+ isCacheValid = true,
+ localCacheMaxAge = Date().time + 60 * DateUtils.MINUTE_IN_MILLIS,
+ )
+ assertFalse(provider.isCacheExpired(false))
+
+ provider.cacheState = ContileTopSitesProvider.CacheState(
+ isCacheValid = true,
+ serverCacheMaxAge = Date().time - 60 * DateUtils.MINUTE_IN_MILLIS,
+ )
+ assertTrue(provider.isCacheExpired(true))
+
+ provider.cacheState = ContileTopSitesProvider.CacheState(
+ isCacheValid = true,
+ serverCacheMaxAge = Date().time + 60 * DateUtils.MINUTE_IN_MILLIS,
+ )
+ assertFalse(provider.isCacheExpired(true))
+ }
+
+ @Test
+ fun `GIVEN cache is not expired WHEN top sites are refreshed THEN do nothing`() = runTest {
+ val provider = spy(
+ ContileTopSitesProvider(
+ testContext,
+ client = prepareClient(),
+ maxCacheAgeInSeconds = 600,
+ ),
+ )
+
+ whenever(provider.isCacheExpired(false)).thenReturn(false)
+ provider.refreshTopSitesIfCacheExpired()
+ verify(provider, never()).getTopSites(allowCache = false)
+ }
+
+ @Test
+ fun `GIVEN cache is expired WHEN top sites are refreshed THEN fetch and write new response to cache`() =
+ runTest {
+ val jsonResponse = loadResourceAsString("/contile/contile.json")
+ val provider = spy(
+ ContileTopSitesProvider(
+ testContext,
+ client = prepareClient(jsonResponse),
+ ),
+ )
+
+ whenever(provider.isCacheExpired(false)).thenReturn(true)
+
+ provider.refreshTopSitesIfCacheExpired()
+
+ verify(provider).getTopSites(allowCache = false)
+ verify(provider).writeToDiskCache(eq(300000L), any())
+ }
+
+ @Test
+ fun `GIVEN a NO_CONTENT status response WHEN top sites are fetched THEN cache is cleared`() = runTest {
+ val client = prepareClient(status = Response.NO_CONTENT)
+ val provider = spy(ContileTopSitesProvider(testContext, client))
+ val file = mock<File>()
+
+ whenever(provider.isCacheExpired(false)).thenReturn(true)
+ whenever(provider.getBaseCacheFile()).thenReturn(file)
+ provider.refreshTopSitesIfCacheExpired()
+
+ verify(file).delete()
+ assertNull(provider.cacheState.localCacheMaxAge)
+ assertNull(provider.cacheState.serverCacheMaxAge)
+ }
+
+ private fun prepareClient(
+ jsonResponse: String = loadResourceAsString("/contile/contile.json"),
+ status: Int = 200,
+ headers: Headers = MutableHeaders(
+ listOf(
+ Header("cache-control", "max-age=100"),
+ Header("cache-control", "stale-if-error=200"),
+ ),
+ ),
+ ): Client {
+ val mockedClient = mock<Client>()
+ val mockedResponse = mock<Response>()
+ val mockedBody = mock<Response.Body>()
+
+ whenever(mockedBody.string(any())).thenReturn(jsonResponse)
+ whenever(mockedResponse.body).thenReturn(mockedBody)
+ whenever(mockedResponse.status).thenReturn(status)
+ whenever(mockedResponse.headers).thenReturn(headers)
+ whenever(mockedClient.fetch(any())).thenReturn(mockedResponse)
+
+ return mockedClient
+ }
+
+ @Test
+ fun `WHEN extracting top sites from a json object contains top sites THEN all top sites are correctly set`() {
+ val topSites = with(
+ TopSite.Provided(
+ 1,
+ "firefox",
+ "www.mozilla.com",
+ "www.mozilla.com",
+ "www.mozilla.com",
+ "www.mozilla.com",
+ null,
+ ),
+ ) {
+ listOf(this, this.copy(id = 2))
+ }
+
+ val jsonObject = JSONObject(
+ mapOf(
+ CACHE_TOP_SITES_KEY to JSONArray().also { array ->
+ topSites.map { it.toJsonObject() }.forEach { array.put(it) }
+ },
+ ),
+ )
+
+ assertEquals(topSites, jsonObject.getTopSites())
+ }
+
+ private fun TopSite.Provided.toJsonObject() =
+ JSONObject()
+ .put("id", id)
+ .put("name", title)
+ .put("url", url)
+ .put("click_url", clickUrl)
+ .put("image_url", imageUrl)
+ .put("impression_url", impressionUrl)
+}
diff --git a/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt b/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt
new file mode 100644
index 0000000000..9641b1a5d6
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt
@@ -0,0 +1,97 @@
+/* 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.service.contile
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.Configuration
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.await
+import androidx.work.testing.WorkManagerTestInitHelper
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.contile.ContileTopSitesUpdater.Companion.PERIODIC_WORK_TAG
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+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
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class ContileTopSitesUpdaterTest {
+
+ @Before
+ fun setUp() {
+ WorkManagerTestInitHelper.initializeTestWorkManager(
+ testContext,
+ Configuration.Builder().build(),
+ )
+ }
+
+ @After
+ fun tearDown() {
+ WorkManager.getInstance(testContext).cancelUniqueWork(PERIODIC_WORK_TAG)
+ }
+
+ @Test
+ fun `WHEN periodic work is started THEN work is queued`() = runTest {
+ val updater = ContileTopSitesUpdater(testContext, provider = mock())
+ val workManager = WorkManager.getInstance(testContext)
+ var workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
+
+ assertTrue(workInfo.isEmpty())
+ assertNull(ContileTopSitesUseCases.provider)
+
+ updater.startPeriodicWork()
+
+ assertNotNull(ContileTopSitesUseCases.provider)
+ assertNotNull(ContileTopSitesUseCases.requireContileTopSitesProvider())
+
+ workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
+ val work = workInfo.first()
+
+ assertEquals(1, workInfo.size)
+ assertEquals(WorkInfo.State.ENQUEUED, work.state)
+ assertTrue(work.tags.contains(PERIODIC_WORK_TAG))
+ }
+
+ @Test
+ fun `GIVEN periodic work is started WHEN period work is stopped THEN no work is queued`() = runTest {
+ val updater = ContileTopSitesUpdater(testContext, provider = mock())
+ val workManager = WorkManager.getInstance(testContext)
+ var workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
+
+ assertTrue(workInfo.isEmpty())
+
+ updater.startPeriodicWork()
+
+ workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
+
+ assertEquals(1, workInfo.size)
+
+ updater.stopPeriodicWork()
+
+ workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
+ val work = workInfo.first()
+
+ assertNull(ContileTopSitesUseCases.provider)
+ assertEquals(WorkInfo.State.CANCELLED, work.state)
+ }
+
+ @Test
+ fun `WHEN period work request is created THEN it contains the correct constraints`() {
+ val updater = ContileTopSitesUpdater(testContext, provider = mock())
+ val workRequest = updater.createPeriodicWorkRequest()
+
+ assertTrue(workRequest.tags.contains(PERIODIC_WORK_TAG))
+ assertEquals(updater.getWorkerConstraints(), workRequest.workSpec.constraints)
+ }
+}
diff --git a/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt b/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt
new file mode 100644
index 0000000000..e4f2e7f63c
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt
@@ -0,0 +1,69 @@
+/* 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.service.contile
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.spy
+import java.io.IOException
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class ContileTopSitesUpdaterWorkerTest {
+
+ @After
+ fun cleanup() {
+ ContileTopSitesUseCases.destroy()
+ }
+
+ @Test
+ fun `WHEN worker does successful work THEN return a success result`() = runTest {
+ val provider: ContileTopSitesProvider = mock()
+ val worker = spy(
+ TestListenableWorkerBuilder<ContileTopSitesUpdaterWorker>(testContext)
+ .build(),
+ )
+
+ ContileTopSitesUseCases.initialize(provider)
+
+ whenever(provider.getTopSites(anyBoolean())).thenReturn(emptyList())
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.success(), result)
+ }
+
+ @Test
+ fun `WHEN worker does unsuccessful work THEN return a failure result`() = runTest {
+ val provider: ContileTopSitesProvider = mock()
+ val worker = spy(
+ TestListenableWorkerBuilder<ContileTopSitesUpdaterWorker>(testContext)
+ .build(),
+ )
+ val throwable = IOException("test")
+
+ ContileTopSitesUseCases.initialize(provider)
+
+ whenever(provider.getTopSites(anyBoolean())).then {
+ throw throwable
+ }
+
+ val result = worker.startWork().await()
+
+ assertEquals(ListenableWorker.Result.failure(), result)
+ }
+}
diff --git a/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt b/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt
new file mode 100644
index 0000000000..354367f2a1
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt
@@ -0,0 +1,48 @@
+/* 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.service.contile
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.verify
+import java.io.IOException
+
+@ExperimentalCoroutinesApi // for runTest
+class ContileTopSitesUseCasesTest {
+
+ @Test
+ fun `WHEN refresh contile top site use case is called THEN call the provider to fetch top sites bypassing the cache`() = runTest {
+ val provider: ContileTopSitesProvider = mock()
+
+ ContileTopSitesUseCases.initialize(provider)
+
+ whenever(provider.getTopSites(anyBoolean())).thenReturn(emptyList())
+
+ ContileTopSitesUseCases().refreshContileTopSites.invoke()
+
+ verify(provider).getTopSites(eq(false))
+
+ Unit
+ }
+
+ @Test(expected = IOException::class)
+ fun `GIVEN the provider fails to fetch contile top sites WHEN refresh contile top site use case is called THEN an exception is thrown`() = runTest {
+ val provider: ContileTopSitesProvider = mock()
+ val throwable = IOException("test")
+
+ ContileTopSitesUseCases.initialize(provider)
+
+ whenever(provider.getTopSites(anyBoolean())).then {
+ throw throwable
+ }
+
+ ContileTopSitesUseCases().refreshContileTopSites.invoke()
+ }
+}
diff --git a/mobile/android/android-components/components/service/contile/src/test/resources/contile/contile.json b/mobile/android/android-components/components/service/contile/src/test/resources/contile/contile.json
new file mode 100644
index 0000000000..7668717e10
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/test/resources/contile/contile.json
@@ -0,0 +1,24 @@
+{
+ "tiles": [
+ {
+ "id": 1,
+ "name": "Firefox",
+ "url": "https://firefox.com",
+ "click_url": "https://firefox.com/click",
+ "image_url": "https://test.com/image1.jpg",
+ "image_size": 200,
+ "impression_url": "https://test.com",
+ "position": 1
+ },
+ {
+ "id": 2,
+ "name": "Mozilla",
+ "url": "https://mozilla.com",
+ "click_url": "https://mozilla.com/click",
+ "image_url": "https://test.com/image2.jpg",
+ "image_size": 200,
+ "impression_url": "https://example.com",
+ "position": 2
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/service/contile/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/contile/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/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/service/contile/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/contile/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/contile/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/README.md b/mobile/android/android-components/components/service/digitalassetlinks/README.md
new file mode 100644
index 0000000000..f130dfd7a5
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/README.md
@@ -0,0 +1,40 @@
+# [Android Components](../../../README.md) > Service > Digital Asset Links
+
+A library for communicating with the [Digital Asset Links](https://developers.google.com/digital-asset-links) API.
+
+## Usage
+
+### 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:service-digital-asset-links:{latest-version}"
+```
+
+### Obtaining an AssetDescriptor
+
+For web sites, asset descriptors can be obtained by simply passing the origin into the `AssetDescriptor.Web` constructor.
+
+```kotlin
+AssetDescriptor.Web(
+ site = "https://{fully-qualified domain}{:optional port}"
+)
+```
+
+For Android apps, a fingerprint corresponding to the Android app must be used. This can be obtained using the `AndroidAssetFinder` class.
+
+### Remote API
+
+The `DigitalAssetLinksApi` class will handle checking asset links by calling [Google's remote API](https://developers.google.com/digital-asset-links/reference/rest). An API key must be given to the class.
+
+### Local API
+
+The `StatementRelationChecker` class will handle checking asset links on device by fetching and iterating through asset link statements located on a website. Either the `StatementApi` or `DigitalAssetLinksApi` classes may be used to obtain a statement list.
+
+## 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/service/digitalassetlinks/build.gradle b/mobile/android/android-components/components/service/digitalassetlinks/build.gradle
new file mode 100644
index 0000000000..14c2c992f2
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/build.gradle
@@ -0,0 +1,42 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.service.digitalassetlinks'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ implementation project(':concept-fetch')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':support-test')
+}
+
+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/service/digitalassetlinks/proguard-rules.pro b/mobile/android/android-components/components/service/digitalassetlinks/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/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/service/digitalassetlinks/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/digitalassetlinks/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/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/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinder.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinder.kt
new file mode 100644
index 0000000000..bd4e497d34
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinder.kt
@@ -0,0 +1,128 @@
+/* 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.service.digitalassetlinks
+
+import android.annotation.SuppressLint
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.Signature
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.utils.ext.getPackageInfoCompat
+import java.io.ByteArrayInputStream
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
+import java.security.cert.CertificateEncodingException
+import java.security.cert.CertificateException
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+
+/**
+ * Get the SHA256 certificate for an installed Android app.
+ */
+class AndroidAssetFinder {
+
+ /**
+ * Converts the Android App with the given package name into an asset descriptor
+ * by computing the SHA256 certificate for each signing signature.
+ *
+ * The output is lazily computed. If desired, only the first item from the sequence could
+ * be used and other certificates (if any) will not be computed.
+ */
+ fun getAndroidAppAsset(
+ packageName: String,
+ packageManager: PackageManager,
+ ): Sequence<AssetDescriptor.Android> {
+ return packageManager.getSignatures(packageName).asSequence()
+ .mapNotNull { signature -> getCertificateSHA256Fingerprint(signature) }
+ .map { fingerprint -> AssetDescriptor.Android(packageName, fingerprint) }
+ }
+
+ /**
+ * Computes the SHA256 certificate for the given package name. The app with the given package
+ * name has to be installed on device. The output will be a 30 long HEX string with : between
+ * each value.
+ * @return The SHA256 certificate for the package name.
+ */
+ @VisibleForTesting
+ internal fun getCertificateSHA256Fingerprint(signature: Signature): String? {
+ val input = ByteArrayInputStream(signature.toByteArray())
+ return try {
+ val certificate = CertificateFactory.getInstance("X509").generateCertificate(input) as X509Certificate
+ byteArrayToHexString(MessageDigest.getInstance("SHA256").digest(certificate.encoded))
+ } catch (e: CertificateEncodingException) {
+ // Certificate type X509 encoding failed
+ null
+ } catch (e: CertificateException) {
+ throw AssertionError("Should not happen", e)
+ } catch (e: NoSuchAlgorithmException) {
+ throw AssertionError("Should not happen", e)
+ }
+ }
+
+ @Suppress("PackageManagerGetSignatures")
+ // https://stackoverflow.com/questions/39192844/android-studio-warning-when-using-packagemanager-get-signatures
+ private fun PackageManager.getSignatures(packageName: String): Array<Signature> {
+ val packageInfo = getPackageSignatureInfo(packageName) ?: return emptyArray()
+
+ return if (SDK_INT >= Build.VERSION_CODES.P) {
+ val signingInfo = packageInfo.signingInfo
+ if (signingInfo.hasMultipleSigners()) {
+ signingInfo.apkContentsSigners
+ } else {
+ val history = signingInfo.signingCertificateHistory
+ if (history.isEmpty()) {
+ emptyArray()
+ } else {
+ arrayOf(history.first())
+ }
+ }
+ } else {
+ @Suppress("Deprecation")
+ packageInfo.signatures
+ }
+ }
+
+ @SuppressLint("PackageManagerGetSignatures")
+ private fun PackageManager.getPackageSignatureInfo(packageName: String): PackageInfo? {
+ return try {
+ if (SDK_INT >= Build.VERSION_CODES.P) {
+ getPackageInfoCompat(packageName, PackageManager.GET_SIGNING_CERTIFICATES)
+ } else {
+ @Suppress("Deprecation")
+ getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ // Will return null if there is no package found.
+ return null
+ }
+ }
+
+ /**
+ * Converts a byte array to hex string with : inserted between each element.
+ * @param bytes The array to be converted.
+ * @return A string with two letters representing each byte and : in between.
+ */
+ @Suppress("MagicNumber")
+ @VisibleForTesting
+ internal fun byteArrayToHexString(bytes: ByteArray): String {
+ val hexString = StringBuilder(bytes.size * HEX_STRING_SIZE - 1)
+ var v: Int
+ for (j in bytes.indices) {
+ v = bytes[j].toInt() and 0xFF
+ hexString.append(HEX_CHAR_LOOKUP[v.ushr(HALF_BYTE)])
+ hexString.append(HEX_CHAR_LOOKUP[v and 0x0F])
+ if (j < bytes.lastIndex) hexString.append(':')
+ }
+ return hexString.toString()
+ }
+
+ companion object {
+ private const val HALF_BYTE = 4
+ private const val HEX_STRING_SIZE = "0F:".length
+ private val HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray()
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/AssetDescriptor.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/AssetDescriptor.kt
new file mode 100644
index 0000000000..a3f81799e2
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/AssetDescriptor.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.service.digitalassetlinks
+
+/**
+ * Uniquely identifies an asset.
+ *
+ * A digital asset is an identifiable and addressable online entity that typically provides some
+ * service or content.
+ */
+sealed class AssetDescriptor {
+
+ /**
+ * A web site asset descriptor.
+ * @property site URI representing the domain of the website.
+ * @sample
+ * AssetDescriptor.Web(
+ * site = "https://{fully-qualified domain}{:optional port}"
+ * )
+ */
+ data class Web(
+ val site: String,
+ ) : AssetDescriptor()
+
+ /**
+ * An Android app asset descriptor.
+ * @property packageName Package name for the Android app.
+ * @property sha256CertFingerprint A colon-separated hex string.
+ * @sample
+ * AssetDescriptor.Android(
+ * packageName = "com.costingtons.app",
+ * sha256CertFingerprint = "A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
+ * )
+ */
+ data class Android(
+ val packageName: String,
+ val sha256CertFingerprint: String,
+ ) : AssetDescriptor()
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Constants.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Constants.kt
new file mode 100644
index 0000000000..e1860c0463
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Constants.kt
@@ -0,0 +1,10 @@
+/* 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.service.digitalassetlinks
+
+import java.util.concurrent.TimeUnit
+
+@Suppress("MagicNumber", "TopLevelPropertyNaming")
+internal val TIMEOUT = 3L to TimeUnit.SECONDS
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Relation.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Relation.kt
new file mode 100644
index 0000000000..2cc4c32429
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Relation.kt
@@ -0,0 +1,29 @@
+/* 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.service.digitalassetlinks
+
+/**
+ * Describes the nature of a statement, and consists of a kind and a detail.
+ * @property kindAndDetail Kind and detail, separated by a slash character.
+ */
+enum class Relation(val kindAndDetail: String) {
+
+ /**
+ * Grants the target permission to retrieve sign-in credentials stored for the source.
+ * For App -> Web transitions, requests the app to use the declared origin to be used as origin
+ * for the client app in the web APIs context.
+ */
+ USE_AS_ORIGIN("delegate_permission/common.use_as_origin"),
+
+ /**
+ * Requests the ability to handle all URLs from a given origin.
+ */
+ HANDLE_ALL_URLS("delegate_permission/common.handle_all_urls"),
+
+ /**
+ * Grants the target permission to retrieve sign-in credentials stored for the source.
+ */
+ GET_LOGIN_CREDS("delegate_permission/common.get_login_creds"),
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/RelationChecker.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/RelationChecker.kt
new file mode 100644
index 0000000000..7134b6cd68
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/RelationChecker.kt
@@ -0,0 +1,21 @@
+/* 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.service.digitalassetlinks
+
+/**
+ * Verifies that a source is linked to a target.
+ */
+interface RelationChecker {
+
+ /**
+ * Performs a check to ensure a directional relationships exists between the specified
+ * [source] and [target] assets. The relationship must match the [relation] type given.
+ */
+ fun checkRelationship(
+ source: AssetDescriptor.Web,
+ relation: Relation,
+ target: AssetDescriptor,
+ ): Boolean
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Statement.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Statement.kt
new file mode 100644
index 0000000000..5686e74375
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/Statement.kt
@@ -0,0 +1,25 @@
+/* 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.service.digitalassetlinks
+
+/**
+ * Represents a statement that can be found in an assetlinks.json file.
+ */
+sealed class StatementResult
+
+/**
+ * Entry in a Digital Asset Links statement file.
+ */
+data class Statement(
+ val relation: Relation,
+ val target: AssetDescriptor,
+) : StatementResult()
+
+/**
+ * Include statements point to another Digital Asset Links statement file.
+ */
+data class IncludeStatement(
+ val include: String,
+) : StatementResult()
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/StatementListFetcher.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/StatementListFetcher.kt
new file mode 100644
index 0000000000..435332635f
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/StatementListFetcher.kt
@@ -0,0 +1,16 @@
+/* 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.service.digitalassetlinks
+
+/**
+ * Lists all statements made by a given source.
+ */
+interface StatementListFetcher {
+
+ /**
+ * Retrieves a list of all statements from a given [source].
+ */
+ fun listStatements(source: AssetDescriptor.Web): Sequence<Statement>
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/CheckAssetLinksResponse.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/CheckAssetLinksResponse.kt
new file mode 100644
index 0000000000..1ab14d687e
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/CheckAssetLinksResponse.kt
@@ -0,0 +1,25 @@
+/* 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.service.digitalassetlinks.api
+
+import org.json.JSONObject
+
+/**
+ * @property linked True if the assets specified in the request are linked by the relation specified in the request.
+ * @property maxAge From serving time, how much longer the response should be considered valid barring further updates.
+ * Formatted as a duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s".
+ * @property debug Human-readable message containing information about the response.
+ */
+data class CheckAssetLinksResponse(
+ val linked: Boolean,
+ val maxAge: String,
+ val debug: String,
+)
+
+internal fun parseCheckAssetLinksJson(json: JSONObject) = CheckAssetLinksResponse(
+ linked = json.getBoolean("linked"),
+ maxAge = json.getString("maxAge"),
+ debug = json.optString("debugString"),
+)
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApi.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApi.kt
new file mode 100644
index 0000000000..c1a09e06c2
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApi.kt
@@ -0,0 +1,120 @@
+/* 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.service.digitalassetlinks.api
+
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.StatementListFetcher
+import mozilla.components.service.digitalassetlinks.TIMEOUT
+import mozilla.components.service.digitalassetlinks.ext.parseJsonBody
+import mozilla.components.service.digitalassetlinks.ext.safeFetch
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import org.json.JSONObject
+
+/**
+ * Digital Asset Links allows any caller to check pre declared relationships between
+ * two assets which can be either web domains or native applications.
+ * This class checks for a specific relationship declared by two assets via the online API.
+ */
+class DigitalAssetLinksApi(
+ private val httpClient: Client,
+ private val apiKey: String?,
+) : RelationChecker, StatementListFetcher {
+
+ override fun checkRelationship(
+ source: AssetDescriptor.Web,
+ relation: Relation,
+ target: AssetDescriptor,
+ ): Boolean {
+ val request = buildCheckApiRequest(source, relation, target)
+ val response = httpClient.safeFetch(request)
+ val parsed = response?.parseJsonBody { body ->
+ parseCheckAssetLinksJson(JSONObject(body))
+ }
+ return parsed?.linked == true
+ }
+
+ override fun listStatements(source: AssetDescriptor.Web): Sequence<Statement> {
+ val request = buildListApiRequest(source)
+ val response = httpClient.safeFetch(request)
+ val parsed = response?.parseJsonBody { body ->
+ parseListStatementsJson(JSONObject(body))
+ }
+ return parsed?.statements.orEmpty().asSequence()
+ }
+
+ private fun apiUrlBuilder(path: String) = BASE_URL.toUri().buildUpon()
+ .encodedPath(path)
+ .appendQueryParameter("prettyPrint", false.toString())
+ .appendQueryParameter("key", apiKey)
+
+ /**
+ * Returns a [Request] used to check whether the specified (directional) relationship exists
+ * between the specified source and target assets.
+ *
+ * https://developers.google.com/digital-asset-links/reference/rest/v1/assetlinks/check
+ */
+ @VisibleForTesting
+ internal fun buildCheckApiRequest(
+ source: AssetDescriptor,
+ relation: Relation,
+ target: AssetDescriptor,
+ ): Request {
+ val uriBuilder = apiUrlBuilder(CHECK_PATH)
+ .appendQueryParameter("relation", relation.kindAndDetail)
+
+ // source and target follow the same format, so re-use the query logic for both.
+ uriBuilder.appendAssetAsQuery(source, "source")
+ uriBuilder.appendAssetAsQuery(target, "target")
+
+ return Request(
+ url = uriBuilder.build().toString().sanitizeURL(),
+ method = Request.Method.GET,
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun buildListApiRequest(source: AssetDescriptor): Request {
+ val uriBuilder = apiUrlBuilder(LIST_PATH)
+ uriBuilder.appendAssetAsQuery(source, "source")
+
+ return Request(
+ url = uriBuilder.build().toString().sanitizeURL(),
+ method = Request.Method.GET,
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ )
+ }
+
+ private fun Uri.Builder.appendAssetAsQuery(asset: AssetDescriptor, prefix: String) {
+ when (asset) {
+ is AssetDescriptor.Web -> {
+ appendQueryParameter("$prefix.web.site", asset.site)
+ }
+ is AssetDescriptor.Android -> {
+ appendQueryParameter("$prefix.androidApp.packageName", asset.packageName)
+ appendQueryParameter(
+ "$prefix.androidApp.certificate.sha256Fingerprint",
+ asset.sha256CertFingerprint,
+ )
+ }
+ }
+ }
+
+ companion object {
+ private const val BASE_URL = "https://digitalassetlinks.googleapis.com"
+ private const val CHECK_PATH = "/v1/assetlinks:check"
+ private const val LIST_PATH = "/v1/statements:list"
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/ListStatementsResponse.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/ListStatementsResponse.kt
new file mode 100644
index 0000000000..4a9106be9f
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/api/ListStatementsResponse.kt
@@ -0,0 +1,56 @@
+/* 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.service.digitalassetlinks.api
+
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.support.ktx.android.org.json.asSequence
+import org.json.JSONObject
+
+/**
+ * @property statements A list of all the matching statements that have been found.
+ * @property maxAge From serving time, how much longer the response should be considered valid barring further updates.
+ * Formatted as a duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s".
+ * @property debug Human-readable message containing information about the response.
+ */
+data class ListStatementsResponse(
+ val statements: List<Statement>,
+ val maxAge: String,
+ val debug: String,
+)
+
+internal fun parseListStatementsJson(json: JSONObject): ListStatementsResponse {
+ val statements = json.getJSONArray("statements")
+ .asSequence { i -> getJSONObject(i) }
+ .mapNotNull { statementJson ->
+ val relationString = statementJson.getString("relation")
+ val relation = Relation.values().find { relationString == it.kindAndDetail }
+
+ val targetJson = statementJson.getJSONObject("target")
+ val webJson = targetJson.optJSONObject("web")
+ val androidJson = targetJson.optJSONObject("androidApp")
+ val target = when {
+ webJson != null -> AssetDescriptor.Web(site = webJson.getString("site"))
+ androidJson != null -> AssetDescriptor.Android(
+ packageName = androidJson.getString("packageName"),
+ sha256CertFingerprint = androidJson.getJSONObject("certificate")
+ .getString("sha256Fingerprint"),
+ )
+ else -> null
+ }
+
+ if (relation != null && target != null) {
+ Statement(relation, target)
+ } else {
+ null
+ }
+ }
+ return ListStatementsResponse(
+ statements = statements.toList(),
+ maxAge = json.getString("maxAge"),
+ debug = json.optString("debugString"),
+ )
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/ext/Client.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/ext/Client.kt
new file mode 100644
index 0000000000..cbdcf5e14a
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/ext/Client.kt
@@ -0,0 +1,20 @@
+/* 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.service.digitalassetlinks.ext
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.Response.Companion.SUCCESS
+import java.io.IOException
+
+internal fun Client.safeFetch(request: Request): Response? {
+ return try {
+ val response = fetch(request)
+ if (response.status == SUCCESS) response else null
+ } catch (e: IOException) {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/ext/Response.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/ext/Response.kt
new file mode 100644
index 0000000000..36a1d82afc
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/ext/Response.kt
@@ -0,0 +1,20 @@
+/* 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.service.digitalassetlinks.ext
+
+import mozilla.components.concept.fetch.Response
+import org.json.JSONException
+
+/**
+ * Safely parse a JSON [Response] returned by an API.
+ */
+inline fun <T> Response.parseJsonBody(crossinline parser: (String) -> T): T? {
+ val responseBody = use { body.string() }
+ return try {
+ parser(responseBody)
+ } catch (e: JSONException) {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/local/StatementApi.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/local/StatementApi.kt
new file mode 100644
index 0000000000..798650af34
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/local/StatementApi.kt
@@ -0,0 +1,143 @@
+/* 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.service.digitalassetlinks.local
+
+import androidx.core.net.toUri
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.concept.fetch.Headers.Values.CONTENT_TYPE_APPLICATION_JSON
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.IncludeStatement
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.StatementListFetcher
+import mozilla.components.service.digitalassetlinks.StatementResult
+import mozilla.components.service.digitalassetlinks.TIMEOUT
+import mozilla.components.service.digitalassetlinks.ext.safeFetch
+import mozilla.components.support.ktx.android.org.json.asSequence
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+/**
+ * Builds a list of statements by sending HTTP requests to the given website.
+ */
+class StatementApi(private val httpClient: Client) : StatementListFetcher {
+
+ /**
+ * Lazily list all the statements in the given [source] website.
+ * If include statements are present, they will be resolved lazily.
+ */
+ override fun listStatements(source: AssetDescriptor.Web): Sequence<Statement> {
+ val url = source.site.toUri().buildUpon()
+ .path("/.well-known/assetlinks.json")
+ .build()
+ .toString()
+ return getWebsiteStatementList(url, seenSoFar = mutableSetOf())
+ }
+
+ /**
+ * Recursively download all the website statements.
+ * @param assetLinksUrl URL to download.
+ * @param seenSoFar URLs that have been downloaded already.
+ * Used to prevent infinite loops.
+ */
+ private fun getWebsiteStatementList(
+ assetLinksUrl: String,
+ seenSoFar: MutableSet<String>,
+ ): Sequence<Statement> {
+ if (assetLinksUrl in seenSoFar) {
+ return emptySequence()
+ } else {
+ seenSoFar.add(assetLinksUrl)
+ }
+
+ val request = Request(
+ url = assetLinksUrl.sanitizeURL(),
+ method = Request.Method.GET,
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ )
+ val response = httpClient.safeFetch(request)?.let { res ->
+ val contentTypes = res.headers.getAll(CONTENT_TYPE)
+ if (contentTypes.any { it.contains(CONTENT_TYPE_APPLICATION_JSON, ignoreCase = true) }) {
+ res
+ } else {
+ res.close()
+ null
+ }
+ }
+
+ val statements = response?.let { parseStatementResponse(response) }.orEmpty()
+ return sequence<Statement> {
+ val includeStatements = mutableListOf<IncludeStatement>()
+ // Yield all statements that have already been downloaded
+ statements.forEach { statement ->
+ when (statement) {
+ is Statement -> yield(statement)
+ is IncludeStatement -> includeStatements.add(statement)
+ }
+ }
+ // Recursively download include statements
+ yieldAll(
+ includeStatements.asSequence().flatMap {
+ getWebsiteStatementList(it.include, seenSoFar)
+ },
+ )
+ }
+ }
+
+ /**
+ * Parse the JSON [Response] returned by the website.
+ */
+ private fun parseStatementResponse(response: Response): List<StatementResult> {
+ val responseBody = response.use { response.body.string() }
+ return try {
+ val responseJson = JSONArray(responseBody)
+ parseStatementListJson(responseJson)
+ } catch (e: JSONException) {
+ emptyList()
+ }
+ }
+
+ private fun parseStatementListJson(json: JSONArray): List<StatementResult> {
+ return json.asSequence { i -> getJSONObject(i) }
+ .flatMap { parseStatementJson(it) }
+ .toList()
+ }
+
+ private fun parseStatementJson(json: JSONObject): Sequence<StatementResult> {
+ val include = json.optString("include")
+ if (include.isNotEmpty()) {
+ return sequenceOf(IncludeStatement(include))
+ }
+
+ val relationTypes = Relation.values()
+ val relations = json.getJSONArray("relation")
+ .asSequence { i -> getString(i) }
+ .mapNotNull { relation -> relationTypes.find { relation == it.kindAndDetail } }
+
+ return relations.flatMap { relation ->
+ val target = json.getJSONObject("target")
+ val assets = when (target.getString("namespace")) {
+ "web" -> sequenceOf(
+ AssetDescriptor.Web(site = target.getString("site")),
+ )
+ "android_app" -> {
+ val packageName = target.getString("package_name")
+ target.getJSONArray("sha256_cert_fingerprints")
+ .asSequence { i -> getString(i) }
+ .map { fingerprint -> AssetDescriptor.Android(packageName, fingerprint) }
+ }
+ else -> emptySequence()
+ }
+
+ assets.map { asset -> Statement(relation, asset) }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/local/StatementRelationChecker.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/local/StatementRelationChecker.kt
new file mode 100644
index 0000000000..2dc3548f7b
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/main/java/mozilla/components/service/digitalassetlinks/local/StatementRelationChecker.kt
@@ -0,0 +1,35 @@
+/* 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.service.digitalassetlinks.local
+
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.StatementListFetcher
+
+/**
+ * Checks if a matching relationship is present in a remote statement list.
+ */
+class StatementRelationChecker(
+ private val listFetcher: StatementListFetcher,
+) : RelationChecker {
+
+ override fun checkRelationship(source: AssetDescriptor.Web, relation: Relation, target: AssetDescriptor): Boolean {
+ val statements = listFetcher.listStatements(source)
+ return checkLink(statements, relation, target)
+ }
+
+ companion object {
+
+ /**
+ * Check if any of the given [Statement]s are linked to the given [target].
+ */
+ fun checkLink(statements: Sequence<Statement>, relation: Relation, target: AssetDescriptor) =
+ statements.any { statement ->
+ statement.relation == relation && statement.target == target
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinderTest.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinderTest.kt
new file mode 100644
index 0000000000..32c244286d
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinderTest.kt
@@ -0,0 +1,170 @@
+/* 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.service.digitalassetlinks
+
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.Signature
+import android.content.pm.SigningInfo
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class AndroidAssetFinderTest {
+
+ private lateinit var assetFinder: AndroidAssetFinder
+ private lateinit var packageInfo: PackageInfo
+
+ @Mock lateinit var packageManager: PackageManager
+
+ @Mock lateinit var signingInfo: SigningInfo
+
+ @Before
+ fun setup() {
+ assetFinder = spy(AndroidAssetFinder())
+
+ MockitoAnnotations.openMocks(this)
+ packageInfo = PackageInfo()
+ @Suppress("DEPRECATION")
+ `when`(packageManager.getPackageInfo(anyString(), anyInt())).thenReturn(packageInfo)
+ }
+
+ @Test
+ fun `test getAndroidAppAsset returns empty list if name not found`() {
+ @Suppress("DEPRECATION")
+ `when`(packageManager.getPackageInfo(anyString(), anyInt()))
+ .thenThrow(PackageManager.NameNotFoundException::class.java)
+
+ assertEquals(
+ emptyList<AssetDescriptor.Android>(),
+ assetFinder.getAndroidAppAsset("com.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.P])
+ @Test
+ fun `test getAndroidAppAsset on P SDK`() {
+ val signature = mock<Signature>()
+ packageInfo.signingInfo = signingInfo
+ `when`(signingInfo.hasMultipleSigners()).thenReturn(false)
+ `when`(signingInfo.signingCertificateHistory).thenReturn(arrayOf(signature, mock()))
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature)
+
+ assertEquals(
+ listOf(AssetDescriptor.Android("com.test.app", "01:BB:AA:10:30")),
+ assetFinder.getAndroidAppAsset("com.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.P])
+ @Test
+ fun `test getAndroidAppAsset with multiple signatures on P SDK`() {
+ val signature1 = mock<Signature>()
+ val signature2 = mock<Signature>()
+ packageInfo.signingInfo = signingInfo
+ `when`(signingInfo.hasMultipleSigners()).thenReturn(true)
+ `when`(signingInfo.apkContentsSigners).thenReturn(arrayOf(signature1, signature2))
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature1)
+ doReturn("FF:CC:AA:99:77").`when`(assetFinder).getCertificateSHA256Fingerprint(signature2)
+
+ assertEquals(
+ listOf(
+ AssetDescriptor.Android("org.test.app", "01:BB:AA:10:30"),
+ AssetDescriptor.Android("org.test.app", "FF:CC:AA:99:77"),
+ ),
+ assetFinder.getAndroidAppAsset("org.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.P])
+ @Test
+ fun `test getAndroidAppAsset with empty history`() {
+ packageInfo.signingInfo = signingInfo
+ `when`(signingInfo.hasMultipleSigners()).thenReturn(false)
+ `when`(signingInfo.signingCertificateHistory).thenReturn(emptyArray())
+
+ assertEquals(
+ emptyList<AssetDescriptor.Android>(),
+ assetFinder.getAndroidAppAsset("com.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Suppress("Deprecation")
+ @Test
+ fun `test getAndroidAppAsset on deprecated SDK`() {
+ val signature = mock<Signature>()
+ packageInfo.signatures = arrayOf(signature)
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature)
+
+ assertEquals(
+ listOf(AssetDescriptor.Android("com.test.app", "01:BB:AA:10:30")),
+ assetFinder.getAndroidAppAsset("com.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Suppress("Deprecation")
+ @Test
+ fun `test getAndroidAppAsset with multiple signatures on deprecated SDK`() {
+ val signature1 = mock<Signature>()
+ val signature2 = mock<Signature>()
+ packageInfo.signatures = arrayOf(signature1, signature2)
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature1)
+ doReturn("FF:CC:AA:99:77").`when`(assetFinder).getCertificateSHA256Fingerprint(signature2)
+
+ assertEquals(
+ listOf(
+ AssetDescriptor.Android("org.test.app", "01:BB:AA:10:30"),
+ AssetDescriptor.Android("org.test.app", "FF:CC:AA:99:77"),
+ ),
+ assetFinder.getAndroidAppAsset("org.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Suppress("Deprecation")
+ @Test
+ fun `test getAndroidAppAsset is lazily computed`() {
+ val signature1 = mock<Signature>()
+ val signature2 = mock<Signature>()
+ packageInfo.signatures = arrayOf(signature1, signature2)
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature1)
+ doReturn("FF:CC:AA:99:77").`when`(assetFinder).getCertificateSHA256Fingerprint(signature2)
+
+ val result = assetFinder.getAndroidAppAsset("android.package", packageManager).first()
+ assertEquals(
+ AssetDescriptor.Android("android.package", "01:BB:AA:10:30"),
+ result,
+ )
+
+ verify(assetFinder, times(1)).getCertificateSHA256Fingerprint(any())
+ }
+
+ @Test
+ fun `test byteArrayToHexString`() {
+ val array = byteArrayOf(0xaa.toByte(), 0xbb.toByte(), 0xcc.toByte(), 0x10, 0x20, 0x30, 0x01, 0x02)
+ assertEquals(
+ "AA:BB:CC:10:20:30:01:02",
+ assetFinder.byteArrayToHexString(array),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApiTest.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApiTest.kt
new file mode 100644
index 0000000000..53418751b7
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApiTest.kt
@@ -0,0 +1,229 @@
+/* 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.service.digitalassetlinks.api
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.Response.Companion.SUCCESS
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation.HANDLE_ALL_URLS
+import mozilla.components.service.digitalassetlinks.Relation.USE_AS_ORIGIN
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.TIMEOUT
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class DigitalAssetLinksApiTest {
+
+ private val webAsset = AssetDescriptor.Web(site = "https://mozilla.org")
+ private val androidAsset = AssetDescriptor.Android(
+ packageName = "com.mozilla.fenix",
+ sha256CertFingerprint = "01:23:45:67:89",
+ )
+ private val baseRequest = Request(
+ url = "https://mozilla.org",
+ method = Request.Method.GET,
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ )
+ private val apiKey = "X"
+ private lateinit var client: Client
+ private lateinit var api: DigitalAssetLinksApi
+
+ @Before
+ fun setup() {
+ client = mock()
+ api = DigitalAssetLinksApi(client, apiKey)
+
+ doReturn(mockResponse("")).`when`(client).fetch(any())
+ }
+
+ @Test
+ fun `reject for invalid status`() {
+ val response = mockResponse("").copy(status = 400)
+ doReturn(response).`when`(client).fetch(any())
+
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+ assertEquals(emptyList<Statement>(), api.listStatements(webAsset).toList())
+ }
+
+ @Test
+ fun `reject check for invalid json`() {
+ doReturn(mockResponse("")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, webAsset))
+
+ doReturn(mockResponse("{}")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+
+ doReturn(mockResponse("[]")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+
+ doReturn(mockResponse("{\"lnkd\":true}")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+ }
+
+ @Test
+ fun `reject list for invalid json`() {
+ val empty = emptyList<Statement>()
+
+ doReturn(mockResponse("")).`when`(client).fetch(any())
+ assertEquals(empty, api.listStatements(webAsset).toList())
+
+ doReturn(mockResponse("{}")).`when`(client).fetch(any())
+ assertEquals(empty, api.listStatements(webAsset).toList())
+
+ doReturn(mockResponse("[]")).`when`(client).fetch(any())
+ assertEquals(empty, api.listStatements(webAsset).toList())
+
+ doReturn(mockResponse("{\"stmt\":[]}")).`when`(client).fetch(any())
+ assertEquals(empty, api.listStatements(webAsset).toList())
+ }
+
+ @Test
+ fun `return linked from json`() {
+ doReturn(mockResponse("{\"linked\":true,\"maxAge\":\"3s\"}")).`when`(client).fetch(any())
+ assertTrue(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+
+ doReturn(mockResponse("{\"linked\":false}\"maxAge\":\"3s\"}")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+ }
+
+ @Test
+ fun `return empty list if json doesn't match expected format`() {
+ val jsonPrefix = "{\"statements\":["
+ val jsonSuffix = "],\"maxAge\":\"3s\"}"
+ doReturn(mockResponse(jsonPrefix + jsonSuffix)).`when`(client).fetch(any())
+ assertEquals(emptyList<Statement>(), api.listStatements(webAsset).toList())
+
+ val invalidRelation = """
+ {
+ "source": {"web":{"site": "https://mozilla.org"}},
+ "target": {"web":{"site": "https://mozilla.org"}},
+ "relation": "not-a-relation"
+ }
+ """
+ doReturn(mockResponse(jsonPrefix + invalidRelation + jsonSuffix)).`when`(client).fetch(any())
+ assertEquals(emptyList<Statement>(), api.listStatements(webAsset).toList())
+
+ val invalidTarget = """
+ {
+ "source": {"web":{"site": "https://mozilla.org"}},
+ "target": {},
+ "relation": "delegate_permission/common.use_as_origin"
+ }
+ """
+ doReturn(mockResponse(jsonPrefix + invalidTarget + jsonSuffix)).`when`(client).fetch(any())
+ assertEquals(emptyList<Statement>(), api.listStatements(webAsset).toList())
+ }
+
+ @Test
+ fun `parses json statement list with web target`() {
+ val webStatement = """
+ {"statements": [{
+ "source": {"web":{"site": "https://mozilla.org"}},
+ "target": {"web":{"site": "https://mozilla.org"}},
+ "relation": "delegate_permission/common.use_as_origin"
+ }], "maxAge": "59s"}
+ """
+ doReturn(mockResponse(webStatement)).`when`(client).fetch(any())
+ assertEquals(
+ listOf(
+ Statement(
+ relation = USE_AS_ORIGIN,
+ target = webAsset,
+ ),
+ ),
+ api.listStatements(webAsset).toList(),
+ )
+ }
+
+ @Test
+ fun `parses json statement list with android target`() {
+ val androidStatement = """
+ {"statements": [{
+ "source": {"web":{"site": "https://mozilla.org"}},
+ "target": {"androidApp":{
+ "packageName": "com.mozilla.fenix",
+ "certificate": {"sha256Fingerprint": "01:23:45:67:89"}
+ }},
+ "relation": "delegate_permission/common.handle_all_urls"
+ }], "maxAge": "2m"}
+ """
+ doReturn(mockResponse(androidStatement)).`when`(client).fetch(any())
+ assertEquals(
+ listOf(
+ Statement(
+ relation = HANDLE_ALL_URLS,
+ target = androidAsset,
+ ),
+ ),
+ api.listStatements(webAsset).toList(),
+ )
+ }
+
+ @Test
+ fun `passes data in get check request URL for android target`() {
+ api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset)
+ verify(client).fetch(
+ baseRequest.copy(
+ url = "https://digitalassetlinks.googleapis.com/v1/assetlinks:check?" +
+ "prettyPrint=false&key=X&relation=delegate_permission%2Fcommon.use_as_origin&" +
+ "source.web.site=${Uri.encode("https://mozilla.org")}&" +
+ "target.androidApp.packageName=com.mozilla.fenix&" +
+ "target.androidApp.certificate.sha256Fingerprint=${Uri.encode("01:23:45:67:89")}",
+ ),
+ )
+ }
+
+ @Test
+ fun `passes data in get check request URL for web target`() {
+ api.checkRelationship(webAsset, HANDLE_ALL_URLS, webAsset)
+ verify(client).fetch(
+ baseRequest.copy(
+ url = "https://digitalassetlinks.googleapis.com/v1/assetlinks:check?" +
+ "prettyPrint=false&key=X&relation=delegate_permission%2Fcommon.handle_all_urls&" +
+ "source.web.site=${Uri.encode("https://mozilla.org")}&" +
+ "target.web.site=${Uri.encode("https://mozilla.org")}",
+ ),
+ )
+ }
+
+ @Test
+ fun `passes data in get list request URL`() {
+ api.listStatements(webAsset)
+ verify(client).fetch(
+ baseRequest.copy(
+ url = "https://digitalassetlinks.googleapis.com/v1/statements:list?" +
+ "prettyPrint=false&key=X&source.web.site=${Uri.encode("https://mozilla.org")}",
+ ),
+ )
+ }
+
+ private fun mockResponse(data: String) = Response(
+ url = "",
+ status = SUCCESS,
+ headers = MutableHeaders(),
+ body = mockBody(data),
+ )
+
+ private fun mockBody(data: String): Response.Body {
+ val mockBody: Response.Body = mock()
+ doReturn(data).`when`(mockBody).string()
+ return mockBody
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementApiTest.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementApiTest.kt
new file mode 100644
index 0000000000..7ebd8ac67e
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementApiTest.kt
@@ -0,0 +1,356 @@
+/* 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.service.digitalassetlinks.local
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.concept.fetch.Headers.Values.CONTENT_TYPE_APPLICATION_JSON
+import mozilla.components.concept.fetch.Headers.Values.CONTENT_TYPE_FORM_URLENCODED
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.StatementListFetcher
+import mozilla.components.service.digitalassetlinks.TIMEOUT
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import java.io.ByteArrayInputStream
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class StatementApiTest {
+
+ @Mock private lateinit var httpClient: Client
+ private lateinit var listFetcher: StatementListFetcher
+ private val jsonHeaders = MutableHeaders(
+ CONTENT_TYPE to CONTENT_TYPE_APPLICATION_JSON,
+ )
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ listFetcher = StatementApi(httpClient)
+ }
+
+ @Test
+ fun `return empty list if request fails`() {
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "https://mozilla.org/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenThrow(IOException::class.java)
+
+ val source = AssetDescriptor.Web("https://mozilla.org")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `return empty list if response does not have status 200`() {
+ val response = Response(
+ url = "https://firefox.com/.well-known/assetlinks.json",
+ status = 201,
+ headers = jsonHeaders,
+ body = mock(),
+ )
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "https://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("https://firefox.com")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `return empty list if response does not have JSON content type`() {
+ val response = Response(
+ url = "https://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = MutableHeaders(
+ CONTENT_TYPE to CONTENT_TYPE_FORM_URLENCODED,
+ ),
+ body = mock(),
+ )
+
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "https://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("https://firefox.com")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `return empty list if response is not valid JSON`() {
+ val response = Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody("not-json"),
+ )
+
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `return empty list if response is an empty JSON array`() {
+ val response = Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody("[]"),
+ )
+
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `parses example asset links file`() {
+ val response = Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody(
+ """
+ [{
+ "relation": [
+ "delegate_permission/common.handle_all_urls",
+ "delegate_permission/common.use_as_origin"
+ ],
+ "target": {
+ "namespace": "web",
+ "site": "https://www.google.com"
+ }
+ },{
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "org.digitalassetlinks.sampleapp",
+ "sha256_cert_fingerprints": [
+ "10:39:38:EE:45:37:E5:9E:8E:E7:92:F6:54:50:4F:B8:34:6F:C6:B3:46:D0:BB:C4:41:5F:C3:39:FC:FC:8E:C1"
+ ]
+ }
+ },{
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "org.digitalassetlinks.sampleapp2",
+ "sha256_cert_fingerprints": ["AA", "BB"]
+ }
+ }]
+ """,
+ ),
+ )
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(
+ listOf(
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Web("https://www.google.com"),
+ ),
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = AssetDescriptor.Web("https://www.google.com"),
+ ),
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Android(
+ packageName = "org.digitalassetlinks.sampleapp",
+ sha256CertFingerprint = "10:39:38:EE:45:37:E5:9E:8E:E7:92:F6:54:50:4F:B8:34:6F:C6:B3:46:D0:BB:C4:41:5F:C3:39:FC:FC:8E:C1",
+ ),
+ ),
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Android(
+ packageName = "org.digitalassetlinks.sampleapp2",
+ sha256CertFingerprint = "AA",
+ ),
+ ),
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Android(
+ packageName = "org.digitalassetlinks.sampleapp2",
+ sha256CertFingerprint = "BB",
+ ),
+ ),
+ ),
+ listFetcher.listStatements(source).toList(),
+ )
+ }
+
+ @Test
+ fun `resolves include statements`() {
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(
+ Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody(
+ """
+ [{
+ "relation": ["delegate_permission/common.use_as_origin"],
+ "target": {
+ "namespace": "web",
+ "site": "https://www.google.com"
+ }
+ },{
+ "include": "https://example.com/includedstatements.json"
+ }]
+ """,
+ ),
+ ),
+ )
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "https://example.com/includedstatements.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(
+ Response(
+ url = "https://example.com/includedstatements.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody(
+ """
+ [{
+ "relation": ["delegate_permission/common.use_as_origin"],
+ "target": {
+ "namespace": "web",
+ "site": "https://www.example.com"
+ }
+ }]
+ """,
+ ),
+ ),
+ )
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(
+ listOf(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = AssetDescriptor.Web("https://www.google.com"),
+ ),
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = AssetDescriptor.Web("https://www.example.com"),
+ ),
+ ),
+ listFetcher.listStatements(source).toList(),
+ )
+ }
+
+ @Test
+ fun `no infinite loops`() {
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(
+ Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody(
+ """
+ [{
+ "relation": ["delegate_permission/common.use_as_origin"],
+ "target": {
+ "namespace": "web",
+ "site": "https://example.com"
+ }
+ },{
+ "include": "http://firefox.com/.well-known/assetlinks.json"
+ }]
+ """,
+ ),
+ ),
+ )
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(
+ listOf(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = AssetDescriptor.Web("https://example.com"),
+ ),
+ ),
+ listFetcher.listStatements(source).toList(),
+ )
+ }
+
+ private fun stringBody(data: String) = Response.Body(ByteArrayInputStream(data.toByteArray()))
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementRelationCheckerTest.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementRelationCheckerTest.kt
new file mode 100644
index 0000000000..4498ab56a1
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementRelationCheckerTest.kt
@@ -0,0 +1,89 @@
+/* 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.service.digitalassetlinks.local
+
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.StatementListFetcher
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class StatementRelationCheckerTest {
+
+ @Test
+ fun `checks list lazily`() {
+ var numYields = 0
+ val target = AssetDescriptor.Web("https://mozilla.org")
+ val listFetcher = object : StatementListFetcher {
+ override fun listStatements(source: AssetDescriptor.Web) = sequence {
+ numYields = 1
+ yield(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = target,
+ ),
+ )
+ numYields = 2
+ yield(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = target,
+ ),
+ )
+ }
+ }
+
+ val checker = StatementRelationChecker(listFetcher)
+ assertEquals(0, numYields)
+
+ assertTrue(checker.checkRelationship(mock(), Relation.USE_AS_ORIGIN, target))
+ assertEquals(1, numYields)
+
+ // Sanity check that the mock can yield twice
+ numYields = 0
+ listFetcher.listStatements(mock()).toList()
+ assertEquals(2, numYields)
+ }
+
+ @Test
+ fun `fails if relation does not match`() {
+ val target = AssetDescriptor.Android("com.test", "AA:BB")
+ val listFetcher = object : StatementListFetcher {
+ override fun listStatements(source: AssetDescriptor.Web) = sequenceOf(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = target,
+ ),
+ )
+ }
+
+ val checker = StatementRelationChecker(listFetcher)
+ assertFalse(checker.checkRelationship(mock(), Relation.HANDLE_ALL_URLS, target))
+ }
+
+ @Test
+ fun `fails if target does not match`() {
+ val target = AssetDescriptor.Web("https://mozilla.org")
+ val listFetcher = object : StatementListFetcher {
+ override fun listStatements(source: AssetDescriptor.Web) = sequenceOf(
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Web("https://mozilla.com"),
+ ),
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Web("http://mozilla.org"),
+ ),
+ )
+ }
+
+ val checker = StatementRelationChecker(listFetcher)
+ assertFalse(checker.checkRelationship(mock(), Relation.HANDLE_ALL_URLS, target))
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..1f0955d450
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/firefox-accounts/README.md b/mobile/android/android-components/components/service/firefox-accounts/README.md
new file mode 100644
index 0000000000..dc78194d78
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/README.md
@@ -0,0 +1,317 @@
+# [Android Components](../../../README.md) > Service > Firefox Accounts (FxA)
+
+A library for integrating with Firefox Accounts.
+
+## Motivation
+
+The **Firefox Accounts Android Component** provides both low and high level accounts functionality.
+
+At a low level, there is direct interaction with the accounts system:
+* Obtain scoped OAuth tokens that can be used to access the user's data in Mozilla-hosted services like Firefox Sync
+* Fetch client-side scoped keys needed for end-to-end encryption of that data
+* Fetch a user's profile to personalize the application
+
+At a high level, there is an Account Manager:
+* Handles account state management and persistence
+* Abstracts away OAuth details, handling scopes, token caching, recovery, etc. Application can still specify custom scopes if needed
+* Integrates with FxA device management, automatically creating and destroying device records as appropriate
+* (optionally) Provides Send Tab integration - allows sending and receiving tabs within the Firefox Account ecosystem
+* (optionally) Provides Firefox Sync integration
+
+Sample applications:
+* [accounts sample app](https://github.com/mozilla-mobile/android-components/tree/main/samples/firefox-accounts), demonstrates how to use low level APIs
+* [sync app](https://github.com/mozilla-mobile/android-components/tree/main/samples/sync), demonstrates a high level accounts integration, complete with syncing multiple data stores
+
+Useful companion components:
+* [feature-accounts](https://github.com/mozilla-mobile/android-components/tree/main/components/feature/accounts), provides a `tabs` integration on top of `FxaAccountManager`, to handle display of web sign-in UI.
+* [browser-storage-sync](https://github.com/mozilla-mobile/android-components/tree/main/components/browser/storage-sync), provides data storage layers compatible with Firefox Sync.
+
+## Before using this component
+Products sending telemetry and using this component *must request* a data-review following [this process](https://wiki.mozilla.org/Firefox/Data_Collection).
+This component provides data collection using the [Glean SDK](https://mozilla.github.io/glean/book/index.html).
+The list of metrics being collected is available in the [metrics documentation](../../support/sync-telemetry/docs/metrics.md).
+
+## Usage
+### 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:service-firefox-accounts:{latest-version}"
+```
+
+### High level APIs, recommended for most applications
+
+Below is an example of how to integrate most of the common functionality exposed by `FxaAccountManager`.
+Additionally, see `feature-accounts`
+
+```kotlin
+// Make the two "syncable" stores accessible to account manager's sync machinery.
+GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
+GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage)
+
+val accountManager = FxaAccountManager(
+ context = this,
+ serverConfig = ServerConfig.release(CLIENT_ID, REDIRECT_URL),
+ deviceConfig = DeviceConfig(
+ name = "Sample app",
+ type = DeviceType.MOBILE,
+ capabilities = setOf(DeviceCapability.SEND_TAB)
+ ),
+ syncConfig = SyncConfig(setOf(SyncEngine.History, SyncEngine.Bookmarks), syncPeriodInMinutes = 15L)
+)
+
+// Observe changes to the account and profile.
+accountManager.register(accountObserver, owner = this, autoPause = true)
+
+// Observe sync state changes.
+accountManager.registerForSyncEvents(syncObserver, owner = this, autoPause = true)
+
+// Observe incoming account events (e.g. when another device connects or
+// disconnects to/from the account, SEND_TAB commands from other devices, etc).
+// Note that since the device is configured with a SEND_TAB capability, device constellation will be
+// automatically updated during any account initialization flow (restore, login, sign-up, recovery).
+// It is up to the application to keep it up-to-date beyond that.
+// See `account.deviceConstellation().refreshDeviceStateAsync()`.
+accountManager.registerForAccountEvents(accountEventsObserver, owner = this, autoPause = true)
+
+// Now that all of the observers we care about are registered, kick off the account manager.
+// If we're already authenticated
+launch { accountManager.initAsync().await() }
+
+// 'Sync Now' button binding.
+findViewById<View>(R.id.buttonSync).setOnClickListener {
+ accountManager.syncNowAsync(SyncReason.User)
+}
+
+// 'Sign-in' button binding.
+findViewById<View>(R.id.buttonSignIn).setOnClickListener {
+ launch {
+ val authUrl = accountManager.beginAuthenticationAsync().await()
+ authUrl?.let { openWebView(it) }
+ }
+}
+
+// 'Sign-out' button binding
+findViewById<View>(R.id.buttonLogout).setOnClickListener {
+ launch {
+ accountManager.logoutAsync().await()
+ }
+}
+
+// 'Disable periodic sync' button binding
+findViewById<View>(R.id.disablePeriodicSync).setOnClickListener {
+ launch {
+ accountManager.setSyncConfigAsync(
+ SyncConfig(setOf(SyncReason.History, SyncReason.Bookmarks)
+ ).await()
+ }
+}
+
+// 'Enable periodic sync' button binding
+findViewById<View>(R.id.enablePeriodicSync).setOnClickListener {
+ launch {
+ accountManager.setSyncConfigAsync(
+ SyncConfig(setOf(SyncReason.History, SyncReason.Bookmarks), syncPeriodInMinutes = 60L)
+ ).await()
+ }
+}
+
+// Globally disabled syncing an engine - this affects all Firefox Sync clients.
+findViewById<View>(R.id.globallyDisableHistoryEngine).setOnClickListener {
+ SyncEnginesStorage.setStatus(SyncEngine.History, false)
+ accountManager.syncNowAsync(SyncReason.EngineChange)
+}
+
+// Get current status of SyncEngines. Note that this may change after every sync, as other Firefox Sync clients can change it.
+val engineStatusMap = SyncEnginesStorage.getStatus() // type is: Map<SyncEngine, Boolean>
+
+// This is expected to be called from the webview/geckoview integration, which intercepts page loads and gets
+// 'code' and 'state' out of the 'successful sign-in redirect' url.
+fun onLoginComplete(code: String, state: String) {
+ launch {
+ accountManager.finishAuthenticationAsync(code, state).await()
+ }
+}
+
+// Observe changes to account state.
+val accountObserver = object : AccountObserver {
+ override fun onLoggedOut() = launch {
+ // handle logging-out in the UI
+ }
+
+ override fun onAuthenticationProblems() = launch {
+ // prompt user to re-authenticate
+ }
+
+ override fun onAuthenticated(account: OAuthAccount) = launch {
+ // logged-in successfully; display account details
+ }
+
+ override fun onProfileUpdated(profile: Profile) {
+ // display ${profile.displayName} and ${profile.email} if desired
+ }
+}
+
+// Observe changes to sync state.
+val syncObserver = object : SyncStatusObserver {
+ override fun onStarted() = launch {
+ // sync started running; update some UI to indicate this
+ }
+
+ override fun onIdle() = launch {
+ // sync stopped running; update some UI to indicate this
+ }
+
+ override fun onError(error: Exception?) = launch {
+ // sync encountered an error; optionally indicate this in the UI
+ }
+}
+
+// Observe incoming account events.
+val accountEventsObserver = object : AccountEventsObserver {
+ override fun onEvents(event: List<AccountEvent>) {
+ // device received some commands; for example, here's how you can process incoming Send Tab commands:
+ commands
+ .filter { it is AccountEvent.CommandReceived }
+ .map { it.command }
+ .filter { it is DeviceCommandIncoming.TabReceived }
+ .forEach {
+ val tabReceivedCommand = it as DeviceCommandIncoming.TabReceived
+ val fromDeviceName = tabReceivedCommand.from?.displayName
+ showNotification("Tab ${tab.title}, received from: ${fromDisplayName}", tab.url)
+ }
+ // (although note the SendTabFeature makes dealing with these commands
+ // easier still.)
+ }
+}
+```
+
+### Low level APIs
+
+First you need some OAuth information. Generate a `client_id`, `redirectUrl` and find out the scopes for your application.
+See the [Firefox Account documentation](https://mozilla.github.io/application-services/docs/accounts/welcome.html)
+for that.
+
+Once you have the OAuth info, you can start adding `FxAClient` to your Android project.
+As part of the OAuth flow your application will be opening up a WebView or a Custom Tab.
+Currently the SDK does not provide the WebView, you have to write it yourself.
+
+Create a global `account` object:
+
+```kotlin
+var account: FirefoxAccount? = null
+```
+
+You will need to save state for FxA in your app, this example just uses `SharedPreferences`. We suggest using the [Android Keystore]( https://developer.android.com/training/articles/keystore) for this data.
+Define variables to help save state for FxA:
+
+```kotlin
+val STATE_PREFS_KEY = "fxaAppState"
+val STATE_KEY = "fxaState"
+```
+
+Then you can write the following:
+
+```kotlin
+
+account = getAuthenticatedAccount()
+if (account == null) {
+ // Start authentication flow
+ val config = Config(CONFIG_URL, CLIENT_ID, REDIRECT_URL)
+ // Some helpers such as Config.release(CLIENT_ID, REDIRECT_URL)
+ // are also provided for well-known Firefox Accounts servers.
+ account = FirefoxAccount(config)
+}
+
+fun getAuthenticatedAccount(): FirefoxAccount? {
+ val savedJSON = getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).getString(FXA_STATE_KEY, "")
+ return savedJSON?.let {
+ try {
+ FirefoxAccount.fromJSONString(it)
+ } catch (e: FxaException) {
+ null
+ }
+ } ?: null
+}
+```
+
+The code above checks if you have some existing state for FxA, otherwise it configures it. All asynchronous methods on `FirefoxAccount` are executed on `Dispatchers.IO`'s dedicated thread pool. They return `Deferred` which is Kotlin's non-blocking cancellable Future type.
+
+Once the configuration is available and an account instance was created, the authentication flow can be started:
+
+```kotlin
+launch {
+ val url = account.beginOAuthFlow(scopes).await()
+ openWebView(url)
+}
+```
+
+When spawning the WebView, be sure to override the `OnPageStarted` function to intercept the redirect url and fetch the code + state parameters:
+
+```kotlin
+override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ if (url != null && url.startsWith(redirectUrl)) {
+ val uri = Uri.parse(url)
+ val mCode = uri.getQueryParameter("code")
+ val mState = uri.getQueryParameter("state")
+ if (mCode != null && mState != null) {
+ // Pass the code and state parameters back to your main activity
+ listener?.onLoginComplete(mCode, mState, this@LoginFragment)
+ }
+ }
+
+ super.onPageStarted(view, url, favicon)
+}
+```
+
+Finally, complete the OAuth flow, retrieve the profile information, then save your login state once you've gotten valid profile information:
+
+```kotlin
+launch {
+ // Complete authentication flow
+ account.completeOAuthFlow(code, state).await()
+
+ // Display profile information
+ val profile = account.getProfile().await()
+ txtView.txt = profile.displayName
+
+ // Persist login state
+ val json = account.toJSONString()
+ getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).edit()
+ .putString(FXA_STATE_KEY, json).apply()
+}
+```
+
+## Automatic sign-in via trusted on-device FxA Auth providers
+
+If there are trusted FxA auth providers available on the device, and they're signed-in, it's possible
+to automatically sign-in into the same account, gaining access to the same data they have access to (e.g. Firefox Sync).
+
+Currently supported FxA auth providers are:
+- Firefox for Android (release, beta and nightly channels)
+
+`AccountSharing` provides facilities to securely query auth providers for available accounts. It may be used
+directly in concert with a low-level `FirefoxAccount.migrateFromSessionTokenAsync`, or via the high-level `FxaAccountManager`:
+
+```kotlin
+val availableAccounts = accountManager.shareableAccounts(context)
+// Display a list of accounts to the user, identified by account.email and account.sourcePackage
+// Or, pick the first available account. They're sorted in an order of internal preference (release, beta, nightly).
+val selectedAccount = availableAccounts[0]
+launch {
+ val result = accountManager.signInWithShareableAccountAsync(selectedAccount).await()
+ if (result) {
+ // Successfully signed-into an account.
+ // accountManager.authenticatedAccount() is the new account.
+ } else {
+ // Failed to sign-into an account, either due to bad credentials or networking issues.
+ }
+}
+```
+
+## 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/service/firefox-accounts/build.gradle b/mobile/android/android-components/components/service/firefox-accounts/build.gradle
new file mode 100644
index 0000000000..7eac84319f
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/build.gradle
@@ -0,0 +1,66 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ consumerProguardFiles 'proguard-rules-consumer.pro'
+ }
+ }
+
+ namespace 'mozilla.components.service.firefox.accounts'
+}
+
+dependencies {
+ // Types defined in concept-sync are part of the public API of this module.
+ api project(':concept-sync')
+ api project(':concept-storage')
+
+ // Parts of this dependency are typealiase'd or are otherwise part of this module's public API.
+ api ComponentsDependencies.mozilla_appservices_fxaclient
+ implementation ComponentsDependencies.mozilla_appservices_syncmanager
+
+ // Observable is part of public API of the FxaAccountManager.
+ api project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+ implementation project(':lib-dataprotect')
+ implementation project(':lib-state')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_work_runtime
+ implementation ComponentsDependencies.androidx_lifecycle_process
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.androidx_work_testing
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ testImplementation ComponentsDependencies.mozilla_appservices_full_megazord_forUnitTests
+ testImplementation ComponentsDependencies.kotlin_reflect
+}
+
+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/service/firefox-accounts/proguard-rules-consumer.pro b/mobile/android/android-components/components/service/firefox-accounts/proguard-rules-consumer.pro
new file mode 100644
index 0000000000..d3456cd17e
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/proguard-rules-consumer.pro
@@ -0,0 +1 @@
+# ProGuard rules for consumers of this library.
diff --git a/mobile/android/android-components/components/service/firefox-accounts/proguard-rules.pro b/mobile/android/android-components/components/service/firefox-accounts/proguard-rules.pro
new file mode 100644
index 0000000000..50e2b38a97
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/sebastian/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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/service/firefox-accounts/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/firefox-accounts/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..816719811c
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt
new file mode 100644
index 0000000000..37af0f5b76
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt
@@ -0,0 +1,267 @@
+/* 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.service.fxa
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
+import mozilla.appservices.fxaclient.FxaRustAuthState
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.StatePersistenceCallback
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.observer.ObserverRegistry
+import java.lang.ref.WeakReference
+
+const val FXA_STATE_PREFS_KEY = "fxaAppState"
+const val FXA_STATE_KEY = "fxaState"
+
+/**
+ * Represents state of our account on disk - is it new, or restored?
+ */
+internal sealed class AccountOnDisk : WithAccount {
+ data class Restored(val account: OAuthAccount) : AccountOnDisk() {
+ override fun account() = account
+ }
+ data class New(val account: OAuthAccount) : AccountOnDisk() {
+ override fun account() = account
+ }
+}
+
+internal interface WithAccount {
+ fun account(): OAuthAccount
+}
+
+/**
+ * Knows how to read account from disk (or creating a new instance if there's no account),
+ * registering necessary watchers.
+ */
+open class StorageWrapper(
+ private val accountManager: FxaAccountManager,
+ accountEventObserverRegistry: ObserverRegistry<AccountEventsObserver>,
+ private val serverConfig: ServerConfig,
+ private val crashReporter: CrashReporting? = null,
+) {
+ private class PersistenceCallback(
+ private val accountManager: WeakReference<FxaAccountManager>,
+ ) : StatePersistenceCallback {
+ private val logger = Logger("FxaStatePersistenceCallback")
+
+ override fun persist(data: String) {
+ val storage = accountManager.get()?.getAccountStorage()
+ logger.debug("Persisting account state into $storage")
+ storage?.write(data)
+ }
+ }
+
+ private val statePersistenceCallback = PersistenceCallback(WeakReference(accountManager))
+ private val accountEventsIntegration = AccountEventsIntegration(accountEventObserverRegistry)
+
+ internal fun account(): AccountOnDisk {
+ return try {
+ when (val account = accountManager.getAccountStorage().read()) {
+ null -> AccountOnDisk.New(obtainAccount())
+ else -> AccountOnDisk.Restored(account)
+ }
+ } catch (e: FxaPanicException) {
+ // Don't swallow panics from the underlying library.
+ throw e
+ } catch (e: FxaException) {
+ // Locally corrupt accounts are simply treated as 'absent'.
+ AccountOnDisk.New(obtainAccount())
+ }.also {
+ watchAccount(it.account())
+ }
+ }
+
+ private fun watchAccount(account: OAuthAccount) {
+ account.registerPersistenceCallback(statePersistenceCallback)
+ account.deviceConstellation().register(accountEventsIntegration)
+ }
+
+ /**
+ * Exists strictly for testing purposes, allowing tests to specify their own implementation of [OAuthAccount].
+ */
+ @VisibleForTesting
+ open fun obtainAccount(): OAuthAccount = FirefoxAccount(serverConfig, crashReporter)
+}
+
+/**
+ * In the future, this could be an internal account-related events processing layer.
+ * E.g., once we grow events such as "please logout".
+ * For now, we just pass everything downstream as-is.
+ */
+internal class AccountEventsIntegration(
+ private val listenerRegistry: ObserverRegistry<AccountEventsObserver>,
+) : AccountEventsObserver {
+ private val logger = Logger("AccountEventsIntegration")
+
+ override fun onEvents(events: List<AccountEvent>) {
+ logger.info("Received events, notifying listeners")
+ listenerRegistry.notifyObservers { onEvents(events) }
+ }
+}
+
+internal interface AccountStorage {
+ @Throws(Exception::class)
+ fun read(): OAuthAccount?
+ fun write(accountState: String)
+ fun clear()
+}
+
+/**
+ * Account storage layer which uses plaintext storage implementation.
+ *
+ * Migration from [SecureAbove22AccountStorage] will happen upon initialization,
+ * unless disabled via [migrateFromSecureStorage].
+ */
+@SuppressWarnings("TooGenericExceptionCaught")
+internal class SharedPrefAccountStorage(
+ val context: Context,
+ private val crashReporter: CrashReporting? = null,
+ migrateFromSecureStorage: Boolean = true,
+) : AccountStorage {
+ internal val logger = Logger("mozac/SharedPrefAccountStorage")
+
+ init {
+ if (migrateFromSecureStorage) {
+ // In case we switched from SecureAbove22AccountStorage to this implementation, migrate persisted account
+ // and clear out the old storage layer.
+ val secureStorage = SecureAbove22AccountStorage(
+ context,
+ crashReporter,
+ migrateFromPlaintextStorage = false,
+ )
+ try {
+ secureStorage.read()?.let { secureAccount ->
+ this.write(secureAccount.toJSONString())
+ secureStorage.clear()
+ }
+ } catch (e: Exception) {
+ // Certain devices crash on various Keystore exceptions. While trying to migrate
+ // to use the plaintext storage we don't want to crash if we can't access secure
+ // storage, and just catch the errors.
+ logger.error("Migrating from secure storage failed", e)
+ }
+ }
+ }
+
+ /**
+ * @throws FxaException if JSON failed to parse into a [FirefoxAccount].
+ */
+ @Throws(FxaException::class)
+ override fun read(): OAuthAccount? {
+ val savedJSON = accountPreferences().getString(FXA_STATE_KEY, null)
+ ?: return null
+
+ // May throw a generic FxaException if it fails to process saved JSON.
+ val account = FirefoxAccount.fromJSONString(savedJSON, crashReporter)
+ val state = account.getAuthState()
+ if (state != FxaRustAuthState.CONNECTED && crashReporter != null) {
+ crashReporter.submitCaughtException(
+ AbnormalAccountStorageEvent.RestoringNonConnectedAccount(
+ "Restoring account from an unexpected state: $state",
+ ),
+ )
+ }
+ return account
+ }
+
+ override fun write(accountState: String) {
+ accountPreferences()
+ .edit()
+ .putString(FXA_STATE_KEY, accountState)
+ .apply()
+ }
+
+ override fun clear() {
+ accountPreferences()
+ .edit()
+ .remove(FXA_STATE_KEY)
+ .apply()
+ }
+
+ private fun accountPreferences(): SharedPreferences {
+ return context.getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE)
+ }
+}
+
+/**
+ * A base class for exceptions describing abnormal account storage behaviour.
+ */
+internal abstract class AbnormalAccountStorageEvent(message: String? = null) : Exception(message) {
+ /**
+ * Account state was expected to be present, but it wasn't.
+ */
+ internal class UnexpectedlyMissingAccountState(message: String? = null) : AbnormalAccountStorageEvent(message)
+ internal class RestoringNonConnectedAccount(message: String? = null) : AbnormalAccountStorageEvent(message)
+}
+
+/**
+ * Account storage layer which uses encrypted-at-rest storage implementation for supported API levels (23+).
+ * On older API versions account state is stored in plaintext.
+ *
+ * Migration from [SharedPrefAccountStorage] will happen upon initialization,
+ * unless disabled via [migrateFromPlaintextStorage].
+ */
+internal class SecureAbove22AccountStorage(
+ context: Context,
+ private val crashReporter: CrashReporting? = null,
+ migrateFromPlaintextStorage: Boolean = true,
+) : AccountStorage {
+ companion object {
+ private const val STORAGE_NAME = "fxaStateAC"
+ private const val KEY_ACCOUNT_STATE = "fxaState"
+ private const val PREF_NAME = "fxaStatePrefAC"
+ private const val PREF_KEY_HAS_STATE = "fxaStatePresent"
+ }
+
+ private val store = SecureAbove22Preferences(context, STORAGE_NAME)
+
+ // Prefs are used here to keep track of abnormal storage behaviour - namely, account state disappearing without
+ // being cleared first through this class. Note that clearing application data will clear both 'store' and 'prefs'.
+ private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+
+ init {
+ if (migrateFromPlaintextStorage) {
+ // In case we switched from SharedPrefAccountStorage to this implementation, migrate persisted account
+ // and clear out the old storage layer.
+ val plaintextStorage = SharedPrefAccountStorage(context, migrateFromSecureStorage = false)
+ plaintextStorage.read()?.let { plaintextAccount ->
+ this.write(plaintextAccount.toJSONString())
+ plaintextStorage.clear()
+ }
+ }
+ }
+
+ /**
+ * @throws FxaException if JSON failed to parse into a [FirefoxAccount].
+ */
+ @Throws(FxaException::class)
+ override fun read(): OAuthAccount? {
+ return store.getString(KEY_ACCOUNT_STATE).also {
+ // If account state is missing, but we expected it to be present, report an exception.
+ if (it == null && prefs.getBoolean(PREF_KEY_HAS_STATE, false)) {
+ crashReporter?.submitCaughtException(AbnormalAccountStorageEvent.UnexpectedlyMissingAccountState())
+ // Clear prefs to make sure we only submit this exception once.
+ prefs.edit().clear().apply()
+ }
+ }?.let { FirefoxAccount.fromJSONString(it, crashReporter) }
+ }
+
+ override fun write(accountState: String) {
+ store.putString(KEY_ACCOUNT_STATE, accountState)
+ prefs.edit().putBoolean(PREF_KEY_HAS_STATE, true).apply()
+ }
+
+ override fun clear() {
+ store.clear()
+ prefs.edit().clear().apply()
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Config.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Config.kt
new file mode 100644
index 0000000000..bcb7ebd5e4
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Config.kt
@@ -0,0 +1,90 @@
+/* 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.service.fxa
+
+import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
+
+typealias ServerConfig = mozilla.appservices.fxaclient.FxaConfig
+typealias Server = mozilla.appservices.fxaclient.FxaServer
+
+/**
+ * @property periodMinutes How frequently periodic sync should happen.
+ * @property initialDelayMinutes What should the initial delay for the periodic sync be.
+ */
+data class PeriodicSyncConfig(
+ val periodMinutes: Int = 240,
+ val initialDelayMinutes: Int = 5,
+)
+
+/**
+ * Configuration for sync.
+ *
+ * @property supportedEngines A set of supported sync engines, exposed via [GlobalSyncableStoreProvider].
+ * @property periodicSyncConfig Optional configuration for running sync periodically.
+ * Periodic sync is disabled if this is `null`.
+ */
+data class SyncConfig(
+ val supportedEngines: Set<SyncEngine>,
+ val periodicSyncConfig: PeriodicSyncConfig?,
+)
+
+/**
+ * Describes possible sync engines that device can support.
+ *
+ * @property nativeName Internally, Rust SyncManager represents engines as strings. Forward-compatibility
+ * with new engines is one of the reasons for this. E.g. during any sync, an engine may appear that we
+ * do not know about. At the public API level, we expose a concrete [SyncEngine] type to allow for more
+ * robust integrations. We do not expose "unknown" engines via our public API, but do handle them
+ * internally (by persisting their enabled/disabled status).
+ *
+ * [nativeName] must match engine strings defined in the sync15 crate, e.g. https://github.com/mozilla/application-services/blob/main/components/sync15/src/state.rs#L23-L38
+ *
+ * @property nativeName Name of the corresponding Sync1.5 collection.
+*/
+sealed class SyncEngine(val nativeName: String) {
+ // NB: When adding new types, make sure to think through implications for the SyncManager.
+ // See https://github.com/mozilla-mobile/android-components/issues/4557
+
+ /**
+ * A history engine.
+ */
+ object History : SyncEngine("history")
+
+ /**
+ * A bookmarks engine.
+ */
+ object Bookmarks : SyncEngine("bookmarks")
+
+ /**
+ * A 'logins/passwords' engine.
+ */
+ object Passwords : SyncEngine("passwords")
+
+ /**
+ * A remote tabs engine.
+ */
+ object Tabs : SyncEngine("tabs")
+
+ /**
+ * A credit cards engine.
+ */
+ object CreditCards : SyncEngine("creditcards")
+
+ /**
+ * An addresses engine.
+ */
+ object Addresses : SyncEngine("addresses")
+
+ /**
+ * An engine that's none of the above, described by [name].
+ */
+ data class Other(val name: String) : SyncEngine(name)
+
+ /**
+ * This engine is used internally, but hidden from the public API because we don't fully support
+ * this data type right now.
+ */
+ internal object Forms : SyncEngine("forms")
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Exceptions.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Exceptions.kt
new file mode 100644
index 0000000000..dc8f6f6a80
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Exceptions.kt
@@ -0,0 +1,107 @@
+/* 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.service.fxa
+
+/**
+ * High-level exception class for the exceptions thrown in the Rust library.
+ */
+typealias FxaException = mozilla.appservices.fxaclient.FxaException
+
+/**
+ * Thrown on a network error.
+ */
+typealias FxaNetworkException = mozilla.appservices.fxaclient.FxaException.Network
+
+/**
+ * Thrown when the Rust library hits an assertion or panic (this is always a bug).
+ */
+typealias FxaPanicException = mozilla.appservices.fxaclient.FxaException.Panic
+
+/**
+ * Thrown when the operation requires additional authorization.
+ */
+typealias FxaUnauthorizedException = mozilla.appservices.fxaclient.FxaException.Authentication
+
+/**
+ * Thrown when we try opening paring link from a Firefox configured to use a different content server
+ */
+typealias FxaOriginMismatchException = mozilla.appservices.fxaclient.FxaException.OriginMismatch
+
+/**
+ * Thrown if the application attempts to complete an OAuth flow when no OAuth flow has been
+ * initiated. This may indicate a user who navigated directly to the OAuth `redirect_uri` for the
+ * application.
+ */
+typealias FxaNoExistingAuthFlow = mozilla.appservices.fxaclient.FxaException.NoExistingAuthFlow
+
+/**
+ * Thrown when a scoped key was missing in the server response when requesting the OLD_SYNC scope.
+ */
+typealias FxaSyncScopedKeyMissingException =
+ mozilla.appservices.fxaclient.FxaException.SyncScopedKeyMissingInServerResponse
+
+/**
+ * Thrown when the Rust library hits an unexpected error that isn't a panic.
+ * This may indicate library misuse, network errors, etc.
+ */
+typealias FxaUnspecifiedException = mozilla.appservices.fxaclient.FxaException.Other
+
+/**
+ * @return 'true' if this exception should be re-thrown and eventually crash the app.
+ */
+fun FxaException.shouldPropagate(): Boolean {
+ return when (this) {
+ // Throw on panics
+ is FxaPanicException -> true
+ // Don't throw for recoverable errors.
+ is FxaNetworkException,
+ is FxaUnauthorizedException,
+ is FxaUnspecifiedException,
+ is FxaOriginMismatchException,
+ is FxaNoExistingAuthFlow,
+ -> false
+ // Throw on newly encountered exceptions.
+ // If they're actually recoverable and you see them in crash reports, update this check.
+ else -> true
+ }
+}
+
+/**
+ * Exceptions related to the account manager.
+ */
+sealed class AccountManagerException(message: String) : Exception(message) {
+ /**
+ * Hit a circuit-breaker during auth recovery flow.
+ * @param operation An operation which triggered an auth recovery flow that hit a circuit breaker.
+ */
+ class AuthRecoveryCircuitBreakerException(operation: String) : AccountManagerException(
+ "Auth recovery circuit breaker triggered by: $operation",
+ )
+
+ /**
+ * Unexpectedly encountered an access token without a key.
+ * @param operation An operation which triggered this state.
+ */
+ class MissingKeyFromSyncScopedAccessToken(operation: String) : AccountManagerException(
+ "Encountered an access token without a key: $operation",
+ )
+
+ /**
+ * Failure when running side effects to complete the authentication process.
+ */
+ class AuthenticationSideEffectsFailed : AccountManagerException(
+ "Failure when running side effects to complete authentication",
+ )
+}
+
+/**
+ * FxaException wrapper easily identifying it as the result of a failed operation of sending tabs.
+ */
+class SendCommandException(fxaException: FxaException) : Exception(fxaException)
+
+/**
+ * Thrown if we saw a keyed access token without a key (e.g. obtained for SCOPE_SYNC).
+ */
+internal class AccessTokenUnexpectedlyWithoutKey : Exception()
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
new file mode 100644
index 0000000000..7fc31785e3
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
@@ -0,0 +1,258 @@
+/* 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.service.fxa
+
+import android.net.Uri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.plus
+import kotlinx.coroutines.withContext
+import mozilla.appservices.fxaclient.FxaClient
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.sync.AuthFlowUrl
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.FxAEntryPoint
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.StatePersistenceCallback
+import mozilla.components.support.base.log.logger.Logger
+
+typealias PersistCallback = mozilla.appservices.fxaclient.FxaClient.PersistCallback
+
+/**
+ * FirefoxAccount represents the authentication state of a client.
+ */
+class FirefoxAccount internal constructor(
+ private val inner: FxaClient,
+ crashReporter: CrashReporting? = null,
+) : OAuthAccount {
+ private val job = SupervisorJob()
+ private val scope = CoroutineScope(Dispatchers.IO) + job
+
+ private val logger = Logger("FirefoxAccount")
+
+ /**
+ * Why this exists: in the `init` block below you'll notice that we register a persistence callback
+ * as soon as we initialize this object. Essentially, we _always_ have a persistence callback
+ * registered with [FxaClient]. However, our own lifecycle is such that we will not know
+ * how to actually persist account state until sometime after this object has been created.
+ * Currently, we're expecting [FxaAccountManager] to configure a real callback.
+ * This wrapper exists to facilitate that flow of events.
+ */
+ private class WrappingPersistenceCallback : PersistCallback {
+ private val logger = Logger("WrappingPersistenceCallback")
+
+ @Volatile
+ private var persistenceCallback: StatePersistenceCallback? = null
+
+ fun setCallback(callback: StatePersistenceCallback) {
+ logger.debug("Setting persistence callback")
+ persistenceCallback = callback
+ }
+
+ override fun persist(data: String) {
+ val callback = persistenceCallback
+
+ if (callback == null) {
+ logger.warn("FxaClient tried persist state, but persistence callback is not set")
+ } else {
+ logger.debug("Logging state to $callback")
+ callback.persist(data)
+ }
+ }
+ }
+
+ private var persistCallback = WrappingPersistenceCallback()
+ private val deviceConstellation = FxaDeviceConstellation(inner, scope, crashReporter)
+
+ init {
+ inner.registerPersistCallback(persistCallback)
+ }
+
+ /**
+ * Construct a FirefoxAccount from a [Config], a clientId, and a redirectUri.
+ *
+ * @param crashReporter A crash reporter instance.
+ *
+ * Note that it is not necessary to `close` the Config if this constructor is used (however
+ * doing so will not cause an error).
+ */
+ constructor(
+ config: ServerConfig,
+ crashReporter: CrashReporting? = null,
+ ) : this(FxaClient(config), crashReporter)
+
+ override fun close() {
+ job.cancel()
+ inner.close()
+ }
+
+ override fun registerPersistenceCallback(callback: StatePersistenceCallback) {
+ logger.info("Registering persistence callback")
+ persistCallback.setCallback(callback)
+ }
+
+ internal fun getAuthState() = inner.getAuthState()
+
+ override suspend fun beginOAuthFlow(
+ scopes: Set<String>,
+ entryPoint: FxAEntryPoint,
+ ) = withContext(scope.coroutineContext) {
+ handleFxaExceptions(logger, "begin oauth flow", { null }) {
+ val url = inner.beginOAuthFlow(scopes.toTypedArray(), entryPoint.entryName)
+ val state = Uri.parse(url).getQueryParameter("state")!!
+ AuthFlowUrl(state, url)
+ }
+ }
+
+ override suspend fun beginPairingFlow(
+ pairingUrl: String,
+ scopes: Set<String>,
+ entryPoint: FxAEntryPoint,
+ ) = withContext(scope.coroutineContext) {
+ // Eventually we should specify this as a param here, but for now, let's
+ // use a generic value (it's used only for server-side telemetry, so the
+ // actual value doesn't matter much)
+ handleFxaExceptions(logger, "begin oauth pairing flow", { null }) {
+ val url = inner.beginPairingFlow(pairingUrl, scopes.toTypedArray(), entryPoint.entryName)
+ val state = Uri.parse(url).getQueryParameter("state")!!
+ AuthFlowUrl(state, url)
+ }
+ }
+
+ override suspend fun getProfile(ignoreCache: Boolean) = withContext(scope.coroutineContext) {
+ handleFxaExceptions(logger, "getProfile", { null }) {
+ inner.getProfile(ignoreCache).into()
+ }
+ }
+
+ override fun getCurrentDeviceId(): String? {
+ // This is awkward, yes. Underlying method simply reads some data from in-memory state, and yet it throws
+ // in case that data isn't there. See https://github.com/mozilla/application-services/issues/2202.
+ return try {
+ inner.getCurrentDeviceId()
+ } catch (e: FxaPanicException) {
+ throw e
+ } catch (e: FxaException) {
+ null
+ }
+ }
+
+ override fun getSessionToken(): String? {
+ return try {
+ // This is awkward, yes. Underlying method simply reads some data from in-memory state, and yet it throws
+ // in case that data isn't there. See https://github.com/mozilla/application-services/issues/2202.
+ inner.getSessionToken()
+ } catch (e: FxaPanicException) {
+ throw e
+ } catch (e: FxaException) {
+ null
+ }
+ }
+
+ override suspend fun getTokenServerEndpointURL() = withContext(scope.coroutineContext) {
+ handleFxaExceptions(logger, "getTokenServerEndpointURL", { null }) {
+ inner.getTokenServerEndpointURL()
+ }
+ }
+
+ override suspend fun getManageAccountURL(entryPoint: FxAEntryPoint): String? {
+ return handleFxaExceptions(logger, "getManageAccountURL", { null }) {
+ inner.getManageAccountURL(entryPoint.entryName)
+ }
+ }
+
+ override fun getPairingAuthorityURL(): String {
+ return inner.getPairingAuthorityURL()
+ }
+
+ /**
+ * Fetches the connection success url.
+ */
+ fun getConnectionSuccessURL(): String {
+ return inner.getConnectionSuccessURL()
+ }
+
+ override suspend fun completeOAuthFlow(code: String, state: String) = withContext(scope.coroutineContext) {
+ handleFxaExceptions(logger, "complete oauth flow") {
+ inner.completeOAuthFlow(code, state)
+ }
+ }
+
+ override suspend fun getAccessToken(singleScope: String) = withContext(scope.coroutineContext) {
+ handleFxaExceptions(logger, "get access token", { null }) {
+ inner.getAccessToken(singleScope).into()
+ }
+ }
+
+ override fun authErrorDetected() {
+ // fxalib maintains some internal token caches that need to be cleared whenever we
+ // hit an auth problem. Call below makes that clean-up happen.
+ inner.clearAccessTokenCache()
+ }
+
+ override suspend fun checkAuthorizationStatus(singleScope: String) = withContext(scope.coroutineContext) {
+ // Now that internal token caches are cleared, we can perform a connectivity check.
+ // Do so by requesting a new access token using an internally-stored "refresh token".
+ // Success here means that we're still able to connect - our cached access token simply expired.
+ // Failure indicates that we need to re-authenticate.
+ try {
+ inner.getAccessToken(singleScope)
+ // We were able to obtain a token, so we're in a good authorization state.
+ true
+ } catch (e: FxaUnauthorizedException) {
+ // We got back a 401 while trying to obtain a new access token, which means our refresh
+ // token is also in a bad state. We need re-authentication for the tested scope.
+ false
+ } catch (e: FxaPanicException) {
+ // Re-throw any panics we may encounter.
+ throw e
+ } catch (e: FxaException) {
+ // On any other FxaExceptions (networking, etc) we have to return an indeterminate result.
+ null
+ }
+ // Re-throw all other exceptions.
+ }
+
+ override suspend fun disconnect() = withContext(scope.coroutineContext) {
+ // TODO can this ever throw FxaUnauthorizedException? would that even make sense? or is that a bug?
+ handleFxaExceptions(logger, "disconnect", { false }) {
+ inner.disconnect()
+ true
+ }
+ }
+
+ override fun deviceConstellation(): DeviceConstellation {
+ return deviceConstellation
+ }
+
+ override fun toJSONString(): String = inner.toJSONString()
+
+ companion object {
+ /**
+ * Restores the account's authentication state from a JSON string produced by
+ * [FirefoxAccount.toJSONString].
+ *
+ * @param crashReporter object used for logging caught exceptions
+ *
+ * @param persistCallback This callback will be called every time the [FirefoxAccount]
+ * internal state has mutated.
+ * The FirefoxAccount instance can be later restored using the
+ * [FirefoxAccount.fromJSONString]` class method.
+ * It is the responsibility of the consumer to ensure the persisted data
+ * is saved in a secure location, as it can contain Sync Keys and
+ * OAuth tokens.
+ *
+ * @return [FirefoxAccount] representing the authentication state
+ */
+ fun fromJSONString(
+ json: String,
+ crashReporter: CrashReporting?,
+ persistCallback: PersistCallback? = null,
+ ): FirefoxAccount {
+ return FirefoxAccount(FxaClient.fromJSONString(json, persistCallback), crashReporter)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt
new file mode 100644
index 0000000000..99c4fb196f
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt
@@ -0,0 +1,286 @@
+/* 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.service.fxa
+
+import android.content.Context
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.withContext
+import mozilla.appservices.fxaclient.FxaClient
+import mozilla.appservices.fxaclient.FxaException
+import mozilla.appservices.fxaclient.FxaStateCheckerEvent
+import mozilla.appservices.fxaclient.FxaStateCheckerState
+import mozilla.appservices.syncmanager.SyncTelemetry
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCommandOutgoing
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.DeviceConstellationObserver
+import mozilla.components.concept.sync.DevicePushSubscription
+import mozilla.components.concept.sync.ServiceResult
+import mozilla.components.service.fxa.manager.AppServicesStateMachineChecker
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+
+internal sealed class FxaDeviceConstellationException(message: String? = null) : Exception(message) {
+ /**
+ * Failure while ensuring device capabilities.
+ */
+ class EnsureCapabilitiesFailed(message: String? = null) : FxaDeviceConstellationException(message)
+}
+
+/**
+ * Provides an implementation of [DeviceConstellation] backed by a [FxaClient
+ */
+class FxaDeviceConstellation(
+ private val account: FxaClient,
+ private val scope: CoroutineScope,
+ @get:VisibleForTesting
+ internal val crashReporter: CrashReporting? = null,
+) : DeviceConstellation, Observable<AccountEventsObserver> by ObserverRegistry() {
+ private val logger = Logger("FxaDeviceConstellation")
+
+ private val deviceObserverRegistry = ObserverRegistry<DeviceConstellationObserver>()
+
+ @Volatile
+ private var constellationState: ConstellationState? = null
+
+ override fun state(): ConstellationState? = constellationState
+
+ @VisibleForTesting
+ internal enum class DeviceFinalizeAction {
+ Initialize,
+ EnsureCapabilities,
+ None,
+ }
+
+ @Suppress("ComplexMethod")
+ @Throws(FxaPanicException::class)
+ override suspend fun finalizeDevice(
+ authType: AuthType,
+ config: DeviceConfig,
+ ): ServiceResult = withContext(scope.coroutineContext) {
+ val finalizeAction = when (authType) {
+ AuthType.Signin,
+ AuthType.Signup,
+ AuthType.Pairing,
+ is AuthType.OtherExternal,
+ AuthType.MigratedCopy,
+ -> DeviceFinalizeAction.Initialize
+ AuthType.Existing,
+ AuthType.MigratedReuse,
+ -> DeviceFinalizeAction.EnsureCapabilities
+ AuthType.Recovered -> DeviceFinalizeAction.None
+ }
+
+ if (finalizeAction == DeviceFinalizeAction.None) {
+ ServiceResult.Ok
+ } else {
+ val capabilities = config.capabilities.map { it.into() }.toSet()
+ // Note: sending the event for the result to the the state machine checker is split
+ // between here and `FxaAccountManager`
+ // - This function reports successes and auth failures, since it's the only one that
+ // knows if `initializeDevice()` or `EnsureDeviceCapabilities()` was called.
+ // - `FxaAccountManager` reports other failures, since it runs this code inside
+ // `withServiceRetries` so it's the only one that knows if the call will be retried
+ if (finalizeAction == DeviceFinalizeAction.Initialize) {
+ try {
+ AppServicesStateMachineChecker.checkInternalState(FxaStateCheckerState.InitializeDevice)
+ account.initializeDevice(config.name, config.type.into(), capabilities)
+ AppServicesStateMachineChecker.handleInternalEvent(FxaStateCheckerEvent.InitializeDeviceSuccess)
+ ServiceResult.Ok
+ } catch (e: FxaPanicException) {
+ throw e
+ } catch (e: FxaUnauthorizedException) {
+ AppServicesStateMachineChecker.handleInternalEvent(FxaStateCheckerEvent.CallError)
+ ServiceResult.AuthError
+ } catch (e: FxaException) {
+ ServiceResult.OtherError
+ }
+ } else {
+ try {
+ AppServicesStateMachineChecker.checkInternalState(FxaStateCheckerState.EnsureDeviceCapabilities)
+ account.ensureCapabilities(capabilities)
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.EnsureDeviceCapabilitiesSuccess,
+ )
+ ServiceResult.Ok
+ } catch (e: FxaPanicException) {
+ throw e
+ } catch (e: FxaUnauthorizedException) {
+ AppServicesStateMachineChecker.handleInternalEvent(FxaStateCheckerEvent.EnsureCapabilitiesAuthError)
+ // Unless we've added a new capability, in practice 'ensureCapabilities' isn't
+ // actually expected to do any work: everything should have been done by initializeDevice.
+ // So if it did, and failed, let's report this so that we're aware of this!
+ // See https://github.com/mozilla-mobile/android-components/issues/8164
+ crashReporter?.submitCaughtException(
+ FxaDeviceConstellationException.EnsureCapabilitiesFailed(e.toString()),
+ )
+ ServiceResult.AuthError
+ } catch (e: FxaException) {
+ ServiceResult.OtherError
+ }
+ }
+ }
+ }
+
+ override suspend fun processRawEvent(payload: String) = withContext(scope.coroutineContext) {
+ handleFxaExceptions(logger, "processing raw commands") {
+ val events = when (val accountEvent: AccountEvent = account.handlePushMessage(payload).into()) {
+ is AccountEvent.DeviceCommandIncoming -> account.pollDeviceCommands().map {
+ AccountEvent.DeviceCommandIncoming(command = it.into())
+ }
+ else -> listOf(accountEvent)
+ }
+ processEvents(events)
+ }
+ }
+
+ @MainThread
+ override fun registerDeviceObserver(
+ observer: DeviceConstellationObserver,
+ owner: LifecycleOwner,
+ autoPause: Boolean,
+ ) {
+ logger.debug("registering device observer")
+ deviceObserverRegistry.register(observer, owner, autoPause)
+ }
+
+ override suspend fun setDeviceName(name: String, context: Context) = withContext(scope.coroutineContext) {
+ val rename = handleFxaExceptions(logger, "changing device name") {
+ account.setDeviceDisplayName(name)
+ }
+ FxaDeviceSettingsCache(context).updateCachedName(name)
+ // See the latest device (name) changes after changing it.
+
+ rename && refreshDevices()
+ }
+
+ override suspend fun setDevicePushSubscription(
+ subscription: DevicePushSubscription,
+ ) = withContext(scope.coroutineContext) {
+ handleFxaExceptions(logger, "updating device push subscription") {
+ account.setDevicePushSubscription(
+ subscription.endpoint,
+ subscription.publicKey,
+ subscription.authKey,
+ )
+ }
+ }
+
+ override suspend fun sendCommandToDevice(
+ targetDeviceId: String,
+ outgoingCommand: DeviceCommandOutgoing,
+ ) = withContext(scope.coroutineContext) {
+ val result = handleFxaExceptions(logger, "sending device command", { error -> error }) {
+ when (outgoingCommand) {
+ is DeviceCommandOutgoing.SendTab -> {
+ account.sendSingleTab(targetDeviceId, outgoingCommand.title, outgoingCommand.url)
+ val errors: List<Throwable> = SyncTelemetry.processFxaTelemetry(account.gatherTelemetry())
+ for (error in errors) {
+ crashReporter?.submitCaughtException(error)
+ }
+ }
+ else -> logger.debug("Skipped sending unsupported command type: $outgoingCommand")
+ }
+ null
+ }
+
+ if (result != null) {
+ when (result) {
+ // Don't submit network exceptions to our crash reporter. They're just noise.
+ is FxaException.Network -> {
+ logger.warn("Failed to 'sendCommandToDevice' due to a network exception")
+ }
+ else -> {
+ logger.warn("Failed to 'sendCommandToDevice'", result)
+ crashReporter?.submitCaughtException(SendCommandException(result))
+ }
+ }
+
+ false
+ } else {
+ true
+ }
+ }
+
+ // Poll for missed commands. Commands are the only event-type that can be
+ // polled for, although missed commands will be delivered as AccountEvents.
+ override suspend fun pollForCommands() = withContext(scope.coroutineContext) {
+ val events = handleFxaExceptions(logger, "polling for device commands", { null }) {
+ account.pollDeviceCommands().map { AccountEvent.DeviceCommandIncoming(command = it.into()) }
+ }
+
+ if (events == null) {
+ false
+ } else {
+ processEvents(events)
+ val errors: List<Throwable> = SyncTelemetry.processFxaTelemetry(account.gatherTelemetry())
+ for (error in errors) {
+ crashReporter?.submitCaughtException(error)
+ }
+ true
+ }
+ }
+
+ private fun processEvents(events: List<AccountEvent>) {
+ notifyObservers { onEvents(events) }
+ }
+
+ override suspend fun refreshDevices(): Boolean {
+ return withContext(scope.coroutineContext) {
+ logger.info("Refreshing device list...")
+
+ // Attempt to fetch devices, or bail out on failure.
+ val allDevices = fetchAllDevices() ?: return@withContext false
+
+ // Find the current device.
+ val currentDevice = allDevices.find { it.isCurrentDevice }?.also {
+ // If our current device's push subscription needs to be renewed, then we
+ // possibly missed some push notifications, so check for that here.
+ // (This doesn't actually perform the renewal, FxaPushSupportFeature does that.)
+ if (it.subscription == null || it.subscriptionExpired) {
+ logger.info("Current device needs push endpoint registration, so checking for missed commands")
+ pollForCommands()
+ }
+ }
+
+ // Filter out the current devices.
+ val otherDevices = allDevices.filter { !it.isCurrentDevice }
+
+ val newState = ConstellationState(currentDevice, otherDevices)
+ constellationState = newState
+
+ logger.info("Refreshed device list; saw ${allDevices.size} device(s).")
+
+ // NB: at this point, 'constellationState' might have changed.
+ // Notify with an immutable, local 'newState' instead.
+ deviceObserverRegistry.notifyObservers {
+ logger.info("Notifying observer about constellation updates.")
+ onDevicesUpdate(newState)
+ }
+
+ true
+ }
+ }
+
+ /**
+ * Get all devices in the constellation.
+ * @return A list of all devices in the constellation, or `null` on failure.
+ */
+ private suspend fun fetchAllDevices(): List<Device>? {
+ return handleFxaExceptions(logger, "fetching all devices", { null }) {
+ account.getDevices().map { it.into() }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceSettingsCache.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceSettingsCache.kt
new file mode 100644
index 0000000000..303326cef9
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceSettingsCache.kt
@@ -0,0 +1,66 @@
+/* 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.service.fxa
+
+import android.content.Context
+import android.content.SharedPreferences
+import mozilla.appservices.sync15.DeviceType
+import mozilla.appservices.syncmanager.DeviceSettings
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.SharedPreferencesCache
+import org.json.JSONObject
+import java.lang.IllegalArgumentException
+import java.lang.IllegalStateException
+
+private const val CACHE_NAME = "FxaDeviceSettingsCache"
+private const val CACHE_KEY = CACHE_NAME
+private const val KEY_FXA_DEVICE_ID = "kid"
+private const val KEY_DEVICE_NAME = "syncKey"
+private const val KEY_DEVICE_TYPE = "tokenServerUrl"
+
+/**
+ * A thin wrapper around [SharedPreferences] which knows how to serialize/deserialize [DeviceSettings].
+ *
+ * This class exists to provide background sync workers with access to [DeviceSettings].
+ */
+class FxaDeviceSettingsCache(context: Context) : SharedPreferencesCache<DeviceSettings>(context) {
+ override val logger = Logger("SyncAuthInfoCache")
+ override val cacheKey = CACHE_KEY
+ override val cacheName = CACHE_NAME
+
+ override fun DeviceSettings.toJSON(): JSONObject {
+ return JSONObject().also {
+ it.put(KEY_FXA_DEVICE_ID, this.fxaDeviceId)
+ it.put(KEY_DEVICE_NAME, this.name)
+ it.put(KEY_DEVICE_TYPE, this.kind.toString())
+ }
+ }
+
+ override fun fromJSON(obj: JSONObject): DeviceSettings {
+ return DeviceSettings(
+ fxaDeviceId = obj.getString(KEY_FXA_DEVICE_ID),
+ name = obj.getString(KEY_DEVICE_NAME),
+ kind = obj.getString(KEY_DEVICE_TYPE).toDeviceType(),
+ )
+ }
+
+ /**
+ * @param name New device name to write into the cache.
+ */
+ fun updateCachedName(name: String) {
+ val cached = getCached() ?: throw IllegalStateException("Trying to update cached value in an empty cache")
+ setToCache(cached.copy(name = name))
+ }
+
+ private fun String.toDeviceType(): DeviceType {
+ return when (this) {
+ "DESKTOP" -> DeviceType.DESKTOP
+ "MOBILE" -> DeviceType.MOBILE
+ "TABLET" -> DeviceType.TABLET
+ "VR" -> DeviceType.VR
+ "TV" -> DeviceType.TV
+ else -> throw IllegalArgumentException("Unknown device type in cached string: $this")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/SyncAuthInfoCache.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/SyncAuthInfoCache.kt
new file mode 100644
index 0000000000..8261c30b9f
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/SyncAuthInfoCache.kt
@@ -0,0 +1,58 @@
+/* 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.service.fxa
+
+import android.content.Context
+import android.content.SharedPreferences
+import mozilla.components.concept.sync.SyncAuthInfo
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.SharedPreferencesCache
+import org.json.JSONObject
+import java.util.concurrent.TimeUnit
+
+private const val CACHE_NAME = "SyncAuthInfoCache"
+private const val CACHE_KEY = CACHE_NAME
+private const val KEY_FXA_ACCESS_TOKEN = "fxaAccessToken"
+private const val KEY_FXA_ACCESS_TOKEN_EXPIRES_AT = "fxaAccessTokenExpiresAt"
+private const val KEY_KID = "kid"
+private const val KEY_SYNC_KEY = "syncKey"
+private const val KEY_TOKEN_SERVER_URL = "tokenServerUrl"
+
+/**
+ * A thin wrapper around [SharedPreferences] which knows how to serialize/deserialize [SyncAuthInfo].
+ *
+ * This class exists to provide background sync workers with access to [SyncAuthInfo].
+ */
+class SyncAuthInfoCache(context: Context) : SharedPreferencesCache<SyncAuthInfo>(context) {
+ override val logger = Logger("SyncAuthInfoCache")
+ override val cacheKey = CACHE_KEY
+ override val cacheName = CACHE_NAME
+
+ override fun SyncAuthInfo.toJSON(): JSONObject {
+ return JSONObject().also {
+ it.put(KEY_KID, this.kid)
+ it.put(KEY_FXA_ACCESS_TOKEN, this.fxaAccessToken)
+ it.put(KEY_FXA_ACCESS_TOKEN_EXPIRES_AT, this.fxaAccessTokenExpiresAt)
+ it.put(KEY_SYNC_KEY, this.syncKey)
+ it.put(KEY_TOKEN_SERVER_URL, this.tokenServerUrl)
+ }
+ }
+
+ override fun fromJSON(obj: JSONObject): SyncAuthInfo {
+ return SyncAuthInfo(
+ kid = obj.getString(KEY_KID),
+ fxaAccessToken = obj.getString(KEY_FXA_ACCESS_TOKEN),
+ fxaAccessTokenExpiresAt = obj.getLong(KEY_FXA_ACCESS_TOKEN_EXPIRES_AT),
+ syncKey = obj.getString(KEY_SYNC_KEY),
+ tokenServerUrl = obj.getString(KEY_TOKEN_SERVER_URL),
+ )
+ }
+
+ fun expired(): Boolean {
+ val expiresAt = getCached()?.fxaAccessTokenExpiresAt ?: return true
+ val now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
+
+ return expiresAt <= now
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/SyncFacts.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/SyncFacts.kt
new file mode 100644
index 0000000000..a0233fc5d4
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/SyncFacts.kt
@@ -0,0 +1,40 @@
+/* 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.service.fxa
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to FxA Sync operations.
+ */
+class SyncFacts {
+
+ /**
+ * Specific types of telemetry items.
+ */
+ object Items {
+ const val SYNC_FAILED = "sync_failed"
+ }
+}
+
+private fun emitSyncFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.SERVICE_FIREFOX_ACCOUNTS,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitSyncFailedFact() = emitSyncFact(Action.INTERACTION, SyncFacts.Items.SYNC_FAILED)
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
new file mode 100644
index 0000000000..737ff3c273
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
@@ -0,0 +1,251 @@
+/* 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/. */
+@file:SuppressWarnings("MatchingDeclarationName")
+
+package mozilla.components.service.fxa
+
+import mozilla.appservices.fxaclient.AccessTokenInfo
+import mozilla.appservices.fxaclient.AccountEvent
+import mozilla.appservices.fxaclient.Device
+import mozilla.appservices.fxaclient.IncomingDeviceCommand
+import mozilla.appservices.fxaclient.Profile
+import mozilla.appservices.fxaclient.ScopedKey
+import mozilla.appservices.fxaclient.TabHistoryEntry
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.Avatar
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.OAuthScopedKey
+import mozilla.components.concept.sync.SyncAuthInfo
+import mozilla.appservices.fxaclient.DeviceCapability as RustDeviceCapability
+import mozilla.appservices.fxaclient.DevicePushSubscription as RustDevicePushSubscription
+import mozilla.appservices.sync15.DeviceType as RustDeviceType
+
+/**
+ * Converts a raw 'action' string into an [AuthType] instance.
+ * Actions come to us from FxA during an OAuth login, either over the WebChannel or via the redirect URL.
+ */
+fun String?.toAuthType(): AuthType {
+ return when (this) {
+ "signin" -> AuthType.Signin
+ "signup" -> AuthType.Signup
+ "pairing" -> AuthType.Pairing
+ // We want to gracefully handle an 'action' we don't know about.
+ // This also covers the `null` case.
+ else -> AuthType.OtherExternal(this)
+ }
+}
+
+/**
+ * Captures basic OAuth authentication data (code, state) and any additional data FxA passes along.
+ * @property authType Type of authentication which caused this object to be created.
+ * @property code OAuth code.
+ * @property state OAuth state.
+ * @property declinedEngines An optional list of [SyncEngine]s that user declined to sync.
+ */
+data class FxaAuthData(
+ val authType: AuthType,
+ val code: String,
+ val state: String,
+ val declinedEngines: Set<SyncEngine>? = null,
+) {
+ override fun toString(): String {
+ return "authType: $authType, code: XXX, state: XXX, declinedEngines: $declinedEngines"
+ }
+}
+
+// The rest of this file describes translations between fxaclient's internal type definitions and analogous
+// types defined by concept-sync. It's a little tedious, but ensures decoupling between abstract
+// definitions and a concrete implementation. In practice, this means that concept-sync doesn't need
+// impose a dependency on fxaclient native library.
+
+fun AccessTokenInfo.into(): mozilla.components.concept.sync.AccessTokenInfo {
+ return mozilla.components.concept.sync.AccessTokenInfo(
+ scope = this.scope,
+ token = this.token,
+ key = this.key?.into(),
+ expiresAt = this.expiresAt,
+ )
+}
+
+/**
+ * Converts a generic [AccessTokenInfo] into a Firefox Sync-friendly [SyncAuthInfo] instance which
+ * may be used for data synchronization.
+ *
+ * @return An [SyncAuthInfo] which is guaranteed to have a sync key.
+ * @throws IllegalStateException if [AccessTokenInfo] didn't have key information.
+ */
+fun mozilla.components.concept.sync.AccessTokenInfo.asSyncAuthInfo(tokenServerUrl: String): SyncAuthInfo {
+ val keyInfo = this.key ?: throw AccessTokenUnexpectedlyWithoutKey()
+
+ return SyncAuthInfo(
+ kid = keyInfo.kid,
+ fxaAccessToken = this.token,
+ fxaAccessTokenExpiresAt = this.expiresAt,
+ syncKey = keyInfo.k,
+ tokenServerUrl = tokenServerUrl,
+ )
+}
+
+fun ScopedKey.into(): OAuthScopedKey {
+ return OAuthScopedKey(kid = this.kid, k = this.k, kty = this.kty, scope = this.scope)
+}
+
+fun Profile.into(): mozilla.components.concept.sync.Profile {
+ return mozilla.components.concept.sync.Profile(
+ uid = this.uid,
+ email = this.email,
+ avatar = this.avatar.let {
+ Avatar(
+ url = it,
+ isDefault = this.isDefaultAvatar,
+ )
+ },
+ displayName = this.displayName,
+ )
+}
+
+internal fun RustDeviceType.into(): DeviceType {
+ return when (this) {
+ RustDeviceType.DESKTOP -> DeviceType.DESKTOP
+ RustDeviceType.MOBILE -> DeviceType.MOBILE
+ RustDeviceType.TABLET -> DeviceType.TABLET
+ RustDeviceType.TV -> DeviceType.TV
+ RustDeviceType.VR -> DeviceType.VR
+ RustDeviceType.UNKNOWN -> DeviceType.UNKNOWN
+ }
+}
+
+/**
+ * Convert between the native-code DeviceType data class
+ * and the one from the corresponding a-c concept.
+ */
+fun DeviceType.into(): RustDeviceType {
+ return when (this) {
+ DeviceType.DESKTOP -> RustDeviceType.DESKTOP
+ DeviceType.MOBILE -> RustDeviceType.MOBILE
+ DeviceType.TABLET -> RustDeviceType.TABLET
+ DeviceType.TV -> RustDeviceType.TV
+ DeviceType.VR -> RustDeviceType.VR
+ DeviceType.UNKNOWN -> RustDeviceType.UNKNOWN
+ }
+}
+
+/**
+ * Convert between the native-code DeviceCapability data class
+ * and the one from the corresponding a-c concept.
+ */
+fun DeviceCapability.into(): RustDeviceCapability {
+ return when (this) {
+ DeviceCapability.SEND_TAB -> RustDeviceCapability.SEND_TAB
+ }
+}
+
+/**
+ * Convert between the a-c concept DeviceCapability class and the corresponding
+ * native-code DeviceCapability data class.
+ */
+fun RustDeviceCapability.into(): DeviceCapability {
+ return when (this) {
+ RustDeviceCapability.SEND_TAB -> DeviceCapability.SEND_TAB
+ }
+}
+
+/**
+ * Convert between the a-c concept DevicePushSubscription class and the corresponding
+ * native-code DevicePushSubscription data class.
+ */
+fun mozilla.components.concept.sync.DevicePushSubscription.into(): RustDevicePushSubscription {
+ return RustDevicePushSubscription(
+ endpoint = this.endpoint,
+ authKey = this.authKey,
+ publicKey = this.publicKey,
+ )
+}
+
+/**
+ * Convert between the native-code DevicePushSubscription data class
+ * and the one from the corresponding a-c concept.
+ */
+fun RustDevicePushSubscription.into(): mozilla.components.concept.sync.DevicePushSubscription {
+ return mozilla.components.concept.sync.DevicePushSubscription(
+ endpoint = this.endpoint,
+ authKey = this.authKey,
+ publicKey = this.publicKey,
+ )
+}
+
+fun Device.into(): mozilla.components.concept.sync.Device {
+ return mozilla.components.concept.sync.Device(
+ id = this.id,
+ isCurrentDevice = this.isCurrentDevice,
+ deviceType = this.deviceType.into(),
+ displayName = this.displayName,
+ lastAccessTime = this.lastAccessTime,
+ subscriptionExpired = this.pushEndpointExpired,
+ capabilities = this.capabilities.map { it.into() },
+ subscription = this.pushSubscription?.into(),
+ )
+}
+
+fun mozilla.components.concept.sync.Device.into(): Device {
+ return Device(
+ id = this.id,
+ isCurrentDevice = this.isCurrentDevice,
+ deviceType = this.deviceType.into(),
+ displayName = this.displayName,
+ lastAccessTime = this.lastAccessTime,
+ pushEndpointExpired = this.subscriptionExpired,
+ capabilities = this.capabilities.map { it.into() },
+ pushSubscription = this.subscription?.into(),
+ )
+}
+
+fun TabHistoryEntry.into(): mozilla.components.concept.sync.TabData {
+ return mozilla.components.concept.sync.TabData(
+ title = this.title,
+ url = this.url,
+ )
+}
+
+fun mozilla.components.concept.sync.TabData.into(): TabHistoryEntry {
+ return TabHistoryEntry(
+ title = this.title,
+ url = this.url,
+ )
+}
+
+fun AccountEvent.into(): mozilla.components.concept.sync.AccountEvent {
+ return when (this) {
+ is AccountEvent.CommandReceived ->
+ mozilla.components.concept.sync.AccountEvent.DeviceCommandIncoming(command = this.command.into())
+ is AccountEvent.ProfileUpdated ->
+ mozilla.components.concept.sync.AccountEvent.ProfileUpdated
+ is AccountEvent.AccountAuthStateChanged ->
+ mozilla.components.concept.sync.AccountEvent.AccountAuthStateChanged
+ is AccountEvent.AccountDestroyed ->
+ mozilla.components.concept.sync.AccountEvent.AccountDestroyed
+ is AccountEvent.DeviceConnected ->
+ mozilla.components.concept.sync.AccountEvent.DeviceConnected(deviceName = this.deviceName)
+ is AccountEvent.DeviceDisconnected ->
+ mozilla.components.concept.sync.AccountEvent.DeviceDisconnected(
+ deviceId = this.deviceId,
+ isLocalDevice = this.isLocalDevice,
+ )
+ is AccountEvent.Unknown -> mozilla.components.concept.sync.AccountEvent.Unknown
+ }
+}
+
+fun IncomingDeviceCommand.into(): mozilla.components.concept.sync.DeviceCommandIncoming {
+ return when (this) {
+ is IncomingDeviceCommand.TabReceived -> this.into()
+ }
+}
+
+fun IncomingDeviceCommand.TabReceived.into(): mozilla.components.concept.sync.DeviceCommandIncoming.TabReceived {
+ return mozilla.components.concept.sync.DeviceCommandIncoming.TabReceived(
+ from = this.sender?.into(),
+ entries = this.payload.entries.map { it.into() },
+ )
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Utils.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Utils.kt
new file mode 100644
index 0000000000..64322a6205
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Utils.kt
@@ -0,0 +1,153 @@
+/* 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.service.fxa
+
+import mozilla.components.concept.sync.AuthFlowUrl
+import mozilla.components.concept.sync.FxAEntryPoint
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.ServiceResult
+import mozilla.components.service.fxa.manager.GlobalAccountManager
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Runs a provided lambda, and if that throws non-panic, non-auth FxA exception, runs [handleErrorBlock].
+ * If that lambda throws an FxA auth exception, notifies [authErrorRegistry], and runs [postHandleAuthErrorBlock].
+ *
+ * @param block A lambda to execute which mail fail with an [FxaException].
+ * @param postHandleAuthErrorBlock A lambda to execute if [block] failed with [FxaUnauthorizedException].
+ * @param handleErrorBlock A lambda to execute if [block] fails with a non-panic, non-auth [FxaException].
+ * @return object of type T, as defined by [block].
+ */
+suspend fun <T> handleFxaExceptions(
+ logger: Logger,
+ operation: String,
+ block: suspend () -> T,
+ postHandleAuthErrorBlock: (e: FxaUnauthorizedException) -> T,
+ handleErrorBlock: (e: FxaException) -> T,
+): T {
+ return try {
+ logger.info("Executing: $operation")
+ val res = block()
+ logger.info("Successfully executed: $operation")
+ res
+ } catch (e: FxaException) {
+ // We'd like to simply crash in case of certain errors (e.g. panics).
+ if (e.shouldPropagate()) {
+ throw e
+ }
+ when (e) {
+ is FxaUnauthorizedException -> {
+ logger.warn("Auth error while running: $operation")
+ GlobalAccountManager.authError(operation)
+ postHandleAuthErrorBlock(e)
+ }
+ else -> {
+ logger.error("Error while running: $operation", e)
+ handleErrorBlock(e)
+ }
+ }
+ }
+}
+
+/**
+ * Helper method that handles [FxaException] and allows specifying a lazy default value via [default]
+ * block for use in case of errors. Execution is wrapped in log statements.
+ */
+suspend fun <T> handleFxaExceptions(
+ logger: Logger,
+ operation: String,
+ default: (error: FxaException) -> T,
+ block: suspend () -> T,
+): T {
+ return handleFxaExceptions(logger, operation, block, { default(it) }, { default(it) })
+}
+
+/**
+ * Helper method that handles [FxaException] and returns a [Boolean] success flag as a result.
+ */
+suspend fun handleFxaExceptions(logger: Logger, operation: String, block: () -> Unit): Boolean {
+ return handleFxaExceptions(
+ logger,
+ operation,
+ { false },
+ {
+ block()
+ true
+ },
+ )
+}
+
+/**
+ * Simplified version of Kotlin's inline class version that can be used as a return value.
+ */
+internal sealed class Result<out T> {
+ data class Success<out T>(val value: T) : Result<T>()
+ object Failure : Result<Nothing>()
+}
+
+/**
+ * A helper function which allows retrying a [block] of suspend code for a few times in case it fails.
+ *
+ * @param logger [Logger] that will be used to log retry attempts/results
+ * @param retryCount How many retry attempts are allowed
+ * @param block A suspend function to execute
+ * @return A [Result.Success] wrapping result of execution of [block] on (eventual) success,
+ * or [Result.Failure] otherwise.
+ */
+internal suspend fun <T> withRetries(logger: Logger, retryCount: Int, block: suspend () -> T): Result<T> {
+ var attempt = 0
+ var res: T? = null
+ while (attempt < retryCount && (res == null || res == false)) {
+ attempt += 1
+ logger.info("withRetries: attempt $attempt/$retryCount")
+ res = block()
+ }
+ return if (res == null || res == false) {
+ logger.warn("withRetries: all attempts failed")
+ Result.Failure
+ } else {
+ Result.Success(res)
+ }
+}
+
+/**
+ * A helper function which allows retrying a [block] of suspend code for a few times in case it fails.
+ * Short-circuits execution if [block] returns [ServiceResult.AuthError] during any of its attempts.
+ *
+ * @param logger [Logger] that will be used to log retry attempts/results
+ * @param retryCount How many retry attempts are allowed
+ * @param block A suspend function to execute
+ * @return A [ServiceResult] representing result of [block] execution.
+ */
+internal suspend fun withServiceRetries(
+ logger: Logger,
+ retryCount: Int,
+ block: suspend () -> ServiceResult,
+): ServiceResult {
+ var attempt = 0
+ do {
+ attempt += 1
+ logger.info("withServiceRetries: attempt $attempt/$retryCount")
+ when (val res = block()) {
+ ServiceResult.Ok, ServiceResult.AuthError -> return res
+ ServiceResult.OtherError -> {}
+ }
+ } while (attempt < retryCount)
+
+ logger.warn("withServiceRetries: all attempts failed")
+ return ServiceResult.OtherError
+}
+
+internal suspend fun String?.asAuthFlowUrl(
+ account: OAuthAccount,
+ scopes: Set<String>,
+ entrypoint: FxAEntryPoint,
+): AuthFlowUrl? {
+ return if (this != null) {
+ account.beginPairingFlow(this, scopes, entrypoint)
+ } else {
+ account.beginOAuthFlow(scopes, entrypoint)
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/AppServicesStateMachineChecker.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/AppServicesStateMachineChecker.kt
new file mode 100644
index 0000000000..e7ba7fdcdc
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/AppServicesStateMachineChecker.kt
@@ -0,0 +1,224 @@
+/* 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.service.fxa.manager
+
+import mozilla.appservices.fxaclient.FxaEvent
+import mozilla.appservices.fxaclient.FxaRustAuthState
+import mozilla.appservices.fxaclient.FxaState
+import mozilla.appservices.fxaclient.FxaStateCheckerEvent
+import mozilla.appservices.fxaclient.FxaStateCheckerState
+import mozilla.appservices.fxaclient.FxaStateMachineChecker
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.service.fxa.into
+import mozilla.appservices.fxaclient.DeviceConfig as ASDeviceConfig
+
+/**
+ * Checks the new app-services state machine logic against the current android-components code
+ *
+ * This is a temporary measure to prep for migrating the android-components code to using the new
+ * app-services state machine. It performs a "dry-run" test of the code where we check the new logic
+ * against the current logic, without performing any side-effects.
+ *
+ * If one of the checks fails, then we report an error to Sentry. After that the calls become
+ * no-ops to avoid spamming sentry with errors from a single client. By checking the Sentry errors,
+ * we can find places where the application-services logic doesn't match the current
+ * android-components logic and fix the issue.
+ *
+ * Once we determine that the new application-services code is correct, let's switch the
+ * android-components code to using it (https://bugzilla.mozilla.org/show_bug.cgi?id=1867793) and
+ * delete all this code.
+ */
+object AppServicesStateMachineChecker {
+ /**
+ * The Rust state machine checker. This handles the actual checks and sentry reporting.
+ */
+ private val rustChecker = FxaStateMachineChecker()
+
+ /**
+ * Handle an event about to be processed
+ *
+ * Call this when processing an android-components event, after checking that it's valid.
+ */
+ internal fun handleEvent(event: Event, deviceConfig: DeviceConfig, scopes: Set<String>) {
+ val convertedEvent = when (event) {
+ Event.Account.Start -> FxaEvent.Initialize(
+ ASDeviceConfig(
+ name = deviceConfig.name,
+ deviceType = deviceConfig.type.into(),
+ capabilities = ArrayList(deviceConfig.capabilities.map { it.into() }),
+ ),
+ )
+ is Event.Account.BeginEmailFlow -> FxaEvent.BeginOAuthFlow(ArrayList(scopes), event.entrypoint.entryName)
+ is Event.Account.BeginPairingFlow -> {
+ // pairingUrl should always be non-null, if it is somehow null let's use a
+ // placeholder value that can be identified when checking in sentry
+ val pairingUrl = event.pairingUrl ?: "<null>"
+ FxaEvent.BeginPairingFlow(pairingUrl, ArrayList(scopes), event.entrypoint.entryName)
+ }
+ is Event.Account.AuthenticationError -> {
+ // There are basically 2 ways for this to happen:
+ //
+ // - Another component called `FxaAccountManager.encounteredAuthError()`. In this
+ // case, we should initiate the state transition by sending the state machine the
+ // `FxaEvent.CheckAuthorizationStatus`
+ // - `FxaAccountManager` sent it to itself, because there was an error when
+ // `internalStateSideEffects` called `finalizeDevice()`. In this case, we're
+ // already in the middle of a state transition and already sent the state machine
+ // the `EnsureCapabilitiesAuthError` event, so we should ignore it.
+ if (event.operation == "finalizeDevice") {
+ return
+ } else {
+ FxaEvent.CheckAuthorizationStatus
+ }
+ }
+ Event.Account.AccessTokenKeyError -> FxaEvent.CheckAuthorizationStatus
+ Event.Account.Logout -> FxaEvent.Disconnect
+ // This is the one ProgressEvent that's considered a "public event" in app-services
+ is Event.Progress.AuthData -> FxaEvent.CompleteOAuthFlow(event.authData.code, event.authData.state)
+ is Event.Progress.CancelAuth -> FxaEvent.CancelOAuthFlow
+ else -> return
+ }
+ rustChecker.handlePublicEvent(convertedEvent)
+ }
+
+ /**
+ * Check a new account state
+ *
+ * Call this after transitioning to an android-components account state.
+ */
+ internal fun checkAccountState(state: AccountState) {
+ val convertedState = when (state) {
+ AccountState.NotAuthenticated -> FxaState.Disconnected
+ is AccountState.Authenticating -> FxaState.Authenticating(state.oAuthUrl)
+ AccountState.Authenticated -> FxaState.Connected
+ AccountState.AuthenticationProblem -> FxaState.AuthIssues
+ }
+ rustChecker.checkPublicState(convertedState)
+ }
+
+ /**
+ * General validation for new progress state being processed by the AC state machine.
+ *
+ * This handles all validation for most state transitions in a simple manner. The one transition
+ * it can't handle is completing oauth, which entails multiple FxA calls and can fail in multiple
+ * different ways. For that, the lower-level `checkInternalState` and `handleInternalEvent` are
+ * used.
+ */
+ @Suppress("LongMethod")
+ internal fun validateProgressEvent(progressEvent: Event.Progress, via: Event, scopes: Set<String>) {
+ when (progressEvent) {
+ Event.Progress.AccountRestored -> {
+ AppServicesStateMachineChecker.checkInternalState(FxaStateCheckerState.GetAuthState)
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.GetAuthStateSuccess(FxaRustAuthState.CONNECTED),
+ )
+ }
+ Event.Progress.AccountNotFound -> {
+ AppServicesStateMachineChecker.checkInternalState(FxaStateCheckerState.GetAuthState)
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.GetAuthStateSuccess(FxaRustAuthState.DISCONNECTED),
+ )
+ }
+ is Event.Progress.StartedOAuthFlow -> {
+ when (via) {
+ is Event.Account.BeginEmailFlow -> {
+ AppServicesStateMachineChecker.checkInternalState(
+ FxaStateCheckerState.BeginOAuthFlow(ArrayList(scopes), via.entrypoint.entryName),
+ )
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.BeginOAuthFlowSuccess(progressEvent.oAuthUrl),
+ )
+ }
+ is Event.Account.BeginPairingFlow -> {
+ AppServicesStateMachineChecker.checkInternalState(
+ FxaStateCheckerState.BeginPairingFlow(
+ via.pairingUrl!!,
+ ArrayList(scopes),
+ via.entrypoint.entryName,
+ ),
+ )
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.BeginPairingFlowSuccess(progressEvent.oAuthUrl),
+ )
+ }
+ // This branch should never be taken, if it does we'll probably see a state
+ // check error down the line.
+ else -> Unit
+ }
+ }
+ Event.Progress.FailedToBeginAuth -> {
+ when (via) {
+ is Event.Account.BeginEmailFlow -> {
+ AppServicesStateMachineChecker.checkInternalState(
+ FxaStateCheckerState.BeginOAuthFlow(ArrayList(scopes), via.entrypoint.entryName),
+ )
+ }
+ is Event.Account.BeginPairingFlow -> {
+ AppServicesStateMachineChecker.checkInternalState(
+ FxaStateCheckerState.BeginPairingFlow(
+ via.pairingUrl!!,
+ ArrayList(scopes),
+ via.entrypoint.entryName,
+ ),
+ )
+ }
+ // This branch should never be taken, if it does we'll probably see a state
+ // check error down the line.
+ else -> Unit
+ }
+ AppServicesStateMachineChecker.handleInternalEvent(FxaStateCheckerEvent.CallError)
+ }
+ Event.Progress.LoggedOut -> {
+ AppServicesStateMachineChecker.checkInternalState(
+ FxaStateCheckerState.Disconnect,
+ )
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.DisconnectSuccess,
+ )
+ }
+ Event.Progress.RecoveredFromAuthenticationProblem -> {
+ AppServicesStateMachineChecker.checkInternalState(FxaStateCheckerState.CheckAuthorizationStatus)
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.CheckAuthorizationStatusSuccess(true),
+ )
+ }
+ Event.Progress.FailedToRecoverFromAuthenticationProblem -> {
+ if (via is Event.Account.AuthenticationError &&
+ via.errorCountWithinTheTimeWindow >= AUTH_CHECK_CIRCUIT_BREAKER_COUNT
+ ) {
+ // In this case, the state machine fails early and doesn't actualy make any
+ // calls
+ return
+ }
+
+ AppServicesStateMachineChecker.checkInternalState(FxaStateCheckerState.CheckAuthorizationStatus)
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.CheckAuthorizationStatusSuccess(false),
+ )
+ }
+ else -> Unit
+ }
+ }
+
+ /**
+ * Check an app-services internal state
+ *
+ * The app-services internal states correspond to internal firefox account method calls. Call
+ * this before making one of those calls.
+ */
+ internal fun checkInternalState(state: FxaStateCheckerState) {
+ rustChecker.checkInternalState(state)
+ }
+
+ /**
+ * Handle an app-services internal event
+ *
+ * The app-services internal states correspond the results of internal firefox account method
+ * calls. Call this before after making a call.
+ */
+ internal fun handleInternalEvent(event: FxaStateCheckerEvent) {
+ rustChecker.handleInternalEvent(event)
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt
new file mode 100644
index 0000000000..03a7d25635
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt
@@ -0,0 +1,938 @@
+/* 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.service.fxa.manager
+
+import android.content.Context
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.withContext
+import mozilla.appservices.fxaclient.FxaStateCheckerEvent
+import mozilla.appservices.fxaclient.FxaStateCheckerState
+import mozilla.appservices.syncmanager.DeviceSettings
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthFlowError
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.concept.sync.FxAEntryPoint
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.concept.sync.ServiceResult
+import mozilla.components.service.fxa.AccessTokenUnexpectedlyWithoutKey
+import mozilla.components.service.fxa.AccountManagerException
+import mozilla.components.service.fxa.AccountOnDisk
+import mozilla.components.service.fxa.AccountStorage
+import mozilla.components.service.fxa.FxaAuthData
+import mozilla.components.service.fxa.FxaDeviceSettingsCache
+import mozilla.components.service.fxa.FxaSyncScopedKeyMissingException
+import mozilla.components.service.fxa.Result
+import mozilla.components.service.fxa.SecureAbove22AccountStorage
+import mozilla.components.service.fxa.ServerConfig
+import mozilla.components.service.fxa.SharedPrefAccountStorage
+import mozilla.components.service.fxa.StorageWrapper
+import mozilla.components.service.fxa.SyncAuthInfoCache
+import mozilla.components.service.fxa.SyncConfig
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.asAuthFlowUrl
+import mozilla.components.service.fxa.asSyncAuthInfo
+import mozilla.components.service.fxa.emitSyncFailedFact
+import mozilla.components.service.fxa.into
+import mozilla.components.service.fxa.sync.SyncManager
+import mozilla.components.service.fxa.sync.SyncReason
+import mozilla.components.service.fxa.sync.SyncStatusObserver
+import mozilla.components.service.fxa.sync.WorkManagerSyncManager
+import mozilla.components.service.fxa.sync.clearSyncState
+import mozilla.components.service.fxa.withRetries
+import mozilla.components.service.fxa.withServiceRetries
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import mozilla.components.support.base.utils.NamedThreadFactory
+import java.io.Closeable
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.CoroutineContext
+
+// Necessary to fetch a profile.
+const val SCOPE_PROFILE = "profile"
+
+// Necessary to obtain sync keys.
+const val SCOPE_SYNC = "https://identity.mozilla.com/apps/oldsync"
+
+// Necessary to obtain a sessionToken, which gives full access to the account.
+const val SCOPE_SESSION = "https://identity.mozilla.com/tokens/session"
+
+// If we see more than AUTH_CHECK_CIRCUIT_BREAKER_COUNT checks, and each is less than
+// AUTH_CHECK_CIRCUIT_BREAKER_RESET_MS since the last check, then we'll trigger a "circuit breaker".
+const val AUTH_CHECK_CIRCUIT_BREAKER_RESET_MS = 1000L * 10
+const val AUTH_CHECK_CIRCUIT_BREAKER_COUNT = 10
+// This logic is in place to protect ourselves from endless auth recovery loops, while at the same
+// time allowing for a few 401s to hit the state machine in a quick succession.
+// For example, initializing the account state machine & syncing after letting our access tokens expire
+// due to long period of inactivity will trigger a few 401s, and that shouldn't be a cause for concern.
+
+const val MAX_NETWORK_RETRIES = 3
+
+/**
+ * An account manager which encapsulates various internal details of an account lifecycle and provides
+ * an observer interface along with a public API for interacting with an account.
+ * The internal state machine abstracts over state space as exposed by the fxaclient library, not
+ * the internal states experienced by lower-level representation of a Firefox Account; those are opaque to us.
+ *
+ * Class is 'open' to facilitate testing.
+ *
+ * @param context A [Context] instance that's used for internal messaging and interacting with local storage.
+ * @param serverConfig A [ServerConfig] used for account initialization.
+ * @param deviceConfig A description of the current device (name, type, capabilities).
+ * @param syncConfig Optional, initial sync behaviour configuration. Sync will be disabled if this is `null`.
+ * @param applicationScopes A set of scopes which will be requested during account authentication.
+ */
+@Suppress("TooManyFunctions", "LargeClass")
+open class FxaAccountManager(
+ private val context: Context,
+ @get:VisibleForTesting val serverConfig: ServerConfig,
+ private val deviceConfig: DeviceConfig,
+ private val syncConfig: SyncConfig?,
+ private val applicationScopes: Set<String> = emptySet(),
+ private val crashReporter: CrashReporting? = null,
+ // We want a single-threaded execution model for our account-related "actions" (state machine side-effects).
+ // That is, we want to ensure a sequential execution flow, but on a background thread.
+ private val coroutineContext: CoroutineContext = Executors.newSingleThreadExecutor(
+ NamedThreadFactory("FxaAccountManager"),
+ ).asCoroutineDispatcher() + SupervisorJob(),
+) : Closeable, Observable<AccountObserver> by ObserverRegistry() {
+ private val logger = Logger("FirefoxAccountStateMachine")
+
+ @Volatile
+ private var latestAuthState: String? = null
+
+ // Used to detect multiple auth recovery attempts at once
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1864994)
+ private var authRecoveryScheduled = AtomicBoolean(false)
+ private fun checkForMultipleRequiveryCalls() {
+ val alreadyScheduled = authRecoveryScheduled.getAndSet(true)
+ if (alreadyScheduled) {
+ crashReporter?.recordCrashBreadcrumb(Breadcrumb("multiple fxa recoveries scheduled at once"))
+ }
+ }
+ private fun finishedAuthRecovery() {
+ authRecoveryScheduled.set(false)
+ }
+
+ init {
+ GlobalAccountManager.setInstance(this)
+ }
+
+ private val accountOnDisk by lazy { getStorageWrapper().account() }
+ private val account by lazy { accountOnDisk.account() }
+
+ // Note on threading: we use a single-threaded executor, so there's no concurrent access possible.
+ // However, that executor doesn't guarantee that it'll always use the same thread, and so vars
+ // are marked as volatile for across-thread visibility. Similarly, event queue uses a concurrent
+ // list, although that's probably an overkill.
+ @Volatile private var profile: Profile? = null
+
+ // We'd like to persist this state, so that we can short-circuit transition to AuthenticationProblem on
+ // initialization, instead of triggering the full state machine knowing in advance we'll hit auth problems.
+ // See https://github.com/mozilla-mobile/android-components/issues/5102
+ @Volatile private var state: State = State.Idle(AccountState.NotAuthenticated)
+
+ @Volatile private var isAccountManagerReady: Boolean = false
+ private val eventQueue = ConcurrentLinkedQueue<Event>()
+
+ @VisibleForTesting
+ val accountEventObserverRegistry = ObserverRegistry<AccountEventsObserver>()
+
+ @VisibleForTesting
+ open val syncStatusObserverRegistry = ObserverRegistry<SyncStatusObserver>()
+
+ // We always obtain a "profile" scope, as that's assumed to be needed for any application integration.
+ // We obtain a sync scope only if this was requested by the application via SyncConfig.
+ // Additionally, we obtain any scopes that the application requested explicitly.
+ private val scopes: Set<String>
+ get() = if (syncConfig != null) {
+ setOf(SCOPE_PROFILE, SCOPE_SYNC)
+ } else {
+ setOf(SCOPE_PROFILE)
+ }.plus(applicationScopes)
+
+ // Internal backing field for the syncManager. This will be `null` if passed in SyncConfig (either
+ // via the constructor, or via [setSyncConfig]) is also `null` - that is, sync will be disabled.
+ // Note that trying to perform a sync while account isn't authenticated will not succeed.
+ @GuardedBy("this")
+ private var syncManager: SyncManager? = null
+
+ init {
+ syncConfig?.let {
+ // Initialize sync manager with the passed-in config.
+ if (syncConfig.supportedEngines.isEmpty()) {
+ throw IllegalArgumentException("Set of supported engines can't be empty")
+ }
+
+ syncManager = createSyncManager(syncConfig).also {
+ // Observe account state changes.
+ this.register(AccountsToSyncIntegration(it))
+
+ // Observe sync status changes.
+ it.registerSyncStatusObserver(SyncToAccountsIntegration(this))
+ }
+ }
+
+ if (syncManager == null) {
+ logger.info("Sync is disabled")
+ } else {
+ logger.info("Sync is enabled")
+ }
+ }
+
+ /**
+ * @return A list of currently supported [SyncEngine]s. `null` if sync isn't configured.
+ */
+ fun supportedSyncEngines(): Set<SyncEngine>? {
+ // Notes on why this exists:
+ // Parts of the system that make up an "fxa + sync" experience need to know which engines
+ // are supported by an application. For example, FxA web content UI may present a "choose what
+ // to sync" dialog during account sign-up, and application needs to be able to configure that
+ // dialog. A list of supported engines comes to us from the application via passed-in SyncConfig.
+ // Naturally, we could let the application configure any other part of the system that needs
+ // to have access to supported engines. From the implementor's point of view, this is an extra
+ // hurdle - instead of configuring only the account manager, they need to configure additional
+ // classes. Additionally, we currently allow updating sync config "in-flight", not just at
+ // the time of initialization. Providing an API for accessing currently configured engines
+ // makes re-configuring SyncConfig less error-prone, as only one class needs to be told of the
+ // new config.
+ // Merits of allowing applications to re-configure SyncConfig after initialization are under
+ // question, however: currently, we do not use that capability.
+ return syncConfig?.supportedEngines
+ }
+
+ /**
+ * Request an immediate synchronization, as configured according to [syncConfig].
+ *
+ * @param reason A [SyncReason] indicating why this sync is being requested.
+ * @param debounce Boolean flag indicating if this sync may be debounced (in case another sync executed recently).
+ * @param customEngineSubset A subset of supported engines to sync. Defaults to all supported engines.
+ */
+ suspend fun syncNow(
+ reason: SyncReason,
+ debounce: Boolean = false,
+ customEngineSubset: List<SyncEngine> = listOf(),
+ ) = withContext(coroutineContext) {
+ check(
+ customEngineSubset.isEmpty() ||
+ syncConfig?.supportedEngines?.containsAll(customEngineSubset) == true,
+ ) {
+ "Custom engines for sync must be a subset of supported engines."
+ }
+ when (val s = state) {
+ // Can't sync while we're still doing stuff.
+ is State.Active -> Unit
+ is State.Idle -> when (s.accountState) {
+ // All good, request a sync.
+ AccountState.Authenticated -> {
+ // Make sure auth cache is populated before we try to sync.
+ try {
+ maybeUpdateSyncAuthInfoCache()
+ } catch (e: AccessTokenUnexpectedlyWithoutKey) {
+ crashReporter?.submitCaughtException(
+ AccountManagerException.MissingKeyFromSyncScopedAccessToken("syncNow"),
+ )
+ processQueue(Event.Account.AccessTokenKeyError)
+ // No point in trying to sync now.
+ return@withContext
+ }
+
+ // Access to syncManager is guarded by `this`.
+ synchronized(this@FxaAccountManager) {
+ checkNotNull(syncManager == null) {
+ "Sync is not configured. Construct this class with a 'syncConfig' or use 'setSyncConfig'"
+ }
+ syncManager?.now(reason, debounce, customEngineSubset)
+ }
+ }
+ else -> logger.info("Ignoring syncNow request, not in the right state: $s")
+ }
+ }
+ }
+
+ /**
+ * Indicates if sync is currently running.
+ */
+ fun isSyncActive() = syncManager?.isSyncActive() ?: false
+
+ /**
+ * Call this after registering your observers, and before interacting with this class.
+ */
+ suspend fun start() = withContext(coroutineContext) {
+ processQueue(Event.Account.Start)
+
+ if (!isAccountManagerReady) {
+ notifyObservers { onReady(authenticatedAccount()) }
+ isAccountManagerReady = true
+ }
+ }
+
+ /**
+ * Main point for interaction with an [OAuthAccount] instance.
+ * @return [OAuthAccount] if we're in an authenticated state, null otherwise. Returned [OAuthAccount]
+ * may need to be re-authenticated; consumers are expected to check [accountNeedsReauth].
+ */
+ fun authenticatedAccount(): OAuthAccount? = when (val s = state) {
+ is State.Idle -> when (s.accountState) {
+ AccountState.Authenticated,
+ AccountState.AuthenticationProblem,
+ -> account
+ else -> null
+ }
+ else -> null
+ }
+
+ /**
+ * Indicates if account needs to be re-authenticated via [beginAuthentication].
+ * Most common reason for an account to need re-authentication is a password change.
+ *
+ * TODO this may return a false-positive, if we're currently going through a recovery flow.
+ * Prefer to be notified of auth problems via [AccountObserver], which is reliable.
+ *
+ * @return A boolean flag indicating if account needs to be re-authenticated.
+ */
+ fun accountNeedsReauth() = when (val s = state) {
+ is State.Idle -> when (s.accountState) {
+ AccountState.AuthenticationProblem -> true
+ else -> false
+ }
+ else -> false
+ }
+
+ /**
+ * Returns a [Profile] for an account, attempting to fetch it if necessary.
+ * @return [Profile] if one is available and account is an authenticated state.
+ */
+ fun accountProfile(): Profile? = when (val s = state) {
+ is State.Idle -> when (s.accountState) {
+ AccountState.Authenticated,
+ AccountState.AuthenticationProblem,
+ -> profile
+ else -> null
+ }
+ else -> null
+ }
+
+ @VisibleForTesting
+ internal suspend fun refreshProfile(ignoreCache: Boolean): Profile? {
+ return authenticatedAccount()?.getProfile(ignoreCache = ignoreCache)?.let { newProfile ->
+ profile = newProfile
+ notifyObservers {
+ onProfileUpdated(newProfile)
+ }
+ profile
+ }
+ }
+
+ /**
+ * Begins an authentication process. Should be finalized by calling [finishAuthentication] once
+ * user successfully goes through the authentication at the returned url.
+ * @param pairingUrl Optional pairing URL in case a pairing flow is being initiated.
+ * @param entrypoint an enum representing the feature entrypoint requesting the URL.
+ * the entrypoint is used in telemetry.
+ * @return An authentication url which is to be presented to the user.
+ */
+ suspend fun beginAuthentication(
+ pairingUrl: String? = null,
+ entrypoint: FxAEntryPoint,
+ ): String? = withContext(coroutineContext) {
+ // It's possible that at this point authentication is considered to be "in-progress".
+ // For example, if user started authentication flow, but cancelled it (closing a custom tab)
+ // without finishing.
+ // In a clean scenario (no prior auth attempts), this event will be ignored by the state machine.
+ processQueue(Event.Progress.CancelAuth)
+
+ val event = if (pairingUrl != null) {
+ Event.Account.BeginPairingFlow(pairingUrl, entrypoint)
+ } else {
+ Event.Account.BeginEmailFlow(entrypoint)
+ }
+
+ // Process the event, then use the new state to check the result of the operation
+ processQueue(event)
+ when (val state = state) {
+ is State.Idle -> (state.accountState as? AccountState.Authenticating)?.oAuthUrl
+ else -> null
+ }.also { result ->
+ if (result == null) {
+ logger.warn("beginAuthentication: error processing next state ($state)")
+ }
+ }
+ }
+
+ /**
+ * Finalize authentication that was started via [beginAuthentication].
+ *
+ * If authentication wasn't started via this manager we won't accept this authentication attempt,
+ * returning `false`. This may happen if [WebChannelFeature] is enabled, and user is manually
+ * logging into accounts.firefox.com in a regular tab.
+ *
+ * Guiding principle behind this is that logging into accounts.firefox.com should not affect
+ * logged-in state of the browser itself, even though the two may have an established communication
+ * channel via [WebChannelFeature].
+ */
+ suspend fun finishAuthentication(authData: FxaAuthData) = withContext(coroutineContext) {
+ when {
+ latestAuthState == null -> {
+ logger.warn("Trying to finish authentication that was never started.")
+ false
+ }
+ authData.state != latestAuthState -> {
+ logger.warn("Trying to finish authentication for an invalid auth state; ignoring.")
+ false
+ }
+ authData.state == latestAuthState -> {
+ authData.declinedEngines?.let { persistDeclinedEngines(it) }
+ processQueue(Event.Progress.AuthData(authData))
+ true
+ }
+ else -> throw IllegalStateException("Unexpected finishAuthentication state")
+ }
+ }
+
+ /**
+ * Logout of the account, if currently logged-in.
+ */
+ suspend fun logout() = withContext(coroutineContext) { processQueue(Event.Account.Logout) }
+
+ /**
+ * Register a [AccountEventsObserver] to monitor events relevant to an account/device.
+ */
+ fun registerForAccountEvents(observer: AccountEventsObserver, owner: LifecycleOwner, autoPause: Boolean) {
+ accountEventObserverRegistry.register(observer, owner, autoPause)
+ }
+
+ /**
+ * Register a [SyncStatusObserver] to monitor sync activity performed by this manager.
+ */
+ fun registerForSyncEvents(observer: SyncStatusObserver, owner: LifecycleOwner, autoPause: Boolean) {
+ syncStatusObserverRegistry.register(observer, owner, autoPause)
+ }
+
+ /**
+ * Unregister a [SyncStatusObserver] from being informed about "sync lifecycle" events.
+ * The method is safe to call even if the provided observer was not registered before.
+ */
+ fun unregisterForSyncEvents(observer: SyncStatusObserver) {
+ syncStatusObserverRegistry.unregister(observer)
+ }
+
+ override fun close() {
+ GlobalAccountManager.close()
+ coroutineContext.cancel()
+ account.close()
+ }
+
+ internal suspend fun encounteredAuthError(
+ operation: String,
+ errorCountWithinTheTimeWindow: Int = 1,
+ ) {
+ checkForMultipleRequiveryCalls()
+ return withContext(coroutineContext) {
+ processQueue(
+ Event.Account.AuthenticationError(operation, errorCountWithinTheTimeWindow),
+ )
+ }
+ }
+
+ /**
+ * Pumps the state machine until all events are processed and their side-effects resolve.
+ */
+ private suspend fun processQueue(event: Event) {
+ crashReporter?.recordCrashBreadcrumb(
+ Breadcrumb("fxa-state-machine-checker: a-c transition started (event: ${event.breadcrumbDisplay()})"),
+ )
+ eventQueue.add(event)
+ do {
+ val toProcess: Event = eventQueue.poll()!!
+ val transitionInto = state.next(toProcess)
+
+ crashReporter?.recordCrashBreadcrumb(
+ Breadcrumb(
+ "fxa-state-machine-checker: a-c transition " +
+ "(event: ${toProcess.breadcrumbDisplay()}, into: ${transitionInto?.breadcrumbDisplay()})",
+ ),
+ )
+
+ if (transitionInto == null) {
+ logger.warn("Got invalid event '$toProcess' for state $state.")
+ continue
+ }
+
+ AppServicesStateMachineChecker.handleEvent(toProcess, deviceConfig, scopes)
+ if (transitionInto is State.Idle) {
+ AppServicesStateMachineChecker.checkAccountState(transitionInto.accountState)
+ }
+
+ logger.info("Processing event '$toProcess' for state $state. Next state is $transitionInto")
+
+ state = transitionInto
+
+ stateActions(state, toProcess)?.let { successiveEvent ->
+ logger.info("Ran '$toProcess' side-effects for state $state, got successive event $successiveEvent")
+ if (successiveEvent is Event.Progress) {
+ // Note: stateActions should only return progress events, so this captures all
+ // possibilities.
+ AppServicesStateMachineChecker.validateProgressEvent(successiveEvent, toProcess, scopes)
+ }
+ eventQueue.add(successiveEvent)
+ }
+ } while (!eventQueue.isEmpty())
+ }
+
+ /**
+ * Side-effects of entering [AccountState] type states
+ *
+ * Upon entering these states, observers are typically notified. The sole exception occurs
+ * during the completion of authentication, where it is necessary to populate the
+ * SyncAuthInfoCache for the background synchronization worker.
+ *
+ * @throws [AccountManagerException.AuthenticationSideEffectsFailed] if there was a failure to
+ * run the side effects for a newly authenticated account.
+ */
+ private suspend fun accountStateSideEffects(
+ forState: State.Idle,
+ via: Event,
+ ): Unit = when (forState.accountState) {
+ AccountState.NotAuthenticated -> when (via) {
+ Event.Progress.LoggedOut -> {
+ resetAccount()
+ notifyObservers { onLoggedOut() }
+ }
+ Event.Progress.FailedToBeginAuth -> {
+ resetAccount()
+ notifyObservers { onFlowError(AuthFlowError.FailedToBeginAuth) }
+ }
+ Event.Progress.FailedToCompleteAuth -> {
+ resetAccount()
+ notifyObservers { onFlowError(AuthFlowError.FailedToCompleteAuth) }
+ }
+ Event.Progress.FailedToRecoverFromAuthenticationProblem -> {
+ finishedAuthRecovery()
+ }
+ else -> Unit
+ }
+ AccountState.Authenticated -> when (via) {
+ is Event.Progress.CompletedAuthentication -> {
+ val operation = when (via.authType) {
+ AuthType.Existing -> "CompletingAuthentication:accountRestored"
+ else -> "CompletingAuthentication:AuthData"
+ }
+ if (authenticationSideEffects(operation)) {
+ notifyObservers { onAuthenticated(account, via.authType) }
+ refreshProfile(ignoreCache = false)
+ Unit
+ } else {
+ throw AccountManagerException.AuthenticationSideEffectsFailed()
+ }
+ }
+ Event.Progress.RecoveredFromAuthenticationProblem -> {
+ finishedAuthRecovery()
+ // Clear our access token cache; it'll be re-populated as part of the
+ // regular state machine flow.
+ SyncAuthInfoCache(context).clear()
+ // Should we also call authenticationSideEffects here?
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1865086)
+ notifyObservers { onAuthenticated(account, AuthType.Recovered) }
+ refreshProfile(ignoreCache = true)
+ Unit
+ }
+ else -> Unit
+ }
+ AccountState.AuthenticationProblem -> {
+ SyncAuthInfoCache(context).clear()
+ notifyObservers { onAuthenticationProblems() }
+ }
+ else -> Unit
+ }
+
+ /**
+ * Side-effects of entering [ProgressState] states. These side-effects are actions we need to take
+ * to perform a state transition. For example, we wipe local state while entering a [ProgressState.LoggingOut].
+ *
+ * @return An optional follow-up [Event] that we'd like state machine to process after entering [forState]
+ * and processing its side-effects.
+ */
+ @Suppress("NestedBlockDepth", "LongMethod")
+ private suspend fun internalStateSideEffects(
+ forState: State.Active,
+ via: Event,
+ ): Event? = when (forState.progressState) {
+ ProgressState.Initializing -> {
+ when (accountOnDisk) {
+ is AccountOnDisk.New -> Event.Progress.AccountNotFound
+ is AccountOnDisk.Restored -> {
+ Event.Progress.AccountRestored
+ }
+ }
+ }
+ ProgressState.LoggingOut -> {
+ Event.Progress.LoggedOut
+ }
+ ProgressState.BeginningAuthentication -> when (via) {
+ is Event.Account.BeginPairingFlow, is Event.Account.BeginEmailFlow -> {
+ val pairingUrl = if (via is Event.Account.BeginPairingFlow) {
+ via.pairingUrl
+ } else {
+ null
+ }
+ val entrypoint = if (via is Event.Account.BeginEmailFlow) {
+ via.entrypoint
+ } else if (via is Event.Account.BeginPairingFlow) {
+ via.entrypoint
+ } else {
+ // This should be impossible, both `BeginPairingFlow` and `BeginEmailFlow`
+ // have a required `entrypoint` and we are matching against only instances
+ // of those data classes.
+ throw IllegalStateException("BeginningAuthentication with a flow that is neither email nor pairing")
+ }
+ val result = withRetries(logger, MAX_NETWORK_RETRIES) {
+ pairingUrl.asAuthFlowUrl(account, scopes, entrypoint = entrypoint)
+ }
+ when (result) {
+ is Result.Success -> {
+ latestAuthState = result.value!!.state
+ Event.Progress.StartedOAuthFlow(result.value.url)
+ }
+ Result.Failure -> {
+ Event.Progress.FailedToBeginAuth
+ }
+ }
+ }
+ else -> null
+ }
+ ProgressState.CompletingAuthentication -> when (via) {
+ Event.Progress.AccountRestored -> {
+ val authType = AuthType.Existing
+ when (withServiceRetries(logger, MAX_NETWORK_RETRIES) { finalizeDevice(authType) }) {
+ ServiceResult.Ok -> {
+ Event.Progress.CompletedAuthentication(authType)
+ }
+ ServiceResult.AuthError -> {
+ checkForMultipleRequiveryCalls()
+ Event.Account.AuthenticationError("finalizeDevice")
+ }
+ ServiceResult.OtherError -> {
+ AppServicesStateMachineChecker.handleInternalEvent(FxaStateCheckerEvent.CallError)
+ Event.Progress.FailedToCompleteAuthRestore
+ }
+ }
+ }
+ is Event.Progress.AuthData -> {
+ val completeAuth = suspend {
+ AppServicesStateMachineChecker.checkInternalState(
+ FxaStateCheckerState.CompleteOAuthFlow(via.authData.code, via.authData.state),
+ )
+ withRetries(logger, MAX_NETWORK_RETRIES) {
+ account.completeOAuthFlow(via.authData.code, via.authData.state)
+ }.also {
+ if (it is Result.Failure) {
+ AppServicesStateMachineChecker.handleInternalEvent(FxaStateCheckerEvent.CallError)
+ } else {
+ AppServicesStateMachineChecker.handleInternalEvent(
+ FxaStateCheckerEvent.CompleteOAuthFlowSuccess,
+ )
+ }
+ }
+ }
+ val finalize = suspend {
+ // Note: finalizeDevice state checking happens in the DeviceConstellation.kt
+ withServiceRetries(logger, MAX_NETWORK_RETRIES) { finalizeDevice(via.authData.authType) }.also {
+ if (it is ServiceResult.OtherError) {
+ AppServicesStateMachineChecker.handleInternalEvent(FxaStateCheckerEvent.CallError)
+ }
+ }
+ }
+ // If we can't 'complete', we won't run 'finalize' due to short-circuiting.
+ if (completeAuth() is Result.Failure || finalize() !is ServiceResult.Ok) {
+ Event.Progress.FailedToCompleteAuth
+ } else {
+ Event.Progress.CompletedAuthentication(via.authData.authType)
+ }
+ }
+ else -> null
+ }
+ ProgressState.RecoveringFromAuthProblem -> {
+ via as Event.Account.AuthenticationError
+ // Somewhere in the system, we've just hit an authentication problem.
+ // There are two main causes:
+ // 1) an access token we've obtain from fxalib via 'getAccessToken' expired
+ // 2) password was changed, or device was revoked
+ // We can recover from (1) and test if we're in (2) by asking the fxalib
+ // to give us a new access token. If it succeeds, then we can go back to whatever
+ // state we were in before. Future operations that involve access tokens should
+ // succeed.
+ // If we fail with a 401, then we know we have a legitimate authentication problem.
+ logger.info("Hit auth problem. Trying to recover.")
+
+ // Ensure we clear any auth-relevant internal state, such as access tokens.
+ account.authErrorDetected()
+
+ // Circuit-breaker logic to protect ourselves from getting into endless authorization
+ // check loops. If we determine that application is trying to check auth status too
+ // frequently, drive the state machine into an unauthorized state.
+ if (via.errorCountWithinTheTimeWindow >= AUTH_CHECK_CIRCUIT_BREAKER_COUNT) {
+ crashReporter?.submitCaughtException(
+ AccountManagerException.AuthRecoveryCircuitBreakerException(via.operation),
+ )
+ logger.warn("Unable to recover from an auth problem, triggered a circuit breaker.")
+
+ Event.Progress.FailedToRecoverFromAuthenticationProblem
+ } else {
+ // Since we haven't hit the circuit-breaker above, perform an authorization check.
+ // We request an access token for a "profile" scope since that's the only
+ // scope we're guaranteed to have at this point. That is, we don't rely on
+ // passed-in application-specific scopes.
+ when (account.checkAuthorizationStatus(SCOPE_PROFILE)) {
+ true -> {
+ logger.info("Able to recover from an auth problem.")
+
+ // And now we can go back to our regular programming.
+ Event.Progress.RecoveredFromAuthenticationProblem
+ }
+ null, false -> {
+ // We are either certainly in the scenario (2), or were unable to determine
+ // our connectivity state. Let's assume we need to re-authenticate.
+ // This uncertainty about real state means that, hopefully rarely,
+ // we will disconnect users that hit transient network errors during
+ // an authorization check.
+ // See https://github.com/mozilla-mobile/android-components/issues/3347
+ logger.info("Unable to recover from an auth problem, notifying observers.")
+
+ Event.Progress.FailedToRecoverFromAuthenticationProblem
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Side-effects matrix. Defines non-pure operations that must take place for state+event combinations.
+ */
+ @Suppress("ComplexMethod", "ReturnCount", "ThrowsCount", "NestedBlockDepth", "LongMethod")
+ private suspend fun stateActions(forState: State, via: Event): Event? = when (forState) {
+ // We're about to enter a new state ('forState') via some event ('via').
+ // States will have certain side-effects associated with different event transitions.
+ // In other words, the same state may have different side-effects depending on the event
+ // which caused a transition.
+ // For example, a "NotAuthenticated" state may be entered after a logout, and its side-effects
+ // will include clean-up and re-initialization of an account. Alternatively, it may be entered
+ // after we've checked local disk, and didn't find a persisted authenticated account.
+ is State.Idle -> try {
+ accountStateSideEffects(forState, via)
+ null
+ } catch (_: AccountManagerException.AuthenticationSideEffectsFailed) {
+ Event.Account.Logout
+ }
+ is State.Active -> internalStateSideEffects(forState, via)
+ }
+
+ private suspend fun resetAccount() {
+ // Clean up internal account state and destroy the current FxA device record (if one exists).
+ // This can fail (network issues, auth problems, etc), but nothing we can do at this point.
+ account.disconnect()
+
+ // Clean up resources.
+ profile = null
+ // Delete persisted state.
+ getAccountStorage().clear()
+ // Even though we might not have Sync enabled, clear out sync-related storage
+ // layers as well; if they're already empty (unused), nothing bad will happen
+ // and extra overhead is quite small.
+ SyncAuthInfoCache(context).clear()
+ SyncEnginesStorage(context).clear()
+ clearSyncState(context)
+ }
+
+ private suspend fun maybeUpdateSyncAuthInfoCache() {
+ // Update cached sync auth info only if we have a syncConfig (e.g. sync is enabled)...
+ if (syncConfig == null) {
+ return
+ }
+
+ // .. and our cache is stale.
+ val cache = SyncAuthInfoCache(context)
+ if (!cache.expired()) {
+ return
+ }
+
+ val accessToken = try {
+ account.getAccessToken(SCOPE_SYNC)
+ } catch (e: FxaSyncScopedKeyMissingException) {
+ // We received an access token, but no sync key which means we can't really use the
+ // connected FxA account. Throw an exception so that the account transitions to the
+ // `AuthenticationProblem` state. Things should be fixed when the user re-logs in.
+ //
+ // This used to be thrown when the android-components code noticed the issue in
+ // `asSyncAuthInfo()`. However, the application-services code now also checks for this
+ // and throws its own error. To keep the flow above this the same, we catch the
+ // app-services exception and throw the android-components one.
+ //
+ // Eventually, we should remove AccessTokenUnexpectedlyWithoutKey and have the higher
+ // functions catch `FxaSyncScopedKeyMissingException` directly
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1869862)
+ throw AccessTokenUnexpectedlyWithoutKey()
+ }
+ val tokenServerUrl = if (accessToken != null) {
+ // Only try to get the endpoint if we have an access token.
+ account.getTokenServerEndpointURL()
+ } else {
+ null
+ }
+
+ if (accessToken != null && tokenServerUrl != null) {
+ SyncAuthInfoCache(context).setToCache(accessToken.asSyncAuthInfo(tokenServerUrl))
+ } else {
+ // At this point, SyncAuthInfoCache may be entirely empty. In this case, we won't be
+ // able to sync via the background worker. We will attempt to populate SyncAuthInfoCache
+ // again in `syncNow` (in response to a direct user action) and after application restarts.
+ logger.warn("Couldn't populate SyncAuthInfoCache. Sync may not work.")
+ logger.warn("Is null? - accessToken: ${accessToken == null}, tokenServerUrl: ${tokenServerUrl == null}")
+ }
+ }
+
+ private fun persistDeclinedEngines(declinedEngines: Set<SyncEngine>) {
+ // Sync may not be configured at all (e.g. won't run), but if we received a
+ // list of declined engines, that indicates user intent, so we preserve it
+ // within SyncEnginesStorage regardless. If sync becomes enabled later on,
+ // we will be respecting user choice around what to sync.
+ val enginesStorage = SyncEnginesStorage(context)
+ declinedEngines.forEach { declinedEngine ->
+ enginesStorage.setStatus(declinedEngine, false)
+ }
+
+ // Enable all engines that were not explicitly disabled. Only do this in
+ // the presence of a "declinedEngines" list, since that indicates user
+ // intent. Absence of that list means that user was not presented with a
+ // choice during authentication, and so we can't assume an enabled state
+ // for any of the engines.
+ syncConfig?.supportedEngines?.forEach { supportedEngine ->
+ if (!declinedEngines.contains(supportedEngine)) {
+ enginesStorage.setStatus(supportedEngine, true)
+ }
+ }
+ }
+
+ private suspend fun finalizeDevice(authType: AuthType) = account.deviceConstellation().finalizeDevice(
+ authType,
+ deviceConfig,
+ )
+
+ /**
+ * Populates caches necessary for the sync worker (sync auth info and FxA device).
+ * @return 'true' on success, 'false' on failure, indicating that sync won't work.
+ */
+ private suspend fun authenticationSideEffects(operation: String): Boolean {
+ // Make sure our SyncAuthInfo cache is hot, background sync worker needs it to function.
+ try {
+ maybeUpdateSyncAuthInfoCache()
+ } catch (e: AccessTokenUnexpectedlyWithoutKey) {
+ crashReporter?.submitCaughtException(
+ AccountManagerException.MissingKeyFromSyncScopedAccessToken(operation),
+ )
+ // Since we don't know what's causing a missing key for the SCOPE_SYNC access tokens, we
+ // do not attempt to recover here. If this is a persistent state for an account, a recovery
+ // will just enter into a loop that our circuit breaker logic is unlikely to catch, due
+ // to recovery attempts likely being made on startup.
+ // See https://github.com/mozilla-mobile/android-components/issues/8527
+ return false
+ }
+
+ // Sync workers also need to know about the current FxA device.
+ FxaDeviceSettingsCache(context).setToCache(
+ DeviceSettings(
+ fxaDeviceId = account.getCurrentDeviceId()!!,
+ name = deviceConfig.name,
+ kind = deviceConfig.type.into(),
+ ),
+ )
+ return true
+ }
+
+ /**
+ * Exists strictly for testing purposes, allowing tests to specify their own implementation of [OAuthAccount].
+ */
+ @VisibleForTesting
+ open fun getStorageWrapper(): StorageWrapper {
+ return StorageWrapper(this, accountEventObserverRegistry, serverConfig, crashReporter)
+ }
+
+ @VisibleForTesting
+ internal open fun createSyncManager(config: SyncConfig): SyncManager {
+ return WorkManagerSyncManager(context, config)
+ }
+
+ internal open fun getAccountStorage(): AccountStorage {
+ return if (deviceConfig.secureStateAtRest) {
+ SecureAbove22AccountStorage(context, crashReporter)
+ } else {
+ SharedPrefAccountStorage(context, crashReporter)
+ }
+ }
+
+ /**
+ * Account status events flowing into the sync manager.
+ */
+ @VisibleForTesting
+ internal class AccountsToSyncIntegration(
+ private val syncManager: SyncManager,
+ ) : AccountObserver {
+ override fun onLoggedOut() {
+ syncManager.stop()
+ }
+
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ val reason = when (authType) {
+ is AuthType.OtherExternal, AuthType.Signin, AuthType.Signup, AuthType.MigratedReuse,
+ AuthType.MigratedCopy, AuthType.Pairing,
+ -> SyncReason.FirstSync
+ AuthType.Existing, AuthType.Recovered -> SyncReason.Startup
+ }
+ syncManager.start()
+ syncManager.now(reason)
+ }
+
+ override fun onProfileUpdated(profile: Profile) {
+ // Sync currently doesn't care about the FxA profile.
+ // In the future, we might kick-off an immediate sync here.
+ }
+
+ override fun onAuthenticationProblems() {
+ emitSyncFailedFact()
+ syncManager.stop()
+ }
+ }
+
+ /**
+ * Sync status changes flowing into account manager.
+ */
+ private class SyncToAccountsIntegration(
+ private val accountManager: FxaAccountManager,
+ ) : SyncStatusObserver {
+ override fun onStarted() {
+ accountManager.syncStatusObserverRegistry.notifyObservers { onStarted() }
+ }
+
+ override fun onIdle() {
+ accountManager.syncStatusObserverRegistry.notifyObservers { onIdle() }
+ }
+
+ override fun onError(error: Exception?) {
+ accountManager.syncStatusObserverRegistry.notifyObservers { onError(error) }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/GlobalAccountManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/GlobalAccountManager.kt
new file mode 100644
index 0000000000..837ea888cb
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/GlobalAccountManager.kt
@@ -0,0 +1,79 @@
+/* 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.service.fxa.manager
+
+import androidx.annotation.VisibleForTesting
+import java.lang.ref.WeakReference
+import java.util.concurrent.TimeUnit
+
+/**
+ * A singleton which exposes an instance of [FxaAccountManager] for internal consumption.
+ * Populated during initialization of [FxaAccountManager].
+ * This exists to allow various internal parts without a direct reference to an instance of
+ * [FxaAccountManager] to notify it of encountered auth errors via [authError].
+ */
+internal object GlobalAccountManager {
+ private var instance: WeakReference<FxaAccountManager>? = null
+ private var lastAuthErrorCheckPoint: Long = 0L
+ private var authErrorCountWithinWindow: Int = 0
+
+ internal interface Clock {
+ fun getTimeCheckPoint(): Long
+ }
+
+ private val systemClock = object : Clock {
+ override fun getTimeCheckPoint(): Long {
+ // nanoTime to decouple from wall-time.
+ return TimeUnit.NANOSECONDS.toMillis(System.nanoTime())
+ }
+ }
+
+ internal fun setInstance(am: FxaAccountManager) {
+ instance = WeakReference(am)
+ lastAuthErrorCheckPoint = 0
+ authErrorCountWithinWindow = 0
+ }
+
+ internal fun close() {
+ instance = null
+ }
+
+ internal suspend fun authError(
+ operation: String,
+ forSync: Boolean = false,
+ @VisibleForTesting clock: Clock = systemClock,
+ ) {
+ val authErrorCheckPoint: Long = clock.getTimeCheckPoint()
+
+ val timeSinceLastAuthErrorMs: Long? = if (lastAuthErrorCheckPoint == 0L) {
+ null
+ } else {
+ authErrorCheckPoint - lastAuthErrorCheckPoint
+ }
+ lastAuthErrorCheckPoint = authErrorCheckPoint
+
+ if (timeSinceLastAuthErrorMs == null) {
+ // First error, start our count.
+ authErrorCountWithinWindow = 1
+ } else if (timeSinceLastAuthErrorMs <= AUTH_CHECK_CIRCUIT_BREAKER_RESET_MS) {
+ // In general, skip additional checks inside the `AUTH_CHECK_CIRCUIT_BREAKER_RESET_MS`.
+ // This avoids queueing up multiple auth recovery checks when multiple operations run at
+ // the same time and result in auth errors.
+ //
+ // The one exception is sync, which retries on a successful recovery. In that case we
+ // should to run through the recovery process to make sure the sync happens.
+ if (!forSync) {
+ return
+ }
+ // Error within the reset time window, increment the count.
+ authErrorCountWithinWindow += 1
+ } else {
+ // Error outside the reset window, reset the count.
+ authErrorCountWithinWindow = 1
+ }
+
+ instance?.get()?.encounteredAuthError(operation, authErrorCountWithinWindow)
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt
new file mode 100644
index 0000000000..111715703a
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt
@@ -0,0 +1,216 @@
+/* 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.service.fxa.manager
+
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.FxAEntryPoint
+import mozilla.components.service.fxa.FxaAuthData
+
+/**
+ * This file is the "heart" of the accounts system. It's a finite state machine.
+ * Described below are all of the possible states, transitions between them and events which can
+ * trigger these transitions.
+ *
+ * There are two types of states - [State.Idle] and [State.Active]. Idle states are modeled by
+ * [AccountState], while active states are modeled by [ProgressState].
+ *
+ * Correspondingly, there are also two types of events - [Event.Account] and [Event.Progress].
+ * [Event.Account] events will be "understood" by states of type [AccountState].
+ * [Event.Progress] events will be "understood" by states of type [ProgressState].
+ * The word "understood" means "may trigger a state transition".
+ *
+ * [Event.Account] events are also referred to as "external events", since they are sent either directly
+ * by the user, or by some event of external origin (e.g. hitting an auth problem while communicating
+ * with a server).
+ *
+ * Similarly, [Event.Progress] events are referred to as "internal events", since they are sent by
+ * the internal processes within the state machine itself, e.g. as a result of some action being taken
+ * as a side-effect of a state transition.
+ *
+ * States of type [AccountState] are "stable" states (or, idle). After reaching one of these states,
+ * state machine will remain there unless an [Event.Account] is received. An example of a stable state
+ * is LoggedOut. No transitions will take place unless user tries to sign-in, or system tries to restore
+ * an account from disk (e.g. during startup).
+ *
+ * States of type [ProgressState] are "transition" state (or, active). These states represent certain processes
+ * that take place - for example, an in-progress authentication, or an account recovery.
+ * Once these processes are completed (either succeeding or failing), an [Event.Progress] is expected
+ * to be received, triggering a state transition.
+ *
+ * Most states have "side-effects" - actions that happen during a transition into the state.
+ * These side-effects are described in [FxaAccountManager]. For example, a side-effect of
+ * [ProgressState.BeginningAuthentication] may be sending a request to an auth server to initialize an OAuth flow.
+ *
+ * Side-effects of [AccountState] states are simply about notifying external observers that a certain
+ * stable account state was reached.
+ *
+ * Side-effects of [ProgressState] states are various actions we need to take to execute the transition,
+ * e.g. talking to servers, serializing some state to disk, cleaning up, etc.
+ *
+ * State transitions are described by a transition matrix, which is described in [State.next].
+ */
+
+internal sealed class AccountState {
+ object Authenticated : AccountState()
+ data class Authenticating(val oAuthUrl: String) : AccountState()
+ object AuthenticationProblem : AccountState()
+ object NotAuthenticated : AccountState()
+}
+
+internal enum class ProgressState {
+ Initializing,
+ BeginningAuthentication,
+ CompletingAuthentication,
+ RecoveringFromAuthProblem,
+ LoggingOut,
+}
+
+internal sealed class Event {
+ internal sealed class Account : Event() {
+ internal object Start : Account()
+ data class BeginEmailFlow(val entrypoint: FxAEntryPoint) : Account()
+ data class BeginPairingFlow(val pairingUrl: String?, val entrypoint: FxAEntryPoint) : Account()
+ data class AuthenticationError(val operation: String, val errorCountWithinTheTimeWindow: Int = 1) : Account() {
+ override fun toString(): String {
+ return "${this.javaClass.simpleName} - $operation"
+ }
+ }
+ object AccessTokenKeyError : Account()
+
+ object Logout : Account()
+ }
+
+ internal sealed class Progress : Event() {
+ object AccountNotFound : Progress()
+ object AccountRestored : Progress()
+
+ data class AuthData(val authData: FxaAuthData) : Progress()
+
+ object FailedToBeginAuth : Progress()
+ object FailedToCompleteAuthRestore : Progress()
+ object FailedToCompleteAuth : Progress()
+
+ object CancelAuth : Progress()
+
+ object FailedToRecoverFromAuthenticationProblem : Progress()
+ object RecoveredFromAuthenticationProblem : Progress()
+
+ object LoggedOut : Progress()
+
+ data class StartedOAuthFlow(val oAuthUrl: String) : Progress()
+ data class CompletedAuthentication(val authType: AuthType) : Progress()
+ }
+
+ /**
+ * Get a string to display in the breadcrumbs
+ *
+ * The main point of this function is to avoid using the string "auth", which gets filtered by
+ * Sentry. Use "ath" as a hacky replacement.
+ */
+ fun breadcrumbDisplay(): String = when (this) {
+ is Account.Start -> "Account.Start"
+ is Account.BeginEmailFlow -> "Account.BeginEmailFlow"
+ is Account.BeginPairingFlow -> "Account.BeginPairingFlow"
+ is Account.AuthenticationError -> "Account.AthenticationError($operation)"
+ is Account.AccessTokenKeyError -> "Account.AccessTknKeyError"
+ is Account.Logout -> "Account.Logout"
+ is Progress.AccountNotFound -> "Progress.AccountNotFound"
+ is Progress.AccountRestored -> "Progress.AccountRestored"
+ is Progress.AuthData -> "Progress.LoggedOut"
+ is Progress.FailedToBeginAuth -> "Progress.FailedToBeginAth"
+ is Progress.FailedToCompleteAuthRestore -> "Progress.FailedToCompleteAthRestore"
+ is Progress.FailedToCompleteAuth -> "Progress.FailedToCompleteAth"
+ is Progress.CancelAuth -> "Progress.CancelAth"
+ is Progress.FailedToRecoverFromAuthenticationProblem -> "Progress.FailedToRecoverFromAthenticationProblem"
+ is Progress.RecoveredFromAuthenticationProblem -> "Progress.RecoveredFromAthenticationProblem"
+ is Progress.LoggedOut -> "Progress.LoggedOut"
+ is Progress.StartedOAuthFlow -> "Progress.StartedOAthFlow"
+ is Progress.CompletedAuthentication -> "Progress.CompletedAthentication"
+ }
+}
+
+internal sealed class State {
+ data class Idle(val accountState: AccountState) : State()
+ data class Active(val progressState: ProgressState) : State()
+
+ /**
+ * Get a string to display in the breadcrumbs
+ *
+ * The main point of this function is to avoid using the string "auth", which gets filtered by
+ * Sentry. Use "ath" as a hacky replacement.
+ */
+ fun breadcrumbDisplay(): String = when (this) {
+ is Idle -> when (accountState) {
+ is AccountState.Authenticated -> "AccountState.Athenticated"
+ is AccountState.Authenticating -> "AccountState.Athenticating"
+ is AccountState.AuthenticationProblem -> "AccountState.AthenticationProblem"
+ is AccountState.NotAuthenticated -> "AccountState.NotAthenticated"
+ }
+ is Active -> when (progressState) {
+ ProgressState.Initializing -> "ProgressState.Initializing"
+ ProgressState.BeginningAuthentication -> "ProgressState.BeginningAthentication"
+ ProgressState.CompletingAuthentication -> "ProgressState.CompletingAthentication"
+ ProgressState.RecoveringFromAuthProblem -> "ProgressState.RecoveringFromAthProblem"
+ ProgressState.LoggingOut -> "ProgressState.LoggingOut"
+ }
+ }
+}
+
+internal fun State.next(event: Event): State? = when (this) {
+ // Reacting to external events.
+ is State.Idle -> when (this.accountState) {
+ AccountState.NotAuthenticated -> when (event) {
+ Event.Account.Start -> State.Active(ProgressState.Initializing)
+ is Event.Account.BeginEmailFlow -> State.Active(ProgressState.BeginningAuthentication)
+ is Event.Account.BeginPairingFlow -> State.Active(ProgressState.BeginningAuthentication)
+ else -> null
+ }
+ is AccountState.Authenticating -> when (event) {
+ is Event.Progress.AuthData -> State.Active(ProgressState.CompletingAuthentication)
+ Event.Progress.CancelAuth -> State.Idle(AccountState.NotAuthenticated)
+ else -> null
+ }
+ AccountState.Authenticated -> when (event) {
+ is Event.Account.AuthenticationError -> State.Active(ProgressState.RecoveringFromAuthProblem)
+ is Event.Account.AccessTokenKeyError -> State.Idle(AccountState.AuthenticationProblem)
+ is Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
+ else -> null
+ }
+ AccountState.AuthenticationProblem -> when (event) {
+ is Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
+ is Event.Account.BeginEmailFlow -> State.Active(ProgressState.BeginningAuthentication)
+ else -> null
+ }
+ }
+ // Reacting to internal events.
+ is State.Active -> when (this.progressState) {
+ ProgressState.Initializing -> when (event) {
+ Event.Progress.AccountNotFound -> State.Idle(AccountState.NotAuthenticated)
+ Event.Progress.AccountRestored -> State.Active(ProgressState.CompletingAuthentication)
+ else -> null
+ }
+ ProgressState.BeginningAuthentication -> when (event) {
+ Event.Progress.FailedToBeginAuth -> State.Idle(AccountState.NotAuthenticated)
+ is Event.Progress.StartedOAuthFlow -> State.Idle(AccountState.Authenticating(event.oAuthUrl))
+ else -> null
+ }
+ ProgressState.CompletingAuthentication -> when (event) {
+ is Event.Progress.CompletedAuthentication -> State.Idle(AccountState.Authenticated)
+ is Event.Account.AuthenticationError -> State.Active(ProgressState.RecoveringFromAuthProblem)
+ Event.Progress.FailedToCompleteAuthRestore -> State.Idle(AccountState.NotAuthenticated)
+ Event.Progress.FailedToCompleteAuth -> State.Idle(AccountState.NotAuthenticated)
+ else -> null
+ }
+ ProgressState.RecoveringFromAuthProblem -> when (event) {
+ Event.Progress.RecoveredFromAuthenticationProblem -> State.Idle(AccountState.Authenticated)
+ Event.Progress.FailedToRecoverFromAuthenticationProblem -> State.Idle(AccountState.AuthenticationProblem)
+ else -> null
+ }
+ ProgressState.LoggingOut -> when (event) {
+ Event.Progress.LoggedOut -> State.Idle(AccountState.NotAuthenticated)
+ else -> null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/SyncEnginesStorage.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/SyncEnginesStorage.kt
new file mode 100644
index 0000000000..76fcaec743
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/SyncEnginesStorage.kt
@@ -0,0 +1,57 @@
+/* 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.service.fxa.manager
+
+import android.content.Context
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.sync.toSyncEngine
+
+/**
+ * Storage layer for the enabled/disabled state of [SyncEngine].
+ */
+class SyncEnginesStorage(private val context: Context) {
+ companion object {
+ const val SYNC_ENGINES_KEY = "syncEngines"
+ }
+
+ /**
+ * @return A map describing known enabled/disabled state of [SyncEngine].
+ */
+ fun getStatus(): Map<SyncEngine, Boolean> {
+ val resultMap = mutableMapOf<SyncEngine, Boolean>()
+ // When adding new SyncEngines, think through implications for default values.
+ // See https://github.com/mozilla-mobile/android-components/issues/4557
+
+ // TODO does this need to be reversed? Go through what we have in local storage, and populate
+ // result map based on that. reason: we may have "other" engines.
+ // this will be empty if `setStatus` was never called.
+ storage().all.forEach {
+ if (it.value is Boolean) {
+ resultMap[it.key.toSyncEngine()] = it.value as Boolean
+ }
+ }
+
+ return resultMap
+ }
+
+ /**
+ * Update enabled/disabled state of [engine].
+ *
+ * @param engine A [SyncEngine] for which to update state.
+ * @param status New state.
+ */
+ fun setStatus(engine: SyncEngine, status: Boolean) {
+ storage().edit().putBoolean(engine.nativeName, status).apply()
+ }
+
+ /**
+ * Clears out any stored [SyncEngine] state.
+ */
+ internal fun clear() {
+ storage().edit().clear().apply()
+ }
+
+ private fun storage() = context.getSharedPreferences(SYNC_ENGINES_KEY, Context.MODE_PRIVATE)
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/ext/FxaAccountManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/ext/FxaAccountManager.kt
new file mode 100644
index 0000000000..91c71f1403
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/ext/FxaAccountManager.kt
@@ -0,0 +1,18 @@
+/* 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.service.fxa.manager.ext
+
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.service.fxa.manager.FxaAccountManager
+
+/**
+ * Executes [block] and provides the [DeviceConstellation] of an [OAuthAccount] if present.
+ */
+inline fun FxaAccountManager.withConstellation(block: DeviceConstellation.() -> Unit) {
+ authenticatedAccount()?.let {
+ block(it.deviceConstellation())
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
new file mode 100644
index 0000000000..b09e7f3dcc
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
@@ -0,0 +1,28 @@
+/* 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.service.fxa.store
+
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.lib.state.Action
+
+/**
+ * Actions for updating the global [SyncState] via [SyncStore].
+ */
+sealed class SyncAction : Action {
+ /**
+ * Update the [SyncState.status] of the [SyncStore].
+ */
+ data class UpdateSyncStatus(val status: SyncStatus) : SyncAction()
+
+ /**
+ * Update the [SyncState.account] of the [SyncStore].
+ */
+ data class UpdateAccount(val account: Account?) : SyncAction()
+
+ /**
+ * Update the [SyncState.constellationState] of the [SyncStore].
+ */
+ data class UpdateDeviceConstellation(val deviceConstellation: ConstellationState) : SyncAction()
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
new file mode 100644
index 0000000000..86c9d05d2a
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
@@ -0,0 +1,63 @@
+/* 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.service.fxa.store
+
+import mozilla.components.concept.sync.Avatar
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.lib.state.State
+import mozilla.components.service.fxa.sync.WorkManagerSyncManager
+
+/**
+ * Global state of Sync.
+ *
+ * @property status The current status of Sync.
+ * @property account The current Sync account, if any.
+ * @property constellationState The current constellation state, if any.
+ */
+data class SyncState(
+ val status: SyncStatus = SyncStatus.NotInitialized,
+ val account: Account? = null,
+ val constellationState: ConstellationState? = null,
+) : State
+
+/**
+ * Various statuses described the [SyncState].
+ *
+ * Starts as [NotInitialized].
+ * Becomes [Started] during the length of a Sync.
+ * Becomes [Idle] when a Sync is completed.
+ * Becomes [Error] when a Sync encounters an error.
+ * Becomes [LoggedOut] when Sync is logged out.
+ *
+ * See [WorkManagerSyncManager] for implementation details.
+ */
+enum class SyncStatus {
+ Started,
+ Idle,
+ Error,
+ NotInitialized,
+ LoggedOut,
+}
+
+/**
+ * Account information available for a synced account.
+ *
+ * @property uid See [Profile.uid].
+ * @property email See [Profile.email].
+ * @property avatar See [Profile.avatar].
+ * @property displayName See [Profile.displayName].
+ * @property currentDeviceId See [OAuthAccount.getCurrentDeviceId].
+ * @property sessionToken See [OAuthAccount.getSessionToken].
+ */
+data class Account(
+ val uid: String?,
+ val email: String?,
+ val avatar: Avatar?,
+ val displayName: String?,
+ val currentDeviceId: String?,
+ val sessionToken: String?,
+)
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
new file mode 100644
index 0000000000..50f4b6747e
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
@@ -0,0 +1,28 @@
+/* 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.service.fxa.store
+
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.Store
+
+/**
+ * [Store] for the global [SyncState]. This should likely be a singleton.
+ */
+class SyncStore(
+ middleware: List<Middleware<SyncState, SyncAction>> = emptyList(),
+) : Store<SyncState, SyncAction>(
+ initialState = SyncState(),
+ reducer = ::reduce,
+ middleware = middleware,
+)
+
+private fun reduce(syncState: SyncState, syncAction: SyncAction): SyncState {
+ return when (syncAction) {
+ is SyncAction.UpdateSyncStatus -> syncState.copy(status = syncAction.status)
+ is SyncAction.UpdateAccount -> syncState.copy(account = syncAction.account)
+ is SyncAction.UpdateDeviceConstellation ->
+ syncState.copy(constellationState = syncAction.deviceConstellation)
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
new file mode 100644
index 0000000000..4feda96e60
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
@@ -0,0 +1,138 @@
+/* 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.service.fxa.store
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.DeviceConstellationObserver
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.sync.SyncStatusObserver
+import java.lang.Exception
+
+/**
+ * Connections an [FxaAccountManager] with a [SyncStore], so that updates to Sync
+ * state can be observed.
+ *
+ * @param store The [SyncStore] to publish state updates based on [fxaAccountManager] observations.
+ * @param fxaAccountManager Account manager that is used to interact with Sync backends.
+ * @param lifecycleOwner The lifecycle owner that will tie to the when account manager observations.
+ * Recommended that this be an Application or at minimum a persistent Activity.
+ * @param autoPause Whether the account manager observations will stop between onPause and onResume.
+ * @param coroutineScope Scope used to launch various suspending operations.
+ */
+class SyncStoreSupport(
+ private val store: SyncStore,
+ private val fxaAccountManager: Lazy<FxaAccountManager>,
+ private val lifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
+ private val autoPause: Boolean = false,
+ private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+) {
+ /**
+ * Initialize the integration. This will cause it to register itself as an observer
+ * of the [FxaAccountManager] and begin dispatching [SyncStore] updates.
+ */
+ fun initialize() {
+ val accountManager = fxaAccountManager.value
+ accountManager.registerForSyncEvents(
+ AccountSyncObserver(store),
+ owner = lifecycleOwner,
+ autoPause = autoPause,
+ )
+
+ val accountObserver = FxaAccountObserver(
+ store,
+ ConstellationObserver(store),
+ lifecycleOwner,
+ autoPause,
+ coroutineScope,
+ )
+ accountManager.register(accountObserver, owner = lifecycleOwner, autoPause = autoPause)
+ }
+}
+
+/**
+ * Maps various [SyncStatusObserver] callbacks to [SyncAction] dispatches.
+ *
+ * @param store The [SyncStore] that updates will be dispatched to.
+ */
+internal class AccountSyncObserver(private val store: SyncStore) : SyncStatusObserver {
+ override fun onStarted() {
+ store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.Started))
+ }
+
+ override fun onIdle() {
+ store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.Idle))
+ }
+
+ override fun onError(error: Exception?) {
+ store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.Error))
+ }
+}
+
+/**
+ * Maps various [AccountObserver] callbacks to [SyncAction] dispatches.
+ *
+ * @param store The [SyncStore] that updates will be dispatched to.
+ * @param deviceConstellationObserver Will be registered as an observer to any constellations
+ * received in [AccountObserver.onAuthenticated].
+ *
+ * See [SyncStoreSupport] for the rest of the param definitions.
+ */
+internal class FxaAccountObserver(
+ private val store: SyncStore,
+ private val deviceConstellationObserver: DeviceConstellationObserver,
+ private val lifecycleOwner: LifecycleOwner,
+ private val autoPause: Boolean,
+ private val coroutineScope: CoroutineScope,
+) : AccountObserver {
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ coroutineScope.launch(Dispatchers.Main) {
+ account.deviceConstellation().registerDeviceObserver(
+ deviceConstellationObserver,
+ owner = lifecycleOwner,
+ autoPause = autoPause,
+ )
+ }
+ coroutineScope.launch {
+ val syncAccount = account.getProfile()?.toAccount(account) ?: return@launch
+ store.dispatch(SyncAction.UpdateAccount(syncAccount))
+ }
+ }
+
+ override fun onLoggedOut() {
+ store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.LoggedOut))
+ store.dispatch(SyncAction.UpdateAccount(null))
+ }
+}
+
+/**
+ * Maps various [DeviceConstellationObserver] callbacks to [SyncAction] dispatches.
+ *
+ * @param store The [SyncStore] that updates will be dispatched to.
+ */
+internal class ConstellationObserver(private val store: SyncStore) : DeviceConstellationObserver {
+ override fun onDevicesUpdate(constellation: ConstellationState) {
+ store.dispatch(SyncAction.UpdateDeviceConstellation(constellation))
+ }
+}
+
+// Could be refactored to use a context receiver once 1.6.2 upgrade lands
+private fun Profile.toAccount(oAuthAccount: OAuthAccount): Account =
+ Account(
+ uid = uid,
+ email = email,
+ avatar = avatar,
+ displayName = displayName,
+ currentDeviceId = oAuthAccount.getCurrentDeviceId(),
+ sessionToken = oAuthAccount.getSessionToken(),
+ )
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/SyncManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/SyncManager.kt
new file mode 100644
index 0000000000..097fce264f
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/SyncManager.kt
@@ -0,0 +1,234 @@
+/* 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.service.fxa.sync
+
+import mozilla.components.concept.storage.KeyProvider
+import mozilla.components.concept.sync.SyncableStore
+import mozilla.components.service.fxa.SyncConfig
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.SyncEnginesStorage
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import java.io.Closeable
+import java.lang.Exception
+import java.util.concurrent.TimeUnit
+
+/**
+ * A collection of objects describing a reason for running a sync.
+ */
+sealed class SyncReason {
+ /**
+ * Application is starting up, and wants to sync data.
+ */
+ object Startup : SyncReason()
+
+ /**
+ * User is requesting a sync (e.g. pressed a "sync now" button).
+ */
+ object User : SyncReason()
+
+ /**
+ * User changed enabled/disabled state of one or more [SyncEngine]s.
+ */
+ object EngineChange : SyncReason()
+
+ /**
+ * Internal use only: first time running a sync.
+ */
+ internal object FirstSync : SyncReason()
+
+ /**
+ * Internal use only: running a periodic sync.
+ */
+ internal object Scheduled : SyncReason()
+}
+
+/**
+ * An interface for consumers that wish to observer "sync lifecycle" events.
+ */
+interface SyncStatusObserver {
+ /**
+ * Gets called at the start of a sync, before any configured syncable is synchronized.
+ */
+ fun onStarted()
+
+ /**
+ * Gets called at the end of a sync, after every configured syncable has been synchronized.
+ * A set of enabled [SyncEngine]s could have changed, so observers are expected to query
+ * [SyncEnginesStorage.getStatus].
+ */
+ fun onIdle()
+
+ /**
+ * Gets called if sync encounters an error that's worthy of processing by status observers.
+ * @param error Optional relevant exception.
+ */
+ fun onError(error: Exception?)
+}
+
+/**
+ * A lazy instance of a [SyncableStore] with an optional [KeyProvider] instance, used if this store
+ * has encrypted contents. Lazy wrapping is in place to ensure we don't eagerly instantiate the storage.
+ *
+ * @property lazyStore A [SyncableStore] wrapped in [Lazy].
+ * @property keyProvider An optional [KeyProvider] wrapped in [Lazy]. If present, it'll be used for
+ * crypto operations on the storage.
+ */
+data class LazyStoreWithKey(
+ val lazyStore: Lazy<SyncableStore>,
+ val keyProvider: Lazy<KeyProvider>? = null,
+)
+
+/**
+ * A singleton registry of [SyncableStore] objects. [WorkManagerSyncDispatcher] will use this to
+ * access configured [SyncableStore] instances.
+ *
+ * This pattern provides a safe way for library-defined background workers to access globally
+ * available instances of stores within an application.
+ */
+object GlobalSyncableStoreProvider {
+ private val stores: MutableMap<SyncEngine, LazyStoreWithKey> = mutableMapOf()
+
+ /**
+ * Configure an instance of [SyncableStore] for a [SyncEngine] enum.
+ * @param storePair A pair associating [SyncableStore] with a [SyncEngine].
+ */
+ fun configureStore(storePair: Pair<SyncEngine, Lazy<SyncableStore>>, keyProvider: Lazy<KeyProvider>? = null) {
+ stores[storePair.first] = LazyStoreWithKey(lazyStore = storePair.second, keyProvider = keyProvider)
+ }
+
+ internal fun getLazyStoreWithKey(syncEngine: SyncEngine): LazyStoreWithKey? {
+ return stores[syncEngine]
+ }
+}
+
+/**
+ * Internal interface to enable testing SyncManager implementations independently from SyncDispatcher.
+ */
+internal interface SyncDispatcher : Closeable, Observable<SyncStatusObserver> {
+ fun isSyncActive(): Boolean
+ fun syncNow(
+ reason: SyncReason,
+ debounce: Boolean = false,
+ customEngineSubset: List<SyncEngine> = listOf(),
+ )
+ fun startPeriodicSync(unit: TimeUnit, period: Long, initialDelay: Long)
+ fun stopPeriodicSync()
+ fun workersStateChanged(isRunning: Boolean)
+}
+
+/**
+ * A base sync manager implementation.
+ * @param syncConfig A [SyncConfig] object describing how sync should behave.
+ */
+internal abstract class SyncManager(
+ private val syncConfig: SyncConfig,
+) {
+ open val logger = Logger("SyncManager")
+
+ // A SyncDispatcher instance bound to an account and a set of syncable stores.
+ private var syncDispatcher: SyncDispatcher? = null
+
+ private val syncStatusObserverRegistry = ObserverRegistry<SyncStatusObserver>()
+
+ // Manager encapsulates events emitted by the wrapped observers, and passes them on to the outside world.
+ // This split allows us to define a nice public observer API (manager -> consumers), along with
+ // a more robust internal observer API (dispatcher -> manager).
+ // Currently the interfaces are the same, hence the name "pass-through".
+ private val dispatcherStatusObserver = PassThroughSyncStatusObserver(syncStatusObserverRegistry)
+
+ /**
+ * Indicates if sync is currently running.
+ */
+ internal fun isSyncActive() = syncDispatcher?.isSyncActive() ?: false
+
+ internal fun registerSyncStatusObserver(observer: SyncStatusObserver) {
+ syncStatusObserverRegistry.register(observer)
+ }
+
+ /**
+ * Request an immediate synchronization of all configured stores.
+ *
+ * @param reason A [SyncReason] indicating why this sync is being requested.
+ * @param debounce Whether or not this sync should debounced.
+ * @param customEngineSubset A subset of supported engines to sync. Defaults to all supported engines.
+ */
+ internal fun now(
+ reason: SyncReason,
+ debounce: Boolean = false,
+ customEngineSubset: List<SyncEngine> = listOf(),
+ ) = synchronized(this) {
+ if (syncDispatcher == null) {
+ logger.info("Sync is not enabled. Ignoring 'sync now' request.")
+ }
+ syncDispatcher?.syncNow(reason, debounce, customEngineSubset)
+ }
+
+ /**
+ * Enables synchronization, with behaviour described in [syncConfig].
+ */
+ internal fun start() = synchronized(this) {
+ logger.debug("Enabling...")
+ syncDispatcher = initDispatcher(newDispatcher(syncDispatcher, syncConfig.supportedEngines))
+ logger.debug("set and initialized new dispatcher: $syncDispatcher")
+ }
+
+ /**
+ * Disables synchronization.
+ */
+ internal fun stop() = synchronized(this) {
+ logger.debug("Disabling...")
+ syncDispatcher?.close()
+ syncDispatcher = null
+ }
+
+ internal abstract fun createDispatcher(supportedEngines: Set<SyncEngine>): SyncDispatcher
+
+ internal abstract fun dispatcherUpdated(dispatcher: SyncDispatcher)
+
+ private fun newDispatcher(
+ currentDispatcher: SyncDispatcher?,
+ supportedEngines: Set<SyncEngine>,
+ ): SyncDispatcher {
+ // Let the existing dispatcher, if present, cleanup.
+ currentDispatcher?.close()
+ // TODO will events from old and new dispatchers overlap..? How do we ensure correct sequence
+ // for outside observers?
+ currentDispatcher?.unregister(dispatcherStatusObserver)
+
+ // Create a new dispatcher bound to current stores and account.
+ return createDispatcher(supportedEngines)
+ }
+
+ private fun initDispatcher(dispatcher: SyncDispatcher): SyncDispatcher {
+ dispatcher.register(dispatcherStatusObserver)
+ syncConfig.periodicSyncConfig?.let {
+ dispatcher.startPeriodicSync(
+ TimeUnit.MINUTES,
+ period = it.periodMinutes.toLong(),
+ initialDelay = it.initialDelayMinutes.toLong(),
+ )
+ }
+ dispatcherUpdated(dispatcher)
+ return dispatcher
+ }
+
+ private class PassThroughSyncStatusObserver(
+ private val passThroughRegistry: ObserverRegistry<SyncStatusObserver>,
+ ) : SyncStatusObserver {
+ override fun onStarted() {
+ passThroughRegistry.notifyObservers { onStarted() }
+ }
+
+ override fun onIdle() {
+ passThroughRegistry.notifyObservers { onIdle() }
+ }
+
+ override fun onError(error: Exception?) {
+ passThroughRegistry.notifyObservers { onError(error) }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/Types.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/Types.kt
new file mode 100644
index 0000000000..243971006f
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/Types.kt
@@ -0,0 +1,71 @@
+/* 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.service.fxa.sync
+
+import mozilla.appservices.syncmanager.SyncAuthInfo
+import mozilla.components.service.fxa.SyncEngine
+
+/**
+ * Converts from a list of raw strings describing engines to a set of [SyncEngine] objects.
+ */
+fun List<String>.toSyncEngines(): Set<SyncEngine> {
+ return this.map { it.toSyncEngine() }.toSet()
+}
+
+/**
+ * Conversion from our SyncAuthInfo into its "native" version used at the interface boundary.
+ */
+internal fun mozilla.components.concept.sync.SyncAuthInfo.toNative(): SyncAuthInfo {
+ return SyncAuthInfo(
+ kid = this.kid,
+ fxaAccessToken = this.fxaAccessToken,
+ syncKey = this.syncKey,
+ tokenserverUrl = this.tokenServerUrl,
+ )
+}
+
+internal fun String.toSyncEngine(): SyncEngine {
+ return when (this) {
+ "history" -> SyncEngine.History
+ "bookmarks" -> SyncEngine.Bookmarks
+ "passwords" -> SyncEngine.Passwords
+ "tabs" -> SyncEngine.Tabs
+ "creditcards" -> SyncEngine.CreditCards
+ "addresses" -> SyncEngine.Addresses
+ // This handles a case of engines that we don't yet model in SyncEngine.
+ else -> SyncEngine.Other(this)
+ }
+}
+
+internal fun SyncReason.toRustSyncReason(): mozilla.appservices.syncmanager.SyncReason {
+ return when (this) {
+ SyncReason.Startup -> mozilla.appservices.syncmanager.SyncReason.STARTUP
+ SyncReason.User -> mozilla.appservices.syncmanager.SyncReason.USER
+ SyncReason.Scheduled -> mozilla.appservices.syncmanager.SyncReason.SCHEDULED
+ SyncReason.EngineChange -> mozilla.appservices.syncmanager.SyncReason.ENABLED_CHANGE
+ SyncReason.FirstSync -> mozilla.appservices.syncmanager.SyncReason.ENABLED_CHANGE
+ }
+}
+
+internal fun SyncReason.asString(): String {
+ return when (this) {
+ SyncReason.FirstSync -> "first_sync"
+ SyncReason.Scheduled -> "scheduled"
+ SyncReason.EngineChange -> "engine_change"
+ SyncReason.User -> "user"
+ SyncReason.Startup -> "startup"
+ }
+}
+
+internal fun String.toSyncReason(): SyncReason {
+ return when (this) {
+ "startup" -> SyncReason.Startup
+ "user" -> SyncReason.User
+ "engine_change" -> SyncReason.EngineChange
+ "scheduled" -> SyncReason.Scheduled
+ "first_sync" -> SyncReason.FirstSync
+ else -> throw IllegalStateException("Invalid SyncReason: $this")
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/WorkManagerSyncManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/WorkManagerSyncManager.kt
new file mode 100644
index 0000000000..eb7b1e79b0
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/WorkManagerSyncManager.kt
@@ -0,0 +1,585 @@
+/* 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.service.fxa.sync
+
+import android.content.Context
+import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
+import androidx.work.BackoffPolicy
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.appservices.sync15.SyncTelemetryPing
+import mozilla.appservices.syncmanager.ServiceStatus
+import mozilla.appservices.syncmanager.SyncEngineSelection
+import mozilla.appservices.syncmanager.SyncParams
+import mozilla.appservices.syncmanager.SyncTelemetry
+import mozilla.components.concept.storage.KeyProvider
+import mozilla.components.service.fxa.FxaDeviceSettingsCache
+import mozilla.components.service.fxa.SyncAuthInfoCache
+import mozilla.components.service.fxa.SyncConfig
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.GlobalAccountManager
+import mozilla.components.service.fxa.manager.SyncEnginesStorage
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import java.io.Closeable
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.TimeUnit
+import mozilla.appservices.syncmanager.SyncManager as RustSyncManager
+
+@VisibleForTesting
+internal enum class SyncWorkerTag {
+ Common,
+ Immediate, // will not debounce a sync
+ Debounce, // will debounce if another sync happened recently
+}
+
+private enum class SyncWorkerName {
+ Periodic,
+ Immediate,
+}
+
+private const val KEY_DATA_STORES = "stores"
+private const val KEY_REASON = "reason"
+
+private const val SYNC_WORKER_BACKOFF_DELAY_MINUTES = 3L
+
+/**
+ * The Rust implemented SyncManager. Must be a singleton as it carries some state between
+ * syncs. Does no IO at creation time so is safe to call on any thread.
+ */
+val syncManager: RustSyncManager by lazy { RustSyncManager() }
+
+/**
+ * A [SyncManager] implementation which uses WorkManager APIs to schedule sync tasks.
+ *
+ * Must be initialized on the main thread.
+ */
+internal class WorkManagerSyncManager(
+ private val context: Context,
+ syncConfig: SyncConfig,
+) : SyncManager(syncConfig) {
+ override val logger = Logger("BgSyncManager")
+
+ init {
+ WorkersLiveDataObserver.init(context)
+
+ if (syncConfig.periodicSyncConfig == null) {
+ logger.info("Periodic syncing is disabled.")
+ } else {
+ logger.info("Periodic syncing enabled: ${syncConfig.periodicSyncConfig}")
+ }
+ }
+
+ override fun createDispatcher(supportedEngines: Set<SyncEngine>): SyncDispatcher {
+ return WorkManagerSyncDispatcher(context, supportedEngines)
+ }
+
+ override fun dispatcherUpdated(dispatcher: SyncDispatcher) {
+ WorkersLiveDataObserver.setDispatcher(dispatcher)
+ }
+}
+
+/**
+ * A singleton wrapper around the the LiveData "forever" observer - i.e. an observer not bound
+ * to a lifecycle owner. This observer is always active.
+ * We will have different dispatcher instances throughout the lifetime of the app, but always a
+ * single LiveData instance.
+ */
+internal object WorkersLiveDataObserver {
+ private lateinit var workManager: WorkManager
+ private val workersLiveData by lazy {
+ workManager.getWorkInfosByTagLiveData(SyncWorkerTag.Common.name)
+ }
+
+ private var dispatcher: SyncDispatcher? = null
+
+ /**
+ * Initializes the Observer.
+ *
+ * @param context the context that will be used to with the [WorkManager] to observe workers.
+ */
+ @UiThread
+ fun init(context: Context) {
+ workManager = WorkManager.getInstance(context)
+
+ // Only set our observer once.
+ if (workersLiveData.hasObservers()) return
+
+ // This must be called on the UI thread.
+ workersLiveData.observeForever {
+ val isRunning = when (it?.any { worker -> worker.state == WorkInfo.State.RUNNING }) {
+ null -> false
+ false -> false
+ true -> true
+ }
+
+ dispatcher?.workersStateChanged(isRunning)
+
+ // TODO process errors coming out of worker.outputData
+ }
+ }
+
+ fun setDispatcher(dispatcher: SyncDispatcher) {
+ this.dispatcher = dispatcher
+ }
+}
+
+internal class WorkManagerSyncDispatcher(
+ private val context: Context,
+ private val supportedEngines: Set<SyncEngine>,
+) : SyncDispatcher, Observable<SyncStatusObserver> by ObserverRegistry(), Closeable {
+ private val logger = Logger("WMSyncDispatcher")
+
+ // TODO does this need to be volatile?
+ private var isSyncActive = false
+
+ init {
+ // Stop any currently active periodic syncing. Consumers of this class are responsible for
+ // starting periodic syncing via [startPeriodicSync] if they need it.
+ stopPeriodicSync()
+ }
+
+ override fun workersStateChanged(isRunning: Boolean) {
+ if (isSyncActive && !isRunning) {
+ notifyObservers { onIdle() }
+ isSyncActive = false
+ } else if (!isSyncActive && isRunning) {
+ notifyObservers { onStarted() }
+ isSyncActive = true
+ }
+ }
+
+ override fun isSyncActive(): Boolean {
+ return isSyncActive
+ }
+
+ override fun syncNow(
+ reason: SyncReason,
+ debounce: Boolean,
+ customEngineSubset: List<SyncEngine>,
+ ) {
+ logger.debug("Immediate sync requested, reason = $reason, debounce = $debounce")
+ val delayMs = if (reason == SyncReason.Startup) {
+ // Startup delay is there to avoid SQLITE_BUSY crashes, since we currently do a poor job
+ // of managing database connections, and we expect there to be database writes at the start.
+ // We've done bunch of work to make this better (see https://github.com/mozilla-mobile/android-components/issues/1369),
+ // but it's not clear yet this delay is completely safe to remove.
+ SYNC_STARTUP_DELAY_MS
+ } else {
+ 0L
+ }
+ WorkManager.getInstance(context).beginUniqueWork(
+ SyncWorkerName.Immediate.name,
+ // Use the 'keep' policy to minimize overhead from multiple "sync now" operations coming in
+ // at the same time.
+ ExistingWorkPolicy.KEEP,
+ regularSyncWorkRequest(reason, delayMs, debounce, customEngineSubset),
+ ).enqueue()
+ }
+
+ override fun close() {
+ unregisterObservers()
+ stopPeriodicSync()
+ }
+
+ /**
+ * Periodic background syncing is mainly intended to reduce workload when we sync during
+ * application startup.
+ */
+ override fun startPeriodicSync(unit: TimeUnit, period: Long, initialDelay: Long) {
+ logger.debug("Starting periodic syncing, period = $period, time unit = $unit")
+ // Use the 'update' policy as a simple way to upgrade periodic worker configurations across
+ // application versions. We do this instead of versioning workers.
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(
+ SyncWorkerName.Periodic.name,
+ ExistingPeriodicWorkPolicy.UPDATE,
+ periodicSyncWorkRequest(unit, period, initialDelay),
+ )
+ }
+
+ /**
+ * Disables periodic syncing in the background. Currently running syncs may continue until completion.
+ * Safe to call this even if periodic syncing isn't currently enabled.
+ */
+ override fun stopPeriodicSync() {
+ logger.debug("Cancelling periodic syncing")
+ WorkManager.getInstance(context).cancelUniqueWork(SyncWorkerName.Periodic.name)
+ }
+
+ private fun periodicSyncWorkRequest(unit: TimeUnit, period: Long, initialDelay: Long): PeriodicWorkRequest {
+ val data = getWorkerData(SyncReason.Scheduled)
+ // Periodic interval must be at least PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
+ // e.g. not more frequently than 15 minutes.
+ return PeriodicWorkRequestBuilder<WorkManagerSyncWorker>(period, unit, initialDelay, unit)
+ .setConstraints(
+ Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build(),
+ )
+ .setInputData(data)
+ .addTag(SyncWorkerTag.Common.name)
+ .addTag(SyncWorkerTag.Debounce.name)
+ .setBackoffCriteria(
+ BackoffPolicy.EXPONENTIAL,
+ SYNC_WORKER_BACKOFF_DELAY_MINUTES,
+ TimeUnit.MINUTES,
+ )
+ .build()
+ }
+
+ private fun regularSyncWorkRequest(
+ reason: SyncReason,
+ delayMs: Long = 0L,
+ debounce: Boolean = false,
+ customEngineSubset: List<SyncEngine> = listOf(),
+ ): OneTimeWorkRequest {
+ val data = getWorkerData(reason, customEngineSubset)
+ return OneTimeWorkRequestBuilder<WorkManagerSyncWorker>()
+ .setConstraints(
+ Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build(),
+ )
+ .setInputData(data)
+ .addTag(SyncWorkerTag.Common.name)
+ .addTag(if (debounce) SyncWorkerTag.Debounce.name else SyncWorkerTag.Immediate.name)
+ .setInitialDelay(delayMs, TimeUnit.MILLISECONDS)
+ .setBackoffCriteria(
+ BackoffPolicy.EXPONENTIAL,
+ SYNC_WORKER_BACKOFF_DELAY_MINUTES,
+ TimeUnit.MINUTES,
+ )
+ .build()
+ }
+
+ @VisibleForTesting
+ internal fun getWorkerData(
+ reason: SyncReason,
+ customEngineSubset: List<SyncEngine> = listOf(),
+ ): Data {
+ val enginesToSync = customEngineSubset.takeIf { it.isNotEmpty() } ?: supportedEngines
+ return Data.Builder()
+ .putStringArray(KEY_DATA_STORES, enginesToSync.map { it.nativeName }.toTypedArray())
+ .putString(KEY_REASON, reason.asString())
+ .build()
+ }
+}
+
+internal class WorkManagerSyncWorker(
+ private val context: Context,
+ private val params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+ private val logger = Logger("SyncWorker")
+
+ @VisibleForTesting
+ internal fun isDebounced(): Boolean {
+ return params.tags.contains(SyncWorkerTag.Debounce.name)
+ }
+
+ @VisibleForTesting
+ internal fun lastSyncedWithinStaggerBuffer(engine: String): Boolean {
+ if (!isDebounced()) {
+ return false
+ }
+
+ return engineSyncTimestamp[engine]?.let {
+ (System.currentTimeMillis() - it) < SYNC_STAGGER_BUFFER_MS
+ } ?: false
+ }
+
+ private fun updateEngineSyncedTime(engine: String) {
+ engineSyncTimestamp[engine] = System.currentTimeMillis()
+ }
+
+ override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
+ logger.debug("Starting sync... Tagged as: ${params.tags}")
+
+ // We will need a list of SyncableStores.
+ val syncableStores = params.inputData.getStringArray(KEY_DATA_STORES)?.filter {
+ !lastSyncedWithinStaggerBuffer(it)
+ }?.associate {
+ // Convert from a string back to our SyncEngine type.
+ val engine = when (it) {
+ SyncEngine.History.nativeName -> SyncEngine.History
+ SyncEngine.Bookmarks.nativeName -> SyncEngine.Bookmarks
+ SyncEngine.Passwords.nativeName -> SyncEngine.Passwords
+ SyncEngine.Tabs.nativeName -> SyncEngine.Tabs
+ SyncEngine.CreditCards.nativeName -> SyncEngine.CreditCards
+ SyncEngine.Addresses.nativeName -> SyncEngine.Addresses
+ else -> throw IllegalStateException("Invalid syncable store: $it")
+ }
+
+ updateEngineSyncedTime(engine.nativeName)
+ engine to checkNotNull(GlobalSyncableStoreProvider.getLazyStoreWithKey(engine)) {
+ "SyncableStore missing from GlobalSyncableStoreProvider: ${engine.nativeName}"
+ }
+ }
+
+ if (syncableStores.isNullOrEmpty()) {
+ // Short-circuit if there are no configured stores.
+ // Don't update the "last-synced" timestamp because we haven't actually synced anything.
+ Result.success()
+ } else {
+ doSync(syncableStores)
+ }
+ }
+
+ @Suppress("LongMethod", "ComplexMethod")
+ private suspend fun doSync(syncableStores: Map<SyncEngine, LazyStoreWithKey>): Result {
+ val engineKeyProviders = mutableMapOf<SyncEngine, KeyProvider>()
+
+ // We need to tell RustSyncManager which engines to sync.
+ val enginesToSync = SyncEngineSelection.Some(syncableStores.map { it.key.nativeName })
+
+ // We need to tell RustSyncManager about instances of supported stores ('places' and 'logins').
+ // NOTE: This need only be done once, not each sync - but the only impact is that
+ // it's slightly less efficient so refactoring might not be worthwhile.
+ syncableStores.entries.forEach {
+ // We're assuming all syncable stores live in Rust.
+ // Currently `RustSyncManager` doesn't support non-Rust sync engines.
+ when (it.key) {
+ SyncEngine.History -> it.value.lazyStore.value.registerWithSyncManager()
+ SyncEngine.Bookmarks -> it.value.lazyStore.value.registerWithSyncManager()
+ SyncEngine.CreditCards -> {
+ it.value.lazyStore.value.registerWithSyncManager()
+
+ checkNotNull(it.value.keyProvider) {
+ "CreditCards store must be configured with a KeyProvider"
+ }
+
+ engineKeyProviders[it.key] = it.value.keyProvider!!.value
+ }
+ SyncEngine.Addresses -> {
+ it.value.lazyStore.value.registerWithSyncManager()
+ }
+ SyncEngine.Passwords -> {
+ it.value.lazyStore.value.registerWithSyncManager()
+
+ checkNotNull(it.value.keyProvider) {
+ "Passwords store must be configured with a KeyProvider"
+ }
+
+ engineKeyProviders[it.key] = it.value.keyProvider!!.value
+ }
+ SyncEngine.Tabs -> {
+ it.value.lazyStore.value.registerWithSyncManager()
+ }
+ else -> throw NotImplementedError("Unsupported engine: ${it.key}")
+ }
+ }
+
+ // We need to know the reason we're syncing.
+ val reason = params.inputData.getString(KEY_REASON)!!.toSyncReason()
+
+ // We need a cached "sync auth info" object.
+ val syncAuthInfo = SyncAuthInfoCache(context).getCached() ?: return Result.failure()
+
+ // We need any persisted state that we received from RustSyncManager in the past.
+ // We should be able to pass a `null` value, but currently the library will crash.
+ // See https://github.com/mozilla/application-services/issues/1829
+ val currentSyncState = getSyncState(context) ?: ""
+
+ // We need to tell RustSyncManager about our local "which engines are enabled/disabled" state.
+ // This is a global state, shared by every sync client for this account. State that we will
+ // pass here will overwrite current global state and may be propagated to every sync client.
+ // This should be empty if there have been no changes to this state.
+ // We pass this state if user changed it (EngineChange) or if we're in a first sync situation.
+ // A "first sync" will happen after signing up or signing in.
+ // In both cases, user may have been asked which engines they'd like to sync.
+ val enabledChanges = when (reason) {
+ SyncReason.EngineChange, SyncReason.FirstSync -> {
+ val engineMap = SyncEnginesStorage(context).getStatus().toMutableMap()
+ // For historical reasons, a "history engine" really means two sync collections: history and forms.
+ // Underlying library doesn't manage this for us, and other clients will get confused
+ // if we modify just "history" without also modifying "forms", and vice versa.
+ // So: whenever a change to the "history" engine happens locally, inject the same "forms" change.
+ // This should be handled by RustSyncManager. See https://github.com/mozilla/application-services/issues/1832
+ engineMap[SyncEngine.History]?.let {
+ engineMap[SyncEngine.Forms] = it
+ }
+ engineMap.mapKeys { it.key.nativeName }
+ }
+ else -> emptyMap()
+ }
+
+ // We need to tell RustSyncManager about our current FxA device. It needs that information
+ // in order to sync the 'clients' collection.
+ // We're depending on cache being populated. An alternative to using a "cache" is to
+ // configure the worker with the values stored in it: device name, type and fxaDeviceID.
+ // While device type and fxaDeviceID are stable, users are free to change device name whenever.
+ // We need to reflect these changes during a sync. Our options are then: provide a global cache,
+ // or re-configure our workers every time a change is made to the device name.
+ // We have the same basic story already with syncAuthInfo cache, and a cache is much easier
+ // to implement/reason about than worker reconfiguration.
+ val deviceSettings = FxaDeviceSettingsCache(context).getCached()!!
+
+ // Obtain encryption keys for stores that came along with KeyProviders.
+ // This can take a bit of time!
+ val localEncryptionKeys = engineKeyProviders.mapKeys {
+ it.key.nativeName
+ }.mapValues {
+ it.value.getOrGenerateKey().key
+ }
+
+ // We're now ready to sync.
+ val syncParams = SyncParams(
+ reason = reason.toRustSyncReason(),
+ engines = enginesToSync,
+ authInfo = syncAuthInfo.toNative(),
+ enabledChanges = enabledChanges,
+ persistedState = currentSyncState,
+ deviceSettings = deviceSettings,
+ localEncryptionKeys = localEncryptionKeys,
+ )
+
+ val syncResult = syncManager.sync(syncParams)
+
+ // Persist the sync state; it may have changed during a sync, and RustSyncManager relies on us
+ // to store it.
+ setSyncState(context, syncResult.persistedState)
+
+ // Log the results.
+ syncResult.failures.entries.forEach {
+ logger.error("Failed to sync ${it.key}, reason: ${it.value}")
+ }
+ syncResult.successful.forEach {
+ logger.info("Successfully synced $it")
+ }
+
+ // Process any changes to the list of declined/accepted engines.
+ // NB: We may have received engine state information about an engine we're unfamiliar with.
+ // `SyncEngine.Other(string)` stands in for unknown engines.
+ val declinedEngines = syncResult.declined?.map { it.toSyncEngine() }?.toSet() ?: emptySet()
+ // We synthesize the 'accepted' list ourselves: engines we know about - declined engines.
+ // This assumes that "engines we know about" is a subset of engines RustSyncManager knows about.
+ // RustSyncManager will handle this, eventually.
+ // See: https://github.com/mozilla/application-services/issues/1685
+ val acceptedEngines = syncableStores.keys.filter { !declinedEngines.contains(it) }
+
+ // Persist engine state changes.
+ with(SyncEnginesStorage(context)) {
+ declinedEngines.forEach { setStatus(it, status = false) }
+ acceptedEngines.forEach { setStatus(it, status = true) }
+ }
+
+ // Process telemetry.
+ syncResult.telemetryJson?.let { SyncTelemetry.processSyncTelemetry(SyncTelemetryPing.fromJSONString(it)) }
+
+ // Finally, declare success, failure or request a retry based on 'sync status'.
+ return when (syncResult.status) {
+ // Happy case.
+ ServiceStatus.OK -> {
+ // Worker should set the "last-synced" timestamp, and since we have a single timestamp,
+ // it's not clear if a single failure should prevent its update. That's the current behaviour
+ // in Fennec, but for very specific reasons that aren't relevant here. We could have
+ // a timestamp per store, or whatever we want here really.
+ // For now, we just update it every time we succeed to sync.
+ setLastSynced(context, System.currentTimeMillis())
+ Result.success()
+ }
+
+ // Retry cases.
+ // NB: retry doesn't mean "immediate retry". It means "retry, but respecting this worker's
+ // backoff policy, as configured during worker's creation.
+ // TODO FOR ALL retries: look at workerParams.mRunAttemptCount, don't retry after a certain number.
+ ServiceStatus.NETWORK_ERROR -> {
+ logger.error("Network error")
+ Result.retry()
+ }
+ ServiceStatus.BACKED_OFF -> {
+ logger.error("Backed-off error")
+ // As part of `syncResult`, we get back `nextSyncAllowedAt`. Ideally, we should not retry
+ // before that passes. However, we can not reconfigure back-off policy for an already
+ // created Worker. So, we just rely on a sensible default. `RustSyncManager` will fail
+ // to sync with a BACKED_OFF error without hitting the server if we don't respect
+ // `nextSyncAllowedAt`, so we should be good either way.
+ Result.retry()
+ }
+
+ // Failure cases.
+ ServiceStatus.AUTH_ERROR -> {
+ logger.error("Auth error")
+ GlobalAccountManager.authError("RustSyncManager.sync", forSync = true)
+ Result.failure()
+ }
+ ServiceStatus.SERVICE_ERROR -> {
+ logger.error("Service error")
+ Result.failure()
+ }
+ ServiceStatus.OTHER_ERROR -> {
+ logger.error("'Other' error :(")
+ Result.failure()
+ }
+ }
+ }
+
+ companion object {
+ @VisibleForTesting
+ internal const val SYNC_STAGGER_BUFFER_MS = 5 * 1000L // 5 seconds.
+
+ @VisibleForTesting
+ internal val engineSyncTimestamp = ConcurrentHashMap<String, Long>()
+ }
+}
+
+private const val SYNC_STATE_PREFS_KEY = "syncPrefs"
+private const val SYNC_LAST_SYNCED_KEY = "lastSynced"
+private const val SYNC_STATE_KEY = "persistedState"
+
+private const val SYNC_STARTUP_DELAY_MS = 5 * 1000L // 5 seconds.
+
+fun getLastSynced(context: Context): Long {
+ return context
+ .getSharedPreferences(SYNC_STATE_PREFS_KEY, Context.MODE_PRIVATE)
+ .getLong(SYNC_LAST_SYNCED_KEY, 0)
+}
+
+internal fun clearSyncState(context: Context) {
+ context.getSharedPreferences(SYNC_STATE_PREFS_KEY, Context.MODE_PRIVATE)
+ .edit().clear().apply()
+}
+
+internal fun getSyncState(context: Context): String? {
+ return context
+ .getSharedPreferences(SYNC_STATE_PREFS_KEY, Context.MODE_PRIVATE)
+ .getString(SYNC_STATE_KEY, null)
+}
+
+/**
+ * Saves the lastSyncedTime to the shared preferences
+ *
+ * @param context the context
+ * @param lastSyncedTime - the last synced time in milliseconds
+ */
+fun setLastSynced(context: Context, lastSyncedTime: Long) {
+ context
+ .getSharedPreferences(SYNC_STATE_PREFS_KEY, Context.MODE_PRIVATE)
+ .edit()
+ .putLong(SYNC_LAST_SYNCED_KEY, lastSyncedTime)
+ .apply()
+}
+
+internal fun setSyncState(context: Context, state: String) {
+ context
+ .getSharedPreferences(SYNC_STATE_PREFS_KEY, Context.MODE_PRIVATE)
+ .edit()
+ .putString(SYNC_STATE_KEY, state)
+ .apply()
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/AccountStorageTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/AccountStorageTest.kt
new file mode 100644
index 0000000000..bb72c1276a
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/AccountStorageTest.kt
@@ -0,0 +1,166 @@
+/* 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.service.fxa
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.appservices.fxaclient.FxaConfig
+import mozilla.appservices.fxaclient.FxaServer
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.robolectric.annotation.Config
+import kotlin.reflect.KClass
+
+// Note that tests that involve secure storage specify API=21, because of issues testing secure storage on
+// 23+ API levels. See https://github.com/mozilla-mobile/android-components/issues/4956
+
+@RunWith(AndroidJUnit4::class)
+class SharedPrefAccountStorageTest {
+ @Config(sdk = [21])
+ @Test
+ fun `plain storage crud`() {
+ val storage = SharedPrefAccountStorage(testContext)
+ val account = FirefoxAccount(
+ FxaConfig(FxaServer.Release, "someId", "http://www.firefox.com"),
+ )
+ assertNull(storage.read())
+ storage.write(account.toJSONString())
+ assertNotNull(storage.read())
+ storage.clear()
+ assertNull(storage.read())
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `migration from SecureAbove22AccountStorage`() {
+ val secureStorage = SecureAbove22AccountStorage(testContext)
+ val account = FirefoxAccount(
+ FxaConfig(FxaServer.Release, "someId", "http://www.firefox.com"),
+ )
+
+ assertNull(secureStorage.read())
+ secureStorage.write(account.toJSONString())
+ assertNotNull(secureStorage.read())
+
+ // Now that we have account state in secureStorage, it should be migrated over to plainStorage when it's init'd.
+ val plainStorage = SharedPrefAccountStorage(testContext)
+ assertNotNull(plainStorage.read())
+ // And secureStorage must have been cleared during this migration.
+ assertNull(secureStorage.read())
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `missing state is reported during a migration`() {
+ val secureStorage = SecureAbove22AccountStorage(testContext)
+ val account = FirefoxAccount(
+ FxaConfig(FxaServer.Release, "someId", "http://www.firefox.com"),
+ )
+ secureStorage.write(account.toJSONString())
+
+ // Clear the underlying storage layer "behind the back" of account storage.
+ SecureAbove22Preferences(testContext, "fxaStateAC").clear()
+
+ val crashReporter: CrashReporting = mock()
+ val plainStorage = SharedPrefAccountStorage(testContext, crashReporter)
+ assertCaughtException(crashReporter, AbnormalAccountStorageEvent.UnexpectedlyMissingAccountState::class)
+
+ assertNull(plainStorage.read())
+
+ reset(crashReporter)
+ assertNull(secureStorage.read())
+ verifyNoInteractions(crashReporter)
+ }
+}
+
+@RunWith(AndroidJUnit4::class)
+class SecureAbove22AccountStorageTest {
+ @Config(sdk = [21])
+ @Test
+ fun `secure storage crud`() {
+ val crashReporter: CrashReporting = mock()
+ val storage = SecureAbove22AccountStorage(testContext, crashReporter)
+ val account = FirefoxAccount(
+ FxaConfig(FxaServer.Release, "someId", "http://www.firefox.com"),
+ )
+ assertNull(storage.read())
+ storage.write(account.toJSONString())
+ assertNotNull(storage.read())
+ storage.clear()
+ assertNull(storage.read())
+ verifyNoInteractions(crashReporter)
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `migration from SharedPrefAccountStorage`() {
+ val plainStorage = SharedPrefAccountStorage(testContext)
+ val account = FirefoxAccount(
+ FxaConfig(FxaServer.Release, "someId", "http://www.firefox.com"),
+ )
+
+ assertNull(plainStorage.read())
+ plainStorage.write(account.toJSONString())
+ assertNotNull(plainStorage.read())
+
+ // Now that we have account state in plainStorage, it should be migrated over to secureStorage when it's init'd.
+ val crashReporter: CrashReporting = mock()
+ val secureStorage = SecureAbove22AccountStorage(testContext, crashReporter)
+ assertNotNull(secureStorage.read())
+ // And plainStorage must have been cleared during this migration.
+ assertNull(plainStorage.read())
+ verifyNoInteractions(crashReporter)
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `missing state is reported`() {
+ val crashReporter: CrashReporting = mock()
+ val storage = SecureAbove22AccountStorage(testContext, crashReporter)
+ val account = FirefoxAccount(
+ FxaConfig(FxaServer.Release, "someId", "http://www.firefox.com"),
+ )
+ storage.write(account.toJSONString())
+
+ // Clear the underlying storage layer "behind the back" of account storage.
+ SecureAbove22Preferences(testContext, "fxaStateAC").clear()
+ assertNull(storage.read())
+ assertCaughtException(crashReporter, AbnormalAccountStorageEvent.UnexpectedlyMissingAccountState::class)
+ // Make sure exception is only reported once per "incident".
+ reset(crashReporter)
+ assertNull(storage.read())
+ verifyNoInteractions(crashReporter)
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `missing state is ignored without a configured crash reporter`() {
+ val storage = SecureAbove22AccountStorage(testContext)
+ val account = FirefoxAccount(
+ FxaConfig(FxaServer.Release, "someId", "http://www.firefox.com"),
+ )
+ storage.write(account.toJSONString())
+
+ // Clear the underlying storage layer "behind the back" of account storage.
+ SecureAbove22Preferences(testContext, "fxaStateAC").clear()
+ assertNull(storage.read())
+ }
+}
+
+private fun <T : AbnormalAccountStorageEvent> assertCaughtException(crashReporter: CrashReporting, type: KClass<T>) {
+ val captor = argumentCaptor<AbnormalAccountStorageEvent>()
+ verify(crashReporter).submitCaughtException(captor.capture())
+ Assert.assertEquals(type, captor.value::class)
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
new file mode 100644
index 0000000000..afc2fc3ee9
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
@@ -0,0 +1,1646 @@
+/* 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.service.fxa
+
+import android.content.Context
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import mozilla.appservices.fxaclient.FxaConfig
+import mozilla.appservices.fxaclient.FxaServer
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.sync.AccessTokenInfo
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthFlowUrl
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.FxAEntryPoint
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.OAuthScopedKey
+import mozilla.components.concept.sync.Profile
+import mozilla.components.concept.sync.ServiceResult
+import mozilla.components.concept.sync.StatePersistenceCallback
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.GlobalAccountManager
+import mozilla.components.service.fxa.manager.SyncEnginesStorage
+import mozilla.components.service.fxa.sync.SyncDispatcher
+import mozilla.components.service.fxa.sync.SyncManager
+import mozilla.components.service.fxa.sync.SyncReason
+import mozilla.components.service.fxa.sync.SyncStatusObserver
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.After
+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.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+import java.util.concurrent.TimeUnit
+import kotlin.coroutines.CoroutineContext
+
+internal fun testAuthFlowUrl(entrypoint: String = "test-entrypoint"): AuthFlowUrl {
+ return AuthFlowUrl(EXPECTED_AUTH_STATE, "https://example.com/auth-flow-start?entrypiont=$entrypoint&state=$EXPECTED_AUTH_STATE")
+}
+
+internal class TestableStorageWrapper(
+ manager: FxaAccountManager,
+ accountEventObserverRegistry: ObserverRegistry<AccountEventsObserver>,
+ serverConfig: FxaConfig,
+ private val block: () -> OAuthAccount = {
+ val account: OAuthAccount = mock()
+ `when`(account.deviceConstellation()).thenReturn(mock())
+ account
+ },
+) : StorageWrapper(manager, accountEventObserverRegistry, serverConfig) {
+ override fun obtainAccount(): OAuthAccount = block()
+}
+
+// Same as the actual account manager, except we get to control how FirefoxAccountShaped instances
+// are created. This is necessary because due to some build issues (native dependencies not available
+// within the test environment) we can't use fxaclient supplied implementation of FirefoxAccountShaped.
+// Instead, we express all of our account-related operations over an interface.
+internal open class TestableFxaAccountManager(
+ context: Context,
+ config: FxaConfig,
+ private val storage: AccountStorage,
+ capabilities: Set<DeviceCapability> = emptySet(),
+ syncConfig: SyncConfig? = null,
+ coroutineContext: CoroutineContext,
+ crashReporter: CrashReporting? = null,
+ block: () -> OAuthAccount = {
+ val account: OAuthAccount = mock()
+ `when`(account.deviceConstellation()).thenReturn(mock())
+ account
+ },
+) : FxaAccountManager(context, config, DeviceConfig("test", DeviceType.UNKNOWN, capabilities), syncConfig, emptySet(), crashReporter, coroutineContext) {
+ private val testableStorageWrapper = TestableStorageWrapper(this, accountEventObserverRegistry, serverConfig, block)
+
+ override var syncStatusObserverRegistry = ObserverRegistry<SyncStatusObserver>()
+
+ override fun getStorageWrapper(): StorageWrapper {
+ return testableStorageWrapper
+ }
+
+ override fun getAccountStorage(): AccountStorage {
+ return storage
+ }
+
+ override fun createSyncManager(config: SyncConfig): SyncManager = mock()
+}
+
+const val EXPECTED_AUTH_STATE = "goodAuthState"
+const val UNEXPECTED_AUTH_STATE = "badAuthState"
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class FxaAccountManagerTest {
+
+ val entryPoint: FxAEntryPoint = mock<FxAEntryPoint>().apply {
+ whenever(entryName).thenReturn("home-menu")
+ }
+
+ @After
+ fun cleanup() {
+ SyncAuthInfoCache(testContext).clear()
+ SyncEnginesStorage(testContext).clear()
+ }
+
+ internal class TestSyncDispatcher(registry: ObserverRegistry<SyncStatusObserver>) : SyncDispatcher, Observable<SyncStatusObserver> by registry {
+ val inner: SyncDispatcher = mock()
+ override fun isSyncActive(): Boolean {
+ return inner.isSyncActive()
+ }
+
+ override fun syncNow(
+ reason: SyncReason,
+ debounce: Boolean,
+ customEngineSubset: List<SyncEngine>,
+ ) {
+ inner.syncNow(reason, debounce, customEngineSubset)
+ }
+
+ override fun startPeriodicSync(unit: TimeUnit, period: Long, initialDelay: Long) {
+ inner.startPeriodicSync(unit, period, initialDelay)
+ }
+
+ override fun stopPeriodicSync() {
+ inner.stopPeriodicSync()
+ }
+
+ override fun workersStateChanged(isRunning: Boolean) {
+ inner.workersStateChanged(isRunning)
+ }
+
+ override fun close() {
+ inner.close()
+ }
+ }
+
+ internal class TestSyncManager(config: SyncConfig) : SyncManager(config) {
+ val dispatcherRegistry = ObserverRegistry<SyncStatusObserver>()
+ val dispatcher: TestSyncDispatcher = TestSyncDispatcher(dispatcherRegistry)
+
+ private var dispatcherUpdatedCount = 0
+ override fun createDispatcher(supportedEngines: Set<SyncEngine>): SyncDispatcher {
+ return dispatcher
+ }
+
+ override fun dispatcherUpdated(dispatcher: SyncDispatcher) {
+ dispatcherUpdatedCount++
+ }
+ }
+
+ class TestSyncStatusObserver : SyncStatusObserver {
+ var onStartedCount = 0
+ var onIdleCount = 0
+ var onErrorCount = 0
+
+ override fun onStarted() {
+ onStartedCount++
+ }
+
+ override fun onIdle() {
+ onIdleCount++
+ }
+
+ override fun onError(error: Exception?) {
+ onErrorCount++
+ }
+ }
+
+ @Test
+ fun `restored account state persistence`() = runTest {
+ val accountStorage: AccountStorage = mock()
+ val profile = Profile("testUid", "test@example.com", null, "Test Profile")
+ val constellation: DeviceConstellation = mockDeviceConstellation()
+ val account = StatePersistenceTestableAccount(profile, constellation)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "http://auth-url/redirect"),
+ accountStorage,
+ setOf(DeviceCapability.SEND_TAB),
+ null,
+ this.coroutineContext,
+ ) {
+ account
+ }
+
+ `when`(constellation.finalizeDevice(eq(AuthType.Existing), any())).thenReturn(ServiceResult.Ok)
+ // We have an account at the start.
+ `when`(accountStorage.read()).thenReturn(account)
+
+ assertNull(account.persistenceCallback)
+ manager.start()
+
+ // Assert that persistence callback is set.
+ assertNotNull(account.persistenceCallback)
+
+ // Assert that ensureCapabilities fired, but not the device initialization (since we're restoring).
+ verify(constellation).finalizeDevice(eq(AuthType.Existing), any())
+
+ // Assert that persistence callback is interacting with the storage layer.
+ account.persistenceCallback!!.persist("test")
+ verify(accountStorage).write("test")
+ }
+
+ @Test
+ fun `restored account state persistence, finalizeDevice hit an intermittent error`() = runTest {
+ val accountStorage: AccountStorage = mock()
+ val profile = Profile("testUid", "test@example.com", null, "Test Profile")
+ val constellation: DeviceConstellation = mockDeviceConstellation()
+ val account = StatePersistenceTestableAccount(profile, constellation)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "http://auth-url/redirect"),
+ accountStorage,
+ setOf(DeviceCapability.SEND_TAB),
+ null,
+ this.coroutineContext,
+ ) {
+ account
+ }
+
+ `when`(constellation.finalizeDevice(eq(AuthType.Existing), any())).thenReturn(ServiceResult.OtherError)
+ // We have an account at the start.
+ `when`(accountStorage.read()).thenReturn(account)
+
+ assertNull(account.persistenceCallback)
+ manager.start()
+
+ // Assert that persistence callback is set.
+ assertNotNull(account.persistenceCallback)
+
+ // Assert that finalizeDevice fired with a correct auth type. 3 times since we re-try.
+ verify(constellation, times(3)).finalizeDevice(eq(AuthType.Existing), any())
+
+ // Assert that persistence callback is interacting with the storage layer.
+ account.persistenceCallback!!.persist("test")
+ verify(accountStorage).write("test")
+
+ // Since we weren't able to finalize the account state, we're no longer authenticated.
+ assertNull(manager.authenticatedAccount())
+ }
+
+ @Test
+ fun `restored account state persistence, hit an auth error`() = runTest {
+ val accountStorage: AccountStorage = mock()
+ val profile = Profile("testUid", "test@example.com", null, "Test Profile")
+ val constellation: DeviceConstellation = mockDeviceConstellation()
+ val account = StatePersistenceTestableAccount(profile, constellation, ableToRecoverFromAuthError = false)
+
+ val accountObserver: AccountObserver = mock()
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "http://auth-url/redirect"),
+ accountStorage,
+ setOf(DeviceCapability.SEND_TAB),
+ null,
+ this.coroutineContext,
+ ) {
+ account
+ }
+
+ manager.register(accountObserver)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.AuthError)
+ // We have an account at the start.
+ `when`(accountStorage.read()).thenReturn(account)
+
+ assertNull(account.persistenceCallback)
+
+ assertFalse(manager.accountNeedsReauth())
+ assertFalse(account.authErrorDetectedCalled)
+ assertFalse(account.checkAuthorizationStatusCalled)
+ verify(accountObserver, never()).onAuthenticationProblems()
+
+ manager.start()
+
+ assertTrue(manager.accountNeedsReauth())
+ verify(accountObserver, times(1)).onAuthenticationProblems()
+ assertTrue(account.authErrorDetectedCalled)
+ assertTrue(account.checkAuthorizationStatusCalled)
+ }
+
+ @Test(expected = FxaPanicException::class)
+ fun `restored account state persistence, hit an fxa panic which is re-thrown`() = runTest {
+ val accountStorage: AccountStorage = mock()
+ val profile = Profile("testUid", "test@example.com", null, "Test Profile")
+ val constellation: DeviceConstellation = mock()
+ val account = StatePersistenceTestableAccount(profile, constellation)
+
+ val accountObserver: AccountObserver = mock()
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "http://auth-url/redirect"),
+ accountStorage,
+ setOf(DeviceCapability.SEND_TAB),
+ null,
+ this.coroutineContext,
+ ) {
+ account
+ }
+
+ manager.register(accountObserver)
+
+ // Hit a panic while we're restoring account.
+ doAnswer {
+ throw FxaPanicException("don't panic!")
+ }.`when`(constellation).finalizeDevice(any(), any())
+
+ // We have an account at the start.
+ `when`(accountStorage.read()).thenReturn(account)
+
+ assertNull(account.persistenceCallback)
+
+ assertFalse(manager.accountNeedsReauth())
+ verify(accountObserver, never()).onAuthenticationProblems()
+
+ manager.start()
+ }
+
+ @Test
+ fun `newly authenticated account state persistence`() = runTest {
+ val accountStorage: AccountStorage = mock()
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val constellation: DeviceConstellation = mockDeviceConstellation()
+ val account = StatePersistenceTestableAccount(profile, constellation)
+ val accountObserver: AccountObserver = mock()
+ // We are not using the "prepareHappy..." helper method here, because our account isn't a mock,
+ // but an actual implementation of the interface.
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ setOf(DeviceCapability.SEND_TAB),
+ null,
+ this.coroutineContext,
+ ) {
+ account
+ }
+
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ manager.register(accountObserver)
+
+ // Kick it off, we'll get into a "NotAuthenticated" state.
+ manager.start()
+
+ // Perform authentication.
+
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ // Assert that initDevice fired, but not ensureCapabilities (since we're initing a new account).
+ verify(constellation).finalizeDevice(eq(AuthType.Signin), any())
+
+ // Assert that persistence callback is interacting with the storage layer.
+ account.persistenceCallback!!.persist("test")
+ verify(accountStorage).write("test")
+ }
+
+ @Test
+ fun `auth state verification while finishing authentication`() = runTest {
+ val accountStorage: AccountStorage = mock()
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val constellation: DeviceConstellation = mockDeviceConstellation()
+ val account = StatePersistenceTestableAccount(profile, constellation)
+ val accountObserver: AccountObserver = mock()
+ // We are not using the "prepareHappy..." helper method here, because our account isn't a mock,
+ // but an actual implementation of the interface.
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ setOf(DeviceCapability.SEND_TAB),
+ null,
+ this.coroutineContext,
+ ) {
+ account
+ }
+
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ manager.register(accountObserver)
+ // Kick it off, we'll get into a "NotAuthenticated" state.
+ manager.start()
+
+ // Attempt to finish authentication without starting it first.
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", UNEXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() == null)
+
+ // Start authentication. StatePersistenceTestableAccount will produce state=EXPECTED_AUTH_STATE.
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
+
+ // Now attempt to finish it with a correct state.
+ `when`(constellation.finalizeDevice(eq(AuthType.Signin), any())).thenReturn(ServiceResult.Ok)
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ // Assert that manager is authenticated.
+ assertEquals(account, manager.authenticatedAccount())
+ }
+
+ class StatePersistenceTestableAccount(
+ private val profile: Profile,
+ private val constellation: DeviceConstellation,
+ val ableToRecoverFromAuthError: Boolean = false,
+ val tokenServerEndpointUrl: String? = null,
+ val accessToken: (() -> AccessTokenInfo)? = null,
+ ) : OAuthAccount {
+
+ var persistenceCallback: StatePersistenceCallback? = null
+ var checkAuthorizationStatusCalled = false
+ var authErrorDetectedCalled = false
+
+ override suspend fun beginOAuthFlow(scopes: Set<String>, entryPoint: FxAEntryPoint): AuthFlowUrl? {
+ return AuthFlowUrl(EXPECTED_AUTH_STATE, testAuthFlowUrl(entrypoint = entryPoint.entryName).url)
+ }
+
+ override suspend fun beginPairingFlow(pairingUrl: String, scopes: Set<String>, entryPoint: FxAEntryPoint): AuthFlowUrl? {
+ return AuthFlowUrl(EXPECTED_AUTH_STATE, testAuthFlowUrl(entrypoint = entryPoint.entryName).url)
+ }
+
+ override suspend fun getProfile(ignoreCache: Boolean): Profile? {
+ return profile
+ }
+
+ override fun getCurrentDeviceId(): String? {
+ return "testFxaDeviceId"
+ }
+
+ override fun getSessionToken(): String? {
+ return null
+ }
+
+ override suspend fun completeOAuthFlow(code: String, state: String): Boolean {
+ return true
+ }
+
+ override suspend fun getAccessToken(singleScope: String): AccessTokenInfo? {
+ val token = accessToken?.invoke()
+ if (token != null) return token
+
+ fail()
+ return null
+ }
+
+ override fun authErrorDetected() {
+ authErrorDetectedCalled = true
+ }
+
+ override suspend fun checkAuthorizationStatus(singleScope: String): Boolean? {
+ checkAuthorizationStatusCalled = true
+ return ableToRecoverFromAuthError
+ }
+
+ override suspend fun getTokenServerEndpointURL(): String? {
+ if (tokenServerEndpointUrl != null) return tokenServerEndpointUrl
+
+ fail()
+ return ""
+ }
+
+ override suspend fun getManageAccountURL(entryPoint: FxAEntryPoint): String? {
+ return "https://firefox.com/settings"
+ }
+
+ override fun getPairingAuthorityURL(): String {
+ return "https://firefox.com/pair"
+ }
+
+ override fun registerPersistenceCallback(callback: StatePersistenceCallback) {
+ persistenceCallback = callback
+ }
+
+ override fun deviceConstellation(): DeviceConstellation {
+ return constellation
+ }
+
+ override suspend fun disconnect(): Boolean {
+ return true
+ }
+
+ override fun toJSONString(): String {
+ fail()
+ return ""
+ }
+
+ override fun close() {
+ // Only expect 'close' to be called if we can't recover from an auth error.
+ if (ableToRecoverFromAuthError) {
+ fail()
+ }
+ }
+ }
+
+ @Test
+ fun `error reading persisted account`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val readException = FxaNetworkException("pretend we failed to fetch the account")
+ `when`(accountStorage.read()).thenThrow(readException)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = this.coroutineContext,
+ )
+
+ val accountObserver = object : AccountObserver {
+ override fun onLoggedOut() {
+ fail()
+ }
+
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ fail()
+ }
+
+ override fun onAuthenticationProblems() {
+ fail()
+ }
+
+ override fun onProfileUpdated(profile: Profile) {
+ fail()
+ }
+ }
+
+ manager.register(accountObserver)
+ manager.start()
+ }
+
+ @Test
+ fun `no persisted account`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = this.coroutineContext,
+ )
+
+ val accountObserver: AccountObserver = mock()
+
+ manager.register(accountObserver)
+ manager.start()
+
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+ verify(accountObserver, never()).onProfileUpdated(any())
+ verify(accountObserver, never()).onLoggedOut()
+
+ verify(accountStorage, times(1)).read()
+ verify(accountStorage, never()).write(any())
+ verify(accountStorage, never()).clear()
+
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+ }
+
+ @Test
+ fun `with persisted account and profile`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val profile = Profile(
+ "testUid",
+ "test@example.com",
+ null,
+ "Test Profile",
+ )
+ `when`(mockAccount.getProfile(ignoreCache = false)).thenReturn(profile)
+ // We have an account at the start.
+ `when`(accountStorage.read()).thenReturn(mockAccount)
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(eq(AuthType.Existing), any())).thenReturn(ServiceResult.Ok)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ emptySet(),
+ null,
+ this.coroutineContext,
+ )
+
+ val accountObserver: AccountObserver = mock()
+
+ manager.register(accountObserver)
+
+ manager.start()
+
+ // Make sure that account and profile observers are fired exactly once.
+ verify(accountObserver, times(1)).onAuthenticated(mockAccount, AuthType.Existing)
+ verify(accountObserver, times(1)).onProfileUpdated(profile)
+ verify(accountObserver, never()).onLoggedOut()
+
+ verify(accountStorage, times(1)).read()
+ verify(accountStorage, never()).write(any())
+ verify(accountStorage, never()).clear()
+
+ assertEquals(mockAccount, manager.authenticatedAccount())
+ assertEquals(profile, manager.accountProfile())
+
+ // Make sure 'logoutAsync' clears out state and fires correct observers.
+ reset(accountObserver)
+ reset(accountStorage)
+ `when`(mockAccount.disconnect()).thenReturn(true)
+
+ // Simulate SyncManager populating SyncEnginesStorage with some state.
+ SyncEnginesStorage(testContext).setStatus(SyncEngine.History, true)
+ SyncEnginesStorage(testContext).setStatus(SyncEngine.Passwords, false)
+ assertTrue(SyncEnginesStorage(testContext).getStatus().isNotEmpty())
+
+ verify(mockAccount, never()).disconnect()
+ manager.logout()
+
+ assertTrue(SyncEnginesStorage(testContext).getStatus().isEmpty())
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+ verify(accountObserver, never()).onProfileUpdated(any())
+ verify(accountObserver, times(1)).onLoggedOut()
+ verify(mockAccount, times(1)).disconnect()
+
+ verify(accountStorage, never()).read()
+ verify(accountStorage, never()).write(any())
+ verify(accountStorage, times(1)).clear()
+
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+ }
+
+ @Test
+ fun `happy authentication and profile flow`() = runTest {
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountStorage = mock<AccountStorage>()
+ val accountObserver: AccountObserver = mock()
+ val manager = prepareHappyAuthenticationFlow(mockAccount, profile, accountStorage, accountObserver, this.coroutineContext)
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountStorage, times(1)).read()
+ verify(accountStorage, never()).clear()
+
+ verify(accountObserver, times(1)).onAuthenticated(mockAccount, AuthType.Signin)
+ verify(accountObserver, times(1)).onProfileUpdated(profile)
+ verify(accountObserver, never()).onLoggedOut()
+
+ assertEquals(mockAccount, manager.authenticatedAccount())
+ assertEquals(profile, manager.accountProfile())
+
+ val cachedAuthInfo = SyncAuthInfoCache(testContext).getCached()
+ assertNotNull(cachedAuthInfo)
+ assertEquals("kid", cachedAuthInfo!!.kid)
+ assertEquals("someToken", cachedAuthInfo.fxaAccessToken)
+ assertEquals("k", cachedAuthInfo.syncKey)
+ assertEquals("some://url", cachedAuthInfo.tokenServerUrl)
+ assertTrue(cachedAuthInfo.fxaAccessTokenExpiresAt > 0)
+ }
+
+ @Test(expected = FxaPanicException::class)
+ fun `fxa panic during initDevice flow`() = runTest {
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountStorage = mock<AccountStorage>()
+ val accountObserver: AccountObserver = mock()
+ val manager = prepareHappyAuthenticationFlow(mockAccount, profile, accountStorage, accountObserver, this.coroutineContext)
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ doAnswer {
+ throw FxaPanicException("Don't panic!")
+ }.`when`(constellation).finalizeDevice(any(), any())
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+ }
+
+ @Test(expected = FxaPanicException::class)
+ fun `fxa panic during pairing flow`() = runTest {
+ val mockAccount: OAuthAccount = mock()
+ `when`(mockAccount.deviceConstellation()).thenReturn(mock())
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountStorage = mock<AccountStorage>()
+ `when`(mockAccount.getProfile(ignoreCache = false)).thenReturn(profile)
+
+ doAnswer {
+ throw FxaPanicException("Don't panic!")
+ }.`when`(mockAccount).beginPairingFlow(any(), any(), any())
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ manager.start()
+ manager.beginAuthentication("http://pairing.com", entryPoint)
+ fail()
+ }
+
+ @Test
+ fun `happy pairing authentication and profile flow`() = runTest {
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountStorage = mock<AccountStorage>()
+ val accountObserver: AccountObserver = mock()
+ val manager = prepareHappyAuthenticationFlow(mockAccount, profile, accountStorage, accountObserver, this.coroutineContext)
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(pairingUrl = "auth://pairing", entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountStorage, times(1)).read()
+ verify(accountStorage, never()).clear()
+
+ verify(accountObserver, times(1)).onAuthenticated(mockAccount, AuthType.Signin)
+ verify(accountObserver, times(1)).onProfileUpdated(profile)
+ verify(accountObserver, never()).onLoggedOut()
+
+ assertEquals(mockAccount, manager.authenticatedAccount())
+ assertEquals(profile, manager.accountProfile())
+ }
+
+ @Test
+ fun `repeated unfinished authentication attempts succeed`() = runTest {
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountStorage = mock<AccountStorage>()
+ val accountObserver: AccountObserver = mock()
+ val manager = prepareHappyAuthenticationFlow(mockAccount, profile, accountStorage, accountObserver, this.coroutineContext)
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ // Begin auth for the first time.
+ reset(accountObserver)
+ assertEquals(
+ testAuthFlowUrl(entrypoint = "home-menu").url,
+ manager.beginAuthentication(
+ pairingUrl = "auth://pairing",
+ entrypoint = entryPoint,
+ ),
+ )
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ // Now, try to begin again before finishing the first one.
+ assertEquals(
+ testAuthFlowUrl(entrypoint = "home-menu").url,
+ manager.beginAuthentication(
+ pairingUrl = "auth://pairing",
+ entrypoint = entryPoint,
+ ),
+ )
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ // The rest should "just work".
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountStorage, times(1)).read()
+ verify(accountStorage, never()).clear()
+
+ verify(accountObserver, times(1)).onAuthenticated(mockAccount, AuthType.Signin)
+ verify(accountObserver, times(1)).onProfileUpdated(profile)
+ verify(accountObserver, never()).onLoggedOut()
+
+ assertEquals(mockAccount, manager.authenticatedAccount())
+ assertEquals(profile, manager.accountProfile())
+ }
+
+ @Test
+ fun `unhappy authentication flow`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountObserver: AccountObserver = mock()
+ val manager = prepareUnhappyAuthenticationFlow(mockAccount, profile, accountStorage, accountObserver, this.coroutineContext)
+
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+
+ assertNull(manager.beginAuthentication(entrypoint = entryPoint))
+
+ // Confirm that account state observable doesn't receive authentication errors.
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ // Try again, without any network problems this time.
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl())
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ assertEquals(testAuthFlowUrl().url, manager.beginAuthentication(entrypoint = entryPoint))
+
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+ verify(accountStorage, times(1)).clear()
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountStorage, times(1)).read()
+ verify(accountStorage, times(1)).clear()
+
+ verify(accountObserver, times(1)).onAuthenticated(mockAccount, AuthType.Signin)
+ verify(accountObserver, times(1)).onProfileUpdated(profile)
+ verify(accountObserver, never()).onLoggedOut()
+
+ assertEquals(mockAccount, manager.authenticatedAccount())
+ assertEquals(profile, manager.accountProfile())
+ }
+
+ @Test
+ fun `unhappy pairing authentication flow`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountObserver: AccountObserver = mock()
+ val manager = prepareUnhappyAuthenticationFlow(mockAccount, profile, accountStorage, accountObserver, this.coroutineContext)
+
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+
+ assertNull(manager.beginAuthentication(pairingUrl = "auth://pairing", entrypoint = entryPoint))
+
+ // Confirm that account state observable doesn't receive authentication errors.
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ // Try again, without any network problems this time.
+ `when`(
+ mockAccount.beginPairingFlow(
+ anyString(),
+ any(),
+ any(),
+ ),
+ ).thenReturn(testAuthFlowUrl())
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ assertEquals(
+ testAuthFlowUrl().url,
+ manager.beginAuthentication(
+ pairingUrl = "auth://pairing",
+ entrypoint = entryPoint,
+ ),
+ )
+
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+ verify(accountStorage, times(1)).clear()
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountStorage, times(1)).read()
+ verify(accountStorage, times(1)).clear()
+
+ verify(accountObserver, times(1)).onAuthenticated(mockAccount, AuthType.Signin)
+ verify(accountObserver, times(1)).onProfileUpdated(profile)
+ verify(accountObserver, never()).onLoggedOut()
+
+ assertEquals(mockAccount, manager.authenticatedAccount())
+ assertEquals(profile, manager.accountProfile())
+ }
+
+ @Test
+ fun `authentication issues are propagated via AccountObserver`() = runTest {
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountStorage = mock<AccountStorage>()
+ val accountObserver: AccountObserver = mock()
+ val manager = prepareHappyAuthenticationFlow(mockAccount, profile, accountStorage, accountObserver, this.coroutineContext)
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountObserver, never()).onAuthenticationProblems()
+ assertFalse(manager.accountNeedsReauth())
+
+ // Our recovery flow should attempt to hit this API. Model the "can't recover" condition by returning 'false'.
+ `when`(mockAccount.checkAuthorizationStatus(eq("profile"))).thenReturn(false)
+
+ // At this point, we're logged in. Trigger a 401.
+ manager.encounteredAuthError("a test")
+
+ verify(accountObserver, times(1)).onAuthenticationProblems()
+ assertTrue(manager.accountNeedsReauth())
+ assertEquals(mockAccount, manager.authenticatedAccount())
+
+ // Make sure profile is still available.
+ assertEquals(profile, manager.accountProfile())
+
+ // Able to re-authenticate.
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Pairing, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountObserver).onAuthenticated(mockAccount, AuthType.Pairing)
+ verify(accountObserver, never()).onAuthenticationProblems()
+ assertFalse(manager.accountNeedsReauth())
+ assertEquals(profile, manager.accountProfile())
+ }
+
+ @Test
+ fun `authentication issues are recoverable via checkAuthorizationState`() = runTest {
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountStorage = mock<AccountStorage>()
+ val accountObserver: AccountObserver = mock()
+ val crashReporter: CrashReporting = mock()
+ val manager = prepareHappyAuthenticationFlow(
+ mockAccount,
+ profile,
+ accountStorage,
+ accountObserver,
+ this.coroutineContext,
+ setOf(DeviceCapability.SEND_TAB),
+ crashReporter,
+ )
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+ `when`(constellation.refreshDevices()).thenReturn(true)
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountObserver, never()).onAuthenticationProblems()
+ assertFalse(manager.accountNeedsReauth())
+
+ // Recovery flow will hit this API, and will recover if it returns 'true'.
+ `when`(mockAccount.checkAuthorizationStatus(eq("profile"))).thenReturn(true)
+
+ // At this point, we're logged in. Trigger a 401.
+ manager.encounteredAuthError("a test")
+ assertRecovered(true, "a test", constellation, accountObserver, manager, mockAccount, crashReporter)
+ }
+
+ @Test
+ fun `authentication recovery flow has a circuit breaker`() = runTest {
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
+ val accountStorage = mock<AccountStorage>()
+ val accountObserver: AccountObserver = mock()
+ val crashReporter: CrashReporting = mock()
+ val manager = prepareHappyAuthenticationFlow(
+ mockAccount,
+ profile,
+ accountStorage,
+ accountObserver,
+ this.coroutineContext,
+ setOf(DeviceCapability.SEND_TAB),
+ crashReporter,
+ )
+ GlobalAccountManager.setInstance(manager)
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+ `when`(constellation.refreshDevices()).thenReturn(true)
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountObserver, never()).onAuthenticationProblems()
+ assertFalse(manager.accountNeedsReauth())
+
+ // Recovery flow will hit this API, and will recover if it returns 'true'.
+ `when`(mockAccount.checkAuthorizationStatus(eq("profile"))).thenReturn(true)
+
+ // At this point, we're logged in. Trigger a 401 for the first time.
+ manager.encounteredAuthError("a test")
+ // ... and just for good measure, trigger another 401 to simulate overlapping API calls legitimately hitting 401s.
+ manager.encounteredAuthError("a test", errorCountWithinTheTimeWindow = 3)
+ assertRecovered(true, "a test", constellation, accountObserver, manager, mockAccount, crashReporter)
+
+ // We've fully recovered by now, let's hit another 401 sometime later (count has been reset).
+ manager.encounteredAuthError("a test")
+ assertRecovered(true, "a test", constellation, accountObserver, manager, mockAccount, crashReporter)
+
+ // Suddenly, we're in a bad loop, expect to hit our circuit-breaker here.
+ manager.encounteredAuthError("another test", errorCountWithinTheTimeWindow = 50)
+ assertRecovered(false, "another test", constellation, accountObserver, manager, mockAccount, crashReporter)
+ }
+
+ private suspend fun assertRecovered(
+ success: Boolean,
+ operation: String,
+ constellation: DeviceConstellation,
+ accountObserver: AccountObserver,
+ manager: FxaAccountManager,
+ mockAccount: OAuthAccount,
+ crashReporter: CrashReporting,
+ ) {
+ // During recovery, only 'sign-in' finalize device call should have been made.
+ verify(constellation, times(1)).finalizeDevice(eq(AuthType.Signin), any())
+ verify(constellation, never()).finalizeDevice(eq(AuthType.Recovered), any())
+
+ assertEquals(mockAccount, manager.authenticatedAccount())
+
+ if (success) {
+ // Since we've recovered, outside observers should not have witnessed the momentary problem state.
+ verify(accountObserver, never()).onAuthenticationProblems()
+ assertFalse(manager.accountNeedsReauth())
+ verify(crashReporter, never()).submitCaughtException(any())
+ } else {
+ // We were unable to recover, outside observers should have been told.
+ verify(accountObserver, times(1)).onAuthenticationProblems()
+ assertTrue(manager.accountNeedsReauth())
+
+ val captor = argumentCaptor<Throwable>()
+ verify(crashReporter).submitCaughtException(captor.capture())
+ assertEquals("Auth recovery circuit breaker triggered by: $operation", captor.value.message)
+ assertTrue(captor.value is AccountManagerException.AuthRecoveryCircuitBreakerException)
+ }
+ }
+
+ @Test
+ fun `unhappy profile fetching flow`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+ `when`(mockAccount.getProfile(ignoreCache = false)).thenReturn(null)
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl())
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = this.coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ val accountObserver: AccountObserver = mock()
+
+ manager.register(accountObserver)
+ manager.start()
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl().url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ verify(accountStorage, times(1)).read()
+ verify(accountStorage, never()).clear()
+
+ verify(accountObserver, times(1)).onAuthenticated(mockAccount, AuthType.Signin)
+ verify(accountObserver, never()).onProfileUpdated(any())
+ verify(accountObserver, never()).onLoggedOut()
+
+ assertEquals(mockAccount, manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ // Make sure we can re-try fetching a profile. This time, let's have it succeed.
+ reset(accountObserver)
+ val profile = Profile(
+ uid = "testUID",
+ avatar = null,
+ email = "test@example.com",
+ displayName = "test profile",
+ )
+
+ `when`(mockAccount.getProfile(ignoreCache = true)).thenReturn(profile)
+ assertNull(manager.accountProfile())
+ assertEquals(profile, manager.refreshProfile(true))
+
+ verify(accountObserver, times(1)).onProfileUpdated(profile)
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+ verify(accountObserver, never()).onLoggedOut()
+ }
+
+ @Test
+ fun `profile fetching flow hit an unrecoverable auth problem`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ // Our recovery flow should attempt to hit this API. Model the "can't recover" condition by returning false.
+ `when`(mockAccount.checkAuthorizationStatus(eq("profile"))).thenReturn(false)
+
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl())
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = this.coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ lateinit var waitFor: Job
+ `when`(mockAccount.getProfile(ignoreCache = false)).then {
+ // Hit an auth error.
+ waitFor = CoroutineScope(coroutineContext).launch { manager.encounteredAuthError("a test") }
+ null
+ }
+
+ val accountObserver: AccountObserver = mock()
+
+ manager.register(accountObserver)
+ manager.start()
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+ verify(accountObserver, never()).onAuthenticationProblems()
+ verify(mockAccount, never()).checkAuthorizationStatus(any())
+ assertFalse(manager.accountNeedsReauth())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl().url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+
+ waitFor.join()
+ assertTrue(manager.accountNeedsReauth())
+ verify(accountObserver, times(1)).onAuthenticationProblems()
+ verify(mockAccount, times(1)).checkAuthorizationStatus(eq("profile"))
+ Unit
+ }
+
+ @Test
+ fun `profile fetching flow hit an unrecoverable auth problem for which we can't determine a recovery state`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ // Our recovery flow should attempt to hit this API. Model the "don't know what's up" condition by returning null.
+ `when`(mockAccount.checkAuthorizationStatus(eq("profile"))).thenReturn(null)
+
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl())
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = this.coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ lateinit var waitFor: Job
+ `when`(mockAccount.getProfile(ignoreCache = false)).then {
+ // Hit an auth error.
+ waitFor = CoroutineScope(coroutineContext).launch { manager.encounteredAuthError("a test") }
+ null
+ }
+
+ val accountObserver: AccountObserver = mock()
+
+ manager.register(accountObserver)
+ manager.start()
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+ verify(accountObserver, never()).onAuthenticationProblems()
+ verify(mockAccount, never()).checkAuthorizationStatus(any())
+ assertFalse(manager.accountNeedsReauth())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl().url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+
+ waitFor.join()
+ assertTrue(manager.accountNeedsReauth())
+ verify(accountObserver, times(1)).onAuthenticationProblems()
+ verify(mockAccount, times(1)).checkAuthorizationStatus(eq("profile"))
+ Unit
+ }
+
+ @Test
+ fun `profile fetching flow hit a recoverable auth problem`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val captor = argumentCaptor<AuthType>()
+
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+
+ val profile = Profile(
+ uid = "testUID",
+ avatar = null,
+ email = "test@example.com",
+ displayName = "test profile",
+ )
+
+ // Recovery flow will hit this API, return a success.
+ `when`(mockAccount.checkAuthorizationStatus(eq("profile"))).thenReturn(true)
+
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl())
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = this.coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ var didFailProfileFetch = false
+ lateinit var waitFor: Job
+ `when`(mockAccount.getProfile(ignoreCache = false)).then {
+ // Hit an auth error, but only once. As we recover from it, we'll attempt to fetch a profile
+ // again. At that point, we'd like to succeed.
+ if (!didFailProfileFetch) {
+ didFailProfileFetch = true
+ waitFor = CoroutineScope(coroutineContext).launch { manager.encounteredAuthError("a test") }
+ null
+ } else {
+ profile
+ }
+ }
+ // Upon recovery, we'll hit an 'ignore cache' path.
+ `when`(mockAccount.getProfile(ignoreCache = true)).thenReturn(profile)
+
+ val accountObserver: AccountObserver = mock()
+
+ manager.register(accountObserver)
+ manager.start()
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+ verify(accountObserver, never()).onAuthenticationProblems()
+ verify(mockAccount, never()).checkAuthorizationStatus(any())
+ assertFalse(manager.accountNeedsReauth())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl().url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signup, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+ waitFor.join()
+ assertFalse(manager.accountNeedsReauth())
+ assertEquals(mockAccount, manager.authenticatedAccount())
+ assertEquals(profile, manager.accountProfile())
+ verify(accountObserver, never()).onAuthenticationProblems()
+ // Once for the initial auth success, then once again after we recover from an auth problem.
+ verify(accountObserver, times(2)).onAuthenticated(eq(mockAccount), captor.capture())
+ assertEquals(AuthType.Signup, captor.allValues[0])
+ assertEquals(AuthType.Recovered, captor.allValues[1])
+ // Verify that we went through the recovery flow.
+ verify(mockAccount, times(1)).checkAuthorizationStatus(eq("profile"))
+ Unit
+ }
+
+ @Test(expected = FxaPanicException::class)
+ fun `profile fetching flow hit an fxa panic, which is re-thrown`() = runTest {
+ val accountStorage = mock<AccountStorage>()
+ val mockAccount: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+
+ `when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
+ `when`(mockAccount.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.finalizeDevice(any(), any())).thenReturn(ServiceResult.Ok)
+ doAnswer {
+ throw FxaPanicException("500")
+ }.`when`(mockAccount).getProfile(ignoreCache = false)
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl())
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = this.coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ val accountObserver: AccountObserver = mock()
+
+ manager.register(accountObserver)
+ manager.start()
+
+ // We start off as logged-out, but the event won't be called (initial default state is assumed).
+ verify(accountObserver, never()).onLoggedOut()
+ verify(accountObserver, never()).onAuthenticated(any(), any())
+ verify(accountObserver, never()).onAuthenticationProblems()
+ assertFalse(manager.accountNeedsReauth())
+
+ reset(accountObserver)
+ assertEquals(testAuthFlowUrl().url, manager.beginAuthentication(entrypoint = entryPoint))
+ assertNull(manager.authenticatedAccount())
+ assertNull(manager.accountProfile())
+
+ manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", EXPECTED_AUTH_STATE))
+ assertTrue(manager.authenticatedAccount() != null)
+ }
+
+ @Test
+ fun `accounts to sync integration`() {
+ val syncManager: SyncManager = mock()
+ val integration = FxaAccountManager.AccountsToSyncIntegration(syncManager)
+
+ // onAuthenticated - mapping of AuthType to SyncReason
+ integration.onAuthenticated(mock(), AuthType.Signin)
+ verify(syncManager, times(1)).start()
+ verify(syncManager, times(1)).now(eq(SyncReason.FirstSync), anyBoolean(), eq(listOf()))
+ integration.onAuthenticated(mock(), AuthType.Signup)
+ verify(syncManager, times(2)).start()
+ verify(syncManager, times(2)).now(eq(SyncReason.FirstSync), anyBoolean(), eq(listOf()))
+ integration.onAuthenticated(mock(), AuthType.Pairing)
+ verify(syncManager, times(3)).start()
+ verify(syncManager, times(3)).now(eq(SyncReason.FirstSync), anyBoolean(), eq(listOf()))
+ integration.onAuthenticated(mock(), AuthType.MigratedReuse)
+ verify(syncManager, times(4)).start()
+ verify(syncManager, times(4)).now(eq(SyncReason.FirstSync), anyBoolean(), eq(listOf()))
+ integration.onAuthenticated(mock(), AuthType.OtherExternal("test"))
+ verify(syncManager, times(5)).start()
+ verify(syncManager, times(5)).now(eq(SyncReason.FirstSync), anyBoolean(), eq(listOf()))
+ integration.onAuthenticated(mock(), AuthType.Existing)
+ verify(syncManager, times(6)).start()
+ verify(syncManager, times(1)).now(eq(SyncReason.Startup), anyBoolean(), eq(listOf()))
+ integration.onAuthenticated(mock(), AuthType.Recovered)
+ verify(syncManager, times(7)).start()
+ verify(syncManager, times(2)).now(eq(SyncReason.Startup), anyBoolean(), eq(listOf()))
+ verifyNoMoreInteractions(syncManager)
+
+ // onProfileUpdated - no-op
+ integration.onProfileUpdated(mock())
+ verifyNoMoreInteractions(syncManager)
+
+ // onAuthenticationProblems
+ integration.onAuthenticationProblems()
+ verify(syncManager).stop()
+ verifyNoMoreInteractions(syncManager)
+
+ // onLoggedOut
+ integration.onLoggedOut()
+ verify(syncManager, times(2)).stop()
+ verifyNoMoreInteractions(syncManager)
+ }
+
+ @Test
+ fun `GIVEN a sync observer WHEN registering it THEN add it to the sync observer registry`() {
+ val fxaManager = TestableFxaAccountManager(
+ context = testContext,
+ config = mock(),
+ storage = mock(),
+ capabilities = setOf(DeviceCapability.SEND_TAB),
+ syncConfig = null,
+ coroutineContext = mock(),
+ )
+ fxaManager.syncStatusObserverRegistry = mock()
+ val observer: SyncStatusObserver = mock()
+ val lifecycleOwner: LifecycleOwner = mock()
+
+ fxaManager.registerForSyncEvents(observer, lifecycleOwner, false)
+
+ verify(fxaManager.syncStatusObserverRegistry).register(observer, lifecycleOwner, false)
+ verifyNoMoreInteractions(fxaManager.syncStatusObserverRegistry)
+ }
+
+ @Test
+ fun `GIVEN a sync observer WHEN unregistering it THEN remove it from the sync observer registry`() {
+ val fxaManager = TestableFxaAccountManager(
+ context = testContext,
+ config = mock(),
+ storage = mock(),
+ capabilities = setOf(DeviceCapability.SEND_TAB),
+ syncConfig = null,
+ coroutineContext = mock(),
+ )
+ fxaManager.syncStatusObserverRegistry = mock()
+ val observer: SyncStatusObserver = mock()
+
+ fxaManager.unregisterForSyncEvents(observer)
+
+ verify(fxaManager.syncStatusObserverRegistry).unregister(observer)
+ verifyNoMoreInteractions(fxaManager.syncStatusObserverRegistry)
+ }
+
+ private suspend fun prepareHappyAuthenticationFlow(
+ mockAccount: OAuthAccount,
+ profile: Profile,
+ accountStorage: AccountStorage,
+ accountObserver: AccountObserver,
+ coroutineContext: CoroutineContext,
+ capabilities: Set<DeviceCapability> = emptySet(),
+ crashReporter: CrashReporting? = null,
+ ): FxaAccountManager {
+ val accessTokenInfo = AccessTokenInfo(
+ "testSc0pe",
+ "someToken",
+ OAuthScopedKey("kty", "testSc0pe", "kid", "k"),
+ System.currentTimeMillis() + 1000 * 10,
+ )
+
+ `when`(mockAccount.getProfile(ignoreCache = false)).thenReturn(profile)
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl(entrypoint = "home-menu"))
+ `when`(mockAccount.beginPairingFlow(anyString(), any(), any())).thenReturn(testAuthFlowUrl(entrypoint = "home-menu"))
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ `when`(mockAccount.getAccessToken(anyString())).thenReturn(accessTokenInfo)
+ `when`(mockAccount.getTokenServerEndpointURL()).thenReturn("some://url")
+
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ capabilities,
+ SyncConfig(setOf(SyncEngine.History, SyncEngine.Bookmarks), PeriodicSyncConfig()),
+ coroutineContext = coroutineContext,
+ crashReporter = crashReporter,
+ ) {
+ mockAccount
+ }
+
+ manager.register(accountObserver)
+ manager.start()
+
+ return manager
+ }
+
+ private suspend fun prepareUnhappyAuthenticationFlow(
+ mockAccount: OAuthAccount,
+ profile: Profile,
+ accountStorage: AccountStorage,
+ accountObserver: AccountObserver,
+ coroutineContext: CoroutineContext,
+ ): FxaAccountManager {
+ `when`(mockAccount.getProfile(ignoreCache = false)).thenReturn(profile)
+ `when`(mockAccount.deviceConstellation()).thenReturn(mock())
+ `when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(null)
+ `when`(mockAccount.beginPairingFlow(anyString(), any(), any())).thenReturn(null)
+ `when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ // There's no account at the start.
+ `when`(accountStorage.read()).thenReturn(null)
+
+ val manager = TestableFxaAccountManager(
+ testContext,
+ FxaConfig(FxaServer.Release, "dummyId", "bad://url"),
+ accountStorage,
+ coroutineContext = coroutineContext,
+ ) {
+ mockAccount
+ }
+
+ manager.register(accountObserver)
+
+ manager.start()
+
+ return manager
+ }
+
+ private suspend fun mockDeviceConstellation(): DeviceConstellation {
+ val c: DeviceConstellation = mock()
+ `when`(c.refreshDevices()).thenReturn(true)
+ return c
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt
new file mode 100644
index 0000000000..3132e0d752
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt
@@ -0,0 +1,498 @@
+/* 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.service.fxa
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.plus
+import mozilla.appservices.fxaclient.FxaException
+import mozilla.appservices.fxaclient.IncomingDeviceCommand
+import mozilla.appservices.fxaclient.SendTabPayload
+import mozilla.appservices.fxaclient.TabHistoryEntry
+import mozilla.appservices.syncmanager.DeviceSettings
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceCommandIncoming
+import mozilla.components.concept.sync.DeviceCommandOutgoing
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.concept.sync.DeviceConstellationObserver
+import mozilla.components.concept.sync.DevicePushSubscription
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.TabData
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.`when`
+import mozilla.appservices.fxaclient.AccountEvent as ASAccountEvent
+import mozilla.appservices.fxaclient.Device as NativeDevice
+import mozilla.appservices.fxaclient.DevicePushSubscription as NativeDevicePushSubscription
+import mozilla.appservices.fxaclient.FxaClient as NativeFirefoxAccount
+import mozilla.appservices.sync15.DeviceType as RustDeviceType
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class FxaDeviceConstellationTest {
+ lateinit var account: NativeFirefoxAccount
+ lateinit var constellation: FxaDeviceConstellation
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ account = mock()
+ val scope = CoroutineScope(coroutinesTestRule.testDispatcher) + SupervisorJob()
+ constellation = FxaDeviceConstellation(account, scope, mock())
+ }
+
+ @Test
+ fun `finalize device`() = runTestOnMain {
+ fun expectedFinalizeAction(authType: AuthType): FxaDeviceConstellation.DeviceFinalizeAction = when (authType) {
+ AuthType.Existing -> FxaDeviceConstellation.DeviceFinalizeAction.EnsureCapabilities
+ AuthType.Signin -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
+ AuthType.Signup -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
+ AuthType.Pairing -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
+ is AuthType.OtherExternal -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
+ AuthType.MigratedCopy -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
+ AuthType.MigratedReuse -> FxaDeviceConstellation.DeviceFinalizeAction.EnsureCapabilities
+ AuthType.Recovered -> FxaDeviceConstellation.DeviceFinalizeAction.None
+ }
+ fun initAuthType(simpleClassName: String): AuthType = when (simpleClassName) {
+ "Existing" -> AuthType.Existing
+ "Signin" -> AuthType.Signin
+ "Signup" -> AuthType.Signup
+ "Pairing" -> AuthType.Pairing
+ "OtherExternal" -> AuthType.OtherExternal("test")
+ "MigratedCopy" -> AuthType.MigratedCopy
+ "MigratedReuse" -> AuthType.MigratedReuse
+ "Recovered" -> AuthType.Recovered
+ else -> throw AssertionError("Unknown AuthType: $simpleClassName")
+ }
+ val config = DeviceConfig("test name", DeviceType.TABLET, setOf(DeviceCapability.SEND_TAB))
+ AuthType::class.sealedSubclasses.map { initAuthType(it.simpleName!!) }.forEach {
+ constellation.finalizeDevice(it, config)
+ when (expectedFinalizeAction(it)) {
+ FxaDeviceConstellation.DeviceFinalizeAction.Initialize -> {
+ verify(account).initializeDevice("test name", RustDeviceType.TABLET, setOf(mozilla.appservices.fxaclient.DeviceCapability.SEND_TAB))
+ }
+ FxaDeviceConstellation.DeviceFinalizeAction.EnsureCapabilities -> {
+ verify(account).ensureCapabilities(setOf(mozilla.appservices.fxaclient.DeviceCapability.SEND_TAB))
+ }
+ FxaDeviceConstellation.DeviceFinalizeAction.None -> {
+ verifyNoInteractions(account)
+ }
+ }
+ reset(account)
+ }
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `updating device name`() = runTestOnMain {
+ val currentDevice = testDevice("currentTestDevice", true)
+ `when`(account.getDevices()).thenReturn(arrayOf(currentDevice))
+
+ // Can't update cached value in an empty cache
+ try {
+ constellation.setDeviceName("new name", testContext)
+ fail()
+ } catch (e: IllegalStateException) {}
+
+ val cache = FxaDeviceSettingsCache(testContext)
+ cache.setToCache(DeviceSettings("someId", "test name", RustDeviceType.MOBILE))
+
+ // No device state observer.
+ assertTrue(constellation.setDeviceName("new name", testContext))
+ verify(account, times(2)).setDeviceDisplayName("new name")
+
+ assertEquals(DeviceSettings("someId", "new name", RustDeviceType.MOBILE), cache.getCached())
+
+ // Set up the observer...
+ val observer = object : DeviceConstellationObserver {
+ var state: ConstellationState? = null
+
+ override fun onDevicesUpdate(constellation: ConstellationState) {
+ state = constellation
+ }
+ }
+ constellation.registerDeviceObserver(observer, startedLifecycleOwner(), false)
+
+ assertTrue(constellation.setDeviceName("another name", testContext))
+ verify(account).setDeviceDisplayName("another name")
+
+ assertEquals(DeviceSettings("someId", "another name", RustDeviceType.MOBILE), cache.getCached())
+
+ // Since we're faking the data, here we're just testing that observer is notified with the
+ // up-to-date constellation.
+ assertEquals(observer.state!!.currentDevice!!.displayName, "testName")
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `set device push subscription`() = runTestOnMain {
+ val subscription = DevicePushSubscription("http://endpoint.com", "pk", "auth key")
+ constellation.setDevicePushSubscription(subscription)
+
+ verify(account).setDevicePushSubscription("http://endpoint.com", "pk", "auth key")
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `process raw device command`() = runTestOnMain {
+ // No commands, no observer.
+ `when`(account.handlePushMessage("raw events payload")).thenReturn(mozilla.appservices.fxaclient.AccountEvent.Unknown)
+ assertTrue(constellation.processRawEvent("raw events payload"))
+
+ // No commands, with observer.
+ val eventsObserver = object : AccountEventsObserver {
+ var latestEvents: List<AccountEvent>? = null
+
+ override fun onEvents(events: List<AccountEvent>) {
+ latestEvents = events
+ }
+ }
+
+ // No commands, with an observer.
+ constellation.register(eventsObserver)
+ assertTrue(constellation.processRawEvent("raw events payload"))
+ assertEquals(listOf(AccountEvent.Unknown), eventsObserver.latestEvents)
+
+ // Some commands, with an observer. More detailed command handling tests below.
+ val testDevice1 = testDevice("test1", false)
+ val testTab1 = TabHistoryEntry("Hello", "http://world.com/1")
+ `when`(account.handlePushMessage("raw events payload")).thenReturn(
+ ASAccountEvent.CommandReceived(
+ command = IncomingDeviceCommand.TabReceived(testDevice1, SendTabPayload(listOf(testTab1), "flowid", "streamid")),
+ ),
+ )
+
+ `when`(account.pollDeviceCommands()).thenReturn(
+ arrayOf(
+ IncomingDeviceCommand.TabReceived(testDevice1, SendTabPayload(listOf(testTab1), "flowid", "streamid")),
+ ),
+ )
+ assertTrue(constellation.processRawEvent("raw events payload"))
+ verify(account).pollDeviceCommands()
+
+ val events = eventsObserver.latestEvents!!
+ val command = (events[0] as AccountEvent.DeviceCommandIncoming).command
+ assertEquals(testDevice1.into(), (command as DeviceCommandIncoming.TabReceived).from)
+ assertEquals(listOf(testTab1.into()), command.entries)
+ }
+
+ @Test
+ fun `send command to device`() = runTestOnMain {
+ `when`(account.gatherTelemetry()).thenReturn("{}")
+ assertTrue(
+ constellation.sendCommandToDevice(
+ "targetID",
+ DeviceCommandOutgoing.SendTab("Mozilla", "https://www.mozilla.org"),
+ ),
+ )
+
+ verify(account).sendSingleTab("targetID", "Mozilla", "https://www.mozilla.org")
+ }
+
+ @Test
+ fun `send command to device will report exceptions`() = runTestOnMain {
+ val exception = FxaException.Other("")
+ val exceptionCaptor = argumentCaptor<SendCommandException>()
+ doAnswer { throw exception }.`when`(account).sendSingleTab(any(), any(), any())
+
+ val success = constellation.sendCommandToDevice(
+ "targetID",
+ DeviceCommandOutgoing.SendTab("Mozilla", "https://www.mozilla.org"),
+ )
+
+ assertFalse(success)
+ verify(constellation.crashReporter!!).submitCaughtException(exceptionCaptor.capture())
+ assertSame(exception, exceptionCaptor.value.cause)
+ }
+
+ @Test
+ fun `send command to device won't report network exceptions`() = runTestOnMain {
+ val exception = FxaException.Network("timeout!")
+ doAnswer { throw exception }.`when`(account).sendSingleTab(any(), any(), any())
+
+ val success = constellation.sendCommandToDevice(
+ "targetID",
+ DeviceCommandOutgoing.SendTab("Mozilla", "https://www.mozilla.org"),
+ )
+
+ assertFalse(success)
+ verify(constellation.crashReporter!!, never()).submitCaughtException(any())
+ Unit
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `refreshing constellation`() = runTestOnMain {
+ // No devices, no observers.
+ `when`(account.getDevices()).thenReturn(emptyArray())
+
+ constellation.refreshDevices()
+
+ val observer = object : DeviceConstellationObserver {
+ var state: ConstellationState? = null
+
+ override fun onDevicesUpdate(constellation: ConstellationState) {
+ state = constellation
+ }
+ }
+ constellation.registerDeviceObserver(observer, startedLifecycleOwner(), false)
+
+ // No devices, with an observer.
+ constellation.refreshDevices()
+ assertEquals(ConstellationState(null, listOf()), observer.state)
+
+ val testDevice1 = testDevice("test1", false)
+ val testDevice2 = testDevice("test2", false)
+ val currentDevice = testDevice("currentTestDevice", true)
+
+ // Single device, no current device.
+ `when`(account.getDevices()).thenReturn(arrayOf(testDevice1))
+ constellation.refreshDevices()
+
+ assertEquals(ConstellationState(null, listOf(testDevice1.into())), observer.state)
+ assertEquals(ConstellationState(null, listOf(testDevice1.into())), constellation.state())
+
+ // Current device, no other devices.
+ `when`(account.getDevices()).thenReturn(arrayOf(currentDevice))
+ constellation.refreshDevices()
+ assertEquals(ConstellationState(currentDevice.into(), listOf()), observer.state)
+ assertEquals(ConstellationState(currentDevice.into(), listOf()), constellation.state())
+
+ // Current device with other devices.
+ `when`(account.getDevices()).thenReturn(
+ arrayOf(
+ currentDevice,
+ testDevice1,
+ testDevice2,
+ ),
+ )
+ constellation.refreshDevices()
+
+ assertEquals(ConstellationState(currentDevice.into(), listOf(testDevice1.into(), testDevice2.into())), observer.state)
+ assertEquals(ConstellationState(currentDevice.into(), listOf(testDevice1.into(), testDevice2.into())), constellation.state())
+
+ // Current device with expired subscription.
+ val currentDeviceExpired = testDevice("currentExpired", true, expired = true)
+ `when`(account.getDevices()).thenReturn(
+ arrayOf(
+ currentDeviceExpired,
+ testDevice2,
+ ),
+ )
+
+ `when`(account.pollDeviceCommands()).thenReturn(emptyArray())
+ `when`(account.gatherTelemetry()).thenReturn("{}")
+
+ constellation.refreshDevices()
+
+ verify(account, times(1)).pollDeviceCommands()
+
+ assertEquals(ConstellationState(currentDeviceExpired.into(), listOf(testDevice2.into())), observer.state)
+ assertEquals(ConstellationState(currentDeviceExpired.into(), listOf(testDevice2.into())), constellation.state())
+
+ // Current device with no subscription.
+ val currentDeviceNoSub = testDevice("currentNoSub", true, expired = false, subscribed = false)
+
+ `when`(account.getDevices()).thenReturn(
+ arrayOf(
+ currentDeviceNoSub,
+ testDevice2,
+ ),
+ )
+
+ `when`(account.pollDeviceCommands()).thenReturn(emptyArray())
+ `when`(account.gatherTelemetry()).thenReturn("{}")
+
+ constellation.refreshDevices()
+
+ verify(account, times(2)).pollDeviceCommands()
+ assertEquals(ConstellationState(currentDeviceNoSub.into(), listOf(testDevice2.into())), constellation.state())
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `polling for commands triggers observers`() = runTestOnMain {
+ // No commands, no observers.
+ `when`(account.gatherTelemetry()).thenReturn("{}")
+ `when`(account.pollDeviceCommands()).thenReturn(emptyArray())
+ assertTrue(constellation.pollForCommands())
+
+ val eventsObserver = object : AccountEventsObserver {
+ var latestEvents: List<AccountEvent>? = null
+
+ override fun onEvents(events: List<AccountEvent>) {
+ latestEvents = events
+ }
+ }
+
+ // No commands, with an observer.
+ constellation.register(eventsObserver)
+ assertTrue(constellation.pollForCommands())
+ assertEquals(listOf<AccountEvent>(), eventsObserver.latestEvents)
+
+ // Some commands.
+ `when`(account.pollDeviceCommands()).thenReturn(
+ arrayOf(
+ IncomingDeviceCommand.TabReceived(null, SendTabPayload(emptyList(), "", "")),
+ ),
+ )
+ assertTrue(constellation.pollForCommands())
+
+ var command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
+ assertEquals(null, (command as DeviceCommandIncoming.TabReceived).from)
+ assertEquals(listOf<TabData>(), command.entries)
+
+ val testDevice1 = testDevice("test1", false)
+ val testDevice2 = testDevice("test2", false)
+ val testTab1 = TabHistoryEntry("Hello", "http://world.com/1")
+ val testTab2 = TabHistoryEntry("Hello", "http://world.com/2")
+ val testTab3 = TabHistoryEntry("Hello", "http://world.com/3")
+
+ // Zero tabs from a single device.
+ `when`(account.pollDeviceCommands()).thenReturn(
+ arrayOf(
+ IncomingDeviceCommand.TabReceived(testDevice1, SendTabPayload(emptyList(), "", "")),
+ ),
+ )
+ assertTrue(constellation.pollForCommands())
+
+ Assert.assertNotNull(eventsObserver.latestEvents)
+ assertEquals(1, eventsObserver.latestEvents!!.size)
+ command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
+ assertEquals(testDevice1.into(), (command as DeviceCommandIncoming.TabReceived).from)
+ assertEquals(listOf<TabData>(), command.entries)
+
+ // Single tab from a single device.
+ `when`(account.pollDeviceCommands()).thenReturn(
+ arrayOf(
+ IncomingDeviceCommand.TabReceived(testDevice2, SendTabPayload(listOf(testTab1), "", "")),
+ ),
+ )
+ assertTrue(constellation.pollForCommands())
+
+ command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
+ assertEquals(testDevice2.into(), (command as DeviceCommandIncoming.TabReceived).from)
+ assertEquals(listOf(testTab1.into()), command.entries)
+
+ // Multiple tabs from a single device.
+ `when`(account.pollDeviceCommands()).thenReturn(
+ arrayOf(
+ IncomingDeviceCommand.TabReceived(testDevice2, SendTabPayload(listOf(testTab1, testTab3), "", "")),
+ ),
+ )
+ assertTrue(constellation.pollForCommands())
+
+ command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
+ assertEquals(testDevice2.into(), (command as DeviceCommandIncoming.TabReceived).from)
+ assertEquals(listOf(testTab1.into(), testTab3.into()), command.entries)
+
+ // Multiple tabs received from multiple devices.
+ `when`(account.pollDeviceCommands()).thenReturn(
+ arrayOf(
+ IncomingDeviceCommand.TabReceived(testDevice2, SendTabPayload(listOf(testTab1, testTab2), "", "")),
+ IncomingDeviceCommand.TabReceived(testDevice1, SendTabPayload(listOf(testTab3), "", "")),
+ ),
+ )
+ assertTrue(constellation.pollForCommands())
+
+ command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
+ assertEquals(testDevice2.into(), (command as DeviceCommandIncoming.TabReceived).from)
+ assertEquals(listOf(testTab1.into(), testTab2.into()), command.entries)
+ command = (eventsObserver.latestEvents!![1] as AccountEvent.DeviceCommandIncoming).command
+ assertEquals(testDevice1.into(), (command as DeviceCommandIncoming.TabReceived).from)
+ assertEquals(listOf(testTab3.into()), command.entries)
+
+ // TODO FirefoxAccount needs @Throws annotations for these tests to actually work.
+ // Failure to poll for commands. Panics are re-thrown.
+// `when`(account.pollDeviceCommands()).thenThrow(FxaPanicException("Don't panic!"))
+// try {
+// runBlocking(coroutinesTestRule.testDispatcher) {
+// constellation.refreshAsync()
+// }
+// fail()
+// } catch (e: FxaPanicException) {}
+//
+// // Network exception are handled.
+// `when`(account.pollDeviceCommands()).thenThrow(FxaNetworkException("four oh four"))
+// runBlocking(coroutinesTestRule.testDispatcher) {
+// Assert.assertFalse(constellation.refreshAsync())
+// }
+// // Unspecified exception are handled.
+// `when`(account.pollDeviceCommands()).thenThrow(FxaUnspecifiedException("hmmm..."))
+// runBlocking(coroutinesTestRule.testDispatcher) {
+// Assert.assertFalse(constellation.refreshAsync())
+// }
+// // Unauthorized exception are handled.
+// val authErrorObserver = object : AuthErrorObserver {
+// var latestException: AuthException? = null
+//
+// override fun onAuthErrorAsync(e: AuthException): Deferred<Unit> {
+// latestException = e
+// val r = CompletableDeferred<Unit>()
+// r.complete(Unit)
+// return r
+// }
+// }
+// authErrorRegistry.register(authErrorObserver)
+//
+// val authException = FxaUnauthorizedException("oh no you didn't!")
+// `when`(account.pollDeviceCommands()).thenThrow(authException)
+// runBlocking(coroutinesTestRule.testDispatcher) {
+// Assert.assertFalse(constellation.refreshAsync())
+// }
+// assertEquals(authErrorObserver.latestException!!.cause, authException)
+ }
+
+ private fun testDevice(id: String, current: Boolean, expired: Boolean = false, subscribed: Boolean = true): NativeDevice {
+ return NativeDevice(
+ id = id,
+ displayName = "testName",
+ deviceType = RustDeviceType.MOBILE,
+ isCurrentDevice = current,
+ lastAccessTime = 123L,
+ capabilities = listOf(),
+ pushEndpointExpired = expired,
+ pushSubscription = if (subscribed) NativeDevicePushSubscription("http://endpoint.com", "pk", "auth key") else null,
+ )
+ }
+
+ private fun startedLifecycleOwner(): LifecycleOwner {
+ val lifecycleOwner = mock<LifecycleOwner>()
+ val lifecycle = mock<Lifecycle>()
+ `when`(lifecycle.currentState).thenReturn(Lifecycle.State.STARTED)
+ `when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
+ return lifecycleOwner
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceSettingsCacheTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceSettingsCacheTest.kt
new file mode 100644
index 0000000000..5bde0e0281
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceSettingsCacheTest.kt
@@ -0,0 +1,69 @@
+/* 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.service.fxa
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.appservices.sync15.DeviceType
+import mozilla.appservices.syncmanager.DeviceSettings
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.lang.IllegalStateException
+
+@RunWith(AndroidJUnit4::class)
+class FxaDeviceSettingsCacheTest {
+ @Test
+ fun `fxa device settings cache basics`() {
+ val cache = FxaDeviceSettingsCache(testContext)
+ assertNull(cache.getCached())
+
+ try {
+ cache.updateCachedName("new name")
+ fail()
+ } catch (e: IllegalStateException) {}
+
+ cache.setToCache(DeviceSettings("some id", "some name", DeviceType.VR))
+ assertEquals(
+ DeviceSettings("some id", "some name", DeviceType.VR),
+ cache.getCached(),
+ )
+
+ cache.updateCachedName("new name")
+ assertEquals(
+ DeviceSettings("some id", "new name", DeviceType.VR),
+ cache.getCached(),
+ )
+
+ cache.clear()
+ assertNull(cache.getCached())
+
+ cache.setToCache(DeviceSettings("some id", "mobile", DeviceType.MOBILE))
+ assertEquals(
+ DeviceSettings("some id", "mobile", DeviceType.MOBILE),
+ cache.getCached(),
+ )
+
+ cache.setToCache(DeviceSettings("some id", "some tv", DeviceType.TV))
+ assertEquals(
+ DeviceSettings("some id", "some tv", DeviceType.TV),
+ cache.getCached(),
+ )
+
+ cache.setToCache(DeviceSettings("some id", "some tablet", DeviceType.TABLET))
+ assertEquals(
+ DeviceSettings("some id", "some tablet", DeviceType.TABLET),
+ cache.getCached(),
+ )
+
+ cache.setToCache(DeviceSettings("some id", "some desktop", DeviceType.DESKTOP))
+ assertEquals(
+ DeviceSettings("some id", "some desktop", DeviceType.DESKTOP),
+ cache.getCached(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/SyncAuthInfoCacheTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/SyncAuthInfoCacheTest.kt
new file mode 100644
index 0000000000..cb3d1a27a7
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/SyncAuthInfoCacheTest.kt
@@ -0,0 +1,51 @@
+/* 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.service.fxa
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.sync.SyncAuthInfo
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SyncAuthInfoCacheTest {
+ @Test
+ fun `public api`() {
+ val cache = SyncAuthInfoCache(testContext)
+ assertNull(cache.getCached())
+
+ val authInfo = SyncAuthInfo(
+ kid = "testKid",
+ fxaAccessToken = "fxaAccess",
+ // expires in the future (in seconds)
+ fxaAccessTokenExpiresAt = (System.currentTimeMillis() / 1000L) + 60,
+ syncKey = "long secret key",
+ tokenServerUrl = "http://www.token.server/url",
+ )
+
+ cache.setToCache(authInfo)
+
+ assertEquals(authInfo, cache.getCached())
+ assertFalse(cache.expired())
+
+ val authInfo2 = authInfo.copy(
+ // expires in the past (in seconds)
+ fxaAccessTokenExpiresAt = (System.currentTimeMillis() / 1000L) - 60,
+ )
+
+ cache.setToCache(authInfo2)
+ assertEquals(authInfo2, cache.getCached())
+ assertTrue(cache.expired())
+
+ cache.clear()
+
+ assertNull(cache.getCached())
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/UtilsKtTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/UtilsKtTest.kt
new file mode 100644
index 0000000000..a3515c1299
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/UtilsKtTest.kt
@@ -0,0 +1,391 @@
+/* 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.service.fxa
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.sync.ServiceResult
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.GlobalAccountManager
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+
+@ExperimentalCoroutinesApi // for runTest
+class UtilsKtTest {
+ @Test
+ fun `handleFxaExceptions form 1 returns correct data back`() = runTest {
+ assertEquals(
+ 1,
+ handleFxaExceptions(
+ mock(),
+ "test op",
+ {
+ 1
+ },
+ { fail() },
+ { fail() },
+ ),
+ )
+
+ assertEquals(
+ "Hello",
+ handleFxaExceptions(
+ mock(),
+ "test op",
+ {
+ "Hello"
+ },
+ { fail() },
+ { fail() },
+ ),
+ )
+ }
+
+ @Test
+ fun `handleFxaExceptions form 1 does not swallow non-panics`() = runTest {
+ val accountManager: FxaAccountManager = mock()
+ GlobalAccountManager.setInstance(accountManager)
+
+ // Network.
+ assertEquals(
+ "pass!",
+ handleFxaExceptions(
+ mock(),
+ "test op",
+ {
+ throw FxaNetworkException("oops")
+ },
+ { "fail" },
+ { error ->
+ assertEquals("oops", error.message)
+ assertTrue(error is FxaNetworkException)
+ "pass!"
+ },
+ ),
+ )
+
+ verifyNoInteractions(accountManager)
+
+ assertEquals(
+ "pass!",
+ handleFxaExceptions(
+ mock(),
+ "test op",
+ {
+ throw FxaUnauthorizedException("auth!")
+ },
+ {
+ "pass!"
+ },
+ {
+ fail()
+ },
+ ),
+ )
+
+ verify(accountManager).encounteredAuthError(eq("test op"), anyInt())
+
+ reset(accountManager)
+ assertEquals(
+ "pass!",
+ handleFxaExceptions(
+ mock(),
+ "test op",
+ {
+ throw FxaUnspecifiedException("dunno")
+ },
+ { "fail" },
+ { error ->
+ assertEquals("dunno", error.message)
+ assertTrue(error is FxaUnspecifiedException)
+ "pass!"
+ },
+ ),
+ )
+ verifyNoInteractions(accountManager)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `handleFxaExceptions form 1 re-throws non-fxa exceptions`() = runTest {
+ handleFxaExceptions(
+ mock(),
+ "test op",
+ {
+ throw IllegalStateException("bad state")
+ },
+ { fail() },
+ { fail() },
+ )
+ }
+
+ @Test(expected = FxaPanicException::class)
+ fun `handleFxaExceptions form 1 re-throws fxa panic exceptions`() = runTest {
+ handleFxaExceptions(
+ mock(),
+ "test op",
+ {
+ throw FxaPanicException("don't panic!")
+ },
+ { fail() },
+ { fail() },
+ )
+ }
+
+ @Test
+ fun `handleFxaExceptions form 2 works`() = runTest {
+ val accountManager: FxaAccountManager = mock()
+ GlobalAccountManager.setInstance(accountManager)
+
+ assertTrue(
+ handleFxaExceptions(mock(), "test op") {
+ Unit
+ },
+ )
+
+ assertFalse(
+ handleFxaExceptions(mock(), "test op") {
+ throw FxaUnspecifiedException("dunno")
+ },
+ )
+
+ verifyNoInteractions(accountManager)
+
+ assertFalse(
+ handleFxaExceptions(mock(), "test op") {
+ throw FxaUnauthorizedException("401")
+ },
+ )
+
+ verify(accountManager).encounteredAuthError("test op")
+
+ reset(accountManager)
+
+ assertFalse(
+ handleFxaExceptions(mock(), "test op") {
+ throw FxaNetworkException("dunno")
+ },
+ )
+
+ verifyNoInteractions(accountManager)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `handleFxaExceptions form 2 re-throws non-fxa exceptions`() = runTest {
+ val accountManager: FxaAccountManager = mock()
+ GlobalAccountManager.setInstance(accountManager)
+
+ handleFxaExceptions(mock(), "test op") {
+ throw IllegalStateException("bad state")
+ }
+ verifyNoInteractions(accountManager)
+ }
+
+ @Test(expected = FxaPanicException::class)
+ fun `handleFxaExceptions form 2 re-throws fxa panic exceptions`() = runTest {
+ val accountManager: FxaAccountManager = mock()
+ GlobalAccountManager.setInstance(accountManager)
+
+ handleFxaExceptions(mock(), "test op") {
+ throw FxaPanicException("dunno")
+ }
+
+ verifyNoInteractions(accountManager)
+ }
+
+ @Test
+ fun `handleFxaExceptions form 3 works`() = runTest {
+ val accountManager: FxaAccountManager = mock()
+ GlobalAccountManager.setInstance(accountManager)
+
+ assertEquals(
+ 1,
+ handleFxaExceptions(mock(), "test op", { 2 }) {
+ 1
+ },
+ )
+
+ assertEquals(
+ 0,
+ handleFxaExceptions(mock(), "test op", { 0 }) {
+ throw FxaUnspecifiedException("dunno")
+ },
+ )
+
+ verifyNoInteractions(accountManager)
+
+ assertEquals(
+ -1,
+ handleFxaExceptions(mock(), "test op", { -1 }) {
+ throw FxaUnauthorizedException("401")
+ },
+ )
+
+ verify(accountManager).encounteredAuthError(eq("test op"), anyInt())
+
+ reset(accountManager)
+
+ assertEquals(
+ "bad",
+ handleFxaExceptions(mock(), "test op", { "bad" }) {
+ throw FxaNetworkException("dunno")
+ },
+ )
+
+ verifyNoInteractions(accountManager)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `handleFxaExceptions form 3 re-throws non-fxa exceptions`() = runTest {
+ val accountManager: FxaAccountManager = mock()
+ GlobalAccountManager.setInstance(accountManager)
+
+ handleFxaExceptions(mock(), "test op", { "nope" }) {
+ throw IllegalStateException("bad state")
+ }
+ verifyNoInteractions(accountManager)
+ }
+
+ @Test(expected = FxaPanicException::class)
+ fun `handleFxaExceptions form 3 re-throws fxa panic exceptions`() = runTest {
+ val accountManager: FxaAccountManager = mock()
+ GlobalAccountManager.setInstance(accountManager)
+
+ handleFxaExceptions(mock(), "test op", { "nope" }) {
+ throw FxaPanicException("dunno")
+ }
+ verifyNoInteractions(accountManager)
+ }
+
+ @Test
+ fun `withRetries immediate success`() = runTest {
+ when (val res = withRetries(mock(), 3) { true }) {
+ is Result.Success -> assertTrue(res.value)
+ is Result.Failure -> fail()
+ }
+ when (val res = withRetries(mock(), 3) { "hello!" }) {
+ is Result.Success -> assertEquals("hello!", res.value)
+ is Result.Failure -> fail()
+ }
+ val eventual = SucceedOn(2, 42)
+ when (val res = withRetries(mock(), 3) { eventual.nullFailure() }) {
+ is Result.Success -> assertEquals(42, res.value)
+ is Result.Failure -> fail()
+ }
+ }
+
+ @Test
+ fun `withRetries immediate failure`() = runTest {
+ when (withRetries(mock(), 3) { false }) {
+ is Result.Success -> fail()
+ is Result.Failure -> {}
+ }
+ when (withRetries(mock(), 3) { null }) {
+ is Result.Success -> fail()
+ is Result.Failure -> {}
+ }
+ }
+
+ @Test
+ fun `withRetries eventual success`() = runTest {
+ val eventual = SucceedOn(2, true)
+ when (val res = withRetries(mock(), 5) { eventual.nullFailure() }) {
+ is Result.Success -> {
+ assertTrue(res.value!!)
+ assertEquals(2, eventual.attempts)
+ }
+ is Result.Failure -> fail()
+ }
+ val eventual2 = SucceedOn(2, "world!")
+ when (val res = withRetries(mock(), 3) { eventual2.nullFailure() }) {
+ is Result.Success -> {
+ assertEquals("world!", res.value)
+ assertEquals(2, eventual2.attempts)
+ }
+ is Result.Failure -> fail()
+ }
+ }
+
+ @Test
+ fun `withRetries eventual failure`() = runTest {
+ val eventual = SucceedOn(6, true)
+ when (withRetries(mock(), 5) { eventual.nullFailure() }) {
+ is Result.Success -> fail()
+ is Result.Failure -> {
+ assertEquals(5, eventual.attempts)
+ }
+ }
+ val eventual2 = SucceedOn(15, "hello!")
+ when (withRetries(mock(), 3) { eventual2.nullFailure() }) {
+ is Result.Success -> fail()
+ is Result.Failure -> {
+ assertEquals(3, eventual2.attempts)
+ }
+ }
+ }
+
+ @Test
+ fun `withServiceRetries immediate success`() = runTest {
+ when (withServiceRetries(mock(), 3, suspend { ServiceResult.Ok })) {
+ is ServiceResult.Ok -> {}
+ else -> fail()
+ }
+ }
+
+ @Test
+ fun `withServiceRetries generic failure keeps retrying`() = runTest {
+ // keeps retrying on generic error
+ val eventual = SucceedOn(0, ServiceResult.Ok, ServiceResult.OtherError)
+ when (withServiceRetries(mock(), 3) { eventual.reifiedFailure() }) {
+ is ServiceResult.Ok -> fail()
+ else -> {
+ assertEquals(3, eventual.attempts)
+ }
+ }
+ }
+
+ @Test
+ fun `withServiceRetries auth failure short circuit`() = runTest {
+ // keeps retrying on generic error
+ val eventual = SucceedOn(0, ServiceResult.Ok, ServiceResult.AuthError)
+ when (withServiceRetries(mock(), 3) { eventual.reifiedFailure() }) {
+ is ServiceResult.Ok -> fail()
+ else -> {
+ assertEquals(1, eventual.attempts)
+ }
+ }
+ }
+
+ @Test
+ fun `withServiceRetries eventual success`() = runTest {
+ val eventual = SucceedOn(3, ServiceResult.Ok, ServiceResult.OtherError)
+ when (withServiceRetries(mock(), 5) { eventual.reifiedFailure() }) {
+ is ServiceResult.Ok -> {
+ assertEquals(3, eventual.attempts)
+ }
+ else -> fail()
+ }
+ }
+
+ private class SucceedOn<S>(private val successOn: Int, private val succeedWith: S, private val failWith: S? = null) {
+ var attempts = 0
+ fun nullFailure(): S? {
+ attempts += 1
+ return when {
+ successOn == 0 || attempts < successOn -> failWith
+ else -> succeedWith!!
+ }
+ }
+ fun reifiedFailure(): S = nullFailure()!!
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/GlobalAccountManagerTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/GlobalAccountManagerTest.kt
new file mode 100644
index 0000000000..bd56bd33a8
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/GlobalAccountManagerTest.kt
@@ -0,0 +1,55 @@
+/* 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.service.fxa.manager
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito
+
+class GlobalAccountManagerTest {
+ @ExperimentalCoroutinesApi
+ @Test
+ fun `GlobalAccountManager authError processing`() = runTest {
+ val manager: FxaAccountManager = mock()
+ GlobalAccountManager.setInstance(manager)
+
+ val testClock: GlobalAccountManager.Clock = mock()
+ Mockito.`when`(testClock.getTimeCheckPoint()).thenReturn(1L)
+
+ GlobalAccountManager.authError("hello", forSync = true, clock = testClock)
+ Mockito.verify(manager).encounteredAuthError("hello", 1)
+
+ // another error within a second, count goes up
+ Mockito.`when`(testClock.getTimeCheckPoint()).thenReturn(1000L)
+ GlobalAccountManager.authError("fxa oops", forSync = true, clock = testClock)
+ Mockito.verify(manager).encounteredAuthError("fxa oops", 2)
+
+ // But non-sync operations don't cause another recovery
+ Mockito.clearInvocations(manager)
+ GlobalAccountManager.authError("fxa oops", forSync = false, clock = testClock)
+ Mockito.verifyNoInteractions(manager)
+
+ // error five minutes later, count is reset
+ Mockito.`when`(testClock.getTimeCheckPoint()).thenReturn(1000L * 60 * 5)
+ GlobalAccountManager.authError("fxa oops 2", forSync = true, clock = testClock)
+ Mockito.verify(manager).encounteredAuthError("fxa oops 2", 1)
+
+ // the count is ramped up if auth errors become frequent again
+ Mockito.`when`(testClock.getTimeCheckPoint()).thenReturn(1000L * 60 * 5 + 1000L)
+ GlobalAccountManager.authError("fxa oops 2", forSync = true, clock = testClock)
+ Mockito.verify(manager).encounteredAuthError("fxa oops 2", 2)
+
+ Mockito.`when`(testClock.getTimeCheckPoint()).thenReturn(1000L * 60 * 5 + 2000L)
+ GlobalAccountManager.authError("fxa oops 2", forSync = true, clock = testClock)
+ Mockito.verify(manager).encounteredAuthError("fxa oops 2", 3)
+
+ // ... and is reset again as errors slow down.
+ Mockito.`when`(testClock.getTimeCheckPoint()).thenReturn(1000L * 60 * 5 + 1000L * 60 * 15)
+ GlobalAccountManager.authError("profile fetch", forSync = true, clock = testClock)
+ Mockito.verify(manager).encounteredAuthError("profile fetch", 1)
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt
new file mode 100644
index 0000000000..6323861f0e
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt
@@ -0,0 +1,134 @@
+/* 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.service.fxa.manager
+
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class StateKtTest {
+ private fun assertNextStateForStateEventPair(state: State, event: Event, nextState: State?) {
+ val expectedNextState = when (state) {
+ is State.Idle -> when (state.accountState) {
+ AccountState.NotAuthenticated -> when (event) {
+ Event.Account.Start -> State.Active(ProgressState.Initializing)
+ is Event.Account.BeginEmailFlow -> State.Active(ProgressState.BeginningAuthentication)
+ is Event.Account.BeginPairingFlow -> State.Active(ProgressState.BeginningAuthentication)
+ else -> null
+ }
+ is AccountState.Authenticating -> when (event) {
+ Event.Progress.CancelAuth -> State.Idle(AccountState.NotAuthenticated)
+ is Event.Progress.AuthData -> State.Active(ProgressState.CompletingAuthentication)
+ else -> null
+ }
+ AccountState.Authenticated -> when (event) {
+ is Event.Account.AuthenticationError -> State.Active(ProgressState.RecoveringFromAuthProblem)
+ Event.Account.AccessTokenKeyError -> State.Idle(AccountState.AuthenticationProblem)
+ Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
+ else -> null
+ }
+ AccountState.AuthenticationProblem -> when (event) {
+ is Event.Account.BeginEmailFlow -> State.Active(ProgressState.BeginningAuthentication)
+ Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
+ else -> null
+ }
+ }
+ is State.Active -> when (state.progressState) {
+ ProgressState.Initializing -> when (event) {
+ Event.Progress.AccountNotFound -> State.Idle(AccountState.NotAuthenticated)
+ Event.Progress.AccountRestored -> State.Active(ProgressState.CompletingAuthentication)
+ else -> null
+ }
+ ProgressState.BeginningAuthentication -> when (event) {
+ Event.Progress.FailedToBeginAuth -> State.Idle(AccountState.NotAuthenticated)
+ is Event.Progress.StartedOAuthFlow -> State.Idle(AccountState.Authenticating(event.oAuthUrl))
+ else -> null
+ }
+ ProgressState.CompletingAuthentication -> when (event) {
+ Event.Progress.FailedToCompleteAuth -> State.Idle(AccountState.NotAuthenticated)
+ Event.Progress.FailedToCompleteAuthRestore -> State.Idle(AccountState.NotAuthenticated)
+ is Event.Progress.CompletedAuthentication -> State.Idle(AccountState.Authenticated)
+ else -> null
+ }
+ ProgressState.RecoveringFromAuthProblem -> when (event) {
+ Event.Progress.RecoveredFromAuthenticationProblem -> State.Idle(AccountState.Authenticated)
+ Event.Progress.FailedToRecoverFromAuthenticationProblem -> State.Idle(AccountState.AuthenticationProblem)
+ else -> null
+ }
+ ProgressState.LoggingOut -> when (event) {
+ Event.Progress.LoggedOut -> State.Idle(AccountState.NotAuthenticated)
+ else -> null
+ }
+ }
+ }
+
+ assertEquals(expectedNextState, nextState)
+ }
+
+ private fun instantiateAccountState(simpleName: String): AccountState {
+ return when (simpleName) {
+ "NotAuthenticated" -> AccountState.NotAuthenticated
+ "Authenticating" -> AccountState.Authenticating("https://example.com/oauth-start")
+ "Authenticated" -> AccountState.Authenticated
+ "AuthenticationProblem" -> AccountState.AuthenticationProblem
+ else -> {
+ throw AssertionError("Unknown AccountState: $simpleName")
+ }
+ }
+ }
+
+ private fun instantiateEvent(eventClassSimpleName: String): Event {
+ return when (eventClassSimpleName) {
+ "Start" -> Event.Account.Start
+ "BeginPairingFlow" -> Event.Account.BeginPairingFlow("http://some.pairing.url.com", mock())
+ "BeginEmailFlow" -> Event.Account.BeginEmailFlow(mock())
+ "CancelAuth" -> Event.Progress.CancelAuth
+ "StartedOAuthFlow" -> Event.Progress.StartedOAuthFlow("https://example.com/oauth-start")
+ "AuthenticationError" -> Event.Account.AuthenticationError("fxa op")
+ "AccessTokenKeyError" -> Event.Account.AccessTokenKeyError
+ "Logout" -> Event.Account.Logout
+ "AccountNotFound" -> Event.Progress.AccountNotFound
+ "AccountRestored" -> Event.Progress.AccountRestored
+ "AuthData" -> Event.Progress.AuthData(mock())
+ "LoggedOut" -> Event.Progress.LoggedOut
+ "FailedToRecoverFromAuthenticationProblem" -> Event.Progress.FailedToRecoverFromAuthenticationProblem
+ "RecoveredFromAuthenticationProblem" -> Event.Progress.RecoveredFromAuthenticationProblem
+ "CompletedAuthentication" -> Event.Progress.CompletedAuthentication(mock<AuthType.Existing>())
+ "FailedToBeginAuth" -> Event.Progress.FailedToBeginAuth
+ "FailedToCompleteAuth" -> Event.Progress.FailedToCompleteAuth
+ "FailedToCompleteAuthRestore" -> Event.Progress.FailedToCompleteAuthRestore
+ else -> {
+ throw AssertionError("Unknown event: $eventClassSimpleName")
+ }
+ }
+ }
+
+ @Test
+ fun `state transition matrix`() {
+ // We want to test every combination of state/event. Do that by iterating over entire sets.
+ ProgressState.values().forEach { state ->
+ Event.Progress::class.sealedSubclasses.map { instantiateEvent(it.simpleName!!) }.forEach {
+ val ss = State.Active(state)
+ assertNextStateForStateEventPair(
+ ss,
+ it,
+ ss.next(it),
+ )
+ }
+ }
+
+ AccountState::class.sealedSubclasses.map { instantiateAccountState(it.simpleName!!) }.forEach { state ->
+ Event.Account::class.sealedSubclasses.map { instantiateEvent(it.simpleName!!) }.forEach {
+ val ss = State.Idle(state)
+ assertNextStateForStateEventPair(
+ ss,
+ it,
+ ss.next(it),
+ )
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/SyncEnginesStorageTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/SyncEnginesStorageTest.kt
new file mode 100644
index 0000000000..e94b41769b
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/SyncEnginesStorageTest.kt
@@ -0,0 +1,47 @@
+/* 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.service.fxa.manager
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SyncEnginesStorageTest {
+ @Test
+ fun `sync engine storage basics`() {
+ val store = SyncEnginesStorage(testContext)
+ assertEquals(emptyMap<SyncEngine, Boolean>(), store.getStatus())
+
+ store.setStatus(SyncEngine.Bookmarks, false)
+ assertEquals(mapOf(SyncEngine.Bookmarks to false), store.getStatus())
+
+ store.setStatus(SyncEngine.Bookmarks, true)
+ assertEquals(mapOf(SyncEngine.Bookmarks to true), store.getStatus())
+
+ store.setStatus(SyncEngine.Passwords, false)
+ assertEquals(mapOf(SyncEngine.Bookmarks to true, SyncEngine.Passwords to false), store.getStatus())
+
+ store.setStatus(SyncEngine.Bookmarks, false)
+ assertEquals(mapOf(SyncEngine.Bookmarks to false, SyncEngine.Passwords to false), store.getStatus())
+
+ store.setStatus(SyncEngine.Other("test"), true)
+ assertEquals(
+ mapOf(
+ SyncEngine.Bookmarks to false,
+ SyncEngine.Passwords to false,
+ SyncEngine.Other("test") to true,
+ ),
+ store.getStatus(),
+ )
+
+ store.clear()
+ assertTrue(store.getStatus().isNullOrEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/ext/FxaAccountManagerKtTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/ext/FxaAccountManagerKtTest.kt
new file mode 100644
index 0000000000..502f32a6db
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/ext/FxaAccountManagerKtTest.kt
@@ -0,0 +1,36 @@
+/* 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.service.fxa.manager.ext
+
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class FxaAccountManagerKtTest {
+
+ @Test
+ fun `block is executed only account is available`() {
+ val accountManager: FxaAccountManager = mock()
+ val block: DeviceConstellation.() -> Unit = mock()
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+
+ accountManager.withConstellation(block)
+
+ verify(block, never()).invoke(constellation)
+
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+
+ accountManager.withConstellation(block)
+
+ verify(block).invoke(constellation)
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
new file mode 100644
index 0000000000..e0d90118fa
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
@@ -0,0 +1,190 @@
+/* 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.service.fxa.store
+
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.Avatar
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.coMock
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+import java.lang.Exception
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class SyncStoreSupportTest {
+
+ private val accountManager = mock<FxaAccountManager>()
+ private val lifecycleOwner = mock<LifecycleOwner>()
+ private val autoPause = false
+ private val coroutineScope = TestScope()
+
+ private lateinit var store: SyncStore
+ private lateinit var syncObserver: AccountSyncObserver
+ private lateinit var constellationObserver: ConstellationObserver
+ private lateinit var accountObserver: FxaAccountObserver
+ private lateinit var integration: SyncStoreSupport
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(StandardTestDispatcher(coroutineScope.testScheduler))
+
+ store = SyncStore()
+ syncObserver = AccountSyncObserver(store)
+ constellationObserver = ConstellationObserver(store)
+ accountObserver = FxaAccountObserver(
+ store = store,
+ deviceConstellationObserver = constellationObserver,
+ lifecycleOwner = lifecycleOwner,
+ autoPause = autoPause,
+ coroutineScope = coroutineScope,
+ )
+
+ integration = SyncStoreSupport(
+ store = store,
+ fxaAccountManager = lazyOf(accountManager),
+ lifecycleOwner = lifecycleOwner,
+ autoPause = autoPause,
+ coroutineScope = coroutineScope,
+ )
+ }
+
+ @Test
+ fun `GIVEN integration WHEN initialize is called THEN observers registered`() {
+ integration.initialize()
+
+ verify(accountManager).registerForSyncEvents(any(), eq(lifecycleOwner), eq(autoPause))
+ verify(accountManager).register(any(), eq(lifecycleOwner), eq(autoPause))
+ }
+
+ @Test
+ fun `GIVEN sync observer WHEN onStarted observed THEN sync status updated`() {
+ syncObserver.onStarted()
+
+ store.waitUntilIdle()
+ assertEquals(SyncStatus.Started, store.state.status)
+ }
+
+ @Test
+ fun `GIVEN sync observer WHEN onIdle observed THEN sync status updated`() {
+ syncObserver.onIdle()
+
+ store.waitUntilIdle()
+ assertEquals(SyncStatus.Idle, store.state.status)
+ }
+
+ @Test
+ fun `GIVEN sync observer WHEN onError observed THEN sync status updated`() {
+ syncObserver.onError(Exception())
+
+ store.waitUntilIdle()
+ assertEquals(SyncStatus.Error, store.state.status)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onAuthenticated observed THEN device observer registered`() = runTest {
+ val constellation = mock<DeviceConstellation>()
+ val account = mock<OAuthAccount> {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ }
+
+ accountObserver.onAuthenticated(account, mock<AuthType.Existing>())
+ runCurrent()
+
+ verify(constellation).registerDeviceObserver(constellationObserver, lifecycleOwner, autoPause)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onAuthenticated observed with profile THEN account state updated`() = coroutineScope.runTest {
+ val profile = generateProfile()
+ val constellation = mock<DeviceConstellation>()
+ val account = coMock<OAuthAccount> {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ whenever(getCurrentDeviceId()).thenReturn("id")
+ whenever(getSessionToken()).thenReturn("token")
+ whenever(getProfile()).thenReturn(profile)
+ }
+
+ accountObserver.onAuthenticated(account, mock<AuthType.Existing>())
+ runCurrent()
+
+ val expected = Account(
+ profile.uid,
+ profile.email,
+ profile.avatar,
+ profile.displayName,
+ "id",
+ "token",
+ )
+ store.waitUntilIdle()
+ assertEquals(expected, store.state.account)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onAuthenticated observed without profile THEN account not updated`() = coroutineScope.runTest {
+ val constellation = mock<DeviceConstellation>()
+ val account = coMock<OAuthAccount> {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ whenever(getProfile()).thenReturn(null)
+ }
+
+ accountObserver.onAuthenticated(account, mock<AuthType.Existing>())
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(null, store.state.account)
+ }
+
+ @Test
+ fun `GIVEN user is logged in WHEN onLoggedOut observed THEN sync status and account updated`() = coroutineScope.runTest {
+ val account = coMock<OAuthAccount> {
+ whenever(deviceConstellation()).thenReturn(mock())
+ whenever(getProfile()).thenReturn(null)
+ }
+ accountObserver.onAuthenticated(account, mock<AuthType.Existing>())
+ runCurrent()
+
+ accountObserver.onLoggedOut()
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(SyncStatus.LoggedOut, store.state.status)
+ assertEquals(null, store.state.account)
+ }
+
+ @Test
+ fun `GIVEN device observer WHEN onDevicesUpdate observed THEN constellation state updated`() {
+ val constellation = mock<ConstellationState>()
+ constellationObserver.onDevicesUpdate(constellation)
+
+ store.waitUntilIdle()
+ assertEquals(constellation, store.state.constellationState)
+ }
+
+ private fun generateProfile(
+ uid: String = "uid",
+ email: String = "email",
+ avatar: Avatar = Avatar("url", true),
+ displayName: String = "displayName",
+ ) = Profile(uid, email, avatar, displayName)
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/TypesTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/TypesTest.kt
new file mode 100644
index 0000000000..16f7839759
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/TypesTest.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.service.fxa.sync
+
+import mozilla.components.service.fxa.SyncEngine
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+class TypesTest {
+
+ @Test
+ fun `raw strings are correctly mapped to SyncEngine types`() {
+ assertEquals(SyncEngine.Tabs, "tabs".toSyncEngine())
+ assertEquals(SyncEngine.History, "history".toSyncEngine())
+ assertEquals(SyncEngine.Bookmarks, "bookmarks".toSyncEngine())
+ assertEquals(SyncEngine.Passwords, "passwords".toSyncEngine())
+ assertEquals(SyncEngine.CreditCards, "creditcards".toSyncEngine())
+ assertEquals(SyncEngine.Addresses, "addresses".toSyncEngine())
+ assertEquals(SyncEngine.Other("other"), "other".toSyncEngine())
+ }
+
+ @Test
+ fun `a list of raw strings are correctly mapped to a set of SyncEngine engines`() {
+ assertEquals(
+ setOf(SyncEngine.History),
+ listOf("history").toSyncEngines(),
+ )
+
+ assertEquals(
+ setOf(SyncEngine.Bookmarks, SyncEngine.History),
+ listOf("history", "bookmarks").toSyncEngines(),
+ )
+
+ assertEquals(
+ setOf(SyncEngine.History, SyncEngine.CreditCards),
+ listOf("history", "creditcards").toSyncEngines(),
+ )
+
+ assertEquals(
+ setOf(SyncEngine.Other("other"), SyncEngine.CreditCards),
+ listOf("other", "creditcards").toSyncEngines(),
+ )
+
+ assertEquals(
+ setOf(SyncEngine.Bookmarks, SyncEngine.History),
+ listOf("history", "bookmarks", "bookmarks", "history").toSyncEngines(),
+ )
+ }
+
+ @Test
+ fun `raw strings are correctly mapped to SyncReason types`() {
+ assertEquals(SyncReason.Startup, "startup".toSyncReason())
+ assertEquals(SyncReason.FirstSync, "first_sync".toSyncReason())
+ assertEquals(SyncReason.Scheduled, "scheduled".toSyncReason())
+ assertEquals(SyncReason.User, "user".toSyncReason())
+ assertEquals(SyncReason.EngineChange, "engine_change".toSyncReason())
+ }
+
+ @Test
+ fun `SyncReason types are correctly mapped to strings`() {
+ assertEquals("startup", SyncReason.Startup.asString())
+ assertEquals("first_sync", SyncReason.FirstSync.asString())
+ assertEquals("scheduled", SyncReason.Scheduled.asString())
+ assertEquals("user", SyncReason.User.asString())
+ assertEquals("engine_change", SyncReason.EngineChange.asString())
+ }
+
+ @Test
+ fun `invalid sync reason raw strings throw IllegalStateException when mapped`() {
+ assertThrows("Invalid SyncReason: some_reason", IllegalStateException::class.java) {
+ "some_reason".toSyncReason()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/WorkManagerSyncManagerTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/WorkManagerSyncManagerTest.kt
new file mode 100644
index 0000000000..77dd6dd633
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/WorkManagerSyncManagerTest.kt
@@ -0,0 +1,97 @@
+/* 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.service.fxa.sync
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.WorkerParameters
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import mozilla.components.service.fxa.sync.WorkManagerSyncWorker.Companion.SYNC_STAGGER_BUFFER_MS
+import mozilla.components.service.fxa.sync.WorkManagerSyncWorker.Companion.engineSyncTimestamp
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class WorkManagerSyncManagerTest {
+ private lateinit var mockParam: WorkerParameters
+ private lateinit var mockTags: Set<String>
+ private lateinit var mockTaskExecutor: TaskExecutor
+
+ @Before
+ fun setUp() {
+ mockParam = mock()
+ mockTags = mock()
+ mockTaskExecutor = mock()
+ `when`(mockParam.taskExecutor).thenReturn(mockTaskExecutor)
+ `when`(mockTaskExecutor.serialTaskExecutor).thenReturn(mock())
+ `when`(mockParam.tags).thenReturn(mockTags)
+ }
+
+ @Test
+ fun `sync state access`() {
+ assertNull(getSyncState(testContext))
+ assertEquals(0L, getLastSynced(testContext))
+
+ // 'clear' doesn't blow up for empty state
+ clearSyncState(testContext)
+ // ... and doesn't affect anything, either
+ assertNull(getSyncState(testContext))
+ assertEquals(0L, getLastSynced(testContext))
+
+ setSyncState(testContext, "some state")
+ assertEquals("some state", getSyncState(testContext))
+
+ setLastSynced(testContext, 123L)
+ assertEquals(123L, getLastSynced(testContext))
+
+ clearSyncState(testContext)
+ assertNull(getSyncState(testContext))
+ assertEquals(0L, getLastSynced(testContext))
+ }
+
+ @Test
+ fun `GIVEN work is not set to be debounced THEN it is not considered to be synced within the buffer`() {
+ `when`(mockTags.contains(SyncWorkerTag.Debounce.name)).thenReturn(false)
+
+ engineSyncTimestamp["test"] = System.currentTimeMillis() - SYNC_STAGGER_BUFFER_MS - 100L
+ engineSyncTimestamp["test2"] = System.currentTimeMillis()
+
+ val workerManagerSyncWorker = WorkManagerSyncWorker(testContext, mockParam)
+
+ assertFalse(workerManagerSyncWorker.isDebounced())
+ assertFalse(workerManagerSyncWorker.lastSyncedWithinStaggerBuffer("test"))
+ assertFalse(workerManagerSyncWorker.lastSyncedWithinStaggerBuffer("test2"))
+ }
+
+ @Test
+ fun `GIVEN work is set to be debounced THEN last synced timestamp is compared to buffer`() {
+ `when`(mockTags.contains(SyncWorkerTag.Debounce.name)).thenReturn(true)
+
+ engineSyncTimestamp["test"] = System.currentTimeMillis() - SYNC_STAGGER_BUFFER_MS - 100L
+ engineSyncTimestamp["test2"] = System.currentTimeMillis()
+
+ val workerManagerSyncWorker = WorkManagerSyncWorker(testContext, mockParam)
+
+ assert(workerManagerSyncWorker.isDebounced())
+ assertFalse(workerManagerSyncWorker.lastSyncedWithinStaggerBuffer("test"))
+ assert(workerManagerSyncWorker.lastSyncedWithinStaggerBuffer("test2"))
+ }
+
+ @Test
+ fun `GIVEN work is set to be debounced WHEN there is not a saved time stamp THEN work will not be debounced`() {
+ `when`(mockTags.contains(SyncWorkerTag.Debounce.name)).thenReturn(true)
+
+ val workerManagerSyncWorker = WorkManagerSyncWorker(testContext, mockParam)
+
+ assert(workerManagerSyncWorker.isDebounced())
+ assertFalse(workerManagerSyncWorker.lastSyncedWithinStaggerBuffer("test"))
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/firefox-accounts/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..1f0955d450
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/firefox-accounts/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/glean/README.md b/mobile/android/android-components/components/service/glean/README.md
new file mode 100644
index 0000000000..c08c0d17f1
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/README.md
@@ -0,0 +1,21 @@
+# [Android Components](../../../README.md) > Service > Glean
+
+A client-side telemetry SDK for collecting metrics and sending them to Mozilla's telemetry service.
+
+Visit the [complete Glean SDK documentation](https://mozilla.github.io/glean/).
+
+## Contact
+
+To contact us you can:
+- Find us in the [#glean channel on chat.mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org).
+* To report issues or request changes, file a bug in [Bugzilla in Data Platform & Tools :: Glean: SDK][newbugzilla].
+- Send an email to *glean-team@mozilla.com*.
+* The Glean Android team is: *:dexter*, *:travis*, *:mdroettboom*, *:janerik*, *:brizental*.
+
+## 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/
+
+[newbugzilla]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean%3A+SDK&priority=P3&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D
diff --git a/mobile/android/android-components/components/service/glean/build.gradle b/mobile/android/android-components/components/service/glean/build.gradle
new file mode 100644
index 0000000000..aaed4bac50
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/build.gradle
@@ -0,0 +1,58 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.service.glean'
+}
+
+// Define library names and version constants.
+String GLEAN_LIBRARY = "org.mozilla.telemetry:glean:${Versions.mozilla_glean}"
+String GLEAN_LIBRARY_FORUNITTESTS = "org.mozilla.telemetry:glean-native-forUnitTests:${Versions.mozilla_glean}"
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_work_runtime
+
+ api GLEAN_LIBRARY
+
+ // So consumers can set a HTTP client.
+ api project(':concept-fetch')
+
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':support-utils')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation ComponentsDependencies.androidx_work_testing
+
+ testImplementation project(':support-test')
+ testImplementation project(':lib-fetch-httpurlconnection')
+ testImplementation project(':lib-fetch-okhttp')
+
+ testImplementation GLEAN_LIBRARY_FORUNITTESTS
+}
+
+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/service/glean/gradle.properties b/mobile/android/android-components/components/service/glean/gradle.properties
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/gradle.properties
diff --git a/mobile/android/android-components/components/service/glean/proguard-rules.pro b/mobile/android/android-components/components/service/glean/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/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/service/glean/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/glean/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/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/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt
new file mode 100644
index 0000000000..232ac1b83f
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt
@@ -0,0 +1,145 @@
+/* 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.service.glean
+
+import android.content.Context
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.glean.config.Configuration
+import mozilla.components.service.glean.private.RecordedExperiment
+import org.json.JSONObject
+import mozilla.telemetry.glean.Glean as GleanCore
+
+typealias BuildInfo = mozilla.telemetry.glean.BuildInfo
+
+/**
+ * In contrast with other glean-ac classes (i.e. Configuration), we can't
+ * use typealias to export mozilla.telemetry.glean.Glean, as we need to provide
+ * a different default [Configuration]. Moreover, we can't simply delegate other
+ * methods or inherit, since that doesn't work for `object` in Kotlin.
+ */
+object Glean {
+ /**
+ * Initialize Glean.
+ *
+ * This should only be initialized once by the application, and not by
+ * libraries using Glean. A message is logged to error and no changes are made
+ * to the state if initialize is called a more than once.
+ *
+ * A LifecycleObserver will be added to send pings when the application goes
+ * into the background.
+ *
+ * @param applicationContext [Context] to access application features, such
+ * as shared preferences
+ * @param uploadEnabled A [Boolean] that determines the initial state of the uploader
+ * @param configuration A Glean [Configuration] object with global settings.
+ * @param buildInfo A Glean [BuildInfo] object with build-time metadata. This
+ * object is generated at build time by glean_parser at the import path
+ * ${YOUR_PACKAGE_ROOT}.GleanMetrics.GleanBuildInfo.buildInfo
+ */
+ @MainThread
+ fun initialize(
+ applicationContext: Context,
+ uploadEnabled: Boolean,
+ configuration: Configuration,
+ buildInfo: BuildInfo,
+ ) {
+ GleanCore.initialize(
+ applicationContext = applicationContext,
+ uploadEnabled = uploadEnabled,
+ configuration = configuration.toWrappedConfiguration(),
+ buildInfo = buildInfo,
+ )
+ }
+
+ /**
+ * Register the pings generated from `pings.yaml` with Glean.
+ *
+ * @param pings The `Pings` object generated for your library or application
+ * by Glean.
+ */
+ fun registerPings(pings: Any) {
+ GleanCore.registerPings(pings)
+ }
+
+ /**
+ * Enable or disable Glean collection and upload.
+ *
+ * Metric collection is enabled by default.
+ *
+ * When disabled, metrics aren't recorded at all and no data
+ * is uploaded.
+ *
+ * @param enabled When true, enable metric collection.
+ */
+ fun setUploadEnabled(enabled: Boolean) {
+ GleanCore.setUploadEnabled(enabled)
+ }
+
+ /**
+ * Indicate that an experiment is running. Glean will then add an
+ * experiment annotation to the environment which is sent with pings. This
+ * information is not persisted between runs.
+ *
+ * @param experimentId The id of the active experiment (maximum
+ * 30 bytes)
+ * @param branch The experiment branch (maximum 30 bytes)
+ * @param extra Optional metadata to output with the ping
+ */
+ @JvmOverloads
+ fun setExperimentActive(
+ experimentId: String,
+ branch: String,
+ extra: Map<String, String>? = null,
+ ) {
+ GleanCore.setExperimentActive(
+ experimentId = experimentId,
+ branch = branch,
+ extra = extra,
+ )
+ }
+
+ /**
+ * Indicate that an experiment is no longer running.
+ *
+ * @param experimentId The id of the experiment to deactivate.
+ */
+ fun setExperimentInactive(experimentId: String) {
+ GleanCore.setExperimentInactive(experimentId = experimentId)
+ }
+
+ /**
+ * Set configuration to override metrics' enabled state, typically from a remote_settings
+ * experiment or rollout.
+ *
+ * @param enabled Map of metrics' enabled state.
+ */
+ fun setMetricsEnabledConfig(enabled: Map<String, Boolean>) {
+ GleanCore.setMetricsEnabledConfig(JSONObject(enabled).toString())
+ }
+
+ /**
+ * Tests whether an experiment is active, for testing purposes only.
+ *
+ * @param experimentId the id of the experiment to look for.
+ * @return true if the experiment is active and reported in pings, otherwise false
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ fun testIsExperimentActive(experimentId: String): Boolean {
+ return GleanCore.testIsExperimentActive(experimentId)
+ }
+
+ /**
+ * Returns the stored data for the requested active experiment, for testing purposes only.
+ *
+ * @param experimentId the id of the experiment to look for.
+ * @return the [RecordedExperiment] for the experiment
+ * @throws [NullPointerException] if the requested experiment is not active or data is corrupt.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ fun testGetExperimentData(experimentId: String): RecordedExperiment {
+ return GleanCore.testGetExperimentData(experimentId)
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt
new file mode 100644
index 0000000000..11911c7046
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt
@@ -0,0 +1,50 @@
+/* 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.service.glean.config
+
+import mozilla.telemetry.glean.net.PingUploader
+import mozilla.telemetry.glean.config.Configuration as GleanCoreConfiguration
+
+/**
+ * The Configuration class describes how to configure the Glean.
+ *
+ * @property httpClient The HTTP client implementation to use for uploading pings.
+ * If you don't provide your own networking stack with an HTTP client to use,
+ * you can fall back to a simple implementation on top of `java.net` using
+ * `ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() as Client })`
+ * @property serverEndpoint (optional) the server pings are sent to. Please note that this is
+ * is only meant to be changed for tests.
+ * @property channel (optional )the release channel the application is on, if known. This will be
+ * sent along with all the pings, in the `client_info` section.
+ * @property maxEvents (optional) the number of events to store before the events ping is sent
+ */
+data class Configuration @JvmOverloads constructor(
+ val httpClient: PingUploader,
+ val serverEndpoint: String = DEFAULT_TELEMETRY_ENDPOINT,
+ val channel: String? = null,
+ val maxEvents: Int? = null,
+ val enableEventTimestamps: Boolean = false,
+) {
+ // The following is required to support calling our API from Java.
+ companion object {
+ const val DEFAULT_TELEMETRY_ENDPOINT = GleanCoreConfiguration.DEFAULT_TELEMETRY_ENDPOINT
+ }
+
+ /**
+ * Convert the Android Components configuration object to the Glean SDK
+ * configuration object.
+ *
+ * @return a [mozilla.telemetry.glean.config.Configuration] instance.
+ */
+ fun toWrappedConfiguration(): GleanCoreConfiguration {
+ return GleanCoreConfiguration(
+ serverEndpoint = serverEndpoint,
+ channel = channel,
+ maxEvents = maxEvents,
+ httpClient = httpClient,
+ enableEventTimestamps = enableEventTimestamps,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt
new file mode 100644
index 0000000000..06947e6d5b
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.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.service.glean.net
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Header
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.toMutableHeaders
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.telemetry.glean.net.HeadersList
+import mozilla.telemetry.glean.net.HttpStatus
+import mozilla.telemetry.glean.net.RecoverableFailure
+import mozilla.telemetry.glean.net.UploadResult
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+import mozilla.telemetry.glean.net.PingUploader as CorePingUploader
+
+typealias PingUploader = CorePingUploader
+
+/**
+ * A simple ping Uploader, which implements a "send once" policy, never
+ * storing or attempting to send the ping again. This uses Android Component's
+ * `concept-fetch`.
+ *
+ * @param usePrivateRequest Sets the [Request.private] flag in all requests using this uploader.
+ */
+class ConceptFetchHttpUploader(
+ internal val client: Lazy<Client>,
+ private val usePrivateRequest: Boolean = false,
+) : PingUploader {
+ private val logger = Logger("glean/ConceptFetchHttpUploader")
+
+ companion object {
+ // The timeout, in milliseconds, to use when connecting to the server.
+ const val DEFAULT_CONNECTION_TIMEOUT = 10000L
+
+ // The timeout, in milliseconds, to use when reading from the server.
+ const val DEFAULT_READ_TIMEOUT = 30000L
+
+ /**
+ * Export a constructor that is usable from Java.
+ *
+ * This looses the lazyness of creating the `client`.
+ */
+ @JvmStatic
+ fun fromClient(client: Client): ConceptFetchHttpUploader {
+ return ConceptFetchHttpUploader(lazy { client })
+ }
+ }
+
+ /**
+ * Synchronously upload a ping to a server.
+ *
+ * @param url the URL path to upload the data to
+ * @param data the serialized text data to send
+ * @param headers a [HeadersList] containing String to String [Pair] with
+ * the first entry being the header name and the second its value.
+ *
+ * @return true if the ping was correctly dealt with (sent successfully
+ * or faced an unrecoverable error), false if there was a recoverable
+ * error callers can deal with.
+ */
+ override fun upload(url: String, data: ByteArray, headers: HeadersList): UploadResult {
+ val request = buildRequest(url, data, headers)
+
+ return try {
+ performUpload(client.value, request)
+ } catch (e: IOException) {
+ logger.warn("IOException while uploading ping", e)
+ RecoverableFailure(0)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun buildRequest(
+ url: String,
+ data: ByteArray,
+ headers: HeadersList,
+ ): Request {
+ val conceptHeaders = headers.map { (name, value) -> Header(name, value) }.toMutableHeaders()
+
+ return Request(
+ url = url,
+ method = Request.Method.POST,
+ connectTimeout = Pair(DEFAULT_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS),
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT, TimeUnit.MILLISECONDS),
+ headers = conceptHeaders,
+ // Make sure we are not sending cookies. Unfortunately, HttpURLConnection doesn't
+ // offer a better API to do that, so we nuke all cookies going to our telemetry
+ // endpoint.
+ cookiePolicy = Request.CookiePolicy.OMIT,
+ body = Request.Body(data.inputStream()),
+ private = usePrivateRequest,
+ conservative = true,
+ )
+ }
+
+ @Throws(IOException::class)
+ internal fun performUpload(client: Client, request: Request): UploadResult {
+ logger.debug("Submitting ping to: ${request.url}")
+ client.fetch(request).use { response ->
+ return HttpStatus(response.status)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt
new file mode 100644
index 0000000000..e6e0be9424
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt
@@ -0,0 +1,119 @@
+/* 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.service.glean.private
+
+import androidx.annotation.VisibleForTesting
+
+typealias CommonMetricData = mozilla.telemetry.glean.private.CommonMetricData
+typealias EventExtras = mozilla.telemetry.glean.private.EventExtras
+typealias Lifetime = mozilla.telemetry.glean.private.Lifetime
+typealias NoExtras = mozilla.telemetry.glean.private.NoExtras
+typealias NoReasonCodes = mozilla.telemetry.glean.private.NoReasonCodes
+typealias ReasonCode = mozilla.telemetry.glean.private.ReasonCode
+
+typealias BooleanMetricType = mozilla.telemetry.glean.private.BooleanMetricType
+typealias CounterMetricType = mozilla.telemetry.glean.private.CounterMetricType
+typealias CustomDistributionMetricType = mozilla.telemetry.glean.private.CustomDistributionMetricType
+typealias DatetimeMetricType = mozilla.telemetry.glean.private.DatetimeMetricType
+typealias DenominatorMetricType = mozilla.telemetry.glean.private.DenominatorMetricType
+typealias HistogramMetricBase = mozilla.telemetry.glean.private.HistogramBase
+typealias HistogramType = mozilla.telemetry.glean.private.HistogramType
+typealias LabeledMetricType<T> = mozilla.telemetry.glean.private.LabeledMetricType<T>
+typealias MemoryDistributionMetricType = mozilla.telemetry.glean.private.MemoryDistributionMetricType
+typealias MemoryUnit = mozilla.telemetry.glean.private.MemoryUnit
+typealias NumeratorMetricType = mozilla.telemetry.glean.private.NumeratorMetricType
+typealias PingType<T> = mozilla.telemetry.glean.private.PingType<T>
+typealias QuantityMetricType = mozilla.telemetry.glean.private.QuantityMetricType
+typealias RateMetricType = mozilla.telemetry.glean.private.RateMetricType
+typealias RecordedExperiment = mozilla.telemetry.glean.private.RecordedExperiment
+typealias StringListMetricType = mozilla.telemetry.glean.private.StringListMetricType
+typealias StringMetricType = mozilla.telemetry.glean.private.StringMetricType
+typealias TextMetricType = mozilla.telemetry.glean.private.TextMetricType
+typealias TimeUnit = mozilla.telemetry.glean.private.TimeUnit
+typealias TimespanMetricType = mozilla.telemetry.glean.private.TimespanMetricType
+typealias TimingDistributionMetricType = mozilla.telemetry.glean.private.TimingDistributionMetricType
+typealias UrlMetricType = mozilla.telemetry.glean.private.UrlMetricType
+typealias UuidMetricType = mozilla.telemetry.glean.private.UuidMetricType
+
+// FIXME(bug 1885170): Wrap the Glean SDK `EventMetricType` to overwrite the `testGetValue` function.
+/**
+ * This implements the developer facing API for recording events.
+ *
+ * Instances of this class type are automatically generated by the parsers at built time,
+ * allowing developers to record events that were previously registered in the metrics.yaml file.
+ *
+ * The Events API only exposes the [record] method, which takes care of validating the input
+ * data and making sure that limits are enforced.
+ */
+class EventMetricType<ExtraObject> internal constructor(
+ private var inner: mozilla.telemetry.glean.private.EventMetricType<ExtraObject>,
+) where ExtraObject : EventExtras {
+ /**
+ * The public constructor used by automatically generated metrics.
+ */
+ constructor(meta: CommonMetricData, allowedExtraKeys: List<String>) :
+ this(inner = mozilla.telemetry.glean.private.EventMetricType(meta, allowedExtraKeys))
+
+ /**
+ * Record an event by using the information provided by the instance of this class.
+ *
+ * @param extra The event extra properties.
+ * Values are converted to strings automatically
+ * This is used for events where additional richer context is needed.
+ * The maximum length for values is 100 bytes.
+ *
+ * Note: `extra` is not optional here to avoid overlapping with the above definition of `record`.
+ * If no `extra` data is passed the above function will be invoked correctly.
+ */
+ fun record(extra: ExtraObject? = null) {
+ inner.record(extra)
+ }
+
+ /**
+ * Returns the stored value for testing purposes only. This function will attempt to await the
+ * last task (if any) writing to the the metric's storage engine before returning a value.
+ *
+ * @param pingName represents the name of the ping to retrieve the metric for.
+ * Defaults to the first value in `sendInPings`.
+ * @return value of the stored events
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ @JvmOverloads
+ fun testGetValue(pingName: String? = null): List<mozilla.telemetry.glean.private.RecordedEvent>? {
+ var events = inner.testGetValue(pingName)
+ if (events == null) {
+ return events
+ }
+
+ // Remove the `glean_timestamp` extra.
+ // This is added by Glean and does not need to be exposed to testing.
+ for (event in events) {
+ if (event.extra == null) {
+ continue
+ }
+
+ // We know it's not null
+ var map = event.extra!!.toMutableMap()
+ map.remove("glean_timestamp")
+ if (map.isEmpty()) {
+ event.extra = null
+ } else {
+ event.extra = map
+ }
+ }
+
+ return events
+ }
+
+ /**
+ * Returns the number of errors recorded for the given metric.
+ *
+ * @param errorType The type of the error recorded.
+ * @return the number of errors recorded for the metric.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ fun testGetNumRecordedErrors(errorType: mozilla.components.service.glean.testing.ErrorType) =
+ inner.testGetNumRecordedErrors(errorType)
+}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/ErrorType.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/ErrorType.kt
new file mode 100644
index 0000000000..8f4e53d501
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/ErrorType.kt
@@ -0,0 +1,10 @@
+/* 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.service.glean.testing
+
+/**
+ * Different types of errors that can be reported through Glean's error reporting metrics.
+ */
+typealias ErrorType = mozilla.telemetry.glean.testing.ErrorType
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt
new file mode 100644
index 0000000000..0a02b953bb
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt
@@ -0,0 +1,31 @@
+/* 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.service.glean.testing
+
+/**
+ * This implements a JUnit rule for writing tests for Glean SDK metrics.
+ *
+ * The rule takes care of sending Glean SDK pings to a local server, at the
+ * address: "http://localhost:<port>".
+ *
+ * This is useful for Android instrumented tests, where we don't want to
+ * initialize Glean more than once but still want to send pings to a local
+ * server for validation.
+ *
+ * FIXME(bug 1787234): State of the local server can persist across multiple test classes,
+ * leading to hard-to-diagnose intermittent test failures.
+ * It might be necessary to limit use of `GleanTestLocalServer` to a single test class for now.
+ *
+ * Example usage:
+ *
+ * ```
+ * // Add the following lines to you test class.
+ * @get:Rule
+ * val gleanRule = GleanTestLocalServer(3785)
+ * ```
+ *
+ * @param localPort the port of the local ping server
+ */
+typealias GleanTestLocalServer = mozilla.telemetry.glean.testing.GleanTestLocalServer
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt
new file mode 100644
index 0000000000..91d20fad70
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt
@@ -0,0 +1,7 @@
+/* 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.service.glean.testing
+
+typealias GleanTestRule = mozilla.telemetry.glean.testing.GleanTestRule
diff --git a/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanFromJavaTest.java b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanFromJavaTest.java
new file mode 100644
index 0000000000..c495e68bbc
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanFromJavaTest.java
@@ -0,0 +1,68 @@
+/* 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.service.glean;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.work.testing.WorkManagerTestInitHelper;
+
+import android.content.Context;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient;
+import mozilla.components.service.glean.config.Configuration;
+import mozilla.components.service.glean.net.ConceptFetchHttpUploader;
+import mozilla.telemetry.glean.BuildInfo;
+
+@RunWith(RobolectricTestRunner.class)
+public class GleanFromJavaTest {
+ // The only purpose of these tests is to make sure the Glean API is
+ // callable from Java. If something goes wrong, it should complain about missing
+ // methods at build-time.
+
+ @Test
+ public void testInitGleanWithDefaults() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WorkManagerTestInitHelper.initializeTestWorkManager(context);
+ ConceptFetchHttpUploader httpClient = ConceptFetchHttpUploader.fromClient(new HttpURLConnectionClient());
+ Configuration config = new Configuration(httpClient);
+ BuildInfo buildInfo = new BuildInfo("test", "test", Calendar.getInstance());
+ Glean.INSTANCE.initialize(context, true, config, buildInfo);
+ }
+
+ @Test
+ public void testInitGleanWithConfiguration() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WorkManagerTestInitHelper.initializeTestWorkManager(context);
+ ConceptFetchHttpUploader httpClient = ConceptFetchHttpUploader.fromClient(new HttpURLConnectionClient());
+ Configuration config =
+ new Configuration(httpClient, Configuration.DEFAULT_TELEMETRY_ENDPOINT, "test-channel");
+ BuildInfo buildInfo = new BuildInfo("test", "test", Calendar.getInstance());
+ Glean.INSTANCE.initialize(context, true, config, buildInfo);
+ }
+
+ @Test
+ public void testGleanExperimentsAPIWithDefaults() {
+ Glean.INSTANCE.setExperimentActive("test-exp-id-1", "test-branch-1");
+ }
+
+ @Test
+ public void testGleanExperimentsAPIWithOptional() {
+ Map<String, String> experimentProperties = new HashMap<>();
+ experimentProperties.put("test-prop1", "test-prop-result1");
+
+ Glean.INSTANCE.setExperimentActive(
+ "test-exp-id-1",
+ "test-branch-1",
+ experimentProperties
+ );
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt
new file mode 100644
index 0000000000..7eaae408f2
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt
@@ -0,0 +1,44 @@
+/* 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.service.glean
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import mozilla.components.service.glean.private.BooleanMetricType
+import mozilla.components.service.glean.private.CommonMetricData
+import mozilla.components.service.glean.private.Lifetime
+import mozilla.components.service.glean.testing.GleanTestRule
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class GleanTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ @get:Rule
+ val gleanRule = GleanTestRule(context)
+
+ @Test
+ fun `Glean correctly initializes and records a metric`() {
+ // Define a 'booleanMetric' boolean metric, which will be stored in "store1"
+ val booleanMetric = BooleanMetricType(
+ CommonMetricData(
+ disabled = false,
+ category = "telemetry",
+ lifetime = Lifetime.APPLICATION,
+ name = "boolean_metric",
+ sendInPings = listOf("store1"),
+ ),
+ )
+
+ booleanMetric.set(true)
+
+ assertTrue(booleanMetric.testGetValue()!!)
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt
new file mode 100644
index 0000000000..48d0d6c3a4
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt
@@ -0,0 +1,340 @@
+/* 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.service.glean.net
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.lib.fetch.okhttp.OkHttpClient
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.telemetry.glean.config.Configuration
+import mozilla.telemetry.glean.net.HttpStatus
+import mozilla.telemetry.glean.net.RecoverableFailure
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import java.io.IOException
+import java.net.CookieHandler
+import java.net.CookieManager
+import java.net.HttpCookie
+import java.net.URI
+import java.util.concurrent.TimeUnit
+
+class ConceptFetchHttpUploaderTest {
+ private val testPath: String = "/some/random/path/not/important"
+ private val testPing: String = "{ 'ping': 'test' }"
+ private val testDefaultConfig = Configuration()
+
+ /**
+ * Create a mock webserver that accepts all requests.
+ * @return a [MockWebServer] instance
+ */
+ private fun getMockWebServer(): MockWebServer {
+ val server = MockWebServer()
+ server.dispatcher =
+ object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse {
+ return MockResponse().setBody("OK")
+ }
+ }
+
+ return server
+ }
+
+ @Test
+ fun `connection timeouts must be properly set`() {
+ val uploader =
+ spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() }))
+
+ val request = uploader.buildRequest(testPath, testPing.toByteArray(), emptyMap())
+
+ assertEquals(
+ Pair(ConceptFetchHttpUploader.DEFAULT_READ_TIMEOUT, TimeUnit.MILLISECONDS),
+ request.readTimeout,
+ )
+ assertEquals(
+ Pair(ConceptFetchHttpUploader.DEFAULT_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS),
+ request.connectTimeout,
+ )
+ }
+
+ @Test
+ fun `Glean headers are correctly dispatched`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response("URL", 200, mock(), mock()),
+ )
+
+ val expectedHeaders = mapOf(
+ "Content-Type" to "application/json; charset=utf-8",
+ "Test-header" to "SomeValue",
+ "OtherHeader" to "Glean/Test 25.0.2",
+ )
+
+ val uploader = ConceptFetchHttpUploader(lazy { mockClient })
+ uploader.upload(testPath, testPing.toByteArray(), expectedHeaders)
+ val requestCaptor = argumentCaptor<Request>()
+ verify(mockClient).fetch(requestCaptor.capture())
+
+ expectedHeaders.forEach { (headerName, headerValue) ->
+ assertEquals(
+ headerValue,
+ requestCaptor.value.headers!![headerName],
+ )
+ }
+ }
+
+ @Test
+ fun `Cookie policy must be properly set`() {
+ val uploader =
+ spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() }))
+
+ val request = uploader.buildRequest(testPath, testPing.toByteArray(), emptyMap())
+
+ assertEquals(request.cookiePolicy, Request.CookiePolicy.OMIT)
+ }
+
+ @Test
+ fun `upload() returns true for successful submissions (200)`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response(
+ "URL",
+ 200,
+ mock(),
+ mock(),
+ ),
+ )
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ assertEquals(HttpStatus(200), uploader.upload(testPath, testPing.toByteArray(), emptyMap()))
+ }
+
+ @Test
+ fun `upload() returns false for server errors (5xx)`() {
+ for (responseCode in 500..527) {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response(
+ "URL",
+ responseCode,
+ mock(),
+ mock(),
+ ),
+ )
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ assertEquals(HttpStatus(responseCode), uploader.upload(testPath, testPing.toByteArray(), emptyMap()))
+ }
+ }
+
+ @Test
+ fun `upload() returns true for successful submissions (2xx)`() {
+ for (responseCode in 200..226) {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response(
+ "URL",
+ responseCode,
+ mock(),
+ mock(),
+ ),
+ )
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ assertEquals(HttpStatus(responseCode), uploader.upload(testPath, testPing.toByteArray(), emptyMap()))
+ }
+ }
+
+ @Test
+ fun `upload() returns true for failing submissions with broken requests (4xx)`() {
+ for (responseCode in 400..451) {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response(
+ "URL",
+ responseCode,
+ mock(),
+ mock(),
+ ),
+ )
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ assertEquals(HttpStatus(responseCode), uploader.upload(testPath, testPing.toByteArray(), emptyMap()))
+ }
+ }
+
+ @Test
+ fun `upload() correctly uploads the ping data with default configuration`() {
+ val server = getMockWebServer()
+
+ val client = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+
+ val submissionUrl = "http://" + server.hostName + ":" + server.port + testPath
+ assertEquals(HttpStatus(200), client.upload(submissionUrl, testPing.toByteArray(), mapOf("test" to "header")))
+
+ val request = server.takeRequest()
+ assertEquals(testPath, request.path)
+ assertEquals("POST", request.method)
+ assertEquals(testPing, request.body.readUtf8())
+ assertEquals("header", request.getHeader("test"))
+
+ server.shutdown()
+ }
+
+ @Test
+ fun `upload() correctly uploads the ping data with httpurlconnection client`() {
+ val server = getMockWebServer()
+
+ val client = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+
+ val submissionUrl = "http://" + server.hostName + ":" + server.port + testPath
+ assertEquals(HttpStatus(200), client.upload(submissionUrl, testPing.toByteArray(), mapOf("test" to "header")))
+
+ val request = server.takeRequest()
+ assertEquals(testPath, request.path)
+ assertEquals("POST", request.method)
+ assertEquals(testPing, request.body.readUtf8())
+ assertEquals("header", request.getHeader("test"))
+ assertTrue(request.headers.values("Cookie").isEmpty())
+
+ server.shutdown()
+ }
+
+ @Test
+ fun `upload() correctly uploads the ping data with OkHttp client`() {
+ val server = getMockWebServer()
+
+ val client = ConceptFetchHttpUploader(lazy { OkHttpClient() })
+
+ val submissionUrl = "http://" + server.hostName + ":" + server.port + testPath
+ assertEquals(HttpStatus(200), client.upload(submissionUrl, testPing.toByteArray(), mapOf("test" to "header")))
+
+ val request = server.takeRequest()
+ assertEquals(testPath, request.path)
+ assertEquals("POST", request.method)
+ assertEquals(testPing, request.body.readUtf8())
+ assertEquals("header", request.getHeader("test"))
+ assertTrue(request.headers.values("Cookie").isEmpty())
+
+ server.shutdown()
+ }
+
+ @Test
+ fun `upload() must not transmit any cookie`() {
+ val server = getMockWebServer()
+
+ val testConfig = testDefaultConfig.copy(
+ serverEndpoint = "http://localhost:" + server.port,
+ )
+
+ // Set the default cookie manager/handler to be used for the http upload.
+ val cookieManager = CookieManager()
+ CookieHandler.setDefault(cookieManager)
+
+ // Store a sample cookie.
+ val cookie = HttpCookie("cookie-time", "yes")
+ cookie.domain = testConfig.serverEndpoint
+ cookie.path = testPath
+ cookie.version = 0
+ cookieManager.cookieStore.add(URI(testConfig.serverEndpoint), cookie)
+
+ // Store a cookie for a subdomain of the same domain's as the server endpoint,
+ // to make sure we don't accidentally remove it.
+ val cookie2 = HttpCookie("cookie-time2", "yes")
+ cookie2.domain = "sub.localhost"
+ cookie2.path = testPath
+ cookie2.version = 0
+ cookieManager.cookieStore.add(URI("http://sub.localhost:${server.port}/test"), cookie2)
+
+ // Add another cookie for the same domain. This one should be removed as well.
+ val cookie3 = HttpCookie("cookie-time3", "yes")
+ cookie3.domain = "localhost"
+ cookie3.path = testPath
+ cookie3.version = 0
+ cookieManager.cookieStore.add(URI("http://localhost:${server.port}/test"), cookie3)
+
+ // Trigger the connection.
+ val client = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+ val submissionUrl = testConfig.serverEndpoint + testPath
+ assertEquals(HttpStatus(200), client.upload(submissionUrl, testPing.toByteArray(), emptyMap()))
+
+ val request = server.takeRequest()
+ assertEquals(testPath, request.path)
+ assertEquals("POST", request.method)
+ assertEquals(testPing, request.body.readUtf8())
+ assertTrue(request.headers.values("Cookie").isEmpty())
+
+ // Check that we still have a cookie.
+ assertEquals(1, cookieManager.cookieStore.cookies.size)
+ assertEquals("cookie-time2", cookieManager.cookieStore.cookies[0].name)
+
+ server.shutdown()
+ }
+
+ @Test
+ fun `upload() should return false when upload fails`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenThrow(IOException())
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ // And IOException during upload is a failed upload that we should retry. The client should
+ // return false in this case.
+ assertEquals(RecoverableFailure(0), uploader.upload("path", "ping".toByteArray(), emptyMap()))
+ }
+
+ @Test
+ fun `the lazy client should only be instantiated after the first upload`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response("URL", 200, mock(), mock()),
+ )
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+ assertFalse(uploader.client.isInitialized())
+
+ // After calling upload, the client must get instantiated.
+ uploader.upload("path", "ping".toByteArray(), emptyMap())
+ assertTrue(uploader.client.isInitialized())
+ }
+
+ @Test
+ fun `usePrivateRequest sends all requests with private flag`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response("URL", 200, mock(), mock()),
+ )
+
+ val expectedHeaders = mapOf(
+ "Content-Type" to "application/json; charset=utf-8",
+ "Test-header" to "SomeValue",
+ "OtherHeader" to "Glean/Test 25.0.2",
+ )
+
+ val uploader = ConceptFetchHttpUploader(lazy { mockClient }, true)
+ uploader.upload(testPath, testPing.toByteArray(), expectedHeaders)
+
+ val captor = argumentCaptor<Request>()
+
+ verify(mockClient).fetch(captor.capture())
+
+ assertTrue(captor.value.private)
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/glean/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..1f0955d450
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/mobile/android/android-components/components/service/glean/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/glean/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/location/.gitignore b/mobile/android/android-components/components/service/location/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/mobile/android/android-components/components/service/location/README.md b/mobile/android/android-components/components/service/location/README.md
new file mode 100644
index 0000000000..9b333a44f9
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Service > Location
+
+ A library for accessing Mozilla's and other location services.
+
+## Usage
+
+### 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:service-location:{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/service/location/build.gradle b/mobile/android/android-components/components/service/location/build.gradle
new file mode 100644
index 0000000000..e9446bf656
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/build.gradle
@@ -0,0 +1,44 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.service.location'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation project(':concept-fetch')
+ implementation project(':support-ktx')
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ testImplementation project(':lib-fetch-httpurlconnection')
+}
+
+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/service/location/proguard-rules.pro b/mobile/android/android-components/components/service/location/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/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/service/location/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/location/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/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/service/location/src/main/java/mozilla/components/service/location/LocationService.kt b/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/LocationService.kt
new file mode 100644
index 0000000000..8838b51bd1
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/LocationService.kt
@@ -0,0 +1,57 @@
+/* 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.service.location
+
+/**
+ * Interface describing a [LocationService] that returns a [Region].
+ */
+interface LocationService {
+ /**
+ * Determines the current [Region] of the user.
+ */
+ suspend fun fetchRegion(readFromCache: Boolean = true): Region?
+
+ /**
+ * Get if there is already a cached region.
+ */
+ fun hasRegionCached(): Boolean
+
+ /**
+ * A [Region] returned by the location service.
+ *
+ * The [Region] use region codes and names from the GENC dataset, which is for the most part
+ * compatible with the ISO 3166 standard. While the API endpoint and [Region] class refers to
+ * country, no claim about the political status of any region is made by this service.
+ *
+ * @param countryCode Country code; ISO 3166.
+ * @param countryName Name of the country (English); ISO 3166.
+ */
+ data class Region(
+ val countryCode: String,
+ val countryName: String,
+ )
+
+ companion object {
+ /**
+ * Creates a dummy [LocationService] implementation that always returns `null`.
+ */
+ fun dummy() = object : LocationService {
+ override suspend fun fetchRegion(readFromCache: Boolean): Region? = null
+ override fun hasRegionCached(): Boolean = false
+ }
+
+ /**
+ * Creates a default [LocationService] implementation that always returns the "XX" region.
+ *
+ * The advantage of using the default implementation over the dummy implementations is that
+ * code may stop retrying fetching a region if a region was returned from the service
+ * instead of `null` which indicates a failure.
+ */
+ fun default() = object : LocationService {
+ override suspend fun fetchRegion(readFromCache: Boolean): Region? = Region("XX", "None")
+ override fun hasRegionCached(): Boolean = true
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/MozillaLocationService.kt b/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/MozillaLocationService.kt
new file mode 100644
index 0000000000..663ec7c5fe
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/MozillaLocationService.kt
@@ -0,0 +1,187 @@
+/* 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.service.location
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.NONE
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.Build
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.sanitizeURL
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+private const val GEOIP_SERVICE_URL = "https://location.services.mozilla.com/v1/"
+private const val CONNECT_TIMEOUT_SECONDS = 10L
+private const val READ_TIMEOUT_SECONDS = 10L
+private const val USER_AGENT = "MozAC/" + Build.version
+private const val EMPTY_REQUEST_BODY = "{}"
+private const val CACHE_FILE = "mozac.service.location.region"
+private const val KEY_COUNTRY_CODE = "country_code"
+private const val KEY_COUNTRY_NAME = "country_name"
+private const val KEY_CACHED_AT = "cached_at"
+
+// The amount of time (in seconds) to cache the result of `MozillaLocationService.fetchRegion()`.
+private const val CACHE_LIFETIME_IN_MS = 24 * 60 * 60 * 1000
+
+/**
+ * The Mozilla Location Service (MLS) is an open service which lets devices determine their location
+ * based on network infrastructure like Bluetooth beacons, cell towers and WiFi access points.
+ *
+ * - https://location.services.mozilla.com/
+ * - https://mozilla.github.io/ichnaea/api/index.html
+ *
+ * Note: Accessing the Mozilla Location Service requires an API token:
+ * https://location.services.mozilla.com/contact
+ *
+ * @param client The HTTP client that this [MozillaLocationService] should use for requests.
+ * @param apiKey The API key that is used to access the Mozilla location service.
+ * @param serviceUrl An optional URL override usually only needed for testing.
+ */
+class MozillaLocationService(
+ private val context: Context,
+ private val client: Client,
+ apiKey: String,
+ serviceUrl: String = GEOIP_SERVICE_URL,
+ private val currentTime: () -> Long = { System.currentTimeMillis() },
+) : LocationService {
+ private val regionServiceUrl = (serviceUrl + "country?key=%s").format(apiKey)
+
+ /**
+ * Determines the current [LocationService.Region] based on the IP address used to access the service.
+ *
+ * https://mozilla.github.io/ichnaea/api/region.html
+ *
+ * @param readFromCache Whether a previously returned region (from the cache) can be returned
+ * (default) or whether a request to the service should always be made.
+ */
+ override suspend fun fetchRegion(
+ readFromCache: Boolean,
+ ): LocationService.Region? = withContext(Dispatchers.IO) {
+ if (readFromCache && isCacheValid()) {
+ context.loadCachedRegion()?.let { return@withContext it }
+ }
+
+ client.fetchRegion(regionServiceUrl)?.also {
+ context.cacheRegion(it)
+ }
+ }
+
+ /**
+ * Get if there is already a cached region.
+ * This does not guarantee we have the current actual region but only the last value
+ * which may be obsolete at this time.
+ */
+ override fun hasRegionCached(): Boolean {
+ return context.hasCachedRegion()
+ }
+
+ /**
+ * Check to see if the cache is still valid.
+ */
+ private fun isCacheValid(): Boolean {
+ return currentTime() < context.cachedAt() + CACHE_LIFETIME_IN_MS
+ }
+
+ private fun Context.cacheRegion(region: LocationService.Region) {
+ regionCache()
+ .edit()
+ .putString(KEY_COUNTRY_CODE, region.countryCode)
+ .putString(KEY_COUNTRY_NAME, region.countryName)
+ .putLong(KEY_CACHED_AT, currentTime())
+ .apply()
+ }
+}
+
+private fun Context.loadCachedRegion(): LocationService.Region? {
+ val cache = regionCache()
+
+ return if (cache.contains(KEY_COUNTRY_CODE) && cache.contains(KEY_COUNTRY_NAME)) {
+ LocationService.Region(
+ cache.getString(KEY_COUNTRY_CODE, null)!!,
+ cache.getString(KEY_COUNTRY_NAME, null)!!,
+ )
+ } else {
+ null
+ }
+}
+
+private fun Context.cachedAt(): Long {
+ val cache = regionCache()
+ return cache.getLong(KEY_CACHED_AT, 0L)
+}
+
+private fun Context.hasCachedRegion(): Boolean {
+ val cache = regionCache()
+ return cache.contains(KEY_COUNTRY_CODE) && cache.contains(KEY_COUNTRY_NAME)
+}
+
+@VisibleForTesting(otherwise = NONE)
+internal fun Context.clearRegionCache() {
+ regionCache()
+ .edit()
+ .clear()
+ .apply()
+}
+
+private fun Context.regionCache(): SharedPreferences {
+ return getSharedPreferences(CACHE_FILE, Context.MODE_PRIVATE)
+}
+
+private fun Client.fetchRegion(regionServiceUrl: String): LocationService.Region? {
+ val request = Request(
+ url = regionServiceUrl.sanitizeURL(),
+ method = Request.Method.POST,
+ headers = MutableHeaders(
+ Headers.Names.CONTENT_TYPE to Headers.Values.CONTENT_TYPE_APPLICATION_JSON,
+ Headers.Names.USER_AGENT to USER_AGENT,
+ ),
+ // We are posting an empty request body here. This means the service will only use the IP
+ // address to provide a region response. Technically it's possible to also provide data
+ // about nearby Bluetooth, cell or WiFi networks.
+ body = Request.Body.fromString(EMPTY_REQUEST_BODY),
+ connectTimeout = Pair(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS),
+ readTimeout = Pair(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS),
+ conservative = true,
+ )
+
+ return try {
+ fetch(request).toRegion()
+ } catch (e: IOException) {
+ Logger.debug(message = "Could not fetch region from location service", throwable = e)
+ null
+ }
+}
+
+private fun Response.toRegion(): LocationService.Region? {
+ if (!isSuccess) {
+ close()
+ return null
+ }
+
+ use {
+ return try {
+ val json = JSONObject(body.string(Charsets.UTF_8))
+ LocationService.Region(
+ json.getString(KEY_COUNTRY_CODE),
+ json.getString(KEY_COUNTRY_NAME),
+ )
+ } catch (e: JSONException) {
+ Logger.debug(message = "Could not parse JSON returned from location service", throwable = e)
+ null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt b/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt
new file mode 100644
index 0000000000..eca31d268f
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt
@@ -0,0 +1,23 @@
+/* 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.service.location
+
+import junit.framework.TestCase.assertNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class LocationServiceTest {
+ @ExperimentalCoroutinesApi
+ @Test
+ fun `dummy implementation returns null`() {
+ runTest(UnconfinedTestDispatcher()) {
+ assertNull(LocationService.dummy().fetchRegion(false))
+ assertNull(LocationService.dummy().fetchRegion(true))
+ assertNull(LocationService.dummy().fetchRegion(false))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt b/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt
new file mode 100644
index 0000000000..d2cf7d64aa
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt
@@ -0,0 +1,399 @@
+/* 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.service.location
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.currentTime
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.fakes.FakeClock
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import java.io.IOException
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class MozillaLocationServiceTest {
+ @Before
+ @After
+ fun cleanUp() {
+ testContext.clearRegionCache()
+ }
+
+ @Test
+ fun `WHEN calling fetchRegion AND the service returns a region THEN a Region object is returned`() = runTest {
+ val server = MockWebServer()
+ server.enqueue(MockResponse().setBody("{\"country_name\": \"Germany\", \"country_code\": \"DE\"}"))
+
+ try {
+ server.start()
+
+ val service = MozillaLocationService(
+ testContext,
+ HttpURLConnectionClient(),
+ apiKey = "test",
+ serviceUrl = server.url("/").toString(),
+ )
+
+ val region = service.fetchRegion()
+
+ assertNotNull(region!!)
+
+ assertEquals("DE", region.countryCode)
+ assertEquals("Germany", region.countryName)
+
+ val request = server.takeRequest()
+
+ assertEquals(server.url("/country?key=test"), request.requestUrl)
+ } finally {
+ server.shutdown()
+ }
+ }
+
+ @Test
+ fun `WHEN client throws IOException THEN the returned region is null`() = runTest {
+ val client: Client = mock()
+ doThrow(IOException()).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNull(region)
+ }
+
+ @Test
+ fun `WHEN fetching region THEN request is sent to the location service`() = runTest {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"France\", \"country_code\": \"FR\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNotNull(region!!)
+
+ assertEquals("FR", region.countryCode)
+ assertEquals("France", region.countryName)
+
+ val captor = argumentCaptor<Request>()
+ verify(client).fetch(captor.capture())
+
+ val request = captor.value
+ assertEquals("https://location.services.mozilla.com/v1/country?key=test", request.url)
+ }
+
+ @Test
+ fun `WHEN fetching region AND service returns 404 THEN region is null`() = runTest {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 404,
+ headers = MutableHeaders(),
+ body = Response.Body.empty(),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNull(region)
+ }
+
+ @Test
+ fun `WHEN fetching region AND service returns 500 THEN region is null`() = runTest {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 500,
+ headers = MutableHeaders(),
+ body = Response.Body("Internal Server Error".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNull(region)
+ }
+
+ @Test
+ fun `WHEN fetching region AND service returns broken JSON THEN region is null`() = runTest {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"France\",".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNull(region)
+ }
+
+ @Test
+ fun `WHEN fetching region AND service returns empty JSON object THEN region is null`() = runTest {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNull(region)
+ }
+
+ @Test
+ fun `WHEN fetching region AND service returns incomplete JSON THEN region is null`() = runTest {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_code\": \"DE\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNull(region)
+ }
+
+ @Test
+ fun `WHEN fetching region for the second time THEN region is read from cache`() = runTest {
+ run {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"Nepal\", \"country_code\": \"NP\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNotNull(region!!)
+
+ assertEquals("NP", region.countryCode)
+ assertEquals("Nepal", region.countryName)
+
+ verify(client).fetch(any())
+ }
+
+ run {
+ val client: Client = mock()
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNotNull(region!!)
+
+ assertEquals("NP", region.countryCode)
+ assertEquals("Nepal", region.countryName)
+
+ verify(client, never()).fetch(any())
+ }
+ }
+
+ @Test
+ fun `WHEN fetching region for the second time and setting readFromCache = false THEN request is sent again`() = runTest {
+ run {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"Nepal\", \"country_code\": \"NP\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion()
+
+ assertNotNull(region!!)
+
+ assertEquals("NP", region.countryCode)
+ assertEquals("Nepal", region.countryName)
+
+ verify(client).fetch(any())
+ }
+
+ run {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"Liberia\", \"country_code\": \"LR\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(testContext, client, apiKey = "test")
+ val region = service.fetchRegion(readFromCache = false)
+
+ assertNotNull(region!!)
+
+ assertEquals("LR", region.countryCode)
+ assertEquals("Liberia", region.countryName)
+
+ verify(client).fetch(any())
+ }
+ }
+
+ @Test
+ fun `WHEN fetching region and the cache is valid THEN request is not sent again`() = runTest {
+ val clock = FakeClock()
+
+ run {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"Nepal\", \"country_code\": \"NP\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(
+ testContext,
+ client,
+ apiKey = "test",
+ currentTime = clock::time,
+ )
+ val region = service.fetchRegion(readFromCache = true)
+
+ assertNotNull(region!!)
+
+ assertEquals("NP", region.countryCode)
+ assertEquals("Nepal", region.countryName)
+
+ verify(client).fetch(any())
+ }
+
+ // Let's jump 23 hours in the future
+ clock.advanceBy(23 * 60 * 60 * 1000)
+
+ run {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"Liberia\", \"country_code\": \"LR\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(
+ testContext,
+ client,
+ apiKey = "test",
+ currentTime = clock::time,
+ )
+ val region = service.fetchRegion(readFromCache = true)
+
+ assertNotNull(region!!)
+
+ assertEquals("NP", region.countryCode)
+ assertEquals("Nepal", region.countryName)
+
+ verify(client, never()).fetch(any())
+ }
+ }
+
+ @Test
+ fun `WHEN fetching region and the cache is invalid THEN request is sent again`() = runTest {
+ val clock = FakeClock()
+
+ run {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"Nepal\", \"country_code\": \"NP\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(
+ testContext,
+ client,
+ apiKey = "test",
+ currentTime = clock::time,
+ )
+ val region = service.fetchRegion(readFromCache = true)
+
+ assertNotNull(region!!)
+
+ assertEquals("NP", region.countryCode)
+ assertEquals("Nepal", region.countryName)
+
+ verify(client).fetch(any())
+ }
+
+ // Let's jump 24 hours in the future
+ clock.advanceBy(24 * 60 * 60 * 1000)
+
+ run {
+ val client: Client = mock()
+ val response = Response(
+ url = "http://example.org",
+ status = 200,
+ headers = MutableHeaders(),
+ body = Response.Body("{\"country_name\": \"Liberia\", \"country_code\": \"LR\"}".byteInputStream()),
+ )
+ doReturn(response).`when`(client).fetch(any())
+
+ val service = MozillaLocationService(
+ testContext,
+ client,
+ apiKey = "test",
+ currentTime = clock::time,
+ )
+ val region = service.fetchRegion(readFromCache = true)
+
+ assertNotNull(region!!)
+
+ assertEquals("LR", region.countryCode)
+ assertEquals("Liberia", region.countryName)
+
+ verify(client).fetch(any())
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/location/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/location/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..1f0955d450
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/mobile/android/android-components/components/service/location/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/location/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/location/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/nimbus/.gitignore b/mobile/android/android-components/components/service/nimbus/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/mobile/android/android-components/components/service/nimbus/README.md b/mobile/android/android-components/components/service/nimbus/README.md
new file mode 100644
index 0000000000..b697a2b03c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/README.md
@@ -0,0 +1,352 @@
+# [Android Components](../../../README.md) > Service > Nimbus
+
+A wrapper for the Nimbus SDK.
+
+Contents:
+
+- [Usage](#usage)
+
+## Usage
+
+### 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:service-nimbus:{latest-version}"
+```
+
+### Initializing the Experiments library
+
+**TODO**
+
+### Updating of experiments
+
+**TODO**
+
+### Checking if a user is part of an experiment
+
+**TODO**
+
+## Testing Nimbus
+
+This section contains information about the Kinto and UI schemas needed to set up and run experiments on the "Dev" Kinto instance located at https://kinto.dev.mozaws.net.
+**NOTE** The dev server instance requires LDAP authorization, but does not require connection to the internal Mozilla VPN.
+
+## Where to add the Kinto and UI schemas
+
+For testing purposes, create a collection with an id of `nimbus-mobile-experiments` in the `main` bucket on the [Kinto dev server](https://kinto.dev.mozaws.net/v1/admin/).
+
+### JSON Schema
+
+```JSON
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://mozilla.org/example.json",
+ "type": "object",
+ "title": "Nimbus Schema",
+ "description": "This is the description of the current Nimbus experiment schema, which can be found at https://github.com/mozilla/nimbus-shared",
+ "default": {},
+ "examples": [
+ {
+ "slug": "secure-gold",
+ "endDate": null,
+ "branches": [
+ {
+ "slug": "control",
+ "ratio": 1
+ },
+ {
+ "slug": "treatment",
+ "ratio": 1
+ }
+ ],
+ "probeSets": [],
+ "startDate": null,
+ "application": "org.mozilla.fenix",
+ "bucketConfig": {
+ "count": 100,
+ "start": 0,
+ "total": 10000,
+ "namespace": "secure-gold",
+ "randomizationUnit": "nimbus_id"
+ },
+ "userFacingName": "Diagnostic test experiment",
+ "referenceBranch": "control",
+ "isEnrollmentPaused": false,
+ "proposedEnrollment": 7,
+ "userFacingDescription": "This is a test experiment for diagnostic purposes.",
+ "id": "secure-gold"
+ }
+ ],
+ "required": [
+ "slug",
+ "branches",
+ "application",
+ "bucketConfig",
+ "userFacingName",
+ "referenceBranch",
+ "isEnrollmentPaused",
+ "proposedEnrollment",
+ "userFacingDescription",
+ "id"
+ ],
+ "properties": {
+ "slug": {
+ "$id": "#/properties/slug",
+ "type": "string",
+ "title": "Slug",
+ "description": "The slug is the unique identifier for the experiment.",
+ "default": "",
+ "examples": ["fenix-search-widget-experiment"]
+ },
+ "endDate": {
+ "$id": "#/properties/endDate",
+ "type": ["string", "null"],
+ "format": "date-time",
+ "title": "End Date",
+ "description": "This is the date that the experiment will end.",
+ "default": null,
+ "examples": [null]
+ },
+ "branches": {
+ "$id": "#/properties/branches",
+ "type": "array",
+ "title": "Branches",
+ "description": "Branches relate to the specific treatments to be applied for the experiment.",
+ "default": [],
+ "examples": [
+ [
+ {
+ "slug": "control",
+ "ratio": 1
+ },
+ {
+ "slug": "treatment",
+ "ratio": 1
+ }
+ ]
+ ],
+ "additionalItems": true,
+ "items": {
+ "$id": "#/properties/branches/items",
+ "anyOf": [
+ {
+ "$id": "#/properties/branches/items/anyOf/0",
+ "type": "object",
+ "title": "Branch Items",
+ "description": "Each branch has a slug, or name, and a ratio that weights selection into that branch",
+ "default": {},
+ "examples": [
+ {
+ "slug": "control",
+ "ratio": 1
+ }
+ ],
+ "required": ["slug", "ratio"],
+ "properties": {
+ "slug": {
+ "$id": "#/properties/branches/items/anyOf/0/properties/slug",
+ "type": "string",
+ "title": "Branch Slug",
+ "description": "The branch slug is the unique name of the branch, within this experiment.",
+ "default": "control",
+ "examples": ["control"]
+ },
+ "ratio": {
+ "$id": "#/properties/branches/items/anyOf/0/properties/ratio",
+ "type": "integer",
+ "title": "Branch Ratio",
+ "description": "This is the weighting of the branch for branch selection.",
+ "default": 1,
+ "examples": [1]
+ }
+ },
+ "additionalProperties": true
+ }
+ ]
+ }
+ },
+ "probeSets": {
+ "$id": "#/properties/probeSets",
+ "type": "array",
+ "title": "Probe Sets",
+ "description": "Currently unimplemented/used",
+ "default": [],
+ "examples": [[]],
+ "additionalItems": true,
+ "items": {
+ "$id": "#/properties/probeSets/items"
+ }
+ },
+ "startDate": {
+ "$id": "#/properties/startDate",
+ "type": ["string", "null"],
+ "format": "date-time",
+ "title": "Start Date",
+ "description": "The date that the experiment will start",
+ "default": null,
+ "examples": [null]
+ },
+ "application": {
+ "$id": "#/properties/application",
+ "type": "string",
+ "title": "Application",
+ "description": "This is the application to target",
+ "default": "",
+ "examples": [
+ "org.mozilla.fenix",
+ "org.mozilla.firefox",
+ "org.mozilla.ios.firefox"
+ ]
+ },
+ "bucketConfig": {
+ "$id": "#/properties/bucketConfig",
+ "type": "object",
+ "title": "Bucket Configuration",
+ "description": "This is the configuration of the bucketing for determining the experiment sample size",
+ "default": {},
+ "examples": [
+ {
+ "count": 2000,
+ "start": 0,
+ "total": 10000,
+ "namespace": "performance-experiments",
+ "randomizationUnit": "nimbus_id"
+ }
+ ],
+ "required": ["count", "start", "total", "namespace", "randomizationUnit"],
+ "properties": {
+ "count": {
+ "$id": "#/properties/bucketConfig/properties/count",
+ "type": "integer",
+ "title": "Count",
+ "description": "The total count of buckets to assign to be eligible to enroll in the experiment.",
+ "default": 0,
+ "examples": [2000]
+ },
+ "start": {
+ "$id": "#/properties/bucketConfig/properties/start",
+ "type": "integer",
+ "title": "Starting Bucket",
+ "description": "This is the bucket that the count of buckets will start from.",
+ "default": 0,
+ "examples": [0]
+ },
+ "total": {
+ "$id": "#/properties/bucketConfig/properties/total",
+ "type": "integer",
+ "title": "Total Buckets",
+ "description": "This is the total number of buckets to divide the population into for enrollment purposes.",
+ "default": 10000,
+ "examples": [10000]
+ },
+ "namespace": {
+ "$id": "#/properties/bucketConfig/properties/namespace",
+ "type": "string",
+ "title": "Namespace",
+ "description": "This is the bucket namespace and should always match the experiment slug",
+ "default": "",
+ "examples": ["secure-gold"]
+ },
+ "randomizationUnit": {
+ "$id": "#/properties/bucketConfig/properties/randomizationUnit",
+ "type": "string",
+ "title": "Randomization Unit",
+ "description": "This is the id to use for randomization for the purpose of bucketing. Currently only nimbus_id implemented.",
+ "default": "nimbus_id",
+ "examples": ["nimbus_id"]
+ }
+ },
+ "additionalProperties": true
+ },
+ "userFacingName": {
+ "$id": "#/properties/userFacingName",
+ "type": "string",
+ "title": "User Facing Name",
+ "description": "The user-facing name of the experiment.",
+ "default": "",
+ "examples": ["Diagnostic test experiment"]
+ },
+ "referenceBranch": {
+ "$id": "#/properties/referenceBranch",
+ "type": "string",
+ "title": "Reference Branch",
+ "description": "Not currently implemented, do not change default",
+ "default": "control",
+ "examples": ["control"]
+ },
+ "isEnrollmentPaused": {
+ "$id": "#/properties/isEnrollmentPaused",
+ "type": "boolean",
+ "title": "Enrollment Paused",
+ "description": "True if the enrollment is paused, false if enrollment is active.",
+ "default": false,
+ "examples": [false]
+ },
+ "proposedEnrollment": {
+ "$id": "#/properties/proposedEnrollment",
+ "type": "integer",
+ "title": "Proposed Enrollment",
+ "description": "The length in days that enrollment is proposed.",
+ "default": 7,
+ "examples": [7]
+ },
+ "userFacingDescription": {
+ "$id": "#/properties/userFacingDescription",
+ "type": "string",
+ "title": "User Facing Description",
+ "description": "This is the description of the experiment that would be presented to the user.",
+ "default": "",
+ "examples": ["This is a test experiment for diagnostic purposes."]
+ },
+ "id": {
+ "$id": "#/properties/id",
+ "type": "string",
+ "title": "ID",
+ "description": "An analog of the slug? Not sure, make this match slug...",
+ "default": "",
+ "examples": ["secure-gold"]
+ }
+ },
+ "additionalProperties": true
+}
+```
+
+## UI Schema
+
+```JSON
+{
+ "ui:order": [
+ "slug",
+ "userFacingName",
+ "userFacingDescription",
+ "application",
+ "startDate",
+ "endDate",
+ "bucketConfig",
+ "branches",
+ "referenceBranch",
+ "isEnrollmentPaused",
+ "proposedEnrollment",
+ "id",
+ "probeSets"
+ ],
+ "userFacingDescription": {
+ "ui:widget": "textarea"
+ },
+ "bucketConfig": {
+ "ui:order": ["start", "count", "total", "namespace", "randomizationUnit"]
+ },
+ "branches": {
+ "ui:order": ["slug", "ratio"]
+ }
+}
+
+```
+
+## 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/service/nimbus/build.gradle b/mobile/android/android-components/components/service/nimbus/build.gradle
new file mode 100644
index 0000000000..b8ef48ec0a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/build.gradle
@@ -0,0 +1,122 @@
+/* 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/. */
+
+buildscript {
+ repositories {
+ gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
+ maven {
+ url repository
+ if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
+ allowInsecureProtocol = true
+ }
+ }
+ }
+ }
+
+ dependencies {
+ classpath "${ApplicationServicesConfig.groupId}:tooling-nimbus-gradle:${ApplicationServicesConfig.version}"
+ classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}"
+ }
+}
+
+plugins {
+ id "com.jetbrains.python.envs" version "$python_envs_plugin"
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ debug {
+ // Export experiments proguard rules even in debug since consuming apps may still
+ // enable proguard/R8
+ consumerProguardFiles 'proguard-rules-consumer.pro'
+ }
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ consumerProguardFiles 'proguard-rules-consumer.pro'
+ }
+ }
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ namespace 'mozilla.components.service.nimbus'
+}
+
+dependencies {
+ // These dependencies are part of this module's public API.
+ api (ComponentsDependencies.mozilla_appservices_nimbus) {
+ // Use our own version of the Glean dependency,
+ // which might be different from the version declared by A-S.
+ exclude group: 'org.mozilla.components', module: 'service-glean'
+ }
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_coordinatorlayout
+ implementation ComponentsDependencies.androidx_recyclerview
+ implementation ComponentsDependencies.androidx_work_runtime
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.mozilla_appservices_nimbus
+
+ implementation project(':support-base')
+ implementation project(':support-locale')
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ // We only compile against GeckoView and Glean. It's up to the app to add those dependencies if it wants to
+ // send crash reports to Socorro (GV).
+ compileOnly project(":service-glean")
+
+ testImplementation ComponentsDependencies.mozilla_appservices_full_megazord_forUnitTests
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.mozilla_glean_forUnitTests
+ testImplementation ComponentsDependencies.androidx_work_testing
+ testImplementation project(':support-test')
+ testImplementation project(":service-glean")
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
+
+
+apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
+
+nimbus {
+ // The path to the Nimbus feature manifest file
+ manifestFile = "messaging.fml.yaml"
+
+ // Map from the variant name to the channel as experimenter and nimbus understand it.
+ // If nimbus's channels were accurately set up well for this project, then this
+ // shouldn't be needed.
+ channels = [
+ debug: "debug",
+ release: "release",
+ ]
+
+ // This is an optional value, and updates the plugin to use a copy of application
+ // services. The path should be relative to the root project directory.
+ // *NOTE*: This example will not work for all projects, but should work for Fenix, Focus, and Android Components
+ applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir')
+ ? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null
+}
+
+apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
diff --git a/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml b/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml
new file mode 100644
index 0000000000..c53456e31e
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml
@@ -0,0 +1,194 @@
+---
+about:
+ description: Nimbus Feature Manifest for Android
+ kotlin:
+ package: mozilla.components.service.nimbus
+ class: .messaging.FxNimbusMessaging
+channels:
+ - release
+ - debug
+features:
+ nimbus-system:
+ description: |
+ Configuration of the Nimbus System in Android.
+ variables:
+ refresh-interval-foreground:
+ description: |
+ The minimum interval in minutes between fetching experiment
+ recipes in the foreground.
+ type: Int
+ default: 60 # 1 hour
+
+ messaging:
+ description: |
+ The in-app messaging system.
+
+ allow-coenrollment: true
+
+ variables:
+ messages:
+ description: A growable collection of messages
+ type: Map<MessageKey, MessageData>
+ string-alias: MessageKey
+ default: {}
+
+ triggers:
+ description: >
+ A collection of out the box trigger
+ expressions. Each entry maps to a
+ valid JEXL expression.
+ type: Map<TriggerName, String>
+ string-alias: TriggerName
+ default: {}
+ styles:
+ description: >
+ A map of styles to configure message
+ appearance.
+ type: Map<StyleName, StyleData>
+ string-alias: StyleName
+ default: {}
+
+ $$surfaces:
+ description: |
+ A list available surfaces for this app.
+
+ This should not be written to by experiments, and should be hidden to users.
+ type: List<SurfaceName>
+ string-alias: SurfaceName
+ default: []
+
+ actions:
+ type: Map<ActionName, String>
+ description: A growable map of action URLs.
+ string-alias: ActionName
+ default:
+ OPEN_URL: ://open
+
+ on-control:
+ type: ControlMessageBehavior
+ description: What should be displayed when a control message is selected.
+ default: show-next-message
+ notification-config:
+ description: Configuration of the notification worker for all notification messages.
+ type: NotificationConfig
+ default: {}
+ message-under-experiment:
+ description: Deprecated in favor of `MessageData#experiment`. This will be removed in future releases.
+ type: Option<MessageKey>
+ default: null
+ $$experiment:
+ description: The only acceptable value for `MessageData#experiment`. This should not be set by experiment.
+ type: ExperimentSlug
+ string-alias: ExperimentSlug
+ default: "{experiment}"
+ defaults:
+
+objects:
+ MessageData:
+ description: >
+ An object to describe a message. It uses human
+ readable strings to describe the triggers, action and
+ style of the message as well as the text of the message
+ and call to action.
+ fields:
+ action:
+ type: ActionName
+ description: >
+ A URL of a page or a deeplink.
+ This may have substitution variables in.
+ # This should never be defaulted.
+ default: OPEN_URL
+ action-params:
+ description: >
+ A string map containing query parameters that will be appended to the action URL.
+ This is useful for opening URLs in tabs, or specifying that the tab should be private.
+ The values may have substitutions, e.g. "url": "https://example.com/id={uuid}",
+ "private": "true".
+
+ The params and their values are all determined downstream of the messaging component, by
+ the embedding app's deeplink processing machinery.
+ type: Map<String, String>
+ default: {}
+ title:
+ type: Option<Text>
+ description: "The title text displayed to the user"
+ default: null
+ text:
+ type: Text
+ description: "The message text displayed to the user"
+ # This should never be defaulted.
+ default: ""
+ is-control:
+ type: Boolean
+ description: "Indicates if this message is the control message, if true shouldn't be displayed"
+ default: false
+ experiment:
+ type: Option<ExperimentSlug>
+ description: The slug of the experiment that this message came from.
+ default: null
+ button-label:
+ type: Option<Text>
+ description: >
+ The text on the button. If no text
+ is present, the whole message is clickable.
+ default: null
+ style:
+ type: StyleName
+ description: >
+ The style as described in a
+ `StyleData` from the styles table.
+ default: DEFAULT
+ surface:
+ description:
+ The surface identifier for this message.
+ type: SurfaceName
+ default: homescreen
+ trigger-if-all:
+ type: List<TriggerName>
+ description: >
+ A list of strings corresponding to
+ targeting expressions. The message will be
+ shown if all expressions are `true`.
+ default: []
+ exclude-if-any:
+ type: List<TriggerName>
+ description: >
+ A list of strings corresponding to
+ targeting expressions. The message will not be
+ shown if any of the expressions are `true`.
+ default: [ ]
+ StyleData:
+ description: >
+ A group of properties (predominantly visual) to
+ describe the style of the message.
+ fields:
+ priority:
+ type: Int
+ description: >
+ The importance of this message.
+ 0 is not very important, 100 is very important.
+ default: 50
+ max-display-count:
+ type: Int
+ description: >
+ How many sessions will this message be shown to the user
+ before it is expired.
+ default: 5
+ NotificationConfig:
+ description: Attributes controlling the global configuration of notification messages.
+ fields:
+ refresh-interval:
+ type: Int
+ description: >
+ How often, in minutes, the notification message worker will wake up and check for new
+ messages.
+ default: 240 # 4 hours
+
+enums:
+ ControlMessageBehavior:
+ description: An enum to influence what should be displayed when a control message is selected.
+ variants:
+ show-next-message:
+ description: The next eligible message should be shown.
+ show-none:
+ description: The surface should show no message.
diff --git a/mobile/android/android-components/components/service/nimbus/metrics.yaml b/mobile/android/android-components/components/service/nimbus/metrics.yaml
new file mode 100644
index 0000000000..4f02dd76f5
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/metrics.yaml
@@ -0,0 +1,110 @@
+# 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/.
+
+# This file defines the metrics that are recorded by glean telemetry. They are
+# automatically converted to Kotlin code at build time using the `glean_parser`
+# PyPI package.
+---
+
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+
+messaging:
+ message_shown:
+ type: event
+ description: |
+ A message was shown to the user.
+ extra_keys:
+ message_key:
+ description: The id of the message
+ type: string
+ bugs:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ data_reviews:
+ - https://github.com/mozilla-mobile/fenix/pull/24426
+ - https://github.com/mozilla-mobile/firefox-android/pull/1101
+ notification_emails:
+ - android-probes@mozilla.com
+ - cgordon@mozilla.com
+ data_sensitivity:
+ - interaction
+ expires: never
+ message_dismissed:
+ type: event
+ description: |
+ A message was dismissed by the user.
+ extra_keys:
+ message_key:
+ description: The id of the message
+ type: string
+ bugs:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ data_reviews:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ - https://github.com/mozilla-mobile/firefox-android/pull/1101
+ notification_emails:
+ - android-probes@mozilla.com
+ - cgordon@mozilla.com
+ data_sensitivity:
+ - interaction
+ expires: never
+ message_clicked:
+ type: event
+ description: |
+ A message was clicked by the user.
+ extra_keys:
+ message_key:
+ description: The id of the message
+ type: string
+ action_uuid:
+ description: The uuid of the action
+ type: string
+ bugs:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ data_reviews:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ - https://github.com/mozilla-mobile/firefox-android/pull/1101
+ notification_emails:
+ - android-probes@mozilla.com
+ - cgordon@mozilla.com
+ data_sensitivity:
+ - interaction
+ expires: never
+ message_expired:
+ type: event
+ description: |
+ A message maxDisplayCount has been surpassed.
+ extra_keys:
+ message_key:
+ description: The id of the message
+ type: string
+ bugs:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ data_reviews:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ - https://github.com/mozilla-mobile/firefox-android/pull/1101
+ notification_emails:
+ - android-probes@mozilla.com
+ - cgordon@mozilla.com
+ data_sensitivity:
+ - interaction
+ expires: never
+ malformed:
+ type: event
+ description: |
+ A message was malformed.
+ extra_keys:
+ message_key:
+ description: The id of the message
+ type: string
+ bugs:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ data_reviews:
+ - https://github.com/mozilla-mobile/fenix/issues/24224
+ - https://github.com/mozilla-mobile/firefox-android/pull/1101
+ notification_emails:
+ - android-probes@mozilla.com
+ - cgordon@mozilla.com
+ data_sensitivity:
+ - interaction
+ expires: never
diff --git a/mobile/android/android-components/components/service/nimbus/proguard-rules-consumer.pro b/mobile/android/android-components/components/service/nimbus/proguard-rules-consumer.pro
new file mode 100644
index 0000000000..4865f3c386
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/proguard-rules-consumer.pro
@@ -0,0 +1,4 @@
+# ProGuard rules for consumers of this library.
+
+# Experiments specific protections
+-keep class mozilla.components.service.nimbus.** { *; }
diff --git a/mobile/android/android-components/components/service/nimbus/proguard-rules.pro b/mobile/android/android-components/components/service/nimbus/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/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/service/nimbus/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/nimbus/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1eccdee26a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/Nimbus.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/Nimbus.kt
new file mode 100644
index 0000000000..f4f0f0e42e
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/Nimbus.kt
@@ -0,0 +1,78 @@
+/* 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.service.nimbus
+
+import android.content.Context
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import org.mozilla.experiments.nimbus.EnrolledExperiment
+import org.mozilla.experiments.nimbus.NimbusAppInfo
+import org.mozilla.experiments.nimbus.NimbusDelegate
+import org.mozilla.experiments.nimbus.NimbusDeviceInfo
+import org.mozilla.experiments.nimbus.NimbusInterface
+import org.mozilla.experiments.nimbus.NimbusServerSettings
+import org.mozilla.experiments.nimbus.Nimbus as ApplicationServicesNimbus
+
+/**
+ * Union of NimbusInterface which comes from another repo, and Observable.
+ *
+ * This only exists to allow the [Nimbus] class to be interchangeable [NimbusDisabled] class below.
+ */
+interface NimbusApi : NimbusInterface, Observable<NimbusInterface.Observer>
+
+// Re-export these classes which were in this package previously.
+// Clients which used these classes do not need to change.
+typealias NimbusAppInfo = NimbusAppInfo
+typealias NimbusServerSettings = NimbusServerSettings
+
+/**
+ * This is the main entry point to the Nimbus experiment subsystem.
+ *
+ * It can only be run after Glean has been set up, the megazord has finished loading, and viaduct
+ * has been initialized.
+ */
+class Nimbus(
+ context: Context,
+ appInfo: NimbusAppInfo,
+ coenrollingFeatureIds: List<String> = listOf(),
+ server: NimbusServerSettings?,
+ deviceInfo: NimbusDeviceInfo = NimbusDeviceInfo.default(),
+ delegate: NimbusDelegate = NimbusDelegate.default(),
+ private val observable: Observable<NimbusInterface.Observer> = ObserverRegistry(),
+) : ApplicationServicesNimbus(
+ context = context,
+ appInfo = appInfo,
+ coenrollingFeatureIds = coenrollingFeatureIds,
+ server = server,
+ deviceInfo = deviceInfo,
+ delegate = delegate,
+ observer = Observer(observable),
+),
+ NimbusApi,
+ Observable<NimbusInterface.Observer> by observable {
+ private class Observer(val observable: Observable<NimbusInterface.Observer>) : NimbusInterface.Observer {
+ override fun onExperimentsFetched() {
+ observable.notifyObservers { onExperimentsFetched() }
+ }
+
+ override fun onUpdatesApplied(updated: List<EnrolledExperiment>) {
+ observable.notifyObservers { onUpdatesApplied(updated) }
+ }
+ }
+}
+
+/**
+ * An empty implementation of the `NimbusInterface` to allow clients who have not enabled Nimbus (either
+ * by feature flags, or by not using a server endpoint.
+ *
+ * Any implementations using this class will report that the user has not been enrolled into any
+ * experiments, and will not report anything to Glean. Importantly, any calls to
+ * `getExperimentBranch(slug)` will return `null`, i.e. as if the user is not enrolled into the
+ * experiment.
+ */
+class NimbusDisabled(
+ override val context: Context,
+ private val observable: Observable<NimbusInterface.Observer> = ObserverRegistry(),
+) : NimbusApi, Observable<NimbusInterface.Observer> by observable
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/NimbusBuilder.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/NimbusBuilder.kt
new file mode 100644
index 0000000000..ca4af5e51b
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/NimbusBuilder.kt
@@ -0,0 +1,54 @@
+/* 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.service.nimbus
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.utils.NamedThreadFactory
+import org.mozilla.experiments.nimbus.AbstractNimbusBuilder
+import org.mozilla.experiments.nimbus.NimbusDelegate
+import java.util.concurrent.Executors
+
+private val logger = Logger("service/Nimbus")
+
+/**
+ * Class to build instances of Nimbus.
+ *
+ * This _does not_ invoke any networking calls on the subsequent [Nimbus] object, so may safely
+ * used before the engine is warmed up.
+ */
+class NimbusBuilder(context: Context) : AbstractNimbusBuilder<NimbusApi>(context) {
+ override fun createDelegate(): NimbusDelegate =
+ NimbusDelegate(
+ dbScope = createNamedCoroutineScope("NimbusDbScope"),
+ fetchScope = createNamedCoroutineScope("NimbusFetchScope"),
+ errorReporter = errorReporter,
+ logger = { logger.info(it) },
+ )
+
+ override fun newNimbus(
+ appInfo: NimbusAppInfo,
+ serverSettings: NimbusServerSettings?,
+ ) = Nimbus(
+ context,
+ appInfo = appInfo,
+ coenrollingFeatureIds = getCoenrollingFeatureIds(),
+ server = serverSettings,
+ deviceInfo = createDeviceInfo(),
+ delegate = createDelegate(),
+ ).apply {
+ this.register(createObserver())
+ }
+
+ override fun newNimbusDisabled() = NimbusDisabled(context)
+}
+
+private fun createNamedCoroutineScope(name: String) = CoroutineScope(
+ Executors.newSingleThreadExecutor(
+ NamedThreadFactory(name),
+ ).asCoroutineDispatcher(),
+)
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/NimbusUtils.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/NimbusUtils.kt
new file mode 100644
index 0000000000..ed090b14b2
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/NimbusUtils.kt
@@ -0,0 +1,20 @@
+/* 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.service.nimbus
+
+import org.mozilla.experiments.nimbus.NimbusMessagingHelperInterface
+import org.mozilla.experiments.nimbus.internal.NimbusException
+
+/**
+ * Extension method that returns true when the condition is evaluated to true, and false otherwise
+ * @param condition The condition given as String.
+ */
+fun NimbusMessagingHelperInterface.evalJexlSafe(
+ condition: String,
+) = try {
+ evalJexl(condition)
+} catch (e: NimbusException.EvaluationException) {
+ false
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/JexlAttributeProvider.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/JexlAttributeProvider.kt
new file mode 100644
index 0000000000..9213e8568c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/JexlAttributeProvider.kt
@@ -0,0 +1,21 @@
+/* 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.service.nimbus.messaging
+
+import android.content.Context
+import org.json.JSONObject
+
+/**
+ * A provider that will be used to evaluate if message is eligible to be shown.
+ */
+interface JexlAttributeProvider {
+ /**
+ * Returns a [JSONObject] that contains all the custom attributes, evaluated when the function
+ * was called.
+ *
+ * This is used to drive display triggers of messages.
+ */
+ fun getCustomAttributes(context: Context): JSONObject
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/Message.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/Message.kt
new file mode 100644
index 0000000000..46aa647c92
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/Message.kt
@@ -0,0 +1,82 @@
+/* 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.service.nimbus.messaging
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * A data class that holds a representation of a message from Nimbus.
+ *
+ * In order to be eligible to be shown, all `triggerIfAll` expressions
+ * AND none of the `excludeIfAny` expressions must evaluate to `true`.
+ *
+ * @param id identifies a message as unique.
+ * @param data Data information provided from Nimbus.
+ * @param action A strings that represents which action should be performed
+ * after a message is clicked.
+ * @param style Indicates how a message should be styled.
+ * @param triggerIfAll A list of strings corresponding to JEXL targeting expressions. The message
+ * will be shown if _all_ expressions evaluate to `true`.
+ * @param excludeIfAny A list of strings corresponding to JEXL targeting expressions. The message
+ * will _not_ be shown if _any_ expressions evaluate to `true`.
+ * @param metadata Metadata that help to identify if a message should shown.
+ */
+data class Message(
+ val id: String,
+ internal val data: MessageData,
+ internal val action: String,
+ internal val style: StyleData,
+ internal val triggerIfAll: List<String>,
+ internal val excludeIfAny: List<String> = listOf(),
+ internal val metadata: Metadata,
+) {
+ val text: String
+ get() = data.text
+
+ val title: String?
+ get() = data.title
+
+ val buttonLabel: String?
+ get() = data.buttonLabel
+
+ val priority: Int
+ get() = style.priority
+
+ val surface: MessageSurfaceId
+ get() = data.surface
+
+ val isExpired: Boolean
+ get() = metadata.displayCount >= style.maxDisplayCount
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ val displayCount: Int
+ get() = metadata.displayCount
+
+ /**
+ * Returns true if the passed boot id, taken from [BootUtils] matches the one associated
+ * with this message when it was last displayed.
+ */
+ fun hasShownThisCycle(bootId: String) = bootId == metadata.latestBootIdentifier
+
+ /**
+ * A data class that holds metadata that help to identify if a message should shown.
+ *
+ * @param id identifies a message as unique.
+ * @param displayCount Indicates how many times a message is displayed.
+ * @param pressed Indicates if a message has been clicked.
+ * @param dismissed Indicates if a message has been closed.
+ * @param lastTimeShown A timestamp indicating when was the last time, the message was shown.
+ * @param latestBootIdentifier A unique boot identifier for when the message was last displayed
+ * (this may be a boot count or a boot id).
+ */
+ data class Metadata(
+ val id: String,
+ val displayCount: Int = 0,
+ val pressed: Boolean = false,
+ val dismissed: Boolean = false,
+ val lastTimeShown: Long = 0L,
+ val latestBootIdentifier: String? = null,
+ )
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageMetadataStorage.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageMetadataStorage.kt
new file mode 100644
index 0000000000..619c5607b1
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageMetadataStorage.kt
@@ -0,0 +1,26 @@
+/* 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.service.nimbus.messaging
+
+/**
+ * A storage that persists [Message.Metadata] into disk.
+ */
+interface MessageMetadataStorage {
+ /**
+ * Provide all the message metadata saved in the storage.
+ */
+ suspend fun getMetadata(): Map<String, Message.Metadata>
+
+ /**
+ * Given a [metadata] add the message metadata on the storage.
+ * @return the added message on the [MessageMetadataStorage]
+ */
+ suspend fun addMetadata(metadata: Message.Metadata): Message.Metadata
+
+ /**
+ * Given a [metadata] update the message metadata on the storage.
+ */
+ suspend fun updateMetadata(metadata: Message.Metadata)
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageSurfaceId.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageSurfaceId.kt
new file mode 100644
index 0000000000..e7f419c455
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageSurfaceId.kt
@@ -0,0 +1,10 @@
+/* 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.service.nimbus.messaging
+
+/**
+ * The identity of a message surface
+ */
+typealias MessageSurfaceId = String
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt
new file mode 100644
index 0000000000..391dfe0cb7
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt
@@ -0,0 +1,130 @@
+/* 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.service.nimbus.messaging
+
+import android.content.Intent
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.service.nimbus.GleanMetrics.Messaging as GleanMessaging
+
+/**
+ * Bookkeeping for message actions in terms of Glean messages and the messaging store.
+ *
+ * @param messagingStorage a NimbusMessagingStorage instance
+ * @param deepLinkScheme the deepLinkScheme for the app
+ * @param now will be used to get the current time
+ */
+open class NimbusMessagingController(
+ private val messagingStorage: NimbusMessagingStorage,
+ private val deepLinkScheme: String,
+) : NimbusMessagingControllerInterface {
+ /**
+ * Records telemetry and metadata for a newly processed displayed message.
+ */
+ override suspend fun onMessageDisplayed(displayedMessage: Message, bootIdentifier: String?): Message {
+ sendShownMessageTelemetry(displayedMessage.id)
+ val nextMessage = messagingStorage.onMessageDisplayed(displayedMessage, bootIdentifier)
+ if (nextMessage.isExpired) {
+ sendExpiredMessageTelemetry(nextMessage.id)
+ }
+ return nextMessage
+ }
+
+ /**
+ * Called when a message has been dismissed by the user.
+ *
+ * Records a messageDismissed event, and records that the message
+ * has been dismissed.
+ */
+ override suspend fun onMessageDismissed(message: Message) {
+ val messageMetadata = message.metadata
+ sendDismissedMessageTelemetry(messageMetadata.id)
+ val updatedMetadata = messageMetadata.copy(dismissed = true)
+ messagingStorage.updateMetadata(updatedMetadata)
+ }
+
+ /**
+ * Called once the user has clicked on a message.
+ *
+ * This records that the message has been clicked on, but does not record a
+ * glean event. That should be done via [processMessageActionToUri].
+ */
+ override suspend fun onMessageClicked(message: Message) {
+ val messageMetadata = message.metadata
+ val updatedMetadata = messageMetadata.copy(pressed = true)
+ messagingStorage.updateMetadata(updatedMetadata)
+ }
+
+ /**
+ * Create and return the relevant [Intent] for the given [Message].
+ *
+ * @param message the [Message] to create the [Intent] for.
+ * @return an [Intent] using the processed [Message].
+ */
+ override fun getIntentForMessage(message: Message) = Intent(
+ Intent.ACTION_VIEW,
+ processMessageActionToUri(message),
+ )
+
+ /**
+ * Will attempt to get the [Message] for the given [id].
+ *
+ * @param id the [Message.id] of the [Message] to try to match.
+ * @return the [Message] with a matching [id], or null if no [Message] has a matching [id].
+ */
+ override suspend fun getMessage(id: String): Message? {
+ return messagingStorage.getMessage(id)
+ }
+
+ /**
+ * The [message] action needs to be examined for string substitutions
+ * and any `uuid` needs to be recorded in the Glean event.
+ *
+ * We call this `process` as it has a side effect of logging a Glean event while it
+ * creates a URI string for the message action.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ fun processMessageActionToUri(message: Message): Uri {
+ val (uuid, action) = messagingStorage.generateUuidAndFormatMessage(message)
+ sendClickedMessageTelemetry(message.id, uuid)
+
+ return convertActionIntoDeepLinkSchemeUri(action)
+ }
+
+ private fun sendDismissedMessageTelemetry(messageId: String) {
+ GleanMessaging.messageDismissed.record(GleanMessaging.MessageDismissedExtra(messageId))
+ }
+
+ private fun sendShownMessageTelemetry(messageId: String) {
+ GleanMessaging.messageShown.record(GleanMessaging.MessageShownExtra(messageId))
+ }
+
+ private fun sendExpiredMessageTelemetry(messageId: String) {
+ GleanMessaging.messageExpired.record(GleanMessaging.MessageExpiredExtra(messageId))
+ }
+
+ private fun sendClickedMessageTelemetry(messageId: String, uuid: String?) {
+ GleanMessaging.messageClicked.record(
+ GleanMessaging.MessageClickedExtra(messageKey = messageId, actionUuid = uuid),
+ )
+ }
+
+ private fun convertActionIntoDeepLinkSchemeUri(action: String): Uri =
+ if (action.startsWith("://")) {
+ "$deepLinkScheme$action".toUri()
+ } else {
+ action.toUri()
+ }
+
+ override suspend fun getMessages(): List<Message> =
+ messagingStorage.getMessages()
+
+ override suspend fun getNextMessage(surfaceId: MessageSurfaceId) =
+ getNextMessage(surfaceId, getMessages())
+
+ override fun getNextMessage(surfaceId: MessageSurfaceId, messages: List<Message>) =
+ messagingStorage.getNextMessage(surfaceId, messages)
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerInterface.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerInterface.kt
new file mode 100644
index 0000000000..0b7e08046f
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerInterface.kt
@@ -0,0 +1,73 @@
+/* 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.service.nimbus.messaging
+
+import android.content.Intent
+
+/**
+ * API for interacting with the messaging component.
+ *
+ * The primary methods for interacting with the component are:
+ * - [getNextMessage] to get the next [Message] for a given surface.
+ * - [onMessageDisplayed] to be called when the message is displayed.
+ * - [onMessageClicked] and [getIntentForMessage] to be called when the user taps on
+ * the message, and to get the action for the message.
+ * - [onMessageDismissed] to be called when the user dismisses the message.
+ */
+interface NimbusMessagingControllerInterface {
+ /**
+ * Get all messages currently on the system. This includes any that have expired,
+ * dismissed or clicked upon.
+ */
+ suspend fun getMessages(): List<Message>
+
+ /**
+ * Selects the next available message for the given surface from the given
+ * list of messages.
+ */
+ fun getNextMessage(surfaceId: MessageSurfaceId, messages: List<Message>): Message?
+
+ /**
+ * A convenience method for `getNextMessage(surfaceId, getMessages())`.
+ */
+ suspend fun getNextMessage(surfaceId: MessageSurfaceId): Message?
+
+ /**
+ * Records telemetry and metadata for a newly processed displayed message.
+ */
+ suspend fun onMessageDisplayed(displayedMessage: Message, bootIdentifier: String? = null): Message
+
+ /**
+ * Called when a message has been dismissed by the user.
+ *
+ * Records a messageDismissed event, and records that the message
+ * has been dismissed.
+ */
+ suspend fun onMessageDismissed(message: Message)
+
+ /**
+ * Called once the user has clicked on a message.
+ *
+ * This records that the message has been clicked on, but does not record a
+ * glean event. That should be done when calling [getIntentForMessage].
+ */
+ suspend fun onMessageClicked(message: Message)
+
+ /**
+ * Create and return the relevant [Intent] for the given [Message].
+ *
+ * @param message the [Message] to create the [Intent] for.
+ * @return an [Intent] using the processed [Message].
+ */
+ fun getIntentForMessage(message: Message): Intent
+
+ /**
+ * Will attempt to get the [Message] for the given [id].
+ *
+ * @param id the [Message.id] of the [Message] to try to match.
+ * @return the [Message] with a matching [id], or null if no [Message] has a matching [id].
+ */
+ suspend fun getMessage(id: String): Message?
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorage.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorage.kt
new file mode 100644
index 0000000000..9828883337
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorage.kt
@@ -0,0 +1,416 @@
+/* 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.service.nimbus.messaging
+
+import android.content.Context
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import kotlinx.coroutines.runBlocking
+import mozilla.components.support.base.log.logger.Logger
+import org.json.JSONObject
+import org.mozilla.experiments.nimbus.NimbusMessagingHelperInterface
+import org.mozilla.experiments.nimbus.NimbusMessagingInterface
+import org.mozilla.experiments.nimbus.internal.FeatureHolder
+import org.mozilla.experiments.nimbus.internal.NimbusException
+import mozilla.components.service.nimbus.GleanMetrics.Messaging as GleanMessaging
+
+/**
+ * This ID must match the name given in the `messaging.fml.yaml` file, which
+ * itself generates the classname for [mozilla.components.service.nimbus.messaging.FxNimbusMessaging].
+ *
+ * If that ever changes, it should also change here.
+ *
+ * This constant is the id for the messaging feature (the Nimbus feature). We declare it here
+ * so as to afford the best chance of it being changed if a rename operation is needed.
+ *
+ * It is used in the Studies view, to filter out any experiments which only use a messaging surface.
+ */
+const val MESSAGING_FEATURE_ID = "messaging"
+
+/**
+ * Provides messages from [messagingFeature] and combine with the metadata store on [metadataStorage].
+ */
+class NimbusMessagingStorage(
+ private val context: Context,
+ private val metadataStorage: MessageMetadataStorage,
+ private val onMalformedMessage: (String) -> Unit = {
+ GleanMessaging.malformed.record(GleanMessaging.MalformedExtra(it))
+ },
+ private val nimbus: NimbusMessagingInterface,
+ private val messagingFeature: FeatureHolder<Messaging>,
+ private val attributeProvider: JexlAttributeProvider? = null,
+ private val now: () -> Long = { System.currentTimeMillis() },
+) {
+ /**
+ * Contains all malformed messages where they key can be the value or a trigger of the message
+ * and the value is the message id.
+ */
+ @VisibleForTesting
+ val malFormedMap = mutableMapOf<String, String>()
+ private val logger = Logger("MessagingStorage")
+ private val customAttributes: JSONObject
+ get() = attributeProvider?.getCustomAttributes(context) ?: JSONObject()
+
+ /**
+ * Returns a Nimbus message helper, for evaluating JEXL.
+ *
+ * The JEXL context is time-sensitive, so this should be created new for each set of evaluations.
+ *
+ * Since it has a native peer, it should be [destroy]ed after finishing the set of evaluations.
+ */
+ fun createMessagingHelper(): NimbusMessagingHelperInterface =
+ nimbus.createMessageHelper(customAttributes)
+
+ /**
+ * Returns the [Message] for the given [key] or returns null if none found.
+ */
+ suspend fun getMessage(key: String): Message? =
+ createMessage(messagingFeature.value(), key)
+
+ private suspend fun createMessage(featureValue: Messaging, key: String): Message? {
+ val message = createMessageOrNull(featureValue, key)
+ if (message == null) {
+ reportMalformedMessage(key)
+ }
+ return message
+ }
+
+ @Suppress("ReturnCount")
+ private suspend fun createMessageOrNull(featureValue: Messaging, key: String): Message? {
+ val message = featureValue.messages[key] ?: return null
+
+ val action = if (!message.isControl) {
+ if (message.text.isBlank()) {
+ return null
+ }
+ sanitizeAction(message.action, featureValue.actions)
+ ?: return null
+ } else {
+ "CONTROL_ACTION"
+ }
+
+ val triggerIfAll = sanitizeTriggers(message.triggerIfAll, featureValue.triggers) ?: return null
+ val excludeIfAny = sanitizeTriggers(message.excludeIfAny, featureValue.triggers) ?: return null
+ val style = sanitizeStyle(message.style, featureValue.styles) ?: return null
+
+ val storageMetadata = metadataStorage.getMetadata()
+
+ return Message(
+ id = key,
+ data = message,
+ action = action,
+ style = style,
+ triggerIfAll = triggerIfAll,
+ excludeIfAny = excludeIfAny,
+ metadata = storageMetadata[key] ?: addMetadata(key),
+ )
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun reportMalformedMessage(key: String) {
+ messagingFeature.recordMalformedConfiguration(key)
+ onMalformedMessage(key)
+ }
+
+ /**
+ * Returns a list of currently available messages descending sorted by their [StyleData.priority].
+ *
+ * "Currently available" means all messages contained in the Nimbus SDK, validated and denormalized.
+ *
+ * The messages have the JEXL triggers and actions that came from the Nimbus SDK, but these themselves
+ * are not validated at this time.
+ *
+ * The messages also have state attached, which manage how many times the messages has been shown,
+ * and if the user has interacted with it or not.
+ *
+ * The list of messages may also contain control messages which should not be shown to the user.
+ *
+ * All additional filtering of these messages will happen in [getNextMessage].
+ */
+ suspend fun getMessages(): List<Message> {
+ val featureValue = messagingFeature.value()
+ val nimbusMessages = featureValue.messages
+ return nimbusMessages.keys
+ .mapNotNull { key ->
+ createMessage(featureValue, key)
+ }.sortedByDescending {
+ it.style.priority
+ }
+ }
+
+ /**
+ * Returns the next message for this surface.
+ *
+ * Message selection takes into account,
+ * - the message's surface,
+ * - how many times the message has been displayed already
+ * - whether or not the user has interacted with the message already.
+ * - the message eligibility, via JEXL triggers.
+ *
+ * If more than one message for this surface is eligible to be shown, then the
+ * first one to be encountered in [messages] list is returned.
+ */
+ fun getNextMessage(surface: MessageSurfaceId, messages: List<Message>): Message? {
+ val availableMessages = messages
+ .filter {
+ it.surface == surface
+ }
+ .filter {
+ !it.isExpired &&
+ !it.metadata.dismissed &&
+ !it.metadata.pressed
+ }
+ return createMessagingHelper().use {
+ getNextMessage(
+ surface,
+ availableMessages,
+ setOf(),
+ it,
+ )
+ }
+ }
+
+ @Suppress("ReturnCount")
+ private fun getNextMessage(
+ surface: MessageSurfaceId,
+ availableMessages: List<Message>,
+ excluded: Set<String>,
+ helper: NimbusMessagingHelperInterface,
+ ): Message? {
+ val message = availableMessages
+ .filter { !excluded.contains(it.id) }
+ .firstOrNull {
+ try {
+ isMessageEligible(it, helper)
+ } catch (e: NimbusException) {
+ reportMalformedMessage(it.id)
+ false
+ }
+ } ?: return null
+
+ // If this is an experimental message, but not a placebo, then just return the message.
+ if (!message.data.isControl) {
+ return message
+ }
+
+ // This is a control message which we're definitely not going to show to anyone,
+ // however, we need to do the bookkeeping and as if we were.
+ //
+ // Since no one is going to see it, then we need to do it ourselves, here.
+ runBlocking {
+ onMessageDisplayed(message)
+ }
+
+ // This is a control, so we need to either return the next message (there may not be one)
+ // or not display anything.
+ return when (getOnControlBehavior()) {
+ ControlMessageBehavior.SHOW_NEXT_MESSAGE ->
+ getNextMessage(
+ surface,
+ availableMessages,
+ excluded + message.id,
+ helper,
+ )
+
+ ControlMessageBehavior.SHOW_NONE -> null
+ }
+ }
+
+ /**
+ * Record the time and optional [bootIdentifier] of the display of the given message.
+ *
+ * If the message is part of an experiment, then record an exposure event for that
+ * experiment.
+ *
+ * This is determined by the value in the [message.data.experiment] property.
+ */
+ suspend fun onMessageDisplayed(message: Message, bootIdentifier: String? = null): Message {
+ // Record an exposure event if this is an experimental message.
+ val slug = message.data.experiment
+ if (slug != null) {
+ // We know that it's experimental, and we know which experiment it came from.
+ messagingFeature.recordExperimentExposure(slug)
+ } else if (message.data.isControl) {
+ // It's not experimental, but it is a control. This is obviously malformed.
+ reportMalformedMessage(message.id)
+ }
+
+ // Now update the display counts.
+ val updatedMetadata = message.metadata.copy(
+ displayCount = message.metadata.displayCount + 1,
+ lastTimeShown = now(),
+ latestBootIdentifier = bootIdentifier,
+ )
+ val nextMessage = message.copy(
+ metadata = updatedMetadata,
+ )
+ updateMetadata(nextMessage.metadata)
+ return nextMessage
+ }
+
+ /**
+ * Returns a pair of uuid and valid action for the provided [message].
+ *
+ * The message's action-params are appended as query parameters to the action URI,
+ * URI encoding the values as it goes.
+ *
+ * Uses Nimbus' targeting attributes to do basic string interpolation.
+ *
+ * e.g.
+ * `https://example.com/{locale}/whats-new.html?version={app_version}`
+ *
+ * If the string `{uuid}` is detected in the [message]'s action, then it is
+ * replaced with a random UUID. This is returned as the first value of the returned
+ * [Pair].
+ *
+ * The fully resolved (with all substitutions) action is returned as the second value
+ * of the [Pair].
+ */
+ internal fun generateUuidAndFormatMessage(message: Message): Pair<String?, String> =
+ createMessagingHelper().use { helper ->
+ generateUuidAndFormatMessage(message, helper)
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun generateUuidAndFormatMessage(
+ message: Message,
+ helper: NimbusMessagingHelperInterface,
+ ): Pair<String?, String> {
+ // The message action is part or all of a valid URL, likely to be a deeplink.
+ // If it's an internal deeplink, we don't yet know the scheme, so
+ // we'll prepend it later: these should just have a :// prefix right now.
+ // e.g. market://details?id=org.mozilla.blah or ://open
+
+ // We need to construct it from the parts coming in from the message:
+ // the message.action, the message.actionParams and the attribute context.
+ //
+ // Any part of the action may have string params taken from the attribute
+ // context, and a special {uuid}.
+ // e.g. market://details?id={app_id} becomes market://details?id=org.mozilla.blah
+ // If there is a {uuid}, we want to create a uuid for later usage, i.e. recording in Glean
+ var uuid: String? = null
+ fun formatWithUuid(string: String): String {
+ if (uuid == null) {
+ uuid = helper.getUuid(string)
+ }
+ return helper.stringFormat(string, uuid)
+ }
+
+ // We also want to do any string substitutions e.g. locale
+ // or UUID for the action.
+ val action = formatWithUuid(message.action)
+
+ // Now we use a string builder to add the actionParams as query params to the URL.
+ val sb = StringBuilder(action)
+
+ // Before the first query parameter is a `?`, and subsequent ones are `&`.
+ // The action may already have a query parameter.
+ var separator = if (action.contains('?')) {
+ '&'
+ } else {
+ '?'
+ }
+
+ for ((queryParamName, queryParamValue) in message.data.actionParams) {
+ val v = formatWithUuid(queryParamValue)
+ sb
+ .append(separator)
+ .append(queryParamName)
+ .append('=')
+ .append(Uri.encode(v))
+
+ separator = '&'
+ }
+
+ return uuid to sb.toString()
+ }
+
+ /**
+ * Updated the provided [metadata] in the storage.
+ */
+ suspend fun updateMetadata(metadata: Message.Metadata) {
+ metadataStorage.updateMetadata(metadata)
+ }
+
+ @VisibleForTesting
+ internal fun sanitizeAction(
+ unsafeAction: String,
+ nimbusActions: Map<String, String>,
+ ): String? = nimbusActions[unsafeAction]
+
+ @VisibleForTesting
+ internal fun sanitizeTriggers(
+ unsafeTriggers: List<String>,
+ nimbusTriggers: Map<String, String>,
+ ): List<String>? =
+ unsafeTriggers.map {
+ val safeTrigger = nimbusTriggers[it]
+ if (safeTrigger.isNullOrBlank() || safeTrigger.isEmpty()) {
+ return null
+ }
+ safeTrigger
+ }
+
+ @VisibleForTesting
+ internal fun sanitizeStyle(
+ unsafeStyle: String,
+ nimbusStyles: Map<String, StyleData>,
+ ): StyleData? = nimbusStyles[unsafeStyle]
+
+ /**
+ * Return true if the message passed as a parameter is eligible
+ *
+ * Aimed to be used from tests only, but currently public because some tests inside Fenix need
+ * it. This should be set as internal when this bug is fixed:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1823472
+ */
+ @VisibleForTesting
+ fun isMessageEligible(
+ message: Message,
+ helper: NimbusMessagingHelperInterface,
+ ): Boolean {
+ return message.triggerIfAll.all { condition ->
+ evalJexl(message, helper, condition)
+ } && !message.excludeIfAny.any { condition ->
+ evalJexl(message, helper, condition)
+ }
+ }
+
+ private fun evalJexl(
+ message: Message,
+ helper: NimbusMessagingHelperInterface,
+ condition: String,
+ ): Boolean =
+ try {
+ if (malFormedMap.containsKey(condition)) {
+ throw NimbusException.EvaluationException(condition)
+ }
+ helper.evalJexl(condition)
+ } catch (e: NimbusException) {
+ malFormedMap[condition] = message.id
+ logger.info("Unable to evaluate ${message.id} trigger: $condition")
+ throw NimbusException.EvaluationException(condition)
+ }
+
+ @VisibleForTesting
+ internal fun getOnControlBehavior(): ControlMessageBehavior = messagingFeature.value().onControl
+
+ private suspend fun addMetadata(id: String): Message.Metadata {
+ return metadataStorage.addMetadata(
+ Message.Metadata(
+ id = id,
+ ),
+ )
+ }
+}
+
+/**
+ * A helper method to safely destroy the message helper after use.
+ */
+fun <R> NimbusMessagingHelperInterface.use(block: (NimbusMessagingHelperInterface) -> R) =
+ block(this).also {
+ this.destroy()
+ }
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorage.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorage.kt
new file mode 100644
index 0000000000..c31e7e968e
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorage.kt
@@ -0,0 +1,96 @@
+/* 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.service.nimbus.messaging
+
+import android.content.Context
+import android.util.AtomicFile
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.ktx.util.readAndDeserialize
+import mozilla.components.support.ktx.util.writeString
+import org.json.JSONArray
+import org.json.JSONObject
+import java.io.File
+
+internal const val FILE_NAME = "nimbus_messages_metadata.json"
+
+/**
+ * A storage that persists [Message.Metadata] into disk.
+ */
+class OnDiskMessageMetadataStorage(
+ private val context: Context,
+) : MessageMetadataStorage {
+ private val diskCacheLock = Any()
+
+ @VisibleForTesting
+ internal var metadataMap: MutableMap<String, Message.Metadata> = hashMapOf()
+
+ override suspend fun getMetadata(): Map<String, Message.Metadata> {
+ if (metadataMap.isEmpty()) {
+ metadataMap = readFromDisk().toMutableMap()
+ }
+ return metadataMap
+ }
+
+ override suspend fun addMetadata(metadata: Message.Metadata): Message.Metadata {
+ metadataMap[metadata.id] = metadata
+ writeToDisk()
+ return metadata
+ }
+
+ override suspend fun updateMetadata(metadata: Message.Metadata) {
+ addMetadata(metadata)
+ }
+
+ @VisibleForTesting
+ internal fun readFromDisk(): Map<String, Message.Metadata> {
+ synchronized(diskCacheLock) {
+ return getFile().readAndDeserialize {
+ JSONArray(it).toMetadataMap()
+ } ?: emptyMap()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun writeToDisk() {
+ synchronized(diskCacheLock) {
+ val json = metadataMap.values.toList().fold("") { acc, next ->
+ if (acc.isEmpty()) {
+ next.toJson()
+ } else {
+ "$acc,${next.toJson()}"
+ }
+ }
+ getFile().writeString { "[$json]" }
+ }
+ }
+
+ private fun getFile(): AtomicFile {
+ return AtomicFile(File(context.filesDir, FILE_NAME))
+ }
+}
+
+internal fun JSONArray.toMetadataMap(): Map<String, Message.Metadata> {
+ return (0 until length()).map { index ->
+ getJSONObject(index).toMetadata()
+ }.associateBy {
+ it.id
+ }
+}
+
+@Suppress("MaxLineLength") // To avoid adding any extra space to the string.
+internal fun Message.Metadata.toJson(): String {
+ return """{"id":"$id","displayCount":$displayCount,"pressed":$pressed,"dismissed":$dismissed,"lastTimeShown":$lastTimeShown,"latestBootIdentifier":"$latestBootIdentifier"}"""
+}
+
+internal fun JSONObject.toMetadata(): Message.Metadata {
+ return Message.Metadata(
+ id = optString("id"),
+ displayCount = optInt("displayCount"),
+ pressed = optBoolean("pressed"),
+ dismissed = optBoolean("dismissed"),
+ lastTimeShown = optLong("lastTimeShown"),
+ latestBootIdentifier = optString("latestBootIdentifier"),
+ )
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchAdapter.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchAdapter.kt
new file mode 100644
index 0000000000..cfa3ddd94a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchAdapter.kt
@@ -0,0 +1,98 @@
+/* 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.service.nimbus.ui
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.service.nimbus.R
+import org.mozilla.experiments.nimbus.Branch
+
+/**
+ * An adapter for displaying a experimental branch for a Nimbus experiment.
+ *
+ * @param nimbusBranchesDelegate An instance of [NimbusBranchesAdapterDelegate] that provides
+ * methods for handling the Nimbus branch items.
+ */
+class NimbusBranchAdapter(
+ private val nimbusBranchesDelegate: NimbusBranchesAdapterDelegate,
+) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+
+ // The list of [Branch]s to display.
+ private var branches: List<Branch> = emptyList()
+
+ // The selected [Branch] slug to highlight.
+ private var selectedBranch: String = ""
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int,
+ ): RecyclerView.ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.mozac_service_nimbus_branch_item, parent, false)
+ val selectedIconView: ImageView = view.findViewById(R.id.selected_icon)
+ val titleView: TextView = view.findViewById(R.id.nimbus_branch_name)
+ val summaryView: TextView = view.findViewById(R.id.nimbus_branch_description)
+
+ return NimbusBranchItemViewHolder(
+ view,
+ nimbusBranchesDelegate,
+ selectedIconView,
+ titleView,
+ summaryView,
+ )
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ holder as NimbusBranchItemViewHolder
+ holder.bind(branches[position], selectedBranch)
+ }
+
+ override fun getItemCount(): Int = branches.size
+
+ /**
+ * Updates the list of [Branch]s and the selected branch that are displayed.
+ *
+ * @param branches The list of [Branch]s to display.
+ * @param selectedBranch The [Branch] slug to highlight.
+ */
+ fun updateData(branches: List<Branch>, selectedBranch: String) {
+ val diffUtil = DiffUtil.calculateDiff(
+ NimbusBranchesDiffUtil(
+ oldBranches = this.branches,
+ newBranches = branches,
+ oldSelectedBranch = this.selectedBranch,
+ newSelectedBranch = selectedBranch,
+ ),
+ )
+
+ this.branches = branches
+ this.selectedBranch = selectedBranch
+
+ diffUtil.dispatchUpdatesTo(this)
+ }
+}
+
+internal class NimbusBranchesDiffUtil(
+ private val oldBranches: List<Branch>,
+ private val newBranches: List<Branch>,
+ private val oldSelectedBranch: String,
+ private val newSelectedBranch: String,
+) : DiffUtil.Callback() {
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ oldBranches[oldItemPosition].slug == newBranches[newItemPosition].slug &&
+ oldSelectedBranch == newSelectedBranch
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ oldBranches[oldItemPosition] == newBranches[newItemPosition]
+
+ override fun getOldListSize(): Int = oldBranches.size
+
+ override fun getNewListSize(): Int = newBranches.size
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchItemViewHolder.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchItemViewHolder.kt
new file mode 100644
index 0000000000..69cdc7f1cc
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchItemViewHolder.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.service.nimbus.ui
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import org.mozilla.experiments.nimbus.Branch
+
+/**
+ * A view holder for displaying a branch of a Nimbus experiment.
+ */
+class NimbusBranchItemViewHolder(
+ view: View,
+ private val nimbusBranchesDelegate: NimbusBranchesAdapterDelegate,
+ private val selectedIconView: ImageView,
+ private val titleView: TextView,
+ private val summaryView: TextView,
+) : RecyclerView.ViewHolder(view) {
+
+ internal fun bind(branch: Branch, selectedBranch: String) {
+ selectedIconView.isVisible = selectedBranch == branch.slug
+ titleView.text = branch.slug
+ summaryView.text = branch.slug
+
+ itemView.setOnClickListener {
+ nimbusBranchesDelegate.onBranchItemClicked(branch)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchesAdapterDelegate.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchesAdapterDelegate.kt
new file mode 100644
index 0000000000..04a79dfff4
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusBranchesAdapterDelegate.kt
@@ -0,0 +1,19 @@
+/* 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.service.nimbus.ui
+
+import org.mozilla.experiments.nimbus.Branch
+
+/**
+ * Provides method for handling the branch items in the Nimbus branches view.
+ */
+interface NimbusBranchesAdapterDelegate {
+ /**
+ * Handler for when a branch item is clicked.
+ *
+ * @param branch The [Branch] that was clicked.
+ */
+ fun onBranchItemClicked(branch: Branch) = Unit
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentAdapter.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentAdapter.kt
new file mode 100644
index 0000000000..848bacb7df
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentAdapter.kt
@@ -0,0 +1,49 @@
+/* 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.service.nimbus.ui
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import mozilla.components.service.nimbus.R
+import org.mozilla.experiments.nimbus.AvailableExperiment
+
+/**
+ * An adapter for displaying nimbus experiment items.
+ */
+class NimbusExperimentAdapter(
+ private val nimbusExperimentsDelegate: NimbusExperimentsAdapterDelegate,
+ experiments: List<AvailableExperiment>,
+) : ListAdapter<AvailableExperiment, NimbusExperimentItemViewHolder>(DiffCallback) {
+
+ init {
+ submitList(experiments)
+ }
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int,
+ ): NimbusExperimentItemViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.mozac_service_nimbus_experiment_item, parent, false)
+ val titleView: TextView = view.findViewById(R.id.nimbus_experiment_name)
+ val summaryView: TextView = view.findViewById(R.id.nimbus_experiment_description)
+ return NimbusExperimentItemViewHolder(view, nimbusExperimentsDelegate, titleView, summaryView)
+ }
+
+ override fun onBindViewHolder(holder: NimbusExperimentItemViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ private object DiffCallback : DiffUtil.ItemCallback<AvailableExperiment>() {
+ override fun areContentsTheSame(oldItem: AvailableExperiment, newItem: AvailableExperiment) =
+ oldItem == newItem
+
+ override fun areItemsTheSame(oldItem: AvailableExperiment, newItem: AvailableExperiment) =
+ oldItem.slug == newItem.slug
+ }
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentItemViewHolder.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentItemViewHolder.kt
new file mode 100644
index 0000000000..2d36208313
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentItemViewHolder.kt
@@ -0,0 +1,30 @@
+/* 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.service.nimbus.ui
+
+import android.view.View
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import org.mozilla.experiments.nimbus.AvailableExperiment
+
+/**
+ * A view holder for displaying Nimbus experiment items.
+ */
+class NimbusExperimentItemViewHolder(
+ view: View,
+ private val nimbusExperimentsDelegate: NimbusExperimentsAdapterDelegate,
+ private val titleView: TextView,
+ private val summaryView: TextView,
+) : RecyclerView.ViewHolder(view) {
+
+ internal fun bind(experiment: AvailableExperiment) {
+ titleView.text = experiment.userFacingName
+ summaryView.text = experiment.userFacingDescription
+
+ itemView.setOnClickListener {
+ nimbusExperimentsDelegate.onExperimentItemClicked(experiment)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentsAdapterDelegate.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentsAdapterDelegate.kt
new file mode 100644
index 0000000000..e51578aaf8
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/ui/NimbusExperimentsAdapterDelegate.kt
@@ -0,0 +1,19 @@
+/* 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.service.nimbus.ui
+
+import org.mozilla.experiments.nimbus.AvailableExperiment
+
+/**
+ * Provides methods for handling the experiment items in the Nimbus experiments manager.
+ */
+interface NimbusExperimentsAdapterDelegate {
+ /**
+ * Handler for when an experiment item is clicked.
+ *
+ * @param experiment The [AvailableExperiment] that was clicked.
+ */
+ fun onExperimentItemClicked(experiment: AvailableExperiment) = Unit
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_branch_item.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_branch_item.xml
new file mode 100644
index 0000000000..1567c38647
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_branch_item.xml
@@ -0,0 +1,58 @@
+<?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/. -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="48dp">
+
+ <ImageView
+ android:id="@+id/selected_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:importantForAccessibility="no"
+ android:visibility="visible"
+ app:tint="?android:attr/textColorPrimary"
+ app:srcCompat="@drawable/mozac_ic_checkmark_24"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/nimbus_branch_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="32dp"
+ android:layout_marginEnd="16dp"
+ android:textAlignment="viewStart"
+ android:textSize="16sp"
+ app:layout_goneMarginStart="72dp"
+ app:layout_constraintBottom_toTopOf="@id/nimbus_branch_description"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/selected_icon"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="Control Branch" />
+
+ <TextView
+ android:id="@+id/nimbus_branch_description"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="32dp"
+ android:layout_marginEnd="16dp"
+ android:textAlignment="viewStart"
+ android:textSize="12sp"
+ android:visibility="visible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_goneMarginStart="72dp"
+ app:layout_constraintStart_toEndOf="@id/selected_icon"
+ app:layout_constraintTop_toBottomOf="@id/nimbus_branch_name"
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="This is control." />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiment_details.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiment_details.xml
new file mode 100644
index 0000000000..fb1aaf9950
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiment_details.xml
@@ -0,0 +1,16 @@
+<?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/. -->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/nimbus_experiment_branches_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="2dp" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiment_item.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiment_item.xml
new file mode 100644
index 0000000000..ec4e3a9a6f
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiment_item.xml
@@ -0,0 +1,57 @@
+<?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/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/nimbus_experiment_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp">
+
+ <LinearLayout
+ android:id="@+id/nimbus_experiment_details_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginEnd="8dp"
+ android:orientation="vertical"
+ android:paddingStart="8dp"
+ android:paddingTop="8dp"
+ android:paddingEnd="8dp"
+ android:paddingBottom="8dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/nimbus_experiment_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textSize="16sp"
+ tools:text="Test CFR Experiment" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/nimbus_experiment_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ tools:text="If we do this/build this/create this change in the experiment for these users, then we will see this outcome." />
+
+ </LinearLayout>
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiments.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiments.xml
new file mode 100644
index 0000000000..511d4332db
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/layout/mozac_service_nimbus_experiments.xml
@@ -0,0 +1,28 @@
+<?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/. -->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/nimbus_experiments_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="2dp" />
+
+ <TextView
+ android:id="@+id/nimbus_experiments_empty_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="4dp"
+ android:layout_gravity="center"
+ android:text="@string/mozac_service_nimbus_no_experiments"
+ android:textAlignment="center"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ android:visibility="gone"/>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..c471842b2c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">እዚህ ምንም ሙከራዎች የሉም</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..a270594e84
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ar/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">ما من تجارب هنا</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..3cb6071229
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nun hai nengún esperimentu</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..47a749951b
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">بورادا تجروبه یوخدور</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..b66dfa3260
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Tén wénten ékspérimen driki</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..2710a62d24
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Эксперыментаў няма</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..56c3405ebe
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Няма налични експерименти</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..573428755c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Tamm arnod ebet amañ</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..f5db38489b
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Ovdje nema eksperimenata</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..e435d23410
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No hi ha cap experiment</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..a5372c21e2
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Majun tojtob\'enel wawe\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..b2f67da782
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Walay mga eksperimento dinhi</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..b47389014a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">هیچ تاقیکردنەوەیەک نیە لێرە</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..4c2c008bb7
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nisuna sperimentazione quì</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..83086a59cb
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Žádné experimenty</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..cc489b5882
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Dim arbrofion yma</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..df0cb7f34c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Ingen eksperimenter her</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..16a327461d
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Keine Experimente vorhanden</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..7f70d7c37d
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Žedne eksperimenty how</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..5395d1dc98
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Δεν υπάρχουν πειράματα</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..3125bfbf4a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No experiments here</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..3125bfbf4a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No experiments here</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..d01b12d0be
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Ne estas eksperimentoj ĉi tie</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..f1ae0a6f22
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No hay experimentos aquí</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..f1ae0a6f22
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No hay experimentos aquí</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..f1ae0a6f22
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No hay experimentos aquí</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..f1ae0a6f22
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No hay experimentos aquí</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..f1ae0a6f22
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No hay experimentos aquí</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..e2cc4b13a1
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Siin pole eksperimente</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..ff662a5acb
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Esperimenturik ez hemen</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..fc543f9b6c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">هیچ آزمایشی این‌جا نیست</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..7ede19351d
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Täältä ei löydy kokeiluja</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..1127156a3c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Aucune expérience ici</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..48bc258f77
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nissun esperiment disponibil</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..702b8c2edf
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Gjin eksperiminten hjir</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..ffce34e92b
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Chan eil deuchainn an-seo</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..9451c74fd7
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Aquí non hai experimentos</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..ac819bbcc5
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Ndaipóri tembiapopyahu ápe</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..6c52400c4d
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">यहां कोई प्रयोग नहीं</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..f5db38489b
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Ovdje nema eksperimenata</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..8b51206a10
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Žane eksperimenty tu</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..a2764a06be
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Itt nincsenek kísérletek</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..ef010a3518
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Այստեղ փորձեր չկան</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..2e94be9b97
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nulle experimentos hic</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..372b741190
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Tidak ada eksperimen di sini</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..b9522b4049
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Engar tilraunir hér</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..27e9558e43
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nessun esperimento disponibile</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..87c8b04455
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">אין כאן ניסויים</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..3a9af1f6a6
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">実験は行われていません</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..b60345950e
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">კვლევები არაა</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..4e653f3d12
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Bul jerde tájiriybeler joq</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..614ee2c945
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Ulac tirma da</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..054b3d3f74
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Мұнда эксперименттер жоқ</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..bcdbb78082
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Niha cerebe tune ye</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..596201714a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-kn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">ಇಲ್ಲಿ ಯಾವುದೇ ಪ್ರಯೋಗಗಳಿಲ್ಲ</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..19b797951c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">실험 없음</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..928dfcb6fa
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">ບໍ່ມີການທົດລອງຢູ່ທີ່ນີ້</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..0f1c38f2e2
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Eksperimentų čia nėra</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..328f26e311
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-my/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">ဤ နေရာတွင် စမ်းသပ်မှုများ မရှိပါ။</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..7723d719ce
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Det finnes ingen eksperimenter</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..36bd5f58b6
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">यहाँ कुनै प्रयोगहरु छैनन्</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..20e0a03446
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Geen experimenten hier</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..76b520c14f
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Det finst ingen eksperiment her</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..0363dc3931
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Cap d’experimentacion aquí</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..5af8b944e8
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">ਕੋਈ ਤਜਰਬਾ ਨਹੀਂ ਹੈ</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..143af8eeb3
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">اِتھے کوئی تجرںے نہیں ہن</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..b28e5e3c5b
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nie ma tu żadnych eksperymentów</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..6a4bf369ec
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nenhum experimento aqui</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..9cdcb7a381
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Sem experiências disponíveis</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..2dbff8d044
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nagins experiments disponibels</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..2747339cc6
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Здесь нет экспериментов</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..d73005300a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">ᱪᱮᱫ ᱦᱚᱸ ᱮᱠᱥᱯᱮᱨᱤᱢᱮᱱᱴ ᱱᱚᱰᱮ ᱵᱟᱝ ᱦᱩᱭᱩᱜ ᱠᱟᱱᱟ ᱾</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..2d7106f6b8
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nissunu esperimentu inoghe</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..91458c666c
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">මෙහි අත්හදා බැලීම් නැත</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..cfe2804573
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Žiadne experimenty</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..0135c35baf
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">اتھاں کوئی تجربے کائنی</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..bf11b3070a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Tu ni poskusov</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..bfa8e35c39
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">S’ka eksperimente këtu</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..a65ed53580
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Овде нема експеримената</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..4081d44c33
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Taya uji coba di dieu</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..903f10c246
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Det finns inga experiment</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..eb8b0f6b49
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-te/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">ఇక్కడ ప్రయోగాలేమీ లేవు</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..7622d210f7
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Дар ин ҷо ягон озмоиш нест</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..8b8ccb68cd
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">ไม่มีการทดลองที่นี่</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..42f0f679cd
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-tl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Walang mga eksperimento dito</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..d32b690f73
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Şu anda deney yok</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..0addf2cd36
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Nitāj ēkspērimênto hua hiūj nan</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..f2edcfa4b8
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Монда экспериментлар юк</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..1ac7a88698
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">بۇ يەردە سىناق يوق</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..098db371e9
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Досліджень немає</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..3a58ee296d
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-ur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">یہاں کوئی تجربات نہیں ہیں</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..b13185d87b
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Bu erda hech qanday tajriba yoʻq</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..363f692689
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Hiện không có thử nghiệm nào ở đây</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..3ae1b05c96
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">Kò sí ìṣàyẹ̀wò níbí</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..2ca8dea7b9
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">暂无实验项</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..9d7bae1ef2
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">目前沒有實驗項目</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/main/res/values/strings.xml b/mobile/android/android-components/components/service/nimbus/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..ae3ee392d2
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Text displayed when there are no experiments to be shown -->
+ <string name="mozac_service_nimbus_no_experiments">No experiments here</string>
+</resources>
diff --git a/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/NimbusTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/NimbusTest.kt
new file mode 100644
index 0000000000..64ea914a78
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/NimbusTest.kt
@@ -0,0 +1,47 @@
+/* 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.service.nimbus
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.experiments.nimbus.NimbusAppInfo
+import org.mozilla.experiments.nimbus.NimbusDelegate
+import org.mozilla.experiments.nimbus.NimbusInterface
+
+@RunWith(AndroidJUnit4::class)
+class NimbusTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private val appInfo = NimbusAppInfo(
+ appName = "NimbusUnitTest",
+ channel = "test",
+ )
+
+ @Test
+ fun `Nimbus disabled and enabled can have observers registered on it`() {
+ val enabled: NimbusApi = Nimbus(context, appInfo, listOf(), null, delegate = NimbusDelegate.default())
+ val disabled: NimbusApi = NimbusDisabled(context)
+
+ val observer = object : NimbusInterface.Observer {}
+
+ enabled.register(observer)
+ disabled.register(observer)
+ }
+
+ @Test
+ fun `NimbusDisabled is empty`() {
+ val nimbus: NimbusApi = NimbusDisabled(context)
+ nimbus.fetchExperiments()
+ nimbus.applyPendingExperiments()
+ assertTrue("getActiveExperiments should be empty", nimbus.getActiveExperiments().isEmpty())
+ assertEquals(null, nimbus.getExperimentBranch("test-experiment"))
+ }
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerTest.kt
new file mode 100644
index 0000000000..00cdb14d6a
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerTest.kt
@@ -0,0 +1,276 @@
+/* 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.service.nimbus.messaging
+
+import android.content.Intent
+import androidx.core.net.toUri
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.glean.testing.GleanTestRule
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mozilla.experiments.nimbus.NullVariables
+import org.robolectric.RobolectricTestRunner
+import java.util.UUID
+import mozilla.components.service.nimbus.GleanMetrics.Messaging as GleanMessaging
+
+@RunWith(RobolectricTestRunner::class)
+class NimbusMessagingControllerTest {
+
+ private val storage: NimbusMessagingStorage = mock(NimbusMessagingStorage::class.java)
+
+ @get:Rule
+ val gleanTestRule = GleanTestRule(testContext)
+
+ private val coroutinesTestRule = MainCoroutineRule()
+ private val coroutineScope = coroutinesTestRule.scope
+
+ private val deepLinkScheme = "deepLinkScheme"
+ private val controller = NimbusMessagingController(storage, deepLinkScheme)
+
+ @Before
+ fun setup() {
+ NullVariables.instance.setContext(testContext)
+ }
+
+ @Test
+ fun `GIVEN message not expired WHEN calling onMessageDisplayed THEN record a messageShown event and update storage`() =
+ coroutineScope.runTest {
+ val message = createMessage("id-1", style = StyleData(maxDisplayCount = 2))
+ val displayedMessage = createMessage("id-1", style = StyleData(maxDisplayCount = 2), displayCount = 1)
+ `when`(storage.onMessageDisplayed(eq(message), any())).thenReturn(displayedMessage)
+
+ // Assert telemetry is initially null
+ assertNull(GleanMessaging.messageShown.testGetValue())
+ assertNull(GleanMessaging.messageExpired.testGetValue())
+
+ controller.onMessageDisplayed(message)
+
+ // Shown telemetry
+ assertNotNull(GleanMessaging.messageShown.testGetValue())
+ val shownEvent = GleanMessaging.messageShown.testGetValue()!!
+ assertEquals(1, shownEvent.size)
+ assertEquals(message.id, shownEvent.single().extra!!["message_key"])
+
+ // Expired telemetry
+ assertNull(GleanMessaging.messageExpired.testGetValue())
+
+ verify(storage).onMessageDisplayed(eq(message), any())
+ }
+
+ @Test
+ fun `GIVEN message is expired WHEN calling onMessageDisplayed THEN record messageShown, messageExpired events and update storage`() =
+ coroutineScope.runTest {
+ val message =
+ createMessage("id-1", style = StyleData(maxDisplayCount = 1), displayCount = 0)
+ val displayedMessage = createMessage("id-1", style = StyleData(maxDisplayCount = 1), displayCount = 1)
+ `when`(storage.onMessageDisplayed(any(), any())).thenReturn(displayedMessage)
+ // Assert telemetry is initially null
+ assertNull(GleanMessaging.messageShown.testGetValue())
+ assertNull(GleanMessaging.messageExpired.testGetValue())
+
+ controller.onMessageDisplayed(message)
+
+ // Shown telemetry
+ assertNotNull(GleanMessaging.messageShown.testGetValue())
+ val shownEvent = GleanMessaging.messageShown.testGetValue()!!
+ assertEquals(1, shownEvent.size)
+ assertEquals(message.id, shownEvent.single().extra!!["message_key"])
+
+ // Expired telemetry
+ assertNotNull(GleanMessaging.messageExpired.testGetValue())
+ val expiredEvent = GleanMessaging.messageExpired.testGetValue()!!
+ assertEquals(1, expiredEvent.size)
+ assertEquals(message.id, expiredEvent.single().extra!!["message_key"])
+
+ verify(storage).onMessageDisplayed(message)
+ }
+
+ @Test
+ fun `WHEN calling onMessageDismissed THEN record a messageDismissed event and update metadata`() =
+ coroutineScope.runTest {
+ val message = createMessage("id-1")
+ assertNull(GleanMessaging.messageDismissed.testGetValue())
+
+ controller.onMessageDismissed(message)
+
+ assertNotNull(GleanMessaging.messageDismissed.testGetValue())
+ val event = GleanMessaging.messageDismissed.testGetValue()!!
+ assertEquals(1, event.size)
+ assertEquals(message.id, event.single().extra!!["message_key"])
+
+ verify(storage).updateMetadata(message.metadata.copy(dismissed = true))
+ }
+
+ @Test
+ fun `GIVEN action is URL WHEN calling processMessageActionToUri THEN record a clicked telemetry event`() {
+ val message = createMessage("id-1")
+
+ `when`(storage.generateUuidAndFormatMessage(message))
+ .thenReturn(Pair(null, "://mock-uri"))
+
+ // Assert telemetry is initially null
+ assertNull(GleanMessaging.messageClicked.testGetValue())
+
+ val expectedUri = "$deepLinkScheme://mock-uri".toUri()
+
+ val actualUri = controller.processMessageActionToUri(message)
+
+ // Updated telemetry
+ assertNotNull(GleanMessaging.messageClicked.testGetValue())
+ val clickedEvent = GleanMessaging.messageClicked.testGetValue()!!
+ assertEquals(1, clickedEvent.size)
+ assertEquals(message.id, clickedEvent.single().extra!!["message_key"])
+
+ assertEquals(expectedUri, actualUri)
+ }
+
+ @Test
+ fun `GIVEN a URL with a {uuid} WHEN calling processMessageActionToUri THEN record a clicked telemetry event`() {
+ val url = "http://mozilla.org?uuid={uuid}"
+ val message = createMessage("id-1", action = "://open", messageData = MessageData(actionParams = mapOf("url" to url)))
+ val uuid = UUID.randomUUID().toString()
+ `when`(storage.generateUuidAndFormatMessage(message)).thenReturn(Pair(uuid, "://mock-uri"))
+
+ // Assert telemetry is initially null
+ assertNull(GleanMessaging.messageClicked.testGetValue())
+
+ val expectedUri = "$deepLinkScheme://mock-uri".toUri()
+
+ val actualUri = controller.processMessageActionToUri(message)
+
+ // Updated telemetry
+ val clickedEvents = GleanMessaging.messageClicked.testGetValue()
+ assertNotNull(clickedEvents)
+ val clickedEvent = clickedEvents!!.single()
+ assertEquals(message.id, clickedEvent.extra!!["message_key"])
+ assertEquals(uuid, clickedEvent.extra!!["action_uuid"])
+
+ assertEquals(expectedUri, actualUri)
+ }
+
+ @Test
+ fun `GIVEN action is deeplink WHEN calling processMessageActionToUri THEN return a deeplink URI`() {
+ val message = createMessage("id-1", action = "://a-deep-link")
+ `when`(storage.generateUuidAndFormatMessage(message))
+ .thenReturn(Pair(null, message.action))
+
+ // Assert telemetry is initially null
+ assertNull(GleanMessaging.messageClicked.testGetValue())
+
+ val expectedUri = "$deepLinkScheme${message.action}".toUri()
+ val actualUri = controller.processMessageActionToUri(message)
+
+ // Updated telemetry
+ assertNotNull(GleanMessaging.messageClicked.testGetValue())
+ val clickedEvent = GleanMessaging.messageClicked.testGetValue()!!
+ assertEquals(1, clickedEvent.size)
+ assertEquals(message.id, clickedEvent.single().extra!!["message_key"])
+
+ assertEquals(expectedUri, actualUri)
+ }
+
+ @Test
+ fun `GIVEN action unknown format WHEN calling processMessageActionToUri THEN return the action URI`() {
+ val message = createMessage("id-1", action = "unknown")
+ `when`(storage.generateUuidAndFormatMessage(message))
+ .thenReturn(Pair(null, message.action))
+
+ // Assert telemetry is initially null
+ assertNull(GleanMessaging.messageClicked.testGetValue())
+
+ val expectedUri = message.action.toUri()
+ val actualUri = controller.processMessageActionToUri(message)
+
+ // Updated telemetry
+ assertNotNull(GleanMessaging.messageClicked.testGetValue())
+ val clickedEvent = GleanMessaging.messageClicked.testGetValue()!!
+ assertEquals(1, clickedEvent.size)
+ assertEquals(message.id, clickedEvent.single().extra!!["message_key"])
+
+ assertEquals(expectedUri, actualUri)
+ }
+
+ @Test
+ fun `WHEN calling onMessageClicked THEN update stored metadata for message`() =
+ coroutineScope.runTest {
+ val message = createMessage("id-1")
+ assertFalse(message.metadata.pressed)
+
+ controller.onMessageClicked(message)
+
+ val updatedMetadata = message.metadata.copy(pressed = true)
+ verify(storage).updateMetadata(updatedMetadata)
+ }
+
+ @Test
+ fun `WHEN getIntentForMessageAction is called THEN return a generated Intent with the processed Message action`() {
+ val message = createMessage("id-1", action = "unknown")
+ `when`(storage.generateUuidAndFormatMessage(message))
+ .thenReturn(Pair(null, message.action))
+ assertNull(GleanMessaging.messageClicked.testGetValue())
+
+ val actualIntent = controller.getIntentForMessage(message)
+
+ // Updated telemetry
+ assertNotNull(GleanMessaging.messageClicked.testGetValue())
+ val event = GleanMessaging.messageClicked.testGetValue()!!
+ assertEquals(1, event.size)
+ assertEquals(message.id, event.single().extra!!["message_key"])
+
+ // The processed Intent data
+ assertEquals(Intent.ACTION_VIEW, actualIntent.action)
+ val expectedUri = message.action.toUri()
+ assertEquals(expectedUri, actualIntent.data)
+ }
+
+ @Test
+ fun `GIVEN stored messages contains a matching message WHEN calling getMessage THEN return the matching message`() =
+ coroutineScope.runTest {
+ val message1 = createMessage("1")
+ `when`(storage.getMessage(message1.id)).thenReturn(message1)
+ val actualMessage = controller.getMessage(message1.id)
+
+ assertEquals(message1, actualMessage)
+ }
+
+ @Test
+ fun `GIVEN stored messages doesn't contain a matching message WHEN calling getMessage THEN return null`() =
+ coroutineScope.runTest {
+ `when`(storage.getMessage("unknown id")).thenReturn(null)
+ val actualMessage = controller.getMessage("unknown id")
+
+ assertNull(actualMessage)
+ }
+
+ private fun createMessage(
+ id: String,
+ messageData: MessageData = MessageData(),
+ action: String = messageData.action,
+ style: StyleData = StyleData(),
+ displayCount: Int = 0,
+ ): Message =
+ Message(
+ id,
+ data = messageData,
+ style = style,
+ metadata = Message.Metadata(id, displayCount),
+ triggerIfAll = emptyList(),
+ excludeIfAny = emptyList(),
+ action = action,
+ )
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorageTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorageTest.kt
new file mode 100644
index 0000000000..a5c17e1cb2
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorageTest.kt
@@ -0,0 +1,927 @@
+/* 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.service.nimbus.messaging
+
+import android.net.Uri
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.nimbus.messaging.ControlMessageBehavior.SHOW_NEXT_MESSAGE
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertThrows
+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.Mock
+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`
+import org.mockito.MockitoAnnotations
+import org.mozilla.experiments.nimbus.FeaturesInterface
+import org.mozilla.experiments.nimbus.NimbusMessagingHelperInterface
+import org.mozilla.experiments.nimbus.NimbusMessagingInterface
+import org.mozilla.experiments.nimbus.NullVariables
+import org.mozilla.experiments.nimbus.Res
+import org.mozilla.experiments.nimbus.internal.FeatureHolder
+import org.mozilla.experiments.nimbus.internal.NimbusException
+import org.robolectric.RobolectricTestRunner
+import java.util.UUID
+
+private const val MOCK_TIME_MILLIS = 1000L
+
+@RunWith(RobolectricTestRunner::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class NimbusMessagingStorageTest {
+ @Mock private lateinit var metadataStorage: MessageMetadataStorage
+
+ @Mock private lateinit var nimbus: NimbusMessagingInterface
+
+ private lateinit var storage: NimbusMessagingStorage
+ private lateinit var messagingFeature: FeatureHolder<Messaging>
+ private var malformedWasReported = false
+ private var malformedMessageIds = mutableSetOf<String>()
+ private val reportMalformedMessage: (String) -> Unit = {
+ malformedMessageIds.add(it)
+ malformedWasReported = true
+ }
+ private lateinit var featuresInterface: FeaturesInterface
+
+ private val displayOnceStyle = StyleData(maxDisplayCount = 1)
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ NullVariables.instance.setContext(testContext)
+
+ val (messagingFeatureTmp, featuresInterfaceTmp) = createMessagingFeature()
+
+ messagingFeature = spy(messagingFeatureTmp)
+ featuresInterface = featuresInterfaceTmp
+
+ runBlocking {
+ `when`(metadataStorage.getMetadata())
+ .thenReturn(mapOf("message-1" to Message.Metadata(id = "message-1")))
+
+ `when`(metadataStorage.addMetadata(any()))
+ .thenReturn(mock())
+ }
+
+ storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ messagingFeature,
+ ) { MOCK_TIME_MILLIS }
+
+ val helper: NimbusMessagingHelperInterface = mock()
+ `when`(helper.evalJexl(any())).thenReturn(true)
+ `when`(nimbus.createMessageHelper(any())).thenReturn(helper)
+ }
+
+ @After
+ fun tearDown() {
+ malformedWasReported = false
+ malformedMessageIds.clear()
+ }
+
+ @Test
+ fun `WHEN calling getCoenrollingFeatureIds THEN messaging is in that list`() {
+ assertTrue(FxNimbusMessaging.getCoenrollingFeatureIds().contains(MESSAGING_FEATURE_ID))
+ }
+
+ @Test
+ fun `WHEN calling getMessages THEN provide a list of available messages for a given surface`() =
+ runTest {
+ val homescreenMessage = storage.getMessages().first()
+
+ assertEquals("message-1", homescreenMessage.id)
+ assertEquals("message-1", homescreenMessage.metadata.id)
+
+ val notificationMessage = storage.getMessages().last()
+ assertEquals("message-2", notificationMessage.id)
+ }
+
+ @Test
+ fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() =
+ runTest {
+ val messages = mapOf(
+ "low-message" to createMessageData(style = "low-priority"),
+ "high-message" to createMessageData(style = "high-priority"),
+ "medium-message" to createMessageData(style = "medium-priority"),
+ )
+ val styles = mapOf(
+ "high-priority" to createStyle(priority = 100),
+ "medium-priority" to createStyle(priority = 50),
+ "low-priority" to createStyle(priority = 1),
+ )
+ val (messagingFeature, _) = createMessagingFeature(
+ styles = styles,
+ messages = messages,
+ )
+
+ val storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ messagingFeature,
+ )
+
+ val results = storage.getMessages()
+
+ assertEquals("high-message", results[0].id)
+ assertEquals("medium-message", results[1].id)
+ assertEquals("low-message", results[2].id)
+ }
+
+ @Test
+ fun `GIVEN pressed message WHEN calling getMessages THEN filter out the pressed message`() =
+ runTest {
+ val metadataList = mapOf(
+ "pressed-message" to Message.Metadata(id = "pressed-message", pressed = true),
+ "normal-message" to Message.Metadata(id = "normal-message", pressed = false),
+ )
+ val messages = mapOf(
+ "pressed-message" to createMessageData(style = "high-priority"),
+ "normal-message" to createMessageData(style = "high-priority"),
+ )
+ val styles = mapOf(
+ "high-priority" to createStyle(priority = 100),
+ )
+ val metadataStorage: MessageMetadataStorage = mock()
+ val (messagingFeature, _) = createMessagingFeature(
+ styles = styles,
+ messages = messages,
+ )
+
+ `when`(metadataStorage.getMetadata()).thenReturn(metadataList)
+
+ val storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ messagingFeature,
+ )
+
+ val results = storage.getMessages()
+
+ assertEquals(2, results.size)
+
+ val message = storage.getNextMessage(HOMESCREEN, results)!!
+ assertEquals("normal-message", message.id)
+ }
+
+ @Test
+ fun `GIVEN dismissed message WHEN calling getMessages THEN filter out the dismissed message`() =
+ runTest {
+ val metadataList = mapOf(
+ "dismissed-message" to Message.Metadata(id = "dismissed-message", dismissed = true),
+ "normal-message" to Message.Metadata(id = "normal-message", dismissed = false),
+ )
+ val messages = mapOf(
+ "dismissed-message" to createMessageData(style = "high-priority"),
+ "normal-message" to createMessageData(style = "high-priority"),
+ )
+ val styles = mapOf(
+ "high-priority" to createStyle(priority = 100),
+ )
+ val metadataStorage: MessageMetadataStorage = mock()
+ val (messagingFeature, _) = createMessagingFeature(
+ styles = styles,
+ messages = messages,
+ )
+
+ `when`(metadataStorage.getMetadata()).thenReturn(metadataList)
+
+ val storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ messagingFeature,
+ )
+
+ val results = storage.getMessages()
+ assertEquals(2, results.size)
+
+ val message = storage.getNextMessage(HOMESCREEN, results)!!
+ assertEquals("normal-message", message.id)
+ }
+
+ @Test
+ fun `GIVEN a message that the maxDisplayCount WHEN calling getMessages THEN filter out the message`() =
+ runTest {
+ val metadataList = mapOf(
+ "shown-many-times-message" to Message.Metadata(
+ id = "shown-many-times-message",
+ displayCount = 10,
+ ),
+ "shown-two-times-message" to Message.Metadata(
+ id = "shown-two-times-message",
+ displayCount = 2,
+ ),
+ "normal-message" to Message.Metadata(id = "normal-message", displayCount = 0),
+ )
+ val messages = mapOf(
+ "shown-many-times-message" to createMessageData(
+ style = "high-priority",
+ ),
+ "shown-two-times-message" to createMessageData(
+ style = "high-priority",
+ ),
+ "normal-message" to createMessageData(style = "high-priority"),
+ )
+ val styles = mapOf(
+ "high-priority" to createStyle(priority = 100, maxDisplayCount = 2),
+ )
+ val metadataStorage: MessageMetadataStorage = mock()
+ val (messagingFeature, _) = createMessagingFeature(
+ styles = styles,
+ messages = messages,
+ )
+
+ `when`(metadataStorage.getMetadata()).thenReturn(metadataList)
+
+ val storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ messagingFeature,
+ )
+
+ val results = storage.getMessages()
+ assertEquals(3, results.size)
+
+ val message = storage.getNextMessage(HOMESCREEN, results)!!
+ assertEquals("normal-message", message.id)
+ }
+
+ @Test
+ fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() =
+ runTest {
+ val messages = storage.getMessages()
+ val firstMessage = messages.first()
+
+ assertEquals("message-1", firstMessage.id)
+ assertEquals("message-1", firstMessage.metadata.id)
+ assertTrue(messages.size == 2)
+ assertTrue(malformedWasReported)
+ }
+
+ @Test
+ fun `GIVEN a malformed action WHEN calling sanitizeAction THEN return null`() {
+ val actionsMap = mapOf("action-1" to "action-1-url")
+
+ val notFoundAction =
+ storage.sanitizeAction("no-found-action", actionsMap)
+ val emptyAction = storage.sanitizeAction("", actionsMap)
+ val blankAction = storage.sanitizeAction(" ", actionsMap)
+
+ assertNull(notFoundAction)
+ assertNull(emptyAction)
+ assertNull(blankAction)
+ }
+
+ @Test
+ fun `GIVEN a previously stored malformed action WHEN calling sanitizeAction THEN return null and not report malFormed`() {
+ val actionsMap = mapOf("action-1" to "action-1-url")
+
+ storage.malFormedMap["malformed-action"] = "messageId"
+
+ val action = storage.sanitizeAction("malformed-action", actionsMap)
+
+ assertNull(action)
+ assertFalse(malformedWasReported)
+ }
+
+ @Test
+ fun `GIVEN a non-previously stored malformed action WHEN calling sanitizeAction THEN return null`() {
+ val actionsMap = mapOf("action-1" to "action-1-url")
+
+ val action = storage.sanitizeAction("malformed-action", actionsMap)
+
+ assertNull(action)
+ }
+
+ @Test
+ fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() = runTest {
+ storage.updateMetadata(mock())
+
+ verify(metadataStorage).updateMetadata(any())
+ }
+
+ @Test
+ fun `WHEN calling onMessageDisplayed with message & boot id THEN metadata for count, lastTimeShown & latestBootIdentifier is updated`() =
+ runTest {
+ val message = storage.getMessage("message-1")!!
+ assertEquals(0, message.displayCount)
+
+ val bootId = "test boot id"
+ val expectedMessage = message.copy(
+ metadata = Message.Metadata(
+ id = "message-1",
+ displayCount = 1,
+ lastTimeShown = MOCK_TIME_MILLIS,
+ latestBootIdentifier = bootId,
+ ),
+ )
+
+ assertEquals(expectedMessage, storage.onMessageDisplayed(message, bootId))
+ }
+
+ @Test
+ fun `WHEN calling onMessageDisplayed with message THEN metadata for count, lastTimeShown is updated`() =
+ runTest {
+ val message = storage.getMessage("message-1")!!
+ assertEquals(0, message.displayCount)
+
+ val bootId = null
+ val expectedMessage = message.copy(
+ metadata = Message.Metadata(
+ id = "message-1",
+ displayCount = 1,
+ lastTimeShown = MOCK_TIME_MILLIS,
+ latestBootIdentifier = bootId,
+ ),
+ )
+
+ assertEquals(expectedMessage, storage.onMessageDisplayed(message, bootId))
+ }
+
+ @Test
+ fun `GIVEN a valid action WHEN calling sanitizeAction THEN return the action`() {
+ val actionsMap = mapOf("action-1" to "action-1-url")
+
+ val validAction = storage.sanitizeAction("action-1", actionsMap)
+
+ assertEquals("action-1-url", validAction)
+ }
+
+ @Test
+ fun `GIVEN a trigger action WHEN calling sanitizeTriggers THEN return null`() {
+ val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
+
+ val notFoundTrigger =
+ storage.sanitizeTriggers(listOf("no-found-trigger"), triggersMap)
+ val emptyTrigger = storage.sanitizeTriggers(listOf(""), triggersMap)
+ val blankTrigger = storage.sanitizeTriggers(listOf(" "), triggersMap)
+
+ assertNull(notFoundTrigger)
+ assertNull(emptyTrigger)
+ assertNull(blankTrigger)
+ }
+
+ @Test
+ fun `GIVEN a previously stored malformed trigger WHEN calling sanitizeTriggers THEN no report malformed and return null`() {
+ val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
+
+ storage.malFormedMap[" "] = "messageId"
+
+ val trigger = storage.sanitizeTriggers(listOf(" "), triggersMap)
+
+ assertNull(trigger)
+ assertFalse(malformedWasReported)
+ }
+
+ @Test
+ fun `GIVEN a non previously stored malformed trigger WHEN calling sanitizeTriggers THEN return null`() {
+ val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
+
+ val trigger = storage.sanitizeTriggers(listOf(" "), triggersMap)
+
+ assertNull(trigger)
+ }
+
+ @Test
+ fun `GIVEN a valid trigger WHEN calling sanitizeAction THEN return the trigger`() {
+ val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
+
+ val validTrigger = storage.sanitizeTriggers(listOf("trigger-1"), triggersMap)
+
+ assertEquals(listOf("trigger-1-expression"), validTrigger)
+ }
+
+ @Test
+ fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() {
+ val helper: NimbusMessagingHelperInterface = mock()
+ val message = Message(
+ "same-id",
+ mock(),
+ action = "action",
+ mock(),
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ `when`(helper.evalJexl(any())).thenReturn(true)
+
+ val result = storage.isMessageEligible(message, helper)
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN a malformed message key WHEN calling reportMalformedMessage THEN record a malformed feature event`() {
+ val key = "malformed-message"
+ storage.reportMalformedMessage(key)
+
+ assertTrue(malformedWasReported)
+ verify(featuresInterface).recordMalformedConfiguration("messaging", key)
+ }
+
+ @Test
+ fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() {
+ val helper: NimbusMessagingHelperInterface = mock()
+ val message = Message(
+ "same-id",
+ mock(),
+ action = "action",
+ mock(),
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ `when`(helper.evalJexl(any())).then { throw NimbusException.EvaluationException("") }
+
+ assertThrows(NimbusException.EvaluationException::class.java) {
+ storage.isMessageEligible(message, helper)
+ }
+ }
+
+ @Test
+ fun `GIVEN a previously malformed trigger WHEN calling isMessageEligible THEN throw and not evaluate`() {
+ val helper: NimbusMessagingHelperInterface = mock()
+ val message = Message(
+ "same-id",
+ mock(),
+ action = "action",
+ mock(),
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ storage.malFormedMap["trigger"] = "same-id"
+
+ `when`(helper.evalJexl(any())).then { throw NimbusException.EvaluationException("") }
+
+ assertThrows(NimbusException.EvaluationException::class.java) {
+ storage.isMessageEligible(message, helper)
+ }
+
+ verify(helper, never()).evalJexl("trigger")
+ }
+
+ @Test
+ fun `GIVEN a non previously malformed trigger WHEN calling isMessageEligible THEN throw and not evaluate`() {
+ val helper: NimbusMessagingHelperInterface = mock()
+ val message = Message(
+ "same-id",
+ mock(),
+ action = "action",
+ mock(),
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ `when`(helper.evalJexl(any())).then { throw NimbusException.EvaluationException("") }
+
+ assertFalse(storage.malFormedMap.containsKey("trigger"))
+
+ assertThrows(NimbusException.EvaluationException::class.java) {
+ storage.isMessageEligible(message, helper)
+ }
+
+ assertTrue(storage.malFormedMap.containsKey("trigger"))
+ }
+
+ @Test
+ fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() {
+ val spiedStorage = spy(storage)
+ val message = Message(
+ "same-id",
+ mock(),
+ action = "action",
+ mock(),
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ doReturn(false).`when`(spiedStorage).isMessageEligible(any(), any())
+
+ val result = spiedStorage.getNextMessage(HOMESCREEN, listOf(message))
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() {
+ val spiedStorage = spy(storage)
+ val message = Message(
+ "same-id",
+ createMessageData(surface = HOMESCREEN),
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ doReturn(true).`when`(spiedStorage).isMessageEligible(any(), any())
+
+ val result = spiedStorage.getNextMessage(HOMESCREEN, listOf(message))
+
+ assertEquals(message.id, result!!.id)
+ }
+
+ @Test
+ fun `GIVEN a message under experiment WHEN calling onMessageDisplayed THEN call recordExposureEvent`() = runTest {
+ val spiedStorage = spy(storage)
+ val experiment = "my-experiment"
+ val messageData: MessageData = createMessageData(isControl = false, experiment = experiment)
+
+ val message = Message(
+ "same-id",
+ messageData,
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ doReturn(true).`when`(spiedStorage).isMessageEligible(any(), any())
+
+ val result = spiedStorage.getNextMessage(HOMESCREEN, listOf(message))
+ verify(featuresInterface, never()).recordExposureEvent("messaging", experiment)
+
+ spiedStorage.onMessageDisplayed(message)
+ verify(featuresInterface).recordExposureEvent("messaging", experiment)
+ assertEquals(message.id, result!!.id)
+ }
+
+ @Test
+ fun `GIVEN a control message WHEN calling getNextMessage THEN return the next eligible message`() {
+ val spiedStorage = spy(storage)
+ val experiment = "my-experiment"
+ val messageData: MessageData = createMessageData()
+ val controlMessageData: MessageData = createMessageData(isControl = true, experiment = experiment)
+
+ doReturn(SHOW_NEXT_MESSAGE).`when`(spiedStorage).getOnControlBehavior()
+
+ val message = Message(
+ "id",
+ messageData,
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ val controlMessage = Message(
+ "control-id",
+ controlMessageData,
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ doReturn(true).`when`(spiedStorage).isMessageEligible(any(), any())
+
+ val result = spiedStorage.getNextMessage(
+ HOMESCREEN,
+ listOf(controlMessage, message),
+ )
+
+ verify(messagingFeature).recordExperimentExposure(experiment)
+ assertEquals(message.id, result!!.id)
+ }
+
+ @Test
+ fun `GIVEN a malformed control message WHEN calling getNextMessage THEN return the next eligible message`() {
+ val spiedStorage = spy(storage)
+ val messageData: MessageData = createMessageData()
+ // the message isControl, but has no experiment property.
+ val controlMessageData: MessageData = createMessageData(isControl = true)
+
+ doReturn(SHOW_NEXT_MESSAGE).`when`(spiedStorage).getOnControlBehavior()
+
+ val message = Message(
+ "id",
+ messageData,
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ val controlMessage = Message(
+ "control-id",
+ controlMessageData,
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ doReturn(true).`when`(spiedStorage).isMessageEligible(any(), any())
+
+ val result = spiedStorage.getNextMessage(
+ HOMESCREEN,
+ listOf(controlMessage, message),
+ )
+
+ verify(messagingFeature).recordMalformedConfiguration("control-id")
+
+ assertEquals(message.id, result!!.id)
+ }
+
+ @Test
+ fun `GIVEN a control message WHEN calling getNextMessage THEN return the next eligible message with the correct surface`() {
+ val spiedStorage = spy(storage)
+ val experiment = "my-experiment"
+ val messageData: MessageData = createMessageData()
+ val incorrectMessageData: MessageData = createMessageData(surface = NOTIFICATION)
+ val controlMessageData: MessageData = createMessageData(isControl = true, experiment = experiment)
+
+ doReturn(SHOW_NEXT_MESSAGE).`when`(spiedStorage).getOnControlBehavior()
+
+ val message = Message(
+ "id",
+ messageData,
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ val incorrectMessage = Message(
+ "incorrect-id",
+ incorrectMessageData,
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ val controlMessage = Message(
+ "control-id",
+ controlMessageData,
+ action = "action",
+ style = displayOnceStyle,
+ listOf("trigger"),
+ metadata = Message.Metadata("same-id"),
+ )
+
+ doReturn(true).`when`(spiedStorage).isMessageEligible(any(), any())
+
+ var result = spiedStorage.getNextMessage(
+ HOMESCREEN,
+ listOf(controlMessage, incorrectMessage, message),
+ )
+
+ verify(messagingFeature, times(1)).recordExperimentExposure(experiment)
+ assertEquals(message.id, result!!.id)
+
+ result = spiedStorage.getNextMessage(
+ HOMESCREEN,
+ listOf(controlMessage, incorrectMessage),
+ )
+
+ verify(messagingFeature, times(2)).recordExperimentExposure(experiment)
+ assertNull(result)
+ }
+
+ @Test
+ fun `WHEN a storage instance is created THEN do not invoke the feature`() = runTest {
+ storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ messagingFeature,
+ )
+
+ // We should not be using the feature holder until getMessages is called.
+ verify(messagingFeature, never()).value()
+ }
+
+ @Test
+ fun `WHEN calling getMessage THEN return message with matching key OR null if doesn't exist`() =
+ runTest {
+ val messages = mapOf(
+ "low-message" to createMessageData(style = "low-priority"),
+ "high-message" to createMessageData(style = "high-priority"),
+ "medium-message" to createMessageData(style = "medium-priority"),
+ )
+ val styles = mapOf(
+ "high-priority" to createStyle(priority = 100),
+ "medium-priority" to createStyle(priority = 50),
+ "low-priority" to createStyle(priority = 1),
+ )
+
+ val (messagingFeature, _) = createMessagingFeature(
+ styles = styles,
+ messages = messages,
+ )
+
+ `when`(metadataStorage.getMetadata()).thenReturn(
+ mapOf(
+ "message-1" to Message.Metadata(id = "message-1"),
+ ),
+ )
+
+ val storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ messagingFeature,
+ )
+
+ assertEquals("high-message", storage.getMessage("high-message")?.id)
+ assertEquals("medium-message", storage.getMessage("medium-message")?.id)
+ assertEquals("low-message", storage.getMessage("low-message")?.id)
+ assertEquals(null, storage.getMessage("no-message")?.id)
+ }
+
+ @Test
+ fun `GIVEN a message without text THEN reject the message and report it as malformed`() = runTest {
+ val (feature, _) = createMessagingFeature(
+ styles = mapOf(
+ "style-1" to createStyle(priority = 100),
+ ),
+ triggers = mapOf("trigger-1" to "://trigger-1"),
+ messages = mapOf(
+ "missing-text" to createMessageData(text = ""),
+ "control" to createMessageData(text = "", isControl = true),
+ "ok" to createMessageData(),
+ ),
+ )
+ val storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ feature,
+ )
+
+ assertNotNull(storage.getMessage("ok"))
+ assertNotNull(storage.getMessage("control"))
+ assertNull(storage.getMessage("missing-text"))
+ assertTrue(malformedMessageIds.contains("missing-text"))
+ assertFalse(malformedMessageIds.contains("ok"))
+ assertFalse(malformedMessageIds.contains("control"))
+ }
+
+ @Test
+ fun `GIVEN a message with an action and params THEN do string interpolation`() = runTest {
+ val (feature, _) = createMessagingFeature(
+ actions = mapOf("OPEN_URL" to "://open", "INSTALL_FOCUS" to "market://details?app=org.mozilla.focus"),
+ messages = mapOf(
+ "open-url" to createMessageData(
+ action = "OPEN_URL",
+ actionParams = mapOf("url" to "https://mozilla.org"),
+ ),
+
+ // with uuid in the param value
+ "open-url-with-uuid" to createMessageData(
+ action = "OPEN_URL",
+ actionParams = mapOf("url" to "https://mozilla.org?uuid={uuid}"),
+ ),
+
+ // with ? in the action
+ "install-focus" to createMessageData(
+ action = "INSTALL_FOCUS",
+ actionParams = mapOf("utm" to "my-utm"),
+ ),
+ ),
+ )
+ val storage = NimbusMessagingStorage(
+ testContext,
+ metadataStorage,
+ reportMalformedMessage,
+ nimbus,
+ messagingFeature = feature,
+ )
+
+ val myUuid = UUID.randomUUID().toString()
+ val helper = object : NimbusMessagingHelperInterface {
+ override fun evalJexl(expression: String) = false
+ override fun getUuid(template: String): String? =
+ if (template.contains("{uuid}")) {
+ myUuid
+ } else {
+ null
+ }
+
+ override fun stringFormat(template: String, uuid: String?): String =
+ uuid?.let {
+ template.replace("{uuid}", it)
+ } ?: template
+ }
+
+ run {
+ val message = storage.getMessage("open-url")!!
+ assertEquals(message.action, "://open")
+ val (uuid, url) = storage.generateUuidAndFormatMessage(message, helper)
+ assertEquals(uuid, null)
+ val urlParam = "https://mozilla.org"
+ assertEquals(url, "://open?url=${Uri.encode(urlParam)}")
+ }
+
+ run {
+ val message = storage.getMessage("open-url-with-uuid")!!
+ assertEquals(message.action, "://open")
+ val (uuid, url) = storage.generateUuidAndFormatMessage(message, helper)
+ assertEquals(uuid, myUuid)
+ val urlParam = "https://mozilla.org?uuid=$myUuid"
+ assertEquals(url, "://open?url=${Uri.encode(urlParam)}")
+ }
+
+ run {
+ val message = storage.getMessage("install-focus")!!
+ assertEquals(message.action, "market://details?app=org.mozilla.focus")
+ val (uuid, url) = storage.generateUuidAndFormatMessage(message, helper)
+ assertEquals(uuid, null)
+ assertEquals(url, "market://details?app=org.mozilla.focus&utm=my-utm")
+ }
+ }
+
+ private fun createMessageData(
+ text: String = "text-1",
+ action: String = "action-1",
+ actionParams: Map<String, String> = mapOf(),
+ style: String = "style-1",
+ triggers: List<String> = listOf("trigger-1"),
+ surface: MessageSurfaceId = HOMESCREEN,
+ isControl: Boolean = false,
+ experiment: String? = null,
+ ) = MessageData(
+ action = action,
+ actionParams = actionParams,
+ style = style,
+ triggerIfAll = triggers,
+ surface = surface,
+ isControl = isControl,
+ text = Res.string(text),
+ experiment = experiment,
+ )
+
+ private fun createMessagingFeature(
+ triggers: Map<String, String> = mapOf("trigger-1" to "trigger-1-expression"),
+ styles: Map<String, StyleData> = mapOf("style-1" to createStyle()),
+ actions: Map<String, String> = mapOf("action-1" to "action-1-url"),
+ messages: Map<String, MessageData> = mapOf(
+ "message-1" to createMessageData(surface = HOMESCREEN),
+ "message-2" to createMessageData(surface = NOTIFICATION),
+ "malformed" to createMessageData(action = "malformed-action"),
+ "blanktext" to createMessageData(text = ""),
+ ),
+ ): Pair<FeatureHolder<Messaging>, FeaturesInterface> {
+ val messaging = Messaging(
+ actions = actions,
+ triggers = triggers,
+ messages = messages,
+ styles = styles,
+ )
+ val featureInterface: FeaturesInterface = mock()
+ // "messaging" is a hard coded value generated from Nimbus.
+ val messagingFeature = FeatureHolder({ featureInterface }, "messaging") { _, _ ->
+ messaging
+ }
+ messagingFeature.withCachedValue(messaging)
+
+ return messagingFeature to featureInterface
+ }
+
+ private fun createStyle(priority: Int = 1, maxDisplayCount: Int = 5): StyleData {
+ val style1: StyleData = mock()
+ `when`(style1.priority).thenReturn(priority)
+ `when`(style1.maxDisplayCount).thenReturn(maxDisplayCount)
+ return style1
+ }
+
+ companion object {
+ private const val HOMESCREEN = "homescreen"
+ private const val NOTIFICATION = "notification"
+ }
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorageTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorageTest.kt
new file mode 100644
index 0000000000..e40ee1dffc
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorageTest.kt
@@ -0,0 +1,139 @@
+/* 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.service.nimbus.messaging
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.robolectric.testContext
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class OnDiskMessageMetadataStorageTest {
+
+ private lateinit var storage: OnDiskMessageMetadataStorage
+
+ @Before
+ fun setup() {
+ storage = OnDiskMessageMetadataStorage(
+ testContext,
+ )
+ }
+
+ @Test
+ fun `GIVEN metadata is not loaded from disk WHEN calling getMetadata THEN load it`() =
+ runTest {
+ val spiedStorage = spy(storage)
+
+ `when`(spiedStorage.readFromDisk()).thenReturn(emptyMap())
+
+ spiedStorage.getMetadata()
+
+ verify(spiedStorage).readFromDisk()
+ }
+
+ @Test
+ fun `GIVEN metadata is loaded from disk WHEN calling getMetadata THEN do not load it from disk`() =
+ runTest {
+ val spiedStorage = spy(storage)
+
+ spiedStorage.metadataMap = hashMapOf("" to Message.Metadata("id"))
+
+ spiedStorage.getMetadata()
+
+ verify(spiedStorage, never()).readFromDisk()
+ }
+
+ @Test
+ fun `WHEN calling addMetadata THEN add in memory and disk`() = runTest {
+ val spiedStorage = spy(storage)
+
+ assertTrue(spiedStorage.metadataMap.isEmpty())
+
+ `when`(spiedStorage.writeToDisk()).then { }
+
+ spiedStorage.addMetadata(Message.Metadata("id"))
+
+ assertFalse(spiedStorage.metadataMap.isEmpty())
+ verify(spiedStorage).writeToDisk()
+ }
+
+ @Test
+ fun `WHEN calling updateMetadata THEN delegate to addMetadata`() = runTest {
+ val spiedStorage = spy(storage)
+ val metadata = Message.Metadata("id")
+ `when`(spiedStorage.writeToDisk()).then { }
+
+ spiedStorage.updateMetadata(metadata)
+
+ verify(spiedStorage).addMetadata(metadata)
+ }
+
+ @Test
+ fun `WHEN calling toJson THEN return an string json representation`() {
+ val metadata = Message.Metadata(
+ id = "id",
+ displayCount = 1,
+ pressed = false,
+ dismissed = false,
+ lastTimeShown = 0L,
+ latestBootIdentifier = "9",
+ )
+
+ val expected =
+ """{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0,"latestBootIdentifier":"9"}"""
+
+ assertEquals(expected, metadata.toJson())
+ }
+
+ @Test
+ fun `WHEN calling toMetadata THEN return Metadata representation`() {
+ val json =
+ """{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0,"latestBootIdentifier":"9"}"""
+
+ val jsonObject = JSONObject(json)
+
+ val metadata = Message.Metadata(
+ id = "id",
+ displayCount = 1,
+ pressed = false,
+ dismissed = false,
+ lastTimeShown = 0L,
+ latestBootIdentifier = "9",
+ )
+
+ assertEquals(metadata, jsonObject.toMetadata())
+ }
+
+ @Test
+ fun `WHEN calling toMetadataMap THEN return map representation`() {
+ val json =
+ """[{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0,"latestBootIdentifier":"9"}]"""
+
+ val jsonArray = JSONArray(json)
+
+ val metadata = Message.Metadata(
+ id = "id",
+ displayCount = 1,
+ pressed = false,
+ dismissed = false,
+ lastTimeShown = 0L,
+ latestBootIdentifier = "9",
+ )
+
+ assertEquals(metadata, jsonArray.toMetadataMap()[metadata.id])
+ }
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/ui/NimbusBranchItemViewHolderTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/ui/NimbusBranchItemViewHolderTest.kt
new file mode 100644
index 0000000000..5a41936e71
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/ui/NimbusBranchItemViewHolderTest.kt
@@ -0,0 +1,92 @@
+/* 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.service.nimbus.ui
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mozilla.experiments.nimbus.Branch
+
+@RunWith(AndroidJUnit4::class)
+class NimbusBranchItemViewHolderTest {
+
+ private val branch = Branch(
+ slug = "control",
+ ratio = 1,
+ )
+ private lateinit var nimbusBranchesDelegate: NimbusBranchesAdapterDelegate
+ private lateinit var selectedIconView: ImageView
+ private lateinit var titleView: TextView
+ private lateinit var summaryView: TextView
+
+ @Before
+ fun setup() {
+ nimbusBranchesDelegate = mock()
+ selectedIconView = mock()
+ titleView = mock()
+ summaryView = mock()
+ }
+
+ @Test
+ fun `GIVEN a branch WHEN bind is called THEN title and summary text is set`() {
+ val view = View(testContext)
+ val holder = NimbusBranchItemViewHolder(
+ view,
+ nimbusBranchesDelegate,
+ selectedIconView,
+ titleView,
+ summaryView,
+ )
+
+ holder.bind(branch, "")
+
+ verify(selectedIconView).isVisible = false
+ verify(titleView).text = branch.slug
+ verify(summaryView).text = branch.slug
+ }
+
+ @Test
+ fun `WHEN item is clicked THEN delegate is called`() {
+ val view = View(testContext)
+ val holder =
+ NimbusBranchItemViewHolder(
+ view,
+ nimbusBranchesDelegate,
+ selectedIconView,
+ titleView,
+ summaryView,
+ )
+
+ holder.bind(branch, "")
+ holder.itemView.performClick()
+
+ verify(nimbusBranchesDelegate).onBranchItemClicked(branch)
+ }
+
+ @Test
+ fun `WHEN the selected branch matches THEN the selected icon is visible`() {
+ val view = View(testContext)
+ val holder =
+ NimbusBranchItemViewHolder(
+ view,
+ nimbusBranchesDelegate,
+ selectedIconView,
+ titleView,
+ summaryView,
+ )
+
+ holder.bind(branch, branch.slug)
+
+ verify(selectedIconView).isVisible = true
+ }
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/ui/NimbusExperimentItemViewHolderTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/ui/NimbusExperimentItemViewHolderTest.kt
new file mode 100644
index 0000000000..fbdfc40dfa
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/ui/NimbusExperimentItemViewHolderTest.kt
@@ -0,0 +1,61 @@
+/* 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.service.nimbus.ui
+
+import android.view.View
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mozilla.experiments.nimbus.AvailableExperiment
+
+@RunWith(AndroidJUnit4::class)
+class NimbusExperimentItemViewHolderTest {
+
+ private val experiment = AvailableExperiment(
+ slug = "secure-gold",
+ userFacingDescription = "This is a test experiment for diagnostic purposes.",
+ userFacingName = "Diagnostic test experiment",
+ branches = emptyList(),
+ referenceBranch = null,
+ )
+
+ private lateinit var nimbusExperimentsDelegate: NimbusExperimentsAdapterDelegate
+ private lateinit var titleView: TextView
+ private lateinit var summaryView: TextView
+
+ @Before
+ fun setup() {
+ nimbusExperimentsDelegate = mock()
+ titleView = mock()
+ summaryView = mock()
+ }
+
+ @Test
+ fun `GIVEN a experiment WHEN bind is called THEN title and summary text is set`() {
+ val view = View(testContext)
+ val holder =
+ NimbusExperimentItemViewHolder(view, nimbusExperimentsDelegate, titleView, summaryView)
+
+ holder.bind(experiment)
+ verify(titleView).text = experiment.userFacingName
+ verify(summaryView).text = experiment.userFacingDescription
+ }
+
+ @Test
+ fun `WHEN item is clicked THEN delegate is called`() {
+ val view = View(testContext)
+ val holder =
+ NimbusExperimentItemViewHolder(view, nimbusExperimentsDelegate, titleView, summaryView)
+
+ holder.bind(experiment)
+ holder.itemView.performClick()
+ verify(nimbusExperimentsDelegate).onExperimentItemClicked(experiment)
+ }
+}
diff --git a/mobile/android/android-components/components/service/nimbus/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/nimbus/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..1f0955d450
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/mobile/android/android-components/components/service/nimbus/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/nimbus/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/nimbus/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/pocket/.gitignore b/mobile/android/android-components/components/service/pocket/.gitignore
new file mode 100644
index 0000000000..3c06d0139f
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/.gitignore
@@ -0,0 +1,2 @@
+src/test/resources/pocket/apiKey.txt
+src/test/resources/pocket/listenAccessToken.txt
diff --git a/mobile/android/android-components/components/service/pocket/README.md b/mobile/android/android-components/components/service/pocket/README.md
new file mode 100644
index 0000000000..86c2e0ec75
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/README.md
@@ -0,0 +1,44 @@
+# [Android Components](../../../README.md) > Service > Pocket
+
+A library for easily getting Pocket recommendations that transparently handles downloading, caching and periodically refreshing Pocket data.
+
+Currently this supports:
+
+- Pocket recommended stories.
+- Pocket sponsored stories.
+
+## Usage
+1. For Pocket recommended stories:
+ - Use `PocketStoriesService#startPeriodicStoriesRefresh` and `PocketStoriesService#stopPeriodicStoriesRefresh`
+ as high up in the client app as possible (preferably in the Application object or in a single Activity) to ensure the
+ background story refresh functionality works for the entirety of the app lifetime.
+ - Use `PocketStoriesService.getStories` to get the current list of Pocket recommended stories.
+
+2. For Pocket sponsored stories:
+ - Use `PocketStoriesService#startPeriodicSponsoredStoriesRefresh` and `PocketStoriesService#stopPeriodicSponsoredStoriesRefresh`
+ as high up in the client app as possible (preferably in the Application object or in a single Activity) to ensure the
+ background story refresh functionality works for the entirety of the app lifetime.
+ - Use `PocketStoriesService.getSponsoredStories` to get the current list of Pocket recommended stories.
+ - Use `PocketStoriesService,recordStoriesImpressions` to try and persist that a list of sponsored stories were shown to the user. (Safe to call even if those stories are not persisted).
+ - Use `PocketStoriesService.deleteProfile` to delete all server stored information about the device to which sponsored stories were previously downloaded. This may include data like network ip and application tokens.
+
+ ##### Pacing and rotating:
+ A new `PocketSponsoredStoryCaps` is available in the response from `PocketStoriesService.getSponsoredStories` which allows checking `currentImpressions`, `lifetimeCount`, `flightCount`, `flightPeriod` based on which the client can decide which stories to show.
+ All this is based on clients calling `PocketStoriesService,recordStoriesImpressions` to record new impressions in between application restarts.
+
+
+
+
+### 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:service-pocket:{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/service/pocket/build.gradle b/mobile/android/android-components/components/service/pocket/build.gradle
new file mode 100644
index 0000000000..2835723a98
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/build.gradle
@@ -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/. */
+
+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"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ sourceSets {
+ test.assets.srcDirs += files("$projectDir/schemas".toString())
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ namespace 'mozilla.components.service.pocket'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_work_runtime
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':concept-fetch')
+
+ testImplementation ComponentsDependencies.kotlin_reflect
+
+ testImplementation ComponentsDependencies.androidx_arch_core_testing
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.androidx_room_testing
+ testImplementation ComponentsDependencies.androidx_work_testing
+
+ testImplementation project(':support-test')
+ testImplementation project(':lib-fetch-httpurlconnection')
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_arch_core_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+}
+
+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/service/pocket/proguard-rules.pro b/mobile/android/android-components/components/service/pocket/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/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/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/1.json b/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/1.json
new file mode 100644
index 0000000000..04c1aa5bab
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/1.json
@@ -0,0 +1,70 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "6f93143cfe11253bf96ec0ff80483bcf",
+ "entities": [
+ {
+ "tableName": "stories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `publisher` TEXT NOT NULL, `category` TEXT NOT NULL, `timeToRead` INTEGER NOT NULL, `timesShown` INTEGER NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publisher",
+ "columnName": "publisher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "category",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeToRead",
+ "columnName": "timeToRead",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timesShown",
+ "columnName": "timesShown",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6f93143cfe11253bf96ec0ff80483bcf')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/2.json b/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/2.json
new file mode 100644
index 0000000000..917776ad30
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/2.json
@@ -0,0 +1,120 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "1ea41b5cc0791d92dd8f0db8b387fe6c",
+ "entities": [
+ {
+ "tableName": "stories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `publisher` TEXT NOT NULL, `category` TEXT NOT NULL, `timeToRead` INTEGER NOT NULL, `timesShown` INTEGER NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publisher",
+ "columnName": "publisher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "category",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeToRead",
+ "columnName": "timeToRead",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timesShown",
+ "columnName": "timesShown",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "spocs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `sponsor` TEXT NOT NULL, `clickShim` TEXT NOT NULL, `impressionShim` TEXT NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sponsor",
+ "columnName": "sponsor",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clickShim",
+ "columnName": "clickShim",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionShim",
+ "columnName": "impressionShim",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ea41b5cc0791d92dd8f0db8b387fe6c')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/3.json b/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/3.json
new file mode 100644
index 0000000000..0644b05dca
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/3.json
@@ -0,0 +1,194 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "966f55824415a21a73640bd2641772f2",
+ "entities": [
+ {
+ "tableName": "stories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `publisher` TEXT NOT NULL, `category` TEXT NOT NULL, `timeToRead` INTEGER NOT NULL, `timesShown` INTEGER NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publisher",
+ "columnName": "publisher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "category",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeToRead",
+ "columnName": "timeToRead",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timesShown",
+ "columnName": "timesShown",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "spocs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `sponsor` TEXT NOT NULL, `clickShim` TEXT NOT NULL, `impressionShim` TEXT NOT NULL, `priority` INTEGER NOT NULL, `lifetimeCapCount` INTEGER NOT NULL, `flightCapCount` INTEGER NOT NULL, `flightCapPeriod` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sponsor",
+ "columnName": "sponsor",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clickShim",
+ "columnName": "clickShim",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionShim",
+ "columnName": "impressionShim",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lifetimeCapCount",
+ "columnName": "lifetimeCapCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flightCapCount",
+ "columnName": "flightCapCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flightCapPeriod",
+ "columnName": "flightCapPeriod",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "spocs_impressions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spocId` INTEGER NOT NULL, `impressionId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `impressionDateInSeconds` INTEGER NOT NULL, FOREIGN KEY(`spocId`) REFERENCES `spocs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "spocId",
+ "columnName": "spocId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionId",
+ "columnName": "impressionId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionDateInSeconds",
+ "columnName": "impressionDateInSeconds",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "impressionId"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "spocs",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "spocId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '966f55824415a21a73640bd2641772f2')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/4.json b/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/4.json
new file mode 100644
index 0000000000..b1842722a4
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/4.json
@@ -0,0 +1,204 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "cc5b4d41781399f6ab7f123c10546acc",
+ "entities": [
+ {
+ "tableName": "stories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `publisher` TEXT NOT NULL, `category` TEXT NOT NULL, `timeToRead` INTEGER NOT NULL, `timesShown` INTEGER NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publisher",
+ "columnName": "publisher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "category",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeToRead",
+ "columnName": "timeToRead",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timesShown",
+ "columnName": "timesShown",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "spocs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `sponsor` TEXT NOT NULL, `clickShim` TEXT NOT NULL, `impressionShim` TEXT NOT NULL, `priority` INTEGER NOT NULL, `lifetimeCapCount` INTEGER NOT NULL, `flightCapCount` INTEGER NOT NULL, `flightCapPeriod` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sponsor",
+ "columnName": "sponsor",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clickShim",
+ "columnName": "clickShim",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionShim",
+ "columnName": "impressionShim",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lifetimeCapCount",
+ "columnName": "lifetimeCapCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flightCapCount",
+ "columnName": "flightCapCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flightCapPeriod",
+ "columnName": "flightCapPeriod",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "spocs_impressions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spocId` INTEGER NOT NULL, `impressionId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `impressionDateInSeconds` INTEGER NOT NULL, FOREIGN KEY(`spocId`) REFERENCES `spocs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "spocId",
+ "columnName": "spocId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionId",
+ "columnName": "impressionId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionDateInSeconds",
+ "columnName": "impressionDateInSeconds",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "impressionId"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_spocs_impressions_spocId",
+ "unique": false,
+ "columnNames": [
+ "spocId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_spocs_impressions_spocId` ON `${TABLE_NAME}` (`spocId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "spocs",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "spocId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cc5b4d41781399f6ab7f123c10546acc')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/androidTest/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabaseTest.kt b/mobile/android/android-components/components/service/pocket/src/androidTest/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabaseTest.kt
new file mode 100644
index 0000000000..f31f41a318
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/androidTest/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabaseTest.kt
@@ -0,0 +1,723 @@
+/* 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.service.pocket.stories.db
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.runBlocking
+import mozilla.components.service.pocket.spocs.db.SpocEntity
+import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase.Companion
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+private const val MIGRATION_TEST_DB = "migration-test"
+
+class PocketRecommendationsDatabaseTest {
+ private lateinit var context: Context
+ private lateinit var executor: ExecutorService
+ private lateinit var database: PocketRecommendationsDatabase
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ PocketRecommendationsDatabase::class.java,
+ )
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+
+ context = ApplicationProvider.getApplicationContext()
+ database = Room.inMemoryDatabaseBuilder(context, PocketRecommendationsDatabase::class.java).build()
+ }
+
+ @After
+ fun tearDown() {
+ executor.shutdown()
+ database.clearAllTables()
+ }
+
+ @Test
+ fun `test1To2MigrationAddsNewSpocsTable`() = runBlocking {
+ // Create the database with the version 1 schema
+ val dbVersion1 = helper.createDatabase(MIGRATION_TEST_DB, 1).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_STORIES}' " +
+ "(url, title, imageUrl, publisher, category, timeToRead, timesShown) " +
+ "VALUES (" +
+ "'${story.url}'," +
+ "'${story.title}'," +
+ "'${story.imageUrl}'," +
+ "'${story.publisher}'," +
+ "'${story.category}'," +
+ "'${story.timeToRead}'," +
+ "'${story.timesShown}'" +
+ ")",
+ )
+ }
+ // Validate the persisted data which will be re-checked after migration
+ dbVersion1.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ ),
+ )
+ }
+
+ // Migrate the initial database to the version 2 schema
+ val dbVersion2 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB,
+ 2,
+ true,
+ Migrations.migration_1_2,
+ ).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' " +
+ "(url, title, imageUrl, sponsor, clickShim, impressionShim) " +
+ "VALUES (" +
+ "'${spoc.url}'," +
+ "'${spoc.title}'," +
+ "'${spoc.imageUrl}'," +
+ "'${spoc.sponsor}'," +
+ "'${spoc.clickShim}'," +
+ "'${spoc.impressionShim}'" +
+ ")",
+ )
+ }
+ // Re-check the initial data we had
+ dbVersion2.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ ),
+ )
+ }
+ // Finally validate that the new spocs are persisted successfully
+ dbVersion2.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(spoc.url, cursor.getString(0))
+ assertEquals(spoc.title, cursor.getString(1))
+ assertEquals(spoc.imageUrl, cursor.getString(2))
+ assertEquals(spoc.sponsor, cursor.getString(3))
+ assertEquals(spoc.clickShim, cursor.getString(4))
+ assertEquals(spoc.impressionShim, cursor.getString(5))
+ }
+ }
+
+ @Test
+ fun `test2To3MigrationDropsOldSpocsTableAndAddsNewSpocsAndSpocsImpressionsTables`() = runBlocking {
+ // Create the database with the version 2 schema
+ val dbVersion2 = helper.createDatabase(MIGRATION_TEST_DB, 2).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${Companion.TABLE_NAME_STORIES}' " +
+ "(url, title, imageUrl, publisher, category, timeToRead, timesShown) " +
+ "VALUES (" +
+ "'${story.url}'," +
+ "'${story.title}'," +
+ "'${story.imageUrl}'," +
+ "'${story.publisher}'," +
+ "'${story.category}'," +
+ "'${story.timeToRead}'," +
+ "'${story.timesShown}'" +
+ ")",
+ )
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' " +
+ "(url, title, imageUrl, sponsor, clickShim, impressionShim) " +
+ "VALUES (" +
+ "'${spoc.url}'," +
+ "'${spoc.title}'," +
+ "'${spoc.imageUrl}'," +
+ "'${spoc.sponsor}'," +
+ "'${spoc.clickShim}'," +
+ "'${spoc.impressionShim}'" +
+ ")",
+ )
+ }
+
+ // Validate the recommended stories data which will be re-checked after migration
+ dbVersion2.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_STORIES}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ ),
+ )
+ }
+
+ // Migrate to v3 database
+ val dbVersion3 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB,
+ 3,
+ true,
+ Migrations.migration_2_3,
+ )
+
+ // Check that recommended stories are unchanged.
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ ),
+ )
+ }
+
+ // Finally validate that we have two new empty tables for spocs and spocs impressions.
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}",
+ ).use { cursor ->
+ assertEquals(0, cursor.count)
+ assertEquals(11, cursor.columnCount)
+
+ assertEquals("id", cursor.getColumnName(0))
+ assertEquals("url", cursor.getColumnName(1))
+ assertEquals("title", cursor.getColumnName(2))
+ assertEquals("imageUrl", cursor.getColumnName(3))
+ assertEquals("sponsor", cursor.getColumnName(4))
+ assertEquals("clickShim", cursor.getColumnName(5))
+ assertEquals("impressionShim", cursor.getColumnName(6))
+ assertEquals("priority", cursor.getColumnName(7))
+ assertEquals("lifetimeCapCount", cursor.getColumnName(8))
+ assertEquals("flightCapCount", cursor.getColumnName(9))
+ assertEquals("flightCapPeriod", cursor.getColumnName(10))
+ }
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}",
+ ).use { cursor ->
+ assertEquals(0, cursor.count)
+ assertEquals(3, cursor.columnCount)
+
+ assertEquals("spocId", cursor.getColumnName(0))
+ assertEquals("impressionId", cursor.getColumnName(1))
+ assertEquals("impressionDateInSeconds", cursor.getColumnName(2))
+ }
+ }
+
+ @Test
+ fun `test1To3MigrationAddsNewSpocsAndSpocsImpressionsTables`() = runBlocking {
+ // Create the database with the version 1 schema
+ val dbVersion1 = helper.createDatabase(MIGRATION_TEST_DB, 1).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${Companion.TABLE_NAME_STORIES}' " +
+ "(url, title, imageUrl, publisher, category, timeToRead, timesShown) " +
+ "VALUES (" +
+ "'${story.url}'," +
+ "'${story.title}'," +
+ "'${story.imageUrl}'," +
+ "'${story.publisher}'," +
+ "'${story.category}'," +
+ "'${story.timeToRead}'," +
+ "'${story.timesShown}'" +
+ ")",
+ )
+ }
+ // Validate the persisted data which will be re-checked after migration
+ dbVersion1.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_STORIES}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ ),
+ )
+ }
+
+ val impression = SpocImpressionEntity(spoc.id).apply {
+ impressionId = 1
+ impressionDateInSeconds = 700L
+ }
+ // Migrate the initial database to the version 2 schema
+ val dbVersion3 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB,
+ 3,
+ true,
+ Migrations.migration_1_3,
+ ).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' (" +
+ "id, url, title, imageUrl, sponsor, clickShim, impressionShim, " +
+ "priority, lifetimeCapCount, flightCapCount, flightCapPeriod" +
+ ") VALUES (" +
+ "'${spoc.id}'," +
+ "'${spoc.url}'," +
+ "'${spoc.title}'," +
+ "'${spoc.imageUrl}'," +
+ "'${spoc.sponsor}'," +
+ "'${spoc.clickShim}'," +
+ "'${spoc.impressionShim}'," +
+ "'${spoc.priority}'," +
+ "'${spoc.lifetimeCapCount}'," +
+ "'${spoc.flightCapCount}'," +
+ "'${spoc.flightCapPeriod}'" +
+ ")",
+ )
+
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}' (" +
+ "spocId, impressionId, impressionDateInSeconds" +
+ ") VALUES (" +
+ "'${impression.spocId}'," +
+ "'${impression.impressionId}'," +
+ "'${impression.impressionDateInSeconds}'" +
+ ")",
+ )
+ }
+ // Re-check the initial data we had
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ ),
+ )
+ }
+ // Finally validate that the new spocs are persisted successfully
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(spoc.id, cursor.getInt(0))
+ assertEquals(spoc.url, cursor.getString(1))
+ assertEquals(spoc.title, cursor.getString(2))
+ assertEquals(spoc.imageUrl, cursor.getString(3))
+ assertEquals(spoc.sponsor, cursor.getString(4))
+ assertEquals(spoc.clickShim, cursor.getString(5))
+ assertEquals(spoc.impressionShim, cursor.getString(6))
+ assertEquals(spoc.priority, cursor.getInt(7))
+ assertEquals(spoc.lifetimeCapCount, cursor.getInt(8))
+ assertEquals(spoc.flightCapCount, cursor.getInt(9))
+ assertEquals(spoc.flightCapPeriod, cursor.getInt(10))
+ }
+ // And that the impression was also persisted successfully
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(impression.spocId, cursor.getInt(0))
+ assertEquals(impression.impressionId, cursor.getInt(1))
+ assertEquals(impression.impressionDateInSeconds, cursor.getLong(2))
+ }
+ }
+
+ @Test
+ fun `test3To4MigrationAddsNewIndexKeepsOldDataAndAllowsNewData`() = runBlocking {
+ // Create the database with the version 3 schema
+ val dbVersion3 = helper.createDatabase(MIGRATION_TEST_DB, 3).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${Companion.TABLE_NAME_STORIES}' " +
+ "(url, title, imageUrl, publisher, category, timeToRead, timesShown) " +
+ "VALUES (" +
+ "'${story.url}'," +
+ "'${story.title}'," +
+ "'${story.imageUrl}'," +
+ "'${story.publisher}'," +
+ "'${story.category}'," +
+ "'${story.timeToRead}'," +
+ "'${story.timesShown}'" +
+ ")",
+ )
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' (" +
+ "id, url, title, imageUrl, sponsor, clickShim, impressionShim, " +
+ "priority, lifetimeCapCount, flightCapCount, flightCapPeriod" +
+ ") VALUES (" +
+ "'${spoc.id}'," +
+ "'${spoc.url}'," +
+ "'${spoc.title}'," +
+ "'${spoc.imageUrl}'," +
+ "'${spoc.sponsor}'," +
+ "'${spoc.clickShim}'," +
+ "'${spoc.impressionShim}'," +
+ "'${spoc.priority}'," +
+ "'${spoc.lifetimeCapCount}'," +
+ "'${spoc.flightCapCount}'," +
+ "'${spoc.flightCapPeriod}'" +
+ ")",
+ )
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}' (" +
+ "spocId, impressionId, impressionDateInSeconds" +
+ ") VALUES (" +
+ "${spoc.id}, 0, 1" +
+ ")",
+ )
+ // Add a new impression of the same spoc to test proper the index uniqueness
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}' (" +
+ "spocId, impressionId, impressionDateInSeconds" +
+ ") VALUES (" +
+ "${spoc.id}, 1, 2" +
+ ")",
+ )
+ }
+
+ // Validate the data before migration
+ dbVersion3.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_STORIES}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ ),
+ )
+ }
+ dbVersion3.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_SPOCS}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ spoc,
+ SpocEntity(
+ id = cursor.getInt(0),
+ url = cursor.getString(1),
+ title = cursor.getString(2),
+ imageUrl = cursor.getString(3),
+ sponsor = cursor.getString(4),
+ clickShim = cursor.getString(5),
+ impressionShim = cursor.getString(6),
+ priority = cursor.getInt(7),
+ lifetimeCapCount = cursor.getInt(8),
+ flightCapCount = cursor.getInt(9),
+ flightCapPeriod = cursor.getInt(10),
+ ),
+ )
+ }
+ dbVersion3.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_SPOCS_IMPRESSIONS}",
+ ).use { cursor ->
+ assertEquals(2, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(spoc.id, cursor.getInt(0))
+ cursor.moveToNext()
+ assertEquals(spoc.id, cursor.getInt(0))
+ }
+
+ // Migrate to v4 database
+ val dbVersion4 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB,
+ 4,
+ true,
+ Migrations.migration_3_4,
+ )
+
+ // Check that we have the same data as before. Just that a new index was added for faster queries.
+ dbVersion4.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_STORIES}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ ),
+ )
+ }
+ dbVersion4.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_SPOCS}",
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ spoc,
+ SpocEntity(
+ id = cursor.getInt(0),
+ url = cursor.getString(1),
+ title = cursor.getString(2),
+ imageUrl = cursor.getString(3),
+ sponsor = cursor.getString(4),
+ clickShim = cursor.getString(5),
+ impressionShim = cursor.getString(6),
+ priority = cursor.getInt(7),
+ lifetimeCapCount = cursor.getInt(8),
+ flightCapCount = cursor.getInt(9),
+ flightCapPeriod = cursor.getInt(10),
+ ),
+ )
+ }
+ dbVersion4.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_SPOCS_IMPRESSIONS}",
+ ).use { cursor ->
+ assertEquals(2, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(spoc.id, cursor.getInt(0))
+ cursor.moveToNext()
+ assertEquals(spoc.id, cursor.getInt(0))
+ }
+
+ // After adding an index check that inserting new data works as expected
+ val otherSpoc = spoc.copy(
+ id = spoc.id + 2,
+ url = spoc.url + "2",
+ title = spoc.title + "2",
+ imageUrl = spoc.imageUrl + "2",
+ sponsor = spoc.sponsor + "2",
+ clickShim = spoc.clickShim + "2",
+ impressionShim = spoc.impressionShim + "2",
+ priority = spoc.priority + 2,
+ lifetimeCapCount = spoc.lifetimeCapCount - 2,
+ flightCapCount = spoc.flightCapPeriod * 2,
+ flightCapPeriod = spoc.flightCapPeriod / 2,
+ )
+ dbVersion4.execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' (" +
+ "id, url, title, imageUrl, sponsor, clickShim, impressionShim, " +
+ "priority, lifetimeCapCount, flightCapCount, flightCapPeriod" +
+ ") VALUES (" +
+ "'${otherSpoc.id}'," +
+ "'${otherSpoc.url}'," +
+ "'${otherSpoc.title}'," +
+ "'${otherSpoc.imageUrl}'," +
+ "'${otherSpoc.sponsor}'," +
+ "'${otherSpoc.clickShim}'," +
+ "'${otherSpoc.impressionShim}'," +
+ "'${otherSpoc.priority}'," +
+ "'${otherSpoc.lifetimeCapCount}'," +
+ "'${otherSpoc.flightCapCount}'," +
+ "'${otherSpoc.flightCapPeriod}'" +
+ ")",
+ )
+ dbVersion4.execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}' (" +
+ "spocId, impressionId, impressionDateInSeconds" +
+ ") VALUES (" +
+ "${spoc.id}, 22, 33" +
+ ")",
+ )
+ // Test a new spoc and a new impressions of it are properly recorded.Z
+ dbVersion4.execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}' (" +
+ "spocId, impressionId, impressionDateInSeconds" +
+ ") VALUES (" +
+ "${otherSpoc.id}, 23, 34" +
+ ")",
+ )
+ // Add a new impression of the same spoc to test proper the index uniqueness
+ dbVersion4.execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}' (" +
+ "spocId, impressionId, impressionDateInSeconds" +
+ ") VALUES (" +
+ "${otherSpoc.id}, 24, 35" +
+ ")",
+ )
+ dbVersion4.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_SPOCS} ORDER BY 'id'",
+ ).use { cursor ->
+ assertEquals(2, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ spoc,
+ SpocEntity(
+ id = cursor.getInt(0),
+ url = cursor.getString(1),
+ title = cursor.getString(2),
+ imageUrl = cursor.getString(3),
+ sponsor = cursor.getString(4),
+ clickShim = cursor.getString(5),
+ impressionShim = cursor.getString(6),
+ priority = cursor.getInt(7),
+ lifetimeCapCount = cursor.getInt(8),
+ flightCapCount = cursor.getInt(9),
+ flightCapPeriod = cursor.getInt(10),
+ ),
+ )
+
+ cursor.moveToNext()
+ assertEquals(
+ otherSpoc,
+ SpocEntity(
+ id = cursor.getInt(0),
+ url = cursor.getString(1),
+ title = cursor.getString(2),
+ imageUrl = cursor.getString(3),
+ sponsor = cursor.getString(4),
+ clickShim = cursor.getString(5),
+ impressionShim = cursor.getString(6),
+ priority = cursor.getInt(7),
+ lifetimeCapCount = cursor.getInt(8),
+ flightCapCount = cursor.getInt(9),
+ flightCapPeriod = cursor.getInt(10),
+ ),
+ )
+ }
+ dbVersion4.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_SPOCS_IMPRESSIONS} ORDER BY 'impressionId'",
+ ).use { cursor ->
+ assertEquals(5, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(spoc.id, cursor.getInt(0))
+ assertEquals(0, cursor.getInt(1))
+ assertEquals(1, cursor.getInt(2))
+ cursor.moveToNext()
+ assertEquals(spoc.id, cursor.getInt(0))
+ assertEquals(1, cursor.getInt(1))
+ assertEquals(2, cursor.getInt(2))
+ cursor.moveToNext()
+ assertEquals(spoc.id, cursor.getInt(0))
+ assertEquals(22, cursor.getInt(1))
+ assertEquals(33, cursor.getInt(2))
+ cursor.moveToNext()
+ assertEquals(otherSpoc.id, cursor.getInt(0))
+ assertEquals(23, cursor.getInt(1))
+ assertEquals(34, cursor.getInt(2))
+ cursor.moveToNext()
+ assertEquals(otherSpoc.id, cursor.getInt(0))
+ assertEquals(24, cursor.getInt(1))
+ assertEquals(35, cursor.getInt(2))
+ }
+ }
+}
+
+private val story = PocketStoryEntity(
+ title = "How to Get Rid of Black Mold Naturally",
+ url = "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F6757%252F1628024495_6109ae86db6cc.png",
+ publisher = "Pocket",
+ category = "general",
+ timeToRead = 4,
+ timesShown = 23,
+)
+
+private val spoc = SpocEntity(
+ id = 191739319,
+ url = "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
+ title = "Eating Keto Has Never Been So Easy With Green Chef",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
+ sponsor = "Green Chef",
+ clickShim = "193815086ClickShim",
+ impressionShim = "193815086ImpressionShim",
+ priority = 3,
+ lifetimeCapCount = 50,
+ flightCapCount = 10,
+ flightCapPeriod = 86400,
+)
diff --git a/mobile/android/android-components/components/service/pocket/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/pocket/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/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/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt
new file mode 100644
index 0000000000..f05dd7dbe7
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt
@@ -0,0 +1,69 @@
+/* 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.service.pocket
+
+import android.annotation.SuppressLint
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+
+/**
+ * Provides global access to the dependencies needed for updating Pocket stories.
+ */
+internal object GlobalDependencyProvider {
+ internal object RecommendedStories {
+ /**
+ * Possible actions regarding the list of recommended stories.
+ */
+ @SuppressLint("StaticFieldLeak")
+ internal var useCases: PocketStoriesUseCases? = null
+ private set
+
+ /**
+ * Convenience method for setting all details used when communicating with the Pocket server.
+ *
+ * @param useCases [PocketStoriesUseCases] containing all possible actions regarding
+ * the list of recommended stories.
+ */
+ internal fun initialize(
+ useCases: PocketStoriesUseCases,
+ ) {
+ this.useCases = useCases
+ }
+
+ /**
+ * Convenience method for cleaning up any resources held for communicating with the Pocket server.
+ */
+ internal fun reset() {
+ this.useCases = null
+ }
+ }
+
+ internal object SponsoredStories {
+ /**
+ * Possible actions regarding the list of sponsored stories.
+ */
+ @SuppressLint("StaticFieldLeak")
+ internal var useCases: SpocsUseCases? = null
+ private set
+
+ /**
+ * Convenience method for setting all details used when communicating with the Pocket server.
+ *
+ * @param useCases [SpocsUseCases] containing all possible actions regarding the list of sponsored stories.
+ */
+ internal fun initialize(
+ useCases: SpocsUseCases,
+ ) {
+ this.useCases = useCases
+ }
+
+ /**
+ * Convenience method for cleaning up any resources held for communicating with the Pocket server.
+ */
+ internal fun reset() {
+ useCases = null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/Logger.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/Logger.kt
new file mode 100644
index 0000000000..ab1732b707
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/Logger.kt
@@ -0,0 +1,12 @@
+/* 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.service.pocket
+
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Internal logger for the ":service-pocket" module.
+ */
+internal val logger = Logger("service-pocket")
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt
new file mode 100644
index 0000000000..ba077afb0b
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt
@@ -0,0 +1,68 @@
+/* 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.service.pocket
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.support.base.worker.Frequency
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+
+internal const val DEFAULT_SPONSORED_STORIES_SITE_ID = "1240699"
+internal const val DEFAULT_REFRESH_INTERVAL = 4L
+internal const val DEFAULT_SPONSORED_STORIES_REFRESH_INTERVAL = 4L
+
+@Suppress("TopLevelPropertyNaming")
+internal val DEFAULT_REFRESH_TIMEUNIT = TimeUnit.HOURS
+
+@Suppress("TopLevelPropertyNaming")
+internal val DEFAULT_SPONSORED_STORIES_REFRESH_TIMEUNIT = TimeUnit.HOURS
+
+/**
+ * Indicating all details for how the pocket stories should be refreshed.
+ *
+ * @param client [Client] implementation used for downloading the Pocket stories.
+ * @param frequency Optional - The interval at which to try and refresh items. Defaults to 4 hours.
+ * @param profile Optional - The profile used for downloading sponsored Pocket stories.
+ * @param sponsoredStoriesRefreshFrequency Optional - The interval at which to try and refresh sponsored stories.
+ * Defaults to 4 hours.
+ * @param sponsoredStoriesParams Optional - Configuration containing parameters used to get the spoc content.
+ */
+class PocketStoriesConfig(
+ val client: Client,
+ val frequency: Frequency = Frequency(
+ DEFAULT_REFRESH_INTERVAL,
+ DEFAULT_REFRESH_TIMEUNIT,
+ ),
+ val profile: Profile? = null,
+ val sponsoredStoriesRefreshFrequency: Frequency = Frequency(
+ DEFAULT_SPONSORED_STORIES_REFRESH_INTERVAL,
+ DEFAULT_SPONSORED_STORIES_REFRESH_TIMEUNIT,
+ ),
+ val sponsoredStoriesParams: PocketStoriesRequestConfig = PocketStoriesRequestConfig(),
+)
+
+/**
+ * Configuration for sponsored stories request indicating parameters used to get spoc content.
+ *
+ * @property siteId Optional - ID of the site parameter, should be used with care as it changes the
+ * set of sponsored stories fetched from the server.
+ * @property country Optional - Value of the country parameter, shall be used with care as it allows
+ * overriding the IP location and receiving a set of sponsored stories not suited for the real location.
+ * @property city Optional - Value of the city parameter, shall be used with care as it allows
+ * overriding the IP location and receiving a set of sponsored stories not suited for the real location.
+ */
+class PocketStoriesRequestConfig(
+ val siteId: String = DEFAULT_SPONSORED_STORIES_SITE_ID,
+ val country: String = "",
+ val city: String = "",
+)
+
+/**
+ * Sponsored stories configuration data.
+ *
+ * @param profileId Unique profile identifier which will be presented with sponsored stories.
+ * @param appId Unique identifier of the application using this feature.
+ */
+class Profile(val profileId: UUID, val appId: String)
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt
new file mode 100644
index 0000000000..6688d75926
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt
@@ -0,0 +1,172 @@
+/* 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.service.pocket
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+import mozilla.components.service.pocket.update.PocketStoriesRefreshScheduler
+import mozilla.components.service.pocket.update.SpocsRefreshScheduler
+
+/**
+ * Allows for getting a list of pocket stories based on the provided [PocketStoriesConfig]
+ *
+ * @param context Android Context. Prefer sending application context to limit the possibility of even small leaks.
+ * @param pocketStoriesConfig configuration for how and what pocket stories to get.
+ */
+class PocketStoriesService(
+ private val context: Context,
+ private val pocketStoriesConfig: PocketStoriesConfig,
+) {
+ @VisibleForTesting
+ internal var storiesRefreshScheduler = PocketStoriesRefreshScheduler(pocketStoriesConfig)
+
+ @VisibleForTesting
+ internal var spocsRefreshscheduler = SpocsRefreshScheduler(pocketStoriesConfig)
+
+ @VisibleForTesting
+ internal var storiesUseCases = PocketStoriesUseCases(
+ appContext = context,
+ fetchClient = pocketStoriesConfig.client,
+ )
+
+ @VisibleForTesting
+ internal var spocsUseCases = when (pocketStoriesConfig.profile) {
+ null -> {
+ logger.debug("Missing profile for sponsored stories")
+ null
+ }
+ else -> SpocsUseCases(
+ appContext = context,
+ fetchClient = pocketStoriesConfig.client,
+ profileId = pocketStoriesConfig.profile.profileId,
+ appId = pocketStoriesConfig.profile.appId,
+ sponsoredStoriesParams = pocketStoriesConfig.sponsoredStoriesParams,
+ )
+ }
+
+ /**
+ * Entry point to start fetching Pocket stories in the background.
+ *
+ * Use this at an as high as possible level in your application.
+ * Must be paired in a similar way with the [stopPeriodicStoriesRefresh] method.
+ *
+ * This starts the process of downloading and caching Pocket stories in the background,
+ * making them available for the [getStories] method.
+ */
+ fun startPeriodicStoriesRefresh() {
+ GlobalDependencyProvider.RecommendedStories.initialize(storiesUseCases)
+ storiesRefreshScheduler.schedulePeriodicRefreshes(context)
+ }
+
+ /**
+ * Single stopping point for the "get Pocket stories" functionality.
+ *
+ * Use this at an as high as possible level in your application.
+ * Must be paired in a similar way with the [startPeriodicStoriesRefresh] method.
+ *
+ * This stops the process of downloading and caching Pocket stories in the background.
+ */
+ fun stopPeriodicStoriesRefresh() {
+ storiesRefreshScheduler.stopPeriodicRefreshes(context)
+ GlobalDependencyProvider.RecommendedStories.reset()
+ }
+
+ /**
+ * Get a list of Pocket recommended stories based on the initial configuration.
+ *
+ * To be called after [startPeriodicStoriesRefresh] to ensure the recommendations are up-to-date.
+ * Might return an empty list or a list of older than expected stories if
+ * [startPeriodicStoriesRefresh] hasn't yet completed.
+ */
+ suspend fun getStories(): List<PocketRecommendedStory> {
+ return storiesUseCases.getStories()
+ }
+
+ /**
+ * Entry point to start fetching Pocket sponsored stories in the background.
+ *
+ * Use this at an as high as possible level in your application.
+ * Must be paired in a similar way with the [stopPeriodicSponsoredStoriesRefresh] method.
+ *
+ * This starts the process of downloading and caching Pocket sponsored stories in the background,
+ * making them available for the [getSponsoredStories] method.
+ */
+ fun startPeriodicSponsoredStoriesRefresh() {
+ val useCases = spocsUseCases
+ if (useCases == null) {
+ logger.warn("Cannot start sponsored stories refresh. Service has incomplete setup")
+ return
+ }
+
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ spocsRefreshscheduler.stopProfileDeletion(context)
+ spocsRefreshscheduler.schedulePeriodicRefreshes(context)
+ }
+
+ /**
+ * Single stopping point for the "refresh sponsored Pocket stories" functionality.
+ *
+ * Use this at an as high as possible level in your application.
+ * Must be paired in a similar way with the [startPeriodicSponsoredStoriesRefresh] method.
+ *
+ * This stops the process of downloading and caching Pocket sponsored stories in the background.
+ */
+ fun stopPeriodicSponsoredStoriesRefresh() {
+ spocsRefreshscheduler.stopPeriodicRefreshes(context)
+ }
+
+ /**
+ * Fetch sponsored Pocket stories and refresh the locally persisted list.
+ */
+ suspend fun refreshSponsoredStories() {
+ spocsUseCases?.refreshStories?.invoke()
+ }
+
+ /**
+ * Get a list of Pocket sponsored stories based on the initial configuration.
+ */
+ suspend fun getSponsoredStories(): List<PocketSponsoredStory> {
+ return spocsUseCases?.getStories?.invoke() ?: emptyList()
+ }
+
+ /**
+ * Delete all stored user data used for downloading personalized sponsored stories.
+ * This returns immediately but will handle the profile deletion in background.
+ */
+ fun deleteProfile() {
+ val useCases = spocsUseCases
+ if (useCases == null) {
+ logger.warn("Cannot delete sponsored stories profile. Service has incomplete setup")
+ return
+ }
+
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ spocsRefreshscheduler.stopPeriodicRefreshes(context)
+ spocsRefreshscheduler.scheduleProfileDeletion(context)
+ }
+
+ /**
+ * Update how many times certain stories were shown to the user.
+ *
+ * Safe to call from any background thread.
+ * Automatically synchronized with the other [PocketStoriesService] methods.
+ */
+ suspend fun updateStoriesTimesShown(updatedStories: List<PocketRecommendedStory>) {
+ storiesUseCases.updateTimesShown(updatedStories)
+ }
+
+ /**
+ * Persist locally that the sponsored Pocket stories containing the ids from [storiesShown]
+ * were shown to the user.
+ * This is safe to call with any ids, even ones for stories not currently persisted anymore.
+ */
+ suspend fun recordStoriesImpressions(storiesShown: List<Int>) {
+ spocsUseCases?.recordImpression?.invoke(storiesShown)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStory.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStory.kt
new file mode 100644
index 0000000000..702641268d
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStory.kt
@@ -0,0 +1,99 @@
+/* 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.service.pocket
+
+/**
+ * A Pocket story downloaded from the Internet and intended to be displayed in the application.
+ */
+sealed class PocketStory {
+ /**
+ * Title of the story.
+ */
+ abstract val title: String
+
+ /**
+ * Url where the story can be full read.
+ */
+ abstract val url: String
+
+ /**
+ * A Pocket recommended story.
+ *
+ * @property title The title of the story.
+ * @property url A "pocket.co" shortlink for the original story's page.
+ * @property imageUrl A url to a still image representing the story.
+ * @property publisher Optional publisher name/domain, e.g. "The New Yorker" / "nationalgeographic.co.uk"".
+ * **May be empty**.
+ * @property category Topic of interest under which similar stories are grouped.
+ * @property timeToRead Inferred time needed to read the entire story. **May be -1**.
+ */
+ data class PocketRecommendedStory(
+ override val title: String,
+ override val url: String,
+ val imageUrl: String,
+ val publisher: String,
+ val category: String,
+ val timeToRead: Int,
+ val timesShown: Long,
+ ) : PocketStory()
+
+ /**
+ * A Pocket sponsored story.
+ *
+ * @property id Unique id of this story.
+ * @property title The title of the story.
+ * @property url 3rd party url containing the original story.
+ * @property imageUrl A url to a still image representing the story.
+ * Contains a "resize" parameter in the form of "resize=w618-h310" allowing to get the image
+ * with a specific resolution and the CENTER_CROP ScaleType.
+ * @property sponsor 3rd party sponsor of this story, e.g. "NextAdvisor".
+ * @property shim Unique identifiers for when the user interacts with this story.
+ * @property priority Priority level in deciding which stories to be shown first.
+ * A lowest number means a higher priority.
+ * @property caps Story caps indented to control the maximum number of times the story should be shown.
+ */
+ data class PocketSponsoredStory(
+ val id: Int,
+ override val title: String,
+ override val url: String,
+ val imageUrl: String,
+ val sponsor: String,
+ val shim: PocketSponsoredStoryShim,
+ val priority: Int,
+ val caps: PocketSponsoredStoryCaps,
+ ) : PocketStory()
+
+ /**
+ * Sponsored story unique identifiers intended to be used in telemetry.
+ *
+ * @property click Unique identifier for when the sponsored story is clicked.
+ * @property impression Unique identifier for when the user sees this sponsored story.
+ */
+ data class PocketSponsoredStoryShim(
+ val click: String,
+ val impression: String,
+ )
+
+ /**
+ * Sponsored story caps indented to control the maximum number of times the story should be shown.
+ *
+ * @property currentImpressions List of all recorded impression of a sponsored Pocket story
+ * expressed in seconds from Epoch (as the result of `System.currentTimeMillis / 1000`).
+ * @property lifetimeCount Lifetime maximum number of times this story should be shown.
+ * This is independent from the count based on [flightCount] and [flightPeriod] and must never be reset.
+ * @property flightCount Maximum number of times this story should be shown in [flightPeriod].
+ * @property flightPeriod Period expressed as a number of seconds in which this story should be shown
+ * for at most [flightCount] times.
+ * Any time the period comes to an end the [flightCount] count should be restarted.
+ * Even if based on [flightCount] and [flightCount] this story can still be shown a couple more times
+ * if [lifetimeCount] was met then the story should not be shown anymore.
+ */
+ data class PocketSponsoredStoryCaps(
+ val currentImpressions: List<Long> = emptyList(),
+ val lifetimeCount: Int,
+ val flightCount: Int,
+ val flightPeriod: Int,
+ )
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/ConceptFetch.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/ConceptFetch.kt
new file mode 100644
index 0000000000..24e4f0871f
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/ConceptFetch.kt
@@ -0,0 +1,30 @@
+/* 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.service.pocket.ext
+
+import androidx.annotation.WorkerThread
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.service.pocket.logger
+import java.io.IOException
+
+// extension functions for :concept-fetch module.
+
+/**
+ * @return returns the string contained within the response body for the given [request] or null, on error.
+ */
+@WorkerThread // synchronous network call.
+internal fun Client.fetchBodyOrNull(request: Request): String? {
+ val response: Response? = try {
+ fetch(request)
+ } catch (e: IOException) {
+ logger.debug("network error", e)
+ null
+ }
+
+ return response?.use { if (response.isSuccess) response.body.string() else null }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt
new file mode 100644
index 0000000000..42df3c1507
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt
@@ -0,0 +1,99 @@
+/* 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.service.pocket.ext
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryShim
+import mozilla.components.service.pocket.spocs.api.ApiSpoc
+import mozilla.components.service.pocket.spocs.db.SpocEntity
+import mozilla.components.service.pocket.stories.api.PocketApiStory
+import mozilla.components.service.pocket.stories.db.PocketLocalStoryTimesShown
+import mozilla.components.service.pocket.stories.db.PocketStoryEntity
+
+@VisibleForTesting
+internal const val DEFAULT_CATEGORY = "general"
+
+@VisibleForTesting
+internal const val DEFAULT_TIMES_SHOWN = 0L
+
+/**
+ * Map Pocket API objects to the object type that we persist locally.
+ */
+internal fun PocketApiStory.toPocketLocalStory(): PocketStoryEntity =
+ PocketStoryEntity(
+ url,
+ title,
+ imageUrl,
+ publisher,
+ category,
+ timeToRead,
+ DEFAULT_TIMES_SHOWN,
+ )
+
+/**
+ * Map Room entities to the object type that we expose to service clients.
+ */
+internal fun PocketStoryEntity.toPocketRecommendedStory(): PocketRecommendedStory =
+ PocketRecommendedStory(
+ url = url,
+ title = title,
+ imageUrl = imageUrl,
+ publisher = publisher,
+ category = if (category.isNotBlank()) category else DEFAULT_CATEGORY,
+ timeToRead = timeToRead,
+ timesShown = timesShown,
+ )
+
+/**
+ * Maps an object of the type exposed to clients to one that can partially update only the "timesShown"
+ * property of the type we persist locally.
+ */
+internal fun PocketRecommendedStory.toPartialTimeShownUpdate(): PocketLocalStoryTimesShown =
+ PocketLocalStoryTimesShown(url, timesShown)
+
+/**
+ * Map sponsored Pocket stories to the object type that we persist locally.
+ */
+internal fun ApiSpoc.toLocalSpoc(): SpocEntity =
+ SpocEntity(
+ id = id,
+ url = url,
+ title = title,
+ imageUrl = imageSrc,
+ sponsor = sponsor,
+ clickShim = shim.click,
+ impressionShim = shim.impression,
+ priority = priority,
+ lifetimeCapCount = caps.lifetimeCount,
+ flightCapCount = caps.flightCount,
+ flightCapPeriod = caps.flightPeriod,
+ )
+
+/**
+ * Map Room entities to the object type that we expose to service clients.
+ */
+internal fun SpocEntity.toPocketSponsoredStory(
+ impressions: List<Long> = emptyList(),
+) = PocketSponsoredStory(
+ id = id,
+ title = title,
+ url = url,
+ imageUrl = imageUrl,
+ sponsor = sponsor,
+ shim = PocketSponsoredStoryShim(
+ click = clickShim,
+ impression = impressionShim,
+ ),
+ priority = priority,
+ caps = PocketSponsoredStoryCaps(
+ currentImpressions = impressions,
+ lifetimeCount = lifetimeCapCount,
+ flightCount = flightCapCount,
+ flightPeriod = flightCapPeriod,
+ ),
+)
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt
new file mode 100644
index 0000000000..f111e84747
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt
@@ -0,0 +1,50 @@
+/* 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.service.pocket.ext
+
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
+import java.util.concurrent.TimeUnit
+
+/**
+ * Get a list of all story impressions (expressed in seconds from Epoch) in the period between
+ * `now` down to [PocketSponsoredStoryCaps.flightPeriod].
+ */
+fun PocketSponsoredStory.getCurrentFlightImpressions(): List<Long> {
+ val now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
+ return caps.currentImpressions.filter {
+ now - it < caps.flightPeriod
+ }
+}
+
+/**
+ * Get if this story was already shown for the maximum number of times available in it's lifetime.
+ */
+fun PocketSponsoredStory.hasLifetimeImpressionsLimitReached(): Boolean {
+ return caps.currentImpressions.size >= caps.lifetimeCount
+}
+
+/**
+ * Get if this story was already shown for the maximum number of times available in the period
+ * specified by [PocketSponsoredStoryCaps.flightPeriod].
+ */
+fun PocketSponsoredStory.hasFlightImpressionsLimitReached(): Boolean {
+ return getCurrentFlightImpressions().size >= caps.flightCount
+}
+
+/**
+ * Record a new impression at this instant time and get this story back with updated impressions details.
+ * This only updates the in-memory data.
+ *
+ * It's recommended to use this method anytime a new impression needs to be recorded for a `PocketSponsoredStory`
+ * to ensure values consistency.
+ */
+fun PocketSponsoredStory.recordNewImpression(): PocketSponsoredStory {
+ return this.copy(
+ caps = caps.copy(
+ currentImpressions = caps.currentImpressions + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt
new file mode 100644
index 0000000000..609b6ae935
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt
@@ -0,0 +1,69 @@
+/* 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.service.pocket.spocs
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.ext.toLocalSpoc
+import mozilla.components.service.pocket.ext.toPocketSponsoredStory
+import mozilla.components.service.pocket.spocs.api.ApiSpoc
+import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+
+/**
+ * Wrapper over our local database containing Spocs.
+ * Allows for easy CRUD operations.
+ */
+internal class SpocsRepository(context: Context) {
+ private val database: Lazy<PocketRecommendationsDatabase> = lazy { PocketRecommendationsDatabase.get(context) }
+
+ @VisibleForTesting
+ internal val spocsDao by lazy { database.value.spocsDao() }
+
+ /**
+ * Get the current locally persisted list of sponsored Pocket stories
+ * complete with the list of all locally persisted impressions data.
+ */
+ suspend fun getAllSpocs(): List<PocketSponsoredStory> {
+ val spocs = spocsDao.getAllSpocs()
+ val impressions = spocsDao.getSpocsImpressions().groupBy { it.spocId }
+
+ return spocs.map { spoc ->
+ spoc.toPocketSponsoredStory(
+ impressions[spoc.id]
+ ?.map { impression -> impression.impressionDateInSeconds }
+ ?: emptyList(),
+ )
+ }
+ }
+
+ /**
+ * Delete all currently persisted sponsored Pocket stories.
+ */
+ suspend fun deleteAllSpocs() {
+ spocsDao.deleteAllSpocs()
+ }
+
+ /**
+ * Replace the current list of locally persisted sponsored Pocket stories.
+ *
+ * @param spocs The list of sponsored Pocket stories to persist locally.
+ */
+ suspend fun addSpocs(spocs: List<ApiSpoc>) {
+ spocsDao.cleanOldAndInsertNewSpocs(spocs.map { it.toLocalSpoc() })
+ }
+
+ /**
+ * Add a new impression record for each of the spocs identified by the ids from [spocsShown].
+ * Will ignore adding new entries if the intended spocs are not persisted locally anymore.
+ * Recorded entries will automatically be cleaned when the spoc they target is deleted.
+ *
+ * @param spocsShown List of [PocketSponsoredStory.id] for which to record new impressions.
+ */
+ suspend fun recordImpressions(spocsShown: List<Int>) {
+ spocsDao.recordImpressions(spocsShown.map { SpocImpressionEntity(it) })
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt
new file mode 100644
index 0000000000..d0f7e8fa75
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt
@@ -0,0 +1,186 @@
+/* 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.service.pocket.spocs
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStoriesRequestConfig
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.spocs.api.SpocsEndpoint
+import mozilla.components.service.pocket.stories.api.PocketResponse.Failure
+import mozilla.components.service.pocket.stories.api.PocketResponse.Success
+import java.util.UUID
+
+/**
+ * Possible actions regarding the list of sponsored stories.
+ *
+ * @param appContext Android Context. Prefer sending application context to limit the possibility of even small leaks.
+ * @param fetchClient the HTTP client to use for network requests.
+ * @param profileId Unique profile identifier used for downloading sponsored Pocket stories.
+ * @param appId Unique app identifier used for downloading sponsored Pocket stories.
+ * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
+ */
+internal class SpocsUseCases(
+ private val appContext: Context,
+ private val fetchClient: Client,
+ private val profileId: UUID,
+ private val appId: String,
+ private val sponsoredStoriesParams: PocketStoriesRequestConfig,
+) {
+ /**
+ * Download and persist an updated list of sponsored stories.
+ */
+ internal val refreshStories by lazy {
+ RefreshSponsoredStories(appContext, fetchClient, profileId, appId)
+ }
+
+ /**
+ * Get the list of available Pocket sponsored stories.
+ */
+ internal val getStories by lazy {
+ GetSponsoredStories(appContext)
+ }
+
+ internal val recordImpression by lazy {
+ RecordImpression(appContext)
+ }
+
+ /**
+ * Delete all stored user data used for downloading sponsored stories.
+ */
+ internal val deleteProfile by lazy {
+ DeleteProfile(appContext, fetchClient, profileId, appId)
+ }
+
+ /**
+ * Allows for refreshing the list of Pocket sponsored stories we have cached.
+ *
+ * @param appContext Android Context. Prefer sending application context to limit the possibility
+ * of even small leaks.
+ * @param fetchClient the HTTP client to use for network requests.
+ * @param profileId Unique profile identifier when using this feature.
+ * @param appId Unique identifier of the application using this feature.
+ * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
+ */
+ internal inner class RefreshSponsoredStories(
+ @get:VisibleForTesting
+ internal val appContext: Context = this@SpocsUseCases.appContext,
+ @get:VisibleForTesting
+ internal val fetchClient: Client = this@SpocsUseCases.fetchClient,
+ @get:VisibleForTesting
+ internal val profileId: UUID = this@SpocsUseCases.profileId,
+ @get:VisibleForTesting
+ internal val appId: String = this@SpocsUseCases.appId,
+ @get:VisibleForTesting
+ internal val sponsoredStoriesParams: PocketStoriesRequestConfig = this@SpocsUseCases.sponsoredStoriesParams,
+ ) {
+ /**
+ * Do a full download from Pocket -> persist locally cycle for sponsored stories.
+ */
+ suspend operator fun invoke(): Boolean {
+ val provider = getSpocsProvider(fetchClient, profileId, appId, sponsoredStoriesParams)
+ val response = provider.getSponsoredStories()
+
+ if (response is Success) {
+ getSpocsRepository(appContext).addSpocs(response.data)
+ return true
+ }
+
+ return false
+ }
+ }
+
+ /**
+ * Allows for querying the list of available Pocket sponsored stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
+
+ */
+ internal inner class GetSponsoredStories(
+ @get:VisibleForTesting
+ internal val context: Context = this@SpocsUseCases.appContext,
+ ) {
+ /**
+ * Do an internet query for a list of Pocket sponsored stories.
+ */
+ suspend operator fun invoke(): List<PocketSponsoredStory> {
+ return getSpocsRepository(context).getAllSpocs()
+ }
+ }
+
+ /**
+ * Allows for atomically updating the [PocketRecommendedStory.timesShown] property of some recommended stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
+ */
+ internal inner class RecordImpression(
+ @get:VisibleForTesting
+ internal val context: Context = this@SpocsUseCases.appContext,
+ ) {
+ /**
+ * Update how many times certain stories were shown to the user.
+ */
+ suspend operator fun invoke(storiesShown: List<Int>) {
+ if (storiesShown.isNotEmpty()) {
+ getSpocsRepository(context).recordImpressions(storiesShown)
+ }
+ }
+ }
+
+ /**
+ * Allows deleting all stored user data used for downloading sponsored stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
+ * @param fetchClient the HTTP client to use for network requests.
+ * @param profileId Unique profile identifier previously used for downloading sponsored Pocket stories.
+ * @param appId Unique app identifier previously used for downloading sponsored Pocket stories.
+ * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
+ */
+ internal inner class DeleteProfile(
+ @get:VisibleForTesting
+ internal val context: Context = this@SpocsUseCases.appContext,
+ @get:VisibleForTesting
+ internal val fetchClient: Client = this@SpocsUseCases.fetchClient,
+ @get:VisibleForTesting
+ internal val profileId: UUID = this@SpocsUseCases.profileId,
+ @get:VisibleForTesting
+ internal val appId: String = this@SpocsUseCases.appId,
+ @get:VisibleForTesting
+ internal val sponsoredStoriesParams: PocketStoriesRequestConfig = this@SpocsUseCases.sponsoredStoriesParams,
+ ) {
+ /**
+ * Delete all stored user data used for downloading personalized sponsored stories.
+ */
+ suspend operator fun invoke(): Boolean {
+ val provider = getSpocsProvider(fetchClient, profileId, appId, sponsoredStoriesParams)
+ return when (provider.deleteProfile()) {
+ is Success -> {
+ getSpocsRepository(context).deleteAllSpocs()
+ true
+ }
+ is Failure -> {
+ // Don't attempt to delete locally persisted stories to prevent mismatching issues
+ // with profile deletion failing - applications still "showing it" but
+ // with no sponsored articles to show.
+ false
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getSpocsRepository(context: Context) = SpocsRepository(context)
+
+ @VisibleForTesting
+ internal fun getSpocsProvider(
+ client: Client,
+ profileId: UUID,
+ appId: String,
+ sponsoredStoriesParams: PocketStoriesRequestConfig,
+ ) =
+ SpocsEndpoint.newInstance(client, profileId, appId, sponsoredStoriesParams)
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt
new file mode 100644
index 0000000000..7f89df2ab7
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt
@@ -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/. */
+
+package mozilla.components.service.pocket.spocs.api
+
+/**
+ * A Pocket sponsored as downloaded from the sponsored stories endpoint.
+ *
+ * @property id Unique id of this story.
+ * @property title the title of the story.
+ * @property url 3rd party url containing the original story.
+ * @property imageSrc a url to a still image representing the story.
+ * Contains a "resize" parameter in the form of "resize=w618-h310" allowing to get the image
+ * with a specific resolution and the CENTER_CROP ScaleType.
+ * @property sponsor 3rd party sponsor of this story, e.g. "NextAdvisor".
+ * @property shim Unique identifiers for when the user interacts with this story.
+ * @property priority Priority level in deciding which stories to be shown first.
+ * A lowest number means a higher priority.
+ * @property caps Story caps indented to control the maximum number of times the story should be shown.
+ */
+internal data class ApiSpoc(
+ val id: Int,
+ val title: String,
+ val url: String,
+ val imageSrc: String,
+ val sponsor: String,
+ val shim: ApiSpocShim,
+ val priority: Int,
+ val caps: ApiSpocCaps,
+)
+
+/**
+ * Sponsored story unique identifiers intended to be used in telemetry.
+ *
+ * @property click Unique identifier for when the sponsored story is clicked.
+ * @property impression Unique identifier for when the user sees this sponsored story.
+ */
+internal data class ApiSpocShim(
+ val click: String,
+ val impression: String,
+)
+
+/**
+ * Sponsored story caps indented to control the maximum number of times the story should be shown.
+ *
+ * @property lifetimeCount Lifetime maximum number of times this story should be shown.
+ * This is independent from the count based on [flightCount] and [flightPeriod] and must never be reset.
+ * @property flightCount Maximum number of times this story should be shown in [flightPeriod].
+ * @property flightPeriod Period expressed as a number of seconds in which this story should be shown
+ * for at most [flightCount] times.
+ * Any time the period comes to an end the [flightCount] count should be restarted.
+ * Even if based on [flightCount] and [flightCount] this story can still be shown a couple more times
+ * if [lifetimeCount] was met then the story should not be shown anymore.
+ */
+internal data class ApiSpocCaps(
+ val lifetimeCount: Int,
+ val flightCount: Int,
+ val flightPeriod: Int,
+)
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt
new file mode 100644
index 0000000000..a17bebd6ba
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt
@@ -0,0 +1,67 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStoriesRequestConfig
+import mozilla.components.service.pocket.spocs.api.SpocsEndpoint.Companion.newInstance
+import mozilla.components.service.pocket.stories.api.PocketEndpoint.Companion.newInstance
+import mozilla.components.service.pocket.stories.api.PocketResponse
+import java.util.UUID
+
+/**
+ * Makes requests to the sponsored stories API and returns the requested data.
+ *
+ * @see [newInstance] to retrieve an instance.
+ */
+internal class SpocsEndpoint internal constructor(
+ @get:VisibleForTesting internal val rawEndpoint: SpocsEndpointRaw,
+ private val jsonParser: SpocsJSONParser,
+) : SpocsProvider {
+
+ /**
+ * Download a new list of sponsored Pocket stories.
+ *
+ * If the API returns unexpectedly formatted results, these entries will be omitted and the rest of the items are
+ * returned.
+ *
+ * @return a [PocketResponse.Success] with the sponsored Pocket stories (list may be empty)
+ * or [PocketResponse.Failure] if the request didn't complete successfully.
+ */
+ @WorkerThread
+ override suspend fun getSponsoredStories(): PocketResponse<List<ApiSpoc>> {
+ val response = rawEndpoint.getSponsoredStories()
+ val spocs = if (response.isNullOrBlank()) null else jsonParser.jsonToSpocs(response)
+ return PocketResponse.wrap(spocs)
+ }
+
+ @WorkerThread
+ override suspend fun deleteProfile(): PocketResponse<Boolean> {
+ val response = rawEndpoint.deleteProfile()
+ return PocketResponse.wrap(response)
+ }
+
+ companion object {
+ /**
+ * Returns a new instance of [SpocsEndpoint].
+ *
+ * @param client the HTTP client to use for network requests.
+ * @param profileId Unique profile identifier which will be presented with sponsored stories.
+ * @param appId Unique identifier of the application using this feature.
+ * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
+ */
+ fun newInstance(
+ client: Client,
+ profileId: UUID,
+ appId: String,
+ sponsoredStoriesParams: PocketStoriesRequestConfig,
+ ): SpocsEndpoint {
+ val rawEndpoint = SpocsEndpointRaw.newInstance(client, profileId, appId, sponsoredStoriesParams)
+ return SpocsEndpoint(rawEndpoint, SpocsJSONParser)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt
new file mode 100644
index 0000000000..8b1639a7fc
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt
@@ -0,0 +1,178 @@
+/* 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.service.pocket.spocs.api
+
+import android.net.Uri
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Request.Body
+import mozilla.components.concept.fetch.Request.Method
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.service.pocket.BuildConfig
+import mozilla.components.service.pocket.PocketStoriesRequestConfig
+import mozilla.components.service.pocket.ext.fetchBodyOrNull
+import mozilla.components.service.pocket.logger
+import mozilla.components.service.pocket.spocs.api.SpocsEndpointRaw.Companion.newInstance
+import mozilla.components.service.pocket.stories.api.PocketEndpointRaw.Companion.newInstance
+import org.json.JSONObject
+import java.io.IOException
+import java.util.UUID
+
+private const val SPOCS_ENDPOINT_DEV_BASE_URL = "https://spocs.getpocket.dev/"
+private const val SPOCS_ENDPOINT_PROD_BASE_URL = "https://spocs.getpocket.com/"
+private const val SPOCS_ENDPOINT_DOWNLOAD_SPOCS_PATH = "spocs"
+private const val SPOCS_ENDPOINT_DELETE_PROFILE_PATH = "user"
+private const val SPOCS_PROXY_VERSION_KEY = "version"
+private const val SPOCS_PROXY_VERSION_VALUE = 2
+private const val SPOCS_PROXY_PROFILE_KEY = "pocket_id"
+private const val SPOCS_PROXY_APP_KEY = "consumer_key"
+private const val SPOCS_PROXY_SITE_KEY = "site"
+private const val SPOCS_PROXY_COUNTRY_KEY = "country"
+private const val SPOCS_PROXY_CITY_KEY = "city"
+
+/**
+ * Makes requests to the Pocket endpoint and returns the raw JSON data.
+ *
+ * @see [SpocsEndpoint], which wraps this to make it more practical.
+ * @see [newInstance] to retrieve an instance.
+ */
+internal class SpocsEndpointRaw internal constructor(
+ @get:VisibleForTesting internal val client: Client,
+ @get:VisibleForTesting internal val profileId: UUID,
+ @get:VisibleForTesting internal val appId: String,
+ @get:VisibleForTesting internal val sponsoredStoriesParams: PocketStoriesRequestConfig,
+) {
+ /**
+ * Gets the current sponsored stories recommendations from the Pocket server.
+ *
+ * @return The stories recommendations as a raw JSON string or null on error.
+ */
+ @WorkerThread
+ fun getSponsoredStories(): String? {
+ val url = Uri.Builder()
+ .encodedPath(baseUrl + SPOCS_ENDPOINT_DOWNLOAD_SPOCS_PATH)
+ if (sponsoredStoriesParams.siteId.isNotBlank()) {
+ url.appendQueryParameter(SPOCS_PROXY_SITE_KEY, sponsoredStoriesParams.siteId)
+ }
+ if (sponsoredStoriesParams.country.isNotBlank()) {
+ url.appendQueryParameter(SPOCS_PROXY_COUNTRY_KEY, sponsoredStoriesParams.country)
+ }
+ if (sponsoredStoriesParams.city.isNotBlank()) {
+ url.appendQueryParameter(SPOCS_PROXY_CITY_KEY, sponsoredStoriesParams.city)
+ }
+ url.build()
+
+ val request = Request(
+ url = url.toString(),
+ method = Method.POST,
+ headers = getRequestHeaders(),
+ body = getDownloadStoriesRequestBody(),
+ conservative = true,
+ )
+ return client.fetchBodyOrNull(request)
+ }
+
+ /**
+ * Request to delete all data stored on server about [profileId].
+ *
+ * @return [Boolean] indicating whether the delete operation was successful or not.
+ */
+ @WorkerThread
+ fun deleteProfile(): Boolean {
+ val url = Uri.Builder()
+ .encodedPath(baseUrl + SPOCS_ENDPOINT_DELETE_PROFILE_PATH)
+ if (sponsoredStoriesParams.siteId.isNotBlank()) {
+ url.appendQueryParameter(SPOCS_PROXY_SITE_KEY, sponsoredStoriesParams.siteId)
+ }
+ if (sponsoredStoriesParams.country.isNotBlank()) {
+ url.appendQueryParameter(SPOCS_PROXY_COUNTRY_KEY, sponsoredStoriesParams.country)
+ }
+ if (sponsoredStoriesParams.city.isNotBlank()) {
+ url.appendQueryParameter(SPOCS_PROXY_CITY_KEY, sponsoredStoriesParams.city)
+ }
+ url.build()
+
+ val request = Request(
+ url = url.toString(),
+ method = Method.DELETE,
+ headers = getRequestHeaders(),
+ body = getDeleteProfileRequestBody(),
+ conservative = true,
+ )
+
+ val response: Response? = try {
+ client.fetch(request)
+ } catch (e: IOException) {
+ logger.debug("Network error", e)
+ null
+ }
+
+ response?.close()
+ return response?.isSuccess ?: false
+ }
+
+ private fun getRequestHeaders() = MutableHeaders(
+ "Content-Type" to "application/json; charset=UTF-8",
+ "Accept" to "*/*",
+ )
+
+ private fun getDownloadStoriesRequestBody(): Body {
+ val params = mapOf(
+ SPOCS_PROXY_VERSION_KEY to SPOCS_PROXY_VERSION_VALUE,
+ SPOCS_PROXY_PROFILE_KEY to profileId.toString(),
+ SPOCS_PROXY_APP_KEY to appId,
+ )
+
+ return Body(JSONObject(params).toString().byteInputStream())
+ }
+
+ private fun getDeleteProfileRequestBody(): Body {
+ val params = mapOf(
+ SPOCS_PROXY_PROFILE_KEY to profileId.toString(),
+ )
+
+ return Body(JSONObject(params).toString().byteInputStream())
+ }
+
+ companion object {
+ /**
+ * Returns a new instance of [SpocsEndpointRaw].
+ *
+ * @param client HTTP client to use for network requests.
+ * @param profileId Unique profile identifier which will be presented with sponsored stories.
+ * @param appId Unique identifier of the application using this feature.
+ * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
+ */
+ fun newInstance(
+ client: Client,
+ profileId: UUID,
+ appId: String,
+ sponsoredStoriesParams: PocketStoriesRequestConfig,
+ ): SpocsEndpointRaw {
+ return SpocsEndpointRaw(client, profileId, appId, sponsoredStoriesParams)
+ }
+
+ /**
+ * Convenience for checking whether the current build is a debug build and overwriting this in tests.
+ */
+ @VisibleForTesting
+ internal var isDebugBuild = BuildConfig.DEBUG
+
+ /**
+ * Get the base url for sponsored stories specific to development or production.
+ */
+ @VisibleForTesting
+ internal val baseUrl
+ get() = if (isDebugBuild) {
+ SPOCS_ENDPOINT_DEV_BASE_URL
+ } else {
+ SPOCS_ENDPOINT_PROD_BASE_URL
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt
new file mode 100644
index 0000000000..b8fe1ea4e3
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt
@@ -0,0 +1,91 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.logger
+import mozilla.components.support.ktx.android.org.json.mapNotNull
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+@VisibleForTesting
+internal const val KEY_ARRAY_SPOCS = "spocs"
+
+@VisibleForTesting
+internal const val JSON_SPOC_SHIMS_KEY = "shim"
+
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_KEY = "caps"
+
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_LIFETIME_KEY = "lifetime"
+
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_FLIGHT_KEY = "campaign"
+
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_FLIGHT_COUNT_KEY = "count"
+
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY = "period"
+private const val JSON_SPOC_ID_KEY = "id"
+private const val JSON_SPOC_TITLE_KEY = "title"
+private const val JSON_SPOC_SPONSOR_KEY = "sponsor"
+private const val JSON_SPOC_URL_KEY = "url"
+private const val JSON_SPOC_IMAGE_SRC_KEY = "image_src"
+private const val JSON_SPOC_SHIM_CLICK_KEY = "click"
+private const val JSON_SPOC_SHIM_IMPRESSION_KEY = "impression"
+private const val JSON_SPOC_PRIORITY = "priority"
+
+/**
+ * Holds functions that parse the JSON returned by the Pocket API and converts them to more usable Kotlin types.
+ */
+internal object SpocsJSONParser {
+ /**
+ * @return The stories, removing entries that are invalid, or null on error; the list will never be empty.
+ */
+ fun jsonToSpocs(json: String): List<ApiSpoc>? = try {
+ val rawJSON = JSONObject(json)
+ val spocsJSON = rawJSON.getJSONArray(KEY_ARRAY_SPOCS)
+ val spocs = spocsJSON.mapNotNull(JSONArray::getJSONObject) { jsonToSpoc(it) }
+
+ // We return null, rather than the empty list, because devs might forget to check an empty list.
+ spocs.ifEmpty { null }
+ } catch (e: JSONException) {
+ logger.warn("invalid JSON from the SPOCS endpoint", e)
+ null
+ }
+
+ private fun jsonToSpoc(json: JSONObject): ApiSpoc? = try {
+ ApiSpoc(
+ id = json.getInt(JSON_SPOC_ID_KEY),
+ title = json.getString(JSON_SPOC_TITLE_KEY),
+ sponsor = json.getString(JSON_SPOC_SPONSOR_KEY),
+ url = json.getString(JSON_SPOC_URL_KEY),
+ imageSrc = json.getString(JSON_SPOC_IMAGE_SRC_KEY),
+ shim = jsonToShim(json.getJSONObject(JSON_SPOC_SHIMS_KEY)),
+ priority = json.getInt(JSON_SPOC_PRIORITY),
+ caps = jsonToCaps(json.getJSONObject(JSON_SPOC_CAPS_KEY)),
+ )
+ } catch (e: JSONException) {
+ null
+ }
+
+ private fun jsonToShim(json: JSONObject) = ApiSpocShim(
+ click = json.getString(JSON_SPOC_SHIM_CLICK_KEY),
+ impression = json.getString(JSON_SPOC_SHIM_IMPRESSION_KEY),
+ )
+
+ private fun jsonToCaps(json: JSONObject): ApiSpocCaps {
+ val flightCaps = json.getJSONObject(JSON_SPOC_CAPS_FLIGHT_KEY)
+
+ return ApiSpocCaps(
+ lifetimeCount = json.getInt(JSON_SPOC_CAPS_LIFETIME_KEY),
+ flightCount = flightCaps.getInt(JSON_SPOC_CAPS_FLIGHT_COUNT_KEY),
+ flightPeriod = flightCaps.getInt(JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt
new file mode 100644
index 0000000000..dcb5819cd9
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt
@@ -0,0 +1,27 @@
+/* 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.service.pocket.spocs.api
+
+import mozilla.components.service.pocket.stories.api.PocketResponse
+
+/**
+ * All possible operations related to SPocs - Sponsored Pocket stories.
+ */
+internal interface SpocsProvider {
+ /**
+ * Download new sponsored stories.
+ *
+ * @return [PocketResponse.Success] containing a list of sponsored stories or
+ * [PocketResponse.Failure] if the request didn't complete successfully.
+ */
+ suspend fun getSponsoredStories(): PocketResponse<List<ApiSpoc>>
+
+ /**
+ * Delete all data associated with [profileId].
+ *
+ * @return [PocketResponse.Success] if the request completed successfully, [PocketResponse.Failure] otherwise.
+ */
+ suspend fun deleteProfile(): PocketResponse<Boolean>
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocEntity.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocEntity.kt
new file mode 100644
index 0000000000..02c68b7845
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocEntity.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.service.pocket.spocs.db
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+
+/**
+ * A sponsored Pocket story that is to be mapped to SQLite table.
+ *
+ * @property id Unique story id serving as the primary key of this entity.
+ * @property url URL where the original story can be read.
+ * @property title Title of the story.
+ * @property imageUrl URL of the hero image for this story.
+ * @property sponsor 3rd party sponsor of this story, e.g. "NextAdvisor".
+ * @property clickShim Telemetry identifier for when the sponsored story is clicked.
+ * @property impressionShim Telemetry identifier for when the sponsored story is seen by the user.
+ * @property priority Priority level in deciding which stories to be shown first.
+ * @property lifetimeCapCount Indicates how many times a sponsored story can be shown in total.
+ * @property flightCapCount Indicates how many times a sponsored story can be shown within a period.
+ * @property flightCapPeriod Indicates the period (number of seconds) in which at most [flightCapCount]
+ * stories can be shown.
+ */
+@Entity(tableName = PocketRecommendationsDatabase.TABLE_NAME_SPOCS)
+internal data class SpocEntity(
+ @PrimaryKey
+ val id: Int,
+ val url: String,
+ val title: String,
+ val imageUrl: String,
+ val sponsor: String,
+ val clickShim: String,
+ val impressionShim: String,
+ val priority: Int,
+ val lifetimeCapCount: Int,
+ val flightCapCount: Int,
+ val flightCapPeriod: Int,
+)
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntity.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntity.kt
new file mode 100644
index 0000000000..25878bbf67
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntity.kt
@@ -0,0 +1,45 @@
+/* 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.service.pocket.spocs.db
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+
+/**
+ * One sponsored Pocket story impression.
+ * Allows to easily create a relation between a particular spoc identified by it's [SpocEntity.id]
+ * and any number of impressions.
+ *
+ * @property spocId [SpocEntity.id] that this serves as an impression of.
+ * Used as a foreign key allowing to only add impressions for other persisted spocs and
+ * automatically remove all impressions when the spoc they refer to is deleted.
+ * @property impressionId Unique id of this entity. Primary key.
+ * @property impressionDateInSeconds Epoch based timestamp expressed in seconds (from System.currentTimeMillis / 1000)
+ * for when the spoc identified by [spocId] was shown to the user.
+ */
+@Entity(
+ tableName = PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS,
+ foreignKeys = [
+ ForeignKey(
+ entity = SpocEntity::class,
+ parentColumns = arrayOf("id"),
+ childColumns = arrayOf("spocId"),
+ onDelete = ForeignKey.CASCADE,
+ ),
+ ],
+ indices = [
+ Index(value = ["spocId"], unique = false),
+ ],
+)
+internal data class SpocImpressionEntity(
+ val spocId: Int,
+) {
+ @PrimaryKey(autoGenerate = true)
+ var impressionId: Int = 0
+ var impressionDateInSeconds: Long = System.currentTimeMillis() / 1000
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt
new file mode 100644
index 0000000000..fadfec87c3
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt
@@ -0,0 +1,73 @@
+/* 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.service.pocket.spocs.db
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+import java.util.concurrent.TimeUnit
+
+@Dao
+internal interface SpocsDao {
+ @Transaction
+ suspend fun cleanOldAndInsertNewSpocs(spocs: List<SpocEntity>) {
+ val newSpocs = spocs.map { it.id }
+ val oldStoriesToDelete = getAllSpocs()
+ .filterNot { newSpocs.contains(it.id) }
+
+ deleteSpocs(oldStoriesToDelete)
+ insertSpocs(spocs)
+ }
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE) // Maybe some details changed
+ suspend fun insertSpocs(stories: List<SpocEntity>)
+
+ @Transaction
+ suspend fun recordImpressions(stories: List<SpocImpressionEntity>) {
+ stories.forEach {
+ recordImpression(it.spocId, it.impressionDateInSeconds)
+ }
+ }
+
+ /**
+ * INSERT OR IGNORE method needed to prevent against "FOREIGN KEY constraint failed" exceptions
+ * if clients try to insert new impressions spocs not existing anymore in the database in cases where
+ * a different list of spocs were downloaded but the client operates with stale in-memory data.
+ *
+ * @param targetSpocId The `id` of the [SpocEntity] to add a new impression for.
+ * A new impression will be persisted only if a story with the indicated [targetSpocId] currently exists.
+ * @param targetImpressionDateInSeconds The timestamp expressed in seconds from Epoch for this impression.
+ * Defaults to the current time expressed in seconds as get from `System.currentTimeMillis / 1000`.
+ */
+ @Query(
+ "WITH newImpression(spocId, impressionDateInSeconds) AS (VALUES" +
+ "(:targetSpocId, :targetImpressionDateInSeconds)" +
+ ")" +
+ "INSERT INTO spocs_impressions(spocId, impressionDateInSeconds) " +
+ "SELECT impression.spocId, impression.impressionDateInSeconds " +
+ "FROM newImpression impression " +
+ "WHERE EXISTS (SELECT 1 FROM spocs spoc WHERE spoc.id = impression.spocId)",
+ )
+ suspend fun recordImpression(
+ targetSpocId: Int,
+ targetImpressionDateInSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
+ )
+
+ @Query("DELETE FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}")
+ suspend fun deleteAllSpocs()
+
+ @Delete
+ suspend fun deleteSpocs(stories: List<SpocEntity>)
+
+ @Query("SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}")
+ suspend fun getAllSpocs(): List<SpocEntity>
+
+ @Query("SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}")
+ suspend fun getSpocsImpressions(): List<SpocImpressionEntity>
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepository.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepository.kt
new file mode 100644
index 0000000000..8bc6be41e1
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepository.kt
@@ -0,0 +1,47 @@
+/* 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.service.pocket.stories
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.ext.toPartialTimeShownUpdate
+import mozilla.components.service.pocket.ext.toPocketLocalStory
+import mozilla.components.service.pocket.ext.toPocketRecommendedStory
+import mozilla.components.service.pocket.stories.api.PocketApiStory
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+
+/**
+ * Wrapper over our local database.
+ * Allows for easy CRUD operations.
+ */
+internal class PocketRecommendationsRepository(context: Context) {
+ private val database: Lazy<PocketRecommendationsDatabase> = lazy { PocketRecommendationsDatabase.get(context) }
+
+ @VisibleForTesting
+ internal val pocketRecommendationsDao by lazy { database.value.pocketRecommendationsDao() }
+
+ /**
+ * Get the current locally persisted list of Pocket recommended articles.
+ */
+ suspend fun getPocketRecommendedStories(): List<PocketRecommendedStory> {
+ return pocketRecommendationsDao.getPocketStories().map { it.toPocketRecommendedStory() }
+ }
+
+ suspend fun updateShownPocketRecommendedStories(updatedStories: List<PocketRecommendedStory>) {
+ return pocketRecommendationsDao.updateTimesShown(
+ updatedStories.map { it.toPartialTimeShownUpdate() },
+ )
+ }
+
+ /**
+ * Replace the current list of locally persisted Pocket recommended articles.
+ *
+ * @param stories The list of Pocket recommended articles to persist locally.
+ */
+ suspend fun addAllPocketApiStories(stories: List<PocketApiStory>) {
+ pocketRecommendationsDao.cleanOldAndInsertNewPocketStories(stories.map { it.toPocketLocalStory() })
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketStoriesUseCases.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketStoriesUseCases.kt
new file mode 100644
index 0000000000..5d50824524
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketStoriesUseCases.kt
@@ -0,0 +1,112 @@
+/* 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.service.pocket.stories
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.stories.api.PocketEndpoint
+import mozilla.components.service.pocket.stories.api.PocketResponse
+
+/**
+ * Possible actions regarding the list of recommended stories.
+ *
+ * @param appContext Android Context. Prefer sending application context to limit the possibility of even small leaks.
+ * @param fetchClient the HTTP client to use for network requests.
+ */
+internal class PocketStoriesUseCases(
+ private val appContext: Context,
+ private val fetchClient: Client,
+) {
+ /**
+ * Download and persist an updated list of recommended stories.
+ */
+ internal val refreshStories by lazy { RefreshPocketStories(appContext, fetchClient) }
+
+ /**
+ * Get the list of available Pocket sponsored stories.
+ */
+ internal val getStories by lazy { GetPocketStories(appContext) }
+
+ /**
+ * Atomically update the number of impressions for a list of Pocket recommended stories.
+ */
+ internal val updateTimesShown by lazy { UpdateStoriesTimesShown(appContext) }
+
+ /**
+ * Allows for refreshing the list of pocket stories we have cached.
+ *
+ * @param appContext Android Context. Prefer sending application context to limit the possibility
+ * of even small leaks.
+ * @param fetchClient the HTTP client to use for network requests.
+ */
+ internal inner class RefreshPocketStories(
+ @get:VisibleForTesting
+ internal val appContext: Context = this@PocketStoriesUseCases.appContext,
+ @get:VisibleForTesting
+ internal val fetchClient: Client = this@PocketStoriesUseCases.fetchClient,
+ ) {
+ /**
+ * Do a full download from Pocket -> persist locally cycle for recommended stories.
+ */
+ suspend operator fun invoke(): Boolean {
+ val pocket = getPocketEndpoint(fetchClient)
+ val response = pocket.getRecommendedStories()
+
+ if (response is PocketResponse.Success) {
+ getPocketRepository(appContext)
+ .addAllPocketApiStories(response.data)
+ return true
+ }
+
+ return false
+ }
+ }
+
+ /**
+ * Allows for querying the list of locally available Pocket recommended stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
+ */
+ internal inner class GetPocketStories(
+ @get:VisibleForTesting
+ internal val context: Context = this@PocketStoriesUseCases.appContext,
+ ) {
+ /**
+ * Returns the current locally persisted list of Pocket recommended stories.
+ */
+ suspend operator fun invoke(): List<PocketRecommendedStory> {
+ return getPocketRepository(context)
+ .getPocketRecommendedStories()
+ }
+ }
+
+ /**
+ * Allows for atomically updating the [PocketRecommendedStory.timesShown] property of some recommended stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
+ */
+ internal inner class UpdateStoriesTimesShown(
+ @get:VisibleForTesting
+ internal val context: Context = this@PocketStoriesUseCases.appContext,
+ ) {
+ /**
+ * Update how many times certain stories were shown to the user.
+ */
+ suspend operator fun invoke(storiesShown: List<PocketRecommendedStory>) {
+ if (storiesShown.isNotEmpty()) {
+ getPocketRepository(context)
+ .updateShownPocketRecommendedStories(storiesShown)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getPocketRepository(context: Context) = PocketRecommendationsRepository(context)
+
+ @VisibleForTesting
+ internal fun getPocketEndpoint(client: Client) = PocketEndpoint.newInstance(client)
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketApiStory.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketApiStory.kt
new file mode 100644
index 0000000000..203dd9c35d
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketApiStory.kt
@@ -0,0 +1,28 @@
+/* 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.service.pocket.stories.api
+
+internal const val STRING_NOT_FOUND_DEFAULT_VALUE = ""
+internal const val INT_NOT_FOUND_DEFAULT_VALUE = -1
+
+/**
+ * A Pocket recommended story as downloaded from home-recommendations endpoint
+ *
+ * @property title the title of the story.
+ * @property url a "pocket.co" shortlink for the original story's page.
+ * @property imageUrl a url to a still image representing the story.
+ * @property publisher optional publisher name/domain, e.g. "The New Yorker" / "nationalgeographic.co.uk"".
+ * **May be empty**.
+ * @property category topic of interest under which similar stories are grouped. **May be empty**.
+ * @property timeToRead inferred time needed to read the entire story. **May be -1**.
+ */
+internal data class PocketApiStory(
+ val title: String,
+ val url: String,
+ val imageUrl: String,
+ val publisher: String,
+ val category: String,
+ val timeToRead: Int,
+)
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpoint.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpoint.kt
new file mode 100644
index 0000000000..d447293e52
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpoint.kt
@@ -0,0 +1,49 @@
+/* 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.service.pocket.stories.api
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.stories.api.PocketEndpoint.Companion.newInstance
+
+/**
+ * Makes requests to the Pocket API and returns the requested data.
+ *
+ * @see [newInstance] to retrieve an instance.
+ */
+internal class PocketEndpoint internal constructor(
+ @get:VisibleForTesting internal val rawEndpoint: PocketEndpointRaw,
+ private val jsonParser: PocketJSONParser,
+) {
+
+ /**
+ * Gets a response, filled with the Pocket stories recommendations from the Pocket API server on success.
+ *
+ * If the API returns unexpectedly formatted results, these entries will be omitted and the rest of the items are
+ * returned.
+ *
+ * @return a [PocketResponse.Success] with the Pocket stories recommendations (the list will never be empty)
+ * or, on error, a [PocketResponse.Failure].
+ */
+ @WorkerThread
+ fun getRecommendedStories(): PocketResponse<List<PocketApiStory>> {
+ val response = rawEndpoint.getRecommendedStories()
+ val stories = response?.let { jsonParser.jsonToPocketApiStories(it) }
+ return PocketResponse.wrap(stories)
+ }
+
+ companion object {
+ /**
+ * Returns a new instance of [PocketEndpoint].
+ *
+ * @param client the HTTP client to use for network requests.
+ */
+ fun newInstance(client: Client): PocketEndpoint {
+ val rawEndpoint = PocketEndpointRaw.newInstance(client)
+ return PocketEndpoint(rawEndpoint, PocketJSONParser())
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpointRaw.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpointRaw.kt
new file mode 100644
index 0000000000..bba2adcfbc
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpointRaw.kt
@@ -0,0 +1,52 @@
+/* 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.service.pocket.stories.api
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.service.pocket.ext.fetchBodyOrNull
+import mozilla.components.service.pocket.stories.api.PocketEndpointRaw.Companion.newInstance
+
+/**
+ * Makes requests to the Pocket endpoint and returns the raw JSON data.
+ *
+ * @see [PocketEndpoint], which wraps this to make it more practical.
+ * @see [newInstance] to retrieve an instance.
+ */
+internal class PocketEndpointRaw internal constructor(
+ @get:VisibleForTesting internal val client: Client,
+) {
+ /**
+ * Gets the current stories recommendations from the Pocket server.
+ *
+ * @return The stories recommendations as a raw JSON string or null on error.
+ */
+ @WorkerThread
+ fun getRecommendedStories(): String? = makeRequest()
+
+ /**
+ * @return The requested JSON as a String or null on error.
+ */
+ @WorkerThread // synchronous request.
+ private fun makeRequest(): String? {
+ val request = Request(pocketEndpointUrl, conservative = true)
+ return client.fetchBodyOrNull(request)
+ }
+
+ companion object {
+ private const val pocketEndpointUrl = "https://firefox-android-home-recommendations.getpocket.com/"
+
+ /**
+ * Returns a new instance of [PocketEndpointRaw].
+ *
+ * @param client the HTTP client to use for network requests.
+ */
+ fun newInstance(client: Client): PocketEndpointRaw {
+ return PocketEndpointRaw(client)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketJSONParser.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketJSONParser.kt
new file mode 100644
index 0000000000..a2de1017dd
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketJSONParser.kt
@@ -0,0 +1,78 @@
+/* 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.service.pocket.stories.api
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.logger
+import mozilla.components.support.ktx.android.org.json.mapNotNull
+import mozilla.components.support.ktx.android.org.json.tryGetInt
+import mozilla.components.support.ktx.android.org.json.tryGetString
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+private const val JSON_STORY_TITLE_KEY = "title"
+private const val JSON_STORY_URL_KEY = "url"
+private const val JSON_STORY_IMAGE_URL_KEY = "imageUrl"
+private const val JSON_STORY_PUBLISHER_KEY = "publisher"
+private const val JSON_STORY_CATEGORY_KEY = "category"
+private const val JSON_STORY_TIME_TO_READ_KEY = "timeToRead"
+
+/**
+ * Holds functions that parse the JSON returned by the Pocket API and converts them to more usable Kotlin types.
+ */
+internal class PocketJSONParser {
+ /**
+ * @return The stories, removing entries that are invalid, or null on error; the list will never be empty.
+ */
+ fun jsonToPocketApiStories(json: String): List<PocketApiStory>? = try {
+ val rawJSON = JSONObject(json)
+ val storiesJSON = rawJSON.getJSONArray(KEY_ARRAY_ITEMS)
+ val stories = storiesJSON.mapNotNull(JSONArray::getJSONObject) { jsonToPocketApiStory(it) }
+
+ // We return null, rather than the empty list, because devs might forget to check an empty list.
+ stories.ifEmpty { null }
+ } catch (e: JSONException) {
+ logger.warn("invalid JSON from the Pocket endpoint", e)
+ null
+ }
+
+ private fun jsonToPocketApiStory(json: JSONObject): PocketApiStory? = try {
+ val title = json.tryGetString(JSON_STORY_TITLE_KEY)
+ val url = json.tryGetString(JSON_STORY_URL_KEY)
+ val imageUrl = json.tryGetString(JSON_STORY_IMAGE_URL_KEY)
+
+ // These three properties are required for any valid recommendation.
+ if (title == null || url == null || imageUrl == null) {
+ null
+ } else {
+ PocketApiStory(
+ title = title,
+ url = url,
+ imageUrl = imageUrl,
+ // The following three properties are optional.
+ publisher = json.tryGetString(JSON_STORY_PUBLISHER_KEY)
+ ?: STRING_NOT_FOUND_DEFAULT_VALUE,
+ category = json.tryGetString(JSON_STORY_CATEGORY_KEY)
+ ?: STRING_NOT_FOUND_DEFAULT_VALUE,
+ timeToRead = json.tryGetInt(JSON_STORY_TIME_TO_READ_KEY)
+ ?: INT_NOT_FOUND_DEFAULT_VALUE,
+ )
+ }
+ } catch (e: JSONException) {
+ null
+ }
+
+ companion object {
+ @VisibleForTesting const val KEY_ARRAY_ITEMS = "recommendations"
+
+ /**
+ * Returns a new instance of [PocketJSONParser].
+ */
+ fun newInstance(): PocketJSONParser {
+ return PocketJSONParser()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketResponse.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketResponse.kt
new file mode 100644
index 0000000000..23e8fdcf04
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketResponse.kt
@@ -0,0 +1,42 @@
+/* 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.service.pocket.stories.api
+
+/**
+ * A response from the Pocket API: the subclasses determine the type of the result and contain usable data.
+ */
+internal sealed class PocketResponse<T> {
+
+ /**
+ * A successful response from the Pocket API.
+ *
+ * @param data The data returned from the Pocket API.
+ */
+ data class Success<T> internal constructor(val data: T) : PocketResponse<T>()
+
+ /**
+ * A failure response from the Pocket API.
+ */
+ class Failure<T> internal constructor() : PocketResponse<T>()
+
+ companion object {
+
+ /**
+ * Wraps the given [target] in a [PocketResponse]: if [target] is
+ * - null, then Failure
+ * - a Collection and empty, then Failure
+ * - a String and empty, then Failure
+ * - a Boolean and false, then Failure
+ * - otherwise, Success
+ */
+ internal fun <T : Any> wrap(target: T?): PocketResponse<T> = when (target) {
+ null -> Failure()
+ is Collection<*> -> if (target.isEmpty()) Failure() else Success(target)
+ is String -> if (target.isBlank()) Failure() else Success(target)
+ is Boolean -> if (target == false) Failure() else Success(target)
+ else -> Success(target)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDao.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDao.kt
new file mode 100644
index 0000000000..c493e04dd6
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDao.kt
@@ -0,0 +1,46 @@
+/* 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.service.pocket.stories.db
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Update
+
+@Dao
+internal interface PocketRecommendationsDao {
+ /**
+ * Add new stories to the database.
+ * Stories already existing will not be updated in any way.
+ * Already persisted stories but not present in [stories] will be removed from the database.
+ *
+ * @param stories new list of [PocketStoryEntity]s to replace the currently persisted ones.
+ */
+ @Transaction
+ suspend fun cleanOldAndInsertNewPocketStories(stories: List<PocketStoryEntity>) {
+ // If any url changed that story is obsolete and should be deleted.
+ val newStoriesUrls = stories.map { it.url to it.imageUrl }
+ val oldStoriesToDelete = getPocketStories()
+ .filterNot { newStoriesUrls.contains(it.url to it.imageUrl) }
+ delete(oldStoriesToDelete)
+
+ insertPocketStories(stories)
+ }
+
+ @Update(entity = PocketStoryEntity::class)
+ suspend fun updateTimesShown(updatedStories: List<PocketLocalStoryTimesShown>)
+
+ @Query("SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}")
+ suspend fun getPocketStories(): List<PocketStoryEntity>
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertPocketStories(stories: List<PocketStoryEntity>)
+
+ @Delete
+ suspend fun delete(stories: List<PocketStoryEntity>)
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt
new file mode 100644
index 0000000000..5a3e4f0efe
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt
@@ -0,0 +1,185 @@
+/* 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.service.pocket.stories.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import mozilla.components.service.pocket.spocs.db.SpocEntity
+import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
+import mozilla.components.service.pocket.spocs.db.SpocsDao
+
+/**
+ * Internal database for storing Pocket items.
+ */
+@Database(
+ entities = [
+ PocketStoryEntity::class,
+ SpocEntity::class,
+ SpocImpressionEntity::class,
+ ],
+ version = 4,
+)
+internal abstract class PocketRecommendationsDatabase : RoomDatabase() {
+ abstract fun pocketRecommendationsDao(): PocketRecommendationsDao
+ abstract fun spocsDao(): SpocsDao
+
+ companion object {
+ private const val DATABASE_NAME = "pocket_recommendations"
+ const val TABLE_NAME_STORIES = "stories"
+ const val TABLE_NAME_SPOCS = "spocs"
+ const val TABLE_NAME_SPOCS_IMPRESSIONS = "spocs_impressions"
+
+ @Volatile
+ private var instance: PocketRecommendationsDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): PocketRecommendationsDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ PocketRecommendationsDatabase::class.java,
+ DATABASE_NAME,
+ )
+ .addMigrations(
+ Migrations.migration_1_2,
+ Migrations.migration_2_3,
+ Migrations.migration_1_3,
+ Migrations.migration_3_4,
+ )
+ .build().also {
+ instance = it
+ }
+ }
+ }
+}
+
+internal object Migrations {
+ val migration_1_2 = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL(
+ "CREATE TABLE IF NOT EXISTS " +
+ "`${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}` (" +
+ "`url` TEXT NOT NULL, " +
+ "`title` TEXT NOT NULL, " +
+ "`imageUrl` TEXT NOT NULL, " +
+ "`sponsor` TEXT NOT NULL, " +
+ "`clickShim` TEXT NOT NULL, " +
+ "`impressionShim` TEXT NOT NULL, " +
+ "PRIMARY KEY(`url`)" +
+ ")",
+ )
+ }
+ }
+
+ /**
+ * Migration for when adding support for pacing sponsored stories.
+ */
+ val migration_2_3 = object : Migration(2, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // There are many new columns added. Drop the old table allowing to start fresh.
+ // This migration is expected to only be needed in debug builds
+ // with the feature not being live in any Fenix release.
+ db.execSQL(
+ "DROP TABLE ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}",
+ )
+
+ db.createNewSpocsTables()
+ }
+ }
+
+ /**
+ * Migration for when adding sponsored stories along with pacing support.
+ */
+ val migration_1_3 = object : Migration(1, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.createNewSpocsTables()
+ }
+ }
+
+ /**
+ * Migration for when adding a new index to the spoc impression entity.
+ */
+ val migration_3_4 = object : Migration(3, 4) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Rename the old tables to allow creating new ones
+ db.execSQL(
+ "ALTER TABLE `${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}` " +
+ "RENAME TO temp_spocs",
+ )
+ db.execSQL(
+ "ALTER TABLE `${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}` " +
+ "RENAME TO temp_spocs_impressions",
+ )
+
+ // Create new tables with the new schema
+ db.createNewSpocsTables()
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS `index_spocs_impressions_spocId` " +
+ "ON `${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}` (`spocId`)",
+ )
+
+ // Copy the old data to the new tables
+ db.execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' (" +
+ "id, url, title, imageUrl, sponsor, clickShim, impressionShim, " +
+ "priority, lifetimeCapCount, flightCapCount, flightCapPeriod" +
+ ") SELECT " +
+ "id, url, title, imageUrl, sponsor, clickShim, impressionShim, " +
+ "priority, lifetimeCapCount, flightCapCount, flightCapPeriod " +
+ "FROM temp_spocs",
+ )
+ db.execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}' (" +
+ "spocId, impressionId, impressionDateInSeconds" +
+ ") SELECT " +
+ "spocId, impressionId, impressionDateInSeconds " +
+ "FROM temp_spocs_impressions",
+ )
+
+ // Cleanup
+ db.execSQL("DROP TABLE temp_spocs")
+ db.execSQL("DROP TABLE temp_spocs_impressions")
+ }
+ }
+
+ private fun SupportSQLiteDatabase.createNewSpocsTables() {
+ execSQL(
+ "CREATE TABLE IF NOT EXISTS " +
+ "`${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}` (" +
+ "`id` INTEGER NOT NULL, " +
+ "`url` TEXT NOT NULL, " +
+ "`title` TEXT NOT NULL, " +
+ "`imageUrl` TEXT NOT NULL, " +
+ "`sponsor` TEXT NOT NULL, " +
+ "`clickShim` TEXT NOT NULL, " +
+ "`impressionShim` TEXT NOT NULL, " +
+ "`priority` INTEGER NOT NULL, " +
+ "`lifetimeCapCount` INTEGER NOT NULL, " +
+ "`flightCapCount` INTEGER NOT NULL, " +
+ "`flightCapPeriod` INTEGER NOT NULL, " +
+ "PRIMARY KEY(`id`)" +
+ ")",
+ )
+
+ execSQL(
+ "CREATE TABLE IF NOT EXISTS " +
+ "`${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}` (" +
+ "`spocId` INTEGER NOT NULL, " +
+ "`impressionId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
+ "`impressionDateInSeconds` INTEGER NOT NULL, " +
+ "FOREIGN KEY(`spocId`) " +
+ "REFERENCES `spocs`(`id`) " +
+ "ON UPDATE NO ACTION ON DELETE CASCADE " +
+ ")",
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketStoryEntity.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketStoryEntity.kt
new file mode 100644
index 0000000000..885978f801
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketStoryEntity.kt
@@ -0,0 +1,31 @@
+/* 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.service.pocket.stories.db
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * A Pocket recommended story that is to be mapped to SQLite table.
+ */
+@Entity(tableName = PocketRecommendationsDatabase.TABLE_NAME_STORIES)
+internal data class PocketStoryEntity(
+ @PrimaryKey
+ val url: String,
+ val title: String,
+ val imageUrl: String,
+ val publisher: String,
+ val category: String,
+ val timeToRead: Int,
+ val timesShown: Long,
+)
+
+/**
+ * A [PocketStoryEntity] only containing data about the [timesShown] property allowing for quick updates.
+ */
+internal data class PocketLocalStoryTimesShown(
+ val url: String,
+ val timesShown: Long,
+)
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorker.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorker.kt
new file mode 100644
index 0000000000..e563cba564
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorker.kt
@@ -0,0 +1,36 @@
+/* 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.service.pocket.update
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.service.pocket.GlobalDependencyProvider
+
+/**
+ * WorkManager Worker used for deleting the profile used for downloading Pocket sponsored stories.
+ */
+internal class DeleteSpocsProfileWorker(
+ context: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+
+ override suspend fun doWork(): Result {
+ return withContext(Dispatchers.IO) {
+ if (GlobalDependencyProvider.SponsoredStories.useCases?.deleteProfile?.invoke() == true) {
+ Result.success()
+ } else {
+ Result.retry()
+ }
+ }
+ }
+
+ internal companion object {
+ const val DELETE_SPOCS_PROFILE_WORK_TAG =
+ "mozilla.components.feature.pocket.spocs.profile.delete.work.tag"
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/PocketStoriesRefreshScheduler.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/PocketStoriesRefreshScheduler.kt
new file mode 100644
index 0000000000..7cdc7b8daf
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/PocketStoriesRefreshScheduler.kt
@@ -0,0 +1,64 @@
+/* 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.service.pocket.update
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import mozilla.components.service.pocket.PocketStoriesConfig
+import mozilla.components.service.pocket.logger
+import mozilla.components.service.pocket.update.RefreshPocketWorker.Companion.REFRESH_WORK_TAG
+import mozilla.components.support.base.worker.Frequency
+
+/**
+ * Class used to schedule Pocket recommended stories refresh.
+ */
+internal class PocketStoriesRefreshScheduler(
+ private val pocketStoriesConfig: PocketStoriesConfig,
+) {
+ internal fun schedulePeriodicRefreshes(context: Context) {
+ logger.info("Scheduling pocket recommendations background refresh")
+
+ val refreshWork = createPeriodicWorkerRequest(
+ frequency = pocketStoriesConfig.frequency,
+ )
+
+ getWorkManager(context)
+ .enqueueUniquePeriodicWork(REFRESH_WORK_TAG, ExistingPeriodicWorkPolicy.KEEP, refreshWork)
+ }
+
+ internal fun stopPeriodicRefreshes(context: Context) {
+ getWorkManager(context)
+ .cancelAllWorkByTag(REFRESH_WORK_TAG)
+ }
+
+ @VisibleForTesting
+ internal fun createPeriodicWorkerRequest(
+ frequency: Frequency,
+ ): PeriodicWorkRequest {
+ val constraints = getWorkerConstrains()
+
+ return PeriodicWorkRequestBuilder<RefreshPocketWorker>(
+ frequency.repeatInterval,
+ frequency.repeatIntervalTimeUnit,
+ ).apply {
+ setConstraints(constraints)
+ addTag(REFRESH_WORK_TAG)
+ }.build()
+ }
+
+ @VisibleForTesting
+ internal fun getWorkerConstrains() = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ @VisibleForTesting
+ internal fun getWorkManager(context: Context) = WorkManager.getInstance(context)
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshPocketWorker.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshPocketWorker.kt
new file mode 100644
index 0000000000..c5790598b9
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshPocketWorker.kt
@@ -0,0 +1,36 @@
+/* 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.service.pocket.update
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.service.pocket.GlobalDependencyProvider
+
+/**
+ * WorkManager Worker used for downloading and persisting locally a new list of Pocket recommended stories.
+ */
+internal class RefreshPocketWorker(
+ context: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+
+ override suspend fun doWork(): Result {
+ return withContext(Dispatchers.IO) {
+ if (GlobalDependencyProvider.RecommendedStories.useCases?.refreshStories?.invoke() == true) {
+ Result.success()
+ } else {
+ Result.retry()
+ }
+ }
+ }
+
+ internal companion object {
+ const val REFRESH_WORK_TAG =
+ "mozilla.components.feature.pocket.recommendations.refresh.work.tag"
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshSpocsWorker.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshSpocsWorker.kt
new file mode 100644
index 0000000000..def14f9c16
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshSpocsWorker.kt
@@ -0,0 +1,36 @@
+/* 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.service.pocket.update
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.service.pocket.GlobalDependencyProvider
+
+/**
+ * WorkManager Worker used for downloading and persisting locally a new list of Pocket recommended stories.
+ */
+internal class RefreshSpocsWorker(
+ context: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+
+ override suspend fun doWork(): Result {
+ return withContext(Dispatchers.IO) {
+ if (GlobalDependencyProvider.SponsoredStories.useCases?.refreshStories?.invoke() == true) {
+ Result.success()
+ } else {
+ Result.retry()
+ }
+ }
+ }
+
+ internal companion object {
+ const val REFRESH_SPOCS_WORK_TAG =
+ "mozilla.components.feature.pocket.spocs.refresh.work.tag"
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/SpocsRefreshScheduler.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/SpocsRefreshScheduler.kt
new file mode 100644
index 0000000000..01ffd998a0
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/SpocsRefreshScheduler.kt
@@ -0,0 +1,94 @@
+/* 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.service.pocket.update
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import mozilla.components.service.pocket.PocketStoriesConfig
+import mozilla.components.service.pocket.logger
+import mozilla.components.service.pocket.update.DeleteSpocsProfileWorker.Companion.DELETE_SPOCS_PROFILE_WORK_TAG
+import mozilla.components.service.pocket.update.RefreshSpocsWorker.Companion.REFRESH_SPOCS_WORK_TAG
+import mozilla.components.support.base.worker.Frequency
+
+/**
+ * Class used to schedule Pocket recommended stories refresh.
+ */
+internal class SpocsRefreshScheduler(
+ private val pocketStoriesConfig: PocketStoriesConfig,
+) {
+ internal fun schedulePeriodicRefreshes(context: Context) {
+ logger.info("Scheduling sponsored stories background refresh")
+
+ val refreshWork = createPeriodicRefreshWorkerRequest(
+ frequency = pocketStoriesConfig.sponsoredStoriesRefreshFrequency,
+ )
+
+ getWorkManager(context)
+ .enqueueUniquePeriodicWork(REFRESH_SPOCS_WORK_TAG, ExistingPeriodicWorkPolicy.KEEP, refreshWork)
+ }
+
+ internal fun stopPeriodicRefreshes(context: Context) {
+ getWorkManager(context)
+ .cancelAllWorkByTag(REFRESH_SPOCS_WORK_TAG)
+ }
+
+ internal fun scheduleProfileDeletion(context: Context) {
+ logger.info("Scheduling sponsored stories profile deletion")
+
+ val deleteProfileWork = createOneTimeProfileDeletionWorkerRequest()
+
+ getWorkManager(context)
+ .enqueueUniqueWork(DELETE_SPOCS_PROFILE_WORK_TAG, ExistingWorkPolicy.KEEP, deleteProfileWork)
+ }
+
+ internal fun stopProfileDeletion(context: Context) {
+ getWorkManager(context)
+ .cancelAllWorkByTag(DELETE_SPOCS_PROFILE_WORK_TAG)
+ }
+
+ @VisibleForTesting
+ internal fun createOneTimeProfileDeletionWorkerRequest(): OneTimeWorkRequest {
+ val constraints = getWorkerConstrains()
+
+ return OneTimeWorkRequestBuilder<DeleteSpocsProfileWorker>()
+ .apply {
+ setConstraints(constraints)
+ addTag(DELETE_SPOCS_PROFILE_WORK_TAG)
+ }
+ .build()
+ }
+
+ @VisibleForTesting
+ internal fun createPeriodicRefreshWorkerRequest(
+ frequency: Frequency,
+ ): PeriodicWorkRequest {
+ val constraints = getWorkerConstrains()
+
+ return PeriodicWorkRequestBuilder<RefreshSpocsWorker>(
+ frequency.repeatInterval,
+ frequency.repeatIntervalTimeUnit,
+ ).apply {
+ setConstraints(constraints)
+ addTag(REFRESH_SPOCS_WORK_TAG)
+ }.build()
+ }
+
+ @VisibleForTesting
+ internal fun getWorkerConstrains() = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ @VisibleForTesting
+ internal fun getWorkManager(context: Context) = WorkManager.getInstance(context)
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/res/values/ids.xml b/mobile/android/android-components/components/service/pocket/src/main/res/values/ids.xml
new file mode 100644
index 0000000000..7a05fcff1a
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/main/res/values/ids.xml
@@ -0,0 +1,10 @@
+<?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>
+ <item name="payload_pocket_token" type="id"/>
+ <item name="payload_pocket_user_agent" type="id"/>
+ <item name="payload_pocket_items_count" type="id"/>
+ <item name="payload_pocket_items_locale" type="id"/>
+</resources>
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt
new file mode 100644
index 0000000000..a87f46887d
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt
@@ -0,0 +1,50 @@
+/* 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.service.pocket
+
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class GlobalDependencyProviderTest {
+ @Test
+ fun `GIVEN RecommendedStories WHEN initializing THEN store the provided arguments`() {
+ val useCases: PocketStoriesUseCases = mock()
+
+ GlobalDependencyProvider.RecommendedStories.initialize(useCases)
+
+ assertSame(useCases, GlobalDependencyProvider.RecommendedStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN RecommendedStories WHEN resetting THEN clear all current state`() {
+ GlobalDependencyProvider.RecommendedStories.initialize(mock())
+
+ GlobalDependencyProvider.RecommendedStories.reset()
+
+ assertNull(GlobalDependencyProvider.RecommendedStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN SponsoredStories WHEN initializing THEN store the provided arguments`() {
+ val useCases: SpocsUseCases = mock()
+
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+
+ assertSame(useCases, GlobalDependencyProvider.SponsoredStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN SponsoredStories WHEN resetting THEN clear all current state`() {
+ GlobalDependencyProvider.SponsoredStories.initialize(mock())
+
+ GlobalDependencyProvider.SponsoredStories.reset()
+
+ assertNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt
new file mode 100644
index 0000000000..2e8f504f4a
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt
@@ -0,0 +1,62 @@
+/* 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.service.pocket
+
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.support.base.worker.Frequency
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import kotlin.reflect.KVisibility
+
+class PocketStoriesConfigTest {
+ @Test
+ fun `GIVEN a PocketStoriesConfig THEN its visibility is internal`() {
+ assertClassVisibility(PocketStoriesConfig::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `WHEN instantiating a PocketStoriesConfig THEN frequency has a default value`() {
+ val config = PocketStoriesConfig(mock())
+
+ val defaultFrequency = Frequency(DEFAULT_REFRESH_INTERVAL, DEFAULT_REFRESH_TIMEUNIT)
+ assertEquals(defaultFrequency.repeatInterval, config.frequency.repeatInterval)
+ assertEquals(defaultFrequency.repeatIntervalTimeUnit, config.frequency.repeatIntervalTimeUnit)
+ }
+
+ @Test
+ fun `WHEN instantiating a PocketStoriesConfig THEN sponsored stories refresh frequency has a default value`() {
+ val config = PocketStoriesConfig(mock())
+
+ val defaultFrequency = Frequency(
+ DEFAULT_SPONSORED_STORIES_REFRESH_INTERVAL,
+ DEFAULT_SPONSORED_STORIES_REFRESH_TIMEUNIT,
+ )
+ assertEquals(defaultFrequency.repeatInterval, config.sponsoredStoriesRefreshFrequency.repeatInterval)
+ assertEquals(defaultFrequency.repeatIntervalTimeUnit, config.sponsoredStoriesRefreshFrequency.repeatIntervalTimeUnit)
+ }
+
+ @Test
+ fun `WHEN instantiating a PocketStoriesConfig THEN profile is by default null`() {
+ val config = PocketStoriesConfig(mock())
+
+ assertNull(config.profile)
+ }
+
+ @Test
+ fun `GIVEN a Frequency THEN its visibility is internal`() {
+ assertClassVisibility(Frequency::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `WHEN instantiating a PocketStoriesConfig THEN sponsoredStoriesParams default value is used`() {
+ val config = PocketStoriesConfig(mock())
+
+ assertEquals(DEFAULT_SPONSORED_STORIES_SITE_ID, config.sponsoredStoriesParams.siteId)
+ assertEquals("", config.sponsoredStoriesParams.country)
+ assertEquals("", config.sponsoredStoriesParams.city)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt
new file mode 100644
index 0000000000..04c7a23939
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt
@@ -0,0 +1,229 @@
+/* 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.service.pocket
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.helpers.assertConstructorsVisibility
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.spocs.SpocsUseCases.GetSponsoredStories
+import mozilla.components.service.pocket.spocs.SpocsUseCases.RecordImpression
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases.GetPocketStories
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases.UpdateStoriesTimesShown
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import java.util.UUID
+import kotlin.reflect.KVisibility
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class PocketStoriesServiceTest {
+ private val storiesUseCases: PocketStoriesUseCases = mock()
+ private val spocsUseCases: SpocsUseCases = mock()
+ private val service = PocketStoriesService(testContext, PocketStoriesConfig(mock())).also {
+ it.storiesRefreshScheduler = mock()
+ it.spocsRefreshscheduler = mock()
+ it.storiesUseCases = storiesUseCases
+ it.spocsUseCases = spocsUseCases
+ }
+
+ @After
+ fun teardown() {
+ GlobalDependencyProvider.SponsoredStories.reset()
+ GlobalDependencyProvider.RecommendedStories.reset()
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService THEN it should be publicly available`() {
+ assertConstructorsVisibility(PocketStoriesConfig::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN startPeriodicStoriesRefresh THEN persist dependencies and schedule stories refresh`() {
+ service.startPeriodicStoriesRefresh()
+
+ assertNotNull(GlobalDependencyProvider.RecommendedStories.useCases)
+ verify(service.storiesRefreshScheduler).schedulePeriodicRefreshes(any())
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN stopPeriodicStoriesRefresh THEN stop refreshing stories and clear dependencies`() {
+ service.stopPeriodicStoriesRefresh()
+
+ verify(service.storiesRefreshScheduler).stopPeriodicRefreshes(any())
+ assertNull(GlobalDependencyProvider.RecommendedStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService is initialized with a valid profile WHEN called to start periodic refreshes THEN persist dependencies, cancel profile deletion and schedule stories refresh`() {
+ val client: Client = mock()
+ val profileId = UUID.randomUUID()
+ val appId = "test"
+ val service = PocketStoriesService(
+ context = testContext,
+ pocketStoriesConfig = PocketStoriesConfig(
+ client = client,
+ profile = Profile(
+ profileId = profileId,
+ appId = appId,
+ ),
+ ),
+ ).apply {
+ spocsRefreshscheduler = mock()
+ }
+
+ service.startPeriodicSponsoredStoriesRefresh()
+
+ assertNotNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ verify(service.spocsRefreshscheduler).stopProfileDeletion(any())
+ verify(service.spocsRefreshscheduler).schedulePeriodicRefreshes(any())
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService is initialized with an invalid profile WHEN called to start periodic refreshes THEN don't schedule periodic refreshes and don't persist dependencies`() {
+ val service = PocketStoriesService(
+ context = testContext,
+ pocketStoriesConfig = PocketStoriesConfig(
+ client = mock(),
+ profile = null,
+ ),
+ ).apply {
+ spocsRefreshscheduler = mock()
+ }
+
+ service.startPeriodicSponsoredStoriesRefresh()
+
+ verify(service.spocsRefreshscheduler, never()).schedulePeriodicRefreshes(any())
+ assertNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN called to stop periodic refreshes THEN stop refreshing stories`() {
+ // Mock periodic refreshes were started previously and profile details were set.
+ // Now they will have to be cleaned.
+ GlobalDependencyProvider.SponsoredStories.initialize(mock())
+ service.spocsRefreshscheduler = mock()
+
+ service.stopPeriodicSponsoredStoriesRefresh()
+
+ verify(service.spocsRefreshscheduler).stopPeriodicRefreshes(any())
+ }
+
+ @Test
+ fun `WHEN called to refresh locally saved sponsored stories THEN refresh usecase is invoked`() = runTest {
+ val refreshStories: SpocsUseCases.RefreshSponsoredStories = mock()
+ doReturn(refreshStories).`when`(spocsUseCases).refreshStories
+
+ service.refreshSponsoredStories()
+
+ verify(refreshStories).invoke()
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN getStories THEN stories useCases should return`() = runTest {
+ val stories = listOf(mock<PocketRecommendedStory>())
+ val getStoriesUseCase: GetPocketStories = mock()
+ doReturn(stories).`when`(getStoriesUseCase).invoke()
+ doReturn(getStoriesUseCase).`when`(storiesUseCases).getStories
+
+ val result = service.getStories()
+
+ assertEquals(stories, result)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN updateStoriesTimesShown THEN delegate to spocs useCases`() = runTest {
+ val updateTimesShownUseCase: UpdateStoriesTimesShown = mock()
+ doReturn(updateTimesShownUseCase).`when`(storiesUseCases).updateTimesShown
+ val stories = listOf(mock<PocketRecommendedStory>())
+
+ service.updateStoriesTimesShown(stories)
+
+ verify(updateTimesShownUseCase).invoke(stories)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN getSponsoredStories THEN delegate to spocs useCases`() = runTest {
+ val noProfileResponse = service.getSponsoredStories()
+ assertTrue(noProfileResponse.isEmpty())
+
+ val stories = listOf(mock<PocketSponsoredStory>())
+ val getStoriesUseCase: GetSponsoredStories = mock()
+ doReturn(stories).`when`(getStoriesUseCase).invoke()
+ doReturn(getStoriesUseCase).`when`(spocsUseCases).getStories
+ val existingProfileResponse = service.getSponsoredStories()
+ assertEquals(stories, existingProfileResponse)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService is initialized with a valid profile WHEN called to delete profile THEN persist dependencies, cancel stories refresh and schedule profile deletion`() {
+ val client: Client = mock()
+ val profileId = UUID.randomUUID()
+ val appId = "test"
+ val service = PocketStoriesService(
+ context = testContext,
+ pocketStoriesConfig = PocketStoriesConfig(
+ client = client,
+ profile = Profile(
+ profileId = profileId,
+ appId = appId,
+ ),
+ ),
+ ).apply {
+ spocsRefreshscheduler = mock()
+ }
+
+ service.deleteProfile()
+
+ assertNotNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ verify(service.spocsRefreshscheduler).stopPeriodicRefreshes(any())
+ verify(service.spocsRefreshscheduler).scheduleProfileDeletion(any())
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService is initialized with an invalid profile WHEN called to delete profile THEN don't schedule profile deletion and don't persist dependencies`() {
+ val service = PocketStoriesService(
+ context = testContext,
+ pocketStoriesConfig = PocketStoriesConfig(
+ client = mock(),
+ profile = null,
+ ),
+ ).apply {
+ spocsRefreshscheduler = mock()
+ }
+
+ service.deleteProfile()
+
+ verify(service.spocsRefreshscheduler, never()).scheduleProfileDeletion(any())
+ assertNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN recordStoriesImpressions THEN delegate to spocs useCases`() = runTest {
+ val recordImpressionsUseCase: RecordImpression = mock()
+ doReturn(recordImpressionsUseCase).`when`(spocsUseCases).recordImpression
+ val storiesIds = listOf(22, 33)
+
+ service.recordStoriesImpressions(storiesIds)
+
+ verify(recordImpressionsUseCase).invoke(storiesIds)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoryTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoryTest.kt
new file mode 100644
index 0000000000..36a559b710
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoryTest.kt
@@ -0,0 +1,100 @@
+/* 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.service.pocket
+
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.helpers.assertConstructorsVisibility
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import kotlin.reflect.KVisibility
+
+class PocketStoryTest {
+ @Test
+ fun `GIVEN PocketSponsoredStory THEN it should be publicly available`() {
+ assertConstructorsVisibility(PocketSponsoredStory::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `GIVEN PocketSponsoredStoryCaps THEN it should be publicly available`() {
+ assertConstructorsVisibility(PocketRecommendedStory::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `GIVEN PocketRecommendedStory THEN it should be publicly available`() {
+ assertConstructorsVisibility(PocketRecommendedStory::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `GIVEN a PocketRecommendedStory WHEN it's title is accessed from parent THEN it returns the previously set value`() {
+ val pocketRecommendedStory = PocketRecommendedStory(
+ title = "testTitle",
+ url = "",
+ imageUrl = "",
+ publisher = "",
+ category = "",
+ timeToRead = 0,
+ timesShown = 0,
+ )
+
+ val result = (pocketRecommendedStory as PocketStory).title
+
+ assertEquals("testTitle", result)
+ }
+
+ @Test
+ fun `GIVEN a PocketRecommendedStory WHEN it's url is accessed from parent THEN it returns the previously set value`() {
+ val pocketRecommendedStory = PocketRecommendedStory(
+ title = "",
+ url = "testUrl",
+ imageUrl = "",
+ publisher = "",
+ category = "",
+ timeToRead = 0,
+ timesShown = 0,
+ )
+
+ val result = (pocketRecommendedStory as PocketStory).url
+
+ assertEquals("testUrl", result)
+ }
+
+ @Test
+ fun `GIVEN a PocketSponsoredStory WHEN it's title is accessed from parent THEN it returns the previously set value`() {
+ val pocketRecommendedStory = PocketSponsoredStory(
+ id = 1,
+ title = "testTitle",
+ url = "",
+ imageUrl = "",
+ sponsor = "",
+ shim = mock(),
+ priority = 11,
+ caps = mock(),
+ )
+
+ val result = (pocketRecommendedStory as PocketStory).title
+
+ assertEquals("testTitle", result)
+ }
+
+ @Test
+ fun `GIVEN a PocketSponsoredStory WHEN it's url is accessed from parent THEN it returns the previously set value`() {
+ val pocketRecommendedStory = PocketSponsoredStory(
+ id = 2,
+ title = "",
+ url = "testUrl",
+ imageUrl = "",
+ sponsor = "",
+ shim = mock(),
+ priority = 33,
+ caps = mock(),
+ )
+
+ val result = (pocketRecommendedStory as PocketStory).url
+
+ assertEquals("testUrl", result)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/ConceptFetchKtTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/ConceptFetchKtTest.kt
new file mode 100644
index 0000000000..0155e57ded
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/ConceptFetchKtTest.kt
@@ -0,0 +1,80 @@
+/* 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.service.pocket.ext
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+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.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.io.IOException
+
+private const val EXPECTED_DEFAULT_RESPONSE_BODY = "default response body"
+private const val TEST_URL = "https://mozilla.org"
+
+class ConceptFetchKtTest {
+
+ private lateinit var client: Client
+ private lateinit var defaultResponse: Response
+ private lateinit var failureResponse: Response
+ private lateinit var testRequest: Request
+
+ @Before
+ fun setUp() {
+ val responseBody = Response.Body(EXPECTED_DEFAULT_RESPONSE_BODY.byteInputStream())
+ val failureResponseBody = Response.Body("failure response body)".byteInputStream())
+ defaultResponse = spy(Response(TEST_URL, 200, MutableHeaders(), responseBody))
+ failureResponse = spy(Response(TEST_URL, 404, MutableHeaders(), failureResponseBody))
+ testRequest = Request(TEST_URL, conservative = true)
+
+ client = mock<Client>().also {
+ whenever(it.fetch(any())).thenReturn(defaultResponse)
+ }
+ }
+
+ @Test
+ fun `GIVEN fetch throws an exception WHEN fetchBodyOrNull is called THEN null is returned`() {
+ whenever(client.fetch(any())).thenThrow(IOException())
+ assertNull(client.fetchBodyOrNull(testRequest))
+ }
+
+ @Test
+ fun `GIVEN fetch returns a failure response WHEN fetchBodyOrNull is called THEN null is returned`() {
+ setUpClientFailureResponse()
+ assertNull(client.fetchBodyOrNull(testRequest))
+ }
+
+ @Test
+ fun `GIVEN fetch returns a success response WHEN fetchBodyOrNull is called THEN the response body is returned`() {
+ val actual = client.fetchBodyOrNull(testRequest)
+ assertEquals(EXPECTED_DEFAULT_RESPONSE_BODY, actual)
+ }
+
+ @Test
+ fun `GIVEN fetch returns a success response WHEN fetchBodyOrNull is called THEN the response is closed`() {
+ client.fetchBodyOrNull(testRequest)
+ verify(defaultResponse, times(1)).close()
+ }
+
+ @Test
+ fun `GIVEN fetch returns a failure response WHEN fetchBodyOrNull is called THEN the response is closed`() {
+ setUpClientFailureResponse()
+ client.fetchBodyOrNull(testRequest)
+ verify(failureResponse, times(1)).close()
+ }
+
+ private fun setUpClientFailureResponse() {
+ whenever(client.fetch(any())).thenReturn(failureResponse)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt
new file mode 100644
index 0000000000..bdf6a7bbe6
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt
@@ -0,0 +1,114 @@
+/* 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.service.pocket.ext
+
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import kotlin.reflect.full.memberProperties
+
+class MappersKtTest {
+ @Test
+ fun `GIVEN a PocketApiStory WHEN toPocketLocalStory is called THEN a one to one mapping is performed and timesShown is set to 0`() {
+ val apiStory = PocketTestResources.apiExpectedPocketStoriesRecommendations[0]
+
+ val result = apiStory.toPocketLocalStory()
+
+ assertNotEquals(apiStory::class.memberProperties, result::class.memberProperties)
+ assertSame(apiStory.url, result.url)
+ assertSame(apiStory.title, result.title)
+ assertSame(apiStory.imageUrl, result.imageUrl)
+ assertSame(apiStory.publisher, result.publisher)
+ assertSame(apiStory.category, result.category)
+ assertSame(apiStory.timeToRead, result.timeToRead)
+ assertEquals(DEFAULT_TIMES_SHOWN, result.timesShown)
+ }
+
+ @Test
+ fun `GIVEN a PocketLocalStory WHEN toPocketRecommendedStory is called THEN a one to one mapping is performed`() {
+ val localStory = PocketTestResources.dbExpectedPocketStory
+
+ val result = localStory.toPocketRecommendedStory()
+
+ assertNotEquals(localStory::class.memberProperties, result::class.memberProperties)
+ assertSame(localStory.url, result.url)
+ assertSame(localStory.title, result.title)
+ assertSame(localStory.imageUrl, result.imageUrl)
+ assertSame(localStory.publisher, result.publisher)
+ assertSame(localStory.category, result.category)
+ assertSame(localStory.timeToRead, result.timeToRead)
+ assertEquals(localStory.timesShown, result.timesShown)
+ }
+
+ @Test
+ fun `GIVEN a PocketLocalStory with no category WHEN toPocketRecommendedStory is called THEN a one to one mapping is performed and the category is set to general`() {
+ val localStory = PocketTestResources.dbExpectedPocketStory.copy(category = "")
+
+ val result = localStory.toPocketRecommendedStory()
+
+ assertNotEquals(localStory::class.memberProperties, result::class.memberProperties)
+ assertSame(localStory.url, result.url)
+ assertSame(localStory.title, result.title)
+ assertSame(localStory.imageUrl, result.imageUrl)
+ assertSame(localStory.publisher, result.publisher)
+ assertSame(DEFAULT_CATEGORY, result.category)
+ assertSame(localStory.timeToRead, result.timeToRead)
+ assertEquals(localStory.timesShown, result.timesShown)
+ }
+
+ @Test
+ fun `GIVEN a PcoketRecommendedStory WHEN toPartialTimeShownUpdate is called THEN only the url and timesShown properties are kept`() {
+ val story = PocketTestResources.clientExpectedPocketStory
+
+ val result = story.toPartialTimeShownUpdate()
+
+ assertNotEquals(story::class.memberProperties, result::class.memberProperties)
+ assertEquals(2, result::class.memberProperties.size)
+ assertSame(story.url, result.url)
+ assertSame(story.timesShown, result.timesShown)
+ }
+
+ @Test
+ fun `GIVEN a spoc downloaded from Internet WHEN it is converted to a local spoc THEN a one to one mapping is made`() {
+ val apiStory = PocketTestResources.apiExpectedPocketSpocs[0]
+
+ val result = apiStory.toLocalSpoc()
+
+ assertEquals(apiStory.id, result.id)
+ assertSame(apiStory.title, result.title)
+ assertSame(apiStory.url, result.url)
+ assertSame(apiStory.imageSrc, result.imageUrl)
+ assertSame(apiStory.sponsor, result.sponsor)
+ assertSame(apiStory.shim.click, result.clickShim)
+ assertSame(apiStory.shim.impression, result.impressionShim)
+ assertEquals(apiStory.priority, result.priority)
+ assertEquals(apiStory.caps.lifetimeCount, result.lifetimeCapCount)
+ assertEquals(apiStory.caps.flightCount, result.flightCapCount)
+ assertEquals(apiStory.caps.flightPeriod, result.flightCapPeriod)
+ }
+
+ @Test
+ fun `GIVEN a local spoc WHEN it is converted to be exposed to clients THEN a one to one mapping is made`() {
+ val localStory = PocketTestResources.dbExpectedPocketSpoc
+
+ val result = localStory.toPocketSponsoredStory()
+
+ assertEquals(localStory.id, result.id)
+ assertSame(localStory.title, result.title)
+ assertSame(localStory.url, result.url)
+ assertSame(localStory.imageUrl, result.imageUrl)
+ assertSame(localStory.sponsor, result.sponsor)
+ assertSame(localStory.clickShim, result.shim.click)
+ assertSame(localStory.impressionShim, result.shim.impression)
+ assertEquals(localStory.priority, result.priority)
+ assertEquals(localStory.lifetimeCapCount, result.caps.lifetimeCount)
+ assertEquals(localStory.flightCapCount, result.caps.flightCount)
+ assertEquals(localStory.flightCapPeriod, result.caps.flightPeriod)
+ assertTrue(result.caps.currentImpressions.isEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt
new file mode 100644
index 0000000000..6fea0accbf
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt
@@ -0,0 +1,135 @@
+/* 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.service.pocket.ext
+
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+
+class PocketStoryKtTest {
+ private val nowInSeconds = System.currentTimeMillis() / 1000
+ private val flightPeriod = 100
+ private val flightImpression1 = nowInSeconds - flightPeriod / 2
+ private val flightImpression2 = nowInSeconds - flightPeriod / 3
+ private val currentImpressions = listOf(
+ nowInSeconds - flightPeriod * 2, // older impression that doesn't fit the flight period
+ flightImpression1,
+ flightImpression2,
+ )
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking for the current flight impression THEN return all impressions in flight period`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 10,
+ flightCount = 5,
+ flightPeriod = flightPeriod,
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.getCurrentFlightImpressions()
+
+ assertEquals(listOf(flightImpression1, flightImpression2), result)
+ }
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking if lifetime impressions reached THEN return false if not`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 10,
+ flightCount = 5,
+ flightPeriod = flightPeriod,
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.hasLifetimeImpressionsLimitReached()
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking if lifetime impressions reached THEN return true if so`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 3,
+ flightCount = 3,
+ flightPeriod = flightPeriod,
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.hasLifetimeImpressionsLimitReached()
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking if flight impressions reached THEN return false if not`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 10,
+ flightCount = 5,
+ flightPeriod = flightPeriod,
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.hasFlightImpressionsLimitReached()
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking if flight impressions reached THEN return true if so`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 3,
+ flightCount = 2,
+ flightPeriod = flightPeriod,
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.hasFlightImpressionsLimitReached()
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN a sponsored story WHEN recording a new impression THEN update the same story to contain a new impression recorded in seconds`() {
+ val story = PocketTestResources.dbExpectedPocketSpoc.toPocketSponsoredStory(currentImpressions)
+
+ assertEquals(3, story.caps.currentImpressions.size)
+ val result = story.recordNewImpression()
+
+ assertEquals(story.id, result.id)
+ assertSame(story.title, result.title)
+ assertSame(story.url, result.url)
+ assertSame(story.imageUrl, result.imageUrl)
+ assertSame(story.sponsor, result.sponsor)
+ assertSame(story.shim, result.shim)
+ assertEquals(story.priority, result.priority)
+ assertEquals(story.caps.lifetimeCount, result.caps.lifetimeCount)
+ assertEquals(story.caps.flightCount, result.caps.flightCount)
+ assertEquals(story.caps.flightPeriod, result.caps.flightPeriod)
+
+ assertEquals(4, result.caps.currentImpressions.size)
+ assertEquals(currentImpressions, result.caps.currentImpressions.take(3))
+ // Check if a new impression has been added for around this current time.
+ assertTrue(
+ LongRange(nowInSeconds - 5, nowInSeconds + 5)
+ .contains(result.caps.currentImpressions[3]),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/Assert.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/Assert.kt
new file mode 100644
index 0000000000..1219dc139e
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/Assert.kt
@@ -0,0 +1,83 @@
+/* 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.service.pocket.helpers
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.service.pocket.stories.api.PocketResponse
+import mozilla.components.support.test.any
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import kotlin.reflect.KClass
+import kotlin.reflect.KVisibility
+
+fun <T : Any> assertConstructorsVisibility(assertedClass: KClass<T>, visibility: KVisibility) {
+ assertedClass.constructors.forEach {
+ assertEquals(visibility, it.visibility)
+ }
+}
+
+fun <T : Any> assertClassVisibility(assertedClass: KClass<T>, visibility: KVisibility) {
+ assertEquals(visibility, assertedClass.visibility)
+}
+
+/**
+ * @param client the underlying mock client for the raw endpoint making the request.
+ * @param makeRequest makes the request using the raw endpoint.
+ * @param assertParams makes assertions on the passed in request.
+ */
+fun assertRequestParams(client: Client, makeRequest: () -> Unit, assertParams: (Request) -> Unit) {
+ whenever(client.fetch(any())).thenAnswer {
+ val request = it.arguments[0] as Request
+ assertParams(request)
+ Response("https://mozilla.org", 200, MutableHeaders(), Response.Body("".byteInputStream()))
+ }
+
+ makeRequest()
+
+ // Ensure fetch is called so that the assertions in assertParams are called.
+ verify(client, times(1)).fetch(any())
+}
+
+/**
+ * @param client the underlying mock client for the raw endpoint making the request.
+ * @param makeRequest makes the request using the raw endpoint and returns the body text, or null on error
+ */
+fun assertSuccessfulRequestReturnsResponseBody(client: Client, makeRequest: () -> String?) {
+ val expectedBody = "{\"jsonStr\": true}"
+ val body = mock(Response.Body::class.java).also {
+ whenever(it.string()).thenReturn(expectedBody)
+ }
+ val response = MockResponses.getSuccess().also {
+ whenever(it.body).thenReturn(body)
+ }
+ whenever(client.fetch(any())).thenReturn(response)
+
+ assertEquals(expectedBody, makeRequest())
+}
+
+/**
+ * @param client the underlying mock client for the raw endpoint making the request.
+ * @param response the response to return when the request is made.
+ * @param makeRequest makes the request using the raw endpoint.
+ */
+fun assertResponseIsClosed(client: Client, response: Response, makeRequest: () -> Unit) {
+ whenever(client.fetch(any())).thenReturn(response)
+ makeRequest()
+ verify(response, times(1)).close()
+}
+
+fun assertResponseIsFailure(response: Any) {
+ assertEquals(PocketResponse.Failure::class.java, response.javaClass)
+}
+
+fun assertResponseIsSuccess(response: Any) {
+ assertEquals(PocketResponse.Success::class.java, response.javaClass)
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/MockResponses.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/MockResponses.kt
new file mode 100644
index 0000000000..d0742d49f4
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/MockResponses.kt
@@ -0,0 +1,30 @@
+/* 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.service.pocket.helpers
+
+import mozilla.components.concept.fetch.Response
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.mockito.Mockito.mock
+
+/**
+ * A collection of helper functions to generate mock [Response]s.
+ */
+object MockResponses {
+
+ fun getError(): Response = getMockResponse(404)
+
+ fun getSuccess(): Response = getMockResponse(200).also {
+ // A successful response must contain a body.
+ val body = mock(Response.Body::class.java).also { body ->
+ whenever(body.string()).thenReturn("{}")
+ }
+ whenever(it.body).thenReturn(body)
+ }
+
+ private fun getMockResponse(status: Int): Response = mock<Response>().also {
+ whenever(it.status).thenReturn(status)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt
new file mode 100644
index 0000000000..aaa0cbc716
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt
@@ -0,0 +1,171 @@
+/* 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.service.pocket.helpers
+
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.spocs.api.ApiSpoc
+import mozilla.components.service.pocket.spocs.api.ApiSpocCaps
+import mozilla.components.service.pocket.spocs.api.ApiSpocShim
+import mozilla.components.service.pocket.spocs.db.SpocEntity
+import mozilla.components.service.pocket.stories.api.PocketApiStory
+import mozilla.components.service.pocket.stories.db.PocketStoryEntity
+
+private const val POCKET_DIR = "pocket"
+
+/**
+ * Accessors to resources used in testing.
+ */
+internal object PocketTestResources {
+ val pocketEndpointFiveStoriesResponse = this::class.java.classLoader!!.getResource(
+ "$POCKET_DIR/stories_recommendations_response.json",
+ )!!.readText()
+
+ val pocketEndpointThreeSpocsResponse = this::class.java.classLoader!!.getResource(
+ "$POCKET_DIR/sponsored_stories_response.json",
+ )!!.readText()
+
+ val pocketEndpointNullTitleStoryBadResponse = this::class.java.classLoader!!.getResource(
+ "$POCKET_DIR/story_recommendation_null_title_response.json",
+ )!!.readText()
+
+ val pocketEndpointNullUrlStoryBadResponse = this::class.java.classLoader!!.getResource(
+ "$POCKET_DIR/story_recommendation_null_url_response.json",
+ )!!.readText()
+
+ val pocketEndpointNullImageUrlStoryBadResponse = this::class.java.classLoader!!.getResource(
+ "$POCKET_DIR/story_recommendation_null_imageUrl_response.json",
+ )!!.readText()
+
+ val apiExpectedPocketStoriesRecommendations: List<PocketApiStory> = listOf(
+ PocketApiStory(
+ title = "How to Remember Anything You Really Want to Remember, Backed by Science",
+ url = "https://getpocket.com/explore/item/how-to-remember-anything-you-really-want-to-remember-backed-by-science",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fwww.incimages.com%252Fuploaded_files%252Fimage%252F1920x1080%252Fgetty-862457080_394628.jpg",
+ publisher = "Pocket",
+ category = "general",
+ timeToRead = 3,
+ ),
+ PocketApiStory(
+ title = "‘I Don’t Want to Be Like a Family With My Co-Workers’",
+ url = "https://www.thecut.com/article/i-dont-want-to-be-like-a-family-with-my-co-workers.html",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpyxis.nymag.com%2Fv1%2Fimgs%2Fac8%2Fd22%2F315cd0cf1e3a43edfe0e0548f2edbcb1a1-ask-a-boss.1x.rsocial.w1200.jpg",
+ publisher = "The Cut",
+ category = "general",
+ timeToRead = 5,
+ ),
+ PocketApiStory(
+ title = "How America Failed in Afghanistan",
+ url = "https://www.newyorker.com/news/q-and-a/how-america-failed-in-afghanistan",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fmedia.newyorker.com%2Fphotos%2F6119484157b611aec9c99b43%2F16%3A9%2Fw_1280%2Cc_limit%2FChotiner-Afghanistan01.jpg",
+ publisher = "The New Yorker",
+ category = "general",
+ timeToRead = 14,
+ ),
+ PocketApiStory(
+ title = "How digital beauty filters perpetuate colorism",
+ url = "https://www.technologyreview.com/2021/08/15/1031804/digital-beauty-filters-photoshop-photo-editing-colorism-racism/",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fwp.technologyreview.com%2Fwp-content%2Fuploads%2F2021%2F08%2FBeautyScoreColorism.jpg%3Fresize%3D1200%2C600",
+ publisher = "MIT Technology Review",
+ category = "general",
+ timeToRead = 11,
+ ),
+ PocketApiStory(
+ title = "How to Get Rid of Black Mold Naturally",
+ url = "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F6757%252F1628024495_6109ae86db6cc.png",
+ publisher = "Pocket",
+ category = "general",
+ timeToRead = 4,
+ ),
+ )
+
+ val apiExpectedPocketSpocs: List<ApiSpoc> = listOf(
+ ApiSpoc(
+ id = 193815086,
+ title = "Eating Keto Has Never Been So Easy With Green Chef",
+ url = "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
+ imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
+ sponsor = "Green Chef",
+ shim = ApiSpocShim(
+ click = "193815086ClickShim",
+ impression = "193815086ImpressionShim",
+ ),
+ priority = 3,
+ caps = ApiSpocCaps(
+ lifetimeCount = 50,
+ flightPeriod = 86400,
+ flightCount = 10,
+ ),
+ ),
+ ApiSpoc(
+ id = 177986195,
+ title = "This Leading Cash Back Card Is a Slam Dunk if You Want a One-Card Wallet",
+ url = "https://www.fool.com/the-ascent/credit-cards/landing/discover-it-cash-back-review-v2-csr/?utm_site=theascent&utm_campaign=ta-cc-co-pocket-discb-04012022-5-na-firefox&utm_medium=cpc&utm_source=pocket",
+ imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/359f56a5423c4926ab3aa148e448d839.webp&resize=w618-h310",
+ sponsor = "The Ascent",
+ shim = ApiSpocShim(
+ click = "177986195ClickShim",
+ impression = "177986195ImpressionShim",
+ ),
+ priority = 2,
+ caps = ApiSpocCaps(
+ lifetimeCount = 50,
+ flightPeriod = 86400,
+ flightCount = 10,
+ ),
+ ),
+ ApiSpoc(
+ id = 192560056,
+ title = "The Incredible Lawn Hack That Can Make Your Neighbors Green With Envy Over Your Lawn",
+ url = "https://go.lawnbuddy.org/zf/50/7673?campaign=SUN_Pocket2022&creative=SUN_LawnCompare4-TheIncredibleLawnHackThatCanMakeYourNeighborsGreenWithEnvyOverYourLawn-WithoutSpendingAFortuneOnNewGrassAndWithoutBreakingASweat-20220420",
+ imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/ce16302e184342cda0619c08b7604c9c.jpg&resize=w618-h310",
+ sponsor = "Sunday",
+ shim = ApiSpocShim(
+ click = "192560056ClickShim",
+ impression = "192560056ImpressionShim",
+ ),
+ priority = 1,
+ caps = ApiSpocCaps(
+ lifetimeCount = 50,
+ flightPeriod = 86400,
+ flightCount = 10,
+ ),
+ ),
+ )
+
+ val dbExpectedPocketStory = PocketStoryEntity(
+ title = "How to Get Rid of Black Mold Naturally",
+ url = "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F6757%252F1628024495_6109ae86db6cc.png",
+ publisher = "Pocket",
+ category = "general",
+ timeToRead = 4,
+ timesShown = 23,
+ )
+
+ val clientExpectedPocketStory = PocketRecommendedStory(
+ title = "How digital beauty filters perpetuate colorism",
+ url = "https://www.technologyreview.com/2021/08/15/1031804/digital-beauty-filters-photoshop-photo-editing-colorism-racism/",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fwp.technologyreview.com%2Fwp-content%2Fuploads%2F2021%2F08%2FBeautyScoreColorism.jpg%3Fresize%3D1200%2C600",
+ publisher = "MIT Technology Review",
+ category = "general",
+ timeToRead = 11,
+ timesShown = 3,
+ )
+
+ val dbExpectedPocketSpoc = SpocEntity(
+ id = 193815086,
+ url = "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
+ title = "Eating Keto Has Never Been So Easy With Green Chef",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
+ sponsor = "Green Chef",
+ clickShim = "193815086ClickShim",
+ impressionShim = "193815086ImpressionShim",
+ priority = 3,
+ lifetimeCapCount = 50,
+ flightCapCount = 10,
+ flightCapPeriod = 86400,
+ )
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt
new file mode 100644
index 0000000000..8e3dda6f02
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt
@@ -0,0 +1,93 @@
+/* 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.service.pocket.spocs
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.pocket.ext.toLocalSpoc
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
+import mozilla.components.service.pocket.spocs.db.SpocsDao
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class SpocsRepositoryTest {
+ private val spocsRepo = spy(SpocsRepository(testContext))
+ private val dao = mock(SpocsDao::class.java)
+
+ @Before
+ fun setUp() {
+ doReturn(dao).`when`(spocsRepo).spocsDao
+ }
+
+ @Test
+ fun `GIVEN SpocsRepository WHEN asking for all spocs THEN return db entities mapped to domain type`() = runTest {
+ val spoc = PocketTestResources.dbExpectedPocketSpoc
+ val impressions = listOf(
+ SpocImpressionEntity(spoc.id),
+ SpocImpressionEntity(333),
+ SpocImpressionEntity(spoc.id),
+ )
+ doReturn(listOf(spoc)).`when`(dao).getAllSpocs()
+ doReturn(impressions).`when`(dao).getSpocsImpressions()
+
+ val result = spocsRepo.getAllSpocs()
+
+ verify(dao).getAllSpocs()
+ assertEquals(1, result.size)
+ assertSame(spoc.title, result[0].title)
+ assertSame(spoc.url, result[0].url)
+ assertSame(spoc.imageUrl, result[0].imageUrl)
+ assertSame(spoc.impressionShim, result[0].shim.impression)
+ assertSame(spoc.clickShim, result[0].shim.click)
+ assertEquals(spoc.priority, result[0].priority)
+ assertEquals(2, result[0].caps.currentImpressions.size)
+ assertEquals(spoc.lifetimeCapCount, result[0].caps.lifetimeCount)
+ assertEquals(spoc.flightCapCount, result[0].caps.flightCount)
+ assertEquals(spoc.flightCapPeriod, result[0].caps.flightPeriod)
+ }
+
+ @Test
+ fun `GIVEN SpocsRepository WHEN asking to delete all spocs THEN delete all from the database`() = runTest {
+ spocsRepo.deleteAllSpocs()
+
+ verify(dao).deleteAllSpocs()
+ }
+
+ @Test
+ fun `GIVEN SpocsRepository WHEN adding a new list of spocs THEN replace all present in the database`() = runTest {
+ val spoc = PocketTestResources.apiExpectedPocketSpocs[0]
+
+ spocsRepo.addSpocs(listOf(spoc))
+
+ verify(dao).cleanOldAndInsertNewSpocs(listOf(spoc.toLocalSpoc()))
+ }
+
+ @Test
+ fun `GIVEN SpocsRepository WHEN recording new spocs impressions THEN add this to the database`() = runTest {
+ val spocsIds = listOf(3, 33, 444)
+ val impressionsCaptor = argumentCaptor<List<SpocImpressionEntity>>()
+
+ spocsRepo.recordImpressions(spocsIds)
+
+ verify(dao).recordImpressions(impressionsCaptor.capture())
+ assertEquals(spocsIds.size, impressionsCaptor.value.size)
+ assertEquals(spocsIds[0], impressionsCaptor.value[0].spocId)
+ assertEquals(spocsIds[1], impressionsCaptor.value[1].spocId)
+ assertEquals(spocsIds[2], impressionsCaptor.value[2].spocId)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt
new file mode 100644
index 0000000000..1ce56903cc
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt
@@ -0,0 +1,314 @@
+/* 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.service.pocket.spocs
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStoriesRequestConfig
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.spocs.SpocsUseCases.RefreshSponsoredStories
+import mozilla.components.service.pocket.spocs.api.SpocsEndpoint
+import mozilla.components.service.pocket.stories.api.PocketResponse
+import mozilla.components.service.pocket.stories.api.PocketResponse.Failure
+import mozilla.components.service.pocket.stories.api.PocketResponse.Success
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.UUID
+import kotlin.reflect.KVisibility
+
+@OptIn(ExperimentalCoroutinesApi::class) // for runTest
+@RunWith(AndroidJUnit4::class)
+class SpocsUseCasesTest {
+ private val fetchClient: Client = mock()
+ private val profileId = UUID.randomUUID()
+ private val appId = "test"
+ private val sponsoredStoriesParams = PocketStoriesRequestConfig("123", "US", "NY")
+ private val useCases = spy(SpocsUseCases(testContext, fetchClient, profileId, appId, sponsoredStoriesParams))
+ private val spocsProvider: SpocsEndpoint = mock()
+ private val spocsRepo: SpocsRepository = mock()
+
+ @Before
+ fun setup() {
+ doReturn(spocsProvider).`when`(useCases).getSpocsProvider(any(), any(), any(), any())
+ doReturn(spocsRepo).`when`(useCases).getSpocsRepository(any())
+ }
+
+ @Test
+ fun `GIVEN a SpocsUseCases THEN its visibility is internal`() {
+ assertClassVisibility(SpocsUseCases::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a RefreshSponsoredStories THEN its visibility is internal`() {
+ assertClassVisibility(RefreshSponsoredStories::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a GetSponsoredStories THEN its visibility is internal`() {
+ assertClassVisibility(SpocsUseCases.GetSponsoredStories::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a DeleteProfile THEN its visibility is internal`() {
+ assertClassVisibility(SpocsUseCases.DeleteProfile::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is constructed THEN use the same parameters`() {
+ val refreshUseCase = useCases.refreshStories
+
+ assertSame(testContext, refreshUseCase.appContext)
+ assertSame(fetchClient, refreshUseCase.fetchClient)
+ assertSame(profileId, refreshUseCase.profileId)
+ assertSame(appId, refreshUseCase.appId)
+ assertSame(sponsoredStoriesParams, refreshUseCase.sponsoredStoriesParams)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN RefreshSponsoredStories is constructed separately THEN default to use the same parameters`() {
+ val refreshUseCase = useCases.RefreshSponsoredStories()
+
+ assertSame(testContext, refreshUseCase.appContext)
+ assertSame(fetchClient, refreshUseCase.fetchClient)
+ assertSame(profileId, refreshUseCase.profileId)
+ assertSame(appId, refreshUseCase.appId)
+ assertSame(sponsoredStoriesParams, refreshUseCase.sponsoredStoriesParams)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN RefreshSponsoredStories is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+ val fetchClient2: Client = mock()
+ val profileId2 = UUID.randomUUID()
+ val appId2 = "test"
+ val sponsoredStoriesParams2 = PocketStoriesRequestConfig("1", "CA", "OW")
+
+ val refreshUseCase = useCases.RefreshSponsoredStories(context2, fetchClient2, profileId2, appId2, sponsoredStoriesParams2)
+
+ assertSame(context2, refreshUseCase.appContext)
+ assertSame(fetchClient2, refreshUseCase.fetchClient)
+ assertSame(profileId2, refreshUseCase.profileId)
+ assertSame(appId2, refreshUseCase.appId)
+ assertSame(sponsoredStoriesParams2, refreshUseCase.sponsoredStoriesParams)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is called THEN download stories from API and return early if unsuccessful response`() = runTest {
+ val refreshUseCase = useCases.RefreshSponsoredStories()
+ val unsuccessfulResponse = getFailedSponsoredStories()
+ doReturn(unsuccessfulResponse).`when`(spocsProvider).getSponsoredStories()
+
+ val result = refreshUseCase.invoke()
+
+ assertFalse(result)
+ verify(spocsProvider).getSponsoredStories()
+ verify(spocsRepo, never()).addSpocs(any())
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is called THEN download stories from API and save a successful response locally`() = runTest {
+ val refreshUseCase = useCases.RefreshSponsoredStories()
+ val successfulResponse = getSuccessfulSponsoredStories()
+ doReturn(successfulResponse).`when`(spocsProvider).getSponsoredStories()
+
+ val result = refreshUseCase.invoke()
+
+ assertTrue(result)
+ verify(spocsProvider).getSponsoredStories()
+ verify(spocsRepo).addSpocs((successfulResponse as Success).data)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is constructed THEN use the same parameters`() {
+ val sponsoredStoriesUseCase = useCases.getStories
+
+ assertSame(testContext, sponsoredStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN GetSponsoredStories is constructed separately THEN default to use the same parameters`() {
+ val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
+
+ assertSame(testContext, sponsoredStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN GetSponsoredStories is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+
+ val sponsoredStoriesUseCase = useCases.GetSponsoredStories(context2)
+
+ assertSame(context2, sponsoredStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is called THEN return the stories from repository`() = runTest {
+ val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
+ val stories = listOf(PocketTestResources.clientExpectedPocketStory)
+ doReturn(stories).`when`(spocsRepo).getAllSpocs()
+
+ val result = sponsoredStoriesUseCase.invoke()
+
+ verify(spocsRepo).getAllSpocs()
+ assertEquals(result, stories)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is called THEN return return an empty list if none are available in the repository`() = runTest {
+ val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
+ doReturn(emptyList<PocketRecommendedStory>()).`when`(spocsRepo).getAllSpocs()
+
+ val result = sponsoredStoriesUseCase.invoke()
+
+ verify(spocsRepo).getAllSpocs()
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RecordImpression is constructed THEN use the same parameters`() {
+ val recordImpressionsUseCase = useCases.getStories
+
+ assertSame(testContext, recordImpressionsUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN RecordImpression is constructed separately THEN default to use the same parameters`() {
+ val recordImpressionsUseCase = useCases.RecordImpression()
+
+ assertSame(testContext, recordImpressionsUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN RecordImpression is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+
+ val recordImpressionsUseCase = useCases.RecordImpression(context2)
+
+ assertSame(context2, recordImpressionsUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RecordImpression is called THEN record impressions in database`() = runTest {
+ val recordImpressionsUseCase = useCases.RecordImpression()
+ val storiesIds = listOf(5, 55, 4321)
+ val spocsIdsCaptor = argumentCaptor<List<Int>>()
+
+ recordImpressionsUseCase(storiesIds)
+
+ verify(spocsRepo).recordImpressions(spocsIdsCaptor.capture())
+ assertEquals(3, spocsIdsCaptor.value.size)
+ assertEquals(storiesIds[0], spocsIdsCaptor.value[0])
+ assertEquals(storiesIds[1], spocsIdsCaptor.value[1])
+ assertEquals(storiesIds[2], spocsIdsCaptor.value[2])
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN DeleteProfile is constructed THEN use the same parameters`() {
+ val deleteProfileUseCase = useCases.deleteProfile
+
+ assertSame(testContext, deleteProfileUseCase.context)
+ assertSame(fetchClient, deleteProfileUseCase.fetchClient)
+ assertSame(profileId, deleteProfileUseCase.profileId)
+ assertSame(appId, deleteProfileUseCase.appId)
+ assertSame(sponsoredStoriesParams, deleteProfileUseCase.sponsoredStoriesParams)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN DeleteProfile is constructed separately THEN default to use the same parameters`() {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+
+ assertSame(testContext, deleteProfileUseCase.context)
+ assertSame(fetchClient, deleteProfileUseCase.fetchClient)
+ assertSame(profileId, deleteProfileUseCase.profileId)
+ assertSame(appId, deleteProfileUseCase.appId)
+ assertSame(sponsoredStoriesParams, deleteProfileUseCase.sponsoredStoriesParams)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN DeleteProfile is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+ val fetchClient2: Client = mock()
+ val profileId2 = UUID.randomUUID()
+ val appId2 = "test"
+ val sponsoredStoriesParams2 = PocketStoriesRequestConfig("1", "CA", "OW")
+
+ val deleteProfileUseCase = useCases.DeleteProfile(context2, fetchClient2, profileId2, appId2, sponsoredStoriesParams2)
+
+ assertSame(context2, deleteProfileUseCase.context)
+ assertSame(fetchClient2, deleteProfileUseCase.fetchClient)
+ assertSame(profileId2, deleteProfileUseCase.profileId)
+ assertSame(appId2, deleteProfileUseCase.appId)
+ assertSame(sponsoredStoriesParams2, deleteProfileUseCase.sponsoredStoriesParams)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN DeleteProfile is called THEN return true if profile deletion was successful`() = runTest {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+ val successfulResponse = Success(true)
+ doReturn(successfulResponse).`when`(spocsProvider).deleteProfile()
+
+ val result = deleteProfileUseCase.invoke()
+
+ verify(spocsProvider).deleteProfile()
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN DeleteProfile is called THEN return false if profile deletion was not successful`() = runTest {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+ val unsuccessfulResponse = Failure<Any>()
+ doReturn(unsuccessfulResponse).`when`(spocsProvider).deleteProfile()
+
+ val result = deleteProfileUseCase.invoke()
+
+ verify(spocsProvider).deleteProfile()
+ assertFalse(result)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN profile deletion is succesfull THEN delete all locally persisted spocs`() = runTest {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+ val successfulResponse = Success(true)
+ doReturn(successfulResponse).`when`(spocsProvider).deleteProfile()
+
+ deleteProfileUseCase.invoke()
+
+ verify(spocsRepo).deleteAllSpocs()
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN profile deletion is not succesfull THEN keep all locally persisted spocs`() = runTest {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+ val unsuccessfulResponse = Failure<Any>()
+ doReturn(unsuccessfulResponse).`when`(spocsProvider).deleteProfile()
+
+ deleteProfileUseCase.invoke()
+
+ verify(spocsRepo, never()).deleteAllSpocs()
+ }
+
+ private fun getSuccessfulSponsoredStories() =
+ PocketResponse.wrap(PocketTestResources.apiExpectedPocketSpocs)
+
+ private fun getFailedSponsoredStories() = PocketResponse.wrap(null)
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt
new file mode 100644
index 0000000000..c2af1d7cb4
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt
@@ -0,0 +1,327 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.service.pocket.PocketStoriesRequestConfig
+import mozilla.components.service.pocket.helpers.MockResponses
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.helpers.assertRequestParams
+import mozilla.components.service.pocket.helpers.assertResponseIsClosed
+import mozilla.components.service.pocket.helpers.assertSuccessfulRequestReturnsResponseBody
+import mozilla.components.service.pocket.stories.api.PocketEndpointRaw
+import mozilla.components.service.pocket.stories.api.PocketEndpointRaw.Companion
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import java.io.IOException
+import java.util.UUID
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class SpocsEndpointRawTest {
+ private val profileId = UUID.randomUUID()
+ private val appId = "test"
+ private val sponsoredStoriesParams: PocketStoriesRequestConfig = mock()
+
+ private lateinit var endpoint: SpocsEndpointRaw
+ private lateinit var client: Client
+
+ private lateinit var errorResponse: Response
+ private lateinit var successResponse: Response
+ private lateinit var defaultResponse: Response
+
+ @Before
+ fun setUp() {
+ errorResponse = MockResponses.getError()
+ successResponse = MockResponses.getSuccess()
+ defaultResponse = errorResponse
+
+ client = mock<Client>().also {
+ doReturn(defaultResponse).`when`(it).fetch(any())
+ }
+
+ whenever(sponsoredStoriesParams.siteId).thenReturn("")
+ whenever(sponsoredStoriesParams.country).thenReturn("")
+ whenever(sponsoredStoriesParams.city).thenReturn("")
+
+ endpoint = SpocsEndpointRaw(client, profileId, appId, sponsoredStoriesParams)
+ }
+
+ @Test
+ fun `GIVEN a PocketEndpointRaw THEN its visibility is internal`() {
+ assertClassVisibility(PocketEndpointRaw::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a debug build WHEN requesting spocs THEN the appropriate pocket proxy url is used`() {
+ SpocsEndpointRaw.isDebugBuild = true
+ val expectedUrl = "https://spocs.getpocket.dev/spocs"
+
+ assertRequestParams(
+ client,
+ makeRequest = {
+ endpoint.getSponsoredStories()
+ },
+ assertParams = { request ->
+ assertEquals(expectedUrl, request.url)
+ assertEquals(Request.Method.POST, request.method)
+
+ val requestBody = JSONObject(
+ request.body!!.useStream {
+ it.bufferedReader().readText()
+ },
+ )
+ assertEquals(2, requestBody["version"])
+ assertEquals(appId, requestBody["consumer_key"])
+ assertEquals(profileId.toString(), requestBody["pocket_id"])
+
+ request.headers!!.first {
+ it.name.equals("Content-Type", true)
+ }.value.contains("application/json", true)
+ },
+ )
+ }
+
+ @Test
+ fun `GIVEN a debug build AND a request configuration WHEN requesting spocs THEN the appropriate pocket proxy url is used`() {
+ SpocsEndpointRaw.isDebugBuild = true
+ val expectedUrl = "https://spocs.getpocket.dev/spocs?site=123&country=US&city=NY"
+ whenever(sponsoredStoriesParams.siteId).thenReturn("123")
+ whenever(sponsoredStoriesParams.country).thenReturn("US")
+ whenever(sponsoredStoriesParams.city).thenReturn("NY")
+
+ assertRequestParams(
+ client,
+ makeRequest = {
+ endpoint.getSponsoredStories()
+ },
+ assertParams = { request ->
+ assertEquals(expectedUrl, request.url)
+ assertEquals(Request.Method.POST, request.method)
+
+ val requestBody = JSONObject(
+ request.body!!.useStream {
+ it.bufferedReader().readText()
+ },
+ )
+ assertEquals(2, requestBody["version"])
+ assertEquals(appId, requestBody["consumer_key"])
+ assertEquals(profileId.toString(), requestBody["pocket_id"])
+
+ request.headers!!.first {
+ it.name.equals("Content-Type", true)
+ }.value.contains("application/json", true)
+ },
+ )
+ }
+
+ @Test
+ fun `GIVEN a release build WHEN requesting spocs THEN the appropriate pocket proxy url is used`() {
+ SpocsEndpointRaw.isDebugBuild = false
+ val expectedUrl = "https://spocs.getpocket.com/spocs"
+
+ assertRequestParams(
+ client,
+ makeRequest = {
+ endpoint.getSponsoredStories()
+ },
+ assertParams = { request ->
+ assertEquals(expectedUrl, request.url)
+ assertEquals(Request.Method.POST, request.method)
+
+ val requestBody = JSONObject(
+ request.body!!.useStream {
+ it.bufferedReader().readText()
+ },
+ )
+ assertEquals(2, requestBody["version"])
+ assertEquals(appId, requestBody["consumer_key"])
+ assertEquals(profileId.toString(), requestBody["pocket_id"])
+
+ request.headers!!.first {
+ it.name.equals("Content-Type", true)
+ }.value.contains("application/json", true)
+ },
+ )
+ }
+
+ @Test
+ fun `GIVEN a release build AND a request configuration WHEN requesting spocs THEN the appropriate pocket proxy url is used`() {
+ SpocsEndpointRaw.isDebugBuild = false
+ val expectedUrl = "https://spocs.getpocket.com/spocs?site=123&country=US&city=NY"
+ whenever(sponsoredStoriesParams.siteId).thenReturn("123")
+ whenever(sponsoredStoriesParams.country).thenReturn("US")
+ whenever(sponsoredStoriesParams.city).thenReturn("NY")
+
+ assertRequestParams(
+ client,
+ makeRequest = {
+ endpoint.getSponsoredStories()
+ },
+ assertParams = { request ->
+ assertEquals(expectedUrl, request.url)
+ assertEquals(Request.Method.POST, request.method)
+
+ val requestBody = JSONObject(
+ request.body!!.useStream {
+ it.bufferedReader().readText()
+ },
+ )
+ assertEquals(2, requestBody["version"])
+ assertEquals(appId, requestBody["consumer_key"])
+ assertEquals(profileId.toString(), requestBody["pocket_id"])
+
+ request.headers!!.first {
+ it.name.equals("Content-Type", true)
+ }.value.contains("application/json", true)
+ },
+ )
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the client throws an IOException THEN null is returned`() {
+ doThrow(IOException::class.java).`when`(client).fetch(any())
+
+ assertNull(endpoint.getSponsoredStories())
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is null THEN null is returned`() {
+ doReturn(null).`when`(client).fetch(any())
+
+ assertNull(endpoint.getSponsoredStories())
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is not a success THEN null is returned`() {
+ doReturn(errorResponse).`when`(client).fetch(any())
+
+ assertNull(endpoint.getSponsoredStories())
+ }
+
+ @Test
+ fun `GIVEN a debug build WHEN requesting profile deletion THEN the appropriate pocket proxy url is used`() {
+ SpocsEndpointRaw.isDebugBuild = true
+ val expectedUrl = "https://spocs.getpocket.dev/user"
+
+ assertRequestParams(
+ client,
+ makeRequest = {
+ endpoint.deleteProfile()
+ },
+ assertParams = { request ->
+ assertEquals(expectedUrl, request.url)
+ assertEquals(Request.Method.DELETE, request.method)
+ },
+ )
+ }
+
+ @Test
+ fun `GIVEN a release build WHEN requesting profile deletion THEN the appropriate pocket proxy url is used`() {
+ SpocsEndpointRaw.isDebugBuild = false
+ val expectedUrl = "https://spocs.getpocket.com/user"
+
+ assertRequestParams(
+ client,
+ makeRequest = {
+ endpoint.deleteProfile()
+ },
+ assertParams = { request ->
+ assertEquals(expectedUrl, request.url)
+ assertEquals(Request.Method.DELETE, request.method)
+ },
+ )
+ }
+
+ @Test
+ fun `WHEN requesting profile deletion and the client throws an IOException THEN false is returned`() {
+ doThrow(IOException::class.java).`when`(client).fetch(any())
+
+ assertFalse(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `WHEN requesting account deletion and the response is not a success THEN false is returned`() {
+ doReturn(errorResponse).`when`(client).fetch(any())
+
+ assertFalse(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is a success THEN the response body is returned`() {
+ assertSuccessfulRequestReturnsResponseBody(client, endpoint::getSponsoredStories)
+ }
+
+ @Test
+ fun `WHEN requesting profile deletion and the response is a success THEN true is returned`() {
+ val response = MockResponses.getSuccess()
+ doReturn(response).`when`(client).fetch(any())
+
+ assertTrue(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is an error THEN response is closed`() {
+ assertResponseIsClosed(client, errorResponse) {
+ endpoint.getSponsoredStories()
+ }
+ }
+
+ @Test
+ fun `GIVEN a response from the request to delete profile WHEN inferring it's success THEN don't use the reponse body`() {
+ // Leverage the fact that a stream can only be read once to know if it was previously read.
+
+ doReturn(errorResponse).`when`(client).fetch(any())
+ errorResponse.use { "Only the response status should be used, not the response body" }
+
+ doReturn(successResponse).`when`(client).fetch(any())
+ successResponse.use { "Only the response status should be used, not the response body" }
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is a success THEN response is closed`() {
+ assertResponseIsClosed(client, successResponse) {
+ endpoint.getSponsoredStories()
+ }
+ }
+
+ @Test
+ fun `WHEN newInstance is called THEN a new instance configured with the client provided is returned`() {
+ val result = Companion.newInstance(client)
+
+ assertSame(client, result.client)
+ }
+
+ @Test
+ fun `GIVEN a debug build WHEN querying the base url THEN use the development endpoint`() {
+ SpocsEndpointRaw.isDebugBuild = true
+ val expectedUrl = "https://spocs.getpocket.dev/"
+
+ assertEquals(expectedUrl, SpocsEndpointRaw.baseUrl)
+ }
+
+ @Test
+ fun `GIVEN a release build WHEN querying the base url THEN use the production endpoint`() {
+ SpocsEndpointRaw.isDebugBuild = false
+ val expectedUrl = "https://spocs.getpocket.com/"
+
+ assertEquals(expectedUrl, SpocsEndpointRaw.baseUrl)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt
new file mode 100644
index 0000000000..3d3e7188c4
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt
@@ -0,0 +1,161 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStoriesRequestConfig
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.helpers.assertResponseIsFailure
+import mozilla.components.service.pocket.helpers.assertResponseIsSuccess
+import mozilla.components.service.pocket.stories.api.PocketResponse
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.UUID
+import kotlin.reflect.KVisibility
+
+@OptIn(ExperimentalCoroutinesApi::class) // for runTest
+@RunWith(AndroidJUnit4::class)
+class SpocsEndpointTest {
+
+ private lateinit var endpoint: SpocsEndpoint
+ private var raw: SpocsEndpointRaw = mock() // we shorten the name to avoid confusion with endpoint.
+ private var jsonParser: SpocsJSONParser = mock()
+ private var client: Client = mock()
+
+ @Before
+ fun setUp() {
+ endpoint = SpocsEndpoint(raw, jsonParser)
+ }
+
+ @Test
+ fun `GIVEN a SpocsEndpoint THEN its visibility is internal`() {
+ assertClassVisibility(SpocsEndpoint::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a request for spocs WHEN getting a null response THEN a failure is returned`() = runTest {
+ doReturn(null).`when`(raw).getSponsoredStories()
+
+ assertResponseIsFailure(endpoint.getSponsoredStories())
+ }
+
+ @Test
+ fun `GIVEN a request for spocs WHEN getting a null response THEN we do not attempt to parse stories`() = runTest {
+ doReturn(null).`when`(raw).getSponsoredStories()
+
+ doThrow(
+ AssertionError("JSONParser should not be called for a null endpoint response"),
+ ).`when`(jsonParser).jsonToSpocs(any())
+
+ endpoint.getSponsoredStories()
+ }
+
+ @Test
+ fun `GIVEN a request for deleting profile WHEN the response is unsuccessful THEN a failure is returned`() = runTest {
+ doReturn(false).`when`(raw).deleteProfile()
+
+ assertResponseIsFailure(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `GIVEN a request for deleting profile WHEN the response is successful THEN success is returned`() = runTest {
+ doReturn(true).`when`(raw).deleteProfile()
+
+ assertResponseIsSuccess(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `GIVEN a request for spocs WHEN getting an empty response THEN a failure is returned`() = runTest {
+ arrayOf(
+ "",
+ " ",
+ ).forEach { response ->
+ doReturn(response).`when`(raw).getSponsoredStories()
+
+ assertResponseIsFailure(endpoint.getSponsoredStories())
+ }
+ }
+
+ @Test
+ fun `GIVEN a request for spocs WHEN getting an empty response THEN we do not attempt to parse stories`() = runTest {
+ arrayOf(
+ "",
+ " ",
+ ).forEach { response ->
+ doReturn(response).`when`(raw).getSponsoredStories()
+ doThrow(
+ AssertionError("JSONParser should not be called for an empty endpoint response"),
+ ).`when`(jsonParser).jsonToSpocs(any())
+
+ endpoint.getSponsoredStories()
+ }
+ }
+
+ @Test
+ fun `GIVEN a request for stories WHEN getting a response THEN parse it through PocketJSONParser`() = runTest {
+ arrayOf(
+ "{}",
+ """{"expectedJSON": 101}""",
+ """{ "spocs": [] }""",
+ ).forEach { response ->
+ doReturn(response).`when`(raw).getSponsoredStories()
+
+ endpoint.getSponsoredStories()
+
+ verify(jsonParser, times(1)).jsonToSpocs(response)
+ }
+ }
+
+ @Test
+ fun `GIVEN a request for stories WHEN getting a valid response THEN success is returned`() = runTest {
+ endpoint = SpocsEndpoint(raw, SpocsJSONParser)
+ val response = PocketTestResources.pocketEndpointThreeSpocsResponse
+ doReturn(response).`when`(raw).getSponsoredStories()
+
+ val result = endpoint.getSponsoredStories()
+
+ assertTrue(result is PocketResponse.Success)
+ }
+
+ @Test
+ fun `GIVEN a request for stories WHEN getting a valid response THEN a success response with parsed stories is returned`() = runTest {
+ endpoint = SpocsEndpoint(raw, SpocsJSONParser)
+ val response = PocketTestResources.pocketEndpointThreeSpocsResponse
+ doReturn(response).`when`(raw).getSponsoredStories()
+ val expected = PocketTestResources.apiExpectedPocketSpocs
+
+ val result = endpoint.getSponsoredStories()
+
+ assertEquals(expected, (result as? PocketResponse.Success)?.data)
+ }
+
+ @Test
+ fun `WHEN newInstance is called THEN a new SpocsEndpoint is returned as a wrapper over a configured SpocsEndpointRaw`() {
+ val profileId = UUID.randomUUID()
+ val appId = "test"
+ val sponsoredStoriesParams = PocketStoriesRequestConfig("123")
+
+ val result = SpocsEndpoint.Companion.newInstance(client, profileId, appId, sponsoredStoriesParams)
+
+ assertSame(client, result.rawEndpoint.client)
+ assertSame(profileId, result.rawEndpoint.profileId)
+ assertSame(appId, result.rawEndpoint.appId)
+ assertSame(sponsoredStoriesParams, result.rawEndpoint.sponsoredStoriesParams)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt
new file mode 100644
index 0000000000..a49d9bd96e
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt
@@ -0,0 +1,200 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class SpocsJSONParserTest {
+ @Test
+ fun `GIVEN a SpocsJSONParser THEN its visibility is internal`() {
+ assertClassVisibility(SpocsJSONParser::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN SpocsJSONParser WHEN parsing spocs THEN ApiSpocs are returned`() {
+ val expectedSpocs = PocketTestResources.apiExpectedPocketSpocs
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val actualSpocs = SpocsJSONParser.jsonToSpocs(pocketJSON)
+
+ assertNotNull(actualSpocs)
+ assertEquals(3, actualSpocs!!.size)
+ assertEquals(expectedSpocs, actualSpocs)
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing titles THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(2) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("title", 2, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing urls THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("url", 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing image urls THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(0) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("image_src", 0, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing sponsors THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("sponsor", 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing click shims THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(2) }
+ val pocketJsonWithMissingTitle = removeShimFromSpoc("click", 2, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing impression shims THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingTitle = removeShimFromSpoc("impression", 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing priority THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingPriority = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingPriority = removeJsonFieldFromArrayIndex("priority", 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingPriority)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingPriority.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing a lifetime count cap THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingLifetimeCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(0) }
+ val pocketJsonWithMissingLifetimeCap = removeCapFromSpoc(JSON_SPOC_CAPS_LIFETIME_KEY, 0, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingLifetimeCap)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingLifetimeCap.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing a flight count cap THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingFlightCountCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingFlightCountCap = removeCapFromSpoc(JSON_SPOC_CAPS_FLIGHT_COUNT_KEY, 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingFlightCountCap)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingFlightCountCap.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing a flight period cap THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingFlightPeriodCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(2) }
+ val pocketJsonWithMissingFlightPeriodCap = removeCapFromSpoc(JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY, 2, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingFlightPeriodCap)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingFlightPeriodCap.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs for an invalid JSON String THEN null is returned`() {
+ assertNull(SpocsJSONParser.jsonToSpocs("{!!}}"))
+ }
+}
+
+private fun removeJsonFieldFromArrayIndex(fieldName: String, indexInArray: Int, json: String): String {
+ val obj = JSONObject(json)
+ val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
+ spocsJson.getJSONObject(indexInArray).remove(fieldName)
+ return obj.toString()
+}
+
+private fun removeShimFromSpoc(shimName: String, spocIndex: Int, json: String): String {
+ val obj = JSONObject(json)
+ val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
+ val spocJson = spocsJson.getJSONObject(spocIndex)
+ spocJson.getJSONObject(JSON_SPOC_SHIMS_KEY).remove(shimName)
+ return obj.toString()
+}
+
+private fun removeCapFromSpoc(cap: String, spocIndex: Int, json: String): String {
+ val obj = JSONObject(json)
+ val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
+ val spocJson = spocsJson.getJSONObject(spocIndex)
+ val capsJSON = spocJson.getJSONObject(JSON_SPOC_CAPS_KEY)
+
+ if (cap == JSON_SPOC_CAPS_LIFETIME_KEY) {
+ capsJSON.remove(cap)
+ } else {
+ capsJSON.getJSONObject(JSON_SPOC_CAPS_FLIGHT_KEY).remove(cap)
+ }
+
+ return obj.toString()
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt
new file mode 100644
index 0000000000..f7dee01418
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt
@@ -0,0 +1,17 @@
+/* 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.service.pocket.spocs.db
+
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import org.junit.Test
+import kotlin.reflect.KVisibility.INTERNAL
+
+class SpocEntityTest {
+ // This is the data type persisted locally. No need to be public
+ @Test
+ fun `GIVEN a spoc entity THEN it's visibility is internal`() {
+ assertClassVisibility(SpocEntity::class, INTERNAL)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt
new file mode 100644
index 0000000000..4e119b0bb2
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt
@@ -0,0 +1,29 @@
+/* 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.service.pocket.spocs.db
+
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import kotlin.reflect.KVisibility.INTERNAL
+
+class SpocImpressionEntityTest {
+ // This is the data type persisted locally. No need to be public
+ @Test
+ fun `GIVEN a spoc entity THEN it's visibility is internal`() {
+ assertClassVisibility(SpocImpressionEntity::class, INTERNAL)
+ }
+
+ @Test
+ fun `WHEN a new impression is created THEN the timestamp should be seconds from Epoch`() {
+ val nowInSeconds = System.currentTimeMillis() / 1000
+ val impression = SpocImpressionEntity(2)
+
+ assertTrue(
+ LongRange(nowInSeconds - 5, nowInSeconds + 5)
+ .contains(impression.impressionDateInSeconds),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt
new file mode 100644
index 0000000000..b4bd5e6c45
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt
@@ -0,0 +1,513 @@
+/* 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.service.pocket.spocs.db
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class SpocsDaoTest {
+ private lateinit var database: PocketRecommendationsDatabase
+ private lateinit var dao: SpocsDao
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+ database = Room
+ .inMemoryDatabaseBuilder(testContext, PocketRecommendationsDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+ dao = database.spocsDao()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+
+ @Test
+ fun `GIVEN an empty table WHEN a story is inserted and then queried THEN return the same story`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+
+ dao.insertSpocs(listOf(story))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(story), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different id is tried to be inserted THEN add that to the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ id = 1,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory, story), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different url is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ title = "updated" + story.url,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different title is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ title = "updated" + story.title,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different image url is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ imageUrl = "updated" + story.imageUrl,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different sponsor is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ sponsor = "updated" + story.sponsor,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different click shim is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ clickShim = "updated" + story.clickShim,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different impression shim is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ impressionShim = "updated" + story.impressionShim,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different priority is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ priority = 765,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with a different lifetime cap count is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ lifetimeCapCount = 123,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with a different flight count cap is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ flightCapCount = 999,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with a different flight period cap is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ flightCapPeriod = 1,
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN no persisted storied WHEN asked to insert a list of stories THEN add them all to the table`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
+
+ dao.insertSpocs(listOf(story1, story2, story3, story4))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(story1, story2, story3, story4), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to delete them THEN remove all from the table`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
+ dao.insertSpocs(listOf(story1, story2, story3, story4))
+
+ dao.deleteAllSpocs()
+ val result = dao.getAllSpocs()
+
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to delete some THEN remove remove the ones already persisted`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
+ val story5 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 5)
+ dao.insertSpocs(listOf(story1, story2, story3, story4))
+
+ dao.deleteSpocs(listOf(story2, story3, story5))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(story1, story4), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN remove from table all stories not found in the new list`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
+ dao.insertSpocs(listOf(story1, story2, story3, story4))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(story2, story4))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(story2, story4), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new ids`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ id = story1.id * 3,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ // Order gets reversed because the original story is replaced and another one is added.
+ assertEquals(listOf(story2, updatedStory1), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only url changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ url = "updated" + story1.url,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only title changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ title = "updated" + story1.title,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only image url changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ imageUrl = "updated" + story1.imageUrl,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only sponsor changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ sponsor = "updated" + story1.sponsor,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the click shim changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ clickShim = "updated" + story1.clickShim,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the impression shim changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ impressionShim = "updated" + story1.impressionShim,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only priority changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ priority = 678,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the lifetime count cap changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ lifetimeCapCount = 4322,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the flight count cap changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ flightCapCount = 111111,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the flight period cap changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ flightCapPeriod = 7,
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN no stories are persisted WHEN asked to record an impression THEN don't persist data and don't throw errors`() = runTest {
+ dao.recordImpression(6543321)
+
+ val result = dao.getSpocsImpressions()
+
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN stories are persisted WHEN asked to record impressions for other stories also THEN persist impression only for existing stories`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ dao.insertSpocs(listOf(story1, story3))
+
+ dao.recordImpressions(
+ listOf(
+ SpocImpressionEntity(story1.id),
+ SpocImpressionEntity(story2.id),
+ SpocImpressionEntity(story3.id),
+ ),
+ )
+ val result = dao.getSpocsImpressions()
+
+ assertEquals(2, result.size)
+ assertEquals(story1.id, result[0].spocId)
+ assertEquals(story3.id, result[1].spocId)
+ }
+
+ @Test
+ fun `GIVEN stories are persisted WHEN asked to record impressions for existing stories THEN persist the impressions`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ dao.insertSpocs(listOf(story1, story2, story3))
+
+ dao.recordImpressions(
+ listOf(
+ SpocImpressionEntity(story1.id),
+ SpocImpressionEntity(story3.id),
+ ),
+ )
+ val result = dao.getSpocsImpressions()
+
+ assertEquals(2, result.size)
+ assertEquals(story1.id, result[0].spocId)
+ assertEquals(story3.id, result[1].spocId)
+ }
+
+ /**
+ * Sets an executor to be used for database transactions.
+ * Needs to be used along with "runTest" to ensure waiting for transactions to finish but not hang tests.
+ */
+ private fun setupDatabseForTransactions() {
+ database = Room
+ .inMemoryDatabaseBuilder(testContext, PocketRecommendationsDatabase::class.java)
+ .setTransactionExecutor(executor)
+ .allowMainThreadQueries()
+ .build()
+ dao = database.spocsDao()
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepositoryTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepositoryTest.kt
new file mode 100644
index 0000000000..9e5b287ee0
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepositoryTest.kt
@@ -0,0 +1,75 @@
+/* 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.service.pocket.stories
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.pocket.ext.toPartialTimeShownUpdate
+import mozilla.components.service.pocket.ext.toPocketLocalStory
+import mozilla.components.service.pocket.ext.toPocketRecommendedStory
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDao
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class PocketRecommendationsRepositoryTest {
+
+ private val pocketRepo = spy(PocketRecommendationsRepository(testContext))
+ private lateinit var dao: PocketRecommendationsDao
+
+ @Before
+ fun setUp() {
+ dao = mock(PocketRecommendationsDao::class.java)
+ `when`(pocketRepo.pocketRecommendationsDao).thenReturn(dao)
+ }
+
+ @Test
+ fun `GIVEN PocketRecommendationsRepository WHEN getPocketRecommendedStories is called THEN return db entities mapped to domain type`() {
+ runTest {
+ val dbStory = PocketTestResources.dbExpectedPocketStory
+ `when`(dao.getPocketStories()).thenReturn(listOf(dbStory))
+
+ val result = pocketRepo.getPocketRecommendedStories()
+
+ verify(dao).getPocketStories()
+ assertEquals(1, result.size)
+ assertEquals(dbStory.toPocketRecommendedStory(), result[0])
+ }
+ }
+
+ @Test
+ fun `GIVEN PocketRecommendationsRepository WHEN addAllPocketApiStories is called THEN persist the received story to db`() {
+ runTest {
+ val apiStories = PocketTestResources.apiExpectedPocketStoriesRecommendations
+ val apiStoriesMappedForDb = apiStories.map { it.toPocketLocalStory() }
+
+ pocketRepo.addAllPocketApiStories(apiStories)
+
+ verify(dao).cleanOldAndInsertNewPocketStories(apiStoriesMappedForDb)
+ }
+ }
+
+ @Test
+ fun `GIVEN PocketRecommendationsRepository WHEN updateShownPocketRecommendedStories should persist the received story to db`() {
+ runTest {
+ val clientStories = listOf(PocketTestResources.clientExpectedPocketStory)
+ val clientStoriesPartialUpdate = clientStories.map { it.toPartialTimeShownUpdate() }
+
+ pocketRepo.updateShownPocketRecommendedStories(clientStories)
+
+ verify(dao).updateTimesShown(clientStoriesPartialUpdate)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketStoriesUseCasesTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketStoriesUseCasesTest.kt
new file mode 100644
index 0000000000..4af13135a8
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketStoriesUseCasesTest.kt
@@ -0,0 +1,197 @@
+/* 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.service.pocket.stories
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.stories.api.PocketEndpoint
+import mozilla.components.service.pocket.stories.api.PocketResponse
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+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 kotlin.reflect.KVisibility
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class PocketStoriesUseCasesTest {
+ private val fetchClient: Client = mock()
+ private val useCases = spy(PocketStoriesUseCases(testContext, fetchClient))
+ private val pocketRepo: PocketRecommendationsRepository = mock()
+ private val pocketEndoint: PocketEndpoint = mock()
+
+ @Before
+ fun setup() {
+ doReturn(pocketEndoint).`when`(useCases).getPocketEndpoint(any())
+ doReturn(pocketRepo).`when`(useCases).getPocketRepository(any())
+ }
+
+ @Test
+ fun `GIVEN a PocketStoriesUseCases THEN its visibility is internal`() {
+ assertClassVisibility(PocketStoriesUseCases::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a RefreshPocketStories THEN its visibility is internal`() {
+ assertClassVisibility(
+ PocketStoriesUseCases.RefreshPocketStories::class,
+ KVisibility.INTERNAL,
+ )
+ }
+
+ @Test
+ fun `GIVEN a GetPocketStories THEN its visibility is public`() {
+ assertClassVisibility(PocketStoriesUseCases.GetPocketStories::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN RefreshPocketStories is constructed THEN use the same parameters`() {
+ val refreshUseCase = useCases.refreshStories
+
+ assertSame(testContext, refreshUseCase.appContext)
+ assertSame(fetchClient, refreshUseCase.fetchClient)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN RefreshPocketStories is constructed separately THEN default to use the same parameters`() {
+ val refreshUseCase = useCases.RefreshPocketStories()
+
+ assertSame(testContext, refreshUseCase.appContext)
+ assertSame(fetchClient, refreshUseCase.fetchClient)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN RefreshPocketStories is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+ val fetchClient2: Client = mock()
+
+ val refreshUseCase = useCases.RefreshPocketStories(context2, fetchClient2)
+
+ assertSame(context2, refreshUseCase.appContext)
+ assertSame(fetchClient2, refreshUseCase.fetchClient)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN RefreshPocketStories is called THEN download stories from API and return early if unsuccessful response`() = runTest {
+ val refreshUseCase = useCases.RefreshPocketStories()
+ val successfulResponse = getSuccessfulPocketStories()
+ doReturn(successfulResponse).`when`(pocketEndoint).getRecommendedStories()
+
+ val result = refreshUseCase.invoke()
+
+ assertTrue(result)
+ verify(pocketEndoint).getRecommendedStories()
+ verify(pocketRepo).addAllPocketApiStories((successfulResponse as PocketResponse.Success).data)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN RefreshPocketStories is called THEN download stories from API and save a successful response locally`() = runTest {
+ val refreshUseCase = useCases.RefreshPocketStories()
+ val successfulResponse = getFailedPocketStories()
+ doReturn(successfulResponse).`when`(pocketEndoint).getRecommendedStories()
+
+ val result = refreshUseCase.invoke()
+
+ assertFalse(result)
+ verify(pocketEndoint).getRecommendedStories()
+ verify(pocketRepo, never()).addAllPocketApiStories(any())
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN GetPocketStories is constructed THEN use the same parameters`() {
+ val getStoriesUseCase = useCases.getStories
+
+ assertSame(testContext, getStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN GetPocketStories is constructed separately THEN default to use the same parameters`() {
+ val getStoriesUseCase = useCases.GetPocketStories()
+
+ assertSame(testContext, getStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN GetPocketStories is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+
+ val getStoriesUseCase = useCases.GetPocketStories(context2)
+
+ assertSame(context2, getStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN GetPocketStories is called THEN delegate the repository to return locally stored stories`() =
+ runTest {
+ val getStoriesUseCase = useCases.GetPocketStories()
+ doReturn(emptyList<PocketRecommendedStory>()).`when`(pocketRepo)
+ .getPocketRecommendedStories()
+ var result = getStoriesUseCase.invoke()
+ verify(pocketRepo).getPocketRecommendedStories()
+ assertTrue(result.isEmpty())
+
+ val stories = listOf(PocketTestResources.clientExpectedPocketStory)
+ doReturn(stories).`when`(pocketRepo).getPocketRecommendedStories()
+ result = getStoriesUseCase.invoke()
+ // getPocketRecommendedStories() should've been called 2 times. Once in the above check, once now.
+ verify(pocketRepo, times(2)).getPocketRecommendedStories()
+ assertEquals(result, stories)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN UpdateStoriesTimesShown is constructed THEN use the same parameters`() {
+ val updateStoriesTimesShown = useCases.updateTimesShown
+
+ assertSame(testContext, updateStoriesTimesShown.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN UpdateStoriesTimesShown is constructed separately THEN default to use the same parameters`() {
+ val updateStoriesTimesShown = useCases.UpdateStoriesTimesShown()
+
+ assertSame(testContext, updateStoriesTimesShown.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN UpdateStoriesTimesShown is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+
+ val updateStoriesTimesShown = useCases.UpdateStoriesTimesShown(context2)
+
+ assertSame(context2, updateStoriesTimesShown.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN UpdateStoriesTimesShown is called THEN delegate the repository to update the stories shown`() = runTest {
+ val updateStoriesTimesShown = useCases.UpdateStoriesTimesShown()
+ val updatedStories: List<PocketRecommendedStory> = mock()
+
+ updateStoriesTimesShown.invoke(updatedStories)
+
+ verify(pocketRepo).updateShownPocketRecommendedStories(updatedStories)
+ }
+
+ private fun getSuccessfulPocketStories() =
+ PocketResponse.wrap(PocketTestResources.apiExpectedPocketStoriesRecommendations)
+
+ private fun getFailedPocketStories() = PocketResponse.wrap(null)
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketApiStoryTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketApiStoryTest.kt
new file mode 100644
index 0000000000..34960a83d1
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketApiStoryTest.kt
@@ -0,0 +1,17 @@
+/* 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.service.pocket.stories.api
+
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import org.junit.Test
+import kotlin.reflect.KVisibility
+
+class PocketApiStoryTest {
+ // This is the data type as received from the Pocket endpoint. No need to be public.
+ @Test
+ fun `GIVEN a PocketRecommendedStory THEN its visibility is internal`() {
+ assertClassVisibility(PocketApiStory::class, KVisibility.INTERNAL)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointRawTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointRawTest.kt
new file mode 100644
index 0000000000..e438b8c849
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointRawTest.kt
@@ -0,0 +1,115 @@
+/* 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.service.pocket.stories.api
+
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Response
+import mozilla.components.service.pocket.helpers.MockResponses
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.helpers.assertRequestParams
+import mozilla.components.service.pocket.helpers.assertResponseIsClosed
+import mozilla.components.service.pocket.helpers.assertSuccessfulRequestReturnsResponseBody
+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.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.IOException
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class PocketEndpointRawTest {
+ private val url = "https://mozilla.org".toUri()
+
+ private lateinit var endpoint: PocketEndpointRaw
+ private lateinit var client: Client
+
+ private lateinit var errorResponse: Response
+ private lateinit var successResponse: Response
+ private lateinit var defaultResponse: Response
+
+ @Before
+ fun setUp() {
+ errorResponse = MockResponses.getError()
+ successResponse = MockResponses.getSuccess()
+ defaultResponse = errorResponse
+
+ client = mock<Client>().also {
+ whenever(it.fetch(any())).thenReturn(defaultResponse)
+ }
+
+ endpoint = PocketEndpointRaw(client)
+ }
+
+ @Test
+ fun `GIVEN a PocketEndpointRaw THEN its visibility is internal`() {
+ assertClassVisibility(PocketEndpointRaw::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `WHEN requesting stories recommendations THEN the firefox android home recommendations url is used`() {
+ val expectedUrl = "https://firefox-android-home-recommendations.getpocket.com/"
+
+ assertRequestParams(
+ client,
+ makeRequest = {
+ endpoint.getRecommendedStories()
+ },
+ assertParams = { request ->
+ assertEquals(expectedUrl, request.url)
+ },
+ )
+ }
+
+ @Test
+ fun `WHEN requesting stories recommendations and the client throws an IOException THEN null is returned`() {
+ whenever(client.fetch(any())).thenThrow(IOException::class.java)
+ assertNull(endpoint.getRecommendedStories())
+ }
+
+ @Test
+ fun `WHEN requesting stories recommendations and the response is null THEN null is returned`() {
+ whenever(client.fetch(any())).thenReturn(null)
+ assertNull(endpoint.getRecommendedStories())
+ }
+
+ @Test
+ fun `WHEN requesting stories recommendations and the response is not a success THEN null is returned`() {
+ whenever(client.fetch(any())).thenReturn(errorResponse)
+ assertNull(endpoint.getRecommendedStories())
+ }
+
+ @Test
+ fun `WHEN requesting stories recommendations and the response is a success THEN the response body is returned`() {
+ assertSuccessfulRequestReturnsResponseBody(client, endpoint::getRecommendedStories)
+ }
+
+ @Test
+ fun `WHEN requesting stories recommendations and the response is an error THEN response is closed`() {
+ assertResponseIsClosed(client, errorResponse) {
+ endpoint.getRecommendedStories()
+ }
+ }
+
+ @Test
+ fun `WHEN requesting stories recommendations and the response is a success THEN response is closed`() {
+ assertResponseIsClosed(client, successResponse) {
+ endpoint.getRecommendedStories()
+ }
+ }
+
+ @Test
+ fun `WHEN newInstance is called THEN a new instance configured with the client provided is returned`() {
+ val result = PocketEndpointRaw.newInstance(client)
+
+ assertSame(client, result.client)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointTest.kt
new file mode 100644
index 0000000000..db53750279
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointTest.kt
@@ -0,0 +1,97 @@
+/* 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.service.pocket.stories.api
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.helpers.assertResponseIsFailure
+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.assertSame
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import kotlin.reflect.KVisibility
+
+class PocketEndpointTest {
+
+ private lateinit var endpoint: PocketEndpoint
+ private lateinit var raw: PocketEndpointRaw // we shorten the name to avoid confusion with endpoint.
+ private lateinit var jsonParser: PocketJSONParser
+
+ private lateinit var client: Client
+
+ @Before
+ fun setUp() {
+ raw = mock()
+ jsonParser = mock()
+ endpoint = PocketEndpoint(raw, jsonParser)
+
+ client = mock()
+ }
+
+ @Test
+ fun `GIVEN a PocketEndpoint THEN its visibility is internal`() {
+ assertClassVisibility(PocketEndpoint::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN an api request for stories WHEN getting a null response THEN a failure is returned`() {
+ whenever(raw.getRecommendedStories()).thenReturn(null)
+ whenever(jsonParser.jsonToPocketApiStories(any())).thenThrow(
+ AssertionError(
+ "We assume this won't get called so we don't mock it",
+ ),
+ )
+
+ assertResponseIsFailure(endpoint.getRecommendedStories())
+ }
+
+ @Test
+ fun `GIVEN an api request for stories WHEN getting an empty response THEN a failure is returned`() {
+ whenever(raw.getRecommendedStories()).thenReturn("")
+ whenever(jsonParser.jsonToPocketApiStories(any())).thenReturn(null)
+
+ assertResponseIsFailure(endpoint.getRecommendedStories())
+ }
+
+ @Test
+ fun `GIVEN an api request for stories WHEN getting a response THEN parse map it through PocketJSONParser`() {
+ arrayOf(
+ "",
+ " ",
+ "{}",
+ """{"expectedJSON": 101}""",
+ ).forEach { expected ->
+ whenever(raw.getRecommendedStories()).thenReturn(expected)
+
+ endpoint.getRecommendedStories()
+
+ verify(jsonParser, times(1)).jsonToPocketApiStories(expected)
+ }
+ }
+
+ @Test
+ fun `GIVEN an api request for stories WHEN getting a valid response THEN a success with the data is returned`() {
+ val expected = PocketTestResources.apiExpectedPocketStoriesRecommendations
+ whenever(raw.getRecommendedStories()).thenReturn("")
+ whenever(jsonParser.jsonToPocketApiStories(any())).thenReturn(expected)
+
+ val actual = endpoint.getRecommendedStories()
+
+ assertEquals(expected, (actual as? PocketResponse.Success)?.data)
+ }
+
+ @Test
+ fun `WHEN newInstance is called THEN a new PocketEndpoint is returned as a wrapper over a configured PocketEndpointRaw`() {
+ val result = PocketEndpoint.newInstance(client)
+
+ assertSame(client, result.rawEndpoint.client)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketJSONParserTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketJSONParserTest.kt
new file mode 100644
index 0000000000..8deeee794f
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketJSONParserTest.kt
@@ -0,0 +1,182 @@
+/* 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.service.pocket.stories.api
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.stories.api.PocketJSONParser.Companion.KEY_ARRAY_ITEMS
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class PocketJSONParserTest {
+
+ private lateinit var parser: PocketJSONParser
+
+ @Before
+ fun setUp() {
+ parser = PocketJSONParser()
+ }
+
+ @Test
+ fun `GIVEN a PocketJSONParser THEN its visibility is internal`() {
+ assertClassVisibility(PocketJSONParser::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN PocketJSONParser WHEN parsing valid stories recommendations THEN PocketApiStories are returned`() {
+ val expectedStories = PocketTestResources.apiExpectedPocketStoriesRecommendations
+ val pocketJSON = PocketTestResources.pocketEndpointFiveStoriesResponse
+ val actualStories = parser.jsonToPocketApiStories(pocketJSON)
+
+ assertNotNull(actualStories)
+ assertEquals(5, actualStories!!.size)
+ assertEquals(expectedStories, actualStories)
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations with missing titles THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointFiveStoriesResponse
+ val expectedStoriesIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketStoriesRecommendations)
+ .apply { removeAt(4) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("title", 4, pocketJSON)
+
+ val result = parser.jsonToPocketApiStories(pocketJsonWithMissingTitle)
+
+ assertEquals(4, result!!.size)
+ assertEquals(expectedStoriesIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations with a null title value THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointNullTitleStoryBadResponse
+ val result = parser.jsonToPocketApiStories(pocketJSON)
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations with missing urls THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointFiveStoriesResponse
+ val expectedStoriesIfMissingUrl = ArrayList(PocketTestResources.apiExpectedPocketStoriesRecommendations)
+ .apply { removeAt(3) }
+ val pocketJsonWithMissingUrl = removeJsonFieldFromArrayIndex("url", 3, pocketJSON)
+
+ val result = parser.jsonToPocketApiStories(pocketJsonWithMissingUrl)
+
+ assertEquals(4, result!!.size)
+ assertEquals(expectedStoriesIfMissingUrl.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations with a null url THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointNullUrlStoryBadResponse
+ val result = parser.jsonToPocketApiStories(pocketJSON)
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations with missing imageUrls THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointFiveStoriesResponse
+ val expectedStoriesIfMissingImageUrl = ArrayList(PocketTestResources.apiExpectedPocketStoriesRecommendations)
+ .apply { removeAt(2) }
+ val pocketJsonWithMissingImageUrl = removeJsonFieldFromArrayIndex("imageUrl", 2, pocketJSON)
+
+ val result = parser.jsonToPocketApiStories(pocketJsonWithMissingImageUrl)
+
+ assertEquals(4, result!!.size)
+ assertEquals(expectedStoriesIfMissingImageUrl.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing story recommendations with a null imageUrl THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointNullImageUrlStoryBadResponse
+ val result = parser.jsonToPocketApiStories(pocketJSON)
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations with missing publishers THEN those entries are kept but with default values`() {
+ val pocketJSON = PocketTestResources.pocketEndpointFiveStoriesResponse
+ val expectedStoriesIfMissingPublishers = PocketTestResources.apiExpectedPocketStoriesRecommendations
+ .mapIndexed { index, story ->
+ if (index == 2) {
+ story.copy(publisher = STRING_NOT_FOUND_DEFAULT_VALUE)
+ } else {
+ story
+ }
+ }
+ val pocketJsonWithMissingPublisher = removeJsonFieldFromArrayIndex("publisher", 2, pocketJSON)
+
+ val result = parser.jsonToPocketApiStories(pocketJsonWithMissingPublisher)
+
+ assertEquals(5, result!!.size)
+ assertEquals(expectedStoriesIfMissingPublishers.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations with missing categories THEN those entries are kept but with default values`() {
+ val pocketJSON = PocketTestResources.pocketEndpointFiveStoriesResponse
+ val expectedStoriesIfMissingCategories = PocketTestResources.apiExpectedPocketStoriesRecommendations
+ .mapIndexed { index, story ->
+ if (index == 3) {
+ story.copy(category = STRING_NOT_FOUND_DEFAULT_VALUE)
+ } else {
+ story
+ }
+ }
+ val pocketJsonWithMissingCategories = removeJsonFieldFromArrayIndex("category", 3, pocketJSON)
+
+ val result = parser.jsonToPocketApiStories(pocketJsonWithMissingCategories)
+
+ assertEquals(5, result!!.size)
+ assertEquals(expectedStoriesIfMissingCategories.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations with missing timeToRead THEN those entries are kept but with default values`() {
+ val pocketJSON = PocketTestResources.pocketEndpointFiveStoriesResponse
+ val expectedStoriesIfMissingTimeToRead = PocketTestResources.apiExpectedPocketStoriesRecommendations
+ .mapIndexed { index, story ->
+ if (index == 4) {
+ story.copy(timeToRead = INT_NOT_FOUND_DEFAULT_VALUE)
+ } else {
+ story
+ }
+ }
+ val pocketJsonWithMissingTimeToRead = removeJsonFieldFromArrayIndex("timeToRead", 4, pocketJSON)
+
+ val result = parser.jsonToPocketApiStories(pocketJsonWithMissingTimeToRead)
+
+ assertEquals(5, result!!.size)
+ assertEquals(expectedStoriesIfMissingTimeToRead.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations for an empty string THEN null is returned`() {
+ assertNull(parser.jsonToPocketApiStories(""))
+ }
+
+ @Test
+ fun `WHEN parsing stories recommendations for an invalid JSON String THEN null is returned`() {
+ assertNull(parser.jsonToPocketApiStories("{!!}}"))
+ }
+}
+
+private fun removeJsonFieldFromArrayIndex(fieldName: String, indexInArray: Int, json: String): String {
+ val obj = JSONObject(json)
+ val storiesJson = obj.getJSONArray(KEY_ARRAY_ITEMS)
+ storiesJson.getJSONObject(indexInArray).remove(fieldName)
+ return obj.toString()
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketResponseTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketResponseTest.kt
new file mode 100644
index 0000000000..3a77bbec7f
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketResponseTest.kt
@@ -0,0 +1,56 @@
+/* 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.service.pocket.stories.api
+
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class PocketResponseTest {
+ @Test
+ fun `GIVEN a null argument WHEN wrap is called THEN a Failure is returned`() {
+ assertTrue(PocketResponse.wrap(null) is PocketResponse.Failure)
+ }
+
+ @Test
+ fun `GIVEN an empty Collection argument WHEN wrap is called THEN a Failure is returned`() {
+ assertTrue(PocketResponse.wrap(emptyList<Any>()) is PocketResponse.Failure<*>)
+ }
+
+ @Test
+ fun `GIVEN a not empty Collection argument WHEN wrap is called THEN a Success wrapping that argument is returned`() {
+ val argument = listOf(1)
+
+ val result = PocketResponse.wrap(argument)
+
+ assertTrue(result is PocketResponse.Success)
+ assertSame(argument, (result as PocketResponse.Success).data)
+ }
+
+ @Test
+ fun `GIVEN an empty String argument WHEN wrap is called THEN a Failure is returned`() {
+ assertTrue(PocketResponse.wrap("") is PocketResponse.Failure<String>)
+ }
+
+ @Test
+ fun `GIVEN a not empty String argument WHEN wrap is called THEN a Success wrapping that argument is returned`() {
+ val argument = "not empty"
+
+ val result = PocketResponse.wrap(argument)
+
+ assertTrue(result is PocketResponse.Success)
+ assertSame(argument, (result as PocketResponse.Success).data)
+ }
+
+ @Test
+ fun `GIVEN a random argument WHEN wrap is called THEN a Success wrapping that argument is returned`() {
+ val argument = 42
+
+ val result = PocketResponse.wrap(argument)
+
+ assertTrue(result is PocketResponse.Success)
+ assertSame(argument, (result as PocketResponse.Success).data)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDaoTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDaoTest.kt
new file mode 100644
index 0000000000..e9f2b8208d
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDaoTest.kt
@@ -0,0 +1,387 @@
+/* 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.service.pocket.stories.db
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class PocketRecommendationsDaoTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+ private lateinit var database: PocketRecommendationsDatabase
+ private lateinit var dao: PocketRecommendationsDao
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+ database = Room
+ .inMemoryDatabaseBuilder(context, PocketRecommendationsDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+ dao = database.pocketRecommendationsDao()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+
+ @Test
+ fun `GIVEN an empty table WHEN a story is inserted and then queried THEN return the same story`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketStory
+
+ dao.insertPocketStories(listOf(story))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with identical url is tried to be inserted THEN add that to the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketStory
+ val newStory = story.copy(
+ url = "updated" + story.url,
+ )
+ dao.insertPocketStories(listOf(story))
+
+ dao.insertPocketStories(listOf(newStory))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story, newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story with the same url exists WHEN another story with updated title is tried to be inserted THEN don't update the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketStory
+ val updatedStory = story.copy(
+ title = "updated" + story.title,
+ )
+ dao.insertPocketStories(listOf(story))
+
+ dao.insertPocketStories(listOf(updatedStory))
+ val result = dao.getPocketStories()
+
+ assertTrue(result.size == 1)
+ assertEquals(story, result[0])
+ }
+
+ @Test
+ fun `GIVEN a story with the same url exists WHEN another story with updated imageUrl is tried to be inserted THEN don't update the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketStory
+ val updatedStory = story.copy(
+ imageUrl = "updated" + story.imageUrl,
+ )
+ dao.insertPocketStories(listOf(story))
+
+ dao.insertPocketStories(listOf(updatedStory))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story), result)
+ }
+
+ @Test
+ fun `GIVEN a story with the same url exists WHEN another story with updated publisher is tried to be inserted THEN don't update the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketStory
+ val updatedStory = story.copy(
+ publisher = "updated" + story.publisher,
+ )
+ dao.insertPocketStories(listOf(story))
+
+ dao.insertPocketStories(listOf(updatedStory))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story), result)
+ }
+
+ @Test
+ fun `GIVEN a story with the same url exists WHEN another story with updated category is tried to be inserted THEN don't update the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketStory
+ val updatedStory = story.copy(
+ category = "updated" + story.category,
+ )
+ dao.insertPocketStories(listOf(story))
+
+ dao.insertPocketStories(listOf(updatedStory))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story), result)
+ }
+
+ @Test
+ fun `GIVEN a story with the same url exists WHEN another story with updated timeToRead is tried to be inserted THEN don't update the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketStory
+ val updatedStory = story.copy(
+ timesShown = story.timesShown * 2,
+ )
+ dao.insertPocketStories(listOf(story))
+
+ dao.insertPocketStories(listOf(updatedStory))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story), result)
+ }
+
+ @Test
+ fun `GIVEN a story with the same url exists WHEN another story with updated timesShown is tried to be inserted THEN don't update the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketStory
+ val updatedStory = story.copy(
+ timesShown = story.timesShown * 2,
+ )
+ dao.insertPocketStories(listOf(story))
+
+ dao.insertPocketStories(listOf(updatedStory))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to delete some THEN remove them from the table`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val story3 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "3")
+ val story4 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "4")
+ dao.insertPocketStories(listOf(story1, story2, story3, story4))
+
+ dao.delete(listOf(story2, story4))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, story3), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to delete one not present in the table THEN don't update the table`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val story3 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "3")
+ val story4 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "4")
+ dao.insertPocketStories(listOf(story1, story2, story3))
+
+ dao.delete(listOf(story4))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, story2, story3), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to update timesShown for one THEN update only that story`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(
+ url = story1.url + "2",
+ timesShown = story1.timesShown * 2,
+ )
+ val story3 = PocketTestResources.dbExpectedPocketStory.copy(
+ url = story1.url + "3",
+ timesShown = story1.timesShown * 3,
+ )
+ val story4 = PocketTestResources.dbExpectedPocketStory.copy(
+ url = story1.url + "4",
+ timesShown = story1.timesShown * 4,
+ )
+ val updatedStory2 = PocketLocalStoryTimesShown(story2.url, 222)
+ val updatedStory4 = PocketLocalStoryTimesShown(story4.url, 444)
+ dao.insertPocketStories(listOf(story1, story2, story3, story4))
+
+ dao.updateTimesShown(listOf(updatedStory2, updatedStory4))
+ val result = dao.getPocketStories()
+
+ assertEquals(
+ listOf(
+ story1,
+ story2.copy(timesShown = 222),
+ story3,
+ story4.copy(timesShown = 444),
+ ),
+ result,
+ )
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to update timesShown for one not present in the table THEN don't update the table`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(
+ url = story1.url + "2",
+ timesShown = story1.timesShown * 2,
+ )
+ val story3 = PocketTestResources.dbExpectedPocketStory.copy(
+ url = story1.url + "3",
+ timesShown = story1.timesShown * 3,
+ )
+ val story4 = PocketTestResources.dbExpectedPocketStory.copy(
+ url = story1.url + "4",
+ timesShown = story1.timesShown * 4,
+ )
+ val otherStoryUpdateDetails = PocketLocalStoryTimesShown("differentUrl", 111)
+ dao.insertPocketStories(listOf(story1, story2, story3, story4))
+
+ dao.updateTimesShown(listOf(otherStoryUpdateDetails))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, story2, story3, story4), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN remove from table all stories not found in the new list`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val story3 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "3")
+ val story4 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "4")
+ dao.insertPocketStories(listOf(story1, story2, story3, story4))
+
+ dao.cleanOldAndInsertNewPocketStories(listOf(story2, story4))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story2, story4), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new urls`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val updatedStory1 = story1.copy(
+ url = "updated" + story1.url,
+ )
+ dao.insertPocketStories(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewPocketStories(listOf(updatedStory1, story2))
+ val result = dao.getPocketStories()
+
+ // Order gets reversed because the original story is replaced and another one is added.
+ assertEquals(listOf(story2, updatedStory1), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new image urls`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val updatedStory2 = story2.copy(
+ imageUrl = "updated" + story2.url,
+ )
+ dao.insertPocketStories(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewPocketStories(listOf(story1, updatedStory2))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, updatedStory2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only title changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val updatedStory1 = story1.copy(
+ title = "updated" + story1.title,
+ )
+ dao.insertPocketStories(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewPocketStories(listOf(updatedStory1, story2))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only publisher changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val updatedStory2 = story2.copy(
+ publisher = "updated" + story2.publisher,
+ )
+ dao.insertPocketStories(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewPocketStories(listOf(story1, updatedStory2))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only category changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val updatedStory1 = story1.copy(
+ category = "updated" + story1.category,
+ )
+ dao.insertPocketStories(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewPocketStories(listOf(updatedStory1, story2))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only timeToRead changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val updatedStory1 = story1.copy(
+ timeToRead = story1.timeToRead * 2,
+ )
+ dao.insertPocketStories(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewPocketStories(listOf(updatedStory1, story2))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only timesShown changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketStory
+ val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
+ val updatedStory2 = story2.copy(
+ timesShown = story2.timesShown * 2,
+ )
+ dao.insertPocketStories(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewPocketStories(listOf(story1, updatedStory2))
+ val result = dao.getPocketStories()
+
+ assertEquals(listOf(story1, story2), result)
+ }
+
+ /**
+ * Sets an executor to be used for database transactions.
+ * Needs to be used along with "runTest" to ensure waiting for transactions to finish but not hang tests.
+ */
+ private fun setupDatabseForTransactions() {
+ database = Room
+ .inMemoryDatabaseBuilder(context, PocketRecommendationsDatabase::class.java)
+ .setTransactionExecutor(executor)
+ .allowMainThreadQueries()
+ .build()
+ dao = database.pocketRecommendationsDao()
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketStoryEntityTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketStoryEntityTest.kt
new file mode 100644
index 0000000000..66c0f66c30
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketStoryEntityTest.kt
@@ -0,0 +1,23 @@
+/* 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.service.pocket.stories.db
+
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import org.junit.Test
+import kotlin.reflect.KVisibility
+
+class PocketStoryEntityTest {
+ // This is the data type persisted locally. No need to be public
+ @Test
+ fun `GIVEN a PocketLocalStory THEN its visibility is internal`() {
+ assertClassVisibility(PocketStoryEntity::class, KVisibility.INTERNAL)
+ }
+
+ // This is a data type only used in local updates. No need to be public
+ @Test
+ fun `GIVEN a PocketLocalStoryTimesShown THEN its visibility is internal`() {
+ assertClassVisibility(PocketLocalStoryTimesShown::class, KVisibility.INTERNAL)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorkerTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorkerTest.kt
new file mode 100644
index 0000000000..ab66365577
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorkerTest.kt
@@ -0,0 +1,65 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker.Result
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.service.pocket.GlobalDependencyProvider
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.spocs.SpocsUseCases.DeleteProfile
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import kotlin.reflect.KVisibility.INTERNAL
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class DeleteSpocsProfileWorkerTest {
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN a DeleteSpocsProfileWorker THEN its visibility is internal`() {
+ assertClassVisibility(RefreshSpocsWorker::class, INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a DeleteSpocsProfileWorker WHEN profile deletion is successful THEN return success`() = runTestOnMain {
+ val useCases: SpocsUseCases = mock()
+ val deleteProfileUseCase: DeleteProfile = mock()
+ doReturn(true).`when`(deleteProfileUseCase).invoke()
+ doReturn(deleteProfileUseCase).`when`(useCases).deleteProfile
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder<DeleteSpocsProfileWorker>(testContext).build()
+
+ val result = worker.startWork().await()
+
+ assertEquals(Result.success(), result)
+ }
+
+ @Test
+ fun `GIVEN a DeleteSpocsProfileWorker WHEN profile deletion fails THEN work should be retried`() = runTestOnMain {
+ val useCases: SpocsUseCases = mock()
+ val deleteProfileUseCase: DeleteProfile = mock()
+ doReturn(false).`when`(deleteProfileUseCase).invoke()
+ doReturn(deleteProfileUseCase).`when`(useCases).deleteProfile
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder<DeleteSpocsProfileWorker>(testContext).build()
+
+ val result = worker.startWork().await()
+
+ assertEquals(Result.retry(), result)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/PocketStoriesRefreshSchedulerTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/PocketStoriesRefreshSchedulerTest.kt
new file mode 100644
index 0000000000..121b8a5145
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/PocketStoriesRefreshSchedulerTest.kt
@@ -0,0 +1,103 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.pocket.PocketStoriesConfig
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.update.RefreshPocketWorker.Companion.REFRESH_WORK_TAG
+import mozilla.components.support.base.worker.Frequency
+import mozilla.components.support.test.any
+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
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.concurrent.TimeUnit
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class PocketStoriesRefreshSchedulerTest {
+ @Test
+ fun `GIVEN a PocketStoriesRefreshScheduler THEN its visibility is internal`() {
+ assertClassVisibility(PocketStoriesRefreshScheduler::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a PocketStoriesRefreshScheduler WHEN schedulePeriodicRefreshes THEN a RefreshPocketWorker is created and enqueued`() {
+ val client: HttpURLConnectionClient = mock()
+ val scheduler = spy(
+ PocketStoriesRefreshScheduler(
+ PocketStoriesConfig(
+ client,
+ Frequency(1, TimeUnit.HOURS),
+ ),
+ ),
+ )
+ val workManager = mock<WorkManager>()
+ val worker = mock<PeriodicWorkRequest>()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+ doReturn(worker).`when`(scheduler).createPeriodicWorkerRequest(any())
+
+ scheduler.schedulePeriodicRefreshes(testContext)
+
+ verify(workManager).enqueueUniquePeriodicWork(REFRESH_WORK_TAG, ExistingPeriodicWorkPolicy.KEEP, worker)
+ }
+
+ @Test
+ fun `GIVEN a PocketStoriesRefreshScheduler WHEN stopPeriodicRefreshes THEN it should cancel all unfinished work`() {
+ val scheduler = spy(PocketStoriesRefreshScheduler(mock()))
+ val workManager = mock<WorkManager>()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+
+ scheduler.stopPeriodicRefreshes(testContext)
+
+ verify(workManager).cancelAllWorkByTag(REFRESH_WORK_TAG)
+ verify(workManager, Mockito.never()).cancelAllWork()
+ }
+
+ @Test
+ fun `GIVEN a PocketStoriesRefreshScheduler WHEN createPeriodicWorkerRequest THEN a properly configured PeriodicWorkRequest is returned`() {
+ val scheduler = spy(PocketStoriesRefreshScheduler(mock()))
+
+ val result = scheduler.createPeriodicWorkerRequest(
+ Frequency(1, TimeUnit.HOURS),
+ )
+
+ verify(scheduler).getWorkerConstrains()
+ assertTrue(result.workSpec.intervalDuration == TimeUnit.HOURS.toMillis(1))
+ assertFalse(result.workSpec.constraints.requiresBatteryNotLow())
+ assertFalse(result.workSpec.constraints.requiresCharging())
+ assertFalse(result.workSpec.constraints.hasContentUriTriggers())
+ assertFalse(result.workSpec.constraints.requiresStorageNotLow())
+ assertFalse(result.workSpec.constraints.requiresDeviceIdle())
+ assertTrue(result.workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED)
+ assertTrue(result.tags.contains(REFRESH_WORK_TAG))
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesRefreshScheduler THEN Worker constraints should be to have Internet`() {
+ val scheduler = PocketStoriesRefreshScheduler(mock())
+
+ val result = scheduler.getWorkerConstrains()
+
+ assertFalse(result.requiresBatteryNotLow())
+ assertFalse(result.requiresCharging())
+ assertFalse(result.hasContentUriTriggers())
+ assertFalse(result.requiresStorageNotLow())
+ assertFalse(result.requiresDeviceIdle())
+ assertTrue(result.requiredNetworkType == NetworkType.CONNECTED)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshPocketWorkerTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshPocketWorkerTest.kt
new file mode 100644
index 0000000000..3691fb76a5
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshPocketWorkerTest.kt
@@ -0,0 +1,64 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.service.pocket.GlobalDependencyProvider
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases.RefreshPocketStories
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import kotlin.reflect.KVisibility
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class RefreshPocketWorkerTest {
+
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN a RefreshPocketWorker THEN its visibility is internal`() {
+ assertClassVisibility(RefreshPocketWorker::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a RefreshPocketWorker WHEN stories are refreshed successfully THEN return success`() = runTestOnMain {
+ val useCases: PocketStoriesUseCases = mock()
+ val refreshStoriesUseCase: RefreshPocketStories = mock()
+ doReturn(true).`when`(refreshStoriesUseCase).invoke()
+ doReturn(refreshStoriesUseCase).`when`(useCases).refreshStories
+ GlobalDependencyProvider.RecommendedStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder<RefreshPocketWorker>(testContext).build()
+
+ val result = worker.startWork().await()
+ assertEquals(ListenableWorker.Result.success(), result)
+ }
+
+ @Test
+ fun `GIVEN a RefreshPocketWorker WHEN stories are could not be refreshed THEN work should be retried`() = runTestOnMain {
+ val useCases: PocketStoriesUseCases = mock()
+ val refreshStoriesUseCase: RefreshPocketStories = mock()
+ doReturn(false).`when`(refreshStoriesUseCase).invoke()
+ doReturn(refreshStoriesUseCase).`when`(useCases).refreshStories
+ GlobalDependencyProvider.RecommendedStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder<RefreshPocketWorker>(testContext).build()
+
+ val result = worker.startWork().await()
+ assertEquals(ListenableWorker.Result.retry(), result)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshSpocsWorkerTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshSpocsWorkerTest.kt
new file mode 100644
index 0000000000..89ea044add
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshSpocsWorkerTest.kt
@@ -0,0 +1,64 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.service.pocket.GlobalDependencyProvider
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.spocs.SpocsUseCases.RefreshSponsoredStories
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import kotlin.reflect.KVisibility.INTERNAL
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class RefreshSpocsWorkerTest {
+
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN a RefreshSpocsWorker THEN its visibility is internal`() {
+ assertClassVisibility(RefreshSpocsWorker::class, INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a RefreshSpocsWorker WHEN stories are refreshed successfully THEN return success`() = runTestOnMain {
+ val useCases: SpocsUseCases = mock()
+ val refreshStoriesUseCase: RefreshSponsoredStories = mock()
+ doReturn(true).`when`(refreshStoriesUseCase).invoke()
+ doReturn(refreshStoriesUseCase).`when`(useCases).refreshStories
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder<RefreshSpocsWorker>(testContext).build()
+
+ val result = worker.startWork().await()
+ assertEquals(ListenableWorker.Result.success(), result)
+ }
+
+ @Test
+ fun `GIVEN a RefreshSpocsWorker WHEN stories are could not be refreshed THEN work should be retried`() = runTestOnMain {
+ val useCases: SpocsUseCases = mock()
+ val refreshStoriesUseCase: RefreshSponsoredStories = mock()
+ doReturn(false).`when`(refreshStoriesUseCase).invoke()
+ doReturn(refreshStoriesUseCase).`when`(useCases).refreshStories
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder<RefreshSpocsWorker>(testContext).build()
+
+ val result = worker.startWork().await()
+ assertEquals(ListenableWorker.Result.retry(), result)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/SpocsRefreshSchedulerTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/SpocsRefreshSchedulerTest.kt
new file mode 100644
index 0000000000..70194c2939
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/SpocsRefreshSchedulerTest.kt
@@ -0,0 +1,161 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.BackoffPolicy
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.pocket.PocketStoriesConfig
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.update.DeleteSpocsProfileWorker.Companion.DELETE_SPOCS_PROFILE_WORK_TAG
+import mozilla.components.service.pocket.update.RefreshSpocsWorker.Companion.REFRESH_SPOCS_WORK_TAG
+import mozilla.components.support.base.worker.Frequency
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.concurrent.TimeUnit
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class SpocsRefreshSchedulerTest {
+ @Test
+ fun `GIVEN a spocs refresh scheduler THEN its visibility is internal`() {
+ assertClassVisibility(SpocsRefreshScheduler::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN scheduling stories refresh THEN a RefreshPocketWorker is created and enqueued`() {
+ val client: HttpURLConnectionClient = mock()
+ val scheduler = spy(
+ SpocsRefreshScheduler(
+ PocketStoriesConfig(
+ client,
+ Frequency(1, TimeUnit.HOURS),
+ ),
+ ),
+ )
+ val workManager = mock<WorkManager>()
+ val worker = mock<PeriodicWorkRequest>()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+ doReturn(worker).`when`(scheduler).createPeriodicRefreshWorkerRequest(any())
+
+ scheduler.schedulePeriodicRefreshes(testContext)
+
+ verify(workManager).enqueueUniquePeriodicWork(REFRESH_SPOCS_WORK_TAG, ExistingPeriodicWorkPolicy.KEEP, worker)
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN stopping stories refresh THEN it should cancel all unfinished work`() {
+ val scheduler = spy(SpocsRefreshScheduler(mock()))
+ val workManager = mock<WorkManager>()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+
+ scheduler.stopPeriodicRefreshes(testContext)
+
+ verify(workManager).cancelAllWorkByTag(REFRESH_SPOCS_WORK_TAG)
+ verify(workManager, Mockito.never()).cancelAllWork()
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN scheduling profile deletion THEN a RefreshPocketWorker is created and enqueued`() {
+ val client: HttpURLConnectionClient = mock()
+ val scheduler = spy(
+ SpocsRefreshScheduler(
+ PocketStoriesConfig(
+ client,
+ Frequency(1, TimeUnit.HOURS),
+ ),
+ ),
+ )
+ val workManager = mock<WorkManager>()
+ val worker = mock<OneTimeWorkRequest>()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+ doReturn(worker).`when`(scheduler).createOneTimeProfileDeletionWorkerRequest()
+
+ scheduler.scheduleProfileDeletion(testContext)
+
+ verify(workManager).enqueueUniqueWork(DELETE_SPOCS_PROFILE_WORK_TAG, ExistingWorkPolicy.KEEP, worker)
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN cancelling profile deletion THEN it should cancel all unfinished work`() {
+ val scheduler = spy(SpocsRefreshScheduler(mock()))
+ val workManager = mock<WorkManager>()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+
+ scheduler.stopProfileDeletion(testContext)
+
+ verify(workManager).cancelAllWorkByTag(DELETE_SPOCS_PROFILE_WORK_TAG)
+ verify(workManager, never()).cancelAllWork()
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN creating a periodic worker THEN a properly configured PeriodicWorkRequest is returned`() {
+ val scheduler = spy(SpocsRefreshScheduler(mock()))
+
+ val result = scheduler.createPeriodicRefreshWorkerRequest(
+ Frequency(1, TimeUnit.HOURS),
+ )
+
+ verify(scheduler).getWorkerConstrains()
+ assertTrue(result.workSpec.intervalDuration == TimeUnit.HOURS.toMillis(1))
+ assertFalse(result.workSpec.constraints.requiresBatteryNotLow())
+ assertFalse(result.workSpec.constraints.requiresCharging())
+ assertFalse(result.workSpec.constraints.hasContentUriTriggers())
+ assertFalse(result.workSpec.constraints.requiresStorageNotLow())
+ assertFalse(result.workSpec.constraints.requiresDeviceIdle())
+ assertTrue(result.workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED)
+ assertTrue(result.tags.contains(REFRESH_SPOCS_WORK_TAG))
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN creating a one time worker THEN a properly configured OneTimeWorkRequest is returned`() {
+ val scheduler = spy(SpocsRefreshScheduler(mock()))
+
+ val result = scheduler.createOneTimeProfileDeletionWorkerRequest()
+
+ verify(scheduler).getWorkerConstrains()
+ assertEquals(0, result.workSpec.intervalDuration)
+ assertEquals(0, result.workSpec.initialDelay)
+ assertEquals(BackoffPolicy.EXPONENTIAL, result.workSpec.backoffPolicy)
+ assertFalse(result.workSpec.constraints.requiresBatteryNotLow())
+ assertFalse(result.workSpec.constraints.requiresCharging())
+ assertFalse(result.workSpec.constraints.hasContentUriTriggers())
+ assertFalse(result.workSpec.constraints.requiresStorageNotLow())
+ assertFalse(result.workSpec.constraints.requiresDeviceIdle())
+ assertTrue(result.workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED)
+ assertTrue(result.tags.contains(DELETE_SPOCS_PROFILE_WORK_TAG))
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler THEN Worker constraints should be to have Internet`() {
+ val scheduler = SpocsRefreshScheduler(mock())
+
+ val result = scheduler.getWorkerConstrains()
+
+ assertFalse(result.requiresBatteryNotLow())
+ assertFalse(result.requiresCharging())
+ assertFalse(result.hasContentUriTriggers())
+ assertFalse(result.requiresStorageNotLow())
+ assertFalse(result.requiresDeviceIdle())
+ assertTrue(result.requiredNetworkType == NetworkType.CONNECTED)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/pocket/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/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/service/pocket/src/test/resources/pocket/sponsored_stories_response.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/sponsored_stories_response.json
new file mode 100644
index 0000000000..45ca1e5b63
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/sponsored_stories_response.json
@@ -0,0 +1,98 @@
+{
+ "feature_flags": {
+ "spoc_v2": true,
+ "collections": false
+ },
+ "spocs": [
+ {
+ "id": 193815086,
+ "flight_id": 191739319,
+ "campaign_id": 1315172,
+ "title": "Eating Keto Has Never Been So Easy With Green Chef",
+ "url": "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
+ "domain": "journiest.com",
+ "excerpt": "Get Green Chef's Special Spring Offer: ${'$'}130 off plus free shipping.",
+ "priority": 3,
+ "raw_image_src": "https://s.zkcdn.net/Advertisers/a3644de3c18948ffbd9aa43e8f9c7bf0.png",
+ "image_src": "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
+ "shim": {
+ "click": "193815086ClickShim",
+ "impression": "193815086ImpressionShim",
+ "delete": "193815086DeleteShim",
+ "save": "193815086SaveShim"
+ },
+ "caps": {
+ "lifetime": 50,
+ "campaign": {
+ "count": 10,
+ "period": 86400
+ },
+ "flight": {
+ "count": 10,
+ "period": 86400
+ }
+ },
+ "sponsor": "Green Chef"
+ },
+ {
+ "id": 177986195,
+ "flight_id": 191739667,
+ "campaign_id": 63548984,
+ "title": "This Leading Cash Back Card Is a Slam Dunk if You Want a One-Card Wallet",
+ "url": "https://www.fool.com/the-ascent/credit-cards/landing/discover-it-cash-back-review-v2-csr/?utm_site=theascent&utm_campaign=ta-cc-co-pocket-discb-04012022-5-na-firefox&utm_medium=cpc&utm_source=pocket",
+ "domain": "fool.com",
+ "excerpt": "Make 2022 your year for a one-card wallet.",
+ "priority": 2,
+ "raw_image_src": "https://s.zkcdn.net/Advertisers/359f56a5423c4926ab3aa148e448d839.webp",
+ "image_src": "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/359f56a5423c4926ab3aa148e448d839.webp&resize=w618-h310",
+ "shim": {
+ "click": "177986195ClickShim",
+ "impression": "177986195ImpressionShim",
+ "delete": "177986195DeleteShim",
+ "save": "177986195SaveShim"
+ },
+ "caps": {
+ "lifetime": 50,
+ "campaign": {
+ "count": 10,
+ "period": 86400
+ },
+ "flight": {
+ "count": 10,
+ "period": 86400
+ }
+ },
+ "sponsor": "The Ascent"
+ },
+ {
+ "id": 192560056,
+ "flight_id": 189212196,
+ "campaign_id": 65544139,
+ "title": "The Incredible Lawn Hack That Can Make Your Neighbors Green With Envy Over Your Lawn",
+ "url": "https://go.lawnbuddy.org/zf/50/7673?campaign=SUN_Pocket2022&creative=SUN_LawnCompare4-TheIncredibleLawnHackThatCanMakeYourNeighborsGreenWithEnvyOverYourLawn-WithoutSpendingAFortuneOnNewGrassAndWithoutBreakingASweat-20220420",
+ "domain": "go.lawnbuddy.org",
+ "excerpt": "Without spending a fortune on new grass and without breaking a sweat.",
+ "priority": 1,
+ "raw_image_src": "https://s.zkcdn.net/Advertisers/ce16302e184342cda0619c08b7604c9c.jpg",
+ "image_src": "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/ce16302e184342cda0619c08b7604c9c.jpg&resize=w618-h310",
+ "shim": {
+ "click": "192560056ClickShim",
+ "impression": "192560056ImpressionShim",
+ "delete": "192560056DeleteShim",
+ "save": "192560056SaveShim"
+ },
+ "caps": {
+ "lifetime": 50,
+ "campaign": {
+ "count": 10,
+ "period": 86400
+ },
+ "flight": {
+ "count": 10,
+ "period": 86400
+ }
+ },
+ "sponsor": "Sunday"
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json
new file mode 100644
index 0000000000..da2b9a2953
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json
@@ -0,0 +1,44 @@
+{
+ "recommendations": [
+ {
+ "category": "general",
+ "url": "https://getpocket.com/explore/item/how-to-remember-anything-you-really-want-to-remember-backed-by-science",
+ "title": "How to Remember Anything You Really Want to Remember, Backed by Science",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fwww.incimages.com%252Fuploaded_files%252Fimage%252F1920x1080%252Fgetty-862457080_394628.jpg",
+ "publisher": "Pocket",
+ "timeToRead": 3
+ },
+ {
+ "category": "general",
+ "url": "https://www.thecut.com/article/i-dont-want-to-be-like-a-family-with-my-co-workers.html",
+ "title": "‘I Don’t Want to Be Like a Family With My Co-Workers’",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpyxis.nymag.com%2Fv1%2Fimgs%2Fac8%2Fd22%2F315cd0cf1e3a43edfe0e0548f2edbcb1a1-ask-a-boss.1x.rsocial.w1200.jpg",
+ "publisher": "The Cut",
+ "timeToRead": 5
+ },
+ {
+ "category": "general",
+ "url": "https://www.newyorker.com/news/q-and-a/how-america-failed-in-afghanistan",
+ "title": "How America Failed in Afghanistan",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fmedia.newyorker.com%2Fphotos%2F6119484157b611aec9c99b43%2F16%3A9%2Fw_1280%2Cc_limit%2FChotiner-Afghanistan01.jpg",
+ "publisher": "The New Yorker",
+ "timeToRead": 14
+ },
+ {
+ "category": "general",
+ "url": "https://www.technologyreview.com/2021/08/15/1031804/digital-beauty-filters-photoshop-photo-editing-colorism-racism/",
+ "title": "How digital beauty filters perpetuate colorism",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fwp.technologyreview.com%2Fwp-content%2Fuploads%2F2021%2F08%2FBeautyScoreColorism.jpg%3Fresize%3D1200%2C600",
+ "publisher": "MIT Technology Review",
+ "timeToRead": 11
+ },
+ {
+ "category": "general",
+ "url": "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
+ "title": "How to Get Rid of Black Mold Naturally",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F6757%252F1628024495_6109ae86db6cc.png",
+ "publisher": "Pocket",
+ "timeToRead": 4
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story.json
new file mode 100644
index 0000000000..a8b2d9bd70
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story.json
@@ -0,0 +1,8 @@
+{
+ "category": "career",
+ "url": "https://getpocket.com/explore/item/this-scheduling-strategy-can-save-you-hours-per-week",
+ "title": "This Scheduling Strategy Can Save You Hours Per Week",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F6668%252F1627343665_GettyImages-1189531274.jpg",
+ "publisher": "Pocket",
+ "timeToRead": 3
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_imageUrl_response.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_imageUrl_response.json
new file mode 100644
index 0000000000..3f05508681
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_imageUrl_response.json
@@ -0,0 +1,12 @@
+{
+ "recommendations": [
+ {
+ "category": "science",
+ "url": "https://getpocket.com/explore/item/you-think-you-know-what-blue-is-but-you-have-no-idea",
+ "title": "You Think You Know What Blue Is, But You Have No Idea",
+ "imageUrl": null,
+ "publisher": "Pocket",
+ "timeToRead": 3
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_title_response.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_title_response.json
new file mode 100644
index 0000000000..9ca1105afe
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_title_response.json
@@ -0,0 +1,12 @@
+{
+ "recommendations": [
+ {
+ "category": "science",
+ "url": "https://getpocket.com/explore/item/you-think-you-know-what-blue-is-but-you-have-no-idea",
+ "title": null,
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F3713%252F1584373694_GettyImages-83522858.jpg",
+ "publisher": "Pocket",
+ "timeToRead": 3
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_url_response.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_url_response.json
new file mode 100644
index 0000000000..39483424be
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_null_url_response.json
@@ -0,0 +1,12 @@
+{
+ "recommendations": [
+ {
+ "category": "science",
+ "url": null,
+ "title": "You Think You Know What Blue Is, But You Have No Idea",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F3713%252F1584373694_GettyImages-83522858.jpg",
+ "publisher": "Pocket",
+ "timeToRead": 3
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json
new file mode 100644
index 0000000000..8fa6e33ad7
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json
@@ -0,0 +1,12 @@
+{
+ "recommendations": [
+ {
+ "category": "science",
+ "url": "https://getpocket.com/explore/item/you-think-you-know-what-blue-is-but-you-have-no-idea",
+ "title": "You Think You Know What Blue Is, But You Have No Idea",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F3713%252F1584373694_GettyImages-83522858.jpg",
+ "publisher": "Pocket",
+ "timeToRead": 3
+ }
+ ]
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/pocket/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/sync-autofill/README.md b/mobile/android/android-components/components/service/sync-autofill/README.md
new file mode 100644
index 0000000000..b4c1a95a85
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Service > Firefox Sync - Autofill
+
+A library for autofilling addresses and credit cards based on `concept-storage` backed by [application-services' Autofill lib](https://github.com/mozilla/application-services).
+
+## Usage
+
+### 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:service-sync-autofill:{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/service/sync-autofill/build.gradle b/mobile/android/android-components/components/service/sync-autofill/build.gradle
new file mode 100644
index 0000000000..ecb7d7d8fb
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/build.gradle
@@ -0,0 +1,49 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.browser.storage.sync.autofill'
+}
+
+dependencies {
+ api ComponentsDependencies.mozilla_appservices_autofill
+
+ api project(':concept-storage')
+ api project(':concept-sync')
+ api project(':concept-base')
+ api project(':lib-dataprotect')
+
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.mozilla_appservices_full_megazord_forUnitTests
+}
+
+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/service/sync-autofill/proguard-rules.pro b/mobile/android/android-components/components/service/sync-autofill/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/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/service/sync-autofill/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/sync-autofill/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/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/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt
new file mode 100644
index 0000000000..c48559577d
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt
@@ -0,0 +1,209 @@
+/* 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.service.sync.autofill
+
+import android.content.Context
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.withContext
+import mozilla.appservices.autofill.AutofillApiException.NoSuchRecord
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.CreditCardsAddressesStorage
+import mozilla.components.concept.storage.NewCreditCardFields
+import mozilla.components.concept.storage.UpdatableAddressFields
+import mozilla.components.concept.storage.UpdatableCreditCardFields
+import mozilla.components.concept.sync.SyncableStore
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.logElapsedTime
+import java.io.Closeable
+import mozilla.appservices.autofill.Store as RustAutofillStorage
+
+const val AUTOFILL_DB_NAME = "autofill.sqlite"
+
+/**
+ * An implementation of [CreditCardsAddressesStorage] backed by the application-services' `autofill`
+ * library.
+ *
+ * @param context A [Context] used for disk access.
+ * @param securePrefs A [SecureAbove22Preferences] wrapped in [Lazy] to avoid eager instantiation.
+ * Used for storing encryption key material.
+ */
+class AutofillCreditCardsAddressesStorage(
+ context: Context,
+ securePrefs: Lazy<SecureAbove22Preferences>,
+) : CreditCardsAddressesStorage, SyncableStore, AutoCloseable {
+ private val logger = Logger("AutofillCCAddressesStorage")
+
+ private val coroutineContext by lazy { Dispatchers.IO }
+
+ val crypto by lazy { AutofillCrypto(context, securePrefs.value, this) }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val conn by lazy {
+ AutofillStorageConnection.init(dbPath = context.getDatabasePath(AUTOFILL_DB_NAME).absolutePath)
+ AutofillStorageConnection
+ }
+
+ /**
+ * "Warms up" this storage layer by establishing the database connection.
+ */
+ suspend fun warmUp() = withContext(coroutineContext) {
+ logElapsedTime(logger, "Warming up storage") { conn }
+ Unit
+ }
+
+ override suspend fun addCreditCard(
+ creditCardFields: NewCreditCardFields,
+ ): CreditCard = withContext(coroutineContext) {
+ val key = crypto.getOrGenerateKey()
+
+ // Assume our key is good, and that this operation shouldn't fail.
+ val encryptedCardNumber = crypto.encrypt(key, creditCardFields.plaintextCardNumber)!!
+ val updatableCreditCardFields = UpdatableCreditCardFields(
+ billingName = creditCardFields.billingName,
+ cardNumber = encryptedCardNumber,
+ cardNumberLast4 = creditCardFields.cardNumberLast4,
+ expiryMonth = creditCardFields.expiryMonth,
+ expiryYear = creditCardFields.expiryYear,
+ cardType = creditCardFields.cardType,
+ )
+
+ conn.getStorage().addCreditCard(updatableCreditCardFields.into()).into()
+ }
+
+ override suspend fun updateCreditCard(
+ guid: String,
+ creditCardFields: UpdatableCreditCardFields,
+ ) = withContext(coroutineContext) {
+ val updatableCreditCardFields = when (creditCardFields.cardNumber) {
+ // If credit card number changed, we need to encrypt it.
+ is CreditCardNumber.Plaintext -> {
+ val key = crypto.getOrGenerateKey()
+ // Assume our key is good, and that this operation shouldn't fail.
+ val encryptedCardNumber = crypto.encrypt(
+ key,
+ creditCardFields.cardNumber as CreditCardNumber.Plaintext,
+ )!!
+ UpdatableCreditCardFields(
+ billingName = creditCardFields.billingName,
+ cardNumber = encryptedCardNumber,
+ cardNumberLast4 = creditCardFields.cardNumberLast4,
+ expiryMonth = creditCardFields.expiryMonth,
+ expiryYear = creditCardFields.expiryYear,
+ cardType = creditCardFields.cardType,
+ )
+ }
+ // If card number didn't change, we're just round-tripping an existing encrypted version.
+ is CreditCardNumber.Encrypted -> {
+ UpdatableCreditCardFields(
+ billingName = creditCardFields.billingName,
+ cardNumber = creditCardFields.cardNumber,
+ cardNumberLast4 = creditCardFields.cardNumberLast4,
+ expiryMonth = creditCardFields.expiryMonth,
+ expiryYear = creditCardFields.expiryYear,
+ cardType = creditCardFields.cardType,
+ )
+ }
+ }
+ conn.getStorage().updateCreditCard(guid, updatableCreditCardFields.into())
+ }
+
+ override suspend fun getCreditCard(guid: String): CreditCard? = withContext(coroutineContext) {
+ try {
+ conn.getStorage().getCreditCard(guid).into()
+ } catch (e: NoSuchRecord) {
+ null
+ }
+ }
+
+ override suspend fun getAllCreditCards(): List<CreditCard> = withContext(coroutineContext) {
+ conn.getStorage().getAllCreditCards().map { it.into() }
+ }
+
+ override suspend fun deleteCreditCard(guid: String): Boolean = withContext(coroutineContext) {
+ conn.getStorage().deleteCreditCard(guid)
+ }
+
+ override suspend fun touchCreditCard(guid: String) = withContext(coroutineContext) {
+ conn.getStorage().touchCreditCard(guid)
+ }
+
+ override suspend fun addAddress(addressFields: UpdatableAddressFields): Address =
+ withContext(coroutineContext) {
+ conn.getStorage().addAddress(addressFields.into()).into()
+ }
+
+ override suspend fun getAddress(guid: String): Address? = withContext(coroutineContext) {
+ try {
+ conn.getStorage().getAddress(guid).into()
+ } catch (e: NoSuchRecord) {
+ null
+ }
+ }
+
+ override suspend fun getAllAddresses(): List<Address> = withContext(coroutineContext) {
+ conn.getStorage().getAllAddresses().map { it.into() }
+ }
+
+ override suspend fun updateAddress(guid: String, address: UpdatableAddressFields) =
+ withContext(coroutineContext) {
+ conn.getStorage().updateAddress(guid, address.into())
+ }
+
+ override suspend fun deleteAddress(guid: String): Boolean = withContext(coroutineContext) {
+ conn.getStorage().deleteAddress(guid)
+ }
+
+ override suspend fun touchAddress(guid: String) = withContext(coroutineContext) {
+ conn.getStorage().touchAddress(guid)
+ }
+
+ override fun getCreditCardCrypto(): AutofillCrypto {
+ return crypto
+ }
+
+ override suspend fun scrubEncryptedData() = withContext(coroutineContext) {
+ conn.getStorage().scrubEncryptedData()
+ }
+
+ override fun registerWithSyncManager() {
+ conn.getStorage().registerWithSyncManager()
+ }
+
+ override fun close() {
+ coroutineContext.cancel()
+ conn.close()
+ }
+}
+
+/**
+ * A singleton wrapping a [RustAutofillStorage] connection.
+ */
+internal object AutofillStorageConnection : Closeable {
+ @GuardedBy("this")
+ private var storage: RustAutofillStorage? = null
+
+ internal fun init(dbPath: String = AUTOFILL_DB_NAME) = synchronized(this) {
+ if (storage == null) {
+ storage = RustAutofillStorage(dbPath)
+ }
+ }
+
+ internal fun getStorage(): RustAutofillStorage = synchronized(this) {
+ check(storage != null) { "must call init first" }
+ return storage!!
+ }
+
+ override fun close() = synchronized(this) {
+ check(storage != null) { "must call init first" }
+ storage!!.destroy()
+ storage = null
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCrypto.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCrypto.kt
new file mode 100644
index 0000000000..9846280958
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCrypto.kt
@@ -0,0 +1,111 @@
+/* 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.service.sync.autofill
+
+import android.content.Context
+import android.content.SharedPreferences
+import mozilla.appservices.autofill.AutofillApiException
+import mozilla.appservices.autofill.decryptString
+import mozilla.appservices.autofill.encryptString
+import mozilla.components.concept.storage.CreditCardCrypto
+import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.KeyGenerationReason
+import mozilla.components.concept.storage.KeyManager
+import mozilla.components.concept.storage.ManagedKey
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A class that knows how to encrypt & decrypt strings, backed by application-services' autofill lib.
+ * Used for protecting credit card numbers at rest.
+ *
+ * This class manages creation and storage of the encryption key.
+ * It also keeps track of abnormal events, such as managed key going missing or getting corrupted.
+ *
+ * @param context [Context] used for obtaining [SharedPreferences] for managing internal prefs.
+ * @param securePrefs A [SecureAbove22Preferences] instance used for storing the managed key.
+ */
+class AutofillCrypto(
+ private val context: Context,
+ private val securePrefs: SecureAbove22Preferences,
+ private val storage: AutofillCreditCardsAddressesStorage,
+) : CreditCardCrypto, KeyManager() {
+ private val logger = Logger("AutofillCrypto")
+ private val plaintextPrefs by lazy { context.getSharedPreferences(AUTOFILL_PREFS, Context.MODE_PRIVATE) }
+
+ override fun encrypt(
+ key: ManagedKey,
+ plaintextCardNumber: CreditCardNumber.Plaintext,
+ ): CreditCardNumber.Encrypted? {
+ return try {
+ CreditCardNumber.Encrypted(encryptString(key.key, plaintextCardNumber.number))
+ } catch (e: AutofillApiException) {
+ logger.warn("Failed to encrypt", e)
+ null
+ }
+ }
+
+ override fun decrypt(
+ key: ManagedKey,
+ encryptedCardNumber: CreditCardNumber.Encrypted,
+ ): CreditCardNumber.Plaintext? {
+ if (encryptedCardNumber.number.isEmpty()) {
+ logger.info("Skipping decryption of previously scrubbed CC number")
+ return null
+ }
+ return try {
+ CreditCardNumber.Plaintext(decryptString(key.key, encryptedCardNumber.number))
+ } catch (e: AutofillApiException) {
+ logger.warn("Failed to decrypt", e)
+ null
+ }
+ }
+
+ override fun createKey() = mozilla.appservices.autofill.createKey()
+
+ override fun isKeyRecoveryNeeded(rawKey: String, canary: String): KeyGenerationReason.RecoveryNeeded? {
+ return try {
+ if (CANARY_PHRASE_PLAINTEXT == decryptString(rawKey, canary)) {
+ null
+ } else {
+ KeyGenerationReason.RecoveryNeeded.Corrupt
+ }
+ } catch (e: AutofillApiException) {
+ KeyGenerationReason.RecoveryNeeded.Corrupt
+ }
+ }
+
+ override fun getStoredCanary(): String? {
+ return plaintextPrefs.getString(CANARY_PHRASE_CIPHERTEXT_KEY, null)
+ }
+
+ override fun getStoredKey(): String? {
+ return securePrefs.getString(AUTOFILL_KEY)
+ }
+
+ override fun storeKeyAndCanary(key: String) {
+ // To consider: should this be a non-destructive operation, just in case?
+ // e.g. if we thought we lost the key, but actually did not, that would let us recover data later on.
+ // otherwise, if we mess up and override a perfectly good key, the data is gone for good.
+ securePrefs.putString(AUTOFILL_KEY, key)
+ // To detect key corruption or absence, use the newly generated key to encrypt a known string.
+ // See isKeyValid below.
+ plaintextPrefs
+ .edit()
+ .putString(CANARY_PHRASE_CIPHERTEXT_KEY, encryptString(key, CANARY_PHRASE_PLAINTEXT))
+ .apply()
+ }
+
+ override suspend fun recoverFromKeyLoss(reason: KeyGenerationReason.RecoveryNeeded) {
+ storage.scrubEncryptedData()
+ }
+
+ companion object {
+ const val AUTOFILL_PREFS = "autofillCrypto"
+ const val AUTOFILL_KEY = "autofillKey"
+ const val CANARY_PHRASE_CIPHERTEXT_KEY = "canaryPhrase"
+ const val CANARY_PHRASE_PLAINTEXT = "a string for checking validity of the key"
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt
new file mode 100644
index 0000000000..d3e6122fcb
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt
@@ -0,0 +1,51 @@
+/* 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.service.sync.autofill
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardValidationDelegate
+import mozilla.components.concept.storage.CreditCardValidationDelegate.Result
+import mozilla.components.concept.storage.CreditCardsAddressesStorage
+
+/**
+ * A delegate that will check against the [CreditCardsAddressesStorage] to determine if a given
+ * [CreditCard] can be persisted and returns information about why it can or cannot.
+ *
+ * @param storage An instance of [CreditCardsAddressesStorage].
+ */
+class DefaultCreditCardValidationDelegate(
+ private val storage: Lazy<CreditCardsAddressesStorage>,
+) : CreditCardValidationDelegate {
+
+ private val coroutineContext by lazy { Dispatchers.IO }
+
+ override suspend fun shouldCreateOrUpdate(creditCard: CreditCardEntry): Result =
+ withContext(coroutineContext) {
+ val creditCards = storage.value.getAllCreditCards()
+
+ val foundCreditCard = if (creditCards.isEmpty()) {
+ // No credit cards exist in the storage -> create a new credit card
+ null
+ } else {
+ val crypto = storage.value.getCreditCardCrypto()
+ val key = crypto.getOrGenerateKey()
+
+ creditCards.find {
+ val cardNumber = crypto.decrypt(key, it.encryptedCardNumber)?.number
+
+ it.guid == creditCard.guid || cardNumber == creditCard.number
+ }
+ }
+
+ if (foundCreditCard == null) {
+ Result.CanBeCreated
+ } else {
+ Result.CanBeUpdated(foundCreditCard)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/Errors.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/Errors.kt
new file mode 100644
index 0000000000..9e1ef01e40
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/Errors.kt
@@ -0,0 +1,27 @@
+/* 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.service.sync.autofill
+
+/**
+ * Unrecoverable errors related to [AutofillCreditCardsAddressesStorage].
+ * Do not catch these.
+ */
+internal sealed class AutofillStorageException(reason: Exception? = null) : RuntimeException(reason) {
+ /**
+ * Thrown if an attempt was made to persist a plaintext version of a credit card number.
+ */
+ class TriedToPersistPlaintextCardNumber : AutofillStorageException()
+}
+
+/**
+ * Unrecoverable errors related to [AutofillCrypto].
+ * Do not catch these.
+ */
+internal sealed class AutofillCryptoException(cause: Exception? = null) : RuntimeException(cause) {
+ /**
+ * Thrown if [AutofillCrypto] encounters an unexpected, unrecoverable state.
+ */
+ class IllegalState : AutofillCryptoException()
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt
new file mode 100644
index 0000000000..c040fbd68f
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt
@@ -0,0 +1,107 @@
+/* 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.service.sync.autofill
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.CreditCardValidationDelegate
+import mozilla.components.concept.storage.CreditCardsAddressesStorage
+import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
+import mozilla.components.concept.storage.ManagedKey
+import mozilla.components.concept.storage.NewCreditCardFields
+import mozilla.components.concept.storage.UpdatableCreditCardFields
+import mozilla.components.support.ktx.kotlin.last4Digits
+
+/**
+ * [CreditCardsAddressesStorageDelegate] implementation.
+ *
+ * @param storage The [CreditCardsAddressesStorage] used for looking up addresses and credit cards to autofill.
+ * @param scope [CoroutineScope] for long running operations. Defaults to using the [Dispatchers.IO].
+ * @param isCreditCardAutofillEnabled callback allowing to limit [storage] operations if autofill is disabled.
+ * @param validationDelegate The [DefaultCreditCardValidationDelegate] used to check if a credit card
+ * can be saved in [storage] and returns information about why it can or cannot
+ */
+class GeckoCreditCardsAddressesStorageDelegate(
+ private val storage: Lazy<CreditCardsAddressesStorage>,
+ private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+ private val validationDelegate: DefaultCreditCardValidationDelegate = DefaultCreditCardValidationDelegate(storage),
+ private val isCreditCardAutofillEnabled: () -> Boolean = { false },
+ private val isAddressAutofillEnabled: () -> Boolean = { false },
+) : CreditCardsAddressesStorageDelegate {
+
+ override suspend fun getOrGenerateKey(): ManagedKey {
+ val crypto = storage.value.getCreditCardCrypto()
+ return crypto.getOrGenerateKey()
+ }
+
+ override suspend fun decrypt(
+ key: ManagedKey,
+ encryptedCardNumber: CreditCardNumber.Encrypted,
+ ): CreditCardNumber.Plaintext? {
+ val crypto = storage.value.getCreditCardCrypto()
+ return crypto.decrypt(key, encryptedCardNumber)
+ }
+
+ override suspend fun onAddressesFetch(): List<Address> = withContext(scope.coroutineContext) {
+ if (!isAddressAutofillEnabled()) {
+ emptyList()
+ } else {
+ storage.value.getAllAddresses()
+ }
+ }
+
+ override suspend fun onAddressSave(address: Address) {
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun onCreditCardsFetch(): List<CreditCard> =
+ withContext(scope.coroutineContext) {
+ if (!isCreditCardAutofillEnabled()) {
+ emptyList()
+ } else {
+ storage.value.getAllCreditCards()
+ }
+ }
+
+ override suspend fun onCreditCardSave(creditCard: CreditCardEntry) {
+ if (!creditCard.isValid) return
+
+ scope.launch {
+ when (val result = validationDelegate.shouldCreateOrUpdate(creditCard)) {
+ is CreditCardValidationDelegate.Result.CanBeCreated -> {
+ storage.value.addCreditCard(
+ NewCreditCardFields(
+ billingName = creditCard.name,
+ plaintextCardNumber = CreditCardNumber.Plaintext(creditCard.number),
+ cardNumberLast4 = creditCard.number.last4Digits(),
+ expiryMonth = creditCard.expiryMonth.toLong(),
+ expiryYear = creditCard.expiryYear.toLong(),
+ cardType = creditCard.cardType,
+ ),
+ )
+ }
+ is CreditCardValidationDelegate.Result.CanBeUpdated -> {
+ storage.value.updateCreditCard(
+ guid = result.foundCreditCard.guid,
+ creditCardFields = UpdatableCreditCardFields(
+ billingName = creditCard.name,
+ cardNumber = CreditCardNumber.Plaintext(creditCard.number),
+ cardNumberLast4 = creditCard.number.last4Digits(),
+ expiryMonth = creditCard.expiryMonth.toLong(),
+ expiryYear = creditCard.expiryYear.toLong(),
+ cardType = creditCard.cardType,
+ ),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/Types.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/Types.kt
new file mode 100644
index 0000000000..5e222e2ea1
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/Types.kt
@@ -0,0 +1,92 @@
+/* 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.service.sync.autofill
+
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.UpdatableAddressFields
+import mozilla.components.concept.storage.UpdatableCreditCardFields
+
+// We have type definitions at the concept level, and "external" types defined within Autofill.
+// In practice these two types are largely the same, and this file is the conversion point.
+
+/**
+ * Conversion from a generic [UpdatableAddressFields] into its richer comrade within the 'autofill' lib.
+ */
+internal fun UpdatableAddressFields.into(): mozilla.appservices.autofill.UpdatableAddressFields {
+ return mozilla.appservices.autofill.UpdatableAddressFields(
+ name = this.name,
+ organization = this.organization,
+ streetAddress = this.streetAddress,
+ addressLevel3 = this.addressLevel3,
+ addressLevel2 = this.addressLevel2,
+ addressLevel1 = this.addressLevel1,
+ postalCode = this.postalCode,
+ country = this.country,
+ tel = this.tel,
+ email = this.email,
+ )
+}
+
+/**
+ * Conversion from a generic [UpdatableCreditCardFields] into its comrade within the 'autofill' lib.
+ */
+internal fun UpdatableCreditCardFields.into(): mozilla.appservices.autofill.UpdatableCreditCardFields {
+ val encryptedCardNumber = when (this.cardNumber) {
+ is CreditCardNumber.Encrypted -> this.cardNumber.number
+ is CreditCardNumber.Plaintext -> throw AutofillStorageException.TriedToPersistPlaintextCardNumber()
+ }
+ return mozilla.appservices.autofill.UpdatableCreditCardFields(
+ ccName = this.billingName,
+ ccNumberEnc = encryptedCardNumber,
+ ccNumberLast4 = this.cardNumberLast4,
+ ccExpMonth = this.expiryMonth,
+ ccExpYear = this.expiryYear,
+ ccType = this.cardType,
+ )
+}
+
+/**
+ * Conversion from a "native" autofill [Address] into its generic comrade.
+ */
+internal fun mozilla.appservices.autofill.Address.into(): Address {
+ return Address(
+ guid = this.guid,
+ name = this.name,
+ organization = this.organization,
+ streetAddress = this.streetAddress,
+ addressLevel3 = this.addressLevel3,
+ addressLevel2 = this.addressLevel2,
+ addressLevel1 = this.addressLevel1,
+ postalCode = this.postalCode,
+ country = this.country,
+ tel = this.tel,
+ email = this.email,
+ timeCreated = this.timeCreated,
+ timeLastUsed = this.timeLastUsed,
+ timeLastModified = this.timeLastModified,
+ timesUsed = this.timesUsed,
+ )
+}
+
+/**
+ * Conversion from a "native" autofill [CreditCard] into its generic comrade.
+ */
+internal fun mozilla.appservices.autofill.CreditCard.into(): CreditCard {
+ return CreditCard(
+ guid = this.guid,
+ billingName = this.ccName,
+ encryptedCardNumber = CreditCardNumber.Encrypted(this.ccNumberEnc),
+ cardNumberLast4 = this.ccNumberLast4,
+ expiryMonth = this.ccExpMonth,
+ expiryYear = this.ccExpYear,
+ cardType = this.ccType,
+ timeCreated = this.timeCreated,
+ timeLastUsed = this.timeLastUsed,
+ timeLastModified = this.timeLastModified,
+ timesUsed = this.timesUsed,
+ )
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt
new file mode 100644
index 0000000000..3fa18c40ca
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt
@@ -0,0 +1,407 @@
+/* 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.service.sync.autofill
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.NewCreditCardFields
+import mozilla.components.concept.storage.UpdatableAddressFields
+import mozilla.components.concept.storage.UpdatableCreditCardFields
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+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.Mockito.spy
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class AutofillCreditCardsAddressesStorageTest {
+
+ private lateinit var storage: AutofillCreditCardsAddressesStorage
+ private lateinit var securePrefs: SecureAbove22Preferences
+
+ @Before
+ fun setup() {
+ // forceInsecure is set in the tests because a keystore wouldn't be configured in the test environment.
+ securePrefs = SecureAbove22Preferences(testContext, "autofill", forceInsecure = true)
+ storage = AutofillCreditCardsAddressesStorage(testContext, lazy { securePrefs })
+ }
+
+ @After
+ fun cleanup() {
+ storage.close()
+ }
+
+ @Test
+ fun `add credit card`() = runTest {
+ val plaintextNumber = CreditCardNumber.Plaintext("4111111111111111")
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Jon Doe",
+ plaintextCardNumber = plaintextNumber,
+ cardNumberLast4 = "1111",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "amex",
+ )
+ val creditCard = storage.addCreditCard(creditCardFields)
+
+ assertNotNull(creditCard)
+
+ assertEquals(creditCardFields.billingName, creditCard.billingName)
+ assertEquals(plaintextNumber, storage.crypto.decrypt(storage.crypto.getOrGenerateKey(), creditCard.encryptedCardNumber))
+ assertEquals(creditCardFields.cardNumberLast4, creditCard.cardNumberLast4)
+ assertEquals(creditCardFields.expiryMonth, creditCard.expiryMonth)
+ assertEquals(creditCardFields.expiryYear, creditCard.expiryYear)
+ assertEquals(creditCardFields.cardType, creditCard.cardType)
+ assertEquals(
+ CreditCard.ellipsesStart +
+ CreditCard.ellipsis + CreditCard.ellipsis + CreditCard.ellipsis + CreditCard.ellipsis +
+ creditCardFields.cardNumberLast4 +
+ CreditCard.ellipsesEnd,
+ creditCard.obfuscatedCardNumber,
+ )
+ }
+
+ @Test
+ fun `get credit card`() = runTest {
+ val plaintextNumber = CreditCardNumber.Plaintext("5500000000000004")
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Jon Doe",
+ plaintextCardNumber = plaintextNumber,
+ cardNumberLast4 = "0004",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "amex",
+ )
+ val creditCard = storage.addCreditCard(creditCardFields)
+
+ assertEquals(creditCard, storage.getCreditCard(creditCard.guid))
+ }
+
+ @Test
+ fun `GIVEN a non-existent credit card guid WHEN getCreditCard is called THEN null is returned`() = runTest {
+ assertNull(storage.getCreditCard("guid"))
+ }
+
+ @Test
+ fun `get all credit cards`() = runTest {
+ val plaintextNumber1 = CreditCardNumber.Plaintext("5500000000000004")
+ val creditCardFields1 = NewCreditCardFields(
+ billingName = "Jane Fields",
+ plaintextCardNumber = plaintextNumber1,
+ cardNumberLast4 = "0004",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "mastercard",
+ )
+ val plaintextNumber2 = CreditCardNumber.Plaintext("4111111111111111")
+ val creditCardFields2 = NewCreditCardFields(
+ billingName = "Banana Apple",
+ plaintextCardNumber = plaintextNumber2,
+ cardNumberLast4 = "1111",
+ expiryMonth = 1,
+ expiryYear = 2030,
+ cardType = "visa",
+ )
+ val plaintextNumber3 = CreditCardNumber.Plaintext("340000000000009")
+ val creditCardFields3 = NewCreditCardFields(
+ billingName = "Pineapple Orange",
+ plaintextCardNumber = plaintextNumber3,
+ cardNumberLast4 = "0009",
+ expiryMonth = 2,
+ expiryYear = 2028,
+ cardType = "amex",
+ )
+ val creditCard1 = storage.addCreditCard(creditCardFields1)
+ val creditCard2 = storage.addCreditCard(creditCardFields2)
+ val creditCard3 = storage.addCreditCard(creditCardFields3)
+
+ val creditCards = storage.getAllCreditCards()
+ val key = storage.crypto.getOrGenerateKey()
+
+ val savedCreditCard1 = creditCards.find { it == creditCard1 }
+ assertNotNull(savedCreditCard1)
+ val savedCreditCard2 = creditCards.find { it == creditCard2 }
+ assertNotNull(savedCreditCard2)
+ val savedCreditCard3 = creditCards.find { it == creditCard3 }
+ assertNotNull(savedCreditCard3)
+
+ assertEquals(plaintextNumber1, storage.crypto.decrypt(key, savedCreditCard1!!.encryptedCardNumber))
+ assertEquals(plaintextNumber2, storage.crypto.decrypt(key, savedCreditCard2!!.encryptedCardNumber))
+ assertEquals(plaintextNumber3, storage.crypto.decrypt(key, savedCreditCard3!!.encryptedCardNumber))
+ }
+
+ @Test
+ fun `update credit card`() = runTest {
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Jon Doe",
+ plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"),
+ cardNumberLast4 = "1111",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "visa",
+ )
+
+ var creditCard = storage.addCreditCard(creditCardFields)
+
+ // Change every field
+ var newCreditCardFields = UpdatableCreditCardFields(
+ billingName = "Jane Fields",
+ cardNumber = CreditCardNumber.Plaintext("30000000000004"),
+ cardNumberLast4 = "0004",
+ expiryMonth = 12,
+ expiryYear = 2038,
+ cardType = "diners",
+ )
+
+ storage.updateCreditCard(creditCard.guid, newCreditCardFields)
+
+ creditCard = storage.getCreditCard(creditCard.guid)!!
+
+ val key = storage.crypto.getOrGenerateKey()
+
+ assertEquals(newCreditCardFields.billingName, creditCard.billingName)
+ assertEquals(newCreditCardFields.cardNumber, storage.crypto.decrypt(key, creditCard.encryptedCardNumber))
+ assertEquals(newCreditCardFields.cardNumberLast4, creditCard.cardNumberLast4)
+ assertEquals(newCreditCardFields.expiryMonth, creditCard.expiryMonth)
+ assertEquals(newCreditCardFields.expiryYear, creditCard.expiryYear)
+ assertEquals(newCreditCardFields.cardType, creditCard.cardType)
+
+ // Change the name only.
+ newCreditCardFields = UpdatableCreditCardFields(
+ billingName = "Bob Jones",
+ cardNumber = creditCard.encryptedCardNumber,
+ cardNumberLast4 = "0004",
+ expiryMonth = 12,
+ expiryYear = 2038,
+ cardType = "diners",
+ )
+
+ storage.updateCreditCard(creditCard.guid, newCreditCardFields)
+
+ creditCard = storage.getCreditCard(creditCard.guid)!!
+
+ assertEquals(newCreditCardFields.billingName, creditCard.billingName)
+ assertEquals(newCreditCardFields.cardNumber, creditCard.encryptedCardNumber)
+ assertEquals(newCreditCardFields.cardNumberLast4, creditCard.cardNumberLast4)
+ assertEquals(newCreditCardFields.expiryMonth, creditCard.expiryMonth)
+ assertEquals(newCreditCardFields.expiryYear, creditCard.expiryYear)
+ assertEquals(newCreditCardFields.cardType, creditCard.cardType)
+ }
+
+ @Test
+ fun `delete credit card`() = runTest {
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Jon Doe",
+ plaintextCardNumber = CreditCardNumber.Plaintext("30000000000004"),
+ cardNumberLast4 = "0004",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "diners",
+ )
+
+ val creditCard = storage.addCreditCard(creditCardFields)
+ assertNotNull(storage.getCreditCard(creditCard.guid))
+
+ val isDeleteSuccessful = storage.deleteCreditCard(creditCard.guid)
+
+ assertTrue(isDeleteSuccessful)
+ assertNull(storage.getCreditCard(creditCard.guid))
+ }
+
+ @Test
+ fun `add address`() = runTest {
+ val addressFields = UpdatableAddressFields(
+ name = "John Smith",
+ organization = "Mozilla",
+ streetAddress = "123 Sesame Street",
+ addressLevel3 = "",
+ addressLevel2 = "",
+ addressLevel1 = "",
+ postalCode = "90210",
+ country = "US",
+ tel = "+1 519 555-5555",
+ email = "foo@bar.com",
+ )
+ val address = storage.addAddress(addressFields)
+
+ assertNotNull(address)
+
+ assertEquals(addressFields.name, address.name)
+ assertEquals(addressFields.organization, address.organization)
+ assertEquals(addressFields.streetAddress, address.streetAddress)
+ assertEquals(addressFields.addressLevel3, address.addressLevel3)
+ assertEquals(addressFields.addressLevel2, address.addressLevel2)
+ assertEquals(addressFields.addressLevel1, address.addressLevel1)
+ assertEquals(addressFields.postalCode, address.postalCode)
+ assertEquals(addressFields.country, address.country)
+ assertEquals(addressFields.tel, address.tel)
+ assertEquals(addressFields.email, address.email)
+ }
+
+ @Test
+ fun `get address`() = runTest {
+ val addressFields = UpdatableAddressFields(
+ name = "John Smith",
+ organization = "Mozilla",
+ streetAddress = "123 Sesame Street",
+ addressLevel3 = "",
+ addressLevel2 = "",
+ addressLevel1 = "",
+ postalCode = "90210",
+ country = "US",
+ tel = "+1 519 555-5555",
+ email = "foo@bar.com",
+ )
+ val address = storage.addAddress(addressFields)
+
+ assertEquals(address, storage.getAddress(address.guid))
+ }
+
+ @Test
+ fun `GIVEN a non-existent address guid WHEN getAddress is called THEN null is returned`() = runTest {
+ assertNull(storage.getAddress("guid"))
+ }
+
+ @Test
+ fun `get all addresses`() = runTest {
+ val addressFields1 = UpdatableAddressFields(
+ name = "John Smith",
+ organization = "Mozilla",
+ streetAddress = "123 Sesame Street",
+ addressLevel3 = "",
+ addressLevel2 = "",
+ addressLevel1 = "",
+ postalCode = "90210",
+ country = "US",
+ tel = "+1 519 555-5555",
+ email = "foo@bar.com",
+ )
+ val addressFields2 = UpdatableAddressFields(
+ name = "Mary Sue",
+ organization = "",
+ streetAddress = "1 New St",
+ addressLevel3 = "",
+ addressLevel2 = "York",
+ addressLevel1 = "SC",
+ postalCode = "29745",
+ country = "US",
+ tel = "+19871234567",
+ email = "mary@example.com",
+ )
+ val addressFields3 = UpdatableAddressFields(
+ name = "Timothy João Berners-Lee",
+ organization = "World Wide Web Consortium",
+ streetAddress = "Rua Adalberto Pajuaba, 404",
+ addressLevel3 = "Campos Elísios",
+ addressLevel2 = "Ribeirão Preto",
+ addressLevel1 = "SP",
+ postalCode = "14055-220",
+ country = "BR",
+ tel = "+0318522222222",
+ email = "timbr@example.org",
+ )
+ val address1 = storage.addAddress(addressFields1)
+ val address2 = storage.addAddress(addressFields2)
+ val address3 = storage.addAddress(addressFields3)
+
+ val addresses = storage.getAllAddresses()
+
+ val savedAddress1 = addresses.find { it == address1 }
+ assertNotNull(savedAddress1)
+ val savedAddress2 = addresses.find { it == address2 }
+ assertNotNull(savedAddress2)
+ val savedAddress3 = addresses.find { it == address3 }
+ assertNotNull(savedAddress3)
+ }
+
+ @Test
+ fun `update address`() = runTest {
+ val addressFields = UpdatableAddressFields(
+ name = "John Smith",
+ organization = "Mozilla",
+ streetAddress = "123 Sesame Street",
+ addressLevel3 = "",
+ addressLevel2 = "",
+ addressLevel1 = "",
+ postalCode = "90210",
+ country = "US",
+ tel = "+1 519 555-5555",
+ email = "foo@bar.com",
+ )
+
+ var address = storage.addAddress(addressFields)
+
+ val newAddressFields = UpdatableAddressFields(
+ name = "Mary Sue",
+ organization = "",
+ streetAddress = "1 New St",
+ addressLevel3 = "",
+ addressLevel2 = "York",
+ addressLevel1 = "SC",
+ postalCode = "29745",
+ country = "US",
+ tel = "+19871234567",
+ email = "mary@example.com",
+ )
+
+ storage.updateAddress(address.guid, newAddressFields)
+
+ address = storage.getAddress(address.guid)!!
+
+ assertEquals(newAddressFields.name, address.name)
+ assertEquals(newAddressFields.organization, address.organization)
+ assertEquals(newAddressFields.streetAddress, address.streetAddress)
+ assertEquals(newAddressFields.addressLevel3, address.addressLevel3)
+ assertEquals(newAddressFields.addressLevel2, address.addressLevel2)
+ assertEquals(newAddressFields.addressLevel1, address.addressLevel1)
+ assertEquals(newAddressFields.postalCode, address.postalCode)
+ assertEquals(newAddressFields.country, address.country)
+ assertEquals(newAddressFields.tel, address.tel)
+ assertEquals(newAddressFields.email, address.email)
+ }
+
+ @Test
+ fun `delete address`() = runTest {
+ val addressFields = UpdatableAddressFields(
+ name = "John Smith",
+ organization = "Mozilla",
+ streetAddress = "123 Sesame Street",
+ addressLevel3 = "",
+ addressLevel2 = "",
+ addressLevel1 = "",
+ postalCode = "90210",
+ country = "US",
+ tel = "+1 519 555-5555",
+ email = "foo@bar.com",
+ )
+
+ val address = storage.addAddress(addressFields)
+ val savedAddress = storage.getAddress(address.guid)
+ assertEquals(address, savedAddress)
+
+ val isDeleteSuccessful = storage.deleteAddress(address.guid)
+ assertTrue(isDeleteSuccessful)
+ assertNull(storage.getAddress(address.guid))
+ }
+
+ @Test
+ fun `WHEN warmUp method is called THEN the database connection is established`(): Unit = runTest {
+ val storageSpy = spy(storage)
+ storageSpy.warmUp()
+
+ verify(storageSpy).conn
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCryptoTest.kt b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCryptoTest.kt
new file mode 100644
index 0000000000..f0223a5f25
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCryptoTest.kt
@@ -0,0 +1,147 @@
+/* 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.service.sync.autofill
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.KeyGenerationReason
+import mozilla.components.concept.storage.ManagedKey
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class AutofillCryptoTest {
+
+ private lateinit var securePrefs: SecureAbove22Preferences
+
+ @Before
+ fun setup() {
+ // forceInsecure is set in the tests because a keystore wouldn't be configured in the test environment.
+ securePrefs = SecureAbove22Preferences(testContext, "autofill", forceInsecure = true)
+ }
+
+ @Test
+ fun `get key - new`() = runTest {
+ val storage = mock<AutofillCreditCardsAddressesStorage>()
+ val crypto = AutofillCrypto(testContext, securePrefs, storage)
+ val key = crypto.getOrGenerateKey()
+ assertEquals(KeyGenerationReason.New, key.wasGenerated)
+
+ // key was persisted, subsequent fetches return it.
+ val key2 = crypto.getOrGenerateKey()
+ assertNull(key2.wasGenerated)
+
+ assertEquals(key.key, key2.key)
+ verifyNoInteractions(storage)
+ }
+
+ @Test
+ fun `get key - lost`() = runTest {
+ val storage = mock<AutofillCreditCardsAddressesStorage>()
+ val crypto = AutofillCrypto(testContext, securePrefs, storage)
+ val key = crypto.getOrGenerateKey()
+ assertEquals(KeyGenerationReason.New, key.wasGenerated)
+
+ // now, let's loose the key. It'll be regenerated
+ securePrefs.clear()
+ val key2 = crypto.getOrGenerateKey()
+ assertEquals(KeyGenerationReason.RecoveryNeeded.Lost, key2.wasGenerated)
+
+ assertNotEquals(key.key, key2.key)
+ verify(storage).scrubEncryptedData()
+ }
+
+ @Test
+ fun `get key - corrupted`() = runTest {
+ val storage = mock<AutofillCreditCardsAddressesStorage>()
+ val crypto = AutofillCrypto(testContext, securePrefs, storage)
+ val key = crypto.getOrGenerateKey()
+ assertEquals(KeyGenerationReason.New, key.wasGenerated)
+
+ // now, let's corrupt the key. It'll be regenerated
+ securePrefs.putString(AutofillCrypto.AUTOFILL_KEY, "garbage")
+
+ val key2 = crypto.getOrGenerateKey()
+ assertEquals(KeyGenerationReason.RecoveryNeeded.Corrupt, key2.wasGenerated)
+
+ assertNotEquals(key.key, key2.key)
+ verify(storage).scrubEncryptedData()
+ }
+
+ @Test
+ fun `get key - corrupted subtly`() = runTest {
+ val storage = mock<AutofillCreditCardsAddressesStorage>()
+ val crypto = AutofillCrypto(testContext, securePrefs, storage)
+ val key = crypto.getOrGenerateKey()
+ assertEquals(KeyGenerationReason.New, key.wasGenerated)
+
+ // now, let's corrupt the key. It'll be regenerated
+ // this key is shaped correctly, but of course it won't be the same as what we got back in the first call to key()
+ securePrefs.putString(AutofillCrypto.AUTOFILL_KEY, "{\"kty\":\"oct\",\"k\":\"GhsmEtujZN_qMEgw1ZHhcJhdAFR9EkUgb94qANel-P4\"}")
+
+ val key2 = crypto.getOrGenerateKey()
+ assertEquals(KeyGenerationReason.RecoveryNeeded.Corrupt, key2.wasGenerated)
+
+ assertNotEquals(key.key, key2.key)
+ verify(storage).scrubEncryptedData()
+ }
+
+ @Test
+ fun `encrypt and decrypt card - normal`() = runTest {
+ val crypto = AutofillCrypto(testContext, securePrefs, mock())
+ val key = crypto.getOrGenerateKey()
+ val plaintext1 = CreditCardNumber.Plaintext("4111111111111111")
+ val plaintext2 = CreditCardNumber.Plaintext("4111111111111111")
+
+ val encrypted1 = crypto.encrypt(key, plaintext1)!!
+ val encrypted2 = crypto.encrypt(key, plaintext2)!!
+
+ // We use a non-deterministic encryption scheme.
+ assertNotEquals(encrypted1, encrypted2)
+
+ assertEquals("4111111111111111", crypto.decrypt(key, encrypted1)!!.number)
+ assertEquals("4111111111111111", crypto.decrypt(key, encrypted2)!!.number)
+ }
+
+ @Test
+ fun `encrypt and decrypt card - bad keys`() = runTest {
+ val crypto = AutofillCrypto(testContext, securePrefs, mock())
+ val plaintext = CreditCardNumber.Plaintext("4111111111111111")
+
+ val badKey = ManagedKey(key = "garbage", wasGenerated = null)
+ assertNull(crypto.encrypt(badKey, plaintext))
+
+ // This isn't a valid key.
+ val corruptKey = ManagedKey(key = "{\"kty\":\"oct\",\"k\":\"GhsmEtujZN_qMEgw1ZHhcJhdAFR9EkU\"}", wasGenerated = null)
+ assertNull(crypto.encrypt(corruptKey, plaintext))
+
+ val goodKey = crypto.getOrGenerateKey()
+ val encrypted = crypto.encrypt(goodKey, plaintext)!!
+
+ assertNull(crypto.decrypt(badKey, encrypted))
+ assertNull(crypto.decrypt(corruptKey, encrypted))
+ }
+
+ @Test
+ fun `decrypt scrubbed card`() = runTest {
+ val crypto = AutofillCrypto(testContext, securePrefs, mock())
+ val key = crypto.getOrGenerateKey()
+ // if a key was previously lost we will wipe the card numbers.
+ val encrypted = CreditCardNumber.Encrypted("")
+ assertNull(crypto.decrypt(key, encrypted))
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegateTest.kt b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegateTest.kt
new file mode 100644
index 0000000000..1bbb8b1630
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegateTest.kt
@@ -0,0 +1,134 @@
+/* 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.service.sync.autofill
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.CreditCardValidationDelegate.Result
+import mozilla.components.concept.storage.NewCreditCardFields
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class DefaultCreditCardValidationDelegateTest {
+
+ private lateinit var storage: AutofillCreditCardsAddressesStorage
+ private lateinit var securePrefs: SecureAbove22Preferences
+ private lateinit var validationDelegate: DefaultCreditCardValidationDelegate
+
+ @Before
+ fun before() = runBlocking {
+ // forceInsecure is set in the tests because a keystore wouldn't be configured in the test environment.
+ securePrefs = SecureAbove22Preferences(testContext, "autofill", forceInsecure = true)
+ storage = AutofillCreditCardsAddressesStorage(testContext, lazy { securePrefs })
+ validationDelegate = DefaultCreditCardValidationDelegate(storage = lazy { storage })
+ }
+
+ @Test
+ fun `WHEN no credit cards exist in the storage, THEN add the new credit card to storage`() =
+ runBlocking {
+ val newCreditCard = createCreditCardEntry(guid = "1")
+ val result = validationDelegate.shouldCreateOrUpdate(newCreditCard)
+
+ assertEquals(Result.CanBeCreated, result)
+ }
+
+ @Test
+ fun `WHEN existing credit card matches by guid and card number, THEN update the credit card in storage`() =
+ runBlocking {
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Pineapple Orange",
+ plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"),
+ cardNumberLast4 = "1111",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "visa",
+ )
+ val creditCard = storage.addCreditCard(creditCardFields)
+ val newCreditCard = createCreditCardEntry(guid = creditCard.guid)
+ val result = validationDelegate.shouldCreateOrUpdate(newCreditCard)
+
+ assertEquals(Result.CanBeUpdated(creditCard), result)
+ }
+
+ @Test
+ fun `WHEN existing credit card matches by guid only, THEN update the credit card in storage`() =
+ runBlocking {
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Pineapple Orange",
+ plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"),
+ cardNumberLast4 = "1111",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "visa",
+ )
+ val creditCard = storage.addCreditCard(creditCardFields)
+ val newCreditCard = createCreditCardEntry(guid = creditCard.guid)
+ val result = validationDelegate.shouldCreateOrUpdate(newCreditCard)
+
+ assertEquals(Result.CanBeUpdated(creditCard), result)
+ }
+
+ @Test
+ fun `WHEN existing credit card matches by card number only, THEN update the credit card in storage`() =
+ runBlocking {
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Pineapple Orange",
+ plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"),
+ cardNumberLast4 = "1111",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "visa",
+ )
+ val creditCard = storage.addCreditCard(creditCardFields)
+ val newCreditCard = createCreditCardEntry(cardNumber = "4111111111111111")
+ val result = validationDelegate.shouldCreateOrUpdate(newCreditCard)
+
+ assertEquals(Result.CanBeUpdated(creditCard), result)
+ }
+
+ @Test
+ fun `WHEN existing credit card does not match by guid and card number, THEN add the new credit card to storage`() =
+ runBlocking {
+ val newCreditCard = createCreditCardEntry(guid = "2")
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Pineapple Orange",
+ plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"),
+ cardNumberLast4 = "1111",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "visa",
+ )
+ storage.addCreditCard(creditCardFields)
+
+ val result = validationDelegate.shouldCreateOrUpdate(newCreditCard)
+
+ assertEquals(Result.CanBeCreated, result)
+ }
+}
+
+fun createCreditCardEntry(
+ guid: String = "id",
+ billingName: String = "Banana Apple",
+ cardNumber: String = "4111111111111110",
+ expiryMonth: String = "1",
+ expiryYear: String = "2030",
+ cardType: String = "amex",
+) = CreditCardEntry(
+ guid = guid,
+ name = billingName,
+ number = cardNumber,
+ expiryMonth = expiryMonth,
+ expiryYear = expiryYear,
+ cardType = cardType,
+)
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegateTest.kt b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegateTest.kt
new file mode 100644
index 0000000000..9974f055a1
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegateTest.kt
@@ -0,0 +1,243 @@
+/* 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.service.sync.autofill
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.CreditCardValidationDelegate
+import mozilla.components.concept.storage.NewCreditCardFields
+import mozilla.components.concept.storage.UpdatableCreditCardFields
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.support.ktx.kotlin.last4Digits
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+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
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class GeckoCreditCardsAddressesStorageDelegateTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ private lateinit var storage: AutofillCreditCardsAddressesStorage
+ private lateinit var securePrefs: SecureAbove22Preferences
+ private lateinit var delegate: GeckoCreditCardsAddressesStorageDelegate
+ private val validationDelegate: DefaultCreditCardValidationDelegate = mock()
+
+ init {
+ testContext.getDatabasePath(AUTOFILL_DB_NAME)!!.parentFile!!.mkdirs()
+ }
+
+ @Before
+ fun before() {
+ // forceInsecure is set in the tests because a keystore wouldn't be configured in the test environment.
+ securePrefs = SecureAbove22Preferences(testContext, "autofill", forceInsecure = true)
+ storage = spy(AutofillCreditCardsAddressesStorage(testContext, lazy { securePrefs }))
+ delegate = GeckoCreditCardsAddressesStorageDelegate(lazy { storage }, scope, validationDelegate)
+ }
+
+ @Test
+ fun `GIVEN a newly added credit card WHEN decrypt is called THEN it returns the plain credit card number`() =
+ runTestOnMain {
+ val plaintextNumber = CreditCardNumber.Plaintext("4111111111111111")
+ val creditCardFields = NewCreditCardFields(
+ billingName = "Jon Doe",
+ plaintextCardNumber = plaintextNumber,
+ cardNumberLast4 = "1111",
+ expiryMonth = 12,
+ expiryYear = 2028,
+ cardType = "amex",
+ )
+ val creditCard = storage.addCreditCard(creditCardFields)
+ val key = delegate.getOrGenerateKey()
+
+ assertEquals(
+ plaintextNumber,
+ delegate.decrypt(key, creditCard.encryptedCardNumber),
+ )
+ }
+
+ @Test
+ fun `GIVEN autofill enabled WHEN onCreditCardsFetch is called THEN it returns all stored cards`() =
+ runTest {
+ val storage: AutofillCreditCardsAddressesStorage = mock()
+ val storedCards = listOf<CreditCard>(mock())
+ doReturn(storedCards).`when`(storage).getAllCreditCards()
+ delegate = GeckoCreditCardsAddressesStorageDelegate(lazy { storage }, scope, isCreditCardAutofillEnabled = { true })
+
+ val result = delegate.onCreditCardsFetch()
+
+ verify(storage, times(1)).getAllCreditCards()
+ assertEquals(storedCards, result)
+ }
+
+ @Test
+ fun `GIVEN autofill disabled WHEN onCreditCardsFetch is called THEN it returns an empty list of cards`() =
+ runTest {
+ val storage: AutofillCreditCardsAddressesStorage = mock()
+ val storedCards = listOf<CreditCard>(mock())
+ doReturn(storedCards).`when`(storage).getAllCreditCards()
+ delegate = GeckoCreditCardsAddressesStorageDelegate(lazy { storage }, scope, isCreditCardAutofillEnabled = { false })
+
+ val result = delegate.onCreditCardsFetch()
+
+ verify(storage, never()).getAllCreditCards()
+ assertEquals(emptyList<CreditCard>(), result)
+ }
+
+ @Test
+ fun `GIVEN a new credit card WHEN onCreditCardSave is called THEN it adds a new credit card in storage`() {
+ runTest {
+ val billingName = "Jon Doe"
+ val cardNumber = "4111111111111111"
+ val expiryMonth = 12L
+ val expiryYear = 2028L
+ val cardType = "amex"
+
+ val creditCardEntry = CreditCardEntry(
+ name = billingName,
+ number = cardNumber,
+ expiryMonth = expiryMonth.toString(),
+ expiryYear = expiryYear.toString(),
+ cardType = cardType,
+ )
+
+ doReturn(CreditCardValidationDelegate.Result.CanBeCreated).`when`(validationDelegate).shouldCreateOrUpdate(creditCardEntry)
+
+ delegate.onCreditCardSave(creditCardEntry)
+
+ verify(storage, times(1)).addCreditCard(
+ creditCardFields = NewCreditCardFields(
+ billingName = billingName,
+ plaintextCardNumber = CreditCardNumber.Plaintext(cardNumber),
+ cardNumberLast4 = cardNumber.last4Digits(),
+ expiryMonth = expiryMonth,
+ expiryYear = expiryYear,
+ cardType = cardType,
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN an existing credit card WHEN onCreditCardSave is called THEN it updates the existing credit card in storage`() {
+ runTest {
+ val billingName = "Jon Doe"
+ val cardNumber = "4111111111111111"
+ val expiryMonth = 12L
+ val expiryYear = 2028L
+ val cardType = "amex"
+
+ val creditCardEntry = CreditCardEntry(
+ name = billingName,
+ number = cardNumber,
+ expiryMonth = expiryMonth.toString(),
+ expiryYear = expiryYear.toString(),
+ cardType = cardType,
+ )
+
+ val creditCard = storage.addCreditCard(
+ NewCreditCardFields(
+ billingName = billingName,
+ plaintextCardNumber = CreditCardNumber.Plaintext(cardNumber),
+ cardNumberLast4 = "1111",
+ expiryMonth = expiryMonth,
+ expiryYear = expiryYear,
+ cardType = cardType,
+ ),
+ )
+ doReturn(CreditCardValidationDelegate.Result.CanBeUpdated(creditCard)).`when`(validationDelegate).shouldCreateOrUpdate(creditCardEntry)
+
+ delegate.onCreditCardSave(
+ creditCardEntry,
+ )
+
+ verify(storage, times(1)).updateCreditCard(
+ guid = creditCard.guid,
+ creditCardFields = UpdatableCreditCardFields(
+ billingName = billingName,
+ cardNumber = CreditCardNumber.Plaintext("4111111111111111"),
+ cardNumberLast4 = "4111111111111111".last4Digits(),
+ expiryMonth = expiryMonth,
+ expiryYear = expiryYear,
+ cardType = cardType,
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN an invalid credit card entry WHEN onCreditCardSave is called THEN the request is ignored`() {
+ runTest {
+ val billingName = "Jon Doe"
+ val cardNumber = ""
+ val expiryMonth = ""
+ val expiryYear = ""
+ val cardType = "amex"
+
+ val creditCardEntry = CreditCardEntry(
+ name = billingName,
+ number = cardNumber,
+ expiryMonth = expiryMonth,
+ expiryYear = expiryYear,
+ cardType = cardType,
+ )
+
+ delegate.onCreditCardSave(
+ creditCardEntry,
+ )
+
+ verify(storage, times(0)).addCreditCard(any())
+ verify(storage, times(0)).updateCreditCard(any(), any())
+ }
+ }
+
+ @Test
+ fun `GIVEN address autofill is enabled WHEN onAddressesFetch is called THEN it returns all stored addresses`() =
+ runTest {
+ val storage: AutofillCreditCardsAddressesStorage = mock()
+ val storedAddresses = listOf<Address>(mock(), mock())
+ doReturn(storedAddresses).`when`(storage).getAllAddresses()
+ delegate = GeckoCreditCardsAddressesStorageDelegate(lazy { storage }, scope, isAddressAutofillEnabled = { true })
+
+ val result = delegate.onAddressesFetch()
+
+ verify(storage, times(1)).getAllAddresses()
+ assertEquals(storedAddresses, result)
+ }
+
+ @Test
+ fun `GIVEN address autofill is disabled WHEN onAddressesFetch is called THEN it returns an empty list of addresses`() =
+ runTest {
+ val storage: AutofillCreditCardsAddressesStorage = mock()
+ val storedCards = listOf<CreditCard>(mock())
+ doReturn(storedCards).`when`(storage).getAllCreditCards()
+ delegate = GeckoCreditCardsAddressesStorageDelegate(lazy { storage }, scope, isAddressAutofillEnabled = { false })
+
+ val result = delegate.onAddressesFetch()
+
+ verify(storage, never()).getAllAddresses()
+ assertEquals(emptyList<CreditCard>(), result)
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/sync-autofill/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..ca6ee9cea8
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline \ No newline at end of file
diff --git a/mobile/android/android-components/components/service/sync-autofill/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/sync-autofill/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-autofill/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/service/sync-logins/README.md b/mobile/android/android-components/components/service/sync-logins/README.md
new file mode 100644
index 0000000000..8cf3711bb4
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/README.md
@@ -0,0 +1,76 @@
+# [Android Components](../../../README.md) > Service > Firefox Sync - Logins
+
+A library for integrating with Firefox Sync - Logins.
+
+## Before using this component
+Products sending telemetry and using this component *must request* a data-review following [this process](https://wiki.mozilla.org/Firefox/Data_Collection).
+This component provides data collection using the [Glean SDK](https://mozilla.github.io/glean/book/index.html).
+The list of metrics being collected is available in the [metrics documentation](../../support/sync-telemetry/docs/metrics.md).
+
+## Motivation
+
+The **Firefox Sync - Logins Component** provides a way for Android applications to do the following:
+
+* Retrieve the Logins (url / password) data type from [Firefox Sync](https://www.mozilla.org/en-US/firefox/features/sync/)
+
+## Usage
+
+### Before using this component
+
+The `mozilla.appservices.logins` component collects telemetry using the [Glean SDK](https://mozilla.github.io/glean/).
+Applications that send telemetry via Glean *must ensure* they have received appropriate data-review following
+[the Firefox Data Collection process](https://wiki.mozilla.org/Firefox/Data_Collection) before integrating this component.
+
+Details on the metrics collected by the `mozilla.appservices.logins` component are available
+[here](https://github.com/mozilla/application-services/tree/main/docs/metrics/logins/metrics.md)
+
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
+
+```
+implementation "org.mozilla.components:service-sync-logins:{latest-version}"
+```
+
+You will also need the Firefox Accounts component to be able to obtain the keys to decrypt the Logins data:
+
+```
+implementation "org.mozilla.components:fxa:{latest-version}
+```
+
+### Known Issues
+
+* Android 6.0 is temporary not supported and will probably crash the application.
+
+## API
+
+This implements the login-related interfaces from `mozilla.components.concept.storage`.
+
+## FAQ
+
+### Which exceptions do I need to handle?
+
+It depends, but probably only `SyncAuthInvalid`, but potentially `IncorrectKey`.
+
+- You need to handle `SyncAuthInvalid`. You can do this by refreshing the FxA authentication (you should only do this once, and not in e.g. a loop). Most/All consumers will need to do this.
+
+- `IncorrectKey`: If you're sure the key you have used is valid, the only way to handle this is likely to delete the file containing the database (as the data is unreadable without the key). On the bright side, for sync users it should all be pulled down on the next sync.
+
+- `NoSuchRecord`, `InvalidRecord` all indicate problems with either your code or the arguments given to various functions. You may trigger and handle these if you like (it may be more convenient in some scenarios), but code that wishes to completely avoid them should be able to.
+
+The errors reported as "raw" `LoginsApiException` are things like Rust panics, errors reported by OpenSSL or SQLcipher, corrupt data on the server (things that are not JSON after decryption), bugs in our code, etc. You don't need to handle these, and it would likely be beneficial (but of course not necessary) to report them via some sort of telemetry, if any is available.
+
+### Can I use an in-memory SQLcipher connection with `DatabaseLoginsStorage`?
+
+Just create a `DatabaseLoginsStorage` with the path `:memory:`, and it will work. You may also use a [SQLite URI filename](https://www.sqlite.org/uri.html) with the parameter `mode=memory`. See https://www.sqlite.org/inmemorydb.html for more options and further information.
+
+### What's `wipeLocal`?
+
+`wipeLocal` deletes all local data from the database, bringing us back to the state prior to the first local write (or sync). That is, it returns it to an empty database.
+
+## 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/service/sync-logins/build.gradle b/mobile/android/android-components/components/service/sync-logins/build.gradle
new file mode 100644
index 0000000000..6a80afa113
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/build.gradle
@@ -0,0 +1,53 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ consumerProguardFiles 'proguard-rules-consumer.pro'
+ }
+ }
+
+ namespace 'mozilla.components.browser.storage.sync.logins'
+}
+
+dependencies {
+ // Parts of this dependency are typealiase'd or are otherwise part of this module's public API.
+ api (ComponentsDependencies.mozilla_appservices_logins) {
+ // Use our own version of the Glean dependency,
+ // which might be different from the version declared by A-S.
+ exclude group: 'org.mozilla.components', module: 'service-glean'
+ }
+ api ComponentsDependencies.mozilla_appservices_sync15
+
+ // Types defined in concept-sync are part of this module's public API.
+ api project(':concept-sync')
+ api project(':lib-dataprotect')
+
+ implementation project(':concept-storage')
+ implementation project(':support-utils')
+ implementation project(':service-glean')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+}
+
+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/service/sync-logins/proguard-rules-consumer.pro b/mobile/android/android-components/components/service/sync-logins/proguard-rules-consumer.pro
new file mode 100644
index 0000000000..d3456cd17e
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/proguard-rules-consumer.pro
@@ -0,0 +1 @@
+# ProGuard rules for consumers of this library.
diff --git a/mobile/android/android-components/components/service/sync-logins/proguard-rules.pro b/mobile/android/android-components/components/service/sync-logins/proguard-rules.pro
new file mode 100644
index 0000000000..50e2b38a97
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/sebastian/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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/service/sync-logins/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/sync-logins/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..816719811c
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/DefaultLoginValidationDelegate.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/DefaultLoginValidationDelegate.kt
new file mode 100644
index 0000000000..96baa7d646
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/DefaultLoginValidationDelegate.kt
@@ -0,0 +1,46 @@
+/* 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.service.sync.logins
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.async
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.concept.storage.LoginValidationDelegate
+import mozilla.components.concept.storage.LoginValidationDelegate.Result
+import mozilla.components.concept.storage.LoginsStorage
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A delegate that will check against [storage] to see if a given Login can be persisted, and return
+ * information about why it can or cannot.
+ */
+class DefaultLoginValidationDelegate(
+ private val storage: Lazy<LoginsStorage>,
+ private val scope: CoroutineScope = CoroutineScope(IO),
+ private val crashReporting: CrashReporting? = null,
+) : LoginValidationDelegate {
+ private val logger = Logger("DefaultAddonUpdater")
+
+ /**
+ * Compares a [Login] to a passed in list of potential dupes [Login] or queries underlying
+ * storage for potential dupes list of [Login] to determine if it should be updated or created.
+ */
+ override fun shouldUpdateOrCreateAsync(entry: LoginEntry): Deferred<Result> {
+ return scope.async {
+ val foundLogin = try {
+ storage.value.findLoginToUpdate(entry)
+ } catch (e: LoginsApiException) {
+ logger.warn("Failure in shouldUpdateOrCreateAsync: $e")
+ crashReporting?.submitCaughtException(e)
+ null
+ }
+ if (foundLogin == null) Result.CanBeCreated else Result.CanBeUpdated(foundLogin)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt
new file mode 100644
index 0000000000..892930e28d
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt
@@ -0,0 +1,69 @@
+/* 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.service.sync.logins
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.concept.storage.LoginStorageDelegate
+import mozilla.components.concept.storage.LoginsStorage
+
+/**
+ * [LoginStorageDelegate] implementation.
+ *
+ * An abstraction that handles the persistence and retrieval of [LoginEntry]s so that Gecko doesn't
+ * have to.
+ *
+ * In order to use this class, attach it to the active [GeckoRuntime] as its `loginStorageDelegate`.
+ * It is not designed to work with other engines.
+ *
+ * This class is part of a complex flow integrating Gecko and Application Services code, which is
+ * described here:
+ *
+ * - GV finds something on a page that it believes could be autofilled
+ * - GV calls [onLoginFetch]
+ * - We retrieve all [Login]s with matching domains (if any) from [loginStorage]
+ * - We return these [Login]s to GV
+ * - GV autofills one of the returned [Login]s into the page
+ * - GV calls [onLoginUsed] to let us know which [Login] was used
+ * - User submits their credentials
+ * - GV detects something that looks like a login submission
+ * - ([GeckoLoginStorageDelegate] is not involved with this step)
+ * `SaveLoginDialogFragment` is shown to the user, who decides whether or not
+ * to save the [LoginEntry] and gives them a chance to manually adjust the
+ * username/password fields.
+ * - `SaveLoginDialogFragment` uses `DefaultLoginValidationDelegate` to determine
+ * what the result of the operation will be: saving a new login,
+ * updating an existing login, or filling in a blank username.
+ * - If the user accepts: GV calls [onLoginSave] with the [LoginEntry]
+ */
+class GeckoLoginStorageDelegate(
+ private val loginStorage: Lazy<LoginsStorage>,
+ private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+) : LoginStorageDelegate {
+
+ override fun onLoginUsed(login: Login) {
+ scope.launch {
+ loginStorage.value.touch(login.guid)
+ }
+ }
+
+ override fun onLoginFetch(domain: String): Deferred<List<Login>> {
+ return scope.async {
+ loginStorage.value.getByBaseDomain(domain)
+ }
+ }
+
+ @Synchronized
+ override fun onLoginSave(login: LoginEntry) {
+ scope.launch {
+ loginStorage.value.addOrUpdate(login)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/LoginsCrypto.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/LoginsCrypto.kt
new file mode 100644
index 0000000000..a16b45cac2
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/LoginsCrypto.kt
@@ -0,0 +1,129 @@
+/* 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.service.sync.logins
+
+import android.content.Context
+import android.content.SharedPreferences
+import mozilla.appservices.logins.KeyRegenerationEventReason
+import mozilla.appservices.logins.checkCanary
+import mozilla.appservices.logins.createCanary
+import mozilla.appservices.logins.decryptFields
+import mozilla.appservices.logins.recordKeyRegenerationEvent
+import mozilla.components.concept.storage.EncryptedLogin
+import mozilla.components.concept.storage.KeyGenerationReason
+import mozilla.components.concept.storage.KeyManager
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.ManagedKey
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+
+/**
+ * A class that knows how to encrypt & decrypt strings, backed by application-services' logins lib.
+ * Used for protecting usernames/passwords at rest.
+ *
+ * This class manages creation and storage of the encryption key.
+ * It also keeps track of abnormal events, such as managed key going missing or getting corrupted.
+ *
+ * @param context [Context] used for obtaining [SharedPreferences] for managing internal prefs.
+ * @param securePrefs A [SecureAbove22Preferences] instance used for storing the managed key.
+ */
+class LoginsCrypto(
+ private val context: Context,
+ private val securePrefs: SecureAbove22Preferences,
+ private val storage: SyncableLoginsStorage,
+) : KeyManager() {
+ private val plaintextPrefs by lazy { context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }
+
+ override suspend fun recoverFromKeyLoss(reason: KeyGenerationReason.RecoveryNeeded) {
+ val telemetryEventReason = when (reason) {
+ is KeyGenerationReason.RecoveryNeeded.Lost -> KeyRegenerationEventReason.Lost
+ is KeyGenerationReason.RecoveryNeeded.Corrupt -> KeyRegenerationEventReason.Corrupt
+ is KeyGenerationReason.RecoveryNeeded.AbnormalState -> KeyRegenerationEventReason.Other
+ }
+ recordKeyRegenerationEvent(telemetryEventReason)
+ storage.conn.getStorage().wipeLocal()
+ }
+
+ override fun getStoredCanary(): String? {
+ return plaintextPrefs.getString(CANARY_PHRASE_CIPHERTEXT_KEY, null)
+ }
+
+ override fun getStoredKey(): String? {
+ return securePrefs.getString(LOGINS_KEY)
+ }
+
+ override fun storeKeyAndCanary(key: String) {
+ // To consider: should this be a non-destructive operation, just in case?
+ // e.g. if we thought we lost the key, but actually did not, that would let us recover data later on.
+ // otherwise, if we mess up and override a perfectly good key, the data is gone for good.
+ securePrefs.putString(LOGINS_KEY, key)
+ // To detect key corruption or absence, use the newly generated key to encrypt a known string.
+ // See isKeyValid below.
+ plaintextPrefs
+ .edit()
+ .putString(CANARY_PHRASE_CIPHERTEXT_KEY, createCanary(CANARY_PHRASE_PLAINTEXT, key))
+ .apply()
+ }
+
+ override fun createKey(): String {
+ return mozilla.appservices.logins.createKey()
+ }
+
+ override fun isKeyRecoveryNeeded(rawKey: String, canary: String): KeyGenerationReason.RecoveryNeeded? {
+ return try {
+ if (checkCanary(canary, CANARY_PHRASE_PLAINTEXT, rawKey)) {
+ null
+ } else {
+ // A bad key should trigger a IncorrectKey, but check this branch just in case.
+ KeyGenerationReason.RecoveryNeeded.Corrupt
+ }
+ } catch (e: IncorrectKey) {
+ KeyGenerationReason.RecoveryNeeded.Corrupt
+ }
+ }
+
+ /**
+ * Decrypts ciphertext fields within [login], producing a plaintext [Login].
+ */
+ suspend fun decryptLogin(login: EncryptedLogin): Login {
+ return decryptLogin(login, getOrGenerateKey())
+ }
+
+ /**
+ * Decrypts ciphertext fields within [login], producing a plaintext [Login].
+ *
+ * This version inputs a ManagedKey. Use this for operations that
+ * decrypt multiple logins to avoid constructing the key multiple times.
+ */
+ fun decryptLogin(login: EncryptedLogin, key: ManagedKey): Login {
+ val secFields = decryptFields(login.secFields, key.key)
+ // Note: The autofill code catches errors on decryptFields and returns
+ // null, but it's not as easy to recover in this case since the code
+ // almost certainly going to need to a [Login], so we just throw in
+ // that case. Decryption errors shouldn't be happen as long as the
+ // canary checking code below is working correctly
+
+ return Login(
+ guid = login.guid,
+ origin = login.origin,
+ username = secFields.username,
+ password = secFields.password,
+ formActionOrigin = login.formActionOrigin,
+ httpRealm = login.httpRealm,
+ usernameField = login.usernameField,
+ passwordField = login.passwordField,
+ timesUsed = login.timesUsed,
+ timeCreated = login.timeCreated,
+ timeLastUsed = login.timeLastUsed,
+ timePasswordChanged = login.timePasswordChanged,
+ )
+ }
+
+ companion object {
+ const val PREFS_NAME = "loginsCrypto"
+ const val LOGINS_KEY = "loginsKey"
+ const val CANARY_PHRASE_CIPHERTEXT_KEY = "canaryPhrase"
+ const val CANARY_PHRASE_PLAINTEXT = "a string for checking validity of the key"
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt
new file mode 100644
index 0000000000..8eafd25aab
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt
@@ -0,0 +1,280 @@
+/* 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.service.sync.logins
+
+import android.content.Context
+import androidx.annotation.GuardedBy
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import mozilla.appservices.logins.DatabaseLoginsStorage
+import mozilla.components.concept.storage.EncryptedLogin
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.concept.storage.LoginsStorage
+import mozilla.components.concept.sync.SyncableStore
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.logElapsedTime
+import java.io.Closeable
+
+// Current database
+const val DB_NAME = "logins2.sqlite"
+
+// Name of our preferences file
+const val PREFS_NAME = "logins"
+
+// SQLCipher migration status.
+// - 0 / unset: We haven't done the SQLCipher migration
+// - 1: We performed v1 of the SQLCipher migration
+//
+// We no longer migrate SQLCipher, if the user hasn't
+// successfully migrated - we delete the DB
+const val SQL_CIPHER_MIGRATION = "sql-cipher-migration"
+
+/**
+ * The telemetry ping from a successful sync
+ */
+typealias SyncTelemetryPing = mozilla.appservices.sync15.SyncTelemetryPing
+
+/**
+ * The base class of all errors emitted by logins storage.
+ *
+ * Concrete instances of this class are thrown for operations which are
+ * not expected to be handled in a meaningful way by the application.
+ *
+ * For example, caught Rust panics, SQL errors, failure to generate secure
+ * random numbers, etc. are all examples of things which will result in a
+ * concrete `LoginsApiException`.
+ */
+typealias LoginsApiException = mozilla.appservices.logins.LoginsApiException
+
+/**
+ * This indicates that the authentication information (e.g. the [SyncUnlockInfo])
+ * provided to [AsyncLoginsStorage.sync] is invalid. This often indicates that it's
+ * stale and should be refreshed with FxA (however, care should be taken not to
+ * get into a loop refreshing this information).
+ */
+typealias SyncAuthInvalidException = mozilla.appservices.logins.LoginsApiException.SyncAuthInvalid
+
+/**
+ * This is thrown if `update()` is performed with a record whose GUID
+ * does not exist.
+ */
+typealias NoSuchRecordException = mozilla.appservices.logins.LoginsApiException.NoSuchRecord
+
+/**
+ * This is thrown on attempts to insert or update a record so that it
+ * is no longer valid, where "invalid" is defined as such:
+ *
+ * - A record with a blank `password` is invalid.
+ * - A record with a blank `hostname` is invalid.
+ * - A record that doesn't have a `formSubmitURL` nor a `httpRealm` is invalid.
+ * - A record that has both a `formSubmitURL` and a `httpRealm` is invalid.
+ */
+typealias InvalidRecordException = mozilla.appservices.logins.LoginsApiException.InvalidRecord
+
+/**
+ * Error encrypting/decrypting logins data
+ */
+typealias IncorrectKey = mozilla.appservices.logins.LoginsApiException.IncorrectKey
+
+/**
+ * Implements [LoginsStorage] and [SyncableStore] using the application-services logins library.
+ *
+ * Synchronization is handled via the SyncManager by calling [registerWithSyncManager]
+ */
+class SyncableLoginsStorage(
+ private val context: Context,
+ private val securePrefs: Lazy<SecureAbove22Preferences>,
+) : LoginsStorage, SyncableStore, AutoCloseable {
+ private val plaintextPrefs by lazy { context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }
+ private val logger = Logger("SyncableLoginsStorage")
+ private val coroutineContext by lazy { Dispatchers.IO }
+ val crypto by lazy { LoginsCrypto(context, securePrefs.value, this) }
+
+ internal val conn by lazy {
+ // We do not migrate SQLCipher anymore, we should delete it if it exists
+ runBlocking(coroutineContext) {
+ deleteSQLCipherDBIfNeeded()
+ }
+ LoginStorageConnection.init(dbPath = context.getDatabasePath(DB_NAME).absolutePath)
+ LoginStorageConnection
+ }
+
+ /**
+ * "Warms up" this storage layer by establishing the database connection.
+ */
+ suspend fun warmUp() = withContext(coroutineContext) {
+ logElapsedTime(logger, "Warming up storage") { conn }
+ Unit
+ }
+
+ /**
+ * @throws [LoginsApiException] if the storage is locked, and on unexpected
+ * errors (IO failure, rust panics, etc)
+ */
+ @Throws(LoginsApiException::class)
+ override suspend fun wipeLocal() = withContext(coroutineContext) {
+ conn.getStorage().wipeLocal()
+ }
+
+ /**
+ * @throws [LoginsApiException] if the storage is locked, and on unexpected
+ * errors (IO failure, rust panics, etc)
+ */
+ @Throws(LoginsApiException::class)
+ override suspend fun delete(guid: String): Boolean = withContext(coroutineContext) {
+ conn.getStorage().delete(guid)
+ }
+
+ /**
+ * @throws [LoginsApiException] if the storage is locked, and on unexpected
+ * errors (IO failure, rust panics, etc)
+ */
+ @Throws(LoginsApiException::class)
+ override suspend fun get(guid: String): Login? = withContext(coroutineContext) {
+ conn.getStorage().get(guid)?.toEncryptedLogin()?.let { crypto.decryptLogin(it) }
+ }
+
+ /**
+ * @throws [NoSuchRecordException] if the login does not exist.
+ * @throws [LoginsApiException] if the storage is locked, and on unexpected
+ * errors (IO failure, rust panics, etc)
+ */
+ @Throws(NoSuchRecordException::class, LoginsApiException::class)
+ override suspend fun touch(guid: String) = withContext(coroutineContext) {
+ conn.getStorage().touch(guid)
+ }
+
+ /**
+ * @throws [LoginsApiException] if the storage is locked, and on unexpected
+ * errors (IO failure, rust panics, etc)
+ */
+ @Throws(LoginsApiException::class)
+ override suspend fun list(): List<Login> = withContext(coroutineContext) {
+ val key = crypto.getOrGenerateKey()
+ conn.getStorage().list().map { crypto.decryptLogin(it.toEncryptedLogin(), key) }
+ }
+
+ /**
+ * @throws [InvalidRecordException] if the record is invalid.
+ * @throws [IncorrectKey] if the encryption key can't decrypt the login
+ * @throws [LoginsApiException] if the storage is locked, and on unexpected
+ * errors (IO failure, rust panics, etc)
+ */
+ @Throws(IncorrectKey::class, InvalidRecordException::class, LoginsApiException::class)
+ override suspend fun add(entry: LoginEntry) = withContext(coroutineContext) {
+ conn.getStorage().add(entry.toLoginEntry(), crypto.getOrGenerateKey().key).toEncryptedLogin()
+ }
+
+ /**
+ * @throws [NoSuchRecordException] if the login does not exist.
+ * @throws [IncorrectKey] if the encryption key can't decrypt the login
+ * @throws [InvalidRecordException] if the update would create an invalid record.
+ * @throws [LoginsApiException] if the storage is locked, and on unexpected
+ * errors (IO failure, rust panics, etc)
+ */
+ @Throws(
+ IncorrectKey::class,
+ NoSuchRecordException::class,
+ InvalidRecordException::class,
+ LoginsApiException::class,
+ )
+ override suspend fun update(guid: String, entry: LoginEntry) = withContext(coroutineContext) {
+ conn.getStorage().update(guid, entry.toLoginEntry(), crypto.getOrGenerateKey().key).toEncryptedLogin()
+ }
+
+ /**
+ * @throws [InvalidRecordException] if the update would create an invalid record.
+ * @throws [IncorrectKey] if the encryption key can't decrypt the login
+ * @throws [LoginsApiException] if the storage is locked, and on unexpected
+ * errors (IO failure, rust panics, etc)
+ */
+ @Throws(IncorrectKey::class, InvalidRecordException::class, LoginsApiException::class)
+ override suspend fun addOrUpdate(entry: LoginEntry) = withContext(coroutineContext) {
+ conn.getStorage().addOrUpdate(entry.toLoginEntry(), crypto.getOrGenerateKey().key).toEncryptedLogin()
+ }
+
+ override fun registerWithSyncManager() {
+ conn.getStorage().registerWithSyncManager()
+ }
+
+ /**
+ * @throws [LoginsApiException] On unexpected errors (IO failure, rust panics, etc)
+ */
+ @Throws(LoginsApiException::class)
+ override suspend fun getByBaseDomain(origin: String): List<Login> = withContext(coroutineContext) {
+ val key = crypto.getOrGenerateKey()
+ conn.getStorage().getByBaseDomain(origin).map { crypto.decryptLogin(it.toEncryptedLogin(), key) }
+ }
+
+ /**
+ * @throws [IncorrectKey] if the encryption key can't decrypt the login
+ * @throws [LoginsApiException] On unexpected errors (IO failure, rust panics, etc)
+ */
+ @Throws(LoginsApiException::class)
+ override suspend fun findLoginToUpdate(entry: LoginEntry): Login? = withContext(coroutineContext) {
+ conn.getStorage().findLoginToUpdate(entry.toLoginEntry(), crypto.getOrGenerateKey().key)?.toLogin()
+ }
+
+ /**
+ * @throws [IncorrectKey] if the encryption key can't decrypt the login
+ */
+ override suspend fun decryptLogin(login: EncryptedLogin) = crypto.decryptLogin(login)
+
+ override fun close() {
+ coroutineContext.cancel()
+ conn.close()
+ }
+
+ /*
+ * We not longer migrate SQLCipher, we delete the DB, key and any
+ * associated prefs
+ */
+ private suspend fun deleteSQLCipherDBIfNeeded() {
+ // Older database that was encrypted using SQLCipher
+ val dbNameSqlCipher = "logins.sqlite"
+ // Prefs key that we stored the old SQLCipher encryption key
+ val encryptionKeySqlCipher = "passwords"
+
+ val version = plaintextPrefs.getInt(SQL_CIPHER_MIGRATION, 0)
+ if (version == 0) {
+ // Older database that was encrypted using SQLCipher, majority of clients won't
+ // have anything to actually delete
+ securePrefs.value.remove(encryptionKeySqlCipher)
+ val file = context.getDatabasePath(dbNameSqlCipher)
+ file.delete()
+ }
+ plaintextPrefs.edit().putInt(SQL_CIPHER_MIGRATION, 1).apply()
+ }
+}
+
+/**
+ * A singleton wrapping a [LoginsStorage] connection.
+ */
+internal object LoginStorageConnection : Closeable {
+ @GuardedBy("this")
+ private var storage: DatabaseLoginsStorage? = null
+
+ internal fun init(dbPath: String = DB_NAME) = synchronized(this) {
+ if (storage == null) {
+ storage = DatabaseLoginsStorage(dbPath)
+ }
+ storage
+ }
+
+ internal fun getStorage(): DatabaseLoginsStorage = synchronized(this) {
+ check(storage != null) { "must call init first" }
+ return storage!!
+ }
+
+ override fun close() = synchronized(this) {
+ check(storage != null) { "must call init first" }
+ storage!!.close()
+ storage = null
+ }
+}
diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/Types.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/Types.kt
new file mode 100644
index 0000000000..7141610251
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/Types.kt
@@ -0,0 +1,87 @@
+/* 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.service.sync.logins
+
+import mozilla.components.concept.storage.EncryptedLogin
+import mozilla.components.concept.storage.Login
+import mozilla.components.concept.storage.LoginEntry
+
+// Convert between application-services data classes and the ones in concept.storage.
+
+/**
+ * Convert A-S EncryptedLogin into A-C [EncryptedLogin].
+ */
+fun mozilla.appservices.logins.EncryptedLogin.toEncryptedLogin() = EncryptedLogin(
+ guid = record.id,
+ origin = fields.origin,
+ formActionOrigin = fields.formActionOrigin,
+ httpRealm = fields.httpRealm,
+ usernameField = fields.usernameField,
+ passwordField = fields.passwordField,
+ timesUsed = record.timesUsed,
+ timeCreated = record.timeCreated,
+ timeLastUsed = record.timeLastUsed,
+ timePasswordChanged = record.timePasswordChanged,
+ secFields = secFields,
+)
+
+/**
+ * Convert A-S Login into A-C [Login].
+ */
+fun mozilla.appservices.logins.Login.toLogin() = Login(
+ guid = record.id,
+ origin = fields.origin,
+ username = secFields.username,
+ password = secFields.password,
+ formActionOrigin = fields.formActionOrigin,
+ httpRealm = fields.httpRealm,
+ usernameField = fields.usernameField,
+ passwordField = fields.passwordField,
+ timesUsed = record.timesUsed,
+ timeCreated = record.timeCreated,
+ timeLastUsed = record.timeLastUsed,
+ timePasswordChanged = record.timePasswordChanged,
+)
+
+/**
+ * Convert A-C [LoginEntry] into A-S LoginEntry.
+ */
+fun LoginEntry.toLoginEntry() = mozilla.appservices.logins.LoginEntry(
+ fields = mozilla.appservices.logins.LoginFields(
+ origin = origin,
+ formActionOrigin = formActionOrigin,
+ httpRealm = httpRealm,
+ usernameField = usernameField,
+ passwordField = passwordField,
+ ),
+ secFields = mozilla.appservices.logins.SecureLoginFields(
+ username = username,
+ password = password,
+ ),
+)
+
+/**
+ * Convert A-C [Login] into A-S Login.
+ */
+fun Login.toLogin() = mozilla.appservices.logins.Login(
+ record = mozilla.appservices.logins.RecordFields(
+ id = guid,
+ timesUsed = timesUsed,
+ timeCreated = timeCreated,
+ timeLastUsed = timeLastUsed,
+ timePasswordChanged = timePasswordChanged,
+ ),
+ fields = mozilla.appservices.logins.LoginFields(
+ origin = origin,
+ formActionOrigin = formActionOrigin,
+ httpRealm = httpRealm,
+ usernameField = usernameField,
+ passwordField = passwordField,
+ ),
+ secFields = mozilla.appservices.logins.SecureLoginFields(
+ username = username,
+ password = password,
+ ),
+)
diff --git a/mobile/android/android-components/components/service/sync-logins/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/sync-logins/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/sync-logins/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/android-test/README.md b/mobile/android/android-components/components/support/android-test/README.md
new file mode 100644
index 0000000000..c04754373d
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Android Test
+
+A collection of helpers for testing components in instrumented (on device) tests (`src/androidTest`)
+
+## Usage
+
+### 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:support-android-test:{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/support/android-test/build.gradle b/mobile/android/android-components/components/support/android-test/build.gradle
new file mode 100644
index 0000000000..a1e198c753
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/build.gradle
@@ -0,0 +1,46 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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')
+ }
+ }
+
+ lint {
+ // Disabled since this caused issues with Android Gradle Plugin 3.2.1+ (NullPointerException:InvalidPackageDetector)
+ tasks.lint.enabled = false
+
+ lintConfig file("lint.xml")
+ }
+
+ namespace 'mozilla.components.support.android.test'
+
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_arch_core_common
+ implementation ComponentsDependencies.androidx_espresso_core
+ implementation ComponentsDependencies.androidx_test_core
+ implementation ComponentsDependencies.testing_leakcanary
+ implementation ComponentsDependencies.testing_mockwebserver
+}
+
+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/support/android-test/lint.xml b/mobile/android/android-components/components/support/android-test/lint.xml
new file mode 100644
index 0000000000..3c7a84d2fb
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/lint.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<lint>
+ <issue id="InvalidPackage">
+ <ignore path="**/byte-buddy-agent-*.jar"/>
+ <ignore path="**/bcprov-*on-*.jar"/>
+ <ignore path="**/shadows-framework-*.jar"/>
+ <ignore path="**/xstream-*.jar"/>
+ <ignore path="**/resources-4.0-*.jar"/>
+ </issue>
+</lint>
diff --git a/mobile/android/android-components/components/support/android-test/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/android-test/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/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/support/android-test/src/main/java/mozilla/components/support/android/test/Matchers.kt b/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/Matchers.kt
new file mode 100644
index 0000000000..873fe13ffe
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/Matchers.kt
@@ -0,0 +1,26 @@
+/* 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.support.android.test
+
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.Matcher
+
+/**
+ * A collection of non-domain specific [Matcher]s.
+ */
+object Matchers {
+
+ /**
+ * Conditionally applies the [not] matcher based on the given argument: inverts the matcher when
+ * [useUnmodifiedMatcher] is false, otherwise returns the matcher unmodified.
+ *
+ * This allows developers to write code more generically by using a boolean argument: e.g. assertIsShown(Boolean)
+ * rather than two methods, assertIsShown() and assertIsNotShown().
+ */
+ fun <T> maybeInvertMatcher(matcher: Matcher<T>, useUnmodifiedMatcher: Boolean): Matcher<T> = when {
+ useUnmodifiedMatcher -> matcher
+ else -> not(matcher)
+ }
+}
diff --git a/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/espresso/ViewInteraction.kt b/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/espresso/ViewInteraction.kt
new file mode 100644
index 0000000000..d11dd74714
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/espresso/ViewInteraction.kt
@@ -0,0 +1,54 @@
+/* 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.support.android.test.espresso
+
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions
+import mozilla.components.support.android.test.espresso.matcher.hasFocus as hasFocusFun
+import mozilla.components.support.android.test.espresso.matcher.isChecked as isCheckedFun
+import mozilla.components.support.android.test.espresso.matcher.isDisplayed as isDisplayedFun
+import mozilla.components.support.android.test.espresso.matcher.isEnabled as isEnabledFun
+import mozilla.components.support.android.test.espresso.matcher.isSelected as isSelectedFun
+
+/**
+ * Shorthand to [ViewActions.click] the View.
+ */
+fun ViewInteraction.click(): ViewInteraction = this.perform(ViewActions.click())!!
+
+/**
+ * Asserts the View has focus or does not have focus based on the Boolean argument.
+ */
+fun ViewInteraction.assertHasFocus(hasFocus: Boolean): ViewInteraction {
+ return this.check(ViewAssertions.matches(hasFocusFun(hasFocus)))
+}
+
+/**
+ * Asserts the View is checked or is not checked based on the Boolean argument.
+ */
+fun ViewInteraction.assertIsChecked(isChecked: Boolean): ViewInteraction {
+ return this.check(ViewAssertions.matches(isCheckedFun(isChecked)))!!
+}
+
+/**
+ * Asserts the View is displayed or is not displayed based on the Boolean argument.
+ */
+fun ViewInteraction.assertIsDisplayed(isDisplayed: Boolean): ViewInteraction {
+ return this.check(ViewAssertions.matches(isDisplayedFun(isDisplayed)))!!
+}
+
+/**
+ * Asserts the View is enabled or is not enabled based on the Boolean argument.
+ */
+fun ViewInteraction.assertIsEnabled(isEnabled: Boolean): ViewInteraction {
+ return this.check(ViewAssertions.matches(isEnabledFun(isEnabled)))!!
+}
+
+/**
+ * Asserts the View is selected or is not selected based on the Boolean argument.
+ */
+fun ViewInteraction.assertIsSelected(isSelected: Boolean): ViewInteraction {
+ return this.check(ViewAssertions.matches(isSelectedFun(isSelected)))!!
+}
diff --git a/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/espresso/matcher/ViewMatchers.kt b/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/espresso/matcher/ViewMatchers.kt
new file mode 100644
index 0000000000..1d3aaf801d
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/espresso/matcher/ViewMatchers.kt
@@ -0,0 +1,43 @@
+/* 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.support.android.test.espresso.matcher
+
+import android.view.View
+import mozilla.components.support.android.test.Matchers.maybeInvertMatcher
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.Matcher
+import androidx.test.espresso.matcher.ViewMatchers.hasFocus as espressoHasFocus
+import androidx.test.espresso.matcher.ViewMatchers.isChecked as espressoIsChecked
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed as espressoIsDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isEnabled as espressoIsEnabled
+import androidx.test.espresso.matcher.ViewMatchers.isSelected as espressoIsSelected
+
+// These functions are defined at the top-level so they appear in autocomplete, like the static methods on
+// Android's [ViewMatchers] class.
+
+/**
+ * The existing [espressoHasFocus] function but uses a boolean to invert rather than requiring the [not] matcher.
+ */
+fun hasFocus(hasFocus: Boolean): Matcher<View> = maybeInvertMatcher(espressoHasFocus(), hasFocus)
+
+/**
+ * The existing [espressoIsChecked] function but uses a boolean to invert rather than requiring the [not] matcher.
+ */
+fun isChecked(isChecked: Boolean): Matcher<View> = maybeInvertMatcher(espressoIsChecked(), isChecked)
+
+/**
+ * The existing [espressoIsDisplayed] function but uses a boolean to invert rather than requiring the [not] matcher.
+ */
+fun isDisplayed(isDisplayed: Boolean): Matcher<View> = maybeInvertMatcher(espressoIsDisplayed(), isDisplayed)
+
+/**
+ * The existing [espressoIsEnabled] function but uses a boolean to invert rather than requiring the [not] matcher.
+ */
+fun isEnabled(isEnabled: Boolean): Matcher<View> = maybeInvertMatcher(espressoIsEnabled(), isEnabled)
+
+/**
+ * The existing [espressoIsSelected] function but uses a boolean to invert rather than requiring the [not] matcher.
+ */
+fun isSelected(isSelected: Boolean): Matcher<View> = maybeInvertMatcher(espressoIsSelected(), isSelected)
diff --git a/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/rules/WebserverRule.kt b/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/rules/WebserverRule.kt
new file mode 100644
index 0000000000..c3c58babfe
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/src/main/java/mozilla/components/support/android/test/rules/WebserverRule.kt
@@ -0,0 +1,65 @@
+/* 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.support.android.test.rules
+
+import android.os.Handler
+import android.os.Looper
+import androidx.test.platform.app.InstrumentationRegistry
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import java.io.IOException
+
+/**
+ * A [TestWatcher] junit rule that will serve content from assets in the test package.
+ */
+class WebserverRule : TestWatcher() {
+ private val webserver: MockWebServer = MockWebServer().apply {
+ dispatcher = AndroidAssetDispatcher()
+ }
+
+ fun url(path: String = ""): String {
+ return webserver.url(path).toString()
+ }
+
+ override fun starting(description: Description?) {
+ webserver.start()
+ }
+
+ override fun finished(description: Description?) {
+ webserver.shutdown()
+ }
+}
+
+private const val HTTP_OK = 200
+private const val HTTP_NOT_FOUND = 404
+
+private class AndroidAssetDispatcher : Dispatcher() {
+ private val mainThreadHandler = Handler(Looper.getMainLooper())
+
+ override fun dispatch(request: RecordedRequest): MockResponse {
+ var path = request.path!!.drop(1)
+ if (path.isEmpty() || path.endsWith("/")) {
+ path += "index.html"
+ }
+
+ val assetContents = try {
+ val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
+ assetManager.open(path).use { inputStream ->
+ inputStream.bufferedReader().use { it.readText() }
+ }
+ } catch (e: IOException) { // e.g. file not found.
+ // We're on a background thread so we need to forward the exception to the main thread.
+ mainThreadHandler.postAtFrontOfQueue {
+ throw IllegalStateException("Could not load resource from path: $path", e)
+ }
+ return MockResponse().setResponseCode(HTTP_NOT_FOUND)
+ }
+ return MockResponse().setResponseCode(HTTP_OK).setBody(assetContents)
+ }
+}
diff --git a/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/MatchersTest.kt b/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/MatchersTest.kt
new file mode 100644
index 0000000000..02762238a7
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/MatchersTest.kt
@@ -0,0 +1,27 @@
+/* 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.support.android.test
+
+import mozilla.components.support.android.test.Matchers.maybeInvertMatcher
+import mozilla.components.support.android.test.helpers.assertEqualsMatchers
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.Matchers
+import org.junit.Test
+
+class MatchersTest {
+
+ @Test
+ fun `WHEN maybeInvertMatcher with unmodifiedMatcher THEN an equivalent matcher is returned`() {
+ val expected = Matchers.contains(4)
+ assertEqualsMatchers(expected, maybeInvertMatcher(expected, useUnmodifiedMatcher = true))
+ }
+
+ @Test
+ fun `WHEN maybeInvertMatcher with a modified matcher THEN the inverted matcher is returned`() {
+ val input = Matchers.contains(4)
+ val expected = not(input)
+ assertEqualsMatchers(expected, maybeInvertMatcher(input, useUnmodifiedMatcher = false))
+ }
+}
diff --git a/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/espresso/matcher/ViewMatchersKtTest.kt b/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/espresso/matcher/ViewMatchersKtTest.kt
new file mode 100644
index 0000000000..0ed215ff18
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/espresso/matcher/ViewMatchersKtTest.kt
@@ -0,0 +1,37 @@
+/* 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.support.android.test.espresso.matcher
+
+import android.view.View
+import androidx.test.espresso.matcher.ViewMatchers
+import mozilla.components.support.android.test.helpers.assertEqualsMatchers
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.Matcher
+import org.junit.Test
+
+private val BOOLEAN_VIEW_MATCHER_TO_UNDERLYING_MATCHER: List<Pair<(Boolean) -> Matcher<View>, Matcher<View>>> = listOf(
+ ::hasFocus to ViewMatchers.hasFocus(),
+ ::isChecked to ViewMatchers.isChecked(),
+ ::isDisplayed to ViewMatchers.isDisplayed(),
+ ::isEnabled to ViewMatchers.isEnabled(),
+ ::isSelected to ViewMatchers.isSelected(),
+)
+
+class ViewMatchersKtTest {
+
+ @Test
+ fun `WHEN checking the unmodified ViewMatcher THEN it equals the underlying ViewMatcher`() {
+ BOOLEAN_VIEW_MATCHER_TO_UNDERLYING_MATCHER.forEach { (input, expected) ->
+ assertEqualsMatchers(expected, input(true))
+ }
+ }
+
+ @Test
+ fun `WHEN checking the modified ViewMatcher tHEN it equals the inversion of the underlying ViewMatcher`() {
+ BOOLEAN_VIEW_MATCHER_TO_UNDERLYING_MATCHER.forEach { (input, inversionOfExpected) ->
+ assertEqualsMatchers(not(inversionOfExpected), input(false))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/helpers/Assert.kt b/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/helpers/Assert.kt
new file mode 100644
index 0000000000..20040d7fb5
--- /dev/null
+++ b/mobile/android/android-components/components/support/android-test/src/test/java/mozilla/components/support/android/test/helpers/Assert.kt
@@ -0,0 +1,18 @@
+/* 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.support.android.test.helpers
+
+import org.hamcrest.Matcher
+import org.junit.Assert.assertEquals
+
+/**
+ * An [assertEquals] method for hamcrest [Matcher]s. This is necessary because [Matcher]s do not define a
+ * .equals method: instead, we compare the String description of two matchers to discover if they're equivalent.
+ * This has some gotchas, e.g. functionally `not not Matcher == Matcher` but in a String description, they do not.
+ */
+// We don't name shadow assertEquals because then importing both requires the consumer to rename the imports.
+fun <T> assertEqualsMatchers(expected: Matcher<T>, actual: Matcher<T>) {
+ assertEquals(expected.toString(), actual.toString())
+}
diff --git a/mobile/android/android-components/components/support/base/README.md b/mobile/android/android-components/components/support/base/README.md
new file mode 100644
index 0000000000..caf0b0e967
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/README.md
@@ -0,0 +1,113 @@
+# [Android Components](../../../README.md) > Support > Base
+
+Base or core component containing building blocks and interfaces for other components.
+
+Usually this component never needs to be added to application projects manually. Other components may have a transitive dependency on some of the classes and interfaces in this component.
+
+## Usage
+
+### 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:support-base:{latest-version}"
+```
+
+## Logging
+
+The base component offers helpers for logging for component (and app) code that allows the app to be in control what gets logged and how.
+
+### Setup
+
+A log messages are routed through the `Log` object which doesn't process any logs itself. Instead it forwards the calls to `LogSink` implementations. The base component includes the `AndroidLogSink` class which implements `LogSink` and forwards log messages to Android's system log.
+
+```Kotlin
+// Forward logs to Android's system log.
+// Most apps want to do that at least for debug builds:
+Log.addSink(AndroidLogSink())
+
+// A default tag can be set. This tag will be used whenever no tag is provided when logging a message.
+Log.addSink(AndroidLogSink("MyAwesomeApp"))
+
+// Log a message (See "Logger" section for more convenient ways to log)
+Log.log(tag = "Test", priority = Log.Priority.DEBUG, message = "Hello World!")
+
+// Set the minimum log level. All log messages with a lower level will be ignored.
+Log.logLevel = Log.Priority.WARN
+```
+
+An application can add multiple `LogSink` implementations to save logs to disk, send them to a crash reporting service or display them inside the app.
+
+### Logger
+
+The `Log` class only offers a low-level logging call. The `logger` sub package contains classes that wrap `Log` and provide a more convenient API for logging.
+
+```Kotlin
+class MyClass {
+ // All log calls on this instance will use the tag MyClassLogger
+ val logger = Logger("MyClassLogger")
+
+ fun doSomething() {
+ // Will log a DEBUG message with tag MyClassLogger
+ logger.debug("Hello World")
+ }
+
+ fun couldThrow() {
+ try {
+ // ..
+ } catch (e: IllegalStateException) {
+ // Will log a ERROR message with stack trace and tag MyClassLogger
+ logger.error("Oops!", e)
+ )
+ }
+
+ fun generic() {
+ // You can also use the Logger class directly if no custom tag is needed:
+ Logger.info("Hello World!")
+ }
+}
+```
+
+## Notifications
+
+Android's APIs require a unique `Int` id for showing and cancelling notifications. Agreeing on unique ids over multiple components and app code without any conflicts is hard. For this reason this component contains a `NotificationIds` object that allocates unique, stable ids based on a provided `String` tag.
+
+```kotlin
+// Get a unique id for the provided tag
+val id = NotificationIds.getIdForTag(context, "mozac.my.feature")
+
+// Extension methods for showing and cancelling notifications
+NotificationManagerCompat
+ .from(context)
+ .notify(context, "mozac.my.feature", notification)
+
+NotificationManagerCompat
+ .from(context)
+ .cancel(context, "mozac.my.feature")
+```
+
+## Facts
+
+A `Fact` is a generic "event" that a component emitted.
+
+Facts are not meant to implement application logic based on them. Instead they can be observed as a stream of "user/app events" inside components that can be analyzed or forwarded to external telemetry services.
+
+By default nothing happens with `Fact` instances. An app needs to register a `FactProcessor` that will receive all emitted `Fact` objects.
+
+The base component comes with a `LogFactProcessor` that will print all emitted `Fact` instances to a `Logger`.
+
+```kotlin
+// Either install processors on the Facts object:
+Facts.registerProcessor(LogFactProcessor())
+
+// Or use the extension method:
+LogFactProcessor()
+ .register()
+```
+
+## 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/support/base/build.gradle b/mobile/android/android-components/components/support/base/build.gradle
new file mode 100644
index 0000000000..76918b4c21
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/build.gradle
@@ -0,0 +1,126 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+def generatedSrcDir = new File(layout.buildDirectory.get().asFile, "generated/components/src/main/java")
+
+// This vcsHash functionality is duplicated in publish.gradle.
+def getVcsHash = { ->
+ def stdout = new ByteArrayOutputStream()
+ def stderr = new ByteArrayOutputStream()
+ try {
+ exec {
+ commandLine 'git', 'rev-parse', '--short', 'HEAD'
+ standardOutput = stdout
+ errorOutput = stderr
+ }
+ } catch (Exception e) {
+ exec {
+ commandLine 'hg', 'id', '--id'
+ environment 'HGPLAIN', '1'
+ standardOutput = stdout
+ }
+ }
+ return stdout.toString().trim()
+}
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ def appServicesVersion = ApplicationServicesConfig.version
+ if (gradle.hasProperty("localProperties.branchBuild.application-services.version")) {
+ appServicesVersion = gradle["localProperties.branchBuild.application-services.version"]
+ }
+
+ buildConfigField("String", "LIBRARY_VERSION", "\"" + config.componentsVersion + "\"")
+ buildConfigField("String", "APPLICATION_SERVICES_VERSION", "\"" + appServicesVersion + "\"")
+ buildConfigField("String", "GLEAN_SDK_VERSION", "\"" + Versions.mozilla_glean + "\"")
+ buildConfigField("String", "VCS_HASH", "\"dev build\"")
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+
+ buildConfigField("String", "VCS_HASH", "\"" + getVcsHash() + "\"")
+ }
+ }
+
+ sourceSets {
+ main {
+ java {
+ srcDirs += generatedSrcDir
+ }
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+
+ namespace 'mozilla.components.support.base'
+
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_lifecycle_viewmodel
+
+ api project(":concept-base")
+ // We expose the app-compat as API so that consumers get access to the Lifecycle classes automatically
+ api ComponentsDependencies.androidx_appcompat
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+}
+
+
+preBuild.finalizedBy("generateComponentEnum")
+
+
+/**
+ * Generates a "Components" enum listing all published components.
+ */
+tasks.register("generateComponentEnum") {
+ doLast {
+ generatedSrcDir.mkdirs()
+
+ def file = new File(generatedSrcDir, "Component.kt")
+ file.delete()
+ file.createNewFile()
+
+ file << "package mozilla.components.support.base" << "\n"
+ file << "\n"
+ file << "// Automatically generated file. DO NOT MODIFY" << "\n"
+ file << "\n"
+ file << "enum class Component {" << "\n"
+
+ file << buildConfig.projects.findAll { project ->
+ project.value.publish
+ }.collect { project ->
+ " " + project.key.replace("-", "_").toUpperCase(Locale.US)
+ }.join(", \n")
+
+ file << "\n"
+ file << "}\n"
+ file << "\n"
+ }
+}
+
+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/support/base/proguard-rules.pro b/mobile/android/android-components/components/support/base/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/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/support/base/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/base/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..f353e19cc2
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+</manifest>
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/Build.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/Build.kt
new file mode 100644
index 0000000000..28e6a25aee
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/Build.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
+
+import mozilla.components.support.base.BuildConfig
+
+/**
+ * Information about the current Android Components build.
+ */
+object Build {
+ /**
+ * The version name of this Android Components release (e.g. 0.54.0 or 0.55.0-SNAPSHOT).
+ */
+ const val version: String = BuildConfig.LIBRARY_VERSION
+
+ /**
+ * The version of "Application Services" libraries this version was *build* against.
+ *
+ * Note that a consuming app may be able to override the actual version that is used at runtime.
+ */
+ const val applicationServicesVersion: String = BuildConfig.APPLICATION_SERVICES_VERSION
+
+ /**
+ * The version of the "Glean SDK" library this version was *build* against.
+ */
+ const val gleanSdkVersion: String = BuildConfig.GLEAN_SDK_VERSION
+
+ /**
+ * Git or hg hash of the latest commit in the Android Components repository checkout this version was built from.
+ */
+ const val gitHash: String = BuildConfig.VCS_HASH
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/Clock.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/Clock.kt
new file mode 100644
index 0000000000..abed593974
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/Clock.kt
@@ -0,0 +1,71 @@
+/* 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.support.base.android
+
+import android.os.SystemClock
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.base.log.logger.Logger
+import java.lang.RuntimeException
+
+private val logger = Logger("Clock")
+
+/**
+ * A wrapper around [SystemClock] and other time-related APIs. Allows mocking the underlying
+ * behavior in tests.
+ */
+object Clock {
+ @VisibleForTesting
+ var delegate: Delegate = createDefaultDelegate()
+
+ /**
+ * Returns milliseconds since boot, including time spent in sleep.
+ */
+ fun elapsedRealtime(): Long = delegate.elapsedRealtime()
+
+ /**
+ * Interface for actual clock implementations that [Clock] will delegate to.
+ */
+ interface Delegate {
+ /**
+ * Returns milliseconds since boot, including time spent in sleep.
+ */
+ fun elapsedRealtime(): Long
+ }
+
+ @VisibleForTesting
+ fun reset() {
+ delegate = createDefaultDelegate()
+ }
+}
+
+@Suppress("TooGenericExceptionCaught")
+private fun createDefaultDelegate(): Clock.Delegate {
+ // If android.os.SystemClock is available we will delegate `Clock` calls to it. Otherwise we
+ // fallback to using `DummyClockDelegate`. This allows us to not have to mock anything in unit
+ // tests that run on a JVM without Android stdlib available. If needed, tests can replace the
+ // whole delegate with a custom (mock) implementation.
+ return try {
+ Class.forName("android.os.SystemClock")
+ SystemClock.elapsedRealtime()
+ AndroidClockDelegate()
+ } catch (e: ClassNotFoundException) {
+ // If android.os.SystemClock is not available then use the dummy clock.
+ logger.info("android.os.SystemClock not available, using DummyClockDelegate")
+ DummyClockDelegate()
+ } catch (e: RuntimeException) {
+ logger.info("SystemClock throws RuntimeException, using DummyClockDelegate")
+ // If calling elapsedRealtime on SystemClock throws a RuntimeException (as done by the
+ // android.jar file loaded in unit tests) then use the dummy clock.
+ DummyClockDelegate()
+ }
+}
+
+private class AndroidClockDelegate : Clock.Delegate {
+ override fun elapsedRealtime() = SystemClock.elapsedRealtime()
+}
+
+private class DummyClockDelegate : Clock.Delegate {
+ override fun elapsedRealtime(): Long = 0
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NotificationsDelegate.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NotificationsDelegate.kt
new file mode 100644
index 0000000000..b80c0a0808
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NotificationsDelegate.kt
@@ -0,0 +1,178 @@
+/* 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.support.base.android
+
+import android.Manifest.permission.POST_NOTIFICATIONS
+import android.annotation.SuppressLint
+import android.app.Notification
+import android.os.Build
+import android.os.RemoteException
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.NotificationManagerCompat
+import androidx.lifecycle.Lifecycle
+
+typealias OnPermissionGranted = () -> Unit
+typealias OnPermissionRejected = () -> Unit
+
+/**
+ * Exception thrown when a [NotificationsDelegate] is not bound to any [AppCompatActivity]
+ */
+class UnboundHandlerException(message: String) : Exception(message)
+
+/**
+ * Handles showing notifications and asking permission, if needed.
+ *
+ * @param notificationManagerCompat a reference to [NotificationManagerCompat].
+ * @property onPermissionGranted optional callback for handling permission acceptance.
+ * @property onPermissionRejected optional callback for handling permission refusal.
+ */
+class NotificationsDelegate(
+ val notificationManagerCompat: NotificationManagerCompat,
+) {
+ var isRequestingPermission: Boolean = false
+ private set
+
+ private var onPermissionGranted: OnPermissionGranted = { }
+ private var onPermissionRejected: OnPermissionRejected = { }
+ private val notificationPermissionHandler: MutableMap<AppCompatActivity, ActivityResultLauncher<String>> =
+ mutableMapOf()
+
+ /**
+ * Provides the context for a permission request.
+ */
+ @Suppress("unused")
+ fun bindToActivity(activity: AppCompatActivity) {
+ val activityResultLauncher =
+ activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
+ isRequestingPermission = false
+ if (granted) {
+ onPermissionGranted.invoke()
+ } else {
+ onPermissionRejected.invoke()
+ }
+ }
+
+ notificationPermissionHandler[activity] = activityResultLauncher
+ }
+
+ /**
+ * Removes activity reference from the [NotificationsDelegate]
+ */
+ @Suppress("unused")
+ fun unBindActivity(activity: AppCompatActivity) {
+ notificationPermissionHandler.remove(activity)
+ }
+
+ /**
+ * Checks if the post permission notification was previously granted.
+ */
+ private fun hasPostNotificationsPermission(): Boolean {
+ return try {
+ notificationManagerCompat.areNotificationsEnabled()
+ } catch (e: RemoteException) {
+ false
+ }
+ }
+
+ /**
+ * Handles showing notifications and asking permission, if needed.
+ *
+ * @param notificationTag the string identifier for a notification. Can be null
+ * @param notificationId ID of the notification. The pair (tag, id) must be unique throughout the app
+ * @param notification the notification to post to the system
+ * @param onPermissionGranted optional callback for handling permission acceptance,
+ * in addition to showing the notification.
+ * Note that it will also be called when the permission is already granted.
+ * @param onPermissionRejected optional callback for handling permission refusal.
+ */
+ @SuppressLint("MissingPermission", "NotifyUsage")
+ fun notify(
+ notificationTag: String? = null,
+ notificationId: Int,
+ notification: Notification,
+ onPermissionGranted: OnPermissionGranted = { },
+ onPermissionRejected: OnPermissionRejected = { },
+ showPermissionRationale: Boolean = false,
+ ) {
+ if (hasPostNotificationsPermission()) {
+ notificationManagerCompat.notify(notificationTag, notificationId, notification)
+ onPermissionGranted.invoke()
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ requestNotificationPermission(
+ onPermissionGranted = {
+ notificationManagerCompat.notify(
+ notificationTag,
+ notificationId,
+ notification,
+ )
+ onPermissionGranted.invoke()
+ },
+ onPermissionRejected = onPermissionRejected,
+ showPermissionRationale = showPermissionRationale,
+ )
+ } else {
+ // this means we cannot show standard notifications without user changing it from OS Settings
+ // redirect to that, or maybe show in-app notifications? See https://bugzilla.mozilla.org/show_bug.cgi?id=1814863
+ // for crash notifications we could show the prompt instead.
+ }
+ }
+ }
+
+ /**
+ * Handles requesting the notification permission.
+ *
+ * @param onPermissionGranted optional callback for handling permission acceptance.
+ * @param onPermissionRejected optional callback for handling permission refusal.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ fun requestNotificationPermission(
+ onPermissionGranted: OnPermissionGranted = { },
+ onPermissionRejected: OnPermissionRejected = { },
+ showPermissionRationale: Boolean = false,
+ ) {
+ // some clients might request notification permission when it is already granted,
+ // so we should check first.
+ if (hasPostNotificationsPermission()) {
+ onPermissionGranted.invoke()
+ return
+ }
+
+ this.onPermissionGranted = onPermissionGranted
+ this.onPermissionRejected = onPermissionRejected
+
+ if (notificationPermissionHandler.isEmpty()) {
+ throw UnboundHandlerException("You must bind the NotificationPermissionHandler to an activity")
+ }
+
+ if (showPermissionRationale) {
+ showPermissionRationale(onPermissionGranted, onPermissionRejected)
+ } else {
+ isRequestingPermission = false
+ notificationPermissionHandler.entries.firstOrNull {
+ it.key.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
+ }?.value?.also {
+ isRequestingPermission = true
+ }?.launch(POST_NOTIFICATIONS)
+ }
+ }
+
+ /**
+ * Handles displaying a notification pre permission prompt.
+ *
+ * @param onPermissionGranted optional callback for handling permission acceptance.
+ * @param onPermissionRejected optional callback for handling permission refusal.
+ */
+ @Suppress("UNUSED_PARAMETER")
+ private fun showPermissionRationale(
+ onPermissionGranted: OnPermissionGranted,
+ onPermissionRejected: OnPermissionRejected,
+ ) {
+ // Content to be decided. Could follow existing NotificationPermissionDialogScreen.
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/Padding.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/Padding.kt
new file mode 100644
index 0000000000..1085c56fde
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/Padding.kt
@@ -0,0 +1,23 @@
+/* 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.support.base.android
+
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.PX
+
+/**
+ * A representation of an Android Padding.
+ *
+ * @param left Padding start in PX.
+ * @param top Padding top in PX.
+ * @param right Padding end in PX.
+ * @param bottom Padding end in PX.
+ */
+data class Padding(
+ @Dimension(unit = PX) val left: Int,
+ @Dimension(unit = PX) val top: Int,
+ @Dimension(unit = PX) val right: Int,
+ @Dimension(unit = PX) val bottom: Int,
+)
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/coroutines/Dispatchers.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/coroutines/Dispatchers.kt
new file mode 100644
index 0000000000..1c927d367c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/coroutines/Dispatchers.kt
@@ -0,0 +1,33 @@
+/* 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.support.base.coroutines
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.asCoroutineDispatcher
+import java.util.concurrent.SynchronousQueue
+import java.util.concurrent.ThreadPoolExecutor
+import java.util.concurrent.TimeUnit
+
+/**
+ * Shared [CoroutineDispatcher]s used by Android Components and app code - in addition to dispatchers
+ * provided by `kotlinx-coroutines-android`.
+ */
+object Dispatchers {
+ /**
+ * [CoroutineDispatcher] for short-lived asynchronous tasks. This dispatcher is using a thread
+ * pool that creates new threads as needed, but will reuse previously constructed threads when
+ * they are available.
+ *
+ * Threads that have not been used for sixty seconds are terminated and removed from the cache.
+ */
+ @Suppress("MagicNumber")
+ val Cached = ThreadPoolExecutor(
+ 0,
+ Integer.MAX_VALUE,
+ 60L,
+ TimeUnit.SECONDS,
+ SynchronousQueue<Runnable>(),
+ ).asCoroutineDispatcher()
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/NotificationManagerCompat.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/NotificationManagerCompat.kt
new file mode 100644
index 0000000000..351fea22ac
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/NotificationManagerCompat.kt
@@ -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/. */
+
+package mozilla.components.support.base.ext
+
+import android.os.Build
+import androidx.core.app.NotificationChannelCompat
+import androidx.core.app.NotificationManagerCompat
+
+/**
+ * Returns whether notifications are enabled, catches any exception that was thrown from
+ * [NotificationManagerCompat.areNotificationsEnabled] and returns false.
+ */
+@Suppress("TooGenericExceptionCaught")
+fun NotificationManagerCompat.areNotificationsEnabledSafe(): Boolean {
+ return try {
+ areNotificationsEnabled()
+ } catch (e: Exception) {
+ false
+ }
+}
+
+/**
+ * If the channel does not exist or is null, this returns false.
+ * If the channel exists with importance more than [NotificationManagerCompat.IMPORTANCE_NONE] and
+ * notifications are enabled for the app, this returns true.
+ * On <= SDK 26, this checks if notifications are enabled for the app.
+ *
+ * @param channelId the id of the notification channel to check.
+ * @return true if the channel is enabled, false otherwise.
+ */
+fun NotificationManagerCompat.isNotificationChannelEnabled(channelId: String): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = getNotificationChannelSafe(channelId)
+ if (channel == null) {
+ false
+ } else {
+ areNotificationsEnabledSafe() && channel.importance != NotificationManagerCompat.IMPORTANCE_NONE
+ }
+ } else {
+ areNotificationsEnabledSafe()
+ }
+}
+
+/**
+ * Returns the notification channel with the given [channelId], or null if the channel does not
+ * exist, catches any exception that was thrown by
+ * [NotificationManagerCompat.getNotificationChannelCompat] and returns null.
+ *
+ * @param channelId the id of the notification channel to check.
+ */
+@Suppress("TooGenericExceptionCaught")
+private fun NotificationManagerCompat.getNotificationChannelSafe(channelId: String): NotificationChannelCompat? {
+ return try {
+ getNotificationChannelCompat(channelId)
+ } catch (e: Exception) {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/Throwable.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/Throwable.kt
new file mode 100644
index 0000000000..32deaff3f7
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/Throwable.kt
@@ -0,0 +1,88 @@
+/* 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.support.base.ext
+
+import mozilla.components.support.base.log.logger.Logger
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlin.collections.HashSet
+
+private const val STACK_TRACE_INITIAL_BUFFER_SIZE = 256
+private const val STACK_TRACE_MAX_LENGTH = 100000
+private const val STACK_TRACE_MAX_FRAME = 50
+
+/**
+ * Returns a formatted string of the [Throwable.stackTrace].
+ */
+fun Throwable.getStacktraceAsString(stackTraceMaxLength: Int = STACK_TRACE_MAX_LENGTH): String {
+ val sw = StringWriter(STACK_TRACE_INITIAL_BUFFER_SIZE)
+ val pw = PrintWriter(sw, false)
+ printStackTrace(pw)
+ pw.flush()
+ return sw.toString().take(stackTraceMaxLength)
+}
+
+/**
+ * Returns a formatted JSON string of the [Throwable.stackTrace].
+ */
+@Suppress("NestedBlockDepth")
+fun Throwable.getStacktraceAsJsonString(maxFrame: Int = STACK_TRACE_MAX_FRAME): String {
+ val throwableList = extractThrowableList()
+ val result = JSONObject()
+ var frameCount = 0
+ try {
+ val exception = JSONObject()
+ val values = JSONArray()
+ for (throwable in throwableList) {
+ if (frameCount >= maxFrame) {
+ break
+ }
+
+ val frames = JSONArray()
+ for (stackTraceElement in throwable.stackTrace) {
+ if (frameCount >= maxFrame) {
+ break
+ }
+
+ val frame = JSONObject()
+ frame.put("module", stackTraceElement.className)
+ frame.put("function", stackTraceElement.methodName)
+ frame.put("in_app", !stackTraceElement.isNativeMethod)
+ frame.put("lineno", stackTraceElement.lineNumber)
+ frame.put("filename", stackTraceElement.fileName)
+ frames.put(frame)
+ frameCount++
+ }
+ val framesObject = JSONObject().put("frames", frames)
+ framesObject.put("type", throwable.toString().substringBefore(':').substringAfterLast('.'))
+ framesObject.put("module", throwable.toString().substringBefore(':').substringBeforeLast('.'))
+ framesObject.put("value", throwable.message)
+ values.put(JSONObject().put("stacktrace", framesObject))
+ }
+
+ exception.put("values", values)
+ result.put("exception", exception)
+ } catch (e: JSONException) {
+ Logger.warn("Could not parse throwable", e)
+ }
+
+ return result.toString()
+}
+
+private fun Throwable.extractThrowableList(): List<Throwable> {
+ val throwables = ArrayList<Throwable>()
+ val circularityDetector = HashSet<Throwable>()
+ var currentThrowable: Throwable? = this
+
+ while (currentThrowable != null && circularityDetector.add(currentThrowable)) {
+ throwables.add(currentThrowable)
+ currentThrowable = currentThrowable.cause
+ }
+
+ return throwables.asReversed()
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Action.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Action.kt
new file mode 100644
index 0000000000..768505d76e
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Action.kt
@@ -0,0 +1,97 @@
+/* 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.support.base.facts
+
+/**
+ * A user or system action that causes [Fact] instances to be emitted.
+ */
+enum class Action {
+ /**
+ * The user has clicked on something.
+ */
+ CLICK,
+
+ /**
+ * The user has toggled something.
+ *
+ * Other than a click this action is performed on items that have a distinct number of states. For example clicking
+ * a checkbox or switch widget could emit a [Fact] with a [TOGGLE] action instead of a [CLICK] action.
+ */
+ TOGGLE,
+
+ /**
+ * The user has committed an input (e.g. entered text into an input field and then pressed enter).
+ */
+ COMMIT,
+
+ /**
+ * The user has started playing something.
+ */
+ PLAY,
+
+ /**
+ * The user has paused something.
+ */
+ PAUSE,
+
+ /**
+ * The user has stopped something.
+ */
+ STOP,
+
+ /**
+ * The user has resumed something.
+ */
+ RESUME,
+
+ /**
+ * The user has confirmed something.
+ */
+ CONFIRM,
+
+ /**
+ * The user has cancelled something.
+ */
+ CANCEL,
+
+ /**
+ * The user has retried something.
+ */
+ TRY_AGAIN,
+
+ /**
+ * The user has opened something.
+ */
+ OPEN,
+
+ /**
+ * A generic interaction that can be caused by a previous action (e.g. the user clicks on a button which causes a
+ * [Fact] with [CLICK] action to be emitted. This click may causes something to load which emits a follow-up a
+ * [Fact] with [INTERACTION] action.
+ */
+ INTERACTION,
+
+ /**
+ * An implementation detail event exposed to understand the system for diagnostic purposes.
+ * For example, for each action the app may add profiler markers to better understand
+ * the app visually inside profiles.
+ */
+ IMPLEMENTATION_DETAIL,
+
+ /**
+ * An action triggered by the Android system.
+ */
+ SYSTEM,
+
+ /**
+ * Something is getting displayed.
+ */
+ DISPLAY,
+
+ /**
+ * The user selected something from a list of options.
+ */
+ SELECT,
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Fact.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Fact.kt
new file mode 100644
index 0000000000..5fc39ea210
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Fact.kt
@@ -0,0 +1,29 @@
+/* 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.support.base.facts
+
+import mozilla.components.support.base.Component
+
+/**
+ * A fact describing a generic event that has occurred in a component.
+ *
+ * @property component Component that emitted this fact.
+ * @property action A user or system action that caused this fact (e.g. Action.CLICK).
+ * @property item An item that caused the action or that the action was performed on (e.g. "toolbar").
+ * @property value An optional value providing more context.
+ * @property metadata A key/value map for facts where additional richer context is needed.
+ */
+data class Fact(
+ val component: Component,
+ val action: Action,
+ val item: String,
+ val value: String? = null,
+ val metadata: Map<String, Any>? = null,
+)
+
+/**
+ * Collect this fact through the [Facts] singleton.
+ */
+fun Fact.collect() = Facts.collect(this)
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/FactProcessor.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/FactProcessor.kt
new file mode 100644
index 0000000000..a30eb04316
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/FactProcessor.kt
@@ -0,0 +1,20 @@
+/* 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.support.base.facts
+
+/**
+ * A [FactProcessor] receives [Fact] instances to process them further.
+ */
+interface FactProcessor {
+ /**
+ * Passes the given [Fact] to the [FactProcessor] for processing.
+ */
+ fun process(fact: Fact)
+}
+
+/**
+ * Registers this [FactProcessor] to collect [Fact] instances from the [Facts] singleton.
+ */
+fun FactProcessor.register() = Facts.registerProcessor(this)
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Facts.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Facts.kt
new file mode 100644
index 0000000000..43ac4a6a41
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/Facts.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.support.base.facts
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Global API for collecting [Fact] objects and forwarding them to [FactProcessor] instances.
+ */
+object Facts {
+ private val processors = mutableListOf<FactProcessor>()
+
+ /**
+ * Registers a new [FactProcessor].
+ */
+ fun registerProcessor(processor: FactProcessor): Facts {
+ processors.add(processor)
+ return this
+ }
+
+ /**
+ * Collects a [Fact] and forwards it to all registered [FactProcessor] instances.
+ */
+ fun collect(fact: Fact) {
+ processors.forEach { it.process(fact) }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ fun clearProcessors() {
+ processors.clear()
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/processor/CollectionProcessor.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/processor/CollectionProcessor.kt
new file mode 100644
index 0000000000..c1718d05ad
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/processor/CollectionProcessor.kt
@@ -0,0 +1,54 @@
+/* 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.support.base.facts.processor
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+
+/**
+ * A [FactProcessor] implementation that keeps all [Fact] objects in a list.
+ *
+ * This [FactProcessor] is only for testing.
+ */
+@VisibleForTesting
+class CollectionProcessor : FactProcessor {
+ private val internalFacts = mutableListOf<Fact>()
+
+ val facts: List<Fact>
+ get() = internalFacts
+
+ override fun process(fact: Fact) {
+ internalFacts.add(fact)
+ }
+
+ companion object {
+ /**
+ * Helper for creating a [CollectionProcessor], registering it and clearing the processors again.
+ *
+ * Use in tests like:
+ *
+ * ```
+ * CollectionProcessor.withFactCollection { facts ->
+ * // During execution of this block the "facts" list will be updated automatically to contain
+ * // all facts that were emitted while executing this block.
+ * // After this block has completed all registered processors will be cleared.
+ * }
+ * ```
+ */
+ fun withFactCollection(block: (List<Fact>) -> Unit) {
+ val processor = CollectionProcessor()
+
+ try {
+ Facts.registerProcessor(processor)
+
+ block.invoke(processor.facts)
+ } finally {
+ Facts.clearProcessors()
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/processor/LogFactProcessor.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/processor/LogFactProcessor.kt
new file mode 100644
index 0000000000..39d0072dea
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/facts/processor/LogFactProcessor.kt
@@ -0,0 +1,21 @@
+/* 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.support.base.facts.processor
+
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * A [FactProcessor] implementation that prints collected [Fact] instances to the log.
+ */
+class LogFactProcessor(
+ private val logger: Logger = Logger("Facts"),
+) : FactProcessor {
+
+ override fun process(fact: Fact) {
+ logger.debug(fact.toString())
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ActivityResultHandler.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ActivityResultHandler.kt
new file mode 100644
index 0000000000..5caf4a7406
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ActivityResultHandler.kt
@@ -0,0 +1,22 @@
+/* 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.support.base.feature
+
+import android.app.Activity
+import android.content.Intent
+
+/**
+ * Generic interface for fragments, activities, features and other components that want to handle
+ * the [Activity.onActivityResult] event.
+ */
+interface ActivityResultHandler {
+
+ /**
+ * Called when the activity we launched exists and we may want to handle the result.
+ *
+ * @return true if the result was consumed and no other component needs to be notified.
+ */
+ fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/LifecycleAwareFeature.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/LifecycleAwareFeature.kt
new file mode 100644
index 0000000000..a7ed110aa0
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/LifecycleAwareFeature.kt
@@ -0,0 +1,32 @@
+/* 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.support.base.feature
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * An interface for all entry points to feature components to implement in order to make them lifecycle aware.
+ */
+interface LifecycleAwareFeature : DefaultLifecycleObserver {
+
+ /**
+ * Method that is called after ON_START event occurred.
+ */
+ fun start()
+
+ /**
+ * Method that is called after ON_STOP event occurred.
+ */
+ fun stop()
+
+ override fun onStart(owner: LifecycleOwner) {
+ start()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ stop()
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/PermissionsFeature.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/PermissionsFeature.kt
new file mode 100644
index 0000000000..8b1ea4d0cb
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/PermissionsFeature.kt
@@ -0,0 +1,51 @@
+/* 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.support.base.feature
+
+typealias OnNeedToRequestPermissions = (permissions: Array<String>) -> Unit
+
+/**
+ * Interface for features that need to request permissions from the user.
+ *
+ * Example integration:
+ *
+ * ```
+ * class MyFragment : Fragment {
+ * val myFeature = MyPermissionsFeature(
+ * onNeedToRequestPermissions = { permissions ->
+ * requestPermissions(permissions, REQUEST_CODE_MY_FEATURE)
+ * }
+ * )
+ *
+ * override fun onRequestPermissionsResult(resultCode: Int, permissions: Array<String>, grantResults: IntArray) {
+ * if (resultCode == REQUEST_CODE_MY_FEATURE) {
+ * myFeature.onPermissionsResult(permissions, grantResults)
+ * }
+ * }
+ *
+ * companion object {
+ * private const val REQUEST_CODE_MY_FEATURE = 1
+ * }
+ * }
+ * ```
+ */
+interface PermissionsFeature {
+
+ /**
+ * A callback invoked when permissions need to be requested by the feature before
+ * it can complete its task. Once the request is completed, [onPermissionsResult]
+ * needs to be invoked.
+ */
+ val onNeedToRequestPermissions: OnNeedToRequestPermissions
+
+ /**
+ * Notifies the feature that a permission request was completed.
+ * The feature should react to this and complete its task.
+ *
+ * @param permissions The permissions that were granted.
+ * @param grantResults The grant results for the corresponding permission
+ */
+ fun onPermissionsResult(permissions: Array<String>, grantResults: IntArray)
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/UserInteractionHandler.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/UserInteractionHandler.kt
new file mode 100644
index 0000000000..25e16ffcce
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/UserInteractionHandler.kt
@@ -0,0 +1,28 @@
+/* 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.support.base.feature
+
+import android.app.Activity
+
+/**
+ * Generic interface for fragments, features and other components that want to handle user
+ * interactions such as 'back' or 'home' button presses.
+ */
+interface UserInteractionHandler {
+ /**
+ * Called when this [UserInteractionHandler] gets the option to handle the user pressing the back key.
+ *
+ * Returns true if this [UserInteractionHandler] consumed the event and no other components need to be notified.
+ */
+ fun onBackPressed(): Boolean
+
+ /**
+ * In most cases, when the home button is pressed, we invoke this callback to inform the app that the user
+ * is going to leave the app.
+ *
+ * See also [Activity.onUserLeaveHint] for more details.
+ */
+ fun onHomePressed(): Boolean = false
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapper.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapper.kt
new file mode 100644
index 0000000000..a6d1e8709c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapper.kt
@@ -0,0 +1,224 @@
+/* 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.support.base.feature
+
+import android.content.Intent
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * Wrapper for [LifecycleAwareFeature] instances that keep a strong references to a [View]. This wrapper is helpful
+ * when the lifetime of the [View] may be shorter than the [Lifecycle] and you need to keep a reference to the
+ * [LifecycleAwareFeature] that may outlive the [View].
+ *
+ * [ViewBoundFeatureWrapper] takes care of stopping [LifecycleAwareFeature] and clearing references once the bound
+ * [View] get detached.
+ *
+ * A common use case is a `Fragment` that needs to keep a reference to a [LifecycleAwareFeature] (e.g. to invoke
+ * `onBackPressed()` and the [LifecycleAwareFeature] holds a reference to [View] instances. Once the `Fragment` gets
+ * detached and not destroyed (e.g. when pushed to the back stack) it will still keep the reference to the
+ * [LifecycleAwareFeature] and therefore to the (now detached) [View] (-> Leak). When the `Fragment` gets re-attached a
+ * new [View] and matching [LifecycleAwareFeature] is getting created leading to multiple concurrent
+ * [LifecycleAwareFeature] and (off-screen) [View] instances existing in memory.
+ *
+ * Example integration:
+ *
+ * ```
+ * class MyFragment : Fragment {
+ * val myFeature = ViewBoundFeatureWrapper<MyFeature>()
+ *
+ * override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ * // Bind wrapper to feature and view. Feature will be stopped and internal reference will be cleared
+ * // when the view gets detached.
+ * myFeature.set(
+ * feature = MyFeature(..., view),
+ * owner = this,
+ * view = view
+ * )
+ * }
+ *
+ * fun doSomething() {
+ * // Get will return the feature instance or null if the instance was cleared (e.g. the fragment is detached)
+ * myFeature.get()?.doSomething()
+ * }
+ *
+ * override fun onBackPressed(): Boolean {
+ * return myFeature.onBackPressed()
+ * }
+ * }
+ * ```
+ */
+class ViewBoundFeatureWrapper<T : LifecycleAwareFeature>() {
+ private var feature: T? = null
+ internal var owner: LifecycleOwner? = null
+ private var view: View? = null
+
+ private var viewBinding: ViewBinding<T>? = null
+ private var lifecycleBinding: LifecycleBinding<T>? = null
+
+ private var isFeatureStarted: Boolean = false
+
+ /**
+ * Convenient constructor for creating a wrapper instance and calling [set] immediately.
+ */
+ constructor(feature: T, owner: LifecycleOwner, view: View) : this() {
+ set(feature, owner, view)
+ }
+
+ /**
+ * Sets the [LifecycleAwareFeature] reference and binds it to the [Lifecycle] of the [LifecycleObserver] as well
+ * as the [View].
+ *
+ * The wrapper will take care of subscribing to the [Lifecycle] and forwarding start/stop events to the
+ * [LifecycleAwareFeature].
+ *
+ * Once the [View] gets detached the [LifecycleAwareFeature] will be stopped and the wrapper will clear all
+ * internal references.
+ */
+ @Synchronized
+ fun set(feature: T, owner: LifecycleOwner, view: View) {
+ if (this.feature != null) {
+ clear()
+ }
+
+ this.feature = feature
+ this.owner = owner
+ this.view = view
+
+ viewBinding = ViewBinding(this).also {
+ view.addOnAttachStateChangeListener(it)
+ }
+
+ lifecycleBinding = LifecycleBinding(this).also {
+ owner.lifecycle.addObserver(it)
+ }
+ }
+
+ /**
+ * Returns the wrapped [LifecycleAwareFeature] or null if the [View] was detached and the reference was cleared.
+ */
+ @Synchronized
+ fun get(): T? = feature
+
+ /**
+ * Runs the given [block] if this wrapper still has a reference to the [LifecycleAwareFeature].
+ */
+ @Synchronized
+ fun withFeature(block: (T) -> Unit) {
+ feature?.let(block)
+ }
+
+ /**
+ * Stops the feature and clears all internal references and observers.
+ */
+ @Synchronized
+ fun clear() {
+ // Stop feature and clear reference
+ if (isFeatureStarted) {
+ feature?.stop()
+ }
+ feature = null
+
+ // Stop observing view and clear references
+ view?.removeOnAttachStateChangeListener(viewBinding)
+ view = null
+ viewBinding = null
+
+ // Stop observing lifecycle and clear references
+ lifecycleBinding?.let {
+ owner?.lifecycle?.removeObserver(it)
+ }
+ owner = null
+ lifecycleBinding = null
+ }
+
+ /**
+ * Convenience method for invoking [UserInteractionHandler.onBackPressed] on a wrapped
+ * [LifecycleAwareFeature] that implements [UserInteractionHandler]. Returns false if
+ * the [LifecycleAwareFeature] was cleared already.
+ */
+ @Synchronized
+ fun onBackPressed(): Boolean {
+ val feature = feature ?: return false
+
+ if (feature !is UserInteractionHandler) {
+ throw IllegalAccessError(
+ "Feature does not implement ${UserInteractionHandler::class.java.simpleName} interface",
+ )
+ }
+
+ return feature.onBackPressed()
+ }
+
+ /**
+ * Convenience method for invoking [ActivityResultHandler.onActivityResult] on a wrapped
+ * [LifecycleAwareFeature] that implements [ActivityResultHandler]. Returns false if
+ * the [LifecycleAwareFeature] was cleared already.
+ */
+ @Synchronized
+ fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
+ val feature = feature ?: return false
+
+ if (feature !is ActivityResultHandler) {
+ throw IllegalAccessError(
+ "Feature does not implement ${ActivityResultHandler::class.java.simpleName} interface",
+ )
+ }
+
+ return feature.onActivityResult(requestCode, data, resultCode)
+ }
+
+ @Synchronized
+ internal fun start() {
+ feature?.start()
+ isFeatureStarted = true
+ }
+
+ @Synchronized
+ internal fun stop() {
+ feature?.stop()
+ isFeatureStarted = false
+ }
+}
+
+/**
+ * [View.OnAttachStateChangeListener] implementation to call [ViewBoundFeatureWrapper.clear] in case the [View] gets
+ * detached.
+ */
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal class ViewBinding<T : LifecycleAwareFeature>(
+ private val wrapper: ViewBoundFeatureWrapper<T>,
+) : View.OnAttachStateChangeListener {
+ override fun onViewDetachedFromWindow(v: View) {
+ wrapper.clear()
+ }
+
+ override fun onViewAttachedToWindow(v: View) = Unit
+}
+
+/**
+ * [LifecycleObserver] implementation to forward start/stop events to the [ViewBoundFeatureWrapper]. Additionally
+ * this implementation will call [ViewBoundFeatureWrapper.clear] in case the [Lifecycle] gets destroyed.
+ */
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal class LifecycleBinding<T : LifecycleAwareFeature>(
+ private val wrapper: ViewBoundFeatureWrapper<T>,
+) : DefaultLifecycleObserver {
+ override fun onStart(owner: LifecycleOwner) {
+ wrapper.start()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ wrapper.stop()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ wrapper.clear()
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ids/SharedIds.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ids/SharedIds.kt
new file mode 100644
index 0000000000..1e7128009c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ids/SharedIds.kt
@@ -0,0 +1,112 @@
+/* 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.support.base.ids
+
+import android.content.Context
+import android.content.SharedPreferences
+
+private const val KEY_NEXT_ID = "nextId"
+private const val KEY_LAST_USED_PREFIX = "lastUsed."
+private const val KEY_ID_PREFIX = "id."
+
+/**
+ * Internal helper to create unique and stable [Int] ids based on [String] tags.
+ *
+ * @param fileName The shared preference file that should be used to save id assignments.
+ * @param idLifeTime The maximum time an id can be unused until it is cleared
+ * @param offset The [Int] offset from which this instance should start providing ids.
+ */
+internal class SharedIds(
+ private val fileName: String,
+ private val idLifeTime: Long,
+ private val offset: Int = 0,
+) {
+ /**
+ * Get a unique ID for the provided unique tag.
+ */
+ @Synchronized
+ fun getIdForTag(context: Context, tag: String): Int {
+ val preferences = preferences(context)
+ val editor = preferences.edit()
+ val key = tagToKey(tag)
+ val lastUsedKey = tagToLastUsedKey(tag)
+
+ removeExpiredIds(preferences, editor)
+
+ // First check if we already have an id for this tag
+ val id = preferences.getInt(key, -1)
+ if (id != -1) {
+ editor.putLong(lastUsedKey, now()).apply()
+ return id
+ }
+
+ // If we do not have an id for this tag then use the next available one and save that.
+ val nextId = preferences.getInt(KEY_NEXT_ID, offset)
+
+ editor.putInt(KEY_NEXT_ID, nextId + 1) // Ignoring overflow for now.
+ editor.putInt(key, nextId)
+ editor.putLong(lastUsedKey, now())
+ editor.apply()
+
+ return nextId
+ }
+
+ /**
+ * Get the next available unique ID for the provided unique tag.
+ */
+ @Synchronized
+ fun getNextIdForTag(context: Context, tag: String): Int {
+ val preferences = preferences(context)
+ val editor = preferences.edit()
+ val key = tagToKey(tag)
+ val lastUsedKey = tagToLastUsedKey(tag)
+
+ removeExpiredIds(preferences, editor)
+
+ // always use the next available one and save that.
+ val nextId = preferences.getInt(KEY_NEXT_ID, offset)
+
+ editor.putInt(KEY_NEXT_ID, nextId + 1) // Ignoring overflow for now.
+ editor.putInt(key, nextId)
+ editor.putLong(lastUsedKey, now())
+ editor.apply()
+
+ return nextId
+ }
+
+ private fun tagToKey(tag: String): String {
+ return "$KEY_ID_PREFIX.$tag"
+ }
+
+ private fun tagToLastUsedKey(tag: String): String {
+ return "$KEY_LAST_USED_PREFIX$tag"
+ }
+
+ private fun preferences(context: Context): SharedPreferences {
+ return context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
+ }
+
+ /**
+ * Remove all expired notification ids.
+ */
+ private fun removeExpiredIds(preferences: SharedPreferences, editor: SharedPreferences.Editor) {
+ preferences.all.entries.filter { entry ->
+ entry.key.startsWith(KEY_LAST_USED_PREFIX)
+ }.filter { entry ->
+ val lastUsed = entry.value as Long
+ lastUsed < now() - idLifeTime
+ }.forEach { entry ->
+ val lastUsedKey = entry.key
+ val tag = lastUsedKey.substring(KEY_LAST_USED_PREFIX.length)
+ editor.remove(tagToKey(tag))
+ editor.remove(lastUsedKey)
+ editor.apply()
+ }
+ }
+
+ fun clear(context: Context) { preferences(context).edit().clear().apply() }
+
+ internal var now: () -> Long = { System.currentTimeMillis() }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ids/SharedIdsHelper.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ids/SharedIdsHelper.kt
new file mode 100644
index 0000000000..36cd32cbc7
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ids/SharedIdsHelper.kt
@@ -0,0 +1,61 @@
+/* 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.support.base.ids
+
+import android.app.NotificationManager
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationManagerCompat
+
+// If the tag is not used again in one week then clear the id
+private const val ID_LIFETIME: Long = 1000L * 60L * 60L * 24L * 7L
+
+// We start at 10000 instead of 0 to avoid conflicts with app code using random low numbers.
+private const val ID_OFFSET = 10000
+
+private const val ID_PREFERENCE_FILE = "mozac_support_base_shared_ids_helper"
+
+/**
+ * Helper for component and app code to use unique notification ids without conflicts.
+ */
+object SharedIdsHelper {
+ private val ids = SharedIds(ID_PREFERENCE_FILE, ID_LIFETIME, ID_OFFSET)
+
+ /**
+ * Get a unique notification ID for the provided unique tag.
+ */
+ fun getIdForTag(context: Context, tag: String): Int = ids.getIdForTag(context, tag)
+
+ /**
+ * Get the next available unique notification ID for the provided unique tag.
+ */
+ fun getNextIdForTag(context: Context, tag: String): Int = ids.getNextIdForTag(context, tag)
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var now: () -> Long
+ get() = ids.now
+ set(value) { ids.now = value }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun clear(context: Context) = ids.clear(context)
+}
+
+/**
+ * Cancel a previously shown notification.
+ *
+ * Uses a unique [String] tag instead of an [Int] id like [NotificationManager.cancel].
+ */
+fun NotificationManager.cancel(context: Context, tag: String) {
+ cancel(tag, SharedIdsHelper.getIdForTag(context, tag))
+}
+
+/**
+ * Cancel a previously shown notification.
+ *
+ * Uses a unique [String] tag instead of an [Int] id like [NotificationManagerCompat.cancel].
+ */
+fun NotificationManagerCompat.cancel(context: Context, tag: String) {
+ cancel(tag, SharedIdsHelper.getIdForTag(context, tag))
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/Log.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/Log.kt
new file mode 100644
index 0000000000..981f740cbf
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/Log.kt
@@ -0,0 +1,93 @@
+/* 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.support.base.log
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.base.log.sink.LogSink
+import mozilla.components.support.base.log.sink.TestModeLogSink
+
+/**
+ * API for logging messages and exceptions.
+ *
+ * This class does not process any logging calls itself. Instead it forwards the calls to registered
+ * <code>LogSink</code> implementations.
+ *
+ * This class only provides a low-level logging call. The <code>logger</code> sub packages contains
+ * logger implementations that wrap <code>Log</code> and provide more convenient APIs.
+ */
+object Log {
+ /**
+ * Minimum log level that log calls need to have to be forwarded to registered sinks. Log calls
+ * with a lower log level will be ignored.
+ */
+ var logLevel: Priority = Priority.DEBUG
+
+ private val testMode: Boolean = System.getProperty("logging.test-mode") == "true"
+
+ private val sinks = if (testMode) {
+ mutableListOf<LogSink>(TestModeLogSink())
+ } else {
+ mutableListOf()
+ }
+
+ /**
+ * Adds a sink that will receive log calls.
+ */
+ fun addSink(sink: LogSink) {
+ synchronized(sinks) {
+ sinks.add(sink)
+ }
+ }
+
+ /**
+ * Low-level logging call.
+ *
+ * @param priority The priority/type of this log message. By default DEBUG is used.
+ * @param tag Used to identify the source of a log message. It usually identifies the class
+ * where the log call occurs.
+ * @param throwable An exception to log.
+ * @param message A message to be logged.
+ */
+ fun log(
+ priority: Priority = Priority.DEBUG,
+ tag: String? = null,
+ throwable: Throwable? = null,
+ message: String,
+ ) {
+ if (priority.value >= logLevel.value) {
+ synchronized(sinks) {
+ sinks.forEach { sink ->
+ sink.log(priority, tag, throwable, message)
+ }
+ }
+ }
+ }
+
+ // Only for testing
+ @VisibleForTesting
+ fun reset() {
+ logLevel = Priority.DEBUG
+
+ synchronized(sinks) {
+ sinks.clear()
+ }
+ }
+
+ /**
+ * Priority constants for logging calls.
+ */
+ enum class Priority(val value: Int) {
+ // For simplicity the values mirror the Android log constants values:
+ // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/util/Log.java
+ //
+ // We intentionally omit ASSERT and VERBOSE. If you change this,
+ // be aware of the impact on consumers.
+
+ DEBUG(android.util.Log.DEBUG),
+ INFO(android.util.Log.INFO),
+ WARN(android.util.Log.WARN),
+ ERROR(android.util.Log.ERROR),
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/logger/Logger.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/logger/Logger.kt
new file mode 100644
index 0000000000..c831f80ba9
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/logger/Logger.kt
@@ -0,0 +1,104 @@
+/* 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.support.base.log.logger
+
+import android.os.SystemClock
+import mozilla.components.support.base.log.Log
+
+/**
+ * A wrapper for the <code>Log</code> object providing a more convenient API for logging.
+ *
+ * @param tag The tag to be used for log messages send via this logger.
+ */
+class Logger(
+ private val tag: String? = null,
+) {
+ /**
+ * Send a DEBUG log message.
+ */
+ fun debug(message: String, throwable: Throwable? = null) {
+ Log.log(Log.Priority.DEBUG, tag = tag, message = message, throwable = throwable)
+ }
+
+ /**
+ * Send a INFO log message.
+ */
+ fun info(message: String, throwable: Throwable? = null) {
+ Log.log(Log.Priority.INFO, tag = tag, message = message, throwable = throwable)
+ }
+
+ /**
+ * Send a WARN log message.
+ */
+ fun warn(message: String, throwable: Throwable? = null) {
+ Log.log(Log.Priority.WARN, tag = tag, message = message, throwable = throwable)
+ }
+
+ /**
+ * Send a ERROR log message.
+ */
+ fun error(message: String, throwable: Throwable? = null) {
+ Log.log(Log.Priority.ERROR, tag = tag, message = message, throwable = throwable)
+ }
+
+ /**
+ * Measure the time it takes to execute the provided block and print a log message before and
+ * after executing the block.
+ *
+ * Example log message:
+ * ⇢ doSomething()
+ * [..]
+ * ⇠ doSomething() [12ms]
+ */
+ fun measure(message: String, block: () -> Unit) {
+ debug("⇢ $message")
+
+ val start = SystemClock.elapsedRealtime()
+
+ try {
+ block()
+ } finally {
+ val took = SystemClock.elapsedRealtime() - start
+ debug("⇠ $message [${took}ms]")
+ }
+ }
+
+ companion object {
+ private val DEFAULT = Logger()
+
+ /**
+ * Send a DEBUG log message.
+ */
+ fun debug(message: String, throwable: Throwable? = null) = DEFAULT.debug(message, throwable)
+
+ /**
+ * Send a INFO log message.
+ */
+ fun info(message: String, throwable: Throwable? = null) = DEFAULT.info(message, throwable)
+
+ /**
+ * Send a WARN log message.
+ */
+ fun warn(message: String, throwable: Throwable? = null) = DEFAULT.warn(message, throwable)
+
+ /**
+ * Send a ERROR log message.
+ */
+ fun error(message: String, throwable: Throwable? = null) = DEFAULT.error(message, throwable)
+
+ /**
+ * Measure the time it takes to execute the provided block and print a log message before and
+ * after executing the block.
+ *
+ * Example log message:
+ * ⇢ doSomething()
+ * [..]
+ * ⇠ doSomething() [12ms]
+ */
+ fun measure(message: String, block: () -> Unit) {
+ return DEFAULT.measure(message, block)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/AndroidLogSink.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/AndroidLogSink.kt
new file mode 100644
index 0000000000..a9ff585fa3
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/AndroidLogSink.kt
@@ -0,0 +1,43 @@
+/* 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.support.base.log.sink
+
+import android.os.Build
+import mozilla.components.support.base.ext.getStacktraceAsString
+import mozilla.components.support.base.log.Log
+
+private const val MAX_TAG_LENGTH = 23
+
+/**
+ * <code>LogSink</code> implementation that writes to Android's log.
+ *
+ * @param defaultTag A default tag that should be used for all logging calls without tag.
+ */
+class AndroidLogSink(
+ private val defaultTag: String = "App",
+) : LogSink {
+ /**
+ * Low-level logging call.
+ */
+ override fun log(priority: Log.Priority, tag: String?, throwable: Throwable?, message: String) {
+ val logTag = tag(tag)
+
+ val logMessage: String = if (throwable != null) {
+ "$message\n${throwable.getStacktraceAsString()}"
+ } else {
+ message
+ }
+
+ android.util.Log.println(priority.value, logTag, logMessage)
+ }
+
+ private fun tag(candidate: String?): String {
+ val tag = candidate ?: defaultTag
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && tag.length > MAX_TAG_LENGTH) {
+ return tag.substring(0, MAX_TAG_LENGTH)
+ }
+ return tag
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/LogSink.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/LogSink.kt
new file mode 100644
index 0000000000..19c90ce8b3
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/LogSink.kt
@@ -0,0 +1,28 @@
+/* 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.support.base.log.sink
+
+import mozilla.components.support.base.log.Log
+
+/**
+ * Common interface for log sinks.
+ */
+interface LogSink {
+
+ /**
+ * Low-level logging call that should be implemented based on the Sink's capabilities.
+ *
+ * @param priority The [Log.Priority] of the log message, defaults to [Log.Priority.DEBUG].
+ * @param tag The tag of the log message.
+ * @param throwable The optional [Throwable] that should be logged.
+ * @param message The message that should be logged.
+ */
+ fun log(
+ priority: Log.Priority = Log.Priority.DEBUG,
+ tag: String? = null,
+ throwable: Throwable? = null,
+ message: String,
+ )
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/TestModeLogSink.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/TestModeLogSink.kt
new file mode 100644
index 0000000000..ee465226ea
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/log/sink/TestModeLogSink.kt
@@ -0,0 +1,25 @@
+/* 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.support.base.log.sink
+
+import mozilla.components.support.base.log.Log
+
+/**
+ * [LogSink] implementation that prints to console.
+ */
+internal class TestModeLogSink : LogSink {
+
+ override fun log(priority: Log.Priority, tag: String?, throwable: Throwable?, message: String) {
+ val printMessage = buildString {
+ append("${priority.name[0]} ")
+ if (tag != null) {
+ append("[$tag] ")
+ }
+ append(message)
+ }
+ println(printMessage)
+ throwable?.printStackTrace()
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/Consumable.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/Consumable.kt
new file mode 100644
index 0000000000..b70046c693
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/Consumable.kt
@@ -0,0 +1,198 @@
+/* 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.support.base.observer
+
+typealias ConsumableListener = () -> Unit
+
+/**
+ * A generic wrapper for values that can get consumed.
+ *
+ * @param value The value to be wrapped.
+ * @param onConsume A callback that gets invoked if the wrapped value gets consumed.
+ */
+class Consumable<T> private constructor(
+ internal var value: T?,
+ onConsume: ConsumableListener? = null,
+) {
+
+ private val listeners = mutableSetOf<ConsumableListener>().also { listeners ->
+ if (onConsume != null) {
+ listeners.add(onConsume)
+ }
+ }
+
+ /**
+ * Invokes the given lambda and marks the value as consumed if the lambda returns true.
+ */
+ @Synchronized
+ fun consume(consumer: (value: T) -> Boolean): Boolean {
+ return if (value?.let(consumer) == true) {
+ value = null
+ listeners.forEach { it() }
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Invokes the given list of lambdas and marks the value as consumed if at least one lambda
+ * returns true.
+ */
+ @Synchronized
+ fun consumeBy(consumers: List<(T) -> Boolean>): Boolean {
+ val value = value ?: return false
+ val results = consumers.map { consumer -> consumer(value) }
+
+ return if (results.contains(true)) {
+ this.value = null
+ listeners.forEach { it() }
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Returns whether the value was consumed.
+ */
+ @Synchronized
+ fun isConsumed() = value == null
+
+ /**
+ * Returns the value of this [Consumable] without consuming it.
+ */
+ @Synchronized
+ fun peek(): T? = value
+
+ /**
+ * Adds a listener to be invoked when this [Consumable] is consumed.
+ *
+ * @param listener the listener to add.
+ */
+ @Synchronized
+ fun onConsume(listener: ConsumableListener) {
+ listeners.add(listener)
+ }
+
+ companion object {
+ /**
+ * Creates a new Consumable wrapping the given value.
+ */
+ fun <T> from(value: T, onConsume: (() -> Unit)? = null): Consumable<T> = Consumable(value, onConsume)
+
+ /**
+ * Creates a new Consumable stream for the provided values.
+ */
+ fun <T> stream(vararg values: T): ConsumableStream<T> = ConsumableStream(
+ values.map { Consumable(it) },
+ )
+
+ /**
+ * Returns an empty Consumable with not value as if it was consumed already.
+ */
+ fun <T> empty(): Consumable<T> = Consumable(null)
+ }
+}
+
+/**
+ * A generic wrapper for a stream of values that can be consumed. Values will
+ * be consumed first in, first out.
+ */
+class ConsumableStream<T> internal constructor(private val consumables: List<Consumable<T>>) {
+
+ /**
+ * Invokes the given lambda with the next consumable value and marks the value
+ * as consumed if the lambda returns true.
+ *
+ * @param consumer a lambda accepting a consumable value.
+ * @return true if the consumable was consumed, otherwise false.
+ */
+ @Synchronized
+ fun consumeNext(consumer: (value: T) -> Boolean): Boolean {
+ val consumable = consumables.find { !it.isConsumed() }
+ return consumable?.consume(consumer) ?: false
+ }
+
+ /**
+ * Invokes the given lambda for each consumable value and marks the values
+ * as consumed if the lambda returns true.
+ *
+ * @param consumer a lambda accepting a consumable value.
+ * @return true if all consumables were consumed, otherwise false.
+ */
+ @Synchronized
+ fun consumeAll(consumer: (value: T) -> Boolean): Boolean {
+ val results = consumables.map { if (!it.isConsumed()) it.consume(consumer) else true }
+ return !results.contains(false)
+ }
+
+ /**
+ * Invokes the given list of lambdas with the next consumable value and marks the
+ * value as consumed if at least one lambda returns true.
+ *
+ * @param consumers the lambdas accepting the next consumable value.
+ * @return true if the consumable was consumed, otherwise false.
+ */
+ @Synchronized
+ fun consumeNextBy(consumers: List<(T) -> Boolean>): Boolean {
+ val consumable = consumables.find { !it.isConsumed() }
+ return consumable?.consumeBy(consumers) ?: false
+ }
+
+ /**
+ * Invokes the given list of lambdas for each consumable value and marks the
+ * values as consumed if at least one lambda returns true.
+ *
+ * @param consumers the lambdas accepting a consumable value.
+ * @return true if all consumables were consumed, otherwise false.
+ */
+ @Synchronized
+ fun consumeAllBy(consumers: List<(T) -> Boolean>): Boolean {
+ val results = consumables.map { if (!it.isConsumed()) it.consumeBy(consumers) else true }
+ return !results.contains(false)
+ }
+
+ /**
+ * Copies the stream and appends the provided values.
+ *
+ * @param values the values to append.
+ * @return a new consumable stream with the values appended.
+ */
+ @Synchronized
+ fun append(vararg values: T): ConsumableStream<T> =
+ ConsumableStream(consumables + values.map { Consumable.from(it) })
+
+ /**
+ * Copies the stream but removes all consumables equal to the provided value.
+ *
+ * @param value the value to remove.
+ * @return a new consumable stream with the matching values removed.
+ */
+ @Synchronized
+ fun remove(value: T): ConsumableStream<T> =
+ ConsumableStream(consumables.filterNot { it.value == value })
+
+ /**
+ * Copies the stream but removes all consumed values.
+ *
+ * @return a new consumable stream with the consumed values removed.
+ */
+ @Synchronized
+ fun removeConsumed(): ConsumableStream<T> =
+ ConsumableStream(consumables.filterNot { it.isConsumed() })
+
+ /**
+ * Returns true if all values in this stream were consumed, otherwise false.
+ */
+ @Synchronized
+ fun isConsumed() = consumables.filterNot { it.isConsumed() }.isEmpty()
+
+ /**
+ * Returns true if the stream is empty, otherwise false.
+ */
+ @Synchronized
+ fun isEmpty() = consumables.isEmpty()
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/Observable.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/Observable.kt
new file mode 100644
index 0000000000..c1d5aec943
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/Observable.kt
@@ -0,0 +1,154 @@
+/* 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.support.base.observer
+
+import android.view.View
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * Interface for observables. This interface is implemented by ObserverRegistry so that classes that
+ * want to be observable can implement the interface by delegation:
+ *
+ * <code>
+ * val registry = ObserverRegistry<MyObserverInterface>()
+ *
+ * class MyObservableClass : Observable<MyObserverInterface> by registry {
+ * ...
+ * }
+ * </code>
+ */
+interface Observable<T> {
+ /**
+ * Registers an observer to get notified about changes.
+ *
+ * @param observer the observer to register.
+ */
+ fun register(observer: T)
+
+ /**
+ * Registers an observer to get notified about changes.
+ *
+ * The observer will automatically unsubscribe if the lifecycle of the provided LifecycleOwner
+ * becomes DESTROYED.
+ *
+ * @param observer the observer to register.
+ * @param owner the lifecycle owner the provided observer is bound to.
+ * @param autoPause whether or not the observer should automatically be
+ * paused/resumed with the bound lifecycle.
+ */
+ fun register(observer: T, owner: LifecycleOwner, autoPause: Boolean = false)
+
+ /**
+ * Registers an observer to get notified about changes.
+ *
+ * The observer will only be notified if the view is attached and will be unregistered/
+ * registered if the attached state changes.
+ *
+ * @param observer the observer to register.
+ * @param view the view the provided observer is bound to.
+ */
+ fun register(observer: T, view: View)
+
+ /**
+ * Unregisters an observer.
+ *
+ * @param observer the observer to unregister.
+ */
+ fun unregister(observer: T)
+
+ /**
+ * Unregisters all observers.
+ */
+ fun unregisterObservers()
+
+ /**
+ * Notifies all registered observers about a change.
+ *
+ * @param block the notification (method on the observer to be invoked).
+ */
+ fun notifyObservers(block: T.() -> Unit)
+
+ /**
+ * Notifies all registered observers about a change. If there is no observer
+ * the notification is queued and sent to the first observer that is
+ * registered.
+ *
+ * @param block the notification (method on the observer to be invoked).
+ */
+ fun notifyAtLeastOneObserver(block: T.() -> Unit)
+
+ /**
+ * Pauses the provided observer. No notifications will be sent to this
+ * observer until [resumeObserver] is called.
+ *
+ * @param observer the observer to pause.
+ */
+ fun pauseObserver(observer: T)
+
+ /**
+ * Resumes the provided observer. Notifications sent since it
+ * was last paused (see [pauseObserver]]) are lost and will not be
+ * re-delivered.
+ *
+ * @param observer the observer to resume.
+ */
+ fun resumeObserver(observer: T)
+
+ /**
+ * Returns a list of lambdas wrapping a consuming method of an observer.
+ */
+ fun <R> wrapConsumers(block: T.(R) -> Boolean): List<(R) -> Boolean>
+
+ /**
+ * If the observable has registered observers.
+ */
+ fun isObserved(): Boolean
+}
+
+/**
+ * A deprecated version of [Observable] to migrate and deprecate existing
+ * components individually. All components implement [Observable] by delegate
+ * so this makes it easy to deprecate without having to override all methods
+ * in each component separately.
+ */
+@Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+interface DeprecatedObservable<T> : Observable<T> {
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun register(observer: T)
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun register(observer: T, owner: LifecycleOwner, autoPause: Boolean)
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun register(observer: T, view: View)
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun unregister(observer: T)
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun unregisterObservers()
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun notifyObservers(block: T.() -> Unit)
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun notifyAtLeastOneObserver(block: T.() -> Unit)
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun pauseObserver(observer: T)
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun resumeObserver(observer: T)
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun <R> wrapConsumers(block: T.(R) -> Boolean): List<(R) -> Boolean>
+
+ @Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+ override fun isObserved(): Boolean
+}
+
+const val OBSERVER_DEPRECATION_MESSAGE =
+ "Use browser store (browser-state component) for observing state changes instead"
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/ObserverRegistry.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/ObserverRegistry.kt
new file mode 100644
index 0000000000..a6f50f73fe
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/observer/ObserverRegistry.kt
@@ -0,0 +1,257 @@
+/* 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.support.base.observer
+
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle.State.DESTROYED
+import androidx.lifecycle.Lifecycle.State.RESUMED
+import androidx.lifecycle.LifecycleOwner
+import java.util.Collections
+import java.util.LinkedList
+import java.util.WeakHashMap
+
+/**
+ * A helper for classes that want to get observed. This class keeps track of registered observers
+ * and can automatically unregister observers if a LifecycleOwner is provided.
+ *
+ * ObserverRegistry is thread-safe.
+ */
+open class ObserverRegistry<T> : Observable<T> {
+ private val observers = mutableSetOf<T>()
+ private val lifecycleObservers = WeakHashMap<T, DefaultLifecycleObserver>()
+ private val viewObservers = WeakHashMap<T, ViewBoundObserver<T>>()
+ private val pausedObservers = Collections.newSetFromMap(WeakHashMap<T, Boolean>())
+ private val queuedNotifications = LinkedList<T.() -> Unit>()
+
+ /**
+ * Registers an observer to get notified about changes. Does nothing if [observer] is already registered.
+ * This method is thread-safe.
+ *
+ * @param observer the observer to register.
+ */
+ @Synchronized
+ open override fun register(observer: T) {
+ observers.add(observer)
+
+ while (!queuedNotifications.isEmpty()) {
+ queuedNotifications.poll()?.let { notify -> observer.notify() }
+ }
+ }
+
+ @Synchronized
+ @MainThread
+ override fun register(observer: T, owner: LifecycleOwner, autoPause: Boolean) {
+ // Don't register if the owner is already destroyed
+ if (owner.lifecycle.currentState == DESTROYED) {
+ return
+ }
+
+ register(observer)
+
+ val lifecycleObserver = if (autoPause) {
+ AutoPauseLifecycleBoundObserver(owner, registry = this, observer = observer)
+ } else {
+ LifecycleBoundObserver(owner, registry = this, observer = observer)
+ }
+
+ lifecycleObservers[observer] = lifecycleObserver
+
+ // In newer AndroidX versions of the lifecycle lib, the default requirement is for
+ // lifecycleRegistry to be only touched on the main thread. We don't know if `onwer`'s
+ // lifecycle registry was created with or without this requirement, but assume so since
+ // that's the default and also the reasonable thing to do.
+ owner.lifecycle.addObserver(lifecycleObserver)
+ }
+
+ @Synchronized
+ override fun register(observer: T, view: View) {
+ val viewObserver = ViewBoundObserver(
+ view,
+ registry = this,
+ observer = observer,
+ )
+
+ viewObservers[observer] = viewObserver
+
+ view.addOnAttachStateChangeListener(viewObserver)
+
+ if (view.isAttachedToWindow) {
+ register(observer)
+ }
+ }
+
+ /**
+ * Unregisters an observer. Does nothing if [observer] is not registered.
+ *
+ * @param observer the observer to unregister.
+ */
+ @Synchronized
+ override fun unregister(observer: T) {
+ // Remove observer
+ observers.remove(observer)
+ pausedObservers.remove(observer)
+
+ // Unregister view observers
+ viewObservers[observer]?.remove()
+
+ // Remove lifecycle/view observers from map
+ lifecycleObservers.remove(observer)
+ viewObservers.remove(observer)
+ }
+
+ @Synchronized
+ override fun unregisterObservers() {
+ // Remove all registered observers
+ observers.toList().forEach { observer ->
+ unregister(observer)
+ }
+
+ // There can still be view observers for views that are not attached yet and therefore the observers were not
+ // registered yet. Let's remove them too.
+ viewObservers.keys.toList().forEach { observer ->
+ unregister(observer)
+ }
+
+ // If any of our sets and maps is not empty now then this would be a serious bug.
+ checkInternalCollectionsAreEmpty()
+ }
+
+ @Synchronized
+ override fun pauseObserver(observer: T) {
+ pausedObservers.add(observer)
+ }
+
+ @Synchronized
+ override fun resumeObserver(observer: T) {
+ pausedObservers.remove(observer)
+ }
+
+ @Synchronized
+ override fun notifyObservers(block: T.() -> Unit) {
+ observers.forEach {
+ if (!pausedObservers.contains(it)) {
+ it.block()
+ }
+ }
+ }
+
+ @Synchronized
+ override fun notifyAtLeastOneObserver(block: T.() -> Unit) {
+ if (observers.isEmpty()) {
+ queuedNotifications.add(block)
+ } else {
+ notifyObservers(block)
+ }
+ }
+
+ @Synchronized
+ override fun <V> wrapConsumers(block: T.(V) -> Boolean): List<(V) -> Boolean> {
+ val consumers: MutableList<(V) -> Boolean> = mutableListOf()
+
+ observers.forEach { observer ->
+ consumers.add { value -> observer.block(value) }
+ }
+
+ return consumers
+ }
+
+ @Synchronized
+ override fun isObserved(): Boolean {
+ // The registry is getting observed if there are registered observer or if there are registered view observers
+ // that will register an observer as soon as their views are attached.
+ return observers.isNotEmpty() || viewObservers.isNotEmpty()
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun checkInternalCollectionsAreEmpty(): Boolean {
+ check(observers.isEmpty())
+ check(pausedObservers.isEmpty())
+ check(lifecycleObservers.isEmpty())
+ check(viewObservers.isEmpty())
+ return true
+ }
+
+ /**
+ * LifecycleObserver implementation to bind an observer to a Lifecycle.
+ */
+ private open class LifecycleBoundObserver<T>(
+ private val owner: LifecycleOwner,
+ protected val registry: ObserverRegistry<T>,
+ protected val observer: T,
+ ) : DefaultLifecycleObserver {
+
+ @MainThread
+ fun remove() {
+ // In newer AndroidX versions of the lifecycle lib, the default requirement is for
+ // lifecycleRegistry to be only touched on the main thread. We don't know if `onwer`'s
+ // lifecycle registry was created with or without this requirement, but assume so since
+ // that's the default and also the reasonable thing to do.
+ owner.lifecycle.removeObserver(this)
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ registry.unregister(observer)
+ }
+ }
+
+ /**
+ * LifecycleObserver implementation to bind an observer to a Lifecycle and pause observing
+ * automatically for the pause and resume events.
+ */
+ private class AutoPauseLifecycleBoundObserver<T>(
+ owner: LifecycleOwner,
+ private val registry: ObserverRegistry<T>,
+ private val observer: T,
+ ) : DefaultLifecycleObserver {
+ init {
+ if (!owner.lifecycle.currentState.isAtLeast(RESUMED)) {
+ registry.pauseObserver(observer)
+ }
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ registry.resumeObserver(observer)
+ }
+
+ override fun onPause(owner: LifecycleOwner) {
+ registry.pauseObserver(observer)
+ }
+ }
+
+ /**
+ * View.OnAttachStateChangeListener implementation to automatically unregister an observer if
+ * the bound view gets detached.
+ */
+ private class ViewBoundObserver<T>(
+ private val view: View,
+ private val registry: ObserverRegistry<T>,
+ private val observer: T,
+ ) : View.OnAttachStateChangeListener {
+ override fun onViewDetachedFromWindow(view: View) {
+ registry.unregister(observer)
+ }
+
+ fun remove() {
+ view.removeOnAttachStateChangeListener(this)
+ }
+
+ override fun onViewAttachedToWindow(view: View) {
+ registry.register(observer)
+ }
+ }
+}
+
+/**
+ * A deprecated version of [ObserverRegistry] to migrate and deprecate existing
+ * components individually. All components implement [ObserverRegistry] by
+ * delegate so this makes it easy to deprecate without having to override
+ * all methods in each component separately.
+ */
+@Deprecated(OBSERVER_DEPRECATION_MESSAGE)
+@Suppress("Deprecation")
+class DeprecatedObserverRegistry<T> : ObserverRegistry<T>(), DeprecatedObservable<T>
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/LazyComponent.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/LazyComponent.kt
new file mode 100644
index 0000000000..f2539ec7ee
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/LazyComponent.kt
@@ -0,0 +1,80 @@
+/* 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.support.base.utils
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import mozilla.components.support.base.log.logger.Logger
+import java.util.concurrent.atomic.AtomicInteger
+
+private val logger = Logger("LazyComponent")
+
+/**
+ * A wrapper around the [lazy] property delegate that is intended for use with application-wide
+ * components (such as services in the Service Locator pattern or dependencies in the Dependency
+ * Injection pattern).
+ *
+ * This class helps address the issue where kotlin's built-in [lazy] property delegate does not
+ * handle recursive initialization by inserting a getter between the lazy reference. For example, in:
+ * ```
+ * val useCases by lazy { UseCases(...) }
+ * val search by lazy { Search(useCases) }
+ * ```
+ * When `search` is referenced, it will initialized and **so will `useCases`**.
+ * This is a problem if there are many dependencies or they are expensive. [LazyComponent]
+ * addresses this issue by allowing the class to be referenced without being initialized. To apply
+ * this to the example above:
+ * ```
+ * val useCases = LazyComponent { UseCases(...) }
+ * val search = LazyComponent { Search(useCases) }
+ * ```
+ *
+ * To call methods on the class and thus initialize it, one would call `LazyComponent.get()`, e.g.
+ * `search.get().startSearch(terms)`.
+ *
+ * This class also adds performance monitoring code to component initialization that, when paired
+ * with testing, can help prevent component initialization at inopportune times.
+ */
+class LazyComponent<T>(initializer: () -> T) {
+ // Lazy is thread safe.
+ private val lazyValue: Lazy<T> = lazy {
+ val initCount = initCount.incrementAndGet() // See property kdoc with regard to overflow.
+
+ initializer().also {
+ @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // the compiler fails with !! but warns with !!.
+ val className = if (it == null) "null" else it!!::class.java.canonicalName
+ logger.debug("Initialized lazyComponent #$initCount: $className")
+ }
+ }
+
+ /**
+ * Returns the component, initializing it if it has not already been initialized.
+ *
+ * To ensure this value is initialized lazily, it is expected that this method will only be
+ * called when the class instance needs to be interacted with, i.e. the return value should not
+ * be stored in a member property during an initializer.
+ */
+ fun get(): T = lazyValue.value
+
+ /**
+ * Returns whether or not the component has been initialized yet.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun isInitialized(): Boolean = lazyValue.isInitialized()
+
+ companion object {
+ /**
+ * The number of [LazyComponent]s initialized. This is intended to be checked during testing.
+ * For example, a team can run a test scenario that starts the app and, if the [initCount]
+ * increases from main, the team can fail the test to alert that a new component is initialized.
+ * This can help the team catch cases where they didn't mean to initialize new components or add
+ * new code on start up, keeping it performant.
+ *
+ * This class assumes the app will not have 4 billion+ components so we don't handle overflow.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ val initCount = AtomicInteger(0)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/NamedThreadFactory.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/NamedThreadFactory.kt
new file mode 100644
index 0000000000..7af81f5ec2
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/NamedThreadFactory.kt
@@ -0,0 +1,26 @@
+/* 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.support.base.utils
+
+import java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * A [ThreadFactory] that names its threads, "prefix-thread-<#>", deferring further thread
+ * creation details to [Executors.defaultThreadFactory].
+ */
+class NamedThreadFactory(
+ private val prefix: String,
+) : ThreadFactory {
+
+ private val backingFactory = Executors.defaultThreadFactory()
+ private val threadNumber = AtomicInteger(1)
+
+ override fun newThread(r: Runnable?): Thread = backingFactory.newThread(r).apply {
+ val threadNumber = threadNumber.getAndIncrement()
+ name = "$prefix-thread-$threadNumber"
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/SharedPreferencesCache.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/SharedPreferencesCache.kt
new file mode 100644
index 0000000000..41673778f2
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/utils/SharedPreferencesCache.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.support.base.utils
+
+import android.content.Context
+import android.content.SharedPreferences
+import mozilla.components.support.base.log.logger.Logger
+import org.json.JSONException
+import org.json.JSONObject
+
+/**
+ * An abstract wrapper around [SharedPreferences] which facilitates caching of [T] objects.
+ */
+abstract class SharedPreferencesCache<T>(val context: Context) {
+ /**
+ * Logger used to report issues.
+ */
+ abstract val logger: Logger
+
+ /**
+ * Name of the 'key' under which serialized data is stored within the cache.
+ */
+ abstract val cacheKey: String
+
+ /**
+ * Name of the cache.
+ */
+ abstract val cacheName: String
+
+ /**
+ * A conversion method from [T] into a [JSONObject].
+ */
+ abstract fun T.toJSON(): JSONObject
+
+ /**
+ * A conversion method from [JSONObject] to [T].
+ */
+ abstract fun fromJSON(obj: JSONObject): T
+
+ /**
+ * @param A [T] value to cache.
+ */
+ fun setToCache(obj: T) {
+ // JSONObject swallows any 'JSONException' thrown in 'toString', and simply returns 'null'.
+ // An error happened while converting a JSONObject into a string. Let's fail loudly and
+ // see if this actually happens in the wild. An alternative is to swallow this error and
+ // log an error message, but we're very unlikely to notice any problems in that case.
+ val s = obj.toJSON().toString() as String? ?: throw IllegalStateException("Failed to stringify")
+ cache().edit().putString(cacheKey, s).apply()
+ }
+
+ /**
+ * @return Cached [T] value or `null`.
+ */
+ fun getCached(): T? {
+ val s = cache().getString(cacheKey, null) ?: return null
+ return try {
+ fromJSON(JSONObject(s))
+ } catch (e: JSONException) {
+ logger.error("Failed to parse cached value", e)
+ null
+ }
+ }
+
+ /**
+ * Clear cached values.
+ */
+ fun clear() {
+ cache().edit().clear().apply()
+ }
+
+ private fun cache(): SharedPreferences {
+ return context.getSharedPreferences(cacheName, Context.MODE_PRIVATE)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/worker/Frequency.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/worker/Frequency.kt
new file mode 100644
index 0000000000..a46372bb86
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/worker/Frequency.kt
@@ -0,0 +1,18 @@
+/* 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.support.base.worker
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Indicates how often the work request should be run.
+ *
+ * @property repeatInterval Long indicating how often the update should happen.
+ * @property repeatIntervalTimeUnit The time unit of [repeatInterval].
+ */
+data class Frequency(
+ val repeatInterval: Long,
+ val repeatIntervalTimeUnit: TimeUnit,
+)
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..07fcc02ecb
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-am/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">ወደ ቅንብሮች ይሂዱ</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">አሰናብት</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..823c51df0e
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ar/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">انتقل إلى الإعدادات</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">أهمِل</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..4520993fc5
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ast/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Dir a la configuración</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Escartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..f30b03f0e7
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-azb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">تنظیم‌لره گئدین</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">باغلا</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..72f2c638f3
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ban/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Nuju pangaturan</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Tutup</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..bd01630ab7
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-be/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Перайсці ў налады</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Закрыць</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..d22576fe45
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-bg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Към настройки</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Прекратяване</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..f17d200fce
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-bn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">সেটিং এ যান</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">বাতিল</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..18504c8e09
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-br/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Mont dʼan arventennoù</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Skarzhañ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..2138bb8ad3
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-bs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Idi na postavke</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Odbaci</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..1c5e2ecc11
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ca/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Vés als paràmetres</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Tanca</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..bc78224364
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-cak/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Tib\'e pa taq nuk\'ulem</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Tewäx</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..9a01c8fcf6
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Adto sa settings</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">i-Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..41a600d3ea
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">بڕۆ بۆ ڕێکخستن</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">پشتگوێخستن</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..6ff646f5b7
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-co/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Andà à e preferenze</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Ricusà</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..347bea09da
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-cs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Přejít do nastavení</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Zavřít</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..dae6964bd6
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-cy/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Mynd i’r gosodiadau</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Cau</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..3580188116
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-da/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Gå til indstillinger</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Afvis</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..785786abc0
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-de/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Einstellungen öffnen</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Schließen</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..f88d0eaa18
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">K nastajenjam</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Zachyśiś</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..c4d2fbd792
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-el/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Μετάβαση στις ρυθμίσεις</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Απόρριψη</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..815bb46e4f
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Go to settings</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..815bb46e4f
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Go to settings</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..fb2da89f6c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-eo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Iri al agordoj</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Ignori</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..759e49a2c2
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir a Ajustes</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..41badc20aa
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir a ajustes</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Ocultar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..7bfccd37c5
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir a ajustes</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..7bfccd37c5
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir a ajustes</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..7bfccd37c5
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-es/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir a ajustes</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..ca8f61c118
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-et/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Mine sätetesse</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Peida</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..2b14cb59e3
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-eu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Joan ezarpenetara</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Baztertu</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..5b6d55b1b1
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-fa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">رفتن به تنظیمات</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">رد کردن</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..8417a5628e
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-fi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Siirry asetuksiin</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Hylkää</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..3fde20673e
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-fr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ouvrir les paramètres</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Ignorer</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..9d2d27e167
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-fur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Va aes impostazions</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Anule</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..b6cbd69772
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Nei ynstellingen</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Slute</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..ee831c6e67
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-gd/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Tadhail air na roghainnean</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Leig seachad</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..0f790a8fe6
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-gl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir á configuración</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Rexeitar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..ab22c14eed
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-gn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Eho ñembohekópe</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Mosẽ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..c712af3530
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">सेटिंग में जाएं</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">खारिज</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..5b73e426c2
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-hr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Idi u postavke</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Odbaci</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..7c47c3296c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">K nastajenjam</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Zaćisnyć</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..53c4dbe2ce
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-hu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ugrás a beállításokhoz</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Elvetés</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..58feb7a89b
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Անցնել Կարգավորումներին</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Բաց թողնել</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..a441f4baf6
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ia/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir a parametros</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Dimitter</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..893cfb3d2e
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-in/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Buka pengaturan</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Tutup</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..4bba285f90
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-is/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Fara í stillingar</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Hafna</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..7f7497043a
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-it/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Vai alle impostazioni</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Annulla</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..ce476f5093
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-iw/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">מעבר להגדרות</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">סגירה</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..5cb7d18f99
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ja/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">設定を開く</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">閉じる</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..7a1b4f76d1
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ka/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">პარამეტრებზე გადასვლა</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">დახურვა</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..45e1c41977
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Sazlawlarǵa ótiw</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Jabıw</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..b8e3871deb
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-kab/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ddu ɣer iɣewwaṛen</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Zgel</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..6dd57bdf5f
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-kk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Баптауларға өту</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Тайдыру</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..b7d0fed929
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Here sazkariyan</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Bigire</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..6a96d80b6d
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-kn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">ಸಿದ್ಧತೆಗಳಿಗೆ ತೆರಳಿ</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">‍ವಜಾಗೊಳಿಸು‍</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..c3e64aeb79
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ko/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">설정으로 이동</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">닫기</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..746a204cf4
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-lo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">ໄປທີ່ການຕັ້ງຄ່າ</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">ຍົກເລີກ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..760045c511
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-lt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Eiti į nustatymus</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Paslėpti</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..b373aae8c1
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-my/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">ဆက်တင် များသို့ သွားပါ။</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">ပိတ်ပါ။</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..dcfa4a1040
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Gå til innstillinger</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Avvis</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..7a3365a08b
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">सेटिङ्गहरुमा जानुहोस्</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">खारेज गर्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..e66d69d918
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-nl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Naar instellingen</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Sluiten</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..ba31fa21b3
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Gå til Innstillingar</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Avvis</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..72e9081511
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-oc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Anar als paramètres</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Ignorar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..14c9dbbd17
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">ਸੈਟਿੰਗਾਂ ਉੱਤੇ ਜਾਓ</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">ਖ਼ਾਰਜ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..1a912adfd2
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">سیٹنگاں نوں جاؤ</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">بند کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..7d622d32d9
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-pl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Przejdź do ustawień</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Zamknij</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..706abe2d8c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir para configurações</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..9d866cc480
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir para as definições</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Dispensar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..df1be229d3
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-rm/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ir als parameters</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Ignorar</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..0d1927bdf6
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ro/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Înlătură</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..fea1ddb536
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ru/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Перейти в настройки</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Закрыть</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..b6f505ff49
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-sat/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">ᱥᱟᱡᱟᱣ ᱛᱮ ᱪᱟᱞᱟᱜ ᱢᱮ</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">ᱵᱟᱹᱰ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..d5885b4d24
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-sc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Bae a sa cunfiguratzione</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Iscarta</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..c145a16a75
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-si/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">සැකසුම් වෙත යන්න</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">ඉවතලන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..f1468d83cb
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-sk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Prejsť do nastavení</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Zavrieť</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..05ff45ad87
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-skr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">ترتیباں تے ون٘ڄو</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">فارغ کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..fa70e6fb5d
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-sl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Pojdi v nastavitve</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Skrij</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..75db2f1735
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-sq/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Kalo te rregullimet</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Hidhe tej</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..d4c93a2568
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-sr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Иди у подешавања</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Одбаци</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..b45e41d7c3
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-su/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Buka setélan</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Tutup</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..506488707c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Gå till Inställningar</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Ignorera</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..631b725830
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-te/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">అమరికలకు వెళ్ళు</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">విస్మరించు</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..62b932e3ab
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-tg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Гузариш ба танзимот</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Нодида гузарондан</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..54abd73fda
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-th/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">ไปยังการตั้งค่า</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">ปิด</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..292c7740a1
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-tl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Pumunta sa mga setting</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Paalisin</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..c425973c90
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-tr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ayarlara git</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Kapat</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..db0b859a53
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-trs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Gūij riña gā\'hue nāgi\'hiô\'</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Si gi\'hiaj guendô\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..af602a3407
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-tt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Көйләүләргә күчү</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Ябу</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..97c7d4e23d
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Ddu ɣer tisɣal</string>
+ </resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..1793bab766
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ug/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">تەڭشەكنى ئاچ</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">بولدىلا</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..691b7dfe8e
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-uk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Перейти до налаштувань</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Відхилити</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..9133982d6d
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-ur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">سیٹنگز پر جائیں</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">برخاست کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..861a4431f6
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-uz/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Sozlamalarni ochish</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Rad qilish</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..6c21554385
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-vi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Đi đến cài đặt</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Bỏ qua</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..55dc8e49db
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-yo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Lọ sí àwọn ètò</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Yọ ọ́ kúrò</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..4ad9164231
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">转至“设置”</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">隐藏</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..52b595544b
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">開啟設定</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">隱藏</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values/mozac_support_base_strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values/mozac_support_base_strings.xml
new file mode 100644
index 0000000000..f542bd4e68
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values/mozac_support_base_strings.xml
@@ -0,0 +1,7 @@
+<?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>
+ <string name="mozac_support_base_locale_preference_key_locale" translatable="false">mozac_support_base_locale_preference_key_locale</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/main/res/values/strings.xml b/mobile/android/android-components/components/support/base/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..128c86862c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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>
+ <!-- Text for the positive action button, that will take the user to the settings page -->
+ <string name="mozac_support_base_permissions_needed_positive_button">Go to settings</string>
+ <!-- Text for the negative action button to dismiss the dialog. -->
+ <string name="mozac_support_base_permissions_needed_negative_button">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/BuildTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/BuildTest.kt
new file mode 100644
index 0000000000..4690c44ab6
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/BuildTest.kt
@@ -0,0 +1,22 @@
+/* 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
+
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class BuildTest {
+ @Test
+ fun `Sanity check Build class`() {
+ assertNotNull(Build.version)
+ assertNotNull(Build.applicationServicesVersion)
+ assertNotNull(Build.gitHash)
+
+ assertTrue(Build.version.isNotBlank())
+ assertTrue(Build.applicationServicesVersion.isNotBlank())
+ assertTrue(Build.gitHash.isNotBlank())
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/android/PaddingTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/android/PaddingTest.kt
new file mode 100644
index 0000000000..423a0a333c
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/android/PaddingTest.kt
@@ -0,0 +1,22 @@
+/* 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.support.base.android
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class PaddingTest {
+
+ @Test
+ fun `padding should init its values`() {
+ val padding = Padding(16, 24, 32, 40)
+
+ val (start, top, end, bottom) = padding
+ assertEquals(start, 16)
+ assertEquals(top, 24)
+ assertEquals(end, 32)
+ assertEquals(bottom, 40)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/ext/NotificationManagerCompatTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/ext/NotificationManagerCompatTest.kt
new file mode 100644
index 0000000000..e1bbffd387
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/ext/NotificationManagerCompatTest.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.support.base.ext
+
+import android.os.Build
+import androidx.core.app.NotificationChannelCompat
+import androidx.core.app.NotificationManagerCompat
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(RobolectricTestRunner::class)
+class NotificationManagerCompatTest {
+
+ private val tested: NotificationManagerCompat = mock()
+
+ @Test
+ fun `WHEN areNotificationsEnabled throws an exception THEN areNotificationsEnabledSafe returns false`() {
+ whenever(tested.areNotificationsEnabled()).thenThrow(java.lang.RuntimeException())
+
+ assertFalse(tested.areNotificationsEnabledSafe())
+ }
+
+ @Test
+ fun `WHEN areNotificationsEnabled returns false THEN areNotificationsEnabledSafe returns false`() {
+ whenever(tested.areNotificationsEnabled()).thenReturn(false)
+
+ assertFalse(tested.areNotificationsEnabledSafe())
+ }
+
+ @Test
+ fun `WHEN areNotificationsEnabled returns true THEN areNotificationsEnabledSafe returns true`() {
+ whenever(tested.areNotificationsEnabled()).thenReturn(true)
+
+ assertTrue(tested.areNotificationsEnabledSafe())
+ }
+
+ @Test
+ fun `WHEN getNotificationChannelCompat returns a channel with IMPORTANCE_DEFAULT and areNotificationsEnabled returns true THEN isNotificationChannelEnabled returns true`() {
+ val testChannel = "test-channel"
+ val notificationChannelCompat =
+ NotificationChannelCompat.Builder(
+ testChannel,
+ NotificationManagerCompat.IMPORTANCE_DEFAULT,
+ ).build()
+
+ whenever(tested.areNotificationsEnabled()).thenReturn(true)
+ whenever(tested.getNotificationChannelCompat(testChannel))
+ .thenReturn(notificationChannelCompat)
+
+ assertTrue(tested.isNotificationChannelEnabled(testChannel))
+ }
+
+ @Test
+ fun `WHEN getNotificationChannelCompat returns a channel with IMPORTANCE_NONE and areNotificationsEnabled returns true THEN isNotificationChannelEnabled returns false`() {
+ val testChannel = "test-channel"
+ val notificationChannelCompat =
+ NotificationChannelCompat.Builder(
+ testChannel,
+ NotificationManagerCompat.IMPORTANCE_NONE,
+ ).build()
+
+ whenever(tested.areNotificationsEnabled()).thenReturn(true)
+ whenever(tested.getNotificationChannelCompat(testChannel))
+ .thenReturn(notificationChannelCompat)
+
+ assertFalse(tested.isNotificationChannelEnabled(testChannel))
+ }
+
+ @Test
+ fun `WHEN getNotificationChannelCompat returns a channel and areNotificationsEnabled returns false THEN isNotificationChannelEnabled returns false`() {
+ val testChannel = "test-channel"
+ val notificationChannelCompat =
+ NotificationChannelCompat.Builder(
+ testChannel,
+ NotificationManagerCompat.IMPORTANCE_DEFAULT,
+ ).build()
+
+ whenever(tested.getNotificationChannelCompat(testChannel))
+ .thenReturn(notificationChannelCompat)
+ whenever(tested.areNotificationsEnabled()).thenReturn(false)
+
+ assertFalse(tested.isNotificationChannelEnabled(testChannel))
+ }
+
+ @Test
+ fun `WHEN getNotificationChannelCompat returns null THEN isNotificationChannelEnabled returns false`() {
+ val testChannel = "test-channel"
+
+ whenever(tested.getNotificationChannelCompat(testChannel)).thenReturn(null)
+ whenever(tested.areNotificationsEnabled()).thenReturn(true)
+
+ assertFalse(tested.isNotificationChannelEnabled(testChannel))
+ }
+
+ @Test
+ fun `WHEN sdk less than 26 and areNotificationsEnabled returns true THEN isNotificationChannelEnabled returns true`() {
+ val testChannel = "test-channel"
+
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 25)
+
+ whenever(tested.areNotificationsEnabled()).thenReturn(true)
+
+ assertTrue(tested.isNotificationChannelEnabled(testChannel))
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/ext/ThrowableTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/ext/ThrowableTest.kt
new file mode 100644
index 0000000000..9df2ff345b
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/ext/ThrowableTest.kt
@@ -0,0 +1,70 @@
+/* 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.support.base.ext
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.lang.RuntimeException
+
+@RunWith(AndroidJUnit4::class)
+class ThrowableTest {
+ @Test
+ fun `throwable stack trace to string is limited to max length`() {
+ val throwable = RuntimeException("TEST ONLY")
+
+ assertEquals(throwable.getStacktraceAsString(1).length, 1)
+ assertEquals(throwable.getStacktraceAsString(10).length, 10)
+ }
+
+ @Test
+ fun `throwable stack trace to string works correctly`() {
+ val throwable = RuntimeException("TEST ONLY")
+
+ assert(throwable.getStacktraceAsString().contains("mozilla.components.support.base.ext.ThrowableTest.throwable stack trace to string works correctly(ThrowableTest.kt:"))
+ }
+
+ @Test
+ fun `throwable stack trace to json string works correctly`() {
+ val throwable = RuntimeException("TEST ONLY")
+
+ val json = JSONObject(throwable.getStacktraceAsJsonString())
+ val exception = json.getJSONObject("exception")
+ val values = exception.getJSONArray("values")
+ assertEquals(values.length(), 1)
+ val stacktrace = values.getJSONObject(0).getJSONObject("stacktrace")
+ assertEquals(stacktrace.getString("type"), "RuntimeException")
+ assertEquals(stacktrace.getString("module"), "java.lang")
+ assertEquals(stacktrace.getString("value"), "TEST ONLY")
+ val frames = stacktrace.getJSONArray("frames")
+ val firstFrame = frames.getJSONObject(0)
+ assert(firstFrame.getBoolean("in_app"))
+ assertEquals(firstFrame.getString("filename"), "ThrowableTest.kt")
+ assertEquals(firstFrame.getString("module"), "mozilla.components.support.base.ext.ThrowableTest")
+ assertEquals(firstFrame.getString("function"), "throwable stack trace to json string works correctly")
+ }
+
+ @Test
+ fun `throwable stack trace to json string max frames works correctly`() {
+ val throwable = RuntimeException("TEST ONLY")
+
+ var json = JSONObject(throwable.getStacktraceAsJsonString(1))
+ var frames = json.getJSONObject("exception").getJSONArray("values").getJSONObject(0)
+ .getJSONObject("stacktrace").getJSONArray("frames")
+ assertEquals(frames.length(), 1)
+
+ json = JSONObject(throwable.getStacktraceAsJsonString(5))
+ frames = json.getJSONObject("exception").getJSONArray("values").getJSONObject(0)
+ .getJSONObject("stacktrace").getJSONArray("frames")
+ assertEquals(frames.length(), 5)
+
+ json = JSONObject(throwable.getStacktraceAsJsonString(10))
+ frames = json.getJSONObject("exception").getJSONArray("values").getJSONObject(0)
+ .getJSONObject("stacktrace").getJSONArray("frames")
+ assertEquals(frames.length(), 10)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactProcessorTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactProcessorTest.kt
new file mode 100644
index 0000000000..3e6f8e13d9
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactProcessorTest.kt
@@ -0,0 +1,36 @@
+/* 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.support.base.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.test.mock
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+
+class FactProcessorTest {
+ @Before
+ @After
+ fun cleanUp() {
+ Facts.clearProcessors()
+ }
+
+ @Test
+ fun `register extension method regsiters processor on Facts singleton`() {
+ val processor: FactProcessor = mock()
+ processor.register()
+
+ val fact = Fact(
+ Component.SUPPORT_TEST,
+ Action.CLICK,
+ "test",
+ )
+
+ fact.collect()
+
+ Mockito.verify(processor).process(fact)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactTest.kt
new file mode 100644
index 0000000000..06d7409ff6
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactTest.kt
@@ -0,0 +1,65 @@
+/* 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.support.base.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.test.mock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class FactTest {
+ @Before
+ @After
+ fun cleanUp() {
+ Facts.clearProcessors()
+ }
+
+ @Test
+ fun `Collect extension method forwards Fact to Facts singleton`() {
+ val processor: FactProcessor = mock()
+ Facts.registerProcessor(processor)
+
+ val fact = Fact(
+ Component.SUPPORT_TEST,
+ Action.CLICK,
+ "test",
+ )
+
+ fact.collect()
+
+ verify(processor).process(fact)
+ }
+
+ @Test
+ fun `Accessing values`() {
+ val fact = Fact(
+ Component.SUPPORT_TEST,
+ Action.CLICK,
+ "test-item",
+ "test-value",
+ mapOf(
+ "key1" to "value1",
+ "key2" to "value2",
+ ),
+ )
+
+ assertEquals(Component.SUPPORT_TEST, fact.component)
+ assertEquals(Action.CLICK, fact.action)
+ assertEquals("test-item", fact.item)
+ assertEquals("test-value", fact.value)
+
+ assertNotNull(fact.metadata)
+ assertEquals(2, fact.metadata!!.size)
+ assertTrue(fact.metadata!!.contains("key1"))
+ assertTrue(fact.metadata!!.contains("key2"))
+ assertEquals("value1", fact.metadata!!["key1"])
+ assertEquals("value2", fact.metadata!!["key2"])
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactsTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactsTest.kt
new file mode 100644
index 0000000000..7b6bf7c9ce
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/FactsTest.kt
@@ -0,0 +1,63 @@
+/* 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.support.base.facts
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.test.mock
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class FactsTest {
+ @Before
+ @After
+ fun cleanUp() {
+ Facts.clearProcessors()
+ }
+
+ @Test
+ fun `Collecting fact without processor`() {
+ Facts.collect(
+ Fact(
+ Component.SUPPORT_TEST,
+ Action.CLICK,
+ "test",
+ ),
+ )
+ }
+
+ @Test
+ fun `Collected fact is forwarded to processors`() {
+ val processor1: FactProcessor = mock()
+ val processor2: FactProcessor = mock()
+
+ Facts
+ .registerProcessor(processor1)
+ .registerProcessor(processor2)
+
+ val fact1 = Fact(
+ Component.SUPPORT_TEST,
+ Action.CLICK,
+ "test",
+ )
+
+ Facts.collect(fact1)
+
+ verify(processor1).process(fact1)
+ verify(processor2).process(fact1)
+
+ val fact2 = Fact(
+ Component.SUPPORT_BASE,
+ Action.TOGGLE,
+ "test",
+ )
+
+ Facts.collect(fact2)
+
+ verify(processor1).process(fact2)
+ verify(processor2).process(fact2)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/processor/LogFactProcessorTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/processor/LogFactProcessorTest.kt
new file mode 100644
index 0000000000..45023d4876
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/facts/processor/LogFactProcessorTest.kt
@@ -0,0 +1,31 @@
+/* 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.support.base.facts.processor
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class LogFactProcessorTest {
+ @Test
+ fun `Processor forwards Fact objects to the log`() {
+ val logger: Logger = mock()
+ val processor = LogFactProcessor(logger)
+
+ val fact = Fact(
+ Component.SUPPORT_TEST,
+ Action.CLICK,
+ "test",
+ )
+
+ processor.process(fact)
+
+ verify(logger).debug(fact.toString())
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapperTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapperTest.kt
new file mode 100644
index 0000000000..a07364acc2
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapperTest.kt
@@ -0,0 +1,474 @@
+/* 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.support.base.feature
+
+import android.app.Activity
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.os.Looper.getMainLooper
+import android.view.View
+import android.view.WindowManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class ViewBoundFeatureWrapperTest {
+
+ @Test
+ fun `Calling onBackPressed on an empty wrapper returns false`() {
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+ assertFalse(wrapper.onBackPressed())
+ }
+
+ @Test
+ fun `onBackPressed is forwarded to feature`() {
+ val feature = MockFeatureWithUserInteractionHandler(onBackPressed = true)
+
+ val wrapper = ViewBoundFeatureWrapper(
+ feature = feature,
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ assertTrue(wrapper.onBackPressed())
+ assertTrue(feature.onBackPressedInvoked)
+
+ assertFalse(
+ ViewBoundFeatureWrapper(
+ feature = MockFeatureWithUserInteractionHandler(onBackPressed = false),
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ ).onBackPressed(),
+ )
+ }
+
+ @Test
+ fun `Calling onActivityResult on an empty wrapper returns false`() {
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+ assertFalse(wrapper.onActivityResult(0, mock(), RESULT_OK))
+ }
+
+ @Test
+ fun `onActivityResult is forwarded to feature`() {
+ val feature = MockFeatureWithActivityResultHandler(onActivityResult = true)
+
+ val wrapper = ViewBoundFeatureWrapper(
+ feature = feature,
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ assertTrue(wrapper.onActivityResult(1, null, RESULT_OK))
+ assertTrue(feature.onActivityResultHandled)
+
+ assertFalse(
+ ViewBoundFeatureWrapper(
+ feature = MockFeatureWithActivityResultHandler(onActivityResult = false),
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ ).onActivityResult(0, mock(), RESULT_OK),
+ )
+ }
+
+ @Test
+ fun `Setting feature registers lifecycle and view observers`() {
+ val lifecycle = spy(MockedLifecycle(Lifecycle.State.CREATED))
+ val owner = MockedLifecycleOwner(lifecycle)
+ val view: View = mock()
+
+ val feature = MockFeature()
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+
+ wrapper.set(
+ feature = feature,
+ owner = owner,
+ view = view,
+ )
+
+ verify(lifecycle).addObserver(any())
+ verify(view).addOnAttachStateChangeListener(any())
+ }
+
+ @Test
+ fun `Lifecycle start event is forwarded to feature`() {
+ val lifecycle = spy(MockedLifecycle(Lifecycle.State.CREATED))
+ val owner = MockedLifecycleOwner(lifecycle)
+ val view: View = mock()
+
+ val feature = spy(MockFeature())
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+
+ wrapper.set(
+ feature = feature,
+ owner = owner,
+ view = view,
+ )
+
+ verify(feature, never()).start()
+ assertFalse(feature.started)
+
+ lifecycle.observer!!.onStart(wrapper.owner!!)
+
+ verify(feature).start()
+ assertTrue(feature.started)
+ }
+
+ @Test
+ fun `Lifecycle stop event is forwarded to feature`() {
+ val lifecycle = spy(MockedLifecycle(Lifecycle.State.CREATED))
+ val owner = MockedLifecycleOwner(lifecycle)
+ val view: View = mock()
+
+ val feature = spy(MockFeature())
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+
+ wrapper.set(
+ feature = feature,
+ owner = owner,
+ view = view,
+ )
+
+ verify(feature, never()).stop()
+ assertFalse(feature.started)
+
+ lifecycle.observer!!.onStop(wrapper.owner!!)
+
+ verify(feature).stop()
+ assertFalse(feature.started)
+ }
+
+ @Test
+ fun `Detaching View will call clear on wrapper`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(activity)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(view.isAttachedToWindow)
+
+ val wrapper = spy(ViewBoundFeatureWrapper<MockFeature>())
+
+ wrapper.set(
+ feature = MockFeature(),
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = view,
+ )
+
+ verify(wrapper, never()).clear()
+
+ activity.windowManager.removeView(view)
+ shadowOf(getMainLooper()).idle()
+
+ verify(wrapper).clear()
+ }
+
+ @Test
+ fun `Getter will return feature or null`() {
+ val feature = MockFeature()
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+
+ assertNull(wrapper.get())
+
+ wrapper.set(
+ feature = feature,
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ assertEquals(feature, wrapper.get())
+
+ wrapper.clear()
+
+ assertNull(wrapper.get())
+ }
+
+ @Test
+ fun `WithFeature block is executed if feature reference is set`() {
+ assertTrue(
+ run {
+ var blockExecuted = false
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+ wrapper.withFeature {
+ blockExecuted = true
+ }
+
+ assertFalse(blockExecuted)
+ true
+ },
+ )
+
+ assertTrue(
+ run {
+ var blockExecuted = false
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+ wrapper.set(
+ feature = MockFeature(),
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ wrapper.withFeature {
+ blockExecuted = true
+ }
+
+ assertTrue(blockExecuted)
+ true
+ },
+ )
+
+ assertTrue(
+ run {
+ var blockExecuted = false
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+ wrapper.set(
+ feature = MockFeature(),
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ wrapper.clear()
+
+ wrapper.withFeature {
+ blockExecuted = true
+ }
+
+ assertFalse(blockExecuted)
+ true
+ },
+ )
+ }
+
+ @Test
+ fun `Clear removes observers`() {
+ val lifecycle = spy(MockedLifecycle(Lifecycle.State.CREATED))
+ val owner = MockedLifecycleOwner(lifecycle)
+ val view: View = mock()
+
+ val feature = MockFeature()
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+
+ wrapper.set(
+ feature = feature,
+ owner = owner,
+ view = view,
+ )
+
+ verify(lifecycle, never()).removeObserver(any())
+ verify(view, never()).removeOnAttachStateChangeListener(any())
+
+ wrapper.clear()
+
+ verify(lifecycle).removeObserver(any())
+ verify(view).removeOnAttachStateChangeListener(any())
+ }
+
+ @Test
+ fun `Clear stops started feature`() {
+ val lifecycle = spy(MockedLifecycle(Lifecycle.State.CREATED))
+ val owner = MockedLifecycleOwner(lifecycle)
+
+ val feature = spy(MockFeature())
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+
+ wrapper.set(
+ feature = feature,
+ owner = owner,
+ view = mock(),
+ )
+
+ lifecycle.observer!!.onStart(wrapper.owner!!)
+
+ verify(feature, never()).stop()
+
+ wrapper.clear()
+
+ verify(feature).stop()
+ }
+
+ @Test
+ fun `Clear does not stop already stopped feature`() {
+ val lifecycle = spy(MockedLifecycle(Lifecycle.State.CREATED))
+ val owner = MockedLifecycleOwner(lifecycle)
+
+ val feature = spy(MockFeature())
+
+ val wrapper = ViewBoundFeatureWrapper<MockFeature>()
+
+ wrapper.set(
+ feature = feature,
+ owner = owner,
+ view = mock(),
+ )
+
+ lifecycle.observer!!.onStart(wrapper.owner!!)
+ lifecycle.observer!!.onStop(wrapper.owner!!)
+
+ reset(feature)
+
+ wrapper.clear()
+
+ verify(feature, never()).stop()
+ }
+
+ @Test(expected = IllegalAccessError::class)
+ fun `onBackPressed throws if feature does not implement BackHandler`() {
+ val feature = MockFeature()
+
+ val wrapper = ViewBoundFeatureWrapper(
+ feature = feature,
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ wrapper.onBackPressed()
+ }
+
+ @Test
+ fun `Setting a feature clears a previously existing feature`() {
+ val feature = MockFeature()
+
+ val wrapper = spy(ViewBoundFeatureWrapper<MockFeature>())
+ wrapper.set(
+ feature = feature,
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ assertEquals(feature, wrapper.get())
+ verify(wrapper, never()).clear()
+
+ val newFeature = MockFeature()
+ wrapper.set(
+ feature = newFeature,
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ verify(wrapper).clear()
+ assertEquals(newFeature, wrapper.get())
+ }
+
+ @Test
+ fun `Clear can be called multiple times without side effects`() {
+ val feature = MockFeature()
+
+ val wrapper = spy(ViewBoundFeatureWrapper<MockFeature>())
+ wrapper.set(
+ feature = feature,
+ owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)),
+ view = mock(),
+ )
+
+ assertEquals(feature, wrapper.get())
+
+ wrapper.clear()
+ wrapper.clear()
+ wrapper.clear()
+
+ assertNull(wrapper.get())
+ }
+
+ @Test
+ fun `Lifecycle destroy event will clear wrapper`() {
+ val lifecycle = spy(MockedLifecycle(Lifecycle.State.CREATED))
+ val owner = MockedLifecycleOwner(lifecycle)
+ val view: View = mock()
+
+ val feature = spy(MockFeature())
+
+ val wrapper = spy(ViewBoundFeatureWrapper<MockFeature>())
+
+ wrapper.set(
+ feature = feature,
+ owner = owner,
+ view = view,
+ )
+
+ verify(wrapper, never()).clear()
+
+ lifecycle.observer!!.onDestroy(wrapper.owner!!)
+
+ verify(wrapper).clear()
+ }
+}
+
+private open class MockFeature : LifecycleAwareFeature {
+ var started = false
+ private set
+
+ override fun start() {
+ started = true
+ }
+
+ override fun stop() {
+ started = false
+ }
+}
+
+private class MockFeatureWithUserInteractionHandler(
+ private val onBackPressed: Boolean = false,
+) : MockFeature(), UserInteractionHandler {
+ var onBackPressedInvoked = false
+ private set
+
+ override fun onBackPressed(): Boolean {
+ onBackPressedInvoked = true
+ return onBackPressed
+ }
+}
+
+private class MockFeatureWithActivityResultHandler(
+ private val onActivityResult: Boolean = false,
+) : MockFeature(), ActivityResultHandler {
+ var onActivityResultHandled = false
+ private set
+
+ override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
+ onActivityResultHandled = true
+ return onActivityResult
+ }
+}
+
+private class MockedLifecycle(var state: State) : Lifecycle() {
+ var observer: LifecycleBinding<*>? = null
+
+ override fun addObserver(observer: LifecycleObserver) {
+ this.observer = observer as LifecycleBinding<*>
+ }
+
+ override fun removeObserver(observer: LifecycleObserver) {
+ this.observer = null
+ }
+
+ override val currentState: State
+ get() = state
+}
+
+private class MockedLifecycleOwner(override val lifecycle: MockedLifecycle) : LifecycleOwner
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/LogTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/LogTest.kt
new file mode 100644
index 0000000000..e6d1953c71
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/LogTest.kt
@@ -0,0 +1,91 @@
+/* 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.support.base.log
+
+import mozilla.components.support.base.log.fake.FakeLogSink
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LogTest {
+ @After
+ fun tearDown() = Log.reset()
+
+ @Test
+ fun `log call will be forwarded to sinks`() {
+ val sink = FakeLogSink()
+ Log.addSink(sink)
+
+ val exception = RuntimeException()
+
+ Log.log(Log.Priority.DEBUG, "Tag", exception, "Hello World!")
+
+ assertEquals(
+ FakeLogSink.Entry(
+ priority = Log.Priority.DEBUG,
+ tag = "Tag",
+ throwable = exception,
+ message = "Hello World!",
+ ),
+ sink.logs.first(),
+ )
+ }
+
+ @Test
+ fun `log call will not be forwarded if log level is too low`() {
+ val sink = FakeLogSink()
+ Log.addSink(sink)
+
+ val exception = RuntimeException()
+
+ Log.logLevel = Log.Priority.INFO
+ Log.log(Log.Priority.DEBUG, "Tag", exception, "Hello World!")
+
+ assertTrue(sink.logs.isEmpty())
+ }
+
+ @Test
+ fun `log messages with the exact log level will be forwarded`() {
+ val sink = FakeLogSink()
+ Log.addSink(sink)
+
+ val exception = RuntimeException()
+
+ Log.logLevel = Log.Priority.WARN
+ Log.log(Log.Priority.WARN, "Tag", exception, "Hello World!")
+
+ assertEquals(
+ FakeLogSink.Entry(
+ priority = Log.Priority.WARN,
+ tag = "Tag",
+ throwable = exception,
+ message = "Hello World!",
+ ),
+ sink.logs.first(),
+ )
+ }
+
+ @Test
+ fun `log messages with higher log level will be forwarded`() {
+ val sink = FakeLogSink()
+ Log.addSink(sink)
+
+ val exception = RuntimeException()
+
+ Log.logLevel = Log.Priority.WARN
+ Log.log(Log.Priority.ERROR, "Tag", exception, "Hello World!")
+
+ assertEquals(
+ FakeLogSink.Entry(
+ priority = Log.Priority.ERROR,
+ tag = "Tag",
+ throwable = exception,
+ message = "Hello World!",
+ ),
+ sink.logs.first(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/fake/FakeLogSink.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/fake/FakeLogSink.kt
new file mode 100644
index 0000000000..681a0689b0
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/fake/FakeLogSink.kt
@@ -0,0 +1,24 @@
+/* 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.support.base.log.fake
+
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.sink.LogSink
+
+class FakeLogSink : LogSink {
+
+ val logs = mutableListOf<Entry>()
+
+ data class Entry(
+ val priority: Log.Priority,
+ val tag: String?,
+ val throwable: Throwable?,
+ val message: String,
+ )
+
+ override fun log(priority: Log.Priority, tag: String?, throwable: Throwable?, message: String) {
+ logs.add(Entry(priority, tag, throwable, message))
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/logger/LoggerTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/logger/LoggerTest.kt
new file mode 100644
index 0000000000..19f15815ca
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/logger/LoggerTest.kt
@@ -0,0 +1,179 @@
+/* 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.support.base.log.logger
+
+import android.os.SystemClock
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.fake.FakeLogSink
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class LoggerTest {
+
+ private lateinit var sink: FakeLogSink
+
+ @Before
+ fun setUp() {
+ sink = FakeLogSink()
+ Log.addSink(sink)
+ }
+
+ @After
+ fun tearDown() = Log.reset()
+
+ @Test
+ fun `debug calls are forwarded to Log and sinks`() {
+ val logger = Logger("Tag")
+
+ val exception = RuntimeException()
+ logger.debug(message = "Hello", throwable = exception)
+
+ assertEquals(
+ FakeLogSink.Entry(
+ priority = Log.Priority.DEBUG,
+ tag = "Tag",
+ throwable = exception,
+ message = "Hello",
+ ),
+ sink.logs.first(),
+ )
+ }
+
+ @Test
+ fun `info calls are forwarded to Log and sinks`() {
+ val logger = Logger("Tag")
+
+ val exception = RuntimeException()
+ logger.info(message = "Hello", throwable = exception)
+
+ assertEquals(
+ FakeLogSink.Entry(
+ priority = Log.Priority.INFO,
+ tag = "Tag",
+ throwable = exception,
+ message = "Hello",
+ ),
+ sink.logs.first(),
+ )
+ }
+
+ @Test
+ fun `warn calls are forwarded to Log and sinks`() {
+ val logger = Logger("Tag")
+
+ val exception = RuntimeException()
+ logger.warn(message = "Hello", throwable = exception)
+
+ assertEquals(
+ FakeLogSink.Entry(
+ priority = Log.Priority.WARN,
+ tag = "Tag",
+ throwable = exception,
+ message = "Hello",
+ ),
+ sink.logs.first(),
+ )
+ }
+
+ @Test
+ fun `error calls are forwarded to Log and sinks`() {
+ val logger = Logger("Tag")
+
+ val exception = RuntimeException()
+ logger.error(message = "Hello", throwable = exception)
+
+ assertEquals(
+ FakeLogSink.Entry(
+ priority = Log.Priority.ERROR,
+ tag = "Tag",
+ throwable = exception,
+ message = "Hello",
+ ),
+ sink.logs.first(),
+ )
+ }
+
+ @Test
+ fun `Companion object provides methods using shared Logger instance without tag`() {
+ val debugException = RuntimeException()
+ Logger.debug("debug message", debugException)
+
+ val infoException = RuntimeException()
+ Logger.info("info message", infoException)
+
+ val warnException = RuntimeException()
+ Logger.warn("warn message", warnException)
+
+ val errorException = RuntimeException()
+ Logger.error("error message", errorException)
+
+ val debugLog = FakeLogSink.Entry(
+ priority = Log.Priority.DEBUG,
+ tag = null,
+ throwable = debugException,
+ message = "debug message",
+ )
+ val infoLog = debugLog.copy(
+ message = "info message",
+ throwable = infoException,
+ priority = Log.Priority.INFO,
+ )
+ val warnLog = debugLog.copy(
+ message = "warn message",
+ throwable = warnException,
+ priority = Log.Priority.WARN,
+ )
+ val errorLog = debugLog.copy(
+ message = "error message",
+ throwable = errorException,
+ priority = Log.Priority.ERROR,
+ )
+ val expectedLogs = listOf(debugLog, infoLog, warnLog, errorLog)
+
+ assertEquals(expectedLogs, sink.logs)
+ }
+
+ @Test
+ fun `measure call logs two messages`() {
+ Logger.measure("testing") { /* do nothing */ }
+
+ val firstLog = FakeLogSink.Entry(
+ priority = Log.Priority.DEBUG,
+ tag = null,
+ throwable = null,
+ message = "⇢ testing",
+ )
+ val secondLog = firstLog.copy(message = "⇠ testing [0ms]")
+ val expectedLogs = listOf(firstLog, secondLog)
+
+ assertEquals(expectedLogs, sink.logs)
+ }
+
+ @Test
+ fun `measure call measures time inside block`() {
+ val logger = Logger("WithTag")
+
+ logger.measure("testing") {
+ // Pretend to do something
+ SystemClock.sleep(10)
+ }
+
+ val firstLog = FakeLogSink.Entry(
+ priority = Log.Priority.DEBUG,
+ tag = "WithTag",
+ throwable = null,
+ message = "⇢ testing",
+ )
+ val secondLog = firstLog.copy(message = "⇠ testing [10ms]")
+ val expectedLogs = listOf(firstLog, secondLog)
+
+ assertEquals(expectedLogs, sink.logs)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/sink/AndroidLogSinkTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/sink/AndroidLogSinkTest.kt
new file mode 100644
index 0000000000..ef5bdd85ba
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/log/sink/AndroidLogSinkTest.kt
@@ -0,0 +1,183 @@
+/* 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.support.base.log.sink
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.log.Log
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowLog
+import java.io.PrintWriter
+
+@RunWith(AndroidJUnit4::class)
+class AndroidLogSinkTest {
+
+ @Before
+ fun setUp() {
+ ShadowLog.clear()
+ }
+
+ @Test
+ fun `debug log will be print to Android log`() {
+ val sink = AndroidLogSink()
+ sink.log(Log.Priority.DEBUG, "Tag", message = "Hello World!")
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals("Hello World!", logs.last().msg)
+ assertEquals("Tag", logs.last().tag)
+ assertNull(logs.last().throwable)
+ assertEquals(android.util.Log.DEBUG, logs.last().type)
+ }
+
+ @Test
+ fun `info log will be print to Android log`() {
+ val sink = AndroidLogSink()
+ sink.log(Log.Priority.INFO, "Tag", message = "Hello World!")
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals("Hello World!", logs.last().msg)
+ assertEquals("Tag", logs.last().tag)
+ assertNull(logs.last().throwable)
+ assertEquals(android.util.Log.INFO, logs.last().type)
+ }
+
+ @Test
+ fun `warn log will be print to Android log`() {
+ val sink = AndroidLogSink()
+ sink.log(Log.Priority.WARN, "Tag", message = "Hello World!")
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals("Hello World!", logs.last().msg)
+ assertEquals("Tag", logs.last().tag)
+ assertNull(logs.last().throwable)
+ assertEquals(android.util.Log.WARN, logs.last().type)
+ }
+
+ @Test
+ fun `error log will be print to Android log`() {
+ val sink = AndroidLogSink()
+ sink.log(Log.Priority.ERROR, "Tag", message = "Hello World!")
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals("Hello World!", logs.last().msg)
+ assertEquals("Tag", logs.last().tag)
+ assertNull(logs.last().throwable)
+ assertEquals(android.util.Log.ERROR, logs.last().type)
+ }
+
+ @Test
+ fun `Sink will use default tag if non is provided`() {
+ val sink = AndroidLogSink()
+ sink.log(message = "Hello!")
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals("Hello!", logs.last().msg)
+ assertEquals("App", logs.last().tag)
+ }
+
+ @Test
+ fun `Sink will use provided tag`() {
+ val sink = AndroidLogSink("Testing")
+ sink.log(message = "What is this?")
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals("What is this?", logs.last().msg)
+ assertEquals("Testing", logs.last().tag)
+ }
+
+ @Test
+ @Config(sdk = [21])
+ fun `Tag will be truncated on SDK 21+`() {
+ val sink = AndroidLogSink("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789")
+ sink.log(message = "Hello!")
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals("Hello!", logs.last().msg)
+ assertEquals("ABCDEFGHIJKLMNOPQRSTUVW", logs.last().tag)
+
+ ShadowLog.clear()
+
+ sink.log(message = "Yes!", tag = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ")
+
+ val logs2 = ShadowLog.getLogs()
+
+ assertEquals(1, logs2.size)
+ assertEquals("Yes!", logs2.last().msg)
+ assertEquals("1234567890ABCDEFGHIJKLM", logs2.last().tag)
+
+ ShadowLog.clear()
+
+ sink.log(message = "No!", tag = "Short")
+
+ val logs3 = ShadowLog.getLogs()
+
+ assertEquals(1, logs3.size)
+ assertEquals("No!", logs3.last().msg)
+ assertEquals("Short", logs3.last().tag)
+ }
+
+ @Test
+ @Config(sdk = [24])
+ fun `Tag will not be truncated on SDK 24+`() {
+ val sink = AndroidLogSink("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789")
+ sink.log(message = "Hello!")
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals("Hello!", logs.last().msg)
+ assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789", logs.last().tag)
+
+ ShadowLog.clear()
+
+ sink.log(message = "Yes!", tag = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ")
+
+ val logs2 = ShadowLog.getLogs()
+
+ assertEquals(1, logs2.size)
+ assertEquals("Yes!", logs2.last().msg)
+ assertEquals("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", logs2.last().tag)
+
+ ShadowLog.clear()
+
+ sink.log(message = "No!", tag = "Short")
+
+ val logs3 = ShadowLog.getLogs()
+
+ assertEquals(1, logs3.size)
+ assertEquals("No!", logs3.last().msg)
+ assertEquals("Short", logs3.last().tag)
+ }
+
+ @Test
+ fun `Sink will print stacktrace of throwable and message`() {
+ val sink = AndroidLogSink()
+
+ sink.log(message = "An error occurred", throwable = MockThrowable())
+
+ val logs = ShadowLog.getLogs()
+ assertEquals(1, logs.size)
+ assertEquals(
+ "An error occurred\njava.lang.RuntimeException: This is broken\n\tat A\n\tat B\n\tat C",
+ logs.last().msg,
+ )
+ }
+
+ private class MockThrowable : Throwable("Kaput") {
+ override fun printStackTrace(writer: PrintWriter) {
+ writer.write("java.lang.RuntimeException: This is broken\n\tat A\n\tat B\n\tat C")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/notification/SharedIdsHelperTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/notification/SharedIdsHelperTest.kt
new file mode 100644
index 0000000000..703282d87e
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/notification/SharedIdsHelperTest.kt
@@ -0,0 +1,71 @@
+/* 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.support.base.notification
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.ids.SharedIdsHelper
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SharedIdsHelperTest {
+
+ @After
+ fun tearDown() {
+ SharedIdsHelper.clear(testContext)
+ SharedIdsHelper.now = { System.currentTimeMillis() }
+ }
+
+ @Test
+ fun `Returns stable ids`() {
+ SharedIdsHelper.now = { 0 }
+
+ val testId = SharedIdsHelper.getIdForTag(testContext, "mozilla.test")
+ assertTrue(testId > 0)
+ assertTrue(testId > 1000)
+ assertEquals(testId, SharedIdsHelper.getIdForTag(testContext, "mozilla.test"))
+ assertEquals(testId, SharedIdsHelper.getIdForTag(testContext, "mozilla.test"))
+
+ val anotherId = SharedIdsHelper.getIdForTag(testContext, "mozilla.test2")
+ assertTrue(testId != anotherId)
+ assertTrue(anotherId > testId)
+ }
+
+ @Test
+ fun `Returns incrementing ids`() {
+ SharedIdsHelper.now = { 0 }
+
+ val testId = SharedIdsHelper.getNextIdForTag(testContext, "mozilla.test")
+ assertTrue(testId > 0)
+ assertTrue(testId > 1000)
+ assertEquals(testId + 1, SharedIdsHelper.getNextIdForTag(testContext, "mozilla.test"))
+ assertEquals(testId + 2, SharedIdsHelper.getNextIdForTag(testContext, "mozilla.test"))
+ }
+
+ @Test
+ fun `Clears unused ids`() {
+ SharedIdsHelper.now = { 0 }
+
+ val firstId = SharedIdsHelper.getIdForTag(testContext, "mozilla.test")
+ val secondId = SharedIdsHelper.getIdForTag(testContext, "mozilla.test.second")
+ assertNotEquals(firstId, secondId)
+
+ SharedIdsHelper.now = { 1000 * 60 * 60 * 24 * 2 }
+ assertEquals(firstId, SharedIdsHelper.getIdForTag(testContext, "mozilla.test"))
+
+ SharedIdsHelper.now = { 1000 * 60 * 60 * 24 * 8 }
+ assertEquals(firstId, SharedIdsHelper.getIdForTag(testContext, "mozilla.test"))
+
+ val updatedId = SharedIdsHelper.getIdForTag(testContext, "mozilla.test.second")
+ assertNotEquals(firstId, updatedId)
+ assertTrue(updatedId > firstId)
+ assertTrue(updatedId > secondId)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/notification/SharedIdsTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/notification/SharedIdsTest.kt
new file mode 100644
index 0000000000..17cca8629a
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/notification/SharedIdsTest.kt
@@ -0,0 +1,55 @@
+/* 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.support.base.notification
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.ids.SharedIds
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_SHARED_PREFERENCES_FILE = "mozilla.test.pref"
+private const val NOTIFICATION_ID_LIFETIME: Long = 1000L * 60L * 60L * 24L * 7L
+private const val ID_OFFSET = 10000
+
+@RunWith(AndroidJUnit4::class)
+class SharedIdsTest {
+
+ @Test
+ fun `Returns stable ids`() {
+ val sharedIds = SharedIds(TEST_SHARED_PREFERENCES_FILE, NOTIFICATION_ID_LIFETIME, ID_OFFSET)
+ val id = sharedIds.getIdForTag(testContext, "mozilla.test")
+
+ assertEquals(ID_OFFSET, id)
+ assertEquals(ID_OFFSET, sharedIds.getIdForTag(testContext, "mozilla.test"))
+ assertEquals(ID_OFFSET, sharedIds.getIdForTag(testContext, "mozilla.test"))
+ }
+
+ @Test
+ fun `Returns incrementing ids`() {
+ val sharedIds = SharedIds(TEST_SHARED_PREFERENCES_FILE, NOTIFICATION_ID_LIFETIME, ID_OFFSET)
+ val id = sharedIds.getNextIdForTag(testContext, "mozilla.test")
+
+ assertEquals(ID_OFFSET, id)
+ assertEquals(ID_OFFSET + 1, sharedIds.getNextIdForTag(testContext, "mozilla.test"))
+ assertEquals(ID_OFFSET + 2, sharedIds.getNextIdForTag(testContext, "mozilla.test"))
+ }
+
+ @Test
+ fun `Remove expired id`() {
+ val sharedIds = SharedIds(TEST_SHARED_PREFERENCES_FILE, NOTIFICATION_ID_LIFETIME, ID_OFFSET)
+ sharedIds.now = { 0 }
+ val id = sharedIds.getIdForTag(testContext, "mozilla.test")
+
+ assertEquals(ID_OFFSET, id)
+
+ sharedIds.now = { NOTIFICATION_ID_LIFETIME + 1 }
+ assertEquals(ID_OFFSET + 1, sharedIds.getIdForTag(testContext, "mozilla.test"))
+
+ sharedIds.now = { (NOTIFICATION_ID_LIFETIME + 1) * 2 }
+ assertEquals(ID_OFFSET + 2, sharedIds.getIdForTag(testContext, "mozilla.test"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/observer/ConsumableTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/observer/ConsumableTest.kt
new file mode 100644
index 0000000000..8a6c4c8966
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/observer/ConsumableTest.kt
@@ -0,0 +1,506 @@
+/* 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.support.base.observer
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ConsumableTest {
+ @Test
+ fun `Consumable created with from() is not consumed and contains value`() {
+ val consumable = Consumable.from(42)
+ assertFalse(consumable.isConsumed())
+ assertEquals(42, consumable.value)
+ }
+
+ @Test
+ fun `Consumable created with empty() is already consumed and contains no value`() {
+ val consumable = Consumable.empty<Int>()
+ assertTrue(consumable.isConsumed())
+ assertNull(consumable.value)
+ }
+
+ @Test
+ fun `value will not be consumed if consuming lambda returns false`() {
+ val consumable = Consumable.from(42)
+
+ assertEquals(42, consumable.value)
+
+ var lambdaExecuted = false
+
+ val consumed = consumable.consume { value ->
+ assertEquals(42, value)
+ lambdaExecuted = true
+ false // Do not consume value
+ }
+
+ assertFalse(consumed)
+ assertTrue(lambdaExecuted)
+ assertFalse(consumable.isConsumed())
+ assertEquals(42, consumable.value)
+ }
+
+ @Test
+ fun `value will be consumed if consuming lambda returns true`() {
+ val consumable = Consumable.from(42)
+
+ assertEquals(42, consumable.value)
+
+ var lambdaExecuted = false
+
+ val consumed = consumable.consume { value ->
+ assertEquals(42, value)
+ lambdaExecuted = true
+ true // Consume value
+ }
+
+ assertTrue(consumed)
+ assertTrue(lambdaExecuted)
+ assertTrue(consumable.isConsumed())
+ assertNull(consumable.value)
+ }
+
+ @Test
+ fun `value will not be consumed if all consuming lambdas do return false`() {
+ val consumer1 = TestConsumer(shouldConsume = false)
+ val consumer2 = TestConsumer(shouldConsume = false)
+ val consumer3 = TestConsumer(shouldConsume = false)
+
+ val consumable = Consumable.from(23)
+ val consumed = consumable.consumeBy(
+ listOf(
+ { value -> consumer1.invoke(value) },
+ { value -> consumer2.invoke(value) },
+ { value -> consumer3.invoke(value) },
+ ),
+ )
+
+ assertFalse(consumed)
+ assertFalse(consumable.isConsumed())
+ assertEquals(23, consumable.value)
+
+ assertTrue(consumer1.callbackTriggered)
+ assertEquals(23, consumer1.callbackValue)
+
+ assertTrue(consumer2.callbackTriggered)
+ assertEquals(23, consumer2.callbackValue)
+
+ assertTrue(consumer3.callbackTriggered)
+ assertEquals(23, consumer3.callbackValue)
+ }
+
+ @Test
+ fun `value will be consumed if at least one lambda returns true`() {
+ val consumer1 = TestConsumer(shouldConsume = false)
+ val consumer2 = TestConsumer(shouldConsume = true)
+ val consumer3 = TestConsumer(shouldConsume = false)
+
+ val consumable = Consumable.from(23)
+ val consumed = consumable.consumeBy(
+ listOf(
+ { value -> consumer1.invoke(value) },
+ { value -> consumer2.invoke(value) },
+ { value -> consumer3.invoke(value) },
+ ),
+ )
+
+ assertTrue(consumed)
+ assertTrue(consumable.isConsumed())
+ assertNull(consumable.value)
+
+ assertTrue(consumer1.callbackTriggered)
+ assertEquals(23, consumer1.callbackValue)
+
+ assertTrue(consumer2.callbackTriggered)
+ assertEquals(23, consumer2.callbackValue)
+
+ assertTrue(consumer3.callbackTriggered)
+ assertEquals(23, consumer3.callbackValue)
+ }
+
+ @Test
+ fun `value will be consumed if multiple lambdas return true`() {
+ val consumer1 = TestConsumer(shouldConsume = true)
+ val consumer2 = TestConsumer(shouldConsume = false)
+ val consumer3 = TestConsumer(shouldConsume = true)
+
+ val consumable = Consumable.from(23)
+ val consumed = consumable.consumeBy(
+ listOf(
+ { value -> consumer1.invoke(value) },
+ { value -> consumer2.invoke(value) },
+ { value -> consumer3.invoke(value) },
+ ),
+ )
+
+ assertTrue(consumed)
+ assertTrue(consumable.isConsumed())
+ assertNull(consumable.value)
+
+ assertTrue(consumer1.callbackTriggered)
+ assertEquals(23, consumer1.callbackValue)
+
+ assertTrue(consumer2.callbackTriggered)
+ assertEquals(23, consumer2.callbackValue)
+
+ assertTrue(consumer3.callbackTriggered)
+ assertEquals(23, consumer3.callbackValue)
+ }
+
+ @Test
+ fun `value can be read and not consumed`() {
+ val consumable = Consumable.from(42)
+
+ assertEquals(42, consumable.peek())
+ assertFalse(consumable.isConsumed())
+
+ consumable.consume { true }
+ assertNull(consumable.peek())
+ }
+
+ @Test
+ fun `listeners are notified when value is consumed`() {
+ var listener1Notified = false
+ var listener2Notified = false
+
+ val consumable = Consumable.from(42) { listener1Notified = true }
+ consumable.onConsume { listener2Notified = true }
+
+ assertFalse(listener1Notified)
+ assertFalse(listener2Notified)
+
+ consumable.consume { true }
+ assertTrue(listener1Notified)
+ assertTrue(listener2Notified)
+ }
+
+ @Test
+ fun `calling consume on an already consumed consumable does nothing`() {
+ val consumable = Consumable.from(42)
+
+ val firstConsumer = TestConsumer(shouldConsume = true)
+ val consumed = consumable.consume(firstConsumer::invoke)
+
+ assertTrue(consumed)
+ assertTrue(firstConsumer.callbackTriggered)
+ assertEquals(42, firstConsumer.callbackValue)
+
+ assertTrue(consumable.isConsumed())
+ assertNull(consumable.value)
+
+ val secondConsumer = TestConsumer(shouldConsume = true)
+ val consumed2 = consumable.consume(secondConsumer::invoke)
+
+ assertFalse(consumed2)
+ assertFalse(secondConsumer.callbackTriggered)
+ assertNull(secondConsumer.callbackValue)
+ }
+
+ @Test
+ fun `calling consumeBy on an already consumed consumable does nothing`() {
+ val consumer1 = TestConsumer(shouldConsume = true)
+ val consumer2 = TestConsumer(shouldConsume = false)
+ val consumer3 = TestConsumer(shouldConsume = true)
+
+ val consumable = Consumable.from(23)
+ assertTrue(consumable.consume { true })
+
+ assertTrue(consumable.isConsumed())
+ assertNull(consumable.value)
+
+ val consumed = consumable.consumeBy(
+ listOf(
+ { value -> consumer1.invoke(value) },
+ { value -> consumer2.invoke(value) },
+ { value -> consumer3.invoke(value) },
+ ),
+ )
+
+ assertFalse(consumed)
+
+ assertFalse(consumer1.callbackTriggered)
+ assertNull(consumer1.callbackValue)
+
+ assertFalse(consumer2.callbackTriggered)
+ assertNull(consumer2.callbackValue)
+
+ assertFalse(consumer3.callbackTriggered)
+ assertNull(consumer3.callbackValue)
+ }
+
+ @Test
+ fun `value will not be consumed if list of consumers is empty`() {
+ val consumable = Consumable.from(23)
+
+ assertFalse(consumable.isConsumed())
+
+ consumable.consumeBy(emptyList())
+
+ assertFalse(consumable.isConsumed())
+ }
+
+ @Test
+ fun `callback gets invoked if value gets consumed`() {
+ var callbackInvoked = false
+
+ val consumable = Consumable.from(42) {
+ callbackInvoked = true
+ }
+
+ consumable.consume { false }
+
+ assertFalse(callbackInvoked)
+
+ consumable.consume { true }
+
+ assertTrue(callbackInvoked)
+ }
+
+ @Test
+ fun `callback gets invoked if one consumer in list consumes value`() {
+ var callbackInvoked = false
+
+ val consumable = Consumable.from(42) {
+ callbackInvoked = true
+ }
+
+ consumable.consumeBy(
+ listOf<(Int) -> Boolean>(
+ { false },
+ { false },
+ { false },
+ { false },
+ ),
+ )
+
+ assertFalse(callbackInvoked)
+
+ consumable.consumeBy(
+ listOf<(Int) -> Boolean>(
+ { false },
+ { false },
+ { true },
+ { false },
+ ),
+ )
+
+ assertTrue(callbackInvoked)
+ }
+
+ @Test
+ fun `stream gets consumed in insertion order`() {
+ val stream = Consumable.stream(1, 2, 3)
+ val consumed = mutableListOf<Int>()
+ stream.consumeAll { consumed.add(it) }
+ assertEquals(listOf(1, 2, 3), consumed)
+ }
+
+ @Test
+ fun `stream can get consumed in multiple steps`() {
+ val stream = Consumable.stream(1, 2, 3)
+ val consumed = mutableListOf<Int>()
+ stream.consumeAll { value -> if (value < 3) consumed.add(value) else false }
+ assertEquals(listOf(1, 2), consumed)
+
+ stream.consumeAll { consumed.add(it) }
+ assertEquals(listOf(1, 2, 3), consumed)
+ }
+
+ @Test
+ fun `stream elements get consumed in insertion order`() {
+ val stream = Consumable.stream(1, 2, 3)
+ val consumed = mutableListOf<Int>()
+ stream.consumeNext { consumed.add(it) }
+ assertEquals(listOf(1), consumed)
+
+ stream.consumeNext { consumed.add(it) }
+ assertEquals(listOf(1, 2), consumed)
+
+ stream.consumeNext { consumed.add(it) }
+ assertEquals(listOf(1, 2, 3), consumed)
+ }
+
+ @Test
+ fun `stream can be consumed by multiple consumers`() {
+ var stream = Consumable.stream(1, 2, 3)
+ var consumed = mutableListOf<Int>()
+
+ // Consume all values using two consumers
+ stream.consumeAllBy(
+ listOf(
+ { value -> consumed.add(value) },
+ { value -> consumed.add(value + 1) },
+ ),
+ )
+ assertEquals(listOf(1, 2, 2, 3, 3, 4), consumed)
+ assertTrue(stream.isConsumed())
+
+ // Consume partial values
+ stream = Consumable.stream(1, 2, 3)
+ consumed = mutableListOf()
+ var allConsumed = stream.consumeAllBy(
+ listOf(
+ { value -> if (value < 3) consumed.add(value) else false },
+ { _ -> false },
+ ),
+ )
+ assertEquals(listOf(1, 2), consumed)
+ assertFalse(allConsumed)
+ assertFalse(stream.isConsumed())
+
+ // Consume remaining values
+ allConsumed = stream.consumeAllBy(
+ listOf(
+ { value -> consumed.add(value) },
+ { _ -> false },
+ ),
+ )
+ assertEquals(listOf(1, 2, 3), consumed)
+ assertTrue(allConsumed)
+ assertTrue(stream.isConsumed())
+
+ // Consume no values
+ stream = Consumable.stream(1, 2, 3)
+ stream.consumeAllBy(
+ listOf(
+ { _ -> false },
+ { _ -> false },
+ ),
+ )
+ assertFalse(stream.isConsumed())
+ }
+
+ @Test
+ fun `stream elements can be consumed by multiple consumers`() {
+ val stream = Consumable.stream(1, 2, 3)
+ val consumed = mutableListOf<Int>()
+ stream.consumeNextBy(
+ listOf(
+ { value -> consumed.add(value) },
+ { value -> consumed.add(value + 1) },
+ ),
+ )
+ assertEquals(listOf(1, 2), consumed)
+
+ stream.consumeNextBy(
+ listOf(
+ { value -> consumed.add(value + 1) },
+ { _ -> false },
+ ),
+ )
+ assertEquals(listOf(1, 2, 3), consumed)
+
+ stream.consumeNextBy(
+ listOf(
+ { _ -> false },
+ { _ -> false },
+ ),
+ )
+ assertFalse(stream.isConsumed())
+
+ stream.consumeNextBy(
+ listOf(
+ { _ -> false },
+ { value -> consumed.add(value + 1) },
+ ),
+ )
+ assertEquals(listOf(1, 2, 3, 4), consumed)
+
+ assertTrue(stream.isConsumed())
+ assertFalse(
+ stream.consumeNextBy(
+ listOf(
+ { _ -> true },
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `stream is consumed when all values are consumed`() {
+ val stream = Consumable.stream(1, 2)
+ assertFalse(stream.isConsumed())
+
+ stream.consumeNext { true }
+ stream.consumeNext { true }
+ assertTrue(stream.isConsumed())
+
+ assertFalse(stream.consumeNext { true })
+ }
+
+ @Test
+ fun `stream retains consumed values`() {
+ val stream = Consumable.stream(1, 2)
+ assertFalse(stream.isEmpty())
+
+ stream.consumeNext { true }
+ stream.consumeNext { true }
+ assertFalse(stream.isEmpty())
+ }
+
+ @Test
+ fun `stream can be appended`() {
+ var stream = Consumable.stream(1)
+ val consumed = mutableListOf<Int>()
+ stream.consumeNext { consumed.add(it) }
+ assertEquals(listOf(1), consumed)
+ assertTrue(stream.isConsumed())
+
+ stream = stream.append(2)
+ assertFalse(stream.isConsumed())
+
+ stream.consumeNext { consumed.add(it) }
+ assertEquals(listOf(1, 2), consumed)
+ assertTrue(stream.isConsumed())
+ }
+
+ @Test
+ fun `values can be removed from stream`() {
+ var stream = Consumable.stream(1, 2)
+ val consumed = mutableListOf<Int>()
+ stream.consumeNext { consumed.add(it) }
+ assertEquals(listOf(1), consumed)
+ assertFalse(stream.isConsumed())
+
+ stream = stream.remove(2)
+ assertTrue(stream.isConsumed())
+
+ assertFalse(stream.consumeNext { consumed.add(it) })
+ assertEquals(listOf(1), consumed)
+ }
+
+ @Test
+ fun `consumed values can be removed from stream`() {
+ var stream = Consumable.stream(1, 2)
+ val consumed = mutableListOf<Int>()
+ stream.consumeNext { consumed.add(it) }
+ assertEquals(listOf(1), consumed)
+
+ stream = stream.removeConsumed()
+ assertFalse(stream.isEmpty())
+
+ stream.consumeNext { consumed.add(it) }
+ assertEquals(listOf(1, 2), consumed)
+
+ stream = stream.removeConsumed()
+ assertTrue(stream.isEmpty())
+ }
+
+ private class TestConsumer(
+ private val shouldConsume: Boolean,
+ ) {
+ var callbackTriggered = false
+ var callbackValue: Int? = null
+
+ fun invoke(value: Int): Boolean {
+ callbackTriggered = true
+ callbackValue = value
+ return shouldConsume
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/observer/ObserverRegistryTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/observer/ObserverRegistryTest.kt
new file mode 100644
index 0000000000..40d2fdd5fc
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/observer/ObserverRegistryTest.kt
@@ -0,0 +1,658 @@
+/* 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.support.base.observer
+
+import android.app.Activity
+import android.os.Looper.getMainLooper
+import android.view.View
+import android.view.WindowManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+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.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class ObserverRegistryTest {
+
+ @Test
+ fun `registered observer gets notified`() {
+ val registry = ObserverRegistry<TestObserver>()
+
+ val observer = TestObserver()
+ registry.register(observer)
+
+ assertFalse(observer.notified)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertTrue(observer.notified)
+ }
+
+ @Test
+ fun `observer gets queued notifications when registered`() {
+ val registry = ObserverRegistry<TestIntObserver>()
+
+ val observer = TestIntObserver()
+ val anotherObserver = TestIntObserver()
+ registry.notifyAtLeastOneObserver {
+ somethingChanged(1)
+ }
+ registry.notifyAtLeastOneObserver {
+ somethingChanged(2)
+ }
+ registry.notifyAtLeastOneObserver {
+ somethingChanged(3)
+ }
+ assertEquals(emptyList<Int>(), observer.notified)
+
+ registry.register(observer)
+ registry.register(anotherObserver)
+ assertEquals(listOf(1, 2, 3), observer.notified)
+ assertEquals(emptyList<Int>(), anotherObserver.notified)
+ }
+
+ @Test
+ fun `observer does not get notified again after unregistering`() {
+ val registry = ObserverRegistry<TestObserver>()
+
+ val observer = TestObserver()
+ registry.register(observer)
+
+ assertFalse(observer.notified)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertTrue(observer.notified)
+
+ observer.notified = false
+ registry.unregister(observer)
+
+ assertFalse(observer.notified)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertFalse(observer.notified)
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `observer gets notified multiple times`() {
+ val registry = ObserverRegistry<TestObserver>()
+
+ val observer = TestObserver()
+ registry.register(observer)
+
+ assertFalse(observer.notified)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertTrue(observer.notified)
+ observer.notified = false
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertTrue(observer.notified)
+ }
+
+ @Test
+ fun `observer does not get notified when unregistered immediately`() {
+ val registry = ObserverRegistry<TestObserver>()
+
+ val observer = TestObserver()
+
+ registry.register(observer)
+ registry.unregister(observer)
+
+ assertFalse(observer.notified)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertFalse(observer.notified)
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `observer will not get registered if lifecycle state is DESTROYED`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ // We cannot set initial DESTROYED state for LifecycleRegistry
+ // so we simulate lifecycle getting destroyed.
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+
+ val registry = ObserverRegistry<TestObserver>()
+ val observer = TestObserver()
+
+ registry.register(observer, owner)
+
+ assertFalse(observer.notified)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertFalse(observer.notified)
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `observer will get removed if lifecycle gets destroyed`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ val registry = ObserverRegistry<TestObserver>()
+ val observer = TestObserver()
+
+ registry.register(observer, owner)
+
+ // Observer gets notified
+ assertFalse(observer.notified)
+ registry.notifyObservers { somethingChanged() }
+ assertTrue(observer.notified)
+
+ // Pretend lifecycle gets destroyed
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+
+ observer.notified = false
+ registry.notifyObservers { somethingChanged() }
+ assertFalse(observer.notified)
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `non-destroy lifecycle changes do not affect observer`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ val registry = ObserverRegistry<TestObserver>()
+ val observer = TestObserver()
+
+ registry.register(observer, owner)
+
+ // STARTED
+ assertFalse(observer.notified)
+ registry.notifyObservers { somethingChanged() }
+ assertTrue(observer.notified)
+
+ // RESUMED
+ owner.lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+ observer.notified = false
+ registry.notifyObservers { somethingChanged() }
+ assertTrue(observer.notified)
+
+ // CREATED
+ owner.lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ observer.notified = false
+ registry.notifyObservers { somethingChanged() }
+ assertTrue(observer.notified)
+ }
+
+ @Test
+ fun `unregisterObservers unregisters all observers`() {
+ val registry = ObserverRegistry<TestObserver>()
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+
+ val observer1 = TestObserver()
+ val observer2 = TestObserver()
+ val observer3 = TestObserver()
+ val observer4 = TestObserver()
+
+ registry.register(observer1)
+ registry.register(observer2)
+ registry.register(observer3, MockedLifecycleOwner(Lifecycle.State.CREATED))
+ registry.register(observer4, view)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(observer1.notified)
+ assertFalse(observer2.notified)
+
+ registry.notifyObservers { somethingChanged() }
+
+ assertTrue(observer1.notified)
+ assertTrue(observer2.notified)
+ assertTrue(observer3.notified)
+ assertTrue(observer4.notified)
+
+ observer1.notified = false
+ observer2.notified = false
+ observer3.notified = false
+ observer4.notified = false
+
+ registry.unregisterObservers()
+
+ registry.notifyObservers { somethingChanged() }
+
+ assertFalse(observer1.notified)
+ assertFalse(observer2.notified)
+ assertFalse(observer3.notified)
+ assertFalse(observer4.notified)
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `unregisterObservers clears references to all observers`() {
+ val registry = ObserverRegistry<TestObserver>()
+
+ val observer1 = TestObserver()
+ val observer2 = TestObserver()
+ val observer3 = TestObserver()
+ val observer4 = TestObserver()
+
+ registry.register(observer1)
+ registry.register(observer2)
+ registry.register(observer3, MockedLifecycleOwner(Lifecycle.State.CREATED))
+ registry.register(observer4, View(testContext))
+
+ registry.unregisterObservers()
+
+ assertFalse(registry.isObserved())
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `unregister removes observers from observers map`() {
+ val registry = ObserverRegistry<String>()
+ val observer = "Observer"
+
+ registry.unregister(observer)
+
+ assertFalse(registry.isObserved())
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `unregister removes observers from lifecycle observers map`() {
+ val registry = ObserverRegistry<String>()
+ val observer = "Observer"
+ val lifecycleOwner = MockedLifecycleOwner(Lifecycle.State.CREATED)
+
+ registry.register(observer, lifecycleOwner)
+ registry.unregister(observer)
+
+ assertFalse(registry.isObserved())
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `unregister removes observers from view observers map`() {
+ val registry = ObserverRegistry<String>()
+ val observer = "Observer"
+ val view = View(testContext)
+
+ registry.register(observer, view)
+ registry.unregister(observer)
+
+ assertFalse(registry.isObserved())
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `observer will not get added if view is detached`() {
+ val view = mock<View>()
+
+ val registry = ObserverRegistry<TestObserver>()
+ val observer = TestObserver()
+
+ @Suppress("UsePropertyAccessSyntax")
+ doReturn(false).`when`(view).isAttachedToWindow()
+
+ registry.register(observer, view)
+
+ assertFalse(observer.notified)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertFalse(observer.notified)
+ }
+
+ @Test
+ fun `observer will get added once view is attached`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+
+ val registry = ObserverRegistry<TestObserver>()
+ val observer = TestObserver()
+
+ assertFalse(view.isAttachedToWindow)
+ registry.register(observer, view)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertFalse(observer.notified)
+
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+ assertTrue(view.isAttachedToWindow)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertTrue(observer.notified)
+ }
+
+ @Test
+ fun `observer will get unregistered if view gets detached`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ val registry = ObserverRegistry<TestObserver>()
+ val observer = TestObserver()
+
+ assertTrue(view.isAttachedToWindow)
+
+ registry.register(observer, view)
+
+ assertFalse(observer.notified)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertTrue(observer.notified)
+
+ observer.notified = false
+
+ activity.windowManager.removeView(view)
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(view.isAttachedToWindow)
+
+ registry.notifyObservers {
+ somethingChanged()
+ }
+
+ assertFalse(observer.notified)
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `unregisterObservers will unregister from view`() {
+ val view: View = mock()
+ doReturn(true).`when`(view).isAttachedToWindow
+
+ val registry = ObserverRegistry<TestObserver>()
+ val observer = TestObserver()
+
+ registry.register(observer, view)
+ verify(view).addOnAttachStateChangeListener(any())
+
+ registry.unregisterObservers()
+ verify(view).removeOnAttachStateChangeListener(any())
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `unregisterObserver will remove attach listener`() {
+ val view: View = mock()
+ doReturn(true).`when`(view).isAttachedToWindow
+
+ val registry = ObserverRegistry<TestObserver>()
+ val observer = TestObserver()
+
+ registry.register(observer, view)
+ verify(view).addOnAttachStateChangeListener(any())
+
+ registry.unregister(observer)
+ verify(view).removeOnAttachStateChangeListener(any())
+
+ assertTrue(registry.checkInternalCollectionsAreEmpty())
+ }
+
+ @Test
+ fun `wrapConsumers will return list of lambdas calling observers`() {
+ val observer1 = TestConsumingObserver(shouldConsume = false)
+ val observer2 = TestConsumingObserver(shouldConsume = true)
+ val observer3 = TestConsumingObserver(shouldConsume = false)
+
+ val registry = ObserverRegistry<TestConsumingObserver>()
+ registry.register(observer1)
+ registry.register(observer2)
+ registry.register(observer3)
+
+ val consumers: List<(Int) -> Boolean> = registry.wrapConsumers { value -> consumeSomething(value) }
+
+ assertFalse(observer1.notified)
+ assertFalse(observer2.notified)
+ assertFalse(observer3.notified)
+
+ val valueToConsume = 23
+ consumers.forEach { it.invoke(valueToConsume) }
+
+ assertTrue(observer1.notified)
+ assertTrue(observer2.notified)
+ assertTrue(observer3.notified)
+
+ assertEquals(valueToConsume, observer1.notifiedWith!!)
+ assertEquals(valueToConsume, observer2.notifiedWith!!)
+ assertEquals(valueToConsume, observer3.notifiedWith!!)
+ }
+
+ @Test
+ fun `observer is paused on lifecycle event (ON_PAUSE)`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.RESUMED)
+
+ val registry = spy(ObserverRegistry<TestObserver>())
+
+ val observer = TestObserver()
+ registry.register(observer, owner, autoPause = false)
+
+ val autoPausedObserver = TestObserver()
+ registry.register(autoPausedObserver, owner, autoPause = true)
+
+ // Both observers get notified
+ assertFalse(observer.notified)
+ assertFalse(autoPausedObserver.notified)
+ registry.notifyObservers { somethingChanged() }
+ assertTrue(observer.notified)
+ assertTrue(autoPausedObserver.notified)
+
+ // Pause observers
+ owner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+
+ observer.notified = false
+ autoPausedObserver.notified = false
+ registry.notifyObservers { somethingChanged() }
+
+ // (Regular) observer still gets notified
+ verify(registry, never()).pauseObserver(observer)
+ assertTrue(observer.notified)
+
+ // (Auto-paused) observer is now paused and no longer gets notified
+ verify(registry).pauseObserver(autoPausedObserver)
+ assertFalse(autoPausedObserver.notified)
+ }
+
+ @Test
+ fun `observer is resumed on lifecycle event (ON_RESUME)`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.RESUMED)
+
+ val registry = spy(ObserverRegistry<TestIntObserver>())
+ val observer = TestIntObserver()
+ registry.register(observer, owner, autoPause = true)
+
+ // Called once on register, since the lifecycle is already resumed
+ verify(registry, times(1)).resumeObserver(observer)
+
+ // Pause observer
+ owner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ registry.notifyObservers { somethingChanged(1) }
+ registry.notifyObservers { somethingChanged(2) }
+ registry.notifyObservers { somethingChanged(3) }
+ assertEquals(emptyList<Int>(), observer.notified)
+
+ // Resume observer
+ owner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+
+ // Resumed observer gets notified
+ registry.notifyObservers { somethingChanged(4) }
+ assertEquals(listOf(4), observer.notified)
+ verify(registry, times(2)).resumeObserver(observer)
+ }
+
+ @Test
+ fun `observer starts paused if needed`() {
+ // Start in state other than RESUMED
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ val registry = spy(ObserverRegistry<TestObserver>())
+
+ val observer = TestObserver()
+ registry.register(observer, owner, autoPause = false)
+
+ val autoPausedObserver = TestObserver()
+ registry.register(autoPausedObserver, owner, autoPause = true)
+
+ verify(registry, never()).pauseObserver(observer)
+ verify(registry).pauseObserver(autoPausedObserver)
+ }
+
+ @Test
+ fun `register observer only once, notify once`() {
+ // Given
+ val observer = TestIntObserver()
+ val registry = ObserverRegistry<TestIntObserver>()
+
+ // When
+ registry.register(observer)
+ registry.register(observer)
+
+ registry.notifyObservers { somethingChanged(1) }
+
+ // Then
+ assertEquals(1, observer.notified.size)
+ }
+
+ @Test
+ fun `register observer only once, don't notify after unregister`() {
+ // Given
+ val observer = TestIntObserver()
+ val registry = ObserverRegistry<TestIntObserver>()
+
+ // When
+ registry.register(observer)
+ registry.register(observer)
+ registry.unregister(observer)
+
+ registry.notifyObservers { somethingChanged(1) }
+
+ // Then
+ assertEquals(0, observer.notified.size)
+ }
+
+ @Test
+ fun `isObserved is true if observers is empty`() {
+ val registry = ObserverRegistry<TestIntObserver>()
+ val observer = TestIntObserver()
+
+ assertFalse(registry.isObserved())
+
+ registry.register(observer)
+
+ assertTrue(registry.isObserved())
+
+ registry.unregister(observer)
+
+ assertFalse(registry.isObserved())
+ }
+
+ @Test
+ fun `isObserved is true if there is still a view observer that may register an observer for a view`() {
+ val registry = ObserverRegistry<TestIntObserver>()
+ val observer: TestIntObserver = mock()
+
+ val view: View = mock()
+ doReturn(false).`when`(view).isAttachedToWindow
+
+ registry.register(observer, view)
+
+ // observer is not registered since the view is not attached yet
+ registry.notifyObservers { somethingChanged(42) }
+ verify(observer, never()).somethingChanged(42)
+
+ // But it still counts as being observed
+ assertTrue(registry.isObserved())
+
+ registry.unregister(observer)
+ assertFalse(registry.isObserved())
+ }
+
+ private class TestObserver {
+ var notified: Boolean = false
+
+ fun somethingChanged() {
+ notified = true
+ }
+ }
+
+ private class TestIntObserver {
+ val notified = mutableListOf<Int>()
+
+ fun somethingChanged(value: Int) {
+ notified += value
+ }
+ }
+
+ private class TestConsumingObserver(
+ private val shouldConsume: Boolean,
+ ) {
+ var notified: Boolean = false
+ var notifiedWith: Int? = null
+
+ fun consumeSomething(value: Int): Boolean {
+ notifiedWith = value
+ notified = true
+ return shouldConsume
+ }
+ }
+
+ private class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {
+ val lifecycleRegistry = LifecycleRegistry(this).apply {
+ currentState = initialState
+ }
+
+ override val lifecycle: Lifecycle = lifecycleRegistry
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/LazyComponentTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/LazyComponentTest.kt
new file mode 100644
index 0000000000..6ba45e099e
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/LazyComponentTest.kt
@@ -0,0 +1,72 @@
+/* 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.support.base.utils
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+class LazyComponentTest {
+
+ private val initCount: Int get() = LazyComponent.initCount.get()
+
+ @Before
+ fun setUp() {
+ LazyComponent.initCount.set(0)
+ }
+
+ @Test
+ fun `WHEN accessing a lazy component THEN it returns the initializer value`() {
+ val actual = LazyComponent { 4 }
+ assertEquals(4, actual.get())
+ }
+
+ @Test
+ fun `WHEN accessing a lazy component THEN the init count is incremented`() {
+ assertEquals(0, initCount)
+
+ val monitored = LazyComponent { 4 }
+ monitored.get()
+
+ assertEquals(1, initCount)
+ }
+
+ @Test
+ fun `WHEN initializing a lazy component THEN the lazy value is not initialized`() {
+ val component = LazyComponent { 4 }
+ assertFalse(component.isInitialized())
+ }
+
+ @Test
+ fun `WHEN initializing and getting a lazy component THEN the lazy value is initialized`() {
+ val component = LazyComponent { 4 }
+ component.get()
+ assertTrue(component.isInitialized())
+ }
+
+ @Test // potentially flaky because it's testing a concurrency condition.
+ fun `WHEN multiple threads try to initialize the same lazy component THEN only one component is initialized`() {
+ var componentsInitialized = 0
+ val component = LazyComponent {
+ componentsInitialized += 1
+ 4
+ }
+
+ // coroutines add complex behavior - suspension, test APIs affecting true concurrency, etc. -
+ // so we use traditional threads for a simpler implementation.
+ val executorService = Executors.newFixedThreadPool(12)
+ for (i in 1..100) {
+ executorService.submit { component.get() }
+ }
+ executorService.shutdown()
+ executorService.awaitTermination(1, TimeUnit.SECONDS)
+
+ assertEquals(1, componentsInitialized)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/NamedThreadFactoryTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/NamedThreadFactoryTest.kt
new file mode 100644
index 0000000000..0789039dcf
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/NamedThreadFactoryTest.kt
@@ -0,0 +1,52 @@
+/* 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.support.base.utils
+
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class NamedThreadFactoryTest {
+
+ private lateinit var runnable: Runnable
+
+ @Before
+ fun setUp() {
+ runnable = mock()
+ }
+
+ @Test
+ fun `WHEN creating a name thread THEN the names match the expected name including incrementing the thread number`() {
+ val expectedPrefix = "A-Name"
+ val factory = NamedThreadFactory(expectedPrefix)
+
+ val thread1 = factory.newThread(runnable)
+ assertEquals("$expectedPrefix-thread-1", thread1.name)
+
+ val thread2 = factory.newThread(runnable)
+ assertEquals("$expectedPrefix-thread-2", thread2.name)
+
+ // Sanity check that we don't need to clean these threads up
+ // (because these threads are not started).
+ assertFalse(thread1.isAlive)
+ assertFalse(thread2.isAlive)
+ }
+
+ @Test
+ fun `WHEN creating a new thread THEN the given runnable is used`() {
+ val factory = NamedThreadFactory("whatever")
+ val thread = factory.newThread(runnable)
+
+ thread.run()
+
+ verify(runnable).run()
+
+ // Sanity check that we don't need to clean up (because these threads are never started).
+ assertFalse(thread.isAlive)
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/SharedPreferencesCacheTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/SharedPreferencesCacheTest.kt
new file mode 100644
index 0000000000..6a00292bc0
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/utils/SharedPreferencesCacheTest.kt
@@ -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/. */
+
+package mozilla.components.support.base.utils
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.test.robolectric.testContext
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SharedPreferencesCacheTest {
+ data class TestType(val someKey: String, val anotherKey: Long)
+
+ class TestTypeCache : SharedPreferencesCache<TestType>(testContext) {
+ override val logger: Logger = Logger("test")
+ override val cacheKey: String = "testKey"
+ override val cacheName: String = "testName"
+
+ override fun TestType.toJSON(): JSONObject {
+ return JSONObject().apply {
+ put("someKey", someKey)
+ put("anotherKey", anotherKey)
+ }
+ }
+
+ override fun fromJSON(obj: JSONObject): TestType {
+ return TestType(obj.getString("someKey"), obj.getLong("anotherKey"))
+ }
+ }
+
+ @Test
+ fun `shared preferences cache basics`() {
+ val cache = TestTypeCache()
+ assertNull(cache.getCached())
+ cache.setToCache(TestType("hi", 177))
+ assertEquals(TestType("hi", 177), cache.getCached())
+
+ // Corrupt the cache...
+ testContext.getSharedPreferences("testName", Context.MODE_PRIVATE).edit()
+ .putString("testKey", "garbage").commit()
+
+ // Should handle bad data as 'no cached values'.
+ assertNull(cache.getCached())
+
+ // Should be able to persist new data after corruption.
+ cache.setToCache(TestType("test", 1337))
+ assertEquals(TestType("test", 1337), cache.getCached())
+
+ // Should be able to clear cache.
+ cache.clear()
+ assertNull(cache.getCached())
+ }
+}
diff --git a/mobile/android/android-components/components/support/base/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/support/base/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/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/support/base/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/base/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/images/README.md b/mobile/android/android-components/components/support/images/README.md
new file mode 100644
index 0000000000..ca05235659
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Images
+
+A collection of helpers for handling images such as icons and thumbnails.
+
+## Usage
+
+### 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:support-images:{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/support/images/build.gradle b/mobile/android/android-components/components/support/images/build.gradle
new file mode 100644
index 0000000000..4ae828bb8a
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/build.gradle
@@ -0,0 +1,70 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ sourceSets {
+ androidTest {
+ // Use the same resources as the unit tests
+ resources.srcDirs += ['src/test/resources']
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.support.images'
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':concept-fetch')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_material
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+}
+
+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/support/images/proguard-rules-consumer.pro b/mobile/android/android-components/components/support/images/proguard-rules-consumer.pro
new file mode 100644
index 0000000000..d3456cd17e
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/proguard-rules-consumer.pro
@@ -0,0 +1 @@
+# ProGuard rules for consumers of this library.
diff --git a/mobile/android/android-components/components/support/images/proguard-rules.pro b/mobile/android/android-components/components/support/images/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/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/support/images/src/androidTest/java/mozilla/components/support/images/decoder/OnDeviceAndroidImageDecoderTest.kt b/mobile/android/android-components/components/support/images/src/androidTest/java/mozilla/components/support/images/decoder/OnDeviceAndroidImageDecoderTest.kt
new file mode 100644
index 0000000000..4f752fe752
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/androidTest/java/mozilla/components/support/images/decoder/OnDeviceAndroidImageDecoderTest.kt
@@ -0,0 +1,114 @@
+/* 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.support.images.decoder
+
+import mozilla.components.support.images.DesiredSize
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+
+class OnDeviceAndroidImageDecoderTest {
+ @Test
+ fun decodingPNG() {
+ val decoder = AndroidImageDecoder()
+
+ val bitmap = decoder.decode(
+ loadImage("png/mozac.png"),
+ DesiredSize(
+ targetSize = 32,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNotNull(bitmap!!)
+ assertEquals(16, bitmap.width)
+ assertEquals(16, bitmap.height)
+ }
+
+ @Test
+ fun decodingGIF() {
+ val decoder = AndroidImageDecoder()
+
+ val bitmap = decoder.decode(
+ loadImage("gif/cat.gif"),
+ DesiredSize(
+ targetSize = 64,
+ minSize = 64,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNotNull(bitmap!!)
+ // 250 / 3 = 83
+ assertEquals(83, bitmap.width)
+ assertEquals(83, bitmap.height)
+ }
+
+ @Test
+ fun decodingJPEG() {
+ val decoder = AndroidImageDecoder()
+
+ val bitmap = decoder.decode(
+ loadImage("jpg/tonys.jpg"),
+ DesiredSize(
+ targetSize = 64,
+ minSize = 64,
+ maxSize = 512,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNotNull(bitmap!!)
+ assertEquals(67, bitmap.width)
+ assertEquals(67, bitmap.height)
+ }
+
+ @Test
+ fun decodingBMP() {
+ val decoder = AndroidImageDecoder()
+
+ val bitmap = decoder.decode(
+ loadImage("bmp/test.bmp"),
+ DesiredSize(
+ targetSize = 64,
+ minSize = 64,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNotNull(bitmap!!)
+ assertEquals(100, bitmap.width)
+ assertEquals(100, bitmap.height)
+ }
+
+ @Test
+ fun decodingWEBP() {
+ val decoder = AndroidImageDecoder()
+
+ val bitmap = decoder.decode(
+ loadImage("webp/test.webp"),
+ DesiredSize(
+ targetSize = 64,
+ minSize = 64,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNotNull(bitmap!!)
+ // 192 / 3 = 64
+ assertEquals(64, bitmap.width)
+ assertEquals(64, bitmap.height)
+ }
+
+ private fun loadImage(fileName: String): ByteArray =
+ javaClass.getResourceAsStream("/$fileName")!!
+ .buffered()
+ .readBytes()
+}
diff --git a/mobile/android/android-components/components/support/images/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/images/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/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/support/images/src/main/java/mozilla/components/support/images/CancelOnDetach.kt b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/CancelOnDetach.kt
new file mode 100644
index 0000000000..f38968530e
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/CancelOnDetach.kt
@@ -0,0 +1,18 @@
+/* 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.support.images
+
+import android.view.View
+import kotlinx.coroutines.Job
+
+/**
+ * Cancels the provided job when a view is detached from the window
+ */
+class CancelOnDetach(private val job: Job) : View.OnAttachStateChangeListener {
+
+ override fun onViewAttachedToWindow(v: View) = Unit
+
+ override fun onViewDetachedFromWindow(v: View) = job.cancel()
+}
diff --git a/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/DesiredSize.kt b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/DesiredSize.kt
new file mode 100644
index 0000000000..acb9d58b8e
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/DesiredSize.kt
@@ -0,0 +1,24 @@
+/* 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.support.images
+
+import androidx.annotation.Px
+
+/**
+ * Represents the desired size of an images to be loaded.
+ *
+ * @property targetSize The size the image will be displayed at, in pixels.
+ * @property minSize The minimum size of an image before it will be thrown out, in pixels.
+ * @property maxSize The maximum size of an image before it will be thrown out, in pixels.
+ * Extremely large images are suspicious and should be ignored.
+ * @property maxScaleFactor The factor that the image can be scaled up before being thrown out.
+ * A lower scale factor results in less pixelation.
+ */
+data class DesiredSize(
+ @Px val targetSize: Int,
+ @Px val minSize: Int,
+ @Px val maxSize: Int,
+ val maxScaleFactor: Float,
+)
diff --git a/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoader.kt b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoader.kt
new file mode 100644
index 0000000000..f4ffd8cc67
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoader.kt
@@ -0,0 +1,174 @@
+/* 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.support.images.compose.loader
+
+import android.graphics.Bitmap
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.support.images.DesiredSize
+import mozilla.components.support.images.decoder.AndroidImageDecoder
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+/**
+ * Loads an image from [url] using [Client] and makes it available in the inner [ImageLoaderScope].
+ *
+ * The loaded image will be available via the [WithImage] composable. While the image is still
+ * loading [Placeholder] will get rendered. If loading or decoding the image failed then [Fallback]
+ * will get rendered.
+ *
+ * @param client The [Client] implementation that should be used for loading the image.
+ * @param url The URL of the image that should get loaded.
+ * @param private Whether or not this is a private request. Like in private browsing mode, private
+ * requests will not cache anything on disk and not send any cookies shared with the browser.
+ * @param connectTimeout A timeout to be used when connecting to the remote server. If the timeout
+ * expires before the connection can be established then [Fallback] will get rendered.
+ * @param readTimeout A timeout to be used when reading from the remote server. If the timeout
+ * expires before there is data available for read then [Fallback] will get rendered.
+ * @param targetSize The target image size, the loaded image should be scaled to.
+ * @param minSize The minimum size before an image will be considered too small and [Fallback] will
+ * get rendered instead.
+ * @param maxSize The maximum size before an image will be considered too large and [Fallback] will
+ * get rendered instead.
+ * @param maxScaleFactor The maximum factor a loaded image will be scaled up or down by until
+ * [Fallback] will get rendered.
+ */
+@Composable
+fun ImageLoader(
+ client: Client,
+ url: String,
+ private: Boolean = true,
+ connectTimeout: Pair<Long, TimeUnit> = Pair(DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS),
+ readTimeout: Pair<Long, TimeUnit> = Pair(DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS),
+ targetSize: Dp = defaultTargetSize,
+ minSize: Dp = targetSize / DEFAULT_MIN_MAX_MULTIPLIER,
+ maxSize: Dp = targetSize * DEFAULT_MIN_MAX_MULTIPLIER,
+ maxScaleFactor: Float = DEFAULT_MAXIMUM_SCALE_FACTOR,
+ content: @Composable ImageLoaderScope.() -> Unit,
+) {
+ val desiredSize = with(LocalDensity.current) {
+ DesiredSize(
+ targetSize = targetSize.roundToPx(),
+ minSize = minSize.roundToPx(),
+ maxSize = maxSize.roundToPx(),
+ maxScaleFactor = maxScaleFactor,
+ )
+ }
+
+ val scope = remember(url) {
+ InternalImageLoaderScope(
+ client,
+ url,
+ private,
+ connectTimeout,
+ readTimeout,
+ desiredSize,
+ )
+ }
+
+ LaunchedEffect(scope.url) {
+ scope.load()
+ }
+
+ scope.content()
+}
+
+private suspend fun InternalImageLoaderScope.load() {
+ val bitmap = withContext(Dispatchers.IO) {
+ fetchAndDecode(
+ client,
+ url,
+ private,
+ connectTimeout,
+ readTimeout,
+ desiredSize,
+ )
+ }
+
+ if (bitmap != null) {
+ loaderState.value = ImageLoaderState.Image(BitmapPainter(bitmap.asImageBitmap()))
+ } else {
+ loaderState.value = ImageLoaderState.Failed
+ }
+}
+
+private suspend fun fetchAndDecode(
+ client: Client,
+ url: String,
+ private: Boolean,
+ connectTimeout: Pair<Long, TimeUnit>,
+ readTimeout: Pair<Long, TimeUnit>,
+ desiredSize: DesiredSize,
+): Bitmap? = withContext(Dispatchers.IO) {
+ val data = fetch(
+ client,
+ url,
+ private,
+ connectTimeout,
+ readTimeout,
+ ) ?: return@withContext null
+
+ decoders.firstNotNullOfOrNull { decoder ->
+ decoder.decode(data, desiredSize)
+ }
+}
+
+private fun fetch(
+ client: Client,
+ url: String,
+ private: Boolean,
+ connectTimeout: Pair<Long, TimeUnit>,
+ readTimeout: Pair<Long, TimeUnit>,
+): ByteArray? {
+ val request = Request(
+ url = url.trim(),
+ method = Request.Method.GET,
+ cookiePolicy = if (private) {
+ Request.CookiePolicy.OMIT
+ } else {
+ Request.CookiePolicy.INCLUDE
+ },
+ connectTimeout = connectTimeout,
+ readTimeout = readTimeout,
+ redirect = Request.Redirect.FOLLOW,
+ useCaches = true,
+ private = private,
+ conservative = true,
+ )
+
+ return try {
+ val response = client.fetch(request)
+ if (response.isSuccess) {
+ response.body.useStream { it.readBytes() }
+ } else {
+ response.close()
+ null
+ }
+ } catch (e: IOException) {
+ null
+ }
+}
+
+private const val DEFAULT_CONNECT_TIMEOUT = 2L // Seconds
+private const val DEFAULT_READ_TIMEOUT = 10L // Seconds
+private const val DEFAULT_MAXIMUM_SCALE_FACTOR = 2.0f
+private const val DEFAULT_MIN_MAX_MULTIPLIER = 3
+
+private val defaultTargetSize = 100.dp
+
+private val decoders = listOf(
+ AndroidImageDecoder(),
+)
diff --git a/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoaderScope.kt b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoaderScope.kt
new file mode 100644
index 0000000000..c874865536
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoaderScope.kt
@@ -0,0 +1,85 @@
+/* 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.support.images.compose.loader
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.graphics.painter.Painter
+import mozilla.components.concept.fetch.Client
+import mozilla.components.support.images.DesiredSize
+import java.util.concurrent.TimeUnit
+
+/**
+ * The scope of an [ImageLoader] block.
+ *
+ * @property loaderState The state this scope is in.
+ */
+interface ImageLoaderScope {
+ val loaderState: MutableState<ImageLoaderState>
+}
+
+/**
+ * Renders the inner [content] block if an image was loaded successfully.
+ */
+@Composable
+fun ImageLoaderScope.WithImage(
+ content: @Composable (Painter) -> Unit,
+) {
+ WithInternalScope {
+ val state = loaderState.value
+ if (state is ImageLoaderState.Image) {
+ content(state.painter)
+ }
+ }
+}
+
+/**
+ * Renders the inner [content] block while the image is still getting loaded.
+ */
+@Composable
+fun ImageLoaderScope.Placeholder(
+ content: @Composable () -> Unit,
+) {
+ WithInternalScope {
+ val state = loaderState.value
+ if (state == ImageLoaderState.Loading) {
+ content()
+ }
+ }
+}
+
+/**
+ * Renders the inner [content] block if loading the image failed.
+ */
+@Composable
+fun ImageLoaderScope.Fallback(
+ content: @Composable () -> Unit,
+) {
+ WithInternalScope {
+ val state = loaderState.value
+ if (state == ImageLoaderState.Failed) {
+ content()
+ }
+ }
+}
+
+internal class InternalImageLoaderScope(
+ val client: Client,
+ val url: String,
+ val private: Boolean,
+ val connectTimeout: Pair<Long, TimeUnit>,
+ val readTimeout: Pair<Long, TimeUnit>,
+ val desiredSize: DesiredSize,
+ override val loaderState: MutableState<ImageLoaderState> = mutableStateOf(ImageLoaderState.Loading),
+) : ImageLoaderScope
+
+@Composable
+private fun ImageLoaderScope.WithInternalScope(
+ content: @Composable InternalImageLoaderScope.() -> Unit,
+) {
+ val internalScope = this as InternalImageLoaderScope
+ internalScope.content()
+}
diff --git a/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoaderState.kt b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoaderState.kt
new file mode 100644
index 0000000000..9c3243e241
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/compose/loader/ImageLoaderState.kt
@@ -0,0 +1,29 @@
+/* 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.support.images.compose.loader
+
+import androidx.compose.ui.graphics.painter.BitmapPainter
+
+/**
+ * The state an [ImageLoaderScope] is in.
+ */
+sealed class ImageLoaderState {
+ /**
+ * The [ImageLoader] is currently loading the image.
+ */
+ object Loading : ImageLoaderState()
+
+ /**
+ * The [ImageLoader] succesfully loaded the image.
+ */
+ data class Image(
+ val painter: BitmapPainter,
+ ) : ImageLoaderState()
+
+ /**
+ * Loading the image failed.
+ */
+ object Failed : ImageLoaderState()
+}
diff --git a/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/decoder/AndroidImageDecoder.kt b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/decoder/AndroidImageDecoder.kt
new file mode 100644
index 0000000000..5164f938fe
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/decoder/AndroidImageDecoder.kt
@@ -0,0 +1,88 @@
+/* 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.support.images.decoder
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.util.Size
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.images.DesiredSize
+import kotlin.math.floor
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * [ImageDecoder] that will use Android's [BitmapFactory] in order to decode the byte data.
+ */
+class AndroidImageDecoder : ImageDecoder {
+ private val logger = Logger("AndroidImageDecoder")
+
+ override fun decode(data: ByteArray, desiredSize: DesiredSize): Bitmap? =
+ try {
+ val bounds = decodeBitmapBounds(data)
+ val maxBoundLength = max(bounds.width, bounds.height).toFloat()
+
+ val sampleSize = floor(maxBoundLength / desiredSize.targetSize.toFloat()).toInt()
+
+ if (isGoodSize(bounds, desiredSize)) {
+ decodeBitmap(data, sampleSize)
+ } else {
+ null
+ }
+ } catch (e: OutOfMemoryError) {
+ logger.error("Failed to decode the byte data due to OutOfMemoryError")
+ null
+ }
+
+ private fun isGoodSize(bounds: Size, desiredSize: DesiredSize): Boolean {
+ val (_, minSize, maxSize, maxScaleFactor) = desiredSize
+ return when {
+ min(bounds.width, bounds.height) <= 0 -> {
+ logger.debug("BitmapFactory returned too small bitmap with width or height <= 0")
+ false
+ }
+
+ min(bounds.width, bounds.height) * maxScaleFactor < minSize -> {
+ logger.debug("BitmapFactory returned too small bitmap")
+ false
+ }
+
+ max(bounds.width, bounds.height) * (1f / maxScaleFactor) > maxSize -> {
+ logger.debug("BitmapFactory returned way too large image")
+ false
+ }
+
+ else -> true
+ }
+ }
+
+ /**
+ * Decodes the width and height of a bitmap without loading it into memory.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun decodeBitmapBounds(data: ByteArray): Size {
+ val options = BitmapFactory.Options().apply {
+ inJustDecodeBounds = true
+ }
+ BitmapFactory.decodeByteArray(data, 0, data.size, options)
+ return Size(options.outWidth, options.outHeight)
+ }
+
+ /**
+ * Decodes a bitmap image.
+ *
+ * @param data Image bytes to decode.
+ * @param sampleSize Scale factor for the image.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun decodeBitmap(data: ByteArray, sampleSize: Int): Bitmap? {
+ val options = BitmapFactory.Options().apply {
+ inSampleSize = sampleSize
+ }
+ return BitmapFactory.decodeByteArray(data, 0, data.size, options)
+ }
+}
diff --git a/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/decoder/ImageDecoder.kt b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/decoder/ImageDecoder.kt
new file mode 100644
index 0000000000..c15ff7ef50
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/main/java/mozilla/components/support/images/decoder/ImageDecoder.kt
@@ -0,0 +1,36 @@
+/* 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.support.images.decoder
+
+import android.graphics.Bitmap
+import mozilla.components.support.images.DesiredSize
+
+/**
+ * An image decoder that can decode a [ByteArray] into a [Bitmap].
+ *
+ * Depending on the image format the decoder may internally decode the [ByteArray] into multiple [Bitmap]. It is up to
+ * the decoder implementation to return the best [Bitmap] to use.
+ */
+interface ImageDecoder {
+ /**
+ * Decodes the given [data] into a a [Bitmap] or null.
+ *
+ * The caller provides a maximum size. This is useful for image formats that may decode into multiple images. The
+ * decoder can use this size to determine which [Bitmap] to return.
+ */
+ fun decode(data: ByteArray, desiredSize: DesiredSize): Bitmap?
+
+ companion object {
+ @Suppress("MagicNumber")
+ enum class ImageMagicNumbers(var value: ByteArray) {
+ // It is irritating that Java bytes are signed...
+ PNG(byteArrayOf((0x89 and 0xFF).toByte(), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)),
+ GIF(byteArrayOf(0x47, 0x49, 0x46, 0x38)),
+ JPEG(byteArrayOf(-0x1, -0x28, -0x1, -0x20)),
+ BMP(byteArrayOf(0x42, 0x4d)),
+ WEB(byteArrayOf(0x57, 0x45, 0x42, 0x50, 0x0a)),
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/CancelOnDetachTest.kt b/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/CancelOnDetachTest.kt
new file mode 100644
index 0000000000..06e0b50c6a
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/CancelOnDetachTest.kt
@@ -0,0 +1,35 @@
+/* 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.support.images
+
+import android.view.View
+import kotlinx.coroutines.Job
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+class CancelOnDetachTest {
+
+ @Test
+ fun `onViewAttached does nothing`() {
+ val job: Job = mock()
+ val view: View = mock()
+
+ CancelOnDetach(job).onViewAttachedToWindow(view)
+
+ verifyNoMoreInteractions(job)
+ verifyNoMoreInteractions(view)
+ }
+
+ @Test
+ fun `onViewDetachedFromWindow cancels the job`() {
+ val job = Job()
+
+ CancelOnDetach(job).onViewDetachedFromWindow(mock())
+
+ assertTrue(job.isCancelled)
+ }
+}
diff --git a/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/DesiredSizeTest.kt b/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/DesiredSizeTest.kt
new file mode 100644
index 0000000000..432e2d4dcc
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/DesiredSizeTest.kt
@@ -0,0 +1,21 @@
+/* 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.support.images
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class DesiredSizeTest {
+
+ @Test
+ fun `minimum size is the same as supplied`() {
+ val desiredSize = DesiredSize(128, 256, 512, 2.0f)
+
+ assertEquals(desiredSize.targetSize, 128)
+ assertEquals(desiredSize.minSize, 256)
+ assertEquals(desiredSize.maxSize, 512)
+ assertEquals(desiredSize.maxScaleFactor, 2.0f)
+ }
+}
diff --git a/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/decoder/AndroidImageDecoderTest.kt b/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/decoder/AndroidImageDecoderTest.kt
new file mode 100644
index 0000000000..fa7116006c
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/test/java/mozilla/components/support/images/decoder/AndroidImageDecoderTest.kt
@@ -0,0 +1,259 @@
+/* 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.support.images.decoder
+
+import android.graphics.Bitmap
+import android.util.Size
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.images.DesiredSize
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.spy
+
+@RunWith(AndroidJUnit4::class)
+class AndroidImageDecoderTest {
+
+ @Test
+ fun `WHEN decoding PNG THEN returns non-null bitmap`() {
+ val decoder = AndroidImageDecoder()
+
+ val bitmap = decoder.decode(
+ loadImage("png/mozac.png"),
+ DesiredSize(
+ targetSize = 32,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNotNull(bitmap!!)
+ }
+
+ @Test
+ fun `WHEN out of memory THEN returns null`() {
+ val decoder = spy(AndroidImageDecoder())
+ doThrow(OutOfMemoryError()).`when`(decoder).decodeBitmap(any(), anyInt())
+
+ val bitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 64,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNull(bitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap width equals zero THEN returns null`() {
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(Size(0, 512)).`when`(decoder).decodeBitmapBounds(any())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 64,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap height equals zero THEN returns null`() {
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(Size(512, 0)).`when`(decoder).decodeBitmapBounds(any())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 64,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN decoding null bitmap THEN returns null`() {
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(null).`when`(decoder).decodeBitmap(any(), anyInt())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 64,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap width too small THEN returns null`() {
+ val size = Size(63, 250)
+
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(size).`when`(decoder).decodeBitmapBounds(any())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 256,
+ minSize = 64,
+ maxSize = 256,
+ maxScaleFactor = 1.0f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap height too small THEN returns null`() {
+ val size = Size(250, 63)
+
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(size).`when`(decoder).decodeBitmapBounds(any())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 256,
+ minSize = 64,
+ maxSize = 256,
+ maxScaleFactor = 1.0f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap height size too small with maxScaleFactor THEN returns null`() {
+ val size = Size(128, 64)
+
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(size).`when`(decoder).decodeBitmapBounds(any())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 256,
+ minSize = 64,
+ maxSize = 256,
+ maxScaleFactor = 0.9f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap width too large THEN returns null`() {
+ val size = Size(2000, 250)
+
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(size).`when`(decoder).decodeBitmapBounds(any())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 256,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 1.0f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap height too large THEN returns null`() {
+ val size = Size(250, 2000)
+
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(size).`when`(decoder).decodeBitmapBounds(any())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 256,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 1.0f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap height too large with maxScaleFactor THEN returns null`() {
+ val size = Size(32, 256)
+
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(size).`when`(decoder).decodeBitmapBounds(any())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 256,
+ minSize = 32,
+ maxSize = 256,
+ maxScaleFactor = 0.9f,
+ ),
+ )
+
+ assertNull(decodedBitmap)
+ }
+
+ @Test
+ fun `WHEN bitmap size is good THEN returns non null`() {
+ val bitmap: Bitmap = mock()
+ val size = Size(128, 128)
+ val decoder = spy(AndroidImageDecoder())
+ doReturn(size).`when`(decoder).decodeBitmapBounds(any())
+ doReturn(bitmap).`when`(decoder).decodeBitmap(any(), anyInt())
+
+ val decodedBitmap = decoder.decode(
+ ByteArray(0),
+ DesiredSize(
+ targetSize = 256,
+ minSize = 64,
+ maxSize = 256,
+ maxScaleFactor = 2.0f,
+ ),
+ )
+
+ assertNotNull(decodedBitmap)
+ }
+
+ private fun loadImage(fileName: String): ByteArray =
+ javaClass.getResourceAsStream("/$fileName")!!
+ .buffered()
+ .readBytes()
+}
diff --git a/mobile/android/android-components/components/support/images/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/support/images/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..49324d83c5
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,3 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
+
diff --git a/mobile/android/android-components/components/support/images/src/test/resources/png/mozac.png b/mobile/android/android-components/components/support/images/src/test/resources/png/mozac.png
new file mode 100644
index 0000000000..2a03203476
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/test/resources/png/mozac.png
Binary files differ
diff --git a/mobile/android/android-components/components/support/images/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/images/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/images/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/ktx/README.md b/mobile/android/android-components/components/support/ktx/README.md
new file mode 100644
index 0000000000..eebecd0229
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Ktx
+
+A set of Kotlin extensions on top of the Android framework and Kotlin standard library.
+
+## Usage
+
+### 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:support-ktx:{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/support/ktx/build.gradle b/mobile/android/android-components/components/support/ktx/build.gradle
new file mode 100644
index 0000000000..b1a2bd1832
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/build.gradle
@@ -0,0 +1,64 @@
+/* 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'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.support.ktx'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += [
+ "-opt-in=kotlinx.coroutines.FlowPreview",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+ ]
+}
+
+dependencies {
+ implementation project(':support-base')
+ implementation project(':support-utils')
+ implementation project(':lib-publicsuffixlist')
+
+ implementation ComponentsDependencies.androidx_core
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-fakes')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.kotlin_reflect
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+
+ androidTestImplementation project(':support-android-test')
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.androidx_test_junit
+}
+
+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/support/ktx/proguard-rules.pro b/mobile/android/android-components/components/support/ktx/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/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/support/ktx/src/androidTest/AndroidManifest.xml b/mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000000..d0418bdaa1
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <activity
+ android:name="mozilla.components.support.ktx.TestActivity"
+ android:exported="false" />
+ </application>
+
+</manifest>
diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt
new file mode 100644
index 0000000000..ad3d123c10
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt
@@ -0,0 +1,12 @@
+/* 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.support.ktx
+
+import android.app.Activity
+
+/**
+ * Empty activity only to be used in UI tests.
+ */
+internal class TestActivity : Activity()
diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt
new file mode 100644
index 0000000000..f0ec9bf2d3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt
@@ -0,0 +1,31 @@
+/* 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.support.ktx.android.net
+
+import android.content.Context
+import androidx.core.net.toUri
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class OnDeviceUriKtTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun isUnderPrivateAppDirectory() {
+ var uri = "file:///data/user/0/${context.packageName}/any_directory/file.text".toUri()
+
+ assertTrue(uri.isUnderPrivateAppDirectory(context))
+
+ uri = "file:///data/data/${context.packageName}/any_directory/file.text".toUri()
+
+ assertTrue(uri.isUnderPrivateAppDirectory(context))
+
+ uri = "file:///data/directory/${context.packageName}/any_directory/file.text".toUri()
+
+ assertFalse(uri.isUnderPrivateAppDirectory(context))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt
new file mode 100644
index 0000000000..93b12d237e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt
@@ -0,0 +1,57 @@
+/* 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.support.ktx.android.view
+
+import android.graphics.Color
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SdkSuppress
+import mozilla.components.support.ktx.TestActivity
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+
+class WindowKtTest {
+ @get:Rule
+ internal val activityRule: ActivityScenarioRule<TestActivity> = ActivityScenarioRule(TestActivity::class.java)
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ fun whenALightColorIsAppliedToStatusBarThenSetIsAppearanceLightStatusBarsToTrue() {
+ activityRule.scenario.onActivity {
+ it.window.setStatusBarTheme(Color.WHITE)
+
+ assertTrue(it.window.createWindowInsetsController().isAppearanceLightStatusBars)
+ }
+ }
+
+ @Test
+ fun whenADarkColorIsAppliedToStatusBarThenSetIsAppearanceLightStatusBarsToFalse() {
+ activityRule.scenario.onActivity {
+ it.window.setStatusBarTheme(Color.BLACK)
+
+ assertFalse(it.window.createWindowInsetsController().isAppearanceLightStatusBars)
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ fun whenALightColorIsAppliedToNavigationBarThemeThenSetIsAppearanceLightNavigationBarsToTrue() {
+ activityRule.scenario.onActivity {
+ it.window.setNavigationBarTheme(Color.WHITE)
+
+ assertTrue(it.window.createWindowInsetsController().isAppearanceLightNavigationBars)
+ }
+ }
+
+ @Test
+ fun whenADarkColorIsAppliedToNavigationBarThemeThenSetIsAppearanceLightNavigationBarsToFalse() {
+ activityRule.scenario.onActivity {
+ it.window.setNavigationBarTheme(Color.BLACK)
+
+ assertFalse(it.window.createWindowInsetsController().isAppearanceLightNavigationBars)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..c93a4ab93a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+ <queries>
+ <intent>
+ <action android:name="android.intent.action.SEND" />
+ <data android:mimeType="text/plain" />
+ </intent>
+ </queries>
+</manifest>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt
new file mode 100644
index 0000000000..5123683117
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt
@@ -0,0 +1,13 @@
+/* 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.support.ktx.android.arch.lifecycle
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+
+/**
+ * Calls [Lifecycle.addObserver] for a variable list of [LifecycleObserver]s.
+ */
+fun Lifecycle.addObservers(vararg observers: LifecycleObserver) = observers.forEach { addObserver(it) }
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt
new file mode 100644
index 0000000000..e1e4dd7299
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt
@@ -0,0 +1,344 @@
+/* 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.support.ktx.android.content
+
+import android.app.ActivityManager
+import android.content.ActivityNotFoundException
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_DIAL
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SENDTO
+import android.content.Intent.EXTRA_EMAIL
+import android.content.Intent.EXTRA_STREAM
+import android.content.Intent.EXTRA_SUBJECT
+import android.content.Intent.EXTRA_TEXT
+import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.hardware.camera2.CameraManager
+import android.net.Uri
+import android.os.Build
+import android.os.Process
+import android.provider.ContactsContract
+import android.view.accessibility.AccessibilityManager
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.checkSelfPermission
+import androidx.core.content.FileProvider
+import androidx.core.content.getSystemService
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.R
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.utils.ext.getPackageInfoCompat
+import java.io.File
+
+/**
+ * The (visible) version name of the application, as specified by the <manifest> tag's versionName
+ * attribute. E.g. "2.0".
+ */
+val Context.appVersionName: String
+ get() = packageManager.getPackageInfoCompat(packageName, 0).versionName
+
+/**
+ * Returns the name (label) of the application or the package name as a fallback.
+ */
+val Context.appName: String
+ get() = packageManager.getApplicationLabel(applicationInfo).toString()
+
+/**
+ * Returns whether or not the operating system is under low memory conditions.
+ */
+fun Context.isOSOnLowMemory(): Boolean {
+ val activityManager: ActivityManager = getSystemService()!!
+ return ActivityManager.MemoryInfo().also { memoryInfo ->
+ activityManager.getMemoryInfo(memoryInfo)
+ }.lowMemory
+}
+
+/**
+ * Returns if a list of permission have been granted, if all the permission have been granted
+ * returns true otherwise false.
+ */
+fun Context.isPermissionGranted(permission: Iterable<String>): Boolean {
+ return permission.all { checkSelfPermission(this, it) == PERMISSION_GRANTED }
+}
+
+fun Context.isPermissionGranted(vararg permission: String): Boolean {
+ return isPermissionGranted(permission.asIterable())
+}
+
+/**
+ * Checks whether or not the device has a camera.
+ *
+ * @return true if a camera was found, otherwise false.
+ */
+@Suppress("TooGenericExceptionCaught")
+fun Context.hasCamera(): Boolean {
+ return try {
+ val cameraManager: CameraManager? = getSystemService()
+ cameraManager?.cameraIdList?.isNotEmpty() ?: false
+ } catch (_: Throwable) {
+ false
+ }
+}
+
+/**
+ * Shares content via [ACTION_SEND] intent.
+ *
+ * @param text the data to be shared [EXTRA_TEXT]
+ * @param subject of the intent [EXTRA_TEXT]
+ * @return true it is able to share false otherwise.
+ */
+fun Context.share(text: String, subject: String = getString(R.string.mozac_support_ktx_share_dialog_title)): Boolean {
+ return try {
+ val intent = Intent(ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(EXTRA_SUBJECT, subject)
+ putExtra(EXTRA_TEXT, text)
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+
+ startActivity(
+ intent.createChooserExcludingCurrentApp(
+ this,
+ getString(R.string.mozac_support_ktx_menu_share_with),
+ ),
+ )
+ true
+ } catch (e: ActivityNotFoundException) {
+ Log.log(Log.Priority.WARN, message = "No activity to share to found", throwable = e, tag = "Reference-Browser")
+ false
+ }
+}
+
+/**
+ * Shares content via [ACTION_SEND] intent.
+ *
+ * @param filePath Path of the copied file.
+ * @param contentType Content type (MIME type) to indicate the media type of the resource.
+ * @param subject of the intent [EXTRA_SUBJECT]
+ * @param message of the intent [EXTRA_TEXT]
+ *
+ * @return true it is able to share false otherwise.
+ */
+fun Context.shareMedia(
+ filePath: String,
+ contentType: String?,
+ subject: String? = null,
+ message: String? = null,
+): Boolean {
+ val contentUri = getContentUriForFile(filePath)
+
+ val intent = Intent().apply {
+ action = ACTION_SEND
+ type = contentType ?: contentResolver.getType(contentUri)
+ flags = FLAG_ACTIVITY_NEW_DOCUMENT or FLAG_GRANT_READ_URI_PERMISSION
+ putExtra(EXTRA_STREAM, contentUri)
+ if (subject != null) {
+ putExtra(EXTRA_SUBJECT, subject)
+ }
+ if (message != null) {
+ putExtra(EXTRA_TEXT, message)
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android Q allows us to show a thumbnail preview of the file to be shared.
+ clipData = ClipData.newRawUri(contentUri.toString(), contentUri)
+ }
+ }
+
+ val shareIntent = Intent.createChooser(intent, getString(R.string.mozac_support_ktx_menu_share_with)).apply {
+ flags = FLAG_ACTIVITY_NEW_TASK or FLAG_GRANT_READ_URI_PERMISSION
+ }
+
+ return try {
+ startActivity(shareIntent)
+ true
+ } catch (error: ActivityNotFoundException) {
+ Log.log(Log.Priority.WARN, message = "No activity to share to found", throwable = error, tag = "shareMedia")
+
+ false
+ }
+}
+
+/**
+ * Creates a content URI for the given [filePath] to add to the device clipboard and maybe displays
+ * confirmation feedback.
+ *
+ * @param filePath Path of the copied file.
+ * @param onCopyConfirmation The confirmation action of copying an image.
+ */
+fun Context.copyImage(
+ filePath: String,
+ onCopyConfirmation: () -> Unit,
+) {
+ val contentUri = getContentUriForFile(filePath)
+
+ val clipData = ClipData.newUri(contentResolver, "Copied media URI", contentUri)
+ getClipboardManager().setPrimaryClip(clipData)
+
+ onCopyConfirmation.invoke()
+}
+
+private fun Context.getContentUriForFile(filePath: String) = FileProvider.getUriForFile(
+ this,
+ "${applicationContext.packageName}.feature.downloads.fileprovider", // (packageName + FILE_PROVIDER_EXTENSION)
+ File(filePath),
+)
+
+private fun Context.getClipboardManager() =
+ getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+/**
+ * Emails content via [ACTION_SENDTO] intent.
+ *
+ * @param address the email address to send to [EXTRA_EMAIL]
+ * @param subject of the intent [EXTRA_TEXT]
+ * @return true it is able to share email false otherwise.
+ */
+fun Context.email(
+ address: String,
+ subject: String = getString(R.string.mozac_support_ktx_share_dialog_title),
+): Boolean {
+ return try {
+ val intent = Intent(ACTION_SENDTO, Uri.parse("mailto:$address"))
+ intent.putExtra(EXTRA_SUBJECT, subject)
+
+ val emailIntent = Intent.createChooser(
+ intent,
+ getString(R.string.mozac_support_ktx_menu_email_with),
+ ).apply {
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+
+ startActivity(emailIntent)
+ true
+ } catch (e: ActivityNotFoundException) {
+ Logger.warn("No activity found to handle email intent", throwable = e)
+ false
+ }
+}
+
+/**
+ * Calls phone number via [ACTION_DIAL] intent.
+ *
+ * Note: we purposely use ACTION_DIAL rather than ACTION_CALL as the latter requires user permission
+ * @param phoneNumber the phone number to send to [ACTION_DIAL]
+ * @param subject of the intent [EXTRA_TEXT]
+ * @return true it is able to share phone call false otherwise.
+ */
+fun Context.call(
+ phoneNumber: String,
+ subject: String = getString(R.string.mozac_support_ktx_share_dialog_title),
+): Boolean {
+ return try {
+ val intent = Intent(ACTION_DIAL, Uri.parse("tel:$phoneNumber"))
+ intent.putExtra(EXTRA_SUBJECT, subject)
+
+ val callIntent = Intent.createChooser(
+ intent,
+ getString(R.string.mozac_support_ktx_menu_call_with),
+ ).apply {
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+
+ startActivity(callIntent)
+ true
+ } catch (e: ActivityNotFoundException) {
+ Logger.warn("No activity found to handle dial intent", throwable = e)
+ false
+ }
+}
+
+/**
+ * Add to contact via [ContactsContract.Intents.Insert.ACTION]
+ *
+ * @param address the email address to add to [ContactsContract.Intents.Insert.EMAIL]
+ * @return true it is able to share email false otherwise.
+ */
+fun Context.addContact(
+ address: String,
+): Boolean {
+ return try {
+ val intent = Intent(ContactsContract.Intents.Insert.ACTION).apply {
+ type = ContactsContract.RawContacts.CONTENT_TYPE
+ putExtra(ContactsContract.Intents.Insert.EMAIL, address)
+ putExtra(
+ ContactsContract.Intents.Insert.EMAIL_TYPE,
+ ContactsContract.CommonDataKinds.Email.TYPE_WORK,
+ )
+ addFlags(FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ startActivity(intent)
+ true
+ } catch (e: ActivityNotFoundException) {
+ Logger.warn("No activity found to handle dial intent", throwable = e)
+ false
+ }
+}
+
+/**
+ * Check if TalkBack service is enabled.
+ *
+ * (via https://stackoverflow.com/a/12362545/512580)
+ */
+inline val Context.isScreenReaderEnabled: Boolean
+ get() = getSystemService<AccessibilityManager>()?.isTouchExplorationEnabled ?: false
+
+@VisibleForTesting
+internal var isMainProcess: Boolean? = null
+
+/**
+ * Returns true if we are running in the main process false otherwise.
+ */
+fun Context.isMainProcess(): Boolean {
+ if (isMainProcess != null) return isMainProcess as Boolean
+
+ val pid = Process.myPid()
+ val activityManager: ActivityManager? = getSystemService()
+
+ isMainProcess = activityManager?.runningAppProcesses.orEmpty().any { processInfo ->
+ processInfo.pid == pid && processInfo.processName == packageName
+ }
+
+ return isMainProcess as Boolean
+}
+
+/**
+ * Takes a function runs it only it if we are running in the main process, otherwise the function will not be executed.
+ * @param [block] function to be executed in the main process.
+ */
+inline fun Context.runOnlyInMainProcess(block: () -> Unit) {
+ if (isMainProcess()) {
+ block()
+ }
+}
+
+/**
+ * Returns the color int corresponding to the attribute.
+ */
+@ColorInt
+fun Context.getColorFromAttr(@AttrRes attr: Int) =
+ ContextCompat.getColor(this, theme.resolveAttribute(attr))
+
+/**
+ * Returns a tinted drawable for the given resource ID.
+ * @param resId ID of the drawable to load.
+ * @param tint Tint color int to apply to the drawable.
+ */
+fun Context.getDrawableWithTint(@DrawableRes resId: Int, @ColorInt tint: Int) =
+ AppCompatResources.getDrawable(this, resId)?.apply {
+ mutate()
+ setTint(tint)
+ }
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt
new file mode 100644
index 0000000000..041e105c33
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt
@@ -0,0 +1,75 @@
+/* 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.support.ktx.android.content
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.LabeledIntent
+import android.os.Build
+import android.os.Parcelable
+import mozilla.components.support.utils.ext.queryIntentActivitiesCompat
+
+/**
+ * Modify the current intent to be used in an intent chooser excluding the current app.
+ *
+ * @param context Android context used for various system interactions.
+ * @param title Title that will be displayed in the chooser.
+ *
+ * @return a new Intent object that you can hand to Context.startActivity() and related methods.
+ */
+fun Intent.createChooserExcludingCurrentApp(
+ context: Context,
+ title: CharSequence,
+): Intent {
+ val chooserIntent: Intent
+ val resolveInfos = context.packageManager.queryIntentActivitiesCompat(this, 0).toHashSet()
+
+ val excludedComponentNames = resolveInfos
+ .map { it.activityInfo }
+ .filter { it.packageName == context.packageName }
+ .map { ComponentName(it.packageName, it.name) }
+
+ // Starting with Android N we can use Intent.EXTRA_EXCLUDE_COMPONENTS to exclude components
+ // other way we are constrained to use Intent.EXTRA_INITIAL_INTENTS.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ chooserIntent = Intent.createChooser(this, title)
+ .putExtra(
+ Intent.EXTRA_EXCLUDE_COMPONENTS,
+ excludedComponentNames.toTypedArray(),
+ )
+ } else {
+ var targetIntents = resolveInfos
+ .filterNot { it.activityInfo.packageName == context.packageName }
+ .map { resolveInfo ->
+ val activityInfo = resolveInfo.activityInfo
+ val targetIntent = Intent(this).apply {
+ component = ComponentName(activityInfo.packageName, activityInfo.name)
+ }
+ LabeledIntent(
+ targetIntent,
+ activityInfo.packageName,
+ resolveInfo.labelRes,
+ resolveInfo.icon,
+ )
+ }
+
+ // Sometimes on Android M and below an empty chooser is displayed, problem reported also here
+ // https://issuetracker.google.com/issues/37085761
+ // To fix that we are creating a chooser with an empty intent
+ chooserIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ Intent.createChooser(Intent(), title)
+ } else {
+ targetIntents = targetIntents.toMutableList()
+ Intent.createChooser(targetIntents.removeAt(0), title)
+ }
+ chooserIntent.putExtra(
+ Intent.EXTRA_INITIAL_INTENTS,
+ targetIntents.toTypedArray<Parcelable>(),
+ )
+ }
+ chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ return chooserIntent
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt
new file mode 100644
index 0000000000..1661c188dd
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt
@@ -0,0 +1,193 @@
+/* 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/. */
+
+@file:Suppress("MatchingDeclarationName")
+
+package mozilla.components.support.ktx.android.content
+
+import android.content.SharedPreferences
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Represents a class that holds a reference to [SharedPreferences].
+ */
+interface PreferencesHolder {
+ val preferences: SharedPreferences
+}
+
+private class BooleanPreference(
+ private val key: String,
+ private val default: Boolean,
+) : ReadWriteProperty<PreferencesHolder, Boolean> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Boolean =
+ thisRef.preferences.getBoolean(key, default)
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Boolean) =
+ thisRef.preferences.edit().putBoolean(key, value).apply()
+}
+
+private class FloatPreference(
+ private val key: String,
+ private val default: Float,
+) : ReadWriteProperty<PreferencesHolder, Float> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Float =
+ thisRef.preferences.getFloat(key, default)
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Float) =
+ thisRef.preferences.edit().putFloat(key, value).apply()
+}
+
+private class IntPreference(
+ private val key: String,
+ private val default: Int,
+) : ReadWriteProperty<PreferencesHolder, Int> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Int =
+ thisRef.preferences.getInt(key, default)
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Int) =
+ thisRef.preferences.edit().putInt(key, value).apply()
+}
+
+private class LongPreference(
+ private val key: String,
+ private val default: Long,
+) : ReadWriteProperty<PreferencesHolder, Long> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Long =
+ thisRef.preferences.getLong(key, default)
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Long) =
+ thisRef.preferences.edit().putLong(key, value).apply()
+}
+
+private class StringPreference(
+ private val key: String,
+ private val default: String,
+ private val persistDefaultIfNotExists: Boolean = false,
+) : ReadWriteProperty<PreferencesHolder, String> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): String {
+ return thisRef.preferences.getString(key, null) ?: run {
+ when (persistDefaultIfNotExists) {
+ true -> {
+ thisRef.preferences.edit().putString(key, default).apply()
+ thisRef.preferences.getString(key, null)!!
+ }
+ false -> default
+ }
+ }
+ }
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: String) =
+ thisRef.preferences.edit().putString(key, value).apply()
+}
+
+private class StringSetPreference(
+ private val key: String,
+ private val default: Set<String>,
+) : ReadWriteProperty<PreferencesHolder, Set<String>> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Set<String> =
+ thisRef.preferences.getStringSet(key, default) ?: default
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Set<String>) =
+ thisRef.preferences.edit().putStringSet(key, value).apply()
+}
+
+/**
+ * Property delegate for getting and setting a boolean shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * val isTelemetryOn by booleanPreference("telemetry", default = false)
+ * }
+ * ```
+ */
+fun booleanPreference(key: String, default: Boolean): ReadWriteProperty<PreferencesHolder, Boolean> =
+ BooleanPreference(key, default)
+
+/**
+ * Property delegate for getting and setting a float number shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * var percentage by floatPreference("percentage", default = 0f)
+ * }
+ * ```
+ */
+fun floatPreference(key: String, default: Float): ReadWriteProperty<PreferencesHolder, Float> =
+ FloatPreference(key, default)
+
+/**
+ * Property delegate for getting and setting an int number shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * var widgetNumInvocations by intPreference("widget_number_of_invocations", default = 0)
+ * }
+ * ```
+ */
+fun intPreference(key: String, default: Int): ReadWriteProperty<PreferencesHolder, Int> =
+ IntPreference(key, default)
+
+/**
+ * Property delegate for getting and setting a long number shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * val appInstanceId by longPreference("app_instance_id", default = 123456789L)
+ * }
+ * ```
+ */
+fun longPreference(key: String, default: Long): ReadWriteProperty<PreferencesHolder, Long> =
+ LongPreference(key, default)
+
+/**
+ * Property delegate for getting and setting a string shared preference.
+ * Optionally this will persist the default value if one is not already persisted.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * var permissionsEnabledEnum by stringPreference(
+ * "permissions_enabled",
+ * default = "blocked",
+ * persistDefaultIfNotExists = true,
+ * )
+ * }
+ * ```
+ */
+fun stringPreference(
+ key: String,
+ default: String,
+ persistDefaultIfNotExists: Boolean = false,
+): ReadWriteProperty<PreferencesHolder, String> =
+ StringPreference(key, default, persistDefaultIfNotExists)
+
+/**
+ * Property delegate for getting and setting a string set shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * var connectedDevices by stringSetPreference("connected_devices", default = emptySet())
+ * }
+ * ```
+ */
+fun stringSetPreference(key: String, default: Set<String>): ReadWriteProperty<PreferencesHolder, Set<String>> =
+ StringSetPreference(key, default)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt
new file mode 100644
index 0000000000..e9a8a9e140
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt
@@ -0,0 +1,23 @@
+/* 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.support.ktx.android.content.pm
+
+import android.content.pm.PackageManager
+import mozilla.components.support.utils.ext.getPackageInfoCompat
+
+/**
+ * Check if a package is installed
+ *
+ * @param packageName The name of the package to check for.
+ */
+fun PackageManager.isPackageInstalled(packageName: String): Boolean {
+ return try {
+ // Turn off all the flags since we don't need the return value
+ getPackageInfoCompat(packageName, 0)
+ true
+ } catch (e: PackageManager.NameNotFoundException) {
+ false
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt
new file mode 100644
index 0000000000..72fa88eba9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt
@@ -0,0 +1,20 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.res.AssetManager
+import org.json.JSONObject
+
+/**
+ * Read a file from the "assets" and create a a JSONObject from its content.
+ *
+ * @param fileName The name of the asset to open. This name can be
+ * hierarchical.
+ */
+fun AssetManager.readJSONObject(fileName: String) = JSONObject(
+ open(fileName).bufferedReader().use {
+ it.readText()
+ },
+)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt
new file mode 100644
index 0000000000..3253c722be
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt
@@ -0,0 +1,102 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.res.Resources
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.text.SpannableStringBuilder
+import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+import android.text.SpannedString
+import androidx.annotation.StringRes
+import java.util.Formatter
+import java.util.Locale
+
+/**
+ * Returns the primary locale according to the user's preference.
+ */
+val Resources.locale: Locale
+ get() = if (SDK_INT >= Build.VERSION_CODES.N) {
+ configuration.locales[0]
+ } else {
+ @Suppress("Deprecation")
+ configuration.locale
+ }
+
+/**
+ * Returns the character sequence associated with a given resource [id],
+ * substituting format arguments with additional styling spans.
+ *
+ * Credit to Michael Spitsin https://medium.com/@programmerr47/working-with-spans-in-android-ca4ab1327bc4
+ *
+ * @param id The desired resource identifier, corresponding to a string resource.
+ * @param spanParts The format arguments that will be used for substitution.
+ * The first element of each pair is the text to insert, similar to [String.format].
+ * The second element of each pair is a span that will be used to style the inserted string.
+ */
+@Suppress("SpreadOperator")
+fun Resources.getSpanned(
+ @StringRes id: Int,
+ vararg spanParts: Pair<Any, Any>,
+): SpannedString {
+ val builder = SpannableStringBuilder()
+ val formatArgs = spanParts.map { (text) -> text }.toTypedArray()
+ val formatter = Formatter(SpannableAppendable(builder, spanParts), locale)
+ formatter.format(getString(id), *formatArgs)
+ return SpannedString(builder)
+}
+
+/**
+ * [Appendable] implementation that wraps [SpannableStringBuilder]
+ * and inserts spans from the span parts array.
+ */
+private class SpannableAppendable(
+ private val builder: SpannableStringBuilder,
+ spanParts: Array<out Pair<Any, Any>>,
+) : Appendable {
+
+ /**
+ * Map of values from span parts, with keys converted to char sequences.
+ */
+ private val spansMap = spanParts
+ .toMap()
+ .mapKeys { (key) -> key.let { it as? CharSequence ?: it.toString() } }
+
+ override fun append(csq: CharSequence?) = apply { appendSmart(csq) }
+
+ override fun append(csq: CharSequence?, start: Int, end: Int) = apply {
+ if (csq != null) {
+ if (start in 0 until end && end <= csq.length) {
+ append(csq.subSequence(start, end))
+ } else {
+ throw IndexOutOfBoundsException("start " + start + ", end " + end + ", s.length() " + csq.length)
+ }
+ }
+ }
+
+ override fun append(c: Char) = apply { builder.append(c.toString()) }
+
+ /**
+ * Tries to find [csq] in the [spansMap] and use the corresponding span value.
+ * If [csq] is not found, the map is searched manually by converting values to strings.
+ * If no match is found afterwards, [csq] is appended with no corresponding span.
+ */
+ private fun appendSmart(csq: CharSequence?) {
+ if (csq != null) {
+ if (csq in spansMap) {
+ val span = spansMap.getValue(csq)
+ builder.append(csq, span, SPAN_EXCLUSIVE_EXCLUSIVE)
+ } else {
+ val possibleMatchDict = spansMap.filter { (text) -> text.toString() == csq }
+ if (possibleMatchDict.isNotEmpty()) {
+ val spanDictEntry = possibleMatchDict.entries.first()
+ builder.append(spanDictEntry.key, spanDictEntry.value, SPAN_EXCLUSIVE_EXCLUSIVE)
+ } else {
+ builder.append(csq)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt
new file mode 100644
index 0000000000..4ad046a05d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt
@@ -0,0 +1,23 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.res.Resources
+import android.util.TypedValue
+import androidx.annotation.AnyRes
+import androidx.annotation.AttrRes
+
+/**
+ * Resolves the resource ID corresponding to the given attribute.
+ *
+ * @sample
+ * context.theme.resolveAttribute(R.attr.textColor) == R.color.light_text_color
+ */
+@AnyRes
+fun Resources.Theme.resolveAttribute(@AttrRes attribute: Int): Int {
+ val outValue = TypedValue()
+ resolveAttribute(attribute, outValue, true)
+ return outValue.resourceId
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt
new file mode 100644
index 0000000000..bac132e64e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt
@@ -0,0 +1,80 @@
+/* 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.support.ktx.android.graphics
+
+import android.graphics.Bitmap
+import android.graphics.BitmapShader
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Shader.TileMode
+import android.util.Base64
+import androidx.annotation.CheckResult
+import java.io.ByteArrayOutputStream
+
+/**
+ * Transform bitmap into base64 encoded data uri (PNG).
+ */
+fun Bitmap.toDataUri(): String {
+ val stream = ByteArrayOutputStream()
+ compress(Bitmap.CompressFormat.PNG, BITMAP_COMPRESSION_QUALITY, stream)
+ val encodedImage = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT)
+ return "data:image/png;base64,$encodedImage"
+}
+
+private const val BITMAP_COMPRESSION_QUALITY = 100
+
+/**
+ * Returns a new bitmap that is the receiver Bitmap with four rounded corners;
+ * the receiver is unmodified.
+ *
+ * This operation is expensive: it requires allocating an identical Bitmap and copying
+ * all of the Bitmap's pixels. Consider these theoretically cheaper alternatives:
+ * - android:background= a drawable with rounded corners
+ * - Wrap your bitmap's ImageView with a layout that masks your view with rounded corners (e.g. CardView)
+ */
+@CheckResult
+fun Bitmap.withRoundedCorners(cornerRadiusPx: Float): Bitmap {
+ val roundedBitmap = Bitmap.createBitmap(width, height, config)
+ val canvas = Canvas(roundedBitmap)
+ val paint = Paint().apply {
+ isAntiAlias = true
+ shader = BitmapShader(this@withRoundedCorners, TileMode.CLAMP, TileMode.CLAMP)
+ }
+
+ canvas.drawRoundRect(
+ 0.0f,
+ 0.0f,
+ width.toFloat(),
+ height.toFloat(),
+ cornerRadiusPx,
+ cornerRadiusPx,
+ paint,
+ )
+ return roundedBitmap
+}
+
+/**
+ * Returns true if all pixels have the same value, false otherwise.
+ */
+fun Bitmap.arePixelsAllTheSame(): Boolean {
+ val testPixel = getPixel(0, 0)
+
+ // For perf, I expect iteration order is important. Under the hood, the pixels are represented
+ // by a single array: if you iterate along the buffer, you can take advantage of cache hits
+ // (since several words in memory are imported each time memory is accessed).
+ //
+ // We choose this iteration order (width first) because getPixels writes into a single array
+ // with index 1 being the same value as getPixel(1, 0) (i.e. it writes width first).
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val color = getPixel(x, y)
+ if (color != testPixel) {
+ return false
+ }
+ }
+ }
+
+ return true
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt
new file mode 100644
index 0000000000..e5d1df1175
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt
@@ -0,0 +1,194 @@
+/* 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.support.ktx.android.net
+
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.provider.OpenableColumns
+import android.webkit.MimeTypeMap
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.sanitizeFileName
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.util.UUID
+
+internal val commonPrefixes = listOf("www.", "mobile.", "m.")
+internal val mobileSubdomains = listOf("mobile", "m")
+
+/**
+ * Returns the host without common prefixes like "www" or "m".
+ */
+val Uri.hostWithoutCommonPrefixes: String?
+ get() {
+ val host = host ?: return null
+ for (prefix in commonPrefixes) {
+ if (host.startsWith(prefix)) return host.substring(prefix.length)
+ }
+ return host
+ }
+
+/**
+ * Checks that the Uri has the same host as [other], with mobile subdomains removed.
+ * @param other The Uri to be compared.
+ */
+fun Uri.sameHostWithoutMobileSubdomainAs(other: Uri): Boolean {
+ val thisHost = hostWithoutCommonPrefixes?.let {
+ it.split(".")
+ .filter { subdomain -> mobileSubdomains.none { mobileSubdomain -> mobileSubdomain == subdomain } }
+ } ?: return false
+ val otherHost = other.hostWithoutCommonPrefixes?.let {
+ it.split(".")
+ .filter { subdomain -> mobileSubdomains.none { mobileSubdomain -> mobileSubdomain == subdomain } }
+ } ?: return false
+ return thisHost == otherHost
+}
+
+/**
+ * Returns true if the [Uri] uses the "http" or "https" protocol scheme.
+ */
+val Uri.isHttpOrHttps: Boolean
+ get() = scheme == "http" || scheme == "https"
+
+/**
+ * Checks that the given URL is in one of the given URL [scopes].
+ *
+ * https://www.w3.org/TR/appmanifest/#dfn-within-scope
+ *
+ * @param scopes Uris that each represent a scope.
+ * A Uri is within the scope if the origin matches and it starts with the scope's path.
+ * @return True if this Uri is within any of the given scopes.
+ */
+fun Uri.isInScope(scopes: Iterable<Uri>): Boolean {
+ val path = path.orEmpty()
+ return scopes.any { scope ->
+ sameOriginAs(scope) && path.startsWith(scope.path.orEmpty())
+ }
+}
+
+/**
+ * Checks that Uri has the same scheme and host as [other].
+ */
+fun Uri.sameSchemeAndHostAs(other: Uri) = scheme == other.scheme && host == other.host
+
+/**
+ * Checks that Uri has the same origin as [other].
+ *
+ * https://html.spec.whatwg.org/multipage/origin.html#same-origin
+ */
+fun Uri.sameOriginAs(other: Uri) = sameSchemeAndHostAs(other) && port == other.port
+
+/**
+ * Indicate if the [this] uri is under the application private directory.
+ */
+fun Uri.isUnderPrivateAppDirectory(context: Context): Boolean {
+ return when (this.scheme) {
+ ContentResolver.SCHEME_FILE -> {
+ try {
+ val uriPath = path ?: return true
+ val uriCanonicalPath = File(uriPath).canonicalPath
+ val dataDirCanonicalPath = File(context.applicationInfo.dataDir).canonicalPath
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+ uriCanonicalPath.startsWith(dataDirCanonicalPath)
+ } else {
+ // We have to do this manual check on early builds of Android 11
+ // as symlink didn't resolve from /data/user/ to data/data
+ // we have to revise this again once Android 11 is out
+ // https://github.com/mozilla-mobile/android-components/issues/7750
+ uriCanonicalPath.startsWith("/data/data") || uriCanonicalPath.startsWith("/data/user")
+ }
+ } catch (e: IOException) {
+ true
+ }
+ }
+ else -> false
+ }
+}
+
+/**
+ * Return a file name for [this] give Uri.
+ * @return A file name for the content, or generated file name if the URL is invalid or the type is unknown
+ */
+fun Uri.getFileName(contentResolver: ContentResolver): String {
+ return when (this.scheme) {
+ ContentResolver.SCHEME_FILE -> File(path ?: "").name.sanitizeFileName()
+ ContentResolver.SCHEME_CONTENT -> getFileNameForContentUris(contentResolver)
+ else -> {
+ generateFileName(getFileExtension(contentResolver))
+ }
+ }
+}
+
+/**
+ * Return a file extension for [this] give Uri (only supports content:// schemes).
+ * @return A file extension for the content, or empty string if the URL is invalid or the type is unknown
+ */
+fun Uri.getFileExtension(contentResolver: ContentResolver): String {
+ return MimeTypeMap.getSingleton().getExtensionFromMimeType(contentResolver.getType(this)) ?: ""
+}
+
+/**
+ * Copy the content of [this] [Uri] into a temporary file in the given [dirToCopy]
+ * @return A "file://" [Uri] which contains the content of [this] [Uri].
+ */
+fun Uri.toFileUri(context: Context, dirToCopy: String = "/temps"): Uri {
+ val contentResolver = context.contentResolver
+ val cacheUploadDirectory = File(context.cacheDir, dirToCopy)
+
+ if (!cacheUploadDirectory.exists()) {
+ cacheUploadDirectory.mkdir()
+ }
+
+ val temporalFile = File(cacheUploadDirectory, getFileName(contentResolver))
+ try {
+ contentResolver.openInputStream(this)!!.use { inStream ->
+ copyFile(temporalFile, inStream)
+ }
+ } catch (e: IOException) {
+ Logger("Uri.kt").warn("Could not convert uri to file uri", e)
+ }
+ return Uri.parse("file:///${Uri.encode(temporalFile.absolutePath)}")
+}
+
+@VisibleForTesting
+internal fun copyFile(temporalFile: File, inStream: InputStream): Long {
+ return FileOutputStream(temporalFile).use { outStream ->
+ inStream.copyTo(outStream)
+ }
+}
+
+@VisibleForTesting
+internal fun Uri.getFileNameForContentUris(contentResolver: ContentResolver): String {
+ var fileName = ""
+ contentResolver.query(this, null, null, null, null)?.use { cursor ->
+ val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ val fileExtension = getFileExtension(contentResolver)
+ fileName = if (nameIndex == -1) {
+ generateFileName(fileExtension)
+ } else {
+ cursor.moveToFirst()
+ cursor.getString(nameIndex) ?: generateFileName(fileExtension)
+ }
+ }
+ return fileName.sanitizeFileName()
+}
+
+/**
+ * Generate a file name using a randomUUID + the current timestamp.
+ */
+@VisibleForTesting
+internal fun generateFileName(fileExtension: String = ""): String {
+ val randomId = UUID.randomUUID().toString().removePrefix("-").trim()
+ val timeStamp = System.currentTimeMillis()
+ return if (fileExtension.isNotEmpty()) {
+ "$randomId$timeStamp.$fileExtension"
+ } else {
+ "$randomId$timeStamp"
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt
new file mode 100644
index 0000000000..d4d7063fc0
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt
@@ -0,0 +1,49 @@
+/* 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/. */
+
+@file:Suppress("MatchingDeclarationName")
+
+package mozilla.components.support.ktx.android.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.annotation.StringRes
+import androidx.core.content.getSystemService
+
+/**
+ * Make sure a notification channel exists.
+ * @param context A [Context], used for creating the notification channel.
+ * @param onSetupChannel A lambda in the context of the NotificationChannel that gives you the
+ * opportunity to apply any setup on the channel before gets created.
+ * @param onCreateChannel A lambda in the context of the NotificationManager that gives you the
+ * opportunity to perform any operation on the [NotificationManager].
+ * @return Returns the channel id to be used.
+ */
+fun ensureNotificationChannelExists(
+ context: Context,
+ channelDate: ChannelData,
+ onSetupChannel: NotificationChannel.() -> Unit = {},
+ onCreateChannel: NotificationManager.() -> Unit = {},
+): String {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager = requireNotNull(context.getSystemService<NotificationManager>())
+ val channel = NotificationChannel(
+ channelDate.id,
+ context.getString(channelDate.name),
+ channelDate.importance,
+ )
+ onSetupChannel(channel)
+ notificationManager.createNotificationChannel(channel)
+ onCreateChannel(notificationManager)
+ }
+
+ return channelDate.id
+}
+
+/**
+ * Wraps the data of a NotificationChannel as this class is available after API 26.
+ */
+class ChannelData(val id: String, @StringRes val name: Int, val importance: Int)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt
new file mode 100644
index 0000000000..de9608508e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt
@@ -0,0 +1,57 @@
+/* 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.support.ktx.android.org.json
+
+import org.json.JSONArray
+import org.json.JSONException
+
+/**
+ * Convenience method to convert a JSONArray into a sequence.
+ *
+ * @param getter callback to get the value for an index in the array.
+ */
+inline fun <V> JSONArray.asSequence(crossinline getter: JSONArray.(Int) -> V): Sequence<V> {
+ val indexRange = 0 until length()
+ return indexRange.asSequence().map { i -> getter(i) }
+}
+
+fun JSONArray.asSequence(): Sequence<Any> = asSequence { i -> get(i) }
+
+/**
+ * Convenience method to convert a JSONArray into a List
+ *
+ * @return list with the JSONArray values, or an empty list if the JSONArray was null
+ */
+@Suppress("UNCHECKED_CAST")
+fun <T> JSONArray?.toList(): List<T> {
+ val array = this ?: return emptyList()
+ return array.asSequence().map { it as T }.toList()
+}
+
+/**
+ * Returns a list containing only the non-null results of applying the given [transform] function
+ * to each element in the original collection as returned by [getFromArray]. If [getFromArray]
+ * or [transform] throws a [JSONException], these elements will also be omitted.
+ *
+ * Here's an example call:
+ * ```kotlin
+ * jsonArray.mapNotNull(JSONArray::getJSONObject) { jsonObj -> jsonObj.getString("author") }
+ * ```
+ */
+inline fun <T, R : Any> JSONArray.mapNotNull(getFromArray: JSONArray.(index: Int) -> T, transform: (T) -> R?): List<R> {
+ val transformedResults = mutableListOf<R>()
+ for (i in 0 until this.length()) {
+ try {
+ val transformed = transform(getFromArray(i))
+ if (transformed != null) { transformedResults.add(transformed) }
+ } catch (e: JSONException) { /* Do nothing: we skip bad data. */ }
+ }
+
+ return transformedResults
+}
+
+fun Iterable<Any>.toJSONArray() = JSONArray().also { array ->
+ forEach { array.put(it) }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt
new file mode 100644
index 0000000000..a939afd2e6
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt
@@ -0,0 +1,98 @@
+/* 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.support.ktx.android.org.json
+
+import org.json.JSONObject
+import java.util.TreeMap
+
+/**
+ * Returns the value mapped by {@code key} if it exists, and
+ * if the value returned is not null. If it's null, it returns null
+ */
+fun JSONObject.tryGet(key: String): Any? = if (isNull(key)) null else get(key)
+
+/**
+ * Returns the value mapped by {@code key} if it exists, and
+ * if the value returned is not null. If it's null, it returns null
+ */
+fun JSONObject.tryGetString(key: String): String? = if (isNull(key)) null else getString(key)
+
+/**
+ * Returns the value mapped by {@code key} if it exists, and
+ * if the value returned is not null. If it's null, it returns null
+ */
+fun JSONObject.tryGetInt(key: String): Int? = if (isNull(key)) null else getInt(key)
+
+/**
+ * Returns the value mapped by {@code key} if it exists, and
+ * if the value returned is not null. If it's null, it returns null
+ */
+fun JSONObject.tryGetLong(key: String): Long? = if (isNull(key)) null else getLong(key)
+
+/**
+ * Puts the specified value under the key if it's not null
+ */
+fun JSONObject.putIfNotNull(key: String, value: Any?) {
+ if (value != null) {
+ put(key, value)
+ }
+}
+
+/**
+ * Sorts the keys of a JSONObject (and all of its child JSONObjects) alphabetically
+ */
+fun JSONObject.sortKeys(): JSONObject {
+ val map = TreeMap<String, Any>()
+ for (key in this.keys()) {
+ map[key] = this[key]
+ }
+ val jsonObject = JSONObject()
+ for (key in map.keys) {
+ if (map[key] is JSONObject) {
+ map[key] = (map[key] as JSONObject).sortKeys()
+ }
+ jsonObject.put(key, map[key])
+ }
+ return jsonObject
+}
+
+/**
+ * Convert a Map<String, String> to a JSONObject
+ */
+fun Map<String, String>.toJSON() = JSONObject().apply {
+ forEach { (key, value) -> put(key, value) }
+}
+
+/**
+ * Merge the contents of another [JSONObject] with this object,
+ * overwriting the colliding keys.
+ *
+ * @param other the [JSONObject] providing the data to be
+ * merged with this one.
+ */
+fun JSONObject.mergeWith(other: JSONObject) {
+ for (key in other.keys()) {
+ put(key, other[key])
+ }
+}
+
+/**
+ * Gets the [JSONObject] value with the given key if it exists.
+ * Otherwise calls the defaultValue function, adds its
+ * result to the object, and returns that.
+ *
+ * @param key the key to get or create.
+ * @param defaultValue a function returning a new default value
+ * @return the existing or new value
+ */
+fun JSONObject.getOrPutJSONObject(key: String, defaultValue: () -> JSONObject): JSONObject {
+ optJSONObject(key)?.let {
+ return it
+ } ?: run {
+ val value = defaultValue()
+ put(key, value)
+ return value
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt
new file mode 100644
index 0000000000..2bd0be117b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt
@@ -0,0 +1,38 @@
+/* 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.support.ktx.android.os
+
+import android.os.Bundle
+
+/**
+ * Returns `true` if the two specified bundles are *structurally* equal to one another,
+ * i.e. contain the same number of the same elements in the same order.
+ */
+@Suppress("ComplexMethod")
+infix fun Bundle.contentEquals(other: Bundle): Boolean {
+ if (size() != other.size()) return false
+
+ @Suppress("DEPRECATION") // we still need to use get(String) in order to compare any Objects.
+ return keySet().all { key ->
+ val valueTwo = other.get(key)
+ when (val valueOne = get(key)) {
+ // Compare bundles deeply
+ is Bundle -> valueTwo is Bundle && valueOne contentEquals valueTwo
+
+ // Compare arrays using contentEquals
+ is BooleanArray -> valueTwo is BooleanArray && valueOne contentEquals valueTwo
+ is ByteArray -> valueTwo is ByteArray && valueOne contentEquals valueTwo
+ is CharArray -> valueTwo is CharArray && valueOne contentEquals valueTwo
+ is DoubleArray -> valueTwo is DoubleArray && valueOne contentEquals valueTwo
+ is FloatArray -> valueTwo is FloatArray && valueOne contentEquals valueTwo
+ is IntArray -> valueTwo is IntArray && valueOne contentEquals valueTwo
+ is LongArray -> valueTwo is LongArray && valueOne contentEquals valueTwo
+ is ShortArray -> valueTwo is ShortArray && valueOne contentEquals valueTwo
+ is Array<*> -> valueTwo is Array<*> && valueOne contentEquals valueTwo
+
+ else -> valueOne == valueTwo
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt
new file mode 100644
index 0000000000..ffa8f6e3b5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt
@@ -0,0 +1,20 @@
+/* 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.support.ktx.android.os
+
+import android.os.StrictMode
+
+/**
+ * Runs the given [functionBlock] and sets the ThreadPolicy after its completion.
+ *
+ * This function is written in the style of [AutoCloseable.use].
+ *
+ * @return the value returned by [functionBlock].
+ */
+inline fun <R> StrictMode.ThreadPolicy.resetAfter(functionBlock: () -> R): R = try {
+ functionBlock()
+} finally {
+ StrictMode.setThreadPolicy(this)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt
new file mode 100644
index 0000000000..8c0cb9a271
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt
@@ -0,0 +1,28 @@
+/* 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.support.ktx.android.os
+
+import android.Manifest.permission.VIBRATE
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.VibrationEffect
+import android.os.VibrationEffect.DEFAULT_AMPLITUDE
+import android.os.Vibrator
+import androidx.annotation.RequiresPermission
+
+/**
+ * Vibrate constantly for the specified period of time.
+ *
+ * @param milliseconds The number of milliseconds to vibrate.
+ */
+@RequiresPermission(VIBRATE)
+fun Vibrator.vibrateOneShot(milliseconds: Long) {
+ if (SDK_INT >= Build.VERSION_CODES.O) {
+ vibrate(VibrationEffect.createOneShot(milliseconds, DEFAULT_AMPLITUDE))
+ } else {
+ @Suppress("Deprecation")
+ vibrate(milliseconds)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt
new file mode 100644
index 0000000000..ffea3eaf64
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt
@@ -0,0 +1,12 @@
+/* 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.support.ktx.android.util
+
+import android.util.Base64
+
+object Base64 {
+ fun encodeToUriString(data: String) =
+ "data:text/html;base64," + Base64.encodeToString(data.toByteArray(), Base64.DEFAULT)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt
new file mode 100644
index 0000000000..ecb54c7ea3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt
@@ -0,0 +1,36 @@
+/* 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.support.ktx.android.util
+
+import android.util.DisplayMetrics
+import android.util.TypedValue
+
+/**
+ * Converts a value in density independent pixels (dp) to a float value.
+ */
+fun Int.dpToFloat(displayMetrics: DisplayMetrics) = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ this.toFloat(),
+ displayMetrics,
+)
+
+/**
+ * Converts a value in density independent pixels (dp) to the actual pixel values for the display.
+ */
+fun Int.dpToPx(displayMetrics: DisplayMetrics) = dpToFloat(displayMetrics).toInt()
+
+/** Converts a value in density independent pixels (dp) to a px value. */
+fun Float.dpToPx(displayMetrics: DisplayMetrics) = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ this,
+ displayMetrics,
+)
+
+/** Converts a value in scale independent pixels (sp) to a px value. */
+fun Float.spToPx(displayMetrics: DisplayMetrics) = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ this,
+ displayMetrics,
+)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt
new file mode 100644
index 0000000000..b1d29d000d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt
@@ -0,0 +1,47 @@
+/* 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.support.ktx.android.util
+
+import android.util.JsonReader
+import android.util.JsonToken
+
+/**
+ * Returns the [JsonToken.STRING] value of the next token or `null` if the next token
+ * is [JsonToken.NULL].
+ */
+fun JsonReader.nextStringOrNull(): String? {
+ return if (peek() == JsonToken.NULL) {
+ nextNull()
+ null
+ } else {
+ nextString()
+ }
+}
+
+/**
+ * Returns the [JsonToken.BOOLEAN] value of the next token or `null` if the next token
+ * is [JsonToken.NULL].
+ */
+fun JsonReader.nextBooleanOrNull(): Boolean? {
+ return if (peek() == JsonToken.NULL) {
+ nextNull()
+ null
+ } else {
+ nextBoolean()
+ }
+}
+
+/**
+ * Returns the [JsonToken.NUMBER] value of the next token or `null` if the next token
+ * is [JsonToken.NULL].
+ */
+fun JsonReader.nextIntOrNull(): Int? {
+ return if (peek() == JsonToken.NULL) {
+ nextNull()
+ null
+ } else {
+ nextInt()
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt
new file mode 100644
index 0000000000..283bd30720
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt
@@ -0,0 +1,95 @@
+/* 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.support.ktx.android.view
+
+import android.app.Activity
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES
+import android.view.View
+import android.view.WindowManager
+import androidx.core.view.ViewCompat
+import androidx.core.view.ViewCompat.onApplyWindowInsets
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Attempts to enter immersive mode - fullscreen with the status bar and navigation buttons hidden,
+ * expanding itself into the notch area for devices running API 28+.
+ *
+ * This will automatically register and use an inset listener: [View.OnApplyWindowInsetsListener]
+ * to restore immersive mode if interactions with various other widgets like the keyboard or dialogs
+ * got the activity out of immersive mode without [exitImmersiveMode] being called.
+ */
+fun Activity.enterImmersiveMode(
+ insetsController: WindowInsetsControllerCompat = window.createWindowInsetsController(),
+) {
+ insetsController.hideInsets()
+
+ ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, insetsCompat ->
+ if (insetsCompat.isVisible(WindowInsetsCompat.Type.statusBars())) {
+ insetsController.hideInsets()
+ }
+ // Allow the decor view to have a chance to process the incoming WindowInsets.
+ onApplyWindowInsets(view, insetsCompat)
+ }
+
+ if (SDK_INT >= VERSION_CODES.P) {
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+ WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+ )
+ window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+ }
+}
+
+private fun WindowInsetsControllerCompat.hideInsets() {
+ apply {
+ hide(WindowInsetsCompat.Type.systemBars())
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+}
+
+/**
+ * Shows the system UI windows that were hidden, thereby exiting the immersive experience.
+ * For devices running API 28+, this function also restores the application's use
+ * of the notch area of the phone to the default behavior.
+ *
+ * @param insetsController is an optional [WindowInsetsControllerCompat] object for controlling the
+ * window insets.
+ */
+fun Activity.exitImmersiveMode(
+ insetsController: WindowInsetsControllerCompat = window.createWindowInsetsController(),
+) {
+ insetsController.show(WindowInsetsCompat.Type.systemBars())
+
+ ViewCompat.setOnApplyWindowInsetsListener(window.decorView, null)
+
+ if (SDK_INT >= VERSION_CODES.P) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ }
+}
+
+/**
+ * Calls [Activity.reportFullyDrawn] while also preventing crashes under some circumstances.
+ *
+ * @param errorLogger the logger to be used if errors are logged.
+ */
+fun Activity.reportFullyDrawnSafe(errorLogger: Logger) {
+ try {
+ reportFullyDrawn()
+ } catch (e: SecurityException) {
+ // This exception is throw on some Samsung devices. We were unable to identify the root
+ // cause but suspect it's related to Samsung security features. See
+ // https://github.com/mozilla-mobile/fenix/issues/12345#issuecomment-655058864 for details.
+ //
+ // We include "Fully drawn" in the log statement so that this error appears when grepping
+ // for fully drawn time.
+ errorLogger.error("Fully drawn - unable to call reportFullyDrawn", e)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt
new file mode 100644
index 0000000000..7861fc7e64
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt
@@ -0,0 +1,19 @@
+/* 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.support.ktx.android.view
+
+import android.view.MotionEvent
+
+/**
+ * Executes the given [functionBlock] function on this resource and then closes it down correctly whether
+ * an exception is thrown or not. This is inspired by [java.lang.AutoCloseable.use].
+ */
+inline fun <R> MotionEvent.use(functionBlock: (MotionEvent) -> R): R {
+ try {
+ return functionBlock(this)
+ } finally {
+ recycle()
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt
new file mode 100644
index 0000000000..0fec74272e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.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/. */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public APIs.
+
+package mozilla.components.support.ktx.android.view
+
+import android.graphics.drawable.Drawable
+import android.widget.TextView
+
+/**
+ * Sets the [Drawable]s (if any) to appear to the start of, above, to the end of,
+ * and below the text. Use `null` if you do not want a Drawable there.
+ * The Drawables must already have had [Drawable.setBounds] called.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * [TextView.setCompoundDrawables] or related methods.
+ */
+inline fun TextView.putCompoundDrawablesRelative(
+ start: Drawable? = null,
+ top: Drawable? = null,
+ end: Drawable? = null,
+ bottom: Drawable? = null,
+) = setCompoundDrawablesRelative(start, top, end, bottom)
+
+/**
+ *
+ * Sets the [Drawable]s (if any) to appear to the start of, above, to the end of,
+ * and below the text. Use `null` if you do not want a Drawable there.
+ * The Drawables' bounds will be set to their intrinsic bounds.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * [TextView.setCompoundDrawables] or related methods.
+ */
+inline fun TextView.putCompoundDrawablesRelativeWithIntrinsicBounds(
+ start: Drawable? = null,
+ top: Drawable? = null,
+ end: Drawable? = null,
+ bottom: Drawable? = null,
+) = setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt
new file mode 100644
index 0000000000..625eae2c96
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt
@@ -0,0 +1,187 @@
+/* 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.support.ktx.android.view
+
+import android.graphics.Rect
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.MainThread
+import androidx.core.content.getSystemService
+import androidx.core.view.ViewCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.ktx.android.util.dpToPx
+import java.lang.ref.WeakReference
+
+/**
+ * Is the horizontal layout direction of this view from Right to Left?
+ */
+val View.isRTL: Boolean
+ get() = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL
+
+/**
+ * Is the horizontal layout direction of this view from Left to Right?
+ */
+val View.isLTR: Boolean
+ get() = layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR
+
+/**
+ * Tries to focus this view and show the soft input window for it.
+ *
+ * @param flags Provides additional operating flags to be used with InputMethodManager.showSoftInput().
+ * Currently may be 0, SHOW_IMPLICIT or SHOW_FORCED.
+ */
+fun View.showKeyboard(flags: Int = InputMethodManager.SHOW_IMPLICIT) {
+ ShowKeyboard(this, flags).post()
+}
+
+/**
+ * Hides the soft input window.
+ */
+fun View.hideKeyboard() {
+ val imm = context.getSystemService<InputMethodManager>()
+ imm?.hideSoftInputFromWindow(windowToken, 0)
+}
+
+/**
+ * Fills the given [Rect] with data about location view in the window.
+ *
+ * @see View.getLocationInWindow
+ */
+fun View.getRectWithViewLocation(): Rect {
+ val locationInWindow = IntArray(2).apply { getLocationInWindow(this) }
+ return Rect(
+ locationInWindow[0],
+ locationInWindow[1],
+ locationInWindow[0] + width,
+ locationInWindow[1] + height,
+ )
+}
+
+/**
+ * Set a padding using [Padding] object.
+ */
+fun View.setPadding(padding: Padding) {
+ with(resources) {
+ setPadding(
+ padding.left.dpToPx(displayMetrics),
+ padding.top.dpToPx(displayMetrics),
+ padding.right.dpToPx(displayMetrics),
+ padding.bottom.dpToPx(displayMetrics),
+ )
+ }
+}
+
+/**
+ * Creates a [CoroutineScope] that is active as long as this [View] is attached. Once this [View]
+ * gets detached this [CoroutineScope] gets cancelled automatically.
+ *
+ * By default coroutines dispatched on the created [CoroutineScope] run on the main dispatcher.
+ *
+ * Note: This scope gets only cancelled if the [View] gets detached. In cases where the [View] never
+ * gets attached this may create a scope that never gets cancelled!
+ */
+@MainThread
+fun View.toScope(): CoroutineScope {
+ val scope = MainScope()
+
+ addOnAttachStateChangeListener(
+ object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(view: View) = Unit
+
+ override fun onViewDetachedFromWindow(view: View) {
+ scope.cancel()
+ view.removeOnAttachStateChangeListener(this)
+ }
+ },
+ )
+
+ return scope
+}
+
+/**
+ * Finds the first a view in the hierarchy, for which the provided predicate is true.
+ */
+fun View.findViewInHierarchy(predicate: (View) -> Boolean): View? {
+ if (predicate(this)) return this
+
+ if (this is ViewGroup) {
+ for (i in 0 until this.childCount) {
+ val childView = this.getChildAt(i).findViewInHierarchy(predicate)
+ if (childView != null) return childView
+ }
+ }
+
+ return null
+}
+
+/**
+ * Registers a one-time callback to be invoked when the global layout state
+ * or the visibility of views within the view tree changes.
+ */
+inline fun View.onNextGlobalLayout(crossinline callback: () -> Unit) {
+ var listener: ViewTreeObserver.OnGlobalLayoutListener? = null
+ listener = ViewTreeObserver.OnGlobalLayoutListener {
+ viewTreeObserver.removeOnGlobalLayoutListener(listener)
+ callback()
+ }
+ viewTreeObserver.addOnGlobalLayoutListener(listener)
+}
+
+private class ShowKeyboard(
+ view: View,
+ private val flags: Int = InputMethodManager.SHOW_IMPLICIT,
+) : Runnable {
+ private val weakReference: WeakReference<View> = WeakReference(view)
+ private val handler: Handler = Handler(Looper.getMainLooper())
+ private var tries: Int = TRIES
+
+ override fun run() {
+ weakReference.get()?.let { view ->
+ if (!view.isFocusable || !view.isFocusableInTouchMode) {
+ // The view is not focusable - we can't show the keyboard for it.
+ return
+ }
+
+ if (!view.requestFocus()) {
+ // Focus this view first.
+ post()
+ return
+ }
+
+ view.context?.getSystemService<InputMethodManager>()?.let { imm ->
+ if (!imm.isActive(view)) {
+ // This view is not the currently active view for the input method yet.
+ post()
+ return
+ }
+
+ if (!imm.showSoftInput(view, flags)) {
+ // Showing they keyboard failed. Try again later.
+ post()
+ }
+ }
+ }
+ }
+
+ fun post() {
+ tries--
+
+ if (tries > 0) {
+ handler.postDelayed(this, INTERVAL_MS)
+ }
+ }
+
+ companion object {
+ private const val INTERVAL_MS = 100L
+ private const val TRIES = 10
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt
new file mode 100644
index 0000000000..0e564cf6cf
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt
@@ -0,0 +1,46 @@
+/* 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.support.ktx.android.view
+
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.view.Window
+import androidx.annotation.ColorInt
+import androidx.core.view.WindowInsetsControllerCompat
+import mozilla.components.support.utils.ColorUtils.isDark
+
+/**
+ * Sets the status bar background color. If the color is light enough, a light navigation bar with
+ * dark icons will be used.
+ */
+fun Window.setStatusBarTheme(@ColorInt color: Int) {
+ createWindowInsetsController().isAppearanceLightStatusBars = !isDark(color)
+ statusBarColor = color
+}
+
+/**
+ * Set the navigation bar background and divider colors. If the color is light enough, a light
+ * navigation bar with dark icons will be used.
+ */
+fun Window.setNavigationBarTheme(
+ @ColorInt navBarColor: Int? = null,
+ @ColorInt navBarDividerColor: Int? = null,
+) {
+ navBarColor?.let {
+ navigationBarColor = it
+ createWindowInsetsController().isAppearanceLightNavigationBars = !isDark(it)
+ }
+
+ if (SDK_INT >= Build.VERSION_CODES.P) {
+ navigationBarDividerColor = navBarDividerColor ?: 0
+ }
+}
+
+/**
+ * Creates a {@link WindowInsetsControllerCompat} for the top-level window decor view.
+ */
+fun Window.createWindowInsetsController(): WindowInsetsControllerCompat {
+ return WindowInsetsControllerCompat(this, this.decorView)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt
new file mode 100644
index 0000000000..00a0a80bc9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt
@@ -0,0 +1,33 @@
+/* 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.support.ktx.android.widget
+
+import android.view.View
+import android.widget.TextView
+
+/* This is the sum of both the default ascender height and the default descender height in Android */
+private const val DEFAULT_FONT_PADDING = 6
+
+/**
+ * Adjusts the text size of the [TextView] according to the height restriction given to the
+ * [View.MeasureSpec] given in the parameter.
+ *
+ * This will take [TextView.getIncludeFontPadding] into account when calculating the available height
+ */
+fun TextView.adjustMaxTextSize(heightMeasureSpec: Int, ascenderPadding: Int = DEFAULT_FONT_PADDING) {
+ val maxHeight = View.MeasureSpec.getSize(heightMeasureSpec)
+
+ var availableHeight = maxHeight.toFloat()
+ if (this.includeFontPadding) {
+ availableHeight -= ascenderPadding * resources.displayMetrics.density
+ }
+
+ availableHeight -= (this.paddingBottom + this.paddingTop) *
+ resources.displayMetrics.density
+
+ if (availableHeight > 0 && this.textSize > availableHeight) {
+ this.textSize = availableHeight / resources.displayMetrics.density
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt
new file mode 100644
index 0000000000..c3e925fc06
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt
@@ -0,0 +1,26 @@
+/* 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.support.ktx.java.io
+
+import java.io.File
+
+/**
+ * Removes all files in the directory named by this abstract pathname. Does nothing if the [File] is not pointing to
+ * a directory.
+ */
+fun File.truncateDirectory() {
+ if (!isDirectory) {
+ return
+ }
+
+ listFiles()?.forEach { file ->
+ if (file.isDirectory) {
+ file.truncateDirectory()
+ file.delete()
+ } else {
+ file.delete()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt
new file mode 100644
index 0000000000..0d12578d3d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt
@@ -0,0 +1,107 @@
+/* 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.support.ktx.kotlin
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import mozilla.components.support.base.log.logger.Logger
+import java.security.MessageDigest
+
+/**
+ * Checks whether the given [test] byte sequence exists at the [offset] of this [ByteArray]
+ */
+fun ByteArray.containsAtOffset(offset: Int, test: ByteArray): Boolean {
+ if (size - offset < test.size) {
+ return false
+ }
+
+ for (i in 0 until test.size) {
+ if (this[offset + i] != test[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+fun ByteArray.toBitmap(opts: BitmapFactory.Options? = null): Bitmap? {
+ return toBitmap(0, size, opts)
+}
+
+fun ByteArray.toBitmap(
+ offset: Int,
+ length: Int,
+ opts: BitmapFactory.Options? = null,
+): Bitmap? {
+ if (length <= 0) {
+ return null
+ }
+
+ return try {
+ val bitmap = BitmapFactory.decodeByteArray(this, offset, length, opts)
+
+ if (bitmap == null) {
+ null
+ } else if (bitmap.width <= 0 || bitmap.height <= 0) {
+ Logger.warn("Decoded bitmap jas dimensions: ${bitmap.width} x ${bitmap.height}")
+ null
+ } else {
+ bitmap
+ }
+ } catch (e: OutOfMemoryError) {
+ Logger.warn("OutOfMemoryError while decoding byte array", e)
+ null
+ }
+}
+
+fun ByteArray.toSha256Digest(): ByteArray {
+ return MessageDigest.getInstance("SHA-256").digest(this)
+}
+
+/**
+ * @return A SHA-1 digest.
+ */
+fun ByteArray.toSha1Digest(): ByteArray {
+ return MessageDigest.getInstance("SHA-1").digest(this)
+}
+
+/**
+ * @return An unpadded byte array, according to PKCS#7.
+ */
+@Suppress("MagicNumber")
+fun ByteArray.pkcs7unpad(): ByteArray {
+ // Last byte tells us the padding length.
+ val paddingLength = this.last()
+ // Padding can't be more than 15 bytes.
+ if (paddingLength in 0..16) {
+ return this.copyOfRange(0, this.size - paddingLength)
+ }
+ return this
+}
+
+fun ByteArray.toHexString(): String {
+ return toHexString(2 * this.size)
+}
+
+@Suppress("MagicNumber")
+fun ByteArray.toHexString(hexLength: Int): String {
+ val hs = StringBuilder(Math.max(2 * this.size, hexLength))
+ var stmp: String
+
+ for (n in 0 until hexLength - 2 * this.size) {
+ hs.append("0")
+ }
+
+ for (n in this.indices) {
+ stmp = Integer.toHexString(this[n].toInt() and 0XFF)
+
+ if (stmp.length == 1) {
+ hs.append("0")
+ }
+ hs.append(stmp)
+ }
+
+ return hs.toString()
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt
new file mode 100644
index 0000000000..03425446ab
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt
@@ -0,0 +1,14 @@
+/* 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/. */
+
+@file:SuppressWarnings("TopLevelPropertyNaming")
+
+package mozilla.components.support.ktx.kotlin
+
+/**
+ * A series of dots (typically three, such as "…") that usually indicates an intentional omission of
+ * a word, sentence, or whole section from a text without altering its original meaning.
+ */
+val Char.Companion.ELLIPSIS: Char
+ get() = '…'
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt
new file mode 100644
index 0000000000..9cda9e3212
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt
@@ -0,0 +1,26 @@
+/* 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.support.ktx.kotlin
+
+/**
+ * Performs a cartesian product of all the elements in two collections and returns each pair to
+ * the [block] function.
+ *
+ * Example:
+ *
+ * ```kotlin
+ * val numbers = listOf(1, 2, 3)
+ * val letters = listOf('a', 'b', 'c')
+ * numbers.crossProduct(letters) { number, letter ->
+ * // Each combination of (1, a), (1, b), (1, c), (2, a), (2, b), etc.
+ * }
+ * ```
+ */
+inline fun <T, U, R> Collection<T>.crossProduct(
+ other: Collection<U>,
+ block: (T, U) -> R,
+) = flatMap { first ->
+ other.map { second -> first to second }.map { block(it.first, it.second) }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
new file mode 100644
index 0000000000..abcd1a741c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
@@ -0,0 +1,442 @@
+/* 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/. */
+
+@file:Suppress("TooManyFunctions")
+
+package mozilla.components.support.ktx.kotlin
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.InetAddresses
+import android.net.Uri
+import android.os.Build
+import android.util.Base64
+import android.util.Patterns
+import android.webkit.URLUtil
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.ktx.android.net.commonPrefixes
+import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
+import mozilla.components.support.ktx.util.URLStringUtils
+import java.io.File
+import java.net.IDN
+import java.net.MalformedURLException
+import java.net.URL
+import java.net.URLEncoder
+import java.security.MessageDigest
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import kotlin.text.RegexOption.IGNORE_CASE
+
+/**
+ * A collection of regular expressions used in the `is*` methods below.
+ */
+private val re = object {
+ val phoneish = "^\\s*tel:\\S?\\d+\\S*\\s*$".toRegex(IGNORE_CASE)
+ val emailish = "^\\s*mailto:\\w+\\S*\\s*$".toRegex(IGNORE_CASE)
+ val geoish = "^\\s*geo:\\S*\\d+\\S*\\s*$".toRegex(IGNORE_CASE)
+}
+
+private const val MAILTO = "mailto:"
+
+// Number of last digits to be shown when credit card number is obfuscated.
+private const val LAST_VISIBLE_DIGITS_COUNT = 4
+
+// This is used for truncating URLs to prevent extreme cases from
+// slowing down UI rendering e.g. in case of a bookmarklet or a data URI.
+// https://github.com/mozilla-mobile/android-components/issues/5249
+const val MAX_URI_LENGTH = 25000
+
+private const val FILE_PREFIX = "file://"
+private const val MAX_VALID_PORT = 65_535
+
+/**
+ * Shortens URLs to be more user friendly.
+ *
+ * The algorithm used to generate these strings is a combination of FF desktop 'top sites',
+ * feedback from the security team, and documentation regarding url elision. See
+ * StringTest.kt for details.
+ *
+ * This method is complex because URLs have a lot of edge cases. Be sure to thoroughly unit
+ * test any changes you make to it.
+ */
+// Unused Parameter: We may resume stripping eTLD, depending on conversations between security and UX
+// Return count: This is a complex method, but it would not be more understandable if broken up
+// ComplexCondition: Breaking out the complex condition would make this logic harder to follow
+@Suppress("UNUSED_PARAMETER", "ReturnCount", "ComplexCondition")
+fun String.toShortUrl(publicSuffixList: PublicSuffixList): String {
+ val inputString = this
+ val uri = inputString.toUri()
+
+ if (
+ inputString.isEmpty() ||
+ !URLUtil.isValidUrl(inputString) ||
+ inputString.startsWith(FILE_PREFIX) ||
+ uri.port !in -1..MAX_VALID_PORT
+ ) {
+ return inputString
+ }
+
+ if (uri.host?.isIpv4OrIpv6() == true ||
+ // If inputString is just a hostname and not a FQDN, use the entire hostname.
+ uri.host?.contains(".") == false
+ ) {
+ return uri.host ?: inputString
+ }
+
+ fun String.stripUserInfo(): String {
+ val userInfo = this.toUri().encodedUserInfo
+ return if (userInfo != null) {
+ val infoIndex = this.indexOf(userInfo)
+ this.removeRange(infoIndex..infoIndex + userInfo.length)
+ } else {
+ this
+ }
+ }
+ fun String.stripPrefixes(): String = this.toUri().hostWithoutCommonPrefixes ?: this
+ fun String.toUnicode() = IDN.toUnicode(this)
+
+ return inputString
+ .stripUserInfo()
+ .lowercase(Locale.getDefault())
+ .stripPrefixes()
+ .toUnicode()
+}
+
+// impl via FFTV https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/main/java/org/mozilla/focus/utils/FormattedDomain.java#129
+@Suppress("DEPRECATION")
+internal fun String.isIpv4(): Boolean = Patterns.IP_ADDRESS.matcher(this).matches()
+
+// impl via FFiOS: https://github.com/mozilla-mobile/firefox-ios/blob/deb9736c905cdf06822ecc4a20152df7b342925d/Shared/Extensions/NSURLExtensions.swift#L292
+// True IPv6 validation is difficult. This is slightly better than nothing
+internal fun String.isIpv6(): Boolean {
+ return this.isNotEmpty() && this.contains(":")
+}
+
+/**
+ * Returns true if the string represents a valid Ipv4 or Ipv6 IP address.
+ * Note: does not validate a dual format Ipv6 ( "y:y:y:y:y:y:x.x.x.x" format).
+ *
+ */
+@Suppress("TooManyFunctions")
+fun String.isIpv4OrIpv6(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ InetAddresses.isNumericAddress(this)
+ } else {
+ this.isIpv4() || this.isIpv6()
+ }
+}
+
+/**
+ * Checks if this String is a URL.
+ */
+fun String.isUrl() = URLStringUtils.isURLLike(this)
+
+/**
+ * Checks if this String is a URL of an extension page.
+ */
+fun String.isExtensionUrl() = this.startsWith("moz-extension://")
+
+/**
+ * Checks if this String is a URL of a resource.
+ */
+fun String.isResourceUrl() = this.startsWith("resource://")
+
+/**
+ * Appends `http` scheme if no scheme is present in this String.
+ */
+fun String.toNormalizedUrl(): String {
+ val s = this.trim()
+ // Most commonly we'll encounter http or https schemes.
+ // For these, avoid running through toNormalizedURL as an optimization.
+ return if (!s.startsWith("http://") &&
+ !s.startsWith("https://")
+ ) {
+ URLStringUtils.toNormalizedURL(s)
+ } else {
+ s
+ }
+}
+
+fun String.isPhone() = re.phoneish.matches(this)
+
+fun String.isEmail() = re.emailish.matches(this)
+
+fun String.isGeoLocation() = re.geoish.matches(this)
+
+/**
+ * Converts a [String] to a [Date] object.
+ * @param format date format used for formatting the this given [String] object.
+ * @param locale the locale to use when converting the String, defaults to [Locale.ROOT].
+ * @return a [Date] object with the values in the provided in this string, if empty string was provided, a current date
+ * will be returned.
+ */
+fun String.toDate(format: String, locale: Locale = Locale.ROOT): Date {
+ val formatter = SimpleDateFormat(format, locale)
+ return if (isNotEmpty()) {
+ formatter.parse(this) ?: Date()
+ } else {
+ Date()
+ }
+}
+
+/**
+ * Calculates a SHA1 hash for this string.
+ */
+@Suppress("MagicNumber")
+fun String.sha1(): String {
+ val characters = "0123456789abcdef"
+ val digest = MessageDigest.getInstance("SHA-1").digest(toByteArray())
+ return digest.joinToString(
+ separator = "",
+ transform = { byte ->
+ String(charArrayOf(characters[byte.toInt() shr 4 and 0x0f], characters[byte.toInt() and 0x0f]))
+ },
+ )
+}
+
+/**
+ * Tries to convert a [String] to a [Date] using a list of [possibleFormats].
+ * @param possibleFormats one ore more possible format.
+ * @return a [Date] object with the values in the provided in this string,
+ * if the conversion is not possible null will be returned.
+ */
+fun String.toDate(
+ vararg possibleFormats: String = arrayOf(
+ "yyyy-MM-dd'T'HH:mm",
+ "yyyy-MM-dd",
+ "yyyy-'W'ww",
+ "yyyy-MM",
+ "HH:mm",
+ ),
+): Date? {
+ possibleFormats.forEach {
+ try {
+ return this.toDate(it)
+ } catch (pe: ParseException) {
+ // move to next possible format
+ }
+ }
+ return null
+}
+
+/**
+ * Tries to parse and get host part if this [String] is valid URL.
+ * Otherwise returns the string.
+ */
+fun String.tryGetHostFromUrl(): String = try {
+ URL(this).host
+} catch (e: MalformedURLException) {
+ this
+}
+
+/**
+ * Returns `true` if this string is a valid URL that contains [searchParameters] in its query parameters.
+ */
+fun String.urlContainsQueryParameters(searchParameters: String): Boolean = try {
+ URL(this).query?.split("&")?.any { it == searchParameters } ?: false
+} catch (e: MalformedURLException) {
+ false
+}
+
+/**
+ * Compares 2 URLs and returns true if they have the same origin,
+ * which means: same protocol, same host, same port.
+ * It will return false if either this or [other] is not a valid URL.
+ */
+fun String.isSameOriginAs(other: String): Boolean {
+ fun canonicalizeOrigin(urlStr: String): String {
+ val url = URL(urlStr)
+ val port = if (url.port == -1) url.defaultPort else url.port
+ val canonicalized = URL(url.protocol, url.host, port, "")
+ return canonicalized.toString()
+ }
+ return try {
+ canonicalizeOrigin(this) == canonicalizeOrigin(other)
+ } catch (e: MalformedURLException) {
+ false
+ }
+}
+
+/**
+ * Returns an origin (protocol, host and port) from an URL string.
+ */
+fun String.getOrigin(): String? {
+ return try {
+ val url = URL(this)
+ val port = if (url.port == -1) url.defaultPort else url.port
+ URL(url.protocol, url.host, port, "").toString()
+ } catch (e: MalformedURLException) {
+ null
+ }
+}
+
+/**
+ * Returns an origin without the default port.
+ * For example for an input of "https://mozilla.org:443" you will get "https://mozilla.org".
+ */
+fun String.stripDefaultPort(): String {
+ return try {
+ val url = URL(this)
+ val port = if (url.port == url.defaultPort) -1 else url.port
+ URL(url.protocol, url.host, port, "").toString()
+ } catch (e: MalformedURLException) {
+ this
+ }
+}
+
+/**
+ * Remove any unwanted character in url like spaces at the beginning or end.
+ */
+fun String.sanitizeURL(): String {
+ return this.trim()
+}
+
+/**
+ * Remove any unwanted character from string containing file name.
+ * For example for an input of "/../../../../../../directory/file.txt" you will get "file.txt"
+ */
+fun String.sanitizeFileName(): String {
+ val file = File(this.substringAfterLast(File.separatorChar))
+ // Remove unwanted subsequent dots in the file name.
+ return if (file.extension.trim().isNotEmpty() && file.nameWithoutExtension.isNotEmpty()) {
+ file.name.replace("\\.\\.+".toRegex(), ".")
+ } else {
+ file.name.replace(".", "")
+ }
+}
+
+/**
+ * Remove leading mailto from the string.
+ * For example for an input of "mailto:example@example.com" you will get "example@example.com"
+ */
+fun String.stripMailToProtocol(): String {
+ return if (this.startsWith(MAILTO)) {
+ this.replaceFirst(MAILTO, "")
+ } else {
+ this
+ }
+}
+
+/**
+ * Translates the string into {@code application/x-www-form-urlencoded} string.
+ */
+fun String.urlEncode(): String {
+ return URLEncoder.encode(this, Charsets.UTF_8.name())
+}
+
+/**
+ * Returns the string if it's length is not higher than @param[maximumLength] or
+ * a @param[replacement] string if String length is higher than @param[maximumLength]
+ */
+fun String.takeOrReplace(maximumLength: Int, replacement: String): String {
+ return if (this.length > maximumLength) replacement else this
+}
+
+/**
+ * Returns the extension (without ".") declared in the mime type of this data url.
+ * In the event that this data url does not contain a mime type or image extension could be read
+ * for any reason [defaultExtension] will be returned
+ *
+ * @param defaultExtension default extension if one could not be read from the mime type. Default is "jpg".
+ */
+fun String.getDataUrlImageExtension(defaultExtension: String = "jpg"): String {
+ return ("data:image\\/([a-zA-Z0-9-.+]+).*").toRegex()
+ .find(this)?.groups?.get(1)?.value ?: defaultExtension
+}
+
+/**
+ * Returns this char sequence if it's not null or empty
+ * or the result of calling [defaultValue] function if the char sequence is null or empty.
+ */
+inline fun <C, R> C?.ifNullOrEmpty(defaultValue: () -> R): C where C : CharSequence, R : C =
+ if (isNullOrEmpty()) defaultValue() else this
+
+/**
+ * Get the representative part of the URL. Usually this is the eTLD part of the host.
+ *
+ * For example this method will return "facebook.com" for "https://www.facebook.com/foobar".
+ */
+fun String.getRepresentativeSnippet(): String {
+ val uri = Uri.parse(this)
+
+ val host = uri.hostWithoutCommonPrefixes
+ if (!host.isNullOrEmpty()) {
+ return host
+ }
+
+ val path = uri.path
+ if (!path.isNullOrEmpty()) {
+ return path
+ }
+
+ return this
+}
+
+/**
+ * Get a representative character for the given URL.
+ *
+ * For example this method will return "f" for "https://m.facebook.com/foobar".
+ */
+fun String.getRepresentativeCharacter(): String {
+ val snippet = this.getRepresentativeSnippet()
+
+ snippet.forEach { character ->
+ if (character.isLetterOrDigit()) {
+ return character.uppercase()
+ }
+ }
+
+ return "?"
+}
+
+/**
+ * Strips common mobile subdomains from a [String].
+ */
+fun String.stripCommonSubdomains(): String {
+ for (prefix in commonPrefixes) {
+ if (this.startsWith(prefix)) return this.substring(prefix.length)
+ }
+ return this
+}
+
+/**
+ * Returns the last 4 digits from a formatted credit card number string.
+ */
+fun String.last4Digits(): String {
+ return this.takeLast(LAST_VISIBLE_DIGITS_COUNT)
+}
+
+/**
+ * Returns a trimmed string. This is used to prevent extreme cases
+ * from slowing down UI rendering with large strings.
+ */
+fun String.trimmed(): String {
+ return this.take(MAX_URI_LENGTH)
+}
+
+/**
+ * Returns a bitmap from its base64 representation.
+ * Returns null if the string is not a valid base64 representation of a bitmap
+ */
+fun String.base64ToBitmap(): Bitmap? =
+ extractBase6RawString()?.let { rawString ->
+ val raw = Base64.decode(rawString, Base64.DEFAULT)
+ BitmapFactory.decodeByteArray(raw, 0, raw.size)
+ }
+
+@VisibleForTesting
+internal fun String.extractBase6RawString(): String? {
+ // Regex that identifies if the strings starts with:
+ // "(data:image/[ANY_FORMAT];base64,"
+ // For example, "data:image/png;base64,"
+ val base64BitmapRegex = "(data:image/[^;]+;base64,)(.*)".toRegex()
+ return base64BitmapRegex.find(this)?.let {
+ val (_, contentString) = it.destructured
+ contentString
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt
new file mode 100644
index 0000000000..ca31cf7b87
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt
@@ -0,0 +1,40 @@
+/* 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.support.ktx.kotlinx.coroutines
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ *
+ * Returns a function that limits the executions of the [block] function, until the [skipTimeInMs]
+ * passes, then the latest value passed to [block] will be used. Any calls before [skipTimeInMs]
+ * passes will be ignored. All calls to the returned function must happen on the same thread.
+ *
+ * Credit to Terenfear https://gist.github.com/Terenfear/a84863be501d3399889455f391eeefe5
+ *
+ * @param skipTimeInMs the time to wait until the next call to [block] be processed.
+ * @param coroutineScope the coroutine scope where [block] will executed.
+ * @param block function to be execute.
+ */
+fun <T> throttleLatest(
+ skipTimeInMs: Long = 300L,
+ coroutineScope: CoroutineScope,
+ block: (T) -> Unit,
+): (T) -> Unit {
+ var throttleJob: Job? = null
+ var latestParam: T
+ return { param: T ->
+ latestParam = param
+ if (throttleJob?.isCompleted != false) {
+ throttleJob = coroutineScope.launch {
+ block(latestParam)
+ delay(skipTimeInMs)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt
new file mode 100644
index 0000000000..eeaf0afa85
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt
@@ -0,0 +1,98 @@
+/* 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.support.ktx.kotlinx.coroutines.flow
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapConcat
+
+/**
+ * Returns a [Flow] containing only changed elements of the lists of the original [Flow].
+ *
+ * ```
+ * Example: Identity function
+ * Transform: x -> x (transformed values are the same as original)
+ * Original Flow: list(0), list(0, 1), list(0, 1, 2, 3), list(4), list(5, 6, 7, 8)
+ * Transformed:
+ * (0) -> (0 emitted because it is a new value)
+ *
+ * (0, 1) -> (0 not emitted because same as previous value,
+ * 1 emitted because it is a new value),
+ *
+ * (0, 1, 2, 3) -> (0 and 1 not emitted because same as previous values,
+ * 2 and 3 emitted because they are new values),
+ *
+ * (4) -> (4 emitted because because it is a new value)
+ *
+ * (5, 6, 7, 8) -> (5, 6, 7, 8 emitted because they are all new values)
+ * Returned Flow: 0, 1, 2, 3, 4, 5, 6, 7, 8
+ * ---
+ *
+ * Example: Modulo 2
+ * Transform: x -> x % 2 (emit changed values if the result of modulo 2 changed)
+ * Original Flow: listOf(1), listOf(1, 2), listOf(3, 4, 5), listOf(3, 4)
+ * Transformed:
+ * (1) -> (1 emitted because it is a new value)
+ *
+ * (1, 0) -> (1 not emitted because same as previous value with the same transformed value,
+ * 2 emitted because it is a new value),
+ *
+ * (1, 0, 1) -> (3, 4, 5 emitted because they are all new values)
+ *
+ * (1, 0) -> (3, 4 not emitted because same as previous values with same transformed values)
+ *
+ * Returned Flow: 1, 2, 3, 4, 5
+ * ---
+ * ```
+ */
+fun <T, R> Flow<List<T>>.filterChanged(transform: (T) -> R): Flow<T> {
+ var lastMappedValues: Map<T, R>? = null
+ return flatMapConcat { values ->
+ val lastMapped = lastMappedValues
+ val changed = if (lastMapped == null) {
+ values
+ } else {
+ values.filter {
+ !lastMapped.containsKey(it) || lastMapped[it] != transform(it)
+ }
+ }
+ lastMappedValues = values.associateWith { transform(it) }
+ changed.asFlow()
+ }
+}
+
+/**
+ * Returns a [Flow] containing only values of the original [Flow] where the result array
+ * of calling [transform] contains at least one different value.
+ *
+ * Example:
+ * ```
+ * Block: x -> [x[0], x[1]] // Map to first two characters of input
+ * Original Flow: "banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home"
+ * Mapped: [b, a], [b, a], [b, u], [a, p], [b, i], [c, o], [c, i], [h, o]
+ * Returned Flow: "banana", "bus, "apple", "big", "coconut", "circle", "home"
+ * ``
+ */
+fun <T, R> Flow<T>.ifAnyChanged(transform: (T) -> Array<R>): Flow<T> {
+ var observedValueOnce = false
+ var lastMappedValues: Array<R>? = null
+
+ return filter { value ->
+ val mapped = transform(value)
+ val hasChanges = lastMappedValues
+ ?.asSequence()
+ ?.filterIndexed { i, r -> mapped[i] != r }
+ ?.any()
+
+ if (!observedValueOnce || hasChanges == true) {
+ lastMappedValues = mapped
+ observedValueOnce = true
+ true
+ } else {
+ false
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt
new file mode 100644
index 0000000000..fb7609db5f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt
@@ -0,0 +1,104 @@
+/* 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.support.ktx.util
+
+import android.util.AtomicFile
+import android.util.JsonReader
+import android.util.JsonWriter
+import org.json.JSONException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStreamWriter
+
+/**
+ * Reads an [AtomicFile] and provides a deserialized version of its content.
+ * @param block A function to be executed after the file is read and provides the content as
+ * a [String]. It is expected that this function returns a deserialized version of the content
+ * of the file.
+ */
+inline fun <T> AtomicFile.readAndDeserialize(block: (String) -> T): T? {
+ return try {
+ openRead().use {
+ val text = it.bufferedReader().use { reader -> reader.readText() }
+ block(text)
+ }
+ } catch (_: IOException) {
+ null
+ } catch (_: JSONException) {
+ null
+ }
+}
+
+/**
+ * Writes an [AtomicFile] and indicates if the file was wrote.
+ * @param block A function with provides the content of the file as a [String]
+ * @return true if the file wrote otherwise false
+ */
+inline fun AtomicFile.writeString(block: () -> String): Boolean {
+ return stream { writer ->
+ writer.write(block())
+ }
+}
+
+/**
+ * Opens the [AtomicFile] for writing and provides a [JsonWriter] to [block] for writing JSON
+ * directly to the file.
+ *
+ * At the end of [block] the writer will be flushed and the file closed.
+ */
+inline fun AtomicFile.streamJSON(block: JsonWriter.() -> Unit): Boolean {
+ return stream { writer ->
+ val jsonWriter = JsonWriter(writer)
+ block(jsonWriter)
+ jsonWriter.flush()
+ }
+}
+
+/**
+ * Opens the [AtomicFile] for reading and provides a [JsonReader] to [block] for reading JSON from
+ * the file.
+ */
+inline fun <R> AtomicFile.readJSON(block: JsonReader.() -> R): R? {
+ var reader: InputStream? = null
+
+ return try {
+ reader = openRead()
+
+ val jsonReader = JsonReader(reader.bufferedReader())
+ block(jsonReader)
+ } catch (e: IOException) {
+ null
+ } finally {
+ reader?.close()
+ }
+}
+
+/**
+ * Opens the [AtomicFile] for writing and provides an [OutputStreamWriter] to [block] for writing
+ * directly to the file.
+ *
+ * At the end of [block] the writer will be flushed and the file closed.
+ */
+inline fun AtomicFile.stream(block: (OutputStreamWriter) -> Unit): Boolean {
+ var outputStream: FileOutputStream? = null
+ return try {
+ outputStream = startWrite()
+
+ outputStream.buffered().writer().apply {
+ block(this)
+ flush()
+ }
+
+ finishWrite(outputStream)
+ true
+ } catch (_: IOException) {
+ failWrite(outputStream)
+ false
+ } catch (_: JSONException) {
+ failWrite(outputStream)
+ false
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..f1dc322160
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ይደውሉ በ…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ኢሜይል በ…</string>
+ <string name="mozac_support_ktx_menu_share_with">ያጋሩ ከ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">በ በኩል አጋራ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..b8d3e21fae
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Gritar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Ninviar un correu con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..329232b470
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">اتصل به عبر…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">أبرِد عبر…</string>
+ <string name="mozac_support_ktx_menu_share_with">شارِك مع…</string>
+ <string name="mozac_support_ktx_share_dialog_title">شارِك عبر</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..d99964cf72
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Unviar un corréu electrónicu con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..164b24b301
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Bununla zəng et…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Bununla e-poçt göndər…</string>
+ <string name="mozac_support_ktx_menu_share_with">Paylaş…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..af07dcca5b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">… -دان تماس توت</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">… ایله ایمیل یوللا</string>
+ <string name="mozac_support_ktx_menu_share_with">… ایله پایلاش</string>
+ <string name="mozac_support_ktx_share_dialog_title">پایلاش</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..5702b6d03f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Panggil sareng…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Rerepél sareng…</string>
+ <string name="mozac_support_ktx_menu_share_with">Wagiang sareng…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Wagiang anggen</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..6a4bb46bb3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Тэлефанаваць праз…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Адправіць электроннай поштай праз…</string>
+ <string name="mozac_support_ktx_menu_share_with">Падзяліцца з…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Падзяліцца праз</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..aba8b7418d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Позвъняване чрез…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Имейл чрез…</string>
+ <string name="mozac_support_ktx_menu_share_with">Споделяне с …</string>
+ <string name="mozac_support_ktx_share_dialog_title">Споделяне чрез</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..e438eeb8a1
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">যার মাধ্যমে কল করবেন…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">যার মাধ্যমে ইমেইল করবেন…</string>
+ <string name="mozac_support_ktx_menu_share_with">যার মাধ্যমে শেয়ার করবেন…</string>
+ <string name="mozac_support_ktx_share_dialog_title">শেয়ারের মাধ্যম</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..ae6e869874
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Gervel gant…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Kas ur postel gant…</string>
+ <string name="mozac_support_ktx_menu_share_with">Rannañ gant…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Rannañ dre</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..2f60e05edd
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Nazovite sa…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Pošaljite email sa…</string>
+ <string name="mozac_support_ktx_menu_share_with">Podijeli sa…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Podijeli putem</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..3a4689607f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Truca amb…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Envia un correu electrònic amb…</string>
+ <string name="mozac_support_ktx_menu_share_with">Comparteix amb…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Comparteix mitjançant</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..4085d2da6e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Katoyon rik\'in…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Titaq rik\'in…</string>
+ <string name="mozac_support_ktx_menu_share_with">Tikomonïx rik\'in…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Tikomonïx rik\'in</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..06103d7468
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Tawag sa</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">i-Email sa</string>
+ <string name="mozac_support_ktx_menu_share_with">i-Share sa</string>
+ <string name="mozac_support_ktx_share_dialog_title">i-Share agi sa</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..1dffae627e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">پەیوەندی بکە بەهۆی…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ئیمێڵ بکە بەهۆی…</string>
+ <string name="mozac_support_ktx_menu_share_with">بڵاوەپێکردن لەگەڵ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">بڵاوکردنەوە لە ڕێگەی</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..09f88611fe
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chjamà cù…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Mandà un messaghju cù…</string>
+ <string name="mozac_support_ktx_menu_share_with">Sparte cù…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Sparte via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..7af3f9ecf4
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Volat pomocí…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Poslat e-mail pomocí…</string>
+ <string name="mozac_support_ktx_menu_share_with">Sdílet pomocí…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Sdílet pomocí</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..434de6b1d5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Galw gyda…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">E-bostio gyda…</string>
+ <string name="mozac_support_ktx_menu_share_with">Rhannu gyda…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Rhannu drwy</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..4521e128e9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ring med…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Send mail med…</string>
+ <string name="mozac_support_ktx_menu_share_with">Del med…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Del via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..5a33b1ebf3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Anrufen mit…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Per E-Mail versenden mit…</string>
+ <string name="mozac_support_ktx_menu_share_with">Teilen mit…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Teilen über</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..b24d4b53ac
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Wołaś z …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Mejlki słaś z …</string>
+ <string name="mozac_support_ktx_menu_share_with">Źěliś z…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Źěliś pśez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..c1348e15a0
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Κλήση με…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Email με…</string>
+ <string name="mozac_support_ktx_menu_share_with">Κοινή χρήση με…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Κοινή χρήση μέσω</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..e22fa27c94
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Call with…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Email with…</string>
+ <string name="mozac_support_ktx_menu_share_with">Share with…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Share via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..e22fa27c94
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Call with…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Email with…</string>
+ <string name="mozac_support_ktx_menu_share_with">Share with…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Share via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..cb1ad4d987
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Voki per…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Sendi retpoŝton per…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dividi kun…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dividi per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..eec2afd893
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar por correo electrónico…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir vía</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..e0b2c75b94
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir mediante</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..0abf4d574f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir a través de</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..7b8ad93650
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir vía</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..0abf4d574f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir a través de</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..f795eeda04
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Helista äpiga…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Saada e-kiri äpiga…</string>
+ <string name="mozac_support_ktx_menu_share_with">Jaga…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Jagamine kasutades</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..7bf9f4b2b2
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Deitu honekin…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Bidali mezu elektronikoa honekin…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partekatu honekin…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partekatu honen bidez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..2923d03c60
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">تماس با…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">رایانامه کردن با…</string>
+ <string name="mozac_support_ktx_menu_share_with">هم‌رسانی با…</string>
+ <string name="mozac_support_ktx_share_dialog_title">همرسانی از طریق</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..d5ee6e1699
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">Lollin e…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Lollin rewrude e</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..571ff1bcd7
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Soita…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Sähköposti…</string>
+ <string name="mozac_support_ktx_menu_share_with">Jaa…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Jaa</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..5396ace76c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Appeler avec…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Envoyer un e-mail avec…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partager avec…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partager à l’aide de</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..4e4364b228
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Clame cun…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Mande e-mail cun…</string>
+ <string name="mozac_support_ktx_menu_share_with">Condivît cun…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Condivît vie</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..67936f7e19
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Belje mei…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">E-maile mei…</string>
+ <string name="mozac_support_ktx_menu_share_with">Diele mei…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Diele fia</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..e215f3efce
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">Comhroinn le…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Comhroinn trí</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..bc0227a440
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Cuir fòn le…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Cuir air a‘ phost-d le…</string>
+ <string name="mozac_support_ktx_menu_share_with">Co-roinn le…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Co-roinn slighe</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..6899c78f10
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar por correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir mediante</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..f3dd2ad99b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ehenói… ndive</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Emondo ñanduti veve… ndive</string>
+ <string name="mozac_support_ktx_menu_share_with">Emoherakuã…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Emoherakuã amóva rupi</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..50fec04dc8
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">… સાથે કૉલ કરો</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">… સાથે ઇમેલ કરો</string>
+ <string name="mozac_support_ktx_menu_share_with">સાથે શેર કરો…</string>
+ <string name="mozac_support_ktx_share_dialog_title">દ્વારા શેર કરો</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..6c5ef983bd
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">इसके साथ कॉल करें…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">इसके साथ ईमेल भेजें…</string>
+ <string name="mozac_support_ktx_menu_share_with">के साथ साझा करें…</string>
+ <string name="mozac_support_ktx_share_dialog_title">इससे साझा करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..03edef01db
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Nazovi s…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Pošalji e-poštu s…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dijeli s …</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dijeli putem</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..99e16cb09f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Wołać z …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Mejlki słać z …</string>
+ <string name="mozac_support_ktx_menu_share_with">Dźělić z…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dźělić přez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..a738cdeee9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Hívás ezzel…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">E-mail ezzel…</string>
+ <string name="mozac_support_ktx_menu_share_with">Megosztás…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Megosztás ezzel</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..7c11d7060f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Զանգահարել՝</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Էլ. նամակ ուղարկել՝</string>
+ <string name="mozac_support_ktx_menu_share_with">Տարածել հետևյալով…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Տարածել միջոցով</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..bf9d4a2856
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Appellar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Inviar email con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..6e0376c98e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Panggil dengan…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Surelkan dengan…</string>
+ <string name="mozac_support_ktx_menu_share_with">Bagikan dengan…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Bagikan lewat</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..8006dd8621
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Hringja með…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Senda tölvupóst með…</string>
+ <string name="mozac_support_ktx_menu_share_with">Deila með…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Deila með</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..22d4fd0735
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chiama con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Invia email con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Condividi con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Condividi con</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..f551cf77cf
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">חיוג באמצעות…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">שליחה בדוא״ל באמצעות…</string>
+ <string name="mozac_support_ktx_menu_share_with">שיתוף עם…</string>
+ <string name="mozac_support_ktx_share_dialog_title">שיתוף באמצעות</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..13c7a83ef0
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">電話をかける…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">メール送信…</string>
+ <string name="mozac_support_ktx_menu_share_with">共有先…</string>
+ <string name="mozac_support_ktx_share_dialog_title">共有方法</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..2abbdb068d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">დარეკვა…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ელფოსტის გაგზავნა…</string>
+ <string name="mozac_support_ktx_menu_share_with">გაზიარება…</string>
+ <string name="mozac_support_ktx_share_dialog_title">გაზიარება პროგრამით</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..7bc33f728b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">…arqalı qońıraw etiw</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Elektron pochta járdeminde…</string>
+ <string name="mozac_support_ktx_menu_share_with">…járdeminde bólisiw</string>
+ <string name="mozac_support_ktx_share_dialog_title">Arqalı bólisiw</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..e64f651208
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Siwel s…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Azen imayl s…</string>
+ <string name="mozac_support_ktx_menu_share_with">Bḍu d…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Bḍu s</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..49590be901
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Көмегімен қоңырау шалу…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Көмегімен эл. пошта хатын жіберу…</string>
+ <string name="mozac_support_ktx_menu_share_with">Көмегімен бөлісу…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Арқылы бөлісу</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..c2ac17ebfc
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Bigere bi…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Emaîl bi…</string>
+ <string name="mozac_support_ktx_menu_share_with">Parve bike bi…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Parve bike bi rêya</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..cdecfceeef
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ಇದರೊಂದಿಗೆ ಕರೆ ಮಾಡಿ…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ಇದರೊಂದಿಗೆ ಇಮೇಲ್ ಮಾಡಿ…</string>
+ <string name="mozac_support_ktx_menu_share_with">ಜೊತೆ ಹಂಚಿಕೊ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ಇವುಗಳ ಮೂಲಕ ಹಂಚು</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..e942f9e254
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">통화 앱…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">이메일 앱…</string>
+ <string name="mozac_support_ktx_menu_share_with">공유…</string>
+ <string name="mozac_support_ktx_share_dialog_title">다음으로 공유</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..b516969167
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">Condividdi con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Condividdi via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..e719b84028
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ໂທດ້ວຍ…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ອີເມລດ້ວຍ…</string>
+ <string name="mozac_support_ktx_menu_share_with">ແບ່ງປັນກັບ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ແບ່ງປັນຜ່ານທາງ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..e8cf2fc4f5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Skambinti naudojant…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Siųsti el. laišką naudojant…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dalintis su…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dalintis per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..8d8d7316e9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">ഇവരുമായി പങ്കിടുക…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ഇതുവഴി പങ്കിടൂ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..ebb1cc7071
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">यासह कॉल करा…</string>
+ <string name="mozac_support_ktx_menu_share_with">यासह शेअर करा…</string>
+ <string name="mozac_support_ktx_share_dialog_title">याद्वारे शेअर करा</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..8faa96afa1
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ခေါ်ဆိုခြင်း ဖြင့်…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">အီးမေး ဖြင့်…</string>
+ <string name="mozac_support_ktx_menu_share_with">… နှင့်မျှဝေပါ</string>
+ <string name="mozac_support_ktx_share_dialog_title">အခြားကနေ မျှဝေ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..a47a064ec3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ring med…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Send e-post med…</string>
+ <string name="mozac_support_ktx_menu_share_with">Del med…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Del via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..423ed59fd2
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">यससँग कल गर्नुहोस्…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">यससँग इमेल गर्नुहोस्…</string>
+ <string name="mozac_support_ktx_menu_share_with">यससँग साझेदारी गर्नुहोस्…</string>
+ <string name="mozac_support_ktx_share_dialog_title">मार्फत साझेदार गर्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..005f02df51
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Bellen met…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">E-mailen met…</string>
+ <string name="mozac_support_ktx_menu_share_with">Delen met…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Delen via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..a47a064ec3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ring med…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Send e-post med…</string>
+ <string name="mozac_support_ktx_menu_share_with">Del med…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Del via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..11b8ea1372
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Sonar amb…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar un corrièl amb…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partejar amb…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partejar via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..e13aee47b5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">…ਨਾਲ ਕਾਲ ਕਰੋ</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">…ਨਾਲ ਈਮੇਲ ਭੇਜੋ</string>
+ <string name="mozac_support_ktx_menu_share_with">…ਨਾਲ ਸਾਂਝਾ ਕਰੋ</string>
+ <string name="mozac_support_ktx_share_dialog_title">ਇਸ ਰਾਹੀਂ ਸਾਂਝਾ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..8ed66cb6de
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">کیہتھوں کال کرو…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">کیہتھوں ای‌میل بھیجو…</string>
+ <string name="mozac_support_ktx_menu_share_with">کیہنوں سانجھا کرو…</string>
+ <string name="mozac_support_ktx_share_dialog_title">کیہتھوں سانجھا کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..ec1f77ca41
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Zadzwoń za pomocą…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Wyślij e-mail za pomocą…</string>
+ <string name="mozac_support_ktx_menu_share_with">Udostępnij za pomocą…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Udostępnij przez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..3027936bdc
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chamar com…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar email com…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartilhar com…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartilhar via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..395aa28f03
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chamar com …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar e-mail com…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partilhar com…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partilhar via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..529df8d789
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Telefonar cun…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Trametter l\'e-mail cun…</string>
+ <string name="mozac_support_ktx_menu_share_with">Cundivider cun…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Cundivider via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..58b1a2121d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Sună cu…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Trimite mesaj pe e-mail cu…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partajează cu…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partajează prin</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..bd99e3a695
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Позвонить через…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Отправить по почте через…</string>
+ <string name="mozac_support_ktx_menu_share_with">Поделиться с помощью…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Поделиться через</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..6caae0a0b5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ᱱᱚᱶᱟ ᱛᱮ ᱠᱚᱞ ᱢᱮ …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ᱱᱚᱶᱟ ᱛᱮ ᱤᱢᱮᱞ ᱢᱮ …</string>
+ <string name="mozac_support_ktx_menu_share_with">ᱱᱚᱶᱟ ᱛᱮ ᱦᱟᱹᱴᱤᱧ ᱢᱮ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ᱫᱟᱨᱟᱭ ᱛᱮ ᱦᱟᱹᱴᱤᱧ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..57751f5529
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Muti cun…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Imbia cun posta eletrònica…</string>
+ <string name="mozac_support_ktx_menu_share_with">Cumpartzi cun…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Cumpartzi cun</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..639bdcbbb2
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">සමඟ අමතන්න…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">මගින් තැපෑලට…</string>
+ <string name="mozac_support_ktx_menu_share_with">සමඟ බෙදාගන්න…</string>
+ <string name="mozac_support_ktx_share_dialog_title">හරහා බෙදාගන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..393a11ab4a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Zavolať pomocou…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Poslať e‑mail pomocou…</string>
+ <string name="mozac_support_ktx_menu_share_with">Zdieľať pomocou…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Zdieľať cez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..e855302218
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">این٘دے نال فون کرو۔۔۔</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">این٘دے نال ای میل کرو۔۔۔</string>
+ <string name="mozac_support_ktx_menu_share_with">این٘دے نال شیئر کرو۔۔۔</string>
+ <string name="mozac_support_ktx_share_dialog_title">شیئر بذریعہ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..827a16fc3c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Pokliči z …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Pošlji po e-pošti z …</string>
+ <string name="mozac_support_ktx_menu_share_with">Deli z …</string>
+ <string name="mozac_support_ktx_share_dialog_title">Deli preko</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..1da3e21d02
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Thirre me…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Dërgoni Email me…</string>
+ <string name="mozac_support_ktx_menu_share_with">Ndajeni me të tjerët përmes…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Ndajeni përmes</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..e009ea8e49
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Позови са…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Пошаљи мејл са…</string>
+ <string name="mozac_support_ktx_menu_share_with">Дели са…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Дели преко</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..c46f025160
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Gero maké…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Surélékan maké…</string>
+ <string name="mozac_support_ktx_menu_share_with">Bagikeun sareng…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Bagikeun kana</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..109c410ba7
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ring med…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Skicka e-post med…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dela med…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dela via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..345e1f965a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">உடன் அழைக்கவும்…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">இதனுடன் மின்னஞ்சல்…</string>
+ <string name="mozac_support_ktx_menu_share_with">இதனுடன் பகிர்…</string>
+ <string name="mozac_support_ktx_share_dialog_title">இதன்வழியாக பகிரவும்</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..20d323b226
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">దీనితో కాల్ చేయి…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">దీనితో ఈమెయిలు చేయి…</string>
+ <string name="mozac_support_ktx_menu_share_with">దీనితో పంచుకో…</string>
+ <string name="mozac_support_ktx_share_dialog_title">దీని ద్వారా పంచుకోండి</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..81fcf1c654
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Занг задан тавассути…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Ирсоли паёми эл. тавассути…</string>
+ <string name="mozac_support_ktx_menu_share_with">Мубодила кардан тавассути…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Мубодила тавассути</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..6bda999442
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">โทรด้วย…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">อีเมลด้วย…</string>
+ <string name="mozac_support_ktx_menu_share_with">แบ่งปันด้วย…</string>
+ <string name="mozac_support_ktx_share_dialog_title">แบ่งปันผ่าน</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..ac8f553b1b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Tumawag gamit ang…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">i-Email kasama…</string>
+ <string name="mozac_support_ktx_menu_share_with">Ibahagi sa…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Ibahagi sa pamamagitan ng</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..fe7f209fc5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Bununla çağrı…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Bununla e-posta…</string>
+ <string name="mozac_support_ktx_menu_share_with">Paylaş…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..8a1414fe57
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ga\'mīn ngà…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Gā\'nïnj gān\'ānj korrêo ngà…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dūyingô\' ngà…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dūyingô\' riña</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..92ab1ac020
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">… ярдәмендә шалтырату</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">… ярдәмендә эл. почта җибәрү</string>
+ <string name="mozac_support_ktx_menu_share_with">Уртаклашу…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Аша уртаклашу</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..63e9286a19
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ɣer s…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Azen imayl s…</string>
+ <string name="mozac_support_ktx_menu_share_with">Bḍu d…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Bḍu s</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..7b79f14b4a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">تېلېفون قىلىش…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ئېلخەت يوللاش…</string>
+ <string name="mozac_support_ktx_menu_share_with">ھەمبەھىرلەش…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ھەمبەھىرلەش</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..528ab00907
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Зателефонувати через…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Надіслати електронним листом через…</string>
+ <string name="mozac_support_ktx_menu_share_with">Поділитися з…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Поділитись через</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..027be3feed
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">اس کے ذریعہ کال کریں…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">اس کے ذریعہ ای میل بھیجیں…</string>
+ <string name="mozac_support_ktx_menu_share_with">… کے ساتھ شیئر کریں</string>
+ <string name="mozac_support_ktx_share_dialog_title">کے زریعے شیئر کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..85303338d4
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Qoʻngʻiroq qilish</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Xat yozish</string>
+ <string name="mozac_support_ktx_menu_share_with">Ulashish</string>
+ <string name="mozac_support_ktx_share_dialog_title">Ulashish:</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..908be2008a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">Condividi co…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Condividi con</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..94efea291f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Gọi với…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Gửi email với…</string>
+ <string name="mozac_support_ktx_menu_share_with">Chia sẻ với…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Chia sẻ qua</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..3fd0eac24b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Pè pẹ̀lú…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Ímeelì pẹ̀lú…</string>
+ <string name="mozac_support_ktx_menu_share_with">Pín-in pẹ̀lú…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Pín-in nípasẹ̀</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..68e8298dca
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">使用下列程序拨打电话…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">使用下列程序发送邮件…</string>
+ <string name="mozac_support_ktx_menu_share_with">使用下列方式分享…</string>
+ <string name="mozac_support_ktx_share_dialog_title">分享到</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..8e97f07a1c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">使用下列程式撥打電話…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">使用下列程式發送郵件…</string>
+ <string name="mozac_support_ktx_menu_share_with">使用下列方式分享…</string>
+ <string name="mozac_support_ktx_share_dialog_title">分享到</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..d20d60e1f4
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?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 xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Call with…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Email with…</string>
+ <string name="mozac_support_ktx_menu_share_with">Share with…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Share via</string>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt
new file mode 100644
index 0000000000..205afd049b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt
@@ -0,0 +1,26 @@
+/* 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.support.ktx.android.arch.lifecycle
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class LifecycleTest {
+
+ @Test
+ fun addObservers() {
+ val observer1: LifecycleObserver = mock()
+ val observer2: LifecycleObserver = mock()
+ val lifecycle: Lifecycle = mock()
+
+ lifecycle.addObservers(observer1, observer2)
+
+ verify(lifecycle).addObserver(observer1)
+ verify(lifecycle).addObserver(observer2)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt
new file mode 100644
index 0000000000..7288467a3e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt
@@ -0,0 +1,55 @@
+/* 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.support.ktx.android.content
+
+import android.content.Context
+import android.view.accessibility.AccessibilityManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.shadows.ShadowAccessibilityManager
+
+@RunWith(AndroidJUnit4::class)
+class ContextKtTest {
+
+ lateinit var accessibilityManager: ShadowAccessibilityManager
+
+ @Before
+ fun setUp() {
+ accessibilityManager = shadowOf(
+ testContext
+ .getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager,
+ )
+ }
+
+ @Test
+ fun `screen reader enabled`() {
+ // Given
+ accessibilityManager.setTouchExplorationEnabled(true)
+
+ // When
+ val isEnabled = testContext.isScreenReaderEnabled
+
+ // Then
+ assertTrue(isEnabled)
+ }
+
+ @Test
+ fun `screen reader disabled`() {
+ // Given
+ accessibilityManager.setTouchExplorationEnabled(false)
+
+ // When
+ val isEnabled = testContext.isScreenReaderEnabled
+
+ // Then
+ assertFalse(isEnabled)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt
new file mode 100644
index 0000000000..aa6782f4f5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt
@@ -0,0 +1,304 @@
+/* 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.support.ktx.android.content
+
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.app.Activity
+import android.app.ActivityManager
+import android.content.ActivityNotFoundException
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.EXTRA_INTENT
+import android.content.Intent.EXTRA_STREAM
+import android.content.Intent.EXTRA_SUBJECT
+import android.content.Intent.EXTRA_TEXT
+import android.content.Intent.EXTRA_TITLE
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.hardware.camera2.CameraManager
+import android.net.Uri
+import android.os.Build
+import androidx.core.content.FileProvider
+import androidx.core.content.getSystemService
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.R
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.fakes.android.FakeContext
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+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.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.Implementation
+import org.robolectric.annotation.Implements
+import org.robolectric.shadows.ShadowApplication
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.ShadowProcess
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class ContextTest {
+
+ @Before
+ fun setup() {
+ isMainProcess = null
+ }
+
+ @Test
+ fun `isOSOnLowMemory() should return the same as getMemoryInfo() lowMemory`() {
+ val extensionFunctionResult = testContext.isOSOnLowMemory()
+
+ val activityManager: ActivityManager? = testContext.getSystemService()
+
+ val normalMethodResult = ActivityManager.MemoryInfo().also { memoryInfo ->
+ activityManager?.getMemoryInfo(memoryInfo)
+ }.lowMemory
+
+ assertEquals(extensionFunctionResult, normalMethodResult)
+ }
+
+ @Test
+ fun `isPermissionGranted() returns same service as checkSelfPermission()`() {
+ val application = ShadowApplication()
+
+ assertEquals(
+ testContext.isPermissionGranted(WRITE_EXTERNAL_STORAGE),
+ testContext.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED,
+ )
+
+ application.grantPermissions(WRITE_EXTERNAL_STORAGE)
+
+ assertEquals(
+ testContext.isPermissionGranted(WRITE_EXTERNAL_STORAGE),
+ testContext.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED,
+ )
+ }
+
+ @Test
+ fun `share invokes startActivity`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.share("https://mozilla.org")
+
+ verify(context).startActivity(argCaptor.capture())
+
+ assertTrue(result)
+ assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags)
+ }
+
+ @Test
+ @Config(shadows = [ShadowFileProvider::class])
+ fun `shareMedia invokes startActivity`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.shareMedia("filePath", "*/*", "subject", "message")
+
+ verify(context).startActivity(argCaptor.capture())
+ assertTrue(result)
+ // verify all the properties we set for the share Intent
+ val chooserIntent = argCaptor.value
+ val chooserTitle: String = chooserIntent.extras!!.getString(EXTRA_TITLE) as String
+
+ @Suppress("DEPRECATION")
+ val shareIntent: Intent = chooserIntent.extras!!.get(EXTRA_INTENT) as Intent
+
+ assertTrue(chooserIntent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0)
+ assertTrue(chooserIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0)
+ assertEquals(context.getString(R.string.mozac_support_ktx_menu_share_with), chooserTitle)
+ assertEquals(ACTION_SEND, shareIntent.action)
+
+ @Suppress("DEPRECATION")
+ assertEquals(ShadowFileProvider.FAKE_URI_RESULT, shareIntent.extras!![EXTRA_STREAM])
+ assertEquals("subject", shareIntent.extras!!.getString(EXTRA_SUBJECT))
+ assertEquals("message", shareIntent.extras!!.getString(EXTRA_TEXT))
+ assertTrue(shareIntent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0)
+ assertTrue(shareIntent.flags and Intent.FLAG_ACTIVITY_NEW_DOCUMENT != 0)
+ }
+
+ @Suppress("UNREACHABLE_CODE")
+ @Test
+ @Config(shadows = [ShadowFileProvider::class])
+ fun `shareMedia returns false if the chooser could not be shown`() {
+ val context = spy(
+ object : FakeContext() {
+ override fun startActivity(intent: Intent?) = throw ActivityNotFoundException()
+ override fun getApplicationContext() = testContext
+ },
+ )
+ doReturn(testContext.resources).`when`(context).resources
+
+ val result = context.shareMedia("filePath", "*/*", "subject", "message")
+
+ assertFalse(result)
+ }
+
+ @Test
+ @Config(shadows = [ShadowFileProvider::class], sdk = [Build.VERSION_CODES.Q])
+ fun `shareMedia will show a thumbnail starting with Android 10`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.shareMedia("filePath", "*/*", "subject", "message")
+
+ verify(context).startActivity(argCaptor.capture())
+ assertTrue(result)
+ // verify all the properties we set for the share Intent
+ val chooserIntent = argCaptor.value
+ assertEquals(1, chooserIntent.clipData!!.itemCount)
+ assertEquals(ShadowFileProvider.FAKE_URI_RESULT, chooserIntent.clipData!!.getItemAt(0).uri)
+ }
+
+ @Test
+ @Config(shadows = [ShadowFileProvider::class], sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P])
+ fun `shareMedia will not show a thumbnail prior to Android 10`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.shareMedia("filePath", "*/*", "subject", "message")
+
+ verify(context).startActivity(argCaptor.capture())
+ assertTrue(result)
+ // verify all the properties we set for the share Intent
+ val chooserIntent = argCaptor.value
+ assertNull(chooserIntent.clipData)
+ }
+
+ @Test
+ @Config(shadows = [ShadowFileProvider::class])
+ fun `copyImage will copy the file URI to the clipboard & invoke the confirmation action`() {
+ val context = spy(testContext)
+ val confirmationAction = mock<() -> Unit>()
+
+ context.copyImage("filePath", confirmationAction)
+
+ val clipboardManager =
+ testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ assertEquals(
+ ShadowFileProvider.FAKE_URI_RESULT,
+ clipboardManager.primaryClip!!.getItemAt(0).uri,
+ )
+ verify(confirmationAction).invoke()
+ }
+
+ @Test
+ fun `email invokes startActivity`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.email("test@mozilla.org")
+
+ verify(context).startActivity(argCaptor.capture())
+
+ assertTrue(result)
+ assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags)
+ }
+
+ @Test
+ fun `call invokes startActivity`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.call("555-5555")
+
+ verify(context).startActivity(argCaptor.capture())
+
+ assertTrue(result)
+ assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags)
+ }
+
+ @Test
+ fun `isMainProcess must only return true if we are in the main process`() {
+ val myPid = Int.MAX_VALUE
+
+ assertTrue(testContext.isMainProcess())
+
+ ShadowProcess.setPid(myPid)
+ isMainProcess = null
+
+ assertFalse(testContext.isMainProcess())
+ }
+
+ @Test
+ fun `runOnlyInMainProcess must only run if we are in the main process`() {
+ val myPid = Int.MAX_VALUE
+ var wasExecuted = false
+
+ testContext.runOnlyInMainProcess {
+ wasExecuted = true
+ }
+
+ assertTrue(wasExecuted)
+
+ wasExecuted = false
+ ShadowProcess.setPid(myPid)
+ isMainProcess = false
+
+ testContext.runOnlyInMainProcess {
+ wasExecuted = true
+ }
+
+ assertFalse(wasExecuted)
+ }
+
+ @Test
+ fun `hasCamera returns true if the device has a camera`() {
+ val context = Robolectric.buildActivity(Activity::class.java).get()
+ assertFalse(context.hasCamera())
+
+ val cameraManager: CameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ shadowOf(cameraManager).addCamera("camera0", ShadowCameraCharacteristics.newCameraCharacteristics())
+ assertTrue(context.hasCamera())
+ }
+
+ @Test
+ fun `hasCamera returns false if exception is thrown`() {
+ val context = spy(testContext)
+ val cameraManager: CameraManager = mock()
+ whenever(context.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+
+ whenever(cameraManager.cameraIdList).thenThrow(IllegalStateException("Test"))
+ assertFalse(context.hasCamera())
+ }
+
+ @Test
+ fun `hasCamera returns false if assertion is thrown`() {
+ val context = spy(testContext)
+ val cameraManager: CameraManager = mock()
+ whenever(context.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+
+ whenever(cameraManager.cameraIdList).thenThrow(AssertionError("Test"))
+ assertFalse(context.hasCamera())
+ }
+}
+
+@Implements(FileProvider::class)
+object ShadowFileProvider {
+ val FAKE_URI_RESULT: Uri = "fakeUri".toUri()
+
+ @Implementation
+ @JvmStatic
+ @Suppress("UNUSED_PARAMETER")
+ fun getUriForFile(
+ context: Context?,
+ authority: String?,
+ file: File,
+ ) = FAKE_URI_RESULT
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt
new file mode 100644
index 0000000000..20f32b799f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt
@@ -0,0 +1,115 @@
+/* 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.support.ktx.android.content
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SharedPreferencesStringTest {
+ private val key = "key"
+ private val defaultValue = "defaultString"
+ private lateinit var preferencesHolder: StringTestPreferenceHolder
+ private lateinit var testPreferences: SharedPreferences
+
+ @Before
+ fun setup() {
+ testPreferences = testContext.getSharedPreferences("test", Context.MODE_PRIVATE)
+ }
+
+ @After
+ fun tearDown() {
+ testPreferences.edit().clear().apply()
+ }
+
+ @Test
+ fun `GIVEN string does not exist and asked to persist the default WHEN asked for it THEN persist the default and return it`() {
+ preferencesHolder = StringTestPreferenceHolder(
+ persistDefaultIfNotExists = true,
+ )
+
+ val result = preferencesHolder.string
+
+ assertEquals(defaultValue, result)
+ assertEquals(defaultValue, testPreferences.getString(key, null))
+ }
+
+ @Test
+ fun `GIVEN string does not exist and not asked to persist the default WHEN asked for it THEN return the default but not persist it`() {
+ preferencesHolder = StringTestPreferenceHolder(
+ persistDefaultIfNotExists = false,
+ )
+
+ val result = preferencesHolder.string
+
+ assertEquals(defaultValue, result)
+ assertNull(testPreferences.getString(key, null))
+ }
+
+ @Test
+ fun `GIVEN string exists and asked to persist the default WHEN asked for it THEN return the existing string and don't persist the default`() {
+ testPreferences.edit().putString(key, "test").apply()
+ preferencesHolder = StringTestPreferenceHolder(
+ persistDefaultIfNotExists = true,
+ )
+
+ val result = preferencesHolder.string
+
+ assertEquals("test", result)
+ }
+
+ @Test
+ fun `GIVEN string exists and not asked to persist the default WHEN asked for it THEN return the existing string and don't persist the default`() {
+ testPreferences.edit().putString(key, "test").apply()
+ preferencesHolder = StringTestPreferenceHolder(
+ persistDefaultIfNotExists = true,
+ )
+
+ val result = preferencesHolder.string
+
+ assertEquals("test", result)
+ }
+
+ @Test
+ fun `GIVEN a value exists WHEN asked to persist a new value THEN update the persisted value`() {
+ testPreferences.edit().putString(key, "test").apply()
+ preferencesHolder = StringTestPreferenceHolder()
+
+ preferencesHolder.string = "update"
+
+ assertEquals(
+ "update",
+ testPreferences.getString(key, null),
+ )
+ }
+
+ @Test
+ fun `GIVEN a value does not exist WHEN asked to persist a new value THEN persist the requested value`() {
+ preferencesHolder = StringTestPreferenceHolder()
+
+ preferencesHolder.string = "test"
+
+ assertEquals(
+ "test",
+ testPreferences.getString(key, null),
+ )
+ }
+
+ private inner class StringTestPreferenceHolder(
+ persistDefaultIfNotExists: Boolean = false,
+ ) : PreferencesHolder {
+ override val preferences = testPreferences
+
+ var string by stringPreference(key, defaultValue, persistDefaultIfNotExists)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt
new file mode 100644
index 0000000000..7ab36893ae
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt
@@ -0,0 +1,229 @@
+/* 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.support.ktx.android.content
+
+import android.content.SharedPreferences
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.openMocks
+
+class SharedPreferencesTest {
+ @Mock private lateinit var sharedPrefs: SharedPreferences
+
+ @Mock private lateinit var editor: SharedPreferences.Editor
+
+ @Before
+ fun setup() {
+ openMocks(this)
+
+ `when`(sharedPrefs.edit()).thenReturn(editor)
+ `when`(editor.putBoolean(anyString(), anyBoolean())).thenReturn(editor)
+ `when`(editor.putFloat(anyString(), anyFloat())).thenReturn(editor)
+ `when`(editor.putInt(anyString(), anyInt())).thenReturn(editor)
+ `when`(editor.putLong(anyString(), anyLong())).thenReturn(editor)
+ `when`(editor.putString(anyString(), anyString())).thenReturn(editor)
+ `when`(editor.putStringSet(anyString(), any())).thenReturn(editor)
+ }
+
+ @Test
+ fun `getter returns boolean from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getBoolean(eq("boolean"), anyBoolean())).thenReturn(true)
+
+ assertTrue(holder.boolean)
+ verify(sharedPrefs).getBoolean("boolean", false)
+ }
+
+ @Test
+ fun `setter applies boolean to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultBoolean = true)
+ holder.boolean = false
+
+ verify(editor).putBoolean("boolean", false)
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default boolean value`() {
+ val holderFalse = MockPreferencesHolder(defaultBoolean = false)
+ // Call the getter for the test
+ holderFalse.boolean
+
+ verify(sharedPrefs).getBoolean("boolean", false)
+
+ val holderTrue = MockPreferencesHolder(defaultBoolean = true)
+ // Call the getter for the test
+ holderTrue.boolean
+
+ verify(sharedPrefs).getBoolean("boolean", true)
+ }
+
+ @Test
+ fun `getter returns float from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getFloat(eq("float"), anyFloat())).thenReturn(2.4f)
+
+ assertEquals(2.4f, holder.float)
+ verify(sharedPrefs).getFloat("float", 0f)
+ }
+
+ @Test
+ fun `setter applies float to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultFloat = 1f)
+ holder.float = 0f
+
+ verify(editor).putFloat("float", 0f)
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default float value`() {
+ val holderDefault = MockPreferencesHolder(defaultFloat = 0f)
+ // Call the getter for the test
+ holderDefault.float
+
+ verify(sharedPrefs).getFloat("float", 0f)
+
+ val holderOther = MockPreferencesHolder(defaultFloat = 12f)
+ // Call the getter for the test
+ holderOther.float
+
+ verify(sharedPrefs).getFloat("float", 12f)
+ }
+
+ @Test
+ fun `getter returns int from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getInt(eq("int"), anyInt())).thenReturn(5)
+
+ assertEquals(5, holder.int)
+ verify(sharedPrefs).getInt("int", 0)
+ }
+
+ @Test
+ fun `setter applies int to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultInt = 1)
+ holder.int = 0
+
+ verify(editor).putInt("int", 0)
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default int value`() {
+ val holderDefault = MockPreferencesHolder(defaultInt = 0)
+ // Call the getter for the test
+ holderDefault.int
+
+ verify(sharedPrefs).getInt("int", 0)
+
+ val holderOther = MockPreferencesHolder(defaultInt = 23)
+ // Call the getter for the test
+ holderOther.int
+
+ verify(sharedPrefs).getInt("int", 23)
+ }
+
+ @Test
+ fun `getter returns long from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getLong(eq("long"), anyLong())).thenReturn(200L)
+
+ assertEquals(200L, holder.long)
+ verify(sharedPrefs).getLong("long", 0)
+ }
+
+ @Test
+ fun `setter applies long to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultLong = 1)
+ holder.long = 0
+
+ verify(editor).putLong("long", 0)
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default long value`() {
+ val holderDefault = MockPreferencesHolder(defaultLong = 0)
+ // Call the getter for the test
+ holderDefault.long
+
+ verify(sharedPrefs).getLong("long", 0)
+
+ val holderOther = MockPreferencesHolder(defaultLong = 23)
+ // Call the getter for the test
+ holderOther.long
+
+ verify(sharedPrefs).getLong("long", 23)
+ }
+
+ @Test
+ fun `getter returns string set from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getStringSet(eq("string_set"), any())).thenReturn(setOf("foo"))
+
+ assertEquals(setOf("foo"), holder.stringSet)
+ verify(sharedPrefs).getStringSet("string_set", emptySet())
+ }
+
+ @Test
+ fun `setter applies string set to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultString = "foo")
+ holder.stringSet = setOf("bar")
+
+ verify(editor).putStringSet("string_set", setOf("bar"))
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default string set value`() {
+ val holderDefault = MockPreferencesHolder()
+ // Call the getter for the test
+ holderDefault.stringSet
+
+ verify(sharedPrefs).getStringSet("string_set", emptySet())
+
+ val holderOther = MockPreferencesHolder(defaultSet = setOf("hello", "world"))
+ // Call the getter for the test
+ holderOther.stringSet
+
+ verify(sharedPrefs).getStringSet("string_set", setOf("hello", "world"))
+ }
+
+ private inner class MockPreferencesHolder(
+ defaultBoolean: Boolean = false,
+ defaultFloat: Float = 0f,
+ defaultInt: Int = 0,
+ defaultLong: Long = 0L,
+ defaultString: String = "",
+ defaultSet: Set<String> = emptySet(),
+ ) : PreferencesHolder {
+ override val preferences = sharedPrefs
+
+ var boolean by booleanPreference("boolean", default = defaultBoolean)
+
+ var float by floatPreference("float", default = defaultFloat)
+
+ var int by intPreference("int", default = defaultInt)
+
+ var long by longPreference("long", default = defaultLong)
+
+ var string by stringPreference("string", default = defaultString)
+
+ var stringSet by stringSetPreference("string_set", default = defaultSet)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt
new file mode 100644
index 0000000000..4e15aa6dbc
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt
@@ -0,0 +1,49 @@
+/* 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.support.ktx.android.content.pm
+
+import android.content.Context
+import android.content.pm.PackageInfo
+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.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class PackageManagerTest {
+ private fun createContext(
+ installedApps: List<String> = emptyList(),
+ ): Context {
+ val pm = testContext.packageManager
+ val packageManager = shadowOf(pm)
+ val context = mock<Context>()
+ `when`(context.packageManager).thenReturn(pm)
+ installedApps.forEach { name ->
+ val packageInfo = PackageInfo().apply {
+ packageName = name
+ }
+ packageManager.addPackageNoDefaults(packageInfo)
+ }
+
+ return context
+ }
+
+ /**
+ * Verify that PackageManager.isPackageInstalled works correctly.
+ */
+ @Test
+ fun `isPackageInstalled() returns true when package is installed, false otherwise`() {
+ val context = createContext(listOf("com.example", "com.test"))
+
+ assert(context.packageManager.isPackageInstalled("com.example"))
+ assert(context.packageManager.isPackageInstalled("com.test"))
+ assertFalse(context.packageManager.isPackageInstalled("com.mozilla"))
+ assertFalse(context.packageManager.isPackageInstalled("com.example.com"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt
new file mode 100644
index 0000000000..dcb087ccae
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt
@@ -0,0 +1,78 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.Context
+import android.content.res.AssetManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import java.io.ByteArrayInputStream
+
+@RunWith(AndroidJUnit4::class)
+class AssetManagerTest {
+
+ /**
+ * Verify that AssetManager.readJSONObject() closes its stream.
+ */
+ @Test
+ fun readJSONObjectClosesStream() {
+ // Setup
+
+ val stream = Mockito.spy(ByteArrayInputStream("{}".toByteArray()))
+
+ val assetManager = mock<AssetManager>()
+ Mockito.`when`(assetManager.open(ArgumentMatchers.anyString())).thenReturn(stream)
+
+ val context = mock<Context>()
+ Mockito.`when`(context.assets).thenReturn(assetManager)
+
+ // Now use our mock classes to call readJSONObject()
+
+ context.assets.readJSONObject("test.txt")
+
+ // Verify that the stream was opened and closed
+
+ Mockito.verify(assetManager).open("test.txt")
+ Mockito.verify(stream).close()
+ }
+
+ /**
+ * The stream returned by the AssetManager will be read and converted into a JSONObject instance.
+ */
+ @Test
+ fun streamsIsTransformedIntoJSONObject() {
+ // Setup
+
+ val stream = Mockito.spy(ByteArrayInputStream("{'firstName': 'John', 'lastName': 'Smith'}".toByteArray()))
+
+ val assetManager = mock<AssetManager>()
+ Mockito.`when`(assetManager.open(ArgumentMatchers.anyString())).thenReturn(stream)
+
+ val context = mock<Context>()
+ Mockito.`when`(context.assets).thenReturn(assetManager)
+
+ // Now read the stream into an JSONObject
+
+ val data = context.assets.readJSONObject("test.txt")
+
+ // Assert that the JSONObject was constructed correctly.
+
+ Mockito.verify(assetManager).open("test.txt")
+
+ assertNotNull(data)
+ assertEquals(2, data.length())
+ assertTrue(data.has("firstName"))
+ assertTrue(data.has("lastName"))
+ assertEquals("John", data.getString("firstName"))
+ assertEquals("Smith", data.getString("lastName"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt
new file mode 100644
index 0000000000..c8be2da46c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt
@@ -0,0 +1,74 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Typeface.BOLD
+import android.graphics.Typeface.ITALIC
+import android.os.Build
+import android.os.LocaleList
+import android.text.Html
+import android.text.style.StyleSpan
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.robolectric.annotation.Config
+import java.util.Locale
+
+@RunWith(AndroidJUnit4::class)
+class ResourcesTest {
+
+ private lateinit var resources: Resources
+ private lateinit var configuration: Configuration
+
+ @Before
+ fun setup() {
+ resources = mock()
+ configuration = spy(Configuration())
+
+ whenever(resources.configuration).thenReturn(configuration)
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.N])
+ @Test
+ fun `locale returns first item in locales list`() {
+ whenever(configuration.locales).thenReturn(LocaleList(Locale.CANADA, Locale.ENGLISH))
+ assertEquals(Locale.CANADA, resources.locale)
+ }
+
+ @Suppress("Deprecation")
+ @Config(sdk = [Build.VERSION_CODES.M])
+ @Test
+ fun `locale returns locale from configuration`() {
+ configuration.locale = Locale.FRENCH
+ assertEquals(Locale.FRENCH, resources.locale)
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.N])
+ @Test
+ fun `getSpanned formats corresponding string`() {
+ val id = 100
+ whenever(configuration.locales).thenReturn(LocaleList(Locale.ROOT))
+ whenever(resources.getString(id)).thenReturn("Allow %1\$s to open %2\$s")
+
+ assertEquals(
+ "<p dir=\"ltr\">Allow <b>App</b> to open <i>Website</i></p>\n",
+ Html.toHtml(
+ resources.getSpanned(
+ id,
+ "App" to StyleSpan(BOLD),
+ "Website" to StyleSpan(ITALIC),
+ ),
+ Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE,
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt
new file mode 100644
index 0000000000..acd57c1194
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt
@@ -0,0 +1,78 @@
+/* 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.support.ktx.android.graphics
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BitmapKtTest {
+
+ private lateinit var subject: Bitmap
+
+ @Before
+ fun setUp() {
+ subject = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+ }
+
+ @Ignore("convert to integration test. Robolectric's shadows are incomplete and cause this to fail.")
+ @Test
+ fun `WHEN withRoundedCorners is called THEN returned bitmap's corners should be transparent and center with color`() {
+ val dimen = 200
+ val fillColor = Color.RED
+
+ val bitmap = Bitmap.createBitmap(dimen, dimen, Bitmap.Config.ARGB_8888).apply {
+ eraseColor(fillColor)
+ }
+ val roundedBitmap = bitmap.withRoundedCorners(40f)
+
+ fun assertCornersAreTransparent() {
+ val cornerLocations = listOf(0, dimen - 1)
+
+ cornerLocations.forEach { x ->
+ cornerLocations.forEach { y ->
+ assertEquals(Color.TRANSPARENT, roundedBitmap.getPixel(x, y))
+ }
+ }
+ }
+
+ fun assertCenterIsFilled() {
+ val center = dimen / 2
+ assertEquals(fillColor, roundedBitmap.getPixel(center, center))
+ }
+
+ assertNotSame(bitmap, roundedBitmap)
+ assertCornersAreTransparent()
+ assertCenterIsFilled()
+ }
+
+ @Test
+ fun `GIVEN an all red bitmap THEN pixels are all the same`() {
+ subject.eraseColor(Color.RED)
+ assertTrue(subject.arePixelsAllTheSame())
+ }
+
+ @Test
+ fun `GIVEN an all transparent bitmap THEN pixels are all the same`() {
+ subject.eraseColor(Color.TRANSPARENT)
+ assertTrue(subject.arePixelsAllTheSame())
+ }
+
+ @Test
+ fun `GIVEN an all red bitmap with one pixel not red THEN pixels are not all the same`() {
+ subject.eraseColor(Color.RED)
+ subject.setPixel(0, 1, Color.rgb(244, 0, 0))
+ assertFalse(subject.arePixelsAllTheSame())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt
new file mode 100644
index 0000000000..213e440e56
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt
@@ -0,0 +1,243 @@
+/* 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.support.ktx.android.net
+
+import android.content.ContentResolver
+import android.database.Cursor
+import android.webkit.MimeTypeMap
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doReturn
+import org.robolectric.Shadows
+
+@RunWith(AndroidJUnit4::class)
+class UriTest {
+
+ @Test
+ fun hostWithoutCommonPrefixes() {
+ assertEquals(
+ "mozilla.org",
+ "https://www.mozilla.org".toUri().hostWithoutCommonPrefixes,
+ )
+
+ assertEquals(
+ "twitter.com",
+ "https://mobile.twitter.com/home".toUri().hostWithoutCommonPrefixes,
+ )
+
+ assertNull("".toUri().hostWithoutCommonPrefixes)
+
+ assertEquals(
+ "",
+ "http://".toUri().hostWithoutCommonPrefixes,
+ )
+
+ assertEquals(
+ "facebook.com",
+ "https://m.facebook.com/".toUri().hostWithoutCommonPrefixes,
+ )
+
+ assertEquals(
+ "github.com",
+ "https://github.com/mozilla-mobile/android-components".toUri().hostWithoutCommonPrefixes,
+ )
+ }
+
+ @Test
+ fun testIsHttpOrHttps() {
+ // No value
+ assertFalse("".toUri().isHttpOrHttps)
+
+ // Garbage
+ assertFalse("lksdjflasuf".toUri().isHttpOrHttps)
+
+ // URLs with http/https
+ assertTrue("https://www.google.com".toUri().isHttpOrHttps)
+ assertTrue("http://www.facebook.com".toUri().isHttpOrHttps)
+ assertTrue("https://mozilla.org/en-US/firefox/products/".toUri().isHttpOrHttps)
+
+ // IP addresses
+ assertTrue("https://192.168.0.1".toUri().isHttpOrHttps)
+ assertTrue("http://63.245.215.20/en-US/firefox/products".toUri().isHttpOrHttps)
+
+ // Other protocols
+ assertFalse("ftp://people.mozilla.org".toUri().isHttpOrHttps)
+ assertFalse("javascript:window.google.com".toUri().isHttpOrHttps)
+ assertFalse("tel://1234567890".toUri().isHttpOrHttps)
+
+ // No scheme
+ assertFalse("google.com".toUri().isHttpOrHttps)
+ assertFalse("git@github.com:mozilla/gecko-dev.git".toUri().isHttpOrHttps)
+ }
+
+ @Test
+ fun testIsInScope() {
+ val url = "https://mozilla.github.io/my-app/".toUri()
+ val prefix = "https://mozilla.github.io/prefix-of/resource.html".toUri()
+ assertFalse(url.isInScope(emptyList()))
+ assertTrue(url.isInScope(listOf("https://mozilla.github.io/my-app/".toUri())))
+ assertFalse(url.isInScope(listOf("https://firefox.com/out-of-scope/".toUri())))
+ assertFalse(url.isInScope(listOf("https://mozilla.github.io/my-app-almost-in-scope".toUri())))
+ assertTrue(prefix.isInScope(listOf("https://mozilla.github.io/prefix".toUri())))
+ assertTrue(prefix.isInScope(listOf("https://mozilla.github.io/prefix-of/".toUri())))
+ }
+
+ @Test
+ fun testSameSchemeAndHostAs() {
+ // Host mismatch.
+ assertFalse("https://foo.bar".toUri().sameSchemeAndHostAs("https://foo.baz".toUri()))
+ // Scheme mismatch.
+ assertFalse("http://127.0.0.1".toUri().sameSchemeAndHostAs("https://127.0.0.1".toUri()))
+ // Port mismatch.
+ assertTrue("https://foo.bar:444".toUri().sameSchemeAndHostAs("https://foo.bar:555".toUri()))
+ // Port OK but scheme different.
+ assertFalse("https://foo.bar:443".toUri().sameSchemeAndHostAs("ftp://foo.bar:443".toUri()))
+
+ assertTrue("https://foo.bar/bobo".toUri().sameSchemeAndHostAs("https://foo.bar:443/obob".toUri()))
+ assertTrue("https://foo.bar:333".toUri().sameSchemeAndHostAs("https://foo.bar:443:333".toUri()))
+ }
+
+ @Test
+ fun testSameOriginAs() {
+ // Host mismatch.
+ assertFalse("https://foo.bar".toUri().sameOriginAs("https://foo.baz".toUri()))
+ // Scheme mismatch.
+ assertFalse("http://127.0.0.1".toUri().sameOriginAs("https://127.0.0.1".toUri()))
+ // Port mismatch.
+ assertFalse("https://foo.bar:444".toUri().sameOriginAs("https://foo.bar:555".toUri()))
+ // Port OK but scheme different.
+ assertFalse("https://foo.bar:443".toUri().sameOriginAs("ftp://foo.bar:443".toUri()))
+
+ assertTrue("https://foo.bar:443/bobo".toUri().sameOriginAs("https://foo.bar:443/obob".toUri()))
+ assertTrue("https://foo.bar:333".toUri().sameOriginAs("https://foo.bar:333".toUri()))
+ }
+
+ @Test
+ fun testGenerateFileName() {
+ val fileExtension = "txt"
+ var fileName = generateFileName(fileExtension)
+
+ assertTrue(fileName.contains(fileExtension))
+
+ fileName = generateFileName()
+
+ assertFalse(fileName.contains("."))
+ }
+
+ @Test
+ fun testGetFileExtension() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ assertEquals("txt", uri.getFileExtension(resolver))
+ }
+
+ @Test
+ fun `getFileNameForContentUris for urls with DISPLAY_NAME`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+ val cursor = mock<Cursor>()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any())
+ doReturn(1).`when`(cursor).getColumnIndex(any())
+ doReturn("myFile.txt").`when`(cursor).getString(anyInt())
+
+ assertEquals("myFile.txt", uri.getFileNameForContentUris(resolver))
+ }
+
+ @Test
+ fun `getFileNameForContentUris for urls without DISPLAY_NAME`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+ val cursor = mock<Cursor>()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any())
+ doReturn(-1).`when`(cursor).getColumnIndex(any())
+
+ val fileName = uri.getFileNameForContentUris(resolver)
+
+ assertTrue(fileName.contains(".txt"))
+ assertTrue(fileName.isNotEmpty())
+ }
+
+ @Test
+ fun `getFileNameForContentUris for urls with null DISPLAY_NAME`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+ val cursor = mock<Cursor>()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any())
+ doReturn(1).`when`(cursor).getColumnIndex(any())
+ doReturn(null).`when`(cursor).getString(anyInt())
+
+ val fileName = uri.getFileNameForContentUris(resolver)
+
+ assertTrue(fileName.contains(".txt"))
+ assertTrue(fileName.isNotEmpty())
+ }
+
+ @Test
+ fun `getFileName for file uri schemes`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "file:///home/user/myfile.html".toUri()
+
+ assertEquals("myfile.html", uri.getFileName(resolver))
+ }
+
+ @Test
+ fun `getFileName for content uri schemes`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+ val cursor = mock<Cursor>()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any())
+ doReturn(1).`when`(cursor).getColumnIndex(any())
+ doReturn(null).`when`(cursor).getString(anyInt())
+
+ val fileName = uri.getFileName(resolver)
+
+ assertTrue(fileName.contains(".txt"))
+ assertTrue(fileName.isNotEmpty())
+ }
+
+ @Test
+ fun `getFileName for UNKNOWN uri schemes will generate file name`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "UNKNOWN://media/external/file/37162".toUri()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ val fileName = uri.getFileName(resolver)
+
+ assertTrue(fileName.contains(".txt"))
+ assertTrue(fileName.isNotEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt
new file mode 100644
index 0000000000..5b1a53bce6
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt
@@ -0,0 +1,137 @@
+/* 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.support.ktx.android.org.json
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class JSONArrayTest {
+
+ private lateinit var testData2Elements: JSONArray
+
+ @Before
+ fun setUp() {
+ testData2Elements = JSONArray().apply {
+ put(JSONObject("""{"a": 1}"""))
+ put(JSONObject("""{"b": 2}"""))
+ }
+ }
+
+ @Test
+ fun itCanBeIterated() {
+ val array = JSONArray("[1, 2, 3]")
+
+ val sum = array.asSequence()
+ .map { it as Int }
+ .sum()
+
+ assertEquals(6, sum)
+ }
+
+ @Test
+ fun toListNull() {
+ val jsonArray: JSONArray? = null
+ val list = jsonArray.toList<Any>()
+ assertEquals(0, list.size)
+ }
+
+ @Test
+ fun toListEmpty() {
+ val jsonArray = JSONArray()
+ val list = jsonArray.toList<Any>()
+ assertEquals(0, list.size)
+ }
+
+ @Test
+ fun toListNotEmpty() {
+ val jsonArray = JSONArray()
+ jsonArray.put("value")
+ jsonArray.put("another-value")
+ val list = jsonArray.toList<String>()
+ assertEquals(2, list.size)
+ assertTrue(list.contains("value"))
+ assertTrue(list.contains("another-value"))
+ }
+
+ @Test
+ fun `WHEN mapNotNull on an empty jsonArray THEN an empty list is returned`() {
+ assertEquals(emptyList<Int>(), JSONArray().mapNotNull(JSONArray::getJSONObject) { 1 })
+ }
+
+ @Test
+ fun `WHEN mapNotNull getFromArray throws a JSONException THEN that item is ignored`() {
+ val expected = listOf("a", "b")
+ testData2Elements.put(404)
+ val actual = testData2Elements.mapNotNull(JSONArray::getJSONObject) { it.keys().asSequence().first() }
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `WHEN mapNotNull transform throws a JSONException THEN that item is ignored`() {
+ val actual = testData2Elements.mapNotNull(JSONArray::getJSONObject) {
+ it.get("b") // key not found for first item: throws an exception.
+ }
+ assertEquals(1, actual.size)
+ assertEquals(2, actual[0])
+ }
+
+ @Test
+ fun `WHEN mapNotNull getFromArray uses casted classes THEN data is mapped`() {
+ val expected = listOf(JSONArrayTest())
+ val input = JSONArray().apply { put(expected[0]) }
+ val actual = input.mapNotNull(getFromArray = { i -> get(i) as JSONArrayTest }) { it }
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `WHEN mapNotNull on an array of Int THEN nulls are removed and data is mapped`() {
+ val expected = listOf(2, 4, 6, 8, 10)
+
+ // Convert expected to input: [2, null, 4, null, ...]
+ val input = JSONArray()
+ expected.forEach {
+ input.put(it)
+ input.put(null)
+ }
+ val actual = input.mapNotNull(JSONArray::getInt) { it }
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `WHEN mapNotNull on an array of JSONObject THEN nulls are removed and data is mapped`() {
+ val expected = listOf(
+ "a" to 1,
+ "b" to 2,
+ "c" to 3,
+ )
+
+ // Convert expected to input: [JSONObject("a" to 1), null, JSONObject("b" to 2), null, ...]
+ val input = JSONArray()
+ expected.forEach {
+ val obj = JSONObject().apply { put(it.first, it.second) }
+ input.put(obj)
+ input.put(null)
+ }
+
+ val actual = input.mapNotNull(JSONArray::getJSONObject) {
+ val keys = it.keys().asSequence().toList()
+ assertEquals(it.toString(), 1, keys.size)
+
+ val key = keys.first()
+ val value = it.get(key) as Int
+ Pair(key, value)
+ }
+
+ assertEquals(expected, actual)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt
new file mode 100644
index 0000000000..1f724f0b55
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt
@@ -0,0 +1,131 @@
+/* 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.support.ktx.android.org.json
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class JSONObjectTest {
+
+ @Test
+ fun sortKeys() {
+ val jsonObject = JSONObject()
+ jsonObject.put("second-key", "second-value")
+ jsonObject.put(
+ "third-key",
+ JSONArray().apply {
+ put(1)
+ put(2)
+ put(3)
+ },
+ )
+ jsonObject.put(
+ "first-key",
+ JSONObject().apply {
+ put("one-key", "one-value")
+ put("a-key", "a-value")
+ put("second-key", "second")
+ },
+ )
+ assertEquals("""{"first-key":{"a-key":"a-value","one-key":"one-value","second-key":"second"},"second-key":"second-value","third-key":[1,2,3]}""", jsonObject.sortKeys().toString())
+ }
+
+ @Test
+ fun putIfNotNull() {
+ val jsonObject = JSONObject()
+ assertEquals(0, jsonObject.length())
+ jsonObject.putIfNotNull("key", null)
+ assertEquals(0, jsonObject.length())
+ jsonObject.putIfNotNull("key", "value")
+ assertEquals(1, jsonObject.length())
+ assertEquals("value", jsonObject["key"])
+ }
+
+ @Test
+ fun tryGetNull() {
+ val jsonObject = JSONObject("""{"key":null}""")
+ assertNull(jsonObject.tryGet("key"))
+ assertNull(jsonObject.tryGet("another-key"))
+ }
+
+ @Test
+ fun tryGetNotNull() {
+ val jsonObject = JSONObject("""{"key":"value"}""")
+ assertEquals("value", jsonObject.tryGet("key"))
+ }
+
+ @Test
+ fun tryGetStringNull() {
+ val jsonObject = JSONObject("""{"key":null}""")
+ assertNull(jsonObject.tryGetString("key"))
+ assertNull(jsonObject.tryGetString("another-key"))
+ }
+
+ @Test
+ fun tryGetStringNotNull() {
+ val jsonObject = JSONObject("""{"key":"value"}""")
+ assertEquals("value", jsonObject.tryGetString("key"))
+ }
+
+ @Test
+ fun tryGetLongNull() {
+ val jsonObject = JSONObject("""{"key":null}""")
+ assertNull(jsonObject.tryGetLong("key"))
+ assertNull(jsonObject.tryGetLong("another-key"))
+ }
+
+ @Test
+ fun tryGetLongNotNull() {
+ val jsonObject = JSONObject("""{"key":218728173837192717}""")
+ assertEquals(218728173837192717, jsonObject.tryGetLong("key"))
+ }
+
+ @Test
+ fun tryGetIntNull() {
+ val jsonObject = JSONObject("""{"key":null}""")
+ assertNull(jsonObject.tryGetInt("key"))
+ assertNull(jsonObject.tryGetInt("another-key"))
+ }
+
+ @Test
+ fun tryGetIntNotNull() {
+ val jsonObject = JSONObject("""{"key":3}""")
+ assertEquals(3, jsonObject.tryGetInt("key"))
+ }
+
+ @Test
+ fun mergeWith() {
+ val merged = JSONObject(
+ mapOf(
+ "toKeep" to 3,
+ "toOverride" to "OHNOZ",
+ ),
+ )
+
+ merged.mergeWith(
+ JSONObject(
+ mapOf(
+ "newKey" to 5,
+ "toOverride" to "YAY",
+ ),
+ ),
+ )
+
+ val expectedObject = JSONObject(
+ mapOf(
+ "toKeep" to 3,
+ "toOverride" to "YAY",
+ "newKey" to 5,
+ ),
+ )
+ assertEquals(expectedObject.toString(), merged.toString())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt
new file mode 100644
index 0000000000..b41997bc19
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt
@@ -0,0 +1,66 @@
+/* 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.support.ktx.android.os
+
+import androidx.core.os.bundleOf
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.base.crash.Breadcrumb
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Date
+
+@RunWith(AndroidJUnit4::class)
+class BundleTest {
+
+ @Test
+ fun `bundles with different sizes should not be equals`() {
+ val small = bundleOf(
+ "hello" to "world",
+ )
+ val big = bundleOf(
+ "hello" to "world",
+ "foo" to "bar",
+ )
+ assertFalse(small.contentEquals(big))
+ }
+
+ @Test
+ fun `bundles with arrays should be equal`() {
+ val (bundle1, bundle2) = (0..1).map {
+ bundleOf(
+ "str" to "world",
+ "int" to 0,
+ "boolArray" to booleanArrayOf(true, false),
+ "byteArray" to "test".toByteArray(),
+ "charArray" to "test".toCharArray(),
+ "doubleArray" to doubleArrayOf(0.0, 1.1),
+ "floatArray" to floatArrayOf(1f, 2f),
+ "intArray" to intArrayOf(0, 1, 2),
+ "longArray" to longArrayOf(0L, 1L),
+ "shortArray" to shortArrayOf(1, 2),
+ "typedArray" to arrayOf("foo", "bar"),
+ "nestedBundle" to bundleOf(),
+ )
+ }
+ assertTrue(bundle1.contentEquals(bundle2))
+ }
+
+ @Test
+ fun `bundles with parcelables should be equal`() {
+ val date = Date()
+ val (bundle1, bundle2) = (0..1).map {
+ bundleOf(
+ "crumbs" to Breadcrumb(
+ message = "msg",
+ level = Breadcrumb.Level.DEBUG,
+ date = date,
+ ),
+ )
+ }
+ assertTrue(bundle1.contentEquals(bundle2))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt
new file mode 100644
index 0000000000..4854110c2f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt
@@ -0,0 +1,64 @@
+/* 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.support.ktx.android.os
+
+import android.os.StrictMode
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class StrictModeTest {
+
+ @Test
+ fun `strict mode policy should be restored`() {
+ StrictMode.setThreadPolicy(
+ StrictMode.ThreadPolicy.Builder()
+ .detectDiskReads()
+ .penaltyLog()
+ .build(),
+ )
+ val policy = StrictMode.getThreadPolicy()
+ assertEquals(
+ 27,
+ StrictMode.allowThreadDiskReads().resetAfter {
+ // Comparing via toString because StrictMode.ThreadPolicy does not redefine equals() and each time
+ // setThreadPolicy is called a new ThreadPolicy object is created (although the mask is the same)
+ assertNotEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+ 27
+ },
+ )
+ assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+ }
+
+ @Test
+ fun `strict mode policy should be restored if function block throws an error`() {
+ val policy = StrictMode.ThreadPolicy.Builder()
+ .detectDiskReads()
+ .penaltyLog()
+ .build().apply {
+ StrictMode.setThreadPolicy(this)
+ }
+
+ val exceptionCaught: Boolean
+
+ assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+
+ try {
+ StrictMode.allowThreadDiskReads().resetAfter {
+ assertNotEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+ throw RuntimeException("Boing!")
+ }
+ } catch (e: RuntimeException) {
+ exceptionCaught = true
+ }
+
+ assertTrue(exceptionCaught)
+ assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt
new file mode 100644
index 0000000000..5661998034
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt
@@ -0,0 +1,45 @@
+/* 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.support.ktx.android.os
+
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.VibrationEffect.DEFAULT_AMPLITUDE
+import android.os.Vibrator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class VibratorTest {
+
+ private lateinit var vibrator: Vibrator
+
+ @Before
+ fun setup() {
+ vibrator = mock()
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.O])
+ @Test
+ fun `vibrateOneShot uses VibrationEffect on new APIs`() {
+ vibrator.vibrateOneShot(50L)
+
+ verify(vibrator).vibrate(VibrationEffect.createOneShot(50L, DEFAULT_AMPLITUDE))
+ }
+
+ @Suppress("Deprecation")
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Test
+ fun `vibrateOneShot uses vibrate on new APIs`() {
+ vibrator.vibrateOneShot(100L)
+
+ verify(vibrator).vibrate(100L)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt
new file mode 100644
index 0000000000..353761d7ba
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt
@@ -0,0 +1,21 @@
+/* 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.support.ktx.android.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class Base64Test {
+
+ @Test
+ fun `encodeToUriString contains required data URI format`() {
+ val s = Base64.encodeToUriString("foo")
+ Assert.assertTrue(s.contains("data:text/html;base64,"))
+ Assert.assertTrue(s.contains("Zm9v"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt
new file mode 100644
index 0000000000..fe92f15c4b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt
@@ -0,0 +1,27 @@
+/* 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.support.ktx.android.util
+
+import android.util.DisplayMetrics
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DisplayMetricsTest {
+
+ @Test
+ fun `dp returns same value as manual conversion`() {
+ val metrics = testContext.resources.displayMetrics
+
+ for (i in 1..500) {
+ val px = (i * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
+ Assert.assertEquals(px, i.dpToPx(metrics))
+ Assert.assertNotEquals(0, i.dpToPx(metrics))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt
new file mode 100644
index 0000000000..341588f695
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt
@@ -0,0 +1,72 @@
+/* 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.support.ktx.android.util
+
+import android.util.JsonReader
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class JsonReaderKtTest {
+ @Test
+ fun `nextStringOrNull - string`() {
+ val json = """{ "key": "value" }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertEquals("value", reader.nextStringOrNull())
+ }
+
+ @Test
+ fun `nextStringOrNull - null`() {
+ val json = """{ "key": null }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertNull(reader.nextStringOrNull())
+ }
+
+ @Test
+ fun `nextBooleanOrNull - true`() {
+ val json = """{ "key": true }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertTrue(reader.nextBooleanOrNull()!!)
+ }
+
+ @Test
+ fun `nextBooleanOrNull - false`() {
+ val json = """{ "key": false }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertFalse(reader.nextBooleanOrNull()!!)
+ }
+
+ @Test
+ fun `nextBooleanOrNull - null`() {
+ val json = """{ "key": null }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertNull(reader.nextBooleanOrNull())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt
new file mode 100644
index 0000000000..f438039b2b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt
@@ -0,0 +1,143 @@
+/* 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.support.ktx.android.view
+
+import android.app.Activity
+import android.os.Build
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.Window
+import android.view.WindowInsets
+import android.view.WindowManager
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.annotation.Config
+
+@Suppress("DEPRECATION")
+@RunWith(AndroidJUnit4::class)
+class ActivityTest {
+
+ private lateinit var activity: Activity
+ private lateinit var window: Window
+ private lateinit var decorView: View
+ private lateinit var viewTreeObserver: ViewTreeObserver
+ private lateinit var windowInsets: WindowInsets
+ private lateinit var insetsController: WindowInsetsControllerCompat
+ private lateinit var layoutParams: WindowManager.LayoutParams
+
+ @Before
+ fun setup() {
+ activity = mock()
+ window = mock()
+ decorView = mock()
+ viewTreeObserver = mock()
+ windowInsets = mock()
+ insetsController = mock()
+ layoutParams = WindowManager.LayoutParams()
+
+ `when`(activity.window).thenReturn(window)
+ `when`(window.decorView).thenReturn(decorView)
+ `when`(window.decorView.viewTreeObserver).thenReturn(viewTreeObserver)
+ `when`(window.decorView.onApplyWindowInsets(windowInsets)).thenReturn(windowInsets)
+ `when`(window.attributes).thenReturn(layoutParams)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.P])
+ fun `GIVEN Android version P WHEN enterImmersiveMode is called THEN systems bars are hidden, inset listener is set and notch flags are set to extend view into notch area`() {
+ activity.enterImmersiveMode(insetsController)
+
+ verify(insetsController).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ verify(window.decorView).setOnApplyWindowInsetsListener(any())
+
+ verify(window).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, layoutParams.layoutInDisplayCutoutMode)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O_MR1])
+ fun `GIVEN Android version O_MR1 WHEN enterImmersiveMode is called THEN systems bars are hidden, inset listener is set and notch flags are not being set`() {
+ activity.enterImmersiveMode(insetsController)
+
+ verify(insetsController).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ verify(window.decorView).setOnApplyWindowInsetsListener(any())
+
+ verify(window, never()).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ }
+
+ @Test
+ fun `GIVEN enterImmersiveMode was called WHEN window insets are changed THEN insetsController hides system bars and sets bars behaviour again`() {
+ val insetListenerCaptor = argumentCaptor<View.OnApplyWindowInsetsListener>()
+ doReturn(30).`when`(windowInsets).systemWindowInsetTop
+
+ activity.enterImmersiveMode(insetsController)
+
+ verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+
+ verify(window.decorView).setOnApplyWindowInsetsListener(insetListenerCaptor.capture())
+ insetListenerCaptor.value.onApplyWindowInsets(window.decorView, windowInsets)
+
+ verify(insetsController, times(2)).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController, times(2)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+
+ @Test
+ fun `GIVEN enterImmersiveMode was called WHEN window insets are not changed THEN insetsController does nothing`() {
+ val insetListenerCaptor = argumentCaptor<View.OnApplyWindowInsetsListener>()
+ doReturn(0).`when`(windowInsets).systemWindowInsetTop
+
+ activity.enterImmersiveMode(insetsController)
+
+ verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+
+ verify(window.decorView).setOnApplyWindowInsetsListener(insetListenerCaptor.capture())
+ insetListenerCaptor.value.onApplyWindowInsets(window.decorView, windowInsets)
+
+ verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+
+ @Test
+ fun `WHEN exitImmersiveMode is called THEN insetsController shows system bars and OnApplyWindowInsetsListener is cleared`() {
+ activity.exitImmersiveMode(insetsController)
+
+ verify(insetsController).show(WindowInsetsCompat.Type.systemBars())
+ verify(window.decorView).setOnApplyWindowInsetsListener(null)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.P])
+ fun `GIVEN Android version P WHEN exitImmersiveMode is called THEN notch flags are reset to defaults`() {
+ activity.exitImmersiveMode()
+
+ verify(window).clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT, layoutParams.layoutInDisplayCutoutMode)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O_MR1])
+ fun `GIVEN Android version O_MR1 WHEN exitImmersiveMode is called THEN notch flags were not being set`() {
+ activity.exitImmersiveMode()
+
+ verify(window, never()).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt
new file mode 100644
index 0000000000..355ae9f9bb
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt
@@ -0,0 +1,57 @@
+/* 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.support.ktx.android.view
+
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MotionEventKtTest {
+
+ private lateinit var subject: MotionEvent
+ private lateinit var subjectSpy: MotionEvent
+
+ @Before
+ fun setUp() {
+ subject = MotionEvent.obtain(100, 100, ACTION_DOWN, 0f, 0f, 0)
+ subjectSpy = spy(subject)
+ }
+
+ @Test
+ fun `WHEN use is called without an exception THEN the object is recycled`() {
+ subjectSpy.use {}
+ verify(subjectSpy, times(1)).recycle()
+ }
+
+ @Test
+ fun `WHEN use is called with an exception THEN the object is recycled`() {
+ try { subjectSpy.use { throw IllegalStateException("Catch me!") } } catch (e: Exception) { /* Do nothing */ }
+ verify(subjectSpy, times(1)).recycle()
+ }
+
+ @Test
+ fun `WHEN use is called and its function returns a value THEN that value is returned`() {
+ val expected = 47
+ assertEquals(expected, subject.use { expected })
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `WHEN use is called and its function throws an exception THEN that exception is thrown`() {
+ subject.use { throw IllegalStateException() }
+ }
+
+ @Test
+ fun `WHEN use is called THEN the use function's argument is the use receiver`() {
+ subject.use { assertEquals(subject, it) }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt
new file mode 100644
index 0000000000..e8c4c46321
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt
@@ -0,0 +1,98 @@
+/* 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.support.ktx.android.view
+
+import android.graphics.drawable.Drawable
+import android.widget.EditText
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertArrayEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class TextViewTest {
+
+ @Test
+ fun `putCompoundDrawablesRelative defaults to all null`() {
+ val view = TextView(testContext)
+
+ view.putCompoundDrawablesRelative()
+
+ assertArrayEquals(
+ arrayOf<Drawable?>(null, null, null, null),
+ view.compoundDrawablesRelative,
+ )
+ }
+
+ @Test
+ fun `putCompoundDrawablesRelativeWithIntrinsicBounds defaults to all null`() {
+ val view = TextView(testContext)
+
+ view.putCompoundDrawablesRelativeWithIntrinsicBounds()
+
+ assertArrayEquals(
+ arrayOf<Drawable?>(null, null, null, null),
+ view.compoundDrawablesRelative,
+ )
+ }
+
+ @Test
+ fun `putCompoundDrawablesRelative should set drawableStart and drawableEnd`() {
+ val view = EditText(testContext)
+ val drawable: Drawable = mock()
+
+ view.putCompoundDrawablesRelative(start = drawable)
+
+ assertArrayEquals(
+ arrayOf(drawable, null, null, null),
+ view.compoundDrawablesRelative,
+ )
+
+ view.putCompoundDrawablesRelative(end = drawable)
+
+ assertArrayEquals(
+ arrayOf(null, null, drawable, null),
+ view.compoundDrawablesRelative,
+ )
+ }
+
+ @Test
+ fun `putCompoundDrawablesRelativeWithIntrinsicBounds should set drawableStart and drawableEnd`() {
+ val view = EditText(testContext)
+ val drawable: Drawable = mock()
+
+ view.putCompoundDrawablesRelativeWithIntrinsicBounds(start = drawable)
+
+ assertArrayEquals(
+ arrayOf(drawable, null, null, null),
+ view.compoundDrawablesRelative,
+ )
+
+ view.putCompoundDrawablesRelativeWithIntrinsicBounds(end = drawable)
+
+ assertArrayEquals(
+ arrayOf(null, null, drawable, null),
+ view.compoundDrawablesRelative,
+ )
+ }
+
+ @Test
+ fun `putCompoundDrawablesRelative should call setCompoundDrawablesRelative`() {
+ val view: TextView = mock()
+ val drawable: Drawable = mock()
+
+ view.putCompoundDrawablesRelative(top = drawable)
+
+ verify(view).setCompoundDrawablesRelative(null, drawable, null, null)
+
+ view.putCompoundDrawablesRelativeWithIntrinsicBounds(bottom = drawable)
+
+ verify(view).setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, drawable)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt
new file mode 100644
index 0000000000..d2e537232f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt
@@ -0,0 +1,221 @@
+/* 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.support.ktx.android.view
+
+import android.app.Activity
+import android.content.Context
+import android.os.Looper.getMainLooper
+import android.view.View
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.shadows.ShadowLooper
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+class ViewTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `showKeyboard should request focus`() {
+ val view = EditText(testContext)
+ assertFalse(view.hasFocus())
+
+ view.showKeyboard()
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
+
+ assertTrue(view.hasFocus())
+ }
+
+ @Test
+ fun `hideKeyboard should hide soft keyboard`() {
+ val view = mock<View>()
+ val context = mock<Context>()
+ val imm = mock<InputMethodManager>()
+ `when`(view.context).thenReturn(context)
+ `when`(context.getSystemService(InputMethodManager::class.java)).thenReturn(imm)
+
+ view.hideKeyboard()
+
+ verify(imm).hideSoftInputFromWindow(view.windowToken, 0)
+ }
+
+ @Test
+ fun `setPadding should set padding`() {
+ val view = TextView(testContext)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ view.setPadding(Padding(16, 20, 24, 28))
+
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+
+ @Test
+ fun `getRectWithViewLocation should transform getLocationInWindow method values`() {
+ val view = spy(View(testContext))
+ doAnswer { invocation ->
+ val locationInWindow = (invocation.getArgument(0) as IntArray)
+ locationInWindow[0] = 100
+ locationInWindow[1] = 200
+ locationInWindow
+ }.`when`(view).getLocationInWindow(any())
+
+ `when`(view.width).thenReturn(150)
+ `when`(view.height).thenReturn(250)
+
+ val outRect = view.getRectWithViewLocation()
+
+ assertEquals(100, outRect.left)
+ assertEquals(200, outRect.top)
+ assertEquals(250, outRect.right)
+ assertEquals(450, outRect.bottom)
+ }
+
+ @Test
+ fun `called after next layout`() {
+ val view = View(testContext)
+
+ var callbackInvoked = false
+ view.onNextGlobalLayout {
+ callbackInvoked = true
+ }
+
+ assertFalse(callbackInvoked)
+
+ view.viewTreeObserver.dispatchOnGlobalLayout()
+
+ assertTrue(callbackInvoked)
+ }
+
+ @Test
+ fun `remove listener after next layout`() {
+ val view = spy(View(testContext))
+ val viewTreeObserver = spy(view.viewTreeObserver)
+ doReturn(viewTreeObserver).`when`(view).viewTreeObserver
+
+ view.onNextGlobalLayout {}
+
+ verify(viewTreeObserver, never()).removeOnGlobalLayoutListener(any())
+
+ viewTreeObserver.dispatchOnGlobalLayout()
+
+ verify(viewTreeObserver).removeOnGlobalLayoutListener(any())
+ }
+
+ @Test
+ fun `can dispatch coroutines to view scope`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(view.isAttachedToWindow)
+
+ val latch = CountDownLatch(1)
+ var coroutineExecuted = false
+
+ view.toScope().launch {
+ coroutineExecuted = true
+ latch.countDown()
+ }
+
+ latch.await(10, TimeUnit.SECONDS)
+
+ assertTrue(coroutineExecuted)
+ }
+
+ @Test
+ fun `scope is cancelled when view is detached`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ val scope = view.toScope()
+
+ assertTrue(view.isAttachedToWindow)
+ assertTrue(scope.isActive)
+
+ activity.windowManager.removeView(view)
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(view.isAttachedToWindow)
+ assertFalse(scope.isActive)
+
+ val latch = CountDownLatch(1)
+ var coroutineExecuted = false
+
+ scope.launch {
+ coroutineExecuted = true
+ latch.countDown()
+ }
+
+ assertFalse(latch.await(5, TimeUnit.SECONDS))
+ assertFalse(coroutineExecuted)
+ }
+
+ @Test
+ fun `correct view is found in the hierarchy matching the predicate`() {
+ val root = LinearLayout(testContext)
+ val layout = RelativeLayout(testContext)
+ val testView = TestView(testContext)
+
+ layout.addView(testView)
+ root.addView(layout)
+
+ val rootFound = root.findViewInHierarchy { it is LinearLayout }
+
+ assertNotNull(rootFound)
+ assertTrue(rootFound is LinearLayout)
+
+ val layoutFound = root.findViewInHierarchy { it is RelativeLayout }
+
+ assertNotNull(layoutFound)
+ assertTrue(layoutFound is RelativeLayout)
+
+ val testViewFound = root.findViewInHierarchy { it is TestView }
+
+ assertNotNull(testViewFound)
+ assertTrue(testViewFound is TestView)
+ }
+
+ private class TestView(context: Context) : View(context)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt
new file mode 100644
index 0000000000..24e49b9ce5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt
@@ -0,0 +1,135 @@
+/* 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.support.ktx.android.view
+
+import android.graphics.Color
+import android.os.Build
+import android.view.View
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.util.ReflectionHelpers.setStaticField
+import kotlin.reflect.jvm.javaField
+
+/**
+ * **Note** Tests for isAppearanceLightStatusBars are in WindowKtTest.
+ */
+@RunWith(AndroidJUnit4::class)
+class WindowTest {
+
+ @Mock private lateinit var window: Window
+
+ @Mock private lateinit var decorView: View
+
+ @Before
+ fun setup() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, 0)
+
+ openMocks(this)
+
+ `when`(window.decorView).thenAnswer { decorView }
+ }
+
+ @After
+ fun teardown() = setStaticField(Build.VERSION::SDK_INT.javaField, 0)
+
+ @Test
+ fun `GIVEN a color WHEN setStatusBarTheme THEN sets the status bar color`() {
+ window.setStatusBarTheme(Color.BLUE)
+ verify(window).statusBarColor = Color.BLUE
+
+ window.setStatusBarTheme(Color.RED)
+ verify(window).statusBarColor = Color.RED
+ }
+
+ @Test
+ fun `GIVEN Android 8 & no args WHEN setNavigationBarTheme THEN no colors are set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1)
+ window.setNavigationBarTheme()
+
+ verifyNoMoreInteractions(window)
+ }
+
+ @Test
+ fun `GIVEN Android 8 & has nav bar color WHEN setNavigationBarTheme THEN only the nav bar color is set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1)
+ window.setNavigationBarTheme(navBarColor = Color.MAGENTA)
+
+ // We can't verify against the navigationBarDividerColor directly due to using SDK O_MR1 so we'll verify using ordering.
+ val inOrder = inOrder(window)
+ inOrder.verify(window).navigationBarColor = Color.MAGENTA
+ // Called for createWindowInsetsController()
+ inOrder.verify(window, times(2)).decorView
+ inOrder.verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun `GIVEN Android 8 & has nav bar divider color WHEN setNavigationBarTheme THEN no colors are set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1)
+ window.setNavigationBarTheme(navBarDividerColor = Color.DKGRAY)
+
+ verifyNoMoreInteractions(window)
+ }
+
+ @Test
+ fun `GIVEN Android 8 & all args WHEN setNavigationBarTheme THEN only the nav bar color is set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1)
+ window.setNavigationBarTheme(navBarColor = Color.MAGENTA, navBarDividerColor = Color.DKGRAY)
+
+ // We can't verify against the navigationBarDividerColor directly due to using SDK O_MR1 so we'll verify using ordering.
+ val inOrder = inOrder(window)
+ inOrder.verify(window).navigationBarColor = Color.MAGENTA
+ // Called for createWindowInsetsController()
+ inOrder.verify(window, times(2)).decorView
+ inOrder.verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun `GIVEN Android 9 & no args WHEN setNavigationBarTheme THEN the nav bar divider color is set to default`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P)
+ window.setNavigationBarTheme()
+
+ verify(window, never()).navigationBarColor
+ verify(window).navigationBarDividerColor = 0
+ }
+
+ @Test
+ fun `GIVEN Android 9 has nav bar color WHEN setNavigationBarTheme THEN the nav bar color is set & nav bar divider color set to default`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P)
+ window.setNavigationBarTheme(navBarColor = Color.BLUE)
+
+ verify(window).navigationBarColor = Color.BLUE
+ verify(window).navigationBarDividerColor = 0
+ }
+
+ @Test
+ fun `GIVEN Android 9 has nav bar divider color WHEN setNavigationBarTheme THEN only the nav bar divider color is set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P)
+ window.setNavigationBarTheme(navBarDividerColor = Color.GREEN)
+
+ verify(window, never()).navigationBarColor
+ verify(window).navigationBarDividerColor = Color.GREEN
+ }
+
+ @Test
+ fun `GIVEN Android 9 & all args WHEN setNavigationBarTheme THEN the nav bar & nav bar divider colors are set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P)
+ window.setNavigationBarTheme(navBarColor = Color.YELLOW, navBarDividerColor = Color.CYAN)
+
+ verify(window).navigationBarColor = Color.YELLOW
+ verify(window).navigationBarDividerColor = Color.CYAN
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt
new file mode 100644
index 0000000000..a03e88ba87
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt
@@ -0,0 +1,142 @@
+/* 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.support.ktx.android.widget
+
+import android.view.View
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TextViewTest {
+
+ @Test
+ fun `check text size is set to the maximum allowable size specified by the height`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 200f
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(94f, textView.textSize)
+ }
+
+ @Test
+ fun `check text size is not adjusted if it is not larger than the allowable size`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 10f
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(10f, textView.textSize)
+ }
+
+ @Test
+ fun `check adjusted text size takes the default ascender padding into account`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(50f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(44f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.adjustMaxTextSize(heightSpec, 25)
+ assertEquals(25f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.adjustMaxTextSize(heightSpec, 25)
+ assertEquals(50f, textView.textSize)
+ }
+
+ @Test
+ fun `check text size is the same as the maximum adjusted text size`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(56, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 50f
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(50f, textView.textSize)
+
+ textView.textSize = 56f
+ textView.includeFontPadding = false
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(56f, textView.textSize)
+ }
+
+ @Test
+ fun `check custom padding affects text size`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.setPadding(5, 5, 5, 5)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(40f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.setPadding(0, 5, 0, 5)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(34f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.setPadding(5, 0, 5, 0)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(44f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.setPadding(0, 5, 0, 0)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(39f, textView.textSize)
+ }
+
+ @Test
+ fun `check negative available height results in text size 0`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.setPadding(0, 25, 0, 25)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(100f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.setPadding(0, 0, 0, 0)
+ textView.adjustMaxTextSize(heightSpec, 51)
+ assertEquals(100f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.setPadding(0, 26, 0, 25)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(100f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.setPadding(0, 25, 0, 25)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(100f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.setPadding(0, 1000, 0, 1000)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(100f, textView.textSize)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt
new file mode 100644
index 0000000000..2122ed9562
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt
@@ -0,0 +1,40 @@
+/* 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.support.ktx.java.io
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.io.File
+import java.util.*
+class FileKtTest {
+
+ @Test
+ fun truncateDirectory() {
+ val root = File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString())
+ assertTrue(root.mkdir())
+
+ val file1 = File(root, "file1")
+ assertTrue(file1.createNewFile())
+
+ val file2 = File(root, "file2")
+ assertTrue(file2.createNewFile())
+
+ val dir1 = File(root, "dir1")
+ assertTrue(dir1.mkdir())
+
+ val dir2 = File(root, "dir2")
+ assertTrue(dir2.mkdir())
+
+ val file3 = File(dir2, "file3")
+ file3.createNewFile()
+
+ assertEquals(4, root.listFiles()?.size)
+
+ root.truncateDirectory()
+
+ assertEquals(0, root.listFiles()?.size)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt
new file mode 100644
index 0000000000..34b9b15e5e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt
@@ -0,0 +1,29 @@
+/* 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.support.ktx.kotlin
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ByteArrayTest {
+
+ @Test
+ fun toHexString() {
+ val stringValue = "Android Components"
+ assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString())
+ assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(-1))
+ assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(36))
+ assertEquals("00416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(38))
+ }
+
+ @Test
+ fun toSha256Digest() {
+ val stringValue = "Android Components"
+ assertEquals(
+ "d2d01f10a9700b60740bdd20c60839dcf6b82be9e6a02719d564146cbe32d68f",
+ stringValue.toByteArray().toSha256Digest().toHexString(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt
new file mode 100644
index 0000000000..669dc01088
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt
@@ -0,0 +1,70 @@
+/* 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.support.ktx.kotlin
+
+import org.junit.Assert
+import org.junit.Test
+
+class CollectionKtTest {
+
+ @Test
+ fun `cross product of each element is called exactly`() {
+ val numbers = listOf(1, 2, 3)
+ val letters = listOf('a', 'b', 'c')
+ var counter = 0
+ numbers.crossProduct(letters) { _, _ ->
+ counter++
+ }
+
+ Assert.assertEquals(numbers.size * letters.size, counter)
+ }
+
+ @Test
+ fun `cross product of each element is of the same type`() {
+ val numbers = listOf(1, 2, 3)
+ val letters = listOf('a', 'b', 'c')
+ numbers.crossProduct(letters) { number, letter ->
+ Assert.assertEquals(Int::class, number::class)
+ Assert.assertEquals(Char::class, letter::class)
+ }
+ }
+
+ @Test
+ fun `cross product of each pair is in order`() {
+ val numbers = listOf(1, 2, 3)
+ val letters = listOf('a', 'b', 'c')
+ val assertions = arrayOf(
+ 1 to 'a',
+ 1 to 'b',
+ 1 to 'c',
+ 2 to 'a',
+ 2 to 'b',
+ 2 to 'c',
+ 3 to 'a',
+ 3 to 'b',
+ 3 to 'c',
+ )
+ var position = 0
+ numbers.crossProduct(letters) { number, letter ->
+ Assert.assertEquals(assertions[position].first, number)
+ Assert.assertEquals(assertions[position].second, letter)
+ position++
+ }
+ }
+
+ @Suppress("USELESS_IS_CHECK")
+ @Test
+ fun `cross product result is list of return type`() {
+ val numbers = listOf(1, 2, 3)
+ val letters = listOf('a', 'b', 'c')
+ val result = numbers.crossProduct(letters) { number, letter ->
+ number to letter
+ }
+ Assert.assertTrue(result is List)
+ Assert.assertEquals(Pair::class, result[0]::class)
+ Assert.assertEquals(Int::class, result[0].first::class)
+ Assert.assertEquals(Char::class, result[0].second::class)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
new file mode 100644
index 0000000000..89ade2aace
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
@@ -0,0 +1,595 @@
+/* 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.support.ktx.kotlin
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Calendar
+import java.util.Calendar.MILLISECOND
+
+const val PUNYCODE = "xn--kpry57d"
+const val IDN = "台灣"
+
+@RunWith(AndroidJUnit4::class)
+class StringTest {
+
+ private val publicSuffixList = PublicSuffixList(testContext)
+
+ @Test
+ fun isUrl() {
+ assertTrue("mozilla.org".isUrl())
+ assertTrue(" mozilla.org ".isUrl())
+ assertTrue("http://mozilla.org".isUrl())
+ assertTrue("https://mozilla.org".isUrl())
+ assertTrue("file://somefile.txt".isUrl())
+ assertTrue("http://mozilla".isUrl())
+ assertTrue("http://192.168.255.255".isUrl())
+ assertTrue("about:crashcontent".isUrl())
+ assertTrue(" about:crashcontent ".isUrl())
+ assertTrue("sample:about ".isUrl())
+
+ assertFalse("mozilla".isUrl())
+ assertFalse("mozilla android".isUrl())
+ assertFalse(" mozilla android ".isUrl())
+ assertFalse("Tweet:".isUrl())
+ assertFalse("inurl:mozilla.org advanced search".isUrl())
+ assertFalse("what is about:crashes".isUrl())
+
+ val extraText = "Check out @asa’s Tweet: https://twitter.com/asa/status/123456789?s=09"
+ val url = extraText.split(" ").find { it.isUrl() }
+ assertNotEquals("Tweet:", url)
+ }
+
+ @Test
+ fun toNormalizedUrl() {
+ val expectedUrl = "http://mozilla.org"
+ assertEquals(expectedUrl, "http://mozilla.org".toNormalizedUrl())
+ assertEquals(expectedUrl, " http://mozilla.org ".toNormalizedUrl())
+ assertEquals(expectedUrl, "mozilla.org".toNormalizedUrl())
+ }
+
+ @Test
+ fun isPhone() {
+ assertTrue("tel:+1234567890".isPhone())
+ assertTrue(" tel:+1234567890".isPhone())
+ assertTrue("tel:+1234567890 ".isPhone())
+ assertTrue("tel:+1234567890 ".isPhone())
+ assertTrue("TEL:+1234567890".isPhone())
+ assertTrue("Tel:+1234567890".isPhone())
+
+ assertFalse("tel:word".isPhone())
+ }
+
+ @Test
+ fun isEmail() {
+ assertTrue("mailto:asa@mozilla.com".isEmail())
+ assertTrue(" mailto:asa@mozilla.com".isEmail())
+ assertTrue("mailto:asa@mozilla.com ".isEmail())
+ assertTrue("MAILTO:asa@mozilla.com".isEmail())
+ assertTrue("Mailto:asa@mozilla.com".isEmail())
+ }
+
+ @Test
+ fun geoLocation() {
+ assertTrue("geo:1,-1".isGeoLocation())
+ assertTrue("geo:1,-1;u=1".isGeoLocation())
+ assertTrue("geo:1,-1,0.5;u=1".isGeoLocation())
+ assertTrue(" geo:1,-1".isGeoLocation())
+ assertTrue("geo:1,-1 ".isGeoLocation())
+ assertTrue("GEO:1,-1".isGeoLocation())
+ assertTrue("Geo:1,-1".isGeoLocation())
+ }
+
+ @Test
+ fun toDate() {
+ val calendar = Calendar.getInstance()
+ calendar.set(2019, 10, 29, 0, 0, 0)
+ calendar[MILLISECOND] = 0
+ assertEquals(calendar.time, "2019-11-29".toDate("yyyy-MM-dd"))
+ calendar.set(2019, 10, 28, 0, 0, 0)
+ assertEquals(calendar.time, "2019-11-28".toDate("yyyy-MM-dd"))
+ assertNotNull("".toDate("yyyy-MM-dd"))
+ }
+
+ @Test
+ fun `string to date conversion using multiple formats`() {
+ assertEquals("2019-08-16T01:02".toDate("yyyy-MM-dd'T'HH:mm"), "2019-08-16T01:02".toDate())
+
+ assertEquals("2019-08-16T01:02:03".toDate("yyyy-MM-dd'T'HH:mm"), "2019-08-16T01:02:03".toDate())
+
+ assertEquals("2019-08-16".toDate("yyyy-MM-dd"), "2019-08-16".toDate())
+ }
+
+ @Test
+ fun sha1() {
+ assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", "".sha1())
+
+ assertEquals("0a4d55a8d778e5022fab701977c5d840bbc486d0", "Hello World".sha1())
+
+ assertEquals("8de545c123907e9f886ba2313560a0abef530594", "ßüöä@!§\$".sha1())
+ }
+
+ @Test
+ fun `Try Get Host From Url`() {
+ val urlTest = "http://www.example.com:1080/docs/resource1.html"
+ val new = urlTest.tryGetHostFromUrl()
+ assertEquals(new, "www.example.com")
+ }
+
+ @Test
+ fun `Try Get Host From Malformed Url`() {
+ val urlTest = "notarealurl"
+ val new = urlTest.tryGetHostFromUrl()
+ assertEquals(new, "notarealurl")
+ }
+
+ @Test
+ fun isSameOriginAs() {
+ // Host mismatch.
+ assertFalse("https://foo.bar".isSameOriginAs("https://foo.baz"))
+ // Scheme mismatch.
+ assertFalse("http://127.0.0.1".isSameOriginAs("https://127.0.0.1"))
+ // Port mismatch (implicit + explicit).
+ assertFalse("https://foo.bar:444".isSameOriginAs("https://foo.bar"))
+ // Port mismatch (explicit).
+ assertFalse("https://foo.bar:444".isSameOriginAs("https://foo.bar:555"))
+ // Port OK but scheme different.
+ assertFalse("https://foo.bar".isSameOriginAs("http://foo.bar:443"))
+ // Port OK (explicit) but scheme different.
+ assertFalse("https://foo.bar:443".isSameOriginAs("ftp://foo.bar:443"))
+
+ assertTrue("https://foo.bar".isSameOriginAs("https://foo.bar"))
+ assertTrue("https://foo.bar/bobo".isSameOriginAs("https://foo.bar/obob"))
+ assertTrue("https://foo.bar".isSameOriginAs("https://foo.bar:443"))
+ assertTrue("https://foo.bar:333".isSameOriginAs("https://foo.bar:333"))
+ }
+
+ @Test
+ fun isExtensionUrl() {
+ assertTrue("moz-extension://1232-abcd".isExtensionUrl())
+ assertFalse("mozilla.org".isExtensionUrl())
+ assertFalse("https://mozilla.org".isExtensionUrl())
+ assertFalse("http://mozilla.org".isExtensionUrl())
+ }
+
+ @Test
+ fun sanitizeURL() {
+ val expectedUrl = "http://mozilla.org"
+ assertEquals(expectedUrl, "\nhttp://mozilla.org\n".sanitizeURL())
+ }
+
+ @Test
+ fun isResourceUrl() {
+ assertTrue("resource://1232-abcd".isResourceUrl())
+ assertFalse("mozilla.org".isResourceUrl())
+ assertFalse("https://mozilla.org".isResourceUrl())
+ assertFalse("http://mozilla.org".isResourceUrl())
+ }
+
+ @Test
+ fun sanitizeFileName() {
+ var file = "/../../../../../../../../../../directory/file.......txt"
+ val fileName = "file.txt"
+
+ assertEquals(fileName, file.sanitizeFileName())
+
+ file = "/root/directory/file.txt"
+
+ assertEquals(fileName, file.sanitizeFileName())
+
+ assertEquals("file", "file".sanitizeFileName())
+
+ assertEquals("file", "file..".sanitizeFileName())
+
+ assertEquals("file", "file.".sanitizeFileName())
+
+ assertEquals("file", ".file".sanitizeFileName())
+
+ assertEquals("test.2020.12.01.txt", "test.2020.12.01.txt".sanitizeFileName())
+ }
+
+ @Test
+ fun `getDataUrlImageExtension returns a default extension if one cannot be extracted from the data url`() {
+ val base64Image = "data:;base64,testImage"
+
+ val result = base64Image.getDataUrlImageExtension()
+
+ assertEquals("jpg", result)
+ }
+
+ @Test
+ fun `getDataUrlImageExtension returns an extension based on the media type included in the the data url`() {
+ val base64Image = "data:image/gif;base64,testImage"
+
+ val result = base64Image.getDataUrlImageExtension()
+
+ assertEquals("gif", result)
+ }
+
+ @Test
+ fun `ifNullOrEmpty returns the same if this CharSequence is not null and not empty`() {
+ val randomString = "something"
+
+ assertSame(randomString, randomString.ifNullOrEmpty { "something else" })
+ }
+
+ @Test
+ fun `ifNullOrEmpty returns the invocation of the passed in argument if this CharSequence is null`() {
+ val nullString: String? = null
+ val validResult = "notNullString"
+
+ assertSame(validResult, nullString.ifNullOrEmpty { validResult })
+ }
+
+ @Test
+ fun `ifNullOrEmpty returns the invocation of the passed in argument if this CharSequence is empty`() {
+ val nullString = ""
+ val validResult = "notEmptyString"
+
+ assertSame(validResult, nullString.ifNullOrEmpty { validResult })
+ }
+
+ @Test
+ fun `getRepresentativeCharacter returns the correct representative character for the given urls`() {
+ assertEquals("M", "https://mozilla.org".getRepresentativeCharacter())
+ assertEquals("W", "http://wikipedia.org".getRepresentativeCharacter())
+ assertEquals("P", "http://plus.google.com".getRepresentativeCharacter())
+ assertEquals("E", "https://en.m.wikipedia.org/wiki/Main_Page".getRepresentativeCharacter())
+
+ // Stripping common prefixes
+ assertEquals("T", "http://www.theverge.com".getRepresentativeCharacter())
+ assertEquals("F", "https://m.facebook.com".getRepresentativeCharacter())
+ assertEquals("T", "https://mobile.twitter.com".getRepresentativeCharacter())
+
+ // Special urls
+ assertEquals("?", "file:///".getRepresentativeCharacter())
+ assertEquals("S", "file:///system/".getRepresentativeCharacter())
+ assertEquals("P", "ftp://people.mozilla.org/test".getRepresentativeCharacter())
+
+ // No values
+ assertEquals("?", "".getRepresentativeCharacter())
+
+ // Rubbish
+ assertEquals("Z", "zZz".getRepresentativeCharacter())
+ assertEquals("Ö", "ölkfdpou3rkjaslfdköasdfo8".getRepresentativeCharacter())
+ assertEquals("?", "_*+*'##".getRepresentativeCharacter())
+ assertEquals("ツ", "¯\\_(ツ)_/¯".getRepresentativeCharacter())
+ assertEquals("ಠ", "ಠ_ಠ Look of Disapproval".getRepresentativeCharacter())
+
+ // Non-ASCII
+ assertEquals("Ä", "http://www.ätzend.de".getRepresentativeCharacter())
+ assertEquals("名", "http://名がドメイン.com".getRepresentativeCharacter())
+ assertEquals("C", "http://√.com".getRepresentativeCharacter())
+ assertEquals("SS", "http://ß.de".getRepresentativeCharacter())
+ assertEquals("Ԛ", "http://ԛәлп.com/".getRepresentativeCharacter()) // cyrillic
+
+ // Punycode
+ assertEquals("X", "http://xn--tzend-fra.de".getRepresentativeCharacter()) // ätzend.de
+ assertEquals("X", "http://xn--V8jxj3d1dzdz08w.com".getRepresentativeCharacter()) // 名がドメイン.com
+
+ // Numbers
+ assertEquals("1", "https://www.1and1.com/".getRepresentativeCharacter())
+
+ // IP
+ assertEquals("1", "https://192.168.0.1".getRepresentativeCharacter())
+ }
+
+ @Test
+ fun `last4Digits returns a string with only last 4 digits `() {
+ assertEquals("8431", "371449635398431".last4Digits())
+ assertEquals("2345", "12345".last4Digits())
+ assertEquals("1234", "1234".last4Digits())
+ assertEquals("123", "123".last4Digits())
+ assertEquals("1", "1".last4Digits())
+ assertEquals("", "".last4Digits())
+ }
+
+ @Test
+ fun `when the full hostname cannot be displayed, elide labels starting from the front`() {
+ // See https://url.spec.whatwg.org/#url-rendering-elision
+ // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#eliding-urls
+
+ val display = "http://1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.com"
+ .shortened()
+
+ val split = display.split(".")
+
+ // If the list ends with 25.com...
+ assertEquals("25", split.dropLast(1).last())
+ // ...and each value is 1 larger than the last...
+ split.dropLast(1)
+ .map { it.toInt() }
+ .windowed(2, 1)
+ .forEach { (prev, next) ->
+ assertEquals(next, prev + 1)
+ }
+ // ...that means that all removed values came from the front of the list
+ }
+
+ @Test
+ fun `the registrable domain is always displayed`() {
+ // https://url.spec.whatwg.org/#url-rendering-elision
+ // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#eliding-urls
+
+ val bigRegistrableDomain = "evil-but-also-shockingly-long-registrable-domain.com"
+ assertTrue(
+ "https://login.your-bank.com.$bigRegistrableDomain/enter/your/password".shortened()
+ .contains(bigRegistrableDomain),
+ )
+ }
+
+ @Test
+ fun `url username and password fields should not be displayed`() {
+ // See https://url.spec.whatwg.org/#url-rendering-simplification
+ // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#simplify
+
+ assertFalse("https://examplecorp.com@attacker.example/".shortened().contains("examplecorp"))
+ assertFalse("https://examplecorp.com@attacker.example/".shortened().contains("com"))
+ assertFalse("https://user:password@example.com/".shortened().contains("user"))
+ assertFalse("https://user:password@example.com/".shortened().contains("password"))
+ }
+
+ @Test
+ fun `eTLDs should not be dropped`() {
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11
+ "http://mozfreddyb.github.io/" shortenedShouldBecome "mozfreddyb.github.io"
+ "http://web.security.plumbing/" shortenedShouldBecome "web.security.plumbing"
+ }
+
+ @Test
+ fun `ipv4 addresses should be returned as is`() {
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11
+ "192.168.85.1" shortenedShouldBecome "192.168.85.1"
+ }
+
+ @Test
+ fun `about buildconfig should not be modified`() {
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11
+ "about:buildconfig" shortenedShouldBecome "about:buildconfig"
+ }
+
+ @Test
+ fun `encoded userinfo should still be considered userinfo`() {
+ "https://user:password%40really.evil.domain%2F@mail.google.com" shortenedShouldBecome "mail.google.com"
+ }
+
+ @Test
+ @Ignore("This would be more correct, but does not appear to be an attack vector")
+ fun `should decode DWORD IP addresses`() {
+ "https://16843009" shortenedShouldBecome "1.1.1.1"
+ }
+
+ @Test
+ @Ignore("This would be more correct, but does not appear to be an attack vector")
+ fun `should decode octal IP addresses`() {
+ "https://000010.000010.000010.000010" shortenedShouldBecome "8.8.8.8"
+ }
+
+ @Test
+ @Ignore("This would be more correct, but does not appear to be an attack vector")
+ fun `should decode hex IP addresses`() {
+ "http://0x01010101" shortenedShouldBecome "1.1.1.1"
+ }
+
+ // BEGIN test cases borrowed from desktop (shortUrl is used for Top Sites on new tab)
+ // Test cases are modified, as we show the eTLD
+ // (https://searchfox.org/mozilla-central/source/browser/components/newtab/test/unit/lib/ShortUrl.test.js)
+ @Test
+ fun `should return a blank string if url is blank`() {
+ "" shortenedShouldBecome ""
+ }
+
+ @Test
+ fun `should return the 'url' if not a valid url`() {
+ "something" shortenedShouldBecome "something"
+ "http:" shortenedShouldBecome "http:"
+ "http::double-colon" shortenedShouldBecome "http::double-colon"
+ // The largest allowed port is 65,535
+ "http://badport:65536/" shortenedShouldBecome "http://badport:65536/"
+ }
+
+ @Test
+ fun `should convert host to idn when calling shortURL`() {
+ "http://$PUNYCODE.blah.com" shortenedShouldBecome "$IDN.blah.com"
+ }
+
+ @Test
+ fun `should get the hostname from url`() {
+ "http://bar.com" shortenedShouldBecome "bar.com"
+ }
+
+ @Test
+ fun `should not strip out www if not first subdomain`() {
+ "http://foo.www.com" shortenedShouldBecome "foo.www.com"
+ "http://www.foo.www.com" shortenedShouldBecome "foo.www.com"
+ }
+
+ @Test
+ fun `should convert to lowercase`() {
+ "HTTP://FOO.COM" shortenedShouldBecome "foo.com"
+ }
+
+ @Test
+ fun `should not include the port`() {
+ "http://foo.com:8888" shortenedShouldBecome "foo.com"
+ }
+
+ @Test
+ fun `should return hostname for localhost`() {
+ "http://localhost:8000/" shortenedShouldBecome "localhost"
+ }
+
+ @Test
+ fun `should return hostname for ip address`() {
+ "http://127.0.0.1/foo" shortenedShouldBecome "127.0.0.1"
+ }
+
+ @Test
+ fun `should return etld for www gov uk (www-only non-etld)`() {
+ "https://www.gov.uk/countersigning" shortenedShouldBecome "gov.uk"
+ }
+
+ @Test
+ fun `should return idn etld for www-only non-etld`() {
+ "https://www.$PUNYCODE/foo" shortenedShouldBecome IDN
+ }
+
+ @Test
+ fun `file uri should return input`() {
+ "file:///foo/bar.txt" shortenedShouldBecome "file:///foo/bar.txt"
+ }
+
+ @Test
+ @Ignore("This behavior conflicts with https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11")
+ fun `should return not the protocol for about`() {
+ "about:newtab" shortenedShouldBecome "newtab"
+ }
+
+ @Test
+ fun `should fall back to full url as a last resort`() {
+ "about:" shortenedShouldBecome "about:"
+ }
+ // END test cases borrowed from desktop
+
+ // BEGIN test cases borrowed from FFTV
+ // (https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/test/java/org/mozilla/focus/utils/TestFormattedDomain.java#228)
+ @Test
+ fun testIsIPv4RealAddress() {
+ assertTrue("192.168.1.1".isIpv4())
+ assertTrue("8.8.8.8".isIpv4())
+ assertTrue("63.245.215.20".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv4WithProtocol() {
+ assertFalse("http://8.8.8.8".isIpv4())
+ assertFalse("https://8.8.8.8".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv4WithPort() {
+ assertFalse("8.8.8.8:400".isIpv4())
+ assertFalse("8.8.8.8:1337".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv4WithPath() {
+ assertFalse("8.8.8.8/index.html".isIpv4())
+ assertFalse("8.8.8.8/".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv4WithIPv6() {
+ assertFalse("2001:db8::1 ".isIpv4())
+ assertFalse("2001:db8:0:1:1:1:1:1".isIpv4())
+ assertFalse("[2001:db8:a0b:12f0::1]".isIpv4())
+ assertFalse("2001:db8: 3333:4444:5555:6666:1.2.3.4".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv6WithIPv6() {
+ assertTrue("2001:db8::1".isIpv6())
+ assertTrue("2001:db8:0:1:1:1:1:1".isIpv6())
+ }
+
+ @Test
+ fun testIsIPv6WithIPv4() {
+ assertFalse("192.168.1.1".isIpv6())
+ assertFalse("8.8.8.8".isIpv6())
+ assertFalse("63.245.215.20".isIpv6())
+ }
+ // END test cases borrowed from FFTV
+
+ @Test
+ fun testStripCommonSubdomains() {
+ assertEquals("mozilla.org", ("mozilla.org").stripCommonSubdomains())
+ assertEquals("mozilla.org", ("www.mozilla.org").stripCommonSubdomains())
+ assertEquals("mozilla.org", ("m.mozilla.org").stripCommonSubdomains())
+ assertEquals("mozilla.org", ("mobile.mozilla.org").stripCommonSubdomains())
+ assertEquals("random.mozilla.org", ("random.mozilla.org").stripCommonSubdomains())
+ }
+
+ @Test
+ fun `GIVEN an invalid base64 image string WHEN converting it into bitmap THEN the result is null`() {
+ val invalidBase64BitmapString = "aa"
+ assertNull(invalidBase64BitmapString.base64ToBitmap())
+ }
+
+ @Test
+ fun `GIVEN a valid base64 png string WHEN converting it into bitmap THEN the result is not null and no exception is thrown`() {
+ val validBase64BitmapString = "data:image/png;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ assertNotNull(validBase64BitmapString.base64ToBitmap())
+ }
+
+ @Test
+ fun `GIVEN a valid base64 image string WHEN converting it into bitmap THEN the result is not null and no exception is thrown`() {
+ val validBase64JpegString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val validBase64JpgString = "data:image/jpg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val validBase64AnythingString = "data:image/anything;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ assertNotNull(validBase64JpegString.base64ToBitmap())
+ assertNotNull(validBase64JpgString.base64ToBitmap())
+ assertNotNull(validBase64AnythingString.base64ToBitmap())
+ }
+
+ @Test
+ fun `GIVEN invalid base64 image strings WHEN converting them into bitmap THEN the result is null`() {
+ val invalidBase64String = "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val invalidBase64String2 = "data:image/jpg;base64;R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val invalidBase64String3 = "image/jpg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ assertNull(invalidBase64String.base64ToBitmap())
+ assertNull(invalidBase64String2.base64ToBitmap())
+ assertNull(invalidBase64String3.base64ToBitmap())
+ }
+
+ @Test
+ fun `GIVEN a valid or invalid base64 image string WHEN extracting its raw content string THEN the result is correct`() {
+ val validBase64JpegString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val validBase64JpgString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val validBase64PngString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val invalidBase64String = "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val invalidBase64String2 = "data:image/jpeg;base64;R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64JpegString.extractBase6RawString())
+ assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64JpgString.extractBase6RawString())
+ assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64PngString.extractBase6RawString())
+ assertNull(invalidBase64String.extractBase6RawString())
+ assertNull(invalidBase64String2.extractBase6RawString())
+ }
+
+ @Test
+ fun `GIVEN a URL with matching parameters WHEN testing if a URL contains query parameters THEN the result is true`() {
+ assertTrue("http://example.com?a".urlContainsQueryParameters("a"))
+ assertTrue("http://example.com?a&b&c".urlContainsQueryParameters("b"))
+ assertTrue("http://example.com?a=b".urlContainsQueryParameters("a=b"))
+ assertTrue("http://example.com?a=b&c=d&e=f".urlContainsQueryParameters("c=d"))
+ assertTrue("http://example.com?a=b&c=d&e=f#g=h".urlContainsQueryParameters("e=f"))
+ }
+
+ @Test
+ fun `GIVEN a URL without matching parameters WHEN testing if a URL contains query parameters THEN the result is false`() {
+ assertFalse("".urlContainsQueryParameters("a"))
+ assertFalse("!@#$%^&*()-+".urlContainsQueryParameters("a"))
+ assertFalse("http://example.com".urlContainsQueryParameters("a"))
+ assertFalse("http://example.com?a&b".urlContainsQueryParameters("c"))
+ assertFalse("http://example.com?a=b".urlContainsQueryParameters("a"))
+ assertFalse("http://example.com?a=b&c=d&e=f#g=h".urlContainsQueryParameters("g=h"))
+ }
+
+ private infix fun String.shortenedShouldBecome(expect: String) {
+ assertEquals(expect, this.shortened())
+ }
+
+ private fun String.shortened() = this.toShortUrl(publicSuffixList)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt
new file mode 100644
index 0000000000..6c987b9a67
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt
@@ -0,0 +1,38 @@
+/* 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.support.ktx.kotlinx.coroutines
+
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+
+class UtilsKtTest {
+
+ @Test
+ fun throttle() = runTest(UnconfinedTestDispatcher()) {
+ val skipTime = 300L
+ var value = 0
+ val throttleBlock = throttleLatest<Int>(skipTime, coroutineScope = this) {
+ value = it
+ }
+
+ for (n in 1..300) {
+ throttleBlock(n)
+ }
+ assertNotEquals(300, value)
+
+ value = 0
+
+ for (n in 1..300) {
+ delay(skipTime)
+ throttleBlock(n)
+ }
+
+ assertEquals(300, value)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt
new file mode 100644
index 0000000000..11156c780a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt
@@ -0,0 +1,90 @@
+/* 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.support.ktx.kotlinx.coroutines.flow
+
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class FlowKtTest {
+
+ data class CharState(val value: Char)
+ data class IntState(val value: Int)
+
+ @Test
+ fun `ifAnyChanged operator with block`() = runTest {
+ val originalFlow = flowOf("banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home")
+
+ val items = originalFlow.ifAnyChanged { item -> arrayOf(item[0], item[1]) }.toList()
+
+ assertEquals(
+ listOf("banana", "bus", "apple", "big", "coconut", "circle", "home"),
+ items,
+ )
+ }
+
+ @Test
+ fun `ifAnyChanged operator uses structural equality`() = runTest {
+ val originalFlow = flowOf("banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home")
+
+ val items =
+ originalFlow.ifAnyChanged {
+ item ->
+ arrayOf(CharState(item[0]), CharState(item[1]))
+ }.toList()
+
+ assertEquals(
+ listOf("banana", "bus", "apple", "big", "coconut", "circle", "home"),
+ items,
+ )
+ }
+
+ @Test
+ fun `filterChanged operator`() = runTest {
+ val intFlow = flowOf(listOf(0), listOf(0, 1), listOf(0, 1, 2, 3), listOf(4), listOf(5, 6, 7, 8))
+ val identityItems = intFlow.filterChanged { item -> item }.toList()
+ assertEquals(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8), identityItems)
+
+ val moduloFlow = flowOf(listOf(1), listOf(1, 2), listOf(3, 4, 5), listOf(3, 4))
+ val moduloItems = moduloFlow.filterChanged { item -> item % 2 }.toList()
+ assertEquals(listOf(1, 2, 3, 4, 5), moduloItems)
+
+ // Here we simulate a non-pure transform function (a function with a side-effect), causing
+ // the transformed values to be different for the same input.
+ var counter = 0
+ val sideEffectFlow = flowOf(listOf(0), listOf(0, 1), listOf(0, 1, 2, 3), listOf(4), listOf(5, 6, 7, 8))
+ val sideEffectItems = sideEffectFlow.filterChanged { item -> item + counter++ }.toList()
+ assertEquals(listOf(0, 0, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8), sideEffectItems)
+ }
+
+ @Test
+ fun `filterChanged operator check for structural equality`() = runTest {
+ val intFlow = flowOf(
+ listOf(IntState(0)),
+ listOf(IntState(0), IntState(1)),
+ listOf(IntState(0), IntState(1), IntState(2), IntState(3)),
+ listOf(IntState(4)),
+ listOf(IntState(5), IntState(6), IntState(7), IntState(8)),
+ )
+
+ val identityItems = intFlow.filterChanged { item -> item }.toList()
+ assertEquals(
+ listOf(
+ IntState(0),
+ IntState(1),
+ IntState(2),
+ IntState(3),
+ IntState(4),
+ IntState(5),
+ IntState(6),
+ IntState(7),
+ IntState(8),
+ ),
+ identityItems,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt
new file mode 100644
index 0000000000..a842a05c83
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt
@@ -0,0 +1,72 @@
+/* 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/. */
+
+@file:Suppress("SameParameterValue")
+
+package mozilla.components.support.ktx.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.getSystemService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import mozilla.components.support.ktx.android.notification.ChannelData
+import mozilla.components.support.ktx.android.notification.ensureNotificationChannelExists
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+
+internal const val NOTIFICATION_CHANNEL_ID = "NOTIFICATION_CHANNEL_ID"
+
+@RunWith(AndroidJUnit4::class)
+class NotificationTest {
+
+ @Test
+ fun `ensureChannelExists - creates a notification channel`() {
+ var setupChannelWasCalled = false
+ var afterCreatedChannelWasCalled = false
+
+ assertFalse(exists(channelId = NOTIFICATION_CHANNEL_ID))
+
+ val channelData = ChannelData(
+ NOTIFICATION_CHANNEL_ID,
+ android.R.string.ok,
+ NotificationManagerCompat.IMPORTANCE_LOW,
+ )
+
+ val setupChannel: NotificationChannel.() -> Unit = {
+ assertFalse(exists(channelId = NOTIFICATION_CHANNEL_ID))
+ setupChannelWasCalled = true
+ lockscreenVisibility = NotificationCompat.VISIBILITY_SECRET
+ }
+
+ val afterCreatedChannel: NotificationManager.() -> Unit = {
+ assertTrue(exists(channelId = NOTIFICATION_CHANNEL_ID))
+ afterCreatedChannelWasCalled = true
+
+ val channel = getChannel(NOTIFICATION_CHANNEL_ID)!!
+
+ assertTrue(channel.lockscreenVisibility == NotificationCompat.VISIBILITY_SECRET)
+ }
+
+ val channelId =
+ ensureNotificationChannelExists(testContext, channelData, setupChannel, afterCreatedChannel)
+
+ assertTrue(setupChannelWasCalled)
+ assertTrue(afterCreatedChannelWasCalled)
+ assertTrue(exists(channelId = NOTIFICATION_CHANNEL_ID))
+ assertEquals(channelId, NOTIFICATION_CHANNEL_ID)
+ }
+
+ private fun exists(channelId: String) = getChannel(channelId) != null
+
+ private fun getChannel(channelId: String): NotificationChannel? {
+ val notificationManager = testContext.getSystemService<NotificationManager>()!!
+ return notificationManager.getNotificationChannel(channelId)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt
new file mode 100644
index 0000000000..48cd9137f8
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt
@@ -0,0 +1,94 @@
+/* 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.support.ktx.util
+
+import android.util.AtomicFile
+import androidx.core.util.writeText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.json.JSONException
+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.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.verify
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class AtomicFileTest {
+
+ @Test
+ fun `writeString - Fails write on IOException`() {
+ val mockedFile: AtomicFile = mock()
+ doThrow(IOException::class.java).`when`(mockedFile).startWrite()
+
+ val result = mockedFile.writeString { "file_content" }
+
+ assertFalse(result)
+ verify(mockedFile).failWrite(any())
+ }
+
+ @Test
+ fun `writeString - Fails write on JSONException`() {
+ val mockedFile: AtomicFile = mock()
+ whenever(mockedFile.startWrite()).thenAnswer {
+ throw JSONException("")
+ }
+
+ val result = mockedFile.writeString { "file_content" }
+
+ assertFalse(result)
+ verify(mockedFile).failWrite(any())
+ }
+
+ @Test
+ fun `writeString - writes the content of the file`() {
+ val tempFile = File.createTempFile("temp", ".tmp")
+ val atomicFile = AtomicFile(tempFile)
+ atomicFile.writeString { "file_content" }
+
+ val result = atomicFile.writeString { "file_content" }
+ assertTrue(result)
+ }
+
+ @Test
+ fun `readAndDeserialize - Returns the content of the file`() {
+ val tempFile = File.createTempFile("temp", ".tmp")
+ val atomicFile = AtomicFile(tempFile)
+ atomicFile.writeText("file_content")
+
+ val fileContent = atomicFile.readAndDeserialize { it }
+ assertNotNull(fileContent)
+ assertEquals("file_content", fileContent)
+ }
+
+ @Test
+ fun `readAndDeserialize - Returns null on FileNotFoundException`() {
+ val mockedFile: AtomicFile = mock()
+ doThrow(FileNotFoundException::class.java).`when`(mockedFile).openRead()
+
+ val content = mockedFile.readAndDeserialize { it }
+ assertNull(content)
+ }
+
+ @Test
+ fun `readAndDeserialize - Returns null on JSONException`() {
+ val mockedFile: AtomicFile = mock()
+ whenever(mockedFile.openRead()).thenAnswer {
+ throw JSONException("")
+ }
+
+ val content = mockedFile.readAndDeserialize { it }
+ assertNull(content)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt
new file mode 100644
index 0000000000..b07e11f698
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt
@@ -0,0 +1,51 @@
+/* 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.support.ktx.util
+
+import android.content.res.Resources
+import android.util.DisplayMetrics
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.ktx.android.util.spToPx
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class DisplayMetricsTest {
+ private lateinit var metrics: DisplayMetrics
+
+ @Before
+ fun setUp() {
+ metrics = mock(DisplayMetrics::class.java)
+ metrics.density = 3f
+ metrics.setToDefaults()
+
+ val resources: Resources = mock(Resources::class.java)
+ `when`(resources.displayMetrics).thenReturn(metrics)
+ }
+
+ @Test
+ fun `Float dpToPx returns correct value`() {
+ val floatValue = 10f
+
+ val result = floatValue.dpToPx(metrics)
+
+ assertEquals(metrics.density * floatValue, result)
+ }
+
+ @Test
+ fun `Float spToPx returns correct value`() {
+ val floatValue = 10f
+
+ val result = floatValue.spToPx(metrics)
+
+ @Suppress("DEPRECATION")
+ assertEquals(metrics.scaledDensity * floatValue, result)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/support/ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/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/support/ktx/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/license/README.md b/mobile/android/android-components/components/support/license/README.md
new file mode 100644
index 0000000000..99af655184
--- /dev/null
+++ b/mobile/android/android-components/components/support/license/README.md
@@ -0,0 +1,63 @@
+# [Android Components](../../../README.md) > Support > License
+
+A component to help generate and display licensing agreements.
+
+### 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:support-license:{latest-version}"
+```
+
+## Usage
+
+### License Plugin
+
+Add the license plugin directly to your **project's** `build.gradle` classpath:
+
+```groovy
+// root build.gradle
+buildscript {
+ dependencies {
+ classpath "com.google.android.gms:oss-licenses-plugin:0.10.4"
+ }
+}
+```
+
+Apply the plugin to the top of your **application's** `build.gradle`:
+
+```groovy
+apply plugin: 'com.google.android.gms.oss-licenses-plugin'
+```
+
+### Display licenses
+
+Extend the abstract `LibrariesListFragment` and provide the resource location to the licenses and the metadata:
+
+```kotlin
+class MyLicense : LibrariesListFragment() {
+ override val resources = LicenseResources(
+ licenses = R.raw.third_party_licenses,
+ licenseMetadata = R.raw.third_party_license_metadata,
+ )
+}
+```
+
+Optionally, add more fragment decorations as desired. For example:
+
+```kotlin
+class MyLicense : LibrariesListFragment() {
+ override fun onResume() {
+ super.onResume()
+
+ showToolbar("Licenses we use")
+ }
+}
+```
+
+## 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/support/license/build.gradle b/mobile/android/android-components/components/support/license/build.gradle
new file mode 100644
index 0000000000..bbfb389e6c
--- /dev/null
+++ b/mobile/android/android-components/components/support/license/build.gradle
@@ -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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'mozilla.components.support.license'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_core
+ implementation ComponentsDependencies.androidx_fragment
+
+ implementation ComponentsDependencies.kotlin_coroutines
+}
+
+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/support/license/proguard-rules.pro b/mobile/android/android-components/components/support/license/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/support/license/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/support/license/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/license/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..37af1fdca2
--- /dev/null
+++ b/mobile/android/android-components/components/support/license/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 xmlns:android="http://schemas.android.com/apk/res/android" /> \ No newline at end of file
diff --git a/mobile/android/android-components/components/support/license/src/main/java/mozilla/components/support/license/LibrariesListFragment.kt b/mobile/android/android-components/components/support/license/src/main/java/mozilla/components/support/license/LibrariesListFragment.kt
new file mode 100644
index 0000000000..9ece38e112
--- /dev/null
+++ b/mobile/android/android-components/components/support/license/src/main/java/mozilla/components/support/license/LibrariesListFragment.kt
@@ -0,0 +1,129 @@
+/* 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.support.license
+
+import android.app.AlertDialog
+import android.graphics.Typeface
+import android.os.Bundle
+import android.text.util.Linkify
+import android.view.View
+import android.widget.ArrayAdapter
+import android.widget.ListView
+import android.widget.TextView
+import androidx.annotation.RawRes
+import androidx.fragment.app.Fragment
+import mozilla.components.support.license.databinding.FragmentLibrariesListBinding
+import java.nio.charset.Charset
+import java.util.Locale
+
+/**
+ * Displays the licenses of all the libraries used by Fenix.
+ *
+ * This is a re-implementation of play-services-oss-licenses library.
+ * We can't use the official implementation in the OSS flavor of Fenix
+ * because it is proprietary and closed-source.
+ *
+ * There are popular FLOSS alternatives to Google's plugin and library
+ * such as AboutLibraries (https://github.com/mikepenz/AboutLibraries)
+ * but we considered the risk of introducing such third-party dependency
+ * to Fenix too high. Therefore, we use Google's gradle plugin to
+ * extract the dependencies and their licenses, and this fragment
+ * to show the extracted licenses to the end-user.
+ */
+abstract class LibrariesListFragment : Fragment(R.layout.fragment_libraries_list) {
+
+ /**
+ * The resource location for the license information.
+ *
+ * @property licenses the license data.
+ * @property metadata the information needed to parse the [licenses] file.
+ */
+ data class LicenseData(
+ @RawRes val licenses: Int,
+ @RawRes val metadata: Int,
+ )
+
+ /**
+ * Required data from the app that was generated using the OSS license plugin.
+ */
+ abstract val licenseData: LicenseData
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val binding = FragmentLibrariesListBinding.bind(view)
+ setupLibrariesListView(binding.aboutLibrariesListview)
+ }
+
+ private fun setupLibrariesListView(listView: ListView) {
+ val libraries = parseLibraries()
+ listView.adapter = ArrayAdapter(
+ listView.context,
+ android.R.layout.simple_list_item_1,
+ libraries,
+ )
+ listView.setOnItemClickListener { _, _, position, _ ->
+ showLicenseDialog(libraries[position])
+ }
+ }
+
+ private fun parseLibraries(): List<LibraryItem> {
+ /*
+ The gradle plugin "oss-licenses-plugin" creates two "raw" resources:
+
+ - third_party_licenses which is the binary concatenation of all the licenses text for
+ all the libraries. License texts can either be an URL to a license file or just the
+ raw text of the license.
+
+ - third_party_licenses_metadata which contains one dependency per line formatted in
+ the following way: "[start_offset]:[length] [name]"
+
+ [start_offset] : first byte in third_party_licenses that contains the license
+ text for this library.
+ [length] : length of the license text for this library in
+ third_party_licenses.
+ [name] : either the name of the library, or its artifact name.
+
+ See https://github.com/google/play-services-plugins/tree/master/oss-licenses-plugin
+ */
+
+ val licensesData = resources
+ .openRawResource(licenseData.licenses)
+ .readBytes()
+ val licensesMetadataReader = resources
+ .openRawResource(licenseData.metadata)
+ .bufferedReader()
+
+ return licensesMetadataReader.use { reader -> reader.readLines() }.map { line ->
+ val (section, name) = line.split(" ", limit = 2)
+ val (startOffset, length) = section.split(":", limit = 2).map(String::toInt)
+ val licenseData = licensesData.sliceArray(startOffset until startOffset + length)
+ val licenseText = licenseData.toString(Charset.forName("UTF-8"))
+ LibraryItem(name, licenseText)
+ }.sortedBy { item -> item.name.lowercase(Locale.ROOT) }
+ }
+
+ private fun showLicenseDialog(libraryItem: LibraryItem) {
+ val dialog = AlertDialog.Builder(requireContext())
+ .setTitle(libraryItem.name)
+ .setMessage(libraryItem.license)
+ .create()
+ dialog.show()
+
+ val textView = dialog.findViewById<TextView>(android.R.id.message)!!
+ Linkify.addLinks(textView, Linkify.WEB_URLS)
+ textView.linksClickable = true
+ textView.textSize = LICENSE_TEXT_SIZE
+ textView.typeface = Typeface.MONOSPACE
+ }
+
+ companion object {
+ private const val LICENSE_TEXT_SIZE = 10F
+ }
+}
+
+private class LibraryItem(val name: String, val license: String) {
+ override fun toString(): String {
+ return name
+ }
+}
diff --git a/mobile/android/android-components/components/support/license/src/main/res/layout/fragment_libraries_list.xml b/mobile/android/android-components/components/support/license/src/main/res/layout/fragment_libraries_list.xml
new file mode 100644
index 0000000000..36060c80e2
--- /dev/null
+++ b/mobile/android/android-components/components/support/license/src/main/res/layout/fragment_libraries_list.xml
@@ -0,0 +1,18 @@
+<?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/. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/about_libraries"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context="mozilla.components.support.license.AboutLibrariesFragment">
+ <ListView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/about_libraries_listview" />
+</LinearLayout>
diff --git a/mobile/android/android-components/components/support/license/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/license/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/license/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/locale/README.md b/mobile/android/android-components/components/support/locale/README.md
new file mode 100644
index 0000000000..87dfee0eb4
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Locale
+
+A component to allow apps to change the system defined language by their custom one.
+
+## Usage
+
+### 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:support-locale:{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/support/locale/build.gradle b/mobile/android/android-components/components/support/locale/build.gradle
new file mode 100644
index 0000000000..e5f4e9d086
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/build.gradle
@@ -0,0 +1,48 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.support.locale'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_core
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation project(':support-base')
+ implementation project(':support-utils')
+ implementation project(':browser-state')
+
+ testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/support/locale/gradle.properties b/mobile/android/android-components/components/support/locale/gradle.properties
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/gradle.properties
diff --git a/mobile/android/android-components/components/support/locale/proguard-rules.pro b/mobile/android/android-components/components/support/locale/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/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/support/locale/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/locale/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/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/support/locale/src/main/java/mozilla/components/support/locale/ActivityContextWrapper.kt b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/ActivityContextWrapper.kt
new file mode 100644
index 0000000000..acedee7330
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/ActivityContextWrapper.kt
@@ -0,0 +1,43 @@
+/* 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.support.locale
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+
+/**
+ * A [ContextWrapper] that holds the original [Activity] Context.
+ *
+ * @param baseContext see [ContextWrapper.getBaseContext].
+ * @param originalContext the Context that the Activity was created with. This might be the same
+ * as baseContext if a non-default value has not been set.
+ */
+class ActivityContextWrapper(
+ baseContext: Context,
+ val originalContext: Context = baseContext,
+) : ContextWrapper(baseContext) {
+ companion object {
+ /**
+ * Recursively try to retrieve the [ActivityContextWrapper.originalContext] from a wrapped
+ * Activity Context.
+ *
+ * @param outerContext the Activity Context that may be wrapped in
+ * an [ActivityContextWrapper].
+ * @return the [ActivityContextWrapper.originalContext] or otherwise null if one
+ * doesn't exist.
+ */
+ fun getOriginalContext(outerContext: Context): Context? {
+ if (outerContext !is ContextWrapper) {
+ return null
+ }
+ return if (outerContext is ActivityContextWrapper) {
+ outerContext.originalContext
+ } else {
+ getOriginalContext(outerContext.baseContext)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt
new file mode 100644
index 0000000000..8c1ac0cffa
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt
@@ -0,0 +1,57 @@
+/* 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.support.locale
+
+import java.util.Locale
+
+fun String.toLocale(): Locale {
+ val index: Int = if (contains('-')) {
+ indexOf('-')
+ } else {
+ indexOf('_')
+ }
+ return if (index != -1) {
+ val langCode = substring(0, index)
+ val countryCode = substring(index + 1)
+ Locale(langCode, countryCode)
+ } else {
+ Locale(this)
+ }
+}
+
+/**
+ * Gets a gecko-compatible locale string (e.g. "es-ES" instead of Java [Locale]
+ * "es_ES") for the default locale.
+ * If the locale can't be determined on the system, the value is "und",
+ * to indicate "undetermined".
+ *
+ * This method approximates the API21 method [Locale.toLanguageTag].
+ *
+ * @return a locale string that supports custom injected locale/languages.
+ */
+fun Locale.getLocaleTag(): String {
+ // Thanks to toLanguageTag() being introduced in API21, we could have
+ // simply returned `locale.toLanguageTag();` from this function. However
+ // what kind of languages the Android build supports is up to the manufacturer
+ // and our apps usually support translations for more rare languages, through
+ // our custom locale injector. For this reason, we can't use `toLanguageTag`
+ // and must try to replicate its logic ourselves.
+
+ // `locale.language` can, but should never be, an empty string.
+ // Modernize certain language codes.
+ val language = when (this.language) {
+ "iw" -> "he"
+ "in" -> "id"
+ "ji" -> "yi"
+ else -> this.language
+ }
+ val country = this.country // Can be an empty string.
+
+ return when {
+ language.isEmpty() -> "und"
+ country.isEmpty() -> language
+ else -> "$language-$country"
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt
new file mode 100644
index 0000000000..af420dce69
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt
@@ -0,0 +1,55 @@
+/* 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.support.locale
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.Configuration
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Base activity for apps that want to customized the system defined language by their own.
+ */
+open class LocaleAwareAppCompatActivity : AppCompatActivity() {
+ @SuppressLint("AppBundleLocaleChanges")
+ override fun attachBaseContext(base: Context) {
+ val locale = LocaleManager.getCurrentLocale(base) ?: LocaleManager.getSystemDefault()
+
+ // According to https://android-review.googlesource.com/c/platform/frameworks/support/+/2137592
+ // We should re-apply overrides.
+ val overrideConfiguration = Configuration()
+
+ // Modify the configuration as needed
+ overrideConfiguration.setLocale(locale)
+
+ val newContext = base.createConfigurationContext(overrideConfiguration)
+ val contextWrapper = ActivityContextWrapper(newContext, base)
+ super.attachBaseContext(contextWrapper)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setLayoutDirectionIfNeeded()
+ }
+
+ /**
+ * Compensates for a bug in Android 8 which doesn't change the layoutDirection on activity recreation
+ * https://github.com/mozilla-mobile/fenix/issues/9413
+ * https://stackoverflow.com/questions/46296202/rtl-layout-bug-in-android-oreo#comment98890942_46298101
+ */
+ @SuppressWarnings("VariableNaming")
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ fun setLayoutDirectionIfNeeded() {
+ val isAndroid8 = Build.VERSION.SDK_INT == Build.VERSION_CODES.O
+ val isAndroid8_1 = Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1
+
+ if (isAndroid8 || isAndroid8_1) {
+ window.decorView.layoutDirection = resources.configuration.layoutDirection
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt
new file mode 100644
index 0000000000..cc565e41cf
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt
@@ -0,0 +1,25 @@
+/* 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.support.locale
+
+import android.app.Application
+import android.content.Context
+import android.content.res.Configuration
+
+/**
+ * Base application for apps that want to customized the system defined language by their own.
+ */
+open class LocaleAwareApplication : Application() {
+
+ override fun attachBaseContext(base: Context) {
+ val context = LocaleManager.updateResources(base)
+ super.attachBaseContext(context)
+ }
+
+ override fun onConfigurationChanged(config: Configuration) {
+ super.onConfigurationChanged(config)
+ LocaleManager.updateResources(this)
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt
new file mode 100644
index 0000000000..7bdbf5cf87
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt
@@ -0,0 +1,145 @@
+/* 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.support.locale
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.content.res.Resources
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.os.ConfigurationCompat
+import mozilla.components.support.base.R
+import java.util.Locale
+
+/**
+ * Helper for apps that want to change locale defined by the system.
+ */
+object LocaleManager {
+ /**
+ * Change the system defined locale to the indicated in the [language] parameter.
+ * This new [language] will be stored and will be the new current locale returned by [getCurrentLocale].
+ *
+ * After calling this function, to visualize the locale changes you have to make sure all your visible activities
+ * get recreated. If your app is using the single activity approach, this will be trivial just call
+ * [AppCompatActivity.recreate]. On the other hand, if you have multiple activity this could be tricky, one
+ * alternative could be restarting your application process see https://github.com/JakeWharton/ProcessPhoenix
+ *
+ * @param context The [Context]
+ * @param localeUseCase The [LocaleUseCases] used to notify the [BrowserStore] of the [Locale] changes.
+ * @param language The new [Locale] that has been selected
+ * @return A new Context object for whose resources are adjusted to match the new [language].
+ */
+ fun setNewLocale(context: Context, localeUseCase: LocaleUseCases? = null, locale: Locale?): Context {
+ Storage.save(context, locale?.toLanguageTag())
+ notifyStore(locale, localeUseCase)
+
+ return updateResources(context)
+ }
+
+ /**
+ * The latest stored locale saved by [setNewLocale].
+ *
+ * @return The current selected locale. If the app is following the system default then this
+ * value will be null.
+ */
+ fun getCurrentLocale(context: Context): Locale? {
+ return Storage.getLocale(context)?.toLocale()
+ }
+
+ /**
+ * Change the current locale to the system defined one. As a result, [getCurrentLocale] will
+ * return null.
+ *
+ * After calling this function, to visualize the locale changes you have to make sure all your visible activities
+ * get recreated. If your app is using the single activity approach, this will be trivial just call
+ * [AppCompatActivity.recreate]. On the other hand, if you have multiple activity this could be tricky, one
+ * alternative could be restarting your application process see https://github.com/JakeWharton/ProcessPhoenix
+ *
+ */
+ fun resetToSystemDefault(context: Context, localeUseCase: LocaleUseCases?) {
+ clear(context)
+ val locale = getSystemDefault()
+
+ updateSystemLocale(locale)
+ updateConfiguration(context, locale)
+
+ notifyStore(locale, localeUseCase)
+ }
+
+ /**
+ * Returns the locale set by the system
+ */
+ fun getSystemDefault(): Locale {
+ val config = Resources.getSystem().configuration
+ return ConfigurationCompat.getLocales(config).get(0) ?: Locale.getDefault()
+ }
+
+ internal fun updateResources(baseContext: Context): Context {
+ val locale = getCurrentLocale(baseContext) ?: getSystemDefault()
+
+ updateSystemLocale(locale)
+ return updateConfiguration(baseContext, locale)
+ }
+
+ /**
+ * Notify the [BrowserStore] that the [Locale] has been changed via [LocaleUseCases].
+ */
+ private fun notifyStore(locale: Locale?, localeUseCase: LocaleUseCases?) {
+ localeUseCase?.let { useCases ->
+ useCases.notifyLocaleChanged(locale)
+ }
+ }
+
+ @SuppressLint("AppBundleLocaleChanges")
+ private fun updateConfiguration(context: Context, locale: Locale): Context {
+ val configuration = Configuration(context.resources.configuration)
+ configuration.setLocale(locale)
+ configuration.setLayoutDirection(locale)
+ return context.createConfigurationContext(configuration)
+ }
+
+ private fun updateSystemLocale(locale: Locale) {
+ Locale.setDefault(locale)
+ }
+
+ internal fun clear(context: Context) {
+ Storage.clear(context)
+ }
+
+ private object Storage {
+ private const val PREFERENCE_FILE = "mozac_support_base_locale_manager_preference"
+ private var currentLocal: String? = null
+
+ fun getLocale(context: Context): String? {
+ return if (currentLocal == null) {
+ val settings = getSharedPreferences(context)
+ val key = context.getString(R.string.mozac_support_base_locale_preference_key_locale)
+ currentLocal = settings.getString(key, null)
+ currentLocal
+ } else {
+ currentLocal
+ }
+ }
+
+ @Synchronized
+ fun save(context: Context, localeCode: String?) {
+ val settings = getSharedPreferences(context)
+ val key = context.getString(R.string.mozac_support_base_locale_preference_key_locale)
+ settings.edit().putString(key, localeCode).apply()
+ currentLocal = localeCode
+ }
+
+ fun clear(context: Context) {
+ val settings = getSharedPreferences(context)
+ settings.edit().clear().apply()
+ currentLocal = null
+ }
+
+ private fun getSharedPreferences(context: Context): SharedPreferences {
+ return context.getSharedPreferences(PREFERENCE_FILE, Context.MODE_PRIVATE)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleMiddleware.kt b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleMiddleware.kt
new file mode 100644
index 0000000000..095323ac31
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleMiddleware.kt
@@ -0,0 +1,64 @@
+/* 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.support.locale
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.LocaleAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Store
+import mozilla.components.support.base.log.logger.Logger
+import java.util.Locale
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * [Middleware] implementation for updating [BrowserState.locale] state changes during restore.
+ */
+class LocaleMiddleware(
+ private val applicationContext: Context,
+ coroutineContext: CoroutineContext = Dispatchers.IO,
+ private val localeManager: LocaleManager,
+) : Middleware<BrowserState, BrowserAction> {
+
+ private val logger = Logger("LocaleMiddleware")
+ private var scope = CoroutineScope(coroutineContext)
+
+ override fun invoke(
+ context: MiddlewareContext<BrowserState, BrowserAction>,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction,
+ ) {
+ when (action) {
+ is LocaleAction.RestoreLocaleStateAction -> restoreLocale(context.store)
+ is LocaleAction.UpdateLocaleAction -> updateLocale(action.locale)
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+ }
+
+ private fun restoreLocale(store: Store<BrowserState, BrowserAction>) = scope.launch {
+ val localeHistory = localeManager.getCurrentLocale(applicationContext)
+ if (localeHistory == null) {
+ logger.debug(
+ "No recoverable locale has been set. Following device locale.",
+ )
+ } else {
+ logger.debug("Locale restored from the storage $localeHistory")
+ }
+ store.dispatch(LocaleAction.UpdateLocaleAction(localeHistory))
+ }
+
+ private fun updateLocale(locale: Locale?) {
+ localeManager.setNewLocale(applicationContext, locale = locale)
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleUseCases.kt b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleUseCases.kt
new file mode 100644
index 0000000000..032c247ded
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleUseCases.kt
@@ -0,0 +1,53 @@
+/* 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.support.locale
+
+import mozilla.components.browser.state.action.LocaleAction
+import mozilla.components.browser.state.action.LocaleAction.UpdateLocaleAction
+import mozilla.components.browser.state.store.BrowserStore
+import java.util.Locale
+
+/**
+ * Contains use cases related to localization.
+ */
+class LocaleUseCases(browserStore: BrowserStore) {
+ /**
+ * Updates the [Locale] to the most recent user selection.
+ */
+ class UpdateLocaleUseCase internal constructor(
+ private val store: BrowserStore,
+ ) {
+ /**
+ * Updates the locale for [BrowserStore] observers.
+ *
+ * @param locale The [Locale] that has been selected.
+ */
+ operator fun invoke(locale: Locale?) {
+ store.dispatch(UpdateLocaleAction(locale))
+ }
+ }
+
+ /**
+ * Use case for restoring the [Locale].
+ */
+ class RestoreUseCase(
+ private val browserStore: BrowserStore,
+ ) {
+ /**
+ * Restores the given [Locale] from storage.
+ */
+ operator fun invoke() {
+ browserStore.dispatch(LocaleAction.RestoreLocaleStateAction)
+ }
+ }
+
+ val notifyLocaleChanged: UpdateLocaleUseCase by lazy {
+ UpdateLocaleUseCase(browserStore)
+ }
+
+ val restore: RestoreUseCase by lazy {
+ RestoreUseCase(browserStore)
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/ActivityContextWrapperTest.kt b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/ActivityContextWrapperTest.kt
new file mode 100644
index 0000000000..ea133a610b
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/ActivityContextWrapperTest.kt
@@ -0,0 +1,47 @@
+/* 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.support.locale
+
+import android.content.Context
+import android.view.ContextThemeWrapper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ActivityContextWrapperTest {
+ @Test
+ fun `WHEN a context has multiple wrappers THEN unwrap and return the original context`() {
+ // Acts as the original activity context.
+ val context: Context = testContext
+ // First wrapper that holds the original without modification.
+ val activityWrapper = ActivityContextWrapper(context)
+ // Second wrapper that may have created a new context from the previous one.
+ val themeWrapper = ContextThemeWrapper(activityWrapper, 0)
+
+ val retrieved = ActivityContextWrapper.getOriginalContext(themeWrapper)
+
+ assertEquals(context, retrieved)
+ assertNotEquals(themeWrapper, retrieved)
+ }
+
+ @Test
+ fun `WHEN there is no ActivityContextWrapper THEN return null`() {
+ // Acts as the original activity context.
+ val context: Context = testContext
+ // A wrapper that may have created a new context from the previous one.
+ val themeWrapper = ContextThemeWrapper(context, 0)
+
+ val retrieved = ActivityContextWrapper.getOriginalContext(themeWrapper)
+ val retrieved2 = ActivityContextWrapper.getOriginalContext(testContext)
+
+ assertNull(retrieved)
+ assertNull(retrieved2)
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleAwareAppCompatActivityTest.kt b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleAwareAppCompatActivityTest.kt
new file mode 100644
index 0000000000..b7dc56eaae
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleAwareAppCompatActivityTest.kt
@@ -0,0 +1,35 @@
+/* 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.support.locale
+
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class LocaleAwareAppCompatActivityTest {
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N, Build.VERSION_CODES.P])
+ fun `when version is not Android 8 don't set layoutDirection`() {
+ val activity = spy(Robolectric.buildActivity(LocaleAwareAppCompatActivity::class.java).get())
+ activity.setLayoutDirectionIfNeeded()
+ verify(activity, Mockito.times(0)).window
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O, Build.VERSION_CODES.O_MR1])
+ fun `when version is Android 8 set layoutDirection`() {
+ val activity = spy(Robolectric.buildActivity(LocaleAwareAppCompatActivity::class.java).get())
+ activity.setLayoutDirectionIfNeeded()
+ verify(activity, Mockito.times(1)).window
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt
new file mode 100644
index 0000000000..cddf2c1029
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt
@@ -0,0 +1,153 @@
+/* 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.support.locale
+
+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 org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+import java.util.Locale
+
+@RunWith(AndroidJUnit4::class)
+class LocaleManagerTest {
+
+ private lateinit var localeUseCases: LocaleUseCases
+
+ @Before
+ fun setup() {
+ LocaleManager.clear(testContext)
+
+ localeUseCases = mock()
+ whenever(localeUseCases.notifyLocaleChanged).thenReturn(mock())
+ }
+
+ @Test
+ @Config(qualifiers = "en-rUS")
+ fun `changing the language to Spanish must change the system locale to Spanish and change the configurations`() {
+ var currentLocale = LocaleManager.getCurrentLocale(testContext)
+
+ assertNull(currentLocale)
+
+ val newContext = LocaleManager.setNewLocale(testContext, localeUseCases, "es".toLocale())
+
+ assertNotEquals(testContext, newContext)
+
+ currentLocale = LocaleManager.getCurrentLocale(testContext)
+
+ assertEquals(currentLocale, "es".toLocale())
+ assertEquals(currentLocale, Locale.getDefault())
+ }
+
+ @Test
+ @Config(qualifiers = "en-rUS")
+ fun `when calling updateResources without a stored language we must not change the system locale`() {
+ val previousSystemLocale = Locale.getDefault()
+ LocaleManager.updateResources(testContext)
+
+ assertEquals(previousSystemLocale, Locale.getDefault())
+ }
+
+ @Test
+ @Config(qualifiers = "en-rUS")
+ fun `when resetting to system locale then the current locale will be the system one`() {
+ assertEquals("en_US".toLocale(), Locale.getDefault())
+
+ LocaleManager.setNewLocale(
+ testContext,
+ localeUseCases,
+ "fr".toLocale(),
+ )
+
+ val storedLocale = Locale.getDefault()
+
+ assertEquals("fr".toLocale(), Locale.getDefault())
+ assertEquals("fr".toLocale(), storedLocale)
+
+ LocaleManager.resetToSystemDefault(testContext, localeUseCases)
+
+ assertEquals("en_US".toLocale(), Locale.getDefault())
+ assertNull(LocaleManager.getCurrentLocale(testContext))
+ }
+
+ @Test
+ @Config(qualifiers = "en-rUS")
+ fun `when setting a new locale then the current locale will be different than the system locale`() {
+ assertEquals("en_US".toLocale(), Locale.getDefault())
+
+ LocaleManager.setNewLocale(testContext, localeUseCases, "fr".toLocale())
+
+ assertEquals("en_US".toLocale(), LocaleManager.getSystemDefault())
+ assertEquals("fr".toLocale(), LocaleManager.getCurrentLocale(testContext))
+ assertEquals("fr".toLocale(), Locale.getDefault())
+ }
+
+ @Test
+ @Config(qualifiers = "en-rUS")
+ fun `when setting a new locale then the store is notified via use case`() {
+ assertEquals("en_US".toLocale(), Locale.getDefault())
+
+ LocaleManager.setNewLocale(testContext, localeUseCases, "fr".toLocale())
+
+ verify(localeUseCases, times(1)).notifyLocaleChanged
+ }
+
+ @Test
+ @Config(qualifiers = "en-rUS")
+ fun `GIVEN the locale use cases are defined WHEN resetting to system default locale THEN the store is notified via use case`() {
+ assertEquals("en_US".toLocale(), Locale.getDefault())
+
+ LocaleManager.setNewLocale(
+ testContext,
+ localeUseCases,
+ "fr".toLocale(),
+ )
+
+ verify(localeUseCases, times(1)).notifyLocaleChanged
+
+ val storedLocale = Locale.getDefault()
+
+ assertEquals("fr".toLocale(), Locale.getDefault())
+ assertEquals("fr".toLocale(), storedLocale)
+
+ LocaleManager.resetToSystemDefault(testContext, localeUseCases)
+
+ verify(localeUseCases, times(2)).notifyLocaleChanged
+ }
+
+ @Test
+ @Config(qualifiers = "en-rUS")
+ fun `WHEN setting a new locale THEN locale use cases are used only when defined`() {
+ assertEquals("en_US".toLocale(), Locale.getDefault())
+
+ val locale1 = "fr".toLocale()
+ LocaleManager.setNewLocale(
+ testContext,
+ locale = locale1,
+ )
+
+ verify(localeUseCases, never()).notifyLocaleChanged
+ assertEquals(locale1, Locale.getDefault())
+
+ val locale2 = "es".toLocale()
+ LocaleManager.setNewLocale(
+ testContext,
+ localeUseCases,
+ locale2,
+ )
+
+ verify(localeUseCases, times(1)).notifyLocaleChanged
+ assertEquals(locale2, Locale.getDefault())
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleMiddlewareTest.kt b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleMiddlewareTest.kt
new file mode 100644
index 0000000000..c9a4430842
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleMiddlewareTest.kt
@@ -0,0 +1,99 @@
+/* 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.support.locale
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.action.LocaleAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class LocaleMiddlewareTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
+ @Before
+ fun setUp() {
+ LocaleManager.clear(testContext)
+ }
+
+ @Test
+ @Ignore("Failing intermittently. To be fixed for https://github.com/mozilla-mobile/android-components/issues/9954")
+ @Config(qualifiers = "en-rUS")
+ fun `GIVEN a locale has been chosen in the app WHEN we restore state THEN locale is retrieved from storage`() = runTest {
+ val localeManager = spy(LocaleManager)
+ val currentLocale = localeManager.getCurrentLocale(testContext)
+ assertNull(currentLocale)
+
+ val localeMiddleware = spy(
+ LocaleMiddleware(
+ testContext,
+ coroutineContext = dispatcher,
+ localeManager = localeManager,
+ ),
+ )
+
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(localeMiddleware),
+ )
+
+ assertEquals(store.state.locale, null)
+
+ store.dispatch(LocaleAction.RestoreLocaleStateAction).joinBlocking()
+ store.waitUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ assertEquals(store.state.locale, currentLocale)
+ }
+
+ @Test
+ @Config(qualifiers = "en-rUS")
+ fun `WHEN we update the locale THEN the locale manager is updated`() = runTest {
+ val localeManager = spy(LocaleManager)
+ val currentLocale = localeManager.getCurrentLocale(testContext)
+ assertNull(currentLocale)
+
+ val localeMiddleware = spy(
+ LocaleMiddleware(
+ testContext,
+ coroutineContext = dispatcher,
+ localeManager = localeManager,
+ ),
+ )
+
+ val store = BrowserStore(
+ initialState = BrowserState(),
+ middleware = listOf(localeMiddleware),
+ )
+
+ assertEquals(store.state.locale, null)
+
+ val newLocale = "es".toLocale()
+ store.dispatch(LocaleAction.UpdateLocaleAction(newLocale)).joinBlocking()
+ dispatcher.scheduler.advanceUntilIdle()
+
+ verify(localeManager).setNewLocale(testContext, locale = newLocale)
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleUseCasesTest.kt b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleUseCasesTest.kt
new file mode 100644
index 0000000000..9fd73486a3
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleUseCasesTest.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.support.locale
+
+import mozilla.components.browser.state.action.LocaleAction
+import mozilla.components.browser.state.action.LocaleAction.UpdateLocaleAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+import java.util.*
+
+class LocaleUseCasesTest {
+
+ private lateinit var browserStore: BrowserStore
+
+ @Before
+ fun setup() {
+ browserStore = mock()
+ }
+
+ @Test
+ fun `WHEN the locale is updated THEN the browser state reflects the change`() {
+ val useCases = LocaleUseCases(browserStore)
+ val locale = Locale("MyFavoriteLanguage")
+
+ useCases.notifyLocaleChanged(locale)
+
+ verify(browserStore).dispatch(UpdateLocaleAction(locale))
+ }
+
+ @Test
+ fun `WHEN state is restored THEN the browser state locale is restored`() {
+ val useCases = LocaleUseCases(browserStore)
+ useCases.restore()
+ verify(browserStore).dispatch(LocaleAction.RestoreLocaleStateAction)
+ }
+}
diff --git a/mobile/android/android-components/components/support/locale/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/support/locale/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/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/support/locale/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/locale/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/locale/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/remotesettings/README.md b/mobile/android/android-components/components/support/remotesettings/README.md
new file mode 100644
index 0000000000..c4600d3a32
--- /dev/null
+++ b/mobile/android/android-components/components/support/remotesettings/README.md
@@ -0,0 +1,18 @@
+# [Android Components](../../../README.md) > Support > Remotesettings
+
+This component defines and installs an application-services `remotesettings` package that:
+- Fetches [configuration?] data from Mozilla remote servers.
+
+### 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:support-remotesettings:{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/support/remotesettings/build.gradle b/mobile/android/android-components/components/support/remotesettings/build.gradle
new file mode 100644
index 0000000000..f723948cc0
--- /dev/null
+++ b/mobile/android/android-components/components/support/remotesettings/build.gradle
@@ -0,0 +1,59 @@
+/* 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/. */
+buildscript {
+ repositories {
+ gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
+ maven {
+ url repository
+ if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
+ allowInsecureProtocol = true
+ }
+ }
+ }
+ }
+ dependencies {
+ classpath ComponentsDependencies.plugin_serialization
+ }
+}
+
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+android {
+ compileSdkVersion config.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.support.remotesettings'
+}
+
+dependencies {
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation ComponentsDependencies.mozilla_remote_settings
+ implementation ComponentsDependencies.kotlin_json
+
+ testImplementation ComponentsDependencies.testing_mockito
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.mozilla_remote_settings
+}
+
+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/support/remotesettings/proguard-rules.pro b/mobile/android/android-components/components/support/remotesettings/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/mobile/android/android-components/components/support/remotesettings/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 \ No newline at end of file
diff --git a/mobile/android/android-components/components/support/remotesettings/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/remotesettings/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/remotesettings/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/support/remotesettings/src/main/java/mozilla/components/support/remotesettings/RemoteSettingsClient.kt b/mobile/android/android-components/components/support/remotesettings/src/main/java/mozilla/components/support/remotesettings/RemoteSettingsClient.kt
new file mode 100644
index 0000000000..c9e448e8a2
--- /dev/null
+++ b/mobile/android/android-components/components/support/remotesettings/src/main/java/mozilla/components/support/remotesettings/RemoteSettingsClient.kt
@@ -0,0 +1,265 @@
+/* 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.support.remotesettings
+
+import android.util.AtomicFile
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.Json
+import mozilla.appservices.remotesettings.Attachment
+import mozilla.appservices.remotesettings.RemoteSettings
+import mozilla.appservices.remotesettings.RemoteSettingsConfig
+import mozilla.appservices.remotesettings.RemoteSettingsException
+import mozilla.appservices.remotesettings.RemoteSettingsRecord
+import mozilla.appservices.remotesettings.RemoteSettingsResponse
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.util.writeString
+import org.json.JSONObject
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.net.URL
+
+/**
+ * Helper class to download collections from remote settings in app services.
+ *
+ * @property storageRootDirectory The top level app-local storage directory.
+ * @property serverUrl The optional url for the settings server. If not specified, the standard server will be used.
+ * @property bucketName The optional name of the bucket containing the collection on the server.
+ * If not specified, the name of the bucket "main" [or the const if updated] will be used.
+ * @property collectionName The name of the collection for the settings server.
+ */
+
+class RemoteSettingsClient(
+ private val storageRootDirectory: File,
+ private val serverUrl: String = "https://firefox.settings.services.mozilla.com",
+ private val bucketName: String = "main",
+ private val collectionName: String,
+) {
+
+ private val config = RemoteSettingsConfig(
+ serverUrl = serverUrl,
+ bucketName = bucketName,
+ collectionName = collectionName,
+ )
+ private val serverHostName = URL(serverUrl).host
+ private val path = "${storageRootDirectory.path}/$serverHostName/$bucketName/$collectionName"
+
+ @VisibleForTesting
+ internal var file = File(path)
+
+ /**
+ * Fetches a response that includes Remote Settings records and the last time the collection was modified.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ suspend fun fetch(): RemoteSettingsResult = withContext(Dispatchers.IO) {
+ try {
+ val serverRecords = RemoteSettings(config).use {
+ it.getRecords()
+ }
+ RemoteSettingsResult.Success(serverRecords)
+ } catch (e: RemoteSettingsException) {
+ Logger.error(e.message.toString())
+ RemoteSettingsResult.NetworkFailure(e)
+ } catch (e: NullPointerException) {
+ Logger.error(e.message.toString())
+ RemoteSettingsResult.NetworkFailure(e)
+ }
+ }
+
+ /**
+ * Updates the local storage with [response] data.
+ * The full response is required because the `lastModified` property may be used for future comparisons.
+ */
+ suspend fun write(response: RemoteSettingsResponse): RemoteSettingsResult = withContext(Dispatchers.IO) {
+ try {
+ val jsonString = Json.encodeToString(
+ SerializableRemoteSettingsResponse.serializer(),
+ response.toSerializable(),
+ )
+ AtomicFile(file).writeString { jsonString }
+ RemoteSettingsResult.Success(response)
+ } catch (e: IOException) {
+ RemoteSettingsResult.DiskFailure(e)
+ } catch (e: SerializationException) {
+ RemoteSettingsResult.DiskFailure(e)
+ }
+ }
+
+ /**
+ * Reads all response for a collection found in the local storage.
+ */
+ suspend fun read(): RemoteSettingsResult = withContext(Dispatchers.IO) {
+ try {
+ if (!file.exists()) {
+ RemoteSettingsResult.DiskFailure(FileNotFoundException("File not found"))
+ } else {
+ val jsonString = file.readText()
+ val response = Json.decodeFromString<SerializableRemoteSettingsResponse>(jsonString)
+ RemoteSettingsResult.Success(response.toRemoteSettingsResponse())
+ }
+ } catch (e: IOException) {
+ RemoteSettingsResult.DiskFailure(e)
+ } catch (e: SerializationException) {
+ RemoteSettingsResult.DiskFailure(e)
+ }
+ }
+}
+
+/**
+ * Reads files from local storage. If file is empty, fetches the file from the remote server.
+ */
+suspend fun RemoteSettingsClient.readOrFetch(): RemoteSettingsResult {
+ val readResult = read()
+ return if (readResult is RemoteSettingsResult.DiskFailure) {
+ fetch()
+ } else {
+ readResult
+ }
+}
+
+/**
+ * Fetches files from remote servers. If file retrieved successfully, updates the local storage.
+ */
+suspend fun RemoteSettingsClient.fetchAndWrite(): RemoteSettingsResult {
+ val fetchResult = fetch()
+ return if (fetchResult is RemoteSettingsResult.Success) {
+ write(fetchResult.response)
+ } else {
+ fetchResult
+ }
+}
+
+/**
+ * Base class for different result states of remote settings operations.
+ */
+sealed class RemoteSettingsResult {
+
+ /**
+ * Represents a successful outcome of a remote settings operation.
+ */
+ data class Success(val response: RemoteSettingsResponse) : RemoteSettingsResult()
+
+ /**
+ * Represents a failure due to issues related to local storage (e.g., file not found, file format error).
+ */
+ data class DiskFailure(val error: Exception) : RemoteSettingsResult()
+
+ /**
+ * Represents a failure due to network-related issues during remote settings operation
+ * (e.g., network not available, server error).
+ */
+ data class NetworkFailure(val error: Exception) : RemoteSettingsResult()
+}
+
+/**
+ * Data class representing serializable version of RemoteSettingsResponse.
+ */
+@Serializable
+private data class SerializableRemoteSettingsResponse(
+ val records: List<SerializableRemoteSettingsRecord>,
+ val lastModified: ULong,
+)
+
+/**
+ * Data class representing serializable version of RemoteSettingsRecord.
+ */
+@Serializable
+private data class SerializableRemoteSettingsRecord(
+ val id: String,
+ val lastModified: ULong,
+ val deleted: Boolean,
+ val attachment: SerializableAttachment?,
+ @Serializable(with = JSONObjectSerializer::class)
+ val fields: JSONObject,
+)
+
+/**
+ * Data class representing serializable version of (RemoteSettings) Attachment.
+ */
+@Serializable
+private data class SerializableAttachment(
+ val filename: String,
+ val mimetype: String,
+ val location: String,
+ val hash: String,
+ val size: ULong,
+)
+
+private object JSONObjectSerializer : KSerializer<JSONObject> {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JSONObject", PrimitiveKind.STRING)
+
+ override fun serialize(encoder: Encoder, value: JSONObject) {
+ encoder.encodeString(value.toString())
+ }
+
+ override fun deserialize(decoder: Decoder): JSONObject {
+ val jsonString = decoder.decodeString()
+ return JSONObject(jsonString)
+ }
+}
+
+private fun RemoteSettingsRecord.toSerializable(): SerializableRemoteSettingsRecord {
+ return SerializableRemoteSettingsRecord(
+ id = this.id,
+ lastModified = this.lastModified,
+ deleted = this.deleted,
+ attachment = this.attachment?.toSerializable(),
+ fields = this.fields,
+ )
+}
+
+private fun RemoteSettingsResponse.toSerializable(): SerializableRemoteSettingsResponse {
+ return SerializableRemoteSettingsResponse(
+ records = this.records.map { it.toSerializable() },
+ lastModified = this.lastModified,
+ )
+}
+
+private fun Attachment.toSerializable(): SerializableAttachment {
+ return SerializableAttachment(
+ filename = this.filename,
+ mimetype = this.mimetype,
+ location = this.location,
+ hash = this.hash,
+ size = this.size,
+ )
+}
+
+private fun SerializableRemoteSettingsResponse.toRemoteSettingsResponse(): RemoteSettingsResponse {
+ return RemoteSettingsResponse(
+ records = this.records.map { it.toRemoteSettingsRecord() },
+ lastModified = this.lastModified,
+ )
+}
+
+private fun SerializableRemoteSettingsRecord.toRemoteSettingsRecord(): RemoteSettingsRecord {
+ return RemoteSettingsRecord(
+ id = this.id,
+ lastModified = this.lastModified,
+ deleted = this.deleted,
+ attachment = this.attachment?.toAttachment(),
+ fields = this.fields,
+ )
+}
+
+private fun SerializableAttachment.toAttachment(): Attachment {
+ return Attachment(
+ filename = this.filename,
+ mimetype = this.mimetype,
+ location = this.location,
+ hash = this.hash,
+ size = this.size,
+ )
+}
diff --git a/mobile/android/android-components/components/support/remotesettings/src/test/java/mozilla/components/support/remotesettings/RemoteSettingsClientTest.kt b/mobile/android/android-components/components/support/remotesettings/src/test/java/mozilla/components/support/remotesettings/RemoteSettingsClientTest.kt
new file mode 100644
index 0000000000..e6b153a9a4
--- /dev/null
+++ b/mobile/android/android-components/components/support/remotesettings/src/test/java/mozilla/components/support/remotesettings/RemoteSettingsClientTest.kt
@@ -0,0 +1,230 @@
+package mozilla.components.support.remotesettings
+
+import kotlinx.coroutines.runBlocking
+import mozilla.appservices.remotesettings.RemoteSettings
+import mozilla.appservices.remotesettings.RemoteSettingsRecord
+import mozilla.appservices.remotesettings.RemoteSettingsResponse
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+import org.robolectric.RobolectricTestRunner
+import java.io.File
+import java.io.FileReader
+import java.io.IOException
+
+@RunWith(RobolectricTestRunner::class)
+class RemoteSettingsClientTest {
+
+ private lateinit var mockRemoteSettings: RemoteSettings
+ private lateinit var mockFile: File
+ private lateinit var mockFileReader: FileReader
+ private lateinit var mockJsonObject: JSONObject
+ private lateinit var remoteSettingsClient: RemoteSettingsClient
+
+ @get:Rule
+ val tempFolder = TemporaryFolder()
+
+ @Before
+ fun setUp() {
+ mockFile = mock()
+ mockFileReader = mock()
+ mockJsonObject = mock()
+ mockRemoteSettings = mock()
+ remoteSettingsClient = RemoteSettingsClient(File(""), "https://firefox.settings.services.mozilla.com", "", "")
+ }
+
+ @Test
+ fun `GIVEN non-empty records WHEN write is called THEN the result is a success`() {
+ val tempFile = File.createTempFile("test", ".json")
+ val records = listOf(
+ RemoteSettingsRecord("1", 123u, false, null, JSONObject()),
+ RemoteSettingsRecord("2", 456u, true, null, JSONObject()),
+ )
+ val response = RemoteSettingsResponse(records, 125614567U)
+ val result = runBlocking { RemoteSettingsClient(tempFile, collectionName = "test").write(response) }
+
+ require(result is RemoteSettingsResult.Success) { "Result should be Success" }
+ assertEquals(response, result.response)
+
+ tempFile.delete()
+ }
+
+ @Test
+ fun `GIVEN non-empty records WHEN write is called THEN read text from file is same as write text to file`() {
+ val path = "test.com/test/test"
+ tempFolder.newFolder("test.com", "test")
+ val file = tempFolder.newFile(path)
+ val client = RemoteSettingsClient(tempFolder.root, "https://test.com", "test", "test")
+ client.file = file
+ val jsonObject = JSONObject(
+ mapOf(
+ "schema" to 1698656464939,
+ "taggedCodes" to JSONArray(
+ listOf(
+ "monline_dg",
+ "monline_3_dg",
+ "monline_4_dg",
+ "monline_7_dg",
+ ),
+ ),
+ "telemetryId" to "baidu",
+ "organicCodes" to JSONArray(emptyList<String>()),
+ "codeParamName" to "tn",
+ "queryParamName" to "wd",
+ "queryParamNames" to JSONArray(
+ listOf(
+ "wd",
+ "word",
+ ),
+ ),
+ "searchPageRegexp" to "^https://(?:m|www)\\.baidu\\.com/(?:s|baidu)",
+ "followOnParamNames" to JSONArray(
+ listOf(
+ "oq",
+ ),
+ ),
+ "extraAdServersRegexps" to JSONArray(
+ listOf(
+ "^https?://www\\.baidu\\.com/baidu\\.php?",
+ ),
+ ),
+ "id" to "19c434a3-d173-4871-9743-290ac92a3f6a",
+ "last_modified" to 1698666532326,
+ ),
+ )
+ val records = listOf(
+ RemoteSettingsRecord("1", 123u, false, null, jsonObject),
+ )
+ val response = RemoteSettingsResponse(records, 125614567U)
+
+ runBlocking {
+ client.write(response)
+ val result = client.read()
+
+ assertEquals(response.records.first().id, (result as RemoteSettingsResult.Success).response.records.first().id)
+ assertEquals(response.records.first().attachment, result.response.records.first().attachment)
+ assertEquals(response.records.first().fields.getJSONArray("followOnParamNames"), result.response.records.first().fields.getJSONArray("followOnParamNames"))
+ }
+ }
+
+ @Test
+ fun `GIVEN non-empty records WHEN read is called THEN result content has top-level fields and arbitrarily nested fields`() {
+ val path = "test.com/test/test"
+ tempFolder.newFolder("test.com", "test")
+ val file = tempFolder.newFile(path)
+ val client = RemoteSettingsClient(tempFolder.root, "https://test.com", "test", "test")
+ file.writeText(
+ """
+{
+ "records": [
+ {
+ "id": "1",
+ "lastModified": 123,
+ "deleted": false,
+ "attachment": null,
+ "fields": "{
+ \"codeParamName\":\"tt\",
+ \"components\":[
+ {\"included\":{\"children\":[{\"countChildren\":true,\"selector\":\".product-ads-carousel__item\"}], \"parent\":{\"selector\":\".product-ads-carousel\"}, \"related\":{\"selector\":\".snippet__control\"}},\"type\":\"ad_carousel\"},
+ {\"included\":{\"children\":[{\"selector\":\".result__extra-content .deep-links--descriptions\",\"type\":\"ad_sitelink\"}], \"parent\":{\"selector\":\".ad-result\"}},\"type\":\"ad_link\"},
+ {\"included\":{\"children\":[{\"selector\":\".search-form__input, .search-form__submit\"}],\"parent\":{\"selector\":\"form.search-form\"},\"related\":{\"selector\":\".search-form__suggestions\"}},\"topDown\":true,\"type\":\"incontent_searchbox\"},
+ {\"default\":true,\"type\":\"ad_link\"}
+ ],
+ \"expectedOrganicCodes\":[],
+ \"extraAdServersRegexps\":[\"^https:\\/\\/www\\\\.bing\\\\.com\\/acli?c?k\"],\"filter_expression\":\"env.version|versionCompare(\\\"110.0a1\\\")>=0\",
+ \"organicCodes\":[],
+ \"queryParamName\":\"q\",
+ \"queryParamNames\":[\"q\"],
+ \"schema\":1698656463945,
+ \"searchPageRegexp\":\"^https:\\/\\/www\\\\.ecosia\\\\.org\\/\",\"shoppingTab\":{\"regexp\":\"\\/shopping?\",\"selector\":\"nav li[data-test-id='search-navigation-item-shopping'] a\"},
+ \"taggedCodes\":[\"mzl\",\"813cf1dd\",\"16eeffc4\"],
+ \"telemetryId\":\"ecosia\"
+ }"
+ }
+ ],
+ "lastModified": 123
+}
+"""
+ .trimIndent(),
+ )
+
+ runBlocking {
+ val result = client.read()
+ assertEquals("1", (result as RemoteSettingsResult.Success).response.records.first().id)
+ assertEquals("ad_carousel", (result.response.records.first().fields.getJSONArray("components").get(0) as JSONObject).getString("type"))
+ }
+ }
+
+ @Test
+ fun `GIVEN non-empty records WHEN fetchAndWrite is called THEN the result is a success`() {
+ val client = mock<RemoteSettingsClient>()
+ val records = listOf(RemoteSettingsRecord("1", 123u, false, null, JSONObject()))
+ val response = RemoteSettingsResponse(records, 125614567U)
+ val fetchResult = RemoteSettingsResult.Success(response)
+ val writeResult = RemoteSettingsResult.Success(response)
+
+ runBlocking {
+ `when`(client.fetch()).thenReturn(fetchResult)
+ `when`(client.write(response)).thenReturn(writeResult)
+
+ val result = client.fetchAndWrite()
+
+ assertEquals(writeResult, result)
+ }
+ }
+
+ @Test
+ fun `GIVEN fetch failure WHEN fetchAndWrite is called THEN the result is a failure`() {
+ val client = mock<RemoteSettingsClient>()
+ val fetchResult = RemoteSettingsResult.NetworkFailure(IOException("Network error"))
+
+ runBlocking {
+ `when`(client.fetch()).thenReturn(fetchResult)
+
+ val result = client.fetchAndWrite()
+
+ assertEquals(fetchResult, result)
+ }
+ }
+
+ @Test
+ fun `GIVEN read success WHEN readOrFetch is called THEN the result is a success`() {
+ val client = mock<RemoteSettingsClient>()
+ val records = emptyList<RemoteSettingsRecord>()
+ val response = RemoteSettingsResponse(records, 125614567U)
+ val readResult = RemoteSettingsResult.Success(response)
+
+ runBlocking {
+ `when`(client.read()).thenReturn(readResult)
+
+ val result = client.readOrFetch()
+
+ assertEquals(readResult, result)
+ }
+ }
+
+ @Test
+ fun `GIVEN read failure, fetch success WHEN readOrFetch is called THEN the result is a success`() {
+ val client = mock<RemoteSettingsClient>()
+ val records = emptyList<RemoteSettingsRecord>()
+ val response = RemoteSettingsResponse(records, 125614567U)
+ val readResult = RemoteSettingsResult.DiskFailure(IOException("Disk error"))
+ val fetchResult = RemoteSettingsResult.Success(response)
+
+ runBlocking {
+ `when`(client.read()).thenReturn(readResult)
+ `when`(client.fetch()).thenReturn(fetchResult)
+
+ val result = client.readOrFetch()
+
+ assertEquals(fetchResult, result)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/remotesettings/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/support/remotesettings/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/support/remotesettings/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/support/remotesettings/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/remotesettings/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/remotesettings/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/rusterrors/README.md b/mobile/android/android-components/components/support/rusterrors/README.md
new file mode 100644
index 0000000000..d1fa8cafcb
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusterrors/README.md
@@ -0,0 +1,5 @@
+Handles errors that come from Rust functions.
+
+This component defines and installs an application-services `ApplicationErrorReporter` class that:
+ - Forwords error reports and breadcrumbs to `SentryServices`
+ - Reports error counts to Glean
diff --git a/mobile/android/android-components/components/support/rusterrors/build.gradle b/mobile/android/android-components/components/support/rusterrors/build.gradle
new file mode 100644
index 0000000000..f11ef03f18
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusterrors/build.gradle
@@ -0,0 +1,39 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ consumerProguardFiles 'proguard-rules-consumer.pro'
+ }
+ }
+
+ namespace 'mozilla.components.support.rusterrors'
+}
+
+dependencies {
+ implementation ComponentsDependencies.mozilla_appservices_errorsupport
+
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation project(':support-base')
+}
+
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/support/rusterrors/proguard-rules-consumer.pro b/mobile/android/android-components/components/support/rusterrors/proguard-rules-consumer.pro
new file mode 100644
index 0000000000..d3456cd17e
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusterrors/proguard-rules-consumer.pro
@@ -0,0 +1 @@
+# ProGuard rules for consumers of this library.
diff --git a/mobile/android/android-components/components/support/rusterrors/proguard-rules.pro b/mobile/android/android-components/components/support/rusterrors/proguard-rules.pro
new file mode 100644
index 0000000000..50e2b38a97
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusterrors/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/sebastian/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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/support/rusterrors/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/rusterrors/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusterrors/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/support/rusterrors/src/main/java/mozilla/components/support/rusterrors/RustErrors.kt b/mobile/android/android-components/components/support/rusterrors/src/main/java/mozilla/components/support/rusterrors/RustErrors.kt
new file mode 100644
index 0000000000..2a5a093a66
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusterrors/src/main/java/mozilla/components/support/rusterrors/RustErrors.kt
@@ -0,0 +1,39 @@
+/* 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.support.rusterrors
+
+import mozilla.appservices.errorsupport.ApplicationErrorReporter
+import mozilla.appservices.errorsupport.setApplicationErrorReporter
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.base.crash.RustCrashReport
+
+/**
+ * Initialize application services error reporting
+ *
+ * Errors reports and breadcrumbs from Application Services will be forwarded
+ * to the CrashReporting instance. Error counting, which is used for expected
+ * errors like network errors, will be counted with Glean.
+ */
+public fun initializeRustErrors(crashReporter: CrashReporting) {
+ setApplicationErrorReporter(AndroidComponentsErrorReportor(crashReporter))
+}
+
+internal class AppServicesErrorReport(
+ override val typeName: String,
+ override val message: String,
+) : Exception(typeName), RustCrashReport
+
+private class AndroidComponentsErrorReportor(
+ val crashReporter: CrashReporting,
+) : ApplicationErrorReporter {
+ override fun reportError(typeName: String, message: String) {
+ crashReporter.submitCaughtException(AppServicesErrorReport(typeName, message))
+ }
+
+ override fun reportBreadcrumb(message: String, module: String, line: UInt, column: UInt) {
+ crashReporter.recordCrashBreadcrumb(Breadcrumb("$module[$line]: $message"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/rusthttp/README.md b/mobile/android/android-components/components/support/rusthttp/README.md
new file mode 100644
index 0000000000..6bde2625c4
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusthttp/README.md
@@ -0,0 +1,7 @@
+# Rust HTTP
+
+This essentially wraps the rust HTTP config library so that consumers who don't
+use a custom megazord don't have to depend on application-services code.
+
+It's separate from RustLog since it's plausible users might only want to
+initialize logging, and not use any app-services network functionality.
diff --git a/mobile/android/android-components/components/support/rusthttp/build.gradle b/mobile/android/android-components/components/support/rusthttp/build.gradle
new file mode 100644
index 0000000000..b62bba28f2
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusthttp/build.gradle
@@ -0,0 +1,42 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ consumerProguardFiles 'proguard-rules-consumer.pro'
+ }
+ }
+
+ namespace 'mozilla.components.support.rusthttp'
+}
+
+dependencies {
+ implementation (ComponentsDependencies.mozilla_appservices_httpconfig) {
+ // Override the version of concept-fetch that A-S depends on,
+ // since we want to replace it with our own.
+ exclude group: 'org.mozilla.components', module: 'concept-fetch'
+ }
+ api project(':concept-fetch')
+}
+
+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/support/rusthttp/proguard-rules-consumer.pro b/mobile/android/android-components/components/support/rusthttp/proguard-rules-consumer.pro
new file mode 100644
index 0000000000..d3456cd17e
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusthttp/proguard-rules-consumer.pro
@@ -0,0 +1 @@
+# ProGuard rules for consumers of this library.
diff --git a/mobile/android/android-components/components/support/rusthttp/proguard-rules.pro b/mobile/android/android-components/components/support/rusthttp/proguard-rules.pro
new file mode 100644
index 0000000000..50e2b38a97
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusthttp/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/sebastian/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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/support/rusthttp/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/rusthttp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusthttp/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/support/rusthttp/src/main/java/mozilla/components/support/rusthttp/RustHttpConfig.kt b/mobile/android/android-components/components/support/rusthttp/src/main/java/mozilla/components/support/rusthttp/RustHttpConfig.kt
new file mode 100644
index 0000000000..bd1b3cadbd
--- /dev/null
+++ b/mobile/android/android-components/components/support/rusthttp/src/main/java/mozilla/components/support/rusthttp/RustHttpConfig.kt
@@ -0,0 +1,38 @@
+/* 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.support.rusthttp
+
+import mozilla.components.concept.fetch.Client
+import mozilla.appservices.httpconfig.RustHttpConfig as AppSvcHttpConfig
+
+/**
+ * An object allowing configuring the HTTP client used by Rust code.
+ */
+object RustHttpConfig {
+
+ /**
+ * Set the HTTP client to be used by all Rust code.
+ *
+ * The `Lazy`'s value is not read until the first request is made.
+ *
+ * This must be called
+ * - after initializing a megazord for users using a custom megazord build.
+ * - before any other calls into application-services rust code which make HTTP requests.
+ */
+ fun setClient(c: Lazy<Client>) {
+ AppSvcHttpConfig.setClient(c)
+ }
+
+ /**
+ * Allows connections to the hard-coded address the Android Emulator uses
+ * to connect to the emulator's host (ie, http://10.0.2.2).
+ *
+ * Only call this in debug builds or if you are sure you are running on an emulator. If this is
+ * not called, viaduct will fail to use that address as it isn't https.
+ */
+ fun allowEmulatorLoopback() {
+ AppSvcHttpConfig.allowAndroidEmulatorLoopback()
+ }
+}
diff --git a/mobile/android/android-components/components/support/rustlog/README.md b/mobile/android/android-components/components/support/rustlog/README.md
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/components/support/rustlog/README.md
diff --git a/mobile/android/android-components/components/support/rustlog/build.gradle b/mobile/android/android-components/components/support/rustlog/build.gradle
new file mode 100644
index 0000000000..54b70c37d2
--- /dev/null
+++ b/mobile/android/android-components/components/support/rustlog/build.gradle
@@ -0,0 +1,50 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ consumerProguardFiles 'proguard-rules-consumer.pro'
+ }
+ }
+
+ namespace 'mozilla.components.support.rustlog'
+}
+
+dependencies {
+ implementation ComponentsDependencies.mozilla_appservices_rust_log_forwarder
+
+ implementation ComponentsDependencies.kotlin_coroutines
+ // Log.Priority is in the public api.
+ api project(':support-base')
+
+ testImplementation ComponentsDependencies.mozilla_appservices_rust_log_forwarder
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.mozilla_appservices_full_megazord_forUnitTests
+
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/support/rustlog/proguard-rules-consumer.pro b/mobile/android/android-components/components/support/rustlog/proguard-rules-consumer.pro
new file mode 100644
index 0000000000..d3456cd17e
--- /dev/null
+++ b/mobile/android/android-components/components/support/rustlog/proguard-rules-consumer.pro
@@ -0,0 +1 @@
+# ProGuard rules for consumers of this library.
diff --git a/mobile/android/android-components/components/support/rustlog/proguard-rules.pro b/mobile/android/android-components/components/support/rustlog/proguard-rules.pro
new file mode 100644
index 0000000000..50e2b38a97
--- /dev/null
+++ b/mobile/android/android-components/components/support/rustlog/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/sebastian/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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/support/rustlog/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/rustlog/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/rustlog/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/support/rustlog/src/main/java/mozilla/components/support/rustlog/RustLog.kt b/mobile/android/android-components/components/support/rustlog/src/main/java/mozilla/components/support/rustlog/RustLog.kt
new file mode 100644
index 0000000000..fb2cfdaac3
--- /dev/null
+++ b/mobile/android/android-components/components/support/rustlog/src/main/java/mozilla/components/support/rustlog/RustLog.kt
@@ -0,0 +1,91 @@
+/* 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.support.rustlog
+
+import androidx.annotation.VisibleForTesting
+import mozilla.appservices.rust_log_forwarder.AppServicesLogger
+import mozilla.appservices.rust_log_forwarder.Level
+import mozilla.appservices.rust_log_forwarder.Record
+import mozilla.appservices.rust_log_forwarder.setLogger
+import mozilla.appservices.rust_log_forwarder.setMaxLevel
+import mozilla.components.support.base.log.Log
+
+internal class RustErrorException(tag: String?, msg: String) : Exception("$tag - $msg")
+
+object RustLog {
+
+ /**
+ * Enable the Rust log adapter.
+ *
+ * After calling this, logs emitted by Rust code are forwarded to any
+ * LogSinks attached to [Log].
+ */
+ fun enable() {
+ setLogger(ForwardOnLog())
+ }
+
+ /**
+ * Disable the rust log adapter.
+ */
+ fun disable() {
+ setLogger(null)
+ }
+
+ /**
+ * Set the maximum level of logs that will be forwarded to [Log]. By
+ * default, the max level is DEBUG.
+ *
+ * This is somewhat redundant with [Log.logLevel] (and a stricter
+ * filter on Log.logLevel will take precedence here), however
+ * setting the max level here can improve performance a great deal,
+ * as it allows the Rust code to skip a great deal of work.
+ *
+ * This includes a `includePII` flag, which allows enabling logs at
+ * the trace level. It is ignored if level is not [Log.Priority.DEBUG].
+ * These trace level logs* may contain the personal information of users
+ * but can be very helpful for tracking down bugs.
+ *
+ * @param level The maximum (inclusive) level to include logs at.
+ * @param includePII If `level` is [Log.Priority.DEBUG], allow
+ * debug logs to contain PII.
+ */
+ fun setMaxLevel(priority: Log.Priority, includePII: Boolean = false) {
+ setMaxLevel(priority.asLevel(includePII))
+ }
+}
+
+@VisibleForTesting
+internal class ForwardOnLog : AppServicesLogger {
+ override fun log(record: Record) {
+ Log.log(record.level.asLogPriority(), record.target, null, record.message)
+ }
+}
+
+@VisibleForTesting
+internal fun Log.Priority.asLevel(includePII: Boolean): Level {
+ return when (this) {
+ Log.Priority.DEBUG -> {
+ if (includePII) {
+ Level.TRACE
+ } else {
+ Level.DEBUG
+ }
+ }
+ Log.Priority.INFO -> Level.INFO
+ Log.Priority.WARN -> Level.WARN
+ Log.Priority.ERROR -> Level.ERROR
+ }
+}
+
+internal fun Level.asLogPriority(): Log.Priority {
+ return when (this) {
+ // No direct mapping for TRACE, but DEBUG is the closest
+ Level.TRACE -> Log.Priority.DEBUG
+ Level.DEBUG -> Log.Priority.DEBUG
+ Level.INFO -> Log.Priority.INFO
+ Level.WARN -> Log.Priority.WARN
+ Level.ERROR -> Log.Priority.ERROR
+ }
+}
diff --git a/mobile/android/android-components/components/support/rustlog/src/test/java/mozilla/components/support/rustlog/RustLogTest.kt b/mobile/android/android-components/components/support/rustlog/src/test/java/mozilla/components/support/rustlog/RustLogTest.kt
new file mode 100644
index 0000000000..c6eb512bfb
--- /dev/null
+++ b/mobile/android/android-components/components/support/rustlog/src/test/java/mozilla/components/support/rustlog/RustLogTest.kt
@@ -0,0 +1,71 @@
+/* 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.support.rustlog
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.Job
+import mozilla.appservices.rust_log_forwarder.Level
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RustLogTest {
+ @Test
+ fun `basic RustLog interactions do not blow up`() {
+ RustLog.enable()
+ RustLog.setMaxLevel(Log.Priority.DEBUG, false)
+ RustLog.setMaxLevel(Log.Priority.DEBUG, true)
+ RustLog.setMaxLevel(Log.Priority.INFO, false)
+ RustLog.setMaxLevel(Log.Priority.INFO, true)
+ RustLog.setMaxLevel(Log.Priority.WARN, false)
+ RustLog.setMaxLevel(Log.Priority.WARN, true)
+ RustLog.setMaxLevel(Log.Priority.ERROR, false)
+ RustLog.setMaxLevel(Log.Priority.ERROR, true)
+ RustLog.disable()
+ RustLog.enable()
+ }
+
+ @Test
+ fun `log priority to level filter`() {
+ assertEquals(Level.DEBUG, Log.Priority.DEBUG.asLevel(false))
+ assertEquals(Level.TRACE, Log.Priority.DEBUG.asLevel(true))
+
+ assertEquals(Level.INFO, Log.Priority.INFO.asLevel(false))
+ assertEquals(Level.INFO, Log.Priority.INFO.asLevel(true))
+
+ assertEquals(Level.WARN, Log.Priority.WARN.asLevel(false))
+ assertEquals(Level.WARN, Log.Priority.WARN.asLevel(true))
+
+ assertEquals(Level.ERROR, Log.Priority.ERROR.asLevel(false))
+ assertEquals(Level.ERROR, Log.Priority.ERROR.asLevel(true))
+ }
+
+ private class TestCrashReporter :
+ CrashReporting {
+ val exceptions: MutableList<Throwable> = mutableListOf()
+
+ override fun submitCaughtException(throwable: Throwable): Job {
+ exceptions.add(throwable)
+ return mock()
+ }
+
+ override fun recordCrashBreadcrumb(breadcrumb: Breadcrumb) {
+ fail()
+ }
+
+ fun assertLastException(expectedCount: Int, msg: String) {
+ assertEquals(expectedCount, exceptions.size)
+ assertTrue(exceptions.last() is RustErrorException)
+ assertEquals(msg, exceptions.last().message)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/rustlog/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/rustlog/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/rustlog/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/test-appservices/README.md b/mobile/android/android-components/components/support/test-appservices/README.md
new file mode 100644
index 0000000000..3935862b80
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-appservices/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Test Appservices
+
+A component for synchronizing Application Services' unit testing dependencies used in Android Components.
+
+## Usage
+
+### 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:support-test-appservices:{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/support/test-appservices/build.gradle b/mobile/android/android-components/components/support/test-appservices/build.gradle
new file mode 100644
index 0000000000..d2254c7cb3
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-appservices/build.gradle
@@ -0,0 +1,37 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ namespace 'mozilla.components.support.test.appservices'
+}
+
+dependencies {
+ api ComponentsDependencies.mozilla_appservices_full_megazord_forUnitTests
+}
+
+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/support/test-appservices/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/test-appservices/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-appservices/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/support/test-fakes/README.md b/mobile/android/android-components/components/support/test-fakes/README.md
new file mode 100644
index 0000000000..68d2f3e190
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-fakes/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Test Fakes
+
+A collection of fake implementations for testing purposes.
+
+## Usage
+
+### 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:support-test-fakes:{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/support/test-fakes/build.gradle b/mobile/android/android-components/components/support/test-fakes/build.gradle
new file mode 100644
index 0000000000..cd2c758cb9
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-fakes/build.gradle
@@ -0,0 +1,38 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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')
+ }
+ }
+
+ namespace 'mozilla.components.support.test.fakes'
+}
+
+dependencies {
+ implementation project(":concept-engine")
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_test_junit
+}
+
+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/support/test-fakes/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/test-fakes/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-fakes/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/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/android/FakeContext.kt b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/android/FakeContext.kt
new file mode 100644
index 0000000000..9b1cad74ff
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/android/FakeContext.kt
@@ -0,0 +1,279 @@
+/* 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.support.test.fakes.android
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.IntentSender
+import android.content.ServiceConnection
+import android.content.SharedPreferences
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.res.AssetManager
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.database.DatabaseErrorHandler
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.UserHandle
+import android.view.Display
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.InputStream
+
+/**
+ * A [Context] implementation for when you just need a Context instance in a test.
+ *
+ * Using the fake is faster than launching all Robolectric bells and whistles.
+ *
+ * The current implementation just throws for most access.
+ */
+@Suppress("LargeClass", "TooManyFunctions")
+open class FakeContext(
+ private val sharedPreferences: SharedPreferences = FakeSharedPreferences(),
+) : Context() {
+ override fun getAssets(): AssetManager = throw NotImplementedError()
+ override fun getResources(): Resources = throw NotImplementedError()
+ override fun getPackageManager(): PackageManager = throw NotImplementedError()
+ override fun getContentResolver(): ContentResolver = throw NotImplementedError()
+ override fun getMainLooper(): Looper = throw NotImplementedError()
+ override fun getApplicationContext(): Context = throw NotImplementedError()
+ override fun setTheme(resid: Int) = throw NotImplementedError()
+ override fun getTheme(): Resources.Theme = throw NotImplementedError()
+ override fun getClassLoader(): ClassLoader = throw NotImplementedError()
+ override fun getPackageName(): String = throw NotImplementedError()
+ override fun getApplicationInfo(): ApplicationInfo = throw NotImplementedError()
+ override fun getPackageResourcePath(): String = throw NotImplementedError()
+ override fun getPackageCodePath(): String = throw NotImplementedError()
+ override fun getSharedPreferences(name: String?, mode: Int): SharedPreferences {
+ return sharedPreferences
+ }
+ override fun moveSharedPreferencesFrom(sourceContext: Context?, name: String?): Boolean =
+ throw NotImplementedError()
+ override fun deleteSharedPreferences(name: String?): Boolean = throw NotImplementedError()
+ override fun openFileInput(name: String?): FileInputStream = throw NotImplementedError()
+ override fun openFileOutput(name: String?, mode: Int): FileOutputStream = throw NotImplementedError()
+ override fun deleteFile(name: String?): Boolean = throw NotImplementedError()
+ override fun getFileStreamPath(name: String?): File = throw NotImplementedError()
+ override fun getDataDir(): File = throw NotImplementedError()
+ override fun getFilesDir(): File = throw NotImplementedError()
+ override fun getNoBackupFilesDir(): File = throw NotImplementedError()
+ override fun getExternalFilesDir(type: String?): File? = throw NotImplementedError()
+ override fun getExternalFilesDirs(type: String?): Array<File> = throw NotImplementedError()
+ override fun getObbDir(): File = throw NotImplementedError()
+ override fun getObbDirs(): Array<File> = throw NotImplementedError()
+ override fun getCacheDir(): File = throw NotImplementedError()
+ override fun getCodeCacheDir(): File = throw NotImplementedError()
+ override fun getExternalCacheDir(): File? = throw NotImplementedError()
+ override fun getExternalCacheDirs(): Array<File> = throw NotImplementedError()
+ override fun getExternalMediaDirs(): Array<File> = throw NotImplementedError()
+ override fun fileList(): Array<String> = throw NotImplementedError()
+ override fun getDir(name: String?, mode: Int): File = throw NotImplementedError()
+ override fun openOrCreateDatabase(
+ name: String?,
+ mode: Int,
+ factory: SQLiteDatabase.CursorFactory?,
+ ): SQLiteDatabase = throw NotImplementedError()
+ override fun openOrCreateDatabase(
+ name: String?,
+ mode: Int,
+ factory: SQLiteDatabase.CursorFactory?,
+ errorHandler: DatabaseErrorHandler?,
+ ): SQLiteDatabase = throw NotImplementedError()
+ override fun moveDatabaseFrom(sourceContext: Context?, name: String?): Boolean =
+ throw NotImplementedError()
+ override fun deleteDatabase(name: String?): Boolean = throw NotImplementedError()
+ override fun getDatabasePath(name: String?): File = throw NotImplementedError()
+ override fun databaseList(): Array<String> = throw NotImplementedError()
+ override fun getWallpaper(): Drawable = throw NotImplementedError()
+ override fun peekWallpaper(): Drawable = throw NotImplementedError()
+ override fun getWallpaperDesiredMinimumWidth(): Int = throw NotImplementedError()
+ override fun getWallpaperDesiredMinimumHeight(): Int = throw NotImplementedError()
+ override fun setWallpaper(bitmap: Bitmap?) = throw NotImplementedError()
+ override fun setWallpaper(data: InputStream?) = throw NotImplementedError()
+ override fun clearWallpaper() = throw NotImplementedError()
+ override fun startActivity(intent: Intent?) = throw NotImplementedError()
+ override fun startActivity(intent: Intent?, options: Bundle?) = throw NotImplementedError()
+ override fun startActivities(intents: Array<out Intent>?) = throw NotImplementedError()
+ override fun startActivities(intents: Array<out Intent>?, options: Bundle?) =
+ throw NotImplementedError()
+ override fun startIntentSender(
+ intent: IntentSender?,
+ fillInIntent: Intent?,
+ flagsMask: Int,
+ flagsValues: Int,
+ extraFlags: Int,
+ ) = throw NotImplementedError()
+ override fun startIntentSender(
+ intent: IntentSender?,
+ fillInIntent: Intent?,
+ flagsMask: Int,
+ flagsValues: Int,
+ extraFlags: Int,
+ options: Bundle?,
+ ) = throw NotImplementedError()
+ override fun sendBroadcast(intent: Intent?) = throw NotImplementedError()
+ override fun sendBroadcast(intent: Intent?, receiverPermission: String?) = throw NotImplementedError()
+ override fun sendOrderedBroadcast(intent: Intent?, receiverPermission: String?) =
+ throw NotImplementedError()
+ override fun sendOrderedBroadcast(
+ intent: Intent,
+ receiverPermission: String?,
+ resultReceiver: BroadcastReceiver?,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?,
+ ) = throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun sendBroadcastAsUser(intent: Intent?, user: UserHandle?) = throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun sendBroadcastAsUser(
+ intent: Intent?,
+ user: UserHandle?,
+ receiverPermission: String?,
+ ) = throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun sendOrderedBroadcastAsUser(
+ intent: Intent?,
+ user: UserHandle?,
+ receiverPermission: String?,
+ resultReceiver: BroadcastReceiver?,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?,
+ ) = throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun sendStickyBroadcast(intent: Intent?) = throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun sendStickyOrderedBroadcast(
+ intent: Intent?,
+ resultReceiver: BroadcastReceiver?,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?,
+ ) = throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun removeStickyBroadcast(intent: Intent?) = throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun sendStickyBroadcastAsUser(intent: Intent?, user: UserHandle?) =
+ throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun sendStickyOrderedBroadcastAsUser(
+ intent: Intent?,
+ user: UserHandle?,
+ resultReceiver: BroadcastReceiver?,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?,
+ ) = throw NotImplementedError()
+
+ @SuppressLint("MissingPermission")
+ override fun removeStickyBroadcastAsUser(intent: Intent?, user: UserHandle?) =
+ throw NotImplementedError()
+ override fun registerReceiver(receiver: BroadcastReceiver?, filter: IntentFilter?): Intent? =
+ throw NotImplementedError()
+ override fun registerReceiver(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter?,
+ flags: Int,
+ ): Intent? = throw NotImplementedError()
+ override fun registerReceiver(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter?,
+ broadcastPermission: String?,
+ scheduler: Handler?,
+ ): Intent? = throw NotImplementedError()
+ override fun registerReceiver(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter?,
+ broadcastPermission: String?,
+ scheduler: Handler?,
+ flags: Int,
+ ): Intent? = throw NotImplementedError()
+ override fun unregisterReceiver(receiver: BroadcastReceiver?) = throw NotImplementedError()
+ override fun startService(service: Intent?): ComponentName? = throw NotImplementedError()
+ override fun startForegroundService(service: Intent?): ComponentName? = throw NotImplementedError()
+ override fun stopService(service: Intent?): Boolean = throw NotImplementedError()
+ override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean =
+ throw NotImplementedError()
+ override fun unbindService(conn: ServiceConnection) = throw NotImplementedError()
+ override fun startInstrumentation(
+ className: ComponentName,
+ profileFile: String?,
+ arguments: Bundle?,
+ ): Boolean = throw NotImplementedError()
+ override fun getSystemService(name: String): Any? = throw NotImplementedError()
+ override fun getSystemServiceName(serviceClass: Class<*>): String? = throw NotImplementedError()
+ override fun checkPermission(permission: String, pid: Int, uid: Int): Int = throw NotImplementedError()
+ override fun checkCallingPermission(permission: String): Int = throw NotImplementedError()
+ override fun checkCallingOrSelfPermission(permission: String): Int = throw NotImplementedError()
+ override fun checkSelfPermission(permission: String): Int = throw NotImplementedError()
+ override fun enforcePermission(permission: String, pid: Int, uid: Int, message: String?) =
+ throw NotImplementedError()
+ override fun enforceCallingPermission(permission: String, message: String?) = throw NotImplementedError()
+ override fun enforceCallingOrSelfPermission(permission: String, message: String?) = throw NotImplementedError()
+ override fun grantUriPermission(toPackage: String?, uri: Uri?, modeFlags: Int) = throw NotImplementedError()
+ override fun revokeUriPermission(uri: Uri?, modeFlags: Int) = throw NotImplementedError()
+ override fun revokeUriPermission(toPackage: String?, uri: Uri?, modeFlags: Int) = throw NotImplementedError()
+ override fun checkUriPermission(uri: Uri?, pid: Int, uid: Int, modeFlags: Int): Int = throw NotImplementedError()
+ override fun checkUriPermission(
+ uri: Uri?,
+ readPermission: String?,
+ writePermission: String?,
+ pid: Int,
+ uid: Int,
+ modeFlags: Int,
+ ): Int = throw NotImplementedError()
+ override fun checkCallingUriPermission(uri: Uri?, modeFlags: Int): Int = throw NotImplementedError()
+ override fun checkCallingOrSelfUriPermission(uri: Uri?, modeFlags: Int): Int = throw NotImplementedError()
+ override fun enforceUriPermission(
+ uri: Uri?,
+ pid: Int,
+ uid: Int,
+ modeFlags: Int,
+ message: String?,
+ ) = throw NotImplementedError()
+ override fun enforceUriPermission(
+ uri: Uri?,
+ readPermission: String?,
+ writePermission: String?,
+ pid: Int,
+ uid: Int,
+ modeFlags: Int,
+ message: String?,
+ ) = throw NotImplementedError()
+ override fun enforceCallingUriPermission(uri: Uri?, modeFlags: Int, message: String?) = throw NotImplementedError()
+ override fun enforceCallingOrSelfUriPermission(uri: Uri?, modeFlags: Int, message: String?) =
+ throw NotImplementedError()
+ override fun createPackageContext(packageName: String?, flags: Int): Context = throw NotImplementedError()
+ override fun createContextForSplit(splitName: String?): Context = throw NotImplementedError()
+ override fun createConfigurationContext(overrideConfiguration: Configuration): Context = throw NotImplementedError()
+ override fun createDisplayContext(display: Display): Context = throw NotImplementedError()
+ override fun createDeviceProtectedStorageContext(): Context = throw NotImplementedError()
+ override fun isDeviceProtectedStorage(): Boolean = throw NotImplementedError()
+}
diff --git a/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/android/FakeSharedPreferences.kt b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/android/FakeSharedPreferences.kt
new file mode 100644
index 0000000000..6d1a13b90b
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/android/FakeSharedPreferences.kt
@@ -0,0 +1,89 @@
+/* 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.support.test.fakes.android
+
+import android.content.SharedPreferences
+
+/**
+ * A simple [SharedPreferences] implementation backed by an in-memory map. Helpful in unit tests and
+ * faster than launching all Robolectric bells and whistles.
+ */
+@Suppress("UNCHECKED_CAST")
+class FakeSharedPreferences(
+ internal val values: MutableMap<String, Any> = mutableMapOf(),
+) : SharedPreferences {
+ override fun getAll(): Map<String, *> = values
+ override fun getString(key: String, defValue: String?): String? = values[key]?.toString() ?: defValue
+ override fun getStringSet(key: String, defValues: MutableSet<String>?): Set<String>? =
+ values[key] as? Set<String> ?: defValues
+ override fun getInt(key: String, defValue: Int): Int = values[key] as? Int ?: defValue
+ override fun getLong(key: String, defValue: Long): Long = values[key] as? Long ?: defValue
+ override fun getFloat(key: String, defValue: Float): Float = values[key] as? Float ?: defValue
+ override fun getBoolean(key: String, defValue: Boolean): Boolean = values[key] as? Boolean ?: defValue
+ override fun contains(key: String): Boolean = values.containsKey(key)
+ override fun edit(): SharedPreferences.Editor = FakeEditor(this)
+ override fun registerOnSharedPreferenceChangeListener(
+ listener: SharedPreferences.OnSharedPreferenceChangeListener,
+ ) = throw NotImplementedError()
+ override fun unregisterOnSharedPreferenceChangeListener(
+ listener: SharedPreferences.OnSharedPreferenceChangeListener,
+ ) = throw NotImplementedError()
+}
+
+internal class FakeEditor(
+ private val preferences: FakeSharedPreferences,
+) : SharedPreferences.Editor {
+ override fun putString(key: String, value: String?): SharedPreferences.Editor {
+ if (value == null) {
+ remove(key)
+ } else {
+ preferences.values[key] = value
+ }
+ return this
+ }
+
+ override fun putStringSet(key: String, values: MutableSet<String>?): SharedPreferences.Editor {
+ if (values == null) {
+ remove(key)
+ } else {
+ preferences.values[key] = values
+ }
+ return this
+ }
+
+ override fun putInt(key: String, value: Int): SharedPreferences.Editor {
+ preferences.values[key] = value
+ return this
+ }
+
+ override fun putLong(key: String, value: Long): SharedPreferences.Editor {
+ preferences.values[key] = value
+ return this
+ }
+
+ override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
+ preferences.values[key] = value
+ return this
+ }
+
+ override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
+ preferences.values[key] = value
+ return this
+ }
+
+ override fun remove(key: String): SharedPreferences.Editor {
+ preferences.values.remove(key)
+ return this
+ }
+
+ override fun clear(): SharedPreferences.Editor {
+ preferences.values.clear()
+ return this
+ }
+
+ override fun commit(): Boolean = true
+
+ override fun apply() = Unit
+}
diff --git a/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngine.kt b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngine.kt
new file mode 100644
index 0000000000..d969e36640
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngine.kt
@@ -0,0 +1,71 @@
+/* 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.support.test.fakes.engine
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.JsonReader
+import mozilla.components.concept.base.profiler.Profiler
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.utils.EngineVersion
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+
+/**
+ * A fake [Engine] implementation to be used in tests that require an [Engine] instance.
+ *
+ * @param expectToRestoreRealEngineSessionState Whether this fake engine should expect to restore
+ * engine sessions from actual engine session state JSON (e.g. from GeckoView). Otherwise this fake
+ * will expect to always deal with [FakeEngineSessionState] instances.
+ */
+class FakeEngine(
+ private val expectToRestoreRealEngineSessionState: Boolean = false,
+) : Engine {
+ override val version: EngineVersion
+ get() = throw NotImplementedError()
+
+ override fun createView(context: Context, attrs: AttributeSet?): EngineView =
+ FakeEngineView(context)
+
+ override fun createSession(private: Boolean, contextId: String?): EngineSession =
+ throw UnsupportedOperationException()
+
+ override fun createSessionState(json: JSONObject) = FakeEngineSessionState(json.getString("engine"))
+
+ override fun createSessionStateFrom(reader: JsonReader): EngineSessionState {
+ if (expectToRestoreRealEngineSessionState) {
+ reader.skipValue()
+ return FakeEngineSessionState("<real state>")
+ }
+
+ var value: String? = null
+
+ reader.beginObject()
+
+ if (reader.hasNext()) {
+ assertEquals("engine", reader.nextName())
+ value = reader.nextString()
+ }
+
+ reader.endObject()
+
+ return FakeEngineSessionState(value ?: "---")
+ }
+
+ override fun name(): String = "fake_engine"
+
+ override fun speculativeConnect(url: String) =
+ throw UnsupportedOperationException()
+
+ override val profiler: Profiler
+ get() = throw NotImplementedError()
+
+ override val settings: Settings = DefaultSettings()
+}
diff --git a/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngineSessionState.kt b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngineSessionState.kt
new file mode 100644
index 0000000000..e3d1dc4a5b
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngineSessionState.kt
@@ -0,0 +1,24 @@
+/* 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.support.test.fakes.engine
+
+import android.util.JsonWriter
+import mozilla.components.concept.engine.EngineSessionState
+
+/**
+ * A fake [EngineSessionState] that can be used with [FakeEngine] in tests.
+ */
+class FakeEngineSessionState(
+ val value: String,
+) : EngineSessionState {
+ override fun writeTo(writer: JsonWriter) {
+ writer.beginObject()
+
+ writer.name("engine")
+ writer.value(value)
+
+ writer.endObject()
+ }
+}
diff --git a/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngineView.kt b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngineView.kt
new file mode 100644
index 0000000000..61ccf87e6f
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-fakes/src/main/java/mozilla/components/support/test/fakes/engine/FakeEngineView.kt
@@ -0,0 +1,33 @@
+/* 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.support.test.fakes.engine
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.view.View
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+
+/**
+ * A fake [EngineView] to be used in tests.
+ */
+class FakeEngineView(context: Context) : View(context), EngineView {
+ override fun render(session: EngineSession) = Unit
+
+ override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) = Unit
+
+ override fun clearSelection() = Unit
+
+ override fun setVerticalClipping(clippingHeight: Int) = Unit
+
+ override fun setDynamicToolbarMaxHeight(height: Int) = Unit
+
+ override fun setActivityContext(context: Context?) = Unit
+
+ override fun release() = Unit
+
+ override var selectionActionDelegate: SelectionActionDelegate? = null
+}
diff --git a/mobile/android/android-components/components/support/test-libstate/README.md b/mobile/android/android-components/components/support/test-libstate/README.md
new file mode 100644
index 0000000000..2756befdfc
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-libstate/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Test
+
+A collection of helpers for testing functionality that relies on the lib-state component in local unit tests (`src/test`).
+
+## Usage
+
+### 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:support-test-libstate:{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/support/test-libstate/build.gradle b/mobile/android/android-components/components/support/test-libstate/build.gradle
new file mode 100644
index 0000000000..b67bf66a7a
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-libstate/build.gradle
@@ -0,0 +1,45 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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')
+ }
+ }
+
+ lint {
+ // Disabled since this caused issues with Android Gradle Plugin 3.2.1+ (NullPointerException:InvalidPackageDetector)
+ tasks.lint.enabled = false
+
+ lintConfig file("lint.xml")
+ }
+
+ namespace 'mozilla.components.support.test.libstate'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation project(':lib-state')
+
+ testImplementation ComponentsDependencies.androidx_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+}
+
+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/support/test-libstate/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/test-libstate/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-libstate/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/support/test-libstate/src/main/java/mozilla/components/support/test/libstate/ext/Store.kt b/mobile/android/android-components/components/support/test-libstate/src/main/java/mozilla/components/support/test/libstate/ext/Store.kt
new file mode 100644
index 0000000000..8620ca6e90
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-libstate/src/main/java/mozilla/components/support/test/libstate/ext/Store.kt
@@ -0,0 +1,26 @@
+/* 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.support.test.libstate.ext
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.runBlocking
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+
+/**
+ * Blocks and returns once all dispatched actions have been processed
+ * i.e. the reducers have run and all observers have been notified of
+ * state changes.
+ */
+fun <S : State, A : Action> Store<S, A>.waitUntilIdle() {
+ val scopeField = Store::class.java.getDeclaredField("scope")
+ scopeField.isAccessible = true
+ val scope = scopeField.get(this) as CoroutineScope
+ runBlocking {
+ scope.coroutineContext[Job]?.children?.forEach { it.join() }
+ }
+}
diff --git a/mobile/android/android-components/components/support/test-libstate/src/main/java/mozilla/components/support/test/middleware/CaptureActionsMiddleware.kt b/mobile/android/android-components/components/support/test-libstate/src/main/java/mozilla/components/support/test/middleware/CaptureActionsMiddleware.kt
new file mode 100644
index 0000000000..55670446d0
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-libstate/src/main/java/mozilla/components/support/test/middleware/CaptureActionsMiddleware.kt
@@ -0,0 +1,88 @@
+/* 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.support.test.middleware
+
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.State
+import kotlin.reflect.KClass
+
+/**
+ * A [Middleware] implementation for unit tests that want to inspect actions dispatched on a `Store`
+ */
+class CaptureActionsMiddleware<S : State, A : Action> : Middleware<S, A> {
+ private val capturedActions = mutableListOf<A>()
+
+ @Synchronized
+ override fun invoke(context: MiddlewareContext<S, A>, next: (A) -> Unit, action: A) {
+ capturedActions.add(action)
+ next(action)
+ }
+
+ /**
+ * Returns the first action of type [clazz] that was dispatched on the store. Throws
+ * [AssertionError] if no such action was dispatched.
+ */
+ @Synchronized
+ @Suppress("UNCHECKED_CAST")
+ fun <X : A> findFirstAction(clazz: KClass<X>): X {
+ return capturedActions.firstOrNull { it.javaClass == clazz.java } as? X
+ ?: throw AssertionError("No action of type $clazz found")
+ }
+
+ /**
+ * Returns the last action of type [clazz] that was dispatched on the store. Throws
+ * [AssertionError] if no such action was dispatched.
+ */
+ @Synchronized
+ @Suppress("UNCHECKED_CAST")
+ fun <X : A> findLastAction(clazz: KClass<X>): X {
+ return capturedActions.lastOrNull { it.javaClass == clazz.java } as? X
+ ?: throw AssertionError("No action of type $clazz found")
+ }
+
+ /**
+ * Asserts that an action of type [clazz] was dispatched and optionally executes a given [block]
+ * with the first action of type [clazz] that was dispatched on the store. Throws [AssertionError]
+ * if no such action was dispatched.
+ */
+ @Synchronized
+ fun <X : A> assertFirstAction(clazz: KClass<X>, block: (X) -> Unit = {}) {
+ val action = findFirstAction(clazz)
+ block(action)
+ }
+
+ /**
+ * Executes the given [block] with the last action of type [clazz] that was dispatched on the
+ * store. Throws [AssertionError] if no such action was dispatched.
+ */
+ @Synchronized
+ fun <X : A> assertLastAction(clazz: KClass<X>, block: (X) -> Unit) {
+ val action = findLastAction(clazz)
+ block(action)
+ }
+
+ /**
+ * Asserts that no action of type [clazz] was dispatched. Throws [AssertionError] if a matching
+ * action was found.
+ */
+ @Synchronized
+ fun <X : A> assertNotDispatched(clazz: KClass<X>) {
+ if (!capturedActions.none { it.javaClass == clazz.java }) {
+ throw AssertionError("Action of type $clazz was dispatched: ${findFirstAction(clazz)}")
+ }
+ }
+
+ /**
+ * Resets the remembered list of actions.
+ *
+ * Usually this is called between test runs to avoid verifying actions of a previous test methods.
+ */
+ @Synchronized
+ fun reset() {
+ capturedActions.clear()
+ }
+}
diff --git a/mobile/android/android-components/components/support/test-libstate/src/test/java/mozilla/components/support/test/libstate/ext/StoreTest.kt b/mobile/android/android-components/components/support/test-libstate/src/test/java/mozilla/components/support/test/libstate/ext/StoreTest.kt
new file mode 100644
index 0000000000..516206215e
--- /dev/null
+++ b/mobile/android/android-components/components/support/test-libstate/src/test/java/mozilla/components/support/test/libstate/ext/StoreTest.kt
@@ -0,0 +1,43 @@
+/* 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.support.test.libstate.ext
+
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class StoreTest {
+
+ @Test
+ fun `waitUntilIdle blocks and returns once reducers were executed`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ store.dispatch(TestAction.IncrementAction)
+ store.waitUntilIdle()
+ assertEquals(24, store.state.counter)
+
+ store.dispatch(TestAction.DecrementAction)
+ store.dispatch(TestAction.DecrementAction)
+ store.waitUntilIdle()
+ assertEquals(22, store.state.counter)
+ }
+}
+
+fun reducer(state: TestState, action: TestAction): TestState = when (action) {
+ is TestAction.IncrementAction -> state.copy(counter = state.counter + 1)
+ is TestAction.DecrementAction -> state.copy(counter = state.counter - 1)
+}
+
+data class TestState(val counter: Int) : State
+
+sealed class TestAction : Action {
+ object IncrementAction : TestAction()
+ object DecrementAction : TestAction()
+}
diff --git a/mobile/android/android-components/components/support/test/README.md b/mobile/android/android-components/components/support/test/README.md
new file mode 100644
index 0000000000..38c8f1ec54
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Test
+
+A collection of helpers for testing components in local unit tests (`src/test`).
+
+## Usage
+
+### 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:support-test:{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/support/test/build.gradle b/mobile/android/android-components/components/support/test/build.gradle
new file mode 100644
index 0000000000..6111218d8c
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/build.gradle
@@ -0,0 +1,58 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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')
+ }
+ }
+
+ lint {
+ // Disabled since this caused issues with Android Gradle Plugin 3.2.1+ (NullPointerException:InvalidPackageDetector)
+ tasks.lint.enabled = false
+
+ lintConfig file("lint.xml")
+ }
+
+ namespace 'mozilla.components.support.test'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.kotlin_reflect
+
+ implementation ComponentsDependencies.androidx_test_junit
+ api ComponentsDependencies.testing_mockito
+ implementation ComponentsDependencies.testing_coroutines
+ implementation ComponentsDependencies.androidx_fragment
+ implementation (ComponentsDependencies.testing_robolectric) {
+ exclude group: 'org.apache.maven'
+ }
+ implementation project(':support-base')
+ testImplementation ComponentsDependencies.testing_maven_ant_tasks
+
+ implementation ComponentsDependencies.androidx_test_core
+
+ testImplementation ComponentsDependencies.androidx_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation project(':support-ktx')
+}
+
+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/support/test/lint.xml b/mobile/android/android-components/components/support/test/lint.xml
new file mode 100644
index 0000000000..3c7a84d2fb
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/lint.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<lint>
+ <issue id="InvalidPackage">
+ <ignore path="**/byte-buddy-agent-*.jar"/>
+ <ignore path="**/bcprov-*on-*.jar"/>
+ <ignore path="**/shadows-framework-*.jar"/>
+ <ignore path="**/xstream-*.jar"/>
+ <ignore path="**/resources-4.0-*.jar"/>
+ </issue>
+</lint>
diff --git a/mobile/android/android-components/components/support/test/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/test/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/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/support/test/src/main/java/mozilla/components/support/test/Expect.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Expect.kt
new file mode 100644
index 0000000000..eae9878667
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Expect.kt
@@ -0,0 +1,22 @@
+/* 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.support.test
+
+import org.junit.Assert.fail
+import kotlin.reflect.KClass
+
+/**
+ * Expect [block] to throw an exception. Otherwise fail the test (junit).
+ */
+inline fun <reified T : Throwable> expectException(clazz: KClass<T>, block: () -> Unit) {
+ try {
+ block()
+ fail("Expected exception to be thrown: $clazz")
+ } catch (e: Throwable) {
+ if (e !is T) {
+ throw e
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/KArgumentCaptor.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/KArgumentCaptor.kt
new file mode 100644
index 0000000000..6b1b6bba52
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/KArgumentCaptor.kt
@@ -0,0 +1,44 @@
+/* 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.support.test
+
+import org.mockito.ArgumentCaptor
+import kotlin.reflect.KClass
+
+/**
+ * Creates a [KArgumentCaptor] for given type.
+ */
+inline fun <reified T : Any> argumentCaptor(): KArgumentCaptor<T> {
+ return KArgumentCaptor(ArgumentCaptor.forClass(T::class.java), T::class)
+}
+class KArgumentCaptor<out T : Any?>(
+ private val captor: ArgumentCaptor<T>,
+ private val tClass: KClass<*>,
+) {
+
+ /**
+ * The first captured value of the argument.
+ * @throws IndexOutOfBoundsException if the value is not available.
+ */
+ val value: T
+ get() = captor.value
+
+ val allValues: List<T>
+ get() = captor.allValues
+
+ @Suppress("UNCHECKED_CAST")
+ fun capture(): T {
+ return captor.capture() ?: castNull()
+ }
+}
+
+/**
+ * Uses a quirk in the bytecode generated by Kotlin
+ * to cast [null] to a non-null type.
+ *
+ * See https://youtrack.jetbrains.com/issue/KT-8135.
+ */
+@Suppress("UNCHECKED_CAST")
+private fun <T> castNull(): T = null as T
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Matchers.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Matchers.kt
new file mode 100644
index 0000000000..b8d8f259ec
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Matchers.kt
@@ -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/. */
+
+package mozilla.components.support.test
+
+import org.mockito.AdditionalMatchers
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+
+/**
+ * Mockito matcher that matches <strong>anything</strong>, including nulls and varargs.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+fun <T> any(): T {
+ Mockito.any<T>()
+ return uninitialized()
+}
+
+/**
+ * Mockito matcher that matches if the argument is the same as the provided value.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+fun <T> eq(value: T): T {
+ return Mockito.eq(value) ?: value
+}
+
+/**
+ * Mockito matcher that matches if the argument is not the same as the provided value.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+fun <T> not(value: T): T {
+ return AdditionalMatchers.not(value) ?: value
+}
+
+/**
+ * Mockito matcher that captures the passed argument.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+fun <T> capture(value: ArgumentCaptor<T>): T {
+ value.capture()
+ return uninitialized()
+}
+
+/**
+ * Mockito matcher that matches <strong>anything</strong> as nullable.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+inline fun <reified T> nullable(): T {
+ Mockito.nullable(T::class.java)
+ return uninitialized()
+}
+
+@Suppress("UNCHECKED_CAST")
+fun <T> uninitialized(): T = null as T
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Mock.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Mock.kt
new file mode 100644
index 0000000000..38943c7577
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Mock.kt
@@ -0,0 +1,54 @@
+/* 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.support.test
+
+import android.view.MotionEvent
+import org.mockito.Mockito
+
+/**
+ * Dynamically create a mock object. This method is helpful when creating mocks of classes
+ * using generics.
+ *
+ * Optional @param setup will be called on the mock after init.
+ *
+ * Instead of:
+ * <code>val foo = Mockito.mock(....Class of Bar<Baz>?...)<code>
+ *
+ * You can just use:
+ * <code>val foo: Bar<Baz> = mock()</code>
+ */
+inline fun <reified T : Any> mock(noinline setup: (T.() -> Unit)? = null): T = Mockito.mock(T::class.java)!!
+ .apply { setup?.invoke(this) }
+
+/**
+ * Equivalent to [mock] but allows inline setup of suspending functions.
+ */
+suspend inline fun <reified T : Any> coMock(noinline setup: (suspend T.() -> Unit)? = null): T = Mockito.mock(T::class.java)!!
+ .apply { setup?.invoke(this) }
+
+/**
+ * Enables stubbing methods. Use it when you want the mock to return particular value when particular method is called.
+ *
+ * Alias for [Mockito.when ].
+ *
+ * Taken from [mockito-kotlin](https://github.com/nhaarman/mockito-kotlin/).
+ */
+@Suppress("NOTHING_TO_INLINE")
+inline fun <T> whenever(methodCall: T) = Mockito.`when`(methodCall)!!
+
+/**
+ * Creates a custom [MotionEvent] for testing. As of SDK 28 [MotionEvent]s can't be mocked anymore and need to be created
+ * through [MotionEvent.obtain].
+ */
+fun mockMotionEvent(
+ action: Int,
+ downTime: Long = System.currentTimeMillis(),
+ eventTime: Long = System.currentTimeMillis(),
+ x: Float = 0f,
+ y: Float = 0f,
+ metaState: Int = 0,
+): MotionEvent {
+ return MotionEvent.obtain(downTime, eventTime, action, x, y, metaState)
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ThrowProperty.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ThrowProperty.kt
new file mode 100644
index 0000000000..fede1bf584
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ThrowProperty.kt
@@ -0,0 +1,19 @@
+/* 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.support.test
+
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+/**
+ * A [ReadWriteProperty] implementation for creating stub properties.
+ */
+class ThrowProperty<T> : ReadWriteProperty<Any, T> {
+ override fun getValue(thisRef: Any, property: KProperty<*>): T =
+ throw UnsupportedOperationException("Cannot get $property")
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: T) =
+ throw UnsupportedOperationException("Cannot set $property")
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt
new file mode 100644
index 0000000000..94e070eda7
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt
@@ -0,0 +1,19 @@
+/* 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.support.test.ext
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.R
+import androidx.appcompat.view.ContextThemeWrapper
+import mozilla.components.support.test.robolectric.testContext
+
+/**
+ * `testContext` wrapped with AppCompat theme.
+ *
+ * Useful for views that uses theme attributes, for example.
+ */
+@VisibleForTesting val appCompatContext: Context
+ get() = ContextThemeWrapper(testContext, R.style.Theme_AppCompat)
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Job.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Job.kt
new file mode 100644
index 0000000000..a1f8f80062
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Job.kt
@@ -0,0 +1,15 @@
+/* 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.support.test.ext
+
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Blocks the current thread until the job is complete.
+ */
+fun Job.joinBlocking() {
+ runBlocking { join() }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/KProperty.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/KProperty.kt
new file mode 100644
index 0000000000..cf27945f54
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/KProperty.kt
@@ -0,0 +1,29 @@
+/* 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.support.test.ext
+
+import kotlin.reflect.KProperty0
+import kotlin.reflect.jvm.isAccessible
+
+/**
+ * Returns true if a lazy property has been initialized, or if the property is not lazy.
+ *
+ * implementation inspired by https://stackoverflow.com/a/42536189
+ */
+val KProperty0<*>.isLazyInitialized: Boolean
+ get() {
+ // Prevent exception for accessing private getDelegate function.
+ val originalAccessLevel = isAccessible
+ isAccessible = true
+
+ val lazyDelegate = getDelegate()
+ require(lazyDelegate is Lazy<*>) { "Expected receiver property to be lazy" }
+
+ val isLazyInitialized = lazyDelegate.isInitialized()
+
+ // Reset access level.
+ isAccessible = originalAccessLevel
+ return isLazyInitialized
+ }
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/fakes/FakeClock.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/fakes/FakeClock.kt
new file mode 100644
index 0000000000..0411873234
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/fakes/FakeClock.kt
@@ -0,0 +1,19 @@
+/* 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.support.test.fakes
+
+/**
+ * A fake clock exposing a [time] property that can be changed at test runtime.
+ *
+ * Ideally a class needing time hides the calls to System.currentTimeMillis() behind a lambda
+ * that can it can invoke. In a test this lambda can be replaced by FakeClock()::time.
+ */
+class FakeClock(
+ var time: Long = 0,
+) {
+ fun advanceBy(time: Long) {
+ this.time += time
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/file/Resources.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/file/Resources.kt
new file mode 100644
index 0000000000..2d5e842731
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/file/Resources.kt
@@ -0,0 +1,19 @@
+/* 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.support.test.file
+
+import org.junit.Assert
+
+/**
+ * Loads a file from the resources folder and returns its content as a string object.
+ * @param path The path where the file is located
+ */
+fun Any.loadResourceAsString(path: String): String {
+ return javaClass.getResourceAsStream(path)!!.bufferedReader().use {
+ it.readText()
+ }.also {
+ Assert.assertNotNull(it)
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Extensions.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Extensions.kt
new file mode 100644
index 0000000000..3b01aabdcd
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Extensions.kt
@@ -0,0 +1,14 @@
+/* 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.support.test.robolectric
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+
+/**
+ * Provides application context for test purposes
+ */
+inline val testContext: Context
+ get() = ApplicationProvider.getApplicationContext()
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Fragments.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Fragments.kt
new file mode 100644
index 0000000000..6ad1d3d526
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Fragments.kt
@@ -0,0 +1,32 @@
+/* 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.support.test.robolectric
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import org.robolectric.Robolectric
+
+/**
+ * Set up an added [Fragment] to a [FragmentActivity] that has been initialized to a resumed state.
+ *
+ * @param fragmentTag the name that will be used to tag the fragment inside the [FragmentManager].
+ * @param fragmentFactory a lambda function that returns a Fragment that will be added to the Activity.
+ *
+ * @return The same [Fragment] that was returned from [fragmentFactory] after it got added to the
+ * Activity.
+ */
+inline fun <T : Fragment> createAddedTestFragment(fragmentTag: String = "test", fragmentFactory: () -> T): T {
+ val activity = Robolectric.buildActivity(FragmentActivity::class.java)
+ .create()
+ .start()
+ .resume()
+ .get()
+
+ return fragmentFactory().also {
+ activity.supportFragmentManager.beginTransaction()
+ .add(it, fragmentTag)
+ .commitNow()
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Permissions.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Permissions.kt
new file mode 100644
index 0000000000..606956631a
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Permissions.kt
@@ -0,0 +1,21 @@
+/* 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.support.test.robolectric
+
+import android.app.Application
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import org.robolectric.Shadows.shadowOf
+
+/**
+ * A helper for working with permission
+ * just pass one or more permission that you need to be granted.
+ * @param permissions list of permissions that you need to be granted.
+ */
+fun grantPermission(vararg permissions: String) {
+ val application = shadowOf(getApplicationContext<Application>())
+ permissions.map {
+ application.grantPermissions(it)
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/shadow/PixelCopyShadow.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/shadow/PixelCopyShadow.kt
new file mode 100644
index 0000000000..45b6c86d71
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/shadow/PixelCopyShadow.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.support.test.robolectric.shadow
+
+import android.annotation.TargetApi
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.os.Build
+import android.os.Handler
+import android.view.PixelCopy
+import android.view.Window
+import org.robolectric.annotation.Implementation
+import org.robolectric.annotation.Implements
+
+/**
+ * Shadow for [PixelCopy] API.
+ */
+@Implements(PixelCopy::class, minSdk = Build.VERSION_CODES.N)
+@TargetApi(Build.VERSION_CODES.N)
+class PixelCopyShadow {
+
+ companion object {
+ var copyResult = PixelCopy.SUCCESS
+
+ @JvmStatic
+ @Implementation
+ // Some parameters are unused but method signature should be the same as for original class.
+ @Suppress("UNUSED_PARAMETER")
+ fun request(
+ source: Window,
+ srcRect: Rect?,
+ dest: Bitmap,
+ listener: PixelCopy.OnPixelCopyFinishedListener,
+ listenerThread: Handler?,
+ ) {
+ listener.onPixelCopyFinished(copyResult)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/Helpers.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/Helpers.kt
new file mode 100644
index 0000000000..17b3bbf9e6
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/Helpers.kt
@@ -0,0 +1,57 @@
+/* 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/. */
+
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // for TestMainDispatcher
+
+package mozilla.components.support.test.rule
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestResult
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.internal.TestMainDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlin.reflect.full.companionObject
+import kotlin.reflect.full.companionObjectInstance
+import kotlin.reflect.full.memberProperties
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * `coroutines.test` default timeout to use when waiting for asynchronous completions of the coroutines
+ * managed by a [TestCoroutineScheduler].
+ */
+private val DEFAULT_DISPATCH_TIMEOUT_SECONDS = 60.seconds
+
+/**
+ * Convenience method of executing [testBody] in a new coroutine running with the
+ * [TestDispatcher] previously set through [Dispatchers.setMain].
+ *
+ * Running a test with a shared [TestDispatcher] allows
+ * - newly created coroutines inside it will automatically be reparented to the test coroutine context.
+ * - leveraging an already set strategy for entering launch / async blocks.
+ * - easier scheduling control.
+ *
+ * @see runTest
+ * @see Dispatchers.setMain
+ * @see TestDispatcher
+ */
+fun runTestOnMain(
+ testTimeoutMs: Duration = DEFAULT_DISPATCH_TIMEOUT_SECONDS,
+ testBody: suspend TestScope.() -> Unit,
+): TestResult {
+ val mainDispatcher = Dispatchers.Main
+ require(mainDispatcher is TestMainDispatcher) {
+ "A TestDispatcher is not available. Use MainCoroutineRule or Dispatchers.setMain to set one before calling this method"
+ }
+
+ // Get the TestDispatcher set through `Dispatchers.setMain(..)`.
+ val companionObject = mainDispatcher::class.companionObject
+ val companionInstance = mainDispatcher::class.companionObjectInstance
+ val testDispatcher = companionObject!!.memberProperties.first().getter.call(companionInstance) as TestDispatcher
+
+ // Delegate to the original implementation of `runTest`. Just with a previously set TestDispatcher.
+ runTest(testDispatcher, timeout = testTimeoutMs, testBody)
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/MainCoroutineRule.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/MainCoroutineRule.kt
new file mode 100644
index 0000000000..b8df5e7ef6
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/MainCoroutineRule.kt
@@ -0,0 +1,56 @@
+/* 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.support.test.rule
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import mozilla.components.support.base.utils.NamedThreadFactory
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import java.util.concurrent.Executors
+
+/**
+ * Create single threaded dispatcher for test environment.
+ */
+@Deprecated("Use a `TestDispatcher` from the kotlinx-coroutines-test library", ReplaceWith("UnconfinedTestDispatcher()"))
+fun createTestCoroutinesDispatcher(): CoroutineDispatcher = Executors.newSingleThreadExecutor(
+ NamedThreadFactory("TestCoroutinesDispatcher"),
+).asCoroutineDispatcher()
+
+/**
+ * JUnit rule to change Dispatchers.Main in coroutines.
+ * This assumes no other calls to `Dispatchers.setMain` are made to override the main dispatcher.
+ *
+ * @param testDispatcher [TestDispatcher] for handling all coroutines execution.
+ * Defaults to [UnconfinedTestDispatcher] which will eagerly enter `launch` or `async` blocks.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class MainCoroutineRule(val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()) : TestWatcher() {
+ /**
+ * Get a [TestScope] that integrates with `runTest` and can be passed as an argument
+ * to the code under test when a [CoroutineScope] is required.
+ *
+ * This will rely on [testDispatcher] for controlling entering `launch` or `async` blocks.
+ */
+ val scope by lazy { TestScope(testDispatcher) }
+
+ override fun starting(description: Description) {
+ Dispatchers.setMain(testDispatcher)
+ super.starting(description)
+ }
+
+ override fun finished(description: Description) {
+ Dispatchers.resetMain()
+ super.finished(description)
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/PermissionsTest.kt b/mobile/android/android-components/components/support/test/src/test/java/PermissionsTest.kt
new file mode 100644
index 0000000000..1182821787
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/PermissionsTest.kt
@@ -0,0 +1,28 @@
+/* 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.support.test.robolectric
+
+import android.Manifest.permission.INTERNET
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PermissionsTest {
+
+ @Test
+ fun `after call grantPermission this permission must be granted `() {
+ var isGranted = testContext.isPermissionGranted(INTERNET)
+ assertFalse(isGranted)
+
+ grantPermission(INTERNET)
+ isGranted = testContext.isPermissionGranted(INTERNET)
+
+ assertTrue(isGranted)
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/ThrowPropertyTest.kt b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/ThrowPropertyTest.kt
new file mode 100644
index 0000000000..24e50c2b6d
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/ThrowPropertyTest.kt
@@ -0,0 +1,24 @@
+/* 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.support.test.robolectric.mozilla.components.support.test
+
+import mozilla.components.support.test.ThrowProperty
+import org.junit.Test
+
+class ThrowPropertyTest {
+ private val testProperty = "test"
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun `exception thrown when get value is called`() {
+ val throwProperty = ThrowProperty<String>()
+ throwProperty.getValue(testProperty, ::testProperty)
+ }
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun `exception thrown when set value is called`() {
+ val throwProperty = ThrowProperty<String>()
+ throwProperty.setValue(testProperty, ::testProperty, "test1")
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/file/ResourcesTest.kt b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/file/ResourcesTest.kt
new file mode 100644
index 0000000000..58cd279fcd
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/file/ResourcesTest.kt
@@ -0,0 +1,17 @@
+/* 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.support.test.robolectric.mozilla.components.support.test.file
+
+import mozilla.components.support.test.file.loadResourceAsString
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ResourcesTest {
+
+ @Test
+ fun getProvidedAppContext() {
+ assertEquals("42", loadResourceAsString("/example_file.txt"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/ExtensionsTest.kt b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/ExtensionsTest.kt
new file mode 100644
index 0000000000..6b86a649a5
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/ExtensionsTest.kt
@@ -0,0 +1,23 @@
+/* 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.support.test.robolectric
+
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ExtensionsTest {
+
+ @Test
+ fun getProvidedAppContext() {
+ Assert.assertEquals(
+ ApplicationProvider.getApplicationContext(),
+ testContext,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/FragmentsTest.kt b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/FragmentsTest.kt
new file mode 100644
index 0000000000..eca68aa7e7
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/FragmentsTest.kt
@@ -0,0 +1,30 @@
+/* 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.support.test.robolectric
+
+import androidx.fragment.app.Fragment
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FragmentsTest {
+
+ @Test
+ fun `setupFragment should add fragment correctly`() {
+ val addedFragment = createAddedTestFragment { Fragment() }
+
+ assertTrue(addedFragment.isAdded)
+ }
+
+ @Test
+ fun `setupFragment should add fragment with correct tag`() {
+ val fragment = createAddedTestFragment(fragmentTag = "aTag") { Fragment() }
+
+ assertNotNull(fragment.parentFragmentManager.findFragmentByTag("aTag"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/resources/example_file.txt b/mobile/android/android-components/components/support/test/src/test/resources/example_file.txt
new file mode 100644
index 0000000000..f70d7bba4a
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/resources/example_file.txt
@@ -0,0 +1 @@
+42 \ No newline at end of file
diff --git a/mobile/android/android-components/components/support/test/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/test/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/utils/README.md b/mobile/android/android-components/components/support/utils/README.md
new file mode 100644
index 0000000000..a881ecd43d
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Utils
+
+Generic utility classes to be shared between projects.
+
+## Usage
+
+### 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:support-utils:{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/support/utils/build.gradle b/mobile/android/android-components/components/support/utils/build.gradle
new file mode 100644
index 0000000000..3303ab3d73
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/build.gradle
@@ -0,0 +1,47 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+ namespace 'mozilla.components.support.utils'
+}
+
+dependencies {
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_core
+ implementation ComponentsDependencies.androidx_core_ktx
+
+ // We expose the app-compat as API so that consumers get access to the Lifecycle classes automatically
+ api ComponentsDependencies.androidx_appcompat
+
+ testImplementation project(":support-test")
+
+ 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/support/utils/proguard-rules.pro b/mobile/android/android-components/components/support/utils/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/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/support/utils/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/utils/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..5ca893c9da
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Adds the ability to discover apps that have matching <intent-filter> elements.
+ Needed for PackageManager.queryIntentActivities calls.
+ See https://developer.android.com/training/package-visibility/declaring for more details.-->
+ <queries>
+ <intent>
+ <action android:name="android.intent.action.SEND" />
+ <data android:mimeType="text/plain" />
+ </intent>
+ </queries>
+</manifest> \ No newline at end of file
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/ktx/util/URLStringUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/ktx/util/URLStringUtils.kt
new file mode 100644
index 0000000000..e199f825e0
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/ktx/util/URLStringUtils.kt
@@ -0,0 +1,167 @@
+/* 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.support.ktx.util
+
+import android.net.Uri
+import android.text.TextUtils
+import androidx.annotation.VisibleForTesting
+import androidx.core.text.TextDirectionHeuristicCompat
+import androidx.core.text.TextDirectionHeuristicsCompat
+import java.util.regex.Pattern
+
+object URLStringUtils {
+ /**
+ * Determine whether a string is a URL.
+ *
+ * This method performs a lenient check to determine whether a string is a URL. Anything that
+ * contains a :, ://, or . and has no internal spaces is potentially a URL. If you need a
+ * stricter check, consider using isURLLikeStrict().
+ */
+ fun isURLLike(string: String) = isURLLenient.matcher(string).matches()
+
+ /**
+ * Determine whether a string is a search term.
+ *
+ * This method recognizes a string as a search term as anything other than a URL.
+ */
+ fun isSearchTerm(string: String) = !isURLLike(string)
+
+ /**
+ * Normalizes a URL String.
+ */
+ fun toNormalizedURL(string: String): String {
+ val trimmedInput = string.trim()
+ var uri = Uri.parse(trimmedInput)
+ if (TextUtils.isEmpty(uri.scheme)) {
+ uri = Uri.parse("http://$trimmedInput")
+ } else {
+ uri = uri.normalizeScheme()
+ }
+ return uri.toString()
+ }
+
+ private val isURLLenient by lazy {
+ // Be lenient about what is classified as potentially a URL.
+ // (\w+-+)*[\w\[]+(://[/]*|:|\.)(\w+-+)*[\w\[:]+([\S&&[^\w-]]\S*)?
+ // -------- --------
+ // 0 or more pairs of consecutive word letters or dashes
+ // ------- --------
+ // followed by at least a single word letter or [ipv6::] character.
+ // --------------- ----------------
+ // Combined, that means "w", "w-w", "w-w-w", etc match, but "w-", "w-w-", "w-w-w-" do not.
+ // --------------
+ // That surrounds :, :// or .
+ // -
+ // At the end, there may be an optional
+ // ------------
+ // non-word, non-- but still non-space character (e.g., ':', '/', '.', '?' but not 'a', '-', '\t')
+ // ---
+ // and 0 or more non-space characters.
+ //
+ // These are some (odd) examples of valid urls according to this pattern:
+ // c-c.com
+ // c-c-c-c.c-c-c
+ // c-http://c.com
+ // about-mozilla:mozilla
+ // c-http.d-x
+ // www.c-
+ // 3-3.3
+ // www.c-c.-
+ //
+ // There are some examples of non-URLs according to this pattern:
+ // -://x.com
+ // -x.com
+ // http://www-.com
+ // www.c-c-
+ // 3-3
+ Pattern.compile(
+ "^\\s*(\\w+-+)*[\\w\\[]+(://[/]*|:|\\.)(\\w+-+)*[\\w\\[:]+([\\S&&[^\\w-]]\\S*)?\\s*$",
+ flags,
+ )
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val UNICODE_CHARACTER_CLASS: Int = 0x100
+
+ // To run tests on a non-Android device (like a computer), Pattern.compile
+ // requires a flag to enable unicode support. Set a value like flags here with a local
+ // copy of UNICODE_CHARACTER_CLASS. Use a local copy because that constant is not
+ // available on Android platforms < 24 (Fenix targets 21). At runtime this is not an issue
+ // because, again, Android REs are always unicode compliant.
+ // NB: The value has to go through an intermediate variable; otherwise, the linter will
+ // complain that this value is not one of the predefined enums that are allowed.
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var flags = 0
+
+ private const val HTTP = "http://"
+ private const val HTTPS = "https://"
+ private const val WWW = "www."
+
+ /**
+ * Generates a shorter version of the provided URL for display purposes by stripping it of
+ * https/http and/or WWW prefixes and/or trailing slash when applicable.
+ *
+ * The returned text will always be displayed from left to right.
+ * If the directionality would otherwise be RTL "\u200E" will be prepended to the result to force LTR.
+ */
+ fun toDisplayUrl(
+ originalUrl: CharSequence,
+ textDirectionHeuristic: TextDirectionHeuristicCompat = TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR,
+ ): CharSequence {
+ val strippedText = maybeStripTrailingSlash(maybeStripUrlProtocol(originalUrl))
+
+ return if (
+ strippedText.isNotBlank() &&
+ textDirectionHeuristic.isRtl(strippedText, 0, 1)
+ ) {
+ "\u200E" + strippedText
+ } else {
+ strippedText
+ }
+ }
+
+ private fun maybeStripUrlProtocol(url: CharSequence): CharSequence {
+ var noPrefixUrl = url
+ if (url.toString().startsWith(HTTPS)) {
+ noPrefixUrl = maybeStripUrlSubDomain(url.toString().replaceFirst(HTTPS, ""))
+ } else if (url.toString().startsWith(HTTP)) {
+ noPrefixUrl = maybeStripUrlSubDomain(url.toString().replaceFirst(HTTP, ""))
+ }
+ return noPrefixUrl
+ }
+
+ private fun maybeStripUrlSubDomain(url: CharSequence): CharSequence {
+ return if (url.toString().startsWith(WWW)) {
+ url.toString().replaceFirst(WWW, "")
+ } else {
+ url
+ }
+ }
+
+ private fun maybeStripTrailingSlash(url: CharSequence): CharSequence {
+ return url.trimEnd('/')
+ }
+
+ /**
+ * Determines whether a string is http or https URL
+ */
+ fun isHttpOrHttps(url: String): Boolean {
+ return !TextUtils.isEmpty(url) && (url.startsWith("http:") || url.startsWith("https:"))
+ }
+
+ /**
+ * Determine whether a string is a valid search query URL.
+ */
+ fun isValidSearchQueryUrl(url: String): Boolean {
+ var trimmedUrl = url.trim { it <= ' ' }
+ if (!trimmedUrl.matches("^.+?://.+?".toRegex())) {
+ // UI hint url doesn't have http scheme, so add it if necessary
+ trimmedUrl = "http://$trimmedUrl"
+ }
+ val isNetworkUrl = isHttpOrHttps(trimmedUrl)
+ val containsToken = trimmedUrl.contains("%s")
+ return isNetworkUrl && containsToken
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BootUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BootUtils.kt
new file mode 100644
index 0000000000..7c6af36e27
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BootUtils.kt
@@ -0,0 +1,63 @@
+/* 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.support.utils
+
+import android.content.Context
+import android.os.Build
+import android.provider.Settings
+import androidx.annotation.RequiresApi
+import java.io.File
+
+/**
+ * Provides access to system properties.
+ */
+interface BootUtils {
+
+ /**
+ * Gets the device boot count.
+ *
+ * **Only for Android versions N(24) and above.**
+ */
+ @RequiresApi(Build.VERSION_CODES.N)
+ fun getDeviceBootCount(context: Context): String
+
+ val deviceBootId: String?
+
+ val bootIdFileExists: Boolean
+
+ companion object {
+ /**
+ * @return either the boot count or a boot id depending on the device Android version.
+ */
+ fun getBootIdentifier(context: Context, bootUtils: BootUtils = BootUtilsImpl()): String {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ bootUtils.getDeviceBootCount(context)
+ } else {
+ return if (bootUtils.bootIdFileExists) {
+ bootUtils.deviceBootId ?: NO_BOOT_IDENTIFIER
+ } else {
+ NO_BOOT_IDENTIFIER
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implementation of [BootUtils].
+ */
+class BootUtilsImpl : BootUtils {
+ private val bootIdFile by lazy { File("/proc/sys/kernel/random/boot_id") }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun getDeviceBootCount(context: Context): String =
+ Settings.Global.getString(context.contentResolver, Settings.Global.BOOT_COUNT)
+
+ override val deviceBootId: String? by lazy { bootIdFile.readLines().firstOrNull()?.trim() }
+
+ override val bootIdFileExists: Boolean by lazy { bootIdFile.exists() }
+}
+
+private const val NO_BOOT_IDENTIFIER = "no boot identifier available"
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/Browsers.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/Browsers.kt
new file mode 100644
index 0000000000..a16aed4996
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/Browsers.kt
@@ -0,0 +1,431 @@
+/* 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.support.utils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.support.utils.ext.getPackageInfoCompat
+import mozilla.components.support.utils.ext.queryIntentActivitiesCompat
+import mozilla.components.support.utils.ext.resolveActivityCompat
+import java.util.HashMap
+
+/**
+ * Helpful tools for dealing with other browsers on this device.
+ *
+ * ```
+ * // Collect information about all installed browsers:
+ * val browsers = Browsers.all(context)
+ *
+ * // Collect information about installed browsers (and apps) that can handle a specific URL:
+ * val browsers = Browsers.forUrl(context, url)`
+ * ```
+ */
+@SuppressLint("QueryPermissionsNeeded") // Yes, this class needs the permission to read all packages
+class Browsers private constructor(
+ context: Context,
+ uri: Uri,
+) {
+ /**
+ * Enum of known browsers and their package names.
+ */
+ enum class KnownBrowser constructor(
+ val packageName: String,
+ ) {
+ FIREFOX("org.mozilla.firefox"),
+
+ FIREFOX_BETA("org.mozilla.firefox_beta"),
+ FIREFOX_AURORA("org.mozilla.fennec_aurora"),
+ FIREFOX_FENNEC_NIGHTLY("org.mozilla.fennec"),
+ FIREFOX_FDROID("org.mozilla.fennec_fdroid"),
+
+ FIREFOX_LITE("org.mozilla.rocket"),
+
+ FIREFOX_NIGHTLY("org.mozilla.fenix"),
+ FENIX_DEBUG("org.mozilla.fenix.debug"),
+
+ FIREFOX_FOCUS_DEBUG("org.mozilla.focus.debug"),
+ FIREFOX_FOCUS_NIGHTLY("org.mozilla.focus.nightly"),
+ FIREFOX_FOCUS_BETA("org.mozilla.focus.beta"),
+ FIREFOX_FOCUS("org.mozilla.focus"),
+
+ REFERENCE_BROWSER("org.mozilla.reference.browser"),
+ REFERENCE_BROWSER_DEBUG("org.mozilla.reference.browser.debug"),
+
+ CHROME("com.android.chrome"),
+ CHROME_BETA("com.chrome.beta"),
+ CHROME_DEV("com.chrome.dev"),
+ CHROME_CANARY("com.chrome.canary"),
+ CHROME_LOCAL_BUILD("com.google.android.apps.chrome"),
+ CHROMIUM_LOCAL_BUILD("org.chromium.chrome"),
+
+ OPERA("com.opera.browser"),
+ OPERA_BETA("com.opera.browser.beta"),
+ OPERA_MINI("com.opera.mini.native"),
+ OPERA_MINI_BETA("com.opera.mini.native.beta"),
+
+ UC_BROWSER("com.UCMobile.intl"),
+ UC_BROWSER_MINI("com.uc.browser.en"),
+
+ ANDROID_STOCK_BROWSER("com.android.browser"),
+
+ SAMSUNG_INTERNET("com.sec.android.app.sbrowser"),
+ SAMSUNG_INTERNET_BETA("com.sec.android.app.sbrowser.beta"),
+
+ ORFOX("info.guardianproject.orfox"),
+ TOR_BROWSER_ALPHA("org.torproject.torbrowser_alpha"),
+
+ MICROSOFT_EDGE("com.microsoft.emmx"),
+ DOLPHIN_BROWSER("mobi.mgeek.TunnyBrowser"),
+ BRAVE_BROWSER("com.brave.browser"),
+ LINK_BUBBLE("com.linkbubble.playstore"),
+ ADBLOCK_BROWSER("org.adblockplus.browser"),
+ CHROMER("arun.com.chromer"),
+ FLYNX("com.flynx"),
+ GHOSTERY_BROWSER("com.ghostery.android.ghostery"),
+ DUCKDUCKGO("com.duckduckgo.mobile.android"),
+ CLIQZ("com.cliqz.browser"),
+ }
+
+ private val packageName = context.packageName
+
+ private val browsers: Map<String, ActivityInfo> = {
+ val packageManager = context.packageManager
+
+ val browsers = resolveBrowsers(context, packageManager, uri)
+
+ // If there's a default browser set then modern Android systems won't return other browsers
+ // anymore when using queryIntentActivities(). That's annoying and our only option is
+ // to go through a list of known browsers and see if anyone of them is installed and
+ // wants to handle our URL.
+ findKnownBrowsers(packageManager, browsers, uri)
+
+ browsers
+ }()
+
+ /**
+ * The [ActivityInfo] of the default browser of the user (or null if none could be found).
+ */
+ val defaultBrowser: ActivityInfo? = findDefault(context, context.packageManager, uri)
+
+ /**
+ * The [ActivityInfo] of the installed Firefox, including Focus, browser (or null if none could be found).
+ */
+ val mozillaBrandedBrowser: ActivityInfo? = findMozillaBrandedBrowser()
+
+ /**
+ * The [ActivityInfo] of the installed Firefox browser (or null if none could be found).
+ *
+ * If multiple Firefox browsers are installed then this will
+ */
+ val firefoxBrandedBrowser: ActivityInfo? = findFirefoxBrandedBrowser()
+
+ /**
+ * Is there a Firefox browser installed on this device?
+ */
+ val hasFirefoxBrandedBrowserInstalled: Boolean = firefoxBrandedBrowser != null
+
+ /**
+ * Is Firefox (Release, Beta, Nightly) the default browser of the user?
+ */
+ val isFirefoxDefaultBrowser: Boolean
+ get() =
+ defaultBrowser != null && (
+ defaultBrowser.packageName == KnownBrowser.FIREFOX.packageName ||
+ defaultBrowser.packageName == KnownBrowser.FIREFOX_BETA.packageName ||
+ defaultBrowser.packageName == KnownBrowser.FIREFOX_AURORA.packageName ||
+ defaultBrowser.packageName == KnownBrowser.FIREFOX_FENNEC_NIGHTLY.packageName ||
+ defaultBrowser.packageName == KnownBrowser.FIREFOX_NIGHTLY.packageName ||
+ defaultBrowser.packageName == KnownBrowser.FIREFOX_FDROID.packageName
+ )
+
+ /**
+ * List of [ActivityInfo] of all known installed browsers.
+ */
+ val installedBrowsers: List<ActivityInfo> = browsers.values.toList()
+
+ /**
+ * Does this device have a default browser that is not Firefox (release) or **this** app calling the method.
+ */
+ val hasThirdPartyDefaultBrowser: Boolean = (
+ defaultBrowser != null &&
+ defaultBrowser.packageName != KnownBrowser.FIREFOX.packageName &&
+ !(mozillaBrandedBrowser != null && defaultBrowser.packageName == mozillaBrandedBrowser.packageName) &&
+ defaultBrowser.packageName != packageName
+ )
+
+ /**
+ * Does this device have multiple third-party browser installed?
+ */
+ val hasMultipleThirdPartyBrowsers: Boolean
+ get() {
+ if (browsers.size > 1) {
+ // There are more than us and Firefox.
+ return true
+ }
+
+ for (info in browsers.values) {
+ if (info !== defaultBrowser &&
+ info.packageName != KnownBrowser.FIREFOX.packageName &&
+ info.packageName != packageName
+ ) {
+ // There's at least one browser that is not *this app* or Firefox and also not the
+ // default browser.
+ return true
+ }
+ }
+
+ return false
+ }
+
+ /**
+ * Does this device have [browser] installed?
+ */
+ fun isInstalled(browser: KnownBrowser): Boolean {
+ return browsers.containsKey(browser.packageName)
+ }
+
+ /**
+ * Does this device have browser with [packageName] installed?
+ */
+ fun isInstalled(packageName: String): Boolean {
+ return browsers.containsKey(packageName)
+ }
+
+ /**
+ * Is **this** application the default browser?
+ */
+ val isDefaultBrowser: Boolean = defaultBrowser != null && packageName == defaultBrowser.packageName
+
+ private fun findMozillaBrandedBrowser(): ActivityInfo? {
+ return when {
+ browsers.containsKey(KnownBrowser.FIREFOX.packageName) ->
+ browsers[KnownBrowser.FIREFOX.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_BETA.packageName) ->
+ browsers[KnownBrowser.FIREFOX_BETA.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_AURORA.packageName) ->
+ browsers[KnownBrowser.FIREFOX_AURORA.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_NIGHTLY.packageName) ->
+ browsers[KnownBrowser.FIREFOX_NIGHTLY.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_FDROID.packageName) ->
+ browsers[KnownBrowser.FIREFOX_FDROID.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_FOCUS.packageName) ->
+ browsers[KnownBrowser.FIREFOX_FOCUS.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_FOCUS_DEBUG.packageName) ->
+ browsers[KnownBrowser.FIREFOX_FOCUS_DEBUG.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_FOCUS_BETA.packageName) ->
+ browsers[KnownBrowser.FIREFOX_FOCUS_BETA.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_FOCUS_NIGHTLY.packageName) ->
+ browsers[KnownBrowser.FIREFOX_FOCUS_NIGHTLY.packageName]
+ else -> null
+ }
+ }
+
+ private fun findFirefoxBrandedBrowser(): ActivityInfo? {
+ return when {
+ browsers.containsKey(KnownBrowser.FIREFOX.packageName) ->
+ browsers[KnownBrowser.FIREFOX.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_BETA.packageName) ->
+ browsers[KnownBrowser.FIREFOX_BETA.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_AURORA.packageName) ->
+ browsers[KnownBrowser.FIREFOX_AURORA.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_NIGHTLY.packageName) ->
+ browsers[KnownBrowser.FIREFOX_NIGHTLY.packageName]
+
+ browsers.containsKey(KnownBrowser.FIREFOX_FDROID.packageName) ->
+ browsers[KnownBrowser.FIREFOX_FDROID.packageName]
+
+ else -> null
+ }
+ }
+
+ private fun resolveBrowsers(
+ context: Context,
+ packageManager: PackageManager,
+ uri: Uri,
+ ): MutableMap<String, ActivityInfo> {
+ val browsers = HashMap<String, ActivityInfo>()
+ val resolvers = findResolvers(context, packageManager, includeThisApp = false, url = uri.toString())
+
+ for (info in resolvers) {
+ browsers[info.activityInfo.packageName] = info.activityInfo
+ }
+
+ return browsers
+ }
+
+ private fun findKnownBrowsers(
+ packageManager: PackageManager,
+ browsers: MutableMap<String, ActivityInfo>,
+ uri: Uri,
+ ) {
+ for (browser in KnownBrowser.values()) {
+ if (browsers.containsKey(browser.packageName)) {
+ continue
+ }
+
+ // resolveActivity() can be slow if the package isn't installed (e.g. 200ms on an N6 with a bad WiFi
+ // connection). Hence we query if the package is installed first, and only call resolveActivity for
+ // installed packages. getPackageInfo() is fast regardless of a package being installed
+ try {
+ // We don't need the result, we only need to detect when the package doesn't exist
+ packageManager.getPackageInfoCompat(browser.packageName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ continue
+ }
+
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = uri
+ intent.setPackage(browser.packageName)
+ intent.addCategory(Intent.CATEGORY_BROWSABLE)
+
+ val info = packageManager.resolveActivityCompat(intent, PackageManager.MATCH_DEFAULT_ONLY)
+ ?: continue
+
+ if (info.activityInfo == null || !info.activityInfo.exported) {
+ continue
+ }
+
+ browsers[info.activityInfo.packageName] = info.activityInfo
+ }
+ }
+
+ private fun findDefault(context: Context, packageManager: PackageManager, uri: Uri): ActivityInfo? {
+ val intent = Intent(Intent.ACTION_VIEW, uri)
+ intent.addCategory(Intent.CATEGORY_BROWSABLE)
+
+ val resolveInfo = packageManager.resolveActivityCompat(intent, PackageManager.MATCH_DEFAULT_ONLY)
+ ?: return null
+
+ if (resolveInfo.activityInfo == null || !resolveInfo.activityInfo.exported) {
+ // We are not allowed to launch this activity.
+ return null
+ }
+
+ return if (!browsers.containsKey(resolveInfo.activityInfo.packageName) &&
+ resolveInfo.activityInfo.packageName != context.packageName
+ ) {
+ // This default browser wasn't returned when we asked for *all* browsers. It's likely
+ // that this is actually the resolver activity (aka intent chooser). Let's ignore it.
+ null
+ } else {
+ resolveInfo.activityInfo
+ }
+ }
+
+ companion object {
+ @VisibleForTesting
+ internal const val SAMPLE_BROWSER_HTTP_URL = "http://www.mozilla.org/index.html"
+ private const val SAMPLE_BROWSER_HTTPS_URL = "https://www.mozilla.org/index.html"
+
+ // Sample URL handled by traditional web browsers. Used to find installed (basic) web browsers.
+ private val SAMPLE_BROWSER_URI = Uri.parse(SAMPLE_BROWSER_HTTP_URL)
+
+ /**
+ * Returns `true` is the provided [packageName] matches a known browser.
+ */
+ fun isBrowser(packageName: String): Boolean {
+ return KnownBrowser.values().asSequence().firstOrNull { browser ->
+ browser.packageName == packageName
+ } != null
+ }
+
+ /**
+ * Collect information about all installed browsers and return a [Browsers] object containing that data.
+ */
+ fun all(context: Context): Browsers = Browsers(context, SAMPLE_BROWSER_URI)
+
+ /**
+ * Collect information about all installed browsers that can handle the specified URL and return a [Browsers]
+ * object containing that data.
+ */
+ fun forUrl(context: Context, url: String) = Browsers(context, Uri.parse(url))
+
+ /**
+ * Finds all the [ResolveInfo] for the installed browsers.
+ * @return A list of all [ResolveInfo] for the installed browsers.
+ */
+ fun findResolvers(
+ context: Context,
+ packageManager: PackageManager,
+ includeThisApp: Boolean = true,
+ ): List<ResolveInfo> {
+ val httpIntent = Intent.parseUri(SAMPLE_BROWSER_HTTP_URL, Intent.URI_INTENT_SCHEME)
+ val httpsIntent = Intent.parseUri(SAMPLE_BROWSER_HTTPS_URL, Intent.URI_INTENT_SCHEME)
+
+ val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PackageManager.MATCH_ALL
+ } else {
+ PackageManager.MATCH_DEFAULT_ONLY
+ }
+
+ val httpResults = packageManager.queryIntentActivitiesCompat(httpIntent, flag)
+ .filter {
+ it.activityInfo.exported &&
+ (includeThisApp || it.activityInfo.packageName != context.packageName)
+ }
+
+ val httpsResults = packageManager.queryIntentActivitiesCompat(httpsIntent, flag)
+ .filter {
+ it.activityInfo.exported &&
+ (includeThisApp || it.activityInfo.packageName != context.packageName)
+ }
+
+ // There apps that have the same activityInfo.name to make it unique we
+ // combine the activityInfo.packageName + activityInfo.name
+ return (httpResults + httpsResults).distinctBy { it.activityInfo.packageName + it.activityInfo.name }
+ }
+
+ /**
+ * Finds all the [ResolveInfo] for the installed browsers that can handle the specified URL [url].
+ * @return A list of all [ResolveInfo] that correspond to the given [url].
+ */
+ fun findResolvers(
+ context: Context,
+ packageManager: PackageManager,
+ url: String,
+ includeThisApp: Boolean = true,
+ contentType: String? = null,
+ ): List<ResolveInfo> {
+ val uri = url.toUri()
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ if (contentType != null) setDataAndTypeAndNormalize(uri, contentType) else data = uri
+ addCategory(Intent.CATEGORY_BROWSABLE)
+ }
+
+ val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PackageManager.MATCH_ALL
+ } else {
+ PackageManager.MATCH_DEFAULT_ONLY
+ }
+ return packageManager.queryIntentActivitiesCompat(intent, flag)
+ .orEmpty()
+ .filter {
+ it.activityInfo.exported && (
+ includeThisApp ||
+ it.activityInfo.packageName != context.packageName
+ )
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BrowsersCache.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BrowsersCache.kt
new file mode 100644
index 0000000000..8831f605e8
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BrowsersCache.kt
@@ -0,0 +1,52 @@
+/* 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.support.utils
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Caches the list of browsers installed on a user's device.
+ *
+ * BrowsersCache caches the list of installed browsers is gathered lazily when it is first accessed
+ * after initial creation or invalidation. For that reason, a context is required every time
+ * the cache is accessed.
+ *
+ * Users are responsible for invalidating the cache at the appropriate time. It is left up to the
+ * user to determine appropriate policies for maintaining the validity of the cache. If, when the
+ * cache is accessed, it is filled, the contents will be returned. As mentioned above, the cache
+ * will be lazily refilled after invalidation. In other words, invalidation is O(1).
+ *
+ * This cache is threadsafe.
+ */
+object BrowsersCache {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var cachedBrowsers: Browsers? = null
+
+ /**
+ * Return installed browsers if cache exist. If not, Collect information about all installed
+ * browsers and return a [Browsers] object containing that data.
+ */
+ @Synchronized
+ fun all(context: Context): Browsers {
+ run {
+ val cachedBrowsers = cachedBrowsers
+ if (cachedBrowsers != null) {
+ return cachedBrowsers
+ }
+ }
+ return Browsers.all(context).also {
+ this.cachedBrowsers = it
+ }
+ }
+
+ /**
+ * Remove installed browsers cache
+ */
+ @Synchronized
+ fun resetAll() {
+ cachedBrowsers = null
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ColorUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ColorUtils.kt
new file mode 100644
index 0000000000..31f24efc52
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ColorUtils.kt
@@ -0,0 +1,45 @@
+/* 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.support.utils
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import androidx.core.graphics.ColorUtils
+
+object ColorUtils {
+
+ /**
+ * Get text color (white or black) that is readable on top of the provided background color.
+ */
+ @JvmStatic
+ fun getReadableTextColor(@ColorInt backgroundColor: Int): Int {
+ return if (isDark(backgroundColor)) Color.WHITE else Color.BLACK
+ }
+
+ /**
+ * @return true if the given [color] is dark enough that white text should be used on top of it.
+ */
+ @JvmStatic
+ @SuppressWarnings("MagicNumber")
+ fun isDark(@ColorInt color: Int): Boolean {
+ if (color == Color.TRANSPARENT || ColorUtils.calculateLuminance(color) >= 0.5) {
+ return false
+ }
+
+ val greyValue = grayscaleFromRGB(color)
+ // 186 chosen rather than the seemingly obvious 128 because of gamma.
+ return greyValue < 186
+ }
+
+ @SuppressWarnings("MagicNumber")
+ private fun grayscaleFromRGB(@ColorInt color: Int): Int {
+ val red = Color.red(color)
+ val green = Color.green(color)
+ val blue = Color.blue(color)
+ // Magic weighting taken from a stackoverflow post, supposedly related to how
+ // humans perceive color.
+ return (0.299 * red + 0.587 * green + 0.114 * blue).toInt()
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/CreditCardUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/CreditCardUtils.kt
new file mode 100644
index 0000000000..4007474acf
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/CreditCardUtils.kt
@@ -0,0 +1,307 @@
+/* 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.support.utils
+
+import androidx.annotation.DrawableRes
+import mozilla.components.support.utils.ext.toCreditCardNumber
+import kotlin.math.floor
+import kotlin.math.log10
+
+/**
+ * Information about a credit card issuing network.
+ *
+ * @param name The name of the credit card issuer network.
+ * @param icon The icon of the credit card issuer network.
+ */
+data class CreditCardIssuerNetwork(
+ val name: String,
+ @DrawableRes val icon: Int,
+)
+
+/**
+ * Information about a credit card issuer identification numbers.
+ *
+ * @param startRange The start issuer identification number range.
+ * @param endRange The end issuer identification number range.
+ * @param cardNumberMaxLength A list of the range of maximum card number lengths.
+ */
+data class CreditCardIIN(
+ val creditCardIssuerNetwork: CreditCardIssuerNetwork,
+ val startRange: Int,
+ val endRange: Int,
+ val cardNumberMaxLength: List<Int>,
+)
+
+/**
+ * Enum of supported credit card network types. This list mirrors the networks from
+ * https://searchfox.org/mozilla-central/source/toolkit/modules/CreditCard.jsm
+ */
+enum class CreditCardNetworkType(val cardName: String) {
+ AMEX("amex"),
+ CARTEBANCAIRE("cartebancaire"),
+ DINERS("diners"),
+ DISCOVER("discover"),
+ JCB("jcb"),
+ MASTERCARD("mastercard"),
+ MIR("mir"),
+ UNIONPAY("unionpay"),
+ VISA("visa"),
+ GENERIC(""),
+}
+
+/**
+ * A mapping of credit card numbers to their respective credit card issuers.
+ */
+@Suppress("MagicNumber", "LargeClass")
+internal object CreditCardUtils {
+
+ private val GENERIC = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.GENERIC.cardName,
+ icon = R.drawable.ic_icon_credit_card_generic,
+ )
+ private val AMEX = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.AMEX.cardName,
+ icon = R.drawable.ic_cc_logo_amex,
+ )
+ private val CARTEBANCAIRE = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.CARTEBANCAIRE.cardName,
+ icon = R.drawable.ic_icon_credit_card_generic,
+ )
+ private val DINERS = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.DINERS.cardName,
+ icon = R.drawable.ic_cc_logo_diners,
+ )
+ private val DISCOVER = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.DISCOVER.cardName,
+ icon = R.drawable.ic_cc_logo_discover,
+ )
+ private val JCB = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.JCB.cardName,
+ icon = R.drawable.ic_cc_logo_jcb,
+ )
+ private val MIR = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.MIR.cardName,
+ icon = R.drawable.ic_cc_logo_mir,
+ )
+ private val UNIONPAY = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.UNIONPAY.cardName,
+ icon = R.drawable.ic_cc_logo_unionpay,
+ )
+ private val VISA = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.VISA.cardName,
+ icon = R.drawable.ic_cc_logo_visa,
+ )
+ private val MASTERCARD = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.MASTERCARD.cardName,
+ icon = R.drawable.ic_cc_logo_mastercard,
+ )
+
+ /**
+ * Map of recognized credit card issuer network name to their [CreditCardIssuerNetwork].
+ */
+ private val creditCardIssuers = mapOf(
+ CreditCardNetworkType.AMEX.cardName to AMEX,
+ CreditCardNetworkType.CARTEBANCAIRE.cardName to CARTEBANCAIRE,
+ CreditCardNetworkType.DINERS.cardName to DINERS,
+ CreditCardNetworkType.DISCOVER.cardName to DISCOVER,
+ CreditCardNetworkType.JCB.cardName to JCB,
+ CreditCardNetworkType.MIR.cardName to MIR,
+ CreditCardNetworkType.UNIONPAY.cardName to UNIONPAY,
+ CreditCardNetworkType.VISA.cardName to VISA,
+ CreditCardNetworkType.MASTERCARD.cardName to MASTERCARD,
+ )
+
+ /**
+ * List of recognized credit card issuer networks.
+ *
+ * Based on https://searchfox.org/mozilla-central/rev/4275e4bd2b2aba34b2c69b314c4b50bdf83520af/toolkit/modules/CreditCard.jsm#40
+ */
+ private val creditCardIINs = listOf(
+ CreditCardIIN(
+ creditCardIssuerNetwork = AMEX,
+ startRange = 34,
+ endRange = 34,
+ cardNumberMaxLength = listOf(15),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = AMEX,
+ startRange = 37,
+ endRange = 37,
+ cardNumberMaxLength = listOf(15),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = CARTEBANCAIRE,
+ startRange = 4035,
+ endRange = 4035,
+ cardNumberMaxLength = listOf(16),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = CARTEBANCAIRE,
+ startRange = 4360,
+ endRange = 4360,
+ cardNumberMaxLength = listOf(16),
+ ),
+ // We diverge from Wikipedia here, because Diners card
+ // support length of 14-19.
+ CreditCardIIN(
+ creditCardIssuerNetwork = DINERS,
+ startRange = 300,
+ endRange = 305,
+ cardNumberMaxLength = listOf(14, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = DINERS,
+ startRange = 3095,
+ endRange = 3095,
+ cardNumberMaxLength = listOf(14, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = DINERS,
+ startRange = 36,
+ endRange = 36,
+ cardNumberMaxLength = listOf(14, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = DINERS,
+ startRange = 38,
+ endRange = 39,
+ cardNumberMaxLength = listOf(14, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = DISCOVER,
+ startRange = 6011,
+ endRange = 6011,
+ cardNumberMaxLength = listOf(16, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = DISCOVER,
+ startRange = 622126,
+ endRange = 622925,
+ cardNumberMaxLength = listOf(16, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = DISCOVER,
+ startRange = 624000,
+ endRange = 626999,
+ cardNumberMaxLength = listOf(16, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = DISCOVER,
+ startRange = 628200,
+ endRange = 628899,
+ cardNumberMaxLength = listOf(16, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = DISCOVER,
+ startRange = 64,
+ endRange = 65,
+ cardNumberMaxLength = listOf(16, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = JCB,
+ startRange = 3528,
+ endRange = 3589,
+ cardNumberMaxLength = listOf(16, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = MASTERCARD,
+ startRange = 2221,
+ endRange = 2720,
+ cardNumberMaxLength = listOf(16),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = MASTERCARD,
+ startRange = 51,
+ endRange = 55,
+ cardNumberMaxLength = listOf(16),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = MIR,
+ startRange = 2200,
+ endRange = 2204,
+ cardNumberMaxLength = listOf(16),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = UNIONPAY,
+ startRange = 62,
+ endRange = 62,
+ cardNumberMaxLength = listOf(16, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = UNIONPAY,
+ startRange = 81,
+ endRange = 81,
+ cardNumberMaxLength = listOf(16, 19),
+ ),
+ CreditCardIIN(
+ creditCardIssuerNetwork = VISA,
+ startRange = 4,
+ endRange = 4,
+ cardNumberMaxLength = listOf(16),
+ ),
+ ).sortedWith { a, b -> b.startRange - a.startRange }
+
+ /**
+ * Returns the [CreditCardIIN] for the provided credit card number.
+ *
+ * Based on https://searchfox.org/mozilla-central/rev/4275e4bd2b2aba34b2c69b314c4b50bdf83520af/toolkit/modules/CreditCard.jsm#229
+ *
+ * @param cardNumber The credit card number.
+ * @return the [CreditCardIIN] for the provided credit card number or null if it does not
+ * match any of the recognized credit card issuers.
+ */
+ @Suppress("ComplexMethod")
+ fun getCreditCardIIN(cardNumber: String): CreditCardIIN? {
+ val safeCardNumber = cardNumber.toCreditCardNumber()
+ for (issuer in creditCardIINs) {
+ if (issuer.cardNumberMaxLength.size == 1 &&
+ issuer.cardNumberMaxLength[0] != safeCardNumber.length
+ ) {
+ continue
+ } else if (issuer.cardNumberMaxLength.size > 1 &&
+ (
+ safeCardNumber.length < issuer.cardNumberMaxLength[0] ||
+ safeCardNumber.length > issuer.cardNumberMaxLength[1]
+ )
+ ) {
+ continue
+ }
+
+ val prefixLength = floor(log10(issuer.startRange.toDouble())) + 1
+ val prefix = safeCardNumber.substring(0, prefixLength.toInt()).toInt()
+
+ if (prefix >= issuer.startRange && prefix <= issuer.endRange) {
+ return issuer
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Returns the [CreditCardIssuerNetwork] for the provided credit card issuer network name.
+ *
+ * @param cardType The credit card issuer network name.
+ * @return the [CreditCardIssuerNetwork] for the provided credit card issuer network.
+ */
+ fun getCreditCardIssuerNetwork(cardType: String): CreditCardIssuerNetwork =
+ creditCardIssuers[cardType] ?: GENERIC
+}
+
+/**
+ * Returns the [CreditCardIIN] for the provided credit card number.
+ *
+ * @return the [CreditCardIIN] for the provided credit card number or null if it does not
+ * match any of the recognized credit card issuers.
+ */
+fun String.creditCardIIN() = CreditCardUtils.getCreditCardIIN(this)
+
+/**
+ * Returns the [CreditCardIssuerNetwork] for the provided credit card type.
+ *
+ * @return the [CreditCardIssuerNetwork] for the provided credit card type orr null if it
+ * does not match any of recognized credit card type.
+ */
+fun String.creditCardIssuerNetwork() = CreditCardUtils.getCreditCardIssuerNetwork(this)
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DomainMatcher.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DomainMatcher.kt
new file mode 100644
index 0000000000..867499eb33
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DomainMatcher.kt
@@ -0,0 +1,108 @@
+/* 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/. */
+
+@file:Suppress("MatchingDeclarationName")
+
+package mozilla.components.support.utils
+
+import android.net.Uri
+import java.net.MalformedURLException
+import java.util.Locale
+
+data class DomainMatch(val url: String, val matchedSegment: String)
+
+// FIXME implement Fennec-style segment matching logic
+// See https://github.com/mozilla-mobile/android-components/issues/1279
+fun segmentAwareDomainMatch(query: String, urls: Iterable<String>): DomainMatch? {
+ val locale = Locale.US
+
+ val caseInsensitiveQuery = query.lowercase(locale)
+ // Process input 'urls' lazily, as the list could be very large and likely we'll find a match
+ // by going through just a small subset.
+ val caseInsensitiveUrls = urls.asSequence().map { it.lowercase(locale) }
+
+ return basicMatch(caseInsensitiveQuery, caseInsensitiveUrls)?.let { matchedUrl ->
+ matchSegment(caseInsensitiveQuery, matchedUrl)?.let { DomainMatch(matchedUrl, it) }
+ }
+}
+
+/**
+ * Check if [url] starts with the exact [text] or if [url]'s domain start with the exact [text].
+ *
+ * @return `true` if [url] starts with [text], `false` otherwise.
+ */
+fun doesUrlStartsWithText(url: String, text: String) = findUrlMatchingText(url, text) != null
+
+private fun basicMatch(query: String, urls: Sequence<String>): String? {
+ return urls.firstOrNull { findUrlMatchingText(it, query) != null }
+}
+
+@SuppressWarnings("ReturnCount")
+private fun findUrlMatchingText(url: String, text: String): String? {
+ if (url.startsWith(text)) {
+ return url
+ }
+
+ val uri = try {
+ Uri.parse(url)
+ } catch (e: MalformedURLException) {
+ null
+ }
+
+ var urlSansProtocol = uri?.host
+ urlSansProtocol += uri?.port?.orEmpty() + uri?.path
+ urlSansProtocol?.let {
+ if (it.startsWith(text) || it.noCommonSubdomains().startsWith(text)) {
+ return url
+ }
+ }
+
+ val host = uri?.host ?: ""
+
+ if (host.startsWith(text)) {
+ return url
+ }
+
+ if (host.noCommonSubdomains().startsWith(text)) {
+ return url
+ }
+
+ return null
+}
+
+private fun matchSegment(query: String, rawUrl: String): String? {
+ if (rawUrl.startsWith(query)) {
+ return rawUrl
+ }
+
+ val url = Uri.parse(rawUrl)
+ return url.host?.let { host ->
+ if (host.startsWith(query)) {
+ host + url.port.orEmpty() + url.path
+ } else {
+ val strippedHost = host.noCommonSubdomains()
+ if (strippedHost != url.host) {
+ strippedHost + url.port.orEmpty() + url.path
+ } else {
+ host + url.port.orEmpty() + url.path
+ }
+ }
+ }
+}
+
+private fun String.noCommonSubdomains(): String {
+ // This kind of stripping allows us to match "twitter" to "mobile.twitter.com".
+ val domainsToStrip = listOf("www", "mobile", "m")
+
+ domainsToStrip.forEach { domain ->
+ if (this.startsWith(domain)) {
+ return this.substring(domain.length + 1)
+ }
+ }
+ return this
+}
+
+private fun Int?.orEmpty(): String {
+ return this.takeIf { it != -1 }?.let { ":$it" }.orEmpty()
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DownloadUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DownloadUtils.kt
new file mode 100644
index 0000000000..e9c60fbb77
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DownloadUtils.kt
@@ -0,0 +1,390 @@
+/* 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.support.utils
+
+import android.net.Uri
+import android.os.Environment
+import android.webkit.MimeTypeMap
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.UnsupportedEncodingException
+import java.util.regex.Pattern
+
+object DownloadUtils {
+
+ /**
+ * This is the regular expression to match the content disposition type segment.
+ *
+ * A content disposition header can start either with inline or attachment followed by comma;
+ * For example: attachment; filename="filename.jpg" or inline; filename="filename.jpg"
+ * (inline|attachment)\\s*; -> Match either inline or attachment, followed by zero o more
+ * optional whitespaces characters followed by a comma.
+ *
+ */
+ private const val contentDispositionType = "(inline|attachment)\\s*;"
+
+ /**
+ * This is the regular expression to match filename* parameter segment.
+ *
+ * A content disposition header could have an optional filename* parameter,
+ * the difference between this parameter and the filename is that this uses
+ * the encoding defined in RFC 5987.
+ *
+ * Some examples:
+ * filename*=utf-8''success.html
+ * filename*=iso-8859-1'en'file%27%20%27name.jpg
+ * filename*=utf-8'en'filename.jpg
+ *
+ * For matching this section we use:
+ * \\s*filename\\s*=\\s*= -> Zero or more optional whitespaces characters
+ * followed by filename followed by any zero or more whitespaces characters and the equal sign;
+ *
+ * (utf-8|iso-8859-1)-> Either utf-8 or iso-8859-1 encoding types.
+ *
+ * '[^']*'-> Zero or more characters that are inside of single quotes '' that are not single
+ * quote.
+ *
+ * (\S*) -> Zero or more characters that are not whitespaces. In this group,
+ * it's where we are going to have the filename.
+ *
+ */
+ private const val contentDispositionFileNameAsterisk =
+ "\\s*filename\\*\\s*=\\s*(utf-8|iso-8859-1)'[^']*'([^;\\s]*)"
+
+ /**
+ * Format as defined in RFC 2616 and RFC 5987
+ * Both inline and attachment types are supported.
+ * More details can be found
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
+ *
+ * The first segment is the [contentDispositionType], there you can find the documentation,
+ * Next, it's the filename segment, where we have a filename="filename.ext"
+ * For example, all of these could be possible in this section:
+ * filename="filename.jpg"
+ * filename="file\"name.jpg"
+ * filename="file\\name.jpg"
+ * filename="file\\\"name.jpg"
+ * filename=filename.jpg
+ *
+ * For matching this section we use:
+ * \\s*filename\\s*=\\s*= -> Zero or more whitespaces followed by filename followed
+ * by zero or more whitespaces and the equal sign.
+ *
+ * As we want to extract the the content of filename="THIS", we use:
+ *
+ * \\s* -> Zero or more whitespaces
+ *
+ * (\"((?:\\\\.|[^|"\\\\])*)\" -> A quotation mark, optional : or \\ or any character,
+ * and any non quotation mark or \\\\ zero or more times.
+ *
+ * For example: filename="file\\name.jpg", filename="file\"name.jpg" and filename="file\\\"name.jpg"
+ *
+ * We don't want to match after ; appears, For example filename="filename.jpg"; foo
+ * we only want to match before the semicolon, so we use. |[^;]*)
+ *
+ * \\s* -> Zero or more whitespaces.
+ *
+ * For supporting cases, where we have both filename and filename*, we use:
+ * "(?:;$contentDispositionFileNameAsterisk)?"
+ *
+ * Some examples:
+ *
+ * attachment; filename="_.jpg"; filename*=iso-8859-1'en'file%27%20%27name.jpg
+ * attachment; filename="_.jpg"; filename*=iso-8859-1'en'file%27%20%27name.jpg
+ */
+ private val contentDispositionPattern = Pattern.compile(
+ contentDispositionType +
+ "\\s*filename\\s*=\\s*(\"((?:\\\\.|[^\"\\\\])*)\"|[^;]*)\\s*" +
+ "(?:;$contentDispositionFileNameAsterisk)?",
+ Pattern.CASE_INSENSITIVE,
+ )
+
+ /**
+ * This is an alternative content disposition pattern where only filename* is available
+ */
+ private val fileNameAsteriskContentDispositionPattern = Pattern.compile(
+ contentDispositionType +
+ contentDispositionFileNameAsterisk,
+ Pattern.CASE_INSENSITIVE,
+ )
+
+ /**
+ * Keys for the capture groups inside contentDispositionPattern
+ */
+ private const val ENCODED_FILE_NAME_GROUP = 5
+ private const val ENCODING_GROUP = 4
+ private const val QUOTED_FILE_NAME_GROUP = 3
+ private const val UNQUOTED_FILE_NAME = 2
+
+ /**
+ * Belongs to the [fileNameAsteriskContentDispositionPattern]
+ */
+ private const val ALTERNATIVE_FILE_NAME_GROUP = 3
+ private const val ALTERNATIVE_ENCODING_GROUP = 2
+
+ /**
+ * Definition as per RFC 5987, section 3.2.1. (value-chars)
+ */
+ private val encodedSymbolPattern = Pattern.compile("%[0-9a-f]{2}|[0-9a-z!#$&+-.^_`|~]", Pattern.CASE_INSENSITIVE)
+
+ /**
+ * Keep aligned with desktop generic content types:
+ * https://searchfox.org/mozilla-central/source/browser/components/downloads/DownloadsCommon.jsm#208
+ */
+ private val GENERIC_CONTENT_TYPES = arrayOf(
+ "application/octet-stream",
+ "binary/octet-stream",
+ "application/unknown",
+ )
+
+ /**
+ * Maximum number of characters for the title length.
+ *
+ * Android OS is Linux-based and therefore would have the limitations of the linux filesystem
+ * it uses under the hood. To the best of our knowledge, Android only supports EXT3, EXT4,
+ * exFAT, and EROFS filesystems. From these three, the maximum file name length is 255.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits"/>
+ */
+ private const val MAX_FILE_NAME_LENGTH = 255
+
+ /**
+ * The HTTP response code for a successful request.
+ */
+ const val RESPONSE_CODE_SUCCESS = 200
+
+ /**
+ * Guess the name of the file that should be downloaded.
+ *
+ * This method is largely identical to [android.webkit.URLUtil.guessFileName]
+ * which unfortunately does not implement RFC 5987.
+ */
+
+ @JvmStatic
+ fun guessFileName(
+ contentDisposition: String?,
+ destinationDirectory: String?,
+ url: String?,
+ mimeType: String?,
+ ): String {
+ // Split fileName between base and extension
+ // Add an extension if filename does not have one
+ val extractedFileName = extractFileNameFromUrl(contentDisposition, url)
+ val sanitizedMimeType = sanitizeMimeType(mimeType)
+
+ val fileName = if (extractedFileName.contains('.')) {
+ if (GENERIC_CONTENT_TYPES.contains(sanitizedMimeType)) {
+ extractedFileName
+ } else {
+ changeExtension(extractedFileName, sanitizedMimeType)
+ }
+ } else {
+ extractedFileName + createExtension(sanitizedMimeType)
+ }
+
+ return destinationDirectory?.let {
+ uniqueFileName(Environment.getExternalStoragePublicDirectory(destinationDirectory), fileName)
+ } ?: fileName
+ }
+
+ // Some site add extra information after the mimetype, for example 'application/pdf; qs=0.001'
+ // we just want to extract the mimeType and ignore the rest.
+ fun sanitizeMimeType(mimeType: String?): String? {
+ return (
+ if (mimeType != null) {
+ if (mimeType.contains(";")) {
+ mimeType.substringBefore(";")
+ } else {
+ mimeType
+ }
+ } else {
+ null
+ }
+ )?.trim()
+ }
+
+ /**
+ * Checks if the file exists so as not to overwrite one already in the destination directory
+ */
+ fun uniqueFileName(directory: File, fileName: String): String {
+ var potentialFileName = File(directory, fileName)
+ val baseFileName = potentialFileName.nameWithoutExtension
+ val fileExtension = potentialFileName.extension.let {
+ if (it.isNotEmpty()) {
+ ".$it"
+ } else {
+ it
+ }
+ }
+
+ var copyVersionNumber = 1
+
+ while (potentialFileName.exists()) {
+ potentialFileName = File(directory, "$baseFileName($copyVersionNumber)$fileExtension")
+ copyVersionNumber += 1
+ }
+
+ return potentialFileName.name
+ }
+
+ /**
+ * Create a Content Disposition formatted string with the receiver used as the filename and
+ * file extension set as PDF.
+ *
+ * This is primarily useful for connecting the "Save to PDF" feature response to downloads.
+ */
+ fun makePdfContentDisposition(filename: String): String {
+ val pdfExtension = ".pdf"
+ return if (filename.endsWith(pdfExtension)) {
+ filename.substringBeforeLast('.')
+ } else {
+ filename
+ }.take(MAX_FILE_NAME_LENGTH - pdfExtension.length)
+ .run {
+ "attachment; filename=$this$pdfExtension;"
+ }
+ }
+
+ private fun extractFileNameFromUrl(contentDisposition: String?, url: String?): String {
+ var filename: String? = null
+
+ // Extract file name from content disposition header field
+ if (contentDisposition != null) {
+ filename = parseContentDisposition(contentDisposition)?.substringAfterLast('/')
+ }
+
+ // If all the other http-related approaches failed, use the plain uri
+ if (filename.isNullOrEmpty()) {
+ // If there is a query string strip it, same as desktop browsers
+ val decodedUrl: String? = Uri.decode(url)?.substringBefore('?')
+ if (decodedUrl?.endsWith('/') == false) {
+ filename = decodedUrl.substringAfterLast('/')
+ }
+ }
+
+ // Finally, if couldn't get filename from URI, get a generic filename
+ if (filename == null) {
+ filename = "downloadfile"
+ }
+
+ return filename
+ }
+
+ private fun parseContentDisposition(contentDisposition: String): String? {
+ return try {
+ parseContentDispositionWithFileName(contentDisposition)
+ ?: parseContentDispositionWithFileNameAsterisk(contentDisposition)
+ } catch (ex: IllegalStateException) {
+ // This function is defined as returning null when it can't parse the header
+ null
+ } catch (ex: UnsupportedEncodingException) {
+ // Do nothing
+ null
+ }
+ }
+
+ private fun parseContentDispositionWithFileName(contentDisposition: String): String? {
+ val m = contentDispositionPattern.matcher(contentDisposition)
+ return if (m.find()) {
+ val encodedFileName = m.group(ENCODED_FILE_NAME_GROUP)
+ val encoding = m.group(ENCODING_GROUP)
+ if (encodedFileName != null && encoding != null) {
+ decodeHeaderField(encodedFileName, encoding)
+ } else {
+ // Return quoted string if available and replace escaped characters.
+ val quotedFileName = m.group(QUOTED_FILE_NAME_GROUP)
+ quotedFileName?.replace("\\\\(.)".toRegex(), "$1")
+ ?: m.group(UNQUOTED_FILE_NAME)
+ }
+ } else {
+ null
+ }
+ }
+
+ private fun parseContentDispositionWithFileNameAsterisk(contentDisposition: String): String? {
+ val alternative = fileNameAsteriskContentDispositionPattern.matcher(contentDisposition)
+
+ return if (alternative.find()) {
+ val encoding = alternative.group(ALTERNATIVE_ENCODING_GROUP) ?: return null
+ val fileName = alternative.group(ALTERNATIVE_FILE_NAME_GROUP) ?: return null
+ decodeHeaderField(fileName, encoding)
+ } else {
+ null
+ }
+ }
+
+ @Throws(UnsupportedEncodingException::class)
+ private fun decodeHeaderField(field: String, encoding: String): String {
+ val m = encodedSymbolPattern.matcher(field)
+ val stream = ByteArrayOutputStream()
+
+ while (m.find()) {
+ val symbol = m.group()
+
+ if (symbol.startsWith("%")) {
+ stream.write(symbol.substring(1).toInt(radix = 16))
+ } else {
+ stream.write(symbol[0].code)
+ }
+ }
+
+ return stream.toString(encoding)
+ }
+
+ /**
+ * Compare the filename extension with the mime type and change it if necessary.
+ */
+ private fun changeExtension(filename: String, providedMimeType: String?): String {
+ val file = File(filename)
+ val mimeTypeMap = MimeTypeMap.getSingleton()
+ val extensionFromMimeType =
+ mimeTypeMap.getExtensionFromMimeType(providedMimeType)
+ if (providedMimeType == null || extensionFromMimeType == null) return filename
+
+ val mimeTypeFromFilename = mimeTypeMap.getMimeTypeFromExtension(file.extension) ?: ""
+
+ val fileHasPossibleExtension = filename.contains(extensionFromMimeType, ignoreCase = true)
+ val isFileMimeTypeDifferentFromProvidedMimeType =
+ !mimeTypeFromFilename.equals(
+ providedMimeType,
+ ignoreCase = true,
+ )
+
+ // Mimetypes could have multiple possible file extensions, for example: text/html could have
+ // either .htm or .html extensions. Since [getExtensionFromMimeType]
+ // we try to only rename when there is a clear indication the existing extension is wrong.
+ return if (isFileMimeTypeDifferentFromProvidedMimeType && !fileHasPossibleExtension) {
+ return "${file.nameWithoutExtension}.$extensionFromMimeType"
+ } else {
+ filename
+ }
+ }
+
+ /**
+ * Guess the extension for a file using the mime type.
+ */
+ private fun createExtension(mimeType: String?): String {
+ var extension: String? = null
+
+ if (mimeType != null) {
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
+ }
+ if (extension == null) {
+ extension = if (mimeType?.startsWith("text/", ignoreCase = true) == true) {
+ // checking startsWith to ignoring encoding value such as "text/html; charset=utf-8"
+ if (mimeType.startsWith("text/html", ignoreCase = true)) {
+ ".html"
+ } else {
+ ".txt"
+ }
+ } else {
+ // If there's no mime type assume binary data
+ ".bin"
+ }
+ }
+
+ return extension
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DrawableUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DrawableUtils.kt
new file mode 100644
index 0000000000..bccd1bbb42
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/DrawableUtils.kt
@@ -0,0 +1,43 @@
+/* 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.support.utils
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.drawable.DrawableCompat
+
+object DrawableUtils {
+ /**
+ * Return a tinted drawable object associated with a particular resource ID.
+ */
+ fun loadAndTintDrawable(
+ context: Context,
+ @DrawableRes resourceId: Int,
+ @ColorInt color: Int,
+ ): Drawable {
+ val drawable = ResourcesCompat.getDrawable(context.resources, resourceId, context.theme)
+ val wrapped = DrawableCompat.wrap(drawable!!.mutate())
+ DrawableCompat.setTint(wrapped, color)
+ return wrapped
+ }
+
+ /**
+ * Return a color state tinted drawable object associated with a particular resource ID.
+ */
+ fun loadAndTintDrawable(
+ context: Context,
+ @DrawableRes resourceId: Int,
+ colorStateList: ColorStateList,
+ ): Drawable {
+ val drawable = ResourcesCompat.getDrawable(context.resources, resourceId, context.theme)
+ val wrapped = DrawableCompat.wrap(drawable!!.mutate())
+ DrawableCompat.setTintList(wrapped, colorStateList)
+ return wrapped
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ManufacturerCodes.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ManufacturerCodes.kt
new file mode 100644
index 0000000000..7cf8c39146
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ManufacturerCodes.kt
@@ -0,0 +1,32 @@
+/* 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.support.utils
+
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Used to check if a device is from a specific manufacturer,
+ * using the value returned by [android.os.Build.MANUFACTURER].
+ */
+object ManufacturerCodes {
+ // Manufacturer codes taken from https://developers.google.com/zero-touch/resources/manufacturer-names
+ private const val HUAWEI: String = "Huawei"
+ private const val SAMSUNG = "Samsung"
+ private const val XIAOMI = "Xiaomi"
+ private const val ONE_PLUS = "OnePlus"
+ private const val LG = "LGE"
+ private const val OPPO = "OPPO"
+
+ @VisibleForTesting
+ internal var manufacturer = Build.MANUFACTURER // is a var for testing purposes
+
+ val isHuawei get() = manufacturer.equals(HUAWEI, ignoreCase = true)
+ val isSamsung get() = manufacturer.equals(SAMSUNG, ignoreCase = true)
+ val isXiaomi get() = manufacturer.equals(XIAOMI, ignoreCase = true)
+ val isOnePlus get() = manufacturer.equals(ONE_PLUS, ignoreCase = true)
+ val isLG get() = manufacturer.equals(LG, ignoreCase = true)
+ val isOppo get() = manufacturer.equals(OPPO, ignoreCase = true)
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/PendingIntentUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/PendingIntentUtils.kt
new file mode 100644
index 0000000000..93491db04e
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/PendingIntentUtils.kt
@@ -0,0 +1,28 @@
+/* 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.support.utils
+
+import android.app.PendingIntent
+import android.os.Build
+
+/**
+ * Helper methods for when working with [PendingIntent]s.
+ */
+object PendingIntentUtils {
+ /**
+ * Android 6 introduced FLAG_IMMUTABLE to prevents apps that receive a PendingIntent from
+ * filling in unpopulated properties. Android 12 requires mutability explicitly.
+ *
+ * This property will return:
+ * - PendingIntent.FLAG_IMMUTABLE - for Android API 23+
+ * - 0 (framework default flags) - for Android APIs lower than 23.
+ */
+ val defaultFlags
+ get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE
+ } else {
+ 0 // No flags. Default behavior.
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/Performance.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/Performance.kt
new file mode 100644
index 0000000000..6fbb9a15f3
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/Performance.kt
@@ -0,0 +1,26 @@
+/* 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.support.utils
+
+import android.os.SystemClock
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Executes the given [block] and logs the elapsed time in milliseconds.
+ * Uses [System.nanoTime] for measurements, since it isn't tied to a wall-clock.
+ *
+ * @param logger [Logger] to use for logging.
+ * @param op Name of the operation [block] performs.
+ * @param block A lambda to measure.
+ * @return [T] result of running [block].
+ */
+@SuppressWarnings("MagicNumber")
+inline fun <T> logElapsedTime(logger: Logger, op: String, block: () -> T): T {
+ logger.info("$op...")
+ val start = SystemClock.elapsedRealtimeNanos()
+ val res = block()
+ logger.info("'$op' took ${(SystemClock.elapsedRealtimeNanos() - start) / 1_000_000} ms")
+ return res
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/RunWhenReadyQueue.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/RunWhenReadyQueue.kt
new file mode 100644
index 0000000000..4dffa3b58f
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/RunWhenReadyQueue.kt
@@ -0,0 +1,65 @@
+/* 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.support.utils
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * A queue that acts as a gate, either executing tasks right away if the queue is marked as "ready",
+ * i.e. gate is open, or queues them to be executed whenever the queue is marked as ready in the
+ * future, i.e. gate becomes open.
+ */
+class RunWhenReadyQueue(
+ private val scope: CoroutineScope = MainScope(),
+) {
+
+ private val tasks = mutableListOf<() -> Unit>()
+ private val isReady = AtomicBoolean(false)
+
+ /**
+ * Was this queue ever marked as 'ready' via a call to [ready]?
+ *
+ * @return Boolean value indicating if this queue is 'ready'.
+ */
+ fun isReady(): Boolean = isReady.get()
+
+ /**
+ * Runs the [task] if this queue is marked as ready, or queues it for later execution.
+ *
+ * @param task: The task to run now if queue is ready or queue for later execution.
+ */
+ fun runIfReadyOrQueue(task: () -> Unit) {
+ if (isReady.get()) {
+ scope.launch { task.invoke() }
+ } else {
+ synchronized(tasks) {
+ tasks.add(task)
+ }
+ }
+ }
+
+ /**
+ * Mark queue as ready. Pending tasks will execute, and all tasks passed to [runIfReadyOrQueue]
+ * after this point will be executed immediately.
+ */
+ fun ready() {
+ // Make sure that calls to `ready` are idempotent.
+ if (!isReady.compareAndSet(false, true)) {
+ return
+ }
+
+ scope.launch {
+ synchronized(tasks) {
+ for (task in tasks) {
+ task.invoke()
+ }
+ tasks.clear()
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeBundle.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeBundle.kt
new file mode 100644
index 0000000000..8599b7104b
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeBundle.kt
@@ -0,0 +1,57 @@
+/* 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.support.utils
+
+import android.os.Bundle
+import android.os.Parcelable
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.ext.getParcelableCompat
+
+/**
+ * See SafeIntent for more background: applications can put garbage values into Bundles. This is primarily
+ * experienced when there's garbage in the Intent's Bundle. However that Bundle can contain further bundles,
+ * and we need to handle those defensively too.
+ *
+ * See bug 1090385 for more.
+ */
+class SafeBundle(val unsafe: Bundle) {
+
+ fun getInt(name: String, defaultValue: Int = 0): Int =
+ safeAccess(defaultValue) { getInt(name, defaultValue) }!!
+
+ fun getString(name: String): String? =
+ safeAccess { getString(name) }
+
+ fun keySet(): Set<String>? =
+ safeAccess { keySet() }
+
+ /**
+ * Returns the value associated with the given key or null.
+ * @param name the key name.
+ * @param clazz the desired class of the object .
+ * null is returned when the Object is not of type clazz, no mapping exists for that key name
+ * or a value of null is explicitly associated with the key.
+ */
+ fun <T : Parcelable> getParcelable(name: String, clazz: Class<T>): T? =
+ safeAccess { getParcelableCompat(name, clazz) }
+
+ @SuppressWarnings("TooGenericExceptionCaught")
+ private fun <T> safeAccess(default: T? = null, block: Bundle.() -> T): T? {
+ return try {
+ block(unsafe)
+ } catch (e: OutOfMemoryError) {
+ Logger.warn("Couldn't get bundle items: OOM. Malformed?")
+ default
+ } catch (e: RuntimeException) {
+ Logger.warn("Couldn't get bundle items.", e)
+ default
+ }
+ }
+}
+
+/**
+ * Returns a [SafeBundle] for the given [Bundle].
+ */
+fun Bundle.toSafeBundle() = SafeBundle(this)
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeIntent.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeIntent.kt
new file mode 100644
index 0000000000..9cd8df391f
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeIntent.kt
@@ -0,0 +1,117 @@
+/* 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.support.utils
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.Parcelable
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.ext.getParcelableArrayListExtraCompat
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import java.util.ArrayList
+
+/**
+ * External applications can pass values into Intents that can cause us to crash: in defense,
+ * we wrap [Intent] and catch the exceptions they may force us to throw. See bug 1090385
+ * for more.
+ */
+class SafeIntent(val unsafe: Intent) {
+
+ val extras: Bundle?
+ get() = safeAccess { unsafe.extras }
+
+ val action: String?
+ get() = unsafe.action
+
+ val flags: Int
+ get() = unsafe.flags
+
+ val isLauncherIntent: Boolean
+ get() = unsafe.categories?.contains(Intent.CATEGORY_LAUNCHER) == true && Intent.ACTION_MAIN == unsafe.action
+
+ val dataString: String?
+ get() = safeAccess { unsafe.dataString }
+
+ val data: Uri?
+ get() = safeAccess { unsafe.data }
+
+ val categories: Set<String>?
+ get() = safeAccess { unsafe.categories }
+
+ fun hasExtra(name: String): Boolean = safeAccess(false) {
+ unsafe.hasExtra(name)
+ }!!
+
+ fun getBooleanExtra(name: String, defaultValue: Boolean): Boolean = safeAccess(defaultValue) {
+ unsafe.getBooleanExtra(name, defaultValue)
+ }!!
+
+ fun getIntExtra(name: String, defaultValue: Int): Int = safeAccess(defaultValue) {
+ unsafe.getIntExtra(name, defaultValue)
+ }!!
+
+ fun getStringExtra(name: String): String? = safeAccess {
+ unsafe.getStringExtra(name)
+ }
+
+ fun getBundleExtra(name: String): SafeBundle? = safeAccess {
+ unsafe.getBundleExtra(name)?.toSafeBundle()
+ }
+
+ fun getCharSequenceExtra(name: String): CharSequence? = safeAccess {
+ unsafe.getCharSequenceExtra(name)
+ }
+
+ /**
+ * Returns the value of an item previously added with putExtra()
+ * or null if no Parcelable value was found.
+ * @param name the key name.
+ * @param clazz the desired class of the object .
+ */
+ fun <T : Parcelable> getParcelableExtra(name: String, clazz: Class<T>): T? = safeAccess {
+ unsafe.getParcelableExtraCompat(name, clazz)
+ }
+
+ /**
+ * Returns the value of an item previously added with putParcelableArrayListExtra(),
+ * or null if no ArrayList value was found.
+ * @param name the key name.
+ * @param clazz the desired class of the object .
+ */
+ fun <T : Parcelable> getParcelableArrayListExtra(name: String, clazz: Class<T>): ArrayList<T>? {
+ return safeAccess {
+ val value: ArrayList<T>? = unsafe.getParcelableArrayListExtraCompat(name, clazz)
+ value
+ }
+ }
+
+ fun getStringArrayListExtra(name: String): ArrayList<String>? = safeAccess {
+ getStringArrayListExtra(name)
+ }
+
+ @SuppressWarnings("TooGenericExceptionCaught")
+ private fun <T> safeAccess(default: T? = null, block: Intent.() -> T): T? {
+ return try {
+ block(unsafe)
+ } catch (e: OutOfMemoryError) {
+ Logger.warn("Could not read from intent: OOM. Malformed?")
+ default
+ } catch (e: RuntimeException) {
+ Logger.warn("Could not read from intent.", e)
+ default
+ }
+ }
+}
+
+/**
+ * Returns a [SafeIntent] for the given [Intent].
+ */
+fun Intent.toSafeIntent(): SafeIntent = SafeIntent(this)
+
+const val EXTRA_ACTIVITY_REFERRER_PACKAGE = "activity_referrer_package"
+const val EXTRA_ACTIVITY_REFERRER_CATEGORY = "activity_referrer_category"
+
+const val INTENT_TYPE_PDF = "application/pdf"
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeUrl.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeUrl.kt
new file mode 100644
index 0000000000..34702016f4
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/SafeUrl.kt
@@ -0,0 +1,39 @@
+/* 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.support.utils
+
+import android.content.Context
+
+/**
+ * Collection of methods used for ensuring the validity and security of URLs.
+ */
+object SafeUrl {
+ /**
+ * Remove recursively the schemes declared in [R.array.mozac_url_schemes_blocklist]
+ * from the front of [unsafeText].
+ */
+ fun stripUnsafeUrlSchemes(context: Context, unsafeText: CharSequence?): String? {
+ if (unsafeText.isNullOrBlank()) {
+ return null
+ }
+
+ val urlSchemesBlocklist = context.resources.getStringArray(R.array.mozac_url_schemes_blocklist)
+ var safeUrl = unsafeText.toString()
+
+ @Suppress("ControlFlowWithEmptyBody", "EmptyWhileBlock")
+ while (urlSchemesBlocklist.find {
+ if (safeUrl.startsWith(it, true)) {
+ safeUrl = safeUrl.replaceFirst(Regex(it, RegexOption.IGNORE_CASE), "")
+ true
+ } else {
+ false
+ }
+ } != null
+ ) {
+ }
+
+ return safeUrl
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/StatusBarUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/StatusBarUtils.kt
new file mode 100644
index 0000000000..16d0634279
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/StatusBarUtils.kt
@@ -0,0 +1,30 @@
+/* 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.support.utils
+
+import android.view.View
+import androidx.core.view.ViewCompat
+import mozilla.components.support.utils.ext.top
+
+object StatusBarUtils {
+ private var statusBarSize = -1
+
+ /**
+ * Determine the height of the status bar asynchronously.
+ */
+ @Suppress("unused")
+ fun getStatusBarHeight(view: View, block: (Int) -> Unit) {
+ if (statusBarSize > 0) {
+ block(statusBarSize)
+ } else {
+ ViewCompat.setOnApplyWindowInsetsListener(view) { _, insetsCompat ->
+ statusBarSize = insetsCompat.top()
+ block(statusBarSize)
+ view.setOnApplyWindowInsetsListener(null)
+ insetsCompat
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/StorageUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/StorageUtils.kt
new file mode 100644
index 0000000000..33b2005f2f
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/StorageUtils.kt
@@ -0,0 +1,46 @@
+/* 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.support.utils
+
+object StorageUtils {
+
+ // Borrowed from https://gist.github.com/ademar111190/34d3de41308389a0d0d8
+ fun levenshteinDistance(a: String, b: String): Int {
+ val lhsLength = a.length
+ val rhsLength = b.length
+
+ // Levenshtein distance upper bound is at most the length of the longer string.
+ // However, for our use case we want distance from an empty string to a non-empty string to
+ // be arbitrarily high; otherwise, an empty string will be of varying distances from strings
+ // of varying lengths. This is the correct result for Levenshtein distance, but an incorrect
+ // outcome for our domain. In other words, Levenshtein distance isn't exactly what we need.
+ if (lhsLength == 0 || rhsLength == 0) {
+ return Int.MAX_VALUE
+ }
+
+ var cost = Array(lhsLength) { it }
+ var newCost = Array(lhsLength) { 0 }
+
+ for (i in 1 until rhsLength) {
+ newCost[0] = i
+
+ for (j in 1 until lhsLength) {
+ val match = if (a[j - 1] == b[i - 1]) 0 else 1
+
+ val costReplace = cost[j - 1] + match
+ val costInsert = cost[j] + 1
+ val costDelete = newCost[j - 1] + 1
+
+ newCost[j] = Math.min(Math.min(costInsert, costDelete), costReplace)
+ }
+
+ val swap = cost
+ cost = newCost
+ newCost = swap
+ }
+
+ return cost[lhsLength - 1]
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ThreadUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ThreadUtils.kt
new file mode 100644
index 0000000000..e69128f64d
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ThreadUtils.kt
@@ -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/. */
+
+package mozilla.components.support.utils
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+
+@Suppress("unused")
+object ThreadUtils {
+ private val looperBackgroundThread by lazy {
+ HandlerThread("BackgroundThread")
+ }
+ private val looperBackgroundHandler by lazy {
+ looperBackgroundThread.start()
+ Handler(looperBackgroundThread.looper)
+ }
+ private val handler = Handler(Looper.getMainLooper())
+ private val uiThread = Looper.getMainLooper().thread
+
+ private var handlerForTest: Handler? = null
+
+ private fun backgroundHandler(): Handler {
+ return handlerForTest ?: looperBackgroundHandler
+ }
+
+ fun setHandlerForTest(handler: Handler) {
+ handlerForTest = handler
+ }
+
+ fun postToBackgroundThread(runnable: Runnable) {
+ backgroundHandler().post(runnable)
+ }
+
+ fun postToBackgroundThread(runnable: () -> Unit) {
+ backgroundHandler().post(runnable)
+ }
+
+ fun postToMainThread(runnable: Runnable) {
+ handler.post(runnable)
+ }
+
+ fun postToMainThreadDelayed(runnable: Runnable, delayMillis: Long) {
+ handler.postDelayed(runnable, delayMillis)
+ }
+
+ fun assertOnUiThread() {
+ val currentThread = Thread.currentThread()
+ val currentThreadId = currentThread.id
+ val expectedThreadId = uiThread.id
+
+ if (currentThreadId == expectedThreadId) {
+ return
+ }
+
+ throw IllegalThreadStateException("Expected UI thread, but running on " + currentThread.name)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/TimePicker.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/TimePicker.kt
new file mode 100644
index 0000000000..b3a1fab69e
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/TimePicker.kt
@@ -0,0 +1,32 @@
+/* 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.support.utils
+
+/**
+ * Contains methods used to change the TimePickerDialog according to the step value.
+ */
+object TimePicker {
+
+ private const val SHOW_SECONDS_PICKER_THRESHOLD = 60f
+ private const val SHOW_MILLISECONDS_PICKER_THRESHOLD = 1f
+
+ /**
+ * Whether or not we should display the milliseconds picker
+ * based on the value of the step attribute
+ *
+ * @param step Value of the step attribute
+ */
+ fun shouldShowMillisecondsPicker(step: Float?): Boolean =
+ step != null && step < SHOW_MILLISECONDS_PICKER_THRESHOLD
+
+ /**
+ * Whether or not we should display the seconds picker
+ * based on the value of the step attribute
+ *
+ * @param step Value of the step attribute
+ */
+ fun shouldShowSecondsPicker(step: Float?): Boolean =
+ step != null && step < SHOW_SECONDS_PICKER_THRESHOLD
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/WebURLFinder.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/WebURLFinder.kt
new file mode 100644
index 0000000000..b211f774bb
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/WebURLFinder.kt
@@ -0,0 +1,162 @@
+/* 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.support.utils
+
+import android.webkit.URLUtil
+import java.net.URI
+import java.net.URISyntaxException
+import java.util.LinkedList
+import java.util.Locale
+import java.util.regex.Pattern
+
+/**
+ * Regular expressions used in this class are taken from Android's Patterns.java.
+ * We brought them in to standardize URL matching across Android versions, instead of relying
+ * on Android version-dependent built-ins that can vary across Android versions.
+ * The original code can be found here:
+ * http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/java/android/util/Patterns.java
+ */
+class WebURLFinder {
+ private val candidates: List<String>
+
+ constructor(string: String?) {
+ if (string == null) {
+ throw IllegalArgumentException("string must not be null")
+ }
+ this.candidates = candidateWebURLs(string)
+ }
+
+ // package-private
+ internal constructor(string: String?, explicitUnicode: Boolean) {
+ if (string == null) {
+ throw IllegalArgumentException("strings must not be null")
+ }
+ this.candidates = candidateWebURLs(string, explicitUnicode)
+ }
+
+ // package-private
+ internal constructor(strings: List<String>?, explicitUnicode: Boolean) {
+ if (strings == null) {
+ throw IllegalArgumentException("strings must not be null")
+ }
+ this.candidates = candidateWebURLs(strings, explicitUnicode)
+ }
+
+ /**
+ * Return best Web URL.
+ *
+ *
+ * "Best" means a Web URL with a scheme, and failing that, a Web URL without a
+ * scheme.
+ *
+ * @return a Web URL or `null`.
+ */
+ fun bestWebURL(): String? = firstWebURLWithScheme() ?: firstWebURLWithoutScheme()
+
+ private fun firstWebURLWithScheme(): String? {
+ candidates.forEach { match ->
+ try {
+ if (URI(match).scheme != null) {
+ return match
+ }
+ } catch (e: URISyntaxException) {
+ // Ignore: on to the next.
+ }
+ }
+
+ return null
+ }
+
+ private fun firstWebURLWithoutScheme(): String? = candidates.firstOrNull()
+
+ @SuppressWarnings("LargeClass")
+ companion object {
+ // Taken from mozilla.components.support.ktx.util.URLStringUtils. See documentation
+ // there for a complete description.
+ private const val autolinkWebUrlPattern =
+ "(\\w+-+)*[\\w\\[]+(://[/]*|:|\\.)(\\w+-+)*[\\w\\[:]+([\\S&&[^\\w-]]\\S*)?"
+
+ private val autolinkWebUrl by lazy {
+ Pattern.compile(autolinkWebUrlPattern, 0)
+ }
+
+ private val autolinkWebUrlExplicitUnicode by lazy {
+ // To run tests on a non-Android device (like a computer), Pattern.compile
+ // requires a flag to enable unicode support. Set a value like flags here with a local
+ // copy of UNICODE_CHARACTER_CLASS. Use a local copy because that constant is not
+ // available on Android platforms < 24 (Fenix targets 21). At runtime this is not an issue
+ // because, again, Android REs are always unicode compliant.
+ // NB: The value has to go through an intermediate variable; otherwise, the linter will
+ // complain that this value is not one of the predefined enums that are allowed.
+ @Suppress("MagicNumber")
+ val UNICODE_CHARACTER_CLASS: Int = 0x100
+ var regexFlags = UNICODE_CHARACTER_CLASS
+ Pattern.compile(autolinkWebUrlPattern, regexFlags)
+ }
+
+ /**
+ * Checks if the given [String] is a valid web URL.
+ *
+ * Valid URI schemes: 'http:', 'https:', 'about:', 'data:'.
+ *
+ * Invalid URI schemes: 'file:', 'javascript:', 'content:'.
+ *
+ * @return True if the [String] is a valid web URL.
+ */
+ @SuppressWarnings("TooGenericExceptionCaught")
+ fun String.isValidWebURL() = try {
+ URI(this)
+
+ val safeUri = lowercase(Locale.ROOT)
+ !safeUri.isInvalidUriScheme()
+ } catch (e: Exception) {
+ false
+ }
+
+ private fun String.isInvalidUriScheme() =
+ URLUtil.isFileUrl(this) || URLUtil.isJavaScriptUrl(this) || URLUtil.isContentUrl(this)
+
+ private fun candidateWebURLs(strings: Collection<String?>, explicitUnicode: Boolean = false): List<String> {
+ val candidates = mutableListOf<String>()
+
+ // no functional transformation lambdas (ie. flatMapNotNull) since it would turn an
+ // O(n) loop into an O(2n) loop
+ for (string in strings) {
+ if (string == null) {
+ continue
+ }
+
+ candidates.addAll(candidateWebURLs(string, explicitUnicode))
+ }
+
+ return candidates
+ }
+
+ private fun candidateWebURLs(string: String, explicitUnicode: Boolean = false): List<String> {
+ val matcher = when {
+ explicitUnicode -> autolinkWebUrlExplicitUnicode.matcher(string)
+ else -> autolinkWebUrl.matcher(string)
+ }
+
+ val matches = LinkedList<String>()
+
+ while (matcher.find()) {
+ // Remove URLs with bad schemes.
+ if (!matcher.group().isValidWebURL()) {
+ continue
+ }
+
+ // Remove parts of email addresses.
+ if (matcher.start() > 0 && string[matcher.start() - 1] == '@') {
+ continue
+ }
+
+ matches.add(matcher.group())
+ }
+
+ return matches
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Bitmap.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Bitmap.kt
new file mode 100644
index 0000000000..286afff677
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Bitmap.kt
@@ -0,0 +1,31 @@
+/* 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.support.utils.ext
+
+import android.graphics.Bitmap
+import android.util.Size
+
+/**
+ * Scales a [Bitmap] to the given [size] while maintaining the aspect ratio.
+ *
+ * @param size The new [Size] to scale the [Bitmap] to.
+ *
+ * @return the scaled [Size].
+ */
+fun Bitmap.resizeMaintainingAspectRatio(size: Size) = if (width > height) {
+ // Scale a wide bitmap
+ val newMaxWidth = size.width
+ val aspectRatio = height.toFloat() / width.toFloat()
+ val scaledHeight = (newMaxWidth * aspectRatio).toInt()
+
+ Size(newMaxWidth, scaledHeight)
+} else {
+ // Scale square or tall bitmap
+ val newMaxHeight = size.height
+ val aspectRatio = width.toFloat() / height.toFloat()
+ val scaledWidth = (newMaxHeight * aspectRatio).toInt()
+
+ Size(scaledWidth, newMaxHeight)
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Bundle.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Bundle.kt
new file mode 100644
index 0000000000..da65efd0bc
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Bundle.kt
@@ -0,0 +1,66 @@
+/* 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.support.utils.ext
+
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import java.io.Serializable
+import java.util.ArrayList
+
+/**
+ * Retrieve extended data from the bundle.
+ */
+fun <T> Bundle.getParcelableCompat(name: String, clazz: Class<T>): T? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getParcelable(name, clazz)
+ } else {
+ @Suppress("DEPRECATION")
+ getParcelable(name) as? T?
+ }
+}
+
+/**
+ * Retrieve extended data from the bundle.
+ */
+fun <T : Serializable> Bundle.getSerializableCompat(name: String, clazz: Class<T>): Serializable? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getSerializable(name, clazz)
+ } else {
+ @Suppress("DEPRECATION")
+ getSerializable(name)
+ }
+}
+
+/**
+ * Retrieve extended data from the bundle.
+ */
+fun <T : Parcelable> Bundle.getParcelableArrayListCompat(name: String, clazz: Class<T>): ArrayList<T>? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getParcelableArrayList(name, clazz)
+ } else {
+ @Suppress("DEPRECATION")
+ getParcelableArrayList(name)
+ }
+}
+
+/**
+ * Retrieve extended data from the bundle.
+ */
+inline fun <reified T : Parcelable> Bundle.getParcelableArrayCompat(name: String, clazz: Class<T>): Array<T>? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getParcelableArray(name, clazz)
+ } else {
+ @Suppress("DEPRECATION")
+ getParcelableArray(name)?.safeCastToArrayOfT()
+ }
+}
+
+/**
+ * Cast a [Parcelable] [Array] to a <T implements [Parcelable]> [Array]
+ */
+inline fun <reified T : Parcelable> Array<Parcelable>.safeCastToArrayOfT(): Array<T> {
+ return filterIsInstance<T>().toTypedArray()
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Context.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Context.kt
new file mode 100644
index 0000000000..bdd1d6515c
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Context.kt
@@ -0,0 +1,89 @@
+/* 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.support.utils.ext
+
+import android.annotation.SuppressLint
+import android.content.ActivityNotFoundException
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Configuration
+import android.os.Build
+import android.provider.Settings
+import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat
+import androidx.core.os.bundleOf
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.ManufacturerCodes
+
+const val SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key"
+const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"
+const val DEFAULT_BROWSER_APP_OPTION = "default_browser"
+const val ACTION_MANAGE_DEFAULT_APPS_SETTINGS_HUAWEI = "com.android.settings.PREFERRED_SETTINGS"
+private val logger = Logger("navigateToDefaultBrowserAppsSettings")
+
+/**
+ * Open OS settings for default browser.
+ */
+@RequiresApi(Build.VERSION_CODES.N)
+fun Context.navigateToDefaultBrowserAppsSettings() {
+ val intent = when {
+ ManufacturerCodes.isHuawei -> Intent(ACTION_MANAGE_DEFAULT_APPS_SETTINGS_HUAWEI)
+ else -> Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS).apply {
+ putExtra(
+ SETTINGS_SELECT_OPTION_KEY,
+ DEFAULT_BROWSER_APP_OPTION,
+ )
+ putExtra(
+ SETTINGS_SHOW_FRAGMENT_ARGS,
+ bundleOf(SETTINGS_SELECT_OPTION_KEY to DEFAULT_BROWSER_APP_OPTION),
+ )
+ }
+ }
+
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ logger.error("ActivityNotFoundException " + e.message.toString())
+ }
+}
+
+/**
+ * Context Context to retrieve service from.
+ * @param broadcastReceiver The BroadcastReceiver to handle the broadcast.
+ * @param filter Selects the Intent broadcasts to be received.
+ * @param exportedFlag [ContextCompat.RECEIVER_EXPORTED], if the receiver
+ * should be able to receiver broadcasts from other applications, or
+ * [ContextCompat.RECEIVER_NOT_EXPORTED] if the receiver should be able
+ * to receive broadcasts only from the system or from within the app.
+ *
+ * @return The first sticky intent found that matches [filter],
+ * or null if there are none.
+ */
+@SuppressLint("UnspecifiedRegisterReceiverFlag")
+fun Context.registerReceiverCompat(
+ broadcastReceiver: BroadcastReceiver?,
+ filter: IntentFilter,
+ exportedFlag: Int,
+): Intent? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ContextCompat.registerReceiver(
+ this,
+ broadcastReceiver,
+ filter,
+ exportedFlag,
+ )
+ } else {
+ registerReceiver(broadcastReceiver, filter)
+ }
+}
+
+/**
+ * @return True if the orientation is landscape,or false if it's not.
+ */
+fun Context.isLandscape(): Boolean {
+ return resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Fragment.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Fragment.kt
new file mode 100644
index 0000000000..7a038d784a
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Fragment.kt
@@ -0,0 +1,26 @@
+/* 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.support.utils.ext
+
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.Fragment
+
+/**
+ * Requests permissions and handles results inside a [Fragment].
+ */
+fun Fragment.requestInPlacePermissions(
+ requestKey: String,
+ permissionsToRequest: Array<String>,
+ onResult: (Map<String, Boolean>) -> Unit,
+) {
+ requireActivity().activityResultRegistry.register(
+ requestKey,
+ ActivityResultContracts.RequestMultiplePermissions(),
+ ) { permissions ->
+ onResult(permissions)
+ }.also {
+ it.launch(permissionsToRequest)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Intent.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Intent.kt
new file mode 100644
index 0000000000..14990a539f
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Intent.kt
@@ -0,0 +1,47 @@
+/* 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.support.utils.ext
+
+import android.content.Intent
+import android.os.Build
+import android.os.Parcelable
+import java.io.Serializable
+import java.util.ArrayList
+
+/**
+ * Retrieve extended data from the intent.
+ */
+fun <T> Intent.getParcelableExtraCompat(name: String, clazz: Class<T>): T? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getParcelableExtra(name, clazz)
+ } else {
+ @Suppress("DEPRECATION")
+ getParcelableExtra(name) as? T?
+ }
+}
+
+/**
+ * Retrieve extended data from the intent.
+ */
+fun <T : Parcelable> Intent.getParcelableArrayListExtraCompat(name: String, clazz: Class<T>): ArrayList<T>? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getParcelableArrayListExtra(name, clazz)
+ } else {
+ @Suppress("DEPRECATION")
+ getParcelableArrayListExtra(name)
+ }
+}
+
+/**
+ * Retrieve extended data from the intent.
+ */
+fun <T : Serializable> Intent.getSerializableExtraCompat(name: String, clazz: Class<T>): Serializable? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getSerializableExtra(name, clazz)
+ } else {
+ @Suppress("DEPRECATION")
+ getSerializableExtra(name)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/PackageManager.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/PackageManager.kt
new file mode 100644
index 0000000000..50e4ade71d
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/PackageManager.kt
@@ -0,0 +1,68 @@
+/* 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.support.utils.ext
+
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Build
+
+/**
+ * Get [ResolveInfo] list for an [Intent] with a specified flag
+ *
+ * @param intent The name of the package to check for.
+ */
+fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flag: Int): MutableList<ResolveInfo> {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(flag.toLong()))
+ } else {
+ @Suppress("DEPRECATION")
+ queryIntentActivities(intent, flag)
+ }
+}
+
+/**
+ * Get [ResolveInfo] for an [Intent] with a specified flag
+ *
+ * @param intent The name of the package to check for.
+ */
+fun PackageManager.resolveActivityCompat(intent: Intent, flag: Int): ResolveInfo? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ resolveActivity(intent, PackageManager.ResolveInfoFlags.of(flag.toLong()))
+ } else {
+ @Suppress("DEPRECATION")
+ resolveActivity(intent, flag)
+ }
+}
+
+/**
+ * Get a package info with a specified flag
+ *
+ * @param packageName The name of the package to check for.
+ */
+fun PackageManager.getPackageInfoCompat(packageName: String, flag: Int): PackageInfo {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flag.toLong()))
+ } else {
+ @Suppress("DEPRECATION")
+ getPackageInfo(packageName, flag)
+ }
+}
+
+/**
+ * Get a application info with a specified flag
+ *
+ * @param host The URI host.
+ */
+fun PackageManager.getApplicationInfoCompat(host: String, flag: Int): ApplicationInfo {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getApplicationInfo(host, PackageManager.ApplicationInfoFlags.of(flag.toLong()))
+ } else {
+ @Suppress("DEPRECATION")
+ getApplicationInfo(host, flag)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Pair.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Pair.kt
new file mode 100644
index 0000000000..5529aa8ce0
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Pair.kt
@@ -0,0 +1,21 @@
+/* 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.support.utils.ext
+
+/**
+ * @returns null if either [first] or [second] is null. Otherwise returns a [Pair] of non-null
+ * values.
+ *
+ * Example:
+ * (Object, Object).toNullablePair() == Pair<Object, Object>
+ * (null, Object).toNullablePair() == null
+ * (Object, null).toNullablePair() == null
+ */
+fun <T, U> Pair<T?, U?>.toNullablePair(): Pair<T, U>? =
+ if (first != null && second != null) {
+ first!! to second!!
+ } else {
+ null
+ }
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Service.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Service.kt
new file mode 100644
index 0000000000..3d56c0a141
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/Service.kt
@@ -0,0 +1,26 @@
+/* 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.support.utils.ext
+
+import android.app.Service
+import android.app.Service.STOP_FOREGROUND_DETACH
+import android.app.Service.STOP_FOREGROUND_REMOVE
+import android.os.Build
+
+/**
+ * Remove this service from foreground state.
+ * @param removeNotification whether the notification is to be removed or not.
+ */
+fun Service.stopForegroundCompat(removeNotification: Boolean) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ when (removeNotification) {
+ true -> stopForeground(STOP_FOREGROUND_REMOVE)
+ false -> stopForeground(STOP_FOREGROUND_DETACH)
+ }
+ } else {
+ @Suppress("DEPRECATION")
+ stopForeground(removeNotification)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/String.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/String.kt
new file mode 100644
index 0000000000..163624802c
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/String.kt
@@ -0,0 +1,13 @@
+/* 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.support.utils.ext
+
+/**
+ * Strips characters other than digits from a string.
+ * Used to strip a credit card number user input of spaces and separators.
+ */
+fun String.toCreditCardNumber(): String {
+ return this.filter { it.isDigit() }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/WindowInsetsCompat.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/WindowInsetsCompat.kt
new file mode 100644
index 0000000000..fcad9b1d4f
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/ext/WindowInsetsCompat.kt
@@ -0,0 +1,38 @@
+/* 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.support.utils.ext
+
+import androidx.core.graphics.Insets
+import androidx.core.view.WindowInsetsCompat
+
+/**
+ * Returns the top system window inset in pixels.
+ */
+fun WindowInsetsCompat.top(): Int =
+ this.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars()).top
+
+/**
+ * Returns the right system window inset in pixels.
+ */
+fun WindowInsetsCompat.right(): Int =
+ this.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars()).right
+
+/**
+ * Returns the left system window inset in pixels.
+ */
+fun WindowInsetsCompat.left(): Int =
+ this.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars()).left
+
+/**
+ * Returns the bottom system window inset in pixels.
+ */
+fun WindowInsetsCompat.bottom(): Int =
+ this.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars()).bottom
+
+/**
+ * Returns the mandatory system gesture insets.
+ */
+fun WindowInsetsCompat.mandatorySystemGestureInsets(): Insets =
+ this.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures())
diff --git a/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/intents.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/intents.kt
new file mode 100644
index 0000000000..666c9a66da
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/intents.kt
@@ -0,0 +1,33 @@
+/* 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/. */
+
+@file:JvmName("IntentUtils")
+
+package mozilla.components.support.utils
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+
+/**
+ * Create a [PendingIntent] instance to run a certain service described with the [Intent].
+ *
+ * This method will allow you to launch a service that will be able to overpass
+ * [background service limitations](https://developer.android.com/about/versions/oreo/background#services)
+ * introduced in Android Oreo.
+ *
+ * @param context an [Intent] to start a service.
+ */
+@JvmName("createForegroundServicePendingIntent")
+fun Intent.asForegroundServicePendingIntent(
+ context: Context,
+ requestCode: Int,
+ flags: Int = PendingIntentUtils.defaultFlags or PendingIntent.FLAG_UPDATE_CURRENT,
+): PendingIntent =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ PendingIntent.getForegroundService(context, requestCode, this, flags)
+ } else {
+ PendingIntent.getService(context, requestCode, this, flags)
+ }
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_amex.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_amex.xml
new file mode 100644
index 0000000000..535799953a
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_amex.xml
@@ -0,0 +1,4 @@
+<vector android:height="50dp" android:viewportHeight="70"
+ android:viewportWidth="70" android:width="50dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#006FCF" android:pathData="M48.9,24.3l1.4,-3.7h6.2v-6.7H14.1v42.4h42.4v-6.7h-5.9L48.4,47l-2.2,2.5H29.6V36.2h-5.5L31,20.7h6.7l1.6,3.5v-3.5h8.3L48.9,24.3L48.9,24.3zM44.3,26.8v-1.5l0.6,1.5l2.8,7.4h2.6l2.8,-7.4l0.5,-1.5v8.8h2.9V22.8h-4.8l-2.2,5.7l-0.6,1.6l-0.6,-1.6l-2.2,-5.7h-4.8V34h3V26.8L44.3,26.8zM38.1,34h3.3l-5,-11.3h-3.9l-5,11.3h3.3l0.9,-2.2h5.5L38.1,34L38.1,34zM33.9,26.6l0.6,-1.4l0.6,1.4l1.2,2.9h-3.5L33.9,26.6L33.9,26.6zM31.9,36.2v11.3h9.4V45h-6.6v-2h6.4v-2.4h-6.4v-2h6.6v-2.4L31.9,36.2L31.9,36.2zM51.9,47.3h3.7l-5.3,-5.6l5.3,-5.6h-3.7l-3.4,3.7l-3.4,-3.7h-3.8l5.3,5.7l-5.3,5.6h3.6l3.4,-3.7L51.9,47.3L51.9,47.3zM53.3,41.8l3.2,3.3v-6.5L53.3,41.8L53.3,41.8z"/>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_diners.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_diners.xml
new file mode 100644
index 0000000000..9f8390e9f7
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_diners.xml
@@ -0,0 +1,80 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="36dp"
+ android:height="30dp"
+ android:viewportWidth="36"
+ android:viewportHeight="30">
+ <path
+ android:pathData="M19.8626,20.068C24.5613,20.0905 28.85,16.2292 28.85,11.5317C28.85,6.3948 24.5613,2.8441 19.8626,2.8458L15.8188,2.8458C11.0638,2.8441 7.15,6.3959 7.15,11.5317C7.15,16.2302 11.0638,20.0905 15.8188,20.068L19.8626,20.068Z"
+ android:strokeWidth="1"
+ android:fillColor="#4186CD"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M15.7604,3.5347C11.3853,3.5361 7.8399,7.0816 7.8389,11.4575C7.8399,15.8326 11.3852,19.3778 15.7604,19.3792C20.1367,19.3778 23.6827,15.8326 23.6833,11.4575C23.6827,7.0816 20.1367,3.5361 15.7604,3.5347ZM10.9389,11.2853C10.9429,9.1633 12.2274,7.3538 14.0389,6.6347L14.0389,15.9347C12.2274,15.2161 10.9428,13.4076 10.9389,11.2853ZM17.4833,15.9347L17.4833,6.6347C19.2943,7.3519 20.58,9.1619 20.5833,11.2844C20.58,13.4075 19.2943,15.2162 17.4833,15.9347Z"
+ android:strokeWidth="1"
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M0.6496,22.9249C0.6496,22.2148 0.2754,22.2616 -0.0833,22.2538L-0.0833,22.0486C0.2276,22.0638 0.5463,22.0638 0.8578,22.0638C1.1928,22.0638 1.6475,22.0486 2.238,22.0486C4.3031,22.0486 5.4278,23.414 5.4278,24.8122C5.4278,25.5941 4.9655,27.5597 2.142,27.5597C1.7354,27.5597 1.3601,27.5442 0.9854,27.5442C0.6266,27.5442 0.2754,27.552 -0.0833,27.5597L-0.0833,27.3548C0.3949,27.3071 0.6266,27.2913 0.6495,26.7543L0.6495,22.9249L0.6496,22.9249ZM1.2944,26.5613C1.2944,27.147 1.7309,27.2153 2.1192,27.2153C3.8321,27.2153 4.3944,25.9753 4.3944,24.8418C4.3944,23.4198 3.4429,22.3931 1.9131,22.3931C1.5876,22.3931 1.4381,22.4154 1.2945,22.4235L1.2944,26.5613Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M5.4278,27.3638L5.5803,27.3638C5.8053,27.3638 5.9666,27.3638 5.9666,27.1145L5.9666,25.0726C5.9666,24.7414 5.846,24.6953 5.5481,24.5454L5.5481,24.4246C5.9262,24.3183 6.3776,24.1761 6.4092,24.1534C6.466,24.1232 6.514,24.1153 6.5544,24.1153C6.5942,24.1153 6.6109,24.1608 6.6109,24.2213L6.6109,27.1145C6.6109,27.3638 6.7879,27.3638 7.0133,27.3638L7.15,27.3638L7.15,27.5597C6.876,27.5597 6.5942,27.5447 6.305,27.5447C6.0152,27.5447 5.7252,27.5521 5.4278,27.5597L5.4278,27.3638L5.4278,27.3638ZM6.1165,22.7375C5.9335,22.7375 5.7722,22.569 5.7722,22.3864C5.7722,22.2105 5.9409,22.0486 6.1165,22.0486C6.2988,22.0486 6.4611,22.1967 6.4611,22.3864C6.4611,22.5761 6.306,22.7375 6.1165,22.7375Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M7.9926,25.1175C7.9926,24.8389 7.9091,24.7638 7.5552,24.6205L7.5552,24.4775C7.8795,24.372 8.1885,24.274 8.551,24.1153C8.5735,24.1153 8.5957,24.1307 8.5957,24.1906L8.5957,24.6808C9.0266,24.372 9.3965,24.1153 9.9028,24.1153C10.5435,24.1153 10.7699,24.5827 10.7699,25.1703L10.7699,27.1145C10.7699,27.3638 10.9359,27.3638 11.1475,27.3638L11.2833,27.3638L11.2833,27.5597C11.0185,27.5597 10.7545,27.5447 10.4834,27.5447C10.2114,27.5447 9.9394,27.5521 9.6681,27.5597L9.6681,27.3638L9.8036,27.3638C10.0155,27.3638 10.1656,27.3638 10.1656,27.1145L10.1656,25.1632C10.1656,24.7331 9.9028,24.5224 9.4719,24.5224C9.2307,24.5224 8.8457,24.7183 8.5956,24.8843L8.5956,27.1144C8.5956,27.3638 8.7625,27.3638 8.9741,27.3638L9.1097,27.3638L9.1097,27.5597C8.8457,27.5597 8.5811,27.5446 8.3095,27.5446C8.0381,27.5446 7.7659,27.552 7.4944,27.5597L7.4944,27.3638L7.6306,27.3638C7.8419,27.3638 7.9925,27.3638 7.9925,27.1144L7.9925,25.1175L7.9926,25.1175Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"
+ tools:ignore="VectorPath"/>
+ <path
+ android:pathData="M11.9432,25.5685C11.9261,25.6409 11.9261,25.7611 11.9432,26.0343C11.9918,26.7964 12.496,27.4224 13.1545,27.4224C13.6084,27.4224 13.9637,27.1817 14.2683,26.8849L14.3833,26.9976C14.0038,27.4869 13.534,27.9042 12.8584,27.9042C11.5472,27.9042 11.2833,26.6678 11.2833,26.1545C11.2833,24.581 12.3715,24.1153 12.9483,24.1153C13.6165,24.1153 14.3342,24.5244 14.342,25.3753C14.342,25.4241 14.342,25.4716 14.3342,25.5204L14.2595,25.5685L11.9432,25.5685ZM13.4566,25.1486C13.6693,25.1486 13.6944,25.072 13.6944,25.0016C13.6944,24.7012 13.4302,24.4597 12.9524,24.4597C12.4327,24.4597 12.0745,24.7243 11.9722,25.1486L13.4566,25.1486Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M14.3833,27.3638L14.5744,27.3638C14.7717,27.3638 14.9132,27.3638 14.9132,27.1145L14.9132,24.9972C14.9132,24.7638 14.652,24.7183 14.5456,24.658L14.5456,24.5454C15.0618,24.3115 15.3449,24.1153 15.4092,24.1153C15.4511,24.1153 15.4719,24.138 15.4719,24.2136L15.4719,24.8917L15.4868,24.8917C15.6632,24.598 15.9607,24.1153 16.3916,24.1153C16.5684,24.1153 16.7944,24.2432 16.7944,24.5147C16.7944,24.7183 16.6605,24.8997 16.4626,24.8997C16.2425,24.8997 16.2425,24.7183 15.9954,24.7183C15.8751,24.7183 15.4794,24.8917 15.4794,25.3441L15.4794,27.1145C15.4794,27.3638 15.6205,27.3638 15.8187,27.3638L16.2144,27.3638L16.2144,27.5597C15.8254,27.552 15.5295,27.5447 15.2248,27.5447C14.9351,27.5447 14.6379,27.552 14.3834,27.5597L14.3834,27.3638L14.3833,27.3638Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M17.2815,26.6677C17.3844,27.1976 17.7003,27.6473 18.2778,27.6473C18.7431,27.6473 18.9173,27.3585 18.9173,27.0774C18.9173,26.1302 17.1944,26.4352 17.1944,25.143C17.1944,24.6934 17.5505,24.1153 18.4198,24.1153C18.6724,24.1153 19.0121,24.188 19.3203,24.3488L19.3764,25.1667L19.1942,25.1667C19.1154,24.6616 18.8389,24.3724 18.3324,24.3724C18.0162,24.3724 17.7161,24.5571 17.7161,24.902C17.7161,25.842 19.55,25.5522 19.55,26.8122C19.55,27.3418 19.1309,27.9042 18.1901,27.9042C17.8739,27.9042 17.5025,27.7918 17.226,27.6316L17.1389,26.7084L17.2815,26.6677Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M26.4311,23.6254L26.2389,23.6254C26.0924,22.6849 25.4529,22.3065 24.5903,22.3065C23.7037,22.3065 22.417,22.9254 22.417,24.8555C22.417,26.4812 23.5272,27.6468 24.7133,27.6468C25.4758,27.6468 26.1081,27.1001 26.2621,26.2554L26.4389,26.3034L26.2621,27.4775C25.9386,27.6872 25.0679,27.9042 24.5589,27.9042C22.7565,27.9042 21.6167,26.6896 21.6167,24.8801C21.6167,23.2313 23.0263,22.0486 24.5362,22.0486C25.1601,22.0486 25.761,22.2586 26.3543,22.4758L26.4311,23.6254Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M26.7834,27.3605L26.9359,27.3605C27.1616,27.3605 27.3232,27.3605 27.3232,27.1069L27.3232,22.839C27.3232,22.3405 27.2023,22.3252 26.8961,22.2411L26.8961,22.1185C27.2181,22.019 27.5562,21.8808 27.7254,21.7886C27.8132,21.7429 27.8782,21.7042 27.9019,21.7042C27.9511,21.7042 27.9672,21.7505 27.9672,21.812L27.9672,27.1069C27.9672,27.3605 28.144,27.3605 28.3697,27.3605L28.5056,27.3605L28.5056,27.5597C28.233,27.5597 27.9511,27.5444 27.661,27.5444C27.3715,27.5444 27.0817,27.5519 26.7833,27.5597L26.7833,27.3605L26.7834,27.3605Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M31.7754,27.0317C31.7754,27.1679 31.8589,27.1753 31.9887,27.1753C32.0806,27.1753 32.1947,27.1679 32.2944,27.1679L32.2944,27.3266C31.9656,27.3564 31.3392,27.5148 31.1939,27.5597L31.1556,27.5369L31.1556,26.927C30.6979,27.2961 30.3459,27.5597 29.8032,27.5597C29.3907,27.5597 28.963,27.2961 28.963,26.6634L28.963,24.7332C28.963,24.5373 28.9327,24.3488 28.5056,24.3113L28.5056,24.1679C28.7809,24.1605 29.3908,24.1153 29.4905,24.1153C29.5752,24.1153 29.5752,24.1679 29.5752,24.3337L29.5752,26.2778C29.5752,26.5045 29.5752,27.1525 30.2393,27.1525C30.499,27.1525 30.8426,26.9566 31.1632,26.6935L31.1632,24.665C31.1632,24.5148 30.7968,24.4321 30.5224,24.3564L30.5224,24.2208C31.209,24.1753 31.6376,24.1153 31.7134,24.1153C31.7753,24.1153 31.7753,24.1679 31.7753,24.2512L31.7753,27.0317L31.7754,27.0317Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M33.3716,24.7207C33.6951,24.4502 34.1322,24.1476 34.5779,24.1476C35.5173,24.1476 36.0833,24.9522 36.0833,25.819C36.0833,26.8615 35.3065,27.9042 34.1481,27.9042C33.5493,27.9042 33.2338,27.7129 33.023,27.6258L32.7805,27.8084L32.611,27.7213C32.6828,27.2517 32.724,26.79 32.724,26.3048L32.724,22.8826C32.724,22.365 32.6018,22.3491 32.2944,22.2617L32.2944,22.1344C32.6192,22.0312 32.9582,21.8876 33.1284,21.7918C33.2176,21.7444 33.2821,21.7042 33.3071,21.7042C33.3554,21.7042 33.3716,21.7522 33.3716,21.8162L33.3716,24.7207L33.3716,24.7207L33.3716,24.7207ZM33.3278,26.7525C33.3278,27.0532 33.6193,27.5597 34.1625,27.5597C35.0299,27.5597 35.3944,26.7289 35.3944,26.025C35.3944,25.1712 34.7305,24.4597 34.0979,24.4597C33.7973,24.4597 33.5464,24.6497 33.3278,24.832L33.3278,26.7525L33.3278,26.7525Z"
+ android:strokeWidth="1"
+ android:fillColor="#211E1F"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_discover.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_discover.xml
new file mode 100644
index 0000000000..d58bd7c7af
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_discover.xml
@@ -0,0 +1,41 @@
+<vector android:height="30dp" android:viewportHeight="500"
+ android:viewportWidth="500" android:width="30dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FFFFFF" android:pathData="M61.3,129.9l377.3,0l0,240.2l-377.2,0z"/>
+ <path android:fillColor="#F48120" android:pathData="M437.3,263.5c0,0 -103.1,72.7 -291.9,105.3h291.9V263.5z"/>
+ <path android:fillColor="#231F20" android:pathData="M438.6,128.5H60v243h380V128.4h-1.4V128.5zM437.3,131.2c0,2.7 0,234.8 0,237.5c-2.7,0 -371.8,0 -374.5,0c0,-2.7 0,-234.8 0,-237.5C65.5,131.2 434.5,131.2 437.3,131.2z"/>
+ <path android:fillColor="#231F20" android:pathData="M93.2,202.8H77.5v55h15.7c8.3,0 14.3,-2 19.6,-6.3c6.3,-5.2 10,-13 10,-21.1C122.7,214.1 110.6,202.8 93.2,202.8zM105.8,244.2c-3.4,3 -7.7,4.4 -14.7,4.4h-2.9v-36.4h2.9c6.9,0 11.1,1.2 14.7,4.4c3.7,3.3 5.9,8.4 5.9,13.7C111.7,235.6 109.5,240.9 105.8,244.2z"/>
+ <path android:fillColor="#231F20" android:pathData="M127.7,202.8h10.7v55h-10.7z"/>
+ <path android:fillColor="#231F20" android:pathData="M164.6,223.9c-6.4,-2.4 -8.3,-4 -8.3,-6.9c0,-3.5 3.4,-6.1 8,-6.1c3.2,0 5.9,1.3 8.6,4.5l5.6,-7.3c-4.6,-4 -10.1,-6.1 -16.2,-6.1c-9.7,0 -17.2,6.8 -17.2,15.8c0,7.6 3.5,11.5 13.5,15.1c4.2,1.5 6.3,2.5 7.4,3.1c2.1,1.4 3.2,3.4 3.2,5.7c0,4.5 -3.5,7.8 -8.3,7.8c-5.1,0 -9.2,-2.6 -11.7,-7.3l-6.9,6.7c4.9,7.3 10.9,10.5 19,10.5c11.1,0 19,-7.4 19,-18.1C180.5,232.4 176.8,228.4 164.6,223.9z"/>
+ <path android:fillColor="#231F20" android:pathData="M183.8,230.4c0,16.2 12.7,28.7 29,28.7c4.6,0 8.6,-0.9 13.4,-3.2v-12.6c-4.3,4.3 -8.1,6 -12.9,6c-10.8,0 -18.5,-7.8 -18.5,-19c0,-10.6 7.9,-18.9 18,-18.9c5.1,0 9,1.8 13.4,6.2V205c-4.7,-2.4 -8.6,-3.4 -13.2,-3.4C196.9,201.6 183.8,214.4 183.8,230.4z"/>
+ <path android:fillColor="#231F20" android:pathData="M311.4,239.8l-14.7,-37l-11.7,0l23.3,56.4l5.8,0l23.7,-56.4l-11.6,0z"/>
+ <path android:fillColor="#231F20" android:pathData="M342.7,257.8l30.4,0l0,-9.3l-19.7,0l0,-14.8l19,0l0,-9.3l-19,0l0,-12.2l19.7,0l0,-9.4l-30.4,0z"/>
+ <path android:fillColor="#231F20" android:pathData="M415.6,219.1c0,-10.3 -7.1,-16.2 -19.5,-16.2h-15.9v55h10.7v-22.1h1.4l14.8,22.1h13.2L403,234.7C411.1,233 415.6,227.5 415.6,219.1zM394,228.2h-3.1v-16.7h3.3c6.7,0 10.3,2.8 10.3,8.2C404.5,225.2 400.9,228.2 394,228.2z"/>
+ <path android:pathData="M259.1,230.5m-29.3,0a29.3,29.3 0,1 1,58.6 0a29.3,29.3 0,1 1,-58.6 0">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="253.8214" android:endY="222.2353"
+ android:startX="274.0753" android:startY="253.947" android:type="linear">
+ <item android:color="#FFF89F20" android:offset="0"/>
+ <item android:color="#FFF79A20" android:offset="0.2502"/>
+ <item android:color="#FFF68D20" android:offset="0.5331"/>
+ <item android:color="#FFF58720" android:offset="0.6196"/>
+ <item android:color="#FFF48120" android:offset="0.7232"/>
+ <item android:color="#FFF37521" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.65"
+ android:pathData="M259.1,230.5m-29.3,0a29.3,29.3 0,1 1,58.6 0a29.3,29.3 0,1 1,-58.6 0" android:strokeAlpha="0.65">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="241.276" android:endY="195.662"
+ android:startX="270.8777" android:startY="253.5202" android:type="linear">
+ <item android:color="#FFF58720" android:offset="0"/>
+ <item android:color="#FFE16F27" android:offset="0.3587"/>
+ <item android:color="#FFD4602C" android:offset="0.703"/>
+ <item android:color="#FFD05B2E" android:offset="0.9816"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillColor="#231F20" android:pathData="M422.4,204.9c0,-1 -0.7,-1.5 -1.8,-1.5H419v4.8h1.2v-1.9l1.4,1.9h1.4l-1.6,-2C422,206.1 422.4,205.6 422.4,204.9zM420.4,205.6h-0.2v-1.3h0.2c0.6,0 0.9,0.2 0.9,0.6C421.3,205.4 421,205.6 420.4,205.6z"/>
+ <path android:fillColor="#231F20" android:pathData="M420.8,201.6c-2.3,0 -4.2,1.9 -4.2,4.2s1.9,4.2 4.2,4.2c2.3,0 4.2,-1.9 4.2,-4.2S423.1,201.6 420.8,201.6zM420.8,209.3c-1.8,0 -3.4,-1.5 -3.4,-3.5c0,-1.9 1.5,-3.5 3.4,-3.5c1.8,0 3.3,1.6 3.3,3.5C424.1,207.7 422.6,209.3 420.8,209.3z"/>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_jcb.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_jcb.xml
new file mode 100644
index 0000000000..be738bb5b5
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_jcb.xml
@@ -0,0 +1,121 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="30dp"
+ android:height="30dp"
+ android:viewportWidth="30"
+ android:viewportHeight="30">
+ <group>
+ <path
+ android:pathData="M3.6218,3.5886C1.7335,3.5886 0.0088,5.2909 0.0088,7.2009C0.0088,8.2527 0.0082,12.4133 0.0071,16.5498C0.3529,16.7662 2.0176,17.3009 2.6871,17.348C4.1529,17.4515 5.0618,16.9668 5.2024,15.7451L5.1953,11.2074L8.4376,11.2074L8.4376,15.6415C8.2676,18.1809 6.0394,18.6986 2.6482,18.528C1.7706,18.4827 0.5771,18.2574 0.0071,18.108C0.0059,22.248 0.0053,25.9545 0.0053,25.9545L5.6488,25.9545C7.1882,25.9545 9.1929,24.5156 9.1929,22.3268L9.1929,3.5886L3.6218,3.5886Z"
+ android:strokeWidth="1"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="14.771376"
+ android:startX="0.005064707"
+ android:endY="14.771376"
+ android:endX="9.193076"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF313477"/>
+ <item android:offset="1" android:color="#FF0077BC"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+ <group>
+ <path
+ android:pathData="M0,18.1062C0.0024,18.1068 0.0047,18.1074 0.0071,18.108C0.0071,17.5927 0.0071,17.0715 0.0071,16.5498C0.0047,16.5474 0.0024,16.5462 0,16.5445L0,18.1062Z"
+ android:strokeWidth="1"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="17.326544"
+ android:startX="0"
+ android:endY="17.326544"
+ android:endX="8.5910004E-5"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF313477"/>
+ <item android:offset="1" android:color="#FF0077BC"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+ <group>
+ <path
+ android:pathData="M13.9759,3.5886C12.0882,3.5886 10.3629,5.2909 10.3629,7.2009C10.3629,7.8668 10.3629,9.7815 10.3624,12.1462C11.4953,11.2615 13.3206,10.8274 15.6441,11.0068C16.9665,11.1086 17.9441,11.2927 18.4776,11.4509L18.4776,13.0209C17.89,12.7274 16.73,12.2904 15.7629,12.2209C13.5718,12.0633 12.4212,13.0898 12.4212,14.7498C12.4212,16.2433 13.3094,17.5227 15.7524,17.3515C16.5582,17.2956 17.9,16.8262 18.4706,16.5545L18.4776,18.0774C17.9865,18.2327 16.4582,18.5651 15.02,18.5768C12.8547,18.5939 11.3265,18.1345 10.3612,17.3886C10.36,21.8074 10.3588,25.9545 10.3588,25.9545L16.0024,25.9545C17.5424,25.9545 19.5476,24.5156 19.5476,22.3268L19.5476,3.5886L13.9759,3.5886Z"
+ android:strokeWidth="1"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="14.771376"
+ android:startX="10.359212"
+ android:endY="14.771376"
+ android:endX="19.547247"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF753136"/>
+ <item android:offset="1" android:color="#FFED1746"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+ <group>
+ <path
+ android:pathData="M22.4759,15.2256L22.4718,17.2015L24.6818,17.2086C25.1112,17.2086 25.6518,16.8409 25.6518,16.1998C25.6518,15.6333 25.1853,15.2209 24.6853,15.2262C24.3765,15.2292 23.8847,15.2274 23.44,15.2262C23.1676,15.2251 22.9129,15.2239 22.7282,15.2239C22.6118,15.2239 22.5229,15.2245 22.4759,15.2256"
+ android:strokeWidth="1"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="16.2149"
+ android:startX="22.4718"
+ android:endY="16.2149"
+ android:endX="25.651682"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF008049"/>
+ <item android:offset="1" android:color="#FF62BA44"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+ <group>
+ <path
+ android:pathData="M22.4682,12.3098L22.46,14.1092L24.5488,14.1239C24.8953,14.1168 25.3829,13.7986 25.3829,13.2421C25.3829,12.6751 24.9571,12.2927 24.5035,12.3033C24.2071,12.3104 23.8006,12.3074 23.4247,12.3045C23.2247,12.3033 23.0335,12.3015 22.8729,12.3015C22.6859,12.3015 22.54,12.3039 22.4682,12.3098"
+ android:strokeWidth="1"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="13.208191"
+ android:startX="22.460648"
+ android:endY="13.208191"
+ android:endX="25.383255"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF008049"/>
+ <item android:offset="1" android:color="#FF62BA44"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+ <group>
+ <path
+ android:pathData="M24.2818,3.5886C22.3941,3.5886 20.6688,5.2909 20.6688,7.2009C20.6688,7.7733 20.6688,9.268 20.6682,11.1768L26.0335,11.1768C27.1006,11.1768 28.3576,11.6339 28.3576,12.9315C28.3576,13.6274 27.9871,14.4162 26.6512,14.6715L26.6512,14.7015C27.4312,14.7015 28.7835,15.158 28.7835,16.5345C28.7835,17.9568 27.3224,18.3509 26.5406,18.3509L20.6671,18.3568C20.6659,22.3945 20.6659,25.9545 20.6659,25.9545L26.3088,25.9545C27.8488,25.9545 29.8529,24.5156 29.8529,22.3268L29.8529,3.5886L24.2818,3.5886Z"
+ android:strokeWidth="1"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="14.771376"
+ android:startX="20.665253"
+ android:endY="14.771376"
+ android:endX="29.853254"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF008049"/>
+ <item android:offset="1" android:color="#FF62BA44"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_mastercard.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_mastercard.xml
new file mode 100644
index 0000000000..0f83295c44
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_mastercard.xml
@@ -0,0 +1,38 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="38dp"
+ android:height="30dp"
+ android:viewportWidth="38"
+ android:viewportHeight="30">
+ <path
+ android:pathData="M7.4852,29.2581L7.4852,27.3623C7.5075,27.0398 7.39,26.7232 7.1626,26.4934C6.9352,26.2636 6.6199,26.1427 6.2972,26.1616C5.8718,26.1338 5.465,26.3396 5.2355,26.6988C5.0263,26.3503 4.6432,26.1442 4.2371,26.1616C3.8835,26.1438 3.5469,26.3145 3.3524,26.6103L3.3524,26.2375L2.6952,26.2375L2.6952,29.2581L3.3587,29.2581L3.3587,27.5961C3.3302,27.387 3.3964,27.176 3.5394,27.0208C3.6825,26.8655 3.8873,26.7822 4.0981,26.7936C4.5341,26.7936 4.7553,27.0779 4.7553,27.5898L4.7553,29.2707L5.4188,29.2707L5.4188,27.5961C5.3914,27.3872 5.4581,27.1769 5.6008,27.022C5.7436,26.867 5.9477,26.7834 6.1581,26.7936C6.6068,26.7936 6.8217,27.0779 6.8217,27.5898L6.8217,29.2707L7.4852,29.2581ZM17.3021,26.2375L16.2215,26.2375L16.2215,25.3212L15.558,25.3212L15.558,26.2375L14.9577,26.2375L14.9577,26.8378L15.5706,26.8378L15.5706,28.228C15.5706,28.9295 15.8424,29.3465 16.6196,29.3465C16.91,29.3475 17.1948,29.2665 17.4411,29.1127L17.2515,28.5503C17.0753,28.6554 16.8753,28.7141 16.6702,28.7209C16.3542,28.7209 16.2341,28.5187 16.2341,28.2154L16.2341,26.8378L17.3084,26.8378L17.3021,26.2375ZM22.9072,26.1616C22.5808,26.152 22.2753,26.3218 22.111,26.604L22.111,26.2375L21.4601,26.2375L21.4601,29.2581L22.1173,29.2581L22.1173,27.5645C22.1173,27.0653 22.3322,26.7872 22.7493,26.7872C22.8898,26.7853 23.0294,26.8111 23.16,26.8631L23.3622,26.2312C23.2121,26.1775 23.054,26.1497 22.8946,26.149L22.9072,26.1616ZM14.4332,26.4776C14.0612,26.2564 13.6335,26.1468 13.2009,26.1616C12.4363,26.1616 11.9371,26.5282 11.9371,27.1285C11.9371,27.6214 12.3036,27.9247 12.9797,28.0195L13.2957,28.0637C13.6559,28.1143 13.8265,28.2091 13.8265,28.3797C13.8265,28.6135 13.5864,28.7462 13.1377,28.7462C12.7768,28.7567 12.4229,28.6454 12.133,28.4302L11.817,28.9421C12.1998,29.2094 12.6584,29.3467 13.1251,29.3339C13.9971,29.3339 14.5027,28.9231 14.5027,28.3481C14.5027,27.773 14.1046,27.5392 13.4474,27.4444L13.1314,27.4002C12.847,27.3623 12.6196,27.3054 12.6196,27.1032C12.6196,26.901 12.8344,26.7493 13.1946,26.7493C13.5275,26.7532 13.8539,26.8424 14.1425,27.0084L14.4332,26.4776ZM32.0354,26.1616C31.7089,26.152 31.4034,26.3218 31.2391,26.604L31.2391,26.2375L30.5883,26.2375L30.5883,29.2581L31.2455,29.2581L31.2455,27.5645C31.2455,27.0653 31.4603,26.7872 31.8774,26.7872C32.0179,26.7853 32.1575,26.8111 32.2881,26.8631L32.4903,26.2312C32.3403,26.1775 32.1821,26.1497 32.0227,26.149L32.0354,26.1616ZM23.5676,27.7415C23.5518,28.1707 23.7176,28.5867 24.0243,28.8874C24.331,29.188 24.7502,29.3456 25.179,29.3213C25.5736,29.3413 25.9614,29.2128 26.2659,28.9611L25.95,28.4302C25.7228,28.6035 25.4457,28.6988 25.16,28.702C24.6563,28.6612 24.2683,28.2405 24.2683,27.7351C24.2683,27.2297 24.6563,26.809 25.16,26.7683C25.4457,26.7715 25.7228,26.8668 25.95,27.04L26.2659,26.5092C25.9614,26.2574 25.5736,26.1289 25.179,26.149C24.7502,26.1247 24.331,26.2822 24.0243,26.5829C23.7176,26.8836 23.5518,27.2996 23.5676,27.7288L23.5676,27.7415ZM29.7225,27.7415L29.7225,26.2375L29.0653,26.2375L29.0653,26.604C28.8389,26.3127 28.4862,26.148 28.1174,26.1616C27.2449,26.1616 26.5376,26.8689 26.5376,27.7415C26.5376,28.614 27.2449,29.3213 28.1174,29.3213C28.4862,29.3349 28.8389,29.1702 29.0653,28.8789L29.0653,29.2454L29.7225,29.2454L29.7225,27.7415ZM27.277,27.7415C27.3062,27.2456 27.7264,26.8641 28.2228,26.8827C28.7191,26.9013 29.1096,27.3133 29.1015,27.8099C29.0935,28.3066 28.69,28.7057 28.1933,28.7083C27.9404,28.7115 27.6976,28.6089 27.5236,28.4253C27.3496,28.2417 27.2602,27.9938 27.277,27.7415ZM19.3464,26.1616C18.4739,26.1739 17.7765,26.8911 17.7887,27.7636C17.8009,28.6361 18.5181,29.3335 19.3906,29.3213C19.8452,29.3446 20.2922,29.1982 20.645,28.9105L20.329,28.4239C20.0795,28.624 19.7704,28.7352 19.4506,28.7399C18.9989,28.7787 18.5984,28.451 18.547,28.0005L20.7903,28.0005C20.7903,27.9184 20.7903,27.8362 20.7903,27.7478C20.7903,26.7999 20.2026,26.168 19.3558,26.168L19.3464,26.1616ZM19.3464,26.7493C19.545,26.7451 19.7371,26.82 19.8805,26.9574C20.0239,27.0949 20.1068,27.2837 20.111,27.4824L18.5312,27.4824C18.555,27.0605 18.9114,26.7349 19.3337,26.7493L19.3464,26.7493ZM35.8111,27.7478L35.8111,25.0242L35.1792,25.0242L35.1792,26.604C34.9528,26.3127 34.6,26.148 34.2313,26.1616C33.3588,26.1616 32.6515,26.8689 32.6515,27.7415C32.6515,28.614 33.3588,29.3213 34.2313,29.3213C34.6,29.3349 34.9528,29.1702 35.1792,28.8789L35.1792,29.2454L35.8111,29.2454L35.8111,27.7478ZM36.9075,28.8189C36.9486,28.8184 36.9893,28.8259 37.0275,28.841C37.0638,28.8558 37.097,28.8772 37.1255,28.9042C37.1533,28.9314 37.1758,28.9636 37.1918,28.999C37.2242,29.0736 37.2242,29.1582 37.1918,29.2328C37.1758,29.2682 37.1533,29.3003 37.1255,29.3276C37.097,29.3545 37.0638,29.376 37.0275,29.3908C36.9897,29.4075 36.9488,29.4161 36.9075,29.416C36.7852,29.4151 36.6745,29.3437 36.6231,29.2328C36.5912,29.1581 36.5912,29.0736 36.6231,28.999C36.6392,28.9636 36.6617,28.9314 36.6895,28.9042C36.7179,28.8772 36.7511,28.8558 36.7874,28.841C36.8286,28.8246 36.8727,28.8171 36.917,28.8189L36.9075,28.8189ZM36.9075,29.3529C36.939,29.353 36.9702,29.3466 36.9991,29.3339C37.0263,29.3218 37.0509,29.3046 37.0718,29.2833C37.1537,29.194 37.1537,29.0568 37.0718,28.9674C37.051,28.9461 37.0263,28.9289 36.9991,28.9168C36.9702,28.9042 36.939,28.8978 36.9075,28.8979C36.876,28.898 36.8448,28.9045 36.8159,28.9168C36.7875,28.9285 36.7617,28.9457 36.74,28.9674C36.6581,29.0568 36.6581,29.194 36.74,29.2833C36.7617,29.305 36.7875,29.3222 36.8159,29.3339C36.8477,29.3476 36.8823,29.3541 36.917,29.3529L36.9075,29.3529ZM36.9264,28.9769C36.9559,28.9754 36.985,28.9844 37.0086,29.0021C37.0282,29.0184 37.0388,29.0431 37.037,29.0685C37.0379,29.0897 37.0299,29.1103 37.0149,29.1254C36.9966,29.142 36.9732,29.152 36.9486,29.1538L37.0402,29.2581L36.9675,29.2581L36.8822,29.1538L36.8538,29.1538L36.8538,29.2581L36.7937,29.2581L36.7937,28.98L36.9264,28.9769ZM36.8569,29.0306L36.8569,29.1064L36.9264,29.1064C36.9388,29.1101 36.952,29.1101 36.9644,29.1064C36.9689,29.0975 36.9689,29.0869 36.9644,29.078C36.9689,29.069 36.9689,29.0585 36.9644,29.0495C36.952,29.0458 36.9388,29.0458 36.9264,29.0495L36.8569,29.0306ZM33.3814,27.7478C33.4106,27.2519 33.8308,26.8704 34.3272,26.889C34.8235,26.9076 35.2139,27.3196 35.2059,27.8163C35.1979,28.3129 34.7943,28.712 34.2976,28.7146C34.0447,28.7178 33.802,28.6152 33.628,28.4316C33.454,28.2481 33.3646,28.0001 33.3814,27.7478ZM11.1914,27.7478L11.1914,26.2375L10.5342,26.2375L10.5342,26.604C10.3078,26.3127 9.955,26.148 9.5863,26.1616C8.7138,26.1616 8.0065,26.8689 8.0065,27.7415C8.0065,28.614 8.7138,29.3213 9.5863,29.3213C9.955,29.3349 10.3078,29.1702 10.5342,28.8789L10.5342,29.2454L11.1914,29.2454L11.1914,27.7478ZM8.7459,27.7478C8.7751,27.2519 9.1953,26.8704 9.6917,26.889C10.188,26.9076 10.5785,27.3196 10.5704,27.8163C10.5624,28.3129 10.1589,28.712 9.6622,28.7146C9.4082,28.7196 9.1637,28.6178 8.9883,28.434C8.8129,28.2502 8.7227,28.0013 8.7396,27.7478L8.7459,27.7478Z"
+ android:strokeWidth="1"
+ android:fillColor="#231F20"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"
+ tools:ignore="VectorPath"/>
+ <path
+ android:pathData="M14.2152,3.2197h9.9528v17.8866h-9.9528z"
+ android:strokeWidth="1"
+ android:fillColor="#FF5F00"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M14.8471,12.1646C14.8434,8.6737 16.4455,5.3751 19.1915,3.2197C14.5279,-0.4454 7.8307,0.0884 3.8066,4.4461C-0.2175,8.8038 -0.2175,15.5222 3.8066,19.8799C7.8307,24.2375 14.5279,24.7714 19.1915,21.1063C16.4464,18.9516 14.8443,15.6544 14.8471,12.1646Z"
+ android:strokeWidth="1"
+ android:fillColor="#EB001B"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M37.5963,12.1646C37.5962,16.5196 35.1095,20.4922 31.1923,22.3954C27.2751,24.2985 22.6152,23.7979 19.1915,21.1063C21.9355,18.9498 23.5377,15.653 23.5377,12.163C23.5377,8.673 21.9355,5.3762 19.1915,3.2197C22.6152,0.5281 27.2751,0.0275 31.1923,1.9306C35.1095,3.8337 37.5962,7.8064 37.5963,12.1614L37.5963,12.1646Z"
+ android:strokeWidth="1"
+ android:fillColor="#F79E1B"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M36.5094,19.2646L36.5094,18.8533L36.6579,18.8533L36.6579,18.7682L36.2819,18.7682L36.2819,18.8533L36.443,18.8533L36.443,19.2646L36.5094,19.2646ZM37.2392,19.2646L37.2392,18.7682L37.1255,18.7682L36.9928,19.1228L36.8601,18.7682L36.759,18.7682L36.759,19.2646L36.8411,19.2646L36.8411,18.8923L36.9644,19.215L37.0497,19.215L37.1729,18.8923L37.1729,19.2682L37.2392,19.2646Z"
+ android:strokeWidth="1"
+ android:fillColor="#F79E1B"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_mir.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_mir.xml
new file mode 100644
index 0000000000..367897fff4
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_mir.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="36dp"
+ android:height="30dp"
+ android:viewportWidth="36"
+ android:viewportHeight="30">
+ <path
+ android:pathData="M7.8118,11.3125L6.4857,15.9063L6.2588,15.9063L4.9327,11.3125C4.7058,10.5352 3.9939,10 3.1802,10L0,10L0,20L3.1842,20L3.1842,14.0898L3.4111,14.0898L5.2339,20L7.5106,20L9.3295,14.0898L9.5564,14.0898L9.5564,20L12.7406,20L12.7406,10L9.5603,10C8.7506,10 8.0387,10.5352 7.8118,11.3125ZM25.442,20L28.6457,20L28.6457,17.043L31.8691,17.043C33.555,17.043 34.9907,16.0898 35.5461,14.75L25.442,14.75L25.442,20ZM19.7661,11.0547L17.5247,15.9102L17.2978,15.9102L17.2978,10L14.1136,10L14.1136,20L16.8166,20C17.5286,20 18.1739,19.5859 18.4713,18.9453L20.7127,14.0938L20.9396,14.0938L20.9396,20L24.1238,20L24.1238,10L21.4207,10C20.7087,10 20.0634,10.4141 19.7661,11.0547Z"
+ android:strokeWidth="1"
+ android:fillColor="#006848"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M32.1859,10C33.1052,10 33.9384,10.3516 34.5682,10.9297C35.2723,11.5703 35.7143,12.4922 35.7143,13.5195C35.7143,13.7305 35.6908,13.9375 35.6556,14.1406L29.7411,14.1406C27.7382,14.1406 26.0405,12.8281 25.4694,11.0156C25.4616,10.9961 25.4577,10.9727 25.4499,10.9492C25.4342,10.8945 25.4225,10.8359 25.4068,10.7813C25.3482,10.5273 25.309,10.2695 25.2934,10L32.1859,10Z"
+ android:strokeWidth="1"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="22.94978"
+ android:startX="35.714317"
+ android:endY="22.94978"
+ android:endX="25.357185"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF1E5CD8"/>
+ <item android:offset="1" android:color="#FF02AFFF"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_unionpay.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_unionpay.xml
new file mode 100644
index 0000000000..b231f3b7a8
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_unionpay.xml
@@ -0,0 +1,143 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="36dp"
+ android:height="30dp"
+ android:viewportWidth="36"
+ android:viewportHeight="30">
+ <group>
+ <path
+ android:pathData="M7.0234,3.2181L15.9747,3.2181C17.2242,3.2181 18.0014,4.2366 17.7099,5.4903L13.5424,23.3829C13.2483,24.6322 11.9973,25.6516 10.7469,25.6516L1.7964,25.6516C0.5486,25.6516 -0.2303,24.6322 0.0612,23.3829L4.2304,5.4903C4.5219,4.2366 5.7723,3.2181 7.0234,3.2181"
+ android:strokeWidth="1"
+ android:fillColor="#E21837"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ </group>
+ <group>
+ <path
+ android:pathData="M15.2299,3.2181L25.5237,3.2181C26.773,3.2181 26.2097,4.2366 25.9158,5.4903L21.7489,23.3829C21.4566,24.6322 21.5478,25.6516 20.2958,25.6516L10.002,25.6516C8.7501,25.6516 7.9753,24.6322 8.2694,23.3829L12.4361,5.4903C12.7319,4.2366 13.9797,3.2181 15.2299,3.2181"
+ android:strokeWidth="1"
+ android:fillColor="#00457C"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ </group>
+ <group>
+ <path
+ android:pathData="M25.1154,3.2181L34.0667,3.2181C35.318,3.2181 36.0952,4.2366 35.8013,5.4903L31.6344,23.3829C31.3403,24.6322 30.0884,25.6516 28.8374,25.6516L19.8902,25.6516C18.6382,25.6516 17.8617,24.6322 18.155,23.3829L22.3224,5.4903C22.6139,4.2366 23.8635,3.2181 25.1154,3.2181"
+ android:strokeWidth="1"
+ android:fillColor="#007B84"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ </group>
+ <path
+ android:pathData="M26.5822,16.4284L25.4904,20.0395L25.7854,20.0395L25.5569,20.7852L25.2654,20.7852L25.1963,21.0152L24.1575,21.0152L24.2282,20.7852L22.1197,20.7852L22.3302,20.0949L22.5449,20.0949L23.6511,16.4284L23.8719,15.6894L24.9305,15.6894L24.8197,16.0618C24.8197,16.0618 25.1018,15.8591 25.3693,15.79C25.6361,15.7193 27.171,15.6938 27.171,15.6938L26.9443,16.4284L26.5822,16.4284ZM24.7156,16.4284L24.4353,17.3513C24.4353,17.3513 24.7505,17.209 24.9203,17.1621C25.0942,17.1161 25.354,17.1008 25.354,17.1008L25.5569,16.4284L24.7156,16.4284ZM24.2956,17.8082L24.0057,18.7688C24.0057,18.7688 24.327,18.6052 24.4983,18.553C24.6722,18.514 24.9364,18.4806 24.9364,18.4806L25.1411,17.8082L24.2956,17.8082ZM23.6214,20.048L24.4651,20.048L24.7071,19.2375L23.866,19.2375L23.6214,20.048Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M27.0492,15.6937L28.1809,15.6937L28.192,16.1148C28.1844,16.1864 28.2457,16.2195 28.3779,16.2195L28.6081,16.2195L28.3976,16.9151L27.7864,16.9151C27.258,16.9526 27.0568,16.7257 27.0706,16.4702L27.0492,15.6937Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M27.1992,18.9929L26.1212,18.9929L26.306,18.3741L27.5385,18.3741L27.713,17.8081L26.4969,17.8081L26.7039,17.1102L30.0884,17.1102L29.8778,17.8081L28.7426,17.8081L28.5655,18.3741L29.7039,18.3741L29.5148,18.9929L28.2851,18.9929L28.066,19.2536L28.5655,19.2536L28.6872,20.0336C28.7009,20.111 28.7009,20.1621 28.7265,20.1955C28.752,20.2227 28.9021,20.2371 28.9889,20.2371L29.1406,20.2371L28.9097,20.9958L28.5253,20.9958C28.4666,20.9958 28.3778,20.9914 28.2552,20.9864C28.1409,20.9761 28.0599,20.9087 27.9816,20.8697C27.9109,20.8363 27.806,20.7512 27.7804,20.6054L27.6602,19.8272L27.1004,20.5934C26.923,20.8363 26.6828,21.0213 26.2763,21.0213L25.4939,21.0213L25.6992,20.3436L25.9983,20.3436C26.0854,20.3436 26.1602,20.3096 26.2174,20.2806C26.2745,20.2568 26.3256,20.2269 26.3828,20.1427L27.1992,18.9929Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M15.3969,17.2977L18.2521,17.2977L18.0406,17.9777L16.8996,17.9777L16.7213,18.559L17.889,18.559L17.6758,19.2613L16.5092,19.2613L16.2254,20.2056C16.1913,20.3097 16.5031,20.3232 16.6157,20.3232L17.1987,20.2431L16.9644,21.0212L15.6492,21.0212C15.5427,21.0212 15.4644,21.0059 15.35,20.9804C15.2392,20.9522 15.1899,20.9027 15.1412,20.8279C15.0934,20.7511 15.0186,20.688 15.0697,20.5226L15.448,19.2733L14.7996,19.2733L15.0151,18.559L15.6653,18.559L15.8383,17.9777L15.1899,17.9777L15.3969,17.2977Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M17.3171,16.0738L18.4881,16.0738L18.276,16.7855L16.6761,16.7855L16.5033,16.9354C16.4282,17.0079 16.4027,16.9782 16.3054,17.0301C16.2151,17.0753 16.0259,17.1656 15.7796,17.1656L15.2673,17.1656L15.4737,16.482L15.6279,16.482C15.7575,16.482 15.847,16.47 15.8921,16.4419C15.9432,16.4087 16.0004,16.3363 16.0634,16.2196L16.3591,15.6852L17.5215,15.6852L17.3171,16.0738Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M18.991,15.6937L19.9881,15.6937L19.8423,16.1957C19.8423,16.1957 20.1578,15.9435 20.3784,15.853C20.5974,15.772 21.0944,15.6987 21.0944,15.6987L22.7086,15.6893L22.159,17.521C22.066,17.8339 21.9578,18.0365 21.8904,18.1295C21.8299,18.2224 21.7584,18.3034 21.6185,18.3802C21.483,18.4533 21.3587,18.4943 21.2443,18.5063C21.1379,18.5139 20.9742,18.5165 20.7475,18.5198L19.1921,18.5198L18.7549,19.967C18.7132,20.111 18.6936,20.18 18.7208,20.2192C18.7429,20.2526 18.7957,20.2917 18.869,20.2917L19.5552,20.2269L19.32,21.0213L18.5537,21.0213C18.3091,21.0213 18.1318,21.0154 18.0074,21.006C17.8889,20.9957 17.7654,21.006 17.6819,20.9429C17.6112,20.8799 17.5021,20.7963 17.5047,20.7119C17.5123,20.6336 17.5455,20.5031 17.595,20.3233L18.991,15.6937ZM21.108,17.5424L19.4742,17.5424L19.3737,17.8712L20.7883,17.8712C20.9546,17.8517 20.9895,17.8755 21.0041,17.8679L21.108,17.5424ZM19.5628,17.2448C19.5628,17.2448 19.8825,16.9526 20.4297,16.8579C20.554,16.8348 21.3305,16.8427 21.3305,16.8427L21.449,16.4506L19.8015,16.4506L19.5628,17.2448Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"
+ tools:ignore="VectorPath"/>
+ <path
+ android:pathData="M21.8989,18.6477L21.806,19.0875C21.7669,19.2254 21.7327,19.3277 21.6288,19.4163C21.5197,19.5093 21.3919,19.6066 21.0927,19.6066L20.5388,19.6295L20.5344,20.1263C20.5285,20.2661 20.566,20.2524 20.5881,20.2747C20.6136,20.3002 20.6367,20.3096 20.6605,20.3198L20.8361,20.3096L21.3646,20.2805L21.1455,21.006L20.5388,21.006C20.1142,21.006 19.7989,20.9957 19.6966,20.9148C19.5943,20.85 19.5807,20.7691 19.5825,20.6292L19.6215,18.6911L20.5898,18.6911L20.5763,19.0875L20.8089,19.0875C20.8889,19.0875 20.9426,19.0797 20.976,19.0585C21.0057,19.0363 21.0271,19.0066 21.0406,18.9579L21.1379,18.6477L21.8989,18.6477Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M8.0819,8.9318C8.0487,9.0904 7.4273,11.9556 7.4256,11.9582C7.2918,12.5377 7.1947,12.9511 6.864,13.218C6.6766,13.3729 6.4575,13.4479 6.2035,13.4479C5.7953,13.4479 5.5575,13.2452 5.5174,12.8608L5.5097,12.7286C5.5097,12.7286 5.6341,11.9523 5.6341,11.9479C5.6341,11.9479 6.2862,9.3365 6.403,8.9913C6.4089,8.9719 6.4106,8.9617 6.4123,8.9523C5.1432,8.9634 4.9182,8.9523 4.903,8.9318C4.8945,8.9599 4.8628,9.1218 4.8628,9.1218L4.1971,12.0646L4.1402,12.3145L4.0294,13.1309C4.0294,13.3729 4.0771,13.5708 4.1716,13.7377C4.4751,14.2678 5.3402,14.347 5.8294,14.347C6.4599,14.347 7.0514,14.2133 7.4511,13.9687C8.1449,13.5588 8.3265,12.918 8.4883,12.3485L8.5634,12.0561C8.5634,12.0561 9.235,9.3444 9.3493,8.9913C9.3534,8.9719 9.3552,8.9617 9.3611,8.9523C8.4408,8.9617 8.1687,8.9523 8.0819,8.9318"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M11.7978,14.319C11.3486,14.3114 11.1884,14.3114 10.6626,14.3395L10.643,14.2994C10.6881,14.0982 10.7376,13.9014 10.7828,13.6985L10.8476,13.424C10.9447,12.9988 11.0392,12.5037 11.0503,12.3519C11.0606,12.2615 11.0922,12.0348 10.8321,12.0348C10.723,12.0348 10.6089,12.0876 10.4931,12.1415C10.4298,12.3682 10.3022,13.0049 10.2406,13.2946C10.1104,13.9055 10.1025,13.9764 10.0438,14.2782L10.0063,14.319C9.5427,14.3114 9.3808,14.3114 8.8473,14.3395L8.8233,14.2935C8.9119,13.9313 9.0007,13.5647 9.0858,13.2017C9.3101,12.2164 9.3638,11.8398 9.4242,11.3386L9.4676,11.3096C9.9884,11.2363 10.1145,11.221 10.6779,11.1067L10.7256,11.1597L10.6388,11.4732C10.735,11.4161 10.8262,11.3591 10.9224,11.3096C11.1884,11.1791 11.4843,11.1401 11.6462,11.1401C11.894,11.1401 12.1643,11.209 12.2767,11.4946C12.3834,11.7495 12.3125,12.0621 12.172,12.6791L12.0995,12.9946C11.9555,13.6806 11.9315,13.8067 11.8506,14.2782L11.7978,14.319Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"
+ tools:ignore="VectorPath"/>
+ <path
+ android:pathData="M13.6268,14.319C13.3549,14.3172 13.1793,14.3114 13.0097,14.3172C12.8402,14.319 12.6748,14.3275 12.4224,14.3395L12.4089,14.3172L12.3927,14.2935C12.4617,14.0334 12.4992,13.9431 12.5332,13.8503C12.5681,13.7574 12.5998,13.6653 12.6611,13.4011C12.7405,13.0559 12.7889,12.8155 12.8206,12.6043C12.8581,12.3996 12.8775,12.2258 12.9057,12.0246L12.9253,12.0093L12.9467,11.9897C13.2168,11.9522 13.3881,11.9266 13.5637,11.8985C13.7409,11.8756 13.9191,11.8398 14.1986,11.7861L14.2089,11.8099L14.2165,11.8354C14.1655,12.0486 14.1118,12.2615 14.0605,12.478C14.0112,12.6955 13.9584,12.9076 13.9106,13.1207C13.81,13.5741 13.769,13.7436 13.7452,13.8665C13.7215,13.9806 13.7153,14.0436 13.6763,14.2782L13.6505,14.2994L13.6268,14.319Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M17.6708,12.7678C17.8285,12.0758 17.7057,11.7529 17.5514,11.5558C17.3171,11.2558 16.9028,11.1596 16.4734,11.1596C16.2151,11.1596 15.5998,11.1852 15.1191,11.6283C14.774,11.9479 14.6145,12.3817 14.5183,12.7977C14.421,13.2212 14.3095,13.984 15.0109,14.2678C15.2274,14.3608 15.5391,14.3863 15.7403,14.3863C16.2543,14.3863 16.781,14.2449 17.1773,13.8247C17.4825,13.4837 17.6221,12.9766 17.6708,12.7678M16.4913,12.7183C16.469,12.8353 16.3667,13.269 16.2288,13.454C16.1324,13.5904 16.0183,13.6729 15.8922,13.6729C15.8546,13.6729 15.6312,13.6729 15.6279,13.3414C15.6262,13.1778 15.6594,13.0107 15.7004,12.8292C15.8189,12.3051 15.9585,11.8652 16.3157,11.8652C16.5951,11.8652 16.6157,12.1925 16.4913,12.7183"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M28.677,14.365C28.1333,14.3606 27.9773,14.3606 27.4753,14.382L27.4439,14.3412C27.5794,13.8246 27.7157,13.3099 27.8375,12.7875C27.9952,12.1099 28.031,11.8219 28.0821,11.4254L28.1231,11.3923C28.6635,11.3155 28.8134,11.2934 29.375,11.1894L29.3911,11.2363C29.2882,11.6624 29.1876,12.0861 29.087,12.5139C28.8808,13.407 28.8058,13.8603 28.7266,14.3275L28.677,14.365Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M28.9353,12.8292C29.093,12.1415 28.4564,12.7679 28.3558,12.5412C28.2007,12.1867 28.2969,11.4691 27.6722,11.2286C27.4318,11.1339 26.8676,11.2559 26.3828,11.6975C26.0417,12.0128 25.8781,12.4448 25.7852,12.8591C25.6872,13.2767 25.5748,14.0395 26.2728,14.3113C26.4951,14.406 26.6945,14.4339 26.8975,14.4239C27.5989,14.3864 28.1332,13.326 28.5297,12.9075C28.8347,12.5753 28.8884,13.0321 28.9353,12.8292M27.8614,12.7782C27.8341,12.8905 27.7309,13.3278 27.593,13.511C27.5009,13.6415 27.2819,13.7225 27.1558,13.7225C27.12,13.7225 26.8992,13.7225 26.8916,13.3969C26.8898,13.2348 26.923,13.0679 26.9649,12.8846C27.0851,12.3699 27.223,11.9342 27.581,11.9342C27.8614,11.9342 27.9816,12.2515 27.8614,12.7782"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M20.7458,14.319C20.2942,14.3114 20.1399,14.3114 19.6123,14.3395L19.5927,14.2994C19.6379,14.0982 19.6889,13.9014 19.7358,13.6985L19.7971,13.424C19.896,12.9988 19.9907,12.5037 20.0001,12.3519C20.0103,12.2615 20.0417,12.0348 19.7836,12.0348C19.671,12.0348 19.5587,12.0876 19.4426,12.1415C19.3813,12.3682 19.2508,13.0049 19.1878,13.2946C19.0617,13.9055 19.0523,13.9764 18.9953,14.2782L18.9552,14.319C18.4924,14.3114 18.3329,14.3114 17.7986,14.3395L17.7748,14.2935C17.8634,13.9313 17.952,13.5647 18.0373,13.2017C18.2614,12.2164 18.3126,11.8398 18.3757,11.3386L18.4156,11.3096C18.9355,11.2363 19.0643,11.221 19.6276,11.1067L19.671,11.1597L19.5901,11.4732C19.683,11.4161 19.7777,11.3591 19.8722,11.3096C20.1364,11.1791 20.4338,11.1401 20.5959,11.1401C20.8405,11.1401 21.1123,11.209 21.2282,11.4946C21.3329,11.7495 21.2614,12.0621 21.12,12.6791L21.0493,12.9946C20.9009,13.6806 20.8795,13.8067 20.8003,14.2782L20.7458,14.319Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"
+ tools:ignore="VectorPath"/>
+ <path
+ android:pathData="M25.1333,10.6099C25.0541,10.9688 24.8213,11.2703 24.5222,11.4161C24.2758,11.5398 23.9743,11.55 23.6632,11.55L23.462,11.55L23.4773,11.4691C23.4773,11.4691 23.848,9.8565 23.8465,9.8624L23.8582,9.7797L23.8635,9.7167L24.0116,9.7322C24.0116,9.7322 24.7761,9.7976 24.794,9.7994C25.0958,9.9161 25.2201,10.217 25.1333,10.6099M24.6468,8.9284C24.6441,8.9284 24.2717,8.9319 24.2717,8.9319C23.2984,8.9436 22.9081,8.9404 22.7477,8.9207C22.7342,8.9914 22.7086,9.1175 22.7086,9.1175C22.7086,9.1175 22.36,10.7334 22.36,10.736C22.36,10.736 21.5257,14.1715 21.4864,14.3334C22.3362,14.3234 22.6846,14.3234 22.8313,14.3395C22.8646,14.1776 23.0614,13.2178 23.0632,13.2178C23.0632,13.2178 23.231,12.514 23.2412,12.4882C23.2412,12.4882 23.294,12.4149 23.3468,12.3861L23.4245,12.3861C24.1574,12.3861 24.9849,12.3861 25.6336,11.9087C26.075,11.5815 26.3767,11.0982 26.5113,10.5111C26.5465,10.3671 26.572,10.1958 26.572,10.0243C26.572,9.7994 26.5268,9.577 26.3964,9.4031C26.0656,8.9404 25.4069,8.9319 24.6468,8.9284"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"
+ tools:ignore="VectorPath"/>
+ <path
+ android:pathData="M33.1242,11.1852L33.0808,11.1357C32.5251,11.2482 32.4245,11.2661 31.914,11.3351L31.8764,11.3726C31.8747,11.3787 31.8732,11.3881 31.8705,11.3966L31.8688,11.3881C31.4887,12.265 31.4998,12.0758 31.1905,12.7661C31.1887,12.7347 31.1887,12.7151 31.187,12.6817L31.1095,11.1852L31.0609,11.1357C30.4788,11.2482 30.465,11.2661 29.9274,11.3351L29.8855,11.3726C29.8796,11.3905 29.8796,11.4101 29.8761,11.4315L29.8796,11.4391C29.9468,11.7826 29.9307,11.706 29.9981,12.2479C30.0295,12.5139 30.0714,12.7814 30.1028,13.0441C30.1558,13.4837 30.1855,13.7002 30.2503,14.3711C29.8873,14.9702 29.8013,15.1969 29.4518,15.7227L29.474,15.772C29.9981,15.7526 30.1198,15.7526 30.5086,15.7526L30.5929,15.6562C30.887,15.023 33.1242,11.1852 33.1242,11.1852"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M14.1194,11.5559C14.4176,11.3489 14.4552,11.0633 14.2047,10.9149C13.9507,10.765 13.505,10.8128 13.205,11.0198C12.9058,11.2227 12.8716,11.5107 13.1247,11.6624C13.3745,11.8081 13.822,11.7647 14.1194,11.5559"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+ <path
+ android:pathData="M30.5537,15.709L30.1165,16.4583C29.9784,16.7146 29.7218,16.9056 29.3136,16.9073L28.6183,16.8953L28.8212,16.2213L28.9575,16.2213C29.0282,16.2213 29.0793,16.2178 29.1185,16.1982C29.1543,16.1864 29.1799,16.1583 29.208,16.1173L29.4663,15.709L30.5537,15.709Z"
+ android:strokeWidth="1"
+ android:fillColor="#FEFEFE"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_visa.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_visa.xml
new file mode 100644
index 0000000000..d2e1e3a506
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_cc_logo_visa.xml
@@ -0,0 +1,28 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="44dp"
+ android:height="30dp"
+ android:viewportWidth="44"
+ android:viewportHeight="30">
+ <group>
+ <path
+ android:pathData="M22.7998,12.888C22.7747,14.8474 24.5645,15.9409 25.9129,16.591C27.2982,17.2581 27.7635,17.6858 27.7582,18.2822C27.7477,19.1952 26.6531,19.5981 25.6287,19.6138C23.8415,19.6412 22.8024,19.1363 21.9763,18.7544L21.3325,21.7353C22.1613,22.1133 23.696,22.443 25.2876,22.4573C29.0233,22.4573 31.4675,20.6327 31.4807,17.8035C31.4952,14.213 26.4614,14.0142 26.4958,12.4093C26.5077,11.9227 26.977,11.4035 28.0054,11.2714C28.5143,11.2046 29.9195,11.1536 31.5124,11.8796L32.1377,8.9954C31.2811,8.6868 30.1799,8.3911 28.8091,8.3911C25.2929,8.3911 22.8196,10.2407 22.7998,12.888M38.1457,8.6397C37.4636,8.6397 36.8886,9.0334 36.6321,9.6377L31.2956,22.2455L35.0286,22.2455L35.7716,20.2141L40.3334,20.2141L40.7643,22.2455L44.0545,22.2455L41.1834,8.6397L38.1457,8.6397M38.6678,12.3151L39.7452,17.4242L36.7947,17.4242L38.6678,12.3151M18.2736,8.6397L15.3311,22.2455L18.8883,22.2455L21.8295,8.6397L18.2736,8.6397M9.3085,17.9003L7.8108,10.0261C7.635,9.1472 6.941,8.6397 6.1704,8.6397L0.1174,8.6397L0.0328,9.0347C1.2754,9.3015 2.6872,9.7318 3.5424,10.1923C4.0659,10.4735 4.2153,10.7194 4.3871,11.3878L7.2239,22.2455L10.9834,22.2455L16.7468,8.6397L13.0112,8.6397L9.3085,17.9003Z"
+ android:strokeWidth="1"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"
+ tools:ignore="VectorPath">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="8.552165"
+ android:startX="4.099774"
+ android:endY="22.340685"
+ android:endX="41.938087"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF222357"/>
+ <item android:offset="1" android:color="#FF254AA5"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_icon_credit_card_generic.xml b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_icon_credit_card_generic.xml
new file mode 100644
index 0000000000..f9a47fcc81
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/drawable/ic_icon_credit_card_generic.xml
@@ -0,0 +1,19 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:pathData="M4.5,9.4H3.2c-0.3,0 -0.5,0.2 -0.5,0.5s0.2,0.5 0.5,0.5h1.3c0.3,0 0.5,-0.2 0.5,-0.5S4.8,9.4 4.5,9.4z"
+ android:fillColor="@android:color/white"/>
+ <path
+ android:pathData="M9.3,9.4H6.2c-0.3,0 -0.5,0.2 -0.5,0.5s0.2,0.5 0.5,0.5h3.2c0.3,0 0.5,-0.2 0.5,-0.5S9.6,9.4 9.3,9.4z"
+ android:fillColor="@android:color/white"/>
+ <path
+ android:pathData="M14,2H2C0.9,2 0,2.9 0,4v8c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4C16,2.9 15.1,2 14,2zM14,12H2V7.7h12V12zM14,6H2V4h12V6z"
+ android:fillColor="@android:color/white"/>
+</vector>
diff --git a/mobile/android/android-components/components/support/utils/src/main/res/values/arrays.xml b/mobile/android/android-components/components/support/utils/src/main/res/values/arrays.xml
new file mode 100644
index 0000000000..cbc9d5c4e2
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/main/res/values/arrays.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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>
+ <!--
+ List of url schemes used by methods of SafeUrl.
+ -->
+ <string-array name="mozac_url_schemes_blocklist" />
+</resources>
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BootUtilsTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BootUtilsTest.kt
new file mode 100644
index 0000000000..fa4cab2669
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BootUtilsTest.kt
@@ -0,0 +1,85 @@
+/* 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.support.utils
+
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.utils.BootUtils.Companion.getBootIdentifier
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.robolectric.annotation.Config
+
+private const val NO_BOOT_IDENTIFIER = "no boot identifier available"
+
+@RunWith(AndroidJUnit4::class)
+class BootUtilsTest {
+
+ @Mock private lateinit var bootUtils: BootUtils
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `WHEN no boot id file & Android version is less than N(24) THEN getBootIdentifier returns NO_BOOT_IDENTIFIER`() {
+ `when`(bootUtils.bootIdFileExists).thenReturn(false)
+
+ assertEquals(NO_BOOT_IDENTIFIER, getBootIdentifier(testContext, bootUtils))
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `WHEN boot id file returns null & Android version is less than N(24) THEN getBootIdentifier returns NO_BOOT_IDENTIFIER`() {
+ `when`(bootUtils.bootIdFileExists).thenReturn(true)
+ `when`(bootUtils.deviceBootId).thenReturn(null)
+
+ assertEquals(NO_BOOT_IDENTIFIER, getBootIdentifier(testContext, bootUtils))
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `WHEN boot id file has text & Android version is less than N(24) THEN getBootIdentifier returns the boot id`() {
+ `when`(bootUtils.bootIdFileExists).thenReturn(true)
+ val bootId = "test"
+ `when`(bootUtils.deviceBootId).thenReturn(bootId)
+
+ assertEquals(bootId, getBootIdentifier(testContext, bootUtils))
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M])
+ fun `WHEN boot id file has text with whitespace & Android version is less than N(24) THEN getBootIdentifier returns the trimmed boot id`() {
+ `when`(bootUtils.bootIdFileExists).thenReturn(true)
+ val bootId = " test "
+ `when`(bootUtils.deviceBootId).thenReturn(bootId)
+
+ assertEquals(bootId, getBootIdentifier(testContext, bootUtils))
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `WHEN Android version is N(24) THEN getBootIdentifier returns the boot count`() {
+ val bootCount = "9"
+ `when`(bootUtils.getDeviceBootCount(any())).thenReturn(bootCount)
+ assertEquals(bootCount, getBootIdentifier(testContext, bootUtils))
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O])
+ fun `WHEN Android version is more than N(24) THEN getBootIdentifier returns the boot count`() {
+ val bootCount = "9"
+ `when`(bootUtils.getDeviceBootCount(any())).thenReturn(bootCount)
+ assertEquals(bootCount, getBootIdentifier(testContext, bootUtils))
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BrowsersCacheTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BrowsersCacheTest.kt
new file mode 100644
index 0000000000..4e5a2d03f6
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BrowsersCacheTest.kt
@@ -0,0 +1,152 @@
+/* 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/. */
+@file:Suppress("DEPRECATION")
+
+package mozilla.components.support.utils
+
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageInfo
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Ignore
+import org.junit.Test
+import org.robolectric.Shadows.shadowOf
+
+@Ignore("https://bugzilla.mozilla.org/show_bug.cgi?id=1794926")
+class BrowsersCacheTest {
+
+ // NB: There is always one more browser than pretendBrowsersAreInstalled installs because
+ // the application we are testing is recognized as a browser itself!
+
+ @Test
+ fun `cached list of browsers match before-after installation when cache is not invalidated`() {
+ BrowsersCache.resetAll()
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
+ Browsers.KnownBrowser.REFERENCE_BROWSER.packageName,
+ ),
+ )
+
+ val initialBrowserList = BrowsersCache.all(testContext)
+ assertEquals(3, initialBrowserList.installedBrowsers.size)
+
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
+ Browsers.KnownBrowser.FIREFOX.packageName,
+ Browsers.KnownBrowser.CHROME.packageName,
+ Browsers.KnownBrowser.SAMSUNG_INTERNET.packageName,
+ Browsers.KnownBrowser.DUCKDUCKGO.packageName,
+ Browsers.KnownBrowser.REFERENCE_BROWSER.packageName,
+ ),
+ )
+ val updatedBrowserList = BrowsersCache.all(testContext)
+ assertEquals(3, updatedBrowserList.installedBrowsers.size)
+ }
+
+ @Test
+ fun `cached list of browsers change before-after installation when cache is invalidated`() {
+ BrowsersCache.resetAll()
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
+ Browsers.KnownBrowser.REFERENCE_BROWSER.packageName,
+ ),
+ )
+
+ val initialBrowserList = BrowsersCache.all(testContext)
+ assertEquals(3, initialBrowserList.installedBrowsers.size)
+
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
+ Browsers.KnownBrowser.FIREFOX.packageName,
+ Browsers.KnownBrowser.CHROME.packageName,
+ Browsers.KnownBrowser.SAMSUNG_INTERNET.packageName,
+ Browsers.KnownBrowser.DUCKDUCKGO.packageName,
+ Browsers.KnownBrowser.REFERENCE_BROWSER.packageName,
+ ),
+ )
+
+ BrowsersCache.resetAll()
+
+ val updatedBrowserList = BrowsersCache.all(testContext)
+ assertEquals(7, updatedBrowserList.installedBrowsers.size)
+ }
+
+ @Test
+ fun `resetting the cache should empty it`() {
+ BrowsersCache.resetAll()
+
+ BrowsersCache.all(testContext)
+
+ assertNotNull(BrowsersCache.cachedBrowsers)
+
+ BrowsersCache.resetAll()
+
+ assertNull(BrowsersCache.cachedBrowsers)
+ }
+
+ // pretendBrowsersAreInstalled was taken, verbatim, from a-c.
+ // See support/utils/src/test/java/mozilla/components/support/utils/BrowsersTest.kt
+ private fun pretendBrowsersAreInstalled(
+ browsers: List<String> = listOf(),
+ defaultBrowser: String? = null,
+ url: String = "http://www.mozilla.org/index.html",
+ browsersExported: Boolean = true,
+ defaultBrowserExported: Boolean = true,
+ ) {
+ val packageManager = testContext.packageManager
+ val shadow = shadowOf(packageManager)
+
+ browsers.forEach { packageName ->
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.`package` = packageName
+ intent.data = Uri.parse(url)
+ intent.addCategory(Intent.CATEGORY_BROWSABLE)
+
+ val packageInfo = PackageInfo().apply {
+ this.packageName = packageName
+ }
+
+ shadow.installPackage(packageInfo)
+
+ val activityInfo = ActivityInfo().apply {
+ exported = browsersExported
+ this.packageName = packageName
+ }
+
+ val resolveInfo = ResolveInfo().apply {
+ resolvePackageName = packageName
+ this.activityInfo = activityInfo
+ }
+
+ shadow.addResolveInfoForIntent(intent, resolveInfo)
+ }
+
+ if (defaultBrowser != null) {
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse(url)
+ intent.addCategory(Intent.CATEGORY_BROWSABLE)
+
+ val activityInfo = ActivityInfo().apply {
+ exported = defaultBrowserExported
+ packageName = defaultBrowser
+ }
+
+ val resolveInfo = ResolveInfo().apply {
+ resolvePackageName = defaultBrowser
+ this.activityInfo = activityInfo
+ }
+
+ shadow.addResolveInfoForIntent(intent, resolveInfo)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BrowsersTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BrowsersTest.kt
new file mode 100644
index 0000000000..613014038f
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BrowsersTest.kt
@@ -0,0 +1,380 @@
+/* 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.support.utils
+
+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.robolectric.testContext
+import mozilla.components.support.utils.Browsers.Companion.SAMPLE_BROWSER_HTTP_URL
+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.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+
+@Suppress("DEPRECATION") // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/11832
+@RunWith(AndroidJUnit4::class)
+class BrowsersTest {
+
+ @Test
+ fun `with empty package manager`() {
+ val browsers = Browsers.all(testContext)
+
+ assertNull(browsers.defaultBrowser)
+ assertNull(browsers.mozillaBrandedBrowser)
+ assertFalse(browsers.hasFirefoxBrandedBrowserInstalled)
+ assertTrue(browsers.installedBrowsers.isEmpty())
+ assertFalse(browsers.hasThirdPartyDefaultBrowser)
+ assertFalse(browsers.hasMultipleThirdPartyBrowsers)
+ assertFalse(browsers.isDefaultBrowser)
+ assertFalse(browsers.isFirefoxDefaultBrowser)
+ }
+
+ @Test
+ fun `with firefox as default browser`() {
+ pretendBrowsersAreInstalled(
+ defaultBrowser = Browsers.KnownBrowser.FIREFOX.packageName,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertNotNull(browsers.defaultBrowser)
+ assertEquals(Browsers.KnownBrowser.FIREFOX.packageName, browsers.defaultBrowser!!.packageName)
+
+ assertNotNull(browsers.mozillaBrandedBrowser)
+ assertEquals(Browsers.KnownBrowser.FIREFOX.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+
+ assertEquals(1, browsers.installedBrowsers.size)
+
+ assertFalse(browsers.hasThirdPartyDefaultBrowser)
+ assertFalse(browsers.hasMultipleThirdPartyBrowsers)
+ assertFalse(browsers.isDefaultBrowser)
+ assertTrue(browsers.isFirefoxDefaultBrowser)
+ }
+
+ @Test
+ fun `with multiple browsers installed`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
+ Browsers.KnownBrowser.FIREFOX.packageName,
+ Browsers.KnownBrowser.CHROME.packageName,
+ Browsers.KnownBrowser.SAMSUNG_INTERNET.packageName,
+ Browsers.KnownBrowser.DUCKDUCKGO.packageName,
+ Browsers.KnownBrowser.REFERENCE_BROWSER.packageName,
+ ),
+ defaultBrowser = Browsers.KnownBrowser.REFERENCE_BROWSER.packageName,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertNotNull(browsers.defaultBrowser)
+ assertEquals(Browsers.KnownBrowser.REFERENCE_BROWSER.packageName, browsers.defaultBrowser!!.packageName)
+
+ assertNotNull(browsers.mozillaBrandedBrowser)
+ assertEquals(Browsers.KnownBrowser.FIREFOX.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+
+ assertEquals(6, browsers.installedBrowsers.size)
+
+ assertTrue(browsers.hasThirdPartyDefaultBrowser)
+ assertTrue(browsers.hasMultipleThirdPartyBrowsers)
+ assertFalse(browsers.isDefaultBrowser)
+ assertFalse(browsers.isFirefoxDefaultBrowser)
+
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.REFERENCE_BROWSER))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.FIREFOX_NIGHTLY))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.FIREFOX))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.CHROME))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.REFERENCE_BROWSER))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.DUCKDUCKGO))
+
+ assertFalse(browsers.isInstalled(Browsers.KnownBrowser.CHROME_BETA))
+ assertFalse(browsers.isInstalled(Browsers.KnownBrowser.FIREFOX_BETA))
+ assertFalse(browsers.isInstalled(Browsers.KnownBrowser.ANDROID_STOCK_BROWSER))
+ assertFalse(browsers.isInstalled(Browsers.KnownBrowser.UC_BROWSER))
+
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.REFERENCE_BROWSER.packageName))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.FIREFOX.packageName))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.CHROME.packageName))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.REFERENCE_BROWSER.packageName))
+ assertTrue(browsers.isInstalled(Browsers.KnownBrowser.DUCKDUCKGO.packageName))
+
+ assertFalse(browsers.isInstalled(Browsers.KnownBrowser.CHROME_BETA.packageName))
+ assertFalse(browsers.isInstalled(Browsers.KnownBrowser.FIREFOX_BETA.packageName))
+ assertFalse(browsers.isInstalled(Browsers.KnownBrowser.ANDROID_STOCK_BROWSER.packageName))
+ assertFalse(browsers.isInstalled(Browsers.KnownBrowser.UC_BROWSER.packageName))
+ }
+
+ @Test
+ fun `With only firefox beta installed`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(Browsers.KnownBrowser.FIREFOX_BETA.packageName),
+ defaultBrowser = Browsers.KnownBrowser.FIREFOX_BETA.packageName,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertEquals(Browsers.KnownBrowser.FIREFOX_BETA.packageName, browsers.defaultBrowser!!.packageName)
+ assertEquals(Browsers.KnownBrowser.FIREFOX_BETA.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+ assertTrue(browsers.isFirefoxDefaultBrowser)
+ }
+
+ @Test
+ fun `With only firefox nightly installed`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName),
+ defaultBrowser = Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertEquals(Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName, browsers.defaultBrowser!!.packageName)
+ assertEquals(Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+ assertTrue(browsers.isFirefoxDefaultBrowser)
+ }
+
+ @Test
+ fun `With only firefox aurora installed`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(Browsers.KnownBrowser.FIREFOX_AURORA.packageName),
+ defaultBrowser = Browsers.KnownBrowser.FIREFOX_AURORA.packageName,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertEquals(Browsers.KnownBrowser.FIREFOX_AURORA.packageName, browsers.defaultBrowser!!.packageName)
+ assertEquals(Browsers.KnownBrowser.FIREFOX_AURORA.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+ assertTrue(browsers.isFirefoxDefaultBrowser)
+ }
+
+ @Test
+ fun `With only firefox froid installed`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(Browsers.KnownBrowser.FIREFOX_FDROID.packageName),
+ defaultBrowser = Browsers.KnownBrowser.FIREFOX_FDROID.packageName,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertEquals(Browsers.KnownBrowser.FIREFOX_FDROID.packageName, browsers.defaultBrowser!!.packageName)
+ assertEquals(Browsers.KnownBrowser.FIREFOX_FDROID.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+ assertTrue(browsers.isFirefoxDefaultBrowser)
+ }
+
+ @Test
+ fun `With this app being the default browser`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(testContext.packageName),
+ defaultBrowser = testContext.packageName,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertTrue(browsers.isDefaultBrowser)
+ assertFalse(browsers.isFirefoxDefaultBrowser)
+ assertFalse(browsers.hasThirdPartyDefaultBrowser)
+ }
+
+ @Test
+ fun `With unknown browsers`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ "org.example.random",
+ "org.example.a.browser",
+ Browsers.KnownBrowser.REFERENCE_BROWSER.packageName,
+ ),
+ defaultBrowser = "org.example.unknown.browser",
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertEquals("org.example.unknown.browser", browsers.defaultBrowser!!.packageName)
+ assertNull(browsers.mozillaBrandedBrowser)
+ assertFalse(browsers.hasFirefoxBrandedBrowserInstalled)
+ assertEquals(2, browsers.installedBrowsers.size)
+ assertTrue(browsers.hasThirdPartyDefaultBrowser)
+ assertTrue(browsers.hasMultipleThirdPartyBrowsers)
+ assertFalse(browsers.isDefaultBrowser)
+ assertFalse(browsers.isFirefoxDefaultBrowser)
+
+ val installedBrowsers = browsers.installedBrowsers.map { it.packageName }
+ assertTrue(installedBrowsers.contains("org.example.unknown.browser"))
+ assertTrue(installedBrowsers.contains(Browsers.KnownBrowser.REFERENCE_BROWSER.packageName))
+ }
+
+ @Test
+ fun `With default browser that is not exported`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName,
+ Browsers.KnownBrowser.FIREFOX.packageName,
+ ),
+ )
+
+ pretendBrowsersAreInstalled(
+ defaultBrowser = "org.example.unknown.browser",
+ defaultBrowserExported = false,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertNull(browsers.defaultBrowser)
+ assertEquals(Browsers.KnownBrowser.FIREFOX.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+ browsers.installedBrowsers.forEach { println(it.packageName + " : " + it.exported) }
+ assertEquals(2, browsers.installedBrowsers.size)
+ assertFalse(browsers.hasThirdPartyDefaultBrowser)
+
+ val installedBrowsers = browsers.installedBrowsers.map { it.packageName }
+ assertTrue(installedBrowsers.contains(Browsers.KnownBrowser.FIREFOX.packageName))
+ assertTrue(installedBrowsers.contains(Browsers.KnownBrowser.FIREFOX_NIGHTLY.packageName))
+ }
+
+ @Test
+ fun `With some browsers not exported`() {
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ Browsers.KnownBrowser.FIREFOX.packageName,
+ ),
+ )
+
+ pretendBrowsersAreInstalled(
+ browsers = listOf(
+ "org.example.area51.browser",
+ Browsers.KnownBrowser.CHROME.packageName,
+ ),
+ browsersExported = false,
+ )
+
+ val browsers = Browsers.all(testContext)
+
+ assertNull(browsers.defaultBrowser)
+ assertEquals(Browsers.KnownBrowser.FIREFOX.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+ browsers.installedBrowsers.forEach { println(it.packageName + " : " + it.exported) }
+ assertFalse(browsers.hasThirdPartyDefaultBrowser)
+ }
+
+ @Test
+ fun `forUrl() with empty package manager`() {
+ val browsers = Browsers.forUrl(testContext, SAMPLE_BROWSER_HTTP_URL)
+
+ assertNull(browsers.defaultBrowser)
+ assertNull(browsers.mozillaBrandedBrowser)
+ assertFalse(browsers.hasFirefoxBrandedBrowserInstalled)
+ assertTrue(browsers.installedBrowsers.isEmpty())
+ assertFalse(browsers.hasThirdPartyDefaultBrowser)
+ assertFalse(browsers.hasMultipleThirdPartyBrowsers)
+ assertFalse(browsers.isDefaultBrowser)
+ assertFalse(browsers.isFirefoxDefaultBrowser)
+ }
+
+ @Test
+ fun `forUrl() with firefox as default browser`() {
+ pretendBrowsersAreInstalled(
+ defaultBrowser = Browsers.KnownBrowser.FIREFOX.packageName,
+ )
+
+ val browsers = Browsers.forUrl(testContext, SAMPLE_BROWSER_HTTP_URL)
+
+ assertNotNull(browsers.defaultBrowser)
+ assertEquals(Browsers.KnownBrowser.FIREFOX.packageName, browsers.defaultBrowser!!.packageName)
+
+ assertNotNull(browsers.mozillaBrandedBrowser)
+ assertEquals(Browsers.KnownBrowser.FIREFOX.packageName, browsers.mozillaBrandedBrowser!!.packageName)
+
+ assertTrue(browsers.hasFirefoxBrandedBrowserInstalled)
+
+ assertEquals(1, browsers.installedBrowsers.size)
+
+ assertFalse(browsers.hasThirdPartyDefaultBrowser)
+ assertFalse(browsers.hasMultipleThirdPartyBrowsers)
+ assertFalse(browsers.isDefaultBrowser)
+ assertTrue(browsers.isFirefoxDefaultBrowser)
+ }
+
+ @Test
+ fun `forUrl() with wrong non-uri`() {
+ pretendBrowsersAreInstalled(
+ defaultBrowser = Browsers.KnownBrowser.FIREFOX.packageName,
+ )
+
+ val browsers = Browsers.forUrl(testContext, "not-a-uri")
+
+ assertNull(browsers.defaultBrowser)
+ assertNull(browsers.mozillaBrandedBrowser)
+ assertFalse(browsers.hasFirefoxBrandedBrowserInstalled)
+ assertTrue(browsers.installedBrowsers.isEmpty())
+ assertFalse(browsers.hasThirdPartyDefaultBrowser)
+ assertFalse(browsers.hasMultipleThirdPartyBrowsers)
+ assertFalse(browsers.isDefaultBrowser)
+ assertFalse(browsers.isFirefoxDefaultBrowser)
+ }
+
+ private fun pretendBrowsersAreInstalled(
+ browsers: List<String> = listOf(),
+ defaultBrowser: String? = null,
+ url: String = SAMPLE_BROWSER_HTTP_URL,
+ browsersExported: Boolean = true,
+ defaultBrowserExported: Boolean = true,
+ ) {
+ val packageManager = testContext.packageManager
+ val shadow = shadowOf(packageManager)
+
+ browsers.forEach { packageName ->
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.`package` = packageName
+ intent.data = Uri.parse(url)
+ intent.addCategory(Intent.CATEGORY_BROWSABLE)
+
+ val packageInfo = PackageInfo()
+ packageInfo.packageName = packageName
+
+ shadow.installPackage(packageInfo)
+
+ val activityInfo = ActivityInfo()
+ activityInfo.exported = browsersExported
+ activityInfo.packageName = packageName
+
+ val resolveInfo = ResolveInfo()
+ resolveInfo.resolvePackageName = packageName
+ resolveInfo.activityInfo = activityInfo
+
+ shadow.addResolveInfoForIntent(intent, resolveInfo)
+ }
+
+ if (defaultBrowser != null) {
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse(url)
+ intent.addCategory(Intent.CATEGORY_BROWSABLE)
+
+ val activityInfo = ActivityInfo()
+ activityInfo.exported = defaultBrowserExported
+ activityInfo.packageName = defaultBrowser
+
+ val resolveInfo = ResolveInfo()
+ resolveInfo.resolvePackageName = defaultBrowser
+ resolveInfo.activityInfo = activityInfo
+
+ shadow.addResolveInfoForIntent(intent, resolveInfo)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BundleTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BundleTest.kt
new file mode 100644
index 0000000000..e7835c7cef
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BundleTest.kt
@@ -0,0 +1,116 @@
+/* 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.support.utils
+
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.utils.ext.getParcelableArrayCompat
+import mozilla.components.support.utils.ext.safeCastToArrayOfT
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BundleTest {
+
+ @Test
+ fun `safeCastToArrayOfT with expected elements`() {
+ val testArray = Array<Parcelable>(4) { Expected() }
+
+ val result = testArray.safeCastToArrayOfT<Expected>()
+
+ assertArrayEquals(testArray, result)
+ }
+
+ @Test
+ fun `safeCastToArrayOfT with unexpected elements returns empty array and does not throw exception`() {
+ val testArray = Array<Parcelable>(4) { Unexpected() }
+
+ val result = testArray.safeCastToArrayOfT<Expected>()
+
+ assertArrayEquals(emptyArray(), result)
+ }
+
+ @Test
+ fun `safeCastToArrayOfT with expected and unexpected elements returns array with only expected`() {
+ val testArray = Array<Parcelable>(4) { Expected() }
+ testArray.plus(Unexpected())
+
+ val result = testArray.safeCastToArrayOfT<Expected>()
+
+ assertEquals(4, result.size)
+ assertTrue(result.isArrayOf<Expected>())
+ }
+
+ @Test
+ fun `getParcelableArrayCompat with expected type`() {
+ val bundle = Bundle()
+
+ val testArray = Array(4) { Expected() }
+
+ bundle.putParcelableArray(KEY, testArray)
+
+ val result = bundle.getParcelableArrayCompat(KEY, Expected::class.java)
+
+ assertArrayEquals(testArray, result)
+ }
+
+ @Test
+ fun `getParcelableArrayCompat with unexpected type returns empty array and does not throw exception`() {
+ val bundle = Bundle()
+
+ val testArray = Array(4) { Unexpected() }
+
+ bundle.putParcelableArray(KEY, testArray)
+
+ val result = bundle.getParcelableArrayCompat(KEY, Expected::class.java)
+
+ assertArrayEquals(emptyArray(), result)
+ }
+
+ @Test
+ fun `getParcelableArrayCompat with both expected unexpected type returns array with only expected`() {
+ val bundle = Bundle()
+
+ val testArray = Array<Parcelable>(4) { Expected() }
+
+ testArray.plus(Unexpected())
+
+ bundle.putParcelableArray(KEY, testArray)
+
+ val result = bundle.getParcelableArrayCompat(KEY, Expected::class.java)
+
+ assertEquals(4, result?.size)
+ assertTrue(result?.isArrayOf<Expected>() == true)
+ }
+
+ companion object {
+ private const val KEY = "test key"
+
+ class Expected : Parcelable {
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeString("good")
+ }
+ }
+
+ private class Unexpected : Parcelable {
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeString("wrong")
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ColorUtilsTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ColorUtilsTest.kt
new file mode 100644
index 0000000000..beb5ad8225
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ColorUtilsTest.kt
@@ -0,0 +1,55 @@
+/* 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.support.utils
+
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ColorUtilsTest {
+
+ @Test
+ fun getReadableTextColor() {
+ assertEquals(Color.BLACK.toLong(), ColorUtils.getReadableTextColor(Color.WHITE).toLong())
+ assertEquals(Color.WHITE.toLong(), ColorUtils.getReadableTextColor(Color.BLACK).toLong())
+
+ // Slack
+ assertEquals(Color.BLACK.toLong(), ColorUtils.getReadableTextColor(-0x90b14).toLong())
+
+ // Google+
+ assertEquals(Color.WHITE.toLong(), ColorUtils.getReadableTextColor(-0x24bbc9).toLong())
+
+ // Telegram
+ assertEquals(Color.WHITE.toLong(), ColorUtils.getReadableTextColor(-0xad825d).toLong())
+
+ // IRCCloud
+ assertEquals(Color.BLACK.toLong(), ColorUtils.getReadableTextColor(-0xd0804).toLong())
+
+ // Yahnac
+ assertEquals(Color.WHITE.toLong(), ColorUtils.getReadableTextColor(-0xa8400).toLong())
+ }
+
+ @Test
+ fun isDark() {
+ assertTrue(ColorUtils.isDark(Color.BLACK))
+ assertTrue(ColorUtils.isDark(Color.GRAY))
+ assertTrue(ColorUtils.isDark(Color.DKGRAY))
+ assertTrue(ColorUtils.isDark(Color.RED))
+ assertTrue(ColorUtils.isDark(Color.BLUE))
+ assertTrue(ColorUtils.isDark(Color.MAGENTA))
+
+ assertFalse(ColorUtils.isDark(Color.GREEN))
+ assertFalse(ColorUtils.isDark(Color.YELLOW))
+ assertFalse(ColorUtils.isDark(Color.LTGRAY))
+ assertFalse(ColorUtils.isDark(Color.CYAN))
+ assertFalse(ColorUtils.isDark(Color.WHITE))
+ assertFalse(ColorUtils.isDark(Color.TRANSPARENT))
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/CreditCardUtilsTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/CreditCardUtilsTest.kt
new file mode 100644
index 0000000000..49a18e36c2
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/CreditCardUtilsTest.kt
@@ -0,0 +1,107 @@
+/* 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.support.utils
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class CreditCardUtilsTest {
+
+ @Test
+ fun `GIVEN a list of recognized card numbers and their respective card type WHEN creditCardIIN is called for a given a card number THEN the correct cardType name is returned`() {
+ /**
+ * Test cases based on
+ * https://searchfox.org/mozilla-central/source/toolkit/modules/tests/xpcshell/test_CreditCard.js
+ */
+ val recognizedCards = listOf(
+ // Edge cases
+ Pair("2221000000000000", "mastercard"),
+ Pair("2720000000000000", "mastercard"),
+ Pair("2200000000000000", "mir"),
+ Pair("2204000000000000", "mir"),
+ Pair("340000000000000", "amex"),
+ Pair("370000000000000", "amex"),
+ Pair("3000000000000000", "diners"),
+ Pair("3050000000000000", "diners"),
+ Pair("3095000000000000", "diners"),
+ Pair("36000000000000", "diners"),
+ Pair("3800000000000000", "diners"),
+ Pair("3900000000000000", "diners"),
+ Pair("3528000000000000", "jcb"),
+ Pair("3589000000000000", "jcb"),
+ Pair("4035000000000000", "cartebancaire"),
+ Pair("4360000000000000", "cartebancaire"),
+ Pair("4000000000000000", "visa"),
+ Pair("4999999999999999", "visa"),
+ Pair("5400000000000000", "mastercard"),
+ Pair("5500000000000000", "mastercard"),
+ Pair("5100000000000000", "mastercard"),
+ Pair("5399999999999999", "mastercard"),
+ Pair("6011000000000000", "discover"),
+ Pair("6221260000000000", "discover"),
+ Pair("6229250000000000", "discover"),
+ Pair("6240000000000000", "discover"),
+ Pair("6269990000000000", "discover"),
+ Pair("6282000000000000", "discover"),
+ Pair("6288990000000000", "discover"),
+ Pair("6400000000000000", "discover"),
+ Pair("6500000000000000", "discover"),
+ Pair("6200000000000000", "unionpay"),
+ Pair("8100000000000000", "unionpay"),
+ // Valid according to Luhn number
+ Pair("2204941877211882", "mir"),
+ Pair("2720994326581252", "mastercard"),
+ Pair("374542158116607", "amex"),
+ Pair("36006666333344", "diners"),
+ Pair("3541675340715696", "jcb"),
+ Pair("3543769248058305", "jcb"),
+ Pair("4035501428146300", "cartebancaire"),
+ Pair("4111111111111111", "visa"),
+ Pair("5346755600299631", "mastercard"),
+ Pair("5495770093313616", "mastercard"),
+ Pair("5574238524540144", "mastercard"),
+ Pair("6011029459267962", "discover"),
+ Pair("6278592974938779", "unionpay"),
+ Pair("8171999927660000", "unionpay"),
+ Pair("30569309025904", "diners"),
+ Pair("38520000023237", "diners"),
+ Pair("3 8 5 2 0 0 0 0 0 2 3 2 3 7", "diners"),
+ )
+
+ for ((cardNumber, cardType) in recognizedCards) {
+ assertEquals(cardNumber.creditCardIIN()?.creditCardIssuerNetwork?.name, cardType)
+ }
+
+ val unrecognizedCards = listOf(
+ "411111111111111",
+ "41111111111111111",
+ "",
+ "9111111111111111",
+ )
+
+ for (cardNumber in unrecognizedCards) {
+ assertNull(cardNumber.creditCardIIN())
+ }
+ }
+
+ @Test
+ fun `GIVEN a various card type strings WHEN creditCardIssuerNetwork is called THEN the correct CreditCardIssuerNetwork is returned`() {
+ val amexCard = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.AMEX.cardName,
+ icon = R.drawable.ic_cc_logo_amex,
+ )
+
+ assertEquals(amexCard, CreditCardNetworkType.AMEX.cardName.creditCardIssuerNetwork())
+
+ val genericCard = CreditCardIssuerNetwork(
+ name = CreditCardNetworkType.GENERIC.cardName,
+ icon = R.drawable.ic_icon_credit_card_generic,
+ )
+
+ assertEquals(genericCard, "".creditCardIssuerNetwork())
+ assertEquals(genericCard, "blah".creditCardIssuerNetwork())
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/DomainMatcherTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/DomainMatcherTest.kt
new file mode 100644
index 0000000000..a01ff767d7
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/DomainMatcherTest.kt
@@ -0,0 +1,100 @@
+/* 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.support.utils
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DomainMatcherTest {
+
+ @Test
+ fun `should perform basic domain matching for a given query`() {
+ assertNull(segmentAwareDomainMatch("moz", listOf()))
+
+ val urls = listOf(
+ "http://www.mozilla.org", "http://Firefox.com",
+ "https://mobile.twitter.com", "https://m.youtube.com",
+ "https://en.Wikipedia.org/Wiki/Mozilla",
+ "https://www.github.com/mozilla-mobile/fenix",
+ "http://192.168.254.254:8000", "http://192.168.254.254:8000/admin",
+ "http://иННая.локаль", // TODO add more test data for non-english locales
+ "about:config", "about:crashes", "http://localhost:8080/index.html",
+ "https://www.reddit.com/r/vancouver/comments/quu9lt/hwy_1_just_north_of_lytton_is_gone/",
+ )
+ // Full url matching.
+ assertEquals(
+ DomainMatch("http://www.mozilla.org", "http://www.mozilla.org"),
+ segmentAwareDomainMatch("http://www.m", urls),
+ )
+ // Protocol stripping.
+ assertEquals(
+ DomainMatch("http://www.mozilla.org", "www.mozilla.org"),
+ segmentAwareDomainMatch("www.moz", urls),
+ )
+ // Subdomain stripping.
+ assertEquals(
+ DomainMatch("http://www.mozilla.org", "mozilla.org"),
+ segmentAwareDomainMatch("moz", urls),
+ )
+ assertEquals(
+ DomainMatch("https://mobile.twitter.com", "twitter.com"),
+ segmentAwareDomainMatch("twit", urls),
+ )
+ assertEquals(
+ DomainMatch("https://m.youtube.com", "youtube.com"),
+ segmentAwareDomainMatch("yo", urls),
+ )
+ // Subdomain stripping for sub-paths
+ assertEquals(
+ DomainMatch("https://www.github.com/mozilla-mobile/fenix", "github.com/mozilla-mobile/fenix"),
+ segmentAwareDomainMatch("github.com/moz", urls),
+ )
+ // Case insensitivity.
+ assertEquals(
+ DomainMatch("http://firefox.com", "firefox.com"),
+ segmentAwareDomainMatch("fire", urls),
+ )
+ // Urls with ports.
+ assertEquals(
+ DomainMatch("http://192.168.254.254:8000", "192.168.254.254:8000"),
+ segmentAwareDomainMatch("192", urls),
+ )
+ assertEquals(
+ DomainMatch("http://192.168.254.254:8000/admin", "192.168.254.254:8000/admin"),
+ segmentAwareDomainMatch("192.168.254.254:8000/a", urls),
+ )
+
+ assertEquals(
+ DomainMatch("http://localhost:8080/index.html", "localhost:8080/index.html"),
+ segmentAwareDomainMatch("localhost", urls),
+ )
+
+ // About urls.
+ assertEquals(
+ DomainMatch("about:config", "about:config"),
+ segmentAwareDomainMatch("abo", urls),
+ )
+ assertEquals(
+ DomainMatch("about:config", "about:config"),
+ segmentAwareDomainMatch("about:", urls),
+ )
+ assertEquals(
+ DomainMatch("about:crashes", "about:crashes"),
+ segmentAwareDomainMatch("about:cr", urls),
+ )
+
+ // Non-english locale.
+ assertEquals(
+ DomainMatch("http://инная.локаль", "инная.локаль"),
+ segmentAwareDomainMatch("ин", urls),
+ )
+
+ assertNull(segmentAwareDomainMatch("nomatch", urls))
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/DownloadUtilsTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/DownloadUtilsTest.kt
new file mode 100644
index 0000000000..55df6d9c13
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/DownloadUtilsTest.kt
@@ -0,0 +1,189 @@
+/* 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.support.utils
+
+import android.webkit.MimeTypeMap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.robolectric.Shadows
+
+@RunWith(AndroidJUnit4::class)
+class DownloadUtilsTest {
+
+ @Rule @JvmField
+ val folder = TemporaryFolder()
+
+ private fun assertContentDisposition(expected: String, contentDisposition: String) {
+ assertEquals(expected, DownloadUtils.guessFileName(contentDisposition, null, null, null))
+ }
+
+ @Test
+ fun guessFileName_contentDisposition() {
+ // Default file name
+ assertContentDisposition("downloadfile.bin", "")
+
+ CONTENT_DISPOSITION_TYPES.forEach { contentDisposition ->
+ // continuing with default filenames
+ assertContentDisposition("downloadfile.bin", contentDisposition)
+ assertContentDisposition("downloadfile.bin", "$contentDisposition;")
+ assertContentDisposition("downloadfile.bin", "$contentDisposition; filename")
+ assertContentDisposition(".bin", "$contentDisposition; filename=")
+ assertContentDisposition(".bin", "$contentDisposition; filename=\"\"")
+
+ // Provided filename field
+ assertContentDisposition("filename.jpg", "$contentDisposition; filename=\"filename.jpg\"")
+ assertContentDisposition("file\"name.jpg", "$contentDisposition; filename=\"file\\\"name.jpg\"")
+ assertContentDisposition("file\\name.jpg", "$contentDisposition; filename=\"file\\\\name.jpg\"")
+ assertContentDisposition("file\\\"name.jpg", "$contentDisposition; filename=\"file\\\\\\\"name.jpg\"")
+ assertContentDisposition("filename.jpg", "$contentDisposition; filename=filename.jpg")
+ assertContentDisposition("filename.jpg", "$contentDisposition; filename=filename.jpg; foo")
+ assertContentDisposition("filename.jpg", "$contentDisposition; filename=\"filename.jpg\"; foo")
+
+ // UTF-8 encoded filename* field
+ assertContentDisposition(
+ "\uD83E\uDD8A + x.jpg",
+ "$contentDisposition; filename=\"_.jpg\"; filename*=utf-8'en'%F0%9F%A6%8A%20+%20x.jpg",
+ )
+ assertContentDisposition(
+ "filename 的副本.jpg",
+ contentDisposition + ";filename=\"_.jpg\";" +
+ "filename*=UTF-8''filename%20%E7%9A%84%E5%89%AF%E6%9C%AC.jpg",
+ )
+ assertContentDisposition(
+ "filename.jpg",
+ "$contentDisposition; filename=_.jpg; filename*=utf-8'en'filename.jpg",
+ )
+ // Wrong order of the "filename*" segment
+ assertContentDisposition(
+ "filename.jpg",
+ "$contentDisposition; filename*=utf-8'en'filename.jpg; filename=_.jpg",
+ )
+ // Semicolon at the end
+ assertContentDisposition(
+ "filename.jpg",
+ "$contentDisposition; filename*=utf-8'en'filename.jpg; foo",
+ )
+
+ // ISO-8859-1 encoded filename* field
+ assertContentDisposition(
+ "file' 'name.jpg",
+ "$contentDisposition; filename=\"_.jpg\"; filename*=iso-8859-1'en'file%27%20%27name.jpg",
+ )
+
+ assertContentDisposition("success.html", "$contentDisposition; filename*=utf-8''success.html; foo")
+ assertContentDisposition("success.html", "$contentDisposition; filename*=utf-8''success.html")
+ }
+ }
+
+ @Test
+ fun uniqueFilenameNoExtension() {
+ assertEquals("test", DownloadUtils.uniqueFileName(folder.root, "test"))
+
+ folder.newFile("test")
+ assertEquals("test(1)", DownloadUtils.uniqueFileName(folder.root, "test"))
+
+ folder.newFile("test(1)")
+ assertEquals("test(2)", DownloadUtils.uniqueFileName(folder.root, "test"))
+ }
+
+ @Test
+ fun uniqueFilename() {
+ assertEquals("test.zip", DownloadUtils.uniqueFileName(folder.root, "test.zip"))
+
+ folder.newFile("test.zip")
+ assertEquals("test(1).zip", DownloadUtils.uniqueFileName(folder.root, "test.zip"))
+
+ folder.newFile("test(1).zip")
+ assertEquals("test(2).zip", DownloadUtils.uniqueFileName(folder.root, "test.zip"))
+ }
+
+ @Test
+ fun guessFileName_url() {
+ assertUrl("downloadfile.bin", "http://example.com/")
+ assertUrl("downloadfile.bin", "http://example.com/filename/")
+ assertUrl("filename.jpg", "http://example.com/filename.jpg")
+ assertUrl("filename.jpg", "http://example.com/foo/bar/filename.jpg")
+ }
+
+ @Test
+ fun guessFileName_mimeType() {
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("jpg", "image/jpeg")
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("zip", "application/zip")
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("tar.gz", "application/gzip")
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("bin", "application/octet-stream")
+
+ // For one mimetype to multiple extensions mapping
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("com", "application/x-msdos-program")
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("exe", "application/x-msdos-program")
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("bat", "application/x-msdos-program")
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("dll", "application/x-msdos-program")
+ // Matches the last inserted extension
+ assertEquals("dll", MimeTypeMap.getSingleton().getExtensionFromMimeType("application/x-msdos-program"))
+ assertEquals("application/x-msdos-program", MimeTypeMap.getSingleton().getMimeTypeFromExtension("exe"))
+
+ assertEquals("file.jpg", DownloadUtils.guessFileName(null, null, "http://example.com/file.jpg", "image/jpeg"))
+
+ // This is difference with URLUtil.guessFileName
+ assertEquals("file.jpg", DownloadUtils.guessFileName(null, null, "http://example.com/file.bin", "image/jpeg"))
+
+ assertEquals(
+ "Caesium-wahoo-v3.6-b792615ced1b.zip",
+ DownloadUtils.guessFileName(null, null, "https://download.msfjarvis.website/caesium/wahoo/beta/Caesium-wahoo-v3.6-b792615ced1b.zip", "application/zip"),
+ )
+ assertEquals(
+ "compressed.TAR.GZ",
+ DownloadUtils.guessFileName(null, null, "http://example.com/compressed.TAR.GZ", "application/gzip"),
+ )
+ assertEquals("file.html", DownloadUtils.guessFileName(null, null, "http://example.com/file?abc", "text/html"))
+ assertEquals("file.html", DownloadUtils.guessFileName(null, null, "http://example.com/file", "text/html"))
+ assertEquals("file.html", DownloadUtils.guessFileName(null, null, "http://example.com/file", "text/html; charset=utf-8"))
+ assertEquals("file.txt", DownloadUtils.guessFileName(null, null, "http://example.com/file.txt", "text/html"))
+ assertEquals("file.data", DownloadUtils.guessFileName(null, null, "http://example.com/file.data", "application/octet-stream"))
+ assertEquals("file.data", DownloadUtils.guessFileName(null, null, "http://example.com/file.data", "binary/octet-stream"))
+ assertEquals("file.data", DownloadUtils.guessFileName(null, null, "http://example.com/file.data", "application/unknown"))
+
+ assertEquals("file.jpg", DownloadUtils.guessFileName(null, null, "http://example.com/file.zip", "image/jpeg"))
+
+ // extra information in content-type
+ assertEquals("file.jpg", DownloadUtils.guessFileName(null, null, "http://example.com/file.jpg", "application/octet-stream; Charset=utf-8"))
+
+ // Should not change to file.dll
+ assertEquals("file.exe", DownloadUtils.guessFileName(null, null, "http://example.com/file.exe", "application/x-msdos-program"))
+ assertEquals("file.exe", DownloadUtils.guessFileName(null, null, "http://example.com/file.exe", "application/vnd.microsoft.portable-executable"))
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).clearMappings()
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("exe", "application/x-msdos-program")
+
+ assertEquals("file.exe", DownloadUtils.guessFileName(null, null, "http://example.com/file.bin", "application/x-msdos-program"))
+ }
+
+ @Test
+ fun sanitizeMimeType() {
+ assertEquals("application/pdf", DownloadUtils.sanitizeMimeType("application/pdf; qs=0.001"))
+ assertEquals("application/pdf", DownloadUtils.sanitizeMimeType("application/pdf"))
+ assertEquals(null, DownloadUtils.sanitizeMimeType(null))
+ }
+
+ @Test
+ fun makePdfContentDisposition() {
+ assertEquals("attachment; filename=foo.pdf;", DownloadUtils.makePdfContentDisposition("foo"))
+ assertEquals("attachment; filename=foo.html.pdf;", DownloadUtils.makePdfContentDisposition("foo.html"))
+ assertEquals("attachment; filename=foo.pdf;", DownloadUtils.makePdfContentDisposition("foo.pdf"))
+ assertEquals("attachment; filename=${"a".repeat(251)}.pdf;", DownloadUtils.makePdfContentDisposition("a".repeat(260)))
+ assertEquals("attachment; filename=${"a".repeat(251)}.pdf;", DownloadUtils.makePdfContentDisposition("a".repeat(260) + ".pdf"))
+ }
+
+ companion object {
+ private val CONTENT_DISPOSITION_TYPES = listOf("attachment", "inline")
+
+ private fun assertUrl(expected: String, url: String) {
+ assertEquals(expected, DownloadUtils.guessFileName(null, null, url, null))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ManufacturerCodesTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ManufacturerCodesTest.kt
new file mode 100644
index 0000000000..19b5b2d588
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ManufacturerCodesTest.kt
@@ -0,0 +1,114 @@
+/* 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.support.utils
+
+import mozilla.components.support.utils.ManufacturerCodes.manufacturer
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ManufacturerCodesTest {
+
+ @Test
+ fun testIsHuawei() {
+ manufacturer = "Huawei" // expected value for Huawei devices
+ assertTrue(ManufacturerCodes.isHuawei)
+
+ assertFalse(ManufacturerCodes.isSamsung)
+
+ assertFalse(ManufacturerCodes.isXiaomi)
+
+ assertFalse(ManufacturerCodes.isOnePlus)
+
+ assertFalse(ManufacturerCodes.isLG)
+
+ assertFalse(ManufacturerCodes.isOppo)
+ }
+
+ @Test
+ fun testIsSamsung() {
+ manufacturer = "Samsung" // expected value for Samsung devices
+
+ assertFalse(ManufacturerCodes.isHuawei)
+
+ assertTrue(ManufacturerCodes.isSamsung)
+
+ assertFalse(ManufacturerCodes.isXiaomi)
+
+ assertFalse(ManufacturerCodes.isOnePlus)
+
+ assertFalse(ManufacturerCodes.isLG)
+
+ assertFalse(ManufacturerCodes.isOppo)
+ }
+
+ @Test
+ fun testIsXiaomi() {
+ manufacturer = "Xiaomi" // expected value for Xiaomi devices
+
+ assertFalse(ManufacturerCodes.isHuawei)
+
+ assertFalse(ManufacturerCodes.isSamsung)
+
+ assertFalse(ManufacturerCodes.isOnePlus)
+
+ assertTrue(ManufacturerCodes.isXiaomi)
+
+ assertFalse(ManufacturerCodes.isLG)
+
+ assertFalse(ManufacturerCodes.isOppo)
+ }
+
+ @Test
+ fun testIsOnePlus() {
+ manufacturer = "OnePlus" // expected value for OnePlus devices
+
+ assertFalse(ManufacturerCodes.isHuawei)
+
+ assertFalse(ManufacturerCodes.isSamsung)
+
+ assertFalse(ManufacturerCodes.isXiaomi)
+
+ assertTrue(ManufacturerCodes.isOnePlus)
+
+ assertFalse(ManufacturerCodes.isLG)
+
+ assertFalse(ManufacturerCodes.isOppo)
+ }
+
+ @Test
+ fun testIsLG() {
+ manufacturer = "LGE" // expected value for LG devices
+
+ assertFalse(ManufacturerCodes.isHuawei)
+
+ assertFalse(ManufacturerCodes.isSamsung)
+
+ assertFalse(ManufacturerCodes.isXiaomi)
+
+ assertFalse(ManufacturerCodes.isOnePlus)
+
+ assertTrue(ManufacturerCodes.isLG)
+
+ assertFalse(ManufacturerCodes.isOppo)
+ }
+
+ @Test
+ fun testIsOppo() {
+ manufacturer = "OPPO" // expected value for Oppo devices
+
+ assertFalse(ManufacturerCodes.isHuawei)
+
+ assertFalse(ManufacturerCodes.isSamsung)
+
+ assertFalse(ManufacturerCodes.isXiaomi)
+
+ assertFalse(ManufacturerCodes.isOnePlus)
+
+ assertFalse(ManufacturerCodes.isLG)
+
+ assertTrue(ManufacturerCodes.isOppo)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/PendingIntentUtilsTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/PendingIntentUtilsTest.kt
new file mode 100644
index 0000000000..328bb79811
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/PendingIntentUtilsTest.kt
@@ -0,0 +1,28 @@
+/* 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.support.utils
+
+import android.app.PendingIntent
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class PendingIntentUtilsTest {
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) // highest API level for which to return 0
+ fun `GIVEN an Android L device WHEN defaultFlags is called THEN return 0`() {
+ assertEquals(0, PendingIntentUtils.defaultFlags)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M]) // first API level for which to return FLAG_IMMUTABLE
+ fun `GIVEN an Android M device WHEN defaultFlags is called THEN return FLAG_IMMUTABLE`() {
+ assertEquals(PendingIntent.FLAG_IMMUTABLE, PendingIntentUtils.defaultFlags)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/RunWhenReadyQueueTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/RunWhenReadyQueueTest.kt
new file mode 100644
index 0000000000..ae5f398084
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/RunWhenReadyQueueTest.kt
@@ -0,0 +1,71 @@
+/* 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.support.utils
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+class RunWhenReadyQueueTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `task should not run until ready is called`() = runTestOnMain {
+ val task = mock<() -> Unit>()
+ val queue = RunWhenReadyQueue(scope)
+
+ verify(task, never()).invoke()
+ assertFalse(queue.isReady())
+
+ queue.runIfReadyOrQueue(task)
+ queue.ready()
+
+ verify(task).invoke()
+ assertTrue(queue.isReady())
+ }
+
+ @Test
+ fun `task should run if ready was called`() = runTestOnMain {
+ val task = mock<() -> Unit>()
+ val queue = RunWhenReadyQueue(scope)
+ queue.ready()
+
+ verify(task, never()).invoke()
+
+ queue.runIfReadyOrQueue(task)
+
+ verify(task).invoke()
+ }
+
+ @Test
+ fun `tasks should run in the order they were queued`() = runTestOnMain {
+ val task1 = mock<() -> Unit>()
+ val task2 = mock<() -> Unit>()
+ val task3 = mock<() -> Unit>()
+ val queue = RunWhenReadyQueue(scope)
+
+ queue.runIfReadyOrQueue(task1)
+ queue.runIfReadyOrQueue(task2)
+ queue.runIfReadyOrQueue(task3)
+ queue.ready()
+
+ val inOrder = inOrder(task1, task2, task3)
+ inOrder.verify(task1).invoke()
+ inOrder.verify(task2).invoke()
+ inOrder.verify(task3).invoke()
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeBundleTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeBundleTest.kt
new file mode 100644
index 0000000000..46970b2149
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeBundleTest.kt
@@ -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/. */
+
+package mozilla.components.support.utils
+
+import android.os.Bundle
+import android.os.Parcelable
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doThrow
+
+class SafeBundleTest {
+
+ private lateinit var bundle: Bundle
+
+ @Before
+ fun setup() {
+ bundle = mock()
+ }
+
+ @Test
+ fun `getInt returns default value if bundle throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java).`when`(bundle).getInt(anyString(), anyInt())
+
+ val expected = 1
+ assertEquals(expected, SafeBundle(bundle).getInt("", expected))
+ }
+
+ @Test
+ fun `getString returns null if bundle throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java).`when`(bundle).getString(anyString())
+
+ assertNull(bundle.toSafeBundle().getString(""))
+ }
+
+ @Test
+ fun `getParcelable returns null if bundle throws OutOfMemoryError`() {
+ @Suppress("DEPRECATION")
+ doThrow(OutOfMemoryError::class.java).`when`(bundle).getParcelable<Parcelable>(anyString())
+
+ assertNull(SafeBundle(bundle).getParcelable("", Parcelable::class.java))
+ }
+
+ @Test
+ fun `getUnsafe returns original bundle`() {
+ assertEquals(bundle, SafeBundle(bundle).unsafe)
+ }
+
+ @Test
+ fun `WHEN toSafeBundle wraps an bundle THEN it has the same unsafe bundle as the SafeBundle constructor`() {
+ // SafeBundle does not override .equals so we have to do comparison with their underlying unsafe bundles.
+ assertEquals(SafeBundle(bundle).unsafe, bundle.toSafeBundle().unsafe)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeIntentTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeIntentTest.kt
new file mode 100644
index 0000000000..45ce8f3342
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeIntentTest.kt
@@ -0,0 +1,254 @@
+/* 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.support.utils
+
+import android.content.Intent
+import android.os.Parcelable
+import mozilla.components.support.test.mock
+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.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+
+class SafeIntentTest {
+
+ private lateinit var intent: Intent
+
+ @Before
+ fun setup() {
+ intent = mock()
+ }
+
+ @Test
+ fun `getStringArrayListExtra returns null if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).getStringArrayListExtra(anyString())
+
+ assertNull(SafeIntent(intent).getStringArrayListExtra("mozilla"))
+ }
+
+ @Test
+ fun `getExtras returns null if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).extras
+
+ assertNull(SafeIntent(intent).extras)
+ }
+
+ @Test
+ fun `getAction return original action`() {
+ val expected = Intent.ACTION_MAIN
+
+ doReturn(expected)
+ .`when`(intent).action
+
+ assertEquals(expected, SafeIntent(intent).action)
+ }
+
+ @Test
+ fun `getFlags returns original flags`() {
+ val expected = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ doReturn(expected)
+ .`when`(intent).flags
+
+ assertEquals(expected, SafeIntent(intent).flags)
+ }
+
+ @Test
+ fun `isLauncherIntent returns false if intent is not Launcher Intent`() {
+ // category is null
+ doReturn(null)
+ .`when`(intent).categories
+
+ assertFalse(SafeIntent(intent).isLauncherIntent)
+
+ // both category and action are not valid
+ val category = HashSet<String>()
+
+ category.add("NOT" + Intent.CATEGORY_LAUNCHER)
+
+ doReturn(category)
+ .`when`(intent).categories
+
+ doReturn("NOT" + Intent.ACTION_MAIN)
+ .`when`(intent).action
+
+ assertFalse(SafeIntent(intent).isLauncherIntent)
+
+ // action is not valid
+ category.clear()
+
+ category.add(Intent.CATEGORY_LAUNCHER)
+
+ doReturn(category)
+ .`when`(intent).categories
+
+ doReturn("NOT" + Intent.ACTION_MAIN)
+ .`when`(intent).action
+
+ assertFalse(SafeIntent(intent).isLauncherIntent)
+
+ // category is not valid
+ category.clear()
+
+ category.add("NOT" + Intent.CATEGORY_LAUNCHER)
+
+ doReturn(category)
+ .`when`(intent).categories
+
+ doReturn(Intent.ACTION_MAIN)
+ .`when`(intent).action
+
+ assertFalse(SafeIntent(intent).isLauncherIntent)
+
+ // both are valid
+ category.clear()
+
+ category.add(Intent.CATEGORY_LAUNCHER)
+
+ doReturn(category)
+ .`when`(intent).categories
+
+ doReturn(Intent.ACTION_MAIN)
+ .`when`(intent).action
+
+ assertTrue(SafeIntent(intent).isLauncherIntent)
+ }
+
+ @Test
+ fun `isLauncherIntent returns true if intent is Launcher Intent`() {
+ // both category and action are not valid
+ val category = HashSet<String>()
+ category.add(Intent.CATEGORY_LAUNCHER)
+
+ doReturn(category)
+ .`when`(intent).categories
+
+ doReturn(Intent.ACTION_MAIN)
+ .`when`(intent).action
+
+ assertTrue(SafeIntent(intent).isLauncherIntent)
+ }
+
+ @Test
+ fun `getDataString returns null if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).dataString
+
+ assertNull(SafeIntent(intent).dataString)
+ }
+
+ @Test
+ fun `getData returns null if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).data
+
+ assertNull(SafeIntent(intent).data)
+ }
+
+ @Test
+ fun `getCategories returns null if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).categories
+
+ assertNull(SafeIntent(intent).categories)
+ }
+
+ @Test
+ fun `hasExtra returns false if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).hasExtra(anyString())
+
+ assertFalse(SafeIntent(intent).hasExtra(""))
+ }
+
+ @Test
+ fun `getBooleanExtra returns false if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).getBooleanExtra(anyString(), anyBoolean())
+
+ assertFalse(SafeIntent(intent).getBooleanExtra("", false))
+ }
+
+ @Test
+ fun `getIntExtra returns default value if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).getIntExtra(anyString(), anyInt())
+
+ val expected = 1
+ assertEquals(expected, SafeIntent(intent).getIntExtra("", expected))
+ }
+
+ @Test
+ fun `getStringExtra returns null if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).getStringExtra(anyString())
+
+ assertNull(SafeIntent(intent).getStringExtra(""))
+ }
+
+ @Test
+ fun `getBundleExtra returns null if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).getBundleExtra(anyString())
+
+ assertNull(SafeIntent(intent).getBundleExtra(""))
+ }
+
+ @Test
+ fun `getCharSequenceExtra returns null if intent throws OutOfMemoryError`() {
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).getCharSequenceExtra(anyString())
+
+ assertNull(SafeIntent(intent).getCharSequenceExtra(""))
+ }
+
+ @Test
+ fun `getParcelableExtra returns null if intent throws OutOfMemoryError`() {
+ @Suppress("DEPRECATION")
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).getParcelableExtra<Parcelable>(anyString())
+
+ assertNull(SafeIntent(intent).getParcelableExtra("", Parcelable::class.java))
+ }
+
+ @Test
+ fun `getParcelableArrayListExtra returns null if intent throws OutOfMemoryError`() {
+ @Suppress("DEPRECATION")
+ doThrow(OutOfMemoryError::class.java)
+ .`when`(intent).getParcelableArrayListExtra<Parcelable>(anyString())
+
+ assertNull(SafeIntent(intent).getParcelableArrayListExtra("", Parcelable::class.java))
+ }
+
+ @Test
+ fun `getParcelableArrayListExtra returns ArrayList if intent is safe`() {
+ val expected = ArrayList<Any>()
+ @Suppress("DEPRECATION")
+ doReturn(expected)
+ .`when`(intent).getParcelableArrayListExtra<Parcelable>(anyString())
+
+ assertEquals(expected, SafeIntent(intent).getParcelableArrayListExtra("", Parcelable::class.java))
+ }
+
+ @Test
+ fun `getUnsafe returns original intent`() {
+ assertEquals(intent, SafeIntent(intent).unsafe)
+ }
+
+ @Test
+ fun `WHEN toSafeIntent wraps an intent THEN it has the same unsafe intent as the SafeIntent constructor`() {
+ // SafeIntent does not override .equals so we have to do comparison with their underlying unsafe intents.
+ assertEquals(SafeIntent(intent).unsafe, intent.toSafeIntent().unsafe)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeUrlTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeUrlTest.kt
new file mode 100644
index 0000000000..082ce34103
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/SafeUrlTest.kt
@@ -0,0 +1,68 @@
+/* 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.support.utils
+
+import android.content.Context
+import android.content.res.Resources
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+
+class SafeUrlTest {
+ @Test
+ fun `WHEN unsafeText is null THEN stripUnsafeUrlSchemes returns null`() {
+ assertNull(SafeUrl.stripUnsafeUrlSchemes(mock(), null))
+ }
+
+ @Test
+ fun `WHEN unsafeText is empty THEN stripUnsafeUrlSchemes returns null`() {
+ assertNull(SafeUrl.stripUnsafeUrlSchemes(mock(), ""))
+ }
+
+ @Test
+ fun `WHEN unsafeText is whitespace THEN stripUnsafeUrlSchemes returns null`() {
+ assertNull(SafeUrl.stripUnsafeUrlSchemes(mock(), " "))
+ }
+
+ @Test
+ fun `WHEN schemes blocklist is empty THEN stripUnsafeUrlSchemes should return the initial String`() {
+ val resources = mock<Resources>()
+ val context = mock<Context>()
+ doReturn(resources).`when`(context).resources
+ doReturn(emptyArray<String>()).`when`(resources).getStringArray(R.array.mozac_url_schemes_blocklist)
+
+ val result = SafeUrl.stripUnsafeUrlSchemes(context, "unsafeText")
+
+ assertEquals("unsafeText", result)
+ }
+
+ @Test
+ fun `WHEN schemes blocklist contains items not found in the argument THEN stripUnsafeUrlSchemes should return the initial String`() {
+ val resources = mock<Resources>()
+ val context = mock<Context>()
+ doReturn(resources).`when`(context).resources
+ doReturn(arrayOf("alien")).`when`(resources).getStringArray(R.array.mozac_url_schemes_blocklist)
+
+ val result = SafeUrl.stripUnsafeUrlSchemes(context, "thisIsAnOkText")
+
+ assertEquals("thisIsAnOkText", result)
+ }
+
+ @Test
+ fun `WHEN schemes blocklist contains items found in the argument THEN stripUnsafeUrlSchemes should recursively remove them from the front`() {
+ val resources = mock<Resources>()
+ val context = mock<Context>()
+ doReturn(resources).`when`(context).resources
+ doReturn(arrayOf("one", "two")).`when`(resources).getStringArray(R.array.mozac_url_schemes_blocklist)
+
+ val result = SafeUrl.stripUnsafeUrlSchemes(context, "two" + "one" + "one" + "two" + "safeText")
+ assertEquals("safeText", result)
+
+ val result2 = SafeUrl.stripUnsafeUrlSchemes(context, "one" + "two" + "one" + "two" + "safeText" + "one")
+ assertEquals("safeText" + "one", result2)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/StorageUtilsTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/StorageUtilsTest.kt
new file mode 100644
index 0000000000..e619e59bc5
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/StorageUtilsTest.kt
@@ -0,0 +1,57 @@
+/* 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.support.utils
+
+import mozilla.components.support.utils.StorageUtils.levenshteinDistance
+import org.junit.Assert.assertEquals
+import org.junit.Ignore
+import org.junit.Test
+
+class StorageUtilsTest {
+
+ @Test
+ @Ignore("Some assertions failed. Should we fix implementation?")
+ fun checkLevenshteinDistance() {
+ // Test data <s>stealed</s> borrowed from
+ // https://oldfashionedsoftware.com/tag/levenshtein-distance/
+
+ // Empty strings
+ assertEquals(Int.MAX_VALUE, levenshteinDistance("", ""))
+ assertEquals(Int.MAX_VALUE, levenshteinDistance("a", ""))
+ assertEquals(Int.MAX_VALUE, levenshteinDistance("", "a"))
+ assertEquals(Int.MAX_VALUE, levenshteinDistance("abc", ""))
+ assertEquals(Int.MAX_VALUE, levenshteinDistance("", "abc"))
+
+ // Equal strings
+ assertEquals(0, levenshteinDistance("a", "a"))
+ assertEquals(0, levenshteinDistance("abc", "abc"))
+
+ // Only inserts are needed
+ assertEquals(1, levenshteinDistance("a", "ab"))
+ assertEquals(1, levenshteinDistance("b", "ab"))
+ assertEquals(1, levenshteinDistance("ac", "abc"))
+ assertEquals(6, levenshteinDistance("abcdefg", "xabxcdxxefxgx"))
+
+ // Only deletes are needed
+ assertEquals(1, levenshteinDistance("ab", "a"))
+ assertEquals(1, levenshteinDistance("ab", "b"))
+ assertEquals(1, levenshteinDistance("abc", "ac"))
+ assertEquals(6, levenshteinDistance("xabxcdxxefxgx", "abcdefg"))
+
+ // Only substitutions are needed
+ assertEquals(1, levenshteinDistance("a", "b"))
+ assertEquals(1, levenshteinDistance("ab", "ac"))
+ assertEquals(1, levenshteinDistance("ac", "bc"))
+ assertEquals(1, levenshteinDistance("abc", "axc"))
+ assertEquals(6, levenshteinDistance("xabxcdxxefxgx", "1ab2cd34ef5g6"))
+
+ // Many operations are needed
+ assertEquals(3, levenshteinDistance("example", "samples"))
+ assertEquals(6, levenshteinDistance("sturgeon", "urgently"))
+ assertEquals(6, levenshteinDistance("levenshtein", "frankenstein"))
+ assertEquals(5, levenshteinDistance("distance", "difference"))
+ assertEquals(10, levenshteinDistance("java was neat", "kotlin is great"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/URLStringUtilsTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/URLStringUtilsTest.kt
new file mode 100644
index 0000000000..cfd5f68fdb
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/URLStringUtilsTest.kt
@@ -0,0 +1,297 @@
+/* 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.support.utils
+
+import androidx.core.text.TextDirectionHeuristicCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.util.URLStringUtils
+import mozilla.components.support.ktx.util.URLStringUtils.isSearchTerm
+import mozilla.components.support.ktx.util.URLStringUtils.isURLLike
+import mozilla.components.support.ktx.util.URLStringUtils.toNormalizedURL
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import kotlin.random.Random
+
+@RunWith(AndroidJUnit4::class)
+class URLStringUtilsTest {
+
+ @Before
+ fun configurePatternFlags() {
+ URLStringUtils.flags = URLStringUtils.UNICODE_CHARACTER_CLASS
+ }
+
+ @Test
+ fun toNormalizedURL() {
+ val expectedUrl = "http://mozilla.org"
+ assertEquals(expectedUrl, toNormalizedURL("http://mozilla.org"))
+ assertEquals(expectedUrl, toNormalizedURL(" http://mozilla.org "))
+ assertEquals(expectedUrl, toNormalizedURL("mozilla.org"))
+ assertEquals(expectedUrl, toNormalizedURL("HTTP://mozilla.org"))
+ assertEquals("file:///mnt/sdcard/", toNormalizedURL("file:///mnt/sdcard/"))
+ assertEquals("http://mozilla.org", toNormalizedURL(" http://mozilla.org"))
+ assertEquals("http://localhost", toNormalizedURL("localhost"))
+
+ assertEquals(
+ "https://www.mozilla.org/en-US/internet-health/",
+ toNormalizedURL("https://www.mozilla.org/en-US/internet-health/"),
+ )
+ }
+
+ @Test
+ fun isUrlLike() {
+ assertFalse(isURLLike("inurl:mozilla.org advanced search"))
+ assertFalse(isURLLike("sf: help"))
+ assertFalse(isURLLike("mozilla./~"))
+ assertFalse(isURLLike("cnn.com politics"))
+
+ assertTrue(isURLLike("about:config"))
+ assertTrue(isURLLike("about:config:8000"))
+ assertTrue(isURLLike("file:///home/user/myfile.html"))
+ assertTrue(isURLLike("file://////////////home//user/myfile.html"))
+ assertTrue(isURLLike("file://C:\\Users\\user\\myfile.html"))
+ assertTrue(isURLLike("http://192.168.255.255"))
+ assertTrue(isURLLike("link.unknown"))
+ // Per https://bugs.chromium.org/p/chromium/issues/detail?id=31405, ICANN will accept
+ // purely numeric gTLDs.
+ assertTrue(isURLLike("3.14.2019"))
+ assertTrue(isURLLike("3-four.14.2019"))
+ assertTrue(isURLLike(" cnn.com "))
+ assertTrue(isURLLike(" cnn.com"))
+ assertTrue(isURLLike("cnn.com "))
+ assertTrue(isURLLike("mozilla.com/~userdir"))
+ assertTrue(isURLLike("my-domain.com"))
+ assertTrue(isURLLike("http://faß.de//"))
+ assertTrue(isURLLike("cnn.cơḿ"))
+ assertTrue(isURLLike("cnn.çơḿ"))
+
+ // Examples from the code comments:
+ assertTrue(isURLLike("c-c.com"))
+ assertTrue(isURLLike("c-c-c-c.c-c-c"))
+ assertTrue(isURLLike("c-http://c.com"))
+ assertTrue(isURLLike("about-mozilla:mozilla"))
+ assertTrue(isURLLike("c-http.d-x"))
+ assertTrue(isURLLike("www.c.-"))
+ assertTrue(isURLLike("3-3.3"))
+ assertTrue(isURLLike("www.c-c.-"))
+
+ assertFalse(isURLLike(" -://x.com "))
+ assertFalse(isURLLike(" -x.com"))
+ assertFalse(isURLLike("http://www-.com"))
+ assertFalse(isURLLike("www.c-c- "))
+ assertFalse(isURLLike("3-3 "))
+
+ // Valid IPv6 literals correctly recognized as valid.
+ val validIPv6Literals = listOf(
+ "[::]",
+ "[::1]",
+ "[1::]",
+ "[1:2:3:4:5:6:7:8]",
+ "[2001:db8::1.2.3.4]",
+ "[::1]:8080",
+ )
+
+ validIPv6Literals.forEach { url ->
+ assertTrue(isURLLike(url))
+ assertTrue(isURLLike("$url/"))
+ assertTrue(isURLLike("https://$url"))
+ assertTrue(isURLLike("https://$url/"))
+ assertTrue(isURLLike("https:$url"))
+ assertTrue(isURLLike("https:$url/"))
+ assertTrue(isURLLike("http://$url"))
+ assertTrue(isURLLike("http://$url/"))
+ assertTrue(isURLLike("http:$url"))
+ assertTrue(isURLLike("http:$url/"))
+ }
+
+ // Invalid IPv6 literals correctly recognized as invalid.
+ assertFalse(isURLLike("::1"))
+ assertFalse(isURLLike(":::"))
+ assertFalse(isURLLike("[[http://]]"))
+ assertFalse(isURLLike("[[["))
+ assertFalse(isURLLike("[[[:"))
+ assertFalse(isURLLike("[[[:/"))
+ assertFalse(isURLLike("http://]]]"))
+
+ // Invalid IPv6 literals correctly recognized as something else.
+ assertTrue(isURLLike("fe80::"))
+ assertTrue(isURLLike("x:["))
+
+ // Invalid IPv6 literals incorrectly recognized as valid.
+ // We allow these for now, until bug 1685152 is fixed.
+ assertTrue(isURLLike("[:::"))
+ assertTrue(isURLLike("http://[::"))
+ assertTrue(isURLLike("http://[::/path"))
+ assertTrue(isURLLike("http://[::?query"))
+ assertTrue(isURLLike("[[http://banana]]"))
+ assertTrue(isURLLike("http://[[["))
+ assertTrue(isURLLike("[[[::"))
+ assertTrue(isURLLike("[[[::/"))
+ assertTrue(isURLLike("http://[1.2.3]"))
+ assertTrue(isURLLike("https://[1:2:3:4:5:6:7]/"))
+ assertTrue(isURLLike("https://[1:2:3:4:5:6:7:8:9]/"))
+
+ // Examples from issues
+ assertTrue(isURLLike("https://abc--cba.com/")) // #7096
+ }
+
+ @Test
+ fun isSearchTerm() {
+ assertTrue(isSearchTerm("inurl:mozilla.org advanced search"))
+ assertTrue(isSearchTerm("sf: help"))
+ assertTrue(isSearchTerm("mozilla./~"))
+ assertTrue(isSearchTerm("cnn.com politics"))
+
+ assertFalse(isSearchTerm("about:config"))
+ assertFalse(isSearchTerm("about:config:8000"))
+ assertFalse(isSearchTerm("file:///home/user/myfile.html"))
+ assertFalse(isSearchTerm("file://////////////home//user/myfile.html"))
+ assertFalse(isSearchTerm("file://C:\\Users\\user\\myfile.html"))
+ assertFalse(isSearchTerm("http://192.168.255.255"))
+ assertFalse(isSearchTerm("link.unknown"))
+ // Per https://bugs.chromium.org/p/chromium/issues/detail?id=31405, ICANN will accept
+ // purely numeric gTLDs.
+ assertFalse(isSearchTerm("3.14.2019"))
+ assertFalse(isSearchTerm("3-four.14.2019"))
+ assertFalse(isSearchTerm(" cnn.com "))
+ assertFalse(isSearchTerm(" cnn.com"))
+ assertFalse(isSearchTerm("cnn.com "))
+ assertFalse(isSearchTerm("my-domain.com"))
+ assertFalse(isSearchTerm("camp-firefox.de"))
+ assertFalse(isSearchTerm("http://my-domain.com"))
+ assertFalse(isSearchTerm("mozilla.com/~userdir"))
+ assertFalse(isSearchTerm("http://faß.de//"))
+ assertFalse(isSearchTerm("cnn.cơḿ"))
+ assertFalse(isSearchTerm("cnn.çơḿ"))
+
+ // Examples from the code comments:
+ assertFalse(isSearchTerm("c-c.com"))
+ assertFalse(isSearchTerm("c-c-c-c.c-c-c"))
+ assertFalse(isSearchTerm("c-http://c.com"))
+ assertFalse(isSearchTerm("about-mozilla:mozilla"))
+ assertFalse(isSearchTerm("c-http.d-x"))
+ assertFalse(isSearchTerm("www.c.-"))
+ assertFalse(isSearchTerm("3-3.3"))
+ assertFalse(isSearchTerm("www.c-c.-"))
+
+ assertTrue(isSearchTerm(" -://x.com "))
+ assertTrue(isSearchTerm(" -x.com"))
+ assertTrue(isSearchTerm("http://www-.com"))
+ assertTrue(isSearchTerm("www.c-c- "))
+ assertTrue(isSearchTerm("3-3 "))
+
+ // Examples from issues
+ assertFalse(isSearchTerm("https://abc--cba.com/")) // #7096
+ }
+
+ @Test
+ fun stripUrlSchemeUrlWithHttps() {
+ val testDisplayUrl = URLStringUtils.toDisplayUrl("https://mozilla.com")
+ assertEquals("mozilla.com", testDisplayUrl)
+ }
+
+ @Test
+ fun stripTrailingSlash() {
+ val testDisplayUrl = URLStringUtils.toDisplayUrl("mozilla.com/")
+ assertEquals("mozilla.com", testDisplayUrl)
+ }
+
+ @Test
+ fun stripUrlSchemeUrlWithHttpsAndTrailingSlash() {
+ val testDisplayUrl = URLStringUtils.toDisplayUrl("https://mozilla.com/")
+ assertEquals("mozilla.com", testDisplayUrl)
+ }
+
+ @Test
+ fun stripUrlSchemeUrlWithHttp() {
+ val testDisplayUrl = URLStringUtils.toDisplayUrl("http://mozilla.com")
+ assertEquals("mozilla.com", testDisplayUrl)
+ }
+
+ @Test
+ fun stripUrlSubdomainUrlWithHttps() {
+ val testDisplayUrl = URLStringUtils.toDisplayUrl("https://www.mozilla.com")
+ assertEquals("mozilla.com", testDisplayUrl)
+ }
+
+ @Test
+ fun stripUrlSubdomainUrlWithHttp() {
+ val testDisplayUrl = URLStringUtils.toDisplayUrl("http://www.mozilla.com")
+ assertEquals("mozilla.com", testDisplayUrl)
+ }
+
+ @Test
+ fun stripUrlSchemeAndSubdomainUrlNoMatch() {
+ val testDisplayUrl = URLStringUtils.toDisplayUrl("zzz://www.mozillahttp://.com")
+ assertEquals("zzz://www.mozillahttp://.com", testDisplayUrl)
+ }
+
+ @Test
+ fun showDisplayUrlAsLTREvenIfTextStartsWithArabicCharacters() {
+ val testDisplayUrl = URLStringUtils.toDisplayUrl("http://ختار.ار/www.mozilla.org/1")
+ assertEquals("\u200Eختار.ار/www.mozilla.org/1", testDisplayUrl)
+ }
+
+ @Test
+ fun toDisplayUrlAlwaysUseATextDirectionHeuristicToDetermineDirectionality() {
+ val textHeuristic = spy(TestTextDirectionHeuristicCompat())
+
+ URLStringUtils.toDisplayUrl("http://ختار.ار/www.mozilla.org/1", textHeuristic)
+ verify(textHeuristic).isRtl("ختار.ار/www.mozilla.org/1", 0, 1)
+
+ URLStringUtils.toDisplayUrl("http://www.mozilla.org/1", textHeuristic)
+ verify(textHeuristic).isRtl("mozilla.org/1", 0, 1)
+ }
+
+ @Test
+ fun toDisplayUrlHandlesBlankStrings() {
+ assertEquals("", URLStringUtils.toDisplayUrl(""))
+
+ assertEquals(" ", URLStringUtils.toDisplayUrl(" "))
+ }
+
+ @Test
+ fun isHttpOrHttpsUrl() {
+ assertFalse(URLStringUtils.isHttpOrHttps(""))
+ assertFalse(URLStringUtils.isHttpOrHttps(" "))
+ assertFalse(URLStringUtils.isHttpOrHttps("mozilla.org"))
+ assertFalse(URLStringUtils.isHttpOrHttps("httpstrf://example.org"))
+ assertTrue(URLStringUtils.isHttpOrHttps("https://www.mozilla.org"))
+ assertTrue(URLStringUtils.isHttpOrHttps("http://example.org"))
+ assertTrue(URLStringUtils.isHttpOrHttps("http://192.168.0.1"))
+ }
+
+ @Test
+ fun isValidSearchQueryUrl() {
+ assertTrue(URLStringUtils.isValidSearchQueryUrl("https://example.com/search/?q=%s"))
+ assertTrue(URLStringUtils.isValidSearchQueryUrl("http://example.com/search/?q=%s"))
+ assertTrue(URLStringUtils.isValidSearchQueryUrl("http-test-site.com/search/?q=%s"))
+ assertFalse(URLStringUtils.isValidSearchQueryUrl("httpss://example.com/search/?q=%s"))
+ assertTrue(URLStringUtils.isValidSearchQueryUrl("example.com/search/?q=%s"))
+ assertTrue(URLStringUtils.isValidSearchQueryUrl(" example.com/search/?q=%s "))
+ assertFalse(URLStringUtils.isValidSearchQueryUrl("htps://example.com/search/?q=%s"))
+ }
+}
+
+/**
+ * Custom [TextDirectionHeuristicCompat] used only in tests to make possible testing of RTL checks.
+ * Overcomes the limitations not allowing Mockito to mock platform implementations.
+ *
+ * The return of both [isRtl] is non-deterministic. Setup a different behavior if needed.
+ */
+private open class TestTextDirectionHeuristicCompat : TextDirectionHeuristicCompat {
+ override fun isRtl(array: CharArray?, start: Int, count: Int): Boolean {
+ return Random.nextBoolean()
+ }
+
+ override fun isRtl(cs: CharSequence?, start: Int, count: Int): Boolean {
+ return Random.nextBoolean()
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/WebURLFinderTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/WebURLFinderTest.kt
new file mode 100644
index 0000000000..8f6d270495
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/WebURLFinderTest.kt
@@ -0,0 +1,233 @@
+/* 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.support.utils
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.utils.WebURLFinder.Companion.isValidWebURL
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WebURLFinderTest {
+ private fun find(string: String?): String? {
+ // Test with explicit unicode support. Implicit unicode support is available in Android
+ // but not on host systems where testing will take place. See the comment in WebURLFinder
+ // for additional information.
+ return WebURLFinder(string, true).bestWebURL()
+ }
+
+ private fun find(strings: List<String>): String? {
+ // Test with explicit unicode support. Implicit unicode support is available in Android
+ // but not on host systems where testing will take place. See the comment in WebURLFinder
+ // for additional information.
+ return WebURLFinder(strings, true).bestWebURL()
+ }
+
+ fun testNoEmail() {
+ assertNull(find("test@test.com"))
+ }
+
+ @Test
+ fun testSchemeFirst() {
+ assertEquals("http://scheme.com", find("test.com http://scheme.com"))
+ assertEquals("http://ç.çơḿ", find("www.cnn.com http://ç.çơḿ"))
+ }
+
+ @Test
+ fun testFullURL() {
+ assertEquals(
+ "http://scheme.com:8080/inner#anchor&arg=1",
+ find("test.com http://scheme.com:8080/inner#anchor&arg=1"),
+ )
+ assertEquals(
+ "http://s-scheme.com:8080/inner#anchor&arg=1",
+ find("test.com http://s-scheme.com:8080/inner#anchor&arg=1"),
+ )
+ assertEquals(
+ "http://t-example:8080/appversion-1.0.0/f/action.xhtml",
+ find("test.com http://t-example:8080/appversion-1.0.0/f/action.xhtml"),
+ )
+ assertEquals(
+ "http://t-example:8080/appversion-1.0.0/f/action.xhtml",
+ find("http://t-example:8080/appversion-1.0.0/f/action.xhtml"),
+ )
+ assertEquals("http://ß.de/", find("http://ß.de/ çnn.çơḿ"))
+ assertEquals("htt-p://ß.de/", find("çnn.çơḿ htt-p://ß.de/"))
+ assertEquals(
+ "http://[2001:db8::1.2.3.4]:8080/inner#anchor&arg=1",
+ find("test.com http://[2001:db8::1.2.3.4]:8080/inner#anchor&arg=1"),
+ )
+ assertEquals("http://[::]", find("test.com http://[::]"))
+ }
+
+ @Test
+ fun testNoScheme() {
+ assertEquals("noscheme.com", find("noscheme.com example.com"))
+ assertEquals("noscheme.com", find("-noscheme.com example.com"))
+ assertEquals("n-oscheme.com", find("n-oscheme.com example.com"))
+ assertEquals("n-oscheme.com", find("----------n-oscheme.com "))
+ assertEquals("n-oscheme.ç", find("----------n-oscheme.ç-----------------------"))
+
+ // We would ideally test "[::] example.com" here, but java.net.URI
+ // doesn't seem to accept IPv6 literals without a scheme.
+ }
+
+ @Test
+ fun testNoBadScheme() {
+ assertNull(find("file:///test javascript:///test.js"))
+ }
+
+ @Test
+ fun testStrings() {
+ assertEquals("http://test.com", find(listOf("http://test.com", "noscheme.com")))
+ assertEquals(
+ "http://test.com",
+ find(listOf("noschemefirst.com", "http://test.com")),
+ )
+ assertEquals(
+ "http://test.com/inner#test",
+ find(
+ listOf(
+ "noschemefirst.com",
+ "http://test.com/inner#test",
+ "http://second.org/fark",
+ ),
+ ),
+ )
+ assertEquals(
+ "http://test.com",
+ find(listOf("javascript:///test.js", "http://test.com")),
+ )
+ assertEquals("http://çnn.çơḿ", find(listOf("www.cnn.com http://çnn.çơḿ")))
+ }
+
+ @Test
+ fun testIsValidWebURL() {
+ assertTrue("http://test.com".isValidWebURL())
+ assertTrue("hTTp://test.com".isValidWebURL())
+ assertTrue("https://test.com".isValidWebURL())
+ assertTrue("htTPs://test.com".isValidWebURL())
+ assertTrue("about://test.com".isValidWebURL())
+ assertTrue("abOUTt://test.com".isValidWebURL())
+ assertTrue("data://test.com".isValidWebURL())
+ assertTrue("daAtA://test.com".isValidWebURL())
+ assertFalse("#http#://test.com".isValidWebURL())
+ assertFalse("file:///sdcard/download".isValidWebURL())
+ assertFalse("filE:///sdcard/Download".isValidWebURL())
+ assertFalse("javascript:alert('Hi')".isValidWebURL())
+ assertFalse("JAVascript:alert('Hi')".isValidWebURL())
+ assertFalse("JAVascript:alert('Hi')".isValidWebURL())
+ assertFalse("content://com.test.app/test".isValidWebURL())
+ assertFalse("coNTent://com.test.app/test".isValidWebURL())
+ }
+
+ @Test
+ fun isUrlLikeEmulated() {
+ // autolinkWebUrlPattern uses a copy of the regex from URLStringUtils,
+ // so here we emulate isURLLike() and copy its tests.
+ val isURLLike: (String) -> Boolean = {
+ find("random_text $it other_random_text") == it.trim()
+ }
+
+ assertFalse(isURLLike("inurl:mozilla.org advanced search"))
+ assertFalse(isURLLike("sf: help"))
+ assertFalse(isURLLike("mozilla./~"))
+ assertFalse(isURLLike("cnn.com politics"))
+
+ assertTrue(isURLLike("about:config"))
+ assertTrue(isURLLike("about:config:8000"))
+
+ // These cases differ from the original isUrlLike test because
+ // file:// is rejected by isInvalidUriScheme.
+ assertFalse(isURLLike("file:///home/user/myfile.html"))
+ assertFalse(isURLLike("file://////////////home//user/myfile.html"))
+ assertFalse(isURLLike("file://C:\\Users\\user\\myfile.html"))
+
+ assertTrue(isURLLike("http://192.168.255.255"))
+ assertTrue(isURLLike("link.unknown"))
+ assertTrue(isURLLike("3.14.2019"))
+ assertTrue(isURLLike("3-four.14.2019"))
+ assertTrue(isURLLike(" cnn.com "))
+ assertTrue(isURLLike(" cnn.com"))
+ assertTrue(isURLLike("cnn.com "))
+ assertTrue(isURLLike("mozilla.com/~userdir"))
+ assertTrue(isURLLike("my-domain.com"))
+ assertTrue(isURLLike("http://faß.de//"))
+ assertTrue(isURLLike("cnn.cơḿ"))
+ assertTrue(isURLLike("cnn.çơḿ"))
+
+ assertTrue(isURLLike("c-c.com"))
+ assertTrue(isURLLike("c-c-c-c.c-c-c"))
+ assertTrue(isURLLike("c-http://c.com"))
+ assertTrue(isURLLike("about-mozilla:mozilla"))
+ assertTrue(isURLLike("c-http.d-x"))
+ assertTrue(isURLLike("www.c.-"))
+ assertTrue(isURLLike("3-3.3"))
+ assertTrue(isURLLike("www.c-c.-"))
+
+ assertFalse(isURLLike(" -://x.com "))
+ assertFalse(isURLLike(" -x.com"))
+ assertFalse(isURLLike("http://www-.com"))
+ assertFalse(isURLLike("www.c-c- "))
+ assertFalse(isURLLike("3-3 "))
+
+ val validIPv6Literals = listOf(
+ "[::]",
+ "[::1]",
+ "[1::]",
+ "[1:2:3:4:5:6:7:8]",
+ "[2001:db8::1.2.3.4]",
+ "[::1]:8080",
+ )
+
+ validIPv6Literals.forEach { url ->
+ // These cases differ from the original isUrlLike test because
+ // java.net.URI doesn't recognize bare IPv6 literals.
+ assertFalse(isURLLike(url))
+ assertFalse(isURLLike("$url/"))
+
+ assertTrue(isURLLike("https://$url"))
+ assertTrue(isURLLike("https://$url/"))
+ assertTrue(isURLLike("https:$url"))
+ assertTrue(isURLLike("https:$url/"))
+ assertTrue(isURLLike("http://$url"))
+ assertTrue(isURLLike("http://$url/"))
+ assertTrue(isURLLike("http:$url"))
+ assertTrue(isURLLike("http:$url/"))
+ }
+
+ assertFalse(isURLLike("::1"))
+ assertFalse(isURLLike(":::"))
+ assertFalse(isURLLike("[[http://]]"))
+ assertFalse(isURLLike("[[["))
+ assertFalse(isURLLike("[[[:"))
+ assertFalse(isURLLike("[[[:/"))
+ assertFalse(isURLLike("http://]]]"))
+
+ assertTrue(isURLLike("fe80::"))
+ assertTrue(isURLLike("x:["))
+
+ // These cases differ from the original isUrlLike test because
+ // the regex is just an approximation. When bug 1685152 is fixed,
+ // the original isURLLike will also return false.
+ assertFalse(isURLLike("[:::"))
+ assertFalse(isURLLike("http://[::"))
+ assertFalse(isURLLike("http://[::/path"))
+ assertFalse(isURLLike("http://[::?query"))
+ assertFalse(isURLLike("[[http://banana]]"))
+ assertFalse(isURLLike("http://[[["))
+ assertFalse(isURLLike("[[[::"))
+ assertFalse(isURLLike("[[[::/"))
+ assertFalse(isURLLike("http://[1.2.3]"))
+ assertFalse(isURLLike("https://[1:2:3:4:5:6:7]/"))
+ assertFalse(isURLLike("https://[1:2:3:4:5:6:7:8:9]/"))
+
+ assertTrue(isURLLike("https://abc--cba.com/"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/BitmapTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/BitmapTest.kt
new file mode 100644
index 0000000000..7e2313d3d0
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/BitmapTest.kt
@@ -0,0 +1,122 @@
+/* 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.support.utils.ext
+
+import android.graphics.Bitmap
+import android.util.Size
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BitmapTest {
+
+ @Test
+ fun `WHEN a square bitmap is smaller than the given max size THEN resizeMaintainingAspectRatio scales the size up maintaining the aspect ratio`() {
+ val maxSize = Size(48, 48)
+ val bitmapSize = Size(24, 24)
+ val bitmap = createBitmap(bitmapSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ assertEquals(maxSize, scaledBitmapSize)
+ }
+
+ @Test
+ fun `WHEN a square bitmap is same as the given max size THEN resizeMaintainingAspectRatio returns the original bitmap size`() {
+ val maxSize = Size(48, 48)
+ val bitmap = createBitmap(maxSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ assertEquals(maxSize, scaledBitmapSize)
+ }
+
+ @Test
+ fun `WHEN a square bitmap is larger than the given max size THEN resizeMaintainingAspectRatio scales the size down maintaining the aspect ratio`() {
+ val maxSize = Size(48, 48)
+ val bitmapSize = Size(96, 96)
+ val bitmap = createBitmap(bitmapSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ assertEquals(maxSize, scaledBitmapSize)
+ }
+
+ @Test
+ fun `WHEN a wide bitmap is smaller than the given max size THEN resizeMaintainingAspectRatio scales the size up maintaining the aspect ratio`() {
+ val maxSize = Size(48, 48)
+ val bitmapSize = Size(24, 12)
+ val bitmap = createBitmap(bitmapSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ val expected = Size(48, 24)
+ assertEquals(expected, scaledBitmapSize)
+ }
+
+ @Test
+ fun `WHEN a wide bitmap is same as the given max size THEN resizeMaintainingAspectRatio returns the bitmap original size`() {
+ val maxSize = Size(48, 48)
+ val bitmapSize = Size(48, 24)
+ val bitmap = createBitmap(bitmapSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ assertEquals(bitmapSize, scaledBitmapSize)
+ }
+
+ @Test
+ fun `WHEN a wide bitmap is larger than the given max size THEN resizeMaintainingAspectRatio scales the size down maintaining the aspect ratio`() {
+ val maxSize = Size(48, 48)
+ val bitmapSize = Size(192, 96)
+ val bitmap = createBitmap(bitmapSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ val expected = Size(48, 24)
+ assertEquals(expected, scaledBitmapSize)
+ }
+
+ @Test
+ fun `WHEN a tall bitmap is smaller than the given max size THEN resizeMaintainingAspectRatio scales the size up maintaining the aspect ratio`() {
+ val maxSize = Size(48, 48)
+ val bitmapSize = Size(12, 24)
+ val bitmap = createBitmap(bitmapSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ val expected = Size(24, 48)
+ assertEquals(expected, scaledBitmapSize)
+ }
+
+ @Test
+ fun `WHEN a tall bitmap is same as the given max size THEN resizeMaintainingAspectRatio returns the bitmap original size`() {
+ val maxSize = Size(48, 48)
+ val bitmapSize = Size(24, 48)
+ val bitmap = createBitmap(bitmapSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ assertEquals(bitmapSize, scaledBitmapSize)
+ }
+
+ @Test
+ fun `WHEN a tall bitmap is larger than the given max size THEN resizeMaintainingAspectRatio scales the size down maintaining the aspect ratio`() {
+ val maxSize = Size(48, 48)
+ val bitmapSize = Size(96, 192)
+ val bitmap = createBitmap(bitmapSize)
+
+ val scaledBitmapSize = bitmap.resizeMaintainingAspectRatio(maxSize)
+
+ val expected = Size(24, 48)
+ assertEquals(expected, scaledBitmapSize)
+ }
+
+ private fun createBitmap(size: Size) = with(size) {
+ Bitmap.createBitmap(IntArray(width * height), width, height, Bitmap.Config.ARGB_8888)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/PairKtTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/PairKtTest.kt
new file mode 100644
index 0000000000..b8328a1413
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/PairKtTest.kt
@@ -0,0 +1,31 @@
+/* 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 mozilla.components.support.utils.ext.toNullablePair
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class PairKtTest {
+
+ @Test
+ fun `a pair with two non-null values should become a non-null pair of non-null types`() {
+ val actual = ("hi" as String? to "there" as String?).toNullablePair()
+
+ assertNotNull(actual)
+ assertNotNull(actual!!.first)
+ assertNotNull(actual.second)
+ }
+
+ @Test
+ fun `a pair with any null values should become null`() {
+ listOf(
+ null to "nonNull",
+ null to null,
+ "nonNull" to null,
+ )
+ .map { it.toNullablePair() }
+ .forEach { assertNull(it) }
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/StringTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/StringTest.kt
new file mode 100644
index 0000000000..7405d7b63e
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/StringTest.kt
@@ -0,0 +1,19 @@
+/* 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.support.utils.ext
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class StringTest {
+
+ @Test
+ fun `GIVEN a string credit card number WHEN calling toCreditCardNumber THEN any character that is not a digit will removed`() {
+ val number = "385 - 2 0 0 - 0 0 0 2 3 2 3 7"
+ val expectedResult = "38520000023237"
+
+ assertEquals(expectedResult, number.toCreditCardNumber())
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/WindowInsetsCompatTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/WindowInsetsCompatTest.kt
new file mode 100644
index 0000000000..30f08ef641
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/ext/WindowInsetsCompatTest.kt
@@ -0,0 +1,81 @@
+/* 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.support.utils.ext
+
+import androidx.core.graphics.Insets
+import androidx.core.view.WindowInsetsCompat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class WindowInsetsCompatTest {
+ private lateinit var windowInsetsCompat: WindowInsetsCompat
+ private lateinit var insets: Insets
+ private lateinit var mandatorySystemGestureInsets: Insets
+
+ private var topPixels: Int = 0
+ private var rightPixels: Int = 0
+ private var leftPixels: Int = 0
+ private var bottomPixels: Int = 0
+
+ @Before
+ @ExperimentalCoroutinesApi
+ fun setUp() {
+ windowInsetsCompat = mock()
+
+ topPixels = 1
+ rightPixels = 2
+ leftPixels = 3
+ bottomPixels = 4
+
+ insets = Insets.of(leftPixels, topPixels, rightPixels, bottomPixels)
+ mandatorySystemGestureInsets = Insets.of(leftPixels, topPixels, rightPixels, bottomPixels)
+
+ whenever(windowInsetsCompat.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars())).thenReturn(
+ insets,
+ )
+ whenever(windowInsetsCompat.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures())).thenReturn(
+ mandatorySystemGestureInsets,
+ )
+ }
+
+ @Test
+ fun testTop() {
+ val topInsets = windowInsetsCompat.top()
+
+ assertEquals(topPixels, topInsets)
+ }
+
+ @Test
+ fun testRight() {
+ val rightInsets = windowInsetsCompat.right()
+
+ assertEquals(rightPixels, rightInsets)
+ }
+
+ @Test
+ fun testLeft() {
+ val leftInsets = windowInsetsCompat.left()
+
+ assertEquals(leftPixels, leftInsets)
+ }
+
+ @Test
+ fun testBottom() {
+ val bottomInsets = windowInsetsCompat.bottom()
+
+ assertEquals(bottomPixels, bottomInsets)
+ }
+
+ @Test
+ fun testMandatorySystemGestureInsets() {
+ val mandatorySystemGestureInsets = windowInsetsCompat.mandatorySystemGestureInsets()
+
+ assertEquals(this.mandatorySystemGestureInsets, mandatorySystemGestureInsets)
+ }
+}
diff --git a/mobile/android/android-components/components/support/utils/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/utils/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/utils/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/support/webextensions/README.md b/mobile/android/android-components/components/support/webextensions/README.md
new file mode 100644
index 0000000000..fdbefaae18
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/README.md
@@ -0,0 +1,37 @@
+# [Android Components](../../../README.md) > Support > Webextensions
+
+A component containing building blocks for features implemented as web extensions.
+
+Usually this component never needs to be added to application projects manually. Other components may have a transitive dependency on some of the classes and interfaces in this component.
+
+## Usage
+
+### 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:support-webextensions:{latest-version}"
+```
+
+## Facts
+
+This component emits the following [Facts](../../support/base/README.md#Facts):
+
+| Action | Item | Extras | Description |
+|-------------|----------------------------|-------------------|---------------------------------|
+| Interaction | web_extensions_initialized | `extensionExtras` | Web extensions are initialized. |
+
+
+#### `extensionExtras`
+
+| Key | Type | Value |
+|-------------|--------------|---------------------------------------|
+| "enabled" | List<String> | List of enabled web extension ids. |
+| "installed" | List<String> | List of installed web extensions ids. |
+
+## 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/support/webextensions/build.gradle b/mobile/android/android-components/components/support/webextensions/build.gradle
new file mode 100644
index 0000000000..87413a7c4b
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/build.gradle
@@ -0,0 +1,55 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lint {
+ warningsAsErrors true
+ abortOnError true
+ }
+
+ namespace 'mozilla.components.support.webextensions'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+}
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':browser-state')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+
+ 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/support/webextensions/proguard-rules.pro b/mobile/android/android-components/components/support/webextensions/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/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/support/webextensions/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/webextensions/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/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/support/webextensions/src/main/java/mozilla/components/support/webextensions/ExtensionsProcessDisabledPromptObserver.kt b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/ExtensionsProcessDisabledPromptObserver.kt
new file mode 100644
index 0000000000..acb3ac84f9
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/ExtensionsProcessDisabledPromptObserver.kt
@@ -0,0 +1,52 @@
+/* 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.support.webextensions
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Observes the [BrowserStore] state for when the extensions process spawning has been disabled and
+ * the user should be prompted. This requires running in both the foreground and background.
+ *
+ * @property store the application's [BrowserStore].
+ * @property shouldCancelOnStop If false, this observer will run indefinitely to be able to react
+ * to state changes when the app is either in the foreground or in the background.
+ * Please note to not have any references to Activity or it's context in an observer where this
+ * is false. Defaults to true.
+ * @property onShowExtensionsProcessDisabledPrompt a callback invoked when the application should
+ * open a prompt.
+ */
+open class ExtensionsProcessDisabledPromptObserver(
+ private val store: BrowserStore,
+ private val shouldCancelOnStop: Boolean = true,
+ private val onShowExtensionsProcessDisabledPrompt: () -> Unit,
+) : LifecycleAwareFeature {
+ private var scope: CoroutineScope? = null
+
+ override fun start() {
+ if (scope == null) {
+ scope = store.flowScoped { flow ->
+ flow.distinctUntilChangedBy { it.showExtensionsProcessDisabledPrompt }
+ .collect { state ->
+ if (state.showExtensionsProcessDisabledPrompt) {
+ onShowExtensionsProcessDisabledPrompt()
+ }
+ }
+ }
+ }
+ }
+
+ override fun stop() {
+ if (shouldCancelOnStop) {
+ scope?.cancel()
+ scope = null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionController.kt b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionController.kt
new file mode 100644
index 0000000000..a1acbd3200
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionController.kt
@@ -0,0 +1,179 @@
+/* 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.support.webextensions
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionRuntime
+import mozilla.components.support.base.log.logger.Logger
+import org.json.JSONObject
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Provides functionality to feature modules that need to interact with a web extension.
+ *
+ * @property extensionId the unique ID of the web extension e.g. mozacReaderview.
+ * @property extensionUrl the url pointing to a resources path for locating the
+ * extension within the APK file e.g. resource://android/assets/extensions/my_web_ext.
+ * @property defaultPort the name of the default port used to exchange messages
+ * between extension scripts and the application. Extensions can open multiple ports
+ * so [sendContentMessage] and [sendBackgroundMessage] allow specifying an
+ * alternative port, if needed.
+ */
+class WebExtensionController(
+ private val extensionId: String,
+ private val extensionUrl: String,
+ private val defaultPort: String,
+) {
+ private val logger = Logger("mozac-webextensions")
+ private var registerContentMessageHandler: (WebExtension) -> Unit? = { }
+ private var registerBackgroundMessageHandler: (WebExtension) -> Unit? = { }
+
+ /**
+ * Makes sure the web extension is installed in the provided runtime. If a
+ * content message handler was registered (see
+ * [registerContentMessageHandler]) before install completed, registration
+ * will happen upon successful installation.
+ *
+ * @param runtime the [WebExtensionRuntime] the web extension should be installed in.
+ * @param onSuccess (optional) callback invoked if the extension was installed successfully
+ * or is already installed.
+ * @param onError (optional) callback invoked if there was an error installing the extension.
+ */
+ fun install(
+ runtime: WebExtensionRuntime,
+ onSuccess: ((WebExtension) -> Unit) = { },
+ onError: ((Throwable) -> Unit) = { _ -> },
+ ) {
+ val installedExtension = installedExtensions[extensionId]
+ if (installedExtension == null) {
+ runtime.installBuiltInWebExtension(
+ extensionId,
+ extensionUrl,
+ onSuccess = {
+ logger.debug("Installed extension: ${it.id}")
+ synchronized(this@WebExtensionController) {
+ registerContentMessageHandler(it)
+ registerBackgroundMessageHandler(it)
+ installedExtensions[extensionId] = it
+ onSuccess(it)
+ }
+ },
+ onError = { throwable ->
+ logger.error("Failed to install extension: $extensionId", throwable)
+ onError(throwable)
+ },
+ )
+ } else {
+ onSuccess(installedExtension)
+ }
+ }
+
+ /**
+ * Registers a content message handler for the provided session. Currently only one
+ * handler can be registered per session. An existing handler will be replaced and
+ * there is no need to unregister.
+ *
+ * @param engineSession the session the content message handler should be registered with.
+ * @param messageHandler the message handler to register.
+ * @param name (optional) name of the port, if not specified [defaultPort] will be used.
+ */
+ fun registerContentMessageHandler(
+ engineSession: EngineSession,
+ messageHandler: MessageHandler,
+ name: String = defaultPort,
+ ) {
+ synchronized(this) {
+ registerContentMessageHandler = {
+ it.registerContentMessageHandler(engineSession, name, messageHandler)
+ }
+
+ installedExtensions[extensionId]?.let { registerContentMessageHandler(it) }
+ }
+ }
+
+ /**
+ * Registers a background message handler for this extension. An existing handler
+ * will be replaced and there is no need to unregister.
+ *
+ * @param messageHandler the message handler to register.
+ * @param name (optional) name of the port, if not specified [defaultPort] will be used.
+ * */
+ fun registerBackgroundMessageHandler(
+ messageHandler: MessageHandler,
+ name: String = defaultPort,
+ ) {
+ synchronized(this) {
+ registerBackgroundMessageHandler = {
+ it.registerBackgroundMessageHandler(name, messageHandler)
+ }
+
+ installedExtensions[extensionId]?.let { registerBackgroundMessageHandler(it) }
+ }
+ }
+
+ /**
+ * Sends a content message to the provided session.
+ *
+ * @param msg the message to send
+ * @param engineSession the session to send the content message to.
+ * @param name (optional) name of the port, if not specified [defaultPort] will be used.
+ */
+ fun sendContentMessage(msg: JSONObject, engineSession: EngineSession?, name: String = defaultPort) {
+ engineSession?.let { session ->
+ installedExtensions[extensionId]?.let { ext ->
+ val port = ext.getConnectedPort(name, session)
+ port?.postMessage(msg)
+ ?: logger.error("No port with name $name connected for provided session. Message $msg not sent.")
+ }
+ }
+ }
+
+ /**
+ * Sends a background message to the provided extension.
+ *
+ * @param msg the message to send
+ * @param name (optional) name of the port, if not specified [defaultPort] will be used.
+ */
+ fun sendBackgroundMessage(
+ msg: JSONObject,
+ name: String = defaultPort,
+ ) {
+ installedExtensions[extensionId]?.let { ext ->
+ val port = ext.getConnectedPort(name)
+ port?.postMessage(msg)
+ ?: logger.error("No port connected for provided extension. Message $msg not sent.")
+ }
+ }
+
+ /**
+ * Checks whether or not a port is connected for the provided session.
+ *
+ * @param engineSession the session the port should be connected to or null for a port to a background script.
+ * @param name (optional) name of the port, if not specified [defaultPort] will be used.
+ */
+ fun portConnected(engineSession: EngineSession?, name: String = defaultPort): Boolean {
+ return installedExtensions[extensionId]?.let { ext ->
+ ext.getConnectedPort(name, engineSession) != null
+ } ?: false
+ }
+
+ /**
+ * Disconnects the port of the provided session.
+ *
+ * @param engineSession the session the port is connected to or null for a port to a background script.
+ * @param name (optional) name of the port, if not specified [defaultPort] will be used.
+ */
+ fun disconnectPort(engineSession: EngineSession?, name: String = defaultPort) {
+ installedExtensions[extensionId]?.disconnectPort(name, engineSession)
+ }
+
+ companion object {
+ @VisibleForTesting
+ val installedExtensions = ConcurrentHashMap<String, WebExtension>()
+ }
+}
diff --git a/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionPopupObserver.kt b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionPopupObserver.kt
new file mode 100644
index 0000000000..2dfadf856e
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionPopupObserver.kt
@@ -0,0 +1,48 @@
+/* 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.support.webextensions
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.map
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Feature implementation that opens popups for web extensions browser actions.
+ *
+ * @property store the application's [BrowserStore].
+ * @property onOpenPopup a callback invoked when the application should open a
+ * popup. This is a lambda accepting the [WebExtensionState] of the extension
+ * that wants to open a popup.
+ */
+class WebExtensionPopupObserver(
+ private val store: BrowserStore,
+ private val onOpenPopup: (WebExtensionState) -> Unit = { },
+) : LifecycleAwareFeature {
+ private var popupScope: CoroutineScope? = null
+
+ override fun start() {
+ popupScope = store.flowScoped { flow ->
+ flow.distinctUntilChangedBy { it.extensions }
+ .map { it.extensions.filterValues { extension -> extension.popupSession != null } }
+ .distinctUntilChanged()
+ .collect { extensionStates ->
+ if (extensionStates.values.isNotEmpty()) {
+ // We currently limit to one active popup session at a time
+ onOpenPopup(extensionStates.values.first())
+ }
+ }
+ }
+ }
+
+ override fun stop() {
+ popupScope?.cancel()
+ }
+}
diff --git a/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt
new file mode 100644
index 0000000000..b4b78d6272
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt
@@ -0,0 +1,535 @@
+/* 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.support.webextensions
+
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.ExtensionsProcessAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.browser.state.selector.allTabs
+import mozilla.components.browser.state.selector.findTabOrCustomTab
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.engine.webextension.ActionHandler
+import mozilla.components.concept.engine.webextension.TabHandler
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionDelegate
+import mozilla.components.concept.engine.webextension.WebExtensionInstallException
+import mozilla.components.concept.engine.webextension.WebExtensionRuntime
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.isExtensionUrl
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
+import mozilla.components.support.webextensions.facts.emitWebExtensionsInitializedFact
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Function to relay the permission request to the app / user.
+ */
+typealias onUpdatePermissionRequest = (
+ current: WebExtension,
+ updated: WebExtension,
+ newPermissions: List<String>,
+ onPermissionsGranted: ((Boolean) -> Unit),
+) -> Unit
+
+/**
+ * Provides functionality to make sure web extension related events in the
+ * [WebExtensionRuntime] are reflected in the browser state by dispatching the
+ * corresponding actions to the [BrowserStore].
+ *
+ * Note that this class can be removed once the browser-state migration
+ * is completed and the [WebExtensionRuntime] (engine) has direct access to the
+ * [BrowserStore]: https://github.com/orgs/mozilla-mobile/projects/31
+ */
+object WebExtensionSupport {
+ private val logger = Logger("mozac-webextensions")
+ private var onUpdatePermissionRequest: onUpdatePermissionRequest? = null
+ private var onExtensionsLoaded: ((List<WebExtension>) -> Unit)? = null
+ private var onCloseTabOverride: ((WebExtension?, String) -> Unit)? = null
+ private var onSelectTabOverride: ((WebExtension?, String) -> Unit)? = null
+
+ val installedExtensions = ConcurrentHashMap<String, WebExtension>()
+
+ /**
+ * A [Deferred] completed during [initialize] once the state of all
+ * installed extensions is known.
+ */
+ private val initializationResult = CompletableDeferred<Unit>()
+
+ /**
+ * [ActionHandler] for session-specific overrides. Forwards actions to the
+ * the provided [store].
+ */
+ private class SessionActionHandler(
+ private val store: BrowserStore,
+ private val sessionId: String,
+ ) : ActionHandler {
+
+ override fun onBrowserAction(extension: WebExtension, session: EngineSession?, action: Action) {
+ store.dispatch(WebExtensionAction.UpdateTabBrowserAction(sessionId, extension.id, action))
+ }
+
+ override fun onPageAction(extension: WebExtension, session: EngineSession?, action: Action) {
+ store.dispatch(WebExtensionAction.UpdateTabPageAction(sessionId, extension.id, action))
+ }
+ }
+
+ /**
+ * [TabHandler] for session-specific tab events. Forwards actions to the
+ * the provided [store].
+ */
+ private class SessionTabHandler(
+ private val store: BrowserStore,
+ private val sessionId: String,
+ private val onCloseTabOverride: ((WebExtension?, String) -> Unit)? = null,
+ private val onSelectTabOverride: ((WebExtension?, String) -> Unit)? = null,
+ ) : TabHandler {
+
+ override fun onCloseTab(webExtension: WebExtension, engineSession: EngineSession): Boolean {
+ val tab = store.state.findTabOrCustomTab(sessionId)
+ return if (tab != null) {
+ closeTab(tab.id, tab.isCustomTab(), store, onCloseTabOverride, webExtension)
+ true
+ } else {
+ false
+ }
+ }
+
+ override fun onUpdateTab(
+ webExtension: WebExtension,
+ engineSession: EngineSession,
+ active: Boolean,
+ url: String?,
+ ): Boolean {
+ val tab = store.state.findTabOrCustomTab(sessionId)
+ return if (tab != null) {
+ if (active && !tab.isCustomTab()) {
+ onSelectTabOverride?.invoke(webExtension, tab.id)
+ ?: store.dispatch(TabListAction.SelectTabAction(tab.id))
+ }
+ url?.let {
+ engineSession.loadUrl(it)
+ }
+ true
+ } else {
+ false
+ }
+ }
+ }
+
+ /**
+ * Registers a listener for web extension related events on the provided
+ * [WebExtensionRuntime] and reacts by dispatching the corresponding actions to the
+ * provided [BrowserStore].
+ *
+ * @param runtime the browser [WebExtensionRuntime] to use.
+ * @param store the application's [BrowserStore].
+ * @param openPopupInTab (optional) flag to determine whether a browser or page action would
+ * display a web extension popup in a tab or not. Defaults to false.
+ * @param onNewTabOverride (optional) override of behaviour that should
+ * be triggered when web extensions open a new tab e.g. when dispatching
+ * to the store isn't sufficient while migrating from browser-session
+ * to browser-state. This is a lambda accepting the [WebExtension], the
+ * [EngineSession] to use, as well as the URL to load, return the ID of
+ * the created session.
+ * @param onCloseTabOverride (optional) override of behaviour that should
+ * be triggered when web extensions close tabs e.g. when dispatching
+ * to the store isn't sufficient while migrating from browser-session
+ * to browser-state. This is a lambda accepting the [WebExtension] and
+ * the session/tab ID to close.
+ * @param onSelectTabOverride (optional) override of behaviour that should
+ * be triggered when a tab is selected to display a web extension popup.
+ * This is a lambda accepting the [WebExtension] and the session/tab ID to
+ * select.
+ * @param onUpdatePermissionRequest (optional) Invoked when a web extension has changed its
+ * permissions while trying to update to a new version. This requires user interaction as
+ * the updated extension will not be installed, until the user grants the new permissions.
+ * @param onExtensionsLoaded (optional) callback invoked when the extensions are loaded by the
+ * engine. Note that the UI (browser/page actions etc.) may not be initialized at this point.
+ * System add-ons (built-in extensions) will not be passed along.
+ */
+ fun initialize(
+ runtime: WebExtensionRuntime,
+ store: BrowserStore,
+ openPopupInTab: Boolean = false,
+ onNewTabOverride: ((WebExtension?, EngineSession, String) -> String)? = null,
+ onCloseTabOverride: ((WebExtension?, String) -> Unit)? = null,
+ onSelectTabOverride: ((WebExtension?, String) -> Unit)? = null,
+ onUpdatePermissionRequest: onUpdatePermissionRequest? = { _, _, _, _ -> },
+ onExtensionsLoaded: ((List<WebExtension>) -> Unit)? = null,
+ ) {
+ this.onUpdatePermissionRequest = onUpdatePermissionRequest
+ this.onExtensionsLoaded = onExtensionsLoaded
+ this.onCloseTabOverride = onCloseTabOverride
+ this.onSelectTabOverride = onSelectTabOverride
+
+ // Queries the runtime for installed extensions and adds them to the store
+ registerInstalledExtensions(store, runtime)
+
+ // Observes the store and registers action and tab handlers for newly added engine sessions
+ registerHandlersForNewSessions(store)
+
+ runtime.registerWebExtensionDelegate(
+ object : WebExtensionDelegate {
+ override fun onNewTab(extension: WebExtension, engineSession: EngineSession, active: Boolean, url: String) {
+ openTab(store, onNewTabOverride, onSelectTabOverride, extension, engineSession, url, active)
+ }
+
+ override fun onBrowserActionDefined(extension: WebExtension, action: Action) {
+ store.dispatch(WebExtensionAction.UpdateBrowserAction(extension.id, action))
+ }
+
+ override fun onPageActionDefined(extension: WebExtension, action: Action) {
+ store.dispatch(WebExtensionAction.UpdatePageAction(extension.id, action))
+ }
+
+ override fun onToggleActionPopup(
+ extension: WebExtension,
+ engineSession: EngineSession,
+ action: Action,
+ ): EngineSession? {
+ return if (!openPopupInTab) {
+ store.dispatch(WebExtensionAction.UpdatePopupSessionAction(extension.id, null, engineSession))
+ engineSession
+ } else {
+ val popupSessionId = store.state.extensions[extension.id]?.popupSessionId
+ if (popupSessionId != null && store.state.tabs.find { it.id == popupSessionId } != null) {
+ if (popupSessionId == store.state.selectedTabId) {
+ closeTab(popupSessionId, false, store, onCloseTabOverride, extension)
+ } else {
+ onSelectTabOverride?.invoke(extension, popupSessionId)
+ ?: store.dispatch(TabListAction.SelectTabAction(popupSessionId))
+ }
+ null
+ } else {
+ val sessionId = openTab(store, onNewTabOverride, onSelectTabOverride, extension, engineSession)
+ store.dispatch(WebExtensionAction.UpdatePopupSessionAction(extension.id, sessionId))
+ engineSession
+ }
+ }
+ }
+
+ override fun onInstalled(extension: WebExtension) {
+ logger.debug("onInstalled ${extension.id}")
+ // Built-in extensions are not installed by users, they are not aware of them
+ // for this reason we don't show any UI related to built-in extensions. Also,
+ // when the add-on has already been installed, we don't need to show anything
+ // either.
+ val shouldDispatchAction = !installedExtensions.containsKey(extension.id) && !extension.isBuiltIn()
+ registerInstalledExtension(store, extension)
+ if (shouldDispatchAction) {
+ store.dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.AfterInstallation.PostInstallation(extension),
+ ),
+ )
+ }
+ }
+
+ override fun onInstallationFailedRequest(
+ extension: WebExtension?,
+ exception: WebExtensionInstallException,
+ ) {
+ logger.error("onInstallationFailedRequest ${extension?.id}", exception)
+ store.dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.BeforeInstallation.InstallationFailed(
+ extension,
+ exception,
+ ),
+ ),
+ )
+ }
+
+ override fun onUninstalled(extension: WebExtension) {
+ installedExtensions.remove(extension.id)
+ store.dispatch(WebExtensionAction.UninstallWebExtensionAction(extension.id))
+ }
+
+ override fun onEnabled(extension: WebExtension) {
+ installedExtensions[extension.id] = extension
+ store.dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(extension.id, true))
+ }
+
+ override fun onDisabled(extension: WebExtension) {
+ installedExtensions[extension.id] = extension
+ store.dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(extension.id, false))
+ }
+
+ override fun onReady(extension: WebExtension) {
+ installedExtensions[extension.id] = extension
+ }
+
+ override fun onAllowedInPrivateBrowsingChanged(extension: WebExtension) {
+ installedExtensions[extension.id] = extension
+ store.dispatch(
+ WebExtensionAction.UpdateWebExtensionAllowedInPrivateBrowsingAction(
+ extension.id,
+ extension.isAllowedInPrivateBrowsing(),
+ ),
+ )
+ }
+
+ override fun onInstallPermissionRequest(
+ extension: WebExtension,
+ onPermissionsGranted: ((Boolean) -> Unit),
+ ) {
+ store.dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.AfterInstallation.Permissions.Required(
+ extension,
+ onPermissionsGranted,
+ ),
+ ),
+ )
+ }
+
+ override fun onUpdatePermissionRequest(
+ current: WebExtension,
+ updated: WebExtension,
+ newPermissions: List<String>,
+ onPermissionsGranted: ((Boolean) -> Unit),
+ ) {
+ this@WebExtensionSupport.onUpdatePermissionRequest?.invoke(
+ current,
+ updated,
+ newPermissions,
+ onPermissionsGranted,
+ )
+ }
+
+ override fun onOptionalPermissionsRequest(
+ extension: WebExtension,
+ permissions: List<String>,
+ onPermissionsGranted: ((Boolean) -> Unit),
+ ) {
+ store.dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.AfterInstallation.Permissions.Optional(
+ extension,
+ permissions,
+ onPermissionsGranted,
+ ),
+ ),
+ )
+ }
+
+ override fun onExtensionListUpdated() {
+ installedExtensions.clear()
+ store.dispatch(WebExtensionAction.UninstallAllWebExtensionsAction)
+ registerInstalledExtensions(store, runtime)
+ }
+
+ override fun onDisabledExtensionProcessSpawning() {
+ store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
+ }
+ },
+ )
+ }
+
+ /**
+ * Awaits for completion of the initialization process (completes when the
+ * state of all installed extensions is known).
+ */
+ suspend fun awaitInitialization() = initializationResult.await()
+
+ /**
+ * Queries the [WebExtensionRuntime] for installed web extensions and adds them to the [store].
+ */
+ private fun registerInstalledExtensions(store: BrowserStore, runtime: WebExtensionRuntime) {
+ runtime.listInstalledWebExtensions(
+ onSuccess = {
+ extensions ->
+ extensions.forEach { registerInstalledExtension(store, it) }
+ emitWebExtensionsInitializedFact(extensions)
+ closeUnsupportedTabs(store, extensions)
+ initializationResult.complete(Unit)
+ onExtensionsLoaded?.invoke(extensions.filter { !it.isBuiltIn() })
+ },
+ onError = {
+ throwable ->
+ logger.error("Failed to query installed extension", throwable)
+ initializationResult.completeExceptionally(throwable)
+ },
+ )
+ }
+
+ /**
+ * Marks the provided [webExtension] as installed by adding it to the [store].
+ */
+ private fun registerInstalledExtension(store: BrowserStore, webExtension: WebExtension) {
+ installedExtensions[webExtension.id] = webExtension
+ store.dispatch(WebExtensionAction.InstallWebExtensionAction(webExtension.toState()))
+
+ // Register action handler for all existing engine sessions on the new extension,
+ // an issue was filed to get us an API, so we don't have to do this per extension:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1603559
+ store.state.allTabs
+ .forEach { tab ->
+ tab.engineState.engineSession?.let { session ->
+ registerSessionHandlers(webExtension, store, session, tab.id)
+ }
+ }
+ }
+
+ /**
+ * Closes any leftover extensions tabs from extensions that are no longer
+ * installed/registered. When an extension is uninstalled, all extension
+ * pages will be closed. So, in theory, there should never be any
+ * leftover tabs. However, since we support temporary registered
+ * extensions and also recently migrated built-in extensions from the
+ * transient registerWebExtensions to the persistent installBuiltIn, we
+ * should handle this case to make sure we don't have any unloadable tabs
+ * around.
+ */
+ private fun closeUnsupportedTabs(store: BrowserStore, extensions: List<WebExtension>) {
+ val supportedUrls = extensions.mapNotNull { it.getMetadata()?.baseUrl }
+
+ // We only need to do this a single time, once tabs are restored. We need to observe the
+ // store (instead of querying it directly), as tabs can be restored asynchronously on
+ // startup and might not be ready yet.
+ var scope: CoroutineScope? = null
+ scope = store.flowScoped { flow ->
+ flow.map { state -> state.tabs.filter { it.restored }.size }
+ .distinctUntilChanged()
+ .collect { size ->
+ if (size > 0) {
+ store.state.tabs.forEach { tab ->
+ val tabUrl = tab.content.url
+ if (tabUrl.isExtensionUrl() && supportedUrls.none { tabUrl.startsWith(it) }) {
+ closeTab(tab.id, false, store, onCloseTabOverride)
+ }
+ }
+ scope?.cancel()
+ }
+ }
+ }
+ }
+
+ /**
+ * Marks the provided [updatedExtension] as updated in the [store].
+ */
+ fun markExtensionAsUpdated(store: BrowserStore, updatedExtension: WebExtension) {
+ installedExtensions[updatedExtension.id] = updatedExtension
+ store.dispatch(WebExtensionAction.UpdateWebExtensionAction(updatedExtension.toState()))
+
+ // Register action handler for all existing engine sessions on the new extension
+ store.state.allTabs.forEach { tab ->
+ tab.engineState.engineSession?.let { session ->
+ registerSessionHandlers(updatedExtension, store, session, tab.id)
+ }
+ }
+ }
+
+ /**
+ * Observes the provided store to register session-specific [ActionHandler]s
+ * for all installed extensions on newly added sessions.
+ */
+ private fun registerHandlersForNewSessions(store: BrowserStore) {
+ // We need to observe for the entire lifetime of the application,
+ // as web extension support is not tied to any particular view.
+ store.flowScoped { flow ->
+ flow.mapNotNull { state -> state.allTabs }
+ .filterChanged {
+ it.engineState.engineSession
+ }
+ .collect { state ->
+ state.engineState.engineSession?.let { session ->
+ installedExtensions.values.forEach { extension ->
+ registerSessionHandlers(extension, store, session, state.id)
+ }
+ }
+ }
+ }
+ }
+
+ private fun registerSessionHandlers(
+ extension: WebExtension,
+ store: BrowserStore,
+ session: EngineSession,
+ sessionId: String,
+ ) {
+ if (extension.supportActions && !extension.hasActionHandler(session)) {
+ val actionHandler = SessionActionHandler(store, sessionId)
+ extension.registerActionHandler(session, actionHandler)
+ }
+
+ if (!extension.hasTabHandler(session)) {
+ val tabHandler = SessionTabHandler(store, sessionId, onCloseTabOverride, onSelectTabOverride)
+ extension.registerTabHandler(session, tabHandler)
+ }
+ }
+
+ private fun openTab(
+ store: BrowserStore,
+ onNewTabOverride: ((WebExtension?, EngineSession, String) -> String)? = null,
+ onSelectTabOverride: ((WebExtension?, String) -> Unit)? = null,
+ webExtension: WebExtension?,
+ engineSession: EngineSession,
+ url: String = "",
+ selected: Boolean = true,
+ ): String {
+ return if (onNewTabOverride != null) {
+ val sessionId = onNewTabOverride.invoke(webExtension, engineSession, url)
+ if (selected) {
+ onSelectTabOverride?.invoke(webExtension, sessionId)
+ }
+ sessionId
+ } else {
+ val tab = createTab(url)
+ store.dispatch(TabListAction.AddTabAction(tab, selected))
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession))
+ tab.id
+ }
+ }
+
+ private fun closeTab(
+ id: String,
+ customTab: Boolean,
+ store: BrowserStore,
+ onCloseTabOverride: ((WebExtension?, String) -> Unit)? = null,
+ webExtension: WebExtension? = null,
+ ) {
+ if (onCloseTabOverride != null) {
+ onCloseTabOverride.invoke(webExtension, id)
+ } else {
+ val action = if (customTab) {
+ CustomTabListAction.RemoveCustomTabAction(id)
+ } else {
+ TabListAction.RemoveTabAction(id)
+ }
+
+ store.dispatch(action)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun WebExtension.toState() =
+ WebExtensionState(
+ id,
+ url,
+ getMetadata()?.name,
+ isEnabled(),
+ isAllowedInPrivateBrowsing(),
+ )
+
+ private fun SessionState.isCustomTab() = this is CustomTabSessionState
+}
diff --git a/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/facts/WebExtensionFacts.kt b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/facts/WebExtensionFacts.kt
new file mode 100644
index 0000000000..ae15ab85c6
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/facts/WebExtensionFacts.kt
@@ -0,0 +1,51 @@
+/* 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.support.webextensions.facts
+
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to the Addons feature.
+ */
+class WebExtensionFacts {
+ /**
+ * Items that specify which portion of the web extension events were invoked.
+ */
+ object Items {
+ const val WEB_EXTENSIONS_INITIALIZED = "web_extensions_initialized"
+ }
+}
+
+private fun emitWebExtensionFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.SUPPORT_WEBEXTENSIONS,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitWebExtensionsInitializedFact(extensions: List<WebExtension>) {
+ val installedAddons = extensions.filter { !it.isBuiltIn() }
+ val enabledAddons = installedAddons.filter { it.isEnabled() }
+ emitWebExtensionFact(
+ Action.INTERACTION,
+ WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED,
+ metadata = mapOf(
+ "installed" to installedAddons.map { it.id },
+ "enabled" to enabledAddons.map { it.id },
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionControllerTest.kt b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionControllerTest.kt
new file mode 100644
index 0000000000..6851025fdb
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionControllerTest.kt
@@ -0,0 +1,238 @@
+/* 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.support.webextensions
+
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.MessageHandler
+import mozilla.components.concept.engine.webextension.Port
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.json.JSONObject
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class WebExtensionControllerTest {
+ private val extensionId = "test-id"
+ private val defaultPort = "test-messaging-id"
+ private val extensionUrl = "test-url"
+
+ @Before
+ fun setup() {
+ WebExtensionController.installedExtensions.clear()
+ }
+
+ @Test
+ fun `install webextension - installs and invokes success and error callbacks`() {
+ val engine: Engine = mock()
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+
+ var onSuccessInvoked = false
+ var onErrorInvoked = false
+ controller.install(engine, onSuccess = { onSuccessInvoked = true }, onError = { onErrorInvoked = true })
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(extensionId),
+ eq(extensionUrl),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+ assertFalse(WebExtensionController.installedExtensions.containsKey(extensionId))
+
+ onSuccess.value.invoke(mock())
+ assertTrue(onSuccessInvoked)
+ assertFalse(onErrorInvoked)
+ assertTrue(WebExtensionController.installedExtensions.containsKey(extensionId))
+
+ controller.install(engine)
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(extensionId),
+ eq(extensionUrl),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+
+ onError.value.invoke(mock())
+ assertTrue(onErrorInvoked)
+ }
+
+ @Test
+ fun `install webextension - invokes success callback if extension already installed`() {
+ val engine: Engine = mock()
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+
+ var onSuccessInvoked = false
+ var onErrorInvoked = false
+ WebExtensionController.installedExtensions[extensionId] = mock()
+ controller.install(engine, onSuccess = { onSuccessInvoked = true }, onError = { onErrorInvoked = true })
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, never()).installBuiltInWebExtension(
+ eq(extensionId),
+ eq(extensionUrl),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+ assertTrue(onSuccessInvoked)
+ assertFalse(onErrorInvoked)
+ }
+
+ @Test
+ fun `register content message handler if extension installed`() {
+ val extension: WebExtension = mock()
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+ WebExtensionController.installedExtensions[extensionId] = extension
+
+ val session: EngineSession = mock()
+ val messageHandler: MessageHandler = mock()
+ controller.registerContentMessageHandler(session, messageHandler)
+ verify(extension).registerContentMessageHandler(session, defaultPort, messageHandler)
+ }
+
+ @Test
+ fun `register content message handler before extension is installed`() {
+ val engine: Engine = mock()
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+ controller.install(engine)
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(extensionId),
+ eq(extensionUrl),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+
+ val session: EngineSession = mock()
+ val messageHandler: MessageHandler = mock()
+ controller.registerContentMessageHandler(session, messageHandler)
+
+ val extension: WebExtension = mock()
+ onSuccess.value.invoke(extension)
+ verify(extension).registerContentMessageHandler(session, defaultPort, messageHandler)
+ }
+
+ @Test
+ fun `send content message`() {
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+
+ val message: JSONObject = mock()
+ val extension: WebExtension = mock()
+ val session: EngineSession = mock()
+ val port: Port = mock()
+ whenever(extension.getConnectedPort(defaultPort, session)).thenReturn(port)
+
+ controller.sendContentMessage(message, null)
+ verify(port, never()).postMessage(message)
+
+ controller.sendContentMessage(message, session)
+ verify(port, never()).postMessage(message)
+
+ WebExtensionController.installedExtensions[extensionId] = extension
+
+ controller.sendContentMessage(message, session)
+ verify(port, times(1)).postMessage(message)
+ }
+
+ @Test
+ fun `register background message handler if extension installed`() {
+ val extension: WebExtension = mock()
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+ WebExtensionController.installedExtensions[extensionId] = extension
+
+ val messageHandler: MessageHandler = mock()
+ controller.registerBackgroundMessageHandler(messageHandler)
+ verify(extension).registerBackgroundMessageHandler(defaultPort, messageHandler)
+ }
+
+ @Test
+ fun `register background message handler before extension is installed`() {
+ val engine: Engine = mock()
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+ controller.install(engine)
+
+ val onSuccess = argumentCaptor<((WebExtension) -> Unit)>()
+ val onError = argumentCaptor<((Throwable) -> Unit)>()
+ verify(engine, times(1)).installBuiltInWebExtension(
+ eq(extensionId),
+ eq(extensionUrl),
+ onSuccess.capture(),
+ onError.capture(),
+ )
+
+ val messageHandler: MessageHandler = mock()
+ controller.registerBackgroundMessageHandler(messageHandler)
+
+ val extension: WebExtension = mock()
+ onSuccess.value.invoke(extension)
+ verify(extension).registerBackgroundMessageHandler(defaultPort, messageHandler)
+ }
+
+ @Test
+ fun `send background message`() {
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+
+ val message: JSONObject = mock()
+ val extension: WebExtension = mock()
+ val port: Port = mock()
+ whenever(extension.getConnectedPort(defaultPort)).thenReturn(port)
+
+ controller.sendBackgroundMessage(message)
+ verify(port, never()).postMessage(message)
+
+ WebExtensionController.installedExtensions[extensionId] = extension
+
+ controller.sendBackgroundMessage(message)
+ verify(port, times(1)).postMessage(message)
+ }
+
+ @Test
+ fun `check if port connected`() {
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+
+ val extension: WebExtension = mock()
+ val session: EngineSession = mock()
+ whenever(extension.getConnectedPort(defaultPort, session)).thenReturn(mock())
+
+ assertFalse(controller.portConnected(null))
+ assertFalse(controller.portConnected(mock()))
+ assertFalse(controller.portConnected(session))
+
+ WebExtensionController.installedExtensions[extensionId] = extension
+
+ assertTrue(controller.portConnected(session))
+ assertFalse(controller.portConnected(session, "invalid"))
+ }
+
+ @Test
+ fun `disconnect port`() {
+ val extension: WebExtension = mock()
+ val controller = WebExtensionController(extensionId, extensionUrl, defaultPort)
+
+ controller.disconnectPort(null)
+ verify(extension, never()).disconnectPort(eq(defaultPort), any())
+
+ val session: EngineSession = mock()
+ controller.disconnectPort(session)
+ verify(extension, never()).disconnectPort(eq(defaultPort), eq(session))
+
+ WebExtensionController.installedExtensions[extensionId] = extension
+ controller.disconnectPort(session)
+ verify(extension, times(1)).disconnectPort(eq(defaultPort), eq(session))
+ }
+}
diff --git a/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionPopupObserverTest.kt b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionPopupObserverTest.kt
new file mode 100644
index 0000000000..4a367fe3e6
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionPopupObserverTest.kt
@@ -0,0 +1,55 @@
+/* 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.support.webextensions
+
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+
+class WebExtensionPopupObserverTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `observes and forwards request to open popup`() {
+ val extensionId = "ext1"
+ val engineSession: EngineSession = mock()
+ val store = BrowserStore(
+ BrowserState(
+ extensions = mapOf(extensionId to WebExtensionState(extensionId)),
+ ),
+ )
+
+ var extensionOpeningPopup: WebExtensionState? = null
+ val observer = WebExtensionPopupObserver(store) {
+ extensionOpeningPopup = it
+ }
+
+ observer.start()
+ assertNull(extensionOpeningPopup)
+
+ store.dispatch(WebExtensionAction.UpdatePopupSessionAction(extensionId, popupSession = engineSession)).joinBlocking()
+ assertNotNull(extensionOpeningPopup)
+ assertEquals(extensionId, extensionOpeningPopup!!.id)
+ assertEquals(engineSession, extensionOpeningPopup!!.popupSession)
+
+ // Verify that stopped feature does not observe and forward requests to open popup
+ extensionOpeningPopup = null
+ observer.stop()
+ store.dispatch(WebExtensionAction.UpdatePopupSessionAction(extensionId, popupSession = mock())).joinBlocking()
+ assertNull(extensionOpeningPopup)
+ }
+}
diff --git a/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt
new file mode 100644
index 0000000000..b4e45b3f55
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt
@@ -0,0 +1,1077 @@
+/* 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.support.webextensions
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.webextension.Action
+import mozilla.components.concept.engine.webextension.ActionHandler
+import mozilla.components.concept.engine.webextension.Incognito
+import mozilla.components.concept.engine.webextension.Metadata
+import mozilla.components.concept.engine.webextension.TabHandler
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.WebExtensionDelegate
+import mozilla.components.concept.engine.webextension.WebExtensionInstallException
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+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 mozilla.components.support.test.whenever
+import mozilla.components.support.webextensions.WebExtensionSupport.toState
+import mozilla.components.support.webextensions.facts.WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import mozilla.components.support.base.facts.Action as FactsAction
+
+class WebExtensionSupportTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @After
+ fun tearDown() {
+ WebExtensionSupport.installedExtensions.clear()
+ }
+
+ @Test
+ fun `sets web extension delegate on engine`() {
+ val engine: Engine = mock()
+ val store = BrowserStore()
+
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(any())
+ }
+
+ @Test
+ fun `queries engine for installed extensions and adds state to the store`() {
+ val store = spy(BrowserStore())
+
+ val ext1: WebExtension = mock()
+ val ext1Meta: Metadata = mock()
+ whenever(ext1Meta.name).thenReturn("ext1")
+ val ext2: WebExtension = mock()
+ whenever(ext1.id).thenReturn("1")
+ whenever(ext1.url).thenReturn("url1")
+ whenever(ext1.getMetadata()).thenReturn(ext1Meta)
+ whenever(ext1.isEnabled()).thenReturn(true)
+ whenever(ext1.isAllowedInPrivateBrowsing()).thenReturn(true)
+
+ whenever(ext2.id).thenReturn("2")
+ whenever(ext2.url).thenReturn("url2")
+ whenever(ext2.isEnabled()).thenReturn(false)
+ whenever(ext2.isAllowedInPrivateBrowsing()).thenReturn(false)
+
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(listOf(ext1, ext2))
+ }
+
+ CollectionProcessor.withFactCollection { facts ->
+ WebExtensionSupport.initialize(engine, store)
+
+ val interactionFact = facts[0]
+ assertEquals(FactsAction.INTERACTION, interactionFact.action)
+ assertEquals(Component.SUPPORT_WEBEXTENSIONS, interactionFact.component)
+ assertEquals(WEB_EXTENSIONS_INITIALIZED, interactionFact.item)
+ assertEquals(2, interactionFact.metadata?.size)
+ assertTrue(interactionFact.metadata?.containsKey("installed")!!)
+ assertTrue(interactionFact.metadata?.containsKey("enabled")!!)
+ assertEquals(listOf(ext1.id, ext2.id), interactionFact.metadata?.get("installed"))
+ assertEquals(listOf(ext1.id), interactionFact.metadata?.get("enabled"))
+ }
+ assertEquals(ext1, WebExtensionSupport.installedExtensions[ext1.id])
+ assertEquals(ext2, WebExtensionSupport.installedExtensions[ext2.id])
+
+ val actionCaptor = argumentCaptor<WebExtensionAction.InstallWebExtensionAction>()
+ verify(store, times(2)).dispatch(actionCaptor.capture())
+ assertEquals(
+ WebExtensionState(ext1.id, ext1.url, "ext1", true, true),
+ actionCaptor.allValues[0].extension,
+ )
+ assertEquals(
+ WebExtensionState(ext2.id, ext2.url, null, false, false),
+ actionCaptor.allValues[1].extension,
+ )
+ }
+
+ @Test
+ fun `reacts to new tab being opened by adding tab to store`() {
+ val store = spy(BrowserStore())
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ val engineSession: EngineSession = mock()
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org")
+ val actionCaptor = argumentCaptor<mozilla.components.browser.state.action.BrowserAction>()
+ verify(store, times(2)).dispatch(actionCaptor.capture())
+ assertEquals(
+ "https://mozilla.org",
+ (actionCaptor.allValues.first() as TabListAction.AddTabAction).tab.content.url,
+ )
+ assertEquals(
+ engineSession,
+ (actionCaptor.allValues.last() as EngineAction.LinkEngineSessionAction).engineSession,
+ )
+ }
+
+ @Test
+ fun `allows overriding onNewTab behaviour`() {
+ val store = BrowserStore()
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ val engineSession: EngineSession = mock()
+ var onNewTabCalled = false
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(
+ engine,
+ store,
+ onNewTabOverride = { _, _, _ ->
+ onNewTabCalled = true
+ "123"
+ },
+ )
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org")
+ assertTrue(onNewTabCalled)
+ }
+
+ @Test
+ fun `reacts to tab being closed by removing tab from store`() {
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ whenever(ext.isEnabled()).thenReturn(true)
+ whenever(ext.hasTabHandler(any())).thenReturn(false, true)
+ val engineSession: EngineSession = mock()
+ val tabId = "testTabId"
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = tabId, url = "https://www.mozilla.org", engineSession = engineSession),
+ ),
+ ),
+ ),
+ )
+ val installedList = mutableListOf(ext)
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(installedList)
+ }
+
+ val tabHandlerCaptor = argumentCaptor<TabHandler>()
+ WebExtensionSupport.initialize(engine, store)
+
+ store.waitUntilIdle()
+ verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
+ tabHandlerCaptor.value.onCloseTab(ext, engineSession)
+ verify(store).dispatch(TabListAction.RemoveTabAction(tabId))
+ }
+
+ @Test
+ fun `reacts to custom tab being closed by removing tab from store`() {
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ whenever(ext.isEnabled()).thenReturn(true)
+ whenever(ext.hasTabHandler(any())).thenReturn(false, true)
+ val engineSession: EngineSession = mock()
+ val tabId = "testTabId"
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ customTabs = listOf(
+ createCustomTab(id = tabId, url = "https://www.mozilla.org", engineSession = engineSession, source = SessionState.Source.Internal.CustomTab),
+ ),
+ ),
+ ),
+ )
+ val installedList = mutableListOf(ext)
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(installedList)
+ }
+
+ val tabHandlerCaptor = argumentCaptor<TabHandler>()
+ WebExtensionSupport.initialize(engine, store)
+
+ store.waitUntilIdle()
+ verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
+ tabHandlerCaptor.value.onCloseTab(ext, engineSession)
+ verify(store).dispatch(CustomTabListAction.RemoveCustomTabAction(tabId))
+ }
+
+ @Test
+ fun `allows overriding onCloseTab behaviour`() {
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ whenever(ext.isEnabled()).thenReturn(true)
+ whenever(ext.hasTabHandler(any())).thenReturn(false, true)
+ val engineSession: EngineSession = mock()
+ var onCloseTabCalled = false
+ val tabId = "testTabId"
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = tabId, url = "https://www.mozilla.org", engineSession = engineSession),
+ ),
+ ),
+ ),
+ )
+
+ val installedList = mutableListOf(ext)
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(installedList)
+ }
+
+ val tabHandlerCaptor = argumentCaptor<TabHandler>()
+ WebExtensionSupport.initialize(
+ engine,
+ store,
+ onSelectTabOverride = { _, _ -> },
+ onCloseTabOverride = { _, _ -> onCloseTabCalled = true },
+ )
+
+ store.waitUntilIdle()
+ verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
+ tabHandlerCaptor.value.onCloseTab(ext, engineSession)
+ assertTrue(onCloseTabCalled)
+ }
+
+ @Test
+ fun `reacts to tab being updated`() {
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ whenever(ext.isEnabled()).thenReturn(true)
+ whenever(ext.hasTabHandler(any())).thenReturn(false, true)
+ val engineSession: EngineSession = mock()
+ val tabId = "testTabId"
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = tabId, url = "https://www.mozilla.org", engineSession = engineSession),
+ ),
+ ),
+ )
+
+ val installedList = mutableListOf(ext)
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(installedList)
+ }
+
+ val tabHandlerCaptor = argumentCaptor<TabHandler>()
+ WebExtensionSupport.initialize(engine, store)
+
+ // Update tab to select it
+ verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
+ assertNull(store.state.selectedTabId)
+ assertTrue(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, null))
+ store.waitUntilIdle()
+ assertEquals("testTabId", store.state.selectedTabId)
+
+ // Update URL of tab
+ assertTrue(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, false, "url"))
+ verify(engineSession).loadUrl("url")
+
+ // Update non-existing tab
+ store.dispatch(TabListAction.RemoveTabAction(tabId)).joinBlocking()
+ assertFalse(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, "url"))
+ }
+
+ @Test
+ fun `reacts to custom tab being updated`() {
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ whenever(ext.isEnabled()).thenReturn(true)
+ whenever(ext.hasTabHandler(any())).thenReturn(false, true)
+ val engineSession: EngineSession = mock()
+ val tabId = "testTabId"
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(
+ createCustomTab(id = tabId, url = "https://www.mozilla.org", engineSession = engineSession, source = SessionState.Source.Internal.CustomTab),
+ ),
+ ),
+ )
+
+ val installedList = mutableListOf(ext)
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(installedList)
+ }
+
+ val tabHandlerCaptor = argumentCaptor<TabHandler>()
+ WebExtensionSupport.initialize(engine, store)
+
+ // Update tab to select it
+ verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
+ assertNull(store.state.selectedTabId)
+ assertTrue(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, null))
+ store.waitUntilIdle()
+
+ // Update URL of tab
+ assertTrue(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, false, "url"))
+ verify(engineSession).loadUrl("url")
+
+ // Update non-existing tab
+ store.dispatch(CustomTabListAction.RemoveCustomTabAction(tabId)).joinBlocking()
+ assertFalse(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, "url"))
+ }
+
+ @Test
+ fun `reacts to new extension being installed`() {
+ val engineSession: EngineSession = mock()
+ val tab =
+ createTab(id = "1", url = "https://www.mozilla.org", engineSession = engineSession)
+
+ val customTabEngineSession: EngineSession = mock()
+ val customTab =
+ createCustomTab(id = "2", url = "https://www.mozilla.org", engineSession = customTabEngineSession, source = SessionState.Source.Internal.CustomTab)
+
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ customTabs = listOf(customTab),
+ ),
+ ),
+ )
+
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("extensionId")
+ whenever(ext.url).thenReturn("url")
+ whenever(ext.supportActions).thenReturn(true)
+ whenever(ext.isBuiltIn()).thenReturn(false)
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ // Verify that we dispatch to the store and mark the extension as installed
+ delegateCaptor.value.onInstalled(ext)
+ verify(store).dispatch(
+ WebExtensionAction.InstallWebExtensionAction(
+ WebExtensionState(ext.id, ext.url, ext.getMetadata()?.name, ext.isEnabled()),
+ ),
+ )
+ verify(store).dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.AfterInstallation.PostInstallation(ext),
+ ),
+ )
+ assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
+
+ // Verify that we register action and tab handlers for all existing sessions on the extension
+ val actionHandlerCaptor = argumentCaptor<ActionHandler>()
+ val webExtensionActionCaptor = argumentCaptor<WebExtensionAction>()
+ val tabHandlerCaptor = argumentCaptor<TabHandler>()
+ val selectTabActionCaptor = argumentCaptor<TabListAction.SelectTabAction>()
+ verify(ext).registerActionHandler(eq(customTabEngineSession), actionHandlerCaptor.capture())
+ verify(ext).registerTabHandler(eq(customTabEngineSession), tabHandlerCaptor.capture())
+ verify(ext).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture())
+ verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
+
+ // Verify we only register the handlers once
+ whenever(ext.hasActionHandler(engineSession)).thenReturn(true)
+ whenever(ext.hasTabHandler(engineSession)).thenReturn(true)
+
+ actionHandlerCaptor.value.onBrowserAction(ext, engineSession, mock())
+ verify(store, times(3)).dispatch(webExtensionActionCaptor.capture())
+ assertEquals(ext.id, (webExtensionActionCaptor.allValues.last() as WebExtensionAction.UpdateTabBrowserAction).extensionId)
+
+ store.dispatch(ContentAction.UpdateUrlAction(sessionId = "1", url = "https://www.firefox.com")).joinBlocking()
+ verify(ext, times(1)).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture())
+ verify(ext, times(1)).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
+
+ reset(store)
+
+ tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, null)
+ verify(store).dispatch(selectTabActionCaptor.capture())
+ assertEquals("1", selectTabActionCaptor.value.tabId)
+ tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, "url")
+ verify(engineSession).loadUrl("url")
+ }
+
+ @Test
+ fun `reacts to install permission request`() {
+ val store = spy(BrowserStore())
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ val onPermissionsGranted: ((Boolean) -> Unit) = mock()
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ // Verify they we confirm the permission request
+ delegateCaptor.value.onInstallPermissionRequest(ext, onPermissionsGranted)
+
+ verify(store).dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.AfterInstallation.Permissions.Required(ext, onPermissionsGranted),
+ ),
+ )
+ }
+
+ @Test
+ fun `reacts to extension being uninstalled`() {
+ val store = spy(BrowserStore())
+
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("extensionId")
+ whenever(ext.url).thenReturn("url")
+ whenever(ext.supportActions).thenReturn(true)
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onInstalled(ext)
+ verify(store).dispatch(
+ WebExtensionAction.InstallWebExtensionAction(
+ WebExtensionState(ext.id, ext.url, ext.getMetadata()?.name, ext.isEnabled()),
+ ),
+ )
+ assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
+
+ // Verify that we dispatch to the store and mark the extension as uninstalled
+ delegateCaptor.value.onUninstalled(ext)
+ verify(store).dispatch(WebExtensionAction.UninstallWebExtensionAction(ext.id))
+ assertNull(WebExtensionSupport.installedExtensions[ext.id])
+ }
+
+ @Test
+ fun `GIVEN BuiltIn extension WHEN calling onInstalled THEN do not show the PostInstallation prompt`() {
+ val store = spy(BrowserStore())
+
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("extensionId")
+ whenever(ext.url).thenReturn("url")
+ whenever(ext.supportActions).thenReturn(true)
+ whenever(ext.isBuiltIn()).thenReturn(true)
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onInstalled(ext)
+ verify(store, times(0)).dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.AfterInstallation.PostInstallation(ext),
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN already installed extension WHEN calling onInstalled THEN do not show the PostInstallation prompt`() {
+ val store = spy(BrowserStore())
+
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("extensionId")
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ // We simulate a first install...
+ delegateCaptor.value.onInstalled(ext)
+ // ... and then an update, which also calls `onInstalled()`.
+ delegateCaptor.value.onInstalled(ext)
+
+ verify(store, times(1)).dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.AfterInstallation.PostInstallation(ext),
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN extension WHEN calling onInstallationFailedRequest THEN show the installation prompt error`() {
+ val store = spy(BrowserStore())
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ val exception = WebExtensionInstallException.Blocklisted(throwable = Exception())
+
+ whenever(ext.id).thenReturn("extensionId")
+ whenever(ext.url).thenReturn("url")
+ whenever(ext.supportActions).thenReturn(true)
+ whenever(ext.isBuiltIn()).thenReturn(false)
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onInstallationFailedRequest(ext, exception)
+
+ verify(store, times(1)).dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.BeforeInstallation.InstallationFailed(ext, exception),
+ ),
+ )
+ }
+
+ @Test
+ fun `reacts to extension being enabled`() {
+ val store = spy(BrowserStore())
+
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("extensionId")
+ whenever(ext.url).thenReturn("url")
+ whenever(ext.supportActions).thenReturn(true)
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onEnabled(ext)
+ verify(store).dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(ext.id, true))
+ assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
+ }
+
+ @Test
+ fun `reacts to extension being disabled`() {
+ val store = spy(BrowserStore())
+
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("extensionId")
+ whenever(ext.url).thenReturn("url")
+ whenever(ext.supportActions).thenReturn(true)
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onDisabled(ext)
+ verify(store).dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(ext.id, false))
+ assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
+ }
+
+ @Test
+ fun `observes store and registers handlers on new engine sessions`() {
+ val tab = createTab(id = "1", url = "https://www.mozilla.org")
+ val customTab = createCustomTab(id = "2", url = "https://www.mozilla.org", source = SessionState.Source.Internal.CustomTab)
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ customTabs = listOf(customTab),
+ ),
+ ),
+ )
+
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("extensionId")
+ whenever(ext.url).thenReturn("url")
+ whenever(ext.supportActions).thenReturn(true)
+
+ // Install extension
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+ delegateCaptor.value.onInstalled(ext)
+
+ // Verify that action/tab handler is registered when a new engine session is created
+ val actionHandlerCaptor = argumentCaptor<ActionHandler>()
+ val tabHandlerCaptor = argumentCaptor<TabHandler>()
+ verify(ext, never()).registerActionHandler(any(), any())
+ verify(ext, never()).registerTabHandler(
+ session = any(),
+ tabHandler = any(),
+ )
+
+ val engineSession1: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession1)).joinBlocking()
+ verify(ext).registerActionHandler(eq(engineSession1), actionHandlerCaptor.capture())
+ verify(ext).registerTabHandler(eq(engineSession1), tabHandlerCaptor.capture())
+
+ val engineSession2: EngineSession = mock()
+ store.dispatch(EngineAction.LinkEngineSessionAction(customTab.id, engineSession2)).joinBlocking()
+ verify(ext).registerActionHandler(eq(engineSession2), actionHandlerCaptor.capture())
+ verify(ext).registerTabHandler(eq(engineSession2), tabHandlerCaptor.capture())
+ }
+
+ @Test
+ fun `reacts to browser action being defined by dispatching to the store`() {
+ val store = spy(BrowserStore())
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ val browserAction: Action = mock()
+ whenever(ext.id).thenReturn("test")
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onBrowserActionDefined(ext, browserAction)
+ val actionCaptor = argumentCaptor<WebExtensionAction.UpdateBrowserAction>()
+ verify(store).dispatch(actionCaptor.capture())
+ assertEquals("test", actionCaptor.value.extensionId)
+ assertEquals(browserAction, actionCaptor.value.browserAction)
+ }
+
+ @Test
+ fun `reacts to page action being defined by dispatching to the store`() {
+ val store = spy(BrowserStore())
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ val pageAction: Action = mock()
+ whenever(ext.id).thenReturn("test")
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onPageActionDefined(ext, pageAction)
+ val actionCaptor = argumentCaptor<WebExtensionAction.UpdatePageAction>()
+ verify(store).dispatch(actionCaptor.capture())
+ assertEquals("test", actionCaptor.value.extensionId)
+ assertEquals(pageAction, actionCaptor.value.pageAction)
+ }
+
+ @Test
+ fun `reacts to action popup being toggled by opening tab as needed`() {
+ val engine: Engine = mock()
+
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+
+ val engineSession: EngineSession = mock()
+ val browserAction: Action = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ extensions = mapOf(ext.id to WebExtensionState(ext.id)),
+ ),
+ ),
+ )
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store, openPopupInTab = true)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ // Toggling should open tab
+ delegateCaptor.value.onToggleActionPopup(ext, engineSession, browserAction)
+ var actionCaptor = argumentCaptor<mozilla.components.browser.state.action.BrowserAction>()
+ verify(store, times(3)).dispatch(actionCaptor.capture())
+ var values = actionCaptor.allValues
+ assertEquals("", (values[0] as TabListAction.AddTabAction).tab.content.url)
+ assertEquals(engineSession, (values[1] as EngineAction.LinkEngineSessionAction).engineSession)
+ assertEquals("test", (values[2] as WebExtensionAction.UpdatePopupSessionAction).extensionId)
+ val popupSessionId = (values[2] as WebExtensionAction.UpdatePopupSessionAction).popupSessionId
+ assertNotNull(popupSessionId)
+ }
+
+ @Test
+ fun `reacts to action popup being toggled by selecting tab as needed`() {
+ val engine: Engine = mock()
+
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+
+ val engineSession: EngineSession = mock()
+ val browserAction: Action = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab(id = "popupTab", url = "https://www.mozilla.org")),
+ extensions = mapOf(ext.id to WebExtensionState(ext.id, popupSessionId = "popupTab")),
+ ),
+ ),
+ )
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store, openPopupInTab = true)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ // Toggling again should select popup tab
+ var actionCaptor = argumentCaptor<mozilla.components.browser.state.action.BrowserAction>()
+ delegateCaptor.value.onToggleActionPopup(ext, engineSession, browserAction)
+
+ store.waitUntilIdle()
+ verify(store, times(1)).dispatch(actionCaptor.capture())
+ assertEquals("popupTab", (actionCaptor.value as TabListAction.SelectTabAction).tabId)
+ }
+
+ @Test
+ fun `reacts to action popup being toggled by closing tab as needed`() {
+ val engine: Engine = mock()
+
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+
+ val engineSession: EngineSession = mock()
+ val browserAction: Action = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(createTab(id = "popupTab", url = "https://www.mozilla.org")),
+ selectedTabId = "popupTab",
+ extensions = mapOf(ext.id to WebExtensionState(ext.id, popupSessionId = "popupTab")),
+ ),
+ ),
+ )
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store, openPopupInTab = true)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ // Toggling again should close tab
+ var actionCaptor = argumentCaptor<mozilla.components.browser.state.action.BrowserAction>()
+ delegateCaptor.value.onToggleActionPopup(ext, engineSession, browserAction)
+ store.waitUntilIdle()
+
+ verify(store).dispatch(actionCaptor.capture())
+ assertEquals("popupTab", (actionCaptor.value as TabListAction.RemoveTabAction).tabId)
+ }
+
+ @Test
+ fun `reacts to action popup being toggled by opening a popup`() {
+ val engine: Engine = mock()
+
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+
+ val engineSession: EngineSession = mock()
+ val browserAction: Action = mock()
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ extensions = mapOf(ext.id to WebExtensionState(ext.id)),
+ ),
+ ),
+ )
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ // Toggling should allow state to have popup EngineSession instance
+ delegateCaptor.value.onToggleActionPopup(ext, engineSession, browserAction)
+ val actionCaptor = argumentCaptor<mozilla.components.browser.state.action.BrowserAction>()
+ verify(store).dispatch(actionCaptor.capture())
+
+ val value = actionCaptor.value
+ assertNotNull((value as WebExtensionAction.UpdatePopupSessionAction).popupSession)
+ }
+
+ @Test
+ fun `invokes onUpdatePermissionRequest callback`() {
+ var executed = false
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ extensions = mapOf(ext.id to WebExtensionState(ext.id)),
+ ),
+ ),
+ )
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(
+ runtime = engine,
+ store = store,
+ onUpdatePermissionRequest = { _, _, _, _ ->
+ executed = true
+ },
+ )
+
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+ delegateCaptor.value.onUpdatePermissionRequest(mock(), mock(), mock(), mock())
+ assertTrue(executed)
+ }
+
+ @Test
+ fun `invokes onExtensionsLoaded callback`() {
+ var executed = false
+ val engine: Engine = mock()
+
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ whenever(ext.isBuiltIn()).thenReturn(false)
+
+ val builtInExt: WebExtension = mock()
+ whenever(builtInExt.id).thenReturn("test2")
+ whenever(builtInExt.isBuiltIn()).thenReturn(true)
+
+ val store = spy(BrowserStore(BrowserState(extensions = mapOf(ext.id to WebExtensionState(ext.id)))))
+
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(listOf(ext, builtInExt))
+ }
+
+ val onExtensionsLoaded: ((List<WebExtension>) -> Unit) = {
+ assertEquals(1, it.size)
+ assertEquals(ext, it[0])
+ executed = true
+ }
+ WebExtensionSupport.initialize(runtime = engine, store = store, onExtensionsLoaded = onExtensionsLoaded)
+ assertTrue(executed)
+ }
+
+ @Test
+ fun `reacts to extension list being updated in the engine`() {
+ val store = spy(BrowserStore())
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ whenever(ext.isEnabled()).thenReturn(true)
+ val installedList = mutableListOf(ext)
+
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(installedList)
+ }
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ assertEquals(1, WebExtensionSupport.installedExtensions.size)
+ assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onExtensionListUpdated()
+ store.waitUntilIdle()
+
+ val actionCaptor = argumentCaptor<WebExtensionAction>()
+ verify(store, times(3)).dispatch(actionCaptor.capture())
+ assertEquals(3, actionCaptor.allValues.size)
+ // Initial install
+ assertTrue(actionCaptor.allValues[0] is WebExtensionAction.InstallWebExtensionAction)
+ assertEquals(WebExtensionState(ext.id), (actionCaptor.allValues[0] as WebExtensionAction.InstallWebExtensionAction).extension)
+
+ // Uninstall all
+ assertTrue(actionCaptor.allValues[1] is WebExtensionAction.UninstallAllWebExtensionsAction)
+
+ // Reinstall
+ assertTrue(actionCaptor.allValues[2] is WebExtensionAction.InstallWebExtensionAction)
+ assertEquals(WebExtensionState(ext.id), (actionCaptor.allValues[2] as WebExtensionAction.InstallWebExtensionAction).extension)
+ assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
+
+ // Verify installed extensions are cleared
+ installedList.clear()
+ delegateCaptor.value.onExtensionListUpdated()
+ store.waitUntilIdle()
+ assertTrue(WebExtensionSupport.installedExtensions.isEmpty())
+ }
+
+ @Test
+ fun `reacts to WebExtensionDelegate onReady by updating the extension details stored in the installedExtensions map`() {
+ val store = spy(BrowserStore())
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("test")
+ whenever(ext.isEnabled()).thenReturn(true)
+ val extMeta: Metadata = mock()
+ whenever(extMeta.incognito).thenReturn(Incognito.SPANNING)
+ whenever(ext.getMetadata()).thenReturn(extMeta)
+ val installedList = mutableListOf(ext)
+
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(installedList)
+ }
+
+ // Initialize WebExtensionSupport and expect the extension metadata
+ // to be the one coming from the first mock WebExtension instance.
+ WebExtensionSupport.initialize(engine, store)
+ assertEquals(1, WebExtensionSupport.installedExtensions.size)
+ assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
+ assertEquals(extMeta, WebExtensionSupport.installedExtensions[ext.id]?.getMetadata())
+
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ // Mock a call to the WebExtensionDelegate.onReady delegate and the
+ // extension and its metadata instances stored in the installExtensions
+ // map to have been updated as a side-effect of that.
+ val extOnceReady: WebExtension = mock()
+ whenever(extOnceReady.id).thenReturn("test")
+ whenever(extOnceReady.isEnabled()).thenReturn(true)
+ val extOnceReadyMeta: Metadata = mock()
+ whenever(extOnceReady.getMetadata()).thenReturn(extOnceReadyMeta)
+
+ delegateCaptor.value.onReady(extOnceReady)
+
+ assertEquals(1, WebExtensionSupport.installedExtensions.size)
+ assertEquals(extOnceReady, WebExtensionSupport.installedExtensions[ext.id])
+ assertEquals(extOnceReadyMeta, WebExtensionSupport.installedExtensions[ext.id]?.getMetadata())
+
+ store.waitUntilIdle()
+ }
+
+ @Test
+ fun `reacts to extensions process spawning disabled`() {
+ val store = BrowserStore()
+ val engine: Engine = mock()
+
+ assertFalse(store.state.showExtensionsProcessDisabledPrompt)
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onDisabledExtensionProcessSpawning()
+ store.waitUntilIdle()
+
+ assertTrue(store.state.showExtensionsProcessDisabledPrompt)
+ }
+
+ @Test
+ fun `closes tabs from unsupported extensions`() {
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(id = "1", url = "https://www.mozilla.org", restored = true),
+ createTab(id = "2", url = "moz-extension://1234-5678/test", restored = true),
+ createTab(id = "3", url = "moz-extension://1234-5678-9/", restored = true),
+ ),
+ ),
+ )
+
+ val ext1: WebExtension = mock()
+ val ext1Meta: Metadata = mock()
+ whenever(ext1Meta.baseUrl).thenReturn("moz-extension://1234-5678/")
+ whenever(ext1.id).thenReturn("1")
+ whenever(ext1.url).thenReturn("url1")
+ whenever(ext1.getMetadata()).thenReturn(ext1Meta)
+ whenever(ext1.isEnabled()).thenReturn(true)
+ whenever(ext1.isAllowedInPrivateBrowsing()).thenReturn(true)
+
+ val ext2: WebExtension = mock()
+ whenever(ext2.id).thenReturn("2")
+ whenever(ext2.url).thenReturn("url2")
+ whenever(ext2.isEnabled()).thenReturn(true)
+ whenever(ext2.isAllowedInPrivateBrowsing()).thenReturn(false)
+
+ val engine: Engine = mock()
+ val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
+ whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
+ callbackCaptor.value.invoke(listOf(ext1, ext2))
+ }
+
+ WebExtensionSupport.initialize(engine, store)
+
+ store.waitUntilIdle()
+ assertNotNull(store.state.findTab("1"))
+ assertNotNull(store.state.findTab("2"))
+ assertNull(store.state.findTab("3"))
+
+ // Make sure we're running a single cleanup and stop the scope after
+ store.dispatch(TabListAction.AddTabAction(createTab(id = "4", url = "moz-extension://1234-5678-90/")))
+ .joinBlocking()
+
+ store.waitUntilIdle()
+ assertNotNull(store.state.findTab("4"))
+ }
+
+ @Test
+ fun `marks extensions as updated`() {
+ val engineSession: EngineSession = mock()
+ val tab =
+ createTab(id = "1", url = "https://www.mozilla.org", engineSession = engineSession)
+
+ val customTabEngineSession: EngineSession = mock()
+ val customTab =
+ createCustomTab(id = "2", url = "https://www.mozilla.org", engineSession = customTabEngineSession, source = SessionState.Source.Internal.CustomTab)
+
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ customTabs = listOf(customTab),
+ ),
+ ),
+ )
+
+ val ext: WebExtension = mock()
+ whenever(ext.id).thenReturn("extensionId")
+ whenever(ext.url).thenReturn("url")
+ whenever(ext.supportActions).thenReturn(true)
+
+ WebExtensionSupport.markExtensionAsUpdated(store, ext)
+ assertSame(ext, WebExtensionSupport.installedExtensions[ext.id])
+ verify(store).dispatch(WebExtensionAction.UpdateWebExtensionAction(ext.toState()))
+
+ // Verify that we register new action and tab handlers for the updated extension
+ val actionHandlerCaptor = argumentCaptor<ActionHandler>()
+ val tabHandlerCaptor = argumentCaptor<TabHandler>()
+ verify(ext).registerActionHandler(eq(customTabEngineSession), actionHandlerCaptor.capture())
+ verify(ext).registerTabHandler(eq(customTabEngineSession), tabHandlerCaptor.capture())
+ verify(ext).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture())
+ verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
+ }
+
+ @Test
+ fun `reacts to optional permissions request`() {
+ val store = spy(BrowserStore())
+ val engine: Engine = mock()
+ val ext: WebExtension = mock()
+ val permissions = listOf("perm1", "perm2")
+ val onPermissionsGranted: ((Boolean) -> Unit) = mock()
+ val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
+ WebExtensionSupport.initialize(engine, store)
+ verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
+
+ delegateCaptor.value.onOptionalPermissionsRequest(ext, permissions, onPermissionsGranted)
+ verify(store).dispatch(
+ WebExtensionAction.UpdatePromptRequestWebExtensionAction(
+ WebExtensionPromptRequest.AfterInstallation.Permissions.Optional(ext, permissions, onPermissionsGranted),
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/webextensions/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/support/webextensions/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/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/support/webextensions/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/webextensions/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/webextensions/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/tooling/detekt/README.md b/mobile/android/android-components/components/tooling/detekt/README.md
new file mode 100644
index 0000000000..64c68f4679
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/detekt/README.md
@@ -0,0 +1,29 @@
+# [Android Components](../../../README.md) > Tooling > Detekt
+
+Custom Detekt rules for the components repository.
+
+These additional detekt rules are run as part of the _Android Components_ build pipeline.
+Published for internal usage only.
+
+## Usage
+
+Add into `build.gradle`:
+```
+dependencies {
+ // ...
+
+ detektPlugins "org.mozilla.components:tooling-detekt:$android_components_version"
+}
+```
+
+## Rules
+
+Section `mozilla-rules`:
+
+ - `AbsentOrWrongFileLicense` - check for correct license header in Kotlin files.
+
+## 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/tooling/detekt/build.gradle b/mobile/android/android-components/components/tooling/detekt/build.gradle
new file mode 100644
index 0000000000..ad0c3eb038
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/detekt/build.gradle
@@ -0,0 +1,28 @@
+/* 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/. */
+
+apply plugin: 'java-library'
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation ComponentsDependencies.tools_detekt_api
+
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.tools_detekt_api
+ testImplementation ComponentsDependencies.tools_detekt_test
+}
+
+tasks.register("lintRelease") {
+ doLast {
+ // Do nothing. We execute the same set of tasks for all our modules in parallel on taskcluster.
+ // This project doesn't have a lint task.
+ }
+}
+
+tasks.register("assembleAndroidTest") {
+ doLast {
+ // Do nothing. Like the `lint` task above this is just a dummy task so that this module
+ // behaves like our others and we do not need to special case it in automation.
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/detekt/src/main/kotlin/mozilla/components/tooling/detekt/MozillaRuleSetProvider.kt b/mobile/android/android-components/components/tooling/detekt/src/main/kotlin/mozilla/components/tooling/detekt/MozillaRuleSetProvider.kt
new file mode 100644
index 0000000000..582726b30c
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/detekt/src/main/kotlin/mozilla/components/tooling/detekt/MozillaRuleSetProvider.kt
@@ -0,0 +1,24 @@
+/* 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.tooling.detekt
+
+import io.gitlab.arturbosch.detekt.api.Config
+import io.gitlab.arturbosch.detekt.api.RuleSet
+import io.gitlab.arturbosch.detekt.api.RuleSetProvider
+
+/**
+ * Set of custom mozilla rules to be loaded in detekt utility.
+ */
+class MozillaRuleSetProvider : RuleSetProvider {
+
+ override val ruleSetId = "mozilla-rules"
+
+ override fun instance(config: Config) = RuleSet(
+ ruleSetId,
+ listOf(
+ ProjectLicenseRule(config),
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/tooling/detekt/src/main/kotlin/mozilla/components/tooling/detekt/ProjectLicenseRule.kt b/mobile/android/android-components/components/tooling/detekt/src/main/kotlin/mozilla/components/tooling/detekt/ProjectLicenseRule.kt
new file mode 100644
index 0000000000..2e8f03a587
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/detekt/src/main/kotlin/mozilla/components/tooling/detekt/ProjectLicenseRule.kt
@@ -0,0 +1,52 @@
+/* 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.tooling.detekt
+
+import io.gitlab.arturbosch.detekt.api.CodeSmell
+import io.gitlab.arturbosch.detekt.api.Config
+import io.gitlab.arturbosch.detekt.api.Debt
+import io.gitlab.arturbosch.detekt.api.Entity
+import io.gitlab.arturbosch.detekt.api.Issue
+import io.gitlab.arturbosch.detekt.api.Rule
+import io.gitlab.arturbosch.detekt.api.Severity
+import org.jetbrains.kotlin.psi.KtFile
+
+/**
+ * Check header license in Kotlin files.
+ */
+class ProjectLicenseRule(config: Config = Config.empty) : Rule(config) {
+
+ private val expectedLicense = """
+ |/* 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/. */
+ """.trimMargin()
+
+ override val issue = Issue(
+ id = "AbsentOrWrongFileLicense",
+ severity = Severity.Style,
+ description = "License text is absent or incorrect in the file.",
+ debt = Debt.FIVE_MINS,
+ )
+
+ override fun visitKtFile(file: KtFile) {
+ if (!file.hasValidLicense) {
+ reportCodeSmell(file)
+ }
+ }
+
+ private val KtFile.hasValidLicense: Boolean
+ get() = text.startsWith(expectedLicense)
+
+ private fun reportCodeSmell(file: KtFile) {
+ report(
+ CodeSmell(
+ issue,
+ Entity.from(file),
+ "Expected license not found or incorrect in the file: ${file.name}.",
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider b/mobile/android/android-components/components/tooling/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
new file mode 100644
index 0000000000..30cc744210
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
@@ -0,0 +1 @@
+mozilla.components.tooling.detekt.MozillaRuleSetProvider
diff --git a/mobile/android/android-components/components/tooling/detekt/src/test/kotlin/ProjectLicenseRuleTest.kt b/mobile/android/android-components/components/tooling/detekt/src/test/kotlin/ProjectLicenseRuleTest.kt
new file mode 100644
index 0000000000..ff67988482
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/detekt/src/test/kotlin/ProjectLicenseRuleTest.kt
@@ -0,0 +1,63 @@
+/* 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.tooling.detekt
+
+import io.gitlab.arturbosch.detekt.test.lint
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ProjectLicenseRuleTest {
+
+ @Test
+ fun testAbsentLicense() {
+ val findings = ProjectLicenseRule().lint(fileContent)
+
+ assertEquals(1, findings.size)
+ assertEquals(
+ "Expected license not found or incorrect in the file: Test.kt.",
+ findings.first().message,
+ )
+ }
+
+ @Test
+ fun testInvalidLicense() {
+ val file = """
+ |/* This Source Code Form is subject to the terms of the Mozilla Public License.
+ | * You can obtain one at http://mozilla.org/MPL/2.0/. */
+ |
+ $fileContent
+ """.trimMargin()
+ val findings = ProjectLicenseRule().lint(file)
+
+ assertEquals(1, findings.size)
+ assertEquals(
+ "Expected license not found or incorrect in the file: Test.kt.",
+ findings.first().message,
+ )
+ }
+
+ @Test
+ fun testValidLicense() {
+ val file = """
+ |/* 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/. */
+ |
+ $fileContent
+ """.trimMargin()
+ val findings = ProjectLicenseRule().lint(file)
+
+ assertEquals(0, findings.size)
+ }
+}
+
+private val fileContent = """
+ |package my.package
+ |
+ |/** My awesome class */
+ |class MyClass () {
+ | fun foo () {}
+ |}
+""".trimMargin()
diff --git a/mobile/android/android-components/components/tooling/fetch-tests/README.md b/mobile/android/android-components/components/tooling/fetch-tests/README.md
new file mode 100644
index 0000000000..367cc7b79b
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/fetch-tests/README.md
@@ -0,0 +1,11 @@
+# [Android Components](../../../README.md) > Tooling > Fetch tests
+
+A generic test suite for components that implement [concept-fetch](../../concept/fetch/README.md).
+
+All implementations of [concept-fetch](../../concept/fetch/README.md) are expected to pass this test suite. A shared test suite guarantees that the HTTP clients are "pluggable" and implementations behave similar enough so that every component can work with every HTTP client.
+
+## 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/tooling/fetch-tests/build.gradle b/mobile/android/android-components/components/tooling/fetch-tests/build.gradle
new file mode 100644
index 0000000000..b5c24f0f2d
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/fetch-tests/build.gradle
@@ -0,0 +1,38 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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')
+ }
+ }
+
+ lint {
+ lintConfig file("lint.xml")
+ }
+
+ namespace 'mozilla.components.tooling.fetch.tests'
+
+}
+
+dependencies {
+ implementation project(':concept-fetch')
+
+ implementation ComponentsDependencies.testing_mockwebserver
+ implementation ComponentsDependencies.testing_junit
+ implementation ComponentsDependencies.kotlin_coroutines
+}
diff --git a/mobile/android/android-components/components/tooling/fetch-tests/lint.xml b/mobile/android/android-components/components/tooling/fetch-tests/lint.xml
new file mode 100644
index 0000000000..2e10996a44
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/fetch-tests/lint.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+<lint>
+ <issue id="InvalidPackage">
+ <ignore path="**/bcprov-*on-*.jar"/>
+ <ignore path="**/junit-*.jar"/>
+ </issue>
+</lint>
diff --git a/mobile/android/android-components/components/tooling/fetch-tests/src/main/AndroidManifest.xml b/mobile/android/android-components/components/tooling/fetch-tests/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/fetch-tests/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/tooling/fetch-tests/src/main/java/mozilla/components/tooling/fetch/tests/FetchTestCases.kt b/mobile/android/android-components/components/tooling/fetch-tests/src/main/java/mozilla/components/tooling/fetch/tests/FetchTestCases.kt
new file mode 100644
index 0000000000..3752b12c23
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/fetch-tests/src/main/java/mozilla/components/tooling/fetch/tests/FetchTestCases.kt
@@ -0,0 +1,546 @@
+/* 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.tooling.fetch.tests
+
+import android.annotation.SuppressLint
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.isSuccess
+import okhttp3.Headers
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.SocketPolicy
+import okio.Buffer
+import okio.GzipSink
+import okio.buffer
+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.Assert.fail
+import org.junit.Test
+import java.io.File
+import java.io.IOException
+import java.lang.Exception
+import java.net.SocketTimeoutException
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+
+/**
+ * Generic test cases for concept-fetch implementations.
+ *
+ * We expect any implementation of concept-fetch to pass all test cases here.
+ */
+@Suppress("IllegalIdentifier", "FunctionName", "unused")
+abstract class FetchTestCases {
+ /**
+ * Creates a new [Client] for running a specific test case with it.
+ */
+ abstract fun createNewClient(): Client
+
+ /**
+ * Creates a new [MockWebServer] to accept test requests.
+ */
+ open fun createWebServer(): MockWebServer = MockWebServer()
+
+ @Test
+ open fun get200WithStringBody() = withServerResponding(
+ MockResponse()
+ .setBody("Hello World"),
+ ) { client ->
+ val response = client.fetch(Request(rootUrl()))
+
+ assertEquals(200, response.status)
+ assertEquals("Hello World", response.body.string())
+ }
+
+ @Test
+ open fun get404WithBody() {
+ withServerResponding(
+ MockResponse()
+ .setResponseCode(404)
+ .setBody("Error"),
+ ) { client ->
+ val response = client.fetch(Request(rootUrl()))
+
+ assertEquals(404, response.status)
+ assertEquals("Error", response.body.string())
+ }
+ }
+
+ @Test
+ open fun get200WithHeaders() {
+ withServerResponding(
+ MockResponse(),
+ ) { client ->
+ val response = client.fetch(
+ Request(
+ url = rootUrl(),
+ headers = MutableHeaders()
+ .set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
+ .set("Accept-Encoding", "gzip, deflate")
+ .set("Accept-Language", "en-US,en;q=0.5")
+ .set("Connection", "keep-alive")
+ .set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0"),
+ ),
+ ).also { it.close() }
+ assertEquals(200, response.status)
+
+ val request = takeRequest()
+
+ assertTrue(request.headers.size >= 5)
+
+ val names = request.headers.names()
+ assertTrue(names.contains("Accept"))
+ assertTrue(names.contains("Accept-Encoding"))
+ assertTrue(names.contains("Accept-Language"))
+ assertTrue(names.contains("Connection"))
+ assertTrue(names.contains("User-Agent"))
+
+ assertEquals(
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ request.headers.get("Accept"),
+ )
+
+ assertEquals(
+ "gzip, deflate",
+ request.headers.get("Accept-Encoding"),
+ )
+
+ assertEquals(
+ "en-US,en;q=0.5",
+ request.headers.get("Accept-Language"),
+ )
+
+ assertEquals(
+ "keep-alive",
+ request.headers.get("Connection"),
+ )
+
+ assertEquals(
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0",
+ request.headers.get("User-Agent"),
+ )
+ }
+ }
+
+ @Test
+ open fun post200WithBody() {
+ withServerResponding(
+ MockResponse(),
+ ) { client ->
+ val response = client.fetch(
+ Request(
+ url = rootUrl(),
+ method = Request.Method.POST,
+ body = Request.Body.fromString("Hello World"),
+ ),
+ ).also { it.close() }
+ assertEquals(200, response.status)
+
+ val request = takeRequest()
+
+ assertEquals("POST", request.method)
+ assertEquals("Hello World", request.body.readUtf8())
+ }
+ }
+
+ @Test
+ open fun get200WithGzippedBody() {
+ withServerResponding(
+ MockResponse()
+ .setBody(gzip("This is compressed"))
+ .addHeader("Content-Encoding: gzip"),
+ ) { client ->
+ val response = client.fetch(Request(rootUrl()))
+ assertEquals(200, response.status)
+
+ assertEquals("This is compressed", response.body.string())
+ }
+ }
+
+ @Test
+ open fun get302FollowRedirects() {
+ withServerResponding(
+ MockResponse().setResponseCode(302)
+ .addHeader("Location", "/x"),
+ MockResponse().setBody("Hello World!"),
+ ) { client ->
+ val response = client.fetch(
+ Request(
+ url = rootUrl(),
+ redirect = Request.Redirect.FOLLOW,
+ ),
+ )
+ assertEquals(200, response.status)
+
+ assertEquals("Hello World!", response.body.string())
+ }
+ }
+
+ @Test
+ open fun get302FollowRedirectsDisabled() {
+ withServerResponding(
+ MockResponse().setResponseCode(302)
+ .addHeader("Location", "/x"),
+ MockResponse().setBody("Hello World!"),
+ ) { client ->
+ val response = client.fetch(
+ Request(
+ url = rootUrl(),
+ redirect = Request.Redirect.MANUAL,
+ cookiePolicy = Request.CookiePolicy.OMIT,
+ ),
+ ).also { it.close() }
+ assertEquals(302, response.status)
+ }
+ }
+
+ @SuppressLint("FetchResponseClose") // intentional failure
+ @Test
+ open fun get200WithReadTimeout() {
+ withServerResponding(
+ MockResponse()
+ .setBody("Yep!")
+ .setSocketPolicy(SocketPolicy.NO_RESPONSE),
+ ) { client ->
+ try {
+ val response = client.fetch(
+ Request(url = rootUrl(), readTimeout = Pair(1, TimeUnit.SECONDS)),
+ )
+
+ // We're doing this the old-fashioned way instead of using the
+ // expected= attribute, because the test is launched on a different
+ // thread (using a different coroutine context) than this block.
+ fail("Expected read timeout (SocketTimeoutException), but got response: ${response.status}")
+ } catch (e: SocketTimeoutException) {
+ // expected
+ } catch (e: Exception) {
+ fail("Expected SocketTimeoutException")
+ }
+ }
+ }
+
+ @Test
+ open fun put201FileUpload() {
+ val file = File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString())
+ file.writer().use { it.write("I am an image file!") }
+
+ withServerResponding(
+ MockResponse()
+ .setResponseCode(201)
+ .setHeader("Location", "/your-image.png")
+ .setBody("Thank you!"),
+ ) { client ->
+ val response = client.fetch(
+ Request(
+ url = rootUrl(),
+ method = Request.Method.PUT,
+ headers = MutableHeaders(
+ "Content-Type" to "image/png",
+ ),
+ body = Request.Body.fromFile(file),
+ ),
+ )
+
+ // Verify response
+
+ assertTrue(response.isSuccess)
+ assertEquals(201, response.status)
+
+ assertEquals("Thank you!", response.body.string())
+
+ assertTrue(response.headers.contains("Location"))
+
+ assertEquals("/your-image.png", response.headers.get("Location"))
+
+ // Verify request received by server
+
+ val request = takeRequest()
+
+ assertEquals("PUT", request.method)
+
+ assertEquals("image/png", request.getHeader("Content-Type"))
+
+ assertEquals("I am an image file!", request.body.readUtf8())
+ }
+ }
+
+ @Test
+ open fun get200WithDuplicatedCacheControlResponseHeaders() {
+ withServerResponding(
+ MockResponse()
+ .addHeader("Cache-Control", "no-cache")
+ .addHeader("Cache-Control", "no-store")
+ .setBody("I am the content"),
+ ) { client ->
+ val response = client.fetch(Request(rootUrl()))
+
+ response.headers.forEach { (name, value) -> println("$name = $value") }
+
+ assertEquals(200, response.status)
+ assertEquals(3, response.headers.size)
+
+ assertEquals("Cache-Control", response.headers[0].name)
+ assertEquals("Cache-Control", response.headers[1].name)
+ assertEquals("Content-Length", response.headers[2].name)
+
+ assertEquals("no-cache", response.headers[0].value)
+ assertEquals("no-store", response.headers[1].value)
+ assertEquals("16", response.headers[2].value)
+
+ assertEquals("no-store", response.headers.get("Cache-Control"))
+ assertEquals("16", response.headers.get("Content-Length"))
+
+ response.close()
+ }
+ }
+
+ @Test
+ open fun get200WithDuplicatedCacheControlRequestHeaders() {
+ withServerResponding(
+ MockResponse(),
+ ) { client ->
+ val response = client.fetch(
+ Request(
+ url = rootUrl(),
+ headers = MutableHeaders(
+ "Cache-Control" to "no-cache",
+ "Cache-Control" to "no-store",
+ ),
+ ),
+ ).also { it.close() }
+
+ assertEquals(200, response.status)
+
+ val request = takeRequest()
+
+ var cacheHeaders = request.headers.values("Cache-Control")
+
+ assertFalse(cacheHeaders.isEmpty())
+
+ // If multiple headers with the same name are present we accept
+ // implementations that *request* a comma-separated list of values
+ // as well as those adding additional (duplicated) headers.
+ // Technically, comma-separate values are correct:
+ // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
+ // Web servers will understand both representations and for responses
+ // we already unify headers across implementations. So, this should
+ // be transparent to users.
+ if (cacheHeaders[0].contains(",")) {
+ cacheHeaders = cacheHeaders[0].split(",")
+ }
+
+ assertEquals(2, cacheHeaders.size)
+ assertEquals("no-cache", cacheHeaders[0].trim())
+ assertEquals("no-store", cacheHeaders[1].trim())
+ }
+ }
+
+ @Test
+ open fun get200OverridingDefaultHeaders() {
+ withServerResponding(
+ MockResponse(),
+ ) { client ->
+ val response = client.fetch(
+ Request(
+ url = rootUrl(),
+ headers = MutableHeaders(
+ "Accept" to "text/html",
+ "Accept-Encoding" to "deflate",
+ "User-Agent" to "SuperBrowser/1.0",
+ "Connection" to "close",
+ ),
+ ),
+ ).also { it.close() }
+
+ assertEquals(200, response.status)
+
+ val request = takeRequest()
+
+ for (i in 0 until request.headers.size) {
+ println(" Header: " + request.headers.name(i) + " = " + request.headers.value(i))
+ }
+
+ val acceptHeaders = request.headers.values("Accept")
+ assertEquals(1, acceptHeaders.size)
+ assertEquals("text/html", acceptHeaders[0])
+ }
+ }
+
+ @Test
+ open fun get200WithCookiePolicy() = withServerResponding(
+ MockResponse().addHeader("Set-Cookie", "name=value"),
+ MockResponse(),
+ MockResponse(),
+ ) { client ->
+
+ val responseWithCookies = client.fetch(Request(rootUrl())).also { it.close() }
+ assertEquals(200, responseWithCookies.status)
+ assertEquals("name=value", responseWithCookies.headers["Set-Cookie"])
+ assertNull(takeRequest().getHeader("Cookie"))
+
+ // Send additional request, using CookiePolicy.INCLUDE, which should
+ // include the cookie set by the previous response.
+ val response1 = client.fetch(
+ Request(url = rootUrl(), cookiePolicy = Request.CookiePolicy.INCLUDE),
+ ).also { it.close() }
+
+ assertEquals(200, response1.status)
+ assertEquals("name=value", takeRequest().getHeader("Cookie"))
+
+ // Send additional request, using CookiePolicy.OMIT, which should
+ // NOT include the cookie.
+ val response2 = client.fetch(
+ Request(url = rootUrl(), cookiePolicy = Request.CookiePolicy.OMIT),
+ ).also { it.close() }
+
+ assertEquals(200, response2.status)
+ assertNull(takeRequest().getHeader("Cookie"))
+ }
+
+ @Test
+ open fun get200WithContentTypeCharset() = withServerResponding(
+ MockResponse()
+ .addHeader("Content-Type", "text/html; charset=ISO-8859-1")
+ .setBody(Buffer().writeString("ÄäÖöÜü", Charsets.ISO_8859_1)),
+ MockResponse()
+ .addHeader("Content-Type", "text/html; charset=invalid")
+ .setBody("Hello World"),
+ ) { client ->
+
+ val response = client.fetch(Request(rootUrl()))
+
+ assertEquals(200, response.status)
+ assertEquals("ÄäÖöÜü", response.body.string())
+
+ val response2 = client.fetch(Request(rootUrl()))
+
+ assertEquals(200, response2.status)
+ assertEquals("Hello World", response2.body.string())
+ }
+
+ @Test
+ open fun get200WithCacheControl() = withServerResponding(
+ MockResponse()
+ .addHeader("Cache-Control", "max-age=600")
+ .setBody("Cache this!"),
+ MockResponse().setBody("Could've cached this!"),
+ ) { client ->
+
+ val responseWithCacheControl = client.fetch(Request(rootUrl()))
+ assertEquals(200, responseWithCacheControl.status)
+ assertEquals("Cache this!", responseWithCacheControl.body.string())
+ assertNotNull(responseWithCacheControl.headers["Cache-Control"])
+
+ // Request should hit cache.
+ val response1 = client.fetch(Request(rootUrl()))
+ assertEquals(200, response1.status)
+ assertEquals("Cache this!", response1.body.string())
+
+ // Request should hit network.
+ val response2 = client.fetch(Request(rootUrl(), useCaches = false))
+ assertEquals(200, response2.status)
+ assertEquals("Could've cached this!", response2.body.string())
+ }
+
+ @SuppressLint("FetchResponseClose") // intentional failure
+ @Test
+ open fun getThrowsIOExceptionWhenHostNotReachable() {
+ try {
+ val client = createNewClient()
+ val response = client.fetch(Request(url = "http://invalid.offline"))
+
+ // We're doing this the old-fashioned way instead of using the
+ // expected= attribute, because the test is launched on a different
+ // thread (using a different coroutine context) than this block.
+ fail("Expected IOException, but got response: ${response.status}")
+ } catch (e: IOException) {
+ // expected
+ } catch (e: Exception) {
+ fail("Expected IOException")
+ }
+ }
+
+ @Test
+ open fun getDataUri() {
+ val client = createNewClient()
+ val response = client.fetch(Request(url = "data:text/plain;charset=utf-8;base64,SGVsbG8sIFdvcmxkIQ=="))
+ assertEquals("13", response.headers["Content-Length"])
+ assertEquals("text/plain;charset=utf-8", response.headers["Content-Type"])
+ assertEquals("Hello, World!", response.body.string())
+
+ val responseNoCharset = client.fetch(Request(url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="))
+ assertEquals("13", responseNoCharset.headers["Content-Length"])
+ assertEquals("text/plain", responseNoCharset.headers["Content-Type"])
+ assertEquals("Hello, World!", responseNoCharset.body.string())
+
+ val responseNoContentType = client.fetch(Request(url = "data:;base64,SGVsbG8sIFdvcmxkIQ=="))
+ assertEquals("13", responseNoContentType.headers["Content-Length"])
+ assertNull(responseNoContentType.headers["Content-Type"])
+ assertEquals("Hello, World!", responseNoContentType.body.string())
+
+ val responseNoBase64 = client.fetch(Request(url = "data:text/plain;charset=utf-8,Hello%2C%20World%21"))
+ assertEquals("13", responseNoBase64.headers["Content-Length"])
+ assertEquals("text/plain;charset=utf-8", responseNoBase64.headers["Content-Type"])
+ assertEquals("Hello, World!", responseNoBase64.body.string())
+ }
+
+ private inline fun withServerResponding(
+ vararg responses: MockResponse,
+ crossinline block: MockWebServer.(Client) -> Unit,
+ ) {
+ val server = createWebServer()
+
+ responses.forEach {
+ server.enqueue(it)
+ }
+
+ try {
+ val client = createNewClient()
+ // Subclasses (implementation specific tests) might be instrumented
+ // and run on a device so we need to avoid network requests on the
+ // main thread.
+ runBlocking(Dispatchers.IO) {
+ server.start()
+ server.block(client)
+ }
+ } finally {
+ try { server.shutdown() } catch (e: IOException) {}
+ }
+ }
+
+ private fun MockWebServer.rootUrl() = url("/").toString()
+}
+
+@Throws(IOException::class)
+private fun gzip(data: String): Buffer {
+ val result = Buffer()
+ val sink = GzipSink(result).buffer()
+ sink.writeUtf8(data)
+ sink.close()
+ return result
+}
+
+private fun Headers.filtered(): Headers {
+ val builder = newBuilder()
+ ignoredHeaders.forEach { header ->
+ builder.removeAll(header)
+ }
+ return builder.build()
+}
+
+// The following headers are getting ignored when verifying headers sent by a Client implementation
+private val ignoredHeaders = listOf(
+ // GeckoView"s GeckoWebExecutor sends additional "Sec-Fetch-*" headers. Instead of
+ // adding those headers to all our implementations, we are just ignoring them in tests.
+ "Sec-Fetch-Dest",
+ "Sec-Fetch-Mode",
+ "Sec-Fetch-Site",
+)
diff --git a/mobile/android/android-components/components/tooling/lint/README.md b/mobile/android/android-components/components/tooling/lint/README.md
new file mode 100644
index 0000000000..55a7f7fe91
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/README.md
@@ -0,0 +1,11 @@
+# [Android Components](../../../README.md) > Tooling > Lint
+
+Custom Lint rules for the components repository.
+
+These additional lint rules are run as part of the _Android Components_ build pipeline. Currently we do not publish packaged versions of these lint rules for consumption outside of the _Android Components_ repository.
+
+## 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/tooling/lint/build.gradle b/mobile/android/android-components/components/tooling/lint/build.gradle
new file mode 100644
index 0000000000..adee887580
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/build.gradle
@@ -0,0 +1,42 @@
+/* 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: 'java-library'
+apply plugin: 'kotlin'
+
+dependencies {
+ compileOnly ComponentsDependencies.tools_lintapi
+ compileOnly ComponentsDependencies.tools_lintchecks
+
+ compileOnly ComponentsDependencies.kotlin_reflect
+ testImplementation ComponentsDependencies.kotlin_reflect
+
+ testImplementation ComponentsDependencies.tools_lint
+ testImplementation ComponentsDependencies.tools_linttests
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_mockito
+}
+
+jar {
+ manifest {
+ attributes('Lint-Registry-v2': 'mozilla.components.tooling.lint.LintIssueRegistry')
+ }
+}
+
+tasks.register("lint") {
+ doLast {
+ // Do nothing. We execute the same set of tasks for all our modules in parallel on taskcluster.
+ // This project doesn't have a lint task. To avoid special casing our automation I just added
+ // an empty lint task here.
+ }
+}
+
+tasks.register("assembleAndroidTest") {
+ doLast {
+ // Do nothing. Like the `lint` task above this is just a dummy task so that this module
+ // behaves like our others and we do not need to special case it in automation.
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/AndroidSrcXmlDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/AndroidSrcXmlDetector.kt
new file mode 100644
index 0000000000..5799871f4a
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/AndroidSrcXmlDetector.kt
@@ -0,0 +1,82 @@
+/* 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.tooling.lint
+
+import com.android.SdkConstants.ATTR_SRC
+import com.android.SdkConstants.FQCN_IMAGE_BUTTON
+import com.android.SdkConstants.FQCN_IMAGE_VIEW
+import com.android.SdkConstants.IMAGE_BUTTON
+import com.android.SdkConstants.IMAGE_VIEW
+import com.android.resources.ResourceFolderType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.ResourceXmlDetector
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.XmlContext
+import com.google.common.annotations.VisibleForTesting
+import org.w3c.dom.Element
+
+/**
+ * A custom lint check that prohibits not using the app:srcCompat for ImageViews
+ */
+class AndroidSrcXmlDetector : ResourceXmlDetector() {
+ companion object {
+ const val SCHEMA = "http://schemas.android.com/apk/res/android"
+ const val FULLY_QUALIFIED_APP_COMPAT_IMAGE_BUTTON =
+ "androidx.appcompat.widget.AppCompatImageButton"
+ const val FULLY_QUALIFIED_APP_COMPAT_VIEW_CLASS =
+ "androidx.appcompat.widget.AppCompatImageView"
+ const val APP_COMPAT_IMAGE_BUTTON = "AppCompatImageButton"
+ const val APP_COMPAT_IMAGE_VIEW = "AppCompatImageView"
+
+ const val ERROR_MESSAGE = "Using android:src to define resource instead of app:srcCompat"
+
+ @VisibleForTesting
+ val ISSUE_XML_SRC_USAGE = Issue.create(
+ id = "AndroidSrcXmlDetector",
+ briefDescription = "Prohibits using android:src in ImageViews and ImageButtons",
+ explanation = "ImageView (and descendants) images should be declared using app:srcCompat",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ AndroidSrcXmlDetector::class.java,
+ Scope.RESOURCE_FILE_SCOPE,
+ ),
+ )
+ }
+
+ override fun appliesTo(folderType: ResourceFolderType): Boolean {
+ // Return true if we want to analyze resource files in the specified resource
+ // folder type. In this case we only need to analyze layout resource files.
+ return folderType == ResourceFolderType.LAYOUT
+ }
+
+ override fun getApplicableElements(): Collection<String>? {
+ return setOf(
+ FQCN_IMAGE_VIEW,
+ IMAGE_VIEW,
+ FQCN_IMAGE_BUTTON,
+ IMAGE_BUTTON,
+ FULLY_QUALIFIED_APP_COMPAT_IMAGE_BUTTON,
+ FULLY_QUALIFIED_APP_COMPAT_VIEW_CLASS,
+ APP_COMPAT_IMAGE_BUTTON,
+ APP_COMPAT_IMAGE_VIEW,
+ )
+ }
+
+ override fun visitElement(context: XmlContext, element: Element) {
+ if (!element.hasAttributeNS(SCHEMA, ATTR_SRC)) return
+ val node = element.getAttributeNodeNS(SCHEMA, ATTR_SRC)
+
+ context.report(
+ issue = ISSUE_XML_SRC_USAGE,
+ scope = node,
+ location = context.getLocation(node),
+ message = ERROR_MESSAGE,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ConceptFetchDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ConceptFetchDetector.kt
new file mode 100644
index 0000000000..0a31e61620
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ConceptFetchDetector.kt
@@ -0,0 +1,249 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.checks.CheckResultDetector
+import com.android.tools.lint.checks.DataFlowAnalyzer
+import com.android.tools.lint.checks.EscapeCheckingDataFlowAnalyzer
+import com.android.tools.lint.checks.TargetMethodDataFlowAnalyzer
+import com.android.tools.lint.checks.isMissingTarget
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.getUMethod
+import com.android.tools.lint.detector.api.isJava
+import com.android.tools.lint.detector.api.skipLabeledExpression
+import com.intellij.psi.LambdaUtil
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiResourceVariable
+import com.intellij.psi.PsiVariable
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UCallableReferenceExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * Checks for missing [mozilla.components.concept.fetch.Response.close] call on fetches that might not have used the
+ * resources.
+ *
+ * Review the unit tests for examples on what this [Detector] can identify.
+ */
+class ConceptFetchDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> {
+ return listOf("fetch")
+ }
+
+ override fun visitMethodCall(
+ context: JavaContext,
+ node: UCallExpression,
+ method: PsiMethod,
+ ) {
+ val containingClass = method.containingClass ?: return
+ val evaluator = context.evaluator
+
+ if (evaluator.extendsClass(
+ containingClass,
+ CLIENT_CLS,
+ false,
+ )
+ ) {
+ val returnType = method.getUMethod()?.returnTypeReference ?: return
+ val qualifiedName = returnType.getQualifiedName()
+ if (qualifiedName != null && qualifiedName == RESPONSE_CLS) {
+ checkClosed(context, node)
+ }
+ }
+ }
+
+ @Suppress("ReturnCount") // Extracted from `CleanupDetector#checkRecycled`.
+ private fun checkClosed(context: JavaContext, node: UCallExpression) {
+ // If it's an AutoCloseable in a try-with-resources clause, don't flag it: these will be
+ // cleaned up automatically
+ if (node.sourcePsi.isTryWithResources()) {
+ return
+ }
+
+ val parentMethod = node.getParentOfType(UMethod::class.java) ?: return
+
+ // Check if any of the 'body' methods are used. They are all closeable; do not report.
+ val bodyMethodTracker = BodyMethodTracker(listOf(node))
+ if (parentMethod.wasMethodCalled(bodyMethodTracker)) {
+ return
+ }
+
+ // Check if response has escaped (particularly through an extension function); do not report.
+ val responseEscapedTracker = ResponseEscapedTracker(listOf(node))
+ if (parentMethod.hasEscaped(responseEscapedTracker)) {
+ return
+ }
+
+ // Check if 'use' or 'close' were called; do not report.
+ val closeableTracker = CloseableTracker(listOf(node), context)
+ if (!parentMethod.isMissingTarget(closeableTracker)) {
+ return
+ }
+
+ context.report(
+ ISSUE_FETCH_RESPONSE_CLOSE,
+ node,
+ context.getCallLocation(node, includeReceiver = true, includeArguments = false),
+ "Response created but not closed: did you forget to call `close()`?",
+ if (CheckResultDetector.isExpressionValueUnused(node)) {
+ fix()
+ .replace()
+ .name("Call close()")
+ .range(context.getLocation(node))
+ .end()
+ .with(".close()")
+ .build()
+ } else {
+ null
+ },
+ )
+ }
+
+ private fun UMethod.hasEscaped(analyzer: EscapeCheckingDataFlowAnalyzer): Boolean {
+ accept(analyzer)
+ return analyzer.escaped
+ }
+
+ private fun UMethod.wasMethodCalled(analyzer: BodyMethodTracker): Boolean {
+ accept(analyzer)
+ return analyzer.found
+ }
+
+ private fun PsiElement?.isTryWithResources(): Boolean {
+ return this != null &&
+ isJava(this) &&
+ PsiTreeUtil.getParentOfType(this, PsiResourceVariable::class.java) != null
+ }
+
+ private class BodyMethodTracker(
+ initial: Collection<UElement>,
+ initialReferences: Collection<PsiVariable> = emptyList(),
+ ) : DataFlowAnalyzer(initial, initialReferences) {
+ var found = false
+ override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression): Boolean {
+ val methodName: String? = with(node.selector as? UCallExpression) {
+ this?.methodName ?: this?.methodIdentifier?.name
+ }
+
+ when (methodName) {
+ USE_STREAM,
+ USE_BUFFERED_READER,
+ STRING,
+ -> {
+ if (node.receiver.getExpressionType()?.canonicalText == BODY_CLS) {
+ // We are using any of the `body` methods which are all closeable.
+ found = true
+ return true
+ }
+ }
+ }
+
+ return super.visitQualifiedReferenceExpression(node)
+ }
+ }
+
+ private class ResponseEscapedTracker(
+ initial: Collection<UElement>,
+ ) : EscapeCheckingDataFlowAnalyzer(initial, emptyList()) {
+ override fun returnsSelf(call: UCallExpression): Boolean {
+ val type = call.receiver?.getExpressionType()?.canonicalText ?: return super.returnsSelf(call)
+ return type == RESPONSE_CLS
+ }
+ }
+
+ // Extracted from `CleanupDetector#checkRecycled#visitor`.
+ private class CloseableTracker(
+ initial: Collection<UElement>,
+ private val context: JavaContext,
+ ) : TargetMethodDataFlowAnalyzer(initial, emptyList()) {
+ override fun isTargetMethodName(name: String): Boolean {
+ return name == USE || name == CLOSE
+ }
+
+ @Suppress("ReturnCount")
+ override fun isTargetMethod(
+ name: String,
+ method: PsiMethod?,
+ call: UCallExpression?,
+ methodRef: UCallableReferenceExpression?,
+ ): Boolean {
+ if (USE == name) {
+ // Kotlin: "use" calls close;
+ // Ensure that "use" call accepts a single lambda parameter, so that it would
+ // loosely match kotlin.io.use() signature and at the same time allow custom
+ // overloads for types not extending Closeable
+ if (call != null && call.valueArgumentCount == 1) {
+ val argumentType =
+ call.valueArguments.first().skipLabeledExpression().getExpressionType()
+ if (argumentType != null && LambdaUtil.isFunctionalType(argumentType)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ if (method != null) {
+ val containingClass = method.containingClass
+ val targetName = containingClass?.qualifiedName ?: return true
+ if (targetName == RESPONSE_CLS) {
+ return true
+ }
+ val recycleClass =
+ context.evaluator.findClass(RESPONSE_CLS) ?: return true
+ return context.evaluator.extendsClass(recycleClass, targetName, false)
+ } else {
+ // Unresolved method call -- assume it's okay
+ return true
+ }
+ }
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE_FETCH_RESPONSE_CLOSE = Issue.create(
+ id = "FetchResponseClose",
+ briefDescription = "Response stream fetched but not closed.",
+ explanation = """
+ A `Client.fetch` returns a `Response` that, on success, is consumed typically with
+ a `use` stream in Kotlin or a try-with-resources in Java. In the failure or manual
+ resource managed cases, we need to ensure that `Response.close` is always called.
+
+ Additionally, all methods on `Response.body` are AutoCloseable so using any of
+ those will release those resources after execution.
+ """.trimIndent(),
+ category = Category.CORRECTNESS,
+ priority = 6,
+ severity = Severity.ERROR,
+ androidSpecific = true,
+ implementation = Implementation(
+ ConceptFetchDetector::class.java,
+ Scope.JAVA_FILE_SCOPE,
+ ),
+ )
+
+ // Target method names
+ private const val CLOSE = "close"
+ private const val USE = "use"
+ private const val USE_STREAM = "useStream"
+ private const val USE_BUFFERED_READER = "useBufferedReader"
+ private const val STRING = "string"
+
+ private const val CLIENT_CLS = "mozilla.components.concept.fetch.Client"
+ private const val RESPONSE_CLS = "mozilla.components.concept.fetch.Response"
+ private const val BODY_CLS = "mozilla.components.concept.fetch.Response.Body"
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/FactCollectDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/FactCollectDetector.kt
new file mode 100644
index 0000000000..4cd3f32f97
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/FactCollectDetector.kt
@@ -0,0 +1,103 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.checks.DataFlowAnalyzer
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * A custom lint check that warns if [Fact.collect()] is not called on a newly created [Fact] instance
+ */
+class FactCollectDetector : Detector(), SourceCodeScanner {
+
+ companion object {
+ private const val FULLY_QUALIFIED_FACT_CLASS_NAME =
+ "mozilla.components.support.base.facts.Fact"
+ private const val EXPECTED_METHOD_SIMPLE_NAME =
+ "collect" // The `Fact.collect` extension method
+
+ private val IMPLEMENTATION = Implementation(
+ FactCollectDetector::class.java,
+ Scope.JAVA_FILE_SCOPE,
+ )
+
+ val ISSUE_FACT_COLLECT_CALLED: Issue = Issue
+ .create(
+ id = "FactCollect",
+ briefDescription = "Fact created but not collected",
+ explanation = """
+ An instance of `Fact` was created but not collected. You must call
+ `collect()` on the instance to actually process it.
+ """.trimIndent(),
+ category = Category.CORRECTNESS,
+ priority = 6,
+ severity = Severity.ERROR,
+ implementation = IMPLEMENTATION,
+ )
+ }
+
+ override fun getApplicableConstructorTypes(): List<String> {
+ return listOf(FULLY_QUALIFIED_FACT_CLASS_NAME)
+ }
+
+ override fun visitConstructor(
+ context: JavaContext,
+ node: UCallExpression,
+ constructor: PsiMethod,
+ ) {
+ var isCollectCalled = false
+ var escapes = false
+ val visitor = object : DataFlowAnalyzer(setOf(node)) {
+ override fun receiver(call: UCallExpression) {
+ if (call.methodName == EXPECTED_METHOD_SIMPLE_NAME) {
+ isCollectCalled = true
+ }
+ }
+
+ override fun argument(call: UCallExpression, reference: UElement) {
+ escapes = true
+ }
+
+ override fun field(field: UElement) {
+ escapes = true
+ }
+
+ override fun returns(expression: UReturnExpression) {
+ escapes = true
+ }
+ }
+ val method = node.getParentOfType<UMethod>(UMethod::class.java, true) ?: return
+ method.accept(visitor)
+ if (!isCollectCalled && !escapes) {
+ reportUsage(context, node)
+ }
+ }
+
+ private fun reportUsage(context: JavaContext, node: UCallExpression) {
+ context.report(
+ issue = ISSUE_FACT_COLLECT_CALLED,
+ scope = node,
+ location = context.getCallLocation(
+ call = node,
+ includeReceiver = true,
+ includeArguments = false,
+ ),
+ message = "Fact created but not shown: did you forget to call `collect()` ?",
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetector.kt
new file mode 100644
index 0000000000..f775f6ee3f
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetector.kt
@@ -0,0 +1,81 @@
+/* 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.tooling.lint
+
+import com.android.SdkConstants.ATTR_TINT
+import com.android.SdkConstants.FQCN_IMAGE_BUTTON
+import com.android.SdkConstants.FQCN_IMAGE_VIEW
+import com.android.SdkConstants.IMAGE_BUTTON
+import com.android.SdkConstants.IMAGE_VIEW
+import com.android.resources.ResourceFolderType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.ResourceXmlDetector
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.XmlContext
+import org.w3c.dom.Element
+
+/**
+ * A custom lint check that prohibits not using the app:tint for ImageViews
+ */
+class ImageViewAndroidTintXmlDetector : ResourceXmlDetector() {
+ companion object {
+ const val SCHEMA = "http://schemas.android.com/apk/res/android"
+ const val FULLY_QUALIFIED_APP_COMPAT_IMAGE_BUTTON =
+ "androidx.appcompat.widget.AppCompatImageButton"
+ const val FULLY_QUALIFIED_APP_COMPAT_VIEW_CLASS =
+ "androidx.appcompat.widget.AppCompatImageView"
+ const val APP_COMPAT_IMAGE_BUTTON = "AppCompatImageButton"
+ const val APP_COMPAT_IMAGE_VIEW = "AppCompatImageView"
+
+ const val ERROR_MESSAGE =
+ "Using android:tint to tint ImageView instead of app:tint with AppCompatImageView"
+
+ val ISSUE_XML_SRC_USAGE = Issue.create(
+ id = "AndroidSrcXmlDetector",
+ briefDescription = "Prohibits using android:tint in ImageViews and ImageButtons",
+ explanation = "ImageView (and descendants) should be tinted using app:tint",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ ImageViewAndroidTintXmlDetector::class.java,
+ Scope.RESOURCE_FILE_SCOPE,
+ ),
+ )
+ }
+
+ override fun appliesTo(folderType: ResourceFolderType): Boolean {
+ // Return true if we want to analyze resource files in the specified resource
+ // folder type. In this case we only need to analyze layout resource files.
+ return folderType == ResourceFolderType.LAYOUT
+ }
+
+ override fun getApplicableElements(): Collection<String>? {
+ return setOf(
+ FQCN_IMAGE_VIEW,
+ IMAGE_VIEW,
+ FQCN_IMAGE_BUTTON,
+ IMAGE_BUTTON,
+ FULLY_QUALIFIED_APP_COMPAT_IMAGE_BUTTON,
+ FULLY_QUALIFIED_APP_COMPAT_VIEW_CLASS,
+ APP_COMPAT_IMAGE_BUTTON,
+ APP_COMPAT_IMAGE_VIEW,
+ )
+ }
+
+ override fun visitElement(context: XmlContext, element: Element) {
+ if (!element.hasAttributeNS(SCHEMA, ATTR_TINT)) return
+ val node = element.getAttributeNodeNS(SCHEMA, ATTR_TINT)
+
+ context.report(
+ issue = ISSUE_XML_SRC_USAGE,
+ scope = node,
+ location = context.getLocation(node),
+ message = ERROR_MESSAGE,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintIssueRegistry.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintIssueRegistry.kt
new file mode 100644
index 0000000000..31118405dc
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintIssueRegistry.kt
@@ -0,0 +1,25 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.detector.api.Issue
+
+/**
+ * Registry which provides a list of our custom lint checks to be performed on an Android project.
+ */
+@Suppress("unused")
+class LintIssueRegistry : IssueRegistry() {
+ override val api: Int = com.android.tools.lint.detector.api.CURRENT_API
+ override val issues: List<Issue> = listOf(
+ LintLogChecks.ISSUE_LOG_USAGE,
+ AndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE,
+ TextViewAndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE,
+ ImageViewAndroidTintXmlDetector.ISSUE_XML_SRC_USAGE,
+ FactCollectDetector.ISSUE_FACT_COLLECT_CALLED,
+ NotificationManagerChecks.ISSUE_NOTIFICATION_USAGE,
+ ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE,
+ )
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintLogChecks.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintLogChecks.kt
new file mode 100644
index 0000000000..a462335162
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintLogChecks.kt
@@ -0,0 +1,61 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.getContainingUClass
+import java.util.EnumSet
+
+internal const val ANDROID_LOG_CLASS = "android.util.Log"
+internal const val ERROR_MESSAGE = "Using Android Log instead of base component"
+
+/**
+ * Custom lint checks related to logging.
+ */
+class LintLogChecks : Detector(), Detector.UastScanner {
+ private val componentPackages = listOf("mozilla.components", "org.mozilla.telemetry", "org.mozilla.samples")
+
+ override fun getApplicableMethodNames() = listOf("v", "d", "i", "w", "e")
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (context.evaluator.isMemberInClass(method, ANDROID_LOG_CLASS)) {
+ val inComponentPackage = componentPackages.any {
+ node.methodIdentifier?.getContainingUClass()?.qualifiedName?.startsWith(it) == true
+ }
+
+ if (inComponentPackage) {
+ context.report(
+ ISSUE_LOG_USAGE,
+ node,
+ context.getLocation(node),
+ ERROR_MESSAGE,
+ )
+ }
+ }
+ }
+
+ companion object {
+ internal val ISSUE_LOG_USAGE = Issue.create(
+ "LogUsage",
+ "Log/Logger from base component should be used.",
+ """The Log or Logger class from the base component should be used for logging instead of
+ Android's Log class. This will allow the app to control what logs should be accepted
+ and how they should be processed.
+ """.trimIndent(),
+ Category.MESSAGES,
+ 5,
+ Severity.WARNING,
+ Implementation(LintLogChecks::class.java, EnumSet.of(Scope.JAVA_FILE)),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/NotificationManagerChecks.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/NotificationManagerChecks.kt
new file mode 100644
index 0000000000..b2141abbbb
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/NotificationManagerChecks.kt
@@ -0,0 +1,84 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.getContainingUClass
+import java.util.EnumSet
+
+internal const val ANDROID_NOTIFICATION_MANAGER_COMPAT_CLASS =
+ "androidx.core.app.NotificationManagerCompat"
+internal const val ANDROID_NOTIFICATION_MANAGER_CLASS =
+ "android.app.NotificationManager"
+
+internal const val NOTIFY_ERROR_MESSAGE = "Using Android NOTIFY instead of base component"
+
+/**
+ * Custom lint that ensures [NotificationManagerCompat] and [NotificationManager]'s method [notify]
+ * is not called directly from code.
+ * Calling notify directly from code eludes the checks implemented in [NotificationsDelegate]
+ */
+class NotificationManagerChecks : Detector(), Detector.UastScanner {
+ private val componentPackages =
+ listOf("mozilla.components", "org.mozilla.telemetry", "org.mozilla.samples")
+ private val appPackages = listOf("org.mozilla.fenix", "org.mozilla.focus")
+
+ override fun getApplicableMethodNames() = listOf("notify")
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (context.evaluator.isMemberInClass(method, ANDROID_NOTIFICATION_MANAGER_COMPAT_CLASS) ||
+ context.evaluator.isMemberInClass(method, ANDROID_NOTIFICATION_MANAGER_CLASS)
+ ) {
+ val inComponentPackage = componentPackages.any {
+ node.methodIdentifier?.getContainingUClass()?.qualifiedName?.startsWith(it) == true
+ }
+
+ val inAppPackage = appPackages.any {
+ node.methodIdentifier?.getContainingUClass()?.qualifiedName?.startsWith(it) == true
+ }
+
+ if (inComponentPackage) {
+ context.report(
+ ISSUE_NOTIFICATION_USAGE,
+ node,
+ context.getLocation(node),
+ NOTIFY_ERROR_MESSAGE,
+ )
+ }
+
+ if (inAppPackage) {
+ context.report(
+ ISSUE_NOTIFICATION_USAGE,
+ node,
+ context.getLocation(node),
+ NOTIFY_ERROR_MESSAGE,
+ )
+ }
+ }
+ }
+
+ companion object {
+ internal val ISSUE_NOTIFICATION_USAGE = Issue.create(
+ "NotifyUsage",
+ "NotificationsDelegate should be used instead of NotificationManager.",
+ """NotificationsDelegate should be used for showing notifications instead of a NotificationManager
+ or a NotificationManagerCompat. This will allow the app to control requesting the notification permission
+ when needed and handling the request result.
+ """.trimIndent(),
+ Category.MESSAGES,
+ 5,
+ Severity.WARNING,
+ Implementation(NotificationManagerChecks::class.java, EnumSet.of(Scope.JAVA_FILE)),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetector.kt
new file mode 100644
index 0000000000..0be9ab89a8
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetector.kt
@@ -0,0 +1,97 @@
+/* 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.tooling.lint
+
+import com.android.SdkConstants.ATTR_DRAWABLE_BOTTOM
+import com.android.SdkConstants.ATTR_DRAWABLE_END
+import com.android.SdkConstants.ATTR_DRAWABLE_LEFT
+import com.android.SdkConstants.ATTR_DRAWABLE_RIGHT
+import com.android.SdkConstants.ATTR_DRAWABLE_START
+import com.android.SdkConstants.ATTR_DRAWABLE_TOP
+import com.android.SdkConstants.FQCN_TEXT_VIEW
+import com.android.SdkConstants.TEXT_VIEW
+import com.android.resources.ResourceFolderType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.ResourceXmlDetector
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.XmlContext
+import org.w3c.dom.Element
+
+/**
+ * A custom lint check that prohibits not using the app:srcCompat for ImageViews
+ */
+class TextViewAndroidSrcXmlDetector : ResourceXmlDetector() {
+ companion object {
+ const val SCHEMA = "http://schemas.android.com/apk/res/android"
+
+ const val ERROR_MESSAGE =
+ "Using android:drawableX to define resource instead of app:drawableXCompat"
+
+ val ISSUE_XML_SRC_USAGE = Issue.create(
+ id = "TextViewAndroidSrcXmlDetector",
+ briefDescription = "Prohibits using android namespace to define drawables in TextViews",
+ explanation = "TextView drawables should be declared using app:drawableXCompat",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ TextViewAndroidSrcXmlDetector::class.java,
+ Scope.RESOURCE_FILE_SCOPE,
+ ),
+ )
+ }
+
+ override fun appliesTo(folderType: ResourceFolderType): Boolean {
+ // Return true if we want to analyze resource files in the specified resource
+ // folder type. In this case we only need to analyze layout resource files.
+ return folderType == ResourceFolderType.LAYOUT
+ }
+
+ override fun getApplicableElements(): Collection<String>? {
+ return setOf(
+ FQCN_TEXT_VIEW,
+ TEXT_VIEW,
+ )
+ }
+
+ override fun visitElement(context: XmlContext, element: Element) {
+ val node = when {
+ element.hasAttributeNS(SCHEMA, ATTR_DRAWABLE_BOTTOM) -> element.getAttributeNodeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_BOTTOM,
+ )
+ element.hasAttributeNS(SCHEMA, ATTR_DRAWABLE_END) -> element.getAttributeNodeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_END,
+ )
+ element.hasAttributeNS(SCHEMA, ATTR_DRAWABLE_LEFT) -> element.getAttributeNodeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_LEFT,
+ )
+ element.hasAttributeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_RIGHT,
+ ) -> element.getAttributeNodeNS(SCHEMA, ATTR_DRAWABLE_RIGHT)
+ element.hasAttributeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_START,
+ ) -> element.getAttributeNodeNS(SCHEMA, ATTR_DRAWABLE_START)
+ element.hasAttributeNS(SCHEMA, ATTR_DRAWABLE_TOP) -> element.getAttributeNodeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_TOP,
+ )
+ else -> null
+ } ?: return
+
+ context.report(
+ issue = ISSUE_XML_SRC_USAGE,
+ scope = node,
+ location = context.getLocation(node),
+ message = ERROR_MESSAGE,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/AndroidSrcXmlDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/AndroidSrcXmlDetectorTest.kt
new file mode 100644
index 0000000000..bae0412d45
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/AndroidSrcXmlDetectorTest.kt
@@ -0,0 +1,68 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Tests for the [AndroidSrcXmlDetector] custom lint check.
+ */
+@RunWith(JUnit4::class)
+class AndroidSrcXmlDetectorTest : LintDetectorTest() {
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(AndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE)
+
+ override fun getDetector(): Detector = AndroidSrcXmlDetector()
+
+ @Test
+ fun expectPass() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun expectFail() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_close"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expect(
+ """
+res/layout/layout.xml:5: Error: Using android:src to define resource instead of app:srcCompat [AndroidSrcXmlDetector]
+ android:src="@drawable/ic_close"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ConceptFetchDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ConceptFetchDetectorTest.kt
new file mode 100644
index 0000000000..cd60356015
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ConceptFetchDetectorTest.kt
@@ -0,0 +1,277 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles.gradle
+import com.android.tools.lint.checks.infrastructure.TestFiles.java
+import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
+import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ConceptFetchDetectorTest {
+
+ @Test
+ fun `should report when close is not invoked on a Response instance`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.concept.fetch.*
+
+ val client = Client()
+
+ fun isSuccessful() : Boolean {
+ val response = client.fetch(Request("https://mozilla.org"))
+ return response.isSuccess
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expect(
+ """
+ src/test/test.kt:8: Error: Response created but not closed: did you forget to call close()? [FetchResponseClose]
+ val response = client.fetch(Request("https://mozilla.org"))
+ ~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ fun `should not report from a result that is closed in another function`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.concept.fetch.*
+
+ val client = Client()
+
+ fun getResult() {
+ return try {
+ client.fetch(request).toResult()
+ } catch (e: IOException) {
+ Logger.debug(message = "Could not fetch region from location service", throwable = e)
+ null
+ }
+ }
+
+ data class Result(
+ val name: String,
+ )
+
+ private fun Response.toResult(): Region? {
+ if (!this.isSuccess) {
+ close()
+ return null
+ }
+
+ use {
+ return try {
+ Result("{}")
+ } catch (e: JSONException) {
+ Logger.debug(message = "Could not parse JSON returned from service", throwable = e)
+ null
+ }
+ }
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass when auto-closeable 'use' function is invoked`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.concept.fetch.*
+ import kotlin.io.*
+
+ val client = Client()
+
+ fun getResult() {
+ client.fetch(request).use { response ->
+ response.hashCode()
+ }
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass if body (auto-closeable methods) is used from the response`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.concept.fetch.*
+ import kotlin.io.*
+
+ val client = Client()
+
+ fun getResult() { // OK
+ val response = client.fetch(request)
+ response?.body.string(Charset.UTF_8)
+ }
+
+ fun getResult2() { // OK
+ client.fetch(request).body.useStream()
+ }
+
+ fun getResult3() { // OK; escaped.
+ val response = client.fetch(request)
+ process(response)
+ }
+
+ fun process(response: Response) {
+ response.hashCode()
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass if try-with-resources is used from the response`() {
+ lint()
+ .files(
+ gradle(
+ // For `try (cursor)` (without declaration) we'll need level 9
+ // or PSI/UAST will return an empty variable list
+ """
+ android {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_9
+ targetCompatibility JavaVersion.VERSION_1_9
+ }
+ }
+ """,
+ ).indented(),
+ java(
+ """
+ package test;
+
+ import mozilla.components.concept.fetch.Client;
+ import mozilla.components.concept.fetch.Response;
+ import mozilla.components.concept.fetch.Response.Body;
+ import mozilla.components.concept.fetch.Request;
+
+ public class TryWithResources {
+ public void test(Client client, Request request) {
+ try(Response response = client.fetch(request)) {
+ if (response != null) {
+ //noinspection StatementWithEmptyBody
+ while (response.hashCode()) {
+ // ..
+ }
+ }
+ } catch (Exception e) {
+ // do nothing
+ }
+ }
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expectClean()
+ }
+
+ private val clientClassFileStub = kotlin(
+ """
+ package mozilla.components.concept.fetch
+
+ data class Request(
+ val url: String,
+ val method: Method = Method.GET,
+ val headers: MutableHeaders? = MutableHeaders(),
+ val connectTimeout: Pair<Long, TimeUnit>? = null,
+ val readTimeout: Pair<Long, TimeUnit>? = null,
+ val body: Body? = null,
+ val redirect: Redirect = Redirect.FOLLOW,
+ val cookiePolicy: CookiePolicy = CookiePolicy.INCLUDE,
+ val useCaches: Boolean = true,
+ val private: Boolean = false,
+ )
+
+ class Client {
+ fun fetch(request: Request): Response {
+ return Response(
+ url = "https://mozilla.org",
+ )
+ }
+ }
+ """.trimIndent(),
+ )
+ private val responseClassfileStub = kotlin(
+ """
+ package mozilla.components.concept.fetch
+
+ data class Response(
+ val url: String,
+ val status: Int,
+ val headers: Headers,
+ val body: Body,
+ ) : Closeable {
+ override fun close() {
+ body.close()
+ }
+
+ open class Body(
+ private val stream: InputStream,
+ contentType: String? = null,
+ ) {
+ fun <R> useStream(block: (InputStream) -> R): R {
+ }
+
+ fun <R> useBufferedReader(charset: Charset? = null, block: (BufferedReader) -> R): R = use {
+ }
+
+ fun string(charset: Charset? = null): String = use {
+ }
+ }
+ }
+
+ val Response.isSuccess: Boolean
+ get() = status in SUCCESS_STATUS_RANGE
+ """,
+ ).indented()
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/FactCollectDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/FactCollectDetectorTest.kt
new file mode 100644
index 0000000000..b70eca91c4
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/FactCollectDetectorTest.kt
@@ -0,0 +1,220 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
+import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Tests for the [FactCollectDetector] custom lint check.
+ */
+@RunWith(JUnit4::class)
+class FactCollectDetectorTest {
+
+ private val factClassfileStub = kotlin(
+ """
+ package mozilla.components.support.base.facts
+
+ data class Fact(
+ val component: Component,
+ val action: Action,
+ val item: String,
+ val value: String? = null,
+ val metadata: Map<String, Any>? = null
+ )
+
+ fun Fact.collect() = Facts.collect(this)
+ """,
+ ).indented()
+
+ @Test
+ fun `should report when collect is not invoked on Fact instance`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ private fun emitAwesomebarFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ) {
+ Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ )
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expect(
+ """
+ src/test/test.kt:12: Error: Fact created but not shown: did you forget to call collect() ? [FactCollect]
+ Fact(
+ ~~~~
+ 1 errors, 0 warnings
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ fun `should pass when collect is invoked on Fact instance`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ private fun emitAwesomebarFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ) {
+ Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ ).collect()
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass when an instance escapes through a return statement`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ private fun createFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ): Fact {
+ return Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ )
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass when an instance escapes through a method parameter`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ private fun createFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ) {
+ val fact = Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ )
+ method(fact)
+ }
+
+ private fun method(parameter: Fact) {
+
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass when an instance escapes through a field assignment`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ class FactSender {
+ private var fact: Fact? = null
+
+ private fun createFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ) {
+ fact = Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ )
+ }
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expectClean()
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetectorTest.kt
new file mode 100644
index 0000000000..0a715407bd
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetectorTest.kt
@@ -0,0 +1,69 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Tests for the [ImageViewAndroidTintXmlDetector] custom lint check.
+ */
+@RunWith(JUnit4::class)
+class ImageViewAndroidTintXmlDetectorTest : LintDetectorTest() {
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(ImageViewAndroidTintXmlDetector.ISSUE_XML_SRC_USAGE)
+
+ override fun getDetector(): Detector = ImageViewAndroidTintXmlDetector()
+
+ @Test
+ fun expectPass() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun expectFail() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_close"
+ android:tint="@color/photonBlue90"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expect(
+ """
+res/layout/layout.xml:6: Error: Using android:tint to tint ImageView instead of app:tint with AppCompatImageView [AndroidSrcXmlDetector]
+ android:tint="@color/photonBlue90"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/LintLogChecksTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/LintLogChecksTest.kt
new file mode 100644
index 0000000000..1b10f883fa
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/LintLogChecksTest.kt
@@ -0,0 +1,56 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.client.api.JavaEvaluator
+import com.android.tools.lint.detector.api.JavaContext
+import com.intellij.psi.PsiMethod
+import mozilla.components.tooling.lint.LintLogChecks.Companion.ISSUE_LOG_USAGE
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UIdentifier
+import org.jetbrains.uast.getContainingUClass
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class LintLogChecksTest {
+
+ @Test
+ fun `report log error in components code only`() {
+ val evaluator = mock(JavaEvaluator::class.java)
+ val context = mock(JavaContext::class.java)
+ val node = mock(UCallExpression::class.java)
+ val method = mock(PsiMethod::class.java)
+ val methodIdentifier = mock(UIdentifier::class.java)
+ val clazz = mock(UClass::class.java)
+
+ `when`(evaluator.isMemberInClass(method, ANDROID_LOG_CLASS)).thenReturn(true)
+ `when`(context.evaluator).thenReturn(evaluator)
+
+ val logCheck = LintLogChecks()
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, never()).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+
+ `when`(node.methodIdentifier).thenReturn(methodIdentifier)
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, never()).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+
+ `when`(methodIdentifier.getContainingUClass()).thenReturn(clazz)
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, never()).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+
+ `when`(clazz.qualifiedName).thenReturn("com.some.app.Class")
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, never()).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+
+ `when`(clazz.qualifiedName).thenReturn("mozilla.components.some.Class")
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, times(1)).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetectorTest.kt
new file mode 100644
index 0000000000..f764084350
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetectorTest.kt
@@ -0,0 +1,68 @@
+/* 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.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Tests for the [TextViewAndroidSrcXmlDetector] custom lint check.
+ */
+@RunWith(JUnit4::class)
+class TextViewAndroidSrcXmlDetectorTest : LintDetectorTest() {
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(TextViewAndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE)
+
+ override fun getDetector(): Detector = TextViewAndroidSrcXmlDetector()
+
+ @Test
+ fun expectPass() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun expectFail() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_close"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expect(
+ """
+res/layout/layout.xml:5: Error: Using android:drawableX to define resource instead of app:drawableXCompat [TextViewAndroidSrcXmlDetector]
+ android:drawableStart="@drawable/ic_close"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/tooling/lint/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/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/ui/autocomplete/README.md b/mobile/android/android-components/components/ui/autocomplete/README.md
new file mode 100644
index 0000000000..da22f2fea5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > UI > Autocomplete
+
+A set of components to provide autocomplete functionality.
+
+## Usage
+
+### 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:ui-autocomplete:{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/ui/autocomplete/build.gradle b/mobile/android/android-components/components/ui/autocomplete/build.gradle
new file mode 100644
index 0000000000..16f9792a02
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/build.gradle
@@ -0,0 +1,40 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.ui.autocomplete'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_appcompat
+
+ implementation project(":support-base")
+ implementation project(":support-utils")
+
+ testImplementation project(":support-test")
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+}
+
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/ui/autocomplete/proguard-rules.pro b/mobile/android/android-components/components/ui/autocomplete/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/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/ui/autocomplete/src/main/AndroidManifest.xml b/mobile/android/android-components/components/ui/autocomplete/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/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/ui/autocomplete/src/main/java/mozilla/components/ui/autocomplete/InlineAutocompleteEditText.kt b/mobile/android/android-components/components/ui/autocomplete/src/main/java/mozilla/components/ui/autocomplete/InlineAutocompleteEditText.kt
new file mode 100644
index 0000000000..c029f226eb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/src/main/java/mozilla/components/ui/autocomplete/InlineAutocompleteEditText.kt
@@ -0,0 +1,905 @@
+/* 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.ui.autocomplete
+
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Context.INPUT_METHOD_SERVICE
+import android.graphics.Rect
+import android.os.Build
+import android.provider.Settings.Secure.DEFAULT_INPUT_METHOD
+import android.provider.Settings.Secure.getString
+import android.text.Editable
+import android.text.NoCopySpan
+import android.text.Selection
+import android.text.Spannable
+import android.text.Spanned
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.text.style.BackgroundColorSpan
+import android.text.style.ForegroundColorSpan
+import android.util.AttributeSet
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import android.view.inputmethod.BaseInputConnection
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputConnection
+import android.view.inputmethod.InputConnectionWrapper
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatEditText
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+import mozilla.components.support.utils.SafeUrl
+import androidx.appcompat.R as appcompatR
+
+typealias OnCommitListener = () -> Unit
+typealias OnFilterListener = (String) -> Unit
+typealias OnSearchStateChangeListener = (Boolean) -> Unit
+typealias OnTextChangeListener = (String, String) -> Unit
+typealias OnDispatchKeyEventPreImeListener = (KeyEvent?) -> Boolean
+typealias OnKeyPreImeListener = (View, Int, KeyEvent) -> Boolean
+typealias OnSelectionChangedListener = (Int, Int) -> Unit
+typealias OnWindowsFocusChangeListener = (Boolean) -> Unit
+
+typealias TextFormatter = (String) -> String
+
+/**
+ * Aids in testing functionality which relies on some aspects of InlineAutocompleteEditText.
+ */
+interface AutocompleteView {
+
+ /**
+ * Current text.
+ */
+ val originalText: String
+
+ /**
+ * Apply provided [result] autocomplete result.
+ */
+ fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult)
+
+ /**
+ * Notify that there is no autocomplete result available.
+ */
+ fun noAutocompleteResult()
+}
+
+/**
+ * A UI edit text component which supports inline autocompletion.
+ *
+ * The background color of autocomplete spans can be configured using
+ * the custom autocompleteBackgroundColor attribute e.g.
+ * app:autocompleteBackgroundColor="#ffffff".
+ *
+ * A filter listener (see [setOnFilterListener]) needs to be attached to
+ * provide autocomplete results. It will be invoked when the input
+ * text changes. The listener gets direct access to this component (via its view
+ * parameter), so it can call {@link applyAutocompleteResult} in return.
+ *
+ * A commit listener (see [setOnCommitListener]) can be attached which is
+ * invoked when the user selected the result i.e. is done editing.
+ *
+ * Various other listeners can be attached to enhance default behaviour e.g.
+ * [setOnSelectionChangedListener] and [setOnWindowsFocusChangeListener] which
+ * will be invoked in response to [onSelectionChanged] and [onWindowFocusChanged]
+ * respectively (see also [setOnTextChangeListener],
+ * [setOnSelectionChangedListener], and [setOnWindowsFocusChangeListener]).
+ */
+@Suppress("LargeClass", "TooManyFunctions")
+open class InlineAutocompleteEditText @JvmOverloads constructor(
+ ctx: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = appcompatR.attr.editTextStyle,
+) : AppCompatEditText(ctx, attrs, defStyleAttr), AutocompleteView {
+
+ data class AutocompleteResult(
+ val text: String,
+ val source: String,
+ val totalItems: Int,
+ ) {
+ fun startsWith(text: String): Boolean = this.text.startsWith(text)
+ }
+
+ private var commitListener: OnCommitListener? = null
+ fun setOnCommitListener(l: OnCommitListener) { commitListener = l }
+
+ private var filterListener: OnFilterListener? = null
+ fun setOnFilterListener(l: OnFilterListener) { filterListener = l }
+ fun refreshAutocompleteSuggestions() { filterListener?.invoke(originalText) }
+
+ private var searchStateChangeListener: OnSearchStateChangeListener? = null
+ fun setOnSearchStateChangeListener(l: OnSearchStateChangeListener) { searchStateChangeListener = l }
+
+ private var textChangeListener: OnTextChangeListener? = null
+ fun setOnTextChangeListener(l: OnTextChangeListener) { textChangeListener = l }
+
+ private var dispatchKeyEventPreImeListener: OnDispatchKeyEventPreImeListener? = null
+ fun setOnDispatchKeyEventPreImeListener(l: OnDispatchKeyEventPreImeListener?) { dispatchKeyEventPreImeListener = l }
+
+ private var keyPreImeListener: OnKeyPreImeListener? = null
+ fun setOnKeyPreImeListener(l: OnKeyPreImeListener) { keyPreImeListener = l }
+
+ private var selectionChangedListener: OnSelectionChangedListener? = null
+ fun setOnSelectionChangedListener(l: OnSelectionChangedListener) { selectionChangedListener = l }
+
+ private var windowFocusChangeListener: OnWindowsFocusChangeListener? = null
+ fun setOnWindowsFocusChangeListener(l: OnWindowsFocusChangeListener) { windowFocusChangeListener = l }
+
+ // The previous autocomplete result returned to us
+ var autocompleteResult: AutocompleteResult? = null
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ set
+
+ // Length of the user-typed portion of the result
+ private var autoCompletePrefixLength: Int = 0
+
+ // If text change is due to us setting autocomplete
+ private var settingAutoComplete: Boolean = false
+
+ // Spans used for marking the autocomplete text
+ private var autoCompleteSpans: List<Any>? = null
+
+ // Do not process autocomplete result
+ private var discardAutoCompleteResult: Boolean = false
+
+ val nonAutocompleteText: String
+ get() = getNonAutocompleteText(text)
+
+ override val originalText: String
+ get() = text.subSequence(0, autoCompletePrefixLength).toString()
+
+ /**
+ * The background color used for the autocomplete suggestion.
+ */
+ var autoCompleteBackgroundColor: Int = {
+ val a = context.obtainStyledAttributes(attrs, R.styleable.InlineAutocompleteEditText)
+ val color = a.getColor(
+ R.styleable.InlineAutocompleteEditText_autocompleteBackgroundColor,
+ DEFAULT_AUTOCOMPLETE_BACKGROUND_COLOR,
+ )
+ a.recycle()
+ color
+ }()
+
+ /**
+ * The Foreground color used for the autocomplete suggestion.
+ */
+ var autoCompleteForegroundColor: Int? = null
+
+ private val inputMethodManger get() = context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager?
+
+ @SuppressWarnings("ReturnCount")
+ private val onKeyPreIme = fun (_: View, keyCode: Int, event: KeyEvent): Boolean {
+ // We only want to process one event per tap
+ if (event.action != KeyEvent.ACTION_DOWN) {
+ return false
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ // If the edit text has a composition string, don't submit the text yet.
+ // ENTER is needed to commit the composition string.
+ val content = text
+ if (!hasCompositionString(content)) {
+ commitListener?.invoke()
+ return true
+ }
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ removeAutocomplete(text)
+ return false
+ }
+
+ return false
+ }
+
+ private val onKey = fun (_: View, keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ if (event.action != KeyEvent.ACTION_DOWN) {
+ return true
+ }
+
+ commitListener?.invoke()
+ return true
+ }
+
+ // Delete autocomplete text when backspacing or forward deleting.
+ return (
+ (
+ keyCode == KeyEvent.KEYCODE_DEL ||
+ keyCode == KeyEvent.KEYCODE_FORWARD_DEL
+ ) &&
+ removeAutocomplete(text)
+ )
+ }
+
+ private val onSelectionChanged = fun (selStart: Int, selEnd: Int) {
+ // The user has repositioned the cursor somewhere. We need to adjust
+ // the autocomplete text depending on where the new cursor is.
+ val text = text
+ val start = text.getSpanStart(AUTOCOMPLETE_SPAN)
+
+ val nothingSelected = start == selStart && start == selEnd
+ if (settingAutoComplete || nothingSelected || start < 0) {
+ // Do not commit autocomplete text if there is no autocomplete text
+ // or if selection is still at start of autocomplete text
+ return
+ }
+
+ if (selStart <= start && selEnd <= start) {
+ // The cursor is in user-typed text; remove any autocomplete text.
+ removeAutocomplete(text)
+ } else {
+ // The cursor is in the autocomplete text; commit it so it becomes regular text.
+ commitAutocomplete(text)
+ }
+ }
+
+ private val isAmazonEchoShowKeyboard: Boolean
+ get() = INPUT_METHOD_AMAZON_ECHO_SHOW == getCurrentInputMethod()
+
+ public override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+
+ if (this.keyPreImeListener == null) { this.keyPreImeListener = onKeyPreIme }
+ if (this.selectionChangedListener == null) { this.selectionChangedListener = onSelectionChanged }
+
+ setOnKeyListener(onKey)
+ addTextChangedListener(TextChangeListener())
+ }
+
+ public override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
+
+ // Make search icon inactive when edit toolbar search term isn't a user entered
+ // search term
+ val isActive = !TextUtils.isEmpty(text)
+
+ searchStateChangeListener?.invoke(isActive)
+
+ if (gainFocus) {
+ resetAutocompleteState()
+ return
+ }
+
+ removeAutocomplete(text)
+
+ try {
+ restartInput()
+ inputMethodManger?.hideSoftInputFromWindow(windowToken, 0)
+ } catch (ignored: NullPointerException) {
+ // See bug 782096 for details
+ }
+ }
+
+ override fun setText(text: CharSequence?, type: BufferType) {
+ val textString = text?.toString() ?: ""
+ super.setText(textString, type)
+
+ // Any autocomplete text would have been overwritten, so reset our autocomplete states.
+ resetAutocompleteState()
+ }
+
+ /**
+ * Sets the text of the edit text.
+ * @param text The text to set.
+ * @param shouldAutoComplete If false, [TextChangeListener] the text watcher will be disabled for this set.
+ */
+ fun setText(text: CharSequence?, shouldAutoComplete: Boolean = true) {
+ val wasSettingAutoComplete = settingAutoComplete
+
+ // Disable listeners in order to stop auto completion
+ settingAutoComplete = !shouldAutoComplete
+ setText(text, BufferType.EDITABLE)
+ settingAutoComplete = wasSettingAutoComplete
+ }
+
+ /**
+ * Appends the given text to the end of the current text.
+ * @param text The text to append.
+ * @param shouldAutoComplete If false, [TextChangeListener] text watcher will be disabled for this append.
+ */
+ fun appendText(text: CharSequence?, shouldAutoComplete: Boolean = true) {
+ val wasSettingAutoComplete = settingAutoComplete
+
+ // Disable listeners in order to stop auto completion
+ settingAutoComplete = !shouldAutoComplete
+ append(text)
+ settingAutoComplete = wasSettingAutoComplete
+ }
+
+ override fun getText(): Editable {
+ return super.getText() as Editable
+ }
+
+ override fun sendAccessibilityEventUnchecked(event: AccessibilityEvent) {
+ // We need to bypass the isShown() check in the default implementation
+ // for TYPE_VIEW_TEXT_SELECTION_CHANGED events so that accessibility
+ // services could detect a url change.
+ if (event.eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED &&
+ parent != null && !isShown
+ ) {
+ onInitializeAccessibilityEvent(event)
+ dispatchPopulateAccessibilityEvent(event)
+ parent.requestSendAccessibilityEvent(this, event)
+ } else {
+ super.sendAccessibilityEventUnchecked(event)
+ }
+ }
+
+ /**
+ * Mark the start of autocomplete changes so our text change
+ * listener does not react to changes in autocomplete text
+ */
+ private fun beginSettingAutocomplete() {
+ beginBatchEdit()
+ settingAutoComplete = true
+ }
+
+ /**
+ * Mark the end of autocomplete changes
+ */
+ private fun endSettingAutocomplete() {
+ settingAutoComplete = false
+ endBatchEdit()
+ }
+
+ /**
+ * Reset autocomplete states to their initial values
+ */
+ private fun resetAutocompleteState() {
+ autoCompleteSpans = mutableListOf(
+ AUTOCOMPLETE_SPAN,
+ BackgroundColorSpan(autoCompleteBackgroundColor),
+ ).apply {
+ autoCompleteForegroundColor?.let { add(ForegroundColorSpan(it)) }
+ }
+ autocompleteResult = null
+ // Pretend we already autocompleted the existing text,
+ // so that actions like backspacing don't trigger autocompletion.
+ autoCompletePrefixLength = text.length
+ isCursorVisible = true
+ }
+
+ /**
+ * Remove any autocomplete text
+ *
+ * @param text Current text content that may include autocomplete text
+ */
+ private fun removeAutocomplete(text: Editable): Boolean {
+ val start = text.getSpanStart(AUTOCOMPLETE_SPAN)
+ if (start < 0) {
+ // No autocomplete text
+ return false
+ }
+
+ beginSettingAutocomplete()
+
+ // When we call delete() here, the autocomplete spans we set are removed as well.
+ text.delete(start, text.length)
+
+ // Keep autoCompletePrefixLength the same because the prefix has not changed.
+ // Clear mAutoCompleteResult to make sure we get fresh autocomplete text next time.
+ autocompleteResult = null
+
+ // Reshow the cursor.
+ isCursorVisible = true
+
+ endSettingAutocomplete()
+ return true
+ }
+
+ /**
+ * Convert any autocomplete text to regular text
+ *
+ * @param text Current text content that may include autocomplete text
+ */
+ private fun commitAutocomplete(text: Editable): Boolean {
+ val start = text.getSpanStart(AUTOCOMPLETE_SPAN)
+ if (start < 0) {
+ // No autocomplete text
+ return false
+ }
+
+ beginSettingAutocomplete()
+
+ // Remove all spans here to convert from autocomplete text to regular text
+ for (span in autoCompleteSpans!!) {
+ text.removeSpan(span)
+ }
+
+ // Keep mAutoCompleteResult the same because the result has not changed.
+ // Reset autoCompletePrefixLength because the prefix now includes the autocomplete text.
+ autoCompletePrefixLength = text.length
+
+ // Reshow the cursor.
+ isCursorVisible = true
+
+ endSettingAutocomplete()
+
+ // Invoke textChangeListener manually, because previous autocomplete text is now committed
+ textChangeListener?.apply {
+ val fullText = text.toString()
+ invoke(fullText, fullText)
+ }
+
+ return true
+ }
+
+ /**
+ * Applies the provided result by updating the current autocomplete
+ * text and selection, if any.
+ *
+ * @param result the [AutocompleteResult] to apply
+ */
+ override fun applyAutocompleteResult(result: AutocompleteResult) {
+ // If discardAutoCompleteResult is true, we temporarily disabled
+ // autocomplete (due to backspacing, etc.) and we should bail early.
+ if (discardAutoCompleteResult) {
+ return
+ }
+
+ if (!isEnabled) {
+ autocompleteResult = null
+ return
+ }
+
+ val text = text
+ val autoCompleteStart = text.getSpanStart(AUTOCOMPLETE_SPAN)
+ autocompleteResult = result
+
+ if (autoCompleteStart > -1) {
+ // Autocomplete text already exists; we should replace existing autocomplete text.
+ replaceAutocompleteText(result, autoCompleteStart)
+ } else {
+ // No autocomplete text yet; we should add autocomplete text
+ addAutocompleteText(result)
+ }
+
+ announceForAccessibility(text.toString())
+ }
+
+ private fun replaceAutocompleteText(result: AutocompleteResult, autoCompleteStart: Int) {
+ // Autocomplete text already exists; we should replace existing autocomplete text.
+ val text = text
+ val resultLength = result.text.length
+
+ // If the result and the current text don't have the same prefixes,
+ // the result is stale and we should wait for the another result to come in.
+ if (!TextUtils.regionMatches(result.text, 0, text, 0, autoCompleteStart)) {
+ return
+ }
+
+ beginSettingAutocomplete()
+
+ // Replace the existing autocomplete text with new one.
+ // replace() preserves the autocomplete spans that we set before.
+ text.replace(autoCompleteStart, text.length, result.text, autoCompleteStart, resultLength)
+
+ // Reshow the cursor if there is no longer any autocomplete text.
+ if (autoCompleteStart == resultLength) {
+ isCursorVisible = true
+ }
+
+ endSettingAutocomplete()
+ }
+
+ private fun addAutocompleteText(result: AutocompleteResult) {
+ // No autocomplete text yet; we should add autocomplete text
+ val text = text
+ val textLength = text.length
+ val resultLength = result.text.length
+
+ // If the result prefix doesn't match the current text,
+ // the result is stale and we should wait for the another result to come in.
+ if (resultLength <= textLength || !TextUtils.regionMatches(result.text, 0, text, 0, textLength)) {
+ return
+ }
+
+ val spans = text.getSpans(textLength, textLength, Any::class.java)
+ val spanStarts = IntArray(spans.size)
+ val spanEnds = IntArray(spans.size)
+ val spanFlags = IntArray(spans.size)
+
+ // Save selection/composing span bounds so we can restore them later.
+ for (i in spans.indices) {
+ val span = spans[i]
+ val spanFlag = text.getSpanFlags(span)
+
+ // We don't care about spans that are not selection or composing spans.
+ // For those spans, spanFlag[i] will be 0 and we don't restore them.
+ if (spanFlag and Spanned.SPAN_COMPOSING == 0 &&
+ span !== Selection.SELECTION_START &&
+ span !== Selection.SELECTION_END
+ ) {
+ continue
+ }
+
+ spanStarts[i] = text.getSpanStart(span)
+ spanEnds[i] = text.getSpanEnd(span)
+ spanFlags[i] = spanFlag
+ }
+
+ beginSettingAutocomplete()
+
+ // First add trailing text.
+ text.append(result.text, textLength, resultLength)
+
+ // Restore selection/composing spans.
+ for (i in spans.indices) {
+ val spanFlag = spanFlags[i]
+ if (spanFlag == 0) {
+ // Skip if the span was ignored before.
+ continue
+ }
+ text.setSpan(spans[i], spanStarts[i], spanEnds[i], spanFlag)
+ }
+
+ // Mark added text as autocomplete text.
+ for (span in autoCompleteSpans!!) {
+ text.setSpan(span, textLength, resultLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+
+ // Hide the cursor.
+ isCursorVisible = false
+
+ // Make sure the autocomplete text is visible. If the autocomplete text is too
+ // long, it would appear the cursor will be scrolled out of view. However, this
+ // is not the case in practice, because EditText still makes sure the cursor is
+ // still in view.
+ bringPointIntoView(resultLength)
+
+ endSettingAutocomplete()
+ }
+
+ override fun noAutocompleteResult() {
+ removeAutocomplete(text)
+ }
+
+ /**
+ * Code to handle deleting autocomplete first when backspacing.
+ * If there is no autocomplete text, both removeAutocomplete() and commitAutocomplete()
+ * are no-ops and return false. Therefore we can use them here without checking explicitly
+ * if we have autocomplete text or not.
+ *
+ * Also turns off text prediction for private mode tabs.
+ */
+ @SuppressWarnings("ComplexMethod")
+ override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
+ val ic = super.onCreateInputConnection(outAttrs) ?: return null
+
+ return object : InputConnectionWrapper(ic, false) {
+ override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
+ if (removeAutocomplete(text)) {
+ // If we have autocomplete text, the cursor is at the boundary between
+ // regular and autocomplete text. So regardless of which direction we
+ // are deleting, we should delete the autocomplete text first.
+ //
+ // On Amazon Echo Show devices, restarting input prevents us from backspacing
+ // the last few characters of autocomplete: #911. However, on non-Echo devices,
+ // not restarting input will cause the keyboard to desync when backspacing: #1489.
+ if (!isAmazonEchoShowKeyboard) {
+ restartInput()
+ }
+ return false
+ }
+ return super.deleteSurroundingText(beforeLength, afterLength)
+ }
+
+ // available on API level 24+
+ override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
+ if (removeAutocomplete(text)) {
+ restartInput()
+ return false
+ }
+ return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
+ }
+
+ /**
+ * Optionally remove the current autocompletion depending on the new [text].
+ *
+ * Cases in which the autocompletion will be removed:
+ * - if the user pressed the backspace to remove the autocompletion or
+ * - if the user modified their input such that the autocompletion does not apply anymore.
+ *
+ * @return `true` if this method consumed the user input, `false` otherwise.
+ */
+ @Suppress("ComplexCondition")
+ private fun removeAutocompleteOnComposing(text: CharSequence): Boolean {
+ val editable = getText()
+
+ // Remove the autocomplete text as soon as possible if not applicable anymore.
+ if (!editableText.startsWith(text) && removeAutocomplete(editable)) {
+ return false // If the user modified their input then allow the new text to be set.
+ }
+
+ val composingStart = BaseInputConnection.getComposingSpanStart(editable)
+ val composingEnd = BaseInputConnection.getComposingSpanEnd(editable)
+ // We only delete the autocomplete text when the user is backspacing,
+ // i.e. when the composing text is getting shorter.
+ if (composingStart >= 0 &&
+ composingEnd >= 0 &&
+ composingEnd - composingStart > text.length &&
+ removeAutocomplete(editable)
+ ) {
+ finishComposingText()
+ // Make the IME aware that we interrupted the setComposingText call,
+ // by calling restartInput()
+ restartInput()
+ return true
+ }
+ return false
+ }
+
+ override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean {
+ return if (removeAutocompleteOnComposing(text)) {
+ false
+ } else {
+ super.commitText(text, newCursorPosition)
+ }
+ }
+
+ override fun setComposingText(text: CharSequence, newCursorPosition: Int): Boolean {
+ return if (removeAutocompleteOnComposing(text)) {
+ false
+ } else {
+ super.setComposingText(text, newCursorPosition)
+ }
+ }
+ }
+ }
+
+ private fun restartInput() {
+ inputMethodManger?.restartInput(this)
+ }
+
+ private fun getCurrentInputMethod(): String {
+ val inputMethod = getString(context.contentResolver, DEFAULT_INPUT_METHOD)
+ return inputMethod ?: ""
+ }
+
+ /**
+ * This class watches for text changes and adds or removes autocomplete text accordingly.
+ * Using this class is preferred when making text changes as it will not interfere
+ * with any composing text at the same time as custom keyboards.
+ *
+ * Known issue: autocomplete will not be added when replacing the current text with one
+ * that has a text length equal to the one being replaced minus 1.
+ * */
+ private inner class TextChangeListener : TextWatcher {
+
+ /**
+ * Holds the value of the non-autocomplete text before any changes have been made.
+ * */
+ private var beforeChangedTextNonAutocomplete: String = ""
+
+ /**
+ * The number of characters that have been changed in [onTextChanged].
+ * When using keyboards that do not have their own text correction enabled
+ * and the user is pressing backspace this value will be 0.
+ * */
+ private var textChangedCount: Int = 0
+
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ if (!isEnabled || settingAutoComplete) return
+ beforeChangedTextNonAutocomplete = getNonAutocompleteText(text)
+ }
+
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ if (settingAutoComplete) return
+
+ // In this initial implementation, we do not include the changed text to minimize PII.
+ Fact(Component.UI_AUTOCOMPLETE, Action.IMPLEMENTATION_DETAIL, "onTextChanged", "InlineAutocompleteEditText")
+ .collect()
+
+ if (!isEnabled) return
+
+ textChangedCount = count
+ }
+
+ override fun afterTextChanged(editable: Editable) {
+ if (!isEnabled || settingAutoComplete) return
+
+ val afterNonAutocompleteText = getNonAutocompleteText(editable)
+
+ val hasTextShortenedByOne: Boolean =
+ beforeChangedTextNonAutocomplete.length == afterNonAutocompleteText.length + 1
+
+ // Covers both keyboards with text correction activated and those without.
+ val hasBackspaceBeenPressed =
+ textChangedCount == 0 || hasTextShortenedByOne
+
+ // No autocompleting when typing a search query
+ val afterTextIsSearch = afterNonAutocompleteText.contains(" ")
+
+ val hasTextBeenAdded: Boolean =
+ (
+ afterNonAutocompleteText.contains(beforeChangedTextNonAutocomplete) ||
+ beforeChangedTextNonAutocomplete.isEmpty()
+ ) &&
+ afterNonAutocompleteText.length > beforeChangedTextNonAutocomplete.length
+
+ var shouldAddAutocomplete: Boolean = hasTextBeenAdded || (!afterTextIsSearch && !hasBackspaceBeenPressed)
+
+ autoCompletePrefixLength = afterNonAutocompleteText.length
+
+ // If we are not autocompleting, we set discardAutoCompleteResult to true
+ // to discard any autocomplete results that are in-flight, and vice versa.
+ discardAutoCompleteResult = !shouldAddAutocomplete
+
+ if (!shouldAddAutocomplete) {
+ // Remove the old autocomplete text until any new autocomplete text gets added.
+ removeAutocomplete(editable)
+ } else {
+ // If this text already matches our autocomplete text, autocomplete likely
+ // won't change. Just reuse the old autocomplete value.
+ autocompleteResult?.takeIf { it.startsWith(afterNonAutocompleteText) }?.let {
+ applyAutocompleteResult(it)
+ shouldAddAutocomplete = false
+ }
+ }
+
+ // Update search icon with an active state since user is typing
+ searchStateChangeListener?.invoke(afterNonAutocompleteText.isNotEmpty())
+
+ if (shouldAddAutocomplete) {
+ filterListener?.invoke(afterNonAutocompleteText)
+ }
+
+ textChangeListener?.invoke(afterNonAutocompleteText, text.toString())
+ }
+ }
+
+ override fun dispatchKeyEventPreIme(event: KeyEvent?): Boolean {
+ return event?.let {
+ dispatchKeyEventPreImeListener?.invoke(it) ?: onKeyPreIme(it.keyCode, it)
+ } ?: super.dispatchKeyEventPreIme(event)
+ }
+
+ override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean {
+ return keyPreImeListener?.invoke(this, keyCode, event) ?: false
+ }
+
+ public override fun onSelectionChanged(selStart: Int, selEnd: Int) {
+ selectionChangedListener?.invoke(selStart, selEnd)
+ super.onSelectionChanged(selStart, selEnd)
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ windowFocusChangeListener?.invoke(hasFocus)
+ }
+
+ override fun onTextContextMenuItem(id: Int): Boolean {
+ // Ensure more control over what gets pasted from the framework floating menu.
+ // Behavior closely following the default implementation from TextView#onTextContextMenuItem().
+ if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) {
+ val selectionStart = selectionStart
+ val selectionEnd = selectionEnd
+
+ val min = 0.coerceAtLeast(selectionStart.coerceAtMost(selectionEnd))
+ val max = 0.coerceAtLeast(selectionStart.coerceAtLeast(selectionEnd))
+
+ if (id == android.R.id.pasteAsPlainText ||
+ (id == android.R.id.paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+ ) {
+ paste(min, max, false)
+ } else {
+ paste(min, max, true)
+ }
+
+ return true // action was performed
+ }
+
+ return callOnTextContextMenuItemSuper(id)
+ }
+
+ @Suppress("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ return if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M &&
+ event.actionMasked == MotionEvent.ACTION_UP
+ ) {
+ // Android 6 occasionally throws a NullPointerException inside Editor.onTouchEvent()
+ // for ACTION_UP when attempting to display (uninitialised) text handles. The Editor
+ // and TextView IME interactions are quite complex, so I don't know how to properly
+ // work around this issue, but we can at least catch the NPE to prevent crashing
+ // the whole app.
+ // (Editor tries to make both selection handles visible, but in certain cases they haven't
+ // been initialised yet, causing the NPE. It doesn't bother to check the selection handle
+ // state, and making some other calls to ensure the handles exist doesn't seem like a
+ // clean solution either since I don't understand most of the selection logic. This implementation
+ // only seems to exist in Android 6, both Android 5 and 7 have different implementations.)
+ try {
+ super.onTouchEvent(event)
+ } catch (ignored: NullPointerException) {
+ // Ignore this (see above) - since we're now in an unknown state let's clear all selection
+ // (which is still better than an arbitrary crash that we can't control):
+ clearFocus()
+ true
+ }
+ } else {
+ super.onTouchEvent(event)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun callOnTextContextMenuItemSuper(id: Int) = super.onTextContextMenuItem(id)
+
+ /**
+ * Paste clipboard content between min and max positions.
+ *
+ * Method matching TextView#paste() but which also strips unwanted schemes before actually pasting.
+ */
+ @Suppress("NestedBlockDepth")
+ @VisibleForTesting
+ internal fun paste(min: Int, max: Int, withFormatting: Boolean) {
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = clipboard.primaryClip
+
+ if (clip != null) {
+ var didFirst = false
+ for (i in 0 until clip.itemCount) {
+ val textToBePasted: CharSequence?
+ textToBePasted = if (withFormatting) {
+ clip.getItemAt(i).coerceToStyledText(context)
+ } else {
+ // Get an item as text and remove all spans by toString().
+ val text = clip.getItemAt(i).coerceToText(context)
+ (text as? Spanned)?.toString() ?: text
+ }
+
+ // Actually stripping unwanted schemes
+ val safeTextToBePasted = SafeUrl.stripUnsafeUrlSchemes(context, textToBePasted)
+
+ if (safeTextToBePasted != null) {
+ if (!didFirst) {
+ Selection.setSelection(editableText as Spannable?, max)
+ editableText.replace(min, max, safeTextToBePasted)
+ didFirst = true
+ } else {
+ editableText.insert(selectionEnd, "\n")
+ editableText.insert(selectionEnd, safeTextToBePasted)
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ internal val AUTOCOMPLETE_SPAN = NoCopySpan.Concrete()
+ internal const val DEFAULT_AUTOCOMPLETE_BACKGROUND_COLOR = 0xffb5007f.toInt()
+
+ // The Echo Show IME does not conflict with Fire TV: com.amazon.tv.ime/.FireTVIME
+ // However, it may be used by other Amazon keyboards. In theory, if they have the same IME
+ // ID, they should have similar behavior.
+ const val INPUT_METHOD_AMAZON_ECHO_SHOW = "com.amazon.bluestone.keyboard/.DictationIME"
+
+ /**
+ * Get the portion of text that is not marked as autocomplete text.
+ *
+ * @param text Current text content that may include autocomplete text
+ */
+ private fun getNonAutocompleteText(text: Editable): String {
+ val start = text.getSpanStart(AUTOCOMPLETE_SPAN)
+ return if (start < 0) {
+ // No autocomplete text; return the whole string.
+ text.toString()
+ } else {
+ // Only return the portion that's not autocomplete text
+ TextUtils.substring(text, 0, start)
+ }
+ }
+
+ private fun hasCompositionString(content: Editable): Boolean {
+ val spans = content.getSpans(0, content.length, Any::class.java)
+ return spans.any { span -> content.getSpanFlags(span) and Spanned.SPAN_COMPOSING != 0 }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/ui/autocomplete/src/main/res/values/attrs.xml b/mobile/android/android-components/components/ui/autocomplete/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000..2b23026264
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/src/main/res/values/attrs.xml
@@ -0,0 +1,9 @@
+<?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>
+ <declare-styleable name="InlineAutocompleteEditText">
+ <attr name="autocompleteBackgroundColor" format="color" />
+ </declare-styleable>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/autocomplete/src/test/java/mozilla/components/ui/autocomplete/InlineAutocompleteEditTextTest.kt b/mobile/android/android-components/components/ui/autocomplete/src/test/java/mozilla/components/ui/autocomplete/InlineAutocompleteEditTextTest.kt
new file mode 100644
index 0000000000..d455b0a28e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/src/test/java/mozilla/components/ui/autocomplete/InlineAutocompleteEditTextTest.kt
@@ -0,0 +1,585 @@
+/* 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.ui.autocomplete
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.os.Build
+import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+import android.view.KeyEvent
+import android.view.ViewParent
+import android.view.accessibility.AccessibilityEvent
+import android.view.inputmethod.BaseInputConnection
+import android.view.inputmethod.EditorInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.autocomplete.InlineAutocompleteEditText.AutocompleteResult
+import mozilla.components.ui.autocomplete.InlineAutocompleteEditText.Companion.AUTOCOMPLETE_SPAN
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric.buildAttributeSet
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class InlineAutocompleteEditTextTest {
+
+ private val attributes = buildAttributeSet().build()
+
+ @Test
+ fun autoCompleteResult() {
+ val result = AutocompleteResult("testText", "testSource", 1)
+ assertEquals("testText", result.text)
+ assertEquals("testSource", result.source)
+ assertEquals(1, result.totalItems)
+ }
+
+ @Test
+ fun getNonAutocompleteText() {
+ val et = InlineAutocompleteEditText(testContext)
+ et.setText("Test")
+ assertEquals("Test", et.nonAutocompleteText)
+
+ et.text.setSpan(AUTOCOMPLETE_SPAN, 2, 3, SPAN_EXCLUSIVE_EXCLUSIVE)
+ assertEquals("Te", et.nonAutocompleteText)
+
+ et.text.setSpan(AUTOCOMPLETE_SPAN, 0, 3, SPAN_EXCLUSIVE_EXCLUSIVE)
+ assertEquals("", et.nonAutocompleteText)
+ }
+
+ @Test
+ fun getOriginalText() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.setText("Test")
+ assertEquals("Test", et.originalText)
+
+ et.text.setSpan(AUTOCOMPLETE_SPAN, 2, 3, SPAN_EXCLUSIVE_EXCLUSIVE)
+ assertEquals("Test", et.originalText)
+
+ et.text.setSpan(AUTOCOMPLETE_SPAN, 0, 3, SPAN_EXCLUSIVE_EXCLUSIVE)
+ assertEquals("Test", et.originalText)
+ }
+
+ @Test
+ fun onFocusChange() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ val searchStates = mutableListOf<Boolean>()
+
+ et.setOnSearchStateChangeListener { b: Boolean -> searchStates.add(searchStates.size, b) }
+ et.onFocusChanged(false, 0, null)
+
+ et.setText("text")
+ et.text.setSpan(AUTOCOMPLETE_SPAN, 0, 3, SPAN_EXCLUSIVE_EXCLUSIVE)
+ et.onFocusChanged(false, 0, null)
+ assertTrue(et.text.isEmpty())
+
+ et.setText("text")
+ et.text.setSpan(AUTOCOMPLETE_SPAN, 0, 3, SPAN_EXCLUSIVE_EXCLUSIVE)
+ et.onFocusChanged(true, 0, null)
+ assertFalse(et.text.isEmpty())
+ assertEquals(listOf(false, true, true), searchStates)
+ }
+
+ @Test
+ fun sendAccessibilityEventUnchecked() {
+ val et = spy(InlineAutocompleteEditText(testContext, attributes))
+ doReturn(false).`when`(et).isShown
+ doReturn(mock(ViewParent::class.java)).`when`(et).parent
+
+ val event = AccessibilityEvent()
+ event.eventType = AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
+ et.sendAccessibilityEventUnchecked(event)
+
+ verify(et).onInitializeAccessibilityEvent(event)
+ verify(et).dispatchPopulateAccessibilityEvent(event)
+ verify(et.parent).requestSendAccessibilityEvent(et, event)
+ }
+
+ @Test
+ fun onAutocompleteSetsEmptyResult() {
+ val et = spy(InlineAutocompleteEditText(testContext, attributes))
+
+ doReturn(false).`when`(et).isEnabled
+ et.applyAutocompleteResult(AutocompleteResult("text", "source", 1))
+ assertNull(et.autocompleteResult)
+ }
+
+ @Test
+ fun onAutocompleteDiscardsStaleResult() {
+ val et = spy(InlineAutocompleteEditText(testContext, attributes))
+ doReturn(true).`when`(et).isEnabled
+ et.setText("text")
+
+ et.applyAutocompleteResult(AutocompleteResult("stale result", "source", 1))
+ assertEquals("text", et.text.toString())
+
+ et.text.setSpan(AUTOCOMPLETE_SPAN, 1, 3, SPAN_EXCLUSIVE_EXCLUSIVE)
+ et.applyAutocompleteResult(AutocompleteResult("stale result", "source", 1))
+ assertEquals("text", et.text.toString())
+ }
+
+ @Test
+ fun onAutocompleteReplacesExistingAutocompleteText() {
+ val et = spy(InlineAutocompleteEditText(testContext, attributes))
+ doReturn(true).`when`(et).isEnabled
+
+ et.setText("text")
+ et.text.setSpan(AUTOCOMPLETE_SPAN, 1, 3, SPAN_EXCLUSIVE_EXCLUSIVE)
+ et.applyAutocompleteResult(AutocompleteResult("text completed", "source", 1))
+ assertEquals("text completed", et.text.toString())
+ }
+
+ @Test
+ fun onAutocompleteAppendsExistingText() {
+ val et = spy(InlineAutocompleteEditText(testContext, attributes))
+ doReturn(true).`when`(et).isEnabled
+
+ et.setText("text")
+ et.applyAutocompleteResult(AutocompleteResult("text completed", "source", 1))
+ assertEquals("text completed", et.text.toString())
+ }
+
+ @Test
+ fun onAutocompleteSetsSpan() {
+ val et = spy(InlineAutocompleteEditText(testContext, attributes))
+ doReturn(true).`when`(et).isEnabled
+
+ et.setText("text")
+ et.applyAutocompleteResult(AutocompleteResult("text completed", "source", 1))
+
+ assertEquals(4, et.text.getSpanStart(AUTOCOMPLETE_SPAN))
+ assertEquals(14, et.text.getSpanEnd(AUTOCOMPLETE_SPAN))
+ assertEquals(SPAN_EXCLUSIVE_EXCLUSIVE, et.text.getSpanFlags(AUTOCOMPLETE_SPAN))
+ }
+
+ @Test
+ fun onKeyPreImeListenerInvocation() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ var invokedWithParams: List<Any>? = null
+ et.setOnKeyPreImeListener { p1, p2, p3 ->
+ invokedWithParams = listOf(p1, p2, p3)
+ true
+ }
+ val event = mock(KeyEvent::class.java)
+ et.onKeyPreIme(1, event)
+ assertEquals(listOf(et, 1, event), invokedWithParams)
+ }
+
+ @Test
+ fun onSelectionChangedListenerInvocation() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ var invokedWithParams: List<Any>? = null
+ et.setOnSelectionChangedListener { p1, p2 ->
+ invokedWithParams = listOf(p1, p2)
+ }
+ et.onSelectionChanged(0, 1)
+ assertEquals(listOf(0, 1), invokedWithParams)
+ }
+
+ @Test
+ fun onSelectionChangedCommitsResult() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.onAttachedToWindow()
+
+ et.setText("text")
+ et.applyAutocompleteResult(AutocompleteResult("text completed", "source", 1))
+ assertEquals(4, et.text.getSpanStart(AUTOCOMPLETE_SPAN))
+
+ et.onSelectionChanged(4, 14)
+ assertEquals(-1, et.text.getSpanStart(AUTOCOMPLETE_SPAN))
+ }
+
+ @Test
+ fun onWindowFocusChangedListenerInvocation() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ var invokedWithParams: List<Any>? = null
+ et.setOnWindowsFocusChangeListener { p1 ->
+ invokedWithParams = listOf(p1)
+ }
+ et.onWindowFocusChanged(true)
+ assertEquals(listOf(true), invokedWithParams)
+ }
+
+ @Test
+ fun onCommitListenerInvocation() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ var invoked = false
+ et.setOnCommitListener { invoked = true }
+ et.onAttachedToWindow()
+
+ et.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
+ assertTrue(invoked)
+ }
+
+ @Test
+ fun onTextChangeListenerInvocation() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ var invokedWithParams: List<Any>? = null
+ et.setOnTextChangeListener { p1, p2 ->
+ invokedWithParams = listOf(p1, p2)
+ }
+ et.onAttachedToWindow()
+
+ et.setText("text")
+ assertEquals(listOf("text", "text"), invokedWithParams)
+ }
+
+ @Test
+ fun onSearchStateChangeListenerInvocation() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.onAttachedToWindow()
+
+ var invokedWithParams: List<Any>? = null
+ et.setOnSearchStateChangeListener { p1 ->
+ invokedWithParams = listOf(p1)
+ }
+
+ et.setText("")
+ assertEquals(listOf(false), invokedWithParams)
+
+ et.setText("text")
+ assertEquals(listOf(true), invokedWithParams)
+ }
+
+ @Test
+ fun onFilterListenerInvocation() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.onAttachedToWindow()
+
+ var lastInvokedWithText: String? = null
+ var invokedCounter = 0
+ et.setOnFilterListener { p1 ->
+ lastInvokedWithText = p1
+ invokedCounter++
+ }
+
+ // Already have an autocomplete result, and setting a text to the same value as the result.
+ et.applyAutocompleteResult(AutocompleteResult("text", "source", 1))
+ et.setText("text")
+ // Autocomplete filter shouldn't have been called, because we already have a matching result.
+ assertEquals(0, invokedCounter)
+
+ et.setText("text")
+ assertEquals(1, invokedCounter)
+ assertEquals("text", lastInvokedWithText)
+
+ // Test backspace. We don't expect autocomplete to have been called.
+ et.setText("tex")
+ assertEquals(1, invokedCounter)
+
+ // Presence of a space is counted as a 'search query', we don't autocomplete those.
+ et.setText("search term")
+ assertEquals(1, invokedCounter)
+
+ // Empty text isn't autocompleted either.
+ et.setText("")
+ assertEquals(1, invokedCounter)
+
+ // Autocomplete for the first letter
+ et.setText("t")
+ assertEquals(2, invokedCounter)
+ et.applyAutocompleteResult(AutocompleteResult("text", "source", 1))
+
+ // Autocomplete should be called for the next letter that doesn't match the result
+ et.setText("ta")
+ assertEquals(3, invokedCounter)
+ }
+
+ @Test
+ fun `GIVEN an autocomplete listener WHEN asked to refresh autocomplete suggestions THEN restart the autocomplete functionality with the curret text`() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.onAttachedToWindow()
+ et.setText("Test")
+ var lastInvokedWithText: String? = null
+ var invokedCounter = 0
+ et.setOnFilterListener { p1 ->
+ lastInvokedWithText = p1
+ invokedCounter++
+ }
+
+ et.refreshAutocompleteSuggestions()
+
+ assertEquals("Test", lastInvokedWithText)
+ assertEquals(1, invokedCounter)
+ }
+
+ @Test
+ fun onCreateInputConnection() {
+ val et = spy(InlineAutocompleteEditText(testContext, attributes))
+ val icw = et.onCreateInputConnection(mock(EditorInfo::class.java))
+ doReturn(true).`when`(et).isEnabled
+
+ et.setText("text")
+ et.applyAutocompleteResult(AutocompleteResult("text completed", "source", 1))
+ assertEquals("text completed", et.text.toString())
+
+ icw?.deleteSurroundingText(0, 1)
+ assertNull(et.autocompleteResult)
+ assertEquals("text", et.text.toString())
+
+ et.applyAutocompleteResult(AutocompleteResult("text completed", "source", 1))
+ assertEquals("text completed", et.text.toString())
+
+ BaseInputConnection.setComposingSpans(et.text)
+ icw?.commitText("text", 4)
+ assertNull(et.autocompleteResult)
+ assertEquals("text", et.text.toString())
+
+ et.applyAutocompleteResult(AutocompleteResult("text completed", "source", 1))
+ assertEquals("text completed", et.text.toString())
+
+ BaseInputConnection.setComposingSpans(et.text)
+ icw?.setComposingText("text", 4)
+ assertNull(et.autocompleteResult)
+ assertEquals("text", et.text.toString())
+ }
+
+ @Test
+ fun removeAutocompleteOnComposing() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ val ic = et.onCreateInputConnection(mock(EditorInfo::class.java))
+
+ ic?.setComposingText("text", 1)
+ assertEquals("text", et.text.toString())
+
+ et.applyAutocompleteResult(AutocompleteResult("text completed", "source", 1))
+ assertEquals("text completed", et.text.toString())
+
+ // Simulating a backspace which should remove the autocomplete and leave original text
+ ic?.setComposingText("tex", 1)
+ assertEquals("text", et.text.toString())
+
+ // Verify that we finished composing
+ assertEquals(-1, BaseInputConnection.getComposingSpanStart(et.text))
+ assertEquals(-1, BaseInputConnection.getComposingSpanEnd(et.text))
+ }
+
+ @Test
+ fun `GIVEN the current text contains an autocompletion WHEN a new character does not match the autocompletion THEN remove the autocompletion`() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ val ic = et.onCreateInputConnection(mock(EditorInfo::class.java))
+
+ ic?.setComposingText("mo", 1)
+ assertEquals("mo", et.text.toString())
+
+ et.applyAutocompleteResult(AutocompleteResult("mozilla", "source", 1))
+ assertEquals("mozilla", et.text.toString())
+
+ // Simulating the user entering a new character which makes the current autocomplete invalid
+ ic?.setComposingText("mod", 1)
+ assertEquals("mod", et.text.toString())
+
+ // Verify that autocompletion works for the new text
+ et.applyAutocompleteResult(AutocompleteResult("moderator", "source", 1))
+ assertEquals("moderator", et.text.toString())
+ }
+
+ @Test
+ fun `GIVEN empty edit field WHEN text 'g' added THEN autocomplete to google`() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.setText("")
+ et.onAttachedToWindow()
+
+ et.autocompleteResult = AutocompleteResult(
+ text = "google.com",
+ source = "test-source",
+ totalItems = 100,
+ )
+
+ et.setText("g")
+ assertEquals("google.com", "${et.text}")
+ }
+
+ @Test
+ fun `GIVEN empty edit field WHEN text 'g ' added THEN don't autocomplete to google`() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.setText("")
+ et.onAttachedToWindow()
+
+ et.autocompleteResult = AutocompleteResult(
+ text = "google.com",
+ source = "test-source",
+ totalItems = 100,
+ )
+
+ et.setText("g ")
+ assertEquals("g ", "${et.text}")
+ }
+
+ @Test
+ fun `GIVEN field with 'google' WHEN backspacing THEN doesn't autocomplete`() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.setText("google")
+ et.onAttachedToWindow()
+
+ et.autocompleteResult = AutocompleteResult(
+ text = "google.com",
+ source = "test-source",
+ totalItems = 100,
+ )
+
+ et.setText("googl")
+ assertEquals("googl", "${et.text}")
+ }
+
+ @Test
+ fun `GIVEN field with selected text WHEN text 'g' added THEN autocomplete to google`() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.setText("testestest")
+ et.selectAll()
+ et.onAttachedToWindow()
+ et.autocompleteResult = AutocompleteResult(
+ text = "google.com",
+ source = "test-source",
+ totalItems = 100,
+ )
+
+ et.setText("g")
+ assertEquals("google.com", "${et.text}")
+ }
+
+ @Test
+ fun `GIVEN field with selected text 'google ' WHEN text 'g' added THEN autocomplete to google`() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.setText("https://www.google.com/")
+ et.selectAll()
+ et.onAttachedToWindow()
+ et.autocompleteResult = AutocompleteResult(
+ text = "google.com",
+ source = "test-source",
+ totalItems = 100,
+ )
+
+ et.setText("g")
+ assertEquals("google.com", "${et.text}")
+ }
+
+ @Test
+ fun `WHEN setting text THEN isEnabled is never modified`() {
+ val et = spy(InlineAutocompleteEditText(testContext, attributes))
+ et.setText("", shouldAutoComplete = false)
+ // assigning here so it verifies the setter, not the getter
+ verify(et, never()).isEnabled = true
+ }
+
+ @Test
+ fun `WHEN onTextContextMenuItem is called for options other than paste THEN we should not paste() and just call super`() {
+ val editText = spy(InlineAutocompleteEditText(testContext, attributes))
+
+ editText.onTextContextMenuItem(android.R.id.copy)
+ editText.onTextContextMenuItem(android.R.id.shareText)
+ editText.onTextContextMenuItem(android.R.id.cut)
+ editText.onTextContextMenuItem(android.R.id.selectAll)
+
+ verify(editText, never()).paste(anyInt(), anyInt(), anyBoolean())
+ verify(editText, times(4)).callOnTextContextMenuItemSuper(anyInt())
+ }
+
+ @Test
+ fun `WHEN onTextContextMenuItem is called for paste THEN we should paste() and not call super`() {
+ val editText = spy(InlineAutocompleteEditText(testContext, attributes))
+
+ editText.onTextContextMenuItem(android.R.id.paste)
+
+ verify(editText).paste(anyInt(), anyInt(), anyBoolean())
+ verify(editText, never()).callOnTextContextMenuItemSuper(anyInt())
+ }
+
+ @Test
+ fun `WHEN onTextContextMenuItem is called for pasteAsPlainText THEN we should paste() and not call super`() {
+ val editText = spy(InlineAutocompleteEditText(testContext, attributes))
+
+ editText.onTextContextMenuItem(android.R.id.pasteAsPlainText)
+
+ verify(editText).paste(anyInt(), anyInt(), anyBoolean())
+ verify(editText, never()).callOnTextContextMenuItemSuper(anyInt())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1])
+ fun `GIVEN an Android L device, WHEN onTextContextMenuItem is called for paste THEN we should paste() with formatting`() {
+ val editText = spy(InlineAutocompleteEditText(testContext, attributes))
+
+ editText.onTextContextMenuItem(android.R.id.paste)
+
+ verify(editText).paste(anyInt(), anyInt(), eq(true))
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.M, Build.VERSION_CODES.N, Build.VERSION_CODES.O, Build.VERSION_CODES.P])
+ fun `GIVEN an Android M device, WHEN onTextContextMenuItem is called for paste THEN we should paste() without formatting`() {
+ val editText = spy(InlineAutocompleteEditText(testContext, attributes))
+
+ editText.onTextContextMenuItem(android.R.id.paste)
+
+ verify(editText).paste(anyInt(), anyInt(), eq(false))
+ }
+
+ @Test
+ fun `GIVEN no previous text WHEN paste is selected THEN paste() should be called with 0,0`() {
+ val editText = spy(InlineAutocompleteEditText(testContext, attributes))
+
+ editText.onTextContextMenuItem(android.R.id.paste)
+
+ verify(editText).paste(eq(0), eq(0), eq(false))
+ }
+
+ @Test
+ fun `GIVEN 5 chars previous text WHEN paste is selected THEN paste() should be called with 0,5`() {
+ val editText = spy(InlineAutocompleteEditText(testContext, attributes))
+ editText.setText("chars")
+ editText.selectAll()
+
+ editText.onTextContextMenuItem(android.R.id.paste)
+
+ verify(editText).paste(eq(0), eq(5), eq(false))
+ }
+
+ @Test
+ fun `WHEN paste() is called with new text THEN we will display the new text`() {
+ val editText = spy(InlineAutocompleteEditText(testContext, attributes))
+ (testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).apply {
+ setPrimaryClip(ClipData.newPlainText("Test", "test"))
+ }
+
+ assertEquals("", editText.text.toString())
+
+ editText.paste(0, 0, false)
+
+ assertEquals("test", editText.text.toString())
+ }
+
+ fun `WHEN committing autocomplete THEN textChangedListener is invoked`() {
+ val et = InlineAutocompleteEditText(testContext, attributes)
+ et.setText("")
+
+ et.onAttachedToWindow()
+ et.autocompleteResult = AutocompleteResult(
+ text = "google.com",
+ source = "test-source",
+ totalItems = 100,
+ )
+ et.setText("g")
+ var callbackInvoked = false
+ et.setOnTextChangeListener { _, _ ->
+ callbackInvoked = true
+ }
+ et.setSelection(3)
+ assertTrue(callbackInvoked)
+ }
+}
diff --git a/mobile/android/android-components/components/ui/autocomplete/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/ui/autocomplete/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/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/ui/autocomplete/src/test/resources/robolectric.properties b/mobile/android/android-components/components/ui/autocomplete/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/autocomplete/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/ui/colors/README.md b/mobile/android/android-components/components/ui/colors/README.md
new file mode 100644
index 0000000000..2b7855fee9
--- /dev/null
+++ b/mobile/android/android-components/components/ui/colors/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > UI > Colors
+
+The standard set of [Photon](https://design.firefox.com/photon/) colors.
+
+## Usage
+
+### 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:ui-colors:{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/ui/colors/build.gradle b/mobile/android/android-components/components/ui/colors/build.gradle
new file mode 100644
index 0000000000..865b113e50
--- /dev/null
+++ b/mobile/android/android-components/components/ui/colors/build.gradle
@@ -0,0 +1,40 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.ui.colors'
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation ComponentsDependencies.androidx_compose_ui
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/colors/proguard-rules.pro b/mobile/android/android-components/components/ui/colors/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/colors/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/ui/colors/src/main/AndroidManifest.xml b/mobile/android/android-components/components/ui/colors/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/ui/colors/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/ui/colors/src/main/java/mozilla/components/ui/colors/PhotonColors.kt b/mobile/android/android-components/components/ui/colors/src/main/java/mozilla/components/ui/colors/PhotonColors.kt
new file mode 100644
index 0000000000..214070deed
--- /dev/null
+++ b/mobile/android/android-components/components/ui/colors/src/main/java/mozilla/components/ui/colors/PhotonColors.kt
@@ -0,0 +1,178 @@
+/* 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.ui.colors
+
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Colors from the [Photon Design System](https://design.firefox.com/photon/visuals/color.html).
+ *
+ * _"Firefox colors are bold, vibrant and attractive. They enhance the experience by providing visual
+ * clues and by bringing attention to primary actions."_
+ */
+object PhotonColors {
+ // Firefox Blue is one of our primary colors. We use blue as accent color for highlighting buttons,
+ // links and active states like the current tab in Firefox for desktop.
+ val Blue05 = Color(0xFFAAF2FF)
+ val Blue10 = Color(0xFF80EBFF)
+ val Blue20 = Color(0xFF00DDFF)
+ val Blue30 = Color(0xFF00B3F4)
+ val Blue40 = Color(0xFF0090ED)
+ val Blue50 = Color(0xFF0060DF)
+ val Blue50A44 = Blue50.copy(alpha = 0.44f)
+ val Blue50A80 = Blue50.copy(alpha = 0.8f)
+ val Blue60 = Color(0xFF0250BB)
+ val Blue70 = Color(0xFF054096)
+ val Blue80 = Color(0xFF073072)
+ val Blue90 = Color(0xFF09204D)
+
+ // Green is primarily used for positive / success / update status in user interface.
+ val Green05 = Color(0xFFE3FFF3)
+ val Green10 = Color(0xFFD1FFEE)
+ val Green20 = Color(0xFFB3FFE3)
+ val Green30 = Color(0xFF87FFD1)
+ val Green40 = Color(0xFF54FFBD)
+ val Green50 = Color(0xFF3FE1B0)
+ val Green60 = Color(0xFF2AC3A2)
+ val Green70 = Color(0xFF008787)
+ val Green80 = Color(0xFF005E5E)
+ val Green90 = Color(0xFF08403F)
+
+ // Red is primarily used for attention / error status or a destructive action in user interface.
+ val Red05 = Color(0xFFFFDFE7)
+ val Red10 = Color(0xFFFFBDC5)
+ val Red20 = Color(0xFFFF9AA2)
+ val Red30 = Color(0xFFFF848B)
+ val Red40 = Color(0xFFFF6A75)
+ val Red50 = Color(0xFFFF4F5E)
+ val Red60 = Color(0xFFE22850)
+ val Red70 = Color(0xFFC50042)
+ val Red80 = Color(0xFF810220)
+ val Red90 = Color(0xFF440306)
+
+ // Yellow is primarily used for attention / caution / warning status in user interface.
+ val Yellow05 = Color(0xFFFFFFCC)
+ val Yellow10 = Color(0xFFFFFF98)
+ val Yellow20 = Color(0xFFFFEA80)
+ val Yellow30 = Color(0xFFFFD567)
+ val Yellow40 = Color(0xFFFFBD4F)
+ val Yellow40A41 = Yellow40.copy(alpha = .41f)
+ val Yellow50 = Color(0xFFFFA436)
+ val Yellow60 = Color(0xFFE27F2E)
+ val Yellow60A40 = Yellow60.copy(alpha = 0.4f)
+ val Yellow70 = Color(0xFFC45A27)
+ val Yellow70A77 = Yellow70.copy(alpha = 0.77f)
+ val Yellow80 = Color(0xFFA7341F)
+ val Yellow90 = Color(0xFF960E18)
+
+ // Purple is commonly used to indicate privacy.
+ val Purple05 = Color(0xFFF7E2FF)
+ val Purple10 = Color(0xFFF6B8FF)
+ val Purple20 = Color(0xFFF68FFF)
+ val Purple30 = Color(0xFFF770FF)
+ val Purple40 = Color(0xFFD74CF0)
+ val Purple50 = Color(0xFFB833E1)
+ val Purple60 = Color(0xFF952BB9)
+ val Purple70 = Color(0xFF722291)
+ val Purple80 = Color(0xFF4E1A69)
+ val Purple90 = Color(0xFF2B1141)
+
+ // Firefox Orange is only used for branding. Do not use it otherwise!
+ val Orange05 = Color(0xFFFFF4DE)
+ val Orange10 = Color(0xFFFFD5B2)
+ val Orange20 = Color(0xFFFFB587)
+ val Orange30 = Color(0xFFFFA266)
+ val Orange40 = Color(0xFFFF8A50)
+ val Orange50 = Color(0xFFFF7139)
+ val Orange60 = Color(0xFFE25920)
+ val Orange70 = Color(0xFFCC3D00)
+ val Orange80 = Color(0xFF9E280B)
+ val Orange90 = Color(0xFF7C1504)
+
+ // Pink is only used for Firefox Focus. Do not use it otherwise!
+ val Pink05 = Color(0xFFFFDEF0)
+ val Pink10 = Color(0xFFFFB4DB)
+ val Pink20 = Color(0xFFFF8AC5)
+ val Pink30 = Color(0xFFFF6BBA)
+ val Pink40 = Color(0xFFFF4AA2)
+ val Pink50 = Color(0xFFFF298A)
+ val Pink60 = Color(0xFFE21587)
+ val Pink70 = Color(0xFFC60084)
+ val Pink70A69 = Pink70.copy(alpha = 0.69f)
+ val Pink80 = Color(0xFF7F145B)
+ val Pink90 = Color(0xff50134b)
+
+ // Firefox Ink is commonly used for Firefox branding and new product websites.
+ val Ink05 = Color(0xFF393473)
+ val Ink10 = Color(0xFF342F6D)
+ val Ink20 = Color(0xFF312A64)
+ val Ink20A48 = Color(0x7A312A64)
+ val Ink20A50 = Color(0x80312A64)
+ val Ink30 = Color(0xFF2E255D)
+ val Ink40 = Color(0xFF2B2156)
+ val Ink50 = Color(0xFF291D4F)
+ val Ink60 = Color(0xFF271948)
+ val Ink70 = Color(0xFF241541)
+ val Ink80 = Color(0xFF20123A)
+ val Ink80A96 = Color(0xF520123A)
+ val Ink90 = Color(0xFF1D1133)
+
+ // Light grey should primarily be used for the Light Theme and secondary buttons.
+ val LightGrey05 = Color(0xFFFBFBFE)
+ val LightGrey05A40 = Color(0x66FBFBFE)
+ val LightGrey05A60 = Color(0x99FBFBFE)
+ val LightGrey10 = Color(0xFFF9F9FB)
+ val LightGrey20 = Color(0xFFF0F0F4)
+ val LightGrey30 = Color(0xFFE0E0E6)
+ val LightGrey40 = Color(0xFFCFCFD8)
+ val LightGrey50 = Color(0xFFBFBFC9)
+ val LightGrey60 = Color(0xFFAFAFBA)
+ val LightGrey70 = Color(0xFF9F9FAD)
+ val LightGrey80 = Color(0xFF8F8F9D)
+ val LightGrey90 = Color(0xFF80808E)
+
+ // Dark grey should primarily be used for the Dark theme and secondary buttons.
+ val DarkGrey05 = Color(0xFF5B5B66)
+ val DarkGrey05A45 = Color(0x735B5B66)
+ val DarkGrey10 = Color(0xFF52525E)
+ val DarkGrey20 = Color(0xFF4A4A55)
+ val DarkGrey30 = Color(0xFF42414D)
+ val DarkGrey30A95 = Color(0xF242414D)
+ val DarkGrey30A96 = Color(0xF542414D)
+ val DarkGrey40 = Color(0xFF3A3944)
+ val DarkGrey50 = Color(0xFF32313C)
+ val DarkGrey60 = Color(0xFF2B2A33)
+ val DarkGrey70 = Color(0xFF23222B)
+ val DarkGrey80 = Color(0xFF1C1B22)
+ val DarkGrey90 = Color(0xFF15141A)
+ val DarkGrey90A40 = Color(0x6615141A)
+ val DarkGrey90A60 = Color(0x9915141A)
+ val DarkGrey90A95 = Color(0xF215141A)
+ val DarkGrey90A96 = Color(0xF515141A)
+
+ // Violet
+ val Violet05 = Color(0xFFE7DFFF)
+ val Violet10 = Color(0xFFD9BFFF)
+ val Violet20 = Color(0xFFCB9EFF)
+ val Violet20A60 = Color(0x99CB9EFF)
+ val Violet30 = Color(0xFFC689FF)
+ val Violet40 = Color(0xFFAB71FF)
+ val Violet40A12 = Color(0x1FAB71FF)
+ val Violet40A30 = Color(0x4DAB71FF)
+ val Violet50 = Color(0xFF9059FF)
+ val Violet50A32 = Color(0x529059FF)
+ val Violet50A48 = Color(0x7A9059FF)
+ val Violet60 = Color(0xFF7542E5)
+ val Violet60A50 = Color(0x807542E5)
+ val Violet70 = Color(0xFF592ACB)
+ val Violet70A12 = Color(0x1F592ACB)
+ val Violet70A80 = Color(0xCC592ACB)
+ val Violet80 = Color(0xFF45278D)
+ val Violet90 = Color(0xFF321C64)
+ val Violet90A20 = Color(0x33321C64)
+
+ val White = Color(0xFFFFFFFF)
+ val Black = Color(0xFF000000)
+}
diff --git a/mobile/android/android-components/components/ui/colors/src/main/res/values/photon_colors.xml b/mobile/android/android-components/components/ui/colors/src/main/res/values/photon_colors.xml
new file mode 100644
index 0000000000..c873639d17
--- /dev/null
+++ b/mobile/android/android-components/components/ui/colors/src/main/res/values/photon_colors.xml
@@ -0,0 +1,219 @@
+<?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 xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources">
+ <!--
+ Firefox colors are bold, vibrant and attractive. They enhance the experience by providing
+ visual clues and by bringing attention to primary actions.
+
+ The color palette includes primary and secondary colors that can be used for interfaces and illustrations.
+
+ https://design.firefox.com/photon/visuals/color.html
+ -->
+
+ <!--
+ Blue
+ Firefox Blue is one of our primary colors. We use blue as accent color for highlighting buttons, links and active states like the current tab in Firefox for desktop.
+ -->
+ <color name="photonBlue05">#ffaaf2ff</color>
+ <color name="photonBlue10">#ff80ebff</color>
+ <color name="photonBlue20">#ff00ddff</color>
+ <color name="photonBlue30">#ff00b3f4</color>
+ <color name="photonBlue40">#ff0090ed</color>
+ <color name="photonBlue50">#ff0060df</color>
+ <color name="photonBlue50A44">#700060df</color>
+ <color name="photonBlue50A80">#cc0060df</color>
+ <color name="photonBlue60">#ff0250bb</color>
+ <color name="photonBlue70">#ff054096</color>
+ <color name="photonBlue80">#ff073072</color>
+ <color name="photonBlue90">#ff09204d</color>
+
+ <!--
+ Magenta
+ Firefox Magenta is one of our primary colors. We currently have no common usage for it.
+
+ In the future, Magenta should be redefined into Pink wherever possible.
+ -->
+ <color name="photonMagenta50">#ffff1ad9</color>
+ <color name="photonMagenta60">#ffed00b5</color>
+ <color name="photonMagenta70">#ffb5007f</color>
+ <color name="photonMagenta80">#ff7d004f</color>
+ <color name="photonMagenta90">#ff440027</color>
+
+ <!-- Pink -->
+ <color name="photonPink05">#ffffdef0</color>
+ <color name="photonPink10">#ffffb4db</color>
+ <color name="photonPink20">#ffff8ac5</color>
+ <color name="photonPink30">#ffff6bba</color>
+ <color name="photonPink40">#ffff4aa2</color>
+ <color name="photonPink50">#ffff298a</color>
+ <color name="photonPink60">#ffe21587</color>
+ <color name="photonPink70">#ffc60084</color>
+ <color name="photonPink70A69">#b0c60084</color>
+ <color name="photonPink80">#ff7f145b</color>
+ <color name="photonPink90">#ff50134b</color>
+
+ <!-- Green -->
+ <color name="photonGreen05">#ffe3fff3</color>
+ <color name="photonGreen10">#ffd1ffee</color>
+ <color name="photonGreen20">#ffb3ffe3</color>
+ <color name="photonGreen30">#ff87ffd1</color>
+ <color name="photonGreen40">#ff54ffbd</color>
+ <color name="photonGreen50">#ff3fe1b0</color>
+ <color name="photonGreen60">#ff2ac3a2</color>
+ <color name="photonGreen70">#ff008787</color>
+ <color name="photonGreen80">#ff005e5e</color>
+ <color name="photonGreen90">#ff08403f</color>
+
+ <!-- Red -->
+ <color name="photonRed05">#ffffdfe7</color>
+ <color name="photonRed10">#ffffbdc5</color>
+ <color name="photonRed20">#ffff9aa2</color>
+ <color name="photonRed30">#ffff848b</color>
+ <color name="photonRed40">#ffff6a75</color>
+ <color name="photonRed50">#ffff4f5e</color>
+ <color name="photonRed60">#ffe22850</color>
+ <color name="photonRed70">#ffc50042</color>
+ <color name="photonRed80">#ff810220</color>
+ <color name="photonRed90">#ff440306</color>
+
+ <!-- Yellow -->
+ <color name="photonYellow05">#ffffffcc</color>
+ <color name="photonYellow10">#ffffff98</color>
+ <color name="photonYellow20">#ffffea80</color>
+ <color name="photonYellow30">#ffffd567</color>
+ <color name="photonYellow40">#ffffbd4f</color>
+ <color name="photonYellow40A41">#69ffbd4f</color>
+ <color name="photonYellow50">#ffffa436</color>
+ <color name="photonYellow60">#ffe27f2e</color>
+ <color name="photonYellow60A40">#66e27f2e</color>
+ <color name="photonYellow70">#ffc45a27</color>
+ <color name="photonYellow70A77">#c4c45a27</color>
+ <color name="photonYellow80">#ffa7341f</color>
+ <color name="photonYellow90">#ff960e18</color>
+
+ <!--
+ Teal
+ Teal is a colour palette that we no longer use.
+ -->
+ <color name="photonTeal50">#00feff</color>
+ <color name="photonTeal60">#00c8d7</color>
+ <color name="photonTeal70">#008ea4</color>
+ <color name="photonTeal80">#005a71</color>
+ <color name="photonTeal90">#002d3e</color>
+
+ <!-- Purple -->
+ <color name="photonPurple05">#fff7e2ff</color>
+ <color name="photonPurple10">#fff6b8ff</color>
+ <color name="photonPurple20">#fff68fff</color>
+ <color name="photonPurple30">#fff770ff</color>
+ <color name="photonPurple40">#ffd74cf0</color>
+ <color name="photonPurple50">#ffb833e1</color>
+ <color name="photonPurple60">#ff952bb9</color>
+ <color name="photonPurple70">#ff722291</color>
+ <color name="photonPurple80">#ff4e1a69</color>
+ <color name="photonPurple90">#ff2b1141</color>
+
+ <!-- Orange -->
+ <color name="photonOrange05">#fffff4de</color>
+ <color name="photonOrange10">#ffffd5b2</color>
+ <color name="photonOrange20">#ffffb587</color>
+ <color name="photonOrange30">#ffffa266</color>
+ <color name="photonOrange40">#ffff8a50</color>
+ <color name="photonOrange50">#ffff7139</color>
+ <color name="photonOrange60">#ffe25920</color>
+ <color name="photonOrange70">#ffcc3d00</color>
+ <color name="photonOrange80">#ff9e280b</color>
+ <color name="photonOrange90">#ff7c1504</color>
+
+ <!-- Ink -->
+ <color name="photonInk05">#ff393473</color>
+ <color name="photonInk10">#ff342f6d</color>
+ <color name="photonInk20">#ff312a64</color>
+ <color name="photonInk20A48">#7A312a64</color>
+ <color name="photonInk20A50">#80312A64</color>
+ <color name="photonInk30">#ff2e255d</color>
+ <color name="photonInk40">#ff2b2156</color>
+ <color name="photonInk50">#ff291d4f</color>
+ <color name="photonInk60">#ff271948</color>
+ <color name="photonInk70">#ff241541</color>
+ <color name="photonInk80">#ff20123a</color>
+ <color name="photonInk80A96">#f520123a</color>
+ <color name="photonInk90">#ff1d1133</color>
+
+ <!--
+ Grey
+ Grey is a colour palette that we no longer use.
+ In the future, Grey should be redefined into LightGrey and DarkGrey wherever possible.
+ -->
+ <color name="photonGrey10">#f9f9fa</color>
+ <color name="photonGrey20">#ededf0</color>
+ <color name="photonGrey30">#d7d7db</color>
+ <color name="photonGrey40">#b1b1b3</color>
+ <color name="photonGrey50">#737373</color>
+ <color name="photonGrey60">#4a4a4f</color>
+ <color name="photonGrey70">#38383d</color>
+ <color name="photonGrey80">#2a2a2e</color>
+ <color name="photonGrey90">#0c0c0d</color>
+
+ <!-- Light Grey -->
+ <color name="photonLightGrey05">#fffbfbfe</color>
+ <color name="photonLightGrey05A40">#66fbfbfe</color>
+ <color name="photonLightGrey05A60">#99fbfbfe</color>
+ <color name="photonLightGrey10">#fff9f9fb</color>
+ <color name="photonLightGrey20">#fff0f0f4</color>
+ <color name="photonLightGrey30">#ffe0e0e6</color>
+ <color name="photonLightGrey40">#ffcfcfd8</color>
+ <color name="photonLightGrey50">#ffbfbfc9</color>
+ <color name="photonLightGrey60">#ffafafba</color>
+ <color name="photonLightGrey70">#ff9f9fad</color>
+ <color name="photonLightGrey80">#ff8f8f9d</color>
+ <color name="photonLightGrey90">#ff80808e</color>
+
+ <!-- Dark Grey -->
+ <color name="photonDarkGrey05">#ff5b5b66</color>
+ <color name="photonDarkGrey05A45">#735b5b66</color>
+ <color name="photonDarkGrey10">#ff52525e</color>
+ <color name="photonDarkGrey20">#ff4a4a55</color>
+ <color name="photonDarkGrey30">#ff42414d</color>
+ <color name="photonDarkGrey30A95">#f242414d</color>
+ <color name="photonDarkGrey30A96">#f542414d</color>
+ <color name="photonDarkGrey40">#ff3a3944</color>
+ <color name="photonDarkGrey50">#ff32313c</color>
+ <color name="photonDarkGrey60">#ff2b2a33</color>
+ <color name="photonDarkGrey70">#ff23222b</color>
+ <color name="photonDarkGrey80">#ff1c1b22</color>
+ <color name="photonDarkGrey90">#ff15141a</color>
+ <color name="photonDarkGrey90A40">#6615141a</color>
+ <color name="photonDarkGrey90A60">#9915141a</color>
+ <color name="photonDarkGrey90A95">#f215141a</color>
+ <color name="photonDarkGrey90A96">#f515141a</color>
+
+ <!-- Violet -->
+ <color name="photonViolet05">#ffE7DFFF</color>
+ <color name="photonViolet10">#ffd9bfff</color>
+ <color name="photonViolet20">#ffcb9eFF</color>
+ <color name="photonViolet20A60">#99cb9eFF</color>
+ <color name="photonViolet30">#ffc689FF</color>
+ <color name="photonViolet40">#ffab71ff</color>
+ <color name="photonViolet40A12">#1fab71ff</color>
+ <color name="photonViolet40A30">#4dab71ff</color>
+ <color name="photonViolet50">#ff9059FF</color>
+ <color name="photonViolet50A32">#529059FF</color>
+ <color name="photonViolet50A48">#7A9059FF</color>
+ <color name="photonViolet60">#ff7542e5</color>
+ <color name="photonViolet60A50">#807542E5</color>
+ <color name="photonViolet70">#ff592ACB</color>
+ <color name="photonViolet70A12">#1F592ACB</color>
+ <color name="photonViolet70A80">#CC592ACB</color>
+ <color name="photonViolet80">#ff45278d</color>
+ <color name="photonViolet90">#ff321c64</color>
+ <color name="photonViolet90A20">#33321c64</color>
+
+ <!-- White -->
+ <color name="photonWhite">#ffffff</color>
+
+ <!-- Black -->
+ <color name="photonBlack">#000000</color>
+</resources>
diff --git a/mobile/android/android-components/components/ui/fonts/README.md b/mobile/android/android-components/components/ui/fonts/README.md
new file mode 100644
index 0000000000..33b94590e0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/fonts/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > UI > Fonts
+
+The standard set of fonts used by Mozilla Android products.
+
+## Usage
+
+### 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:ui-fonts:{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/ui/fonts/build.gradle b/mobile/android/android-components/components/ui/fonts/build.gradle
new file mode 100644
index 0000000000..f3eb06ce5e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/fonts/build.gradle
@@ -0,0 +1,31 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.ui.fonts'
+}
+
+dependencies {
+ // None
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/fonts/proguard-rules.pro b/mobile/android/android-components/components/ui/fonts/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/fonts/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/ui/fonts/src/main/AndroidManifest.xml b/mobile/android/android-components/components/ui/fonts/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/ui/fonts/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/ui/fonts/src/main/res/values/roboto_fonts.xml b/mobile/android/android-components/components/ui/fonts/src/main/res/values/roboto_fonts.xml
new file mode 100644
index 0000000000..eeb5222aab
--- /dev/null
+++ b/mobile/android/android-components/components/ui/fonts/src/main/res/values/roboto_fonts.xml
@@ -0,0 +1,18 @@
+<?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 xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources">
+
+ <!-- Designers specify the Roboto fontFamily as "Roboto Medium" but they appear in Android as
+ "sans-serif-medium": this file aliases the Android definitions to those used by the designers.
+
+ This solution is adapted from SO: https://stackoverflow.com/a/26545517 and https://stackoverflow.com/a/42927799 -->
+ <string name="font_roboto_regular">sans-serif</string>
+ <string name="font_roboto_light">sans-serif-light</string>
+ <string name="font_roboto_condensed">sans-serif-condensed</string>
+ <string name="font_roboto_black">sans-serif-black</string>
+ <string name="font_roboto_thin">sans-serif-thin</string> <!-- API 17+ -->
+ <string name="font_roboto_medium">sans-serif-medium</string> <!-- API 21+ -->
+
+</resources>
diff --git a/mobile/android/android-components/components/ui/icons/README.md b/mobile/android/android-components/components/ui/icons/README.md
new file mode 100644
index 0000000000..47c4e989c0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > UI > Icons
+
+A collection of often used browser icons.
+
+## Usage
+
+### 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:ui-icons:{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/ui/icons/build.gradle b/mobile/android/android-components/components/ui/icons/build.gradle
new file mode 100644
index 0000000000..d49b47c2f8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/build.gradle
@@ -0,0 +1,31 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.ui.icons'
+}
+
+dependencies {
+ // None
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/proguard-rules.pro b/mobile/android/android-components/components/ui/icons/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/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/ui/icons/src/main/AndroidManifest.xml b/mobile/android/android-components/components/ui/icons/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/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/ui/icons/src/main/assets/mozac_error_asleep.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_asleep.svg
new file mode 100644
index 0000000000..8829f8a102
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_asleep.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#linear-gradient-3);}.cls-6{fill:url(#linear-gradient-4);}.cls-7{fill:#cc3d00;}.cls-8{fill:#e31587;}.cls-9{fill:#ff4f5e;}.cls-10{fill:#c60084;}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="-496.29" y1="61.33" x2="-547.1" y2="10.52" gradientTransform="matrix(-1, 0, 0, 1, -471.81, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd567"/><stop offset="1" stop-color="#fff361"/></linearGradient><linearGradient id="linear-gradient-2" x1="-572.12" y1="-14.51" x2="-454.86" y2="102.75" gradientTransform="matrix(-1, 0, 0, 1, -471.81, 0)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="#ffd567"/><stop offset="0.86" stop-color="#ffc456"/><stop offset="1" stop-color="#ffbd4f"/></linearGradient><linearGradient id="linear-gradient-3" x1="14.23" y1="9.62" x2="63.25" y2="58.64" gradientUnits="userSpaceOnUse"><stop offset="0.25" stop-color="#ffd567"/><stop offset="1" stop-color="#ffa436"/></linearGradient><linearGradient id="linear-gradient-4" x1="29.38" y1="-5.52" x2="78.4" y2="43.5" xlink:href="#linear-gradient-3"/></defs><title>fx-fenix_error_2</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M49.79,56.33a8,8,0,0,1-7.92,8H10.05a6.08,6.08,0,0,1,0-12.15,6.26,6.26,0,0,1,1.45.18,6.48,6.48,0,0,1-.16-1.41,6.26,6.26,0,0,1,9.81-5.16,10.53,10.53,0,0,1,20.72,2.64A8,8,0,0,1,49.79,56.33Z"/><path class="cls-2" d="M188.39,27.89a8.76,8.76,0,0,0-2.19.27,9.25,9.25,0,0,0,.24-2.12,9.45,9.45,0,0,0-14.78-7.78,15.88,15.88,0,0,0-31.24,4,11.93,11.93,0,1,0,0,23.85h48a9.11,9.11,0,1,0,0-18.21Z"/><path class="cls-3" d="M50.35,4.45a31,31,0,0,1,31,31c0,.42,0,.84,0,1.27A3.67,3.67,0,0,1,80.82,44h-.66A31,31,0,1,1,50.35,4.45Z"/><path class="cls-4" d="M19.34,36.73c0-.43,0-.85,0-1.27a30.86,30.86,0,0,1,8.55-21.37A31.87,31.87,0,0,0,71.71,57.94,31,31,0,0,1,20.53,44.05h-.7a3.68,3.68,0,0,1-.49-7.32Z"/><path class="cls-5" d="M31.48,31.9a1,1,0,0,1-.69-.26,1.06,1.06,0,0,1-.08-1.49A6.36,6.36,0,0,1,35.46,28h0a6.38,6.38,0,0,1,4.75,2.13,1.05,1.05,0,1,1-1.58,1.39,4.25,4.25,0,0,0-3.17-1.43h0a4.21,4.21,0,0,0-3.17,1.43A1.12,1.12,0,0,1,31.48,31.9Z"/><path class="cls-6" d="M69.68,31.9a1,1,0,0,1-.79-.36,4.25,4.25,0,0,0-3.17-1.43h0a4.21,4.21,0,0,0-3.17,1.43A1.05,1.05,0,0,1,61,30.14,6.36,6.36,0,0,1,65.74,28h0a6.36,6.36,0,0,1,4.75,2.13,1,1,0,0,1-.1,1.48A1,1,0,0,1,69.68,31.9Z"/><path class="cls-7" d="M70,37.1a1.41,1.41,0,0,0-2,.12,3,3,0,0,1-2.25,1,3,3,0,0,1-2.24-1,1.42,1.42,0,0,0-2-.12,1.39,1.39,0,0,0-.34,1.62,1.46,1.46,0,0,0,.09.39,4.8,4.8,0,0,0,9,0,1.07,1.07,0,0,0,.1-.39A1.36,1.36,0,0,0,70,37.1Z"/><path class="cls-7" d="M39.69,37.1a1.41,1.41,0,0,0-2,.12,3,3,0,0,1-2.24,1h0a3,3,0,0,1-2.25-1,1.41,1.41,0,0,0-2-.12,1.38,1.38,0,0,0-.35,1.62,1.18,1.18,0,0,0,.1.39,4.91,4.91,0,0,0,4.47,3.06h0a4.92,4.92,0,0,0,4.48-3.06,1.3,1.3,0,0,0,.09-.39A1.34,1.34,0,0,0,39.69,37.1Z"/><path class="cls-7" d="M55,52a4.34,4.34,0,1,1-8.67,0c0-2.4,1.94-3.4,4.34-3.4S55,49.58,55,52Z"/><path class="cls-8" d="M66.68,16.08H60.39a1.15,1.15,0,0,1-.82-2L64,9.68H60.6a1.15,1.15,0,1,1,0-2.3h6.2a1.15,1.15,0,0,1,.82,2l-4.43,4.43h3.5a1.15,1.15,0,1,1,0,2.3Z"/><path class="cls-9" d="M75.48,6.52H71.39A.87.87,0,0,1,70.78,5l2.69-2.69H71.53a.87.87,0,1,1,0-1.74h4a.84.84,0,0,1,.8.54.85.85,0,0,1-.19.94L73.48,4.78h2a.87.87,0,1,1,0,1.74Z"/><path class="cls-10" d="M80,24.27H71.5a1.72,1.72,0,0,1-1.23-2.95L76,15.61H71.76a1.73,1.73,0,0,1,0-3.46h8.4a1.73,1.73,0,0,1,1.23,2.95l-5.71,5.71H80a1.73,1.73,0,0,1,0,3.46Z"/></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_confused.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_confused.svg
new file mode 100644
index 0000000000..0bed690133
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_confused.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#linear-gradient-3);}.cls-6{fill:url(#linear-gradient-4);}.cls-7{fill:url(#linear-gradient-5);}.cls-8{fill:url(#linear-gradient-6);}.cls-9{fill:#cc3d00;}.cls-10{fill:url(#linear-gradient-7);}.cls-11{fill:url(#linear-gradient-8);}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="75.94" y1="61.05" x2="25.13" y2="10.24" gradientTransform="matrix(-1, 0, 0, 1, 100.19, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd567"/><stop offset="1" stop-color="#fff361"/></linearGradient><linearGradient id="linear-gradient-2" x1="0.1" y1="-14.79" x2="117.37" y2="102.48" gradientTransform="matrix(-1, 0, 0, 1, 100.19, 0)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="#ffd567"/><stop offset="0.86" stop-color="#ffc456"/><stop offset="1" stop-color="#ffbd4f"/></linearGradient><linearGradient id="linear-gradient-3" x1="26.98" y1="-14.71" x2="82.12" y2="43.5" gradientUnits="userSpaceOnUse"><stop offset="0.25" stop-color="#ffd567"/><stop offset="1" stop-color="#ffa436"/></linearGradient><linearGradient id="linear-gradient-4" x1="13.11" y1="-1.58" x2="68.26" y2="56.63" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-5" x1="20.19" y1="-8.28" x2="75.33" y2="49.93" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-6" x1="4.78" y1="6.32" x2="59.92" y2="64.53" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-7" x1="4.86" y1="6.24" x2="60.01" y2="64.45" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-8" x1="58.01" y1="29.76" x2="89.14" y2="-1.37" gradientUnits="userSpaceOnUse"><stop offset="0.26" stop-color="#e31587"/><stop offset="0.63" stop-color="#ff298a"/><stop offset="1" stop-color="#ff8a50"/></linearGradient></defs><title>fx-fenix_error_4</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M105,57A8,8,0,0,1,97,65H65.22a6.08,6.08,0,0,1,0-12.15,6.26,6.26,0,0,1,1.45.18,6.48,6.48,0,0,1-.16-1.41,6.26,6.26,0,0,1,9.81-5.16A10.53,10.53,0,0,1,97,49.09,8,8,0,0,1,105,57Z"/><path class="cls-2" d="M203.71,26.36a8.76,8.76,0,0,0-2.19.27,9.31,9.31,0,0,0,.24-2.12A9.45,9.45,0,0,0,187,16.72a15.88,15.88,0,0,0-31.24,4,11.93,11.93,0,1,0,0,23.85h48a9.11,9.11,0,0,0,0-18.21Z"/><path class="cls-3" d="M50.12,4.18a31,31,0,0,1,31,31c0,.42,0,.84,0,1.26a3.68,3.68,0,0,1-.52,7.32h-.66A31,31,0,1,1,50.12,4.18Z"/><path class="cls-4" d="M19.11,36.45q0-.63,0-1.26a30.86,30.86,0,0,1,8.55-21.37A31.88,31.88,0,0,0,71.48,57.67,31,31,0,0,1,20.3,43.78h-.7a3.68,3.68,0,0,1-.49-7.33Z"/><path class="cls-5" d="M65.62,28.93h0a6.44,6.44,0,0,1-4.83-2.16,1.07,1.07,0,0,1,1.61-1.41,4.28,4.28,0,0,0,3.22,1.45h0a4.28,4.28,0,0,0,3.22-1.45,1.06,1.06,0,0,1,1.58,1.42A6.4,6.4,0,0,1,65.62,28.93Z"/><path class="cls-6" d="M30.65,22.26A1,1,0,0,1,30,22a1.07,1.07,0,0,1-.09-1.51,6.49,6.49,0,0,1,4.83-2.16h0a6.47,6.47,0,0,1,4.82,2.16,1.06,1.06,0,0,1-.1,1.51,1.05,1.05,0,0,1-1.5-.1,4.35,4.35,0,0,0-3.22-1.45h0a4.25,4.25,0,0,0-3.22,1.45A1.14,1.14,0,0,1,30.65,22.26Z"/><path class="cls-7" d="M65.52,41.54A8.19,8.19,0,0,1,62,40.76a1.16,1.16,0,0,1-.55-1.54A1.15,1.15,0,0,1,63,38.68a6,6,0,0,0,5.19-.07,1.15,1.15,0,0,1,1.55.52,1.16,1.16,0,0,1-.52,1.54A8.44,8.44,0,0,1,65.52,41.54Z"/><path class="cls-8" d="M50,56a8.2,8.2,0,0,1-3.49-.79,1.15,1.15,0,1,1,1-2.08,6,6,0,0,0,5.18-.07,1.16,1.16,0,1,1,1,2.07A8.55,8.55,0,0,1,50,56Z"/><path class="cls-9" d="M59.5,47.07a1.29,1.29,0,0,0-1.81.25,4.07,4.07,0,0,1-6.56,0,6.66,6.66,0,0,0-10.68,0,1.3,1.3,0,1,0,2.07,1.56,4.06,4.06,0,0,1,6.55,0h0a6.65,6.65,0,0,0,5.34,2.69h0a6.67,6.67,0,0,0,5.34-2.69A1.31,1.31,0,0,0,59.5,47.07Z"/><path class="cls-9" d="M34.67,25.17a3.38,3.38,0,0,1,3.4,3.4V36.2c0,1.89-1.51,1.17-3.4,1.17h0c-1.88,0-3.4.72-3.4-1.17V28.57a3.4,3.4,0,0,1,3.4-3.4Z"/><path class="cls-9" d="M70.27,34.5a1.07,1.07,0,0,0-.1-.39,4.87,4.87,0,0,0-9.08,0,1,1,0,0,0-.1.39A1.43,1.43,0,0,0,63.37,36a3,3,0,0,1,2.27-1h0a3,3,0,0,1,2.28,1,1.44,1.44,0,0,0,2,.11A1.46,1.46,0,0,0,70.27,34.5Z"/><path class="cls-10" d="M31.58,38.92a1.15,1.15,0,0,1-.51-2.18,8.2,8.2,0,0,1,7.2-.1,1.15,1.15,0,1,1-1,2.08,6,6,0,0,0-5.19.07A1.25,1.25,0,0,1,31.58,38.92Z"/><path class="cls-11" d="M83.74,7c0,5.59-6.21,5.21-6.21,9.08v0a.81.81,0,0,1-.81.81H73.33a.81.81,0,0,1-.81-.81v-.23c0-6,5.46-5.59,5.46-8.39,0-1.21-.9-1.93-2.39-1.93A5.14,5.14,0,0,0,72,7.1a.79.79,0,0,1-1,.05L68.75,5.41a.81.81,0,0,1-.09-1.2,10,10,0,0,1,7.49-3.07C81.22,1.14,83.74,3.87,83.74,7Zm-5.52,16.2A3.15,3.15,0,1,1,75.07,20,3.13,3.13,0,0,1,78.22,23.15Z"/></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_eye_roll.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_eye_roll.svg
new file mode 100644
index 0000000000..3f8a939693
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_eye_roll.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:#ffbd4f;}.cls-6{fill:#cc3d00;}.cls-7{fill:url(#linear-gradient-3);}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="matrix(1, 0.01, 0, 0.7, 0.04, 23.76)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="75.92" y1="60.35" x2="25.11" y2="9.54" gradientTransform="matrix(-1, 0, 0, 1, 100.19, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd567"/><stop offset="1" stop-color="#fff361"/></linearGradient><linearGradient id="linear-gradient-2" x1="0.08" y1="-15.49" x2="117.35" y2="101.78" gradientTransform="matrix(-1, 0, 0, 1, 100.19, 0)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="#ffd567"/><stop offset="0.86" stop-color="#ffc456"/><stop offset="1" stop-color="#ffbd4f"/></linearGradient><linearGradient id="linear-gradient-3" x1="3.98" y1="6.32" x2="59.13" y2="64.53" gradientUnits="userSpaceOnUse"><stop offset="0.25" stop-color="#ffd567"/><stop offset="1" stop-color="#ffa436"/></linearGradient></defs><title>fx-fenix_error_7</title><path class="cls-1" d="M4.43,34.39A23.25,23.25,0,0,1,27.72,11.18l174.59.49a23.25,23.25,0,1,1-.07,46.49l-174.6-.48A23.25,23.25,0,0,1,4.43,34.39Z"/><path class="cls-2" d="M142.55,24a8,8,0,0,1-8,7.9l-31.83-.18a6.08,6.08,0,0,1,.07-12.15,5.9,5.9,0,0,1,1.46.19,5.5,5.5,0,0,1-.16-1.41A6.27,6.27,0,0,1,114,13.27,10.53,10.53,0,0,1,134.68,16,8,8,0,0,1,142.55,24Z"/><path class="cls-2" d="M206.71,48.43a8.81,8.81,0,0,0-2.19.25,9.84,9.84,0,0,0,.26-2.12A9.46,9.46,0,0,0,190,38.7a15.87,15.87,0,0,0-31.25,3.81,11.93,11.93,0,1,0-.14,23.86l48,.27a9.11,9.11,0,1,0,.1-18.21Z"/><path class="cls-3" d="M50.14,3.47a31,31,0,0,1,31,31c0,.42,0,.84,0,1.26a3.67,3.67,0,0,1-.52,7.31H80A31,31,0,1,1,50.14,3.47Z"/><path class="cls-4" d="M19.13,35.75c0-.42,0-.84,0-1.26a30.86,30.86,0,0,1,8.55-21.37A31.88,31.88,0,0,0,71.5,57,31,31,0,0,1,20.33,43.08h-.71a3.68,3.68,0,0,1-.49-7.33Z"/><path class="cls-5" d="M61.31,26.81a1.06,1.06,0,0,0,.8-.37A4.36,4.36,0,0,1,65.34,25h0a4.27,4.27,0,0,1,3.22,1.45A1.07,1.07,0,1,0,70.15,25a6.45,6.45,0,0,0-4.83-2.17h0A6.48,6.48,0,0,0,60.49,25a1.06,1.06,0,0,0,.1,1.51A1,1,0,0,0,61.31,26.81Z"/><path class="cls-5" d="M31,19.81a1.08,1.08,0,0,0,.8-.37A4.34,4.34,0,0,1,35,18h0a4.29,4.29,0,0,1,3.23,1.45,1.06,1.06,0,0,0,1.5.09A1.07,1.07,0,0,0,39.8,18,6.48,6.48,0,0,0,35,15.85h0A6.45,6.45,0,0,0,30.14,18,1.08,1.08,0,0,0,31,19.81Z"/><path class="cls-6" d="M43.85,51.9a1.65,1.65,0,0,0,1.27.71c.08,0,1.9.08,4.91.08s4.83-.08,4.9-.08a1.65,1.65,0,0,0,1.27-.71,1.62,1.62,0,0,0,.22-1.43c-.42-1.39-2.33-2-6.38-2s-5.95.59-6.39,2A1.75,1.75,0,0,0,43.85,51.9Z"/><path class="cls-6" d="M59.43,31.39v1.78a1.29,1.29,0,0,0,0,.35,3.51,3.51,0,0,0,7-.35v0h2.3a1.75,1.75,0,1,0,0-3.49h-7.6A1.76,1.76,0,0,0,59.43,31.39Z"/><path class="cls-6" d="M29.55,31.39v1.78a1.29,1.29,0,0,0,0,.35,3.52,3.52,0,0,0,7-.35v0h2.31a1.75,1.75,0,0,0,0-3.49H31.31a1.75,1.75,0,0,0-1.75,1.75Z"/><path class="cls-7" d="M30.7,39a1.17,1.17,0,0,1-1-.63,1.15,1.15,0,0,1,.52-1.55,8.2,8.2,0,0,1,7.2-.1,1.15,1.15,0,1,1-1,2.08,6,6,0,0,0-5.18.07A1.11,1.11,0,0,1,30.7,39Z"/></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_hourglass.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_hourglass.svg
new file mode 100644
index 0000000000..62d88c9e19
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_hourglass.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#radial-gradient-2);}.cls-4{fill:url(#linear-gradient);}.cls-5{fill:url(#linear-gradient-2);}.cls-6{fill:#ffb587;}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><radialGradient id="radial-gradient-2" cx="25.52" cy="-3.12" r="113.73" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff4de"/><stop offset="0.15" stop-color="#fff1d9"/><stop offset="0.32" stop-color="#ffe7cc"/><stop offset="0.5" stop-color="#ffd6b5"/><stop offset="0.69" stop-color="#ffbf95"/><stop offset="0.76" stop-color="#ffb587"/></radialGradient><linearGradient id="linear-gradient" x1="17.06" y1="12.67" x2="58.93" y2="45.61" gradientUnits="userSpaceOnUse"><stop offset="0.44" stop-color="#ff8a50"/><stop offset="0.91" stop-color="#ff298a"/></linearGradient><linearGradient id="linear-gradient-2" x1="21.99" y1="63.28" x2="81.31" y2="3.96" gradientUnits="userSpaceOnUse"><stop offset="0.24" stop-color="#e31587"/><stop offset="0.63" stop-color="#ff298a"/><stop offset="1" stop-color="#ff8a50"/></linearGradient></defs><title>fx-fenix_error_8</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M238.23,29.47a8,8,0,0,1-7.91,8H198.49a6.08,6.08,0,0,1,0-12.16,5.84,5.84,0,0,1,1.46.19,6,6,0,0,1-.17-1.41,6.28,6.28,0,0,1,9.81-5.17,10.54,10.54,0,0,1,20.73,2.65A7.94,7.94,0,0,1,238.23,29.47Z"/><path class="cls-2" d="M98,37.08a9.25,9.25,0,0,0-2.19.27,9.84,9.84,0,0,0,.24-2.12,9.45,9.45,0,0,0-14.79-7.78,15.87,15.87,0,0,0-31.23,4,11.93,11.93,0,1,0,0,23.85H98a9.11,9.11,0,0,0,0-18.21Z"/><path class="cls-3" d="M60.21,25.49c-2.44,2.44-5.34,5.6-5.7,9.72.36,4.12,3.26,7.29,5.7,9.72A15.48,15.48,0,0,1,65.6,56.68v0h0v6.62h-31V56.7h0v0A15.48,15.48,0,0,1,40,44.93c2.44-2.43,5.34-5.6,5.7-9.72-.36-4.12-3.26-7.28-5.7-9.72a15.46,15.46,0,0,1-5.39-11.75V7h31v6.71A15.46,15.46,0,0,1,60.21,25.49Z"/><path class="cls-4" d="M50.09,55.48c1.16,0,7,6.39,7,6.39h-14S48.94,55.48,50.09,55.48ZM49.48,47a.7.7,0,1,0,.69.69A.69.69,0,0,0,49.48,47Zm.69,2.48a.7.7,0,1,0,.7.69A.69.69,0,0,0,50.17,49.49Zm-1.69,4.16a.7.7,0,1,0,.7.69A.69.69,0,0,0,48.48,53.65Zm2.08-9.82a.7.7,0,1,0,.69.7A.7.7,0,0,0,50.56,43.83Zm-12-24.76c0,2.89,3.27,5.48,5.3,7.23,1.5,1.51,4.07,4.34,4.07,7.91,0,.12,0,.23,0,.34.07,2.92.44,6.16,1.37,7.32a.7.7,0,1,0,1.24.44.69.69,0,0,0,0-.13c1.24-.76,1.68-4.73,1.72-8.23.21-3,2.63-6.21,4.07-7.65,2-1.75,5.3-4.34,5.3-7.23C61.63,17.66,38.56,17.66,38.56,19.07ZM50.9,52.32a.7.7,0,1,0,.69.7A.7.7,0,0,0,50.9,52.32Z"/><g id="Layer_4" data-name="Layer 4"><path class="cls-5" d="M65.6,8.76h-31a1.73,1.73,0,0,1,0-3.46h31a1.73,1.73,0,1,1,0,3.46Zm1.73,54.56a1.73,1.73,0,0,0-1.73-1.73h-31a1.73,1.73,0,0,0,0,3.46h31A1.74,1.74,0,0,0,67.33,63.32Z"/></g><g id="Layer_5" data-name="Layer 5"><path class="cls-6" d="M61.63,18.65c0,1.08-5.16,1.94-11.54,1.94s-11.53-.86-11.53-1.94,5.16-1.93,11.53-1.93S61.63,17.58,61.63,18.65Z"/></g></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_inspect.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_inspect.svg
new file mode 100644
index 0000000000..b64c203ae7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_inspect.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:#cc3d00;}.cls-6{fill:url(#linear-gradient-3);}.cls-7{fill:url(#linear-gradient-4);}.cls-8{fill:url(#linear-gradient-5);}.cls-9{fill:url(#linear-gradient-6);}.cls-10{fill:#ffbd4f;}.cls-11{fill:url(#linear-gradient-7);}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="76.3" y1="61.03" x2="25.49" y2="10.22" gradientTransform="matrix(-1, 0, 0, 1, 100.19, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd567"/><stop offset="1" stop-color="#fff361"/></linearGradient><linearGradient id="linear-gradient-2" x1="0.47" y1="-14.81" x2="117.73" y2="102.46" gradientTransform="matrix(-1, 0, 0, 1, 100.19, 0)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="#ffd567"/><stop offset="0.86" stop-color="#ffc456"/><stop offset="1" stop-color="#ffbd4f"/></linearGradient><linearGradient id="linear-gradient-3" x1="32.96" y1="-14.67" x2="79.83" y2="42.94" gradientUnits="userSpaceOnUse"><stop offset="0.25" stop-color="#ffd567"/><stop offset="1" stop-color="#ffa436"/></linearGradient><linearGradient id="linear-gradient-4" x1="12.99" y1="1.58" x2="59.86" y2="59.19" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-5" x1="6.75" y1="6.72" x2="53.62" y2="64.33" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-6" x1="28.56" y1="-11.09" x2="75.43" y2="46.52" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-7" x1="47.79" y1="16.87" x2="1.03" y2="63.63" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff7139"/><stop offset="0.22" stop-color="#ff624a"/><stop offset="0.66" stop-color="#ff3c75"/><stop offset="0.86" stop-color="#ff298a"/></linearGradient></defs><title>fx-fenix_error_1</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M109.55,21.81a8,8,0,0,1-7.91,8H69.81a6.08,6.08,0,0,1,0-12.16,5.84,5.84,0,0,1,1.46.19,6,6,0,0,1-.17-1.41,6.28,6.28,0,0,1,9.81-5.17,10.54,10.54,0,0,1,20.73,2.65A7.94,7.94,0,0,1,109.55,21.81Z"/><path class="cls-2" d="M234.35,42.44a9.25,9.25,0,0,0-2.19.27,9.84,9.84,0,0,0,.24-2.12,9.45,9.45,0,0,0-14.79-7.78,15.87,15.87,0,0,0-31.23,4,11.93,11.93,0,1,0,0,23.85h48a9.11,9.11,0,0,0,0-18.21Z"/><path class="cls-3" d="M49.76,4.15a31,31,0,0,1,31,31c0,.42,0,.84,0,1.26a3.67,3.67,0,0,1-.52,7.31h-.66A31,31,0,1,1,49.76,4.15Z"/><path class="cls-4" d="M18.75,36.43q0-.63,0-1.26A30.86,30.86,0,0,1,27.27,13.8,31.88,31.88,0,0,0,71.12,57.65,31,31,0,0,1,19.94,43.76h-.7a3.68,3.68,0,0,1-3.68-3.69A3.64,3.64,0,0,1,18.75,36.43Z"/><circle class="cls-5" cx="55.16" cy="46.74" r="4.02"/><path class="cls-5" d="M33.61,20.86a5.19,5.19,0,0,1,5.2,5.2V37.74c0,2.88-2.32,3.22-5.2,3.22h0c-2.88,0-5.2-.34-5.2-3.22V26.06a5.19,5.19,0,0,1,5.2-5.2Z"/><path class="cls-5" d="M68.61,34.77v-3a3.4,3.4,0,0,0-6.79,0v3Z"/><path class="cls-6" d="M69.23,26.05h-8a1.07,1.07,0,1,1,0-2.13h8a1.07,1.07,0,0,1,0,2.13Z"/><path class="cls-7" d="M55.11,54.88a5.34,5.34,0,0,1-2.21-.49,1.06,1.06,0,0,1,.92-1.92,3,3,0,0,0,2.66,0,1.07,1.07,0,0,1,1,1.91A5.2,5.2,0,0,1,55.11,54.88Z"/><path class="cls-8" d="M38.79,41.44H28.54a1.64,1.64,0,1,1,0-3.27H38.79a1.64,1.64,0,1,1,0,3.27Z"/><path class="cls-9" d="M68.55,35H61.87a1.07,1.07,0,0,1,0-2.14h6.68a1.07,1.07,0,1,1,0,2.14Z"/><path class="cls-5" d="M45.84,19a17.33,17.33,0,0,0-26.09,22.7l-2.67,2.67,3.39,3.39,2.67-2.67A17.33,17.33,0,0,0,45.84,19ZM33.58,45.43a14.15,14.15,0,0,1-13.42-9.69l.06.27a14.14,14.14,0,0,1,23.3-14.77l0,0a14.14,14.14,0,0,1-10,24.17Z"/><path class="cls-10" d="M24,23A14.14,14.14,0,0,1,47.33,28.2a14.11,14.11,0,1,0-27.17,7.54A14.13,14.13,0,0,1,24,23Z"/><g id="Layer_5" data-name="Layer 5"><path class="cls-11" d="M2.62,62h0C1,60.34.44,58.29,1.31,57.44L16.62,43.82c.88-.85,2.3-.82,3.89.83h0c1.6,1.64,1.58,3.06.7,3.91L7.13,63.44C6.25,64.29,4.22,63.63,2.62,62Z"/></g><polygon class="cls-5" points="4.41 54.82 10.06 60.47 16.88 53.04 11.84 47.99 4.41 54.82"/></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_lock.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_lock.svg
new file mode 100644
index 0000000000..fc72ea7b8e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_lock.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:#ffa266;opacity:0.8;}.cls-6{fill:#b1b1bc;}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="47.51" y1="28.54" x2="84.23" y2="-8.18" gradientUnits="userSpaceOnUse"><stop offset="0.07" stop-color="#e31587"/><stop offset="0.29" stop-color="#ff298a"/><stop offset="0.7" stop-color="#ff8a50"/></linearGradient><linearGradient id="linear-gradient-2" x1="80.65" y1="22.6" x2="33.16" y2="67.53" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff7139"/><stop offset="0.22" stop-color="#ff624a"/><stop offset="0.66" stop-color="#ff3c75"/><stop offset="0.86" stop-color="#ff298a"/></linearGradient></defs><title>fx-fenix_error_5</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M239,55.51a7.94,7.94,0,0,1-7.92,8H199.26a6.08,6.08,0,0,1,0-12.15,5.9,5.9,0,0,1,1.46.18,6,6,0,0,1-.17-1.41,6.28,6.28,0,0,1,9.81-5.17,10.53,10.53,0,0,1,20.72,2.65A7.94,7.94,0,0,1,239,55.51Z"/><path class="cls-2" d="M104.13,27.89a8.74,8.74,0,0,0-2.18.27,9.84,9.84,0,0,0,.24-2.12A9.45,9.45,0,0,0,87.4,18.26a15.87,15.87,0,0,0-31.23,4,11.93,11.93,0,1,0,0,23.85h48a9.11,9.11,0,1,0,0-18.21Z"/><path class="cls-3" d="M66.28,34.75A2.59,2.59,0,0,1,63,33L58.61,18.57A9.24,9.24,0,0,0,40.94,24a2.6,2.6,0,0,1-5,1.52,14.43,14.43,0,0,1,27.6-8.46L68,31.51A2.6,2.6,0,0,1,66.28,34.75Z"/><g id="Layer_3" data-name="Layer 3"><path class="cls-4" d="M76.37,54.16,46.16,63.42a5.33,5.33,0,0,1-6.67-3.54L34.14,42.41a5.33,5.33,0,0,1,3.54-6.67l30.21-9.26A5.34,5.34,0,0,1,74.56,30l5.36,17.47A5.35,5.35,0,0,1,76.37,54.16Z"/></g><g id="Layer_4" data-name="Layer 4"><path class="cls-5" d="M44.23,59.51l-.26.08a1.53,1.53,0,0,1-1.91-1L37,42.05a1.52,1.52,0,0,1,1-1.91l.25-.08a1.54,1.54,0,0,1,1.91,1l5.07,16.53A1.53,1.53,0,0,1,44.23,59.51Z"/></g><path class="cls-6" d="M19.47,32.68l13-2.16a.17.17,0,0,0,0-.34L19.47,28a.17.17,0,0,0-.2.17v4.33A.17.17,0,0,0,19.47,32.68Z"/><g id="Layer_3-2" data-name="Layer 3"><path class="cls-6" d="M23.61,41.74l8.88-6.9a.17.17,0,0,0-.15-.3l-10.83,3a.17.17,0,0,0-.11.24l2,3.86A.17.17,0,0,0,23.61,41.74Z"/></g><g id="Layer_4-2" data-name="Layer 4"><path class="cls-6" d="M21.51,23.11l10.83,3a.17.17,0,0,0,.15-.3L23.61,19a.17.17,0,0,0-.26.06l-2,3.86A.17.17,0,0,0,21.51,23.11Z"/></g></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_no_internet.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_no_internet.svg
new file mode 100644
index 0000000000..cdb183fb97
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_no_internet.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#linear-gradient-3);}.cls-6{fill:url(#linear-gradient-4);}.cls-7{fill:url(#linear-gradient-5);}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="29.85" y1="50.09" x2="66.88" y2="13.07" gradientUnits="userSpaceOnUse"><stop offset="0.23" stop-color="#e31587"/><stop offset="1" stop-color="#ff298a"/></linearGradient><linearGradient id="linear-gradient-2" x1="38.42" y1="58.65" x2="75.44" y2="21.63" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="35.56" y1="55.8" x2="72.58" y2="18.77" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-4" x1="32.71" y1="52.94" x2="69.73" y2="15.92" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-5" x1="24.89" y1="60.37" x2="73.67" y2="11.59" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff298a"/><stop offset="1" stop-color="#ff7139"/></linearGradient></defs><title>fx-fenix_error_3</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M242.83,38.66a8,8,0,0,1-7.92,8H203.09a6.08,6.08,0,0,1,0-12.15,5.9,5.9,0,0,1,1.46.18,6,6,0,0,1-.17-1.41,6.28,6.28,0,0,1,9.81-5.17,10.53,10.53,0,0,1,20.72,2.65A7.94,7.94,0,0,1,242.83,38.66Z"/><path class="cls-2" d="M113.32,46.27a9.23,9.23,0,0,0-2.18.27,9.84,9.84,0,0,0,.24-2.12,9.45,9.45,0,0,0-14.79-7.78,15.87,15.87,0,0,0-31.23,4,11.93,11.93,0,1,0,0,23.85h48a9.11,9.11,0,1,0,0-18.21Z"/><path class="cls-3" d="M66.55,32a2.3,2.3,0,0,1-1.36-.44,25.62,25.62,0,0,0-29.89,0,2.34,2.34,0,1,1-2.73-3.8,30.28,30.28,0,0,1,35.34,0A2.34,2.34,0,0,1,66.55,32Z"/><path class="cls-4" d="M54.31,49.17A2.3,2.3,0,0,1,53,48.73a4.69,4.69,0,0,0-5.42,0,2.34,2.34,0,1,1-2.72-3.8,9.42,9.42,0,0,1,10.87,0,2.34,2.34,0,0,1-1.37,4.24Z"/><path class="cls-5" d="M58.39,43.46A2.3,2.3,0,0,1,57,43a11.82,11.82,0,0,0-13.58,0,2.34,2.34,0,0,1-2.73-3.8,16.57,16.57,0,0,1,19,0,2.34,2.34,0,0,1-1.37,4.24Z"/><path class="cls-6" d="M62.47,37.75a2.3,2.3,0,0,1-1.36-.44,18.92,18.92,0,0,0-21.74,0,2.34,2.34,0,1,1-2.72-3.8,23.27,23.27,0,0,1,27.18,0,2.34,2.34,0,0,1-1.36,4.24Z"/><g id="Layer_5" data-name="Layer 5"><path class="cls-7" d="M50.29,4a31,31,0,1,0,31,31A31,31,0,0,0,50.29,4ZM32.08,53.17a25.73,25.73,0,0,1-1.65-34.58L66.67,54.83a25.76,25.76,0,0,1-34.59-1.66ZM70.35,51.1,34.16,14.9A25.74,25.74,0,0,1,70.35,51.1Z"/></g></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_question_file.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_question_file.svg
new file mode 100644
index 0000000000..9bc4e60044
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_question_file.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#linear-gradient-3);}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="25.2" y1="59.03" x2="68.31" y2="15.92" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd567"/><stop offset="1" stop-color="#fff361"/></linearGradient><linearGradient id="linear-gradient-2" x1="-5.38" y1="-38.79" x2="59.3" y2="55.74" gradientUnits="userSpaceOnUse"><stop offset="0.35" stop-color="#ffd567"/><stop offset="0.9" stop-color="#ffa436"/></linearGradient><linearGradient id="linear-gradient-3" x1="28.71" y1="53.17" x2="63.08" y2="18.8" gradientUnits="userSpaceOnUse"><stop offset="0.26" stop-color="#e31587"/><stop offset="0.63" stop-color="#ff298a"/><stop offset="1" stop-color="#ff8a50"/></linearGradient></defs><title>fx-fenix_error_12</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M109.55,21.81a8,8,0,0,1-7.91,8H69.81a6.08,6.08,0,0,1,0-12.16,5.84,5.84,0,0,1,1.46.19,6,6,0,0,1-.17-1.41,6.28,6.28,0,0,1,9.81-5.17,10.54,10.54,0,0,1,20.73,2.65A7.94,7.94,0,0,1,109.55,21.81Z"/><path class="cls-2" d="M189.11,48.67a9.31,9.31,0,0,0-2.19.26,9.22,9.22,0,0,0,.24-2.11A9.45,9.45,0,0,0,172.38,39a15.88,15.88,0,0,0-31.24,4,11.93,11.93,0,1,0,0,23.86h48a9.11,9.11,0,1,0,0-18.21Z"/><path class="cls-3" d="M68,63.86,32.38,64a5.28,5.28,0,0,1-5.31-5.26l-.2-49a5.29,5.29,0,0,1,5.27-5.32l22.21-.09A5.26,5.26,0,0,1,58.1,5.86L71.6,19.21A5.28,5.28,0,0,1,73.17,23l.15,35.59A5.31,5.31,0,0,1,68,63.86Z"/><g id="Layer_4" data-name="Layer 4"><path class="cls-4" d="M48.71,12.73H35.54a1.35,1.35,0,0,1,0-2.7H48.71a1.35,1.35,0,1,1,0,2.7Zm1.35,7.48a1.35,1.35,0,0,0-1.35-1.34H35.54a1.35,1.35,0,1,0,0,2.69H48.71A1.35,1.35,0,0,0,50.06,20.21Zm0,8.84a1.35,1.35,0,0,0-1.35-1.35H35.54a1.35,1.35,0,0,0,0,2.7H48.71A1.36,1.36,0,0,0,50.06,29.05Zm11.4,8.83a1.35,1.35,0,0,0-1.35-1.35H35.54a1.35,1.35,0,0,0,0,2.7H60.11A1.36,1.36,0,0,0,61.46,37.88ZM66,29.05a1.34,1.34,0,0,0-1.35-1.35H55.56a1.35,1.35,0,0,0,0,2.7h9.09A1.35,1.35,0,0,0,66,29.05ZM56.57,46.71a1.35,1.35,0,0,0-1.35-1.35H35.54a1.35,1.35,0,0,0,0,2.7H55.22A1.35,1.35,0,0,0,56.57,46.71Zm8,8.83a1.35,1.35,0,0,0-1.35-1.35H35.54a1.35,1.35,0,0,0,0,2.7h27.7A1.34,1.34,0,0,0,64.59,55.54Z"/></g><path class="cls-5" d="M59.71,25.83c0,7-7.81,6.56-7.81,11.41v0a1,1,0,0,1-1,1H46.63a1,1,0,0,1-1-1V37c0-7.5,6.86-7,6.86-10.54,0-1.52-1.13-2.43-3-2.43A6.52,6.52,0,0,0,45,26a1,1,0,0,1-1.32.06l-2.8-2.19a1,1,0,0,1-.11-1.5,12.52,12.52,0,0,1,9.41-3.85C56.54,18.55,59.71,22,59.71,25.83ZM52.77,46.18a4,4,0,1,1-4-4A3.94,3.94,0,0,1,52.77,46.18Z"/></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_shred_file.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_shred_file.svg
new file mode 100644
index 0000000000..a7df64add3
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_shred_file.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#linear-gradient-3);}.cls-6{fill:url(#linear-gradient-4);}.cls-7{fill:url(#linear-gradient-5);}.cls-8{fill:url(#linear-gradient-6);}.cls-9{fill:url(#radial-gradient-2);}.cls-10{fill:url(#linear-gradient-7);}.cls-11{fill:url(#linear-gradient-8);}.cls-12{fill:url(#linear-gradient-9);}.cls-13{fill:url(#linear-gradient-10);}.cls-14{fill:url(#linear-gradient-11);}.cls-15{fill:url(#linear-gradient-12);}.cls-16{fill:url(#linear-gradient-13);}.cls-17{fill:url(#linear-gradient-14);}.cls-18{fill:url(#linear-gradient-15);}.cls-19{fill:url(#linear-gradient-16);}.cls-20{fill:url(#linear-gradient-17);}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="15.02" y1="84.89" x2="70.79" y2="29.12" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd567"/><stop offset="1" stop-color="#fff361"/></linearGradient><linearGradient id="linear-gradient-2" x1="18.75" y1="73.73" x2="74.52" y2="17.96" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="0.09" y1="96.83" x2="55.86" y2="41.06" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-4" x1="6.43" y1="77.84" x2="62.2" y2="22.07" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-5" x1="20.2" y1="58.66" x2="75.97" y2="2.89" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-6" x1="21.49" y1="98.33" x2="77.26" y2="42.56" xlink:href="#linear-gradient"/><radialGradient id="radial-gradient-2" cx="15.16" cy="-31.08" r="110.94" gradientUnits="userSpaceOnUse"><stop offset="0.35" stop-color="#ffea80"/><stop offset="1" stop-color="#ffbd4f"/></radialGradient><linearGradient id="linear-gradient-7" x1="6.05" y1="-29.94" x2="57.88" y2="45.81" gradientUnits="userSpaceOnUse"><stop offset="0.35" stop-color="#ffd567"/><stop offset="0.9" stop-color="#ffa436"/></linearGradient><linearGradient id="linear-gradient-8" x1="12.83" y1="-34.59" x2="64.66" y2="41.16" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-9" x1="-4.9" y1="-22.45" x2="46.93" y2="53.3" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-10" x1="0.69" y1="-26.28" x2="52.52" y2="49.47" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-11" x1="6.28" y1="-30.11" x2="58.11" y2="45.64" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-12" x1="16.05" y1="-36.79" x2="67.88" y2="38.96" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-13" x1="-1.6" y1="-24.71" x2="50.23" y2="51.04" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-14" x1="3.99" y1="-28.54" x2="55.82" y2="47.21" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-15" x1="6.24" y1="-30.07" x2="58.07" y2="45.68" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-16" x1="9.53" y1="-32.33" x2="61.36" y2="43.42" xlink:href="#linear-gradient-7"/><linearGradient id="linear-gradient-17" x1="10.99" y1="1.15" x2="76.77" y2="54.97" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff298a"/><stop offset="1" stop-color="#ff7139"/></linearGradient></defs><title>fx-fenix_error_13</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M241.3,32.53a8,8,0,0,1-7.92,7.95H201.56a6.08,6.08,0,0,1,0-12.15,6.26,6.26,0,0,1,1.45.18,6.48,6.48,0,0,1-.16-1.41,6.26,6.26,0,0,1,9.81-5.16,10.53,10.53,0,0,1,20.72,2.64A8,8,0,0,1,241.3,32.53Z"/><path class="cls-2" d="M140.9,48.57a9.25,9.25,0,0,0-2.19.27,9.25,9.25,0,0,0,.24-2.12,9.45,9.45,0,0,0-14.78-7.78,15.88,15.88,0,0,0-31.24,4,11.93,11.93,0,1,0,0,23.85h48a9.11,9.11,0,1,0,0-18.21Z"/><polygon class="cls-3" points="47.88 47 47.88 52.66 52.31 52.64 52.31 47 47.88 47"/><rect class="cls-4" x="47.88" y="39.93" width="4.43" height="4.91"/><polygon class="cls-5" points="39.67 52.21 39.67 57.89 44.1 57.88 44.1 52.21 39.67 52.21"/><rect class="cls-6" x="39.67" y="39.93" width="4.43" height="4.91"/><path class="cls-7" d="M68.59,19.81a4.26,4.26,0,0,0-1.26-3L56.51,6.11a4.27,4.27,0,0,0-3-1.23L35.71,5a4.25,4.25,0,0,0-4.23,4.26l.17,39.27a4.23,4.23,0,0,0,4.24,4.21v-18h3.78v3H44.1v-3h3.78v3h4.43v-3h3.78v10.1h4.43V34.74H64.3V52.59h.18a4.24,4.24,0,0,0,4.22-4.27ZM38.43,9.46H49a1.08,1.08,0,0,1,0,2.16H38.43a1.08,1.08,0,1,1,0-2.16Zm0,7.07H49a1.09,1.09,0,0,1,0,2.17H38.43a1.09,1.09,0,0,1,0-2.17Zm0,7.08H49a1.08,1.08,0,1,1,0,2.16H38.43a1.08,1.08,0,0,1,0-2.16Zm19.69,9.24H38.43a1.08,1.08,0,1,1,0-2.16H58.12a1.08,1.08,0,1,1,0,2.16Zm3.64-7.08H54.48a1.08,1.08,0,1,1,0-2.16h7.28a1.08,1.08,0,1,1,0,2.16Z"/><polygon class="cls-8" points="56.09 58.72 56.09 64.33 60.52 64.31 60.52 58.72 56.09 58.72"/><path class="cls-9" d="M58.12,30.69H38.43a1.08,1.08,0,1,0,0,2.16H58.12a1.08,1.08,0,1,0,0-2.16Z"/><path class="cls-10" d="M58.12,30.69H38.43a1.08,1.08,0,1,0,0,2.16H58.12a1.08,1.08,0,1,0,0-2.16Z"/><path class="cls-9" d="M38.43,11.62H49a1.08,1.08,0,0,0,0-2.16H38.43a1.08,1.08,0,1,0,0,2.16Z"/><path class="cls-11" d="M38.43,11.62H49a1.08,1.08,0,0,0,0-2.16H38.43a1.08,1.08,0,1,0,0,2.16Z"/><rect class="cls-9" x="39.67" y="44.84" width="4.43" height="2.16"/><rect class="cls-12" x="39.67" y="44.84" width="4.43" height="2.16"/><rect class="cls-9" x="47.88" y="44.84" width="4.43" height="2.16"/><rect class="cls-13" x="47.88" y="44.84" width="4.43" height="2.16"/><rect class="cls-9" x="56.09" y="44.84" width="4.43" height="2.16"/><rect class="cls-14" x="56.09" y="44.84" width="4.43" height="2.16"/><path class="cls-9" d="M61.76,23.61H54.48a1.08,1.08,0,1,0,0,2.16h7.28a1.08,1.08,0,1,0,0-2.16Z"/><path class="cls-15" d="M61.76,23.61H54.48a1.08,1.08,0,1,0,0,2.16h7.28a1.08,1.08,0,1,0,0-2.16Z"/><rect class="cls-9" x="39.67" y="37.76" width="4.43" height="2.16"/><rect class="cls-16" x="39.67" y="37.76" width="4.43" height="2.16"/><rect class="cls-9" x="47.88" y="37.76" width="4.43" height="2.16"/><rect class="cls-17" x="47.88" y="37.76" width="4.43" height="2.16"/><path class="cls-9" d="M38.43,25.77H49a1.08,1.08,0,1,0,0-2.16H38.43a1.08,1.08,0,0,0,0,2.16Z"/><path class="cls-18" d="M38.43,25.77H49a1.08,1.08,0,1,0,0-2.16H38.43a1.08,1.08,0,0,0,0,2.16Z"/><path class="cls-9" d="M38.43,18.7H49a1.09,1.09,0,0,0,0-2.17H38.43a1.09,1.09,0,0,0,0,2.17Z"/><path class="cls-19" d="M38.43,18.7H49a1.09,1.09,0,0,0,0-2.17H38.43a1.09,1.09,0,0,0,0,2.17Z"/><path class="cls-20" d="M76.12,35.39H24.19a2.33,2.33,0,0,1-2.34-1.87,2.25,2.25,0,0,1,2.22-2.63H76a2.37,2.37,0,0,1,2.4,1.92A2.25,2.25,0,0,1,76.12,35.39Z"/></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_surprised.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_surprised.svg
new file mode 100644
index 0000000000..94889f53d0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_surprised.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#radial-gradient);}.cls-2{fill:#cdcdd4;opacity:0.15;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:#cc3d00;}.cls-6{fill:url(#linear-gradient-3);}.cls-7{fill:url(#linear-gradient-4);}.cls-8{fill:url(#linear-gradient-5);}.cls-9{fill:url(#linear-gradient-6);}.cls-10{fill:url(#linear-gradient-7);}</style><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient" x1="-1068.38" y1="60.76" x2="-1119.19" y2="9.95" gradientTransform="matrix(-1, 0, 0, 1, -1043.81, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd567"/><stop offset="1" stop-color="#fff361"/></linearGradient><linearGradient id="linear-gradient-2" x1="-1144.21" y1="-15.08" x2="-1026.95" y2="102.18" gradientTransform="matrix(-1, 0, 0, 1, -1043.81, 0)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="#ffd567"/><stop offset="0.86" stop-color="#ffc456"/><stop offset="1" stop-color="#ffbd4f"/></linearGradient><linearGradient id="linear-gradient-3" x1="21.26" y1="-19.95" x2="85.41" y2="44.2" gradientUnits="userSpaceOnUse"><stop offset="0.25" stop-color="#ffd567"/><stop offset="1" stop-color="#ffa436"/></linearGradient><linearGradient id="linear-gradient-4" x1="2.02" y1="-0.71" x2="66.17" y2="63.44" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-5" x1="16.77" y1="-15.46" x2="80.92" y2="48.69" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-6" x1="9.32" y1="-8.02" x2="73.47" y2="56.13" xlink:href="#linear-gradient-3"/><linearGradient id="linear-gradient-7" x1="-0.86" y1="2.17" x2="63.29" y2="66.32" xlink:href="#linear-gradient-3"/></defs><title>fx-fenix_error_9</title><path class="cls-1" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-2" d="M216.79,21.81a8,8,0,0,1-7.92,8H177.05a6.08,6.08,0,0,1,0-12.16,5.76,5.76,0,0,1,1.45.19,6.62,6.62,0,0,1-.16-1.41,6.28,6.28,0,0,1,9.81-5.17,10.53,10.53,0,0,1,20.72,2.65A8,8,0,0,1,216.79,21.81Z"/><path class="cls-2" d="M152.39,26.36a8.76,8.76,0,0,0-2.19.27,9.31,9.31,0,0,0,.24-2.12,9.45,9.45,0,0,0-14.78-7.79,15.88,15.88,0,0,0-31.24,4,11.93,11.93,0,1,0,0,23.85h48a9.11,9.11,0,1,0,0-18.21Z"/><path class="cls-3" d="M50.44,3.88a31,31,0,0,1,31,31c0,.42,0,.85,0,1.27a3.67,3.67,0,0,1-.52,7.31h-.66A31,31,0,1,1,50.44,3.88Z"/><path class="cls-4" d="M19.43,36.16c0-.42,0-.85,0-1.27A30.88,30.88,0,0,1,28,13.52,31.87,31.87,0,0,0,71.8,57.37,31,31,0,0,1,20.62,43.48h-.7a3.68,3.68,0,0,1-.49-7.32Z"/><circle class="cls-5" cx="55.56" cy="46.16" r="4.02"/><path class="cls-5" d="M69,34.2v-3a3.39,3.39,0,1,0-6.78,0v3Z"/><path class="cls-6" d="M69.63,25.48h-8a1.07,1.07,0,0,1,0-2.14h8a1.07,1.07,0,1,1,0,2.14Z"/><path class="cls-7" d="M55.51,54.31a5.27,5.27,0,0,1-2.2-.49,1.06,1.06,0,1,1,.91-1.92,3,3,0,0,0,2.66,0,1.07,1.07,0,0,1,1,1.91A5.28,5.28,0,0,1,55.51,54.31Z"/><path class="cls-8" d="M69,34.45H62.27a1.07,1.07,0,1,1,0-2.13H69a1.07,1.07,0,0,1,0,2.13Z"/><path class="cls-9" d="M38.8,16.35a1,1,0,0,1,.7.27,1.06,1.06,0,0,1,.08,1.5,6.44,6.44,0,0,1-4.82,2.17h0a6.44,6.44,0,0,1-4.82-2.17,1.06,1.06,0,1,1,1.6-1.4,4.34,4.34,0,0,0,3.22,1.45h0A4.27,4.27,0,0,0,38,16.72,1,1,0,0,1,38.8,16.35Z"/><path class="cls-5" d="M34.77,25.45a3.4,3.4,0,0,1,3.41,3.4v7.64c0,1.88-1.52,2.1-3.41,2.1h0c-1.88,0-3.4-.22-3.4-2.1V28.85a3.4,3.4,0,0,1,3.4-3.4Z"/><path class="cls-10" d="M38.16,38.9h-6.7a1.07,1.07,0,1,1,0-2.13h6.7a1.07,1.07,0,1,1,0,2.13Z"/></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_unplugged.svg b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_unplugged.svg
new file mode 100644
index 0000000000..719986efbf
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/assets/mozac_error_unplugged.svg
@@ -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/. -->
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 70"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#radial-gradient);}.cls-3{fill:#cdcdd4;opacity:0.15;}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:#ededf0;}.cls-6{fill:url(#linear-gradient-3);}.cls-7{fill:url(#linear-gradient-4);}.cls-8{clip-path:url(#clip-path);}.cls-9{fill:url(#linear-gradient-6);}.cls-10{fill:url(#linear-gradient-7);}.cls-11{fill:url(#linear-gradient-8);}.cls-12{fill:url(#linear-gradient-9);}.cls-13{fill:url(#linear-gradient-10);}.cls-14{fill:url(#linear-gradient-11);}.cls-15{fill:url(#linear-gradient-12);}.cls-16{fill:url(#linear-gradient-13);}.cls-17{fill:url(#linear-gradient-14);}.cls-18{fill:url(#linear-gradient-15);}.cls-19{fill:url(#linear-gradient-16);}.cls-20{fill:url(#linear-gradient-17);}.cls-21{fill:url(#linear-gradient-18);}.cls-22{fill:url(#linear-gradient-19);}.cls-23{fill:url(#linear-gradient-20);}.cls-24{fill:url(#linear-gradient-21);}.cls-25{fill:url(#linear-gradient-22);}.cls-26{fill:url(#linear-gradient-23);}.cls-27{fill:url(#linear-gradient-24);}.cls-28{fill:url(#linear-gradient-25);}.cls-29{fill:url(#linear-gradient-26);}.cls-30{fill:url(#linear-gradient-27);}.cls-31{fill:url(#linear-gradient-28);}.cls-32{fill:url(#linear-gradient-29);}.cls-33{fill:url(#linear-gradient-30);}.cls-34{fill:url(#linear-gradient-31);}.cls-35{fill:url(#linear-gradient-32);}.cls-36{fill:url(#linear-gradient-33);}.cls-37{fill:url(#linear-gradient-34);}.cls-38{fill:url(#linear-gradient-35);}.cls-39{fill:url(#linear-gradient-36);}.cls-40{fill:url(#linear-gradient-37);}.cls-41{fill:url(#linear-gradient-38);}.cls-42{fill:url(#linear-gradient-39);}.cls-43{fill:url(#linear-gradient-40);}.cls-44{fill:url(#linear-gradient-41);}.cls-45{fill:url(#linear-gradient-42);}.cls-46{fill:url(#linear-gradient-43);}.cls-47{fill:url(#linear-gradient-44);}.cls-48{fill:url(#linear-gradient-45);}.cls-49{fill:url(#linear-gradient-46);}.cls-50{fill:url(#linear-gradient-47);}.cls-51{fill:url(#linear-gradient-48);}.cls-52{fill:url(#linear-gradient-49);}.cls-53{fill:url(#linear-gradient-50);}.cls-54{fill:url(#linear-gradient-51);}.cls-55{fill:url(#linear-gradient-52);}.cls-56{fill:url(#linear-gradient-53);}.cls-57{fill:url(#linear-gradient-54);}.cls-58{fill:url(#linear-gradient-55);}.cls-59{fill:url(#linear-gradient-56);}.cls-60{fill:url(#linear-gradient-57);}.cls-61{fill:url(#linear-gradient-58);}.cls-62{fill:url(#linear-gradient-59);}.cls-63{fill:url(#linear-gradient-60);}.cls-64{fill:url(#linear-gradient-61);}.cls-65{fill:url(#linear-gradient-62);}.cls-66{fill:url(#linear-gradient-63);}.cls-67{fill:url(#linear-gradient-64);}.cls-68{fill:url(#linear-gradient-65);}.cls-69{fill:url(#linear-gradient-66);}.cls-70{fill:url(#linear-gradient-67);}.cls-71{fill:url(#linear-gradient-68);}.cls-72{fill:#ffbd4f;}.cls-73{fill:#cc3d00;}</style><linearGradient id="linear-gradient" x1="70.63" y1="-19.77" x2="143.74" y2="53.34" gradientUnits="userSpaceOnUse"><stop offset="0.44" stop-color="#ff8a50"/><stop offset="0.91" stop-color="#ff298a"/></linearGradient><radialGradient id="radial-gradient" cx="143.54" cy="81.07" r="146.71" gradientTransform="translate(0 24.46) scale(1 0.7)" gradientUnits="userSpaceOnUse"><stop offset="0.08" stop-color="#cdcdd4" stop-opacity="0"/><stop offset="0.36" stop-color="#cdcdd4" stop-opacity="0.02"/><stop offset="0.65" stop-color="#cdcdd4" stop-opacity="0.08"/><stop offset="0.94" stop-color="#cdcdd4" stop-opacity="0.18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity="0.2"/></radialGradient><linearGradient id="linear-gradient-2" x1="89.14" y1="29.87" x2="133.56" y2="-24.13" gradientUnits="userSpaceOnUse"><stop offset="0.07" stop-color="#e31587"/><stop offset="0.29" stop-color="#ff298a"/><stop offset="0.7" stop-color="#ff8a50"/></linearGradient><linearGradient id="linear-gradient-3" x1="44.2" y1="6.66" x2="117.31" y2="79.77" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-4" x1="62.2" y1="-11.34" x2="135.31" y2="61.77" xlink:href="#linear-gradient"/><clipPath id="clip-path"><path class="cls-1" d="M98.33,21.53l13.6-13.6a1.66,1.66,0,0,1,2.35,0h0a1.66,1.66,0,0,1,0,2.35l-.45.45a2.88,2.88,0,0,1,.34.27l6,6.06a3.65,3.65,0,0,1,.49,4.56,1,1,0,0,0,.14,1.24,4.67,4.67,0,0,1,1,5.09l4.31,5.79a2.12,2.12,0,0,1-.43,3,2.08,2.08,0,0,1-1.27.42,2.13,2.13,0,0,1-1.71-.85L119,31.16a4.66,4.66,0,0,1-5.69-.7,1,1,0,0,0-1.25-.14,3.66,3.66,0,0,1-4.56-.49l-6.06-6.06a2.24,2.24,0,0,1-.27-.34l-.45.45a1.66,1.66,0,0,1-2.35,0h0A1.68,1.68,0,0,1,98.33,21.53Z"/></clipPath><linearGradient id="linear-gradient-6" x1="85.1" y1="-34.24" x2="158.21" y2="38.87" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-7" x1="84.82" y1="-33.96" x2="157.93" y2="39.15" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-8" x1="84.53" y1="-33.67" x2="157.64" y2="39.44" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-9" x1="84.24" y1="-33.38" x2="157.35" y2="39.73" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-10" x1="83.94" y1="-33.08" x2="157.05" y2="40.02" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-11" x1="83.65" y1="-32.79" x2="156.76" y2="40.32" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-12" x1="83.36" y1="-32.5" x2="156.47" y2="40.61" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-13" x1="83.07" y1="-32.21" x2="156.18" y2="40.9" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-14" x1="82.77" y1="-31.91" x2="155.88" y2="41.19" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-15" x1="82.48" y1="-31.62" x2="155.59" y2="41.49" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-16" x1="82.19" y1="-31.33" x2="155.3" y2="41.78" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-17" x1="81.9" y1="-31.04" x2="155.01" y2="42.07" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-18" x1="81.6" y1="-30.74" x2="154.71" y2="42.36" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-19" x1="81.31" y1="-30.45" x2="154.42" y2="42.66" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-20" x1="81.02" y1="-30.16" x2="154.13" y2="42.95" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-21" x1="80.73" y1="-29.87" x2="153.84" y2="43.24" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-22" x1="80.43" y1="-29.57" x2="153.54" y2="43.53" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-23" x1="80.14" y1="-29.28" x2="153.25" y2="43.83" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-24" x1="79.85" y1="-28.99" x2="152.96" y2="44.12" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-25" x1="79.56" y1="-28.7" x2="152.67" y2="44.41" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-26" x1="79.26" y1="-28.4" x2="152.37" y2="44.7" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-27" x1="78.97" y1="-28.11" x2="152.08" y2="45" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-28" x1="78.68" y1="-27.82" x2="151.79" y2="45.29" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-29" x1="78.39" y1="-27.53" x2="151.5" y2="45.58" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-30" x1="78.09" y1="-27.23" x2="151.2" y2="45.87" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-31" x1="77.8" y1="-26.94" x2="150.91" y2="46.17" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-32" x1="77.51" y1="-26.65" x2="150.62" y2="46.46" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-33" x1="77.21" y1="-26.35" x2="150.32" y2="46.76" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-34" x1="76.92" y1="-26.06" x2="150.02" y2="47.05" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-35" x1="76.62" y1="-25.76" x2="149.73" y2="47.35" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-36" x1="76.32" y1="-25.46" x2="149.43" y2="47.65" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-37" x1="76.02" y1="-25.16" x2="149.13" y2="47.95" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-38" x1="75.73" y1="-24.87" x2="148.84" y2="48.24" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-39" x1="75.43" y1="-24.57" x2="148.54" y2="48.54" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-40" x1="75.13" y1="-24.27" x2="148.24" y2="48.84" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-41" x1="74.84" y1="-23.97" x2="147.94" y2="49.13" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-42" x1="74.54" y1="-23.68" x2="147.65" y2="49.43" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-43" x1="74.24" y1="-23.38" x2="147.35" y2="49.73" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-44" x1="73.94" y1="-23.08" x2="147.05" y2="50.03" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-45" x1="73.65" y1="-22.79" x2="146.76" y2="50.32" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-46" x1="73.35" y1="-22.49" x2="146.46" y2="50.62" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-47" x1="73.05" y1="-22.19" x2="146.16" y2="50.92" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-48" x1="72.76" y1="-21.89" x2="145.86" y2="51.21" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-49" x1="72.46" y1="-21.6" x2="145.57" y2="51.51" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-50" x1="72.16" y1="-21.3" x2="145.27" y2="51.81" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-51" x1="71.86" y1="-21" x2="144.97" y2="52.11" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-52" x1="71.57" y1="-20.71" x2="144.68" y2="52.4" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-53" x1="71.27" y1="-20.41" x2="144.38" y2="52.7" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-54" x1="70.98" y1="-20.12" x2="144.09" y2="52.99" gradientTransform="translate(48.26 -72.48) rotate(43.03)" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-55" x1="70.69" y1="-19.83" x2="143.8" y2="53.28" gradientTransform="matrix(0.73, 0.68, -0.68, 0.73, 48.37, -72.2)" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-56" x1="70.4" y1="-19.53" x2="143.5" y2="53.57" gradientTransform="translate(48.48 -71.92) rotate(43.03)" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-57" x1="70.11" y1="-19.25" x2="143.21" y2="53.86" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-58" x1="69.82" y1="-18.96" x2="142.93" y2="54.15" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-59" x1="69.53" y1="-18.67" x2="142.64" y2="54.44" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-60" x1="69.25" y1="-18.38" x2="142.35" y2="54.72" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-61" x1="68.96" y1="-18.1" x2="142.07" y2="55.01" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-62" x1="68.67" y1="-17.81" x2="141.78" y2="55.3" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-63" x1="68.38" y1="-17.52" x2="141.49" y2="55.59" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-64" x1="68.1" y1="-17.24" x2="141.21" y2="55.87" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-65" x1="67.81" y1="-16.95" x2="140.92" y2="56.16" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-66" x1="61.85" y1="-10.99" x2="134.96" y2="62.12" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-67" x1="-1068.45" y1="61" x2="-1119.26" y2="10.19" gradientTransform="matrix(-1, 0, 0, 1, -1043.81, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd567"/><stop offset="1" stop-color="#fff361"/></linearGradient><linearGradient id="linear-gradient-68" x1="-1144.28" y1="-14.84" x2="-1027.02" y2="102.42" gradientTransform="matrix(-1, 0, 0, 1, -1043.81, 0)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="#ffd567"/><stop offset="0.86" stop-color="#ffc456"/><stop offset="1" stop-color="#ffbd4f"/></linearGradient></defs><title>fx-fenix_error_6</title><path class="cls-2" d="M4.45,35.06A23.24,23.24,0,0,1,27.6,11.73l174.6-.5a23.25,23.25,0,1,1,.18,46.49l-174.59.5A23.25,23.25,0,0,1,4.45,35.06Z"/><path class="cls-3" d="M109.55,21.81a8,8,0,0,1-7.91,8H69.81a6.08,6.08,0,0,1,0-12.16,5.84,5.84,0,0,1,1.46.19,6,6,0,0,1-.17-1.41,6.28,6.28,0,0,1,9.81-5.17,10.54,10.54,0,0,1,20.73,2.65A7.94,7.94,0,0,1,109.55,21.81Z"/><path class="cls-3" d="M237.41,25.59a9.31,9.31,0,0,0-2.19.27,9.25,9.25,0,0,0,.24-2.12A9.45,9.45,0,0,0,220.68,16a15.88,15.88,0,0,0-31.24,4,11.93,11.93,0,1,0,0,23.85h48a9.11,9.11,0,1,0,0-18.21Z"/><path class="cls-4" d="M101.94,3.8a1.2,1.2,0,0,1,1.7,0l8.41,8.84a1.21,1.21,0,0,1-1.75,1.66L101.9,5.5a1.23,1.23,0,0,1-.34-.86A1.21,1.21,0,0,1,101.94,3.8Zm-7.16,7.3a1.2,1.2,0,0,0,.33.86l8.41,8.84a1.2,1.2,0,1,0,1.74-1.66L96.85,10.3a1.2,1.2,0,0,0-1.7-.05A1.18,1.18,0,0,0,94.78,11.1Z"/><path class="cls-5" d="M62.21,54.14a.52.52,0,0,1,.52-.54h.86a.54.54,0,0,1,.53.51.54.54,0,0,1-.52.55l-.86,0h0A.53.53,0,0,1,62.21,54.14Zm4-.2a.53.53,0,0,1,.46-.59c1.34-.17,2.73-.41,4.13-.73a.53.53,0,1,1,.24,1,42.53,42.53,0,0,1-4.24.75h-.06A.53.53,0,0,1,66.23,53.94Zm-8.65.3a27.41,27.41,0,0,1-4.19-1.08.52.52,0,0,1-.33-.67.53.53,0,0,1,.68-.33,25.22,25.22,0,0,0,4,1,.53.53,0,0,1,.43.62.53.53,0,0,1-.52.44Zm-7.19-2.31-.77-.38a.53.53,0,1,1,.48-.95l.75.37a.54.54,0,0,1,.25.71.53.53,0,0,1-.48.3A.46.46,0,0,1,50.39,51.93Zm-49-3.06a.53.53,0,0,1,0-.75s.23-.24.64-.62a.53.53,0,0,1,.75,0,.53.53,0,0,1,0,.75c-.39.35-.59.56-.59.56h0a.56.56,0,0,1-.39.17A.51.51,0,0,1,1.37,48.87Zm43.82,0c-1.07-.73-2.07-1.47-3-2.18l-.41-.31a.53.53,0,1,1,.63-.85l.41.3c1,.71,2,1.44,3,2.16a.53.53,0,0,1-.3,1A.58.58,0,0,1,45.19,48.89Zm-40.8-2.7a.54.54,0,0,1,.11-.74A36.16,36.16,0,0,1,8.12,43.1a.53.53,0,0,1,.72.2.52.52,0,0,1-.2.72A37,37,0,0,0,5.13,46.3a.51.51,0,0,1-.31.11A.53.53,0,0,1,4.39,46.19Zm34.76-1.61-.7-.46a.52.52,0,0,1-.17-.73.53.53,0,0,1,.73-.17l.73.47a.53.53,0,0,1-.3,1A.51.51,0,0,1,39.15,44.58ZM34,42a30.3,30.3,0,0,0-4-1.23.53.53,0,0,1,.24-1A33.68,33.68,0,0,1,34.34,41a.53.53,0,0,1-.19,1A.63.63,0,0,1,34,42Zm-21.47-.37a.55.55,0,0,1,.31-.69l.81-.29a.53.53,0,1,1,.35,1l-.78.29a.63.63,0,0,1-.19,0A.54.54,0,0,1,12.49,41.58Zm3.84-1.25a.53.53,0,0,1,.39-.64A28.55,28.55,0,0,1,21,39a.53.53,0,0,1,.57.48.54.54,0,0,1-.48.58,27.68,27.68,0,0,0-4.11.66l-.13,0A.52.52,0,0,1,16.33,40.33Zm10.57-.17-.83-.09a.52.52,0,0,1-.48-.57.53.53,0,0,1,.58-.48l.86.09A.53.53,0,0,1,27,40.17Z"/><path class="cls-6" d="M114.3,64.51a16.79,16.79,0,0,0,5.49-.93,1.11,1.11,0,0,0,.7-1.41,1.13,1.13,0,0,0-1.42-.7c-6.65,2.27-12.27-.62-18.77-4C92.56,53.51,83.78,49,71,52a1.11,1.11,0,0,0-.83,1.34,1.13,1.13,0,0,0,1.35.83A31.19,31.19,0,0,1,87,54.3a60.54,60.54,0,0,1,12.25,5.18C104.27,62.05,109,64.51,114.3,64.51Z"/><path class="cls-7" d="M118,63.12a1.43,1.43,0,0,1,.89-1.82c.25-.08.5-.17.74-.27a1.44,1.44,0,0,1,1.86.82,1.43,1.43,0,0,1-.82,1.85l-.85.32a1.53,1.53,0,0,1-.47.07A1.42,1.42,0,0,1,118,63.12Zm4.72-1.92a1.43,1.43,0,0,1,.32-2,12.62,12.62,0,0,0,2.7-2.66,1.43,1.43,0,0,1,2.31,1.7,15.47,15.47,0,0,1-3.33,3.28,1.45,1.45,0,0,1-.84.27A1.42,1.42,0,0,1,122.74,61.2Zm6-7a1.44,1.44,0,0,1-1-1.75c.06-.25.12-.5.17-.75a1.43,1.43,0,1,1,2.81.56c-.06.3-.13.61-.21.91a1.44,1.44,0,0,1-1.39,1.07A1.53,1.53,0,0,1,128.72,54.21Zm-.63-6.35a19,19,0,0,0-.71-3.87,1.42,1.42,0,0,1,1-1.77,1.44,1.44,0,0,1,1.78,1,22.57,22.57,0,0,1,.81,4.46,1.43,1.43,0,0,1-1.33,1.53h-.11A1.43,1.43,0,0,1,128.09,47.86Zm-2.5-8.33c-.12-.24-.25-.48-.38-.72a1.43,1.43,0,0,1,2.51-1.38c.15.26.29.53.42.79a1.44,1.44,0,0,1-.62,1.93,1.39,1.39,0,0,1-.65.16A1.44,1.44,0,0,1,125.59,39.53ZM123,35.38a39,39,0,0,0-2.56-3.2,1.43,1.43,0,0,1,2.14-1.9c1,1.12,1.92,2.27,2.75,3.42a1.44,1.44,0,0,1-.33,2,1.42,1.42,0,0,1-.83.27A1.45,1.45,0,0,1,123,35.38Zm-6-6.72-.61-.56a1.43,1.43,0,1,1,1.9-2.14l.65.59A1.43,1.43,0,0,1,117,28.66Zm-3.8-3.2c-.69-.53-1.41-1.06-2.13-1.57a1.43,1.43,0,0,1,1.66-2.34c.75.54,1.5,1.09,2.22,1.64a1.43,1.43,0,0,1-.88,2.57A1.41,1.41,0,0,1,113.19,25.46Z"/><path class="cls-1" d="M98.33,21.53l13.6-13.6a1.66,1.66,0,0,1,2.35,0h0a1.66,1.66,0,0,1,0,2.35l-.45.45a2.88,2.88,0,0,1,.34.27l6,6.06a3.65,3.65,0,0,1,.49,4.56,1,1,0,0,0,.14,1.24,4.67,4.67,0,0,1,1,5.09l4.31,5.79a2.12,2.12,0,0,1-.43,3,2.08,2.08,0,0,1-1.27.42,2.13,2.13,0,0,1-1.71-.85L119,31.16a4.66,4.66,0,0,1-5.69-.7,1,1,0,0,0-1.25-.14,3.66,3.66,0,0,1-4.56-.49l-6.06-6.06a2.24,2.24,0,0,1-.27-.34l-.45.45a1.66,1.66,0,0,1-2.35,0h0A1.68,1.68,0,0,1,98.33,21.53Z"/><g class="cls-8"><polygon class="cls-9" points="126.39 7.28 126.88 7.82 126.88 7.28 126.39 7.28"/><polygon class="cls-10" points="125.82 7.28 126.88 8.42 126.88 7.82 126.39 7.28 125.82 7.28"/><polygon class="cls-11" points="125.26 7.28 126.88 9.03 126.88 8.42 125.82 7.28 125.26 7.28"/><polygon class="cls-12" points="124.69 7.28 126.88 9.63 126.88 9.03 125.26 7.28 124.69 7.28"/><polygon class="cls-13" points="124.13 7.28 126.88 10.23 126.88 9.63 124.69 7.28 124.13 7.28"/><polygon class="cls-14" points="123.56 7.28 126.88 10.84 126.88 10.23 124.13 7.28 123.56 7.28"/><polygon class="cls-15" points="123 7.28 126.88 11.45 126.88 10.84 123.56 7.28 123 7.28"/><polygon class="cls-16" points="122.43 7.28 126.88 12.05 126.88 11.45 123 7.28 122.43 7.28"/><polygon class="cls-17" points="121.87 7.28 126.88 12.66 126.88 12.05 122.43 7.28 121.87 7.28"/><polygon class="cls-18" points="121.3 7.28 126.88 13.26 126.88 12.66 121.87 7.28 121.3 7.28"/><polygon class="cls-19" points="120.74 7.28 126.88 13.87 126.88 13.26 121.3 7.28 120.74 7.28"/><polygon class="cls-20" points="120.17 7.28 126.88 14.47 126.88 13.87 120.74 7.28 120.17 7.28"/><polygon class="cls-21" points="119.61 7.28 126.88 15.08 126.88 14.47 120.17 7.28 119.61 7.28"/><polygon class="cls-22" points="119.04 7.28 126.88 15.68 126.88 15.08 119.61 7.28 119.04 7.28"/><polygon class="cls-23" points="118.48 7.28 126.88 16.29 126.88 15.68 119.04 7.28 118.48 7.28"/><polygon class="cls-24" points="117.91 7.28 126.88 16.89 126.88 16.29 118.48 7.28 117.91 7.28"/><polygon class="cls-25" points="117.35 7.28 126.88 17.5 126.88 16.89 117.91 7.28 117.35 7.28"/><polygon class="cls-26" points="116.78 7.28 126.88 18.1 126.88 17.5 117.35 7.28 116.78 7.28"/><polygon class="cls-27" points="116.22 7.28 126.88 18.71 126.88 18.1 116.78 7.28 116.22 7.28"/><polygon class="cls-28" points="115.66 7.28 126.88 19.31 126.88 18.71 116.22 7.28 115.66 7.28"/><polygon class="cls-29" points="115.09 7.28 126.88 19.92 126.88 19.31 115.66 7.28 115.09 7.28"/><polygon class="cls-30" points="114.53 7.28 126.88 20.52 126.88 19.92 115.09 7.28 114.53 7.28"/><polygon class="cls-31" points="113.96 7.28 126.88 21.13 126.88 20.52 114.53 7.28 113.96 7.28"/><polygon class="cls-32" points="113.4 7.28 126.88 21.73 126.88 21.13 113.96 7.28 113.4 7.28"/><polygon class="cls-33" points="112.83 7.28 126.88 22.34 126.88 21.73 113.4 7.28 112.83 7.28"/><polygon class="cls-34" points="112.27 7.28 112.27 7.28 126.88 22.94 126.88 22.34 112.83 7.28 112.27 7.28"/><polygon class="cls-35" points="112.27 7.28 111.97 7.57 126.88 23.55 126.88 22.94 112.27 7.28"/><polygon class="cls-36" points="111.97 7.57 111.66 7.85 126.88 24.15 126.88 23.55 111.97 7.57"/><polygon class="cls-37" points="111.66 7.85 111.36 8.13 126.88 24.76 126.88 24.15 111.66 7.85"/><polygon class="cls-38" points="111.36 8.13 111.06 8.41 126.88 25.36 126.88 24.76 111.36 8.13"/><polygon class="cls-39" points="111.06 8.41 110.76 8.69 126.88 25.97 126.88 25.36 111.06 8.41"/><polygon class="cls-40" points="110.76 8.69 110.46 8.97 126.88 26.57 126.88 25.97 110.76 8.69"/><polygon class="cls-41" points="110.46 8.97 110.15 9.26 126.88 27.18 126.88 26.57 110.46 8.97"/><polygon class="cls-42" points="110.15 9.26 109.85 9.54 126.88 27.78 126.88 27.18 110.15 9.26"/><polygon class="cls-43" points="109.85 9.54 109.55 9.82 126.88 28.39 126.88 27.78 109.85 9.54"/><polygon class="cls-44" points="109.55 9.82 109.25 10.1 126.88 28.99 126.88 28.39 109.55 9.82"/><polygon class="cls-45" points="109.25 10.1 108.95 10.38 126.88 29.6 126.88 28.99 109.25 10.1"/><polygon class="cls-46" points="108.95 10.38 108.64 10.66 126.88 30.2 126.88 29.6 108.95 10.38"/><polygon class="cls-47" points="108.64 10.66 108.34 10.95 126.88 30.81 126.88 30.2 108.64 10.66"/><polygon class="cls-48" points="108.34 10.95 108.04 11.23 126.88 31.41 126.88 30.81 108.34 10.95"/><polygon class="cls-49" points="108.04 11.23 107.74 11.51 126.88 32.02 126.88 31.41 108.04 11.23"/><polygon class="cls-50" points="107.74 11.51 107.44 11.79 126.88 32.63 126.88 32.02 107.74 11.51"/><polygon class="cls-51" points="107.44 11.79 107.14 12.07 126.88 33.23 126.88 32.63 107.44 11.79"/><polygon class="cls-52" points="107.14 12.07 106.83 12.35 126.88 33.84 126.88 33.23 107.14 12.07"/><polygon class="cls-53" points="106.83 12.35 106.53 12.64 126.88 34.44 126.88 33.84 106.83 12.35"/><polygon class="cls-54" points="106.53 12.64 106.23 12.92 126.88 35.05 126.88 34.44 106.53 12.64"/><polygon class="cls-55" points="106.23 12.92 105.93 13.2 126.88 35.65 126.88 35.05 106.23 12.92"/><polygon class="cls-56" points="105.93 13.2 105.63 13.48 126.82 36.19 126.88 36.13 126.88 35.65 105.93 13.2"/><rect class="cls-57" x="115.87" y="9.44" width="0.41" height="31.06" transform="translate(14.18 85.92) rotate(-43.03)"/><rect class="cls-58" x="115.57" y="9.73" width="0.41" height="31.06" transform="translate(13.9 85.79) rotate(-43.03)"/><rect class="cls-59" x="115.26" y="10.01" width="0.41" height="31.06" transform="translate(13.63 85.66) rotate(-43.03)"/><polygon class="cls-60" points="104.72 14.33 104.42 14.61 125.45 37.14 125.8 37.14 125.92 37.03 104.72 14.33"/><polygon class="cls-61" points="104.42 14.61 104.12 14.89 124.89 37.14 125.45 37.14 104.42 14.61"/><polygon class="cls-62" points="104.12 14.89 103.82 15.17 124.32 37.14 124.89 37.14 104.12 14.89"/><polygon class="cls-63" points="103.82 15.17 103.51 15.45 123.76 37.14 124.32 37.14 103.82 15.17"/><polygon class="cls-64" points="103.51 15.45 103.21 15.73 123.19 37.14 123.76 37.14 103.51 15.45"/><polygon class="cls-65" points="103.21 15.73 102.91 16.02 122.63 37.14 123.19 37.14 103.21 15.73"/><polygon class="cls-66" points="102.91 16.02 102.61 16.3 122.06 37.14 122.63 37.14 102.91 16.02"/><polygon class="cls-67" points="102.61 16.3 102.31 16.58 121.5 37.14 122.06 37.14 102.61 16.3"/><polygon class="cls-68" points="102.31 16.58 102 16.86 120.94 37.14 121.5 37.14 102.31 16.58"/><polygon class="cls-69" points="102 16.86 97.69 20.89 97.69 37.14 120.94 37.14 102 16.86"/></g><path class="cls-70" d="M50.51,4.12a31,31,0,0,1,31,31c0,.42,0,.85,0,1.27A3.67,3.67,0,0,1,81,43.71h-.66A31,31,0,1,1,50.51,4.12Z"/><path class="cls-71" d="M19.5,36.4c0-.42,0-.85,0-1.27A30.88,30.88,0,0,1,28,13.76,31.87,31.87,0,0,0,71.87,57.61,31,31,0,0,1,20.69,43.72H20a3.68,3.68,0,0,1-.49-7.32Z"/><path class="cls-72" d="M39,20.18a1.05,1.05,0,0,1-.8-.36A4.32,4.32,0,0,0,35,18.37h0a4.29,4.29,0,0,0-3.23,1.45,1.07,1.07,0,0,1-1.59-1.42A6.45,6.45,0,0,1,35,16.23h0a6.44,6.44,0,0,1,4.83,2.17,1.05,1.05,0,0,1-.1,1.5A1,1,0,0,1,39,20.18Z"/><path class="cls-72" d="M70.37,20.18a1.07,1.07,0,0,1-.8-.36,4.3,4.3,0,0,0-3.22-1.45h0a4.29,4.29,0,0,0-3.23,1.45,1.07,1.07,0,0,1-1.59-1.42,6.41,6.41,0,0,1,4.83-2.17h0a6.41,6.41,0,0,1,4.83,2.17,1.06,1.06,0,0,1-.08,1.5A1.09,1.09,0,0,1,70.37,20.18Z"/><path class="cls-73" d="M35.17,23a3.38,3.38,0,0,1,3.38,3.39V34a3.38,3.38,0,0,1-3.38,3.38h0A3.38,3.38,0,0,1,31.79,34V26.41A3.37,3.37,0,0,1,35.17,23Z"/><path class="cls-73" d="M65.77,23a3.38,3.38,0,0,1,3.38,3.39V34a3.38,3.38,0,0,1-3.38,3.38h0A3.38,3.38,0,0,1,62.39,34V26.41A3.37,3.37,0,0,1,65.77,23Z"/><path class="cls-73" d="M50.83,40.25A6.86,6.86,0,0,0,44,47.09v9.28a1.27,1.27,0,0,0,1.27,1.27H56.42a1.27,1.27,0,0,0,1.27-1.27V47.11A6.86,6.86,0,0,0,50.83,40.25Z"/></svg> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_add_to_homescreen_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_add_to_homescreen_24.xml
new file mode 100644
index 0000000000..c9ab6e5d53
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_add_to_homescreen_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M6.25 4.5A0.75 0.75 0 0 1 7 3.75h10a0.75 0.75 0 0 1 0.75 0.75V5h1.75V4.5A2.5 2.5 0 0 0 17 2H7a2.5 2.5 0 0 0-2.5 2.5v15A2.5 2.5 0 0 0 7 22h10a2.5 2.5 0 0 0 2.5-2.5v-6h-1.75V17H6.25V4.5zM10 20.25h4v-1.5h-4v1.5z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M13 13.5h-2v2h2v-2zm-2-3.25h2v2h-2v-2zm5.25 3.25h-2v2h2v-2zm-8.5 0h2v2h-2v-2zm7.78-6.017a0.75 0.75 0 0 0-1.28 0.53V11.5c0 0.414 0.336 0.75 0.75 0.75h3.487a0.75 0.75 0 0 0 0.53-1.28l-1.125-1.125L22 5.738 20.762 4.5l-4.107 4.108-1.125-1.125z"
+ tools:ignore="VectorRaster" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_app_menu_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_app_menu_24.xml
new file mode 100644
index 0000000000..6e30f173e1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_app_menu_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.375 11.25h15.25V13H4.375v-1.75zm0 5h15.25V18H4.375v-1.75zm0-10.25h15.25v1.75H4.375V6z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_app_menu_space_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_app_menu_space_24.xml
new file mode 100644
index 0000000000..35f9896179
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_app_menu_space_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.375 11.25h15.25V13H4.375v-1.75zm0 5h15.25V18H4.375v-1.75zm0-10.25H12v1.75H4.375V6z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_down_left_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_down_left_24.xml
new file mode 100644
index 0000000000..6ff8a72414
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_down_left_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M3.251 19.376c0 0.483 0.392 0.875 0.875 0.875h9.996V18.5H6.24L20.25 4.488l-1.237-1.237L5.001 17.265V9.334h-1.75v10.042z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_up_left_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_up_left_24.xml
new file mode 100644
index 0000000000..a3e053f9fc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_up_left_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.126 3.251a0.875 0.875 0 0 0-0.875 0.875v9.996h1.75V6.24l14.012 14.01 1.238-1.237L6.237 5.001h7.931v-1.75H4.126z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_up_right_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_up_right_24.xml
new file mode 100644
index 0000000000..ec4ac86fed
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_append_up_right_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M19.376,3.251c0.483,0 0.875,0.392 0.875,0.875v9.996H18.5V6.24L4.488,20.25l-1.237,-1.237L17.265,5.001H9.334v-1.75h10.042z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_arrow_clockwise_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_arrow_clockwise_24.xml
new file mode 100644
index 0000000000..f618ea5a92
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_arrow_clockwise_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M20.447 12.751h-1.751c-0.377 3.646-3.466 6.499-7.211 6.499-3.998 0-7.25-3.252-7.25-7.25s3.252-7.25 7.25-7.25c2.768 0 5.154 1.573 6.373 3.859l-1.452 1.452A0.55 0.55 0 0 0 16.795 11h4.171a0.55 0.55 0 0 0 0.55-0.55V6.279a0.55 0.55 0 0 0-0.939-0.389l-1.422 1.422C17.572 4.73 14.73 3 11.485 3c-4.963 0-9 4.037-9 9s4.037 9 9 9c4.709 0 8.578-3.637 8.962-8.249z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_24.xml
new file mode 100644
index 0000000000..d5fd8e11e5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.751 4.819V3.068C17.363 3.452 21 7.321 21 12.03c0 4.963-4.037 9-9 9s-9-4.037-9-9c0-3.245 1.73-6.087 4.313-7.669L5.891 2.939A0.55 0.55 0 0 1 6.28 2h4.17A0.55 0.55 0 0 1 11 2.55v4.17a0.55 0.55 0 0 1-0.939 0.389L8.609 5.657C6.323 6.876 4.75 9.262 4.75 12.03c0 3.998 3.252 7.25 7.25 7.25s7.25-3.252 7.25-7.25c0-3.745-2.853-6.834-6.499-7.211z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9.5 9.055v5.89a0.803 0.803 0 0 0 1.202 0.697l5.154-2.945a0.803 0.803 0 0 0 0-1.395l-5.154-2.945A0.804 0.804 0 0 0 9.5 9.055z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_slash_24.xml
new file mode 100644
index 0000000000..2b90a56673
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_slash_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 6.72a0.55 0.55 0 0 1-0.936 0.391L6.89 2.936A0.549 0.549 0 0 1 7.28 2h4.17A0.55 0.55 0 0 1 12 2.55v4.17zm7.395 8.72a7.21 7.21 0 0 0 0.855-3.41c0-3.744-2.852-6.834-6.499-7.211V3.068C18.363 3.452 22 7.321 22 12.03a8.93 8.93 0 0 1-1.33 4.685l-1.275-1.275zm-2.669-2.668l0.13-0.074a0.803 0.803 0 0 0 0-1.395l-3.734-2.134 3.604 3.603z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M22.25 20.762L3.488 2 2.25 3.238l3.5 3.499A8.87 8.87 0 0 0 4 12.03c0 4.963 4.037 9 9 9a8.92 8.92 0 0 0 5.301-1.741l2.711 2.71 1.238-1.237zM5.75 12.03c0-1.502 0.465-2.888 1.255-4.037l3.495 3.495v3.457a0.804 0.804 0 0 0 1.202 0.698l1.88-1.074 3.465 3.466A7.18 7.18 0 0 1 13 19.28c-3.998 0-7.25-3.252-7.25-7.25z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_circle_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_circle_24.xml
new file mode 100644
index 0000000000..0bf6b500f1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_circle_24.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 6a3.006 3.006 0 0 0-2.897 3.799 2.967 2.967 0 0 0 2.098 2.098A3.006 3.006 0 0 0 15 9a3 3 0 0 0-3-3z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 3.75c4.549 0 8.25 3.701 8.25 8.25 0 4.549-3.701 8.25-8.25 8.25-4.549 0-8.25-3.701-8.25-8.25 0-4.549 3.701-8.25 8.25-8.25zM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 18.75a6.753 6.753 0 0 0 5.908-3.492A2.491 2.491 0 0 0 15.75 14h-7.5c-0.926 0-1.726 0.51-2.158 1.258A6.753 6.753 0 0 0 12 18.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_circle_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_circle_fill_24.xml
new file mode 100644
index 0000000000..b4726ed624
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_circle_fill_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M10.979 21.948a10.074 10.074 0 0 0 2.48-0.054 9.958 9.958 0 0 0 4.791-2.095v0.002A9.986 9.986 0 0 0 22 12c0-5.514-4.486-10-10-10S2 6.486 2 12a9.986 9.986 0 0 0 3.75 7.8 9.951 9.951 0 0 0 5.229 2.15zM9.103 10.8A3.006 3.006 0 0 1 12 7a3 3 0 0 1 3 3 3.006 3.006 0 0 1-3.8 2.897 2.967 2.967 0 0 1-2.097-2.098zM12 19.75c2.488 0 4.7-1.183 6.119-3.012A2.492 2.492 0 0 0 15.75 15h-7.5a2.492 2.492 0 0 0-2.369 1.738A7.732 7.732 0 0 0 12 19.75z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_info_circle_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_info_circle_fill_24.xml
new file mode 100644
index 0000000000..c2bc674121
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_info_circle_fill_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M1 11C1 5.486 5.486 1 11 1s10 4.486 10 10c0 0.631-0.066 1.246-0.178 1.845A6.464 6.464 0 0 0 18.75 12.5a6.47 6.47 0 0 0-4.149 1.502v-0.001L14.603 14H7.25a2.492 2.492 0 0 0-2.369 1.738A7.732 7.732 0 0 0 11 18.75c0.434 0 0.855-0.048 1.269-0.117a9.483 9.483 0 0 1-0.007 0.107A3.72 3.72 0 0 0 12.25 19a6.5 6.5 0 0 0 0.277 1.871A9.976 9.976 0 0 1 11 21C5.486 21 1 16.514 1 11zm7.103-1.201a2.967 2.967 0 0 0 2.098 2.098A3.006 3.006 0 0 0 14 9a3 3 0 0 0-3-3 3.006 3.006 0 0 0-2.897 3.799z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M18.75 24c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5zm0.75-8v1.5H18V16h1.5zm0 2.75V22H18v-3.25h1.5z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_warning_circle_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_warning_circle_fill_24.xml
new file mode 100644
index 0000000000..f0787da169
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_avatar_warning_circle_fill_24.xml
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="m11.021,20.209 l0.949,-1.523c-0.318,0.041 -0.641,0.064 -0.97,0.064a7.732,7.732 0,0 1,-6.119 -3.012A2.492,2.492 0,0 1,7.25 14h7.5c0.026,0 0.05,0.003 0.076,0.005l0.056,0.005 -2.237,3.592 3.483,-5.591c0.979,-1.571 3.266,-1.571 4.244,0l0.464,0.745A9.973,9.973 0,0 0,21 11c0,-5.514 -4.486,-10 -10,-10S1,5.486 1,11c0,5.418 4.334,9.833 9.715,9.986 0.06,-0.264 0.15,-0.526 0.306,-0.777zM8.103,9.799A3.006,3.006 0,0 1,11 6a3,3 0,0 1,3 3,3.006 3.006,0 0,1 -3.8,2.897 2.967,2.967 0,0 1,-2.097 -2.098z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M22.537,22.436h-8.574a1.25,1.25 0,0 1,-1.085 -1.87l4.285,-7.499a1.252,1.252 0,0 1,2.174 0l4.285,7.499a1.25,1.25 0,0 1,-1.085 1.87zM17.5,15L19,15v2.75h-1.5L17.5,15zM17.5,19L19,19v1.5h-1.5L17.5,19z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_back_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_back_24.xml
new file mode 100644
index 0000000000..e1e62043a9
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_back_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.387 5.375l-6.88 6.881a0.875 0.875 0 0 0 0 1.238l6.88 6.88 1.238-1.237-5.388-5.387H20.75V12H6.237l5.388-5.387-1.238-1.238z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_20.xml
new file mode 100644
index 0000000000..47030805a0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_20.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M10.208 4.085a0.252 0.252 0 0 0-0.42 0.07L9.787 4.157l-1.4 3.52a0.75 0.75 0 0 1-0.65 0.472L3.96 8.389a0.25 0.25 0 0 0-0.141 0.443v0.001l2.91 2.42a0.75 0.75 0 0 1 0.248 0.763l-0.942 3.676a0.217 0.217 0 0 0 0.004 0.14c0.015 0.039 0.045 0.08 0.09 0.112 0.089 0.064 0.188 0.07 0.28 0.012l3.2-2.02a0.75 0.75 0 0 1 0.801 0l3.206 2.024a0.246 0.246 0 0 0 0.367-0.277v-0.001l-0.94-3.666a0.75 0.75 0 0 1 0.247-0.763l2.912-2.42a0.25 0.25 0 0 0-0.141-0.445l-3.778-0.24a0.75 0.75 0 0 1-0.65-0.47l-1.4-3.52a0.77 0.77 0 0 1-0.025-0.073zm1.483-0.321a0.749 0.749 0 0 0-0.046-0.165c-0.589-1.455-2.669-1.477-3.252 0.005L8.392 3.606 7.169 6.682l-3.307 0.21H3.86c-1.574 0.105-2.222 2.081-1 3.095 0 0 0.001 0 0 0l2.546 2.116-0.822 3.209c-0.405 1.542 1.305 2.747 2.626 1.912l2.8-1.767 2.797 1.765c1.348 0.866 3.01-0.385 2.63-1.904l-0.823-3.215 2.544-2.115c1.223-1.014 0.576-2.991-0.998-3.096l-3.309-0.21-1.16-2.918z"
+ tools:ignore="VectorPath,VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_24.xml
new file mode 100644
index 0000000000..360c5809c2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_24.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7.23 21.5c-0.44 0-0.89-0.14-1.26-0.41a2.13 2.13 0 0 1-0.82-2.27l1.06-4.15-3.3-2.74c-0.68-0.57-0.95-1.48-0.67-2.32 0.27-0.84 1.02-1.43 1.91-1.48l4.28-0.28 1.58-3.98a2.15 2.15 0 0 1 2-1.36c0.89 0 1.67 0.53 2 1.36l1.58 3.98 4.28 0.28c0.88 0.06 1.63 0.64 1.91 1.48 0.27 0.84 0.01 1.76-0.67 2.32l-3.3 2.74 1.06 4.15c0.22 0.86-0.1 1.75-0.82 2.27s-1.67 0.55-2.42 0.08l-3.62-2.29-3.62 2.29c-0.35 0.22-0.75 0.34-1.15 0.34L7.23 21.5zM12 4.25c-0.1 0-0.29 0.03-0.37 0.25L9.84 9C9.72 9.31 9.42 9.53 9.08 9.55L4.25 9.86c-0.24 0.02-0.33 0.18-0.36 0.28-0.03 0.08-0.06 0.28 0.13 0.44l3.72 3.09c0.26 0.22 0.37 0.56 0.29 0.89l-1.19 4.69c-0.06 0.23 0.07 0.37 0.15 0.43 0.08 0.06 0.25 0.14 0.45 0.01l4.09-2.59c0.29-0.18 0.65-0.18 0.93 0l4.09 2.59c0.2 0.13 0.37 0.05 0.45-0.01 0.08-0.06 0.21-0.2 0.15-0.43l-1.2-4.69c-0.08-0.33 0.03-0.67 0.29-0.89l3.72-3.09c0.19-0.16 0.15-0.36 0.13-0.43a0.386 0.386 0 0 0-0.36-0.28L14.9 9.56a0.886 0.886 0 0 1-0.76-0.55l-1.79-4.5a0.378 0.378 0 0 0-0.37-0.25L12 4.25z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_badge_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_badge_fill_20.xml
new file mode 100644
index 0000000000..aa602c8e62
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_badge_fill_20.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="#fff"
+ android:pathData="M10 5.686c0.292 0 0.55 0.175 0.658 0.446l0.86 2.159 2.319 0.15a0.705 0.705 0 0 1 0.628 0.489 0.703 0.703 0 0 1-0.222 0.763l-1.788 1.484 0.574 2.253a0.708 0.708 0 0 1-1.065 0.774L10 12.962l-1.965 1.242a0.706 0.706 0 0 1-0.795-0.026 0.705 0.705 0 0 1-0.27-0.748l0.574-2.252-1.788-1.485A0.705 0.705 0 0 1 5.535 8.93a0.705 0.705 0 0 1 0.627-0.488l2.32-0.15 0.86-2.159A0.704 0.704 0 0 1 10 5.686zm0-1.25c-0.808 0-1.522 0.485-1.82 1.235L7.613 7.095 6.082 7.194a1.95 1.95 0 0 0-1.736 1.35 1.948 1.948 0 0 0 0.611 2.11l1.181 0.981-0.379 1.487a1.95 1.95 0 0 0 0.746 2.068 1.95 1.95 0 0 0 2.198 0.071L10 14.441l1.297 0.82a1.958 1.958 0 0 0 2.944-2.138l-0.379-1.487 1.18-0.98a1.946 1.946 0 0 0 0.612-2.111 1.948 1.948 0 0 0-1.735-1.35l-1.532-0.099-0.568-1.426A1.947 1.947 0 0 0 10 4.436z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7.656 14.314A0.708 0.708 0 0 1 6.97 13.43l0.574-2.252-1.788-1.485A0.705 0.705 0 0 1 5.535 8.93a0.705 0.705 0 0 1 0.627-0.488l2.32-0.15 0.86-2.159A0.704 0.704 0 0 1 10 5.686c0.292 0 0.55 0.175 0.658 0.446l0.86 2.159 2.319 0.15a0.705 0.705 0 0 1 0.628 0.489 0.703 0.703 0 0 1-0.222 0.763l-1.788 1.484 0.574 2.253a0.705 0.705 0 0 1-0.27 0.748 0.706 0.706 0 0 1-0.795 0.026L10 12.962l-1.965 1.242a0.706 0.706 0 0 1-0.379 0.11z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_fill_20.xml
new file mode 100644
index 0000000000..61a9bfe15b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_fill_20.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5.926 17.5c-0.254 0-0.507-0.079-0.723-0.237a1.225 1.225 0 0 1-0.469-1.3l0.997-3.915-3.108-2.581A1.224 1.224 0 0 1 2.239 8.14 1.225 1.225 0 0 1 3.33 7.292L7.362 7.03l1.495-3.754A1.22 1.22 0 0 1 10 2.5c0.508 0 0.956 0.304 1.144 0.776l1.495 3.754 4.032 0.261c0.507 0.033 0.936 0.366 1.092 0.849a1.223 1.223 0 0 1-0.386 1.327l-3.108 2.581 0.997 3.916a1.226 1.226 0 0 1-0.469 1.3c-0.41 0.298-0.951 0.316-1.381 0.045L10 15.149l-3.415 2.159A1.238 1.238 0 0 1 5.926 17.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_fill_24.xml
new file mode 100644
index 0000000000..e5a17775e3
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_fill_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6.84 21.5c-0.322 0-0.642-0.101-0.916-0.3a1.55 1.55 0 0 1-0.594-1.647l1.263-4.959-3.938-3.27a1.552 1.552 0 0 1-0.487-1.681A1.553 1.553 0 0 1 3.55 8.568l5.108-0.331 1.893-4.755A1.551 1.551 0 0 1 12 2.5c0.643 0 1.211 0.386 1.449 0.983l1.893 4.755 5.107 0.331a1.552 1.552 0 0 1 1.383 1.076 1.55 1.55 0 0 1-0.489 1.681l-3.936 3.269 1.263 4.96a1.551 1.551 0 0 1-0.595 1.647c-0.519 0.377-1.205 0.4-1.75 0.057L12 18.522l-4.326 2.735A1.565 1.565 0 0 1 6.84 21.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_slash_24.xml
new file mode 100644
index 0000000000..5527aceeef
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_slash_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M3.238 1.99L22 20.752l-1.238 1.238-1.95-1.95c-0.14 0.41-0.404 0.776-0.773 1.044a2.142 2.142 0 0 1-2.416 0.078L12 18.873l-3.624 2.288a2.144 2.144 0 0 1-2.416-0.078 2.142 2.142 0 0 1-0.82-2.272l1.059-4.153-3.297-2.738a2.139 2.139 0 0 1-0.673-2.321 2.143 2.143 0 0 1 1.909-1.483l2.583-0.167-4.72-4.721L3.237 1.99zm5.13 7.605L4.251 9.862a0.382 0.382 0 0 0-0.357 0.277 0.383 0.383 0 0 0 0.126 0.434l3.722 3.092a0.872 0.872 0 0 1 0.289 0.889l-1.195 4.689a0.383 0.383 0 0 0 0.154 0.426 0.387 0.387 0 0 0 0.451 0.015l4.091-2.585a0.87 0.87 0 0 1 0.936 0l4.091 2.585a0.385 0.385 0 0 0 0.451-0.015 0.384 0.384 0 0 0 0.154-0.426L16.873 18.1 8.368 9.595z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M21.771 9.599a2.138 2.138 0 0 0-1.908-1.482L15.586 7.84 14 3.857A2.14 2.14 0 0 0 12 2.5a2.14 2.14 0 0 0-2 1.357L9.208 5.848l1.347 1.347 1.071-2.691A0.382 0.382 0 0 1 12 4.25c0.103 0 0.286 0.033 0.375 0.254l1.789 4.495a0.874 0.874 0 0 0 0.756 0.55l4.829 0.313a0.384 0.384 0 0 1 0.357 0.277 0.385 0.385 0 0 1-0.126 0.435l-3.304 2.743 1.243 1.243 3.178-2.639a2.143 2.143 0 0 0 0.674-2.322z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_tray_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_tray_24.xml
new file mode 100644
index 0000000000..2b35f1e963
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_tray_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7.203 16.763a1.222 1.222 0 0 0 1.382 0.045L12 14.649l3.416 2.16c0.43 0.271 0.971 0.253 1.381-0.045 0.41-0.298 0.594-0.809 0.469-1.3l-0.997-3.916 3.108-2.581c0.391-0.324 0.543-0.845 0.386-1.327a1.225 1.225 0 0 0-1.092-0.849L14.639 6.53l-1.495-3.754A1.224 1.224 0 0 0 12 2a1.22 1.22 0 0 0-1.143 0.776L9.362 6.53 5.33 6.792A1.225 1.225 0 0 0 4.239 7.64a1.224 1.224 0 0 0 0.384 1.327l3.108 2.581-0.997 3.915a1.225 1.225 0 0 0 0.469 1.3z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5 19.5V17H3.25v2.5a2.5 2.5 0 0 0 2.5 2.5h12.5a2.5 2.5 0 0 0 2.5-2.5V17H19v2.5a0.75 0.75 0 0 1-0.75 0.75H5.75A0.75 0.75 0 0 1 5 19.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_tray_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_tray_fill_24.xml
new file mode 100644
index 0000000000..2b35f1e963
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_bookmark_tray_fill_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7.203 16.763a1.222 1.222 0 0 0 1.382 0.045L12 14.649l3.416 2.16c0.43 0.271 0.971 0.253 1.381-0.045 0.41-0.298 0.594-0.809 0.469-1.3l-0.997-3.916 3.108-2.581c0.391-0.324 0.543-0.845 0.386-1.327a1.225 1.225 0 0 0-1.092-0.849L14.639 6.53l-1.495-3.754A1.224 1.224 0 0 0 12 2a1.22 1.22 0 0 0-1.143 0.776L9.362 6.53 5.33 6.792A1.225 1.225 0 0 0 4.239 7.64a1.224 1.224 0 0 0 0.384 1.327l3.108 2.581-0.997 3.915a1.225 1.225 0 0 0 0.469 1.3z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5 19.5V17H3.25v2.5a2.5 2.5 0 0 0 2.5 2.5h12.5a2.5 2.5 0 0 0 2.5-2.5V17H19v2.5a0.75 0.75 0 0 1-0.75 0.75H5.75A0.75 0.75 0 0 1 5 19.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_briefcase.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_briefcase.xml
new file mode 100644
index 0000000000..748b164535
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_briefcase.xml
@@ -0,0 +1,16 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M23.1,5.3c0-1.4-1.2-2.7-2.8-2.7h-8.7c-1.4,0-2.7,1.2-2.7,2.7v4.4H7.1v19.6h17.8V9.8h-1.8V5.3z M20.8,9.8H11
+ V5.3c0-0.4,0.2-0.5,0.5-0.5h8.7c0.4,0,0.5,0.2,0.5,0.5V9.8z M1.8,9.8h2.7v19.6H1.8c-0.9,0-1.8-0.9-1.8-1.8v-16
+ C0,10.5,0.9,9.8,1.8,9.8z M32,11.6v16c0,0.9-0.7,1.8-1.8,1.8h-2.7V9.8h2.7C31.3,9.8,32,10.5,32,11.6z" />
+</vector>
+
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_broken_lock.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_broken_lock.xml
new file mode 100644
index 0000000000..1f670e0029
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_broken_lock.xml
@@ -0,0 +1,21 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7 7.562V11H6.5A2.5 2.5 0 0 0 4 13.5v6A2.5 2.5 0 0 0 6.5 22h11c1.066 0 1.97-0.67 2.329-1.61L18.5 19.061V19.7l-0.8 0.8H6.3l-0.8-0.8v-6.4l0.8-0.8h5.638l-1.5-1.5H8.5V9.062L7 7.562z" />
+ <path
+ android:name="line"
+ android:fillColor="#FF4F5E"
+ android:pathData="M4.78 3.22l17.5 17.5A0.748 0.748 0 0 1 21.749 22a0.748 0.748 0 0 1-0.53-0.22L3.719 4.281A0.75 0.75 0 1 1 4.78 3.22z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 3.5c1.93 0 3.5 1.57 3.5 3.5v4h-0.809l1.5 1.5H17.7l0.8 0.8v1.509l1.5 1.5V13.5a2.5 2.5 0 0 0-2.5-2.5H17V7a5 5 0 0 0-5-5 4.99 4.99 0 0 0-4.127 2.182L8.971 5.28C9.574 4.222 10.698 3.5 12 3.5z" />
+
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_camera_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_camera_24.xml
new file mode 100644
index 0000000000..e244e12aca
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_camera_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 7.017a2.5 2.5 0 0 1 2.5-2.5h9.75a2.5 2.5 0 0 1 2.5 2.5v0.255l0.982-0.983C19.308 4.714 22 5.83 22 8.057v7.887c0 2.228-2.692 3.343-4.268 1.768L16.75 16.73v0.253a2.5 2.5 0 0 1-2.5 2.5H4.5a2.5 2.5 0 0 1-2.5-2.5V7.017zm2.5-0.75a0.75 0.75 0 0 0-0.75 0.75v9.966c0 0.414 0.336 0.75 0.75 0.75h9.75a0.75 0.75 0 0 0 0.75-0.75v-2.366a0.875 0.875 0 0 1 1.494-0.619l2.476 2.476a0.75 0.75 0 0 0 1.28-0.53V8.057a0.75 0.75 0 0 0-1.28-0.53l-2.476 2.476A0.875 0.875 0 0 1 15 9.384V7.017a0.75 0.75 0 0 0-0.75-0.75H4.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_camera_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_camera_slash_24.xml
new file mode 100644
index 0000000000..fb59ea92ff
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_camera_slash_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M1.238 1.99L20 20.752l-1.238 1.238-2.66-2.66a2.443 2.443 0 0 1-0.852 0.153H5.5a2.502 2.502 0 0 1-2.5-2.5V7.018c0-0.24 0.035-0.472 0.1-0.69L0 3.228 1.238 1.99zM4.75 7.977v9.006c0 0.413 0.337 0.75 0.75 0.75h9.005L4.75 7.977z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M21.457 5.747a2.463 2.463 0 0 0-2.726 0.542L17.75 7.271V7.018c0-1.38-1.121-2.5-2.5-2.5H5.876l1.749 1.75h7.625c0.413 0 0.75 0.337 0.75 0.75v2.366a0.876 0.876 0 0 0 1.494 0.619l2.475-2.476a0.738 0.738 0 0 1 0.818-0.162 0.736 0.736 0 0 1 0.463 0.692v7.887a0.734 0.734 0 0 1-0.463 0.692 0.74 0.74 0 0 1-0.817-0.161l-2.476-2.477A0.875 0.875 0 0 0 16 14.616v0.362l2.737 2.737c0.729 0.726 1.772 0.93 2.72 0.538A2.461 2.461 0 0 0 23 15.943V8.057a2.461 2.461 0 0 0-1.543-2.31z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cart.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cart.xml
new file mode 100644
index 0000000000..c29759e94d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cart.xml
@@ -0,0 +1,17 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M26.9,21.4H9.4c-0.7,0-1.3,0.5-1.3,1.3s0.5,1.3,1.3,1.3h17.5c0.7,0,1.3-0.5,1.3-1.3
+ C28.5,21.9,27.8,21.4,26.9,21.4z M13.3,30.1c1.3,0,2.7-1.2,2.7-2.7c0-1.3-1.2-2.7-2.7-2.7s-2.7,1.2-2.7,2.7
+ C10.6,29,12,30.1,13.3,30.1z M23.9,30.1c1.3,0,2.7-1.2,2.7-2.7c0-1.3-1.2-2.7-2.7-2.7c-1.5,0-2.7,1.2-2.7,2.7
+ C21.4,29,22.6,30.1,23.9,30.1z M31.5,7.4L31.5,7.4H7.6V7.2L5.7,2.5C5.4,2.2,5.1,1.9,4.6,1.9H0.7C0,1.9,0,2.5,0,2.9
+ C0,3.5-0.2,4,0.7,4.2h2.7l0.7,1.5l4,13.3c0,0.2,0.2,0.5,0.8,0.5h18.5c0.3,0,0.7-0.2,0.7-0.5L32,8.3C32,8.1,31.8,7.4,31.5,7.4z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_checkmark_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_checkmark_24.xml
new file mode 100644
index 0000000000..e3e97451ef
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_checkmark_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M22 6.37L10.384 17.987a0.875 0.875 0 0 1-1.238 0L2.001 10.84l1.237-1.237 6.527 6.527L20.762 5.133 22 6.37z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_20.xml
new file mode 100644
index 0000000000..f362622f33
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_20.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10 12.19L5.06 7.25 4 8.31l5.47 5.47a0.75 0.75 0 0 0 1.06 0L16 8.31l-1.061-1.06L10 12.19z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_24.xml
new file mode 100644
index 0000000000..995c2045df
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 14.887l6.263-6.263 1.238 1.238-6.882 6.882a0.875 0.875 0 0 1-1.238 0l-6.88-6.882 1.237-1.238L12 14.887z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_8.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_8.xml
new file mode 100644
index 0000000000..9ce948fafe
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_down_8.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="8dp"
+ android:height="8dp"
+ android:viewportWidth="8"
+ android:viewportHeight="8">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4 4.293l2.647-2.647 0.707 0.708-3 3a0.5 0.5 0 0 1-0.708 0l-3-3 0.708-0.707L4 4.293z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_left_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_left_24.xml
new file mode 100644
index 0000000000..2bf58b14ee
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_left_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.256 11.987l6.882-6.88 1.238 1.237-6.264 6.262 6.264 6.263-1.238 1.238-6.882-6.882a0.875 0.875 0 0 1 0-1.238z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_right_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_right_24.xml
new file mode 100644
index 0000000000..038db30bf7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_right_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M14.887 11.606L8.624 5.344l1.238-1.238 6.882 6.881a0.875 0.875 0 0 1 0 1.238l-6.882 6.882-1.238-1.238 6.263-6.263z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_up_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_up_20.xml
new file mode 100644
index 0000000000..e313fba3c1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_up_20.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10 7a0.75 0.75 0 0 1 0.53 0.22L16 12.69l-1.061 1.06L10 8.81l-4.94 4.94L4 12.69l5.47-5.47A0.75 0.75 0 0 1 10 7z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_up_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_up_24.xml
new file mode 100644
index 0000000000..b50e8547aa
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chevron_up_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.381 7.247a0.875 0.875 0 0 1 1.238 0l6.882 6.881-1.238 1.238L12 9.103l-6.262 6.263L4.5 14.128l6.881-6.88z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chill.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chill.xml
new file mode 100644
index 0000000000..36017ece6b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_chill.xml
@@ -0,0 +1,19 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9.1,18.5l-5.7,5.9C3.2,23.8,3,23.3,3,22.6c0-2.5,2-4.4,4.4-4.4C7.8,18.1,8.5,18.3,9.1,18.5 M26.5,18.5l-5.7,5.9
+ c-0.2-0.5-0.4-1.1-0.4-1.8c0-2.5,2-4.4,4.4-4.4C25.4,18.1,26,18.3,26.5,18.5 M24.7,2L24.7,2c-0.7,0-1.4,0.7-1.4,1.4s0.7,1.4,1.4,1.4
+ c2.5,0,4.4,2,4.4,4.4v7.6c-1.6-1.2-3.6-1.8-5.5-1.4c-2.1,0.4-3.9,1.6-5,3.4c-1.6-1.2-3.9-1.2-5.5,0c-1.1-1.8-2.8-3-5-3.4
+ c-2-0.4-3.9,0.2-5.5,1.4V9.2c0-2.5,2-4.4,4.4-4.4c0.5,0,0.9-0.4,1.2-0.7c0.2-0.4,0.2-0.9,0-1.4C8.2,2.3,7.6,2,7.1,2
+ C3.2,2,0,5.2,0,9.2v13.5C0,26.7,3.2,30,7.1,30l0,0c3.9,0,7.1-3.2,7.1-7.3c0-0.2,0-0.4,0-0.5c0.2-0.9,0.9-1.4,1.8-1.4
+ s1.6,0.5,1.8,1.4v0.2c0,0.2,0,0.2,0,0.4c0,2,0.7,3.7,2.1,5c1.4,1.4,3,2.1,5,2.1l0,0c2,0,3.6-0.7,5-2.1c1.4-1.2,2.1-3.2,2.1-5V9.2
+ C32,5.2,28.8,2,24.7,2" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_circle.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_circle.xml
new file mode 100644
index 0000000000..e3f75f3aff
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_circle.xml
@@ -0,0 +1,9 @@
+<?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/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@color/mozac_ui_icons_fill" />
+ <size android:height="24dp" android:width="24dp" />
+</shape>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_clipboard_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_clipboard_24.xml
new file mode 100644
index 0000000000..f8207ab859
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_clipboard_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.75 10.25h6.5V12h-6.5v-1.75zm0 4h3.972V16H8.75v-1.75z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M17 3h-1.535A3.999 3.999 0 0 0 12 1c-1.48 0-2.773 0.804-3.465 2H7a2.5 2.5 0 0 0-2.5 2.5v14A2.5 2.5 0 0 0 7 22h10a2.5 2.5 0 0 0 2.5-2.5v-14A2.5 2.5 0 0 0 17 3zM9.5 8A1.5 1.5 0 0 1 8 6.5V5c0-0.084 0.003-0.167 0.008-0.25H7A0.75 0.75 0 0 0 6.25 5.5v14c0 0.414 0.336 0.75 0.75 0.75h10a0.75 0.75 0 0 0 0.75-0.75v-14A0.75 0.75 0 0 0 17 4.75h-1.008C15.997 4.833 16 4.916 16 5v1.5A1.5 1.5 0 0 1 14.5 8h-5zM11 5.5v-2h2v2h-2z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_collection_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_collection_24.xml
new file mode 100644
index 0000000000..c71981eae3
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_collection_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M16.75 13.5h-9.5v-1.75h9.5v1.75zm-9.5 4h9.5v-1.75h-9.5v1.75z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M8.057 2C7.17 2 6.347 2.47 5.9 3.237l-2.53 4.32A0.875 0.875 0 0 0 3.25 8v11.5a2.5 2.5 0 0 0 2.5 2.5h12.5a2.5 2.5 0 0 0 2.5-2.5V8a0.876 0.876 0 0 0-0.12-0.442L18.1 3.236A2.502 2.502 0 0 0 15.944 2H8.057zM7.41 4.12a0.749 0.749 0 0 1 0.647-0.37h7.886c0.266 0 0.513 0.142 0.648 0.372l1.831 3.128H5.578L7.41 4.12zM5 9v10.5c0 0.414 0.336 0.75 0.75 0.75h12.5A0.75 0.75 0 0 0 19 19.5V9H5z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_competitiveness_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_competitiveness_24.xml
new file mode 100644
index 0000000000..8de4dda69f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_competitiveness_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M5.793 3.875C5.793 3.392 6.185 3 6.668 3h10.664c0.483 0 0.875 0.392 0.875 0.875v2.123h2.918C21.608 5.998 22 6.39 22 6.873v3.146c0 2.53-2.022 4.59-4.539 4.65a6.211 6.211 0 0 1-4.586 3.193v2.388h4.143V22H6.982v-1.75h4.143v-2.388a6.211 6.211 0 0 1-4.586-3.193A4.652 4.652 0 0 1 2 10.019V6.873C2 6.39 2.392 5.998 2.875 5.998h2.918V3.875zm12.316 8.944a2.903 2.903 0 0 0 2.141-2.8V7.748h-2.043v3.968c0 0.377-0.034 0.745-0.098 1.103zM5.793 11.716c0 0.377 0.034 0.745 0.098 1.103a2.903 2.903 0 0 1-2.141-2.8V7.748h2.043v3.968zm1.75-6.966v6.966a4.457 4.457 0 1 0 8.914 0V4.75H7.543z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies_24.xml
new file mode 100644
index 0000000000..3955146580
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M21.976 11.488A8.767 8.767 0 0 1 20 11.828V13h-2.5v-1.347c-3.905-0.877-6.833-4.366-6.833-8.532 0-0.365 0.03-0.724 0.073-1.077C5.82 2.667 2 6.87 2 11.956c0 5.514 4.486 10 10 10s10-4.486 10-10c0-0.158-0.016-0.312-0.024-0.468zM6.5 13H4v-2.5h2.5V13zm3.5 5.5H7.5V16H10v2.5zM10 8H7.5V5.5H10V8zm3.5 5H11v-2.5h2.5V13zm3.5 5.5h-2.5V16H17v2.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies_slash_24.xml
new file mode 100644
index 0000000000..9b9adcdcf5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies_slash_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5.379 4.487L7.5 6.609V5.5H10V8H8.891l10.58 10.58A9.95 9.95 0 0 0 22 11.956c0-0.105-0.007-0.209-0.014-0.313l-0.01-0.155A8.767 8.767 0 0 1 20 11.828V13h-2.5v-1.347c-3.905-0.877-6.833-4.366-6.833-8.532 0-0.365 0.03-0.724 0.073-1.077a9.971 9.971 0 0 0-5.361 2.443z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M3.304 7.041A9.883 9.883 0 0 0 2 11.956c0 5.514 4.486 10 10 10a9.929 9.929 0 0 0 4.92-1.299L19.262 23l1.238-1.238-2.117-2.117 0.018-0.015L4.333 5.562A8.69 8.69 0 0 0 4.318 5.58L1.738 3 0.5 4.238 3.304 7.04zM6.5 13H4v-2.5h2.5V13zm3.5 5.5H7.5V16H10v2.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_copy_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_copy_24.xml
new file mode 100644
index 0000000000..7fb92271da
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_copy_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9.25 3a2.5 2.5 0 0 0-2.5 2.5H8.5a0.75 0.75 0 0 1 0.75-0.75h8A0.75 0.75 0 0 1 18 5.5v10a0.75 0.75 0 0 1-0.75 0.75V18a2.5 2.5 0 0 0 2.5-2.5v-10a2.5 2.5 0 0 0-2.5-2.5h-8z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5.25 7a2.5 2.5 0 0 0-2.5 2.5v10a2.5 2.5 0 0 0 2.5 2.5h8a2.5 2.5 0 0 0 2.5-2.5v-10a2.5 2.5 0 0 0-2.5-2.5h-8zM4.5 9.5a0.75 0.75 0 0 1 0.75-0.75h8A0.75 0.75 0 0 1 14 9.5v10a0.75 0.75 0 0 1-0.75 0.75h-8A0.75 0.75 0 0 1 4.5 19.5v-10z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_credit_card_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_credit_card_24.xml
new file mode 100644
index 0000000000..69721f0c35
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_credit_card_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9 13.125H6v1.75h3v-1.75z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M2 6.5A2.5 2.5 0 0 1 4.5 4h15A2.5 2.5 0 0 1 22 6.5v10a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 16.5v-10zm2.5-0.75A0.75 0.75 0 0 0 3.75 6.5v1.75h16.5V6.5a0.75 0.75 0 0 0-0.75-0.75h-15zM3.75 16.5V10h16.5v6.5a0.75 0.75 0 0 1-0.75 0.75h-15a0.75 0.75 0 0 1-0.75-0.75z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_critical_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_critical_24.xml
new file mode 100644
index 0000000000..364f898f93
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_critical_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.125 17v-1.75h1.75V17h-1.75zm0-10v6.5h1.75V7h-1.75z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12zm10-8.25a8.25 8.25 0 1 0 0 16.5 8.25 8.25 0 0 0 0-16.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_critical_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_critical_fill_24.xml
new file mode 100644
index 0000000000..e81b1bd08a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_critical_fill_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M12 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10zm-0.875-5v-1.75h1.75V17h-1.75zm0-10v6.25h1.75V7h-1.75z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_20.xml
new file mode 100644
index 0000000000..07a4a4f9cb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_20.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.06 10L16 5.06 14.938 4l-4.94 4.94L5.06 4 4 5.062l4.939 4.94L4 14.938l1.06 1.06L10 11.061l4.939 4.938 1.06-1.06L11.06 10z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_24.xml
new file mode 100644
index 0000000000..6af42311d3
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 10.753L5.238 3.99 4 5.228l6.762 6.762L4 18.752l1.238 1.238L12 13.227l6.762 6.763L20 18.752l-6.763-6.762L20 5.228 18.762 3.99 12 10.753z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_24.xml
new file mode 100644
index 0000000000..98e4a06d50
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 10.763L9.238 8 8 9.238 10.762 12 8 14.762 9.238 16 12 13.237 14.762 16 16 14.762 13.237 12 16 9.238 14.762 8 12 10.763z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 3.75a8.25 8.25 0 1 0 0 16.5 8.25 8.25 0 0 0 0-16.5zM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_fill_20.xml
new file mode 100644
index 0000000000..937bd3d312
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_fill_20.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M18,10a8,8 0,1 1,-16 0,8 8,0 0,1 16,0zM6.5,12.439 L8.94,10l-2.44,-2.439 1.061,-1.06L10,8.939l2.439,-2.44 1.06,1.061L11.06,10l2.44,2.439 -1.061,1.06 -2.44,-2.438 -2.438,2.438 -1.06,-1.06z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_fill_24.xml
new file mode 100644
index 0000000000..b7255c0764
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cross_circle_fill_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 12c0 5.514 4.486 10 10 10s10-4.486 10-10S17.514 2 12 2 2 6.486 2 12zm7.238-4L12 10.763 14.762 8 16 9.238 13.237 12 16 14.762 14.762 16 12 13.237 9.238 16 8 14.762 10.762 12 8 9.238 9.238 8z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cryptominer_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cryptominer_24.xml
new file mode 100644
index 0000000000..aa6e3d7453
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cryptominer_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M13 4.5V2h-1.75v2.5H7.604c-0.401 0-0.784 0.16-1.065 0.444L4.935 6.562C4.656 6.843 4.5 7.222 4.5 7.618V9.25a1.5 1.5 0 0 0 1.5 1.5h5.25V22H13V10.75h5a1.5 1.5 0 0 0 1.5-1.5V6A1.5 1.5 0 0 0 18 4.5h-5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_data_clearance_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_data_clearance_24.xml
new file mode 100644
index 0000000000..a6c54e16b8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_data_clearance_24.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6.63 19.94c1.39 1.37 3.23 2.05 5.06 2.05l-0.01-0.01c1.84 0 3.68-0.68 5.08-2.05 1.75-1.76 2.71-4.12 2.71-6.67 0-1.73-0.48-3.42-1.39-4.9a0.873 0.873 0 0 0-0.74-0.42c-0.3 0-0.58 0.16-0.74 0.42a7.56 7.56 0 0 1-1.24 1.53c-0.43-3.58-2.92-6.65-6.46-7.85a0.864 0.864 0 0 0-0.89 0.2C7.78 2.47 7.69 2.82 7.79 3.13c0.78 2.5 0.2 5.24-1.5 7.23L6.2 10.45c-0.03 0.04-0.09 0.11-0.09 0.11l-0.05 0.06a0.265 0.265 0 0 0-0.025 0.03 0.22 0.22 0 0 1-0.025 0.03c-2.17 2.76-1.93 6.77 0.62 9.26zm1.22-1.25c-1.94-1.89-2.12-4.85-0.45-6.95v-0.02a9.416 9.416 0 0 0 2.42-7.36c2.36 1.38 3.86 3.89 3.86 6.64 0 0.19-0.01 0.38-0.02 0.57-0.02 0.33 0.14 0.64 0.42 0.81 0.28 0.18 0.63 0.17 0.91 0 0.83-0.52 1.58-1.16 2.21-1.9 0.35 0.88 0.53 1.82 0.53 2.78 0 2.08-0.78 4.01-2.19 5.43a5.413 5.413 0 0 1-1.298 0.937A4.92 4.92 0 0 0 14.5 18.07c0-1.78-0.87-3.37-2.24-4.01a0.593 0.593 0 0 0-0.52 0c-1.37 0.64-2.24 2.23-2.24 4.01 0 0.665 0.134 1.294 0.362 1.864A5.424 5.424 0 0 1 7.85 18.69z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_debug_drawer_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_debug_drawer_24.xml
new file mode 100644
index 0000000000..cf9df6537b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_debug_drawer_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M15.484,2 L14.11,4.382A5.973,5.973 0,0 0,12 4c-0.742,0 -1.453,0.135 -2.109,0.383L8.516,2 7,2.876l1.36,2.356a6.017,6.017 0,0 0,-2.144 3.169L4.75,7.508V4H3v4c0,0.306 0.16,0.589 0.42,0.748L6,10.318v5.364l-2.58,1.57A0.875,0.875 0,0 0,3 18v4h1.75v-3.508l1.466,-0.893A6.01,6.01 0,0 0,12 22a6.01,6.01 0,0 0,5.784 -4.4l1.466,0.892V22H21v-4a0.875,0.875 0,0 0,-0.42 -0.747L18,15.683v-5.365l2.58,-1.57A0.875,0.875 0,0 0,21 8V4h-1.75v3.508l-1.466,0.893a6.017,6.017 0,0 0,-2.145 -3.169l1.36,-2.357L15.485,2z" />
+ <path
+ android:fillColor="#fff"
+ android:pathData="M15.05,12.914c-0.16,-0.26 -0.587,-0.26 -0.748,0a3.96,3.96 0,0 1,-0.623 0.77c-0.218,-1.797 -1.467,-3.341 -3.244,-3.942a0.44,0.44 0,0 0,-0.56 0.546,3.831 3.831,0 0,1 -0.753,3.628c-0.015,0.014 -0.03,0.028 -0.044,0.044l-0.046,0.054 -0.027,0.032 -0.024,0.03a3.477,3.477 0,0 0,-0.732 1.926A3.75,3.75 0,0 0,15.75 16v-0.624c0,-0.87 -0.242,-1.722 -0.7,-2.462z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_delete_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_delete_24.xml
new file mode 100644
index 0000000000..295ed43ac7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_delete_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.75 10v8H9v-8h1.75zM15 10v8h-1.75v-8H15z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M10.5 2A2.5 2.5 0 0 0 8 4.5V6H3.25v1.75H4.5V19.5A2.5 2.5 0 0 0 7 22h10a2.5 2.5 0 0 0 2.5-2.5V7.75h1.25V6H16V4.5A2.5 2.5 0 0 0 13.5 2h-3zm3.75 4V4.5a0.75 0.75 0 0 0-0.75-0.75h-3A0.75 0.75 0 0 0 9.75 4.5V6h4.5zm3.5 1.75H6.25V19.5c0 0.414 0.336 0.75 0.75 0.75h10a0.75 0.75 0 0 0 0.75-0.75V7.75z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_desktop_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_desktop_24.xml
new file mode 100644
index 0000000000..a0d1de4e7c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_desktop_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.5 4A2.5 2.5 0 0 0 2 6.5v10A2.5 2.5 0 0 0 4.5 19H1v1.75h22V19h-3.5a2.5 2.5 0 0 0 2.5-2.5v-10A2.5 2.5 0 0 0 19.5 4h-15zM3.75 6.5A0.75 0.75 0 0 1 4.5 5.75h15a0.75 0.75 0 0 1 0.75 0.75v10a0.75 0.75 0 0 1-0.75 0.75h-15a0.75 0.75 0 0 1-0.75-0.75v-10z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_desktop_send_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_desktop_send_24.xml
new file mode 100644
index 0000000000..3668554e2a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_desktop_send_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.5 4A2.5 2.5 0 0 0 2 6.5v3h1.75v-3A0.75 0.75 0 0 1 4.5 5.75h15a0.75 0.75 0 0 1 0.75 0.75v10a0.75 0.75 0 0 1-0.75 0.75h-15a0.75 0.75 0 0 1-0.75-0.75v-2H2v2A2.5 2.5 0 0 0 4.5 19H1v1.75h22V19h-3.5a2.5 2.5 0 0 0 2.5-2.5v-10A2.5 2.5 0 0 0 19.5 4h-15z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9.28 14.996A0.75 0.75 0 0 1 8 14.466V12.75H1v-1.5h7V9.534a0.75 0.75 0 0 1 1.28-0.53l2.466 2.465a0.75 0.75 0 0 1 0 1.061L9.28 14.996z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_mobile_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_mobile_24.xml
new file mode 100644
index 0000000000..30b9f1eac2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_device_mobile_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M7.5,2A2.5,2.5 0,0 0,5 4.5v15A2.5,2.5 0,0 0,7.5 22h9a2.5,2.5 0,0 0,2.5 -2.5v-15A2.5,2.5 0,0 0,16.5 2h-9zM6.75,4.5a0.75,0.75 0,0 1,0.75 -0.75h9a0.75,0.75 0,0 1,0.75 0.75L17.25,17L6.75,17L6.75,4.5zM10,20.25h4v-1.5h-4v1.5z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_dollar.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_dollar.xml
new file mode 100644
index 0000000000..b0ddfbf563
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_dollar.xml
@@ -0,0 +1,19 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M16.2,0c-8.9,0-16,7.3-16,16c0,8.9,7.1,16,15.8,16s15.8-7.1,15.8-16C32,7.3,24.9,0,16.2,0z M17.1,25.1v1.6
+ c0,0.4-0.4,0.5-0.7,0.5c-0.4,0-0.7-0.2-0.7-0.5v-1.6c-3.2-0.2-5-1.8-5.5-4.3c0-0.2,0-0.2,0-0.4c0-0.5,0.4-0.9,0.9-0.9
+ c0.2,0,0.2,0,0.4,0c0.5,0,0.9,0.2,1.1,0.7c0.4,1.8,1.2,2.7,3.4,2.8v-6.8c-3.6-0.4-5.3-1.8-5.3-4.6c0-3,2.5-4.6,5.2-4.8V5.7
+ c0-0.4,0.4-0.5,0.7-0.5c0.4,0,0.7,0.2,0.7,0.5v1.1c2.7,0.4,4.4,1.8,5,3.9c0,0.2,0,0.2,0,0.4c0,0.5-0.4,0.7-0.7,0.9
+ c-0.2,0-0.2,0-0.4,0c-0.4,0-0.7-0.2-0.9-0.7c-0.4-1.4-1.2-2.3-3-2.5v6c3.2,0.7,5.5,1.8,5.5,5.2C22.8,23.5,20.1,25.1,17.1,25.1z
+ M12.4,11.6c0,1.6,0.7,2.5,3.2,3V8.7C13.7,8.9,12.4,10,12.4,11.6z M17.1,16.9v6.4c2.3-0.2,3.6-1.2,3.6-3.2
+ C20.6,17.8,19.2,17.2,17.1,16.9z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_download_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_download_24.xml
new file mode 100644
index 0000000000..c79a08a8ba
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_download_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.875 15.012V3h-1.75v12.013l-3.888-3.888L6 12.362l5.381 5.382a0.875 0.875 0 0 0 1.238 0l5.38-5.382-1.237-1.237-3.887 3.887z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5 17v2.5c0 0.414 0.336 0.75 0.75 0.75h12.5A0.75 0.75 0 0 0 19 19.5V17h1.75v2.5a2.5 2.5 0 0 1-2.5 2.5H5.75a2.5 2.5 0 0 1-2.5-2.5V17H5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_dropdown_arrow.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_dropdown_arrow.xml
new file mode 100644
index 0000000000..11e35191b8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_dropdown_arrow.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7 10L12 15L17 10H7Z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_edit_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_edit_24.xml
new file mode 100644
index 0000000000..4cc97d5cf7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_edit_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M5.277 2.732a2.5 2.5 0 0 1 3.536 0l12.93 12.931A0.875 0.875 0 0 1 22 16.282v4.843A0.875 0.875 0 0 1 21.125 22h-4.843a0.875 0.875 0 0 1-0.619-0.256L2.733 8.813a2.5 2.5 0 0 1 0-3.536l2.544-2.545zM6.515 3.97a0.75 0.75 0 0 1 1.06 0l1.768 1.767-3.606 3.606L3.97 7.575a0.75 0.75 0 0 1 0-1.06L6.515 3.97zm0.46 6.61l9.67 9.67h3.605v-3.606l-9.67-9.67-3.605 3.607z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_ellipsis_horizontal_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_ellipsis_horizontal_24.xml
new file mode 100644
index 0000000000..a75ad8f3e2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_ellipsis_horizontal_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6.5 11.75H4v2.5h2.5v-2.5zm6.75 0h-2.5v2.5h2.5v-2.5zm6.75 0h-2.5v2.5H20v-2.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_ellipsis_vertical_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_ellipsis_vertical_24.xml
new file mode 100644
index 0000000000..e5d6a89b19
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_ellipsis_vertical_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11 17.5V20h2.5v-2.5H11zm0-6.75v2.5h2.5v-2.5H11zM11 4v2.5h2.5V4H11z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_experiments_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_experiments_24.xml
new file mode 100644
index 0000000000..949626f212
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_experiments_24.xml
@@ -0,0 +1,28 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M16.75 6a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 9.125a2.875 2.875 0 1 0 0 5.75 2.875 2.875 0 0 0 0-5.75zM10.875 12a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6 16.75a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M21.035 9.616A14.057 14.057 0 0 1 19.988 12c0.436 0.799 0.793 1.599 1.049 2.384 0.815 2.52 0.55 4.62-0.74 5.912-0.8 0.8-1.908 1.205-3.234 1.205-0.817 0-1.717-0.155-2.678-0.465A14.057 14.057 0 0 1 12 19.989c-0.8 0.436-1.6 0.793-2.384 1.047-0.961 0.311-1.861 0.465-2.678 0.465-1.326 0-2.434-0.406-3.234-1.205-1.292-1.293-1.555-3.393-0.74-5.912 0.254-0.785 0.61-1.585 1.047-2.384a14.057 14.057 0 0 1-1.047-2.384c-0.815-2.519-0.552-4.619 0.74-5.911 1.292-1.293 3.39-1.555 5.91-0.74 0.786 0.254 1.586 0.611 2.385 1.047a14.09 14.09 0 0 1 2.383-1.048c2.52-0.814 4.618-0.552 5.912 0.74 1.292 1.293 1.555 3.392 0.74 5.912zM6.953 4.253c-0.86 0-1.554 0.231-2.011 0.689-0.792 0.792-0.906 2.3-0.314 4.136 0.126 0.389 0.288 0.785 0.468 1.183a21.376 21.376 0 0 1 5.164-5.164c-0.397-0.18-0.793-0.342-1.183-0.468-0.776-0.251-1.494-0.376-2.124-0.376zm7.97 15.118c1.834 0.594 3.342 0.481 4.135-0.313 0.792-0.792 0.906-2.3 0.314-4.136a11.508 11.508 0 0 0-0.468-1.183 21.31 21.31 0 0 1-2.36 2.803l-0.001 0.001a21.375 21.375 0 0 1-2.803 2.36c0.397 0.18 0.793 0.342 1.183 0.468zm3.98-9.111c0.18-0.397 0.342-0.793 0.468-1.183 0.593-1.835 0.479-3.343-0.314-4.136-0.457-0.458-1.152-0.689-2.011-0.689-0.63 0-1.347 0.125-2.124 0.376-0.39 0.126-0.785 0.288-1.183 0.468 0.264 0.186 0.524 0.393 0.784 0.6l0.167 0.132a2.487 2.487 0 0 0-0.395 1.929A18.529 18.529 0 0 0 12 6.036a19.128 19.128 0 0 0-3.306 2.658A19.128 19.128 0 0 0 6.036 12a18.606 18.606 0 0 0 1.73 2.305 2.483 2.483 0 0 0-1.933 0.39 78.202 78.202 0 0 0-0.116-0.146 16.845 16.845 0 0 1-0.62-0.809c-0.18 0.397-0.342 0.793-0.468 1.183-0.594 1.835-0.48 3.343 0.313 4.135 0.792 0.793 2.298 0.907 4.135 0.313 0.389-0.126 0.785-0.288 1.183-0.468-0.268-0.19-0.532-0.4-0.796-0.61l-0.16-0.126a2.48 2.48 0 0 0 0.392-1.933c0.75 0.654 1.524 1.238 2.305 1.729a19.068 19.068 0 0 0 3.305-2.657v-0.001A19.125 19.125 0 0 0 17.965 12a18.55 18.55 0 0 0-1.727-2.302 2.482 2.482 0 0 0 1.928-0.397c0.04 0.051 0.08 0.102 0.122 0.153 0.212 0.267 0.425 0.534 0.616 0.806z"
+ tools:ignore="VectorPath,VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_24.xml
new file mode 100644
index 0000000000..67a37d7624
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.5 3.75a1.75 1.75 0 0 0-1.75 1.75v2.375A0.875 0.875 0 0 1 9.875 8.75H6.5A0.75 0.75 0 0 0 5.75 9.5V11h1.5a3.5 3.5 0 1 1 0 7h-1.5v1.5c0 0.414 0.336 0.75 0.75 0.75h12a0.75 0.75 0 0 0 0.75-0.75v-10a0.75 0.75 0 0 0-0.75-0.75h-3.375a0.875 0.875 0 0 1-0.875-0.875V5.5a1.75 1.75 0 0 0-1.75-1.75zM9 5.5a3.5 3.5 0 1 1 7 0V7h2.5A2.5 2.5 0 0 1 21 9.5v10a2.5 2.5 0 0 1-2.5 2.5h-12A2.5 2.5 0 0 1 4 19.5v-2.375c0-0.483 0.392-0.875 0.875-0.875H7.25a1.75 1.75 0 1 0 0-3.5H4.875A0.875 0.875 0 0 1 4 11.875V9.5A2.5 2.5 0 0 1 6.5 7H9V5.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_cog_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_cog_24.xml
new file mode 100644
index 0000000000..596a85a3e4
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_cog_24.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M3.5,19.25h7.986a2.424,2.424 0,0 0,0.176 1.75H3.5A2.503,2.503 0,0 1,1 18.5v-2.375c0,-0.483 0.392,-0.875 0.875,-0.875H4.25c0.965,0 1.75,-0.785 1.75,-1.75s-0.785,-1.75 -1.75,-1.75H1.875A0.875,0.875 0,0 1,1 10.875V8.5C1,7.122 2.121,6 3.5,6H6V4.5C6,2.57 7.57,1 9.5,1S13,2.57 13,4.5V6h2.5C16.879,6 18,7.122 18,8.5V11h-0.439c-0.478,0 -0.928,0.149 -1.311,0.398V8.5a0.75,0.75 0,0 0,-0.75 -0.75h-3.375a0.875,0.875 0,0 1,-0.875 -0.875V4.5c0,-0.965 -0.785,-1.75 -1.75,-1.75s-1.75,0.785 -1.75,1.75v2.375a0.875,0.875 0,0 1,-0.875 0.875H3.5a0.75,0.75 0,0 0,-0.75 0.75V10h1.5c1.93,0 3.5,1.57 3.5,3.5S6.18,17 4.25,17h-1.5v1.5c0,0.414 0.337,0.75 0.75,0.75z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="m22.022,18.397 l0.835,0.881a0.92,0.92 0,0 1,0.129 1.089l-0.441,0.764a0.924,0.924 0,0 1,-1.015 0.431l-1.155,-0.288a4.037,4.037 0,0 1,-0.714 0.43l-0.338,1.14a0.923,0.923 0,0 1,-0.879 0.656h-0.883a0.92,0.92 0,0 1,-0.881 -0.664l-0.324,-1.127a4.005,4.005 0,0 1,-0.73 -0.435l-1.154,0.288a0.92,0.92 0,0 1,-1.015 -0.431l-0.441,-0.764a0.921,0.921 0,0 1,0.129 -1.09l0.834,-0.879 -0.004,-0.03a2.62,2.62 0,0 1,-0.033 -0.367c0,-0.136 0.018,-0.267 0.036,-0.398l0.003,-0.019 -0.829,-0.858a0.92,0.92 0,0 1,-0.135 -1.095l0.442,-0.764a0.923,0.923 0,0 1,1.007 -0.433l1.174,0.282c0.244,-0.178 0.48,-0.318 0.716,-0.424l0.324,-1.127a0.921,0.921 0,0 1,0.881 -0.664h0.883c0.403,0 0.764,0.27 0.879,0.656l0.338,1.14c0.227,0.104 0.453,0.238 0.699,0.418l1.174,-0.282a0.923,0.923 0,0 1,1.008 0.433l0.441,0.764a0.922,0.922 0,0 1,-0.134 1.095l-0.829,0.858 0.003,0.019a2.9,2.9 0,0 1,0.036 0.398c0,0.118 -0.015,0.233 -0.03,0.347l-0.007,0.05zM16.5,18a1.5,1.5 0,1 0,3.001 -0.001A1.5,1.5 0,0 0,16.5 18z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_fill_24.xml
new file mode 100644
index 0000000000..52b50249fa
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_extension_fill_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M18.5 22h-12A2.5 2.5 0 0 1 4 19.5v-2.125C4 16.892 4.392 16.5 4.875 16.5H7A2.25 2.25 0 1 0 7 12H4.875A0.875 0.875 0 0 1 4 11.125V9.5A2.5 2.5 0 0 1 6.5 7H10V4.875a2.875 2.875 0 1 1 5.75 0V7h2.75A2.5 2.5 0 0 1 21 9.5v10a2.5 2.5 0 0 1-2.5 2.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_external_link_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_external_link_24.xml
new file mode 100644
index 0000000000..79e00bceb8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_external_link_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5.5,4A2.5,2.5 0,0 0,3 6.5v12A2.5,2.5 0,0 0,5.5 21h12a2.5,2.5 0,0 0,2.5 -2.5V12h-1.75v6.5a0.75,0.75 0,0 1,-0.75 0.75h-12a0.75,0.75 0,0 1,-0.75 -0.75v-12a0.75,0.75 0,0 1,0.75 -0.75H12V4H5.5z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M14.788,4h4.462a0.75,0.75 0,0 1,0.75 0.75v4.541a0.75,0.75 0,0 1,-1.285 0.526l-1.633,-1.662L12.238,13 11,11.762l4.856,-4.855 -1.603,-1.631A0.75,0.75 0,0 1,14.788 4z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_eye_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_eye_24.xml
new file mode 100644
index 0000000000..9cba339370
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_eye_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 9a3.375 3.375 0 1 0 0 6.75A3.375 3.375 0 0 0 12 9zm-1.625 3.375a1.625 1.625 0 1 1 3.25 0 1.625 1.625 0 0 1-3.25 0z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 5.269c-4.497 0-8.3 2.837-9.8 6.796a0.875 0.875 0 0 0 0 0.62c1.5 3.96 5.303 6.796 9.8 6.796 4.497 0 8.3-2.837 9.8-6.796a0.874 0.874 0 0 0 0-0.62C20.3 8.105 16.497 5.27 12 5.27zm0 12.462c-3.62 0-6.71-2.21-8.04-5.356C5.29 9.228 8.38 7.019 12 7.019c3.62 0 6.71 2.21 8.04 5.356-1.33 3.147-4.42 5.356-8.04 5.356z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_eye_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_eye_slash_24.xml
new file mode 100644
index 0000000000..2d0c82ea6c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_eye_slash_24.xml
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M8.227 5.99l-3.74-3.74L3.25 3.488 6.573 6.81A10.552 10.552 0 0 0 2.2 12.065a0.885 0.885 0 0 0 0 0.621c1.54 4.064 5.479 6.795 9.8 6.795a10.4 10.4 0 0 0 5.595-1.648l2.917 2.917 1.238-1.238-2.75-2.75 0.004-0.003-1.221-1.22-0.004 0.003-2.475-2.476 0.001-0.006-3.99-3.99-0.006 0.001-1.71-1.71h0.006L8.232 5.986 8.227 5.988zm4.988 7.463l-2.293-2.293a1.622 1.622 0 0 0-0.547 1.215 1.623 1.623 0 0 0 2.84 1.078zm-4.59-1.078a3.36 3.36 0 0 1 1.06-2.453L7.838 8.077a8.7 8.7 0 0 0-3.88 4.298C5.337 15.64 8.457 17.731 12 17.731c1.57 0 3.054-0.411 4.342-1.152l-1.889-1.888A3.365 3.365 0 0 1 12 15.75a3.379 3.379 0 0 1-3.375-3.375z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.427 7.061a6.95 6.95 0 0 0 0.203-0.018c0.122-0.012 0.245-0.024 0.37-0.024 3.544 0 6.664 2.091 8.04 5.356a8.909 8.909 0 0 1-1.233 2.066l1.236 1.236a10.466 10.466 0 0 0 1.757-2.992 0.885 0.885 0 0 0 0-0.621C20.26 8 16.321 5.269 12 5.269c-0.726 0-1.439 0.083-2.134 0.231l1.561 1.561z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M14.526 10.16a3.381 3.381 0 0 0-0.311-0.311l0.311 0.311z"
+ tools:ignore="VectorRaster" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fence.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fence.xml
new file mode 100644
index 0000000000..ccd01bd3c7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fence.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M28,4l-2,2v4h-4V6l-2-2l-2,2v4h-4V6l-2-2l-2,2v4H6V6L4,4L2,6v22h4v-4h4v4h4v-4h4v4h4v-4h4v4h4V6L28,4z M6,22V12 h4v10H6z M14,22V12h4v10H14z M22,22V12h4v10H22z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fingerprinter_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fingerprinter_24.xml
new file mode 100644
index 0000000000..a1eccfd3a8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fingerprinter_24.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.439 2.284c3.823-0.958 8.078 0.618 10.116 3.76 2.536 3.902 1.498 9.15-2.925 14.773l-1.375-1.082c3.923-4.988 4.929-9.51 2.833-12.738-1.645-2.533-5.102-3.802-8.221-3.015-2.965 0.751-4.826 3.16-5.103 6.607L5.73 11H3.976l0.045-0.552c0.342-4.24 2.681-7.216 6.418-8.164z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.253 21.173l-0.891-1.506 0.753-0.445a9.99 9.99 0 0 0 0.69-0.448c4.12-2.935 4.307-8.107 4.308-8.159l0.011-0.115c0.232-1.445-0.94-2.839-2.614-3.108-0.97-0.158-1.996 0.103-2.694 0.67-0.327 0.267-0.724 0.72-0.754 1.363V11c0 2.206-1.794 4-4 4H2v-1.75h4.062c1.24 0 2.25-1.01 2.25-2.25V9.384C8.36 8.336 8.856 7.398 9.711 6.703c1.091-0.887 2.614-1.272 4.076-1.041 2.607 0.42 4.423 2.682 4.073 5.06-0.03 0.672-0.418 6.186-5.041 9.478-0.26 0.184-0.529 0.36-0.813 0.527l-0.753 0.446z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.062 10v1.625A5.38 5.38 0 0 1 6.687 17H2v1.75h4.688a7.133 7.133 0 0 0 7.125-7.125V10h-1.751z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_folder_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_folder_24.xml
new file mode 100644
index 0000000000..d63d873480
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_folder_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.5 5.25A0.75 0.75 0 0 0 3.75 6v12c0 0.414 0.336 0.75 0.75 0.75h15A0.75 0.75 0 0 0 20.25 18V9a0.75 0.75 0 0 0-0.75-0.75h-6.83a0.875 0.875 0 0 1-0.62-0.258L9.543 5.471A0.748 0.748 0 0 0 9.011 5.25H4.5zM2 6a2.5 2.5 0 0 1 2.5-2.5h4.511a2.5 2.5 0 0 1 1.773 0.737l2.25 2.263H19.5A2.5 2.5 0 0 1 22 9v9a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 18V6z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_folder_add_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_folder_add_24.xml
new file mode 100644
index 0000000000..c6938d1006
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_folder_add_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M3.5 4.75A0.75 0.75 0 0 0 2.75 5.5v12c0 0.414 0.336 0.75 0.75 0.75h11V20h-11A2.5 2.5 0 0 1 1 17.5v-12A2.5 2.5 0 0 1 3.5 3h4.511a2.5 2.5 0 0 1 1.773 0.737L12.034 6H18.5A2.5 2.5 0 0 1 21 8.5v5.125h-1.75V8.5a0.75 0.75 0 0 0-0.75-0.75h-6.83a0.875 0.875 0 0 1-0.62-0.258L8.543 4.971A0.748 0.748 0 0 0 8.011 4.75H3.5z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M19.125 20H16v-1.75h3.125v-3.125h1.75v3.125H24V20h-3.125v3.125h-1.75V20z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_font.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_font.xml
new file mode 100644
index 0000000000..9e5d320ba6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_font.xml
@@ -0,0 +1,14 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M18.9,16.1c-0.5,0.4 -1,0.6 -1.5,0.8 -0.4,0.2 -0.9,0.3 -1.3,0.3 -0.6,0 -1.1,-0.2 -1.5,-0.6 -0.4,-0.4 -0.6,-0.9 -0.6,-1.6 0,-0.8 0.3,-1.3 0.9,-1.8 0.6,-0.5 1.9,-0.9 4.1,-1.3v-0.4c0,-0.6 -0.1,-1 -0.4,-1.3 -0.2,-0.3 -0.6,-0.4 -1.2,-0.4L17,9.8c-0.1,0 -0.3,0 -0.4,0.1v1.5h-1.5c-0.3,0 -0.4,0 -0.6,-0.1 -0.1,-0.1 -0.2,-0.2 -0.2,-0.5 0,-0.5 0.3,-0.9 1,-1.3s1.5,-0.6 2.5,-0.6c1.1,0 1.9,0.2 2.4,0.6 0.5,0.4 0.7,1.2 0.7,2.2L20.9,16l0.2,0.2 0.9,0.1v0.7h-2.9l-0.2,-0.9zM18.9,15.4v-2.6c-1.1,0.2 -1.8,0.5 -2.2,0.8 -0.4,0.3 -0.6,0.7 -0.6,1.1 0,0.4 0.1,0.7 0.3,0.9 0.2,0.2 0.5,0.3 0.9,0.3 0.2,0 0.4,0 0.7,-0.1 0.3,-0.1 0.6,-0.2 0.9,-0.4zM6.6,6h1.7L12,16l1,0.2v0.8L8.7,17v-0.8l1,-0.1 0.1,-0.2 -1,-2.8L5.3,13.1l-1,2.7 0.2,0.2 1,0.1v0.9L2,17v-0.8l1,-0.2L6.6,6zM7,8.2l-1.4,4h2.9L7,8.2z"
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillAlpha=".8"/>
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_food.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_food.xml
new file mode 100644
index 0000000000..d82801ad65
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_food.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M14.1,0.9v5.3h-1.4V0.9c0-1.1-1.4-1.1-1.4,0v5.3h-1.2V0.9c0-1.1-1.4-1.1-1.4,0v5.3H7.2V0.9c0-1.2-1.6-1.1-1.6,0
+ v10.4c0,1.8,1.2,3,2.8,3v15.2c0,1.6,1.1,2.5,2.1,2.5s2.1-0.9,2.1-2.5V14.3c1.6,0,2.8-1.4,2.8-2.8V0.9C15.6-0.4,14.1-0.2,14.1,0.9z
+ M19.8,3.7v25.8c0,3.2,4.2,3.2,4.2,0V17.1h2.3V3.7C26.5-1.2,19.8-1.2,19.8,3.7z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_forward_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_forward_24.xml
new file mode 100644
index 0000000000..8b66c40787
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_forward_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M17.638 13.75H3.125V12h14.513L12.25 6.613l1.238-1.238 6.88 6.881a0.875 0.875 0 0 1 0 1.238l-6.88 6.88-1.238-1.237 5.387-5.387z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fruit.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fruit.xml
new file mode 100644
index 0000000000..250b848431
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_fruit.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M16.5,8c-2.1-0.9-3.9-1.2-6.6-0.9C4.6,8,1.8,12.6,1.8,18c0,5.9,4.8,14,9.8,14c1.6,0,3.9-1.2,4.4-1.2
+ c0.5,0,2.8,1.2,4.4,1.2c5,0,9.8-8.4,9.8-14c0-5.9-3.2-10.8-9.8-10.8C19,7.1,17.8,7.5,16.5,8z M11.7,0c1.1,0.2,3.2,0.9,4.1,2.3
+ c0.9,1.4,0.5,3.6,0.2,4.6c-1.2-0.2-3.2-0.7-4.1-2.3C11,3.2,11.4,1.1,11.7,0L11.7,0z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_gift.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_gift.xml
new file mode 100644
index 0000000000..8bb994b81f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_gift.xml
@@ -0,0 +1,17 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M30.3,8.1h-4.5V8c0.7-0.7,1.3-1.9,1.3-3.2c0-2.6-2.1-4.7-4.9-4.7c-1.5,0-4.5,1.5-6.4,3.4C14,1.6,11,0.1,9.5,0.1
+ c-2.6,0-4.9,2.1-4.9,4.7C4.7,6.1,5.2,7.2,6,8H1.7C0.6,8,0,8.7,0,9.6v4.5c0,0.2,0.2,0.4,0.4,0.4h13.8V9.6h3.2v4.9h14.2
+ c0.2,0,0.4-0.2,0.4-0.4V9.6C32,8.7,31.4,8.1,30.3,8.1z M9.5,6.5C8.6,6.5,8,5.9,8,4.8s0.6-1.5,1.5-1.5s3.7,1.9,4.7,2.8
+ C13.7,6.3,9.5,6.5,9.5,6.5z M22.3,6.5c0,0-4.1-0.2-4.7-0.4c0.9-1.1,3.7-2.8,4.7-2.8S24,3.8,24,4.8S23.2,6.5,22.3,6.5z M1.7,17.7
+ h12.7v14.2H1.7V17.7z M17.6,17.7h12.7v14.2H17.6V17.7z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_globe_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_globe_24.xml
new file mode 100644
index 0000000000..2bedff87fb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_globe_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zM9.731 4.066a8.257 8.257 0 0 0-5.938 7.09h3.266a13.027 13.027 0 0 1 2.672-7.09zm-2.672 8.84h-3.26a8.258 8.258 0 0 0 5.873 7.01 13.025 13.025 0 0 1-2.613-7.01zm4.911 7.01a11.295 11.295 0 0 1-3.157-7.01h6.369a11.296 11.296 0 0 1-3.212 7.01zm-3.157-8.76a11.298 11.298 0 0 1 3.217-7.072 11.299 11.299 0 0 1 3.161 7.072H8.813zm8.132 0a13.027 13.027 0 0 0-2.616-7.073 8.257 8.257 0 0 1 5.878 7.073h-3.262zm-0.008 1.75a13.026 13.026 0 0 1-2.669 7.028 8.257 8.257 0 0 0 5.933-7.028h-3.264z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_grid.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_grid.xml
new file mode 100644
index 0000000000..ddd9e4b881
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_grid.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:width="24dp"
+ android:height="24dp">
+ <path
+ android:pathData="M9.667 3L4.333 3C3.597 3 3 3.597 3 4.333L3 9.667C3 10.403 3.597 11 4.333 11l5.334 0C10.403 11 11 10.403 11 9.667L11 4.333C11 3.597 10.403 3 9.667 3m10 0l-5.334 0C13.597 3 13 3.597 13 4.333l0 5.334C13 10.403 13.597 11 14.333 11l5.334 0C20.403 11 21 10.403 21 9.667L21 4.333C21 3.597 20.403 3 19.667 3m-10 10l-5.334 0C3.597 13 3 13.597 3 14.333l0 5.334C3 20.403 3.597 21 4.333 21l5.334 0C10.403 21 11 20.403 11 19.667l0 -5.334C11 13.597 10.403 13 9.667 13m10 0l-5.334 0C13.597 13 13 13.597 13 14.333l0 5.334C13 20.403 13.597 21 14.333 21l5.334 0C20.403 21 21 20.403 21 19.667l0 -5.334C21 13.597 20.403 13 19.667 13"
+ android:fillColor="@color/mozac_ui_icons_fill"/>
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_grid_add_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_grid_add_24.xml
new file mode 100644
index 0000000000..ca83cc4f1d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_grid_add_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M3,5.5A2.5,2.5 0,0 1,5.5 3h3A2.5,2.5 0,0 1,11 5.5v3A2.5,2.5 0,0 1,8.5 11h-3A2.5,2.5 0,0 1,3 8.5v-3zM5.5,4.75a0.75,0.75 0,0 0,-0.75 0.75v3c0,0.414 0.336,0.75 0.75,0.75h3a0.75,0.75 0,0 0,0.75 -0.75v-3a0.75,0.75 0,0 0,-0.75 -0.75h-3zM12.75,5.5a2.5,2.5 0,0 1,2.5 -2.5h3a2.5,2.5 0,0 1,2.5 2.5v3a2.5,2.5 0,0 1,-2.5 2.5h-3a2.5,2.5 0,0 1,-2.5 -2.5v-3zM15.25,4.75a0.75,0.75 0,0 0,-0.75 0.75v3c0,0.414 0.336,0.75 0.75,0.75h3A0.75,0.75 0,0 0,19 8.5v-3a0.75,0.75 0,0 0,-0.75 -0.75h-3zM5.5,12.75a2.5,2.5 0,0 0,-2.5 2.5v3a2.5,2.5 0,0 0,2.5 2.5h3a2.5,2.5 0,0 0,2.5 -2.5v-3a2.5,2.5 0,0 0,-2.5 -2.5h-3zM4.75,15.25a0.75,0.75 0,0 1,0.75 -0.75h3a0.75,0.75 0,0 1,0.75 0.75v3a0.75,0.75 0,0 1,-0.75 0.75h-3a0.75,0.75 0,0 1,-0.75 -0.75v-3z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M15.875,15.875V13h1.75v2.875H20.5v1.75h-2.875V20.5h-1.75v-2.875H13v-1.75h2.875z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_help_circle_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_help_circle_24.xml
new file mode 100644
index 0000000000..352515dbe6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_help_circle_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M13 16h-1.75v1.75H13V16zm-1.75-2.73v0.98H13v-0.98c0-0.27 0.18-0.52 0.46-0.65a3.165 3.165 0 0 0 1.82-2.86 3.15 3.15 0 1 0-6.3 0h1.75c0-0.77 0.63-1.4 1.4-1.4 0.77 0 1.4 0.63 1.4 1.4 0 0.54-0.32 1.04-0.81 1.27-0.91 0.42-1.47 1.28-1.47 2.24z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 22C6.49 22 2 17.51 2 12S6.49 2 12 2s10 4.49 10 10-4.49 10-10 10zm0-18.25c-4.55 0-8.25 3.7-8.25 8.25s3.7 8.25 8.25 8.25 8.25-3.7 8.25-8.25-3.7-8.25-8.25-8.25z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_help_circle_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_help_circle_fill_24.xml
new file mode 100644
index 0000000000..a4f9cf8aec
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_help_circle_fill_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M2 12c0 5.51 4.49 10 10 10s10-4.49 10-10S17.51 2 12 2 2 6.49 2 12zm9.25 2.25v-0.98c0-0.96 0.56-1.82 1.47-2.24 0.49-0.23 0.81-0.73 0.81-1.27 0-0.77-0.63-1.4-1.4-1.4-0.77 0-1.4 0.63-1.4 1.4H8.98a3.15 3.15 0 1 1 6.3 0c0 1.22-0.71 2.34-1.82 2.86C13.18 12.75 13 13 13 13.27v0.98h-1.75zM13 16h-1.75v1.75H13V16z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_history_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_history_24.xml
new file mode 100644
index 0000000000..328727849e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_history_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 3.75a8.25 8.25 0 1 0 0 16.5 8.25 8.25 0 0 0 0-16.5zM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.125 12V7h1.75v4.495l3.74 2.16-0.874 1.515-4.178-2.412A0.875 0.875 0 0 1 11.125 12z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_home_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_home_24.xml
new file mode 100644
index 0000000000..43b4264c5f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_home_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M2.445 11.78l0.805-0.698V18.5a2.5 2.5 0 0 0 2.5 2.5h12.5a2.5 2.5 0 0 0 2.5-2.5v-7.419l0.806 0.7 1.146-1.323-9.042-7.842a2.533 2.533 0 0 0-3.32 0L1.3 10.458l1.146 1.322zm10.069-7.842a0.783 0.783 0 0 0-1.026 0L5 9.564V18.5c0 0.414 0.336 0.75 0.75 0.75h2.5v-4.5a2.5 2.5 0 0 1 2.5-2.5h2.5a2.5 2.5 0 0 1 2.5 2.5v4.5h2.5A0.75 0.75 0 0 0 19 18.5V9.564l-6.486-5.626zM14 19.25v-4.5A0.75 0.75 0 0 0 13.25 14h-2.5A0.75 0.75 0 0 0 10 14.75v4.5h4z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_image_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_image_24.xml
new file mode 100644
index 0000000000..1d12948178
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_image_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M18.25 20.75H5.75a2.503 2.503 0 0 1-2.5-2.5V5.75c0-1.379 1.122-2.5 2.5-2.5h12.5c1.379 0 2.5 1.121 2.5 2.5v12.5c0 1.379-1.121 2.5-2.5 2.5zM5.75 5A0.75 0.75 0 0 0 5 5.75v12.5C5 18.663 5.336 19 5.75 19h12.5c0.413 0 0.75-0.337 0.75-0.75V5.75A0.752 0.752 0 0 0 18.25 5H5.75z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M16.5 17.25h-9a0.75 0.75 0 0 1-0.75-0.75v-2.301c0-0.264 0.105-0.518 0.291-0.705l2.783-2.799a1.001 1.001 0 0 1 1.415-0.004L14 13.439l0.792-0.747a1 1 0 0 1 1.396 0.023l0.773 0.779c0.186 0.187 0.29 0.44 0.29 0.704V16.5a0.752 0.752 0 0 1-0.751 0.75zM17.25 8h-2.5v2.5h2.5V8z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_image_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_image_slash_24.xml
new file mode 100644
index 0000000000..49ed64618e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_image_slash_24.xml
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M3.238 2l1.48 1.48 0.001-0.001 1.521 1.52H6.237l8.11 8.111 0.002-0.001 2.9 2.9v0.003L19 17.762V17.76l1.521 1.52v0.003L22 20.763 20.762 22l-1.476-1.477a2.47 2.47 0 0 1-1.036 0.227H5.75a2.503 2.503 0 0 1-2.5-2.5V5.75c0-0.37 0.081-0.72 0.226-1.036L2 3.238 3.238 2zm6.403 8.878L7.04 13.494a1.001 1.001 0 0 0-0.291 0.705v2.3c0 0.415 0.336 0.75 0.75 0.75h8.514l1.75 1.75H5.75A0.75 0.75 0 0 1 5 18.249V6.238l4.64 4.641z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M17.25 8h-2.5v2.5h2.5V8z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M18.25 5H8.361l-1.75-1.75H18.25c1.379 0 2.5 1.12 2.5 2.5v11.639L19 15.639v-9.89a0.752 0.752 0 0 0-0.75-0.75z"
+ tools:ignore="VectorRaster" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_information_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_information_24.xml
new file mode 100644
index 0000000000..24be636c52
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_information_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 3.75a8.25 8.25 0 1 0 0 16.5 8.25 8.25 0 0 0 0-16.5zM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.875 7v1.75h-1.75V7h1.75zm0 3.5V17h-1.75v-6.5h1.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_information_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_information_fill_24.xml
new file mode 100644
index 0000000000..ec7aea93c8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_information_fill_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zm0.875-15v1.75h-1.75V7h1.75zm0 10v-6.25h-1.75V17h1.75z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lightbulb_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lightbulb_24.xml
new file mode 100644
index 0000000000..0b3ddafe53
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lightbulb_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.5 9.5a7.5 7.5 0 1 1 15 0c0 2.568-1.3 4.812-3.254 6.16l-0.559 2.17a0.875 0.875 0 0 1-0.847 0.657H9.161c-0.4 0-0.748-0.27-0.847-0.657l-0.559-2.17C5.8 14.312 4.5 12.068 4.5 9.5zM12 3.75A5.75 5.75 0 0 0 6.25 9.5c0 2.057 1.091 3.852 2.73 4.87 0.191 0.12 0.33 0.308 0.385 0.526l0.474 1.841h4.323l0.474-1.841a0.875 0.875 0 0 1 0.385-0.525c1.638-1.019 2.729-2.814 2.729-4.871A5.75 5.75 0 0 0 12 3.75zM15.5 22h-7v-1.75h7V22z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_link_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_link_24.xml
new file mode 100644
index 0000000000..d08122a767
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_link_24.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5.37 13.28C5.85 13.76 6.47 14 7.1 14l0.01 0.01c0.62 0 1.25-0.24 1.73-0.72l0.38-0.38-1.24-1.24-0.38 0.38c-0.26 0.27-0.72 0.27-0.99 0L3.97 9.41a0.754 0.754 0 0 1 0-1.06l4.38-4.38c0.29-0.29 0.77-0.29 1.06 0l2.64 2.64c0.14 0.13 0.21 0.3 0.21 0.49s-0.08 0.37-0.21 0.5l-0.38 0.38 1.24 1.24 0.38-0.38c0.96-0.96 0.96-2.51 0-3.47l-2.64-2.64c-0.98-0.97-2.56-0.97-3.54 0L2.73 7.1c-0.97 0.98-0.97 2.56 0 3.54l2.64 2.64z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9.066 7.821L7.829 9.06l7.106 7.106 1.237-1.237L9.066 7.82z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M15.12 22c-0.64 0-1.28-0.24-1.77-0.73l-2.64-2.64a2.45 2.45 0 0 1 0-3.47l0.38-0.38 1.24 1.24-0.38 0.38c-0.13 0.13-0.21 0.31-0.21 0.5s0.07 0.36 0.21 0.49l2.64 2.64c0.29 0.29 0.77 0.29 1.06 0l4.38-4.38c0.29-0.29 0.29-0.77 0-1.06l-2.64-2.64c-0.26-0.27-0.73-0.26-0.99 0l-0.38 0.38-1.24-1.24 0.38-0.38a2.45 2.45 0 0 1 3.47 0l2.64 2.64c0.97 0.98 0.97 2.56 0 3.54l-4.38 4.38C16.4 21.76 15.76 22 15.12 22z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_location_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_location_24.xml
new file mode 100644
index 0000000000..2ea1d78e61
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_location_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.25 10.5c0 2.07 1.68 3.75 3.75 3.75 2.07 0 3.75-1.68 3.75-3.75 0-2.07-1.68-3.75-3.75-3.75-2.07 0-3.75 1.68-3.75 3.75zm1.75 0c0-1.1 0.9-2 2-2s2 0.9 2 2-0.9 2-2 2-2-0.9-2-2z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.31 21.34C10.79 21.78 11.4 22 12 22c0.61 0 1.21-0.22 1.69-0.66 2.64-2.42 7.06-7.02 7.06-10.59C20.75 5.92 16.82 2 12 2s-8.75 3.93-8.75 8.75c0 3.57 4.42 8.17 7.06 10.59zM5 10.75a7.008 7.008 0 0 1 6.995-7 7.008 7.008 0 0 1 6.995 7c0 2.09-2.42 5.56-6.49 9.3-0.29 0.26-0.72 0.26-1.01 0C7.43 16.32 5 12.84 5 10.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_location_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_location_slash_24.xml
new file mode 100644
index 0000000000..db74edb5a5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_location_slash_24.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7.128 5.737A6.969 6.969 0 0 1 12 3.752a7.005 7.005 0 0 1 6.997 6.997c0 1.181-0.777 2.806-2.186 4.67l1.246 1.246c1.506-1.952 2.69-4.07 2.69-5.916 0-4.823-3.924-8.747-8.747-8.747A8.714 8.714 0 0 0 5.891 4.5l1.237 1.237z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M15.85 16.612L6.153 6.915l0.013-0.02-1.253-1.252-0.014 0.019L3.238 4 2 5.238l1.997 1.997a8.641 8.641 0 0 0-0.744 3.514c0 3.57 4.423 8.168 7.061 10.59a2.488 2.488 0 0 0 1.687 0.663c0.604 0 1.206-0.221 1.687-0.662a45.793 45.793 0 0 0 2.227-2.187L18.762 22 20 20.762l-2.909-2.908 0.016-0.02-1.242-1.24-0.015 0.018zm-1.173 1.303a43.43 43.43 0 0 1-2.174 2.136 0.738 0.738 0 0 1-1.006 0c-4.066-3.736-6.494-7.213-6.494-9.302 0-0.756 0.121-1.483 0.344-2.164l9.33 9.33z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.537 9.146A1.99 1.99 0 0 1 12 8.5c1.103 0 2 0.897 2 2a1.99 1.99 0 0 1-0.646 1.463l1.237 1.237a3.733 3.733 0 0 0 1.159-2.7A3.755 3.755 0 0 0 12 6.75a3.734 3.734 0 0 0-2.7 1.16l1.237 1.236z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_20.xml
new file mode 100644
index 0000000000..17978486c7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_20.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M7,9L7,7a3,3 0,1 1,6 0v2h0.25c0.69,0 1.25,0.56 1.25,1.25v4.5c0,0.69 -0.56,1.25 -1.25,1.25h-6.5c-0.69,0 -1.25,-0.56 -1.25,-1.25v-4.5C5.5,9.56 6.06,9 6.75,9L7,9zM8.25,7a1.75,1.75 0,1 1,3.5 0v2h-3.5L8.25,7z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_24.xml
new file mode 100644
index 0000000000..fd8eeb959f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M7 11V7a5 5 0 0 1 10 0v4a2.5 2.5 0 0 1 2.5 2.5v6A2.5 2.5 0 0 1 17 22H7a2.5 2.5 0 0 1-2.5-2.5v-6A2.5 2.5 0 0 1 7 11zm1.75-4a3.25 3.25 0 0 1 6.5 0v4h-6.5V7zm-2.5 6.5A0.75 0.75 0 0 1 7 12.75h10a0.75 0.75 0 0 1 0.75 0.75v6A0.75 0.75 0 0 1 17 20.25H7a0.75 0.75 0 0 1-0.75-0.75v-6z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_slash_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_slash_20.xml
new file mode 100644
index 0000000000..48d0d4a80c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_slash_20.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M13,9h0.25c0.69,0 1.25,0.56 1.25,1.25v2.592L10.658,9h1.092V7a1.745,1.745 0,0 0,-3.455 -0.363l-0.967,-0.967A2.988,2.988 0,0 1,10 4c1.654,0 3,1.346 3,3v2zM5.5,10.25C5.5,9.56 6.06,9 6.75,9h2.138l5.612,5.612v0.138c0,0.69 -0.56,1.25 -1.25,1.25h-6.5c-0.69,0 -1.25,-0.56 -1.25,-1.25v-4.5z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="m4.385,4.501 l12.114,12.114 -0.884,0.884L3.501,5.385l0.884,-0.884z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_slash_24.xml
new file mode 100644
index 0000000000..b31e23b456
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_slash_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9.13 5.508A3.242 3.242 0 0 1 12 3.75 3.254 3.254 0 0 1 15.25 7v4h-0.628l1.75 1.75H17a0.75 0.75 0 0 1 0.75 0.75v0.627l1.75 1.75V13.5c0-1.379-1.121-2.5-2.5-2.5V7c0-2.757-2.243-5-5-5-1.73 0-3.256 0.884-4.154 2.224L9.13 5.508z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M19.5 18.013v-0.015l-1.75-1.75v0.015l-3.513-3.513h0.014L12.501 11h-0.014L8.75 7.263V7.249L7.195 5.694l-0.003 0.01L3.988 2.5 2.75 3.738 7 7.988V11a2.503 2.503 0 0 0-2.5 2.5v6C4.5 20.879 5.622 22 7 22h10a2.504 2.504 0 0 0 2.354-1.658L21.012 22l1.238-1.238-2.75-2.75zm-1.75 0.724V19.5A0.75 0.75 0 0 1 17 20.25H7a0.75 0.75 0 0 1-0.75-0.75v-6A0.75 0.75 0 0 1 7 12.75h4.762l5.988 5.988zm-9-9V11h1.262L8.75 9.738z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_warning_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_warning_20.xml
new file mode 100644
index 0000000000..2e854faba6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_warning_20.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M8.963,18.436h8.574a1.25,1.25 0,0 0,1.085 -1.87l-4.285,-7.499a1.252,1.252 0,0 0,-2.174 0l-4.285,7.499a1.25,1.25 0,0 0,1.085 1.87zM14,11.5h-1.5v2.75L14,14.25L14,11.5zM14,15.5h-1.5L12.5,17L14,17v-1.5zM3,7L3,5a3,3 0,1 1,6 0v2h0.25c0.69,0 1.25,0.56 1.25,1.25v1.711L8.192,14L2.75,14c-0.69,0 -1.25,-0.56 -1.25,-1.25v-4.5C1.5,7.56 2.06,7 2.75,7L3,7zM4.25,5a1.75,1.75 0,1 1,3.5 0v2h-3.5L4.25,5z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_warning_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_warning_24.xml
new file mode 100644
index 0000000000..92a72604ac
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_lock_warning_24.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M22.799 20.749L18.8 13.755a1.496 1.496 0 0 0-2.599 0l-3.999 6.994C11.628 21.751 12.349 23 13.501 23H21.5c1.151 0 1.872-1.249 1.299-2.251z" />
+ <path
+ android:fillColor="#fff"
+ android:pathData="M18.25 16h-1.5v3h1.5v-3zm0 4h-1.5v1.5h1.5V20z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.899 20.004l0.431-0.754H5a0.752 0.752 0 0 1-0.75-0.75v-6c0-0.413 0.337-0.75 0.75-0.75h10c0.289 0 0.533 0.17 0.658 0.41a2.93 2.93 0 0 1 1.644-0.634A2.503 2.503 0 0 0 15 10V6c0-2.757-2.243-5-5-5S5 3.243 5 6v4a2.502 2.502 0 0 0-2.5 2.5v6C2.5 19.879 3.621 21 5 21h5.563c0.059-0.342 0.155-0.68 0.336-0.996zM6.75 6A3.254 3.254 0 0 1 10 2.75 3.254 3.254 0 0 1 13.25 6v4h-6.5V6z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_login_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_login_24.xml
new file mode 100644
index 0000000000..5b1261437e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_login_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 12a5 5 0 0 1 9.9-1H22v1.75h-2.25V16H18v-3.25h-6.056A5.001 5.001 0 0 1 2 12zm5-3.25a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_chrome_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_chrome_24.xml
new file mode 100644
index 0000000000..99a9ace127
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_chrome_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12zm17.214-4A8.257 8.257 0 0 0 12 3.75a8.235 8.235 0 0 0-6.264 2.886l2.446 4.235C8.671 9.215 10.186 8 12 8h7.214zm0.565 1.25h-4.886a3.978 3.978 0 0 1 0.411 5.002l-3.462 5.996C11.894 20.25 11.947 20.25 12 20.25c4.549 0 8.25-3.701 8.25-8.25 0-0.964-0.166-1.89-0.471-2.75zm-9.3 10.86l2.44-4.226C12.623 15.954 12.318 16 12 16a3.995 3.995 0 0 1-3.339-1.804l-0.002 0.001-3.725-6.452A8.2 8.2 0 0 0 3.75 12c0 4.03 2.904 7.394 6.73 8.11zM12 14.75A2.754 2.754 0 0 0 14.75 12 2.754 2.754 0 0 0 12 9.25 2.754 2.754 0 0 0 9.25 12c0 0.565 0.172 1.091 0.465 1.528l0.006 0.01A2.75 2.75 0 0 0 12 14.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_firefox_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_firefox_24.xml
new file mode 100644
index 0000000000..cb2d61d9c1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_firefox_24.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M21.334 8.852a5.478 5.478 0 0 0-2.008-2.457c0.491 0.92 0.833 1.913 1.014 2.941v0.021c-1.125-2.731-3.051-3.832-4.624-6.228L15.48 2.754a3.38 3.38 0 0 1-0.11-0.201 1.692 1.692 0 0 1-0.154-0.396 0.035 0.035 0 0 0-0.011-0.01 0.033 0.033 0 0 0-0.024 0h-0.007a7.04 7.04 0 0 0-3.443 5.383c0.117-0.007 0.234-0.017 0.352-0.017a5.078 5.078 0 0 1 4.387 2.479 3.543 3.543 0 0 0-2.413-0.566 3.996 3.996 0 0 1 1.971 4.745 4.004 4.004 0 0 1-4.327 2.77 4.55 4.55 0 0 1-1.765-0.443 4.093 4.093 0 0 1-2.359-3.165s0.461-1.667 3.308-1.667a2.386 2.386 0 0 0 1.205-1.071 15.96 15.96 0 0 1-2.426-1.397c-0.363-0.349-0.536-0.516-0.69-0.641a2.947 2.947 0 0 0-0.259-0.19A4.356 4.356 0 0 1 8.688 6a7.386 7.386 0 0 0-2.41 1.802 5.12 5.12 0 0 1-0.345-2.427 1.956 1.956 0 0 0-0.334 0.173A7.23 7.23 0 0 0 4.62 6.359a8.682 8.682 0 0 0-0.934 1.084 8.076 8.076 0 0 0-1.35 2.934c0 0.011-0.095 0.406-0.164 0.893l-0.031 0.227a6.357 6.357 0 0 0-0.06 0.555v0.029c-0.006 0.095-0.014 0.2-0.02 0.322v0.05a9.853 9.853 0 0 0 10.008 9.686 9.926 9.926 0 0 0 9.873-8.048l0.045-0.375a9.634 9.634 0 0 0-0.653-4.864z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_safari_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_safari_24.xml
new file mode 100644
index 0000000000..0a5384c40a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_logo_safari_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.101 16.703l5.346-2.617c0.298-0.139 0.503-0.354 0.651-0.642l2.599-5.337c0.298-0.633-0.158-1.118-0.81-0.801L10.56 9.904c-0.289 0.14-0.485 0.335-0.643 0.652L7.3 15.902c-0.298 0.615 0.186 1.099 0.801 0.801zm3.894-3.453a1.245 1.245 0 0 1-1.245-1.255c0-0.694 0.551-1.245 1.245-1.245 0.694 0 1.255 0.551 1.255 1.245 0 0.694-0.561 1.255-1.255 1.255z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12zm18.25 0c0-4.549-3.701-8.25-8.25-8.25-4.549 0-8.25 3.701-8.25 8.25 0 4.549 3.701 8.25 8.25 8.25 4.549 0 8.25-3.701 8.25-8.25z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_microphone_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_microphone_24.xml
new file mode 100644
index 0000000000..e74647aa45
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_microphone_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.25 5.75a3.75 3.75 0 1 1 7.5 0v4.75a3.75 3.75 0 1 1-7.5 0V5.75zm3.75-2a2 2 0 0 0-2 2v4.75a2 2 0 1 0 4 0V5.75a2 2 0 0 0-2-2z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.125 17.686a7.251 7.251 0 0 1-6.375-7.198H6.5a5.5 5.5 0 1 0 11 0h1.75a7.251 7.251 0 0 1-6.375 7.198v2.564H17V22H7v-1.75h4.125v-2.564z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_microphone_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_microphone_slash_24.xml
new file mode 100644
index 0000000000..4fe5205213
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_microphone_slash_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10 7.636V5.75c0-1.103 0.897-2 2-2s2 0.897 2 2v4.75c0 0.328-0.087 0.634-0.228 0.908l1.269 1.269A3.718 3.718 0 0 0 15.75 10.5V5.75A3.755 3.755 0 0 0 12 2a3.755 3.755 0 0 0-3.75 3.75v0.136L10 7.636zm7.529 7.53a7.216 7.216 0 0 0 1.721-4.679H17.5c0 1.3-0.456 2.494-1.213 3.436l1.242 1.243zM8.25 8.013L3.238 3 2 4.238l6.25 6.25V10.5A3.755 3.755 0 0 0 12 14.25h0.012l1.519 1.518c-0.486 0.143-1 0.219-1.531 0.219a5.507 5.507 0 0 1-5.5-5.5H4.75c0 3.702 2.788 6.764 6.375 7.197v2.566H7V22h10v-1.75h-4.125v-2.566a7.157 7.157 0 0 0 2.018-0.554l4.87 4.87L21 20.762l-4.558-4.558 0.003-0.002-1.248-1.248-0.003 0.002-1.26-1.26 0.003-0.002-1.306-1.306-0.004 0.001L10 9.763V9.757l-1.75-1.75v0.006z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_more_grid_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_more_grid_24.xml
new file mode 100644
index 0000000000..a3ff0f9ee2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_more_grid_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4 4.5h2.5V7H4V4.5zm2.5 6H4V13h2.5v-2.5zm6.75 0h-2.5V13h2.5v-2.5zm4.25 0H20V13h-2.5v-2.5zm-11 6H4V19h2.5v-2.5zm4.25 0h2.5V19h-2.5v-2.5zm9.25 0h-2.5V19H20v-2.5zm-6.75-12h-2.5V7h2.5V4.5zm4.25 0H20V7h-2.5V4.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_mozilla.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_mozilla.xml
new file mode 100644
index 0000000000..e06824d921
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_mozilla.xml
@@ -0,0 +1,7 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
+ <path android:pathData="M0 0h24v24H0zm20.4 15.035v-4.764c0-2.436-1.694-3.565-3.565-3.565-1.623 0-2.788 0.847-3.353 2.223-0.494-1.517-1.87-2.223-3.353-2.223-1.447 0-2.505 0.67-3.14 1.765V6.918H2.646v2.258h1.377V15H2.647v2.259H9V15H7.023v-3.6c0-1.412 0.565-2.435 1.977-2.435 1.165 0 1.765 0.67 1.765 2.47v5.824h4.34V15H13.73v-3.6c0-1.412 0.565-2.435 1.977-2.435 1.165 0 1.765 0.67 1.765 2.47v5.86h4.34v-2.26z" android:fillColor="@color/mozac_ui_icons_fill"/>
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_night_mode_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_night_mode_24.xml
new file mode 100644
index 0000000000..33b152e9cd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_night_mode_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.98 22c-0.34 0-0.67-0.02-1.01-0.05-4.87-0.49-8.72-4.56-8.95-9.46a9.967 9.967 0 0 1 7.9-10.27 1.371 1.371 0 0 1 1.41 2.11c-0.97 1.5-1.31 3.3-0.94 5.09 0.54 2.64 2.7 4.73 5.36 5.2 1.23 0.22 2.43 0.12 3.57-0.3 0.54-0.2 1.13-0.06 1.51 0.36 0.38 0.41 0.47 0.99 0.24 1.49a10.02 10.02 0 0 1-9.1 5.81L11.98 22zM9.42 4.17a8.213 8.213 0 0 0-5.66 8.24c0.19 4.04 3.37 7.39 7.38 7.8 3.22 0.31 6.3-1.25 7.93-3.96-1.17 0.29-2.4 0.34-3.62 0.12-3.36-0.6-6.08-3.24-6.77-6.57-0.4-1.94-0.14-3.9 0.73-5.62l0.01-0.01z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_24.xml
new file mode 100644
index 0000000000..c657fa63bd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M18 8H6V6.25h12V8zM6 12h7v-1.75H6V12z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 4.5A2.5 2.5 0 0 1 4.5 2h15A2.5 2.5 0 0 1 22 4.5v10a2.5 2.5 0 0 1-2.5 2.5H18v4.125a0.875 0.875 0 0 1-1.494 0.619L11.763 17H4.5A2.5 2.5 0 0 1 2 14.5v-10zm2.5-0.75A0.75 0.75 0 0 0 3.75 4.5v10c0 0.414 0.336 0.75 0.75 0.75h7.625c0.232 0 0.455 0.092 0.619 0.256l3.506 3.507v-2.888c0-0.483 0.392-0.875 0.875-0.875H19.5a0.75 0.75 0 0 0 0.75-0.75v-10a0.75 0.75 0 0 0-0.75-0.75h-15z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_dot_badge_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_dot_badge_fill_20.xml
new file mode 100644
index 0000000000..ccc3d2277a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_dot_badge_fill_20.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="#fff"
+ android:pathData="M10 15.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10 14a4 4 0 1 0 0-8 4 4 0 0 0 0 8z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_slash_24.xml
new file mode 100644
index 0000000000..2fac0b2e16
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_notification_slash_24.xml
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M18.237 17L22 20.762 20.762 22 18 19.237v1.888a0.876 0.876 0 0 1-1.494 0.619L11.763 17H4.5A2.502 2.502 0 0 1 2 14.5v-10c0-0.37 0.081-0.72 0.227-1.036L1 2.238 2.238 1l1.23 1.23 0.001-0.001L4.99 3.75H4.987l6.5 6.5h0.003L13 11.76v0.003l3.648 3.647 0.002-0.001L18.24 17h-0.003zm-9.224-6.75H6V12h4.763l5.487 5.487v1.526l-3.506-3.507a0.876 0.876 0 0 0-0.619-0.256H4.5a0.752 0.752 0 0 1-0.75-0.75V4.987l5.263 5.263z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7.111 3.75H19.5c0.413 0 0.75 0.337 0.75 0.75v10c0 0.413-0.337 0.75-0.75 0.75h-0.889l1.626 1.626A2.494 2.494 0 0 0 22 14.5v-10C22 3.121 20.879 2 19.5 2H5.361l1.75 1.75z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.361 8H18V6.25H9.611L11.361 8z"
+ tools:ignore="VectorRaster" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_open_in.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_open_in.xml
new file mode 100644
index 0000000000..22ed8d0e8e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_open_in.xml
@@ -0,0 +1,7 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
+ <path android:fillColor="@color/mozac_ui_icons_fill" android:pathData="M7 5C5.895 5 5 5.895 5 7v10c0 1.105 0.895 2 2 2h10c1.105 0 2-0.895 2-2v-1c0-0.552 0.448-1 1-1s1 0.448 1 1v1c0 2.21-1.79 4-4 4H7c-2.21 0-4-1.79-4-4V7c0-2.21 1.79-4 4-4h1c0.552 0 1 0.448 1 1S8.552 5 8 5H7zm6 0c-0.552 0-1-0.448-1-1s0.448-1 1-1h7c0.552 0 1 0.448 1 1v7c0 0.552-0.448 1-1 1s-1-0.448-1-1V6.414l-6.293 6.293c-0.39 0.39-1.024 0.39-1.414 0-0.39-0.39-0.39-1.024 0-1.414L17.586 5H13z"/>
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_packaging_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_packaging_24.xml
new file mode 100644
index 0000000000..cb11947a79
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_packaging_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M5.5 3A2.5 2.5 0 0 0 3 5.5v13A2.5 2.5 0 0 0 5.5 21h13a2.5 2.5 0 0 0 2.5-2.5v-13A2.5 2.5 0 0 0 18.5 3h-13zM4.75 5.5A0.75 0.75 0 0 1 5.5 4.75H8v6.754a0.875 0.875 0 0 0 1.3 0.765l2.7-1.5 2.7 1.5a0.875 0.875 0 0 0 1.3-0.765V4.75h2.5a0.75 0.75 0 0 1 0.75 0.75v13a0.75 0.75 0 0 1-0.75 0.75h-13a0.75 0.75 0 0 1-0.75-0.75v-13zm9.5-0.75h-4.5v5.267l1.825-1.014a0.875 0.875 0 0 1 0.85 0l1.825 1.014V4.75z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_page_zoom_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_page_zoom_24.xml
new file mode 100644
index 0000000000..4656a7edfc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_page_zoom_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.625 16v-3.625H7v-1.75h3.625V7h1.75v3.625H16v1.75h-3.625V16h-1.75z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2.004 11.224c0.14-4.952 4.27-9.08 9.222-9.22C16.589 1.852 21 6.171 21 11.5v7a2.5 2.5 0 0 1-2.5 2.5h-7c-5.33 0-9.65-4.413-9.496-9.776zM3.75 11.5c0 4.273 3.477 7.75 7.75 7.75s7.75-3.477 7.75-7.75-3.477-7.75-7.75-7.75-7.75 3.477-7.75 7.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_page_zoom_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_page_zoom_fill_24.xml
new file mode 100644
index 0000000000..7618b38421
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_page_zoom_fill_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.226,2.004c-4.952,0.14 -9.081,4.268 -9.222,9.22C1.85,16.587 6.17,21 11.5,21h7a2.5,2.5 0,0 0,2.5 -2.5v-7c0,-5.33 -4.411,-9.648 -9.774,-9.496zM10.625,16v-3.625H7v-1.75h3.625V7h1.75v3.625H16v1.75h-3.625V16h-1.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_passkey_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_passkey_24.xml
new file mode 100644
index 0000000000..1c21042887
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_passkey_24.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M14,7a4,4 0,1 1,-8 0,4 4,0 0,1 8,0z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M19.375,15.883a3.505,3.505 0,0 0,2.505 -4.315,3.461 3.461,0 0,0 -2.447,-2.447A3.724,3.724 0,0 0,18.5 9a3.5,3.5 0,0 0,-3.5 3.5,3.491 3.491,0 0,0 2.625,3.376V22h1.75v-1H21v-2.5h-1.625v-2.617zM16.75,12.5c0,-0.965 0.785,-1.75 1.75,-1.75 0.162,0 0.328,0.022 0.495,0.065a1.705,1.705 0,0 1,1.191 1.191,1.77 1.77,0 0,1 -0.3,1.564 1.752,1.752 0,0 1,-3.136 -1.07z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M16.375,16.736a4.733,4.733 0,0 1,-2.607 -3.986H6.75A4.75,4.75 0,0 0,2 17.5V21h14.375v-4.264z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pause_badge_fill_16.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pause_badge_fill_16.xml
new file mode 100644
index 0000000000..0ce77d4936
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pause_badge_fill_16.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6 12H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1zm5 0h-1a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_permissions_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_permissions_24.xml
new file mode 100644
index 0000000000..0320b89369
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_permissions_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M2 6.5A2.5 2.5 0 0 1 4.5 4h15A2.5 2.5 0 0 1 22 6.5v10a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 16.5v-10zm17.5 10.75H12V5.75h7.5a0.75 0.75 0 0 1 0.75 0.75v10a0.75 0.75 0 0 1-0.75 0.75zM6.908 13.561L10.5 9.97 9.439 8.91l-3.061 3.061-1.817-1.817-1.06 1.061 2.347 2.347a0.75 0.75 0 0 0 1.06 0z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M13.125 9.56l1.06-1.06 1.94 1.94 1.939-1.94 1.06 1.061-1.938 1.94 1.938 1.938-1.06 1.06-1.94-1.938-1.939 1.94-1.06-1.061 1.94-1.94-1.94-1.94z"
+ tools:ignore="VectorRaster" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pet.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pet.xml
new file mode 100644
index 0000000000..2294410c09
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pet.xml
@@ -0,0 +1,17 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M28.5,8.1c0-1.1-1-1.9-2.1-2.4V3.7c-0.2-0.2-0.3-0.3-0.6-0.3c-0.6,0-1.1,0.8-1.3,2.1c-0.2,0-0.3,0-0.5,0l0,0
+ c0-0.2,0-0.3-0.2-0.5c-0.3-1.1-0.8-1.9-1.3-2.6C22,2.6,21.7,3.2,21.7,4L22,6.3c-0.3,0.2-0.6,0.3-1,0.6l-3.5,3.7l0,0
+ c0,0-6.3-0.8-10.9,0.2c-0.6,0-1,0.2-1.1,0.3c-0.5,0.2-0.8,0.3-1.1,0.6c-1.1-0.8-2.2-2.1-3.2-4c0-0.3-0.5-0.5-0.8-0.5s-0.5,0.6-0.3,1
+ c0.8,2.1,2.1,3.5,3.4,4.5c-0.5,0.5-0.8,1-1,1.6c0,0-0.3,2.2-0.3,5.5l1.4,8c0,1,0.8,1.8,1.9,1.8c1,0,1.9-0.8,1.9-1.8V23l0.5-1.3h8.8
+ l0.8,1.3v4.7c0,1,0.8,1.8,1.9,1.8c1,0,1.6-0.6,1.8-1.4l0,0l1.9-9l0,0l2.1-6.4h3c3.4,0,3.7-2.9,3.7-2.9L28.5,8.1z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_24.xml
new file mode 100644
index 0000000000..48c5ab7837
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M12.172 3.238L10.78 4.629l3.823 3.824 3.266 0.072c1.875 0.041 2.786 2.31 1.46 3.637l-2.965 2.964L22 20.762 20.762 22l-5.636-5.636-2.964 2.964c-1.327 1.328-3.594 0.415-3.637-1.458l-0.073-3.267L4.63 10.78l-1.391 1.392L2 10.934 10.934 2l1.238 1.238zM5.866 9.543l3.676-3.676 4.32 4.32 3.969 0.088a0.38 0.38 0 0 1 0.26 0.65l-7.167 7.167a0.38 0.38 0 0 1-0.65-0.262l-0.088-3.967-4.32-4.32z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_badge_fill_16.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_badge_fill_16.xml
new file mode 100644
index 0000000000..6838f756f5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_badge_fill_16.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:fillColor="#fff"
+ android:pathData="M7.428 3.416l0.598 0.598 0.287 0.287L7.71 4.905 9.115 6.31l1.751 0.088a0.836 0.836 0 0 1 0.747 0.542 0.835 0.835 0 0 1-0.191 0.902l-1.347 1.347 2.51 2.51-0.143 0.143-0.598 0.598-0.143 0.143-2.51-2.51-1.347 1.347a0.836 0.836 0 0 1-0.902 0.191 0.836 0.836 0 0 1-0.542-0.747L6.31 9.115 4.905 7.71 4.301 8.313l-0.22-0.22-0.067-0.066-0.598-0.599 4.012-4.012zm0-1.416L6.72 2.708 2.708 6.72 2 7.428l0.708 0.708 0.598 0.598 0.068 0.067 0.219 0.219 0.708 0.708 0.604-0.604 0.425 0.427 0.069 1.366a1.848 1.848 0 0 0 3.152 1.213l0.639-0.639 1.802 1.802L11.699 14l0.708-0.708 0.143-0.143 0.598-0.598 0.144-0.144L14 11.699l-0.708-0.708-1.802-1.802 0.64-0.639a1.827 1.827 0 0 0 0.418-1.969 1.848 1.848 0 0 0-1.631-1.183L9.551 5.33 9.126 4.905 9.73 4.301 9.022 3.593 8.735 3.306 8.136 2.708 7.428 2z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.866 6.399L9.115 6.31 7.71 4.905l0.604-0.604-0.287-0.287-0.599-0.598-4.012 4.012 0.598 0.598 0.067 0.067 0.22 0.22 0.604-0.603L6.31 9.115l0.088 1.751a0.836 0.836 0 0 0 0.542 0.747 0.835 0.835 0 0 0 0.902-0.191l1.347-1.347 2.51 2.51 0.143-0.143 0.598-0.598 0.143-0.143-2.51-2.51 1.347-1.347a0.835 0.835 0 0 0 0.191-0.902 0.83 0.83 0 0 0-0.745-0.543z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_fill_24.xml
new file mode 100644
index 0000000000..98e17c09f4
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_fill_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.172 3.238L10.78 4.629l3.823 3.823 3.864 0.086a1.85 1.85 0 0 1 1.698 1.176 1.847 1.847 0 0 1-0.414 2.024l-3.388 3.388L22 20.762 20.762 22l-5.636-5.637-3.389 3.389a1.853 1.853 0 0 1-2.024 0.414 1.871 1.871 0 0 1-1.177-1.698l-0.085-3.864-3.822-3.823-1.391 1.39L2 10.935 10.934 2l1.238 1.238z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_filled.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_filled.xml
new file mode 100644
index 0000000000..80394cc3be
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_filled.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.617 2.076a1 1 0 0 1 1.09 0.217l8 8A1 1 0 0 1 21 12c-1.034 0-1.886 0.007-2.6 0.212-0.647 0.184-1.149 0.522-1.506 1.235-0.189 0.378-0.293 0.773-0.379 1.247-0.026 0.144-0.053 0.314-0.082 0.5-0.057 0.357-0.123 0.77-0.207 1.148-0.288 1.297-0.875 2.721-2.519 4.365a1 1 0 0 1-1.414 0L8.5 16.914l-4.793 4.793a1 1 0 1 1-1.414-1.414L7.086 15.5l-3.793-3.793a1 1 0 0 1 0-1.414c1.644-1.644 3.068-2.23 4.365-2.52 0.378-0.083 0.79-0.149 1.149-0.206 0.185-0.029 0.355-0.056 0.498-0.082 0.475-0.086 0.87-0.19 1.248-0.38 0.713-0.356 1.05-0.858 1.235-1.505C11.993 4.885 12 4.034 12 3a1 1 0 0 1 0.617-0.924z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_slash_24.xml
new file mode 100644
index 0000000000..a7a930a25e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_slash_24.xml
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 10.933L10.933 2l1.238 1.238-1.39 1.39 2.793 2.795-1.237 1.238-2.794-2.794-3.676 3.676 2.795 2.794-1.238 1.238-2.795-2.794-1.391 1.39L2 10.933z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M12.163 19.328l2.963-2.963L20.761 22l1.238-1.238-5.635-5.635 2.966-2.966A2.103 2.103 0 0 0 19.8 9.86a2.102 2.102 0 0 0-1.931-1.336l-0.774-0.017 4.52-4.52-1.238-1.237-5.703 5.703h-0.008l-1.268 1.268 0.004 0.004-3.676 3.676-0.004-0.004-1.268 1.268v0.008l-6.34 6.34 1.238 1.237 5.156-5.156 0.017 0.774c0.02 0.867 0.531 1.606 1.337 1.93a2.108 2.108 0 0 0 2.301-0.47zm-1.942-3.947l0.054 2.448a0.362 0.362 0 0 0 0.238 0.345c0.089 0.036 0.257 0.07 0.411-0.084l7.167-7.167a0.362 0.362 0 0 0 0.084-0.411 0.362 0.362 0 0 0-0.345-0.238l-2.448-0.054-5.161 5.161z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_slash_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_slash_fill_24.xml
new file mode 100644
index 0000000000..468269cd9f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_pin_slash_fill_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="m13.57,7.42 l-1.23,1.24 -4.92,4.92 -2.79,-2.8 -1.39,1.39L2,10.93 10.93,2l1.24,1.24 -1.39,1.39 2.79,2.79zM16.36,15.12 L22,20.76h0.01L20.77,22l-5.64,-5.64 -2.96,2.96c-0.42,0.41 -0.95,0.63 -1.5,0.63 -0.27,0 -0.55,-0.05 -0.81,-0.16a2.092,2.092 0,0 1,-1.34 -1.93l-0.02,-0.77 -5.15,5.15L2.11,21l6.34,-6.35 1.27,-1.26L20.37,2.74l1.24,1.24 -4.52,4.52 0.77,0.02c0.87,0.02 1.61,0.54 1.93,1.34 0.32,0.8 0.14,1.69 -0.47,2.3l-2.96,2.96z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_play_badge_fill_16.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_play_badge_fill_16.xml
new file mode 100644
index 0000000000..4381c247f8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_play_badge_fill_16.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4 11.998V4a1 1 0 0 1 1.496-0.868l7 3.999a1 1 0 0 1 0 1.736l-7 3.999A1 1 0 0 1 4 11.998z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_plugin_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_plugin_24.xml
new file mode 100644
index 0000000000..762c701245
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_plugin_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.25 6V4a1 1 0 0 0-1-1h-3a1 1 0 0 0-1 1v2H4.5A2.5 2.5 0 0 0 2 8.5v10A2.5 2.5 0 0 0 4.5 21h15a2.5 2.5 0 0 0 2.5-2.5v-10A2.5 2.5 0 0 0 19.5 6h-0.75V4a1 1 0 0 0-1-1h-3a1 1 0 0 0-1 1v2h-3.5zm-6.5 2.5A0.75 0.75 0 0 1 4.5 7.75h15a0.75 0.75 0 0 1 0.75 0.75v10a0.75 0.75 0 0 1-0.75 0.75h-15a0.75 0.75 0 0 1-0.75-0.75v-10z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_plus_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_plus_24.xml
new file mode 100644
index 0000000000..57d12452ff
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_plus_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.125 12.875v7.875h1.75v-7.875h7.875v-1.75h-7.875V3.25h-1.75v7.875H3.25v1.75h7.875z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_preferences.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_preferences.xml
new file mode 100644
index 0000000000..9085f636b0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_preferences.xml
@@ -0,0 +1,14 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:autoMirrored="true"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M15,7h-2.1a4.967,4.967 0,0 0,-0.732 -1.753l1.49,-1.49a1,1 0,0 0,-1.414 -1.414l-1.49,1.49A4.968,4.968 0,0 0,9 3.1V1a1,1 0,0 0,-2 0v2.1a4.968,4.968 0,0 0,-1.753 0.732l-1.49,-1.49a1,1 0,0 0,-1.414 1.415l1.49,1.49A4.967,4.967 0,0 0,3.1 7H1a1,1 0,0 0,0 2h2.1a4.968,4.968 0,0 0,0.737 1.763c-0.014,0.013 -0.032,0.017 -0.045,0.03l-1.45,1.45a1,1 0,1 0,1.414 1.414l1.45,-1.45c0.013,-0.013 0.018,-0.031 0.03,-0.045A4.968,4.968 0,0 0,7 12.9V15a1,1 0,0 0,2 0v-2.1a4.968,4.968 0,0 0,1.753 -0.732l1.49,1.49a1,1 0,0 0,1.414 -1.414l-1.49,-1.49A4.967,4.967 0,0 0,12.9 9H15a1,1 0,0 0,0 -2zM5,8a3,3 0,1 1,3 3,3 3,0 0,1 -3,-3z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_price_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_price_24.xml
new file mode 100644
index 0000000000..dbcc92a58f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_price_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9.317 7.637l-1.591 1.59-1.591-1.59 1.59-1.591 1.592 1.59z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2.75 2a0.875 0.875 0 0 0-0.875 0.875v8.584c0 0.232 0.092 0.455 0.256 0.619l8.18 8.179a2.5 2.5 0 0 0 3.535 0l6.285-6.285a2.5 2.5 0 0 0 0-3.536l-8.18-8.18A0.875 0.875 0 0 0 11.334 2H2.75zm0.875 9.097V3.75h7.345l7.923 7.924a0.75 0.75 0 0 1 0 1.06l-6.285 6.285a0.75 0.75 0 0 1-1.06 0l-7.923-7.922z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_print_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_print_24.xml
new file mode 100644
index 0000000000..681fffdff0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_print_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7 3.5A2.5 2.5 0 0 1 9.5 1h5A2.5 2.5 0 0 1 17 3.5V7h2.5A2.5 2.5 0 0 1 22 9.5v6a2.5 2.5 0 0 1-2.5 2.5H17v1.5a2.5 2.5 0 0 1-2.5 2.5h-5A2.5 2.5 0 0 1 7 19.5V18H4.5A2.5 2.5 0 0 1 2 15.5v-6A2.5 2.5 0 0 1 4.5 7H7V3.5zM8.75 7h6.5V3.5a0.75 0.75 0 0 0-0.75-0.75h-5A0.75 0.75 0 0 0 8.75 3.5V7zm0 7v5.5c0 0.414 0.336 0.75 0.75 0.75h5a0.75 0.75 0 0 0 0.75-0.75V14h-6.5zM19 10h-2v2h2v-2z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_24.xml
new file mode 100644
index 0000000000..5d3237c2ce
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_24.xml
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M21.914,8.849c0.05,-0.99 -0.62,-1.873 -1.587,-2.093a7.425,7.425 0,0 0,-6.702 1.804l-0.308,0.286a1.939,1.939 0,0 1,-2.643 -0.001l-0.301,-0.28a7.427,7.427 0,0 0,-6.707 -1.808h-0.002A2.043,2.043 0,0 0,2.08 8.844v0.006c-0.247,0.975 0.134,4.575 0.35,5.141 0.434,2.287 2.303,4.007 4.543,4.007 1.12,0 2.132,-0.447 2.933,-1.161l0.488,-0.418a2.436,2.436 0,0 1,3.139 -0.025l0.734,0.606v-0.003c0.772,0.621 1.718,1.001 2.754,1.001 2.24,0 4.109,-1.72 4.543,-4.007 0.216,-0.567 0.611,-4.152 0.35,-5.142zM9.79,12.69c-0.533,0.603 -1.36,0.991 -2.29,0.991 -0.93,0 -1.757,-0.388 -2.29,-0.991a0.866,0.866 0,0 1,0 -1.131c0.533,-0.603 1.36,-0.991 2.29,-0.991 0.93,0 1.757,0.388 2.29,0.991 0.28,0.317 0.28,0.815 0,1.131zM18.79,12.69c-0.533,0.603 -1.36,0.991 -2.29,0.991 -0.93,0 -1.757,-0.388 -2.29,-0.991a0.866,0.866 0,0 1,0 -1.131c0.533,-0.603 1.36,-0.991 2.29,-0.991 0.93,0 1.757,0.388 2.29,0.991 0.28,0.317 0.28,0.815 0,1.131z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_20.xml
new file mode 100644
index 0000000000..b6ce9a33e8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_20.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="?mozac_ic_private_mode_circle_fill_background_color"
+ android:pathData="M10,18c-4.411,0 -8,-3.589 -8,-8s3.589,-8 8,-8 8,3.589 8,8 -3.589,8 -8,8z" />
+ <path
+ android:fillColor="?mozac_ic_private_mode_circle_fill_icon_color"
+ android:pathData="M14.957,8.51a1.02,1.02 0,0 0,-0.793 -1.046,3.714 3.714,0 0,0 -3.351,0.902l-0.154,0.143a0.968,0.968 0,0 1,-1.321 0l-0.15,-0.14a3.712,3.712 0,0 0,-3.354 -0.905c-0.482,0.11 -0.817,0.549 -0.794,1.043v0.003c-0.123,0.488 0.067,2.287 0.175,2.571 0.217,1.144 1.152,2.004 2.272,2.004 0.56,0 1.066,-0.223 1.467,-0.581l0.244,-0.209a1.219,1.219 0,0 1,1.57 -0.012l0.366,0.303v-0.002c0.386,0.31 0.859,0.5 1.377,0.5 1.12,0 2.055,-0.86 2.272,-2.004 0.107,-0.283 0.304,-2.075 0.174,-2.57zM8.874,10.511c-0.32,0.387 -0.816,0.637 -1.374,0.637a1.775,1.775 0,0 1,-1.374 -0.637,0.587 0.587,0 0,1 0,-0.727c0.32,-0.387 0.816,-0.636 1.374,-0.636 0.558,0 1.054,0.249 1.374,0.637a0.586,0.586 0,0 1,0 0.726zM13.874,10.511c-0.32,0.387 -0.816,0.637 -1.374,0.637a1.775,1.775 0,0 1,-1.374 -0.637,0.587 0.587,0 0,1 0,-0.727c0.32,-0.387 0.816,-0.637 1.374,-0.637 0.558,0 1.054,0.249 1.374,0.637a0.587,0.587 0,0 1,0 0.727z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_24.xml
new file mode 100644
index 0000000000..bcb350e8fd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_24.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?mozac_ic_private_mode_circle_fill_background_color"
+ android:pathData="M12,22C6.486,22 2,17.514 2,12S6.486,2 12,2s10,4.486 10,10 -4.486,10 -10,10z" />
+ <path
+ android:fillColor="?mozac_ic_private_mode_circle_fill_icon_color"
+ android:pathData="M18.196,10.053a1.275,1.275 0,0 0,-0.992 -1.308,4.642 4.642,0 0,0 -4.189,1.127l-0.192,0.179a1.208,1.208 0,0 1,-1.651 -0.001l-0.188,-0.175a4.643,4.643 0,0 0,-4.192 -1.13,1.277 1.277,0 0,0 -0.992,1.304v0.004c-0.154,0.609 0.084,2.859 0.219,3.213 0.271,1.429 1.439,2.505 2.84,2.505 0.699,0 1.332,-0.279 1.833,-0.726l0.305,-0.261a1.524,1.524 0,0 1,1.962 -0.016l0.458,0.379v-0.002a2.734,2.734 0,0 0,1.72 0.626c1.4,0 2.569,-1.075 2.841,-2.505 0.134,-0.354 0.38,-2.594 0.218,-3.213zM10.603,12.524A2.072,2.072 0,0 1,9 13.267c-0.651,0 -1.23,-0.291 -1.603,-0.743a0.685,0.685 0,0 1,0 -0.848A2.072,2.072 0,0 1,9 10.933c0.65,0 1.23,0.291 1.603,0.743a0.685,0.685 0,0 1,0 0.848zM16.603,12.524a2.072,2.072 0,0 1,-1.603 0.743c-0.651,0 -1.23,-0.291 -1.603,-0.743a0.685,0.685 0,0 1,0 -0.848A2.072,2.072 0,0 1,15 10.933c0.651,0 1.23,0.291 1.603,0.743a0.685,0.685 0,0 1,0 0.848z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_48.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_48.xml
new file mode 100644
index 0000000000..dfb4091077
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_48.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:fillColor="?mozac_ic_private_mode_circle_fill_background_color"
+ android:pathData="M24,24m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0" />
+ <path
+ android:fillColor="?mozac_ic_private_mode_circle_fill_icon_color"
+ android:pathData="M33.914,20.849c0.05,-0.99 -0.62,-1.873 -1.587,-2.093a7.425,7.425 0,0 0,-6.702 1.804l-0.308,0.286a1.939,1.939 0,0 1,-2.643 -0.001l-0.301,-0.28a7.427,7.427 0,0 0,-6.707 -1.808h-0.002a2.043,2.043 0,0 0,-1.584 2.087v0.006c-0.247,0.975 0.134,4.575 0.35,5.141 0.434,2.287 2.303,4.007 4.543,4.007 1.119,0 2.132,-0.447 2.933,-1.161l0.488,-0.418a2.436,2.436 0,0 1,3.139 -0.025l0.734,0.606v-0.003c0.772,0.621 1.718,1.001 2.754,1.001 2.24,0 4.109,-1.72 4.543,-4.007 0.216,-0.567 0.611,-4.152 0.35,-5.142zM21.79,24.69c-0.533,0.603 -1.36,0.991 -2.29,0.991 -0.93,0 -1.757,-0.388 -2.29,-0.991a0.866,0.866 0,0 1,0 -1.131c0.533,-0.603 1.36,-0.991 2.29,-0.991 0.93,0 1.757,0.388 2.29,0.991 0.28,0.317 0.28,0.815 0,1.131zM30.79,24.69c-0.533,0.603 -1.36,0.991 -2.29,0.991 -0.93,0 -1.757,-0.388 -2.29,-0.991a0.866,0.866 0,0 1,0 -1.131c0.533,-0.603 1.36,-0.991 2.29,-0.991 0.93,0 1.757,0.388 2.29,0.991 0.28,0.317 0.28,0.815 0,1.131z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_stroke_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_stroke_20.xml
new file mode 100644
index 0000000000..2ee67055dd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_private_mode_circle_fill_stroke_20.xml
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_private_mode_circle_fill"
+ android:pathData="M1.5 10c0 4.686 3.814 8.5 8.5 8.5s8.5-3.814 8.5-8.5-3.814-8.5-8.5-8.5S1.5 5.314 1.5 10z"
+ android:strokeWidth="1"
+ android:strokeColor="#fff"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M12.49 6.99h1.28c0.67 0 1.21 0.54 1.21 1.21v2c0 1.01-0.5 1.95-1.34 2.51l-0.16 0.11c-0.2 0.13-0.43 0.2-0.67 0.2h-0.34c-0.25 0-0.49-0.08-0.7-0.23l-1.13-0.81a0.897 0.897 0 0 0-0.52-0.17H9.84c-0.19 0-0.37 0.06-0.52 0.17l-1.13 0.81c-0.2 0.15-0.45 0.23-0.7 0.23H7.15c-0.24 0-0.47-0.07-0.67-0.2l-0.16-0.11a3.01 3.01 0 0 1-1.34-2.51v-2c0-0.67 0.54-1.21 1.21-1.21h1.3c0.71 0 1.37 0.32 1.81 0.87 0.17 0.21 0.42 0.33 0.69 0.33 0.27 0 0.52-0.12 0.69-0.33 0.44-0.55 1.11-0.87 1.81-0.87zm-3.62 3.2c-0.32 0.36-0.82 0.59-1.37 0.59-0.55 0-1.05-0.23-1.37-0.59a0.517 0.517 0 0 1 0-0.68C6.45 9.15 6.95 8.92 7.5 8.92c0.55 0 1.05 0.23 1.37 0.59 0.17 0.19 0.17 0.49 0 0.68zm2.26 0c0.32 0.36 0.82 0.59 1.37 0.59 0.55 0 1.05-0.23 1.37-0.59 0.17-0.19 0.17-0.49 0-0.68-0.32-0.36-0.82-0.59-1.37-0.59-0.55 0-1.05 0.23-1.37 0.59-0.17 0.19-0.17 0.49 0 0.68z"
+ tools:ignore="VectorPath,VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_qr_code_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_qr_code_24.xml
new file mode 100644
index 0000000000..d3765da043
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_qr_code_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5.5 3A2.5 2.5 0 0 0 3 5.5V9h1.75V5.5A0.75 0.75 0 0 1 5.5 4.75H9V3H5.5zm5.5 7.25A0.75 0.75 0 0 1 10.25 11h-2.5A0.75 0.75 0 0 1 7 10.25v-2.5A0.75 0.75 0 0 1 7.75 7h2.5A0.75 0.75 0 0 1 11 7.75v2.5zM17 15h-2v-2h2v-2h-2V9h2V7h-2v2h-2v2h2v2h-2v2h-2v-2H9v2H7v2h2v-2h2v2h2v-2h2v2h2v-2zM3 18.5V15h1.75v3.5c0 0.414 0.336 0.75 0.75 0.75H9V21H5.5A2.5 2.5 0 0 1 3 18.5zM15 3h3.5A2.5 2.5 0 0 1 21 5.5V9h-1.75V5.5a0.75 0.75 0 0 0-0.75-0.75H15V3zm6 15.5V15h-1.75v3.5a0.75 0.75 0 0 1-0.75 0.75H15V21h3.5a2.5 2.5 0 0 0 2.5-2.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_quality_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_quality_24.xml
new file mode 100644
index 0000000000..e087551b52
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_quality_24.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.042 13.996l3.457-3.46L14.262 9.3l-2.84 2.84-1.685-1.685L8.5 11.692l2.305 2.304a0.875 0.875 0 0 0 1.237 0z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M13.732 2.674a2.449 2.449 0 0 0-3.463 0L8.774 4.17H6.66a2.448 2.448 0 0 0-2.448 2.448v2.114l-1.495 1.494a2.449 2.449 0 0 0 0 3.463l1.495 1.494v2.114a2.448 2.448 0 0 0 2.448 2.448h2.114l1.494 1.495a2.449 2.449 0 0 0 3.463 0l1.494-1.495h2.114a2.448 2.448 0 0 0 2.448-2.448v-2.114l1.494-1.494a2.446 2.446 0 0 0 0.002-3.463l-1.495-1.494V6.617a2.448 2.448 0 0 0-2.448-2.448h-2.114l-1.494-1.495zm-2.225 1.238a0.7 0.7 0 0 1 0.987 0l1.751 1.75a0.875 0.875 0 0 0 0.619 0.257h2.476c0.386 0 0.698 0.312 0.698 0.698v2.476c0 0.232 0.092 0.455 0.256 0.619l1.751 1.75a0.696 0.696 0 0 1 0 0.987l-1.752 1.752a0.875 0.875 0 0 0-0.256 0.619v2.476a0.698 0.698 0 0 1-0.698 0.698h-2.476a0.875 0.875 0 0 0-0.619 0.256l-1.75 1.751a0.7 0.7 0 0 1-0.988 0l-1.751-1.75a0.875 0.875 0 0 0-0.619-0.257H6.66a0.698 0.698 0 0 1-0.698-0.698V14.82a0.875 0.875 0 0 0-0.256-0.619l-1.751-1.75a0.7 0.7 0 0 1 0-0.988l1.75-1.751a0.875 0.875 0 0 0 0.257-0.619V6.617c0-0.386 0.312-0.698 0.698-0.698h2.476a0.875 0.875 0 0 0 0.619-0.256l1.752-1.751z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reader_view_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reader_view_24.xml
new file mode 100644
index 0000000000..6ee608ecfd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reader_view_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M7 3.75A0.75 0.75 0 0 0 6.25 4.5v14c0 0.414 0.336 0.75 0.75 0.75h10a0.75 0.75 0 0 0 0.75-0.75v-14A0.75 0.75 0 0 0 17 3.75H7zM4.5 4.5A2.5 2.5 0 0 1 7 2h10a2.5 2.5 0 0 1 2.5 2.5v14A2.5 2.5 0 0 1 17 21H7a2.5 2.5 0 0 1-2.5-2.5v-14z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.75 6.25h6.5V8h-6.5V6.25zm0 4h6.5V12h-6.5v-1.75zm0 4h3.972V16H8.75v-1.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reader_view_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reader_view_fill_24.xml
new file mode 100644
index 0000000000..f170cc57e7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reader_view_fill_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M4.5,4.5A2.5,2.5 0,0 1,7 2h10a2.5,2.5 0,0 1,2.5 2.5v14A2.5,2.5 0,0 1,17 21L7,21a2.5,2.5 0,0 1,-2.5 -2.5v-14zM8.75,6.25h6.5L15.25,8h-6.5L8.75,6.25zM8.75,10.25h6.5L15.25,12h-6.5v-1.75zM8.75,14.25h3.972L12.722,16L8.75,16v-1.75z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reading_list_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reading_list_24.xml
new file mode 100644
index 0000000000..632926fcbf
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reading_list_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000"
+ android:pathData="M3 5.5A2.5 2.5 0 0 1 5.5 3h4.75a0.88 0.88 0 0 1 0.434 0.115L12 3.867l1.316-0.752A0.875 0.875 0 0 1 13.75 3h4.75A2.5 2.5 0 0 1 21 5.5v13a2.5 2.5 0 0 1-2.5 2.5h-4.518l-1.548 0.885a0.875 0.875 0 0 1-0.868 0L10.018 21H5.5A2.5 2.5 0 0 1 3 18.5v-13zm2.5-0.75A0.75 0.75 0 0 0 4.75 5.5v13c0 0.414 0.336 0.75 0.75 0.75h4.75a0.88 0.88 0 0 1 0.434 0.115L12 20.117l1.316-0.752a0.874 0.874 0 0 1 0.434-0.115h4.75a0.75 0.75 0 0 0 0.75-0.75v-13a0.75 0.75 0 0 0-0.75-0.75h-4.518l-1.548 0.885a0.875 0.875 0 0 1-0.868 0L10.018 4.75H5.5z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M12 13H7v-1.75h5V13zm-1 4H7v-1.75h4V17zm1-8H7V7.25h5V9z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reading_list_add_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reading_list_add_24.xml
new file mode 100644
index 0000000000..13e979a615
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reading_list_add_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11 21a0.87 0.87 0 0 1-0.434-0.115L9.018 20H4.5A2.503 2.503 0 0 1 2 17.5v-13C2 3.121 3.122 2 4.5 2h4.75a0.88 0.88 0 0 1 0.434 0.115L11 2.867l1.316-0.752A0.88 0.88 0 0 1 12.75 2h4.75C18.878 2 20 3.121 20 4.5V13h-1.75V4.5a0.75 0.75 0 0 0-0.75-0.75h-4.518l-1.548 0.885a0.873 0.873 0 0 1-0.868 0L9.018 3.75H4.5A0.75 0.75 0 0 0 3.75 4.5v13c0 0.413 0.336 0.75 0.75 0.75h4.75a0.88 0.88 0 0 1 0.434 0.115L11 19.117l1.316-0.752a0.88 0.88 0 0 1 0.434-0.115H13v1.746l-1.566 0.889A0.87 0.87 0 0 1 11 21zm9-2.75V15h-1.75v3.25H15V20h3.25v3H20v-3h3v-1.75h-3z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6 12h5v-1.75H6V12zm4 4H6v-1.75h4V16zM6 8h5V6.25H6V8z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reorder.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reorder.xml
new file mode 100644
index 0000000000..0be284c3ff
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_reorder.xml
@@ -0,0 +1,7 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
+ <path android:pathData="M3 15h18v-2H3v2zm0 4h18v-2H3v2zm0-8h18V9H3v2zm0-6v2h18V5H3z" android:fillColor="@color/mozac_ui_icons_fill"/>
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_rocket.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_rocket.xml
new file mode 100644
index 0000000000..248a5018d5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_rocket.xml
@@ -0,0 +1,17 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M22,3a1,1 0,0 0,-1 -1C16.3,2 10.873,5.66 7.089,10H4a1,1 0,0 0,-0.895 0.553l-1,2a1,1 0,0 0,0.579 1.395l1.67,0.557a1,1 0,0 0,0.111 0.212,21.8 21.8,0 0,0 4.818,4.818 0.791,0.791 0,0 0,0.082 0.042l0.7,1.789a1,1 0,0 0,1.378 0.529l2,-1A1,1 0,0 0,14 20V16.911C18.34,13.127 22,7.705 22,3ZM12.326,15.729A24.182,24.182 0,0 1,9.909 17.5,19.674 19.674,0 0,1 6.5,14.09a24.65,24.65 0,0 1,1.761 -2.4C11.623,7.659 16.23,4.6 19.921,4.079 19.4,7.765 16.349,12.365 12.326,15.729Z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M17.33,7.578a4.906,4.906 0,0 0,-0.915 -0.913,0.416 0.416,0 0,0 -0.378,-0.061A10.162,10.162 0,0 0,13.829 7.92a0.417,0.417 0,0 0,-0.041 0.625l1.667,1.667a0.42,0.42 0,0 0,0.295 0.121h0.029a0.414,0.414 0,0 0,0.3 -0.164,11.312 11.312,0 0,0 1.309,-2.189A0.422,0.422 0,0 0,17.33 7.578Z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_rocket_filled.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_rocket_filled.xml
new file mode 100644
index 0000000000..f10710664d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_rocket_filled.xml
@@ -0,0 +1,17 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6.623,19.222a10.637,10.637 0,0 1,-2.548 0.7,10.637 10.637,0 0,1 0.7,-2.548 1,1 0,1 0,-1.851 -0.754A11.638,11.638 0,0 0,2 21a1,1 0,0 0,1 1,11.638 11.638,0 0,0 4.377,-0.927 1,1 0,1 0,-0.754 -1.851Z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.411,10L4,10a1,1 0,0 0,-0.895 0.553l-1,2a1,1 0,0 0,0.579 1.395l3,1a1,1 0,0 0,0.27 0.045,20.877 20.877,0 0,0 3,3.011 1,1 0,0 0,0.032 0.612l1.083,2.75a1,1 0,0 0,1.378 0.529l2,-1A1,1 0,0 0,14 20L14,15.589C17.968,11.922 21,7.029 21,3 16.971,3 12.078,6.032 8.411,10ZM14.083,8.25A9.92,9.92 0,0 1,16.167 7,4.48 4.48,0 0,1 17,7.833a11.121,11.121 0,0 1,-1.25 2.084Z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_save_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_save_24.xml
new file mode 100644
index 0000000000..9c821755e0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_save_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6,4.5A2.5,2.5 0,0 1,8.5 2h8A2.5,2.5 0,0 1,19 4.5v15.625a0.875,0.875 0,0 1,-1.315 0.756L12.5,17.863 7.315,20.88A0.875,0.875 0,0 1,6 20.125L6,4.5zM8.5,3.75a0.75,0.75 0,0 0,-0.75 0.75v14.103l4.31,-2.51a0.875,0.875 0,0 1,0.88 0l4.31,2.51L17.25,4.5a0.75,0.75 0,0 0,-0.75 -0.75h-8z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_save_file_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_save_file_24.xml
new file mode 100644
index 0000000000..e9c274bfc8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_save_file_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2,4.5A2.5,2.5 0,0 1,4.5 2h7c0.232,0 0.455,0.092 0.619,0.256l4.625,4.625A0.875,0.875 0,0 1,17 7.5V11h-1.75V8.5H11.5a1,1 0,0 1,-1 -1V3.75h-6a0.75,0.75 0,0 0,-0.75 0.75v14c0,0.414 0.336,0.75 0.75,0.75H13V21H4.5A2.5,2.5 0,0 1,2 18.5v-14zM19,13h-1.75v4h-1.612a0.756,0.756 0,0 0,-0.535 1.291l2.487,2.487a0.757,0.757 0,0 0,1.07 0l2.487,-2.487A0.756,0.756 0,0 0,20.612 17H19v-4z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_search_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_search_24.xml
new file mode 100644
index 0000000000..370db05187
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_search_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10 3a7 7 0 1 0 4.292 12.53L19.763 21l1.238-1.238-5.471-5.47A7 7 0 0 0 10 3zm-5.25 7a5.25 5.25 0 1 1 10.5 0 5.25 5.25 0 0 1-10.5 0z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_select_all.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_select_all.xml
new file mode 100644
index 0000000000..1445bed499
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_select_all.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M15.222,22L5.778,22A3.778,3.778 0,0 1,2 18.222L2,8.778A3.778,3.778 0,0 1,5.778 5h9.444A3.778,3.778 0,0 1,19 8.778v9.444A3.778,3.778 0,0 1,15.222 22zM5.857,7A1.857,1.857 0,0 0,4 8.857v9.286C4,19.169 4.831,20 5.857,20h9.286A1.857,1.857 0,0 0,17 18.143L17,8.857A1.857,1.857 0,0 0,15.143 7L5.857,7zM6,4a2,2 0,0 1,2 -2h7.987a6,6 0,0 1,6 6v0.016l-0.024,7.987a2,2 0,0 1,-2.006 1.994l0.03,-9.986L19.987,8a4,4 0,0 0,-4 -4L6,4zM14.284,9.752a1,1 0,0 1,1.432 1.396l-5.95,6.1a1,1 0,0 1,-1.429 0.003l-3.05,-3.1a1,1 0,0 1,1.426 -1.402l2.334,2.372 5.237,-5.37z"
+ android:fillColor="@color/mozac_ui_icons_fill" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_settings_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_settings_24.xml
new file mode 100644
index 0000000000..ce46e1f161
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_settings_24.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.25 12c0-2.07 1.68-3.75 3.75-3.75 2.07 0 3.75 1.68 3.75 3.75 0 2.07-1.68 3.75-3.75 3.75-2.07 0-3.75-1.68-3.75-3.75zM14 12c0-1.1-0.9-2-2-2s-2 0.9-2 2 0.9 2 2 2 2-0.9 2-2z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M9.19 20.46c0.26 0.91 1.1 1.54 2.05 1.54h1.52l0.05 0.02c0.94 0 1.78-0.63 2.05-1.53l0.51-1.72c0.28-0.15 0.57-0.32 0.87-0.53l1.73 0.43c0.92 0.24 1.89-0.18 2.36-1l0.76-1.31c0.47-0.82 0.35-1.86-0.3-2.54l-1.25-1.32c0.02-0.16 0.03-0.32 0.03-0.49 0-0.18-0.02-0.35-0.04-0.52l1.23-1.28c0.65-0.68 0.78-1.73 0.31-2.55l-0.76-1.31a2.157 2.157 0 0 0-2.35-1.01l-1.77 0.42c-0.29-0.2-0.57-0.37-0.85-0.51l-0.51-1.72C14.56 2.63 13.72 2 12.78 2h-1.51c-0.95 0-1.79 0.63-2.05 1.54L8.73 5.23C8.44 5.37 8.15 5.55 7.85 5.75L6.08 5.33C5.16 5.11 4.2 5.53 3.73 6.34L2.97 7.65C2.49 8.47 2.62 9.52 3.28 10.2l1.23 1.28C4.48 11.65 4.47 11.82 4.47 12c0 0.17 0.01 0.33 0.03 0.49l-1.25 1.32c-0.65 0.68-0.77 1.73-0.3 2.54l0.76 1.31c0.47 0.82 1.44 1.23 2.36 1l1.73-0.43c0.3 0.21 0.6 0.39 0.9 0.54l0.49 1.69zm-0.67-3.88a0.85 0.85 0 0 0-0.53-0.18L8 16.42c-0.07 0-0.14 0.01-0.21 0.03l-2.13 0.53a0.376 0.376 0 0 1-0.42-0.18l-0.76-1.31a0.376 0.376 0 0 1 0.05-0.46l1.54-1.62A0.88 0.88 0 0 0 6.3 12.7l-0.03-0.22-0.003-0.027A3.501 3.501 0 0 1 6.23 12.01c0-0.127 0.017-0.255 0.034-0.383L6.28 11.5l0.03-0.2a0.897 0.897 0 0 0-0.24-0.72L4.54 9a0.395 0.395 0 0 1-0.06-0.46l0.76-1.31a0.37 0.37 0 0 1 0.42-0.18l2.16 0.52c0.25 0.06 0.52 0.01 0.73-0.15 0.43-0.33 0.83-0.56 1.23-0.73 0.24-0.11 0.43-0.31 0.5-0.57l0.6-2.08c0.05-0.17 0.2-0.28 0.37-0.28h1.51c0.17 0 0.32 0.11 0.37 0.27l0.62 2.1c0.08 0.25 0.26 0.46 0.5 0.56 0.39 0.16 0.77 0.39 1.21 0.72 0.21 0.15 0.48 0.21 0.73 0.15l2.16-0.52c0.17-0.04 0.34 0.03 0.42 0.18l0.76 1.31c0.08 0.15 0.06 0.34-0.06 0.46l-1.53 1.58c-0.19 0.19-0.27 0.46-0.24 0.72l0.03 0.2 0.017 0.13c0.017 0.124 0.033 0.246 0.033 0.38 0 0.16-0.02 0.32-0.04 0.47l-0.03 0.22c-0.04 0.26 0.05 0.52 0.23 0.71l1.54 1.62c0.11 0.13 0.14 0.31 0.05 0.46l-0.76 1.31c-0.08 0.15-0.26 0.22-0.42 0.18l-2.13-0.53a0.852 0.852 0 0 0-0.74 0.15c-0.45 0.34-0.84 0.58-1.23 0.74-0.24 0.1-0.43 0.31-0.5 0.56l-0.62 2.1c-0.05 0.16-0.2 0.27-0.37 0.27h-1.52c-0.17 0-0.32-0.12-0.37-0.28l-0.6-2.08a0.84 0.84 0 0 0-0.5-0.57c-0.4-0.17-0.81-0.42-1.25-0.75z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_share_android_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_share_android_24.xml
new file mode 100644
index 0000000000..426216f346
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_share_android_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M14 6.5a3.5 3.5 0 1 1 0.947 2.395l-5.052 2.251a3.507 3.507 0 0 1-0.003 1.72l5.047 2.248a3.5 3.5 0 1 1-0.837 1.543l-5.058-2.253a3.5 3.5 0 1 1 0.009-4.798l5.052-2.252A3.505 3.505 0 0 1 14 6.5zm3.5-1.75a1.75 1.75 0 1 0 0 3.5 1.75 1.75 0 0 0 0-3.5zm-11 5.5a1.75 1.75 0 1 0 0 3.5 1.75 1.75 0 0 0 0-3.5zm9.25 7.25a1.75 1.75 0 1 1 3.5 0 1.75 1.75 0 0 1-3.5 0z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_share_apple_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_share_apple_24.xml
new file mode 100644
index 0000000000..993051d08a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_share_apple_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 3c0.232 0 0.455 0.092 0.619 0.256l5.38 5.382-1.237 1.237-3.887-3.887V18h-1.75V5.987L7.237 9.875 6 8.638l5.381-5.382A0.875 0.875 0 0 1 12 3z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5 17v2.5c0 0.414 0.336 0.75 0.75 0.75h12.5A0.75 0.75 0 0 0 19 19.5V17h1.75v2.5a2.5 2.5 0 0 1-2.5 2.5H5.75a2.5 2.5 0 0 1-2.5-2.5V17H5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shield_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shield_24.xml
new file mode 100644
index 0000000000..47b961d4d1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shield_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 22a3 3 0 0 1-1.46-0.38c-2.08-1.16-3.99-2.96-5.39-5.08-0.71-1.08-1.21-2.38-1.49-3.89L2.95 8.8C2.71 7.52 3.33 6.24 4.47 5.62l6.02-3.26a3 3 0 0 1 2.85 0l6.17 3.32c1.15 0.62 1.77 1.9 1.53 3.19l-0.71 3.79c-0.28 1.5-0.78 2.8-1.48 3.87-1.4 2.12-3.31 3.92-5.39 5.09C13.01 21.87 12.5 22 12 22zM11.92 3.75c-0.2 0-0.41 0.05-0.59 0.15L5.31 7.16C4.83 7.42 4.58 7.95 4.68 8.49l0.71 3.85c0.23 1.27 0.65 2.36 1.23 3.24 1.24 1.88 2.94 3.49 4.78 4.52 0.38 0.21 0.84 0.21 1.22 0 1.84-1.03 3.54-2.63 4.78-4.52 0.58-0.88 0.99-1.96 1.22-3.23l0.71-3.79c0.1-0.54-0.16-1.07-0.64-1.33l-6.17-3.32c-0.19-0.1-0.39-0.15-0.59-0.15l-0.01-0.01z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shield_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shield_slash_24.xml
new file mode 100644
index 0000000000..9d2077dcad
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shield_slash_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.692 5.327L11.328 3.9a1.26 1.26 0 0 1 1.187-0.001l6.174 3.322c0.48 0.259 0.736 0.792 0.637 1.328l-0.705 3.793a9.696 9.696 0 0 1-0.688 2.224l1.307 1.307c0.503-0.934 0.878-2.006 1.102-3.211l0.705-3.794a2.988 2.988 0 0 0-1.528-3.188l-6.174-3.322a2.993 2.993 0 0 0-2.849 0.003L7.401 4.035l1.291 1.292z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M18.409 17.171l3.59 3.591L20.763 22l-3.465-3.466c-1.134 1.248-2.445 2.31-3.831 3.085a3.005 3.005 0 0 1-1.464 0.378 3.005 3.005 0 0 1-1.464-0.378c-2.08-1.161-3.992-2.965-5.388-5.079-0.71-1.075-1.21-2.384-1.487-3.888L2.954 8.8a2.991 2.991 0 0 1 1.46-3.148L2 3.238 3.238 2 6.02 4.783 6.025 4.78l1.29 1.291-0.004 0.003 9.834 9.833 0.003-0.004 1.264 1.264-0.003 0.004zm-2.358 0.117L5.704 6.943 5.308 7.157a1.248 1.248 0 0 0-0.635 1.326l0.709 3.852c0.235 1.272 0.647 2.362 1.227 3.24 1.244 1.886 2.942 3.49 4.78 4.517a1.27 1.27 0 0 0 1.222 0c1.248-0.698 2.423-1.666 3.438-2.804z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shipping_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shipping_24.xml
new file mode 100644
index 0000000000..67ac9492d1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shipping_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M15.227 18.75A2.996 2.996 0 0 0 17.664 20a2.996 2.996 0 0 0 2.437-1.25h0.275c0.897 0 1.625-0.728 1.625-1.625V11.7a0.875 0.875 0 0 0-0.117-0.437l-2.203-3.825A0.875 0.875 0 0 0 18.923 7h-3.23a2.501 2.501 0 0 0-2.486-2.233H4.5a2.5 2.5 0 0 0-2.5 2.5v8.983c0 1.151 0.778 2.12 1.837 2.411A2.997 2.997 0 0 0 6.336 20a2.996 2.996 0 0 0 2.437-1.25h6.454zM4.5 6.517a0.75 0.75 0 0 0-0.75 0.75v8.211A3 3 0 0 1 9.336 17h4.621V7.267a0.75 0.75 0 0 0-0.75-0.75H4.5zM15.707 12h4.544v-0.066L18.417 8.75h-2.71V12zM5.086 17a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0zm11.328 0a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shopping_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shopping_24.xml
new file mode 100644
index 0000000000..662fb38991
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_shopping_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M13.452 2H6v1.75h7.088l6.629 6.656 1.24-1.234-6.885-6.914A0.875 0.875 0 0 0 13.452 2zM8.933 12.379l1.414-1.415L8.933 9.55l-1.415 1.414 1.415 1.415z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M3.5 6.375C3.5 5.892 3.892 5.5 4.375 5.5h7.265c0.232 0 0.455 0.092 0.619 0.256l6.735 6.735a2.5 2.5 0 0 1 0 3.536l-5.215 5.215a2.5 2.5 0 0 1-3.536 0l-6.487-6.487A0.875 0.875 0 0 1 3.5 14.136V6.375zM5.25 7.25v6.524l6.23 6.23a0.75 0.75 0 0 0 1.061 0l5.215-5.215a0.75 0.75 0 0 0 0-1.06L11.278 7.25H5.25z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_social_tracker_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_social_tracker_24.xml
new file mode 100644
index 0000000000..8ed2638b8a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_social_tracker_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M5.25 16h-2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1zm13.686 1a2.5 2.5 0 0 0 2.372-3.291l-2.083-6.25a2.5 2.5 0 0 0-2.372-1.709H8v9.164l2.009 3.064c0.118 0.18 0.213 0.375 0.281 0.58l0.951 2.852c0.117 0.352 0.447 0.59 0.818 0.59h0.003a1.874 1.874 0 0 0 1.875-1.875V17h4.999z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sparkle_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sparkle_24.xml
new file mode 100644
index 0000000000..71ae38b99e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sparkle_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.837 17.376l0.232-1.08c0.11-0.512 0.84-0.512 0.95 0l0.232 1.08c0.04 0.187 0.186 0.333 0.373 0.373l1.08 0.232c0.512 0.11 0.512 0.84 0 0.95l-1.08 0.232a0.485 0.485 0 0 0-0.373 0.373l-0.232 1.08c-0.11 0.512-0.84 0.512-0.95 0l-0.232-1.08a0.485 0.485 0 0 0-0.373-0.373l-1.08-0.232c-0.512-0.11-0.512-0.84 0-0.95l1.08-0.232a0.485 0.485 0 0 0 0.373-0.373zm0-12.912l0.232-1.08c0.11-0.512 0.84-0.512 0.95 0l0.232 1.08c0.04 0.187 0.186 0.333 0.373 0.373l1.08 0.232c0.512 0.11 0.512 0.84 0 0.95l-1.08 0.232a0.485 0.485 0 0 0-0.373 0.373l-0.232 1.08c-0.11 0.512-0.84 0.512-0.95 0l-0.232-1.08a0.485 0.485 0 0 0-0.373-0.373l-1.08-0.232c-0.512-0.11-0.512-0.84 0-0.95l1.08-0.232a0.485 0.485 0 0 0 0.373-0.373z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M14.16 6.981l-1.034 2.623a2.707 2.707 0 0 1-1.52 1.522L8.98 12.161a0.365 0.365 0 0 0 0 0.679l2.623 1.034a2.703 2.703 0 0 1 1.522 1.522l1.034 2.623a0.365 0.365 0 0 0 0.679 0l1.034-2.623a2.703 2.703 0 0 1 1.522-1.522l2.622-1.034a0.366 0.366 0 0 0 0-0.68l-2.622-1.034a2.703 2.703 0 0 1-1.522-1.522L14.84 6.981a0.365 0.365 0 0 0-0.679 0zm-1.628-0.642c0.704-1.786 3.23-1.786 3.935 0l1.034 2.623c0.097 0.245 0.29 0.44 0.536 0.536l2.622 1.034c1.786 0.704 1.786 3.232 0 3.936l-2.622 1.034a0.952 0.952 0 0 0-0.536 0.536l-1.034 2.623c-0.704 1.785-3.23 1.786-3.935 0l-1.034-2.623a0.952 0.952 0 0 0-0.536-0.536L8.34 14.468c-1.785-0.704-1.786-3.23 0-3.935L10.96 9.5a0.959 0.959 0 0 0 0.538-0.538l1.034-2.623z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_fill_20.xml
new file mode 100644
index 0000000000..a8c7f438ce
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_fill_20.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6.741 16a0.982 0.982 0 0 1-0.579-0.189 0.98 0.98 0 0 1-0.375-1.04l0.798-3.132-2.487-2.066a0.979 0.979 0 0 1-0.307-1.062 0.98 0.98 0 0 1 0.873-0.679L7.89 7.623 9.086 4.62A0.978 0.978 0 0 1 10 4a0.98 0.98 0 0 1 0.915 0.621l1.196 3.003 3.226 0.209a0.979 0.979 0 0 1 0.873 0.679 0.978 0.978 0 0 1-0.309 1.061l-2.486 2.064 0.798 3.133a0.981 0.981 0 0 1-0.375 1.04 0.98 0.98 0 0 1-1.105 0.036L10 14.119l-2.732 1.727A0.984 0.984 0 0 1 6.741 16z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_half_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_half_fill_20.xml
new file mode 100644
index 0000000000..fa35cd5990
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_half_fill_20.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10 14.119l-2.732 1.727a0.984 0.984 0 0 1-1.481-1.075l0.798-3.132-2.487-2.066a0.979 0.979 0 0 1-0.307-1.062 0.98 0.98 0 0 1 0.873-0.679L7.89 7.623 9.086 4.62A0.978 0.978 0 0 1 10 4v10.119z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_one_half_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_one_half_fill_20.xml
new file mode 100644
index 0000000000..c3871dcb8f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_star_one_half_fill_20.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="@color/mozac_ic_star_unfilled"
+ android:pathData="M6.741 16a0.982 0.982 0 0 1-0.579-0.189 0.98 0.98 0 0 1-0.375-1.04l0.798-3.132-2.487-2.066a0.979 0.979 0 0 1-0.307-1.062 0.98 0.98 0 0 1 0.873-0.679L7.89 7.623 9.086 4.62A0.978 0.978 0 0 1 10 4a0.98 0.98 0 0 1 0.915 0.621l1.196 3.003 3.226 0.209a0.979 0.979 0 0 1 0.873 0.679 0.978 0.978 0 0 1-0.309 1.061l-2.486 2.064 0.798 3.133a0.981 0.981 0 0 1-0.375 1.04 0.98 0.98 0 0 1-1.105 0.036L10 14.119l-2.732 1.727A0.984 0.984 0 0 1 6.741 16z" />
+ <path
+ android:fillColor="@color/mozac_ic_star_filled"
+ android:pathData="M10 14.119l-2.732 1.727a0.984 0.984 0 0 1-1.481-1.075l0.798-3.132-2.487-2.066a0.979 0.979 0 0 1-0.307-1.062 0.98 0.98 0 0 1 0.873-0.679L7.89 7.623 9.086 4.62A0.978 0.978 0 0 1 10 4v10.119z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_stop.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_stop.xml
new file mode 100644
index 0000000000..cbc5269212
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_stop.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M10.367,12.573 L4.22,18.72a0.75,0.75 0,1 0,1.06 1.06l6.156,-6.156h1.127l6.156,6.156a0.748,0.748 0,0 0,1.06 0,0.75 0.75,0 0,0 0,-1.061l-6.147,-6.147 0.001,-1.146L19.78,5.28a0.75,0.75 0,1 0,-1.061 -1.061l-6.156,6.156h-1.128L5.28,4.22a0.75,0.75 0,1 0,-1.061 1.061l6.146,6.146 0.002,1.146z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_storage_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_storage_24.xml
new file mode 100644
index 0000000000..0b0408778e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_storage_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.5 7.75A0.75 0.75 0 0 0 3.75 8.5v7c0 0.414 0.336 0.75 0.75 0.75h15a0.75 0.75 0 0 0 0.75-0.75v-7a0.75 0.75 0 0 0-0.75-0.75h-15zM2 8.5A2.5 2.5 0 0 1 4.5 6h15A2.5 2.5 0 0 1 22 8.5v7a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 15.5v-7z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.5 10.75H6v2.5h2.5v-2.5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_storage_slash_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_storage_slash_24.xml
new file mode 100644
index 0000000000..6efcb91aef
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_storage_slash_24.xml
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6 11h2.25v2.25H6V11z"
+ tools:ignore="VectorRaster" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M22 20.752L3.238 1.99 2 3.228 4.773 6H4.5A2.502 2.502 0 0 0 2 8.5v7C2 16.879 3.121 18 4.5 18h12.273l3.99 3.99L22 20.752zm-6.977-4.502l-8.5-8.5H4.5A0.752 0.752 0 0 0 3.75 8.5v7c0 0.413 0.337 0.75 0.75 0.75h10.523z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.109 7.75H19.5c0.413 0 0.75 0.337 0.75 0.75v7a0.744 0.744 0 0 1-0.659 0.732l1.325 1.325A2.497 2.497 0 0 0 22 15.5v-7C22 7.121 20.879 6 19.5 6H9.359l1.75 1.75z"
+ tools:ignore="VectorRaster" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sync_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sync_24.xml
new file mode 100644
index 0000000000..8b4e07514e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sync_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M4.75 12.03c0-3.998 3.252-7.25 7.25-7.25 0.028 0 0.055 0.006 0.083 0.006v2.163c0 0.49 0.592 0.735 0.939 0.389l2.949-2.949a0.55 0.55 0 0 0 0-0.778l-2.949-2.949a0.55 0.55 0 0 0-0.939 0.389v1.985c-0.028 0-0.055-0.006-0.083-0.006-4.963 0-9 4.037-9 9a8.983 8.983 0 0 0 3.236 6.904L7.48 17.69a7.239 7.239 0 0 1-2.73-5.66zm12.977-6.937l-1.248 1.248a7.237 7.237 0 0 1 2.771 5.69c0 3.998-3.252 7.25-7.25 7.25-0.028 0-0.055-0.006-0.083-0.007v-2.223a0.55 0.55 0 0 0-0.939-0.389l-2.949 2.949a0.55 0.55 0 0 0 0 0.778l2.949 2.949a0.55 0.55 0 0 0 0.939-0.389v-1.922c0.028 0 0.055 0.003 0.083 0.003 4.963 0 9-4.037 9-9a8.984 8.984 0 0 0-3.273-6.937z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sync_tabs_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sync_tabs_24.xml
new file mode 100644
index 0000000000..5e776b729e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_sync_tabs_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M0 4.75a2.5 2.5 0 0 1 2.5-2.5h8a2.5 2.5 0 0 1 2.5 2.5v4.5a2.5 2.5 0 0 1-2.5 2.5h-8A2.5 2.5 0 0 1 0 9.25v-4.5zM2.5 4a0.75 0.75 0 0 0-0.75 0.75v4.5C1.75 9.664 2.086 10 2.5 10h7.75V4H2.5z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M20.275 7.75H14.75V6h5.525a2.5 2.5 0 0 1 2.5 2.5v9.75c0 0.818-0.393 1.544-1 2H24V22H2v-1.75h3.25a2.497 2.497 0 0 1-1-2V13.5H6v4.75C6 18.664 6.336 19 6.75 19h13.525a0.75 0.75 0 0 0 0.75-0.75V8.5a0.75 0.75 0 0 0-0.75-0.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab.xml
new file mode 100644
index 0000000000..8be248636b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="16"
+ android:viewportWidth="16">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M15,11h-1V5a2,2 0,0 0,-2 -2H4a2,2 0,0 0,-2 2v6H1a1,1 0,0 0,0 2h14a1,1 0,1 0,0 -2z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_badge_fill_20.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_badge_fill_20.xml
new file mode 100644
index 0000000000..f99ada90d9
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_badge_fill_20.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:fillColor="#fff"
+ android:pathData="M14.75 5c0.966 0 1.75 0.784 1.75 1.75v6.5A1.75 1.75 0 0 1 14.75 15h-9.5a1.75 1.75 0 0 1-1.75-1.75v-6.5C3.5 5.784 4.284 5 5.25 5h9.5zm0-1.5h-9.5A3.254 3.254 0 0 0 2 6.75v6.5a3.254 3.254 0 0 0 3.25 3.25h9.5A3.254 3.254 0 0 0 18 13.25v-6.5a3.254 3.254 0 0 0-3.25-3.25z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M14.75 5h-9.5A1.75 1.75 0 0 0 3.5 6.75v6.5C3.5 14.216 4.284 15 5.25 15h9.5a1.75 1.75 0 0 0 1.75-1.75v-6.5A1.75 1.75 0 0 0 14.75 5z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_new.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_new.xml
new file mode 100644
index 0000000000..8822eb5979
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_new.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="16"
+ android:viewportWidth="16">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11,11L11,9a1,1 0,0 1,1 -1h1a1,1 0,0 1,1 1L14,5a2,2 0,0 0,-2 -2L4,3a2,2 0,0 0,-2 2v6L1,11a1,1 0,0 0,0 2h7v-1a1,1 0,0 1,1 -1zM15.5,12L13,12L13,9.5a0.5,0.5 0,0 0,-1 0L12,12L9.5,12a0.5,0.5 0,0 0,0 1L12,13v2.5a0.5,0.5 0,0 0,1 0L13,13h2.5a0.5,0.5 0,0 0,0 -1z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_number_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_number_24.xml
new file mode 100644
index 0000000000..30b357c61c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_number_24.xml
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 6.5A2.5 2.5 0 0 1 4.5 4h15A2.5 2.5 0 0 1 22 6.5v10a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 16.5v-10zm2.5-0.75A0.75 0.75 0 0 0 3.75 6.5v10c0 0.414 0.336 0.75 0.75 0.75h15a0.75 0.75 0 0 0 0.75-0.75v-10a0.75 0.75 0 0 0-0.75-0.75h-15z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M8.641 15.1c1.64 0 2.82-0.878 2.823-2.057-0.003-0.909-0.703-1.651-1.576-1.797v-0.05a1.633 1.633 0 0 0 1.306-1.611c-0.003-1.126-1.083-1.957-2.553-1.957-1.473 0-2.553 0.83-2.55 1.957a1.615 1.615 0 0 0 1.307 1.612v0.05c-0.887 0.145-1.58 0.887-1.576 1.796C5.818 14.223 7 15.1 8.642 15.1zm0-1.13c-0.731 0-1.221-0.433-1.221-1.047 0-0.629 0.515-1.084 1.221-1.084 0.703 0 1.222 0.455 1.222 1.084 0 0.617-0.494 1.047-1.222 1.047zm0-3.25c-0.614 0-1.05-0.4-1.05-0.976 0-0.568 0.429-0.958 1.05-0.958 0.618 0 1.051 0.394 1.051 0.958 0 0.576-0.44 0.977-1.05 0.977zm6.712 4.38c1.64 0 2.82-0.878 2.823-2.057-0.004-0.909-0.703-1.651-1.577-1.797v-0.05a1.633 1.633 0 0 0 1.307-1.611c-0.004-1.126-1.083-1.957-2.553-1.957-1.474 0-2.554 0.83-2.55 1.957a1.615 1.615 0 0 0 1.307 1.612v0.05c-0.888 0.145-1.58 0.887-1.577 1.796-0.004 1.18 1.179 2.056 2.82 2.056zm0-1.13c-0.732 0-1.222-0.433-1.222-1.047 0-0.629 0.515-1.084 1.222-1.084 0.703 0 1.221 0.455 1.221 1.084 0 0.617-0.494 1.047-1.221 1.047zm0-3.25c-0.615 0-1.052-0.4-1.052-0.976 0-0.568 0.43-0.958 1.052-0.958 0.617 0 1.05 0.394 1.05 0.958 0 0.576-0.44 0.977-1.05 0.977z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_tray_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_tray_24.xml
new file mode 100644
index 0000000000..43e6db826f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tab_tray_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M2 4.5A2.5 2.5 0 0 1 4.5 2h15A2.5 2.5 0 0 1 22 4.5v10a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 14.5v-10zm2.5-0.75A0.75 0.75 0 0 0 3.75 4.5v10c0 0.414 0.336 0.75 0.75 0.75h15a0.75 0.75 0 0 0 0.75-0.75v-10a0.75 0.75 0 0 0-0.75-0.75h-15zm0 17.75A2.5 2.5 0 0 1 2 19h1.75c0 0.414 0.336 0.75 0.75 0.75h15A0.75 0.75 0 0 0 20.25 19H22a2.5 2.5 0 0 1-2.5 2.5h-15z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_themes_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_themes_24.xml
new file mode 100644
index 0000000000..180523e4c2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_themes_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M18 10V3.5C18 3.22 17.78 3 17.5 3h-11C6.22 3 6 3.22 6 3.5V10H5c-0.28 0-0.5 0.22-0.5 0.5v3A2.5 2.5 0 0 0 7 16h3v4.5c0 0.83 0.67 1.5 1.5 1.5h1c0.83 0 1.5-0.67 1.5-1.5V16h3a2.5 2.5 0 0 0 2.5-2.5v-3c0-0.28-0.22-0.5-0.5-0.5h-1zM7.75 10h8.51V5.37h-0.57l-1.22 1.14c-0.37 0.35-0.94 0.34-1.3-0.02l-1.12-1.12h-0.83l-0.77 0.64c-0.39 0.33-0.96 0.28-1.3-0.09L8.09 4.75H7.75V10z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tool_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tool_24.xml
new file mode 100644
index 0000000000..78cea3d700
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tool_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M13.11,22h-2.22a2.68,2.68 0,0 1,-2.68 -2.68v-6.14C6.08,11.67 5.05,9.11 5.58,6.5c0.37,-1.85 1.55,-3.44 3.23,-4.38l0.43,-0.21a0.879,0.879 0,0 1,1.26 0.77l0.05,4.07c0,0.5 0.42,0.91 0.93,0.91h1.07c0.51,0 0.93,-0.42 0.93,-0.93V2.7c0,-0.3 0.16,-0.58 0.41,-0.74a0.86,0.86 0,0 1,0.85 -0.04l0.41,0.2c2.12,1.18 3.41,3.37 3.41,5.74 0,2.14 -1.03,4.1 -2.77,5.33v6.14c0,1.48 -1.2,2.68 -2.68,2.68V22zM8.77,4.29c-0.74,0.67 -1.28,1.58 -1.48,2.56 -0.41,2.04 0.47,4.04 2.25,5.11 0.26,0.16 0.42,0.44 0.42,0.75v6.61c0,0.51 0.42,0.93 0.93,0.93h2.22c0.51,0 0.93,-0.42 0.93,-0.93v-6.61c0,-0.31 0.16,-0.59 0.42,-0.75 1.47,-0.88 2.35,-2.42 2.35,-4.11 0,-1.36 -0.58,-2.65 -1.58,-3.55v2.44c0,1.48 -1.2,2.68 -2.68,2.68h-1.07c-1.46,0 -2.66,-1.18 -2.68,-2.64l-0.03,-2.49z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_translate_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_translate_24.xml
new file mode 100644
index 0000000000..7fd0e1d3a1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_translate_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M20.144 22H22l-4.716-11.383A1 1 0 0 0 16.36 10h-0.846c-0.404 0-0.769 0.244-0.924 0.617l-2.352 5.677c-0.677-0.35-1.327-0.761-1.909-1.275a16.962 16.962 0 0 1-1.374-1.36 18.85 18.85 0 0 0 3.773-6.66h0.983V5.25H8.71V3H7.002v2.25H2V7h8.931a17.079 17.079 0 0 1-3.07 5.297A17.047 17.047 0 0 1 5.859 8.75H4.033a18.988 18.988 0 0 0 2.621 4.811c-0.16 0.153-0.315 0.311-0.482 0.458a9.258 9.258 0 0 1-3.195 1.83v1.826a10.96 10.96 0 0 0 4.31-2.33c0.163-0.144 0.313-0.298 0.471-0.446 0.459 0.505 0.941 0.992 1.458 1.447a10.995 10.995 0 0 0 2.347 1.583L9.875 22h1.856l1.346-3.25h5.721L20.144 22zm-6.342-5l2.135-5.155L18.073 17h-4.271z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tree.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tree.xml
new file mode 100644
index 0000000000..305131d365
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_tree.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M0.7,18c0,4.9,3.6,8.8,8.1,9.5v4.3c0.2,0,3.2,0,3.2,0v-4.3c1.8-0.4,3.6-1.1,4.9-2.5c0.2-0.2,0.2-0.2,0.2-0.5
+ c-0.2-0.4-0.2-1.1-0.2-1.6c0-2,0.2-4.9,1.6-7.9c0,0,0.9-1.6,0.7-1.8C18,7.2,14.4,0,10.4,0C5,0,0.7,12.6,0.7,18z M18.3,22.8
+ c0,3.1,2.2,5.6,4.9,6.3V32h3.2v-2.9c2.7-0.7,4.9-3.2,4.9-6.3c0-3.6-2.9-12.9-6.5-12.9S18.3,19.2,18.3,22.8z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_update_circle_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_update_circle_24.xml
new file mode 100644
index 0000000000..872902f4c3
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_update_circle_24.xml
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.386 7.25a0.875 0.875 0 0 1 1.228 0L17 11.567l-1.227 1.247-2.898-2.851V17h-1.75V9.963l-2.898 2.851L7 11.567l4.386-4.317z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zM3.75 12a8.25 8.25 0 1 1 16.5 0 8.25 8.25 0 0 1-16.5 0z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_vacation.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_vacation.xml
new file mode 100644
index 0000000000..07a6924ee1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_vacation.xml
@@ -0,0 +1,26 @@
+<?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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="32"
+ android:viewportWidth="32">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M3.6,27l-2.5-1.8L0.8,25c-0.7-0.4-0.7-1.2-0.4-2c0.4-0.5,1.1-0.7,1.6-0.5l3.6,1.2c0-0.4,0.2-0.9,0.4-1.4
+ c0.2-0.7,0.5-1.6,1.1-2.3c0.2-0.4,0.5-0.7,0.7-1.2c0.2-0.4,0.5-0.9,0.9-1.2c0.4-0.9,1.1-1.6,1.8-2.5c0.7-0.9,1.4-1.6,2.3-2.5
+ c0.4-0.4,0.9-0.7,1.2-1.2c0.4-0.2,0.9-0.7,1.2-1.1c0.2-0.2,0.2-0.2,0.4-0.4L3.1,7.3c-0.2,0-0.2,0-0.4,0l-2,0.9
+ C0.2,8.3-0.3,7.6,0.2,7.1l2-2C2.4,5,2.4,5,2.5,5h17.9c0.5-0.5,1.2-1.1,1.8-1.6c0.7-0.7,1.4-1.2,2.1-1.8c0.4-0.4,0.7-0.5,1.1-0.7
+ c0.4-0.2,0.7-0.4,1.1-0.5c0.5,0,0.9-0.2,1.2-0.2s0.7,0,1.1,0s0.7,0,1.1,0c0.4,0,0.5,0,0.7,0.2c0.5,0,0.7,0.2,0.7,0.2
+ c0.2,0,0.2,0.2,0.4,0.4c0,0,0,0.4,0.2,0.7c0,0.2,0,0.5,0.2,0.7c0,0.4,0,0.7,0,1.1s0,0.7,0,1.1c0,0.4,0,0.9-0.2,1.2
+ c-0.2,0.4-0.4,0.7-0.5,1.1c-0.2,0.4-0.5,0.7-0.7,1.1c-0.5,0.7-1.1,1.4-1.8,2.1c-0.4,0.4-0.7,0.7-1.1,1.1v17.8c0,0.2,0,0.4-0.2,0.4
+ l-2,2c-0.5,0.5-1.2,0-1.1-0.5l0.7-2c0-0.2,0-0.2,0-0.4L22.8,16c-0.4,0.4-0.7,0.7-0.9,0.9c-0.4,0.4-0.7,0.9-1.2,1.2
+ c-0.4,0.4-0.7,0.9-1.2,1.2c-0.7,0.9-1.6,1.6-2.5,2.3c-0.9,0.7-1.6,1.4-2.5,2c-0.4,0.4-0.9,0.5-1.2,0.9c-0.4,0.2-0.7,0.5-1.2,0.7
+ c-0.7,0.4-1.6,0.7-2.3,1.1c-0.4,0.2-0.7,0.2-1.2,0.4L9.6,30c0.4,0.7,0,1.4-0.7,1.8c-0.5,0.2-1.2,0-1.6-0.5l-0.2-0.4l-1.8-2.3
+ c-0.2,0-0.2,0-0.4,0.2c-0.4,0-0.5,0.2-0.7,0.2s-0.2,0-0.4,0c-0.2,0-0.2,0-0.4,0c-0.2,0-0.4,0-0.5,0c-0.2,0-0.2,0-0.2,0s0,0,0-0.2
+ c0-0.2,0-0.2,0-0.5c0-0.2,0-0.2,0-0.4c0-0.2,0-0.2,0-0.4C3.4,27.5,3.4,27.3,3.6,27L3.6,27z M5.7,28.4L5.7,28.4L5.7,28.4L5.7,28.4z"
+ tools:ignore="VectorPath" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_warning_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_warning_24.xml
new file mode 100644
index 0000000000..fcb74e05dc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_warning_24.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M12.875 15.625h-1.75v1.75h1.75v-1.75z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.125 14V9h1.75v5h-1.75z" />
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.956 4.92c0.165 0 0.47 0.05 0.655 0.385l7.04 12.66a0.729 0.729 0 0 1-0.009 0.745 0.726 0.726 0 0 1-0.646 0.37H5.004a0.727 0.727 0 0 1-0.645-0.368 0.728 0.728 0 0 1-0.012-0.743L11.3 5.309a0.725 0.725 0 0 1 0.657-0.389zm0-1.75c-0.859 0-1.717 0.433-2.19 1.297l-6.953 12.66c-0.915 1.666 0.29 3.703 2.191 3.703h13.993c1.907 0 3.112-2.049 2.185-3.715l-7.04-12.66a2.478 2.478 0 0 0-2.186-1.285z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_warning_fill_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_warning_fill_24.xml
new file mode 100644
index 0000000000..b49799f930
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_warning_fill_24.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M11.956 3.336c-0.867 0-1.733 0.437-2.212 1.309L2.815 17.262C1.892 18.944 3.108 21 5.027 21h13.946c1.925 0 3.141-2.068 2.205-3.75L14.162 4.633a2.5 2.5 0 0 0-2.206-1.297zM11.125 13.5V9h1.75v4.5h-1.75zm0 1.75h1.75V17h-1.75v-1.75z" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_web_extension_default_icon.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_web_extension_default_icon.xml
new file mode 100644
index 0000000000..8aa89c3b51
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_web_extension_default_icon.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z" />
+</vector>
+
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_whats_new_24.xml b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_whats_new_24.xml
new file mode 100644
index 0000000000..e2156ea6d4
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_whats_new_24.xml
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/mozac_ui_icons_fill"
+ android:fillType="evenOdd"
+ android:pathData="M12 3.107A3.375 3.375 0 0 0 6.541 7H5.5A2.5 2.5 0 0 0 3 9.5v1c0 0.818 0.393 1.544 1 2v7A2.5 2.5 0 0 0 6.5 22h11a2.5 2.5 0 0 0 2.5-2.5v-7c0.608-0.457 1-1.183 1-2v-1A2.5 2.5 0 0 0 18.5 7h-1.041A3.375 3.375 0 0 0 12 3.107zM9.5 3.75A1.625 1.625 0 1 0 9.5 7h1.625V5.375c0-0.898-0.727-1.625-1.625-1.625zm3.375 5v2.5H18.5a0.75 0.75 0 0 0 0.75-0.75v-1a0.75 0.75 0 0 0-0.75-0.75h-5.625zM14.5 7a1.625 1.625 0 1 0-1.625-1.625V7H14.5zm-9 1.75h5.625v2.5H5.5a0.75 0.75 0 0 1-0.75-0.75v-1A0.75 0.75 0 0 1 5.5 8.75zM12.875 13h5.376l-0.001 6.5a0.75 0.75 0 0 1-0.75 0.75h-4.625V13zm-1.75 0v7.25H6.5a0.75 0.75 0 0 1-0.75-0.75V13h5.375z"
+ tools:ignore="VectorRaster"
+ tools:targetApi="n" />
+</vector>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/values/attrs.xml b/mobile/android/android-components/components/ui/icons/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000..c41244c9c1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/values/attrs.xml
@@ -0,0 +1,12 @@
+<?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>
+ <!-- Background color for mozac_ic_private_mode_circle_fill_20,
+ mozac_ic_private_mode_circle_fill_24 and mozac_ic_private_mode_circle_fill_48 -->
+ <attr name="mozac_ic_private_mode_circle_fill_background_color" format="reference" />
+ <!-- Icon color for mozac_ic_private_mode_circle_fill_20,
+ mozac_ic_private_mode_circle_fill_24 and mozac_ic_private_mode_circle_fill_48 -->
+ <attr name="mozac_ic_private_mode_circle_fill_icon_color" format="reference" />
+</resources>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/values/colors.xml b/mobile/android/android-components/components/ui/icons/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..b785281bc7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+<?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>
+ <color name="mozac_ui_icons_fill">#FFFFFF</color>
+
+ <!-- Star icon fill colors for mozac_ic_star_one_half_fill_20 -->
+ <color name="mozac_ic_star_filled">#000000</color>
+ <color name="mozac_ic_star_unfilled">#FFFFFF</color>
+
+ <!-- Private Mode mask icon circle fill colors for mozac_ic_private_mode_circle_fill_stroke_20 -->
+ <color name="mozac_ui_private_mode_circle_fill">#000000</color>
+</resources>
diff --git a/mobile/android/android-components/components/ui/icons/src/main/res/values/mozac_ui_icons_strings.xml b/mobile/android/android-components/components/ui/icons/src/main/res/values/mozac_ui_icons_strings.xml
new file mode 100644
index 0000000000..9dbb12a529
--- /dev/null
+++ b/mobile/android/android-components/components/ui/icons/src/main/res/values/mozac_ui_icons_strings.xml
@@ -0,0 +1,17 @@
+<?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>
+ <string translatable="false" name="mozac_error_shred_file">mozac_error_shred_file</string>
+ <string translatable="false" name="mozac_error_question_file">mozac_error_question_file</string>
+ <string translatable="false" name="mozac_error_surprised">mozac_error_surprised</string>
+ <string translatable="false" name="mozac_error_hourglass">mozac_error_hourglass</string>
+ <string translatable="false" name="mozac_error_eye_roll">mozac_error_eye_roll</string>
+ <string translatable="false" name="mozac_error_unplugged">mozac_error_unplugged</string>
+ <string translatable="false" name="mozac_error_lock">mozac_error_lock</string>
+ <string translatable="false" name="mozac_error_confused">mozac_error_confused</string>
+ <string translatable="false" name="mozac_error_no_internet">mozac_error_no_internet</string>
+ <string translatable="false" name="mozac_error_asleep">mozac_error_asleep</string>
+ <string translatable="false" name="mozac_error_inspect">mozac_error_inspect</string>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/tabcounter/.gitignore b/mobile/android/android-components/components/ui/tabcounter/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/mobile/android/android-components/components/ui/tabcounter/README.md b/mobile/android/android-components/components/ui/tabcounter/README.md
new file mode 100644
index 0000000000..38500615e0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/README.md
@@ -0,0 +1,37 @@
+# [Android Components](../../../README.md) > UI > Tabcounter
+
+A button that shows the current tab count and can animate state changes.
+
+## Usage
+
+Create a tab counter in XML:
+
+```xml
+<mozilla.components.ui.tabcounter.TabCounter
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ mozac:tabCounterTintColor="@color/primary" />
+```
+
+Styleable attributes can be set on your theme as well:
+
+```xml
+<style name="AppTheme">
+ ...
+ <item name="tabCounterTintColor">#FFFFFF</item>
+</style>
+```
+
+### 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:ui-tabcounter:{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/ui/tabcounter/build.gradle b/mobile/android/android-components/components/ui/tabcounter/build.gradle
new file mode 100644
index 0000000000..04e3ccc2e1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/build.gradle
@@ -0,0 +1,50 @@
+/* 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/. */
+
+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
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'mozilla.components.ui.tabcounter'
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':support-ktx')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation project(':concept-menu')
+ implementation project(':browser-menu2')
+ implementation project(':support-base')
+ implementation project(':ui-colors')
+ implementation project(':ui-icons')
+
+ testImplementation project(":support-test")
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/ui/tabcounter/proguard-rules.pro b/mobile/android/android-components/components/ui/tabcounter/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/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/ui/tabcounter/src/main/AndroidManifest.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..928c7b2243
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/AndroidManifest.xml
@@ -0,0 +1,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/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application android:supportsRtl="true" />
+
+</manifest>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/java/mozilla/components/ui/tabcounter/TabCounter.kt b/mobile/android/android-components/components/ui/tabcounter/src/main/java/mozilla/components/ui/tabcounter/TabCounter.kt
new file mode 100644
index 0000000000..2ba8aa8540
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/java/mozilla/components/ui/tabcounter/TabCounter.kt
@@ -0,0 +1,348 @@
+/* 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.ui.tabcounter
+
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Typeface
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
+import mozilla.components.support.utils.DrawableUtils
+import mozilla.components.ui.tabcounter.databinding.MozacUiTabcounterLayoutBinding
+import java.text.NumberFormat
+
+class TabCounter @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0,
+) : RelativeLayout(context, attrs, defStyle) {
+
+ private val animationSet: AnimatorSet
+ private var binding: MozacUiTabcounterLayoutBinding
+ private var counterBox: ImageView
+ private var counterText: TextView
+ private var counterRoot: FrameLayout
+ private var counterMask: ImageView
+
+ init {
+ binding = MozacUiTabcounterLayoutBinding.inflate(LayoutInflater.from(context), this)
+ counterBox = binding.counterBox
+ counterText = binding.counterText
+ counterRoot = binding.counterRoot
+ counterMask = binding.counterMask
+
+ setCount(INTERNAL_COUNT)
+
+ context.obtainStyledAttributes(attrs, R.styleable.TabCounter, defStyle, 0).apply {
+ val counterColor = getColorStateList(
+ R.styleable.TabCounter_tabCounterTintColor,
+ ) ?: ContextCompat.getColorStateList(context, R.color.mozac_ui_tabcounter_default_tint)
+
+ counterColor?.let {
+ setColor(it)
+ }
+
+ clipChildren = false
+
+ recycle()
+ }
+
+ animationSet = createAnimatorSet()
+ }
+
+ /**
+ * Sets the colors of the tab counter box and text.
+ */
+ @VisibleForTesting
+ internal fun setColor(colorStateList: ColorStateList) {
+ val tabCounterBox =
+ DrawableUtils.loadAndTintDrawable(context, R.drawable.mozac_ui_tabcounter_box, colorStateList)
+ counterBox.setImageDrawable(tabCounterBox)
+ counterText.setTextColor(colorStateList)
+ }
+
+ private fun updateContentDescription(count: Int) {
+ counterRoot.contentDescription = if (count == 1) {
+ context?.getString(R.string.mozac_tab_counter_open_tab_tray_single)
+ } else {
+ String.format(
+ context.getString(R.string.mozac_tab_counter_open_tab_tray_plural),
+ count.toString(),
+ )
+ }
+ }
+
+ fun setCountWithAnimation(count: Int) {
+ setCount(count)
+
+ // No need to animate on these cases.
+ when {
+ INTERNAL_COUNT == 0 -> return // Initial state.
+ INTERNAL_COUNT == count -> return // There isn't any tab added or removed.
+ INTERNAL_COUNT > MAX_VISIBLE_TABS -> return // There are still over MAX_VISIBLE_TABS tabs open.
+ }
+
+ // Cancel previous animations if necessary.
+ if (animationSet.isRunning) {
+ animationSet.cancel()
+ }
+ // Trigger animations.
+ animationSet.start()
+ }
+
+ /**
+ * Toggles the visibility of the mask overlay on the counter
+ *
+ * @param showMask [Boolean] used to determine whether to show or hide the mask.
+ */
+ fun toggleCounterMask(showMask: Boolean) {
+ counterMask.isVisible = showMask
+ }
+
+ fun setCount(count: Int) {
+ updateContentDescription(count)
+ adjustTextSize(count)
+ counterText.text = formatCountForDisplay(count)
+ INTERNAL_COUNT = count
+ }
+
+ private fun createAnimatorSet(): AnimatorSet {
+ val animatorSet = AnimatorSet()
+ createBoxAnimatorSet(animatorSet)
+ createTextAnimatorSet(animatorSet)
+ return animatorSet
+ }
+
+ private fun createBoxAnimatorSet(animatorSet: AnimatorSet) {
+ // The first animator, fadeout in 33 ms (49~51, 2 frames).
+ val fadeOut = ObjectAnimator.ofFloat(
+ counterBox,
+ "alpha",
+ ANIM_BOX_FADEOUT_FROM,
+ ANIM_BOX_FADEOUT_TO,
+ ).setDuration(ANIM_BOX_FADEOUT_DURATION)
+
+ // Move up on y-axis, from 0.0 to -5.3 in 50ms, with fadeOut (49~52, 3 frames).
+ val moveUp1 = ObjectAnimator.ofFloat(
+ counterBox,
+ "translationY",
+ ANIM_BOX_MOVEUP1_TO,
+ ANIM_BOX_MOVEUP1_FROM,
+ ).setDuration(ANIM_BOX_MOVEUP1_DURATION)
+
+ // Move down on y-axis, from -5.3 to -1.0 in 116ms, after moveUp1 (52~59, 7 frames).
+ val moveDown2 = ObjectAnimator.ofFloat(
+ counterBox,
+ "translationY",
+ ANIM_BOX_MOVEDOWN2_FROM,
+ ANIM_BOX_MOVEDOWN2_TO,
+ ).setDuration(ANIM_BOX_MOVEDOWN2_DURATION)
+
+ // FadeIn in 66ms, with moveDown2 (52~56, 4 frames).
+ val fadeIn = ObjectAnimator.ofFloat(
+ counterBox,
+ "alpha",
+ ANIM_BOX_FADEIN_FROM,
+ ANIM_BOX_FADEIN_TO,
+ ).setDuration(ANIM_BOX_FADEIN_DURATION)
+
+ // Move down on y-axis, from -1.0 to 2.7 in 116ms, after moveDown2 (59~66, 7 frames).
+ val moveDown3 = ObjectAnimator.ofFloat(
+ counterBox,
+ "translationY",
+ ANIM_BOX_MOVEDOWN3_FROM,
+ ANIM_BOX_MOVEDOWN3_TO,
+ ).setDuration(ANIM_BOX_MOVEDOWN3_DURATION)
+
+ // Move up on y-axis, from 2.7 to 0 in 133ms, after moveDown3 (66~74, 8 frames).
+ val moveUp4 = ObjectAnimator.ofFloat(
+ counterBox,
+ "translationY",
+ ANIM_BOX_MOVEDOWN4_FROM,
+ ANIM_BOX_MOVEDOWN4_TO,
+ ).setDuration(ANIM_BOX_MOVEDOWN4_DURATION)
+
+ // Scale up height from 2% to 105% in 100ms, after moveUp1 and delay 16ms (53~59, 6 frames).
+ val scaleUp1 = ObjectAnimator.ofFloat(
+ counterBox,
+ "scaleY",
+ ANIM_BOX_SCALEUP1_FROM,
+ ANIM_BOX_SCALEUP1_TO,
+ ).setDuration(ANIM_BOX_SCALEUP1_DURATION)
+ scaleUp1.startDelay = ANIM_BOX_SCALEUP1_DELAY // delay 1 frame after moveUp1
+
+ // Scale down height from 105% to 99% in 116ms, after scaleUp1 (59~66, 7 frames).
+ val scaleDown2 = ObjectAnimator.ofFloat(
+ counterBox,
+ "scaleY",
+ ANIM_BOX_SCALEDOWN2_FROM,
+ ANIM_BOX_SCALEDOWN2_TO,
+ ).setDuration(ANIM_BOX_SCALEDOWN2_DURATION)
+
+ // Scale up height from 99% to 100% in 133ms, after scaleDown2 (66~74, 8 frames).
+ val scaleUp3 = ObjectAnimator.ofFloat(
+ counterBox,
+ "scaleY",
+ ANIM_BOX_SCALEUP3_FROM,
+ ANIM_BOX_SCALEUP3_TO,
+ ).setDuration(ANIM_BOX_SCALEUP3_DURATION)
+
+ animatorSet.play(fadeOut).with(moveUp1)
+ animatorSet.play(moveUp1).before(moveDown2)
+ animatorSet.play(moveDown2).with(fadeIn)
+ animatorSet.play(moveDown2).before(moveDown3)
+ animatorSet.play(moveDown3).before(moveUp4)
+
+ animatorSet.play(moveUp1).before(scaleUp1)
+ animatorSet.play(scaleUp1).before(scaleDown2)
+ animatorSet.play(scaleDown2).before(scaleUp3)
+ }
+
+ private fun createTextAnimatorSet(animatorSet: AnimatorSet) {
+ val firstAnimator = animatorSet.childAnimations[0]
+
+ // Fadeout in 100ms, with firstAnimator (49~51, 2 frames).
+ val fadeOut = ObjectAnimator.ofFloat(
+ counterText,
+ "alpha",
+ ANIM_TEXT_FADEOUT_FROM,
+ ANIM_TEXT_FADEOUT_TO,
+ ).setDuration(ANIM_TEXT_FADEOUT_DURATION)
+
+ // FadeIn in 66 ms, after fadeOut with delay 96ms (57~61, 4 frames).
+ val fadeIn = ObjectAnimator.ofFloat(
+ counterText,
+ "alpha",
+ ANIM_TEXT_FADEIN_FROM,
+ ANIM_TEXT_FADEIN_TO,
+ ).setDuration(ANIM_TEXT_FADEIN_DURATION)
+ fadeIn.startDelay = (ANIM_TEXT_FADEIN_DELAY) // delay 6 frames after fadeOut
+
+ // Move down on y-axis, from 0 to 4.4 in 66ms, with fadeIn (57~61, 4 frames).
+ val moveDown = ObjectAnimator.ofFloat(
+ counterText,
+ "translationY",
+ ANIM_TEXT_MOVEDOWN_FROM,
+ ANIM_TEXT_MOVEDOWN_TO,
+ ).setDuration(ANIM_TEXT_MOVEDOWN_DURATION)
+ moveDown.startDelay = (ANIM_TEXT_MOVEDOWN_DELAY) // delay 6 frames after fadeOut
+
+ // Move up on y-axis, from 0 to 4.4 in 66ms, after moveDown (61~69, 8 frames).
+ val moveUp = ObjectAnimator.ofFloat(
+ counterText,
+ "translationY",
+ ANIM_TEXT_MOVEUP_FROM,
+ ANIM_TEXT_MOVEUP_TO,
+ ).setDuration(ANIM_TEXT_MOVEUP_DURATION)
+
+ animatorSet.play(firstAnimator).with(fadeOut)
+ animatorSet.play(fadeOut).before(fadeIn)
+ animatorSet.play(fadeIn).with(moveDown)
+ animatorSet.play(moveDown).before(moveUp)
+ }
+
+ private fun formatCountForDisplay(count: Int): String {
+ return if (count > MAX_VISIBLE_TABS) {
+ counterText.updatePadding(bottom = INFINITE_CHAR_PADDING_BOTTOM)
+ SO_MANY_TABS_OPEN
+ } else {
+ NumberFormat.getInstance().format(count.toLong())
+ }
+ }
+
+ private fun adjustTextSize(newCount: Int) {
+ val newRatio = if (newCount in TWO_DIGITS_TAB_COUNT_THRESHOLD..MAX_VISIBLE_TABS) {
+ TWO_DIGITS_SIZE_RATIO
+ } else {
+ ONE_DIGIT_SIZE_RATIO
+ }
+
+ val counterBoxWidth =
+ context.resources.getDimensionPixelSize(R.dimen.mozac_tab_counter_box_width_height)
+ val textSize = newRatio * counterBoxWidth
+ counterText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
+ counterText.setTypeface(null, Typeface.BOLD)
+ counterText.setPadding(0, 0, 0, 0)
+ }
+
+ companion object {
+ var INTERNAL_COUNT = 0
+
+ const val MAX_VISIBLE_TABS = 99
+ const val SO_MANY_TABS_OPEN = "∞"
+ const val INFINITE_CHAR_PADDING_BOTTOM = 6
+
+ const val ONE_DIGIT_SIZE_RATIO = 0.5f
+ const val TWO_DIGITS_SIZE_RATIO = 0.4f
+ const val TWO_DIGITS_TAB_COUNT_THRESHOLD = 10
+
+ // createBoxAnimatorSet
+ private const val ANIM_BOX_FADEOUT_FROM = 1.0f
+ private const val ANIM_BOX_FADEOUT_TO = 0.0f
+ private const val ANIM_BOX_FADEOUT_DURATION = 33L
+
+ private const val ANIM_BOX_MOVEUP1_FROM = 0.0f
+ private const val ANIM_BOX_MOVEUP1_TO = -5.3f
+ private const val ANIM_BOX_MOVEUP1_DURATION = 50L
+
+ private const val ANIM_BOX_MOVEDOWN2_FROM = -5.3f
+ private const val ANIM_BOX_MOVEDOWN2_TO = -1.0f
+ private const val ANIM_BOX_MOVEDOWN2_DURATION = 167L
+
+ private const val ANIM_BOX_FADEIN_FROM = 0.01f
+ private const val ANIM_BOX_FADEIN_TO = 1.0f
+ private const val ANIM_BOX_FADEIN_DURATION = 66L
+ private const val ANIM_BOX_MOVEDOWN3_FROM = -1.0f
+ private const val ANIM_BOX_MOVEDOWN3_TO = 2.7f
+ private const val ANIM_BOX_MOVEDOWN3_DURATION = 116L
+
+ private const val ANIM_BOX_MOVEDOWN4_FROM = 2.7f
+ private const val ANIM_BOX_MOVEDOWN4_TO = 0.0f
+ private const val ANIM_BOX_MOVEDOWN4_DURATION = 133L
+
+ private const val ANIM_BOX_SCALEUP1_FROM = 0.02f
+ private const val ANIM_BOX_SCALEUP1_TO = 1.05f
+ private const val ANIM_BOX_SCALEUP1_DURATION = 100L
+ private const val ANIM_BOX_SCALEUP1_DELAY = 16L
+
+ private const val ANIM_BOX_SCALEDOWN2_FROM = 1.05f
+ private const val ANIM_BOX_SCALEDOWN2_TO = 0.99f
+ private const val ANIM_BOX_SCALEDOWN2_DURATION = 116L
+
+ private const val ANIM_BOX_SCALEUP3_FROM = 0.99f
+ private const val ANIM_BOX_SCALEUP3_TO = 1.00f
+ private const val ANIM_BOX_SCALEUP3_DURATION = 133L
+
+ // createTextAnimatorSet
+ private const val ANIM_TEXT_FADEOUT_FROM = 1.0f
+ private const val ANIM_TEXT_FADEOUT_TO = 0.0f
+ private const val ANIM_TEXT_FADEOUT_DURATION = 33L
+
+ private const val ANIM_TEXT_FADEIN_FROM = 0.01f
+ private const val ANIM_TEXT_FADEIN_TO = 1.0f
+ private const val ANIM_TEXT_FADEIN_DURATION = 66L
+ private const val ANIM_TEXT_FADEIN_DELAY = 16L * 6
+
+ private const val ANIM_TEXT_MOVEDOWN_FROM = 0.0f
+ private const val ANIM_TEXT_MOVEDOWN_TO = 4.4f
+ private const val ANIM_TEXT_MOVEDOWN_DURATION = 66L
+ private const val ANIM_TEXT_MOVEDOWN_DELAY = 16L * 6
+
+ private const val ANIM_TEXT_MOVEUP_FROM = 4.4f
+ private const val ANIM_TEXT_MOVEUP_TO = 0.0f
+ private const val ANIM_TEXT_MOVEUP_DURATION = 66L
+ }
+}
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/java/mozilla/components/ui/tabcounter/TabCounterMenu.kt b/mobile/android/android-components/components/ui/tabcounter/src/main/java/mozilla/components/ui/tabcounter/TabCounterMenu.kt
new file mode 100644
index 0000000000..4f0329a85b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/java/mozilla/components/ui/tabcounter/TabCounterMenu.kt
@@ -0,0 +1,101 @@
+/* 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.ui.tabcounter
+
+import android.content.Context
+import androidx.core.content.ContextCompat.getColor
+import mozilla.components.browser.menu2.BrowserMenuController
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * The menu that is shown when clicking on the [TabCounter]
+ *
+ * @param context the context.
+ * @param onItemTapped behavior for when an item in the menu is tapped.
+ * @param iconColor optional color to specify tint of menu icons
+ */
+open class TabCounterMenu(
+ context: Context,
+ onItemTapped: (Item) -> Unit,
+ iconColor: Int? = null,
+) {
+
+ /**
+ * Represents the menu items.
+ *
+ * [CloseTab] menu item for closing a tab.
+ * [NewTab] menu item for opening a new tab.
+ * [NewPrivateTab] menu item for opening a new private tab.
+ * [DuplicateTab] menu item for duplicating the current tab.
+ */
+ @Suppress("UndocumentedPublicClass")
+ open class Item {
+ object CloseTab : Item()
+ object NewTab : Item()
+ object NewPrivateTab : Item()
+ object DuplicateTab : Item()
+ }
+
+ var newTabItem: TextMenuCandidate
+ var newPrivateTabItem: TextMenuCandidate
+ var closeTabItem: TextMenuCandidate
+ var duplicateTabItem: TextMenuCandidate
+
+ val menuController: MenuController by lazy { BrowserMenuController() }
+
+ init {
+ newTabItem = TextMenuCandidate(
+ text = context.getString(R.string.mozac_browser_menu_new_tab),
+ start = DrawableMenuIcon(
+ context,
+ iconsR.drawable.mozac_ic_plus_24,
+ tint = iconColor ?: getColor(context, R.color.mozac_ui_tabcounter_default_text),
+ ),
+ textStyle = TextStyle(),
+ ) {
+ onItemTapped(Item.NewTab)
+ }
+
+ newPrivateTabItem = TextMenuCandidate(
+ text = context.getString(R.string.mozac_browser_menu_new_private_tab),
+ start = DrawableMenuIcon(
+ context,
+ iconsR.drawable.mozac_ic_private_mode_24,
+ tint = iconColor ?: getColor(context, R.color.mozac_ui_tabcounter_default_text),
+ ),
+ textStyle = TextStyle(),
+ ) {
+ onItemTapped(Item.NewPrivateTab)
+ }
+
+ closeTabItem = TextMenuCandidate(
+ text = context.getString(R.string.mozac_close_tab),
+ start = DrawableMenuIcon(
+ context,
+ iconsR.drawable.mozac_ic_cross_24,
+ tint = iconColor ?: getColor(context, R.color.mozac_ui_tabcounter_default_text),
+ ),
+ textStyle = TextStyle(),
+ ) {
+ onItemTapped(Item.CloseTab)
+ }
+
+ duplicateTabItem = TextMenuCandidate(
+ text = context.getString(R.string.mozac_ui_tabcounter_duplicate_tab),
+ start = DrawableMenuIcon(
+ context,
+ iconsR.drawable.mozac_ic_tab,
+ tint = iconColor ?: getColor(context, R.color.mozac_ui_tabcounter_default_text),
+ ),
+ textStyle = TextStyle(),
+ ) {
+ onItemTapped(Item.DuplicateTab)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_bar.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_bar.xml
new file mode 100644
index 0000000000..75aef5e019
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_bar.xml
@@ -0,0 +1,11 @@
+<?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/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="line">
+ <stroke
+ android:width="1dp"
+ android:color="@color/mozac_ui_tabcounter_default_tint" />
+ <size android:height="2dp" />
+</shape> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_box.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_box.xml
new file mode 100644
index 0000000000..489efcb568
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_box.xml
@@ -0,0 +1,15 @@
+<?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/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+<path
+ android:pathData="M4.5,4A2.5,2.5 0,0 0,2 6.5v11A2.5,2.5 0,0 0,4.5 20h15a2.5,2.5 0,0 0,2.5 -2.5v-11A2.5,2.5 0,0 0,19.5 4h-15zM20.5,17.7 L19.7,18.5L4.3,18.5l-0.8,-0.8L3.5,6.3l0.8,-0.8h15.4l0.8,0.8v11.4z"
+ android:strokeWidth="1"
+ android:fillColor="@color/mozac_ui_tabcounter_default_tint"/>
+</vector>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_round_rectangle_ripple.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_round_rectangle_ripple.xml
new file mode 100644
index 0000000000..e79ba0457f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/drawable/mozac_ui_tabcounter_round_rectangle_ripple.xml
@@ -0,0 +1,13 @@
+<?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/. -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#22000000">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="#000000" />
+ <corners android:radius="2dp" />
+ </shape>
+ </item>
+</ripple>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/layout/mozac_ui_tabcounter_layout.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/layout/mozac_ui_tabcounter_layout.xml
new file mode 100644
index 0000000000..ce03eed04b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/layout/mozac_ui_tabcounter_layout.xml
@@ -0,0 +1,50 @@
+<?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/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ tools:layout_height="wrap_content"
+ tools:layout_width="wrap_content">
+
+ <FrameLayout
+ android:id="@+id/counter_root"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:clipChildren="false">
+
+ <ImageView
+ android:id="@+id/counter_box"
+ android:layout_width="@dimen/mozac_tab_counter_box_width_height"
+ android:layout_height="@dimen/mozac_tab_counter_box_width_height"
+ android:contentDescription="@string/mozac_tab_counter_content_description"
+ android:importantForAccessibility="no"
+ android:layout_gravity="center"
+ app:srcCompat="@drawable/mozac_ui_tabcounter_box" />
+
+ <TextView
+ android:id="@+id/counter_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAlignment="center"
+ android:textColor="@color/mozac_ui_tabcounter_default_tint"
+ android:layout_marginBottom="0.5dp"
+ android:textSize="12sp"
+ android:textStyle="bold"
+ tools:text="16" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/counter_mask"
+ app:srcCompat="@drawable/mozac_ic_private_mode_circle_fill_stroke_20"
+ android:translationX="8dp"
+ android:translationY="-8dp"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="top|end"
+ android:visibility="gone" />
+ </FrameLayout>
+</merge>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..0aeb48b020
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-am/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ትር ክፈት። ትሮችን ለመቀየር መታ ያድርጉ።</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ትር ክፈት። ትሮችን ለመቀየር መታ ያድርጉ።</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">አዲስ ትር</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">አዲስ የግል ትር</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ትርን ዝጋ</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">ትርን አባዛ</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">የትር ቆጣሪ ሰሪ-አሞሌ አዝራር።</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..4f66de8ec0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ar/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">لسان واحد مفتوح. انقر لتبديل الألسنة.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s من الألسنة مفتوح. انقر لتبديل الألسنة.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">لسان جديد</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">لسان خاص جديد</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">أغلِق اللسان</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">كرّر اللسان</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">زر ”عدد الألسنة“ في شريط الأدوات.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..ed05585c87
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ast/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 llingüeta abierta. Toca pa cambiar a otra.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s llingüetes abiertes. Toca pa cambiar a otra.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Llingüeta nueva</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Llingüeta privada nueva</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Zarrar la llingüeta</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar la llingüeta</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">El botón del contador de llingüetes de la barra de ferramientes.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..9f856c949a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-azb/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">آچیق تاغ. تاغ‌لاری دگیشدیرمک اوچون توخونون.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s آچیق تاغ. تاغلاری دگیشدیرمک اوچون توخونون.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">یئنی تاغ</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">یئنی گیزلی تاغ</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">تاغی باغلا</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">تاغی ایکی‌له</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">تاغ سایی‌جی‌نین تولبار دویمه‌سی.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..395e798a81
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-be/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 адкрытая картка. Націсніце, каб пераключыць карткі.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Адкрытых картак: %1$s. Націсніце, каб пераключыць карткі.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Новая картка</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Новая прыватная картка</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Закрыць картку</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Дубляваць картку</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Кнопка лічыльніка картак на панэлі інструментаў.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..0714493b0d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bg/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 отворен раздел. Докоснете за превключване на раздели.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s отворени раздела. Докоснете, за превключване на раздели.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Нов раздел</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Нов поверителен раздел</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Затваряне на раздел</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Дублиране на раздел</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Бутон към инструментите от брояча на раздели.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..1136e9a18c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bn/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">খোলা ট্যাব ১টি। ট্যাব পাল্টাও।</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$sটি খোলা ট্যাব। ট্যাব পাল্টাও।</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">নতুন ট্যাব</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">নতুন ব্যক্তিগত ট্যাব</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ট্যাব বন্ধ করুন</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">অনুরূপ ট্যাব</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ট্যাব গণনাকারী টুলবার বোতাম।</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..813c4a9382
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-br/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ivinell digor. Stokit evit mont dʼun ivinell all.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ivinell digor. Stokit evit mont dʼun ivinell all.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Ivinell nevez</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Ivinell prevez nevez</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Serriñ an ivinell</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Eilañ an ivinell</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">An afell kontañ ivinelloù er varrenn-ostilhoù.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..8b89e9972f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-bs/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 otvoren tab. Dodirnite za promjenu tabova.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s otvorenih tabova. Dodirnite za promjenu tabova.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Novi tab</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Novi privatni tab</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Zatvori tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dupliciraj tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Tipka brojača tabova na alatnoj traci.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..141fb164b0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ca/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestanya oberta. Toqueu per canviar de pestanya.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestanyes obertes. Toqueu per canviar de pestanya.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Pestanya nova</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Pestanya privada nova</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Tanca la pestanya</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplica la pestanya</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">El botó del comptador de pestanyes de la barra d’eines.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..0ac9556c3d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cak/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ruwi\' jaqon. Tachapa\' richin nak\'ëx ruwi\'.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ruwi\' ejaqon. Tachapa\' richin nak\'ëx ruwi\'.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">K\'ak\'a\' ruwi\'</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">K\'ak\'a\' ichinan ruwi\'</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Titz\'apïx ruwi\'</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Tikamulüx ruwi\'</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Ri rupitz\'b\'al ajilanel ruwi\' pa rukajtz\'ik samajib\'äl.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..c629119fc1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ang abli nga tab. i-Tap para mobalhin ug mga tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ang abli nga mga tab. i-Tap para mobalhin ug mga tab.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Bag-o nga tab</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Bag-o nga pribadong tab</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">i-Close ang tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicate nga tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Ang tab counter toolbar button.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..46e0ffeadb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">١ بازدەر کراوەیە. پەنجەدابگرە بۆ گۆڕینی بازدەرەکان.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s بازدەر کراوەیە. پەنجەدابگرە بۆ گۆڕینی بازدەرەکان.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">بازدەری نوێ</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">بازدەری تایبەتی نوێ</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">بازدەر دابخە</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">بازدەری دووبارە</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">دوگمەی ژمارەی بازدەرەکان لە توڵامراز.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..d72dfc8f8f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-co/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 unghjetta aperta. Picchichjà per cambià d’unghjetta.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s unghjette aperte. Picchichjà per cambià d’unghjetta.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nova unghjetta</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nova unghjetta privata</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Chjode l’unghjetta</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duppià l’unghjetta</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">U buttone di cuntatore d’unghjetta in a barra d’attrezzi.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..de8db31078
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cs/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Jeden otevřený panel. Klepnutím panely přepnete.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s otevřených panelů. Klepnutím přepnete panely.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nový panel</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nový anonymní panel</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Zavřít panel</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplikovat panel</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Tlačítko s počtem panelů na liště.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..78e83d1611
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-cy/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tab ar agor. Tapio i newid tabiau.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tab ar agor. Tapio i newid tabiau.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Tab newydd</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Tab preifat newydd</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Cau tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dyblygu tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Botwm y bar offer cyfrif tabiau.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..15549d060c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-da/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 åbent faneblad. Tryk for at skifte faneblade.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s åbne faneblade. Tryk for at skifte faneblade.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nyt faneblad</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nyt privat faneblad</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Luk faneblad</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Kopier faneblad</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Værktøjslinjeknappen til fanebladstæller.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..69ff81ea3b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-de/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 offener Tab. Antippen, um Tabs zu wechseln.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s offene Tabs. Antippen, um Tabs zu wechseln.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Neuer Tab</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Neuer privater Tab</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Tab schließen</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Tab klonen</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Die Schaltfläche der Tab-Zähler-Symbolleiste.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..6eb7e31beb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Wócynjone rejtariki: 1. Pótusniśo, aby rejtariki pśešaltował.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Wócynjone rejtariki: %1$s. Pótusniśo, aby rejtariki pśešaltował.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nowy rejtarik</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nowy priwatny rejtarik</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Rejtarik zacyniś</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Rejtarik pódwójś</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Symbol rejtarikowego licaka na symbolowej rědce.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..9a3eb98254
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-el/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ανοικτή καρτέλα. Πατήστε για εναλλαγή καρτελών.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ανοικτές καρτέλες. Πατήστε για εναλλαγή καρτελών.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Νέα καρτέλα</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Νέα ιδιωτική καρτέλα</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Κλείσιμο καρτέλας</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Αντιγραφή καρτέλας</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Το κουμπί μέτρησης καρτελών της γραμμής εργαλείων.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..04c14f2b61
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">New tab</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">New private tab</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Close tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicate tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">The tab counter toolbar button.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..04c14f2b61
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">New tab</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">New private tab</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Close tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicate tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">The tab counter toolbar button.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..8936839830
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-eo/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 malfermita langeto. Tuŝetu por ŝanĝi langeton.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s malfermitaj langetoj. Tuŝetu por ŝanĝi langetojn.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nova langeto</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nova privata langeto</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Fermi langeton</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duobligi langeton</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">La ilara butono kun nombro de langetoj.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..dccbab6fc8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Tocá para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Tocá para cambiar de pestaña.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nueva pestaña</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nueva pestaña privada</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Cerrar pestaña</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Pestaña duplicada</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">El botón contador de pestañas de la barra de herramientas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..fec84a0de1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Toca para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Toca para cambiar de pestaña.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nueva pestaña</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nueva pestaña privada</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Cerrar pestaña</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar pestaña</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">El botón contador de pestañas de la barra de herramientas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..fec84a0de1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Toca para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Toca para cambiar de pestaña.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nueva pestaña</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nueva pestaña privada</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Cerrar pestaña</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar pestaña</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">El botón contador de pestañas de la barra de herramientas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..fec84a0de1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Toca para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Toca para cambiar de pestaña.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nueva pestaña</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nueva pestaña privada</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Cerrar pestaña</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar pestaña</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">El botón contador de pestañas de la barra de herramientas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..fec84a0de1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-es/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 pestaña abierta. Toca para cambiar de pestaña.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s pestañas abiertas. Toca para cambiar de pestaña.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nueva pestaña</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nueva pestaña privada</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Cerrar pestaña</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar pestaña</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">El botón contador de pestañas de la barra de herramientas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..a1dca163dd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-et/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 avatud kaart. Kaartide vahetamiseks puuduta.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s avatud kaarti. Kaartide vahetamiseks puuduta.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Uus kaart</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Uus privaatne kaart</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Sulge kaart</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Klooni kaart</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Kaartide loenduri tööriistariba nupp.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..f2d08959b2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-eu/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Irekitako fitxa bat. Sakatu fitxaz aldatzeko.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Irekitako %1$s fitxa. Sakatu fitxaz aldatzeko.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Fitxa berria</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Fitxa pribatu berria</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Itxi fitxa</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Bikoiztu fitxa</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Fitxen kontagailuaren tresna-barrako botoia.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..75cc015960
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fa/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">۱ زبانهٔ باز. برای تعویض زبانه‌ها ضربه بزنید.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s زبانهٔ باز. برای تغییر زبانه‌ها ضربه بزنید.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">زبانهٔ جدید</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">زبانهٔ خصوصی جدید</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">بستن زبانه</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">تکثیر زبانه</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">دکمهٔ نوار ابزار شمارندهٔ زبانه‌ها.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..c9c6f48e08
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ff/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Tabbere hesere</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Tabbere suuriinde hesere</string>
+ </resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..cd5743f8f5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fi/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 avoin välilehti. Napauta vaihtaaksesi.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s avointa välilehteä. Napauta vaihtaaksesi.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Uusi välilehti</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Uusi yksityinen välilehti</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Sulje välilehti</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Monista välilehti</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Välilehtien laskurin työkalupalkin painike.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..de7950588c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fr/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 onglet ouvert. Appuyez pour changer d’onglet.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s onglets ouverts. Appuyez pour changer d’onglet.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nouvel onglet</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nouvel onglet privé</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Fermer l’onglet</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dupliquer l’onglet</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Le bouton compteur d’onglets de la barre d’outils.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..365119d527
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fur/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 schede vierte. Tocje par cambiâ schede.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s schedis viertis. Tocje par cambiâ schede.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Gnove schede</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Gnove schede privade</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Siere schede</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dupliche schede</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Il boton cul contadôr des schedis te sbare dai struments.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..7c6443528e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 iepen ljepblêd. Tik om tusken ljepblêden te wikseljen.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s iepen ljepblêden. Tik om tusken ljepblêden te wikseljen.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nij ljepblêd</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nij priveeljepblêd</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Ljepblêd slute</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Ljepblêd duplisearje</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">De ljepblêdteller-arkbalkeknop.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..b9bbced3ea
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gd/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Tha taba fosgailte. Thoir gnogag airson leum a ghearradh gu taba eile.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Tha tabaichean (%1$s) fosgailte. Thoir gnogag airson leum a ghearradh gu taba eile.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Taba ùr</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Taba prìobhaideach ùr</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Dùin an taba</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dùblaich an taba</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Putan bàr-inneal cunntair nan taba.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..c4112f16fd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gl/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 lapela aberta. Toque para cambiar de lapela.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s lapelas abertas. Toque para cambiar de lapela.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nova lapela</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nova lapela privada</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Pechar lapela</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar o separador</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">O botón da barra de ferramentas do contador de lapelas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..b1fa6062c3
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-gn/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tendayke ijurujáva. Eikutu emoambue hag̃ua tendayke.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tendayke ijurujáva. Eikutu emoambue hag̃ua tendayke.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Tendayke pyahu</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Tendayke pyahu ñemigua</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Tendayke mboty</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Tendayke ikõiva</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Pe votõ tendayke papaha tembiporu renda pegua.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..519a98c230
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 खुले टैब। टैब स्विच करने के लिए टैप करें।</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s खुले टैब। टैब स्विच करने के लिए टैप करें।</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">नया टैब</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">नई निजी टैब</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">टैब बंद करें</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">डुप्लीकेट टैब</string>
+ </resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..0233a91fa4
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hr/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 otvorena kartica. Dodirni za prebacivanje kartica.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s otvorene kartice. Dodirni za prebacivanje kartica.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nova kartica</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nova privatna kartica</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Zatvori karticu</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dupliciraj karticu</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Tipka brojača kartica na alatnoj traci.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..1affc525e1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Wočinjene rajtarki: 1. Podótkńće so, zo byšće rajtarki přepinał.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Wočinjene rajtarki: %1$s. Podótkńće so, zo byšće rajtarki přepinał.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nowy rajtark</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nowy priwatny rajtark</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Rajtark začinić</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Rajtark podwojić</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Symbol rajtarkoweho ličaka na symbolowej lajsće.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..abc31799fc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hu/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 nyitott lap. Koppintson a lapváltáshoz.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s nyitott lap. Koppintson a lapváltáshoz.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Új lap</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Új privát lap</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Lap bezárása</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Lap duplikálása</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">A lapszámláló eszköztárgomb.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..201c4bff6f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 բաց ներդիր: Հպեք՝ ներդիրին անցնելու համար:</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s բաց ներդիրներ: Հպեք՝ ներդիրին անցնելու համար:</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Նոր ներդիր</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Նոր մասնավոր ներդիր</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Փակել ներդիրը</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Կրկնօրինակել ներդիրը</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Ներդիրի հաշվիչի գործիքագոտու կոճակը:</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..2e80e79466
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ia/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 scheda aperte. Tocca pro cambiar le scheda.
+</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s schedas aperte. Tocca pro cambiar le scheda.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nove scheda</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nove scheda private</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Clauder le scheda</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar le scheda</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Le button contator de schedas del barra de instrumentos.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..63a84de095
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-in/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tab terbuka. Ketuk untuk beralih tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tab terbuka. Ketuk untuk beralih tab.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Tab baru</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Tab pribadi baru</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Tutup tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Gandakan tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Tombol bilah alat penghitung tab.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..85bf97412a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-is/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 opinn flipi. Ýttu til að skipta um flipa.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s opnir flipar. Ýttu til að skipta um flipa.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nýr flipi</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nýr huliðsflipi</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Loka flipa</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Tvítaka flipa</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Flipateljara-hnappurinn á verkfæraslánni.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..bb6ba5c1c4
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-it/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Aperta 1 scheda. Tocca per cambiare scheda.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Aperte %1$s schede. Tocca per cambiare scheda.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nuova scheda</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nuova scheda anonima</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Chiudi scheda</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplica scheda</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Il pulsante nella barra degli strumenti con il numero di schede</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..57d4a70de1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-iw/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">לשונית אחת פתוחה. יש להקיש כדי להחליף לשוניות.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s לשוניות פתוחות. יש להקיש כדי להחליף לשוניות.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">לשונית חדשה</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">לשונית פרטית חדשה</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">סגירת לשונית</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">שכפול לשונית</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">כפתור סרגל הכלים של מונה הלשוניות.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..9780a8380f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ja/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">開いているタブ 1 個。タップしてタブを切り替えます。</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">開いているタブ %1$s 個。タップしてタブを切り替えます。</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">新しいタブ</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">新しいプライベートタブ</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">タブを閉じる</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">タブを複製</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">タブカウンターのツールバーボタンです。</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..00d535442e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ka/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 გახსნილი ჩანართი. შეეხეთ ჩანართების გადასართველად.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s გახსნილი ჩანართი. შეეხეთ ჩანართების გადასართველად.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">ახალი ჩანართი</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">ახალი პირადი ჩანართი</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ჩანართის დახურვა</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">ჩანართის გაორმაგება</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ჩანართის მრიცხველის ღილაკი სამართავ ზოლზე.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..5854750cf2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 bet ashıq. Basqa betlerge ótiw ushın basıń.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s bet ashıq. Basqa betlerge ótiw ushın basıń.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Jańa bet</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Jańa jeke bet</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Betti jabıw</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Betti nusqalaw</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Ásbaplar panelinde betler sanaw túymesi.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..e3c1f477b6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kab/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 yiccer i yeldin. Sit akken ad tbeddleḍ iccer.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s waccaren yeldin. Sit akken ad tettbeddileḍ gar waccaren.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Iccer amaynut</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Iccer uslig amaynut</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Mdel iccer</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Sleg iccer</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Taqeffalt n ugalis n yifecka n umesmiḍan n waccaren.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..0149ee5e5c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kk/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ашық бет. Беттерді ауыстыру үшін шертіңіз.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ашық бет. Беттерді ауыстыру үшін шертіңіз.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Жаңа бет</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Жаңа жекелік беті</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Бетті жабу</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Бетті қосарлау</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Беттер санағышы болатын панель батырмасы.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..d05d912cb7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 hilpekîna vekirî. Ji bo hilpekînê biguherînî, bitikîne.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s hilpekînên vekirî. Ji bo hilpekînê biguherînî, bitikîne.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Hilpekîna nû</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Hilpekîna veşartî ya nû</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Hilpekînê bigire</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Hilpekînê zêde bike</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Bişkoka darikê amûran a jimarkera hilpekînan.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..4811b27a84
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ko/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">열린 탭 1개. 탭을 전환하려면 누르세요.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">열린 탭 %1$s개. 탭을 전환하려면 누르세요.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">새 탭</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">새 사생활 보호 탭</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">탭 닫기</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">탭 복제</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">탭 카운터 도구 모음 버튼입니다.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..a82e9d6e4e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-lo/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ເປີດແທັບ. ແຕະເພື່ອປ່ຽນແທັບ.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ເປີດແທັບ. ແຕະເພື່ອປ່ຽນແທັບ.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">ແທັບໃຫມ່</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">ແທັບສ່ວນໂຕໃຫມ່</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ປິດແທັບ</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">ແທັບທີ່ຊໍ້າກັນ</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ປຸ່ມແຖບເຄື່ອງມື ໂຕນັບແຖບ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..9b592c0023
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-lt/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 atverta kortelė. Bakstelėkite, norėdami pereiti tarp kortelių.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s atvertos kortelės. Bakstelėkite, norėdami pereiti tarp kortelių.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nauja kortelė</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nauja privačioji kortelė</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Užverti kortelę</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dubliuoti kortelę</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Kortelių skaičiaus mygtukas priemonių juostoje.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..13d029ac49
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-my/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">ဖွင့်ထားသော တပ်ဗ် 1 ။ တပ်ဗ်များ ပြောင်းရန် နှိပ်ပါ။ </string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">ဖွင့်ထားသော တပ်ဗ်များ %1$s ။ တက်ဗ်များ ပြောင်းရန် နှိပ်ပါ။ </string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">တပ်ဗ် အသစ်</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">ကိုယ်ပိုင် သီးသန့် တက်ဗ် အသစ်</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">တပ်ဗ်ကို ပိတ်ပါ</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">တပ်ဗ်ကို ပွားပါ</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">တပ်ဗ် ကောင်တာ တူးဘား ခလုတ်</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..941a1a5647
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 åpen fane. Trykk for å bytte fane.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s åpne faner. Trykk for å bytte fane.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Ny fane</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Ny privat fane</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Lukk fane</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dupliser fane</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Fane-teller verktøylinjeknapp.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..b0458adc54
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ट्याब खोल्नुहोस् । ट्याबहरु बिचमा स्वीच गर्नको लागि ट्याप गर्नुहोस् ।</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ट्याबहरु खोल्नुहोस् । ट्याबहरु बिचमा स्वीच गर्नको लागि ट्याप गर्नुहोस् ।</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">नयाँ ट्याब</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">नयाँ निजी ट्याब</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ट्याब बन्द गर्नुहोस्</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">नक्कल ट्याब</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ट्याब काउन्टर उपकरणपट्टी बटन् ।</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..e34f69d0bc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nl/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open tabblad. Tik om tussen tabbladen te wisselen.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open tabbladen. Tik om tussen tabbladen te wisselen.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nieuw tabblad</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nieuw privétabblad</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Tabblad sluiten</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Tabblad dupliceren</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">De tabbladteller-werkbalkknop.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..71cd5c74de
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open fane. Trykk for å byte fane.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s opne faner. Trykk for å byte fane.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Ny fane</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Ny privat fane</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Lat att fane</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dupliser fane</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Fane-teljar verktøylinjeknapp.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..78c7d4501e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-oc/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 onglet dubèrt. Tocatz per bascular.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s onglets dubèrts. Tocatz per bascular.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Onglet novèl</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Onglet de nav. privada</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Tampar l’onglet</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar l’onglet</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Lo boton comptador d’onglets de la barra d’aisinas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..5405b5f6e0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-or/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">ନୂଆ ଟ୍ୟାବ୍</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">ନୂଆ ବ୍ୟକ୍ତିଗତ ଟ୍ୟାବ୍</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ଟ୍ୟାବ୍ ବନ୍ଦ କରନ୍ତୁ</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">ଟ୍ୟାବ୍ ନକଲ କରନ୍ତୁ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..68a1578f17
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ਟੈਬ ਖੁੱਲ੍ਹੀ ਹੈ। ਟੈਬਾਂ ਵਿੱਚ ਬਦਲਣ ਲਈ ਛੂਹੋ।</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ਟੈਬਾਂ ਖੁੱਲ੍ਹੀਆਂ। ਟੈਬਾਂ ਵਿੱਚ ਸਵਿੱਚ ਕਰਨ ਵਾਸਤੇ ਟੈਪ ਕਰੋ।</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">ਨਵੀਂ ਟੈਬ</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">ਨਵੀਂ ਨਿੱਜੀ ਟੈਬ</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ਟੈਬ ਬੰਦ ਕਰੋ</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">ਡੁਪਲੀਕੇਟ ਟੈਬ</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ਟੈਬ ਗਿਣਤੀ ਟੂਲ-ਪੱਟੀ ਬਟਨ ਹੈ।</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..a91863a90b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">اک ٹیب کھُلھی اے۔ ہورناں ٹیب جاوݨ لئی اِتھے چھوہو۔</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ٹیباں کھُلھیاں ہن۔ ہورناں ٹیب جاوݨ لئی اِتھے چھوہو۔</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">نویں ٹیب کھولھو</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">نجی ٹیب کھولھو</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ٹیب بند کرو</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">ٹیب کاپی کرو</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ٹیب دی گݨتی والا بٹن اے۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..6179d81a5a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pl/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Otwarte karty: 1. Stuknij, aby przełączyć karty.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Otwarte karty: %1$s. Stuknij, aby przełączyć karty.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nowa karta</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nowa karta prywatna</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Zamknij kartę</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplikuj kartę</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Przycisk paska narzędzi z liczbą kart.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..0f4f87ffab
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 aba aberta. Toque para alternar abas.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s abas abertas. Toque para alternar abas.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nova aba</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nova aba privativa</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Fechar aba</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar aba</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">O botão contador de abas da barra de ferramentas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..9abddce5aa
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 separador aberto. Toque para mudar de separador.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s separadores abertos. Toque para mudar de separadores.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Novo separador</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Novo separador privado</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Fechar separador</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicar separador</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">O botão da barra de ferramentas com o número de separadores.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..4a162573f2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-rm/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tab avert. Tutgar per midar tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tabs averts. Tutgar per midar tab.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nov tab</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nov tab privat</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Serrar il tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplitgar il tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Il buttun en la trav d\'utensils cun il dumber da tabs.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..e2726988b1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ro/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 filă deschisă. Atinge pentru a comuta între file.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s file deschise. Atinge pentru a comuta între file.</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Filă privată nouă</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Închide fila</string>
+ </resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..dcde495148
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ru/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 открытая вкладка. Нажмите, чтобы переключить вкладки.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Открытых вкладок: %1$s. Нажмите, чтобы переключить вкладки.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Новая вкладка</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Новая приватная вкладка</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Закрыть вкладку</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Дублировать вкладку</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Кнопка счётчика вкладок на панели инструментов.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..a6878d4562
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sat/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ᱠᱷᱩᱞᱟᱹ ᱴᱮᱵᱽ ᱾ ᱴᱮᱵᱽ ᱠᱚ ᱥᱣᱤᱪ ᱞᱟᱹᱜᱤᱫ ᱚᱛᱟᱭ ᱢᱮ ᱾</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ᱠᱷᱩᱞᱟᱹ ᱴᱮᱵᱽᱠᱚ ᱾ ᱴᱮᱵᱽᱠᱚ ᱥᱣᱤᱪ ᱞᱟᱹᱜᱤᱫ ᱚᱛᱟᱭ ᱢᱮ ᱾</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">ᱱᱟᱶᱟ ᱴᱮᱵᱽ</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">ᱱᱟᱶᱟ ᱱᱤᱡᱮᱨᱟᱠ ᱴᱮᱵᱽ</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ᱴᱮᱵᱽ ᱵᱚᱸᱫᱽᱚᱭ ᱢᱮ</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">ᱰᱩᱯᱞᱤᱠᱮᱴ ᱴᱮᱵᱽ</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ᱴᱮᱵᱽ ᱠᱟᱣᱱᱴᱟᱹᱨ ᱴᱩᱞᱵᱟᱨ ᱵᱩᱛᱟᱹᱢ ᱾</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..d19f2c5d25
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sc/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ischeda aberta. Toca pro cuncambiare ischedas.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ischedas abertas. Toca pro cuncambiare ischedas.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Ischeda noa</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Ischeda privada noa</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Serra s’ischeda</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Dùplica s’ischeda</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Su butone de su contadore de ischedas de sa barra de ainas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..783718eb2c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-si/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">විවෘත පටිති 1. මාරු වීමට ඔබන්න.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">විවෘත පටිති %1$s. මාරු වීමට ඔබන්න.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">නව පටිත්ත</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">නව පෞද්. පටිත්ත</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">පටිත්ත වසන්න</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">පටිත්තෙහි අනුපිටපතක්</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">පටිති ගණනය මෙවලම් තීරු බොත්තම.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..5f8dc12703
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sk/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 otvorená karta. Ťuknutím prepnete karty.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s otvorených kariet. Ťuknutím prepnete karty.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nová karta</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nová súkromná karta</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Zavrieť kartu</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplikovať kartu</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Tlačidlo počítadla kariet na paneli nástrojov.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..b4315df355
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-skr/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">١ ٹیب کھولو۔ ٹیبز بدلݨ کیتے دباؤ۔</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ٹیبز کھولو۔ ٹیبز بدلݨ کیتے دباؤ۔</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">نواں ٹیب</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">نویں نجی ٹیب</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ٹیب بند کرو</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">واڳی ٹیب</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ٹیباں ڳݨݨ آلا ٹولبار بٹݨ۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..fe24fbb221
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sl/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 odprt zavihek. Tapnite za preklop zavihkov.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Odprtih zavihkov: %1$s. Tapnite za preklop zavihkov.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nov zavihek</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nov zasebni zavihek</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Zapri zavihek</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Podvoji zavihek</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Gumb števca zavihkov v orodni vrstici.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..a17e1ece9d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sq/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 skedë e hapur. Prekeni që të ndërroni skeda.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s skeda të hapura. Prekeni që të ndërroni skeda.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Skedë e re</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Skedë e re private</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Mbylle skedën</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Përdytëso skedën</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Butoni i numrit të skedave te paneli.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..7963c5b8f8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sr/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 отворени језичак. Додирни за пребацивање језичака.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s отворених језичака. Додирни за пребацивање језичака.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Нови језичак</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Нови приватни језичак</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Затвори језичак</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Удвостручи језичак</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Дугме за бројач језичака.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..fdc4401b78
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-su/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 tab muka. Toél pikeun pindah tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s tab muka. Toél pikeun pindah tab.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Tab anyar</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Tab nyamuni anyar</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Tutup tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplikat tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Tombol tulbar pangitung tab.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..bed22b6674
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 öppen flik. Tryck för att växla mellan flikar.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s öppna flikar. Tryck för att växla mellan flikar.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Ny flik</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Ny privat flik</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Stäng flik</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicera flik</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Verktygsfältknapp för flikräknare.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..abdc3c96f5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-szl/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">Jedna ôtwarto karta. Tyknij, coby przełōnczyć karty.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">Ôtwarte karty: %1$s. Tyknij, coby je zmiynić.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Nowo karta</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Nowo prywatno karta</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Zawrzij karta</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Tupluj karta</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Knefel z poskym z noczyniami do rachowanio kart. </string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..056d3aebcd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-te/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 తెరిచివున్న ట్యాబు. ట్యాబుల మధ్య మారడానికి తాకండి.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s తెరిచివున్న ట్యాబులు. ట్యాబుల మధ్య మారడానికి తాకండి.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">కొత్త ట్యాబు</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">కొత్త అంతరంగిక ట్యాబు</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ట్యాబును మూసివేయి</string>
+ </resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..950305d610
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tg/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 варақаи кушода. Барои гузариш байни варақаҳо, зарба занед.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s варақаи кушода. Барои гузариш байни варақаҳо, зарба занед.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Варақаи нав</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Варақаи хусусии нав</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Пӯшидани варақа</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Такроран кушодани варақа</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Тугмаи ҳисобкунаки варақаҳо дар навори абзорҳо.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..cee1d74cd6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-th/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 แท็บที่เปิด แตะเพื่อสลับไปยังแท็บ</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s แท็บที่เปิด แตะเพื่อสลับไปยังแท็บ</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">แท็บใหม่</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">แท็บส่วนตัวใหม่</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ปิดแท็บ</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">ทำสำเนาแท็บ</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ปุ่มแถบเครื่องมือตัวนับแท็บ</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..ca7ef24882
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tl/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 nakabukas na tab. I-tap para lumipat ng tab.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s nakabukas na tab. I-tap para lumipat ng tab.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Bagong tab</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Bagong pribadong tab</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Isara ang tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Kaparehong tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Ang toolbar button para sa bilang ng tab.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..896b18dbf7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tr/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 açık sekme. Sekme değiştirmek için dokunun.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s açık sekme. Sekme değiştirmek için dokunun.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Yeni sekme</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Yeni gizli sekme</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Sekmeyi kapat</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Sekmeyi çoğalt</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Sekme sayacı araç çubuğu düğmesi.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..732c0ebab6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-trs/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 nā\'nïn rakïj ñanj. Gūru\'man ra\'a da\' nādūnāt rakïj ñanj.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s nā\'nïn nej rakïj ñanj. Gūru\'man ra\'a da\' nādūnāt nej rakïj ñanj.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Rakïj ñanj nākàa</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Rakïj ñaj nākà gārasun \'ngō rïn\'</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Narán rakïj ñanj</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Rakïj ñanj nata’a</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Butûn riña ‘na’ nej dukuán ahia nej si rāsun nej rakïj ñanj.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..d89b5bbbaa
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tt/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ачык таб. Табларны күчерү өчен басыгыз.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ачык таб. Табларны күчерү өчен басыгыз.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Яңа таб</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Яңа хосусый таб</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Табны ябу</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Табны кабатлау</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Кораллар панелендәге таблар cанагычы төймәсе.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..3ca40446d7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Aseksel amaynu</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Aseksel uslig amaynu</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Mdel aseksel</string>
+ </resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..a2a92a2c73
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ug/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 بەتكۈچ ئوچۇق. بەتكۈچنى ئالماشتۇرۇش ئۈچۈن چېكىڭ.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s بەتكۈچ ئوچۇق. بەتكۈچنى ئالماشتۇرۇش ئۈچۈن چېكىڭ.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">يېڭى بەتكۈچ</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">يېڭى شەخسىي بەتكۈچ</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">بەتكۈچنى تاقاش</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">تەكرار بەتكۈچ</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">بەتكۈچ سانىغۇچ قورال بالداق توپچىسى.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..1f46eb3d93
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-uk/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 відкрита вкладка. Торкніться, щоб перемкнути вкладки.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s відкритих вкладок. Торкніться, щоб перемкнути вкладки.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Нова вкладка</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Нова приватна вкладка</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Закрити вкладку</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Дублювати вкладку</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Кнопка панелі інструментів лічильника вкладок.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..da3b2630ae
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-ur/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 کھلا ٹیب۔ ٹیبز بدلنے کے لئے دبائیں۔</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s کھلے ٹیب۔ ٹیب بدلنے کے لئے دبائیں۔</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">نیا ٹیب</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">نیا نجی ٹیب</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">ٹیب بند کریں</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">دوهرا ٹیب</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">ٹیب کاؤنٹر ٹول بار کا بٹن۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..a9b46408c3
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-uz/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 ta ochiq varaq. Boshqa varaqqa oʻtish uchun bosing.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s ta ochiq varaq. Boshqa varaqqa oʻtish uchun bosing.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Yangi varaq</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Yangi maxfiy varaq</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Varaqni yopish</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Varaqni nusxalash</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Asboblar panelidagi varaq taymer tugmasi.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..af59036db1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-vi/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 thẻ đang mở. Chạm để chuyển thẻ.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s thẻ đang mở. Chạm để chuyển thẻ.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Thẻ mới</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Thẻ riêng tư mới</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Đóng thẻ</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Nhân đôi thẻ</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Nút thanh công cụ bộ đếm thẻ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..13d72778f6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-yo/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 sí táàbù. Tẹ̀ ẹ́ láti bọ́ sí àwọn táàbù.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s sí táàbù. Tẹ̀ ẹ́ láti bọ́ sí àwọn táàbù.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">Táàbù tuntun</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">Táàbù ìkọ̀kọ̀ tuntun</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Pa táàbù dé</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Ẹ̀dà táàbù</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">Táàbù náà rí bọ́tìnì irinṣé.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..c2a979f9d5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">打开了 1 个标签页,点击即可切换。</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">打开了 %1$s 个标签页,点击即可切换。</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">新建标签页</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">新建隐私标签页</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">关闭标签页</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">克隆标签页</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">标签页计数器工具栏按钮。</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..ef18d2a974
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">開啟了 1 個分頁,點擊即可切換分頁。</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">開啟了 %1$s 個分頁,點擊即可切換分頁。</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">開新分頁</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">開新隱私分頁</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">關閉分頁</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">複製分頁</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">分頁計數器工具列按鈕。</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/attrs.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000..70195f2198
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/attrs.xml
@@ -0,0 +1,11 @@
+<?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>
+
+ <declare-styleable name="TabCounter">
+ <attr name="tabCounterTintColor" format="reference|color" />
+ </declare-styleable>
+
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/colors.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..f01e0ca318
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/colors.xml
@@ -0,0 +1,9 @@
+<?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>
+ <color name="mozac_ui_tabcounter_default_tint">#FF272727</color>
+ <color name="mozac_ui_tabcounter_private_tint">#FFFFFF</color>
+ <color name="mozac_ui_tabcounter_default_text">#20123A</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/dimens.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..e247e31552
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+<?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>
+ <dimen name="mozac_tab_counter_box_width_height">24dp</dimen>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/strings.xml b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..b63520c1a0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?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>
+ <!-- Message announced to the user when tab tray is selected with 1 tab -->
+ <string name="mozac_tab_counter_open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
+ <!-- Message announced to the user when tab tray is selected with multiple tabs -->
+ <string name="mozac_tab_counter_open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
+ <!-- Browser menu button that creates a new tab -->
+ <string name="mozac_browser_menu_new_tab">New tab</string>
+ <!-- Browser menu button that creates a private tab -->
+ <string name="mozac_browser_menu_new_private_tab">New private tab</string>
+ <!-- Browser menu button to close tab. Closes the current session when pressed. -->
+ <string name="mozac_close_tab">Close tab</string>
+ <!-- Menu option to duplicate the current tab -->
+ <string name="mozac_ui_tabcounter_duplicate_tab">Duplicate tab</string>
+ <!-- Content description of the tab counter toolbar button -->
+ <string name="mozac_tab_counter_content_description">The tab counter toolbar button.</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/test/java/mozilla/components/ui/tabcounter/TabCounterMenuTest.kt b/mobile/android/android-components/components/ui/tabcounter/src/test/java/mozilla/components/ui/tabcounter/TabCounterMenuTest.kt
new file mode 100644
index 0000000000..40ec54066b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/test/java/mozilla/components/ui/tabcounter/TabCounterMenuTest.kt
@@ -0,0 +1,53 @@
+/* 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.ui.tabcounter
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class TabCounterMenuTest {
+
+ @Test
+ fun `return only the new tab item`() {
+ val onItemTapped: (TabCounterMenu.Item) -> Unit = spy { Unit }
+ val menu = TabCounterMenu(testContext, onItemTapped)
+
+ val item = menu.newTabItem
+ assertEquals("New tab", item.text)
+ item.onClick()
+
+ verify(onItemTapped).invoke(TabCounterMenu.Item.NewTab)
+ }
+
+ @Test
+ fun `return only the new private tab item`() {
+ val onItemTapped: (TabCounterMenu.Item) -> Unit = spy { Unit }
+ val menu = TabCounterMenu(testContext, onItemTapped)
+
+ val item = menu.newPrivateTabItem
+ assertEquals("New private tab", item.text)
+ item.onClick()
+
+ verify(onItemTapped).invoke(TabCounterMenu.Item.NewPrivateTab)
+ }
+
+ @Test
+ fun `return a close button`() {
+ val onItemTapped: (TabCounterMenu.Item) -> Unit = spy { Unit }
+ val menu = TabCounterMenu(testContext, onItemTapped)
+
+ val item = menu.closeTabItem
+ assertEquals("Close tab", item.text)
+ item.onClick()
+
+ verify(onItemTapped).invoke(TabCounterMenu.Item.CloseTab)
+ }
+}
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/test/java/mozilla/components/ui/tabcounter/TabCounterTest.kt b/mobile/android/android-components/components/ui/tabcounter/src/test/java/mozilla/components/ui/tabcounter/TabCounterTest.kt
new file mode 100644
index 0000000000..881a19e47a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/test/java/mozilla/components/ui/tabcounter/TabCounterTest.kt
@@ -0,0 +1,70 @@
+/* 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.ui.tabcounter
+
+import android.content.res.ColorStateList
+import android.view.LayoutInflater
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.tabcounter.TabCounter.Companion.SO_MANY_TABS_OPEN
+import mozilla.components.ui.tabcounter.databinding.MozacUiTabcounterLayoutBinding
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TabCounterTest {
+
+ private lateinit var tabCounter: TabCounter
+ private lateinit var binding: MozacUiTabcounterLayoutBinding
+
+ @Before
+ fun setUp() {
+ tabCounter = TabCounter(testContext)
+ binding =
+ MozacUiTabcounterLayoutBinding.inflate(LayoutInflater.from(testContext), tabCounter)
+ }
+
+ @Test
+ fun `Default tab count is set to zero`() {
+ assertEquals("0", binding.counterText.text)
+ }
+
+ @Test
+ fun `Set tab count as single digit value shows count`() {
+ tabCounter.setCount(1)
+ assertEquals("1", binding.counterText.text)
+ }
+
+ @Test
+ fun `Set tab count as two digit number shows count`() {
+ tabCounter.setCount(99)
+ assertEquals("99", binding.counterText.text)
+ }
+
+ @Test
+ fun `Setting tab count as three digit value shows correct icon`() {
+ tabCounter.setCount(100)
+ assertEquals(SO_MANY_TABS_OPEN, binding.counterText.text)
+ }
+
+ @Test
+ fun `Setting tab color shows correct icon`() {
+ val colorStateList: ColorStateList = mock()
+
+ tabCounter.setColor(colorStateList)
+ assertEquals(binding.counterText.textColors, colorStateList)
+ }
+
+ @Test
+ fun `Toggling the counterMask will set the mask to visible`() {
+ assertEquals(binding.counterMask.visibility, View.GONE)
+ tabCounter.toggleCounterMask(true)
+ assertEquals(binding.counterMask.visibility, View.VISIBLE)
+ }
+}
diff --git a/mobile/android/android-components/components/ui/tabcounter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/ui/tabcounter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/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/ui/tabcounter/src/test/resources/robolectric.properties b/mobile/android/android-components/components/ui/tabcounter/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/tabcounter/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/ui/widgets/README.md b/mobile/android/android-components/components/ui/widgets/README.md
new file mode 100644
index 0000000000..524ef9d794
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > UI > Widgets
+
+The standard set of Mozilla widgets.
+
+## Usage
+
+### 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:ui-widgets:{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/ui/widgets/build.gradle b/mobile/android/android-components/components/ui/widgets/build.gradle
new file mode 100644
index 0000000000..e1ccc51159
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/build.gradle
@@ -0,0 +1,51 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+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'
+ }
+ }
+
+ namespace 'mozilla.components.ui.widgets'
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':concept-base')
+ implementation project(':concept-engine')
+ implementation project(':ui-colors')
+ implementation project(':ui-icons')
+ implementation project(':support-ktx')
+ implementation project(':concept-toolbar')
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.google_material
+ implementation ComponentsDependencies.androidx_swiperefreshlayout
+
+ testImplementation project(":support-test")
+ testImplementation project(':support-test-fakes')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ 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/ui/widgets/proguard-rules.pro b/mobile/android/android-components/components/ui/widgets/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/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/ui/widgets/src/main/AndroidManifest.xml b/mobile/android/android-components/components/ui/widgets/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/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/ui/widgets/src/main/java/mozilla/components/ui/widgets/Extentions.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/Extentions.kt
new file mode 100644
index 0000000000..fb17b7eb5a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/Extentions.kt
@@ -0,0 +1,37 @@
+/* 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.ui.widgets
+
+import android.widget.TextView
+import android.view.View.TEXT_ALIGNMENT_CENTER as CENTER
+
+/**
+ * A shortcut to align buttons' text to center inside AlertDialog.
+ */
+fun androidx.appcompat.app.AlertDialog.withCenterAlignedButtons(): androidx.appcompat.app.AlertDialog {
+ findViewById<TextView>(android.R.id.button1)?.let { it.textAlignment = CENTER }
+ findViewById<TextView>(android.R.id.button2)?.let { it.textAlignment = CENTER }
+ findViewById<TextView>(android.R.id.button3)?.let { it.textAlignment = CENTER }
+ return this
+}
+
+/**
+ * A shortcut to align buttons' text to center inside AlertDialog.
+ *
+ * Important: On Android API levels lower than 24, this method must be called only AFTER the dialog
+ * has been shown. Calling this method prior to displaying the dialog on those API levels will cause
+ * partial initialization of the view, leading to a crash.
+ *
+ * Usage example:
+ * dialog.setOnShowListener {
+ * dialog.withCenterAlignedButtons()
+ * }
+ */
+fun android.app.AlertDialog.withCenterAlignedButtons(): android.app.AlertDialog {
+ findViewById<TextView>(android.R.id.button1)?.let { it.textAlignment = CENTER }
+ findViewById<TextView>(android.R.id.button2)?.let { it.textAlignment = CENTER }
+ findViewById<TextView>(android.R.id.button3)?.let { it.textAlignment = CENTER }
+ return this
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/SnackbarDelegate.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/SnackbarDelegate.kt
new file mode 100644
index 0000000000..8e2dd2fad7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/SnackbarDelegate.kt
@@ -0,0 +1,55 @@
+/* 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.ui.widgets
+
+import android.view.View
+import com.google.android.material.snackbar.Snackbar
+
+/**
+ * Delegate to display a snackbar.
+ */
+interface SnackbarDelegate {
+ /**
+ * Displays a snackbar.
+ *
+ * @param snackBarParentView The view to find a parent from for displaying the Snackbar.
+ * @param text The text to show. Can be formatted text.
+ * @param duration How long to display the message.
+ * @param action String resource to display for the action.
+ * @param listener callback to be invoked when the action is clicked.
+ */
+ fun show(
+ snackBarParentView: View,
+ text: Int,
+ duration: Int,
+ action: Int = 0,
+ listener: ((v: View) -> Unit)? = null,
+ )
+}
+
+/**
+ * Default implementation for [SnackbarDelegate]. Will display a standard default Snackbar.
+ */
+class DefaultSnackbarDelegate : SnackbarDelegate {
+ override fun show(
+ snackBarParentView: View,
+ text: Int,
+ duration: Int,
+ action: Int,
+ listener: ((v: View) -> Unit)?,
+ ) {
+ val snackbar = Snackbar.make(
+ snackBarParentView,
+ text,
+ duration,
+ )
+
+ if (action != 0 && listener != null) {
+ snackbar.setAction(action, listener)
+ }
+
+ snackbar.show()
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/VerticalSwipeRefreshLayout.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/VerticalSwipeRefreshLayout.kt
new file mode 100644
index 0000000000..aa8bc10b67
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/VerticalSwipeRefreshLayout.kt
@@ -0,0 +1,216 @@
+/* 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.ui.widgets
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import androidx.annotation.VisibleForTesting
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import kotlin.math.abs
+
+/**
+ * [SwipeRefreshLayout] that filters only vertical scrolls for triggering pull to refresh.
+ *
+ * Following situations will not trigger pull to refresh:
+ * - a scroll happening more on the horizontal axis
+ * - a scale in/out gesture
+ * - a quick scale gesture
+ *
+ * To control responding to scrolls and showing the pull to refresh throbber or not
+ * use the [View.isEnabled] property.
+ */
+class VerticalSwipeRefreshLayout @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+) : SwipeRefreshLayout(context, attrs) {
+ @VisibleForTesting
+ internal var isQuickScaleInProgress = false
+
+ @VisibleForTesting
+ internal var quickScaleEvents = QuickScaleEvents()
+ private var previousX = 0f
+ private var previousY = 0f
+ private val doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout()
+ private val doubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop
+ private val doubleTapSlopSquare = doubleTapSlop * doubleTapSlop
+
+ @VisibleForTesting
+ internal var hadMultiTouch: Boolean = false
+
+ @VisibleForTesting
+ internal var disallowInterceptTouchEvent = false
+
+ @Suppress("ComplexMethod", "ReturnCount")
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
+ // Setting "isEnabled = false" is recommended for users of this ViewGroup
+ // who who are not interested in the pull to refresh functionality
+ // Setting this easily avoids executing code unneededsly before the check for "canChildScrollUp".
+ if (!isEnabled || disallowInterceptTouchEvent) {
+ return false
+ }
+
+ if (MotionEvent.ACTION_DOWN == event.action) {
+ hadMultiTouch = false
+ }
+
+ // Layman's scale gesture (with two fingers) detector.
+ // Allows for quick, serial inference as opposed to using ScaleGestureDetector
+ // which uses callbacks and would be hard to synchronize in the little time we have.
+ if (event.pointerCount > 1 || hadMultiTouch) {
+ hadMultiTouch = true
+ return false
+ }
+
+ val eventAction = event.action
+
+ // Cleanup if the gesture has been aborted or quick scale just ended/
+ if (MotionEvent.ACTION_CANCEL == eventAction ||
+ (MotionEvent.ACTION_UP == eventAction && isQuickScaleInProgress)
+ ) {
+ forgetQuickScaleEvents()
+ return callSuperOnInterceptTouchEvent(event)
+ }
+
+ // Disable pull to refresh if quick scale is in progress.
+ maybeAddDoubleTapEvent(event)
+ if (isQuickScaleInProgress(quickScaleEvents)) {
+ isQuickScaleInProgress = true
+ return false
+ }
+
+ // Disable pull to refresh if the move was more on the X axis.
+ if (MotionEvent.ACTION_DOWN == eventAction) {
+ previousX = event.x
+ previousY = event.y
+ } else if (MotionEvent.ACTION_MOVE == eventAction) {
+ val xDistance = abs(event.x - previousX)
+ val yDistance = abs(event.y - previousY)
+ previousX = event.x
+ previousY = event.y
+ if (xDistance > yDistance) {
+ return false
+ }
+ }
+
+ return callSuperOnInterceptTouchEvent(event)
+ }
+
+ override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {
+ // Ignoring nested scrolls from descendants.
+ // Allowing descendants to trigger nested scrolls would defeat the purpose of this class
+ // and result in pull to refresh to happen for all movements on the Y axis
+ // (even as part of scale/quick scale gestures) while also doubling the throbber with the overscroll shadow.
+ return if (isEnabled) {
+ return false
+ } else {
+ callSuperOnStartNestedScroll(child, target, nestedScrollAxes)
+ }
+ }
+
+ @SuppressLint("Recycle") // we do recycle the events in forgetQuickScaleEvents()
+ @VisibleForTesting
+ internal fun maybeAddDoubleTapEvent(event: MotionEvent) {
+ val currentEventAction = event.action
+
+ // A double tap event must follow the order:
+ // ACTION_DOWN - ACTION_UP - ACTION_DOWN
+ // all these events happening in an interval defined by a system constant - DOUBLE_TAP_TIMEOUT
+
+ if (MotionEvent.ACTION_DOWN == currentEventAction) {
+ if (quickScaleEvents.upEvent != null) {
+ if (event.eventTime - quickScaleEvents.upEvent!!.eventTime > doubleTapTimeout) {
+ // Too much time passed for the MotionEvents sequence to be considered
+ // a quick scale gesture. Restart counting.
+ forgetQuickScaleEvents()
+ quickScaleEvents.firstDownEvent = MotionEvent.obtain(event)
+ } else {
+ quickScaleEvents.secondDownEvent = MotionEvent.obtain(event)
+ }
+ } else {
+ // This may be the first time the user touches the screen or
+ // the gesture was not finished with ACTION_UP.
+ forgetQuickScaleEvents()
+ quickScaleEvents.firstDownEvent = MotionEvent.obtain(event)
+ }
+ }
+ // For the double tap events series we need ACTION_DOWN first
+ // and then ACTION_UP second.
+ else if (MotionEvent.ACTION_UP == currentEventAction && quickScaleEvents.firstDownEvent != null) {
+ quickScaleEvents.upEvent = MotionEvent.obtain(event)
+ }
+ }
+
+ override fun requestDisallowInterceptTouchEvent(b: Boolean) {
+ // We need to disable Pull to Refresh on this layout be we don't want to propagate the
+ // request to the parent, because they may use the gesture for other purpose, like
+ // propagating it to ToolbarBehavior
+ this.disallowInterceptTouchEvent = b
+ }
+
+ @VisibleForTesting
+ internal fun forgetQuickScaleEvents() {
+ quickScaleEvents.firstDownEvent?.recycle()
+ quickScaleEvents.upEvent?.recycle()
+ quickScaleEvents.secondDownEvent?.recycle()
+ quickScaleEvents.firstDownEvent = null
+ quickScaleEvents.upEvent = null
+ quickScaleEvents.secondDownEvent = null
+
+ isQuickScaleInProgress = false
+ }
+
+ @VisibleForTesting
+ internal fun isQuickScaleInProgress(events: QuickScaleEvents): Boolean {
+ return if (events.isNotNull()) {
+ isQuickScaleInProgress(events.firstDownEvent!!, events.upEvent!!, events.secondDownEvent!!)
+ } else {
+ false
+ }
+ }
+
+ // Method closely following GestureDetectorCompat#isConsideredDoubleTap.
+ // Allows for serial inference of double taps as opposed to using callbacks.
+ @VisibleForTesting
+ internal fun isQuickScaleInProgress(
+ firstDown: MotionEvent,
+ firstUp: MotionEvent,
+ secondDown: MotionEvent,
+ ): Boolean {
+ if (secondDown.eventTime - firstUp.eventTime > doubleTapTimeout) {
+ return false
+ }
+
+ val deltaX = firstDown.x.toInt() - secondDown.x.toInt()
+ val deltaY = firstDown.y.toInt() - secondDown.y.toInt()
+
+ return deltaX * deltaX + deltaY * deltaY < doubleTapSlopSquare
+ }
+
+ @VisibleForTesting
+ internal fun callSuperOnInterceptTouchEvent(event: MotionEvent) =
+ super.onInterceptTouchEvent(event)
+
+ @VisibleForTesting
+ internal fun callSuperOnStartNestedScroll(child: View, target: View, nestedScrollAxes: Int) =
+ super.onStartNestedScroll(child, target, nestedScrollAxes)
+
+ private fun QuickScaleEvents.isNotNull(): Boolean {
+ return firstDownEvent != null && upEvent != null && secondDownEvent != null
+ }
+
+ /**
+ * Wrapper over the MotionEvents that compose a quickScale gesture.
+ */
+ @VisibleForTesting
+ internal data class QuickScaleEvents(
+ var firstDownEvent: MotionEvent? = null,
+ var upEvent: MotionEvent? = null,
+ var secondDownEvent: MotionEvent? = null,
+ )
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/WidgetSiteItemView.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/WidgetSiteItemView.kt
new file mode 100644
index 0000000000..0eca198957
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/WidgetSiteItemView.kt
@@ -0,0 +1,106 @@
+/* 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.ui.widgets
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.appcompat.content.res.AppCompatResources.getDrawable
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+
+/**
+ * Shared UI widget for showing a website in a list of websites,
+ * such as in bookmarks, history, site exceptions, or collections.
+ */
+class WidgetSiteItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private val labelView: TextView by lazy { findViewById<TextView>(R.id.label) }
+ private val captionView: TextView by lazy { findViewById<TextView>(R.id.caption) }
+ private val iconWrapper: FrameLayout by lazy { findViewById<FrameLayout>(R.id.favicon_wrapper) }
+ private val secondaryButton: ImageButton by lazy { findViewById<ImageButton>(R.id.secondary_button) }
+
+ /**
+ * ImageView that should display favicons.
+ */
+ val iconView: ImageView by lazy { findViewById<ImageView>(R.id.favicon) }
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.mozac_widget_site_item, this, true)
+ }
+
+ /**
+ * Sets the text displayed inside of the site item view.
+ *
+ * @param label Main label text, such as a site title.
+ * @param caption Sub caption text, such as a URL. If null, the caption is hidden.
+ */
+ fun setText(label: CharSequence, caption: CharSequence?) {
+ labelView.text = label
+ captionView.text = caption
+ captionView.isVisible = caption != null
+ }
+
+ /**
+ * Add a view that will overlay the favicon, such as a checkmark.
+ */
+ fun addIconOverlay(overlay: View) {
+ iconWrapper.addView(overlay)
+ }
+
+ /**
+ * Add a secondary button, such as an overflow menu.
+ *
+ * @param icon Drawable to display in the button.
+ * @param contentDescription Accessible description of the button's purpose.
+ * @param onClickListener Listener called when the button is clicked.
+ */
+ fun setSecondaryButton(
+ icon: Drawable?,
+ contentDescription: CharSequence,
+ onClickListener: (View) -> Unit,
+ ) {
+ secondaryButton.isVisible = true
+ secondaryButton.setImageDrawable(icon)
+ secondaryButton.contentDescription = contentDescription
+ secondaryButton.setOnClickListener(onClickListener)
+ }
+
+ /**
+ * Add a secondary button, such as an overflow menu.
+ *
+ * @param icon Drawable to display in the button.
+ * @param contentDescription Accessible description of the button's purpose.
+ * @param onClickListener Listener called when the button is clicked.
+ */
+ fun setSecondaryButton(
+ @DrawableRes icon: Int,
+ @StringRes contentDescription: Int,
+ onClickListener: (View) -> Unit,
+ ) = setSecondaryButton(
+ icon = getDrawable(context, icon),
+ contentDescription = context.getString(contentDescription),
+ onClickListener = onClickListener,
+ )
+
+ /**
+ * Removes the secondary button if it was previously set in [setSecondaryButton].
+ */
+ fun removeSecondaryButton() {
+ secondaryButton.isVisible = false
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetector.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetector.kt
new file mode 100644
index 0000000000..b15df65078
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetector.kt
@@ -0,0 +1,192 @@
+/* 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.ui.widgets.behavior
+
+import android.content.Context
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.base.crash.CrashReporting
+import kotlin.math.abs
+
+/**
+ * Wraps exceptions that are caught by [BrowserGestureDetector].
+ * Instances of this class are submitted via [CrashReporting]. This wrapping helps easily identify
+ * exceptions related to [BrowserGestureDetector].
+ */
+internal class BrowserGestureDetectorException(e: Throwable) : Throwable(e)
+
+/**
+ * Custom [MotionEvent] gestures detector with scroll / zoom callbacks.
+ *
+ * Favors zoom gestures in detriment of the scroll gestures with:
+ * - higher sensitivity for multi-finger zoom gestures
+ * - ignoring scrolls if zoom is in progress
+ *
+ * @param applicationContext context used for registering internal gesture listeners.
+ * @param listener client interested in zoom / scroll events.
+ */
+internal class BrowserGestureDetector(
+ applicationContext: Context,
+ listener: GesturesListener,
+ private val crashReporting: CrashReporting? = null,
+) {
+ @VisibleForTesting
+ internal var gestureDetector = GestureDetector(
+ applicationContext,
+ CustomScrollDetectorListener { previousEvent: MotionEvent?, currentEvent: MotionEvent, distanceX, distanceY ->
+ run {
+ listener.onScroll?.invoke(distanceX, distanceY)
+
+ // We got many crashes because of the initial event - ACTION_DOWN being null.
+ // Investigations to be continued in android-components/issues/8552.
+ // In the meantime we'll protect against this with a simple null check.
+ if (previousEvent != null) {
+ if (abs(currentEvent.y - previousEvent.y) >= abs(currentEvent.x - previousEvent.x)) {
+ listener.onVerticalScroll?.invoke(distanceY)
+ } else {
+ listener.onHorizontalScroll?.invoke(distanceX)
+ }
+ }
+ }
+ },
+ )
+
+ @VisibleForTesting
+ internal var scaleGestureDetector = ScaleGestureDetector(
+ applicationContext,
+ CustomScaleDetectorListener(
+ listener.onScaleBegin ?: {},
+ listener.onScale ?: {},
+ listener.onScaleEnd ?: {},
+ ),
+ )
+
+ /**
+ * Accepts MotionEvents and dispatches zoom / scroll events to the registered listener when appropriate.
+ *
+ * Applications should pass a complete and consistent event stream to this method.
+ * A complete and consistent event stream involves all MotionEvents from the initial ACTION_DOWN
+ * to the final ACTION_UP or ACTION_CANCEL.
+ *
+ * @return if the event was handled by any of the registered detectors
+ */
+ @Suppress("ComplexCondition")
+ internal fun handleTouchEvent(event: MotionEvent): Boolean {
+ val eventAction = event.actionMasked
+
+ // A double tap for a quick scale gesture (quick double tap followed by a drag)
+ // would trigger a ACTION_CANCEL event before the MOVE_EVENT.
+ // This would prevent the scale detector from properly inferring the movement.
+ // We'll want to ignore ACTION_CANCEL but process the next stream of events.
+ if (eventAction != MotionEvent.ACTION_CANCEL) {
+ scaleGestureDetector.onTouchEvent(event)
+ }
+
+ // Ignore scrolling if zooming is already in progress.
+ // Always pass motion begin / end events just to have the detector ready
+ // to infer scrolls when the scale gesture ended.
+ return if (!scaleGestureDetector.isInProgress ||
+ eventAction == MotionEvent.ACTION_DOWN ||
+ eventAction == MotionEvent.ACTION_UP ||
+ eventAction == MotionEvent.ACTION_CANCEL
+ ) {
+ @Suppress("TooGenericExceptionCaught")
+ try {
+ gestureDetector.onTouchEvent(event)
+ } catch (e: Exception) {
+ crashReporting?.submitCaughtException(BrowserGestureDetectorException(e))
+ false
+ }
+ } else {
+ false
+ }
+ }
+
+ /**
+ * A convenience containing listeners for zoom / scroll events
+ *
+ * Provide implementation for the events you are interested in.
+ * The others will be no-op.
+ */
+ internal class GesturesListener(
+ /**
+ * Responds to scroll events for a gesture in progress.
+ * The distance in x and y is also supplied for convenience.
+ */
+ val onScroll: ((distanceX: Float, distanceY: Float) -> Unit)? = { _, _ -> run {} },
+
+ /**
+ * Responds to an in progress scroll occuring more on the vertical axis.
+ * The scroll distance is also supplied for convenience.
+ */
+ val onVerticalScroll: ((distance: Float) -> Unit)? = {},
+
+ /**
+ * Responds to an in progress scroll occurring more on the horizontal axis.
+ * The scroll distance is also supplied for convenience.
+ */
+ val onHorizontalScroll: ((distance: Float) -> Unit)? = {},
+
+ /**
+ * Responds to the the beginning of a new scale gesture.
+ * Reported by new pointers going down.
+ */
+ val onScaleBegin: ((scaleFactor: Float) -> Unit)? = {},
+
+ /**
+ * Responds to scaling events for a gesture in progress.
+ * The scaling factor is also supplied for convenience.
+ * This value is represents the difference from the previous scale event to the current event.
+ */
+ val onScale: ((scaleFactor: Float) -> Unit)? = {},
+
+ /**
+ * Responds to the end of a scale gesture.
+ * Reported by existing pointers going up.
+ */
+ val onScaleEnd: ((scaleFactor: Float) -> Unit)? = {},
+ )
+
+ private class CustomScrollDetectorListener(
+ val onScrolling: (
+ previousEvent: MotionEvent?,
+ currentEvent: MotionEvent,
+ distanceX: Float,
+ distanceY: Float,
+ ) -> Unit,
+ ) : GestureDetector.SimpleOnGestureListener() {
+ override fun onScroll(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float,
+ ): Boolean {
+ onScrolling(e1, e2, distanceX, distanceY)
+ return true
+ }
+ }
+
+ private class CustomScaleDetectorListener(
+ val onScaleBegin: (scaleFactor: Float) -> Unit = {},
+ val onScale: (scaleFactor: Float) -> Unit = {},
+ val onScaleEnd: (scaleFactor: Float) -> Unit = {},
+ ) : ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
+ onScaleBegin(detector.scaleFactor)
+ return true
+ }
+
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ onScale(detector.scaleFactor)
+ return true
+ }
+
+ override fun onScaleEnd(detector: ScaleGestureDetector) {
+ onScaleEnd(detector.scaleFactor)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewClippingBehavior.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewClippingBehavior.kt
new file mode 100644
index 0000000000..d0f532bfbc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewClippingBehavior.kt
@@ -0,0 +1,90 @@
+/* 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.ui.widgets.behavior
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.toolbar.ScrollableToolbar
+import mozilla.components.support.ktx.android.view.findViewInHierarchy
+import kotlin.math.roundToInt
+
+/**
+ * A [CoordinatorLayout.Behavior] implementation that allows the [EngineView] to automatically
+ * size itself in relation to the Y translation of the [ScrollableToolbar].
+ *
+ * This is useful for dynamic [ScrollableToolbar]s ensuring the web content is displayed immediately
+ * below / above the toolbar even when that is animated.
+ *
+ * @param context [Context] used for various Android interactions
+ * @param attrs XML set attributes configuring this
+ * @param engineViewParent NestedScrollingChild parent of the [EngineView]
+ * @param toolbarHeight size of [ScrollableToolbar] when it is placed above the [EngineView]
+ * @param toolbarPosition whether the [ScrollableToolbar] is placed above or below the [EngineView]
+ */
+class EngineViewClippingBehavior(
+ context: Context?,
+ attrs: AttributeSet?,
+ engineViewParent: View,
+ toolbarHeight: Int,
+ toolbarPosition: ToolbarPosition,
+) : CoordinatorLayout.Behavior<View>(context, attrs) {
+
+ @VisibleForTesting
+ internal val engineView = engineViewParent.findViewInHierarchy { it is EngineView } as EngineView?
+
+ @VisibleForTesting
+ internal var toolbarChangedAction: (Float) -> Unit?
+ private val bottomToolbarChangedAction = { newToolbarTranslationY: Float ->
+ if (!newToolbarTranslationY.isNaN()) {
+ engineView?.setVerticalClipping(-newToolbarTranslationY.roundToInt())
+ }
+ }
+ private val topToolbarChangedAction = { newToolbarTranslationY: Float ->
+ // the top toolbar is translated upwards when collapsing-> all values received are 0 or negative
+ engineView?.let {
+ it.setVerticalClipping(newToolbarTranslationY.roundToInt())
+ // Need to add the toolbarHeight to effectively place the engineView below the toolbar.
+ engineViewParent.translationY = newToolbarTranslationY + toolbarHeight
+ }
+ }
+
+ init {
+ toolbarChangedAction = if (toolbarPosition == ToolbarPosition.TOP) {
+ topToolbarChangedAction
+ } else {
+ bottomToolbarChangedAction
+ }
+ }
+
+ override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
+ if (dependency is ScrollableToolbar) {
+ return true
+ }
+
+ return super.layoutDependsOn(parent, child, dependency)
+ }
+
+ /**
+ * Apply vertical clipping to [EngineView]. This requires [EngineViewClippingBehavior] to be set
+ * in/on the [EngineView] or its parent. Must be a direct descending child of [CoordinatorLayout].
+ */
+ override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
+ toolbarChangedAction.invoke(dependency.translationY)
+
+ return true
+ }
+}
+
+/**
+ * Where the toolbar is placed on the screen.
+ */
+enum class ToolbarPosition {
+ TOP,
+ BOTTOM,
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.kt
new file mode 100644
index 0000000000..08da7e5064
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehavior.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.ui.widgets.behavior
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.ViewCompat
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.support.ktx.android.view.findViewInHierarchy
+
+/**
+ * Where the view is placed on the screen.
+ */
+enum class ViewPosition {
+ TOP,
+ BOTTOM,
+}
+
+/**
+ * A [CoordinatorLayout.Behavior] implementation to be used when placing [View] at the bottom of the screen.
+ *
+ * This is safe to use even if the [View] may be added / removed from a parent layout later
+ * or if it could have Visibility.GONE set.
+ *
+ * This implementation will:
+ * - Show/Hide the [View] automatically when scrolling vertically.
+ * - Snap the [View] to be hidden or visible when the user stops scrolling.
+ */
+class EngineViewScrollingBehavior(
+ val context: Context?,
+ attrs: AttributeSet?,
+ private val viewPosition: ViewPosition,
+ private val crashReporting: CrashReporting? = null,
+) : CoordinatorLayout.Behavior<View>(context, attrs) {
+ // This implementation is heavily based on this blog article:
+ // https://android.jlelse.eu/scroll-your-bottom-navigation-view-away-with-10-lines-of-code-346f1ed40e9e
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var shouldSnapAfterScroll: Boolean = false
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var startedScroll = false
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var isScrollEnabled = false
+
+ /**
+ * Reference to [EngineView] used to check user's [android.view.MotionEvent]s.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var engineView: EngineView? = null
+
+ /**
+ * Reference to the actual [View] that we'll animate.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var dynamicScrollView: View? = null
+
+ /**
+ * Depending on how user's touch was consumed by EngineView / current website,
+ *
+ * we will animate the dynamic navigation bar if:
+ * - touches were used for zooming / panning operations in the website.
+ *
+ * We will do nothing if:
+ * - the website is not scrollable
+ * - the website handles the touch events itself through it's own touch event listeners.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val shouldScroll: Boolean
+ get() = engineView?.getInputResultDetail()?.let {
+ (it.canScrollToBottom() || it.canScrollToTop()) && isScrollEnabled
+ } ?: false
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var gesturesDetector: BrowserGestureDetector = createGestureDetector()
+
+ @VisibleForTesting
+ internal var yTranslator: ViewYTranslator = createYTranslationStrategy()
+
+ private fun createYTranslationStrategy() = ViewYTranslator(viewPosition)
+
+ override fun onStartNestedScroll(
+ coordinatorLayout: CoordinatorLayout,
+ child: View,
+ directTargetChild: View,
+ target: View,
+ axes: Int,
+ type: Int,
+ ): Boolean {
+ return if (dynamicScrollView != null) {
+ startNestedScroll(axes, type, child)
+ } else {
+ return false // not interested in subsequent scroll events
+ }
+ }
+
+ override fun onStopNestedScroll(
+ coordinatorLayout: CoordinatorLayout,
+ child: View,
+ target: View,
+ type: Int,
+ ) {
+ if (dynamicScrollView != null) {
+ stopNestedScroll(type, child)
+ }
+ }
+
+ override fun onInterceptTouchEvent(
+ parent: CoordinatorLayout,
+ child: View,
+ ev: MotionEvent,
+ ): Boolean {
+ if (dynamicScrollView != null) {
+ gesturesDetector.handleTouchEvent(ev)
+ }
+ return false // allow events to be passed to below listeners
+ }
+
+ override fun onLayoutChild(
+ parent: CoordinatorLayout,
+ child: View,
+ layoutDirection: Int,
+ ): Boolean {
+ dynamicScrollView = child
+ engineView = parent.findViewInHierarchy { it is EngineView } as? EngineView
+
+ return super.onLayoutChild(parent, child, layoutDirection)
+ }
+
+ /**
+ * Used to expand the [View]
+ */
+ fun forceExpand(view: View) {
+ yTranslator.expandWithAnimation(view)
+ }
+
+ /**
+ * Used to collapse the [View]
+ */
+ fun forceCollapse(view: View) {
+ yTranslator.collapseWithAnimation(view)
+ }
+
+ /**
+ * Allow this view to be animated.
+ *
+ * @see disableScrolling
+ */
+ fun enableScrolling() {
+ isScrollEnabled = true
+ }
+
+ /**
+ * Disable scrolling of the view irrespective of the intrinsic checks.
+ *
+ * @see enableScrolling
+ */
+ fun disableScrolling() {
+ isScrollEnabled = false
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun tryToScrollVertically(distance: Float) {
+ dynamicScrollView?.let { view ->
+ if (shouldScroll && startedScroll) {
+ yTranslator.translate(view, distance)
+ } else if (engineView?.getInputResultDetail()?.isTouchHandlingUnknown() == false) {
+ // Force expand the view if the user scrolled up, it is not already expanded and
+ // an animation to expand it is not already in progress,
+ // otherwise the user could get stuck in a state where they cannot show the view
+ // See https://github.com/mozilla-mobile/android-components/issues/7101
+ yTranslator.forceExpandIfNotAlready(view, distance)
+ }
+ }
+ }
+
+ /**
+ * Helper function to ease testing.
+ * (Re)Initializes the [BrowserGestureDetector] in a new context.
+ *
+ * Useful in spied behaviors, to ensure callbacks are of the spy and not of the initially created object
+ * if the passed in argument is the result of [createGestureDetector].
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun initGesturesDetector(detector: BrowserGestureDetector) {
+ gesturesDetector = detector
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun createGestureDetector() =
+ BrowserGestureDetector(
+ context!!,
+ BrowserGestureDetector.GesturesListener(
+ onVerticalScroll = ::tryToScrollVertically,
+ onScaleBegin = {
+ // Scale shouldn't animate the view but a small y translation is still possible
+ // because of a previous scroll. Try to be swift about such an in progress animation.
+ yTranslator.snapImmediately(dynamicScrollView)
+ },
+ ),
+ crashReporting = crashReporting,
+ )
+
+ @VisibleForTesting
+ internal fun startNestedScroll(axes: Int, type: Int, view: View): Boolean {
+ return if (shouldScroll && axes == ViewCompat.SCROLL_AXIS_VERTICAL) {
+ startedScroll = true
+ shouldSnapAfterScroll = type == ViewCompat.TYPE_TOUCH
+ yTranslator.cancelInProgressTranslation()
+ true
+ } else if (engineView?.getInputResultDetail()?.isTouchUnhandled() == true) {
+ // Force expand the view if event is unhandled, otherwise user could get stuck in a
+ // state where they cannot show the view
+ yTranslator.cancelInProgressTranslation()
+ yTranslator.expandWithAnimation(view)
+ false
+ } else {
+ false
+ }
+ }
+
+ @VisibleForTesting
+ internal fun stopNestedScroll(type: Int, view: View) {
+ startedScroll = false
+ if (shouldSnapAfterScroll || type == ViewCompat.TYPE_NON_TOUCH) {
+ yTranslator.snapWithAnimation(view)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslationStrategy.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslationStrategy.kt
new file mode 100644
index 0000000000..8311f2d21a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslationStrategy.kt
@@ -0,0 +1,189 @@
+/* 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.ui.widgets.behavior
+
+import android.animation.ValueAnimator
+import android.view.View
+import android.view.animation.DecelerateInterpolator
+import androidx.annotation.VisibleForTesting
+import kotlin.math.max
+import kotlin.math.min
+
+@VisibleForTesting
+internal const val SNAP_ANIMATION_DURATION = 150L
+
+/**
+ * Helper class with methods for different behaviors for when translating a [View] on the Y axis.
+ */
+internal abstract class ViewYTranslationStrategy {
+ @VisibleForTesting
+ var animator = ValueAnimator().apply {
+ interpolator = DecelerateInterpolator()
+ duration = SNAP_ANIMATION_DURATION
+ }
+
+ /**
+ * Snap the [View] to be collapsed or expanded, depending on whatever state is closer
+ * over a short amount of time.
+ */
+ abstract fun snapWithAnimation(view: View)
+
+ /**
+ * Snap the [View] to be collapsed or expanded, depending on whatever state is closer immediately.
+ */
+ abstract fun snapImmediately(view: View?)
+
+ /**
+ * Translate the [View] to it's full visible height.
+ */
+ abstract fun expandWithAnimation(view: View)
+
+ /**
+ * Force expanding the [View] depending on the [distance] value that should be translated
+ * cancelling any other translation already in progress.
+ */
+ abstract fun forceExpandWithAnimation(view: View, distance: Float)
+
+ /**
+ * Translate the [View] to it's full 0 visible height.
+ */
+ abstract fun collapseWithAnimation(view: View)
+
+ /**
+ * Translate [view] immediately to the specified [distance] amount (positive or negative).
+ */
+ abstract fun translate(view: View, distance: Float)
+
+ /**
+ * Translate [view] to the indicated [targetTranslationY] vaue over a short amount of time.
+ */
+ open fun animateToTranslationY(view: View, targetTranslationY: Float) = with(animator) {
+ addUpdateListener { view.translationY = it.animatedValue as Float }
+ setFloatValues(view.translationY, targetTranslationY)
+ start()
+ }
+
+ /**
+ * Cancel any translation animations currently in progress.
+ */
+ fun cancelInProgressTranslation() = animator.cancel()
+}
+
+/**
+ * Helper class containing methods for translating a [View] on the Y axis
+ * between 0 and [View.getHeight]
+ */
+internal class BottomViewBehaviorStrategy : ViewYTranslationStrategy() {
+ @VisibleForTesting
+ internal var wasLastExpanding = false
+
+ override fun snapWithAnimation(view: View) {
+ if (view.translationY >= (view.height / 2f)) {
+ collapseWithAnimation(view)
+ } else {
+ expandWithAnimation(view)
+ }
+ }
+
+ override fun snapImmediately(view: View?) {
+ if (animator.isStarted) {
+ animator.end()
+ } else {
+ view?.apply {
+ translationY = if (translationY >= height / 2) {
+ height.toFloat()
+ } else {
+ 0f
+ }
+ }
+ }
+ }
+
+ override fun expandWithAnimation(view: View) {
+ animateToTranslationY(view, 0f)
+ }
+
+ override fun forceExpandWithAnimation(view: View, distance: Float) {
+ val shouldExpandToolbar = distance < 0
+ val isToolbarExpanded = view.translationY == 0f
+ if (shouldExpandToolbar && !isToolbarExpanded && !wasLastExpanding) {
+ animator.cancel()
+ expandWithAnimation(view)
+ }
+ }
+
+ override fun collapseWithAnimation(view: View) {
+ animateToTranslationY(view, view.height.toFloat())
+ }
+
+ override fun translate(view: View, distance: Float) {
+ view.translationY =
+ max(0f, min(view.height.toFloat(), view.translationY + distance))
+ }
+
+ override fun animateToTranslationY(view: View, targetTranslationY: Float) {
+ wasLastExpanding = targetTranslationY <= view.translationY
+ super.animateToTranslationY(view, targetTranslationY)
+ }
+}
+
+/**
+ * Helper class containing methods for translating a [View] on the Y axis
+ * between -[View.getHeight] and 0.
+ */
+internal class TopViewBehaviorStrategy : ViewYTranslationStrategy() {
+ @VisibleForTesting
+ internal var wasLastExpanding = false
+
+ override fun snapWithAnimation(view: View) {
+ if (view.translationY >= -(view.height / 2f)) {
+ expandWithAnimation(view)
+ } else {
+ collapseWithAnimation(view)
+ }
+ }
+
+ override fun snapImmediately(view: View?) {
+ if (animator.isStarted) {
+ animator.end()
+ } else {
+ view?.apply {
+ translationY = if (translationY >= -height / 2) {
+ 0f
+ } else {
+ -height.toFloat()
+ }
+ }
+ }
+ }
+
+ override fun expandWithAnimation(view: View) {
+ animateToTranslationY(view, 0f)
+ }
+
+ override fun forceExpandWithAnimation(view: View, distance: Float) {
+ val isExpandingInProgress = animator.isStarted && wasLastExpanding
+ val shouldExpandToolbar = distance < 0
+ val isToolbarExpanded = view.translationY == 0f
+ if (shouldExpandToolbar && !isToolbarExpanded && !isExpandingInProgress) {
+ animator.cancel()
+ expandWithAnimation(view)
+ }
+ }
+
+ override fun collapseWithAnimation(view: View) {
+ animateToTranslationY(view, -view.height.toFloat())
+ }
+
+ override fun translate(view: View, distance: Float) {
+ view.translationY =
+ min(0f, max(-view.height.toFloat(), view.translationY - distance))
+ }
+
+ override fun animateToTranslationY(view: View, targetTranslationY: Float) {
+ wasLastExpanding = targetTranslationY >= view.translationY
+ super.animateToTranslationY(view, targetTranslationY)
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslator.kt b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslator.kt
new file mode 100644
index 0000000000..042b810b40
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/java/mozilla/components/ui/widgets/behavior/ViewYTranslator.kt
@@ -0,0 +1,81 @@
+/* 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.ui.widgets.behavior
+
+import android.view.View
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Helper class with methods for translating on the Y axis a top / bottom [View].
+ *
+ * @param viewPosition whether the view is displayed immediately at the top of the screen or
+ * immediately at the bottom. This affects how it will be translated:
+ * - if place at the bottom it will be Y translated between 0 and [View.getHeight]
+ * - if place at the top it will be Y translated between -[View.getHeight] and 0
+ */
+class ViewYTranslator(viewPosition: ViewPosition) {
+ @VisibleForTesting
+ internal var strategy = getTranslationStrategy(viewPosition)
+
+ /**
+ * Snap the [View] to be collapsed or expanded, depending on whatever state is closer
+ * over a short amount of time.
+ */
+ internal fun snapWithAnimation(view: View) {
+ strategy.snapWithAnimation(view)
+ }
+
+ /**
+ * Snap the [View] to be collapsed or expanded, depending on whatever state is closer immediately.
+ */
+ fun snapImmediately(view: View?) {
+ strategy.snapImmediately(view)
+ }
+
+ /**
+ * Translate the [View] to it's full visible height over a short amount of time.
+ */
+ internal fun expandWithAnimation(view: View) {
+ strategy.expandWithAnimation(view)
+ }
+
+ /**
+ * Translate the [View] to be hidden from view over a short amount of time.
+ */
+ internal fun collapseWithAnimation(view: View) {
+ strategy.collapseWithAnimation(view)
+ }
+
+ /**
+ * Force expanding the [View] depending on the [distance] value that should be translated
+ * cancelling any other translation already in progress.
+ */
+ fun forceExpandIfNotAlready(view: View, distance: Float) {
+ strategy.forceExpandWithAnimation(view, distance)
+ }
+
+ /**
+ * Translate [view] immediately to the specified [distance] amount (positive or negative).
+ */
+ fun translate(view: View, distance: Float) {
+ strategy.translate(view, distance)
+ }
+
+ /**
+ * Cancel any translation animations currently in progress.
+ */
+ fun cancelInProgressTranslation() {
+ strategy.cancelInProgressTranslation()
+ }
+
+ @VisibleForTesting
+ internal fun getTranslationStrategy(viewPosition: ViewPosition): ViewYTranslationStrategy {
+ return if (viewPosition == ViewPosition.TOP) {
+ TopViewBehaviorStrategy()
+ } else {
+ BottomViewBehaviorStrategy()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/drawable/mozac_widget_favicon_background.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/drawable/mozac_widget_favicon_background.xml
new file mode 100644
index 0000000000..93e6f01141
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/drawable/mozac_widget_favicon_background.xml
@@ -0,0 +1,17 @@
+<?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/. -->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:shape="rectangle">
+ <corners android:radius="4dp" />
+ <solid
+ android:color="?mozac_widget_favicon_background_color"
+ tools:color="@color/photonWhite" />
+ <stroke
+ android:width="1dp"
+ android:color="?mozac_widget_favicon_border_color"
+ tools:color="@color/photonLightGrey30" />
+</shape>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/drawable/rounded_button_background.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/drawable/rounded_button_background.xml
new file mode 100644
index 0000000000..260a02530d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/drawable/rounded_button_background.xml
@@ -0,0 +1,18 @@
+<?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/. -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape>
+ <solid android:color="#000000" />
+ <corners android:radius="4dp" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="4dp" />
+ </shape>
+ </item>
+</ripple>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/layout/mozac_widget_site_item.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/layout/mozac_widget_site_item.xml
new file mode 100644
index 0000000000..3ddfd7a2c3
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/layout/mozac_widget_site_item.xml
@@ -0,0 +1,77 @@
+<?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/. -->
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/mozac_widget_site_item_height"
+ android:background="?android:attr/selectableItemBackground">
+
+ <FrameLayout
+ android:id="@+id/favicon_wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/label">
+ <ImageView
+ android:id="@+id/favicon"
+ style="@style/Mozac.Widgets.Favicon"
+ android:importantForAccessibility="no"
+ tools:src="@android:drawable/ic_secure" />
+ </FrameLayout>
+
+ <TextView
+ android:id="@+id/label"
+ style="@style/Mozac.Widgets.SiteItem.Label"
+ android:layout_width="0dp"
+ tools:textColor="#20123A"
+ tools:text="Example site"
+ app:layout_goneMarginEnd="16dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/caption"
+ app:layout_constraintStart_toEndOf="@id/favicon_wrapper"
+ app:layout_constraintEnd_toStartOf="@id/secondary_button"
+ app:layout_constraintVertical_chainStyle="packed" />
+
+ <TextView
+ android:id="@+id/caption"
+ style="@style/Mozac.Widgets.SiteItem.Caption"
+ android:layout_width="0dp"
+ android:layout_marginTop="2dp"
+ tools:text="https://example.com/"
+ tools:textColor="@color/photonLightGrey90"
+ app:layout_constraintEnd_toEndOf="@id/label"
+ app:layout_constraintStart_toStartOf="@id/label"
+ app:layout_constraintTop_toBottomOf="@id/label"
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+ <ImageButton
+ android:id="@+id/secondary_button"
+ android:layout_width="@dimen/mozac_widget_site_item_secondary_button_size"
+ android:layout_height="@dimen/mozac_widget_site_item_secondary_button_size"
+ android:padding="@dimen/mozac_widget_site_item_secondary_button_padding"
+ android:layout_marginStart="12dp"
+ android:layout_marginEnd="12dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:visibility="gone"
+ tools:visibility="visible"
+ tools:src="@drawable/mozac_ic_ellipsis_vertical_24"
+ tools:ignore="ContentDescription"
+ tools:tint="#20123A"
+ app:tint="?attr/mozac_primary_text_color"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/label"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+</merge>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..cdde258cef
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">ምስል ወደ ቅንጥብ ሰሌዳ ተቀድቷል</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..eb2045ed67
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ar/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">نُسخت الصورة إلى الحافظة</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..013fbec0b8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">La imaxe copióse al cartafueyu</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..706d647459
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">عکس کلیپ‌بوردا کوپی اولدو</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..7096e05e05
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Відарыс скапіяваны ў буфер абмену</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..9c2defef43
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Изображението е копирано</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..3ae6d8340a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Skeudenn eilet er golver</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..b0e64d0437
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Slika je kopirana u privremenu memoriju</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..75a4596b48
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">S’ha copiat la imatge al porta-retalls</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..dee19cb2a1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Xwachib\'ëx ri wachib\'äl pa molwuj</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..dc5ae642c1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Fiura cupiata in u preme’papei</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..039f1848dc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Obrázek zkopírován do schránky</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..274e4c8a92
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Copïwyd delwedd i’r clipfwrdd</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..1e8162dcf1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Billede kopieret til udklipsholder</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..a6f23ccb4a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Grafik in Zwischenablage kopiert</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..5119090d13
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Wobraz jo se kopěrował do mjazywótkłada</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..57da3ec96d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Η εικόνα αντιγράφτηκε στο πρόχειρο</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..638ee8543e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Image copied to clipboard</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..638ee8543e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Image copied to clipboard</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..9cc052f510
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Bildo kopiita al la tondujo</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..06f644e903
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagen copiada al portapapeles</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..06f644e903
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagen copiada al portapapeles</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..06f644e903
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagen copiada al portapapeles</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..06f644e903
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagen copiada al portapapeles</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..06f644e903
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagen copiada al portapapeles</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..b77f8fe2fc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Pilt kopeeriti vahemällu</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..18039c3b03
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Irudia arbelean kopiatu da</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..4d9c243f3c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">تصویر به تخته‌گیره رونوشت شد</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..426de77fe1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Kuva kopioitu leikepöydälle</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..c41cec7a23
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Image copiée dans le presse-papiers</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..5fb71c5b51
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagjin copiade intes notis</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..db7c6fa54d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Ofbylding nei klamboerd kopiearre</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..a2c04bd312
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Chaidh lethbhreac dhen dealbh a chur air an stòr-bhòrd</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..3e7252778d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Copiouse a imaxe ao portapapeis</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..af5e3ba7cb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Embohasa ta’ãnga kuatiajokohápe</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..dbfcb2d809
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Slika je kopirana u međuspremnik</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..473527a6cd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Wobraz je so do mjezyskłada kopěrował</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..0ca081817d
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Kép vágólapra másolva</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..4567efaed4
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Պատկերը պատճենվել է սեղմատախտակին</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..6b8f02500b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagine copiate al area de transferentia</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..4b6807e51e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Gambar disalin ke papan klip</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..c2e2bb5714
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Mynd afrituð á klippispjald</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..79c58ec52e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Immagine copiata negli appunti</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..85bf7a942a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">התמונה הועתקה ללוח</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..805eacfc87
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">画像をクリップボードにコピーしました</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..8de04f6263
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">სურათის ასლი აღებულია</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..cb2c90ba5b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Súwret almasıw buferine kóshirip alındı</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..093f1d2616
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Tugna tettwanɣel ɣef wafus</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..80b99ef483
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Сурет алмасу буферіне көшірілді</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..b299c8f6b7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Wêne li panoyê hate kopîkirin</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..5d6ebf83e7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">클립보드에 이미지 복사됨</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..ab066dba6f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">ສຳເນົາຮູບໃສ່ຄລິບບອດແລ້ວ</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..bef36125f1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Bilde kopiert til utklippstavlen</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..06070807bb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Afbeelding naar klembord gekopieerd</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..16c41f8d38
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Bilde kopiert til utklippstavla</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..131c779121
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imatge copiat al quichapapièrs</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..03300db934
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">ਚਿੱਤਰ ਨੂੰ ਕਲਿੱਪਬੋਰਡ ਲਈ ਕਾਪੀ ਕੀਤਾ</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..cb4e8c879c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">کاپی کیتی گئی</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..8a79fb5c5e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Skopiowano obraz do schowka</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..223e60bc6a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagem copiada para área de transferência</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..48dc017c65
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imagem copiada para a área de transferência</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..8b428f02b1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Copià il maletg en l\'archiv provisoric</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..be7fc12e65
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ro/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Imaginea a fost copiată în clipboard</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..35ac3be80a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Изображение скопировано в буфер обмена</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..d945ea02c5
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">ᱨᱮᱴᱚᱯᱵᱚᱰ ᱨᱮ ᱪᱤᱛᱟᱹᱨ ᱱᱚᱠᱚᱞ ᱮᱱᱟ</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..6d122f7e8b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Immàgine copiada in punta de billete</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..95d46d5ddd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">රූපය පසුරුපුවරුවට පිටපත් විය</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..02dc6628cb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Obrázok bol skopírovaný do schránky</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..da370ffcb7
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">تصویر کلپ بورڈ تے نقل تھی ڳئی</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..b3a3418289
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Slika kopirana v odložišče</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..b76b67a352
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Figura u kopjua në të papastër</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..f0fbe5c485
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Слика је копирана у привремену меморију</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..91a37e77b6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Gambar ditiron kana papan klip</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..51210de628
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Bilden har kopierats till urklipp</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..b304da28c1
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Тасвир ба ҳофизаи муваққатӣ нусха бардошта шуд</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..52a77b64dc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">คัดลอกภาพไปยังคลิปบอร์ดแล้ว</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..b0ef721f12
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Resim panoya kopyalandı</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..d905bed3df
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Ñadū’hua ngà nanun riña portapapel</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..ad4402d83c
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Рәсем алмашу буферына копияләнде</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..2719590251
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">سۈرەت چاپلاش تاختىسىغا كۆچۈرۈلدى</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..49cffe6ba2
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Зображення скопійовано в буфер обміну</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..8127c7855b
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Đã sao chép ảnh vào khay nhớ tạm</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..98d08c5c6a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">图像已复制到剪贴板</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..39326e4ac0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">已將圖片複製至剪貼簿</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values/attrs.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000..60a3b3a35f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values/attrs.xml
@@ -0,0 +1,19 @@
+<?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>
+ <attr name="mozac_font_semibold" format="reference" />
+ <attr name="mozac_accent" format="reference" />
+ <attr name="mozac_contrast_text" format="reference" />
+
+ <!-- Background color for favicon widget box -->
+ <attr name="mozac_widget_favicon_background_color" format="reference" />
+ <!-- Border color for favicon widget box -->
+ <attr name="mozac_widget_favicon_border_color" format="reference" />
+
+ <!-- Label color and icon button tint for site item widget -->
+ <attr name="mozac_primary_text_color" format="reference" />
+ <!-- Caption color for site item widget -->
+ <attr name="mozac_caption_text_color" format="reference" />
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values/colors.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..105264e605
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?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>
+ <!-- Button Colors -->
+ <color name="mozac_grey_button_color">#E0E0E6</color>
+ <color name="mozac_destructive_button_text_color">#C50042</color>
+ <color name="mozac_grey_button_text_color">#312A65</color>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values/dimens.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..9f6d4587fd
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values/dimens.xml
@@ -0,0 +1,14 @@
+<?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>
+ <dimen name="mozac_widget_favicon_size">40dp</dimen>
+ <dimen name="mozac_widget_favicon_padding">8dp</dimen>
+
+ <dimen name="mozac_widget_site_item_height">56dp</dimen>
+ <dimen name="mozac_widget_site_item_label_text_size">16sp</dimen>
+ <dimen name="mozac_widget_site_item_caption_text_size">12sp</dimen>
+ <dimen name="mozac_widget_site_item_secondary_button_size">32dp</dimen>
+ <dimen name="mozac_widget_site_item_secondary_button_padding">4dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values/strings.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..6a82d7eaef
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?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>
+ <!-- Text for confirmation "snackbar" shown after copying an image to the clipboard. -->
+ <string name="snackbar_copy_image_to_clipboard_confirmation">Image copied to clipboard</string>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/main/res/values/styles.xml b/mobile/android/android-components/components/ui/widgets/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..515b98e12e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/main/res/values/styles.xml
@@ -0,0 +1,63 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="Mozac.Widgets.TestTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
+ <item name="mozac_widget_favicon_background_color">@color/photonWhite</item>
+ <item name="mozac_widget_favicon_border_color">@color/photonLightGrey30</item>
+ <item name="mozac_primary_text_color">@color/photonInk90</item>
+ <item name="mozac_caption_text_color">@color/photonInk50</item>
+ </style>
+
+ <!-- Button styling -->
+ <style name="Mozac.Widgets.NeutralButton" parent="Widget.MaterialComponents.Button.TextButton">
+ <item name="iconTint">@color/mozac_grey_button_text_color</item>
+ <item name="iconPadding">8dp</item>
+ <item name="iconGravity">textStart</item>
+ <item name="android:textAlignment">center</item>
+ <item name="android:background">@drawable/rounded_button_background</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textAllCaps">false</item>
+ <item name="backgroundTint">@color/mozac_grey_button_color</item>
+ <item name="android:textColor">@color/mozac_grey_button_text_color</item>
+ <item name="android:letterSpacing">0</item>
+ <item name="fontFamily">?mozac_font_semibold</item>
+ </style>
+
+ <style name="Mozac.Widgets.DestructiveButton" parent="Mozac.Widgets.NeutralButton">
+ <item name="iconTint">@color/mozac_destructive_button_text_color</item>
+ <item name="android:textColor">@color/mozac_destructive_button_text_color</item>
+ </style>
+
+ <style name="Mozac.Widgets.PositiveButton" parent="Mozac.Widgets.NeutralButton">
+ <item name="backgroundTint">?mozac_accent</item>
+ <item name="iconTint">?mozac_contrast_text</item>
+ <item name="android:textColor">?mozac_contrast_text</item>
+ </style>
+
+ <!-- Favicon styling -->
+ <style name="Mozac.Widgets.Favicon" parent="">
+ <item name="android:layout_width">@dimen/mozac_widget_favicon_size</item>
+ <item name="android:layout_height">@dimen/mozac_widget_favicon_size</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:padding">@dimen/mozac_widget_favicon_padding</item>
+ <item name="android:background">@drawable/mozac_widget_favicon_background</item>
+ </style>
+
+ <style name="Mozac.Widgets.SiteItem.Label" parent="">
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAlignment">viewStart</item>
+ <item name="android:textSize">@dimen/mozac_widget_site_item_label_text_size</item>
+ <item name="android:textColor">?attr/mozac_primary_text_color</item>
+ </style>
+ <style name="Mozac.Widgets.SiteItem.Caption" parent="Mozac.Widgets.SiteItem.Label">
+ <item name="android:textSize">@dimen/mozac_widget_site_item_caption_text_size</item>
+ <item name="android:textColor">?attr/mozac_caption_text_color</item>
+ </style>
+</resources>
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/TestUtils.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/TestUtils.kt
new file mode 100644
index 0000000000..cda854782e
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/TestUtils.kt
@@ -0,0 +1,73 @@
+/* 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.ui.widgets
+
+import android.view.MotionEvent
+
+object TestUtils {
+ fun getMotionEvent(
+ action: Int,
+ x: Float = 0f,
+ y: Float = 0f,
+ eventTime: Long = System.currentTimeMillis(),
+ previousEvent: MotionEvent? = null,
+ ): MotionEvent {
+ val downTime = previousEvent?.downTime ?: System.currentTimeMillis()
+
+ var pointerCount = previousEvent?.pointerCount ?: 0
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ pointerCount++
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ pointerCount = 1
+ } else if (previousEvent?.action == MotionEvent.ACTION_POINTER_UP) {
+ pointerCount--
+ }
+
+ val properties = Array(
+ pointerCount,
+ TestUtils::getPointerProperties,
+ )
+ val pointerCoords =
+ getPointerCoords(
+ x,
+ y,
+ pointerCount,
+ previousEvent,
+ )
+
+ return MotionEvent.obtain(
+ downTime, eventTime,
+ action, pointerCount, properties,
+ pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0,
+ )
+ }
+
+ private fun getPointerCoords(
+ x: Float,
+ y: Float,
+ pointerCount: Int,
+ previousEvent: MotionEvent? = null,
+ ): Array<MotionEvent.PointerCoords?> {
+ val currentEventCoords = MotionEvent.PointerCoords().apply {
+ this.x = x; this.y = y; pressure = 1f; size = 1f
+ }
+
+ return if (pointerCount > 1 && previousEvent != null) {
+ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ this.x = previousEvent.x; this.y = previousEvent.y; pressure = 1f; size = 1f
+ },
+ currentEventCoords,
+ )
+ } else {
+ arrayOf(currentEventCoords)
+ }
+ }
+
+ private fun getPointerProperties(id: Int): MotionEvent.PointerProperties =
+ MotionEvent.PointerProperties().apply {
+ this.id = id; this.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/VerticalSwipeRefreshLayoutTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/VerticalSwipeRefreshLayoutTest.kt
new file mode 100644
index 0000000000..b06e67be3f
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/VerticalSwipeRefreshLayoutTest.kt
@@ -0,0 +1,430 @@
+/* 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.ui.widgets
+
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_POINTER_DOWN
+import android.view.MotionEvent.ACTION_POINTER_UP
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.widgets.VerticalSwipeRefreshLayout.QuickScaleEvents
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class VerticalSwipeRefreshLayoutTest {
+ private lateinit var swipeLayout: VerticalSwipeRefreshLayout
+
+ @Before
+ fun setup() {
+ swipeLayout = VerticalSwipeRefreshLayout(testContext)
+ }
+
+ @Test
+ fun `onInterceptTouchEvent should abort pull to refresh and return false if the View is disabled`() {
+ swipeLayout = spy(swipeLayout)
+ val secondFingerEvent = TestUtils.getMotionEvent(ACTION_POINTER_DOWN)
+
+ swipeLayout.isEnabled = false
+ assertFalse(swipeLayout.onInterceptTouchEvent(secondFingerEvent))
+ verify(swipeLayout, times(0)).callSuperOnInterceptTouchEvent(secondFingerEvent)
+ }
+
+ @Test
+ fun `onInterceptTouchEvent should abort pull to refresh and return false if the motion was multitouch`() {
+ swipeLayout.isEnabled = true
+ swipeLayout.setOnChildScrollUpCallback { _, _ -> false }
+
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, x = 0f, y = 5f, previousEvent = downEvent)
+ val secondFingerEvent = TestUtils.getMotionEvent(ACTION_POINTER_DOWN, previousEvent = moveEvent)
+ val secondFingerNextEvent =
+ TestUtils.getMotionEvent(ACTION_MOVE, x = 0f, y = 5f, previousEvent = secondFingerEvent)
+ val secondFingerUp = TestUtils.getMotionEvent(ACTION_POINTER_UP, previousEvent = secondFingerNextEvent)
+ val upEvent = TestUtils.getMotionEvent(ACTION_UP, previousEvent = secondFingerUp)
+ val newDownEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+
+ swipeLayout.onInterceptTouchEvent(downEvent)
+ assertFalse(swipeLayout.onInterceptTouchEvent(moveEvent))
+ assertFalse(swipeLayout.hadMultiTouch)
+ assertFalse(swipeLayout.onInterceptTouchEvent(secondFingerEvent))
+ assertTrue(swipeLayout.hadMultiTouch)
+ assertFalse(swipeLayout.onInterceptTouchEvent(secondFingerNextEvent))
+ assertTrue(swipeLayout.hadMultiTouch)
+ assertFalse(swipeLayout.onInterceptTouchEvent(secondFingerUp))
+ assertTrue(swipeLayout.hadMultiTouch)
+ assertFalse(swipeLayout.onInterceptTouchEvent(upEvent))
+ assertTrue(swipeLayout.hadMultiTouch)
+ assertFalse(swipeLayout.onInterceptTouchEvent(newDownEvent))
+ assertFalse(swipeLayout.hadMultiTouch)
+ }
+
+ @Test
+ fun `onInterceptTouchEvent should abort pull to refresh and return false if zoom is in progress`() {
+ swipeLayout = spy(swipeLayout)
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f)
+ val pointerDownEvent =
+ TestUtils.getMotionEvent(ACTION_POINTER_DOWN, 200f, 200f, previousEvent = downEvent)
+ swipeLayout.isEnabled = true
+ swipeLayout.setOnChildScrollUpCallback { _, _ -> true }
+
+ swipeLayout.onInterceptTouchEvent(downEvent)
+ verify(swipeLayout, times(1)).callSuperOnInterceptTouchEvent(downEvent)
+
+ swipeLayout.onInterceptTouchEvent(pointerDownEvent)
+ assertFalse(swipeLayout.onInterceptTouchEvent(pointerDownEvent))
+ verify(swipeLayout, times(0)).callSuperOnInterceptTouchEvent(pointerDownEvent)
+ }
+
+ @Test
+ fun `onInterceptTouchEvent should cleanup if ACTION_CANCEL`() {
+ swipeLayout = spy(swipeLayout)
+ val cancelEvent = TestUtils.getMotionEvent(
+ ACTION_CANCEL,
+ previousEvent = TestUtils.getMotionEvent(ACTION_DOWN),
+ )
+ swipeLayout.isEnabled = true
+ swipeLayout.setOnChildScrollUpCallback { _, _ -> true }
+
+ swipeLayout.onInterceptTouchEvent(cancelEvent)
+
+ verify(swipeLayout).forgetQuickScaleEvents()
+ verify(swipeLayout).callSuperOnInterceptTouchEvent(cancelEvent)
+ }
+
+ @Test
+ fun `onInterceptTouchEvent should cleanup if quick scale ended`() {
+ swipeLayout = spy(swipeLayout)
+ val upEvent = TestUtils.getMotionEvent(
+ ACTION_CANCEL,
+ previousEvent = TestUtils.getMotionEvent(ACTION_DOWN),
+ )
+ swipeLayout.isEnabled = true
+ swipeLayout.isQuickScaleInProgress = true
+ swipeLayout.setOnChildScrollUpCallback { _, _ -> true }
+
+ swipeLayout.onInterceptTouchEvent(upEvent)
+
+ verify(swipeLayout).forgetQuickScaleEvents()
+ verify(swipeLayout).callSuperOnInterceptTouchEvent(upEvent)
+ }
+
+ @Test
+ fun `onInterceptTouchEvent should disable pull to refresh if quick scale is in progress`() {
+ // default DOUBLE_TAP_TIMEOUT is 300ms
+
+ swipeLayout = spy(swipeLayout)
+ val firstDownEvent = TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 100)
+ val upEvent =
+ TestUtils.getMotionEvent(ACTION_UP, eventTime = 200, previousEvent = firstDownEvent)
+ val newDownEvent =
+ TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 500, previousEvent = upEvent)
+ val previousEvents = QuickScaleEvents(firstDownEvent, upEvent, null)
+ swipeLayout.quickScaleEvents = previousEvents
+ swipeLayout.isQuickScaleInProgress = false
+
+ assertFalse(swipeLayout.onInterceptTouchEvent(newDownEvent))
+ assertTrue(swipeLayout.isQuickScaleInProgress)
+ verify(swipeLayout).maybeAddDoubleTapEvent(newDownEvent)
+ verify(swipeLayout, times(0)).callSuperOnInterceptTouchEvent(newDownEvent)
+ }
+
+ @Test
+ fun `onInterceptTouchEvent should disable pull to refresh if move was more on the x axys`() {
+ // default DOUBLE_TAP_TIMEOUT is 300ms
+
+ swipeLayout = spy(swipeLayout)
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, x = 0f, y = 0f, eventTime = 0)
+ val moveEvent = TestUtils.getMotionEvent(
+ ACTION_MOVE,
+ x = 1f,
+ y = 0f,
+ eventTime = 100,
+ previousEvent = downEvent,
+ )
+ swipeLayout.isEnabled = true
+ swipeLayout.isQuickScaleInProgress = false
+ swipeLayout.setOnChildScrollUpCallback { _, _ -> false }
+
+ swipeLayout.onInterceptTouchEvent(downEvent)
+ verify(swipeLayout).callSuperOnInterceptTouchEvent(downEvent)
+
+ assertFalse(swipeLayout.onInterceptTouchEvent(moveEvent))
+ verify(swipeLayout, times(0)).callSuperOnInterceptTouchEvent(moveEvent)
+ }
+
+ @Test
+ fun `onInterceptTouchEvent should allow pull to refresh if move was more on the y axys`() {
+ // default DOUBLE_TAP_TIMEOUT is 300ms
+
+ swipeLayout = spy(swipeLayout)
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, x = 0f, y = 0f, eventTime = 0)
+ val moveEvent = TestUtils.getMotionEvent(
+ ACTION_MOVE,
+ x = 0f,
+ y = 1f,
+ eventTime = 100,
+ previousEvent = downEvent,
+ )
+ swipeLayout.isEnabled = true
+ swipeLayout.isQuickScaleInProgress = false
+ swipeLayout.setOnChildScrollUpCallback { _, _ -> false }
+
+ swipeLayout.onInterceptTouchEvent(downEvent)
+ verify(swipeLayout).callSuperOnInterceptTouchEvent(downEvent)
+
+ swipeLayout.onInterceptTouchEvent(moveEvent)
+ verify(swipeLayout).callSuperOnInterceptTouchEvent(moveEvent)
+ }
+
+ @Test
+ fun `Should not respond descendants initiated scrolls if this View is enabled`() {
+ swipeLayout = spy(swipeLayout)
+ val childView: View = mock()
+ val targetView: View = mock()
+ val scrollAxis = 0
+
+ swipeLayout.isEnabled = true
+
+ assertFalse(swipeLayout.onStartNestedScroll(childView, targetView, scrollAxis))
+ verify(swipeLayout, times(0)).callSuperOnStartNestedScroll(
+ childView,
+ targetView,
+ scrollAxis,
+ )
+ }
+
+ @Test
+ fun `Should delegate super#onStartNestedScroll if this View is not enabled`() {
+ swipeLayout = spy(swipeLayout)
+ val childView: View = mock()
+ val targetView: View = mock()
+ val scrollAxis = 0
+
+ swipeLayout.isEnabled = false
+ swipeLayout.onStartNestedScroll(childView, targetView, scrollAxis)
+
+ verify(swipeLayout).callSuperOnStartNestedScroll(childView, targetView, scrollAxis)
+ }
+
+ @Test
+ fun `maybeAddDoubleTapEvent should not modify quickScaleEvents if not for ACTION_DOWN or ACTION_UP`() {
+ val emptyListOfEvents = QuickScaleEvents()
+ swipeLayout.quickScaleEvents = emptyListOfEvents
+
+ swipeLayout.maybeAddDoubleTapEvent(TestUtils.getMotionEvent(ACTION_POINTER_DOWN))
+
+ assertEquals(emptyListOfEvents, swipeLayout.quickScaleEvents)
+ }
+
+ @Test
+ fun `maybeAddDoubleTapEvent will add ACTION_UP as second event if there is already one event in sequence`() {
+ val firstEvent = spy(TestUtils.getMotionEvent(ACTION_DOWN))
+ val secondEvent =
+ spy(TestUtils.getMotionEvent(ACTION_UP, eventTime = 133, previousEvent = firstEvent))
+ val expectedResult = Triple<MotionEvent?, MotionEvent?, MotionEvent?>(
+ firstEvent,
+ secondEvent,
+ null,
+ )
+ swipeLayout.quickScaleEvents = QuickScaleEvents(firstEvent, null, null)
+
+ swipeLayout.maybeAddDoubleTapEvent(secondEvent)
+
+ // A Triple assert or MotionEvent assert fails probably because of the copies made
+ // Verifying the expected actions and eventTime should be good enough.
+ assertEquals(expectedResult.first, swipeLayout.quickScaleEvents.firstDownEvent)
+ assertEquals(
+ expectedResult.second!!.actionMasked,
+ swipeLayout.quickScaleEvents.upEvent!!.actionMasked,
+ )
+ assertEquals(
+ expectedResult.second!!.eventTime,
+ swipeLayout.quickScaleEvents.upEvent!!.eventTime,
+ )
+ assertEquals(null, swipeLayout.quickScaleEvents.secondDownEvent)
+ }
+
+ @Test
+ fun `maybeAddDoubleTapEvent will not add ACTION_UP if there is not a first event already in sequence`() {
+ val firstEvent = spy(TestUtils.getMotionEvent(ACTION_DOWN))
+ val secondEvent =
+ spy(TestUtils.getMotionEvent(ACTION_UP, eventTime = 133, previousEvent = firstEvent))
+ val expectedResult = QuickScaleEvents()
+ swipeLayout.quickScaleEvents = expectedResult
+
+ swipeLayout.maybeAddDoubleTapEvent(secondEvent)
+
+ assertEquals(null, swipeLayout.quickScaleEvents.firstDownEvent)
+ assertEquals(null, swipeLayout.quickScaleEvents.upEvent)
+ assertEquals(null, swipeLayout.quickScaleEvents.secondDownEvent)
+ }
+
+ @Test
+ fun `maybeAddDoubleTapEvent will add the first ACTION_DOWN if the events list is otherwise empty`() {
+ swipeLayout = spy(swipeLayout)
+ val emptyListOfEvents = QuickScaleEvents()
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 133)
+ swipeLayout.quickScaleEvents = emptyListOfEvents
+
+ swipeLayout.maybeAddDoubleTapEvent(downEvent)
+
+ verify(swipeLayout).forgetQuickScaleEvents()
+ assertEquals(downEvent.actionMasked, swipeLayout.quickScaleEvents.firstDownEvent!!.actionMasked)
+ assertEquals(downEvent.eventTime, swipeLayout.quickScaleEvents.firstDownEvent!!.eventTime)
+ assertEquals(null, swipeLayout.quickScaleEvents.upEvent)
+ assertEquals(null, swipeLayout.quickScaleEvents.secondDownEvent)
+ }
+
+ @Test
+ fun `maybeAddDoubleTapEvent will reset the first ACTION_DOWN if the events list does not contain other events`() {
+ swipeLayout = spy(swipeLayout)
+ val previousDownEvent = TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 111)
+ val previousEvents = QuickScaleEvents(previousDownEvent, null, null)
+ val newDownEvent = TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 222)
+ swipeLayout.quickScaleEvents = previousEvents
+
+ swipeLayout.maybeAddDoubleTapEvent(newDownEvent)
+
+ verify(swipeLayout).forgetQuickScaleEvents()
+ assertEquals(newDownEvent.actionMasked, swipeLayout.quickScaleEvents.firstDownEvent!!.actionMasked)
+ assertEquals(newDownEvent.eventTime, swipeLayout.quickScaleEvents.firstDownEvent!!.eventTime)
+ assertEquals(null, swipeLayout.quickScaleEvents.upEvent)
+ assertEquals(null, swipeLayout.quickScaleEvents.secondDownEvent)
+ }
+
+ @Test
+ fun `maybeAddDoubleTapEvent will reset ACTION_DOWN if timeout was reached`() {
+ // default DOUBLE_TAP_TIMEOUT is 300ms
+
+ swipeLayout = spy(swipeLayout)
+ val firstDownEvent = TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 100)
+ val upEvent =
+ TestUtils.getMotionEvent(ACTION_UP, eventTime = 200, previousEvent = firstDownEvent)
+ val newDownEvent =
+ TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 501, previousEvent = upEvent)
+ val previousEvents = QuickScaleEvents(firstDownEvent, upEvent, null)
+ swipeLayout.quickScaleEvents = previousEvents
+
+ swipeLayout.maybeAddDoubleTapEvent(newDownEvent)
+
+ verify(swipeLayout).forgetQuickScaleEvents()
+ assertEquals(newDownEvent.actionMasked, swipeLayout.quickScaleEvents.firstDownEvent!!.actionMasked)
+ assertEquals(newDownEvent.eventTime, swipeLayout.quickScaleEvents.firstDownEvent!!.eventTime)
+ assertEquals(null, swipeLayout.quickScaleEvents.upEvent)
+ assertEquals(null, swipeLayout.quickScaleEvents.secondDownEvent)
+ }
+
+ @Test
+ fun `maybeAddDoubleTapEvent will add a second ACTION_DOWN already have two events and timeout is not reached`() {
+ // default DOUBLE_TAP_TIMEOUT is 300ms
+
+ swipeLayout = spy(swipeLayout)
+ val firstDownEvent = TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 100)
+ val upEvent =
+ TestUtils.getMotionEvent(ACTION_UP, eventTime = 200, previousEvent = firstDownEvent)
+ val newDownEvent =
+ TestUtils.getMotionEvent(ACTION_DOWN, eventTime = 500, previousEvent = upEvent)
+ val previousEvents = QuickScaleEvents(firstDownEvent, upEvent, null)
+ swipeLayout.quickScaleEvents = previousEvents
+
+ swipeLayout.maybeAddDoubleTapEvent(newDownEvent)
+
+ verify(swipeLayout, times(0)).forgetQuickScaleEvents()
+ assertEquals(firstDownEvent.actionMasked, swipeLayout.quickScaleEvents.firstDownEvent!!.actionMasked)
+ assertEquals(firstDownEvent.eventTime, swipeLayout.quickScaleEvents.firstDownEvent!!.eventTime)
+ assertEquals(upEvent.actionMasked, swipeLayout.quickScaleEvents.upEvent!!.actionMasked)
+ assertEquals(upEvent.eventTime, swipeLayout.quickScaleEvents.upEvent!!.eventTime)
+ assertEquals(newDownEvent.actionMasked, swipeLayout.quickScaleEvents.secondDownEvent!!.actionMasked)
+ assertEquals(newDownEvent.eventTime, swipeLayout.quickScaleEvents.secondDownEvent!!.eventTime)
+ }
+
+ @Test
+ fun `forgetQuickScaleEvents should recycle all events and reset the quickScaleStatus`() {
+ val firstEvent = spy(TestUtils.getMotionEvent(ACTION_DOWN))
+ val secondEvent = spy(TestUtils.getMotionEvent(ACTION_UP, previousEvent = firstEvent))
+ val thirdEvent = spy(TestUtils.getMotionEvent(ACTION_DOWN))
+ swipeLayout.quickScaleEvents = QuickScaleEvents(firstEvent, secondEvent, thirdEvent)
+ swipeLayout.isQuickScaleInProgress = true
+
+ swipeLayout.forgetQuickScaleEvents()
+
+ verify(firstEvent).recycle()
+ verify(secondEvent).recycle()
+ verify(thirdEvent).recycle()
+ assertEquals(QuickScaleEvents(), swipeLayout.quickScaleEvents)
+ assertFalse(swipeLayout.isQuickScaleInProgress)
+ }
+
+ @Test
+ fun `isQuickScaleInProgress should return false if timeout was reached`() {
+ // default DOUBLE_TAP_TIMEOUT is 300ms
+
+ val firstEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val secondEvent = TestUtils.getMotionEvent(ACTION_UP, 0f, 0f, 0, firstEvent)
+ val thirdEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f, 301L, secondEvent)
+
+ assertFalse(swipeLayout.isQuickScaleInProgress(firstEvent, secondEvent, thirdEvent))
+ }
+
+ @Test
+ fun `isQuickScaleInProgress should return false if taps were too apart`() {
+ val firstEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val secondEvent = TestUtils.getMotionEvent(ACTION_UP, 0f, 0f, 0, firstEvent)
+ val thirdEvent = TestUtils.getMotionEvent(ACTION_DOWN, 200f, 20f, 200L, secondEvent)
+
+ assertFalse(swipeLayout.isQuickScaleInProgress(firstEvent, secondEvent, thirdEvent))
+ }
+
+ @Test
+ fun `isQuickScaleInProgress should return true if taps were close`() {
+ val firstEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val secondEvent = TestUtils.getMotionEvent(ACTION_UP, 0f, 0f, 0, firstEvent)
+ val thirdEvent = TestUtils.getMotionEvent(ACTION_DOWN, 20f, 20f, 200L, secondEvent)
+
+ assertTrue(swipeLayout.isQuickScaleInProgress(firstEvent, secondEvent, thirdEvent))
+ }
+
+ @Test
+ fun `isQuickScaleInProgress should return false if any event is null`() {
+ // Using the same values as in the above test that asserts true
+ val firstEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val secondEvent = TestUtils.getMotionEvent(ACTION_UP, 0f, 0f, 0, firstEvent)
+ val thirdEvent = TestUtils.getMotionEvent(ACTION_DOWN, 20f, 20f, 200L, secondEvent)
+ val oneNullEvent = QuickScaleEvents(firstEvent, secondEvent, null)
+ val twoNullEvents = QuickScaleEvents(null, null, thirdEvent)
+ val allNullEvents = QuickScaleEvents(null, null, null)
+
+ assertFalse(swipeLayout.isQuickScaleInProgress(oneNullEvent))
+ assertFalse(swipeLayout.isQuickScaleInProgress(twoNullEvents))
+ assertFalse(swipeLayout.isQuickScaleInProgress(allNullEvents))
+ }
+
+ @Test
+ fun `isQuickScaleInProgress should return true for valid sequence of non null events`() {
+ // Using the same values as in the above test that asserts true
+ swipeLayout = spy(swipeLayout)
+ val firstEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val secondEvent = TestUtils.getMotionEvent(ACTION_UP, 0f, 0f, 0, firstEvent)
+ val thirdEvent = TestUtils.getMotionEvent(ACTION_DOWN, 20f, 20f, 200L, secondEvent)
+ val quickScaleEvents = QuickScaleEvents(firstEvent, secondEvent, thirdEvent)
+
+ assertTrue(swipeLayout.isQuickScaleInProgress(quickScaleEvents))
+ verify(swipeLayout).isQuickScaleInProgress(firstEvent, secondEvent, thirdEvent)
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/WidgetSiteItemViewTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/WidgetSiteItemViewTest.kt
new file mode 100644
index 0000000000..babc038dc0
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/WidgetSiteItemViewTest.kt
@@ -0,0 +1,93 @@
+/* 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.ui.widgets
+
+import android.graphics.drawable.Drawable
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.appcompat.view.ContextThemeWrapper
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import mozilla.components.ui.icons.R as iconsR
+
+@RunWith(AndroidJUnit4::class)
+class WidgetSiteItemViewTest {
+
+ private lateinit var view: WidgetSiteItemView
+
+ @Before
+ fun setup() {
+ val context = ContextThemeWrapper(testContext, R.style.Mozac_Widgets_TestTheme)
+ view = WidgetSiteItemView(context)
+ }
+
+ @Test
+ fun `setText hides the caption`() {
+ val labelView = view.findViewById<TextView>(R.id.label)
+ val captionView = view.findViewById<TextView>(R.id.caption)
+
+ view.setText(label = "label", caption = null)
+ assertEquals("label", labelView.text)
+ assertTrue(captionView.isGone)
+
+ view.setText(label = "Label", caption = "")
+ assertEquals("Label", labelView.text)
+ assertEquals("", captionView.text)
+ assertTrue(captionView.isVisible)
+ }
+
+ @Test
+ fun `setSecondaryButton shows the button`() {
+ val secondaryButton = view.findViewById<ImageButton>(R.id.secondary_button)
+ val drawable = mock<Drawable>()
+ var clicked = false
+ view.setSecondaryButton(
+ icon = drawable,
+ contentDescription = "Menu",
+ onClickListener = { clicked = true },
+ )
+ assertTrue(secondaryButton.isVisible)
+ assertEquals(drawable, secondaryButton.drawable)
+ assertEquals("Menu", secondaryButton.contentDescription)
+
+ secondaryButton.performClick()
+ assertTrue(clicked)
+ }
+
+ @Test
+ fun `setSecondaryButton with resource IDs shows the button`() {
+ val secondaryButton = view.findViewById<ImageButton>(R.id.secondary_button)
+ var clicked = false
+ view.setSecondaryButton(
+ icon = iconsR.drawable.mozac_ic_lock_24,
+ contentDescription = iconsR.string.mozac_error_lock,
+ onClickListener = { clicked = true },
+ )
+ assertTrue(secondaryButton.isVisible)
+ assertNotNull(secondaryButton.drawable)
+ assertEquals("mozac_error_lock", secondaryButton.contentDescription)
+
+ secondaryButton.performClick()
+ assertTrue(clicked)
+ }
+
+ @Test
+ fun `removeSecondaryButton does nothing if set was not called`() {
+ val secondaryButton = view.findViewById<ImageButton>(R.id.secondary_button)
+ assertTrue(secondaryButton.isGone)
+
+ view.removeSecondaryButton()
+ assertTrue(secondaryButton.isGone)
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetectorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetectorTest.kt
new file mode 100644
index 0000000000..66c4b826ff
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/BrowserGestureDetectorTest.kt
@@ -0,0 +1,231 @@
+/* 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.ui.widgets.behavior
+
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_POINTER_DOWN
+import android.view.MotionEvent.ACTION_UP
+import android.view.ScaleGestureDetector
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.support.test.any
+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.ArgumentMatchers.anyFloat
+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 BrowserGestureDetectorTest {
+ // Robolectric currently (April 17th 2020) only offer a stub in it's `ShadowScaleGestureDetector`
+ // so unit tests based on the actual implementation of `ScaleGestureListener` are not possible.
+
+ // Used spies and not mocks as it was observed that verifying more of the below as mocks
+ // will fail the tests because of "UnfinishedVerificationException"
+ private val scrollListener = spy { _: Float, _: Float -> run {} }
+ private val verticalScrollListener = spy { _: Float -> run {} }
+ private val horizontalScrollListener = spy { _: Float -> run {} }
+ private val scaleBeginListener = spy { _: Float -> run {} }
+ private val scaleInProgressListener = spy { _: Float -> run {} }
+ private val scaleEndListener = spy { _: Float -> run {} }
+ private val gesturesListener = BrowserGestureDetector.GesturesListener(
+ onScroll = scrollListener,
+ onVerticalScroll = verticalScrollListener,
+ onHorizontalScroll = horizontalScrollListener,
+ onScaleBegin = scaleBeginListener,
+ onScale = scaleInProgressListener,
+ onScaleEnd = scaleEndListener,
+ )
+
+ @Test
+ fun `Detector should not attempt to detect zoom if MotionEvent's action is ACTION_CANCEL`() {
+ val detector = spy(BrowserGestureDetector(testContext, mock()))
+ val scaleDetector: ScaleGestureDetector = mock()
+ detector.scaleGestureDetector = scaleDetector
+
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val cancelEvent = TestUtils.getMotionEvent(ACTION_CANCEL, previousEvent = downEvent)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, previousEvent = downEvent)
+ detector.handleTouchEvent(downEvent)
+ detector.handleTouchEvent(cancelEvent)
+ detector.handleTouchEvent(downEvent)
+ detector.handleTouchEvent(moveEvent)
+
+ verify(scaleDetector, times(3)).onTouchEvent(any<MotionEvent>())
+ }
+
+ @Test
+ fun `Detector should not attempt to detect scrolls if a zoom gesture is in progress`() {
+ val detector = spy(BrowserGestureDetector(testContext, mock()))
+ val scrollDetector: GestureDetector = mock()
+ val scaleDetector: ScaleGestureDetector = mock()
+ detector.gestureDetector = scrollDetector
+ detector.scaleGestureDetector = scaleDetector
+ `when`(scaleDetector.isInProgress).thenReturn(true)
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, previousEvent = downEvent)
+
+ detector.handleTouchEvent(downEvent)
+ detector.handleTouchEvent(moveEvent)
+
+ // In this case we let ACTION_DOWN, ACTION_UP, ACTION_CANCEL be handled but not others.
+ verify(scrollDetector, times(1)).onTouchEvent(downEvent)
+ verify(scrollDetector, never()).onTouchEvent(moveEvent)
+ }
+
+ @Test
+ fun `Detector's handleTouchEvent returns false if the event was not handled`() {
+ val detector = spy(BrowserGestureDetector(testContext, mock()))
+ val unhandledEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+
+ // Neither the scale detector, nor the scroll detector should be interested
+ // in a one of a time ACTION_CANCEL MotionEvent
+ val wasEventHandled = detector.handleTouchEvent(
+ TestUtils.getMotionEvent(ACTION_CANCEL, previousEvent = unhandledEvent),
+ )
+
+ assertFalse(wasEventHandled)
+ }
+
+ @Test
+ fun `Detector's handleTouchEvent returns true if the event was handled`() {
+ val detector = spy(BrowserGestureDetector(testContext, mock()))
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 0f, previousEvent = downEvent)
+ val moveEvent2 = TestUtils.getMotionEvent(ACTION_MOVE, 100f, 100f, previousEvent = moveEvent)
+
+ detector.handleTouchEvent(downEvent)
+ detector.handleTouchEvent(moveEvent)
+ val wasEventHandled = detector.handleTouchEvent(moveEvent2)
+
+ assertTrue(wasEventHandled)
+ }
+
+ @Test
+ fun `Detector should inform about scroll and vertical scrolls events`() {
+ val detector = spy(BrowserGestureDetector(testContext, gesturesListener))
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 0f, previousEvent = downEvent)
+ val moveEvent2 = TestUtils.getMotionEvent(ACTION_MOVE, 100f, 200f, previousEvent = moveEvent)
+
+ detector.handleTouchEvent(downEvent)
+ detector.handleTouchEvent(moveEvent)
+ detector.handleTouchEvent(moveEvent2)
+
+ // If the movement was more on the Y axis both "onScroll" and "onVerticalScroll" callbacks
+ // should be called but no others.
+ verify(scrollListener).invoke(-100f, -200f)
+ verify(verticalScrollListener).invoke(-200f)
+ verify(horizontalScrollListener, never()).invoke(anyFloat())
+ verify(scaleBeginListener, never()).invoke(anyFloat())
+ verify(scaleInProgressListener, never()).invoke(anyFloat())
+ verify(scaleEndListener, never()).invoke(anyFloat())
+ }
+
+ @Test
+ fun `Detector should prioritize vertical scrolls over horizontal scrolls`() {
+ val detector = spy(BrowserGestureDetector(testContext, gesturesListener))
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 0f, previousEvent = downEvent)
+ val moveEvent2 = TestUtils.getMotionEvent(ACTION_MOVE, 100f, 100f, previousEvent = moveEvent)
+
+ detector.handleTouchEvent(downEvent)
+ detector.handleTouchEvent(moveEvent)
+ detector.handleTouchEvent(moveEvent2)
+
+ // If the movement was for the same amount on both the Y axis and the X axis
+ // both "onScroll" and "onVerticalScroll" callbacks should be called but no others.
+ verify(scrollListener).invoke(-100f, -100f)
+ verify(verticalScrollListener).invoke(-100f)
+ verify(horizontalScrollListener, never()).invoke(anyFloat())
+ verify(scaleBeginListener, never()).invoke(anyFloat())
+ verify(scaleInProgressListener, never()).invoke(anyFloat())
+ verify(scaleEndListener, never()).invoke(anyFloat())
+ }
+
+ @Test
+ fun `Detector should inform about scroll and horizontal scrolls events`() {
+ val detector = spy(BrowserGestureDetector(testContext, gesturesListener))
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 0f, previousEvent = downEvent)
+ val moveEvent2 = TestUtils.getMotionEvent(ACTION_MOVE, 101f, 100f, previousEvent = moveEvent)
+
+ detector.handleTouchEvent(downEvent)
+ detector.handleTouchEvent(moveEvent)
+ detector.handleTouchEvent(moveEvent2)
+
+ // If the movement was for the same amount on both the Y axis and the X axis
+ // both "onScroll" and "onVerticalScroll" callbacks should be called but no others.
+ verify(scrollListener).invoke(-101f, -100f)
+ verify(horizontalScrollListener).invoke(-101f)
+ verify(verticalScrollListener, never()).invoke(anyFloat())
+ verify(scaleBeginListener, never()).invoke(anyFloat())
+ verify(scaleInProgressListener, never()).invoke(anyFloat())
+ verify(scaleEndListener, never()).invoke(anyFloat())
+ }
+
+ @Test
+ fun `Detector should always pass the ACTION_DOWN, ACTION_UP, ACTION_CANCEL events to the scroll detector`() {
+ val detector = spy(BrowserGestureDetector(testContext, mock()))
+ val scrollDetector: GestureDetector = mock()
+ val scaleDetector: ScaleGestureDetector = mock()
+ detector.gestureDetector = scrollDetector
+ detector.scaleGestureDetector = scaleDetector
+ // The aforementioned events should always be passed to the scroll detector,
+ // even if scaling is in progress.
+ `when`(scaleDetector.isInProgress).thenReturn(true)
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, previousEvent = downEvent)
+ val upEvent = TestUtils.getMotionEvent(ACTION_UP, previousEvent = moveEvent)
+ val cancelEvent = TestUtils.getMotionEvent(ACTION_CANCEL, previousEvent = upEvent)
+
+ listOf(downEvent, moveEvent, upEvent, cancelEvent).forEach {
+ detector.handleTouchEvent(it)
+ }
+
+ // With scaling in progress we let ACTION_DOWN, ACTION_UP, ACTION_CANCEL be handled but not others.
+ verify(scrollDetector, times(1)).onTouchEvent(downEvent)
+ verify(scrollDetector, times(1)).onTouchEvent(upEvent)
+ verify(scrollDetector, times(1)).onTouchEvent(cancelEvent)
+ verify(scrollDetector, never()).onTouchEvent(moveEvent)
+ }
+
+ @Test
+ fun `Detector should not crash when the scroll detector receives a null first MotionEvent`() {
+ val crashReporting: CrashReporting = mock()
+ val detector = BrowserGestureDetector(testContext, gesturesListener, crashReporting)
+ // We need a previous event for ACTION_MOVE.
+ // Don't use ACTION_DOWN (first pointer on the screen) but ACTION_POINTER_DOWN (other later pointer)
+ // just to artificially be able to recreate the bug from 8552. This should not happen IRL.
+ val downEvent = TestUtils.getMotionEvent(ACTION_POINTER_DOWN)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 0f, previousEvent = downEvent)
+ val moveEvent2 = TestUtils.getMotionEvent(ACTION_MOVE, 100f, 200f, previousEvent = moveEvent)
+
+ detector.handleTouchEvent(downEvent)
+ detector.handleTouchEvent(moveEvent)
+ detector.handleTouchEvent(moveEvent2)
+
+ verify(scrollListener).invoke(-100f, -200f)
+
+ // We don't crash but neither can identify vertical / horizontal scrolls.
+
+ verify(verticalScrollListener, never()).invoke(anyFloat())
+ verify(horizontalScrollListener, never()).invoke(anyFloat())
+ verify(scaleBeginListener, never()).invoke(anyFloat())
+ verify(scaleInProgressListener, never()).invoke(anyFloat())
+ verify(scaleEndListener, never()).invoke(anyFloat())
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewClippingBehaviorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewClippingBehaviorTest.kt
new file mode 100644
index 0000000000..13538bc591
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewClippingBehaviorTest.kt
@@ -0,0 +1,221 @@
+/* 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.ui.widgets.behavior
+
+import android.content.Context
+import android.view.View
+import android.widget.EditText
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.toolbar.ScrollableToolbar
+import mozilla.components.support.test.fakes.engine.FakeEngineView
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class EngineViewClippingBehaviorTest {
+
+ @Test
+ fun `EngineView clipping and bottom toolbar offset are kept in sync`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val toolbar: View = mock()
+ doReturn(100).`when`(toolbar).height
+ doReturn(42f).`when`(toolbar).translationY
+
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView.asView(),
+ toolbar.height,
+ ToolbarPosition.BOTTOM,
+ )
+
+ behavior.onDependentViewChanged(mock(), mock(), toolbar)
+
+ verify(engineView).setVerticalClipping(-42)
+ assertEquals(0f, engineView.asView().translationY)
+ }
+
+ @Test
+ fun `EngineView clipping and top toolbar offset are kept in sync`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val toolbar: View = mock()
+ doReturn(100).`when`(toolbar).height
+ doReturn(42f).`when`(toolbar).translationY
+
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView.asView(),
+ toolbar.height,
+ ToolbarPosition.TOP,
+ )
+
+ behavior.onDependentViewChanged(mock(), mock(), toolbar)
+
+ verify(engineView).setVerticalClipping(42)
+ assertEquals(142f, engineView.asView().translationY)
+ }
+
+ @Test
+ fun `Behavior does not depend on normal views`() {
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ mock(),
+ 0,
+ ToolbarPosition.BOTTOM,
+ )
+
+ assertFalse(behavior.layoutDependsOn(mock(), mock(), TextView(testContext)))
+ assertFalse(behavior.layoutDependsOn(mock(), mock(), EditText(testContext)))
+ assertFalse(behavior.layoutDependsOn(mock(), mock(), ImageView(testContext)))
+ }
+
+ @Test
+ fun `Behavior depends on BrowserToolbar`() {
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ mock(),
+ 0,
+ ToolbarPosition.BOTTOM,
+ )
+
+ assertTrue(behavior.layoutDependsOn(mock(), mock(), BrowserToolbar(testContext)))
+ }
+
+ @Test
+ fun `GIVEN a bottom toolbar WHEN translation has below a half decimal THEN set vertical clipping with the floor value`() {
+ val engineView: FakeEngineView = mock()
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView,
+ 100,
+ ToolbarPosition.BOTTOM,
+ )
+
+ behavior.toolbarChangedAction(40.4f)
+
+ verify(engineView).setVerticalClipping(-40)
+ }
+
+ @Test
+ fun `GIVEN a bottom toolbar WHEN translation has exactly half of a decimal THEN set vertical clipping with the ceiling value`() {
+ val engineView: FakeEngineView = mock()
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView,
+ 100,
+ ToolbarPosition.BOTTOM,
+ )
+
+ behavior.toolbarChangedAction(40.5f)
+
+ verify(engineView).setVerticalClipping(-41)
+ }
+
+ @Test
+ fun `GIVEN a bottom toolbar WHEN translation has more than a half decimal THEN set vertical clipping with the ceiling value`() {
+ val engineView: FakeEngineView = mock()
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView,
+ 100,
+ ToolbarPosition.BOTTOM,
+ )
+
+ behavior.toolbarChangedAction(40.6f)
+
+ verify(engineView).setVerticalClipping(-41)
+ }
+
+ @Test
+ fun `GIVEN a top toolbar WHEN translation has below a half decimal THEN set vertical clipping with the floor value`() {
+ val engineView: FakeEngineView = mock()
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView,
+ 100,
+ ToolbarPosition.TOP,
+ )
+
+ behavior.toolbarChangedAction(40.4f)
+
+ verify(engineView).setVerticalClipping(40)
+ }
+
+ @Test
+ fun `GIVEN a top toolbar WHEN translation has exactly half of a decimal THEN set vertical clipping with the ceiling value`() {
+ val engineView: FakeEngineView = mock()
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView,
+ 100,
+ ToolbarPosition.TOP,
+ )
+
+ behavior.toolbarChangedAction(40.5f)
+
+ verify(engineView).setVerticalClipping(41)
+ }
+
+ @Test
+ fun `GIVEN a top toolbar WHEN translation has more than a half decimal THEN set vertical clipping with the ceiling value`() {
+ val engineView: FakeEngineView = mock()
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView,
+ 100,
+ ToolbarPosition.TOP,
+ )
+
+ behavior.toolbarChangedAction(40.6f)
+
+ verify(engineView).setVerticalClipping(41)
+ }
+
+ @Test
+ fun `GIVEN a bottom toolbar WHEN translation returns NaN THEN no exception thrown`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val toolbar: View = mock()
+ doReturn(100).`when`(toolbar).height
+ doReturn(Float.NaN).`when`(toolbar).translationY
+
+ val behavior = EngineViewClippingBehavior(
+ mock(),
+ null,
+ engineView.asView(),
+ toolbar.height,
+ ToolbarPosition.BOTTOM,
+ )
+
+ behavior.onDependentViewChanged(mock(), mock(), toolbar)
+ assertEquals(0f, engineView.asView().translationY)
+ }
+}
+
+private class BrowserToolbar(context: Context) : TextView(context), ScrollableToolbar {
+ override fun enableScrolling() {}
+ override fun disableScrolling() {}
+ override fun expand() {}
+ override fun collapse() {}
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorTest.kt
new file mode 100644
index 0000000000..0f0c10b71a
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/EngineViewScrollingBehaviorTest.kt
@@ -0,0 +1,575 @@
+/* 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.ui.widgets.behavior
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.View
+import android.widget.FrameLayout
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.ViewCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.INPUT_UNHANDLED
+import mozilla.components.concept.engine.InputResultDetail
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class EngineViewScrollingBehaviorTest {
+ @Test
+ fun `onStartNestedScroll should attempt scrolling only if browserToolbar is valid`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ doReturn(true).`when`(behavior).shouldScroll
+
+ behavior.dynamicScrollView = null
+ var acceptsNestedScroll = behavior.onStartNestedScroll(
+ coordinatorLayout = mock(),
+ child = mock(),
+ directTargetChild = mock(),
+ target = mock(),
+ axes = ViewCompat.SCROLL_AXIS_VERTICAL,
+ type = ViewCompat.TYPE_TOUCH,
+ )
+ assertFalse(acceptsNestedScroll)
+ verify(behavior, never()).startNestedScroll(anyInt(), anyInt(), any())
+
+ behavior.dynamicScrollView = mock()
+ acceptsNestedScroll = behavior.onStartNestedScroll(
+ coordinatorLayout = mock(),
+ child = mock(),
+ directTargetChild = mock(),
+ target = mock(),
+ axes = ViewCompat.SCROLL_AXIS_VERTICAL,
+ type = ViewCompat.TYPE_TOUCH,
+ )
+ assertTrue(acceptsNestedScroll)
+ verify(behavior).startNestedScroll(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun `startNestedScroll should cancel an ongoing snap animation`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ doReturn(true).`when`(behavior).shouldScroll
+
+ val acceptsNestedScroll = behavior.startNestedScroll(
+ axes = ViewCompat.SCROLL_AXIS_VERTICAL,
+ type = ViewCompat.TYPE_TOUCH,
+ view = mock(),
+ )
+
+ assertTrue(acceptsNestedScroll)
+ verify(yTranslator).cancelInProgressTranslation()
+ }
+
+ @Test
+ fun `startNestedScroll should not accept nested scrolls on the horizontal axis`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ doReturn(true).`when`(behavior).shouldScroll
+
+ var acceptsNestedScroll = behavior.startNestedScroll(
+ axes = ViewCompat.SCROLL_AXIS_VERTICAL,
+ type = ViewCompat.TYPE_TOUCH,
+ view = mock(),
+ )
+ assertTrue(acceptsNestedScroll)
+
+ acceptsNestedScroll = behavior.startNestedScroll(
+ axes = ViewCompat.SCROLL_AXIS_HORIZONTAL,
+ type = ViewCompat.TYPE_TOUCH,
+ view = mock(),
+ )
+ assertFalse(acceptsNestedScroll)
+ }
+
+ @Test
+ fun `GIVEN a gesture that doesn't scroll the toolbar WHEN startNestedScroll THEN toolbar is expanded and nested scroll not accepted`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val engineView: EngineView = mock()
+ val inputResultDetail: InputResultDetail = mock()
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ doReturn(false).`when`(behavior).shouldScroll
+ doReturn(true).`when`(inputResultDetail).isTouchUnhandled()
+ behavior.engineView = engineView
+ doReturn(inputResultDetail).`when`(engineView).getInputResultDetail()
+
+ val acceptsNestedScroll = behavior.startNestedScroll(
+ axes = ViewCompat.SCROLL_AXIS_VERTICAL,
+ type = ViewCompat.TYPE_TOUCH,
+ view = mock(),
+ )
+
+ verify(yTranslator).cancelInProgressTranslation()
+ verify(yTranslator).expandWithAnimation(any())
+ assertFalse(acceptsNestedScroll)
+ }
+
+ @Test
+ fun `Behavior should not accept nested scrolls on the horizontal axis`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ behavior.dynamicScrollView = mock()
+ doReturn(true).`when`(behavior).shouldScroll
+
+ var acceptsNestedScroll = behavior.onStartNestedScroll(
+ coordinatorLayout = mock(),
+ child = mock(),
+ directTargetChild = mock(),
+ target = mock(),
+ axes = ViewCompat.SCROLL_AXIS_VERTICAL,
+ type = ViewCompat.TYPE_TOUCH,
+ )
+ assertTrue(acceptsNestedScroll)
+
+ acceptsNestedScroll = behavior.onStartNestedScroll(
+ coordinatorLayout = mock(),
+ child = mock(),
+ directTargetChild = mock(),
+ target = mock(),
+ axes = ViewCompat.SCROLL_AXIS_HORIZONTAL,
+ type = ViewCompat.TYPE_TOUCH,
+ )
+ assertFalse(acceptsNestedScroll)
+ }
+
+ @Test
+ fun `Behavior should delegate the onStartNestedScroll logic`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val view: View = mock()
+ behavior.dynamicScrollView = view
+ val inputType = ViewCompat.TYPE_TOUCH
+ val axes = ViewCompat.SCROLL_AXIS_VERTICAL
+
+ behavior.onStartNestedScroll(
+ coordinatorLayout = mock(),
+ child = view,
+ directTargetChild = mock(),
+ target = mock(),
+ axes = axes,
+ type = inputType,
+ )
+
+ verify(behavior).startNestedScroll(axes, inputType, view)
+ }
+
+ @Test
+ fun `onStopNestedScroll should attempt stopping nested scrolling only if browserToolbar is valid`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+
+ behavior.dynamicScrollView = null
+ behavior.onStopNestedScroll(
+ coordinatorLayout = mock(),
+ child = mock(),
+ target = mock(),
+ type = 0,
+ )
+ verify(behavior, never()).stopNestedScroll(anyInt(), any())
+
+ behavior.dynamicScrollView = mock()
+ behavior.onStopNestedScroll(
+ coordinatorLayout = mock(),
+ child = mock(),
+ target = mock(),
+ type = 0,
+ )
+ verify(behavior).stopNestedScroll(anyInt(), any())
+ }
+
+ @Test
+ fun `Behavior should delegate the onStopNestedScroll logic`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val inputType = ViewCompat.TYPE_TOUCH
+ val view: View = mock()
+
+ behavior.dynamicScrollView = null
+ behavior.onStopNestedScroll(
+ coordinatorLayout = mock(),
+ child = view,
+ target = mock(),
+ type = inputType,
+ )
+ verify(behavior, never()).stopNestedScroll(inputType, view)
+ }
+
+ @Test
+ fun `stopNestedScroll will snap toolbar up if toolbar is more than 50 percent visible`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ behavior.dynamicScrollView = mock()
+ doReturn(true).`when`(behavior).shouldScroll
+
+ val child = mock<View>()
+ doReturn(100).`when`(child).height
+ doReturn(10f).`when`(child).translationY
+
+ behavior.onStartNestedScroll(
+ coordinatorLayout = mock(),
+ child = child,
+ directTargetChild = mock(),
+ target = mock(),
+ axes = ViewCompat.SCROLL_AXIS_VERTICAL,
+ type = ViewCompat.TYPE_TOUCH,
+ )
+
+ assertTrue(behavior.shouldSnapAfterScroll)
+ verify(yTranslator).cancelInProgressTranslation()
+ verify(yTranslator, never()).expandWithAnimation(any())
+ verify(yTranslator, never()).collapseWithAnimation(any())
+
+ behavior.stopNestedScroll(0, child)
+
+ verify(yTranslator).snapWithAnimation(child)
+ }
+
+ @Test
+ fun `stopNestedScroll will snap toolbar down if toolbar is less than 50 percent visible`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ doReturn(true).`when`(behavior).shouldScroll
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+
+ val child = mock<View>()
+ behavior.dynamicScrollView = child
+ doReturn(100).`when`(child).height
+ doReturn(90f).`when`(child).translationY
+
+ behavior.onStartNestedScroll(
+ coordinatorLayout = mock(),
+ child = child,
+ directTargetChild = mock(),
+ target = mock(),
+ axes = ViewCompat.SCROLL_AXIS_VERTICAL,
+ type = ViewCompat.TYPE_TOUCH,
+ )
+
+ assertTrue(behavior.shouldSnapAfterScroll)
+ verify(yTranslator).cancelInProgressTranslation()
+ verify(yTranslator, never()).expandWithAnimation(any())
+ verify(yTranslator, never()).collapseWithAnimation(any())
+
+ behavior.stopNestedScroll(0, child)
+
+ verify(yTranslator).snapWithAnimation(child)
+ }
+
+ @Test
+ fun `onStopNestedScroll should snap the toolbar only if browserToolbar is valid`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ behavior.dynamicScrollView = null
+
+ behavior.onStopNestedScroll(
+ coordinatorLayout = mock(),
+ child = mock(),
+ target = mock(),
+ type = ViewCompat.TYPE_TOUCH,
+ )
+
+ verify(behavior, never()).stopNestedScroll(anyInt(), any())
+ }
+
+ @Test
+ fun `Behavior will intercept MotionEvents and pass them to the custom gesture detector`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val gestureDetector: BrowserGestureDetector = mock()
+ behavior.initGesturesDetector(gestureDetector)
+ behavior.dynamicScrollView = mock()
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+
+ behavior.onInterceptTouchEvent(mock(), mock(), downEvent)
+
+ verify(gestureDetector).handleTouchEvent(downEvent)
+ }
+
+ @Test
+ fun `Behavior should only dispatch MotionEvents to the gesture detector only if browserToolbar is valid`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val gestureDetector: BrowserGestureDetector = mock()
+ behavior.initGesturesDetector(gestureDetector)
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN)
+
+ behavior.onInterceptTouchEvent(mock(), mock(), downEvent)
+
+ verify(gestureDetector, never()).handleTouchEvent(downEvent)
+ }
+
+ @Test
+ fun `Behavior will apply translation to toolbar only for vertical scrolls`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ behavior.initGesturesDetector(behavior.createGestureDetector())
+ val child = spy(View(testContext, null, 0))
+ behavior.dynamicScrollView = child
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 100f, downEvent)
+
+ behavior.onInterceptTouchEvent(mock(), mock(), downEvent)
+ behavior.onInterceptTouchEvent(mock(), mock(), moveEvent)
+
+ verify(behavior).tryToScrollVertically(-100f)
+ }
+
+ @Test
+ fun `GIVEN a null InputResultDetail from the EngineView WHEN shouldScroll is called THEN it returns false`() {
+ val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)
+ behavior.engineView = null
+ assertFalse(behavior.shouldScroll)
+ behavior.engineView = mock()
+ `when`(behavior.engineView!!.getInputResultDetail()).thenReturn(null)
+
+ assertFalse(behavior.shouldScroll)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail with the right values and scroll enabled WHEN shouldScroll is called THEN it returns true`() {
+ val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)
+ val engineView: EngineView = mock()
+ behavior.engineView = engineView
+ behavior.isScrollEnabled = true
+ val validInputResultDetail: InputResultDetail = mock()
+ doReturn(validInputResultDetail).`when`(engineView).getInputResultDetail()
+
+ doReturn(true).`when`(validInputResultDetail).canScrollToBottom()
+ doReturn(false).`when`(validInputResultDetail).canScrollToTop()
+ assertTrue(behavior.shouldScroll)
+
+ doReturn(false).`when`(validInputResultDetail).canScrollToBottom()
+ doReturn(true).`when`(validInputResultDetail).canScrollToTop()
+ assertTrue(behavior.shouldScroll)
+
+ doReturn(true).`when`(validInputResultDetail).canScrollToBottom()
+ doReturn(true).`when`(validInputResultDetail).canScrollToTop()
+ assertTrue(behavior.shouldScroll)
+ }
+
+ @Test
+ fun `GIVEN an InputResultDetail with the right values but with scroll disabled WHEN shouldScroll is called THEN it returns false`() {
+ val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)
+ behavior.engineView = mock()
+ behavior.isScrollEnabled = false
+ val validInputResultDetail: InputResultDetail = mock()
+ doReturn(true).`when`(validInputResultDetail).canScrollToBottom()
+ doReturn(true).`when`(validInputResultDetail).canScrollToTop()
+
+ assertFalse(behavior.shouldScroll)
+ }
+
+ @Test
+ fun `GIVEN scroll enabled but EngineView cannot scroll to bottom WHEN shouldScroll is called THEN it returns false`() {
+ val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)
+ behavior.engineView = mock()
+ behavior.isScrollEnabled = true
+ val validInputResultDetail: InputResultDetail = mock()
+ doReturn(false).`when`(validInputResultDetail).canScrollToBottom()
+ doReturn(true).`when`(validInputResultDetail).canScrollToTop()
+
+ assertFalse(behavior.shouldScroll)
+ }
+
+ @Test
+ fun `GIVEN scroll enabled but EngineView cannot scroll to top WHEN shouldScroll is called THEN it returns false`() {
+ val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)
+ behavior.engineView = mock()
+ behavior.isScrollEnabled = true
+ val validInputResultDetail: InputResultDetail = mock()
+ doReturn(true).`when`(validInputResultDetail).canScrollToBottom()
+ doReturn(false).`when`(validInputResultDetail).canScrollToTop()
+
+ assertFalse(behavior.shouldScroll)
+ }
+
+ @Test
+ fun `Behavior will vertically scroll nested scroll started and EngineView handled the event`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ doReturn(true).`when`(behavior).shouldScroll
+ val child = spy(View(testContext, null, 0))
+ behavior.dynamicScrollView = child
+ doReturn(100).`when`(child).height
+ doReturn(0f).`when`(child).translationY
+ behavior.startedScroll = true
+
+ behavior.tryToScrollVertically(25f)
+
+ verify(yTranslator).translate(child, 25f)
+ }
+
+ @Test
+ fun `Behavior will not scroll vertically if startedScroll is false`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ doReturn(true).`when`(behavior).shouldScroll
+ val child = spy(View(testContext, null, 0))
+ behavior.dynamicScrollView = child
+ doReturn(100).`when`(child).height
+ doReturn(0f).`when`(child).translationY
+ behavior.startedScroll = false
+
+ behavior.tryToScrollVertically(25f)
+
+ verify(yTranslator, never()).translate(any(), anyFloat())
+ }
+
+ @Test
+ fun `Behavior will not scroll vertically if EngineView did not handled the event`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ doReturn(false).`when`(behavior).shouldScroll
+ val child = spy(View(testContext, null, 0))
+ behavior.dynamicScrollView = child
+ doReturn(100).`when`(child).height
+ doReturn(0f).`when`(child).translationY
+
+ behavior.tryToScrollVertically(25f)
+
+ verify(yTranslator, never()).translate(any(), anyFloat())
+ }
+
+ @Test
+ fun `forceExpand should delegate the translator`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ val view: View = mock()
+
+ behavior.forceExpand(view)
+
+ verify(yTranslator).expandWithAnimation(view)
+ }
+
+ @Test
+ fun `forceCollapse should delegate the translator`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ val view: View = mock()
+
+ behavior.forceCollapse(view)
+
+ verify(yTranslator).collapseWithAnimation(view)
+ }
+
+ @Test
+ fun `Behavior will forceExpand when scrolling up and !shouldScroll if the touch was handled in the browser`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ behavior.initGesturesDetector(behavior.createGestureDetector())
+ val view: View = spy(View(testContext, null, 0))
+ behavior.dynamicScrollView = view
+ val engineView: EngineView = mock()
+ behavior.engineView = engineView
+ val handledTouchInput = InputResultDetail.newInstance().copy(INPUT_UNHANDLED)
+ doReturn(handledTouchInput).`when`(engineView).getInputResultDetail()
+
+ doReturn(100).`when`(view).height
+ doReturn(100f).`when`(view).translationY
+
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 30f, downEvent)
+
+ behavior.onInterceptTouchEvent(mock(), mock(), downEvent)
+ behavior.onInterceptTouchEvent(mock(), mock(), moveEvent)
+
+ verify(behavior).tryToScrollVertically(-30f)
+ verify(yTranslator).forceExpandIfNotAlready(view, -30f)
+ }
+
+ @Test
+ fun `Behavior will not forceExpand when scrolling up and !shouldScroll if the touch was not yet handled in the browser`() {
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+ val yTranslator: ViewYTranslator = mock()
+ behavior.yTranslator = yTranslator
+ behavior.initGesturesDetector(behavior.createGestureDetector())
+ val view: View = spy(View(testContext, null, 0))
+ behavior.dynamicScrollView = view
+ val engineView: EngineView = mock()
+ behavior.engineView = engineView
+ val handledTouchInput = InputResultDetail.newInstance()
+ doReturn(handledTouchInput).`when`(engineView).getInputResultDetail()
+
+ doReturn(100).`when`(view).height
+ doReturn(100f).`when`(view).translationY
+
+ val downEvent = TestUtils.getMotionEvent(ACTION_DOWN, 0f, 0f)
+ val moveEvent = TestUtils.getMotionEvent(ACTION_MOVE, 0f, 30f, downEvent)
+
+ behavior.onInterceptTouchEvent(mock(), mock(), downEvent)
+ behavior.onInterceptTouchEvent(mock(), mock(), moveEvent)
+
+ verify(behavior).tryToScrollVertically(-30f)
+ verify(yTranslator, never()).forceExpandIfNotAlready(view, -30f)
+ }
+
+ @Test
+ fun `onLayoutChild initializes browserToolbar and engineView`() {
+ val view = View(testContext)
+ val engineView = createDummyEngineView(testContext).asView()
+ val container = CoordinatorLayout(testContext).apply {
+ addView(View(testContext))
+ addView(engineView)
+ }
+ val behavior = spy(EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM))
+
+ behavior.onLayoutChild(container, view, ViewCompat.LAYOUT_DIRECTION_LTR)
+
+ assertEquals(view, behavior.dynamicScrollView)
+ assertEquals(engineView, behavior.engineView)
+ }
+
+ @Test
+ fun `enableScrolling sets isScrollEnabled to true`() {
+ val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)
+
+ assertFalse(behavior.isScrollEnabled)
+ behavior.enableScrolling()
+
+ assertTrue(behavior.isScrollEnabled)
+ }
+
+ @Test
+ fun `disableScrolling sets isScrollEnabled to false`() {
+ val behavior = EngineViewScrollingBehavior(testContext, null, ViewPosition.BOTTOM)
+ behavior.isScrollEnabled = true
+
+ assertTrue(behavior.isScrollEnabled)
+ behavior.disableScrolling()
+
+ assertFalse(behavior.isScrollEnabled)
+ }
+
+ private fun createDummyEngineView(context: Context): EngineView = DummyEngineView(context)
+
+ open class DummyEngineView(context: Context) : FrameLayout(context), EngineView {
+ override fun setVerticalClipping(clippingHeight: Int) {}
+ override fun setDynamicToolbarMaxHeight(height: Int) {}
+ override fun setActivityContext(context: Context?) {}
+ override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) = Unit
+ override fun render(session: EngineSession) {}
+ override fun release() {}
+ override var selectionActionDelegate: SelectionActionDelegate? = null
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/TestUtils.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/TestUtils.kt
new file mode 100644
index 0000000000..2e5bbaa9df
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/TestUtils.kt
@@ -0,0 +1,62 @@
+/* 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.ui.widgets.behavior
+
+import android.view.MotionEvent
+
+object TestUtils {
+ fun getMotionEvent(
+ action: Int,
+ x: Float = 0f,
+ y: Float = 0f,
+ previousEvent: MotionEvent? = null,
+ ): MotionEvent {
+ val currentTime = System.currentTimeMillis()
+ val downTime = previousEvent?.downTime ?: System.currentTimeMillis()
+
+ var pointerCount = previousEvent?.pointerCount ?: 0
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ pointerCount++
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ pointerCount = 1
+ }
+
+ val properties = Array(pointerCount, TestUtils::getPointerProperties)
+ val pointerCoords = getPointerCoords(x, y, pointerCount)
+
+ return MotionEvent.obtain(
+ downTime, currentTime,
+ action, pointerCount, properties,
+ pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0,
+ )
+ }
+
+ private fun getPointerCoords(
+ x: Float,
+ y: Float,
+ pointerCount: Int,
+ previousEvent: MotionEvent? = null,
+ ): Array<MotionEvent.PointerCoords?> {
+ val currentEventCoords = MotionEvent.PointerCoords().apply {
+ this.x = x; this.y = y; pressure = 1f; size = 1f
+ }
+
+ return if (pointerCount > 1 && previousEvent != null) {
+ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ this.x = previousEvent.x; this.y = previousEvent.y; pressure = 1f; size = 1f
+ },
+ currentEventCoords,
+ )
+ } else {
+ arrayOf(currentEventCoords)
+ }
+ }
+
+ private fun getPointerProperties(id: Int): MotionEvent.PointerProperties =
+ MotionEvent.PointerProperties().apply {
+ this.id = id; this.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslationStrategyTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslationStrategyTest.kt
new file mode 100644
index 0000000000..62d152f7f6
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslationStrategyTest.kt
@@ -0,0 +1,712 @@
+/* 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.ui.widgets.behavior
+
+import android.animation.ValueAnimator
+import android.view.View
+import android.view.animation.DecelerateInterpolator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+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
+
+@RunWith(AndroidJUnit4::class)
+class ViewYTranslationStrategyTest {
+ @Test
+ fun `snapAnimator should use a DecelerateInterpolator with SNAP_ANIMATION_DURATION for bottom toolbar translations`() {
+ val strategy = BottomViewBehaviorStrategy()
+
+ assertTrue(strategy.animator.interpolator is DecelerateInterpolator)
+ assertEquals(SNAP_ANIMATION_DURATION, strategy.animator.duration)
+ }
+
+ @Test
+ fun `snapAnimator should use a DecelerateInterpolator with SNAP_ANIMATION_DURATION for top toolbar translations`() {
+ val strategy = TopViewBehaviorStrategy()
+
+ assertTrue(strategy.animator.interpolator is DecelerateInterpolator)
+ assertEquals(SNAP_ANIMATION_DURATION, strategy.animator.duration)
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy should start with isToolbarExpanding = false`() {
+ val strategy = BottomViewBehaviorStrategy()
+
+ assertFalse(strategy.wasLastExpanding)
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy should start with isToolbarExpanding = false`() {
+ val strategy = TopViewBehaviorStrategy()
+
+ assertFalse(strategy.wasLastExpanding)
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapWithAnimation should collapse toolbar if more than half hidden`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(50f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(51f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(100f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(333f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ verify(strategy, times(4)).collapseWithAnimation(view)
+ verify(strategy, never()).expandWithAnimation(view)
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapWithAnimation should expand toolbar if more than half visible`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(49f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(0f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(-50f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ verify(strategy, times(3)).expandWithAnimation(view)
+ verify(strategy, never()).collapseWithAnimation(view)
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapWithAnimation should collapse toolbar if more than half hidden`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(-51f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(-100f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(-333f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ verify(strategy, times(3)).collapseWithAnimation(view)
+ verify(strategy, never()).expandWithAnimation(view)
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapWithAnimation should expand toolbar if more than half visible`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(-50f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(-49f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(0f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ doReturn(50f).`when`(view).translationY
+ strategy.snapWithAnimation(view)
+
+ verify(strategy, times(4)).expandWithAnimation(view)
+ verify(strategy, never()).collapseWithAnimation(view)
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapImmediately should end translations animations if in progress`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(true).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+
+ strategy.snapImmediately(view)
+
+ verify(animator).end()
+ verify(view, never()).translationY
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapImmediately should translate away the toolbar if half translated`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(50f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 100f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapImmediately should translate away the toolbar if more than half`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(55f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 100f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapImmediately should translate away the toolbar if translated off screen`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(555f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 100f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapImmediately should translate to 0 the toolbar if translated less than half`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(49f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapImmediately should translate to 0 the toolbar if translated 0`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(0f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - snapImmediately should translate to 0 the toolbar if translated inside the screen`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(-1f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapImmediately should end translations animations if in progress`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(true).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+
+ strategy.snapImmediately(view)
+
+ verify(animator).end()
+ verify(view, never()).translationY
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapImmediately should translate translate to 0 the toolbar if translated less than half`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(-49f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapImmediately should translate to 0 the toolbar if translated 0`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(0f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapImmediately should translate to 0 the toolbar if translated inside the screen`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(1f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapImmediately should translate to 0 the toolbar if half translated`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(-50f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapImmediately should translate away the toolbar if more than half translated`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(-55f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = -100f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - snapImmediately should translate to 0 the toolbar if translated offscreen`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ doReturn(-111f).`when`(view).translationY
+ strategy.snapImmediately(view)
+ verify(view).translationY = -100f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - expandWithAnimation should translate the toolbar to to y 0`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val view: View = mock()
+
+ strategy.expandWithAnimation(view)
+
+ verify(strategy).animateToTranslationY(view, 0f)
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - expandWithAnimation should translate the toolbar to to y 0`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val view: View = mock()
+
+ strategy.expandWithAnimation(view)
+
+ verify(strategy).animateToTranslationY(view, 0f)
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - forceExpandWithAnimation should expand toolbar`() {
+ // Setting the scenario in which forceExpandWithAnimation will actually do what the name says.
+ // Below this test we can change each variable one at a time to test them in isolation.
+
+ val strategy = spy(BottomViewBehaviorStrategy())
+ strategy.wasLastExpanding = false
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100f).`when`(view).translationY
+
+ strategy.forceExpandWithAnimation(view, -100f)
+
+ verify(strategy.animator).cancel()
+ verify(strategy).expandWithAnimation(any())
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - forceExpandWithAnimation should not force expand the toolbar if not currently collapsing`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ strategy.wasLastExpanding = true
+ val animator: ValueAnimator = mock()
+ doReturn(true).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100f).`when`(view).translationY
+
+ strategy.forceExpandWithAnimation(view, -100f)
+
+ verify(strategy.animator, never()).cancel()
+ verify(strategy, never()).expandWithAnimation(any())
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - forceExpandWithAnimation should not expand if user swipes down`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ strategy.wasLastExpanding = false
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(100f).`when`(view).translationY
+
+ strategy.forceExpandWithAnimation(view, 100f)
+
+ verify(strategy.animator, never()).cancel()
+ verify(strategy, never()).expandWithAnimation(any())
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - forceExpandWithAnimation should not expand the toolbar if it is already expanded`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ strategy.wasLastExpanding = false
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(0f).`when`(view).translationY
+
+ strategy.forceExpandWithAnimation(view, -100f)
+
+ verify(strategy.animator, never()).cancel()
+ verify(strategy, never()).expandWithAnimation(any())
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - forceExpandWithAnimation should expand toolbar`() {
+ // Setting the scenario in which forceExpandWithAnimation will actually do what the name says.
+ // Below this test we can change each variable one at a time to test them in isolation.
+
+ val strategy = spy(TopViewBehaviorStrategy())
+ strategy.wasLastExpanding = false
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(-100f).`when`(view).translationY
+
+ strategy.forceExpandWithAnimation(view, -100f)
+
+ verify(strategy.animator).cancel()
+ verify(strategy).expandWithAnimation(any())
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - forceExpandWithAnimation should not force expand the toolbar if not currently collapsing`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ strategy.wasLastExpanding = true
+ val animator: ValueAnimator = mock()
+ doReturn(true).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(-100f).`when`(view).translationY
+
+ strategy.forceExpandWithAnimation(view, -100f)
+
+ verify(strategy.animator, never()).cancel()
+ verify(strategy, never()).expandWithAnimation(any())
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - forceExpandWithAnimation should not expand if user swipes up`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ strategy.wasLastExpanding = false
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(-100f).`when`(view).translationY
+
+ strategy.forceExpandWithAnimation(view, 10f)
+
+ verify(strategy.animator, never()).cancel()
+ verify(strategy, never()).expandWithAnimation(any())
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - forceExpandWithAnimation should not expand the toolbar if it is already expanded`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ strategy.wasLastExpanding = false
+ val animator: ValueAnimator = mock()
+ doReturn(false).`when`(animator).isStarted
+ strategy.animator = animator
+ val view: View = mock()
+ doReturn(0f).`when`(view).translationY
+
+ strategy.forceExpandWithAnimation(view, -100f)
+
+ verify(strategy.animator, never()).cancel()
+ verify(strategy, never()).expandWithAnimation(any())
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - collapseWithAnimation should animate translating the toolbar down, off-screen`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ strategy.collapseWithAnimation(view)
+
+ verify(strategy).animateToTranslationY(view, 100f)
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - collapseWithAnimation should animate translating the toolbar up, off-screen`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+
+ strategy.collapseWithAnimation(view)
+
+ verify(strategy).animateToTranslationY(view, -100f)
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - translate should translate up the toolbar with the distance parameter`() {
+ val strategy = BottomViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(50f).`when`(view).translationY
+
+ strategy.translate(view, -25f)
+
+ verify(view).translationY = 25f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - translate should translate down the toolbar with the distance parameter`() {
+ val strategy = BottomViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(50f).`when`(view).translationY
+
+ strategy.translate(view, 25f)
+
+ verify(view).translationY = 75f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - translate should not translate up the toolbar if already expanded`() {
+ val strategy = BottomViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(0f).`when`(view).translationY
+
+ strategy.translate(view, -1f)
+
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - translate should not translate up the toolbar more than to 0`() {
+ val strategy = BottomViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(50f).`when`(view).translationY
+
+ strategy.translate(view, -51f)
+
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - translate should not translate down the toolbar if already collapsed`() {
+ val strategy = BottomViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(100f).`when`(view).translationY
+
+ strategy.translate(view, 1f)
+
+ verify(view).translationY = 100f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - translate should not translate down the toolbar more than it's height`() {
+ val strategy = BottomViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(50f).`when`(view).translationY
+
+ strategy.translate(view, 51f)
+
+ verify(view).translationY = 100f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - translate should translate down the toolbar with the distance parameter`() {
+ val strategy = TopViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(-50f).`when`(view).translationY
+
+ strategy.translate(view, 25f)
+
+ verify(view).translationY = -75f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - translate should translate up the toolbar with the distance parameter`() {
+ val strategy = TopViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(-50f).`when`(view).translationY
+
+ strategy.translate(view, 25f)
+
+ verify(view).translationY = -75f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - translate should not translate down the toolbar if already expanded`() {
+ val strategy = TopViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(0f).`when`(view).translationY
+
+ strategy.translate(view, -1f)
+
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - translate should not translate down the toolbar more than to 0`() {
+ val strategy = TopViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(-50f).`when`(view).translationY
+
+ strategy.translate(view, -51f)
+
+ verify(view).translationY = 0f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - translate should not translate up the toolbar if already collapsed`() {
+ val strategy = TopViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(-100f).`when`(view).translationY
+
+ strategy.translate(view, 1f)
+
+ verify(view).translationY = -100f
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - translate should not translate up the toolbar more than it's height`() {
+ val strategy = TopViewBehaviorStrategy()
+ val view: View = mock()
+ doReturn(100).`when`(view).height
+ doReturn(-50f).`when`(view).translationY
+
+ strategy.translate(view, 51f)
+
+ verify(view).translationY = -100f
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - animateToTranslationY should set wasLastExpanding if expanding`() {
+ val strategy = BottomViewBehaviorStrategy()
+ strategy.wasLastExpanding = false
+ val view: View = mock()
+ doReturn(50f).`when`(view).translationY
+
+ strategy.animateToTranslationY(view, 10f)
+ assertTrue(strategy.wasLastExpanding)
+
+ strategy.animateToTranslationY(view, 60f)
+ assertFalse(strategy.wasLastExpanding)
+ }
+
+ @Test
+ fun `BottomToolbarBehaviorStrategy - animateToTranslationY should animate to the indicated y translation`() {
+ val strategy = spy(BottomViewBehaviorStrategy())
+ strategy.wasLastExpanding = false
+ val view = View(testContext)
+ val animator: ValueAnimator = spy(strategy.animator)
+ strategy.animator = animator
+
+ strategy.animateToTranslationY(view, 10f)
+
+ verify(animator).start()
+ animator.end()
+ assertEquals(10f, view.translationY)
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - animateToTranslationY should set wasLastExpanding if expanding`() {
+ val strategy = TopViewBehaviorStrategy()
+ strategy.wasLastExpanding = false
+ val view: View = mock()
+ doReturn(-50f).`when`(view).translationY
+
+ strategy.animateToTranslationY(view, -10f)
+ assertTrue(strategy.wasLastExpanding)
+
+ strategy.animateToTranslationY(view, -60f)
+ assertFalse(strategy.wasLastExpanding)
+ }
+
+ @Test
+ fun `TopToolbarBehaviorStrategy - animateToTranslationY should animate to the indicated y translation`() {
+ val strategy = spy(TopViewBehaviorStrategy())
+ strategy.wasLastExpanding = false
+ val view = View(testContext)
+ val animator: ValueAnimator = spy(strategy.animator)
+ strategy.animator = animator
+
+ strategy.animateToTranslationY(view, -10f)
+
+ verify(animator).start()
+ animator.end()
+ assertEquals(-10f, view.translationY)
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslatorTest.kt b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslatorTest.kt
new file mode 100644
index 0000000000..6a3a908adc
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/java/mozilla/components/ui/widgets/behavior/ViewYTranslatorTest.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.ui.widgets.behavior
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ViewYTranslatorTest {
+ @Test
+ fun `yTranslator should use BottomToolbarBehaviorStrategy for bottom placed toolbars`() {
+ val yTranslator = ViewYTranslator(ViewPosition.BOTTOM)
+
+ assertTrue(yTranslator.strategy is BottomViewBehaviorStrategy)
+ }
+
+ @Test
+ fun `yTranslator should use TopToolbarBehaviorStrategy for top placed toolbars`() {
+ val yTranslator = ViewYTranslator(ViewPosition.TOP)
+
+ assertTrue(yTranslator.strategy is TopViewBehaviorStrategy)
+ }
+
+ @Test
+ fun `yTranslator should delegate it's strategy for snapWithAnimation`() {
+ val yTranslator = ViewYTranslator(ViewPosition.BOTTOM)
+ val strategy: ViewYTranslationStrategy = mock()
+ yTranslator.strategy = strategy
+ val view: View = mock()
+
+ yTranslator.snapWithAnimation(view)
+
+ verify(strategy).snapWithAnimation(view)
+ }
+
+ @Test
+ fun `yTranslator should delegate it's strategy for expandWithAnimation`() {
+ val yTranslator = ViewYTranslator(ViewPosition.BOTTOM)
+ val strategy: ViewYTranslationStrategy = mock()
+ yTranslator.strategy = strategy
+ val view: View = mock()
+
+ yTranslator.expandWithAnimation(view)
+
+ verify(strategy).expandWithAnimation(view)
+ }
+
+ @Test
+ fun `yTranslator should delegate it's strategy for collapseWithAnimation`() {
+ val yTranslator = ViewYTranslator(ViewPosition.BOTTOM)
+ val strategy: ViewYTranslationStrategy = mock()
+ yTranslator.strategy = strategy
+ val view: View = mock()
+
+ yTranslator.collapseWithAnimation(view)
+
+ verify(strategy).collapseWithAnimation(view)
+ }
+
+ @Test
+ fun `yTranslator should delegate it's strategy for forceExpandIfNotAlready`() {
+ val yTranslator = ViewYTranslator(ViewPosition.BOTTOM)
+ val strategy: ViewYTranslationStrategy = mock()
+ yTranslator.strategy = strategy
+ val view: View = mock()
+
+ yTranslator.forceExpandIfNotAlready(view, 14f)
+
+ verify(strategy).forceExpandWithAnimation(view, 14f)
+ }
+
+ @Test
+ fun `yTranslator should delegate it's strategy for translate`() {
+ val yTranslator = ViewYTranslator(ViewPosition.BOTTOM)
+ val strategy: ViewYTranslationStrategy = mock()
+ yTranslator.strategy = strategy
+ val view: View = mock()
+
+ yTranslator.translate(view, 23f)
+
+ verify(strategy).translate(view, 23f)
+ }
+
+ @Test
+ fun `yTranslator should delegate it's strategy for cancelInProgressTranslation`() {
+ val yTranslator = ViewYTranslator(ViewPosition.BOTTOM)
+ val strategy: ViewYTranslationStrategy = mock()
+ yTranslator.strategy = strategy
+
+ yTranslator.cancelInProgressTranslation()
+
+ verify(strategy).cancelInProgressTranslation()
+ }
+
+ @Test
+ fun `yTranslator should delegate it's strategy for snapImmediately`() {
+ val yTranslator = ViewYTranslator(ViewPosition.BOTTOM)
+ val strategy: ViewYTranslationStrategy = mock()
+ yTranslator.strategy = strategy
+ val view: View = mock()
+
+ yTranslator.snapImmediately(view)
+
+ verify(strategy).snapImmediately(view)
+ }
+}
diff --git a/mobile/android/android-components/components/ui/widgets/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/ui/widgets/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/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/ui/widgets/src/test/resources/robolectric.properties b/mobile/android/android-components/components/ui/widgets/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/ui/widgets/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28